(超详细带注释)!!从0到1开发文章后台管理系统-包含前端和后端(前端篇)

321 阅读18分钟

说明

该项目是一个基础的 React 后台管理系统,后端使用NodeJS为服务器,功能简洁明了,涵盖了用户登录、注册,退出以及数据的增删改查等操作,非常适合初学者学习参考。

项目演示

{F26407CD-D684-49AA-922C-A0A1979DA05B}.png

{BAFE46C4-945D-49C6-A4DA-83B64A831E2C}.png

{3E11796B-7D70-402F-BB4B-4589375ACFED}.png

1.项目搭建


开发工具:VsCode

1.1 项目技术栈

  • Vite6
  • AntDesign5
  • Axios1.9
  • react-router7
  • react-redux9
  • Scss0.2
  • Node22.15.0
  • Npm10.9.2

1.2 新建React项目

	npm install -g create-vite //全局安装vite

	create-vite react-article --template react //使用vite创建项目

	cd react-article //进入项目目录

	npm install //安装依赖

	npm run dev //运行开发服务器

1.3 项目目录

  1. 在 src 文件夹下,我们先删除多余的文件,只保留 App.js、index.js、index.css。
  2. 删除 index.js 文件里面 React.StrictMode函数

{186E6DB5-89A8-4597-84CE-411D675CEE27}.png 3. 按照以下新建文件夹

├── node_modules            # 项目依赖的第三方库(自动生成)
├── public                  # 静态资源目录(直接复制到构建输出)
│
├── src                     # 项目核心源代码目录
│   ├── apis                # 后端API请求封装(如axios配置)
│   ├── assets              # 静态资源(图片、字体、CSS等)
│   ├── components          # 可复用UI组件
│   ├── pages               # 页面级组件(按路由划分)
	│   ├── ArticleManage       # 文章管理模块
	│   ├── Home                # 首页模块
	│   ├── Layout              # 全局布局组件
	│   ├── Login               # 登录页模块
	│   ├── NotFound            # 404页面模块
│   ├── router              # 路由配置(React Router)
│   ├── store               # 状态管理(Redux)
│   ├── utils               # 工具函数库
│   ├── App.jsx             # 项目根组件(主入口组件)
│   └── main.jsx            # 应用入口文件(渲染根组件)
│
├── .babelrc                # Babel配置文件(语法转换规则)
├── .editorconfig           # 统一编辑器格式配置(缩进/编码等)
├── .eslintrc.js            # ESLint静态检查配置
├── .gitignore              # Git忽略规则(排除node_modules等)
├── .postcssrc.js           # PostCSS插件配置(CSS预处理)
├── .prettierrc.cjs         # Prettier代码格式化配置
├── vite.config.js          # Vite构建工具配置(代理/插件等)
│
├── index.html              # 前端主HTML文件(Vite入口)
├── package.json            # 项目依赖和脚本命令定义
├── package-lock.json       # 依赖版本锁定文件
└── README.md               # 项目说明文档

1.4 安装AntDesign

  1. 官网:
	https://ant-design.antgroup.com/docs/spec/introduce-cn

2. 安装Antd

	npm install antd --save

3. 全局配置国际化文案:中文

	//在main.jsx里导入
	import zhCN from 'antd/locale/zh_CN'
	import { ConfigProvider } from 'antd'

	

1.5 安装react-router

  1. 安装
	npm install react-router-dom

2. 在router文件夹下新建index.jsx ==lazy表示懒加载页面组件==

	import { createBrowserRouter } from 'react-router'
	import { lazy } from 'react'
	import LayoutApp from '../pages/Layout'
	import { AuthRoute } from '@/components/AuthRoute'
	
	// 使用懒加载导入组件
	const Home = lazy(() => import('@/pages/Home'))
	const Article = lazy(() => import('@/pages/ArticleManage/Article'))
	const Artcate = lazy(() => import('@/pages/ArticleManage/Artcate'))
	const UserInfo = lazy(() => import('@/pages/Layout/component/UserInfo'))
	const UpdatePwd = lazy(() => import('@/pages/Layout/component/UpdatePwd'))
	const Login = lazy(() => import('@/pages/Login'))
	const NotFound = lazy(() => import('@/pages/NotFound/404'))
	
	const router = createBrowserRouter([
	  {
	    path: '/',
	    element: (
	      <AuthRoute> {/* 路由鉴权,保护需要登录才能访问的页面 */}
	        <LayoutApp /> {/* 主布局组件 */}
	      </AuthRoute>
	    ),
	    children: [
	      {
	        path: '/', //首页
	        element: <Home />,
	        handle: { meta: { title: '首页' } }
	      },
	      {
	        path: '/article/articles', // 文章管理页面
	        element: <Article />,
	        handle: { meta: { title: '文章管理' } }
	      },
	      {
	        path: '/article/artcate', // 文章分类页面
	        element: <Artcate />,
	        handle: { meta: { title: '文章分类' } }
	      },
	      {
	        path: '/userInfo', // 用户信息页面
	        element: <UserInfo />,
	        handle: { meta: { title: '用户信息' } }
	      },
	      {
	        path: '/updatePwd', // 修改密码页面
	        element: <UpdatePwd />,
	        handle: { meta: { title: '修改密码' } }
	      }
	    ]
	  },
	  {
	    path: '/login', // 登录页面(不需要鉴权)
	    element: <Login />
	  },
	  {
	    path: '*', // 404 未找到页面(兜底路由)
	    element: <NotFound />
	  }
	])
	
	export default router

  1. 在项目的 main.jsx 里面配置路由 {0C06D2FA-B115-4A9D-91C7-444A613FEE15}.png

1.6 安装Redux和Redux Toolkit

	npm install react-redux
	
	npm install @reduxjs/toolkit
  1. 在store目录下新建index.js {F3A32643-874B-4FCD-8DE2-A5F7F9B64A7F}.png
  2. 在main.jsx里配置Redux {C5501D1C-8941-429B-A9B7-3E105E306CFC}.png

1.7 安装scss和normalize.css

  1. 安装scss预处理器
	npm install sass --save-dev

2. 安装normalize.css重置样式

	npm install normalize.css

3. 在main.jsx里导入normalize.css

	 import 'normalize.css'

1.8 安装配置 axios

npm i axios -- save
  1. 在untils里新建request.js
	import axios from 'axios'
	import { message } from 'antd'
	import { getToken } from '@/utils'
	const request = axios.create({
	  baseURL: 'http://127.0.0.1',
	  timeout: 5000
	})
	// 请求拦截器
	request.interceptors.request.use(
	  config => {
	    // 注入token验证
	    const token = getToken()
	    if (token) {
	      config.headers.Authorization = 'Bearer ' + token
	    }
	    return config
	  },
	  error => {
	    return Promise.reject(error)
	  }
	)
	// 添加响应拦截器
	request.interceptors.response.use(
	  response => {
	    if (response.data.status === 1) {
	      message.error(response.data.message)
	    }
	    return response.data
	  },
	  error => {
	    return Promise.reject(error)
	  }
	)
	export default request

2. 在utils里新建token.js

	// 封装token方法
	// 存
	export const setToken = token => {
	  sessionStorage.setItem('token', token)
	}
	// 取
	export const getToken = () => {
	  return sessionStorage.getItem('token')
	}
	// 删
	export const removeToken = () => {
	  sessionStorage.removeItem('token')
	}

