feat(callback/app): 回调管理增强——注册操作列加「探测」(width 140→240 修复挤压溢出)+探测结果弹窗;推送日志加详情抽屉(完整 payload pretty JSON+响应摘要+重试链);应用管理加搜索框+回调注册数列+停用二次确认
build / build (push) Successful in 2m1s
build / build (push) Successful in 2m1s
This commit is contained in:
@@ -137,6 +137,10 @@ class FlowApi {
|
||||
appEnable(id: number | string, enabled: boolean) {
|
||||
return post(`${ROOT}/admin/app/enable`, { id, enabled });
|
||||
}
|
||||
/** 应用辅助统计:appKey → 回调注册数(本校) */
|
||||
appStats() {
|
||||
return get(`${ROOT}/admin/app/stats`);
|
||||
}
|
||||
defGet(id: number | string) {
|
||||
return get(`${ROOT}/admin/def/get`, { id });
|
||||
}
|
||||
@@ -223,6 +227,10 @@ class FlowApi {
|
||||
regResetSecret(id: number | string) {
|
||||
return post(`${ROOT}/admin/callback/resetSecret/${id}`);
|
||||
}
|
||||
/** 回调探测:对该注册同步推一条 flow.ping(未知事件码),返回 {success,httpStatus,elapsedMs,respSnippet,error} */
|
||||
callbackProbe(id: number | string) {
|
||||
return post(`${ROOT}/admin/callback/probe/${id}`);
|
||||
}
|
||||
callbackLogs(params: { status?: number; event?: string; instanceId?: number; page?: number; size?: number }) {
|
||||
return get(`${ROOT}/admin/callback/logs`, params);
|
||||
}
|
||||
|
||||
@@ -15,13 +15,29 @@ const tab = ref("apps");
|
||||
// ==================== tab1 应用管理 ====================
|
||||
const loading = ref(false);
|
||||
const rows = ref<any[]>([]);
|
||||
const searchKey = ref("");
|
||||
const regStats = ref<Record<string, number>>({});
|
||||
const appRegionRef = ref<HTMLElement | null>(null);
|
||||
const appH = useElementHeight(appRegionRef);
|
||||
|
||||
// 搜索过滤(应用名/appKey 子串,纯前端)
|
||||
const filteredRows = () => {
|
||||
const k = searchKey.value.trim().toLowerCase();
|
||||
if (!k) return rows.value;
|
||||
return rows.value.filter((r: any) =>
|
||||
String(r.appName || "").toLowerCase().includes(k) || String(r.appKey || "").includes(k));
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ title: "应用", key: "appName", minWidth: 200 },
|
||||
{ title: "appKey", key: "appKey", width: 120, align: "center" },
|
||||
{
|
||||
title: "回调注册数",
|
||||
width: 110,
|
||||
align: "center",
|
||||
render: (h: any, p: any) => h("span", String(regStats.value[String(p.row.appKey)] ?? 0)),
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
width: 90,
|
||||
@@ -43,12 +59,18 @@ async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
rows.value = ((await flowApi.appList().send()) as any[]) || [];
|
||||
try {
|
||||
regStats.value = ((await flowApi.appStats().send()) as any) || {};
|
||||
} catch (e) {
|
||||
regStats.value = {};
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggle(row: any, enabled: boolean) {
|
||||
const doToggle = async () => {
|
||||
row._switching = true;
|
||||
try {
|
||||
await flowApi.appEnable(row.id, enabled).send();
|
||||
@@ -60,6 +82,19 @@ async function onToggle(row: any, enabled: boolean) {
|
||||
} finally {
|
||||
row._switching = false;
|
||||
}
|
||||
};
|
||||
if (enabled) {
|
||||
doToggle();
|
||||
return;
|
||||
}
|
||||
MyConfirm({
|
||||
title: "停用应用",
|
||||
msg: `确定停用「${row.appName}(${row.appKey})」吗?`,
|
||||
info: "停用后新流程定义不能再归属到该应用(编辑器归属下拉与 defSave 校验拦截);存量定义不受影响",
|
||||
async onOk() {
|
||||
await doToggle();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== tab2 科室管理 ====================
|
||||
@@ -322,13 +357,15 @@ onMounted(() => {
|
||||
<div class="tab-body" v-show="tab === 'apps'">
|
||||
<div class="fix mb-12">
|
||||
<Button class="fl" type="primary" size="large" @click="load">刷新(同步学校池)</Button>
|
||||
<Input class="fl" v-model="searchKey" placeholder="搜索应用名 / appKey" clearable
|
||||
style="width: 220px; margin-left: 10px" suffix="ios-search" />
|
||||
<span class="fl" style="color: #808695; font-size: 12px; line-height: 40px; margin-left: 10px">
|
||||
应用清单来自本校已开通应用(UserBase 学校组开通,含学校别名);开启后可用于流程归属与筛选。
|
||||
新开通/改名在 UserBase 管理端操作后回本页刷新即可
|
||||
</span>
|
||||
</div>
|
||||
<div class="table-region" ref="appRegionRef">
|
||||
<Table :columns="columns" :data="rows" :loading="loading" class="link-table table-bottom-box"
|
||||
<Table :columns="columns" :data="filteredRows()" :loading="loading" class="link-table table-bottom-box"
|
||||
:height="appH || undefined" no-data-text="本校暂无已开通应用(请先在用户中心开通)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,11 +33,12 @@ const regColumns = [
|
||||
{ title: "状态", width: 80, align: "center", render: (h: any, p: any) => h(Tag, { color: p.row.status === 0 ? "green" : "gray" }, () => (p.row.status === 0 ? "启用" : "停用")) },
|
||||
actionColumn(
|
||||
[
|
||||
{ key: "probe", label: "探测", icon: "icon-fasong", on: (r: any) => onProbe(r) },
|
||||
{ key: "disable", label: "停用", icon: "icon-stop", on: (r: any) => onRegStatus(r, true), disabledWhen: (r: any) => r.status !== 0, danger: true },
|
||||
{ key: "enable", label: "启用", icon: "icon-shiyong", on: (r: any) => onRegStatus(r, false), disabledWhen: (r: any) => r.status === 0 },
|
||||
{ key: "resetSecret", label: "重置secret", icon: "icon-shezhi", on: (r: any) => onResetSecret(r) },
|
||||
],
|
||||
{ mode: "text", width: 140, fixed: "right" },
|
||||
{ mode: "text", width: 240, fixed: "right" },
|
||||
),
|
||||
];
|
||||
|
||||
@@ -57,8 +58,27 @@ const logColumns = [
|
||||
{ title: "重试", key: "retryCount", width: 60, align: "center" },
|
||||
{ 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", " ") || "-") },
|
||||
actionColumn([{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => openLogDetail(r) }], { mode: "text", width: 100, fixed: "right" }),
|
||||
];
|
||||
|
||||
// ---- 推送日志详情抽屉(2026-10-11:完整请求体/响应摘要/重试链可视) ----
|
||||
const logDetail = ref(false);
|
||||
const logDetailRow = ref<any>(null);
|
||||
|
||||
function openLogDetail(row: any) {
|
||||
logDetailRow.value = row;
|
||||
logDetail.value = true;
|
||||
}
|
||||
|
||||
function prettyJson(raw: any): string {
|
||||
if (raw === null || raw === undefined || raw === "") return "-";
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(raw), null, 2);
|
||||
} catch (e) {
|
||||
return String(raw);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 挂起列表(持续重试中:待推且重试达阈值;人工判定无意义 → 标记死信止血) ----
|
||||
const hungs = ref<any[]>([]);
|
||||
const hungLoading = ref(false);
|
||||
@@ -133,6 +153,22 @@ function onResetSecret(row: any) {
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 回调探测(2026-10-11):签一条 flow.ping 走真实管道,结果弹窗展示并落推送日志 ----
|
||||
const probeResult = ref<any>(null);
|
||||
const probeShowing = ref(false);
|
||||
const probingId = ref<number | null>(null);
|
||||
|
||||
async function onProbe(row: any) {
|
||||
probingId.value = row.id;
|
||||
try {
|
||||
const d: any = await flowApi.callbackProbe(row.id).send();
|
||||
probeResult.value = { ...d, appKey: row.appKey, callbackUrl: row.callbackUrl };
|
||||
probeShowing.value = true;
|
||||
} finally {
|
||||
probingId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onMarkDead(row: any) {
|
||||
MyConfirm({
|
||||
title: "标记死信",
|
||||
@@ -224,9 +260,49 @@ onMounted(loadRegs);
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回调探测结果 -->
|
||||
<Modal v-model="probeShowing" :title="`回调探测${probeResult?.success ? '成功' : '失败'}(${probeResult?.appKey})`"
|
||||
:width="640" :footer-hide="true">
|
||||
<div v-if="probeResult" class="probe-result">
|
||||
<Alert :type="probeResult.success ? 'success' : 'error'" show-icon>
|
||||
{{ probeResult.success ? "接收方可达且验签通过(HTTP 2xx)" : "探测失败:接收方不可达或验签被拒" }}
|
||||
</Alert>
|
||||
<div class="kv"><span class="k">回调地址</span><span class="v">{{ probeResult.callbackUrl }}</span></div>
|
||||
<div class="kv"><span class="k">HTTP 状态</span><span class="v">{{ probeResult.httpStatus ?? "-" }}</span></div>
|
||||
<div class="kv"><span class="k">耗时</span><span class="v">{{ probeResult.elapsedMs }} ms</span></div>
|
||||
<div class="kv" v-if="probeResult.error"><span class="k">错误</span><span class="v text-error">{{ probeResult.error }}</span></div>
|
||||
<div class="kv" v-if="probeResult.respSnippet">
|
||||
<span class="k">响应摘要</span>
|
||||
<pre class="v pre">{{ probeResult.respSnippet }}</pre>
|
||||
</div>
|
||||
<div class="tip" style="margin-top:10px">探测事件 flow.ping 已落「推送日志」(探测结果以接收方按规范记日志后 200 为准)</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- 推送日志详情抽屉 -->
|
||||
<Drawer :title="`推送详情:${logDetailRow?.event || ''}`" :value="logDetail" :width="560" class="log-detail-drawer">
|
||||
<div v-if="logDetailRow" class="log-detail">
|
||||
<div class="kv"><span class="k">事件ID</span><span class="v">{{ logDetailRow.eventId }}</span></div>
|
||||
<div class="kv"><span class="k">办件 / biz</span><span class="v">{{ logDetailRow.instanceId }} / {{ logDetailRow.bizType }}/{{ logDetailRow.bizId }}</span></div>
|
||||
<div class="kv"><span class="k">标题</span><span class="v">{{ logDetailRow.bizTitle || "-" }}</span></div>
|
||||
<div class="kv"><span class="k">状态</span><span class="v">{{ LOG_STATUS[logDetailRow.status] }}</span></div>
|
||||
<div class="kv"><span class="k">重试次数</span><span class="v">{{ logDetailRow.retryCount }}</span></div>
|
||||
<div class="kv"><span class="k">末次 HTTP</span><span class="v">{{ logDetailRow.lastHttpStatus || "-" }}</span></div>
|
||||
<div class="kv"><span class="k">末次错误</span><span class="v">{{ logDetailRow.lastError || "-" }}</span></div>
|
||||
<div class="kv"><span class="k">推送时间</span><span class="v">{{ (logDetailRow.pushTime || "-").replace("T", " ") }}</span></div>
|
||||
<div class="kv"><span class="k">下次重推</span><span class="v">{{ (logDetailRow.nextRetryTime || "-").replace("T", " ") }}</span></div>
|
||||
<div class="kv"><span class="k">响应摘要</span><pre class="v pre">{{ logDetailRow.respBody || "-" }}</pre></div>
|
||||
<div class="kv"><span class="k">请求体(完整快照)</span><pre class="v pre">{{ prettyJson(logDetailRow.payloadJson) }}</pre></div>
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tip { margin-bottom: 10px; padding: 6px 10px; background: #f8f8f9; border-radius: 4px; color: #515a6e; font-size: 12px; }
|
||||
.probe-result .kv, .log-detail .kv { margin-bottom: 8px; font-size: 13px; }
|
||||
.probe-result .k, .log-detail .k { display: inline-block; width: 120px; color: #808695; vertical-align: top; }
|
||||
.probe-result .v, .log-detail .v { color: #515a6e; word-break: break-all; white-space: pre-wrap; }
|
||||
.pre { margin: 0; padding: 8px; background: #f8f8f9; border-radius: 4px; max-height: 320px; overflow: auto; font-size: 12px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user