From cd629ebc2899c4bb30b51ce5f99a84574804f863 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期五, 31 七月 2026 15:40:21 +0800
Subject: [PATCH] 银川 1.API密钥管理页面开发联调 2.模型管理页面开发联调 3.知识库管理页面开发联调 4.通知公告页面开发联调

---
 src/api/bpm/oa/notice/index.ts                       |   66 +++
 src/views/ai/knowledge/knowledge/retrieval/index.vue |   53 ++
 src/views/bpm/oa/notice/data.ts                      |  248 +++++++++++++
 src/views/bpm/oa/notice/detail.vue                   |  114 ++++++
 src/views/system/notify/my/data.ts                   |   12 
 src/views/ai/knowledge/segment/data.ts               |    5 
 src/views/ai/model/model/data.ts                     |   25 -
 src/views/bpm/oa/notice/modules/form.vue             |  108 ++++++
 src/views/srm/tender/detail.vue                      |   26 
 src/api/ai/knowledge/document/index.ts               |   62 +++
 src/views/bpm/oa/notice/index.vue                    |  168 +++++++++
 src/views/ai/knowledge/document/index.vue            |   30 +
 src/views/system/notify/message/data.ts              |   15 
 src/api/ai/knowledge/segment/index.ts                |   50 ++
 src/views/ai/knowledge/segment/index.vue             |    8 
 src/views/ai/knowledge/document/data.ts              |   84 ----
 16 files changed, 927 insertions(+), 147 deletions(-)

diff --git a/src/api/ai/knowledge/document/index.ts b/src/api/ai/knowledge/document/index.ts
index 589a2fe..404ae25 100644
--- a/src/api/ai/knowledge/document/index.ts
+++ b/src/api/ai/knowledge/document/index.ts
@@ -7,11 +7,42 @@
     id: number; // 缂栧彿
     knowledgeId: number; // 鐭ヨ瘑搴撶紪鍙�
     name: string; // 鏂囨。鍚嶇О
+    url: string; // 鏂囨。 URL
     contentLength: number; // 瀛楃鏁�
     tokens: number; // token 鏁�
     segmentMaxTokens: number; // 鍒嗙墖鏈�澶� token 鏁�
     retrievalCount: number; // 鍙洖娆℃暟
     status: number; // 鏄惁鍚敤
+  }
+
+  export interface CreateDocumentParams {
+    knowledgeId: number;
+    name: string;
+    url: string;
+  }
+
+  export interface CreateDocumentListParams {
+    knowledgeId: number;
+    segmentMaxTokens: number;
+    list: Array<{ name: string; url: string }>;
+  }
+
+  export interface UpdateDocumentParams {
+    id: number;
+    name?: string;
+    url?: string;
+    segmentMaxTokens?: number;
+  }
+
+  export interface UpdateDocumentStatusParams {
+    id: number;
+    status: number;
+  }
+
+  export interface DocumentProgress {
+    documentId: number;
+    count: number;
+    embeddingCount: number;
   }
 }
 
@@ -24,26 +55,34 @@
 
 /** 鏌ヨ鐭ヨ瘑搴撴枃妗h鎯� */
 export function getKnowledgeDocument(id: number) {
-  return requestClient.get(`/ai/knowledge/document/get?id=${id}`);
+  return requestClient.get<AiKnowledgeDocumentApi.KnowledgeDocument>(
+    `/ai/knowledge/document/get?id=${id}`,
+  );
 }
 
 /** 鏂板鐭ヨ瘑搴撴枃妗o紙鍗曚釜锛� */
