乐趣区

密选分享初版

背景

以 XXX 男装 top 店与其档口,供货商的交流痛点为出发,解决选款,上架,交流等问题。

重点功能

  1. 供应商编辑商品发送到分销商
  2. 选款人员权限与协同管理
  3. 分销商 GOOD PASS 选款
  4. 商品图片下载管理
  5. 淘宝天猫上架

操作流程

供应链 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: 构建过程或辅助工具的变动

发布上线流程

  1. 把需要上线的代码合并 develop。
  2. develop 分支代码 build, 发布到测试服务器,交由测试,产品验收。
  3. 解决 bug 后,再由测试,产品再验收。
  4. develop 合并 master 后再 build,发布到正式服务器,交由测试,产品使用。
  5. 正式紧急 bug,fix 直接修改测试好后合并 master,小版本上线。不重要的下个版本再处理
  6. 写版本日志记录,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 方法:有时候有些数据需要整合以后的验证

待优化提升点

  1. 添加前端日志记录,方便线上 bug 追踪
  2. 部署自动化,防止项目上线错误的发生
退出移动版