背景
以 XXX 男装 top 店与其档口,供货商的交流痛点为出发,解决选款,上架,交流等问题。
重点功能
- 供应商编辑商品发送到分销商
- 选款人员权限与协同管理
- 分销商 GOOD PASS 选款
- 商品图片下载管理
- 淘宝天猫上架
操作流程
供应链 APP 提报商品 –> 密选供应商编辑并发送到密选 –> 密选选款并直接淘宝天猫上架
技术选型
框架
UI 框架:react
UI 组件库:Ant Design
HTTP 库:Axios
APP 状态管理:Redux
代码风格:ESLint Prettier
css 预处理:Less
打包工具:webpack
第三方插件
七牛图片上传:qiniu-js
react 添加多个 className:classNames
复制功能:clipboard
文件下载保存:file-saver
文件压缩:jszip
时间处理:moment
……
业务插件
淘宝天猫上架:mx-quick-shelf
项目文件结构
项目目录
├── /config/ # 配置相关:devServer 配置,env 多环境的配置,项目文件夹绝对路径,antd 定制主题
├── /dist/ # 输出目录
├── /public/ # html 模板 icon 文件
├── /scripts/ # 构建配置
├── /src/ # 业务逻辑代码
│ ├── /assets/ # 项目静态资源文件
│ ├── /common/ # 路由表及页面组件 loader& 菜单管理
│ ├── /components/ # UI 组件及 UI 相关方法
│ │ ├── /Authorized/ # 权限组件 & 权限管理
│ ├── /global/ # 全局状态管理
│ ├── /layouts/ # 项目布局组件
│ ├── /pages/ # 项目页面组件
│ ├── /styles/ # 全局样式
│ ├── /utils/ # 工具函数
│ ├── App.js # 项目入口组件
│ ├── index.js # 项目入口文件, 挂载组件, 初始化
│ ├── reducers.js # 合并 combine reducers
│ └── store.js # compose middlewares & create store
├── .gitignore # git 配置
├── .babelrc # babel-loader 配置
├── .eslintrc # Eslint 配置
├── .prettierrc # Prettierrc 配置
└── package.json # 项目信息
页面目录
├── /pages/ # 项目页面组件
│ ├── /Home/ # 首页
│ │ ├── /components/ # 页面私有组件
│ │ ├── /view/ # 视图组件
│ │ │ ├── /index.js # dom 与控制
│ │ │ ├── /index.less # 样式
│ │ ├── /actions.js # reduex action
│ │ ├── /actionTypes.js # reduex action type
│ │ ├── /index.js # 入口
│ │ ├── /reducer.js # reduex reducer
│ │ └── /service.js # http 请求
版本控制与发布流程
版本控制管理
分支规范(git)
- feature: 新功能制作区 测试问题处理区
- develop: 测试在跑的最新代码,迭代下一个版本
- master: 正式在跑的最新代码
- fix: 快速解决正式问题
- release: 针对某个版本处理
commit message 规范(git)
- feat: 新功能
- fix: 修改 bug
- docs: 文档更新
- style: 格式(css)
- refactor: 重构 (密选选款卡片的变更)
- test: 测试代码
- chore: 构建过程或辅助工具的变动
发布上线流程
- 把需要上线的代码合并 develop。
- develop 分支代码 build, 发布到测试服务器,交由测试,产品验收。
- 解决 bug 后,再由测试,产品再验收。
- develop 合并 master 后再 build,发布到正式服务器,交由测试,产品使用。
- 正式紧急 bug,fix 直接修改测试好后合并 master,小版本上线。不重要的下个版本再处理
- 写版本日志记录,master 提交代码,添加 git 版本标签,方便追溯。
版本日志记录
## [3.1.0] - 2019-09-19
### Added
- 淘宝上架
### Changed
- 供应商标题与计划标题代码整合
### Fixed
- 搜索调用接口多次触发
### Removed
- 删除图片放大功能
### Deprecated
- 不建议使用,未来会删掉
### Security
- 安全相关的 bug
部分功能展示与讲解
页面状态保持
页面说明
代码说明
// 进入页面 获取分销商分组
componentDidMount() {this.getSendList({ redirect: true})
}
// 重新渲染的时候调用,把路由中的值赋值到 state
static getDerivedStateFromProps(nextProps, prevState) {const { match} = nextProps
const {queryType, dayCollectId} = match.params
if (prevState.queryType !== Number(queryType)) {
return {
...prevState,
queryType: Number(queryType || 0)
}
}
if (prevState.dayCollectId !== dayCollectId) {
return {
...prevState,
dayCollectId: dayCollectId || ''
}
}
return null
}
// 有值改变的时候执行
async componentDidUpdate(prevProps, prevState) {const { dayCollectId, queryType} = this.state
// 重新获取分销商分组
if (queryType !== prevState.queryType) {
this.getSendList({redirect: true // 是否重定向})
}
//
if (dayCollectId !== prevState.dayCollectId && dayCollectId) {
this.setSendInfo({redirect: false})
this.getSendItemInfo()}
}
// 获取分销商分组
async getSendList(option) {const { pageNum, pageSize, queryType, vagueSearch} = this.state
await getSendList({
vagueSearch,
pageNum,
pageSize,
queryType
}).then(json => {const { success, data: sendList} = json
if (success) {
this.setState(
{sendList},
() => {this.setSendInfo(option)
}
)
}
})
}
// 设置分销商详情
setSendInfo(option) {const { history} = this.props
const {sendList, queryType, dayCollectId} = this.state
option = Object.assign(
{
redirect: false,
resume: true
},
option || {})
if (sendList.length < 1) {return}
const existed = sendList.find(item => {return item.id === dayCollectId})
const selectSendItem = option.resume && existed ? existed : sendList[0]
this.setState({
spuItem: selectSendItem,
sendItemList: []})
option.redirect && history.replace(`/goods/distributors_sent/${queryType}/${selectSendItem.id}`)
}
// 获取分销商详情
getSendItemInfo(){// ...}
店铺授权类目
页面说明
代码展示
// CategoryModal.js
<div style={styles.categoryChooseContent}>
{categoryData.map((item, index) => (<div key={item.id} style={styles.categoryItem}>
<CategoryItem
loading={item.loading}
categorys={item.categorys}
onChoose={item => {handleCategorChoose(item, index)
}}
/>
</div>
))}
// CategoryItem.js
function CategoryItem({loading = false, categorys, onChoose}) {const [categorysList, setCategorysList] = useState([])
const [selected, setSelected] = useState('')
// 在 load 以后渲染类目。useEffect(() => {if (!loading) {setSelected('')
setCategorysList(categorys)
} else {setCategorysList([])
}
}, [loading])
// 选中类目
const handleChoose = item => {setSelected(item.cid)
onChoose(item)
}
// 搜索
const handleChange = e => {const { value} = e.target
delay(() => {const filterList = categorys.filter(item => item.name.indexOf(value) >= 0)
setCategorysList(filterList)
}, 800)
}
return (<Spin spinning={loading}>
<div style={styles.header}>
<Search placeholder="名称 / 拼音字母" onChange={handleChange} />
</div>
<div style={styles.body}>
{categorysList && categorysList.length > 0 && (
<ul>
{categorysList.map(item => (
<li
key={item.cid}
className={item.cid === selected ? 'qs_category_selected' : 'qs_category'}
style={styles.category}
onClick={() => handleChoose(item)}>
<span>{item.name}</span>
{item.isParent ? <Icon type="right" style={styles.iconRight} /> : null}
</li>
))}
</ul>
)}
</div>
</Spin>
)
}
踩过的坑
mx-quick-shelf 业务插件引入问题
mx-quick-shelf
➜ [mx-quick-shelf] npm link
secret-goods-pc
➜ [secret-goods-pc] npm link mx-quick-shelf 原则上这步就行但是需要:
➜ [mx-quick-shelf] npm link ../../work/secret-goods-pc/node_modules/react
➜ [mx-quick-shelf] npm link ../../work/secret-goods-pc/node_modules/react-dom
antd form 表单问题
<Form.Item label={_attributes.name} {...itemLayout} {...extraProps}>
{getFieldDecorator(`${_attributes.id}`, {
rules: _rules,
initialValue: _value,
validateFirst: true
})(formItemDom()
)}
</Form.Item>
坑点
要实现 内部验证与外部验证,最好的方法是 form 嵌套 form。刚开始的方法:
- form 标签不能嵌套,子组件外部不要包 form 标签, 尴尬
- form 传递到子组件,id 使用 XXX.[x].xxx 方法:有时候有些数据需要整合以后的验证
待优化提升点
- 添加前端日志记录,方便线上 bug 追踪
- 部署自动化,防止项目上线错误的发生