把子应用接入从「人背文档 + 逐字对齐字符串」变成「向导引导 + 机器生成正确配置 + 每步可自检」。 - Onboarding.vue:五步(应用档案 / 工程接入 / 回调注册 / 流程编排 / 联调验收),步骤条可任意跳转不强制线性 - STEP② 配置生成器为核心:按「端点形态(平台验签式/X-Flw 式)× 接入场景(新接入/存量升级)× ① 的 context-path/bizType」实时生成 yh.flow-callback 块、Maven 依赖、FlowCallbackHandler 骨架。X-Flw 式追加「端点第二段非 oapi、必须显式 callback-url 直连」警示;无 ctx 时 base-path 整行注释。对齐规范 §7.2 推导式 - 前置条件清单把散落文档里的前置变成可勾选待办;Redis 公共组件与 trust-registry 两条高危静默断点红底强调 - STEP③ 登记检查:私网/开发机地址、base_path 漏填、与声明的 target 不一致逐条判定给修复建议;「发一条 flow.ping」复用 callbackProbe - STEP④ 变量契约显性化:解析 contentJson 取 CONDITION 节点出边表达式,与已知变量(近 50 条办件 formData 聚合 + 内置 8 个,口径同编辑器)核对,引用从未出现的键即警示「运行时会挂起」 - STEP⑤ 六项自检 + 一键生成 markdown 自检报告(纯前端拼装不落库) 数据源全部复用已有接口(appList/regList/callbackProbe/defList/defGet/callbackLogs/adminInstances),零新增表、零新增写接口。路由 /admin/onboarding(modId 108,需 UserBase 配 app_module)。vite build 通过,产物已构建进 front/dist/。
This commit is contained in:
@@ -81,6 +81,15 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import("@/views/flow/admin/AppSetting.vue"),
|
||||
meta: { title: "应用设置", modId: 107 },
|
||||
},
|
||||
{
|
||||
// 接入向导(2026-10-15 议题二 P1):五步引导 + 配置生成器(源头消灭配置形态静默断点)。
|
||||
// 与「应用设置-对接健康度」互补:向导管接入时一次引导,健康度管对接后持续体检。
|
||||
// 菜单需在 UserBase 侧配置同 path 的 app_module(custom_id=108)
|
||||
path: "/admin/onboarding",
|
||||
name: "flow_admin_onboarding",
|
||||
component: () => import("@/views/flow/admin/Onboarding.vue"),
|
||||
meta: { title: "接入向导", modId: 108 },
|
||||
},
|
||||
{
|
||||
// 办件进度嵌入页(iframe 免签嵌入,App.vue 对 /embed/* 前缀旁路 CAS 与 layout):
|
||||
// 不挂菜单、无 parentId(子应用 Drawer 内打开,非管理面导航);query.token 必填
|
||||
|
||||
@@ -0,0 +1,741 @@
|
||||
<script setup lang="ts">
|
||||
// 接入向导(2026-10-15 议题二 P1):把子应用接入流程中心从「人背文档 + 逐字对齐字符串」变成
|
||||
// 「向导引导 + 机器生成正确配置 + 每步可自检」。五步:应用档案 → 工程接入 → 回调注册 → 流程编排 → 联调验收。
|
||||
// 数据源全部复用已有接口(appList / regList / callbackProbe / defList / defGet / callbackLogs / adminInstances),
|
||||
// 零新增表、零新增写接口。配置生成器对齐《子应用接入流程中心规范.md》§7.2/§7.5。
|
||||
// 方案文档:docs/流程中心接入向导与配置生成器-实施方案-20261015.md
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { Message, Tag } from "view-ui-plus";
|
||||
import { flowApi } from "@/api/module/flow";
|
||||
import Utils from "@/utils";
|
||||
import { extractExprVars, EXPR_BUILTIN_VARS } from "@/views/flow/admin/def-editor/model";
|
||||
|
||||
// ==================== 步骤骨架 ====================
|
||||
const STEPS = [
|
||||
{ title: "应用档案", hint: "我是谁:appKey / target / context-path / bizType" },
|
||||
{ title: "工程接入", hint: "生成正确形态的 yh.flow-callback 配置" },
|
||||
{ title: "回调注册", hint: "登记行存在且地址合规 + 探测" },
|
||||
{ title: "流程编排", hint: "流程定义 → 变量契约显性化" },
|
||||
{ title: "联调验收", hint: "六项自检 + 自检报告" },
|
||||
];
|
||||
const step = ref(0);
|
||||
|
||||
// ==================== STEP① 应用档案 ====================
|
||||
const apps = ref<any[]>([]);
|
||||
const appMap = ref<Record<string, string>>({});
|
||||
const s1 = ref({
|
||||
appKey: "",
|
||||
target: "",
|
||||
ctx: "",
|
||||
bizTypes: "",
|
||||
bindLevel: "app",
|
||||
});
|
||||
const BIND_LEVELS = [
|
||||
{ value: "app", label: "应用级", desc: "整个应用一种审批流(一校一行绑定表)" },
|
||||
{ value: "object", label: "对象级", desc: "每个业务对象各走各的流(对象行加 flow_guid 列)" },
|
||||
{ value: "form", label: "表单级", desc: "自定义表单统一审批路径,事项零配置继承" },
|
||||
];
|
||||
|
||||
// ==================== STEP② 工程接入(配置生成器) ====================
|
||||
const s2 = ref({
|
||||
// 端点形态:platform=平台验签式(式 A,推荐)|xflw=注册级 X-Flw 式(式 B)
|
||||
endpointMode: "platform" as "platform" | "xflw",
|
||||
// 接入场景:new=新接入|upgrade=存量升级(已接回调,升 starter 版本即可)
|
||||
scenario: "new" as "new" | "upgrade",
|
||||
configDone: false,
|
||||
});
|
||||
// 前置条件清单(规范 §7.1 + work-leave 四个静默断点);后两条高危,红底强调
|
||||
const PREREQS = [
|
||||
{ key: "openapi", label: "已配 yh-openapi-spring-boot-starter", danger: false, tip: "Registrar 自注册用" },
|
||||
{ key: "appauth", label: "平台验签式:已配 yh.app-auth.server.enforce-patterns: [/oapi/*]", danger: false, tip: "式 A 的验签过滤器" },
|
||||
{ key: "datasource", label: "应用有 JDBC DataSource", danger: false, tip: "幂等表/事件表落地" },
|
||||
{ key: "redis", label: "projman 已勾选 Redis 公共组件", danger: true, tip: "断点 3:缺 Redis 验签链无密钥,编译启动都不报错,直到验签 401" },
|
||||
{ key: "trust", label: "trust-registry: true 已配", danger: true, tip: "断点 4:缺失 = 验签不过,症状与「收不到回调」同构" },
|
||||
{ key: "endpoint", label: "接收端点已部署且可访问", danger: false, tip: "注册接口会探活,端点必须先上线" },
|
||||
];
|
||||
const prereqChecked = ref<Record<string, boolean>>({});
|
||||
const prereqAllDone = computed(() => PREREQS.every((p) => prereqChecked.value[p.key]));
|
||||
|
||||
/** 生成的 yh.flow-callback 配置块(技术核心,对齐规范 §7.2 推导式) */
|
||||
const generatedYaml = computed(() => {
|
||||
const ctx = s1.value.ctx.trim();
|
||||
const bizTypes = s1.value.bizTypes
|
||||
.split(",")
|
||||
.map((s: string) => s.trim())
|
||||
.filter(Boolean);
|
||||
const isXflw = s2.value.endpointMode === "xflw";
|
||||
const L: string[] = ["yh:", " flow-callback:", " enabled: true"];
|
||||
L.push(ctx
|
||||
? ` base-path: ${ctx} # 接收方 servlet context-path;§4.2:填错/漏填 ⇒ 推送全 401 无限重试`
|
||||
: " # base-path: # 本地无 context-path 就不填(填了反而 401)");
|
||||
L.push(` biz-types: [${bizTypes.join(", ")}] # 本应用 bizType 白名单;空=不过滤全交 Handler`);
|
||||
L.push(` verify-flw-signature: ${isXflw}${isXflw ? " # 式 B:注册级 X-Flw 签名" : " # 式 A:平台应用签名(AppAuthFilter)"}`);
|
||||
if (isXflw) {
|
||||
L.push(" path: /callback/flow");
|
||||
L.push(" # ⚠ 端点第二段非 oapi → 网关 TargetRouter 裸 404,必须显式配 callback-url 直连:");
|
||||
L.push(" # callback-url: http://<host>:<port>/callback/flow");
|
||||
} else {
|
||||
L.push(" # path: /oapi/flow/callback # 平台验签式用 starter 默认值,不必写");
|
||||
}
|
||||
L.push(" store:");
|
||||
L.push(" auto-create-table: true # 首次使用自动 CREATE TABLE IF NOT EXISTS");
|
||||
return L.join("\n");
|
||||
});
|
||||
|
||||
/** 生成的依赖块(存量升级场景不生成,改提示升版本) */
|
||||
const generatedPom = computed(() => {
|
||||
if (s2.value.scenario === "upgrade") {
|
||||
return "<!-- 存量升级:无需新增依赖,升级 yh-flow-callback-spring-boot-starter 版本即可。\n 回调接收与条件路由判断由同一个 jar 提供。 -->";
|
||||
}
|
||||
const extra = s2.value.endpointMode === "platform"
|
||||
? "\n<!-- 平台验签式额外需要:应用已配 yh-app-auth-spring-boot-starter -->"
|
||||
: "";
|
||||
return "<dependency>\n <groupId>com.yunhai.platform</groupId>\n <artifactId>yh-flow-callback-spring-boot-starter</artifactId>\n</dependency>" + extra;
|
||||
});
|
||||
|
||||
/** 生成的 Handler 骨架(规范 §7.3) */
|
||||
const generatedHandler = computed(() =>
|
||||
"@Bean\npublic FlowCallbackHandler flowCallbackHandler(YourRepository repo) {\n return event -> {\n // 走到这里已验签、已过幂等/乱序门/bizType 过滤,且与业务推进同一事务\n switch (event.getEvent()) {\n case \"flow.submitted\": /* 在途 */ break;\n case \"flow.task.created\": /* 建待办 */ break;\n case \"flow.task.handled\": /* 记录办理 */ break;\n case \"flow.completed\": /* 终态·通过(终态不可逆!) */ break;\n case \"flow.rejected\": /* 终态·驳回 */ break;\n case \"flow.withdrawn\": /* 终态·撤回 */ break;\n default: /* 未知事件码 → 记日志后正常返回,200 吞掉(向前兼容) */ break;\n }\n };\n}");
|
||||
|
||||
// ==================== STEP③ 回调注册 ====================
|
||||
const regs = ref<any[]>([]);
|
||||
const regLoading = ref(false);
|
||||
/** 当前 appKey 的登记行(同一应用多个 flowGuid 各一行) */
|
||||
const appRegs = computed(() =>
|
||||
regs.value.filter((r: any) => String(r.appKey) === String(s1.value.appKey)));
|
||||
const regCheck = ref<{ level: "ok" | "warn" | "bad"; text: string; advice?: string }[]>([]);
|
||||
const regCheckedAt = ref("");
|
||||
const probeResult = ref<any>(null);
|
||||
const probing = ref(false);
|
||||
|
||||
async function loadRegs() {
|
||||
regLoading.value = true;
|
||||
try {
|
||||
regs.value = ((await flowApi.regList().send()) as any[]) || [];
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "登记列表加载失败");
|
||||
regs.value = [];
|
||||
} finally {
|
||||
regLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 登记行检查(调研 §3.3 痛点② 的直接兜底) */
|
||||
async function checkRegs() {
|
||||
if (!s1.value.appKey) {
|
||||
Message.warning("请先在第 ① 步选择应用");
|
||||
step.value = 0;
|
||||
return;
|
||||
}
|
||||
await loadRegs();
|
||||
const mine = appRegs.value;
|
||||
const out: { level: "ok" | "warn" | "bad"; text: string; advice?: string }[] = [];
|
||||
if (!mine.length) {
|
||||
out.push({ level: "bad", text: "该应用没有任何回调登记行", advice: "在管理端保存绑定时调 ensureRegistered(schId, flowGuid)(规范 §7.4);不要在发起时才注册" });
|
||||
} else {
|
||||
// 地址形态:私网/开发机地址必然拒连无限重试(断点 1)
|
||||
const PRIVATE = /:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|10\.[0-9.]+|192\.168\.[0-9.]+|172\.(1[6-9]|2[0-9]|3[01])\.[0-9.]+)/;
|
||||
const bad = mine.filter((r: any) => r.callbackUrl && PRIVATE.test(r.callbackUrl));
|
||||
if (bad.length) {
|
||||
out.push({ level: "bad", text: bad.length + " 条登记地址是私网/开发机地址(" + bad[0].callbackUrl + ")", advice: "推送必然拒连并无限重试:删掉本地镜像行 → 管理端重新保存绑定,走 starter 自注册" });
|
||||
}
|
||||
// basePath:有 ctx 却漏填 ⇒ 推送全 401(§4.2 高频事故)
|
||||
const emptyBase = mine.filter((r: any) => !r.basePath);
|
||||
if (emptyBase.length && s1.value.ctx.trim()) {
|
||||
out.push({ level: "bad", text: emptyBase.length + " 条登记未填 base_path,而你在第 ① 步声明了 context-path", advice: "漏填 base_path → 推送全 401 → 无限重试;症状表现为「收不到回调」" });
|
||||
} else if (emptyBase.length) {
|
||||
out.push({ level: "warn", text: emptyBase.length + " 条登记未填 base_path(你声明了无 context-path,需确认)", advice: "本地无 ctx 才留空;生产有 ctx 必须填" });
|
||||
}
|
||||
// 登记地址与第 ① 步声明的 target 一致性
|
||||
const target = s1.value.target.trim();
|
||||
if (target) {
|
||||
const mismatch = mine.filter((r: any) => r.callbackUrl && !String(r.callbackUrl).includes("/" + target));
|
||||
if (mismatch.length) {
|
||||
out.push({ level: "warn", text: mismatch.length + " 条登记地址不含声明的 target「" + target + "」", advice: "确认是旧登记残留,还是登记地址推导有误" });
|
||||
}
|
||||
}
|
||||
if (!out.length) {
|
||||
out.push({ level: "ok", text: mine.length + " 条登记全部合规(地址为网关形态、base_path 一致)" });
|
||||
}
|
||||
}
|
||||
regCheck.value = out;
|
||||
regCheckedAt.value = new Date().toLocaleTimeString();
|
||||
}
|
||||
|
||||
/** 探测:同步发一条 flow.ping 走真实管道(复用管理端 callbackProbe) */
|
||||
async function probeFirst() {
|
||||
const first = appRegs.value[0];
|
||||
if (!first) {
|
||||
Message.warning("该应用没有可探测的登记行");
|
||||
return;
|
||||
}
|
||||
probing.value = true;
|
||||
probeResult.value = null;
|
||||
try {
|
||||
probeResult.value = (await flowApi.callbackProbe(first.id).send()) || null;
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "探测失败");
|
||||
} finally {
|
||||
probing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== STEP④ 流程编排 ====================
|
||||
const defs = ref<any[]>([]);
|
||||
const defLoading = ref(false);
|
||||
const selectedDefId = ref<number | null>(null);
|
||||
/** 选中流程的 CONDITION 节点清单 + 变量契约核对 */
|
||||
const condNodes = ref<{ name: string; judgeMode: string; routerKey: string; outlets: { name: string; expr: string; unknown: string[] }[] }[]>([]);
|
||||
const knownVars = ref<string[]>([]);
|
||||
const DEF_STATUS: Record<number, string> = { 0: "草稿", 1: "已发布", 2: "停用" };
|
||||
|
||||
async function loadDefs() {
|
||||
if (!s1.value.appKey) return;
|
||||
defLoading.value = true;
|
||||
try {
|
||||
defs.value = ((await flowApi.defList({ appKey: s1.value.appKey }).send()) as any[]) || [];
|
||||
if (!selectedDefId.value && defs.value.length) selectedDefId.value = defs.value[0].id;
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "流程定义加载失败");
|
||||
defs.value = [];
|
||||
} finally {
|
||||
defLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 已知变量 = 近期 50 条办件 formData 顶层键(去重截 40)+ 内置 8 个;口径同编辑器与调试弹窗 */
|
||||
async function loadKnownVars() {
|
||||
const keys = new Set<string>();
|
||||
try {
|
||||
const rows: any[] = (await flowApi.adminInstances({ page: 1, size: 50 }).send())?.content || [];
|
||||
rows.forEach((r: any) => {
|
||||
try {
|
||||
const v = JSON.parse(r.formDataJson || "{}");
|
||||
if (v && typeof v === "object" && !Array.isArray(v)) Object.keys(v).forEach((k: string) => keys.add(k));
|
||||
} catch { /* 脏 formData 跳过 */ }
|
||||
});
|
||||
} catch { /* 办件拉取失败:退化为仅内置,不阻塞 */ }
|
||||
knownVars.value = Array.from(new Set([...Array.from(keys).slice(0, 40), ...EXPR_BUILTIN_VARS]));
|
||||
}
|
||||
|
||||
const hasPublished = computed(() => defs.value.some((d: any) => d.status === 1));
|
||||
|
||||
/** 解析选中流程的 CONDITION 节点,把表达式变量与已知键核对(发布期 warn 校验的可视化复述) */
|
||||
async function inspectDef(id: number) {
|
||||
selectedDefId.value = id;
|
||||
condNodes.value = [];
|
||||
try {
|
||||
const d: any = await flowApi.defGet(id).send();
|
||||
const content = typeof d?.contentJson === "string" ? JSON.parse(d.contentJson || "{}") : (d?.contentJson || {});
|
||||
const nodes: any[] = content.nodes || [];
|
||||
const edges: any[] = content.edges || [];
|
||||
condNodes.value = nodes
|
||||
.filter((n: any) => n.type === "CONDITION")
|
||||
.map((n: any) => ({
|
||||
name: n.name || n.id,
|
||||
judgeMode: n.judgeMode || "SERVICE",
|
||||
routerKey: n.routerKey || "",
|
||||
outlets: edges
|
||||
.filter((e: any) => e.from === n.id)
|
||||
.map((e: any) => {
|
||||
const expr = e.condition || "";
|
||||
return { name: e.name || "(未命名出口)", expr, unknown: expr ? extractExprVars(expr).filter((v: string) => !knownVars.value.includes(v)) : [] };
|
||||
}),
|
||||
}));
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "流程详情加载失败");
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== STEP⑤ 联调验收 ====================
|
||||
const accept = ref<{ no: number; name: string; level: "ok" | "warn" | "bad" | "none"; detail: string; advice?: string }[]>([]);
|
||||
const acceptDone = ref(false);
|
||||
const acceptLoading = ref(false);
|
||||
|
||||
/** 六项自检:把「手册的诊断命令集」变成按钮 */
|
||||
async function runAcceptance() {
|
||||
acceptLoading.value = true;
|
||||
try {
|
||||
const R: typeof accept.value = [];
|
||||
const app = apps.value.find((a: any) => String(a.appKey) === String(s1.value.appKey));
|
||||
// 1 应用白名单启用(必须通过)
|
||||
R.push(app && app.enabled === 1
|
||||
? { no: 1, name: "应用白名单启用", level: "ok", detail: app.appName + "(" + app.appKey + ")已开通且启用" }
|
||||
: { no: 1, name: "应用白名单启用", level: "bad", detail: app ? "应用已停用" : "应用不在白名单", advice: "请先在用户中心开通/启用该应用" });
|
||||
// 2 前置条件全勾选(黄色可带病通过)
|
||||
const missed = PREREQS.filter((p) => !prereqChecked.value[p.key]);
|
||||
R.push(missed.length
|
||||
? { no: 2, name: "前置条件全勾选", level: "warn", detail: "尚有 " + missed.length + " 项未勾选:" + missed.map((m) => m.label).join(";"), advice: "回到第 ② 步补勾,优先 Redis 公共组件与 trust-registry" }
|
||||
: { no: 2, name: "前置条件全勾选", level: "ok", detail: PREREQS.length + " 项前置条件已确认" });
|
||||
// 3 登记行存在且地址合规(必须通过)
|
||||
const regBad = regCheck.value.filter((c) => c.level === "bad");
|
||||
const regWarn = regCheck.value.filter((c) => c.level === "warn");
|
||||
if (!regCheckedAt.value) {
|
||||
R.push({ no: 3, name: "登记行存在且地址合规", level: "warn", detail: "尚未在第 ③ 步执行「检查注册」", advice: "回第 ③ 步点「检查注册」" });
|
||||
} else if (regBad.length) {
|
||||
R.push({ no: 3, name: "登记行存在且地址合规", level: "bad", detail: regBad.map((c) => c.text).join(";"), advice: regBad[0].advice });
|
||||
} else {
|
||||
R.push({ no: 3, name: "登记行存在且地址合规", level: regWarn.length ? "warn" : "ok", detail: regWarn.length ? "地址合规,但有 " + regWarn.length + " 条告警:" + regWarn.map((c) => c.text).join(";") : appRegs.value.length + " 条登记全部合规", advice: regWarn[0]?.advice });
|
||||
}
|
||||
// 4 探测可达且验签通过(黄色可带病通过)
|
||||
if (!probeResult.value) {
|
||||
R.push({ no: 4, name: "探测可达且验签通过", level: "warn", detail: "尚未执行探测", advice: "回第 ③ 步点「发一条 flow.ping」" });
|
||||
} else if (probeResult.value.success) {
|
||||
R.push({ no: 4, name: "探测可达且验签通过", level: "ok", detail: "HTTP " + probeResult.value.httpStatus + " · 耗时 " + probeResult.value.elapsedMs + "ms" });
|
||||
} else {
|
||||
R.push({ no: 4, name: "探测可达且验签通过", level: "warn", detail: "探测失败:" + (probeResult.value.error || "HTTP " + (probeResult.value.httpStatus ?? "?")), advice: "401 → 查 base-path / trust-registry;拒连 → 查登记地址" });
|
||||
}
|
||||
// 5 有已发布流程(必须通过)
|
||||
R.push(hasPublished.value
|
||||
? { no: 5, name: "有已发布流程", level: "ok", detail: defs.value.filter((d: any) => d.status === 1).length + " 个已发布流程" }
|
||||
: { no: 5, name: "有已发布流程", level: "bad", detail: defs.value.length ? "仅有草稿/停用流程" : "该应用无任何流程定义", advice: "回第 ④ 步:发布流程后再回填绑定" });
|
||||
// 6 近 7 日推送有成功记录(无数据不扣分)
|
||||
try {
|
||||
const page: any = await flowApi.callbackLogs({ status: 1, size: 1 }).send();
|
||||
const total = page?.totalCount ?? page?.total ?? 0;
|
||||
R.push(total > 0
|
||||
? { no: 6, name: "近 7 日推送有成功记录", level: "ok", detail: "累计成功推送 " + total + " 条" }
|
||||
: { no: 6, name: "近 7 日推送有成功记录", level: "warn", detail: "暂无成功推送记录(可能还没发起过办件,可跳过)" });
|
||||
} catch {
|
||||
R.push({ no: 6, name: "近 7 日推送有成功记录", level: "warn", detail: "推送日志拉取失败,跳过该项" });
|
||||
}
|
||||
accept.value = R;
|
||||
acceptDone.value = true;
|
||||
} finally {
|
||||
acceptLoading.value = false;
|
||||
}
|
||||
}
|
||||
/** 一键生成接入自检报告(纯前端拼装,不落库;可直接复制进工单) */
|
||||
const reportVisible = ref(false);
|
||||
const reportText = computed(() => {
|
||||
const app = apps.value.find((a: any) => String(a.appKey) === String(s1.value.appKey));
|
||||
const L: string[] = [];
|
||||
L.push("# 子应用接入流程中心 · 自检报告");
|
||||
L.push("");
|
||||
L.push("- 生成时间:" + new Date().toLocaleString());
|
||||
L.push("- 应用:" + (app ? app.appName + "(" + app.appKey + ")" : s1.value.appKey || "未选择"));
|
||||
L.push("- 网关 target:" + (s1.value.target || "未填(缺省取 base-path)"));
|
||||
L.push("- context-path:" + (s1.value.ctx || "无"));
|
||||
L.push("- bizType:" + (s1.value.bizTypes || "未填"));
|
||||
L.push("- 绑定档位:" + (BIND_LEVELS.find((b) => b.value === s1.value.bindLevel)?.label || "-"));
|
||||
L.push("- 接入口径:" + (s2.value.endpointMode === "xflw" ? "注册级 X-Flw 式(式 B)" : "平台验签式(式 A)"));
|
||||
L.push("");
|
||||
L.push("## 六项自检结果");
|
||||
L.push("");
|
||||
if (!acceptDone.value) {
|
||||
L.push("_尚未执行自检,请先点「执行六项自检」。_");
|
||||
} else {
|
||||
const icon = { ok: "✅", warn: "⚠️", bad: "❌", none: "➖" };
|
||||
accept.value.forEach((r) => {
|
||||
L.push(icon[r.level] + " **" + r.no + ". " + r.name + "** —— " + r.detail);
|
||||
if (r.advice) L.push(" - 建议:" + r.advice);
|
||||
});
|
||||
}
|
||||
L.push("");
|
||||
L.push("## 生成的配置块");
|
||||
L.push("");
|
||||
L.push("```yaml");
|
||||
L.push(generatedYaml.value);
|
||||
L.push("```");
|
||||
L.push("");
|
||||
L.push("## 前置条件确认");
|
||||
L.push("");
|
||||
PREREQS.forEach((p) => L.push((prereqChecked.value[p.key] ? "- [x]" : "- [ ]") + " " + p.label + (p.danger ? "(高危断点)" : "")));
|
||||
L.push("");
|
||||
L.push("## 登记行");
|
||||
L.push("");
|
||||
if (!appRegs.value.length) L.push("_无登记行_");
|
||||
else {
|
||||
L.push("| 流程 GUID | 回调地址 | base_path | 状态 |");
|
||||
L.push("|---|---|---|---|");
|
||||
appRegs.value.forEach((r: any) => L.push("| " + (r.flowGuid || "-") + " | " + (r.callbackUrl || "-") + " | " + (r.basePath || "-") + " | " + (r.status === 0 ? "启用" : "停用") + " |"));
|
||||
}
|
||||
L.push("");
|
||||
L.push("## 条件路由变量契约");
|
||||
L.push("");
|
||||
if (!condNodes.value.length) L.push("_所选流程无 CONDITION 节点_");
|
||||
else {
|
||||
condNodes.value.forEach((n) => {
|
||||
L.push("### " + n.name + "(" + (n.judgeMode === "EXPRESSION" ? "表达式" : "外部判断服务") + (n.routerKey ? " · routerKey=" + n.routerKey : "") + ")");
|
||||
n.outlets.forEach((o) => {
|
||||
L.push("- 出口「" + o.name + "」:" + (o.expr || "(无表达式)"));
|
||||
if (o.unknown.length) L.push(" - ⚠️ 引用了未在已知变量中的:" + o.unknown.join("、") + "(疑似拼错,运行时会挂起)");
|
||||
});
|
||||
});
|
||||
}
|
||||
return L.join("\n");
|
||||
});
|
||||
|
||||
async function copyText(t: string, ok = "已复制") {
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(t);
|
||||
Message.success(ok);
|
||||
return;
|
||||
}
|
||||
await Utils.copyToClipboard(t);
|
||||
Message.success(ok);
|
||||
} catch {
|
||||
Message.error("复制失败,请手动选择文本复制");
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
onMounted(async () => {
|
||||
try {
|
||||
apps.value = ((await flowApi.appList().send()) as any[]) || [];
|
||||
appMap.value = {};
|
||||
apps.value.forEach((a: any) => { if (a.appKey) appMap.value[a.appKey] = a.appName; });
|
||||
} catch { /* 应用列表拉取失败:第 ① 步会给红字提示 */ }
|
||||
await loadKnownVars();
|
||||
});
|
||||
|
||||
/** 进入某步时的按需装载 */
|
||||
async function goto(n: number) {
|
||||
step.value = n;
|
||||
if (n === 2) await loadRegs();
|
||||
if (n === 3) await loadDefs();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-wrapper yh-fill">
|
||||
<div class="main-container ob-wrap">
|
||||
<!-- 顶栏 -->
|
||||
<div class="ob-head fix">
|
||||
<span class="fl ob-title">接入向导</span>
|
||||
<span class="fl ob-sub">把「子应用接入流程中心」从人背文档变成向导引导 + 机器生成正确配置</span>
|
||||
<Button class="fr" type="primary" size="large" @click="reportVisible = true">生成接入自检报告</Button>
|
||||
</div>
|
||||
|
||||
<!-- 步骤条(可任意跳转,不强制线性) -->
|
||||
<div class="ob-steps">
|
||||
<div v-for="(s, i) in STEPS" :key="i" class="ob-step" :class="{ 'is-cur': step === i, 'is-done': i < step }" @click="goto(i)">
|
||||
<span class="ob-num">{{ i + 1 }}</span>
|
||||
<span class="ob-st">
|
||||
<b>{{ s.title }}</b>
|
||||
<em>{{ s.hint }}</em>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ STEP① 应用档案 ============ -->
|
||||
<div v-show="step === 0" class="ob-body">
|
||||
<div class="ob-card">
|
||||
<h3>我是谁</h3>
|
||||
<p class="ob-desc">这四个值是后续所有生成器的输入;context-path 是 §4.2 高频事故点,请如实填写。</p>
|
||||
<div class="ob-form">
|
||||
<div class="ob-row">
|
||||
<label class="ob-lab">应用<i>*</i></label>
|
||||
<Select v-model="s1.appKey" style="width: 320px" placeholder="选择已开通的应用" @on-change="loadDefs">
|
||||
<Option v-for="a in apps" :key="a.appKey" :value="a.appKey" :label="a.appName + '(' + a.appKey + ')'" />
|
||||
</Select>
|
||||
<span v-if="!apps.length" class="ob-err">应用清单拉取失败或尚未开通 —— 请先在用户中心开通该应用</span>
|
||||
</div>
|
||||
<div class="ob-row">
|
||||
<label class="ob-lab">网关 target</label>
|
||||
<Input v-model="s1.target" style="width: 320px" placeholder="如 notice;可留空,缺省取 base-path" />
|
||||
<span class="ob-tip">网关路由首段;契约上与 base-path 恒等,通常留空即可</span>
|
||||
</div>
|
||||
<div class="ob-row">
|
||||
<label class="ob-lab">context-path(base-path)</label>
|
||||
<Input v-model="s1.ctx" style="width: 320px" placeholder="如 /notice;本地无 ctx 留空" />
|
||||
<span class="ob-warn-txt">⚠ 填错/漏填 ⇒ 推送全 401 无限重试,症状表现为「收不到回调」</span>
|
||||
</div>
|
||||
<div class="ob-row">
|
||||
<label class="ob-lab">bizType</label>
|
||||
<Input v-model="s1.bizTypes" style="width: 320px" placeholder="如 notice;多个用逗号分隔" />
|
||||
<span class="ob-tip">发起时声明的业务关联对;回调按它路由到单据行</span>
|
||||
</div>
|
||||
<div class="ob-row">
|
||||
<label class="ob-lab">绑定档位</label>
|
||||
<RadioGroup v-model="s1.bindLevel" type="button">
|
||||
<Radio v-for="b in BIND_LEVELS" :key="b.value" :label="b.value">{{ b.label }}</Radio>
|
||||
</RadioGroup>
|
||||
<span class="ob-tip">{{ (BIND_LEVELS.find((b) => b.value === s1.bindLevel) || {}).desc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ STEP② 工程接入(配置生成器) ============ -->
|
||||
<div v-show="step === 1" class="ob-body">
|
||||
<div class="ob-card">
|
||||
<h3>端点形态与接入场景</h3>
|
||||
<div class="ob-form">
|
||||
<div class="ob-row">
|
||||
<label class="ob-lab">端点形态<i>*</i></label>
|
||||
<RadioGroup v-model="s2.endpointMode">
|
||||
<Radio label="platform">平台验签式(式 A,推荐)</Radio>
|
||||
<Radio label="xflw">注册级 X-Flw 式(式 B)</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div class="ob-row">
|
||||
<label class="ob-lab">接入场景<i>*</i></label>
|
||||
<RadioGroup v-model="s2.scenario">
|
||||
<Radio label="new">新接入</Radio>
|
||||
<Radio label="upgrade">存量升级(已接回调)</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ob-card">
|
||||
<h3>A · Maven 依赖</h3>
|
||||
<pre class="ob-code" :class="{ 'is-warn': s2.scenario === 'upgrade' }">{{ generatedPom }}</pre>
|
||||
<Button size="small" class="ob-copy" @click="copyText(generatedPom, '依赖块已复制')">复制</Button>
|
||||
</div>
|
||||
|
||||
<div class="ob-card">
|
||||
<h3>B · yh.flow-callback 配置块(自动生成)</h3>
|
||||
<p class="ob-desc">按上方选择 + 第 ① 步输入实时生成,形态对齐规范 §7.2 推导式。</p>
|
||||
<pre class="ob-code">{{ generatedYaml }}</pre>
|
||||
<Button size="small" class="ob-copy" @click="copyText(generatedYaml, '配置块已复制')">复制</Button>
|
||||
<div v-if="s2.endpointMode === 'xflw'" class="ob-alert">
|
||||
⚠ X-Flw 式端点第二段非 <code>oapi</code>,网关 TargetRouter 裸 404,<b>必须显式配 callback-url 直连</b>(规范 §7.5)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ob-card">
|
||||
<h3>C · FlowCallbackHandler 骨架</h3>
|
||||
<pre class="ob-code">{{ generatedHandler }}</pre>
|
||||
<Button size="small" class="ob-copy" @click="copyText(generatedHandler, 'Handler 骨架已复制')">复制</Button>
|
||||
</div>
|
||||
|
||||
<div class="ob-card">
|
||||
<h3>D · 前置条件清单</h3>
|
||||
<p class="ob-desc">把散落文档里的前置条件变成可勾选待办;红底两项是 work-leave 复盘出的高危静默断点。</p>
|
||||
<div class="ob-prereqs">
|
||||
<label v-for="p in PREREQS" :key="p.key" class="ob-prereq" :class="{ 'is-danger': p.danger }">
|
||||
<Checkbox v-model="prereqChecked[p.key]" />
|
||||
<span class="ob-prereq-t">{{ p.label }}</span>
|
||||
<span class="ob-prereq-tip">{{ p.tip }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="ob-row" style="margin-top: 14px">
|
||||
<Checkbox v-model="s2.configDone" />
|
||||
<span style="margin-left: 8px">我已完成上述配置部署</span>
|
||||
<span class="ob-tip" style="margin-left: 12px">计入第 ⑤ 步判分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ob-card">
|
||||
<h3>E · 避坑速查(规范 §10)</h3>
|
||||
<Collapse class="ob-pit">
|
||||
<Panel name="1">
|
||||
接入前必读的 10 条坑
|
||||
<template #content>
|
||||
<ol class="ob-pit-list">
|
||||
<li><b>basePath 401</b>:填错/漏填 ⇒ 推送必 401 无限重试。</li>
|
||||
<li><b>重复注册轮换 secret</b>:本地镜像行有效就别再 reg。</li>
|
||||
<li><b>注册探活</b>:接收端点必须先上线再注册。</li>
|
||||
<li><b>4xx 也无限重试</b>:流程中心只认 2xx,脏消息必须 200 吞掉。</li>
|
||||
<li><b>验签用原始字节</b>:先反序列化再序列化会验签失败。</li>
|
||||
<li><b>事件行与业务同事务</b>:拆开会导致重推被幂等吞掉丢单。</li>
|
||||
<li><b>乱序是常态</b>:靠 instanceVersion 门 + 终态不可逆守卫。</li>
|
||||
<li><b>bizId 是字符串</b>:无论数值与否都用字符串处理。</li>
|
||||
<li><b>地址推导与漂移</b>:callback-url 可省略;reconcile-on-register 默认关。</li>
|
||||
<li><b>待办在流程中心</b>:子应用不维护待办表,用 progress/instance 接口拉。</li>
|
||||
</ol>
|
||||
</template>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ STEP③ 回调注册 ============ -->
|
||||
<div v-show="step === 2" class="ob-body">
|
||||
<div class="ob-card">
|
||||
<div class="fix">
|
||||
<h3 class="fl">登记行({{ appRegs.length }} 条)</h3>
|
||||
<span class="fr">
|
||||
<Button size="small" :loading="regLoading" @click="loadRegs">刷新</Button>
|
||||
<Button size="small" type="primary" style="margin-left: 8px" @click="checkRegs">检查注册</Button>
|
||||
<Button size="small" style="margin-left: 8px" :loading="probing" @click="probeFirst">发一条 flow.ping</Button>
|
||||
</span>
|
||||
</div>
|
||||
<p class="ob-desc">
|
||||
登记由子应用在「管理端保存绑定」时调 <code>ensureRegistered</code> 完成,向导只检查不代注册。
|
||||
<span v-if="regCheckedAt">上次检查:{{ regCheckedAt }}</span>
|
||||
</p>
|
||||
<Table :columns="[
|
||||
{ title: '流程 GUID', key: 'flowGuid', minWidth: 200 },
|
||||
{ title: '回调地址', key: 'callbackUrl', minWidth: 240 },
|
||||
{ title: 'base_path', key: 'basePath', width: 110, render: (h: any, p: any) => h('span', p.row.basePath || '(空)') },
|
||||
{ title: '状态', width: 80, align: 'center', render: (h: any, p: any) => h(Tag, { color: p.row.status === 0 ? 'green' : 'gray' }, () => p.row.status === 0 ? '启用' : '停用') },
|
||||
]" :data="appRegs" :loading="regLoading" size="small" max-height="260"
|
||||
no-data-text="该应用暂无登记行(未接入:回第②步配置后,在管理端保存绑定触发注册)" />
|
||||
<div v-if="regCheck.length" class="ob-results">
|
||||
<div v-for="(c, i) in regCheck" :key="i" class="ob-result" :class="'is-' + c.level">
|
||||
<b>{{ c.level === 'ok' ? '✓ 通过' : c.level === 'warn' ? '⚠ 告警' : '✗ 失败' }}</b>
|
||||
<span>{{ c.text }}</span>
|
||||
<em v-if="c.advice">建议:{{ c.advice }}</em>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="probeResult" class="ob-result" :class="probeResult.success ? 'is-ok' : 'is-warn'">
|
||||
<b>{{ probeResult.success ? '✓ 探测成功' : '⚠ 探测失败' }}</b>
|
||||
<span>HTTP {{ probeResult.httpStatus ?? '?' }} · 耗时 {{ probeResult.elapsedMs ?? '?' }} ms<span v-if="probeResult.respSnippet"> · 响应片段:{{ probeResult.respSnippet }}</span><span v-if="probeResult.error"> · {{ probeResult.error }}</span></span>
|
||||
<em v-if="!probeResult.success">401 → 查 base-path 与 trust-registry;拒连/未知主机 → 查登记地址是否网关形态</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ STEP④ 流程编排 ============ -->
|
||||
<div v-show="step === 3" class="ob-body">
|
||||
<div class="ob-card">
|
||||
<h3>流程定义</h3>
|
||||
<p class="ob-desc">至少需要一个「已发布」流程;发起时用它的 flowGuid。</p>
|
||||
<Table :columns="[
|
||||
{ title: '流程', key: 'name', minWidth: 180 },
|
||||
{ title: 'flowGuid', key: 'flowGuid', minWidth: 200 },
|
||||
{ title: '状态', width: 90, align: 'center', render: (h: any, p: any) => h(Tag, { color: p.row.status === 1 ? 'green' : p.row.status === 0 ? 'orange' : 'gray' }, () => DEF_STATUS[p.row.status] || p.row.status) },
|
||||
{ title: '操作', width: 90, align: 'center', render: (h: any, p: any) => h(Button, { props: { type: 'text', size: 'small' }, on: { click: () => inspectDef(p.row.id) } }, () => '查看条件') },
|
||||
]" :data="defs" :loading="defLoading" size="small" max-height="220"
|
||||
no-data-text="该应用无流程定义(先回第①步确认应用,或去流程定义页新建)" />
|
||||
</div>
|
||||
|
||||
<div class="ob-card" v-if="condNodes.length">
|
||||
<h3>条件路由变量契约</h3>
|
||||
<p class="ob-desc">
|
||||
EXPRESSION 模式的判断依据就是子应用 apply 时 <code>formData</code> 的顶层键。这里把表达式引用的变量与
|
||||
已知键(近 50 条办件 formData 聚合 + 内置 8 个)核对 —— 引用了从未出现的键,运行时会<b>挂起</b>。
|
||||
</p>
|
||||
<div v-for="(n, i) in condNodes" :key="i" class="ob-cond">
|
||||
<div class="ob-cond-h">
|
||||
<b>{{ n.name }}</b>
|
||||
<Tag :color="n.judgeMode === 'EXPRESSION' ? 'blue' : 'purple'" style="margin-left: 8px">
|
||||
{{ n.judgeMode === 'EXPRESSION' ? '表达式' : '外部判断服务' }}
|
||||
</Tag>
|
||||
<span v-if="n.routerKey" class="ob-cond-k">routerKey = {{ n.routerKey }}</span>
|
||||
</div>
|
||||
<div v-for="(o, j) in n.outlets" :key="j" class="ob-cond-o">
|
||||
<span class="ob-cond-on">出口「{{ o.name }}」</span>
|
||||
<code class="ob-cond-e">{{ o.expr || '(无表达式)' }}</code>
|
||||
<span v-if="o.unknown.length" class="ob-cond-u">⚠ 引用了未在已知变量中的:<b>{{ o.unknown.join('、') }}</b> —— 疑似拼错,运行时会挂起</span>
|
||||
<span v-else-if="o.expr" class="ob-cond-ok">✓ 变量均在已知集合内</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ob-card" v-else-if="selectedDefId">
|
||||
<p class="ob-desc">所选流程没有 CONDITION 节点(无需变量契约核对)。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ STEP⑤ 联调验收 ============ -->
|
||||
<div v-show="step === 4" class="ob-body">
|
||||
<div class="ob-card">
|
||||
<div class="fix">
|
||||
<h3 class="fl">六项自检</h3>
|
||||
<Button class="fr" size="small" type="primary" :loading="acceptLoading" @click="runAcceptance">执行六项自检</Button>
|
||||
</div>
|
||||
<p class="ob-desc">把「手册的诊断命令集」变成按钮。1/3/5 必须通过;2/4 黄色可带病通过;6 无数据不扣分。</p>
|
||||
<div v-if="!acceptDone" class="ob-empty">尚未执行自检</div>
|
||||
<div v-else class="ob-results">
|
||||
<div v-for="r in accept" :key="r.no" class="ob-result" :class="'is-' + r.level">
|
||||
<b>{{ r.no }}. {{ r.name }} —— {{ r.level === 'ok' ? '✓ 通过' : r.level === 'warn' ? '⚠ 关注' : '✗ 失败' }}</b>
|
||||
<span>{{ r.detail }}</span>
|
||||
<em v-if="r.advice">建议:{{ r.advice }}</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作条 -->
|
||||
<div class="ob-foot">
|
||||
<Button size="large" :disabled="step === 0" @click="step > 0 && step--">上一步</Button>
|
||||
<span class="ob-foot-r">
|
||||
<span v-if="step === 1 && !prereqAllDone" class="ob-foot-hint">尚有前置条件未勾选,仍可继续</span>
|
||||
<span v-if="step === 3 && !hasPublished" class="ob-foot-hint">尚无已发布流程,联调验收第 5 项会失败</span>
|
||||
<Button size="large" v-if="step < 4" type="primary" @click="goto(step + 1)">下一步</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 接入自检报告 -->
|
||||
<Drawer v-model="reportVisible" title="接入自检报告" :width="720" :mask-closable="false">
|
||||
<p class="ob-desc" style="margin-bottom: 10px">纯前端拼装、不落库。可直接复制进工单或群。</p>
|
||||
<pre class="ob-code ob-report">{{ reportText }}</pre>
|
||||
<template #footer>
|
||||
<Button @click="reportVisible = false">关闭</Button>
|
||||
<Button type="primary" @click="copyText(reportText.value, '报告已复制')">复制报告</Button>
|
||||
</template>
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ob-wrap { display: flex; flex-direction: column; padding: 16px 20px; overflow: auto; }
|
||||
.ob-head { margin-bottom: 4px; }
|
||||
.ob-title { font-size: 18px; font-weight: 600; color: #17233d; }
|
||||
.ob-sub { margin-left: 14px; color: #808695; font-size: 12px; line-height: 34px; }
|
||||
|
||||
/* 步骤条 */
|
||||
.ob-steps { display: flex; gap: 8px; margin: 14px 0 18px; }
|
||||
.ob-step { flex: 1; display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid #e5e6eb; border-radius: 4px; cursor: pointer; background: #fff; transition: all .15s; }
|
||||
.ob-step:hover { border-color: #2d8cf0; }
|
||||
.ob-step.is-cur { border-color: #2d8cf0; background: #f0f7ff; }
|
||||
.ob-step.is-done { opacity: .75; }
|
||||
.ob-num { flex: none; width: 24px; height: 24px; border-radius: 50%; background: #e5e6eb; color: #515a6e; text-align: center; line-height: 24px; font-weight: 600; font-size: 13px; }
|
||||
.ob-step.is-cur .ob-num { background: #2d8cf0; color: #fff; }
|
||||
.ob-step.is-done .ob-num { background: #19a35a; color: #fff; }
|
||||
.ob-st { display: flex; flex-direction: column; line-height: 1.35; min-width: 0; }
|
||||
.ob-st b { font-size: 13px; color: #17233d; }
|
||||
.ob-st em { font-style: normal; font-size: 11px; color: #98a1b3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.ob-body { overflow: auto; }
|
||||
.ob-card { background: #fff; border: 1px solid #e5e6eb; border-radius: 4px; padding: 16px 18px; margin-bottom: 14px; }
|
||||
.ob-card h3 { font-size: 14px; color: #17233d; margin-bottom: 4px; }
|
||||
.ob-desc { color: #808695; font-size: 12px; line-height: 1.7; margin-bottom: 12px; }
|
||||
.ob-desc code { background: #f4f6f9; padding: 1px 4px; border-radius: 3px; font-family: Consolas, Menlo, monospace; }
|
||||
|
||||
.ob-form { display: flex; flex-direction: column; gap: 14px; }
|
||||
.ob-row { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; }
|
||||
.ob-lab { flex: none; width: 168px; text-align: right; line-height: 32px; color: #515a6e; font-size: 13px; }
|
||||
.ob-lab i { color: #e03e3e; font-style: normal; margin-left: 2px; }
|
||||
.ob-tip { line-height: 32px; color: #98a1b3; font-size: 12px; }
|
||||
.ob-warn-txt { line-height: 32px; color: #d98106; font-size: 12px; }
|
||||
.ob-err { line-height: 32px; color: #e03e3e; font-size: 12px; }
|
||||
|
||||
.ob-code { position: relative; background: #f7f8fa; border: 1px solid #e5e6eb; border-radius: 4px; padding: 12px 14px; font-family: Consolas, Menlo, monospace; font-size: 12px; line-height: 1.6; color: #314659; white-space: pre-wrap; word-break: break-all; margin: 0; }
|
||||
.ob-code.is-warn { background: #fff8e6; border-color: #f0d9a0; }
|
||||
.ob-copy { margin-top: 8px; }
|
||||
.ob-report { max-height: 60vh; overflow: auto; }
|
||||
.ob-alert { margin-top: 10px; background: #fff5e6; border: 1px solid #f0d9a0; border-radius: 4px; padding: 8px 12px; color: #b26a00; font-size: 12px; line-height: 1.6; }
|
||||
.ob-alert code { background: rgba(0,0,0,.06); padding: 1px 4px; border-radius: 3px; }
|
||||
|
||||
.ob-prereqs { display: flex; flex-direction: column; gap: 10px; }
|
||||
.ob-prereq { display: flex; align-items: flex-start; gap: 8px; padding: 8px 10px; border: 1px solid #e5e6eb; border-radius: 4px; }
|
||||
.ob-prereq.is-danger { border-color: #f5c2c2; background: #fff5f5; }
|
||||
.ob-prereq-t { font-size: 13px; color: #314659; line-height: 20px; }
|
||||
.ob-prereq-tip { font-size: 11px; color: #98a1b3; line-height: 20px; margin-left: auto; text-align: right; max-width: 46%; }
|
||||
.ob-pit-list { padding-left: 20px; color: #515a6e; font-size: 12px; line-height: 1.9; }
|
||||
.ob-pit-list b { color: #314659; }
|
||||
|
||||
/* 检查结果 */
|
||||
.ob-results { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
|
||||
.ob-result { border-radius: 4px; padding: 10px 12px; font-size: 12px; line-height: 1.7; border: 1px solid #e5e6eb; background: #f7f8fa; }
|
||||
.ob-result b { display: block; margin-bottom: 2px; }
|
||||
.ob-result em { display: block; font-style: normal; color: #808695; margin-top: 2px; }
|
||||
.ob-result.is-ok { background: #eefaf2; border-color: #b7e4c7; }
|
||||
.ob-result.is-ok b { color: #19a35a; }
|
||||
.ob-result.is-warn { background: #fff8e6; border-color: #f0d9a0; }
|
||||
.ob-result.is-warn b { color: #d98106; }
|
||||
.ob-result.is-bad { background: #fdecec; border-color: #f5c2c2; }
|
||||
.ob-result.is-bad b { color: #e03e3e; }
|
||||
.ob-empty { color: #98a1b3; font-size: 12px; text-align: center; padding: 30px 0; }
|
||||
|
||||
/* 条件契约 */
|
||||
.ob-cond { border: 1px solid #e5e6eb; border-radius: 4px; padding: 12px 14px; margin-bottom: 10px; }
|
||||
.ob-cond-h { display: flex; align-items: center; margin-bottom: 8px; }
|
||||
.ob-cond-h b { font-size: 13px; color: #17233d; }
|
||||
.ob-cond-k { margin-left: auto; color: #98a1b3; font-size: 11px; font-family: Consolas, Menlo, monospace; }
|
||||
.ob-cond-o { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 4px 0; font-size: 12px; }
|
||||
.ob-cond-on { color: #515a6e; flex: none; }
|
||||
.ob-cond-e { background: #f4f6f9; padding: 2px 6px; border-radius: 3px; font-family: Consolas, Menlo, monospace; color: #314659; }
|
||||
.ob-cond-u { color: #d98106; }
|
||||
.ob-cond-ok { color: #19a35a; }
|
||||
|
||||
.ob-foot { display: flex; align-items: center; justify-content: space-between; padding: 12px 0 4px; border-top: 1px solid #e5e6eb; margin-top: auto; }
|
||||
.ob-foot-r { display: flex; align-items: center; gap: 12px; }
|
||||
.ob-foot-hint { color: #d98106; font-size: 12px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user