3. 在apis里新建user.js和article.js

    // user.js

	import { request } from '@/utils'
	// 用户登录
	export const loginApi = formData => {
	  return request({
	    url: '/api/login',
	    method: 'POST',
	    data: formData
	  })
	}
	
	// 用户注册
	export const regUserApi = formData => {
	  return request({
	    url: '/api/regUser',
	    method: 'POST',
	    data: formData
	  })
	}
	
	// 用户个人信息
	export const reqUserInfo = () => {
	  return request({
	    url: '/my/userInfo',
	    method: 'GET'
	  })
	}
	
	// 更新个人信息
	export const reqUpdateUserInfo = formData => {
	  return request({
	    url: '/my/userInfo',
	    method: 'POST',
	    data: formData
	  })
	}
	
	// 修改用户密码
	export const reqUpdatePwd = (formData) => {
	  return request({
	    url: '/my/updatePwd',
	    method: 'POST',
	    data: formData
	  })
	}
	
	// 上传头像图片接口
	export const reqUpdate = (formData) => {
	  return request({
	    url: '/my/upload',
	    method: 'POST',
	    data: formData
	  })
	}
  //   article.js
  
	import { request } from '@/utils'
	
	// 获取分类列表
	export const reqArtcate = () => {
	  return request({
	    url: '/article/artcate',
	    method: 'get',
	  })
	}
	
	// 查询文章分类
	export const reqArtcateList = params => {
	  return request({
	    url: '/article/artcateList',
	    method: 'get',
	    params
	  })
	}
	
	// 新增文章分类
	export const reqAddArtcate = formData => {
	  return request({
	    url: '/article/artcate',
	    method: 'post',
	    data: formData
	  })
	}
	
	// 编辑文章分类
	export const reqEditArtcate = formData => {
	  return request({
	    url: '/article/artcate',
	    method: 'put',
	    data: formData
	  })
	}
	
	// 获取文章列表
	export const reqArticles = params => {
	  return request({
	    url: '/article/articles',
	    method: 'get',
	    params
	  })
	}
	
	// 新增文章列表
	export const reqAddArticles = formData => {
	  return request({
	    url: '/article/articles',
	    method: 'post',
	    data: formData
	  })
	}
	
	
	// 删除文章列表一项
	export const reqDeleteArticles = id => {
	  return request({
	    url: '/article/articles',
	    method: 'DELETE',
	    data: id
	  })
	}
	
	
	// 更新文章列表一项
	export const reqUpdateArticles = (formData) => {
	  return request({
	    url: '/article/articles',
	    method: 'PUT',
	    data: formData
	  })
	}

2. 业务组件

2.1 用户登录和注册

  1. 在pages下新建Login/index.jsx
	import './index.scss'
	import { Card, Button, Form, Input, Flex, message, Typography } from 'antd'
	import { LockOutlined, UserOutlined, MailOutlined, SmileOutlined } from '@ant-design/icons'
	import { useState } from 'react'
	import { useNavigate } from 'react-router'
	import { useDispatch } from 'react-redux' // 导入react-redux的dispatch钩子
	import { fetchLogin } from '@/store/modules/user' // 导入用户相关的Redux action
	import { regUserApi } from '@/apis/user' // 导入用户注册的API函数
	
	// 从Typography解构出Title和Text组件
	const { Title, Text } = Typography
	
	function Login() {
	  // 状态管理:控制显示登录(true)还是注册(false)表单
	  const [showLogin, setShowLogin] = useState(true)
	  // 获取路由导航函数
	  const navigate = useNavigate()
	  // 获取Redux的dispatch方法
	  const dispatch = useDispatch()
	
	  /**
	   * 处理登录表单提交
	   * @param {Object} values - 表单数据 {username, password}
	   */
	  const onLoginFinish = async values => {
	    // 触发登录的异步action
	    const res = await dispatch(fetchLogin(values))
	    // 如果登录成功(status为0)
	    if (res.status === 0) {
	      // 跳转到首页
	      navigate('/')
	      // 显示成功提示
	      message.success('登录成功')
	    }
	  }
	
	  /**
	   * 处理注册表单提交
	   * @param {Object} values - 表单数据 {username, password, nickname, email}
	   */
	  const onRegUserFinish = async values => {
	    // 调用注册API
	    const res = await regUserApi(values)
	    // 如果注册成功(status为0)
	    if (res.status == 0) {
	      // 显示成功提示
	      message.success('注册成功!')
	      // 自动切换到登录界面
	      setShowLogin(true)
	    }
	  }
	
	  return (
	    <div className='login-container'>
	      <div className='login-card-wrapper'>
	        {/* 登录卡片 - 当showLogin为true时显示 */}
	        {showLogin && (
	          <Card className='auth-card'>
	            <div className='auth-header'>
	              {/* 标题 */}
	              <Title level={3} className='auth-title'>
	                文章后台管理系统
	              </Title>
	              {/* 副标题 */}
	              <Text type='secondary'>高效管理您的文章内容</Text>
	            </div>
	
	            {/* 登录表单 */}
	            <Form name='login-form' layout='vertical' onFinish={onLoginFinish}  autoComplete='off'>
	              {/* 账号输入框 */}
	              <Form.Item name='username' rules={[{ required: true, message: '请输入您的账号!' }]}>
	                <Input prefix={<UserOutlined className='input-icon' />} placeholder='请输入账号' size='large' />
	              </Form.Item>
	
	              {/* 密码输入框 */}
	              <Form.Item name='password' rules={[{ required: true, message: '请输入您的密码!' }]}>
	                <Input.Password
	                  prefix={<LockOutlined className='input-icon' />}
	                  placeholder='请输入密码'
	                  size='large'
	                />
	              </Form.Item>
	
	              <Form.Item>
	                <Flex justify='space-between' align='center'>
	                  {/* 切换到注册表单的链接 */}
	                  <Button type='link' onClick={() => setShowLogin(false)} className='switch-btn'>
	                    注册新账号
	                  </Button>
	                </Flex>
	              </Form.Item>
	
	              {/* 登录提交按钮 */}
	              <Form.Item>
	                <Button type='primary' htmlType='submit' size='large' block className='submit-btn'>
	                  登录
	                </Button>
	              </Form.Item>
	            </Form>
	          </Card>
	        )}
	
	        {/* 注册卡片 - 当showLogin为false时显示 */}
	        {!showLogin && (
	          <Card className='auth-card'>
	            <div className='auth-header'>
	              {/* 标题 */}
	              <Title level={3} className='auth-title'>
	                用户注册
	              </Title>
	              {/* 副标题 */}
	              <Text type='secondary'>加入文章管理系统</Text>
	            </div>
	
	            {/* 注册表单 */}
	            <Form name='register-form' layout='vertical' onFinish={onRegUserFinish} autoComplete='off'>
	              {/* 账号输入框 */}
	              <Form.Item label='账号' name='username' rules={[{ required: true, message: '请输入账号!' }]}>
	                <Input prefix={<UserOutlined className='input-icon' />} size='large' />
	              </Form.Item>
	
	              {/* 密码输入框 */}
	              <Form.Item label='密码' name='password' rules={[{ required: true, message: '请输入密码!' }]}>
	                <Input.Password prefix={<LockOutlined className='input-icon' />} size='large' />
	              </Form.Item>
	
	              {/* 姓名输入框 */}
	              <Form.Item label='姓名' name='nickname' rules={[{ required: true, message: '请输入姓名!' }]}>
	                <Input prefix={<SmileOutlined className='input-icon' />} size='large' />
	              </Form.Item>
	
	              {/* 邮箱输入框 */}
	              <Form.Item
	                label='邮箱'
	                name='email'
	                rules={[
	                  { required: true, message: '请输入邮箱!' },
	                  { type: 'email', message: '请输入正确邮箱!' }
	                ]}
              >	
	                <Input prefix={<MailOutlined className='input-icon' />} size='large' />
	              </Form.Item>
	
	              {/* 注册提交按钮 */}
	              <Form.Item>
	                <Button type='primary' htmlType='submit' size='large' block className='submit-btn'>
	                  注册
	                </Button>
	              </Form.Item>
	
	              <div className='text-center'>
	                {/* 切换到登录表单的链接 */}
	                <Button type='link' onClick={() => setShowLogin(true)} className='switch-btn'>
	                  已有账号?立即登录
	                </Button>
	              </div>
	            </Form>
	          </Card>
	        )}
	      </div>
	    </div>
	  )
	}
	
	export default Login

  1. 新建index.scss样式
	.login-container {
	  display: flex;
	  justify-content: center;
	  align-items: center;
	  min-height: 100vh;
	  background: #f0f2f5;
	  background-image: url('../../assets/login_nback.png');
	  padding: 20px;
	  box-sizing: border-box;
	  .login-card-wrapper {
	    width: 100%;
	    max-width: 500px;
	  }
	
	  .auth-card {
	    border-radius: 8px;
	    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
	    padding: 12px;
	
	    .auth-header {
	      text-align: center;
	      margin-bottom: 24px;
	
	      .auth-title {
	        margin-bottom: 8px;
	        color: #1890ff;
	      }
	    }
	
	    .input-icon {
	      color: rgba(0, 0, 0, 0.25);
	    }
	
	    .submit-btn {
	      margin-top: 12px;
	      height: 42px;
	      font-weight: 500;
	    }
	
	    .switch-btn {
	      padding: 0;
	      color: #1890ff;
	    }
	
	    .text-center {
	      text-align: center;
	    }
	  }
	}

  1. 在store里新建modules/user.js,使用了 redux 发起网络请求,进行状态管理
	import { createSlice } from '@reduxjs/toolkit'
	import { loginApi } from '@/apis/user'
	import { setToken as _setToken, removeToken as _removeToken } from '@/utils'
	// 用户相关的状态管理
	const userStore = createSlice({
	  name: 'user',
	  // 数据状态
	  initialState: {
	    token: sessionStorage.getItem('token') || ''
	  },
	  reducers: {
	    setToken(state, action) {
	      state.token = action.payload
	      _setToken(action.payload)
	    },
	    removeToken(state, action) {
	      state.token = ''
	      _removeToken()
	    }
	  }
	})
	// 登录
	const fetchLogin = loginForm => {
	  return async dispatch => {
	    const res = await loginApi(loginForm)
	    if (res.status === 0) {
	      dispatch(setToken(res.token))
	    }
	    return res
	  }
	}
	// 退出登录
	const logout = () => {
	  return dispatch => {
	    dispatch(removeToken())
	  }
	}
	const { setToken, removeToken } = userStore.actions
	const userReducer = userStore.reducer
	export { setToken, fetchLogin, removeToken, logout }
	export default userReducer

