云
2026-09-20 35722562e9e13f0504acc15b740d042ecb810199
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
<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>