电子流合同系统开发

1,861 阅读6分钟

项目背景

由于部门业务迅速发展,与合作商的合同数量增长速度快,以往通过人工流程对合同进行录入、查找的管理模式逐步跟不上合同所需的处理速度,因此将线下无序繁杂的合同升级为规范的线上合同的需求日益凸显,基于该业务诉求诞生出该合同系统。

合同系统的诞生极大地解决了录入、管理合同时耗费大量人力精力的问题,在减少业务同学工作量的同时,也更好地服务与合作伙伴,减少合同数据填写出错、遗漏等问题;

技术选型

技术选型原因
前端JS框架Vue2.0社区热度高,组件种类多,技术栈成熟,类HTML模板熟悉成本低,路由(vue-router)、状态管理器(vuex)等高实用性组件由Vue核心团队维护,是官方推荐组件
UI库Iview提供基于Vue开发的UI组件库,技术相对成熟,组件种类多,当前基础建设系统使用iview进行开发(*主要原因),避免引入其他UI库导致代码杂乱
css开发工具SCSS提高css编写效率,使用函数式编程编写css代码具有较高的扩展性及可读性,维护成本较低

合同项目部分功能页面截图

合同人工录入-基础信息:

合同人工录入-详细信息

合同查询

合同查询-作品列表信息

合同审核

开发目录划分及其意义?

一个项目如果有一个合理的项目目录结构,能让开发者及之后维护的人对项目熟悉了解的成本大大降低

如上图所示,总目录ContractV2下创建了多个不同的文件夹对应功能及说明如下所示:

组件化、模块化开发的优势及如何定义?

在新的项目需求拿到手时,不要急于立马进行开发,先好好梳理一遍项目结构、流程、交互,对功能点进行归纳分类,与产品、需求方反复沟通,明确项目业务流程是否通顺、功能点是否具有复用性,一方面是为了能在开发时,对项目功能划分具有大局性思维,另一方面是为了在开发时避免做无用功,导致对不必封装的组件、模块进行了封装,对需要封装的组件模块却直接用业务代码实现了,导致后期要重构代码。

优点缺点
组件化、模块化开发维护成本低
可扩展性高
耦合程度低,删改简便
复用性强
改造成本中
学习成本低
基于状态码或业务逻辑进行扩展时须兼容多种应用场景
项目进行前期不便于确定是否需要模块化、组件化开发
定制化程度低
组件间通讯不方便
直接写业务逻辑实现实现速度快
功能封装程度低令单功能模块冗余代码少
适用于高定制化开发
维护成本高
可扩展性低
复用性低
改造成本中
学习成本中
业务、组件间代码耦合程度高,不便于删改

基于iview的tabs组件进行的二次组件化封装代码及实例使用代码:


组件化封装 start

<style lang="scss" scoped>
    .contract-tabs {
        margin-top: 20px;
        /deep/.can-not-close+.ivu-icon-ios-close {
            display: none;
        }
        /deep/.ivu-tabs-bar {
            margin-bottom: 0;
        }
        .ivu-page {
            text-align: right;
        }
    }
<style>
<template>
    <Tabs ref="tabs" type="card" :value="curTab" :animated="false" closable @on-tab-remove="handleTabRemove" :before-remove="removeBefore" class="contract-tabs">
        <template v-for="(tab,index) in tabColumns">
            <TabPane v-if="tab.show" :label="tab['closeable'] ? tab.label : (h) => tab.renderLabel(h, tab)" :class="{'disable-close': !tab.closeable}" :key="randomKey(index)" :name="tab.name">
                <Table v-if="tab.type == 'table'" :loading="tab.isLoading" height="630" border :columns="tab.columns" :data="tab.data"></Table>
                <Page :current="tab.page.currentPage" :total="tab.page.total" size="small" show-total @on-change="(page)=>{tab.page.onChange(page,index)}" show-elevator :page-size="tab.page.page_size" v-if="tab.page"></Page>
            </TabPane>
        </template>
    </Tabs>
</template>
import Vue from 'vue';
import {Tabs} from 'iview';

//Tab原属性
const tabsNativeProps = Tabs.props,
    tabsNativePropNames = Object.keys(tabsNativeProps);

//Tab属性监听
const originProxyWatcher = tabsNativePropNames.reduce((watch, name) => {
    watch[name] = function (val) {
        Vue.set(this.wrapProps, name, val);
    };
    return watch;
}, {});

//Tab原方法
const tabNativeMethods = Tabs.methods,
    tabNativeMethodsName = Object.keys(tabNativeMethods);

//Tab原方法代理
const originMethodsProxy = tabNativeMethodsName.reduce((proxy, name) => {
    proxy[name] = function (...args) {
        const $tabs = this.refs['tabs'];
        $tabs[name].apply($tabs, args);
    };
    return proxy;
}, {});


