Просмотр исходного кода

fix(s0): close manufacturing and warehouse form lookup gaps

STEP 2B 的 20 个页面从 DATA LIST PASS 推进到 FULL PAGE READY。
逐页按字段语义判定后最小改动,未因 "enrichment maps = 0" 或 "el-select = 0" 批量改写。

后端(仅 2 个 controller,均无 N+1、显式租户限定):
- ItemPacks / TaskAssignments:实体本就按设计声明了 [SugarColumn(IsIgnore = true)]
  展示字段(注释写明来自 ItemMaster),但 controller 从未填充,导致「物料名称」
  「型号」列恒为空。按仓库既有范式(MaterialProcessElements)补单次查询 + 字典映射。

前端:
- ContractReviewCycle(P0):loadOrgList('201'/'501') → loadCompanyFactoryOptions,
  复用 Structural Closure 的 applySingleOrgFallback;并修两处单-org 连带缺陷:
  applyDefaultFactory 取根组织 pid('0') 当公司、filteredFactoryOptions 缺 self-match,
  二者会把 factoryRefId 清空、令 15 条 breakdown 永不发请求。
  修复后 5 主 + 15 从可完整读取,分解工时按阶段汇总 8/12/8/10/2 与主表一致。
- 新增 useS0BusinessLookups:复用既有列表/options 接口的模块级缓存 lookup
  (item/line/personSkill/workCenter/location/employee/customer/barcodeType),
  每 key 每页仅取一次,无 N+1;选项 value 为业务编码,落库语义不变。
- 关系字段下拉:LineMaterial、MaterialProcessElement、PreprocessElement、
  ItemPack、TaskAssignment、BarcodeRule。
- PersonSkillAssignment:接口本就返回 employeeName / skillDescription,仅补展示列。

按语义判定为 NO CHANGE = PASS:WorkOrderControl(全为 switch/number/历史文本)、
ProductionElementParam(要素编码/参数编码为业务码非 FK)。

chore: bump version server 1.0.409 / Web 2.4.331
YY968XX 21 часов назад
Родитель
Сommit
7dc696b7e5

+ 1 - 1
Web/package.json

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

+ 94 - 0
Web/src/views/aidop/s0/composables/useS0BusinessLookups.ts