-export function createKnowledge(data: any) {
+export function createKnowledge(data: AiKnowledgeDocumentApi.CreateDocumentParams) {
   return requestClient.post('/ai/knowledge/document/create', data);
 }
 
 /** 鏂板鐭ヨ瘑搴撴枃妗o紙澶氫釜锛� */
-export function createKnowledgeDocumentList(data: any) {
+export function createKnowledgeDocumentList(
+  data: AiKnowledgeDocumentApi.CreateDocumentListParams,
+) {
   return requestClient.post('/ai/knowledge/document/create-list', data);
 }
 
 /** 淇敼鐭ヨ瘑搴撴枃妗� */
-export function updateKnowledgeDocument(data: any) {
+export function updateKnowledgeDocument(
+  data: AiKnowledgeDocumentApi.UpdateDocumentParams,
+) {
   return requestClient.put('/ai/knowledge/document/update', data);
 }
 
 /** 淇敼鐭ヨ瘑搴撴枃妗g姸鎬� */
-export function updateKnowledgeDocumentStatus(data: any) {
+export function updateKnowledgeDocumentStatus(
+  data: AiKnowledgeDocumentApi.UpdateDocumentStatusParams,
+) {
   return requestClient.put('/ai/knowledge/document/update-status', data);
 }
 
@@ -51,3 +90,16 @@
 export function deleteKnowledgeDocument(id: number) {
   return requestClient.delete(`/ai/knowledge/document/delete?id=${id}`);
 }
+
+/** 澶勭悊鏂囨。锛堝垎娈�+鍚戦噺鍖栵級 */
+export function processDocument(id: number) {
+  return requestClient.post(`/ai/knowledge/document/process/${id}`);
+}
+
+/** 鏌ヨ鏂囨。澶勭悊杩涘害 */
+export function getDocumentProgress(documentIds: number[]) {
+  return requestClient.get<AiKnowledgeDocumentApi.DocumentProgress[]>(
+    '/ai/knowledge/document/progress',
+    { params: { documentIds: documentIds.join(',') } },
+  );
+}
diff --git a/src/api/ai/knowledge/segment/index.ts b/src/api/ai/knowledge/segment/index.ts
index 577ce0b..258054e 100644
--- a/src/api/ai/knowledge/segment/index.ts
+++ b/src/api/ai/knowledge/segment/index.ts
@@ -14,6 +14,27 @@
     retrievalCount: number; // 鍙洖娆℃暟
     status: number; // 鏂囨。鐘舵��
     createTime: number; // 鍒涘缓鏃堕棿
+    score?: number; // 鐩镐技搴﹀垎鏁帮紙0~1锛夛紝浠呮悳绱㈡帴鍙h繑鍥�
+    documentName?: string; // 鏉ユ簮鏂囨。鍚嶇О锛屼粎鎼滅储鎺ュ彛杩斿洖
+  }
+
+  export interface SearchParams {
+    knowledgeId: number;
+    content: string;
+    topK?: number;
+    similarityThreshold?: number;
+  }
+
+  export interface SplitResult {
+    content: string;
+    contentLength: number;
+    tokens: number;
+  }
+
+  export interface ProcessInfo {
+    documentId: number;
+    count: number;
+    embeddingCount: number;
   }
 }
 
@@ -61,21 +82,30 @@
 
 /** 鍒囩墖鍐呭 */
 export function splitContent(url: string, segmentMaxTokens: number) {
-  return requestClient.get('/ai/knowledge/segment/split', {
-    params: { url, segmentMaxTokens },
-  });
+  return requestClient.get<AiKnowledgeSegmentApi.SplitResult[]>(
+    '/ai/knowledge/segment/split',
+    {
+      params: { url, segmentMaxTokens },
+    },
+  );
 }
 
 /** 鑾峰彇鏂囨。澶勭悊鍒楄〃 */
 export function getKnowledgeSegmentProcessList(documentIds: number[]) {
-  return requestClient.get('/ai/knowledge/segment/get-process-list', {
-    params: { documentIds: documentIds.join(',') },
-  });
+  return requestClient.get<AiKnowledgeSegmentApi.ProcessInfo[]>(
+    '/ai/knowledge/segment/get-process-list',
+    {
+      params: { documentIds: documentIds.join(',') },
+    },
+  );
 }
 
-/** 鎼滅储鐭ヨ瘑搴撳垎娈� */
-export function searchKnowledgeSegment(params: any) {
-  return requestClient.get('/ai/knowledge/segment/search', {
+/** 鎼滅储鐭ヨ瘑搴撳垎娈碉紙RAG 鍚戦噺妫�绱級 */
+export function searchKnowledgeSegment(
+  params: AiKnowledgeSegmentApi.SearchParams,
+) {
+  return requestClient.post<AiKnowledgeSegmentApi.KnowledgeSegment[]>(
+    '/ai/knowledge/segment/search',
     params,
-  });
+  );
 }
diff --git a/src/api/bpm/oa/notice/index.ts b/src/api/bpm/oa/notice/index.ts
new file mode 100644
index 0000000..d66825a
--- /dev/null
+++ b/src/api/bpm/oa/notice/index.ts
@@ -0,0 +1,66 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace OaNoticeApi {
+  export interface NoticeVO {
+    id: number;
+    title: string;
+    content: string;
+    type: number;
+    status: number;
+    sendScope: number;
+    isTop: number;
+    publishUserId: number;
+    publishTime: string;
+    createTime: string;
+    userIds?: number[];
+  }
+
+  export interface NoticeSaveReqVO {
+    id?: number;
+    title: string;
+    content: string;
+    type: number;
+    status: number;
+    sendScope: number;
+    isTop?: number;
+    userIds?: number[];
+  }
+}
+
+const BASE = '/bpm/oa/notice';
+
+/** 鍒嗛〉鏌ヨ閫氱煡鍏憡 */
+export function getNoticePage(params: PageParam) {
+  return requestClient.get<PageResult<OaNoticeApi.NoticeVO>>(`${BASE}/page`, {
+    params,
+  });
+}
+
+/** 鏌ヨ閫氱煡鍏憡璇︽儏锛堣皟鐢ㄥ嵆鏍囪宸茶锛� */
+export function getNotice(id: number) {
+  return requestClient.get<OaNoticeApi.NoticeVO>(`${BASE}/get`, {
+    params: { id },
+  });
+}
+
+/** 鍒涘缓閫氱煡鍏憡 */
+export function createNotice(data: OaNoticeApi.NoticeSaveReqVO) {
+  return requestClient.post(`${BASE}/create`, data);
+}
+
+/** 淇敼閫氱煡鍏憡 */
+export function updateNotice(data: OaNoticeApi.NoticeSaveReqVO) {
+  return requestClient.put(`${BASE}/update`, data);
+}
+
+/** 鍒犻櫎閫氱煡鍏憡 */
+export function deleteNotice(id: number) {
+  return requestClient.delete(`${BASE}/delete`, { params: { id } });
+}
+
+/** 鑾峰彇鎴戠殑鏈閫氱煡鍏憡鏁伴噺 */
+export function getMyUnreadNoticeCount() {
+  return requestClient.get<number>(`${BASE}/my-unread-count`);
+}
diff --git a/src/views/ai/knowledge/document/data.ts b/src/views/ai/knowledge/document/data.ts
index 69ccd43..5556582 100644
--- a/src/views/ai/knowledge/document/data.ts
+++ b/src/views/ai/knowledge/document/data.ts
@@ -2,90 +2,8 @@
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
 import type { AiKnowledgeDocumentApi } from '#/api/ai/knowledge/document';
 
-import { AiModelTypeEnum, CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
 import { getDictOptions } from '@vben/hooks';
-
-import { z } from '#/adapter/form';
-import { getModelSimpleList } from '#/api/ai/model/model';
-
-/** 鏂板/淇敼鐨勮〃鍗� */
-export function useFormSchema(): VbenFormSchema[] {
-  return [
-    {
-      component: 'Input',
-      fieldName: 'id',
-      dependencies: {
-        triggerFields: [''],
-        show: () => false,
-      },
-    },
-    {
-      component: 'Input',
-      fieldName: 'name',
-      label: '鐭ヨ瘑搴撳悕绉�',
-      rules: 'required',
-    },
-    {
-      fieldName: 'description',
-      label: '鐭ヨ瘑搴撴弿杩�',
-      component: 'Textarea',
-      componentProps: {
-        rows: 3,
-        placeholder: '璇疯緭鍏ョ煡璇嗗簱鎻忚堪',
-      },
-    },
-    {
-      component: 'ApiSelect',
-      fieldName: 'embeddingModelId',
-      label: '鍚戦噺妯″瀷',
-      componentProps: {
-        api: () => getModelSimpleList(AiModelTypeEnum.EMBEDDING),
-        labelField: 'name',
-        valueField: 'id',
-        allowClear: true,
-        placeholder: '璇烽�夋嫨鍚戦噺妯″瀷',
-      },
-      rules: 'required',
-    },
-    {
-      fieldName: 'topK',
-      label: '妫�绱� topK',
-      component: 'InputNumber',
-      componentProps: {
-        class: '!w-full',
-        placeholder: '璇疯緭鍏ユ绱� topK',
-        min: 0,
-        max: 10,
-      },
-      rules: 'required',
-    },
-    {
-      fieldName: 'similarityThreshold',
-      label: '妫�绱㈢浉浼煎害闃堝��',
-      component: 'InputNumber',
-      componentProps: {
-        class: '!w-full',
-        placeholder: '璇疯緭鍏ユ绱㈢浉浼煎害闃堝��',
-        min: 0,
-        max: 1,
-        step: 0.01,
-        precision: 2,
-      },
-      rules: 'required',
-    },
-    {
-      fieldName: 'status',
-      label: '鏄惁鍚敤',
-      component: 'RadioGroup',
-      componentProps: {
-        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
-        buttonStyle: 'solid',
-        optionType: 'button',
-      },
-      rules: z.number().default(CommonStatusEnum.ENABLE),
-    },
-  ];
-}
 
 /** 鍒楄〃鐨勬悳绱㈣〃鍗� */
 export function useGridFormSchema(): VbenFormSchema[] {
diff --git a/src/views/ai/knowledge/document/index.vue b/src/views/ai/knowledge/document/index.vue
index 8585ac5..f0a4170 100644
--- a/src/views/ai/knowledge/document/index.vue
+++ b/src/views/ai/knowledge/document/index.vue
@@ -15,6 +15,7 @@
 import {
   deleteKnowledgeDocument,
   getKnowledgeDocumentPage,
+  processDocument,
   updateKnowledgeDocumentStatus,
 } from '#/api/ai/knowledge/document';
 import { $t } from '#/locales';
@@ -69,6 +70,23 @@
     name: 'AiKnowledgeSegment',
     query: { documentId: id },
   });
+}
+
+/** 澶勭悊鏂囨。锛堝垎娈�+鍚戦噺鍖栵級 */
+async function handleProcess(row: AiKnowledgeDocumentApi.KnowledgeDocument) {
+  const hideLoading = message.loading({
+    content: `姝e湪澶勭悊鏂囨。銆�${row.name}銆�...`,
+    duration: 0,
+  });
+  try {
+    await processDocument(row.id!);
+    message.success(`鏂囨。銆�${row.name}銆戝鐞嗘垚鍔焋);
+    handleRefresh();
+  } catch {
+    message.error(`鏂囨。銆�${row.name}銆戝鐞嗗け璐);
+  } finally {
+    hideLoading();
+  }
 }
 
 /** 鏇存柊鏂囨。鐘舵�� */
@@ -137,6 +155,11 @@
 
 <template>
   <Page auto-content-height>
+    <div class="mb-2">
+      <a-button type="default" @click="router.back()">
+        杩斿洖涓婁竴椤�
+      </a-button>
+    </div>
     <Grid table-title="鐭ヨ瘑搴撴枃妗e垪琛�">
       <template #toolbar-tools>
         <TableAction
@@ -169,6 +192,13 @@
               onClick: handleSegment.bind(null, row.id),
             },
             {
+              label: '澶勭悊',
+              type: 'link',
+              icon: ACTION_ICON.REFRESH,
+              auth: ['ai:knowledge:update'],
+              onClick: handleProcess.bind(null, row),
+            },
+            {
               label: $t('common.delete'),
               type: 'link',
               danger: true,
diff --git a/src/views/ai/knowledge/knowledge/retrieval/index.vue b/src/views/ai/knowledge/knowledge/retrieval/index.vue
index f7b4e4f..cdf76d6 100644
--- a/src/views/ai/knowledge/knowledge/retrieval/index.vue
+++ b/src/views/ai/knowledge/knowledge/retrieval/index.vue
@@ -1,4 +1,6 @@
 <script setup lang="ts">
+import type { AiKnowledgeSegmentApi } from '#/api/ai/knowledge/segment';
+
 import { onMounted, reactive, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 
@@ -17,15 +19,24 @@
 import { getKnowledge } from '#/api/ai/knowledge/knowledge';
 import { searchKnowledgeSegment } from '#/api/ai/knowledge/segment';
 
+type RetrievalSegment = AiKnowledgeSegmentApi.KnowledgeSegment & {
+  expanded?: boolean;
+};
+
 /** 鐭ヨ瘑搴撴枃妗e彫鍥炴祴璇� */
 defineOptions({ name: 'KnowledgeDocumentRetrieval' });
 
 const route = useRoute();
 const router = useRouter();
 
-const loading = ref(false); // 鍔犺浇鐘舵��
-const segments = ref<any[]>([]); // 鍙洖缁撴灉
-const queryParams = reactive({
+const loading = ref(false);
+const segments = ref<RetrievalSegment[]>([]);
+const queryParams = reactive<{
+  id: number | undefined;
+  content: string;
+  topK: number;
+  similarityThreshold: number;
+}>({
   id: undefined,
   content: '',
   topK: 10,
@@ -38,6 +49,7 @@
     message.warning('璇疯緭鍏ユ煡璇㈡枃鏈�');
     return;
   }
+  if (queryParams.id == null) return;
 
   loading.value = true;
   segments.value = [];
@@ -49,15 +61,22 @@
       topK: queryParams.topK,
       similarityThreshold: queryParams.similarityThreshold,
     });
-    segments.value = data || [];
+    segments.value = (data || []) as RetrievalSegment[];
   } finally {
     loading.value = false;
   }
 }
 
 /** 鍒囨崲娈佃惤灞曞紑鐘舵�� */
-function toggleExpand(segment: any) {
+function toggleExpand(segment: RetrievalSegment) {
   segment.expanded = !segment.expanded;
+}
+
+/** 鑾峰彇鐩镐技搴﹀垎鏁扮殑鏍峰紡绫诲悕 */
+function getScoreClass(score: number) {
+  if (score > 0.8) return 'bg-green-50 text-green-600';
+  if (score >= 0.6) return 'bg-yellow-50 text-yellow-600';
+  return 'bg-gray-100 text-gray-400';
 }
 
 /** 鑾峰彇鐭ヨ瘑搴撻厤缃俊鎭� */
@@ -69,25 +88,30 @@
       queryParams.similarityThreshold =
         knowledge.similarityThreshold || queryParams.similarityThreshold;
     }
-  } catch {}
+  } catch {
+    // 鑾峰彇鐭ヨ瘑搴撻厤缃け璐ワ紝浣跨敤榛樿鍊�
+  }
 }
 
 /** 鍒濆鍖� */
 onMounted(() => {
-  // 濡傛灉鐭ヨ瘑搴� ID 涓嶅瓨鍦紝鏄剧ず閿欒鎻愮ず骞跺叧闂〉闈�
   if (!route.query.id) {
     message.error('鐭ヨ瘑搴� ID 涓嶅瓨鍦紝鏃犳硶杩涜鍙洖娴嬭瘯');
     router.back();
     return;
   }
-  queryParams.id = route.query.id as any;
-
-  // 鑾峰彇鐭ヨ瘑搴撲俊鎭苟璁剧疆榛樿鍊�
-  getKnowledgeInfo(queryParams.id as any);
+  const id = Number(route.query.id);
+  queryParams.id = id;
+  getKnowledgeInfo(id);
 });
 </script>
 <template>
   <Page auto-content-height>
