项目制作流程

发布于:2025-05-20 ⋅ 阅读:(21) ⋅ 点赞:(0)

一、使用 CRA 创建项目

npx create-react-app name

二、按照业务规范整理项目目录 (重点src目录)

三、安装插件

npm install sass -D

npm install antd --save

npm install react-router-dom

四、配置基础路由 Router

1. 安装路由包 react-router-dom

2. 准备两个基础路由组件 Layout 和 Login

3. 在 router/index.js 文件中引入组件进行路由配置,导出 router 实例

4. 在入口文件中渲染 <RouterProvider />,传入  router 实例

router/index.js

import { createBrowserRouter } from "react-router-dom";

// 配置路由实例
const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
  },
  {
    path: "/login",
    element: <Login />,
  },
]);

export default router;
import { RouterProvider } from "react-router-dom";
import router from "./router";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<RouterProvider router={router} />);

五、登录

使用 AntD 现成的组件 创建登录页的内容结构

 主要组件:Card、Form、Input、Button

    <div>
      <Card>
        <img  />
        {/* 登录表单 */}
        <Form>
          <Form.Item>
              <Input />
          </Form.Item> 
          <Form.Item>
              <Input />
          </Form.Item> 
        </Form>
      </Card>
    </div>

1. 表单校验实现

表单校验可以在提交登录之前,校验用户的输入是否符合预期,如果不符合就阻止提交,显示错误信息

    <Form.Item
      label="Username"
      name="username"
      rules={[{ required: true, message: '请输入用户名!' }]}
    >
      <Input size="large" placeholder="请输入手机号" />
    </Form.Item>

 增加失焦时校验

<Form validateTrigger="onBlur">
    ...
</Form>

手机号为有效格式

          <Form.Item
            name="mobile"
            // 多条校验逻辑 先校验第一条 第一条通过之后再校验第二条
            rules={[
              {
                required: true,
                message: '请输入手机号',
              },
              {
                pattern: /^1[3-9]\d{9}$/,
                message: '请输入正确的手机号格式'
              }
            ]}>
            <Input size="large" placeholder="请输入手机号" />
          </Form.Item>

2. 获取表单数据

当用户输入了正确的表单内容,点击确认按钮时,需要收集用户输入的内容,用来提交接口请求

解决方案:给 Form 组件绑定 onFinish 回调函数,通过回调函数的参数获取用户输入的内容

  const onFinish = async (values) => {
    console.log(values)
  }


  <Form onFinish={onFinish} validateTrigger="onBlur">
      ...
          <Form.Item>
            <Button type="primary" htmlType="submit" size="large" block>
              登录
            </Button>
          </Form.Item>
  </Form>

3. 封装 request 请求模块

 在整个项目中会发送很多网络请求,使用 axios 三方库做好统一封装,方便统一管理和复用

npm i axios

 utils/request.js

// axios的封装处理
import axios from "axios"

// 1. 根域名配置
// 2. 超时时间
// 3. 请求拦截器 / 响应拦截器

const request = axios.create({
  baseURL: 'http://geek.itheima.net/v1_0',
  timeout: 5000
})

// 添加请求拦截器
// 在请求发送之前 做拦截 插入一些自定义的配置 [参数的处理]
request.interceptors.request.use((config) => {
  return config
}, (error) => {
  return Promise.reject(error)
})

// 添加响应拦截器
// 在响应返回到客户端之前 做拦截 重点处理返回的数据
request.interceptors.response.use((response) => {
  // 2xx 范围内的状态码都会触发该函数。
  // 对响应数据做点什么
  return response.data
}, (error) => {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error)
})

export { request }

utils/index.js

// 统一中转工具模块函数
// import {request} from '@/utils'

import { request } from './request'

export {
  request,
}

4. 使用 redux 管理 token

Token 作为一个用户的标识数据,需要在很多个模块中共享,Redux 可以方便的解决共享问题

(1)redux 中编写获取 Token 的 异步获取和同步修改

(2)Login 组件负责提交 action 并且把表单数据传递过来

