gaoluyang
2026-05-09 db2e215ffe92af3a6558344b2150cadbab7cfe2c
Merge branch 'dev_NEW_pro' into dev_浪潮

# Conflicts:
# src/views/customerService/feedbackRegistration/components/formDia.vue
已添加18个文件
已修改31个文件
11077 ■■■■■ 文件已修改
multiple/assets/favicon/DZZBfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/KSfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/DZZBLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/KSLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/config.json 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/accountSubject.js 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/deliveryLedger.js 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/AI/待办助手.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/AI/生产助手.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/AI/老板助手.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/AI/财务助手.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/AI/采购助手.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/AI/销售助手.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/aiIndustrialBrain/reference-cards.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/aiIndustrialBrain/reference-chat.png 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/generalAssistant.js 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/index.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/purchaseAssistant.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/index.vue 1362 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PurchaseAIChatSidebar/index.vue 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 239 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/permission.js 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aiIndustrialBrain/components/AiAssistantWorkspace.vue 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aiIndustrialBrain/index.vue 1499 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue 852 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/expiryAfterSales/components/formDia.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/expiryAfterSales/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/feedbackRegistration/components/formDia.vue 895 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/operationManagement/index.vue 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/generalLedger/index.vue 527 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/dispatchLog/Record.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/receiptManagement/Record.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockReport/index.vue 1251 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/dimission/components/formDia.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/index.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionCosting/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/index.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionTraceability/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderEdit/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/index.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/productionPlan/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/deliveryLedger/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 3632 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/DZZBfavicon.ico
multiple/assets/favicon/KSfavicon.ico
multiple/assets/logo/DZZBLogo.png
multiple/assets/logo/KSLogo.png
multiple/config.json
@@ -24,6 +24,24 @@
    "logo": "logo/SDJCLogo.png",
    "favicon": "favicon/SDJCfavicon.ico"
  },
  "KS": {
    "env": {
      "VITE_APP_TITLE": "黎城康森商砼有限公司",
      "VITE_BASE_API": "http://36.138.236.176:9000",
      "VITE_JAVA_API": "http://36.138.236.176:9001"
    },
    "logo": "logo/KSLogo.png",
    "favicon": "favicon/KSfavicon.ico"
  },
  "DZZB": {
    "env": {
      "VITE_APP_TITLE": "山西丹朱装备制造股份有限公司",
      "VITE_BASE_API": "http://36.138.236.176:9000",
      "VITE_JAVA_API": "http://36.138.236.176:9001"
    },
    "logo": "logo/DZZBLogo.png",
    "favicon": "favicon/DZZBfavicon.ico"
  },
  "logo": "/src/assets/logo/logo.png",
  "favicon": "/public/favicon.ico"
}
}
package.json
@@ -56,5 +56,6 @@
  },
  "overrides": {
    "quill": "2.0.2"
  }
  },
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
src/api/financialManagement/accountSubject.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
import request from "@/utils/request";
// æŸ¥è¯¢æ€»å¸ç§‘目列表
export function listAccountSubject(query) {
  return request({
    url: "/accountSubject/list",
    method: "get",
    params: query,
  });
}
// æ–°å¢žæ€»å¸ç§‘ç›®
export function addAccountSubject(data) {
  return request({
    url: "/accountSubject/add",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹æ€»å¸ç§‘ç›®
export function updateAccountSubject(data) {
  return request({
    url: "/accountSubject/edit",
    method: "put",
    data: data,
  });
}
// åˆ é™¤æ€»å¸ç§‘ç›®
export function delAccountSubject(ids) {
  return request({
    url: "/accountSubject/remove/" + ids,
    method: "delete",
  });
}
// å¯¼å‡ºæ€»å¸ç§‘ç›®
export function exportAccountSubject(data) {
  return request({
    url: "/accountSubject/export",
    method: "post",
    data: data,
    responseType: "blob",
  });
}
src/api/salesManagement/deliveryLedger.js
@@ -17,6 +17,14 @@
    method: "get",
  });
}
// ä¿®æ”¹å‘货台账
export function getDeliveryDetailByShippingNo(query) {
  return request({
    url: "/shippingInfo/getDateilByShippingNo",
    method: "get",
    params: query,
  });
}
export function addOrUpdateDeliveryLedger(query) {
  return request({
@@ -51,4 +59,3 @@
    data,
  });
}
src/assets/AI/´ý°ìÖúÊÖ.png
src/assets/AI/Éú²úÖúÊÖ.png
src/assets/AI/ÀϰåÖúÊÖ.png
src/assets/AI/²ÆÎñÖúÊÖ.png
src/assets/AI/²É¹ºÖúÊÖ.png
src/assets/AI/ÏúÊÛÖúÊÖ.png
src/assets/aiIndustrialBrain/reference-cards.png
src/assets/aiIndustrialBrain/reference-chat.png
src/components/AIChatSidebar/assistants/generalAssistant.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
import { Cpu } from '@element-plus/icons-vue'
export const generalAssistant = {
  key: 'general',
  label: '待办助理',
  title: '待办智能助理',
  tooltip: '待办助手',
  icon: Cpu,
  apiBase: '/xiaozhi',
  storageKey: 'ai_chat_uuid',
  placeholder: '请输入您的问题... (Enter å‘送, Shift+Enter æ¢è¡Œ)',
  welcomeMessage: '你好',
  description: '我可以回答你的问题,为你提供业务数据解读信息、处理建议和辅助决策支持。',
  allowFileUpload: true,
  emptySessionText: '暂无历史会话',
  quickPrompts: [
    '我当前有哪些审批待办需要处理?',
    '帮我列出今天新增的审批待办。',
    '当前待我审批的单据,按时间倒序列出来。',
    '我发起的审批里,哪些还在处理中?',
    '查询流程编号 XXX çš„审批详情。',
    '流程编号 XXX çŽ°åœ¨å¡åœ¨å“ªä¸ªå®¡æ‰¹èŠ‚ç‚¹ï¼Ÿå½“å‰å®¡æ‰¹äººæ˜¯è°ï¼Ÿ',
    '帮我查看流程编号 XXX çš„审批流转记录。',
    '近7天我的审批待办统计情况怎么样?',
    '本月我的审批中,通过、驳回、处理中各有多少?',
    '近30天各类型审批数量分布是什么?',
    '帮我审批通过流程编号 XXX,备注“同意”。',
    '帮我驳回流程编号 XXX,备注“请补充说明”。',
    '撤销我刚刚对流程编号 XXX çš„审批操作。',
    '帮我修改流程编号 XXX çš„备注为“已补充附件”。',
    '删除我发起的流程编号 XXX。'
  ]
}
src/components/AIChatSidebar/assistants/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
import { generalAssistant } from './generalAssistant'
import { purchaseAssistant } from './purchaseAssistant'
export { generalAssistant, purchaseAssistant }
export const builtInAssistants = [generalAssistant, purchaseAssistant]
src/components/AIChatSidebar/assistants/purchaseAssistant.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
import { ShoppingCart } from '@element-plus/icons-vue'
export const purchaseAssistant = {
  key: 'purchase',
  label: '采购助理',
  title: '采购智能助理',
  tooltip: '采购智能助理',
  icon: ShoppingCart,
  apiBase: '/purchase-ai',
  storageKey: 'purchase_ai_chat_uuid',
  placeholder: '请输入采购问题... (Enter å‘送, Shift+Enter æ¢è¡Œ)',
  welcomeMessage: '你好',
  description: '我可以协助你分析采购订单、到货进度、供应商表现和付款情况,帮助你快速定位采购异常。',
  allowFileUpload: true,
  allowMultipleFileUpload: true,
  fileAnalyzeUrl: '/purchase-ai/analyze-files',
  emptySessionText: '暂无采购会话',
  quickPrompts: [
    '本月采购金额排名前十的物料有哪些?',
    '哪些采购订单还未入库?',
    '最近7天供应商到货异常有哪些?',
    '帮我统计待付款采购单',
    '列出本月采购退货情况'
  ]
}
src/components/AIChatSidebar/index.vue
@@ -1,7 +1,7 @@
<template>
  <div class="ai-chat-sidebar-wrapper">
    <!-- æ‚¬æµ®å›¾æ ‡ -->
    <div class="ai-chat-trigger" @click="toggleSidebar" v-show="!visible">
    <div v-if="!hideTrigger" class="ai-chat-trigger" @click="toggleSidebar" v-show="!visible">
      <el-tooltip :content="currentAssistant.tooltip" placement="left">
        <div class="trigger-icon">
          <el-icon :size="30" color="#fff"><component :is="currentAssistant.icon" /></el-icon>
@@ -12,14 +12,16 @@
    <!-- ä¾§è¾¹æ å¯¹è¯æ¡† -->
    <el-drawer
        v-model="visible"
        :size="drawerSize"
        direction="rtl"
        :size="computedDrawerSize"
        :direction="drawerDirection"
        :with-header="true"
        class="ai-chat-drawer"
        :modal="false"
        modal-class="ai-chat-overlay"
        :show-close="false"
        :append-to-body="false"
        :close-on-press-escape="!hideTrigger"
        :close-on-click-modal="!hideTrigger"
        @close="handleClose"
    >
      <template #header>
@@ -50,8 +52,16 @@
                <el-icon :size="18"><Plus /></el-icon>
              </el-button>
            </el-tooltip>
            <div class="action-divider"></div>
            <el-tooltip content="关闭" placement="bottom">
            <el-button
                v-if="headerExtraActionText"
                link
                class="header-action-btn header-action-btn--text"
                @click="handleHeaderExtraAction"
            >
              {{ headerExtraActionText }}
            </el-button>
            <div v-if="!hideTrigger" class="action-divider"></div>
            <el-tooltip v-if="!hideTrigger" content="关闭" placement="bottom">
              <el-button link class="header-action-btn close-btn" @click="handleManualClose">
                <el-icon :size="18"><Close /></el-icon>
              </el-button>
@@ -105,23 +115,17 @@
              <div class="assistant-scan-ring"></div>
              <div class="assistant-orbit assistant-orbit-a"></div>
              <div class="assistant-orbit assistant-orbit-b"></div>
              <div class="assistant-bot">
                <div class="assistant-bot-antenna assistant-bot-antenna-left"></div>
                <div class="assistant-bot-antenna assistant-bot-antenna-right"></div>
                <div class="assistant-bot-head">
                  <div class="assistant-bot-head-glow"></div>
                  <div class="assistant-bot-eye assistant-bot-eye-left"></div>
                  <div class="assistant-bot-eye assistant-bot-eye-right"></div>
                  <div class="assistant-bot-mouth"></div>
                </div>
                <div class="assistant-bot-neck"></div>
                <div class="assistant-bot-body">
                  <div class="assistant-bot-core">
                    <div class="assistant-bot-core-ring"></div>
                    <el-icon :size="22"><component :is="currentAssistant.icon" /></el-icon>
              <div class="assistant-model-shell">
                <div class="assistant-model-cut">
                  <img
                      v-if="currentAssistantAvatar"
                      class="assistant-model-img"
                      :src="currentAssistantAvatar"
                      :alt="currentAssistant.label"
                  />
                  <div v-else class="assistant-model-fallback">
                    <el-icon :size="30"><component :is="currentAssistant.icon" /></el-icon>
                  </div>
                  <div class="assistant-bot-arm assistant-bot-arm-left"></div>
                  <div class="assistant-bot-arm assistant-bot-arm-right"></div>
                </div>
              </div>
              <div class="assistant-status">
@@ -169,10 +173,6 @@
            </div>
          </div>
          <div v-show="!hasMessages" class="hero-dot-grid" aria-hidden="true">
            <span v-for="dot in 28" :key="dot"></span>
          </div>
          <div class="message-list" ref="messageListRef">
            <div
                v-for="(message, index) in messages"
@@ -186,6 +186,37 @@
              <div class="message-content">
                <!-- æ–‡æœ¬å†…容 -->
                <div class="text-box" v-html="message.htmlContent"></div>
                <div v-if="message.localUploadFiles?.length" class="message-local-file-list">
                  <div
                      v-for="(file, fileIndex) in message.localUploadFiles"
                      :key="`${file.previewId || file.name}-${fileIndex}`"
                      :class="['message-local-file-item', { clickable: !!file.accessUrl && !file.isImage }]"
                      @click="handleMessageFileClick(file)"
                  >
                    <el-image
                        v-if="file.isImage && file.previewUrl"
                        :src="file.previewUrl"
                        :preview-src-list="getImagePreviewList(message.localUploadFiles)"
                        :initial-index="getImagePreviewInitialIndex(message.localUploadFiles, file.previewUrl)"
                        :z-index="4000"
                        preview-teleported
                        fit="cover"
                        class="message-local-file-thumb"
                    />
                    <el-icon v-else class="message-local-file-icon"><Document /></el-icon>
                    <div class="message-local-file-meta">
                      <span
                          :class="['message-local-file-name', { clickable: !!file.accessUrl }]"
                          :title="file.name"
                          @click.stop="openMessageAttachment(file)"
                      >
                        {{ file.name }}
                      </span>
                      <small v-if="Number(file.size) > 0" class="message-local-file-size">{{ formatFileSize(file.size) }}</small>
                    </div>
                  </div>
                </div>
                <!-- å›¾è¡¨å†…容 -->
                <div v-if="message.chartOptions && message.chartRenderReady" class="charts-wrapper">
@@ -443,9 +474,22 @@
            </div>
            <div class="input-box">
              <div v-if="selectedFiles.length" class="selected-file-list">
                <div v-for="(file, fileIndex) in selectedFiles" :key="`${file.name}-${fileIndex}`" class="selected-file-tag">
                  <el-icon><Document /></el-icon>
                  <span class="file-name">{{ file.name }}</span>
                <div v-for="(file, fileIndex) in selectedFileSnapshots" :key="`${file.previewId || file.name}-${fileIndex}`" class="selected-file-tag">
                  <el-image
                      v-if="file.isImage && file.previewUrl"
                      :src="file.previewUrl"
                      :preview-src-list="getImagePreviewList(selectedFileSnapshots)"
                      :initial-index="getImagePreviewInitialIndex(selectedFileSnapshots, file.previewUrl)"
                      :z-index="4000"
                      preview-teleported
                      fit="cover"
                      class="selected-file-thumb"
                  />
                  <el-icon v-else><Document /></el-icon>
                  <div class="selected-file-meta">
                    <span class="file-name">{{ file.name }}</span>
                    <small class="file-size">{{ formatFileSize(file.size) }}</small>
                  </div>
                  <el-icon class="remove-file" @click="removeSelectedFile(fileIndex)"><Close /></el-icon>
                </div>
              </div>
