后台系统从零搭建(二)—— 系统架构设计4之CSSModule、主题、登录页

234 阅读4分钟

本系列从零搭建一个后台系统,技术选型React18 + ReactRouter7 + Vite4 + Antd5 + zustand + TS。 这个系列文章将会从零开始,一步一步搭建一个后台系统,这个系统将会包括登录、权限、菜单、用户、角色等功能。

已实现的项目地址,如果需要接口,还需要运行接口项目

后台系统从零搭建(一)—— 项目基础
后台系统从零搭建(二)—— 系统架构设计1之路由封装
后台系统从零搭建(二)—— 系统架构设计2之Axios请求封装
后台系统从零搭建(二)—— 系统架构设计3之环境变量封装
后台系统从零搭建(二)—— 系统架构设计4之CSSModule、主题、登录页
后台系统从零搭建(二)—— 系统架构设计5之公共布局Layout
后台系统从零搭建(二)—— 系统架构设计6之zustand状态管理
后台系统从零搭建(二)—— 系统架构设计7之菜单和路由的关联
后台系统从零搭建(三)—— 具体页面之用户管理(通用的增删改查逻辑和form-render)
后台系统从零搭建(三)—— 具体页面之菜单管理和角色管理
后台系统从零搭建(三)—— 具体页面之部门管理(抽离通用的增删改查逻辑)
后台系统从零搭建(四)—— 终结篇之权限系统怎么设计-RBAC模式

本文主要介绍系统架构设计之CSSModule、主题、登录页。

开启CSSModule

CSSModule是一种CSS模块化的解决方案,它可以让我们在不同的模块中使用相同的类名,而不会相互影响。这边将CSSModule集成到项目中,以便于我们在项目中使用。

React默认支持CSSModule,样式表只需要以.module.css/less/sass命名。为了方便使用嵌套样式,这边我们使用less作为样式表的解决方案,

安装依赖:

pnpm install less -D

Home组件为例,我们可以给Home组件的样式文件index.module.less添加样式:

// src/pages/Home/index.module.less
.page-home {
  .title {
    font-size: 24px;
    color: #333;
  }
  .content {
    font-size: 16px;
    color: #f69;
  }
  // 类名前加 :global 表示类名不会被 css module 处理
  :global {
    .ant-btn {
      background-color: #f69;
      color: #fff;
    }
  }
}

然后在Home组件中引入样式:

// src/views/Home/index.tsx
import React from 'react'
import style from './index.module.less'
import { Button } from 'antd'

type HomeProps = {}
const Home: React.FC<HomeProps> = () => {
  return (
    <div className={style['page-home']}>
      <h1 className={style.title}>Home</h1>
      <p className={style.content}>This is the home page.</p>
      <Button>按钮</Button>
    </div>
  )
}

Home.displayName = 'Home'
export default Home

css_module1.png,本质就是给类名添加了一个哈希值,这样就不会相互影响了。

注意类名必须一致,在Home组件中使用style['page-home']style.title来引用样式。

如果不希望类名被CSSModule处理,可以在类名前加:global

:global {
  .ant-btn {
    background-color: #f69;
    color: #fff;
  }
}

主题色设置

Antd提供了一种主题色的设置方式,官网说明

设置主题色

在App.tsx中增加主题色设置:

// src/App.tsx
import router from '@/router'
import { ConfigProvider } from 'antd'
import { RouterProvider } from 'react-router-dom'

function App() {
  return (
    <div className='App'>
      <ConfigProvider
        theme={{
          token: {
            colorPrimary: '#24be91',
          },
        }}
      >
        <RouterProvider router={router} />
      </ConfigProvider>
    </div>
  )
}
export default App

将App组件包裹在ConfigProvider中,设置theme属性,colorPrimary为主题色。

将Home组件中的Button样式去掉,设置type属性为primary,这样就会使用主题色。

如果想特定修改组件变量,如下设置