npm i react-redux @reduxjs/toolkit

store/modules/user.js

// 和用户相关的状态管理
import { createSlice } from '@reduxjs/toolkit'
import { setToken as _setToken, getToken } from '@/utils'

const userStore = createSlice({
  name: "user",
  // 数据状态
  initialState: {
    token: getToken() || '',
  },
  // 同步修改方法
  reducers: {
    setToken (state, action) {
      state.token = action.payload
      _setToken(action.payload)
    },
  }
})

// 解构出actionCreater
const { setToken } = userStore.actions

// 获取reducer函数
const userReducer = userStore.reducer

// 登录获取token异步方法封装
const fetchLogin = (loginForm) => {
  return async (dispatch) => {
    // 1. 发送异步请求
    const res = await request.post('authorizations', loginForm)
    // 2. 提交同步 action 进行 token 存入
    dispatch(setToken(res.data.token))
  }
}

export { fetchLogin, setToken }

export default userReducer

store/index.js

// 组合redux子模块 + 导出store实例
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './modules/user'

export default configureStore({
  reducer: {
    user: userReducer
  }
})

index.js

import { RouterProvider } from 'react-router-dom'
import router from './router'
import { Provider } from 'react-redux'
import store from './store'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <Provider store={store}>
    <RouterProvider router={router} />
  </Provider>
)

封装 localStorage - Token 持久化

现存问题:

Redux 存入 Token 之后如果刷新浏览器,Token 会丢失(持久化就是防止刷新时丢失 Token)

问题原因:

Redux 是基于浏览器内存的储存方式,刷新时状态恢复为初始值

utils/token.js

// 封装基于ls存取删三个方法
const TOKENKEY = 'token_key'

function setToken (token) {
  return localStorage.setItem(TOKENKEY, token)
}

function getToken () {
  return localStorage.getItem(TOKENKEY)
}

function removeToken () {
  return localStorage.removeItem(TOKENKEY)
}

export {
  setToken,
  getToken,
  removeToken
}

utils/index.js

// 统一中转工具模块函数
// import {request} from '@/utils'

import { request } from './request'
import { setToken, getToken, removeToken } from './token'

export {
  request,
  setToken,
  getToken,
  removeToken
}

pages/login/index.js

import { useDispatch } from 'react-redux'
import { fetchLogin } from '@/store/modules/user'
import { useNavigate } from 'react-router-dom'

const Login = () => {
  const dispatch = useDispatch()
  const navigate = useNavigate()
  const onFinish = async (values) => {
    console.log(values)
    // 触发异步action fetchLogin
    await dispatch(fetchLogin(values))
    // 1. 跳转到首页
    navigate('/')
    // 2. 提示一下用户
    message.success('登录成功')
  }
 
  return (...)
}

5. Axios 请求拦截器注入 Token

Token 作为用户的一个标识数据,后端很多接口都会以它作为接口权限判断的依据;请求拦截器注入 Token 之后,所有用到 Axios 实例的接口请求都自动携带了 Token

utils/request.js

import { getToken } from "./token"

// 添加请求拦截器
// 在请求发送之前 做拦截 插入一些自定义的配置 [参数的处理]
request.interceptors.request.use((config) => {
  // 操作这个config 注入token数据
  // 1. 获取到token
  // 2. 按照后端的格式要求做token拼接
  const token = getToken()
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
}, (error) => {
  return Promise.reject(error)
})

6. 使用 Token 做路由权限控制

有些路由页面的内容信息比较敏感,如果用户没有经过登录获取到有效 Token,是没有权限跳转的,根据 Token 的有无控制当前路由是否可以跳转,就是路由的权限控制

components/AuthRoute.js

// 封装高阶组件
// 核心逻辑: 有token 正常跳转  无token 去登录

import { getToken } from '@/utils'
import { Navigate } from 'react-router-dom'

export function AuthRoute ({ children }) {
  const token = getToken()
  if (token) {
    return <>{children}</>
  } else {
    return <Navigate to={'/login'} replace />
  }
}

router/index.js