@@ -478,8 +522,17 @@
import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue'
import request from '@/utils/request'
import * as echarts from 'echarts'
import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, ShoppingCart, Promotion, RefreshRight } from '@element-plus/icons-vue'
import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, Promotion, RefreshRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { builtInAssistants, generalAssistant } from './assistants'
import todoAssistantAvatar from '@/assets/AI/待办助手.png'
import salesAssistantAvatar from '@/assets/AI/销售助手.png'
import purchaseAssistantAvatar from '@/assets/AI/采购助手.png'
import productionAssistantAvatar from '@/assets/AI/生产助手.png'
import financeAssistantAvatar from '@/assets/AI/财务助手.png'
import bossAssistantAvatar from '@/assets/AI/老板助手.png'
const emit = defineEmits(['header-extra-action'])
const props = defineProps({
  assistants: {
@@ -489,72 +542,51 @@
  defaultAssistant: {
    type: String,
    default: ''
  },
  hideTrigger: {
    type: Boolean,
    default: false
  },
  autoOpen: {
    type: Boolean,
    default: false
  },
  drawerSize: {
    type: [String, Number],
    default: ''
  },
  drawerDirection: {
    type: String,
    default: 'rtl'
  },
  headerExtraActionText: {
    type: String,
    default: ''
  }
})
const builtInAssistants = [
  {
    key: 'general',
    label: '待办助理',
    title: '待办智能助理',
    tooltip: '待办助手',
    icon: Cpu,
    apiBase: '/xiaozhi',
    storageKey: 'ai_chat_uuid',
    placeholder: '请输入您的问题... (Enter å‘送, Shift+Enter æ¢è¡Œ)',
    welcomeMessage: '你好',
    description: '我可以回答你的问题,为你提供业务数据解读信息、处理建议和辅助决策支持。',
    allowFileUpload: true,
    emptySessionText: '暂无历史会话'
  },
  {
    key: 'purchase',
    label: '采购助理',
    title: '采购智能助理',
    tooltip: '采购智能助理',
    icon: ShoppingCart,
    apiBase: '/purchase-ai',
    storageKey: 'purchase_ai_chat_uuid',
    placeholder: '请输入采购问题... (Enter å‘送, Shift+Enter æ¢è¡Œ)',
    welcomeMessage: '你好',
    description: '我可以协助你分析采购订单、到货进度、供应商表现和付款情况,帮助你快速定位采购异常。',
    allowFileUpload: true,
    allowMultipleFileUpload: true,
    fileAnalyzeUrl: '/purchase-ai/analyze-files',
    emptySessionText: '暂无采购会话'
  }
]
const hideTrigger = computed(() => props.hideTrigger)
const headerExtraActionText = computed(() => String(props.headerExtraActionText || '').trim())
const drawerDirection = computed(() => (props.drawerDirection === 'ttb' || props.drawerDirection === 'btt' || props.drawerDirection === 'ltr' || props.drawerDirection === 'rtl')
  ? props.drawerDirection
  : 'rtl')
const assistants = computed(() => props.assistants?.length ? props.assistants : builtInAssistants)
const selectedAssistantKey = ref(props.defaultAssistant || assistants.value[0]?.key || 'general')
const currentAssistant = computed(() => assistants.value.find(item => item.key === selectedAssistantKey.value) || assistants.value[0] || builtInAssistants[0])
const showAssistantSwitch = computed(() => assistants.value.length > 1)
const assistantQuickPromptMap = {
  general: [
    '我当前有哪些审批待办需要处理?',
    '帮我列出今天新增的审批待办。',
    '当前待我审批的单据,按时间倒序列出来。',
    '我发起的审批里,哪些还在处理中?',
    '查询流程编号 XXX çš„审批详情。',
    '流程编号 XXX çŽ°åœ¨å¡åœ¨å“ªä¸ªå®¡æ‰¹èŠ‚ç‚¹ï¼Ÿå½“å‰å®¡æ‰¹äººæ˜¯è°ï¼Ÿ',
    '帮我查看流程编号 XXX çš„审批流转记录。',
    '近7天我的审批待办统计情况怎么样?',
    '本月我的审批中,通过、驳回、处理中各有多少?',
    '近30天各类型审批数量分布是什么?',
    '帮我审批通过流程编号 XXX,备注“同意”。',
    '帮我驳回流程编号 XXX,备注“请补充说明”。',
    '撤销我刚刚对流程编号 XXX çš„审批操作。',
    '帮我修改流程编号 XXX çš„备注为“已补充附件”。',
    '删除我发起的流程编号 XXX。'
  ],
  purchase: [
    '本月采购金额排名前十的物料有哪些?',
    '哪些采购订单还未入库?',
    '最近7天供应商到货异常有哪些?',
    '帮我统计待付款采购单',
    '列出本月采购退货情况'
  ]
const assistantAvatarByKey = {
  general: todoAssistantAvatar,
  todo: todoAssistantAvatar,
  purchase: purchaseAssistantAvatar,
  sales: salesAssistantAvatar,
  production: productionAssistantAvatar,
  finance: financeAssistantAvatar,
  boss: bossAssistantAvatar
}
const currentAssistantAvatar = computed(() => {
  const assistant = currentAssistant.value || {}
  return assistant.avatar || assistantAvatarByKey[assistant.key] || ''
})
const showAssistantSwitch = computed(() => assistants.value.length > 1)
const quickPromptLimit = 3
const quickPromptStart = ref(0)
const quickPrompts = computed(() => {
@@ -562,7 +594,7 @@
  if (Array.isArray(assistant.quickPrompts) && assistant.quickPrompts.length) {
    return assistant.quickPrompts
  }
  return assistantQuickPromptMap[assistant.key] || assistantQuickPromptMap.general
  return generalAssistant.quickPrompts || []
})
const displayedQuickPrompts = computed(() => {
  const prompts = quickPrompts.value || []
@@ -578,17 +610,19 @@
const visible = ref(false)
const windowWidth = ref(window.innerWidth)
const drawerSize = computed(() => {
const responsiveDrawerSize = computed(() => {
  if (windowWidth.value < 768) return '100%'
  if (windowWidth.value < 1200) return '50%'
  return '50%'
})
const computedDrawerSize = computed(() => props.drawerSize || responsiveDrawerSize.value)
const messageListRef = ref(null)
const isSending = ref(false)
const currentAbortController = ref(null)
const inputMessage = ref('')
const selectedFiles = ref([])
const uploadFileList = ref([])
const selectedFileSnapshots = ref([])
const messages = ref([])
const uuid = ref('')
const chartInstances = ref({})
@@ -792,6 +826,142 @@
const sessions = ref([])
const loadingSessions = ref(false)
const isImageFileType = (fileType = '') => String(fileType || '').toLowerCase().startsWith('image/')
const imageFilePathPattern = /\.(png|jpe?g|gif|webp|bmp|svg)$/i
const getPathnameFromFilePath = (filePath = '') => {
  const rawPath = String(filePath || '').trim()
  if (!rawPath) return ''
  try {
    const baseOrigin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
    return new URL(rawPath, baseOrigin).pathname || ''
  } catch (err) {
    return rawPath.split('?')[0]
  }
}
const isImageFilePath = (filePath = '') => {
  const pathname = getPathnameFromFilePath(filePath).toLowerCase()
  return imageFilePathPattern.test(pathname)
}
const getHistoryFileName = (filePath = '', index = 0) => {
  const pathname = getPathnameFromFilePath(filePath)
  const fileName = pathname.split('/').filter(Boolean).pop()
  if (!fileName) return `file-${index + 1}`
  try {
    return decodeURIComponent(fileName)
  } catch (err) {
    return fileName
  }
}
const getImagePreviewList = (files = []) => {
  if (!Array.isArray(files)) return []
  return files
    .filter(item => item?.isImage && item?.previewUrl)
    .map(item => item.previewUrl)
}
const getImagePreviewInitialIndex = (files = [], previewUrl = '') => {
  const list = getImagePreviewList(files)
  const index = list.indexOf(previewUrl)
  return index >= 0 ? index : 0
}
const formatFileSize = (size) => {
  const bytes = Number(size)
  if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1).replace(/\.0$/, '')} KB`
  return `${(bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, '')} MB`
}
const createLocalFileSnapshot = (file, index = 0) => {
  const rawFile = typeof File !== 'undefined' && file instanceof File ? file : null
  const fileType = rawFile?.type || ''
  const isImage = isImageFileType(fileType)
  const canCreateObjectURL = typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function'
  const previewUrl = isImage && rawFile && canCreateObjectURL ? URL.createObjectURL(rawFile) : ''
  return {
    previewId: `${rawFile?.name || 'file'}-${rawFile?.size || 0}-${rawFile?.lastModified || Date.now()}-${index}`,
    name: rawFile?.name || `file-${index + 1}`,
    size: rawFile?.size || 0,
    type: fileType,
    isImage,
    previewUrl,
    accessUrl: '',
    rawFile,
    isObjectUrl: !!previewUrl
  }
}
const createHistoryFileSnapshot = (filePath, memoryId = '', messageIndex = 0, fileIndex = 0) => {
  const normalizedPath = String(filePath || '').trim()
  if (!normalizedPath) return null
  const isImage = isImageFilePath(normalizedPath)
  return {
    previewId: `${memoryId || 'history'}-${messageIndex}-${fileIndex}`,
    name: getHistoryFileName(normalizedPath, fileIndex),
    size: 0,
    type: '',
    isImage,
    previewUrl: isImage ? normalizedPath : '',
    accessUrl: normalizedPath,
    rawFile: null,
    isObjectUrl: false
  }
}
const revokeLocalFileSnapshots = (snapshots = []) => {
  const canRevokeObjectURL = typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function'
  if (!canRevokeObjectURL) return
  if (!Array.isArray(snapshots)) return
  snapshots.forEach((snapshot) => {
    if (snapshot?.isObjectUrl && snapshot?.previewUrl) {
      URL.revokeObjectURL(snapshot.previewUrl)
    }
  })
}
const mapHistoryFilePathsToSnapshots = (filePaths = [], memoryId = '', messageIndex = 0) => {
  if (!Array.isArray(filePaths)) return []
  return filePaths
    .map((filePath, fileIndex) => createHistoryFileSnapshot(filePath, memoryId, messageIndex, fileIndex))
    .filter(Boolean)
}
const openMessageAttachment = (file) => {
  const accessUrl = String(file?.accessUrl || '').trim()
  if (!accessUrl) return
  if (typeof window === 'undefined' || typeof window.open !== 'function') return
  window.open(accessUrl, '_blank', 'noopener,noreferrer')
}
const handleMessageFileClick = (file) => {
  if (!file?.accessUrl || file?.isImage) return
  openMessageAttachment(file)
}
const revokeMessageLocalFileSnapshots = (messageList = []) => {
  if (!Array.isArray(messageList)) return
  messageList.forEach((msg) => {
    if (Array.isArray(msg?.localUploadFiles)) {
      revokeLocalFileSnapshots(msg.localUploadFiles)
      msg.localUploadFiles = []
    }
  })
}
const clearSelectedFiles = ({ releaseSnapshots = true } = {}) => {
  if (releaseSnapshots) {
    revokeLocalFileSnapshots(selectedFileSnapshots.value)
  }
  selectedFiles.value = []
  uploadFileList.value = []
  selectedFileSnapshots.value = []
}
const abortCurrentRequest = () => {
  if (!currentAbortController.value) return
@@ -835,6 +1005,7 @@
const selectSession = async (session) => {
  showHistory.value = false
  clearSelectedFiles()
  uuid.value = session.memoryId
  localStorage.setItem(currentAssistant.value.storageKey, uuid.value)
@@ -842,6 +1013,7 @@
  try {
    const res = await request.get(`${currentAssistant.value.apiBase}/history/messages/${uuid.value}`)
    if (res.code === 200) {
      revokeMessageLocalFileSnapshots(messages.value)
      disposeCharts()
      messages.value = []
      const historyMsgs = res.data || []
@@ -853,7 +1025,7 @@
        const messageObj = {
          isUser,
          content: msg.content,
          content: msg.content || '',
          htmlContent: '',
          isTyping: false,
          chartOptions: null,
@@ -861,7 +1033,8 @@
          type: '',
          tableData: null,
          payloadTreeData: null,
          payloadHiddenData: null
          payloadHiddenData: null,
          localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
        }
        messages.value.push(messageObj)
@@ -876,15 +1049,15 @@
          }
          // è§£æžåŽ†å²æ¶ˆæ¯ä¸­çš„ JSON
          const extracted = extractEmbeddedSuccessJson(msg.content)
          const extracted = extractEmbeddedSuccessJson(msg.content || '')
          if (extracted) {
            applyStructuredMessageData(messageObj, extracted.data, botMsgIndex)
          }
          updateOutputState(msg.content, botMsgIndex)
          messageObj.htmlContent = convertStreamOutput(msg.content, botMsgIndex)
          updateOutputState(msg.content || '', botMsgIndex)
          messageObj.htmlContent = convertStreamOutput(msg.content || '', botMsgIndex)
        } else {
          messageObj.htmlContent = convertTextToHtml(msg.content)
          messageObj.htmlContent = convertTextToHtml(msg.content || '')
        }
      })
      scrollToBottom()
@@ -919,15 +1092,16 @@
}
onMounted(() => {
  initUUID()
  // åˆå§‹æ¬¢è¿Ž
  if (messages.value.length === 0) {
    hello()
  if (props.autoOpen) {
    visible.value = true
  }
  initUUID()
  window.addEventListener('resize', handleWindowResize)
})
onUnmounted(() => {
  revokeMessageLocalFileSnapshots(messages.value)
  clearSelectedFiles()
  disposeCharts()
  window.removeEventListener('resize', handleWindowResize)
})
@@ -936,21 +1110,36 @@
  if (!prevKey || nextKey === prevKey) return
  abortCurrentRequest()
  revokeMessageLocalFileSnapshots(messages.value)
  disposeCharts()
  messages.value = []
  outputState.value = {}
  sessions.value = []
  showHistory.value = false
  selectedFiles.value = []
  uploadFileList.value = []
  clearSelectedFiles()
  inputMessage.value = ''
  quickPromptStart.value = 0
  initUUID()
  hello()
})
watch(() => props.defaultAssistant, (nextKey) => {
  if (!nextKey || nextKey === selectedAssistantKey.value) return
  if (!assistants.value.some(item => item.key === nextKey)) return
  selectedAssistantKey.value = nextKey
})
watch(() => props.autoOpen, (nextValue) => {
  if (nextValue) {
    visible.value = true
  }
})
const handleWindowResize = () => {
  windowWidth.value = window.innerWidth
}
const handleHeaderExtraAction = () => {
  emit('header-extra-action')
}
const toggleSidebar = () => {
@@ -961,10 +1150,12 @@
}
const handleClose = () => {
  if (hideTrigger.value) return
  visible.value = false
}
const handleManualClose = () => {
  if (hideTrigger.value) return
  if (isSending.value) {
    abortCurrentRequest()
  }
@@ -980,22 +1171,17 @@
  uuid.value = storedUUID
}
const hello = () => {
  sendRequest(currentAssistant.value.welcomeMessage || '你好')
}
const newChat = () => {
  revokeMessageLocalFileSnapshots(messages.value)
  disposeCharts()
  messages.value = []
  outputState.value = {}
  sessions.value = []
  showHistory.value = false
  selectedFiles.value = []
  uploadFileList.value = []
  clearSelectedFiles()
  quickPromptStart.value = 0
  localStorage.removeItem(currentAssistant.value.storageKey)
  initUUID()
  hello()
}
const handleNewChat = () => {
@@ -1667,6 +1853,76 @@
  '审批用户ID列表'
])
const purchaseIntegerFieldKeys = new Set([
  'id',
  'supplierId',
  'recorderId',
  'salesContractNoId',
  'salesLedgerId',
  'Type',
  'businessPersonId',
  'productId',
  'productModelId',
  'ticketRegistrationId',
  'type',
  'approvalStatus',
  'inventoryWarningQuantity'
])
const purchaseDecimalFieldKeys = new Set([
  'invoiceAmount',
  'contractAmount',
  'receiptPaymentAmount',
  'unReceiptPaymentAmount',
  'quantity',
  'taxRate',
  'taxInclusiveUnitPrice',
  'taxInclusiveTotalPrice',
  'taxExclusiveTotalPrice',
  'priceWithTax',
  'totalPriceWithTax'
])
const purchaseBooleanFieldKeys = new Set([
  'hasChildren',
  'isWhite',
  'isInspected',
  'isChecked'
])
const purchaseStringFieldKeys = new Set([
  'entryDateStart',
  'entryDateEnd',
  'purchaseContractNumber',
  'supplierName',
  'recorderName',
  'salesContractNo',
  'projectName',
  'entryDate',
  'executionDate',
  'remarks',
  'attachmentMaterials',
  'createdAt',
  'updatedAt',
  'phoneNumber',
  'invoiceNumber',
  'paymentMethod',
  'templateName',
  'productCategory',
  'specificationModel',
  'unit',
  'invoiceType'
])
const purchaseGenericArrayFieldKeys = new Set([
  'purchaseLedgers',
  'productData'
])
const purchaseStringArrayFieldKeys = new Set(['tempFileIds'])
const purchaseObjectArrayFieldKeys = new Set(['SalesLedgerFiles'])
const normalizePurchaseProductRecord = (record) => {
  if (!record || typeof record !== 'object' || Array.isArray(record)) return record
  return mapPayloadKeys(record, purchasePayloadFieldKeyMap)
@@ -1746,10 +2002,191 @@
  }, {})
}
const normalizeAttachmentMaterialsValue = (value) => {
  if (value === null || value === undefined) return value
  if (typeof value === 'string') return value
  if (typeof value === 'number' || typeof value === 'boolean') return String(value)
  try {
    return JSON.stringify(value)
  } catch (err) {
    return String(value)
  }
}
const normalizePurchaseAttachmentMaterialsField = (value) => {
  if (Array.isArray(value)) {
    return value.map(item => normalizePurchaseAttachmentMaterialsField(item))
  }
  if (value && typeof value === 'object') {
    return Object.entries(value).reduce((result, [key, item]) => {
      if (key === 'attachmentMaterials') {
        result[key] = normalizeAttachmentMaterialsValue(item)
      } else {
        result[key] = normalizePurchaseAttachmentMaterialsField(item)
      }
      return result
    }, {})
  }
  return value
}
const normalizePurchaseNumericText = (text = '') => {
  return String(text)
    .replace(/[,\s,]/g, '')
    .replace(/[¥¥元%]/g, '')
}
const parsePurchaseNumberValue = (value) => {
  if (typeof value === 'number') {
    return Number.isFinite(value) ? value : null
  }
  if (typeof value === 'boolean') {
    return value ? 1 : 0
  }
  if (typeof value !== 'string') {
    return null
  }
  const text = normalizePurchaseNumericText(value.trim())
  if (!text) return null
  if (!/^[-+]?\d+(\.\d+)?$/.test(text)) return null
  const numberValue = Number(text)
  return Number.isFinite(numberValue) ? numberValue : null
}
const normalizePurchaseIntegerFieldValue = (value) => {
  if (value === null || value === undefined) return value
  if (value === '') return null
  const numberValue = parsePurchaseNumberValue(value)
  if (numberValue === null) return null
  return Math.trunc(numberValue)
}
const normalizePurchaseDecimalFieldValue = (value) => {
  if (value === null || value === undefined) return value
  if (value === '') return null
  const numberValue = parsePurchaseNumberValue(value)
  return numberValue === null ? null : numberValue
}
const normalizePurchaseBooleanFieldValue = (value) => {
  if (value === null || value === undefined) return value
  if (value === '') return null
  if (typeof value === 'boolean') return value
  if (typeof value === 'number') return value !== 0
  if (typeof value !== 'string') return null
  const text = value.trim().toLowerCase()
  if (!text) return null
  if (['true', '1', 'yes', 'y', '是', 'å·²', 'checked'].includes(text)) return true
  if (['false', '0', 'no', 'n', '否', '未', 'unchecked'].includes(text)) return false
  return null
}
const normalizePurchaseStringFieldValue = (value) => {
  if (value === null || value === undefined) return value
  if (typeof value === 'string') return value
  if (typeof value === 'number' || typeof value === 'boolean') return String(value)
  try {
    return JSON.stringify(value)
  } catch (err) {
    return String(value)
  }
}
const normalizePurchaseStringArrayFieldValue = (value) => {
  if (Array.isArray(value)) {
    return value
      .map(item => normalizePurchaseStringFieldValue(item))
      .filter(item => item !== null && item !== undefined && item !== '')
  }
  if (value === null || value === undefined || value === '') return []
  if (typeof value === 'string') {
    const text = value.trim()
    if (!text) return []
    if (/^\[.*\]$/.test(text)) {
      try {
        const parsedValue = JSON.parse(text)
        if (Array.isArray(parsedValue)) {
          return parsedValue
            .map(item => normalizePurchaseStringFieldValue(item))
            .filter(item => item !== null && item !== undefined && item !== '')
        }
      } catch (err) {
        // Keep as plain text when not valid JSON array.
      }
    }
    const splitValues = text
      .split(/[,\n,]/)
      .map(item => item.trim())
      .filter(Boolean)
    return splitValues.length > 1 ? splitValues : [text]
  }
  const normalizedValue = normalizePurchaseStringFieldValue(value)
  return normalizedValue === null || normalizedValue === undefined || normalizedValue === ''
    ? []
    : [normalizedValue]
}
const normalizePurchaseObjectArrayFieldValue = (value) => {
  if (Array.isArray(value)) {
    return value.filter(item => item && typeof item === 'object')
  }
  if (value && typeof value === 'object') {
    return [value]
  }
  return []
}
const normalizePurchaseValueByFieldKey = (fieldKey, value) => {
  if (fieldKey === 'attachmentMaterials') return normalizeAttachmentMaterialsValue(value)
  if (purchaseIntegerFieldKeys.has(fieldKey)) return normalizePurchaseIntegerFieldValue(value)
  if (purchaseDecimalFieldKeys.has(fieldKey)) return normalizePurchaseDecimalFieldValue(value)
  if (purchaseBooleanFieldKeys.has(fieldKey)) return normalizePurchaseBooleanFieldValue(value)
  if (purchaseStringArrayFieldKeys.has(fieldKey)) return normalizePurchaseStringArrayFieldValue(value)
  if (purchaseObjectArrayFieldKeys.has(fieldKey)) return normalizePurchaseObjectArrayFieldValue(value)
  if (purchaseStringFieldKeys.has(fieldKey)) return normalizePurchaseStringFieldValue(value)
  return value
}
const normalizePurchasePayloadFieldTypes = (value, fieldKey = '') => {
  if (purchaseGenericArrayFieldKeys.has(fieldKey)) {
    if (Array.isArray(value)) {
      return value.map(item => normalizePurchasePayloadFieldTypes(item))
    }
    if (value && typeof value === 'object') {
      return [normalizePurchasePayloadFieldTypes(value)]
    }
    return []
  }
  if (purchaseStringArrayFieldKeys.has(fieldKey)) {
    return normalizePurchaseStringArrayFieldValue(value)
  }
  if (purchaseObjectArrayFieldKeys.has(fieldKey)) {
    return normalizePurchaseObjectArrayFieldValue(value)
  }
  if (Array.isArray(value)) {
    return value.map(item => normalizePurchasePayloadFieldTypes(item))
  }
  if (value && typeof value === 'object') {
    return Object.entries(value).reduce((result, [key, item]) => {
      result[key] = normalizePurchasePayloadFieldTypes(item, key)
      return result
    }, {})
  }
  return normalizePurchaseValueByFieldKey(fieldKey, value)
}
const sanitizePurchasePayloadForSubmit = (payload, businessType) => {
  if (businessType !== 'purchase_ledger' || !payload || typeof payload !== 'object') return payload
  const sanitized = mergeLegacyProductDataIntoLedgers(Array.isArray(payload) ? [...payload] : { ...payload })
  let sanitized = mergeLegacyProductDataIntoLedgers(Array.isArray(payload) ? [...payload] : { ...payload })
  sanitized = normalizePurchaseAttachmentMaterialsField(sanitized)
  sanitized = normalizePurchasePayloadFieldTypes(sanitized)
  if (Array.isArray(sanitized.purchaseLedgers)) {
    sanitized.purchaseLedgers = sanitized.purchaseLedgers.map(filterPurchaseLedgerRecord)
  }
@@ -1954,16 +2391,20 @@
    return isLt10M
  })
  clearSelectedFiles()
  selectedFiles.value = validFiles
  uploadFileList.value = fileList.filter(item => item.raw && validFiles.includes(item.raw))
  selectedFileSnapshots.value = validFiles.map((rawFile, index) => createLocalFileSnapshot(rawFile, index))
}
const removeSelectedFile = (index) => {
  const [removedSnapshot] = selectedFileSnapshots.value.splice(index, 1)
  revokeLocalFileSnapshots(removedSnapshot ? [removedSnapshot] : [])
  selectedFiles.value.splice(index, 1)
  uploadFileList.value.splice(index, 1)
}
const analyzeFiles = async (files, message = '') => {
const analyzeFiles = async (files, message = '', localFileSnapshots = []) => {
  const uploadFiles = Array.isArray(files) ? files : [files].filter(Boolean)
  if (!uploadFiles.length) return
  if (isSending.value) return
@@ -1976,7 +2417,8 @@
    isUser: true,
    content: userMsg,
    htmlContent: convertTextToHtml(userMsg),
    isTyping: false
    isTyping: false,
    localUploadFiles: Array.isArray(localFileSnapshots) ? localFileSnapshots : []
  })
  const botMsgIndex = messages.value.length
@@ -2075,9 +2517,9 @@
  const msg = inputMessage.value?.trim() || ''
  if ((msg || selectedFiles.value.length) && !isSending.value) {
    if (selectedFiles.value.length) {
      analyzeFiles([...selectedFiles.value], msg)
      selectedFiles.value = []
      uploadFileList.value = []
      const localFileSnapshots = selectedFileSnapshots.value
      analyzeFiles([...selectedFiles.value], msg, localFileSnapshots)
      clearSelectedFiles({ releaseSnapshots: false })
    } else {
      sendRequest(msg)
    }
@@ -2659,8 +3101,9 @@
    height: 100%;
  }
  :deep(.el-drawer__header) {
    margin-bottom: 0;
    padding: 0;
    margin-bottom: 0 !important;
    padding: 0 !important;
    border-bottom: 1px solid rgba(255, 255, 255, 0.12);
    background: $gradient-dark;
    color: #fff;
  }
@@ -2671,7 +3114,7 @@
  justify-content: space-between;
  align-items: center;
  width: 100%;
  padding: 18px 20px;
  padding: 12px 18px;
  background: $gradient-dark;
  position: relative;
  overflow: hidden;
@@ -2803,6 +3246,15 @@
        opacity: 1;
      }
    }
    :deep(.header-action-btn--text) {
      width: auto !important;
      min-width: 104px;
      padding: 8px 14px !important;
      font-size: 14px;
      font-weight: 600;
      white-space: nowrap;
    }
  }
  .assistant-switcher {
@@ -2885,7 +3337,7 @@
    top: 0;
    left: 0;
    right: 0;
    height: 240px;
    height: 128px;
    background: linear-gradient(180deg, rgba(0, 85, 212, 0.06) 0%, transparent 100%);
    pointer-events: none;
  }
@@ -3124,6 +3576,87 @@
        background: rgba(0, 85, 212, 0.25);
        border-radius: 2px;
      }
    }
    .message-local-file-list {
      margin-top: 8px;
      display: grid;
      gap: 8px;
      max-width: 100%;
    }
    .message-local-file-item {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 8px 10px;
      border-radius: 10px;
      border: 1px solid rgba(88, 117, 255, 0.2);
      background: rgba(255, 255, 255, 0.9);
      max-width: 100%;
      &.clickable {
        cursor: pointer;
        transition: all 0.2s ease;
        &:hover {
          border-color: rgba(44, 109, 255, 0.38);
          background: rgba(243, 247, 255, 0.96);
        }
      }
    }
    .message-local-file-thumb {
      width: 40px;
      height: 40px;
      border-radius: 6px;
      overflow: hidden;
      flex-shrink: 0;
      border: 1px solid rgba(124, 148, 255, 0.26);
      background: #f4f7ff;
      cursor: zoom-in;
      :deep(.el-image__inner) {
        width: 100%;
        height: 100%;
      }
    }
    .message-local-file-icon {
      font-size: 20px;
      color: $primary-blue;
      flex-shrink: 0;
    }
    .message-local-file-meta {
      min-width: 0;
      display: flex;
      flex-direction: column;
      gap: 2px;
    }
    .message-local-file-name {
      font-size: 12px;
      color: #1f2a44;
      font-weight: 600;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      &.clickable {
        color: $primary-blue;
        cursor: pointer;
        &:hover {
          text-decoration: underline;
        }
      }
    }
    .message-local-file-size {
      font-size: 11px;
      color: #7f8ba1;
      line-height: 1.2;
    }
  }
@@ -3537,6 +4070,28 @@
        font-size: 18px;
      }
      .selected-file-thumb {
        width: 30px;
        height: 30px;
        border-radius: 6px;
        overflow: hidden;
        border: 1px solid rgba(0, 85, 212, 0.2);
        flex-shrink: 0;
        cursor: zoom-in;
        :deep(.el-image__inner) {
          width: 100%;
          height: 100%;
        }
      }
      .selected-file-meta {
        min-width: 0;
        display: flex;
        flex-direction: column;
        gap: 2px;
      }
      .file-name {
        font-size: 13px;
        color: $deep-blue;
@@ -3544,6 +4099,12 @@
        overflow: hidden;
        text-overflow: ellipsis;
        font-weight: 600;
      }
      .file-size {
        font-size: 11px;
        color: #5f86b4;
        line-height: 1.1;
      }
      .remove-file {
@@ -3703,75 +4264,59 @@
.chat-hero {
  display: grid;
  grid-template-columns: 164px minmax(0, 1fr);
  gap: 18px;
  align-items: start;
  padding: 14px 18px 6px;
  grid-template-columns: 176px minmax(0, 1fr);
  gap: 14px;
  align-items: stretch;
  padding: 8px 18px 4px;
  &.compact {
    grid-template-columns: 122px minmax(0, 1fr);
    gap: 12px;
    padding: 8px 18px 2px;
    grid-template-columns: 132px minmax(0, 1fr);
    gap: 10px;
    padding: 4px 18px 2px;
  }
}
.assistant-stand {
  position: relative;
  min-height: 252px;
  min-height: 206px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  padding-top: 18px;
  padding-top: 8px;
  overflow: hidden;
  &.compact {
    min-height: 176px;
    padding-top: 8px;
    min-height: 160px;
    padding-top: 4px;
  }
  &.thinking {
    .assistant-halo {
      opacity: 1;
      transform: scale(1.08);
      filter: blur(8px);
      transform: scale(1.12);
      filter: blur(9px);
    }
    .assistant-scan-ring {
      opacity: 1;
      animation-duration: 1.6s;
      opacity: 0.95;
      animation-duration: 1.5s;
    }
    .assistant-orbit {
      opacity: 1;
      opacity: 0.76;
    }
    .assistant-bot {
      transform: translateY(-4px) scale(1.02);
    .assistant-model-shell {
      transform: translateY(-5px) scale(1.02);
    }
    .assistant-bot-head {
      box-shadow: 0 0 30px rgba(80, 157, 255, 0.36);
    .assistant-model-cut {
      animation-duration: 2.2s;
    }
    .assistant-bot-eye {
      animation: robotBlinkFast 1.1s infinite;
      box-shadow: 0 0 16px rgba(72, 186, 255, 0.95);
    }
    .assistant-bot-mouth {
      width: 28px;
      opacity: 1;
      animation: robotTalk 1.2s ease-in-out infinite;
    }
    .assistant-bot-core {
      animation: corePulse 1.4s ease-in-out infinite;
      box-shadow: 0 0 24px rgba(78, 120, 255, 0.26);
    }
    .assistant-bot-core-ring {
      animation: coreRotate 3s linear infinite;
    .assistant-model-img {
      filter: saturate(1.06) drop-shadow(0 18px 20px rgba(22, 48, 80, 0.22));
    }
    .assistant-status {
@@ -3785,219 +4330,174 @@
      animation: thinkingDot 1s ease-in-out infinite;
    }
    .assistant-base-lg {
      animation-duration: 1.8s;
    }
    .assistant-base-md {
      animation-duration: 1.5s;
    }
    .assistant-base-sm {
      box-shadow: 0 0 24px rgba(255, 93, 122, 0.48);
      box-shadow: 0 0 24px rgba(30, 91, 255, 0.36);
      animation-duration: 1.25s;
    }
  }
}
.assistant-halo {
  position: absolute;
  top: 22px;
  width: 130px;
  height: 130px;
  top: 24px;
  width: 146px;
  height: 146px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(46, 140, 224, 0.3) 0%, rgba(0, 85, 212, 0.18) 42%, rgba(113, 54, 244, 0.12) 60%, transparent 78%);
  background: radial-gradient(circle, rgba(31, 122, 114, 0.26) 0%, rgba(30, 91, 255, 0.2) 42%, rgba(109, 65, 237, 0.12) 66%, transparent 80%);
  filter: blur(6px);
  opacity: 0.82;
  opacity: 0.78;
  transition: all 0.35s ease;
}
.assistant-scan-ring {
  position: absolute;
  top: 40px;
  width: 132px;
  height: 132px;
  top: 44px;
  width: 136px;
  height: 136px;
  border-radius: 50%;
  border: 1px solid rgba(90, 159, 224, 0.22);
  border: 1px solid rgba(67, 145, 223, 0.24);
  box-shadow: inset 0 0 16px rgba(255, 255, 255, 0.25);
  opacity: 0.55;
  opacity: 0.52;
  animation: scanRing 4s linear infinite;
}
.assistant-orbit {
  position: absolute;
  top: 52px;
  width: 150px;
  height: 150px;
  width: 156px;
  height: 156px;
  border-radius: 50%;
  border: 1px dashed rgba(92, 135, 255, 0.22);
  opacity: 0.45;
  border: 1px dashed rgba(92, 135, 255, 0.24);
  opacity: 0.42;
}
.assistant-orbit-a {
  animation: orbitRotate 8s linear infinite;
  animation: orbitRotate 8.6s linear infinite;
}
.assistant-orbit-b {
  width: 118px;
  height: 118px;
  width: 124px;
  height: 124px;
  top: 68px;
  border-color: rgba(255, 108, 150, 0.22);
  animation: orbitRotateReverse 5.6s linear infinite;
  border-color: rgba(31, 122, 114, 0.24);
  animation: orbitRotateReverse 6.2s linear infinite;
}
.assistant-bot {
.assistant-model-shell {
  position: relative;
  z-index: 1;
  width: 148px;
  height: 178px;
  display: flex;
  flex-direction: column;
  align-items: center;
  align-items: flex-end;
  justify-content: center;
  margin-top: 12px;
  margin-top: 4px;
  transition: transform 0.35s ease;
}
.assistant-bot-antenna {
  position: absolute;
  top: -4px;
  width: 4px;
  height: 20px;
  border-radius: 999px;
  background: linear-gradient(180deg, #fefefe, #aac9ff);
  &::before {
    content: '';
    position: absolute;
    top: -6px;
    left: 50%;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    bottom: 2px;
    width: 164px;
    height: 42px;
    transform: translateX(-50%);
    background: linear-gradient(135deg, #54bfff, #7a41ff);
    box-shadow: 0 0 14px rgba(84, 191, 255, 0.65);
    border-radius: 50%;
    background: radial-gradient(
      ellipse at center,
      rgba(43, 126, 211, 0.32) 0%,
      rgba(43, 126, 211, 0.14) 46%,
      rgba(43, 126, 211, 0) 74%
    );
    filter: blur(2.6px);
    animation: baseGlow 4.6s ease-in-out infinite;
    z-index: 1;
  }
  &::after {
    content: '';
    position: absolute;
    left: 50%;
    bottom: 10px;
    width: 138px;
    height: 28px;
    transform: translateX(-50%);
    border-radius: 50%;
    border: 1px solid rgba(36, 116, 198, 0.6);
    box-shadow:
      inset 0 0 0 1px rgba(255, 255, 255, 0.58),
      0 0 22px rgba(42, 116, 196, 0.24);
    animation: basePulse 3.2s ease-in-out infinite;
    z-index: 4;
  }
}
.assistant-bot-antenna-left {
  left: 36px;
  transform: rotate(-14deg);
}
.assistant-bot-antenna-right {
  right: 36px;
  transform: rotate(14deg);
}
.assistant-bot-head {
.assistant-model-cut {
  position: relative;
  width: 132px;
  height: 178px;
  z-index: 6;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  transform-origin: center 84%;
  animation: avatarFloat 3.2s ease-in-out infinite;
}
.assistant-model-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center bottom;
  display: block;
  filter: saturate(1.03) drop-shadow(0 14px 18px rgba(22, 49, 79, 0.2));
  transition: filter 0.35s ease;
}
.assistant-model-fallback {
  width: 92px;
  height: 78px;
  border-radius: 28px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #e8f1ff 100%);
  border: 1px solid rgba(0, 85, 212, 0.14);
  box-shadow: 0 16px 32px rgba(0, 85, 212, 0.14);
  height: 92px;
  border-radius: 24px;
  color: #fff;
  background: linear-gradient(145deg, rgba(31, 122, 114, 0.9), rgba(30, 91, 255, 0.9));
  border: 1px solid rgba(255, 255, 255, 0.3);
  box-shadow: 0 12px 24px rgba(31, 85, 173, 0.22);
  display: flex;
  align-items: center;
  justify-content: center;
}
.assistant-bot-head-glow {
  position: absolute;
  inset: 10px 16px auto;
  height: 20px;
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(0, 85, 212, 0.16), transparent);
}
.assistant-bot-eye {
  position: absolute;
  top: 30px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: radial-gradient(circle, #8ef0ff 0%, #56c0ff 42%, #2869ff 100%);
  box-shadow: 0 0 12px rgba(72, 186, 255, 0.72);
  animation: robotBlink 3.2s infinite;
}
.assistant-bot-eye-left {
  left: 22px;
}
.assistant-bot-eye-right {
  right: 22px;
}
.assistant-bot-mouth {
.assistant-base {
  position: absolute;
  left: 50%;
  bottom: 16px;
  width: 22px;
  height: 4px;
  bottom: 8px;
  transform: translateX(-50%);
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(72, 186, 255, 0.2), rgba(72, 186, 255, 0.9), rgba(72, 186, 255, 0.2));
}
.assistant-bot-neck {
  width: 16px;
  height: 10px;
  border-radius: 0 0 10px 10px;
  background: linear-gradient(180deg, #dceaff, #bdd5ff);
  margin-top: -2px;
}
.assistant-bot-body {
  position: relative;
  width: 104px;
  height: 92px;
  margin-top: 2px;
  border-radius: 28px 28px 34px 34px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #e3eeff 100%);
  border: 1px solid rgba(0, 85, 212, 0.14);
  box-shadow: 0 18px 36px rgba(0, 85, 212, 0.16);
  display: flex;
  align-items: center;
  justify-content: center;
}
.assistant-bot-arm {
  position: absolute;
  top: 18px;
  width: 16px;
  height: 44px;
  border-radius: 999px;
  background: linear-gradient(180deg, #eff5ff, #c7dbff);
  border: 1px solid rgba(0, 85, 212, 0.12);
}
.assistant-bot-arm-left {
  left: -10px;
  transform: rotate(16deg);
}
.assistant-bot-arm-right {
  right: -10px;
  transform: rotate(-16deg);
}
.assistant-bot-core {
  position: relative;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: $primary-blue;
  background: radial-gradient(circle, rgba(255, 255, 255, 1) 0%, #dae8ff 55%, #adc7ff 100%);
}
.assistant-bot-core-ring {
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  border: 1px solid rgba(88, 135, 255, 0.3);
  border-top-color: rgba(255, 96, 139, 0.85);
  border-right-color: rgba(79, 145, 255, 0.9);
  border: 1px solid rgba(36, 116, 198, 0.28);
  background: radial-gradient(
    ellipse at center,
    rgba(255, 255, 255, 0.94) 0%,
    rgba(81, 164, 233, 0.16) 58%,
    rgba(30, 91, 255, 0.06) 100%
  );
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
}
.assistant-status {
  position: relative;
  z-index: 1;
  margin-top: 14px;
  padding: 6px 12px;
  margin-top: 7px;
  padding: 5px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-size: 11px;
  font-weight: 600;
  color: $deep-blue;
  background: rgba(255, 255, 255, 0.95);
@@ -4017,81 +4517,25 @@
}
.assistant-base {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  border-radius: 50%;
  border: 2px solid rgba(255, 93, 122, 0.22);
  background: radial-gradient(circle, rgba(255, 255, 255, 0.9) 0%, rgba(255, 111, 145, 0.1) 70%, transparent 100%);
}
.assistant-base-lg {
  width: 118px;
  height: 30px;
  pointer-events: none;
}
.assistant-base-md {
  bottom: 6px;
  width: 88px;
  height: 20px;
  border-color: rgba(255, 93, 122, 0.34);
  bottom: 15px;
  width: 104px;
  height: 22px;
  border-color: rgba(36, 116, 198, 0.48);
  animation: basePulse 2.8s ease-in-out infinite;
}
.assistant-base-sm {
  bottom: 11px;
  width: 54px;
  height: 10px;
  background: linear-gradient(90deg, rgba(255, 93, 122, 0.95), rgba(255, 173, 188, 0.9));
  bottom: 20px;
  width: 68px;
  height: 14px;
  background: linear-gradient(90deg, rgba(31, 122, 114, 0.82), rgba(45, 124, 255, 0.9));
  border: none;
  box-shadow: 0 0 18px rgba(255, 93, 122, 0.38);
}
@keyframes robotBlink {
  0%, 44%, 48%, 100% {
    transform: scaleY(1);
  }
  46% {
    transform: scaleY(0.14);
  }
}
@keyframes robotBlinkFast {
  0%, 100% {
    transform: scaleY(1);
  }
  50% {
    transform: scaleY(0.3);
  }
}
@keyframes robotTalk {
  0%, 100% {
    transform: translateX(-50%) scaleX(1);
  }
  50% {
    transform: translateX(-50%) scaleX(1.35);
  }
}
@keyframes corePulse {
  0%, 100% {
    transform: scale(1);
    filter: brightness(1);
  }
  50% {
    transform: scale(1.08);
    filter: brightness(1.08);
  }
}
@keyframes coreRotate {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
  box-shadow: 0 0 18px rgba(45, 124, 255, 0.34);
  animation: basePulse 2.2s ease-in-out infinite;
}
@keyframes orbitRotate {
@@ -4132,9 +4576,85 @@
  }
}
.assistant-base-lg {
  width: 142px;
  height: 32px;
  animation: basePulse 3.4s ease-in-out infinite;
  &::before {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 130px;
    height: 130px;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: conic-gradient(
      from 180deg,
      transparent 0deg,
      rgba(36, 116, 198, 0.65) 48deg,
      transparent 114deg,
      rgba(36, 116, 198, 0.55) 212deg,
      transparent 286deg,
      rgba(31, 122, 114, 0.45) 334deg,
      transparent 360deg
    );
    -webkit-mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
    mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
    opacity: 0.62;
    animation: baseSpin 9s linear infinite;
  }
}
@keyframes avatarFloat {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-7px);
  }
}
@keyframes basePulse {
  0%,
  100% {
    transform: translateX(-50%) scale(1);
    opacity: 0.88;
  }
  50% {
    transform: translateX(-50%) scale(1.05);
    opacity: 0.98;
  }
}
@keyframes baseSpin {
  from {
    transform: translate(-50%, -50%) rotate(0deg);
  }
  to {
    transform: translate(-50%, -50%) rotate(360deg);
  }
}
@keyframes baseGlow {
  0%,
  100% {
    transform: translateX(-50%) scaleX(1);
    opacity: 0.82;
  }
  50% {
    transform: translateX(-50%) scaleX(1.06);
    opacity: 0.96;
  }
}
.welcome-card {
  position: relative;
  padding: 14px 14px 12px;
  align-self: stretch;
  min-height: 206px;
  padding: 9px 10px 8px;
  border-radius: 16px;
  background:
    linear-gradient(#fff, #fff) padding-box,
@@ -4143,7 +4663,8 @@
  box-shadow: 0 16px 36px rgba(0, 85, 212, 0.12);
  &.compact {
    padding: 10px 12px;
    min-height: 160px;
    padding: 8px 9px 7px;
    border-radius: 12px;
    box-shadow: 0 8px 16px rgba(0, 85, 212, 0.07);
@@ -4152,8 +4673,8 @@
    }
    .welcome-title {
      font-size: 17px;
      line-height: 1.3;
      font-size: 16px;
      line-height: 1.25;
      br {
        display: none;
@@ -4161,65 +4682,69 @@
    }
    .welcome-desc {
      margin-top: 6px;
      font-size: 12px;
      line-height: 1.55;
      margin-top: 4px;
      font-size: 11px;
      line-height: 1.5;
    }
    .quick-prompt-list {
      margin-top: 10px;
      gap: 6px;
      margin-top: 8px;
      gap: 5px;
    }
    .quick-prompt-btn {
      padding: 8px 10px;
      font-size: 12px;
      padding: 7px 9px;
      font-size: 11px;
      border-radius: 7px;
    }
    .more-prompts-btn {
      margin-top: 8px;
      font-size: 12px;
      margin-top: 6px;
      font-size: 11px;
    }
  }
}
.welcome-eyebrow {
  font-size: 11px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 2px;
  color: rgba(0, 85, 212, 0.58);
  margin-bottom: 8px;
  margin-bottom: 5px;
}
.welcome-title {
  margin: 0;
  font-size: 26px;
  line-height: 1.2;
  font-size: 20px;
  line-height: 1.15;
  font-weight: 800;
  color: #172033;
  br {
    display: none;
  }
}
.welcome-desc {
  margin: 10px 0 0;
  font-size: 13px;
  line-height: 1.7;
  margin: 5px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: #5f6980;
}
.quick-prompt-list {
  display: grid;
  gap: 8px;
  margin-top: 14px;
  gap: 6px;
  margin-top: 8px;
}
.quick-prompt-btn {
  width: 100%;
  border: none;
  border-radius: 10px;
  padding: 11px 14px;
  border-radius: 9px;
  padding: 7px 10px;
  text-align: left;
  font-size: 13px;
  font-size: 12px;
  font-weight: 600;
  color: #fff;
  cursor: pointer;
@@ -4267,14 +4792,14 @@
}
.more-prompts-btn {
  margin-top: 10px;
  padding: 0 12px;
  height: 32px;
  margin-top: 6px;
  padding: 0 10px;
  height: 26px;
  border: 1px solid rgba(208, 65, 81, 0.12);
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(255, 241, 245, 0.96));
  color: #d04151;
  font-size: 13px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  display: inline-flex;
@@ -4289,21 +4814,6 @@
    border-color: transparent;
    color: #fff;
    box-shadow: 0 14px 24px rgba(138, 61, 246, 0.18);
  }
}
.hero-dot-grid {
  display: grid;
  grid-template-columns: repeat(14, 1fr);
  gap: 7px;
  padding: 0 18px 14px;
  span {
    display: block;
    width: 100%;
    aspect-ratio: 1;
    border-radius: 2px;
    background: linear-gradient(135deg, rgba(255, 110, 138, 0.95), rgba(255, 190, 201, 0.55));
  }
}
@@ -4399,12 +4909,6 @@
  .welcome-title {
    font-size: 21px;
  }
  .hero-dot-grid {
    grid-template-columns: repeat(12, 1fr);
    gap: 6px;
    padding: 0 14px 12px;
  }
  .message-list {
src/components/PurchaseAIChatSidebar/index.vue
@@ -3,24 +3,8 @@
</template>
<script setup>
import { ShoppingCart } from '@element-plus/icons-vue'
import AIChatSidebar from '@/components/AIChatSidebar/index.vue'
import { purchaseAssistant } from '@/components/AIChatSidebar/assistants'
const assistants = [
  {
    key: 'purchase',
    label: '采购助理',
    title: '采购智能助理',
    tooltip: '采购智能助理',
    icon: ShoppingCart,
    apiBase: '/purchase-ai',
    storageKey: 'purchase_ai_chat_uuid',
    placeholder: '请输入采购问题... (Enter å‘送, Shift+Enter æ¢è¡Œ)',
    welcomeMessage: '你好',
    allowFileUpload: true,
    allowMultipleFileUpload: true,
    fileAnalyzeUrl: '/purchase-ai/analyze-files',
    emptySessionText: '暂无采购会话'
  }
]
const assistants = [purchaseAssistant]
</script>
src/layout/index.vue
@@ -16,12 +16,13 @@
      <app-main />
      <settings ref="settingRef" />
    </div>
    <AIChatSidebar v-if="aiEnabled" />
    <AIChatSidebar v-if="showGlobalAiChat" />
  </div>
</template>
<script setup>
  import { useWindowSize } from "@vueuse/core";
  import { useRoute } from "vue-router";
  import Sidebar from "./components/Sidebar/index.vue";
  import { AppMain, Navbar, Settings, TagsView } from "./components";
  import AIChatSidebar from "@/components/AIChatSidebar/index.vue";
@@ -33,6 +34,7 @@
  const settingsStore = useSettingsStore();
  const userStore = useUserStore();
  const route = useRoute();
  const theme = computed(() => settingsStore.theme);
  const sideTheme = computed(() => settingsStore.sideTheme);
  const sidebar = computed(() => useAppStore().sidebar);
@@ -40,6 +42,10 @@
  const needTagsView = computed(() => settingsStore.tagsView);
  const fixedHeader = computed(() => settingsStore.fixedHeader);
  const aiEnabled = computed(() => Number(userStore.aiEnabled) === 1);
  const showGlobalAiChat = computed(() => {
    const isIndustrialBrainRoute = String(route.path || "").startsWith("/ai-industrial-brain");
    return !isIndustrialBrainRoute && aiEnabled.value;
  });
  const classObj = computed(() => ({
    hideSidebar: !sidebar.value.opened,
src/router/index.js
@@ -86,6 +86,18 @@
    ],
  },
  {
    path: "/ai-industrial-brain",
    component: Layout,
    children: [
      {
        path: "index",
        component: () => import("@/views/aiIndustrialBrain/index.vue"),
        name: "AiIndustrialBrain",
        meta: { title: "AI工业大脑", icon: "skill" },
      },
    ],
  },
  {
    path: "/user",
    component: Layout,
    hidden: true,
@@ -134,118 +146,121 @@
    ],
  },
  // è´¢åŠ¡ç®¡ç†æ¨¡å—è·¯ç”±
  // {
  //   path: "/financial",
  //   component: Layout,
  //   hidden: false,
  //   redirect: "/financial/general-ledger",
  //   alwaysShow: true,
  //   meta: { title: "财务管理", icon: "money" },
  //   children: [
  //     {
  //       path: "general-ledger",
  //       component: () => import("@/views/financialManagement/generalLedger/index.vue"),
  //       name: "GeneralLedger",
  //       meta: { title: "总帐科目" },
  //     },
  //     {
  //       path: "sales-out",
  //       component: () => import("@/views/financialManagement/receivable/salesOut.vue"),
  //       name: "SalesOut",
  //       meta: { title: "销售出库" },
  //     },
  //     {
  //       path: "sales-return",
  //       component: () => import("@/views/financialManagement/receivable/salesReturn.vue"),
  //       name: "SalesReturn",
  //       meta: { title: "销售退货" },
  //     },
  //     {
  //       path: "receivable-reconciliation",
  //       component: () => import("@/views/financialManagement/receivable/reconciliation.vue"),
  //       name: "ReceivableReconciliation",
  //       meta: { title: "应收对账" },
  //     },
  //     {
  //       path: "invoice-apply",
  //       component: () => import("@/views/financialManagement/receivable/invoiceApply.vue"),
  //       name: "InvoiceApply",
  //       meta: { title: "开票申请" },
  //     },
  //     {
  //       path: "output-invoice",
  //       component: () => import("@/views/financialManagement/receivable/outputInvoice.vue"),
  //       name: "OutputInvoice",
  //       meta: { title: "销项发票" },
  //     },
  //     {
  //       path: "receipt",
  //       component: () => import("@/views/financialManagement/receivable/receipt.vue"),
  //       name: "Receipt",
  //       meta: { title: "收款单" },
  //     },
  //     {
  //       path: "purchase-in",
  //       component: () => import("@/views/financialManagement/payable/purchaseIn.vue"),
  //       name: "PurchaseIn",
  //       meta: { title: "采购入库" },
  //     },
  //     {
  //       path: "payable-reconciliation",
  //       component: () => import("@/views/financialManagement/payable/reconciliation.vue"),
  //       name: "PayableReconciliation",
  //       meta: { title: "应付对账" },
  //     },
  //     {
  //       path: "input-invoice",
  //       component: () => import("@/views/financialManagement/payable/input-invoice.vue"),
  //       name: "InputInvoice",
  //       meta: { title: "进项发票" },
  //     },
  //     {
  //       path: "payment-apply",
  //       component: () => import("@/views/financialManagement/payable/paymentApply.vue"),
  //       name: "PaymentApply",
  //       meta: { title: "付款申请" },
  //     },
  //     {
  //       path: "payment",
  //       component: () => import("@/views/financialManagement/payable/payment.vue"),
  //       name: "Payment",
  //       meta: { title: "付款单" },
  //     },
  //     {
  //       path: "fixed-assets",
  //       component: () => import("@/views/financialManagement/assets/fixedAssets.vue"),
  //       name: "FixedAssets",
  //       meta: { title: "固定资产" },
  //     },
  //     {
  //       path: "intangible-assets",
  //       component: () => import("@/views/financialManagement/assets/intangibleAssets.vue"),
  //       name: "IntangibleAssets",
  //       meta: { title: "无形资产" },
  //     },
  //     {
  //       path: "voucher",
  //       component: () => import("@/views/financialManagement/voucher/index.vue"),
  //       name: "Voucher",
  //       meta: { title: "凭证" },
  //     },
  //     {
  //       path: "voucher-general-ledger",
  //       component: () => import("@/views/financialManagement/voucher/generalLedger.vue"),
  //       name: "VoucherGeneralLedger",
  //       meta: { title: "科目总帐" },
  //     },
  //     {
  //       path: "voucher-detail-ledger",
  //       component: () => import("@/views/financialManagement/voucher/detailLedger.vue"),
  //       name: "VoucherDetailLedger",
  //       meta: { title: "科目明细帐" },
  //     },
  //   ],
  // },
  {
    path: "/financial",
    component: Layout,
    hidden: false,
    redirect: "/financial/general-ledger",
    alwaysShow: true,
    meta: { title: "财务管理", icon: "money" },
    children: [
      {
        path: "sales-out",
        component: () => import("@/views/financialManagement/receivable/salesOut.vue"),
        name: "SalesOut",
        meta: { title: "销售出库" },
      },
      {
        path: "sales-return",
        component: () => import("@/views/financialManagement/receivable/salesReturn.vue"),
        name: "SalesReturn",
        meta: { title: "销售退货" },
      },
      {
        path: "invoice-apply",
        component: () => import("@/views/financialManagement/receivable/invoiceApply.vue"),
        name: "InvoiceApply",
        meta: { title: "开票申请" },
      },
      {
        path: "output-invoice",
        component: () => import("@/views/financialManagement/receivable/outputInvoice.vue"),
        name: "OutputInvoice",
        meta: { title: "销项发票" },
      },
      {
        path: "receipt",
        component: () => import("@/views/financialManagement/receivable/receipt.vue"),
        name: "Receipt",
        meta: { title: "收款单" },
      },
      {
        path: "receivable-reconciliation",
        component: () => import("@/views/financialManagement/receivable/reconciliation.vue"),
        name: "ReceivableReconciliation",
        meta: { title: "应收对账" },
      },
      {
        path: "purchase-in",
        component: () => import("@/views/financialManagement/payable/purchaseIn.vue"),
        name: "PurchaseIn",
        meta: { title: "采购入库" },
      },
      {
        path: "input-invoice",
        component: () => import("@/views/financialManagement/payable/input-invoice.vue"),
        name: "InputInvoice",
        meta: { title: "进项发票" },
      },
      {
        path: "payment-apply",
        component: () => import("@/views/financialManagement/payable/paymentApply.vue"),
        name: "PaymentApply",
        meta: { title: "付款申请" },
      },
      {
        path: "payment",
        component: () => import("@/views/financialManagement/payable/payment.vue"),
        name: "Payment",
        meta: { title: "付款单" },
      },
      {
        path: "payable-reconciliation",
        component: () => import("@/views/financialManagement/payable/reconciliation.vue"),
        name: "PayableReconciliation",
        meta: { title: "应付对账" },
      },
      {
        path: "fixed-assets",
        component: () => import("@/views/financialManagement/assets/fixedAssets.vue"),
        name: "FixedAssets",
        meta: { title: "固定资产" },
      },
      {
        path: "intangible-assets",
        component: () => import("@/views/financialManagement/assets/intangibleAssets.vue"),
        name: "IntangibleAssets",
        meta: { title: "无形资产" },
      },
      {
        path: "general-ledger",
        component: () => import("@/views/financialManagement/generalLedger/index.vue"),
        name: "GeneralLedger",
        meta: { title: "总帐科目" },
      },
      {
        path: "voucher",
        component: () => import("@/views/financialManagement/voucher/index.vue"),
        name: "Voucher",
        meta: { title: "凭证" },
      },
      {
        path: "voucher-general-ledger",
        component: () => import("@/views/financialManagement/voucher/generalLedger.vue"),
        name: "VoucherGeneralLedger",
        meta: { title: "科目总帐" },
      },
      {
        path: "voucher-detail-ledger",
        component: () => import("@/views/financialManagement/voucher/detailLedger.vue"),
        name: "VoucherDetailLedger",
        meta: { title: "科目明细帐" },
      },
    ],
  },
];
// åŠ¨æ€è·¯ç”±ï¼ŒåŸºäºŽç”¨æˆ·æƒé™åŠ¨æ€åŽ»åŠ è½½
src/store/modules/permission.js
@@ -44,17 +44,18 @@
            const defaultData = JSON.parse(JSON.stringify(rawRoutes))
            const sidebarRoutes = filterAsyncRouter(sdata)
            const rewriteRoutes = filterAsyncRouter(rdata, false, true)
            const defaultRoutes = filterAsyncRouter(defaultData)
            const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
            asyncRoutes.forEach(route => { router.addRoute(route) })
            this.setRoutes(rewriteRoutes)
            // å°†è´¢åŠ¡ç®¡ç†è·¯ç”±åˆå¹¶åˆ°ä¾§è¾¹æ 
            this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
            this.setDefaultRoutes(sidebarRoutes)
            this.setTopbarRoutes(defaultRoutes)
            resolve(rewriteRoutes)
          })
        })
            const defaultRoutes = filterAsyncRouter(defaultData)
            const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
            asyncRoutes.forEach(route => { router.addRoute(route) })
            this.setRoutes(rewriteRoutes)
            const constantSidebarRoutes = filterAiFeatureRoutes(constantRoutes, aiEnabled)
            // å°†è´¢åŠ¡ç®¡ç†è·¯ç”±åˆå¹¶åˆ°ä¾§è¾¹æ 
            this.setSidebarRouters(constantSidebarRoutes.concat(sidebarRoutes))
            this.setDefaultRoutes(sidebarRoutes)
            this.setTopbarRoutes(defaultRoutes)
            resolve(rewriteRoutes)
          })
        })
      }
    }
  })
@@ -118,7 +119,7 @@
  })
}
function filterChildren(childrenMap, lastRouter = false) {
function filterChildren(childrenMap, lastRouter = false) {
  var children = []
  childrenMap.forEach(el => {
    el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path
@@ -128,11 +129,11 @@
      children.push(el)
    }
  })
  return children
}
// åŠ¨æ€è·¯ç”±éåŽ†ï¼ŒéªŒè¯æ˜¯å¦å…·å¤‡æƒé™
export function filterDynamicRoutes(routes) {
  return children
}
// åŠ¨æ€è·¯ç”±éåŽ†ï¼ŒéªŒè¯æ˜¯å¦å…·å¤‡æƒé™
export function filterDynamicRoutes(routes) {
  const res = []
  routes.forEach(route => {
    if (route.permissions) {
src/views/aiIndustrialBrain/components/AiAssistantWorkspace.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,192 @@
<template>
  <transition name="fade">
    <section v-if="visible" class="assistant-workspace">
      <div class="assistant-workspace__panel">
        <button
          v-if="assistantMode === 'pending'"
          type="button"
          class="workspace-back-btn"
          @click="$emit('close')"
        >
          <el-icon><ArrowLeftBold /></el-icon>
          <span>返回工业大屏</span>
        </button>
        <div class="assistant-workspace__body">
          <AIChatSidebar
            v-if="assistantMode !== 'pending'"
            :key="assistantMode"
            class="workspace-chat"
            :assistants="assistantMode === 'purchase' ? [purchaseAssistant] : [generalAssistant]"
            :default-assistant="assistantMode"
            :hide-trigger="true"
            :auto-open="true"
            drawer-size="100%"
            drawer-direction="ttb"
            header-extra-action-text="返回工业大屏"
            @header-extra-action="$emit('close')"
          />
          <div v-else class="workspace-pending">
            <div class="workspace-pending__content">
              <h3>{{ agentTitle }}</h3>
              <p>正在开发,敬请期待......</p>
            </div>
          </div>
        </div>
      </div>
    </section>
  </transition>
</template>
<script setup>
import { computed } from "vue";
import { ArrowLeftBold } from "@element-plus/icons-vue";
import AIChatSidebar from "@/components/AIChatSidebar/index.vue";
import { generalAssistant, purchaseAssistant } from "@/components/AIChatSidebar/assistants";
const props = defineProps({
  visible: {
    type: Boolean,
    default: false,
  },
  agent: {
    type: Object,
    default: () => ({}),
  },
});
defineEmits(["close"]);
const agentKey = computed(() => String(props.agent?.key || ""));
const agentTitle = computed(() => String(props.agent?.name || "AI助手"));
const assistantMode = computed(() => {
  if (agentKey.value === "purchase") return "purchase";
  if (agentKey.value === "general") return "general";
  return "pending";
});
</script>
<style scoped>
.assistant-workspace {
  position: fixed;
  inset: 0;
  z-index: 2100;
  padding: 12px;
  background: rgba(33, 49, 63, 0.24);
  backdrop-filter: blur(2px);
}
.assistant-workspace__panel {
  position: relative;
  height: 100%;
  border-radius: 22px;
  border: 1px solid var(--surface-border);
  background: linear-gradient(180deg, #f9fcfb 0%, #f0f5f2 100%);
  box-shadow: var(--shadow-md);
  overflow: hidden;
}
.assistant-workspace__body {
  height: 100%;
  min-height: 100%;
}
.workspace-back-btn {
  position: absolute;
  top: 16px;
  right: 20px;
  z-index: 5;
  height: 36px;
  padding: 0 14px;
  border: 1px solid rgba(38, 112, 183, 0.3);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.92);
  color: #25528f;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
}
.workspace-back-btn:hover {
  border-color: rgba(31, 122, 114, 0.45);
  color: #1f5ddf;
  box-shadow: 0 8px 16px rgba(31, 122, 114, 0.14);
  transform: translateY(-1px);
}
.workspace-chat {
  width: 100%;
  height: 100%;
}
.workspace-chat :deep(.ai-chat-sidebar-wrapper) {
  height: 100%;
}
.workspace-chat :deep(.ai-chat-drawer) {
  height: 100%;
}
.workspace-chat :deep(.el-drawer) {
  height: 100% !important;
  width: 100% !important;
}
.workspace-pending {
  height: 100%;
  display: grid;
  place-items: center;
  padding: 20px;
  color: var(--text-secondary);
}
.workspace-pending__content {
  display: grid;
  gap: 12px;
  text-align: center;
}
.workspace-pending__content h3 {
  margin: 0;
  font-size: 36px;
  color: var(--text-primary);
}
.workspace-pending__content p {
  margin: 0;
  font-size: 24px;
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
@media (max-width: 1600px) {
  .workspace-back-btn {
    top: 12px;
    right: 14px;
    height: 32px;
    padding: 0 12px;
    font-size: 13px;
  }
  .workspace-pending__content h3 {
    font-size: 30px;
  }
  .workspace-pending__content p {
    font-size: 20px;
  }
}
</style>
src/views/aiIndustrialBrain/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1499 @@
<template>
  <div ref="screenRef" class="ai-brain-screen">
    <section class="brain-stage">
      <header class="brain-head">
        <div class="head-date">
          <p>{{ weekLabel }}</p>
          <p>{{ dateLabel }}</p>
        </div>
        <div class="head-title">
          <span>AI工业大脑</span>
        </div>
        <div class="head-actions">
          <button type="button" class="head-back-btn" @click="goBack">
            <el-icon><ArrowLeftBold /></el-icon>
            <span>返回</span>
          </button>
        </div>
      </header>
      <section class="brain-intro">
        <h2>工业AI数字员工,赋能智造新纪元</h2>
        <p>六大AI助手协同企业管理、销售、采购、生产、财务及数据全链路</p>
        <div class="intro-sign">阿里云 Ã— åƒé—®å¤§æ¨¡åž‹ Ã— æ™ºèƒ½ä½“AI</div>
      </section>
      <section class="carousel-area">
        <button type="button" class="nav-btn nav-btn--left" @click="prevCard">
          <el-icon><ArrowLeftBold /></el-icon>
        </button>
        <div class="carousel-track">
          <article
            v-for="card in visibleCards"
            :key="card.agent.key"
            class="agent-card"
            :class="{ 'agent-card--active': card.offset === 0 }"
            :style="getCardStyle(card.offset)"
            @click="openAssistant(card.realIndex)"
          >
            <div class="agent-card__head" :class="{ 'agent-card__head--active': card.offset === 0 }">
              {{ card.agent.name }}
            </div>
            <div class="agent-card__body" :class="{ 'agent-card__body--active': card.offset === 0 }">
              <div class="avatar-shell" :class="{ 'avatar-shell--active': card.offset === 0 }">
                <div class="avatar-base"></div>
                <div class="avatar-cut">
                  <img v-if="card.agent.avatar" class="avatar-cut__img" :src="card.agent.avatar" :alt="card.agent.name" />
                </div>
              </div>
              <div v-if="card.offset === 0" class="highlight-list">
                <div
                  v-for="highlight in card.agent.highlights"
                  :key="highlight"
                  class="highlight-item"
                >
                  {{ highlight }}
                </div>
              </div>
            </div>
          </article>
        </div>
        <button type="button" class="nav-btn nav-btn--right" @click="nextCard">
          <el-icon><ArrowRightBold /></el-icon>
        </button>
      </section>
      <section class="brain-footer">
        <div class="footer-grid-overlay"></div>
        <div class="footer-metrics">
          <article class="footer-metric">
            <span class="footer-metric__label">在线智能体</span>
            <strong class="footer-metric__value">{{ agents.length }}个</strong>
            <small class="footer-metric__hint">全链路协同运行</small>
          </article>
          <article class="footer-metric footer-metric--focus">
            <span class="footer-metric__label">当前焦点</span>
            <strong class="footer-metric__value">{{ getFooterAgentName(focusAgent.name) }}</strong>
            <small class="footer-metric__hint">{{ focusAgent.highlights?.[0] || "智能分析联动" }}</small>
          </article>
          <article class="footer-metric footer-metric--period">
            <span class="footer-metric__label">轮播周期</span>
            <strong class="footer-metric__value">{{ carouselSecondsText }}</strong>
            <div class="footer-period-control">
              <button type="button" class="period-btn" @click="adjustCarouselSeconds(-0.5)">-</button>
              <input
                v-model.number="carouselSeconds"
                class="period-input"
                type="number"
                min="2"
                max="12"
                step="0.5"
              />
              <span class="period-unit">s</span>
              <button type="button" class="period-btn" @click="adjustCarouselSeconds(0.5)">+</button>
            </div>
            <input
              v-model.number="carouselSeconds"
              class="footer-period-slider"
              type="range"
              min="2"
              max="12"
              step="0.5"
            />
            <small class="footer-metric__hint">可手动设置 2.0s - 12.0s</small>
          </article>
        </div>
        <div class="footer-rail">
          <div class="footer-rail__line">
            <span class="footer-rail__flow"></span>
          </div>
          <div class="footer-rail__nodes">
            <button
              v-for="node in footerNodes"
              :key="node.key"
              type="button"
              class="footer-node"
              :class="{ 'footer-node--active': node.index === carouselIndex }"
              @click="openAssistant(node.index)"
            >
              <span class="footer-node__dot"></span>
              <span class="footer-node__name">{{ getFooterAgentName(node.name) }}</span>
            </button>
          </div>
        </div>
      </section>
    </section>
    <AiAssistantWorkspace
      :visible="fullscreenVisible"
      :agent="currentAgent"
      @close="closeFullscreen"
    />
  </div>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import { ArrowLeftBold, ArrowRightBold } from "@element-plus/icons-vue";
import AiAssistantWorkspace from "./components/AiAssistantWorkspace.vue";
import todoAvatar from "@/assets/AI/待办助手.png";
import salesAvatar from "@/assets/AI/销售助手.png";
import purchaseAvatar from "@/assets/AI/采购助手.png";
import productionAvatar from "@/assets/AI/生产助手.png";
import financeAvatar from "@/assets/AI/财务助手.png";
const router = useRouter();
const agents = [
  {
    key: "general",
    name: "AI待办助手",
    highlights: ["跨模块流程诊断", "经营风险智能提醒"],
  },
  {
    key: "sales",
    name: "AI销售助手",
    highlights: ["客户流失风险分析", "回款与报价策略建议"],
  },
  {
    key: "purchase",
    name: "AI采购助手",
    highlights: ["供应链指标分析", "采购订单智能生成"],
  },
  {
    key: "production",
    name: "AI生产助手",
    highlights: ["工序瓶颈定位", "产能与报废智能预警"],
  },
  {
    key: "finance",
    name: "AI财务助手",
    highlights: ["现金流压力预判", "费用结构智能分析"],
  },
];
const avatarByAgentKey = {
  general: todoAvatar,
  sales: salesAvatar,
  purchase: purchaseAvatar,
  production: productionAvatar,
  finance: financeAvatar,
};
for (let i = agents.length - 1; i >= 0; i -= 1) {
  const agent = agents[i];
  const avatar = avatarByAgentKey[agent.key];
  if (!avatar) {
    agents.splice(i, 1);
    continue;
  }
  agent.avatar = avatar;
}
const carouselIndex = ref(Math.min(2, Math.max(agents.length - 1, 0)));
const fullscreenVisible = ref(false);
const screenRef = ref(null);
const carouselIntervalMs = ref(4500);
let carouselTimer = null;
const fallbackAgent = {
  key: "fallback",
  name: "AI助手",
  avatar: "",
  highlights: [],
};
const currentAgent = computed(() => agents[carouselIndex.value] || agents[0] || fallbackAgent);
const focusAgent = computed(() => currentAgent.value || fallbackAgent);
const footerNodes = computed(() =>
  agents.map((agent, index) => ({
    key: agent.key,
    name: agent.name,
    index,
  }))
);
const carouselSeconds = computed({
  get: () => Number((carouselIntervalMs.value / 1000).toFixed(1)),
  set: (value) => {
    const next = Number(value);
    if (!Number.isFinite(next)) return;
    const clamped = Math.max(2, Math.min(12, Math.round(next * 2) / 2));
    carouselIntervalMs.value = Math.round(clamped * 1000);
  },
});
const carouselSecondsText = computed(() => `${carouselSeconds.value.toFixed(1)}s`);
const weekLabel = computed(() => {
  const weekMap = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
  return weekMap[new Date().getDay()];
});
const dateLabel = computed(() => {
  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");
  return `${year}å¹´${month}月${day}日`;
});
const visibleCards = computed(() => {
  const total = agents.length;
  return agents
    .map((agent, index) => {
      let offset = index - carouselIndex.value;
      if (offset > total / 2) offset -= total;
      if (offset < -total / 2) offset += total;
      return { agent, offset, realIndex: index };
    })
    .filter((item) => Math.abs(item.offset) <= 2)
    .sort((a, b) => a.offset - b.offset);
});
function getCardStyle(offset) {
  const distance = Math.abs(offset);
  const scale = distance === 0 ? 1 : distance === 1 ? 0.88 : 0.78;
  const opacity = distance === 0 ? 1 : distance === 1 ? 0.92 : 0.76;
  return {
    transform: `translateX(${offset * 340}px) scale(${scale})`,
    zIndex: String(50 - distance),
    opacity,
  };
}
function getFooterAgentName(name) {
  return String(name || "AI助手").replace(/^AI/, "");
}
function adjustCarouselSeconds(delta) {
  carouselSeconds.value = carouselSeconds.value + delta;
}
function prevCard() {
  const total = agents.length;
  if (!total) return;
  carouselIndex.value = (carouselIndex.value - 1 + total) % total;
}
function nextCard() {
  const total = agents.length;
  if (!total) return;
  carouselIndex.value = (carouselIndex.value + 1) % total;
}
async function enterBrowserFullscreen() {
  if (document.fullscreenElement) return;
  const target = screenRef.value || document.documentElement;
  if (!target || typeof target.requestFullscreen !== "function") return;
  try {
    await target.requestFullscreen();
  } catch (error) {
    // Ignore: browser may block fullscreen when there is no direct user activation.
  }
}
async function exitBrowserFullscreen() {
  if (!document.fullscreenElement || typeof document.exitFullscreen !== "function") return;
  try {
    await document.exitFullscreen();
  } catch (error) {
    // Ignore fullscreen exit failures.
  }
}
function goBack() {
  closeFullscreen();
  exitBrowserFullscreen();
  if (window.history.length > 1) {
    router.back();
    return;
  }
  router.push("/index");
}
function openAssistant(index) {
  if (!agents.length) return;
  carouselIndex.value = index;
  fullscreenVisible.value = true;
}
function closeFullscreen() {
  fullscreenVisible.value = false;
}
function startCarousel() {
  stopCarousel();
  if (fullscreenVisible.value) return;
  carouselTimer = window.setInterval(() => {
    nextCard();
  }, carouselIntervalMs.value);
}
function stopCarousel() {
  if (carouselTimer) {
    window.clearInterval(carouselTimer);
    carouselTimer = null;
  }
}
function handleEscClose(event) {
  if (event.key === "Escape" && fullscreenVisible.value) {
    closeFullscreen();
  }
}
watch(
  () => fullscreenVisible.value,
  (opened) => {
    if (opened) {
      stopCarousel();
    } else {
      startCarousel();
    }
  }
);
watch(
  () => carouselIntervalMs.value,
  () => {
    if (!fullscreenVisible.value) {
      startCarousel();
    }
  }
);
onMounted(() => {
  startCarousel();
  window.addEventListener("keydown", handleEscClose);
  window.requestAnimationFrame(() => {
    enterBrowserFullscreen();
  });
});
onBeforeUnmount(() => {
  stopCarousel();
  window.removeEventListener("keydown", handleEscClose);
  exitBrowserFullscreen();
});
</script>
<style scoped>
.ai-brain-screen {
  position: fixed;
  inset: 0;
  z-index: 1900;
  padding: 10px;
  overflow: hidden;
  background: var(--app-bg);
}
.brain-stage {
  position: relative;
  height: 100%;
  min-height: 100%;
  border-radius: 22px;
  border: 1px solid var(--surface-border);
  background:
    radial-gradient(circle at 14% 8%, rgba(31, 122, 114, 0.14), transparent 40%),
    radial-gradient(circle at 86% 12%, rgba(30, 91, 255, 0.1), transparent 42%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(245, 249, 247, 0.94)),
    repeating-linear-gradient(
      135deg,
      rgba(255, 255, 255, 0.05) 0,
      rgba(255, 255, 255, 0.05) 14px,
      rgba(31, 122, 114, 0.03) 14px,
      rgba(31, 122, 114, 0.03) 28px
    );
  box-shadow: var(--shadow-sm);
}
.brain-head {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr) 180px;
  align-items: center;
  padding: 12px 18px 0;
}
.head-date {
  color: var(--text-secondary);
  font-size: 24px;
  font-weight: 600;
}
.head-date p {
  margin: 0;
  line-height: 1.2;
}
.head-title {
  justify-self: center;
  width: min(760px, 95%);
  height: 68px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 0 0 46px 46px;
  color: #fff;
  font-size: 42px;
  font-style: italic;
  font-weight: 700;
  letter-spacing: 1px;
  background: linear-gradient(135deg, #1f7a72 0%, #1e5bff 100%);
  box-shadow: 0 16px 30px rgba(31, 122, 114, 0.24);
}
.head-actions {
  justify-self: end;
}
.head-back-btn {
  height: 40px;
  padding: 0 14px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border: none;
  border-radius: 999px;
  font-size: 16px;
  font-weight: 600;
  color: var(--colorPrimary);
  background: var(--surface-base);
  box-shadow: 0 8px 18px rgba(31, 49, 38, 0.12);
  cursor: pointer;
}
.brain-intro {
  text-align: center;
  margin-top: 34px;
}
.brain-intro h2 {
  margin: 0;
  font-size: 44px;
  font-style: italic;
  font-weight: 700;
  color: var(--text-primary);
}
.brain-intro p {
  margin: 12px 0 10px;
  font-size: 28px;
  color: var(--text-secondary);
}
.intro-sign {
  display: inline-block;
  padding: 6px 18px;
  border-radius: 999px;
  font-size: 24px;
  font-weight: 700;
  color: #1e5bff;
  background: rgba(255, 255, 255, 0.82);
  border: 1px solid rgba(30, 91, 255, 0.18);
}
.carousel-area {
  position: relative;
  margin-top: 34px;
  padding: 0 72px 12px;
}
.carousel-track {
  position: relative;
  height: 500px;
  overflow: hidden;
}
.brain-footer {
  position: relative;
  margin: 0 72px;
  height: clamp(226px, 25vh,0);
  border-radius: 18px;
  border: 1px solid rgba(31, 122, 114, 0.28);
  background:
    linear-gradient(120deg, rgba(31, 122, 114, 0.14), rgba(30, 91, 255, 0.14)),
    linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(236, 244, 249, 0.9));
  box-shadow:
    0 16px 34px rgba(31, 81, 131, 0.12),
    inset 0 1px 0 rgba(255, 255, 255, 0.72);
  overflow: hidden;
}
.brain-footer::before {
  content: "";
  position: absolute;
  left: -22%;
  bottom: -120%;
  width: 52%;
  height: 260%;
  background: radial-gradient(ellipse at center, rgba(30, 91, 255, 0.2) 0%, rgba(30, 91, 255, 0) 72%);
  pointer-events: none;
}
.brain-footer::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(110deg, transparent 12%, rgba(255, 255, 255, 0.24) 38%, transparent 64%);
  transform: translateX(-120%);
  animation: footerSweep 5.8s linear infinite;
  pointer-events: none;
}
.footer-grid-overlay {
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(
      90deg,
      rgba(31, 122, 114, 0.07) 0,
      rgba(31, 122, 114, 0.07) 1px,
      transparent 1px,
      transparent 36px
    ),
    repeating-linear-gradient(
      0deg,
      rgba(30, 91, 255, 0.06) 0,
      rgba(30, 91, 255, 0.06) 1px,
      transparent 1px,
      transparent 28px
    );
  opacity: 0.72;
  pointer-events: none;
}
.footer-metrics {
  position: relative;
  z-index: 2;
  padding: 14px 20px 72px;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}
.footer-metric {
  min-height: 76px;
  border-radius: 12px;
  padding: 10px 14px;
  border: 1px solid rgba(37, 124, 188, 0.2);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(245, 250, 255, 0.82));
  box-shadow: 0 10px 18px rgba(29, 83, 134, 0.08);
  display: grid;
  grid-template-rows: auto auto 1fr;
  gap: 4px;
}
.footer-metric--focus {
  border-color: rgba(38, 122, 194, 0.34);
  box-shadow:
    0 12px 22px rgba(30, 91, 255, 0.12),
    inset 0 0 0 1px rgba(85, 148, 232, 0.2);
}
.footer-metric__label {
  font-size: 14px;
  color: rgba(38, 72, 108, 0.88);
  font-weight: 600;
}
.footer-metric__value {
  font-size: 30px;
  line-height: 1;
  font-style: italic;
  font-weight: 700;
  color: #1f5ddf;
  text-shadow: 0 3px 10px rgba(30, 91, 255, 0.18);
}
.footer-metric__hint {
  margin-top: auto;
  font-size: 13px;
  color: rgba(52, 89, 128, 0.82);
}
.footer-metric--period .footer-metric__hint {
  margin-top: 0;
  line-height: 1.25;
}
.footer-metric--period {
  min-height: 122px;
  grid-template-rows: auto auto auto auto auto;
  gap: 6px;
}
.footer-period-control {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.period-btn {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid rgba(38, 112, 183, 0.28);
  background: rgba(255, 255, 255, 0.9);
  color: #2054c9;
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}
.period-input {
  width: 56px;
  height: 24px;
  border-radius: 8px;
  border: 1px solid rgba(38, 112, 183, 0.24);
  background: rgba(255, 255, 255, 0.94);
  color: #1f5ddf;
  font-size: 13px;
  font-weight: 600;
  text-align: center;
  padding: 0 4px;
}
.period-unit {
  font-size: 12px;
  font-weight: 600;
  color: rgba(40, 80, 117, 0.86);
}
.footer-period-slider {
  width: min(250px, 100%);
  height: 3px;
  accent-color: #2a6ded;
  cursor: pointer;
}
.footer-rail {
  position: absolute;
  left: 20px;
  right: 20px;
  bottom: 18px;
  z-index: 2;
}
.footer-rail__line {
  position: relative;
  height: 2px;
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(31, 122, 114, 0.12), rgba(30, 91, 255, 0.6), rgba(31, 122, 114, 0.12));
  overflow: hidden;
}
.footer-rail__flow {
  position: absolute;
  top: -1px;
  left: -18%;
  width: 22%;
  height: 4px;
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(31, 122, 114, 0), rgba(59, 146, 244, 0.92), rgba(30, 91, 255, 0));
  filter: blur(0.2px);
  animation: railFlow 3.1s ease-in-out infinite;
}
.footer-rail__nodes {
  margin-top: 12px;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 8px;
}
.footer-node {
  height: 34px;
  border: 1px solid rgba(38, 112, 183, 0.18);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.76);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  color: rgba(40, 80, 117, 0.92);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.26s ease;
}
.footer-node:hover {
  transform: translateY(-1px);
  border-color: rgba(31, 122, 114, 0.34);
  box-shadow: 0 8px 14px rgba(31, 122, 114, 0.14);
}
.footer-node--active {
  color: #fff;
  border-color: transparent;
  background: linear-gradient(135deg, rgba(31, 122, 114, 0.94), rgba(30, 91, 255, 0.94));
  box-shadow: 0 10px 18px rgba(30, 91, 255, 0.28);
}
.footer-node__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(30, 91, 255, 0.72);
  box-shadow: 0 0 10px rgba(30, 91, 255, 0.52);
}
.footer-node--active .footer-node__dot {
  background: #fff;
  box-shadow: 0 0 12px rgba(255, 255, 255, 0.72);
  animation: nodePulse 1.4s ease-in-out infinite;
}
.agent-card {
  position: absolute;
  left: 50%;
  top: 0;
  width: 460px;
  margin-left: -230px;
  cursor: pointer;
  transform-origin: center bottom;
  transition: transform 0.35s ease, opacity 0.35s ease;
}
.agent-card__head {
  height: 56px;
  border-radius: 12px 12px 0 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 28px;
  color: #fff;
  font-weight: 700;
  background: linear-gradient(135deg, #1f7a72 0%, #1e5bff 100%);
}
.agent-card__head--active {
  box-shadow:
    0 12px 22px rgba(30, 91, 255, 0.26),
    inset 0 0 0 1px rgba(255, 255, 255, 0.28);
  position: relative;
}
.agent-card__head--active::after {
  content: "";
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 6px;
  height: 3px;
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.96), rgba(255, 255, 255, 0.22));
}
.agent-card__body {
  position: relative;
  height: 430px;
  border: 1px solid var(--surface-border-strong);
  border-top: none;
  border-radius: 0 0 20px 20px;
  background: rgba(255, 255, 255, 0.96);
  overflow: hidden;
  display: flex;
  justify-content: center;
  align-items: flex-end;
  isolation: isolate;
  box-shadow: 0 12px 24px rgba(31, 49, 38, 0.1);
}
.agent-card__body--active {
  background: linear-gradient(180deg, rgba(248, 252, 251, 0.96), rgba(225, 241, 250, 0.9));
  border-color: rgba(31, 122, 114, 0.35);
}
.agent-card__body--active::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(108deg, transparent 28%, rgba(255, 255, 255, 0.34) 50%, transparent 72%);
  transform: translateX(-125%);
  animation: bodySweep 3.6s linear infinite;
  pointer-events: none;
  z-index: 1;
}
.avatar-shell {
  position: relative;
  width: 248px;
  height: 430px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  --base-core: rgba(53, 143, 222, 0.4);
  --base-ring: rgba(39, 122, 201, 0.62);
  --base-glow: rgba(46, 133, 214, 0.28);
}
.avatar-shell::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -10px;
  width: 268px;
  height: 58px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(
    ellipse at center,
    rgba(55, 140, 219, 0.22) 0%,
    rgba(55, 140, 219, 0.11) 46%,
    rgba(55, 140, 219, 0) 74%
  );
  filter: blur(2.4px);
  animation: baseGlow 4.6s ease-in-out infinite;
  z-index: 1;
  pointer-events: none;
}
.avatar-shell::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 248px;
  height: 46px;
  transform: translateX(-50%);
  border-radius: 50%;
  border: 2px solid var(--base-ring);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.64),
    0 0 24px var(--base-glow);
  animation: basePulse 3.1s ease-in-out infinite;
  z-index: 4;
}
.avatar-base {
  position: absolute;
  left: 50%;
  bottom: 2px;
  width: 224px;
  height: 38px;
  transform: translateX(-50%);
  z-index: 2;
  pointer-events: none;
}
.avatar-base::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background:
    radial-gradient(
      ellipse at center,
      rgba(255, 255, 255, 0.96) 0%,
      rgba(255, 255, 255, 0.92) 36%,
      var(--base-core) 68%,
      rgba(38, 118, 195, 0.08) 100%
    );
  box-shadow:
    0 0 30px var(--base-core),
    0 0 10px rgba(255, 255, 255, 0.34) inset;
  z-index: 3;
}
.avatar-base::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 194px;
  height: 194px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background:
    conic-gradient(
      from 180deg,
      transparent 0deg,
      var(--base-ring) 48deg,
      transparent 112deg,
      var(--base-ring) 208deg,
      transparent 284deg,
      rgba(33, 114, 191, 0.48) 332deg,
      transparent 360deg
    );
  -webkit-mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
  mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
  opacity: 0.62;
  animation: baseRotate 10.5s linear infinite;
  z-index: 2;
}
.avatar-shell--active {
  --base-core: rgba(50, 141, 217, 0.52);
  --base-ring: rgba(42, 127, 205, 0.76);
  --base-glow: rgba(38, 130, 211, 0.38);
}
.avatar-cut {
  position: relative;
  width: 220px;
  height: 430px;
  z-index: 6;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  filter: saturate(1.04) drop-shadow(0 14px 18px rgba(24, 44, 66, 0.14));
  transform-origin: center 82%;
  animation: avatarFloat 3.2s ease-in-out infinite;
}
.avatar-cut__img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center bottom;
  display: block;
}
.agent-card--active .avatar-cut {
  animation-duration: 2.6s;
}
.highlight-list {
  position: absolute;
  right: 10px;
  top: 14px;
  display: grid;
  gap: 8px;
  width: 220px;
  z-index: 16;
}
.highlight-item {
  border-radius: 10px;
  padding: 8px 10px;
  font-size: 18px;
  line-height: 1.4;
  color: #fff;
  background: rgba(33, 49, 63, 0.92);
  box-shadow: 0 8px 16px rgba(21, 30, 40, 0.22);
}
.agent-card--active .highlight-item {
  background: rgba(31, 122, 114, 0.9);
}
.nav-btn {
  position: absolute;
  top: 212px;
  z-index: 80;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  border: none;
  font-size: 30px;
  color: var(--colorPrimary);
  background: var(--surface-base);
  box-shadow: 0 10px 20px rgba(31, 49, 38, 0.16);
  cursor: pointer;
}
.nav-btn--left {
  left: 14px;
}
.nav-btn--right {
  right: 14px;
}
.ai-fullscreen {
  position: fixed;
  inset: 0;
  z-index: 2100;
  padding: 12px;
  background: rgba(33, 49, 63, 0.24);
  backdrop-filter: blur(2px);
}
.ai-panel {
  height: 100%;
  border-radius: 22px;
  border: 1px solid var(--surface-border);
  background: linear-gradient(180deg, #f9fcfb 0%, #f0f5f2 100%);
  display: grid;
  grid-template-rows: 62px minmax(0, 1fr) 110px;
  box-shadow: var(--shadow-md);
}
.ai-panel__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 24px;
}
.ai-brand {
  font-size: 34px;
  color: var(--text-primary);
  font-weight: 700;
}
.ai-close {
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 50%;
  background: transparent;
  font-size: 30px;
  color: var(--text-secondary);
  cursor: pointer;
}
.ai-panel__center {
  padding: 8px 20px 10px;
  display: grid;
  grid-template-rows: 120px 290px minmax(0, 1fr);
  gap: 10px;
  min-height: 0;
}
.welcome-card {
  border-radius: 14px;
  background: linear-gradient(135deg, rgba(232, 244, 242, 0.95), rgba(230, 237, 250, 0.9));
  padding: 16px 18px;
  display: flex;
  justify-content: space-between;
  gap: 12px;
  border: 1px solid var(--surface-border);
}
.welcome-card__text h3 {
  margin: 0;
  font-size: 28px;
  color: var(--text-primary);
}
.welcome-card__text p {
  margin: 8px 0 0;
  font-size: 20px;
  color: var(--text-secondary);
}
.mini-avatar {
  width: 120px;
  height: 120px;
  border-radius: 14px;
  border: 1px solid var(--surface-border);
  background-color: #fff;
  background-clip: border-box;
  overflow: hidden;
}
.mini-avatar__img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center bottom;
  display: block;
}
.recommend-card {
  border-radius: 14px;
  border: 1px solid var(--surface-border);
  background: rgba(255, 255, 255, 0.86);
  padding: 12px 14px;
}
.recommend-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  color: var(--text-primary);
  font-size: 24px;
  font-weight: 700;
}
.refresh-btn {
  border: none;
  background: transparent;
  color: var(--text-secondary);
  font-size: 18px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  cursor: pointer;
}
.recommend-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px 18px;
}
.recommend-item {
  border: 1px solid var(--surface-border);
  border-radius: 8px;
  text-align: left;
  padding: 8px 10px;
  font-size: 18px;
  color: var(--text-secondary);
  background: #fff;
  cursor: pointer;
}
.recommend-item:hover {
  background: rgba(31, 122, 114, 0.08);
  color: var(--colorPrimary);
}
.chat-card {
  border-radius: 14px;
  border: 1px solid var(--surface-border);
  background: #fff;
  min-height: 0;
  overflow: hidden;
}
.chat-empty {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-tertiary);
  font-size: 18px;
}
.chat-messages {
  height: 100%;
  overflow-y: auto;
  padding: 14px;
  display: grid;
  gap: 10px;
}
.chat-row {
  display: flex;
}
.chat-row--assistant {
  justify-content: flex-start;
}
.chat-row--user {
  justify-content: flex-end;
}
.chat-bubble {
  max-width: 72%;
  border-radius: 12px;
  padding: 10px 12px;
  font-size: 18px;
  line-height: 1.5;
  white-space: pre-wrap;
  color: var(--text-primary);
  background: var(--surface-soft);
  border: 1px solid var(--surface-border);
}
.chat-row--user .chat-bubble {
  color: #fff;
  background: linear-gradient(135deg, #1f7a72 0%, #1e5bff 100%);
  border: none;
}
.ai-panel__input {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 130px;
  gap: 12px;
  padding: 14px 20px 18px;
}
.ask-input :deep(.el-input__wrapper) {
  height: 74px;
  border-radius: 18px;
  box-shadow: 0 0 0 1px var(--surface-border) inset;
  background: #fff;
}
.ask-input :deep(.el-input__inner) {
  font-size: 20px;
}
.send-btn {
  align-self: center;
  height: 56px;
  font-size: 20px;
  min-width: 98px;
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
@keyframes avatarFloat {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-8px);
  }
}
@keyframes basePulse {
  0%,
  100% {
    transform: translateX(-50%) scale(1);
    opacity: 0.88;
  }
  50% {
    transform: translateX(-50%) scale(1.045);
    opacity: 0.95;
  }
}
@keyframes baseRotate {
  from {
    transform: translate(-50%, -50%) rotate(0deg);
  }
  to {
    transform: translate(-50%, -50%) rotate(360deg);
  }
}
@keyframes baseGlow {
  0%,
  100% {
    transform: translateX(-50%) scaleX(1);
    opacity: 0.84;
  }
  50% {
    transform: translateX(-50%) scaleX(1.06);
    opacity: 0.96;
  }
}
@keyframes bodySweep {
  0% {
    transform: translateX(-125%);
  }
  100% {
    transform: translateX(135%);
  }
}
@keyframes footerSweep {
  0% {
    transform: translateX(-120%);
  }
  100% {
    transform: translateX(140%);
  }
}
@keyframes railFlow {
  0% {
    transform: translateX(0);
    opacity: 0;
  }
  20% {
    opacity: 1;
  }
  80% {
    opacity: 1;
  }
  100% {
    transform: translateX(520%);
    opacity: 0;
  }
}
@keyframes nodePulse {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.25);
  }
}
@media (max-width: 1600px) {
  .head-title {
    font-size: 34px;
    height: 60px;
  }
  .brain-intro h2 {
    font-size: 36px;
  }
  .brain-intro p {
    font-size: 22px;
  }
  .intro-sign {
    font-size: 20px;
  }
  .agent-card {
    width: 380px;
    margin-left: -190px;
  }
  .agent-card__head {
    font-size: 24px;
    height: 54px;
  }
  .agent-card__body {
    height: 390px;
  }
  .highlight-list {
    width: 184px;
  }
  .highlight-item {
    font-size: 15px;
  }
  .avatar-shell {
    width: 220px;
    height: 390px;
  }
  .avatar-cut {
    width: 202px;
    height: 390px;
  }
  .avatar-base {
    width: 194px;
    height: 34px;
  }
  .avatar-base::after {
    width: 164px;
    height: 164px;
  }
  .avatar-shell::before {
    width: 236px;
    height: 48px;
    bottom: -9px;
  }
  .avatar-shell::after {
    width: 220px;
    height: 40px;
    bottom: 0;
  }
  .brain-footer {
    margin: 0 52px;
    height: clamp(210px, 23vh, 264px);
  }
  .footer-metrics {
    padding: 12px 14px 66px;
    gap: 8px;
  }
  .footer-metric {
    min-height: 66px;
    padding: 8px 10px;
  }
  .footer-metric--period {
    min-height: 108px;
    gap: 4px;
  }
  .footer-metric__label {
    font-size: 12px;
  }
  .footer-metric__value {
    font-size: 24px;
  }
  .footer-metric__hint {
    font-size: 11px;
  }
  .footer-period-control {
    gap: 6px;
  }
  .period-btn {
    width: 20px;
    height: 20px;
    font-size: 12px;
  }
  .period-input {
    width: 50px;
    height: 22px;
    font-size: 12px;
  }
  .footer-period-slider {
    width: 100%;
  }
  .footer-rail {
    left: 14px;
    right: 14px;
    bottom: 14px;
  }
  .footer-rail__nodes {
    margin-top: 10px;
    gap: 6px;
  }
  .footer-node {
    height: 30px;
    font-size: 12px;
    gap: 6px;
  }
  .footer-node__dot {
    width: 7px;
    height: 7px;
  }
  .ai-brand {
    font-size: 28px;
  }
  .welcome-card__text h3,
  .recommend-card__head {
    font-size: 22px;
  }
  .welcome-card__text p,
  .recommend-item,
  .chat-bubble,
  .refresh-btn,
  .chat-empty,
  .ask-input :deep(.el-input__inner),
  .send-btn {
    font-size: 16px;
  }
}
</style>
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
@@ -1,85 +1,103 @@
<template>
  <div>
    <el-dialog
      v-model="dialogFormVisible"
      :title="operationType === 'approval' ? '审批' : '详情'"
      width="700px"
      @close="closeDia"
    >
            <el-form :model="form" label-width="140px" label-position="top" ref="formRef">
                <el-row>
                    <el-col :span="24">
                        <el-form-item label="流程编号:" prop="approveId">
                            <el-input v-model="form.approveId" placeholder="自动编号" clearable disabled/>
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-col :span="24">
                        <el-form-item label="申请部门:">
                            <el-select
                                disabled
                                v-model="form.approveDeptId"
                                placeholder="选择部门"
                            >
                                <el-option
                                    v-for="user in productOptions"
                                    :key="user.deptId"
                                    :label="user.deptName"
                                    :value="user.deptId"
                                />
                            </el-select>
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row v-if="!isQuotationApproval && !isPurchaseApproval">
                    <el-col :span="24">
                        <el-form-item :label="props.approveType == 5 ? '采购合同号:' : '审批事由:'" prop="approveReason">
                            <el-input v-model="form.approveReason" placeholder="请输入" clearable type="textarea" disabled/>
                        </el-form-item>
                    </el-col>
                </el-row>
            </el-form>
    <el-dialog v-model="dialogFormVisible"
               :title="operationType === 'approval' ? '审批' : '详情'"
               width="700px"
               @close="closeDia">
      <el-form :model="form"
               label-width="140px"
               label-position="top"
               ref="formRef">
        <el-row>
          <el-col :span="24">
            <el-form-item label="流程编号:"
                          prop="approveId">
              <el-input v-model="form.approveId"
                        placeholder="自动编号"
                        clearable
                        disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="申请部门:">
              <el-select disabled
                         v-model="form.approveDeptId"
                         placeholder="选择部门">
                <el-option v-for="user in productOptions"
                           :key="user.deptId"
                           :label="user.deptName"
                           :value="user.deptId" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row v-if="!isQuotationApproval && !isPurchaseApproval">
          <el-col :span="24">
            <el-form-item :label="props.approveType == 5 ? '采购合同号:' : '审批事由:'"
                          prop="approveReason">
              <el-input v-model="form.approveReason"
                        placeholder="请输入"
                        clearable
                        type="textarea"
                        disabled />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <!-- æŠ¥ä»·å®¡æ‰¹ï¼šå±•示报价详情(复用销售报价"查看详情对话框"内容结构) -->
      <div v-if="isQuotationApproval" style="margin: 10px 0 18px;">
      <div v-if="isQuotationApproval"
           style="margin: 10px 0 18px;">
        <el-divider content-position="left">报价详情</el-divider>
        <el-skeleton :loading="quotationLoading" animated>
        <el-skeleton :loading="quotationLoading"
                     animated>
          <template #template>
            <el-skeleton-item variant="h3" style="width: 30%" />
            <el-skeleton-item variant="text" style="width: 100%" />
            <el-skeleton-item variant="text" style="width: 100%" />
            <el-skeleton-item variant="h3"
                              style="width: 30%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
          </template>
          <template #default>
            <el-empty v-if="!currentQuotation || !currentQuotation.quotationNo" description="未查询到对应报价详情" />
            <el-empty v-if="!currentQuotation || !currentQuotation.quotationNo"
                      description="未查询到对应报价详情" />
            <template v-else>
              <el-descriptions :column="2" border>
              <el-descriptions :column="2"
                               border>
                <el-descriptions-item label="报价单号">{{ currentQuotation.quotationNo }}</el-descriptions-item>
                <el-descriptions-item label="客户名称">{{ currentQuotation.customer }}</el-descriptions-item>
                <el-descriptions-item label="业务员">{{ currentQuotation.salesperson }}</el-descriptions-item>
                <el-descriptions-item label="报价日期">{{ currentQuotation.quotationDate }}</el-descriptions-item>
                <el-descriptions-item label="有效期至">{{ currentQuotation.validDate }}</el-descriptions-item>
                <el-descriptions-item label="付款方式">{{ currentQuotation.paymentMethod }}</el-descriptions-item>
                <el-descriptions-item label="报价总额" :span="2">
                <el-descriptions-item label="报价总额"
                                      :span="2">
                  <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
                    Â¥{{ Number(currentQuotation.totalAmount ?? 0).toFixed(2) }}
                  </span>
                </el-descriptions-item>
              </el-descriptions>
              <div style="margin-top: 20px;">
                <h4>产品明细</h4>
                <el-table :data="currentQuotation.products || []" border style="width: 100%">
                  <el-table-column prop="product" label="产品名称" />
                  <el-table-column prop="specification" label="规格型号" />
                  <el-table-column prop="unit" label="单位" />
                  <el-table-column prop="unitPrice" label="单价">
                <el-table :data="currentQuotation.products || []"
                          border
                          style="width: 100%">
                  <el-table-column prop="product"
                                   label="产品名称" />
                  <el-table-column prop="specification"
                                   label="规格型号" />
                  <el-table-column prop="unit"
                                   label="单位" />
                  <el-table-column prop="unitPrice"
                                   label="单价">
                    <template #default="scope">Â¥{{ Number(scope.row.unitPrice ?? 0).toFixed(2) }}</template>
                  </el-table-column>
                </el-table>
              </div>
              <div v-if="currentQuotation.remark" style="margin-top: 20px;">
              <div v-if="currentQuotation.remark"
                   style="margin-top: 20px;">
                <h4>备注</h4>
                <p>{{ currentQuotation.remark }}</p>
              </div>
@@ -87,20 +105,26 @@
          </template>
        </el-skeleton>
      </div>
      <!-- é‡‡è´­å®¡æ‰¹ï¼šå±•示采购详情 -->
      <div v-if="isPurchaseApproval" style="margin: 10px 0 18px;">
      <div v-if="isPurchaseApproval"
           style="margin: 10px 0 18px;">
        <el-divider content-position="left">采购详情</el-divider>
        <el-skeleton :loading="purchaseLoading" animated>
        <el-skeleton :loading="purchaseLoading"
                     animated>
          <template #template>
            <el-skeleton-item variant="h3" style="width: 30%" />
            <el-skeleton-item variant="text" style="width: 100%" />
            <el-skeleton-item variant="text" style="width: 100%" />
            <el-skeleton-item variant="h3"
                              style="width: 30%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
          </template>
          <template #default>
            <el-empty v-if="!currentPurchase || !currentPurchase.purchaseContractNumber" description="未查询到对应采购详情" />
            <el-empty v-if="!currentPurchase || !currentPurchase.purchaseContractNumber"
                      description="未查询到对应采购详情" />
            <template v-else>
              <el-descriptions :column="2" border>
              <el-descriptions :column="2"
                               border>
                <el-descriptions-item label="采购合同号">{{ currentPurchase.purchaseContractNumber }}</el-descriptions-item>
                <el-descriptions-item label="供应商名称">{{ currentPurchase.supplierName }}</el-descriptions-item>
                <el-descriptions-item label="项目名称">{{ currentPurchase.projectName }}</el-descriptions-item>
@@ -108,24 +132,32 @@
                <el-descriptions-item label="签订日期">{{ currentPurchase.executionDate }}</el-descriptions-item>
                <el-descriptions-item label="录入日期">{{ currentPurchase.entryDate }}</el-descriptions-item>
                <el-descriptions-item label="付款方式">{{ currentPurchase.paymentMethod }}</el-descriptions-item>
                <el-descriptions-item label="合同金额" :span="2">
                <el-descriptions-item label="合同金额"
                                      :span="2">
                  <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
                    Â¥{{ Number(currentPurchase.contractAmount ?? 0).toFixed(2) }}
                  </span>
                </el-descriptions-item>
              </el-descriptions>
              <div style="margin-top: 20px;">
                <h4>产品明细</h4>
                <el-table :data="currentPurchase.productData || []" border style="width: 100%">
                  <el-table-column prop="productCategory" label="产品名称" />
                  <el-table-column prop="specificationModel" label="规格型号" />
                  <el-table-column prop="unit" label="单位" />
                  <el-table-column prop="quantity" label="数量" />
                  <el-table-column prop="taxInclusiveUnitPrice" label="含税单价">
                <el-table :data="currentPurchase.productData || []"
                          border
                          style="width: 100%">
                  <el-table-column prop="productCategory"
                                   label="产品名称" />
                  <el-table-column prop="specificationModel"
                                   label="规格型号" />
                  <el-table-column prop="unit"
                                   label="单位" />
                  <el-table-column prop="quantity"
                                   label="数量" />
                  <el-table-column prop="taxInclusiveUnitPrice"
                                   label="含税单价">
                    <template #default="scope">Â¥{{ Number(scope.row.taxInclusiveUnitPrice ?? 0).toFixed(2) }}</template>
                  </el-table-column>
                  <el-table-column prop="taxInclusiveTotalPrice" label="含税总价">
                  <el-table-column prop="taxInclusiveTotalPrice"
                                   label="含税总价">
                    <template #default="scope">Â¥{{ Number(scope.row.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</template>
                  </el-table-column>
                </el-table>
@@ -134,52 +166,138 @@
          </template>
        </el-skeleton>
      </div>
      <el-form :model="{ activities }" ref="formRef" label-position="top">
        <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical">
          <el-step
            v-for="(activity, index) in activities"
            :key="index"
                        finish-status="success"
            :title="getNodeTitle(index, activities.length)"
            :description="activity.approveNodeUser"
            :icon="getNodeIcon(activity, index)"
          >
                        <template #icon>
                            <el-icon v-if="activity.approveNodeStatus === 2" color="red" :size="22"><WarningFilled /></el-icon>
                            <el-icon v-else-if="activity.isShen" color="#1890ff" :size="22"><Edit /></el-icon>
                            <el-icon v-else-if="activity.approveNodeStatus === 1" color="#67C23A" :size="26"><Check /></el-icon>
                            <el-icon v-else color="#C0C4CC" :size="22"><MoreFilled /></el-icon>
                        </template>
      <!-- å‘货审批:展示发货详情 -->
      <div v-if="isDeliveryApproval"
           style="margin: 10px 0 18px;">
        <el-divider content-position="left">发货详情</el-divider>
        <el-skeleton :loading="deliveryLoading"
                     animated>
          <template #template>
            <el-skeleton-item variant="h3"
                              style="width: 30%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
          </template>
          <template #default>
            <el-empty v-if="!currentDelivery || !currentDelivery.shippingInfo"
                      description="未查询到对应发货详情" />
            <template v-else>
              <el-descriptions :column="2"
                               border>
                <el-descriptions-item label="销售订单">{{ currentDelivery.shippingInfo.salesContractNo || '--' }}</el-descriptions-item>
                <el-descriptions-item label="发货订单号">{{ currentDelivery.shippingInfo.shippingNo || '--' }}</el-descriptions-item>
                <el-descriptions-item label="客户名称">{{ currentDelivery.shippingInfo.customerName || '--' }}</el-descriptions-item>
                <el-descriptions-item label="发货类型">{{ currentDelivery.shippingInfo.type || '--' }}</el-descriptions-item>
                <el-descriptions-item label="发货日期">{{ currentDelivery.shippingInfo.shippingDate || '--' }}</el-descriptions-item>
                <el-descriptions-item label="审核状态">{{ currentDelivery.shippingInfo.status || '--' }}</el-descriptions-item>
                <el-descriptions-item label="发货车牌号">{{ currentDelivery.shippingInfo.shippingCarNumber || '--' }}</el-descriptions-item>
                <el-descriptions-item label="快递公司">{{ currentDelivery.shippingInfo.expressCompany || '--' }}</el-descriptions-item>
                <el-descriptions-item label="快递单号"
                                      :span="2">{{ currentDelivery.shippingInfo.expressNumber || '--' }}</el-descriptions-item>
              </el-descriptions>
              <div style="margin-top: 20px;">
                <h4>产品明细</h4>
                <el-table :data="deliveryProductList"
                          border
                          size="small"
                          style="width: 100%">
                  <el-table-column prop="batchNo"
                                   label="批号"
                                   show-overflow-tooltip />
                  <el-table-column prop="productName"
                                   label="产品名称"
                                   show-overflow-tooltip />
                  <el-table-column prop="specificationModel"
                                   label="规格型号"
                                   show-overflow-tooltip />
                  <el-table-column prop="deliveryQuantity"
                                   label="发货数量"
                                   align="center" />
                </el-table>
              </div>
              <div v-if="currentDelivery.shippingInfo.storageBlobVOs && currentDelivery.shippingInfo.storageBlobVOs.length"
                   style="margin-top: 20px;">
                <h4>发货图片</h4>
                <ImagePreview :file-list="currentDelivery.shippingInfo.storageBlobVOs" />
              </div>
            </template>
          </template>
        </el-skeleton>
      </div>
      <el-form :model="{ activities }"
               ref="formRef"
               label-position="top">
        <el-steps :active="getActiveStep()"
                  finish-status="success"
                  process-status="process"
                  align-center
                  direction="vertical">
          <el-step v-for="(activity, index) in activities"
                   :key="index"
                   finish-status="success"
                   :title="getNodeTitle(index, activities.length)"
                   :description="activity.approveNodeUser"
                   :icon="getNodeIcon(activity, index)">
            <template #icon>
              <el-icon v-if="activity.approveNodeStatus === 2"
                       color="red"
                       :size="22">
                <WarningFilled />
              </el-icon>
              <el-icon v-else-if="activity.isShen"
                       color="#1890ff"
                       :size="22">
                <Edit />
              </el-icon>
              <el-icon v-else-if="activity.approveNodeStatus === 1"
                       color="#67C23A"
                       :size="26">
                <Check />
              </el-icon>
              <el-icon v-else
                       color="#C0C4CC"
                       :size="22">
                <MoreFilled />
              </el-icon>
            </template>
            <template #title>
              <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span>
            </template>
            <template #description>
              <div class="node-user">
                <div class="avatar-wrapper">
                  <img :src="userStore.avatar" class="user-avatar" alt=""/>
                  <img :src="userStore.avatar"
                       class="user-avatar"
                       alt="" />
                </div>
                <span style="color: #000000">{{ activity.approveNodeUser }}-{{activity.isApproval}}</span>
              </div>
              <div v-if="!activity.isShen" class="node-reason">
              <div v-if="!activity.isShen"
                   class="node-reason">
                <span>审批意见:</span>{{ activity.approveNodeReason }}
              </div>
              <div v-else-if="activity.isShen">
                <el-form-item
                  :prop="'activities.' + index + '.approveNodeReason'"
                  :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]"
                >
                  <el-input v-model="activity.approveNodeReason" clearable type="textarea" :disabled="operationType === 'view'"></el-input>
                <el-form-item :prop="'activities.' + index + '.approveNodeReason'"
                              :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]">
                  <el-input v-model="activity.approveNodeReason"
                            clearable
                            type="textarea"
                            :disabled="operationType === 'view'"></el-input>
                </el-form-item>
              </div>
            </template>
          </el-step>
        </el-steps>
      </el-form>
      <template #footer v-if="operationType === 'approval'">
      <template #footer
                v-if="operationType === 'approval'">
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm(2)">不通过</el-button>
          <el-button type="primary" @click="submitForm(1)">通过</el-button>
          <el-button type="primary"
                     @click="submitForm(2)">不通过</el-button>
          <el-button type="primary"
                     @click="submitForm(1)">通过</el-button>
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
@@ -188,221 +306,329 @@
</template>
<script setup>
import { computed, getCurrentInstance, nextTick, reactive, ref, toRefs } from "vue";
import {
    approveProcessDetails,
    getDept,
    updateApproveNode
} from "@/api/collaborativeApproval/approvalProcess.js";
import useUserStore from "@/store/modules/user.js";
import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue'
import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js";
const emit = defineEmits(['close'])
const { proxy } = getCurrentInstance()
  import {
    computed,
    getCurrentInstance,
    nextTick,
    reactive,
    ref,
    toRefs,
  } from "vue";
  import {
    approveProcessDetails,
    getDept,
    updateApproveNode,
  } from "@/api/collaborativeApproval/approvalProcess.js";
  import useUserStore from "@/store/modules/user.js";
  import {
    WarningFilled,
    Edit,
    Check,
    MoreFilled,
  } from "@element-plus/icons-vue";
  import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
  import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js";
  import { getDeliveryDetailByShippingNo } from "@/api/salesManagement/deliveryLedger.js";
  import ImagePreview from "@/components/AttachmentPreview/image/index.vue";
  const emit = defineEmits(["close"]);
  const { proxy } = getCurrentInstance();
const props = defineProps({
  approveType: {
    type: [Number, String],
    default: 0
  }
})
const dialogFormVisible = ref(false);
const operationType = ref('')
const activities = ref([])
const formRef = ref(null);
const userStore = useUserStore()
const productOptions = ref([]);
const quotationLoading = ref(false)
const currentQuotation = ref({})
const purchaseLoading = ref(false)
const currentPurchase = ref({})
const isQuotationApproval = computed(() => Number(props.approveType) === 6)
const isPurchaseApproval = computed(() => Number(props.approveType) === 5)
const data = reactive({
    form: {
        approveId: "",
        approveDeptId: "",
        approveReason: "",
        checkResult: "",
    },
});
const { form } = toRefs(data);
// èŠ‚ç‚¹æ ‡é¢˜
const getNodeTitle = (index, len) => {
  if (index === len - 1) return '结束';
  return '审批';
};
// èŽ·å–å½“å‰æ¿€æ´»æ­¥éª¤
const getActiveStep = () => {
  // å¦‚果所有 isShen éƒ½ä¸º false,返回最后一个步骤(全部完成)
  const hasActive = activities.value.some(a => a.isShen === true);
  if (!hasActive) return activities.value.length;
  // å½“前节点索引
  return activities.value.findIndex(a => a.isShen  == true);
};
// æ­¥éª¤icon
const getNodeIcon = (activity, index) => {
  if (activity.approveNodeStatus === 2) return 'el-icon-warning'; // ä¸é€šè¿‡
  if (activity.isShen) return 'Edit';
  return '';
};
// æ‰“开弹框
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  currentQuotation.value = {}
  currentPurchase.value = {}
    form.value = {...row}
    // ç«‹å³æ¸…除表单验证状态(因为字段是disabled的,不需要验证)
    nextTick(() => {
        if (formRef.value) {
            formRef.value.clearValidate();
        }
    });
    // ç¡®ä¿é€‰é¡¹åŠ è½½å®ŒæˆåŽå†åŒ¹é…å€¼ç±»åž‹
    getProductOptions().then(() => {
        // ç¡®ä¿å€¼ç±»åž‹åŒ¹é…ï¼ˆå¦‚果选项已加载)
        if (productOptions.value.length > 0 && form.value.approveDeptId) {
            const matchedOption = productOptions.value.find(opt =>
                opt.deptId == form.value.approveDeptId ||
                String(opt.deptId) === String(form.value.approveDeptId)
            );
            if (matchedOption) {
                form.value.approveDeptId = matchedOption.deptId;
            }
        }
        // å†æ¬¡æ¸…除验证,确保选项加载后值匹配正确
        nextTick(() => {
            if (formRef.value) {
                formRef.value.clearValidate();
            }
        });
    });
  // æŠ¥ä»·å®¡æ‰¹ï¼šç”¨å®¡æ‰¹äº‹ç”±å­—段承载的"报价单号"去查报价列表
  if (isQuotationApproval.value) {
    const quotationNo = row?.approveReason;
    if (quotationNo) {
      quotationLoading.value = true
      getQuotationList({ quotationNo }).then((res) => {
        const records = res?.data?.records || []
        currentQuotation.value = records[0] || {}
      }).finally(() => {
        quotationLoading.value = false
      })
    }
  }
  // é‡‡è´­å®¡æ‰¹ï¼šç”¨å®¡æ‰¹äº‹ç”±å­—段承载的"采购合同号"去查采购详情
  if (isPurchaseApproval.value) {
    const purchaseContractNumber = row?.approveReason;
    if (purchaseContractNumber) {
      purchaseLoading.value = true
      getPurchaseByCode({ purchaseContractNumber }).then((res) => {
        currentPurchase.value = res
      }).catch((err) => {
        console.error('查询采购详情失败:', err)
        proxy.$modal.msgError('查询采购详情失败')
      }).finally(() => {
        purchaseLoading.value = false
      })
    }
  }
  approveProcessDetails(row.approveId).then((res) => {
    activities.value = res.data
    // å¢žåŠ isApproval字段
    activities.value.forEach(item => {
            if (item.url && item.url.includes('word')) {
                item.urlTem = item.url.replaceAll('word', 'img')
            } else {
                item.urlTem = item.url
            }
      if (item.approveNodeStatus === 2) {
        item.isApproval = '已驳回';
      } else if (item.approveNodeStatus === 1) {
        item.isApproval = '已同意';
      } else {
        item.isApproval = '未审批';
      }
    })
  })
}
const getProductOptions = () => {
    return getDept().then((res) => {
        productOptions.value = res.data;
    });
};
// æäº¤å®¡æ‰¹
const submitForm = (status) => {
  const filteredActivities = activities.value.filter(activity => activity.isShen);
  if (!filteredActivities || filteredActivities.length === 0) {
    proxy.$modal.msgError("未找到待审批的节点");
    return;
  }
  const currentActivity = filteredActivities[0];
  if (!currentActivity) {
    proxy.$modal.msgError("未找到待审批的节点");
    return;
  }
  currentActivity.approveNodeStatus = status;
  // åˆ¤æ–­æ˜¯å¦ä¸ºæœ€åŽä¸€æ­¥
  const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length-1;
  updateApproveNode({ ...currentActivity, isLast }).then(() => {
    proxy.$modal.msgSuccess("提交成功");
    closeDia();
  const props = defineProps({
    approveType: {
      type: [Number, String],
      default: 0,
    },
  });
};
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  proxy.resetForm("formRef");
  dialogFormVisible.value = false;
  quotationLoading.value = false
  currentQuotation.value = {}
  purchaseLoading.value = false
  currentPurchase.value = {}
  emit('close')
};
defineExpose({
  openDialog,
});
  const dialogFormVisible = ref(false);
  const operationType = ref("");
  const activities = ref([]);
  const formRef = ref(null);
  const userStore = useUserStore();
  const productOptions = ref([]);
  const quotationLoading = ref(false);
  const currentQuotation = ref({});
  const purchaseLoading = ref(false);
  const currentPurchase = ref({});
  const deliveryLoading = ref(false);
  const currentDelivery = ref({});
  const deliveryProductList = ref([]);
  const isQuotationApproval = computed(() => Number(props.approveType) === 6);
  const isPurchaseApproval = computed(() => Number(props.approveType) === 5);
  const isDeliveryApproval = computed(() => Number(props.approveType) === 7);
  const data = reactive({
    form: {
      approveId: "",
      approveDeptId: "",
      approveReason: "",
      checkResult: "",
    },
  });
  const { form } = toRefs(data);
  // èŠ‚ç‚¹æ ‡é¢˜
  const getNodeTitle = (index, len) => {
    if (index === len - 1) return "结束";
    return "审批";
  };
  // èŽ·å–å½“å‰æ¿€æ´»æ­¥éª¤
  const getActiveStep = () => {
    // å¦‚果所有 isShen éƒ½ä¸º false,返回最后一个步骤(全部完成)
    const hasActive = activities.value.some(a => a.isShen === true);
    if (!hasActive) return activities.value.length;
    // å½“前节点索引
    return activities.value.findIndex(a => a.isShen == true);
  };
  // æ­¥éª¤icon
  const getNodeIcon = (activity, index) => {
    if (activity.approveNodeStatus === 2) return "el-icon-warning"; // ä¸é€šè¿‡
    if (activity.isShen) return "Edit";
    return "";
  };
  // æ‰“开弹框
  const openDialog = (type, row) => {
    operationType.value = type;
    dialogFormVisible.value = true;
    currentQuotation.value = {};
    currentPurchase.value = {};
    form.value = { ...row };
    // ç«‹å³æ¸…除表单验证状态(因为字段是disabled的,不需要验证)
    nextTick(() => {
      if (formRef.value) {
        formRef.value.clearValidate();
      }
    });
    // ç¡®ä¿é€‰é¡¹åŠ è½½å®ŒæˆåŽå†åŒ¹é…å€¼ç±»åž‹
    getProductOptions().then(() => {
      // ç¡®ä¿å€¼ç±»åž‹åŒ¹é…ï¼ˆå¦‚果选项已加载)
      if (productOptions.value.length > 0 && form.value.approveDeptId) {
        const matchedOption = productOptions.value.find(
          opt =>
            opt.deptId == form.value.approveDeptId ||
            String(opt.deptId) === String(form.value.approveDeptId)
        );
        if (matchedOption) {
          form.value.approveDeptId = matchedOption.deptId;
        }
      }
      // å†æ¬¡æ¸…除验证,确保选项加载后值匹配正确
      nextTick(() => {
        if (formRef.value) {
          formRef.value.clearValidate();
        }
      });
    });
    // æŠ¥ä»·å®¡æ‰¹ï¼šç”¨å®¡æ‰¹äº‹ç”±å­—段承载的"报价单号"去查报价列表
    if (isQuotationApproval.value) {
      const quotationNo = row?.approveReason;
      if (quotationNo) {
        quotationLoading.value = true;
        getQuotationList({ quotationNo })
          .then(res => {
            const records = res?.data?.records || [];
            currentQuotation.value = records[0] || {};
          })
          .finally(() => {
            quotationLoading.value = false;
          });
      }
    }
    // é‡‡è´­å®¡æ‰¹ï¼šç”¨å®¡æ‰¹äº‹ç”±å­—段承载的"采购合同号"去查采购详情
    if (isPurchaseApproval.value) {
      const purchaseContractNumber = row?.approveReason;
      if (purchaseContractNumber) {
        purchaseLoading.value = true;
        getPurchaseByCode({ purchaseContractNumber })
          .then(res => {
            currentPurchase.value = res;
          })
          .catch(err => {
            console.error("查询采购详情失败:", err);
            proxy.$modal.msgError("查询采购详情失败");
          })
          .finally(() => {
            purchaseLoading.value = false;
          });
      }
    }
    // å‘货审批:用审批事由字段承载的"发货单号"去查发货详情
    if (isDeliveryApproval.value) {
      const deliveryNo = row?.approveReason;
      if (deliveryNo) {
        deliveryLoading.value = true;
        currentDelivery.value = {};
        deliveryProductList.value = [];
        getDeliveryDetailByShippingNo({ shippingNo: deliveryNo })
          .then(res => {
            const detailData = res?.data || res || {};
            currentDelivery.value = detailData;
            deliveryProductList.value =
              detailData.shippingProductDetailDtoList || [];
          })
          .catch(err => {
            console.error("查询发货详情失败:", err);
            proxy.$modal.msgError("查询发货详情失败");
          })
          .finally(() => {
            deliveryLoading.value = false;
          });
      }
    }
    approveProcessDetails(row.approveId).then(res => {
      activities.value = res.data;
      // å¢žåŠ isApproval字段
      activities.value.forEach(item => {
        if (item.url && item.url.includes("word")) {
          item.urlTem = item.url.replaceAll("word", "img");
        } else {
          item.urlTem = item.url;
        }
        if (item.approveNodeStatus === 2) {
          item.isApproval = "已驳回";
        } else if (item.approveNodeStatus === 1) {
          item.isApproval = "已同意";
        } else {
          item.isApproval = "未审批";
        }
      });
    });
  };
  const getDeliveryProductInfoList = () => {
    const row = currentDelivery.value;
    if (!row) return [];
    const normalizeBatchNoList = value => {
      if (Array.isArray(value)) return value;
      if (typeof value === "string" && value.includes(",")) {
        return value
          .split(",")
          .map(item => item.trim())
          .filter(Boolean);
      }
      return value ? [value] : [];
    };
    const detailList = deliveryProductList.value.length
      ? deliveryProductList.value
      : [
          row.batchNoDetailList,
          row.batchNoList,
          row.shippingBatchList,
          row.shippingInfoDetailList,
          row.detailList,
          row.batchDetailList,
        ].find(value => Array.isArray(value) && value.length);
    const batchNoList = normalizeBatchNoList(row.batchNo);
    const toTableRow = (item = {}) => ({
      batchNo:
        typeof item === "string" || typeof item === "number"
          ? item
          : item.batchNo ?? item.batchNumber ?? row.batchNo ?? "--",
      productName: item.productName ?? row.productName ?? "--",
      specificationModel:
        item.specificationModel ?? item.model ?? row.specificationModel ?? "--",
      deliveryQuantity:
        item.deliveryQuantity ??
        item.quantity ??
        item.shippingQuantity ??
        row.deliveryQuantity ??
        row.quantity ??
        "--",
    });
    if (detailList?.length) {
      return detailList.map(toTableRow);
    }
    if (batchNoList.length) {
      return batchNoList.map(batchNo => toTableRow({ batchNo }));
    }
    return [toTableRow()];
  };
  const getApprovalStatusText = status => {
    const statusMap = {
      0: "待审核",
      1: "审核通过",
      2: "审核拒绝",
      3: "审核中",
    };
    return statusMap[status] || "待审核";
  };
  const getProductOptions = () => {
    return getDept().then(res => {
      productOptions.value = res.data;
    });
  };
  // æäº¤å®¡æ‰¹
  const submitForm = status => {
    const filteredActivities = activities.value.filter(
      activity => activity.isShen
    );
    if (!filteredActivities || filteredActivities.length === 0) {
      proxy.$modal.msgError("未找到待审批的节点");
      return;
    }
    const currentActivity = filteredActivities[0];
    if (!currentActivity) {
      proxy.$modal.msgError("未找到待审批的节点");
      return;
    }
    currentActivity.approveNodeStatus = status;
    // åˆ¤æ–­æ˜¯å¦ä¸ºæœ€åŽä¸€æ­¥
    const isLast =
      activities.value.findIndex(a => a.isShen) === activities.value.length - 1;
    updateApproveNode({ ...currentActivity, isLast }).then(() => {
      proxy.$modal.msgSuccess("提交成功");
      closeDia();
    });
  };
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
    quotationLoading.value = false;
    currentQuotation.value = {};
    purchaseLoading.value = false;
    currentPurchase.value = {};
    emit("close");
  };
  defineExpose({
    openDialog,
  });
</script>
<style scoped>
.node-user {
  margin: 10px 0;
  font-size: 16px;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 8px;
}
.node-status {
  color: #1890ff;
  margin-left: 8px;
  font-size: 14px;
}
.node-reason {
  font-size: 15px;
  color: #333;
  margin: 10px 0;
}
.user-avatar {
    cursor: pointer;
    width: 30px;
    height: 30px;
    border-radius: 50px;
}
.signImg {
    cursor: pointer;
    width: 200px;
    height: 60px;
}
  .node-user {
    margin: 10px 0;
    font-size: 16px;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .node-status {
    color: #1890ff;
    margin-left: 8px;
    font-size: 14px;
  }
  .node-reason {
    font-size: 15px;
    color: #333;
    margin: 10px 0;
  }
  .user-avatar {
    cursor: pointer;
    width: 30px;
    height: 30px;
    border-radius: 50px;
  }
  .signImg {
    cursor: pointer;
    width: 200px;
    height: 60px;
  }
</style>
src/views/customerService/expiryAfterSales/components/formDia.vue
@@ -20,7 +20,7 @@
                                v-model="form.productName"
                                placeholder="请输入产品名称"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('productName')"
                            />
                        </el-form-item>
                    </el-col>
@@ -30,7 +30,7 @@
                                v-model="form.batchNumber"
                                placeholder="请输入产品批号"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('batchNumber')"
                            />
                        </el-form-item>
                    </el-col>
@@ -46,7 +46,7 @@
                                type="date"
                                placeholder="请选择临期日期"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('expiryDate')"
                            />
                        </el-form-item>
                    </el-col>
@@ -57,7 +57,7 @@
                                :min="0"
                                placeholder="请输入库存数量"
                                style="width: 100%"
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('stockQuantity')"
                            />
                        </el-form-item>
                    </el-col>
@@ -69,7 +69,7 @@
                                v-model="form.customerName"
                                placeholder="请输入客户名称"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('customerName')"
                            />
                        </el-form-item>
                    </el-col>
@@ -79,7 +79,7 @@
                                v-model="form.contactPhone"
                                placeholder="请输入联系电话"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('contactPhone')"
                            />
                        </el-form-item>
                    </el-col>
@@ -91,7 +91,7 @@
                                v-model="form.problemDesc"
                                placeholder="请输入问题描述"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('problemDesc')"
                                type="textarea"
                                :rows="3"
                            />
@@ -105,7 +105,7 @@
                                v-model="form.handlerId"
                                placeholder="请选择处理人"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('handlerId')"
                                style="width: 100%"
                            >
                                <el-option
@@ -127,7 +127,7 @@
                                type="date"
                                placeholder="请选择处理日期"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('handleDate')"
                            />
                        </el-form-item>
                    </el-col>
@@ -139,7 +139,7 @@
                                v-model="form.handleResult"
                                placeholder="请输入处理结果"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('handleResult')"
                                type="textarea"
                                :rows="3"
                            />
@@ -175,6 +175,8 @@
            return '新增临期售后';
        case 'edit':
            return '编辑临期售后';
        case 'handle':
            return '处理临期售后';
        case 'view':
            return '查看临期售后';
        default:
@@ -212,6 +214,13 @@
})
const { form, rules } = toRefs(data);
const userList = ref([])
const handleEditableFields = ["handlerId", "handleDate", "handleResult"];
const isFieldDisabled = (field) => {
    if (operationType.value === "view") return true;
    if (operationType.value === "handle") return !handleEditableFields.includes(field);
    return false;
};
// æ‰“开弹框
const openDialog = (type, row) => {
@@ -242,7 +251,7 @@
    } else {
        // ç¼–辑或查看时填充数据
        form.value = { ...row };
        if (type === 'edit' && !form.value.handlerId) {
        if (type === 'handle' && !form.value.handlerId) {
            form.value.handlerId = userStore.id;
            form.value.handleDate = getCurrentDate();
        }
@@ -250,36 +259,49 @@
}
const submitForm = () => {
    if (operationType.value === "handle") {
        if (!form.value.handlerId || !form.value.handleDate || !form.value.handleResult) {
            proxy.$modal.msgWarning("请填写处理人、处理日期和处理结果");
            return;
        }
        handleSubmit();
        return;
    }
    proxy.$refs["formRef"].validate(valid => {
        if (valid) {
            const submitData = {
                id: form.value.id,
                productName: form.value.productName,
                batchNumber: form.value.batchNumber,
                expireDate: form.value.expiryDate,
                stockQuantity: form.value.stockQuantity,
                customerName: form.value.customerName,
                contactPhone: form.value.contactPhone,
                disRes: form.value.problemDesc,
                status: form.value.status,
                disposeUserId: form.value.handlerId,
                disposeNickName: userList.value.find(item => item.userId === form.value.handlerId)?.nickName,
                disposeResult: form.value.handleResult,
                disDate: form.value.handleDate
            };
            const apiCall = operationType.value === 'add' ? expiryAfterSalesAdd : expiryAfterSalesUpdate;
            apiCall(submitData).then(() => {
                proxy.$modal.msgSuccess(operationType.value === 'add' ? "新增成功" : "更新成功");
                closeDia();
            }).catch(error => {
                console.error('提交数据失败:', error);
                proxy.$modal.msgError('提交数据失败,请稍后重试');
            });
            handleSubmit();
        }
    });
}
const handleSubmit = () => {
    const submitData = {
        id: form.value.id,
        productName: form.value.productName,
        batchNumber: form.value.batchNumber,
        expireDate: form.value.expiryDate,
        stockQuantity: form.value.stockQuantity,
        customerName: form.value.customerName,
        contactPhone: form.value.contactPhone,
        disRes: form.value.problemDesc,
        status: operationType.value === "handle" ? 2 : form.value.status,
        disposeUserId: form.value.handlerId,
        disposeNickName: userList.value.find(item => item.userId === form.value.handlerId)?.nickName,
        disposeResult: form.value.handleResult,
        disDate: form.value.handleDate
    };
    const apiCall = operationType.value === 'add' ? expiryAfterSalesAdd : expiryAfterSalesUpdate;
    apiCall(submitData).then(() => {
        const successText = operationType.value === "add" ? "新增成功" : operationType.value === "handle" ? "处理成功" : "更新成功";
        proxy.$modal.msgSuccess(successText);
        closeDia();
    }).catch(error => {
        console.error('提交数据失败:', error);
        proxy.$modal.msgError('提交数据失败,请稍后重试');
    });
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
    proxy.resetForm("formRef");
src/views/customerService/expiryAfterSales/index.vue
@@ -39,7 +39,7 @@
                <el-button type="danger" @click="handleDelete">删除</el-button>
            </div>
        </div>
        <div class="table_list">
            <PIMTable
                rowKey="id"
@@ -60,7 +60,7 @@
                <template #operation="{ row }">
                    <el-button type="primary" link @click="openForm('view', row)">查看</el-button>
                    <el-button type="primary" link @click="openForm('edit', row)" v-if="row.status === 1">编辑</el-button>
                    <el-button type="primary" link @click="openForm('handle', row)" v-if="row.status === 1">处理</el-button>
                </template>
            </PIMTable>
        </div>
@@ -201,7 +201,7 @@
        current: page.value.current,
        size: page.value.size
    };
    expiryAfterSalesListPage(queryParams).then(res => {
        // æ˜ å°„后端返回数据到前端表格
        tableData.value = res.data.records.map(item => ({
src/views/customerService/feedbackRegistration/components/formDia.vue
@@ -1,490 +1,507 @@
<template>
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        title="新增售后单"
        width="90%"
        @close="closeDia"
    >
    <el-dialog v-model="dialogFormVisible"
               title="新增售后单"
               width="90%"
               @close="closeDia">
      <div>
        <span class="descriptions">基础资料</span>
        <el-form
            :model="form"
            label-width="140px"
            label-position="top"
            :rules="rules"
            ref="formRef"
        >
        <el-form :model="form"
                 label-width="140px"
                 label-position="top"
                 :rules="rules"
                 ref="formRef">
          <el-row :gutter="30">
            <el-col :span="4">
              <el-form-item label="客户名称:" prop="customerName">
                <el-select
                    v-model="form.customerName"
                    filterable
                    @change="customerNameChange"
                >
                  <el-option
                      v-for="item in customerNameOptions"
                      :key="item.value"
                      :label="item.label"
                      :value="item.value"
                  />
              <el-form-item label="客户名称:"
                            prop="customerName">
                <el-select v-model="form.customerName"
                           filterable
                           @change="customerNameChange">
                  <el-option v-for="item in customerNameOptions"
                             :key="item.value"
                             :label="item.label"
                             :value="item.value" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="4">
              <el-form-item label="售后类型:" prop="serviceType">
                <el-select
                    v-model="form.serviceType"
                    filterable
                >
                  <el-option
                      v-for="dict in serviceTypeOptions"
                      :key="dict.value"
                      :label="dict.label"
                      :value="dict.value"
                  />
              <el-form-item label="售后类型:"
                            prop="serviceType">
                <el-select v-model="form.serviceType"
                           filterable>
                  <el-option v-for="dict in serviceTypeOptions"
                             :key="dict.value"
                             :label="dict.label"
                             :value="dict.value" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="4">
              <el-form-item label="关联销售单号:" prop="salesContractNo">
                <el-select
                    v-model="form.salesContractNo"
                    @change="associatedSalesOrderNumberChange"
                    filterable
                >
                  <el-option
                      v-for="item in associatedSalesOrderNumberOptions"
                      :key="item.value"
                      :label="item.label"
                      :value="item.value"
                  />
              <el-form-item label="关联销售单号:"
                            prop="salesContractNo">
                <el-select v-model="form.salesContractNo"
                           @change="associatedSalesOrderNumberChange"
                           filterable>
                  <el-option v-for="item in associatedSalesOrderNumberOptions"
                             :key="item.value"
                             :label="item.label"
                             :value="item.value" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="4">
              <el-form-item label="紧急程度:" prop="urgency">
                <el-select
                    v-model="form.urgency"
                    filterable
                >
                  <el-option
                      v-for="dict in urgencyOptions"
                      :key="dict.value"
                      :label="dict.label"
                      :value="dict.value"
                  />
              <el-form-item label="紧急程度:"
                            prop="urgency">
                <el-select v-model="form.urgency"
                           filterable>
                  <el-option v-for="dict in urgencyOptions"
                             :key="dict.value"
                             :label="dict.label"
                             :value="dict.value" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="4">
              <el-form-item label="问题描述:" prop="proDesc">
                <el-input
                    v-model="form.proDesc"
                    placeholder="请输入问题描述"
                />
              <el-form-item label="问题描述:"
                            prop="proDesc">
                <el-input v-model="form.proDesc"
                          placeholder="请输入问题描述" />
              </el-form-item>
            </el-col>
          </el-row>
        </el-form>
        <hr>
          <div style="padding-top: 20px">
            <div style="display: flex; justify-content: space-between">
              <span class="descriptions">关联产品</span>
            <el-button
              type="primary"
              style="margin-right: 12px; margin-bottom: 10px"
              @click="isShowProductSelectDialog = true"
            >
        <div style="padding-top: 20px">
          <div style="display: flex; justify-content: space-between">
            <span class="descriptions">关联产品</span>
            <el-button type="primary"
                       style="margin-right: 12px; margin-bottom: 10px"
                       @click="isShowProductSelectDialog = true">
              é€‰æ‹©äº§å“
            </el-button>
            </div>
            <PIMTable
                :isShowPagination="false"
                rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
            >
              <template #approveStatus="{ row }">
                <el-tag :type="getApproveStatusType(row)" size="small">
                  {{ getApproveStatusText(row) }}
                </el-tag>
              </template>
              <template #shippingStatus="{ row }">
                <el-tag :type="getShippingStatusType(row)" size="small">
                  {{ getShippingStatusText(row) }}
                </el-tag>
              </template>
            </PIMTable>
          </div>
          <PIMTable :isShowPagination="false"
                    rowKey="id"
                    :column="tableColumn"
                    :tableData="tableData">
            <template #approveStatus="{ row }">
              <el-tag :type="getApproveStatusType(row)"
                      size="small">
                {{ getApproveStatusText(row) }}
              </el-tag>
            </template>
            <template #shippingStatus="{ row }">
              <el-tag :type="getShippingStatusType(row)"
                      size="small">
                {{ getShippingStatusText(row) }}
              </el-tag>
            </template>
          </PIMTable>
        </div>
      </div>
            <template #footer>
                <div class="dialog-footer">
                    <el-button type="primary" @click="submitForm">确认</el-button>
                    <el-button @click="closeDia">取消</el-button>
                </div>
            </template>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- é€‰æ‹©äº§å“å¼¹çª— -->
    <ProductSelectDialog
      v-model="isShowProductSelectDialog"
      :products="currentSalesOrderProducts"
      :selected-ids="currentSelectedProductIds"
      @confirm="handleSelectProducts"
    />
    <ProductSelectDialog v-model="isShowProductSelectDialog"
                         :products="currentSalesOrderProducts"
                         :selected-ids="currentSelectedProductIds"
                         @confirm="handleSelectProducts" />
  </div>
</template>
<script setup>
import { ref, reactive, toRefs, getCurrentInstance, computed } from "vue";
import ProductSelectDialog from "./ProductSelectDialog.vue";
import useUserStore from "@/store/modules/user.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
import {afterSalesServiceAdd, afterSalesServiceUpdate, getAllCustomerList, getSalesLedger } from "@/api/customerService/index.js";
import { getCurrentDate } from "@/utils/index.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
const operationType = ref('')
const formRef = ref(null)
const customerNameOptions = ref([])
const userStore = useUserStore();
  import { ref, reactive, toRefs, getCurrentInstance, computed } from "vue";
  import ProductSelectDialog from "./ProductSelectDialog.vue";
  import useUserStore from "@/store/modules/user.js";
  import { userListNoPageByTenantId } from "@/api/system/user.js";
  import {
    afterSalesServiceAdd,
    afterSalesServiceUpdate,
    getAllCustomerList,
    getSalesLedger,
  } from "@/api/customerService/index.js";
  import { getCurrentDate } from "@/utils/index.js";
  const { proxy } = getCurrentInstance();
  const emit = defineEmits(["close"]);
  const dialogFormVisible = ref(false);
  const operationType = ref("");
  const formRef = ref(null);
  const customerNameOptions = ref([]);
  const userStore = useUserStore();
const data = reactive({
    form: {
    topic: "",
    serviceType: "",
    urgency: "",
    salesLedgerId: null,
    productModelIds: "",
    customerId: null,
    salesContractNo: "",
    proDesc: "",
    customerName: ""
    },
    rules: {
    customerName: [{required: true, message: "请选择客户名称", trigger: "change"}],
    serviceType: [{required: true, message: "请选择售后类型", trigger: "change"}],
    urgency: [{required: true, message: "请选择紧急程度", trigger: "change"}],
        feedbackDate: [{required: true, message: "请选择", trigger: "change"}],
    }
})
// è‡ªå®šä¹‰æ ¡éªŒå‡½æ•°ï¼šåˆ¤æ–­æ˜¯å¦éœ€è¦æ ¡éªŒå”®åŽç¼–号
const { form, rules } = toRefs(data);
const userList = ref([])
const formatCurrency = (val) => {
  if (val === null || val === undefined || val === '') return '-'
  const num = Number(val)
  return Number.isFinite(num) ? num.toFixed(2) : '-'
}
const { post_sale_waiting_list, degree_of_urgency } = proxy.useDict(
  "post_sale_waiting_list",
  "degree_of_urgency"
);
const serviceTypeOptions = computed(() => post_sale_waiting_list?.value || []);
const urgencyOptions = computed(() => degree_of_urgency?.value || []);
const getProductRowId = (row) => {
  return row?.id ?? row?.productModelId ?? row?.modelId ?? `${row?.productCategory || row?.productName || ""}-${row?.specificationModel || row?.model || ""}-${row?.unit || ""}`
}
const normalizeProductRow = (row) => {
  return {
    ...row,
    id: getProductRowId(row),
    productCategory: row?.productCategory ?? row?.productName ?? '',
    specificationModel: row?.specificationModel ?? row?.model ?? '',
    unit: row?.unit ?? '',
    approveStatus: row?.approveStatus ?? null,
    shippingStatus: row?.shippingStatus ?? '',
    expressCompany: row?.expressCompany ?? '',
    expressNumber: row?.expressNumber ?? '',
    shippingCarNumber: row?.shippingCarNumber ?? '',
    shippingDate: row?.shippingDate ?? '',
    quantity: row?.quantity ?? 0,
    taxRate: row?.taxRate ?? 0,
    taxInclusiveUnitPrice: row?.taxInclusiveUnitPrice ?? 0,
    taxInclusiveTotalPrice: row?.taxInclusiveTotalPrice ?? 0,
    taxExclusiveTotalPrice: row?.taxExclusiveTotalPrice ?? 0,
    noQuantity: row?.noQuantity ?? 0,
  }
}
const tableColumn = ref([
  { label: "产品大类", prop: "productCategory" },
  { label: "规格型号", prop: "specificationModel" },
  { label: "单位", prop: "unit" },
  {
    label: "产品状态",
    prop: "approveStatus",
    width: 100,
    align: "center",
    dataType: "slot",
    slot: "approveStatus",
  },
  {
    label: "发货状态",
    align: "center",
    width: 140,
    dataType: "slot",
    slot: "shippingStatus",
  },
  { label: "快递公司", prop: "expressCompany", width: 140 },
  { label: "快递单号", prop: "expressNumber", width: 160 },
  { label: "发货车牌", prop: "shippingCarNumber", minWidth: 100, align: "center" },
  { label: "发货日期", prop: "shippingDate", minWidth: 100, align: "center" },
  { label: "数量", prop: "quantity", width: 100 },
  { label: "税率(%)", prop: "taxRate", width: 100 },
  {
    label: "含税单价(元)",
    prop: "taxInclusiveUnitPrice",
    width: 160,
    formatData: formatCurrency,
  },
  {
    label: "含税总价(元)",
    prop: "taxInclusiveTotalPrice",
    width: 160,
    formatData: formatCurrency,
  },
  {
    label: "不含税总价(元)",
    prop: "taxExclusiveTotalPrice",
    width: 160,
    formatData: formatCurrency,
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: 'right',
    operation: [
      {
        name: "删除",
        type: "text",
        clickFun: (row) => {
          tableData.value = tableData.value.filter(i => getProductRowId(i) !== getProductRowId(row))
        },
      },
    ],
  },
])
const tableData = ref([])
// é€‰æ‹©äº§å“å¼¹çª—
const isShowProductSelectDialog = ref(false)
const handleSelectProducts = (rows) => {
  if (!Array.isArray(rows)) return
  const existingIds = new Set(tableData.value.map(i => String(getProductRowId(i))))
  const mapped = rows
    .map(normalizeProductRow)
    .filter(r => !existingIds.has(String(getProductRowId(r))))
  tableData.value = tableData.value.concat(mapped)
}
const currentSelectedProductIds = computed(() => {
  return tableData.value.map(item => getProductRowId(item)).filter(item => item !== undefined && item !== null && item !== '')
})
const associatedSalesOrderNumberChange = () => {
  const opt = associatedSalesOrderNumberOptions.value.find(
    (item) => item.value === form.value.salesContractNo
  )
  tableData.value = (opt?.productData || []).map(normalizeProductRow)
  form.value.salesLedgerId = opt?.id || null
}
const associatedSalesOrderNumberOptions = ref([])
const currentSalesOrderProducts = computed(() => {
  const opt = associatedSalesOrderNumberOptions.value.find(
    (item) => item.value === form.value.salesContractNo
  )
  return (opt?.productData || []).map(normalizeProductRow)
})
const customerNameChange = (val) => {
  form.value.salesContractNo = "";
  form.value.salesLedgerId = null;
  tableData.value = [];
  associatedSalesOrderNumberOptions.value = [];
  const opt = customerNameOptions.value.find(item => item.value === val);
  if (opt) {
    form.value.customerId = opt.id;
  } else {
    form.value.customerId = null;
  }
  getSalesLedger({
    customerName: form.value.customerName
  }).then(res => {
    if(res.code === 200){
      associatedSalesOrderNumberOptions.value = res.data.records.map(item => ({
        label: item.salesContractNo,
        value: item.salesContractNo,
        productData:item.productData,
        id: item.id
      }))
    }
  })
}
const getApproveStatusText = (row) => {
  if (!row) return '不足'
  if (row.approveStatus === 1 && (!row.shippingDate || !row.shippingCarNumber)) {
    return '充足'
  }
  if (row.approveStatus === 0 && (row.shippingDate || row.shippingCarNumber)) {
    return '已出库'
  }
  return '不足'
}
const getApproveStatusType = (row) => {
  const statusText = getApproveStatusText(row)
  return statusText === '不足' ? 'danger' : 'success'
}
const getShippingStatusText = (row) => {
  if (!row) return '待发货'
  if (row.shippingDate || row.shippingCarNumber) {
    return '已发货'
  }
  const status = row.shippingStatus
  if (status === null || status === undefined || status === '') {
    return '待发货'
  }
  const map = {
    '待发货': '待发货',
    '待审核': '待审核',
    '审核中': '审核中',
    '审核拒绝': '审核拒绝',
    '审核通过': '审核通过',
    '已发货': '已发货'
  }
  return map[String(status).trim()] || '待发货'
}
const getShippingStatusType = (row) => {
  if (!row) return 'info'
  if (row.shippingDate || row.shippingCarNumber) {
    return 'success'
  }
  const status = row.shippingStatus
  if (status === null || status === undefined || status === '') {
    return 'info'
  }
  const map = {
    '待发货': 'info',
    '待审核': 'warning',
    '审核中': 'warning',
    '审核拒绝': 'danger',
    '审核通过': 'success',
    '已发货': 'success'
  }
  return map[String(status).trim()] || 'info'
}
// æ‰“开弹框
const openDialog =async (type, row) => {
  // è¯·æ±‚多个接口,获取数据
  let res = await getAllCustomerList({
    current: 1,
  size: 1000,
  total: 0,
  const data = reactive({
    form: {
      topic: "",
      serviceType: "",
      urgency: "",
      salesLedgerId: null,
      productModelIds: "",
      customerId: null,
      salesContractNo: "",
      proDesc: "",
      customerName: "",
    },
    rules: {
      customerName: [
        { required: true, message: "请选择客户名称", trigger: "change" },
      ],
      serviceType: [
        { required: true, message: "请选择售后类型", trigger: "change" },
      ],
      urgency: [{ required: true, message: "请选择紧急程度", trigger: "change" }],
      feedbackDate: [{ required: true, message: "请选择", trigger: "change" }],
    },
  });
  if(res.data.records){
    customerNameOptions.value = res.data.records.map(item => ({
      label: item.customerName,
      value: item.customerName,
      id: item.id
    }));
  }
  // è‡ªå®šä¹‰æ ¡éªŒå‡½æ•°ï¼šåˆ¤æ–­æ˜¯å¦éœ€è¦æ ¡éªŒå”®åŽç¼–号
  operationType.value = type;
  dialogFormVisible.value = true;
    form.value = {}
    proxy.resetForm("formRef");
    form.value.checkUserId = userStore.id;
    form.value.feedbackDate = getCurrentDate();
  // æ–°å¢žæ—¶æ¸…空已选关联产品
  if (type === "add") {
    tableData.value = []
  }
    userListNoPageByTenantId().then((res) => {
        userList.value = res.data;
    });
    if (type === "edit") {
        form.value = {...row}
    if (form.value.customerName) {
      const res = await getSalesLedger({ customerName: form.value.customerName })
      if (res?.code === 200) {
        console.log(res)
        associatedSalesOrderNumberOptions.value = (res.data?.records || []).map(item => ({
  const { form, rules } = toRefs(data);
  const userList = ref([]);
  const formatCurrency = val => {
    if (val === null || val === undefined || val === "") return "-";
    const num = Number(val);
    return Number.isFinite(num) ? num.toFixed(2) : "-";
  };
  const { post_sale_waiting_list, degree_of_urgency } = proxy.useDict(
    "post_sale_waiting_list",
    "degree_of_urgency"
  );
  const serviceTypeOptions = computed(() => post_sale_waiting_list?.value || []);
  const urgencyOptions = computed(() => degree_of_urgency?.value || []);
  const getProductRowId = row => {
    return (
      row?.id ??
      row?.productModelId ??
      row?.modelId ??
      `${row?.productCategory || row?.productName || ""}-${
        row?.specificationModel || row?.model || ""
      }-${row?.unit || ""}`
    );
  };
  const normalizeProductRow = row => {
    return {
      ...row,
      id: getProductRowId(row),
      productCategory: row?.productCategory ?? row?.productName ?? "",
      specificationModel: row?.specificationModel ?? row?.model ?? "",
      unit: row?.unit ?? "",
      approveStatus: row?.approveStatus ?? null,
      shippingStatus: row?.shippingStatus ?? "",
      expressCompany: row?.expressCompany ?? "",
      expressNumber: row?.expressNumber ?? "",
      shippingCarNumber: row?.shippingCarNumber ?? "",
      shippingDate: row?.shippingDate ?? "",
      quantity: row?.quantity ?? 0,
      taxRate: row?.taxRate ?? 0,
      taxInclusiveUnitPrice: row?.taxInclusiveUnitPrice ?? 0,
      taxInclusiveTotalPrice: row?.taxInclusiveTotalPrice ?? 0,
      taxExclusiveTotalPrice: row?.taxExclusiveTotalPrice ?? 0,
      noQuantity: row?.noQuantity ?? 0,
    };
  };
  const tableColumn = ref([
    { label: "产品大类", prop: "productCategory" },
    { label: "规格型号", prop: "specificationModel" },
    { label: "单位", prop: "unit" },
    {
      label: "产品状态",
      prop: "approveStatus",
      width: 100,
      align: "center",
      dataType: "slot",
      slot: "approveStatus",
    },
    {
      label: "发货状态",
      align: "center",
      width: 140,
      dataType: "slot",
      slot: "shippingStatus",
    },
    { label: "快递公司", prop: "expressCompany", width: 140 },
    { label: "快递单号", prop: "expressNumber", width: 160 },
    {
      label: "发货车牌",
      prop: "shippingCarNumber",
      minWidth: 100,
      align: "center",
    },
    { label: "发货日期", prop: "shippingDate", minWidth: 100, align: "center" },
    { label: "数量", prop: "quantity", width: 100 },
    { label: "税率(%)", prop: "taxRate", width: 100 },
    {
      label: "含税单价(元)",
      prop: "taxInclusiveUnitPrice",
      width: 160,
      formatData: formatCurrency,
    },
    {
      label: "含税总价(元)",
      prop: "taxInclusiveTotalPrice",
      width: 160,
      formatData: formatCurrency,
    },
    {
      label: "不含税总价(元)",
      prop: "taxExclusiveTotalPrice",
      width: 160,
      formatData: formatCurrency,
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      operation: [
        {
          name: "删除",
          type: "text",
          clickFun: row => {
            tableData.value = tableData.value.filter(
              i => getProductRowId(i) !== getProductRowId(row)
            );
          },
        },
      ],
    },
  ]);
  const tableData = ref([]);
  // é€‰æ‹©äº§å“å¼¹çª—
  const isShowProductSelectDialog = ref(false);
  const handleSelectProducts = rows => {
    if (!Array.isArray(rows)) return;
    const existingIds = new Set(
      tableData.value.map(i => String(getProductRowId(i)))
    );
    const mapped = rows
      .map(normalizeProductRow)
      .filter(r => !existingIds.has(String(getProductRowId(r))));
    tableData.value = tableData.value.concat(mapped);
  };
  const currentSelectedProductIds = computed(() => {
    return tableData.value
      .map(item => getProductRowId(item))
      .filter(item => item !== undefined && item !== null && item !== "");
  });
  const associatedSalesOrderNumberChange = () => {
    const opt = associatedSalesOrderNumberOptions.value.find(
      item => item.value === form.value.salesContractNo
    );
    tableData.value = (opt?.productData || []).map(normalizeProductRow);
    form.value.salesLedgerId = opt?.id || null;
  };
  const associatedSalesOrderNumberOptions = ref([]);
  const currentSalesOrderProducts = computed(() => {
    const opt = associatedSalesOrderNumberOptions.value.find(
      item => item.value === form.value.salesContractNo
    );
    return (opt?.productData || []).map(normalizeProductRow);
  });
  const customerNameChange = val => {
    form.value.salesContractNo = "";
    form.value.salesLedgerId = null;
    tableData.value = [];
    associatedSalesOrderNumberOptions.value = [];
    const opt = customerNameOptions.value.find(item => item.value === val);
    if (opt) {
      form.value.customerId = opt.id;
    } else {
      form.value.customerId = null;
    }
    getSalesLedger({
      customerName: form.value.customerName,
    }).then(res => {
      if (res.code === 200) {
        associatedSalesOrderNumberOptions.value = res.data.records.map(item => ({
          label: item.salesContractNo,
          value: item.salesContractNo,
          productData: item.productData,
          id: item.id
        }))
          id: item.id,
        }));
      }
    });
  };
  const getApproveStatusText = row => {
    if (!row) return "不足";
    if (
      row.approveStatus === 1 &&
      (!row.shippingDate || !row.shippingCarNumber)
    ) {
      return "充足";
    }
    console.log(form.value)
    }
}
const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
        if (valid) {
      // åŒ¹é…äº§å“åž‹å·IDs
      form.value.productModelIds = tableData.value.map(item => item.id).join(",")
            if (operationType.value === "add") {
                afterSalesServiceAdd(form.value).then(response => {
                    proxy.$modal.msgSuccess("新增成功")
                    closeDia()
                })
            } else {
                afterSalesServiceUpdate(form.value).then(response => {
                    proxy.$modal.msgSuccess("修改成功")
                    closeDia()
                })
            }
        }
    })
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
    proxy.resetForm("formRef");
  dialogFormVisible.value = false;
  emit('close')
};
defineExpose({
  openDialog,
});
    if (row.approveStatus === 0 && (row.shippingDate || row.shippingCarNumber)) {
      return "已出库";
    }
    return "不足";
  };
  const getApproveStatusType = row => {
    const statusText = getApproveStatusText(row);
    return statusText === "不足" ? "danger" : "success";
  };
  const getShippingStatusText = row => {
    if (!row) return "待发货";
    if (row.shippingDate || row.shippingCarNumber) {
      return "已发货";
    }
    const status = row.shippingStatus;
    if (status === null || status === undefined || status === "") {
      return "待发货";
    }
    const map = {
      å¾…发货: "待发货",
      å¾…审核: "待审核",
      å®¡æ ¸ä¸­: "审核中",
      å®¡æ ¸æ‹’绝: "审核拒绝",
      å®¡æ ¸é€šè¿‡: "审核通过",
      å·²å‘è´§: "已发货",
    };
    return map[String(status).trim()] || "待发货";
  };
  const getShippingStatusType = row => {
    if (!row) return "info";
    if (row.shippingDate || row.shippingCarNumber) {
      return "success";
    }
    const status = row.shippingStatus;
    if (status === null || status === undefined || status === "") {
      return "info";
    }
    const map = {
      å¾…发货: "info",
      å¾…审核: "warning",
      å®¡æ ¸ä¸­: "warning",
      å®¡æ ¸æ‹’绝: "danger",
      å®¡æ ¸é€šè¿‡: "success",
      å·²å‘è´§: "success",
    };
    return map[String(status).trim()] || "info";
  };
  // æ‰“开弹框
  const openDialog = async (type, row) => {
    // è¯·æ±‚多个接口,获取数据
    let res = await getAllCustomerList({
      current: 1,
      size: 1000,
      total: 0,
    });
    console.log(res, "res");
    if (res.data.records) {
      customerNameOptions.value = res.data.records.map(item => ({
        label: item.customerName,
        value: item.customerName,
        id: item.id,
      }));
    } else {
    }
    operationType.value = type;
    dialogFormVisible.value = true;
    form.value = {};
    proxy.resetForm("formRef");
    form.value.checkUserId = userStore.id;
    form.value.feedbackDate = getCurrentDate();
    // æ–°å¢žæ—¶æ¸…空已选关联产品
    if (type === "add") {
      tableData.value = [];
    }
    userListNoPageByTenantId().then(res => {
      userList.value = res.data;
    });
    if (type === "edit") {
      form.value = { ...row };
      if (form.value.customerName) {
        const res = await getSalesLedger({
          customerName: form.value.customerName,
        });
        if (res?.code === 200) {
          console.log(res);
          associatedSalesOrderNumberOptions.value = (res.data?.records || []).map(
            item => ({
              label: item.salesContractNo,
              value: item.salesContractNo,
              productData: item.productData,
              id: item.id,
            })
          );
        }
      }
      console.log(form.value);
    }
  };
  const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
      if (valid) {
        // åŒ¹é…äº§å“åž‹å·IDs
        form.value.productModelIds = tableData.value
          .map(item => item.id)
          .join(",");
        if (operationType.value === "add") {
          afterSalesServiceAdd(form.value).then(response => {
            proxy.$modal.msgSuccess("新增成功");
            closeDia();
          });
        } else {
          afterSalesServiceUpdate(form.value).then(response => {
            proxy.$modal.msgSuccess("修改成功");
            closeDia();
          });
        }
      }
    });
  };
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
    emit("close");
  };
  defineExpose({
    openDialog,
  });
</script>
<style scoped lang="scss">
.descriptions {
  margin-bottom: 20px;
  display: inline-block;
  font-size: 1rem;
  font-weight: 600;
  padding-left: 12px;
  position: relative;
}
  .descriptions {
    margin-bottom: 20px;
    display: inline-block;
    font-size: 1rem;
    font-weight: 600;
    padding-left: 12px;
    position: relative;
  }
.descriptions::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 4px;
  height: 1rem;
  background-color: #002FA7; /* Element é»˜è®¤çº¢è‰² */
  border-radius: 2px;
}
  .descriptions::before {
    content: "";
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 4px;
    height: 1rem;
    background-color: #002fa7; /* Element é»˜è®¤çº¢è‰² */
    border-radius: 2px;
  }
</style>
src/views/equipmentManagement/operationManagement/index.vue
@@ -104,7 +104,7 @@
          align="center"
        >
          <template #default="scope">
            {{ scope.row.runtimeDuration || '-' }}
            {{ getRuntimeDurationDisplay(scope.row) }}
          </template>
        </el-table-column>
        <el-table-column
@@ -154,7 +154,8 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, onUnmounted, computed } from 'vue'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
import {
  VideoPlay,
@@ -193,6 +194,98 @@
  return filtered
})
// è¿è¡Œä¸­æ— ç»“束时间时,运行时长需随当前时间变化,用 tick è§¦å‘模板重算
const runtimeDisplayTick = ref(0)
/** å–后端可能使用的开始/结束时间字段 */
const pickStartTime = (row) => row?.startRuntimeTime ?? row?.startTime ?? row?.start_time
const pickEndTime = (row) => row?.endRuntimeTime ?? row?.endTime ?? row?.end_time
/**
 * è§£æžæŽ¥å£/前端写入的各类时间:时间戳、ISO å­—符串、yyyy-MM-dd HH:mm:ss、Jackson æ•°ç»„ [y,M,d,h,m,s]、含中文的 toLocaleString ç­‰
 */
const parseDeviceTime = (input) => {
  if (input === null || input === undefined || input === '') return null
  if (typeof input === 'number' && !Number.isNaN(input)) {
    const d = dayjs(input)
    return d.isValid() ? d.toDate() : null
  }
  if (Array.isArray(input)) {
    const [y, mo, day, h = 0, mi = 0, se = 0] = input
    if (y == null || y === '') return null
    const d = dayjs()
        .year(Number(y))
        .month(Number(mo || 1) - 1)
        .date(Number(day || 1))
        .hour(Number(h) || 0)
        .minute(Number(mi) || 0)
        .second(Number(se) || 0)
    return d.isValid() ? d.toDate() : null
  }
  const s = String(input).trim()
  if (!s || s === '-') return null
  let d = dayjs(s)
  if (d.isValid()) return d.toDate()
  d = dayjs(s.replace(/-/g, '/'))
  if (d.isValid()) return d.toDate()
  d = dayjs(s.replace(/\//g, '-'))
  if (d.isValid()) return d.toDate()
  return null
}
const formatDurationMs = (durationMs) => {
  if (durationMs == null || Number.isNaN(durationMs) || durationMs < 0) return '-'
  const hours = Math.floor(durationMs / (1000 * 60 * 60))
  const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60))
  if (hours === 0 && minutes === 0) return '不足1分钟'
  return `${hours}小时${minutes}分钟`
}
const hasMeaningfulEnd = (endRaw) =>
    endRaw !== null &&
    endRaw !== undefined &&
    String(endRaw).trim() !== '' &&
    String(endRaw).trim() !== '-'
const formatStoredDuration = (row) => {
  const rd = row?.runtimeDuration
  if (rd === null || rd === undefined) return ''
  const t = String(rd).trim()
  return t === '' || t === '-' ? '' : String(rd)
}
/** è¿è¡Œä¸­ï¼šå§‹ç»ˆç”¨ã€Œå½“前时间 - å¼€å§‹æ—¶é—´ã€ï¼›å·²åœæ­¢ï¼šä¼˜å…ˆæŽ¥å£ runtimeDuration,否则用结束-开始;无结束可看已存时长或动态推算 */
const getRuntimeDurationDisplay = (row) => {
  void runtimeDisplayTick.value
  const start = parseDeviceTime(pickStartTime(row))
  if (!start) {
    return formatStoredDuration(row) || '-'
  }
  const statusStr = String(row?.status ?? '').trim()
  const isRunning = statusStr === '运行中' || statusStr === '1'
  const endRaw = pickEndTime(row)
  const hasEnd = hasMeaningfulEnd(endRaw)
  // æ— ç»“束时间:运行中一定动态算;已停止则优先展示后端已存时长,没有再按当前时间推算
  if (!hasEnd) {
    if (isRunning) return formatDurationMs(Date.now() - start.getTime())
    const stored = formatStoredDuration(row)
    if (stored) return stored
    return formatDurationMs(Date.now() - start.getTime())
  }
  if (isRunning) {
    return formatDurationMs(Date.now() - start.getTime())
  }
  const end = parseDeviceTime(endRaw)
  const stored = formatStoredDuration(row)
  if (stored) return stored
  if (end) return formatDurationMs(end.getTime() - start.getTime())
  return '-'
}
// æ£€æŸ¥è®¾å¤‡æ˜¯å¦è¶…时未启动
const isOverdue = (device) => {
@@ -246,12 +339,11 @@
      device.endRuntimeTime = currentTime
      // è®¡ç®—运行时长
      if (device.startRuntimeTime) {
        const startTime = new Date(device.startRuntimeTime)
        const endTime = new Date(currentTime)
        const duration = endTime - startTime
        const hours = Math.floor(duration / (1000 * 60 * 60))
        const minutes = Math.floor((duration % (1000 * 60 * 60)) / (1000 * 60))
        device.runtimeDuration = `${hours}小时${minutes}分钟`
        const startTime = parseDeviceTime(device.startRuntimeTime)
        const endTime = parseDeviceTime(currentTime)
        if (startTime && endTime) {
          device.runtimeDuration = formatDurationMs(endTime.getTime() - startTime.getTime())
        }
      }
    }
    const params = {
@@ -297,9 +389,31 @@
// ç»„件挂载时初始化数据
const POLL_MS = 60 * 1000
const RUNTIME_TICK_MS = 30 * 1000
let listPollTimer = null
let runtimeTickTimer = null
// ç»„件挂载时拉取数据,并每分钟刷新一次列表;运行中时长每 30 ç§’刷新显示
onMounted(() => {
  getList()
  listPollTimer = setInterval(() => {
    getList()
  }, POLL_MS)
  runtimeTickTimer = setInterval(() => {
    runtimeDisplayTick.value++
  }, RUNTIME_TICK_MS)
})
onUnmounted(() => {
  if (listPollTimer != null) {
    clearInterval(listPollTimer)
    listPollTimer = null
  }
  if (runtimeTickTimer != null) {
    clearInterval(runtimeTickTimer)
    runtimeTickTimer = null
  }
})
</script>
src/views/financialManagement/generalLedger/index.vue
@@ -1,23 +1,39 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
    <el-form :model="filters"
             :inline="true">
      <el-form-item label="科目编码:">
        <el-input v-model="filters.subjectCode" placeholder="请输入科目编码" clearable style="width: 200px;" />
        <el-input v-model="filters.subjectCode"
                  placeholder="请输入科目编码"
                  clearable
                  style="width: 200px;" />
      </el-form-item>
      <el-form-item label="科目名称:">
        <el-input v-model="filters.subjectName" placeholder="请输入科目名称" clearable style="width: 200px;" />
        <el-input v-model="filters.subjectName"
                  placeholder="请输入科目名称"
                  clearable
                  style="width: 200px;" />
      </el-form-item>
      <el-form-item label="科目类型:">
        <el-select v-model="filters.subjectType" placeholder="请选择" clearable style="width: 200px;">
          <el-option label="资产类" value="asset" />
          <el-option label="负债类" value="liability" />
          <el-option label="权益类" value="equity" />
          <el-option label="成本类" value="cost" />
          <el-option label="损益类" value="profit_loss" />
        <el-select v-model="filters.subjectType"
                   placeholder="请选择"
                   clearable
                   style="width: 200px;">
          <el-option label="资产类"
                     value="资产类" />
          <el-option label="负债类"
                     value="负债类" />
          <el-option label="权益类"
                     value="权益类" />
          <el-option label="成本类"
                     value="成本类" />
          <el-option label="损益类"
                     value="损益类" />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">搜索</el-button>
        <el-button type="primary"
                   @click="getTableData">搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </el-form-item>
    </el-form>
@@ -25,76 +41,85 @@
      <div class="actions">
        <div></div>
        <div>
          <el-button type="primary" @click="add" icon="Plus">新增</el-button>
          <el-button @click="handleOut" icon="Download">导出</el-button>
          <el-button type="primary"
                     @click="add"
                     icon="Plus">新增</el-button>
          <el-button @click="handleOut"
                     icon="Download">导出</el-button>
        </div>
      </div>
      <PIMTable
        rowKey="id"
        :column="columns"
        :tableData="dataList"
        :page="{
      <PIMTable rowKey="id"
                :column="columns"
                :tableData="dataList"
                :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @pagination="changePage"
      >
        <template #subjectType="{ row }">
          <el-tag :type="getSubjectTypeType(row.subjectType)">{{ getSubjectTypeLabel(row.subjectType) }}</el-tag>
        </template>
        <template #balanceDirection="{ row }">
          <el-tag :type="row.balanceDirection === 'debit' ? 'success' : 'danger'">
            {{ row.balanceDirection === 'debit' ? '借方' : '贷方' }}
          </el-tag>
        </template>
        <template #status="{ row }">
          <el-tag :type="row.status === 'active' ? 'success' : 'info'">
            {{ row.status === 'active' ? '启用' : '禁用' }}
          </el-tag>
        </template>
        <template #operation="{ row }">
          <el-button type="primary" link @click="edit(row)">编辑</el-button>
          <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
        </template>
                @pagination="changePage">
      </PIMTable>
    </div>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="600px" @confirm="submitForm" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
        <el-form-item label="科目编码" prop="subjectCode">
          <el-input v-model="form.subjectCode" placeholder="请输入科目编码" />
    <FormDialog :title="dialogTitle"
                v-model="dialogVisible"
                width="600px"
                @confirm="submitForm"
                @cancel="dialogVisible = false">
      <el-form :model="form"
               :rules="rules"
               ref="formRef"
               label-width="100px">
        <el-form-item label="科目编码"
                      prop="subjectCode">
          <el-input v-model="form.subjectCode"
                    placeholder="请输入科目编码" />
        </el-form-item>
        <el-form-item label="科目名称" prop="subjectName">
          <el-input v-model="form.subjectName" placeholder="请输入科目名称" />
        <el-form-item label="科目名称"
                      prop="subjectName">
          <el-input v-model="form.subjectName"
                    placeholder="请输入科目名称" />
        </el-form-item>
        <el-form-item label="科目类型" prop="subjectType">
          <el-select v-model="form.subjectType" placeholder="请选择科目类型" style="width: 100%;">
            <el-option label="资产类" value="asset" />
            <el-option label="负债类" value="liability" />
            <el-option label="权益类" value="equity" />
            <el-option label="成本类" value="cost" />
            <el-option label="损益类" value="profit_loss" />
        <el-form-item label="科目类型"
                      prop="subjectType">
          <el-select v-model="form.subjectType"
                     placeholder="请选择科目类型"
                     style="width: 100%;">
            <el-option label="资产类"
                       value="资产类" />
            <el-option label="负债类"
                       value="负债类" />
            <el-option label="权益类"
                       value="权益类" />
            <el-option label="成本类"
                       value="成本类" />
            <el-option label="损益类"
                       value="损益类" />
          </el-select>
        </el-form-item>
        <el-form-item label="余额方向" prop="balanceDirection">
        <el-form-item label="余额方向"
                      prop="balanceDirection">
          <el-radio-group v-model="form.balanceDirection">
            <el-radio label="debit">借方</el-radio>
            <el-radio label="credit">贷方</el-radio>
            <el-radio label="借方">借方</el-radio>
            <el-radio label="贷方">贷方</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="状态" prop="status">
        <el-form-item label="状态"
                      prop="status">
          <el-radio-group v-model="form.status">
            <el-radio label="active">启用</el-radio>
            <el-radio label="inactive">禁用</el-radio>
            <el-radio :label="0">启用</el-radio>
            <el-radio :label="1">禁用</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
        <el-form-item label="备注"
                      prop="remark">
          <el-input v-model="form.remark"
                    type="textarea"
                    :rows="3"
                    placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" @click="submitForm">确定</el-button>
        <el-button type="primary"
                   @click="submitForm">确定</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
@@ -102,190 +127,238 @@
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
  import { ref, reactive, onMounted, getCurrentInstance } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import {
    listAccountSubject,
    addAccountSubject,
    updateAccountSubject,
    delAccountSubject,
    exportAccountSubject,
  } from "@/api/financialManagement/accountSubject";
defineOptions({
  name: "总帐科目",
});
  defineOptions({
    name: "总帐科目",
  });
const filters = reactive({
  subjectCode: "",
  subjectName: "",
  subjectType: "",
});
  const { proxy } = getCurrentInstance();
const pagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
const columns = [
  { label: "科目编码", prop: "subjectCode", width: "120" },
  { label: "科目名称", prop: "subjectName", width: "150" },
  { label: "科目类型", prop: "subjectType", slot: "subjectType" },
  { label: "余额方向", prop: "balanceDirection", slot: "balanceDirection" },
  { label: "状态", prop: "status", slot: "status" },
  { label: "备注", prop: "remark", showOverflowTooltip: true },
  { label: "操作", prop: "operation", slot: "operation", width: "150", fixed: "right" },
];
const dataList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const currentId = ref(null);
const form = reactive({
  subjectCode: "",
  subjectName: "",
  subjectType: "",
  balanceDirection: "debit",
  status: "active",
  remark: "",
});
const rules = {
  subjectCode: [{ required: true, message: "请输入科目编码", trigger: "blur" }],
  subjectName: [{ required: true, message: "请输入科目名称", trigger: "blur" }],
  subjectType: [{ required: true, message: "请选择科目类型", trigger: "change" }],
};
const mockData = [
  { id: 1, subjectCode: "1001", subjectName: "库存现金", subjectType: "asset", balanceDirection: "debit", status: "active", remark: "" },
  { id: 2, subjectCode: "1002", subjectName: "银行存款", subjectType: "asset", balanceDirection: "debit", status: "active", remark: "" },
  { id: 3, subjectCode: "1122", subjectName: "应收账款", subjectType: "asset", balanceDirection: "debit", status: "active", remark: "" },
  { id: 4, subjectCode: "2202", subjectName: "应付账款", subjectType: "liability", balanceDirection: "credit", status: "active", remark: "" },
  { id: 5, subjectCode: "4001", subjectName: "实收资本", subjectType: "equity", balanceDirection: "credit", status: "active", remark: "" },
  { id: 6, subjectCode: "5001", subjectName: "生产成本", subjectType: "cost", balanceDirection: "debit", status: "active", remark: "" },
  { id: 7, subjectCode: "6001", subjectName: "主营业务收入", subjectType: "profit_loss", balanceDirection: "credit", status: "active", remark: "" },
  { id: 8, subjectCode: "6401", subjectName: "主营业务成本", subjectType: "profit_loss", balanceDirection: "debit", status: "active", remark: "" },
];
const getSubjectTypeLabel = (type) => {
  const map = {
    asset: "资产类",
    liability: "负债类",
    equity: "权益类",
    cost: "成本类",
    profit_loss: "损益类",
  };
  return map[type] || type;
};
const getSubjectTypeType = (type) => {
  const map = {
    asset: "success",
    liability: "danger",
    equity: "warning",
    cost: "info",
    profit_loss: "primary",
  };
  return map[type] || "";
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.subjectCode) {
    result = result.filter(item => item.subjectCode.includes(filters.subjectCode));
  }
  if (filters.subjectName) {
    result = result.filter(item => item.subjectName.includes(filters.subjectName));
  }
  if (filters.subjectType) {
    result = result.filter(item => item.subjectType === filters.subjectType);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
const resetFilters = () => {
  filters.subjectCode = "";
  filters.subjectName = "";
  filters.subjectType = "";
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
  getTableData();
};
const add = () => {
  isEdit.value = false;
  dialogTitle.value = "新增科目";
  Object.assign(form, {
  const filters = reactive({
    subjectCode: "",
    subjectName: "",
    subjectType: "",
    balanceDirection: "debit",
    status: "active",
  });
  const pagination = reactive({
    currentPage: 1,
    pageSize: 10,
    total: 0,
  });
  const columns = [
    { label: "科目编码", prop: "subjectCode", width: "120" },
    { label: "科目名称", prop: "subjectName", width: "150" },
    { label: "科目类型", prop: "subjectType" },
    {
      label: "余额方向",
      prop: "balanceDirection",
      dataType: "tag",
      formatData: value => {
        if (value === "借方") {
          return "借方";
        }
        return "贷方";
      },
      formatType: value => {
        if (value === "借方") {
          return "primary";
        }
        return "danger";
      },
    },
    {
      label: "状态",
      prop: "status",
      dataType: "tag",
      formatData: value => {
        if (value === 0 || value === "0") {
          return "启用";
        }
        return "禁用";
      },
      formatType: value => {
        if (value === 0 || value === "0") {
          return "success";
        }
        return "info";
      },
    },
    { label: "备注", prop: "remark", showOverflowTooltip: true },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: "150",
      operation: [
        {
          name: "编辑",
          type: "primary",
          clickFun: row => {
            edit(row);
          },
        },
        {
          name: "删除",
          type: "danger",
          clickFun: row => {
            handleDelete(row);
          },
        },
      ],
    },
  ];
  const dataList = ref([]);
  const dialogVisible = ref(false);
  const dialogTitle = ref("");
  const formRef = ref(null);
  const isEdit = ref(false);
  const form = reactive({
    id: undefined,
    subjectCode: "",
    subjectName: "",
    subjectType: "",
    balanceDirection: "借方",
    status: 0,
    remark: "",
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑科目";
  Object.assign(form, row);
  dialogVisible.value = true;
};
  const rules = {
    subjectCode: [{ required: true, message: "请输入科目编码", trigger: "blur" }],
    subjectName: [{ required: true, message: "请输入科目名称", trigger: "blur" }],
    subjectType: [
      { required: true, message: "请选择科目类型", trigger: "change" },
    ],
  };
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form };
  const getSubjectTypeType = type => {
    const map = {
      èµ„产类: "success",
      è´Ÿå€ºç±»: "danger",
      æƒç›Šç±»: "warning",
      æˆæœ¬ç±»: "info",
      æŸç›Šç±»: "primary",
    };
    return map[type] || "";
  };
  const getTableData = () => {
    const query = {
      pageNum: pagination.currentPage,
      pageSize: pagination.pageSize,
      ...filters,
    };
    listAccountSubject(query).then(response => {
      dataList.value = response.data.records;
      pagination.total = response.data.total;
    });
  };
  const resetFilters = () => {
    filters.subjectCode = "";
    filters.subjectName = "";
    filters.subjectType = "";
    pagination.currentPage = 1;
    getTableData();
  };
  const changePage = obj => {
    pagination.currentPage = obj.page;
    pagination.pageSize = obj.limit;
    getTableData();
  };
  const add = () => {
    isEdit.value = false;
    dialogTitle.value = "新增科目";
    Object.assign(form, {
      id: undefined,
      subjectCode: "",
      subjectName: "",
      subjectType: "",
      balanceDirection: "借方",
      status: 0,
      remark: "",
    });
    dialogVisible.value = true;
  };
  const edit = row => {
    isEdit.value = true;
    dialogTitle.value = "编辑科目";
    Object.assign(form, row);
    dialogVisible.value = true;
  };
  const submitForm = () => {
    formRef.value.validate(valid => {
      if (valid) {
        if (isEdit.value) {
          updateAccountSubject(form).then(() => {
            ElMessage.success("编辑成功");
            dialogVisible.value = false;
            getTableData();
          });
        } else {
          addAccountSubject(form).then(() => {
            ElMessage.success("新增成功");
            dialogVisible.value = false;
            getTableData();
          });
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form });
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
    });
  };
const handleDelete = (row) => {
  ElMessageBox.confirm("确认删除该科目吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData.splice(index, 1);
    }
    ElMessage.success("删除成功");
  const handleDelete = row => {
    const ids = row.id;
    ElMessageBox.confirm("确认删除该科目吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        return delAccountSubject(ids);
      })
      .then(() => {
        ElMessage.success("删除成功");
        getTableData();
      });
  };
  const handleOut = () => {
    proxy.download(
      "accountSubject/export",
      {
        ...filters,
      },
      `account_subject_${new Date().getTime()}.xlsx`
    );
  };
  onMounted(() => {
    getTableData();
  });
};
const handleOut = () => {
  ElMessage.success("导出成功");
};
onMounted(() => {
  getTableData();
});
</script>
<style lang="scss" scoped>
.actions {
  display: flex;
  justify-content: space-between;
  margin-bottom: 15px;
}
  .actions {
    display: flex;
    justify-content: space-between;
    margin-bottom: 15px;
  }
</style>
src/views/inventoryManagement/dispatchLog/Record.vue
@@ -96,7 +96,9 @@
        </el-table-column>
                <el-table-column label="审批状态" prop="approvalStatus" show-overflow-tooltip>
                    <template #default="scope">
                        {{ getApprovalStatusLabel(scope.row.approvalStatus) }}
                        <el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)" size="small">
                            {{ getApprovalStatusLabel(scope.row.approvalStatus) }}
                        </el-tag>
                    </template>
                </el-table-column>
            </el-table>
@@ -179,7 +181,7 @@
};
const getList = () => {
    tableLoading.value = true;
    getStockOutPage({ ...searchForm.value, ...page, type: props.type, topParentProductId: props.topParentProductId })
    getStockOutPage({ ...searchForm.value, ...page,  topParentProductId: props.topParentProductId })
        .then((res) => {
            tableLoading.value = false;
            tableData.value = res.data.records;
@@ -216,6 +218,13 @@
    return approvalStatusLabelMap[status] || "待审批";
};
// é€šè¿‡/驳回固定色;其余(含待审批、空值、未映射但文案为待审批)统一用 warning é¢„警色
const getApprovalStatusTagType = (status) => {
    if (status === 1 || status === "1" || status === "approved" || status === "APPROVED") return "success";
    if (status === 2 || status === "2" || status === "rejected" || status === "REJECTED") return "danger";
    return "warning";
};
// èŽ·å–æ¥æºç±»åž‹é€‰é¡¹
const fetchStockRecordTypeOptions = () => {
  if (props.type === '0') {
src/views/inventoryManagement/receiptManagement/Record.vue
@@ -91,7 +91,9 @@
                         prop="approvalStatus"
                         show-overflow-tooltip>
          <template #default="scope">
            {{ getApprovalStatusLabel(scope.row.approvalStatus) }}
            <el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)" size="small">
              {{ getApprovalStatusLabel(scope.row.approvalStatus) }}
            </el-tag>
          </template>
        </el-table-column>
      </el-table>
@@ -187,6 +189,13 @@
  return approvalStatusLabelMap[status] || "待审批";
};
// é€šè¿‡/驳回固定色;其余(含待审批、空值、未映射但文案为待审批)统一用 warning é¢„警色
const getApprovalStatusTagType = (status) => {
  if (status === 1 || status === "1" || status === "approved" || status === "APPROVED") return "success";
  if (status === 2 || status === "2" || status === "rejected" || status === "REJECTED") return "danger";
  return "warning";
};
const pageProductChange = obj => {
  page.current = obj.page;
  page.size = obj.limit;
@@ -195,7 +204,7 @@
const getList = () => {
  tableLoading.value = true;
  const params = {...page, type: props.type, topParentProductId: props.topParentProductId};
  const params = {...page,  topParentProductId: props.topParentProductId};
  params.timeStr = searchForm.value.timeStr;
  params.productName = searchForm.value.productName;
  params.recordType = searchForm.value.recordType;
src/views/inventoryManagement/stockReport/index.vue
@@ -4,708 +4,683 @@
    <div class="search_form">
      <div class="search_left">
        <span class="search_title">报表类型:</span>
        <el-select
          v-model="searchForm.reportType"
          style="width: 150px;"
          placeholder="请选择"
          @change="handleReportTypeChange"
        >
          <el-option label="日报" value="daily" />
          <el-option label="月报" value="monthly" />
          <el-option label="进出存报表" value="inout" />
        <el-select v-model="searchForm.reportType"
                   style="width: 150px;"
                   placeholder="请选择"
                   @change="handleReportTypeChange">
          <el-option label="日报"
                     value="daily" />
          <el-option label="月报"
                     value="monthly" />
          <el-option label="进出存报表"
                     value="inout" />
        </el-select>
        <span class="search_title ml10">时间范围:</span>
         <el-date-picker
           v-if="searchForm.reportType === 'daily'"
           v-model="searchForm.singleDate"
           type="date"
           placeholder="请选择日期"
           format="YYYY-MM-DD"
           value-format="YYYY-MM-DD"
           style="width: 200px;"
         />
        <el-date-picker
          v-else-if="searchForm.reportType === 'monthly'"
          v-model="searchForm.monthRange"
          type="monthrange"
          range-separator="至"
          start-placeholder="开始月份"
          end-placeholder="结束月份"
          format="YYYY-MM-DD"
          value-format="YYYY-MM-DD"
          style="width: 240px;"
        />
        <el-date-picker
          v-else
          v-model="searchForm.dateRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          format="YYYY-MM-DD"
          value-format="YYYY-MM-DD"
          style="width: 240px;"
        />
        <el-button type="primary" @click="onSearch" style="margin-left: 10px">
        <el-date-picker v-if="searchForm.reportType === 'daily'"
                        v-model="searchForm.singleDate"
                        type="date"
                        placeholder="请选择日期"
                        format="YYYY-MM-DD"
                        value-format="YYYY-MM-DD"
                        style="width: 200px;" />
        <el-date-picker v-else-if="searchForm.reportType === 'monthly'"
                        v-model="searchForm.monthRange"
                        type="monthrange"
                        range-separator="至"
                        start-placeholder="开始月份"
                        end-placeholder="结束月份"
                        format="YYYY-MM-DD"
                        value-format="YYYY-MM-DD"
                        style="width: 240px;" />
        <el-date-picker v-else
                        v-model="searchForm.dateRange"
                        type="daterange"
                        range-separator="至"
                        start-placeholder="开始日期"
                        end-placeholder="结束日期"
                        format="YYYY-MM-DD"
                        value-format="YYYY-MM-DD"
                        style="width: 240px;" />
        <el-button type="primary"
                   @click="onSearch"
                   style="margin-left: 10px">
          æŸ¥è¯¢
        </el-button>
        <el-button @click="handleReset">重置</el-button>
      </div>
      <div class="search_right">
<!--        <el-button type="success" @click="handleExport" icon="Download">-->
<!--          å¯¼å‡ºæŠ¥è¡¨-->
<!--        </el-button>-->
        <!--        <el-button type="success" @click="handleExport" icon="Download">-->
        <!--          å¯¼å‡ºæŠ¥è¡¨-->
        <!--        </el-button>-->
      </div>
    </div>
<!--    &lt;!&ndash; ç»Ÿè®¡å¡ç‰‡ &ndash;&gt;-->
<!--    <div class="stats_cards" v-if="reportData.summary">-->
<!--      <el-row :gutter="20">-->
<!--        <el-col :span="6">-->
<!--          <el-card class="stats_card">-->
<!--            <div class="stats_content">-->
<!--              <div class="stats_icon in">-->
<!--                <el-icon><TrendCharts /></el-icon>-->
<!--              </div>-->
<!--              <div class="stats_info">-->
<!--                <div class="stats_value">{{ reportData.summary.totalIn || 0 }}</div>-->
<!--                <div class="stats_label">总入库量</div>-->
<!--              </div>-->
<!--            </div>-->
<!--          </el-card>-->
<!--        </el-col>-->
<!--        <el-col :span="6">-->
<!--          <el-card class="stats_card">-->
<!--            <div class="stats_content">-->
<!--              <div class="stats_icon out">-->
<!--                <el-icon><TrendCharts /></el-icon>-->
<!--              </div>-->
<!--              <div class="stats_info">-->
<!--                <div class="stats_value">{{ reportData.summary.totalOut || 0 }}</div>-->
<!--                <div class="stats_label">总出库量</div>-->
<!--              </div>-->
<!--            </div>-->
<!--          </el-card>-->
<!--        </el-col>-->
<!--        <el-col :span="6">-->
<!--          <el-card class="stats_card">-->
<!--            <div class="stats_content">-->
<!--              <div class="stats_icon stock">-->
<!--                <el-icon><Box /></el-icon>-->
<!--              </div>-->
<!--              <div class="stats_info">-->
<!--                <div class="stats_value">{{ reportData.summary.currentStock || 0 }}</div>-->
<!--                <div class="stats_label">当前库存</div>-->
<!--              </div>-->
<!--            </div>-->
<!--          </el-card>-->
<!--        </el-col>-->
<!--        <el-col :span="6">-->
<!--          <el-card class="stats_card">-->
<!--            <div class="stats_content">-->
<!--              <div class="stats_icon turnover">-->
<!--                <el-icon><Refresh /></el-icon>-->
<!--              </div>-->
<!--              <div class="stats_info">-->
<!--                <div class="stats_value">{{ reportData.summary.turnoverRate || 0 }}%</div>-->
<!--                <div class="stats_label">周转率</div>-->
<!--              </div>-->
<!--            </div>-->
<!--          </el-card>-->
<!--        </el-col>-->
<!--      </el-row>-->
<!--    </div>-->
<!--    &lt;!&ndash; å›¾è¡¨åŒºåŸŸ &ndash;&gt;-->
<!--    <div class="chart_section" v-if="reportData.chartData">-->
<!--      <el-row :gutter="20">-->
<!--        <el-col :span="12">-->
<!--          <el-card>-->
<!--            <template #header>-->
<!--              <span>库存趋势图</span>-->
<!--            </template>-->
<!--            <div ref="trendChart" style="height: 300px;"></div>-->
<!--          </el-card>-->
<!--        </el-col>-->
<!--        <el-col :span="12">-->
<!--          <el-card>-->
<!--            <template #header>-->
<!--              <span>进出库对比</span>-->
<!--            </template>-->
<!--            <div ref="comparisonChart" style="height: 300px;"></div>-->
<!--          </el-card>-->
<!--        </el-col>-->
<!--      </el-row>-->
<!--    </div>-->
    <!--    &lt;!&ndash; ç»Ÿè®¡å¡ç‰‡ &ndash;&gt;-->
    <!--    <div class="stats_cards" v-if="reportData.summary">-->
    <!--      <el-row :gutter="20">-->
    <!--        <el-col :span="6">-->
    <!--          <el-card class="stats_card">-->
    <!--            <div class="stats_content">-->
    <!--              <div class="stats_icon in">-->
    <!--                <el-icon><TrendCharts /></el-icon>-->
    <!--              </div>-->
    <!--              <div class="stats_info">-->
    <!--                <div class="stats_value">{{ reportData.summary.totalIn || 0 }}</div>-->
    <!--                <div class="stats_label">总入库量</div>-->
    <!--              </div>-->
    <!--            </div>-->
    <!--          </el-card>-->
    <!--        </el-col>-->
    <!--        <el-col :span="6">-->
    <!--          <el-card class="stats_card">-->
    <!--            <div class="stats_content">-->
    <!--              <div class="stats_icon out">-->
    <!--                <el-icon><TrendCharts /></el-icon>-->
    <!--              </div>-->
    <!--              <div class="stats_info">-->
    <!--                <div class="stats_value">{{ reportData.summary.totalOut || 0 }}</div>-->
    <!--                <div class="stats_label">总出库量</div>-->
    <!--              </div>-->
    <!--            </div>-->
    <!--          </el-card>-->
    <!--        </el-col>-->
    <!--        <el-col :span="6">-->
    <!--          <el-card class="stats_card">-->
    <!--            <div class="stats_content">-->
    <!--              <div class="stats_icon stock">-->
    <!--                <el-icon><Box /></el-icon>-->
    <!--              </div>-->
    <!--              <div class="stats_info">-->
    <!--                <div class="stats_value">{{ reportData.summary.currentStock || 0 }}</div>-->
    <!--                <div class="stats_label">当前库存</div>-->
    <!--              </div>-->
    <!--            </div>-->
    <!--          </el-card>-->
    <!--        </el-col>-->
    <!--        <el-col :span="6">-->
    <!--          <el-card class="stats_card">-->
    <!--            <div class="stats_content">-->
    <!--              <div class="stats_icon turnover">-->
    <!--                <el-icon><Refresh /></el-icon>-->
    <!--              </div>-->
    <!--              <div class="stats_info">-->
    <!--                <div class="stats_value">{{ reportData.summary.turnoverRate || 0 }}%</div>-->
    <!--                <div class="stats_label">周转率</div>-->
    <!--              </div>-->
    <!--            </div>-->
    <!--          </el-card>-->
    <!--        </el-col>-->
    <!--      </el-row>-->
    <!--    </div>-->
    <!--    &lt;!&ndash; å›¾è¡¨åŒºåŸŸ &ndash;&gt;-->
    <!--    <div class="chart_section" v-if="reportData.chartData">-->
    <!--      <el-row :gutter="20">-->
    <!--        <el-col :span="12">-->
    <!--          <el-card>-->
    <!--            <template #header>-->
    <!--              <span>库存趋势图</span>-->
    <!--            </template>-->
    <!--            <div ref="trendChart" style="height: 300px;"></div>-->
    <!--          </el-card>-->
    <!--        </el-col>-->
    <!--        <el-col :span="12">-->
    <!--          <el-card>-->
    <!--            <template #header>-->
    <!--              <span>进出库对比</span>-->
    <!--            </template>-->
    <!--            <div ref="comparisonChart" style="height: 300px;"></div>-->
    <!--          </el-card>-->
    <!--        </el-col>-->
    <!--      </el-row>-->
    <!--    </div>-->
    <!-- è¯¦ç»†æ•°æ®è¡¨æ ¼ -->
    <div class="table_section">
      <el-card>
        <template #header>
          <span>{{ getTableTitle() }}</span>
        </template>
         <el-table
           v-loading="tableLoading"
           :data="reportData.tableData"
           border
           height="400"
           style="width: 100%"
           :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
         >
          <el-table-column
            align="center"
            label="序号"
            type="index"
            width="60"
          />
           <el-table-column
             label="入库时间"
             prop="createTime"
             width="200"
             show-overflow-tooltip
             v-if="searchForm.reportType !== 'inout'"
           />
           <el-table-column
             label="入库批次"
             prop="inboundBatches"
             width="240"
             show-overflow-tooltip
             v-if="searchForm.reportType !== 'inout'"
           />
           <el-table-column
             label="产品大类"
             prop="productName"
             show-overflow-tooltip
           />
           <el-table-column
             label="规格型号"
             prop="model"
             show-overflow-tooltip
           />
           <el-table-column
             label="单位"
             prop="unit"
             show-overflow-tooltip
           />
           <el-table-column
             label="入库数量"
             prop="totalStockIn"
             align="center"
             v-if="searchForm.reportType === 'inout'"
           />
           <el-table-column
               label="入库数量"
               prop="stockInNum"
               align="center"
               v-else
           />
           <el-table-column
             label="出库数量"
             prop="totalStockOut"
             width="100"
             align="center"
             v-if="searchForm.reportType === 'inout'"
           />
           <el-table-column
             label="现在库存"
             prop="currentStock"
             align="center"
           />
           <el-table-column label="来源"
                            prop="recordType"
                            v-if="searchForm.reportType !== 'inout'"
                            show-overflow-tooltip>
             <template #default="scope">
               {{ getRecordType(scope.row.recordType) }}
             </template>
           </el-table-column>
           <el-table-column
             label="入库人"
             prop="createBy"
             width="80"
             v-if="searchForm.reportType !== 'inout'"
             show-overflow-tooltip
           />
        <el-table v-loading="tableLoading"
                  :data="reportData.tableData"
                  border
                  height="400"
                  style="width: 100%"
                  :header-cell-style="{ background: '#F0F1F5', color: '#333333' }">
          <el-table-column align="center"
                           label="序号"
                           type="index"
                           width="60" />
          <el-table-column label="入库时间"
                           prop="createTime"
                           width="200"
                           show-overflow-tooltip
                           v-if="searchForm.reportType !== 'inout'" />
          <el-table-column label="入库批次"
                           prop="inboundBatches"
                           width="180"
                           show-overflow-tooltip
                           v-if="searchForm.reportType !== 'inout'" />
          <el-table-column label="批号"
                           prop="batchNo"
                           width="180"
                           show-overflow-tooltip
                           v-if="searchForm.reportType !== 'inout'" />
          <el-table-column label="产品大类"
                           prop="productName"
                           show-overflow-tooltip />
          <el-table-column label="规格型号"
                           prop="model"
                           show-overflow-tooltip />
          <el-table-column label="单位"
                           prop="unit"
                           show-overflow-tooltip />
          <el-table-column label="入库数量"
                           prop="totalStockIn"
                           align="center"
                           v-if="searchForm.reportType === 'inout'" />
          <el-table-column label="入库数量"
                           prop="stockInNum"
                           align="center"
                           v-else />
          <el-table-column label="出库数量"
                           prop="totalStockOut"
                           width="100"
                           align="center"
                           v-if="searchForm.reportType === 'inout'" />
          <el-table-column label="现在库存"
                           prop="currentStock"
                           align="center" />
          <el-table-column label="来源"
                           prop="recordType"
                           v-if="searchForm.reportType !== 'inout'"
                           show-overflow-tooltip>
            <template #default="scope">
              {{ getRecordType(scope.row.recordType) }}
            </template>
          </el-table-column>
          <el-table-column label="入库人"
                           prop="createBy"
                           width="80"
                           v-if="searchForm.reportType !== 'inout'"
                           show-overflow-tooltip />
        </el-table>
        <pagination
          :total="total"
          layout="total, sizes, prev, pager, next, jumper"
          :page="page.current"
          :limit="page.size"
          @pagination="paginationChange"
        />
        <pagination :total="total"
                    layout="total, sizes, prev, pager, next, jumper"
                    :page="page.current"
                    :limit="page.size"
                    @pagination="paginationChange" />
      </el-card>
    </div>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick, getCurrentInstance } from 'vue'
import { ElMessage } from 'element-plus'
import * as echarts from 'echarts'
import pagination from '@/components/PIMTable/Pagination.vue'
import {
  getStockInventoryInAndOutReportList,
  getStockInventoryReportList
} from "@/api/inventoryManagement/stockInventory.js";
import {
  findAllQualifiedStockInRecordTypeOptions,findAllUnQualifiedStockInRecordTypeOptions,
} from "@/api/basicData/enum.js";
  import { ref, reactive, onMounted, nextTick, getCurrentInstance } from "vue";
  import { ElMessage } from "element-plus";
  import * as echarts from "echarts";
  import pagination from "@/components/PIMTable/Pagination.vue";
  import {
    getStockInventoryInAndOutReportList,
    getStockInventoryReportList,
  } from "@/api/inventoryManagement/stockInventory.js";
  import {
    findAllQualifiedStockInRecordTypeOptions,
    findAllUnQualifiedStockInRecordTypeOptions,
  } from "@/api/basicData/enum.js";
  const { proxy } = getCurrentInstance();
  // å“åº”式数据
  const tableLoading = ref(false);
  const trendChart = ref(null);
  const comparisonChart = ref(null);
const { proxy } = getCurrentInstance()
// å“åº”式数据
const tableLoading = ref(false)
const trendChart = ref(null)
const comparisonChart = ref(null)
  const searchForm = reactive({
    reportType: "daily",
    singleDate: "",
    dateRange: [],
    monthRange: [],
  });
const searchForm = reactive({
  reportType: 'daily',
  singleDate: '',
  dateRange: [],
  monthRange: []
})
const reportData = ref({
  summary: null,
  chartData: null,
  tableData: []
})
const page = reactive({
  current: 1,
  size: 10,
})
const total = ref(0)
const stockRecordTypeOptions = ref([])
const getRecordType = (recordType) => {
  return stockRecordTypeOptions.value.find(item => item.value === recordType)?.label || ''
}
// èŽ·å–æ¥æºç±»åž‹é€‰é¡¹
const fetchStockRecordTypeOptions = () => {
  findAllQualifiedStockInRecordTypeOptions()
      .then(res => {
        stockRecordTypeOptions.value = res.data;
        findAllUnQualifiedStockInRecordTypeOptions()
          .then(res => {
          stockRecordTypeOptions.value = [...stockRecordTypeOptions.value,...res.data];
      })
      })
}
// èŽ·å–è¡¨æ ¼æ ‡é¢˜
const getTableTitle = () => {
  const typeMap = {
    daily: '日报详细数据',
    monthly: '月报详细数据',
    inout: '进出存报表详细数据'
  }
  return typeMap[searchForm.reportType] || '报表详细数据'
}
// æŠ¥è¡¨ç±»åž‹æ”¹å˜
const handleReportTypeChange = () => {
  page.current = 1
  reportData.value = {
  const reportData = ref({
    summary: null,
    chartData: null,
    tableData: []
  }
}
    tableData: [],
  });
// æŸ¥è¯¢æ•°æ®
const handleQuery = async () => {
  if (!validateSearchForm()) {
    return
  }
  tableLoading.value = true
  try {
    const baseParams = getQueryParams()
  const page = reactive({
    current: 1,
    size: 10,
  });
  const total = ref(0);
  const stockRecordTypeOptions = ref([]);
  const getRecordType = recordType => {
    return (
      stockRecordTypeOptions.value.find(item => item.value === recordType)
        ?.label || ""
    );
  };
  // èŽ·å–æ¥æºç±»åž‹é€‰é¡¹
  const fetchStockRecordTypeOptions = () => {
    findAllQualifiedStockInRecordTypeOptions().then(res => {
      stockRecordTypeOptions.value = res.data;
      findAllUnQualifiedStockInRecordTypeOptions().then(res => {
        stockRecordTypeOptions.value = [
          ...stockRecordTypeOptions.value,
          ...res.data,
        ];
      });
    });
  };
  // èŽ·å–è¡¨æ ¼æ ‡é¢˜
  const getTableTitle = () => {
    const typeMap = {
      daily: "日报详细数据",
      monthly: "月报详细数据",
      inout: "进出存报表详细数据",
    };
    return typeMap[searchForm.reportType] || "报表详细数据";
  };
  // æŠ¥è¡¨ç±»åž‹æ”¹å˜
  const handleReportTypeChange = () => {
    page.current = 1;
    reportData.value = {
      summary: null,
      chartData: null,
      tableData: [],
    };
  };
  // æŸ¥è¯¢æ•°æ®
  const handleQuery = async () => {
    if (!validateSearchForm()) {
      return;
    }
    tableLoading.value = true;
    try {
      const baseParams = getQueryParams();
      const params = {
        ...baseParams,
        current: page.current,
        size: page.size,
      };
      let response;
      if (searchForm.reportType === "inout") {
        response = await getStockInventoryInAndOutReportList(params);
      } else {
        response = await getStockInventoryReportList(params);
      }
      if (response.code === 200) {
        reportData.value.tableData = response.data.records || [];
        total.value = response.data.total || 0;
        // reportData.value.summary = response.data.summary
        // reportData.value.chartData = response.data.chartData
        // nextTick(() => {
        //   initCharts()
        // })
      }
    } catch (error) {
      ElMessage.error("查询失败:" + error.message);
    } finally {
      tableLoading.value = false;
    }
  };
  // æŸ¥è¯¢æŒ‰é’®ï¼šé‡ç½®åˆ°ç¬¬ä¸€é¡µå¹¶æŸ¥è¯¢
  const onSearch = () => {
    page.current = 1;
    handleQuery();
  };
  // åˆ†é¡µå˜åŒ–
  const paginationChange = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    handleQuery();
  };
  // // ç”Ÿæˆå‡æ•°æ®
  // const generateMockData = () => {
  //   // ç”Ÿæˆç»Ÿè®¡å¡ç‰‡å‡æ•°æ®
  //   const summary = {
  //     totalIn: 1000,
  //     totalOut: 600,
  //     currentStock: 400,
  //     turnoverRate: 30
  //   }
  //   // ç”Ÿæˆå›¾è¡¨å‡æ•°æ®
  //   const trendDates = ['2025-09-15', '2025-09-16', '2025-09-17', '2025-09-18', '2025-09-19']
  //   const trendValues = [300, 350, 400, 380, 420]
  //   const comparisonDates = ['2025-09-15', '2025-09-16', '2025-09-17']
  //   const inValues = [100, 150, 200]
  //   const outValues = [80, 120, 100]
  //   const chartData = {
  //     trendDates,
  //     trendValues,
  //     comparisonDates,
  //     inValues,
  //     outValues
  //   }
  //   reportData.value = {
  //     summary,
  //     chartData,
  //     tableData: []
  //   }
  // }
  // éªŒè¯æœç´¢è¡¨å•
  const validateSearchForm = () => {
    if (searchForm.reportType === "daily") {
      if (!searchForm.singleDate) {
        ElMessage.warning("请选择日期");
        return false;
      }
    } else if (searchForm.reportType === "inout") {
      if (!searchForm.dateRange || searchForm.dateRange.length !== 2) {
        ElMessage.warning("请选择日期范围");
        return false;
      }
    } else if (searchForm.reportType === "monthly") {
      if (!searchForm.monthRange || searchForm.monthRange.length !== 2) {
        ElMessage.warning("请选择月份范围");
        return false;
      }
    }
    return true;
  };
  // èŽ·å–æŸ¥è¯¢å‚æ•°
  const getQueryParams = () => {
    const params = {
      ...baseParams,
      current: page.current,
      size: page.size,
    }
    let response
      reportType: searchForm.reportType,
      reportDate: "",
      startMonth: "",
      endMonth: "",
      startDate: "",
      endDate: "",
    };
    if (searchForm.reportType === 'inout') {
      response = await getStockInventoryInAndOutReportList(params)
    if (searchForm.reportType === "daily") {
      params.reportDate = searchForm.singleDate;
    } else if (searchForm.reportType === "monthly") {
      params.startMonth = searchForm.monthRange[0];
      params.endMonth = searchForm.monthRange[1];
    } else {
      response = await getStockInventoryReportList(params)
      params.startDate = searchForm.dateRange[0];
      params.endDate = searchForm.dateRange[1];
    }
    if (response.code === 200) {
      reportData.value.tableData = response.data.records || []
      total.value = response.data.total || 0
      // reportData.value.summary = response.data.summary
      // reportData.value.chartData = response.data.chartData
      // nextTick(() => {
      //   initCharts()
      // })
    return params;
  };
  // é‡ç½®æœç´¢
  const handleReset = () => {
    searchForm.reportType = "daily";
    searchForm.singleDate = "";
    searchForm.dateRange = [];
    searchForm.monthRange = [];
    reportData.value = {
      summary: null,
      chartData: null,
      tableData: [],
    };
  };
  // å¯¼å‡ºæŠ¥è¡¨
  const handleExport = async () => {
    if (!validateSearchForm()) {
      return;
    }
  } catch (error) {
    ElMessage.error('查询失败:' + error.message)
  } finally {
    tableLoading.value = false
  }
}
// æŸ¥è¯¢æŒ‰é’®ï¼šé‡ç½®åˆ°ç¬¬ä¸€é¡µå¹¶æŸ¥è¯¢
const onSearch = () => {
  page.current = 1
  handleQuery()
}
    try {
      const params = getQueryParams();
      // const response = await exportStockReport(params)
      proxy.download("/stockin/exportCopy", params, "库存报表.xlsx");
      // åˆ›å»ºä¸‹è½½é“¾æŽ¥
      // const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
      // const url = window.URL.createObjectURL(blob)
      // const link = document.createElement('a')
      // link.href = url
      // link.download = `${getTableTitle()}_${new Date().getTime()}.xlsx`
      // document.body.appendChild(link)
      // link.click()
      // document.body.removeChild(link)
      // window.URL.revokeObjectURL(url)
// åˆ†é¡µå˜åŒ–
const paginationChange = (obj) => {
  page.current = obj.page
  page.size = obj.limit
  handleQuery()
}
// // ç”Ÿæˆå‡æ•°æ®
// const generateMockData = () => {
//   // ç”Ÿæˆç»Ÿè®¡å¡ç‰‡å‡æ•°æ®
//   const summary = {
//     totalIn: 1000,
//     totalOut: 600,
//     currentStock: 400,
//     turnoverRate: 30
//   }
//   // ç”Ÿæˆå›¾è¡¨å‡æ•°æ®
//   const trendDates = ['2025-09-15', '2025-09-16', '2025-09-17', '2025-09-18', '2025-09-19']
//   const trendValues = [300, 350, 400, 380, 420]
//   const comparisonDates = ['2025-09-15', '2025-09-16', '2025-09-17']
//   const inValues = [100, 150, 200]
//   const outValues = [80, 120, 100]
//   const chartData = {
//     trendDates,
//     trendValues,
//     comparisonDates,
//     inValues,
//     outValues
//   }
//   reportData.value = {
//     summary,
//     chartData,
//     tableData: []
//   }
// }
// éªŒè¯æœç´¢è¡¨å•
const validateSearchForm = () => {
  if (searchForm.reportType === 'daily') {
    if (!searchForm.singleDate) {
      ElMessage.warning('请选择日期')
      return false
      // ElMessage.success('导出成功')
    } catch (error) {
      ElMessage.error("导出失败:" + error.message);
    }
  } else if (searchForm.reportType === 'inout') {
    if (!searchForm.dateRange || searchForm.dateRange.length !== 2) {
      ElMessage.warning('请选择日期范围')
      return false
    }
  } else if (searchForm.reportType === 'monthly') {
    if (!searchForm.monthRange || searchForm.monthRange.length !== 2) {
      ElMessage.warning('请选择月份范围')
      return false
    }
  }
  return true
}
  };
// èŽ·å–æŸ¥è¯¢å‚æ•°
const getQueryParams = () => {
  const params = {
    reportType: searchForm.reportType,
    reportDate: "",
    startMonth: "",
    endMonth: "",
    startDate: "",
    endDate: ""
  }
  if (searchForm.reportType === 'daily') {
    params.reportDate = searchForm.singleDate
  } else if (searchForm.reportType === 'monthly') {
    params.startMonth = searchForm.monthRange[0]
    params.endMonth = searchForm.monthRange[1]
  } else {
    params.startDate = searchForm.dateRange[0]
    params.endDate = searchForm.dateRange[1]
  }
  return params
}
  // åˆå§‹åŒ–图表
  const initCharts = () => {
    if (!reportData.value.chartData) return;
// é‡ç½®æœç´¢
const handleReset = () => {
  searchForm.reportType = 'daily'
  searchForm.singleDate = ''
  searchForm.dateRange = []
  searchForm.monthRange = []
  reportData.value = {
    summary: null,
    chartData: null,
    tableData: []
  }
}
    initTrendChart();
    initComparisonChart();
  };
// å¯¼å‡ºæŠ¥è¡¨
const handleExport = async () => {
  if (!validateSearchForm()) {
    return
  }
  try {
    const params = getQueryParams()
    // const response = await exportStockReport(params)
    proxy.download("/stockin/exportCopy", params, '库存报表.xlsx')
    // åˆ›å»ºä¸‹è½½é“¾æŽ¥
    // const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
    // const url = window.URL.createObjectURL(blob)
    // const link = document.createElement('a')
    // link.href = url
    // link.download = `${getTableTitle()}_${new Date().getTime()}.xlsx`
    // document.body.appendChild(link)
    // link.click()
    // document.body.removeChild(link)
    // window.URL.revokeObjectURL(url)
    // ElMessage.success('导出成功')
  } catch (error) {
    ElMessage.error('导出失败:' + error.message)
  }
}
  // åˆå§‹åŒ–趋势图
  const initTrendChart = () => {
    if (!trendChart.value) return;
// åˆå§‹åŒ–图表
const initCharts = () => {
  if (!reportData.value.chartData) return
  initTrendChart()
  initComparisonChart()
}
// åˆå§‹åŒ–趋势图
const initTrendChart = () => {
  if (!trendChart.value) return
  const chart = echarts.init(trendChart.value)
  const option = {
    title: {
      text: '库存变化趋势',
      left: 'center'
    },
    tooltip: {
      trigger: 'axis'
    },
    legend: {
      data: ['库存量'],
      top: 30
    },
    xAxis: {
      type: 'category',
      data: reportData.value.chartData.trendDates || []
    },
    yAxis: {
      type: 'value'
    },
    series: [{
      name: '库存量',
      type: 'line',
      data: reportData.value.chartData.trendValues || [],
      smooth: true,
      itemStyle: {
        color: '#409EFF'
      }
    }]
  }
  chart.setOption(option)
}
// åˆå§‹åŒ–对比图
const initComparisonChart = () => {
  if (!comparisonChart.value) return
  const chart = echarts.init(comparisonChart.value)
  const option = {
    title: {
      text: '进出库对比',
      left: 'center'
    },
    tooltip: {
      trigger: 'axis'
    },
    legend: {
      data: ['入库', '出库'],
      top: 30
    },
    xAxis: {
      type: 'category',
      data: reportData.value.chartData.comparisonDates || []
    },
    yAxis: {
      type: 'value'
    },
    series: [
      {
        name: '入库',
        type: 'bar',
        data: reportData.value.chartData.inValues || [],
        itemStyle: {
          color: '#67C23A'
        }
    const chart = echarts.init(trendChart.value);
    const option = {
      title: {
        text: "库存变化趋势",
        left: "center",
      },
      {
        name: '出库',
        type: 'bar',
        data: reportData.value.chartData.outValues || [],
        itemStyle: {
          color: '#F56C6C'
        }
      }
    ]
  }
  chart.setOption(option)
}
      tooltip: {
        trigger: "axis",
      },
      legend: {
        data: ["库存量"],
        top: 30,
      },
      xAxis: {
        type: "category",
        data: reportData.value.chartData.trendDates || [],
      },
      yAxis: {
        type: "value",
      },
      series: [
        {
          name: "库存量",
          type: "line",
          data: reportData.value.chartData.trendValues || [],
          smooth: true,
          itemStyle: {
            color: "#409EFF",
          },
        },
      ],
    };
    chart.setOption(option);
  };
// ç»„件挂载时设置默认时间
onMounted(() => {
  const today = new Date()
  searchForm.singleDate = today.toISOString().split('T')[0]
  const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000)
  searchForm.dateRange = [
    yesterday.toISOString().split('T')[0],
    today.toISOString().split('T')[0]
  ]
  // åˆå§‹åŒ–对比图
  const initComparisonChart = () => {
    if (!comparisonChart.value) return;
  fetchStockRecordTypeOptions()
  // åˆå§‹åŒ–加载一次数据
  handleQuery()
})
    const chart = echarts.init(comparisonChart.value);
    const option = {
      title: {
        text: "进出库对比",
        left: "center",
      },
      tooltip: {
        trigger: "axis",
      },
      legend: {
        data: ["入库", "出库"],
        top: 30,
      },
      xAxis: {
        type: "category",
        data: reportData.value.chartData.comparisonDates || [],
      },
      yAxis: {
        type: "value",
      },
      series: [
        {
          name: "入库",
          type: "bar",
          data: reportData.value.chartData.inValues || [],
          itemStyle: {
            color: "#67C23A",
          },
        },
        {
          name: "出库",
          type: "bar",
          data: reportData.value.chartData.outValues || [],
          itemStyle: {
            color: "#F56C6C",
          },
        },
      ],
    };
    chart.setOption(option);
  };
  // ç»„件挂载时设置默认时间
  onMounted(() => {
    const today = new Date();
    searchForm.singleDate = today.toISOString().split("T")[0];
    const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
    searchForm.dateRange = [
      yesterday.toISOString().split("T")[0],
      today.toISOString().split("T")[0],
    ];
    fetchStockRecordTypeOptions();
    // åˆå§‹åŒ–加载一次数据
    handleQuery();
  });
</script>
<style scoped>
.app-container {
  padding: 20px;
}
  .app-container {
    padding: 20px;
  }
.search_form {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  padding: 20px;
  background: #fff;
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
  .search_form {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    padding: 20px;
    background: #fff;
    border-radius: 4px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }
.search_left {
  display: flex;
  align-items: center;
}
  .search_left {
    display: flex;
    align-items: center;
  }
.search_title {
  font-weight: 500;
  color: #333;
  margin-right: 8px;
}
  .search_title {
    font-weight: 500;
    color: #333;
    margin-right: 8px;
  }
.ml10 {
  margin-left: 10px;
}
  .ml10 {
    margin-left: 10px;
  }
.stats_cards {
  margin-bottom: 20px;
}
  .stats_cards {
    margin-bottom: 20px;
  }
.stats_card {
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
  .stats_card {
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  }
.stats_content {
  display: flex;
  align-items: center;
  padding: 10px 0;
}
  .stats_content {
    display: flex;
    align-items: center;
    padding: 10px 0;
  }
.stats_icon {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 15px;
  font-size: 24px;
  color: #fff;
}
  .stats_icon {
    width: 50px;
    height: 50px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 15px;
    font-size: 24px;
    color: #fff;
  }
.stats_icon.in {
  background: linear-gradient(135deg, #67C23A, #85CE61);
}
  .stats_icon.in {
    background: linear-gradient(135deg, #67c23a, #85ce61);
  }
.stats_icon.out {
  background: linear-gradient(135deg, #F56C6C, #F78989);
}
  .stats_icon.out {
    background: linear-gradient(135deg, #f56c6c, #f78989);
  }
.stats_icon.stock {
  background: linear-gradient(135deg, #409EFF, #66B1FF);
}
  .stats_icon.stock {
    background: linear-gradient(135deg, #409eff, #66b1ff);
  }
.stats_icon.turnover {
  background: linear-gradient(135deg, #E6A23C, #EEBE77);
}
  .stats_icon.turnover {
    background: linear-gradient(135deg, #e6a23c, #eebe77);
  }
.stats_info {
  flex: 1;
}
  .stats_info {
    flex: 1;
  }
.stats_value {
  font-size: 24px;
  font-weight: bold;
  color: #333;
  line-height: 1;
  margin-bottom: 5px;
}
  .stats_value {
    font-size: 24px;
    font-weight: bold;
    color: #333;
    line-height: 1;
    margin-bottom: 5px;
  }
.stats_label {
  font-size: 14px;
  color: #666;
}
  .stats_label {
    font-size: 14px;
    color: #666;
  }
.chart_section {
  margin-bottom: 20px;
}
  .chart_section {
    margin-bottom: 20px;
  }
.table_section {
  margin-bottom: 20px;
}
  .table_section {
    margin-bottom: 20px;
  }
:deep(.el-card__header) {
  background: #f8f9fa;
  border-bottom: 1px solid #e9ecef;
  font-weight: 500;
}
  :deep(.el-card__header) {
    background: #f8f9fa;
    border-bottom: 1px solid #e9ecef;
    font-weight: 500;
  }
:deep(.el-table .el-table__header-wrapper th) {
  background-color: #F0F1F5 !important;
  color: #333333;
  font-weight: 600;
}
  :deep(.el-table .el-table__header-wrapper th) {
    background-color: #f0f1f5 !important;
    color: #333333;
    font-weight: 600;
  }
:deep(.el-table .el-table__body-wrapper td) {
  padding: 8px 0;
}
  :deep(.el-table .el-table__body-wrapper td) {
    padding: 8px 0;
  }
</style>
src/views/personnelManagement/dimission/components/formDia.vue
@@ -102,6 +102,7 @@
                    v-model="form.leaveDate"
                    type="date"
                    :disabled="operationType === 'edit'"
                    :disabled-date="disabledFutureDate"
                    placeholder="请选择离职日期"
                    value-format="YYYY-MM-DD"
                    format="YYYY-MM-DD"
@@ -180,6 +181,19 @@
const dialogFormVisible = ref(false);
const operationType = ref('')
const getTodayDate = () => {
  const now = new Date();
  const year = now.getFullYear();
  const month = `${now.getMonth() + 1}`.padStart(2, '0');
  const day = `${now.getDate()}`.padStart(2, '0');
  return `${year}-${month}-${day}`;
};
const disabledFutureDate = (time) => {
  const todayEnd = new Date();
  todayEnd.setHours(23, 59, 59, 999);
  return time.getTime() > todayEnd.getTime();
};
const data = reactive({
  form: {
    staffOnJobId: undefined,
@@ -220,6 +234,7 @@
      }
    ]
  } else {
    form.value.leaveDate = getTodayDate()
    getList()
  }
}
@@ -329,4 +344,4 @@
  color: #303133;
  font-size: 14px;
}
</style>
</style>
src/views/procurementManagement/purchaseReturnOrder/index.vue
@@ -88,10 +88,10 @@
          <el-table-column label="含税单价(元)" prop="taxInclusiveUnitPrice" width="130">
            <template #default="scope">{{ formatAmount(scope.row.taxInclusiveUnitPrice) }}</template>
          </el-table-column>
          <el-table-column label="含税总价(元)" prop="taxInclusiveTotalPrice" width="130">
          <el-table-column label="退货总价(元)" prop="taxInclusiveTotalPrice" width="130">
            <template #default="scope">{{ formatAmount(scope.row.taxInclusiveTotalPrice) }}</template>
          </el-table-column>
          <el-table-column label="不含税总价(元)" prop="taxExclusiveTotalPrice" width="140">
          <el-table-column label="不退货总价(元)" prop="taxExclusiveTotalPrice" width="140">
            <template #default="scope">{{ formatAmount(scope.row.taxExclusiveTotalPrice) }}</template>
          </el-table-column>
          <el-table-column label="是否质检" prop="isChecked" width="100" align="center">
@@ -182,7 +182,7 @@
    prop: 'returnUserName',
    width: 110,
  },
  {
    label: '整单折扣额',
    prop: 'totalDiscountAmount',
@@ -236,7 +236,7 @@
      },
  ],
  },
])
const data = reactive({
  searchForm: {
@@ -281,8 +281,8 @@
    const payload = res?.data || {}
    detailData.value = payload
    // æ‹¼æŽ¥è¿žä¸ªå¯¹è±¡æˆä¸€ä¸ªå¯¹è±¡ï¼Œæ–¹ä¾¿å±•示 item å’Œ item.salesLedgerProduct é‡Œçš„字段
    detailProducts.value =
      payload.purchaseReturnOrderProductsDetailVoList.map(item => ({ ...item, ...item.salesLedgerProduct })) ||
      []
src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -111,6 +111,13 @@
      <el-table-column label="单位"
                       prop="unit"
                       width="100" />
      <el-table-column label="计费类型"
                       prop="type"
                       width="100">
        <template #default="scope">
          {{scope.row.type==0 ? "计时" : "计件"}}
        </template>
      </el-table-column>
      <el-table-column label="是否质检"
                       prop="isQuality"
                       width="100">
@@ -181,12 +188,16 @@
                  {{ item.model }}
                  <!-- <span v-if="item.unit" class="product-unit">{{ item.unit }}</span> -->
                </div>
                <el-tag class="product-tag"
                        :type="item.type == 1 ? 'primary' : 'success'"
                        style="margin-left: 8px;">{{ item.type==0?'计时':'计件' }}</el-tag>
                <el-tag type="primary"
                        class="product-tag"
                        style="margin-left: 8px;"
                        v-if="item.isQuality">质检</el-tag>
                <el-tag type="primary"
                        class="product-tag"
                        :style="item.isQuality?'margin-left:8px':''"
                        style="margin-left: 8px;"
                        v-if="item.isProduction">生产</el-tag>
              </div>
              <div v-else
@@ -424,6 +435,13 @@
                      v-else>
          <span>{{ form.unit }}</span>
        </el-form-item>
        <el-form-item label="计费类型"
                      prop="type">
          <el-radio-group v-model="form.type">
            <el-radio :label="0">计时</el-radio>
            <el-radio :label="1">计件</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="是否质检"
                      prop="isQuality">
          <el-switch v-model="form.isQuality"
@@ -558,6 +576,7 @@
    model: "",
    unit: "",
    isQuality: false,
    type: 0,
    isProduction: false,
  });
@@ -687,6 +706,7 @@
      model: row.model || "",
      unit: row.unit || "",
      isQuality: row.isQuality,
      type: row.type || 0,
      isProduction: row.isProduction,
    };
    dialogVisible.value = true;
@@ -758,6 +778,7 @@
                operationName: getProcessName(form.value.technologyOperationId),
                productModelId: form.value.productModelId,
                isQuality: form.value.isQuality,
                type: form.value.type,
                isProduction: form.value.isProduction,
                dragSort,
              })
@@ -766,6 +787,7 @@
                technologyOperationId: form.value.technologyOperationId,
                productModelId: form.value.productModelId,
                isQuality: form.value.isQuality,
                type: form.value.type,
                isProduction: form.value.isProduction,
                dragSort,
              });
@@ -793,6 +815,7 @@
                operationName: getProcessName(form.value.technologyOperationId),
                productModelId: form.value.productModelId,
                isQuality: form.value.isQuality,
                type: form.value.type,
                isProduction: form.value.isProduction,
              })
            : addOrUpdateProcessRouteItem1({
@@ -801,6 +824,7 @@
                productModelId: form.value.productModelId,
                id: form.value.id,
                isQuality: form.value.isQuality,
                type: form.value.type,
                isProduction: form.value.isProduction,
              });
@@ -832,6 +856,7 @@
      model: "",
      unit: "",
      isQuality: false,
      type: 0,
      isProduction: false,
    };
    formRef.value?.resetFields();
@@ -1068,6 +1093,7 @@
    processOptions.value.forEach(item => {
      if (item.id == value) {
        form.value.isQuality = item.isQuality;
        form.value.type = item.type || 0;
        form.value.isProduction = item.isProduction;
      }
    });
src/views/productionManagement/productionCosting/index.vue
@@ -130,6 +130,11 @@
      minWidth: 100,
    },
    {
      label: "工时(h)",
      prop: "workHour",
      minWidth: 100,
    },
    {
      label: "生产数量",
      prop: "quantity",
      minWidth: 100,
src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
@@ -312,12 +312,20 @@
        !item.materialName ||
        (Number(item.pickQty) > 0 &&
          (!item.batchNo || item.batchNo.length === 0)) ||
        (item.batchNo && item.batchNo.length > 0 && Number(item.pickQty) <= 0) ||
        item.demandedQuantity === null ||
        item.demandedQuantity === undefined ||
        item.pickQty === null ||
        item.pickQty === undefined
    );
    if (invalidRow) {
      if (
        invalidRow.batchNo &&
        invalidRow.batchNo.length > 0 &&
        Number(invalidRow.pickQty) <= 0
      ) {
        return { valid: false, message: "选择了批号时,领用数量必须大于零" };
      }
      return { valid: false, message: "请完善工序、原料、批号和数量后再保存" };
    }
    return { valid: true, message: "" };
src/views/productionManagement/productionOrder/index.vue
@@ -617,7 +617,7 @@
  const openBindRouteDialog = async (row, type) => {
    bindForm.orderId = row.id;
    bindForm.routeId = type === "add" ? null : row.processRouteCode;
    bindForm.routeId = type === "add" ? null : row.technologyRoutingId;
    bindRouteDialogVisible.value = true;
    routeOptions.value = [];
    if (!row.productModelId) {
src/views/productionManagement/productionProcess/index.vue
@@ -162,7 +162,8 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary" @click="handleProcessSubmit">确定</el-button>
          <el-button type="primary"
                     @click="handleProcessSubmit">确定</el-button>
          <el-button @click="processDialogVisible = false">取消</el-button>
        </span>
      </template>
@@ -251,7 +252,9 @@
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary" :disabled="!selectedParam" @click="handleParamSubmit">确定</el-button>
          <el-button type="primary"
                     :disabled="!selectedParam"
                     @click="handleParamSubmit">确定</el-button>
          <el-button @click="paramDialogVisible = false">取消</el-button>
        </span>
      </template>
@@ -275,9 +278,9 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="editParamDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleEditParamSubmit">确定</el-button>
          <el-button @click="editParamDialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
src/views/productionManagement/productionReporting/index.vue
@@ -187,6 +187,11 @@
      width: 120,
    },
    {
      label: "工时(h)",
      width: 100,
      prop: "workHour",
    },
    {
      label: "工序",
      prop: "process",
      width: 120,
src/views/productionManagement/productionTraceability/index.vue
@@ -80,11 +80,17 @@
                  {{ row.workOrder.model || '-' }}
                </template>
              </el-table-column>
              <el-table-column label="工序"
                               prop="workOrder.operationName"
                               align="center" />
              <el-table-column prop="workOrder.planQuantity"
                               label="需求数量"
                               align="center" />
              <el-table-column prop="workOrder.completeQuantity"
                               label="完成数量"
                               align="center" />
              <el-table-column prop="workOrder.scrapQty"
                               label="报废数量"
                               align="center" />
              <el-table-column prop="workOrder.completionStatus"
                               label="完成进度"
@@ -154,6 +160,15 @@
                {{ parseTime(row.createTime) }}
              </template>
            </el-table-column>
            <el-table-column label="工时(h)"
                             prop="workHour"
                             align="center" />
            <el-table-column label="产出数量"
                             prop="quantity"
                             align="center" />
            <el-table-column label="报废数量"
                             prop="scrapQty"
                             align="center" />
            <el-table-column label="操作"
                             align="center"
                             width="200">
@@ -387,6 +402,7 @@
      workOrder: row.workOrder || {},
      reports: (row.reportList || []).map(r => ({
        ...r.reportMain,
        ...(r.reportOutputList[0] || {}),
        productionOperationParamList: r.reportParamList || [],
      })),
    };
src/views/productionManagement/workOrderEdit/index.vue
@@ -111,9 +111,9 @@
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="assignReporterDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleSaveReporters">确定</el-button>
          <el-button @click="assignReporterDialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
src/views/productionManagement/workOrderManagement/index.vue
@@ -161,6 +161,17 @@
                       :value="user.userId" />
          </el-select>
        </el-form-item>
        <!-- å·¥æ—¶ -->
        <el-form-item label="工时"
                      v-if="currentReportRowData?.type == 0"
                      prop="workHour">
          <el-input v-model.number="reportForm.workHour"
                    type="number"
                    min="0"
                    style="width: 280px"
                    placeholder="请输入工时" /><span style="margin-left:10px"
                class="param-unit">h</span>
        </el-form-item>
        <div v-if="params.length > 0"
             class="param-grid"
             v-loading="paramLoading">
@@ -259,6 +270,7 @@
    addProductMain,
    downProductWorkOrder,
  } from "@/api/productionManagement/workOrder.js";
  import { listMaterialPickingDetail } from "@/api/productionManagement/productionOrder.js";
  import { findProcessParamListOrder } from "@/api/productionManagement/productProcessRoute.js";
  import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js";
  import { getDicts } from "@/api/system/dict/data";
@@ -423,6 +435,7 @@
    productMainId: null,
    productionOrderRoutingOperationId: "",
    productionOrderId: "",
    workHour: 0,
    paramGroups: {},
  });
@@ -649,7 +662,21 @@
    fileDialogVisible.value = true;
  };
  const showReportDialog = row => {
  const showReportDialog = async row => {
    if (row.productionOrderId) {
      try {
        const res = await listMaterialPickingDetail(row.productionOrderId);
        const records = Array.isArray(res.data)
          ? res.data
          : res.data?.records || [];
        if (res.code === 200 && records.length === 0) {
          proxy.$modal.msgError("未领料无法报工");
          return;
        }
      } catch (error) {
        console.error("查询领料详情失败:", error);
      }
    }
    currentReportRowData.value = row;
    reportForm.planQuantity = row.planQuantity;
    reportForm.quantity =
@@ -663,6 +690,11 @@
    reportForm.productionOrderRoutingOperationId =
      row.productionOrderRoutingOperationId;
    reportForm.productionOrderId = row.productionOrderId;
    if (row.type == 0) {
      reportForm.workHour = row.workHour || 0;
    } else {
      reportForm.workHour = 0;
    }
    nextTick(() => {
      reportFormRef.value?.clearValidate();
      if (row.productionOrderRoutingOperationId && row.productionOrderId) {
@@ -762,6 +794,7 @@
        productionOrderRoutingOperationId:
          reportForm.productionOrderRoutingOperationId,
        productionOrderId: reportForm.productionOrderId,
        workHour: reportForm.workHour,
        productionOperationParamList: productionOperationParamList,
      };
src/views/productionPlan/productionPlan/index.vue
@@ -13,6 +13,14 @@
                    style="width: 160px;"
                    @keyup.enter="handleQuery" />
        </el-form-item>
        <el-form-item label="销售合同号:"
                      prop="salesContractNo">
          <el-input v-model="searchForm.salesContractNo"
                    placeholder="请输入"
                    clearable
                    style="width: 160px;"
                    @keyup.enter="handleQuery" />
        </el-form-item>
        <el-form-item label="需求日期范围:"
                      prop="dateRange">
          <el-date-picker v-model="searchForm.dateRange"
@@ -146,7 +154,8 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary" @click="handleMergeSubmit">确定下发</el-button>
          <el-button type="primary"
                     @click="handleMergeSubmit">确定下发</el-button>
          <el-button @click="isShowNewModal = false">取消</el-button>
        </span>
      </template>
@@ -239,7 +248,8 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确定</el-button>
          <el-button type="primary"
                     @click="handleSubmit">确定</el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
        </span>
      </template>
@@ -269,6 +279,7 @@
    productionPlanUpdate,
    productionPlanDelete,
    productionPlanCombine,
    exportProductionPlan,
  } from "@/api/productionPlan/productionPlan.js";
  import { productTreeList, modelListPage } from "@/api/basicData/product.js";
  import { workshopPage } from "@/api/basicData/workshop.js";
@@ -281,11 +292,6 @@
  const loadProdData = () => {
    console.log("Mock loadProdData called");
    return Promise.resolve({ code: 200, msg: "同步成功" });
  };
  const exportProductionPlan = () => {
    console.log("Mock exportProductionPlan called");
    return Promise.resolve();
  };
  // const productionPlanCombine = payload => {
@@ -650,6 +656,7 @@
  const data = reactive({
    searchForm: {
      mpsNo: "",
      salesContractNo: "",
      productName: "",
      model: "",
      status: "",
@@ -678,6 +685,7 @@
    }
    Object.assign(searchForm.value, {
      mpsNo: "",
      salesContractNo: "",
      productName: "",
      model: "",
      status: "",
src/views/salesManagement/deliveryLedger/index.vue
@@ -56,7 +56,7 @@
                link
                type="primary"
                :disabled="!isApproved(scope.row.status)"
                @click="openForm('edit', scope.row)">补充发货信息
                @click="openForm('edit', scope.row)">发货
            </el-button>
            <el-button
                link
@@ -319,9 +319,9 @@
// æ‰“开弹框
const openForm = async (type, row) => {
  // è¡¥å……发货信息:仅“审核通过”允许编辑
  // å‘货:仅“审核通过”允许编辑
  if (type === 'edit' && row && !isApproved(row.status)) {
    proxy.$modal.msgWarning("只有审核通过的数据才可以补充发货信息");
    proxy.$modal.msgWarning("只有审核通过的数据才可以发货");
    return;
  }
src/views/salesManagement/salesLedger/index.vue
ÎļþÌ«´ó