【无标题】

规则表达式(amis组合条件)

代码:

javascript 复制代码
<template>
  <div class="rule-expr">
    <div class="rule-body-panel">
      <rule-group :group="ruleTree" :level="0" :is-root="true" :create-group="createGroup"
        :create-condition="createCondition" :field-dict-type="fieldDictType"
        :func-dict-type="funcDictType"></rule-group>
    </div>

  </div>
</template>

<script>
import RuleGroup from './rule-group.vue';

let NODE_ID = 1;

export default {
  name: 'RuleExpr',
  components: {
    RuleGroup
  },
  props: {
    value: [Array, Object, String],
    fieldDictType: {
      type: String,
      default: 'CHAN_EXPR_VAR'
    },
    funcDictType: {
      type: String,
      default: 'CHAN_FUNC_TYPE'
    }
  },
  data() {
    return {
      ruleTree: this.createGroup()
    };
  },
  watch: {
    value: {
      handler(val) {
        this.ruleTree = this.resolveRuleBody(val);
      },
      immediate: true
    }
  },
  methods: {
    nextNodeId() {
      return `rule-node-${NODE_ID++}`;
    },
    normalizeConjunction(conjunction, fallback = 'and') {
      const value = String(conjunction || fallback || 'and').trim().toLowerCase();
      return value === 'or' ? 'or' : 'and';
    },
    createCondition(data = {}) {
      return {
        id: data.id || this.nextNodeId(),
        varName: data.varName || '',
        exprFunc: data.exprFunc || '',
        matchVal: data.matchVal || ''
      };
    },
    createGroup(data = {}) {
      const children = Array.isArray(data.children) && data.children.length
        ? data.children.map(child => this.buildEditorNode(child))
        : [];
      return {
        id: data.id || this.nextNodeId(),
        conjunction: this.normalizeConjunction(data.conjunction, 'and'),
        children
      };
    },
    isGroupNode(node) {
      return !!(node && Array.isArray(node.children));
    },
    buildEditorNode(node) {
      if (!node || typeof node !== 'object') {
        return this.createCondition();
      }

      if (Array.isArray(node.children)) {
        return this.createGroup(node);
      }

      return this.createCondition(node);
    },
    normalizeRuleTree(node) {
      if (!node) {
        return null;
      }
      if (this.isGroupNode(node)) {
        return {
          id: node.id || this.nextNodeId(),
          conjunction: this.normalizeConjunction(node.conjunction, 'and'),
          children: (node.children || []).map(child => this.normalizeRuleTree(child)).filter(Boolean)
        };
      }
      return {
        id: node.id || this.nextNodeId(),
        varName: node.varName || '',
        exprFunc: node.exprFunc || '',
        matchVal: node.matchVal || ''
      };
    },
    resolveRuleBody(ruleBody) {
      if (!ruleBody) {
        return this.createGroup();
      }

      let parsedRuleBody = ruleBody;
      if (typeof parsedRuleBody === 'string') {
        try {
          parsedRuleBody = JSON.parse(parsedRuleBody);
        } catch (e) {
          return this.createGroup();
        }
      }

      if (parsedRuleBody && parsedRuleBody.conditions) {
        parsedRuleBody = parsedRuleBody.conditions;
      }

      if (parsedRuleBody && Array.isArray(parsedRuleBody.children)) {
        return this.buildEditorNode(parsedRuleBody);
      }

      if (Array.isArray(parsedRuleBody)) {
        return this.createGroup();
      }

      if (parsedRuleBody && (parsedRuleBody.varName || parsedRuleBody.exprFunc || parsedRuleBody.matchVal)) {
        return this.createGroup({
          children: [this.createCondition(parsedRuleBody)]
        });
      }

      return this.createGroup();
    },
    validateNode(node) {
      if (!node) {
        return false;
      }
      if (this.isGroupNode(node)) {
        return Array.isArray(node.children) && node.children.length && node.children.every(child => this.validateNode(child));
      }
      return !!node.varName && !!node.exprFunc && !!String(node.matchVal || '').trim();
    },
    validateRuleBody() {
      const valid = this.validateNode(this.ruleTree);
      if (!valid) {
        this.$message.warning('请完整填写规则内容');
        return false;
      }
      return true;
    },
    getConditionsTree() {
      return this.normalizeRuleTree(this.ruleTree);
    }
  }
};
</script>

<style scoped lang="scss">
.rule-expr {
  display: flex;
  flex-direction: column;
}

.rule-body-panel {
  border-radius: 8px;
  background: #fff;
  padding: 20px 24px 16px;
}
</style>

rule-group.vue

