# 拉单前端开发步骤

1 引入工具类

2 将片段代码进行整合到你的前端代码中

3 维护对应的页面模板（手工处理）



# 拉单前端代码 DEMO

```js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

import {createPage,  high, print, output,toast} from 'nc-lightapp-front';
const {NCDiv, NCAffix, NCTabs, NCScrollElement, NCTooltip, NCToggleViewBtn} = base;
import {Utils} from './Utils';
const URLS = {  // 请求路径
    //查询来源单据url
	querySourceBillUrl: '/nccloud/XXXXX/XXXXX/XXXXXXXAction.do', // 拉单查询"参数审批单"请求路径
	//转单URL
	transferUrl: '/nccloud/XXXXX/XXXXX/XXXXXXAction.do', // 转单请求路径
};
const SHOWMODE_FETCHList = "fetchlist"; // 拉单列表
const SHOWMODE_FETCHCARD = 'fetchcard'; // 拉单卡片

/**
 * 拉单demo示例
 */
class ApplicationPage extends Component {

    constructor(props) {
        super(props);
        this.state = this.createState();
    }

	/**
	 * 创建state
	 */
    createState = () => {
		//基础state
        let state = {
        }
		// 拉单
		Utils.apply(state, {
			toggleViewBtn: { // 主子拉平按钮配置
				expand: false, // 默认是否为主子拉平
				onClick: () => { // 主子拉平按钮点击处理
					this.state.toggleViewBtn.expand = !this.state.toggleViewBtn.expand ;
					this.setState(this.state, () => this.props.transferTable.changeViewType());
				}
			},
			//这个根据你自己的进行定义，可以配置多个拉单，结构类似如下
			fetch_DEMO: { // 拉单上游列表配置
				srcBilltype: "", // 来源单据类型
				queryUrl: URLS.querySourceBillUrl, // 查询请求url
				headIdName: '', // "上游单据"主表主键名称
				bodyIdName: '', // "上游单据"子表主键名称
				fetchHead: { // 拉单上游页面头部标题配置
					title: 'DEMO标题', // 头部标题
					backBtnClick : this.onFetchListBack, // 点击返回
					initShowBackBtn: true // 是否显示返回按钮
				},
				fetchButton: { // 拉单上游页面头部按钮配置
					area: 'fetch_table', // 按钮注册中的按钮区域
					onButtonClick : (props, key, hotkey, isFromSide) => { // 拉单上游界面按钮点击事件
						let fetchConf = this.state.fetch_DEMO;
						this.onTransferSearch(fetchConf, (data,num) => { // 查询后的回调
							toast({color: 'success' , content: '刷新成功'});
						});
					}
				},
				search: {
					area: '此编码去模板上找到进行配置', // 查询区组件区域编码
					clickSearchBtn: () => {	// 点击查询监听事件
						let fetchConf = this.state.fetch_DEMO;
						this.onTransferSearch(fetchConf, (data, num) => { // 查询后的回调
							if (num && num > 0) {
								toast({color: 'success', content: '查询成功，共'+num+'条！'});
							} else {
								toast({color: 'warning', content: '未查询到数据'});
							}
						});
					},
					onAfterEvent: (key, value) => {	// 拉单的查询区编辑后事件处理。
						//todo...
					}
				},
				transferTable: { // 转单表格
					dataSource: '自己定义一个key', // 缓存数据的key
					headTableId: '去模板上找到你注册的对应区域编码', // 表格组件id
					bodyTableId: '去模板上找到你注册的对应区域编码', // 子表模板id
					fullTableId: '去模板上找到你注册的对应区域编码', // 主子拉平的id
					tableType: 'nest', // 表格显示类型 nest:主子表 full:主子拉平 simple:单表
					showSeletedBtn: true, // 左下角“查看已选”是否显示
					showChangeViewBtn: true, // 显示主子拉平的按钮是否显示
					transferBtnText: '转单', // 转单按钮显示文字
					containerSelector: '#transferList', // 父组件容器
					// 转单按钮监听事件
					onTransferBtnClick: () => {
						let params = {
							data: Utils.onTransferWrapper({props: this.props, fetchConf: this.state.fetch_1001}),
							pageCode: this.config.pagecode,
							formId: '去模板上找到你注册的对应区域编码',
							formListIds: '去模板上找到你注册的对应区域编码'
						};
						this.onTransfer(params);
					},
					onChangeViewClick: () => {
						this.state.toggleViewBtn.onClick();
					},
					componentInitFinished: () => {
						//缓存数据赋值成功的钩子函数
						//若初始化数据后需要对数据做修改，可以在这里处理
					}
				}
			},
			fetchCard: {										          //拉单卡片界面配置
				backMode: "返回的模式编码，这个编码用来操控界面的状态，是你自己进行定义的，这个需要结合你当前编写的代码进行配置",   //拉单卡片返回界面
				fetchHead: { // 拉单卡片头部配置（）
					title: 'DEMO标题', // 标题
					backBtnClick: this.onFetchCardBack // 点击返回
				},
				fetchButton: { // 按钮区域
					area: 'common', // 按钮注册中的按钮区域
					onButtonClick: (props, key, hotkey, isFromSide) => this.onBtnClickHead(props,key) // 点击按钮
				},
				tranlist: { // 转单列表
					headcode: '去模板上找到你注册的对应区域编码', // 表头的区域编码
					bodycode: [ '去模板上找到你注册的对应区域编码',  '去模板上找到你注册的对应区域编码', ], // 表体区域编码
					transferListId: '去模板上找到你注册的对应区域编码', // 转单列表 id
					selectIndex: -1, // 左侧列表选中位置
					onTransferItemSelected: Utils.onTransferItemSelectedWrapper(this.onTransferItemSelected) // 转单后卡片左侧缩略图选中点击处理
				},
				selectIndex: 0 // 左侧列表选中位置
			}
		});
        return state;   //返回state解构
    }
	
	/**
	 * 方法功能：
	 *      拉单列表返回
	 */
	onFetchListBack = () => {
	    // 切换　页面模式
	    this.state.showmode = "你需要切换到的页面";
	    // 设置不显示标题栏返回按钮
	    this.state.head.initShowBackBtn = false;
	    this.setState(this.state, () => {
			//重新查询列表，可以复用已有的查询列表功能，需要自己去看下当前页面是否有，没有的话需要自己写一下
			//todo．．．．
	    });
		
		//路由的方式，可以参考你当前页面的进行开发
	}
	
	/**
	 * 方法功能：
	 *      点击拉单列表页面的查询
	 */
	onTransferSearch = (param = {}, callback = EMPTY_FN) => {
	    let {queryUrl, headIdName, bodyIdName, search, transferTable} = param; // 解构上游拉单参数
	    let {headTableId, bodyTableId, tableType} = transferTable;
	    // 获取查询条件
	    let queryInfo = this.props.search.getQueryInfo(search.area);
	    if (!queryInfo) C
	    // 查询请求
	    ajax({
	        url: queryUrl,
	        headers: this.ajaxHeader(),
	        data: {
				queryTreeFormatVO: queryInfo, // 查询条件
				pageCode: this.config.pagecode,
				formId: headTableId,
				tableId: [bodyTableId]
	        },
	        loading: true,
	        success: (res) => {
	            let data = res.data && res.data.data;
	            let num = 0;
	            // 设置拉单列表数据
	            if (tableType == 'simple') {
	                let fetchListData = data && data[headTableId] || {rows: []};
	                num = fetchListData.rows.length;
	                this.props.transferTable.setTransferTableValue(headTableId, bodyTableId, fetchListData, headIdName, bodyIdName);
	            } else {
	                num = data ? data.length : 0;
	                // 适配无子表拉单
	                data && data.forEach(item => {
	                    !item.body && (item.body = {[bodyTableId]: {areacode: bodyTableId, rows: []}})
	                })
	                this.props.transferTable.setTransferTableValue(headTableId, bodyTableId, data || [], headIdName, bodyIdName);
	            }
	            callback(data, num);
	        }
	    });
	}

	
	/**
	 * 渲染
	 */
    render() {
		
		// 拉单列表渲染方法
		const renderFetchListDemo = () => { // 拉单上游列表界面
		    let {toggleViewBtn,fetch_DEMO} = this.state;
		    // 如果当前显示不是拉单列表界面，则返回空。
		    if (showmode != SHOWMODE_FETCHList) {
		        return '';
		    }
		    let {
		        fetchHead,
		        fetchButton,
		        search,
		        transferTable
		    } = fetch_DEMO;
		    let selectedShow = this.props.transferTable.getSelectedListDisplay(transferTable.headTableId);
		    if (selectedShow) { // 查看已选时界面
		        return (
		            <div id="transferList" className="nc-bill-list">
		                <div className="nc-bill-transferTable-area">
		                    {this.props.transferTable.createTransferTable(transferTable)}
		                </div>
		            </div>
		        );
		    }
		    return (
		        <div className="nc-bill-list">
		            {/* 头部 header */}
		            <NCDiv className="nc-bill-header-area" areaCode={NCDiv.config.HEADER}>
		                {/* 标题 title */}
		                <div
		                    className="header-title-search-area">{this.props.BillHeadInfo.createBillHeadInfo(fetchHead)}</div>
		                <div className="header-button-area">
		                    {/* 按钮区  btn-group */}
		                    {this.props.button.createButtonApp(fetchButton)}
							{/* 主子拉平的按钮 */}
							{<NCToggleViewBtn {...toggleViewBtn} />}
		                </div>
		            </NCDiv>
		            {/* 渲染查询区 */}
		            <NCDiv areaCode={NCDiv.config.SEARCH} className="nc-bill-search-area">
		                {this.props.search.NCCreateSearch(search.area, {...search})}
		            </NCDiv>
		            {/* 渲染列表区 */}
		            <NCDiv className="nc-bill-table-area">
		                {this.props.transferTable.createTransferTable({...transferTable})}
		            </NCDiv>
		        </div>
		    );
		};
		
		//拉单卡片界面渲染
		const renderFetchCardDemo = () => {
		    let {fetchCard} = this.state;
		    let {form, formlist, cardPagniation} = card;
		    let {fetchHead, fetchButton, tranlist, selectIndex} = fetchCard;
		    if (showmode != SHOWMODE_FETCHCARD) {
		        return '';
		    }
		    // cardtable存在bug，拷贝使用的是Object.assign()，会修改当前state
		    let newFormlist = deepClone(formlist);
		    let {'treegridSlave0Sub' : tableConfig, ...other} = newFormlist;
		    return (
		        <div id="transferCard" className="nc-bill-transferList">
		            <NCAffix>
		                <NCDiv className='nc-bill-header-area' areaCode={NCDiv.config.HEADER}>
		                    <div className='title'>
								{/*创建头部*/}
		                        {this.props.BillHeadInfo.createBillHeadInfo(fetchHead)}
		                    </div>
		                    <div className="header-button-area">
								{/*头部按钮*/}
		                        {this.props.button.createButtonApp(fetchButton)}
		                    </div>
		                </NCDiv>
		            </NCAffix>
		            <div className="nc-bill-transferList-content">
		                {this.props.transferTable.createTransferList(tranlist)}
		                <div className="transferList-content-right nc-bill-card">
		                    <div className="nc-bill-form-area">
								{/* 主表表单 */}
		                        {this.props.form.createForm("表单区域编码，可以复用已有卡片的", "表单区域配置")}
		                    </div>
							<div className="nc-bill-bottom-area">
		                        <NCDiv areaCode={NCDiv.config.TABLE} className="nc-bill-table-area">
		                            {this.props.cardTable.createCardTable(tableConfig.area, {...tableConfig, multiConfig: other})}
		                        </NCDiv>
							</div>
		                </div>
		            </div>
		        </div>
		    );
		}
		
		
		return  <div>
					{/* 拉单列表渲染 */}
					{renderFetchListDemo()}
					{/* 拉单卡片渲染 */}
					{renderFetchCardDemo()}
                </div>

    }
}
```



