Browse Source

fix(aidop): 修复产线日历/加班保存与查询异常,并收敛表单必填策略。

处理 ShiftsStart 精度与 Ufld 类型映射问题,移除多处域名/地点前端必填并同步版本到 Web 2.4.90 / API 1.0.57。

Made-with: Cursor
Pengxy 1 month ago
parent
commit
ace4dd0432

+ 1 - 1
Web/package.json

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

+ 20 - 13
Web/src/views/aidop/production/executableDailyPlanList.vue

@@ -1,9 +1,6 @@
 <template>
 	<AidopDemoShell :title="pageTitle" subtitle="当前日期往后的日计划(PeriodSequenceDet)">
 		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
-			<el-form-item label="公司域名" required>
-				<el-input v-model="query.domain" placeholder="如 8010" clearable style="width: 110px" />
-			</el-form-item>
 			<el-form-item label="生产指令">
 				<el-input v-model="query.workOrds" placeholder="模糊" clearable style="width: 130px" />
 			</el-form-item>
@@ -192,7 +189,6 @@ function colOn(key: string) {
 }
 
 const query = reactive({
-	domain: '8010',
 	workOrds: '',
 	itemNum: '',
 	batch: '',
@@ -254,14 +250,9 @@ function resetQuery() {
 }
 
 async function loadList() {
-	if (!query.domain?.trim()) {
-		ElMessage.warning('请填写公司域名');
-		return;
-	}
 	loading.value = true;
 	try {
 		const data = await fetchExecutableDailyPlanList({
-			domain: query.domain.trim(),
 			workOrds: query.workOrds || undefined,
 			itemNum: query.itemNum || undefined,
 			batch: query.batch || undefined,
@@ -284,7 +275,7 @@ async function loadList() {
 }
 
 async function onWoStatusChange(row: ExecutableDailyPlanRow, v: string) {
-	const domain = row.domain ?? query.domain;
+	const domain = (row.domain ?? '').trim();
 	if (!row.workOrds || !domain) {
 		ElMessage.warning('缺少工单或域名');
 		return;
@@ -299,11 +290,27 @@ async function onWoStatusChange(row: ExecutableDailyPlanRow, v: string) {
 	}
 }
 
-async function onRelease() {
+function resolveDomainForRelease(): string | null {
 	if (!selectedRows.value.length) {
 		ElMessage.warning('请先勾选要下达的行');
-		return;
+		return null;
 	}
+	const domain = (selectedRows.value[0]?.domain ?? '').trim();
+	if (!domain) {
+		ElMessage.warning('所选行缺少公司域名');
+		return null;
+	}
+	const allSame = selectedRows.value.every((r) => (r.domain ?? '').trim() === domain);
+	if (!allSame) {
+		ElMessage.warning('所选行的公司域名不一致,请仅勾选同一域名的数据');
+		return null;
+	}
+	return domain;
+}
+
+async function onRelease() {
+	const domain = resolveDomainForRelease();
+	if (!domain) return;
 	try {
 		await ElMessageBox.confirm(`确认将选中的 ${selectedRows.value.length} 条日计划标记为已下达?`, '日计划下达', { type: 'warning' });
 	} catch {
@@ -312,7 +319,7 @@ async function onRelease() {
 	releasing.value = true;
 	try {
 		const ids = selectedRows.value.map((r) => r.id).join(',');
-		await releaseExecutableDailyPlan({ domain: query.domain.trim(), ids });
+		await releaseExecutableDailyPlan({ domain, ids });
 		ElMessage.success('下达成功');
 		await loadList();
 	} catch (e: any) {

+ 2 - 10
Web/src/views/aidop/production/holidayMasterForm.vue

@@ -19,12 +19,6 @@
 				<el-option label="调班" value="调班" />
 			</el-select>
 		</el-form-item>
-		<el-form-item label="工厂编码" prop="domain">
-			<el-input v-model="form.domain" maxlength="8" show-word-limit :disabled="readonly" />
-		</el-form-item>
-		<el-form-item label="地点" prop="site">
-			<el-input v-model="form.site" maxlength="4" show-word-limit placeholder="最多4位" :disabled="readonly" />
-		</el-form-item>
 	</el-form>
 	<div v-if="!readonly" class="form-actions">
 		<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
@@ -64,8 +58,6 @@ const rules: FormRules = {
 	holiday: [{ required: true, message: '请填写节假日名称', trigger: 'blur' }],
 	dated: [{ required: true, message: '请选择日期', trigger: 'change' }],
 	ufld1: [{ required: true, message: '请选择类型', trigger: 'change' }],
-	domain: [{ required: true, message: '请填写工厂编码', trigger: 'blur' }],
-	site: [{ required: true, message: '请填写地点', trigger: 'blur' }],
 };
 
 async function loadDetail() {
@@ -88,8 +80,8 @@ async function submit() {
 			holiday: form.holiday,
 			dated: form.dated ? `${form.dated}T00:00:00` : undefined,
 			ufld1: form.ufld1,
-			domain: form.domain,
-			site: form.site,
+			domain: form.domain || '8010',
+			site: form.site || '0000',
 		});
 		ElMessage.success('保存成功');
 		emit('saved');

+ 11 - 4
Web/src/views/aidop/production/qualityLineRestDetailForm.vue

@@ -1,8 +1,5 @@
 <template>
 	<el-form ref="formRef" :model="form" :rules="rules" label-width="140px" :disabled="readonly">
-		<el-form-item label="公司域名" prop="domain">
-			<el-input v-model="form.domain" maxlength="8" show-word-limit placeholder="选生产线后自动带出" :disabled="readonly" />
-		</el-form-item>
 		<el-form-item label="生产线" prop="prodLine">
 			<el-select
 				v-model="form.prodLine"
@@ -77,7 +74,6 @@ const timePoint = computed({
 });
 
 const rules: FormRules = {
-	domain: [{ required: true, message: '请填写或选择生产线以带出域名', trigger: 'blur' }],
 	prodLine: [{ required: true, message: '请选择生产线', trigger: 'change' }],
 	restTimePoint: [{ required: true, message: '请选择休息开始时间', trigger: 'change' }],
 };
@@ -87,6 +83,12 @@ function onProdLineChange(val: string) {
 	if (o?.extra) form.domain = o.extra;
 }
 
+function ensureDomainFromProdLine() {
+	if (form.domain?.trim()) return;
+	const o = props.lineOptions.find((x) => x.value === form.prodLine);
+	if (o?.extra) form.domain = o.extra;
+}
+
 async function loadDetail() {
 	if (!props.id) return;
 	const d = await fetchQualityLineRestDetail(props.id);
@@ -100,6 +102,11 @@ async function loadDetail() {
 
 async function submit() {
 	await formRef.value?.validate();
+	ensureDomainFromProdLine();
+	if (!form.domain?.trim()) {
+		ElMessage.warning('所选生产线未配置公司域名,请先完善生产线主数据');
+		return;
+	}
 	saving.value = true;
 	try {
 		await saveQualityLineRest({

+ 26 - 18
Web/src/views/aidop/production/shopCalendarWorkCtrForm.vue

@@ -1,11 +1,5 @@
 <template>
 	<el-form ref="formRef" :model="form" :rules="rules" label-width="140px" :disabled="readonly">
-		<el-form-item label="公司域名" prop="domain">
-			<el-input v-model="form.domain" placeholder="最多8位" maxlength="8" show-word-limit clearable :disabled="readonly" />
-		</el-form-item>
-		<el-form-item label="地点" prop="site">
-			<el-input v-model="form.site" placeholder="最多8位" maxlength="8" show-word-limit clearable :disabled="readonly" />
-		</el-form-item>
 		<el-form-item label="生产线" prop="prodLine">
 			<el-select v-model="form.prodLine" placeholder="请选择" filterable style="width: 100%" :disabled="readonly">
 				<el-option v-for="o in lineOptions" :key="o.value" :label="o.label" :value="o.value" />
@@ -20,13 +14,29 @@
 			<el-input v-model="form.workCtr" placeholder="可选,最多8位" maxlength="8" clearable :disabled="readonly" />
 		</el-form-item>
 		<el-form-item label="班次1开始" prop="shiftsStart1">
-			<el-input v-model="form.shiftsStart1" placeholder="HH:mm,如 08:00" maxlength="5" clearable style="width: 200px" :disabled="readonly" />
+			<el-time-picker
+				v-model="form.shiftsStart1"
+				format="HH:mm"
+				value-format="HH:mm"
+				placeholder="请选择时间"
+				clearable
+				style="width: 200px"
+				:disabled="readonly"
+			/>
 		</el-form-item>
 		<el-form-item label="班次1时长(小时)" prop="shiftsHours1">
 			<el-input-number v-model="form.shiftsHours1" :min="0" :max="24" :step="0.5" :disabled="readonly" />
 		</el-form-item>
 		<el-form-item label="班次2开始">
-			<el-input v-model="form.shiftsStart2" placeholder="可选 HH:mm" maxlength="5" clearable style="width: 200px" :disabled="readonly" />
+			<el-time-picker
+				v-model="form.shiftsStart2"
+				format="HH:mm"
+				value-format="HH:mm"
+				placeholder="可选"
+				clearable
+				style="width: 200px"
+				:disabled="readonly"
+			/>
 		</el-form-item>
 		<el-form-item label="班次2时长(小时)">
 			<el-input-number v-model="form.shiftsHours2" :min="0" :max="24" :step="0.5" :disabled="readonly" />
@@ -73,8 +83,8 @@ const saving = ref(false);
 
 const form = reactive({
 	id: 0 as number,
-	domain: '',
-	site: '',
+	domain: '8010',
+	site: '0000',
 	prodLine: '',
 	weekDay: 1,
 	workCtr: '',
@@ -86,8 +96,6 @@ const form = reactive({
 });
 
 const rules: FormRules = {
-	domain: [{ required: true, message: '请填写公司域名', trigger: 'blur' }],
-	site: [{ required: true, message: '请填写地点', trigger: 'blur' }],
 	prodLine: [{ required: true, message: '请选择生产线', trigger: 'change' }],
 	weekDay: [{ required: true, message: '请选择星期', trigger: 'change' }],
 };
@@ -111,8 +119,8 @@ async function loadDetail() {
 	form.weekDay = parseWeekDay(d.weekDay);
 	form.shiftsHours1 = d.shiftsHours1 ?? undefined;
 	form.shiftsHours2 = d.shiftsHours2 ?? undefined;
-	form.domain = d.domain ?? '';
-	form.site = d.site ?? '';
+	form.domain = d.domain ?? '8010';
+	form.site = d.site ?? '0000';
 	form.workCtr = d.workCtr ?? '';
 	form.ufld1 = d.descr ?? '';
 	form.shiftsStart1 = d.shiftsStart1Text || '';
@@ -125,8 +133,8 @@ async function submit() {
 	try {
 		await saveShopCalendar({
 			id: form.id || undefined,
-			domain: form.domain,
-			site: form.site,
+			domain: form.domain || '8010',
+			site: form.site || '0000',
 			prodLine: form.prodLine,
 			weekDay: form.weekDay,
 			workCtr: form.workCtr || undefined,
@@ -152,8 +160,8 @@ watch(
 		if (props.id) await loadDetail();
 		else {
 			form.id = 0;
-			form.domain = '';
-			form.site = '';
+			form.domain = '8010';
+			form.site = '0000';
 			form.prodLine = '';
 			form.weekDay = 1;
 			form.workCtr = '';

+ 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.56</AssemblyVersion>
-    <FileVersion>1.0.56</FileVersion>
-    <Version>1.0.56</Version>
+    <AssemblyVersion>1.0.57</AssemblyVersion>
+    <FileVersion>1.0.57</FileVersion>
+    <Version>1.0.57</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 2 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/Production/Dto/ExecutableDailyPlanDto.cs

@@ -6,9 +6,8 @@ public class ExecutableDailyPlanListInput
     public int Page { get; set; } = 1;
     public int PageSize { get; set; } = 20;
 
-    /// <summary>公司域名(PeriodSequenceDet.Domain)</summary>
-    [Required(ErrorMessage = "公司域名不能为空")]
-    public string Domain { get; set; } = string.Empty;
+    /// <summary>公司域名(可选)</summary>
+    public string? Domain { get; set; }
 
     /// <summary>生产指令(模糊)</summary>
     public string? WorkOrds { get; set; }

+ 6 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/Production/ExecutableDailyPlanService.cs

@@ -24,19 +24,20 @@ public class ExecutableDailyPlanService : IDynamicApiController, ITransient
     [HttpGet("daily-plan/list")]
     public async Task<object> GetList([FromQuery] ExecutableDailyPlanListInput input)
     {
-        var pars = new List<SugarParameter>
-        {
-            new("@Domain", input.Domain.Trim())
-        };
+        var pars = new List<SugarParameter>();
 
         var innerWhere = new List<string>
         {
-            "p.`Domain` = @Domain",
             "p.OrdQty > 0",
             "p.IsActive = 1",
             "DATE(p.PlanDate) >= CURDATE()",
             "LOWER(IFNULL(w.Status,'')) <> 'c'"
         };
+        if (!string.IsNullOrWhiteSpace(input.Domain))
+        {
+            innerWhere.Add("p.`Domain` = @Domain");
+            pars.Add(new SugarParameter("@Domain", input.Domain.Trim()));
+        }
 
         if (!string.IsNullOrWhiteSpace(input.WorkOrds))
         {

+ 3 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Production/QualityLineWorkDetailService.cs

@@ -48,7 +48,9 @@ public class QualityLineWorkDetailService : IDynamicApiController, ITransient
                 qd.`Domain` AS Domain,
                 qd.Site AS Site
             FROM QualityLineWorkDetail qd
-            LEFT JOIN LineMaster lm ON qd.`Domain` = lm.`Domain` AND qd.ProdLine = lm.`Line`
+            LEFT JOIN LineMaster lm
+                ON qd.`Domain` COLLATE utf8mb4_general_ci = lm.`Domain` COLLATE utf8mb4_general_ci
+               AND qd.ProdLine COLLATE utf8mb4_general_ci = lm.`Line` COLLATE utf8mb4_general_ci
             WHERE {where}
             """;
 

+ 5 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/Production/ResourceOvertimeService.cs

@@ -105,7 +105,7 @@ public class ResourceOvertimeService : IDynamicApiController, ITransient
     [HttpPost("overtime/save")]
     public async Task<object> Save([FromBody] ResourceOvertimeSaveInput input)
     {
-        var account = Truncate(_userManager.Account ?? "system", 24);
+        var account = Truncate(_userManager.Account ?? "system", 8);
         var now = DateTime.Now;
         var domain = Truncate(input.Domain.Trim(), 8);
         var site = string.IsNullOrWhiteSpace(input.Site) ? null : Truncate(input.Site.Trim(), 8);
@@ -247,8 +247,8 @@ public class ResourceOvertimeService : IDynamicApiController, ITransient
         public bool IsActive { get; set; }
         public bool IsConfirm { get; set; }
         public bool IsChanged { get; set; }
-        public decimal? Ufld1 { get; set; }
-        public decimal? Ufld2 { get; set; }
+        public string? Ufld1 { get; set; }
+        public string? Ufld2 { get; set; }
         public decimal? Time1 { get; set; }
         public decimal? Time2 { get; set; }
         public string? CreateUser { get; set; }
@@ -267,8 +267,8 @@ public class ResourceOvertimeService : IDynamicApiController, ITransient
         public DateTime? StartTime { get; set; }
         public DateTime? EndTime { get; set; }
         public string? Descr { get; set; }
-        public decimal? Ufld1 { get; set; }
-        public decimal? Ufld2 { get; set; }
+        public string? Ufld1 { get; set; }
+        public string? Ufld2 { get; set; }
         public decimal? WorkMinutes { get; set; }
         public decimal? RestMinutes { get; set; }
     }

+ 10 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/Production/ShopCalendarWorkCtrService.cs

@@ -110,7 +110,7 @@ public class ShopCalendarWorkCtrService : IDynamicApiController, ITransient
     [HttpPost("shop-calendar/save")]
     public async Task<object> Save([FromBody] ShopCalendarWorkCtrSaveInput input)
     {
-        var account = Truncate(_userManager.Account ?? "system", 24);
+        var account = Truncate(_userManager.Account ?? "system", 8);
         var now = DateTime.Now;
         var domain = Truncate(input.Domain.Trim(), 8);
         var site = Truncate(input.Site.Trim(), 8);
@@ -118,8 +118,8 @@ public class ShopCalendarWorkCtrService : IDynamicApiController, ITransient
         var workCtr = string.IsNullOrWhiteSpace(input.WorkCtr) ? null : Truncate(input.WorkCtr.Trim(), 8);
         var ufld1 = string.IsNullOrWhiteSpace(input.Ufld1) ? null : Truncate(input.Ufld1.Trim(), 8);
 
-        var s1 = ParseTimeToDecimal(input.ShiftsStart1) ?? 0m;
-        var s2 = ParseTimeToDecimal(input.ShiftsStart2) ?? 0m;
+        var s1 = NormalizeShiftDecimal(ParseTimeToDecimal(input.ShiftsStart1));
+        var s2 = NormalizeShiftDecimal(ParseTimeToDecimal(input.ShiftsStart2));
         var h1 = input.ShiftsHours1 ?? 0m;
         var h2 = input.ShiftsHours2 ?? 0m;
 
@@ -255,6 +255,13 @@ public class ShopCalendarWorkCtrService : IDynamicApiController, ITransient
         return h + m / 60m;
     }
 
+    private static decimal NormalizeShiftDecimal(decimal? value)
+    {
+        if (value is null) return 0m;
+        // DB 列是 decimal(10,5),避免循环小数导致超精度写入报错。
+        return Math.Round(value.Value, 5, MidpointRounding.AwayFromZero);
+    }
+
     private static string? DecimalHoursToTimeString(decimal? d)
     {
         if (d is null || d.Value == 0) return null;