|
@@ -0,0 +1,571 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * 「trade_status=16 超时关闭」前端转译的执行级验证台(2026-10-10)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 【要证的三句话】
|
|
|
|
|
+ * A. 订单列表页:挂**真 SFC**(真模板),喂 trade_status="16" 的订单,状态列
|
|
|
|
|
+ * 渲染出「超时关闭」;且整个页面文本里**不再出现裸 "16"**。
|
|
|
|
|
+ * B. 订单详情弹层:驱动 detail / detailVisible 后,详情里的状态 tag 同样转译
|
|
|
|
|
+ * (列表与详情是模板里两处独立的映射点,测一处不能代表另一处)。
|
|
|
|
|
+ * C. 收银台 result computed:16 → 超时关闭(不再落进「表外状态原样显示」的兜底)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 【两向读数】表里既有「必须转译」的 16,也有正控:已知状态(1 / 8)行为不变;
|
|
|
|
|
+ * 表外的 "99" 仍**原样显示** —— 那是「表外不猜」的既定哲学,谁顺手把它删掉,A4/C4 该红。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 【配方来源】../2026-09-29-lsy-amount-floor-harness/amount-floor-harness.mjs:
|
|
|
|
|
+ * 同一套「编译真 SFC + 打桩 import + typescript 转译 + new Function」。本台多两件:
|
|
|
|
|
+ * ① 订单页要**渲染真模板**:compileScript(inlineTemplate:false) + 单独 compileTemplate
|
|
|
|
|
+ * 出 render(**必须带 bindingMetadata**,script setup 的 __isScriptSetup 旗子会让
|
|
|
|
|
+ * runtime 拒绝走 _ctx 代理),二者拼回一个组件再挂;
|
|
|
|
|
+ * ② el-table / el-tag / el-dialog / el-option 用「会转发插槽或记录读数」的桩
|
|
|
|
|
+ * (el-table 按 data 逐行把 row 塞进列插槽;el-tag 记 {type,text};el-option 记
|
|
|
|
|
+ * {label,value}),其余 el-* 直通默认插槽或渲染空。
|
|
|
|
|
+ * 模板里的 HAR_TRADE_STATUS_TEXT 等映射表来自**真 har.ts**(转译求值后取出),
|
|
|
|
|
+ * 只有 HarAPI 本身换成桩(免得真发请求)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 【A5b 为什么直接钉表值】两处 el-tag 都写成 `TAG[st] || 'info'` —— 兜底把
|
|
|
|
|
+ * 「TAG 表里 16 缺格」和「给了 info」渲染成同一个样子,变异体 M2(删掉这格)
|
|
|
|
|
+ * 在渲染层分辨不出(2026-10-10 实测:只加渲染断言时 M2 全绿)。所以表值单独钉。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 【不设 process.exitCode】失败也退 0,只认最后那行 SUMMARY(同目录各台惯例)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 用法:node har-status16-harness.mjs
|
|
|
|
|
+ */
|
|
|
|
|
+import fs from "node:fs";
|
|
|
|
|
+import { createRequire } from "node:module";
|
|
|
|
|
+
|
|
|
|
|
+const FE = "D:/project2/payment-platform/frontend/";
|
|
|
|
|
+const require = createRequire(FE + "package.json");
|
|
|
|
|
+const vue = require("vue");
|
|
|
|
|
+const { parse, compileScript, compileTemplate } = require("@vue/compiler-sfc");
|
|
|
|
|
+const ts = require("typescript");
|
|
|
|
|
+
|
|
|
|
|
+const ORDER = FE + "src/views/module_payment/har/order/index.vue";
|
|
|
|
|
+const CASHIER = FE + "src/views/module_payment/har/cashier/index.vue";
|
|
|
|
|
+const HAR_TS = FE + "src/api/module_payment/har.ts";
|
|
|
|
|
+
|
|
|
|
|
+// 无 DOM 台子的垫片:页面里用的是 `window.setTimeout` / `window.clearTimeout`。
|
|
|
|
|
+if (typeof globalThis.window === "undefined") {
|
|
|
|
|
+ globalThis.window = globalThis;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 通用:翻 SFC 里的 import 成桩 ----------
|
|
|
|
|
+const braceImport = (spec) =>
|
|
|
|
|
+ new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*['"]${spec}['"];?`, "g");
|
|
|
|
|
+const braceAsConst = (slot) => (m, names) => {
|
|
|
|
|
+ const clean = names
|
|
|
|
|
+ .replace(/\btype\s+[\w$.]+\s*,?/g, "")
|
|
|
|
|
+ .replace(/\bas\b/g, ":");
|
|
|
|
|
+ return `const {${clean}} = globalThis.__U.${slot};`;
|
|
|
|
|
+};
|
|
|
|
|
+const transpile = (code, fileName) =>
|
|
|
|
|
+ ts.transpileModule(code, {
|
|
|
|
|
+ compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
|
|
|
|
|
+ fileName,
|
|
|
|
|
+ }).outputText;
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 真 har.ts:取映射表(HarAPI 换成桩) ----------
|
|
|
|
|
+const HAR_NAMES = [
|
|
|
|
|
+ "HAR_TRADE_STATUS_TEXT",
|
|
|
|
|
+ "HAR_TRADE_STATUS_TAG",
|
|
|
|
|
+ "HAR_TRADE_TYPE_OPTIONS",
|
|
|
|
|
+ "HAR_ORDER_STATUS_PAID",
|
|
|
|
|
+ "HAR_NOTIFY_STATUS_TEXT",
|
|
|
|
|
+ "HAR_BIZ_UN_STAGE",
|
|
|
|
|
+];
|
|
|
|
|
+function evalRealHarModule() {
|
|
|
|
|
+ let code = fs.readFileSync(HAR_TS, "utf8");
|
|
|
|
|
+ code = code.replace(
|
|
|
|
|
+ /import\s+(\w+)\s+from\s*['"]@\/utils\/request['"];?/,
|
|
|
|
|
+ "const $1 = globalThis.__U.request;"
|
|
|
|
|
+ );
|
|
|
|
|
+ code = code.replace(/export\s+default\s+/g, "const __har_default = ");
|
|
|
|
|
+ code = code.replace(/^\s*export\s+/gm, "");
|
|
|
|
|
+ if (/^\s*import\s/m.test(code) || /^\s*export\s/m.test(code)) {
|
|
|
|
|
+ throw new Error("har.ts 还有没换掉的 import/export —— 配方要更新");
|
|
|
|
|
+ }
|
|
|
|
|
+ code = transpile(code, "har.ts");
|
|
|
|
|
+ code +=
|
|
|
|
|
+ "\nglobalThis.__U.realHar = { __default: __har_default, " +
|
|
|
|
|
+ HAR_NAMES.map((n) => `${n}: (typeof ${n} !== "undefined" ? ${n} : undefined)`).join(", ") +
|
|
|
|
|
+ " };";
|
|
|
|
|
+ new Function(code)();
|
|
|
|
|
+ for (const n of HAR_NAMES) {
|
|
|
|
|
+ if (globalThis.__U.realHar[n] === undefined) throw new Error("har.ts 里没有导出 " + n);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 桩:请求 / UI 库 / store ----------
|
|
|
|
|
+const apiCalls = [];
|
|
|
|
|
+const apiStub = new Proxy(
|
|
|
|
|
+ {},
|
|
|
|
|
+ {
|
|
|
|
|
+ get: (_t, name) =>
|
|
|
|
|
+ name === "then"
|
|
|
|
|
+ ? undefined
|
|
|
|
|
+ : async (...args) => {
|
|
|
|
|
+ apiCalls.push({ name: String(name), args });
|
|
|
|
|
+ return { data: { code: 0, data: { items: [], total: 0 } } };
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+);
|
|
|
|
|
+const callable = new Proxy(async () => undefined, {
|
|
|
|
|
+ get: (_t, k) => (k === "then" ? undefined : callable),
|
|
|
|
|
+ apply: () => Promise.resolve(),
|
|
|
|
|
+});
|
|
|
|
|
+const epStub = new Proxy({}, { get: () => callable });
|
|
|
|
|
+const noop = () => {};
|
|
|
|
|
+const enterpriseStoreStub = () => ({
|
|
|
|
|
+ getEnterpriseList: [],
|
|
|
|
|
+ loadEnterpriseList: async () => {},
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 无 DOM 渲染器:垫出足够的节点操作,让真模板能挂/能更新 ----------
|
|
|
|
|
+const noopNode = () => {};
|
|
|
|
|
+const renderer = vue.createRenderer({
|
|
|
|
|
+ createElement: (tag) => ({ kind: "el", tag, children: [], parent: null }),
|
|
|
|
|
+ createText: (text) => ({ kind: "text", text, parent: null }),
|
|
|
|
|
+ createComment: (text) => ({ kind: "comment", text, parent: null }),
|
|
|
|
|
+ setText: (n, text) => {
|
|
|
|
|
+ n.text = text;
|
|
|
|
|
+ },
|
|
|
|
|
+ setElementText: (el, text) => {
|
|
|
|
|
+ el.children = [{ kind: "text", text, parent: el }];
|
|
|
|
|
+ },
|
|
|
|
|
+ insert: (child, parent, anchor) => {
|
|
|
|
|
+ child.parent = parent;
|
|
|
|
|
+ if (anchor) {
|
|
|
|
|
+ const i = parent.children.indexOf(anchor);
|
|
|
|
|
+ parent.children.splice(i < 0 ? parent.children.length : i, 0, child);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ parent.children.push(child);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ remove: (child) => {
|
|
|
|
|
+ const p = child.parent;
|
|
|
|
|
+ if (p) {
|
|
|
|
|
+ const i = p.children.indexOf(child);
|
|
|
|
|
+ if (i >= 0) p.children.splice(i, 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ child.parent = null;
|
|
|
|
|
+ },
|
|
|
|
|
+ parentNode: (n) => n.parent ?? null,
|
|
|
|
|
+ nextSibling: (n) => {
|
|
|
|
|
+ const p = n.parent;
|
|
|
|
|
+ if (!p) return null;
|
|
|
|
|
+ const i = p.children.indexOf(n);
|
|
|
|
|
+ return i >= 0 ? p.children[i + 1] ?? null : null;
|
|
|
|
|
+ },
|
|
|
|
|
+ patchProp: noopNode, // 属性/class/style 与本台断言无关(只读文本)
|
|
|
|
|
+ insertStaticContent: (content, parent, anchor, ns, start, end) => {
|
|
|
|
|
+ if (start && end) {
|
|
|
|
|
+ // 缓存节点搬到新位置(同 DOM 语义)
|
|
|
|
|
+ const move = (n) => {
|
|
|
|
|
+ if (n.parent) renderer.remove ? null : null;
|
|
|
|
|
+ if (n.parent) {
|
|
|
|
|
+ const i = n.parent.children.indexOf(n);
|
|
|
|
|
+ if (i >= 0) n.parent.children.splice(i, 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ n.parent = parent;
|
|
|
|
|
+ if (anchor) {
|
|
|
|
|
+ const i = parent.children.indexOf(anchor);
|
|
|
|
|
+ parent.children.splice(i < 0 ? parent.children.length : i, 0, n);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ parent.children.push(n);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ move(start);
|
|
|
|
|
+ if (end !== start) move(end);
|
|
|
|
|
+ return [start, end];
|
|
|
|
|
+ }
|
|
|
|
|
+ const n = { kind: "static", html: String(content), children: [], parent: null };
|
|
|
|
|
+ n.parent = parent;
|
|
|
|
|
+ parent.children.push(n);
|
|
|
|
|
+ return [n, n];
|
|
|
|
|
+ },
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 桩组件:转发插槽的 el-table / el-tag / el-dialog / el-option ----------
|
|
|
|
|
+const tagPairs = []; // el-tag 每次渲染记一对 {type, text} —— TAG 表就是靠它钉
|
|
|
|
|
+const optionPairs = []; // el-option 每次渲染记一对 {label, value} —— 筛选下拉吃 TEXT 表
|
|
|
|
|
+
|
|
|
|
|
+function collectText(nodes, out = []) {
|
|
|
|
|
+ for (const n of Array.isArray(nodes) ? nodes : [nodes]) {
|
|
|
|
|
+ if (n == null || n === false) continue;
|
|
|
|
|
+ if (typeof n === "string" || typeof n === "number") out.push(String(n));
|
|
|
|
|
+ else if (Array.isArray(n)) collectText(n, out);
|
|
|
|
|
+ else if (typeof n === "object" && n.children != null) collectText(n.children, out);
|
|
|
|
|
+ }
|
|
|
|
|
+ return out;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const ElTableStub = {
|
|
|
|
|
+ name: "el-table",
|
|
|
|
|
+ props: { data: { type: Array, default: () => [] } },
|
|
|
|
|
+ setup(props, { slots }) {
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ const cols = slots.default ? slots.default().flat(Infinity) : [];
|
|
|
|
|
+ const out = [];
|
|
|
|
|
+ for (const row of props.data || []) {
|
|
|
|
|
+ for (const col of cols) {
|
|
|
|
|
+ const colSlots = col && col.children;
|
|
|
|
|
+ if (colSlots && typeof colSlots === "object" && typeof colSlots.default === "function") {
|
|
|
|
|
+ out.push(vue.h("div", { class: "cell" }, colSlots.default({ row })));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return vue.h("div", out);
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+};
|
|
|
|
|
+const ElTagStub = {
|
|
|
|
|
+ name: "el-tag",
|
|
|
|
|
+ props: { type: { type: String, default: "" } },
|
|
|
|
|
+ setup(props, { slots }) {
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ const kids = slots.default ? slots.default() : [];
|
|
|
|
|
+ tagPairs.push({ type: props.type, text: collectText(kids).join("") });
|
|
|
|
|
+ return vue.h("span", kids);
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+};
|
|
|
|
|
+const ElOptionStub = {
|
|
|
|
|
+ name: "el-option",
|
|
|
|
|
+ props: { label: { type: String, default: "" }, value: { type: [String, Number], default: "" } },
|
|
|
|
|
+ setup(props) {
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ optionPairs.push({ label: props.label, value: String(props.value) });
|
|
|
|
|
+ return null; // 选项的文字在真实组件里来自 label prop,槽里没有内容可渲染
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+};
|
|
|
|
|
+const ElDialogStub = {
|
|
|
|
|
+ name: "el-dialog",
|
|
|
|
|
+ props: { modelValue: { type: Boolean, default: false } },
|
|
|
|
|
+ setup(props, { slots }) {
|
|
|
|
|
+ return () =>
|
|
|
|
|
+ props.modelValue
|
|
|
|
|
+ ? vue.h("div", [slots.default ? slots.default() : null, slots.footer ? slots.footer() : null])
|
|
|
|
|
+ : null;
|
|
|
|
|
+ },
|
|
|
|
|
+};
|
|
|
|
|
+function registerStubs(app) {
|
|
|
|
|
+ const passthrough = (name) => ({
|
|
|
|
|
+ name,
|
|
|
|
|
+ setup: (_p, { slots }) => () => (slots.default ? slots.default() : null),
|
|
|
|
|
+ });
|
|
|
|
|
+ for (const n of [
|
|
|
|
|
+ "el-alert",
|
|
|
|
|
+ "el-button",
|
|
|
|
|
+ "el-card",
|
|
|
|
|
+ "el-date-picker",
|
|
|
|
|
+ "el-descriptions",
|
|
|
|
|
+ "el-descriptions-item",
|
|
|
|
|
+ "el-empty",
|
|
|
|
|
+ "el-form",
|
|
|
|
|
+ "el-form-item",
|
|
|
|
|
+ "el-input",
|
|
|
|
|
+ "el-input-number",
|
|
|
|
|
+ "el-pagination",
|
|
|
|
|
+ "el-select",
|
|
|
|
|
+ "el-table-column",
|
|
|
|
|
+ ]) {
|
|
|
|
|
+ app.component(n, passthrough(n));
|
|
|
|
|
+ }
|
|
|
|
|
+ app.component("el-table", ElTableStub);
|
|
|
|
|
+ app.component("el-tag", ElTagStub);
|
|
|
|
|
+ app.component("el-option", ElOptionStub);
|
|
|
|
|
+ app.component("el-dialog", ElDialogStub);
|
|
|
|
|
+ app.directive("hasPerm", {});
|
|
|
|
|
+ app.directive("loading", {});
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 编译 + 挂载:订单页(真模板) ----------
|
|
|
|
|
+function compileOrder() {
|
|
|
|
|
+ const src = fs.readFileSync(ORDER, "utf8");
|
|
|
|
|
+ const { descriptor, errors } = parse(src, { filename: ORDER });
|
|
|
|
|
+ if (errors && errors.length) throw new Error("SFC 解析失败: " + errors.map(String).join("; "));
|
|
|
|
|
+
|
|
|
|
|
+ const script = compileScript(descriptor, { id: "s16order", inlineTemplate: false });
|
|
|
|
|
+ let code = script.content;
|
|
|
|
|
+ code = code
|
|
|
|
|
+ .replace(braceImport("vue"), braceAsConst("vue"))
|
|
|
|
|
+ .replace(braceImport("element-plus"), braceAsConst("ep"))
|
|
|
|
|
+ .replace(/import\s+dayjs\s+from\s*['"]dayjs['"];?/, "const dayjs = globalThis.__U.dayjs;")
|
|
|
|
|
+ .replace(
|
|
|
|
|
+ /import\s+(\w+)\s*,\s*\{([^}]*)\}\s*from\s*['"]@\/api\/module_payment\/har['"];?/g,
|
|
|
|
|
+ (_m, def, names) => {
|
|
|
|
|
+ const clean = names
|
|
|
|
|
+ .split(",")
|
|
|
|
|
+ .map((s) => s.trim())
|
|
|
|
|
+ .filter((s) => s && !/^type\s/.test(s));
|
|
|
|
|
+ return (
|
|
|
|
|
+ `const ${def} = globalThis.__U.api;` +
|
|
|
|
|
+ (clean.length ? ` const { ${clean.join(", ")} } = globalThis.__U.realHar;` : "")
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ .replace(
|
|
|
|
|
+ /import\s*\{\s*useEnterpriseStore\s*\}\s*from\s*['"]@\/store\/modules\/enterprise\.store['"];?/,
|
|
|
|
|
+ "const useEnterpriseStore = globalThis.__U.enterpriseStore;"
|
|
|
|
|
+ )
|
|
|
|
|
+ .replace(
|
|
|
|
|
+ /import\s*\{\s*useLiveRefetch\s*\}\s*from\s*['"]@\/composables['"];?/,
|
|
|
|
|
+ "const useLiveRefetch = globalThis.__U.noop;"
|
|
|
|
|
+ );
|
|
|
|
|
+ if (/^\s*import\s/m.test(code)) throw new Error("order 还有没换掉的 import:\n" + code.slice(0, 500));
|
|
|
|
|
+ code = transpile(code, "order.ts");
|
|
|
|
|
+ code = code.replace(/export\s+default/, "const __sfc =");
|
|
|
|
|
+ if (/^\s*export\s/m.test(code)) throw new Error("order 还有没换掉的 export");
|
|
|
|
|
+ code += "\nglobalThis.__U.orderSfc = __sfc;";
|
|
|
|
|
+ new Function(code)();
|
|
|
|
|
+
|
|
|
|
|
+ const tpl = compileTemplate({
|
|
|
|
|
+ source: descriptor.template.content,
|
|
|
|
|
+ filename: ORDER,
|
|
|
|
|
+ id: "s16order",
|
|
|
|
|
+ // bindingMetadata 必须带:script setup 的 setup 结果带 __isScriptSetup 旗子,
|
|
|
|
|
+ // runtime 的 hasSetupBinding 见到它就拒绝走 _ctx 代理(查不到 = undefined + 警告)。
|
|
|
|
|
+ // 带上它,模板才编成 $setup.xx 直取。2026-10-10 实测踩过一次。
|
|
|
|
|
+ compilerOptions: { bindingMetadata: script.bindings },
|
|
|
|
|
+ });
|
|
|
|
|
+ if (tpl.errors && tpl.errors.length) throw new Error("模板编译失败: " + tpl.errors.map(String).join("; "));
|
|
|
|
|
+ let tcode = tpl.code.replace(braceImport("vue"), braceAsConst("vue"));
|
|
|
|
|
+ tcode = tcode.replace(/export\s+function\s+render/, "function render");
|
|
|
|
|
+ if (/^\s*(import|export)\s/m.test(tcode)) {
|
|
|
|
|
+ throw new Error("模板码还有没换掉的 import/export:\n" + tcode.slice(0, 400));
|
|
|
|
|
+ }
|
|
|
|
|
+ tcode = transpile(tcode, "order.render.ts");
|
|
|
|
|
+ tcode += "\nglobalThis.__U.orderRender = render;";
|
|
|
|
|
+ new Function(tcode)();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function mountOrder() {
|
|
|
|
|
+ const Real = globalThis.__U.orderSfc;
|
|
|
|
|
+ let bindings = null;
|
|
|
|
|
+ const Wrapped = {
|
|
|
|
|
+ render: globalThis.__U.orderRender,
|
|
|
|
|
+ setup(props, ctx) {
|
|
|
|
|
+ bindings = Real.setup(props, ctx);
|
|
|
|
|
+ return bindings;
|
|
|
|
|
+ },
|
|
|
|
|
+ };
|
|
|
|
|
+ const root = { kind: "el", tag: "__root__", children: [], parent: null };
|
|
|
|
|
+ const app = renderer.createApp({ render: () => vue.h(Wrapped) });
|
|
|
|
|
+ registerStubs(app);
|
|
|
|
|
+ app.mount(root);
|
|
|
|
|
+ if (!bindings) throw new Error("setup 没有返回绑定");
|
|
|
|
|
+ return { bindings, root };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 编译 + 挂载:收银台(只驱动绑定,不渲染模板,同金额闸台) ----------
|
|
|
|
|
+function compileCashier() {
|
|
|
|
|
+ const src = fs.readFileSync(CASHIER, "utf8");
|
|
|
|
|
+ const { descriptor, errors } = parse(src, { filename: CASHIER });
|
|
|
|
|
+ if (errors && errors.length) throw new Error("SFC 解析失败: " + errors.map(String).join("; "));
|
|
|
|
|
+
|
|
|
|
|
+ let code = compileScript(descriptor, { id: "s16cashier", inlineTemplate: false }).content;
|
|
|
|
|
+ code = code
|
|
|
|
|
+ .replace(braceImport("vue"), braceAsConst("vue"))
|
|
|
|
|
+ .replace(braceImport("element-plus"), braceAsConst("ep"))
|
|
|
|
|
+ .replace(/import\s+(\w+)\s+from\s*['"]qrcode['"];?/, "const $1 = globalThis.__U.ep;")
|
|
|
|
|
+ .replace(
|
|
|
|
|
+ /import\s+(\w+)\s*,\s*\{([^}]*)\}\s*from\s*['"]@\/api\/module_payment\/har['"];?/g,
|
|
|
|
|
+ (_m, def) => `const ${def} = globalThis.__U.api;`
|
|
|
|
|
+ );
|
|
|
|
|
+ if (/^\s*import\s/m.test(code)) throw new Error("cashier 还有没换掉的 import:\n" + code.slice(0, 500));
|
|
|
|
|
+ code = transpile(code, "cashier.ts");
|
|
|
|
|
+ code = code.replace(/export\s+default/, "const __sfc =");
|
|
|
|
|
+ if (/^\s*export\s/m.test(code)) throw new Error("cashier 还有没换掉的 export");
|
|
|
|
|
+ code += "\nglobalThis.__U.cashierSfc = __sfc;";
|
|
|
|
|
+ new Function(code)();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function mountCashier() {
|
|
|
|
|
+ const Real = globalThis.__U.cashierSfc;
|
|
|
|
|
+ let bindings = null;
|
|
|
|
|
+ const Wrapped = {
|
|
|
|
|
+ ...Real,
|
|
|
|
|
+ render: () => null, // 模板不渲染(同金额闸台):本段只驱动 result / apply
|
|
|
|
|
+ setup(props, ctx) {
|
|
|
|
|
+ bindings = Real.setup(props, ctx);
|
|
|
|
|
+ return bindings;
|
|
|
|
|
+ },
|
|
|
|
|
+ };
|
|
|
|
|
+ renderer.createApp({ render: () => vue.h(Wrapped) }).mount({ kind: "el", tag: "__root__", children: [], parent: null });
|
|
|
|
|
+ if (!bindings) throw new Error("setup 没有返回绑定");
|
|
|
|
|
+ return bindings;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 读数工具 ----------
|
|
|
|
|
+function flattenText(node, out = []) {
|
|
|
|
|
+ if (!node) return out;
|
|
|
|
|
+ if (node.kind === "text") out.push(String(node.text));
|
|
|
|
|
+ else if (node.kind === "static") out.push(String(node.html).replace(/<[^>]*>/g, ""));
|
|
|
|
|
+ for (const c of node.children || []) flattenText(c, out);
|
|
|
|
|
+ return out;
|
|
|
|
|
+}
|
|
|
|
|
+const flush = async (n = 4) => {
|
|
|
|
|
+ for (let i = 0; i < n; i++) {
|
|
|
|
|
+ await new Promise((r) => setTimeout(r, 0));
|
|
|
|
|
+ await vue.nextTick();
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 夹具(所有非状态字段都不含子串 "16",负向断言才干净) ----------
|
|
|
|
|
+const MK = (no, status, extra = {}) => ({
|
|
|
|
|
+ id: "R-" + no,
|
|
|
|
|
+ order_no: "APTEST-A" + no,
|
|
|
|
|
+ out_biz_no: "BIZ-A" + no,
|
|
|
|
|
+ price: "1.00",
|
|
|
|
|
+ title: "样例单" + no,
|
|
|
|
|
+ trade_status: status,
|
|
|
|
|
+ trade_type: "alipay_un_stage",
|
|
|
|
|
+ merchant_code: "M0001",
|
|
|
|
|
+ created_time: "2026-10-10 09:00:00",
|
|
|
|
|
+ notify_status: "FAILED",
|
|
|
|
|
+ ...extra,
|
|
|
|
|
+});
|
|
|
|
|
+const ROW16 = MK("1", "16");
|
|
|
|
|
+const ROW8 = MK("2", "8");
|
|
|
|
|
+const ROW1 = MK("3", "1");
|
|
|
|
|
+const ROW99 = MK("4", "99");
|
|
|
|
|
+const ROWS = [ROW16, ROW8, ROW1, ROW99];
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 跑 ----------
|
|
|
|
|
+globalThis.__U = {
|
|
|
|
|
+ vue,
|
|
|
|
|
+ api: apiStub,
|
|
|
|
|
+ ep: epStub,
|
|
|
|
|
+ request: noop,
|
|
|
|
|
+ dayjs: require("dayjs"),
|
|
|
|
|
+ enterpriseStore: enterpriseStoreStub,
|
|
|
|
|
+ noop,
|
|
|
|
|
+ realHar: null,
|
|
|
|
|
+ orderSfc: null,
|
|
|
|
|
+ orderRender: null,
|
|
|
|
|
+ cashierSfc: null,
|
|
|
|
|
+};
|
|
|
|
|
+// orderList 要喂夹具行;其余接口空列表(merchantList / enterprise 等)
|
|
|
|
|
+const rawApi = apiStub;
|
|
|
|
|
+globalThis.__U.api = new Proxy(
|
|
|
|
|
+ {},
|
|
|
|
|
+ {
|
|
|
|
|
+ get: (_t, name) =>
|
|
|
|
|
+ name === "then"
|
|
|
|
|
+ ? undefined
|
|
|
|
|
+ : async (...args) => {
|
|
|
|
|
+ apiCalls.push({ name: String(name), args });
|
|
|
|
|
+ if (name === "orderList") {
|
|
|
|
|
+ return { data: { code: 0, data: { items: ROWS, total: ROWS.length } } };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (name === "orderDetail") {
|
|
|
|
|
+ return { data: { code: 0, data: ROW16 } };
|
|
|
|
|
+ }
|
|
|
|
|
+ return { data: { code: 0, data: { items: [], total: 0 } } };
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+evalRealHarModule();
|
|
|
|
|
+compileOrder();
|
|
|
|
|
+compileCashier();
|
|
|
|
|
+
|
|
|
|
|
+const results = [];
|
|
|
|
|
+const check = (tag, ok, detail) => {
|
|
|
|
|
+ results.push({ tag, ok });
|
|
|
|
|
+ console.log(` ${tag.padEnd(4)} ${ok ? "PASS" : "FAIL"} ${detail}`);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+console.log(`har.ts = ${HAR_TS}`);
|
|
|
|
|
+console.log(`order = ${ORDER}`);
|
|
|
|
|
+console.log(`cashier = ${CASHIER}`);
|
|
|
|
|
+console.log("");
|
|
|
|
|
+console.log("== A/B 订单列表页与详情弹层(真 SFC + 真模板) ==");
|
|
|
|
|
+
|
|
|
|
|
+const { bindings: bOrder, root: rootOrder } = mountOrder();
|
|
|
|
|
+await flush();
|
|
|
|
|
+
|
|
|
|
|
+const textsList = flattenText(rootOrder);
|
|
|
|
|
+const allList = textsList.join(" | ");
|
|
|
|
|
+check("A1", allList.includes("超时关闭"), "列表 16 行转译为「超时关闭」");
|
|
|
|
|
+check("A2", !textsList.some((t) => t.includes("16")), "全页文本不含裸 \"16\"");
|
|
|
|
|
+check("A3", allList.includes("交易关闭"), "正控:8 行「交易关闭」仍是老文案");
|
|
|
|
|
+check("A4", allList.includes("99"), "正控:表外值 99 仍原样显示(兜底没被删)");
|
|
|
|
|
+check(
|
|
|
|
|
+ "A5",
|
|
|
|
|
+ tagPairs.some((p) => p.text === "超时关闭" && p.type === "info"),
|
|
|
|
|
+ "tag 配对 {type:info, text:超时关闭} 存在"
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ "A6",
|
|
|
|
|
+ tagPairs.some((p) => p.text === "支付成功" && p.type === "success"),
|
|
|
|
|
+ "正控:1 行仍是 {success, 支付成功}"
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ "A5b",
|
|
|
|
|
+ globalThis.__U.realHar.HAR_TRADE_STATUS_TAG["16"] === "info",
|
|
|
|
|
+ "TAG 表里 16 显式给了 info(模板 `|| 'info'` 兜底让渲染分辨不出,直接钉表值)"
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ "A7",
|
|
|
|
|
+ optionPairs.some((o) => o.value === "16" && o.label === "超时关闭"),
|
|
|
|
|
+ "筛选下拉(v-for 吃 TEXT 表)出现 {16 → 超时关闭}"
|
|
|
|
|
+);
|
|
|
|
|
+check(
|
|
|
|
|
+ "A8",
|
|
|
|
|
+ optionPairs.some((o) => o.value === "8" && o.label === "交易关闭"),
|
|
|
|
|
+ "正控:下拉里 8 仍是「交易关闭」"
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+const tagCountBefore = tagPairs.length;
|
|
|
|
|
+bOrder.detail.value = ROW16;
|
|
|
|
|
+bOrder.detailVisible.value = true;
|
|
|
|
|
+await flush();
|
|
|
|
|
+const added = tagPairs.slice(tagCountBefore);
|
|
|
|
|
+const textsAfterDetail = flattenText(rootOrder);
|
|
|
|
|
+check(
|
|
|
|
|
+ "B1",
|
|
|
|
|
+ added.some((p) => p.text === "超时关闭" && p.type === "info"),
|
|
|
|
|
+ "详情弹层里的状态 tag 同款转译"
|
|
|
|
|
+);
|
|
|
|
|
+check("B2", !textsAfterDetail.some((t) => t.includes("16")), "打开详情后全页文本仍不含裸 \"16\"");
|
|
|
|
|
+
|
|
|
|
|
+console.log("");
|
|
|
|
|
+console.log("== C 收银台 result / apply ==");
|
|
|
|
|
+
|
|
|
|
|
+const bCashier = mountCashier();
|
|
|
|
|
+await flush();
|
|
|
|
|
+
|
|
|
|
|
+const r16 = (() => {
|
|
|
|
|
+ bCashier.order.value = { trade_status: "16", price: "1.00" };
|
|
|
|
|
+ return bCashier.result.value;
|
|
|
|
|
+})();
|
|
|
|
|
+check(
|
|
|
|
|
+ "C1",
|
|
|
|
|
+ r16.title === "超时关闭" && r16.tone === "muted" && String(r16.detail || "").includes("没有收到钱"),
|
|
|
|
|
+ `16 → ${JSON.stringify(r16.title)} / ${JSON.stringify(r16.detail)}`
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+bCashier.order.value = { trade_status: "1", rcv_amount: "1.00", price: "1.00" };
|
|
|
|
|
+const r1 = bCashier.result.value;
|
|
|
|
|
+check("C2", r1.title === "收款成功" && r1.money === "1.00", `正控:1 → ${JSON.stringify(r1.title)} / money=${r1.money}`);
|
|
|
|
|
+
|
|
|
|
|
+bCashier.order.value = { trade_status: "8" };
|
|
|
|
|
+const r8 = bCashier.result.value;
|
|
|
|
|
+check("C3", r8.title === "交易关闭", `正控:8 → ${JSON.stringify(r8.title)}`);
|
|
|
|
|
+
|
|
|
|
|
+bCashier.order.value = { trade_status: "99" };
|
|
|
|
|
+const r99 = bCashier.result.value;
|
|
|
|
|
+check("C4", r99.title === "99", `正控:表外值 99 仍原样(兜底没被删)`);
|
|
|
|
|
+
|
|
|
|
|
+bCashier.phase.value = "waiting";
|
|
|
|
|
+bCashier.apply = bCashier.apply; // 绑定即函数本身
|
|
|
|
|
+bCashier.apply({ trade_status: "16", price: "1.00" });
|
|
|
|
|
+check("C5", bCashier.phase.value === "done", "apply(16) 停轮询进 done(现状行为回归)");
|
|
|
|
|
+
|
|
|
|
|
+console.log("");
|
|
|
|
|
+const bad = results.filter((r) => !r.ok).length;
|
|
|
|
|
+console.log(`SUMMARY ${bad === 0 ? "PASS" : "FAIL"} 断言=${results.length} 不符=${bad}`);
|
|
|
|
|
+
|
|
|
|
|
+// 兜底退出:即使有残留定时器也不吊在事件循环上;失败也退 0,只认 SUMMARY。
|
|
|
|
|
+process.exit(0);
|