Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e01ef87380 | ||
|
|
94f263d489 | ||
|
|
adcdd1217e | ||
|
|
b96d3d6451 | ||
|
|
dda7336323 | ||
|
|
f0ec500348 | ||
|
|
5cf1ce8d1f |
@@ -144,7 +144,7 @@ class FlowApi {
|
||||
defGet(id: number | string) {
|
||||
return get(`${ROOT}/admin/def/get`, { id });
|
||||
}
|
||||
defSave(body: { id?: number; code: string; name: string; contentJson: string; baseGuid?: string; appKey?: string }) {
|
||||
defSave(body: { id?: number; code: string; name: string; contentJson: string; baseGuid?: string; appKey?: string; changeLog?: string }) {
|
||||
return post(`${ROOT}/admin/def/save`, body);
|
||||
}
|
||||
/** 元数据原地编辑(2026-10-10 议题2):只改 name/appKey 不升版本;已发布亦可 */
|
||||
@@ -164,6 +164,10 @@ class FlowApi {
|
||||
defVersions(flowGuid: string) {
|
||||
return get(`${ROOT}/admin/def/versions`, { flowGuid });
|
||||
}
|
||||
/** 在途办件数(议题二④):flowGuid → 审批中办件数,无在途的流程线也返回 0 键 */
|
||||
defInflight(flowGuids: string[]) {
|
||||
return get(`${ROOT}/admin/def/inflight`, { flowGuids: flowGuids.join(",") });
|
||||
}
|
||||
/** 流程定义示意图(管理面:defId 任意版本,草稿可预览) */
|
||||
adminDefDiagram(params: { id: number | string; mode?: string }) {
|
||||
return get(`${ROOT}/admin/def/diagram`, params);
|
||||
|
||||
+184
-25
@@ -4,7 +4,7 @@
|
||||
// 本页不再直接编辑 JSON 文本(编辑器内置"源码"高级模式兜底);行内「源码」为只读查看+复制。
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Message, Tag } from "view-ui-plus";
|
||||
import { Message, Table, Tag } from "view-ui-plus";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import SourceDrawer from "@/views/flow/components/SourceDrawer.vue";
|
||||
import { flowApi } from "@/api/module/flow";
|
||||
@@ -29,27 +29,40 @@ const defsH = useElementHeight(defsRegionRef);
|
||||
const apps = ref<{ appKey: string; appName: string }[]>([]);
|
||||
const appMap = ref<Record<string, string>>({});
|
||||
const filterApp = ref("");
|
||||
// 议题二②:列表视图(group=同 flowGuid 折叠一行,默认;flat=现状全版本平铺)——纯前端切换,defList 已全量返回
|
||||
const viewMode = ref<"group" | "flat">("group");
|
||||
// 分组展开态(key=flowGuid):Table 数据由 computed 派生,展开态必须存外面才不会丢
|
||||
const openMap = ref<Record<string, boolean>>({});
|
||||
// 议题二④:flowGuid → 在途办件数;拉取失败降级空 map(行内显示「—」,列表不崩)
|
||||
const inflightMap = ref<Record<string, number>>({});
|
||||
|
||||
const columns = [
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ title: "流程", key: "name", minWidth: 180 },
|
||||
{ title: "编码", key: "code", width: 160 },
|
||||
{ title: "应用", key: "appKey", width: 130, align: "center", render: (h: any, p: any) => {
|
||||
// 未归属标记(2026-10-12 appKey 必填化):存量 NULL 线在此醒目,随编辑/升版本收敛
|
||||
/** 在途展示:数字=件数(0 也展示);undefined=接口无该线数据 → 「—」 */
|
||||
function inflightText(guid: string): string {
|
||||
const n = inflightMap.value[guid];
|
||||
return n == null ? "在途 —" : `在途 ${n} 件`;
|
||||
}
|
||||
|
||||
/** 创建时间格式化 YYYY-MM-DD HH:mm(后端 LocalDateTime 带 T,空值显示 -) */
|
||||
function fmtTime(raw: any): string {
|
||||
return raw ? String(raw).replace("T", " ").slice(0, 16) : "-";
|
||||
}
|
||||
|
||||
/** 应用列(未归属标记 2026-10-12 appKey 必填化):平铺/分组/展开行共用,口径不漂移 */
|
||||
const renderApp = (h: any, p: any) => {
|
||||
if (!p.row.appKey) {
|
||||
return h(Tag, { color: "warning" }, () => "未归属");
|
||||
}
|
||||
return h("span", appMap.value[p.row.appKey] || p.row.appKey);
|
||||
} },
|
||||
{ title: "版本", key: "version", width: 70, align: "center" },
|
||||
{ title: "状态", width: 90, align: "center", render: (h: any, p: any) => {
|
||||
};
|
||||
|
||||
/** 状态列(草稿橙/已发布绿/停用灰):三态共用 */
|
||||
const renderStatus = (h: any, p: any) => {
|
||||
const c = p.row.status === 1 ? "green" : p.row.status === 0 ? "orange" : "gray";
|
||||
return h(Tag, { color: c }, () => DEF_STATUS[p.row.status] || p.row.status);
|
||||
} },
|
||||
{ title: "GUID", key: "flowGuid", width: 200 },
|
||||
{ title: "发布时间", width: 160, render: (h: any, p: any) => h("span", (p.row.publishTime || "").replace("T", " ") || "-") },
|
||||
actionColumn(
|
||||
[
|
||||
};
|
||||
|
||||
/** 行操作集(平铺态、分组展开行同一套;禁用规则按 status) */
|
||||
const rowActions = [
|
||||
{ key: "edit", label: "编辑", icon: "icon-edit", on: (r: any) => onEdit(r), disabledWhen: (r: any) => r.status !== 0 },
|
||||
{ key: "publish", label: "发布", icon: "icon-fasong", on: (r: any) => onPublish(r), disabledWhen: (r: any) => r.status !== 0 },
|
||||
{ key: "newVersion", label: "升版本", icon: "icon-tianjia", on: (r: any) => onNewVersion(r), disabledWhen: (r: any) => r.status !== 1 },
|
||||
@@ -63,20 +76,143 @@ const columns = [
|
||||
{ key: "diagram", label: "图示", icon: "icon-tupian", on: (r: any) => onDiagram(r) },
|
||||
{ key: "source", label: "源码", icon: "icon-liebiao", on: (r: any) => onSource(r) },
|
||||
{ key: "versions", label: "版本历史", icon: "icon-shijian", on: (r: any) => onVersions(r) },
|
||||
],
|
||||
{ mode: "text", width: 180, fixed: "right" },
|
||||
),
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ title: "流程", key: "name", minWidth: 180 },
|
||||
{ title: "编码", key: "code", width: 160 },
|
||||
{ title: "应用", key: "appKey", width: 130, align: "center", render: renderApp },
|
||||
{ title: "版本", key: "version", width: 70, align: "center" },
|
||||
{ title: "状态", width: 90, align: "center", render: renderStatus },
|
||||
{ title: "GUID", key: "flowGuid", width: 240, render: (h: any, p: any) => {
|
||||
// 平铺态也带在途(议题二④):计数按流程线口径,故挂在 GUID 旁
|
||||
const n = inflightMap.value[p.row.flowGuid];
|
||||
return h("span", {}, [
|
||||
p.row.flowGuid,
|
||||
h(Tag, { color: n ? "blue" : "default", style: "margin-left:6px" }, () => inflightText(p.row.flowGuid)),
|
||||
]);
|
||||
} },
|
||||
{ title: "发布时间", width: 160, render: (h: any, p: any) => h("span", fmtTime(p.row.publishTime)) },
|
||||
actionColumn(rowActions, { mode: "text", width: 180, fixed: "right" }),
|
||||
];
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
rows.value = (await flowApi.defList({ appKey: filterApp.value || undefined }).send()) || [];
|
||||
loadInflight();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 在途统计(议题二④):列表到位后取 distinct flowGuids 批量查,失败静默降级(行内显示「—」) */
|
||||
async function loadInflight() {
|
||||
const guids = Array.from(new Set(rows.value.map((r: any) => r.flowGuid).filter(Boolean))).slice(0, 100);
|
||||
if (!guids.length) {
|
||||
inflightMap.value = {};
|
||||
return;
|
||||
}
|
||||
try {
|
||||
inflightMap.value = (await flowApi.defInflight(guids).send()) || {};
|
||||
} catch {
|
||||
inflightMap.value = {};
|
||||
}
|
||||
}
|
||||
|
||||
/** 分组行(议题二②):同 flowGuid 折叠一行,代表行=已发布中最高版;无已发布取最高版并标「无生效版」。
|
||||
* 空 flowGuid(历史脏数据)无流程线可折叠,各自单列为一行,避免被整体丢弃。 */
|
||||
const groups = computed(() => {
|
||||
const bucket: Record<string, any[]> = {};
|
||||
const order: string[] = [];
|
||||
const noGuid: any[] = [];
|
||||
rows.value.forEach((r: any) => {
|
||||
const g = r.flowGuid || "";
|
||||
if (!g) {
|
||||
noGuid.push(r);
|
||||
return;
|
||||
}
|
||||
if (!bucket[g]) {
|
||||
bucket[g] = [];
|
||||
order.push(g);
|
||||
}
|
||||
bucket[g].push(r);
|
||||
});
|
||||
const out = order.map((g) => {
|
||||
const vs = bucket[g].slice().sort((a: any, b: any) => b.version - a.version);
|
||||
const published = vs.filter((v: any) => v.status === 1);
|
||||
const cur = published[0] || vs[0];
|
||||
return {
|
||||
_key: g, flowGuid: g, id: cur.id, name: cur.name, code: cur.code, appKey: cur.appKey,
|
||||
version: cur.version, status: cur.status, publishTime: cur.publishTime,
|
||||
total: vs.length, hasPublished: published.length > 0,
|
||||
versions: vs, _expanded: !!openMap.value[g],
|
||||
};
|
||||
});
|
||||
// 空 flowGuid 兜底:openMap 用合成 key,避免与其他行共用 "" 键互相牵连
|
||||
noGuid.forEach((r: any) => {
|
||||
const k = `__noguid__${r.id}`;
|
||||
out.push({
|
||||
_key: k, flowGuid: "", id: r.id, name: r.name, code: r.code, appKey: r.appKey,
|
||||
version: r.version, status: r.status, publishTime: r.publishTime,
|
||||
total: 1, hasPublished: r.status === 1, versions: [r], _expanded: !!openMap.value[k],
|
||||
});
|
||||
});
|
||||
return out;
|
||||
});
|
||||
|
||||
/** 展开行:该流程线全部版本明细(行对象即 FlwDef,操作复用 rowActions)。
|
||||
* 在途计数接口按 flowGuid 聚合,没有按版本的可靠数据,故明细列不铺在途、只在分组行展示。 */
|
||||
const versionColumns = [
|
||||
{ title: "版本", width: 70, render: (h: any, p: any) => h("b", `v${p.row.version}`) },
|
||||
{ title: "状态", width: 90, align: "center", render: renderStatus },
|
||||
{ title: "创建人", width: 100, render: (h: any, p: any) => h("span", p.row.createBy || "-") },
|
||||
{ title: "创建时间", width: 140, render: (h: any, p: any) => h("span", fmtTime(p.row.createTime)) },
|
||||
{ title: "改动说明", minWidth: 230, render: (h: any, p: any) => (p.row.changeLog
|
||||
? h("span", p.row.changeLog)
|
||||
: h("span", { style: "color:#999" }, "(未填写)")) },
|
||||
actionColumn(rowActions, { mode: "text", width: 210 }),
|
||||
];
|
||||
|
||||
const groupColumns = [
|
||||
{ type: "expand", width: 40, render: (h: any, p: any) => h(Table, {
|
||||
columns: versionColumns, data: p.row.versions, size: "small", noDataText: "暂无数据",
|
||||
}) },
|
||||
{ title: "序号", type: "index", width: 60, align: "center" },
|
||||
{ title: "流程", minWidth: 260, render: (h: any, p: any) => {
|
||||
const n = inflightMap.value[p.row.flowGuid];
|
||||
return h("span", {}, [
|
||||
h("b", p.row.name),
|
||||
h(Tag, { color: p.row.hasPublished ? "green" : "default", style: "margin-left:6px" },
|
||||
() => (p.row.hasPublished ? `当前生效 v${p.row.version}` : "无生效版")),
|
||||
h(Tag, { color: n ? "blue" : "default", style: "margin-left:6px" }, () => inflightText(p.row.flowGuid)),
|
||||
]);
|
||||
} },
|
||||
{ title: "编码", key: "code", width: 160 },
|
||||
{ title: "应用", key: "appKey", width: 130, align: "center", render: renderApp },
|
||||
{ title: "版本", width: 120, render: (h: any, p: any) => h("span", `v${p.row.version} · 共 ${p.row.total} 版`) },
|
||||
{ title: "状态", width: 90, align: "center", render: renderStatus },
|
||||
{ title: "GUID", width: 130, render: (h: any, p: any) => (p.row.flowGuid
|
||||
? h("span", { title: p.row.flowGuid }, `${String(p.row.flowGuid).slice(0, 8)}…`)
|
||||
: h("span", { style: "color:#999" }, "—")) },
|
||||
{ title: "发布时间", width: 160, render: (h: any, p: any) => h("span", fmtTime(p.row.publishTime)) },
|
||||
actionColumn(
|
||||
[
|
||||
{ key: "versions", label: "版本历史", icon: "icon-shijian", on: (r: any) => onVersions(r) },
|
||||
{ key: "editMeta", label: "编辑信息", icon: "icon-bianji", on: (r: any) => onEditMeta(r) },
|
||||
],
|
||||
{ mode: "text", width: 140, fixed: "right" },
|
||||
),
|
||||
];
|
||||
|
||||
/** 分组态点行展开/收起(展开箭头同一路径:箭头内部态会被下一帧的 _expanded 重新校准) */
|
||||
function onGroupRowClick(row: any) {
|
||||
if (row && row._key) {
|
||||
openMap.value[row._key] = !openMap.value[row._key];
|
||||
}
|
||||
}
|
||||
|
||||
/** 应用清单(筛选下拉):应用设置白名单(flw_app enabled=1),失败降级为空下拉 */
|
||||
async function loadAppKeys() {
|
||||
try {
|
||||
@@ -119,10 +255,13 @@ async function onPublish(row: any) {
|
||||
}
|
||||
|
||||
function onOffline(row: any) {
|
||||
// 议题二④:在途数 >0 才追加提示(0 或接口降级态沿用原文案)
|
||||
const n = inflightMap.value[row.flowGuid] || 0;
|
||||
MyConfirm({
|
||||
title: "停用",
|
||||
msg: `确定停用流程「${row.name}」吗?`,
|
||||
info: "停用后无法发起新办件,进行中的办件不受影响",
|
||||
info: "停用后无法发起新办件,进行中的办件不受影响"
|
||||
+ (n ? `;当前在途 ${n} 件仍跑本版直至完成` : ""),
|
||||
async onOk() {
|
||||
await flowApi.defOffline(row.id).send();
|
||||
Message.success("已停用");
|
||||
@@ -241,19 +380,39 @@ onMounted(() => {
|
||||
style="width: 200px; margin-left: 8px" @on-change="load">
|
||||
<Option v-for="a in apps" :key="a.appKey" :value="a.appKey">{{ a.appName }}</Option>
|
||||
</Select>
|
||||
<!-- 议题二②:分组(同 flowGuid 折叠一行,默认)/ 平铺(现状全版本行)纯前端切换 -->
|
||||
<RadioGroup v-model="viewMode" type="button" button-style="solid" class="fl" style="margin-left: 8px">
|
||||
<Radio label="group">仅最新版(分组)</Radio>
|
||||
<Radio label="flat">显示全部版本</Radio>
|
||||
</RadioGroup>
|
||||
<span class="fl" style="margin-left: 12px; color: #808695; font-size: 12px; line-height: 32px">
|
||||
{{ viewMode === "group"
|
||||
? "同 flowGuid 多版折叠为一行,点行展开可见各版创建人 / 创建时间 / 改动说明"
|
||||
: "平铺所有版本行(现状口径)" }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="table-region" ref="defsRegionRef">
|
||||
<Table :columns="columns" :data="rows" :loading="loading" class="link-table table-bottom-box"
|
||||
<Table v-if="viewMode === 'group'" :columns="groupColumns" :data="groups" :loading="loading"
|
||||
class="link-table table-bottom-box" :height="defsH || undefined" no-data-text="暂无数据"
|
||||
@on-row-click="onGroupRowClick" />
|
||||
<Table v-else :columns="columns" :data="rows" :loading="loading" class="link-table table-bottom-box"
|
||||
:height="defsH || undefined" no-data-text="暂无数据" />
|
||||
</div>
|
||||
|
||||
<Modal v-model="versionVisible" title="版本历史" width="640">
|
||||
<Modal v-model="versionVisible" title="版本历史" width="820">
|
||||
<p style="color: #808695; font-size: 12px; line-height: 1.6; margin-bottom: 8px">
|
||||
审计信息:创建人 / 创建时间 / 改动说明为本次新增列(存量版本未填说明时显示「(未填写)」)
|
||||
</p>
|
||||
<Table :columns="[
|
||||
{ title: '版本', key: 'version', width: 80 },
|
||||
{ title: '名称', key: 'name' },
|
||||
{ title: '状态', width: 90, render: (h, p) => h(Tag, () => DEF_STATUS[p.row.status]) },
|
||||
{ title: '发布时间', width: 160, render: (h, p) => (p.row.publishTime || '').replace('T', ' ') || '-' },
|
||||
{ title: '版本', key: 'version', width: 70, render: (h, p) => h('b', 'v' + p.row.version) },
|
||||
{ title: '名称', key: 'name', width: 150 },
|
||||
{ title: '状态', width: 80, render: (h, p) => h(Tag, () => DEF_STATUS[p.row.status]) },
|
||||
{ title: '发布时间', width: 132, render: (h, p) => (p.row.publishTime || '').replace('T', ' ').slice(0, 16) || '-' },
|
||||
{ title: '创建人', width: 90, render: (h, p) => p.row.createBy || '-' },
|
||||
{ title: '创建时间', width: 132, render: (h, p) => (p.row.createTime || '').replace('T', ' ').slice(0, 16) || '-' },
|
||||
{ title: '改动说明', minWidth: 230, render: (h, p) => p.row.changeLog
|
||||
? h('span', p.row.changeLog) : h('span', { style: 'color:#999' }, '(未填写)') },
|
||||
]" :data="versions" size="small" no-data-text="暂无数据" />
|
||||
</Modal>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
import { Message, Tag } from "view-ui-plus";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import ExprEvalModal from "@/views/flow/components/ExprEvalModal.vue";
|
||||
import { flowApi } from "@/api/module/flow";
|
||||
import { actionColumn } from "@/utils/tableActions";
|
||||
import { useElementHeight } from "@/utils/hook/useElementHeight";
|
||||
@@ -20,6 +21,12 @@ const columns = [
|
||||
{ title: "注册键", key: "routerKey", width: 220 },
|
||||
{ title: "名称", key: "name", width: 160 },
|
||||
{ title: "服务目录键", key: "serviceKey", width: 110 },
|
||||
{
|
||||
// 2026-10-12 SERVICE 收口:reg_app_key 非空=yh-flow-callback starter 自注册行(归属应用)
|
||||
title: "来源",
|
||||
width: 160,
|
||||
render: (h: any, p: any) => h("span", p.row.regAppKey ? `自注册 · ${p.row.regAppKey}` : "手工"),
|
||||
},
|
||||
{ title: "判断端点", key: "path", minWidth: 300 },
|
||||
{
|
||||
title: "basePath",
|
||||
@@ -129,33 +136,11 @@ function onEnable(row: any) {
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 表达式调试(2026-10-10 四议题):与运行时同一求值核心(/admin/router/eval) ----
|
||||
const evalVisible = ref(false);
|
||||
const evaluating = ref(false);
|
||||
const evalForm = ref<{ expression: string; variablesJson: string }>({ expression: "", variablesJson: "" });
|
||||
const evalResult = ref<{ result: boolean; value?: string; error?: string } | null>(null);
|
||||
// ---- 表达式调试(2026-10-10 四议题;2026-10-12 议题三改为三模式共享弹窗 ExprEvalModal)----
|
||||
const exprVisible = ref(false);
|
||||
|
||||
function onEval() {
|
||||
evalForm.value = { expression: "", variablesJson: "" };
|
||||
evalResult.value = null;
|
||||
evalVisible.value = true;
|
||||
}
|
||||
|
||||
async function doEval() {
|
||||
if (!evalForm.value.expression.trim()) {
|
||||
Message.warning("请填写表达式");
|
||||
return;
|
||||
}
|
||||
evaluating.value = true;
|
||||
evalResult.value = null;
|
||||
try {
|
||||
evalResult.value = (await flowApi.routerEval({
|
||||
expression: evalForm.value.expression.trim(),
|
||||
variablesJson: evalForm.value.variablesJson.trim(),
|
||||
}).send()) || null;
|
||||
} finally {
|
||||
evaluating.value = false;
|
||||
}
|
||||
exprVisible.value = true;
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
@@ -208,34 +193,7 @@ onMounted(load);
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model="evalVisible" title="表达式调试" :mask-closable="false" :width="560">
|
||||
<p style="color: #808695; font-size: 12px; line-height: 1.6; margin-bottom: 10px">
|
||||
与 CONDITION 节点 EXPRESSION 模式同一套求值(AviatorScript 沙箱)。变量 = 样例 JSON 的
|
||||
键 + 线上另注入 schId/bizType/bizId/submitterId/instanceId/instanceVersion/nodeId/nodeName
|
||||
(调试时请在样例里自行提供内置变量取值)。变量未提供会报错——线上同口径(宁挂起不误放行)
|
||||
</p>
|
||||
<Input v-model="evalForm.expression" placeholder="表达式,如 amount > 1000" :maxlength="200"
|
||||
style="margin-bottom: 8px" />
|
||||
<Input v-model="evalForm.variablesJson" type="textarea" :rows="5"
|
||||
placeholder='样例变量 JSON,如 {"amount": 2000, "bizType": "leave"}' />
|
||||
<div v-if="evalResult" style="margin-top: 10px; font-size: 13px">
|
||||
<template v-if="evalResult.error">
|
||||
<span style="color: #ed4014">✗ 求值失败:{{ evalResult.error }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span :style="{ color: evalResult.result ? '#19be6b' : '#ff9900' }">
|
||||
{{ evalResult.result ? "✓ true(命中该出口)" : "✗ false(不命中)" }}
|
||||
</span>
|
||||
<span v-if="evalResult.value !== undefined" style="color: #808695; margin-left: 8px">
|
||||
求值值:{{ evalResult.value }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button @click="evalVisible = false">关闭</Button>
|
||||
<Button type="primary" :loading="evaluating" @click="doEval">求值</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<ExprEvalModal v-model="exprVisible" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
// 分支按出口名互斥选路(运行时调判断服务/表达式求值),分支尾隐式汇合到容器后继。
|
||||
// 分支头部的出口名 Input 即 contentJson 里 CONDITION 命名出边的 name(判断接口 outlets 契约);
|
||||
// EXPRESSION 模式(2026-10-10)下分支头部再就地编辑出边 condition,序号即求值优先级。
|
||||
import { computed, inject } from "vue";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { BRANCH_LABELS, EDITOR_CTX, MAX_BRANCHES, MIN_BRANCHES, ConditionNode } from "../model";
|
||||
import NodeList from "./NodeList.vue";
|
||||
import ExprEvalModal from "@/views/flow/components/ExprEvalModal.vue";
|
||||
|
||||
const props = defineProps<{ node: ConditionNode }>();
|
||||
|
||||
@@ -35,10 +36,19 @@ function editCondition(i: number, v: string): void {
|
||||
while (props.node.conditions.length <= i) props.node.conditions.push("");
|
||||
props.node.conditions[i] = v;
|
||||
}
|
||||
|
||||
// B5 内联试算:把该出口表达式带进共享调试弹窗(ExprEvalModal)
|
||||
const tryVisible = ref(false);
|
||||
const tryExpr = ref("");
|
||||
|
||||
function openTry(i: number): void {
|
||||
tryExpr.value = (props.node.conditions || [])[i] || "";
|
||||
tryVisible.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="condition-box" :class="{ error: hasError }">
|
||||
<div :id="'nd-' + node.uid" class="condition-box" :class="{ error: hasError }">
|
||||
<div class="cb-title" @click="ctx.openNode(node.uid)">
|
||||
条件分支(按出口互斥选路)
|
||||
<span class="cb-hint">(点击标题配置{{ expression ? "表达式" : "判断服务" }})</span>
|
||||
@@ -69,14 +79,17 @@ function editCondition(i: number, v: string): void {
|
||||
<span class="cb-col-del" title="删除分支">×</span>
|
||||
</Poptip>
|
||||
</div>
|
||||
<div v-if="expression" class="cb-cond-row">
|
||||
<input
|
||||
v-if="expression"
|
||||
class="cb-cond"
|
||||
:value="(node.conditions || [])[i] || ''"
|
||||
placeholder="条件表达式,如 amount > 1000"
|
||||
maxlength="200"
|
||||
@input="editCondition(i, ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<!-- B5 内联试算:用真实办件变量跑该出口表达式(同「条件路由」页弹窗,预填表达式) -->
|
||||
<span class="cb-try" title="用真实办件变量试算该表达式" @click.stop="openTry(i)">试算</span>
|
||||
</div>
|
||||
<NodeList :items="branch" :container="`${node.uid}:${i}`" />
|
||||
<div v-if="!branch.length" class="cb-empty">该出口直达汇合点(直接通过)</div>
|
||||
</div>
|
||||
@@ -85,6 +98,7 @@ function editCondition(i: number, v: string): void {
|
||||
+ 添加分支
|
||||
</Button>
|
||||
<div v-for="(issue, k) in issues" :key="k" class="cb-issue" :class="issue.level">⚠ {{ issue.text }}</div>
|
||||
<ExprEvalModal v-model="tryVisible" :preset-expression="tryExpr" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -192,6 +206,31 @@ function editCondition(i: number, v: string): void {
|
||||
.cb-cond::placeholder {
|
||||
color: #c5b3e8;
|
||||
}
|
||||
.cb-cond-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.cb-cond-row .cb-cond {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.cb-try {
|
||||
flex: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #0b7d72;
|
||||
border: 1px solid #b7e3dc;
|
||||
border-radius: 10px;
|
||||
padding: 0 8px;
|
||||
line-height: 18px;
|
||||
user-select: none;
|
||||
}
|
||||
.cb-try:hover {
|
||||
background: #f2fbf9;
|
||||
border-color: #0b7d72;
|
||||
}
|
||||
.cb-col-del {
|
||||
flex: none;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -21,7 +21,8 @@ const ctx = inject(EDITOR_CTX)!;
|
||||
const form = reactive({
|
||||
name: "",
|
||||
routerKey: "",
|
||||
judgeMode: "SERVICE" as JudgeMode,
|
||||
// 2026-10-12 SERVICE 收口:新建缺省翻转为表达式(主路径);存量归一化见 syncFromNode(勿动)
|
||||
judgeMode: "EXPRESSION" as JudgeMode,
|
||||
});
|
||||
|
||||
function syncFromNode(): void {
|
||||
@@ -134,6 +135,11 @@ function onRemove(): void {
|
||||
<div v-else class="hint">
|
||||
判断逻辑由业务方实现:引擎运行到该节点时同步调用判断接口,按返回出口名走对应分支
|
||||
</div>
|
||||
<div class="hint">
|
||||
仅当判断依赖流程到达时点的实时业务数据时使用;简单条件请用"表达式"(apply 时 formData
|
||||
自带判断数据)。判断端点与登记由
|
||||
<code>yh-flow-callback-spring-boot-starter</code>(流程中心接入件)自动完成,见《子应用接入流程中心规范》§6.6。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="fld">
|
||||
|
||||
@@ -20,7 +20,7 @@ function onOpen(e: MouseEvent): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="node-card" :class="{ error: hasError }" @click="onOpen">
|
||||
<div :id="'nd-' + node.uid" class="node-card" :class="{ error: hasError }" @click="onOpen">
|
||||
<div class="nc-head">
|
||||
<span class="nc-icon">审</span>
|
||||
<span class="nc-name">{{ node.name || "未命名节点" }}</span>
|
||||
|
||||
@@ -19,7 +19,7 @@ function onOpen(e: MouseEvent): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="notify-card" :class="{ error: hasError }" @click="onOpen">
|
||||
<div :id="'nd-' + node.uid" class="notify-card" :class="{ error: hasError }" @click="onOpen">
|
||||
<div class="nc-head">
|
||||
<span class="nc-icon">抄</span>
|
||||
<span class="nc-name">{{ node.name || "未命名节点" }}</span>
|
||||
|
||||
@@ -55,6 +55,8 @@ const baseGuid = ref("");
|
||||
/** 载入源定义的状态(已发布/停用版本进入编辑器时提示保存会新建草稿版本) */
|
||||
const sourceStatus = ref(0);
|
||||
const sourceVersion = ref(0);
|
||||
/** 源流程线 flowGuid(新建态为空,右面板显示「保存后生成」) */
|
||||
const sourceFlowGuid = ref("");
|
||||
/** 归属应用(flw_def.app_key;按应用筛选维度,空=未分配) */
|
||||
const appKey = ref("");
|
||||
/** 应用清单下拉(应用设置白名单 flw_app enabled=1,显示应用名),拉取失败降级为仅当前值 */
|
||||
@@ -71,6 +73,44 @@ const routers = reactive<RouterOption[]>([]);
|
||||
const issues = computed<Issue[]>(() => validateModel(model, depts, routers));
|
||||
const defJson = computed(() => toContentJson(model));
|
||||
|
||||
/** 结构统计(右面板分区4):walkItems 已递归条件/并行分支的子树 */
|
||||
const stats = computed(() => {
|
||||
const s: Record<string, number> = { approval: 0, notify: 0, condition: 0, parallel: 0 };
|
||||
walkItems(model.list, (item) => {
|
||||
if (s[item.kind] !== undefined) s[item.kind] += 1;
|
||||
});
|
||||
return s;
|
||||
});
|
||||
|
||||
/** 校验清单(右面板分区3):错误项在前、警告项在后(原型 E2 顺序) */
|
||||
const issueList = computed<Issue[]>(() => [
|
||||
...issues.value.filter((i) => i.level === "error"),
|
||||
...issues.value.filter((i) => i.level !== "error"),
|
||||
]);
|
||||
|
||||
/** 点清单项 → 画布滚动到对应节点并高亮 ~3s;uid 为空(空容器类)不可定位 */
|
||||
function locateIssue(uid: string): void {
|
||||
const el = document.getElementById("nd-" + uid);
|
||||
if (!el) return;
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
el.classList.add("nd-locate");
|
||||
window.setTimeout(() => el.classList.remove("nd-locate"), 2800);
|
||||
}
|
||||
|
||||
/** flowGuid 复制:clipboard 不可用/被拒时降级 console,不打断编辑 */
|
||||
function copyGuid(): void {
|
||||
const g = sourceFlowGuid.value;
|
||||
if (!g) return;
|
||||
if (navigator.clipboard?.writeText) {
|
||||
navigator.clipboard.writeText(g).then(
|
||||
() => Message.success("flowGuid 已复制"),
|
||||
() => console.warn("flowGuid 复制失败,请手工选取:", g),
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.warn("flowGuid 复制失败,请手工选取:", g);
|
||||
}
|
||||
|
||||
/** 归一化(去 layout 坐标、节点按 id 排序):卡片树投影与画布序列化表示不同但内容等价,脏比较必须同表示 */
|
||||
function canonicalJson(content: any): string {
|
||||
const copy: any = JSON.parse(JSON.stringify(content));
|
||||
@@ -100,6 +140,10 @@ const conditionDrawer = ref(false);
|
||||
const settingsDrawer = ref(false);
|
||||
const jsonDrawer = ref(false);
|
||||
const editingUid = ref("");
|
||||
// 议题二③:升版本改动说明弹窗(仅「本次保存将创建新版本」时出现,可跳过)
|
||||
const logModal = ref(false);
|
||||
const logText = ref("");
|
||||
const LOG_CHIPS = ["修复审批人缺失", "调整审批链", "新增抄送对象", "修改条件表达式", "调整流程设置"];
|
||||
|
||||
const editingNode = computed<ApprovalNode | null>(() => {
|
||||
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
||||
@@ -197,6 +241,7 @@ async function load(): Promise<void> {
|
||||
const d: any = await flowApi.defGet(queryId).send();
|
||||
sourceStatus.value = d?.status ?? 0;
|
||||
sourceVersion.value = d?.version ?? 0;
|
||||
sourceFlowGuid.value = d?.flowGuid || queryGuid || "";
|
||||
// 非草稿进入提示(2026-10-10 议题2):结构改动保存将升版本;仅改名称/归属应用请回列表用「编辑信息」
|
||||
if (d?.status === 1 || d?.status === 2) {
|
||||
Message.info("结构改动保存将升版本;仅改名称/归属应用请返回列表用「编辑信息」(不升版本)");
|
||||
@@ -399,6 +444,28 @@ async function onSave(): Promise<void> {
|
||||
}
|
||||
|
||||
async function doSave(): Promise<void> {
|
||||
// 议题二③:本次保存将复制出新版本行时先问改动说明(挂在 doSave 入口,onSave 与同编码软确认两条路径都经过)
|
||||
if (willCreateVersion()) {
|
||||
logText.value = "";
|
||||
logModal.value = true;
|
||||
return;
|
||||
}
|
||||
await submitSave();
|
||||
}
|
||||
|
||||
/** 升版本判据:baseGuid 入口(列表「升版本」)必带 baseGuid;编辑已发布/停用版会走后端复制升版分支 */
|
||||
function willCreateVersion(): boolean {
|
||||
return (!editId.value && !!baseGuid.value) || (!!editId.value && sourceStatus.value !== 0);
|
||||
}
|
||||
|
||||
/** 常用改动类型点选:追加到说明文本(分号连接),可再手工补充 */
|
||||
function appendLogChip(text: string): void {
|
||||
const cur = logText.value.trim();
|
||||
logText.value = cur ? `${cur};${text}` : text;
|
||||
}
|
||||
|
||||
async function submitSave(changeLog?: string): Promise<void> {
|
||||
logModal.value = false;
|
||||
const content = JSON.stringify(toContentJson(model));
|
||||
saving.value = true;
|
||||
try {
|
||||
@@ -410,9 +477,12 @@ async function doSave(): Promise<void> {
|
||||
contentJson: content,
|
||||
baseGuid: baseGuid.value || undefined,
|
||||
appKey: (appKey.value || "").trim() || undefined,
|
||||
changeLog: changeLog || undefined,
|
||||
}).send();
|
||||
savedSnapshot.value = currentSnapshot.value;
|
||||
Message.success("已保存草稿");
|
||||
Message.success(willCreateVersion()
|
||||
? (changeLog ? "已保存草稿 · 改动说明已留存" : "已保存草稿(未填写改动说明)")
|
||||
: "已保存草稿");
|
||||
router.push("/admin/defs");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
@@ -480,33 +550,12 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
<Tag v-else-if="baseGuid" color="blue">升版本 · 基于 v{{ sourceVersion }}({{ DEF_STATUS[sourceStatus] }})</Tag>
|
||||
<Tag v-else color="green">新建</Tag>
|
||||
</div>
|
||||
<div class="hd-mid">
|
||||
<label>名称 <Input v-model="model.name" placeholder="流程名称" style="width: 200px" :maxlength="30" /></label>
|
||||
<label>
|
||||
编码
|
||||
<Input
|
||||
v-model="model.code"
|
||||
placeholder="内部管理键,发布后不可改"
|
||||
style="width: 220px"
|
||||
:disabled="!!editId || !!baseGuid"
|
||||
:maxlength="40"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
应用 <span style="color: #ed4014">*</span>
|
||||
<Select v-model="appKey" placeholder="归属应用(必选)" style="width: 170px" filterable>
|
||||
<Option v-for="a in appOptions" :key="a.appKey" :value="a.appKey">{{ a.appName }}</Option>
|
||||
</Select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="hd-right">
|
||||
<Button @click="settingsDrawer = true">流程设置</Button>
|
||||
<Button @click="jsonDrawer = true">源码</Button>
|
||||
<Button :disabled="!editId" @click="onOpenDiagram">图示预览</Button>
|
||||
<Button type="primary" :loading="saving" @click="onSave">保存草稿</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="ed-body">
|
||||
<main v-if="!loading" class="ed-canvas">
|
||||
<div class="ed-hint">点击卡片配置节点 · 悬停虚线处 + 插入节点(审批 / 抄送 / 并行分支)</div>
|
||||
<div class="ed-flow">
|
||||
@@ -515,25 +564,114 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
<div class="capsule end">流程结束</div>
|
||||
</div>
|
||||
</main>
|
||||
<main v-else-if="loading" class="ed-canvas loading">
|
||||
<main v-else class="ed-canvas loading">
|
||||
<Spin size="large" fix />
|
||||
</main>
|
||||
|
||||
<footer class="ed-status" :class="{ bad: countErrors(issues).length }">
|
||||
<span v-if="countErrors(issues).length" class="st-bad">
|
||||
✗ {{ countErrors(issues).length }} 个问题:{{ countErrors(issues).map((i) => i.text).join(";") }}
|
||||
<!-- 议题一:右信息面板(五分区,规格对齐原型 E1) -->
|
||||
<aside class="ed-info">
|
||||
<section class="ip-sec">
|
||||
<h4>流程信息<span class="ip-mini">可编辑 · 保存走现有 defSave</span></h4>
|
||||
<div class="ip-fld">
|
||||
<label>名称</label>
|
||||
<Input v-model="model.name" placeholder="流程名称" :maxlength="30" />
|
||||
</div>
|
||||
<div class="ip-fld">
|
||||
<label>编码<span class="ip-mini">升版本态禁用(发布后不可改)</span></label>
|
||||
<Input v-model="model.code" placeholder="内部管理键,发布后不可改"
|
||||
:disabled="!!editId || !!baseGuid" :maxlength="40" />
|
||||
</div>
|
||||
<div class="ip-fld">
|
||||
<label>归属应用 <span style="color: #ed4014">*</span></label>
|
||||
<Select v-model="appKey" placeholder="归属应用(必选)" filterable>
|
||||
<Option v-for="a in appOptions" :key="a.appKey" :value="a.appKey">{{ a.appName }}</Option>
|
||||
</Select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ip-sec">
|
||||
<h4>状态与版本<span class="ip-mini">只读</span></h4>
|
||||
<div class="ip-tags">
|
||||
<Tag v-if="editId && sourceStatus === 0" color="orange">草稿</Tag>
|
||||
<Tag v-else-if="editId" color="red">{{ DEF_STATUS[sourceStatus] }} · 保存将新建草稿版本</Tag>
|
||||
<Tag v-else-if="baseGuid" color="blue">升版本 · 基于 v{{ sourceVersion }}</Tag>
|
||||
<Tag v-else color="green">新建</Tag>
|
||||
<Tag v-if="sourceVersion" color="gray">v{{ sourceVersion }}</Tag>
|
||||
</div>
|
||||
<div class="ip-guid">
|
||||
<span v-if="sourceFlowGuid" class="ip-mono">{{ sourceFlowGuid }}</span>
|
||||
<span v-else class="ip-mini">保存后生成</span>
|
||||
<Button v-if="sourceFlowGuid" size="small" @click="copyGuid">复制</Button>
|
||||
</div>
|
||||
<div class="ip-tip">flowGuid 发布后不变,业务方绑定流程用此键<br>保存仅生成草稿;发布校验以后端为准</div>
|
||||
</section>
|
||||
|
||||
<section class="ip-sec">
|
||||
<h4>校验问题清单
|
||||
<span class="ip-mini">{{ countErrors(issues).length ? `${issueList.length} 项待处理`
|
||||
: (countWarns(issues).length ? `${countWarns(issues).length} 个警告` : "无问题") }}</span>
|
||||
</h4>
|
||||
<div v-if="!issueList.length" class="ip-ok">✓ 校验通过</div>
|
||||
<div v-else-if="!countErrors(issues).length" class="ip-ok">
|
||||
✓ 校验通过({{ countWarns(issues).length }} 个警告)
|
||||
</div>
|
||||
<div v-for="(i, idx) in issueList" :key="`${i.uid}-${idx}`" class="ip-issue"
|
||||
:class="[(i.level === 'error' ? 'err' : 'warn'), (i.uid ? 'loc' : 'fix')]"
|
||||
@click="i.uid && locateIssue(i.uid)">
|
||||
<span class="ip-ic">{{ i.level === "error" ? "✗" : "!" }}</span>
|
||||
<span class="ip-tx">{{ i.path }}:{{ i.msg }}
|
||||
<span v-if="i.level !== 'error'" class="ip-sub">(警告,不阻塞保存)</span>
|
||||
</span>
|
||||
<span v-else class="st-ok">
|
||||
✓ 校验通过<span v-if="countWarns(issues).length">({{ countWarns(issues).length }} 个警告)</span>
|
||||
</span>
|
||||
<span class="st-right">保存仅生成草稿;发布校验以后端为准</span>
|
||||
</footer>
|
||||
<span v-if="i.uid" class="ip-go">定位 →</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ip-sec">
|
||||
<h4>结构统计</h4>
|
||||
<div class="ip-stats">
|
||||
<div class="ip-stat"><b>{{ stats.approval }}</b><span>审批节点</span></div>
|
||||
<div class="ip-stat"><b>{{ stats.notify }}</b><span>抄送节点</span></div>
|
||||
<div class="ip-stat"><b>{{ stats.condition }}</b><span>条件分支</span></div>
|
||||
<div class="ip-stat"><b>{{ stats.parallel }}</b><span>并行分支</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ip-sec">
|
||||
<h4>快捷入口</h4>
|
||||
<div class="ip-quick">
|
||||
<Button size="small" @click="settingsDrawer = true">流程设置</Button>
|
||||
<Button size="small" @click="jsonDrawer = true">源码</Button>
|
||||
<Button size="small" :disabled="!editId" @click="onOpenDiagram">图示预览</Button>
|
||||
</div>
|
||||
<div class="ip-tip">「图示预览」仅已载入的草稿/版本可用(同现状 :disabled="!editId")</div>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<ApprovalDrawer v-model="nodeDrawer" :node="editingNode" @removed="nodeDrawer = false" />
|
||||
<NotifyDrawer v-model="notifyDrawer" :node="editingNotify" @removed="notifyDrawer = false" />
|
||||
<ConditionDrawer v-model="conditionDrawer" :node="editingCondition" @removed="conditionDrawer = false" />
|
||||
<SettingsDrawer v-model="settingsDrawer" :settings="model.settings" @save="onUrgeSave" />
|
||||
<JsonDrawer v-model="jsonDrawer" :def="defJson" @applied="onJsonApplied" />
|
||||
|
||||
<!-- 议题二③:升版本改动说明(仅「本次保存将创建新版本」时出现,跳过也放行) -->
|
||||
<Modal v-model="logModal" title="保存为新版本草稿" :mask-closable="false" width="520">
|
||||
<p class="cl-note">
|
||||
当前为「升版本 · 基于 v{{ sourceVersion }}({{ DEF_STATUS[sourceStatus] }})」,
|
||||
保存将创建新版本草稿(flowGuid 不变,版本号自动 +1)。
|
||||
</p>
|
||||
<div class="cl-label">本次改动说明<span class="cl-skip">可跳过</span></div>
|
||||
<div class="cl-chips">
|
||||
<span v-for="c in LOG_CHIPS" :key="c" class="cl-chip" @click="appendLogChip(c)">{{ c }}</span>
|
||||
</div>
|
||||
<Input v-model="logText" type="textarea" :rows="3" :maxlength="500" show-word-limit
|
||||
placeholder="一句话说清本版改了什么,如:教务处节点补充审批人配置;版本历史与列表可见" />
|
||||
<template #footer>
|
||||
<span class="cl-hint">跳过也允许保存——但「多一条记录」的区分度就没了</span>
|
||||
<Button :disabled="saving" @click="submitSave()">跳过</Button>
|
||||
<Button type="primary" :loading="saving" @click="submitSave(logText.trim())">保存草稿</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -573,31 +711,18 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
.hd-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
.hd-mid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
.hd-mid label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #808695;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.hd-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
margin-left: auto;
|
||||
}
|
||||
.ed-canvas {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
padding: 20px 16px 60px;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
@@ -636,28 +761,194 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
color: #808695;
|
||||
border: 1.5px solid #808695;
|
||||
}
|
||||
.ed-status {
|
||||
/* 升版本改动说明弹窗(议题二③,规格对齐原型 V2) */
|
||||
.cl-note {
|
||||
background: #fff7e6;
|
||||
border: 1px solid #ffd77a;
|
||||
border-radius: 6px;
|
||||
padding: 9px 12px;
|
||||
font-size: 12px;
|
||||
color: #8a5a17;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.cl-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #17233d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.cl-skip {
|
||||
font-weight: 400;
|
||||
color: #808695;
|
||||
font-size: 12px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.cl-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.cl-chip {
|
||||
border: 1px solid #e3e6eb;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 3px 11px;
|
||||
font-size: 12px;
|
||||
color: #515a6e;
|
||||
cursor: pointer;
|
||||
}
|
||||
.cl-chip:hover {
|
||||
border-color: var(--brand);
|
||||
color: var(--brand);
|
||||
}
|
||||
.cl-hint {
|
||||
float: left;
|
||||
font-size: 12px;
|
||||
color: #b0b6c1;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
/* 议题一:左画布 + 右信息面板(原型 E1,面板宽 340) */
|
||||
.ed-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
}
|
||||
.ed-info {
|
||||
width: 340px;
|
||||
flex: none;
|
||||
overflow: auto;
|
||||
border-left: 1px solid #e8eaec;
|
||||
background: #fff;
|
||||
padding: 4px 14px 20px;
|
||||
}
|
||||
.ip-sec {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f1f3;
|
||||
}
|
||||
.ip-sec:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.ip-sec h4 {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #17233d;
|
||||
margin: 0 0 10px;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
.ip-mini {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #a5abb8;
|
||||
}
|
||||
.ip-fld {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ip-fld label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.ip-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.ip-guid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
gap: 8px;
|
||||
}
|
||||
.ip-mono {
|
||||
font-family: Consolas, Monaco, "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
z-index: 9;
|
||||
color: #515a6e;
|
||||
word-break: break-all;
|
||||
}
|
||||
.ed-status.bad {
|
||||
background: #fff5f5;
|
||||
.ip-tip {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #a5abb8;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.st-ok {
|
||||
.ip-ok {
|
||||
font-size: 12.5px;
|
||||
color: #19be6b;
|
||||
}
|
||||
.st-bad {
|
||||
.ip-issue {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.6;
|
||||
padding: 5px 6px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.ip-issue.err {
|
||||
background: #fff5f5;
|
||||
color: #ed4014;
|
||||
}
|
||||
.st-right {
|
||||
margin-left: auto;
|
||||
color: #b0b6c1;
|
||||
white-space: nowrap;
|
||||
.ip-issue.warn {
|
||||
background: #fff9f0;
|
||||
color: #ff9900;
|
||||
}
|
||||
.ip-issue.loc {
|
||||
cursor: pointer;
|
||||
}
|
||||
.ip-issue.loc:hover {
|
||||
filter: brightness(0.97);
|
||||
}
|
||||
.ip-issue.fix {
|
||||
cursor: default;
|
||||
}
|
||||
.ip-ic {
|
||||
flex: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
.ip-tx {
|
||||
flex: 1;
|
||||
}
|
||||
.ip-sub {
|
||||
color: #a5abb8;
|
||||
}
|
||||
.ip-go {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
.ip-stats {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.ip-stat {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
background: #f7f9fc;
|
||||
border-radius: 6px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.ip-stat b {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
color: #17233d;
|
||||
}
|
||||
.ip-stat span {
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
}
|
||||
.ip-quick {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* 校验清单定位高亮(原型 E2,用项目错误红不做原型紫);卡片在子组件内,需 :deep 命中 */
|
||||
:deep(.nd-locate) {
|
||||
box-shadow: 0 0 0 4px rgba(237, 64, 20, 0.18) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -177,14 +177,14 @@ export function createParallel(): ParallelNode {
|
||||
return { uid: newUid(), kind: "parallel", branches: [[createApproval()], [createApproval()]] };
|
||||
}
|
||||
|
||||
/** 条件分支:默认 2 分支占位出口名,插入后弹配置抽屉选判断方式(缺省服务) */
|
||||
/** 条件分支:默认 2 分支占位出口名,插入后弹配置抽屉选判断方式(2026-10-12 收口:缺省表达式) */
|
||||
export function createCondition(): ConditionNode {
|
||||
return {
|
||||
uid: newUid(),
|
||||
kind: "condition",
|
||||
name: "条件判断",
|
||||
routerKey: "",
|
||||
judgeMode: "SERVICE",
|
||||
judgeMode: "EXPRESSION",
|
||||
conditions: ["", ""],
|
||||
outlets: ["出口一", "出口二"],
|
||||
branches: [[createApproval()], [createApproval()]],
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
<script setup lang="ts">
|
||||
// 表达式调试弹窗(议题三,原型 S1-S5):三模式取变量 + 与运行时同一求值口径(/admin/router/eval)。
|
||||
// 抽成共享组件供条件路由页与(B5)编辑器出口试算共用:外部只传 modelValue / presetExpression。
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { Message, Tag } from "view-ui-plus";
|
||||
import { INSTANCE_STATUS, INSTANCE_STATUS_COLOR, flowApi } from "@/api/module/flow";
|
||||
|
||||
const props = defineProps<{ modelValue: boolean; presetExpression?: string }>();
|
||||
const emit = defineEmits<{ (e: "update:modelValue", v: boolean): void }>();
|
||||
|
||||
type Mode = "instance" | "routelog" | "manual";
|
||||
const mode = ref<Mode>("instance");
|
||||
const expr = ref("");
|
||||
const varsJson = ref("");
|
||||
const varsParseError = ref(false);
|
||||
|
||||
const instances = ref<any[]>([]);
|
||||
const instLoading = ref(false);
|
||||
const pickedId = ref<any>(null);
|
||||
const bizTypeFilter = ref("");
|
||||
const traceRows = ref<any[]>([]);
|
||||
const traceLoading = ref(false);
|
||||
const traceInstanceId = ref<any>(null);
|
||||
const result = ref<{ result: boolean; value?: string; error?: string } | null>(null);
|
||||
const evaluating = ref(false);
|
||||
const exprBox = ref<any>(null);
|
||||
|
||||
/** 内置变量(线上由引擎注入;schId 属管理端上下文不展示,nodeId/nodeName 需按调试目标自填) */
|
||||
const BUILTIN_KEYS = ["instanceId", "instanceVersion", "bizType", "bizId", "submitterId", "nodeId", "nodeName"];
|
||||
const BUILTIN_SET = new Set(BUILTIN_KEYS);
|
||||
const picked = computed(() => instances.value.find((r) => r.id === pickedId.value) || null);
|
||||
const builtinRows = computed(() => {
|
||||
const p = picked.value;
|
||||
if (!p) return [];
|
||||
return [
|
||||
{ k: "instanceId", v: p.id },
|
||||
{ k: "instanceVersion", v: p.version },
|
||||
{ k: "bizType", v: p.bizType },
|
||||
{ k: "bizId", v: p.bizId },
|
||||
{ k: "submitterId", v: p.submitterId },
|
||||
{ k: "nodeId", v: "", todo: true },
|
||||
{ k: "nodeName", v: "", todo: true },
|
||||
];
|
||||
});
|
||||
|
||||
/** 当前变量 JSON 的顶层键(解析失败为空,不阻塞手动编辑) */
|
||||
const formKeys = computed<string[]>(() => {
|
||||
try {
|
||||
const v = JSON.parse(varsJson.value || "{}");
|
||||
return v && typeof v === "object" && !Array.isArray(v) ? Object.keys(v) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
/** 手动模式的键来源:最近 50 条办件 formData 聚合出现过的键(去重后截 40) */
|
||||
const manualKeys = computed<string[]>(() => {
|
||||
const set = new Set<string>();
|
||||
instances.value.forEach((r) => {
|
||||
try {
|
||||
const v = JSON.parse(r.formDataJson || "{}");
|
||||
if (v && typeof v === "object") Object.keys(v).forEach((k) => set.add(k));
|
||||
} catch {
|
||||
/* 脏 formData 跳过,不影响其它办件的键聚合 */
|
||||
}
|
||||
});
|
||||
return Array.from(set).slice(0, 40);
|
||||
});
|
||||
/** chips:formData 键(手动模式取历史办件聚合键)+ 内置键;与键名重复时去重,内置键单独置灰标注 */
|
||||
const chips = computed(() => Array.from(new Set([
|
||||
...(mode.value === "manual" ? manualKeys.value : formKeys.value),
|
||||
...BUILTIN_KEYS,
|
||||
])));
|
||||
|
||||
/** bizType 筛选 chips(取当前页 distinct) */
|
||||
const bizTypes = computed(() => Array.from(new Set(instances.value.map((r) => r.bizType).filter(Boolean))));
|
||||
const shownInstances = computed(() =>
|
||||
bizTypeFilter.value ? instances.value.filter((r) => r.bizType === bizTypeFilter.value) : instances.value
|
||||
);
|
||||
|
||||
async function loadInstances(): Promise<void> {
|
||||
instLoading.value = true;
|
||||
try {
|
||||
const res: any = await flowApi.adminInstances({ page: 1, size: 50 }).send();
|
||||
instances.value = res?.content || [];
|
||||
} catch (e: any) {
|
||||
instances.value = [];
|
||||
Message.error(e?.message || "办件列表拉取失败");
|
||||
} finally {
|
||||
instLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 选中办件 → formData 解析后缩进填入;解析失败填原文并红字提示 */
|
||||
function fillFromInstance(id: any): void {
|
||||
const p = instances.value.find((r) => r.id === id);
|
||||
if (!p) return;
|
||||
result.value = null;
|
||||
varsParseError.value = false;
|
||||
try {
|
||||
varsJson.value = JSON.stringify(JSON.parse(p.formDataJson || "{}"), null, 2);
|
||||
} catch {
|
||||
varsJson.value = String(p.formDataJson || "");
|
||||
varsParseError.value = true;
|
||||
}
|
||||
if (mode.value === "routelog") void loadTrace(p.id);
|
||||
}
|
||||
|
||||
async function loadTrace(instanceId: any): Promise<void> {
|
||||
if (!instanceId) return;
|
||||
traceLoading.value = true;
|
||||
traceInstanceId.value = instanceId;
|
||||
try {
|
||||
const d: any = await flowApi.detail(instanceId).send();
|
||||
traceRows.value = (d?.trace || []).filter((r: any) => r.kind === "ROUTE");
|
||||
} catch (e: any) {
|
||||
traceRows.value = [];
|
||||
Message.error(e?.message || "办件详情拉取失败");
|
||||
} finally {
|
||||
traceLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 载入该办件变量并复现(保留当前表达式) */
|
||||
async function replayFromLog(): Promise<void> {
|
||||
if (!traceInstanceId.value) {
|
||||
Message.warning("请先选择办件");
|
||||
return;
|
||||
}
|
||||
fillFromInstance(traceInstanceId.value);
|
||||
await doEval();
|
||||
}
|
||||
|
||||
function outletText(r: any): string {
|
||||
return r.outlet ? r.outlet : "未命中";
|
||||
}
|
||||
|
||||
/** chips 点击插入表达式光标处 */
|
||||
function insertChip(key: string): void {
|
||||
const input: HTMLInputElement | null = exprBox.value?.$el?.querySelector?.("input") || null;
|
||||
const cur = expr.value;
|
||||
if (!input) {
|
||||
expr.value = cur ? `${cur} ${key}` : key;
|
||||
return;
|
||||
}
|
||||
const at = input.selectionStart ?? cur.length;
|
||||
const end = input.selectionEnd ?? at;
|
||||
expr.value = cur.slice(0, at) + key + cur.slice(end);
|
||||
requestAnimationFrame(() => {
|
||||
input.focus();
|
||||
input.setSelectionRange(at + key.length, at + key.length);
|
||||
});
|
||||
}
|
||||
|
||||
async function doEval(): Promise<void> {
|
||||
if (!expr.value.trim()) {
|
||||
Message.warning("请填写表达式");
|
||||
return;
|
||||
}
|
||||
evaluating.value = true;
|
||||
result.value = null;
|
||||
try {
|
||||
result.value = (await flowApi.routerEval({
|
||||
expression: expr.value.trim(),
|
||||
variablesJson: varsJson.value.trim(),
|
||||
}).send()) || null;
|
||||
} finally {
|
||||
evaluating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
mode.value = "instance";
|
||||
expr.value = props.presetExpression || "";
|
||||
varsJson.value = "";
|
||||
varsParseError.value = false;
|
||||
pickedId.value = null;
|
||||
bizTypeFilter.value = "";
|
||||
traceRows.value = [];
|
||||
traceInstanceId.value = null;
|
||||
result.value = null;
|
||||
void loadInstances();
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (v) => {
|
||||
if (v) reset();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :model-value="modelValue" title="表达式调试" :mask-closable="false" :width="700"
|
||||
@update:model-value="(v: boolean) => emit('update:modelValue', v)">
|
||||
<p class="em-note">
|
||||
与 CONDITION 节点 EXPRESSION 模式同一套求值(AviatorScript 沙箱)。变量 = 样例 JSON 的键 +
|
||||
线上另注入 schId/bizType/bizId/submitterId/instanceId/instanceVersion/nodeId/nodeName
|
||||
(内置变量条仅展示,不参与求值,需在表达式里用到时请自行写入样例 JSON)。
|
||||
变量未提供会报错——线上同口径(宁挂起不误放行,不按 nil 求值)。
|
||||
</p>
|
||||
|
||||
<RadioGroup v-model="mode" type="button" button-style="solid" class="em-seg">
|
||||
<Radio label="instance">从办件导入</Radio>
|
||||
<Radio label="routelog">从路由日志导入</Radio>
|
||||
<Radio label="manual">手动编写</Radio>
|
||||
</RadioGroup>
|
||||
|
||||
<div class="em-fld">
|
||||
<label>表达式<span class="em-sub">AviatorScript 沙箱 · 与运行时同一求值口径</span></label>
|
||||
<Input ref="exprBox" v-model="expr" class="em-mono" placeholder="表达式,如 days >= 3" :maxlength="200" />
|
||||
<div v-if="chips.length" class="em-chips">
|
||||
<span v-for="k in chips" :key="k" class="em-chip" :class="{ bt: BUILTIN_SET.has(k) }"
|
||||
@click="insertChip(k)">{{ k }}</span>
|
||||
</div>
|
||||
<div v-if="chips.length" class="em-sub">↑ 可用变量(点击插入表达式光标处;灰色=内置变量,由运行时自动注入)</div>
|
||||
</div>
|
||||
|
||||
<!-- 模式1/2 共用办件选择器;路由日志模式选中即拉该办件 trace -->
|
||||
<div v-if="mode !== 'manual'" class="em-fld">
|
||||
<label>选择办件
|
||||
<span class="em-sub">变量 = 该办件 formData 原样 + 内置变量;选完自动填充,可再手工改</span>
|
||||
</label>
|
||||
<Select v-model="pickedId" filterable clearable :loading="instLoading"
|
||||
placeholder="按标题/业务键搜索最近 50 条办件" @on-change="fillFromInstance">
|
||||
<Option v-for="r in shownInstances" :key="r.id" :value="r.id"
|
||||
:label="`#${r.id} ${r.bizTitle || r.bizId || ''}`">
|
||||
#{{ r.id }} · {{ r.bizType || "-" }}/{{ r.bizId || "-" }} · {{ r.bizTitle || "(无标题)" }}
|
||||
· {{ r.submitterName || r.submitterId || "-" }} ·
|
||||
{{ (r.submitTime || "").replace("T", " ").slice(0, 16) || "-" }}
|
||||
<Tag :color="INSTANCE_STATUS_COLOR[r.status] || 'gray'" style="margin-left: 6px">
|
||||
{{ INSTANCE_STATUS[r.status] || r.status }}
|
||||
</Tag>
|
||||
</Option>
|
||||
</Select>
|
||||
<div v-if="bizTypes.length" class="em-chips">
|
||||
<span class="em-chip flt" :class="{ on: !bizTypeFilter }" @click="bizTypeFilter = ''">全部类型</span>
|
||||
<span v-for="t in bizTypes" :key="t" class="em-chip flt" :class="{ on: bizTypeFilter === t }"
|
||||
@click="bizTypeFilter = t">{{ t }}</span>
|
||||
</div>
|
||||
<div v-if="picked" class="em-bi">
|
||||
<span class="em-bi-t">内置变量(线上由引擎注入,只读展示):</span>
|
||||
<span v-for="b in builtinRows" :key="b.k" class="em-bi-i">
|
||||
{{ b.k }}={{ b.todo ? "(按调试目标自填)" : b.v }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="mode === 'routelog'" class="em-fld">
|
||||
<div v-if="traceLoading" class="em-sub">正在读取该办件的路由判断留痕…</div>
|
||||
<template v-else-if="traceRows && traceRows.length">
|
||||
<Table :columns="[
|
||||
{ title: '节点名', key: 'nodeName', width: 140 },
|
||||
{ title: '注册键', key: 'routerKey', width: 150 },
|
||||
{ title: '命中出口', width: 120, render: (h, p) => outletText(p.row) },
|
||||
{ title: 'attempt', key: 'attempt', width: 80 },
|
||||
{ title: '时间', width: 140, render: (h, p) => (p.row.time || '').replace('T', ' ').slice(0, 19) || '-' },
|
||||
]" :data="traceRows" size="small" :max-height="180" no-data-text="暂无数据" />
|
||||
<div v-for="(r, i) in traceRows" :key="`raw${i}`" class="em-raw">
|
||||
<span class="em-sub">{{ r.nodeName }} rawResponse({{ (r.rawResponse || "").length }} 字):</span>
|
||||
<pre :title="r.rawResponse || ''">{{ (r.rawResponse || "(无留痕)").slice(0, 200)
|
||||
}}{{ (r.rawResponse || "").length > 200 ? "…" : "" }}</pre>
|
||||
</div>
|
||||
<Button size="small" :loading="evaluating" @click="replayFromLog">载入该办件变量并复现</Button>
|
||||
<div class="em-sub">复现后可改表达式定位问题,再到办件台账对该办件重跑路由。</div>
|
||||
</template>
|
||||
<div v-else class="em-sub">
|
||||
{{ picked ? "该办件无条件判断记录" : "选择办件后展示其 flw_route_log 逐边留痕" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="em-fld">
|
||||
<label>样例变量 JSON(formData)
|
||||
<span v-if="varsParseError" style="color: #ed4014">该办件 formData 不是合法 JSON,已填原文,请手工修正</span>
|
||||
<span v-else-if="mode === 'manual'" class="em-sub">粘贴应用实际传递的 formData JSON;不知道传什么?切「从办件导入」取真实变量</span>
|
||||
</label>
|
||||
<Input v-model="varsJson" type="textarea" :rows="8" :class="{ 'em-bad': varsParseError }"
|
||||
placeholder='如 {"days": 5, "bizType": "leave"}' />
|
||||
</div>
|
||||
|
||||
<hr class="em-divider">
|
||||
<div class="em-fld">
|
||||
<label>求值结果</label>
|
||||
<div v-if="!result" class="em-sub">点击「求值」查看结果(与 CONDITION 节点 EXPRESSION 模式同一求值口径)。</div>
|
||||
<template v-else-if="result.error">
|
||||
<div class="em-err">✗ 求值失败:{{ result.error }}</div>
|
||||
<div class="em-sub">宁挂起不误放行——变量缺失/类型不符都会在此暴露,线上同口径挂起而非按 nil 放行</div>
|
||||
</template>
|
||||
<template v-else-if="result.result">
|
||||
<div class="em-ok">✓ true(命中该出口)
|
||||
<span v-if="result.value !== undefined" class="em-sub">求值值:{{ result.value }}</span>
|
||||
</div>
|
||||
<div class="em-sub">线上行为:条件节点按出口顺序取首个为真者走该分支</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="em-warn">✗ false(不命中)
|
||||
<span v-if="result.value !== undefined" class="em-sub">求值值:{{ result.value }}</span>
|
||||
</div>
|
||||
<div class="em-sub">线上行为:该出口不走;全部出口不命中将挂起等管理端重跑路由</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="em-hint">变量缺失即报错——线上同口径(宁挂起不误放行,不按 nil 求值)</span>
|
||||
<Button @click="emit('update:modelValue', false)">关闭</Button>
|
||||
<Button type="primary" :loading="evaluating" @click="doEval">求值</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.em-note {
|
||||
color: #808695;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.em-seg {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.em-fld {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.em-fld > label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #515a6e;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.em-sub {
|
||||
color: #a5abb8;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.em-fld > label .em-sub {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.em-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
.em-chip {
|
||||
border: 1px solid #e3e6eb;
|
||||
border-radius: 12px;
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
color: #0b7d72;
|
||||
background: #f2fbf9;
|
||||
cursor: pointer;
|
||||
}
|
||||
.em-chip.flt {
|
||||
color: #515a6e;
|
||||
background: #fff;
|
||||
}
|
||||
/* 内置变量 chip:与 formData 键区分(对齐原型 .chip.bt 口径) */
|
||||
.em-chip.bt {
|
||||
border-color: #dcdee2;
|
||||
color: #515a6e;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
.em-chip.bt:hover {
|
||||
color: #fff;
|
||||
background: #515a6e;
|
||||
border-color: #515a6e;
|
||||
}
|
||||
.em-chip.flt.on {
|
||||
border-color: var(--brand);
|
||||
color: var(--brand);
|
||||
}
|
||||
.em-bi {
|
||||
margin-top: 8px;
|
||||
background: #f7f9fc;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.em-bi-t {
|
||||
color: #0b7d72;
|
||||
font-weight: 600;
|
||||
}
|
||||
.em-bi-i {
|
||||
margin-right: 10px;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
color: #515a6e;
|
||||
}
|
||||
.em-mono :deep(input) {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
}
|
||||
.em-bad :deep(.ivu-input) {
|
||||
border-color: #ed4014;
|
||||
}
|
||||
.em-raw {
|
||||
margin: 6px 0;
|
||||
}
|
||||
.em-raw pre {
|
||||
margin: 2px 0 0;
|
||||
padding: 6px 8px;
|
||||
background: #fafbfd;
|
||||
border: 1px solid #eef0f3;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.em-divider {
|
||||
border: none;
|
||||
border-top: 1px solid #eef0f3;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.em-ok {
|
||||
color: #19be6b;
|
||||
font-size: 13px;
|
||||
}
|
||||
.em-warn {
|
||||
color: #ff9900;
|
||||
font-size: 13px;
|
||||
}
|
||||
.em-err {
|
||||
color: #ed4014;
|
||||
font-size: 13px;
|
||||
}
|
||||
.em-hint {
|
||||
float: left;
|
||||
color: #b0b6c1;
|
||||
font-size: 12px;
|
||||
line-height: 32px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user