@@ -0,0 +1,94 @@
+/**
+ * S0 制造/仓储域 —— 业务关系字段的统一 lookup 取数。
+ *
+ * 设计约束(与 Quality 域 qualityLookups 同口径):
+ * 1. 只复用既有列表/options 接口,不新增后端端点;这些接口本身已走
+ *    AdoS0TenantScope 租户作用域,前端不传 tenantId、不参与 scope 判定。
+ * 2. 每个 lookup 全页面只取一次(模块级 Promise 缓存),下拉与列表解析共用同一份,
+ *    **不产生 per-row 请求**。
+ * 3. 选项 value 一律是**业务编码**(ItemNum / Line / PersonSkill.Code),
+ *    不是内部自增主键或雪花 id —— 保存回后端的仍是原业务编码,落库语义不变。
+ */
+import { s0CustomersApi, s0MaterialsApi } from '../api/s0SalesApi';
+import { s0EmployeesApi, s0LabelTypesApi, s0LocationsApi } from '../api/s0WarehouseApi';
+import { s0MfgPersonSkillsApi, s0MfgProductionLinesApi, s0MfgWorkCentersApi } from '../api/s0ManufacturingApi';
+
+export interface S0LookupOption {
+	value: string;
+	label: string;
+}
+
+export type S0LookupKey = 'item' | 'line' | 'personSkill' | 'workCenter' | 'location' | 'employee' | 'customer' | 'barcodeType';
+
+/** 下拉一次性取数上限:S0 主数据为配置级数据量,单页取全量即可,避免分页拼装。 */
+const PAGE_SIZE = 500;
+
+function norm(value: unknown, name: unknown): S0LookupOption | null {
+	const v = value == null ? '' : String(value).trim();
+	if (!v) return null;
+	const n = name == null ? '' : String(name).trim();
+	return { value: v, label: n ? `${v} / ${n}` : v };
+}
+
+function dedupe(list: Array<S0LookupOption | null>): S0LookupOption[] {
+	const seen = new Set<string>();
+	const out: S0LookupOption[] = [];
+	for (const o of list) {
+		if (!o || seen.has(o.value)) continue;
+		seen.add(o.value);
+		out.push(o);
+	}
+	return out;
+}
+
+const loaders: Record<S0LookupKey, () => Promise<S0LookupOption[]>> = {
+	item: async () => {
+		const d = await s0MaterialsApi.list({ page: 1, pageSize: PAGE_SIZE });
+		return dedupe((d.list ?? []).map((x: any) => norm(x.itemNum, x.descr)));
+	},
+	line: async () => {
+		const d = await s0MfgProductionLinesApi.list({ page: 1, pageSize: PAGE_SIZE });
+		return dedupe((d.list ?? []).map((x: any) => norm(x.line, x.describe)));
+	},
+	personSkill: async () => {
+		const d = await s0MfgPersonSkillsApi.options();
+		return dedupe((d ?? []).map((x: any) => norm(x.code ?? x.value, x.name)));
+	},
+	workCenter: async () => {
+		const d = await s0MfgWorkCentersApi.list({ page: 1, pageSize: PAGE_SIZE });
+		return dedupe((d.list ?? []).map((x: any) => norm(x.workCtr ?? x.code, x.descr ?? x.name)));
+	},
+	location: async () => {
+		const d = await s0LocationsApi.list({ page: 1, pageSize: PAGE_SIZE });
+		return dedupe((d.list ?? []).map((x: any) => norm(x.location, x.descr)));
+	},
+	employee: async () => {
+		const d = await s0EmployeesApi.list({ page: 1, pageSize: PAGE_SIZE });
+		return dedupe((d.list ?? []).map((x: any) => norm(x.employee, x.name)));
+	},
+	customer: async () => {
+		const d = await s0CustomersApi.list({ page: 1, pageSize: PAGE_SIZE });
+		return dedupe((d.list ?? []).map((x: any) => norm(x.cust, x.sortName ?? x.custFullName)));
+	},
+	barcodeType: async () => {
+		const d = await s0LabelTypesApi.list({ page: 1, pageSize: PAGE_SIZE });
+		return dedupe((d.list ?? []).map((x: any) => norm(x.barType, x.class)));
+	},
+};
+
+const cache = new Map<S0LookupKey, Promise<S0LookupOption[]>>();
+
+/** 取 lookup 选项;同一 key 在页面生命周期内只请求一次。 */
+export function loadS0Lookup(key: S0LookupKey): Promise<S0LookupOption[]> {
+	let hit = cache.get(key);
+	if (!hit) {
+		hit = loaders[key]().catch(() => [] as S0LookupOption[]);
+		cache.set(key, hit);
+	}
+	return hit;
+}
+
+/** 新增/编辑成功后调用,避免下拉停留在旧数据上。 */
+export function invalidateS0Lookups(): void {
+	cache.clear();
+}

+ 23 - 4
Web/src/views/aidop/s0/manufacturing/LineMaterialList.vue

@@ -61,7 +61,9 @@
 					</el-col>
 					<el-col :span="12">
 						<el-form-item label="物料编码" prop="part">
-							<el-input v-model="form.part" />
+							<el-select v-model="form.part" filterable clearable allow-create default-first-option placeholder="请选择物料编码" style="width: 100%">
+								<el-option v-for="o in lookup.item" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select>
 						</el-form-item>
 					</el-col>
 					<el-col :span="12">
@@ -71,12 +73,16 @@
 					</el-col>
 					<el-col :span="12">
 						<el-form-item label="工作中心编码">
-							<el-input v-model="form.site" />
+							<el-select v-model="form.site" filterable clearable allow-create default-first-option placeholder="请选择工作中心编码" style="width: 100%">
+								<el-option v-for="o in lookup.workCenter" :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.line" />
+							<el-select v-model="form.line" filterable clearable allow-create default-first-option placeholder="请选择生产线/设备编码" style="width: 100%">
+								<el-option v-for="o in lookup.line" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select>
 						</el-form-item>
 					</el-col>
 					<el-col :span="12">
