فهرست منبع

feat(s0): expand employee form to legacy fields + real-source dropdowns

雇员编辑表单对齐旧系统全字段:补出生/入职/离职日期、婚姻、工作中心、卡ID、默认生产线,
并新增城市/国家/业务电话三列(DDL 1.0.332.sql 幂等 ADD COLUMN + 实体/DTO/Controller 映射)。
关键字段下拉化真实源:部门=DepartmentMaster、岗位=GeneralizedCode(JobTitle,Comments中文名)、
默认生产线=LineMaster、工作中心=WorkCtrMaster(按域过滤,域自部门主数据派生);
性别/在职/婚姻用 0/1 字典下拉;出生/入职/离职为日期选择器。
修编辑抹空数据丢失:表单原仅9字段,submit 缺字段致后端 Update 覆盖 null,现补全字段回传保值
(实测 01371 dateEmployed/defaultWorkLocation 保值)。新增 GET employees/job-titles 岗位下拉端点。

bump version Web 2.4.285 / server 1.0.332
YY968XX 1 ماه پیش
والد
کامیت
5c697e6ac1

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.284",
+	"version": "2.4.285",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 13 - 0
Web/src/views/aidop/s0/api/s0WarehouseApi.ts

@@ -72,6 +72,9 @@ export interface S0EmployeeRow {
 	workCtr?: string | null;
 	carId?: string | null;
 	defaultWorkLocation?: string | null;
+	city?: string | null;
+	country?: string | null;
+	businessPhone?: string | null;
 	isActive: boolean;
 	createUser?: string | null;
 	createTime?: string;
@@ -98,11 +101,20 @@ export interface S0EmployeeUpsert {
 	workCtr?: string;
 	carId?: string;
 	defaultWorkLocation?: string;
+	city?: string;
+	country?: string;
+	businessPhone?: string;
 	isActive: boolean;
 	createUser?: string;
 	updateUser?: string;
 }
 
+// 岗位下拉项(后端 GeneralizedCodeMaster.FldName='JobTitle':value=编码 / label=中文名)
+export interface S0JobTitleOption {
+	value: string;
+	label: string;
+}
+
 export const s0EmployeesApi = {
 	list: (params: Record<string, unknown>) =>
 		service.get<Paged<S0EmployeeRow>>('/api/s0/warehouse/employees', { params }).then(unwrap),
@@ -111,6 +123,7 @@ export const s0EmployeesApi = {
 	update: (id: number, body: S0EmployeeUpsert) =>
 		service.put<S0EmployeeRow>(`/api/s0/warehouse/employees/${id}`, body).then(unwrap),
 	delete: (id: number) => service.delete(`/api/s0/warehouse/employees/${id}`).then(unwrap),
+	jobTitleOptions: () => service.get<S0JobTitleOption[]>('/api/s0/warehouse/employees/job-titles').then(unwrap),
 };
 
 // ========== 成本中心 ==========

+ 140 - 36
Web/src/views/aidop/s0/warehouse/EmployeeList.vue

@@ -23,12 +23,16 @@
 		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%" max-height="calc(100vh - 260px)">
 			<el-table-column prop="employee" label="员工编码" width="130" show-overflow-tooltip />
 			<el-table-column prop="name" label="姓名" width="100" show-overflow-tooltip />
-			<el-table-column prop="sex" label="性别" width="70" show-overflow-tooltip />
+			<el-table-column label="性别" width="70" show-overflow-tooltip>
+				<template #default="{ row }">{{ sexLabel(row.sex) }}</template>
+			</el-table-column>
 			<el-table-column prop="phone" label="电话" width="130" show-overflow-tooltip />
 			<el-table-column prop="department" label="部门" width="110" show-overflow-tooltip />
 			<el-table-column prop="departDescr" label="部门说明" min-width="130" show-overflow-tooltip />
 			<el-table-column prop="jobTitleDisplay" label="岗位" width="120" show-overflow-tooltip />
-			<el-table-column prop="employmentStatus" label="在职状态" width="100" show-overflow-tooltip />
+			<el-table-column label="在职状态" width="100" show-overflow-tooltip>
+				<template #default="{ row }">{{ employmentLabel(row.employmentStatus) }}</template>
+			</el-table-column>
 			<el-table-column prop="dateEmployed" label="入职日期" width="110" show-overflow-tooltip />
 			<el-table-column label="启用" width="80" align="center">
 				<template #default="{ row }">
@@ -49,53 +53,84 @@
 				@current-change="loadList" @size-change="loadList" />
 		</div>
 
-		<el-dialog v-model="dialogVisible" :title="dialogTitle" width="720px" destroy-on-close @closed="resetForm">
-			<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+		<el-dialog v-model="dialogVisible" :title="dialogTitle" width="900px" destroy-on-close @closed="resetForm">
+			<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
 				<el-row :gutter="16">
-					<el-col :span="12">
-						<el-form-item v-if="false" label="公司" prop="companyRefId">
+					<el-col v-if="false" :span="12">
+						<el-form-item label="公司" prop="companyRefId">
 							<el-select v-model="form.companyRefId" clearable filterable placeholder="请选择公司" style="width: 100%">
 								<el-option v-for="item in companyOptions" :key="item.id" :label="item.name || item.code || `${item.id}`" :value="item.id" />
 							</el-select>
 						</el-form-item>
 					</el-col>
-					<el-col :span="12">
-						<el-form-item v-if="false" label="工厂" prop="factoryRefId">
+					<el-col v-if="false" :span="12">
+						<el-form-item label="工厂" prop="factoryRefId">
 							<el-select v-model="form.factoryRefId" clearable filterable placeholder="请选择工厂" style="width: 100%" :disabled="!form.companyRefId">
 								<el-option v-for="item in formFactoryOptions" :key="item.id" :label="item.name || item.code || `${item.id}`" :value="item.id" />
 							</el-select>
 						</el-form-item>
 					</el-col>
-					<el-col :span="12">
-						<el-form-item label="员工编码" prop="employee"><el-input v-model="form.employee" :disabled="!!editingId" /></el-form-item>
-					</el-col>
-					<el-col :span="12">
-						<el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item>
-					</el-col>
-					<el-col :span="12">
-						<el-form-item label="性别"><el-input v-model="form.sex" placeholder="M/F" /></el-form-item>
-					</el-col>
-					<el-col :span="12">
-						<el-form-item label="电话"><el-input v-model="form.phone" /></el-form-item>
+					<el-col :span="8"><el-form-item label="员工编码" prop="employee"><el-input v-model="form.employee" :disabled="!!editingId" /></el-form-item></el-col>
+					<el-col :span="8"><el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item></el-col>
+					<el-col :span="8">
+						<el-form-item label="性别">
+							<el-select v-model="form.sex" clearable placeholder="请选择" style="width: 100%">
+								<el-option v-for="o in SEX_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select>
+						</el-form-item>
 					</el-col>
-					<el-col :span="12">
-						<el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item>
+					<el-col :span="8">
+						<el-form-item label="部门">
+							<el-select v-model="form.department" clearable filterable placeholder="请选择部门" style="width: 100%">
+								<el-option v-for="o in deptOptions" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select>
+						</el-form-item>
 					</el-col>
-					<el-col :span="12">
-						<el-form-item label="部门"><el-input v-model="form.department" /></el-form-item>
+					<el-col :span="8">
+						<el-form-item label="岗位">
+							<el-select v-model="form.jobTitle" clearable filterable placeholder="请选择岗位" style="width: 100%">
+								<el-option v-for="o in jobTitleOptions" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select>
+						</el-form-item>
 					</el-col>
-					<el-col :span="12">
-						<el-form-item label="岗位"><el-input v-model="form.jobTitle" /></el-form-item>
+					<el-col :span="8">
+						<el-form-item label="在职状态">
+							<el-select v-model="form.employmentStatus" clearable placeholder="请选择" style="width: 100%">
+								<el-option v-for="o in EMPLOYMENT_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select>
+						</el-form-item>
 					</el-col>
-					<el-col :span="12">
-						<el-form-item label="在职状态"><el-input v-model="form.employmentStatus" /></el-form-item>
+					<el-col :span="8">
+						<el-form-item label="默认生产线">
+							<el-select v-model="form.defaultWorkLocation" clearable filterable placeholder="请选择生产线" style="width: 100%">
+								<el-option v-for="o in lineOptions" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select>
+						</el-form-item>
 					</el-col>
-					<el-col v-if="false" :span="12">
-						<el-form-item label="域编码"><el-input v-model="form.domainCode" /></el-form-item>
+					<el-col :span="8">
+						<el-form-item label="工作中心">
+							<el-select v-model="form.workCtr" clearable filterable placeholder="请选择工作中心" style="width: 100%">
+								<el-option v-for="o in workCtrOptions" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select>
+						</el-form-item>
 					</el-col>
-					<el-col :span="12">
-						<el-form-item label="启用"><el-switch v-model="form.isActive" /></el-form-item>
+					<el-col :span="8">
+						<el-form-item label="婚姻状态">
+							<el-select v-model="form.maritalStatus" clearable placeholder="请选择" style="width: 100%">
+								<el-option v-for="o in MARITAL_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select>
+						</el-form-item>
 					</el-col>
+					<el-col :span="8"><el-form-item label="电话"><el-input v-model="form.phone" /></el-form-item></el-col>
+					<el-col :span="8"><el-form-item label="业务电话"><el-input v-model="form.businessPhone" /></el-form-item></el-col>
+					<el-col :span="8"><el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item></el-col>
+					<el-col :span="8"><el-form-item label="卡ID"><el-input v-model="form.carId" /></el-form-item></el-col>
+					<el-col :span="8"><el-form-item label="城市"><el-input v-model="form.city" /></el-form-item></el-col>
+					<el-col :span="8"><el-form-item label="国家"><el-input v-model="form.country" /></el-form-item></el-col>
+					<el-col :span="8"><el-form-item label="出生日期"><el-date-picker v-model="form.birthDate" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" /></el-form-item></el-col>
+					<el-col :span="8"><el-form-item label="入职日期"><el-date-picker v-model="form.dateEmployed" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" /></el-form-item></el-col>
+					<el-col :span="8"><el-form-item label="离职日期"><el-date-picker v-model="form.dateTerminated" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" /></el-form-item></el-col>
+					<el-col :span="8"><el-form-item label="启用"><el-switch v-model="form.isActive" /></el-form-item></el-col>
 				</el-row>
 			</el-form>
 			<template #footer>
@@ -111,9 +146,22 @@ import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
-import { s0EmployeesApi, type S0EmployeeRow, type S0EmployeeUpsert } from '../api/s0WarehouseApi';
+import { s0EmployeesApi, s0DepartmentsApi, type S0EmployeeRow, type S0EmployeeUpsert, type S0JobTitleOption } from '../api/s0WarehouseApi';
+import { s0MfgProductionLinesApi, s0MfgWorkCentersApi } from '../api/s0ManufacturingApi';
 import { loadOrgList, type OrgOption } from '../api/s0SalesApi';
 
+// 字典口径(前端固定 0/1):性别 0女1男、在职 0在职1离职、婚姻 0未婚1已婚
+const SEX_OPTIONS = [{ value: '0', label: '女' }, { value: '1', label: '男' }];
+const EMPLOYMENT_OPTIONS = [{ value: '0', label: '在职' }, { value: '1', label: '离职' }];
+const MARITAL_OPTIONS = [{ value: '0', label: '未婚' }, { value: '1', label: '已婚' }];
+function sexLabel(v?: string | null): string { return SEX_OPTIONS.find((o) => o.value === v)?.label ?? (v ?? ''); }
+function employmentLabel(v?: string | null): string { return EMPLOYMENT_OPTIONS.find((o) => o.value === v)?.label ?? (v ?? ''); }
+
+interface Option { value: string; label: string }
+interface DeptLike { department?: string | null; descr?: string | null; domainCode?: string | null }
+interface LineLike { line?: string | null; describe?: string | null }
+interface WorkCtrLike { workCtr?: string | null; descr?: string | null }
+
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || '雇员列表');
 
@@ -127,7 +175,16 @@ const editingId = ref<number | null>(null);
 const saving = ref(false);
 const formRef = ref<FormInstance>();
 
-const form = reactive<S0EmployeeUpsert>({ companyRefId: undefined, factoryRefId: undefined, domainCode: '', employee: '', name: '', sex: '', phone: '', email: '', department: '', jobTitle: '', employmentStatus: '', isActive: true });
+function emptyForm(): S0EmployeeUpsert {
+	return { companyRefId: undefined, factoryRefId: undefined, domainCode: '', employee: '', name: '', sex: '', phone: '', businessPhone: '', email: '', birthDate: null, department: '', dateEmployed: null, employmentStatus: '', maritalStatus: '', jobTitle: '', dateTerminated: null, workCtr: '', carId: '', defaultWorkLocation: '', city: '', country: '', isActive: true };
+}
+const form = reactive<S0EmployeeUpsert>(emptyForm());
+
+// 下拉数据源(真实主数据)
+const deptOptions = ref<Option[]>([]);
+const lineOptions = ref<Option[]>([]);
+const workCtrOptions = ref<Option[]>([]);
+const jobTitleOptions = ref<Option[]>([]);
 
 // 单甲方私有云口径:公司/工厂不在前端启用,统一注入全局默认组织(不依赖登录账号 tenant_id)
 const DEFAULT_COMPANY_REF_ID = '1329900200001';
@@ -159,6 +216,12 @@ watch(
 	},
 );
 
