소스 검색

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/init.ts
yuliang 4 년 전
부모
커밋
97e8de61a0
13개의 변경된 파일754개의 추가작업 그리고 20개의 파일을 삭제
  1. 21 0
      src/Defaults.ts
  2. 55 0
      src/controls/base.ts
  3. 24 0
      src/controls/cols.js
  4. 31 12
      src/controls/combo.js
  5. 27 0
      src/controls/rows.js
  6. 19 7
      src/controls/textfield.js
  7. 2 0
      src/index.ts
  8. 6 0
      src/init.ts
  9. 295 0
      src/lib/ajax.ts
  10. 148 0
      src/lib/config.ts
  11. 23 0
      src/lib/lib.ts
  12. 102 0
      src/types.ts
  13. 1 1
      src/utils.ts

+ 21 - 0
src/Defaults.ts

@@ -3,3 +3,24 @@ export const windows = {
     width: 900,
     height: 600,
 }
+
+export const combo = {
+    labelAlign: 'right',
+    labelWidth: 70,
+    selectOnFocus: true,
+}
+
+export const text = {
+    labelAlign: 'right',
+    labelWidth: 70,
+}
+
+export const maintab = {}
+
+export const rows = {}
+
+export const cols = {
+    defaults: {
+        flex: 1
+    },
+}

+ 55 - 0
src/controls/base.ts

@@ -0,0 +1,55 @@
+import _ from 'lodash'
+
+type DragType = 'row-container' | 'col-container' | 'col-item' | 'row-item'
+
+/**
+ * 构建所有组件的公共属性
+ * @param config 原始config
+ * @param dragType 组件模式
+ *      不填,代表不能在设计时被拖拽
+ *      row-container 是一个rows容器
+ *      col-container 是一个cols容器
+ *      col-item cols中的一个格子(非容器),比如 textfield / combofield 等等
+ *      row-item rows中的一行,比如 tree / grid / panel 等等
+ */
+export function baseConfig(config, dragType?: DragType) {
+    if (config.designMode && dragType) {
+
+        let cc = ''
+        switch (dragType) {
+            case "col-container":
+                cc = 'design_col_container'
+                break
+
+            case "col-item":
+                cc = 'design_col_item'
+                break
+
+            case "row-container":
+                cc = 'design_row_container'
+                break
+
+            case "row-item":
+                cc = 'design_row_item'
+                break
+        }
+
+        if (typeof config.cls === 'string') {
+            _.extend(config, {
+                cls: [config.cls, 'yvan_design', cc]
+            })
+
+        } else if (_.isArray(config.cls)) {
+            _.extend(config, {
+                cls: [...config.cls, 'yvan_design', cc]
+            })
+
+        } else {
+            _.extend(config, {
+                cls: ['yvan_design', cc]
+            })
+        }
+    }
+
+    return config
+}

+ 24 - 0
src/controls/cols.js

@@ -0,0 +1,24 @@
+import _ from 'lodash'
+import {cols} from '../Defaults'
+import {baseConfig} from "./base";
+
+export default function () {
+
+    Ext.define('Yvan.Cols', {
+        extend: 'Ext.container.Container',
+        xtype: 'cols',
+
+        constructor(config) {
+            const self = this
+            const newConfig = _.defaultsDeep({
+                // 强制性属性
+                layout: 'hbox',
+                minHeight: config.designMode ? 32 : 0
+
+            }, baseConfig(config, 'col-container'), config, cols)
+
+            this.superclass.constructor.call(self, newConfig)
+        },
+    });
+
+}

+ 31 - 12
src/controls/combo.js

