背景

以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请求

版本控制与发布流程

版本控制管理

clipboard.png

分支规范(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(){
        // ... 
    }





店铺授权类目

页面说明

clipboard.png

代码展示

// 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

现在解决办法webpack
   alias: {
     react: utils.resolve('./node_modules/react')
}

antd form 表单问题

clipboard.png

<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. 部署自动化,防止项目上线错误的发生

LuDongWei
620 声望20 粉丝

英语不擅长的前端工作者