소스 검색

style(s0): drop redundant customer confirm column and rework BOM material pickers

A · FUNC-S0-021 客户管理:下线「已确认」列与筛选项
   IsConfirm 是旧系统 20 张主数据表通用的遗留列(CustMaster/ItemMaster/SuppMaster/…),
   本页无写入口(表单无任何绑定控件)、控制器无「确认」动作端点、全仓无任何消费方读它;
   实测 1333 条客户中 1332 条为 1,唯一的 0 恰是本页新增留下的(新增恒写 false)。
   该列展示的「否」实为「这条是在本页新建的」,会误导使用者,故移除列 + 查询筛选。
   ⚠️ 字段仍保留在 form / rowToForm / buildPayload 中原样透传:buildPayload 是 `...form`
   全量提交,若一并删掉,编辑保存会把库里的 1 覆盖成 false。

B · FUNC-S0-001 标准 BOM:父项/子项物料下拉去「记忆」
   原实现父子两个下拉共用一个 materialOptions,且编辑时预置当前行的两条,
   点开只看到那两条,像是系统记住了历史选择,容易让客户误以为只有这些可选。
   改为:父项/子项各自独立候选列表;点开即按空关键字取默认前 10 条;
   输入关键字后按关键字远程检索(检索条数沿用 50,不缩小范围);
   每次点开都重新取默认候选,不沿用上次结果。
   当前已选项仍会并入自己那一份候选,否则 el-select 回显不出 label。
   另加请求序号防止「默认列表」与「关键字结果」乱序返回相互覆盖。

后端 / DTO / DB / 校验逻辑均未改动。

chore: bump version Web 2.4.366
YY968XX 1 주 전
부모
커밋
76227eb1cc
3개의 변경된 파일116개의 추가작업 그리고 46개의 파일을 삭제
  1. 1 1
      Web/package.json
  2. 111 29
      Web/src/views/aidop/s0/manufacturing/StandardBomManagement.vue
  3. 4 16
      Web/src/views/aidop/s0/sales/CustomerList.vue

+ 1 - 1
Web/package.json

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

+ 111 - 29
Web/src/views/aidop/s0/manufacturing/StandardBomManagement.vue

@@ -77,18 +77,25 @@
 							</el-select>
 						</el-form-item>
 					</el-col>
+					<!--
+						物料下拉:点开即给默认前 10 条,输入关键字后按关键字远程检索。
+						父项 / 子项各用各的候选列表,且每次点开都重新取默认候选——
+						不保留上一次的结果,避免让人误以为「只有这几条可选」。
+					-->
 					<el-col :span="24">
 						<el-form-item label="父项物料" prop="parentMaterialId">
 							<el-select
 								v-model="form.parentMaterialId"
 								filterable
 								remote
-								:remote-method="remoteSearchMaterials"
-								:loading="materialLoading"
-								placeholder="搜索物料"
+								:remote-method="loadParentMaterials"
+								:loading="parentMaterialLoading"
+								placeholder="点击选择,或输入物料编码/名称搜索"
 								style="width: 100%"
+								@visible-change="onParentVisibleChange"
+								@change="onParentMaterialChange"
 							>
-								<el-option v-for="m in materialOptions" :key="m.id" :label="`${m.code} ${m.name}`" :value="m.id" />
+								<el-option v-for="m in parentMaterialOptions" :key="m.id" :label="`${m.code} ${m.name}`" :value="m.id" />
 							</el-select>
 						</el-form-item>
 					</el-col>
@@ -98,12 +105,14 @@
 								v-model="form.componentMaterialId"
 								filterable
 								remote
-								:remote-method="remoteSearchMaterials"
-								:loading="materialLoading"
-								placeholder="搜索物料"
+								:remote-method="loadComponentMaterials"
+								:loading="componentMaterialLoading"
+								placeholder="点击选择,或输入物料编码/名称搜索"
 								style="width: 100%"
+								@visible-change="onComponentVisibleChange"
+								@change="onComponentMaterialChange"
 							>
-								<el-option v-for="m in materialOptions" :key="m.id" :label="`${m.code} ${m.name}`" :value="m.id" />
+								<el-option v-for="m in componentMaterialOptions" :key="m.id" :label="`${m.code} ${m.name}`" :value="m.id" />
 							</el-select>
 						</el-form-item>
 					</el-col>
@@ -195,8 +204,15 @@ const { companyOptions, factoryOptions, loadOrgs, factoriesForCompany } = useS0M
 const queryFactories = computed(() => factoriesForCompany(query.companyRefId));
 const formFactories = computed(() => factoriesForCompany(form.companyRefId));
 