2.2 404页面

  1. 在pages新建NotFound/404.jsx和404.scss
	// NotFound.js
	import { useNavigate } from 'react-router';
	import './404.scss';
	const NotFound = () => {
	  const navigate = useNavigate()
	  const handleGoHome = () => {
	    navigate('/')
	  }
	  return (
	    <div className='not-found-container'>
	      <div className='not-found-content'>
	        <h1 className='not-found-title'>404</h1>
	        <h2 className='not-found-subtitle'>页面未找到</h2>
	        <p className='not-found-text'>抱歉,您访问的页面不存在或已被移除。</p>
	        <button onClick={handleGoHome} className='not-found-button'>
	          返回首页
	        </button>
	      </div>
	    </div>
	  )
	}
	export default NotFound
// 404.scss

.not-found-container {
	  display: flex;
	  justify-content: center;
	  align-items: center;
	  height: 100vh;
	  background-color: #f8f9fa;
	  text-align: center;
	}
	
	.not-found-content {
	  max-width: 600px;
	}
	
	.not-found-title {
	  font-size: 120px;
	  font-weight: 700;
	  color: #343a40;
	  margin: 0;
	  line-height: 1;
	}
	
	.not-found-subtitle {
	  font-size: 32px;
	  font-weight: 600;
	  color: #495057;
	  margin: 20px 0 10px;
	}
	
	.not-found-text {
	  font-size: 18px;
	  color: #6c757d;
	  margin-bottom: 30px;
	}
	
	.not-found-button {
	  padding: 12px 30px;
	  font-size: 16px;
	  font-weight: 500;
	  color: white;
	  background-color: #007bff;
	  border: none;
	  border-radius: 5px;
	  cursor: pointer;
	  transition: background-color 0.3s;
	}
	
	.not-found-button:hover {
	  background-color: #0056b3;
	}
	
    // 一定要在router/index.js配置

	  {
	    path: '*', // 404 未找到页面(兜底路由)
	    element: <NotFound />
	  }

2.3 Layout

  1. 在pages里新建Layout文件夹
   index.jsx
	import { useEffect, useState } from 'react'
	import { Layout } from 'antd'
	const { Header, Content, Sider } = Layout
	import BreadcrumbNav from './component/BreadcrumbNav'
	import UserView from './component/UserView'
	import './index.scss'
	import { reqUserInfo } from '@/apis/user'
	import { Outlet } from 'react-router'
	import LeftMenu from './component/LeftMenu'
	const LayoutApp = () => {
	  // 用户信息状态
	  const [userInfo, setUserInfo] = useState({})
	
	  // 获取用户信息
	  useEffect(() => {
	    const getUserInfo = async () => {
	      try {
	        const res = await reqUserInfo()
	        if (res.status === 0) {
	          setUserInfo(res.data)
	          sessionStorage.setItem('userInfo', JSON.stringify(res.data))
	        }
	      } catch (error) {
	        console.error('获取用户信息失败:', error)
	      }
	    }
	    getUserInfo()
	  }, [])
	
	  return (
	    <div className='app-layout-container'>
	      <Layout>
	        {/* 侧边栏 */}
	        <Sider
	          breakpoint='lg'
	          collapsedWidth='0'
	          width={220} // 明确设置宽度
        >	
	          <LeftMenu />
	        </Sider>
	
	        <Layout>
	          {/* 顶部导航栏 */}
	          <Header className='app-header'>
	            <BreadcrumbNav />
	            <UserView userInfo={userInfo} />
	          </Header>
	
	          {/* 内容区域 */}
	          <Content className='app-content'>
	            <div className='content-container'>
	              <Outlet /> {/* 子路由出口 */}
	            </div>
	          </Content>
	        </Layout>
	      </Layout>
	    </div>
	  )
	}
	export default LayoutApp

2. Layout样式

index.scss
	.tooltip_box {
	  padding: 0;
	  margin: 0;
	  list-style: none;
	  li {
	    color: #000;
	    padding: 10px 20px 10px 10px;
	    cursor: pointer;
	    span {
	      margin-left: 5px;
	    }
	  }
	  li:hover {
	    background-color: #eef2ff;
	    border-radius: 10px;
	  }
	}
	.app-layout-container {
	  display: flex;
	  flex-direction: column;
	  min-height: 100vh;
	
	  .app-header {
	    padding: 0 40px;
	    background: #fff;
	    display: flex;
	    justify-content: space-between;
	    align-items: center;
	  }
	
	  .app-content {
	    margin: 24px 16px;
	
	    .content-container {
	      background: #f5f5f5;
	      border-radius: 8px;
	      min-height: calc(100vh - 112px); // 确保内容区域足够高
	      overflow: hidden;
	    }
	  }
	}
	
	.user-info-container {
	  display: flex;
	  justify-content: center;
	  padding: 24px;
	}
	
	.user-info-card {
	  width: 800px;
	  border-radius: 8px;
	  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
	}
	
	.info-title {
	  text-align: center;
	  margin-bottom: 24px;
	}
	
	.avatar-preview {
	  text-align: center;
	  margin-bottom: 24px;
	}
	
	.avatar-image {
	  margin-bottom: 16px;
	}
	
	.submit-button {
	  width: 100%;
	}

