# 极客园项目-PC **Repository Path**: jetwang88/geek-pc ## Basic Information - **Project Name**: 极客园项目-PC - **Description**: 基于React技术栈 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: http://www.jetwang.cn/geek-pc - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2023-04-06 - **Last Updated**: 2023-07-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: react-redux, React, Ant-Design ## README # 极客园项目-PC > 极客园 PC 端项目:个人自媒体管理端 - 项目功能和演示,包括 - 登录、退出 - 首页 - 内容(文章)管理:文章列表、发布文章、修改文章 - 技术栈: - 项目搭建:React 官方脚手架 `create-react-app` - react hooks - 状态管理:redux,以及:`react-redux` 绑定库 - UI 组件库:`antd` v4 - ajax请求库:`axios` - 路由:`react-router-dom` 以及 `history` - 富文本编辑器:`react-quill` - CSS 预编译器:`sass` - CSS Modules 避免组件之间的样式冲突 ## 1. 创建项目 > npx create-react-app geek-pc ## 2. 整理目录结构 ```js /src /assets 项目资源文件,比如,图片 等 /components 通用组件 /pages 页面组件 /router 路由配置 /store Redux 状态仓库 /utils 工具,比如,token、axios 的封装等 App.css 根组件样式文件 App.js 根组件 index.css 全局样式 index.js 项目入口 ``` ## 3. 安装sass > yarn add sass `index.scss` ```scss * { padding: 0; margin: 0; box-sizing: border-box; } body { font-family: PingFang SC, 'Courier New', Courier, monospace, sans-serif; } #root, .app { height: 100%; } ``` ## 4. 配置路由 > 1. 安装路由:`yarn add react-router-dom` > 2. 在 pages 目录中创建两个文件夹:Login、Layout、NotFound > 3. 分别在三个目录中创建 index.jsx 文件,并创建一个简单的组件后导出 > 4. 在 App 组件中,导入路由组件以及 3 个页面组件 > 5. 配置 Login、Layout、NotFound 的路由规则 `App.js` ```jsx import { useRoutes } from 'react-router-dom' // 导入页面组件 import Login from './pages/Login' import Layout from './pages/Layout' import NotFound from './pages/NotFound' const routes = [ { path: '/', element: }, { path: '/login', element: }, { path: '*', element: }, ] export default function App() { const element = useRoutes(routes) return (
{ element }
) } ``` `index.js` ```js import React from 'react' import ReactDOM from 'react-dom/client' import './index.scss' import App from './App' import { BrowserRouter } from 'react-router-dom' const root = ReactDOM.createRoot(document.getElementById('root')) root.render( ) ``` ## 5. 使用antd组件 > 1. 安装 antd 组件库:`yarn add antd@4` > 2. 全局导入 antd 组件库的样式 > 3. 导入 Button 组件 > 4. 在 Login 页面渲染 Button 组件 `index.js` ```diff import React from 'react' import ReactDOM from 'react-dom/client' + // 先导入 antd 样式文件 + import 'antd/dist/antd.min.css' + // 再导入全局样式文件,防止样式覆盖 import './index.scss' import App from './App' import { BrowserRouter } from 'react-router-dom' const root = ReactDOM.createRoot(document.getElementById('root')) root.render( ) ``` `Login.js` ```js import React from 'react' import { Button } from 'antd' export default function login() { return (
) } ``` ## 6. 配置路径别名 > 能够配置@路径别名简化路径处理 > > 1. 安装修改 CRA 配置的包:`yarn add -D @craco/craco` > 2. 在项目根目录中创建 craco 的配置文件:`craco.config.js`,并在配置文件中配置路径别名 > 3. 修改 `package.json` 中的脚本命令 > 4. 在代码中,就可以通过 `@` 来表示 src 目录的绝对路径 > 5. 重启项目,让配置生效 `craco.config.js` ```js const path = require('path') module.exports = { // webpack 配置 webpack: { // 配置别名 alias: { // 约定:使用 @ 表示 src 文件所在路径 '@': path.resolve(__dirname, 'src') } } } ``` `package.json` ```json // 将 start/build/test 三个命令修改为 craco 方式 "scripts": { "start": "craco start", "build": "craco build", "test": "craco test", "eject": "react-scripts eject" }, ``` `App.js` ```diff import { useRoutes } from 'react-router-dom' // 导入页面组件 import Login from './pages/Login' import Layout from './pages/Layout' + import NotFound from '@/pages/NotFound' const routes = [ { path: '/', element: }, { path: '/login', element: }, { path: '*', element: }, ] export default function App() { const element = useRoutes(routes) return (
{ element }
) } ``` ## 7. 路径别名提示 > 能够让vscode识别@路径并给出路径提示 > > 1. 在项目根目录创建 `jsconfig.json` 配置文件 > 2. 在配置文件中添加以下配置 `jsconfig.json` ```json { "compilerOptions": { "baseUrl": "./", "paths": { "@/*": ["src/*"] } } } ``` ## 8. 登录界面 ### 8.1 基本结构 `Login.jsx` ```jsx import React from 'react' import logo from '@/assets/logo.png' import { Card } from 'antd' import './index.scss' export default function login() { return (
{/* 登录表单 */}
) } ``` `Login/index.scss` ```css .login { width: 100%; height: 100%; position: absolute; left: 0; top: 0; background: center/cover url(../../assets/login.png); .login-logo { width: 200px; height: 60px; display: block; margin: 0 auto 20px; } .login-container { width: 440px; height: 360px; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); box-shadow: 0 0 50px rgb(0 0 0 / 10%); } .login-checkbox-label { color: #1890ff; } } ``` ### 8.2 表单结构 ```jsx import React from 'react' import logo from '@/assets/logo.png' import { Card, Form, Input, Checkbox, Button } from 'antd' import './index.scss' export default function login() { return (
{/* 登录表单 */}
我已阅读并同意「用户协议」和「隐私条款」
) } ``` ### 8.3 表单校验 ```jsx import React from 'react' import logo from '@/assets/logo.png' import { Card, Form, Input, Checkbox, Button } from 'antd' import './index.scss' export default function login() { return (
{/* 登录表单 */}
我已阅读并同意「用户协议」和「隐私条款」
) } ``` ### 8.4 获取表单值 ```jsx function onFinish(values) { console.log(values) }
...
``` ## 9. 引入Redux > 1. 安装 redux 相关的包:`yarn add @reduxjs/toolkit react-redux axios` > 2. 在 store 目录中分别创建:modules 文件夹、index.js 文件 > 3. 新建login模块,存储token `store目录结构` ```js /store /modules login.js index.js ``` `modules/login.js` ```js import { createSlice } from "@reduxjs/toolkit" export const login = createSlice({ // 1.命名空间 name: 'login', // 2.初始化状态 initialState: { token: '' }, // 3.定义reducers reducers: { // action函数(同步) setToken(preState, action) { preState.token = action.payload }, delToken(preState) { preState.token = '' } } }) // 导出reducer export default login.reducer ``` `store/index.js` ```js import { configureStore } from "@reduxjs/toolkit" import login from './modules/login' export default configureStore({ reducer: { login } }) ``` `index.js` ```js ...... import { Provider } from 'react-redux' import store from './store' const root = ReactDOM.createRoot(document.getElementById('root')) root.render( ) ``` ## 10. 封装axios > 1. 创建 utils/request.js 文件 > 2. 创建 axios 实例,配置 baseURL,简化接口路径 > 3. 在 utils/index.js 中,默认导出 request `utils/request.js` ```js import axios from "axios" const request = axios.create({ baseURL: 'http://geek.itheima.net/v1_0', timeout: 5000 }) export default request ``` ## 11. Redux登录 > 1. 在 Login 组件中分发登录的异步 action > 2. 在store中login模块定义异步action,获取token并存储到redux和持久化处理 > 3. 登录成功后,跳转到首页 `modules/login.js` ```js import { createSlice } from "@reduxjs/toolkit" import request from '@/utils/request' export const login = createSlice({ // 1.命名空间 name: 'login', // 2.初始化状态 initialState: { token: localStorage.getItem('geek-token') || '' }, // 3.定义reducers reducers: { // action函数(同步) setToken(preState, action) { preState.token = action.payload }, delToken(preState) { preState.token = '' } } }) // 导出action export const { setToken, delToken } = login.actions // 异步action export function loginAction(formData) { return async (dispatch) => { // 获取token const { data: { data } } = await request.post('/authorizations', formData) dispatch(setToken(data.token)) // 本地存储一份 localStorage.setItem('geek-token', data.token) } } // 导出reducer export default login.reducer ``` `Login/index.tsx` ```tsx import React from 'react' import logo from '@/assets/logo.png' import { Card, Form, Input, Checkbox, Button, message } from 'antd' import './index.scss' import { useDispatch } from 'react-redux' import { useNavigate } from 'react-router-dom' import { loginAction } from '@/store/modules/login' export default function Login() { const dispatch = useDispatch() const history = useNavigate() const onFinish = async values => { try { await dispatch(loginAction(values)) history('/') } catch (e) { message.error(e.message) } } ...... } ``` > 使用 Redux 的套路: > > `组件 dispatch 异步 action -> 提供异步 action -> 完成异步操作 -> 继续 dispatch 普通 action 来发起状态更新 -> reducers 处理状态更新` ## 12. 封装token工具模块 > 能够统一处理 token 的持久化相关操作 > > 1. 创建 utils/token.js 文件 > 2. 分别提供 getToken/saveToken/clearToken/isAuth 四个工具函数并导出 > 3. 将登录操作中用到 token 的地方,替换为该工具函数 `utils/token.js` ```js const TOKEN_KEY = 'geek-pc' // 获取token const getToken = () => localStorage.getItem(TOKEN_KEY) // 存储token const saveToken = token => localStorage.setItem(TOKEN_KEY, token) // 清除token const clearToken = () => localStorage.removeItem(TOKEN_KEY) // 是否登录 const isAuth = () => !!getToken() export { isAuth, getToken, saveToken, clearToken } ``` ```js import { getToken, saveToken } from "@/utils/token" export const login = createSlice({ // 1.命名空间 name: 'login', // 2.初始化状态 initialState: { token: getToken() || '' }, ..... }) ``` ## 13. 路由鉴权 > 能够实现未登录时访问拦截并跳转到登录页 > > 1. 在 components 目录中,创建 AuthRoute/index.js 文件 > 2. 使用**鉴权方法**,判断是否登录 > 3. 登录时,直接渲染相应页面组件 > 4. 未登录时,重定向到登录页面 > 5. 将需要鉴权的页面路由配置,替换为 AuthRoute 组件 `components/AuthRoute` ```jsx import { Navigate } from "react-router-dom" import { isAuth } from "@/utils/token" export default function AuthRoute({ element }) { return ( <> {isAuth() ? element : } ) } ``` `App.js` ```jsx import { useRoutes } from 'react-router-dom' import AuthRoute from './components/AuthRoute' // 导入页面组件 import Login from './pages/Login' import Layout from './pages/Layout' import NotFound from '@/pages/NotFound' const routes = [ { path: '/', element: } /> }, { path: '/login', element: }, { path: '*', element: }, ] export default function App() { const element = useRoutes(routes) return (
{element}
) } ``` ## 14. 首页布局 > 能够根据antd布局组件搭建基础布局 > > 1. 打开 antd/Layout 布局组件文档,找到示例:顶部-侧边通栏 > 2. 拷贝示例代码到我们的 Layout 页面中 > 3. 分析并调整页面布局 `pages/Layout/index.js` ```jsx import './index.scss' import React from 'react' import { Layout, Menu, Popconfirm } from 'antd' import { LogoutOutlined, WindowsFilled, SnippetsFilled, HighlightFilled } from '@ant-design/icons' const { Header, Sider } = Layout export default function GeekLayout() { // 菜单 const items = [ { label: '数据概览', key: '1', icon: }, { label: '内容管理', key: '2', icon: }, { label: '发布文章', key: '3', icon: }, ] const onClick = e => { console.log(e.key) } return (
{/* + 用户信息 */}
user.name 退出
{/* + 菜单 */} 内容
) } ``` `page/Layout/index.scss ` ```scss .ant-layout { height: 100%; } .header { padding: 0; } .logo { width: 200px; height: 60px; background: url(../../assets/logo.png) no-repeat center / 160px auto; } .layout-content { overflow-y: auto; } .user-info { position: absolute; right: 0; top: 0; padding-right: 20px; color: #fff; .user-name { margin-right: 20px; } .user-logout { display: inline-block; cursor: pointer; } } ``` ## 15. CSSModules > - CSS Modules 即:CSS 模块,可以理解为对 CSS 进行模块化处理 > - 目的:为了在 React 开发时,**解决组件之间类名重复导致的样式冲突问题** > - 使用 CSS Modules 前后的对比: > - 使用前:自己手动为每个组件起一个唯一的类名 > - 使用后:自动生成类名,即使将来多人合作开发项目,也不会导致类名冲突 > - React 脚手架中为 CSSModules 自动生成的类名格式为:`[filename]\_[classname]\_\_[hash]` > - filename:文件名称 > - classname:自己在 CSS 文件中写的类名 > - hash:随机生成的哈希值 ```scss /* GeekLayout 组件的 css 文件中:*/ .header {} /* React 项目中,CSS Modules 处理后生成的类名:*/ .GeekLayout_header__adb4t {} ``` ## 16. CSSModules使用 > **内容**: > > 1. CSS 文件名称以 `.module.css` 结尾的,此时,React 就会将其当做 CSSModules 来处理,比如,`index.module.scss` > 2. 如果不想使用 CSSModules 的功能,只需要让样式文件名称中不带`.module` 即可,比如,`index.css` > > **步骤**: > > 1. 创建样式文件,名称格式为:`index.module.scss` > > 2. 在 `index.module.scss` 文件中,按照原来的方式写 CSS 即可 > > 3. **在 JS 中通过 `import styles from './index.module.scss'` 来导入样式文件** > > 4. 在 JSX 结构中,通过 `className={styles.类名}` 形式来使用样式(此处的 类名 就是 CSS 中写的类名) ```jsx // Login/index.module.css .a { color: red; } // Login/index.js import styles from './index.module.scss' // 对象中的属性 a 就是:我们自己写的类名 // 属性的值 就是:React 脚手架帮我们自动生成的一个类名,这个类名是随机生成的,所以,是全局唯一的!!! // styles => { a: "Login_a__2O2Gg" } const Login = () => { return (
Login
) } export default Login ``` ## 17. CSSModules规则 > 能够说出为什么 CSSModules 中的类名推荐使用驼峰命名法 1. **CSSModules 类名推荐使用驼峰命名法**,这有利于在组件的 JS 代码中访问 ```scss /* index.mdouel.css */ /* 推荐使用 驼峰命名法 */ .a { color: red; } .listItem { font-size: 30px; } /* 不推荐使用 短横线(-)链接的形式 */ .list-item { font-size: 30px; } ``` 2. **不推荐嵌套样式** - 对于 CSS 来说,嵌套样式,很重要的一个目的就是提升 CSS 样式权重,避免样式冲突 - 但是,CSSModules 生成的类名是全局唯一的,就不存在权重不够或者类名重复导致的样式冲突问题 ## 18. CSSModules全局样式 > 能够在 CSSModules 中使用全局样式 > > - 在 `*.module.css` 文件中,类名都是“局部的”,也就是只在当前组件内生效 > > - 有些特殊情况下,如果不想要让某个类名是局部的,就需要通过 `:global()` 来处理,处理后,这个类名就变为全局的了 > > - 从代码上来看,全局的类名是不会被 CSSModules 处理的 ```scss /* 该类型会被 CSSModules 处理 */ .title { color: yellowgreen; } /* 如果这个类名,不需要进行 CSSModules 处理,可以通过添加 :global() 来包裹 */ :global(.title) { color: yellowgreen; } ``` ## 19. CSSModules配合SASS使用 > 能够将 CSSModules 配合 SASS 使用 > > - 每个组件的根节点使用 CSSModules 形式的类名( 根元素的类名: `root` ) > - 其他所有的子节点,都使用普通的 CSS 类名 > > 这样处理的优势:解决组件间样式冲突问题的同时,让给组件添加样式尽量简单 > > 说明:对`layout`组件进行样式模块化改造 ```scss .root { // 根节点自己的样式 :global { // 所有子节点的样式,都放在此处,因为是在 global 中,所以,此处的类名不会被 CSSModules 处理 .header {} .logo {} .user-info {} } } ``` > 组件中使用 CSSModules: ```jsx import styles from './index.module.scss' const GeekLayout = () => { return (
) } ``` ## 20. 嵌套路由配置 > 1. 在 pages 目录中,分别创建:Home(数据概览)、Article(内容管理)、Publish(发布文章)页面文件夹 > 2. 分别在三个文件夹中创建 index.js 并创建基础组件后导出 > 3. 在 router公共布局页面下,配置children子路由 > 4. 在Layout父组件中,放置子路由挂载点`Outlet` `router/index.js` ```js import Home from '@/pages/Home' import Article from '@/pages/Article' import Publish from '@/pages/Publish' import Login from '@/pages/Login' import NotFound from '@/pages/NotFound' import AuthRoute from '@/components/AuthRoute' import Layout from '@/pages/Layout' const routes = [ { path: '/', element: } />, children: [ { path: '/', element: }, { path: 'article', element:
}, { path: 'publish', element: } ] }, { path: '/login', element: }, { path: '*', element: }, ] export default routes ``` `pages/layout/index.jsx` ```tsx import { Outlet } from 'react-router-dom' ``` ## 21. 菜单高亮切换 > 能够在点击对应菜单时,保持对应菜单高亮 > > 1. 将 Menu 的 key 属性修改为与其对应的路由地址 > 2. 获取到当前正在访问页面的路由地址 > 3. 将当前路由地址设置为 selectedKeys 属性的值 ```tsx + import { useLocation, useNavigate } from 'react-router-dom' const GeekLayout = () => { + const history = useNavigate() + const location = useLocation() + const selectedKey = location.pathname // 菜单 const items = [ { label: '数据概览', key: '/', icon: }, { label: '内容管理', key: '/article', icon: }, { label: '发布文章', key: '/publish', icon: }, ] const onClick = (e) => { - // 跳转子路由 + history(e.key) } return ( // ... ) } ``` ## 22. 展示个人信息 `modules/user.js` ```js import request from "@/utils/request" import { createSlice } from "@reduxjs/toolkit" export const user = createSlice({ name: 'user', initialState: { info: {} }, reducers: { setUser(preState, action) { preState.info = action.payload } } }) export const { setUser } = user.actions export const getUserAction = () => { return async (dispatch, getState) => { const { data: { data }} = await request.get('/user/profile', { headers: { Authorization: `Bearer ${getState().login.token}` } }) dispatch(setUser(data)) } } export default user.reducer ``` `store/index.js` ```js import { configureStore } from "@reduxjs/toolkit" import login from './modules/login' import user from './modules/user' export default configureStore({ reducer: { login, user } }) ``` `Layout/index.jsx` ```jsx import { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { getUserAction } from '@/store/modules/user' const GeekLayout = () => { const dispatch = useDispatch() const user = useSelector(state => state.user) useEffect(() => { try { dispatch(getUserAction()) } catch {} }, [dispatch]) render() { return ( // ...
{user.info.name}
// ... ) } } ``` ## 23. 退出登录 `modules/login.js` ```js import { getToken, saveToken, clearToken } from "@/utils/token" export function logoutAction() { return (dispatch) => { dispatch(delToken()) clearToken() } } ``` `Layout/index.js` ```tsx import { logoutAction } from '@/store/modules/login' export default function GeekLayout() { const onLogout = () => { dispatch(logoutAction()) history('/login') } render() { return ( // ... 退出 // ... ) } } ``` ## 24. 请求统一添加token > 能够通过拦截器统一添加token > > 因为不管是登录时,还是每次刷新页面时,已经将 token 存储在 redux 中了, > > 所以,可以直接通过 `store.getState()` 来获取到 redux 状态 > > 1. 导入 store (!!! 可能导致循环引用,待解决, 暂时使用本地缓存...) > 2. 判断是否是登录请求 > 3. 如果是,不做任何处理 > 4. 如果不是,统一添加 Authorization 请求头 ```js // 前置拦截器 request.interceptors.request.use(config => { // 获取token const token = getToken() // 除登录请求外,其余请求统一加上token if (!config.url.startsWith('/authorizations')) { config.headers.Authorization = `Bearer ${token}` } return config }, err => { return Promise.reject(err) }) ``` ```js export const getUserAction = () => { return async (dispatch, getState) => { const { data: { data }} = await request.get('/user/profile') dispatch(setUser(data)) } } ``` ## 25. 处理token失效清空(看不懂) > 能够统一处理token失效重定向到登录页面 > > 为了能够**在非组件环境下拿到路由信息,进行路由跳转等操作**,需要使用路由中提供的 `Router` 组件,并自定义 `history` 对象 > > 1. 安装:`yarn add history` > 2. 创建 router/history.js 文件 > 3. 在该文件中,创建一个 hisotry 对象并导出 > 4. 在入口index.js 中导入 history 对象,并设置为 Router 的 history > 5. 通过响应拦截器处理 token 失效 `router/history.js` ```js /** * 获取react-router实例对象,在js中跳转页面 */ import { useState, useLayoutEffect } from 'react'; import { createBrowserHistory, createHashHistory } from 'history'; import { Router } from 'react-router-dom'; // 1. history // export const history = createBrowserHistory(); // 2. hash // == 创建路由实例对象 =》作用:js中使用跳转页面 == export const history = createHashHistory(); // == 函数组件 => 作用:包裹根组件,注册history export const HistoryRouter = ({ history, children }) => { const [state, setState] = useState({ action: history.action, location: history.location }); useLayoutEffect(() => { history.listen(setState); }, [history]); return }; ``` `index.js` ```js import React from 'react' import ReactDOM from 'react-dom/client' // 先导入 antd 样式文件 import 'antd/dist/antd.min.css' // 再导入全局样式文件,防止样式覆盖 import './index.scss' import App from './App' import { Provider } from 'react-redux' import store from './store' import { HistoryRouter, history } from '@/router/history' const root = ReactDOM.createRoot(document.getElementById('root')) root.render( ) ``` `utils/request.js` ```js // 响应拦截器 request.interceptors.response.use((config) => { return config }, err => { if (!err.response) { message.error('网络繁忙,请稍后重试!') return Promise.reject(err) } if (err.response.status === 401) { message.error(err.response.data?.message, 1.5, () => { // 删除token clearToken() customHistory.push('/login', { from: customHistory.location.pathname }) }) } return Promise.reject(err) }) ``` ## 26. 首页展示 `Home/index.module.scss` ```scss .root { width: 100%; height: 100%; background: #f5f5f5 url(../../assets/chart.png) no-repeat; } ``` `Home/index.tsx` ```jsx import React from 'react' import style from './index.module.scss' export default function Home() { return (
) } ``` ## 27. 文章筛选区域结构 `Article/index.jsx` ```tsx import { Link } from 'react-router-dom' import { Card, Breadcrumb, Form, Button, Radio, DatePicker, Select } from 'antd' import 'moment/locale/zh-cn' import locale from 'antd/es/date-picker/locale/zh_CN' const { Option } = Select const { RangePicker } = DatePicker export default function Arrticl() { return (
首页 内容管理 } style={{ marginBottom: 20 }} >
全部 草稿 待审核 审核通过 审核失败
) } ``` ## 28. 渲染文章表格 `article.js` ```js import request from "@/utils/request" import { createSlice } from "@reduxjs/toolkit" export const article = createSlice({ name: 'article', initialState: { channelList: [], articleList: [], query: { count: 0, page: 1, pageSize: 10 } }, reducers: { setChannelList(preState, action) { preState.channelList = action.payload }, setArticleList(preState, action) { preState.articleList = action.payload.results preState.query = { count: action.payload.total_count, page: action.payload.page, pageSize: action.payload.per_page } } } }) export const { setChannelList, setArticleList } = article.actions export function getChannelAction() { return async (dispatch) => { const { data: { data: { channels } } } = await request.get('/channels') dispatch(setChannelList(channels)) } } export function getArticleAction(params) { return async (dispatch) => { const { data: { data } } = await request.get('/mp/articles', { params }) dispatch(setArticleList(data)) } } export default article.reducer ``` `Article/index.jsx` ```jsx import { Link } from 'react-router-dom' import { Card, Breadcrumb, Form, Button, Radio, DatePicker, Select, Table, Tag, Space } from 'antd' import 'moment/locale/zh-cn' import locale from 'antd/es/date-picker/locale/zh_CN' import { EditOutlined, DeleteOutlined } from '@ant-design/icons' import img404 from '@/assets/error.png' import { useDispatch, useSelector } from 'react-redux' import { useEffect } from 'react' import { getArticleAction, getChannelAction } from '@/store/modules/article' const { Option } = Select const { RangePicker } = DatePicker export default function Arrticl() { // 优化文章状态的处理 const articleStatus = { 0: { color: 'yellow', text: '草稿' }, 1: { color: '#ccc', text: '待审核' }, 2: { color: 'green', text: '审核通过' }, 3: { color: 'red', text: '审核失败' }, } const columns = [ { title: '封面', dataIndex: 'cover', render: cover => { return }, }, { title: '标题', dataIndex: 'title', width: 220, }, { title: '状态', dataIndex: 'status', render: data => { const tagData = articleStatus[data] return {tagData.text} }, }, { title: '发布时间', dataIndex: 'pubdate', }, { title: '阅读数', dataIndex: 'read_count', }, { title: '评论数', dataIndex: 'comment_count', }, { title: '点赞数', dataIndex: 'like_count', }, { title: '操作', render: data => { return (