@@ -1,16 +1,35 @@
+import _ from 'lodash'
+import {combo} from '../Defaults'
+import {baseConfig} from "./base";
+
 export default function () {
-    /**
-     * 改变必填项之前加星号
-     */
-    const t1 = Ext.form.field.Text.prototype.initComponent
-    Ext.form.field.Text.override({
-        initComponent: function () {
-            if (this.allowBlank === false || this.validateBlank === true) {
-                this.beforeLabelTextTpl = [
-                    '<span style="color:red;font-weight:bold" data-qtip="必填选项">*</span>'
-                ];
-            }
-            t1.call(this)
+    const cc = Ext.form.field.ComboBox.prototype.constructor
+    const {initComponent} = Ext.form.field.ComboBox.prototype
+    Ext.form.field.ComboBox.override({
+        constructor(config) {
+            const newConfig = _.defaultsDeep({
+                // 强制属性
+            }, baseConfig(config, 'col-item'), config, combo)
+
+            cc.call(this, newConfig)
+        },
+
+        initComponent() {
+            this.on({
+                focus: {
+                    // 获得焦点后自动下拉
+                    fn(sender) {
+                        sender.expand();
+                        this.doQuery(this.allQuery, true);
+                    },
+                },
+            })
+
+            initComponent.call(this)
+        },
+
+        reload() {
+
         }
     });
 }

+ 27 - 0
src/controls/rows.js

@@ -0,0 +1,27 @@
+import _ from 'lodash'
+import {rows} from '../Defaults'
+import {baseConfig} from "./base";
+
+export default function () {
+
+    Ext.define('Yvan.Rows', {
+        extend: 'Ext.container.Container',
+        xtype: 'rows',
+
+        constructor(config) {
+            const self = this
+            const newConfig = _.defaultsDeep({
+                // 强制性属性
+                layout: 'anchor', border: false,
+                defaults: {
+                    border: false, anchor: '100%',
+                    margin: '0 0 5 0',
+                },
+
+            }, baseConfig(config, 'row-container'), config, rows)
+
+            this.superclass.constructor.call(self, newConfig)
+        },
+    });
+
+}

+ 19 - 7
src/controls/textfield.js

@@ -1,19 +1,31 @@
 import _ from 'lodash'
-import $ from 'jquery'
+import {baseConfig} from "./base";
+import {text} from "../Defaults";
 
 export default function () {
-    /**
-     * 改变必填项之前加星号
-     */
-    const t1 = Ext.form.field.Text.prototype.initComponent
+
+    const cc = Ext.form.field.ComboBox.prototype.constructor
+    const {initComponent} = Ext.form.field.Text.prototype
     Ext.form.field.Text.override({
-        initComponent: function () {
+        constructor(config) {
+            const newConfig = _.defaultsDeep({
+                // 强制属性
+            }, baseConfig(config, 'col-item'), config, text)
+
+            cc.call(this, newConfig)
+        },
+
+        initComponent() {
+
+            /**
+             * 改变必填项之前加星号
+             */
             if (this.allowBlank === false || this.validateBlank === true) {
                 this.beforeLabelTextTpl = [
                     '<span style="color:red;font-weight:bold" data-qtip="必填选项">*</span>'
                 ];
             }
-            t1.call(this)
+            initComponent.call(this)
         }
     });
 }

+ 2 - 0
src/index.ts

@@ -17,5 +17,7 @@ export {
     Defaults
 }
 
+export * from './lib/ajax'
 export * from './Scope'
 export * from './init'
+export * from './lib/config'

+ 6 - 0
src/init.ts

@@ -4,6 +4,9 @@ import initTextfield from './controls/textfield'
 import initToolbar from './controls/toolbar/toolbar'
 import initContainer from './controls/container'
 import initSplitter from './controls/splitter'
+import initCombo from './controls/combo'
+import initRows from './controls/rows'
+import initCols from './controls/cols'
 
 export function init() {
     // 引入 filters 过滤插件
@@ -55,6 +58,9 @@ export function init() {
 
     initMainTab()
     initTextfield()
+    initCombo()
+    initRows()
+    initCols()
     initToolbar()
     initContainer()
     initSplitter()

+ 295 - 0
src/lib/ajax.ts

@@ -0,0 +1,295 @@
+import axios, {AxiosRequestConfig, AxiosResponse} from 'axios'
+import {PlainObject} from "../types";
+import _ from 'lodash'
+import Qs from 'qs'
+import {serverInvokeUrlTransform, sqlUrlTransform, ajax} from "./config";
+
+export type ApiMethod =
+    'get'
+    | 'post'
+    | 'put'
+    | 'patch'
+    | 'delete'
+    | 'download'
+    | 'post-json'
+    | 'post-file'
+    | 'invoke'
+    | 'sql'
+
+export type ApiDataType = 'json' | 'form-data' | 'form'
+
+/**
+ * 调用服务器 Ajax
+ */
+export function invokeServer(url: string, ...args: any[]) {
+    // @ts-ignore
+    return ajax.func({
+        url: url,
+        method: 'invoke',
+        args: args
+    })
+}
+
+export interface ApiObject {
+    url: string
+    param: PlainObject
+    method: ApiMethod
+    data?: object
+    args: any[],
+    db?: string
+    headers?: PlainObject
+    filterModel?: { [key: string]: any; };
+    sortModel?: { colId: string | undefined; sort: string | null | undefined; }[];
+    config?: {
+        withCredentials?: boolean
+        cancelExecutor?: (cancel: Function) => void
+    }
+    dataType?: ApiDataType
+
+    /**
+     * 下载文件名
+     */
+    fileName?: string,
+
+    /**
+     * 上传文件(如果需要的话)
+     */
+    file?: any,
+}
+
+
+export type ApiFunction = (api: ApiObject) => Promise<ApiResult>;
+
+/**
+ * 组件中 Api 的声明
+ */
+export type Api = ApiFunction | ApiObject;
+
+export interface ApiResult {
+    rawData: object
+    status: number
+    headers: object
+    data?: any
+    pagination?: any
+    success: boolean
+    msg: string
+    errors?: {
+        [propName: string]: string
+    }
+}
+
+export interface CreateAjaxOption {
+    baseUrl: string
+    timeout: number
+}
+
+
+/**
+ * 创建一个 Ajax 客户端
+ */
+export function createAjax(createOption: CreateAjaxOption): ApiFunction {
+
+    if (createOption.baseUrl) {
+        axios.defaults.baseURL = createOption.baseUrl
+        axios.defaults.timeout = createOption.timeout
+        axios.defaults.timeoutErrorMessage = '网络超时'
+    }
+
+    return function (option: ApiObject) {
+
+        //@ts-ignore
+        option.method = (option.method || 'get').toLocaleLowerCase();
+
+        //@ts-ignore
+        const ax: AxiosRequestConfig = {
+            ...option
+        };
+
+        switch (option.method) {
+            case 'get':
+                ax.method = 'GET';
+                ax.params = option.param;
+                ax.headers = {
+                    ...option.headers
+                };
+                break
+
+            case 'post':
+                ax.method = 'POST';
+                ax.headers = {
+                    'Content-Type': 'application/x-www-form-urlencoded',
+                    ...option.headers
+                };
+                ax.params = option.param;
+                ax.data = Qs.stringify(option.data);
+                break
+
+            case 'put' :
+            case 'patch':
+            case 'delete':
+                ax.method = option.method;
+                ax.headers = option.headers;
+                ax.params = option.param;
+                ax.data = Qs.stringify(option.data);
+                break
+
+            case 'download':
+                downLoad(createOption.baseUrl + option.url, option.fileName || 'file',
+                    option.data, option.headers);
+                return new Promise<ApiResult>((resolver, reject) => {
+                });
+                break
+
+            case "invoke":
+                ax.url = serverInvokeUrlTransform(option.url)
+                ax.method = 'POST';
+                ax.headers = {
+                    'Content-Type': 'application/json',
+                    ...option.headers
+                };
+                if (typeof option.args === 'object') {
+                    ax.data = JSON.stringify({
+                        args: option.args,
+                    })
+                } else {
+                    ax.data = JSON.stringify({
+                        args: [
+                            {
+                                ...option.data,
+                                ...option.param
+                            }
+                        ],
+                        filterModel: option.filterModel,
+                        sortModel: option.sortModel
+                    })
+                }
+                break
+
+            case 'post-json':
+                ax.method = 'POST';
+                ax.headers = {
+                    'Content-Type': 'application/json',
+                    ...option.headers
+                };
+                ax.data = JSON.stringify({...option.data, filterModel: option.filterModel, sortModel: option.sortModel})
+                break
+
+            case 'post-file':
+                //TODO 刘壮. 上传文件
+                var forms = new FormData();
+                ax.headers = {
+                    'Content-Type': 'multipart/form-data',
+                    ...option.headers
+                };
+                _.forOwn(option.data, (value, key) => {
+                    if (key === 'files') {
+                        let i = 0;
+                        _.each(value, f => {
+                            // @ts-ignore
+                            forms.append('file' + (++i), f)
+                        })
+                    } else {
+                        forms.append(key, value)
+                    }
+                });
+                ax.data = forms;
+                ax.method = 'POST'
+                break
+
+            case "sql":
+                ax.url = sqlUrlTransform(option.url)
+                ax.method = 'POST';
+                ax.headers = {
+                    'Content-Type': 'application/json',
+                    ...option.headers
+                };
+                ax.data = JSON.stringify({
+                    args: [option.data],
+                    db: option.db,
+                    filterModel: option.filterModel,
+                    sortModel: option.sortModel
+                })
+                break
+
+            default:
+                throw new Error('not implements')
+        }
+
+        return new Promise<ApiResult>((resolver, reject) => {
+            axios(ax).then((resolverRaw: AxiosResponse<any>) => {
+                const apiResult: ApiResult = {
+                    rawData: resolverRaw.data,
+                    status: resolverRaw.status,
+                    success: (resolverRaw.data && resolverRaw.data.success),
+                    data: resolverRaw.data.data,
+                    pagination: resolverRaw.data.pagination,
+                    msg: (resolverRaw.data.msg),
+                    errors: resolverRaw.data.errors,
+                    headers: resolverRaw.headers
+                }
+                resolver(apiResult)
+
+            }).catch((reason: any) => {
+                reject(reason)
+            })
+        })
+    }
+}
+
+
+export function downLoad(downLoadUrl: string, filename: string, data: any, header: any, isJson: boolean = false) {
+    const YvanUI: any = _.get(window, 'YvanUI');
+    YvanUI.loading();
+    const createObjectURL = (object: any) => {
+        return (window.URL) ? window.URL.createObjectURL(object) : _.get(window, 'webkitURL').createObjectURL(object)
+    };
+
+    // const formData = new FormData();
+    // _.forOwn(data, (v, k) => {
+    //     formData.append(k, v);
+    // });
+    let formData = '';
+
+    const xhr = new XMLHttpRequest();
+    xhr.open('POST', downLoadUrl);
+    xhr.responseType = 'blob';
+    // xhr.setRequestHeader('Authorization', $.cookie('auth'))
+    if (isJson) {
+        formData = data ? JSON.stringify(data) : '';
+        xhr.setRequestHeader('Content-Type', 'application/json');
+    } else {
+        formData = data ? Qs.stringify(data) : '';
+        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+    }
+    //
+    if (header) {
+        _.forOwn(header, (v, k) => {
+            xhr.setRequestHeader(k, v);
+        })
+    }
+    xhr.onload = function (e: any) {
+        if (this.status === 200) {
+            const blob = this.response;
+            if (_.hasIn(window, 'navigator.msSaveOrOpenBlob')) {
+                navigator.msSaveBlob(blob, filename)
+                YvanUI.clearLoading();
+
+            } else {
+                const a = document.createElement('a')
+                const url = createObjectURL(blob)
+                a.href = url
+                a.download = filename
+                document.append(a)
+                a.click()
+                a.remove()
+                window.URL.revokeObjectURL(url)
+                YvanUI.clearLoading()
+            }
+        }
+    };
+    xhr.send(formData);
+}
+
+
+
+

+ 148 - 0
src/lib/config.ts

@@ -0,0 +1,148 @@
+import {VJson, VJsonProcess} from "../types";
+import {ApiFunction} from "./ajax";
+import _ from 'lodash'
+
+export type ServerInvokeUrlTransformFunction = (jsUrl: string) => string | undefined;
+
+export type SqlUrlTransformFunction = (jsUrl: string) => string | undefined;
+
+/**
+ * YvanUI 全局扩展配置
+ */
+export interface ConfigOption {
+
+    /**
+     * 是否在设计器模式
+     */
+    designMode: boolean
+
+    /**
+     * 扩展自定义的 vjson 运行时 修改
+     */
+    beforeResolverVJson: (module: any, vJson: VJson) => VJson;
+
+    /**
+     * 扩展自定义的 ajax 方法
+     */
+    ajax: ApiFunction;
+
+    /**
+     * serverJS Url转换为Ajax请求
+     */
+    serverInvokeUrlTransform: ServerInvokeUrlTransformFunction;
+
+    /**
+     * Sql Url转换为Ajax请求
+     */
+    sqlUrlTransform: SqlUrlTransformFunction;
+
+    /**
+     * 组件渲染过滤器, 如果方法返回 false 代表该组件不需要被渲染.
+     * 可以修改 vjson 的内容,但不能删除他
+     */
+    componentRenderFilter: (vjson: any) => undefined | boolean
+
+    /**
+     * 获取拼音首字母的函数
+     */
+    pinyinFunction: (py: string) => string
+
+    /**
+     * 需要添加的 VJsonProcess 处理过程
+     */
+    vjsonProcess: VJsonProcess
+}
+
+/**
+ * 全局 vjson 运行时
+ */
+export let beforeResolverVJson: (undefined | ((module: any, vJson: VJson) => VJson)) = undefined;
+
+/**
+ * 组件渲染过滤器, 如果方法返回 false 代表该组件不需要被渲染.
+ * 可以修改 vjson 的内容,但不能删除他
+ */
+export let componentRenderFilter: (undefined | ((vjson: any) => undefined | boolean)) = undefined;
+
+/**
+ * 全局 ajax 方法
+ */
+export const ajax: {
+    func?: ApiFunction
+} = {}
+
+/**
+ * 计算拼音的函数
+ */
+let pinyinFunc: Function
+
+export const vjsonProcessChain: VJsonProcess[] = []
+
+/**
+ * 添加一个 VJson 的处理过程
+ * @param func VJson 处理函数
+ */
+export function pushVJsonProcess(func: VJsonProcess) {
+    vjsonProcessChain.push(func)
+}
+
+let designMode: boolean = false
+
+export function isDesignMode(): boolean {
+    return designMode
+}
+
+export function setDesignMode(v: boolean) {
+    designMode = v
+}
+
+/**
+ * 将业务定义的 url 转换为调用服务端 groovy 的 url
+ */
+export function serverInvokeUrlTransform(url: string): string {
+    return _.get(window, '_YvanUI_serverInvokePrefix')(url);
+}
+
+/**
+ * 将业务定义的 url 转换为调用服务端 sql 的 Url
+ */
+export function sqlUrlTransform(url: string): string {
+    return _.get(window, '_YvanUI_sqlPrefix')(url);
+}
+
+/**
+ * YvanUI 全局扩展配置
+ */
+export function extend(option: Partial<ConfigOption>) {
+    if (option.beforeResolverVJson) {
+        beforeResolverVJson = option.beforeResolverVJson
+    }
+
+    if (option.ajax) {
+        ajax.func = option.ajax
+    }
+
+    if (option.serverInvokeUrlTransform) {
+        _.extend(window, {_YvanUI_serverInvokePrefix: option.serverInvokeUrlTransform});
+    }
+
+    if (option.sqlUrlTransform) {
+        _.extend(window, {_YvanUI_sqlPrefix: option.sqlUrlTransform});
+    }
+
+    if (option.componentRenderFilter) {
+        componentRenderFilter = option.componentRenderFilter
+    }
+
+    if (option.pinyinFunction) {
+        pinyinFunc = option.pinyinFunction
+    }
+
+    if (typeof option.designMode !== 'undefined') {
+        setDesignMode(option.designMode)
+    }
+
+    if (option.vjsonProcess) {
+        pushVJsonProcess(option.vjsonProcess)
+    }
+}

+ 23 - 0
src/lib/lib.ts

@@ -0,0 +1,23 @@
+import _ from 'lodash'
+import {FunctionRegiste} from "../types";
+
+export const LibList: FunctionRegiste[] = []
+
+export function Lib(registe: FunctionRegiste) {
+    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
+        LibList.push({
+            ...registe,
+            name: propertyKey,
+            target: target[propertyKey]
+        })
+
+        if (registe.type === 'system') {
+            _.set(window, 'system.' + propertyKey, target[propertyKey])
+
+        } else if (registe.type === 'format') {
+            _.set(window, 'format.' + propertyKey, target[propertyKey])
+        }
+
+        return target
+    }
+}

+ 102 - 0
src/types.ts

@@ -0,0 +1,102 @@
+export interface PlainObject {
+    [propsName: string]: any;
+}
+
+export interface VJson {
+    [propName: string]: any;
+}
+
+/**
+ * 处理 VJson 的
+ * @param this 是指 scope 对象
+ * @param vjson 是待处理的 VJson 对象
+ * @result 返回处理 VJson 的 Promise 异步结果
+ */
+export type VJsonProcess = (this: any, vjson: VJson) => Promise<VJson>
+
+/**
+ * 注册函数中的参数
+ */
+export interface FunctionArgument {
+    /**
+     * 列举类型
+     */
+    type: 'module' | 'control'
+
+    /**
+     * 参数中文说明
+     */
+    title: string
+
+    /**
+     * 参数名称
+     */
+    name: string
+
+    /**
+     * 过滤器
+     * @param list
+     */
+    filter?: (items: any[]) => any[]
+}
+
+/**
+ * 注册函数, 用在 @Lib 装饰器
+ */
+export interface FunctionRegiste {
+    /**
+     * 函数名
+     */
+    name?: string
+
+    /**
+     * 中文说明
+     */
+    title?: string
+
+    /**
+     * 作者
+     */
+    author?: string
+
+    /**
+     * 创建时间
+     */
+    createAt?: string
+
+    /**
+     * 更新时间
+     */
+    updateAt?: string
+
+    /**
+     * 备注
+     */
+    remark?: string
+
+    /**
+     * 类型
+     * system: 系统方法
+     * format: 格式化作用
+     * ajaxMethod: AjaxMethod
+     * meta: 元数据
+     */
+    type: 'system' | 'format' | 'ajaxMethod' | 'meta'
+
+    /**
+     * 分类名称
+     */
+    category?: string
+
+    args?: FunctionArgument[]
+
+    /**
+     * 文档链接
+     */
+    link?: string
+
+    /**
+     * 方法体
+     */
+    target?: Function
+}

+ 1 - 1
src/utils.ts

@@ -5,4 +5,4 @@ export function invokeMethod(fn: any, sender: any, args: any) {
     if (typeof fn === 'function') {
         fn.apply(sender, ...args)
     }
-}
+}