规则表达式(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>
效果:
