소스 검색

fix(s0): improve supplier form usability

YY968XX 1 개월 전
부모
커밋
48e8cc6392

+ 1 - 1
Web/package.json

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

+ 26 - 7
Web/src/views/aidop/s0/api/s0WarehouseApi.ts

@@ -185,14 +185,33 @@ export interface S0LocationUpsert {
 	updateUser?: string;
 }
 
+export interface S0LocationOptionRow {
+	value: string;
+	label: string;
+	code: string;
+	name?: string | null;
+	domainCode?: string | null;
+	isActive: boolean;
+}
+
+export interface S0LocationOptionsQuery {
+	companyRefId?: string | number;
+	factoryRefId?: string | number;
+	domainCode?: string;
+	keyword?: string;
+	isActive?: boolean;
+	limit?: number;
+}
+
+const locationsBase = '/api/s0/warehouse/locations';
 export const s0LocationsApi = {
-	list: (params: Record<string, unknown>) =>
-		service.get<Paged<S0LocationRow>>('/api/s0/warehouse/locations', { params }).then(unwrap),
-	get: (id: number) => service.get<S0LocationRow>(`/api/s0/warehouse/locations/${id}`).then(unwrap),
-	create: (body: S0LocationUpsert) => service.post<S0LocationRow>('/api/s0/warehouse/locations', body).then(unwrap),
-	update: (id: number, body: S0LocationUpsert) =>
-		service.put<S0LocationRow>(`/api/s0/warehouse/locations/${id}`, body).then(unwrap),
-	delete: (id: number) => service.delete(`/api/s0/warehouse/locations/${id}`).then(unwrap),
+	list: (params: Record<string, unknown>) => service.get<Paged<S0LocationRow>>(locationsBase, { params }).then(unwrap),
+	get: (id: number) => service.get<S0LocationRow>(`${locationsBase}/${id}`).then(unwrap),
+	create: (body: S0LocationUpsert) => service.post<S0LocationRow>(locationsBase, body).then(unwrap),
+	update: (id: number, body: S0LocationUpsert) => service.put<S0LocationRow>(`${locationsBase}/${id}`, body).then(unwrap),
+	delete: (id: number) => service.delete(`${locationsBase}/${id}`).then(unwrap),
+	options: (params?: S0LocationOptionsQuery) =>
+		service.get<S0LocationOptionRow[]>(`${locationsBase}/options`, { params }).then(unwrap),
 };
 
 // ========== 货架 ==========

+ 107 - 22
Web/src/views/aidop/s0/supply/SupplierList.vue

@@ -154,7 +154,17 @@
 					</el-col>
 					<el-col :span="12">
 						<el-form-item label="供应商库位">
-							<el-input v-model="form.purContact" placeholder="库位编码" />
+							<el-select
+								v-model="form.purContact"
+								filterable
+								clearable
+								allow-create
+								default-first-option
+								placeholder="请选择供应商库位"
+								style="width: 100%"
+							>
+								<el-option v-for="opt in locationOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+							</el-select>
 						</el-form-item>
 					</el-col>
 					<el-col :span="12">
@@ -204,30 +214,43 @@
 							<el-switch v-model="form.taxIn" />
 						</el-form-item>
 					</el-col>
-					<el-col :span="12">
-						<el-form-item label="对账周期1开始">
-							<el-input-number v-model="form.apStartDay1" :min="0" :max="999" controls-position="right" style="width: 100%" />
-						</el-form-item>
-					</el-col>
-					<el-col :span="12">
-						<el-form-item label="对账周期1结束">
-							<el-input-number v-model="form.apEndDay1" :min="0" :max="999" controls-position="right" style="width: 100%" />
-						</el-form-item>
-					</el-col>
-					<el-col :span="12">
-						<el-form-item label="对账周期2开始">
-							<el-input-number v-model="form.apStartDay2" :min="0" :max="999" controls-position="right" style="width: 100%" />
-						</el-form-item>
-					</el-col>
-					<el-col :span="12">
-						<el-form-item label="对账周期2结束">
-							<el-input-number v-model="form.apEndDay2" :min="0" :max="999" controls-position="right" style="width: 100%" />
+					<el-col :span="24">
+						<el-form-item label="对账周期">
+							<el-radio-group v-model="apCyclePreset" @change="onApCyclePresetChange">
+								<el-radio-button value="none">未启用</el-radio-button>
+								<el-radio-button value="fullMonth">整月对账</el-radio-button>
+								<el-radio-button value="firstHalf">上半月</el-radio-button>
+								<el-radio-button value="secondHalf">下半月</el-radio-button>
+								<el-radio-button value="dualHalf">上下半月双周期</el-radio-button>
+								<el-radio-button value="crossMonth">跨月(26-下月25)</el-radio-button>
+								<el-radio-button value="custom">自定义</el-radio-button>
+							</el-radio-group>
 						</el-form-item>
 					</el-col>
