Prechádzať zdrojové kódy

fix(approval-center): show business type name instead of internal code

D-07:审批中心「业务类型」列此前直接渲染 ApprovalFlowInstance.BizType,
客户可见内部 Code(IPQC_INSPECTION / S7_FQC_INSPBILL 等)。

修法为纯前端复用既有数据源,未改后端:
审批中心挂载时本就为筛选下拉调用 /api/bizType/list(返回 ApprovalBizType 的
Code + Name,全局配置表、无 TenantId),本次把同一份数据构造成 Code→Name 映射
下发给三个 Tab,「业务类型」列改用共享 bizTypeText() 解析。
不额外发请求、不做 N+1、不新增后端 JOIN、不硬编码任何中文。

契约边界保持不变:
  bizType(Code)= 机器契约 —— 路由跳转、按钮显隐、筛选 value、后端查询仍全部用 Code;
  Name          = 仅展示,来源为 ApprovalBizType.Name。
Name 缺失/未配置/已停用时刻意回落 Code 而非 '-',避免配置缺失被静默隐藏。

三个 Tab(我的待办 / 我的已办 / 我发起的)统一走同一解析,杜绝
"待办中文、已办英文"。未改 BizType 存储值、流程实例、状态机、审批结果、流程定义。

新增 ApprovalCenterBizTypeDisplayContractTests(12 passed)钉住:
三 Tab 共用解析 · 无裸 bizType 列 · fallback 保留 Code · 复用单次字典请求 ·
筛选仍提交 Code · 路由判断仍基于 Code · 无前端硬编码中文字典。

chore: bump version Web 2.4.372
YY968XX 1 deň pred
rodič
commit
4e6b6b47ef

+ 1 - 1
Web/package.json

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

+ 5 - 2
Web/src/views/approvalFlow/center/components/DoneList.vue

@@ -2,7 +2,9 @@
 	<el-table :data="data" v-loading="loading" border style="width: 100%" :row-class-name="rowClassName">
 		<el-table-column type="index" label="#" width="55" align="center" />
 		<el-table-column prop="title" label="流程标题" show-overflow-tooltip />
-		<el-table-column prop="bizType" label="业务类型" width="140" class-name="mobile-hide" label-class-name="mobile-hide" />
+		<el-table-column prop="bizType" label="业务类型" width="140" class-name="mobile-hide" label-class-name="mobile-hide">
+			<template #default="{ row }">{{ bizTypeText(row.bizType, props.bizTypeNames) }}</template>
+		</el-table-column>
 		<el-table-column prop="nodeName" label="审批节点" width="140" />
 		<el-table-column prop="status" label="审批结果" width="100" align="center">
 			<template #default="{ row }">
@@ -22,7 +24,8 @@
 </template>
 
 <script setup lang="ts">
-const props = defineProps<{ data: any[]; loading: boolean; highlightBizId?: string }>();
+import { bizTypeText, type BizTypeNameMap } from './bizTypeDisplay';
+const props = defineProps<{ data: any[]; loading: boolean; highlightBizId?: string; bizTypeNames?: BizTypeNameMap }>();
 // 深链定位高亮:openBillId 命中的业务单所在行加底色
 const rowClassName = ({ row }: { row: any }) => (props.highlightBizId && String(row?.bizId ?? '') === props.highlightBizId ? 'flow-deeplink-hit' : '');
 const emit = defineEmits<{ (e: 'timeline', row: any): void; (e: 'viewBiz', row: any): void }>();

+ 5 - 2
Web/src/views/approvalFlow/center/components/InitiatedList.vue

@@ -2,7 +2,9 @@
 	<el-table :data="data" v-loading="loading" border style="width: 100%" :row-class-name="rowClassName">
 		<el-table-column type="index" label="#" width="55" align="center" />
 		<el-table-column prop="title" label="流程标题" show-overflow-tooltip />