+    <div class="mb-2">
+      <a-button type="default" @click="router.back()">
+        杩斿洖涓婁竴椤�
+      </a-button>
+    </div>
     <div class="flex w-full gap-4">
       <Card class="w-3/4 flex-1">
         <div class="mb-15">
@@ -106,7 +130,7 @@
               placeholder="璇疯緭鍏ユ枃鏈�"
             />
             <div class="absolute bottom-2 right-2 text-sm text-gray-400">
-              {{ queryParams.content?.length }} / 200
+              {{ queryParams.content?.length || 0 }} 瀛楃
             </div>
           </div>
           <div class="m-2 flex items-center">
@@ -165,9 +189,10 @@
                   {{ segment.tokens }} Token
                 </span>
                 <span
-                  class="whitespace-nowrap rounded-full bg-blue-50 px-2 py-1 text-sm text-blue-500"
+                  class="whitespace-nowrap rounded-full px-2 py-1 text-sm"
+                  :class="getScoreClass(segment.score ?? 0)"
                 >
-                  score: {{ segment.score }}
+                  score: {{ segment.score?.toFixed(4) }}
                 </span>
               </div>
               <div
diff --git a/src/views/ai/knowledge/segment/data.ts b/src/views/ai/knowledge/segment/data.ts
index 109adda..985a6d2 100644
--- a/src/views/ai/knowledge/segment/data.ts
+++ b/src/views/ai/knowledge/segment/data.ts
@@ -96,6 +96,11 @@
       minWidth: 120,
     },
     {
+      field: 'vectorId',
+      title: '鍚戦噺ID',
+      minWidth: 150,
+    },
+    {
       field: 'retrievalCount',
       title: '鍙洖娆℃暟',
       minWidth: 100,
diff --git a/src/views/ai/knowledge/segment/index.vue b/src/views/ai/knowledge/segment/index.vue
index 981c6ad..2c619dd 100644
--- a/src/views/ai/knowledge/segment/index.vue
+++ b/src/views/ai/knowledge/segment/index.vue
@@ -3,7 +3,7 @@
 import type { AiKnowledgeSegmentApi } from '#/api/ai/knowledge/segment';
 
 import { onMounted } from 'vue';
-import { useRoute } from 'vue-router';
+import { useRoute, useRouter } from 'vue-router';
 
 import { confirm, Page, useVbenModal } from '@vben/common-ui';
 import { DICT_TYPE } from '@vben/constants';
@@ -23,6 +23,7 @@
 import Form from './modules/form.vue';
 
 const route = useRoute();
+const router = useRouter();
 
 const [FormModal, formModalApi] = useVbenModal({
   connectedComponent: Form,
@@ -117,6 +118,11 @@
 <template>
   <Page auto-content-height>
     <FormModal @success="handleRefresh" />
+    <div class="mb-2">
+      <a-button type="default" @click="router.back()">
+        杩斿洖涓婁竴椤�
+      </a-button>
+    </div>
     <Grid table-title="鍒嗘鍒楄〃">
       <template #toolbar-tools>
         <TableAction
diff --git a/src/views/ai/model/model/data.ts b/src/views/ai/model/model/data.ts
index 8680a0c..f40e590 100644
--- a/src/views/ai/model/model/data.ts
+++ b/src/views/ai/model/model/data.ts
@@ -1,16 +1,10 @@
 import type { VbenFormSchema } from '#/adapter/form';
 import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { AiModelApiKeyApi } from '#/api/ai/model/apiKey';
 
 import { AiModelTypeEnum, CommonStatusEnum, DICT_TYPE } from '@vben/constants';
 import { getDictOptions } from '@vben/hooks';
 
 import { z } from '#/adapter/form';
-import { getApiKeySimpleList } from '#/api/ai/model/apiKey';
-
-/** 鍏宠仈鏁版嵁 */
-let apiKeyList: AiModelApiKeyApi.ApiKey[] = [];
-getApiKeySimpleList().then((data) => (apiKeyList = data));
 
 /** 鏂板/淇敼鐨勮〃鍗� */
 export function useFormSchema(): VbenFormSchema[] {
@@ -50,14 +44,12 @@
     },
     {
       fieldName: 'keyId',
-      label: 'API 绉橀挜',
-      component: 'ApiSelect',
+      label: 'API 绉橀挜 ID',
+      component: 'InputNumber',
       componentProps: {
-        placeholder: '璇烽�夋嫨 API 绉橀挜',
-        api: getApiKeySimpleList,
-        labelField: 'name',
-        valueField: 'id',
-        allowClear: true,
+        class: '!w-full',
+        placeholder: '璇疯緭鍏� API 绉橀挜 ID',
+        min: 0,
       },
       rules: 'required',
     },
@@ -222,13 +214,8 @@
       minWidth: 180,
     },
     {
-      title: 'API 绉橀挜',
+      title: 'API 绉橀挜 ID',
       field: 'keyId',
-      formatter: ({ cellValue }) => {
-        return (
-          apiKeyList.find((apiKey) => apiKey.id === cellValue)?.name || '-'
-        );
-      },
       minWidth: 140,
     },
     {
diff --git a/src/views/bpm/oa/notice/data.ts b/src/views/bpm/oa/notice/data.ts
new file mode 100644
index 0000000..6636eab
--- /dev/null
+++ b/src/views/bpm/oa/notice/data.ts
@@ -0,0 +1,248 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { z } from '#/adapter/form';
+
+// ========== 鏈湴鏋氫妇甯搁噺 ==========
+
+/** 閫氱煡绫诲瀷锛�1=閫氱煡 2=鍏憡 */
+export const NOTICE_TYPE = {
+  NOTICE: 1,
+  ANNOUNCEMENT: 2,
+} as const;
+
+export const NOTICE_TYPE_OPTIONS = [
+  { label: '閫氱煡', value: 1 },
+  { label: '鍏憡', value: 2 },
+];
+
+export const NOTICE_TYPE_TAG: Record<number, string> = {
+  1: 'blue',
+  2: 'orange',
+};
+
+/** 閫氱煡鐘舵�侊細0=鑽夌 1=宸插彂甯� 2=宸叉挙閿� */
+export const NOTICE_STATUS = {
+  DRAFT: 0,
+  PUBLISHED: 1,
+  REVOKED: 2,
+} as const;
+
+export const NOTICE_STATUS_OPTIONS = [
+  { label: '鑽夌', value: 0 },
+  { label: '宸插彂甯�', value: 1 },
+  { label: '宸叉挙閿�', value: 2 },
+];
+
+export const NOTICE_STATUS_TAG: Record<number, string> = {
+  0: 'default',
+  1: 'green',
+  2: 'red',
+};
+
+/** 鍙戦�佽寖鍥达細1=閫氱煡鎵�鏈変汉 2=鎸囧畾浜� */
+export const SEND_SCOPE = {
+  ALL: 1,
+  SPECIFIC: 2,
+} as const;
+
+export const SEND_SCOPE_OPTIONS = [
+  { label: '閫氱煡鎵�鏈変汉', value: 1 },
+  { label: '鎸囧畾浜�', value: 2 },
+];
+
+export const SEND_SCOPE_MAP: Record<number, string> = {
+  1: '閫氱煡鎵�鏈変汉',
+  2: '鎸囧畾浜�',
+};
+
+// ========== 琛ㄥ崟 Schema ==========
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      component: 'Input',
+      fieldName: 'id',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      component: 'Input',
+      fieldName: 'title',
+      label: '鏍囬',
+      rules: 'required',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ爣棰�',
+        maxlength: 50,
+        showCount: true,
+      },
+    },
+    {
+      fieldName: 'type',
+      label: '绫诲瀷',
+      component: 'RadioGroup',
+      componentProps: {
+        options: NOTICE_TYPE_OPTIONS,
+        buttonStyle: 'solid',
+        optionType: 'button',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'RadioGroup',
+      componentProps: {
+        options: NOTICE_STATUS_OPTIONS,
+        buttonStyle: 'solid',
+        optionType: 'button',
+      },
+      rules: z.number().default(NOTICE_STATUS.DRAFT),
+    },
+    {
+      fieldName: 'sendScope',
+      label: '鍙戦�佽寖鍥�',
+      component: 'RadioGroup',
+      componentProps: {
+        options: SEND_SCOPE_OPTIONS,
+        buttonStyle: 'solid',
+        optionType: 'button',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'isTop',
+      label: '鏄惁缃《',
+      component: 'RadioGroup',
+      componentProps: {
+        options: [
+          { label: '鍚�', value: 0 },
+          { label: '鏄�', value: 1 },
+        ],
+        buttonStyle: 'solid',
+        optionType: 'button',
+      },
+      rules: z.number().default(0),
+    },
+    {
+      fieldName: 'content',
+      label: '鍐呭',
+      component: 'Textarea',
+      rules: 'required',
+      componentProps: {
+        rows: 6,
+        placeholder: '璇疯緭鍏ュ唴瀹�',
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'title',
+      label: '鏍囬',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ユ爣棰�',
+        allowClear: true,
+      },
+    },
+    {
+      fieldName: 'type',
+      label: '绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        placeholder: '璇烽�夋嫨绫诲瀷',
+        allowClear: true,
+        options: NOTICE_TYPE_OPTIONS,
+      },
+    },
+    {
+      fieldName: 'status',
+      label: '鐘舵��',
+      component: 'Select',
+      componentProps: {
+        placeholder: '璇烽�夋嫨鐘舵��',
+        allowClear: true,
+        options: NOTICE_STATUS_OPTIONS,
+      },
+    },
+    {
+      fieldName: 'sendScope',
+      label: '鍙戦�佽寖鍥�',
+      component: 'Select',
+      componentProps: {
+        placeholder: '璇烽�夋嫨鍙戦�佽寖鍥�',
+        allowClear: true,
+        options: SEND_SCOPE_OPTIONS,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+  return [
+    {
+      field: 'id',
+      title: '缂栧彿',
+      minWidth: 80,
+    },
+    {
+      field: 'isTop',
+      title: '缃《',
+      minWidth: 70,
+      align: 'center',
+      slots: { default: 'isTop' },
+    },
+    {
+      field: 'title',
+      title: '鏍囬',
+      minWidth: 220,
+    },
+    {
+      field: 'type',
+      title: '绫诲瀷',
+      minWidth: 80,
+      align: 'center',
+      slots: { default: 'type' },
+    },
+    {
+      field: 'status',
+      title: '鐘舵��',
+      minWidth: 90,
+      align: 'center',
+      slots: { default: 'status' },
+    },
+    {
+      field: 'sendScope',
+      title: '鍙戦�佽寖鍥�',
+      minWidth: 110,
+      align: 'center',
+      slots: { default: 'sendScope' },
+    },
+    {
+      field: 'publishTime',
+      title: '鍙戝竷鏃堕棿',
+      minWidth: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      field: 'createTime',
+      title: '鍒涘缓鏃堕棿',
+      minWidth: 170,
+      formatter: 'formatDateTime',
+    },
+    {
+      title: '鎿嶄綔',
+      width: 200,
+      fixed: 'right',
+      slots: { default: 'actions' },
+    },
+  ];
+}
diff --git a/src/views/bpm/oa/notice/detail.vue b/src/views/bpm/oa/notice/detail.vue
new file mode 100644
index 0000000..1323d41
--- /dev/null
+++ b/src/views/bpm/oa/notice/detail.vue
@@ -0,0 +1,114 @@
+<script lang="ts" setup>
+import type { OaNoticeApi } from '#/api/bpm/oa/notice';
+
+import { computed, onMounted, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { Page } from '@vben/common-ui';
+
+import { Card, Descriptions, message, Spin, Tag } from 'ant-design-vue';
+import { IconifyIcon } from '@vben/icons';
+
+import { getNotice } from '#/api/bpm/oa/notice';
+
+import {
+  NOTICE_STATUS_OPTIONS,
+  NOTICE_STATUS_TAG,
+  NOTICE_TYPE_OPTIONS,
+  NOTICE_TYPE_TAG,
+  SEND_SCOPE_MAP,
+} from './data';
+
+defineOptions({ name: 'OANoticeDetail' });
+
+const route = useRoute();
+const router = useRouter();
+
+const loading = ref(false);
+const formData = ref<OaNoticeApi.NoticeVO>();
+
+function getStatusLabel(status: number) {
+  return NOTICE_STATUS_OPTIONS.find((o) => o.value === status)?.label || '-';
+}
+
+function getTypeLabel(type: number) {
+  return NOTICE_TYPE_OPTIONS.find((o) => o.value === type)?.label || '-';
+}
+
+const noticeId = computed(() => Number(route.query.id));
+
+async function loadDetail() {
+  if (!noticeId.value) {
+    message.error('閫氱煡鍏憡 ID 涓嶅瓨鍦�');
+    router.back();
+    return;
+  }
+  loading.value = true;
+  try {
+    formData.value = await getNotice(noticeId.value);
+  } finally {
+    loading.value = false;
+  }
+}
+
+onMounted(() => {
+  loadDetail();
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <Spin :spinning="loading" tip="鍔犺浇涓�...">
+      <template v-if="formData">
+        <div class="flex items-center gap-2 px-4 pt-3 pb-2">
+          <a-button type="text" size="small" @click="router.back()">
+            <template #icon>
+              <IconifyIcon icon="ant-design:arrow-left-outlined" />
+            </template>
+          </a-button>
+          <span class="text-lg font-bold">{{ formData.title }}</span>
+          <Tag :color="NOTICE_TYPE_TAG[formData.type]">
+            {{ getTypeLabel(formData.type) }}
+          </Tag>
+          <Tag :color="NOTICE_STATUS_TAG[formData.status]">
+            {{ getStatusLabel(formData.status) }}
+          </Tag>
+        </div>
+
+        <div class="px-4 pb-4">
+          <Card size="small" class="mb-3">
+            <Descriptions :column="2" bordered size="small">
+              <Descriptions.Item label="鍙戦�佽寖鍥�">
+                {{ SEND_SCOPE_MAP[formData.sendScope] || '-' }}
+              </Descriptions.Item>
+              <Descriptions.Item label="鏄惁缃《">
+                <Tag v-if="formData.isTop === 1" color="red">缃《</Tag>
+                <span v-else>鍚�</span>
+              </Descriptions.Item>
+              <Descriptions.Item label="鍙戝竷鏃堕棿">
+                {{ formData.publishTime || '-' }}
+              </Descriptions.Item>
+              <Descriptions.Item label="鍒涘缓鏃堕棿">
+                {{ formData.createTime || '-' }}
+              </Descriptions.Item>
+              <Descriptions.Item
+                v-if="formData.sendScope === 2 && formData.userIds?.length"
+                label="鎸囧畾浜哄憳"
+                :span="2"
+              >
+                {{ formData.userIds?.join(', ') }}
+              </Descriptions.Item>
+            </Descriptions>
+          </Card>
+
+          <Card size="small" title="鍐呭">
+            <div
+              class="min-h-40 rounded bg-gray-50 p-4"
+              v-dompurify-html="formData.content"
+            />
+          </Card>
+        </div>
+      </template>
+    </Spin>
+  </Page>
+</template>
diff --git a/src/views/bpm/oa/notice/index.vue b/src/views/bpm/oa/notice/index.vue
new file mode 100644
index 0000000..f80b6a2
--- /dev/null
+++ b/src/views/bpm/oa/notice/index.vue
@@ -0,0 +1,168 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { OaNoticeApi } from '#/api/bpm/oa/notice';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { deleteNotice, getNoticePage } from '#/api/bpm/oa/notice';
+import { $t } from '#/locales';
+import { router } from '#/router';
+
+import {
+  NOTICE_STATUS_TAG,
+  NOTICE_STATUS_OPTIONS,
+  NOTICE_TYPE_TAG,
+  NOTICE_TYPE_OPTIONS,
+  SEND_SCOPE_MAP,
+  useGridColumns,
+  useGridFormSchema,
+} from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+function handleRefresh() {
+  gridApi.query();
+}
+
+function handleCreate() {
+  formModalApi.setData(null).open();
+}
+
+function handleEdit(row: OaNoticeApi.NoticeVO) {
+  formModalApi.setData(row).open();
+}
+
+async function handleDelete(row: OaNoticeApi.NoticeVO) {
+  const hideLoading = message.loading({
+    content: $t('ui.actionMessage.deleting', [row.title]),
+    duration: 0,
+  });
+  try {
+    await deleteNotice(row.id!);
+    message.success($t('ui.actionMessage.deleteSuccess', [row.title]));
+    handleRefresh();
+  } finally {
+    hideLoading();
+  }
+}
+
+function handleDetail(row: OaNoticeApi.NoticeVO) {
+  router.push({
+    name: 'OANoticeDetail',
+    query: { id: row.id },
+  });
+}
+
+function getStatusLabel(status: number) {
+  return NOTICE_STATUS_OPTIONS.find((o) => o.value === status)?.label || '-';
+}
+
+function getTypeLabel(type: number) {
+  return NOTICE_TYPE_OPTIONS.find((o) => o.value === type)?.label || '-';
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          return await getNoticePage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...formValues,
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<OaNoticeApi.NoticeVO>,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <Grid table-title="閫氱煡鍏憡鍒楄〃">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: $t('ui.actionTitle.create', ['閫氱煡鍏憡']),
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['bpm:oa-notice:create'],
+              onClick: handleCreate,
+            },
+          ]"
+        />
+      </template>
+      <template #isTop="{ row }">
+        <Tag v-if="row.isTop === 1" color="red">缃《</Tag>
+      </template>
+      <template #type="{ row }">
+        <Tag :color="NOTICE_TYPE_TAG[row.type]">
+          {{ getTypeLabel(row.type) }}
+        </Tag>
+      </template>
+      <template #status="{ row }">
+        <Tag :color="NOTICE_STATUS_TAG[row.status]">
+          {{ getStatusLabel(row.status) }}
+        </Tag>
+      </template>
+      <template #sendScope="{ row }">
+        {{ SEND_SCOPE_MAP[row.sendScope] || '-' }}
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: $t('common.detail'),
+              type: 'link',
+              icon: ACTION_ICON.VIEW,
+              auth: ['bpm:oa-notice:query'],
+              onClick: handleDetail.bind(null, row),
+            },
+            {
+              label: $t('common.edit'),
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['bpm:oa-notice:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: $t('common.delete'),
+              type: 'link',
+              danger: true,
+              icon: ACTION_ICON.DELETE,
+              auth: ['bpm:oa-notice:delete'],
+              popConfirm: {
+                title: $t('ui.actionMessage.deleteConfirm', [row.title]),
+                confirm: handleDelete.bind(null, row),
+              },
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/bpm/oa/notice/modules/form.vue b/src/views/bpm/oa/notice/modules/form.vue
new file mode 100644
index 0000000..a75d0f7
--- /dev/null
+++ b/src/views/bpm/oa/notice/modules/form.vue
@@ -0,0 +1,108 @@
+<script lang="ts" setup>
+import type { OaNoticeApi } from '#/api/bpm/oa/notice';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createNotice, getNotice, updateNotice } from '#/api/bpm/oa/notice';
+import { $t } from '#/locales';
+import UserSelect from '#/views/system/user/components/select.vue';
+
+import { SEND_SCOPE, useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<OaNoticeApi.NoticeVO>();
+const selectedUserIds = ref<number[]>([]);
+const currentSendScope = ref<number>(SEND_SCOPE.ALL);
+
+const getTitle = computed(() => {
+  return formData.value?.id
+    ? $t('ui.actionTitle.edit', ['閫氱煡鍏憡'])
+    : $t('ui.actionTitle.create', ['閫氱煡鍏憡']);
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    formItemClass: 'col-span-2',
+    labelWidth: 80,
+  },
+  layout: 'horizontal',
+  schema: useFormSchema(),
+  showDefaultActions: false,
+  handleValuesChange: (values) => {
+    currentSendScope.value = (values.sendScope as number) ?? SEND_SCOPE.ALL;
+  },
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    // 鍙戦�佽寖鍥翠负鎸囧畾浜烘椂鏍¢獙
+    if (currentSendScope.value === SEND_SCOPE.SPECIFIC && selectedUserIds.value.length === 0) {
+      message.warning('璇烽�夋嫨鎸囧畾浜哄憳');
+      return;
+    }
+    modalApi.lock();
+    const data = (await formApi.getValues()) as OaNoticeApi.NoticeSaveReqVO;
+    // 鍚堝苟鎸囧畾浜哄憳
+    if (currentSendScope.value === SEND_SCOPE.SPECIFIC) {
+      data.userIds = selectedUserIds.value;
+    }
+    try {
+      await (formData.value?.id ? updateNotice(data) : createNotice(data));
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      selectedUserIds.value = [];
+      currentSendScope.value = SEND_SCOPE.ALL;
+      return;
+    }
+    const data = modalApi.getData<OaNoticeApi.NoticeVO>();
+    if (!data || !data.id) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      formData.value = await getNotice(data.id);
+      await formApi.setValues(formData.value);
+      selectedUserIds.value = formData.value.userIds ?? [];
+    } finally {
+      modalApi.unlock();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-1/2">
+    <Form class="mx-4" />
+    <div
+      v-if="currentSendScope === SEND_SCOPE.SPECIFIC"
+      class="mx-4 mt-4"
+    >
+      <div class="mb-2 text-sm text-gray-700">鎸囧畾浜哄憳</div>
+      <UserSelect
+        v-model:value="selectedUserIds"
+        :multiple="true"
+        placeholder="璇烽�夋嫨鎸囧畾浜哄憳"
+      />
+    </div>
+  </Modal>
+</template>
diff --git a/src/views/srm/tender/detail.vue b/src/views/srm/tender/detail.vue
index e893f65..772d2eb 100644
--- a/src/views/srm/tender/detail.vue
+++ b/src/views/srm/tender/detail.vue
@@ -452,7 +452,7 @@
               </template>
               <div class="flex items-center justify-between mb-3">
                 <span class="text-xs text-gray-400">鍏� <b class="text-gray-600">{{ bidCount }}</b> 涓姇鏍�</span>
-                <a-button type="primary" size="small" @click="bidForm = { tenderProjectId: projectId, supplierId: 0, bidNo: '' }; bidFormVisible = true">
+                <a-button v-if="!isAwarded" type="primary" size="small" @click="bidForm = { tenderProjectId: projectId, supplierId: 0, bidNo: '' }; bidFormVisible = true">
                   <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
                   鏂板鎶曟爣
                 </a-button>
@@ -470,7 +470,7 @@
                   <template v-if="column.key === 'actions'">
                     <a-space>
                       <a-button size="small" type="link" @click="loadQuotes(record.id); activeTab = 'quote'"><IconifyIcon icon="ant-design:dollar-outlined" />鎶ヤ环</a-button>
-                      <a-popconfirm v-if="record.bidStatus === BID_STATUS.REGISTERED" title="纭鎾ゆ爣锛�" @confirm="handleWithdrawBid(record.id)">
+                      <a-popconfirm v-if="!isAwarded && record.bidStatus === BID_STATUS.REGISTERED" title="纭鎾ゆ爣锛�" @confirm="handleWithdrawBid(record.id)">
                         <a-button size="small" type="link" danger><IconifyIcon icon="ant-design:close-circle-outlined" />鎾ゆ爣</a-button>
                       </a-popconfirm>
                     </a-space>
@@ -493,7 +493,7 @@
                   <a-button size="small" type="link" class="ml-auto" @click="activeTab = 'bid'">鍒囨崲鎶曟爣</a-button>
                 </div>
                 <div class="text-right mb-3">
-                  <a-button v-if="selectedBid.bidStatus !== BID_STATUS.WITHDRAWN" type="primary" size="small" @click="handleAddQuote">
+                  <a-button v-if="!isAwarded && selectedBid.bidStatus !== BID_STATUS.WITHDRAWN" type="primary" size="small" @click="handleAddQuote">
                     <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
                     鏂板鎶ヤ环
                   </a-button>
@@ -513,7 +513,7 @@
                       {{ record.warrantyPeriod != null ? `${record.warrantyPeriod} 涓湀` : '-' }}
                     </template>
                     <template v-if="column.key === 'actions'">
-                      <a-button v-if="selectedBid?.bidStatus !== BID_STATUS.WITHDRAWN" size="small" type="link" @click="handleEditQuote(record)"><IconifyIcon icon="ant-design:edit-outlined" />缂栬緫</a-button>
+                      <a-button v-if="!isAwarded && selectedBid?.bidStatus !== BID_STATUS.WITHDRAWN" size="small" type="link" @click="handleEditQuote(record)"><IconifyIcon icon="ant-design:edit-outlined" />缂栬緫</a-button>
                     </template>
                   </template>
                 </a-table>
@@ -543,7 +543,7 @@
                 <div class="text-center py-8">
                   <a-result status="info" title="灏氭湭寮�鏍�" sub-title="鎵ц寮�鏍囧悗椤圭洰杩涘叆鎶曟爣闃舵锛屼緵搴斿晢鍙寮忔姇鏍�" />
                   <br />
-                  <a-button type="primary" @click="handleCreateBidOpen">
+                  <a-button v-if="!isAwarded" type="primary" @click="handleCreateBidOpen">
                     <template #icon><IconifyIcon icon="ant-design:thunderbolt-outlined" /></template>
                     鎵ц寮�鏍�
                   </a-button>
@@ -558,7 +558,7 @@
               </template>
               <div class="flex items-center justify-between mb-3">
                 <span class="text-xs text-gray-400">鍏� <b class="text-gray-600">{{ evalCount }}</b> 鏉★紝宸叉帓鍚� <b class="text-green-500">{{ rankedCount }}</b> 鏉�</span>
-                <a-space>
+                <a-space v-if="!isAwarded">
                   <a-button size="small" @click="handleCalculateRanking">
                     <template #icon><IconifyIcon icon="ant-design:bar-chart-outlined" /></template>
                     璁$畻鎺掑悕
@@ -590,7 +590,7 @@
                     <span v-else class="text-gray-300">-</span>
                   </template>
                   <template v-if="column.key === 'actions'">
-                    <a-button size="small" type="link" @click="evalForm = { ...record }; evalFormVisible = true"><IconifyIcon icon="ant-design:edit-outlined" />缂栬緫</a-button>
+                    <a-button v-if="!isAwarded" size="small" type="link" @click="evalForm = { ...record }; evalFormVisible = true"><IconifyIcon icon="ant-design:edit-outlined" />缂栬緫</a-button>
                   </template>
                 </template>
               </a-table>
@@ -631,7 +631,7 @@
                             <span class="text-sm">閲囪喘璁㈠崟 ID: {{ award.purchaseOrderId }}</span>
                           </template>
                         </a-result>
-                        <a-popconfirm title="纭畾鍒犻櫎璇ュ畾鏍囪褰曪紵" ok-text="纭畾" cancel-text="鍙栨秷" @confirm="handleDeleteAward">
+                        <a-popconfirm v-if="!isAwarded" title="纭畾鍒犻櫎璇ュ畾鏍囪褰曪紵" ok-text="纭畾" cancel-text="鍙栨秷" @confirm="handleDeleteAward">
                           <a-button type="link" danger size="small" class="mt-2">鍒犻櫎瀹氭爣</a-button>
                         </a-popconfirm>
                       </div>
@@ -639,11 +639,11 @@
                     <template v-else-if="award.awardStatus === AWARD_STATUS.AWARDED">
                       <div class="text-center">
                         <a-result status="success" title="瀹氭爣瀹屾垚" class="p-0 mb-3" />
-                        <a-button type="primary" block @click="handleGeneratePurchaseOrder">
+                        <a-button v-if="!isAwarded" type="primary" block @click="handleGeneratePurchaseOrder">
                           <template #icon><IconifyIcon icon="ant-design:shopping-cart-outlined" /></template>
                           鐢熸垚閲囪喘璁㈠崟
                         </a-button>
-                        <a-popconfirm title="纭畾鍒犻櫎璇ュ畾鏍囪褰曪紵" ok-text="纭畾" cancel-text="鍙栨秷" @confirm="handleDeleteAward">
+                        <a-popconfirm v-if="!isAwarded" title="纭畾鍒犻櫎璇ュ畾鏍囪褰曪紵" ok-text="纭畾" cancel-text="鍙栨秷" @confirm="handleDeleteAward">
                           <a-button type="link" danger size="small" class="mt-2">鍒犻櫎瀹氭爣</a-button>
                         </a-popconfirm>
                       </div>
@@ -651,11 +651,11 @@
                     <template v-else>
                       <div class="text-center">
                         <div class="text-gray-500 text-sm mb-3">瀹氭爣淇℃伅宸插垱寤猴紝璇峰鎵�</div>
-                        <a-button type="primary" block @click="handleApproveAward" style="background: #52c41a; border-color: #52c41a;">
+                        <a-button v-if="!isAwarded" type="primary" block @click="handleApproveAward" style="background: #52c41a; border-color: #52c41a;">
                           <template #icon><IconifyIcon icon="ant-design:check-outlined" /></template>
                           瀹℃壒閫氳繃
                         </a-button>
-                        <a-popconfirm title="纭畾鍒犻櫎璇ュ畾鏍囪褰曪紵" ok-text="纭畾" cancel-text="鍙栨秷" @confirm="handleDeleteAward">
+                        <a-popconfirm v-if="!isAwarded" title="纭畾鍒犻櫎璇ュ畾鏍囪褰曪紵" ok-text="纭畾" cancel-text="鍙栨秷" @confirm="handleDeleteAward">
                           <a-button type="link" danger size="small" class="mt-2">鍒犻櫎瀹氭爣</a-button>
                         </a-popconfirm>
                       </div>
@@ -667,7 +667,7 @@
                 <div class="text-center py-8">
                   <a-result status="info" title="灏氭湭瀹氭爣" sub-title="鍩轰簬璇勬爣鎺掑悕閫夋嫨涓爣渚涘簲鍟嗗苟鍒涘缓瀹氭爣" />
                   <br />
-                  <a-button type="primary" @click="awardForm = { tenderProjectId: projectId, supplierId: 0, bidId: 0, awardNo: '' }; awardFormVisible = true">
+                  <a-button v-if="!isAwarded" type="primary" @click="awardForm = { tenderProjectId: projectId, supplierId: 0, bidId: 0, awardNo: '' }; awardFormVisible = true">
                     <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
                     鍒涘缓瀹氭爣
                   </a-button>
diff --git a/src/views/system/notify/message/data.ts b/src/views/system/notify/message/data.ts
index c6a3152..f09553f 100644
--- a/src/views/system/notify/message/data.ts
+++ b/src/views/system/notify/message/data.ts
@@ -197,9 +197,20 @@
       label: '妯$増鍙傛暟',
       render: (val) => {
         try {
-          return JSON.stringify(val);
+          const params = typeof val === 'string' ? JSON.parse(val) : val;
+          if (!params || typeof params !== 'object') return '-';
+          return h(
+            'div',
+            { class: 'space-y-1' },
+            Object.entries(params).map(([key, value]) =>
+              h('div', { class: 'flex gap-2' }, [
+                h('span', { class: 'font-medium text-gray-600' }, `${key}:`),
+                h('span', { class: 'text-gray-900' }, String(value)),
+              ]),
+            ),
+          );
         } catch {
-          return '';
+          return String(val);
         }
       },
     },
diff --git a/src/views/system/notify/my/data.ts b/src/views/system/notify/my/data.ts
index b31d437..a2789cb 100644
--- a/src/views/system/notify/my/data.ts
+++ b/src/views/system/notify/my/data.ts
@@ -131,7 +131,19 @@
     },
     {
       field: 'templateContent',
+      label: '涓婚',
+    },
+    {
+      field: 'templateParams',
       label: '娑堟伅鍐呭',
+      render: (val) => {
+        try {
+          const params = typeof val === 'string' ? JSON.parse(val) : val;
+          return params?.content || '-';
+        } catch {
+          return String(val);
+        }
+      },
     },
   ];
 }

--
Gitblit v1.9.3