+					<template v-if="apCyclePreset === 'custom'">
+						<el-col :span="12">
+							<el-form-item label="对账周期1开始">
+								<el-input-number v-model="form.apStartDay1" :min="0" :max="999" controls-position="right" style="width: 100%" />
+							</el-form-item>
+						</el-col>
+						<el-col :span="12">
+							<el-form-item label="对账周期1结束">
+								<el-input-number v-model="form.apEndDay1" :min="0" :max="999" controls-position="right" style="width: 100%" />
+							</el-form-item>
+						</el-col>
+						<el-col :span="12">
+							<el-form-item label="对账周期2开始">
+								<el-input-number v-model="form.apStartDay2" :min="0" :max="999" controls-position="right" style="width: 100%" />
+							</el-form-item>
+						</el-col>
+						<el-col :span="12">
+							<el-form-item label="对账周期2结束">
+								<el-input-number v-model="form.apEndDay2" :min="0" :max="999" controls-position="right" style="width: 100%" />
+							</el-form-item>
+						</el-col>
+					</template>
 					<el-col :span="24">
-						<div class="ap-hint">
-							对账周期为整数编码:0 表示空;低位为日(01–31,31 展示为「最后」);大于 200 表示「下月」,否则为「每月」。示例:每月 5 日 → 5;下月 10 日 → 210。
-						</div>
+						<div class="ap-hint">{{ apCyclePresetHint }}</div>
 					</el-col>
 					<el-col :span="24">
 						<el-form-item label="备注">
@@ -258,6 +281,30 @@ import {
 	type S0SuppMasterRow,
 	type S0SuppMasterUpsert,
 } from '../api/s0SupplyApi';
+import { s0LocationsApi, type S0LocationOptionRow } from '../api/s0WarehouseApi';
+
+type ApCyclePresetKey = 'none' | 'fullMonth' | 'firstHalf' | 'secondHalf' | 'dualHalf' | 'crossMonth' | 'custom';
+
+const AP_CYCLE_PRESETS: Record<Exclude<ApCyclePresetKey, 'custom'>, { s1: number; e1: number; s2: number; e2: number; label: string }> = {
+	none: { s1: 0, e1: 0, s2: 0, e2: 0, label: '未启用' },
+	fullMonth: { s1: 101, e1: 131, s2: 0, e2: 0, label: '整月对账(每月 01 - 月最后日)' },
+	firstHalf: { s1: 101, e1: 115, s2: 0, e2: 0, label: '上半月对账(每月 01 - 15 日)' },
+	secondHalf: { s1: 116, e1: 131, s2: 0, e2: 0, label: '下半月对账(每月 16 日 - 月最后日)' },
+	dualHalf: { s1: 101, e1: 115, s2: 116, e2: 131, label: '上下半月双周期(每月 01-15、16 日-月最后日)' },
+	crossMonth: { s1: 126, e1: 225, s2: 0, e2: 0, label: '跨月对账(每月 26 日 - 下月 25 日)' },
+};
+
+function normalizeApDay(value: number | null | undefined): number {
+	return value == null ? 0 : value;
+}
+
+function identifyApPreset(s1: number | null | undefined, e1: number | null | undefined, s2: number | null | undefined, e2: number | null | undefined): ApCyclePresetKey {
+	const t = [normalizeApDay(s1), normalizeApDay(e1), normalizeApDay(s2), normalizeApDay(e2)];
+	for (const [key, p] of Object.entries(AP_CYCLE_PRESETS)) {
+		if (t[0] === p.s1 && t[1] === p.e1 && t[2] === p.s2 && t[3] === p.e2) return key as ApCyclePresetKey;
+	}
+	return 'custom';
+}
 
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || '供应商维护');
@@ -290,6 +337,39 @@ const factoryOptions = ref<OrgOption[]>([]);
 const currencyOptions = ref<OptionItem[]>([]);
 const crTermsOptions = ref<OptionItem[]>([]);
 const taxClassOptions = ref<OptionItem[]>([]);