javascript 复制代码
<template>
  <div class="rule-group" :class="{ 'is-root': isRoot }">
    <div class="group-layout">
      <div  class="group-rail">
        <span class="rail-point rail-point-top"></span>
        <span class="rail-line rail-line-top"></span>
        <span class="rail-tag" @click="toggleConjunction">{{ conjunctionText }}</span>
        <span class="rail-line rail-line-bottom"></span>
        <span class="rail-point rail-point-bottom"></span>
      </div>

      <div class="group-main">

        <div class="group-content">
          <div v-for="(child, index) in group.children" :key="child.id || index" class="group-item">
            <rule-group
              v-if="isGroup(child)"
              :group="child"
              :level="level + 1"
              :is-root="false"
              :create-group="createGroup"
              :create-condition="createCondition"
              :field-dict-type="fieldDictType"
              :func-dict-type="funcDictType"
              @remove="removeChild(index)">
            </rule-group>

            <div v-else class="condition-card">
              <el-select
                v-model="child.varName"
                style="width: 300px;"
                placeholder="请选择字段"
                clearable
                filterable
                >
                  <el-option v-for="item in exprVarOptions" :key="item.dictId" :label="item.dictName" :value="item.dictId"></el-option>
              </el-select>

              <el-select
                v-model="child.exprFunc"
                style="width: 200px;"
                clearable
                placeholder="请选择条件"
                filterable
                >
                  <el-option v-for="item in fileFuncOptions" :key="item.dictId" :label="item.dictName" :value="item.dictId"></el-option>
              </el-select>

              <el-input v-model="child.matchVal" class="condition-value" placeholder="请输入匹配值"></el-input>

              <el-button type="text" class="delete-icon-btn" @click="removeChild(index)">
                <i class="el-icon-delete"></i>
              </el-button>
            </div>
          </div>
        </div>

        <div class="group-actions">
          <a class="group-link" @click.prevent="addCondition">添加条件</a>
          <a class="group-link" @click.prevent="addGroup">添加条件组</a>
          <a v-if="!isRoot" class="group-link danger" @click.prevent="$emit('remove')">删除组</a>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'RuleGroup',
  props: {
    group: {
      type: Object,
      required: true
    },
    level: {
      type: Number,
      default: 0
    },
    isRoot: {
      type: Boolean,
      default: false
    },
    createGroup: {
      type: Function,
      required: true
    },
    createCondition: {
      type: Function,
      required: true
    },
    fieldDictType: {
      type: String,
      default: 'CHAN_EXPR_VAR'
    },
    funcDictType: {
      type: String,
      default: 'CHAN_FUNC_TYPE'
    }
  },
    data() {
	    return {
	      exprVarOptions: [
	       {
		       dictId: 'field01',
		       dictName:'字段1'
		      },
		      {
		      	dictId:'field02',
		      	dictName:'字段2'
		      }
	      ],  //选择字段
	      fileFuncOptions: [
		      {
		       dictId: 'eq',
		       dictName:'等于'
		      },
		      {
		      	dictId:'startWith',
		      	dictName:'匹配开头'
		      }
	      ]  //选择条件
	    };
	  },
  computed: {
    conjunctionText() {
      return String(this.group.conjunction || 'and').toLowerCase() === 'or' ? '或' : '且';
    },
  },
  methods: {
    isGroup(node) {
      return !!(node && Array.isArray(node.children));
    },
    addCondition() {
      this.group.children.push(this.createCondition());
    },
    addGroup() {
      this.group.children.push(this.createGroup());
    },
    toggleConjunction() {
      this.group.conjunction = this.group.conjunction === 'or' ? 'and' : 'or';
    },
    removeChild(index) {
      this.group.children.splice(index, 1);
    }
  }
};
</script>

<style scoped lang="scss">
.rule-group {
  .group-layout {
    position: relative;
    display: flex;
    align-items: stretch;
    padding-left: 46px;
    min-height: 80px;
  }

  .group-rail {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 36px;
  }

  .rail-line {
    position: absolute;
    left: 17px;
    width: 1px;
    background: #c8d9ff;

    &.rail-line-top {
      top: 8px;
      height: calc(50% - 22px);
    }

    &.rail-line-bottom {
      top: calc(50% + 22px);
      bottom: 8px;
    }
  }

  .rail-point {
    position: absolute;
    left: 11px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: #dbe8ff;
    border: 1px solid #b7ccff;

    &.rail-point-top {
      top: 2px;
    }

    &.rail-point-bottom {
      bottom: 2px;
    }
  }

  .rail-tag {
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    min-width: 34px;
    height: 28px;
    line-height: 28px;
    border-radius: 4px;
    background: #e8f0ff;
    color: #4d78ff;
    text-align: center;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    user-select: none;
  }

  .group-main {
    flex: 1;
    min-width: 0;
  }

  .group-content {
    display: flex;
    flex-direction: column;
    gap: 10px;
  }

  .group-item {
    width: 100%;
  }

  .condition-card {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 46px;
    padding: 10px 14px;
    background: #f6f8fc;
    border-radius: 4px;
  }

  .condition-value {
    flex: 1;
    min-width: 160px;
  }

  .delete-icon-btn {
    flex: 0 0 auto;
    padding: 0;
    color: #909399;
    font-size: 16px;

    &:hover,
    &:focus {
      color: #4d78ff;
    }
  }

  .group-actions {
    display: flex;
    align-items: center;
    gap: 18px;
    margin-top: 10px;
    padding-left: 2px;
  }

  .group-link {
    color: #4d78ff;
    font-size: 14px;
    cursor: pointer;
    text-decoration: none;
  }
}
</style>

效果:

相关推荐
kyriewen16 小时前
前端初级岗位暴跌62%:我带了三年的实习生被裁了,而AI是他亲手教的
前端·面试·ai编程
劉三岁16 小时前
Git 给 main 分支打 Tag(版本标记)完整教程
vue.js·github
JiaWen技术圈16 小时前
React Server Functions 深度解析
前端·react.js·前端框架
幸运小圣16 小时前
前端三种输入数据来源生成 worksheet(工作表)新手适用详细篇【SheetJS】
开发语言·前端·javascript
Hilaku16 小时前
如何实现 0 毫秒无感页面跳转?聊聊被低估的 Speculation Rules API
前端·javascript·程序员
旧曲重听116 小时前
我的Vibe Coding一周记…
前端·人工智能·程序人生·面试
北风toto16 小时前
原生html中input标签oninput处理器使用
前端
zhangxingchao17 小时前
AI 大模型核心四:工程体系化思维
前端·人工智能·后端
JiaWen技术圈17 小时前
React 19 并发渲染器:全面解析与实战指南
前端·react.js·前端框架