Browse Source

工单管理,上线

aiwenzhu 2 weeks ago
parent
commit
893e678102

+ 2 - 2
.env.production

@@ -4,14 +4,14 @@ ENV = 'production'
 # base api
 
 
-# VUE_APP_BASE_API = 'https://kptyun.cn/'
+VUE_APP_BASE_API = 'https://kptyun.cn/'
 
 # VUE_APP_BASE_API = 'http://localhost:8084/'
 
 
 
 
- VUE_APP_BASE_API = 'http://210.16.189.72:8099/'
+ # VUE_APP_BASE_API = 'http://210.16.189.72:8099/'
   
 
 # VUE_APP_BASE_API = 'http://localhost:8082/'

+ 58 - 69
src/views/productManagement/installationOrder/components/AddDialog.vue

@@ -3,7 +3,7 @@
   <el-dialog
     title="新增服务计划"
     :visible.sync="visible"
-    width="1000px"
+    width="90%"
     :close-on-click-modal="false"
     :close-on-press-escape="false"
     @close="handleDialogClose"
@@ -205,21 +205,44 @@
             width="50"
             align="center"
           />
-          <el-table-column label="服务项目" align="center" width="150">
+          <el-table-column label="服务项目" align="center" width="200">
             <template slot-scope="scope">
-              <el-select
-                v-model="scope.row.serviceProject"
-                filterable
-                placeholder="请选择服务项目"
-                style="width: 100%"
+              <el-tooltip
+                :content="
+                  projectOptions.find((p) => p.id === scope.row.serviceProject)
+                    ?.name || ''
+                "
+                placement="top"
+                :disabled="
+                  !scope.row.serviceProject ||
+                  projectOptions.find((p) => p.id === scope.row.serviceProject)
+                    ?.name?.length <= 8
+                "
+                effect="light"
               >
-                <el-option
-                  v-for="item in projectOptions"
-                  :key="item.id"
-                  :label="item.name"
-                  :value="item.id"
-                />
-              </el-select>
+                <el-select
+                  v-model="scope.row.serviceProject"
+                  filterable
+                  placeholder="请选择服务项目"
+                  style="width: 100%"
+                >
+                  <el-option
+                    v-for="item in projectOptions"
+                    :key="item.id"
+                    :label="item.name"
+                    :value="item.id"
+                  >
+                    <el-tooltip
+                      :content="item.name"
+                      placement="right"
+                      :disabled="item.name.length <= 8"
+                      effect="light"
+                    >
+                      <span class="ellipsis-text">{{ item.name }}</span>
+                    </el-tooltip>
+                  </el-option>
+                </el-select>
+              </el-tooltip>
             </template>
           </el-table-column>
           <el-table-column