export default {
    pageAuthor: 'janwardchen',
    components: {Tabs},
    props: Object.assign({
        'curTab': {
            type: String,
            default: 'tabIndex1',
        },
        'tabPaneColumns': {
            type: Array,
            default: () => {
                return []
            }
        },
    }, tabsNativeProps),
    watch: Object.assign({}, originProxyWatcher),
    methods: Object.assign({
        renderLabel: (createElement, params) => {
            let {label='',closeable,show=true} = params;
            if (closeable == false || closeable == 'false') {
                return createElement('span',{attrs: {class: 'can-not-close'}}, label);
            }
            if(!show) {
                return null;
            }
        },
        removeBefore(idx) {
            const {closeable} = this.tabPaneColumns[idx];
            if (closeable == false || closeable == 'false') {
                return new Promise((resolve, reject) => {});
            }
        },
        handleTabRemove(name, arg) {
            this.$emit("removeTab",name);
        },
        randomKey(idx) {
            const _t = new Date(),
                _key = idx.toString() + _t.getTime().toString();
            return _key;
        }
    }, originMethodsProxy),
    computed: {
        tabColumns() {
            let _columns = this.tabPaneColumns || [];
            const self = this;
            if (_columns) {
                _columns.forEach((item, idx) => {
                    item['renderLabel'] = self.renderLabel;
                    if(item.page && JSON.stringify(item.page) != '{}') {
                        let {currentPage=1,total=0,page_size=20,onChange=()=>{return false;}} = item.page;
                        item.page.currentPage = Number(currentPage);
                        item.page.total = Number(total);
                        item.page.page_size = Number(page_size);
                        item.page.onChange = onChange;
                    }
                    if(item.show==undefined) {
                        item.show = true;
                    }
                });
            }
            return _columns;
        }
    }
}

组件化封装end


组件实例示例:

<template>
    <ContractTabs :tab-pane-columns="tabPaneColumns" :cur-tab="curTab" @removeTab="removeTab"></ContractTabs>
</template>

<script>
    import ContractTabs from '~/components/contract-tabs.vue';
    export default {
        components: {
            ContractTabs,
        },
        data() {
            return {
                tabPaneColumns: [
                    {label: '补签合同',type: 'table',columns: [
                            {title: '合同号 ', minWidth: 170, key: 'pact_no',align: 'center',},
                            {title: '协议列表 ',minWidth: 140,key: 'is_signed_protocol',align: 'center',render: this.renderColumn(),},
                            {title: '作品列表 minWidth: 200,key: 'works_list',align: 'center',render: this.renderColumn(),},
                            {title: '我方主体 ', minWidth: 110, key: 'ou',align: 'center'},
                            {title: '供应商名称 ', minWidth: 175, key: 'cp_name', align: 'center',},
                            {title: '生效时间 ', minWidth: 180, key: 'effect_time', align: 'center',},
                            {title: '合同状态 ', minWidth: 130, key: 'state', align: 'center',},
                            {title: '经办人 ', minWidth: 100, key: 'head_person', align: 'center',},
                            {title: '录入人 ', minWidth: 100, key: 'head_person', align: 'center',},
                            {title: '审核人 ', minWidth: 100, key: 'head_person', align: 'center',},
                            {title: '合同详情 ',minWidth: 110,key: 'show_dfixed: 'right',render: this.renderColumn(),},
                            {title: '操作 ',minWidth: 145,key: 'sign_protocol_list',align: 'center',fixed: 'right',render: this.renderColumn(),}
                        ],
                        isLoading: true,
                        page: {currentPage: 1, total: 0, page_size: 20, onChange: this.fetchIndexData},
                        data: [],
                        closeable: false,
                        name: 'tabIndex1'
                    },
                ],
            }
        },
        methods: {
            //渲染table列
            renderColumn() {
                .....
            }
        }
    }
</script>

如上图代码所示,在tabPaneColumns中只需把对应的tab数据处理好,传入点击按钮的处理事件,余下的处理交由ContractTabs就好了,这种形式的处理在删减该逻辑时,直接把<ContractTabs> <ContractTabs>删除,就能移除该功能,不会影响到其他模块的代码。

组件示例图:

此外,对于功能的模块化处理也做了一定的封装,如下贴出一些示例:

/**
 * 执行ajax请求
 * @param url 请求接口地址
 * @param data 请求参数
 * @param cb 请求成功后执行的回调
 * @param type 请求方式为post或get  选填
 * @param ignore 回调是否忽略res.status判断
 */
async fetchData(url, data, cb, type, ignore) {
    let method = type || 'get';
    const res = await this.ajax({url, data, method});
    if (ignore) {
        cb(res);
        return false
    } else {
        if (res.status == 2) {
            cb(res);
            return false;
        } else if (res.status == 1) {
            this.$Message.warning(res.msg);
            return false;
        } else {
            console.log(res.status);
            this.$Message.warning(res.msg);
            return false;
        }
    }
},
/**
* 对form表单数据进行统一提交前格式化处理,确保数据通用性
* @param data 待处理的数据,处理完后回吐数据
*/
formatFormData(data) {
    let formData = {};
    if (JSON.stringify(data) != '{}') {
        let ObjArr = Object.keys(data);
        ObjArr.forEach((keys) => {
            data[keys].forEach((item) => {
                formData[item.key] = item.value;
            });
        });
    }
    return formData;
},

如上,这些组件、功能模块都需要在多个地方复用,如果不做任何封装处理,每一个需要用到的地方都直接手动重新编写一次,代码的重复量可想而知是很多的,而且在改动的时候,也很可能会出现改漏改错的情况,这是程序员的噩梦啊。。

合同项目开发总结

  • 项目功能项多,页面多,接口多,各接口的作用、数据、结构多样,因此在开发时,后台每完成一个接口交接到前端时,都必须进行整理,记录接口的入参,作用,调用方式、调用地方、返回的数据各字段作用,否则当接口的量多了,查找或复盘的时候容易一头雾水。
  • 前期对需求必须有足够的了解,多与产品、需求方沟通,确认各功能交互、数据校验严格程度,避免过多进行组件化封装,也避免因缺少沟通了解导致多次改造调整功能、组件、模块。
  • 这次项目中没有使用当前大火的TypeScript,这在开发组件和调试接口时,出现了很多因数据格式、类型而引发的报错问题,需要慢慢的一步一步定位解决,在调试耗时上没有控制好。