build / build (push) Successful in 3m21s
Vue3 + Vite + view-ui-plus 前端源码,含流程工作台、实例详情、 任务处理、管理端(定义/实例/映射/回调)等页面。 保留远程 .gitea/workflows/build.yml CI 配置。
187 lines
8.5 KiB
Vue
187 lines
8.5 KiB
Vue
<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>
|