/* eslint-disable */ import Vue from 'vue' import { BaseModule, BizWatch, VJson } from './YvanUIModule' import { CtlTree } from './CtlTree' import { CtlTreeTable } from './CtlTreeTable' import { CtlTab } from './CtlTab' import { CtlDataview } from './CtlDataview' import { CtlDatatable } from './CtlDatatable' import { CtlECharts } from './CtlECharts' import { CtlButton } from './CtlButton' import { CtlImage } from "./CtlImage"; import { CtlText } from './form/input/CtlText' import { CtlCheckBox } from './form/other/CtlCheckBox' import { CtlCheckBoxGroup } from './form/other/CtlCheckBoxGroup' import { CtlCombo } from './form/select/CtlCombo' import { CtlDatePicker } from './form/input/CtlDatePicker' import { CtlDateRangePicker } from './form/input/CtlDateRangePicker' import { CtlForm } from './form/CtlForm' import { CtlMultiCombo } from './form/select/CtlMultiCombo' import { CtlSearch } from './form/input/CtlSearch' import { CtlTextarea } from './form/input/CtlTextarea' import { CtlCarousel } from './CtlCarousel' import { CtlGrid } from './CtlGrid' import { CtlSwitch } from './form/other/CtlSwitch' import { CtlNumber } from './form/input/CtlNumber' import { CtlRadio } from './form/other/CtlRadio' import { CtlCodeMirror } from './CtlCodeMirror' import { CtlSidebar } from './CtlSidebar' import { CtlXterm } from './CtlXterm' import { CtlBpmn } from './CtlBpmn' import { CtlConsoleLog } from './CtlConsoleLog' import { CtlMenu } from './CtlMenu' import { CtlUploader } from './CtlUploader' import { CtlViewer } from "./CtlViewer" import { CtlExcelImport } from "./form/input/CtlExcelImport"; import * as YvanMessage from './YvanUIMessage' import webix from 'webix' import { createAjax } from "./YvanUIAjax"; import { ExtendOption } from "./YvanUIExtend"; import { CtlDivider } from './CtlDivider' import {CtlEditor} from "./form/input/CtlEditor"; // export const webix = require("../webix/webix"); webix.i18n.setLocale('zh-CN') _.set(window, 'webix', webix); export const exprPropArr = ['require', 'readonly', 'disabled', 'hidden', 'label', 'value', 'badge', 'template']; export const eventPropArr = ['onClick', 'onRowSelect']; export interface MetaData { "id": string, "title": string, "mdmInfo": { "mdmId": string, "mdmSubject": string, "mdmCategory1": string, "mdmCategory2": string, "mdmCategory3": string }, "valueStrategy": { "nullable": boolean, "nullableCreate": boolean, "beforeStrategy": string, "insertStrategy": string, "updateStrategy": string, "dict": any }, "viewInfo": { "name": string, "editType": "编码类" | "代码类" | "标志类" | "文本类" | "金额类" | "比例类" | "数值类" | "日期类" | "时间类" | "日期时间类", "columnWidth": number, "label": string, "searchWidget": string, "sortable": boolean, "filterable": boolean }, "dbInfo": { "table": string, "field": string, "fieldLen": number, "fieldType": string, "fk": boolean, "pk": boolean, "version": string }, "attributes": {} } export const metaViewType = { "编码类": "text", "代码类": "text", "标志类": "checkbox", "文本类": "text", "金额类": "number", "比例类": "number", "数值类": "number", "日期类": "datetime", "时间类": "datetime", "日期时间类": "datetime" } let metaDataList: MetaData[]; export function setMetaDataList(m: MetaData[]) { metaDataList = m; } export function getMetaDataList(): MetaData[] { return metaDataList; } export function getMetaData(metaId: string): MetaData | null { if (metaDataList && metaDataList.length > 0) { for (let i = 0; i < metaDataList.length; i++) { const metaData = metaDataList[i]; if (metaData.id === metaId) { return metaData; } } } return null; } /** * 深度遍历 vjson * @param vjson 视图JSON * @param resolver 分析函数 * @return 记录 resolver 方法返回 true 时,对象的访问路径 */ export function deepTravVJson(vjson: VJson, resolver: (child: VJson) => any): Array { const markerList: Array = [] deepTrav1(vjson, resolver, markerList) return markerList } /** * 深度遍历 vjson 后,所有 mark 对象及访问方法 */ export interface PathMarker { id: number keyName: string object: any } function deepTrav1(obj: VJson, resolver: (child: VJson) => any, marker?: Array) { let isMarker = resolver(obj) if (marker && isMarker === true) { marker.unshift({ id: 0, keyName: '', object: obj }) } _.forOwn(obj, (value, key) => { if (typeof value !== 'object') { return } if (_.has(obj, 'view') && obj.view === 'toolbar') { if (key === 'dataSource' || key === 'suggest' || key === 'options') { return } } else { // 模型、数据、数据源不扫描 if ( key === 'data' || key === 'dataSource' || key === 'suggest' || key === 'options' ) { return } } //事件不扫描 if (key === 'on' && _.isObject(value)) { return } if (_.startsWith(key, 'on') && _.size(key) > 2) { return } const isMarkerChild = deepTrav1(value, resolver, marker) if (marker && isMarkerChild === true) { isMarker = true marker.unshift({ id: obj.id, keyName: key, object: value }) } }) // 删除作废对象 _.remove(obj, (v: any) => v && v.isFiltered); return isMarker } /** * 根据 vjson 中的 ctlName, 合并属性 * @param vjson 原始 vjson 视图 * @param ctlOption 要被扩展的 ctlName 属性对 */ export function viewExtend(vjson: VJson, ctlOption: any): VJson { // 收集所有 ctlName 属性,放到收集器的 collector.ctls // 收集所有 entityName 属性,放到收集器的 collector.entity const collector = { ctls: {} } deepTravVJson(vjson, obj => { if (obj.ctlName) { _.set(collector.ctls, obj.ctlName, obj) } }) _.forOwn(ctlOption, (extendVJson, ctlName) => { if (_.has(collector.ctls, ctlName)) { _.merge(_.get(collector.ctls, ctlName), extendVJson) } }) return vjson } const $internalHooks = ['data', 'destory', 'watches', 'computed'] /** * 收集成员变量 */ function collectDataFromConstructor(vm: Vue, Component: any) { // override _init to prevent to init as Vue instance const originalInit = Component.prototype._init Component.prototype._init = function (this: Vue) { // proxy to actual vm const keys = Object.getOwnPropertyNames(vm) // 2.2.0 compat (props are no longer exposed as self properties) if (vm.$options.props) { for (const key in vm.$options.props) { if (!vm.hasOwnProperty(key)) { keys.push(key) } } } keys.forEach(key => { if (key.charAt(0) !== '_') { Object.defineProperty(this, key, { get: () => _.get(vm, key), set: value => { _.set(vm, key, value) }, configurable: true }) } }) } // should be acquired class property values const data = new Component() // restore original _init to avoid memory leak (#209) Component.prototype._init = originalInit // create plain data object const plainData = {} Object.keys(data).forEach(key => { if (data[key] !== undefined) { _.set(plainData, key, data[key]) } }) return plainData } /** * 收集成员方法 */ function collectMethods(proto: any, options: any) { Object.getOwnPropertyNames(proto).forEach(key => { // 如果是constructor,则不处理。 if (key === 'constructor') { return } // 记录下每级的 viewExtend 方法 if (key === 'viewExtend') { // options.viewExtends.push(proto[key]); console.error( '不支持使用 viewExtend, 请考虑使用 YvanUI.viewExtend(super.viewResolver(),{})' ) return } // 记录下每级的 viewIntercept 方法 if (key === 'viewIntercept') { // options.viewIntercepts.push(proto[key]); console.error('不支持使用 viewIntercept, 请考虑使用 super.viewResolver()') return } // 记录下每级的 onLoad 方法 if (key === 'onLoad') { options.onLoads.push(proto[key]) return } // 记录下每级的 onShow 方法 if (key === 'onShow') { options.onShows.push(proto[key]) } // 如果原型属性(方法)名是vue生命周期钩子名,则直接作为钩子函数挂载在options最外层 if ($internalHooks.indexOf(key) > -1) { options[key] = proto[key] return } // 先获取到原型属性的descriptor。 // 在前文已提及,计算属性其实也是挂载在原型上的,所以需要对descriptor进行判断 const descriptor = Object.getOwnPropertyDescriptor(proto, key)! if (descriptor.value !== void 0) { // 如果属性值是一个function,则认为这是一个方法,挂载在methods下 if (typeof descriptor.value === 'function') { if (!_.has(options.methods, key)) { options.methods[key] = descriptor.value } } else { // 如果不是,则认为是一个普通的data属性。 // 但是这是原型上,所以更类似mixins,因此挂在mixins下。 options.mixins.push({ data(this: Vue) { return { [key]: descriptor.value } } }) } } else if (descriptor.get || descriptor.set) { // 如果value是undefined(ps:void 0 === undefined)。 // 且描述符具有get或者set方法,则认为是计算属性。不理解的参考我上面关于class转换成构造函数的例子 // 这里可能和普通的计算属性不太一样,因为一般计算属性只是用来获取值的,但这里却有setter。 // 不过如果不使用setter,与非class方式开发无异,但有这一步处理,在某些场景会有特效。 ; (options.computed || (options.computed = {}))[key] = { get: descriptor.get, set: descriptor.set } } }) } /** * 基础混入器 */ function createCommonMix() { return {} } /** * 判断组件是否被删除后,是否需要预占宽度 */ function cmpNeedOccupied(code: any) { switch (code.view) { case 'text': case 'combo': case 'date': case 'datetime': case 'datepicker': // case 'button': // button 按钮直接删除即可,不需要预占宽度 case 'checkbox': case 'label': return true; } // 高度和宽度可变的组件,不需要预占任何宽度 return false; } /** * 根据 vjson 格式,嵌入 yvan 组件, 生成能够为 webix 使用的 vjson */ export function wrapperWebixConfig(module: BaseModule, vjson: VJson) { // 形成最终的 viewResolver 方法 deepTravVJson(vjson, obj => { // 检查组件是否被过滤(不渲染) let isFiltered = false; const componentRenderFilter: Function = _.get(window, 'YvanUI.componentRenderFilter'); if (componentRenderFilter && componentRenderFilter(obj) === false) { isFiltered = true; } if (isFiltered) { // 组件不需要渲染 if (_.has(obj, 'hiddenPlaceholder')) { // 如果有 hiddenPlaceholder 属性,最优先使用 hiddenPlaceholder, 替换现有的 vjson const ph = obj.hiddenPlaceholder; _.forOwn(obj, (v, k) => delete obj[k]); _.assign(obj, ph); } else if (cmpNeedOccupied(obj)) { // 需要预占宽度的组件,变成 template _.forOwn(obj, (v, k) => { if (k !== 'height' && k !== 'width') { delete obj[k] } }); obj.view = "template"; obj.borderless = true; obj.template = ""; } else { // 其他情况,直接告知外循环,删除本对象 obj.isFiltered = true; } return; } if (obj.view === 'iframe') { return } // if (module && _.has(obj, 'id')) { // if (obj.id !== 'MainWindowFirstPage') { // console.error('禁用 ID 属性', obj) // } // } const beforeResolverVJson: Function = _.get(window, 'YvanUI.beforeResolverVJson'); if (beforeResolverVJson !== undefined && typeof beforeResolverVJson === "function") { obj = beforeResolverVJson(module, obj); } deepTravVJson(vjson, (ctlVJson: any) => { exprPropArr.forEach(prop => { if (_.has(ctlVJson, prop + '.$watch')) { delete ctlVJson[prop]; } }) }) if (typeof obj.placeId === 'string') { obj.id = module.instanceId + '$' + obj.placeId } if (typeof obj.view === 'string') { switch (obj.view) { case 'button': CtlButton.create(module, obj); break case 'text': CtlText.create(module, obj); break case 'number': case 'counter': CtlNumber.create(module, obj); break case 'datepicker': case 'date': case 'datetime': CtlDatePicker.create(module, obj); break case 'codemirror-editor': CtlCodeMirror.create(module, obj); break case 'dataview': CtlDataview.create(module, obj); break case 'datatable': CtlDatatable.create(module, obj); break case 'xterm': CtlXterm.create(module, obj); break case 'bpmn': CtlBpmn.create(module, obj); break case 'xconsolelog': CtlConsoleLog.create(module, obj); break case 'echarts': CtlECharts.create(module, obj); break case 'sidebar': CtlSidebar.create(module, obj); break case 'daterangepicker': case 'daterange': case 'datetimerange': CtlDateRangePicker.create(module, obj); break case 'combo': case 'combobox': CtlCombo.create(module, obj); break case 'menu': CtlMenu.create(module, obj); break case 'multicombo': CtlMultiCombo.create(module, obj); break case 'search': case 'searchbox': CtlSearch.create(module, obj); break case 'textarea': CtlTextarea.create(module, obj); break case 'check': case 'checkbox': CtlCheckBox.create(module, obj); break case 'checkboxgroup': CtlCheckBoxGroup.create(module, obj); break case 'switch': case 'switchbox': CtlSwitch.create(module, obj); break case 'radio': case 'radiobox': CtlRadio.create(module, obj); break case 'tree': CtlTree.create(module, obj); break case 'treetable': CtlTreeTable.create(module, obj); break case 'tabview': CtlTab.create(module, obj); break case 'grid': CtlGrid.create(module, obj); break case 'form': CtlForm.create(module, obj); break case 'carousel': CtlCarousel.create(module, obj); break case 'uploader': CtlUploader.create(module, obj); break case 'image': CtlImage.create(module, obj); break case 'divider': CtlDivider.create(module, obj); break case 'viewer': CtlViewer.create(module, obj); break case 'excelImport': CtlExcelImport.create(module, obj); break case 'editor': CtlEditor.create(module, obj); break } } }) if (module) { vjson.$scope = module } } /** * 表达式绑定的执行方法 * @param id 原始的vjson的id * @param prop 绑定的vjson的属性 */ function computPropChangeHandle(id: any, prop: string) { return function (this:any, nv: any) { let webixHandle: any = webix.$$( this.instanceId + "_" + id); if (webixHandle) { webixHandle.$view.setAttribute("notDeferTag",true) if (prop === 'hidden') { if (nv) { // webixHandle.hide(); // webixHandle.$view.style.opacity = 0 webixHandle.$view.style.display = 'none' } else { // webixHandle.show(); // webixHandle.$view.style.opacity = 1 webixHandle.$view.style.display = 'inline-block' } } else { webixHandle.define(prop, nv); webixHandle.refresh(); } } else { _.defer(() => { webixHandle = webix.$$(this.instanceId + "_" + id); if (!webixHandle) { return; } if (!webixHandle.$view.getAttribute("notDeferTag")) { return; } if (prop === 'hidden') { if (nv) { // webixHandle.hide(); // webixHandle.$view.style.opacity = 0 webixHandle.$view.style.display = 'none' } else { // webixHandle.show(); // webixHandle.$view.style.opacity = 1 webixHandle.$view.style.display = 'inline-block' } } else { webixHandle.define(prop, nv); webixHandle.refresh(); } }) } }; } /** * 将传统 ts Class 转换为 vue 对象. * 普通模块对象和 dialog 对象都要经过转换 */ export function componentFactory(Component: BaseModule & any, options: any = {}): any { //return VueComponent>(options) if (!options.mixins) { options.mixins = [] } if (!options.methods) { options.methods = {} } if (!options.viewExtends) { options.viewExtends = [] } if (!options.viewIntercepts) { options.viewIntercepts = [] } if (!options.onLoads) { options.onLoads = [] } if (!options.onShows) { options.onShows = [] } // 获取构造函数原型,这个原型上挂在了该类的method for ( let proto = Component.prototype; proto.constructor !== Vue; proto = Object.getPrototypeOf(proto) ) { // 一直向上收集, 直到 Object 级 collectMethods(proto, options) } // 添加公共基础 MIX options.mixins.push(createCommonMix()) // 解析 @Watch 装饰器方法,执行 watch const watches = Component.prototype.watches delete Component.prototype.watches const computMix: any = { watch: {}, computed: {} }; let vjsonTemp: any = undefined //@ts-ignore if (!window['_isDesigner']) { /** * 在非设计模式的时候,要解析vjson中的表达式, 并借助computed的方法 进行双向绑定 */ try { vjsonTemp = Component.prototype.__proto__.viewResolver(); // if (Component.prototype.__proto__.vjsonExtend) { // vjsonTemp = Component.prototype.__proto__.vjsonExtend(vjsonTemp); // } } catch (e) { } if (vjsonTemp) { deepTravVJson(vjsonTemp, (ctlVJson: any) => { exprPropArr.forEach(prop => { if (_.has(ctlVJson, prop + '.$watch')) { // const methodName = _.uniqueId('propComput'); /** * 为了方便校验,computed的方法名和原始vjson设置的viewID保持一致, 用实例Id(instanceId)作为区分各个界面实例的标记 */ const id = _.uniqueId('ctlIdForComput') let handle; if (_.toUpper(_.get(ctlVJson, prop + '.$watch')).indexOf('RETURN') >= 0) { // 包含 return 语句 handle = Function(_.get(ctlVJson, prop + '.$watch')) } else { handle = Function('return ' + _.get(ctlVJson, prop + '.$watch')) } ctlVJson.id = id computMix.watch[id] = { immediate: true, handler: computPropChangeHandle(id, prop) } computMix.computed[id] = handle } }) }) options.mixins.push(computMix) // Component.prototype.__proto__.viewResolver = ()=>{ // return vjsonTemp // } } } // 实例化Component构造函数,并收集其自身的(非原型上的)属性导出 options.mixins.push({ data(this: Vue) { const data: any = { vjsonFinally: vjsonTemp, refs: {}, dialog: undefined, inParamter: undefined, dialogParent: undefined, instanceId: _.uniqueId('_mins_id_'), loadFinished: false, isDesignMode: false, ...collectDataFromConstructor(this, Component) } return data }, created(this: Vue & any) { if (_.isArray(watches)) { _.each(watches, (item: BizWatch) => { this.$watch(item.expr, item.handler, { deep: item.deep, immediate: item.immediate }) //console.log('$watch', item.expr, item.handler, { deep: item.deep, immediate: item.immediate }) }) } }, destroyed() { }, methods: { onLoad(this: Vue) { _.each(options.onLoads, load => { load.call(this) }) const afterModuleRender: Function = _.get(window, 'YvanUI.afterModuleRender'); if (afterModuleRender !== undefined && typeof afterModuleRender === "function") { afterModuleRender(this); } }, onShow(this: Vue) { _.each(options.onShows, load => { load.call(this) }) }, buildView(this: Vue & BaseModule) { // 获取到 viewResolver 方法,拿到最原始的 vjson // @ts-ignore let vjson = _.cloneDeep(this.vjsonFinally ? this.vjsonFinally : this.viewResolver()); let isScroll = _.has(vjson, "_designMode") && (vjson._designMode === "scroll-dialog" || vjson._designMode === "scroll-module"); if (typeof this.vjsonExtend === "function") { vjson = this.vjsonExtend(vjson) delete this.vjsonExtend } // 查看合并后的vjson有没有_designMode属性 if (!isScroll) { isScroll = _.has(vjson, "_designMode") && (vjson._designMode === "scroll-dialog" || vjson._designMode === "scroll-module"); } if (isScroll) { vjson = { view: "scrollview", body: { css: "autoHeight", ...vjson } } } deepTravVJson(vjson,(ctl)=>{ if(ctl.hasOwnProperty("id") && (ctl.id+"").startsWith("ctlIdForComput")){ // 如果有绑定的watch(表达式)的id 就 再加上前缀 ctl.id = this.instanceId + "_" +ctl.id; } }) // 与 yvan 组件进行交换,使 vjson 能被 webix 使用 wrapperWebixConfig(this, vjson) return vjson }, closeDialog(this: Vue & any) { this.dialog.close() }, setInParamter(this: Vue & BaseModule & any, inParamter: INP) { this.inParamter = inParamter }, showDialog(this: Vue & BaseModule & any, inParamter: INP, container: any, isFromSearchBox: boolean = false): void { // 显示对话框 const module: any = this module.inParamter = inParamter module.dialogParent = container // 获取到 viewResolver 方法,拿到最原始的 vjson let vjson = _.cloneDeep(this.vjsonFinally ? this.vjsonFinally : this.viewResolver()); deepTravVJson(vjson,(ctl)=>{ if(ctl.hasOwnProperty("id") && (ctl.id+"").startsWith("ctlIdForComput")){ // 如果有绑定的watch(表达式)的id 就 再加上前缀 ctl.id = this.instanceId + "_" +ctl.id; } }) let isScroll = _.has(vjson, "_designMode") && (vjson._designMode === "scroll-dialog" || vjson._designMode === "scroll-module"); if (typeof this.vjsonExtend === "function") { vjson = this.vjsonExtend(vjson) delete this.vjsonExtend } if (!_.has(vjson, 'body')) { vjson.body = { template: 'dialog 没有 body' } } else { if (!_.has(vjson.body, 'padding')) { vjson.body.padding = 10 } } // 查看合并后的vjson有没有_designMode属性 if (!isScroll) { isScroll = _.has(vjson, "_designMode") && (vjson._designMode === "scroll-dialog" || vjson._designMode === "scroll-module"); } if (isScroll) { vjson.body = { view: "scrollview", body: { css: "autoHeight", ...vjson.body } } } // 与 yvan 组件进行交换,使 vjson 能被 webix 使用 wrapperWebixConfig(module, vjson.body) // 构建 window _.merge(vjson, { view: 'window', close: vjson.close === undefined ? true : vjson.close, move: vjson.move === undefined ? true : vjson.move, modal: vjson.modal === undefined ? true : vjson.modal, left: vjson.left, top: vjson.top, position: (vjson.left || vjson.top) ? '' : 'center', resize: vjson.resize === undefined ? true : vjson.resize, head: { view: "toolbar", margin: -4, cols: [ { view: "label", label: vjson.title, css: 'webix_header webix_win_title', on: { onAfterRender() { module._titleLabel = this; } } }, { view: "icon", icon: "fa fa-window-maximize", click: function (this: any) { dialog.config.fullscreen = !dialog.config.fullscreen; if (dialog.config.fullscreen) { dialog.config.oldtop = dialog.config.top; dialog.config.oldleft = dialog.config.left; dialog.config.left = 0; dialog.config.top = 0; dialog.config.move = false; this.define({ icon: 'fa fa-window-restore' }); } else { dialog.config.top = dialog.config.oldtop; dialog.config.left = dialog.config.oldleft; dialog.config.move = true; this.define({ icon: 'fa fa-window-maximize' }); } dialog.resize(); dialog.show(); this.refresh(); } }, { view: "icon", icon: "fa fa-times", title: '关闭', tooltip: '关闭', click: function () { if (dialog.closeWithConfirm) { YvanMessage.confirm("确定关闭吗").then(() => { dialog.close() }).catch(e => { }) } else { dialog.close() } } } ] }, on: { onShow() { module.onLoad() module.loadFinished = true }, onDestruct() { module.onClose() }, }, }) const dialog: any = webix.ui(vjson) dialog.closeWithConfirm = vjson.closeWithConfirm module._webixId = this.dialog = dialog this.dialog.show(); $(this.dialog.$view).keypress((event) => { if (event.keyCode === 27) { module.onEsc() event.stopPropagation() event.preventDefault() return; } if (event.keyCode === 13) { module.onEnter() if (isFromSearchBox) { event.stopPropagation() } event.preventDefault() return; } }) } } }) // 通过 vue.extend 生成一个 vue 类型的功能模块对象 const VueModule: any = Vue.extend(options) return VueModule } /** * 在目标 DOM 选择器上渲染模块 */ export function render(selector: string, baseModule: BaseModule) { const lastHandle = _.get(window, 'LastRenderHandler') if (lastHandle) { lastHandle.destructor() _.set(window, 'LastRenderHandler', null) } const cfg = (baseModule as any).buildView() cfg.container = selector baseModule._webixId = webix.ui(cfg) baseModule.onLoad() webix.event(window, 'resize', () => { // $$(baseModule._webixId).resize(); baseModule._webixId.resize() }) _.set(window, 'LastRenderHandler', baseModule._webixId) return baseModule._webixId } /** * 在占位空间 spaceId 上渲染 vjson * @param module 当前模块 * @param spaceId 占位空间 * @param vjson 界面描述 */ export function renderPlace( module: BaseModule, spaceId: string, vjson: VJson ) { wrapperWebixConfig(module, vjson) webix.ui(vjson, webix.$$(module.instanceId + '$' + spaceId)) }