Compare commits
3
Commits
859210bf07
...
1369b103ee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1369b103ee | ||
|
|
0542279636 | ||
|
|
fbd428b913 |
Binary file not shown.
|
Before Width: | Height: | Size: 186 KiB |
@@ -252,6 +252,10 @@ class FlowApi {
|
||||
callbackRepush(ids: number[]) {
|
||||
return post(`${ROOT}/admin/callback/repush`, ids);
|
||||
}
|
||||
/** 对接健康度体检(2026-10-15 P2):每应用一行六项检查 + 综合评分,零新增表 */
|
||||
callbackHealth() {
|
||||
return get(`${ROOT}/admin/callback/health`);
|
||||
}
|
||||
|
||||
// ==================== 管理面:条件路由注册 / 重跑 ====================
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 186 KiB |
@@ -98,17 +98,25 @@ h4.subtitle {
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格加载(规范 §4 表格加载行):图形=通道 .ysch-spin(随 --brand),本地零图形资产 */
|
||||
.table-spin-load {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 90px;
|
||||
padding-top: 60px;
|
||||
background-image: url(../image/loading.gif);
|
||||
background-size: 60px 60px;
|
||||
background-position: top center;
|
||||
background-repeat: no-repeat;
|
||||
border: none;
|
||||
color: var(--brand);
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
.table-spin-load .ysch-spin {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-left: -30px;
|
||||
font-size: 60px; /* .ysch-spin 尺寸=1em,60px 图形用字号给 */
|
||||
}
|
||||
|
||||
.ivu-table-cell-tree {
|
||||
|
||||
@@ -116,7 +116,7 @@ defineExpose({
|
||||
no-data-text="暂无数据"
|
||||
>
|
||||
<template #loading>
|
||||
<div class="table-spin-load">加载中</div>
|
||||
<div class="table-spin-load"><i class="ysch-spin"></i>加载中</div>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="page-panel">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Table :columns="columns" :data="list" :height="tableH" :loading="loading" no-data-text="暂无数据"
|
||||
class="link-table table-bottom-box" @on-selection-change="selectChange">
|
||||
<template #loading>
|
||||
<div class="table-spin-load">加载中</div>
|
||||
<div class="table-spin-load"><i class="ysch-spin"></i>加载中</div>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Table :columns="columns" :data="list" :height="tableH" :loading="loading" no-data-text="暂无数据"
|
||||
class="link-table table-bottom-box" @on-selection-change="selectChange">
|
||||
<template #loading>
|
||||
<div class="table-spin-load">加载中</div>
|
||||
<div class="table-spin-load"><i class="ysch-spin"></i>加载中</div>
|
||||
</template>
|
||||
<template v-for="slotName in tableSlotNames" :key="slotName" v-slot:[slotName]="slotProps">
|
||||
<slot :name="slotName" v-bind="slotProps" />
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
// 应用设置(2026-10-10 四议题):tab1 应用管理(flw_app 白名单 + 学校池同步 + 开关);
|
||||
// tab2 科室管理(flw_dept 组+成员模型,取代旧「办理人映射」页——deptKey=本地自增 id,
|
||||
// 切源后旧流程定义引用的 UserBase 组 id 需重建科室后重新配置)。
|
||||
import { ref, onMounted } from "vue";
|
||||
import { Message, Tag } from "view-ui-plus";
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Message, Tag, Button } from "view-ui-plus";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||||
import { flowApi } from "@/api/module/flow";
|
||||
@@ -11,6 +12,7 @@ import { actionColumn } from "@/utils/tableActions";
|
||||
import { useElementHeight } from "@/utils/hook/useElementHeight";
|
||||
|
||||
const tab = ref("apps");
|
||||
const router = useRouter();
|
||||
|
||||
// ==================== tab1 应用管理 ====================
|
||||
const loading = ref(false);
|
||||
@@ -343,6 +345,101 @@ onMounted(() => {
|
||||
load();
|
||||
loadDepts();
|
||||
});
|
||||
|
||||
// ==================== tab3 对接健康度(2026-10-15 议题二 P2) ====================
|
||||
// 每应用一行六项检查 + 综合评分;数据全部来自已有接口(/callback/health 聚合),零新增表。
|
||||
// 应用无注册行 = 未接入(由接入向导负责),不参与体检。
|
||||
const healthLoading = ref(false);
|
||||
const healthRows = ref<any[]>([]);
|
||||
|
||||
const healthColumns = [
|
||||
{
|
||||
title: "应用",
|
||||
key: "appName",
|
||||
minWidth: 180,
|
||||
render: (h: any, p: any) =>
|
||||
h("div", { class: "hc-app" }, [
|
||||
h("span", { class: "hc-app-n" }, p.row.appName),
|
||||
h("span", { class: "hc-app-k" }, p.row.appKey),
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: "评分",
|
||||
width: 96,
|
||||
align: "center",
|
||||
render: (h: any, p: any) =>
|
||||
h("span", { class: ["hc-score", "is-" + p.row.level] }, String(p.row.score)),
|
||||
},
|
||||
{
|
||||
title: "结论",
|
||||
width: 90,
|
||||
align: "center",
|
||||
render: (h: any, p: any) => {
|
||||
const map: Record<string, [string, string]> = {
|
||||
ok: ["正常", "green"],
|
||||
warn: ["关注", "orange"],
|
||||
bad: ["异常", "red"],
|
||||
};
|
||||
const [txt, color] = map[p.row.level] || ["未知", "default"];
|
||||
return h(Tag, { color }, () => txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "检查项",
|
||||
minWidth: 460,
|
||||
render: (h: any, p: any) =>
|
||||
h("div", { class: "hc-checks" }, (p.row.checks || []).map((c: any) =>
|
||||
h("span", {
|
||||
class: ["hc-chip", "is-" + c.level],
|
||||
attrs: { title: (c.advice ? c.advice + "\n" : "") + "实测:" + (c.value || "-") },
|
||||
}, [
|
||||
h("i", { class: ["hc-dot", "is-" + c.level] }),
|
||||
h("span", { class: "hc-chip-n" }, c.name),
|
||||
c.value ? h("em", { class: "hc-chip-v" }, c.value) : null,
|
||||
]))),
|
||||
},
|
||||
{
|
||||
title: "结论说明",
|
||||
key: "summary",
|
||||
minWidth: 220,
|
||||
render: (h: any, p: any) => h("span", { class: "hc-sum" }, p.row.summary || "-"),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 110,
|
||||
align: "center",
|
||||
render: (h: any, p: any) =>
|
||||
h(Button, {
|
||||
props: { type: "text", size: "small" },
|
||||
on: { click: () => onViewLogs(p.row) },
|
||||
}, () => "查推送日志"),
|
||||
},
|
||||
];
|
||||
|
||||
function onViewLogs(row: any) {
|
||||
router.push({ path: "/admin/callbacks", query: { appKey: row.appKey } });
|
||||
}
|
||||
|
||||
async function loadHealth() {
|
||||
healthLoading.value = true;
|
||||
try {
|
||||
healthRows.value = ((await (flowApi as any).callbackHealth().send()) as any[]) || [];
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "健康度加载失败");
|
||||
healthRows.value = [];
|
||||
} finally {
|
||||
healthLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 首次切到「对接健康度」时按需加载(避免进页面就多打一次聚合查询)
|
||||
let healthLoaded = false;
|
||||
watch(tab, (t) => {
|
||||
if (t === "health" && !healthLoaded) {
|
||||
healthLoaded = true;
|
||||
loadHealth();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -351,6 +448,7 @@ onMounted(() => {
|
||||
<Tabs class="tabs-panel" v-model="tab">
|
||||
<TabPane label="应用管理" name="apps"></TabPane>
|
||||
<TabPane label="科室管理" name="depts"></TabPane>
|
||||
<TabPane label="对接健康度" name="health"></TabPane>
|
||||
</Tabs>
|
||||
|
||||
<!-- 应用管理 -->
|
||||
@@ -385,6 +483,21 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对接健康度(2026-10-15 P2):每应用一行六项检查,问题应用置顶 -->
|
||||
<div class="tab-body" v-show="tab === 'health'">
|
||||
<div class="fix mb-12">
|
||||
<Button class="fl" type="primary" size="large" @click="loadHealth">重新体检</Button>
|
||||
<span class="fl" style="color: #808695; font-size: 12px; line-height: 40px; margin-left: 10px">
|
||||
一屏定位「为什么收不到回调」:登记地址形态、base_path、推送成功率、挂起/死信、注册启用态。
|
||||
数据来自回调注册与推送日志,无注册行的应用视为「未接入」,请走接入向导
|
||||
</span>
|
||||
</div>
|
||||
<div class="table-region">
|
||||
<Table :columns="healthColumns" :data="healthRows" :loading="healthLoading"
|
||||
class="link-table table-bottom-box" no-data-text="暂无可体检应用(尚无任何回调注册,请先走接入向导)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal v-model="createVisible" title="新建科室" :mask-closable="false">
|
||||
<Input v-model="createName" placeholder="科室名称,如 人事处" :maxlength="30" />
|
||||
<template #footer>
|
||||
@@ -432,3 +545,27 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 对接健康度面板:一屏一行,检查项紧凑成 chip */
|
||||
.hc-app { display: flex; flex-direction: column; line-height: 1.35; }
|
||||
.hc-app-n { color: #17233d; font-weight: 500; }
|
||||
.hc-app-k { color: #98a1b3; font-size: 12px; }
|
||||
|
||||
.hc-score { display: inline-block; min-width: 34px; padding: 1px 6px; border-radius: 4px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
.hc-score.is-ok { background: #e8f7ee; color: #19a35a; }
|
||||
.hc-score.is-warn { background: #fff5e6; color: #d98106; }
|
||||
.hc-score.is-bad { background: #fdecec; color: #e03e3e; }
|
||||
|
||||
.hc-checks { display: flex; flex-wrap: wrap; gap: 6px; padding: 2px 0; }
|
||||
.hc-chip { display: inline-flex; align-items: center; gap: 4px; max-width: 100%; padding: 1px 7px; border-radius: 3px; font-size: 12px; line-height: 18px; background: #f4f6f9; color: #515a6e; }
|
||||
.hc-chip.is-warn { background: #fff5e6; color: #b26a00; }
|
||||
.hc-chip.is-bad { background: #fdecec; color: #c0392b; }
|
||||
.hc-dot { width: 6px; height: 6px; border-radius: 50%; background: #19a35a; flex: none; }
|
||||
.hc-dot.is-warn { background: #f0a020; }
|
||||
.hc-dot.is-bad { background: #e03e3e; }
|
||||
.hc-chip-n { flex: none; }
|
||||
.hc-chip-v { font-style: normal; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: 0.85; }
|
||||
|
||||
.hc-sum { color: #515a6e; font-size: 12px; }
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 回调管理(概览 §9.2):注册列表/启停/secret 重置 + 推送日志 + 挂起(持续重试可视/标记死信)+ 死信重推。
|
||||
// 规范骨架(bsch应用页规范 §1):tabs-wrapper + Tabs type=card,Tab 切换只 v-show 内容区;「重推选中」落在分页栏左侧批量操作位
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { Message, Tag } from "view-ui-plus";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
|
||||
@@ -15,6 +16,7 @@ const EVENT_OPTIONS = ["flow.submitted", "flow.task.created", "flow.task.handled
|
||||
|
||||
const tab = ref("regs");
|
||||
const loading = ref(false);
|
||||
const route = useRoute();
|
||||
|
||||
// 无分页 tab 的表格高度:实测各自 .table-region(flex:1),替代 useTableHeight 常量减法
|
||||
const regsRegionRef = ref<HTMLElement | null>(null);
|
||||
@@ -24,6 +26,13 @@ const hungH = useElementHeight(hungRegionRef);
|
||||
|
||||
// ---- 注册列表 ----
|
||||
const regs = ref<any[]>([]);
|
||||
// appKey 筛选(2026-10-15):健康度面板「查推送日志」跳转带入 query.appKey 预填
|
||||
const regFilter = ref("");
|
||||
const filteredRegs = () => {
|
||||
const k = regFilter.value.trim().toLowerCase();
|
||||
if (!k) return regs.value;
|
||||
return regs.value.filter((r: any) => String(r.appKey || "").toLowerCase().includes(k));
|
||||
};
|
||||
const regColumns = [
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ title: "应用 appKey", key: "appKey", width: 110 },
|
||||
@@ -208,7 +217,15 @@ function switchTab(name: string) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadRegs);
|
||||
onMounted(() => {
|
||||
// 从健康度面板跳转时带 appKey,预填注册列表筛选并切到「注册列表」tab
|
||||
const q = route.query.appKey;
|
||||
if (q) {
|
||||
regFilter.value = String(q);
|
||||
tab.value = "regs";
|
||||
}
|
||||
loadRegs();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -222,8 +239,15 @@ onMounted(loadRegs);
|
||||
|
||||
<!-- 回调注册:无分页 -->
|
||||
<div class="tab-body" v-show="tab === 'regs'">
|
||||
<div class="fix mb-12">
|
||||
<Input class="fl" v-model="regFilter" placeholder="按 appKey 筛选" clearable
|
||||
style="width: 220px" suffix="ios-search" />
|
||||
<span class="fl" style="color: #808695; font-size: 12px; line-height: 32px; margin-left: 10px">
|
||||
每行一个 (应用 × 流程) 登记;「探测」同步发一条 flow.ping 验证可达与验签
|
||||
</span>
|
||||
</div>
|
||||
<div class="table-region" ref="regsRegionRef">
|
||||
<Table :columns="regColumns" :data="regs" :loading="loading" class="link-table table-bottom-box"
|
||||
<Table :columns="regColumns" :data="filteredRegs()" :loading="loading" class="link-table table-bottom-box"
|
||||
:height="regsH || undefined" no-data-text="暂无数据" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,16 @@
|
||||
// 分支按出口名互斥选路(运行时调判断服务/表达式求值),分支尾隐式汇合到容器后继。
|
||||
// 分支头部的出口名 Input 即 contentJson 里 CONDITION 命名出边的 name(判断接口 outlets 契约);
|
||||
// EXPRESSION 模式(2026-10-10)下分支头部再就地编辑出边 condition,序号即求值优先级。
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { BRANCH_LABELS, EDITOR_CTX, MAX_BRANCHES, MIN_BRANCHES, ConditionNode } from "../model";
|
||||
import { computed, inject, nextTick, ref } from "vue";
|
||||
import {
|
||||
BRANCH_LABELS,
|
||||
EDITOR_CTX,
|
||||
EXPR_BUILTIN_SET,
|
||||
ConditionNode,
|
||||
MAX_BRANCHES,
|
||||
MIN_BRANCHES,
|
||||
extractExprVars,
|
||||
} from "../model";
|
||||
import NodeList from "./NodeList.vue";
|
||||
import ExprEvalModal from "@/views/flow/components/ExprEvalModal.vue";
|
||||
|
||||
@@ -45,6 +53,38 @@ function openTry(i: number): void {
|
||||
tryExpr.value = (props.node.conditions || [])[i] || "";
|
||||
tryVisible.value = true;
|
||||
}
|
||||
|
||||
// ==================== 可用变量 chips(2026-10-14 议题一 P0):内联提示 + 点击插入 ====================
|
||||
/** 可用变量(表单字段聚合键 + 内置),来自编辑器上下文;未拉到为空数组则不展示 chips */
|
||||
const availableVars = computed(() => ctx.availableVars || []);
|
||||
const isBuiltin = (k: string): boolean => EXPR_BUILTIN_SET.has(k);
|
||||
|
||||
const condInputs = ref<Record<number, HTMLInputElement | null>>({});
|
||||
function setCondInput(el: any, i: number): void {
|
||||
condInputs.value[i] = (el as HTMLInputElement) || null;
|
||||
}
|
||||
|
||||
/** chips 点击:插入到该出口表达式光标处(无输入框时追加到末尾) */
|
||||
function insertChip(i: number, key: string): void {
|
||||
const cur = (props.node.conditions || [])[i] || "";
|
||||
const el = condInputs.value[i];
|
||||
const s = el?.selectionStart ?? cur.length;
|
||||
const e = el?.selectionEnd ?? cur.length;
|
||||
editCondition(i, cur.slice(0, s) + key + cur.slice(e));
|
||||
nextTick(() => {
|
||||
const input = condInputs.value[i];
|
||||
if (input) {
|
||||
input.focus();
|
||||
input.setSelectionRange(s + key.length, s + key.length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 该出口表达式里不在可用列表中的标识符(疑似拼错提示,仅提示不拦截) */
|
||||
function unknownVars(i: number): string[] {
|
||||
const known = new Set<string>([...availableVars.value, ...EXPR_BUILTIN_SET]);
|
||||
return extractExprVars((props.node.conditions || [])[i] || "").filter((v) => !known.has(v));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -81,6 +121,7 @@ function openTry(i: number): void {
|
||||
</div>
|
||||
<div v-if="expression" class="cb-cond-row">
|
||||
<input
|
||||
:ref="(el) => setCondInput(el, i)"
|
||||
class="cb-cond"
|
||||
:value="(node.conditions || [])[i] || ''"
|
||||
placeholder="条件表达式,如 amount > 1000"
|
||||
@@ -90,6 +131,23 @@ function openTry(i: number): void {
|
||||
<!-- B5 内联试算:用真实办件变量跑该出口表达式(同「条件路由」页弹窗,预填表达式) -->
|
||||
<span class="cb-try" title="用真实办件变量试算该表达式" @click.stop="openTry(i)">试算</span>
|
||||
</div>
|
||||
<!-- 议题一 P0:可用变量 chips(表单键 + 内置),点击插入到表达式光标处 -->
|
||||
<div v-if="expression && availableVars.length" class="cb-vars">
|
||||
<span
|
||||
v-for="k in availableVars.slice(0, 14)"
|
||||
:key="k"
|
||||
class="cb-var"
|
||||
:class="{ bt: isBuiltin(k) }"
|
||||
:title="isBuiltin(k) ? '内置变量(引擎运行时注入)' : '表单变量(来自近期办件 formData)'"
|
||||
@click.stop="insertChip(i, k)"
|
||||
>{{ k }}</span>
|
||||
<span v-if="availableVars.length > 14" class="cb-vars-more" :title="availableVars.join('、')">
|
||||
+{{ availableVars.length - 14 }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-for="(v, vi) in unknownVars(i)" :key="'u' + vi" class="cb-unknown">
|
||||
⚠ 「{{ v }}」不在可用变量列表,若为动态变量可忽略
|
||||
</div>
|
||||
<NodeList :items="branch" :container="`${node.uid}:${i}`" />
|
||||
<div v-if="!branch.length" class="cb-empty">该出口直达汇合点(直接通过)</div>
|
||||
</div>
|
||||
@@ -231,6 +289,52 @@ function openTry(i: number): void {
|
||||
background: #f2fbf9;
|
||||
border-color: #0b7d72;
|
||||
}
|
||||
.cb-vars {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin: -2px 0 8px;
|
||||
}
|
||||
.cb-var {
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #b7e3dc;
|
||||
background: #f2fbf9;
|
||||
color: #0b7d72;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
}
|
||||
.cb-var:hover {
|
||||
background: #0b7d72;
|
||||
color: #fff;
|
||||
border-color: #0b7d72;
|
||||
}
|
||||
.cb-var.bt {
|
||||
border-color: #dcdee2;
|
||||
background: #f5f7fa;
|
||||
color: #4e5969;
|
||||
}
|
||||
.cb-var.bt:hover {
|
||||
background: #4e5969;
|
||||
color: #fff;
|
||||
border-color: #4e5969;
|
||||
}
|
||||
.cb-vars-more {
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
padding: 0 8px;
|
||||
color: #86909c;
|
||||
cursor: default;
|
||||
}
|
||||
.cb-unknown {
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: #ff9900;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.cb-col-del {
|
||||
flex: none;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -114,13 +114,20 @@ function onRemove(): void {
|
||||
</div>
|
||||
|
||||
<div class="fld">
|
||||
<label>判断方式</label>
|
||||
<label>判断依据数据什么时候能拿到</label>
|
||||
<RadioGroup :model-value="form.judgeMode" @on-change="switchJudgeMode">
|
||||
<Radio label="SERVICE">服务(外部判断接口)</Radio>
|
||||
<Radio label="EXPRESSION">表达式</Radio>
|
||||
<Radio label="EXPRESSION">
|
||||
<span class="rd-t">提交时就已确定</span>
|
||||
<span class="rd-d">用表达式判断(推荐)。变量随单据一起提交,编辑器内闭环,零对接。</span>
|
||||
</Radio>
|
||||
<Radio label="SERVICE">
|
||||
<span class="rd-t">到达该节点时才能确定</span>
|
||||
<span class="rd-d">需实时查库 / 问外部系统时使用。由子应用的判断服务返回出口。</span>
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
<div class="hint">
|
||||
简单条件用表达式在编辑器内闭环;复杂判断(查库/调外部系统)沿用注册的判断服务
|
||||
原措辞为「EXPRESSION / SERVICE」——引擎实现细节,业务管理员难以判断,选错即返工;
|
||||
改按「数据何时可知」提问,配置者无需理解引擎实现
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -193,6 +200,19 @@ function onRemove(): void {
|
||||
.hint.warn {
|
||||
color: #ff9900;
|
||||
}
|
||||
/* 判断方式业务语言化(2026-10-15):标签 + 说明两行式,配置者按"数据何时可知"作答 */
|
||||
.rd-t {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
}
|
||||
.rd-d {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
line-height: 1.5;
|
||||
margin-top: 2px;
|
||||
white-space: normal;
|
||||
}
|
||||
.dw-foot {
|
||||
position: sticky;
|
||||
bottom: -16px;
|
||||
|
||||
@@ -69,8 +69,10 @@ const depts = reactive<DeptOption[]>([]);
|
||||
const userNames = reactive<Record<number, string>>({});
|
||||
/** 条件分支的判断服务注册(flw_router;拉取失败为空数组,校验降级提示) */
|
||||
const routers = reactive<RouterOption[]>([]);
|
||||
/** 可用变量 chips:近期办件 formData 顶层键聚合(去重截 40)+ 内置变量;拉取失败仅内置 */
|
||||
const availableVars = reactive<string[]>([]);
|
||||
|
||||
const issues = computed<Issue[]>(() => validateModel(model, depts, routers));
|
||||
const issues = computed<Issue[]>(() => validateModel(model, depts, routers, availableVars));
|
||||
const defJson = computed(() => toContentJson(model));
|
||||
|
||||
/** 结构统计(右面板分区4):walkItems 已递归条件/并行分支的子树 */
|
||||
@@ -201,6 +203,27 @@ async function loadRouters(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 表达式可用变量:近期 50 条办件 formData 顶层键聚合(去重截 40)+ 内置,口径同调试弹窗;失败仅内置 */
|
||||
const BUILTIN_VARS = ["schId", "instanceId", "instanceVersion", "bizType", "bizId", "nodeId", "nodeName", "submitterId"];
|
||||
async function loadVars(): Promise<void> {
|
||||
const keys = new Set<string>();
|
||||
try {
|
||||
const rows: any[] = (await flowApi.adminInstances({ page: 1, size: 50 }).send())?.content || [];
|
||||
rows.forEach((r) => {
|
||||
try {
|
||||
const v = JSON.parse(r.formDataJson || "{}");
|
||||
if (v && typeof v === "object" && !Array.isArray(v)) Object.keys(v).forEach((k) => keys.add(k));
|
||||
} catch {
|
||||
/* 脏 formData 跳过,不影响其它办件的键聚合 */
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
/* 办件列表拉取失败:退化为仅内置变量,不阻塞编辑 */
|
||||
}
|
||||
const merged = Array.from(new Set([...Array.from(keys).slice(0, 40), ...BUILTIN_VARS]));
|
||||
availableVars.splice(0, availableVars.length, ...merged);
|
||||
}
|
||||
|
||||
function applyParsed(parsed: ParsedDef): void {
|
||||
model.code = parsed.code || model.code;
|
||||
model.name = parsed.name || model.name;
|
||||
@@ -217,6 +240,7 @@ async function load(): Promise<void> {
|
||||
try {
|
||||
await loadDepts();
|
||||
await loadRouters();
|
||||
void loadVars();
|
||||
try {
|
||||
const rows: any[] = (await flowApi.appList().send()) || [];
|
||||
appOptions.value = rows
|
||||
@@ -385,6 +409,7 @@ provide<EditorCtx>(EDITOR_CTX, {
|
||||
depts,
|
||||
userNames,
|
||||
routers,
|
||||
availableVars,
|
||||
issueOf,
|
||||
openNode,
|
||||
removeNode,
|
||||
@@ -551,6 +576,11 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
<Tag v-else color="green">新建</Tag>
|
||||
</div>
|
||||
<div class="hd-right">
|
||||
<!-- 快捷入口上移顶栏(2026-10-15 痛点 7):右栏「快捷入口」区删除,动作类入口统一收敛到顶栏 -->
|
||||
<Button size="small" :disabled="!editId" @click="onOpenDiagram">图示预览</Button>
|
||||
<Button size="small" @click="jsonDrawer = true">源码</Button>
|
||||
<Button size="small" @click="settingsDrawer = true">流程设置</Button>
|
||||
<span class="hd-divider" />
|
||||
<Button type="primary" :loading="saving" @click="onSave">保存草稿</Button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -635,16 +665,6 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
<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>
|
||||
|
||||
@@ -713,10 +733,18 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
}
|
||||
.hd-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
margin-left: auto;
|
||||
}
|
||||
/* 顶栏工具栏分组竖线(2026-10-15 痛点 7):左侧「查看/配置类」与右侧「保存草稿」主操作拉开层级 */
|
||||
.hd-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: #e5e6eb;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.ed-canvas {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -942,11 +970,6 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
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;
|
||||
|
||||
@@ -160,6 +160,60 @@ export const BRANCH_LABELS = ["①", "②", "③", "④", "⑤", "⑥"];
|
||||
|
||||
export const DEFAULT_URGE: UrgeSetting = { enabled: true, intervalHours: 24, maxTimes: 3 };
|
||||
|
||||
// ==================== 表达式变量契约(2026-10-15 抽共享,消除三处漂移) ====================
|
||||
/**
|
||||
* 表达式内置变量白名单。
|
||||
* 唯一权威来源是后端 `ConditionRouteService.builtinEnv`(运行时注入的同名集)。
|
||||
* 前端两处消费者(ConditionBox 的 chips、validate 的未知变量比对)必须共用此常量,
|
||||
* 改动时需同步后端 `DefValidator.EXPRESSION_BUILTINS` 与 `ConditionRouteService.builtinEnv`。
|
||||
*/
|
||||
export const EXPR_BUILTIN_VARS = [
|
||||
"schId",
|
||||
"instanceId",
|
||||
"instanceVersion",
|
||||
"bizType",
|
||||
"bizId",
|
||||
"nodeId",
|
||||
"nodeName",
|
||||
"submitterId",
|
||||
] as const;
|
||||
|
||||
/** 内置变量快速判定集合 */
|
||||
export const EXPR_BUILTIN_SET: ReadonlySet<string> = new Set<string>(EXPR_BUILTIN_VARS);
|
||||
|
||||
/**
|
||||
* AviatorScript 关键字/字面量:不参与"未知变量"提示。
|
||||
* 与后端表达式语法(AviatorScript)对齐,仅用于静态提取时的剔除。
|
||||
*/
|
||||
export const EXPR_KEYWORDS: ReadonlySet<string> = new Set([
|
||||
"true", "false", "nil", "null", "and", "or", "not", "if", "else", "elsif", "let", "while",
|
||||
"for", "return", "in", "like", "new", "end", "break", "continue", "seq", "lambda",
|
||||
]);
|
||||
|
||||
/**
|
||||
* 从表达式里静态提取"疑似变量名"(剔除字符串字面量、函数名、属性访问、关键字)。
|
||||
* 与后端 `ExpressionEvaluator.variableNames()` 同口径的前端简化实现:
|
||||
* 后端解析 AST 更精确,前端只做词法级粗筛——因此结果**只用于 warn 提示,绝不用于阻断**。
|
||||
*/
|
||||
export function extractExprVars(expression: string): string[] {
|
||||
const cond = (expression || "").trim();
|
||||
if (!cond) return [];
|
||||
const stripped = cond
|
||||
.replace(/'[^']*'|"[^"]*"/g, " ") // 字符串字面量
|
||||
.replace(/[A-Za-z_][A-Za-z0-9_]*\s*\(/g, " ") // 函数名
|
||||
.replace(/\.\s*[A-Za-z_][A-Za-z0-9_]*/g, " "); // 命名空间/属性访问
|
||||
const out = new Set<string>();
|
||||
(stripped.match(/[A-Za-z_][A-Za-z0-9_]*/g) || []).forEach((t) => {
|
||||
if (!EXPR_KEYWORDS.has(t)) out.add(t);
|
||||
});
|
||||
return Array.from(out);
|
||||
}
|
||||
|
||||
/** 表达式中不在「已知变量集合」里的标识符(疑似拼错;仅提示不拦截) */
|
||||
export function unknownExprVars(expression: string, known: ReadonlySet<string>): string[] {
|
||||
return extractExprVars(expression).filter((v) => !known.has(v));
|
||||
}
|
||||
|
||||
// ==================== 工厂 ====================
|
||||
|
||||
let uidSeq = 0;
|
||||
@@ -363,6 +417,8 @@ export interface EditorCtx {
|
||||
userNames: Record<number, string>;
|
||||
/** 条件分支的判断服务注册下拉(flw_router 启用项;拉取失败为空数组,校验降级提示) */
|
||||
routers: RouterOption[];
|
||||
/** 可用变量 chips:近期办件 formData 顶层键聚合 + 内置变量(供条件表达式内联提示/插入) */
|
||||
availableVars: string[];
|
||||
issueOf: (uid: string) => { level: "error" | "warn"; text: string }[];
|
||||
openNode: (uid: string) => void;
|
||||
removeNode: (uid: string) => void;
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
import {
|
||||
BRANCH_LABELS,
|
||||
DeptOption,
|
||||
EXPR_BUILTIN_SET,
|
||||
EditorItem,
|
||||
EditorModel,
|
||||
MAX_BRANCHES,
|
||||
MIN_BRANCHES,
|
||||
RouterOption,
|
||||
unknownExprVars,
|
||||
} from "./model";
|
||||
|
||||
export type IssueLevel = "error" | "warn";
|
||||
@@ -28,11 +30,23 @@ export interface Issue {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export function validateModel(model: EditorModel, depts: DeptOption[], routers: RouterOption[] = []): Issue[] {
|
||||
/**
|
||||
* @param availableVars 该归属应用近期办件 formData 顶层键聚合(编辑器已拉取)。
|
||||
* 仅用于表达式变量的"疑似拼错"**警告**(2026-10-15 P0),不参与任何 error 级判定——
|
||||
* 为空数组时退化为仅比对内置变量,宁少报不误杀(动态变量场景)。
|
||||
*/
|
||||
export function validateModel(
|
||||
model: EditorModel,
|
||||
depts: DeptOption[],
|
||||
routers: RouterOption[] = [],
|
||||
availableVars: string[] = []
|
||||
): Issue[] {
|
||||
const issues: Issue[] = [];
|
||||
const push = (uid: string, level: IssueLevel, path: string, msg: string): void => {
|
||||
issues.push({ uid, level, path, msg, text: `${path}:${msg}` });
|
||||
};
|
||||
/** 已知变量集:formData 聚合键 + 内置变量(与 ConditionBox chips 同口径) */
|
||||
const knownVars: ReadonlySet<string> = new Set<string>([...availableVars, ...EXPR_BUILTIN_SET]);
|
||||
|
||||
const rec = (items: EditorItem[], path: string): void => {
|
||||
items.forEach((item) => {
|
||||
@@ -114,6 +128,16 @@ export function validateModel(model: EditorModel, depts: DeptOption[], routers:
|
||||
if (!cond.trim()) {
|
||||
push(item.uid, "error", path,
|
||||
`出口「${o || i + 1}」未配条件表达式(表达式模式下每个出口必须配置,发布会被拦截)`);
|
||||
return;
|
||||
}
|
||||
// 变量引用静态校验(2026-10-15 P0):疑似拼错的键给 warn,**不阻断**发布。
|
||||
// 理由:formData 键由子应用在运行期决定,动态变量场景误杀代价高于漏报;
|
||||
// 服务端 DefValidator.warnBuiltinMisspelling 是权威口径(含 AST 级解析)。
|
||||
const unknown = unknownExprVars(cond, knownVars);
|
||||
if (unknown.length) {
|
||||
push(item.uid, "warn", path,
|
||||
`出口「${o || i + 1}」表达式引用了未在已知变量中的 ${unknown
|
||||
.map((v) => `「${v}」`).join("、")},疑似拼错(若为动态变量可忽略)`);
|
||||
}
|
||||
});
|
||||
} else if (!item.routerKey) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { Message, Tag } from "view-ui-plus";
|
||||
import { INSTANCE_STATUS, INSTANCE_STATUS_COLOR, flowApi } from "@/api/module/flow";
|
||||
import { EXPR_BUILTIN_VARS } from "@/views/flow/admin/def-editor/model";
|
||||
|
||||
const props = defineProps<{ modelValue: boolean; presetExpression?: string }>();
|
||||
const emit = defineEmits<{ (e: "update:modelValue", v: boolean): void }>();
|
||||
@@ -25,9 +26,13 @@ const result = ref<{ result: boolean; value?: string; error?: string } | null>(n
|
||||
const evaluating = ref(false);
|
||||
const exprBox = ref<any>(null);
|
||||
|
||||
/** 内置变量(线上由引擎注入;schId 属管理端上下文不展示,nodeId/nodeName 需按调试目标自填) */
|
||||
/**
|
||||
* 内置变量(2026-10-15 抽共享:与 ConditionBox chips、后端 builtinEnv 同源)——
|
||||
* 线上由引擎注入;schId 属管理端上下文不展示,nodeId/nodeName 需按调试目标自填。
|
||||
* 展示顺序沿用原口径(schId 置末),故此处按名过滤而非直接复用数组顺序。
|
||||
*/
|
||||
const BUILTIN_KEYS = ["instanceId", "instanceVersion", "bizType", "bizId", "submitterId", "nodeId", "nodeName"];
|
||||
const BUILTIN_SET = new Set(BUILTIN_KEYS);
|
||||
const BUILTIN_SET: ReadonlySet<string> = new Set<string>(EXPR_BUILTIN_VARS);
|
||||
const picked = computed(() => instances.value.find((r) => r.id === pickedId.value) || null);
|
||||
const builtinRows = computed(() => {
|
||||
const p = picked.value;
|
||||
|
||||
Reference in New Issue
Block a user