3. 在Layout里新建component文件夹(将Layout的功能抽离成各个组件)

  • 面包屑-BreadcrumbNav.js
        // 头部面包屑
    	import { Breadcrumb } from 'antd'
    	import { RouteMeta } from '@/components/RouteMeta'
    	import { useEffect, useState } from 'react'
    	function _Breadcrumb() {
    	  // 获取当前路由的 meta 数据
    	  const meta = RouteMeta()
    	  const [item, setItem] = useState([])
    	  // 动态设置页面标题
    	  useEffect(() => {
    	    if (meta.title) {
    	      setItem([
    	        {
    	          title: meta.title
    	        }
    	      ])
    	    }
    	  }, [meta.title])
    	  return (
    	    <>
    	      <Breadcrumb items={item} />
    	    </>
    	  )
    	}
    	
    	export default _Breadcrumb
  • 左侧导航栏-LeftMenu.jsx
	import { Menu } from 'antd'
	import { useNavigate, useLocation } from 'react-router'
	import { HomeOutlined, UserOutlined } from '@ant-design/icons'
	function LeftMenu() {
	  const navigate = useNavigate()
	  const location = useLocation()
	  // 菜单配置项
	  const menuItems = [
	    {
	      label: '首页',
	      key: '/',
	      icon: <HomeOutlined />
	    },
	    {
	      label: '文章管理',
	      icon: <UserOutlined />,
	      children: [
	        { key: '/article/articles', label: '文章列表' },
	        { key: '/article/artcate', label: '文章分类' }
	      ]
	    }
	  ]
	  // 处理菜单点击事件
	  const handleMenuClick = ({ key }) => {
	    navigate(key)
	  }
	  // 获取当前选中的菜单项
	  const selectedKeys = [location.pathname]
	
	  return <Menu theme='dark' selectedKeys={selectedKeys} onClick={handleMenuClick} mode='inline' items={menuItems} />
	}
	export default LeftMenu
  • 更新密码-UpdatePwd.jsx
	import { Card, Button, Form, Input, message } from 'antd'
	import { reqUpdatePwd } from '@/apis/user'
	function UpdatePwd() {
	  /**
	   * 处理密码更新提交
	   * @param {Object} values - 表单值,包含 oldPwd 和 newPwd
	   */
	  const handleUpdatePassword = async values => {
	    try {
	      const res = await reqUpdatePwd(values)
	      if (res.status === 0) {
	        message.success(res.message)
	      }
	    } catch (error) {
	      message.error('密码更新失败,请重试')
	    }
	  }
	  return (
	    <div
	      style={{
	        display: 'flex',
	        justifyContent: 'center',
	        alignItems: 'center',
	        padding: '24px' // 添加内边距
	      }}
    >	
	      <Card
	        style={{
	          width: '100%',
	          maxWidth: 800,
	          boxShadow: '0 2px 12px 0 rgba(0, 0, 0, 0.1)' // 添加阴影效果
	        }}
	        title={<span style={{ fontSize: '20px', fontWeight: 'bold' }}>修改密码</span>}
      >	
	        <Form labelCol={{ span: 6 }} wrapperCol={{ span: 16 }} onFinish={handleUpdatePassword} autoComplete='off'>
	          <Form.Item
	            label='旧密码:'
	            name='oldPwd'
	            hasFeedback
	            rules={[
	              { required: true, message: '请输入旧密码!' },
	              { min: 6, message: '密码长度不能少于6个字符' }
	            ]}
          >	
	            <Input.Password placeholder='请输入当前密码' />
	          </Form.Item>
	
	          <Form.Item
	            label='新密码:'
	            name='newPwd'
	            hasFeedback
	            rules={[
	              { required: true, message: '请输入新密码!' },
	              { min: 6, message: '密码长度不能少于6个字符' },
	              ({ getFieldValue }) => ({
	                validator(_, value) {
	                  if (!value || getFieldValue('oldPwd') !== value) {
	                    return Promise.resolve()
	                  }
	                  return Promise.reject(new Error('新密码不能与旧密码相同'))
	                }
	              })
	            ]}
          >	
	            <Input.Password placeholder='请输入新密码' />
	          </Form.Item>
	
	          <Form.Item
	            label='确认新密码:'
	            name='confirmPwd'
	            dependencies={['newPwd']}
	            hasFeedback
	            rules={[
	              { required: true, message: '请确认新密码!' },
	              ({ getFieldValue }) => ({
	                validator(_, value) {
	                  if (!value || getFieldValue('newPwd') === value) {
	                    return Promise.resolve()
	                  }
	                  return Promise.reject(new Error('两次输入的密码不一致'))
	                }
	              })
	            ]}
          >	
	            <Input.Password placeholder='请再次输入新密码' />
	          </Form.Item>
	
	          <Form.Item wrapperCol={{ offset: 6, span: 16 }}>
	            <Button
	              type='primary'
	              htmlType='submit'
	              style={{
	                width: '100%',
	                height: '40px',
	                fontSize: '16px'
	              }}
            >	
	              确认修改
	            </Button>
	          </Form.Item>
	        </Form>
	      </Card>
	    </div>
	  )
	}
	export default UpdatePwd
  • 更新用户信息-UserInfo.jsx
		import { Card, Button, Form, Input, message, Upload, Avatar, Space, Typography } from 'antd'
	import { reqUpdateUserInfo, reqUpdate } from '@/apis/user'
	import { useEffect, useState } from 'react'
	import { UploadOutlined, UserOutlined } from '@ant-design/icons'
	
	const { Title } = Typography
	
	function UserInfo() {
	  // 从sessionStorage获取用户信息
	  const userInfo = JSON.parse(sessionStorage.getItem('userInfo'))
	
	  // 状态管理
	  const [fileList, setFileList] = useState([]) // 上传文件列表
	  const [form] = Form.useForm() // 表单实例
	
	  /**
	   * 上传前的校验
	   * @param {File} file - 上传的文件对象
	   * @returns {boolean|string} - 返回false阻止自动上传,返回Upload.LIST_IGNORE忽略文件
	   */
	  const beforeUpload = file => {
	    const isImage = file.type.startsWith('image/')
	    if (!isImage) {
	      message.error('只能上传图片文件!')
	      return Upload.LIST_IGNORE
	    }
	    setFileList([file])
	    return false // 阻止自动上传
	  }
	
	  /**
	   * 处理头像上传
	   * @param {string} [url] - 已有头像URL
	   * @returns {Promise<Object>} - 返回上传结果 {status, url}
	   */
	  const handleUpload = async url => {
	    // 如果已有URL直接返回
	    if (url) {
	      return { status: 0, url }
	    }
	
	    // 上传新头像
	    const formData = new FormData()
	    formData.append('image', fileList[0])
	    return await reqUpdate(formData)
	  }
	
	  /**
	   * 更新用户信息
	   * @param {Object} values - 表单值
	   */
	  const updateUserInfo = async values => {
	    try {
	      // 处理头像上传
	      const result = await handleUpload(values.user_pic[0]?.url)
	
	      if (result.status === 0) {
	        // 更新用户信息
	        const res = await reqUpdateUserInfo({
	          ...values,
	          user_pic: result.url
	        })
	
	        if (res.status === 0) {
	          message.success(res.message)
	          // 更新sessionStorage中的用户信息
	          const updatedUserInfo = {
	            ...userInfo,
	            ...values,
	            user_pic: result.url
	          }
	          sessionStorage.setItem('userInfo', JSON.stringify(updatedUserInfo))
	        }
	      }
	    } catch (error) {
	      message.error('更新用户信息失败')
	      console.error('更新用户信息错误:', error)
	    }
	  }
	
	  /**
	   * 标准化上传文件值
	   * @param {any} e - 上传事件对象
	   * @returns {Array} - 返回文件列表数组
	   */
	  const normFile = e => {
	    if (Array.isArray(e)) return e
	    return e?.fileList || []
	  }
	
	  /**
	   * 处理头像删除操作
	   */
	  const handleRemove = () => {
	    setFileList([])
	  }
	
	  // 初始化表单数据
	  useEffect(() => {
	    if (userInfo) {
	      form.setFieldsValue({
	        nickname: userInfo.nickname,
	        email: userInfo.email,
	        user_pic: userInfo.user_pic
	          ? [
	              {
	                uid: '-1',
	                name: 'avatar.png',
	                status: 'done',
	                url: userInfo.user_pic
	              }
	            ]
	          : []
	      })
	      setFileList(userInfo.user_pic ? [userInfo.user_pic] : [])
	    }
	  }, [form, userInfo])
	
	  return (
	    <div className='user-info-container'>
	      <Card className='user-info-card'>
	        <Space direction='vertical' size='middle' style={{ display: 'flex' }}>
	          <Title level={3} className='info-title'>
	            用户信息设置
	          </Title>
	
	          {/* 头像预览区域 */}
	          <div className='avatar-preview'>
	            <Avatar size={120} src={userInfo?.user_pic || null} icon={<UserOutlined />} className='avatar-image' />
	          </div>
	
	          {/* 用户信息表单 */}
	          <Form
	            form={form}
	            labelCol={{ span: 6 }}
	            wrapperCol={{ span: 16 }}
	            onFinish={updateUserInfo}
	            autoComplete='off'
          >	
	            {/* 姓名输入项 */}
	            <Form.Item
	              label='姓名'
	              name='nickname'
	              hasFeedback
	              rules={[
	                { required: true, message: '请输入姓名!' },
	                { whitespace: true, message: '姓名不能全是空格!' }
	              ]}
            >	
	              <Input placeholder='请输入您的姓名' maxLength={20} />
	            </Form.Item>
	
	            {/* 邮箱输入项 */}
	            <Form.Item
	              label='邮箱'
	              name='email'
	              hasFeedback
	              rules={[
	                { required: true, message: '请输入邮箱!' },
	                { type: 'email', message: '请输入有效的邮箱地址!' }
	              ]}
            >	
	              <Input placeholder='请输入您的邮箱' />
	            </Form.Item>
	
	            {/* 头像上传项 */}
	            <Form.Item
	              name='user_pic'
	              label='头像'
	              valuePropName='fileList'
	              rules={[{ required: true, message: '请上传头像!' }]}
	              getValueFromEvent={normFile}
	              extra='支持JPG/PNG格式,大小不超过2MB'
            >	
	              <Upload
	                listType='picture-card'
	                beforeUpload={beforeUpload}
	                onRemove={handleRemove}
	                accept='image/jpeg,image/png'
	                fileList={fileList}
	                maxCount={1}
              >	
	                {fileList.length >= 1 ? null : (
	                  <div>
	                    <UploadOutlined style={{ fontSize: '20px', color: '#1890ff' }} />
	                    <div style={{ marginTop: '8px' }}>上传头像</div>
	                  </div>
	                )}
	              </Upload>
	            </Form.Item>
	
	            {/* 提交按钮 */}
	            <Form.Item wrapperCol={{ offset: 6, span: 16 }}>
	              <Button type='primary' htmlType='submit' size='large' className='submit-button'>
	                保存修改
	              </Button>
	            </Form.Item>
	          </Form>
	        </Space>
	      </Card>
	    </div>
	  )
	}
	
	export default UserInfo
  • 顶部右侧用户操作-UserView.jsx
	import { Avatar, Tooltip } from 'antd'
	import { LogoutOutlined, TeamOutlined, SyncOutlined } from '@ant-design/icons'
	import { useDispatch } from 'react-redux'
	import { logout } from '@/store/modules/user'
	import { useNavigate } from 'react-router'
	function UserView({ userInfo }) {
	  const dispatch = useDispatch()
	  const navigate = useNavigate()
	  // 退出登录
	  const logOut = () => {
	    dispatch(logout())
	    
	    // 导航到登录页
	    navigate('/login', { replace: true })
	    // 刷新页面以确保状态完全重置
	    window.location.reload()
	  }
	  return (
	    <>
	      <Tooltip
	        title={
	          <ul className='tooltip_box'>
	            <li onClick={() => navigate('/userinfo')}>
	              <TeamOutlined />
	              <span>个人主页</span>
	            </li>
	            <li onClick={() => navigate('/updatePwd')}>
	              <SyncOutlined />
	              <span>修改密码</span>
	            </li>
	            <li onClick={logOut}>
	              <LogoutOutlined />
	              <span>退出登录</span>
	            </li>
	          </ul>
	        }
	        placement='bottom'
	        color='#fff'
	        style={{ padding: 0 }}
      >	
	        <div className='userInfo'>
	          {userInfo.user_pic && <Avatar src={userInfo.user_pic} style={{ verticalAlign: 'middle', cursor: 'pointer' }} size='large'></Avatar>}
	          {!userInfo.user_pic && <Avatar style={{ verticalAlign: 'middle', cursor: 'pointer',backgroundColor:'#7265e6' }} size='large'>{userInfo.nickname}</Avatar>}
	          {userInfo.nickname}
	        </div>
	      </Tooltip>
	    </>
	  )
	}
	export default UserView