import { createBrowserRouter } from 'react-router-dom'
import { AuthRoute } from '@/components/AuthRoute'

// 配置路由实例
const router = createBrowserRouter([
  {
    path: "/",
    element: <AuthRoute> <Layout /></AuthRoute>,
  },
  {
    path: "/login",
    element: <Login />
  }
])

export default router

六、Layout

1. 样式初始化

样式 reset

npm install normalize.css

index.js

import 'normalize.css'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <Provider store={store}>
    <RouterProvider router={router} />
  </Provider>
)

index.scss

html,
body {
  margin: 0;
  height: 100%;
}

#root {
  height: 100%;
}

2.  二级路由配置

(1)准备三个二级路由

(2)router 中通过 children 配置项进行配置

(3)Layout 组件中配置二级路由出口

router/index.js

const router = createBrowserRouter([
  {
    path: "/",
    element: <AuthRoute> <Layout /></AuthRoute>,
    children: [
      {
        index: true,
        element: <Home />
      },
      {
        path: 'article',
        element: <Article />
      },
      {
        path: 'publish',
        element: <Publish />
      }
    ]
  },
  {
    path: "/login",
    element: <Login />
  }
])

export default router

pages/Layout/index.js

    <Layout>
      <Header>
        ...
      </Header>
      <Layout>
        <Sider>
          <Menu></Menu>
        </Sider>
        <Layout style={{ padding: 20 }}>
          {/* 二级路由的出口 */}
          <Outlet />
        </Layout>
      </Layout>
    </Layout>

3. 点击菜单跳转路由

实现效果:点击左侧菜单可以跳转到对应的目标路由

思路分析:

(1)左侧菜单要和路由形成一一对应的关系

(2)点击时拿到路由路径,调用路由方法跳转(跳转到对应的路由下面)

 具体操作:

(1)菜单参数 Item 中 key 属性换成路由的路由地址

(2)点击菜单时通过 key 获取路由地址跳转

 pages/Layout/index.js

const items = [
  {
    label: '首页',
    key: '/',
    icon: <HomeOutlined />,
  },
  {
    label: '文章管理',
    key: '/article',
    icon: <DiffOutlined />,
  },
  {
    label: '创建文章',
    key: '/publish',
    icon: <EditOutlined />,
  },
]

const GeekLayout = () => {
  const navigate = useNavigate()
  const onMenuClick = (route) => {
    const path = route.key
    navigate(path)
  }

  return (
    <Layout>
      <Header className="header">
       ...
      </Header>
      <Layout>
        <Sider>
          <Menu
            mode="inline"
            theme="dark"
            defaultSelectedKeys={['1']}
            onClick={onMenuClick}
            items={items}
            style={{ height: '100%', borderRight: 0 }}></Menu>
        </Sider>
        <Layout>
          {/* 二级路由的出口 */}
          <Outlet />
        </Layout>
      </Layout>
    </Layout>
  )
}
export default GeekLayout

4. 根据当前路由路径高亮菜单

 实现效果:页面在刷新时可以根据当前的路由路径让对应的左侧菜单高亮显示

思路分析;

(1)获取当前 url 上的路由路径

(2)找到菜单组件负责高亮的属性,绑定当前的路由路径

  // 反向高亮
  // 1. 获取当前路由路径
  const location = useLocation()
  console.log(location.pathname)
  const selectedkey = location.pathname


        <Sider width={200} className="site-layout-background">
          <Menu
            mode="inline"
            theme="dark"
           // 修改
            selectedKeys={selectedkey}
            onClick={onMenuClick}
            items={items}
            style={{ height: '100%', borderRight: 0 }}></Menu>
        </Sider>

5. 展示个人信息

关键问题:用户信息应该放到哪里维护?

和 Token 令牌类似,用户的信息通常很有可能在多个组件中都需要共享使用,所以同样应该放到Redux 中维护

(1)使用 Redux 进行信息管理

(2)Layout 组件中提交 action

(3)Layout 组件中完成渲染

store/modules/user.js

// 和用户相关的状态管理
import { createSlice } from '@reduxjs/toolkit'
import { setToken as _setToken, getToken, removeToken } from '@/utils'
import { loginAPI, getProfileAPI } from '@/apis/user'

const userStore = createSlice({
  name: "user",
  // 数据状态
  initialState: {
    token: getToken() || '',
    userInfo: {}
  },
  // 同步修改方法
  reducers: {
    setToken (state, action) {
      state.token = action.payload
      _setToken(action.payload)
    },
    setUserInfo (state, action) {
      state.userInfo = action.payload
    },
  }
})

// 解构出actionCreater
const { setToken, setUserInfo } = userStore.actions

// 获取reducer函数
const userReducer = userStore.reducer

// 登录获取token异步方法封装
const fetchLogin = (loginForm) => {
  return async (dispatch) => {
    const res = await request.post('/authorizations', loginForm)
    dispatch(setToken(res.data.token))
  }
}

// 获取个人用户信息异步方法
const fetchUserInfo = () => {
  return async (dispatch) => {
    const res = await request.get('/user/profile')
    dispatch(setUserInfo(res.data))
  }
}

export { fetchLogin, fetchUserInfo }

export default userReducer

pages/Layout/index.js

  // 触发个人用户信息action
  const dispatch = useDispatch()
  useEffect(() => {
    dispatch(fetchUserInfo())
  }, [dispatch])

  const name = useSelector(state => state.user.userInfo.name)

   
  <span className="user-name">{name}</span>

6. 退出登录

(1)提示用户是否确认要退出(危险操作,二次确认)

(2)用户确认之后清除用户信息(Token 以及其他个人信息)

(3)跳转到登录页(为下次登录做准备)

pages/Layout/index.js

  // 退出登录确认回调
  const onConfirm = () => {
    console.log('确认退出')
    dispatch(clearUserInfo())
    navigate('/login')
  }
  ...
    <span className="user-logout">
       <Popconfirm title="是否确认退出?" okText="退出" cancelText="取消" onConfirm={onConfirm}>
          <LogoutOutlined /> 退出
      </Popconfirm>
    </span>

store/modules/user.js

  // 同步修改方法
  reducers: {
    setToken (state, action) {
      state.token = action.payload
      _setToken(action.payload)
    },
    setUserInfo (state, action) {
      state.userInfo = action.payload
    },
    clearUserInfo (state) {
      state.token = ''
      state.userInfo = {}
      removeToken()
    }
  }
})

7. 处理 Token 失效

什么是 Token 失效?

为了用户的安全和隐私考虑,在用户长时间未在网络中做出任何操作规定的失效时间到达之后,当前的 Token 就会失效。一旦失效,不能再作为用户令牌标识请求隐私数据

前端如何知道 Token 已经失效了?

通常在 Token 失效之后再去请求接口,后端会返回401状态码,前端可以监控这个状态,做后续的操作

Token 失效了前端做什么?

(1)在 axios 拦截中监控 401 状态码

(2)清除失效 Token, 跳转登录

utils/request.js

import router from "@/router"

// 添加响应拦截器
// 在响应返回到客户端之前 做拦截 重点处理返回的数据
request.interceptors.response.use((response) => {
  // 2xx 范围内的状态码都会触发该函数。
  // 对响应数据做点什么
  return response.data
}, (error) => {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  // 监控 401 token失效
  console.dir(error)
  if (error.response.status === 401) {
    removeToken()
    router.navigate('/login')
   // 强制刷新
    window.location.reload()
  }
  return Promise.reject(error)
})

七、Home

1. Echarts 基础图表渲染

三方图表插件如何在项目中快速使用起来?

(1)按照图表插件文档中的“快速开始”,快速跑起来 Demo

(2)按照业务需求修改配置项做定制处理

npm install echarts

pages/Home/index.js

// 柱状图组件
import * as echarts from 'echarts'
import { useEffect, useRef } from 'react'

