说明
该项目是一个基础的 React 后台管理系统,后端使用NodeJS为服务器,功能简洁明了,涵盖了用户登录、注册,退出以及数据的增删改查等操作,非常适合初学者学习参考。
项目演示
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 项目目录
- 在 src 文件夹下,我们先删除多余的文件,只保留 App.js、index.js、index.css。
- 删除 index.js 文件里面 React.StrictMode函数
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
- 官网:
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
- 安装
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
- 在项目的 main.jsx 里面配置路由
1.6 安装Redux和Redux Toolkit
npm install react-redux
npm install @reduxjs/toolkit
- 在store目录下新建index.js
- 在main.jsx里配置Redux
1.7 安装scss和normalize.css
- 安装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
- 在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 用户登录和注册
- 在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
- 新建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;
}
}
}
- 在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页面
- 在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
- 在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 文章管理-文章分类页面
- 在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 文章管理-文章列表页面
- 在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