
import { v4 as uuidv4 } from "uuid";
import type {
ConditionGroup,
ConditionRule,
ConditionNode
} from "./ConditionBuilder/types";
import type {
BackendRuleItem,
BackendRuleDetail,
BackendRulePayload
} from "./types";
const OP_MAP_TO_API: Record<string, string> = {
">": "gt",
"<": "lt",
"=": "eq",
">=": "ge",
"<=": "le"
};
const OP_MAP_TO_UI: Record<string, string> = {
gt: ">",
lt: "<",
eq: "=",
ge: ">=",
le: "<="
};
export function transformToUI(data: BackendRulePayload): ConditionGroup {
const rootGroup: ConditionGroup = {
id: uuidv4(),
type: "group",
logic: "AND",
children: []
};
if (!data.ruleItemList || data.ruleItemList.length === 0) {
return rootGroup;
}
if (
data.ruleItemList.length === 1 &&
data.ruleItemList[0].ruleItemType === "formula"
) {
const rootFormula = data.ruleItemList[0];
rootGroup.logic =
(rootFormula.ruleFormula?.toUpperCase() as "AND" | "OR") || "AND";
rootGroup.children = (rootFormula.children || []).map(processApiToNode);
}
else {
rootGroup.logic = "AND";
rootGroup.children = data.ruleItemList.map(processApiToNode);
}
return rootGroup;
}
function processApiToNode(item: BackendRuleItem): ConditionNode {
if (item.ruleItemType === "formula") {
return {
id: uuidv4(),
type: "group",
logic: (item.ruleFormula?.toUpperCase() as "AND" | "OR") || "AND",
children: (item.children || []).map(processApiToNode)
} as ConditionGroup;
}
else {
const left = item.ruleDetailList?.[0] || {};
const right = item.ruleDetailList?.[1] || {};
return {
id: uuidv4(),
type: "rule",
metric: left.indicatorCode || "",
dimension: left.objectField || "",
operator: OP_MAP_TO_UI[item.operation || ""] || item.operation || "",
valueType: right.valType === "fixed" ? "value" : "field",
value: String(right.fixedValue !== undefined ? right.fixedValue : "")
} as ConditionRule;
}
}
export function transformToApi(
node: ConditionGroup,
originalData?: Partial<BackendRulePayload>
): BackendRulePayload {
let ruleItemList: BackendRuleItem[] = [];
if (node.logic === "OR") {
ruleItemList = [processNodeToApi(node)];
} else {
ruleItemList = node.children.map(processNodeToApi);
}
return {
id: originalData?.id,
name: originalData?.name,
ruleType: "metric",
ruleItemList: ruleItemList
};
}
function processNodeToApi(node: ConditionNode): BackendRuleItem {
if (node.type === "group") {
return {
ruleItemType: "formula",
ruleFormula: node.logic.toLowerCase() as "and" | "or",
children: node.children.map(processNodeToApi)
};
}
else {
const rule = node as ConditionRule;
const leftSide: BackendRuleDetail = {
name: "指标名称占位",
valType: "metric",
compareType: "parameter",
indicatorCode: rule.metric,
objectField: rule.dimension
};
const rightSide: BackendRuleDetail = {
compareType:
rule.valueType === "value" ? "parameter" : "compareParameter",
valType: rule.valueType === "value" ? "fixed" : "metric",
fixedValue: rule.valueType === "value" ? Number(rule.value) : undefined
};
return {
ruleItemType: "metric",
operation: (OP_MAP_TO_API[rule.operator] as any) || "eq",
ruleDetailList: [leftSide, rightSide]
};
}
}
export interface BackendRuleDetail {
name?: string;
valType: "metric" | "fixed" | "parameter";
compareType?: "parameter" | "compareParameter";
indicatorCode?: string;
objectField?: string;
fixedValue?: string | number;
}
export interface BackendRuleItem {
ruleItemType: "metric" | "formula";
operation?: "gt" | "lt" | "eq" | "ge" | "le";
ruleDetailList?: BackendRuleDetail[];
ruleFormula?: "and" | "or";
children?: BackendRuleItem[];
}
export interface BackendRulePayload {
id?: number | string;
name?: string;
ruleType: string;
ruleItemList: BackendRuleItem[];
}
<script setup lang="ts">
import { ref } from "vue";
import { Refresh, Plus, Delete } from "@element-plus/icons-vue";
import ConditionGroupComp from "../ConditionBuilder/ConditionGroup.vue";
import type { ConditionGroup } from "../ConditionBuilder/types";
import { v4 as uuidv4 } from "uuid";
import type { BackendRulePayload } from "../types";
import { transformToApi, transformToUI } from "../ruleAdapter";
import { ElMessage } from "element-plus";
const visible = ref(false);
const initialData: ConditionGroup = {
id: "root",
type: "group",
logic: "AND",
children: [
{
id: uuidv4(),
type: "rule",
metric: "loss_ratio",
dimension: "yearly",
operator: ">",
valueType: "field",
value: "parent_org"
},
{
id: uuidv4(),
type: "group",
logic: "OR",
children: [
{
id: uuidv4(),
type: "rule",
metric: "loss_ratio",
dimension: "monthly_ratio",
operator: ">",
valueType: "field",
value: "parent_org"
},
{
id: uuidv4(),
type: "rule",
metric: "loss_ratio",
dimension: "monthly_yoy",
operator: "value",
valueType: "value",
value: "0"
}
]
},
{
id: uuidv4(),
type: "rule",
metric: "loss_ratio",
dimension: "yearly",
operator: ">",
valueType: "field",
value: "parent_org"
}
]
};
interface RuleItem {
id: number | string;
name: string;
ruleType: string;
rawData?: BackendRulePayload;
uiData: ConditionGroup;
}
const rulesList = ref<RuleItem[]>([]);
const open = (data?: BackendRulePayload | BackendRulePayload[]) => {
visible.value = true;
if (Array.isArray(data)) {
console.log("正在编辑多个规则:", data);
rulesList.value = data.map((rule, index) => ({
id: rule.id || uuidv4(),
name: rule.name || `规则 ${index + 1}`,
ruleType: rule.ruleType || "metric",
rawData: rule,
uiData: transformToUI(rule)
}));
} else if (data) {
console.log("正在编辑单个规则:", data);
rulesList.value = [
{
id: data.id || uuidv4(),
name: data.name || "规则 1",
ruleType: data.ruleType || "metric",
rawData: data,
uiData: transformToUI(data)
}
];
} else {
rulesList.value = [
{
id: uuidv4(),
name: "规则 1",
ruleType: "metric",
uiData: JSON.parse(JSON.stringify(initialData))
}
];
}
};
const addRule = () => {
const newRule: RuleItem = {
id: uuidv4(),
name: `规则 ${rulesList.value.length + 1}`,
ruleType: "metric",
uiData: JSON.parse(JSON.stringify(initialData))
};
rulesList.value.push(newRule);
};
const deleteRule = (index: number) => {
if (rulesList.value.length <= 1) {
ElMessage.warning("至少需要保留一个规则");
return;
}
rulesList.value.splice(index, 1);
rulesList.value.forEach((rule, i) => {
if (!rule.rawData?.name) {
rule.name = `规则 ${i + 1}`;
}
});
};
const handleReset = (index: number) => {
rulesList.value[index].uiData = JSON.parse(JSON.stringify(initialData));
};
const handleConfirm = () => {
const payload = rulesList.value.map(rule => {
const apiData = transformToApi(rule.uiData, rule.rawData);
return {
...apiData,
name: rule.name
};
});
console.log("最终提交的规则列表:", JSON.stringify(payload, null, 2));
visible.value = false;
};
defineExpose({ open });
</script>
<template>
<el-dialog
v-model="visible"
title="指标预警"
width="900px"
destroy-on-close
class="indicator-dialog"
>
<div class="p-4 min-h-[300px]">
<div
v-for="(rule, index) in rulesList"
:key="rule.id"
class="mb-6 last:mb-0"
>
<div class="flex items-center justify-between mb-3">
<span class="font-medium text-gray-700">{{ rule.name }}</span>
<div class="flex gap-2">
<el-button
link
type="primary"
:icon="Refresh"
size="small"
@click="handleReset(index)"
>
恢复默认设置
</el-button>
<el-button
link
type="danger"
:icon="Delete"
size="small"
@click="deleteRule(index)"
>
删除
</el-button>
</div>
</div>
<ConditionGroupComp :node="rule.uiData" :depth="0" />
<div
v-if="index < rulesList.length - 1"
class="my-6 border-t border-gray-200"
/>
</div>
<div class="flex justify-center mt-6">
<el-button type="primary" :icon="Plus" plain @click="addRule">
新增规则
</el-button>
</div>
</div>
<template #footer>
<div class="flex justify-between items-center w-full">
<div />
<div>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleConfirm">确认</el-button>
</div>
</div>
</template>
</el-dialog>
</template>
<style lang="scss">
// 全局样式覆盖,如果是 scoped 可能会无法覆盖 el-dialog body
.indicator-dialog {
.el-dialog__body {
padding-top: 10px;
padding-bottom: 10px;
}
}
</style>
export type LogicType = "AND" | "OR";
export type NodeType = "group" | "rule";
export interface ConditionRule {
id: string;
type: "rule";
metric: string;
dimension: string;
operator: string;
valueType: string;
value: string;
}
export interface ConditionGroup {
id: string;
type: "group";
logic: LogicType;
children: (ConditionGroup | ConditionRule)[];
}
export type ConditionNode = ConditionGroup | ConditionRule;
<script setup lang="ts">
import { Delete, Plus } from "@element-plus/icons-vue";
import type { ConditionRule } from "./types";
import { computed } from "vue";
const props = defineProps<{
item: ConditionRule;
index: number;
depth: number;
}>();
const emit = defineEmits(["delete", "add"]);
const metricOptions = [{ label: "车险已报告赔付率", value: "loss_ratio" }];
const dimOptions = [
{ label: "年累计", value: "yearly" },
{ label: "月环比", value: "monthly_ratio" },
{ label: "月同比", value: "monthly_yoy" }
];
const opOptions = [
{ label: ">", value: ">" },
{ label: "<", value: "<" },
{ label: "=", value: "=" },
{ label: "数值", value: "value" }
];
</script>
<template>
<div class="flex items-center gap-2 w-full">
<el-select
v-model="props.item.metric"
placeholder="请选择指标"
class="!w-[200px]"
>
<el-option
v-for="opt in metricOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-select v-model="item.dimension" placeholder="维度" class="!w-[120px]">
<el-option
v-for="opt in dimOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-select v-model="item.operator" placeholder="操作符" class="!w-[120px]">
<el-option
v-for="opt in opOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<template v-if="item.operator === 'value'">
<el-input
v-model="item.value"
placeholder="输入数值"
class="!w-[150px]"
/>
</template>
<template v-else>
<el-select v-model="item.value" placeholder="选择对象" class="!w-[150px]">
<el-option label="上级机构" value="parent_org" />
<el-option label="0" value="0" />
</el-select>
</template>
<el-button type="danger" link :icon="Delete" @click="emit('delete')" />
<el-button
v-if="index === 0"
type="primary"
link
:icon="Plus"
class="ml-2"
@click="emit('add')"
>
{{ depth === 0 ? "筛选" : "并且满足" }}
</el-button>
</div>
</template>
<style scoped>
:deep(.el-input__wrapper) {
box-shadow: 0 0 0 1px #dcdfe6 inset;
}
</style>
<script setup lang="ts">
import ConditionRuleComp from "./ConditionRule.vue";
import type { ConditionGroup, ConditionRule } from "./types";
import { v4 as uuidv4 } from "uuid";
const props = defineProps<{
node: ConditionGroup;
depth: number;
}>();
const emit = defineEmits(["delete-node"]);
const toggleLogic = (val: "AND" | "OR") => {
props.node.logic = val;
};
const addRule = () => {
const newRule: ConditionRule = {
id: uuidv4(),
type: "rule",
metric: "",
dimension: "",
operator: "",
valueType: "value",
value: ""
};
props.node.children.push(newRule);
};
const removeChild = (index: number) => {
props.node.children.splice(index, 1);
if (props.node.children.length === 0 && props.depth > 0) {
emit("delete-node");
}
};
</script>
<template>
<div class="flex w-full">
<div class="relative w-14 flex-shrink-0 mr-2 select-none">
<template v-if="node.children.length > 1">
<div
class="absolute left-1/2 top-4 bottom-4 -translate-x-1/2 w-full pointer-events-none"
>
<div
class="absolute left-1/2 top-0 bottom-0 w-[1px] bg-gray-300 -translate-x-1/2"
/>
<div
class="absolute left-1/2 top-0 w-3 h-[1px] bg-gray-300 -translate-x-1/2"
/>
<div
class="absolute left-1/2 bottom-0 w-3 h-[1px] bg-gray-300 -translate-x-1/2"
/>
</div>
<div
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10"
>
<div
class="flex flex-col items-center bg-gray-100 p-1 rounded-lg shadow-sm border border-gray-200"
>
<div
class="w-6 h-6 flex items-center justify-center text-xs rounded cursor-pointer transition-all mb-1"
:class="
node.logic === 'AND'
? 'bg-blue-500 text-white shadow-sm'
: 'text-gray-500 hover:text-gray-700'
"
@click="toggleLogic('AND')"
>
且
</div>
<div
class="w-6 h-6 flex items-center justify-center text-xs rounded cursor-pointer transition-all"
:class="
node.logic === 'OR'
? 'bg-blue-500 text-white shadow-sm'
: 'text-gray-500 hover:text-gray-700'
"
@click="toggleLogic('OR')"
>
或
</div>
</div>
</div>
</template>
</div>
<div class="flex-1 flex flex-col gap-4">
<div
v-for="(child, index) in node.children"
:key="child.id"
class="relative"
>
<template v-if="child.type === 'group'">
<ConditionGroup
:node="(child as ConditionGroup)"
:depth="depth + 1"
@delete-node="removeChild(index)"
/>
</template>
<template v-else>
<ConditionRuleComp
:item="(child as ConditionRule)"
:index="index"
:depth="depth"
@delete="removeChild(index)"
@add="addRule"
/>
</template>
</div>
</div>
</div>
</template>