const Home = () => {
  const chartRef = useRef(null)
  useEffect(() => {
    // 保证dom可用 才进行图表的渲染
    // 1. 获取渲染图表的dom节点
    const chartDom = chartRef.current

    // 2. 图表初始化生成图表实例对象
    const myChart = echarts.init(chartDom)

    // 3. 准备图表参数
    const option = {
      title: {
        text: title
      },
      xAxis: {
        type: 'category',
        data: ['Vue', 'React', 'Angular']
      },
      yAxis: {
        type: 'value'
      },
      series: [
        {
          data: [10, 40, 70],
          type: 'bar'
        }
      ]
    }
    // 4. 使用图表参数完成图表的渲染
    option && myChart.setOption(option)

  }, [title])

  return (
     <div ref={chartRef} style={{ width: '500px', height: '400px' }}></div>
  )
}

export default Home

2. Echarts 组件封装实现

pages/Home/index.js 

import BarChart from "./components/BarChart"

const Home = () => {

  return (
    <div>
      <BarChart title={'三大框架满意度'} />
      <BarChart title={'三大框架使用度'} />
    </div>
  )
}

export default Home

pages/Home/components/BarCharts.js

// 柱状图组件
import * as echarts from 'echarts'
import { useEffect, useRef } from 'react'
// 1. 把功能代码都放到这个组件中
// 2. 把可变的部分抽象成prop参数

const BarChart = ({ title }) => {
  const chartRef = useRef(null)
  useEffect(() => {
    // 保证dom可用 才进行图表的渲染
    // 1. 获取渲染图表的dom节点
    const chartDom = chartRef.current

    // 2. 图表初始化生成图表实例对象
    const myChart = echarts.init(chartDom)

    // 3. 准备图表参数
    const option = {
      title: {
        text: title
      },
      xAxis: {
        type: 'category',
        data: ['Vue', 'React', 'Angular']
      },
      yAxis: {
        type: 'value'
      },
      series: [
        {
          data: [10, 40, 70],
          type: 'bar'
        }
      ]
    }
    // 4. 使用图表参数完成图表的渲染
    option && myChart.setOption(option)

  }, [title])
  return <div ref={chartRef} style={{ width: '500px', height: '400px' }}></div>
}

export default BarChart

八、拓展 - API 模块封装

现存问题:

当前的接口请求放到了功能实现的位置,没有在固定的模块内维护,后期查找维护困难

解决思路:

把项目中的所有接口按照业务模块以函数的形式统一封装到 apis 模块中

apis/user.js

// 用户相关的所有请求
import { request } from "@/utils"
// 1. 登录请求
export function loginAPI (formData) {
  return request({
    url: '/authorizations',
    method: 'POST',
    data: formData
  })
}

// 2. 获取用户信息
export function getProfileAPI () {
  return request({
    url: '/user/profile',
    method: 'GET'
  })
}

store/modules/user.js

import { loginAPI, getProfileAPI } from '@/apis/user'

// 登录获取token异步方法封装
const fetchLogin = (loginForm) => {
  return async (dispatch) => {
    const res = await loginAPI(loginForm)
    dispatch(setToken(res.data.token))
  }
}

// 获取个人用户信息异步方法
const fetchUserInfo = () => {
  return async (dispatch) => {
    const res = await getProfileAPI()
    dispatch(setUserInfo(res.data))
  }
}

九、文章发布

1. 创建并熟悉基础结构

(1)面包屑导航组件 Breadcrumb

(2)表单组件 Form

(3)输入框组件 Input

(4)下拉框组件 Select - Option

(5)按钮组件 Button

2. 准备富文本编辑器

(1)安装 react-quill 富文本编辑器

// react 18
npm i react-quill@2.0.0-beta.2

// react 19
npm i react-quill-new --save

(2)导入编辑器组件和配套样式文件

(3)渲染编辑器组件

(4)调整编辑器组件样式

pages/Publish/index.js

import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'

const { Option } = Select

