Bladeren bron

fix(s0): 客户表单校验与公司-工厂联动修复

- 公司/工厂下拉改为 clearable,工厂在未选公司或无可用工厂时禁用并给出提示
- companyRefId/factoryRefId 默认值从 0 改为 undefined,改用自定义 validator 避免 0 绕过 required
- 工厂列表按父级公司过滤,切换公司时清空工厂与 domainCode
- 保存失败错误信息改为解析 response.data.errors/message/title/detail,传输层异常走兜底文案
- s0CustomersApi.create/update 支持透传 AxiosRequestConfig
YY968XX 2 maanden geleden
bovenliggende
commit
5fda9b9dc5
2 gewijzigde bestanden met toevoegingen van 125 en 19 verwijderingen
  1. 5 4
      Web/src/views/aidop/s0/api/s0SalesApi.ts
  2. 120 15
      Web/src/views/aidop/s0/sales/CustomerList.vue

+ 5 - 4
Web/src/views/aidop/s0/api/s0SalesApi.ts

@@ -1,3 +1,4 @@
+import type { AxiosRequestConfig } from 'axios';
 import service from '/@/utils/request';
 import { SysDictDataApi, SysOrgApi } from '/@/api-services';
 
@@ -309,10 +310,10 @@ export const s0CustomersApi = {
 	list: (params: Record<string, unknown>) =>
 		service.get<Paged<S0CustMasterRow>>('/api/s0/sales/customers', { params }).then(unwrap),
 	get: (id: number) => service.get<S0CustMasterRow>(`/api/s0/sales/customers/${id}`).then(unwrap),
-	create: (body: S0CustMasterUpsert) =>
-		service.post<S0CustMasterRow>('/api/s0/sales/customers', body).then(unwrap),
-	update: (id: number, body: S0CustMasterUpsert) =>
-		service.put<S0CustMasterRow>(`/api/s0/sales/customers/${id}`, body).then(unwrap),
+	create: (body: S0CustMasterUpsert, config?: AxiosRequestConfig) =>
+		service.post<S0CustMasterRow>('/api/s0/sales/customers', body, config).then(unwrap),
+	update: (id: number, body: S0CustMasterUpsert, config?: AxiosRequestConfig) =>
+		service.put<S0CustMasterRow>(`/api/s0/sales/customers/${id}`, body, config).then(unwrap),
 	delete: (id: number) => service.delete(`/api/s0/sales/customers/${id}`).then(unwrap),
 	toggleEnabled: (id: number, body: CustMasterToggleActiveBody) =>
 		service.patch<S0CustMasterRow>(`/api/s0/sales/customers/${id}/toggle-enabled`, body).then(unwrap),

+ 120 - 15
Web/src/views/aidop/s0/sales/CustomerList.vue

@@ -92,17 +92,28 @@
 				<el-row :gutter="16">
 					<el-col :span="12">
 						<el-form-item label="公司" prop="companyRefId">
-							<el-select v-model="form.companyRefId" filterable style="width: 100%">
+							<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 label="工厂" prop="factoryRefId">
-							<el-select v-model="form.factoryRefId" filterable style="width: 100%" @change="syncDomainCodeFromFactory">
+							<el-select
+								v-model="form.factoryRefId"
+								clearable
+								filterable
+								placeholder="请选择工厂"
+								style="width: 100%"
+								:disabled="!form.companyRefId || selectedCompanyHasNoFactories"
+								@change="syncDomainCodeFromFactory"
+							>
 								<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>
+						<div v-if="selectedCompanyHasNoFactories" class="form-help form-help--warning">
+							该公司下暂无可用工厂,请先维护组织数据或机构权限。
+						</div>
 					</el-col>
 					<el-col :span="12">
 						<el-form-item label="客户编码" prop="cust">
@@ -239,9 +250,14 @@ const companyOptions = ref<OrgOption[]>([]);
 const factoryOptions = ref<OrgOption[]>([]);
 const currencyOptions = ref<OptionItem[]>([]);
 
-const form = reactive<S0CustMasterUpsert>({
-	companyRefId: 0,
-	factoryRefId: 0,
+type CustomerFormModel = Omit<S0CustMasterUpsert, 'companyRefId' | 'factoryRefId'> & {
+	companyRefId: number | undefined;
+	factoryRefId: number | undefined;
+};
+
+const form = reactive<CustomerFormModel>({
+	companyRefId: undefined,
+	factoryRefId: undefined,
 	domainCode: '',
 	cust: '',
 	sortName: '',
@@ -265,9 +281,19 @@ const form = reactive<S0CustMasterUpsert>({
 	updateUser: '',
 });
 
+function validateRequiredOrgField(message: string) {
+	return (_rule: unknown, value: number | undefined, callback: (error?: Error) => void) => {
+		if (typeof value !== 'number' || value <= 0) {
+			callback(new Error(message));
+			return;
+		}
+		callback();
+	};
+}
+
 const rules: FormRules = {
-	companyRefId: [{ required: true, message: '请选择公司', trigger: 'change' }],
-	factoryRefId: [{ required: true, message: '请选择工厂', trigger: 'change' }],
+	companyRefId: [{ validator: validateRequiredOrgField('请选择公司'), trigger: 'change' }],
+	factoryRefId: [{ validator: validateRequiredOrgField('请选择工厂'), trigger: 'change' }],
 	cust: [{ required: true, message: '请填写客户编码', trigger: 'blur' }],
 	sortName: [{ required: true, message: '请填写客户简称', trigger: 'blur' }],
 };
@@ -282,16 +308,73 @@ const formFactoryOptions = computed(() => {
 	return factoryOptions.value.filter((item) => item.pid === form.companyRefId);
 });
 
+const selectedCompanyHasNoFactories = computed(() => Boolean(form.companyRefId) && formFactoryOptions.value.length === 0);
+
 function syncDomainCodeFromFactory() {
 	const f = factoryOptions.value.find((item) => item.id === form.factoryRefId);
 	form.domainCode = f?.code ?? '';
 }
 
+function stringifyBusinessError(value: unknown): string | undefined {
+	if (!value) return undefined;
+	if (typeof value === 'string') {
+		const text = value.trim();
+		return text || undefined;
+	}
+	if (Array.isArray(value)) {
+		const parts = value.map((item) => stringifyBusinessError(item)).filter(Boolean);
+		return parts.length ? parts.join(';') : undefined;
+	}
+	if (typeof value === 'object') {
+		const parts = Object.values(value as Record<string, unknown>)
+			.map((item) => stringifyBusinessError(item))
+			.filter(Boolean);
+		return parts.length ? parts.join(';') : undefined;
+	}
+	return undefined;
+}
+
+function isRawTransportMessage(message: string | undefined): boolean {
+	if (!message) return true;
+	return /request failed with status code|network error|timeout|err_/i.test(message);
+}
+
+function getCustomerSaveErrorMessage(error: any): string {
+	const responseMessage =
+		stringifyBusinessError(error?.response?.data?.errors) ||
+		stringifyBusinessError(error?.response?.data?.message) ||
+		stringifyBusinessError(error?.response?.data?.title) ||
+		stringifyBusinessError(error?.response?.data?.detail);
+	if (responseMessage && !isRawTransportMessage(responseMessage)) {
+		return responseMessage;
+	}
+
+	const status = Number(error?.response?.status);
+	if (status === 400 || status === 422) {
+		return '客户保存失败,请检查公司、工厂和必填信息后重试。';
+	}
+
+	return '客户保存失败,请稍后重试。';
+}
+
+function buildPayload(): S0CustMasterUpsert | null {
+	if (!form.companyRefId || form.companyRefId <= 0 || !form.factoryRefId || form.factoryRefId <= 0) {
+		return null;
+	}
+
+	return {
+		...form,
+		companyRefId: form.companyRefId,
+		factoryRefId: form.factoryRefId,
+	};
+}
+
 watch(
 	() => form.companyRefId,
 	() => {
 		if (!formFactoryOptions.value.some((item) => item.id === form.factoryRefId)) {
-			form.factoryRefId = 0;
+			form.factoryRefId = undefined;
+			form.domainCode = '';
 		}
 	}
 );
@@ -311,8 +394,8 @@ async function loadOptions() {
 		loadOrgList('501'),
 		loadDictOptions('s0_currency'),
 	]);
-	companyOptions.value = companies;
-	factoryOptions.value = factories;
+	companyOptions.value = companies.filter((item) => item.id > 0);
+	factoryOptions.value = factories.filter((item) => item.id > 0);
 	currencyOptions.value = currencies;
 
 	if (!companies.length || !factories.length || !currencies.length) {
@@ -357,8 +440,8 @@ function resetQuery() {
 function resetForm() {
 	editingId.value = null;
 	Object.assign(form, {
-		companyRefId: 0,
-		factoryRefId: 0,
+		companyRefId: undefined,
+		factoryRefId: undefined,
 		domainCode: '',
 		cust: '',
 		sortName: '',
@@ -423,19 +506,31 @@ function openEdit(row: S0CustMasterRow) {
 }
 
 async function submitForm() {
+	if (selectedCompanyHasNoFactories.value) {
+		ElMessage.warning('所选公司下暂无可用工厂,请先维护组织数据或机构权限。');
+		return;
+	}
+
 	await formRef.value?.validate();
 	saving.value = true;
 	try {
-		const payload: S0CustMasterUpsert = { ...form };
+		const payload = buildPayload();
+		if (!payload) {
+			ElMessage.warning('请选择有效的公司和工厂后再保存。');
+			return;
+		}
 		if (editingId.value) {
-			await s0CustomersApi.update(editingId.value, payload);
+			await s0CustomersApi.update(editingId.value, payload, { headers: { 'X-Silent-Error': true } });
 			ElMessage.success('已保存');
 		} else {
-			await s0CustomersApi.create(payload);
+			await s0CustomersApi.create(payload, { headers: { 'X-Silent-Error': true } });
 			ElMessage.success('已创建');
 		}
 		dialogVisible.value = false;
 		await loadList();
+	} catch (error) {
+		console.error('[aidop/s0/sales/customer] save failed', error);
+		ElMessage.error(getCustomerSaveErrorMessage(error));
 	} finally {
 		saving.value = false;
 	}
@@ -483,4 +578,14 @@ onMounted(async () => {
 	display: flex;
 	justify-content: flex-end;
 }
+
+.form-help {
+	margin-top: -8px;
+	font-size: 12px;
+	line-height: 1.5;
+}
+
+.form-help--warning {
+	color: var(--el-color-warning);
+}
 </style>