2.4 文章管理-文章分类页面

  1. 在pages下新建ArticleManage文件夹 Artcate.jsx
	import { Card, Table, Button, Form, Input, Space, Tag } from 'antd'
	import { SearchOutlined, RedoOutlined, PlusOutlined } from '@ant-design/icons'
	import { useEffect, useState } from 'react'
	import { reqArtcateList } from '@/apis/article'
	import ArtcateAction from './component/ArtcateAction'
	
	/**
	 * 文章分类管理页面
	 * 功能:分类列表展示、搜索、新增、编辑
	 */
	function Artcate() {
	  const [form] = Form.useForm()
	  const [loading, setLoading] = useState(false)
	  const [modalOpen, setModalOpen] = useState(false)
	  const [status, setStatus] = useState('')
	  const [selectedRow, setSelectedRow] = useState(null) // 当前选中的行数据
	  const [artcateList, setArtcateList] = useState([])
	  // 分页配置
	  const [pagination, setPagination] = useState({
	    current: 1,
	    pageSize: 10,
	    total: 0,
	    showSizeChanger: true, // 显示分页大小切换器
	    pageSizeOptions: ['10', '20', '50'], // 分页大小选项
	    showTotal: total => `共 ${total} 条` // 显示总数
	  })
	
	  // 组件挂载时获取数据
	  useEffect(() => {
	    getArtcateData()
	  }, [pagination.current, pagination.pageSize])
	
	  /**
	   * 获取分类数据
	   * @param {Object} params - 查询参数
	   */
	  const getArtcateData = async (params = {}) => {
	    setLoading(true)
	    try {
	      const res = await reqArtcateList({
	        ...params,
	        pageNum: pagination.current,
	        pageSize: pagination.pageSize
	      })
	      setArtcateList(res.data.list)
	      // 更新分页总数
	      setPagination(prev => ({
	        ...prev,
	        pageSize: res.data.pageSize,
	        current: res.data.pageNum,
	        total: res.data.total,
	        showTotal: total => `共 ${total} 条` // 显示总数
	      }))
	    } finally {
	      setLoading(false)
	    }
	  }
	
	  /**
	   * 表单提交查询
	   * @param {Object} values - 表单值
	   */
	  const onFinish = values => {
	    getArtcateData(values)
	    setPagination(prev => ({ ...prev, current: 1 })) // 搜索时回到第一页
	  }
	
	  /**
	   * 重置查询表单
	   */
	  const onReset = () => {
	    form.resetFields()
	    getArtcateData() // 查询全部数据
	  }
	
	  /**
	   * 新增/编辑分类操作
	   * @param {string} actionType - 操作类型 ('新增' | '修改')
	   * @param {Object} record - 行数据 (编辑时使用)
	   */
	  const handleArtcateAction = (actionType, record = null) => {
	    setStatus(actionType)
	    setSelectedRow(record) // 保存当前行数据
	    setModalOpen(true)
	  }
	
	  /**
	   * 关闭弹窗回调
	   */
	  const handleModalClose = () => {
	    setModalOpen(false)
	    getArtcateData() // 操作成功刷新列表
	  }
	
	  /**
	   * 处理表格分页变化
	   * @param {Object} pagination - 分页对象
	   */
	  const handleTableChange = pagination => {
	    setPagination(pagination)
	  }
	
	  // 表格列配置
	  const columns = [
	    {
	      title: 'ID',
	      dataIndex: 'id',
	      key: 'id',
	      width: 80,
	      align: 'center'
	    },
	    {
	      title: '分类名称',
	      dataIndex: 'name',
	      key: 'name',
	      ellipsis: true
	    },
	    {
	      title: '分类别名',
	      dataIndex: 'alias',
	      key: 'alias',
	      ellipsis: true
	    },
	    {
	      title: '状态',
	      dataIndex: 'is_delete',
	      key: 'is_delete',
	      width: 100,
	      align: 'center',
	      render: (_, { is_delete }) => (
	        <Tag color={is_delete === 0 ? 'green' : 'red'}>{is_delete === 0 ? '启用' : '禁用'}</Tag>
	      )
	    },
	    {
	      title: '操作',
	      key: 'action',
	      width: 150,
	      align: 'center',
	      render: (_, record) => (
	        <Space size='middle'>
	          <Button type='link' size='small' onClick={() => handleArtcateAction('修改', record)}>
	            编辑
	          </Button>
	        </Space>
	      )
	    }
	  ]
	
	  return (
	    <div className='artcate-container'>
	      {/* 搜索卡片 */}
	      <Card title='分类筛选' style={{ marginBottom: 16 }}>
	        <Form form={form} layout='inline' onFinish={onFinish}>
	          <Form.Item label='分类名称' name='name'>
	            <Input placeholder='请输入分类名称' allowClear style={{ width: 200 }} />
	          </Form.Item>
	
	          <Form.Item style={{ flex: 1 }}>
	            <Space style={{ width: '100%', justifyContent: 'space-between' }}>
	              <Space>
	                <Button onClick={onReset} icon={<RedoOutlined />}>
	                  重置
	                </Button>
	                <Button type='primary' htmlType='submit' icon={<SearchOutlined />} loading={loading}>
	                  查询
	                </Button>
	              </Space>
	              <Button type='primary' icon={<PlusOutlined />} onClick={() => handleArtcateAction('新增')}>
	                新增分类
	              </Button>
	            </Space>
	          </Form.Item>
	        </Form>
	      </Card>
	
	      {/* 数据表格卡片 */}
	      <Card title='分类列表'>
	        <Table
	          bordered
	          loading={loading}
	          columns={columns}
	          dataSource={artcateList}
	          rowKey='id'
	          pagination={pagination}
	          onChange={handleTableChange}
	          scroll={{ y: 400 }}
	        />
	      </Card>
	
	      {/* 新增/编辑弹窗 */}
	      <ArtcateAction modalOpen={modalOpen} status={status} onClose={handleModalClose} rowData={selectedRow} />
	    </div>
	  )
	}
	export default Artcate
  • 创建componen文件夹-新增和编辑的弹框抽离出来
  //  ArtcateAction.jsx
	import { Modal, Form, Input, Select, message, Divider, Card } from 'antd'
	import { useEffect } from 'react'
	import {
	  TagOutlined,
	  EditOutlined,
	  CloseCircleOutlined,
	  CheckCircleOutlined,
	  CodeOutlined,
	  StopOutlined,
	  CheckOutlined
	} from '@ant-design/icons'
	import { reqAddArtcate, reqEditArtcate } from '@/apis/article'
	
	function ArtcateAction({ modalOpen, status, onClose, rowData }) {
	  const [form] = Form.useForm()
	
	  const handleOk = async () => {
	    try {
	      const formData = await form.validateFields()
	      if (status === '新增') {
	        const res = await reqAddArtcate(formData)
	        if (res.status === 0) {
	          message.success({
	            content: res.message || '新增分类成功',
	            className: 'custom-message-success',
	            icon: <CheckCircleOutlined />
	          })
	          handleClose(true)
	        }
	      } else {
	        const res = await reqEditArtcate({ ...formData, id: rowData.id })
	        if (res.status === 0) {
	          message.success({
	            content: res.message || '新增分类成功',
	            className: 'custom-message-success',
	            icon: <CheckCircleOutlined />
	          })
	          handleClose(true)
	        }
	      }
	    } catch (error) {
	      if (error.errorFields) {
	        message.warning({
	          content: '请正确填写所有必填字段',
	          className: 'custom-message-warning',
	          icon: <CloseCircleOutlined />
	        })
	      }
	    }
	  }
	
	  const handleClose = (confirmed = false) => {
	    form.resetFields()
	    onClose?.(confirmed)
	  }
	
	  useEffect(() => {
	    if (modalOpen) {
	      form.setFieldsValue({
	        name: rowData?.name || '',
	        alias: rowData?.alias || '',
	        is_delete: rowData?.is_delete ?? 0
	      })
	    }
	  }, [modalOpen, rowData, form])
	
	  return (
	    <Modal
	      title={
	        <div style={{ display: 'flex', alignItems: 'center' }}>
	          <TagOutlined
	            style={{
	              color: '#722ed1',
	              fontSize: 20,
	              marginRight: 12
	            }}
	          />
	          <span
	            style={{
	              fontSize: 18,
	              fontWeight: 600
	            }}
          >	
	            {`${status}文章分类`}
	          </span>
	        </div>
	      }
	      open={modalOpen}
	      onOk={handleOk}
	      onCancel={() => handleClose(false)}
	      okText='提交'
	      cancelText='取消'
	      width={680}
	      okButtonProps={{
	        style: {
	          background: '#722ed1',
	          borderColor: '#722ed1',
	          borderRadius: 4,
	          height: 40,
	          padding: '0 24px'
	        }
	      }}
	      cancelButtonProps={{
	        style: {
	          borderRadius: 4,
	          height: 40,
	          padding: '0 24px'
	        }
	      }}
    >	
	      <Divider
	        style={{
	          margin: '16px 0',
	          borderColor: '#f0f0f0'
	        }}
	      />
	
	      <Form form={form} preserve={false} labelCol={{ span: 4 }} wrapperCol={{ span: 20 }} autoComplete='off'>
	        <Card
	          size='small'
	          style={{
	            border: '1px solid #f0f0f0',
	            borderRadius: 8
	          }}
        >	
	          <Form.Item
	            label={
	              <span style={{ display: 'flex', alignItems: 'center' }}>
	                <EditOutlined
	                  style={{
	                    color: '#13c2c2',
	                    marginRight: 8
	                  }}
	                />
	                <span>分类名称</span>
	              </span>
	            }
	            name='name'
	            rules={[	              { required: true, message: '请输入分类名称' },	              { max: 20, message: '长度不能超过20个字符' }	            ]}
          >	
	            <Input placeholder='如:技术文档' allowClear style={{ borderRadius: 4 }} />
	          </Form.Item>
	
	          <Form.Item
	            label={
	              <span style={{ display: 'flex', alignItems: 'center' }}>
	                <CodeOutlined
	                  style={{
	                    color: '#13c2c2',
	                    marginRight: 8
	                  }}
	                />
	                <span>分类别名</span>
	              </span>
	            }
	            name='alias'
	            rules={[	              { required: true, message: '请输入分类别名' },	              { pattern: /^[a-z0-9-]+$/i, message: '只允许字母、数字和横线' }
	            ]}
          >	
	            <Input placeholder='如:tech-docs' allowClear style={{ borderRadius: 4 }} />
	          </Form.Item>
	
	          <Form.Item
	            label={
	              <span style={{ display: 'flex', alignItems: 'center' }}>
	                <StopOutlined
	                  style={{
	                    color: '#13c2c2',
	                    marginRight: 8
	                  }}
	                />
	                <span>是否禁用</span>
	              </span>
	            }
	            name='is_delete'
	            rules={[{ required: true, message: '请选择状态' }]}
          >	
	            <Select
	              style={{ width: '100%', borderRadius: 4 }}
	              options={[	                {	                  label: (	                    <span style={{ display: 'flex', alignItems: 'center' }}>	                      <CheckOutlined style={{ color: '#52c41a', marginRight: 6 }} />	                      <span style={{ color: '#52c41a' }}>启用</span>	                    </span>	                  ),	                  value: 0	                },	                {	                  label: (	                    <span style={{ display: 'flex', alignItems: 'center' }}>	                      <StopOutlined style={{ color: '#ff4d4f', marginRight: 6 }} />	                      <span style={{ color: '#ff4d4f' }}>禁用</span>	                    </span>	                  ),	                  value: 1	                }	              ]}
	            />
	          </Form.Item>
	        </Card>
	      </Form>
	    </Modal>
	  )
	}
	
	export default ArtcateAction