theme={{
  components: {
    Button: {
      colorPrimary: '#00b96b',
    },
    Input: {
      colorPrimary: '#eb2f96',
    }
  },
}}

如果用当前主题下的一些样式值,可以使用useToken来获取,用Home示例下

// src/views/Home/index.tsx
import React from 'react'
import style from './index.module.less'
import { Button, theme } from 'antd'

const { useToken } = theme

type HomeProps = {}
const Home: React.FC<HomeProps> = () => {
  const { token } = useToken()
  return (
    <div className={style['page-home']}>
      <h1 className={style.title}>Home</h1>
      <p
        style={{
          backgroundColor: token.colorPrimaryBg,
          padding: token.padding,
          borderRadius: token.borderRadius,
          color: token.colorPrimaryText,
          fontSize: token.fontSize,
        }}
        className={style.content}
      >
        This is the home page.
      </p>
      <Button type='primary'>按钮</Button>
    </div>
  )
}

Home.displayName = 'Home'
export default Home

css_module2.png

设置中文

在App.tsx中设置中文

// src/App.tsx
import { ConfigProvider } from 'antd'
import { RouterProvider } from 'react-router-dom'
import zhCN from 'antd/lib/locale/zh_CN'

function App() {
  return (
    <div className='App'>
      <ConfigProvider
        locale={zhCN}
        theme={{
          token: {
            colorPrimary: '#24be91',
          },
        }}
      >
        <RouterProvider router={router} />
      </ConfigProvider>
    </div>
  )
}
export default App

登录

登录页

直接使用AntdForm

// src/views/Login/index.tsx

import React from 'react'
import type { FormProps } from 'antd'
import { Button, Form, Input } from 'antd'

type FieldType = {
  username?: string
  password?: string
}

const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
  console.log('Success:', values)
}

const onFinishFailed: FormProps<FieldType>['onFinishFailed'] = (errorInfo) => {
  console.log('Failed:', errorInfo)
}

const Login: React.FC = () => (
  <Form
    name='basic'
    labelAlign='right'
    labelCol={{ span: 8 }}
    wrapperCol={{ span: 16 }}
    style={{ maxWidth: 600, marginTop: 50 }}
    onFinish={onFinish}
    onFinishFailed={onFinishFailed}
    autoComplete='off'
  >
    <Form.Item<FieldType> label='用户名' name='username' rules={[{ required: true, message: '请输入用户名!' }]}>
      <Input />
    </Form.Item>

    <Form.Item<FieldType> label='密码' name='password' rules={[{ required: true, message: '请输入密码' }]}>
      <Input.Password />
    </Form.Item>

    <Form.Item label={null}>
      <Button type='primary' htmlType='submit'>
        登陆
      </Button>
    </Form.Item>
  </Form>
)

export default Login

login_1.png

登录接口

在Login/service.ts中定义登录接口

import request from '@/utils/request'

export const apiLogin = (data: { username?: string; password?: string }): Promise<string> => {
  return request.post('/api/login', data).then((res) => res.data.token)
}
export default { apiLogin }

在页面上使用

import React from 'react'
import type { FormProps } from 'antd'
import { Button, Form, Input, message } from 'antd'
import api from './service'
import { useNavigate } from 'react-router-dom'

type FieldType = {
  username?: string
  password?: string
}

const Login: React.FC = () => {
  const navigate = useNavigate()
  const onFinish: FormProps<FieldType>['onFinish'] = async (values) => {
    const token = await api.apiLogin(values)
    // 登陆成功后将 token 存储到 localStorage
    localStorage.setItem('token', token)
    // 提示登陆成功
    message.success('登陆成功')
    // callback 用于登陆成功后跳转到之前的页面 全网址
    const callback = new URLSearchParams(window.location.search).get('callback')
    // 替换当前路由 去首页 这样用户就不能回到登陆页
    callback ? (location.href = callback) : navigate('/')
  }
  // 没动。。。。
}

接口我用另一个项目,用的express框架,这边就不贴代码了。回头整个集中贴一下。