zlwu 1 год назад
Родитель
Сommit
d8e2f90136

+ 1 - 1
web/src/views/crm/clue/detail.vue

@@ -7,7 +7,7 @@
           <template #header>
             <div class="flex justify-between items-center">
               <span class="font-bold">基本信息</span>
-              <el-button link type="primary" icon="Edit" @click="handleEdit" :disabled="clueInfo.isConverted === 1"> 修改 </el-button>
+              <el-button type="primary" icon="Edit" @click="handleEdit" :disabled="clueInfo.isConverted === 1"> 修改 </el-button>
             </div>
           </template>
 

+ 1 - 1
web/src/views/crm/clue/detailDrawer.vue

@@ -8,7 +8,7 @@
             <template #header>
               <div class="flex justify-between items-center">
                 <span class="font-bold">基本信息</span>
-                <el-button link type="primary" icon="Edit" @click="handleEdit" :disabled="clueInfo.isConverted === 1"> 修改 </el-button>
+                <el-button type="primary" icon="Edit" @click="handleEdit" :disabled="clueInfo.isConverted === 1"> 修改 </el-button>
               </div>
             </template>
 

+ 28 - 1
web/src/views/crm/clue/index.vue

@@ -84,6 +84,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="mine">我的线索</el-radio-button>
@@ -509,4 +515,25 @@ onMounted(() => {
 });
 </script>
 
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+.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>

Разница между файлами не показана из-за своего большого размера
+ 1170 - 7
web/src/views/crm/contract/index.vue


+ 6 - 8
web/src/views/crm/customer/detail.vue

@@ -7,18 +7,16 @@
           <el-card shadow="hover" class="mb-4">
             <template #header>
               <div class="flex justify-between items-center">
-                <span class="font-bold">基本信息</span>
-                <el-button link type="primary" icon="Edit" @click="handleEdit"> 修改 </el-button>
+                <div class="flex items-center gap-2">
+                  <span class="font-bold">基本信息</span>
+                  <span v-if="customerInfo.customerName" class="font-semibold text-lg ml-2">{{ customerInfo.customerName }}</span>
+                  <dict-tag v-if="customerInfo.customerType" :options="crm_customer_type" :value="customerInfo.customerType" />
+                </div>
+                <el-button type="primary" icon="Edit" @click="handleEdit"> 修改 </el-button>
               </div>
             </template>
 
             <el-descriptions :column="1" border v-if="customerInfo.id">
-              <el-descriptions-item label="客户名称">
-                <span class="font-semibold text-lg">{{ customerInfo.customerName }}</span>
-                <el-tag v-if="customerInfo.customerType" size="small" class="ml-2">
-                  <dict-tag :options="crm_customer_type" :value="customerInfo.customerType" />
-                </el-tag>
-              </el-descriptions-item>
               <el-descriptions-item label="客户编号">
                 {{ customerInfo.customerNo }}
               </el-descriptions-item>

+ 27 - 0
web/src/views/crm/customer/index.vue

@@ -85,6 +85,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="mine">我的客户</el-radio-button>
@@ -457,4 +463,25 @@ onMounted(() => {
 .el-radio-group {
   margin-bottom: 16px;
 }
+
+.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>

+ 0 - 1
web/src/views/crm/opportunity/detail.vue

@@ -14,7 +14,6 @@
             </el-tag>
             <el-button
               type="primary"
-              size="small"
               icon="Edit"
               @click="handleEdit"
               :disabled="opportunityInfo?.status === 'won' || opportunityInfo?.status === 'lost'"

+ 27 - 0
web/src/views/crm/opportunity/index.vue

@@ -64,6 +64,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="mine">我的商机</el-radio-button>
@@ -599,4 +605,25 @@ getList();
 .el-radio-group {
   margin-bottom: 16px;
 }
+
+.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>

+ 28 - 1
web/src/views/crm/pool/index.vue

@@ -37,6 +37,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-row :gutter="20" class="mb-4">
           <el-col :span="6">
@@ -292,4 +298,25 @@ onMounted(() => {
 });
 </script>
 
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+.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>

+ 29 - 0
web/src/views/crm/product/category/index.vue

@@ -32,6 +32,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-row :gutter="10" class="mb8">
           <!-- 快速检索框 -->
@@ -317,3 +323,26 @@ onMounted(() => {
   getList();
 });
 </script>
+
+<style scoped lang="scss">
+.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>

+ 29 - 0
web/src/views/crm/product/index.vue

@@ -45,6 +45,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-row :gutter="10" class="mb8">
           <!-- 快速检索框 -->
@@ -803,3 +809,26 @@ onMounted(() => {
   getCategoryTree();
 });
 </script>
+
+<style scoped lang="scss">
+.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>

+ 0 - 20
web/src/views/crm/quote/detail.vue

@@ -12,7 +12,6 @@
             <el-button type="warning" icon="Check" @click="handleSubmit" v-if="quote?.status === 'draft'">提交审批</el-button>
             <!-- <el-button type="success" icon="Document" @click="handleGeneratePdf">生成PDF</el-button> -->
             <!-- <el-button type="info" icon="Share" @click="handleGenerateShare">分享</el-button> -->
-            <el-button type="primary" icon="DocumentCopy" @click="handleToContract" v-if="quote?.status === 'accepted'">转合同</el-button>
             <!-- <el-button icon="Plus" @click="handleNewVersion">新版本</el-button> -->
           </div>
         </div>
@@ -326,9 +325,6 @@
     <!-- 分享对话框 -->
     <!-- <ShareDialog ref="shareDialogRef" :quoteId="quote?.id" @success="handleShareSuccess" /> -->
 
-    <!-- 转合同对话框 -->
-    <ToContractDialog ref="toContractDialogRef" :quoteInfo="quote" @success="handleToContractSuccess" />
-
     <!-- 客户详情抽屉 -->
     <CustomerDetail ref="customerDetailRef" />
   </div>
@@ -343,7 +339,6 @@ import { ElMessage } from 'element-plus';
 import { useDict } from '@/utils/dict';
 import { parseTime } from '@/utils/ruoyi';
 import ShareDialog from './components/ShareDialog.vue';
-import ToContractDialog from './components/ToContractDialog.vue';
 import ShareStatsTab from './components/ShareStatsTab.vue';
 import ImagePreview from '@/components/ImagePreview/index.vue';
 import CustomerDetail from '@/views/crm/customer/detail.vue';
@@ -355,7 +350,6 @@ const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 // Refs
 const shareDialogRef = ref();
-const toContractDialogRef = ref();
 const customerDetailRef = ref<InstanceType<typeof CustomerDetail>>();
 
 // 数据
@@ -473,20 +467,6 @@ const handleShareSuccess = (shareLink: string) => {
   getDetail();
 };
 
-/** 转合同 */
-const handleToContract = () => {
-  toContractDialogRef.value?.show();
-};
-
-/** 转合同成功回调 */
-const handleToContractSuccess = (contractId: number) => {
-  ElMessage.success('合同创建成功');
-  // 刷新详情
-  getDetail();
-  // 可选: 跳转到合同详情页
-  // router.push(`/crm/contract/detail/${contractId}`)
-};
-
 /** 新版本 */
 const handleNewVersion = () => {
   ElMessage.info('新版本功能待实现');

+ 784 - 7
web/src/views/crm/quote/index.vue

@@ -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>

Некоторые файлы не были показаны из-за большого количества измененных файлов