银川
1.API密钥管理页面开发联调
2.模型管理页面开发联调
3.知识库管理页面开发联调
4.通知公告页面开发联调
| | |
| | | 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; |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | /** æ¥è¯¢ç¥è¯åºææ¡£è¯¦æ
*/ |
| | | 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}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢ç¥è¯åºææ¡£ï¼åä¸ªï¼ */ |
| | | export function createKnowledge(data: any) { |
| | | export function createKnowledge(data: AiKnowledgeDocumentApi.CreateDocumentParams) { |
| | | return requestClient.post('/ai/knowledge/document/create', data); |
| | | } |
| | | |
| | | /** æ°å¢ç¥è¯åºææ¡£ï¼å¤ä¸ªï¼ */ |
| | | 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); |
| | | } |
| | | |
| | | /** ä¿®æ¹ç¥è¯åºææ¡£ç¶æ */ |
| | | export function updateKnowledgeDocumentStatus(data: any) { |
| | | export function updateKnowledgeDocumentStatus( |
| | | data: AiKnowledgeDocumentApi.UpdateDocumentStatusParams, |
| | | ) { |
| | | return requestClient.put('/ai/knowledge/document/update-status', data); |
| | | } |
| | | |
| | |
| | | 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(',') } }, |
| | | ); |
| | | } |
| | |
| | | retrievalCount: number; // å¬åæ¬¡æ° |
| | | status: number; // ææ¡£ç¶æ |
| | | createTime: number; // å建æ¶é´ |
| | | score?: number; // ç¸ä¼¼åº¦åæ°ï¼0~1ï¼ï¼ä»
æç´¢æ¥å£è¿å |
| | | 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; |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | /** åçå
容 */ |
| | | 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, |
| | | }); |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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`); |
| | | } |
| | |
| | | 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[] { |
| | |
| | | import { |
| | | deleteKnowledgeDocument, |
| | | getKnowledgeDocumentPage, |
| | | processDocument, |
| | | updateKnowledgeDocumentStatus, |
| | | } from '#/api/ai/knowledge/document'; |
| | | import { $t } from '#/locales'; |
| | |
| | | name: 'AiKnowledgeSegment', |
| | | query: { documentId: id }, |
| | | }); |
| | | } |
| | | |
| | | /** å¤çææ¡£ï¼å段+åéåï¼ */ |
| | | async function handleProcess(row: AiKnowledgeDocumentApi.KnowledgeDocument) { |
| | | const hideLoading = message.loading({ |
| | | content: `æ£å¨å¤çææ¡£ã${row.name}ã...`, |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await processDocument(row.id!); |
| | | message.success(`ææ¡£ã${row.name}ãå¤çæå`); |
| | | handleRefresh(); |
| | | } catch { |
| | | message.error(`ææ¡£ã${row.name}ãå¤ç失败`); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æ´æ°ææ¡£ç¶æ */ |
| | |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <div class="mb-2"> |
| | | <a-button type="default" @click="router.back()"> |
| | | è¿åä¸ä¸é¡µ |
| | | </a-button> |
| | | </div> |
| | | <Grid table-title="ç¥è¯åºææ¡£å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | 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, |
| | |
| | | <script setup lang="ts"> |
| | | import type { AiKnowledgeSegmentApi } from '#/api/ai/knowledge/segment'; |
| | | |
| | | import { onMounted, reactive, ref } from 'vue'; |
| | | import { useRoute, useRouter } from 'vue-router'; |
| | | |
| | |
| | | import { getKnowledge } from '#/api/ai/knowledge/knowledge'; |
| | | import { searchKnowledgeSegment } from '#/api/ai/knowledge/segment'; |
| | | |
| | | type RetrievalSegment = AiKnowledgeSegmentApi.KnowledgeSegment & { |
| | | expanded?: boolean; |
| | | }; |
| | | |
| | | /** ç¥è¯åºææ¡£å¬åæµè¯ */ |
| | | 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, |
| | |
| | | message.warning('请è¾å
¥æ¥è¯¢ææ¬'); |
| | | return; |
| | | } |
| | | if (queryParams.id == null) return; |
| | | |
| | | loading.value = true; |
| | | segments.value = []; |
| | |
| | | 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'; |
| | | } |
| | | |
| | | /** è·åç¥è¯åºé
ç½®ä¿¡æ¯ */ |
| | |
| | | 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"> |
| | |
| | | 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"> |
| | |
| | | {{ 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 |
| | |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'vectorId', |
| | | title: 'åéID', |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | field: 'retrievalCount', |
| | | title: 'å¬å次æ°', |
| | | minWidth: 100, |
| | |
| | | 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'; |
| | |
| | | import Form from './modules/form.vue'; |
| | | |
| | | const route = useRoute(); |
| | | const router = useRouter(); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | |
| | | <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 |
| | |
| | | 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[] { |
| | |
| | | }, |
| | | { |
| | | 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', |
| | | }, |
| | |
| | | minWidth: 180, |
| | | }, |
| | | { |
| | | title: 'API ç§é¥', |
| | | title: 'API ç§é¥ ID', |
| | | field: 'keyId', |
| | | formatter: ({ cellValue }) => { |
| | | return ( |
| | | apiKeyList.find((apiKey) => apiKey.id === cellValue)?.name || '-' |
| | | ); |
| | | }, |
| | | minWidth: 140, |
| | | }, |
| | | { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | </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> |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | {{ 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> |
| | |
| | | <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> |
| | |
| | | </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> |
| | | è®¡ç®æå |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | 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); |
| | | } |
| | | }, |
| | | }, |
| | |
| | | }, |
| | | { |
| | | 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); |
| | | } |
| | | }, |
| | | }, |
| | | ]; |
| | | } |