主要职能
这是一个react+antdpro后台管理项目,我的主要职责:
- 规划发布项目的架构、开发环境、工具,更新云工程(原商品模块),封装oss
- 了解react的生命周期,使用过api,复用组件
- 合并分支,更新迭代,优化debug,带领团队完成进度,部署项目
方案选型
因为随机分配的小组没有大佬,大部分人都只有三件套或B4、jQ的经验,我们一致认为对于结营项目,最好的方法是从B站上找一个教程,分工完成,视频地址:React项目全程实录#电商项目#react+UmiJS+Antd Pro#React全套技术#哔哩哔哩_bilibili
完成过程
- 云工程(商品列表)
- 上架与推荐
- 添加云工程(商品页面)
- 处理云工程分类(商品分类)
- 封装OSS
- 使用并集成富文本器
- 项目总结与优化
我写的代码 的结构简介图解
参考文档umijs/umi-request: A request tool based on fetch. (github.com)
商品模块共有4个代码文件:
src\pages\Projects\index.jsx
//主页面
import....区域
const Index = () => {
// 将表单初始化的值设置成状态,
const [isModalVisible, setIsModalVisible] = useState(false)
const [editId, setEditId] = useState(undefined)
// 表格的ref, 便于自定义操作表格,ref属性其实就是为了获取DOM节点
const actionRef = useRef()...
//一个异步函数获取数据
const getData = async (params) =>...
//上架和下架商品;推荐和不推荐商品;控制模态框显示和隐藏的三个函数
const handleIsOn;const handleIsRecommend;const isShowModal
//商品属性
const columns = [ ]
return (
<PageContainer>
<ProTable
//这个区域进行传参和添加商品按钮
!isModalVisible ? '' :
<CreateOrEdit
isModalVisible={isModalVisible}
isShowModal={isShowModal}
actionRef={actionRef}
editId={editId}
//引入新建功能的组件
/>
}
</PageContainer>
};
export default Index;
src\services\goods.js
//商品功能的实现
写法的来源参考ProTable - 高级表格 - ProComponents
import request from '@/utils/request';
//定义了5个功能,这里就列出一个获取商品列表
export async function getGoods(params) {
return request('/admin/goods', {params});
}
src\pages\Projects\components\CreateOrEdit.jsx
//添加商品的模态框
import 一大片
const CreateOrEdit = (props) => {
// 将表单初始化的值设置成状态, 在编辑的时候, 使用这个状态
const { isModalVisible } = props // 模态框是否显示
......这里定义了一大片功能// 添加 或者 编辑 的描述文字
const type = editId === undefined ? '添加' : '编辑'if (response.status === undefined) {
// 关闭模态框
....
}
}return (
<Modal
title={`${type}商品`}
visible={isModalVisible}
destroyOnClose={true}><ProFormText
//一大片功能,用antd的form实现
/>
<div>
<OSSUpload>
<Button icon={<UploadOutlined />}>点击上传商品主图</Button>
</OSSUpload>{url}
</div>
</Modal>
);
};export default CreateOrEdit;
src\components\OSS\index.jsx
//这个是可以复用的oss上传组件
import 区域....
export default class OSSUpload
{
//同时在创建模态框那个组件里引入这个上传组件,并添加上传按钮等form
//组件挂载完成后, 进行初始化, 获取oss配置
async componentDidMount() {
await this.init();
}//这是一个异步函数使用的例子,await
init = async () =>
//回调函数的例子
render() {
const { value, accept, showUploadList } = this.props;
const props = {
....
};
return (
<Upload {...props}>
<Button icon={<UploadOutlined />}>点击上传</Button>
</Upload>
);
}
完成步骤
云工程(商品列表)
首先在路由界面添加页面,component(创建模态框的那个),然后在内置的国际化文件添加翻译
上架与推荐
根据后端api写good.js
api文档 - (https://www.showdoc.com.cn/1824645173761431/8414274620688955)(私有整体说明文档访问密码为:666666)
添加云工程(商品页面)
具体如何实现可以看上面的index.jsx图解或文末源码
处理云工程分类(商品分类)
在 src\pages\Projects\components\CreateOrEdit.jsx中
使用了antdpro高级表格的Item
<ProForm.Item
name="category_id"
label="分类"
rules={[{required: true, message: '请选择分类'}]}
>
<Cascader
fieldNames={ {label: 'name', value: 'id'} }
options={options}
placeholder="请选择分类"
/>
</ProForm.Item>
封装OSS(云对象储存)
使用原因:储存式云盘与服务器分离可以大大减少花销;上传下载不经过服务器,只请求授权
将阿里云oss使用代码模板复制来,并进行修改
使用并集成富文本器
项目总结与优化
主要源码
src/pages/index.jsx
import React, {useRef, useState} from 'react';
import {PageContainer} from '@ant-design/pro-layout'
import ProTable from '@ant-design/pro-table'
import {Button, Image, Switch, Alert, message, Tabs} from 'antd'
import {PlusOutlined} from '@ant-design/icons'
import {getGoods, isOn, isRecommend} from '@/services/goods'
import CreateOrEdit from '../Userlist/components/CreateOrEdit'
const Index = () => {
// 将表单初始化的值设置成状态,
const [isModalVisible, setIsModalVisible] = useState(false)
const [editId, setEditId] = useState(undefined)
// 表格的ref, 便于自定义操作表格
const actionRef = useRef()
/**
* 获取商品列表数据
*
* @param params
* @returns {Promise<{total: *, data: *, success: boolean}>}
*/
const getData = async (params) => {
const response = await getGoods(params)
return {
data: response.data,
success: true,
total: response.meta.pagination.total,
};
}
/**
* 上架和下架商品
*
* @param goodsId
* @returns {Promise<void>}
*/
const handleIsOn = async (goodsId) => {
const response = await isOn(goodsId)
if (response.status === undefined) message.success('操作成功')
}
/**
* 推荐和不推荐商品
*
* @param goodsId
* @returns {Promise<void>}
*/
const handleIsRecommend = async (goodsId) => {
const response = await isRecommend(goodsId)
if (response.status === undefined) message.success('操作成功')
}
/**
* 控制模态框显示和隐藏
*/
const isShowModal = (show, id = undefined) => {
setEditId(id)
setIsModalVisible(show)
}
const columns = [
{
title: '商品图',
dataIndex: 'cover_url',
hideInSearch: true,
render: (_, record) =>
<Image
width={64}
src={record.cover_url}
placeholder={
<Image
preview={false}
src={record.cover_url}
width={200}
/>
}
/>
},
{
title: '标题',
dataIndex: 'title'
},
{
title: '价格',
dataIndex: 'price',
hideInSearch: true
},
{
title: '库存',
dataIndex: 'stock',
hideInSearch: true
},
{
title: '销量',
dataIndex: 'sales',
hideInSearch: true
},
{
title: '是否上架',
dataIndex: 'is_on',
render: (_, record) => <Switch
checkedChildren="已上架"
unCheckedChildren="未上架"
defaultChecked={record.is_on === 1}
onChange={ () => handleIsOn(record.id)}
/>,
valueType: 'radioButton',
valueEnum: {
1: {text: '已上架'},
0: {text: '未上架'},
}
},
{
title: '是否推荐',
dataIndex: 'is_recommend',
render: (_, record) => <Switch
checkedChildren="已推荐"
unCheckedChildren="未推荐"
defaultChecked={record.is_recommend === 1}
onChange={ () => handleIsRecommend(record.id)}
/>,
valueType: 'radioButton',
valueEnum: {
1: {text: '已推荐'},
0: {text: '未推荐'},
}
},
{
title: '创建时间',
dataIndex: 'created_at',
hideInSearch: true
},
{
title: '操作',
render: (_, record) => <a onClick={ () => isShowModal(true, record.id) }>编辑</a>
},
]
return (
<PageContainer>
<ProTable
columns={columns}
actionRef={actionRef}
request={(params = {}) => getData(params) }
rowKey="id"
search={ {
labelWidth: 'auto',
} }
pagination={ {
pageSize: 10,
} }
dateFormatter="string"
headerTitle="用户列表"
toolBarRender={() => [
<Button key="button" icon={<PlusOutlined />} type="primary" onClick={() => isShowModal(true)}>
新建
</Button>,
]}
/>
{
// 模态框隐藏的时候, 不挂载组件; 模态显示时候再挂载组件, 这样是为了触发子组件的生命周期
!isModalVisible ? '' :
<CreateOrEdit
isModalVisible={isModalVisible}
isShowModal={isShowModal}
actionRef={actionRef}
editId={editId}
/>
}
</PageContainer>
);
};
export default Index;
src/utils/request
略
src/service/good.js
import request from '@/utils/request';
/**
关于 request 第二参数 options, 常用的两个传参方式:
1. params 传参, 也就是query传参, 多用于 get 请求, 查询数据使用, 类型是对象或者 URLSearchParams
2. data 传参, 也就是body传参, 多用于提交表单数据, 类型是 any, 推荐使用对象
/**
* 获取商品列表
*
* @returns {Promise<void>}
*/
export async function getGoods(params) {
return request('/admin/goods', {params});
}
/**
* 上架和下架商品
*
* @param goodsId 商品 id
* @returns {Promise<any>}
*/
export async function isOn(goodsId) {
return request.patch(`/admin/goods/${goodsId}/on`)
}
/**
* 推荐和不推荐商品
*
* @param goodsId 商品 id
* @returns {Promise<any>}
*/
export async function isRecommend(goodsId) {
return request.patch(`/admin/goods/${goodsId}/recommend`)
}
/**
* 添加商品
*
* @param params
* @returns {Promise<void>}
*/
export async function addGoods(data) {
return request.post('/admin/goods', {data})
}
/**
* 商品详情
*
* @param editId
* @returns {Promise<void>}
*/
export async function showGoods(editId) {
return request.get(`/admin/goods/${editId}?include=category`)
}
/**
* 更新商品
*
* @returns {Promise<void>}
* @param editId
* @param data
*/
export async function updateGoods(editId, data) {
return request.put(`/admin/goods/${editId}`, {data})
}
创建模态框:
src\pages\Projects\components\CreateOrEdit.jsx
import React, {useEffect, useState} from 'react';
import ProForm, {
ProFormText,
ProFormTextArea,
ProFormDigit,
} from "@ant-design/pro-form";
import {message, Modal, Skeleton, Cascader, Button, Image} from "antd";
import {getCategory} from '@/services/category'
import {addGoods, showGoods, updateGoods} from '@/services/goods'
import {UploadOutlined} from "@ant-design/icons";
import Editor from '@/components/Editor'
import OSSUpload from "@/components/OSS";
const CreateOrEdit = (props) => {
// 将表单初始化的值设置成状态, 在编辑的时候, 使用这个状态
const [initialValues, setInitialValues] = useState(undefined)
const [options, setOptions] = useState([])
// 定义Form实例, 用来操作表单
const [formObj] = ProForm.useForm()
// 设置表单的值
// formObj.setFieldsValue({fieldName: value})
const { isModalVisible } = props // 模态框是否显示
const { isShowModal } = props // 操作模态框显示隐藏的方法
const { actionRef } = props // 父组件传过来的表格的引用, 可以用来操作表格, 比如刷新表格
const { editId } = props // 要编辑的ID, 添加的时候是undefined, 只有编辑才有
// 添加 或者 编辑 的描述文字
const type = editId === undefined ? '添加' : '编辑'
useEffect(async () => {
// 查询分类数据
const resCategory = await getCategory()
if (resCategory.status === undefined) setOptions(resCategory)
// 发送请求, 获取商品详情
if (editId !== undefined) {
const response = await showGoods(editId)
console.log(response);
// 获取数据之后, 修改状态, 状态改变, 组件重新渲染, 骨架屏消失, 编辑表单出现
const {pid, id} = response.category
const defaultCategory = pid === 0 ? [id] : [pid, id]
setInitialValues({...response, category_id: defaultCategory})
}
}, [])
/**
* 文件上传成功后, 设置cover字段的value
* @param fileKey
*/
const setCoverKey = fileKey => formObj.setFieldsValue({'cover': fileKey})
/**
* 编辑输入内容后, 设置details字段的value
* @param fileKey
*/
const setDetails = content => formObj.setFieldsValue({'details': content})
/**
* 提交表单, 执行编辑或者添加
*
* @param values
* @returns {Promise<void>}
*/
const handleSubmit = async values => {
let response = {}
if (editId === undefined) { // 执行添加
// 发送请求, 添加商品
response = await addGoods({...values, category_id: values.category_id[1]})
} else { // 执行编辑
// 发送请求, 更新商品
response = await updateGoods(editId, {...values, category_id: values.category_id[1]})
}
if (response.status === undefined) {
message.success(`${type}成功`)
// 刷新表格数据
actionRef.current.reload()
// 关闭模态框
isShowModal(false)
}
}
return (
<Modal
title={`${type}商品`}
visible={isModalVisible}
onCancel={() => isShowModal(false) }
footer={null}
destroyOnClose={true}
>
{
// 只有是编辑的情况下, 并且要显示的数据还没有返回, 才显示骨架屏
initialValues === undefined && editId !== undefined ? <Skeleton active={true} paragraph={{ rows: 4 } } /> :
<ProForm
form={formObj}
initialValues={ initialValues }
onFinish={ values => handleSubmit(values) }
>
<ProForm.Item
name="category_id"
label="分类"
rules={[{required: true, message: '请选择分类'}]}
>
<Cascader
fieldNames={ {label: 'name', value: 'id'} }
options={options}
placeholder="请选择分类"
/>
</ProForm.Item>
<ProFormText
name="title"
label="商品名"
placeholder="请输入商品名"
rules={[{required: true, message: '请输入商品名'},]}
/>
<ProFormTextArea
name="description"
label="描述"
placeholder="请输入描述"
rules={[{required: true, message: '请输入描述'},]}
/>
<ProFormDigit
name="price"
label="价格"
placeholder="请输入价格"
min={0}
max={99999999}
rules={[{required: true, message: '请输入价格'},]}
/>
<ProFormDigit
name="stock"
label="库存"
placeholder="请输入库存"
min={0}
max={99999999}
rules={[{required: true, message: '请输入库存'},]}
/>
<ProFormText name="cover" hidden={true}/>
<ProForm.Item
name="cover"
label="商品主图"
rules={[{required: true, message: '请上传商品主图'}]}
>
<div>
<OSSUpload
accept="image/*"
setCoverKey={setCoverKey}
showUploadList={true}
>
<Button icon={<UploadOutlined />}>点击上传商品主图</Button>
</OSSUpload>
{
initialValues === undefined || !initialValues.cover_url ? '' :
<Image width={200} src={initialValues.cover_url} />
}
</div>
</ProForm.Item>
<ProForm.Item
name="details"
label="商品详情"
rules={[{required: true, message: '请输入详情'}]}
>
<Editor
setDetails={setDetails}
content={initialValues === undefined ? '' : initialValues.details}
/>
</ProForm.Item>
</ProForm>
}
</Modal>
);
};
export default CreateOrEdit;
src\components\OSS\index.jsx
import React from "react";
import {Upload, message, Button} from 'antd';
// import { ossConfig } from "@/services/common ";
import {UploadOutlined} from "@ant-design/icons";
export default class OSSUpload extends React.Component {
state = {
OSSData: {},
};
/**
* 组件挂载完成后, 进行初始化, 获取oss配置
* @returns {Promise<void>}
*/
async componentDidMount() {
await this.init();
}
/**
* 初始化, 获取oss上传签名
*
* @returns {Promise<void>}
*/
init = async () => {
try {
const OSSData = await mockGetOSSData();
this.setState({
OSSData,
});
} catch (error) {
message.error(error);
}
};
/**
* 文件上传过程中触发的回调函数, 直到上传完成
*
* @param fileList
*/
onChange = ( { file } ) => {
if (file.status === 'done') {
// const {setCoverKey, insertImage} = this.props
//
// // 上传成功之后, 把文件的key, 设置为表单某个字段的值
// if (setCoverKey) setCoverKey(file.key)
//
// // 上传完成之后, 如果需要url, 那么返回url给父组件
// if (insertImage) insertImage(file.url)
this.props.setCoverKey(file.key);
message.success('上传成功')
}
};
onRemove = (file) => {
const { value, onChange } = this.props;
const files = value.filter((v) => v.url !== file.url);
if (onChange) {
onChange(files);
}
};
/**
* 额外的上传参数
*
* @returns {Promise<void>}
*/
getExtraData = file => {
const { OSSData } = this.state;
return {
key: file.key,
OSSAccessKeyId: OSSData.accessid,
policy: OSSData.policy,
Signature: OSSData.signature,
};
};
/**
* 选择文件之后, 上传文件之前, 执行回调
*
* @param file
* @returns {Promise<*>}
*/
beforeUpload = async file => {
const { OSSData } = this.state;
const expire = OSSData.expire * 1000;
// 如果签名过期了, 重新获取
if (expire < Date.now()) {
await this.init();
}
const dir = 'react/' // 定义上传的目录
const suffix = file.name.slice(file.name.lastIndexOf('.'));
const filename = Date.now() + suffix;
file.key = OSSData.dir + dir + filename; // 在 getExtraData 函数中会用到, 在云存储中存储的文件的 key
file.url = OSSData.host + OSSData.dir + dir + filename; // 上传完成后, 用于显示内容
return file;
};
render() {
const { value, accept, showUploadList } = this.props;
const props = {
accept: accept || '',
name: 'file',
fileList: value,
action: this.state.OSSData.host,
onChange: this.onChange,
data: this.getExtraData,
beforeUpload: this.beforeUpload,
listType: "picture",
maxCount: 1,
showUploadList
};
return (
<Upload {...props}>
<Button icon={<UploadOutlined />}>点击上传</Button>
{/*{this.props.children}*/}
</Upload>
);
}
}