查询方式优化

This commit is contained in:
fuenmao
2026-11-09 17:08:34 +08:00
parent b79034311b
commit f183bfe508
8 changed files with 510 additions and 395 deletions
+278
View File
@@ -0,0 +1,278 @@
// Offline checks: execute the real composable and page setup code with stubbed APIs.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import vm from "node:vm";
import test from "node:test";
import { compileScript, parse } from "@vue/compiler-sfc";
import ts from "typescript";
import * as vue from "vue";
const require = createRequire(import.meta.url);
const root = new URL("../", import.meta.url);
const read = path => readFileSync(new URL(path, root), "utf8");
const deferred = () => {
let resolve, reject;
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
return { promise, resolve, reject };
};
function harness(overrides = {}) {
const calls = [];
const api = new Proxy(overrides, {
get(target, method) {
return (...args) => {
calls.push({ method, args });
if (method in target) return target[method](...args);
if (method === "listCompanies") return Promise.resolve({ data: [{ id: 1, companyName: "A" }, { id: 2, companyName: "B" }] });
if (method === "listDepartments") return Promise.resolve({ data: [{ id: args[0] * 10, departmentName: "部门" }] });
if (method === "listPositionsByDepartment") return Promise.resolve({ data: [{ id: args[0] * 10, positionName: "岗位" }] });
if (method === "listMaterialTypes") return Promise.resolve({ data: [{ id: 1, status: "enable" }] });
if (method === "onboardingTemplatePage") return Promise.resolve({ data: { list: [], total: 0 } });
throw new Error("Unexpected API: " + method);
};
}
});
function evaluate(source, imports = {}) {
const output = ts.transpileModule(source.replaceAll("import.meta.env", "({})"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, esModuleInterop: true }
}).outputText;
const module = { exports: {} };
vm.runInNewContext(output, {
module, exports: module.exports, console,
require(name) {
if (name in imports) return imports[name];
if (name === "vue") return vue;
if (name === "@/api/modules/biz/onboarding") return { onboardingTaskApi: api };
if (name === "@/hooks/useOnboardingScopeOptions") return scopeModule;
if (name === "@/utils/onboardingPermission") return {
useOnboardingPermission: () => ({ canManage: () => true, disabledReason: () => "", hasAction: () => true })
};
if (name === "@/utils/messageBox" || name === "@/utils") return {};
if (name === "element-plus") return { ElMessage: { success() {}, warning() {}, error() {} }, ElMessageBox: {} };
if (name === "@element-plus/icons-vue" || name.endsWith(".vue")) return {};
return require(name);
}
});
return module.exports;
}
const scopeModule = evaluate(read("src/hooks/useOnboardingScopeOptions.ts"));
function page(path, props = {}) {
const mounted = [];
const descriptor = parse(read(path), { filename: path }).descriptor;
const code = compileScript(descriptor, { id: path }).content;
const component = evaluate(code, { vue: { ...vue, onMounted: callback => mounted.push(callback), onBeforeUnmount() {} } }).default;
const effects = vue.effectScope();
const state = effects.run(() => component.setup(props, { expose() {} }));
return { state, mount: () => Promise.all(mounted.map(callback => callback())), stop: () => effects.stop() };
}
return { api, calls, scopeModule, page };
}
test("dependent options load only after selecting their parent, and reset makes no request", async () => {
const h = harness();
const selection = vue.reactive({});
const options = h.scopeModule.useOnboardingScopeOptions(() => selection);
assert.equal(h.calls.length, 0);
await options.onDepartmentChange(10);
assert.equal(h.calls.length, 0);
selection.companyId = 1;
await options.onCompanyChange(1);
assert.equal(h.calls.length, 1);
assert.equal(h.calls[0].method, "listDepartments");
assert.equal(options.positions.value.length, 0);
selection.departmentId = 10;
await options.onDepartmentChange(10);
assert.equal(h.calls[1].method, "listPositionsByDepartment");
selection.positionId = 100;
selection.companyId = undefined;
options.reset();
assert.equal(selection.departmentId, undefined);
assert.equal(selection.positionId, undefined);
assert.equal(options.departments.value.length, 0);
assert.equal(options.positions.value.length, 0);
assert.equal(h.calls.length, 2);
});
test("slow department responses cannot restore a previous company or survive reset", async () => {
const a = deferred(), b = deferred(), c = deferred();
const h = harness({ listDepartments: id => ({ 1: a, 2: b, 3: c })[id].promise });
const selection = { companyId: 1 };
const options = h.scopeModule.useOnboardingScopeOptions(() => selection);
const first = options.onCompanyChange(1);
selection.companyId = 2;
const second = options.onCompanyChange(2);
b.resolve({ data: [{ id: 20 }] });
await second;
a.resolve({ data: [{ id: 10 }] });
await first;
assert.equal(options.departments.value[0].id, 20);
selection.companyId = 3;
const third = options.onCompanyChange(3);
options.reset();
c.resolve({ data: [{ id: 30 }] });
await third;
assert.equal(options.departments.value.length, 0);
});
test("slow position responses cannot overwrite a new department; failure leaves options empty", async () => {
const a = deferred(), b = deferred();
const h = harness({ listPositionsByDepartment: id => (id === 10 ? a : b).promise });
const selection = { companyId: 1, departmentId: 10 };
const options = h.scopeModule.useOnboardingScopeOptions(() => selection);
const first = options.onDepartmentChange(10);
selection.departmentId = 20;
const second = options.onDepartmentChange(20);
b.resolve({ data: [{ id: 200 }] });
await second;
a.resolve({ data: [{ id: 100 }] });
await first;
assert.equal(options.positions.value[0].id, 200);
const failed = harness({ listDepartments: () => Promise.reject(new Error("offline")) });
const empty = failed.scopeModule.useOnboardingScopeOptions(() => ({ companyId: 1 }));
await empty.onCompanyChange(1);
assert.equal(empty.departments.value.length, 0);
});
for (const [name, tableName, columnName] of [
["contract", "templateTable", "templateColumns"],
["task", "proTable", "columns"],
["employee", "proTable", "columns"]
]) {
test(name + ": initialization, filter cascade and reset use only necessary requests", async () => {
const h = harness();
const page = h.page("src/views/biz/onboarding/" + name + "/index.vue");
const state = page.state;
state[tableName].value = { searchParam: {}, enumMap: new Map() };
await page.mount();
assert.equal(h.calls.length, 1);
assert.equal(h.calls[0].method, "listCompanies");
const department = state[columnName].find(item => item.prop === "departmentId");
const position = state[columnName].find(item => item.prop === "positionId");
assert.equal(department.search.props.disabled, true);
assert.equal(position.search.props.disabled, true);
state[tableName].value.searchParam.companyId = 1;
await state.onSearchCompanyChange(1);
assert.equal(department.search.props.disabled, false);
assert.equal(position.search.props.disabled, true);
assert.equal(state[tableName].value.enumMap.get("departmentId")[0].value, 10);
state[tableName].value.searchParam.departmentId = 10;
await state.onSearchDepartmentChange(10);
assert.equal(position.search.props.disabled, false);
assert.equal(state[tableName].value.enumMap.get("positionId")[0].value, 100);
state[tableName].value.searchParam = {};
state.resetSearchScope();
assert.equal(h.calls.length, 3);
assert.equal(state[tableName].value.enumMap.get("departmentId").length, 0);
assert.equal(state[tableName].value.enumMap.get("positionId").length, 0);
page.stop();
});
}
test("entry config loads no hidden data; material loads share one in-flight request", async () => {
const pending = deferred();
const h = harness({ listMaterialTypes: () => pending.promise });
const page = h.page("src/views/biz/onboarding/entry-config/index.vue");
await page.mount();
assert.equal(h.calls.length, 0);
page.state.activeTab.value = "materials";
await vue.nextTick();
const shared = page.state.loadMaterials();
assert.equal(h.calls.length, 1);
pending.resolve({ data: [{ id: 5 }] });
await shared;
assert.equal(page.state.materialTypes.value[0].id, 5);
page.stop();
});
test("task form fetches only the selected template's materials and preserves names and HR fields", async () => {
const h = harness({
onboardingTemplateDetail: () => Promise.resolve({ data: {
contracts: [{ contractTemplateId: 11, contractTemplateVersionId: 12, required: true }]
} }),
templateDetail: () => Promise.resolve({ data: {
id: 11, companyId: 1, departmentId: 10, status: "enable", templateName: "劳动合同"
} }),
variablePage: () => Promise.resolve({ data: { list: [{
variableCode: "hr.Salary", variableName: "薪资", variableType: "contract",
hrEditable: true, defaultValue: "5000", dataType: "number", required: true
}] } })
});
const page = h.page("src/views/biz/onboarding/task/index.vue");
await page.mount();
page.state.form.companyId = 1;
page.state.form.departmentId = 10;
await page.state.onOnboardingTemplateChange(7);
assert.equal(h.calls.length, 4);
assert.equal(h.calls[1].method, "onboardingTemplateDetail");
assert.equal(h.calls[2].method, "templateDetail");
assert.equal(h.calls[2].args[0].id, 11);
assert.equal(h.calls[3].method, "variablePage");
assert.equal(page.state.contractPreviews.value[0].contractName, "劳动合同");
assert.equal(page.state.form.contractVariables[0].variableValue, "5000");
assert.equal(page.state.form.contractVariables[0].contractName, "劳动合同");
assert.equal(page.state.contractConfigurationError.value, "");
page.stop();
});
test("entry template list loads just companies and its page; form data is deferred", async () => {
const h = harness();
let materials = 0;
const page = h.page("src/views/biz/onboarding/entry-config/components/OnboardingTemplatePanel.vue", {
loadMaterials: async () => { materials++; return [{ id: 7, status: "enable" }]; }
});
await page.mount();
assert.equal(h.calls.length, 2);
assert.equal(materials, 0);
page.state.query.companyId = 1;
await page.state.onQueryCompanyChange(1);
assert.equal(h.calls.length, 4);
assert.equal(h.calls[2].method, "listDepartments");
assert.equal(h.calls[3].method, "onboardingTemplatePage");
page.state.resetQuery();
await vue.nextTick();
assert.equal(h.calls.length, 5);
assert.equal(page.state.queryDepartments.value.length, 0);
await page.state.openCreate();
assert.equal(materials, 1);
assert.equal(page.state.materialTypes.value[0].id, 7);
assert.equal(h.calls.length, 5);
page.stop();
});
test("entry template list ignores older search responses", async () => {
const a = deferred(), b = deferred();
let count = 0;
const h = harness({ onboardingTemplatePage: () => (++count === 1 ? a : b).promise });
const page = h.page("src/views/biz/onboarding/entry-config/components/OnboardingTemplatePanel.vue", { loadMaterials: async () => [] });
const first = page.state.loadTemplates();
page.state.query.keyword = "new";
const second = page.state.loadTemplates();
b.resolve({ data: { list: [{ id: 2 }], total: 1 } });
await second;
a.resolve({ data: { list: [{ id: 1 }], total: 1 } });
await first;
assert.equal(page.state.rows.value[0].id, 2);
assert.equal(page.state.loading.value, false);
page.stop();
});
test("company setup defers options until needed and shares loading across fast tab changes", async () => {
const pending = deferred();
const h = harness({ listCompanies: () => pending.promise });
const page = h.page("src/views/biz/onboarding/setup/index.vue");
await page.mount();
assert.equal(h.calls.length, 0);
page.state.activeTab.value = "department";
await vue.nextTick();
page.state.activeTab.value = "position";
await vue.nextTick();
assert.equal(h.calls.length, 1);
pending.resolve({ data: [{ id: 1, companyName: "A" }] });
await page.state.loadCompanies();
page.state.activeTab.value = "department";
await vue.nextTick();
assert.equal(h.calls.length, 1);
page.stop();
});