查询方式优化

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
+59
View File
@@ -0,0 +1,59 @@
import { ref } from "vue";
import {
onboardingTaskApi,
type OnboardingDepartmentOption,
type OnboardingPositionOption
} from "@/api/modules/biz/onboarding";
type ScopeSelection = { companyId?: number; departmentId?: number; positionId?: number };
/** 公司、部门、岗位只按当前选择加载;过期响应不能覆盖新选择。 */
export function useOnboardingScopeOptions(
getSelection: () => ScopeSelection | undefined,
syncOptions?: () => void
) {
const departments = ref<OnboardingDepartmentOption[]>([]);
const positions = ref<OnboardingPositionOption[]>([]);
let companyRequest = 0;
let departmentRequest = 0;
function reset() {
companyRequest++;
departmentRequest++;
departments.value = [];
positions.value = [];
const selection = getSelection();
if (selection) {
selection.departmentId = undefined;
selection.positionId = undefined;
}
syncOptions?.();
}
async function onCompanyChange(companyId?: number) {
reset();
if (!companyId) return;
const request = companyRequest;
const { data } = await onboardingTaskApi.listDepartments(companyId).catch(() => ({ data: [] }));
if (request !== companyRequest || getSelection()?.companyId !== companyId) return;
departments.value = data || [];
syncOptions?.();
}
async function onDepartmentChange(departmentId?: number) {
const request = ++departmentRequest;
positions.value = [];
const selection = getSelection();
if (selection) selection.positionId = undefined;
syncOptions?.();
const companyId = selection?.companyId;
if (!companyId || !departmentId) return;
const { data } = await onboardingTaskApi.listPositionsByDepartment(departmentId).catch(() => ({ data: [] }));
const current = getSelection();
if (request !== departmentRequest || current?.companyId !== companyId || current?.departmentId !== departmentId) return;
positions.value = data || [];
syncOptions?.();
}
return { departments, positions, onCompanyChange, onDepartmentChange, reset };
}
+34 -95
View File
@@ -5,6 +5,7 @@
title="签署材料模板列表" title="签署材料模板列表"
:columns="templateColumns" :columns="templateColumns"
:request-api="onboardingTaskApi.templatePage" :request-api="onboardingTaskApi.templatePage"
@reset="resetSearchScope"
> >
<template #tableHeader> <template #tableHeader>
<el-button v-auth="'onboardingContractAdd'" type="primary" icon="Plus" @click="openTemplateCreate"> <el-button v-auth="'onboardingContractAdd'" type="primary" icon="Plus" @click="openTemplateCreate">
@@ -584,6 +585,7 @@
</template> </template>
<script setup lang="ts" name="onboardingContract"> <script setup lang="ts" name="onboardingContract">
import { useOnboardingScopeOptions } from "@/hooks/useOnboardingScopeOptions";
import { computed, onMounted, reactive, ref } from "vue"; import { computed, onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -615,10 +617,19 @@ const companies = ref<OnboardingCompanyOption[]>([]);
const departments = ref<OnboardingDepartmentOption[]>([]); const departments = ref<OnboardingDepartmentOption[]>([]);
const positions = ref<OnboardingPositionOption[]>([]); const positions = ref<OnboardingPositionOption[]>([]);
const searchCompanies = ref<OnboardingCompanyOption[]>([]); const searchCompanies = ref<OnboardingCompanyOption[]>([]);
const searchDepartments = ref<OnboardingDepartmentOption[]>([]); const {
const searchPositions = ref<OnboardingPositionOption[]>([]); departments: searchDepartments,
const allSearchDepartments = ref<OnboardingDepartmentOption[]>([]); positions: searchPositions,
const allSearchPositions = ref<OnboardingPositionOption[]>([]); onCompanyChange: onSearchCompanyChange,
onDepartmentChange: onSearchDepartmentChange,
reset: resetSearchScope
} = useOnboardingScopeOptions(
() => templateTable.value?.searchParam,
() => {
templateTable.value?.enumMap.set("departmentId", departmentSearchOptions.value);
templateTable.value?.enumMap.set("positionId", positionSearchOptions.value);
}
);
const companySearchOptions = computed(() => const companySearchOptions = computed(() =>
searchCompanies.value.map(c => ({ searchCompanies.value.map(c => ({
@@ -641,58 +652,6 @@ const positionSearchOptions = computed(() =>
})) }))
); );
async function onSearchCompanyChange(companyId?: number) {
if (companyId) {
const [depRes, posRes] = await Promise.all([
onboardingTaskApi.listDepartments(companyId).catch(() => ({ data: [] })),
onboardingTaskApi.listPositions(companyId).catch(() => ({ data: [] }))
]);
searchDepartments.value = depRes.data || [];
searchPositions.value = posRes.data || [];
} else {
searchDepartments.value = allSearchDepartments.value;
searchPositions.value = allSearchPositions.value;
}
templateTable.value?.enumMap.set("departmentId", departmentSearchOptions.value);
templateTable.value?.enumMap.set("positionId", positionSearchOptions.value);
if (templateTable.value?.searchParam) {
const currentDept = templateTable.value.searchParam.departmentId;
if (currentDept && !searchDepartments.value.some(d => d.id === currentDept)) {
templateTable.value.searchParam.departmentId = undefined;
}
const currentPos = templateTable.value.searchParam.positionId;
if (currentPos && !searchPositions.value.some(p => p.id === currentPos)) {
templateTable.value.searchParam.positionId = undefined;
}
}
}
async function onSearchDepartmentChange(departmentId?: number) {
if (departmentId) {
const posRes = await onboardingTaskApi.listPositionsByDepartment(departmentId).catch(() => ({ data: [] }));
searchPositions.value = posRes.data || [];
} else {
const selectedCompanyId = templateTable.value?.searchParam?.companyId;
if (selectedCompanyId) {
const posRes = await onboardingTaskApi.listPositions(selectedCompanyId).catch(() => ({ data: [] }));
searchPositions.value = posRes.data || [];
} else {
searchPositions.value = allSearchPositions.value;
}
}
templateTable.value?.enumMap.set("positionId", positionSearchOptions.value);
if (templateTable.value?.searchParam) {
const currentPos = templateTable.value.searchParam.positionId;
if (currentPos && !searchPositions.value.some(p => p.id === currentPos)) {
templateTable.value.searchParam.positionId = undefined;
}
}
}
const versions = ref<OnboardingContractVersion[]>([]); const versions = ref<OnboardingContractVersion[]>([]);
type VersionSealField = "companySealEnabled" | "acrossPageSealEnabled"; type VersionSealField = "companySealEnabled" | "acrossPageSealEnabled";
const sealUpdating = reactive<Record<number, VersionSealField | undefined>>({}); const sealUpdating = reactive<Record<number, VersionSealField | undefined>>({});
@@ -797,7 +756,12 @@ const templateColumns: ColumnProps<OnboardingContractTemplate>[] = [
props: { props: {
filterable: true, filterable: true,
clearable: true, clearable: true,
placeholder: "请选择所属部门", get placeholder() {
return templateTable.value?.searchParam?.companyId ? "请选择所属部门" : "请先选择所属公司";
},
get disabled() {
return !templateTable.value?.searchParam?.companyId;
},
onChange: (val?: number) => onSearchDepartmentChange(val) onChange: (val?: number) => onSearchDepartmentChange(val)
} }
} }
@@ -809,7 +773,16 @@ const templateColumns: ColumnProps<OnboardingContractTemplate>[] = [
enum: positionSearchOptions, enum: positionSearchOptions,
search: { search: {
el: "select", el: "select",
props: { filterable: true, clearable: true, placeholder: "请选择适用岗位" } props: {
filterable: true,
clearable: true,
get placeholder() {
return templateTable.value?.searchParam?.departmentId ? "请选择适用岗位" : "请先选择所属部门";
},
get disabled() {
return !templateTable.value?.searchParam?.departmentId;
}
}
} }
}, },
{ prop: "templateCode", label: "模板编码", minWidth: 160, search: { el: "input" } }, { prop: "templateCode", label: "模板编码", minWidth: 160, search: { el: "input" } },
@@ -1138,43 +1111,9 @@ async function saveVariable() {
} }
async function loadSearchOptions() { async function loadSearchOptions() {
try { await loadCompanies();
await loadCompanies(); searchCompanies.value = companies.value;
searchCompanies.value = companies.value; templateTable.value?.enumMap.set("companyId", companySearchOptions.value);
const deptList: OnboardingDepartmentOption[] = [];
const posList: OnboardingPositionOption[] = [];
if (companies.value && companies.value.length > 0) {
const promises = companies.value.map(async comp => {
const [depRes, posRes] = await Promise.all([
onboardingTaskApi.listDepartments(comp.id).catch(() => ({ data: [] })),
onboardingTaskApi.listPositions(comp.id).catch(() => ({ data: [] }))
]);
if (depRes.data) deptList.push(...depRes.data);
if (posRes.data) posList.push(...posRes.data);
});
await Promise.all(promises);
}
const uniqueDeptMap = new Map<number, OnboardingDepartmentOption>();
deptList.forEach(d => uniqueDeptMap.set(d.id, d));
const finalDepts = Array.from(uniqueDeptMap.values());
allSearchDepartments.value = finalDepts;
searchDepartments.value = finalDepts;
const uniquePosMap = new Map<number, OnboardingPositionOption>();
posList.forEach(p => uniquePosMap.set(p.id, p));
const finalPositions = Array.from(uniquePosMap.values());
allSearchPositions.value = finalPositions;
searchPositions.value = finalPositions;
templateTable.value?.enumMap.set("companyId", companySearchOptions.value);
templateTable.value?.enumMap.set("departmentId", departmentSearchOptions.value);
templateTable.value?.enumMap.set("positionId", positionSearchOptions.value);
} catch (e) {
console.error("加载签署材料搜索选项失败", e);
}
} }
onMounted(loadSearchOptions); onMounted(loadSearchOptions);
+36 -100
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="table-box"> <div class="table-box">
<ProTable ref="proTable" title="员工档案" :columns="columns" :request-api="getTableData"> <ProTable ref="proTable" title="员工档案" :columns="columns" :request-api="getTableData" @reset="resetSearchScope">
<template #tableHeader> <template #tableHeader>
<div class="employee-table-toolbar"> <div class="employee-table-toolbar">
<el-button v-auth="'onboardingEmployeeExport'" type="primary" plain icon="Download" :loading="exporting" @click="exportEmployees"> <el-button v-auth="'onboardingEmployeeExport'" type="primary" plain icon="Download" :loading="exporting" @click="exportEmployees">
@@ -576,6 +576,7 @@
</template> </template>
<script setup lang="ts" name="onboardingEmployee"> <script setup lang="ts" name="onboardingEmployee">
import { useOnboardingScopeOptions } from "@/hooks/useOnboardingScopeOptions";
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue"; import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
import { import {
Briefcase, Briefcase,
@@ -596,9 +597,7 @@ import type { UploadFile, UploadRequestOptions, UploadUserFile } from "element-p
import { import {
onboardingTaskApi, onboardingTaskApi,
type OnboardingEmployee, type OnboardingEmployee,
type OnboardingCompanyOption, type OnboardingCompanyOption
type OnboardingDepartmentOption,
type OnboardingPositionOption
} from "@/api/modules/biz/onboarding"; } from "@/api/modules/biz/onboarding";
import type { ColumnProps, ProTableInstance } from "@/components/ProTable/interface"; import type { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { getMessageBoxPromptResult, isMessageBoxConfirmed } from "@/utils/messageBox"; import { getMessageBoxPromptResult, isMessageBoxConfirmed } from "@/utils/messageBox";
@@ -806,10 +805,19 @@ const imageMimeTypes: Record<string, string> = {
}; };
const searchCompanies = ref<OnboardingCompanyOption[]>([]); const searchCompanies = ref<OnboardingCompanyOption[]>([]);
const searchDepartments = ref<OnboardingDepartmentOption[]>([]); const {
const searchPositions = ref<OnboardingPositionOption[]>([]); departments: searchDepartments,
const allSearchDepartments = ref<OnboardingDepartmentOption[]>([]); positions: searchPositions,
const allSearchPositions = ref<OnboardingPositionOption[]>([]); onCompanyChange: onSearchCompanyChange,
onDepartmentChange: onSearchDepartmentChange,
reset: resetSearchScope
} = useOnboardingScopeOptions(
() => proTable.value?.searchParam,
() => {
proTable.value?.enumMap.set("departmentId", departmentSearchOptions.value);
proTable.value?.enumMap.set("positionId", positionSearchOptions.value);
}
);
const companySearchOptions = computed(() => const companySearchOptions = computed(() =>
searchCompanies.value.map(c => ({ searchCompanies.value.map(c => ({
@@ -832,58 +840,6 @@ const positionSearchOptions = computed(() =>
})) }))
); );
async function onSearchCompanyChange(companyId?: number) {
if (companyId) {
const [depRes, posRes] = await Promise.all([
onboardingTaskApi.listDepartments(companyId).catch(() => ({ data: [] })),
onboardingTaskApi.listPositions(companyId).catch(() => ({ data: [] }))
]);
searchDepartments.value = depRes.data || [];
searchPositions.value = posRes.data || [];
} else {
searchDepartments.value = allSearchDepartments.value;
searchPositions.value = allSearchPositions.value;
}
proTable.value?.enumMap.set("departmentId", departmentSearchOptions.value);
proTable.value?.enumMap.set("positionId", positionSearchOptions.value);
if (proTable.value?.searchParam) {
const currentDept = proTable.value.searchParam.departmentId;
if (currentDept && !searchDepartments.value.some(d => d.id === currentDept)) {
proTable.value.searchParam.departmentId = undefined;
}
const currentPos = proTable.value.searchParam.positionId;
if (currentPos && !searchPositions.value.some(p => p.id === currentPos)) {
proTable.value.searchParam.positionId = undefined;
}
}
}
async function onSearchDepartmentChange(departmentId?: number) {
if (departmentId) {
const posRes = await onboardingTaskApi.listPositionsByDepartment(departmentId).catch(() => ({ data: [] }));
searchPositions.value = posRes.data || [];
} else {
const selectedCompanyId = proTable.value?.searchParam?.companyId;
if (selectedCompanyId) {
const posRes = await onboardingTaskApi.listPositions(selectedCompanyId).catch(() => ({ data: [] }));
searchPositions.value = posRes.data || [];
} else {
searchPositions.value = allSearchPositions.value;
}
}
proTable.value?.enumMap.set("positionId", positionSearchOptions.value);
if (proTable.value?.searchParam) {
const currentPos = proTable.value.searchParam.positionId;
if (currentPos && !searchPositions.value.some(p => p.id === currentPos)) {
proTable.value.searchParam.positionId = undefined;
}
}
}
function parseJsonList(value?: string) { function parseJsonList(value?: string) {
if (!value) return []; if (!value) return [];
@@ -1083,7 +1039,12 @@ const columns: ColumnProps<OnboardingEmployee>[] = [
props: { props: {
filterable: true, filterable: true,
clearable: true, clearable: true,
placeholder: "请选择所属部门", get placeholder() {
return proTable.value?.searchParam?.companyId ? "请选择所属部门" : "请先选择所属公司";
},
get disabled() {
return !proTable.value?.searchParam?.companyId;
},
onChange: (val?: number) => onSearchDepartmentChange(val) onChange: (val?: number) => onSearchDepartmentChange(val)
} }
} }
@@ -1095,7 +1056,16 @@ const columns: ColumnProps<OnboardingEmployee>[] = [
enum: positionSearchOptions, enum: positionSearchOptions,
search: { search: {
el: "select", el: "select",
props: { filterable: true, clearable: true, placeholder: "请选择入职岗位" } props: {
filterable: true,
clearable: true,
get placeholder() {
return proTable.value?.searchParam?.departmentId ? "请选择入职岗位" : "请先选择所属部门";
},
get disabled() {
return !proTable.value?.searchParam?.departmentId;
}
}
} }
}, },
{ {
@@ -1713,43 +1683,9 @@ async function download(fileId: number, fallbackFileName?: string) {
} }
async function loadSearchOptions() { async function loadSearchOptions() {
try { const { data } = await onboardingTaskApi.listCompanies();
const { data } = await onboardingTaskApi.listCompanies().catch(() => ({ data: [] })); searchCompanies.value = data || [];
searchCompanies.value = data || []; proTable.value?.enumMap.set("companyId", companySearchOptions.value);
const deptList: OnboardingDepartmentOption[] = [];
const posList: OnboardingPositionOption[] = [];
if (data && data.length > 0) {
const promises = data.map(async (comp: any) => {
const [depRes, posRes] = await Promise.all([
onboardingTaskApi.listDepartments(comp.id).catch(() => ({ data: [] })),
onboardingTaskApi.listPositions(comp.id).catch(() => ({ data: [] }))
]);
if (depRes.data) deptList.push(...depRes.data);
if (posRes.data) posList.push(...posRes.data);
});
await Promise.all(promises);
}
const uniqueDeptMap = new Map<number, OnboardingDepartmentOption>();
deptList.forEach(d => uniqueDeptMap.set(d.id, d));
const finalDepts = Array.from(uniqueDeptMap.values());
allSearchDepartments.value = finalDepts;
searchDepartments.value = finalDepts;
const uniquePosMap = new Map<number, OnboardingPositionOption>();
posList.forEach(p => uniquePosMap.set(p.id, p));
const finalPositions = Array.from(uniquePosMap.values());
allSearchPositions.value = finalPositions;
searchPositions.value = finalPositions;
proTable.value?.enumMap.set("companyId", companySearchOptions.value);
proTable.value?.enumMap.set("departmentId", departmentSearchOptions.value);
proTable.value?.enumMap.set("positionId", positionSearchOptions.value);
} catch (e) {
console.error("加载员工档案搜索选项失败", e);
}
} }
onMounted(loadSearchOptions); onMounted(loadSearchOptions);
@@ -2333,4 +2269,4 @@ onBeforeUnmount(clearMaterialPreviews);
color: #fff; color: #fff;
background: var(--metric-color); background: var(--metric-color);
} }
</style> </style>
@@ -16,7 +16,6 @@
placeholder="全部公司" placeholder="全部公司"
class="company-filter" class="company-filter"
@change="onQueryCompanyChange" @change="onQueryCompanyChange"
@clear="onQueryCompanyChange"
> >
<el-option v-for="item in companies" :key="item.id" :label="item.companyName" :value="item.id" /> <el-option v-for="item in companies" :key="item.id" :label="item.companyName" :value="item.id" />
</el-select> </el-select>
@@ -24,10 +23,10 @@
v-model="query.departmentId" v-model="query.departmentId"
clearable clearable
filterable filterable
placeholder="全部部门" :placeholder="query.companyId ? '全部部门' : '请先选择所属公司'"
:disabled="!query.companyId"
class="department-filter" class="department-filter"
@change="onQueryDepartmentChange" @change="onQueryDepartmentChange"
@clear="onQueryDepartmentChange"
> >
<el-option v-for="item in queryDepartments" :key="item.id" :label="item.departmentName" :value="item.id" /> <el-option v-for="item in queryDepartments" :key="item.id" :label="item.departmentName" :value="item.id" />
</el-select> </el-select>
@@ -35,10 +34,10 @@
v-model="query.positionId" v-model="query.positionId"
clearable clearable
filterable filterable
placeholder="全部岗位" :placeholder="query.departmentId ? '全部岗位' : '请先选择所属部门'"
:disabled="!query.departmentId"
class="position-filter" class="position-filter"
@change="loadTemplates" @change="loadTemplates"
@clear="loadTemplates"
> >
<el-option v-for="item in queryPositions" :key="item.id" :label="item.positionName" :value="item.id" /> <el-option v-for="item in queryPositions" :key="item.id" :label="item.positionName" :value="item.id" />
</el-select> </el-select>
@@ -48,7 +47,6 @@
placeholder="模板状态" placeholder="模板状态"
class="status-filter" class="status-filter"
@change="loadTemplates" @change="loadTemplates"
@clear="loadTemplates"
> >
<el-option label="全部状态" value="" /> <el-option label="全部状态" value="" />
<el-option label="启用" value="enable" /> <el-option label="启用" value="enable" />
@@ -304,16 +302,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue"; import { computed, onMounted, reactive, ref } from "vue";
import { useOnboardingScopeOptions } from "@/hooks/useOnboardingScopeOptions";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { import {
OnboardingCompany, OnboardingCompanyOption,
OnboardingContractTemplate, OnboardingContractTemplate,
OnboardingContractVersion, OnboardingContractVersion,
OnboardingDepartmentOption, OnboardingDepartmentOption,
OnboardingMaterialType, OnboardingMaterialType,
OnboardingPosition, OnboardingPosition,
OnboardingPositionOption,
OnboardingTemplate, OnboardingTemplate,
onboardingTaskApi onboardingTaskApi
} from "@/api/modules/biz/onboarding"; } from "@/api/modules/biz/onboarding";
@@ -328,9 +326,8 @@ const visible = ref(false);
const rows = ref<OnboardingTemplate[]>([]); const rows = ref<OnboardingTemplate[]>([]);
const total = ref(0); const total = ref(0);
const companies = ref<OnboardingCompany[]>([]); const props = defineProps<{ loadMaterials: () => Promise<OnboardingMaterialType[]> }>();
const queryDepartments = ref<OnboardingDepartmentOption[]>([]); const companies = ref<OnboardingCompanyOption[]>([]);
const queryPositions = ref<OnboardingPositionOption[]>([]);
const departments = ref<OnboardingDepartmentOption[]>([]); const departments = ref<OnboardingDepartmentOption[]>([]);
const positions = ref<OnboardingPosition[]>([]); const positions = ref<OnboardingPosition[]>([]);
const contractTemplates = ref<OnboardingContractTemplate[]>([]); const contractTemplates = ref<OnboardingContractTemplate[]>([]);
@@ -364,96 +361,54 @@ const form = reactive<OnboardingTemplate>({
materials: [] materials: []
}); });
async function loadAllQueryOptions() { const {
if (!companies.value || companies.value.length === 0) return; departments: queryDepartments,
const deptList: OnboardingDepartmentOption[] = []; positions: queryPositions,
const posList: OnboardingPositionOption[] = []; onCompanyChange: changeQueryCompany,
onDepartmentChange: changeQueryDepartment,
reset: resetQueryScope
} = useOnboardingScopeOptions(() => query);
const promises = companies.value.map(async comp => { let companiesLoaded = false;
const [depRes, posRes] = await Promise.all([ let companyRequest: Promise<void> | undefined;
onboardingTaskApi.listDepartments(comp.id).catch(() => ({ data: [] })), async function loadCompanies() {
onboardingTaskApi.listPositions(comp.id).catch(() => ({ data: [] })) if (companiesLoaded) return;
]); if (!companyRequest) {
if (depRes.data) deptList.push(...depRes.data); companyRequest = onboardingTaskApi.listCompanies().then(({ data }) => {
if (posRes.data) posList.push(...posRes.data); companies.value = data || [];
}); companiesLoaded = true;
await Promise.all(promises); }).finally(() => { companyRequest = undefined; });
}
const uniqueDeptMap = new Map<number, OnboardingDepartmentOption>(); await companyRequest;
deptList.forEach(d => uniqueDeptMap.set(d.id, d));
queryDepartments.value = Array.from(uniqueDeptMap.values());
const uniquePosMap = new Map<number, OnboardingPositionOption>();
posList.forEach(p => uniquePosMap.set(p.id, p));
queryPositions.value = Array.from(uniquePosMap.values());
} }
async function loadBaseData() { async function loadBaseData() {
const [companyRes, materialRes] = await Promise.all([ const [, materials] = await Promise.all([loadCompanies(), props.loadMaterials()]);
onboardingTaskApi.companyPage({ pageNum: 1, pageSize: 100, status: "enable" }).catch(() => ({ data: { list: [] } })), materialTypes.value = materials;
onboardingTaskApi.listMaterialTypes().catch(() => ({ data: [] }))
]);
companies.value = companyRes.data?.list || [];
materialTypes.value = materialRes.data || [];
await loadAllQueryOptions();
} }
let templateRequest = 0;
async function loadTemplates() { async function loadTemplates() {
const request = ++templateRequest;
loading.value = true; loading.value = true;
try { try {
const res = await onboardingTaskApi.onboardingTemplatePage(query); const res = await onboardingTaskApi.onboardingTemplatePage({ ...query });
if (request !== templateRequest) return;
rows.value = res.data?.list || []; rows.value = res.data?.list || [];
total.value = res.data?.total || 0; total.value = res.data?.total || 0;
} finally { } finally {
loading.value = false; if (request === templateRequest) loading.value = false;
} }
} }
async function onQueryCompanyChange(companyId?: number) { async function onQueryCompanyChange(companyId?: number) {
query.departmentId = undefined; query.pageNum = 1;
query.positionId = undefined; await Promise.all([changeQueryCompany(companyId), loadTemplates()]);
if (!companyId) {
await loadAllQueryOptions();
} else {
const [depRes, posRes] = await Promise.all([
onboardingTaskApi.listDepartments(companyId).catch(() => ({ data: [] })),
onboardingTaskApi.listPositions(companyId).catch(() => ({ data: [] }))
]);
queryDepartments.value = depRes.data || [];
queryPositions.value = (posRes.data || []).map(p => ({
id: p.id,
positionName: p.positionName,
companyId: companyId,
departmentId: p.departmentId
} as any));
}
loadTemplates();
} }
async function onQueryDepartmentChange(departmentId?: number) { async function onQueryDepartmentChange(departmentId?: number) {
query.positionId = undefined; query.pageNum = 1;
if (!departmentId) { await Promise.all([changeQueryDepartment(departmentId), loadTemplates()]);
if (query.companyId) {
const posRes = await onboardingTaskApi.listPositions(query.companyId).catch(() => ({ data: [] }));
queryPositions.value = (posRes.data || []).map(p => ({
id: p.id,
positionName: p.positionName,
companyId: query.companyId!,
departmentId: p.departmentId
} as any));
} else {
await loadAllQueryOptions();
}
} else {
const posRes = await onboardingTaskApi.listPositionsByDepartment(departmentId).catch(() => ({ data: [] }));
queryPositions.value = (posRes.data || []).map(p => ({
id: p.id,
positionName: p.positionName,
companyId: p.companyId,
departmentId: departmentId
} as any));
}
loadTemplates();
} }
function resetQuery() { function resetQuery() {
@@ -463,7 +418,7 @@ function resetQuery() {
query.positionId = undefined; query.positionId = undefined;
query.status = "enable"; query.status = "enable";
query.pageNum = 1; query.pageNum = 1;
loadAllQueryOptions(); resetQueryScope();
loadTemplates(); loadTemplates();
} }
@@ -744,7 +699,7 @@ async function removeTemplate(row: OnboardingTemplate) {
} }
onMounted(async () => { onMounted(async () => {
await Promise.all([loadBaseData(), loadTemplates()]); await Promise.all([loadCompanies(), loadTemplates()]);
}); });
</script> </script>
@@ -1,13 +1,13 @@
<template> <template>
<div class="main-box config-container"> <div class="main-box config-container">
<el-tabs v-model="activeTab" class="config-tabs"> <el-tabs v-model="activeTab" class="config-tabs">
<el-tab-pane label="合同变量字典" name="variables"> <el-tab-pane lazy label="合同变量字典" name="variables">
<div class="tab-card-wrapper"> <div class="tab-card-wrapper">
<VariableDefinitionPanel /> <VariableDefinitionPanel />
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="上传资料类型" name="materials"> <el-tab-pane lazy label="上传资料类型" name="materials">
<div class="tab-card-wrapper material-tab-wrapper"> <div class="tab-card-wrapper material-tab-wrapper">
<div class="toolbar"> <div class="toolbar">
<el-button v-auth="'onboardingEntryConfigAdd'" type="primary" icon="Plus" @click="openMaterialCreate"> <el-button v-auth="'onboardingEntryConfigAdd'" type="primary" icon="Plus" @click="openMaterialCreate">
@@ -64,9 +64,9 @@
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="入职模板" name="templates"> <el-tab-pane lazy label="入职模板" name="templates">
<div class="tab-card-wrapper"> <div class="tab-card-wrapper">
<OnboardingTemplatePanel /> <OnboardingTemplatePanel :load-materials="loadMaterials" />
</div> </div>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
@@ -113,7 +113,7 @@
</template> </template>
<script setup lang="ts" name="onboardingEntryConfig"> <script setup lang="ts" name="onboardingEntryConfig">
import { onMounted, reactive, ref } from "vue"; import { reactive, ref, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { import {
OnboardingMaterialType, OnboardingMaterialType,
@@ -140,9 +140,15 @@ const materialForm = reactive<Partial<OnboardingMaterialType>>({
status: "enable" status: "enable"
}); });
let materialRequest: Promise<OnboardingMaterialType[]> | undefined;
async function loadMaterials() { async function loadMaterials() {
const res = await onboardingTaskApi.listMaterialTypes(); if (!materialRequest) {
materialTypes.value = res.data || []; materialRequest = onboardingTaskApi.listMaterialTypes().then(({ data }) => {
materialTypes.value = data || [];
return materialTypes.value;
}).finally(() => { materialRequest = undefined; });
}
return materialRequest;
} }
function openMaterialCreate() { function openMaterialCreate() {
@@ -184,8 +190,8 @@ async function removeMaterial(row: OnboardingMaterialType) {
await loadMaterials(); await loadMaterials();
} }
onMounted(() => { watch(activeTab, tab => {
loadMaterials(); if (tab === "materials") void loadMaterials();
}); });
</script> </script>
+14 -7
View File
@@ -2,7 +2,7 @@
<div class="main-box setup-container"> <div class="main-box setup-container">
<el-tabs v-model="activeTab" class="setup-tabs"> <el-tabs v-model="activeTab" class="setup-tabs">
<!-- 1. 入职公司管理 --> <!-- 1. 入职公司管理 -->
<el-tab-pane label="入职公司" name="company"> <el-tab-pane lazy label="入职公司" name="company">
<div class="table-wrapper"> <div class="table-wrapper">
<ProTable <ProTable
ref="companyTable" ref="companyTable"
@@ -62,7 +62,7 @@
</el-tab-pane> </el-tab-pane>
<!-- 2. 入职部门管理 --> <!-- 2. 入职部门管理 -->
<el-tab-pane label="入职部门" name="department"> <el-tab-pane lazy label="入职部门" name="department">
<div class="table-wrapper"> <div class="table-wrapper">
<ProTable <ProTable
ref="departmentTable" ref="departmentTable"
@@ -127,7 +127,7 @@
</el-tab-pane> </el-tab-pane>
<!-- 3. 入职岗位管理 --> <!-- 3. 入职岗位管理 -->
<el-tab-pane label="入职岗位" name="position"> <el-tab-pane lazy label="入职岗位" name="position">
<div class="table-wrapper"> <div class="table-wrapper">
<ProTable <ProTable
ref="positionTable" ref="positionTable"
@@ -452,7 +452,7 @@
</template> </template>
<script setup lang="ts" name="onboardingSetup"> <script setup lang="ts" name="onboardingSetup">
import { ref, reactive, computed, onMounted, nextTick } from "vue"; import { ref, reactive, computed, watch, nextTick } from "vue";
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus"; import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
import { Plus, Edit, Delete } from "@element-plus/icons-vue"; import { Plus, Edit, Delete } from "@element-plus/icons-vue";
import { import {
@@ -779,9 +779,14 @@ const positionColumns: ColumnProps<OnboardingPosition>[] = [
} }
]; ];
let companyRequest: Promise<void> | undefined;
async function loadCompanies() { async function loadCompanies() {
const { data } = await onboardingTaskApi.listCompanies(); if (!companyRequest) {
companies.value = data || []; companyRequest = onboardingTaskApi.listCompanies().then(({ data }) => {
companies.value = data || [];
}).finally(() => { companyRequest = undefined; });
}
await companyRequest;
} }
async function loadDepartmentsForCompany(companyId?: number) { async function loadDepartmentsForCompany(companyId?: number) {
@@ -938,7 +943,9 @@ async function removePosition(row: OnboardingPosition) {
positionTable.value?.refresh(); positionTable.value?.refresh();
} }
onMounted(loadCompanies); watch(activeTab, tab => {
if (tab !== "company" && !companies.value.length) void loadCompanies();
});
</script> </script>
<style scoped> <style scoped>
+35 -100
View File
@@ -7,6 +7,7 @@
title="入职任务列表" title="入职任务列表"
:columns="columns" :columns="columns"
:request-api="getTableData" :request-api="getTableData"
@reset="resetSearchScope"
> >
<template #tableHeader> <template #tableHeader>
<div class="task-table-toolbar"> <div class="task-table-toolbar">
@@ -1521,6 +1522,7 @@
</template> </template>
<script setup lang="ts" name="onboardingTask"> <script setup lang="ts" name="onboardingTask">
import { useOnboardingScopeOptions } from "@/hooks/useOnboardingScopeOptions";
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from "vue"; import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from "vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { import {
@@ -1566,10 +1568,19 @@ const companies = ref<OnboardingCompanyOption[]>([]);
const departments = ref<OnboardingDepartmentOption[]>([]); const departments = ref<OnboardingDepartmentOption[]>([]);
const positions = ref<OnboardingPositionOption[]>([]); const positions = ref<OnboardingPositionOption[]>([]);
const searchCompanies = ref<OnboardingCompanyOption[]>([]); const searchCompanies = ref<OnboardingCompanyOption[]>([]);
const searchDepartments = ref<OnboardingDepartmentOption[]>([]); const {
const searchPositions = ref<OnboardingPositionOption[]>([]); departments: searchDepartments,
const allSearchDepartments = ref<OnboardingDepartmentOption[]>([]); positions: searchPositions,
const allSearchPositions = ref<OnboardingPositionOption[]>([]); onCompanyChange: onSearchCompanyChange,
onDepartmentChange: onSearchDepartmentChange,
reset: resetSearchScope
} = useOnboardingScopeOptions(
() => proTable.value?.searchParam,
() => {
proTable.value?.enumMap.set("departmentId", departmentSearchOptions.value);
proTable.value?.enumMap.set("positionId", positionSearchOptions.value);
}
);
const companySearchOptions = computed(() => const companySearchOptions = computed(() =>
searchCompanies.value.map(c => ({ searchCompanies.value.map(c => ({
@@ -1592,58 +1603,6 @@ const positionSearchOptions = computed(() =>
})) }))
); );
async function onSearchCompanyChange(companyId?: number) {
if (companyId) {
const [depRes, posRes] = await Promise.all([
onboardingTaskApi.listDepartments(companyId).catch(() => ({ data: [] })),
onboardingTaskApi.listPositions(companyId).catch(() => ({ data: [] }))
]);
searchDepartments.value = depRes.data || [];
searchPositions.value = posRes.data || [];
} else {
searchDepartments.value = allSearchDepartments.value;
searchPositions.value = allSearchPositions.value;
}
proTable.value?.enumMap.set("departmentId", departmentSearchOptions.value);
proTable.value?.enumMap.set("positionId", positionSearchOptions.value);
if (proTable.value?.searchParam) {
const currentDept = proTable.value.searchParam.departmentId;
if (currentDept && !searchDepartments.value.some(d => d.id === currentDept)) {
proTable.value.searchParam.departmentId = undefined;
}
const currentPos = proTable.value.searchParam.positionId;
if (currentPos && !searchPositions.value.some(p => p.id === currentPos)) {
proTable.value.searchParam.positionId = undefined;
}
}
}
async function onSearchDepartmentChange(departmentId?: number) {
if (departmentId) {
const posRes = await onboardingTaskApi.listPositionsByDepartment(departmentId).catch(() => ({ data: [] }));
searchPositions.value = posRes.data || [];
} else {
const selectedCompanyId = proTable.value?.searchParam?.companyId;
if (selectedCompanyId) {
const posRes = await onboardingTaskApi.listPositions(selectedCompanyId).catch(() => ({ data: [] }));
searchPositions.value = posRes.data || [];
} else {
searchPositions.value = allSearchPositions.value;
}
}
proTable.value?.enumMap.set("positionId", positionSearchOptions.value);
if (proTable.value?.searchParam) {
const currentPos = proTable.value.searchParam.positionId;
if (currentPos && !searchPositions.value.some(p => p.id === currentPos)) {
proTable.value.searchParam.positionId = undefined;
}
}
}
const onboardingTemplates = ref<OnboardingTemplate[]>([]); const onboardingTemplates = ref<OnboardingTemplate[]>([]);
const contractTemplates = ref<OnboardingContractTemplate[]>([]); const contractTemplates = ref<OnboardingContractTemplate[]>([]);
@@ -1952,7 +1911,12 @@ const columns: ColumnProps<OnboardingTask>[] = [
props: { props: {
filterable: true, filterable: true,
clearable: true, clearable: true,
placeholder: "请选择所属部门", get placeholder() {
return proTable.value?.searchParam?.companyId ? "请选择所属部门" : "请先选择所属公司";
},
get disabled() {
return !proTable.value?.searchParam?.companyId;
},
onChange: (val?: number) => onSearchDepartmentChange(val) onChange: (val?: number) => onSearchDepartmentChange(val)
} }
} }
@@ -1964,7 +1928,16 @@ const columns: ColumnProps<OnboardingTask>[] = [
enum: positionSearchOptions, enum: positionSearchOptions,
search: { search: {
el: "select", el: "select",
props: { filterable: true, clearable: true, placeholder: "请选择入职岗位" } props: {
filterable: true,
clearable: true,
get placeholder() {
return proTable.value?.searchParam?.departmentId ? "请选择入职岗位" : "请先选择所属部门";
},
get disabled() {
return !proTable.value?.searchParam?.departmentId;
}
}
} }
}, },
{ {
@@ -2267,48 +2240,10 @@ async function getTableData(params: any) {
} }
async function loadCompanies() { async function loadCompanies() {
try { const { data } = await onboardingTaskApi.listCompanies();
const [companyResult, contractResult] = await Promise.all([ companies.value = data || [];
onboardingTaskApi.listCompanies().catch(() => ({ data: [] })), searchCompanies.value = data || [];
onboardingTaskApi.templatePage({ pageNum: 1, pageSize: 100, status: "enable" }).catch(() => ({ data: { list: [] } })) proTable.value?.enumMap.set("companyId", companySearchOptions.value);
]);
companies.value = companyResult.data || [];
searchCompanies.value = companyResult.data || [];
contractTemplates.value = contractResult.data?.list || [];
const deptList: OnboardingDepartmentOption[] = [];
const posList: OnboardingPositionOption[] = [];
if (companies.value && companies.value.length > 0) {
const promises = companies.value.map(async comp => {
const [depRes, posRes] = await Promise.all([
onboardingTaskApi.listDepartments(comp.id).catch(() => ({ data: [] })),
onboardingTaskApi.listPositions(comp.id).catch(() => ({ data: [] }))
]);
if (depRes.data) deptList.push(...depRes.data);
if (posRes.data) posList.push(...posRes.data);
});
await Promise.all(promises);
}
const uniqueDeptMap = new Map<number, OnboardingDepartmentOption>();
deptList.forEach(d => uniqueDeptMap.set(d.id, d));
const finalDepts = Array.from(uniqueDeptMap.values());
allSearchDepartments.value = finalDepts;
searchDepartments.value = finalDepts;
const uniquePosMap = new Map<number, OnboardingPositionOption>();
posList.forEach(p => uniquePosMap.set(p.id, p));
const finalPositions = Array.from(uniquePosMap.values());
allSearchPositions.value = finalPositions;
searchPositions.value = finalPositions;
proTable.value?.enumMap.set("companyId", companySearchOptions.value);
proTable.value?.enumMap.set("departmentId", departmentSearchOptions.value);
proTable.value?.enumMap.set("positionId", positionSearchOptions.value);
} catch (e) {
console.error("加载公司与搜索选项失败", e);
}
} }
async function onCompanyChange(companyId: number) { async function onCompanyChange(companyId: number) {
+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();
});