# 前端工具类


```js
import {toast} from 'nc-lightapp-front';
const EMPTY_FN = function () {
};
/**
 * 对象合并 类似Object.assign();
 * @returns 第一个参数对象
 */
const apply  = function(){
    if(!arguments){
        throw new ReferenceError("该方法执行时至少传递一个参数对象!");
    }
    //参数的长度
    let length = arguments.length;
    //长度是1 直接返回
    if(length == 1){
        return arguments[0];
    }
    //源
    let target = arguments[0];
    //从第二个元素开始 到 最后一个组成sources
    let sources = [...arguments].slice(1);
    for(let i = 0;i<sources.length;i++){
        //第i个参数
        let param = sources[i];
		if(!param){
			continue;
		}
        Object.keys(param).forEach(key=>{
            //param原型上有该对象或者属性
            if(Object.prototype.hasOwnProperty.call(param,key)){
                target[key] = param[key];
            }
        })
    }
    return target;
}
/**
 * 加载单据模板
 * props：
 * cfg:{必输项
 *    pagecode： 必输项 参数模板的参数
 * },
 * callback(fn): 回调
 */
const loadTemplate = function (props, {pagecode,appcode} = cfg) {
    return new Promise(resolve=>{
        props.createUIDom(
            {
                pagecode,
                appcode
            }, 
            (data) => {
                resolve({...data});
            }
        );
    })
};


/**
 * 加载多语言
 * @param {*} props 必输项
 * @param {*} cfg:{
 *   moduleId: 必输项  【应用编码： 例如 38211902 @attention 这里是应用编码多语文件的名称也要以应用编码命名】
 *   domainName：必输项 【领域编码：例如 uapbd、fi、gl等等】
 * }
 */
const loadLang = function (props, {moduleId,domainName} = cfg) {
    return new Promise((resolve)=>{
    	//请求多语
        props.MultiInit.getMultiLang(
            {
                moduleId,
                domainName, 
                callback: (data, success, inlt) => {
                    if (!success){
                        toast({content: '加载语言包失败', color: 'warning'});
                    }
                    resolve({lang:data,inlt});
                }
            }
        );
    })
};

/**
 * 加载参照  多个参照一块加载
 * @param {*} urls 
 *      示例：[
 *             {
 *                url:'uapbd/refer/supplier/SupplierClassTreeRef/index',
 *               name:'uapbd/refer/supplier/SupplierClassTreeRef/index'
 *             },
 *             {
 *               url:'uapbd/refer/supplier/SupplierClassTreeRef/index',
 *               name:'uapbd/refer/supplier/SupplierClassTreeRef/index'
 *             }
 *            ]
 * @returns 
 */
const loadRefer = function (urls = []) {
    if(!urls || urls.length == 0){
        return Promise.resolve(true);
    }
    let result = {};
    //构造
    let loads = (urls.filter((refObj,index)=>{
        let {url} = refObj;
        let jsname = url.includes(".")?url.substring(0, url.length - 3):url;
        if(!window[jsname]){
            return refObj;
        }
    })||[]).map((refObj)=>{
        return new Promise((resolve,reject)=>{
            let {url,name} = refObj;
            let jsname = url.includes(".")?url.substring(0, url.length - 3):url;
            let script = document.createElement('script');
            let suffix = url.includes(".")?'':'.js'
            script.src = '../../../../' + url+suffix;
            script.type = 'text/javascript';
            script.onload = () => {
                result[name] = window[jsname].default || EMPTY_FN;
                resolve(result);
            };
            script.onerror = () => {
                result[name] = undefined;
                reject(result);
            };
            document.body.appendChild(script);
        })
    });
    //Promise 请求全部参照
    return Promise.all(loads);
    
}
/**
 * 请求资源
 * @param {*} config 
 *           props,  当前应用对象的props
 *           pagecode,  页面编码
 *           appcode,   应用编码
 *           moduleId,  建议使用应用编码
 *           domainName, 领域编码
 *           referObjs = [], 参照请求路径集合 
 *           callback = EMPTY_FN 回调
 */
const loadNCCResource = function (config) {
    let {
            props, 
            pagecode, 
            appcode, 
            moduleId, 
            domainName, 
            referObjs = [], 
            callback = EMPTY_FN
    } = config;
    //加载参照
    loadRefer(referObjs)
    .then(()=>{
        return Promise.all([
            //加载模板
            loadTemplate(props,{pagecode,appcode}),
            //加载多语
            loadLang(props,{moduleId,domainName})
        ]);
    }).then(res=>{
        callback({...res[0],...res[1]});
    })
};

const isArray = function(param){
    return Object.prototype.toString.call(param).slice(8, -1) === 'Array';
};

const isString = function(param) {
    return Object.prototype.toString.call(param).slice(8, -1) === 'String';
};
const isObject = function(param){
    return Object.prototype.toString.call(param).slice(8, -1) === 'Object';
};
const transferDataWapper = function(datas,grandsonMap){
    (isObject(datas)?[datas]:datas || []).forEach((data)=>{
        let {head,body,bodys} = data;
        if(Array.isArray(bodys) && bodys.length){
            let bodysWrapper = (bodys || []).reduce((res,b)=>{
                let body = Object.values(b);
                let {areacode} = body[0];
                res[areacode] = body[0];
                return res;
            },{})
    
            // head['bodys'] = apply({},bodysWrapper);
            data['body'] = apply({},(bodys || body || {}));
            data['bodys'] = apply({},bodysWrapper);
        }else{
			data['body'] = apply({},(bodys || body || {}));
        }
    })
	grandsonMap && (data['grandsonMap'] = apply({}, grandsonMap));
    return datas;
};

const onTransferItemSelectedWrapper = function( func ){
    const fetchDataUnWapper = function(datas){
        ([datas] || []).forEach((data)=>{
            let {head,body,bodys} = data;
            data['body'] = apply({},body ||  head['bodys'] || bodys);
            data['bodys'] = apply({},head['bodys'] || bodys);
        });
        debugger;
        return datas;
    };
    return  function(record, isComplete, curActiveIndex,status){
        func(fetchDataUnWapper(record), isComplete, curActiveIndex,status);
    };
};

const onTransferWrapper = function( {props,fetchConf}){
    let {srcBilltype,headIdName,bodyIdName,transferTable:{headTableId,bodyTableId}} = fetchConf;
    let data = props.transferTable.getTransferTableSelectedValue();
    let records = (data && data[headTableId] ||[]).map(record=>{
        let pk = record[headIdName] ? record[headIdName].value : record.head[headTableId].rows[0].values[headIdName].value;
        let ts = record["ts"] ? record["ts"].value : record.head[headTableId].rows[0].values["ts"].value;
        let children = (record.body && bodyTableId && bodyTableId.length>0 && record.body[bodyTableId]) ? (record.body[bodyTableId].rows || []).map(row=>{
            return {pk:row.values[bodyIdName].value,ts:row.values["ts"].value}
        }):[];
        return {pk,ts,children};
    });
    return [{billType:srcBilltype,records}];
};
//float类型数求和
const FloatAdd = function(arg1,arg2){
    var r1,r2,m,s1 = arg1.toString(),s2 = arg2.toString();
    try{
        r1=s1.split(".")[1].length;
    }catch(e){
        r1=0
    }
    try{
        r2=s2.split(".")[1].length;
    }catch(e){
        r2=0
    }
    m=Math.pow(10,Math.max(r1,r2));
    return (arg1*m+arg2*m)/m;
};
//float类型数乘积
const FloatMultiple = function(f1,f2){
    var m=0,s1=f1.toString(),s2=f2.toString();
    
    try{
        m+=s1.split(".")[1].length;
    }catch(e){

    }
    try{
        m+=s2.split(".")[1].length;
    }catch(e){

    }
    return Number(s1.replace(".",""))*Number(s2.replace(".",""))/Math.pow(10,m)
};
export const Utils = {
    loadNCCResource,
    loadTemplate,
    loadLang,
    loadRefer,
    apply,
    transferDataWapper,
    onTransferItemSelectedWrapper,
    onTransferWrapper,
    FloatAdd,
    FloatMultiple
};




```