+// 日期字段回显归一:后端返回 "YYYY-MM-DD HH:mm:ss",date-picker value-format 为 YYYY-MM-DD,取日期段。
+function toDate(v?: string | null): string | null {
+	if (!v) return null;
+	return v.length >= 10 ? v.slice(0, 10) : v;
+}
+
 async function loadList() {
 	loading.value = true;
 	try {
@@ -170,13 +233,20 @@ async function loadList() {
 function resetQuery() { Object.assign(query, { keyword: '', domainCode: '', department: '', isActive: undefined, page: 1 }); void loadList(); }
 function resetForm() {
 	editingId.value = null;
-	Object.assign(form, { companyRefId: undefined, factoryRefId: undefined, domainCode: '', employee: '', name: '', sex: '', phone: '', email: '', department: '', jobTitle: '', employmentStatus: '', isActive: true });
+	Object.assign(form, emptyForm());
 	formRef.value?.clearValidate();
 }
 function openCreate() { resetForm(); dialogTitle.value = '新增雇员'; dialogVisible.value = true; }
 function openEdit(row: S0EmployeeRow) {
 	resetForm(); editingId.value = row.id; dialogTitle.value = `编辑雇员 ${row.employee}`;
-	Object.assign(form, { companyRefId: row.companyRefId, factoryRefId: row.factoryRefId, domainCode: row.domainCode ?? '', employee: row.employee, name: row.name ?? '', sex: row.sex ?? '', phone: row.phone ?? '', email: row.email ?? '', department: row.department ?? '', jobTitle: row.jobTitle ?? '', employmentStatus: row.employmentStatus ?? '', isActive: row.isActive });
+	Object.assign(form, {
+		companyRefId: row.companyRefId, factoryRefId: row.factoryRefId, domainCode: row.domainCode ?? '',
+		employee: row.employee, name: row.name ?? '', sex: row.sex ?? '', phone: row.phone ?? '', businessPhone: row.businessPhone ?? '',
+		email: row.email ?? '', birthDate: toDate(row.birthDate), department: row.department ?? '', dateEmployed: toDate(row.dateEmployed),
+		employmentStatus: row.employmentStatus ?? '', maritalStatus: row.maritalStatus ?? '', jobTitle: row.jobTitle ?? '',
+		dateTerminated: toDate(row.dateTerminated), workCtr: row.workCtr ?? '', carId: row.carId ?? '',
+		defaultWorkLocation: row.defaultWorkLocation ?? '', city: row.city ?? '', country: row.country ?? '', isActive: row.isActive,
+	});
 	dialogVisible.value = true;
 }
 async function submitForm() {
@@ -193,13 +263,47 @@ function onDelete(row: S0EmployeeRow) {
 	ElMessageBox.confirm(`确定删除雇员「${row.name || row.employee}」?`, '确认', { type: 'warning' })
 		.then(async () => { await s0EmployeesApi.delete(row.id); ElMessage.success('已删除'); await loadList(); }).catch(() => {});
 }
+
 async function loadOrgOptions() {
 	const [companies, factories] = await Promise.all([loadOrgList('201'), loadOrgList('501')]);
 	companyOptions.value = companies.filter((item) => item.id && item.id !== '0');
 	factoryOptions.value = factories.filter((item) => item.id && item.id !== '0');
 }
 
-onMounted(() => { void loadList(); void loadOrgOptions(); });
+// 加载真实下拉源:部门 / 生产线 / 工作中心 / 岗位
+async function loadDropdownSources() {
+	const bigPage = { page: 1, pageSize: 500 };
+	// 工作中心接口按工厂域过滤(Domain 必填),从部门主数据取当前租户实际工作域,避免硬编码。
+	let workingDomain = '';
+	try {
+		const dept = await s0DepartmentsApi.list(bigPage);
+		const deptRows = dept.list as DeptLike[];
+		deptOptions.value = deptRows
+			.filter((d) => !!d.department)
+			.map((d) => ({ value: d.department as string, label: d.descr ? `${d.department} ${d.descr}` : (d.department as string) }));
+		workingDomain = deptRows.map((d) => d.domainCode ?? '').find((dc) => !!dc) ?? '';
+	} catch { deptOptions.value = []; }
+	try {
+		const lines = await s0MfgProductionLinesApi.list(bigPage);
+		lineOptions.value = (lines.list as LineLike[])
+			.filter((l) => !!l.line)
+			.map((l) => ({ value: l.line as string, label: l.describe ? `${l.line} ${l.describe}` : (l.line as string) }));
+	} catch { lineOptions.value = []; }
+	if (workingDomain) {
+		try {
+			const wc = await s0MfgWorkCentersApi.list({ ...bigPage, domain: workingDomain });
+			workCtrOptions.value = (wc.list as WorkCtrLike[])
+				.filter((w) => !!w.workCtr)
+				.map((w) => ({ value: w.workCtr as string, label: w.descr ? `${w.workCtr} ${w.descr}` : (w.workCtr as string) }));
+		} catch { workCtrOptions.value = []; }
+	}
+	try {
+		const jobs: S0JobTitleOption[] = await s0EmployeesApi.jobTitleOptions();
+		jobTitleOptions.value = jobs.map((j) => ({ value: j.value, label: j.label || j.value }));
+	} catch { jobTitleOptions.value = []; }
+}
+
+onMounted(() => { void loadList(); void loadOrgOptions(); void loadDropdownSources(); });
 </script>
 
 <style scoped lang="scss">

+ 6 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.331</AssemblyVersion>
-    <FileVersion>1.0.331</FileVersion>
-    <Version>1.0.331</Version>
+    <AssemblyVersion>1.0.332</AssemblyVersion>
+    <FileVersion>1.0.332</FileVersion>
+    <Version>1.0.332</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -316,6 +316,9 @@
     <None Update="UpdateScripts\1.0.324.verify.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.332.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
     <None Update="UpdateScripts\1.0.328.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>

+ 37 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.332.sql

@@ -0,0 +1,37 @@
+-- 1.0.332.sql  S0:EmployeeMaster 补齐旧系统字段——新增 城市/国家/业务电话 三列(幂等)。
+--   背景:雇员编辑表单对齐旧系统雇员信息,AiDOP EmployeeMaster 缺 City/Country/BusinessPhone 三列,
+--         其余字段(出生/入职/离职日期、婚姻、工作中心、卡号、默认工作地点)实体已有、本 DDL 不涉及。
+--   仅新增可空列,不改既有列类型、不改 tenant_id 可空性、不插入/更新/删除任何业务数据。
+--   EmployeeMaster 为 CodeFirst 管理实体,本 DDL 与 EnableIncreTable 双保险(列存在即跳过,重启不冲突)。
+-- ============================================================================
+
+-- ========== City(城市) ==========
+SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'EmployeeMaster' AND COLUMN_NAME = 'City');
+SET @sql := IF(@has = 0, 'ALTER TABLE EmployeeMaster ADD COLUMN `City` varchar(100) NULL COMMENT ''城市''', 'DO 0');
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
+
+-- ========== Country(国家) ==========
+SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'EmployeeMaster' AND COLUMN_NAME = 'Country');
+SET @sql := IF(@has = 0, 'ALTER TABLE EmployeeMaster ADD COLUMN `Country` varchar(100) NULL COMMENT ''国家''', 'DO 0');
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
+
+-- ========== BusinessPhone(业务电话) ==========
+SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'EmployeeMaster' AND COLUMN_NAME = 'BusinessPhone');
+SET @sql := IF(@has = 0, 'ALTER TABLE EmployeeMaster ADD COLUMN `BusinessPhone` varchar(50) NULL COMMENT ''业务电话''', 'DO 0');
+PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
+
+-- ========== 验证 ==========
+SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE
+FROM information_schema.COLUMNS
+WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'EmployeeMaster'
+  AND COLUMN_NAME IN ('City', 'Country', 'BusinessPhone')
+ORDER BY COLUMN_NAME;
+-- 期望:City/Country/BusinessPhone 三列均存在且可空。
+
+-- ============================================================================
+-- 回滚 SQL(如需):
+--   ALTER TABLE EmployeeMaster DROP COLUMN `City`, DROP COLUMN `Country`, DROP COLUMN `BusinessPhone`;
+-- ============================================================================

+ 38 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0EmployeesController.cs

@@ -1,4 +1,5 @@
 using Admin.NET.Plugin.AiDOP.Dto.S0.Warehouse;
+using Admin.NET.Plugin.AiDOP.Entity;
 using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 using Admin.NET.Plugin.AiDOP.Infrastructure;
 
@@ -15,18 +16,49 @@ public class AdoS0EmployeesController : ControllerBase
 {
     private readonly SqlSugarRepository<AdoS0EmployeeMaster> _rep;
     private readonly SqlSugarRepository<AdoS0DepartmentMaster> _deptRep;
+    private readonly SqlSugarRepository<AdoGeneralizedCodeRecord> _codeRep;
     private readonly AdoS0ReferenceChecker _refChecker;
 
     public AdoS0EmployeesController(
         SqlSugarRepository<AdoS0EmployeeMaster> rep,
         SqlSugarRepository<AdoS0DepartmentMaster> deptRep,
+        SqlSugarRepository<AdoGeneralizedCodeRecord> codeRep,
         AdoS0ReferenceChecker refChecker)
     {
         _rep = rep;
         _deptRep = deptRep;
+        _codeRep = codeRep;
         _refChecker = refChecker;
     }
 
+    /// <summary>
+    /// 岗位下拉选项(legacy GeneralizedCodeMaster.FldName='JobTitle':Val=编码、Comments=中文名,按编码去重)。
+    /// 通用码表为跨租户参考数据,随雇员编辑弹窗提供岗位可选项。
+    /// </summary>
+    [HttpGet("job-titles")]
+    public async Task<IActionResult> GetJobTitleOptionsAsync()
+    {
+        if (!AdoS0TenantScope.TryResolveRequired(out _, out var tenantError)) return tenantError!;
+
+        var rows = await _codeRep.AsQueryable()
+            .Where(x => x.FldName == "JobTitle")
+            .Select(x => new { Val = x.Val, Comments = x.Comments })
+            .ToListAsync();
+
+        var options = rows
+            .Where(x => !string.IsNullOrWhiteSpace(x.Val))
+            .GroupBy(x => x.Val, StringComparer.OrdinalIgnoreCase)
+            .Select(g => new
+            {
+                value = g.Key,
+                label = g.Select(x => x.Comments).FirstOrDefault(c => !string.IsNullOrWhiteSpace(c)) ?? g.Key
+            })
+            .OrderBy(x => x.label, StringComparer.CurrentCulture)
+            .ToList();
+
+        return Ok(options);
+    }
+
     [HttpGet]
     public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0EmployeeQueryDto q)
     {
@@ -121,6 +153,9 @@ public class AdoS0EmployeesController : ControllerBase
         entity.WorkCtr = dto.WorkCtr;
         entity.CarId = dto.CarId;
         entity.DefaultWorkLocation = dto.DefaultWorkLocation;
+        entity.City = dto.City;
+        entity.Country = dto.Country;
+        entity.BusinessPhone = dto.BusinessPhone;
         entity.IsActive = dto.IsActive;
         entity.UpdateUser = dto.UpdateUser;
         entity.UpdateTime = DateTime.Now;
@@ -166,6 +201,9 @@ public class AdoS0EmployeesController : ControllerBase
         WorkCtr = dto.WorkCtr,
         CarId = dto.CarId,
         DefaultWorkLocation = dto.DefaultWorkLocation,
+        City = dto.City,
+        Country = dto.Country,
+        BusinessPhone = dto.BusinessPhone,
         IsActive = dto.IsActive,
         CreateUser = dto.CreateUser
     };

+ 6 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Warehouse/AdoS0WarehouseBasicDtos.cs

@@ -91,6 +91,12 @@ public class AdoS0EmployeeUpsertDto
     public string? CarId { get; set; }
     [MaxLength(100)]
     public string? DefaultWorkLocation { get; set; }
+    [MaxLength(100)]
+    public string? City { get; set; }
+    [MaxLength(100)]
+    public string? Country { get; set; }
+    [MaxLength(50)]
+    public string? BusinessPhone { get; set; }
     public bool IsActive { get; set; } = true;
     [MaxLength(100)]
     public string? CreateUser { get; set; }

+ 9 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0EmployeeMaster.cs

@@ -64,6 +64,15 @@ public class AdoS0EmployeeMaster : ITenantIdFilter
     [SugarColumn(ColumnName = "DefaultWorkLocation", ColumnDescription = "默认工作地点", Length = 100, IsNullable = true)]
     public string? DefaultWorkLocation { get; set; }
 
+    [SugarColumn(ColumnName = "City", ColumnDescription = "城市", Length = 100, IsNullable = true)]
+    public string? City { get; set; }
+
+    [SugarColumn(ColumnName = "Country", ColumnDescription = "国家", Length = 100, IsNullable = true)]
+    public string? Country { get; set; }
+
+    [SugarColumn(ColumnName = "BusinessPhone", ColumnDescription = "业务电话", Length = 50, IsNullable = true)]
+    public string? BusinessPhone { get; set; }
+
     [SugarColumn(ColumnName = "IsActive", ColumnDescription = "是否启用", ColumnDataType = "boolean")]
     public bool IsActive { get; set; } = true;