const Publish = () => {
  
  return (
    <div className="publish">
      <Card
        title={
          <Breadcrumb items={[
            { title: <Link to={'/'}>首页</Link> },
            { title: `${articleId ? '编辑' : '发布'}文章` },
          ]}
          />
        }
      >
        <Form
          labelCol={{ span: 4 }}
          wrapperCol={{ span: 16 }}
          initialValues={{ type: 1 }}
        >
          <Form.Item
            label="标题"
            name="title"
            rules={[{ required: true, message: '请输入文章标题' }]}
          >
            <Input placeholder="请输入文章标题" style={{ width: 400 }} />
          </Form.Item>

          <Form.Item
            label="频道"
            name="channel_id"
            rules={[{ required: true, message: '请选择文章频道' }]}
          >
            <Select placeholder="请选择文章频道" style={{ width: 400 }}>
              <Option value={0}>推荐</Option>
            </Select>
          </Form.Item>

          <Form.Item
            label="内容"
            name="content"
            rules={[{ required: true, message: '请输入文章内容' }]}
          >
            {/* 富文本编辑器 */}
            <ReactQuill
              className="publish-quill"
              theme="snow"
              placeholder="请输入文章内容"
            />
          </Form.Item>

          <Form.Item wrapperCol={{ offset: 4 }}>
            <Space>
              <Button size="large" type="primary" htmlType="submit">
                发布文章
              </Button>
            </Space>
          </Form.Item>
        </Form>
      </Card>
    </div>
  )
}

export default Publish

3. 频道数据获取渲染

(1)根据接口文档在 APIS 模块中封装接口函数

(2)使用 useState 维护数据

(3)在 useEffect 中调用接口获取数据并存入 state

(4)绑定数据到下拉框组件

apis/article.js

// 封装和文章相关的接口函数
import { request } from "@/utils"

// 1. 获取频道列表
export function getChannelAPI () {
  return request({
    url: '/channels',
    method: 'GET'
  })
}

// 2. 提交文章表单
export function createArticleAPI (data) {
  return request({
    url: '/mp/articles?draft=false',
    method: 'POST',
    data
  })
}

pages/Publish/index.js  

  // 获取频道列表
  const [channelList, setChannelList] = useState([])

  useEffect(() => {
    // 1. 封装函数 在函数体内调用接口
    const getChannelList = async () => {
      const res = await getChannelAPI()
      setChannelList(res.data.channels)
    }
    // 2. 调用函数
    getChannelList()
  }, [])

pages/Publish/index.js   

<Form.Item
 label="频道"
 name="channel_id"
 rules={[{ required: true, message: '请选择文章频道' }]}
>
 <Select placeholder="请选择文章频道" style={{ width: 400 }}>
   {/* value属性用户选中之后会自动收集起来作为接口的提交字段 */}
   {channelList.map(item => <Option key={item.id} value={item.id}>{item.name}</Option>)}
 </Select>
</Form.Item>

4. 收集表单数据提交表单

(1)使用 Form 组件收集表单数据

(2)按照接口文档封装接口函数

(3)按照接口文档处理表单数据

(4)提交接口并验证是否成功

pages/Publish/index.js

  // 提交表单
  const onFinish = (formValue) => {
    console.log(formValue)
    const { title, content, channel_id } = formValue
 
    // 1. 按照接口文档的格式处理收集到的表单数据
    const reqData = {
      title,
      content,
      cover: {
        type: 0,
        images: [],
      },
      channel_id
    }
    // 2. 调用接口提交
      createArticleAPI(reqData)
  }

5. 上传文章封面基础功能实现

(1)使用现成组件搭建结构

(2)按照 Upload 组件添加配置实现上传

pages/Publish/index.js   

  // 上传回调
  const [imageList, setImageList] = useState([])
  const onChange = (value) => {
    console.log('正在上传中', value)
    setImageList(value.fileList)
  }

          <Form.Item label="封面">
            <Form.Item name="type">
              <Radio.Group>
                <Radio value={1}>单图</Radio>
                <Radio value={3}>三图</Radio>
                <Radio value={0}>无图</Radio>
              </Radio.Group>
            </Form.Item>
            {/* 
              listType: 决定选择文件框的外观样式
              showUploadList: 控制显示上传列表
            */}
            <Upload
              listType="picture-card"
              showUploadList
              action={'http://geek.itheima.net/v1_0/upload'}
              name='image'
              onChange={onChange}
            >
              <div style={{ marginTop: 8 }}>
                <PlusOutlined />
              </div>
            </Upload>
          </Form.Item>