+const locationOptions = ref<S0LocationOptionRow[]>([]);
+const apCyclePreset = ref<ApCyclePresetKey>('none');
+
+const apCyclePresetHint = computed(() => {
+	if (apCyclePreset.value === 'custom') {
+		return '自定义编码:0 表示空;低位为日(01–31,31 展示为「最后」);大于 200 表示「下月」,否则为「每月」。示例:每月 5 日 → 5;下月 10 日 → 210。';
+	}
+	return AP_CYCLE_PRESETS[apCyclePreset.value]?.label ?? '';
+});
+
+function onApCyclePresetChange(value: string | number | boolean | undefined) {
+	const key = value as ApCyclePresetKey;
+	if (key === 'custom') return;
+	const p = AP_CYCLE_PRESETS[key];
+	if (!p) return;
+	form.apStartDay1 = p.s1;
+	form.apEndDay1 = p.e1;
+	form.apStartDay2 = p.s2;
+	form.apEndDay2 = p.e2;
+}
+
+async function loadLocationOptions(domainCode?: string) {
+	try {
+		const list = await s0LocationsApi.options({
+			domainCode: domainCode || undefined,
+			isActive: true,
+			limit: 500,
+		});
+		locationOptions.value = Array.isArray(list) ? list : [];
+	} catch {
+		locationOptions.value = [];
+	}
+}
 
 const form = reactive<S0SuppMasterUpsert>({
 	companyRefId: undefined,
@@ -337,6 +417,7 @@ const formFactoryOptions = computed(() => {
 function syncDomainCodeFromFactory() {
 	const f = factoryOptions.value.find((item) => item.id === form.factoryRefId);
 	form.domainCode = f?.code ?? '';
+	void loadLocationOptions(form.domainCode);
 }
 
 watch(
@@ -447,8 +528,10 @@ function resetForm() {
 
 function openCreate() {
 	resetForm();
+	apCyclePreset.value = 'none';
 	dialogTitle.value = '新增供应商';
 	dialogVisible.value = true;
+	void loadLocationOptions(form.domainCode);
 }
 
 function openEdit(row: S0SuppMasterRow) {
@@ -481,7 +564,9 @@ function openEdit(row: S0SuppMasterRow) {
 		createUser: row.createUser ?? '',
 		updateUser: row.updateUser ?? '',
 	});
+	apCyclePreset.value = identifyApPreset(row.apStartDay1, row.apEndDay1, row.apStartDay2, row.apEndDay2);
 	dialogVisible.value = true;
+	void loadLocationOptions(form.domainCode);
 }
 
 async function submitForm() {

+ 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.126</AssemblyVersion>
-    <FileVersion>1.0.126</FileVersion>
-    <Version>1.0.126</Version>
+    <AssemblyVersion>1.0.127</AssemblyVersion>
+    <FileVersion>1.0.127</FileVersion>
+    <Version>1.0.127</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 35 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0LocationsController.cs

@@ -54,6 +54,41 @@ public class AdoS0LocationsController : ControllerBase
         return item == null ? NotFound() : Ok(item);
     }
 
+    [HttpGet("options")]
+    public async Task<IActionResult> GetOptionsAsync(
+        [FromQuery] long? companyRefId,
+        [FromQuery] long? factoryRefId,
+        [FromQuery] string? domainCode,
+        [FromQuery] string? keyword,
+        [FromQuery] bool? isActive,
+        [FromQuery] int? limit)
+    {
+        var enabledFilter = isActive ?? true;
+        var take = Math.Clamp(limit ?? 200, 1, 500);
+
+        var list = await _rep.AsQueryable()
+            .WhereIF(companyRefId.HasValue, x => x.CompanyRefId == companyRefId!.Value)
+            .WhereIF(factoryRefId.HasValue, x => x.FactoryRefId == factoryRefId!.Value)
+            .WhereIF(!string.IsNullOrWhiteSpace(domainCode), x => x.DomainCode == domainCode)
+            .WhereIF(!string.IsNullOrWhiteSpace(keyword),
+                x => x.Location.Contains(keyword!) || (x.Descr != null && x.Descr.Contains(keyword!)))
+            .Where(x => x.IsActive == enabledFilter)
+            .OrderBy(x => x.Location)
+            .Take(take)
+            .Select(x => new S0LocationOptionRow
+            {
+                Value = x.Location,
+                Label = x.Descr == null || x.Descr == "" ? x.Location : x.Location + " / " + x.Descr,
+                Code = x.Location,
+                Name = x.Descr,
+                DomainCode = x.DomainCode,
+                IsActive = x.IsActive
+            })
+            .ToListAsync();
+
+        return Ok(list);
+    }
+
     [HttpPost]
     public async Task<IActionResult> CreateAsync([FromBody] AdoS0LocationUpsertDto dto)
     {

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

@@ -146,6 +146,19 @@ public class AdoS0LocationUpsertDto
     public string? UpdateUser { get; set; }
 }
 
+/// <summary>
+/// 库位字典下拉项。供「供应商维护」等模块按 domainCode 过滤库位时使用,按 Location 作为业务值。
+/// </summary>
+public class S0LocationOptionRow
+{
+    public string Value { get; set; } = string.Empty;
+    public string Label { get; set; } = string.Empty;
+    public string Code { get; set; } = string.Empty;
+    public string? Name { get; set; }
+    public string? DomainCode { get; set; }
+    public bool IsActive { get; set; }
+}
+
 // ========== 货架 ==========
 
 public class AdoS0LocationShelfQueryDto