| | |
| | | <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) => { |
| | | // 首先检查MIME类型 |
| | | 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类型匹配,直接返回true |
| | | 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 |
| | | |
| | | // 如果文件存储在MinIO,优先使用预览URL |
| | | 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 { |
| | | // 如果文件存储在MinIO,使用后端直接下载API |
| | | if (fileUrl.includes('order-attachments')) { |
| | | console.log('使用MinIO下载API') |
| | | |
| | | // 使用axios通过代理访问后端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下载') |
| | | // 其他情况直接使用原URL |
| | | 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] // 普通行正常显示 |
| | | } |
| | | |
| | | // 症结与修复说明: |
| | | // 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> |
| | | |
| | | |