From 2c55b1a8a0700df79268550335506637b41610ce Mon Sep 17 00:00:00 2001 From: seatonwan9 Date: 星期日, 24 八月 2025 20:36:36 +0800 Subject: [PATCH] 提交源码 --- src/views/tradeManage/seller/index.vue | 426 +-- src/views/approveManage/tradeApproval/checkFiles.vue | 1168 +++++++++ src/views/approveManage/tradeApproval/list.vue | 421 +-- src/views/tradeManage/detail/index.vue | 632 ++++- src/api/pointsApi.ts | 2 src/api/approvalManage.ts | 22 src/views/tradeManage/upload/index.vue | 871 ++++++- src/views/tradeManage/buyer/index.vue | 487 ++-- src/views/tradeManage/evaluate/index.vue | 935 ++++++- src/views/approveManage/tradeApproval/approve.vue | 1139 +++++++++ src/router/modules/approvalManage.ts | 9 src/api/orderApi.ts | 114 src/views/tradeManage/confirm/index.vue | 585 +++- src/App.vue | 7 14 files changed, 5,368 insertions(+), 1,450 deletions(-) diff --git a/src/App.vue b/src/App.vue index 619c861..d96e197 100644 --- a/src/App.vue +++ b/src/App.vue @@ -29,6 +29,13 @@ }) } provide('reload', reload) +// 涓存椂鍒濆鍖栵細鍐欏叆 userId 涓� unitId锛屼究浜庢湰鍦拌仈璋冨悗绔笅鍗曟帴鍙� +if (!userStore.getUserId) { + userStore.userId = '1' +} +if (!userStore.getUnitId) { + userStore.unitId = '1' +} setInterval(() => { // 杩欎釜鍒ゆ柇浠h〃鐧诲綍鎴愬姛鍚庢墠寮�濮嬪啓鍏ユ椂闂� if(userStore.getAdminToken) { diff --git a/src/api/approvalManage.ts b/src/api/approvalManage.ts index 8ad16a5..3a6b20d 100644 --- a/src/api/approvalManage.ts +++ b/src/api/approvalManage.ts @@ -1,8 +1,9 @@ import createAxios from '@/utils/axios' -let url = '/admin/approval/' -// 瀹℃壒鍒楄〃 +// 缁熶竴璧扮綉鍏冲墠缂� /admin/api +const url = '/admin/api/order' +// 瀹℃壒鍒楄〃锛堝緟瀹℃壒璁㈠崟鍒嗛〉锛� export const fetchApprovalPage = (data: any): ApiPromise => - createAxios({ url: `${url}/trade/page`, method: 'post', data }) as ApiPromise + createAxios({ url: `${url}/approval/page`, method: 'post', data }) as ApiPromise // 瀹℃壒璇︽儏 export const fetchApprovalDetail = (data: any): ApiPromise => @@ -16,4 +17,19 @@ export const checkFiles = (data: any): ApiPromise => createAxios({ url: `${url}/trade/checkFiles`, method: 'post', data }) as ApiPromise +// 瀹℃壒閫氳繃锛氬皢璁㈠崟鐘舵�佹洿鏂颁负"寰呰瘎浠�" +export const approveOrder = (data: any): ApiPromise => + createAxios({ url: `${url}/trade/approve`, method: 'post', data }) as ApiPromise + +// 鏍规嵁璁㈠崟ID鑾峰彇瀹℃壒璁板綍锛堝惈瀹℃壒ID锛� +export const fetchApprovalByOrderId = (orderId: string | number): ApiPromise => + createAxios({ url: `/admin/api/approval/order/${orderId}`, method: 'get' }) as ApiPromise + +// 鎺堟潈瀹℃壒 +export const authorizeApproval = ( + approvalId: string | number, + params: { authorizerId: number; authorizerName: string; authorizationOpinion?: string } +): ApiPromise => + createAxios({ url: `/admin/api/approval/authorize/${approvalId}`, method: 'post', params }) as ApiPromise + diff --git a/src/api/orderApi.ts b/src/api/orderApi.ts index 835ef2b..aad0872 100644 --- a/src/api/orderApi.ts +++ b/src/api/orderApi.ts @@ -1,12 +1,13 @@ import createAxios from '@/utils/axios' -const baseUrl = '/admin/api/order' +// 涓庡叾浠栨ā鍧椾繚鎸佷竴鑷达紝缁忚繃缃戝叧浠� /admin/api 涓哄墠缂� +const url = '/admin/api/order' const orderApi = { // 鑾峰彇涓�娆℃�ч槻閲嶅鎻愪氦 Token getIdempotencyToken(userId?: number): ApiPromise { return createAxios({ - url: `${baseUrl}/idempotency/token`, + url: `${url}/idempotency/token`, method: 'get', params: userId ? { userId } : {} }) as ApiPromise @@ -15,11 +16,118 @@ // 鍒涘缓璁㈠崟锛堝湪 headers 涓惡甯� Idempotency-Token锛� createOrder(data: any, token: string): ApiPromise { return createAxios({ - url: `${baseUrl}/create`, + url: `${url}/create`, method: 'post', data, headers: { 'Idempotency-Token': token } }) as ApiPromise + }, + getBuyerOrderPage(data: any): ApiPromise { + return createAxios({ + url: `${url}/buyer/page`, + method: 'post', + data + }) as ApiPromise + }, + getSellerOrderPage(data: any): ApiPromise { + return createAxios({ + url: `${url}/seller/page`, + method: 'post', + data + }) as ApiPromise + }, + getOrderDetail(orderId: string): ApiPromise { + return createAxios({ + url: `${url}/detail/${orderId}`, + method: 'get' + }) as ApiPromise + }, + generateOrderNo(): ApiPromise { + return createAxios({ + url: `${url}/no/new`, + method: 'get' + }) as ApiPromise + }, + + updateOrderDetail(data: any): ApiPromise { + return createAxios({ + url: `${url}/detail/update`, + method: 'post', + data + }) as ApiPromise + }, + + updateOrderDetailRemarksOnly(data: any): ApiPromise { + return createAxios({ + url: `${url}/detail/remarks/update`, + method: 'post', + data + }) as ApiPromise + }, + + confirmTransaction(orderId: string, userId: number): ApiPromise { + return createAxios({ + url: `${url}/transaction/confirm`, + method: 'post', + params: { + orderId, + userId + } + }) as ApiPromise + }, + + submitEvaluation(data: any): ApiPromise { + return createAxios({ + url: `${url}/evaluation/add`, + method: 'post', + params: { + orderId: data.orderId, + evaluatorId: data.evaluatorId, + evaluatorName: data.evaluatorName, + evaluatorType: '涔板', // 榛樿璇勪环浜虹被鍨嬩负涔板 + content: data.evaluationContent, + rating: data.overallRating, + serviceRating: data.serviceRating, + qualityRating: data.qualityRating, + deliveryRating: data.speedRating, // 閫熷害璇勫垎瀵瑰簲鍚庣鐨勪氦浠樿瘎鍒� + isAnonymous: data.isAnonymous + } + }) as ApiPromise + }, + + // 鍙栨秷璁㈠崟 + cancelOrder(orderId: string): ApiPromise { + return createAxios({ + url: `${url}/cancel/${orderId}`, + method: 'delete' + }) as ApiPromise + }, + + checkAgreementPriceType(orderId: string): ApiPromise { + return createAxios({ + url: `${url}/agreement/check/${orderId}`, + method: 'get' + }) as ApiPromise + }, + + updateOrderStatusToNext(orderId: string): ApiPromise { + return createAxios({ + url: `${url}/status/next`, + method: 'post', + params: { + orderId + } + }) as ApiPromise + }, + + updateOrderStatusToPrevious(orderId: string): ApiPromise { + return createAxios({ + url: `${url}/status/previous`, + method: 'post', + params: { + orderId + } + }) as ApiPromise } } diff --git a/src/api/pointsApi.ts b/src/api/pointsApi.ts index 46d7136..6d8ec43 100644 --- a/src/api/pointsApi.ts +++ b/src/api/pointsApi.ts @@ -27,7 +27,7 @@ // 淇濆瓨绉垎瑙勫垯閰嶇疆 savePointsRules(data: object): ApiPromise { return createAxios({ - url: `${url}rules/save`, + url: `${url}rules/update`, data: data, }) as ApiPromise }, diff --git a/src/router/modules/approvalManage.ts b/src/router/modules/approvalManage.ts index 25165c7..752b697 100644 --- a/src/router/modules/approvalManage.ts +++ b/src/router/modules/approvalManage.ts @@ -29,6 +29,15 @@ keepAlive: false, }, }, + { + path: 'authorization/:id', + name: 'tradeAuthorization', + component: () => import('@/views/approveManage/tradeApproval/approve.vue'), + meta: { + title: '浜ゆ槗鎺堟潈', + keepAlive: false, + }, + }, ], meta: { title: '瀹℃壒绠$悊', diff --git a/src/views/approveManage/tradeApproval/approve.vue b/src/views/approveManage/tradeApproval/approve.vue index b9fa7c0..10e67a7 100644 --- a/src/views/approveManage/tradeApproval/approve.vue +++ b/src/views/approveManage/tradeApproval/approve.vue @@ -1,93 +1,1134 @@ <template> <div class="default-main"> + <!-- 璁㈠崟淇℃伅 + 鐢宠浜轰俊鎭� + 浜ゆ槗鍐呭锛堝悎骞朵负鍚屼竴鍗$墖锛� --> <el-card shadow="never"> - <div class="title">璁㈠崟淇℃伅</div> - <el-descriptions :column="3" border class="mt10"> + <el-descriptions + :column="2" + border + class="mt10 order-desc fixed-label" + label-width="180px" + :label-style="labelStyle" + :content-style="contentStyle" + > + <el-descriptions-item :span="2" class="section-header"> + <template #label> + <el-icon class="section-icon"><Document /></el-icon> + <span>璁㈠崟淇℃伅</span> + </template> + <template #default></template> + </el-descriptions-item> <el-descriptions-item label="璁㈠崟缂栧彿">{{ detail.orderNo }}</el-descriptions-item> <el-descriptions-item label="浜ゆ槗璧勬簮绫诲瀷">{{ detail.resourceTypeName }}</el-descriptions-item> - <el-descriptions-item label="浜ゆ槗鐘舵��">{{ detail.statusName }}</el-descriptions-item> <el-descriptions-item label="鐢宠鏃堕棿">{{ detail.applyTime }}</el-descriptions-item> - <el-descriptions-item label="鍗曚綅">{{ detail.unitName }}</el-descriptions-item> - <el-descriptions-item label="鐢ㄦ埛鍚�">{{ detail.userName }}</el-descriptions-item> + <el-descriptions-item label="浜ゆ槗鐘舵��"> + <el-tag :type="getStatusType(detail.status)" size="small">{{ detail.statusName }}</el-tag> + </el-descriptions-item> </el-descriptions> - </el-card> + + <!-- 鐢宠浜轰俊鎭紙涓庤鍗曚俊鎭悓鍗$墖锛屽鐢ㄥ垎闅旀爣棰樻牱寮忥級 --> + <el-descriptions + :column="2" + border + class="mt15 order-desc fixed-label" + label-width="180px" + :label-style="labelStyle" + :content-style="contentStyle" + > + <el-descriptions-item :span="2" class="section-header"> + <template #label> + <el-icon class="section-icon"><User /></el-icon> + <span>鐢宠浜轰俊鎭�</span> + </template> + <template #default></template> + </el-descriptions-item> + <el-descriptions-item label="濮撳悕">{{ detail.userName || '-' }}</el-descriptions-item> + <el-descriptions-item label="鍗曚綅">{{ detail.unitName || '-' }}</el-descriptions-item> + <el-descriptions-item label="閮ㄩ棬">{{ detail.userDept || '-' }}</el-descriptions-item> + <el-descriptions-item label="鐢ㄦ埛鍚�">{{ detail.userAccount || '-' }}</el-descriptions-item> + </el-descriptions> - <el-card class="mt15" shadow="never"> - <div class="title">浜ゆ槗鍐呭</div> - <el-table :data="detail.items" border class="mt10"> - <el-table-column label="璇︽儏" min-width="280"> - <template #default="{ row }"> - <div> - <div>{{ row.name }}</div> - <div class="gray">瀹㈡埛瀵硅薄锛歿{ row.customerTarget }}</div> - <div class="gray">骞跺彂鑺傜偣鏁帮細{{ row.concurrentNodes }}</div> - </div> + <!-- 浜ゆ槗鍐呭锛堢揣闅忕敵璇蜂汉淇℃伅锛屽悓鍗$墖锛屽鐢ㄥ垎闅旀爣棰樻牱寮忥級 --> + <el-descriptions + :column="2" + border + class="mt15 order-desc fixed-label" + label-width="180px" + :label-style="labelStyle" + :content-style="contentStyle" + > + <el-descriptions-item :span="2" class="section-header"> + <template #label> + <el-icon class="section-icon"><Goods /></el-icon> + <span>浜ゆ槗鍐呭</span> + </template> + <template #default></template> + </el-descriptions-item> + <el-descriptions-item label="浜у搧鍚嶇О"> + <el-link type="primary" :underline="false">{{ detail.productName }}</el-link> + </el-descriptions-item> + <el-descriptions-item label="鎻愪緵鑰�">{{ detail.supplier }}</el-descriptions-item> + <el-descriptions-item label="琛屼笟棰嗗煙">{{ detail.industry }}</el-descriptions-item> + <el-descriptions-item label="鍗曚綅宸ョ▼">{{ detail.projectUnit }}</el-descriptions-item> + <el-descriptions-item label="浜у搧绫诲瀷">{{ detail.productType || '-' }}</el-descriptions-item> + <el-descriptions-item label="浜у搧绠�浠�"> + <div class="desc-wrap">{{ detail.productDesc }}</div> + </el-descriptions-item> + </el-descriptions> + + <!-- 璁㈠崟璇︽儏锛堢Щ鍔ㄥ埌浜ゆ槗鍐呭涓嬮潰锛屽悓涓�鍗$墖鍐咃級 --> + <div ref="orderTableWrapRef"> + <el-table + :data="tableData" + border + class="mt10 order-table" + :header-cell-style="headerCenterStyle" + :cell-style="bodyCellStyle" + :row-class-name="getRowClassName" + :span-method="arraySpanMethod" + > + <el-table-column> + <template #header> + <el-icon class="header-icon"><List /></el-icon> + <span>璇︽儏</span> + </template> + <el-table-column label="" :width="colWidths.detail1"> + <template #default="{ row }"> + <div v-if="!row.isSummary">{{ row.name }}</div> + <div v-else class="summary-merged"> + <div class="summary-left"> + 鍏� <span class="count">{{ detail.items.length }}</span> 浠� + </div> + <div class="summary-right"> + 鎬昏锛�<span class="price">{{ detail.pointTotal }}</span> 绉垎 + <span class="ml20 price">{{ detail.cashTotal }}</span> 鍏� + </div> + </div> + </template> + </el-table-column> + <el-table-column label="" :width="colWidths.detail2"> + <template #default="{ row }"> + <div v-if="!row.isSummary" class="gray">閿�鍞舰寮忥細{{ row.saleType || '-' }}</div> + <div v-if="!row.isSummary" class="gray">璐︽埛鏁伴噺锛歿{ row.accountCount ?? '-' }}</div> + </template> + </el-table-column> + <el-table-column label="" :width="colWidths.detail3"> + <template #default="{ row }"> + <div v-if="!row.isSummary" class="gray">瀹㈡埛瀵硅薄锛歿{ row.customerTarget || '-' }}</div> + <div v-if="!row.isSummary" class="gray">骞跺彂鑺傜偣鏁伴噺锛歿{ row.concurrentNodes ?? '-' }}</div> + </template> + </el-table-column> + </el-table-column> + <el-table-column label="鍗曚环"> + <el-table-column label="" :width="colWidths.price"> + <template #default="{ row }"> + <div v-if="!row.isSummary">{{ formatPrice(row) }}</div> + </template> + </el-table-column> + </el-table-column> + <el-table-column label="鏁伴噺"> + <el-table-column label="" :width="colWidths.quantity"> + <template #default="{ row }"> + <div v-if="!row.isSummary">{{ row.quantity }}</div> + </template> + </el-table-column> + </el-table-column> + <el-table-column label="鏈熼檺(骞�)"> + <el-table-column label="" :width="colWidths.period"> + <template #default="{ row }"> + <div v-if="!row.isSummary">{{ formatPeriod(row) }}</div> + </template> + </el-table-column> + </el-table-column> + </el-table> + </div> + + <!-- 绉婚櫎鍘熸潵鐨勮〃鏍煎簳閮ㄤ俊鎭紝鍥犱负宸茬粡绉诲埌琛ㄦ牸鏈�鍚庝竴琛� --> + + <!-- 浜ゆ槗鏂囦欢锛堢Щ鍔ㄥ埌璁㈠崟璇︽儏涓嬮潰锛屽悓涓�鍗$墖鍐咃級 --> + <div class="file-section" v-if="isAgreementOrder && fileList.length > 0"> + <el-table + :data="fileList" + border + class="file-table" + :header-cell-style="fileTableHeaderStyle" + :cell-style="fileTableCellStyle" + > + <el-table-column min-width="200"> + <template #header> + <el-icon class="header-icon"><Document /></el-icon> + <span>浜ゆ槗鏂囦欢</span> + </template> + <template #default="{ row }"> + <div class="file-name"> + <el-icon class="file-icon"><Document /></el-icon> + <span>{{ row.name }}</span> + </div> + </template> + </el-table-column> + <el-table-column label="鏂囦欢澶у皬" prop="size" width="120"> + <template #default="{ row }"> + {{ formatFileSize(row.size) }} + </template> + </el-table-column> + <el-table-column label="鎿嶄綔" width="180"> + <template #default="{ row }"> + <div class="file-actions"> + <el-button + type="text" + size="small" + class="preview-btn" + @click="handlePreview(row)" + v-if="isPreviewable(row)" + :disabled="!isFileUploaded(row)" + > + 棰勮 + </el-button> + <el-button + type="text" + size="small" + class="download-btn" + @click="handleDownload(row)" + :disabled="!isFileUploaded(row)" + > + 涓嬭浇 + </el-button> + </div> + </template> + </el-table-column> + </el-table> + </div> + </el-card> + + <!-- 浜ゆ槗淇℃伅澶囨敞 --> + <el-card class="mt15" shadow="never"> + <div class="title">浜ゆ槗淇℃伅澶囨敞</div> + <el-table :data="form.items" border class="mt10 remark-table"> + <el-table-column label="璇︽儏" prop="name" min-width="200" /> + <el-table-column label="澶囨敞" min-width="400"> + <template #default="{ row, $index }"> + <el-input + v-model="form.items[$index].remark" + type="textarea" + :rows="1" + placeholder="寮�閫氳处鍙蜂俊鎭強涓暟锛岀櫥褰曟彁绀虹瓑淇℃伅" + maxlength="500" + show-word-limit + class="remark-input" + /> </template> </el-table-column> - <el-table-column label="鍗曚环" prop="priceName" width="140" /> - <el-table-column label="鏁伴噺" prop="quantity" width="80" /> - <el-table-column label="鏈熼檺(骞�)" prop="period" width="120" /> - </el-table> - - <div class="total"> - 鎬昏锛�<span class="price">{{ detail.pointTotal }}</span> 绉垎 - <span class="ml20 price">{{ detail.cashTotal }}</span> 鍏� - </div> + </el-table> </el-card> + <!-- 瀹℃壒鍐呭 --> <el-card class="mt15" shadow="never"> - <div class="title">瀹℃壒</div> - <el-form :model="form" label-width="100px" class="mt10" ref="formRef" :rules="rules"> - <el-form-item label="瀹℃壒鎰忚" prop="remark"> - <el-input v-model="form.remark" type="textarea" :autosize="{ minRows: 4 }" placeholder="璇疯緭鍏ュ鎵规剰瑙�" /> - </el-form-item> - </el-form> - <div class="ba-center mt15"> - <el-button @click="goBack">杩斿洖</el-button> - <el-button type="success" @click="approve(true)">瀹℃壒閫氳繃</el-button> - <el-button type="danger" @click="approve(false)">椹冲洖</el-button> + <div class="title">瀹℃壒鍐呭</div> + <div class="approval-content"> + <div class="approval-form"> + <div class="form-item"> + <label class="required">瀹℃壒鎰忚:</label> + <el-input + v-model="approvalForm.comments" + type="textarea" + :rows="4" + placeholder="璇疯緭鍏ュ鎵规剰瑙�" + maxlength="500" + show-word-limit + /> + </div> + </div> + <div class="approval-actions"> + <el-button @click="goBack">杩斿洖</el-button> + <el-button + type="primary" + @click="handleApprove" + :loading="approvalLoading" + > + {{ isAgreementOrder ? '瀹℃壒閫氳繃' : '瀹屾垚鎺堟潈' }} + </el-button> + <el-button + v-if="isAgreementOrder" + type="danger" + @click="handleReject" + :loading="approvalLoading" + > + 椹冲洖 + </el-button> + </div> </div> </el-card> </div> </template> <script setup lang="ts"> -import { onMounted, reactive, ref } from 'vue' +import { onMounted, reactive, ref, computed, type CSSProperties } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { fetchApprovalDetail, submitApproval } from '@/api/approvalManage' -import { ElMessage, FormInstance } from 'element-plus' +import { Document, User, Goods, List } from '@element-plus/icons-vue' +import { ElMessage, ElMessageBox } from 'element-plus' +import orderApi from '@/api/orderApi' +import { approveOrder } from '@/api/approvalManage' +import { useUserInfo } from '@/stores/modules/userInfo' +import createAxios from '@/utils/axios' const route = useRoute() const router = useRouter() -const formRef = ref<FormInstance>() +const userStore = useUserInfo() const detail = reactive<any>({ items: [] }) -const form = reactive({ remark: '' }) -const rules = { remark: [{ required: true, message: '璇疯緭鍏ュ鎵规剰瑙�', trigger: 'blur' }] } +const form = reactive<any>({ items: [] }) +const fileList = ref<any[]>([]) +const orderTableWrapRef = ref<HTMLElement | null>(null) +const labelStyle = { width: '180px', maxWidth: '180px' } +const contentStyle = { width: 'calc(50% - 180px)' } + +// 鏄惁涓哄崗璁鍗� +const isAgreementOrder = ref(false) + +// 瀹℃壒琛ㄥ崟鏁版嵁 +const approvalForm = reactive({ + comments: '' +}) + +// 瀹℃壒loading鐘舵�� +const approvalLoading = ref(false) + +// 璁$畻琛ㄦ牸鏁版嵁锛屾坊鍔犳眹鎬昏 +const tableData = computed(() => { + const summaryRow = { + id: 'summary', + isSummary: true, + name: '', + saleType: '', + accountCount: 0, + customerTarget: '', + concurrentNodes: 0, + pricePoint: 0, + priceCash: 0, + quantity: 0, + period: 0, + } + return [...detail.items, summaryRow] +}) + +// 鐘舵�佹槧灏勶紙鍚庣涓枃 -> 鍓嶇鏋氫妇锛� +const statusServerToUi: Record<string, string> = { + '寰呬笂浼犳枃浠�': 'WAIT_UPLOAD', + '寰呮巿鏉�': 'WAIT_AUTHORIZE', + '寰呬氦鏄撶‘璁�': 'WAIT_CONFIRM', + '宸插畬鎴�': 'COMPLETED', + '宸茶瘎浠�': 'EVALUATED', +} + +const formatDateTime = (val?: string) => (val ? val.replace('T', ' ').slice(0, 19) : '') + +const normalizePriceType = (val?: string): 'points' | 'currency' | 'agreement' | 'free' => { + if (!val) return 'currency' + const s = String(val) + if (/(绉垎|points)/i.test(s)) return 'points' + if (/(鍗忚|agreement)/i.test(s)) return 'agreement' + if (/(鍏嶈垂|free)/i.test(s)) return 'free' + return 'currency' +} onMounted(async () => { - const { data } = (await fetchApprovalDetail({ id: route.params.id })) as any - Object.assign(detail, data || {}) + const orderId = String(route.params.id || '') + if (!orderId) return + + try { + // 骞惰鑾峰彇璁㈠崟璇︽儏鍜屽崗璁被鍨嬫鏌� + const [orderRes, agreementRes] = await Promise.all([ + orderApi.getOrderDetail(orderId), + orderApi.checkAgreementPriceType(orderId) + ]) + + const res = orderRes as any + const data = res?.data || {} + + // 璁剧疆鏄惁涓哄崗璁鍗� + const agreementResult = agreementRes as any + isAgreementOrder.value = agreementResult?.data === true + + const statusName: string = data.orderStatus || '' + const uiStatus = statusServerToUi[statusName] || 'INFO' + + // 鏄犲皠璁㈠崟璇︽儏澶撮儴淇℃伅 + const head = { + orderNo: data.orderId, + resourceTypeName: '杞欢浜у搧', + status: uiStatus, + statusName, + applyTime: formatDateTime(data.applyTime), + unitName: data.unitName || '-', + userName: data.userName || '-', + userAccount: data.userAccount || '-', + userDept: data.userDept || '-', + userPhone: data.userPhone || '-', + productName: data.productName || '-', + supplier: data.providerName || '-', + industry: data.industry || '-', + projectUnit: data.projectUnit || '-', + productType: data.productType || '-', + productDesc: data.productDesc || '-', + } + + // 鏄庣粏椤规槧灏� + const items: any[] = Array.isArray(data.orderDetails) + ? data.orderDetails.map((d: any, idx: number) => { + const pt = normalizePriceType(d.priceType) + return { + id: String(d.id ?? idx + 1), + name: d.suiteName, + saleType: d.salesForm, + accountCount: d.accountLimit, + customerTarget: d.customerType, + concurrentNodes: d.concurrentNodes, + pricePoint: pt === 'points' ? Number(d.unitPrice || 0) : 0, + priceCash: pt === 'currency' ? Number(d.unitPrice || 0) : 0, + priceProtocol: pt === 'agreement', + quantity: Number(d.quantity || 0), + period: Number(d.duration || 0), + remarks: d.remarks || '', // 鏂板 remarks 瀛楁 + } + }) + : [] + + // 姹囨�伙紙绠�鍗曠浉鍔狅細鍗曚环*鏁伴噺锛� + const pointTotalNum = items.reduce((sum, it) => sum + Number(it.pricePoint || 0) * Number(it.quantity || 0), 0) + const cashTotalNum = items.reduce((sum, it) => sum + Number(it.priceCash || 0) * Number(it.quantity || 0), 0) + + Object.assign(detail, head, { + items, + pointTotal: pointTotalNum.toLocaleString(), + cashTotal: cashTotalNum.toLocaleString(), + }) + + // 鍒濆鍖栬〃鍗曟暟鎹� + form.items = (detail.items || []).map((item: any, index: number) => { + return { + id: item.id, // 淇濆瓨order_detail鐨刬d + name: item.name, + remark: item.remarks || '' // 浣跨敤濂椾欢淇℃伅涓殑remarks瀛楁 + } + }) + + // 鑾峰彇璁㈠崟闄勪欢鍒楄〃锛堝鏋滄湁鐨勮瘽锛� + if (data.attachments && Array.isArray(data.attachments)) { + fileList.value = data.attachments.map((file: any) => ({ + name: file.fileName || file.originalName, + size: file.fileSize || 0, + uid: file.id, + status: 'success', + url: file.fileUrl, + uploaded: true + })) + } else { + fileList.value = [] + } + } catch (error) { + console.error('鑾峰彇璁㈠崟璇︽儏澶辫触:', error) + ElMessage.error('鑾峰彇璁㈠崟璇︽儏澶辫触') + } }) const goBack = () => router.back() -const approve = async (pass: boolean) => { - await formRef.value?.validate() - const { code } = (await submitApproval({ id: route.params.id, pass, remark: form.remark })) as any - if (code === 200) { - ElMessage.success('鎻愪氦鎴愬姛') - router.back() + +// 瀹℃壒閫氳繃澶勭悊 +const handleApprove = async () => { + if (!approvalForm.comments.trim()) { + ElMessage.warning('璇疯緭鍏ュ鎵规剰瑙�') + return + } + + try { + approvalLoading.value = true + await ElMessageBox.confirm('纭畾瑕佸鎵归�氳繃鍚楋紵', '纭鎿嶄綔', { + confirmButtonText: '纭畾', + cancelButtonText: '鍙栨秷', + type: 'warning' + }) + + const orderId = String(route.params.id || '') + const userId = userStore.getUserId ? Number(userStore.getUserId) : undefined + const comments = approvalForm.comments.trim() + + if (!orderId || !userId) { + ElMessage.error('璁㈠崟ID鎴栫敤鎴稩D涓嶈兘涓虹┖') + approvalLoading.value = false + return + } + + // 璋冪敤瀹℃壒閫氳繃API + const result = await approveOrder({ + orderId: orderId, + approvalOpinion: comments, + approverId: userId, + approverName: userStore.getUserDetail || '绠$悊鍛�', + approvalType: isAgreementOrder.value ? '瀹℃壒' : '鎺堟潈', + approvalResult: '閫氳繃' + }) + + if (result && result.code === 200) { + // 鏇存柊浜ゆ槗淇℃伅澶囨敞锛堝彧鏇存柊remarks锛屼笉鏇存柊璁㈠崟鐘舵�侊級 + const updateData = { + orderId: orderId, + orderDetails: form.items.map((item: any) => ({ + id: item.id, + remarks: item.remark || '' // 浣跨敤琛ㄥ崟涓殑澶囨敞 + })) + } + + await orderApi.updateOrderDetailRemarksOnly(updateData) + + // 瀹℃壒閫氳繃鍚庯紝浣跨敤鏂扮殑API鎺ュ彛鏇存柊璁㈠崟鐘舵�佸埌涓嬩竴涓姸鎬� + await orderApi.updateOrderStatusToNext(orderId) + ElMessage.success('瀹℃壒閫氳繃鎴愬姛') + router.back() + } else { + ElMessage.error(result?.msg || '瀹℃壒閫氳繃澶辫触') + } + } catch (error) { + if (error !== 'cancel') { + console.error('瀹℃壒澶辫触:', error) + ElMessage.error('瀹℃壒澶辫触') + } + } finally { + approvalLoading.value = false } } + +// 椹冲洖澶勭悊 +const handleReject = async () => { + if (!approvalForm.comments.trim()) { + ElMessage.warning('璇疯緭鍏ュ鎵规剰瑙�') + return + } + + try { + approvalLoading.value = true + await ElMessageBox.confirm('纭畾瑕侀┏鍥炲悧锛�', '纭鎿嶄綔', { + confirmButtonText: '纭畾', + cancelButtonText: '鍙栨秷', + type: 'warning' + }) + + const orderId = String(route.params.id || '') + const userId = userStore.getUserId ? Number(userStore.getUserId) : undefined + const comments = approvalForm.comments.trim() + + if (!orderId || !userId) { + ElMessage.error('璁㈠崟ID鎴栫敤鎴稩D涓嶈兘涓虹┖') + approvalLoading.value = false + return + } + + // 璋冪敤瀹℃壒椹冲洖API + const result = await approveOrder({ + orderId: orderId, + approvalOpinion: comments, + approverId: userId, + approverName: userStore.getUserDetail || '绠$悊鍛�', + approvalType: isAgreementOrder.value ? '瀹℃壒' : '鎺堟潈', + approvalResult: '椹冲洖' + }) + + if (result && result.code === 200) { + // 椹冲洖璁㈠崟锛屾洿鏂拌鍗曠姸鎬佸埌涓婁竴涓姸鎬� + await orderApi.updateOrderStatusToPrevious(orderId) + ElMessage.success('椹冲洖鎴愬姛') + router.back() + } else { + ElMessage.error(result?.msg || '椹冲洖澶辫触') + } + } catch (error) { + if (error !== 'cancel') { + console.error('椹冲洖澶辫触:', error) + ElMessage.error('椹冲洖澶辫触') + } + } finally { + approvalLoading.value = false + } +} + +// 涓庡垪琛ㄩ〉淇濇寔涓�鑷寸殑鐘舵�佺被鍨嬫槧灏勶紙UI灞曠ず鐢級 +const getStatusType = (status: string) => { + const statusMap: Record<string, 'warning' | 'danger' | 'success' | 'info'> = { + WAIT_APPROVAL: 'warning', + WAIT_UPLOAD: 'warning', + WAIT_CHECK: 'warning', + WAIT_CONFIRM: 'warning', + REJECTED: 'danger', + FINISH: 'success', + } + return statusMap[status] || 'info' +} + +// 璁㈠崟璇︽儏涓�"鍗曚环"鏄剧ず锛氫紭鍏堟樉绀虹Н鍒嗭紝鍏舵鏄剧ず璐у竵锛涙牸寮忕ず渚嬶細 +// "绉垎锛�50,000/濂�" 鎴� "璐у竵锛�7,500/濂�/骞�" 鎴� "鍏嶈垂锛�/骞�" +const formatPrice = (row: any) => { + const point = Number(row.pricePoint || 0) + const cash = Number(row.priceCash || 0) + const protocol = Boolean(row.priceProtocol) + + // 鍏嶈垂 + if (!point && !cash) { + return protocol ? '鍗忚锛�/骞�' : '鍏嶈垂锛�/骞�' + } + if (point) { + return `绉垎锛�${point.toLocaleString()}/濂梎 + } + // 浠呯幇閲� + return `璐у竵锛�${cash.toLocaleString()}/濂�/骞碻 +} + +// 鏈熼檺灞曠ず锛�0 琛ㄧず"姘镐箙"锛屽叾浠栨樉绀烘暟瀛� +const formatPeriod = (row: any) => { + const p = Number(row.period || 0) + return p === 0 ? '姘镐箙' : `${p}` +} + +// 琛ㄥご鏂囧瓧灞呬腑锛屼絾绗竴琛岀殑"璇︽儏"鏂囧瓧闈犲乏瀵归綈 +const headerCenterStyle: CSSProperties = { + textAlign: 'center', + fontSize: '14px', + background: '#f3f6fb' +} + +// 琛ㄤ綋鏂囧瓧澶у皬 +const bodyCellStyle: CSSProperties = { fontSize: '12px' } + +// 涓烘眹鎬昏娣诲姞鐗规畩鏍峰紡绫诲悕 +const getRowClassName = ({ row }: { row: any }) => { + return row.isSummary ? 'summary-row' : '' +} + +// 鍗曞厓鏍煎悎骞舵柟娉� +const arraySpanMethod = ({ row, column, rowIndex, columnIndex }: any) => { + if (row.isSummary) { + // 姹囨�昏锛氱涓�鍒楁樉绀哄悎骞跺唴瀹癸紝鍏朵粬鍒楅殣钘� + if (columnIndex === 0) { + return [1, 6] // 鍚堝苟1琛�6鍒� + } else { + return [0, 0] // 闅愯棌鍏朵粬鍒� + } + } + return [1, 1] // 鏅�氳姝e父鏄剧ず +} + +// 鏂囦欢鍒楄〃琛ㄦ牸琛ㄥご鏂囧瓧灞呬腑锛屼絾绗竴鍒楃殑"浜ゆ槗鏂囦欢"鏂囧瓧闈犲乏瀵归綈 +const fileTableHeaderStyle: CSSProperties = { + textAlign: 'center', + fontSize: '14px', + background: '#f3f6fb' +} +// 鏂囦欢鍒楄〃琛ㄦ牸琛ㄤ綋鏂囧瓧澶у皬 +const fileTableCellStyle: CSSProperties = { fontSize: '12px' } + +// 鏂囦欢澶у皬鏍煎紡鍖� +const formatFileSize = (size: number) => { + if (!size || size === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(size) / Math.log(k)); + return parseFloat((size / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +}; + +// 鍒ゆ柇鏂囦欢鏄惁鍙瑙� +const isPreviewable = (file: any) => { + // 棣栧厛妫�鏌IME绫诲瀷 + const previewableTypes = [ + 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/webp', + 'text/plain', 'text/html', 'text/css', 'text/javascript', + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx + ] + + // 濡傛灉MIME绫诲瀷鍖归厤锛岀洿鎺ヨ繑鍥瀟rue + if (previewableTypes.includes(file.type || '')) { + return true + } + + // 濡傛灉MIME绫诲瀷涓虹┖鎴栦笉鍖归厤锛屾牴鎹枃浠舵墿灞曞悕鍒ゆ柇 + const fileName = file.name || '' + const fileExtension = fileName.toLowerCase().split('.').pop() + + const previewableExtensions = [ + 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', + 'txt', 'html', 'htm', 'css', 'js', + 'pdf', + 'docx', 'xlsx', 'pptx' + ] + + return previewableExtensions.includes(fileExtension) +} + +// 鍒ゆ柇鏂囦欢鏄惁宸蹭笂浼犳垚鍔� +const isFileUploaded = (file: any) => { + // 鏂囦欢鏈塽rl涓旂姸鎬佷负success琛ㄧず宸蹭笂浼犳垚鍔� + return file.url && (file.status === 'success' || file.uploaded) +} + +// 鏂囦欢棰勮 +const handlePreview = async (file: any) => { + if (!file.url) { + ElMessage.warning('鏂囦欢閾炬帴涓嶅瓨鍦�') + return + } + + // 鑾峰彇鏂囦欢鎵╁睍鍚� + const fileName = file.name || '' + const fileExtension = fileName.toLowerCase().split('.').pop() + + let previewUrl = file.url + + // 濡傛灉鏂囦欢瀛樺偍鍦∕inIO锛屼紭鍏堜娇鐢ㄩ瑙圲RL + if (file.url.includes('order-attachments')) { + try { + // 棣栧厛灏濊瘯鑾峰彇棰勮URL + const previewResponse = await createAxios({ + url: `/admin/file/preview`, + method: 'GET', + params: { + fileName: file.url + } + }) + + console.log('棰勮URL鍝嶅簲:', previewResponse) + + // 妫�鏌ュ搷搴旀牸寮� + const responseData = previewResponse as any + if (responseData && responseData.code === 200 && responseData.data) { + previewUrl = responseData.data + console.log('浣跨敤棰勮URL:', previewUrl) + } else { + console.log('棰勮URL鑾峰彇澶辫触锛屼娇鐢ㄤ笅杞芥柟寮�') + // 濡傛灉棰勮URL鑾峰彇澶辫触锛屽洖閫�鍒颁笅杞芥柟寮� + const response = await createAxios({ + url: `/admin/file/download`, + method: 'GET', + responseType: 'blob', + params: { + fileName: file.url, + originalName: file.name + } + }) + + // 鍒涘缓棰勮URL + const blob = new Blob([response as any]) + previewUrl = window.URL.createObjectURL(blob) + console.log('浣跨敤Blob URL:', previewUrl) + } + } catch (error) { + console.error('鑾峰彇鏂囦欢澶辫触:', error) + ElMessage.error('鑾峰彇鏂囦欢澶辫触锛屾棤娉曢瑙�') + return + } + } + + // 鍥剧墖鏂囦欢鐩存帴鍦ㄦ柊绐楀彛鎵撳紑 + if ((file.type && file.type.startsWith('image/')) || + ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension)) { + console.log('棰勮鍥剧墖鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // PDF鏂囦欢鍦ㄦ柊绐楀彛鎵撳紑 + if (file.type === 'application/pdf' || fileExtension === 'pdf') { + console.log('棰勮PDF鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // 鏂囨湰鏂囦欢鍦ㄦ柊绐楀彛鎵撳紑 + if ((file.type && file.type.startsWith('text/')) || + ['txt', 'html', 'htm', 'css', 'js'].includes(fileExtension)) { + console.log('棰勮鏂囨湰鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // Office鏂囨。鍜屽叾浠栨枃浠剁被鍨嬶紝灏濊瘯鍦ㄦ柊绐楀彛鎵撳紑 + try { + console.log('棰勮鍏朵粬鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + } catch (error) { + console.error('棰勮澶辫触:', error) + ElMessage.error('棰勮澶辫触锛岃涓嬭浇鍚庢煡鐪�') + } +} + +// 鏂囦欢涓嬭浇 +const handleDownload = async (file: any) => { + if (!file.url) { + ElMessage.warning('鏂囦欢閾炬帴涓嶅瓨鍦�') + return + } + + console.log('寮�濮嬩笅杞芥枃浠�:', file.name, 'URL:', file.url) + + try { + // 濡傛灉鏂囦欢瀛樺偍鍦∕inIO锛屼娇鐢ㄥ悗绔洿鎺ヤ笅杞紸PI + if (file.url.includes('order-attachments')) { + console.log('浣跨敤MinIO涓嬭浇API') + + // 浣跨敤axios閫氳繃浠g悊璁块棶鍚庣API + const response = await createAxios({ + url: `/admin/file/download`, + method: 'GET', + responseType: 'blob', + params: { + fileName: file.url, + originalName: file.name + } + }) + + console.log('涓嬭浇鍝嶅簲:', response) + + // 鍒涘缓涓嬭浇閾炬帴 + const blob = new Blob([response as any]) + const downloadUrl = window.URL.createObjectURL(blob) + + console.log('鍒涘缓涓嬭浇閾炬帴:', downloadUrl) + + const link = document.createElement('a') + link.href = downloadUrl + link.download = file.name || 'download' + link.target = '_blank' + link.rel = 'noopener noreferrer' + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + // 娓呯悊URL瀵硅薄 + window.URL.revokeObjectURL(downloadUrl) + + ElMessage.success('鏂囦欢涓嬭浇鎴愬姛') + } else { + console.log('浣跨敤鐩存帴URL涓嬭浇') + // 鍏朵粬鎯呭喌鐩存帴浣跨敤鍘烾RL + const link = document.createElement('a') + link.href = file.url + link.download = file.name || 'download' + link.target = '_blank' + link.rel = 'noopener noreferrer' + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + ElMessage.success('寮�濮嬩笅杞芥枃浠�') + } + } catch (error) { + console.error('涓嬭浇澶辫触:', error) + ElMessage.error('涓嬭浇澶辫触锛岃閲嶈瘯') + } +} + + + +// 鐥囩粨涓庝慨澶嶈鏄庯細 +// 1) Element Plus 鐨� el-table 瀛愬垪 width 鐧惧垎姣旀槸鐩稿浜庤〃鏍煎鍣ㄧ殑鍍忕礌瀹藉害璁$畻锛屼絾鍙湁鍦ㄨ〃鏍煎鍣ㄦ湁鏄庣‘瀹藉害鏃舵墠鐢熸晥銆� +// 2) 鍦ㄧ埗鍒�/澶氱骇琛ㄥご涓嬶紝瀛愬垪 width 涓虹櫨鍒嗘瘮鏃讹紝鏇寸ǔ瀹氱殑鍋氭硶鏄皢鍏惰绠椾负鍍忕礌鍊肩粦瀹氱粰瀛愬垪銆� +// 3) 鍥犳鎴戜滑璇诲彇琛ㄦ牸澶栧眰瀹瑰櫒瀹藉害锛屾寜姣斾緥璁$畻鍍忕礌瀹藉害锛岄伩鍏嶅嚭鐜扮櫨鍒嗘瘮涓� table-layout 瀵艰嚧鐨勯敊浣嶄笌鎷変几銆� +const colWidths = computed(() => { + const containerWidth = orderTableWrapRef.value?.clientWidth || 0 + // 鐧惧垎姣斿垎鍒负锛氳鎯� 20% x 3锛屽崟浠� 15%锛屾暟閲� 10%锛屾湡闄� 15% => 鍚堣 100% + return { + detail1: Math.floor(containerWidth * 0.2), + detail2: Math.floor(containerWidth * 0.2), + detail3: Math.floor(containerWidth * 0.2), + price: Math.floor(containerWidth * 0.15), + quantity: Math.floor(containerWidth * 0.1), + period: Math.floor(containerWidth * 0.15), + } +}) </script> <style scoped lang="scss"> .title { font-weight: 600; } +.sub-title { font-weight: 600; margin: 10px 0; } .mt10 { margin-top: 10px; } .mt15 { margin-top: 15px; } .gray { color: #909399; font-size: 12px; } .total { text-align: right; margin-top: 10px; } .price { color: #f56c6c; font-weight: 600; } .ml20 { margin-left: 20px; } +.item-block { padding: 10px; border: 1px solid #ebeef5; border-radius: 4px; margin-bottom: 10px; } + +/* 缁熶竴琛ㄦ牸鍐呭鏂囧瓧澶у皬 */ +.order-table :deep(.el-table__body), +.order-table :deep(.el-table__header) { + font-size: 12px; +} +/* 琛ㄥご绗竴琛岃儗鏅壊锛堜笌浜ゆ槗鍐呭鏍囬琛屼繚鎸佷竴鑷达級 */ +.order-table :deep(.el-table__header-wrapper thead tr:first-child > th) { + background: #f3f6fb !important; + font-size: 14px !important; +} +/* 琛ㄥご绗簩琛岄珮搴︿笌鍐呰竟璺� */ +.order-table :deep(.el-table__header-wrapper thead tr:nth-child(2) > th) { + height: 5px !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + padding-left: 0 !important; + padding-right: 0 !important; +} +/* 寮哄埗琛ㄦ牸鍒楀浐瀹氬竷灞�锛岄伩鍏嶅唴瀹瑰奖鍝嶅垪瀹� */ +.order-table :deep(table) { + table-layout: fixed; + width: 100% !important; +} + +/* 姹囨�昏鏍峰紡 */ +.summary-row { + font-weight: 600; + color: #f56c6c; +} +.summary-total { + text-align: right; + font-weight: 600; +} +.summary-merged { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} +.summary-left { + text-align: left; +} +.summary-left .count { + color: #f56c6c; + font-weight: 600; +} +.summary-right { + text-align: right; +} +.order-table :deep(.summary-row) { + background-color: #fafafa; +} +.order-table :deep(.summary-row td) { + border-top: 2px solid #e4e7ed; +} + +/* 琛ㄥご绗竴琛�"璇︽儏"鏂囧瓧闈犲乏瀵归綈 */ +.order-table :deep(.el-table__header-wrapper thead tr:first-child th:first-child) { + text-align: left !important; +} + +/* 琛ㄥご鍥炬爣鏍峰紡 */ +.header-icon { + margin-right: 6px; + color: #409eff; + vertical-align: middle; +} + +/* 鍒嗛殧鏍囬椋庢牸 */ +.section-header { + background: #f3f6fb; + font-weight: 600; + --el-border-color: #e4e7ed; +} +.section-header :deep(.el-descriptions__label) { + background: #f3f6fb; + border-right: none !important; + width: 100%; +} +.section-header :deep(.el-descriptions__cell) { + background: #f3f6fb; +} +.section-header :deep(.el-descriptions__content) { + display: none !important; + padding: 0 !important; + border: 0 !important; +} +.section-icon { + margin-right: 6px; + color: #409eff; +} + +/* 璋冩暣鎻忚堪缁勪欢杈规浠ヤ究鏍囬琛岄鑹茶鐩栦腑闂村垎闅旂嚎 */ +:deep(.el-descriptions--border .el-descriptions__body .el-descriptions__table .el-descriptions__cell) { + border-right: 1px solid var(--el-border-color); +} +.section-header :deep(.el-descriptions__cell) { + border-right-color: transparent !important; +} + +/* 寮哄埗 Element Plus 鎻忚堪椤圭殑 label 瀹藉害閬靛惊 label-width锛堥伩鍏嶅唴瀹规拺寮�锛� */ +/* 鍏滃簳锛氬嵆浣垮唴鑱旀牱寮忚瑕嗙洊锛屼篃鐢� important 寮哄埗鍥哄畾 */ +.fixed-label :deep(.el-descriptions__label) { + width: 180px !important; + max-width: 180px !important; +} +.fixed-label :deep(.el-descriptions__content) { + width: calc(50% - 180px) !important; +} + +/* 寮哄寲绗竴琛屽垎闅旀爣棰樼殑鑳屾櫙涓庤竟妗嗚鐩� */ +.order-desc :deep(.el-descriptions__table tr:first-child .el-descriptions__cell), +.order-desc :deep(.el-descriptions__table tr:first-child .el-descriptions__label), +.order-desc :deep(.el-descriptions__table tr:first-child .el-descriptions__content) { + background: #eef3fb !important; + border-top-color: transparent !important; + border-bottom-color: #dcdfe6 !important; +} +.order-desc :deep(.el-descriptions__table tr:first-child .el-descriptions__label) { + border-right-color: transparent !important; +} + +/* 缁熶竴涓や釜鎻忚堪琛ㄦ牸鐨勫垪瀵归綈锛堟爣绛惧垪鍥哄畾瀹藉害锛屽唴瀹瑰垪绛夊垎鍓╀綑瀹藉害锛� */ +.order-desc :deep(.el-descriptions__table) { + table-layout: fixed; + width: 100%; +} +/* 浣跨敤绫婚�夋嫨鍣ㄨ�岄潪 nth-child锛屾彁鍗囩ǔ瀹氭�э紝纭繚姣忚涓ゅ垪涓ユ牸瀵归綈 */ +.order-desc :deep(.el-descriptions__table tr:not(:first-child) .el-descriptions__label) { + width: 180px !important; + max-width: 180px !important; + box-sizing: border-box; +} +.order-desc :deep(.el-descriptions__table tr:not(:first-child) .el-descriptions__content) { + width: calc(50% - 180px) !important; +} + +.desc-wrap { + white-space: pre-wrap; + line-height: 22px; +} + +/* 鏂囦欢鍒楄〃琛ㄦ牸鏍峰紡 */ +.file-section { + margin-top: 15px; + .file-title { + font-weight: 600; + margin-bottom: 10px; + } + .file-table { + width: 100%; + .file-name { + display: flex; + align-items: center; + .file-icon { + margin-right: 8px; + color: #409eff; + } + } + .preview-btn { + color: #409eff; + &:hover { + text-decoration: underline; + } + } + } +} + +/* 鏂囦欢琛ㄦ牸琛ㄥご绗竴鍒�"浜ゆ槗鏂囦欢"鏂囧瓧闈犲乏瀵归綈 */ +.file-table :deep(.el-table__header-wrapper thead tr th:first-child) { + text-align: left !important; +} + +/* 鏂囦欢鎿嶄綔鎸夐挳鏍峰紡 */ +.file-actions { + display: flex; + gap: 8px; + align-items: center; + justify-content: center; + + .preview-btn { + color: #409eff; + &:hover { + text-decoration: underline; + } + &:disabled { + color: #c0c4cc; + cursor: not-allowed; + } + } + + .download-btn { + color: #67c23a; + &:hover { + text-decoration: underline; + } + &:disabled { + color: #c0c4cc; + cursor: not-allowed; + } + } +} + +/* 浜ゆ槗淇℃伅澶囨敞琛ㄦ牸鏍峰紡 */ +.remark-table { + width: 100%; + + .remark-input { + width: 100%; + + :deep(.el-textarea__inner) { + border: none; + box-shadow: none; + background: transparent; + resize: none; + padding: 8px 0; + + &:focus { + border: 1px solid #409eff; + box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2); + border-radius: 4px; + background: #fff; + padding: 8px 12px; + } + } + + :deep(.el-input__count) { + background: transparent; + bottom: 2px; + right: 8px; + } + } +} + +/* 瀹℃壒鍐呭鏍峰紡 */ +.approval-content { + .approval-form { + .form-item { + display: flex; + align-items: flex-start; + margin-bottom: 20px; + + label { + width: 120px; + line-height: 32px; + margin-right: 10px; + font-weight: 500; + + &.required::before { + content: '*'; + color: #f56c6c; + margin-right: 4px; + } + } + + .el-textarea { + flex: 1; + } + } + } + + .approval-actions { + display: flex; + justify-content: center; + gap: 15px; + margin-top: 30px; + + .el-button { + min-width: 100px; + } + } +} </style> diff --git a/src/views/approveManage/tradeApproval/checkFiles.vue b/src/views/approveManage/tradeApproval/checkFiles.vue index 6760c42..02db0b4 100644 --- a/src/views/approveManage/tradeApproval/checkFiles.vue +++ b/src/views/approveManage/tradeApproval/checkFiles.vue @@ -1,112 +1,1154 @@ <template> <div class="default-main"> + <!-- 璁㈠崟淇℃伅 + 鐢宠浜轰俊鎭� + 浜ゆ槗鍐呭锛堝悎骞朵负鍚屼竴鍗$墖锛� --> <el-card shadow="never"> - <div class="title">璁㈠崟淇℃伅</div> - <el-descriptions :column="3" border class="mt10"> + <el-descriptions + :column="2" + border + class="mt10 order-desc fixed-label" + label-width="180px" + :label-style="labelStyle" + :content-style="contentStyle" + > + <el-descriptions-item :span="2" class="section-header"> + <template #label> + <el-icon class="section-icon"><Document /></el-icon> + <span>璁㈠崟淇℃伅</span> + </template> + <template #default></template> + </el-descriptions-item> <el-descriptions-item label="璁㈠崟缂栧彿">{{ detail.orderNo }}</el-descriptions-item> <el-descriptions-item label="浜ゆ槗璧勬簮绫诲瀷">{{ detail.resourceTypeName }}</el-descriptions-item> - <el-descriptions-item label="浜ゆ槗鐘舵��">{{ detail.statusName }}</el-descriptions-item> <el-descriptions-item label="鐢宠鏃堕棿">{{ detail.applyTime }}</el-descriptions-item> - <el-descriptions-item label="鍗曚綅">{{ detail.unitName }}</el-descriptions-item> - <el-descriptions-item label="鐢ㄦ埛鍚�">{{ detail.userName }}</el-descriptions-item> + <el-descriptions-item label="浜ゆ槗鐘舵��"> + <el-tag :type="getStatusType(detail.status)" size="small">{{ detail.statusName }}</el-tag> + </el-descriptions-item> </el-descriptions> + + <!-- 鐢宠浜轰俊鎭紙涓庤鍗曚俊鎭悓鍗$墖锛屽鐢ㄥ垎闅旀爣棰樻牱寮忥級 --> + <el-descriptions + :column="2" + border + class="mt15 order-desc fixed-label" + label-width="180px" + :label-style="labelStyle" + :content-style="contentStyle" + > + <el-descriptions-item :span="2" class="section-header"> + <template #label> + <el-icon class="section-icon"><User /></el-icon> + <span>鐢宠浜轰俊鎭�</span> + </template> + <template #default></template> + </el-descriptions-item> + <el-descriptions-item label="濮撳悕">{{ detail.userName || '-' }}</el-descriptions-item> + <el-descriptions-item label="鍗曚綅">{{ detail.unitName || '-' }}</el-descriptions-item> + <el-descriptions-item label="閮ㄩ棬">{{ detail.userDept || '-' }}</el-descriptions-item> + <el-descriptions-item label="鐢ㄦ埛鍚�">{{ detail.userAccount || '-' }}</el-descriptions-item> + </el-descriptions> + + <!-- 浜ゆ槗鍐呭锛堢揣闅忕敵璇蜂汉淇℃伅锛屽悓鍗$墖锛屽鐢ㄥ垎闅旀爣棰樻牱寮忥級 --> + <el-descriptions + :column="2" + border + class="mt15 order-desc fixed-label" + label-width="180px" + :label-style="labelStyle" + :content-style="contentStyle" + > + <el-descriptions-item :span="2" class="section-header"> + <template #label> + <el-icon class="section-icon"><Goods /></el-icon> + <span>浜ゆ槗鍐呭</span> + </template> + <template #default></template> + </el-descriptions-item> + <el-descriptions-item label="浜у搧鍚嶇О"> + <el-link type="primary" :underline="false">{{ detail.productName }}</el-link> + </el-descriptions-item> + <el-descriptions-item label="鎻愪緵鑰�">{{ detail.supplier }}</el-descriptions-item> + <el-descriptions-item label="琛屼笟棰嗗煙">{{ detail.industry }}</el-descriptions-item> + <el-descriptions-item label="鍗曚綅宸ョ▼">{{ detail.projectUnit }}</el-descriptions-item> + <el-descriptions-item label="浜у搧绫诲瀷">{{ detail.productType || '-' }}</el-descriptions-item> + <el-descriptions-item label="浜у搧绠�浠�"> + <div class="desc-wrap">{{ detail.productDesc }}</div> + </el-descriptions-item> + </el-descriptions> + + <!-- 璁㈠崟璇︽儏锛堢Щ鍔ㄥ埌浜ゆ槗鍐呭涓嬮潰锛屽悓涓�鍗$墖鍐咃級 --> + <div ref="orderTableWrapRef"> + <el-table + :data="tableData" + border + class="mt10 order-table" + :header-cell-style="headerCenterStyle" + :cell-style="bodyCellStyle" + :row-class-name="getRowClassName" + :span-method="arraySpanMethod" + > + <el-table-column> + <template #header> + <el-icon class="header-icon"><List /></el-icon> + <span>璇︽儏</span> + </template> + <el-table-column label="" :width="colWidths.detail1"> + <template #default="{ row }"> + <div v-if="!row.isSummary">{{ row.name }}</div> + <div v-else class="summary-merged"> + <div class="summary-left"> + 鍏� <span class="count">{{ detail.items.length }}</span> 浠� + </div> + <div class="summary-right"> + 鎬昏锛�<span class="price">{{ detail.pointTotal }}</span> 绉垎 + <span class="ml20 price">{{ detail.cashTotal }}</span> 鍏� + </div> + </div> + </template> + </el-table-column> + <el-table-column label="" :width="colWidths.detail2"> + <template #default="{ row }"> + <div v-if="!row.isSummary" class="gray">閿�鍞舰寮忥細{{ row.saleType || '-' }}</div> + <div v-if="!row.isSummary" class="gray">璐︽埛鏁伴噺锛歿{ row.accountCount ?? '-' }}</div> + </template> + </el-table-column> + <el-table-column label="" :width="colWidths.detail3"> + <template #default="{ row }"> + <div v-if="!row.isSummary" class="gray">瀹㈡埛瀵硅薄锛歿{ row.customerTarget || '-' }}</div> + <div v-if="!row.isSummary" class="gray">骞跺彂鑺傜偣鏁伴噺锛歿{ row.concurrentNodes ?? '-' }}</div> + </template> + </el-table-column> + </el-table-column> + <el-table-column label="鍗曚环"> + <el-table-column label="" :width="colWidths.price"> + <template #default="{ row }"> + <div v-if="!row.isSummary">{{ formatPrice(row) }}</div> + </template> + </el-table-column> + </el-table-column> + <el-table-column label="鏁伴噺"> + <el-table-column label="" :width="colWidths.quantity"> + <template #default="{ row }"> + <div v-if="!row.isSummary">{{ row.quantity }}</div> + </template> + </el-table-column> + </el-table-column> + <el-table-column label="鏈熼檺(骞�)"> + <el-table-column label="" :width="colWidths.period"> + <template #default="{ row }"> + <div v-if="!row.isSummary">{{ formatPeriod(row) }}</div> + </template> + </el-table-column> + </el-table-column> + </el-table> + </div> + + <!-- 绉婚櫎鍘熸潵鐨勮〃鏍煎簳閮ㄤ俊鎭紝鍥犱负宸茬粡绉诲埌琛ㄦ牸鏈�鍚庝竴琛� --> </el-card> + <!-- 瀹℃壒杩借釜 --> + <el-card class="mt15" shadow="never" v-if="detail.records?.length"> + <div class="title">瀹℃壒杩借釜</div> + + <!-- 鏍囩椤� --> + <el-tabs v-model="activeTab" class="approval-tabs"> + <el-tab-pane label="瀹℃壒璁板綍" name="records"> + <el-table + :data="detail.records" + border + class="mt10 record-table" + :header-cell-style="recordTableHeaderStyle" + :cell-style="recordTableCellStyle" + > + <el-table-column label="鑺傜偣鍚嶇О" prop="nodeName" width="120" /> + <el-table-column label="瀹℃壒浜�" prop="approver" width="120" /> + <el-table-column label="瀹℃壒閮ㄩ棬" prop="department" width="200" /> + <el-table-column label="寮�濮嬫椂闂�" prop="startTime" width="180" /> + <el-table-column label="缁撴潫鏃堕棿" prop="endTime" width="180" /> + <el-table-column label="鐘舵��" width="120"> + <template #default="{ row }"> + <el-tag :type="getRecordStatusType(row.statusName)" size="small"> + {{ row.statusName }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="瀹℃壒鎰忚" width="200"> + <template #default="{ row }"> + {{ row.opinion || '-' }} + </template> + </el-table-column> + </el-table> + </el-tab-pane> + <el-tab-pane label="娴佺▼鑺傜偣" name="nodes"> + <el-table + :data="detail.nodes" + border + class="mt10 node-table" + :header-cell-style="recordTableHeaderStyle" + :cell-style="recordTableCellStyle" + > + <el-table-column label="鑺傜偣鍚嶇О" prop="nodeName" width="120" /> + <el-table-column label="鑺傜偣绫诲瀷" prop="nodeType" width="120" /> + <el-table-column label="澶勭悊浜�" prop="handler" width="160" /> + <el-table-column label="澶勭悊閮ㄩ棬" prop="department" width="200" /> + <el-table-column label="鐘舵��" width="120"> + <template #default="{ row }"> + <el-tag :type="getNodeStatusType(row.status)" size="small"> + {{ row.statusName }} + </el-tag> + </template> + </el-table-column> + </el-table> + </el-tab-pane> + </el-tabs> + + <!-- 杩斿洖鎸夐挳 --> + <div class="action-buttons"> + <el-button @click="goBack">杩斿洖</el-button> + </div> + </el-card> + + <!-- 浜ゆ槗淇℃伅澶囨敞 --> <el-card class="mt15" shadow="never"> - <div class="title">浜ゆ槗鍐呭</div> - <el-table :data="detail.items" border class="mt10"> - <el-table-column label="璇︽儏" min-width="280"> + <div class="title">鏂囦欢鏍告煡</div> + + <!-- 鏂囦欢鍒楄〃 --> + <el-table + :data="fileList" + border + class="mt10 file-table" + :header-cell-style="fileTableHeaderStyle" + :cell-style="fileTableCellStyle" + v-loading="fileLoading" + > + <el-table-column label="搴忓彿" type="index" width="60" align="center" /> + <el-table-column label="鏂囦欢鍚�" prop="originalName" min-width="200"> <template #default="{ row }"> - <div> - <div>{{ row.name }}</div> - <div class="gray">瀹㈡埛瀵硅薄锛歿{ row.customerTarget }}</div> - <div class="gray">骞跺彂鑺傜偣鏁帮細{{ row.concurrentNodes }}</div> + <div class="file-name"> + <el-icon class="file-icon" :class="getFileIconClass(row.fileType)"> + <Document v-if="getFileIconClass(row.fileType) === 'doc'" /> + <Picture v-else-if="getFileIconClass(row.fileType) === 'image'" /> + <VideoPlay v-else-if="getFileIconClass(row.fileType) === 'video'" /> + <Document v-else /> + </el-icon> + <span class="file-text">{{ row.originalName || row.fileName }}</span> </div> </template> </el-table-column> - <el-table-column label="鍗曚环" prop="priceName" width="140" /> - <el-table-column label="鏁伴噺" prop="quantity" width="80" /> - <el-table-column label="鏈熼檺(骞�)" prop="period" width="120" /> - </el-table> - - <div class="total"> - 鎬昏锛�<span class="price">{{ detail.pointTotal }}</span> 绉垎 - <span class="ml20 price">{{ detail.cashTotal }}</span> 鍏� - </div> - </el-card> - - <el-card class="mt15" shadow="never"> - <div class="title">浜ゆ槗鏂囦欢</div> - <el-table :data="files" border class="mt10"> - <el-table-column label="鏂囦欢鍚嶇О" prop="name" /> - <el-table-column label="鏂囦欢澶у皬" prop="size" width="140" /> - <el-table-column label="鎿嶄綔" width="160"> + <el-table-column label="鏂囦欢绫诲瀷" prop="fileType" width="100" align="center"> <template #default="{ row }"> - <el-button type="primary" link @click="preview(row)">棰勮</el-button> - <el-button type="primary" link @click="download(row)">涓嬭浇</el-button> + <el-tag :type="getFileTypeTag(row.fileType)" size="small"> + {{ getFileTypeName(row.fileType) }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="鏂囦欢澶у皬" prop="fileSize" width="100" align="center"> + <template #default="{ row }"> + {{ formatFileSize(row.fileSize) }} + </template> + </el-table-column> + <el-table-column label="涓婁紶鏃堕棿" prop="createdAt" width="140" align="center"> + <template #default="{ row }"> + {{ formatDateTime(row.createdAt) }} + </template> + </el-table-column> + <el-table-column label="涓婁紶浜�" prop="uploadUserName" width="100" align="center" /> + <el-table-column label="闄勪欢绫诲瀷" prop="attachmentType" width="100" align="center"> + <template #default="{ row }"> + <el-tag :type="getAttachmentTypeTag(row.attachmentType)" size="small"> + {{ row.attachmentType || '鍏朵粬' }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="鎿嶄綔" width="180" align="center"> + <template #default="{ row }"> + <el-button + type="primary" + size="small" + @click="handlePreview(row)" + :disabled="!isPreviewable(row)" + > + 棰勮 + </el-button> + <el-button + type="success" + size="small" + @click="handleDownload(row)" + > + 涓嬭浇 + </el-button> </template> </el-table-column> </el-table> - <div class="title mt15">浜ゆ槗淇℃伅澶囨敞</div> - <el-form :model="form" label-width="120px" class="mt10"> - <div v-for="(item, i) in detail.items" :key="i" class="item-block"> - <div class="sub-title">{{ item.name }}</div> - <el-form-item label="澶囨敞"> - <el-input v-model="form.items[i].remark" placeholder="璇疯緭鍏ュ娉�" /> + <!-- 瀹℃壒鎰忚 --> + <div class="approval-section mt15"> + <div class="section-title">瀹℃壒鎰忚</div> + <el-form :model="approvalForm" label-width="100px" class="mt10"> + <el-form-item label="瀹℃壒鎰忚" prop="opinion"> + <el-input + v-model="approvalForm.opinion" + type="textarea" + :rows="4" + placeholder="璇疯緭鍏ュ鎵规剰瑙�" + maxlength="500" + show-word-limit + /> </el-form-item> - </div> - </el-form> - <div class="ba-center mt15"> - <el-button @click="goBack">杩斿洖</el-button> - <el-button type="success" @click="submit(true)">閫氳繃</el-button> - <el-button type="danger" @click="submit(false)">椹冲洖</el-button> + </el-form> + </div> + + <!-- 鎿嶄綔鎸夐挳 --> + <div class="approval-actions"> + <el-button @click="goBack" class="back-btn">杩斿洖</el-button> + <el-button + type="success" + @click="handleApprove(true)" + class="approve-btn" + :loading="approvalLoading" + > + 閫氳繃 + </el-button> + <el-button + type="danger" + @click="handleApprove(false)" + class="reject-btn" + :loading="approvalLoading" + > + 椹冲洖 + </el-button> </div> </el-card> + + <!-- 鏂囦欢棰勮寮圭獥 --> + <el-dialog + v-model="previewVisible" + title="鏂囦欢棰勮" + width="80%" + :close-on-click-modal="false" + :close-on-press-escape="false" + class="file-preview-dialog" + > + <div class="preview-content"> + <filePreview ref="filePreviewRef" @closePreview="previewVisible = false" /> + </div> + </el-dialog> </div> </template> <script setup lang="ts"> -import { onMounted, reactive } from 'vue' +import { onMounted, reactive, ref, computed, nextTick, type CSSProperties } from 'vue' +import { Document, User, Goods, List, Picture, VideoPlay } from '@element-plus/icons-vue' import { useRoute, useRouter } from 'vue-router' -import { fetchApprovalDetail, checkFiles } from '@/api/approvalManage' -import { ElMessage } from 'element-plus' +import { ElMessage, ElMessageBox } from 'element-plus' +import orderApi from '@/api/orderApi' +import { checkFiles } from '@/api/approvalManage' +import filePreview from '@/components/filePreview/index.vue' +import createAxios from '@/utils/axios' +import { useUserInfo } from '@/stores/modules/userInfo' const route = useRoute() const router = useRouter() -const detail = reactive<any>({ items: [] }) -const files = reactive<any[]>([]) -const form = reactive<any>({ items: [] }) +const userStore = useUserInfo() +const detail = reactive<any>({ items: [], records: [], nodes: [] }) +const orderTableWrapRef = ref<HTMLElement | null>(null) +const activeTab = ref('records') +const labelStyle = { width: '180px', maxWidth: '180px' } +const contentStyle = { width: 'calc(50% - 180px)' } -onMounted(async () => { - const { data } = (await fetchApprovalDetail({ id: route.params.id })) as any - Object.assign(detail, data || {}) - files.splice(0, files.length, ...(data?.files || [])) - form.items = (detail.items || []).map(() => ({ remark: '' })) +// 鏂囦欢鐩稿叧鏁版嵁 +const fileList = ref<any[]>([]) +const fileLoading = ref(false) +const previewVisible = ref(false) +const filePreviewRef = ref() + +// 瀹℃壒鐩稿叧鏁版嵁 +const approvalForm = reactive({ + opinion: '' +}) +const approvalLoading = ref(false) + +// 鏂囦欢琛ㄦ牸鏍峰紡 +const fileTableHeaderStyle: CSSProperties = { + textAlign: 'center', + fontSize: '14px', + background: '#f3f6fb' +} +const fileTableCellStyle: CSSProperties = { fontSize: '12px' } + +// 璁$畻琛ㄦ牸鏁版嵁锛屾坊鍔犳眹鎬昏 +const tableData = computed(() => { + const summaryRow = { + id: 'summary', + isSummary: true, + name: '', + saleType: '', + accountCount: 0, + customerTarget: '', + concurrentNodes: 0, + pricePoint: 0, + priceCash: 0, + quantity: 0, + period: 0, + } + return [...detail.items, summaryRow] }) -const preview = (file: any) => window.open(file.previewUrl) -const download = (file: any) => window.open(file.downloadUrl) +// 鐘舵�佹槧灏勶紙鍚庣涓枃 -> 鍓嶇鏋氫妇锛� +const statusServerToUi: Record<string, string> = { + '寰呬笂浼犳枃浠�': 'WAIT_UPLOAD', + '寰呮巿鏉�': 'WAIT_AUTHORIZE', + '寰呬氦鏄撶‘璁�': 'WAIT_CONFIRM', + '宸插畬鎴�': 'COMPLETED', + '宸茶瘎浠�': 'EVALUATED', +} + +const formatDateTime = (val?: string) => (val ? val.replace('T', ' ').slice(0, 19) : '') + +const normalizePriceType = (val?: string): 'points' | 'currency' | 'agreement' | 'free' => { + if (!val) return 'currency' + const s = String(val) + if (/(绉垎|points)/i.test(s)) return 'points' + if (/(鍗忚|agreement)/i.test(s)) return 'agreement' + if (/(鍏嶈垂|free)/i.test(s)) return 'free' + return 'currency' +} + +onMounted(async () => { + const orderId = String(route.params.id || '') + if (!orderId) return + const res = (await orderApi.getOrderDetail(orderId)) as any + const data = res?.data || {} + + const statusName: string = data.orderStatus || '' + const uiStatus = statusServerToUi[statusName] || 'INFO' + + // 鏄犲皠璁㈠崟璇︽儏澶撮儴淇℃伅 + const head = { + orderNo: data.orderId, + resourceTypeName: '杞欢浜у搧', + status: uiStatus, + statusName, + applyTime: formatDateTime(data.applyTime), + unitName: '-', + userName: '-', + userAccount: '-', + userDept: '-', + userPhone: '-', + productName: data.productName || '-', + supplier: data.providerName || '-', + industry: '-', + projectUnit: '-', + productType: '-', + productDesc: '-', + } + + // 鏄庣粏椤规槧灏� + const items: any[] = Array.isArray(data.orderDetails) + ? data.orderDetails.map((d: any, idx: number) => { + const pt = normalizePriceType(d.priceType) + return { + id: String(d.id ?? idx + 1), + name: d.suiteName, + saleType: d.salesForm, + accountCount: d.accountLimit, + customerTarget: d.customerType, + concurrentNodes: d.concurrentNodes, + pricePoint: pt === 'points' ? Number(d.unitPrice || 0) : 0, + priceCash: pt === 'currency' ? Number(d.unitPrice || 0) : 0, + priceProtocol: pt === 'agreement', + quantity: Number(d.quantity || 0), + period: Number(d.duration || 0), + } + }) + : [] + + // 姹囨�伙紙绠�鍗曠浉鍔狅細鍗曚环*鏁伴噺锛� + const pointTotalNum = items.reduce((sum, it) => sum + Number(it.pricePoint || 0) * Number(it.quantity || 0), 0) + const cashTotalNum = items.reduce((sum, it) => sum + Number(it.priceCash || 0) * Number(it.quantity || 0), 0) + + Object.assign(detail, head, { + items, + pointTotal: pointTotalNum.toLocaleString(), + cashTotal: cashTotalNum.toLocaleString(), + records: [], + nodes: [], + }) + + // 鍒濆鍖栨枃浠跺垪琛� + if (data.attachments && Array.isArray(data.attachments)) { + fileList.value = data.attachments + } +}) + +// 涓庡垪琛ㄩ〉淇濇寔涓�鑷寸殑鐘舵�佺被鍨嬫槧灏勶紙UI灞曠ず鐢級 +const getStatusType = (status: string) => { + const statusMap: Record<string, 'warning' | 'danger' | 'success' | 'info'> = { + WAIT_APPROVAL: 'warning', + WAIT_UPLOAD: 'warning', + WAIT_CHECK: 'warning', + WAIT_CONFIRM: 'warning', + REJECTED: 'danger', + FINISH: 'success', + } + return statusMap[status] || 'info' +} + +// 璁㈠崟璇︽儏涓�"鍗曚环"鏄剧ず锛氫紭鍏堟樉绀虹Н鍒嗭紝鍏舵鏄剧ず璐у竵锛涙牸寮忕ず渚嬶細 +// "绉垎锛�50,000/濂�" 鎴� "璐у竵锛�7,500/濂�/骞�" 鎴� "鍏嶈垂锛�/骞�" +const formatPrice = (row: any) => { + const point = Number(row.pricePoint || 0) + const cash = Number(row.priceCash || 0) + const protocol = Boolean(row.priceProtocol) + + // 鍏嶈垂 + if (!point && !cash) { + return protocol ? '鍗忚锛�/骞�' : '鍏嶈垂锛�/骞�' + } + if (point) { + return `绉垎锛�${point.toLocaleString()}/濂梎 + } + // 浠呯幇閲� + return `璐у竵锛�${cash.toLocaleString()}/濂�/骞碻 +} + +// 鏈熼檺灞曠ず锛�0 琛ㄧず"姘镐箙"锛屽叾浠栨樉绀烘暟瀛� +const formatPeriod = (row: any) => { + const p = Number(row.period || 0) + return p === 0 ? '姘镐箙' : `${p}` +} + +// 琛ㄥご鏂囧瓧灞呬腑锛屼絾绗竴琛岀殑"璇︽儏"鏂囧瓧闈犲乏瀵归綈 +const headerCenterStyle: CSSProperties = { + textAlign: 'center', + fontSize: '14px', + background: '#f3f6fb' +} + +// 琛ㄤ綋鏂囧瓧澶у皬 +const bodyCellStyle: CSSProperties = { fontSize: '12px' } + +// 瀹℃壒杩借釜琛ㄦ牸鏍峰紡 +const recordTableHeaderStyle: CSSProperties = { + textAlign: 'center', + fontSize: '14px', + background: '#f3f6fb' +} +const recordTableCellStyle: CSSProperties = { fontSize: '12px' } + +// 涓烘眹鎬昏娣诲姞鐗规畩鏍峰紡绫诲悕 +const getRowClassName = ({ row }: { row: any }) => { + return row.isSummary ? 'summary-row' : '' +} + +// 瀹℃壒璁板綍鐘舵�佺被鍨嬫槧灏� +const getRecordStatusType = (statusName: string) => { + const statusMap: Record<string, 'success' | 'warning' | 'danger' | 'info'> = { + '宸插畬鎴�': 'success', + '瀹¢槄涓�': 'warning', + '宸叉彁浜�': 'info', + '寰呭鏍�': 'warning', + '宸叉嫆缁�': 'danger', + } + return statusMap[statusName] || 'info' +} + +// 娴佺▼鑺傜偣鐘舵�佺被鍨嬫槧灏� +const getNodeStatusType = (status: string) => { + const statusMap: Record<string, 'success' | 'warning' | 'danger' | 'info'> = { + 'completed': 'success', + 'processing': 'warning', + 'pending': 'info', + 'rejected': 'danger', + } + return statusMap[status] || 'info' +} + +// 杩斿洖鎸夐挳 const goBack = () => router.back() -const submit = async (pass: boolean) => { - const { code } = (await checkFiles({ id: route.params.id, pass, ...form })) as any - if (code === 200) { - ElMessage.success('鎿嶄綔鎴愬姛') - router.back() + +// 鏂囦欢鐩稿叧鏂规硶 +const getFileIconClass = (fileType: string) => { + const type = fileType?.toLowerCase() || '' + if (['doc', 'docx', 'pdf', 'txt'].includes(type)) return 'doc' + if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(type)) return 'image' + if (['mp4', 'avi', 'mov', 'wmv'].includes(type)) return 'video' + return 'doc' +} + +const getFileTypeTag = (fileType: string) => { + const type = fileType?.toLowerCase() || '' + if (['doc', 'docx'].includes(type)) return 'primary' + if (['pdf'].includes(type)) return 'danger' + if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(type)) return 'success' + if (['mp4', 'avi', 'mov', 'wmv'].includes(type)) return 'warning' + return 'info' +} + +const getFileTypeName = (fileType: string) => { + const type = fileType?.toLowerCase() || '' + if (['doc', 'docx'].includes(type)) return 'Word' + if (['pdf'].includes(type)) return 'PDF' + if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(type)) return '鍥剧墖' + if (['mp4', 'avi', 'mov', 'wmv'].includes(type)) return '瑙嗛' + if (['xls', 'xlsx'].includes(type)) return 'Excel' + return '鍏朵粬' +} + +const formatFileSize = (size: number) => { + if (!size) return '0 B' + const units = ['B', 'KB', 'MB', 'GB'] + let index = 0 + let fileSize = size + while (fileSize >= 1024 && index < units.length - 1) { + fileSize /= 1024 + index++ + } + return `${fileSize.toFixed(2)} ${units[index]}` +} + +const getAttachmentTypeTag = (type: string) => { + switch (type) { + case '鍚堝悓': return 'primary' + case '鍙戠エ': return 'success' + default: return 'info' } } + +// 鍒ゆ柇鏂囦欢鏄惁鍙瑙� +const isPreviewable = (file: any) => { + // 棣栧厛妫�鏌IME绫诲瀷 + const previewableTypes = [ + 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/webp', + 'text/plain', 'text/html', 'text/css', 'text/javascript', + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx + ] + + // 濡傛灉MIME绫诲瀷鍖归厤锛岀洿鎺ヨ繑鍥瀟rue + if (previewableTypes.includes(file.fileType || '')) { + return true + } + + // 濡傛灉MIME绫诲瀷涓虹┖鎴栦笉鍖归厤锛屾牴鎹枃浠舵墿灞曞悕鍒ゆ柇 + const fileName = file.originalName || file.fileName || '' + const fileExtension = fileName.toLowerCase().split('.').pop() + + const previewableExtensions = [ + 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', + 'txt', 'html', 'htm', 'css', 'js', + 'pdf', + 'docx', 'xlsx', 'pptx' + ] + + return previewableExtensions.includes(fileExtension) +} + +// 鏂囦欢棰勮 +const handlePreview = async (file: any) => { + // 浼樺厛浣跨敤fileUrl锛屽鏋滄病鏈夊垯浣跨敤fileName + const fileUrl = file.fileUrl || file.fileName + if (!fileUrl) { + ElMessage.warning('鏂囦欢閾炬帴涓嶅瓨鍦�') + return + } + + // 鑾峰彇鏂囦欢鎵╁睍鍚� + const fileName = file.originalName || file.fileName || '' + const fileExtension = fileName.toLowerCase().split('.').pop() + + let previewUrl = fileUrl + + // 濡傛灉鏂囦欢瀛樺偍鍦∕inIO锛屼紭鍏堜娇鐢ㄩ瑙圲RL + if (fileUrl.includes('order-attachments')) { + try { + // 棣栧厛灏濊瘯鑾峰彇棰勮URL + const previewResponse = await createAxios({ + url: `/admin/file/preview`, + method: 'GET', + params: { + fileName: fileUrl + } + }) + + console.log('棰勮URL鍝嶅簲:', previewResponse) + + // 妫�鏌ュ搷搴旀牸寮� + const responseData = previewResponse as any + if (responseData && responseData.code === 200 && responseData.data) { + previewUrl = responseData.data + console.log('浣跨敤棰勮URL:', previewUrl) + } else { + console.log('棰勮URL鑾峰彇澶辫触锛屼娇鐢ㄤ笅杞芥柟寮�') + // 濡傛灉棰勮URL鑾峰彇澶辫触锛屽洖閫�鍒颁笅杞芥柟寮� + const response = await createAxios({ + url: `/admin/file/download`, + method: 'GET', + responseType: 'blob', + params: { + fileName: fileUrl, + originalName: file.originalName || file.fileName + } + }) + + // 鍒涘缓棰勮URL + const blob = new Blob([response as any]) + previewUrl = window.URL.createObjectURL(blob) + console.log('浣跨敤Blob URL:', previewUrl) + } + } catch (error) { + console.error('鑾峰彇鏂囦欢澶辫触:', error) + ElMessage.error('鑾峰彇鏂囦欢澶辫触锛屾棤娉曢瑙�') + return + } + } + + // 鍥剧墖鏂囦欢鐩存帴鍦ㄦ柊绐楀彛鎵撳紑 + if ((file.fileType && file.fileType.startsWith('image/')) || + ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension)) { + console.log('棰勮鍥剧墖鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // PDF鏂囦欢鍦ㄦ柊绐楀彛鎵撳紑 + if (file.fileType === 'application/pdf' || fileExtension === 'pdf') { + console.log('棰勮PDF鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // 鏂囨湰鏂囦欢鍦ㄦ柊绐楀彛鎵撳紑 + if ((file.fileType && file.fileType.startsWith('text/')) || + ['txt', 'html', 'htm', 'css', 'js'].includes(fileExtension)) { + console.log('棰勮鏂囨湰鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // Office鏂囨。鍜屽叾浠栨枃浠剁被鍨嬶紝灏濊瘯鍦ㄦ柊绐楀彛鎵撳紑 + try { + console.log('棰勮鍏朵粬鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + } catch (error) { + console.error('棰勮澶辫触:', error) + ElMessage.error('棰勮澶辫触锛岃涓嬭浇鍚庢煡鐪�') + } +} + +// 鏂囦欢涓嬭浇 +const handleDownload = async (file: any) => { + // 浼樺厛浣跨敤fileUrl锛屽鏋滄病鏈夊垯浣跨敤fileName + const fileUrl = file.fileUrl || file.fileName + if (!fileUrl) { + ElMessage.warning('鏂囦欢閾炬帴涓嶅瓨鍦�') + return + } + + console.log('寮�濮嬩笅杞芥枃浠�:', file.originalName || file.fileName, 'URL:', fileUrl) + + try { + // 濡傛灉鏂囦欢瀛樺偍鍦∕inIO锛屼娇鐢ㄥ悗绔洿鎺ヤ笅杞紸PI + if (fileUrl.includes('order-attachments')) { + console.log('浣跨敤MinIO涓嬭浇API') + + // 浣跨敤axios閫氳繃浠g悊璁块棶鍚庣API + const response = await createAxios({ + url: `/admin/file/download`, + method: 'GET', + responseType: 'blob', + params: { + fileName: fileUrl, + originalName: file.originalName || file.fileName + } + }) + + console.log('涓嬭浇鍝嶅簲:', response) + + // 鍒涘缓涓嬭浇閾炬帴 + const blob = new Blob([response as any]) + const downloadUrl = window.URL.createObjectURL(blob) + + console.log('鍒涘缓涓嬭浇閾炬帴:', downloadUrl) + + const link = document.createElement('a') + link.href = downloadUrl + link.download = file.originalName || file.fileName || 'download' + link.target = '_blank' + link.rel = 'noopener noreferrer' + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + // 娓呯悊URL瀵硅薄 + window.URL.revokeObjectURL(downloadUrl) + + ElMessage.success('鏂囦欢涓嬭浇鎴愬姛') + } else { + console.log('浣跨敤鐩存帴URL涓嬭浇') + // 鍏朵粬鎯呭喌鐩存帴浣跨敤鍘烾RL + const link = document.createElement('a') + link.href = fileUrl + link.download = file.originalName || file.fileName || 'download' + link.target = '_blank' + link.rel = 'noopener noreferrer' + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + ElMessage.success('寮�濮嬩笅杞芥枃浠�') + } + } catch (error) { + console.error('涓嬭浇澶辫触:', error) + ElMessage.error('涓嬭浇澶辫触锛岃閲嶈瘯') + } +} + +// 澶勭悊瀹℃壒 +const handleApprove = async (isApprove: boolean) => { + if (!approvalForm.opinion.trim()) { + ElMessage.warning('璇疯緭鍏ュ鎵规剰瑙�') + return + } + + const actionText = isApprove ? '閫氳繃' : '椹冲洖' + + try { + await ElMessageBox.confirm(`纭畾瑕�${actionText}璇ヨ鍗曞悧锛焋, '纭鎿嶄綔', { + confirmButtonText: '纭畾', + cancelButtonText: '鍙栨秷', + type: 'warning' + }) + + approvalLoading.value = true + const orderId = String(route.params.id || '') + + // 璋冪敤鏂囦欢鏍告煡API + const result = await checkFiles({ + orderId: orderId, + isApprove: isApprove, + approvalOpinion: approvalForm.opinion, + approverId: Number(userStore.getUserId) || 1, + approverName: userStore.getUserDetail || '绠$悊鍛�' + }) + + if (result && result.code === 200) { + ElMessage.success(`${actionText}鎴愬姛`) + router.back() + } else { + ElMessage.error(result?.msg || `${actionText}澶辫触`) + } + } catch (error) { + if (error !== 'cancel') { + console.error('瀹℃壒澶辫触:', error) + ElMessage.error('瀹℃壒澶辫触锛岃閲嶈瘯') + } + } finally { + approvalLoading.value = false + } +} + +// 鍗曞厓鏍煎悎骞舵柟娉� +const arraySpanMethod = ({ row, column, rowIndex, columnIndex }: any) => { + if (row.isSummary) { + // 姹囨�昏锛氱涓�鍒楁樉绀哄悎骞跺唴瀹癸紝鍏朵粬鍒楅殣钘� + if (columnIndex === 0) { + return [1, 6] // 鍚堝苟1琛�6鍒� + } else { + return [0, 0] // 闅愯棌鍏朵粬鍒� + } + } + return [1, 1] // 鏅�氳姝e父鏄剧ず +} + +// 鐥囩粨涓庝慨澶嶈鏄庯細 +// 1) Element Plus 鐨� el-table 瀛愬垪 width 鐧惧垎姣旀槸鐩稿浜庤〃鏍煎鍣ㄧ殑鍍忕礌瀹藉害璁$畻锛屼絾鍙湁鍦ㄨ〃鏍煎鍣ㄦ湁鏄庣‘瀹藉害鏃舵墠鐢熸晥銆� +// 2) 鍦ㄧ埗鍒�/澶氱骇琛ㄥご涓嬶紝瀛愬垪 width 涓虹櫨鍒嗘瘮鏃讹紝鏇寸ǔ瀹氱殑鍋氭硶鏄皢鍏惰绠椾负鍍忕礌鍊肩粦瀹氱粰瀛愬垪銆� +// 3) 鍥犳鎴戜滑璇诲彇琛ㄦ牸澶栧眰瀹瑰櫒瀹藉害锛屾寜姣斾緥璁$畻鍍忕礌瀹藉害锛岄伩鍏嶅嚭鐜扮櫨鍒嗘瘮涓� table-layout 瀵艰嚧鐨勯敊浣嶄笌鎷変几銆� +const colWidths = computed(() => { + const containerWidth = orderTableWrapRef.value?.clientWidth || 0 + // 鐧惧垎姣斿垎鍒负锛氳鎯� 20% x 3锛屽崟浠� 15%锛屾暟閲� 10%锛屾湡闄� 15% => 鍚堣 100% + return { + detail1: Math.floor(containerWidth * 0.2), + detail2: Math.floor(containerWidth * 0.2), + detail3: Math.floor(containerWidth * 0.2), + price: Math.floor(containerWidth * 0.15), + quantity: Math.floor(containerWidth * 0.1), + period: Math.floor(containerWidth * 0.15), + } +}) </script> <style scoped lang="scss"> .title { font-weight: 600; } -.sub-title { font-weight: 600; margin: 10px 0; } .mt10 { margin-top: 10px; } .mt15 { margin-top: 15px; } .gray { color: #909399; font-size: 12px; } .total { text-align: right; margin-top: 10px; } .price { color: #f56c6c; font-weight: 600; } .ml20 { margin-left: 20px; } -.item-block { padding: 10px; border: 1px solid #ebeef5; border-radius: 4px; margin-bottom: 10px; } +/* 缁熶竴琛ㄦ牸鍐呭鏂囧瓧澶у皬 */ +.order-table :deep(.el-table__body), +.order-table :deep(.el-table__header) { + font-size: 12px; +} +/* 瀹℃壒杩借釜琛ㄦ牸锛氳〃澶磋儗鏅� + 瀛楀彿 */ +.record-table :deep(.el-table__body), +.record-table :deep(.el-table__header) { + font-size: 12px; +} +.record-table :deep(.el-table__header-wrapper thead tr:first-child > th) { + background: #f5f7fa; +} +/* 琛ㄥご绗竴琛岃儗鏅壊锛堜笌浜ゆ槗鍐呭鏍囬琛屼繚鎸佷竴鑷达級 */ +.order-table :deep(.el-table__header-wrapper thead tr:first-child > th) { + background: #f3f6fb !important; + font-size: 14px !important; +} +/* 琛ㄥご绗簩琛岄珮搴︿笌鍐呰竟璺� */ +.order-table :deep(.el-table__header-wrapper thead tr:nth-child(2) > th) { + height: 5px !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + padding-left: 0 !important; + padding-right: 0 !important; +} +/* 寮哄埗琛ㄦ牸鍒楀浐瀹氬竷灞�锛岄伩鍏嶅唴瀹瑰奖鍝嶅垪瀹� */ +.order-table :deep(table) { + table-layout: fixed; + width: 100% !important; +} + +/* 姹囨�昏鏍峰紡 */ +.summary-row { + font-weight: 600; + color: #f56c6c; +} +.summary-total { + text-align: right; + font-weight: 600; +} +.summary-merged { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} +.summary-left { + text-align: left; +} +.summary-left .count { + color: #f56c6c; + font-weight: 600; +} +.summary-right { + text-align: right; +} +.order-table :deep(.summary-row) { + background-color: #fafafa; +} +.order-table :deep(.summary-row td) { + border-top: 2px solid #e4e7ed; +} + +/* 琛ㄥご绗竴琛�"璇︽儏"鏂囧瓧闈犲乏瀵归綈 */ +.order-table :deep(.el-table__header-wrapper thead tr:first-child th:first-child) { + text-align: left !important; +} + +/* 琛ㄥご鍥炬爣鏍峰紡 */ +.header-icon { + margin-right: 6px; + color: #409eff; + vertical-align: middle; +} + +/* 鍒嗛殧鏍囬椋庢牸 */ +.section-header { + background: #f3f6fb; + font-weight: 600; + --el-border-color: #e4e7ed; +} +.section-header :deep(.el-descriptions__label) { + background: #f3f6fb; + border-right: none !important; + width: 100%; +} +.section-header :deep(.el-descriptions__cell) { + background: #f3f6fb; +} +.section-header :deep(.el-descriptions__content) { + display: none !important; + padding: 0 !important; + border: 0 !important; +} +.section-icon { + margin-right: 6px; + color: #409eff; +} + +/* 璋冩暣鎻忚堪缁勪欢杈规浠ヤ究鏍囬琛岄鑹茶鐩栦腑闂村垎闅旂嚎 */ +:deep(.el-descriptions--border .el-descriptions__body .el-descriptions__table .el-descriptions__cell) { + border-right: 1px solid var(--el-border-color); +} +.section-header :deep(.el-descriptions__cell) { + border-right-color: transparent !important; +} + +/* 寮哄埗 Element Plus 鎻忚堪椤圭殑 label 瀹藉害閬靛惊 label-width锛堥伩鍏嶅唴瀹规拺寮�锛� */ +/* 鍏滃簳锛氬嵆浣垮唴鑱旀牱寮忚瑕嗙洊锛屼篃鐢� important 寮哄埗鍥哄畾 */ +.fixed-label :deep(.el-descriptions__label) { + width: 180px !important; + max-width: 180px !important; +} +.fixed-label :deep(.el-descriptions__content) { + width: calc(50% - 180px) !important; +} + +/* 寮哄寲绗竴琛屽垎闅旀爣棰樼殑鑳屾櫙涓庤竟妗嗚鐩� */ +.order-desc :deep(.el-descriptions__table tr:first-child .el-descriptions__cell), +.order-desc :deep(.el-descriptions__table tr:first-child .el-descriptions__label), +.order-desc :deep(.el-descriptions__table tr:first-child .el-descriptions__content) { + background: #eef3fb !important; + border-top-color: transparent !important; + border-bottom-color: #dcdfe6 !important; +} +.order-desc :deep(.el-descriptions__table tr:first-child .el-descriptions__label) { + border-right-color: transparent !important; +} + +/* 缁熶竴涓や釜鎻忚堪琛ㄦ牸鐨勫垪瀵归綈锛堟爣绛惧垪鍥哄畾瀹藉害锛屽唴瀹瑰垪绛夊垎鍓╀綑瀹藉害锛� */ +.order-desc :deep(.el-descriptions__table) { + table-layout: fixed; + width: 100%; +} +/* 浣跨敤绫婚�夋嫨鍣ㄨ�岄潪 nth-child锛屾彁鍗囩ǔ瀹氭�э紝纭繚姣忚涓ゅ垪涓ユ牸瀵归綈 */ +.order-desc :deep(.el-descriptions__table tr:not(:first-child) .el-descriptions__label) { + width: 180px !important; + max-width: 180px !important; + box-sizing: border-box; +} +.order-desc :deep(.el-descriptions__table tr:not(:first-child) .el-descriptions__content) { + width: calc(50% - 180px) !important; +} + +.desc-wrap { + white-space: pre-wrap; + line-height: 22px; +} + +/* 瀹℃壒杩借釜鏍囩椤垫牱寮� */ +.approval-tabs { + margin-top: 15px; +} + +/* 鎿嶄綔鎸夐挳鏍峰紡 */ +.action-buttons { + display: flex; + justify-content: center; + margin-top: 15px; + .el-button { + margin: 0 10px; + } +} + +/* 鏂囦欢琛ㄦ牸鏍峰紡 */ +.file-table { + .file-name { + display: flex; + align-items: center; + gap: 8px; + + .file-icon { + font-size: 14px; + + &.doc { + color: #409eff; + } + + &.image { + color: #67c23a; + } + + &.video { + color: #e6a23c; + } + } + + .file-text { + color: #303133; + font-size: 14px; + } + } +} + +/* 瀹℃壒鎰忚鍖哄煙 */ +.approval-section { + .section-title { + font-size: 16px; + font-weight: 600; + color: #303133; + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 1px solid #e4e7ed; + } +} + +/* 瀹℃壒鎿嶄綔鎸夐挳鏍峰紡 */ +.approval-actions { + display: flex; + justify-content: center; + margin-top: 20px; + gap: 20px; + + .back-btn { + background: #ffffff; + border: 1px solid #dcdfe6; + color: #606266; + + &:hover { + background: #f5f7fa; + border-color: #c0c4cc; + } + } + + .approve-btn { + background: #67c23a; + border-color: #67c23a; + color: #ffffff; + + &:hover { + background: #85ce61; + border-color: #85ce61; + } + } + + .reject-btn { + background: #f56c6c; + border-color: #f56c6c; + color: #ffffff; + + &:hover { + background: #f78989; + border-color: #f78989; + } + } +} + +/* 鏂囦欢棰勮寮圭獥鏍峰紡 */ +.file-preview-dialog { + :deep(.el-dialog__body) { + padding: 0; + height: 70vh; + } + + .preview-content { + height: 100%; + } +} </style> diff --git a/src/views/approveManage/tradeApproval/list.vue b/src/views/approveManage/tradeApproval/list.vue index 73d6f4b..cc335ab 100644 --- a/src/views/approveManage/tradeApproval/list.vue +++ b/src/views/approveManage/tradeApproval/list.vue @@ -148,13 +148,13 @@ <div v-if="row.isSpacer" class="spacer-cell"></div> <div v-else-if="!row.isMainOrder" class="price-info"> <span v-if="row.priceType === 'points'" class="price-points"> - {{ row.unitPrice }}绉垎 + 绉垎 {{ row.unitPrice }} </span> <span v-else-if="row.priceType === 'currency'" class="price-currency"> - 楼{{ row.unitPrice }} + 璐у竵 {{ row.unitPrice }} </span> <span v-else-if="row.priceType === 'agreement'" class="price-agreement"> - {{ row.unitPrice }} + 鍗忚 </span> <span v-else-if="row.priceType === 'free'" class="price-free"> 鍏嶈垂 @@ -176,8 +176,8 @@ <template #default="{ row }"> <div v-if="row.isSpacer" class="spacer-cell"></div> <div v-else-if="!row.isMainOrder" class="period-info"> - <span>{{ row.period }}</span> - <span v-if="row.isPermanent" class="permanent">姘镐箙</span> + <span v-if="row.period === 0" class="permanent">姘镐箙</span> + <span v-else>{{ row.period }}</span> </div> </template> </el-table-column> @@ -192,10 +192,35 @@ <div v-if="row.parentOrder && row.parentOrder.subOrders && row.parentOrder.subOrders.findIndex((sub: any) => sub.id === row.id) === 0" class="all-actions"> <div class="action-item"> <div class="action-buttons"> - <el-button v-if="row.parentOrder.canApprove" type="primary" link size="small" @click="toApprove(row.parentOrder)">瀹℃壒</el-button> - <el-button v-if="row.parentOrder.canCheck" type="primary" link size="small" @click="toCheckFiles(row.parentOrder)">鏍告煡鏂囦欢</el-button> - <el-button type="primary" link size="small" @click="toDetail(row.parentOrder)">鏌ョ湅</el-button> - <el-button type="primary" link size="small" @click="toTrack(row.parentOrder)">杩借釜</el-button> + <template v-for="action in getAvailableActions(row.parentOrder)" :key="action.type"> + <el-button + v-if="action.type === ActionType.AUTHORIZE" + type="primary" + link + size="small" + @click="handleAction(action, row.parentOrder)" + > + 鎺堟潈 + </el-button> + <el-button + v-else-if="action.type === ActionType.VIEW" + type="primary" + link + size="small" + @click="handleAction(action, row.parentOrder)" + > + 鏌ョ湅 + </el-button> + <el-button + v-else-if="action.type === ActionType.TRACK" + type="primary" + link + size="small" + @click="handleAction(action, row.parentOrder)" + > + 杩借釜 + </el-button> + </template> </div> </div> </div> @@ -219,6 +244,12 @@ /> </div> </el-card> + + <!-- 璁㈠崟鐘舵�佸璇濇 --> + <ProductOrderStatusDialog + v-model="orderStatusDialogVisible" + :order-id="currentOrderId" + /> </div> </template> @@ -227,18 +258,23 @@ import { useRouter } from 'vue-router' import { Search, Refresh } from '@element-plus/icons-vue' import { fetchApprovalPage } from '@/api/approvalManage' +import orderApi from '@/api/orderApi' +import { authorizeApproval, fetchApprovalByOrderId } from '@/api/approvalManage' +import { useUserInfo } from '@/stores/modules/userInfo' +import ProductOrderStatusDialog from '@/views/productManage/productOrderStatusDialog/index.vue' +import { OrderWorkflowController, OrderStatus, ActionType, PageType, StatusMapper } from '@/utils/orderWorkflow' const router = useRouter() +const userStore = useUserInfo() -// 鐘舵�侀�夐」 +// 鐘舵�侀�夐」锛堟洿鏂颁负鏂扮殑宸ヤ綔娴佺▼鐘舵�侊級 const statusOptions = [ { label: '鍏ㄩ儴', value: '' }, - { label: '寰呭鏍�', value: 'WAIT_APPROVAL' }, { label: '寰呬笂浼犳枃浠�', value: 'WAIT_UPLOAD' }, - { label: '寰呮牳鏌ユ枃浠�', value: 'WAIT_CHECK' }, + { label: '寰呮巿鏉�', value: 'WAIT_AUTHORIZE' }, { label: '寰呬氦鏄撶‘璁�', value: 'WAIT_CONFIRM' }, - { label: '宸查┏鍥�', value: 'REJECTED' }, - { label: '宸插畬鎴�', value: 'FINISH' }, + { label: '宸插畬鎴�', value: 'COMPLETED' }, + { label: '宸茶瘎浠�', value: 'EVALUATED' }, ] // 琛屼笟棰嗗煙閫夐」 @@ -289,209 +325,50 @@ // 璁㈠崟鍒楄〃鏁版嵁锛堝寘鍚富璁㈠崟鍜屽瓙璁㈠崟锛� const orderList = ref<any[]>([]) -// 妯℃嫙鏁版嵁鐢ㄤ簬灞曠ず -const mockData = [ - { - id: '1', - isMainOrder: true, - applyTime: '2025-05-21 10:00:00', - orderNo: '4348442557619205545', - demandSide: '涓氦鏂硅繙寤鸿鏈夐檺鍏徃', - supplySide: '涓氦鏂硅繙绉戞妧鏈夐檺鍏徃', - status: 'WAIT_APPROVAL', - statusName: '寰呭鏍�', - canApprove: true, - canCheck: false, - subOrders: [ - { - id: '1-1', - isMainOrder: false, - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - suiteName: '浼佷笟绉佹湁SaaS鐗堣鍙�', - salesForm: '涔版柇', - accountCount: 50, - customerObject: '浼佷笟', - concurrentCount: 50, - priceType: 'points', - unitPrice: '50,000', - quantity: 1, - period: 1, - isPermanent: true, - }, - { - id: '1-2', - isMainOrder: false, - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - suiteName: '浼佷笟绉佹湁SaaS鐗圤TA鍗囩骇鏈嶅姟', - salesForm: 'OTA鏈嶅姟', - accountCount: 50, - customerObject: '浼佷笟', - concurrentCount: 50, - priceType: 'currency', - unitPrice: '7,500', - quantity: 1, - period: 1, - isPermanent: false, - }, - { - id: '1-3', - isMainOrder: false, - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - suiteName: '浼佷笟绉佹湁SaaS鐗堢敤鎴峰閲忓寘', - salesForm: '绉佹湁澧為噺鍖�', - accountCount: 100, - customerObject: '浼佷笟', - concurrentCount: 100, - priceType: 'agreement', - unitPrice: '鍗忚浠�', - quantity: 1, - period: 1, - isPermanent: false, - }, - { - id: '1-4', - isMainOrder: false, - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - suiteName: '涓汉鍏湁SaaS鐗堣鍙�', - salesForm: '绉佹湁澧為噺鍖�', - accountCount: 50, - customerObject: '涓汉', - concurrentCount: 50, - priceType: 'free', - unitPrice: '0', - quantity: 1, - period: 3, - isPermanent: true, - }, - ], - }, - { - id: '2', - isMainOrder: true, - applyTime: '2025-05-20 10:00:00', - orderNo: '4347442557619205545', - demandSide: '鍗庝负鎶�鏈湁闄愬叕鍙�', - supplySide: '鍗庝负杞欢鎶�鏈湁闄愬叕鍙�', - status: 'WAIT_UPLOAD', - statusName: '寰呬笂浼犳枃浠�', - canApprove: false, - canCheck: false, - subOrders: [ - { - id: '2-1', - isMainOrder: false, - productName: '鍩轰簬鍥戒骇鑺墖瀹界獎铻嶅悎鑷粍缃戣澶囩殑搴旂敤', - suiteName: '浼佷笟鍏湁SaaS鐗堣鍙�', - salesForm: '绉熻祦', - accountCount: 100, - customerObject: '浼佷笟', - concurrentCount: 100, - priceType: 'currency', - unitPrice: '80,000', - quantity: 1, - period: 1, - isPermanent: false, - }, - ], - }, - { - id: '3', - isMainOrder: true, - applyTime: '2025-05-19 10:00:00', - orderNo: '4347342557619205545', - demandSide: '涓氦绗洓鑸姟宸ョ▼灞�鏈夐檺鍏徃', - supplySide: '涓氦绗洓鑸姟宸ョ▼灞�鏈夐檺鍏徃', - status: 'WAIT_CHECK', - statusName: '寰呮牳鏌ユ枃浠�', - canApprove: true, - canCheck: true, - subOrders: [ - { - id: '3-1', - isMainOrder: false, - productName: '楂樻々鐮佸ご杈呭姪鍑哄浘宸ュ叿绠�', - suiteName: '椤圭洰鍏湁SaaS鐗堣鍙�', - salesForm: '绉熻祦', - accountCount: 50, - customerObject: '椤圭洰閮�', - concurrentCount: 50, - priceType: 'currency', - unitPrice: '60,000', - quantity: 1, - period: 1, - isPermanent: false, - }, - ], - }, - { - id: '4', - isMainOrder: true, - applyTime: '2025-05-18 10:00:00', - orderNo: '4347442557619205545', - demandSide: '涓浗浜ら�氬缓璁捐偂浠芥湁闄愬叕鍙�', - supplySide: '涓氦鍏矾瑙勫垝璁捐闄㈡湁闄愬叕鍙�', - status: 'WAIT_CONFIRM', - statusName: '寰呬氦鏄撶‘璁�', - canApprove: false, - canCheck: false, - subOrders: [ - { - id: '4-1', - isMainOrder: false, - productName: '鍏矾鏁板瓧鍖栨柟妗堣璁$郴缁�', - suiteName: '浼佷笟鍏湁SaaS鐗堣鍙�', - salesForm: '绉熻祦', - accountCount: 100, - customerObject: '浼佷笟', - concurrentCount: 100, - priceType: 'currency', - unitPrice: '80,000', - quantity: 1, - period: 1, - isPermanent: false, - }, - ], - }, - { - id: '5', - isMainOrder: true, - applyTime: '2025-05-17 10:00:00', - orderNo: '4347342557619205545', - demandSide: '涓氦绗笁鑸姟宸ョ▼灞�鏈夐檺鍏徃', - supplySide: '涓氦绗笁鑸姟宸ョ▼灞�鏈夐檺鍏徃', - status: 'REJECTED', - statusName: '宸查┏鍥�', - canApprove: false, - canCheck: false, - subOrders: [ - { - id: '5-1', - isMainOrder: false, - productName: '鍩轰簬鏃犱汉鏈虹殑浼佷笟绾у彲瑙嗗寲椤圭洰绠$悊绯荤粺', - suiteName: '椤圭洰鍏湁SaaS鐗堣鍙�', - salesForm: '绉熻祦', - accountCount: 50, - customerObject: '椤圭洰閮�', - concurrentCount: 50, - priceType: 'free', - unitPrice: '0', - quantity: 1, - period: 1, - isPermanent: true, - }, - ], - }, -] +// 璁㈠崟鐘舵�佸璇濇鐩稿叧 +const orderStatusDialogVisible = ref(false) +const currentOrderId = ref<string>('') + +// 浣跨敤宸ヤ綔娴佺▼鎺у埗鍣ㄧ殑鐘舵�佹槧灏� +const statusUiToServer: Record<string, string> = { + WAIT_UPLOAD: OrderStatus.WAIT_UPLOAD, + WAIT_AUTHORIZE: OrderStatus.WAIT_AUTHORIZE, + WAIT_CONFIRM: OrderStatus.WAIT_CONFIRM, + COMPLETED: OrderStatus.COMPLETED, + EVALUATED: OrderStatus.EVALUATED, +} + +const statusServerToUi: Record<string, string> = { + [OrderStatus.WAIT_UPLOAD]: 'WAIT_UPLOAD', + [OrderStatus.WAIT_AUTHORIZE]: 'WAIT_AUTHORIZE', + [OrderStatus.WAIT_CONFIRM]: 'WAIT_CONFIRM', + [OrderStatus.COMPLETED]: 'COMPLETED', + [OrderStatus.EVALUATED]: 'EVALUATED', +} + +const formatDateTime = (val?: string) => { + if (!val) return '' + // 鍚庣 LocalDateTime 鍙兘鏄� 2025-05-21T10:00:00 + return val.replace('T', ' ').slice(0, 19) +} + +const normalizePriceType = (val?: string): 'points' | 'currency' | 'agreement' | 'free' => { + if (!val) return 'currency' + const s = String(val) + if (/(绉垎|points)/i.test(s)) return 'points' + if (/(鍗忚|agreement)/i.test(s)) return 'agreement' + if (/(鍏嶈垂|free)/i.test(s)) return 'free' + return 'currency' +} // 鑾峰彇鐘舵�佺被鍨� const getStatusType = (status: string) => { const statusMap: Record<string, 'warning' | 'danger' | 'success' | 'info'> = { - WAIT_APPROVAL: 'warning', WAIT_UPLOAD: 'warning', - WAIT_CHECK: 'warning', + WAIT_AUTHORIZE: 'warning', WAIT_CONFIRM: 'warning', - REJECTED: 'danger', - FINISH: 'success', + COMPLETED: 'success', + EVALUATED: 'success', } return statusMap[status] || 'info' } @@ -575,26 +452,79 @@ } } -// 鎼滅储澶勭悊 +// 鎼滅储澶勭悊锛堟帴鍏ョ湡瀹炲悗绔級 const handleSearch = async () => { - // 妯℃嫙API璋冪敤锛屽疄闄呴」鐩腑杩欓噷浼氳皟鐢ㄧ湡瀹炵殑API - // const { data } = (await fetchApprovalPage({ ...query, pageNo: page.current, pageSize: page.size })) as any - - // 浣跨敤妯℃嫙鏁版嵁 - const flatData: any[] = [] - mockData.forEach(order => { - flatData.push(order) - if (order.subOrders) { - order.subOrders.forEach((subOrder: any) => { - flatData.push({ ...subOrder, parentOrder: order }) - }) - } - // 鍦ㄦ瘡涓鍗曞潡鐨勬湯灏炬彃鍏ヤ竴涓垎闅旂┖琛� - flatData.push({ isSpacer: true, isMainOrder: false, parentOrder: order }) + const payload: any = { + pageNum: page.current, + pageSize: page.size, + productName: query.productName || undefined, + orderId: query.orderNo || undefined, + } + if (query.status) payload.orderStatus = statusUiToServer[query.status] + if (Array.isArray(query.dateRange) && query.dateRange.length === 2) { + payload.applyTimeStart = query.dateRange[0] + payload.applyTimeEnd = query.dateRange[1] + } + + const res = (await fetchApprovalPage(payload)) as any + const pageData = res?.data + const list: any[] = Array.isArray(pageData?.list) ? pageData.list : [] + page.total = Number(pageData?.total || 0) + + // 骞跺彂鑾峰彇姣忎釜璁㈠崟鐨勮鎯咃紙鐢ㄤ簬鏋勯�犲瓙璁㈠崟琛岋級 + const detailsArr = await Promise.all( + list.map(async (order: any) => { + try { + const detailRes = (await orderApi.getOrderDetail(order.orderId)) as any + return detailRes?.data + } catch (e) { + return null + } + }) + ) + + const flatData: any[] = [] + list.forEach((order: any, idx: number) => { + const uiStatus = statusServerToUi[order.orderStatus] || 'WAIT_UPLOAD' + const mainRow: any = { + id: order.orderId, + isMainOrder: true, + applyTime: formatDateTime(order.applyTime || ''), + orderNo: order.orderId, + demandSide: '', + supplySide: order.providerName || '', + status: uiStatus, + statusName: order.orderStatus || '', + orderStatus: StatusMapper.toUIStatus(order.orderStatus), // 杞崲涓烘爣鍑嗙姸鎬佹灇涓� + } + + const detail = detailsArr[idx] + const subOrders: any[] = Array.isArray(detail?.orderDetails) + ? detail.orderDetails.map((d: any, i: number) => ({ + id: `${order.orderId}-${i + 1}`, + isMainOrder: false, + productName: order.productName || '', + suiteName: d.suiteName, + salesForm: d.salesForm, + accountCount: d.accountLimit, + customerObject: d.customerType, + concurrentCount: d.concurrentNodes, + priceType: normalizePriceType(d.priceType), + unitPrice: d.unitPrice, + quantity: d.quantity, + period: d.duration, + })) + : [] + + // 渚涘悎骞堕�昏緫浣跨敤 + ;(mainRow as any).subOrders = subOrders + + flatData.push(mainRow) + subOrders.forEach((sub) => flatData.push({ ...sub, parentOrder: mainRow })) + flatData.push({ isSpacer: true, isMainOrder: false, parentOrder: mainRow }) }) - + orderList.value = flatData - page.total = mockData.length } // 閲嶇疆 @@ -617,7 +547,40 @@ const toApprove = (row: any) => router.push({ name: 'tradeApproval', params: { id: row.id } }) const toCheckFiles = (row: any) => router.push({ name: 'tradeCheckFiles', params: { id: row.id } }) const toDetail = (row: any) => router.push({ name: 'tradeOrderDetail', params: { id: row.id } }) -const toTrack = (row: any) => router.push({ name: 'tradeTrack', params: { id: row.id } }) + +// 杩借釜璁㈠崟 - 鏄剧ず璁㈠崟鐘舵�佸璇濇 +const showOrderTrack = (row: any) => { + currentOrderId.value = row.id + orderStatusDialogVisible.value = true +} + +// 鑾峰彇鍙敤鎿嶄綔鍒楄〃 +const getAvailableActions = (order: any) => { + if (!order.orderStatus) return [] + return OrderWorkflowController.getAvailableActions(PageType.TRADE_APPROVAL, order.orderStatus) +} + +// 澶勭悊鎿嶄綔鐐瑰嚮 +const handleAction = (action: any, order: any) => { + switch (action.type) { + case ActionType.VIEW: + toDetail(order) + break + case ActionType.TRACK: + showOrderTrack(order) + break + case ActionType.AUTHORIZE: + toAuthorize(order) + break + default: + console.warn('鏈煡鐨勬搷浣滅被鍨�:', action.type) + } +} + +// 鎺堟潈锛氳烦杞埌鎺堟潈椤甸潰 +const toAuthorize = (row: any) => { + router.push({ name: 'tradeAuthorization', params: { id: row.id } }) +} onMounted(handleSearch) </script> diff --git a/src/views/tradeManage/buyer/index.vue b/src/views/tradeManage/buyer/index.vue index 73ccae5..dc072b9 100644 --- a/src/views/tradeManage/buyer/index.vue +++ b/src/views/tradeManage/buyer/index.vue @@ -86,10 +86,6 @@ <span class="value">{{ row.orderNo }}</span> </div> <div class="order-item"> - <span class="label">闇�姹傛柟:</span> - <span class="value">{{ row.demandSide }}</span> - </div> - <div class="order-item"> <span class="label">渚涘簲鏂�:</span> <span class="value">{{ row.supplySide }}</span> </div> @@ -148,16 +144,16 @@ <div v-if="row.isSpacer" class="spacer-cell"></div> <div v-else-if="!row.isMainOrder" class="price-info"> <span v-if="row.priceType === 'points'" class="price-points"> - {{ row.unitPrice }} + 绉垎 {{ row.unitPrice }} </span> <span v-else-if="row.priceType === 'currency'" class="price-currency"> - {{ row.unitPrice }} + 璐у竵 {{ row.unitPrice }} </span> <span v-else-if="row.priceType === 'agreement'" class="price-agreement"> - {{ row.unitPrice }} + 鍗忚 </span> <span v-else-if="row.priceType === 'free'" class="price-free"> - {{ row.unitPrice }} + 鍏嶈垂 </span> </div> </template> @@ -176,8 +172,8 @@ <template #default="{ row }"> <div v-if="row.isSpacer" class="spacer-cell"></div> <div v-else-if="!row.isMainOrder" class="period-info"> - <span>{{ row.period }}</span> - <span v-if="row.isPermanent" class="permanent">姘镐箙</span> + <span v-if="row.period === 0" class="permanent">姘镐箙</span> + <span v-else>{{ row.period }}</span> </div> </template> </el-table-column> @@ -192,30 +188,61 @@ <div v-if="row.parentOrder && row.parentOrder.subOrders && row.parentOrder.subOrders.findIndex((sub: any) => sub.id === row.id) === 0" class="all-actions"> <div class="action-item"> <div class="action-buttons"> - <span v-if="row.parentOrder.status === 'WAIT_REVIEW'" class="op-text warning">寰呭鏍�</span> - <template v-else-if="row.parentOrder.status === 'WAIT_UPLOAD'"> - <el-button type="danger" link size="small">鍙栨秷浜ゆ槗</el-button> - <el-button type="primary" link size="small" @click="toUpload(row.parentOrder)">涓婁紶鏂囦欢</el-button> - <el-button type="primary" link size="small" @click="toTrack(row.parentOrder)">杩借釜</el-button> - </template> - <template v-else-if="row.parentOrder.status === 'WAIT_CHECK'"> - <span class="op-text warning">寰呮牳鏌ユ枃浠�</span> - <el-button type="primary" link size="small" @click="toDetail(row.parentOrder)">鏌ョ湅</el-button> - <el-button type="primary" link size="small" @click="toTrack(row.parentOrder)">杩借釜</el-button> - </template> - <template v-else-if="row.parentOrder.status === 'WAIT_CONFIRM'"> - <span class="op-text warning">寰呬氦鏄撶‘璁�</span> - <el-button type="primary" link size="small" @click="toConfirm(row.parentOrder)">纭浜ゆ槗</el-button> - <el-button type="primary" link size="small" @click="toTrack(row.parentOrder)">杩借釜</el-button> - </template> - <template v-else-if="row.parentOrder.status === 'WAIT_EVALUATE'"> - <span class="op-text warning">寰呰瘎浠�</span> - <el-button type="primary" link size="small" @click="toEvaluate(row.parentOrder)">璇勪环</el-button> - <el-button type="primary" link size="small" @click="toTrack(row.parentOrder)">杩借釜</el-button> - </template> - <template v-else> - <el-button type="primary" link size="small" @click="toDetail(row.parentOrder)">鏌ョ湅</el-button> - <el-button type="primary" link size="small" @click="toTrack(row.parentOrder)">杩借釜</el-button> + <template v-for="action in getAvailableActions(row.parentOrder)" :key="action.type"> + <el-button + v-if="action.type === ActionType.VIEW" + type="primary" + link + size="small" + @click="handleAction(action, row.parentOrder)" + > + 鏌ョ湅 + </el-button> + <el-button + v-else-if="action.type === ActionType.TRACK" + type="primary" + link + size="small" + @click="handleAction(action, row.parentOrder)" + > + 杩借釜 + </el-button> + <el-button + v-else-if="action.type === ActionType.UPLOAD_FILE" + type="primary" + link + size="small" + @click="handleAction(action, row.parentOrder)" + > + 鎻愪氦鏂囦欢 + </el-button> + <el-button + v-else-if="action.type === ActionType.CONFIRM_TRADE" + type="primary" + link + size="small" + @click="handleAction(action, row.parentOrder)" + > + 浜ゆ槗纭 + </el-button> + <el-button + v-else-if="action.type === ActionType.EVALUATE" + type="primary" + link + size="small" + @click="handleAction(action, row.parentOrder)" + > + 璇勪环 + </el-button> + <el-button + v-else-if="action.type === ActionType.CANCEL_ORDER" + type="danger" + link + size="small" + @click="handleAction(action, row.parentOrder)" + > + 鍙栨秷璁㈠崟 + </el-button> </template> </div> </div> @@ -240,6 +267,12 @@ /> </div> </el-card> + + <!-- 璁㈠崟鐘舵�佸璇濇 --> + <ProductOrderStatusDialog + v-model="orderStatusDialogVisible" + :order-id="currentOrderId" + /> </div> </template> @@ -247,17 +280,23 @@ import { onMounted, reactive, ref } from 'vue' import { useRouter } from 'vue-router' import { Search, Refresh } from '@element-plus/icons-vue' +import { ElMessage, ElMessageBox } from 'element-plus' +import orderApi from '@/api/orderApi' +import { useUserInfo } from '@/stores/modules/userInfo' +import ProductOrderStatusDialog from '@/views/productManage/productOrderStatusDialog/index.vue' +import { OrderWorkflowController, OrderStatus, ActionType, PageType, StatusMapper } from '@/utils/orderWorkflow' const router = useRouter() +const userStore = useUserInfo() -// 鐘舵�侀�夐」 +// 鐘舵�侀�夐」锛堟洿鏂颁负鏂扮殑宸ヤ綔娴佺▼鐘舵�侊級 const statusOptions = [ { label: '鍏ㄩ儴', value: '' }, - { label: '寰呭鏍�', value: 'WAIT_REVIEW' }, { label: '寰呬笂浼犳枃浠�', value: 'WAIT_UPLOAD' }, - { label: '寰呮牳鏌ユ枃浠�', value: 'WAIT_CHECK' }, + { label: '寰呮巿鏉�', value: 'WAIT_AUTHORIZE' }, { label: '寰呬氦鏄撶‘璁�', value: 'WAIT_CONFIRM' }, - { label: '寰呰瘎浠�', value: 'WAIT_EVALUATE' }, + { label: '宸插畬鎴�', value: 'COMPLETED' }, + { label: '宸茶瘎浠�', value: 'EVALUATED' }, ] // 琛屼笟棰嗗煙閫夐」 @@ -303,203 +342,54 @@ }) // 鍒嗛〉淇℃伅 -const page = reactive({ current: 1, size: 10, total: 18 }) +const page = reactive({ current: 1, size: 10, total: 0 }) // 璁㈠崟鍒楄〃鏁版嵁锛堝寘鍚富璁㈠崟鍜屽瓙璁㈠崟锛� const orderList = ref<any[]>([]) -// 妯℃嫙鏁版嵁鐢ㄤ簬灞曠ず -const mockData = [ - { - id: '1', - isMainOrder: true, - applyTime: '2025-05-21 10:00:00', - orderNo: '4348442557619205545', - demandSide: '涓氦鏂硅繙寤鸿鏈夐檺鍏徃', - supplySide: '涓氦鏂硅繙绉戞妧鏈夐檺鍏徃', - status: 'WAIT_REVIEW', - statusName: '寰呭鏍�', - subOrders: [ - { - id: '1-1', - isMainOrder: false, - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - suiteName: '浼佷笟绉佹湁SaaS鐗堣鍙�', - salesForm: '涔版柇', - accountCount: 50, - customerObject: '浼佷笟', - concurrentCount: 50, - priceType: 'points', - unitPrice: '绉垎: 50,000/濂�', - quantity: 1, - period: 1, - isPermanent: true, - }, - { - id: '1-2', - isMainOrder: false, - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - suiteName: '浼佷笟绉佹湁SaaS鐗圤TA鍗囩骇鏈嶅姟', - salesForm: 'OTA鏈嶅姟', - accountCount: 50, - customerObject: '浼佷笟', - concurrentCount: 50, - priceType: 'currency', - unitPrice: '璐у竵: 7,500/濂�/骞�', - quantity: 1, - period: 1, - isPermanent: false, - }, - { - id: '1-3', - isMainOrder: false, - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - suiteName: '浼佷笟绉佹湁SaaS鐗堢敤鎴峰閲忓寘', - salesForm: '绉佹湁澧為噺鍖�', - accountCount: 100, - customerObject: '浼佷笟', - concurrentCount: 100, - priceType: 'agreement', - unitPrice: '鍗忚:/骞�', - quantity: 1, - period: 1, - isPermanent: false, - }, - { - id: '1-4', - isMainOrder: false, - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - suiteName: '涓汉鍏湁SaaS鐗堣鍙�', - salesForm: '绉佹湁澧為噺鍖�', - accountCount: 50, - customerObject: '涓汉', - concurrentCount: 50, - priceType: 'free', - unitPrice: '鍏嶈垂:/骞�', - quantity: 3, - period: 1, - isPermanent: true, - }, - ], - }, - { - id: '2', - isMainOrder: true, - applyTime: '2025-05-20 10:00:00', - orderNo: '4347442557619205545', - demandSide: '鍗庝负鎶�鏈湁闄愬叕鍙�', - supplySide: '鍗庝负杞欢鎶�鏈湁闄愬叕鍙�', - status: 'WAIT_UPLOAD', - statusName: '寰呬笂浼犳枃浠�', - subOrders: [ - { - id: '2-1', - isMainOrder: false, - productName: '鍩轰簬鍥戒骇鑺墖瀹界獎铻嶅悎鑷粍缃戣澶囩殑搴旂敤', - suiteName: '浼佷笟鍏湁SaaS鐗堣鍙�', - salesForm: '绉熻祦', - accountCount: 100, - customerObject: '浼佷笟', - concurrentCount: 100, - priceType: 'currency', - unitPrice: '璐у竵: 80,000/骞�', - quantity: 1, - period: 2, - isPermanent: false, - }, - ], - }, - { - id: '3', - isMainOrder: true, - applyTime: '2025-05-19 10:00:00', - orderNo: '4347342557619205545', - demandSide: '涓氦绗洓鑸姟宸ョ▼灞�鏈夐檺鍏徃', - supplySide: '涓氦绗洓鑸姟宸ョ▼灞�鏈夐檺鍏徃', - status: 'WAIT_CHECK', - statusName: '寰呮牳鏌ユ枃浠�', - subOrders: [ - { - id: '3-1', - isMainOrder: false, - productName: '楂樻々鐮佸ご杈呭姪鍑哄浘宸ュ叿绠�', - suiteName: '椤圭洰鍏湁SaaS鐗堣鍙�', - salesForm: '绉熻祦', - accountCount: 50, - customerObject: '椤圭洰閮�', - concurrentCount: 50, - priceType: 'currency', - unitPrice: '璐у竵: 60,000/骞�', - quantity: 1, - period: 3, - isPermanent: false, - }, - ], - }, - { - id: '4', - isMainOrder: true, - applyTime: '2025-05-18 10:00:00', - orderNo: '4347442557619205545', - demandSide: '涓浗浜ら�氬缓璁捐偂浠芥湁闄愬叕鍙�', - supplySide: '涓氦鍏矾瑙勫垝璁捐闄㈡湁闄愬叕鍙�', - status: 'WAIT_CONFIRM', - statusName: '寰呬氦鏄撶‘璁�', - subOrders: [ - { - id: '4-1', - isMainOrder: false, - productName: '鍏矾鏁板瓧鍖栨柟妗堣璁$郴缁�', - suiteName: '浼佷笟鍏湁SaaS鐗堣鍙�', - salesForm: '绉熻祦', - accountCount: 100, - customerObject: '浼佷笟', - concurrentCount: 100, - priceType: 'currency', - unitPrice: '璐у竵: 80,000/骞�', - quantity: 1, - period: 1, - isPermanent: false, - }, - ], - }, - { - id: '5', - isMainOrder: true, - applyTime: '2025-05-17 10:00:00', - orderNo: '4347342557619205545', - demandSide: '涓氦绗笁鑸姟宸ョ▼灞�鏈夐檺鍏徃', - supplySide: '涓氦绗笁鑸姟宸ョ▼灞�鏈夐檺鍏徃', - status: 'WAIT_EVALUATE', - statusName: '寰呰瘎浠�', - subOrders: [ - { - id: '5-1', - isMainOrder: false, - productName: '鍩轰簬鏃犱汉鏈虹殑浼佷笟绾у彲瑙嗗寲椤圭洰绠$悊绯荤粺', - suiteName: '椤圭洰鍏湁SaaS鐗堣鍙�', - salesForm: '绉熻祦', - accountCount: 50, - customerObject: '椤圭洰閮�', - concurrentCount: 50, - priceType: 'free', - unitPrice: '鍏嶈垂:/骞�', - quantity: 1, - period: 1, - isPermanent: true, - }, - ], - }, -] +// 璁㈠崟鐘舵�佸璇濇鐩稿叧 +const orderStatusDialogVisible = ref(false) +const currentOrderId = ref<string>('') + +// 浣跨敤宸ヤ綔娴佺▼鎺у埗鍣ㄧ殑鐘舵�佹槧灏� +const statusUiToServer: Record<string, string> = { + WAIT_UPLOAD: OrderStatus.WAIT_UPLOAD, + WAIT_AUTHORIZE: OrderStatus.WAIT_AUTHORIZE, + WAIT_CONFIRM: OrderStatus.WAIT_CONFIRM, + COMPLETED: OrderStatus.COMPLETED, + EVALUATED: OrderStatus.EVALUATED, +} + +const statusServerToUi: Record<string, string> = { + [OrderStatus.WAIT_UPLOAD]: 'WAIT_UPLOAD', + [OrderStatus.WAIT_AUTHORIZE]: 'WAIT_AUTHORIZE', + [OrderStatus.WAIT_CONFIRM]: 'WAIT_CONFIRM', + [OrderStatus.COMPLETED]: 'COMPLETED', + [OrderStatus.EVALUATED]: 'EVALUATED', +} + +const formatDateTime = (val?: string) => { + if (!val) return '' + return val.replace('T', ' ').slice(0, 19) +} + +const normalizePriceType = (val?: string): 'points' | 'currency' | 'agreement' | 'free' => { + if (!val) return 'currency' + const s = String(val) + if (/(绉垎|points)/i.test(s)) return 'points' + if (/(鍗忚|agreement)/i.test(s)) return 'agreement' + if (/(鍏嶈垂|free)/i.test(s)) return 'free' + return 'currency' +} // 鑾峰彇鐘舵�佺被鍨� const getStatusType = (status: string) => { const statusMap: Record<string, 'warning' | 'danger' | 'success' | 'info'> = { - WAIT_REVIEW: 'warning', WAIT_UPLOAD: 'warning', - WAIT_CHECK: 'warning', + WAIT_AUTHORIZE: 'warning', WAIT_CONFIRM: 'warning', - WAIT_EVALUATE: 'warning', + COMPLETED: 'success', + EVALUATED: 'success', } return statusMap[status] || 'info' } @@ -583,26 +473,79 @@ } } -// 鎼滅储澶勭悊 +// 鎼滅储澶勭悊锛堟帴鍏ョ湡瀹炲悗绔級 const handleSearch = async () => { - // 妯℃嫙API璋冪敤锛屽疄闄呴」鐩腑杩欓噷浼氳皟鐢ㄧ湡瀹炵殑API - // const { data } = (await fetchOrderPage({ ...query, pageNo: page.current, pageSize: page.size })) as any - - // 浣跨敤妯℃嫙鏁版嵁 + const payload: any = { + pageNum: page.current, + pageSize: page.size, + productName: query.productName || undefined, + orderId: query.orderNo || undefined, + userId: userStore.getUserId ? Number(userStore.getUserId) : undefined, + } + if (query.status) payload.orderStatus = statusUiToServer[query.status] + if (Array.isArray(query.dateRange) && query.dateRange.length === 2) { + payload.applyTimeStart = query.dateRange[0] + payload.applyTimeEnd = query.dateRange[1] + } + + const res = (await orderApi.getBuyerOrderPage(payload)) as any + const pageData = res?.data + const list: any[] = Array.isArray(pageData?.list) ? pageData.list : [] + page.total = Number(pageData?.total || 0) + + // 骞跺彂鑾峰彇姣忎釜璁㈠崟鐨勮鎯咃紙鐢ㄤ簬鏋勯�犲瓙璁㈠崟琛岋級 + const detailsArr = await Promise.all( + list.map(async (order: any) => { + try { + const detailRes = (await orderApi.getOrderDetail(order.orderId)) as any + return detailRes?.data + } catch (e) { + return null + } + }) + ) + const flatData: any[] = [] - mockData.forEach(order => { - flatData.push(order) - if (order.subOrders) { - order.subOrders.forEach((subOrder: any) => { - flatData.push({ ...subOrder, parentOrder: order }) - }) + list.forEach((order: any, idx: number) => { + const uiStatus = statusServerToUi[order.orderStatus] || 'WAIT_UPLOAD' + const mainRow: any = { + id: order.orderId, + isMainOrder: true, + applyTime: formatDateTime(order.applyTime || ''), + orderNo: order.orderId, + demandSide: '', + supplySide: order.providerName || '', + status: uiStatus, + statusName: order.orderStatus || '', + orderStatus: StatusMapper.toUIStatus(order.orderStatus), // 杞崲涓烘爣鍑嗙姸鎬佹灇涓� } - // 鍦ㄦ瘡涓鍗曞潡鐨勬湯灏炬彃鍏ヤ竴涓垎闅旂┖琛� - flatData.push({ isSpacer: true, isMainOrder: false, parentOrder: order }) + + const detail = detailsArr[idx] + const subOrders: any[] = Array.isArray(detail?.orderDetails) + ? detail.orderDetails.map((d: any, i: number) => ({ + id: `${order.orderId}-${i + 1}`, + isMainOrder: false, + productName: order.productName || '', + suiteName: d.suiteName, + salesForm: d.salesForm, + accountCount: d.accountLimit, + customerObject: d.customerType, + concurrentCount: d.concurrentNodes, + priceType: normalizePriceType(d.priceType), + unitPrice: d.unitPrice, + quantity: d.quantity, + period: d.duration, + })) + : [] + + ;(mainRow as any).subOrders = subOrders + + flatData.push(mainRow) + subOrders.forEach((sub) => flatData.push({ ...sub, parentOrder: mainRow })) + flatData.push({ isSpacer: true, isMainOrder: false, parentOrder: mainRow }) }) - + orderList.value = flatData - page.total = 18 } // 閲嶇疆 @@ -628,6 +571,70 @@ const toEvaluate = (row: any) => router.push({ name: 'tradeOrderEvaluate', params: { id: row.id } }) const toTrack = (row: any) => router.push({ name: 'tradeTrack', params: { id: row.id } }) +// 杩借釜璁㈠崟 - 鏄剧ず璁㈠崟鐘舵�佸璇濇 +const showOrderTrack = (row: any) => { + currentOrderId.value = row.id + orderStatusDialogVisible.value = true +} + +// 鑾峰彇鍙敤鎿嶄綔鍒楄〃 +const getAvailableActions = (order: any) => { + if (!order.orderStatus) return [] + return OrderWorkflowController.getAvailableActions(PageType.BUYER_CENTER, order.orderStatus) +} + +// 澶勭悊鎿嶄綔鐐瑰嚮 +const handleAction = (action: any, order: any) => { + switch (action.type) { + case ActionType.VIEW: + toDetail(order) + break + case ActionType.TRACK: + showOrderTrack(order) + break + case ActionType.UPLOAD_FILE: + toUpload(order) + break + case ActionType.CONFIRM_TRADE: + toConfirm(order) + break + case ActionType.EVALUATE: + toEvaluate(order) + break + case ActionType.CANCEL_ORDER: + cancelOrder(order) + break + default: + console.warn('鏈煡鐨勬搷浣滅被鍨�:', action.type) + } +} + +// 鍙栨秷璁㈠崟 +const cancelOrder = async (order: any) => { + try { + await ElMessageBox.confirm('纭畾瑕佸彇娑堣繖涓鍗曞悧锛熸鎿嶄綔灏嗗垹闄よ鍗曠殑鎵�鏈夌浉鍏充俊鎭紝鍖呮嫭璁㈠崟璇︽儏鍜岄檮浠舵枃浠讹紝涓斾笉鍙仮澶嶃��', '纭鍙栨秷璁㈠崟', { + confirmButtonText: '纭畾鍙栨秷', + cancelButtonText: '鍙栨秷', + type: 'warning', + confirmButtonClass: 'el-button--danger' + }) + + const res = await orderApi.cancelOrder(order.id) + + if (res && res.code === 200) { + ElMessage.success('璁㈠崟鍙栨秷鎴愬姛') + handleSearch() // 鍒锋柊鍒楄〃 + } else { + ElMessage.error(res?.msg || '鍙栨秷璁㈠崟澶辫触') + } + } catch (error) { + if (error !== 'cancel') { + console.error('鍙栨秷璁㈠崟澶辫触:', error) + ElMessage.error('鍙栨秷璁㈠崟澶辫触锛岃閲嶈瘯') + } + } +} + onMounted(handleSearch) </script> diff --git a/src/views/tradeManage/confirm/index.vue b/src/views/tradeManage/confirm/index.vue index cec4efa..9c4a6d7 100644 --- a/src/views/tradeManage/confirm/index.vue +++ b/src/views/tradeManage/confirm/index.vue @@ -170,16 +170,29 @@ {{ formatFileSize(row.size) }} </template> </el-table-column> - <el-table-column label="鎿嶄綔" width="100"> + <el-table-column label="鎿嶄綔" width="180"> <template #default="{ row }"> - <el-button - type="text" - size="small" - class="preview-btn" - @click="handlePreview(row)" - > - 棰勮 - </el-button> + <div class="file-actions"> + <el-button + type="text" + size="small" + class="preview-btn" + @click="handlePreview(row)" + v-if="isPreviewable(row)" + :disabled="!isFileUploaded(row)" + > + 棰勮 + </el-button> + <el-button + type="text" + size="small" + class="download-btn" + @click="handleDownload(row)" + :disabled="!isFileUploaded(row)" + > + 涓嬭浇 + </el-button> + </div> </template> </el-table-column> </el-table> @@ -191,49 +204,24 @@ <div class="title">浜ゆ槗淇℃伅澶囨敞</div> <el-table :data="form.items" border class="mt10 remark-table"> <el-table-column label="璇︽儏" prop="name" min-width="200" /> - <el-table-column label="鎺堟潈寮�濮嬫椂闂�" width="200"> - <template #default="{ row, $index }"> - <el-date-picker - v-model="form.items[$index].start" - type="date" - value-format="YYYY-MM-DD" - placeholder="閫夋嫨鏃ユ湡" - size="small" - style="width: 100%" - /> - </template> - </el-table-column> - <el-table-column label="鎺堟潈缁撴潫鏃堕棿" width="200"> - <template #default="{ row, $index }"> - <div class="end-time-wrapper"> - <el-date-picker - v-model="form.items[$index].end" - type="date" - value-format="YYYY-MM-DD" - placeholder="閫夋嫨鏃ユ湡" - size="small" - style="width: 100%" - :disabled="form.items[$index].forever" - /> - <el-checkbox - v-model="form.items[$index].forever" - class="forever-checkbox" - @change="handleForeverChange($index)" - > - 姘镐箙 - </el-checkbox> - </div> - </template> - </el-table-column> - <el-table-column label="澶囨敞" min-width="300"> - <template #default="{ row, $index }"> - <el-input - v-model="form.items[$index].remark" - placeholder="璇疯緭鍏ュ娉�" - size="small" - /> - </template> - </el-table-column> + <el-table-column label="鎺堟潈寮�濮嬫椂闂�" width="200"> + <template #default="{ row, $index }"> + <span>{{ form.items[$index].start || '-' }}</span> + </template> + </el-table-column> + <el-table-column label="鎺堟潈缁撴潫鏃堕棿" width="200"> + <template #default="{ row, $index }"> + <div class="end-time-wrapper"> + <span v-if="form.items[$index].forever" class="forever-text">姘镐箙</span> + <span v-else>{{ form.items[$index].end || '-' }}</span> + </div> + </template> + </el-table-column> + <el-table-column label="澶囨敞" min-width="300"> + <template #default="{ row, $index }"> + <span>{{ form.items[$index].remark || '-' }}</span> + </template> + </el-table-column> </el-table> <div class="action-buttons"> <el-button @click="goBack">杩斿洖</el-button> @@ -247,11 +235,14 @@ import { onMounted, reactive, ref, computed, type CSSProperties } from 'vue' import { useRoute, useRouter } from 'vue-router' import { Document, User, Goods, List } from '@element-plus/icons-vue' -import { fetchOrderDetail, confirmTrade } from '@/api/tradeManage' -import { ElMessage } from 'element-plus' +import { ElMessage, ElMessageBox } from 'element-plus' +import orderApi from '@/api/orderApi' +import { useUserInfo } from '@/stores/modules/userInfo' +import createAxios from '@/utils/axios' const route = useRoute() const router = useRouter() +const userStore = useUserInfo() const detail = reactive<any>({ items: [] }) const form = reactive<any>({ items: [] }) const fileList = ref<any[]>([]) @@ -277,123 +268,155 @@ return [...detail.items, summaryRow] }) -onMounted(async () => { - // 浣跨敤鍓嶇妯℃嫙鏁版嵁浠ヤ究寮�鍙� UI锛堜笉鏀瑰姩鍚庣鏈嶅姟锛� - const mockDetail = { - orderNo: '4348442557619205545', - resourceTypeName: '杞欢浜у搧', - status: 'WAIT_CONFIRM', - statusName: '寰呯‘璁�', - applyTime: '2025-05-21 10:00:00', - unitName: '涓氦鏂硅繙寤鸿鏈夐檺鍏徃', - userName: '寮犻潤', - userAccount: 'L20159922', - userDept: '淇℃伅涓績', - userPhone: '13800000000', - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - supplier: '涓氦鏂硅繙绉戞妧鏈夐檺鍏徃', - industry: '寤虹瓚宸ョ▼', - projectUnit: '鍦熷缓宸ョ▼', - productType: '杞欢搴�', - productDesc: - '闈㈠悜宸ョ▼椤圭洰鐨勮川閲忓疄娴嬪疄閲忔暟瀛楀寲绠$悊绯荤粺锛屾敮鎸佹爣鍑嗗寲閲囬泦銆佽嚜鍔ㄧ粺璁′笌杩囩▼绠℃帶銆�', - items: [ - { - id: '1', - name: '浼佷笟绉佹湁SaaS鐗堣鍙�', - saleType: '涔版柇', - accountCount: 50, - customerTarget: '浼佷笟', - concurrentNodes: 50, - pricePoint: 50000, - priceCash: 0, - quantity: 1, - period: 0, - }, - { - id: '2', - name: '浼佷笟绉佹湁SaaS鐗圤TA鍗囩骇鏈嶅姟', - saleType: 'OTA鏈嶅姟', - accountCount: 50, - customerTarget: '浼佷笟', - concurrentNodes: 50, - pricePoint: 0, - priceCash: 7500, - quantity: 1, - period: 1, - }, - { - id: '3', - name: '浼佷笟绉佹湁SaaS鐗堢敤鎴峰閲忓寘', - saleType: '绉佹湁澧為噺鍖�', - accountCount: 100, - customerTarget: '浼佷笟', - concurrentNodes: 100, - pricePoint: 0, - priceCash: 0, - priceProtocol: true, - quantity: 1, - period: 1, - }, - { - id: '4', - name: '涓汉鍏湁SaaS鐗堣鍙�', - saleType: '绉佹湁澧為噺鍖�', - accountCount: 50, - customerTarget: '涓汉', - concurrentNodes: 50, - pricePoint: 0, - priceCash: 0, - quantity: 1, - period: 0, - }, - ], - pointTotal: '50,000', - cashTotal: '7,500', - } +// 鐘舵�佹槧灏勶紙鍚庣涓枃 -> 鍓嶇鏋氫妇锛� +const statusServerToUi: Record<string, string> = { + '寰呬笂浼犳枃浠�': 'WAIT_UPLOAD', + '寰呮巿鏉�': 'WAIT_AUTHORIZE', + '寰呬氦鏄撶‘璁�': 'WAIT_CONFIRM', + '宸插畬鎴�': 'COMPLETED', + '宸茶瘎浠�': 'EVALUATED', +} - Object.assign(detail, mockDetail) - form.items = (detail.items || []).map((item: any, index: number) => ({ - name: item.name, - start: '2025-06-01', - end: index === 0 || index === 3 ? '' : '2025-06-01', - forever: index === 0 || index === 3, - remark: index === 3 ? '寮�閫氱鐞嗗憳璐﹀彿1涓�,璐﹀彿admin' : '寮�閫氱鐞嗗憳璐﹀彿1涓�,璐﹀彿admin,鐧诲綍绠$悊鍛樿处鍙峰彲绠$悊鏅�氱敤鎴�' - })) +const formatDateTime = (val?: string) => (val ? val.replace('T', ' ').slice(0, 19) : '') + +const normalizePriceType = (val?: string): 'points' | 'currency' | 'agreement' | 'free' => { + if (!val) return 'currency' + const s = String(val) + if (/(绉垎|points)/i.test(s)) return 'points' + if (/(鍗忚|agreement)/i.test(s)) return 'agreement' + if (/(鍏嶈垂|free)/i.test(s)) return 'free' + return 'currency' +} + +onMounted(async () => { + const orderId = String(route.params.id || '') + if (!orderId) return - // 娣诲姞妯℃嫙鏂囦欢鏁版嵁鐢ㄤ簬灞曠ず - fileList.value = [ - { - name: '绛惧瓧鐩栫珷鏂囦欢.pdf', - size: 2621440, // 2.5MB - uid: '1', - status: 'success' - }, - { - name: 'API Keys.txt', - size: 354, // 354 Bytes - uid: '2', - status: 'success' + try { + const res = (await orderApi.getOrderDetail(orderId)) as any + const data = res?.data || {} + + const statusName: string = data.orderStatus || '' + const uiStatus = statusServerToUi[statusName] || 'INFO' + + // 鏄犲皠璁㈠崟璇︽儏澶撮儴淇℃伅 + const head = { + orderNo: data.orderId, + resourceTypeName: '杞欢浜у搧', + status: uiStatus, + statusName, + applyTime: formatDateTime(data.applyTime), + unitName: data.unitName || '-', + userName: data.userName || '-', + userAccount: data.userAccount || '-', + userDept: data.userDept || '-', + userPhone: data.userPhone || '-', + productName: data.productName || '-', + supplier: data.providerName || '-', + industry: data.industry || '-', + projectUnit: data.projectUnit || '-', + productType: data.productType || '-', + productDesc: data.productDesc || '-', } - ] - - // 娉ㄩ噴鎺夊師鏈夌殑API璋冪敤锛屼娇鐢ㄦā鎷熸暟鎹� - // const { data } = (await fetchOrderDetail({ id: route.params.id })) as any - // Object.assign(detail, data || {}) - // form.items = (detail.items || []).map(() => ({ start: '', end: '', forever: false, remark: '' })) + + // 鏄庣粏椤规槧灏� + const items: any[] = Array.isArray(data.orderDetails) + ? data.orderDetails.map((d: any, idx: number) => { + const pt = normalizePriceType(d.priceType) + return { + id: String(d.id ?? idx + 1), + name: d.suiteName, + saleType: d.salesForm, + accountCount: d.accountLimit, + customerTarget: d.customerType, + concurrentNodes: d.concurrentNodes, + pricePoint: pt === 'points' ? Number(d.unitPrice || 0) : 0, + priceCash: pt === 'currency' ? Number(d.unitPrice || 0) : 0, + priceProtocol: pt === 'agreement', + quantity: Number(d.quantity || 0), + period: Number(d.duration || 0), + remarks: d.remarks || '', // 鏂板 remarks 瀛楁 + } + }) + : [] + + // 姹囨�伙紙绠�鍗曠浉鍔狅細鍗曚环*鏁伴噺锛� + const pointTotalNum = items.reduce((sum, it) => sum + Number(it.pricePoint || 0) * Number(it.quantity || 0), 0) + const cashTotalNum = items.reduce((sum, it) => sum + Number(it.priceCash || 0) * Number(it.quantity || 0), 0) + + Object.assign(detail, head, { + items, + pointTotal: pointTotalNum.toLocaleString(), + cashTotal: cashTotalNum.toLocaleString(), + }) + + // 鍒濆鍖栬〃鍗曟暟鎹� + form.items = (detail.items || []).map((item: any, index: number) => { + // 璁$畻鎺堟潈缁撴潫鏃堕棿 + let endDate = '' + if (item.period > 0) { + // 濡傛灉鏈夋湡闄愶紝璁$畻缁撴潫鏃堕棿 + const startDate = new Date(data.applyTime || new Date()) + const endDateObj = new Date(startDate) + endDateObj.setFullYear(endDateObj.getFullYear() + item.period) + endDate = endDateObj.toISOString().split('T')[0] // 鏍煎紡鍖栦负 YYYY-MM-DD + } + + return { + name: item.name, + start: data.applyTime ? data.applyTime.split('T')[0] : '', // 浣跨敤璁㈠崟鐢宠鏃堕棿 + end: endDate, + forever: item.period === 0, // 鏈熼檺涓�0鏃惰缃负姘镐箙 + remark: item.remarks || '' // 浣跨敤濂椾欢淇℃伅涓殑remarks瀛楁 + } + }) + + // 鑾峰彇璁㈠崟闄勪欢鍒楄〃锛堝鏋滄湁鐨勮瘽锛� + if (data.attachments && Array.isArray(data.attachments)) { + fileList.value = data.attachments.map((file: any) => ({ + name: file.fileName || file.originalName, + size: file.fileSize || 0, + uid: file.id, + status: 'success', + url: file.fileUrl, + uploaded: true + })) + } else { + fileList.value = [] + } + } catch (error) { + console.error('鑾峰彇璁㈠崟璇︽儏澶辫触:', error) + ElMessage.error('鑾峰彇璁㈠崟璇︽儏澶辫触') + } }) const goBack = () => router.back() const submit = async () => { - // 妯℃嫙鎻愪氦鎴愬姛鍝嶅簲 - const mockResponse = { code: 200 } - - // 娉ㄩ噴鎺夊師鏈夌殑API璋冪敤锛屼娇鐢ㄦā鎷熸暟鎹� - // const { code } = (await confirmTrade({ id: route.params.id, ...form })) as any - - if (mockResponse.code === 200) { - ElMessage.success('鎻愪氦鎴愬姛') - router.back() + try { + const orderId = String(route.params.id || '') + const userId = userStore.getUserId ? Number(userStore.getUserId) : undefined + + if (!orderId || !userId) { + ElMessage.error('璁㈠崟ID鎴栫敤鎴稩D涓嶈兘涓虹┖') + return + } + + // 纭鎿嶄綔 + await ElMessageBox.confirm('纭畾瑕佺‘璁や氦鏄擄紵', '纭鎿嶄綔', { + confirmButtonText: '纭畾', + cancelButtonText: '鍙栨秷', + type: 'warning' + }) + + // 鏇存柊璁㈠崟鐘舵�佽繘鍏ヤ笅涓�涓姸鎬� + await orderApi.updateOrderStatusToNext(orderId) + ElMessage.success('浜ゆ槗纭鎴愬姛') + router.back() + } catch (error) { + if (error !== 'cancel') { + console.error('浜ゆ槗纭澶辫触:', error) + ElMessage.error('浜ゆ槗纭澶辫触') + } } } @@ -480,17 +503,202 @@ return parseFloat((size / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; -// 鏂囦欢棰勮澶勭悊 -const handlePreview = (file: any) => { - ElMessage.info(`棰勮鏂囦欢锛�${file.name}`) +// 鍒ゆ柇鏂囦欢鏄惁鍙瑙� +const isPreviewable = (file: any) => { + // 棣栧厛妫�鏌IME绫诲瀷 + const previewableTypes = [ + 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/webp', + 'text/plain', 'text/html', 'text/css', 'text/javascript', + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx + ] + + // 濡傛灉MIME绫诲瀷鍖归厤锛岀洿鎺ヨ繑鍥瀟rue + if (previewableTypes.includes(file.type || '')) { + return true + } + + // 濡傛灉MIME绫诲瀷涓虹┖鎴栦笉鍖归厤锛屾牴鎹枃浠舵墿灞曞悕鍒ゆ柇 + const fileName = file.name || '' + const fileExtension = fileName.toLowerCase().split('.').pop() + + const previewableExtensions = [ + 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', + 'txt', 'html', 'htm', 'css', 'js', + 'pdf', + 'docx', 'xlsx', 'pptx' + ] + + return previewableExtensions.includes(fileExtension) } -// 姘镐箙澶嶉�夋鍙樺寲澶勭悊 -const handleForeverChange = (index: number) => { - if (form.items[index].forever) { - form.items[index].end = '' +// 鍒ゆ柇鏂囦欢鏄惁宸蹭笂浼犳垚鍔� +const isFileUploaded = (file: any) => { + // 鏂囦欢鏈塽rl涓旂姸鎬佷负success琛ㄧず宸蹭笂浼犳垚鍔� + return file.url && (file.status === 'success' || file.uploaded) +} + +// 鏂囦欢棰勮 +const handlePreview = async (file: any) => { + if (!file.url) { + ElMessage.warning('鏂囦欢閾炬帴涓嶅瓨鍦�') + return + } + + // 鑾峰彇鏂囦欢鎵╁睍鍚� + const fileName = file.name || '' + const fileExtension = fileName.toLowerCase().split('.').pop() + + let previewUrl = file.url + + // 濡傛灉鏂囦欢瀛樺偍鍦∕inIO锛屼紭鍏堜娇鐢ㄩ瑙圲RL + if (file.url.includes('order-attachments')) { + try { + // 棣栧厛灏濊瘯鑾峰彇棰勮URL + const previewResponse = await createAxios({ + url: `/admin/file/preview`, + method: 'GET', + params: { + fileName: file.url + } + }) + + console.log('棰勮URL鍝嶅簲:', previewResponse) + + // 妫�鏌ュ搷搴旀牸寮� + const responseData = previewResponse as any + if (responseData && responseData.code === 200 && responseData.data) { + previewUrl = responseData.data + console.log('浣跨敤棰勮URL:', previewUrl) + } else { + console.log('棰勮URL鑾峰彇澶辫触锛屼娇鐢ㄤ笅杞芥柟寮�') + // 濡傛灉棰勮URL鑾峰彇澶辫触锛屽洖閫�鍒颁笅杞芥柟寮� + const response = await createAxios({ + url: `/admin/file/download`, + method: 'GET', + responseType: 'blob', + params: { + fileName: file.url, + originalName: file.name + } + }) + + // 鍒涘缓棰勮URL + const blob = new Blob([response as any]) + previewUrl = window.URL.createObjectURL(blob) + console.log('浣跨敤Blob URL:', previewUrl) + } + } catch (error) { + console.error('鑾峰彇鏂囦欢澶辫触:', error) + ElMessage.error('鑾峰彇鏂囦欢澶辫触锛屾棤娉曢瑙�') + return + } + } + + // 鍥剧墖鏂囦欢鐩存帴鍦ㄦ柊绐楀彛鎵撳紑 + if ((file.type && file.type.startsWith('image/')) || + ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension)) { + console.log('棰勮鍥剧墖鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // PDF鏂囦欢鍦ㄦ柊绐楀彛鎵撳紑 + if (file.type === 'application/pdf' || fileExtension === 'pdf') { + console.log('棰勮PDF鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // 鏂囨湰鏂囦欢鍦ㄦ柊绐楀彛鎵撳紑 + if ((file.type && file.type.startsWith('text/')) || + ['txt', 'html', 'htm', 'css', 'js'].includes(fileExtension)) { + console.log('棰勮鏂囨湰鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // Office鏂囨。鍜屽叾浠栨枃浠剁被鍨嬶紝灏濊瘯鍦ㄦ柊绐楀彛鎵撳紑 + try { + console.log('棰勮鍏朵粬鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + } catch (error) { + console.error('棰勮澶辫触:', error) + ElMessage.error('棰勮澶辫触锛岃涓嬭浇鍚庢煡鐪�') } } + +// 鏂囦欢涓嬭浇 +const handleDownload = async (file: any) => { + if (!file.url) { + ElMessage.warning('鏂囦欢閾炬帴涓嶅瓨鍦�') + return + } + + console.log('寮�濮嬩笅杞芥枃浠�:', file.name, 'URL:', file.url) + + try { + // 濡傛灉鏂囦欢瀛樺偍鍦∕inIO锛屼娇鐢ㄥ悗绔洿鎺ヤ笅杞紸PI + if (file.url.includes('order-attachments')) { + console.log('浣跨敤MinIO涓嬭浇API') + + // 浣跨敤axios閫氳繃浠g悊璁块棶鍚庣API + const response = await createAxios({ + url: `/admin/file/download`, + method: 'GET', + responseType: 'blob', + params: { + fileName: file.url, + originalName: file.name + } + }) + + console.log('涓嬭浇鍝嶅簲:', response) + + // 鍒涘缓涓嬭浇閾炬帴 + const blob = new Blob([response as any]) + const downloadUrl = window.URL.createObjectURL(blob) + + console.log('鍒涘缓涓嬭浇閾炬帴:', downloadUrl) + + const link = document.createElement('a') + link.href = downloadUrl + link.download = file.name || 'download' + link.target = '_blank' + link.rel = 'noopener noreferrer' + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + // 娓呯悊URL瀵硅薄 + window.URL.revokeObjectURL(downloadUrl) + + ElMessage.success('鏂囦欢涓嬭浇鎴愬姛') + } else { + console.log('浣跨敤鐩存帴URL涓嬭浇') + // 鍏朵粬鎯呭喌鐩存帴浣跨敤鍘烾RL + const link = document.createElement('a') + link.href = file.url + link.download = file.name || 'download' + link.target = '_blank' + link.rel = 'noopener noreferrer' + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + ElMessage.success('寮�濮嬩笅杞芥枃浠�') + } + } catch (error) { + console.error('涓嬭浇澶辫触:', error) + ElMessage.error('涓嬭浇澶辫触锛岃閲嶈瘯') + } +} + + // 鐥囩粨涓庝慨澶嶈鏄庯細 // 1) Element Plus 鐨� el-table 瀛愬垪 width 鐧惧垎姣旀槸鐩稿浜庤〃鏍煎鍣ㄧ殑鍍忕礌瀹藉害璁$畻锛屼絾鍙湁鍦ㄨ〃鏍煎鍣ㄦ湁鏄庣‘瀹藉害鏃舵墠鐢熸晥銆� @@ -694,6 +902,36 @@ text-align: left !important; } +/* 鏂囦欢鎿嶄綔鎸夐挳鏍峰紡 */ +.file-actions { + display: flex; + gap: 8px; + align-items: center; + justify-content: center; + + .preview-btn { + color: #409eff; + &:hover { + text-decoration: underline; + } + &:disabled { + color: #c0c4cc; + cursor: not-allowed; + } + } + + .download-btn { + color: #67c23a; + &:hover { + text-decoration: underline; + } + &:disabled { + color: #c0c4cc; + cursor: not-allowed; + } + } +} + /* 浜ゆ槗淇℃伅澶囨敞琛ㄦ牸鏍峰紡 */ .remark-table { width: 100%; @@ -701,8 +939,9 @@ display: flex; align-items: center; gap: 10px; - .forever-checkbox { - margin-left: 10px; + .forever-text { + color: #409eff; + font-weight: 500; } } } diff --git a/src/views/tradeManage/detail/index.vue b/src/views/tradeManage/detail/index.vue index abfcfe1..098fffc 100644 --- a/src/views/tradeManage/detail/index.vue +++ b/src/views/tradeManage/detail/index.vue @@ -143,7 +143,87 @@ </div> <!-- 绉婚櫎鍘熸潵鐨勮〃鏍煎簳閮ㄤ俊鎭紝鍥犱负宸茬粡绉诲埌琛ㄦ牸鏈�鍚庝竴琛� --> - </el-card> + + <!-- 浜ゆ槗鏂囦欢锛堢Щ鍔ㄥ埌璁㈠崟璇︽儏涓嬮潰锛屽悓涓�鍗$墖鍐咃級 --> + <div class="file-section" v-if="isAgreementOrder && fileList.length > 0"> + <el-table + :data="fileList" + border + class="file-table" + :header-cell-style="fileTableHeaderStyle" + :cell-style="fileTableCellStyle" + > + <el-table-column min-width="200"> + <template #header> + <el-icon class="header-icon"><Document /></el-icon> + <span>浜ゆ槗鏂囦欢</span> + </template> + <template #default="{ row }"> + <div class="file-name"> + <el-icon class="file-icon"><Document /></el-icon> + <span>{{ row.name }}</span> + </div> + </template> + </el-table-column> + <el-table-column label="鏂囦欢澶у皬" prop="size" width="120"> + <template #default="{ row }"> + {{ formatFileSize(row.size) }} + </template> + </el-table-column> + <el-table-column label="鎿嶄綔" width="180"> + <template #default="{ row }"> + <div class="file-actions"> + <el-button + type="text" + size="small" + class="preview-btn" + @click="handlePreview(row)" + v-if="isPreviewable(row)" + :disabled="!isFileUploaded(row)" + > + 棰勮 + </el-button> + <el-button + type="text" + size="small" + class="download-btn" + @click="handleDownload(row)" + :disabled="!isFileUploaded(row)" + > + 涓嬭浇 + </el-button> + </div> + </template> + </el-table-column> + </el-table> + </div> + </el-card> + + <!-- 浜ゆ槗淇℃伅澶囨敞锛堜粎鍦ㄨ鍗曠姸鎬佷负"寰呬氦鏄撶‘璁�"鎴栦箣鍚庢椂鏄剧ず锛� --> + <el-card class="mt15" shadow="never" v-if="shouldShowRemark"> + <div class="title">浜ゆ槗淇℃伅澶囨敞</div> + <el-table :data="remarkItems" border class="mt10 remark-table"> + <el-table-column label="璇︽儏" prop="name" min-width="200" /> + <el-table-column label="鎺堟潈寮�濮嬫椂闂�" width="200"> + <template #default="{ row, $index }"> + <span>{{ remarkItems[$index].start || '-' }}</span> + </template> + </el-table-column> + <el-table-column label="鎺堟潈缁撴潫鏃堕棿" width="200"> + <template #default="{ row, $index }"> + <div class="end-time-wrapper"> + <span v-if="remarkItems[$index].forever" class="forever-text">姘镐箙</span> + <span v-else>{{ remarkItems[$index].end || '-' }}</span> + </div> + </template> + </el-table-column> + <el-table-column label="澶囨敞" min-width="300"> + <template #default="{ row, $index }"> + <span>{{ remarkItems[$index].remark || '-' }}</span> + </template> + </el-table-column> + </el-table> + </el-card> <!-- 瀹℃壒杩借釜 --> <el-card class="mt15" shadow="never" v-if="detail.records?.length"> @@ -213,6 +293,9 @@ import { onMounted, reactive, ref, computed, type CSSProperties } from 'vue' import { Document, User, Goods, List } from '@element-plus/icons-vue' import { useRoute, useRouter } from 'vue-router' +import { ElMessage } from 'element-plus' +import orderApi from '@/api/orderApi' +import createAxios from '@/utils/axios' const route = useRoute() const router = useRouter() @@ -221,6 +304,21 @@ const activeTab = ref('records') const labelStyle = { width: '180px', maxWidth: '180px' } const contentStyle = { width: 'calc(50% - 180px)' } + +// 鏂囦欢鐩稿叧 +const fileList = ref<any[]>([]) +const isAgreementOrder = ref(false) + +// 浜ゆ槗淇℃伅澶囨敞 +const remarkItems = ref<any[]>([]) +const shouldShowRemark = computed(() => { + const statusName = detail.statusName || '' + // 褰撹鍗曠姸鎬佷负"寰呬氦鏄撶‘璁�"鎴栦箣鍚庣殑鐘舵�佹椂鏄剧ず浜ゆ槗淇℃伅澶囨敞 + // "寰呬笂浼犳枃浠�"鍜�"寰呮巿鏉�"鐘舵�佷笉鏄剧ず浜ゆ槗淇℃伅澶囨敞 + const statusOrder = ['WAIT_UPLOAD', 'WAIT_AUTHORIZE', 'WAIT_CONFIRM', 'COMPLETED', 'EVALUATED'] + const currentStatus = statusServerToUi[statusName] + return currentStatus && statusOrder.indexOf(currentStatus) >= statusOrder.indexOf('WAIT_CONFIRM') +}) // 璁$畻琛ㄦ牸鏁版嵁锛屾坊鍔犳眹鎬昏 const tableData = computed(() => { @@ -240,120 +338,138 @@ return [...detail.items, summaryRow] }) -onMounted(async () => { - // 浣跨敤鍓嶇妯℃嫙鏁版嵁浠ヤ究寮�鍙� UI锛堜笉鏀瑰姩鍚庣鏈嶅姟锛� - const mockDetail = { - orderNo: '4348442557619205545', - resourceTypeName: '杞欢浜у搧', - status: 'WAIT_APPROVAL', - statusName: '寰呭鏍�', - applyTime: '2025-05-21 10:00:00', - unitName: '涓氦鏂硅繙寤鸿鏈夐檺鍏徃', - userName: '寮犻潤', - userAccount: 'L20159922', - userDept: '淇℃伅涓績', - userPhone: '13800000000', - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - supplier: '涓氦鏂硅繙绉戞妧鏈夐檺鍏徃', - industry: '寤虹瓚宸ョ▼', - projectUnit: '鍦熷缓宸ョ▼', - productType: '杞欢搴�', - productDesc: - '闈㈠悜宸ョ▼椤圭洰鐨勮川閲忓疄娴嬪疄閲忔暟瀛楀寲绠$悊绯荤粺锛屾敮鎸佹爣鍑嗗寲閲囬泦銆佽嚜鍔ㄧ粺璁′笌杩囩▼绠℃帶銆�', - items: [ - { - id: '1', - name: '浼佷笟绉佹湁SaaS鐗堣鍙�', - saleType: '涔版柇', - accountCount: 50, - customerTarget: '浼佷笟', - concurrentNodes: 50, - pricePoint: 50000, - priceCash: 0, - quantity: 1, - period: 0, - }, - { - id: '2', - name: '浼佷笟绉佹湁SaaS鐗圤TA鍗囩骇鏈嶅姟', - saleType: 'OTA鏈嶅姟', - accountCount: 50, - customerTarget: '浼佷笟', - concurrentNodes: 50, - pricePoint: 0, - priceCash: 7500, - quantity: 1, - period: 1, - }, - { - id: '3', - name: '浼佷笟绉佹湁SaaS鐗堢敤鎴峰閲忓寘', - saleType: '绉佹湁澧為噺鍖�', - accountCount: 100, - customerTarget: '浼佷笟', - concurrentNodes: 100, - pricePoint: 0, - priceCash: 0, - priceProtocol: true, - quantity: 1, - period: 1, - }, - { - id: '4', - name: '涓汉鍏湁SaaS鐗堣鍙�', - saleType: '绉佹湁澧為噺鍖�', - accountCount: 50, - customerTarget: '涓汉', - concurrentNodes: 50, - pricePoint: 0, - priceCash: 0, - quantity: 1, - period: 0, - }, - ], - pointTotal: '50,000', - cashTotal: '7,500', - records: [ - { - nodeName: '鎻愪氦浜�', - approver: '寮犻潤', - department: '闂ㄦ埛绯荤粺涓存椂缁�', - startTime: '2025-05-21 16:08:09', - endTime: '2025-05-21 16:08:09', - statusName: '宸插畬鎴�', - opinion: '', - }, - { - nodeName: '浜ゆ槗瀹℃壒', - approver: '鍠讳細宄�', - department: '闂ㄦ埛绯荤粺涓存椂缁�', - startTime: '2025-05-21 16:08:09', - endTime: '', - statusName: '瀹¢槄涓�', - opinion: '', - }, - ], - nodes: [ - { - nodeName: '鎻愪氦鐢宠', - nodeType: '寮�濮嬭妭鐐�', - handler: '寮犻潤', - department: '闂ㄦ埛绯荤粺涓存椂缁�', - status: 'completed', - statusName: '宸插畬鎴�', - }, - { - nodeName: '浜ゆ槗瀹℃壒', - nodeType: '瀹℃壒鑺傜偣', - handler: '鍠讳細宄�', - department: '闂ㄦ埛绯荤粺涓存椂缁�', - status: 'processing', - statusName: '澶勭悊涓�', - }, - ], - } +// 鐘舵�佹槧灏勶紙鍚庣涓枃 -> 鍓嶇鏋氫妇锛� +const statusServerToUi: Record<string, string> = { + '寰呬笂浼犳枃浠�': 'WAIT_UPLOAD', + '寰呮巿鏉�': 'WAIT_AUTHORIZE', + '寰呬氦鏄撶‘璁�': 'WAIT_CONFIRM', + '宸插畬鎴�': 'COMPLETED', + '宸茶瘎浠�': 'EVALUATED', +} - Object.assign(detail, mockDetail) +const formatDateTime = (val?: string) => (val ? val.replace('T', ' ').slice(0, 19) : '') + +const normalizePriceType = (val?: string): 'points' | 'currency' | 'agreement' | 'free' => { + if (!val) return 'currency' + const s = String(val) + if (/(绉垎|points)/i.test(s)) return 'points' + if (/(鍗忚|agreement)/i.test(s)) return 'agreement' + if (/(鍏嶈垂|free)/i.test(s)) return 'free' + return 'currency' +} + +onMounted(async () => { + const orderId = String(route.params.id || '') + if (!orderId) return + + try { + // 骞惰鑾峰彇璁㈠崟璇︽儏鍜屽崗璁被鍨嬫鏌� + const [orderRes, agreementRes] = await Promise.all([ + orderApi.getOrderDetail(orderId), + orderApi.checkAgreementPriceType(orderId) + ]) + + const res = orderRes as any + const data = res?.data || {} + + // 璁剧疆鏄惁涓哄崗璁鍗� + const agreementResult = agreementRes as any + isAgreementOrder.value = agreementResult?.data === true + + const statusName: string = data.orderStatus || '' + const uiStatus = statusServerToUi[statusName] || 'INFO' + + // 鏄犲皠璁㈠崟璇︽儏澶撮儴淇℃伅 + const head = { + orderNo: data.orderId, + resourceTypeName: '杞欢浜у搧', + status: uiStatus, + statusName, + applyTime: formatDateTime(data.applyTime), + unitName: data.unitName || '-', + userName: data.userName || '-', + userAccount: data.userAccount || '-', + userDept: data.userDept || '-', + userPhone: data.userPhone || '-', + productName: data.productName || '-', + supplier: data.providerName || '-', + industry: data.industry || '-', + projectUnit: data.projectUnit || '-', + productType: data.productType || '-', + productDesc: data.productDesc || '-', + } + + // 鏄庣粏椤规槧灏� + const items: any[] = Array.isArray(data.orderDetails) + ? data.orderDetails.map((d: any, idx: number) => { + const pt = normalizePriceType(d.priceType) + return { + id: String(d.id ?? idx + 1), + name: d.suiteName, + saleType: d.salesForm, + accountCount: d.accountLimit, + customerTarget: d.customerType, + concurrentNodes: d.concurrentNodes, + pricePoint: pt === 'points' ? Number(d.unitPrice || 0) : 0, + priceCash: pt === 'currency' ? Number(d.unitPrice || 0) : 0, + priceProtocol: pt === 'agreement', + quantity: Number(d.quantity || 0), + period: Number(d.duration || 0), + remarks: d.remarks || '', // 鏂板 remarks 瀛楁 + } + }) + : [] + + // 姹囨�伙紙绠�鍗曠浉鍔狅細鍗曚环*鏁伴噺锛� + const pointTotalNum = items.reduce((sum, it) => sum + Number(it.pricePoint || 0) * Number(it.quantity || 0), 0) + const cashTotalNum = items.reduce((sum, it) => sum + Number(it.priceCash || 0) * Number(it.quantity || 0), 0) + + Object.assign(detail, head, { + items, + pointTotal: pointTotalNum.toLocaleString(), + cashTotal: cashTotalNum.toLocaleString(), + records: [], + nodes: [], + }) + + // 鍒濆鍖栦氦鏄撲俊鎭娉ㄦ暟鎹� + remarkItems.value = (detail.items || []).map((item: any, index: number) => { + // 璁$畻鎺堟潈缁撴潫鏃堕棿 + let endDate = '' + if (item.period > 0) { + // 濡傛灉鏈夋湡闄愶紝璁$畻缁撴潫鏃堕棿 + const startDate = new Date(data.applyTime || new Date()) + const endDateObj = new Date(startDate) + endDateObj.setFullYear(endDateObj.getFullYear() + item.period) + endDate = endDateObj.toISOString().split('T')[0] // 鏍煎紡鍖栦负 YYYY-MM-DD + } + + return { + name: item.name, + start: data.applyTime ? data.applyTime.split('T')[0] : '', // 浣跨敤璁㈠崟鐢宠鏃堕棿 + end: endDate, + forever: item.period === 0, // 鏈熼檺涓�0鏃惰缃负姘镐箙 + remark: item.remarks || '' // 浣跨敤濂椾欢淇℃伅涓殑remarks瀛楁 + } + }) + + // 鑾峰彇璁㈠崟闄勪欢鍒楄〃锛堝鏋滄湁鐨勮瘽锛� + if (data.attachments && Array.isArray(data.attachments)) { + fileList.value = data.attachments.map((file: any) => ({ + name: file.fileName || file.originalName, + size: file.fileSize || 0, + uid: file.id, + status: 'success', + url: file.fileUrl, + uploaded: true + })) + } else { + fileList.value = [] + } + } catch (error) { + console.error('鑾峰彇璁㈠崟璇︽儏澶辫触:', error) + ElMessage.error('鑾峰彇璁㈠崟璇︽儏澶辫触') + } }) // 涓庡垪琛ㄩ〉淇濇寔涓�鑷寸殑鐘舵�佺被鍨嬫槧灏勶紙UI灞曠ず鐢級 @@ -411,6 +527,15 @@ } const recordTableCellStyle: CSSProperties = { fontSize: '12px' } +// 鏂囦欢鍒楄〃琛ㄦ牸琛ㄥご鏂囧瓧灞呬腑锛屼絾绗竴鍒楃殑"浜ゆ槗鏂囦欢"鏂囧瓧闈犲乏瀵归綈 +const fileTableHeaderStyle: CSSProperties = { + textAlign: 'center', + fontSize: '14px', + background: '#f3f6fb' +} +// 鏂囦欢鍒楄〃琛ㄦ牸琛ㄤ綋鏂囧瓧澶у皬 +const fileTableCellStyle: CSSProperties = { fontSize: '12px' } + // 涓烘眹鎬昏娣诲姞鐗规畩鏍峰紡绫诲悕 const getRowClassName = ({ row }: { row: any }) => { return row.isSummary ? 'summary-row' : '' @@ -441,6 +566,210 @@ // 杩斿洖鎸夐挳 const goBack = () => router.back() + +// 鏂囦欢澶у皬鏍煎紡鍖� +const formatFileSize = (size: number) => { + if (!size || size === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(size) / Math.log(k)); + return parseFloat((size / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +}; + +// 鍒ゆ柇鏂囦欢鏄惁鍙瑙� +const isPreviewable = (file: any) => { + // 棣栧厛妫�鏌IME绫诲瀷 + const previewableTypes = [ + 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/webp', + 'text/plain', 'text/html', 'text/css', 'text/javascript', + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx + ] + + // 濡傛灉MIME绫诲瀷鍖归厤锛岀洿鎺ヨ繑鍥瀟rue + if (previewableTypes.includes(file.type || '')) { + return true + } + + // 濡傛灉MIME绫诲瀷涓虹┖鎴栦笉鍖归厤锛屾牴鎹枃浠舵墿灞曞悕鍒ゆ柇 + const fileName = file.name || '' + const fileExtension = fileName.toLowerCase().split('.').pop() + + const previewableExtensions = [ + 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', + 'txt', 'html', 'htm', 'css', 'js', + 'pdf', + 'docx', 'xlsx', 'pptx' + ] + + return previewableExtensions.includes(fileExtension) +} + +// 鍒ゆ柇鏂囦欢鏄惁宸蹭笂浼犳垚鍔� +const isFileUploaded = (file: any) => { + // 鏂囦欢鏈塽rl涓旂姸鎬佷负success琛ㄧず宸蹭笂浼犳垚鍔� + return file.url && (file.status === 'success' || file.uploaded) +} + +// 鏂囦欢棰勮 +const handlePreview = async (file: any) => { + if (!file.url) { + ElMessage.warning('鏂囦欢閾炬帴涓嶅瓨鍦�') + return + } + + // 鑾峰彇鏂囦欢鎵╁睍鍚� + const fileName = file.name || '' + const fileExtension = fileName.toLowerCase().split('.').pop() + + let previewUrl = file.url + + // 濡傛灉鏂囦欢瀛樺偍鍦∕inIO锛屼紭鍏堜娇鐢ㄩ瑙圲RL + if (file.url.includes('order-attachments')) { + try { + // 棣栧厛灏濊瘯鑾峰彇棰勮URL + const previewResponse = await createAxios({ + url: `/admin/file/preview`, + method: 'GET', + params: { + fileName: file.url + } + }) + + console.log('棰勮URL鍝嶅簲:', previewResponse) + + // 妫�鏌ュ搷搴旀牸寮� + const responseData = previewResponse as any + if (responseData && responseData.code === 200 && responseData.data) { + previewUrl = responseData.data + console.log('浣跨敤棰勮URL:', previewUrl) + } else { + console.log('棰勮URL鑾峰彇澶辫触锛屼娇鐢ㄤ笅杞芥柟寮�') + // 濡傛灉棰勮URL鑾峰彇澶辫触锛屽洖閫�鍒颁笅杞芥柟寮� + const response = await createAxios({ + url: `/admin/file/download`, + method: 'GET', + responseType: 'blob', + params: { + fileName: file.url, + originalName: file.name + } + }) + + // 鍒涘缓棰勮URL + const blob = new Blob([response as any]) + previewUrl = window.URL.createObjectURL(blob) + console.log('浣跨敤Blob URL:', previewUrl) + } + } catch (error) { + console.error('鑾峰彇鏂囦欢澶辫触:', error) + ElMessage.error('鑾峰彇鏂囦欢澶辫触锛屾棤娉曢瑙�') + return + } + } + + // 鍥剧墖鏂囦欢鐩存帴鍦ㄦ柊绐楀彛鎵撳紑 + if ((file.type && file.type.startsWith('image/')) || + ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension)) { + console.log('棰勮鍥剧墖鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // PDF鏂囦欢鍦ㄦ柊绐楀彛鎵撳紑 + if (file.type === 'application/pdf' || fileExtension === 'pdf') { + console.log('棰勮PDF鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // 鏂囨湰鏂囦欢鍦ㄦ柊绐楀彛鎵撳紑 + if ((file.type && file.type.startsWith('text/')) || + ['txt', 'html', 'htm', 'css', 'js'].includes(fileExtension)) { + console.log('棰勮鏂囨湰鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // Office鏂囨。鍜屽叾浠栨枃浠剁被鍨嬶紝灏濊瘯鍦ㄦ柊绐楀彛鎵撳紑 + try { + console.log('棰勮鍏朵粬鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + } catch (error) { + console.error('棰勮澶辫触:', error) + ElMessage.error('棰勮澶辫触锛岃涓嬭浇鍚庢煡鐪�') + } +} + +// 鏂囦欢涓嬭浇 +const handleDownload = async (file: any) => { + if (!file.url) { + ElMessage.warning('鏂囦欢閾炬帴涓嶅瓨鍦�') + return + } + + console.log('寮�濮嬩笅杞芥枃浠�:', file.name, 'URL:', file.url) + + try { + // 濡傛灉鏂囦欢瀛樺偍鍦∕inIO锛屼娇鐢ㄥ悗绔洿鎺ヤ笅杞紸PI + if (file.url.includes('order-attachments')) { + console.log('浣跨敤MinIO涓嬭浇API') + + // 浣跨敤axios閫氳繃浠g悊璁块棶鍚庣API + const response = await createAxios({ + url: `/admin/file/download`, + method: 'GET', + responseType: 'blob', + params: { + fileName: file.url, + originalName: file.name + } + }) + + console.log('涓嬭浇鍝嶅簲:', response) + + // 鍒涘缓涓嬭浇閾炬帴 + const blob = new Blob([response as any]) + const downloadUrl = window.URL.createObjectURL(blob) + + console.log('鍒涘缓涓嬭浇閾炬帴:', downloadUrl) + + const link = document.createElement('a') + link.href = downloadUrl + link.download = file.name || 'download' + link.target = '_blank' + link.rel = 'noopener noreferrer' + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + // 娓呯悊URL瀵硅薄 + window.URL.revokeObjectURL(downloadUrl) + + ElMessage.success('鏂囦欢涓嬭浇鎴愬姛') + } else { + console.log('浣跨敤鐩存帴URL涓嬭浇') + // 鍏朵粬鎯呭喌鐩存帴浣跨敤鍘烾RL + const link = document.createElement('a') + link.href = file.url + link.download = file.name || 'download' + link.target = '_blank' + link.rel = 'noopener noreferrer' + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + ElMessage.success('寮�濮嬩笅杞芥枃浠�') + } + } catch (error) { + console.error('涓嬭浇澶辫触:', error) + ElMessage.error('涓嬭浇澶辫触锛岃閲嶈瘯') + } +} // 鍗曞厓鏍煎悎骞舵柟娉� const arraySpanMethod = ({ row, column, rowIndex, columnIndex }: any) => { @@ -645,6 +974,81 @@ margin: 0 10px; } } + +/* 鏂囦欢鍒楄〃琛ㄦ牸鏍峰紡 */ +.file-section { + margin-top: 15px; + .file-title { + font-weight: 600; + margin-bottom: 10px; + } + .file-table { + width: 100%; + .file-name { + display: flex; + align-items: center; + .file-icon { + margin-right: 8px; + color: #409eff; + } + } + .preview-btn { + color: #409eff; + &:hover { + text-decoration: underline; + } + } + } +} + +/* 鏂囦欢琛ㄦ牸琛ㄥご绗竴鍒�"浜ゆ槗鏂囦欢"鏂囧瓧闈犲乏瀵归綈 */ +.file-table :deep(.el-table__header-wrapper thead tr th:first-child) { + text-align: left !important; +} + +/* 鏂囦欢鎿嶄綔鎸夐挳鏍峰紡 */ +.file-actions { + display: flex; + gap: 8px; + align-items: center; + justify-content: center; + + .preview-btn { + color: #409eff; + &:hover { + text-decoration: underline; + } + &:disabled { + color: #c0c4cc; + cursor: not-allowed; + } + } + + .download-btn { + color: #67c23a; + &:hover { + text-decoration: underline; + } + &:disabled { + color: #c0c4cc; + cursor: not-allowed; + } + } +} + +/* 浜ゆ槗淇℃伅澶囨敞琛ㄦ牸鏍峰紡 */ +.remark-table { + width: 100%; + .end-time-wrapper { + display: flex; + align-items: center; + gap: 10px; + .forever-text { + color: #409eff; + font-weight: 500; + } + } +} </style> diff --git a/src/views/tradeManage/evaluate/index.vue b/src/views/tradeManage/evaluate/index.vue index 0493cb9..f607372 100644 --- a/src/views/tradeManage/evaluate/index.vue +++ b/src/views/tradeManage/evaluate/index.vue @@ -142,6 +142,8 @@ </el-table> </div> + <!-- 绉婚櫎鍘熸潵鐨勮〃鏍煎簳閮ㄤ俊鎭紝鍥犱负宸茬粡绉诲埌琛ㄦ牸鏈�鍚庝竴琛� --> + <!-- 浜ゆ槗鏂囦欢锛堢Щ鍔ㄥ埌璁㈠崟璇︽儏涓嬮潰锛屽悓涓�鍗$墖鍐咃級 --> <div class="file-section" v-if="fileList.length > 0"> <el-table @@ -168,103 +170,189 @@ {{ formatFileSize(row.size) }} </template> </el-table-column> - <el-table-column label="鎿嶄綔" width="100"> + <el-table-column label="鎿嶄綔" width="180"> <template #default="{ row }"> - <el-button - type="text" - size="small" - class="preview-btn" - @click="handlePreview(row)" - > - 棰勮 - </el-button> - </template> - </el-table-column> - </el-table> - </div> - - <!-- 浜ゆ槗淇℃伅澶囨敞锛堢Щ鍔ㄥ埌浜ゆ槗鏂囦欢涓嬮潰锛屽悓涓�鍗$墖鍐咃級 --> - <div class="remark-section" v-if="remarkItems.length > 0"> - <el-table - :data="remarkItems" - border - class="remark-table" - :header-cell-style="remarkTableHeaderStyle" - :cell-style="remarkTableCellStyle" - > - <el-table-column min-width="200"> - <template #header> - <el-icon class="header-icon"><Document /></el-icon> - <span>浜ゆ槗淇℃伅澶囨敞</span> - </template> - <template #default="{ row }"> - <div class="remark-name"> - <span>{{ row.name }}</span> + <div class="file-actions"> + <el-button + type="text" + size="small" + class="preview-btn" + @click="handlePreview(row)" + v-if="isPreviewable(row)" + :disabled="!isFileUploaded(row)" + > + 棰勮 + </el-button> + <el-button + type="text" + size="small" + class="download-btn" + @click="handleDownload(row)" + :disabled="!isFileUploaded(row)" + > + 涓嬭浇 + </el-button> </div> - </template> - </el-table-column> - <el-table-column label="鎺堟潈寮�濮嬫椂闂�" width="200"> - <template #default="{ row }"> - <div class="remark-date">{{ row.start }}</div> - </template> - </el-table-column> - <el-table-column label="鎺堟潈缁撴潫鏃堕棿" width="200"> - <template #default="{ row }"> - <div class="remark-date"> - {{ row.forever ? '姘镐箙' : row.end }} - </div> - </template> - </el-table-column> - <el-table-column label="澶囨敞" min-width="300"> - <template #default="{ row }"> - <div class="remark-content">{{ row.remark }}</div> </template> </el-table-column> </el-table> </div> </el-card> - <!-- 浜ゆ槗璇勪环 --> + <!-- 浜ゆ槗淇℃伅澶囨敞 --> <el-card class="mt15" shadow="never"> - <div class="title">浜ゆ槗璇勪环</div> - <el-form :model="form" label-width="120px" class="mt10" :rules="rules" ref="formRef"> - <el-form-item label="缁煎悎璇勫垎" prop="score"> - <el-rate v-model="form.score" /> - </el-form-item> - <el-form-item label="璇勪环鍐呭" prop="content"> - <el-input v-model="form.content" type="textarea" :autosize="{ minRows: 4 }" placeholder="璇疯緭鍏ヨ瘎浠峰唴瀹�" /> - </el-form-item> - </el-form> - <div class="action-buttons"> - <el-button @click="goBack">杩斿洖</el-button> - <el-button type="primary" @click="submit">鎻愪氦璇勪环</el-button> - </div> - </el-card> - </div> -</template> + <div class="title">浜ゆ槗淇℃伅澶囨敞</div> + <el-table :data="form.items" border class="mt10 remark-table"> + <el-table-column label="璇︽儏" prop="name" min-width="200" /> + <el-table-column label="鎺堟潈寮�濮嬫椂闂�" width="200"> + <template #default="{ row, $index }"> + <span>{{ form.items[$index].start || '-' }}</span> + </template> + </el-table-column> + <el-table-column label="鎺堟潈缁撴潫鏃堕棿" width="200"> + <template #default="{ row, $index }"> + <div class="end-time-wrapper"> + <span v-if="form.items[$index].forever" class="forever-text">姘镐箙</span> + <span v-else>{{ form.items[$index].end || '-' }}</span> + </div> + </template> + </el-table-column> + <el-table-column label="澶囨敞" min-width="300"> + <template #default="{ row, $index }"> + <span>{{ form.items[$index].remark || '-' }}</span> + </template> + </el-table-column> + </el-table> + </el-card> + + <!-- 浜ゆ槗璇勪环 --> + <el-card class="mt15" shadow="never"> + <div class="title">浜ゆ槗璇勪环</div> + <div class="evaluation-content"> + <div class="evaluation-form"> + <!-- 璇勫垎閮ㄥ垎 --> + <div class="rating-section"> + <div class="rating-items"> + <div class="rating-row"> + <div class="rating-item"> + <label class="required">缁煎悎璇勫垎:</label> + <el-rate + v-model="evaluationForm.overallRating" + :max="5" + :texts="['寰堝樊', '杈冨樊', '涓�鑸�', '杈冨ソ', '寰堝ソ']" + show-text + :colors="['#99A9BF', '#F7BA2A', '#FF9900']" + /> + </div> + <div class="rating-item"> + <label class="required">鏈嶅姟璇勫垎:</label> + <el-rate + v-model="evaluationForm.serviceRating" + :max="5" + :texts="['寰堝樊', '杈冨樊', '涓�鑸�', '杈冨ソ', '寰堝ソ']" + show-text + :colors="['#99A9BF', '#F7BA2A', '#FF9900']" + /> + </div> + </div> + <div class="rating-row"> + <div class="rating-item"> + <label class="required">璐ㄩ噺璇勫垎:</label> + <el-rate + v-model="evaluationForm.qualityRating" + :max="5" + :texts="['寰堝樊', '杈冨樊', '涓�鑸�', '杈冨ソ', '寰堝ソ']" + show-text + :colors="['#99A9BF', '#F7BA2A', '#FF9900']" + /> + </div> + <div class="rating-item"> + <label class="required">閫熷害璇勫垎:</label> + <el-rate + v-model="evaluationForm.speedRating" + :max="5" + :texts="['寰堝樊', '杈冨樊', '涓�鑸�', '杈冨ソ', '寰堝ソ']" + show-text + :colors="['#99A9BF', '#F7BA2A', '#FF9900']" + /> + </div> + </div> + </div> + </div> + + <!-- 璇勪环鍐呭 --> + <div class="form-item"> + <label class="required">璇勪环鍐呭:</label> + <el-input + v-model="evaluationForm.content" + type="textarea" + :rows="4" + placeholder="璇疯緭鍏ヨ瘎浠峰唴瀹�" + maxlength="500" + show-word-limit + /> + </div> + + <!-- 鍖垮悕鍙戝竷閫夐」 --> + <div class="form-item"> + <label></label> + <div class="anonymous-option"> + <el-checkbox v-model="evaluationForm.isAnonymous"> + 鍖垮悕鍙戝竷璇勪环 + </el-checkbox> + <div class="anonymous-tip"> + <el-icon><InfoFilled /></el-icon> + <span>閫夋嫨鍖垮悕鍙戝竷鍚庯紝鎮ㄧ殑濮撳悕灏嗕笉浼氬湪璇勪环涓樉绀�</span> + </div> + </div> + </div> + </div> + <div class="evaluation-actions"> + <el-button @click="goBack">杩斿洖</el-button> + <el-button + type="primary" + @click="handleSubmitEvaluation" + :loading="evaluationLoading" + > + 鎻愪氦璇勪环 + </el-button> + </div> + </div> + </el-card> + </div> + </template> <script setup lang="ts"> import { onMounted, reactive, ref, computed, type CSSProperties } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { Document, User, Goods, List } from '@element-plus/icons-vue' -import { fetchOrderDetail, submitEvaluate } from '@/api/tradeManage' -import { ElMessage, FormInstance } from 'element-plus' +import { Document, User, Goods, List, InfoFilled } from '@element-plus/icons-vue' +import { ElMessage, ElMessageBox } from 'element-plus' +import orderApi from '@/api/orderApi' +import { useUserInfo } from '@/stores/modules/userInfo' +import createAxios from '@/utils/axios' const route = useRoute() const router = useRouter() +const userStore = useUserInfo() const detail = reactive<any>({ items: [] }) -const formRef = ref<FormInstance>() -const form = reactive({ score: 0, content: '' }) +const form = reactive<any>({ items: [] }) const fileList = ref<any[]>([]) -const remarkItems = ref<any[]>([]) const orderTableWrapRef = ref<HTMLElement | null>(null) const labelStyle = { width: '180px', maxWidth: '180px' } const contentStyle = { width: 'calc(50% - 180px)' } -const rules = { - score: [{ required: true, message: '璇疯瘎鍒�', trigger: 'change' }], - content: [{ required: true, message: '璇疯緭鍏ヨ瘎浠峰唴瀹�', trigger: 'blur' }], -} +// 璇勪环琛ㄥ崟鏁版嵁 +const evaluationForm = reactive({ + content: '', + overallRating: 0, // 缁煎悎璇勫垎 + serviceRating: 0, // 鏈嶅姟璇勫垎 + qualityRating: 0, // 璐ㄩ噺璇勫垎 + speedRating: 0, // 閫熷害璇勫垎 + isAnonymous: false // 鏄惁鍖垮悕鍙戝竷 +}) + +// 璇勪环loading鐘舵�� +const evaluationLoading = ref(false) // 璁$畻琛ㄦ牸鏁版嵁锛屾坊鍔犳眹鎬昏 const tableData = computed(() => { @@ -284,126 +372,169 @@ return [...detail.items, summaryRow] }) +// 鐘舵�佹槧灏勶紙鍚庣涓枃 -> 鍓嶇鏋氫妇锛� +const statusServerToUi: Record<string, string> = { + '寰呬笂浼犳枃浠�': 'WAIT_UPLOAD', + '寰呮巿鏉�': 'WAIT_AUTHORIZE', + '寰呬氦鏄撶‘璁�': 'WAIT_CONFIRM', + '宸插畬鎴�': 'COMPLETED', + '宸茶瘎浠�': 'EVALUATED', +} + +const formatDateTime = (val?: string) => (val ? val.replace('T', ' ').slice(0, 19) : '') + +const normalizePriceType = (val?: string): 'points' | 'currency' | 'agreement' | 'free' => { + if (!val) return 'currency' + const s = String(val) + if (/(绉垎|points)/i.test(s)) return 'points' + if (/(鍗忚|agreement)/i.test(s)) return 'agreement' + if (/(鍏嶈垂|free)/i.test(s)) return 'free' + return 'currency' +} + onMounted(async () => { - // 浣跨敤鍓嶇妯℃嫙鏁版嵁浠ヤ究寮�鍙� UI锛堜笉鏀瑰姩鍚庣鏈嶅姟锛� - const mockDetail = { - orderNo: '4348442557619205545', - resourceTypeName: '杞欢浜у搧', - status: 'FINISH', - statusName: '宸插畬鎴�', - applyTime: '2025-05-21 10:00:00', - unitName: '涓氦鏂硅繙寤鸿鏈夐檺鍏徃', - userName: '寮犻潤', - userAccount: 'L20159922', - userDept: '淇℃伅涓績', - userPhone: '13800000000', - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - supplier: '涓氦鏂硅繙绉戞妧鏈夐檺鍏徃', - industry: '寤虹瓚宸ョ▼', - projectUnit: '鍦熷缓宸ョ▼', - productType: '杞欢搴�', - productDesc: - '闈㈠悜宸ョ▼椤圭洰鐨勮川閲忓疄娴嬪疄閲忔暟瀛楀寲绠$悊绯荤粺锛屾敮鎸佹爣鍑嗗寲閲囬泦銆佽嚜鍔ㄧ粺璁′笌杩囩▼绠℃帶銆�', - items: [ - { - id: '1', - name: '浼佷笟绉佹湁SaaS鐗堣鍙�', - saleType: '涔版柇', - accountCount: 50, - customerTarget: '浼佷笟', - concurrentNodes: 50, - pricePoint: 50000, - priceCash: 0, - quantity: 1, - period: 0, - }, - { - id: '2', - name: '浼佷笟绉佹湁SaaS鐗圤TA鍗囩骇鏈嶅姟', - saleType: 'OTA鏈嶅姟', - accountCount: 50, - customerTarget: '浼佷笟', - concurrentNodes: 50, - pricePoint: 0, - priceCash: 7500, - quantity: 1, - period: 1, - }, - { - id: '3', - name: '浼佷笟绉佹湁SaaS鐗堢敤鎴峰閲忓寘', - saleType: '绉佹湁澧為噺鍖�', - accountCount: 100, - customerTarget: '浼佷笟', - concurrentNodes: 100, - pricePoint: 0, - priceCash: 0, - priceProtocol: true, - quantity: 1, - period: 1, - }, - { - id: '4', - name: '涓汉鍏湁SaaS鐗堣鍙�', - saleType: '绉佹湁澧為噺鍖�', - accountCount: 50, - customerTarget: '涓汉', - concurrentNodes: 50, - pricePoint: 0, - priceCash: 0, - quantity: 1, - period: 0, - }, - ], - pointTotal: '50,000', - cashTotal: '7,500', - } - - Object.assign(detail, mockDetail) + const orderId = String(route.params.id || '') + if (!orderId) return - // 娣诲姞妯℃嫙鏂囦欢鏁版嵁鐢ㄤ簬灞曠ず - fileList.value = [ - { - name: '绛惧瓧鐩栫珷鏂囦欢.pdf', - size: 2621440, // 2.5MB - uid: '1', - status: 'success' - }, - { - name: 'API Keys.txt', - size: 354, // 354 Bytes - uid: '2', - status: 'success' + try { + const res = (await orderApi.getOrderDetail(orderId)) as any + const data = res?.data || {} + + const statusName: string = data.orderStatus || '' + const uiStatus = statusServerToUi[statusName] || 'INFO' + + // 鏄犲皠璁㈠崟璇︽儏澶撮儴淇℃伅 + const head = { + orderNo: data.orderId, + resourceTypeName: '杞欢浜у搧', + status: uiStatus, + statusName, + applyTime: formatDateTime(data.applyTime), + unitName: data.unitName || '-', + userName: data.userName || '-', + userAccount: data.userAccount || '-', + userDept: data.userDept || '-', + userPhone: data.userPhone || '-', + productName: data.productName || '-', + supplier: data.providerName || '-', + industry: data.industry || '-', + projectUnit: data.projectUnit || '-', + productType: data.productType || '-', + productDesc: data.productDesc || '-', } - ] - // 娣诲姞妯℃嫙浜ゆ槗淇℃伅澶囨敞鏁版嵁 - remarkItems.value = (detail.items || []).map((item: any, index: number) => ({ - name: item.name, - start: '2025-06-01', - end: index === 0 || index === 3 ? '' : '2025-06-01', - forever: index === 0 || index === 3, - remark: index === 3 ? '寮�閫氱鐞嗗憳璐﹀彿1涓�,璐﹀彿admin' : '寮�閫氱鐞嗗憳璐﹀彿1涓�,璐﹀彿admin,鐧诲綍绠$悊鍛樿处鍙峰彲绠$悊鏅�氱敤鎴�' - })) - - // 娉ㄩ噴鎺夊師鏈夌殑API璋冪敤锛屼娇鐢ㄦā鎷熸暟鎹� - // const { data } = (await fetchOrderDetail({ id: route.params.id })) as any - // Object.assign(detail, data || {}) + // 鏄庣粏椤规槧灏� + const items: any[] = Array.isArray(data.orderDetails) + ? data.orderDetails.map((d: any, idx: number) => { + const pt = normalizePriceType(d.priceType) + return { + id: String(d.id ?? idx + 1), + name: d.suiteName, + saleType: d.salesForm, + accountCount: d.accountLimit, + customerTarget: d.customerType, + concurrentNodes: d.concurrentNodes, + pricePoint: pt === 'points' ? Number(d.unitPrice || 0) : 0, + priceCash: pt === 'currency' ? Number(d.unitPrice || 0) : 0, + priceProtocol: pt === 'agreement', + quantity: Number(d.quantity || 0), + period: Number(d.duration || 0), + remarks: d.remarks || '', // 鏂板 remarks 瀛楁 + } + }) + : [] + + // 姹囨�伙紙绠�鍗曠浉鍔狅細鍗曚环*鏁伴噺锛� + const pointTotalNum = items.reduce((sum, it) => sum + Number(it.pricePoint || 0) * Number(it.quantity || 0), 0) + const cashTotalNum = items.reduce((sum, it) => sum + Number(it.priceCash || 0) * Number(it.quantity || 0), 0) + + Object.assign(detail, head, { + items, + pointTotal: pointTotalNum.toLocaleString(), + cashTotal: cashTotalNum.toLocaleString(), + }) + + // 鍒濆鍖栬〃鍗曟暟鎹� + form.items = (detail.items || []).map((item: any, index: number) => { + // 璁$畻鎺堟潈缁撴潫鏃堕棿 + let endDate = '' + if (item.period > 0) { + // 濡傛灉鏈夋湡闄愶紝璁$畻缁撴潫鏃堕棿 + const startDate = new Date(data.applyTime || new Date()) + const endDateObj = new Date(startDate) + endDateObj.setFullYear(endDateObj.getFullYear() + item.period) + endDate = endDateObj.toISOString().split('T')[0] // 鏍煎紡鍖栦负 YYYY-MM-DD + } + + return { + name: item.name, + start: data.applyTime ? data.applyTime.split('T')[0] : '', // 浣跨敤璁㈠崟鐢宠鏃堕棿 + end: endDate, + forever: item.period === 0, // 鏈熼檺涓�0鏃惰缃负姘镐箙 + remark: item.remarks || '' // 浣跨敤濂椾欢淇℃伅涓殑remarks瀛楁 + } + }) + + // 鑾峰彇璁㈠崟闄勪欢鍒楄〃锛堝鏋滄湁鐨勮瘽锛� + if (data.attachments && Array.isArray(data.attachments)) { + fileList.value = data.attachments.map((file: any) => ({ + name: file.fileName || file.originalName, + size: file.fileSize || 0, + uid: file.id, + status: 'success', + url: file.fileUrl, + uploaded: true + })) + } else { + fileList.value = [] + } + } catch (error) { + console.error('鑾峰彇璁㈠崟璇︽儏澶辫触:', error) + ElMessage.error('鑾峰彇璁㈠崟璇︽儏澶辫触') + } }) const goBack = () => router.back() const submit = async () => { - await formRef.value?.validate() - - // 妯℃嫙鎻愪氦鎴愬姛鍝嶅簲 - const mockResponse = { code: 200 } - - // 娉ㄩ噴鎺夊師鏈夌殑API璋冪敤锛屼娇鐢ㄦā鎷熸暟鎹� - // const { code } = (await submitEvaluate({ id: route.params.id, ...form })) as any - - if (mockResponse.code === 200) { - ElMessage.success('鎻愪氦鎴愬姛') - router.back() + try { + const orderId = String(route.params.id || '') + const userId = userStore.getUserId ? Number(userStore.getUserId) : undefined + + if (!orderId || !userId) { + ElMessage.error('璁㈠崟ID鎴栫敤鎴稩D涓嶈兘涓虹┖') + return + } + + // 纭鎿嶄綔 + await ElMessageBox.confirm('纭畾瑕佺‘璁や氦鏄撳苟鎻愪氦瀹℃壒鍚楋紵', '纭鎿嶄綔', { + confirmButtonText: '纭畾', + cancelButtonText: '鍙栨秷', + type: 'warning' + }) + + // 璋冪敤鏇存柊璁㈠崟璇︽儏API锛屽皢鐘舵�佹洿鏂颁负"宸茶瘎浠�" + const updateData = { + orderId: orderId, + orderStatus: '宸茶瘎浠�', // 鏇存柊涓哄凡璇勪环鐘舵�� + orderDetails: detail.items.map((item: any) => ({ + id: item.id, + remarks: item.remarks || '' // 浣跨敤濂椾欢淇℃伅涓殑remarks瀛楁 + })) + } + + const res = (await orderApi.updateOrderDetail(updateData)) as any + + if (res?.code === 200) { + ElMessage.success('浜ゆ槗纭鎴愬姛锛屽凡鎻愪氦瀹℃壒') + router.back() + } else { + ElMessage.error(res?.msg || '浜ゆ槗纭澶辫触') + } + } catch (error) { + if (error !== 'cancel') { + console.error('浜ゆ槗纭澶辫触:', error) + ElMessage.error('浜ゆ槗纭澶辫触') + } } } @@ -481,15 +612,6 @@ // 鏂囦欢鍒楄〃琛ㄦ牸琛ㄤ綋鏂囧瓧澶у皬 const fileTableCellStyle: CSSProperties = { fontSize: '12px' } -// 浜ゆ槗淇℃伅澶囨敞琛ㄦ牸琛ㄥご鏂囧瓧灞呬腑锛屼絾绗竴鍒楃殑"浜ゆ槗淇℃伅澶囨敞"鏂囧瓧闈犲乏瀵归綈 -const remarkTableHeaderStyle: CSSProperties = { - textAlign: 'center', - fontSize: '14px', - background: '#f3f6fb' -} -// 浜ゆ槗淇℃伅澶囨敞琛ㄦ牸琛ㄤ綋鏂囧瓧澶у皬 -const remarkTableCellStyle: CSSProperties = { fontSize: '12px' } - // 鏂囦欢澶у皬鏍煎紡鍖� const formatFileSize = (size: number) => { if (!size || size === 0) return '0 Bytes'; @@ -499,10 +621,265 @@ return parseFloat((size / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; -// 鏂囦欢棰勮澶勭悊 -const handlePreview = (file: any) => { - ElMessage.info(`棰勮鏂囦欢锛�${file.name}`) +// 鍒ゆ柇鏂囦欢鏄惁鍙瑙� +const isPreviewable = (file: any) => { + // 棣栧厛妫�鏌IME绫诲瀷 + const previewableTypes = [ + 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/webp', + 'text/plain', 'text/html', 'text/css', 'text/javascript', + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx + ] + + // 濡傛灉MIME绫诲瀷鍖归厤锛岀洿鎺ヨ繑鍥瀟rue + if (previewableTypes.includes(file.type || '')) { + return true + } + + // 濡傛灉MIME绫诲瀷涓虹┖鎴栦笉鍖归厤锛屾牴鎹枃浠舵墿灞曞悕鍒ゆ柇 + const fileName = file.name || '' + const fileExtension = fileName.toLowerCase().split('.').pop() + + const previewableExtensions = [ + 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', + 'txt', 'html', 'htm', 'css', 'js', + 'pdf', + 'docx', 'xlsx', 'pptx' + ] + + return previewableExtensions.includes(fileExtension) } + +// 鍒ゆ柇鏂囦欢鏄惁宸蹭笂浼犳垚鍔� +const isFileUploaded = (file: any) => { + // 鏂囦欢鏈塽rl涓旂姸鎬佷负success琛ㄧず宸蹭笂浼犳垚鍔� + return file.url && (file.status === 'success' || file.uploaded) +} + +// 鏂囦欢棰勮 +const handlePreview = async (file: any) => { + if (!file.url) { + ElMessage.warning('鏂囦欢閾炬帴涓嶅瓨鍦�') + return + } + + // 鑾峰彇鏂囦欢鎵╁睍鍚� + const fileName = file.name || '' + const fileExtension = fileName.toLowerCase().split('.').pop() + + let previewUrl = file.url + + // 濡傛灉鏂囦欢瀛樺偍鍦∕inIO锛屼紭鍏堜娇鐢ㄩ瑙圲RL + if (file.url.includes('order-attachments')) { + try { + // 棣栧厛灏濊瘯鑾峰彇棰勮URL + const previewResponse = await createAxios({ + url: `/admin/file/preview`, + method: 'GET', + params: { + fileName: file.url + } + }) + + console.log('棰勮URL鍝嶅簲:', previewResponse) + + // 妫�鏌ュ搷搴旀牸寮� + const responseData = previewResponse as any + if (responseData && responseData.code === 200 && responseData.data) { + previewUrl = responseData.data + console.log('浣跨敤棰勮URL:', previewUrl) + } else { + console.log('棰勮URL鑾峰彇澶辫触锛屼娇鐢ㄤ笅杞芥柟寮�') + // 濡傛灉棰勮URL鑾峰彇澶辫触锛屽洖閫�鍒颁笅杞芥柟寮� + const response = await createAxios({ + url: `/admin/file/download`, + method: 'GET', + responseType: 'blob', + params: { + fileName: file.url, + originalName: file.name + } + }) + + // 鍒涘缓棰勮URL + const blob = new Blob([response as any]) + previewUrl = window.URL.createObjectURL(blob) + console.log('浣跨敤Blob URL:', previewUrl) + } + } catch (error) { + console.error('鑾峰彇鏂囦欢澶辫触:', error) + ElMessage.error('鑾峰彇鏂囦欢澶辫触锛屾棤娉曢瑙�') + return + } + } + + // 鍥剧墖鏂囦欢鐩存帴鍦ㄦ柊绐楀彛鎵撳紑 + if ((file.type && file.type.startsWith('image/')) || + ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension)) { + console.log('棰勮鍥剧墖鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // PDF鏂囦欢鍦ㄦ柊绐楀彛鎵撳紑 + if (file.type === 'application/pdf' || fileExtension === 'pdf') { + console.log('棰勮PDF鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // 鏂囨湰鏂囦欢鍦ㄦ柊绐楀彛鎵撳紑 + if ((file.type && file.type.startsWith('text/')) || + ['txt', 'html', 'htm', 'css', 'js'].includes(fileExtension)) { + console.log('棰勮鏂囨湰鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // Office鏂囨。鍜屽叾浠栨枃浠剁被鍨嬶紝灏濊瘯鍦ㄦ柊绐楀彛鎵撳紑 + try { + console.log('棰勮鍏朵粬鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + } catch (error) { + console.error('棰勮澶辫触:', error) + ElMessage.error('棰勮澶辫触锛岃涓嬭浇鍚庢煡鐪�') + } +} + +// 鏂囦欢涓嬭浇 +const handleDownload = async (file: any) => { + if (!file.url) { + ElMessage.warning('鏂囦欢閾炬帴涓嶅瓨鍦�') + return + } + + console.log('寮�濮嬩笅杞芥枃浠�:', file.name, 'URL:', file.url) + + try { + // 濡傛灉鏂囦欢瀛樺偍鍦∕inIO锛屼娇鐢ㄥ悗绔洿鎺ヤ笅杞紸PI + if (file.url.includes('order-attachments')) { + console.log('浣跨敤MinIO涓嬭浇API') + + // 浣跨敤axios閫氳繃浠g悊璁块棶鍚庣API + const response = await createAxios({ + url: `/admin/file/download`, + method: 'GET', + responseType: 'blob', + params: { + fileName: file.url, + originalName: file.name + } + }) + + console.log('涓嬭浇鍝嶅簲:', response) + + // 鍒涘缓涓嬭浇閾炬帴 + const blob = new Blob([response as any]) + const downloadUrl = window.URL.createObjectURL(blob) + + console.log('鍒涘缓涓嬭浇閾炬帴:', downloadUrl) + + const link = document.createElement('a') + link.href = downloadUrl + link.download = file.name || 'download' + link.target = '_blank' + link.rel = 'noopener noreferrer' + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + // 娓呯悊URL瀵硅薄 + window.URL.revokeObjectURL(downloadUrl) + + ElMessage.success('鏂囦欢涓嬭浇鎴愬姛') + } else { + console.log('浣跨敤鐩存帴URL涓嬭浇') + // 鍏朵粬鎯呭喌鐩存帴浣跨敤鍘烾RL + const link = document.createElement('a') + link.href = file.url + link.download = file.name || 'download' + link.target = '_blank' + link.rel = 'noopener noreferrer' + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + ElMessage.success('寮�濮嬩笅杞芥枃浠�') + } + } catch (error) { + console.error('涓嬭浇澶辫触:', error) + ElMessage.error('涓嬭浇澶辫触锛岃閲嶈瘯') + } +} + +// 鎻愪氦璇勪环澶勭悊 +const handleSubmitEvaluation = async () => { + // 楠岃瘉璇勫垎鏄惁宸插~鍐� + if (!evaluationForm.overallRating || !evaluationForm.serviceRating || + !evaluationForm.qualityRating || !evaluationForm.speedRating) { + ElMessage.warning('璇峰畬鎴愭墍鏈夎瘎鍒嗛」鐩�') + return + } + + if (!evaluationForm.content.trim()) { + ElMessage.warning('璇疯緭鍏ヨ瘎浠峰唴瀹�') + return + } + + try { + evaluationLoading.value = true + await ElMessageBox.confirm('纭畾瑕佹彁浜よ瘎浠峰悧锛�', '纭鎿嶄綔', { + confirmButtonText: '纭畾', + cancelButtonText: '鍙栨秷', + type: 'warning' + }) + + const orderId = String(route.params.id || '') + const userId = userStore.getUserId ? Number(userStore.getUserId) : undefined + const content = evaluationForm.content.trim() + + if (!orderId || !userId) { + ElMessage.error('璁㈠崟ID鎴栫敤鎴稩D涓嶈兘涓虹┖') + evaluationLoading.value = false + return + } + + // 璋冪敤鎻愪氦璇勪环API + const result = await orderApi.submitEvaluation({ + orderId: orderId, + evaluationContent: content, + evaluatorId: userId, + evaluatorName: evaluationForm.isAnonymous ? '鍖垮悕鐢ㄦ埛' : (userStore.getUserDetail || '绠$悊鍛�'), + overallRating: evaluationForm.overallRating, + serviceRating: evaluationForm.serviceRating, + qualityRating: evaluationForm.qualityRating, + speedRating: evaluationForm.speedRating, + isAnonymous: evaluationForm.isAnonymous + }) + + if (result && result.code === 200) { + // 鏇存柊璁㈠崟鐘舵�佽繘鍏ヤ笅涓�涓姸鎬� + await orderApi.updateOrderStatusToNext(orderId) + ElMessage.success('璇勪环鎻愪氦鎴愬姛') + router.back() + evaluationLoading.value = false + } else { + ElMessage.error(result?.msg || '璇勪环鎻愪氦澶辫触') + } + } catch (error) { + if (error !== 'cancel') { + console.error('璇勪环鎻愪氦澶辫触:', error) + ElMessage.error('璇勪环鎻愪氦澶辫触') + } + } finally { + evaluationLoading.value = false + } +} + // 鐥囩粨涓庝慨澶嶈鏄庯細 // 1) Element Plus 鐨� el-table 瀛愬垪 width 鐧惧垎姣旀槸鐩稿浜庤〃鏍煎鍣ㄧ殑鍍忕礌瀹藉害璁$畻锛屼絾鍙湁鍦ㄨ〃鏍煎鍣ㄦ湁鏄庣‘瀹藉害鏃舵墠鐢熸晥銆� @@ -706,36 +1083,152 @@ text-align: left !important; } -/* 浜ゆ槗淇℃伅澶囨敞琛ㄦ牸鏍峰紡 */ -.remark-section { - margin-top: 15px; - .remark-table { - width: 100%; - .remark-name { - font-weight: 500; +/* 鏂囦欢鎿嶄綔鎸夐挳鏍峰紡 */ +.file-actions { + display: flex; + gap: 8px; + align-items: center; + justify-content: center; + + .preview-btn { + color: #409eff; + &:hover { + text-decoration: underline; } - .remark-date { - color: #606266; + &:disabled { + color: #c0c4cc; + cursor: not-allowed; } - .remark-content { - color: #606266; - line-height: 1.4; + } + + .download-btn { + color: #67c23a; + &:hover { + text-decoration: underline; + } + &:disabled { + color: #c0c4cc; + cursor: not-allowed; } } } -/* 浜ゆ槗淇℃伅澶囨敞琛ㄦ牸琛ㄥご绗竴鍒�"浜ゆ槗淇℃伅澶囨敞"鏂囧瓧闈犲乏瀵归綈 */ -.remark-table :deep(.el-table__header-wrapper thead tr th:first-child) { - text-align: left !important; +/* 浜ゆ槗淇℃伅澶囨敞琛ㄦ牸鏍峰紡 */ +.remark-table { + width: 100%; + .end-time-wrapper { + display: flex; + align-items: center; + gap: 10px; + .forever-text { + color: #409eff; + font-weight: 500; + } + } } -/* 鎿嶄綔鎸夐挳鏍峰紡 */ -.action-buttons { - display: flex; - justify-content: flex-end; - margin-top: 15px; - .el-button { - margin-left: 10px; +/* 浜ゆ槗璇勪环鏍峰紡 */ +.evaluation-content { + .evaluation-form { + /* 璇勫垎閮ㄥ垎鏍峰紡 */ + .rating-section { + margin-bottom: 30px; + + .rating-title { + font-weight: 600; + margin-bottom: 15px; + color: #303133; + } + + .rating-items { + display: flex; + flex-direction: column; + gap: 20px; + + .rating-row { + display: flex; + gap: 40px; + + .rating-item { + flex: 1; + display: flex; + align-items: center; + + label { + width: 100px; + line-height: 32px; + margin-right: 15px; + font-weight: 500; + white-space: nowrap; + + &.required::before { + content: '*'; + color: #f56c6c; + margin-right: 4px; + } + } + + .el-rate { + flex: 1; + } + } + } + } + } + + .form-item { + display: flex; + align-items: flex-start; + margin-bottom: 20px; + + label { + width: 120px; + line-height: 32px; + margin-right: 10px; + font-weight: 500; + + &.required::before { + content: '*'; + color: #f56c6c; + margin-right: 4px; + } + } + + .el-textarea { + flex: 1; + } + + /* 鍖垮悕閫夐」鏍峰紡 */ + .anonymous-option { + display: flex; + align-items: center; + gap: 15px; + + .anonymous-tip { + display: flex; + align-items: center; + gap: 6px; + color: #909399; + font-size: 12px; + + .el-icon { + color: #409eff; + font-size: 14px; + } + } + } + } + } + + .evaluation-actions { + display: flex; + justify-content: center; + gap: 15px; + margin-top: 30px; + + .el-button { + min-width: 100px; + } } } </style> diff --git a/src/views/tradeManage/seller/index.vue b/src/views/tradeManage/seller/index.vue index 59d87cb..75068d4 100644 --- a/src/views/tradeManage/seller/index.vue +++ b/src/views/tradeManage/seller/index.vue @@ -89,10 +89,6 @@ <span class="label">闇�姹傛柟:</span> <span class="value">{{ row.demandSide }}</span> </div> - <div class="order-item"> - <span class="label">渚涘簲鏂�:</span> - <span class="value">{{ row.supplySide }}</span> - </div> </div> </div> <div v-else-if="row.isSpacer" class="spacer-cell"></div> @@ -148,16 +144,16 @@ <div v-if="row.isSpacer" class="spacer-cell"></div> <div v-else-if="!row.isMainOrder" class="price-info"> <span v-if="row.priceType === 'points'" class="price-points"> - {{ row.unitPrice }} + 绉垎 {{ row.unitPrice }} </span> <span v-else-if="row.priceType === 'currency'" class="price-currency"> - {{ row.unitPrice }} + 璐у竵 {{ row.unitPrice }} </span> <span v-else-if="row.priceType === 'agreement'" class="price-agreement"> - {{ row.unitPrice }} + 鍗忚 </span> <span v-else-if="row.priceType === 'free'" class="price-free"> - {{ row.unitPrice }} + 鍏嶈垂 </span> </div> </template> @@ -176,8 +172,8 @@ <template #default="{ row }"> <div v-if="row.isSpacer" class="spacer-cell"></div> <div v-else-if="!row.isMainOrder" class="period-info"> - <span>{{ row.period }}</span> - <span v-if="row.isPermanent" class="permanent">姘镐箙</span> + <span v-if="row.period === 0" class="permanent">姘镐箙</span> + <span v-else>{{ row.period }}</span> </div> </template> </el-table-column> @@ -192,10 +188,26 @@ <div v-if="row.parentOrder && row.parentOrder.subOrders && row.parentOrder.subOrders.findIndex((sub: any) => sub.id === row.id) === 0" class="all-actions"> <div class="action-item"> <div class="action-buttons"> - <span v-if="row.parentOrder.status === 'WAIT_CONFIRM'" class="op-text warning">寰呬氦鏄撶‘璁�</span> - <span v-else-if="row.parentOrder.status === 'WAIT_EVALUATE'" class="op-text warning">寰呰瘎浠�</span> - <span v-else-if="row.parentOrder.status === 'FINISH'" class="op-text">宸插畬鎴�</span> - <el-button type="primary" link size="small" @click="toDetail(row.parentOrder)">鏌ョ湅</el-button> + <template v-for="action in getAvailableActions(row.parentOrder)" :key="action.type"> + <el-button + v-if="action.type === ActionType.VIEW" + type="primary" + link + size="small" + @click="handleAction(action, row.parentOrder)" + > + 鏌ョ湅 + </el-button> + <el-button + v-else-if="action.type === ActionType.TRACK" + type="primary" + link + size="small" + @click="handleAction(action, row.parentOrder)" + > + 杩借釜 + </el-button> + </template> </div> </div> </div> @@ -219,6 +231,12 @@ /> </div> </el-card> + + <!-- 璁㈠崟鐘舵�佸璇濇 --> + <ProductOrderStatusDialog + v-model="orderStatusDialogVisible" + :order-id="currentOrderId" + /> </div> </template> @@ -226,15 +244,23 @@ import { onMounted, reactive, ref } from 'vue' import { useRouter } from 'vue-router' import { Search, Refresh } from '@element-plus/icons-vue' +import { ElMessage, ElMessageBox } from 'element-plus' +import orderApi from '@/api/orderApi' +import { useUserInfo } from '@/stores/modules/userInfo' +import ProductOrderStatusDialog from '@/views/productManage/productOrderStatusDialog/index.vue' +import { OrderWorkflowController, OrderStatus, ActionType, PageType, StatusMapper } from '@/utils/orderWorkflow' const router = useRouter() +const userStore = useUserInfo() -// 鐘舵�侀�夐」 +// 鐘舵�侀�夐」锛堟洿鏂颁负鏂扮殑宸ヤ綔娴佺▼鐘舵�侊級 const statusOptions = [ { label: '鍏ㄩ儴', value: '' }, + { label: '寰呬笂浼犳枃浠�', value: 'WAIT_UPLOAD' }, + { label: '寰呮巿鏉�', value: 'WAIT_AUTHORIZE' }, { label: '寰呬氦鏄撶‘璁�', value: 'WAIT_CONFIRM' }, - { label: '寰呰瘎浠�', value: 'WAIT_EVALUATE' }, - { label: '宸插畬鎴�', value: 'FINISH' }, + { label: '宸插畬鎴�', value: 'COMPLETED' }, + { label: '宸茶瘎浠�', value: 'EVALUATED' }, ] // 琛屼笟棰嗗煙閫夐」 @@ -282,215 +308,54 @@ // 鍒嗛〉淇℃伅 const page = reactive({ current: 1, size: 10, total: 0 }) -// 璁㈠崟鍒楄〃鏁版嵁 +// 璁㈠崟鍒楄〃鏁版嵁锛堝寘鍚富璁㈠崟鍜屽瓙璁㈠崟锛� const orderList = ref<any[]>([]) -// 妯℃嫙鏁版嵁鐢ㄤ簬灞曠ず -const mockData = [ - { - id: '1', - isMainOrder: true, - applyTime: '2025-05-21 10:00:00', - orderNo: '4348442557619205545', - demandSide: '涓浗璺ˉ宸ョ▼鏈夐檺鍏徃', - supplySide: '涓氦鏂硅繙绉戞妧鏈夐檺鍏徃', - status: 'WAIT_CONFIRM', - statusName: '寰呬氦鏄撶‘璁�', - subOrders: [ - { - id: '1-1', - isMainOrder: false, - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - suiteName: '浼佷笟绉佹湁SaaS鐗堣鍙�', - salesForm: '涔版柇', - accountCount: 50, - demandSide: '涓浗璺ˉ宸ョ▼鏈夐檺鍏徃', - customerObject: '浼佷笟', - concurrentCount: 50, - priceType: 'points', - unitPrice: '绉垎: 50,000/濂�', - quantity: 1, - period: 1, - isPermanent: true, - status: 'WAIT_CONFIRM', - }, - { - id: '1-2', - isMainOrder: false, - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - suiteName: '浼佷笟绉佹湁SaaS鐗圤TA鍗囩骇鏈嶅姟', - salesForm: 'OTA鏈嶅姟', - accountCount: 50, - demandSide: '涓浗璺ˉ宸ョ▼鏈夐檺鍏徃', - customerObject: '浼佷笟', - concurrentCount: 50, - priceType: 'currency', - unitPrice: '璐у竵: 7,500/濂�/骞�', - quantity: 1, - period: 1, - isPermanent: false, - status: 'WAIT_CONFIRM', - }, - { - id: '1-3', - isMainOrder: false, - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - suiteName: '浼佷笟绉佹湁SaaS鐗堢敤鎴峰閲忓寘', - salesForm: '绉佹湁澧為噺鍖�', - accountCount: 100, - demandSide: '涓浗璺ˉ宸ョ▼鏈夐檺鍏徃', - customerObject: '浼佷笟', - concurrentCount: 100, - priceType: 'agreement', - unitPrice: '鍗忚:/骞�', - quantity: 1, - period: 1, - isPermanent: false, - status: 'WAIT_CONFIRM', - }, - { - id: '1-4', - isMainOrder: false, - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - suiteName: '涓汉鍏湁SaaS鐗堣鍙�', - salesForm: '绉佹湁澧為噺鍖�', - accountCount: 50, - demandSide: '涓浗璺ˉ宸ョ▼鏈夐檺鍏徃', - customerObject: '涓汉', - concurrentCount: 50, - priceType: 'free', - unitPrice: '鍏嶈垂:/骞�', - quantity: 3, - period: 3, - isPermanent: true, - status: 'WAIT_CONFIRM', - }, - ], - }, - { - id: '2', - isMainOrder: true, - applyTime: '2025-05-20 10:00:00', - orderNo: '4347442557619205545', - demandSide: '涓氦鍏矾瑙勫垝璁捐闄㈡湁闄愬叕鍙�', - supplySide: '鍗庝负杞欢鎶�鏈湁闄愬叕鍙�', - status: 'WAIT_CONFIRM', - statusName: '寰呬氦鏄撶‘璁�', - subOrders: [ - { - id: '2-1', - isMainOrder: false, - productName: '鍩轰簬鍥戒骇鑺墖瀹界獎铻嶅悎鑷粍缃戣澶囩殑搴旂敤', - suiteName: '浼佷笟鍏湁SaaS鐗堣鍙�', - salesForm: '绉熻祦', - accountCount: 100, - demandSide: '涓氦鍏矾瑙勫垝璁捐闄㈡湁闄愬叕鍙�', - customerObject: '浼佷笟', - concurrentCount: 100, - priceType: 'currency', - unitPrice: '璐у竵: 80,000/骞�', - quantity: 1, - period: 2, - isPermanent: false, - status: 'WAIT_CONFIRM', - }, - ], - }, - { - id: '3', - isMainOrder: true, - applyTime: '2025-05-19 10:00:00', - orderNo: '4347342557619205545', - demandSide: '涓氦绗洓鑸姟宸ョ▼灞�鏈夐檺鍏徃', - supplySide: '涓氦绗洓鑸姟宸ョ▼灞�鏈夐檺鍏徃', - status: 'WAIT_CONFIRM', - statusName: '寰呬氦鏄撶‘璁�', - subOrders: [ - { - id: '3-1', - isMainOrder: false, - productName: '楂樻々鐮佸ご杈呭姪鍑哄浘宸ュ叿绠�', - suiteName: '椤圭洰鍏湁SaaS鐗堣鍙�', - salesForm: '绉熻祦', - accountCount: 50, - demandSide: '涓氦绗洓鑸姟宸ョ▼灞�鏈夐檺鍏徃', - customerObject: '椤圭洰閮�', - concurrentCount: 50, - priceType: 'currency', - unitPrice: '璐у竵: 60,000/骞�', - quantity: 1, - period: 3, - isPermanent: false, - status: 'WAIT_CONFIRM', - }, - ], - }, - { - id: '4', - isMainOrder: true, - applyTime: '2025-05-18 10:00:00', - orderNo: '4347442557619205545', - demandSide: '涓氦鍏矾瑙勫垝璁捐闄㈡湁闄愬叕鍙�', - supplySide: '涓氦鍏矾瑙勫垝璁捐闄㈡湁闄愬叕鍙�', - status: 'WAIT_EVALUATE', - statusName: '寰呰瘎浠�', - subOrders: [ - { - id: '4-1', - isMainOrder: false, - productName: '鍏矾鏁板瓧鍖栨柟妗堣璁$郴缁�', - suiteName: '浼佷笟鍏湁SaaS鐗堣鍙�', - salesForm: '绉熻祦', - accountCount: 100, - demandSide: '涓氦鍏矾瑙勫垝璁捐闄㈡湁闄愬叕鍙�', - customerObject: '浼佷笟', - concurrentCount: 100, - priceType: 'currency', - unitPrice: '璐у竵: 80,000/骞�', - quantity: 1, - period: 1, - isPermanent: false, - status: 'WAIT_EVALUATE', - }, - ], - }, - { - id: '5', - isMainOrder: true, - applyTime: '2025-05-17 10:00:00', - orderNo: '4347342557619205545', - demandSide: '涓氦绗洓鑸姟宸ョ▼灞�鏈夐檺鍏徃', - supplySide: '涓氦绗笁鑸姟宸ョ▼灞�鏈夐檺鍏徃', - status: 'FINISH', - statusName: '宸插畬鎴�', - subOrders: [ - { - id: '5-1', - isMainOrder: false, - productName: '鍩轰簬鏃犱汉鏈虹殑浼佷笟绾у彲瑙嗗寲椤圭洰绠$悊绯荤粺', - suiteName: '椤圭洰鍏湁SaaS鐗堣鍙�', - salesForm: '绉熻祦', - accountCount: 50, - demandSide: '涓氦绗洓鑸姟宸ョ▼灞�鏈夐檺鍏徃', - customerObject: '椤圭洰閮�', - concurrentCount: 50, - priceType: 'free', - unitPrice: '鍏嶈垂:/骞�', - quantity: 1, - period: 1, - isPermanent: true, - status: 'FINISH', - }, - ], - }, -] +// 璁㈠崟鐘舵�佸璇濇鐩稿叧 +const orderStatusDialogVisible = ref(false) +const currentOrderId = ref<string>('') + +// 浣跨敤宸ヤ綔娴佺▼鎺у埗鍣ㄧ殑鐘舵�佹槧灏� +const statusUiToServer: Record<string, string> = { + WAIT_UPLOAD: OrderStatus.WAIT_UPLOAD, + WAIT_AUTHORIZE: OrderStatus.WAIT_AUTHORIZE, + WAIT_CONFIRM: OrderStatus.WAIT_CONFIRM, + COMPLETED: OrderStatus.COMPLETED, + EVALUATED: OrderStatus.EVALUATED, +} + +const statusServerToUi: Record<string, string> = { + [OrderStatus.WAIT_UPLOAD]: 'WAIT_UPLOAD', + [OrderStatus.WAIT_AUTHORIZE]: 'WAIT_AUTHORIZE', + [OrderStatus.WAIT_CONFIRM]: 'WAIT_CONFIRM', + [OrderStatus.COMPLETED]: 'COMPLETED', + [OrderStatus.EVALUATED]: 'EVALUATED', +} + +const formatDateTime = (val?: string) => { + if (!val) return '' + return val.replace('T', ' ').slice(0, 19) +} + +const normalizePriceType = (val?: string): 'points' | 'currency' | 'agreement' | 'free' => { + if (!val) return 'currency' + const s = String(val) + if (/(绉垎|points)/i.test(s)) return 'points' + if (/(鍗忚|agreement)/i.test(s)) return 'agreement' + if (/(鍏嶈垂|free)/i.test(s)) return 'free' + return 'currency' +} + + // 鑾峰彇鐘舵�佺被鍨� const getStatusType = (status: string) => { const statusMap: Record<string, 'warning' | 'danger' | 'success' | 'info'> = { + WAIT_UPLOAD: 'warning', + WAIT_AUTHORIZE: 'warning', WAIT_CONFIRM: 'warning', - WAIT_EVALUATE: 'warning', - FINISH: 'success', + COMPLETED: 'success', + EVALUATED: 'success', } return statusMap[status] || 'info' } @@ -574,23 +439,90 @@ } } -// 鎼滅储澶勭悊 +// 鎼滅储澶勭悊锛堟帴鍏ョ湡瀹炲悗绔級 const handleSearch = async () => { - // 浣跨敤妯℃嫙鏁版嵁 - const flatData: any[] = [] - mockData.forEach(order => { - flatData.push(order) - if (order.subOrders) { - order.subOrders.forEach((subOrder: any) => { - flatData.push({ ...subOrder, parentOrder: order }) - }) + try { + // 鑾峰彇鐢ㄦ埛ID浣滀负providerId锛屽鏋滄病鏈夊垯浣跨敤榛樿鍊� + const userId = userStore.getUserId + const providerId = userId ? Number(userId) : 1 // 浣跨敤榛樿鍊�1浣滀负涓存椂瑙e喅鏂规 + + const payload: any = { + pageNum: page.current, + pageSize: page.size, + productName: query.productName || undefined, + orderId: query.orderNo || undefined, + providerId: providerId, } - // 鍦ㄦ瘡涓鍗曞潡鐨勬湯灏炬彃鍏ヤ竴涓垎闅旂┖琛� - flatData.push({ isSpacer: true, isMainOrder: false, parentOrder: order }) - }) - - orderList.value = flatData - page.total = mockData.length + if (query.status) payload.orderStatus = statusUiToServer[query.status] + if (Array.isArray(query.dateRange) && query.dateRange.length === 2) { + payload.applyTimeStart = query.dateRange[0] + payload.applyTimeEnd = query.dateRange[1] + } + + const res = (await orderApi.getSellerOrderPage(payload)) as any + const pageData = res?.data + const list: any[] = Array.isArray(pageData?.list) ? pageData.list : [] + page.total = Number(pageData?.total || 0) + + // 骞跺彂鑾峰彇姣忎釜璁㈠崟鐨勮鎯咃紙鐢ㄤ簬鏋勯�犲瓙璁㈠崟琛岋級 + const detailsArr = await Promise.all( + list.map(async (order: any) => { + try { + const detailRes = (await orderApi.getOrderDetail(order.orderId)) as any + return detailRes?.data + } catch (e) { + return null + } + }) + ) + + const flatData: any[] = [] + list.forEach((order: any, idx: number) => { + const uiStatus = statusServerToUi[order.orderStatus] || 'WAIT_UPLOAD' + const mainRow: any = { + id: order.orderId, + isMainOrder: true, + applyTime: formatDateTime(order.applyTime || ''), + orderNo: order.orderId, + demandSide: order.demandSideName || '', + supplySide: order.providerName || '', + status: uiStatus, + statusName: order.orderStatus || '', + orderStatus: StatusMapper.toUIStatus(order.orderStatus), // 杞崲涓烘爣鍑嗙姸鎬佹灇涓� + } + + const detail = detailsArr[idx] + const subOrders: any[] = Array.isArray(detail?.orderDetails) + ? detail.orderDetails.map((d: any, i: number) => ({ + id: `${order.orderId}-${i + 1}`, + isMainOrder: false, + productName: order.productName || '', + suiteName: d.suiteName, + salesForm: d.salesForm, + accountCount: d.accountLimit, + customerObject: d.customerType, + concurrentCount: d.concurrentNodes, + priceType: normalizePriceType(d.priceType), + unitPrice: d.unitPrice, + quantity: d.quantity, + period: d.duration, + })) + : [] + + ;(mainRow as any).subOrders = subOrders + + flatData.push(mainRow) + subOrders.forEach((sub) => flatData.push({ ...sub, parentOrder: mainRow })) + flatData.push({ isSpacer: true, isMainOrder: false, parentOrder: mainRow }) + }) + + orderList.value = flatData + } catch (error: any) { + console.error('鏌ヨ鍗栧璁㈠崟鍒楄〃澶辫触:', error) + ElMessage.error(error?.message || '鏌ヨ鍗栧璁㈠崟鍒楄〃澶辫触') + orderList.value = [] + page.total = 0 + } } // 閲嶇疆 @@ -609,6 +541,32 @@ handleSearch() } +// 鑾峰彇鍙敤鎿嶄綔鍒楄〃 +const getAvailableActions = (order: any) => { + if (!order.orderStatus) return [] + return OrderWorkflowController.getAvailableActions(PageType.SELLER_CENTER, order.orderStatus) +} + +// 杩借釜璁㈠崟 - 鏄剧ず璁㈠崟鐘舵�佸璇濇 +const showOrderTrack = (row: any) => { + currentOrderId.value = row.id + orderStatusDialogVisible.value = true +} + +// 澶勭悊鎿嶄綔鐐瑰嚮 +const handleAction = (action: any, order: any) => { + switch (action.type) { + case ActionType.VIEW: + toDetail(order) + break + case ActionType.TRACK: + showOrderTrack(order) + break + default: + console.warn('鏈煡鐨勬搷浣滅被鍨�:', action.type) + } +} + // 璺敱璺宠浆 const toDetail = (row: any) => router.push({ name: 'tradeOrderDetail', params: { id: row.id } }) diff --git a/src/views/tradeManage/upload/index.vue b/src/views/tradeManage/upload/index.vue index 344e497..eceaea1 100644 --- a/src/views/tradeManage/upload/index.vue +++ b/src/views/tradeManage/upload/index.vue @@ -163,6 +163,7 @@ :on-exceed="onExceed" :on-remove="handleRemove" :show-file-list="false" + :before-upload="beforeUpload" > <el-button type="primary">閫夋嫨鏂囦欢</el-button> </el-upload> @@ -190,16 +191,47 @@ {{ formatFileSize(row.size) }} </template> </el-table-column> - <el-table-column label="鎿嶄綔" width="100"> + <el-table-column label="鎿嶄綔" width="280"> <template #default="{ row, $index }"> - <el-button - type="text" - size="small" - class="delete-btn" - @click="handleRemove(row, fileList)" - > - 鍒犻櫎 - </el-button> + <div class="file-actions"> + <el-button + type="text" + size="small" + class="upload-btn" + @click="handleUpload(row)" + v-if="!row.status || row.status === 'ready'" + :loading="row.uploading" + > + {{ row.uploading ? '涓婁紶涓�' : '涓婁紶' }} + </el-button> + <el-button + type="text" + size="small" + class="preview-btn" + @click="handlePreview(row)" + v-if="isPreviewable(row)" + :disabled="!isFileUploaded(row)" + > + 棰勮 + </el-button> + <el-button + type="text" + size="small" + class="download-btn" + @click="handleDownload(row)" + :disabled="!isFileUploaded(row)" + > + 涓嬭浇 + </el-button> + <el-button + type="text" + size="small" + class="delete-btn" + @click="handleRemove(row, fileList)" + > + 鍒犻櫎 + </el-button> + </div> </template> </el-table-column> </el-table> @@ -218,11 +250,15 @@ import { onMounted, reactive, ref, computed, type CSSProperties } from 'vue' import { useRoute, useRouter } from 'vue-router' import { Document, User, Goods, List } from '@element-plus/icons-vue' -import { fetchOrderDetail, uploadTradeFile } from '@/api/tradeManage' -import { ElMessage } from 'element-plus' +import { uploadTradeFile } from '@/api/tradeManage' +import orderApi from '@/api/orderApi' +import { ElMessage, ElMessageBox } from 'element-plus' +import { useUserInfo } from '@/stores/modules/userInfo' +import createAxios from '@/utils/axios' const route = useRoute() const router = useRouter() +const userStore = useUserInfo() const detail = reactive<any>({ items: [] }) const fileList = ref<any[]>([]) const orderTableWrapRef = ref<HTMLElement | null>(null) @@ -247,116 +283,319 @@ return [...detail.items, summaryRow] }) +// 鐘舵�佹槧灏勶紙鍚庣涓枃 -> 鍓嶇鏋氫妇锛� +const statusServerToUi: Record<string, string> = { + '寰呬笂浼犳枃浠�': 'WAIT_UPLOAD', + '寰呮巿鏉�': 'WAIT_AUTHORIZE', + '寰呬氦鏄撶‘璁�': 'WAIT_CONFIRM', + '宸插畬鎴�': 'COMPLETED', + '宸茶瘎浠�': 'EVALUATED', +} + +const formatDateTime = (val?: string) => (val ? val.replace('T', ' ').slice(0, 19) : '') + +const normalizePriceType = (val?: string): 'points' | 'currency' | 'agreement' | 'free' => { + if (!val) return 'currency' + const s = String(val) + if (/(绉垎|points)/i.test(s)) return 'points' + if (/(鍗忚|agreement)/i.test(s)) return 'agreement' + if (/(鍏嶈垂|free)/i.test(s)) return 'free' + return 'currency' +} + onMounted(async () => { - // 浣跨敤鍓嶇妯℃嫙鏁版嵁浠ヤ究寮�鍙� UI锛堜笉鏀瑰姩鍚庣鏈嶅姟锛� - const mockDetail = { - orderNo: '4348442557619205545', - resourceTypeName: '杞欢浜у搧', - status: 'WAIT_UPLOAD', - statusName: '寰呬笂浼犳枃浠�', - applyTime: '2025-05-21 10:00:00', - unitName: '涓氦鏂硅繙寤鸿鏈夐檺鍏徃', - userName: '寮犻潤', - userAccount: 'L20159922', - userDept: '淇℃伅涓績', - userPhone: '13800000000', - productName: '涓氦鏂硅繙鏅鸿兘瀹炴祴瀹為噺绠$悊绯荤粺', - supplier: '涓氦鏂硅繙绉戞妧鏈夐檺鍏徃', - industry: '寤虹瓚宸ョ▼', - projectUnit: '鍦熷缓宸ョ▼', - productType: '杞欢搴�', - productDesc: - '闈㈠悜宸ョ▼椤圭洰鐨勮川閲忓疄娴嬪疄閲忔暟瀛楀寲绠$悊绯荤粺锛屾敮鎸佹爣鍑嗗寲閲囬泦銆佽嚜鍔ㄧ粺璁′笌杩囩▼绠℃帶銆�', - items: [ - { - id: '1', - name: '浼佷笟绉佹湁SaaS鐗堣鍙�', - saleType: '涔版柇', - accountCount: 50, - customerTarget: '浼佷笟', - concurrentNodes: 50, - pricePoint: 50000, - priceCash: 0, - quantity: 1, - period: 0, - }, - { - id: '2', - name: '浼佷笟绉佹湁SaaS鐗圤TA鍗囩骇鏈嶅姟', - saleType: 'OTA鏈嶅姟', - accountCount: 50, - customerTarget: '浼佷笟', - concurrentNodes: 50, - pricePoint: 0, - priceCash: 7500, - quantity: 1, - period: 1, - }, - { - id: '3', - name: '浼佷笟绉佹湁SaaS鐗堢敤鎴峰閲忓寘', - saleType: '绉佹湁澧為噺鍖�', - accountCount: 100, - customerTarget: '浼佷笟', - concurrentNodes: 100, - pricePoint: 0, - priceCash: 0, - priceProtocol: true, - quantity: 1, - period: 1, - }, - { - id: '4', - name: '涓汉鍏湁SaaS鐗堣鍙�', - saleType: '绉佹湁澧為噺鍖�', - accountCount: 50, - customerTarget: '涓汉', - concurrentNodes: 50, - pricePoint: 0, - priceCash: 0, - quantity: 1, - period: 0, - }, - ], - pointTotal: '50,000', - cashTotal: '7,500', + const orderId = String(route.params.id || '') + if (!orderId) { + ElMessage.error('璁㈠崟ID涓嶈兘涓虹┖') + return } - Object.assign(detail, mockDetail) - - // 娣诲姞妯℃嫙鏂囦欢鏁版嵁鐢ㄤ簬灞曠ず - fileList.value = [ - { - name: '绛惧瓧鐩栫珷鏂囦欢.pdf', - size: 2621440, // 2.5MB - uid: '1', - status: 'success' - }, - { - name: 'API Keys.txt', - size: 354, // 354 Bytes - uid: '2', - status: 'success' + try { + const res = (await orderApi.getOrderDetail(orderId)) as any + const data = res?.data || {} + + const statusName: string = data.orderStatus || '' + const uiStatus = statusServerToUi[statusName] || 'INFO' + + // 鏄犲皠璁㈠崟璇︽儏澶撮儴淇℃伅 + const head = { + orderNo: data.orderId, + resourceTypeName: '杞欢浜у搧', + status: uiStatus, + statusName, + applyTime: formatDateTime(data.applyTime), + unitName: data.unitName || '-', + userName: data.userName || '-', + userAccount: data.userAccount || '-', + userDept: data.userDept || '-', + userPhone: data.userPhone || '-', + productName: data.productName || '-', + supplier: data.providerName || '-', + industry: data.industry || '-', + projectUnit: data.projectUnit || '-', + productType: data.productType || '-', + productDesc: data.productDesc || '-', } - ] - - // 娉ㄩ噴鎺夊師鏈夌殑API璋冪敤锛屼娇鐢ㄦā鎷熸暟鎹� - // const { data } = (await fetchOrderDetail({ id: route.params.id })) as any - // Object.assign(detail, data || {}) + + // 鏄庣粏椤规槧灏� + const items: any[] = Array.isArray(data.orderDetails) + ? data.orderDetails.map((d: any, idx: number) => { + const pt = normalizePriceType(d.priceType) + return { + id: String(d.id ?? idx + 1), + name: d.suiteName, + saleType: d.salesForm, + accountCount: d.accountLimit, + customerTarget: d.customerType, + concurrentNodes: d.concurrentNodes, + pricePoint: pt === 'points' ? Number(d.unitPrice || 0) : 0, + priceCash: pt === 'currency' ? Number(d.unitPrice || 0) : 0, + priceProtocol: pt === 'agreement', + quantity: Number(d.quantity || 0), + period: Number(d.duration || 0), + } + }) + : [] + + // 姹囨�伙紙绠�鍗曠浉鍔狅細鍗曚环*鏁伴噺锛� + const pointTotalNum = items.reduce((sum, it) => sum + Number(it.pricePoint || 0) * Number(it.quantity || 0), 0) + const cashTotalNum = items.reduce((sum, it) => sum + Number(it.priceCash || 0) * Number(it.quantity || 0), 0) + + Object.assign(detail, head, { + items, + pointTotal: pointTotalNum.toLocaleString(), + cashTotal: cashTotalNum.toLocaleString(), + }) + + // 濡傛灉鏈夊凡涓婁紶鐨勬枃浠讹紝鏄剧ず鍦ㄦ枃浠跺垪琛ㄤ腑 + if (data.attachments && Array.isArray(data.attachments)) { + fileList.value = data.attachments.map((file: any) => ({ + name: file.fileName, + size: file.fileSize, + uid: file.id, + status: 'success', + url: file.fileUrl + })) + } + } catch (error) { + console.error('鑾峰彇璁㈠崟璇︽儏澶辫触:', error) + ElMessage.error('鑾峰彇璁㈠崟璇︽儏澶辫触') + } }) const onExceed = () => ElMessage.warning('鏈�澶氶�夋嫨5涓枃浠�') + +// 鏂囦欢涓婁紶鍓嶇殑楠岃瘉 +const beforeUpload = (file: File) => { + // 妫�鏌ユ枃浠跺ぇ灏忥紙闄愬埗涓�100MB锛� + const maxSize = 100 * 1024 * 1024 + if (file.size > maxSize) { + ElMessage.error('鏂囦欢澶у皬涓嶈兘瓒呰繃100MB') + return false + } + + // 妫�鏌ユ枃浠跺悕闀垮害 + if (file.name.length > 100) { + ElMessage.error('鏂囦欢鍚嶇О涓嶈兘瓒呰繃100瀛楃') + return false + } + + // 璁剧疆鏂囦欢鍒濆鐘舵�� + const fileObj = { + name: file.name, + size: file.size, + type: file.type, + raw: file, + status: 'ready', // 鍒濆鐘舵�佷负鍑嗗涓婁紶 + uploading: false, + uploaded: false + } + + // 灏嗘枃浠舵坊鍔犲埌鏂囦欢鍒楄〃 + fileList.value.push(fileObj) + + return false // 闃绘鑷姩涓婁紶锛屾敼涓烘墜鍔ㄤ笂浼� +} + +// 涓婁紶鍗曚釜鏂囦欢鍒版湇鍔″櫒 +const uploadSingleFile = async (file: File) => { + const formData = new FormData() + formData.append('file', file) + formData.append('folder', 'order-attachments') + + try { + console.log('寮�濮嬩笂浼犳枃浠�:', file.name, '澶у皬:', file.size) + + const response = await createAxios({ + url: '/admin/file/upload', + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data' + }, + data: formData + }) + + console.log('鏂囦欢涓婁紶鍝嶅簲:', response) + + // 妫�鏌ュ搷搴旀牸寮� - 鏍规嵁瀹為檯杩斿洖鏍煎紡璋冩暣 + const responseData = response as any + + // 鏍规嵁瀹為檯鍝嶅簲鏍煎紡锛岀洿鎺ユ鏌� responseData.code + if (responseData && responseData.code === 200) { + console.log('鏂囦欢涓婁紶鎴愬姛锛岃繑鍥炴暟鎹�:', responseData.data) + return responseData.data // 杩斿洖鏂囦欢URL + } else if (responseData && responseData.data && responseData.data.code === 200) { + // 澶囩敤妫�鏌ワ細濡傛灉鍝嶅簲琚寘瑁呭湪 data 涓� + console.log('鏂囦欢涓婁紶鎴愬姛锛岃繑鍥炴暟鎹�:', responseData.data.data) + return responseData.data.data // 杩斿洖鏂囦欢URL + } else { + // 澶勭悊閿欒鎯呭喌 + const errorMsg = responseData?.msg || responseData?.data?.msg || responseData?.message || responseData?.data?.message || '鏂囦欢涓婁紶澶辫触' + console.error('鏂囦欢涓婁紶澶辫触锛岄敊璇俊鎭�:', errorMsg) + throw new Error(errorMsg) + } + } catch (error) { + console.error('鏂囦欢涓婁紶寮傚父:', error) + throw error + } +} + +// 淇濆瓨鏂囦欢淇℃伅鍒版暟鎹簱 +const saveFileInfo = async (fileData: any) => { + try { + console.log('寮�濮嬩繚瀛樻枃浠朵俊鎭�:', fileData) + + // 浣跨敤FormData鏍煎紡锛屽洜涓哄悗绔娇鐢ˊRequestParam鎺ユ敹鍙傛暟 + const formData = new FormData() + formData.append('orderId', fileData.orderId) + formData.append('fileName', fileData.fileName) + formData.append('originalName', fileData.originalName) + formData.append('fileType', fileData.fileType) + formData.append('fileSize', fileData.fileSize.toString()) + formData.append('fileUrl', fileData.fileUrl) + formData.append('bucketName', fileData.bucketName) + formData.append('objectName', fileData.objectName) + formData.append('uploadUserId', fileData.uploadUserId.toString()) + formData.append('uploadUserName', fileData.uploadUserName) + formData.append('attachmentType', fileData.attachmentType) + formData.append('description', fileData.description) + + console.log('鍑嗗鍙戦�佺殑鏂囦欢淇℃伅:', { + orderId: fileData.orderId, + fileName: fileData.fileName, + fileSize: fileData.fileSize, + fileUrl: fileData.fileUrl, + uploadUserId: fileData.uploadUserId, + uploadUserName: fileData.uploadUserName + }) + + const response = await createAxios({ + url: '/admin/api/order/attachment/upload', + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data' + }, + data: formData + }) + + console.log('淇濆瓨鏂囦欢淇℃伅鍝嶅簲:', response) + + // 浣跨敤涓庢枃浠朵笂浼犵浉鍚岀殑鍝嶅簲鍒ゆ柇閫昏緫 + const responseData = response as any + + if (responseData && responseData.code === 200) { + console.log('鏂囦欢淇℃伅淇濆瓨鎴愬姛锛岃繑鍥炵殑闄勪欢ID:', responseData.data) + const attachmentId = responseData.data + + // 楠岃瘉attachmentId鏄惁涓烘湁鏁堢殑鏁板瓧 + if (typeof attachmentId === 'number' && attachmentId > 0) { + return attachmentId + } else { + console.error('杩斿洖鐨勯檮浠禝D涓嶆槸鏈夋晥鏁板瓧:', attachmentId, typeof attachmentId) + throw new Error(`鏃犳晥鐨勯檮浠禝D: ${attachmentId}`) + } + } else if (responseData && responseData.data && responseData.data.code === 200) { + // 澶囩敤妫�鏌ワ細濡傛灉鍝嶅簲琚寘瑁呭湪 data 涓� + console.log('鏂囦欢淇℃伅淇濆瓨鎴愬姛锛堝鐢ㄦ鏌ワ級锛岃繑鍥炵殑闄勪欢ID:', responseData.data.data) + const attachmentId = responseData.data.data + + // 楠岃瘉attachmentId鏄惁涓烘湁鏁堢殑鏁板瓧 + if (typeof attachmentId === 'number' && attachmentId > 0) { + return attachmentId + } else { + console.error('杩斿洖鐨勯檮浠禝D涓嶆槸鏈夋晥鏁板瓧锛堝鐢ㄦ鏌ワ級:', attachmentId, typeof attachmentId) + throw new Error(`鏃犳晥鐨勯檮浠禝D: ${attachmentId}`) + } + } else { + // 澶勭悊閿欒鎯呭喌 + const errorMsg = responseData?.msg || responseData?.data?.msg || responseData?.message || responseData?.data?.message || '淇濆瓨鏂囦欢淇℃伅澶辫触' + console.error('鏂囦欢淇℃伅淇濆瓨澶辫触:', errorMsg) + throw new Error(errorMsg) + } + } catch (error) { + console.error('淇濆瓨鏂囦欢淇℃伅寮傚父:', error) + throw error + } +} + + + const goBack = () => router.back() + +// 鎻愪氦鏂囦欢骞舵洿鏂拌鍗曠姸鎬� const submit = async () => { - // 妯℃嫙鎻愪氦鎴愬姛鍝嶅簲 - const mockResponse = { code: 200 } - - // 娉ㄩ噴鎺夊師鏈夌殑API璋冪敤锛屼娇鐢ㄦā鎷熸暟鎹� - // const { code } = (await uploadTradeFile({ id: route.params.id, files: fileList.value })) as any - - if (mockResponse.code === 200) { + if (fileList.value.length === 0) { + ElMessage.warning('璇疯嚦灏戜笂浼犱竴涓枃浠�') + return + } + + // 妫�鏌ユ槸鍚︽湁鏈笂浼犵殑鏂囦欢 + const unuploadedFiles = fileList.value.filter(file => !file.uploaded && !file.url) + if (unuploadedFiles.length > 0) { + ElMessage.warning('璇峰厛涓婁紶鎵�鏈夋枃浠�') + return + } + + try { + const orderId = String(route.params.id || '') + const userId = userStore.getUserId + const userName = userStore.username || userStore.name || '鏈煡鐢ㄦ埛' + + // 涓婁紶鎵�鏈夋湭涓婁紶鐨勬枃浠� + const uploadPromises = fileList.value + .filter(fileItem => fileItem.raw && !fileItem.uploaded) + .map(async (fileItem) => { + const fileUrl = await uploadSingleFile(fileItem.raw) + + // 淇濆瓨鏂囦欢淇℃伅鍒版暟鎹簱 + const attachmentData = { + orderId: orderId, + fileName: fileItem.name, + originalName: fileItem.name, + fileType: fileItem.type || 'application/octet-stream', + fileSize: fileItem.size, + fileUrl: fileUrl, + bucketName: 'order-attachments', + objectName: fileUrl.split('/').pop(), + uploadUserId: userId, + uploadUserName: userName, + attachmentType: 'TRADE_FILE', + description: '浜ゆ槗鏂囦欢' + } + + await saveFileInfo(attachmentData) + }) + + await Promise.all(uploadPromises) + + // 鏇存柊璁㈠崟鐘舵�佽繘鍏ヤ笅涓�涓姸鎬� + await orderApi.updateOrderStatusToNext(orderId) + ElMessage.success('鎻愪氦鎴愬姛') router.back() + } catch (error) { + console.error('鎻愪氦澶辫触:', error) + ElMessage.error(error instanceof Error ? error.message : '鎻愪氦澶辫触') } } @@ -456,13 +695,365 @@ // 鏂囦欢鍒楄〃琛ㄦ牸琛ㄤ綋鏂囧瓧澶у皬 const fileTableCellStyle: CSSProperties = { fontSize: '12px' }; +// 鍒ゆ柇鏂囦欢鏄惁鍙瑙� +const isPreviewable = (file: any) => { + // 棣栧厛妫�鏌IME绫诲瀷 + const previewableTypes = [ + 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/webp', + 'text/plain', 'text/html', 'text/css', 'text/javascript', + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx + ] + + // 濡傛灉MIME绫诲瀷鍖归厤锛岀洿鎺ヨ繑鍥瀟rue + if (previewableTypes.includes(file.type || '')) { + return true + } + + // 濡傛灉MIME绫诲瀷涓虹┖鎴栦笉鍖归厤锛屾牴鎹枃浠舵墿灞曞悕鍒ゆ柇 + const fileName = file.name || '' + const fileExtension = fileName.toLowerCase().split('.').pop() + + const previewableExtensions = [ + 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', + 'txt', 'html', 'htm', 'css', 'js', + 'pdf', + 'docx', 'xlsx', 'pptx' + ] + + return previewableExtensions.includes(fileExtension) +} + +// 鏂囦欢棰勮 +const handlePreview = async (file: any) => { + if (!file.url) { + ElMessage.warning('鏂囦欢閾炬帴涓嶅瓨鍦�') + return + } + + // 鑾峰彇鏂囦欢鎵╁睍鍚� + const fileName = file.name || '' + const fileExtension = fileName.toLowerCase().split('.').pop() + + let previewUrl = file.url + + // 濡傛灉鏂囦欢瀛樺偍鍦∕inIO锛屼紭鍏堜娇鐢ㄩ瑙圲RL + if (file.url.includes('order-attachments')) { + try { + // 棣栧厛灏濊瘯鑾峰彇棰勮URL + const previewResponse = await createAxios({ + url: `/admin/file/preview`, + method: 'GET', + params: { + fileName: file.url + } + }) + + console.log('棰勮URL鍝嶅簲:', previewResponse) + + // 妫�鏌ュ搷搴旀牸寮� + const responseData = previewResponse as any + if (responseData && responseData.code === 200 && responseData.data) { + previewUrl = responseData.data + console.log('浣跨敤棰勮URL:', previewUrl) + } else { + console.log('棰勮URL鑾峰彇澶辫触锛屼娇鐢ㄤ笅杞芥柟寮�') + // 濡傛灉棰勮URL鑾峰彇澶辫触锛屽洖閫�鍒颁笅杞芥柟寮� + const response = await createAxios({ + url: `/admin/file/download`, + method: 'GET', + responseType: 'blob', + params: { + fileName: file.url, + originalName: file.name + } + }) + + // 鍒涘缓棰勮URL + const blob = new Blob([response as any]) + previewUrl = window.URL.createObjectURL(blob) + console.log('浣跨敤Blob URL:', previewUrl) + } + } catch (error) { + console.error('鑾峰彇鏂囦欢澶辫触:', error) + ElMessage.error('鑾峰彇鏂囦欢澶辫触锛屾棤娉曢瑙�') + return + } + } + + // 鍥剧墖鏂囦欢鐩存帴鍦ㄦ柊绐楀彛鎵撳紑 + if ((file.type && file.type.startsWith('image/')) || + ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension)) { + console.log('棰勮鍥剧墖鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // PDF鏂囦欢鍦ㄦ柊绐楀彛鎵撳紑 + if (file.type === 'application/pdf' || fileExtension === 'pdf') { + console.log('棰勮PDF鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // 鏂囨湰鏂囦欢鍦ㄦ柊绐楀彛鎵撳紑 + if ((file.type && file.type.startsWith('text/')) || + ['txt', 'html', 'htm', 'css', 'js'].includes(fileExtension)) { + console.log('棰勮鏂囨湰鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + return + } + + // Office鏂囨。鍜屽叾浠栨枃浠剁被鍨嬶紝灏濊瘯鍦ㄦ柊绐楀彛鎵撳紑 + try { + console.log('棰勮鍏朵粬鏂囦欢:', previewUrl) + window.open(previewUrl, '_blank') + } catch (error) { + console.error('棰勮澶辫触:', error) + ElMessage.error('棰勮澶辫触锛岃涓嬭浇鍚庢煡鐪�') + } +} + +// 鍒ゆ柇鏂囦欢鏄惁宸蹭笂浼犳垚鍔� +const isFileUploaded = (file: any) => { + // 鏂囦欢鏈塽rl涓旂姸鎬佷负success琛ㄧず宸蹭笂浼犳垚鍔� + return file.url && (file.status === 'success' || file.uploaded) +} + +// 鍗曚釜鏂囦欢涓婁紶 +const handleUpload = async (file: any) => { + if (!file.raw) { + ElMessage.warning('鏂囦欢鏁版嵁涓嶅瓨鍦�') + return + } + + // 璁剧疆涓婁紶鐘舵�� + file.uploading = true + + try { + // 涓婁紶鏂囦欢鍒版湇鍔″櫒 + const fileUrl = await uploadSingleFile(file.raw) + + // 淇濆瓨鏂囦欢淇℃伅鍒版暟鎹簱 + const orderId = String(route.params.id || '') + const userId = userStore.getUserId + const userName = userStore.username || userStore.name || '鏈煡鐢ㄦ埛' + + const attachmentData = { + orderId: orderId, + fileName: file.name, + originalName: file.name, + fileType: file.type || 'application/octet-stream', + fileSize: file.size, + fileUrl: fileUrl, + bucketName: 'order-attachments', + objectName: fileUrl.split('/').pop(), + uploadUserId: userId, + uploadUserName: userName, + attachmentType: 'TRADE_FILE', + description: '浜ゆ槗鏂囦欢' + } + + const attachmentId = await saveFileInfo(attachmentData) + + // 鏇存柊鏂囦欢鐘舵�� + file.url = fileUrl + file.uid = attachmentId // 璁剧疆姝g‘鐨勯檮浠禝D + file.status = 'success' + file.uploaded = true + file.uploading = false + + ElMessage.success('鏂囦欢涓婁紶鎴愬姛') + } catch (error) { + console.error('鏂囦欢涓婁紶澶辫触:', error) + file.uploading = false + ElMessage.error(error instanceof Error ? error.message : '鏂囦欢涓婁紶澶辫触') + } +} + +// 鏂囦欢涓嬭浇 +const handleDownload = async (file: any) => { + if (!file.url) { + ElMessage.warning('鏂囦欢閾炬帴涓嶅瓨鍦�') + return + } + + console.log('寮�濮嬩笅杞芥枃浠�:', file.name, 'URL:', file.url) + + try { + // 濡傛灉鏂囦欢瀛樺偍鍦∕inIO锛屼娇鐢ㄥ悗绔洿鎺ヤ笅杞紸PI + if (file.url.includes('order-attachments')) { + console.log('浣跨敤MinIO涓嬭浇API') + + // 浣跨敤axios閫氳繃浠g悊璁块棶鍚庣API + const response = await createAxios({ + url: `/admin/file/download`, + method: 'GET', + responseType: 'blob', + params: { + fileName: file.url, + originalName: file.name + } + }) + + console.log('涓嬭浇鍝嶅簲:', response) + + // 鍒涘缓涓嬭浇閾炬帴 + const blob = new Blob([response as any]) + const downloadUrl = window.URL.createObjectURL(blob) + + console.log('鍒涘缓涓嬭浇閾炬帴:', downloadUrl) + + const link = document.createElement('a') + link.href = downloadUrl + link.download = file.name || 'download' + link.target = '_blank' + link.rel = 'noopener noreferrer' + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + // 娓呯悊URL瀵硅薄 + window.URL.revokeObjectURL(downloadUrl) + + ElMessage.success('鏂囦欢涓嬭浇鎴愬姛') + } else { + console.log('浣跨敤鐩存帴URL涓嬭浇') + // 鍏朵粬鎯呭喌鐩存帴浣跨敤鍘烾RL + const link = document.createElement('a') + link.href = file.url + link.download = file.name || 'download' + link.target = '_blank' + link.rel = 'noopener noreferrer' + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + ElMessage.success('寮�濮嬩笅杞芥枃浠�') + } + } catch (error) { + console.error('涓嬭浇澶辫触:', error) + ElMessage.error('涓嬭浇澶辫触锛岃閲嶈瘯') + } +} + +// 鍒犻櫎MinIO鏂囦欢 +const deleteMinioFile = async (fileName: string) => { + try { + console.log('寮�濮嬪垹闄inIO鏂囦欢:', fileName) + + const response = await createAxios({ + url: `/admin/file/delete`, + method: 'DELETE', + params: { + fileName: fileName + } + }) + + console.log('鍒犻櫎MinIO鏂囦欢鍝嶅簲:', response) + + const responseData = response as any + if (responseData && responseData.code === 200) { + console.log('MinIO鏂囦欢鍒犻櫎鎴愬姛') + return true + } else { + const errorMsg = responseData?.msg || responseData?.data?.msg || responseData?.message || responseData?.data?.message || '鍒犻櫎MinIO鏂囦欢澶辫触' + console.error('鍒犻櫎MinIO鏂囦欢澶辫触:', errorMsg) + throw new Error(errorMsg) + } + } catch (error) { + console.error('鍒犻櫎MinIO鏂囦欢寮傚父:', error) + throw error + } +} + +// 鍒犻櫎鏁版嵁搴撻檮浠惰褰� +const deleteAttachmentRecord = async (attachmentId: number) => { + try { + console.log('寮�濮嬪垹闄ゆ暟鎹簱闄勪欢璁板綍:', attachmentId) + + const response = await createAxios({ + url: `/admin/api/order/attachment/delete/${attachmentId}`, + method: 'DELETE' + }) + + console.log('鍒犻櫎鏁版嵁搴撻檮浠惰褰曞搷搴�:', response) + + const responseData = response as any + if (responseData && responseData.code === 200) { + console.log('鏁版嵁搴撻檮浠惰褰曞垹闄ゆ垚鍔�') + return true + } else { + const errorMsg = responseData?.msg || responseData?.data?.msg || responseData?.message || responseData?.data?.message || '鍒犻櫎鏁版嵁搴撻檮浠惰褰曞け璐�' + console.error('鍒犻櫎鏁版嵁搴撻檮浠惰褰曞け璐�:', errorMsg) + throw new Error(errorMsg) + } + } catch (error) { + console.error('鍒犻櫎鏁版嵁搴撻檮浠惰褰曞紓甯�:', error) + throw error + } +} + // 鏂囦欢鍒楄〃绉婚櫎鏂囦欢 -const handleRemove = (file: any, uploadFiles: any) => { - // 浠庢枃浠跺垪琛ㄤ腑绉婚櫎鎸囧畾鏂囦欢 - const index = fileList.value.findIndex(item => item.uid === file.uid) - if (index > -1) { - fileList.value.splice(index, 1) - ElMessage.success('鏂囦欢宸插垹闄�') +const handleRemove = async (file: any, uploadFiles: any) => { + try { + // 鏄剧ず纭瀵硅瘽妗� + await ElMessageBox.confirm( + `纭畾瑕佸垹闄ゆ枃浠� "${file.name}" 鍚楋紵`, + '鍒犻櫎纭', + { + confirmButtonText: '纭畾', + cancelButtonText: '鍙栨秷', + type: 'warning', + center: true + } + ) + + // 鐢ㄦ埛纭鍚庢墽琛屽垹闄ゆ搷浣� + // 濡傛灉鏄凡涓婁紶鐨勬枃浠讹紙鏈塽id涓斾负鏁板瓧锛岃〃绀烘暟鎹簱璁板綍ID锛� + if (file.uid && !isNaN(file.uid) && file.url) { + console.log('鍒犻櫎宸蹭笂浼犵殑鏂囦欢:', file.name, '闄勪欢ID:', file.uid) + + // 1. 鍒犻櫎MinIO涓殑鏂囦欢 + if (file.url.includes('order-attachments')) { + await deleteMinioFile(file.url) + } + + // 2. 鍒犻櫎鏁版嵁搴撲腑鐨勯檮浠惰褰� + await deleteAttachmentRecord(file.uid) + + // 3. 浠庢枃浠跺垪琛ㄤ腑绉婚櫎 + const index = fileList.value.findIndex(item => item.uid === file.uid) + if (index > -1) { + fileList.value.splice(index, 1) + } + + ElMessage.success('鏂囦欢鍒犻櫎鎴愬姛') + } else { + // 濡傛灉鏄湭涓婁紶鐨勬枃浠讹紙鍙湁raw鏂囦欢瀵硅薄锛� + console.log('鍒犻櫎鏈笂浼犵殑鏂囦欢:', file.name) + + // 鐩存帴浠庢枃浠跺垪琛ㄤ腑绉婚櫎 + const index = fileList.value.findIndex(item => item.name === file.name && item.size === file.size) + if (index > -1) { + fileList.value.splice(index, 1) + } + + ElMessage.success('鏂囦欢宸插垹闄�') + } + } catch (error) { + // 濡傛灉鐢ㄦ埛鍙栨秷鍒犻櫎锛宔rror涓�'cancel'锛屼笉闇�瑕佹樉绀洪敊璇秷鎭� + if (error === 'cancel') { + console.log('鐢ㄦ埛鍙栨秷鍒犻櫎鎿嶄綔') + return + } + + console.error('鍒犻櫎鏂囦欢澶辫触:', error) + ElMessage.error(error instanceof Error ? error.message : '鍒犻櫎鏂囦欢澶辫触') } } </script> @@ -649,10 +1240,50 @@ color: #409eff; } } - .delete-btn { - color: #f56c6c; - &:hover { - text-decoration: underline; + .file-actions { + display: flex; + gap: 8px; + align-items: center; + justify-content: center; + + .upload-btn { + color: #e6a23c; + &:hover { + text-decoration: underline; + } + &:disabled { + color: #c0c4cc; + cursor: not-allowed; + } + } + + .preview-btn { + color: #409eff; + &:hover { + text-decoration: underline; + } + &:disabled { + color: #c0c4cc; + cursor: not-allowed; + } + } + + .download-btn { + color: #67c23a; + &:hover { + text-decoration: underline; + } + &:disabled { + color: #c0c4cc; + cursor: not-allowed; + } + } + + .delete-btn { + color: #f56c6c; + &:hover { + text-decoration: underline; + } } } } -- Gitblit v1.8.0