<script lang="ts" setup>
|
/**
|
* 判定规则抽屉。
|
* <p>
|
* 规则列表由设计器持有(use-designer 的 rules),这里直接改那份数据:
|
* 保存时 collectSchema 产出的是整份 Schema,抽屉与画布必须共用同一份规则,
|
* 各存一份就迟早会出现「画布上看到的规则」和「保存下去的规则」不一致。
|
*/
|
import type { QcReportVersionApi } from '#/api/mes/qc/report/version';
|
|
import { computed } from 'vue';
|
|
import { IconifyIcon } from '@vben/icons';
|
|
import {
|
Button,
|
Drawer,
|
Empty,
|
Input,
|
Popconfirm,
|
Select,
|
Switch,
|
} from 'ant-design-vue';
|
|
import { listAllFunctionNames, validateRule } from '#/components/quality';
|
|
defineOptions({ name: 'QcReportRuleEditor' });
|
|
const rules = defineModel<QcReportVersionApi.SchemaRule[]>('rules', { required: true });
|
const open = defineModel<boolean>('open', { default: false });
|
|
const SCOPE_OPTIONS = [
|
{ label: '逐项判定', value: 'item' },
|
{ label: '报告级', value: 'report' },
|
];
|
|
/** 空白规则的写法示例,按作用域给不同的样例,省得用户对着一片空白猜语法 */
|
const EXPRESSION_PLACEHOLDER: Record<string, string> = {
|
item: '例:item.actualValue >= item.lowerLimit AND item.actualValue <= item.upperLimit',
|
report: '例:PASS_RATE(inspectionItems.result) >= 60',
|
};
|
|
/**
|
* 可用函数的清单直接取自引擎白名单,不在界面上另抄一份。
|
* 抄一份就会在引擎加函数时漏改,用户照提示写的表达式反而被引擎拒掉。
|
*/
|
const FUNCTION_HINT = listAllFunctionNames().join('、');
|
|
/** 每条规则的问题,空串表示没问题。与后端保存时的校验同口径 */
|
const ruleErrors = computed(() => rules.value.map((rule) => validateRule(rule)[0] ?? ''));
|
|
const errorCount = computed(() => ruleErrors.value.filter(Boolean).length);
|
|
let sequence = 0;
|
|
function handleAdd() {
|
sequence += 1;
|
rules.value = [
|
...rules.value,
|
{
|
// id 只用于出错时定位,时间戳加序号足够且不依赖随机数
|
id: `rule-${Date.now()}-${sequence}`,
|
name: `规则 ${rules.value.length + 1}`,
|
scope: 'item',
|
expression: '',
|
enabled: true,
|
},
|
];
|
}
|
|
function handleRemove(index: number) {
|
rules.value = rules.value.filter((_, current) => current !== index);
|
}
|
</script>
|
|
<template>
|
<Drawer v-model:open="open" title="判定规则" placement="right" :width="640">
|
<div class="flex h-full flex-col gap-3">
|
<div class="rounded border border-border bg-gray-50 p-2 text-xs leading-5 text-gray-600">
|
<div>
|
<span class="font-medium">逐项判定</span
|
>:对每条检验项分别求值,表达式为真即判该项合格。没有任何逐项规则时,退回按规格上下限判定。
|
</div>
|
<div>
|
<span class="font-medium">报告级</span
|
>:决定整份报告的结论。没有任何报告级规则时,取「全部检验项都合格」。
|
</div>
|
<div class="mt-1">
|
可用字段:<code>item.actualValue</code>、<code>item.lowerLimit</code>、<code
|
>item.upperLimit</code
|
>、<code>item.itemName</code>、<code>item.result</code>、<code>report.total</code>、<code
|
>report.passCount</code
|
>、<code>report.failCount</code>、<code>inspectionItems.result</code>
|
</div>
|
<div>
|
可用函数:{{ FUNCTION_HINT }}
|
</div>
|
</div>
|
|
<div v-if="errorCount > 0" class="text-xs text-red-500">
|
有 {{ errorCount }} 条规则还没写完整,保存前需要先改好。
|
</div>
|
|
<div class="flex-1 overflow-auto">
|
<Empty
|
v-if="rules.length === 0"
|
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
description="还没有判定规则,检验项将按规格上下限判定"
|
/>
|
<div v-else class="flex flex-col gap-3">
|
<div
|
v-for="(rule, index) in rules"
|
:key="rule.id ?? index"
|
class="rounded border border-border p-3"
|
:class="rule.enabled === false ? 'opacity-60' : ''"
|
>
|
<div class="mb-2 flex items-center gap-2">
|
<Input
|
v-model:value="rule.name"
|
size="small"
|
placeholder="规则名称,出错时会带上这个名字"
|
class="flex-1"
|
/>
|
<Select
|
v-model:value="rule.scope"
|
:options="SCOPE_OPTIONS"
|
size="small"
|
class="w-28"
|
/>
|
<Switch v-model:checked="rule.enabled" size="small" />
|
<Popconfirm title="删除该规则?" @confirm="handleRemove(index)">
|
<Button size="small" type="text" danger>
|
<IconifyIcon icon="lucide:trash-2" />
|
</Button>
|
</Popconfirm>
|
</div>
|
<Input.TextArea
|
v-model:value="rule.expression"
|
:rows="2"
|
:placeholder="EXPRESSION_PLACEHOLDER[rule.scope ?? 'item']"
|
class="font-mono text-xs"
|
/>
|
<div v-if="ruleErrors[index]" class="mt-1 text-xs text-red-500">
|
{{ ruleErrors[index] }}
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<Button block type="dashed" @click="handleAdd">
|
<IconifyIcon icon="lucide:plus" class="mr-1" />
|
新增规则
|
</Button>
|
</div>
|
</Drawer>
|
</template>
|