@@ -914,67 +937,33 @@ export default {
 }
 
 :deep(.el-select-dropdown__item) {
-  padding: 0 !important;
-  height: auto !important;
-  margin: 1px;
-  line-height: normal;
-
-  &.hover,
-  &:hover {
-    background-color: transparent;
-
-    .product-option {
-      background-color: #f9fafc;
-      border-color: #e4e7ed;
-      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
-    }
-  }
-
-  &.selected {
-    .product-option {
-      background-color: #f0f7ff;
-      border-color: rgba(64, 158, 255, 0.2);
-
-      .label {
-        background: rgba(64, 158, 255, 0.12);
-        border-color: rgba(64, 158, 255, 0.3);
-      }
-
-      .value {
-        color: #409eff;
-      }
+  padding: 0 8px !important;
+  height: 34px !important;
+  line-height: 34px !important;
+  font-size: 12px; // 设置下拉选项字体大小
+}
 
-      &::after {
-        content: "";
-        position: absolute;
-        right: 8px;
-        top: 50%;
-        transform: translateY(-50%);
-        width: 4px;
-        height: 4px;
-        border-radius: 50%;
-        background-color: #409eff;
-      }
-    }
+:deep(.el-select-dropdown) {
+  .el-select-dropdown__item {
+    font-size: 12px; // 确保下拉选项字体大小一致
   }
 }
 
-:deep(.el-select-dropdown__wrap) {
-  padding: 1px;
-  max-height: 280px;
+.ellipsis-text {
+  display: inline-block;
+  width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-size: 12px; // 设置省略文本字体大小
 }
 
-:deep(.el-tooltip__popper) {
-  max-width: 300px;
-  line-height: 1.4;
-  padding: 8px 12px;
-  font-size: 12px;
-  word-break: break-all;
-  white-space: pre-wrap;
-
-  &.is-light {
-    border: 1px solid #e4e7ed;
-    color: #606266;
+:deep(.el-select) {
+  .el-input__inner {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    font-size: 12px; // 设置输入框字体大小
   }
 }
 </style>

+ 76 - 13
src/views/productManagement/installationOrder/components/EditDialog.vue

@@ -3,7 +3,7 @@
   <el-dialog
     title="编辑服务计划"
     :visible.sync="visible"
-    width="1000px"
+    width="90%"
     :close-on-click-modal="false"
     :close-on-press-escape="false"
     @close="handleDialogClose"
@@ -213,20 +213,43 @@
             width="50"
             align="center"
           />
-          <el-table-column label="服务项目" align="center" width="150">
+          <el-table-column label="服务项目" align="center" width="200">
             <template slot-scope="scope">
-              <el-select
-                v-model="scope.row.projectId"
-                placeholder="请选择服务项目"
-                style="width: 100%"
+              <el-tooltip
+                :content="
+                  projectOptions.find((p) => p.id === scope.row.projectId)
+                    ?.name || ''
+                "
+                placement="top"
+                :disabled="
+                  !scope.row.projectId ||
+                  projectOptions.find((p) => p.id === scope.row.projectId)?.name
+                    ?.length <= 8
+                "
+                effect="light"
               >
-                <el-option
-                  v-for="item in projectOptions"
-                  :key="item.id"
-                  :label="item.name"
-                  :value="item.id"
-                />
-              </el-select>
+                <el-select
+                  v-model="scope.row.projectId"
+                  placeholder="请选择服务项目"
+                  style="width: 100%"
+                >
+                  <el-option
+                    v-for="item in projectOptions"
+                    :key="item.id"
+                    :label="item.name"
+                    :value="item.id"
+                  >
+                    <el-tooltip
+                      :content="item.name"
+                      placement="right"
+                      :disabled="item.name.length <= 8"
+                      effect="light"
+                    >
+                      <span class="ellipsis-text">{{ item.name }}</span>
+                    </el-tooltip>
+                  </el-option>
+                </el-select>
+              </el-tooltip>
             </template>
           </el-table-column>
           <el-table-column
@@ -589,6 +612,7 @@ export default {
 
     // 移除货品
     handleRemoveProduct(index) {
+      // TO DO 验证该货品是否开始服务,如果有服务则不能删除
       this.form.products.splice(index, 1);
     },
 
@@ -794,6 +818,20 @@ export default {
                     },
                   ],
                 },
+                {
+                  name: "refreshInstallationOrderDetailQuantity",
+                  type: "e",
+                  parammaps: {
+                    orderId: this.rowData.id,
+                  },
+                },
+                {
+                  name: "refreshInstallationOrderProcessByOrderId",
+                  type: "e",
+                  parammaps: {
+                    orderId: this.rowData.id,
+                  },
+                },
                 {
                   name: "insertInstallationOrderProcessLog",
                   type: "e",
@@ -1015,4 +1053,29 @@ export default {
     color: #606266;
   }
 }
+
+.ellipsis-text {
+  display: inline-block;
+  width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-size: 12px;
+}
+
+:deep(.el-select-dropdown__item) {
+  padding: 0 8px !important;
+  height: 34px !important;
+  line-height: 34px !important;
+  font-size: 12px;
+}
+
+:deep(.el-select) {
+  .el-input__inner {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    font-size: 12px;
+  }
+}
 </style>

+ 654 - 0
src/views/productManagement/installationSummary/components/CustomerInstallationTable.vue

@@ -0,0 +1,654 @@
+<template>
+  <el-card class="box-card">
+    <div class="header-container">
+      <div class="left">
+        <span class="title">服务概况</span>
+        <span class="update-time" v-if="updateTime">{{ this.updateTime }}</span>
+      </div>
+      <div class="right">
+        <div class="filter-group">
+          <slot name="query-buttons"></slot>
+
+          <el-select
+            v-model="selectedCustomer"
+            filterable
+            remote
+            clearable
+            size="small"
+            reserve-keyword
+            placeholder="请输入客户名称搜索"
+            :loading="customerLoading"
+            style="width: 200px; margin-right: 15px"
+            :remote-method="handleCustomerInput"
+          >
+            <el-option
+              v-for="item in customerOptions"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"
+            />
+          </el-select>
+          <el-button
+            type="primary"
+            icon="el-icon-search"
+            size="small"
+            @click="getTableData"
+            >查询</el-button
+          >
+          <el-button
+            type="primary"
+            icon="el-icon-download"
+            size="small"
+            @click="handleExport"
+            :loading="exportLoading"
+            >导出</el-button
+          >
+        </div>
+      </div>
+    </div>
+    <div class="table-container">
+      <el-table
+        v-loading="loading"
+        :data="tableData"
+        style="width: 100%"
+        border
+        size="small"
+        :height="tableData.length > 10 ? 450 : null"
+        :header-cell-style="{ background: '#f5f7fa' }"
+        @sort-change="handleSortChange"
+      >
+        <el-table-column type="index" label="序号" width="50" align="center" />
+        <el-table-column
+          prop="customerName"
+          label="客户名称"
+          min-width="150"
+          align="center"
+          sortable
+        />
+        <el-table-column
+          prop="orderNo"
+          label="订单编号"
+          min-width="120"
+          align="center"
+          sortable
+        >
+          <template slot-scope="scope">
+            <span class="order-no-text" @click="handleOrderClick(scope.row)">{{
+              scope.row.orderNo
+            }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="goodsName"
+          label="货品名称"
+          min-width="110"
+          align="center"
+          sortable
+        />
+        <el-table-column
+          prop="statusName"
+          label="单据状态"
+          width="110"
+          sortable
+          align="center"
+        >
+          <template slot-scope="scope">
+            <el-tag
+              :type="getStatusType(scope.row.statusName)"
+              size="mini"
+              effect="plain"
+            >
+              {{ scope.row.statusName }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="estimatedCompleteTime"
+          label="预计完成时间"
+          width="130"
+          align="center"
+        />
+        <el-table-column
+          prop="remainingDays"
+          label="距离完成时间还剩"
+          width="130"
+          align="center"
+        >
+          <template slot-scope="scope">
+            <template
+              v-if="
+                scope.row.remainingTime !== null &&
+                scope.row.remainingTime !== undefined
+              "
+            >
+              <span
+                :class="{
+                  'normal-time':
+                    scope.row.remainingTime > 3 && !scope.row.earlyDays,
+                  'warning-time':
+                    scope.row.remainingTime >= 0 &&
+                    scope.row.remainingTime <= 3 &&
+                    !scope.row.earlyDays,
+                  'overdue-time':
+                    (scope.row.remainingTime < 0 && !scope.row.earlyDays) ||
+                    (scope.row.earlyDays && scope.row.earlyDays < 0),
+                  'early-time': scope.row.earlyDays && scope.row.earlyDays > 0,
+                }"
+              >
+                <template
+                  v-if="
+                    scope.row.earlyDays !== null &&
+                    scope.row.earlyDays !== undefined
+                  "
+                >
+                  <template v-if="scope.row.earlyDays > 0">
+                    提前 {{ scope.row.earlyDays }} 天完成
+                  </template>
+                  <template v-else-if="scope.row.earlyDays < 0">
+                    延期 {{ Math.abs(scope.row.earlyDays) }} 天完成
+                  </template>
+                  <template v-else> 按时完成 </template>
+                </template>
+                <template v-else>
+                  <template v-if="scope.row.remainingTime < 0">
+                    已逾期 {{ Math.abs(scope.row.remainingTime) }} 天
+                  </template>
+                  <template v-else> {{ scope.row.remainingTime }} 天 </template>
+                </template>
+              </span>
+            </template>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="progress"
+          label="进度"
+          width="250"
+          align="center"
+        >
+          <template slot-scope="scope">
+            <div class="progress-wrapper">
+              <div class="progress-container">
+                <div class="progress-bar">
+                  <div class="custom-progress">
+                    <div
+                      class="progress-inner"
+                      :style="{
+                        width: Math.min(Math.round(scope.row.rate), 100) + '%',
+                        backgroundColor: getProgressColor(scope.row.statusName),
+                      }"
+                    >
+                      <span
+                        class="progress-text"
+                        v-if="scope.row.installedQuantity > 0"
+                      >
+                        {{ scope.row.installedQuantity }}
+                      </span>
+                    </div>
+                    <span class="total-text">
+                      {{
+                        scope.row.rate === 0
+                          ? scope.row.totalQuantity
+                          : scope.row.totalQuantity
+                      }}
+                    </span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="totalQuantity" label="计划量" align="center" />
+        <el-table-column
+          prop="installedQuantity"
+          label="已完成量"
+          align="center"
+        />
+        <el-table-column
+          prop="uninstalledQuantity"
+          label="未完成量"
+          align="center"
+        />
+        <el-table-column
+          prop="serviceStaffNames"
+          label="服务人员"
+          width="150"
+          align="center"
+        >
+          <template slot-scope="scope">
+            <div class="product-tags">
+              <template v-if="scope.row.serviceStaffNames">
+                <el-tag
+                  v-for="(item, index) in scope.row.serviceStaffNames.split(
+                    ','
+                  )"
+                  :key="index"
+                  size="mini"
+                  :type="['success', 'warning', 'danger', 'primary'][index % 4]"
+                  effect="light"
+                  :class="{ 'long-text': item.length > 10 }"
+                >
+                  {{ item }}
+                </el-tag>
+              </template>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="installedQuantity"
+          label="服务数量"
+          align="center"
+        />
+        <el-table-column prop="days" label="服务天时" align="center" />
+        <el-table-column prop="avgQuantity" label="平均完成量" align="center" />
+      </el-table>
+    </div>
+    <view-dialog
+      :visible.sync="dialogVisible"
+      :row-data="currentRow"
+      :installer-options="personnelOptions"
+      :project-options="projectOptions"
+      :check-button-permission="() => false"
+      :is-current-user-in-service-staff="() => false"
+    />
+  </el-card>
+</template>
+
+<script>
+import ViewDialog from "@/views/productManagement/installationOrder/components/ViewDialog.vue";
+import { GetDataByName } from "@/api/common";
+import * as XLSX from "xlsx";
+
+export default {
+  name: "CustomerInstallationTable",
+  components: {
+    ViewDialog,
+  },
+  // props: {
+  //   updateTime: {
+  //     type: String,
+  //     default: "",
+  //   },
+  // },
+  data() {
+    return {
+      loading: false,
+      customerLoading: false,
+      exportLoading: false,
+      tableData: [],
+      dialogVisible: false,
+      currentRow: null,
+      personnelOptions: [],
+      projectOptions: [],
+      selectedCustomer: null,
+      customerOptions: [],
+      currentTime: "",
+    };
+  },
+  watch: {
+    // selectedCustomer(val) {
+    //   this.getTableData();
+    // },
+  },
+  created() {
+    this.selectedCustomer = null;
+    this.getTableData();
+  },
+  methods: {
+    updateCurrentTime() {
+      const now = new Date();
+      const year = now.getFullYear();
+      const month = String(now.getMonth() + 1).padStart(2, "0");
+      const day = String(now.getDate()).padStart(2, "0");
+      const hours = String(now.getHours()).padStart(2, "0");
+      const minutes = String(now.getMinutes()).padStart(2, "0");
+      const seconds = String(now.getSeconds()).padStart(2, "0");
+      const formattedTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+      this.updateTime = formattedTime;
+    },
+    async handleCustomerInput(query) {
+      if (query !== "") {
+        this.customerLoading = true;
+        try {
+          const response = await GetDataByName({
+            name: "getCustomerNameFuzzy",
+            offset: 0,
+            pagecount: 100,
+            parammaps: {
+              inputvalue: query,
+            },
+          });
+          this.customerOptions = response.data.list || [];
+        } catch (error) {
+          console.error("获取客户列表失败:", error);
+          this.$message.error("获取客户列表失败");
+        } finally {
+          this.customerLoading = false;
+        }
+      } else {
+        this.customerOptions = [];
+      }
+    },
+    async getTableData() {
+      const send_select_list = {
+        name: "getInstallCustomerOverview",
+        offset: 0,
+        pagecount: 0,
+        parammaps: {
+          customerId: this.selectedCustomer || "",
+        },
+      };
+
+      try {
+        this.loading = true;
+        const response = await GetDataByName(send_select_list);
+        this.tableData = response.data.list || [];
+        this.updateCurrentTime();
+        this.$emit("data-loaded");
+      } catch (error) {
+        console.error("获取表格数据失败:", error);
+        this.$message.error("获取表格数据失败");
+      } finally {
+        this.loading = false;
+      }
+    },
+    async handleExport() {
+      try {
+        this.exportLoading = true;
+        const response = await GetDataByName({
+          name: "exportInstallCustomerOverview",
+          offset: 0,
+          pagecount: 0,
+          parammaps: {},
+        });
+
+        if (response.data && response.data.list) {
+          const data = response.data.list;
+          const header = {
+            customerName: "客户名称",
+            orderNo: "订单编号",
+            goodsName: "货品名称",
+            statusName: "单据状态",
+            estimatedCompleteTime: "预计完成时间",
+            remainingTime: "距离完成时间还剩",
+            totalQuantity: "计划量",
+            installedQuantity: "已完成量",
+            uninstalledQuantity: "未完成量",
+            serviceStaffNames: "服务人员",
+            days: "服务天时",
+            avgQuantity: "平均完成量",
+          };
+
+          // 创建工作表
+          const ws = XLSX.utils.json_to_sheet([]);
+          // 添加表头
+          XLSX.utils.sheet_add_json(ws, [header], {
+            header: Object.keys(header),
+            skipHeader: true,
+          });
+          // 添加数据
+          XLSX.utils.sheet_add_json(ws, data, {
+            origin: "A2",
+            skipHeader: true,
+          });
+
+          // 设置列宽
+          const colWidths = Object.keys(header).map(() => ({ wch: 15 }));
+          ws["!cols"] = colWidths;
+
+          // 创建工作簿
+          const wb = XLSX.utils.book_new();
+          XLSX.utils.book_append_sheet(wb, ws, "客户安装概况");
+
+          // 导出文件
+          const now = new Date();
+          const year = now.getFullYear();
+          const month = String(now.getMonth() + 1).padStart(2, "0");
+          const day = String(now.getDate()).padStart(2, "0");
+          const hours = String(now.getHours()).padStart(2, "0");
+          const minutes = String(now.getMinutes()).padStart(2, "0");
+          const seconds = String(now.getSeconds()).padStart(2, "0");
+          const timestamp = `${year}${month}${day}${hours}${minutes}${seconds}`;
+          XLSX.writeFile(wb, `客户安装概况_${timestamp}.xlsx`);
+        }
+      } catch (error) {
+        console.error("导出失败:", error);
+        this.$message.error("导出失败");
+      } finally {
+        this.exportLoading = false;
+      }
+    },
+    handleProgressClick(row) {
+      this.$emit("progress-click", row);
+    },
+    getStatusType(statusName) {
+      const statusMap = {
+        未接单: "info",
+        处理中: "primary",
+        已完成: "success",
+        已完成未验收: "warning",
+        接单驳回: "danger",
+      };
+      return statusMap[statusName] || "info";
+    },
+    getProgressColor(statusName) {
+      const statusColorMap = {
+        未接单: "#909399",
+        处理中: "#409eff",
+        已完成未验收: "#e6a23c",
+        已完成: "#67c23a",
+        接单驳回: "#f56c6c",
+      };
+      return statusColorMap[statusName] || "#909399";
+    },
+    handleSortChange({ prop, order }) {
+      console.log("排序变更:", prop, order);
+    },
+    handleOrderClick(row) {
+      this.currentRow = row;
+      this.dialogVisible = true;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.box-card {
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  transition: all 0.3s ease;
+  min-height: 200px;
+  height: auto;
+
+  .header-container {
+    margin-bottom: 15px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .left {
+      display: flex;
+      align-items: center;
+      min-width: 200px;
+
+      .title {
+        font-size: 13px;
+        color: #333;
+      }
+
+      .update-time {
+        margin-left: 8px;
+        font-size: 13px;
+        color: #666;
+      }
+    }
+
+    .right {
+      flex: 1;
+      display: flex;
+      justify-content: flex-start;
+      padding-left: 40px;
+
+      .filter-group {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+      }
+    }
+  }
+}
+
+.progress-wrapper {
+  padding: 6px 0;
+
+  .progress-container {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .progress-bar {
+      width: 100%;
+      padding: 0 20px;
+
+      .custom-progress {
+        width: 100%;
+        height: 12px;
+        background-color: #ebeef5;
+        border-radius: 100px;
+        overflow: visible;
+        position: relative;
+
+        .progress-inner {
+          height: 100%;
+          transition: all 0.3s ease;
+          border-radius: 100px;
+          position: relative;
+
+          .progress-text {
+            position: absolute;
+            right: 6px;
+            top: 50%;
+            transform: translateY(-50%);
+            color: #fff;
+            font-size: 10px;
+            line-height: 1;
+            text-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
+            white-space: nowrap;
+          }
+        }
+
+        .total-text {
+          position: absolute;
+          right: 6px;
+          top: 50%;
+          transform: translateY(-50%);
+          color: #606266;
+          font-size: 10px;
+          line-height: 1;
+          white-space: nowrap;
+        }
+      }
+    }
+  }
+}
+
+:deep(.normal-time) {
+  color: #606266;
+  font-size: 13px;
+}
+
+:deep(.warning-time) {
+  color: #f56c6c;
+  font-size: 15px;
+  font-weight: bold;
+}
+
+:deep(.overdue-time) {
+  color: #f56c6c;
+  font-size: 15px;
+  font-weight: bold;
+}
+
+:deep(.early-time) {
+  color: #67c23a;
+  font-size: 15px;
+  font-weight: bold;
+}
+
+:deep(.el-table) {
+  font-size: 12px;
+
+  .el-table__header-wrapper {
+    position: sticky;
+    top: 0;
+    z-index: 1;
+  }
+
+  .el-table__header th {
+    padding: 8px 0;
+    background-color: #f5f7fa;
+  }
+
+  .el-table__body td {
+    padding: 8px 0;
+  }
+
+  .el-table__body tr:hover > td {
+    background-color: #f5f7fa !important;
+  }
+}
+
+:deep(.el-table__body-wrapper) {
+  overflow-y: auto;
+  &::-webkit-scrollbar {
+    width: 6px;
+    height: 6px;
+  }
+  &::-webkit-scrollbar-thumb {
+    border-radius: 3px;
+    background: #c0c4cc;
+  }
+  &::-webkit-scrollbar-track {
+    border-radius: 3px;
+    background: #f5f7fa;
+  }
+}
+
+.order-no-text {
+  color: #409eff;
+  cursor: pointer;
+  &:hover {
+    color: #66b1ff;
+  }
+}
+
+.filter-group {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+
+  :deep(.el-button) {
+    height: 32px;
+    padding: 0 15px;
+    font-size: 14px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .el-icon-download,
+    .el-icon-search {
+      margin-right: 5px;
+    }
+  }
+
+  :deep(.el-select) {
+    margin-right: 10px;
+
+    .el-input {
+      .el-input__inner {
+        height: 32px;
+        line-height: 32px;
+      }
+    }
+  }
+}
+</style>

+ 609 - 0
src/views/productManagement/installationSummary/components/OrderInstallationTable.vue

@@ -0,0 +1,609 @@
+<template>
+  <el-card class="box-card">
+    <div class="header-container">
+      <div class="left">
+        <span class="title">服务概况</span>
+        <span class="update-time" v-if="updateTime">{{ this.updateTime }}</span>
+      </div>
+      <div class="right">
+        <div class="filter-group">
+          <slot name="query-buttons"></slot>
+          <el-button
+            type="primary"
+            size="small"
+            :loading="exportLoading"
+            @click="handleExport"
+          >
+            <i class="el-icon-download" />
+            导出
+          </el-button>
+        </div>
+      </div>
+    </div>
+    <div class="table-container">
+      <el-table
+        v-loading="loading"
+        :data="tableData"
+        style="width: 100%"
+        border
+        size="small"
+        :height="tableData.length > 10 ? 450 : null"
+        :header-cell-style="{ background: '#f5f7fa' }"
+        @sort-change="handleSortChange"
+      >
+        <el-table-column type="index" label="序号" width="50" align="center" />
+        <el-table-column
+          prop="orderNo"
+          label="服务单号"
+          min-width="120"
+          align="center"
+          sortable
+        >
+          <template slot-scope="scope">
+            <span class="order-no-text" @click="handleView(scope.row)">
+              {{ scope.row.orderNo }}
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="customerName"
+          label="客户名称"
+          min-width="150"
+          align="center"
+          sortable
+        />
+        <el-table-column
+          prop="projectName"
+          label="项目名称"
+          min-width="110"
+          align="center"
+          sortable
+        />
+        <el-table-column
+          prop="goodsName"
+          label="货品名称"
+          min-width="110"
+          align="center"
+          sortable
+        />
+        <el-table-column
+          prop="serviceStaffNames"
+          label="服务人员"
+          width="150"
+          align="center"
+        >
+          <template slot-scope="scope">
+            <div class="product-tags">
+              <template v-if="scope.row.serviceStaffNames">
+                <el-tag
+                  v-for="(item, index) in scope.row.serviceStaffNames.split(
+                    ','
+                  )"
+                  :key="index"
+                  size="mini"
+                  :type="['success', 'warning', 'danger', 'primary'][index % 4]"
+                  effect="light"
+                  :class="{ 'long-text': item.length > 10 }"
+                >
+                  {{ item }}
+                </el-tag>
+              </template>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="statusName"
+          label="单据状态"
+          width="110"
+          sortable
+          align="center"
+        >
+          <template slot-scope="scope">
+            <el-tag
+              :type="getStatusType(scope.row.statusName)"
+              size="mini"
+              effect="plain"
+            >
+              {{ scope.row.statusName }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="progress"
+          label="进度"
+          width="250"
+          align="center"
+        >
+          <template slot-scope="scope">
+            <div class="progress-wrapper">
+              <div class="progress-container">
+                <div class="progress-bar">
+                  <div class="custom-progress">
+                    <div
+                      class="progress-inner"
+                      :style="{
+                        width: Math.min(Math.round(scope.row.rate), 100) + '%',
+                        backgroundColor: getProgressColor(scope.row.statusName),
+                      }"
+                    >
+                      <span
+                        class="progress-text"
+                        v-if="scope.row.shippedQuantity > 0"
+                      >
+                        {{ scope.row.shippedQuantity }}
+                      </span>
+                    </div>
+                    <span class="total-text">
+                      {{
+                        scope.row.rate === 0
+                          ? scope.row.orderQuantity
+                          : scope.row.orderQuantity
+                      }}
+                    </span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="orderQuantity" label="计划量" align="center" />
+        <el-table-column
+          prop="shippedQuantity"
+          label="已完成量"
+          align="center"
+        />
+        <el-table-column
+          prop="unshippedQuantity"
+          label="未完成量"
+          align="center"
+        />
+        <el-table-column
+          prop="yesterdayFinished"
+          label="昨日完成量"
+          align="center"
+        />
+        <el-table-column
+          prop="todayFinished"
+          label="今日完成量"
+          align="center"
+        />
+        <el-table-column prop="rate" label="完成率" align="center">
+          <template slot-scope="scope">
+            {{ parseInt(scope.row.rate) + "%" }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="remainingDays"
+          label="距离完成时间还剩"
+          width="130"
+          align="center"
+        >
+          <template slot-scope="scope">
+            <template
+              v-if="
+                scope.row.remainingTime !== null &&
+                scope.row.remainingTime !== undefined
+              "
+            >
+              <span
+                :class="{
+                  'normal-time':
+                    scope.row.remainingTime > 3 && !scope.row.earlyDays,
+                  'warning-time':
+                    scope.row.remainingTime >= 0 &&
+                    scope.row.remainingTime <= 3 &&
+                    !scope.row.earlyDays,
+                  'overdue-time':
+                    (scope.row.remainingTime < 0 && !scope.row.earlyDays) ||
+                    (scope.row.earlyDays && scope.row.earlyDays < 0),
+                  'early-time': scope.row.earlyDays && scope.row.earlyDays > 0,
+                }"
+              >
+                <template
+                  v-if="
+                    scope.row.earlyDays !== null &&
+                    scope.row.earlyDays !== undefined
+                  "
+                >
+                  <template v-if="scope.row.earlyDays > 0">
+                    提前 {{ scope.row.earlyDays }} 天完成
+                  </template>
+                  <template v-else-if="scope.row.earlyDays < 0">
+                    延期 {{ Math.abs(scope.row.earlyDays) }} 天完成
+                  </template>
+                  <template v-else> 按时完成 </template>
+                </template>
+                <template v-else>
+                  <template v-if="scope.row.remainingTime < 0">
+                    已逾期 {{ Math.abs(scope.row.remainingTime) }} 天
+                  </template>
+                  <template v-else> {{ scope.row.remainingTime }} 天 </template>
+                </template>
+              </span>
+            </template>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+    <view-dialog
+      :visible.sync="dialogVisible"
+      :row-data="currentRow"
+      :installer-options="personnelOptions"
+      :project-options="projectOptions"
+      :check-button-permission="() => false"
+      :is-current-user-in-service-staff="() => false"
+    />
+  </el-card>
+</template>
+
+<script>
+import ViewDialog from "@/views/productManagement/installationOrder/components/ViewDialog.vue";
+import { GetDataByName } from "@/api/common";
+import * as XLSX from "xlsx";
+
+export default {
+  name: "OrderInstallationTable",
+  components: {
+    ViewDialog,
+  },
+  // props: {
+  //   updateTime: {
+  //     type: String,
+  //     default: "",
+  //   },
+  // },
+  data() {
+    return {
+      loading: false,
+      exportLoading: false,
+      tableData: [],
+      dialogVisible: false,
+      currentRow: null,
+      personnelOptions: [],
+      projectOptions: [],
+      currentTime: "",
+      selectedOrder: null,
+      orderLoading: false,
+      orderOptions: [],
+    };
+  },
+  created() {
+    this.getTableData();
+  },
+  methods: {
+    updateCurrentTime() {
+      const now = new Date();
+      const year = now.getFullYear();
+      const month = String(now.getMonth() + 1).padStart(2, "0");
+      const day = String(now.getDate()).padStart(2, "0");
+      const hours = String(now.getHours()).padStart(2, "0");
+      const minutes = String(now.getMinutes()).padStart(2, "0");
+      const seconds = String(now.getSeconds()).padStart(2, "0");
+      const formattedTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+      this.updateTime = formattedTime;
+    },
+    async getTableData() {
+      try {
+        this.loading = true;
+        const response = await GetDataByName({
+          name: "getInstallOrderOverview",
+          offset: 0,
+          pagecount: 0,
+          parammaps: {},
+        });
+        if (response.data && response.data.list) {
+          this.tableData = response.data.list;
+          this.updateCurrentTime();
+        }
+      } catch (error) {
+        console.error("获取数据失败:", error);
+        this.$message.error("获取数据失败");
+      } finally {
+        this.loading = false;
+      }
+    },
+    handleProgressClick(row) {
+      this.$emit("progress-click", row);
+    },
+    handleView(row) {
+      this.currentRow = row;
+      this.dialogVisible = true;
+    },
+    getStatusType(statusName) {
+      const statusMap = {
+        未接单: "info",
+        处理中: "primary",
+        已完成: "success",
+        已完成未验收: "warning",
+        接单驳回: "danger",
+      };
+      return statusMap[statusName] || "info";
+    },
+    getProgressColor(statusName) {
+      // 根据状态返回对应的颜色
+      const statusColorMap = {
+        未接单: "#909399", // 灰色
+        处理中: "#409eff", // 蓝色
+        已完成未验收: "#e6a23c", // 橙色
+        已完成: "#67c23a", // 绿色
+        接单驳回: "#f56c6c", // 红色
+      };
+
+      return statusColorMap[statusName] || "#909399";
+    },
+    handleSortChange({ prop, order }) {
+      // 处理排序逻辑
+      console.log("排序变更:", prop, order);
+    },
+    async handleExport() {
+      try {
+        this.exportLoading = true;
+        const response = await GetDataByName({
+          name: "exportInstallOrderOverview",
+          offset: 0,
+          pagecount: 0,
+          parammaps: {},
+        });
+
+        if (response.data && response.data.list) {
+          const data = response.data.list;
+          const header = {
+            orderNo: "服务单号",
+            customerName: "客户名称",
+            projectName: "项目名称",
+            goodsName: "货品名称",
+            serviceStaffNames: "服务人员",
+            statusName: "单据状态",
+            orderQuantity: "计划量",
+            shippedQuantity: "已完成量",
+            unshippedQuantity: "未完成量",
+            yesterdayFinished: "昨日完成量",
+            todayFinished: "今日完成量",
+            rate: "完成率",
+            remainingTime: "距离完成时间还剩",
+          };
+
+          // 创建工作表
+          const ws = XLSX.utils.json_to_sheet([]);
+          // 添加表头
+          XLSX.utils.sheet_add_json(ws, [header], {
+            header: Object.keys(header),
+            skipHeader: true,
+          });
+          // 添加数据
+          XLSX.utils.sheet_add_json(ws, data, {
+            origin: "A2",
+            skipHeader: true,
+          });
+
+          // 设置列宽
+          const colWidths = Object.keys(header).map(() => ({ wch: 15 }));
+          ws["!cols"] = colWidths;
+
+          // 创建工作簿
+          const wb = XLSX.utils.book_new();
+          XLSX.utils.book_append_sheet(wb, ws, "订单安装概况");
+
+          // 导出文件
+          const now = new Date();
+          const year = now.getFullYear();
+          const month = String(now.getMonth() + 1).padStart(2, "0");
+          const day = String(now.getDate()).padStart(2, "0");
+          const hours = String(now.getHours()).padStart(2, "0");
+          const minutes = String(now.getMinutes()).padStart(2, "0");
+          const seconds = String(now.getSeconds()).padStart(2, "0");
+          const timestamp = `${year}${month}${day}${hours}${minutes}${seconds}`;
+          XLSX.writeFile(wb, `订单安装概况_${timestamp}.xlsx`);
+        }
+      } catch (error) {
+        console.error("导出失败:", error);
+        this.$message.error("导出失败");
+      } finally {
+        this.exportLoading = false;
+      }
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.box-card {
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  transition: all 0.3s ease;
+  min-height: 200px;
+  height: auto;
+
+  .header-container {
+    margin-bottom: 15px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .left {
+      display: flex;
+      align-items: center;
+      min-width: 200px;
+
+      .title {
+        font-size: 13px;
+        color: #333;
+      }
+
+      .update-time {
+        margin-left: 8px;
+        font-size: 13px;
+        color: #666;
+      }
+    }
+
+    .right {
+      flex: 1;
+      display: flex;
+      justify-content: flex-start;
+      padding-left: 40px;
+
+      .filter-group {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+      }
+    }
+  }
+}
+
+.progress-wrapper {
+  padding: 6px 0;
+
+  .progress-container {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .progress-bar {
+      width: 100%;
+      padding: 0 20px;
+
+      .custom-progress {
+        width: 100%;
+        height: 12px;
+        background-color: #ebeef5;
+        border-radius: 100px;
+        overflow: visible;
+        position: relative;
+
+        .progress-inner {
+          height: 100%;
+          transition: all 0.3s ease;
+          border-radius: 100px;
+          position: relative;
+
+          .progress-text {
+            position: absolute;
+            right: 6px;
+            top: 50%;
+            transform: translateY(-50%);
+            color: #fff;
+            font-size: 10px;
+            line-height: 1;
+            text-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
+            white-space: nowrap;
+          }
+        }
+
+        .total-text {
+          position: absolute;
+          right: 6px;
+          top: 50%;
+          transform: translateY(-50%);
+          color: #606266;
+          font-size: 10px;
+          line-height: 1;
+          white-space: nowrap;
+        }
+      }
+    }
+  }
+}
+
+:deep(.normal-time) {
+  color: #606266;
+  font-size: 13px;
+}
+
+:deep(.warning-time) {
+  color: #f56c6c;
+  font-size: 15px;
+  font-weight: bold;
+}
+
+:deep(.overdue-time) {
+  color: #f56c6c;
+  font-size: 15px;
+  font-weight: bold;
+}
+
+:deep(.early-time) {
+  color: #67c23a;
+  font-size: 15px;
+  font-weight: bold;
+}
+
+:deep(.el-table) {
+  font-size: 12px;
+
+  .el-table__header-wrapper {
+    position: sticky;
+    top: 0;
+    z-index: 1;
+  }
+
+  .el-table__header th {
+    padding: 8px 0;
+    background-color: #f5f7fa;
+  }
+
+  .el-table__body td {
+    padding: 8px 0;
+  }
+
+  .el-table__body tr:hover > td {
+    background-color: #f5f7fa !important;
+  }
+}
+
+:deep(.el-table__body-wrapper) {
+  overflow-y: auto;
+  &::-webkit-scrollbar {
+    width: 6px;
+    height: 6px;
+  }
+  &::-webkit-scrollbar-thumb {
+    border-radius: 3px;
+    background: #c0c4cc;
+  }
+  &::-webkit-scrollbar-track {
+    border-radius: 3px;
+    background: #f5f7fa;
+  }
+}
+
+.order-no-text {
+  color: #409eff;
+  cursor: pointer;
+  &:hover {
+    color: #66b1ff;
+  }
+}
+
+.filter-group {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+
+  :deep(.el-button) {
+    height: 32px;
+    padding: 0 15px;
+    font-size: 14px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .el-icon-download,
+    .el-icon-search {
+      margin-right: 5px;
+    }
+  }
+
+  :deep(.el-select) {
+    margin-right: 10px;
+
+    .el-input {
+      .el-input__inner {
+        height: 32px;
+        line-height: 32px;
+      }
+    }
+  }
+}
+</style>

+ 628 - 0
src/views/productManagement/installationSummary/components/StaffInstallationTable.vue

@@ -0,0 +1,628 @@
+<template>
+  <el-card class="box-card">
+    <div class="header-container">
+      <div class="left">
+        <span class="title">服务概况</span>
+        <span class="update-time" v-if="updateTime">{{ this.updateTime }}</span>
+      </div>
+      <div class="right">
+        <div class="filter-group">
+          <slot name="query-buttons"></slot>
+
+          <el-select
+            v-model="selectedPersonnel"
+            filterable
+            remote
+            clearable
+            size="small"
+            reserve-keyword
+            placeholder="请输入服务人员"
+            :loading="personnelLoading"
+            style="width: 150px; margin-right: 15px"
+          >
+            <el-option
+              v-for="item in personnelOptions"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"
+            />
+          </el-select>
+          <el-button
+            type="primary"
+            icon="el-icon-search"
+            size="small"
+            @click="getTableData"
+            >查询</el-button
+          >
+          <el-button
+            type="primary"
+            icon="el-icon-download"
+            size="small"
+            @click="handleExport"
+            :loading="exportLoading"
+            >导出</el-button
+          >
+        </div>
+      </div>
+    </div>
+    <div class="table-container">
+      <el-table
+        v-loading="loading"
+        :data="tableData"
+        style="width: 100%"
+        border
+        size="small"
+        :height="tableData.length > 10 ? 450 : null"
+        :header-cell-style="{ background: '#f5f7fa' }"
+        @sort-change="handleSortChange"
+      >
+        <el-table-column type="index" label="序号" width="50" align="center" />
+        <el-table-column
+          prop="empname"
+          label="服务人员"
+          min-width="100"
+          align="center"
+          sortable
+        />
+        <el-table-column
+          prop="customerName"
+          label="客户名称"
+          min-width="150"
+          align="center"
+          sortable
+        />
+        <el-table-column
+          prop="orderNo"
+          label="订单编号"
+          min-width="120"
+          align="center"
+          sortable
+        >
+          <template slot-scope="scope">
+            <span class="order-no-text" @click="handleOrderClick(scope.row)">{{
+              scope.row.orderNo
+            }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="goodsName"
+          label="货品名称"
+          min-width="110"
+          align="center"
+          sortable
+        />
+        <el-table-column
+          prop="statusName"
+          label="单据状态"
+          width="110"
+          sortable
+          align="center"
+        >
+          <template slot-scope="scope">
+            <el-tag
+              :type="getStatusType(scope.row.statusName)"
+              size="mini"
+              effect="plain"
+            >
+              {{ scope.row.statusName }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="estimatedCompleteTime"
+          label="预计完成时间"
+          width="130"
+          align="center"
+        />
+        <el-table-column
+          prop="remainingDays"
+          label="距离完成时间还剩"
+          width="130"
+          align="center"
+        >
+          <template slot-scope="scope">
+            <template
+              v-if="
+                scope.row.remainingTime !== null &&
+                scope.row.remainingTime !== undefined
+              "
+            >
+              <span
+                :class="{
+                  'normal-time':
+                    scope.row.remainingTime > 3 && !scope.row.earlyDays,
+                  'warning-time':
+                    scope.row.remainingTime >= 0 &&
+                    scope.row.remainingTime <= 3 &&
+                    !scope.row.earlyDays,
+                  'overdue-time':
+                    (scope.row.remainingTime < 0 && !scope.row.earlyDays) ||
+                    (scope.row.earlyDays && scope.row.earlyDays < 0),
+                  'early-time': scope.row.earlyDays && scope.row.earlyDays > 0,
+                }"
+              >
+                <template
+                  v-if="
+                    scope.row.earlyDays !== null &&
+                    scope.row.earlyDays !== undefined
+                  "
+                >
+                  <template v-if="scope.row.earlyDays > 0">
+                    提前 {{ scope.row.earlyDays }} 天完成
+                  </template>
+                  <template v-else-if="scope.row.earlyDays < 0">
+                    延期 {{ Math.abs(scope.row.earlyDays) }} 天完成
+                  </template>
+                  <template v-else> 按时完成 </template>
+                </template>
+                <template v-else>
+                  <template v-if="scope.row.remainingTime < 0">
+                    已逾期 {{ Math.abs(scope.row.remainingTime) }} 天
+                  </template>
+                  <template v-else> {{ scope.row.remainingTime }} 天 </template>
+                </template>
+              </span>
+            </template>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="progress"
+          label="进度"
+          width="250"
+          align="center"
+        >
+          <template slot-scope="scope">
+            <div class="progress-wrapper">
+              <div class="progress-container">
+                <div class="progress-bar">
+                  <div class="custom-progress">
+                    <div
+                      class="progress-inner"
+                      :style="{
+                        width: Math.min(Math.round(scope.row.rate), 100) + '%',
+                        backgroundColor: getProgressColor(scope.row.statusName),
+                      }"
+                    >
+                      <span
+                        class="progress-text"
+                        v-if="scope.row.installedQuantity > 0"
+                      >
+                        {{ scope.row.installedQuantity }}
+                      </span>
+                    </div>
+                    <span class="total-text">
+                      {{
+                        scope.row.rate === 0
+                          ? scope.row.totalQuantity
+                          : scope.row.totalQuantity
+                      }}
+                    </span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="totalQuantity" label="计划量" align="center" />
+        <el-table-column
+          prop="installedQuantity"
+          label="已完成量"
+          align="center"
+        />
+        <el-table-column
+          prop="uninstalledQuantity"
+          label="未完成量"
+          align="center"
+        />
+        <el-table-column
+          prop="serviceQuantity"
+          label="服务数量"
+          align="center"
+        />
+        <el-table-column prop="days" label="服务天时" align="center" />
+      </el-table>
+    </div>
+    <view-dialog
+      :visible.sync="dialogVisible"
+      :row-data="currentRow"
+      :installer-options="personnelOptions"
+      :project-options="projectOptions"
+      :check-button-permission="() => false"
+      :is-current-user-in-service-staff="() => false"
+    />
+  </el-card>
+</template>
+
+<script>
+import ViewDialog from "@/views/productManagement/installationOrder/components/ViewDialog.vue";
+import { GetDataByName } from "@/api/common";
+import * as XLSX from "xlsx";
+
+export default {
+  name: "StaffInstallationTable",
+  components: {
+    ViewDialog,
+  },
+  // props: {
+  //   updateTime: {
+  //     type: String,
+  //     default: "",
+  //   },
+  // },
+  data() {
+    return {
+      loading: false,
+      customerLoading: false,
+      exportLoading: false,
+      tableData: [],
+      dialogVisible: false,
+      currentRow: null,
+      personnelOptions: [],
+      projectOptions: [],
+      selectedPersonnel: null,
+      personnelLoading: false,
+      updateTime: "",
+    };
+  },
+  watch: {
+    // selectedCustomer(val) {
+    //   this.getTableData();
+    // },
+  },
+  created() {
+    this.selectedPersonnel = null;
+    this.getTableData();
+    this.getPersonnelOptions();
+  },
+  methods: {
+    updateCurrentTime() {
+      const now = new Date();
+      const year = now.getFullYear();
+      const month = String(now.getMonth() + 1).padStart(2, "0");
+      const day = String(now.getDate()).padStart(2, "0");
+      const hours = String(now.getHours()).padStart(2, "0");
+      const minutes = String(now.getMinutes()).padStart(2, "0");
+      const seconds = String(now.getSeconds()).padStart(2, "0");
+      const formattedTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+      this.updateTime = formattedTime;
+    },
+    async getPersonnelOptions() {
+      try {
+        this.personnelLoading = true;
+        const response = await GetDataByName({
+          name: "getUsersSelect",
+          offset: 0,
+          pagecount: 0,
+          parammaps: { enable: "1" },
+        });
+        this.personnelOptions = response.data.list || [];
+      } catch (error) {
+        console.error("获取服务人员列表失败:", error);
+        this.$message.error("获取服务人员列表失败");
+      } finally {
+        this.personnelLoading = false;
+      }
+    },
+    async getTableData() {
+      const send_select_list = {
+        name: "getInstallStaffOverview",
+        offset: 0,
+        pagecount: 0,
+        parammaps: {
+          personnelId: this.selectedPersonnel || "",
+        },
+      };
+
+      try {
+        this.loading = true;
+        const response = await GetDataByName(send_select_list);
+        this.tableData = response.data.list || [];
+        this.updateCurrentTime();
+        this.$emit("data-loaded");
+      } catch (error) {
+        console.error("获取表格数据失败:", error);
+        this.$message.error("获取表格数据失败");
+      } finally {
+        this.loading = false;
+      }
+    },
+    async handleExport() {
+      try {
+        this.exportLoading = true;
+        const response = await GetDataByName({
+          name: "exportInstallStaffOverview",
+          offset: 0,
+          pagecount: 0,
+          parammaps: {},
+        });
+
+        if (response.data && response.data.list) {
+          const data = response.data.list;
+          const header = {
+            empname: "服务人员",
+            customerName: "客户名称",
+            orderNo: "订单编号",
+            goodsName: "货品名称",
+            statusName: "单据状态",
+            estimatedCompleteTime: "预计完成时间",
+            remainingTime: "距离完成时间还剩",
+            totalQuantity: "计划量",
+            installedQuantity: "已完成量",
+            uninstalledQuantity: "未完成量",
+            serviceQuantity: "服务数量",
+            days: "服务天时",
+          };
+
+          // 创建工作表
+          const ws = XLSX.utils.json_to_sheet([]);
+          // 添加表头
+          XLSX.utils.sheet_add_json(ws, [header], {
+            header: Object.keys(header),
+            skipHeader: true,
+          });
+          // 添加数据
+          XLSX.utils.sheet_add_json(ws, data, {
+            origin: "A2",
+            skipHeader: true,
+          });
+
+          // 设置列宽
+          const colWidths = Object.keys(header).map(() => ({ wch: 15 }));
+          ws["!cols"] = colWidths;
+
+          // 创建工作簿
+          const wb = XLSX.utils.book_new();
+          XLSX.utils.book_append_sheet(wb, ws, "人员安装概况");
+
+          // 导出文件
+          const now = new Date();
+          const year = now.getFullYear();
+          const month = String(now.getMonth() + 1).padStart(2, "0");
+          const day = String(now.getDate()).padStart(2, "0");
+          const hours = String(now.getHours()).padStart(2, "0");
+          const minutes = String(now.getMinutes()).padStart(2, "0");
+          const seconds = String(now.getSeconds()).padStart(2, "0");
+          const timestamp = `${year}${month}${day}${hours}${minutes}${seconds}`;
+          XLSX.writeFile(wb, `人员安装概况_${timestamp}.xlsx`);
+        }
+      } catch (error) {
+        console.error("导出失败:", error);
+        this.$message.error("导出失败");
+      } finally {
+        this.exportLoading = false;
+      }
+    },
+    handleProgressClick(row) {
+      this.$emit("progress-click", row);
+    },
+    getStatusType(statusName) {
+      const statusMap = {
+        未接单: "info",
+        处理中: "primary",
+        已完成: "success",
+        已完成未验收: "warning",
+        接单驳回: "danger",
+      };
+      return statusMap[statusName] || "info";
+    },
+    getProgressColor(statusName) {
+      const statusColorMap = {
+        未接单: "#909399",
+        处理中: "#409eff",
+        已完成未验收: "#e6a23c",
+        已完成: "#67c23a",
+        接单驳回: "#f56c6c",
+      };
+      return statusColorMap[statusName] || "#909399";
+    },
+    handleSortChange({ prop, order }) {
+      console.log("排序变更:", prop, order);
+    },
+    handleOrderClick(row) {
+      this.currentRow = row;
+      this.dialogVisible = true;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.box-card {
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  transition: all 0.3s ease;
+  min-height: 200px;
+  height: auto;
+
+  .header-container {
+    margin-bottom: 15px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .left {
+      display: flex;
+      align-items: center;
+      min-width: 200px;
+
+      .title {
+        font-size: 13px;
+        color: #333;
+      }
+
+      .update-time {
+        margin-left: 8px;
+        font-size: 13px;
+        color: #666;
+      }
+    }
+
+    .right {
+      flex: 1;
+      display: flex;
+      justify-content: flex-start;
+      padding-left: 40px;
+
+      .filter-group {
+        display: flex;
+        align-items: center;
+      }
+    }
+  }
+}
+
+.progress-wrapper {
+  padding: 6px 0;
+
+  .progress-container {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .progress-bar {
+      width: 100%;
+      padding: 0 20px;
+
+      .custom-progress {
+        width: 100%;
+        height: 12px;
+        background-color: #ebeef5;
+        border-radius: 100px;
+        overflow: visible;
+        position: relative;
+
+        .progress-inner {
+          height: 100%;
+          transition: all 0.3s ease;
+          border-radius: 100px;
+          position: relative;
+
+          .progress-text {
+            position: absolute;
+            right: 6px;
+            top: 50%;
+            transform: translateY(-50%);
+            color: #fff;
+            font-size: 10px;
+            line-height: 1;
+            text-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
+            white-space: nowrap;
+          }
+        }
+
+        .total-text {
+          position: absolute;
+          right: 6px;
+          top: 50%;
+          transform: translateY(-50%);
+          color: #606266;
+          font-size: 10px;
+          line-height: 1;
+          white-space: nowrap;
+        }
+      }
+    }
+  }
+}
+
+:deep(.normal-time) {
+  color: #606266;
+  font-size: 13px;
+}
+
+:deep(.warning-time) {
+  color: #f56c6c;
+  font-size: 15px;
+  font-weight: bold;
+}
+
+:deep(.overdue-time) {
+  color: #f56c6c;
+  font-size: 15px;
+  font-weight: bold;
+}
+
+:deep(.early-time) {
+  color: #67c23a;
+  font-size: 15px;
+  font-weight: bold;
+}
+
+:deep(.el-table) {
+  font-size: 12px;
+
+  .el-table__header-wrapper {
+    position: sticky;
+    top: 0;
+    z-index: 1;
+  }
+
+  .el-table__header th {
+    padding: 8px 0;
+    background-color: #f5f7fa;
+  }
+
+  .el-table__body td {
+    padding: 8px 0;
+  }
+
+  .el-table__body tr:hover > td {
+    background-color: #f5f7fa !important;
+  }
+}
+
+:deep(.el-table__body-wrapper) {
+  overflow-y: auto;
+  &::-webkit-scrollbar {
+    width: 6px;
+    height: 6px;
+  }
+  &::-webkit-scrollbar-thumb {
+    border-radius: 3px;
+    background: #c0c4cc;
+  }
+  &::-webkit-scrollbar-track {
+    border-radius: 3px;
+    background: #f5f7fa;
+  }
+}
+
+.order-no-text {
+  color: #409eff;
+  cursor: pointer;
+  &:hover {
+    color: #66b1ff;
+  }
+}
+
+.filter-group {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+
+  :deep(.el-button) {
+    height: 32px;
+    padding: 0 15px;
+    font-size: 14px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .el-icon-download,
+    .el-icon-search {
+      margin-right: 5px;
+    }
+  }
+
+  :deep(.el-select) {
+    margin-right: 10px;
+
+    .el-input {
+      .el-input__inner {
+        height: 32px;
+        line-height: 32px;
+      }
+    }
+  }
+}
+</style>

+ 288 - 11
src/views/productManagement/installationSummary/index.vue

@@ -8,14 +8,140 @@
 
     <!-- 顶部统计卡片 -->
     <statistics-panel />
-
     <!-- 服务概况表格 -->
-    <installation-table
+    <!-- <installation-table
       :table-data.sync="tableData"
       :update-time.sync="updateTime"
       @unassigned-click="handleUnassignedOrders"
       @progress-click="handleProgressClick"
-    />
+    /> -->
+    <!-- 使用keep-alive缓存组件 -->
+    <keep-alive>
+      <div class="table-container">
+        <!-- 按订单查询表格 -->
+        <order-installation-table
+          ref="orderTable"
+          v-show="queryType === 'order'"
+          :update-time="updateTime"
+          @progress-click="handleProgressClick"
+          @data-loaded="handleDataLoaded"
+        >
+          <template #query-buttons>
+            <el-button-group>
+              <el-button
+                type="primary"
+                :plain="queryType !== 'order'"
+                :loading="isLoading"
+                :disabled="isLoading"
+                @click="handleManualSwitch('order')"
+              >
+                按订单查询
+              </el-button>
+              <el-button
+                type="primary"
+                :plain="queryType !== 'customer'"
+                :loading="isLoading"
+                :disabled="isLoading"
+                @click="handleManualSwitch('customer')"
+              >
+                按客户查询
+              </el-button>
+              <el-button
+                type="primary"
+                :plain="queryType !== 'staff'"
+                :loading="isLoading"
+                :disabled="isLoading"
+                @click="handleManualSwitch('staff')"
+              >
+                按人员查询
+              </el-button>
+            </el-button-group>
+          </template>
+        </order-installation-table>
+
+        <!-- 按客户查询表格 -->
+        <customer-installation-table
+          ref="customerTable"
+          v-show="queryType === 'customer'"
+          :update-time="updateTime"
+          @progress-click="handleProgressClick"
+          @data-loaded="handleDataLoaded"
+        >
+          <template #query-buttons>
+            <el-button-group>
+              <el-button
+                type="primary"
+                :plain="queryType !== 'order'"
+                :loading="isLoading"
+                :disabled="isLoading"
+                @click="handleManualSwitch('order')"
+              >
+                按订单查询
+              </el-button>
+              <el-button
+                type="primary"
+                :plain="queryType !== 'customer'"
+                :loading="isLoading"
+                :disabled="isLoading"
+                @click="handleManualSwitch('customer')"
+              >
+                按客户查询
+              </el-button>
+              <el-button
+                type="primary"
+                :plain="queryType !== 'staff'"
+                :loading="isLoading"
+                :disabled="isLoading"
+                @click="handleManualSwitch('staff')"
+              >
+                按人员查询
+              </el-button>
+            </el-button-group>
+          </template>
+        </customer-installation-table>
+
+        <!-- 按人员查询表格 -->
+        <staff-installation-table
+          ref="staffTable"
+          v-show="queryType === 'staff'"
+          :update-time="updateTime"
+          @progress-click="handleProgressClick"
+          @data-loaded="handleDataLoaded"
+        >
+          <template #query-buttons>
+            <el-button-group>
+              <el-button
+                type="primary"
+                :plain="queryType !== 'order'"
+                :loading="isLoading"
+                :disabled="isLoading"
+                @click="handleManualSwitch('order')"
+              >
+                按订单查询
+              </el-button>
+              <el-button
+                type="primary"
+                :plain="queryType !== 'customer'"
+                :loading="isLoading"
+                :disabled="isLoading"
+                @click="handleManualSwitch('customer')"
+              >
+                按客户查询
+              </el-button>
+              <el-button
+                type="primary"
+                :plain="queryType !== 'staff'"
+                :loading="isLoading"
+                :disabled="isLoading"
+                @click="handleManualSwitch('staff')"
+              >
+                按人员查询
+              </el-button>
+            </el-button-group>
+          </template>
+        </staff-installation-table>
+      </div>
+    </keep-alive>
 
     <!-- 已安装统计表格 -->
     <!-- <installation-statistics :update-time="updateTime" /> -->
@@ -27,18 +153,22 @@
 
 <script>
 import StatisticsPanel from "./components/StatisticsPanel";
-import InstallationTable from "./components/InstallationTable";
+import OrderInstallationTable from "./components/OrderInstallationTable";
+import CustomerInstallationTable from "./components/CustomerInstallationTable";
+import StaffInstallationTable from "./components/StaffInstallationTable";
 import InstallationStatusChart from "./components/InstallationStatusChart";
-import InstallationStatistics from "./components/InstallationStatistics.vue";
+import InstallationTable from "./components/InstallationTable";
 import { GetDataByName, GetDataByNames } from "@/api/common";
 
 export default {
   name: "InstallationSummary",
   components: {
     StatisticsPanel,
-    InstallationTable,
+    OrderInstallationTable,
+    CustomerInstallationTable,
+    StaffInstallationTable,
     InstallationStatusChart,
-    InstallationStatistics,
+    InstallationTable,
   },
   data() {
     return {
@@ -47,6 +177,12 @@ export default {
       dialogVisible: false,
       tableData: [],
       unassignedOrders: [],
+      queryType: "order", // 默认按订单查询
+      isLoading: false,
+      autoSwitchTimer: null, // 自动切换定时器
+      switchInterval: 60 * 1000, // 1分钟
+      defaultInterval: 60 * 1000, // 1分钟
+      manualInterval: 300 * 1000, // 5分钟
       chartData: {
         labels: [
           "智能膜环",
@@ -87,8 +223,13 @@ export default {
     this.updateCurrentTime();
     setInterval(this.updateCurrentTime, 1000);
     this.getUpdateTime();
-    // setInterval(this.getUpdateTime, 60000);
     this.getTableData();
+    console.log("开始自动切换,间隔:", this.switchInterval);
+    this.startAutoSwitch();
+  },
+  beforeDestroy() {
+    console.log("组件销毁,清除定时器");
+    this.clearAutoSwitch();
   },
   methods: {
     updateCurrentTime() {
@@ -130,20 +271,36 @@ export default {
       console.log("进度条被点击:", row);
     },
     async getTableData() {
+      let apiName = "";
+      switch (this.queryType) {
+        case "order":
+          apiName = "getInstallOrderOverview";
+          break;
+        case "customer":
+          apiName = "getInstallCustomerOverview";
+          break;
+        case "staff":
+          apiName = "getInstallStaffOverview";
+          break;
+      }
+
       const send_select_list = {
-        name: "getInstallOrderOverview",
+        name: apiName,
         offset: 0,
         pagecount: 0,
         parammaps: {},
       };
-      try {
-        console.log("获取安装概况表格数据");
 
+      try {
+        console.log(`获取${this.getQueryTypeName(this.queryType)}数据`);
+        this.isLoading = true;
         const response = await GetDataByName(send_select_list);
         this.tableData = response.data.list || [];
       } catch (error) {
         console.error("获取表格数据失败:", error);
         this.$message.error("获取表格数据失败");
+      } finally {
+        this.isLoading = false;
       }
     },
     handleFilterChange({ month, product }) {
@@ -158,6 +315,106 @@ export default {
         this.$message.error("获取图表数据失败");
       }
     },
+    // 开始自动切换
+    startAutoSwitch() {
+      console.log("启动自动切换定时器");
+      this.clearAutoSwitch();
+      this.autoSwitchTimer = setInterval(() => {
+        console.log("触发自动切换");
+        this.handleAutoSwitch();
+      }, this.switchInterval);
+    },
+
+    // 清除自动切换定时器
+    clearAutoSwitch() {
+      if (this.autoSwitchTimer) {
+        console.log("清除现有定时器");
+        clearInterval(this.autoSwitchTimer);
+        this.autoSwitchTimer = null;
+      }
+    },
+
+    // 处理自动切换
+    async handleAutoSwitch() {
+      console.log("执行自动切换,当前类型:", this.queryType);
+      const types = ["order", "customer", "staff"];
+      const currentIndex = types.indexOf(this.queryType);
+      const nextIndex = (currentIndex + 1) % types.length;
+      const newType = types[nextIndex];
+
+      await this.switchQueryType(newType);
+      this.$message.info(
+        `已自动切换到${this.getQueryTypeName(newType)}查询模式,${
+          this.switchInterval / 1000
+        }秒后下次切换`
+      );
+    },
+
+    // 处理手动切换
+    async handleManualSwitch(type) {
+      if (this.queryType === type || this.isLoading) return;
+
+      console.log("手动切换到:", type);
+      this.switchInterval = this.manualInterval;
+      this.clearAutoSwitch();
+      this.startAutoSwitch();
+
+      await this.switchQueryType(type);
+      this.$message.success(
+        `已切换到${this.getQueryTypeName(type)}查询模式,${
+          this.switchInterval / 1000
+        }秒后自动切换`
+      );
+    },
+
+    // 切换查询类型
+    async switchQueryType(type) {
+      if (this.queryType === type || this.isLoading) return;
+
+      console.log("切换查询类型到:", type);
+      this.isLoading = true;
+      this.queryType = type;
+
+      try {
+        // 获取对应类型的数据
+        await this.getTableData();
+      } catch (error) {
+        console.error("切换查询类型失败:", error);
+        this.$message.error("切换查询类型失败");
+      } finally {
+        this.isLoading = false;
+      }
+    },
+
+    // 获取查询类型名称
+    getQueryTypeName(type) {
+      const typeNames = {
+        order: "按订单",
+        customer: "按客户",
+        staff: "按人员",
+      };
+      return typeNames[type] || type;
+    },
+
+    // 重置切换间隔
+    resetSwitchInterval() {
+      console.log("重置切换间隔");
+      this.switchInterval = this.defaultInterval;
+      this.clearAutoSwitch();
+      this.startAutoSwitch();
+      this.$message.info(`已重置为${this.switchInterval / 1000}秒自动切换`);
+    },
+
+    handleDataLoaded() {
+      // 数据加载完成后更新时间
+      const now = new Date();
+      const month = (now.getMonth() + 1).toString().padStart(2, "0");
+      const day = now.getDate().toString().padStart(2, "0");
+      const hours = now.getHours().toString().padStart(2, "0");
+      const minutes = now.getMinutes().toString().padStart(2, "0");
+      const seconds = now.getSeconds().toString().padStart(2, "0");
+      this.updateTime = `${month}/${day} ${hours}:${minutes}:${seconds}更新`;
+    },
   },
 };
 </script>
@@ -199,4 +456,24 @@ export default {
     font-size: 14px;
   }
 }
+
+.table-container {
+  position: relative;
+  margin-bottom: 24px;
+
+  > * {
+    transition: opacity 0.2s ease;
+  }
+}
+
+@keyframes fadeInScale {
+  from {
+    opacity: 0;
+    transform: scale(0.98);
+  }
+  to {
+    opacity: 1;
+    transform: scale(1);
+  }
+}
 </style>

+ 590 - 0
src/views/productManagement/productionSummary/components/PersonnelProductionTable.vue

@@ -0,0 +1,590 @@
+<template>
+  <el-card class="box-card">
+    <div class="header-container">
+      <div class="left">
+        <span class="title">生产概况</span>
+        <span class="update-time" v-if="updateTime">{{ updateTime }}</span>
+      </div>
+      <div class="right">
+        <div class="filter-group">
+          <slot name="query-buttons"></slot>
+          <el-button
+            type="text"
+            icon="el-icon-download"
+            @click="handleExport"
+            style="
+              padding: 0;
+              font-size: 16px;
+              color: #909399;
+              margin-left: 8px;
+            "
+          />
+        </div>
+      </div>
+    </div>
+    <div class="table-container">
+      <el-table
+        v-loading="loading"
+        :data="localTableData"
+        style="width: 100%"
+        border
+        size="small"
+        :height="localTableData.length > 10 ? 450 : null"
+        :header-cell-style="{ background: '#f5f7fa' }"
+      >
+        <el-table-column type="index" label="序号" width="50" align="center" />
+        <el-table-column
+          prop="goodsName"
+          label="货品名称"
+          width="150"
+          align="center"
+          sortable
+        />
+        <el-table-column
+          prop="model"
+          label="货品型号"
+          width="80"
+          align="center"
+          sortable
+        />
+        <el-table-column
+          prop="orderStatus"
+          label="货品状态"
+          width="100"
+          align="center"
+          sortable
+        >
+          <template slot-scope="scope">
+            <el-tag
+              :type="getStatusType(scope.row.orderStatus)"
+              size="mini"
+              effect="plain"
+            >
+              {{ scope.row.orderStatus }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="progress"
+          label="进度"
+          align="center"
+          width="250"
+          sortable
+        >
+          <template slot-scope="scope">
+            <div
+              class="progress-wrapper"
+              @mouseenter="
+                showTooltip(
+                  $event,
+                  (
+                    (scope.row.installedQuantity / scope.row.totalQuantity) *
+                    100
+                  ).toFixed(0)
+                )
+              "
+              @mouseleave="hideTooltip"
+            >
+              <div class="progress-container">
+                <div class="progress-bar">
+                  <div class="custom-progress">
+                    <div
+                      class="progress-inner"
+                      :style="{
+                        width:
+                          Math.min(
+                            Math.round(
+                              (scope.row.installedQuantity /
+                                scope.row.totalQuantity) *
+                                100
+                            ),
+                            100
+                          ) + '%',
+                        backgroundColor: getProgressColor(
+                          scope.row.orderStatus
+                        ),
+                      }"
+                    >
+                      <span
+                        class="progress-text"
+                        v-if="scope.row.installedQuantity > 0"
+                      >
+                        {{ scope.row.installedQuantity }}
+                      </span>
+                    </div>
+                    <span class="total-text">
+                      {{ scope.row.totalQuantity }}
+                    </span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="totalQuantity" label="计划量" align="center" />
+        <el-table-column
+          prop="installedQuantity"
+          label="已完成量"
+          align="center"
+        />
+        <el-table-column
+          prop="uninstalledQuantity"
+          label="未完成量"
+          align="center"
+        />
+        <el-table-column
+          prop="yesterdayQuantity"
+          label="昨日完成量"
+          align="center"
+        />
+        <el-table-column
+          prop="todayQuantity"
+          label="今日完成量"
+          align="center"
+        />
+        <el-table-column prop="rate" label="完成率" align="center" sortable>
+          <template slot-scope="scope">
+            {{
+              (
+                (scope.row.installedQuantity / scope.row.totalQuantity) *
+                100
+              ).toFixed(1) + "%"
+            }}
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+  </el-card>
+</template>
+
+<script>
+import { GetDataByName, GetDataByNames } from "@/api/common";
+import * as XLSX from "xlsx";
+
+export default {
+  name: "ProductProductionTable",
+  data() {
+    return {
+      loading: false,
+      localTableData: [],
+      tooltip: null,
+      tooltipTimeout: null,
+    };
+  },
+  props: {
+    updateTime: {
+      type: String,
+      required: true,
+    },
+  },
+  created() {
+    this.initData();
+  },
+  methods: {
+    getStatusType(status) {
+      switch (status) {
+        case "生产完成":
+          return "success";
+        case "待生产":
+          return "warning";
+        case "未生产":
+          return "info";
+        case "生产中":
+          return "primary";
+        case "已关单":
+          return "danger";
+        case "未开始":
+          return "info";
+        default:
+          return "info";
+      }
+    },
+    getProgressColor(status) {
+      switch (status) {
+        case "生产完成":
+          return "#67C23A";
+        case "待生产":
+          return "#E6A23C";
+        case "生产中":
+          return "#409EFF";
+        case "已关单":
+          return "#F56C6C";
+        case "未生产":
+          return "#909399";
+        default:
+          return "#909399";
+      }
+    },
+    async initData() {
+      try {
+        await this.fetchTableData();
+      } catch (error) {
+        console.error("初始化数据失败:", error);
+      }
+    },
+    async fetchTableData() {
+      try {
+        this.loading = true;
+        await new Promise((resolve) => setTimeout(resolve, 1000));
+        const send_data = {
+          name: "getProductionSummaryByGoods",
+          parammaps: {},
+        };
+        GetDataByName(send_data)
+          .then((response) => {
+            this.localTableData = response.data.list || [];
+          })
+          .catch((error) => {
+            console.error("获取数据失败:", error);
+          });
+        this.$emit("data-loaded");
+      } catch (error) {
+        console.error("获取生产列表失败:", error);
+        this.$message.error("获取生产列表失败");
+      } finally {
+        this.loading = false;
+      }
+    },
+    showTooltip(event, percentage) {
+      // 清除之前的tooltip和定时器
+      this.hideTooltip();
+
+      // 创建新的tooltip
+      this.tooltip = document.createElement("div");
+      this.tooltip.className = "progress-tooltip";
+      this.tooltip.textContent = percentage + "%";
+
+      document.body.appendChild(this.tooltip);
+
+      const rect = event.target.getBoundingClientRect();
+      const tooltipRect = this.tooltip.getBoundingClientRect();
+
+      this.tooltip.style.left =
+        rect.left + rect.width / 2 - tooltipRect.width / 2 + "px";
+      this.tooltip.style.top = rect.top - tooltipRect.height - 8 + "px";
+
+      // 使用 requestAnimationFrame 确保动画流畅
+      requestAnimationFrame(() => {
+        if (this.tooltip) {
+          this.tooltip.classList.add("show");
+        }
+      });
+
+      // 添加安全清理机制
+      this.tooltipTimeout = setTimeout(() => {
+        this.hideTooltip();
+      }, 3000); // 3秒后自动清理
+    },
+    hideTooltip() {
+      // 清除定时器
+      if (this.tooltipTimeout) {
+        clearTimeout(this.tooltipTimeout);
+        this.tooltipTimeout = null;
+      }
+
+      // 清除tooltip
+      if (this.tooltip) {
+        this.tooltip.classList.remove("show");
+        const tooltipToRemove = this.tooltip;
+
+        setTimeout(() => {
+          if (tooltipToRemove && tooltipToRemove.parentNode) {
+            tooltipToRemove.parentNode.removeChild(tooltipToRemove);
+          }
+        }, 200);
+
+        this.tooltip = null;
+      }
+    },
+    // 获取格式化的日期时间
+    getFormattedDateTime() {
+      const now = new Date();
+      const year = now.getFullYear();
+      const month = String(now.getMonth() + 1).padStart(2, "0");
+      const day = String(now.getDate()).padStart(2, "0");
+      const hours = String(now.getHours()).padStart(2, "0");
+      const minutes = String(now.getMinutes()).padStart(2, "0");
+      const seconds = String(now.getSeconds()).padStart(2, "0");
+      return `${year}${month}${day}${hours}${minutes}${seconds}`;
+    },
+    // 导出功能
+    async handleExport() {
+      try {
+        if (this.localTableData.length === 0) {
+          this.$message.warning("暂无数据可导出");
+          return;
+        }
+
+        // 定义表头
+        const headers = [
+          "序号",
+          "货品名称",
+          "货品型号",
+          "货品状态",
+          "进度",
+          "计划量",
+          "已完成量",
+          "未完成量",
+          "昨日完成量",
+          "今日完成量",
+          "完成率",
+        ];
+
+        // 格式化数据
+        const rows = this.localTableData.map((item, index) => [
+          index + 1,
+          item.goodsName || "",
+          item.model || "",
+          item.orderStatus || "",
+          `${((item.installedQuantity / item.totalQuantity) * 100).toFixed(
+            2
+          )}%`,
+          item.totalQuantity || 0,
+          item.installedQuantity || 0,
+          item.uninstalledQuantity || 0,
+          item.yesterdayQuantity || 0,
+          item.todayQuantity || 0,
+          `${((item.installedQuantity / item.totalQuantity) * 100).toFixed(
+            1
+          )}%`,
+        ]);
+
+        // 创建工作簿
+        const wb = XLSX.utils.book_new();
+
+        // 组合表头和数据
+        const wsData = [headers, ...rows];
+
+        // 创建工作表
+        const ws = XLSX.utils.aoa_to_sheet(wsData);
+
+        // 设置列宽
+        const colWidths = [
+          { wch: 8 }, // 序号
+          { wch: 30 }, // 货品名称
+          { wch: 15 }, // 货品型号
+          { wch: 12 }, // 货品状态
+          { wch: 10 }, // 进度
+          { wch: 10 }, // 计划量
+          { wch: 12 }, // 已完成量
+          { wch: 12 }, // 未完成量
+          { wch: 12 }, // 昨日完成量
+          { wch: 12 }, // 今日完成量
+          { wch: 10 }, // 完成率
+        ];
+        ws["!cols"] = colWidths;
+
+        // 添加工作表到工作簿
+        XLSX.utils.book_append_sheet(wb, ws, "生产概况(按货品)");
+
+        // 生成文件名
+        const fileName = `生产概况(按货品)_${this.getFormattedDateTime()}.xlsx`;
+
+        // 导出文件
+        XLSX.writeFile(wb, fileName);
+
+        this.$message.success("导出成功");
+      } catch (error) {
+        console.error("导出失败:", error);
+        this.$message.error("导出失败");
+      }
+    },
+  },
+  beforeDestroy() {
+    // 组件销毁时清理所有相关资源
+    this.hideTooltip();
+    if (this.tooltipTimeout) {
+      clearTimeout(this.tooltipTimeout);
+    }
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.box-card {
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  transition: all 0.3s ease;
+  min-height: 200px;
+  height: auto;
+
+  .header-container {
+    margin-bottom: 15px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .left {
+      display: flex;
+      align-items: center;
+      min-width: 200px;
+
+      .title {
+        font-size: 13px;
+        color: #333;
+      }
+
+      .update-time {
+        margin-left: 8px;
+        font-size: 13px;
+        color: #666;
+      }
+    }
+
+    .right {
+      flex: 1;
+      display: flex;
+      justify-content: flex-start;
+      padding-left: 40px;
+
+      .filter-group {
+        display: flex;
+        align-items: center;
+      }
+    }
+  }
+
+  .el-card__header {
+    padding: 16px 20px;
+    border-bottom: 1px solid #ebeef5;
+  }
+
+  :deep(.el-card__body) {
+    padding: 16px;
+    transition: all 0.3s ease;
+  }
+}
+
+.table-container {
+  position: relative;
+  padding-bottom: 0;
+}
+
+.progress-wrapper {
+  padding: 6px 0;
+  width: 100%;
+  height: 100%;
+  cursor: pointer;
+}
+
+.progress-container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  .progress-bar {
+    width: 100%;
+    padding: 0 20px;
+
+    .custom-progress {
+      width: 100%;
+      height: 12px;
+      background-color: #ebeef5;
+      border-radius: 100px;
+      overflow: hidden;
+      position: relative;
+
+      .progress-inner {
+        height: 100%;
+        transition: all 0.3s ease;
+        border-radius: 100px;
+        position: relative;
+
+        .progress-text {
+          position: absolute;
+          right: 6px;
+          top: 50%;
+          transform: translateY(-50%);
+          color: #fff;
+          font-size: 10px;
+          line-height: 1;
+          text-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
+          white-space: nowrap;
+        }
+      }
+
+      .total-text {
+        position: absolute;
+        right: 6px;
+        top: 50%;
+        transform: translateY(-50%);
+        color: #606266;
+        font-size: 10px;
+        line-height: 1;
+        white-space: nowrap;
+      }
+    }
+  }
+}
+
+:deep(.el-table) {
+  font-size: 12px;
+
+  .el-table__header-wrapper {
+    position: sticky;
+    top: 0;
+    z-index: 1;
+  }
+
+  .el-table__header th {
+    padding: 8px 0;
+    background-color: #f5f7fa;
+  }
+
+  .el-table__body td {
+    padding: 8px 0;
+  }
+
+  .el-table__body tr:hover > td {
+    background-color: #f5f7fa !important;
+  }
+}
+
+:deep(.el-table__body-wrapper) {
+  overflow-y: auto;
+  &::-webkit-scrollbar {
+    width: 6px;
+    height: 6px;
+  }
+  &::-webkit-scrollbar-thumb {
+    border-radius: 3px;
+    background: #c0c4cc;
+  }
+  &::-webkit-scrollbar-track {
+    border-radius: 3px;
+    background: #f5f7fa;
+  }
+}
+</style>
+
+<style lang="scss">
+.progress-tooltip {
+  position: fixed;
+  background: rgba(0, 0, 0, 0.8);
+  color: #fff;
+  padding: 4px 8px;
+  border-radius: 4px;
+  font-size: 12px;
+  pointer-events: none;
+  z-index: 9999;
+  opacity: 0;
+  transform: translateY(5px);
+  transition: all 0.2s ease;
+
+  &::after {
+    content: "";
+    position: absolute;
+    bottom: -4px;
+    left: 50%;
+    transform: translateX(-50%);
+    border-left: 4px solid transparent;
+    border-right: 4px solid transparent;
+    border-top: 4px solid rgba(0, 0, 0, 0.8);
+  }
+
+  &.show {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+</style>

+ 12 - 4
src/views/productManagement/productionSummary/index.vue

@@ -77,10 +77,8 @@
         </product-production-table>
       </div>
     </keep-alive>
-
     <!-- 发货状态表格 -->
     <delivery-status-table />
-
     <!-- 货品生产情况 -->
     <!-- <production-status-chart
       :chart-data="chartData"
@@ -257,7 +255,18 @@ export default {
     // 处理自动切换
     async handleAutoSwitch() {
       console.log("执行自动切换,当前类型:", this.queryType);
-      const newType = this.queryType === "order" ? "product" : "order";
+      let newType;
+      switch (this.queryType) {
+        case "order":
+          newType = "product";
+          break;
+        case "product":
+          newType = "order";
+          break;
+        default:
+          newType = "order";
+      }
+
       await this.switchQueryType(newType);
       this.$message.info(
         `已自动切换到${newType === "order" ? "按订单" : "按货品"}查询模式,${
@@ -271,7 +280,6 @@ export default {
       if (this.queryType === type || this.isLoading) return;
 
       console.log("手动切换到:", type);
-      // 更新切换间隔
       this.switchInterval = this.manualInterval;
       this.clearAutoSwitch();
       this.startAutoSwitch();