2.5 文章管理-文章列表页面

  1. 在pages/ArticleManage下新建Article.jsx
	import { Card, Table, Button, Form, Input, Space, Select, Popconfirm, message } from 'antd'
	import { SearchOutlined, RedoOutlined, PlusOutlined } from '@ant-design/icons'
	import { getArtcateList } from '@/store/modules/article'
	import { useDispatch, useSelector } from 'react-redux'
	import { useEffect, useState } from 'react'
	import { reqArticles, reqDeleteArticles } from '@/apis/article'
	import ArticleAction from './component/ArticleAction'
	import { reqUpdateArticles } from '@/apis/article'
	
	function Article() {
	  const dispatch = useDispatch()
	  const [loading, setLoading] = useState(false) // 加载状态
	  const artcateList = useSelector(state => state.article.artcateList) // 从Redux获取文章分类列表
	  const [dataSource, setDataSource] = useState([]) // 表格数据源
	  const [form] = Form.useForm() // 表单实例
	  const [modalOpen, setModalOpen] = useState(false)
	  const [status, setStatus] = useState('')
	  const [selectedRow, setSelectedRow] = useState(null) // 当前选中的行数据
	
	  // 分页配置
	  const [pagination, setPagination] = useState({
	    current: 1, // 当前页码
	    pageSize: 10, // 每页条数
	    total: 0, // 总条数
	    showSizeChanger: true, // 显示分页大小切换器
	    pageSizeOptions: ['10', '20', '50'], // 分页大小选项
	    showTotal: total => `共 ${total} 条` // 显示总数
	  })
	
	  /**
	   * 获取文章列表数据
	   * @param {Object} params 查询参数
	   */
	  const fetchArticles = async params => {
	    setLoading(true)
	    try {
	      const res = await reqArticles({
	        ...params,
	        pageNum: pagination.current,
	        pageSize: pagination.pageSize
	      })
	      if (res.status === 0) {
	        setDataSource(res.data.list)
	        setPagination(prev => ({
	          ...prev,
	          pageSize: res.data.pageSize,
	          current: res.data.pageNum,
	          total: res.data.total,
	          showTotal: total => `共 ${total} 条` // 显示总数
	        }))
	      }
	    } catch (error) {
	      console.error('获取文章列表失败:', error)
	    } finally {
	      setLoading(false)
	    }
	  }
	
	  // 重置表单和查询
	  const handleReset = () => {
	    form.resetFields()
	    fetchArticles()
	  }
	
	  // 提交查询
	  const handleSearch = values => {
	    fetchArticles(values)
	  }
	
	  // 分页变化回调
	  const handleTableChange = pagination => {
	    setPagination(pagination)
	  }
	
	  // 初始化数据
	  useEffect(() => {
	    // 获取文章分类列表
	    dispatch(getArtcateList())
	    // 获取文章列表
	    fetchArticles()
	  }, [pagination.current, pagination.pageSize])
	
	  // 新增||编辑
	  const handleArticleAction = (actionType, record = null) => {
	    setStatus(actionType)
	    setSelectedRow(record) // 保存当前行数据
	    setModalOpen(true)
	  }
	  /**
	   * 关闭弹窗回调
	   */
	  const handleModalClose = () => {
	    setModalOpen(false)
	    fetchArticles() // 操作成功刷新列表
	  }
	  // 删除文章列表
	  const handleDelete = async id => {
	    const res = await reqDeleteArticles({ id })
	    if (res.status === 0) {
	      message.success(res.message)
	      fetchArticles()
	    }
	  }
	  // 下架文章列表
	  const handleRemove = async record => {
	    const res = await reqUpdateArticles({ id: record.id, state: record.state })
	    if (res.status === 0) {
	      message.success(res.message)
	      fetchArticles()
	    }
	  }
	  // 表格列配置
	  const columns = [
	    {
	      title: '文章标题',
	      dataIndex: 'title',
	      key: 'title',
	      width: '20%',
	      ellipsis: true // 超出显示省略号
	    },
	    {
	      title: '文章内容',
	      dataIndex: 'content',
	      key: 'content',
	      width: '20%',
	      render: text => (
	        <div className='ellipsis-text' style={{ WebkitLineClamp: 2 }}>
	          {text}
	        </div>
	      )
	    },
	    {
	      title: '文章封面',
	      dataIndex: 'cover_img',
	      key: 'cover_img',
	      width: '10%',
	      render: text =>
	        text ? (
	          <img
	            src={text}
	            alt='封面'
	            style={{
	              width: 60,
	              height: 40,
	              objectFit: 'cover',
	              borderRadius: 4
	            }}
	          />
	        ) : (
	          <span className='text-secondary'>无封面</span>
	        )
	    },
	    {
	      title: '所属分类',
	      dataIndex: 'cate_id',
	      key: 'cate_id',
	      width: '10%',
	      render: cate_id => {
	        const category = artcateList.list.find(item => item.id == cate_id)
	        return category ? category.name : '未分类'
	      }
	    },
	    {
	      title: '状态',
	      dataIndex: 'state',
	      key: 'state',
	      width: '10%',
	      render: text => (
	        <span style={{ color: text === 1 ? '#52c41a' : '#ff4d4f' }}>{text === 1 ? '已发布' : '已下架'}</span>
	      )
	    },
	    {
	      title: '操作',
	      key: 'action',
	      width: '10%',
	      render: (_, record) => (
	        <Space size='small'>
	          <Button type='link' size='small' onClick={() => handleArticleAction('修改', record)}>
	            编辑
	          </Button>
	          <Popconfirm
	            title={`确认${record.state === 1 ? '下架' : '上架'}?`}
	            okText='确定'
	            cancelText='取消'
	            onConfirm={() => handleRemove(record)}
          >	
	            <Button type='link' size='small'>
	              {record.state === 1 ? '下架' : '上架'}
	            </Button>
	          </Popconfirm>
	          <Popconfirm title='确认删除吗?' okText='确定' cancelText='取消' onConfirm={() => handleDelete(record.id)}>
	            <Button type='link' size='small' danger>
	              删除
	            </Button>
	          </Popconfirm>
	        </Space>
	      )
	    }
	  ]
	  return (
	    <div className='article-management'>
	      {/* 搜索卡片 */}
	      <Card title='文章查询' style={{ marginBottom: 16 }}>
	        <Form form={form} layout='inline' onFinish={handleSearch}>
	          <Form.Item label='文章名称' name='title'>
	            <Input placeholder='请输入文章名称' style={{ width: 200 }} allowClear />
	          </Form.Item>
	
	          <Form.Item label='文章分类' name='cate_id'>
	            <Select
	              placeholder='请选择文章分类'
	              style={{ width: 200 }}
	              options={artcateList.list || []}
	              fieldNames={{ label: 'name', value: 'id' }}
	              allowClear
	            />
	          </Form.Item>
	
	          <Form.Item>
	            <Space>
	              <Button icon={<RedoOutlined />} onClick={handleReset}>
	                重置
	              </Button>
	              <Button type='primary' icon={<SearchOutlined />} htmlType='submit' loading={loading}>
	                查询
	              </Button>
	            </Space>
	          </Form.Item>
	        </Form>
	      </Card>
	
	      {/* 表格卡片 */}
	      <Card
	        title='文章列表'
	        extra={
	          <Button type='primary' icon={<PlusOutlined />} onClick={() => handleArticleAction('新增')}>
	            新增文章
	          </Button>
	        }
	        className='table-card'
      >	
	        <Table
	          rowKey='id'
	          loading={loading}
	          bordered
	          columns={columns}
	          dataSource={dataSource}
	          pagination={pagination}
	          onChange={handleTableChange}
	          scroll={{ y: 400 }}
	        />
	      </Card>
	
	      {/*新增&编辑 */}
	      <ArticleAction
	        modalOpen={modalOpen}
	        status={status}
	        onClose={handleModalClose}
	        rowData={selectedRow}
      >	</ArticleAction>
	    </div>
	  )
	}
	
	export default Article

  • 在component里新建ArticleAction.jsx(新增和编辑)
	import { Modal, Form, Input, Select, message, Upload, Divider, Card } from 'antd'
	import { useEffect, useState } from 'react'
	import { useSelector } from 'react-redux'
	import { UploadOutlined, EditOutlined, FileTextOutlined, TagOutlined, PictureOutlined } from '@ant-design/icons'
	import { reqUpdate } from '@/apis/user'
	import { reqAddArticles, reqUpdateArticles } from '@/apis/article'
	
	function ArticleAction({ modalOpen, status, onClose, rowData }) {
	  // 上传文件列表状态
	  const [fileList, setFileList] = useState([])
	  // 表单实例
	  const [form] = Form.useForm()
	  // 从Redux获取文章分类列表
	  const artcateList = useSelector(state => state.article.artcateList)
	
	  /**
	   * 上传前校验
	   * @param {File} file 上传的文件
	   * @returns {boolean} 是否通过校验
	   */
	  const beforeUpload = file => {
	    const isImage = file.type.startsWith('image/')
	    if (!isImage) {
	      message.error('只能上传图片文件!')
	      return Upload.LIST_IGNORE
	    }
	    setFileList([file])
	    return false
	  }
	
	  /**
	   * 处理文件上传
	   * @returns {Promise} 上传结果
	   */
	  const handleUpload = async (url = '') => {
	    if (url) {
	      return { status: 0, url }
	    }
	    // 如果选择了新文件,上传新文件
	    const formData = new FormData()
	    formData.append('image', fileList[0])
	    return await reqUpdate(formData)
	  }
	
	  /**
	   * 提交表单
	   */
	  const handleSubmit = async () => {
	    try {
	      // 验证表单
	      const formData = await form.validateFields()
	      // 处理图片上传
	      const result = await handleUpload(formData.cover_img[0].url)
	      if (result.status !== 0) return
	      if (status === '新增') {
	        const res = await reqAddArticles({ ...formData, cover_img: result.url })
	        if (res.status === 0) {
	          message.success(res.message)
	          onClose()
	        }
	      } else {
	        const res = await reqUpdateArticles({
	          ...formData,
	          id: rowData.id,
	          cover_img: result.url
	        })
	        if (res.status === 0) {
	          message.success(res.message)
	          onClose()
	        }
	      }
	    } catch (error) {
	      if (error.errorFields) {
	        message.warning('请正确填写所有必填字段')
	      }
	    }
	  }
	
	  /**
	   * 标准化上传文件格式
	   * @param {Object} e 上传事件对象
	   * @returns {Array} 文件列表
	   */
	  const normFile = e => {
	    if (!e) return []
	    if (Array.isArray(e)) return e
	    return e.fileList || []
	  }
	
	  // 初始化时设置表单默认值
	  useEffect(() => {
	    if (modalOpen) {
	      if (rowData) {
	        setOriginalCoverImg(rowData.cover_img || '')
	        form.setFieldsValue({
	          ...rowData,
	          cover_img: rowData.cover_img
	            ? [
	                {
	                  uid: '-1',
	                  name: 'cover_image.png',
	                  status: 'done',
	                  url: rowData.cover_img
	                }
	              ]
	            : []
	        })
	        setFileList(
	          rowData.cover_img
	            ? [
	                {
	                  uid: '-1',
	                  name: 'cover_image.png',
	                  status: 'done',
	                  url: rowData.cover_img
	                }
	              ]
	            : []
	        )
	      } else {
	        setOriginalCoverImg('')
	        form.resetFields()
	        setFileList([])
	      }
	    }
	  }, [modalOpen, rowData])
	
	  return (
	    <Modal
	      title={
	        <div style={{ display: 'flex', alignItems: 'center' }}>
	          <FileTextOutlined
	            style={{
	              color: '#722ed1',
	              fontSize: 20,
	              marginRight: 12
	            }}
	          />
	          <span
	            style={{
	              fontSize: 18,
	              fontWeight: 600
	            }}
          >	
	            {`${status}文章`}
	          </span>
	        </div>
	      }
	      open={modalOpen}
	      onOk={handleSubmit}
	      onCancel={() => onClose(false)}
	      okText='提交'
	      cancelText='取消'
	      width={750}
	      okButtonProps={{
	        style: {
	          background: '#722ed1',
	          borderColor: '#722ed1',
	          borderRadius: 4,
	          height: 40,
	          padding: '0 24px'
	        }
	      }}
	      cancelButtonProps={{
	        style: {
	          borderRadius: 4,
	          height: 40,
	          padding: '0 24px'
	        }
	      }}
    >	
	      <Divider
	        style={{
	          margin: '16px 0',
	          borderColor: '#f0f0f0'
	        }}
	      />
	
	      <Form form={form} labelCol={{ span: 4 }} wrapperCol={{ span: 20 }} autoComplete='off'>
	        <Card
	          size='small'
	          style={{
	            marginBottom: 24,
	            border: '1px solid #f0f0f0',
	            borderRadius: 8
	          }}
        >	
	          <Form.Item
	            label={
	              <span style={{ display: 'flex', alignItems: 'center' }}>
	                <TagOutlined
	                  style={{
	                    color: '#13c2c2',
	                    marginRight: 8
	                  }}
	                />
	                <span>所属分类</span>
	              </span>
	            }
	            name='cate_id'
	            rules={[{ required: true, message: '请选择所属分类' }]}
          >	
	            <Select
	              placeholder='请选择文章分类'
	              style={{ width: '100%', borderRadius: 4 }}
	              options={artcateList.list || []}
	              fieldNames={{ label: 'name', value: 'id' }}
	              allowClear
	            />
	          </Form.Item>
	          <Form.Item
	            label={
	              <span style={{ display: 'flex', alignItems: 'center' }}>
	                <EditOutlined
	                  style={{
	                    color: '#13c2c2',
	                    marginRight: 8
	                  }}
	                />
	                <span>文章标题</span>
	              </span>
	            }
	            name='title'
	            rules={[
	              { required: true, message: '请输入文章标题' },
	              { max: 20, message: '长度不能超过20个字符' }
	            ]}
          >	
	            <Input placeholder='请输入文章标题' allowClear style={{ borderRadius: 4 }} />
	          </Form.Item>
	
	          <Form.Item
	            label={
	              <span style={{ display: 'flex', alignItems: 'center' }}>
	                <FileTextOutlined
	                  style={{
	                    color: '#13c2c2',
	                    marginRight: 8
	                  }}
	                />
	                <span>文章内容</span>
	              </span>
	            }
	            name='content'
	            rules={[{ required: true, message: '请输入文章内容' }]}
          >	
	            <Input.TextArea
	              placeholder='请输入文章内容'
	              allowClear
	              rows={6}
	              showCount
	              maxLength={500}
	              style={{ borderRadius: 4 }}
	            />
	          </Form.Item>
	          <Form.Item
	            label={
	              <span style={{ display: 'flex', alignItems: 'center' }}>
	                <PictureOutlined
	                  style={{
	                    color: '#13c2c2',
	                    marginRight: 8
	                  }}
	                />
	                <span>文章封面</span>
	              </span>
	            }
	            name='cover_img'
	            valuePropName='fileList'
	            rules={[{ required: true, message: '请上传头像!' }]}
	            getValueFromEvent={normFile}
	            extra='仅支持JPG/PNG格式'
          >	
	            {/* <Upload
	              listType='picture-card'
	              beforeUpload={beforeUpload}
	              onRemove={() => setFileList([])}
	              fileList={fileList}
	              accept='image/jpeg,image/png'
	              maxCount={1}
	              style={{ width: '100%' }}
            >	
	              {fileList.length >= 1 ? null : (
	                <div
	                  style={{
	                    display: 'flex',
	                    flexDirection: 'column',
	                    alignItems: 'center',
	                    padding: '16px 0'
	                  }}
                >	
	                  <UploadOutlined
	                    style={{
	                      fontSize: 24,
	                      color: '#1890ff'
	                    }}
	                  />
	                  <div
	                    style={{
	                      marginTop: 8,
	                      color: '#666',
	                      fontSize: 14
	                    }}
                  >	
	                    点击上传封面
	                  </div>
	                </div>
	              )}
	            </Upload> */}
	            <Upload
	              listType='picture-card'
	              beforeUpload={beforeUpload}
	              onRemove={() => setFileList([])}
	              accept='image/jpeg,image/png'
	              fileList={fileList}
            >	
	              {fileList.length >= 1 ? null : (
	                <div>
	                  <UploadOutlined style={{ fontSize: '20px', color: '#1890ff' }} />
	                  <div style={{ marginTop: '8px' }}>上传头像</div>
	                </div>
	              )}
	            </Upload>
	          </Form.Item>
	        </Card>
	      </Form>
	    </Modal>
	  )
	}
	
	export default ArticleAction