feat: 提交流程中心前端代码
build / build (push) Successful in 3m21s

Vue3 + Vite + view-ui-plus 前端源码,含流程工作台、实例详情、
任务处理、管理端(定义/实例/映射/回调)等页面。
保留远程 .gitea/workflows/build.yml CI 配置。
This commit is contained in:
chenxing
2026-09-15 10:42:45 +08:00
parent ca30ff12cc
commit 90db00cf16
328 changed files with 25868 additions and 2 deletions
+186
View File
@@ -0,0 +1,186 @@
<script setup lang="ts">
// 回调管理(概览 §9.2):注册列表/启停/secret 重置 + 推送日志 + 挂起(持续重试可视/标记死信)+ 死信重推
import { ref, onMounted } from "vue";
import { Message, Modal } from "view-ui-plus";
import { flowApi } from "@/api/module/flow";
const LOG_STATUS: Record<number, string> = { 0: "待推/重试中", 1: "成功", 2: "死信" };
const EVENT_OPTIONS = ["flow.submitted", "flow.task.created", "flow.task.handled", "flow.completed", "flow.rejected", "flow.withdrawn"];
const tab = ref("regs");
const loading = ref(false);
// ---- 注册列表 ----
const regs = ref<any[]>([]);
const regColumns = [
{ title: "应用 appKey", key: "appKey", width: 110 },
{ title: "流程 GUID", key: "flowGuid", width: 210 },
{ title: "回调地址", key: "callbackUrl", minWidth: 240 },
{ title: "订阅事件", key: "eventTypes", width: 200, render: (h: any, p: any) => h("span", p.row.eventTypes || "全部") },
{ title: "状态", width: 80, render: (h: any, p: any) => h("Tag", { color: p.row.status === 0 ? "green" : "gray" }, () => (p.row.status === 0 ? "启用" : "停用")) },
{ title: "操作", width: 220, render: (h: any, p: any) => {
const btns: any[] = [
h("Button", { size: "small", onClick: () => onRegStatus(p.row, p.row.status === 0) }, () => (p.row.status === 0 ? "停用" : "启用")),
h("Button", { size: "small", type: "warning", onClick: () => onResetSecret(p.row) }, () => "重置secret"),
];
return h("div", { style: "display:flex;gap:4px" }, btns);
} },
];
// ---- 推送日志 ----
const logs = ref<any[]>([]);
const logPage = ref(1);
const logTotal = ref(0);
const logFilter = ref<any>({ status: null, event: "", instanceId: null });
const selectedLogIds = ref<number[]>([]);
const logColumns = [
{ type: "selection", width: 50 },
{ title: "事件", key: "event", width: 150 },
{ title: "办件", key: "instanceId", width: 80 },
{ title: "biz", width: 160, render: (h: any, p: any) => h("span", `${p.row.bizType}/${p.row.bizId}`) },
{ title: "状态", width: 110, render: (h: any, p: any) => {
const c = p.row.status === 1 ? "green" : p.row.status === 2 ? "red" : "orange";
return h("Tag", { color: c }, () => LOG_STATUS[p.row.status]);
} },
{ title: "重试", key: "retryCount", width: 60 },
{ title: "末次结果", key: "lastError", minWidth: 200, render: (h: any, p: any) => h("span", p.row.lastError || p.row.lastHttpStatus || "-") },
{ title: "下次重推", width: 160, render: (h: any, p: any) => h("span", (p.row.nextRetryTime || "").replace("T", " ") || "-") },
];
// ---- 挂起列表(持续重试中:待推且重试达阈值;人工判定无意义 → 标记死信止血) ----
const hungs = ref<any[]>([]);
const hungPage = ref(1);
const hungTotal = ref(0);
const hungLoading = ref(false);
const hungColumns = [
{ title: "事件", key: "event", width: 150 },
{ title: "办件", key: "instanceId", width: 80 },
{ title: "biz", width: 160, render: (h: any, p: any) => h("span", `${p.row.bizType}/${p.row.bizId}`) },
{ title: "重试", key: "retryCount", width: 70, render: (h: any, p: any) => h("Tag", { color: "orange" }, () => `${p.row.retryCount} 次`) },
{ title: "末次结果", key: "lastError", minWidth: 220, render: (h: any, p: any) => h("span", p.row.lastError || p.row.lastHttpStatus || "-") },
{ title: "下次重推", width: 160, render: (h: any, p: any) => h("span", (p.row.nextRetryTime || "").replace("T", " ") || "-") },
{ title: "操作", width: 120, render: (h: any, p: any) =>
h("Button", { size: "small", type: "error", onClick: () => onMarkDead(p.row) }, () => "标记死信") },
];
async function loadRegs() {
regs.value = (await flowApi.regList().send()) || [];
}
async function loadLogs() {
loading.value = true;
try {
const params: any = { page: logPage.value, size: 10 };
if (logFilter.value.status !== null && logFilter.value.status !== undefined) params.status = logFilter.value.status;
if (logFilter.value.event) params.event = logFilter.value.event;
if (logFilter.value.instanceId) params.instanceId = logFilter.value.instanceId;
const data: any = await flowApi.callbackLogs(params).send();
logs.value = data?.content || [];
logTotal.value = data?.total || 0;
} finally {
loading.value = false;
}
}
async function loadHungs() {
hungLoading.value = true;
try {
const data: any = await flowApi.callbackHung({ page: hungPage.value, size: 10 }).send();
hungs.value = data?.content || [];
hungTotal.value = data?.total || 0;
} finally {
hungLoading.value = false;
}
}
async function onRegStatus(row: any, disable: boolean) {
await flowApi.regStatus(row.id, !disable).send();
Message.success("已更新");
loadRegs();
}
async function onResetSecret(row: any) {
const d: any = await flowApi.regResetSecret(row.id).send();
Message.info({ content: `新 secret(仅本次展示):${d?.secret}`, duration: 8 });
loadRegs();
}
function onMarkDead(row: any) {
Modal.confirm({
title: "标记死信",
content: `办件 ${row.instanceId} 事件 ${row.event} 已持续重试 ${row.retryCount} 次。确认该事件已无意义、停止重试并转入死信?`,
onOk: async () => {
await flowApi.callbackMarkDead(row.id).send();
Message.success("已标记死信(可在死信列表重推复活)");
loadHungs();
},
});
}
function onSelectionChange(selection: any[]) {
selectedLogIds.value = selection.map((r) => r.id);
}
async function onRepush() {
if (!selectedLogIds.value.length) {
Message.warning("请先勾选日志行");
return;
}
await flowApi.callbackRepush(selectedLogIds.value).send();
Message.success("已触发重推");
loadLogs();
}
function switchTab(name: string) {
tab.value = name;
if (name === "logs") loadLogs();
if (name === "hung") loadHungs();
}
onMounted(loadRegs);
</script>
<template>
<div class="page">
<Tabs :value="tab" @on-click="switchTab">
<TabPane label="回调注册" name="regs">
<Table :columns="regColumns" :data="regs" :loading="loading" stripe />
</TabPane>
<TabPane label="推送日志" name="logs">
<div class="filter">
<Select v-model="logFilter.status" clearable placeholder="状态" style="width: 130px">
<Option :value="0" label="待推/重试中" />
<Option :value="1" label="成功" />
<Option :value="2" label="死信" />
</Select>
<Select v-model="logFilter.event" clearable placeholder="事件" style="width: 190px">
<Option v-for="e in EVENT_OPTIONS" :key="e" :value="e" :label="e" />
</Select>
<Input v-model="logFilter.instanceId" placeholder="办件ID" style="width: 120px" clearable />
<Button @click="logPage = 1; loadLogs()">查询</Button>
<Button type="primary" @click="onRepush">重推选中</Button>
</div>
<Table :columns="logColumns" :data="logs" :loading="loading" stripe @on-selection-change="onSelectionChange" />
<div class="pager">
<Page :total="logTotal" :current="logPage" :page-size="10" show-total @on-change="(p: number) => { logPage = p; loadLogs(); }" />
</div>
</TabPane>
<TabPane label="挂起中" name="hung">
<div class="tip">
持续重试中的事件(消费方疑似停机;消费方恢复后自动追平,无需干预)。仅当人工判定事件已无意义时才标记死信。
</div>
<Table :columns="hungColumns" :data="hungs" :loading="hungLoading" stripe />
<div class="pager">
<Page :total="hungTotal" :current="hungPage" :page-size="10" show-total @on-change="(p: number) => { hungPage = p; loadHungs(); }" />
</div>
</TabPane>
</Tabs>
</div>
</template>
<style scoped>
.page { padding: 12px 16px; }
.filter { margin-bottom: 10px; display: flex; gap: 8px; }
.pager { margin-top: 12px; text-align: right; }
.tip { margin-bottom: 10px; padding: 6px 10px; background: #f8f8f9; border-radius: 4px; color: #515a6e; font-size: 12px; }
</style>
+175
View File
@@ -0,0 +1,175 @@
<script setup lang="ts">
// 流程定义管理(M1:JSON 文本编辑模式;发布跑静态校验,把"任务无主"拦在发布期)
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { Message } from "view-ui-plus";
import { flowApi } from "@/api/module/flow";
const router = useRouter();
const DEF_STATUS: Record<number, string> = { 0: "草稿", 1: "已发布", 2: "停用" };
const loading = ref(false);
const rows = ref<any[]>([]);
const editVisible = ref(false);
const saving = ref(false);
const form = ref<any>({ id: null, code: "", name: "", contentJson: "" });
const versionVisible = ref(false);
const versions = ref<any[]>([]);
const SAMPLE_JSON = `{
"code": "sample_flow",
"name": "示例流程",
"start": "start",
"nodes": [
{"id": "start", "type": "START", "name": "开始"},
{"id": "n1", "type": "APPROVAL", "name": "科室审核",
"assignee": {"type": "DEPT_KEY", "deptKey": "ybb"}, "countersign": "ALL"},
{"id": "end", "type": "END"}
],
"edges": [
{"from": "start", "to": "n1"},
{"from": "n1", "to": "end"}
],
"settings": {"rejectPolicy": "END_REJECTED", "urge": {"enabled": true, "intervalHours": 24, "maxTimes": 3}}
}`;
const columns = [
{ title: "流程", key: "name", minWidth: 180 },
{ title: "编码", key: "code", width: 160 },
{ title: "版本", key: "version", width: 70 },
{ title: "状态", width: 90, render: (h: any, p: any) => {
const c = p.row.status === 1 ? "green" : p.row.status === 0 ? "orange" : "gray";
return h("Tag", { color: c }, () => DEF_STATUS[p.row.status] || p.row.status);
} },
{ title: "GUID", key: "flowGuid", width: 200 },
{ title: "发布时间", width: 160, render: (h: any, p: any) => h("span", (p.row.publishTime || "").replace("T", " ") || "-") },
{ title: "操作", width: 330, render: (h: any, p: any) => {
const btns: any[] = [];
if (p.row.status === 0) {
btns.push(h("Button", { size: "small", type: "primary", onClick: () => onEdit(p.row) }, () => "编辑"));
btns.push(h("Button", { size: "small", type: "success", onClick: () => onPublish(p.row) }, () => "发布"));
} else if (p.row.status === 1) {
btns.push(h("Button", { size: "small", onClick: () => onNewVersion(p.row) }, () => "升版本"));
btns.push(h("Button", { size: "small", type: "warning", onClick: () => onOffline(p.row) }, () => "停用"));
}
btns.push(h("Button", { size: "small", onClick: () => onDiagram(p.row) }, () => "图示"));
btns.push(h("Button", { size: "small", onClick: () => onVersions(p.row) }, () => "版本历史"));
return h("div", { style: "display:flex;gap:4px" }, btns);
} },
];
async function load() {
loading.value = true;
try {
rows.value = (await flowApi.defList().send()) || [];
} finally {
loading.value = false;
}
}
function onEdit(row: any) {
flowApi.defGet(row.id).send().then((d: any) => {
form.value = { id: d.id, code: d.code, name: d.name, contentJson: d.contentJson };
editVisible.value = true;
});
}
function onNew() {
form.value = { id: null, code: "", name: "", contentJson: SAMPLE_JSON };
editVisible.value = true;
}
async function onNewVersion(row: any) {
// 升版本:以已发布内容为底稿建草稿(flowGuid 不变,version 自动 +1)
const d: any = await flowApi.defGet(row.id).send();
form.value = { id: null, code: row.code, name: row.name, contentJson: d.contentJson, baseGuid: row.flowGuid };
Message.info("已载入当前版本内容,保存即创建新草稿版本");
editVisible.value = true;
}
async function onSave() {
saving.value = true;
try {
await flowApi.defSave({
id: form.value.id || undefined,
code: form.value.code,
name: form.value.name,
contentJson: form.value.contentJson,
}).send();
Message.success("已保存");
editVisible.value = false;
load();
} finally {
saving.value = false;
}
}
async function onPublish(row: any) {
try {
await flowApi.defPublish(row.id).send();
Message.success("发布成功");
load();
} catch (e: any) {
Message.error(e?.message || "发布校验失败");
}
}
async function onOffline(row: any) {
await flowApi.defOffline(row.id).send();
Message.success("已停用");
load();
}
async function onVersions(row: any) {
versions.value = (await flowApi.defVersions(row.flowGuid).send()) || [];
versionVisible.value = true;
}
function onDiagram(row: any) {
router.push({ path: "/diagram", query: { defId: row.id, mode: "auto" } });
}
onMounted(load);
</script>
<template>
<div class="page">
<div class="filter">
<Button type="primary" @click="onNew">新建流程</Button>
</div>
<Table :columns="columns" :data="rows" :loading="loading" stripe />
<Modal v-model="editVisible" :title="form.id ? '编辑流程' : '新建流程'" width="760" :mask-closable="false">
<Form :label-width="80">
<FormItem label="编码">
<Input v-model="form.code" :disabled="!!form.id" placeholder="流程编码(内部管理键,发布后不可改)" />
</FormItem>
<FormItem label="名称">
<Input v-model="form.name" placeholder="流程名" />
</FormItem>
<FormItem label="定义 JSON">
<Input v-model="form.contentJson" type="textarea" :rows="16"
placeholder="§5.1 流程定义 JSON(START/END 唯一、无孤儿无环、DEPT_KEY 需先配好映射)" />
</FormItem>
</Form>
<template #footer>
<Button @click="editVisible = false">取消</Button>
<Button type="primary" :loading="saving" @click="onSave">保存(草稿)</Button>
</template>
</Modal>
<Modal v-model="versionVisible" title="版本历史" width="640">
<Table :columns="[
{ title: '版本', key: 'version', width: 80 },
{ title: '名称', key: 'name' },
{ title: '状态', width: 90, render: (h, p) => h('Tag', () => DEF_STATUS[p.row.status]) },
{ title: '发布时间', width: 160, render: (h, p) => (p.row.publishTime || '').replace('T', ' ') || '-' },
]" :data="versions" size="small" />
</Modal>
</div>
</template>
<style scoped>
.page { padding: 12px 16px; }
.filter { margin-bottom: 10px; }
</style>
+67
View File
@@ -0,0 +1,67 @@
<script setup lang="ts">
// 办件记录(管理端):全校办件查询
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { flowApi, INSTANCE_STATUS } from "@/api/module/flow";
const router = useRouter();
const loading = ref(false);
const page = ref(1);
const total = ref(0);
const filter = ref<any>({ status: null, keyword: "" });
const rows = ref<any[]>([]);
const columns = [
{ title: "办件标题", key: "bizTitle", minWidth: 220 },
{ title: "业务", width: 170, render: (h: any, p: any) => h("span", `${p.row.bizType || "-"}/${p.row.bizId || "-"}`) },
{ title: "状态", width: 100, render: (h: any, p: any) => {
const c = p.row.status === 10 ? "blue" : p.row.status === 20 ? "green" : p.row.status === 30 ? "red" : "gray";
return h("Tag", { color: c }, () => INSTANCE_STATUS[p.row.status] || p.row.status);
} },
{ title: "提交人", key: "submitterName", width: 100 },
{ title: "提交时间", width: 160, render: (h: any, p: any) => h("span", (p.row.submitTime || "").replace("T", " ")) },
{ title: "操作", width: 90, render: (h: any, p: any) =>
h("Button", { size: "small", onClick: () => router.push(`/instance/${p.row.id}`) }, () => "详情") },
];
async function load() {
loading.value = true;
try {
const params: any = { page: page.value, size: 10 };
if (filter.value.status !== null && filter.value.status !== undefined) params.status = filter.value.status;
if (filter.value.keyword) params.keyword = filter.value.keyword;
const data: any = await flowApi.adminInstances(params).send();
rows.value = data?.content || [];
total.value = data?.total || 0;
} finally {
loading.value = false;
}
}
onMounted(load);
</script>
<template>
<div class="page">
<div class="filter">
<Select v-model="filter.status" clearable placeholder="状态" style="width: 130px">
<Option :value="10" label="审批中" />
<Option :value="20" label="已完成" />
<Option :value="30" label="已拒绝" />
<Option :value="40" label="已撤回" />
</Select>
<Input v-model="filter.keyword" placeholder="标题关键字" style="width: 200px" clearable @on-enter="load" />
<Button @click="page = 1; load()">查询</Button>
</div>
<Table :columns="columns" :data="rows" :loading="loading" stripe />
<div class="pager">
<Page :total="total" :current="page" :page-size="10" show-total @on-change="(p: number) => { page = p; load(); }" />
</div>
</div>
</template>
<style scoped>
.page { padding: 12px 16px; }
.filter { margin-bottom: 10px; display: flex; gap: 8px; }
.pager { margin-top: 12px; text-align: right; }
</style>
+125
View File
@@ -0,0 +1,125 @@
<script setup lang="ts">
// 办理人映射管理(概览 §4.7/§六):DEPT_KEY → 办理人;成员检索经 UserBase /oapi/user/byId 补姓名
import { ref, onMounted } from "vue";
import { Message } from "view-ui-plus";
import { flowApi } from "@/api/module/flow";
const loading = ref(false);
const deptKeyFilter = ref("");
const rows = ref<any[]>([]);
const saveVisible = ref(false);
const saving = ref(false);
const form = ref<any>({ deptKey: "", deptName: "", userId: null, userName: "" });
const lookingUp = ref(false);
const columns = [
{ title: "科室编码", key: "deptKey", width: 140 },
{ title: "科室名称", key: "deptName", 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, render: (h: any, p: any) => h("Tag", { color: p.row.status === 0 ? "green" : "gray" }, () => (p.row.status === 0 ? "启用" : "停用")) },
{ title: "操作", width: 100, render: (h: any, p: any) =>
p.row.status === 0
? h("Button", { size: "small", type: "warning", onClick: () => onDisable(p.row) }, () => "停用")
: h("span", "-") },
];
async function load() {
loading.value = true;
try {
rows.value = (await flowApi.mapList(deptKeyFilter.value || undefined).send()) || [];
} finally {
loading.value = false;
}
}
function onAdd() {
form.value = { deptKey: "", deptName: "", userId: null, userName: "" };
saveVisible.value = true;
}
async function onLookup() {
if (!form.value.userId) {
Message.warning("请先填用户ID");
return;
}
lookingUp.value = true;
try {
const u: any = await flowApi.mapLookupUser(form.value.userId).send();
form.value.userName = u?.name || "";
Message.success(`检索到用户:${u?.name || "-"}`);
} catch {
Message.error("UserBase 检索失败");
} finally {
lookingUp.value = false;
}
}
async function onSave() {
if (!form.value.deptKey.trim() || !form.value.userId) {
Message.warning("科室编码与用户ID必填");
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;
}
}
async function onDisable(row: any) {
await flowApi.mapDisable(row.id).send();
Message.success("已停用");
load();
}
onMounted(load);
</script>
<template>
<div class="page">
<div class="filter">
<Input v-model="deptKeyFilter" placeholder="按科室编码筛选" style="width: 200px" clearable @on-enter="load" />
<Button @click="load">查询</Button>
<Button type="primary" @click="onAdd">新增映射</Button>
</div>
<Table :columns="columns" :data="rows" :loading="loading" stripe />
<Modal v-model="saveVisible" title="新增/更新映射" :mask-closable="false">
<Form :label-width="90">
<FormItem label="科室编码">
<Input v-model="form.deptKey" placeholder="流程 JSON 中 DEPT_KEY 引用的编码,如 ybb" />
</FormItem>
<FormItem label="科室名称">
<Input v-model="form.deptName" placeholder="如 医保办(可空)" />
</FormItem>
<FormItem label="用户ID">
<Input v-model="form.userId" placeholder="UserBase 用户主键" />
</FormItem>
<FormItem label="姓名快照">
<Input v-model="form.userName" placeholder="可留空,点检索自动补全" style="width: 180px" />
<Button :loading="lookingUp" @click="onLookup" style="margin-left: 8px">检索</Button>
</FormItem>
</Form>
<template #footer>
<Button @click="saveVisible = false">取消</Button>
<Button type="primary" :loading="saving" @click="onSave">保存</Button>
</template>
</Modal>
</div>
</template>
<style scoped>
.page { padding: 12px 16px; }
.filter { margin-bottom: 10px; display: flex; gap: 8px; }
</style>