-const materialOptions = ref<MaterialRow[]>([]);
-const materialLoading = ref(false);
+// 父项 / 子项各自独立的候选列表:共用一份会让「编辑时预置的两条」同时出现在两个下拉里,
+// 看上去像「系统记住了历史选择」,实际只是回显数据,容易误导使用者。
+const parentMaterialOptions = ref<MaterialRow[]>([]);
+const componentMaterialOptions = ref<MaterialRow[]>([]);
+const parentMaterialLoading = ref(false);
+const componentMaterialLoading = ref(false);
+// 当前已选物料(仅用于 label 回显,不作候选记忆)
+const parentSelected = ref<MaterialRow | null>(null);
+const componentSelected = ref<MaterialRow | null>(null);
 
 const query = reactive({
 	keyword: '',
@@ -280,27 +296,92 @@ function removeOpRow(index: number) {
 	form.ops.splice(index, 1);
 }
 
-async function remoteSearchMaterials(keyword: string) {
+/** 点开下拉(未输入关键字)时给出的候选条数 —— 只是「先看几条」,不是全量。 */
+const MATERIAL_DEFAULT_SIZE = 10;
+/** 输入关键字后的检索条数(沿用原值,不缩小搜索范围)。 */
+const MATERIAL_SEARCH_SIZE = 50;
+
+/**
+ * 物料候选取数。
+ * 物料主数据存在未 scope 化(CompanyRefId/FactoryRefId=0)的历史数据,按公司+工厂硬过滤会搜不到,
+ * 故一律按关键字全局搜索(不传 org)。BOM 表单自身的公司/工厂归属不受影响。
+ */
+async function fetchMaterials(keyword: string): Promise<MaterialRow[]> {
+	const data = await searchMaterialsForMfg({
+		keyword: keyword || undefined,
+		page: 1,
+		pageSize: keyword ? MATERIAL_SEARCH_SIZE : MATERIAL_DEFAULT_SIZE,
+	});
+	return (data.list as any[]).map((m) => ({ id: m.id, code: m.itemNum, name: m.descr }));
+}
+
+/**
+ * 已选项必须留在候选里,否则 el-select 回显不出 label(编辑态打开就会变成一个裸 id)。
+ * 这不是「记忆历史选择」——只保当前这一条,且不跨字段共享。
+ */
+function withCurrent(list: MaterialRow[], current: MaterialRow | null): MaterialRow[] {
+	if (!current) return list;
+	return list.some((m) => m.id === current.id) ? list : [current, ...list];
+}
+
+// 并发保护:点开下拉触发的「默认 10 条」与随后输入触发的「关键字检索」可能乱序返回,
+// 用序号确保只有最后一次请求的结果被采用,避免默认列表覆盖掉搜索结果。
+let parentSeq = 0;
+let componentSeq = 0;
+
+/** 父项物料候选:空关键字=默认前 N 条,有关键字=按关键字检索。 */
+async function loadParentMaterials(keyword: string) {
 	const kw = (keyword || '').trim();
-	if (!kw) return;
-	if (!form.companyRefId || !form.factoryRefId) return;
-	materialLoading.value = true;
+	const seq = ++parentSeq;
+	parentMaterialLoading.value = true;
 	try {
-		// 物料主数据存在未 scope 化(CompanyRefId/FactoryRefId=0)的历史数据,按公司+工厂硬过滤会搜不到;
-		// 改为按关键字全局搜索。BOM 表单本身的公司/工厂仍为必填(见 rules),此处仅解除物料检索侧的 org 过滤。
-		const data = await searchMaterialsForMfg({
-			keyword: kw,
-			page: 1,
-			pageSize: 50,
-		});
-		materialOptions.value = (data.list as any[]).map((m) => ({ id: m.id, code: m.itemNum, name: m.descr }));
+		const list = await fetchMaterials(kw);
+		if (seq !== parentSeq) return;
+		parentMaterialOptions.value = withCurrent(list, parentSelected.value);
+	} finally {
+		if (seq === parentSeq) parentMaterialLoading.value = false;
+	}
+}
+
+/** 子项物料候选:与父项各用各的列表,互不串味。 */
+async function loadComponentMaterials(keyword: string) {
+	const kw = (keyword || '').trim();
+	const seq = ++componentSeq;
+	componentMaterialLoading.value = true;
+	try {
+		const list = await fetchMaterials(kw);
+		if (seq !== componentSeq) return;
+		componentMaterialOptions.value = withCurrent(list, componentSelected.value);
 	} finally {
-		materialLoading.value = false;
+		if (seq === componentSeq) componentMaterialLoading.value = false;
 	}
 }
 
+/** 每次点开都按空关键字重新取默认候选,不沿用上一次的列表(即「不记忆」)。 */
+function onParentVisibleChange(visible: boolean) {
+	if (visible) void loadParentMaterials('');
+}
+function onComponentVisibleChange(visible: boolean) {
+	if (visible) void loadComponentMaterials('');
+}
+
+/** 记住当前选中项,仅用于 label 回显(见 withCurrent)。 */
+function onParentMaterialChange(id?: number) {
+	parentSelected.value = parentMaterialOptions.value.find((m) => m.id === id) ?? null;
+}
+function onComponentMaterialChange(id?: number) {
+	componentSelected.value = componentMaterialOptions.value.find((m) => m.id === id) ?? null;
+}
+
+function clearMaterialCandidates() {
+	parentMaterialOptions.value = [];
+	componentMaterialOptions.value = [];
+	parentSelected.value = null;
+	componentSelected.value = null;
+}
+
 function onFormOrgChange() {
-	materialOptions.value = [];
+	clearMaterialCandidates();
 }
 
 async function loadList() {
@@ -353,7 +434,7 @@ function resetForm() {
 		endEff: undefined,
 		isEnabled: true,
 	});
-	materialOptions.value = [];
+	clearMaterialCandidates();
 	productItemOptions.value = [];
 	formRef.value?.clearValidate();
 }
@@ -370,10 +451,11 @@ async function openEdit(row: S0MfgStandardBomLineRow) {
 	resetForm();
 	editingId.value = row.id;
 	dialogTitle.value = `编辑 ${row.parentItemNum} → ${row.componentItemNum}`;
-	materialOptions.value = [
-		{ id: row.parentMaterialId, code: row.parentItemNum, name: row.parentDescr },
-		{ id: row.componentMaterialId, code: row.componentItemNum, name: row.componentDescr },
-	];
+	// 只预置各自那一条当前值用于 label 回显;点开下拉时会按空关键字重新取默认候选。
+	parentSelected.value = { id: row.parentMaterialId, code: row.parentItemNum, name: row.parentDescr };
+	componentSelected.value = { id: row.componentMaterialId, code: row.componentItemNum, name: row.componentDescr };
+	parentMaterialOptions.value = [parentSelected.value];
+	componentMaterialOptions.value = [componentSelected.value];
 	const detail = (await s0MfgStandardBomLinesApi.get(row.id)) as S0MfgStandardBomLineDetail;
 	const m = detail.master;
 	Object.assign(form, {

+ 4 - 16
Web/src/views/aidop/s0/sales/CustomerList.vue

@@ -10,12 +10,6 @@
 					<el-option label="否" :value="false" />
 				</el-select>
 			</el-form-item>
-			<el-form-item label="已确认">
-				<el-select v-model="query.isConfirm" clearable placeholder="全部" style="width: 120px">
-					<el-option label="是" :value="true" />
-					<el-option label="否" :value="false" />
-				</el-select>
-			</el-form-item>
 			<el-form-item>
 				<el-button type="primary" @click="loadList">查询</el-button>
 				<el-button @click="resetQuery">重置</el-button>
@@ -38,13 +32,6 @@
 					</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="已确认" width="80" align="center">
-				<template #default="{ row }">
-					<el-tag :type="row.isConfirm ? 'success' : 'info'" size="small">
-						{{ row.isConfirm ? '是' : '否' }}
-					</el-tag>
-				</template>
-			</el-table-column>
 			<el-table-column prop="address" label="通讯地址" min-width="160" show-overflow-tooltip />
 			<el-table-column prop="shippingAddress" label="收货地址" min-width="160" show-overflow-tooltip />
 			<el-table-column prop="contact" label="联系人" width="100" show-overflow-tooltip />
@@ -234,7 +221,6 @@ const query = reactive({
 	companyRefId: undefined as string | undefined,
 	factoryRefId: undefined as string | undefined,
 	isActive: undefined as boolean | undefined,
-	isConfirm: undefined as boolean | undefined,
 	page: 1,
 	pageSize: 20,
 });
@@ -280,6 +266,10 @@ const form = reactive<CustomerFormModel>({
 	creditRating: '',
 	remarks: '',
 	isActive: true,
+	// IsConfirm 是旧系统 20 张主数据表通用的遗留列,本页无写入口、后端无任何消费方,
+	// 实测 1333 条客户里 1332 条为 1(唯一的 0 正是本页新增留下的),列表展示会误导使用者,
+	// 故已下线「已确认」列与筛选项。**但仍保留在 form / rowToForm / buildPayload 中原样透传** ——
+	// buildPayload 是 `...form` 全量提交,若从这里删掉,编辑保存会把库里的 1 写成 false。
 	isConfirm: false,
 	createUser: '',
 	updateUser: '',
@@ -419,7 +409,6 @@ async function loadList() {
 			companyRefId: query.companyRefId || undefined,
 			factoryRefId: query.factoryRefId || undefined,
 			isActive: query.isActive,
-			isConfirm: query.isConfirm,
 		});
 		rows.value = data.list;
 		total.value = data.total;
@@ -437,7 +426,6 @@ function resetQuery() {
 	query.companyRefId = undefined;
 	query.factoryRefId = undefined;
 	query.isActive = undefined;
-	query.isConfirm = undefined;
 	query.page = 1;
 	void loadList();
 }