@@ -91,7 +97,9 @@
 					</el-col>
 					<el-col :span="12">
 						<el-form-item label="技能编码">
-							<el-input v-model="form.skillNo" />
+							<el-select v-model="form.skillNo" filterable clearable allow-create default-first-option placeholder="请选择技能编码" style="width: 100%">
+								<el-option v-for="o in lookup.personSkill" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select>
 						</el-form-item>
 					</el-col>
 					<el-col :span="12">
@@ -130,6 +138,17 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgLineMaterial">
+import { reactive as __reactive_lk } from 'vue';
+import { loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
+// 关系字段下拉:复用既有列表/options 接口,模块级缓存,每个 key 每页仅取一次(无 N+1)。
+// 选项 value 为业务编码(ItemNum / Line / 技能编码 / 工作中心编码),保存语义不变;
+// allow-create 保留手工录入历史值的能力,不阻断既有数据。
+const lookup = __reactive_lk<Record<string, S0LookupOption[]>>({ item: [], line: [], personSkill: [], workCenter: [] });
+void loadS0Lookup('item').then((r) => (lookup.item = r));
+void loadS0Lookup('line').then((r) => (lookup.line = r));
+void loadS0Lookup('personSkill').then((r) => (lookup.personSkill = r));
+void loadS0Lookup('workCenter').then((r) => (lookup.workCenter = r));
+
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 15 - 2
Web/src/views/aidop/s0/manufacturing/MaterialProcessElementList.vue

@@ -75,7 +75,9 @@
 					</el-col>
 					<el-col :span="12">
 						<el-form-item label="物料编码" prop="itemNum">
-							<el-input v-model="form.itemNum" />
+							<el-select v-model="form.itemNum" filterable clearable allow-create default-first-option placeholder="请选择物料编码" style="width: 100%">
+								<el-option v-for="o in lookup.item" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select>
 						</el-form-item>
 					</el-col>
 					<el-col :span="12">
@@ -85,7 +87,9 @@
 					</el-col>
 					<el-col :span="12">
 						<el-form-item label="生产线">
-							<el-input v-model="form.line" />
+							<el-select v-model="form.line" filterable clearable allow-create default-first-option placeholder="请选择生产线" style="width: 100%">
+								<el-option v-for="o in lookup.line" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select>
 						</el-form-item>
 					</el-col>
 					<el-col :span="12">
@@ -119,6 +123,15 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgMaterialProcessElement">
+import { reactive as __reactive_lk } from 'vue';
+import { loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
+// 关系字段下拉:复用既有列表/options 接口,模块级缓存,每个 key 每页仅取一次(无 N+1)。
+// 选项 value 为业务编码(ItemNum / Line / 技能编码 / 工作中心编码),保存语义不变;
+// allow-create 保留手工录入历史值的能力,不阻断既有数据。
+const lookup = __reactive_lk<Record<string, S0LookupOption[]>>({ item: [], line: [] });
+void loadS0Lookup('item').then((r) => (lookup.item = r));
+void loadS0Lookup('line').then((r) => (lookup.line = r));
+
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 4 - 0
Web/src/views/aidop/s0/manufacturing/PersonSkillAssignmentList.vue

@@ -31,12 +31,16 @@
 
 		<el-table :data="rows" v-loading="loading" border stripe max-height="calc(100vh - 260px)">
 			<el-table-column prop="employee" label="人员编号" width="140" show-overflow-tooltip />
+			<!-- 姓名/技能名称:接口本就返回 employeeName / skillDescription,此前未展示,
+			     用户只能看到编码。补列即可,无需后端 enrichment。 -->
+			<el-table-column prop="employeeName" label="姓名" width="120" show-overflow-tooltip />
 			<el-table-column prop="site" label="工作组" width="120" show-overflow-tooltip />
 			<el-table-column label="技能编码" width="180" show-overflow-tooltip>
 				<template #default="{ row }">
 					{{ skillNoDisplay(row.skillNo) }}
 				</template>
 			</el-table-column>
+			<el-table-column prop="skillDescription" label="技能名称" width="140" show-overflow-tooltip />
 			<el-table-column prop="skillLevel" label="技能等级" width="110" show-overflow-tooltip />
 			<el-table-column prop="efficiencyCoefficient" label="生产效率" width="110" align="right" />
 			<!-- 工厂域编码为系统后台归属字段:表单控件已隐藏,列表列一并隐藏以保持口径一致(值仍由后端维护并原样保值)。 -->

+ 11 - 1
Web/src/views/aidop/s0/manufacturing/PreprocessElementList.vue

@@ -69,7 +69,9 @@
 					</el-col>
 					<el-col :span="12">
 						<el-form-item label="物料编码" prop="itemNum">
-							<el-input v-model="form.itemNum" />
+							<el-select v-model="form.itemNum" filterable clearable allow-create default-first-option placeholder="请选择物料编码" style="width: 100%">
+								<el-option v-for="o in lookup.item" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select>
 						</el-form-item>
 					</el-col>
 					<el-col :span="12">
@@ -99,6 +101,14 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgPreprocessElement">
+import { reactive as __reactive_lk } from 'vue';
+import { loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
+// 关系字段下拉:复用既有列表/options 接口,模块级缓存,每个 key 每页仅取一次(无 N+1)。
+// 选项 value 为业务编码(ItemNum / Line / 技能编码 / 工作中心编码),保存语义不变;
+// allow-create 保留手工录入历史值的能力,不阻断既有数据。
+const lookup = __reactive_lk<Record<string, S0LookupOption[]>>({ item: [] });
+void loadS0Lookup('item').then((r) => (lookup.item = r));
+
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 20 - 6
Web/src/views/aidop/s0/sales/ContractReviewCycleList.vue

@@ -206,6 +206,7 @@ import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type TableInstance } from 'element-plus';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
+import { filterFactoriesForCompany, loadCompanyFactoryOptions } from '../composables/useS0MfgOrgScope';
 import {
 	loadOrgList,
 	s0ContractReviewCyclesApi,
@@ -250,6 +251,7 @@ const formRef = ref<FormInstance>();
 
 const companyOptions = ref<OrgOption[]>([]);
 const factoryOptions = ref<OrgOption[]>([]);
+const singleOrgMode = ref(false);
 
 // ── 同步开关(工厂级) ─────────────────────────────────────────────────
 const syncEnabled = ref(false);
@@ -275,10 +277,12 @@ const breakdownTitle = computed(() =>
 	currentStageName.value ? `${currentStageName.value} — 部门/组 PI 配置` : '部门/组 PI 配置'
 );
 
-const filteredFactoryOptions = computed(() => {
-	if (!query.companyRefId) return factoryOptions.value;
-	return factoryOptions.value.filter((item) => item.pid === query.companyRefId);
-});
+// 单-org 兼容:根组织 pid 为 '0',其"公司"即自身,按 pid 等值过滤会得到空集,
+// 进而被下方 watch 清空 factoryRefId、令 breakdown 永远不发请求。
+// 统一复用 S0 Structural Closure 的 filterFactoriesForCompany(singleOrgMode 下允许 self-match)。
+const filteredFactoryOptions = computed(() =>
+	filterFactoriesForCompany(factoryOptions.value, query.companyRefId, singleOrgMode.value)
+);
 
 function emptyForm(): S0ContractReviewCycleUpsert {
 	return {
@@ -343,10 +347,16 @@ watch(
 	}
 );
 
+// 组织候选沿用 S0 Structural Closure 已确立的 single-org 兼容契约:
+// typed(201/501) 有值 → 用 typed;typed 全空且租户恰好 1 个组织 → 安全回落;
+// 多组织却取不到 typed → 暴露歧义不臆造(见 useS0MfgOrgScope.applySingleOrgFallback)。
+// 修复:此前直接用裸 loadOrgList('201'/'501'),UAT 单组织租户两者均为空 →
+// factoryRefId 永远为空 → loadBreakdown() 早退 → 15 条分解从不发请求。
 async function loadOptions() {
-	const [companies, factories] = await Promise.all([loadOrgList('201'), loadOrgList('501')]);
+	const { companies, factories, singleOrgMode: single } = await loadCompanyFactoryOptions();
 	companyOptions.value = companies;
 	factoryOptions.value = factories;
+	singleOrgMode.value = single;
 }
 
 // 默认选中第一个工厂(私有云单工厂口径),公司随之带出,杜绝多工厂重复列表
@@ -354,7 +364,11 @@ function applyDefaultFactory() {
 	if (query.factoryRefId) return;
 	const first = factoryOptions.value[0];
 	if (!first) return;
-	query.companyRefId = first.pid ?? query.companyRefId;
+	// 单-org 兼容:根组织的 pid 为 '0'(非法公司 id),直接取 pid 会让后端按
+	// CompanyRefId == 0 过滤而查不到任何数据。与 filterFactoriesForCompany 的
+	// self-match 口径一致——pid 缺失或为 '0' 时,公司即该组织自身。
+	const pid = first.pid && first.pid !== '0' ? first.pid : undefined;
+	query.companyRefId = pid ?? first.id;
 	query.factoryRefId = first.id;
 }
 

+ 14 - 2
Web/src/views/aidop/s0/warehouse/BarcodeRuleList.vue

@@ -51,8 +51,12 @@
 							</el-select>
 						</el-form-item>
 					</el-col>
-					<el-col :span="12"><el-form-item label="客户编码" prop="customer"><el-input v-model="form.customer" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="条码类型" prop="type"><el-input v-model="form.type" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="客户编码" prop="customer"><el-select v-model="form.customer" filterable clearable allow-create default-first-option placeholder="请选择客户编码" style="width: 100%">
+								<el-option v-for="o in lookup.customer" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="条码类型" prop="type"><el-select v-model="form.type" filterable clearable allow-create default-first-option placeholder="请选择条码类型" style="width: 100%">
+								<el-option v-for="o in lookup.barcodeType" :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.waterRules" /></el-form-item></el-col>
 					<el-col :span="12"><el-form-item label="流水长度"><el-input-number v-model="form.waterLen" :min="0" style="width:100%" /></el-form-item></el-col>
 					<el-col :span="12"><el-form-item label="分隔符"><el-input v-model="form.separator" /></el-form-item></el-col>
@@ -77,6 +81,14 @@
 </template>
 
 <script setup lang="ts" name="aidopS0WhBarcodeRule">
+import { reactive as __reactive_lk } from 'vue';
+import { loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
+// 关系字段下拉:复用既有列表/options 接口,模块级缓存,每 key 每页仅取一次(无 N+1)。
+// 选项 value 为业务编码,保存语义不变;allow-create 保留手工录入历史值能力。
+const lookup = __reactive_lk<Record<string, S0LookupOption[]>>({ barcodeType: [], customer: [] });
+void loadS0Lookup('barcodeType').then((r) => (lookup.barcodeType = r));
+void loadS0Lookup('customer').then((r) => (lookup.customer = r));
+
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 7 - 1
Web/src/views/aidop/s0/warehouse/ItemPackList.vue

@@ -63,7 +63,7 @@
 							</el-select>
 						</el-form-item>
 					</el-col>
-					<el-col :span="12"><el-form-item label="物料编码" prop="itemNum"><el-input v-model="form.itemNum" :disabled="!!editingId" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="物料编码" prop="itemNum"><el-select v-model="form.itemNum" :disabled="!!editingId" filterable clearable allow-create default-first-option placeholder="请选择物料编码" style="width: 100%"><el-option v-for="o in lookup.item" :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.packingType" /></el-form-item></el-col>
 					<el-col :span="12"><el-form-item label="包装数量"><el-input-number v-model="form.packingQty" :precision="4" :controls="false" style="width:100%" /></el-form-item></el-col>
 					<el-col :span="12"><el-form-item label="小包装数量"><el-input-number v-model="form.smallPackingQty" :precision="4" :controls="false" style="width:100%" /></el-form-item></el-col>
@@ -88,6 +88,12 @@
 </template>
 
 <script setup lang="ts" name="aidopS0WhItemPack">
+import { reactive as __reactive_lk } from 'vue';
+import { loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
+// 物料下拉:复用既有 materials 列表接口,模块级缓存,无 N+1;value 为 ItemNum 业务编码。
+const lookup = __reactive_lk<Record<string, S0LookupOption[]>>({ item: [] });
+void loadS0Lookup('item').then((r) => (lookup.item = r));
+
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 24 - 5
Web/src/views/aidop/s0/warehouse/TaskAssignmentList.vue

@@ -62,15 +62,25 @@
 						</el-form-item>
 					</el-col>
 					<el-col :span="12"><el-form-item label="提出日期"><el-date-picker v-model="form.tcrq" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" style="width:100%" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="申请人"><el-input v-model="form.sqr" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="申请人"><el-select v-model="form.sqr" filterable clearable allow-create default-first-option placeholder="请选择申请人" style="width: 100%">
+								<el-option v-for="o in lookup.employee" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select></el-form-item></el-col>
 					<el-col :span="12"><el-form-item label="任务类型" prop="rwlx"><el-input v-model="form.rwlx" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="物料编码" prop="wlbm"><el-input v-model="form.wlbm" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="物料编码" prop="wlbm"><el-select v-model="form.wlbm" filterable clearable allow-create default-first-option placeholder="请选择物料编码" style="width: 100%">
+								<el-option v-for="o in lookup.item" :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-number v-model="form.sl" :precision="4" style="width:100%" /></el-form-item></el-col>
 					<el-col :span="12"><el-form-item label="批次"><el-input v-model="form.rqpc" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="原始库位"><el-input v-model="form.yskw" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="目的库位"><el-input v-model="form.mdkw" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="原始库位"><el-select v-model="form.yskw" filterable clearable allow-create default-first-option placeholder="请选择原始库位" style="width: 100%">
+								<el-option v-for="o in lookup.location" :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-select v-model="form.mdkw" filterable clearable allow-create default-first-option placeholder="请选择目的库位" style="width: 100%">
+								<el-option v-for="o in lookup.location" :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-date-picker v-model="form.xqsj" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" style="width:100%" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="处理人"><el-input v-model="form.clr" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="处理人"><el-select v-model="form.clr" filterable clearable allow-create default-first-option placeholder="请选择处理人" style="width: 100%">
+								<el-option v-for="o in lookup.employee" :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.zt" /></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>
 					<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.bz" type="textarea" rows="2" /></el-form-item></el-col>
@@ -85,6 +95,15 @@
 </template>
 
 <script setup lang="ts" name="aidopS0WhTaskAssignment">
+import { reactive as __reactive_lk } from 'vue';
+import { loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
+// 关系字段下拉:复用既有列表/options 接口,模块级缓存,每 key 每页仅取一次(无 N+1)。
+// 选项 value 为业务编码,保存语义不变;allow-create 保留手工录入历史值能力。
+const lookup = __reactive_lk<Record<string, S0LookupOption[]>>({ employee: [], item: [], location: [] });
+void loadS0Lookup('employee').then((r) => (lookup.employee = r));
+void loadS0Lookup('item').then((r) => (lookup.item = r));
+void loadS0Lookup('location').then((r) => (lookup.location = r));
+
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 3 - 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.408</AssemblyVersion>
-    <FileVersion>1.0.408</FileVersion>
-    <Version>1.0.408</Version>
+    <AssemblyVersion>1.0.409</AssemblyVersion>
+    <FileVersion>1.0.409</FileVersion>
+    <Version>1.0.409</Version>
   </PropertyGroup>
 
   <ItemGroup>

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

@@ -1,4 +1,5 @@
 using Admin.NET.Plugin.AiDOP.Dto.S0.Warehouse;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
 using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 using Admin.NET.Plugin.AiDOP.Infrastructure;
 
@@ -43,6 +44,13 @@ public class AdoS0ItemPacksController : ControllerBase
             .Take(q.PageSize)
             .ToListAsync();
 
+        // 补显 ItemDescr / ItemDescr1:实体已按设计声明这两个 [SugarColumn(IsIgnore = true)] 展示字段
+        //(注释即写明「来自 ItemMaster.Descr / Descr1」),但此前 controller 从未填充,
+        // 导致页面「物料名称」「型号」两列恒为空。
+        // 沿用本仓既有范式(AdoS0MfgMaterialProcessElementsController):单次 ItemMaster 查询 + 字典映射,
+        // 不做逐行查询,杜绝 N+1;ItemMaster 显式限定当前租户,不跨租户取名。
+        await FillItemDisplayAsync(list, tenantId);
+
         return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
     }
 
@@ -134,4 +142,34 @@ public class AdoS0ItemPacksController : ControllerBase
         await _rep.DeleteAsync(item);
         return Ok(new { message = "删除成功" });
     }
+
+    /// <summary>
+    /// 批量补显物料名称/型号:一次 ItemMaster 查询 + 字典映射(无 N+1),显式限定当前租户。
+    /// ItemDescr / ItemDescr1 为 [SugarColumn(IsIgnore = true)] 展示字段,不落库。
+    /// </summary>
+    private async Task FillItemDisplayAsync(List<AdoS0ItemPackMaster> rows, long tenantId)
+    {
+        var itemNums = rows.Select(x => x.ItemNum)
+            .Where(x => !string.IsNullOrWhiteSpace(x))
+            .Distinct()
+            .ToList();
+        if (itemNums.Count == 0) return;
+
+        var items = await _rep.Context.Queryable<AdoS0ItemMaster>()
+            .Where(m => m.TenantId == tenantId && itemNums.Contains(m.ItemNum))
+            .OrderByDescending(m => m.IsActive)
+            .OrderBy(m => m.Id)
+            .Select(m => new { m.ItemNum, m.Descr, m.Descr1 })
+            .ToListAsync();
+        var map = items.GroupBy(x => x.ItemNum).ToDictionary(g => g.Key, g => g.First());
+
+        foreach (var r in rows)
+        {
+            if (r.ItemNum != null && map.TryGetValue(r.ItemNum, out var m))
+            {
+                r.ItemDescr = m.Descr;
+                r.ItemDescr1 = m.Descr1;
+            }
+        }
+    }
 }

+ 28 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0TaskAssignmentsController.cs

@@ -1,4 +1,5 @@
 using Admin.NET.Plugin.AiDOP.Dto.S0.Warehouse;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
 using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 using Admin.NET.Plugin.AiDOP.Infrastructure;
 
@@ -44,6 +45,11 @@ public class AdoS0TaskAssignmentsController : ControllerBase
             .Take(q.PageSize)
             .ToListAsync();
 
+        // 补显 ItemDescr:实体已按设计声明该 [SugarColumn(IsIgnore = true)] 展示字段
+        //(注释即写明「来自 ItemMaster」),但此前 controller 从未填充,页面「物料名称」列恒为空。
+        // 单次 ItemMaster 查询 + 字典映射,无 N+1;显式限定当前租户。
+        await FillItemDisplayAsync(list, tenantId);
+
         return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
     }
 
@@ -160,4 +166,26 @@ public class AdoS0TaskAssignmentsController : ControllerBase
             .WhereIF(excludeId.HasValue, x => x.Id != excludeId!.Value)
             .AnyAsync();
     }
+
+    /// <summary>批量补显物料名称:一次 ItemMaster 查询 + 字典映射(无 N+1),显式限定当前租户。</summary>
+    private async Task FillItemDisplayAsync(List<AdoS0TaskAssignment> rows, long tenantId)
+    {
+        var itemNums = rows.Select(x => x.Wlbm)
+            .Where(x => !string.IsNullOrWhiteSpace(x))
+            .Select(x => x!)
+            .Distinct()
+            .ToList();
+        if (itemNums.Count == 0) return;
+
+        var items = await _rep.Context.Queryable<AdoS0ItemMaster>()
+            .Where(m => m.TenantId == tenantId && itemNums.Contains(m.ItemNum))
+            .OrderByDescending(m => m.IsActive)
+            .OrderBy(m => m.Id)
+            .Select(m => new { m.ItemNum, m.Descr })
+            .ToListAsync();
+        var map = items.GroupBy(x => x.ItemNum).ToDictionary(g => g.Key, g => g.First().Descr);
+
+        foreach (var r in rows)
+            if (r.Wlbm != null && map.TryGetValue(r.Wlbm, out var d)) r.ItemDescr = d;
+    }
 }