-		<el-table-column prop="bizType" label="业务类型" width="140" class-name="mobile-hide" label-class-name="mobile-hide" />
+		<el-table-column prop="bizType" label="业务类型" width="140" class-name="mobile-hide" label-class-name="mobile-hide">
+			<template #default="{ row }">{{ bizTypeText(row.bizType, props.bizTypeNames) }}</template>
+		</el-table-column>
 		<el-table-column prop="bizNo" label="业务单号" width="160" show-overflow-tooltip class-name="mobile-hide" label-class-name="mobile-hide" />
 		<el-table-column prop="currentAssigneeName" label="当前审批人" width="140" show-overflow-tooltip class-name="mobile-hide" label-class-name="mobile-hide">
 			<template #default="{ row }">
@@ -29,7 +31,8 @@
 </template>
 
 <script setup lang="ts">
-const props = defineProps<{ data: any[]; loading: boolean; highlightBizId?: string }>();
+import { bizTypeText, type BizTypeNameMap } from './bizTypeDisplay';
+const props = defineProps<{ data: any[]; loading: boolean; highlightBizId?: string; bizTypeNames?: BizTypeNameMap }>();
 // 深链定位高亮:openBillId 命中的业务单所在行加底色
 const rowClassName = ({ row }: { row: any }) => (props.highlightBizId && String(row?.bizId ?? '') === props.highlightBizId ? 'flow-deeplink-hit' : '');
 const emit = defineEmits<{

+ 5 - 2
Web/src/views/approvalFlow/center/components/PendingList.vue

@@ -14,7 +14,9 @@
 					<span>{{ row.title }}</span>
 				</template>
 			</el-table-column>
-			<el-table-column prop="bizType" label="业务类型" width="140" class-name="mobile-hide" label-class-name="mobile-hide" />
+			<el-table-column prop="bizType" label="业务类型" width="140" class-name="mobile-hide" label-class-name="mobile-hide">
+			<template #default="{ row }">{{ bizTypeText(row.bizType, props.bizTypeNames) }}</template>
+		</el-table-column>
 			<el-table-column prop="bizNo" label="业务单号" width="160" show-overflow-tooltip class-name="mobile-hide" label-class-name="mobile-hide">
 				<template #default="{ row }">
 					<el-link v-if="(row.bizType === 'EXCEPTION_REPORT' || row.bizType === 'EXCEPTION_CLOSURE') && row.bizId" type="primary" @click="goToException(row.bizId)">
@@ -117,7 +119,8 @@ function goToImprovement(bizId: number | string) {
 	router.push(`/aidop/smart-ops/improvement-plans?id=${bizId}`);
 }
 
-const props = defineProps<{ data: any[]; loading: boolean; highlightBizId?: string }>();
+import { bizTypeText, type BizTypeNameMap } from './bizTypeDisplay';
+const props = defineProps<{ data: any[]; loading: boolean; highlightBizId?: string; bizTypeNames?: BizTypeNameMap }>();
 // 深链定位高亮:openBillId 命中的业务单所在行加底色,让用户一眼看到系统定位到了哪一单
 const rowClassName = ({ row }: { row: any }) => (props.highlightBizId && String(row?.bizId ?? '') === props.highlightBizId ? 'flow-deeplink-hit' : '');
 const emit = defineEmits<{

+ 21 - 0
Web/src/views/approvalFlow/center/components/bizTypeDisplay.ts

@@ -0,0 +1,21 @@
+/**
+ * 审批中心「业务类型」列的展示解析(我的待办 / 我的已办 / 我发起的 三 Tab 共用)。
+ *
+ * 契约边界:
+ *   bizType(Code)  = 机器契约 —— 路由跳转、按钮显隐、筛选 value、后端查询一律用它,**不得改动**;
+ *   bizTypeName      = 用户展示 —— 取自 ApprovalBizType.Name(全局配置表,无 TenantId)。
+ *
+ * 名称来源为 `/api/bizType/list`,审批中心挂载时已为筛选下拉加载一次,
+ * 此处复用同一份数据,**不额外发请求、不做 N+1 查询**。
+ *
+ * fallback 刻意保留 Code:新增/未配置/已停用的业务类型宁可露出内部 Code,
+ * 也不显示 '-' —— 否则配置缺失会被静默隐藏,用户完全不知道这是什么业务。
+ */
+export type BizTypeNameMap = Record<string, string>;
+
+export function bizTypeText(bizType?: string | null, names?: BizTypeNameMap): string {
+	const code = bizType == null ? '' : String(bizType).trim();
+	if (!code) return '-';
+	const name = names?.[code];
+	return name && name.trim() !== '' ? name : code;
+}

+ 12 - 2
Web/src/views/approvalFlow/center/index.vue

@@ -32,6 +32,7 @@
 				:data="tableData"
 				:loading="loading"
 				:highlight-biz-id="highlightBizId"
+				:biz-type-names="bizTypeNameMap"
 				@approve="openDialog('approve', $event)"
 				@reject="openDialog('reject', $event)"
 				@transfer="openDialog('transfer', $event)"
@@ -43,12 +44,14 @@
 				@batchReject="doBatchReject"
 				@viewBiz="openBizForm"
 			/>
-			<DoneList v-if="activeTab === 'done'" :data="tableData" :loading="loading" :highlight-biz-id="highlightBizId" @timeline="openTimeline" @viewBiz="openBizForm" />
+			<DoneList v-if="activeTab === 'done'" :data="tableData" :loading="loading" :highlight-biz-id="highlightBizId"
+				:biz-type-names="bizTypeNameMap" @timeline="openTimeline" @viewBiz="openBizForm" />
 			<InitiatedList
 				v-if="activeTab === 'initiated'"
 				:data="tableData"
 				:loading="loading"
 				:highlight-biz-id="highlightBizId"
+				:biz-type-names="bizTypeNameMap"
 				@timeline="openTimeline"
 				@urge="doUrge"
 				@withdraw="doWithdraw"
@@ -88,7 +91,7 @@
 </template>
 
 <script lang="ts" setup name="approvalFlowCenter">
-import { ref, onMounted } from 'vue';
+import { ref, computed, onMounted } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import {
@@ -131,6 +134,13 @@ const pendingCount = ref(0);
 const filterBizType = ref('');
 const bizTypeOptions = ref<Array<{ code: string; name: string }>>([]);
 
+// 业务类型 Code → 中文名映射,供三个 Tab 的「业务类型」列展示。
+// 复用筛选下拉已加载的同一份 /api/bizType/list 数据,不额外发请求、无 N+1。
+// 仅用于展示;筛选 value、路由判断、后端查询一律仍用 Code。
+const bizTypeNameMap = computed<Record<string, string>>(() =>
+	Object.fromEntries(bizTypeOptions.value.filter((b) => b?.code).map((b) => [b.code, b.name]))
+);
+
 const loadBizTypes = async () => {
 	try {
 		const res = await getBizTypeList();

+ 118 - 0
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/ApprovalFlow/ApprovalCenterBizTypeDisplayContractTests.cs

@@ -0,0 +1,118 @@
+using Xunit;
+
+namespace Admin.NET.Plugin.AiDOP.Tests.ApprovalFlow;
+
+/// <summary>
+/// 审批中心 D-07 源码契约测试:「业务类型」列展示中文 Name,但 <b>Code 仍是唯一机器契约</b>。
+///
+/// 核心边界:
+///   bizType(Code)= 路由跳转 / 按钮显隐 / 筛选 value / 后端查询 —— 不得被中文化;
+///   bizTypeName    = 仅展示,取自 ApprovalBizType.Name(全局配置表,无 TenantId)。
+/// </summary>
+public class ApprovalCenterBizTypeDisplayContractTests
+{
+    private static string Helper() => File.ReadAllText(FindFile(
+        "Web", "src", "views", "approvalFlow", "center", "components", "bizTypeDisplay.ts"));
+
+    private static string Center() => File.ReadAllText(FindFile(
+        "Web", "src", "views", "approvalFlow", "center", "index.vue"));
+
+    private static string Tab(string name) => File.ReadAllText(FindFile(
+        "Web", "src", "views", "approvalFlow", "center", "components", name));
+
+    /// <summary>三个 Tab 必须走同一个展示解析,不得各写一套。</summary>
+    [Theory]
+    [InlineData("PendingList.vue")]
+    [InlineData("DoneList.vue")]
+    [InlineData("InitiatedList.vue")]
+    public void AllThreeTabs_UseSharedBizTypeText(string file)
+    {
+        var s = Tab(file);
+        Assert.Contains("from './bizTypeDisplay'", s);
+        Assert.Contains("bizTypeText(row.bizType, props.bizTypeNames)", s);
+    }
+
+    /// <summary>三个 Tab 都不得再把 bizType 作为唯一展示(裸 prop 列)。</summary>
+    [Theory]
+    [InlineData("PendingList.vue")]
+    [InlineData("DoneList.vue")]
+    [InlineData("InitiatedList.vue")]
+    public void AllThreeTabs_NoBareBizTypeColumn(string file)
+    {
+        Assert.DoesNotContain(
+            "<el-table-column prop=\"bizType\" label=\"业务类型\" width=\"140\" class-name=\"mobile-hide\" label-class-name=\"mobile-hide\" />",
+            Tab(file));
+    }
+
+    /// <summary>Name 缺失(新增/未配置/已停用的类型)必须回落 Code,不能显示 '-' 把配置缺失藏起来。</summary>
+    [Fact]
+    public void Helper_FallsBackToCode_NotDash()
+    {
+        var s = Helper();
+        Assert.Contains("return name && name.trim() !== '' ? name : code;", s);
+        Assert.Contains("if (!code) return '-';", s);   // 仅 Code 本身为空时才 '-'
+    }
+
+    /// <summary>名称映射复用筛选下拉已加载的同一份数据,不得引入 N+1 / 额外请求。</summary>
+    [Fact]
+    public void Center_ReusesExistingBizTypeList_NoExtraFetch()
+    {
+        var s = Center();
+        Assert.Contains("const bizTypeNameMap = computed<Record<string, string>>", s);
+        Assert.Contains("bizTypeOptions.value", s);
+        // 全页只有一次业务类型字典请求
+        Assert.Equal(1, s.Split("getBizTypeList()").Length - 1);
+    }
+
+    /// <summary>三个 Tab 都拿到映射(否则会出现"待办中文、已办英文")。</summary>
+    [Fact]
+    public void Center_PassesNameMapToAllThreeTabs()
+    {
+        Assert.Equal(3, Center().Split(":biz-type-names=\"bizTypeNameMap\"").Length - 1);
+    }
+
+    /// <summary>筛选控件的 value 仍是 Code,label 才是中文——不得反过来。</summary>
+    [Fact]
+    public void Filter_StillSubmitsCodeNotName()
+    {
+        var s = Center();
+        Assert.Contains(":value=\"bt.code\"", s);
+        Assert.Contains("params.bizType = filterBizType.value;", s);
+        Assert.DoesNotContain(":value=\"bt.name\"", s);
+    }
+
+    /// <summary>路由 / 按钮显隐等业务判断继续读 Code,未被中文化波及。</summary>
+    [Fact]
+    public void RoutingAndButtonLogic_StillKeyedOnCode()
+    {
+        Assert.Contains("row.bizType === 'S7_FQC_INSPBILL'", Tab("PendingList.vue"));
+        Assert.Contains("row.bizType === 'S6_PROCESS_INSPECTION'", Tab("PendingList.vue"));
+        Assert.Contains("row.bizType === 'ORDER_CHANGE_REVIEW'", Tab("DoneList.vue"));
+        Assert.Contains("row.bizType === 'MATERIAL_SHORTAGE'", Tab("InitiatedList.vue"));
+    }
+
+    /// <summary>不得出现把 Code 硬编码成中文的前端字典(中文必须来自后端 Name)。</summary>
+    [Fact]
+    public void NoHardcodedCodeToChineseDictionary()
+    {
+        foreach (var f in new[] { "PendingList.vue", "DoneList.vue", "InitiatedList.vue" })
+        {
+            var s = Tab(f);
+            Assert.DoesNotContain("IPQC_INSPECTION:", s);
+            Assert.DoesNotContain("S7_FQC_INSPBILL:", s);
+        }
+        Assert.DoesNotContain("IPQC_INSPECTION:", Helper());
+    }
+
+    private static string FindFile(params string[] parts)
+    {
+        var dir = new DirectoryInfo(AppContext.BaseDirectory);
+        while (dir != null)
+        {
+            var candidate = Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray());
+            if (File.Exists(candidate)) return candidate;
+            dir = dir.Parent;
+        }
+        throw new FileNotFoundException(string.Join("/", parts));
+    }
+}