buhuazhen
5 天以前 420df5a82f02b82f24b08720d60aebce1241aaef
Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_NEW_pro
已添加10个文件
已修改9个文件
4755 ■■■■■ 文件已修改
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/index.vue 709 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 12 ●●●●● 补丁 | 查看 | 原始文档 | 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/customerService/feedbackRegistration/components/formDia.vue 895 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/operationManagement/index.vue 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/dispatchLog/Record.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/receiptManagement/Record.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockReport/index.vue 1251 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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/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">
@@ -167,10 +171,6 @@
                <span>换一换</span>
              </button>
            </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">
@@ -525,6 +525,14 @@
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: {
@@ -534,12 +542,50 @@
  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 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 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)
@@ -564,11 +610,12 @@
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)
@@ -1045,11 +1092,10 @@
}
onMounted(() => {
  initUUID()
  // åˆå§‹æ¬¢è¿Ž
  if (messages.value.length === 0) {
    hello()
  if (props.autoOpen) {
    visible.value = true
  }
  initUUID()
  window.addEventListener('resize', handleWindowResize)
})
@@ -1074,11 +1120,26 @@
  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 = () => {
@@ -1089,10 +1150,12 @@
}
const handleClose = () => {
  if (hideTrigger.value) return
  visible.value = false
}
const handleManualClose = () => {
  if (hideTrigger.value) return
  if (isSending.value) {
    abortCurrentRequest()
  }
@@ -1108,10 +1171,6 @@
  uuid.value = storedUUID
}
const hello = () => {
  sendRequest(currentAssistant.value.welcomeMessage || '你好')
}
const newChat = () => {
  revokeMessageLocalFileSnapshots(messages.value)
  disposeCharts()
@@ -1123,7 +1182,6 @@
  quickPromptStart.value = 0
  localStorage.removeItem(currentAssistant.value.storageKey)
  initUUID()
  hello()
}
const handleNewChat = () => {
@@ -3043,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;
  }
@@ -3055,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;
@@ -3187,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 {
@@ -3269,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;
  }
@@ -4196,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 {
@@ -4278,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);
@@ -4510,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 {
@@ -4625,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,
@@ -4636,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);
@@ -4645,8 +4673,8 @@
    }
    .welcome-title {
      font-size: 17px;
      line-height: 1.3;
      font-size: 16px;
      line-height: 1.25;
      br {
        display: none;
@@ -4654,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;
@@ -4760,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;
@@ -4782,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));
  }
}
@@ -4892,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/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,
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/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.records){
    customerNameOptions.value = res.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/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>
@@ -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;
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>