|
|
@@ -50,6 +50,12 @@
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
<template #header>
|
|
|
+ <!-- 模块标题 -->
|
|
|
+ <div class="module-title mb-4">
|
|
|
+ <span class="module-title-bar"></span>
|
|
|
+ <span class="module-title-text">报价单管理</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- 视图切换 -->
|
|
|
<el-radio-group v-model="activeView" @change="handleViewChange" class="mb-4">
|
|
|
<el-radio-button value="my">我的报价</el-radio-button>
|
|
|
@@ -349,11 +355,11 @@
|
|
|
<el-tag v-else type="info" size="small">待审批</el-tag>
|
|
|
</div>
|
|
|
<div v-if="item.approvalOpinion" class="approval-opinion">
|
|
|
- <span class="label">审批意见:</span>
|
|
|
+ <span class="label">审批意见:</span>
|
|
|
<span>{{ item.approvalOpinion }}</span>
|
|
|
</div>
|
|
|
<div v-if="item.approvalTime" class="approval-time">
|
|
|
- <span class="label">审批时间:</span>
|
|
|
+ <span class="label">审批时间:</span>
|
|
|
<span>{{ item.approvalTime }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -397,11 +403,295 @@
|
|
|
</div>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
+
|
|
|
+ <!-- 详情抽屉 -->
|
|
|
+ <el-drawer v-model="detailDrawerVisible" title="报价单详情" size="80%" direction="rtl" @close="handleDetailDrawerClose">
|
|
|
+ <div class="drawer-content" v-loading="detailLoading">
|
|
|
+ <!-- 头部操作按钮 -->
|
|
|
+ <div class="detail-header">
|
|
|
+ <span class="detail-title">{{ detailQuote?.quoteNo }} - {{ detailQuote?.quoteTitle }}</span>
|
|
|
+ <div class="detail-actions">
|
|
|
+ <el-button type="primary" icon="Edit" @click="handleDetailEdit" v-if="detailQuote?.status === 'draft'">修改</el-button>
|
|
|
+ <el-button type="warning" icon="Check" @click="handleDetailSubmit" v-if="detailQuote?.status === 'draft'">提交审批</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-tabs v-model="detailActiveTab" class="mt-4">
|
|
|
+ <!-- 基本信息 -->
|
|
|
+ <el-tab-pane label="基本信息" name="basic">
|
|
|
+ <el-descriptions :column="2" border>
|
|
|
+ <el-descriptions-item label="报价单号">{{ detailQuote?.quoteNo }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="报价单标题">{{ detailQuote?.quoteTitle }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="客户名称">
|
|
|
+ <el-link type="primary" @click="handleDetailViewCustomer">{{ detailQuote?.customerName }}</el-link>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="联系人">{{ detailQuote?.contactName }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="联系手机">{{ detailQuote?.contactMobile }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="联系邮箱">{{ detailQuote?.contactEmail }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="版本号">
|
|
|
+ <el-tag v-if="detailQuote?.isLatest === 1" type="success">{{ detailQuote?.version }} (最新)</el-tag>
|
|
|
+ <el-tag v-else type="info">{{ detailQuote?.version }}</el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="状态">
|
|
|
+ <dict-tag :options="crm_quote_status" :value="detailQuote?.status" />
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="报价日期">{{ detailQuote?.quoteDate }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="有效期至">
|
|
|
+ <span :style="{ color: getDetailExpireDateColor(detailQuote?.expireDate) }">
|
|
|
+ {{ detailQuote?.expireDate }} ({{ getDetailDaysRemaining(detailQuote?.expireDate) }})
|
|
|
+ </span>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="负责人">{{ detailQuote?.ownerUserName }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="创建时间">{{ parseTime(detailQuote?.createTime) }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <!-- 金额汇总 -->
|
|
|
+ <el-divider content-position="left">金额汇总</el-divider>
|
|
|
+ <el-descriptions :column="2" border>
|
|
|
+ <el-descriptions-item label="产品总金额">
|
|
|
+ <span class="amount-text">¥{{ formatDetailAmount(detailQuote?.totalAmount) }}</span>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="最终金额">
|
|
|
+ <span class="final-amount-text">¥{{ formatDetailAmount(detailQuote?.finalAmount) }}</span>
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <!-- 产品明细 -->
|
|
|
+ <el-divider content-position="left">产品明细</el-divider>
|
|
|
+ <el-table :data="detailQuote?.productList" border stripe>
|
|
|
+ <el-table-column type="index" label="序号" width="60" align="center" />
|
|
|
+ <el-table-column label="产品名称" prop="productName" min-width="120" :show-overflow-tooltip="true" />
|
|
|
+ <el-table-column label="型号/规格" prop="model" min-width="130" :show-overflow-tooltip="true">
|
|
|
+ <template #default="scope">
|
|
|
+ <span>{{ scope.row.model || '-' }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="产品分类" prop="categoryName" min-width="110" :show-overflow-tooltip="true" />
|
|
|
+ <el-table-column label="税率(%)" prop="taxRate" width="110" align="right">
|
|
|
+ <template #default="scope">
|
|
|
+ <span>{{ scope.row.taxRate || '0' }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="单位" prop="unit" width="80" align="center">
|
|
|
+ <template #default="scope">
|
|
|
+ <dict-tag :options="crm_product_unit" :value="scope.row.unit" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="数量" prop="quantity" width="110" align="right">
|
|
|
+ <template #default="scope">
|
|
|
+ <span>{{ scope.row.quantity || 0 }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="含税单价/元" prop="price" width="130" align="right">
|
|
|
+ <template #default="scope">
|
|
|
+ <span>{{ formatDetailAmount(scope.row.price) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="含税总价/元" prop="subtotal" width="130" align="right">
|
|
|
+ <template #default="scope">
|
|
|
+ <span class="amount-text">¥{{ formatDetailAmount(scope.row.subtotal) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="税费/元" prop="taxAmount" width="120" align="right">
|
|
|
+ <template #default="scope">
|
|
|
+ <span class="text-warning">¥{{ formatDetailAmount(scope.row.taxAmount) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="包装规格" prop="packageSpec" min-width="150" :show-overflow-tooltip="true">
|
|
|
+ <template #default="scope">
|
|
|
+ <span>{{ scope.row.packageSpec || '-' }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="送货方式" prop="deliveryMethod" min-width="150" :show-overflow-tooltip="true">
|
|
|
+ <template #default="scope">
|
|
|
+ <span>{{ scope.row.deliveryMethod || '-' }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 产品汇总 -->
|
|
|
+ <div class="product-summary mt-4">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-statistic title="产品数量" :value="detailQuote?.productList?.length || 0" suffix="个" />
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-statistic title="产品总金额" :value="detailQuote?.totalAmount || 0" prefix="¥" :precision="2" />
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-statistic title="最终金额" :value="detailQuote?.finalAmount || 0" prefix="¥" :precision="2" />
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 报价条款 -->
|
|
|
+ <el-divider content-position="left">报价条款</el-divider>
|
|
|
+ <el-descriptions :column="1" border>
|
|
|
+ <el-descriptions-item label="备注">{{ detailQuote?.remark }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <!-- 附件 -->
|
|
|
+ <el-divider content-position="left" v-if="detailAttachmentList.length > 0">附件</el-divider>
|
|
|
+ <div v-if="detailAttachmentList.length > 0" class="attachment-section">
|
|
|
+ <div class="attachment-header">
|
|
|
+ <el-icon><Paperclip /></el-icon>
|
|
|
+ <span class="attachment-title">附件 ({{ detailAttachmentList.length }})</span>
|
|
|
+ </div>
|
|
|
+ <div class="attachment-list">
|
|
|
+ <div v-for="(file, index) in detailAttachmentList" :key="index" class="upload-file-card">
|
|
|
+ <!-- 缩略图区域 -->
|
|
|
+ <div class="upload-file-thumb">
|
|
|
+ <ImagePreview v-if="isDetailImageFile(file)" :src="file.ossId" class="upload-thumb-image" :isOssId="true" />
|
|
|
+ <div v-else class="upload-file-placeholder">
|
|
|
+ <el-icon><Document /></el-icon>
|
|
|
+ <span class="file-ext">{{ getDetailFileExt(file.name) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 文件名称 -->
|
|
|
+ <div class="upload-file-name" :title="file.name">
|
|
|
+ {{ getDetailShortFileName(file.name) }}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 下载按钮 -->
|
|
|
+ <div class="upload-file-card-actions">
|
|
|
+ <el-button type="success" size="small" @click.stop="handleDetailDownloadFile(file)">
|
|
|
+ <el-icon><Download /></el-icon>
|
|
|
+ 下载
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- 审批流程 -->
|
|
|
+ <el-tab-pane label="审批流程" name="approval">
|
|
|
+ <!-- 审批状态卡片 -->
|
|
|
+ <el-alert
|
|
|
+ v-if="detailQuote?.status === 'submitted'"
|
|
|
+ title="审批中"
|
|
|
+ type="info"
|
|
|
+ description="报价单正在审批流程中,请耐心等待审批结果"
|
|
|
+ :closable="false"
|
|
|
+ class="mb-4"
|
|
|
+ />
|
|
|
+ <el-alert
|
|
|
+ v-else-if="detailQuote?.status === 'accepted'"
|
|
|
+ title="审批已通过"
|
|
|
+ type="success"
|
|
|
+ description="报价单已通过全部审批流程"
|
|
|
+ :closable="false"
|
|
|
+ class="mb-4"
|
|
|
+ />
|
|
|
+ <el-alert
|
|
|
+ v-else-if="detailQuote?.status === 'rejected'"
|
|
|
+ title="审批已拒绝"
|
|
|
+ type="error"
|
|
|
+ :description="`拒绝原因:${detailQuote?.rejectReason || '未填写'}`"
|
|
|
+ :closable="false"
|
|
|
+ class="mb-4"
|
|
|
+ />
|
|
|
+ <el-alert
|
|
|
+ v-else-if="detailQuote?.status === 'contract'"
|
|
|
+ title="已转合同"
|
|
|
+ type="success"
|
|
|
+ description="报价单已通过审批并转为合同"
|
|
|
+ :closable="false"
|
|
|
+ class="mb-4"
|
|
|
+ />
|
|
|
+
|
|
|
+ <el-empty v-if="detailApprovalList.length === 0" description="暂无审批记录" />
|
|
|
+ <el-timeline v-else>
|
|
|
+ <el-timeline-item
|
|
|
+ v-for="item in detailApprovalList"
|
|
|
+ :key="item.id"
|
|
|
+ :timestamp="`第${item.step}步 - ${item.createTime}`"
|
|
|
+ placement="top"
|
|
|
+ :color="getDetailApprovalColor(item.approvalResult)"
|
|
|
+ :hollow="item.approvalResult === 'pending'"
|
|
|
+ >
|
|
|
+ <el-card>
|
|
|
+ <div class="approval-item">
|
|
|
+ <div class="approval-header">
|
|
|
+ <div>
|
|
|
+ <el-avatar :size="32" class="mr-2">{{ item.approverName?.substring(0, 1) }}</el-avatar>
|
|
|
+ <span class="approver-name">{{ item.approverName }}</span>
|
|
|
+ <el-tag v-if="item.approvalResult === 'pending'" type="info" size="small" class="ml-2">待审批</el-tag>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <el-tag v-if="item.approvalResult === 'approved'" type="success" size="small">已通过</el-tag>
|
|
|
+ <el-tag v-else-if="item.approvalResult === 'rejected'" type="danger" size="small">已拒绝</el-tag>
|
|
|
+ <el-tag v-else type="info" size="small">待审批</el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="item.approvalOpinion" class="approval-opinion mt-3">
|
|
|
+ <div class="opinion-label">
|
|
|
+ <el-icon class="mr-1"><ChatLineSquare /></el-icon>
|
|
|
+ <span>审批意见:</span>
|
|
|
+ </div>
|
|
|
+ <div class="opinion-content">{{ item.approvalOpinion }}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="item.approvalTime" class="approval-time mt-2">
|
|
|
+ <el-icon class="mr-1"><Clock /></el-icon>
|
|
|
+ <span>审批时间:{{ item.approvalTime }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="item.remark" class="approval-remark mt-2">
|
|
|
+ <el-icon class="mr-1"><InfoFilled /></el-icon>
|
|
|
+ <span>备注:{{ item.remark }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-timeline-item>
|
|
|
+ </el-timeline>
|
|
|
+
|
|
|
+ <!-- 审批统计 -->
|
|
|
+ <el-divider v-if="detailApprovalList.length > 0" content-position="left">审批统计</el-divider>
|
|
|
+ <el-row v-if="detailApprovalList.length > 0" :gutter="20">
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-statistic title="审批步骤" :value="detailApprovalList.length" suffix="步" />
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-statistic
|
|
|
+ title="已审批"
|
|
|
+ :value="detailApprovalList.filter((item) => item.approvalResult && item.approvalResult !== 'pending').length"
|
|
|
+ suffix="步"
|
|
|
+ />
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-statistic title="待审批" :value="detailApprovalList.filter((item) => item.approvalResult === 'pending').length" suffix="步" />
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="6">
|
|
|
+ <el-statistic
|
|
|
+ title="审批进度"
|
|
|
+ :value="
|
|
|
+ detailApprovalList.length > 0
|
|
|
+ ? Math.round(
|
|
|
+ (detailApprovalList.filter((item) => item.approvalResult && item.approvalResult !== 'pending').length /
|
|
|
+ detailApprovalList.length) *
|
|
|
+ 100
|
|
|
+ )
|
|
|
+ : 0
|
|
|
+ "
|
|
|
+ suffix="%"
|
|
|
+ />
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
+
|
|
|
+ <!-- 客户详情抽屉 -->
|
|
|
+ <CustomerDetail ref="customerDetailRef" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts" name="QuoteIndex">
|
|
|
-import { ref, reactive, onMounted, nextTick, getCurrentInstance, toRefs, type ComponentInternalInstance } from 'vue';
|
|
|
+import { ref, reactive, onMounted, nextTick, getCurrentInstance, toRefs, watch, type ComponentInternalInstance } from 'vue';
|
|
|
import { useRouter } from 'vue-router';
|
|
|
import { useClipboard } from '@vueuse/core';
|
|
|
import type { FormInstance } from 'element-plus';
|
|
|
@@ -416,6 +706,7 @@ import {
|
|
|
createNewVersion,
|
|
|
approveQuote,
|
|
|
listApproval,
|
|
|
+ queryVersions,
|
|
|
type QuoteQuery,
|
|
|
type QuoteVO,
|
|
|
type ApprovalForm,
|
|
|
@@ -424,7 +715,11 @@ import {
|
|
|
import { listUser } from '@/api/system/user';
|
|
|
import type { UserVO } from '@/api/system/user/types';
|
|
|
import { useUserStore } from '@/store/modules/user';
|
|
|
+import { parseTime } from '@/utils/ruoyi';
|
|
|
+import request from '@/utils/request';
|
|
|
import TinyMceEditor from '@/components/TinyMceEditor/index.vue';
|
|
|
+import ImagePreview from '@/components/ImagePreview/index.vue';
|
|
|
+import CustomerDetail from '@/views/crm/customer/detail.vue';
|
|
|
import logoImg from '@/assets/images/logo.gif';
|
|
|
import html2canvas from 'html2canvas';
|
|
|
import jsPDF from 'jspdf';
|
|
|
@@ -592,6 +887,17 @@ const shareFormRef = ref<FormInstance>();
|
|
|
const versionFormRef = ref<FormInstance>();
|
|
|
const approvalFormRef = ref<FormInstance>();
|
|
|
|
|
|
+// 详情抽屉
|
|
|
+const detailDrawerVisible = ref(false);
|
|
|
+const currentDetailId = ref<string | number>();
|
|
|
+const detailLoading = ref(false);
|
|
|
+const detailQuote = ref<QuoteVO>();
|
|
|
+const detailActiveTab = ref('basic');
|
|
|
+const detailAttachmentList = ref<any[]>([]);
|
|
|
+const detailApprovalList = ref<QuoteApprovalVO[]>([]);
|
|
|
+const detailVersions = ref<QuoteVO[]>([]);
|
|
|
+const customerDetailRef = ref();
|
|
|
+
|
|
|
/** 查询列表 */
|
|
|
const getList = async () => {
|
|
|
loading.value = true;
|
|
|
@@ -688,10 +994,9 @@ const handleUpdate = (row: QuoteVO) => {
|
|
|
|
|
|
/** 查看按钮操作 */
|
|
|
const handleView = (row: QuoteVO) => {
|
|
|
- router.push({
|
|
|
- path: '/crm/quote/detail',
|
|
|
- query: { id: row.id }
|
|
|
- });
|
|
|
+ // 打开详情抽屉
|
|
|
+ currentDetailId.value = row.id;
|
|
|
+ detailDrawerVisible.value = true;
|
|
|
};
|
|
|
|
|
|
/** 删除按钮操作 */
|
|
|
@@ -755,6 +1060,11 @@ const confirmSubmit = async () => {
|
|
|
proxy?.$modal.msgSuccess('提交成功');
|
|
|
submitDialog.visible = false;
|
|
|
await getList();
|
|
|
+
|
|
|
+ // 如果抽屉打开,刷新抽屉内容
|
|
|
+ if (detailDrawerVisible.value) {
|
|
|
+ await getDetailForDrawer();
|
|
|
+ }
|
|
|
} finally {
|
|
|
submitDialog.loading = false;
|
|
|
}
|
|
|
@@ -1264,6 +1574,249 @@ const togglePreviewMode = () => {
|
|
|
isPreviewMode.value = !isPreviewMode.value;
|
|
|
};
|
|
|
|
|
|
+/** 获取报价单详情(抽屉) */
|
|
|
+const getDetailForDrawer = async () => {
|
|
|
+ if (!currentDetailId.value) return;
|
|
|
+
|
|
|
+ detailLoading.value = true;
|
|
|
+ try {
|
|
|
+ const res = await getQuote(currentDetailId.value);
|
|
|
+ detailQuote.value = res.data;
|
|
|
+
|
|
|
+ // 加载附件信息
|
|
|
+ if (detailQuote.value?.attachments) {
|
|
|
+ const ossIds = detailQuote.value.attachments.split(',').filter(Boolean);
|
|
|
+ if (ossIds.length > 0) {
|
|
|
+ detailAttachmentList.value = await loadDetailAttachments(ossIds);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取版本历史
|
|
|
+ if (detailQuote.value?.quoteNo) {
|
|
|
+ const versionsRes = await queryVersions(detailQuote.value.quoteNo);
|
|
|
+ detailVersions.value = versionsRes.data || [];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 加载审批记录
|
|
|
+ await loadDetailApprovalRecords(detailQuote.value.id);
|
|
|
+ } finally {
|
|
|
+ detailLoading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 加载审批记录(抽屉) */
|
|
|
+const loadDetailApprovalRecords = async (quoteId: string | number) => {
|
|
|
+ try {
|
|
|
+ const res = await listApproval(quoteId);
|
|
|
+ detailApprovalList.value = res.data || [];
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载审批记录失败:', error);
|
|
|
+ detailApprovalList.value = [];
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 加载附件信息(抽屉) */
|
|
|
+const loadDetailAttachments = async (ossIds: string[]) => {
|
|
|
+ if (ossIds.length === 0) return [];
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await request({
|
|
|
+ url: `/resource/oss/listByIds/${ossIds.join(',')}`,
|
|
|
+ method: 'get'
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response?.code === 200 && response.data) {
|
|
|
+ return response.data.map((item: any) => ({
|
|
|
+ ossId: item.ossId,
|
|
|
+ name: item.originalName || '附件',
|
|
|
+ url: item.url || item.ossId
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载附件详细信息失败:', error);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果加载失败,返回默认信息
|
|
|
+ return ossIds.map((ossId, index) => ({
|
|
|
+ ossId: ossId,
|
|
|
+ name: `附件${index + 1}`,
|
|
|
+ url: ossId
|
|
|
+ }));
|
|
|
+};
|
|
|
+
|
|
|
+/** 判断是否为图片文件(抽屉) */
|
|
|
+const isDetailImageFile = (file: any) => {
|
|
|
+ if (!file.name) return false;
|
|
|
+ return /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name);
|
|
|
+};
|
|
|
+
|
|
|
+/** 获取文件扩展名(抽屉) */
|
|
|
+const getDetailFileExt = (fileName: string) => {
|
|
|
+ if (!fileName) return 'FILE';
|
|
|
+ const ext = fileName.split('.').pop()?.toUpperCase();
|
|
|
+ return ext || 'FILE';
|
|
|
+};
|
|
|
+
|
|
|
+/** 获取缩写的文件名(抽屉) */
|
|
|
+const getDetailShortFileName = (fileName: string) => {
|
|
|
+ if (!fileName) return '';
|
|
|
+
|
|
|
+ const lastDotIndex = fileName.lastIndexOf('.');
|
|
|
+ if (lastDotIndex === -1) {
|
|
|
+ return fileName.length > 15 ? fileName.substring(0, 15) + '...' : fileName;
|
|
|
+ }
|
|
|
+
|
|
|
+ const name = fileName.substring(0, lastDotIndex);
|
|
|
+ const ext = fileName.substring(lastDotIndex);
|
|
|
+
|
|
|
+ if (name.length <= 12) {
|
|
|
+ return fileName;
|
|
|
+ }
|
|
|
+
|
|
|
+ return name.substring(0, 12) + '...' + ext;
|
|
|
+};
|
|
|
+
|
|
|
+/** 下载附件(抽屉) */
|
|
|
+const handleDetailDownloadFile = async (file: any) => {
|
|
|
+ if (!file.ossId) {
|
|
|
+ proxy?.$modal.msgError('无法获取文件下载地址');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await request({
|
|
|
+ url: '/resource/oss/download/' + file.ossId,
|
|
|
+ method: 'get',
|
|
|
+ responseType: 'blob',
|
|
|
+ timeout: 300000
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response) {
|
|
|
+ throw new Error('响应数据为空');
|
|
|
+ }
|
|
|
+
|
|
|
+ let blobData: Blob;
|
|
|
+ if (response instanceof Blob) {
|
|
|
+ blobData = response;
|
|
|
+ } else if (response.data instanceof Blob) {
|
|
|
+ blobData = response.data;
|
|
|
+ } else {
|
|
|
+ throw new Error('无效的响应数据格式');
|
|
|
+ }
|
|
|
+
|
|
|
+ const url = window.URL.createObjectURL(blobData);
|
|
|
+ const link = document.createElement('a');
|
|
|
+ link.href = url;
|
|
|
+ link.download = file.name || '附件';
|
|
|
+ link.style.display = 'none';
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+ document.body.removeChild(link);
|
|
|
+ window.URL.revokeObjectURL(url);
|
|
|
+
|
|
|
+ proxy?.$modal.msgSuccess('下载成功');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('下载文件失败:', error);
|
|
|
+ proxy?.$modal.msgError('下载文件失败,请稍后重试');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 格式化金额(抽屉) */
|
|
|
+const formatDetailAmount = (amount?: number) => {
|
|
|
+ if (!amount) return '0.00';
|
|
|
+ return parseFloat(amount.toString()).toLocaleString('zh-CN', {
|
|
|
+ minimumFractionDigits: 2,
|
|
|
+ maximumFractionDigits: 2
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+/** 获取到期日期颜色(抽屉) */
|
|
|
+const getDetailExpireDateColor = (expireDate?: string) => {
|
|
|
+ if (!expireDate) return '';
|
|
|
+ const now = new Date();
|
|
|
+ const expire = new Date(expireDate);
|
|
|
+ const diff = Math.ceil((expire.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
+ if (diff < 0) return '#909399';
|
|
|
+ if (diff <= 3) return '#F56C6C';
|
|
|
+ if (diff <= 7) return '#E6A23C';
|
|
|
+ return '#67C23A';
|
|
|
+};
|
|
|
+
|
|
|
+/** 获取剩余天数(抽屉) */
|
|
|
+const getDetailDaysRemaining = (expireDate?: string) => {
|
|
|
+ if (!expireDate) return '';
|
|
|
+ const now = new Date();
|
|
|
+ const expire = new Date(expireDate);
|
|
|
+ const diff = Math.ceil((expire.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
+ if (diff < 0) return '已过期';
|
|
|
+ if (diff === 0) return '今天到期';
|
|
|
+ return `剩余${diff}天`;
|
|
|
+};
|
|
|
+
|
|
|
+/** 获取审批状态颜色(抽屉) */
|
|
|
+const getDetailApprovalColor = (approvalResult?: string) => {
|
|
|
+ if (!approvalResult) return '#909399';
|
|
|
+ switch (approvalResult) {
|
|
|
+ case 'approved':
|
|
|
+ return '#67C23A';
|
|
|
+ case 'rejected':
|
|
|
+ return '#F56C6C';
|
|
|
+ case 'pending':
|
|
|
+ return '#909399';
|
|
|
+ default:
|
|
|
+ return '#909399';
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 查看客户(抽屉) */
|
|
|
+const handleDetailViewCustomer = () => {
|
|
|
+ if (detailQuote.value?.customerId) {
|
|
|
+ customerDetailRef.value?.open(detailQuote.value.customerId as number);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 编辑(抽屉) */
|
|
|
+const handleDetailEdit = () => {
|
|
|
+ router.push({
|
|
|
+ path: '/crm/quote/form',
|
|
|
+ query: { id: detailQuote.value?.id }
|
|
|
+ });
|
|
|
+ detailDrawerVisible.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+/** 提交审批(抽屉) */
|
|
|
+const handleDetailSubmit = async () => {
|
|
|
+ if (!detailQuote.value) return;
|
|
|
+
|
|
|
+ submitForm.value = {
|
|
|
+ id: detailQuote.value.id,
|
|
|
+ approverIds: [],
|
|
|
+ remark: ''
|
|
|
+ };
|
|
|
+ // 加载用户列表
|
|
|
+ await loadUserList();
|
|
|
+ submitDialog.visible = true;
|
|
|
+};
|
|
|
+
|
|
|
+/** 关闭抽屉 */
|
|
|
+const handleDetailDrawerClose = () => {
|
|
|
+ detailDrawerVisible.value = false;
|
|
|
+ detailQuote.value = undefined;
|
|
|
+ detailAttachmentList.value = [];
|
|
|
+ detailApprovalList.value = [];
|
|
|
+ detailVersions.value = [];
|
|
|
+ detailActiveTab.value = 'basic';
|
|
|
+ // 刷新列表
|
|
|
+ getList();
|
|
|
+};
|
|
|
+
|
|
|
+/** 监听抽屉打开事件 */
|
|
|
+watch(detailDrawerVisible, (val) => {
|
|
|
+ if (val) {
|
|
|
+ getDetailForDrawer();
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
getList();
|
|
|
});
|
|
|
@@ -1351,4 +1904,228 @@ onMounted(() => {
|
|
|
.el-radio-group {
|
|
|
margin-bottom: 16px;
|
|
|
}
|
|
|
+
|
|
|
+/* 抽屉样式 */
|
|
|
+.drawer-content {
|
|
|
+ padding: 0 20px 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 16px 0;
|
|
|
+ border-bottom: 1px solid #e4e7ed;
|
|
|
+ margin-bottom: 16px;
|
|
|
+
|
|
|
+ .detail-title {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+
|
|
|
+ .detail-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 附件展示样式 */
|
|
|
+.attachment-section {
|
|
|
+ margin-top: 16px;
|
|
|
+ padding: 16px;
|
|
|
+ background: #fafafa;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.attachment-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ padding-bottom: 8px;
|
|
|
+ border-bottom: 1px solid #e4e7ed;
|
|
|
+}
|
|
|
+
|
|
|
+.attachment-title {
|
|
|
+ margin-left: 6px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.attachment-list {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 文件卡片样式 */
|
|
|
+.upload-file-card {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ border-radius: 6px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-file-card:hover {
|
|
|
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
|
+ transform: translateY(-2px);
|
|
|
+}
|
|
|
+
|
|
|
+.upload-file-thumb {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 110px;
|
|
|
+ background: #f5f5f5;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-thumb-image {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-file-placeholder {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-file-placeholder .el-icon {
|
|
|
+ font-size: 28px;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-file-placeholder .file-ext {
|
|
|
+ font-size: 12px;
|
|
|
+ text-transform: uppercase;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-file-name {
|
|
|
+ padding: 6px 8px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: #303133;
|
|
|
+ text-align: center;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ background: #fafafa;
|
|
|
+ border-top: 1px solid #f0f0f0;
|
|
|
+ line-height: 1.2;
|
|
|
+ height: 32px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ word-break: break-all;
|
|
|
+ max-width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-file-card-actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ padding: 6px 8px;
|
|
|
+ background: #fafafa;
|
|
|
+ border-top: 1px solid #f0f0f0;
|
|
|
+ transition: all 0.3s;
|
|
|
+ height: 38px;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-file-card-actions .el-button {
|
|
|
+ padding: 4px 12px;
|
|
|
+ border: 1px solid #e0e0e0;
|
|
|
+ background: #fff;
|
|
|
+ font-size: 12px;
|
|
|
+ border-radius: 3px;
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-file-card-actions .el-button:hover {
|
|
|
+ background: #67c23a;
|
|
|
+ border-color: #67c23a;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 审批流程样式 */
|
|
|
+.approval-item {
|
|
|
+ .approval-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ > div {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .approver-name {
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 15px;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .approval-opinion {
|
|
|
+ .opinion-label {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ color: #606266;
|
|
|
+ font-weight: 500;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .opinion-content {
|
|
|
+ padding: 12px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 4px;
|
|
|
+ color: #303133;
|
|
|
+ line-height: 1.6;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ word-break: break-word;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .approval-time,
|
|
|
+ .approval-remark {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.module-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.module-title-bar {
|
|
|
+ width: 4px;
|
|
|
+ height: 18px;
|
|
|
+ background-color: #67c23a;
|
|
|
+ border-radius: 2px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.module-title-text {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
</style>
|