6. 实现切换封面类型

实现效果:只有当前模式为单图或者三图模式时才显示上传组件

(1)获取到当前的封面类型

(2)对上传组件进行条件渲染

pages/Publish/index.js   

  // 切换图片封面类型
  const [imageType, setImageType] = useState(0)
  const onTypeChange = (e) => {
    console.log('切换封面了', e.target.value)
    setImageType(e.target.value)
  }
        ...
        <Form
          labelCol={{ span: 4 }}
          wrapperCol={{ span: 16 }}
         // 控制单选框区域 初始的时候为 0,图片数量为 0
          initialValues={{ type: 0 }}
          onFinish={onFinish}
          form={form}
        >
         ...
          <Form.Item label="封面">
            <Form.Item name="type">
              <Radio.Group onChange={onTypeChange}>
                <Radio value={1}>单图</Radio>
                <Radio value={3}>三图</Radio>
                <Radio value={0}>无图</Radio>
              </Radio.Group>
            </Form.Item>
            {/* 
              listType: 决定选择文件框的外观样式
              showUploadList: 控制显示上传列表
            */}
            {imageType > 0 && <Upload
              listType="picture-card"
              showUploadList
              action={'http://geek.itheima.net/v1_0/upload'}
              name='image'
              onChange={onChange}
              maxCount={imageType}
              fileList={imageList}
            >
              <div style={{ marginTop: 8 }}>
                <PlusOutlined />
              </div>
            </Upload>}
          </Form.Item>
         ...
        </Form>

7. 控制上传图片的数量

实现的效果:

(1)单图模式时,最多能上传一张图片

(2)三图模式时,最多能上传三张图片

如何实现:

(1)找到限制上传数量的组件属性

(2)使用 imageType 进行绑定控制

pages/Publish/index.js   

            {imageType > 0 && <Upload
              listType="picture-card"
              showUploadList
              action={'http://geek.itheima.net/v1_0/upload'}
              name='image'
              onChange={onChange}
            // 控制图片上传数量
              maxCount={imageType}
              fileList={imageList}
            >
              <div style={{ marginTop: 8 }}>
                <PlusOutlined />
              </div>
            </Upload>}

8. 发布带封面的文章

pages/Publish/index.js   

  // 提交表单
  const onFinish = (formValue) => {
    console.log(formValue)
    // 校验封面类型imageType是否和实际的图片列表imageList数量是相等的
    if (imageList.length !== imageType) return message.warning('封面类型和图片数量不匹配')
   
    const { title, content, channel_id } = formValue
    // 1. 按照接口文档的格式处理收集到的表单数据
    const reqData = {
      title,
      content,
      cover: {
        type: imageType, // 封面模式
        images: imageList.map(item => item.response.data.url), // 图片列表
      },
      channel_id
    }
    // 2. 调用接口提交
     createArticleAPI(reqData)
   }

十、文章列表模块

1. 功能描述和静态结构创建

 pages/Article/index.js

// 引入汉化包 时间选择器显示中文
import locale from 'antd/es/date-picker/locale/zh_CN'

const Article = () => {
  ...
  return (
    <div>
      <Card
        title={
          <Breadcrumb items={[
            { title: <Link to={'/'}>首页</Link> },
            { title: '文章列表' },
          ]} />
        }
        style={{ marginBottom: 20 }}
      >
        <Form initialValues={{ status: null }}>
          <Form.Item label="状态" name="status">
            <Radio.Group>
              <Radio value={''}>全部</Radio>
              <Radio value={1}>待审核</Radio>
              <Radio value={2}>审核通过</Radio>
            </Radio.Group>
          </Form.Item>

          <Form.Item label="频道" name="channel_id">
            <Select
              placeholder="请选择文章频道"
              style={{ width: 120 }}
            >
             <Option value="jack">jack</Option>
             <Option value="jack">jack</Option>
            </Select>
          </Form.Item>

          <Form.Item label="日期" name="date">
            {/* 传入locale属性 控制中文显示*/}
            <RangePicker locale={locale}></RangePicker>
          </Form.Item>

          <Form.Item>
            <Button type="primary" htmlType="submit" style={{ marginLeft: 40 }}>
              筛选
            </Button>
          </Form.Item>
        </Form>
      </Card>
      {/* 表格区域 */}
      <Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
        <Table rowKey="id" columns={columns} dataSource={data} />
      </Card>
    </div>
  )
}

