Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed5f818c1a | ||
|
|
f835965e29 | ||
|
|
e8f9af534b | ||
|
|
acbf601e7a |
+51
-18
@@ -126,12 +126,27 @@ class FlowApi {
|
|||||||
defAppKeys() {
|
defAppKeys() {
|
||||||
return get(`${ROOT}/admin/def/appkeys`);
|
return get(`${ROOT}/admin/def/appkeys`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 管理面:应用设置(2026-10-10) ====================
|
||||||
|
|
||||||
|
/** 应用管理列表(flw_app 白名单;附带学校池静默同步,UserBase 不可达降级本地行) */
|
||||||
|
appList() {
|
||||||
|
return get(`${ROOT}/admin/app/list`);
|
||||||
|
}
|
||||||
|
/** 开关某应用是否对接流程(停用后 defSave 归属应用校验拦截新引用) */
|
||||||
|
appEnable(id: number | string, enabled: boolean) {
|
||||||
|
return post(`${ROOT}/admin/app/enable`, { id, enabled });
|
||||||
|
}
|
||||||
defGet(id: number | string) {
|
defGet(id: number | string) {
|
||||||
return get(`${ROOT}/admin/def/get`, { id });
|
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 }) {
|
||||||
return post(`${ROOT}/admin/def/save`, body);
|
return post(`${ROOT}/admin/def/save`, body);
|
||||||
}
|
}
|
||||||
|
/** 元数据原地编辑(2026-10-10 议题2):只改 name/appKey 不升版本;已发布亦可 */
|
||||||
|
defUpdateMeta(body: { id: number; name: string; appKey?: string }) {
|
||||||
|
return post(`${ROOT}/admin/def/updateMeta`, body);
|
||||||
|
}
|
||||||
defPublish(id: number | string) {
|
defPublish(id: number | string) {
|
||||||
return post(`${ROOT}/admin/def/publish/${id}`);
|
return post(`${ROOT}/admin/def/publish/${id}`);
|
||||||
}
|
}
|
||||||
@@ -150,20 +165,42 @@ class FlowApi {
|
|||||||
return get(`${ROOT}/admin/def/diagram`, params);
|
return get(`${ROOT}/admin/def/diagram`, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 管理面:办理人映射 ====================
|
// ==================== 管理面:科室管理(2026-10-10,取代旧 /map/* 映射 CRUD) ====================
|
||||||
mapList(deptKey?: string) {
|
/** 科室列表(deptKey=自增 id 字符串 + 启用成员数;编辑器 ctx.depts 同结构) */
|
||||||
return get(`${ROOT}/admin/map/list`, deptKey ? { deptKey } : undefined);
|
deptList() {
|
||||||
|
return get(`${ROOT}/admin/dept/list`);
|
||||||
}
|
}
|
||||||
mapSave(body: { deptKey: string; deptName?: string; userId: number; userName?: string }) {
|
deptCreate(deptName: string) {
|
||||||
return post(`${ROOT}/admin/map/save`, body);
|
return post(`${ROOT}/admin/dept/create`, { deptName });
|
||||||
}
|
}
|
||||||
mapDisable(id: number | string) {
|
deptRename(id: number | string, deptName: string) {
|
||||||
return post(`${ROOT}/admin/map/disable/${id}`);
|
return post(`${ROOT}/admin/dept/rename`, { id, deptName });
|
||||||
}
|
}
|
||||||
/** 启用映射(停用行的恢复入口;DEPT_KEY 解析只取启用行) */
|
deptStatus(id: number | string, enabled: boolean) {
|
||||||
mapEnable(id: number | string) {
|
return post(`${ROOT}/admin/dept/status`, { id, enabled });
|
||||||
return post(`${ROOT}/admin/map/enable/${id}`);
|
|
||||||
}
|
}
|
||||||
|
deptDelete(id: number | string) {
|
||||||
|
return post(`${ROOT}/admin/dept/delete`, { id });
|
||||||
|
}
|
||||||
|
deptMembers(deptId: number | string) {
|
||||||
|
return get(`${ROOT}/admin/dept/members`, { deptId });
|
||||||
|
}
|
||||||
|
/** 批量加成员(返回新增数) */
|
||||||
|
deptMemberAdd(deptId: number | string, userIds: number[]) {
|
||||||
|
return post(`${ROOT}/admin/dept/memberAdd`, { deptId, userIds });
|
||||||
|
}
|
||||||
|
deptMemberRemove(memberId: number | string) {
|
||||||
|
return post(`${ROOT}/admin/dept/memberRemove`, { memberId });
|
||||||
|
}
|
||||||
|
/** 一键拉取教师组·预览(不落库,确认后导入) */
|
||||||
|
deptGroupPreview() {
|
||||||
|
return get(`${ROOT}/admin/dept/groupPreview`);
|
||||||
|
}
|
||||||
|
/** 一键拉取教师组·导入(source_group_id 幂等;重复拉取=刷新组名与成员集) */
|
||||||
|
deptImportGroups() {
|
||||||
|
return post(`${ROOT}/admin/dept/importGroups`);
|
||||||
|
}
|
||||||
|
/** 成员检索(UserBase byId;编辑器选人/姓名补全共用,原 /map/lookupUser 通道保留) */
|
||||||
mapLookupUser(userId: number) {
|
mapLookupUser(userId: number) {
|
||||||
return get(`${ROOT}/admin/map/lookupUser`, { userId });
|
return get(`${ROOT}/admin/map/lookupUser`, { userId });
|
||||||
}
|
}
|
||||||
@@ -175,14 +212,6 @@ class FlowApi {
|
|||||||
adminTeacherTree() {
|
adminTeacherTree() {
|
||||||
return get(`${ROOT}/admin/user/teacherTree`);
|
return get(`${ROOT}/admin/user/teacherTree`);
|
||||||
}
|
}
|
||||||
/** 一键拉取教师组·预览(学校子树教师组容器 → 科室+组内成员) */
|
|
||||||
mapGroupPreview() {
|
|
||||||
return get(`${ROOT}/admin/map/groupPreview`);
|
|
||||||
}
|
|
||||||
/** 一键拉取教师组·导入(批量落映射,已存在跳过) */
|
|
||||||
mapImportGroups() {
|
|
||||||
return post(`${ROOT}/admin/map/importGroups`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 管理面:回调注册/日志/死信 ====================
|
// ==================== 管理面:回调注册/日志/死信 ====================
|
||||||
regList() {
|
regList() {
|
||||||
@@ -236,6 +265,10 @@ class FlowApi {
|
|||||||
instanceReroute(instanceId: number | string) {
|
instanceReroute(instanceId: number | string) {
|
||||||
return post(`${ROOT}/admin/instance/${instanceId}/reroute`);
|
return post(`${ROOT}/admin/instance/${instanceId}/reroute`);
|
||||||
}
|
}
|
||||||
|
/** 表达式调试求值(2026-10-10:与运行时同一求值核心;不落库不走流程) */
|
||||||
|
routerEval(body: { expression: string; variablesJson?: string }) {
|
||||||
|
return post(`${ROOT}/admin/router/eval`, body);
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 管理面:办件查询 ====================
|
// ==================== 管理面:办件查询 ====================
|
||||||
adminInstances(params: { status?: number; keyword?: string; page?: number; size?: number }) {
|
adminInstances(params: { status?: number; keyword?: string; page?: number; size?: number }) {
|
||||||
|
|||||||
@@ -66,12 +66,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: { title: "流程编辑器", parentId: 103 },
|
meta: { title: "流程编辑器", parentId: 103 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/admin/maps",
|
// 回调管理(原 104 办理人映射已于 2026-10-10 下线,由 应用设置-科室管理 承载)
|
||||||
name: "flow_admin_maps",
|
|
||||||
component: () => import("@/views/flow/admin/Maps.vue"),
|
|
||||||
meta: { title: "办理人映射", modId: 104 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/admin/callbacks",
|
path: "/admin/callbacks",
|
||||||
name: "flow_admin_callbacks",
|
name: "flow_admin_callbacks",
|
||||||
component: () => import("@/views/flow/admin/Callbacks.vue"),
|
component: () => import("@/views/flow/admin/Callbacks.vue"),
|
||||||
@@ -84,6 +79,14 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import("@/views/flow/admin/Routers.vue"),
|
component: () => import("@/views/flow/admin/Routers.vue"),
|
||||||
meta: { title: "条件路由", modId: 106 },
|
meta: { title: "条件路由", modId: 106 },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// 应用设置(2026-10-10 四议题):tab1 应用管理(flw_app 白名单,学校池同步);
|
||||||
|
// tab2 科室管理(Part E 填充)。菜单需在 UserBase 侧配置同 path 的 app_module(custom_id=107)
|
||||||
|
path: "/admin/app-setting",
|
||||||
|
name: "flow_admin_app_setting",
|
||||||
|
component: () => import("@/views/flow/admin/AppSetting.vue"),
|
||||||
|
meta: { title: "应用设置", modId: 107 },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// 办件进度嵌入页(iframe 免签嵌入,App.vue 对 /embed/* 前缀旁路 CAS 与 layout):
|
// 办件进度嵌入页(iframe 免签嵌入,App.vue 对 /embed/* 前缀旁路 CAS 与 layout):
|
||||||
// 不挂菜单、无 parentId(子应用 Drawer 内打开,非管理面导航);query.token 必填
|
// 不挂菜单、无 parentId(子应用 Drawer 内打开,非管理面导航);query.token 必填
|
||||||
|
|||||||
@@ -0,0 +1,384 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// 应用设置(2026-10-10 四议题):tab1 应用管理(flw_app 白名单 + 学校池同步 + 开关);
|
||||||
|
// tab2 科室管理(flw_dept 组+成员模型,取代旧「办理人映射」页——deptKey=本地自增 id,
|
||||||
|
// 切源后旧流程定义引用的 UserBase 组 id 需重建科室后重新配置)。
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { Message } from "view-ui-plus";
|
||||||
|
import MyConfirm from "@/components/MyConfirm";
|
||||||
|
import UserSelectInput from "@/components/UserSelectInput.vue";
|
||||||
|
import { flowApi } from "@/api/module/flow";
|
||||||
|
import Utils from "@/utils";
|
||||||
|
|
||||||
|
const tab = ref("apps");
|
||||||
|
|
||||||
|
// ==================== tab1 应用管理 ====================
|
||||||
|
const loading = ref(false);
|
||||||
|
const rows = ref<any[]>([]);
|
||||||
|
const tableHeight = ref(Utils.useTableHeight({ isOperat: true }));
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||||
|
{ title: "应用", key: "appName", minWidth: 200 },
|
||||||
|
{ title: "appKey", key: "appKey", width: 120, align: "center" },
|
||||||
|
{
|
||||||
|
title: "对接流程",
|
||||||
|
width: 120,
|
||||||
|
align: "center",
|
||||||
|
render: (h: any, p: any) =>
|
||||||
|
h("i-switch", {
|
||||||
|
props: { value: p.row.enabled === 1, size: "large", loading: p.row._switching },
|
||||||
|
on: { "on-change": (v: boolean) => onToggle(p.row, v) },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
width: 90,
|
||||||
|
align: "center",
|
||||||
|
render: (h: any, p: any) =>
|
||||||
|
h("Tag", { props: { color: p.row.enabled === 1 ? "green" : "gray" } },
|
||||||
|
() => (p.row.enabled === 1 ? "已开启" : "已停用")),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
rows.value = ((await flowApi.appList().send()) as any[]) || [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onToggle(row: any, enabled: boolean) {
|
||||||
|
row._switching = true;
|
||||||
|
try {
|
||||||
|
await flowApi.appEnable(row.id, enabled).send();
|
||||||
|
row.enabled = enabled ? 1 : 0;
|
||||||
|
Message.success(enabled ? `已开启「${row.appName}」` : `已停用「${row.appName}」`);
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "操作失败");
|
||||||
|
load();
|
||||||
|
} finally {
|
||||||
|
row._switching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== tab2 科室管理 ====================
|
||||||
|
const deptLoading = ref(false);
|
||||||
|
const depts = ref<any[]>([]);
|
||||||
|
const deptColumns = [
|
||||||
|
{ title: "序号", type: "index", width: 60, align: "center" },
|
||||||
|
{ title: "科室名", key: "deptName", minWidth: 160 },
|
||||||
|
{ title: "deptKey", key: "deptKey", width: 90, align: "center" },
|
||||||
|
{
|
||||||
|
title: "启用成员",
|
||||||
|
key: "count",
|
||||||
|
width: 90,
|
||||||
|
align: "center",
|
||||||
|
render: (h: any, p: any) => h("span", String(p.row.count)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "来源",
|
||||||
|
width: 90,
|
||||||
|
align: "center",
|
||||||
|
render: (h: any, p: any) => h("Tag", () => (p.row._pulled ? "拉取" : "手工")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
width: 80,
|
||||||
|
align: "center",
|
||||||
|
render: (h: any, p: any) =>
|
||||||
|
h("Tag", { props: { color: p.row.status === 0 ? "green" : "gray" } },
|
||||||
|
() => (p.row.status === 0 ? "启用" : "停用")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
width: 260,
|
||||||
|
align: "center",
|
||||||
|
render: (h: any, p: any) => [
|
||||||
|
Utils.operatingHandle(h, "成员", "icon-yonghu", () => onMembers(p.row)),
|
||||||
|
h("span", { style: "display:inline-block;width:10px" }),
|
||||||
|
Utils.operatingHandle(h, "改名", "icon-edit", () => onRename(p.row)),
|
||||||
|
h("span", { style: "display:inline-block;width:10px" }),
|
||||||
|
p.row.status === 0
|
||||||
|
? Utils.operatingHandle(h, "停用", "icon-stop", () => onDeptStatus(p.row, false))
|
||||||
|
: Utils.operatingHandle(h, "启用", "icon-play", () => onDeptStatus(p.row, true)),
|
||||||
|
h("span", { style: "display:inline-block;width:10px" }),
|
||||||
|
Utils.operatingHandle(h, "删除", "icon-shanchu", () => onDelete(p.row)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function loadDepts() {
|
||||||
|
deptLoading.value = true;
|
||||||
|
try {
|
||||||
|
depts.value = ((await flowApi.deptList().send()) as any[]) || [];
|
||||||
|
} finally {
|
||||||
|
deptLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 新建科室 ----
|
||||||
|
const createVisible = ref(false);
|
||||||
|
const createSaving = ref(false);
|
||||||
|
const createName = ref("");
|
||||||
|
|
||||||
|
function onCreate() {
|
||||||
|
createName.value = "";
|
||||||
|
createVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doCreate() {
|
||||||
|
if (!createName.value.trim()) {
|
||||||
|
Message.warning("科室名称不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createSaving.value = true;
|
||||||
|
try {
|
||||||
|
await flowApi.deptCreate(createName.value.trim()).send();
|
||||||
|
Message.success("已创建");
|
||||||
|
createVisible.value = false;
|
||||||
|
loadDepts();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "创建失败");
|
||||||
|
} finally {
|
||||||
|
createSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 改名 ----
|
||||||
|
const renameVisible = ref(false);
|
||||||
|
const renameSaving = ref(false);
|
||||||
|
const renameRow = ref<any>(null);
|
||||||
|
const renameName = ref("");
|
||||||
|
|
||||||
|
function onRename(row: any) {
|
||||||
|
renameRow.value = row;
|
||||||
|
renameName.value = row.deptName;
|
||||||
|
renameVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doRename() {
|
||||||
|
if (!renameName.value.trim()) {
|
||||||
|
Message.warning("科室名称不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renameSaving.value = true;
|
||||||
|
try {
|
||||||
|
await flowApi.deptRename(renameRow.value.id, renameName.value.trim()).send();
|
||||||
|
Message.success("已改名");
|
||||||
|
renameVisible.value = false;
|
||||||
|
loadDepts();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "改名失败");
|
||||||
|
} finally {
|
||||||
|
renameSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 停启用 / 删除 ----
|
||||||
|
function onDeptStatus(row: any, enabled: boolean) {
|
||||||
|
MyConfirm({
|
||||||
|
title: enabled ? "启用科室" : "停用科室",
|
||||||
|
msg: `确定${enabled ? "启用" : "停用"}「${row.deptName}」吗?`,
|
||||||
|
info: enabled
|
||||||
|
? "引用它的流程节点恢复解析到科室成员"
|
||||||
|
: "停用后引用它的流程定义不能发布;在途办件推进到该节点会报无可用办理人",
|
||||||
|
async onOk() {
|
||||||
|
await flowApi.deptStatus(row.id, enabled).send();
|
||||||
|
Message.success("操作成功");
|
||||||
|
loadDepts();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDelete(row: any) {
|
||||||
|
MyConfirm({
|
||||||
|
title: "删除科室",
|
||||||
|
msg: `确定删除「${row.deptName}」及其全部成员吗?`,
|
||||||
|
info: "删除后引用该 deptKey 的流程定义节点将解析失败(发布校验拦截/办理报错),请先确认无流程引用",
|
||||||
|
async onOk() {
|
||||||
|
await flowApi.deptDelete(row.id).send();
|
||||||
|
Message.success("已删除");
|
||||||
|
loadDepts();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 成员维护 ----
|
||||||
|
const memberVisible = ref(false);
|
||||||
|
const memberRow = ref<any>(null);
|
||||||
|
const members = ref<any[]>([]);
|
||||||
|
const memberLoading = ref(false);
|
||||||
|
const addUserId = ref<number | null>(null);
|
||||||
|
const addUserName = ref("");
|
||||||
|
|
||||||
|
function onMembers(row: any) {
|
||||||
|
memberRow.value = row;
|
||||||
|
memberVisible.value = true;
|
||||||
|
loadMembers(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMembers(row: any) {
|
||||||
|
memberLoading.value = true;
|
||||||
|
try {
|
||||||
|
members.value = ((await flowApi.deptMembers(row.id).send()) as any[]) || [];
|
||||||
|
} finally {
|
||||||
|
memberLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onMemberAdd() {
|
||||||
|
if (!addUserId.value) {
|
||||||
|
Message.warning("请先选择用户");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const added: any = await flowApi.deptMemberAdd(memberRow.value.id, [addUserId.value]).send();
|
||||||
|
Message.success(added > 0 ? `已添加 ${added} 人` : "该成员已存在");
|
||||||
|
addUserId.value = null;
|
||||||
|
addUserName.value = "";
|
||||||
|
loadMembers(memberRow.value);
|
||||||
|
loadDepts();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "添加失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMemberRemove(m: any) {
|
||||||
|
MyConfirm({
|
||||||
|
title: "移除成员",
|
||||||
|
msg: `确定把「${m.userName || "#" + m.userId}」移出「${memberRow.value.deptName}」吗?`,
|
||||||
|
info: "移除后引用该科室的流程节点解析时不再包含此人;在途待办不受影响",
|
||||||
|
async onOk() {
|
||||||
|
await flowApi.deptMemberRemove(m.id).send();
|
||||||
|
loadMembers(memberRow.value);
|
||||||
|
loadDepts();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 一键拉取教师组(初始化语义) ----
|
||||||
|
const groupVisible = ref(false);
|
||||||
|
const groupLoading = ref(false);
|
||||||
|
const groupImporting = ref(false);
|
||||||
|
const groupRows = ref<any[]>([]);
|
||||||
|
const groupColumns = [
|
||||||
|
{ title: "科室名称", key: "deptName", width: 160 },
|
||||||
|
{ title: "成员数", width: 90, align: "center", render: (h: any, p: any) => h("span", String(p.row.users?.length || 0)) },
|
||||||
|
{ title: "成员", key: "members", render: (h: any, p: any) => h("span", (p.row.users || []).map((u: any) => `${u.userName}(${u.userId})`).join("、") || "(空,导入后可手工补)") },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function onGroupPreview() {
|
||||||
|
groupVisible.value = true;
|
||||||
|
groupLoading.value = true;
|
||||||
|
try {
|
||||||
|
groupRows.value = (await flowApi.deptGroupPreview().send()) || [];
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "教师组拉取失败(UserBase 不可达?)");
|
||||||
|
groupVisible.value = false;
|
||||||
|
} finally {
|
||||||
|
groupLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onGroupImport() {
|
||||||
|
groupImporting.value = true;
|
||||||
|
try {
|
||||||
|
const r: any = await flowApi.deptImportGroups().send();
|
||||||
|
Message.success(`导入完成:新建 ${r?.created ?? 0} / 刷新 ${r?.refreshed ?? 0} 个科室,新增成员 ${r?.users ?? 0} 人`);
|
||||||
|
groupVisible.value = false;
|
||||||
|
loadDepts();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "导入失败");
|
||||||
|
} finally {
|
||||||
|
groupImporting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
load();
|
||||||
|
loadDepts();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<div class="main-container">
|
||||||
|
<Tabs v-model="tab">
|
||||||
|
<TabPane label="应用管理" name="apps">
|
||||||
|
<div class="fix mb-12">
|
||||||
|
<Button class="fl" type="primary" size="large" @click="load">刷新(同步学校池)</Button>
|
||||||
|
<span class="fl" style="color: #808695; font-size: 12px; line-height: 40px; margin-left: 10px">
|
||||||
|
应用清单来自本校已开通应用(UserBase 学校组开通,含学校别名);开启后可用于流程归属与筛选。
|
||||||
|
新开通/改名在 UserBase 管理端操作后回本页刷新即可
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Table :columns="columns" :data="rows" :loading="loading" class="link-table table-bottom-box"
|
||||||
|
:height="tableHeight" no-data-text="本校暂无已开通应用(请先在用户中心开通)" />
|
||||||
|
</TabPane>
|
||||||
|
<TabPane label="科室管理" name="depts">
|
||||||
|
<div class="fix mb-12">
|
||||||
|
<Button class="fl" type="primary" size="large" @click="onCreate">新建科室</Button>
|
||||||
|
<Button class="fl" size="large" style="margin-left: 8px" @click="onGroupPreview">一键拉取教师组</Button>
|
||||||
|
<span class="fl" style="color: #808695; font-size: 12px; line-height: 40px; margin-left: 10px">
|
||||||
|
流程节点「按科室」办理的解析源;重复拉取=刷新组名与成员集(不重复建组)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Table :columns="deptColumns" :data="depts" :loading="deptLoading" class="link-table table-bottom-box"
|
||||||
|
:height="tableHeight" no-data-text="暂无科室(可一键拉取教师组或手工新建)" />
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Modal v-model="createVisible" title="新建科室" :mask-closable="false">
|
||||||
|
<Input v-model="createName" placeholder="科室名称,如 人事处" :maxlength="30" />
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="createVisible = false">取消</Button>
|
||||||
|
<Button type="primary" :loading="createSaving" @click="doCreate">创建</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal v-model="renameVisible" title="科室改名" :mask-closable="false">
|
||||||
|
<Input v-model="renameName" placeholder="科室名称" :maxlength="30" />
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="renameVisible = false">取消</Button>
|
||||||
|
<Button type="primary" :loading="renameSaving" @click="doRename">保存</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal v-model="memberVisible" :title="`成员维护 - ${memberRow?.deptName || ''}`" :mask-closable="false" width="640">
|
||||||
|
<div class="fix" style="margin-bottom: 10px">
|
||||||
|
<UserSelectInput
|
||||||
|
class="fl"
|
||||||
|
style="width: 260px"
|
||||||
|
:model-value="addUserId"
|
||||||
|
:display-name="addUserName"
|
||||||
|
@update:model-value="addUserId = $event"
|
||||||
|
@update:display-name="addUserName = $event"
|
||||||
|
/>
|
||||||
|
<Button class="fl" type="primary" style="margin-left: 8px" @click="onMemberAdd">添加</Button>
|
||||||
|
</div>
|
||||||
|
<Table :columns="[
|
||||||
|
{ title: '用户ID', key: 'userId', width: 100 },
|
||||||
|
{ title: '姓名', key: 'userName' },
|
||||||
|
{ title: '操作', width: 90, align: 'center', render: (h, p) => Utils.operatingHandle(h, '移除', 'icon-shanchu', () => onMemberRemove(p.row)) },
|
||||||
|
]" :data="members" :loading="memberLoading" size="small" max-height="360" no-data-text="暂无成员" />
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="memberVisible = false">关闭</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal v-model="groupVisible" title="一键拉取教师组(预览)" :mask-closable="false" width="720">
|
||||||
|
<p style="color: #808695; font-size: 12px; line-height: 1.6; margin-bottom: 8px">
|
||||||
|
以下为本校教师组容器下的科室及组内成员;确认后导入。已拉取过的科室(同名 UserBase 组)将刷新组名与成员集,不会重复建组
|
||||||
|
</p>
|
||||||
|
<Table :columns="groupColumns" :data="groupRows" :loading="groupLoading" size="small" max-height="380"
|
||||||
|
no-data-text="未发现可拉取的教师组科室" />
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="groupVisible = false">取消</Button>
|
||||||
|
<Button type="primary" :loading="groupImporting" :disabled="!groupRows.length" @click="onGroupImport">导入</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// 流程定义管理:列表 + 发布/启用/停用/升版本/版本历史/图示/源码。
|
// 流程定义管理:列表 + 发布/启用/停用/升版本/版本历史/图示/源码。
|
||||||
// 新建与编辑走独立的全屏可视化编辑器(/admin/defs/editor,钉钉式卡片流,见《流程编辑器方案.md》);
|
// 新建与编辑走独立的全屏可视化编辑器(/admin/defs/editor,钉钉式卡片流,见《流程编辑器方案.md》);
|
||||||
// 本页不再直接编辑 JSON 文本(编辑器内置"源码"高级模式兜底);行内「源码」为只读查看+复制。
|
// 本页不再直接编辑 JSON 文本(编辑器内置"源码"高级模式兜底);行内「源码」为只读查看+复制。
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, computed, onMounted } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { Message } from "view-ui-plus";
|
import { Message } from "view-ui-plus";
|
||||||
import MyConfirm from "@/components/MyConfirm";
|
import MyConfirm from "@/components/MyConfirm";
|
||||||
@@ -23,15 +23,16 @@ const sourceText = ref("");
|
|||||||
const sourceTitle = ref("");
|
const sourceTitle = ref("");
|
||||||
// 操作按钮区 + 表格,无搜索、无分页
|
// 操作按钮区 + 表格,无搜索、无分页
|
||||||
const tableHeight = ref(Utils.useTableHeight({ isOperat: true }));
|
const tableHeight = ref(Utils.useTableHeight({ isOperat: true }));
|
||||||
// 按应用筛选(flw_def.app_key,2026-10-10):下拉数据源来自 /def/appkeys
|
// 按应用筛选(flw_def.app_key,2026-10-10):下拉数据源来自应用设置白名单(flw_app enabled=1)
|
||||||
const appKeys = ref<string[]>([]);
|
const apps = ref<{ appKey: string; appName: string }[]>([]);
|
||||||
|
const appMap = ref<Record<string, string>>({});
|
||||||
const filterApp = ref("");
|
const filterApp = ref("");
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||||
{ title: "流程", key: "name", minWidth: 180 },
|
{ title: "流程", key: "name", minWidth: 180 },
|
||||||
{ title: "编码", key: "code", width: 160 },
|
{ title: "编码", key: "code", width: 160 },
|
||||||
{ title: "应用", key: "appKey", width: 110, align: "center", render: (h: any, p: any) => h("span", p.row.appKey || "-") },
|
{ title: "应用", key: "appKey", width: 130, align: "center", render: (h: any, p: any) => h("span", p.row.appKey ? (appMap.value[p.row.appKey] || p.row.appKey) : "-") },
|
||||||
{ title: "版本", key: "version", width: 70, align: "center" },
|
{ title: "版本", key: "version", width: 70, align: "center" },
|
||||||
{ title: "状态", width: 90, align: "center", render: (h: any, p: any) => {
|
{ title: "状态", width: 90, align: "center", render: (h: any, p: any) => {
|
||||||
const c = p.row.status === 1 ? "green" : p.row.status === 0 ? "orange" : "gray";
|
const c = p.row.status === 1 ? "green" : p.row.status === 0 ? "orange" : "gray";
|
||||||
@@ -39,7 +40,7 @@ const columns = [
|
|||||||
} },
|
} },
|
||||||
{ title: "GUID", key: "flowGuid", width: 200 },
|
{ title: "GUID", key: "flowGuid", width: 200 },
|
||||||
{ title: "发布时间", width: 160, render: (h: any, p: any) => h("span", (p.row.publishTime || "").replace("T", " ") || "-") },
|
{ title: "发布时间", width: 160, render: (h: any, p: any) => h("span", (p.row.publishTime || "").replace("T", " ") || "-") },
|
||||||
{ title: "操作", width: 250, align: "center", fixed: "right", render: (h: any, p: any) => {
|
{ title: "操作", width: 310, align: "center", fixed: "right", render: (h: any, p: any) => {
|
||||||
const btns: any[] = [];
|
const btns: any[] = [];
|
||||||
if (p.row.status === 0) {
|
if (p.row.status === 0) {
|
||||||
btns.push(Utils.operatingHandle(h, "编辑", "icon-edit", () => onEdit(p.row)));
|
btns.push(Utils.operatingHandle(h, "编辑", "icon-edit", () => onEdit(p.row)));
|
||||||
@@ -52,6 +53,8 @@ const columns = [
|
|||||||
// 草稿不走这里,草稿用「发布」)
|
// 草稿不走这里,草稿用「发布」)
|
||||||
btns.push(Utils.operatingHandle(h, "启用", "icon-play", () => onEnable(p.row)));
|
btns.push(Utils.operatingHandle(h, "启用", "icon-play", () => onEnable(p.row)));
|
||||||
}
|
}
|
||||||
|
// 元数据原地编辑(2026-10-10 议题2):改名称/归属应用不升版本(含已发布/停用)
|
||||||
|
btns.push(Utils.operatingHandle(h, "编辑信息", "icon-bianji", () => onEditMeta(p.row)));
|
||||||
btns.push(Utils.operatingHandle(h, "图示", "icon-tupian", () => onDiagram(p.row)));
|
btns.push(Utils.operatingHandle(h, "图示", "icon-tupian", () => onDiagram(p.row)));
|
||||||
btns.push(Utils.operatingHandle(h, "源码", "icon-liebiao", () => onSource(p.row)));
|
btns.push(Utils.operatingHandle(h, "源码", "icon-liebiao", () => onSource(p.row)));
|
||||||
btns.push(Utils.operatingHandle(h, "版本历史", "icon-shijian", () => onVersions(p.row)));
|
btns.push(Utils.operatingHandle(h, "版本历史", "icon-shijian", () => onVersions(p.row)));
|
||||||
@@ -68,12 +71,18 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 应用清单(筛选下拉):flw_def 已标注 ∪ 已注册回调的应用,失败降级为空下拉 */
|
/** 应用清单(筛选下拉):应用设置白名单(flw_app enabled=1),失败降级为空下拉 */
|
||||||
async function loadAppKeys() {
|
async function loadAppKeys() {
|
||||||
try {
|
try {
|
||||||
appKeys.value = (await flowApi.defAppKeys().send()) || [];
|
const rows: any[] = (await flowApi.appList().send()) || [];
|
||||||
|
const enabled = rows.filter((r) => r.enabled === 1);
|
||||||
|
apps.value = enabled.map((r) => ({ appKey: String(r.appKey), appName: r.appName || String(r.appKey) }));
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
rows.forEach((r) => { if (r.appKey != null) map[String(r.appKey)] = r.appName || String(r.appKey); });
|
||||||
|
appMap.value = map;
|
||||||
} catch {
|
} catch {
|
||||||
appKeys.value = [];
|
apps.value = [];
|
||||||
|
appMap.value = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +170,50 @@ function onDiagram(row: any) {
|
|||||||
router.push({ path: "/diagram", query: { defId: row.id } });
|
router.push({ path: "/diagram", query: { defId: row.id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 编辑信息(元数据原地编辑,2026-10-10 议题2:改名称/归属应用不升版本) ----
|
||||||
|
const metaVisible = ref(false);
|
||||||
|
const metaSaving = ref(false);
|
||||||
|
const metaForm = ref<{ id: number; name: string; appKey: string; version: number; status: number }>({
|
||||||
|
id: 0, name: "", appKey: "", version: 0, status: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
function onEditMeta(row: any) {
|
||||||
|
metaForm.value = { id: row.id, name: row.name || "", appKey: row.appKey || "", version: row.version, status: row.status };
|
||||||
|
metaVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前 appKey 不在启用集合时兜底插入(保存会被后端拦,提示用户先开启或清空) */
|
||||||
|
const metaAppOptions = computed(() => {
|
||||||
|
const list = apps.value.map((a) => ({ value: a.appKey, label: a.appName }));
|
||||||
|
const cur = metaForm.value.appKey;
|
||||||
|
if (cur && !list.some((o) => o.value === cur)) {
|
||||||
|
list.unshift({ value: cur, label: `${cur}(未开启,保存将被拦)` });
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onMetaSave() {
|
||||||
|
if (!metaForm.value.name.trim()) {
|
||||||
|
Message.warning("流程名称不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
metaSaving.value = true;
|
||||||
|
try {
|
||||||
|
await flowApi.defUpdateMeta({
|
||||||
|
id: metaForm.value.id,
|
||||||
|
name: metaForm.value.name.trim(),
|
||||||
|
appKey: metaForm.value.appKey || undefined,
|
||||||
|
}).send();
|
||||||
|
Message.success("已更新(版本不变)");
|
||||||
|
metaVisible.value = false;
|
||||||
|
load();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || "保存失败");
|
||||||
|
} finally {
|
||||||
|
metaSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
load();
|
load();
|
||||||
loadAppKeys();
|
loadAppKeys();
|
||||||
@@ -175,7 +228,7 @@ onMounted(() => {
|
|||||||
<Button class="fl" type="primary" size="large" @click="onNew">新建流程</Button>
|
<Button class="fl" type="primary" size="large" @click="onNew">新建流程</Button>
|
||||||
<Select v-model="filterApp" clearable filterable placeholder="按应用筛选" class="fl"
|
<Select v-model="filterApp" clearable filterable placeholder="按应用筛选" class="fl"
|
||||||
style="width: 200px; margin-left: 8px" @on-change="load">
|
style="width: 200px; margin-left: 8px" @on-change="load">
|
||||||
<Option v-for="k in appKeys" :key="k" :value="k">{{ k }}</Option>
|
<Option v-for="a in apps" :key="a.appKey" :value="a.appKey">{{ a.appName }}</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -192,6 +245,28 @@ onMounted(() => {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<SourceDrawer v-model="sourceVisible" :text="sourceText" :subtitle="sourceTitle" />
|
<SourceDrawer v-model="sourceVisible" :text="sourceText" :subtitle="sourceTitle" />
|
||||||
|
|
||||||
|
<Modal v-model="metaVisible" title="编辑流程信息(不升版本)" :mask-closable="false" width="460">
|
||||||
|
<p style="color: #808695; font-size: 12px; line-height: 1.6; margin-bottom: 10px">
|
||||||
|
只修改名称与归属应用,版本与流程内容保持不变(v{{ metaForm.version }},{{
|
||||||
|
DEF_STATUS[metaForm.status] || metaForm.status }})。结构改动请走「{{
|
||||||
|
metaForm.status === 0 ? "编辑" : "升版本" }}」进编辑器。
|
||||||
|
</p>
|
||||||
|
<Form :label-width="90">
|
||||||
|
<FormItem label="流程名称">
|
||||||
|
<Input v-model="metaForm.name" placeholder="流程名称" :maxlength="30" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="归属应用">
|
||||||
|
<Select v-model="metaForm.appKey" clearable filterable placeholder="归属应用,可空">
|
||||||
|
<Option v-for="o in metaAppOptions" :key="o.value" :value="o.value">{{ o.label }}</Option>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="metaVisible = false">取消</Button>
|
||||||
|
<Button type="primary" :loading="metaSaving" @click="onMetaSave">保存</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,205 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
// 办理人映射管理(概览 §4.7/§六):DEPT_KEY → 办理人;办理人改为弹窗选人(参照 Notice 选人范式)
|
|
||||||
import { ref, onMounted } from "vue";
|
|
||||||
import { Message } from "view-ui-plus";
|
|
||||||
import MyConfirm from "@/components/MyConfirm";
|
|
||||||
import UserSelectInput from "@/components/UserSelectInput.vue";
|
|
||||||
import { flowApi } from "@/api/module/flow";
|
|
||||||
import Utils from "@/utils";
|
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
const deptKeyFilter = ref("");
|
|
||||||
const rows = ref<any[]>([]);
|
|
||||||
// 操作按钮区 + 搜索区、无分页
|
|
||||||
const tableHeight = ref(Utils.useTableHeight({ isSearch: true, isOperat: true }));
|
|
||||||
|
|
||||||
// 一键拉取教师组(预览确认后批量导入)
|
|
||||||
const groupVisible = ref(false);
|
|
||||||
const groupLoading = ref(false);
|
|
||||||
const groupImporting = ref(false);
|
|
||||||
const groupRows = ref<any[]>([]);
|
|
||||||
const groupColumns = [
|
|
||||||
{ title: "科室编码", key: "deptKey", width: 100 },
|
|
||||||
{ title: "科室名称", key: "deptName", width: 160 },
|
|
||||||
{ title: "成员数", key: "memberCount", width: 90, align: "center", render: (h: any, p: any) => h("span", String(p.row.users?.length || 0)) },
|
|
||||||
{ title: "成员", key: "members", render: (h: any, p: any) => h("span", (p.row.users || []).map((u: any) => `${u.userName}(${u.userId})`).join("、") || "(空,导入后可手工补)") },
|
|
||||||
];
|
|
||||||
|
|
||||||
async function onGroupPreview() {
|
|
||||||
groupVisible.value = true;
|
|
||||||
groupLoading.value = true;
|
|
||||||
try {
|
|
||||||
groupRows.value = (await flowApi.mapGroupPreview().send()) || [];
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(e?.message || "教师组拉取失败(UserBase 不可达?)");
|
|
||||||
groupVisible.value = false;
|
|
||||||
} finally {
|
|
||||||
groupLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onGroupImport() {
|
|
||||||
groupImporting.value = true;
|
|
||||||
try {
|
|
||||||
const r: any = await flowApi.mapImportGroups().send();
|
|
||||||
Message.success(`导入完成:${r?.depts ?? 0} 个科室 / 新增 ${r?.users ?? 0} 条映射 / 跳过已存在 ${r?.skippedExisting ?? 0} 条`);
|
|
||||||
groupVisible.value = false;
|
|
||||||
load();
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(e?.message || "导入失败");
|
|
||||||
} finally {
|
|
||||||
groupImporting.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
|
||||||
{ title: "科室编码", key: "deptKey", width: 140 },
|
|
||||||
{ title: "科室名称", width: 160, render: (h: any, p: any) => h("span", p.row.deptName || "-") },
|
|
||||||
{ title: "办理人", key: "userName", width: 120 },
|
|
||||||
{ title: "用户ID", key: "userId", width: 90 },
|
|
||||||
{ title: "状态", width: 90, align: "center", render: (h: any, p: any) => h("Tag", { color: p.row.status === 0 ? "green" : "gray" }, () => (p.row.status === 0 ? "启用" : "停用")) },
|
|
||||||
{ title: "操作", width: 100, align: "center", fixed: "right", render: (h: any, p: any) =>
|
|
||||||
p.row.status === 0
|
|
||||||
? Utils.operatingHandle(h, "停用", "icon-stop", () => onDisable(p.row))
|
|
||||||
: Utils.operatingHandle(h, "启用", "icon-play", () => onEnable(p.row)) },
|
|
||||||
];
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
rows.value = (await flowApi.mapList(deptKeyFilter.value.trim() || undefined).send()) || [];
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSearch() {
|
|
||||||
load();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 新增/更新映射弹窗(规范:必填红星标注,校验失败提示统一文案) ----
|
|
||||||
const saveVisible = ref(false);
|
|
||||||
const saving = ref(false);
|
|
||||||
const formRef = ref<any>(null);
|
|
||||||
const form = ref<any>({ deptKey: "", deptName: "", userId: null, userName: "" });
|
|
||||||
const saveRules = {
|
|
||||||
deptKey: [{ required: true, message: "请填写红色星号内容!", trigger: "blur" }],
|
|
||||||
userId: [{ required: true, message: "请填写红色星号内容!", trigger: "change" }],
|
|
||||||
};
|
|
||||||
|
|
||||||
function onAdd() {
|
|
||||||
form.value = { deptKey: "", deptName: "", userId: null, userName: "" };
|
|
||||||
saveVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onUserPick(u: any) {
|
|
||||||
// 选人组件回传 id+name 同帧快照(姓名不再自查)
|
|
||||||
form.value.userName = u?.name || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSave() {
|
|
||||||
formRef.value?.validate(async (valid: boolean) => {
|
|
||||||
if (!valid) return;
|
|
||||||
saving.value = true;
|
|
||||||
try {
|
|
||||||
await flowApi.mapSave({
|
|
||||||
deptKey: form.value.deptKey.trim(),
|
|
||||||
deptName: form.value.deptName,
|
|
||||||
userId: Number(form.value.userId),
|
|
||||||
userName: form.value.userName,
|
|
||||||
}).send();
|
|
||||||
Message.success("已保存");
|
|
||||||
saveVisible.value = false;
|
|
||||||
load();
|
|
||||||
} finally {
|
|
||||||
saving.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDisable(row: any) {
|
|
||||||
MyConfirm({
|
|
||||||
title: "停用",
|
|
||||||
msg: `确定停用科室「${row.deptName || row.deptKey}」的办理人映射吗?`,
|
|
||||||
info: "停用后该科室流程节点需重新配置办理人",
|
|
||||||
async onOk() {
|
|
||||||
await flowApi.mapDisable(row.id).send();
|
|
||||||
Message.success("已停用");
|
|
||||||
load();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 启用(2026-09-18 补齐:原来停用后只能重新"新增映射"恢复) */
|
|
||||||
function onEnable(row: any) {
|
|
||||||
MyConfirm({
|
|
||||||
title: "启用",
|
|
||||||
msg: `确定启用科室「${row.deptName || row.deptKey}」的办理人「${row.userName || row.userId}」吗?`,
|
|
||||||
info: "启用后该科室的流程节点会重新解析到这名办理人",
|
|
||||||
async onOk() {
|
|
||||||
await flowApi.mapEnable(row.id).send();
|
|
||||||
Message.success("已启用");
|
|
||||||
load();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="page-wrapper">
|
|
||||||
<div class="main-container">
|
|
||||||
<!-- 操作按钮区:主操作靠左、其余靠右(bsch应用页规范 §1) -->
|
|
||||||
<div class="fix mb-12">
|
|
||||||
<Button class="fl" type="primary" size="large" @click="onAdd">新增映射</Button>
|
|
||||||
<Button class="fr" size="large" :loading="groupLoading" @click="onGroupPreview">一键拉取教师组</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 搜索区:关键字即输即搜,不设查询按钮 -->
|
|
||||||
<div class="search-wrap fix">
|
|
||||||
<Input class="fr" v-model="deptKeyFilter" placeholder="按科室编码筛选" search suffix="ios-search"
|
|
||||||
style="width: 200px" @on-search="onSearch" @on-clear="onSearch" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Table :columns="columns" :data="rows" :loading="loading" class="link-table table-bottom-box"
|
|
||||||
:height="tableHeight" no-data-text="暂无数据" />
|
|
||||||
|
|
||||||
<Modal v-model="saveVisible" title="新增/更新映射" :mask-closable="false">
|
|
||||||
<Form ref="formRef" :model="form" :rules="saveRules" :label-width="90">
|
|
||||||
<FormItem label="科室编码" prop="deptKey">
|
|
||||||
<Input v-model="form.deptKey" placeholder="流程 JSON 中 DEPT_KEY 引用的编码,如 ybb" />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem label="科室名称">
|
|
||||||
<Input v-model="form.deptName" placeholder="如 医保办(可空)" />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem label="办理人" prop="userId">
|
|
||||||
<UserSelectInput
|
|
||||||
:model-value="form.userId"
|
|
||||||
:display-name="form.userName"
|
|
||||||
@update:model-value="form.userId = $event"
|
|
||||||
@update:display-name="form.userName = $event"
|
|
||||||
@on-change="onUserPick"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Form>
|
|
||||||
<template #footer>
|
|
||||||
<Button @click="saveVisible = false">取消</Button>
|
|
||||||
<Button type="primary" :loading="saving" @click="onSave">提交</Button>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal v-model="groupVisible" title="一键拉取教师组(UserBase 学校组树)" width="720" :mask-closable="false">
|
|
||||||
<p style="margin-bottom: 8px; color: #808695">
|
|
||||||
按学校组树中类型=教师组(type 7)的容器组取其子组作为科室(无子组时容器自身即科室),科室编码=UserBase 组 id,成员=组内用户。已存在且启用中的映射自动跳过。
|
|
||||||
</p>
|
|
||||||
<Table :columns="groupColumns" :data="groupRows" :loading="groupLoading" max-height="360" no-data-text="暂无数据" />
|
|
||||||
<template #footer>
|
|
||||||
<Button @click="groupVisible = false">取消</Button>
|
|
||||||
<Button type="primary" :loading="groupImporting" :disabled="!groupRows.length" @click="onGroupImport">确认导入</Button>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -131,6 +131,35 @@ 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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load);
|
onMounted(load);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -139,6 +168,7 @@ onMounted(load);
|
|||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<div class="fix mb-12">
|
<div class="fix mb-12">
|
||||||
<Button class="fl" type="primary" size="large" @click="onAdd">新增注册</Button>
|
<Button class="fl" type="primary" size="large" @click="onAdd">新增注册</Button>
|
||||||
|
<Button class="fl" size="large" style="margin-left: 8px" @click="onEval">表达式调试</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-wrap fix">
|
<div class="search-wrap fix">
|
||||||
@@ -177,6 +207,35 @@ onMounted(load);
|
|||||||
<Button type="primary" :loading="saving" @click="onSave">提交</Button>
|
<Button type="primary" :loading="saving" @click="onSave">提交</Button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 科室下拉项:mapList 聚合 + 当前值(可能已不在映射表中)兜底,避免选项丢失 */
|
/** 科室下拉项:科室管理列表 + 当前值(可能已删除)兜底,避免选项丢失 */
|
||||||
const deptOptions = computed(() => {
|
const deptOptions = computed(() => {
|
||||||
const list = ctx.depts.map((d) => ({
|
const list = ctx.depts.map((d) => ({
|
||||||
value: d.deptKey,
|
value: d.deptKey,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 条件分支容器(2026-09-24):紫系区分并行橙;与 ParallelBox 的本质差异——无 JOIN 语义,
|
// 条件分支容器(2026-09-24):紫系区分并行橙;与 ParallelBox 的本质差异——无 JOIN 语义,
|
||||||
// 分支按出口名互斥选路(运行时调判断服务),分支尾隐式汇合到容器后继。
|
// 分支按出口名互斥选路(运行时调判断服务/表达式求值),分支尾隐式汇合到容器后继。
|
||||||
// 分支头部的出口名 Input 即 contentJson 里 CONDITION 命名出边的 name(判断接口 outlets 契约)。
|
// 分支头部的出口名 Input 即 contentJson 里 CONDITION 命名出边的 name(判断接口 outlets 契约);
|
||||||
|
// EXPRESSION 模式(2026-10-10)下分支头部再就地编辑出边 condition,序号即求值优先级。
|
||||||
import { computed, inject } from "vue";
|
import { computed, inject } from "vue";
|
||||||
import { BRANCH_LABELS, EDITOR_CTX, MAX_BRANCHES, MIN_BRANCHES, ConditionNode } from "../model";
|
import { BRANCH_LABELS, EDITOR_CTX, MAX_BRANCHES, MIN_BRANCHES, ConditionNode } from "../model";
|
||||||
import NodeList from "./NodeList.vue";
|
import NodeList from "./NodeList.vue";
|
||||||
@@ -14,8 +15,10 @@ const issues = computed(() => ctx.issueOf(props.node.uid));
|
|||||||
const hasError = computed(() => issues.value.some((i) => i.level === "error"));
|
const hasError = computed(() => issues.value.some((i) => i.level === "error"));
|
||||||
const canRemoveBranch = computed(() => props.node.branches.length > MIN_BRANCHES);
|
const canRemoveBranch = computed(() => props.node.branches.length > MIN_BRANCHES);
|
||||||
const canAddBranch = computed(() => props.node.branches.length < MAX_BRANCHES);
|
const canAddBranch = computed(() => props.node.branches.length < MAX_BRANCHES);
|
||||||
|
const expression = computed(() => props.node.judgeMode === "EXPRESSION");
|
||||||
|
|
||||||
const routerLabel = computed(() => {
|
const routerLabel = computed(() => {
|
||||||
|
if (expression.value) return "表达式判断(按分支顺序取首个为真)";
|
||||||
if (!props.node.routerKey) return "判断服务:未选择";
|
if (!props.node.routerKey) return "判断服务:未选择";
|
||||||
const r = ctx.routers.find((x) => x.routerKey === props.node.routerKey);
|
const r = ctx.routers.find((x) => x.routerKey === props.node.routerKey);
|
||||||
return r ? `判断服务:${r.name}(${props.node.routerKey})` : `判断服务:${props.node.routerKey}`;
|
return r ? `判断服务:${r.name}(${props.node.routerKey})` : `判断服务:${props.node.routerKey}`;
|
||||||
@@ -25,19 +28,28 @@ const routerLabel = computed(() => {
|
|||||||
function renameOutlet(i: number, v: string): void {
|
function renameOutlet(i: number, v: string): void {
|
||||||
props.node.outlets[i] = v;
|
props.node.outlets[i] = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 出口条件就地编辑(EXPRESSION 模式;与 outlets 下标一一对应) */
|
||||||
|
function editCondition(i: number, v: string): void {
|
||||||
|
props.node.conditions = props.node.conditions || [];
|
||||||
|
while (props.node.conditions.length <= i) props.node.conditions.push("");
|
||||||
|
props.node.conditions[i] = v;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="condition-box" :class="{ error: hasError }">
|
<div class="condition-box" :class="{ error: hasError }">
|
||||||
<div class="cb-title" @click="ctx.openNode(node.uid)">
|
<div class="cb-title" @click="ctx.openNode(node.uid)">
|
||||||
条件分支(按出口互斥选路)
|
条件分支(按出口互斥选路)
|
||||||
<span class="cb-hint">(点击标题配置判断服务)</span>
|
<span class="cb-hint">(点击标题配置{{ expression ? "表达式" : "判断服务" }})</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cb-router">{{ routerLabel }}</div>
|
<div class="cb-router">{{ routerLabel }}</div>
|
||||||
<div class="cb-cols">
|
<div class="cb-cols">
|
||||||
<div v-for="(branch, i) in node.branches" :key="i" class="cb-col">
|
<div v-for="(branch, i) in node.branches" :key="i" class="cb-col">
|
||||||
<div class="cb-col-head">
|
<div class="cb-col-head">
|
||||||
<span class="cb-col-no">{{ BRANCH_LABELS[i] || i + 1 }}</span>
|
<span class="cb-col-no" :title="expression ? '表达式求值优先级' : undefined">
|
||||||
|
{{ BRANCH_LABELS[i] || i + 1 }}
|
||||||
|
</span>
|
||||||
<input
|
<input
|
||||||
class="cb-outlet"
|
class="cb-outlet"
|
||||||
:value="node.outlets[i]"
|
:value="node.outlets[i]"
|
||||||
@@ -57,6 +69,14 @@ function renameOutlet(i: number, v: string): void {
|
|||||||
<span class="cb-col-del" title="删除分支">×</span>
|
<span class="cb-col-del" title="删除分支">×</span>
|
||||||
</Poptip>
|
</Poptip>
|
||||||
</div>
|
</div>
|
||||||
|
<input
|
||||||
|
v-if="expression"
|
||||||
|
class="cb-cond"
|
||||||
|
:value="(node.conditions || [])[i] || ''"
|
||||||
|
placeholder="条件表达式,如 amount > 1000"
|
||||||
|
maxlength="200"
|
||||||
|
@input="editCondition(i, ($event.target as HTMLInputElement).value)"
|
||||||
|
/>
|
||||||
<NodeList :items="branch" :container="`${node.uid}:${i}`" />
|
<NodeList :items="branch" :container="`${node.uid}:${i}`" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,6 +162,25 @@ function renameOutlet(i: number, v: string): void {
|
|||||||
outline: none;
|
outline: none;
|
||||||
border-color: #9254de;
|
border-color: #9254de;
|
||||||
}
|
}
|
||||||
|
.cb-cond {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px dashed #d3b8f9;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5b3fa8;
|
||||||
|
font-family: Consolas, Monaco, monospace;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.cb-cond:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #9254de;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
.cb-cond::placeholder {
|
||||||
|
color: #c5b3e8;
|
||||||
|
}
|
||||||
.cb-col-del {
|
.cb-col-del {
|
||||||
flex: none;
|
flex: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 条件分支配置抽屉(2026-09-24):结构与 NotifyDrawer 同源(本地表单 + 确定回写)。
|
// 条件分支配置抽屉(2026-09-24):结构与 NotifyDrawer 同源(本地表单 + 确定回写)。
|
||||||
// 只管节点名与判断服务(flw_router 下拉);出口命名/分支编排 ConditionBox 就地做。
|
// 管节点名与判断方式(2026-10-10 二选一:服务/表达式);出口命名/条件表达式/分支编排 ConditionBox 就地做。
|
||||||
import { computed, inject, reactive, watch } from "vue";
|
import { computed, inject, reactive, watch } from "vue";
|
||||||
import { Message } from "view-ui-plus";
|
import { Message } from "view-ui-plus";
|
||||||
import { ConditionNode, EDITOR_CTX } from "../model";
|
import { ConditionNode, EDITOR_CTX, JudgeMode } from "../model";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean;
|
modelValue: boolean;
|
||||||
@@ -21,6 +21,7 @@ const ctx = inject(EDITOR_CTX)!;
|
|||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: "",
|
name: "",
|
||||||
routerKey: "",
|
routerKey: "",
|
||||||
|
judgeMode: "SERVICE" as JudgeMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
function syncFromNode(): void {
|
function syncFromNode(): void {
|
||||||
@@ -28,6 +29,7 @@ function syncFromNode(): void {
|
|||||||
if (!n) return;
|
if (!n) return;
|
||||||
form.name = n.name;
|
form.name = n.name;
|
||||||
form.routerKey = n.routerKey || "";
|
form.routerKey = n.routerKey || "";
|
||||||
|
form.judgeMode = n.judgeMode === "EXPRESSION" ? "EXPRESSION" : "SERVICE";
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -56,6 +58,16 @@ const routerHint = computed(() => {
|
|||||||
return `调用 ${r.serviceKey}${r.path};返回出口名即分支走向`;
|
return `调用 ${r.serviceKey}${r.path};返回出口名即分支走向`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 切换判断方式:表达式模式下 conditions 补齐占位(与 outlets 同位) */
|
||||||
|
function switchJudgeMode(mode: JudgeMode): void {
|
||||||
|
form.judgeMode = mode;
|
||||||
|
const n = props.node;
|
||||||
|
if (n && mode === "EXPRESSION") {
|
||||||
|
n.conditions = n.conditions || [];
|
||||||
|
while (n.conditions.length < n.outlets.length) n.conditions.push("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onOk(): void {
|
function onOk(): void {
|
||||||
const n = props.node;
|
const n = props.node;
|
||||||
if (!n) {
|
if (!n) {
|
||||||
@@ -66,12 +78,17 @@ function onOk(): void {
|
|||||||
Message.warning("节点名称不能为空");
|
Message.warning("节点名称不能为空");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!form.routerKey) {
|
if (form.judgeMode === "SERVICE" && !form.routerKey) {
|
||||||
Message.warning("请选择判断服务(未配置的判断服务运行到该节点会挂起)");
|
Message.warning("请选择判断服务(未配置的判断服务运行到该节点会挂起)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
n.name = form.name.trim();
|
n.name = form.name.trim();
|
||||||
n.routerKey = form.routerKey;
|
n.routerKey = form.judgeMode === "SERVICE" ? form.routerKey : "";
|
||||||
|
n.judgeMode = form.judgeMode;
|
||||||
|
if (form.judgeMode === "EXPRESSION") {
|
||||||
|
n.conditions = n.conditions || [];
|
||||||
|
while (n.conditions.length < n.outlets.length) n.conditions.push("");
|
||||||
|
}
|
||||||
emit("update:modelValue", false);
|
emit("update:modelValue", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +113,17 @@ function onRemove(): void {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fld">
|
<div class="fld">
|
||||||
|
<label>判断方式</label>
|
||||||
|
<RadioGroup :model-value="form.judgeMode" @on-change="switchJudgeMode">
|
||||||
|
<Radio label="SERVICE">服务(外部判断接口)</Radio>
|
||||||
|
<Radio label="EXPRESSION">表达式</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
<div class="hint">
|
||||||
|
简单条件用表达式在编辑器内闭环;复杂判断(查库/调外部系统)沿用注册的判断服务
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="form.judgeMode === 'SERVICE'" class="fld">
|
||||||
<label>判断服务</label>
|
<label>判断服务</label>
|
||||||
<Select v-model="form.routerKey" filterable placeholder="请选择判断服务(flw_router 注册)">
|
<Select v-model="form.routerKey" filterable placeholder="请选择判断服务(flw_router 注册)">
|
||||||
<Option v-for="o in routerOptions" :key="o.value" :value="o.value" :label="o.label" />
|
<Option v-for="o in routerOptions" :key="o.value" :value="o.value" :label="o.label" />
|
||||||
@@ -108,6 +136,15 @@ function onRemove(): void {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="fld">
|
||||||
|
<label>表达式说明</label>
|
||||||
|
<div class="hint">
|
||||||
|
每个出口在分支列头部就地填写 AviatorScript 条件(如 amount > 1000);按分支顺序取
|
||||||
|
首个为真的出口。变量=表单字段 + 内置(schId/bizType/bizId/submitterId/instanceId 等)。
|
||||||
|
变量未提供或条件全不为真时流程挂起(不配默认出口),可在"条件路由"页调试表达式
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="fld">
|
<div class="fld">
|
||||||
<label>出口与分支</label>
|
<label>出口与分支</label>
|
||||||
<div class="hint">
|
<div class="hint">
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 科室下拉项:mapList 聚合 + 当前值(可能已不在映射表中)兜底,避免选项丢失 */
|
/** 科室下拉项:科室管理列表 + 当前值(可能已删除)兜底,避免选项丢失 */
|
||||||
const deptOptions = computed(() => {
|
const deptOptions = computed(() => {
|
||||||
const list = ctx.depts.map((d) => ({
|
const list = ctx.depts.map((d) => ({
|
||||||
value: d.deptKey,
|
value: d.deptKey,
|
||||||
|
|||||||
@@ -43,15 +43,19 @@ export function toContentJson(model: EditorModel): DefJson {
|
|||||||
* 条件分支(2026-09-24)无 JOIN 语义:分支尾不互连,隐式汇合到容器后继——cursor 置 null 后
|
* 条件分支(2026-09-24)无 JOIN 语义:分支尾不互连,隐式汇合到容器后继——cursor 置 null 后
|
||||||
* 下一个节点(或 END)只接收各分支尾的连线;firstEdgeName 只作用于分支首边(出口名)。
|
* 下一个节点(或 END)只接收各分支尾的连线;firstEdgeName 只作用于分支首边(出口名)。
|
||||||
* walk 返回"尾集合"(普通序列=末节点,末尾含条件分支=各分支尾)。
|
* walk 返回"尾集合"(普通序列=末节点,末尾含条件分支=各分支尾)。
|
||||||
|
* firstEdgeCondition(2026-10-10):EXPRESSION 模式分支首边的条件表达式,与出口名同位写入。
|
||||||
*/
|
*/
|
||||||
const walk = (items: EditorItem[], prev: string, firstEdgeName?: string): string[] => {
|
const walk = (items: EditorItem[], prev: string, firstEdgeName?: string,
|
||||||
|
firstEdgeCondition?: string): string[] => {
|
||||||
let cursor: string | null = prev;
|
let cursor: string | null = prev;
|
||||||
let pending: string[] = [];
|
let pending: string[] = [];
|
||||||
let first = true;
|
let first = true;
|
||||||
const linkTo = (toId: string): void => {
|
const linkTo = (toId: string): void => {
|
||||||
if (cursor !== null) {
|
if (cursor !== null) {
|
||||||
if (first && firstEdgeName) {
|
if (first && firstEdgeName) {
|
||||||
edges.push({ from: cursor, to: toId, name: firstEdgeName });
|
const e: DefEdgeJson = { from: cursor, to: toId, name: firstEdgeName };
|
||||||
|
if (firstEdgeCondition !== undefined) e.condition = firstEdgeCondition;
|
||||||
|
edges.push(e);
|
||||||
} else {
|
} else {
|
||||||
edges.push({ from: cursor, to: toId });
|
edges.push({ from: cursor, to: toId });
|
||||||
}
|
}
|
||||||
@@ -102,16 +106,21 @@ export function toContentJson(model: EditorModel): DefJson {
|
|||||||
}
|
}
|
||||||
if (item.kind === "condition") {
|
if (item.kind === "condition") {
|
||||||
const condId = `${item.uid}c`;
|
const condId = `${item.uid}c`;
|
||||||
nodes.push({
|
const node: DefNodeJson = {
|
||||||
id: condId,
|
id: condId,
|
||||||
type: "CONDITION",
|
type: "CONDITION",
|
||||||
name: item.name || "条件判断",
|
name: item.name || "条件判断",
|
||||||
routerKey: item.routerKey || "",
|
routerKey: item.routerKey || "",
|
||||||
});
|
};
|
||||||
|
// SERVICE 是后端缺省,不写字段保持最小 JSON;EXPRESSION 显式携带
|
||||||
|
if (item.judgeMode === "EXPRESSION") node.judgeMode = "EXPRESSION";
|
||||||
|
nodes.push(node);
|
||||||
linkTo(condId);
|
linkTo(condId);
|
||||||
const tails: string[] = [];
|
const tails: string[] = [];
|
||||||
|
const expression = item.judgeMode === "EXPRESSION";
|
||||||
item.branches.forEach((branch, bi) => {
|
item.branches.forEach((branch, bi) => {
|
||||||
tails.push(...walk(branch, condId, item.outlets[bi]));
|
const cond = expression ? (item.conditions || [])[bi] || "" : undefined;
|
||||||
|
tails.push(...walk(branch, condId, item.outlets[bi], cond));
|
||||||
});
|
});
|
||||||
cursor = null;
|
cursor = null;
|
||||||
pending = tails;
|
pending = tails;
|
||||||
@@ -311,12 +320,19 @@ export function fromContentJson(def: any): ParsedDef {
|
|||||||
// uid 复用 JSON id 并剥掉生成态的 "c" 尾缀:uid+"c" 是本编辑器的 JSON id 生成约定,
|
// uid 复用 JSON id 并剥掉生成态的 "c" 尾缀:uid+"c" 是本编辑器的 JSON id 生成约定,
|
||||||
// 剥尾保证"图→树→图→树→图"的 id 收敛(否则每轮追加一个 c,往返不幂等)
|
// 剥尾保证"图→树→图→树→图"的 id 收敛(否则每轮追加一个 c,往返不幂等)
|
||||||
const uidRaw = condId.endsWith("c") ? condId.slice(0, -1) : condId;
|
const uidRaw = condId.endsWith("c") ? condId.slice(0, -1) : condId;
|
||||||
|
// 出口条件反解(2026-10-10):EXPRESSION 模式出边 condition 与 outlets 同位
|
||||||
|
const conditions = heads.map((head) => {
|
||||||
|
const e = edges.find((x) => x.from === condId && x.to === head);
|
||||||
|
return e && e.condition ? String(e.condition) : "";
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
item: {
|
item: {
|
||||||
uid: UsedIds.take(uidRaw),
|
uid: UsedIds.take(uidRaw),
|
||||||
kind: "condition",
|
kind: "condition",
|
||||||
name: n.name || "条件判断",
|
name: n.name || "条件判断",
|
||||||
routerKey: n.routerKey || "",
|
routerKey: n.routerKey || "",
|
||||||
|
judgeMode: n.judgeMode === "EXPRESSION" ? "EXPRESSION" : "SERVICE",
|
||||||
|
conditions,
|
||||||
outlets,
|
outlets,
|
||||||
branches: branches.map((b) => (b.length ? b : [createApproval()])),
|
branches: branches.map((b) => (b.length ? b : [createApproval()])),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -57,11 +57,11 @@ const sourceStatus = ref(0);
|
|||||||
const sourceVersion = ref(0);
|
const sourceVersion = ref(0);
|
||||||
/** 归属应用(flw_def.app_key;按应用筛选维度,空=未分配) */
|
/** 归属应用(flw_def.app_key;按应用筛选维度,空=未分配) */
|
||||||
const appKey = ref("");
|
const appKey = ref("");
|
||||||
/** 应用清单下拉(本校已标注 ∪ 已注册回调),拉取失败降级为仅手输 */
|
/** 应用清单下拉(应用设置白名单 flw_app enabled=1,显示应用名),拉取失败降级为仅当前值 */
|
||||||
const appKeys = ref<string[]>([]);
|
const appOptions = ref<{ appKey: string; appName: string }[]>([]);
|
||||||
|
|
||||||
const model = reactive<EditorModel>(createEmptyModel());
|
const model = reactive<EditorModel>(createEmptyModel());
|
||||||
/** 科室映射聚合(mapList):仅用于下拉选项与"无启用办理人"提示 */
|
/** 科室列表(deptList 聚合行):仅用于下拉选项与"无启用人员"提示 */
|
||||||
const depts = reactive<DeptOption[]>([]);
|
const depts = reactive<DeptOption[]>([]);
|
||||||
/** userId → 姓名快照(mapLookupUser 检索补全,失败降级 #id) */
|
/** userId → 姓名快照(mapLookupUser 检索补全,失败降级 #id) */
|
||||||
const userNames = reactive<Record<number, string>>({});
|
const userNames = reactive<Record<number, string>>({});
|
||||||
@@ -120,20 +120,17 @@ const editingCondition = computed<ConditionNode | null>(() => {
|
|||||||
|
|
||||||
async function loadDepts(): Promise<void> {
|
async function loadDepts(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const rows: any[] = (await flowApi.mapList().send()) || [];
|
// 科室管理(2026-10-10 切源):deptList 返回聚合行 {deptKey,deptName,status,count}
|
||||||
const grouped: Record<string, DeptOption> = {};
|
const rows: any[] = (await flowApi.deptList().send()) || [];
|
||||||
rows.forEach((r) => {
|
depts.splice(0, depts.length, ...rows.map((r) => ({
|
||||||
const key = r.deptKey;
|
deptKey: String(r.deptKey),
|
||||||
if (!key) return;
|
deptName: r.deptName || "",
|
||||||
if (!grouped[key]) grouped[key] = { deptKey: key, deptName: r.deptName || "", count: 0 };
|
// 停用科室不进编辑器下拉(与发布校验口径一致:解析不出办理人)
|
||||||
if (!grouped[key].deptName && r.deptName) grouped[key].deptName = r.deptName;
|
count: r.status === 0 ? r.count : 0,
|
||||||
// 状态 0=启用(与办理人映射页口径一致),发布校验要求至少 1 名启用办理人
|
})));
|
||||||
if (r.status === 0) grouped[key].count += 1;
|
|
||||||
});
|
|
||||||
depts.splice(0, depts.length, ...Object.values(grouped));
|
|
||||||
} catch {
|
} catch {
|
||||||
// 映射表拉取失败不阻塞编辑:审批人下拉退化为"仅当前值",校验降级为提示
|
// 科室拉取失败不阻塞编辑:审批人下拉退化为"仅当前值",校验降级为提示
|
||||||
Message.warning("办理人映射拉取失败,科室下拉可能不完整");
|
Message.warning("科室数据拉取失败,科室下拉可能不完整");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,9 +174,12 @@ async function load(): Promise<void> {
|
|||||||
await loadDepts();
|
await loadDepts();
|
||||||
await loadRouters();
|
await loadRouters();
|
||||||
try {
|
try {
|
||||||
appKeys.value = ((await flowApi.defAppKeys().send()) as string[]) || [];
|
const rows: any[] = (await flowApi.appList().send()) || [];
|
||||||
|
appOptions.value = rows
|
||||||
|
.filter((r) => r.enabled === 1)
|
||||||
|
.map((r) => ({ appKey: String(r.appKey), appName: r.appName || String(r.appKey) }));
|
||||||
} catch {
|
} catch {
|
||||||
appKeys.value = [];
|
appOptions.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryId = route.query.id ? Number(route.query.id) : null;
|
const queryId = route.query.id ? Number(route.query.id) : null;
|
||||||
@@ -197,6 +197,10 @@ async function load(): Promise<void> {
|
|||||||
const d: any = await flowApi.defGet(queryId).send();
|
const d: any = await flowApi.defGet(queryId).send();
|
||||||
sourceStatus.value = d?.status ?? 0;
|
sourceStatus.value = d?.status ?? 0;
|
||||||
sourceVersion.value = d?.version ?? 0;
|
sourceVersion.value = d?.version ?? 0;
|
||||||
|
// 非草稿进入提示(2026-10-10 议题2):结构改动保存将升版本;仅改名称/归属应用请回列表用「编辑信息」
|
||||||
|
if (d?.status === 1 || d?.status === 2) {
|
||||||
|
Message.info("结构改动保存将升版本;仅改名称/归属应用请返回列表用「编辑信息」(不升版本)");
|
||||||
|
}
|
||||||
model.code = d?.code || "";
|
model.code = d?.code || "";
|
||||||
model.name = d?.name || "";
|
model.name = d?.name || "";
|
||||||
appKey.value = d?.appKey || "";
|
appKey.value = d?.appKey || "";
|
||||||
@@ -287,6 +291,11 @@ function addBranch(containerUid: string): void {
|
|||||||
const no = item.branches.length + 1;
|
const no = item.branches.length + 1;
|
||||||
item.branches.push([createApproval()]);
|
item.branches.push([createApproval()]);
|
||||||
item.outlets.push(`出口${["一", "二", "三", "四", "五", "六"][no - 1] || no}`);
|
item.outlets.push(`出口${["一", "二", "三", "四", "五", "六"][no - 1] || no}`);
|
||||||
|
// EXPRESSION 模式条件与 outlets 同位(SERVICE 模式不写,序列化时不携带)
|
||||||
|
if (item.judgeMode === "EXPRESSION") {
|
||||||
|
item.conditions = item.conditions || [];
|
||||||
|
while (item.conditions.length < item.outlets.length) item.conditions.push("");
|
||||||
|
}
|
||||||
} else if (item.kind === "approval" && item.outlets && item.outlets.length >= 2) {
|
} else if (item.kind === "approval" && item.outlets && item.outlets.length >= 2) {
|
||||||
const no = (item.branches?.length || 0) + 1;
|
const no = (item.branches?.length || 0) + 1;
|
||||||
(item.branches = item.branches || []).push([createApproval()]);
|
(item.branches = item.branches || []).push([createApproval()]);
|
||||||
@@ -305,6 +314,7 @@ function removeBranch(containerUid: string, index: number): void {
|
|||||||
}
|
}
|
||||||
branches.splice(index, 1);
|
branches.splice(index, 1);
|
||||||
if (item.kind === "condition" || isOutlet) item.outlets!.splice(index, 1);
|
if (item.kind === "condition" || isOutlet) item.outlets!.splice(index, 1);
|
||||||
|
if (item.kind === "condition" && item.conditions) item.conditions.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 退出多出口:仅保留出口①泳道内容,替换为「本节点(去 outlets) + ①泳道节点序列」 */
|
/** 退出多出口:仅保留出口①泳道内容,替换为「本节点(去 outlets) + ①泳道节点序列」 */
|
||||||
@@ -478,9 +488,8 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
|||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
应用
|
应用
|
||||||
<Select v-model="appKey" placeholder="归属应用,可空" style="width: 150px"
|
<Select v-model="appKey" placeholder="归属应用,可空" style="width: 170px" clearable filterable>
|
||||||
clearable filterable allow-create>
|
<Option v-for="a in appOptions" :key="a.appKey" :value="a.appKey">{{ a.appName }}</Option>
|
||||||
<Option v-for="k in appKeys" :key="k" :value="k">{{ k }}</Option>
|
|
||||||
</Select>
|
</Select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,17 +49,25 @@ export interface ParallelNode {
|
|||||||
branches: EditorItem[][];
|
branches: EditorItem[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** CONDITION 判断方式(2026-10-10 四议题):SERVICE=外部判断服务(缺省)|EXPRESSION=内置表达式引擎 */
|
||||||
|
export type JudgeMode = "SERVICE" | "EXPRESSION";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 条件分支容器(2026-09-24 CONDITION):运行时同步调外部判断服务(flw_router,routerKey 寻址)
|
* 条件分支容器(2026-09-24 CONDITION):按返回出口名互斥选路——与并行的本质差异:无 JOIN 语义,
|
||||||
* 按返回出口名互斥选路——与并行的本质差异:无 JOIN 语义,分支尾隐式汇合到容器后继。
|
* 分支尾隐式汇合到容器后继。outlets[i] 即第 i 条分支首边的 name(判断接口的 outlets 契约),
|
||||||
* outlets[i] 即第 i 条分支首边的 name(判断接口的 outlets 契约),必须非空且唯一。
|
* 必须非空且唯一。判断方式二选一(2026-10-10):SERVICE 调 flw_router 注册的外部判断服务;
|
||||||
|
* EXPRESSION 用出边 condition 表达式(AviatorScript,按分支顺序取首个 true)。
|
||||||
*/
|
*/
|
||||||
export interface ConditionNode {
|
export interface ConditionNode {
|
||||||
uid: string;
|
uid: string;
|
||||||
kind: "condition";
|
kind: "condition";
|
||||||
name: string;
|
name: string;
|
||||||
/** 判断服务注册键(flw_router.router_key,发布校验须已注册且启用) */
|
/** 判断服务注册键(flw_router.router_key,SERVICE 模式发布校验须已注册且启用) */
|
||||||
routerKey: string;
|
routerKey: string;
|
||||||
|
/** 判断方式:缺省 SERVICE(旧定义无此字段) */
|
||||||
|
judgeMode?: JudgeMode;
|
||||||
|
/** EXPRESSION 模式各出口条件表达式(与 outlets 一一对应;SERVICE 模式无意义) */
|
||||||
|
conditions?: string[];
|
||||||
/** 各分支出口名(与 branches 一一对应) */
|
/** 各分支出口名(与 branches 一一对应) */
|
||||||
outlets: string[];
|
outlets: string[];
|
||||||
branches: EditorItem[][];
|
branches: EditorItem[][];
|
||||||
@@ -104,6 +112,8 @@ export interface DefNodeJson {
|
|||||||
receivers?: Assignee;
|
receivers?: Assignee;
|
||||||
/** CONDITION 节点的判断服务注册键(flw_router.router_key) */
|
/** CONDITION 节点的判断服务注册键(flw_router.router_key) */
|
||||||
routerKey?: string;
|
routerKey?: string;
|
||||||
|
/** CONDITION 判断方式:SERVICE(缺省)|EXPRESSION(出边 condition 表达式) */
|
||||||
|
judgeMode?: string;
|
||||||
countersign?: Countersign;
|
countersign?: Countersign;
|
||||||
waitFor?: string;
|
waitFor?: string;
|
||||||
deadlineHours?: number;
|
deadlineHours?: number;
|
||||||
@@ -113,6 +123,8 @@ export interface DefEdgeJson {
|
|||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
/** CONDITION EXPRESSION 模式的出口条件表达式(AviatorScript;SERVICE 模式不携带) */
|
||||||
|
condition?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DefJson {
|
export interface DefJson {
|
||||||
@@ -165,13 +177,15 @@ export function createParallel(): ParallelNode {
|
|||||||
return { uid: newUid(), kind: "parallel", branches: [[createApproval()], [createApproval()]] };
|
return { uid: newUid(), kind: "parallel", branches: [[createApproval()], [createApproval()]] };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 条件分支:默认 2 分支占位出口名,插入后弹配置抽屉选判断服务 */
|
/** 条件分支:默认 2 分支占位出口名,插入后弹配置抽屉选判断方式(缺省服务) */
|
||||||
export function createCondition(): ConditionNode {
|
export function createCondition(): ConditionNode {
|
||||||
return {
|
return {
|
||||||
uid: newUid(),
|
uid: newUid(),
|
||||||
kind: "condition",
|
kind: "condition",
|
||||||
name: "条件判断",
|
name: "条件判断",
|
||||||
routerKey: "",
|
routerKey: "",
|
||||||
|
judgeMode: "SERVICE",
|
||||||
|
conditions: ["", ""],
|
||||||
outlets: ["出口一", "出口二"],
|
outlets: ["出口一", "出口二"],
|
||||||
branches: [[createApproval()], [createApproval()]],
|
branches: [[createApproval()], [createApproval()]],
|
||||||
};
|
};
|
||||||
@@ -293,7 +307,7 @@ export interface DeptOption {
|
|||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 审批人摘要文案(卡片第二行;deptOptions 来自 mapList,userNames 为 userId→姓名 快照) */
|
/** 审批人摘要文案(卡片第二行;deptOptions 来自科室管理列表,userNames 为 userId→姓名 快照) */
|
||||||
export function assigneeText(a: Assignee | null, depts: DeptOption[], userNames: Record<number, string>): string {
|
export function assigneeText(a: Assignee | null, depts: DeptOption[], userNames: Record<number, string>): string {
|
||||||
if (!a) return "未配置审批人";
|
if (!a) return "未配置审批人";
|
||||||
if (a.type === "DEPT_KEY") {
|
if (a.type === "DEPT_KEY") {
|
||||||
|
|||||||
@@ -103,9 +103,19 @@ export function validateModel(model: EditorModel, depts: DeptOption[], routers:
|
|||||||
|
|
||||||
if (item.kind === "condition") {
|
if (item.kind === "condition") {
|
||||||
// 条件分支(2026-09-24):判断服务必选(未注册仅 warn,发布期后端权威拦截);
|
// 条件分支(2026-09-24):判断服务必选(未注册仅 warn,发布期后端权威拦截);
|
||||||
// 出口名即判断接口 outlets 契约,非空唯一(镜像后端 CONDITION 校验口径)
|
// 出口名即判断接口 outlets 契约,非空唯一(镜像后端 CONDITION 校验口径)。
|
||||||
|
// EXPRESSION(2026-10-10):每出口条件非空(语法后端拦,前端不重复实现)
|
||||||
if (!item.name.trim()) push(item.uid, "error", path, "节点名称不能为空");
|
if (!item.name.trim()) push(item.uid, "error", path, "节点名称不能为空");
|
||||||
if (!item.routerKey) {
|
const expression = item.judgeMode === "EXPRESSION";
|
||||||
|
if (expression) {
|
||||||
|
item.outlets.forEach((o, i) => {
|
||||||
|
const cond = (item.conditions || [])[i] || "";
|
||||||
|
if (!cond.trim()) {
|
||||||
|
push(item.uid, "error", path,
|
||||||
|
`出口「${o || i + 1}」未配条件表达式(表达式模式下每个出口必须配置,发布会被拦截)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (!item.routerKey) {
|
||||||
push(item.uid, "error", path, "未选择判断服务(发布后运行到该节点会挂起)");
|
push(item.uid, "error", path, "未选择判断服务(发布后运行到该节点会挂起)");
|
||||||
} else if (routers.length && !routers.some((r) => r.routerKey === item.routerKey && r.status === 1)) {
|
} else if (routers.length && !routers.some((r) => r.routerKey === item.routerKey && r.status === 1)) {
|
||||||
push(item.uid, "warn", path, `判断服务「${item.routerKey}」未注册或已停用,发布会被拦截`);
|
push(item.uid, "warn", path, `判断服务「${item.routerKey}」未注册或已停用,发布会被拦截`);
|
||||||
|
|||||||
Reference in New Issue
Block a user