zhangwencui
5 天以前 12afd5c45d5afc25cd6d6650399401537870dbcd
Merge branch 'dev_NEW_pro' of http://114.132.189.42:9002/r/product-inventory-management into dev_NEW_pro
已添加10个文件
已修改8个文件
2621 ■■■■ 文件已修改
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/equipmentManagement/operationManagement/index.vue 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/dispatchLog/Record.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/receiptManagement/Record.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/index.vue 12 ●●●● 补丁 | 查看 | 原始文档 | 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/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/procurementManagement/purchaseReturnOrder/index.vue
@@ -88,10 +88,10 @@
          <el-table-column label="含税单价(元)" prop="taxInclusiveUnitPrice" width="130">
            <template #default="scope">{{ formatAmount(scope.row.taxInclusiveUnitPrice) }}</template>
          </el-table-column>
          <el-table-column label="含税总价(元)" prop="taxInclusiveTotalPrice" width="130">
          <el-table-column label="退货总价(元)" prop="taxInclusiveTotalPrice" width="130">
            <template #default="scope">{{ formatAmount(scope.row.taxInclusiveTotalPrice) }}</template>
          </el-table-column>
          <el-table-column label="不含税总价(元)" prop="taxExclusiveTotalPrice" width="140">
          <el-table-column label="不退货总价(元)" prop="taxExclusiveTotalPrice" width="140">
            <template #default="scope">{{ formatAmount(scope.row.taxExclusiveTotalPrice) }}</template>
          </el-table-column>
          <el-table-column label="是否质检" prop="isChecked" width="100" align="center">
@@ -182,7 +182,7 @@
    prop: 'returnUserName',
    width: 110,
  },
  {
    label: '整单折扣额',
    prop: 'totalDiscountAmount',
@@ -236,7 +236,7 @@
      },
  ],
  },
])
const data = reactive({
  searchForm: {
@@ -281,8 +281,8 @@
    const payload = res?.data || {}
    detailData.value = payload
    // æ‹¼æŽ¥è¿žä¸ªå¯¹è±¡æˆä¸€ä¸ªå¯¹è±¡ï¼Œæ–¹ä¾¿å±•示 item å’Œ item.salesLedgerProduct é‡Œçš„字段
    detailProducts.value =
      payload.purchaseReturnOrderProductsDetailVoList.map(item => ({ ...item, ...item.salesLedgerProduct })) ||
      []