hideMap: function()

in src/framework/MUtil.tsx [238:637]


  hideMap: function (database: any, objectFields: MFieldSchema[], uispec?: M3UISpec): HideMap {
    // 构造字段依赖计算的脚本,类似这样:
    // let {name,role,gender,alias,tel,email,citizenship,student} = {"name":"aaa","alias":"bbb","tel":"1"};citizenship=='中国大陆'

    let l1fields = _.uniq( // 名字要唯一
      objectFields
        .filter(e => !!e.name) // 过滤掉没有名字的字段
        .map(e => _.first(e.name.split("."))) // 对于objectFields里类似student.no/student.state的字段,这里只要拼一个student就可以了
    );
    let showIfScript = `let {${l1fields.join(",")}} = data || {};\nlet hide = {`;
    for (let f of objectFields) {
      if (f.showIf) {
        showIfScript += `'${f.name}': !(${f.showIf}),`;
      }
    }
    for (let s of uispec?.segments ?? []) {
      if (s.showIf) {
        showIfScript += `"segment:${s.label}": !(${s.showIf}),`;
      }
    }
    showIfScript += "}\n";

    // 如果一个segment里的所有字段都隐藏,segment就要隐藏
    for (let s of uispec?.segments ?? []) {
      const cond = s.fields.map(f => "(hide['" + f + "'])").join(" && ");
      showIfScript += `if(${cond}) { hide["segment:${s.label}"] = true }\n`;
    }

    showIfScript += "return hide;\n";

    // eslint-disable-next-line no-new-func
    try {
      const fn = Object.keys(SchemaFunc)
      const fv = Object.values(SchemaFunc)
      let hideMap = new Function("_", "data", ...fn, showIfScript)(_, database, ...fv);
      return hideMap;
    } catch (e) {
      console.error("Calc hideMap failed: " + e.message, "function(_,data){" + showIfScript + "}", database);
      return {};
    }
  },

  /**
   * 计算 showIf 的值
   * @returns Boolean
   * @param database 
    * @param objectFields 
   */
  isShow: function (database: any, objectFields: MFieldSchema[], showIfExpr: string) {
    if (!showIfExpr) return true
    let l1fields = _.uniq(
      objectFields
        .filter(e => !!e.name)
        .map(e => _.first(e.name.split(".")))
    );
    let showIfScript = `let {${l1fields.join(",")}} = data || {};\n return ${showIfExpr}`;
    try {
      const fn = Object.keys(SchemaFunc)
      const fv = Object.values(SchemaFunc)
      let res = new Function("_", "data", ...fn, showIfScript)(_, database, ...fv);
      return res;
    } catch (e) {
      console.error("Calc isShow failed: " + e.message, "function(_,data){" + showIfScript + "}", database);
      return true;
    }
  },

  scoreOf: function(f:MFieldSchema, database:any) {
    const v = _.get(database, f.name);
    if(f.type == "enum"){
      return _.toNumber(MUtil.standardFields(f.enumFields)?.find(e=>e.value == v)?.score);
    } else if(f.type == "set"){
      let score = 0;
      const opts = MUtil.standardFields(f.setFields);
      for(let s of opts){
        if(_.find(opts, {value:v})) {
          score += _.toNumber(s.score);
        }
      }
      return score;
    } else {
      return 0;
    }
  },

  /**
   * 查找fs依赖(通过showIf)的所有字段
   * @param fs
   * @param all 
   * @returns fs依赖的所有字段,包含fs。这个数组中下标小的元素,依赖下标大的元素
   */
   dependency: function (fs:MFieldSchema[], all:MFieldSchema[]) {
    // 先建个索引
    const allFieldsIdx = _.keyBy(all, "name");
    
    // 构造未被依赖的集合
    let ndep = _.keyBy(all, "name"); // 全体
    ndep = _.omit(ndep, fs.map(f=>f.name)); // 去掉fs
    
    // 构造被依赖的集合
    let dep = new Map<string, MFieldSchema>();
    fs.forEach(f => dep[f.name] = f);
    
    // 循环从ndep里把被依赖的字段放到dep中,直到dep不再增加
    // 算法有点粗暴,将就用吧
    for(let i = 0; i < all.length; i ++) { //  轮数最多all.length,防止卡死
      let newDepNames = []
      for(let dn in dep) {
        const i = _.intersection( dep[dn].showIf?.split(/[^a-zA-Z0-9_$]/), Object.keys(ndep) );
        newDepNames = newDepNames.concat(i);
      }

      let prevSize = Object.keys(dep).length;
      for(let n of newDepNames){
        dep[n] = allFieldsIdx[n];
        delete  ndep[n];
      }
      let afterSize = Object.keys(dep).length;
      if(prevSize == afterSize) {
        break; // 如果找不到更多依赖,就可以结束了
      }
    }
    return Object.values(dep);
  },

  /**
   * 用moment格式化
   * @param s 原始数据,参见moment
   * @param srcFormat 原始数据的格式,默认x(时间戳)
   * @param dstFormat 转换成的数据格式,默认YYYY年MM月DD日
   * @param fallback 如果原始数据是nil,返回fallback
   */
  momentFormat: function (s: moment.MomentInput, srcFormat: string = "x", dstFormat: string = "YYYY年MM月DD日", fallback: string = "不详") {
    if (_.isNil(s)) {
      return fallback;
    }
    return moment(s, srcFormat).format(dstFormat)
  },

  /**
   * 将标准的json schema转换成m3的schema
   * @param e json schema
   * @param base 如果填了一个对象,转换出的属性会放在这个对象上
   * @param template 如果有子结构,创建子结构的模板
   */
  jsonSchema2MFieldSchema: function (e: JSONSchema6, base: MFieldSchema = { name: "", type: "string" }, template: Partial<MFieldSchema> = {}): MFieldSchema {
    switch (e.type) {
      case 'string':
        if (e.enum) {
          base.type = "enum";
          base.enumFields = base.enumFields ?? e.enum.map(v => ({ value: (v ?? "").toString() }))
        } else {
          base.type = "string"
          base.max = e.maxLength
        }
        break;
      case 'number':
      case 'integer':
        base.type = "int";
        break;
      case 'object':
        base.type = "object"
        base.objectFields = [];
        base.uispec = {
          type: "flowForm",
          layout: MUtil.phoneLike() ? "vertical" : "horizontal",
          comma: ":",
        };
        for (let key in e.properties) {
          let jsmField = e.properties[key];
          let m3Field: MFieldSchema = _.assign({}, template, { name: key, label: key, type: "object" });
          if (!_.isBoolean(jsmField)) {
            m3Field.label = jsmField.title ?? key;
            this.jsonSchema2MFieldSchema(jsmField, m3Field, template);
            base.objectFields.push(m3Field);
          } else {
            m3Field.type = "不支持的json schema:object.properties的value是boolean";
            base.objectFields.push()
          }
        }
        break;
      case 'array':
        base.type = "array"
        base.arrayMember = { label: "", type: "array" };
        if (_.isArray(e.items)) {
          base.arrayMember.type = "不支持的json schema:array.items是数组";
        } else if (_.isBoolean(e.items)) {
          base.arrayMember.type = "不支持的json schema:array.items是boolean";
        } else if (e.items) {
          this.jsonSchema2MFieldSchema(e.items, base, template);
        } else {
          base.arrayMember.type = "不支持的json schema:array.items是undefined";
        }
        break;
      case 'null':
      case 'any':
      case 'boolean':
      default:
        base.type = "不支持的json schema:array.items是" + e.type;
        break;
    }
    return base;
  },

  /**
   * 修改一个对象里的key
   * @param object 要修改的对象,
   * @param renameTo 
   * @param removeNotExistKey 是否在结果里面去除renameTo里不存在的key
   * @returns 返回一个改过key名字的新对象
   */
  renameKey: function (object: any, renameTo: { [oldKey: string]: string }, removeNotExistKey: boolean = false): any {
    let result: any = {};
    for (let oldKey in object) {
      let newKey = renameTo[oldKey];
      if (!newKey) {
        if (removeNotExistKey) {
          continue;
        } else {
          newKey = oldKey;
        }
      }
      result[newKey] = object[oldKey];
    }
    return result;
  },

  /** 啥也不干的空回调 */
  doNothing: function () { },

  /**
   * 去掉arr中的undefined和null,拼出来一个json path
   */
  jsonPath: function (...arr) {
    return _.compact(arr).join(".");
  },

  /**
   * 获取一个json path的父path。
   * @param path 如果path是空的返回也是空的
   */
  parentPath: function (path: string): string {
    const split = path.replace(/]/g, '').split(/[.\\[]/);
    split.splice(-1, 1);
    return split.join(".");
  },

  /**
   * 校验一个schema
   * @param s 要校验的schema
   * @returns 空数组表示校验通过,否则返回校验错误信息
   */
  validateSchema: function (s: MFieldSchema, parentPath?: string): MValidationFail[] {
    let result: MValidationFail[] = [];
    const error = (msg: string) => result.push({ message: msg, path: MUtil.jsonPath(parentPath, s.name) });
    const checkDup = (a: any[], msgIfFail: string) => {
      if (_.uniq(a).length !== a.length) {
        error(msgIfFail);
      }
    };
    const checkVL = (fs: MEnumField[]) => {
      checkDup(fs.map(f => f.value), s.name + "值有重复");
      checkDup(fs.map(f => f.label).filter(l => l), s.name + "文案有重复");
    }
    const stringOnlyEnum = (candidate: string | MEnumField[] | undefined) => {
      const ns = MUtil.standardFields(candidate).find(f => !_.isString(f.value))
      if (ns) {
        error("暂不支持的选项数据类型:" + JSON.stringify(ns) + ": " + (typeof ns))
      }
    }

    if (s.type === "decoration") {
      return [];
    } else if (s.type === "enum") {
      checkVL(MUtil.standardFields(s.enumFields))
    } else if (s.type === "set") {
      checkVL(MUtil.standardFields(s.setFields))
    } else if (s.type === "matrix") {
      stringOnlyEnum(s.matrix.x);
      stringOnlyEnum(s.matrix.y);
    } else if (s.type === "object") {
      if (!s.objectFields) {
        error("object类型未定义成员");
      } else {
        checkDup(s.objectFields.filter(f => f.type !== 'decoration').map(f => f.name), "字段名有重复");
        for (let f of s.objectFields) {
          result = _.concat(result, MUtil.validateSchema(f, MUtil.jsonPath(parentPath, s.name)));
        }

        if (s.uispec?.type === "segmentForm") {
          if (!s.uispec.segments) {
            error("分段未定义");
          } else {
            checkDup(s.uispec.segments.map(f => f.label), "分段文案有重复");
            const fieldsInSegments = _.flatten(s.uispec.segments.map(f => f.fields));
            checkDup(fieldsInSegments, "分段字段有重复");
            const fields = s.objectFields.map(f => f.name);
            checkDup(fields, "字段名有重复");
            const notInSegment = _.difference(fields, fieldsInSegments);
            if (notInSegment.length > 0) {
              error("字段" + notInSegment.join(",") + "未体现在分段中");
            }
            const fieldNotExist = _.difference(fieldsInSegments, fields);
            if (fieldNotExist.length > 0) {
              error("分段字段名无效:" + notInSegment.join(","));
            }
          }
        }
      }
    }
    return result;
  },

  /**
   * 参考https://stackoverflow.com/questions/3362471/how-can-i-call-a-javascript-constructor-using-call-or-apply
   * 示例: var d = applyToConstructor(Date, [2008, 10, 8, 00, 16, 34, 254]);
   * @param constructor 
   * @param argArray 
   */
  applyToConstructor: function (constructor, argArray) {
    var args = [null, ...argArray];
    var factoryFunction = constructor.bind.apply(constructor, args);
    return new factoryFunction();
  },

  /**
   * 执行表达式或者回调函数
   * @param exprOrFunction 表达式或者回调函数
   * @param paramName 参数名字
   * @param paramValue 参数值
   * @param fallback 如果exprOrFunction无效,返回fallback
   * @param _this this指针
   */
  evalExprOrFunction: function (exprOrFunction, paramName: string[], paramValue: any[], fallback: any = undefined, _this: any = undefined) {
    if (_.isFunction(exprOrFunction)) {
      return exprOrFunction.apply(_this, paramValue);
    } else if (_.isString(exprOrFunction)) {
      //return new Function("_", "value", "parent", "return (" + labelExpr + ");")(_, value, parent);
      const func = MUtil.applyToConstructor(Function, [...paramName, "return (" + exprOrFunction + ")"]);
      return func.apply(_this, paramValue);
    } else {
      return fallback;
    }
  },

  /**
   * 读取url参数,转换成map
   * @param hashPrefix nil表示不读取hash里的参数,字符串表示读取hash里的参数,并加上这个前缀返回。默认是返回,但前缀是空字符串
   */
  getQuery: function (hashPrefix: string = ""): any {
    const arrs = [...window.location.search.split(/[?&]/), ...window.location.hash.split(/[#?&]/)];
    const result = {};
    arrs.forEach((item) => {
      const kv = item.split(/=/);
      if (kv.length <= 2 && kv[0]) {
        result[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]);
      }
    });
    return result;
  },

  /**
   * 动态创建一个CSS样式
   * @param css css内容,会被写进style标签里
   * @param id <style>的id,可以没有。如果已经存在,会被覆盖掉。
   */
  setCss: function (css: string, id?: string) {
    const s = document.getElementById(id) as HTMLStyleElement ?? document.createElement('style');
    s.id = id;
    s.type = 'text/css';
    s.innerHTML = css;
    document.getElementsByTagName('head')[0].appendChild(s);
  },

  /**
   * escape数值
   * @param str - 字符串 
   * @returns 会带上引号,类似"abc",str=nil时会返回""
   */
  escapeCsv: function (str?: string) {
    if (!_.isString(str)) {
      str = _.toString(str);
    }
    return '"' + str.replace(/["]/g, '""') + '"';
  },

  /**
   * 判断两个值是否相同
   * @param v1 一个值
   * @param v2 另一个值
   * @param tolerate true用==判断,false用===判断
   */
  isEquals: function(v1, v2, tolerate: boolean) {
    if(tolerate) {
      return v1 == v2;
    } else {
      return v1 === v2;
    }
  }
}