export default Article

2. 渲染频道数据

在文章管理里面和创建文章里面都要使用 channelList,把这段代码封装成 hook

使用自定义 hook

(1)创建一个 use 打头的函数

(2)在函数中封装业务逻辑,并 return 出组件中要用到的状态数据

(3)组件中导入函数执行并结构状态数据使用

hooks/useChannel.js

// 封装获取频道列表的逻辑
import { useState, useEffect } from 'react'
import { getChannelAPI } from '@/apis/article'
function useChannel () {
  // 1. 获取频道列表所有的逻辑
  // 获取频道列表
  const [channelList, setChannelList] = useState([])

  useEffect(() => {
    // 1. 封装函数 在函数体内调用接口
    const getChannelList = async () => {
      const res = await getChannelAPI()
      setChannelList(res.data.channels)
    }
    // 2. 调用函数
    getChannelList()
  }, [])
  // 2. 把组件中要用到的数据return出去
  return {
    channelList
  }
}

export { useChannel }

pages/Publish/index.js   

import { useChannel } from '@/hooks/useChannel'

const Publish = () => {
  // 获取频道列表
  const { channelList } = useChannel()
  ...
}

 pages/Article/index.js

import { useChannel } from '@/hooks/useChannel'

const Article = () => {
  const { channelList } = useChannel()
...
  return (
    <div>
       ...
      <Form.Item label="频道" name="channel_id">
         <Select
            placeholder="请选择文章频道"
            style={{ width: 120 }}
         >
           {channelList.map(item => <Option key={item.id} value={item.id}>{item.name}</Option>)}
         </Select>
       </Form.Item>
      ...
    </div>
  )
}

export default Article

3. 渲染 table 文章列表

(1)封装请求接口

(2)使用 useState 维护状态数据

(3)使用 useEffect 发送请求

(4)在组件上绑定对应属性完成渲染

apis/articles.js

// 获取文章列表
export function getArticleListAPI (params) {
  return request({
    url: "/mp/articles",
    method: 'GET',
    params
  })
}

pages/Article/index.js

const Article = () => {
  // 获取文章列表
  const [list, setList] = useState([])
  const [count, setCount] = useState(0)
  useEffect(() => {
    async function getList () {
      const res = await getArticleListAPI(reqData)
      setList(res.data.results)
      setCount(res.data.total_count)
    }
    getList()
  }, [reqData])
...
  return (
      {/* 表格区域 */}
      <Card title={`根据筛选条件共查询到 ${count} 条结果:`}>
        <Table rowKey="id" columns={columns} dataSource={list} />
      </Card>
  )
}

4. 适配文章状态

实现效果:根据文章不同状态在状态列显示不同 Tag

实现思路:

(1)如果要适配的状态只有两个 - 三元条件渲染

(2)如果要适配的状态有多个 - 枚举渲染

const Article = () => {
  const navigate = useNavigate()
  const { channelList } = useChannel()
  // 准备列数据
  // 定义状态枚举
  const status = {
    1: <Tag color='warning'>待审核</Tag>,
    2: <Tag color='success'>审核通过</Tag>,
  }
  const columns = [
   ...
    {
      title: '状态',
      dataIndex: 'status',
      // data - 后端返回的状态status 根据它做条件渲染
      // data === 1 => 待审核
      // data === 2 => 审核通过
      render: data => status[data]
    },
   ...
  ]


网站公告

今日签到

点亮在社区的每一天
去签到