From 3b577bb9493c389c5a3f0dca4538ddbf41fb1387 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期三, 29 四月 2026 11:17:51 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_NEW_pro
---
src/views/equipmentManagement/upkeep/index.vue | 88
src/views/equipmentManagement/upkeep/Form/PlanModal.vue | 22
src/views/equipmentManagement/repair/Modal/RepairModal.vue | 16
src/layout/index.vue | 264 +-
src/views/equipmentManagement/repair/index.vue | 22
src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue | 159 +
src/api/productionManagement/productionOrder.js | 2
src/components/AIChatSidebar/index.vue | 3001 ++++++++++++++++++++++++++++++++++
src/views/productionManagement/productionProcess/Edit.vue | 168 +
src/views/productionManagement/workOrderEdit/index.vue | 447 ++--
src/views/productionManagement/workOrderManagement/index.vue | 206 ++
src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue | 72
src/views/productionManagement/productionReporting/index.vue | 61
src/views/productionManagement/productionOrder/index.vue | 78
src/api/basicData/storageAttachment.js | 29
src/components/Dialog/FileList.vue | 253 ++
vite.config.js | 2
src/components/PurchaseAIChatSidebar/index.vue | 24
src/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue | 225 ++
src/views/productionManagement/productionProcess/New.vue | 129 +
20 files changed, 4,767 insertions(+), 501 deletions(-)
diff --git a/src/api/basicData/storageAttachment.js b/src/api/basicData/storageAttachment.js
new file mode 100644
index 0000000..56364a4
--- /dev/null
+++ b/src/api/basicData/storageAttachment.js
@@ -0,0 +1,29 @@
+// 闄勪欢椤甸潰鎺ュ彛
+import request from '@/utils/request'
+
+// 闄勪欢鏌ヨ
+export function attachmentList(query) {
+ return request({
+ url: '/basic/storage_attachment/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 闄勪欢鏂板
+export function createAttachment(data) {
+ return request({
+ url: '/basic/storage_attachment/add',
+ method: 'post',
+ data
+ })
+}
+
+// 闄勪欢鍒犻櫎
+export function deleteAttachment(data) {
+ return request({
+ url: '/basic/storage_attachment/delete',
+ method: 'delete',
+ data
+ })
+}
diff --git a/src/api/productionManagement/productionOrder.js b/src/api/productionManagement/productionOrder.js
index b87adbb..688abfc 100644
--- a/src/api/productionManagement/productionOrder.js
+++ b/src/api/productionManagement/productionOrder.js
@@ -116,7 +116,7 @@
// 鐢熶骇璁㈠崟-琛ユ枡璁板綍鍒楄〃
export function listMaterialSupplementRecord(query) {
return request({
- url: "/productOrderMaterial/supplementRecord",
+ url: "/productionOrderPickRecord/feeding",
method: "get",
params: query,
});
diff --git a/src/components/AIChatSidebar/index.vue b/src/components/AIChatSidebar/index.vue
new file mode 100644
index 0000000..548ec31
--- /dev/null
+++ b/src/components/AIChatSidebar/index.vue
@@ -0,0 +1,3001 @@
+<template>
+ <div class="ai-chat-sidebar-wrapper">
+ <!-- 鎮诞鍥炬爣 -->
+ <div 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>
+ </div>
+ </el-tooltip>
+ </div>
+
+ <!-- 渚ц竟鏍忓璇濇 -->
+ <el-drawer
+ v-model="visible"
+ :size="drawerSize"
+ direction="rtl"
+ :with-header="true"
+ class="ai-chat-drawer"
+ :modal="false"
+ modal-class="ai-chat-overlay"
+ :show-close="false"
+ :append-to-body="false"
+ @close="handleClose"
+ >
+ <template #header>
+ <div class="drawer-header">
+ <div class="header-left">
+ <el-icon :size="20" class="header-icon"><component :is="currentAssistant.icon" /></el-icon>
+ <span class="title">{{ currentAssistant.title }}</span>
+ </div>
+ <div v-if="showAssistantSwitch" class="assistant-switcher">
+ <el-radio-group v-model="selectedAssistantKey" size="small">
+ <el-radio-button
+ v-for="assistant in assistants"
+ :key="assistant.key"
+ :label="assistant.key"
+ >
+ {{ assistant.label }}
+ </el-radio-button>
+ </el-radio-group>
+ </div>
+ <div class="header-actions">
+ <el-tooltip content="浼氳瘽鍘嗗彶" placement="bottom">
+ <el-button link class="header-action-btn" @click="handleToggleHistory">
+ <el-icon :size="18"><Timer /></el-icon>
+ </el-button>
+ </el-tooltip>
+ <el-tooltip content="寮�鍚柊浼氳瘽" placement="bottom">
+ <el-button link class="header-action-btn" @click="handleNewChat">
+ <el-icon :size="18"><Plus /></el-icon>
+ </el-button>
+ </el-tooltip>
+ <div class="action-divider"></div>
+ <el-tooltip content="鍏抽棴" placement="bottom">
+ <el-button link class="header-action-btn close-btn" @click="handleManualClose">
+ <el-icon :size="18"><Close /></el-icon>
+ </el-button>
+ </el-tooltip>
+ </div>
+ </div>
+ </template>
+
+ <div class="chat-container">
+ <!-- 鍘嗗彶浼氳瘽鍒楄〃 -->
+ <div v-if="showHistory" class="history-panel">
+ <div class="history-header">
+ <span>鏈�杩戜細璇�</span>
+ <el-button link type="primary" @click="showHistory = false">杩斿洖瀵硅瘽</el-button>
+ </div>
+ <el-skeleton :loading="loadingSessions" animated>
+ <template #template>
+ <div v-for="i in 5" :key="i" style="padding: 10px">
+ <el-skeleton-item variant="p" style="width: 80%" />
+ </div>
+ </template>
+ <div class="session-list">
+ <div
+ v-for="session in sessions"
+ :key="session.memoryId"
+ :class="['session-item', { active: uuid === session.memoryId }]"
+ @click="selectSession(session)"
+ >
+ <el-icon><ChatDotSquare /></el-icon>
+ <span class="session-name" :title="session.lastMessage || '鏂颁細璇�'">
+ {{ session.lastMessage || '鏂颁細璇�' }}
+ </span>
+ <el-button
+ link
+ type="danger"
+ class="delete-btn"
+ @click.stop="handleDeleteSession(session.memoryId)"
+ >
+ <el-icon><Delete /></el-icon>
+ </el-button>
+ </div>
+ <el-empty v-if="sessions.length === 0" :description="currentAssistant.emptySessionText" />
+ </div>
+ </el-skeleton>
+ </div>
+
+ <div v-else class="chat-main">
+ <div :class="['chat-hero', { compact: hasMessages }]">
+ <div :class="['assistant-stand', { thinking: isSending, compact: hasMessages }]">
+ <div class="assistant-halo"></div>
+ <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>
+ <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">
+ <span class="assistant-status-dot"></span>
+ {{ isSending ? '鎬濊�冧腑...' : currentAssistant.label }}
+ </div>
+ <div class="assistant-base assistant-base-lg"></div>
+ <div class="assistant-base assistant-base-md"></div>
+ <div class="assistant-base assistant-base-sm"></div>
+ </div>
+
+ <div :class="['welcome-card', { compact: hasMessages }]">
+ <div class="welcome-eyebrow">鏅鸿兘鍔╂墜</div>
+ <h3 class="welcome-title">
+ 鎮ㄥソ
+ <br />
+ 鎴戞槸{{ currentAssistant.label }}鍒嗘瀽瑙h鍔╂墜
+ </h3>
+ <p class="welcome-desc">
+ {{ currentAssistant.description || '鎴戝彲浠ュ洿缁曚笟鍔¢棶棰樻彁渚涜В璇汇�佹煡璇㈠缓璁拰鍒嗘瀽鏀寔锛屽府鍔╀綘鏇村揩瀹屾垚鍒ゆ柇涓庡鐞嗐��' }}
+ </p>
+
+ <div class="quick-prompt-list">
+ <button
+ v-for="prompt in displayedQuickPrompts"
+ :key="prompt"
+ type="button"
+ class="quick-prompt-btn"
+ :disabled="isSending"
+ @click="sendQuickPrompt(prompt)"
+ >
+ {{ prompt }}
+ </button>
+ </div>
+
+ <button
+ v-if="quickPrompts.length > quickPromptLimit"
+ type="button"
+ class="more-prompts-btn"
+ @click="refreshQuickPrompts"
+ >
+ <el-icon><RefreshRight /></el-icon>
+ <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">
+ <div
+ v-for="(message, index) in messages"
+ :key="index"
+ :class="['message-item', message.isUser ? 'user-message' : 'bot-message']"
+ >
+ <div class="avatar">
+ <el-icon v-if="message.isUser"><User /></el-icon>
+ <el-icon v-else><Cpu /></el-icon>
+ </div>
+ <div class="message-content">
+ <!-- 鏂囨湰鍐呭 -->
+ <div class="text-box" v-html="message.htmlContent"></div>
+
+ <!-- 鍥捐〃鍐呭 -->
+ <div v-if="message.chartOptions && message.chartRenderReady" class="charts-wrapper">
+ <div
+ v-for="(option, key) in message.chartOptions"
+ :key="key"
+ class="chart-item"
+ :id="`ai-chart-${index}-${key}`"
+ ></div>
+ </div>
+
+ <!-- 琛ㄦ牸鍐呭 -->
+ <div v-if="message.type === 'todo_list' && message.tableData" class="table-wrapper">
+ <el-table :data="message.tableData.items" border stripe size="small" style="width: 100%">
+ <el-table-column
+ v-for="col in message.tableData.columns"
+ :key="col"
+ :prop="col"
+ :label="columnLabelMap[col] || col"
+ min-width="100"
+ show-overflow-tooltip
+ />
+ </el-table>
+ </div>
+
+ <!-- 鎵撳瓧涓姩鐢� -->
+ <div v-if="message.isTyping" class="typing-indicator">
+ <span class="dot"></span>
+ <span class="dot"></span>
+ <span class="dot"></span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="input-area">
+ <div class="input-actions">
+ <el-button link class="utility-action-btn" type="primary" size="small" @click="handleNewChat">
+ <el-icon><Plus /></el-icon>鏂颁細璇�
+ </el-button>
+ <el-button v-if="isSending" link class="utility-action-btn stop-action-btn" type="danger" size="small" @click="stopGeneration">
+ <el-icon><VideoPause /></el-icon>鍋滄鐢熸垚
+ </el-button>
+ <el-upload
+ v-if="currentAssistant.allowFileUpload"
+ class="file-upload-trigger"
+ action="#"
+ :auto-upload="false"
+ :show-file-list="false"
+ :on-change="handleFileChange"
+ :disabled="isSending"
+ >
+ <el-button link class="utility-action-btn upload-action-btn" type="primary" size="small" :disabled="isSending">
+ <el-icon><Upload /></el-icon>鍒嗘瀽鏂囦欢
+ </el-button>
+ </el-upload>
+ </div>
+ <div class="input-box">
+ <div v-if="selectedFile" class="selected-file-tag">
+ <el-icon><Document /></el-icon>
+ <span class="file-name">{{ selectedFile.name }}</span>
+ <el-icon class="remove-file" @click="removeSelectedFile"><Close /></el-icon>
+ </div>
+ <el-input
+ v-model="inputMessage"
+ type="textarea"
+ :rows="selectedFile ? 2 : 3"
+ :placeholder="currentAssistant.placeholder"
+ resize="none"
+ @keydown.enter.exact.prevent="sendMessage"
+ />
+ <el-button
+ type="primary"
+ class="send-btn"
+ :disabled="isSending || (!inputMessage.trim() && !selectedFile)"
+ @click="sendMessage"
+ aria-label="鍙戦��"
+ >
+ <el-icon><Promotion /></el-icon>
+ </el-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-drawer>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue'
+import request from '@/utils/request'
+import * as echarts from 'echarts'
+import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, ShoppingCart, Promotion, RefreshRight } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+const props = defineProps({
+ assistants: {
+ type: Array,
+ default: () => []
+ },
+ defaultAssistant: {
+ type: String,
+ default: ''
+ }
+})
+
+const builtInAssistants = [
+ {
+ key: 'general',
+ label: '寰呭姙鍔╃悊',
+ title: '寰呭姙鏅鸿兘鍔╃悊',
+ tooltip: '寰呭姙鍔╂墜',
+ icon: Cpu,
+ apiBase: '/xiaozhi',
+ storageKey: 'ai_chat_uuid',
+ placeholder: '璇疯緭鍏ユ偍鐨勯棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)',
+ welcomeMessage: '浣犲ソ',
+ description: '鎴戝彲浠ュ洖绛斾綘鐨勯棶棰橈紝涓轰綘鎻愪緵涓氬姟鏁版嵁瑙h淇℃伅銆佸鐞嗗缓璁拰杈呭姪鍐崇瓥鏀寔銆�',
+ allowFileUpload: true,
+ emptySessionText: '鏆傛棤鍘嗗彶浼氳瘽'
+ },
+ {
+ key: 'purchase',
+ label: '閲囪喘鍔╃悊',
+ title: '閲囪喘鏅鸿兘鍔╃悊',
+ tooltip: '閲囪喘鏅鸿兘鍔╃悊',
+ icon: ShoppingCart,
+ apiBase: '/purchase-ai',
+ storageKey: 'purchase_ai_chat_uuid',
+ placeholder: '璇疯緭鍏ラ噰璐棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)',
+ welcomeMessage: '浣犲ソ',
+ description: '鎴戝彲浠ュ崗鍔╀綘鍒嗘瀽閲囪喘璁㈠崟銆佸埌璐ц繘搴︺�佷緵搴斿晢琛ㄧ幇鍜屼粯娆炬儏鍐碉紝甯姪浣犲揩閫熷畾浣嶉噰璐紓甯搞��',
+ allowFileUpload: false,
+ emptySessionText: '鏆傛棤閲囪喘浼氳瘽'
+ }
+]
+
+const assistants = computed(() => props.assistants?.length ? props.assistants : builtInAssistants)
+const selectedAssistantKey = ref(props.defaultAssistant || assistants.value[0]?.key || 'general')
+const currentAssistant = computed(() => assistants.value.find(item => item.key === selectedAssistantKey.value) || assistants.value[0] || builtInAssistants[0])
+const showAssistantSwitch = computed(() => assistants.value.length > 1)
+const assistantQuickPromptMap = {
+ general: [
+ '鎴戝綋鍓嶆湁鍝簺瀹℃壒寰呭姙闇�瑕佸鐞嗭紵',
+ '甯垜鍒楀嚭浠婂ぉ鏂板鐨勫鎵瑰緟鍔炪��',
+ '褰撳墠寰呮垜瀹℃壒鐨勫崟鎹紝鎸夋椂闂村�掑簭鍒楀嚭鏉ャ��',
+ '鎴戝彂璧风殑瀹℃壒閲岋紝鍝簺杩樺湪澶勭悊涓紵',
+ '鏌ヨ娴佺▼缂栧彿 XXX 鐨勫鎵硅鎯呫��',
+ '娴佺▼缂栧彿 XXX 鐜板湪鍗″湪鍝釜瀹℃壒鑺傜偣锛熷綋鍓嶅鎵逛汉鏄皝锛�',
+ '甯垜鏌ョ湅娴佺▼缂栧彿 XXX 鐨勫鎵规祦杞褰曘��',
+ '杩�7澶╂垜鐨勫鎵瑰緟鍔炵粺璁℃儏鍐垫�庝箞鏍凤紵',
+ '鏈湀鎴戠殑瀹℃壒涓紝閫氳繃銆侀┏鍥炪�佸鐞嗕腑鍚勬湁澶氬皯锛�',
+ '杩�30澶╁悇绫诲瀷瀹℃壒鏁伴噺鍒嗗竷鏄粈涔堬紵',
+ '甯垜瀹℃壒閫氳繃娴佺▼缂栧彿 XXX锛屽娉ㄢ�滃悓鎰忊�濄��',
+ '甯垜椹冲洖娴佺▼缂栧彿 XXX锛屽娉ㄢ�滆琛ュ厖璇存槑鈥濄��',
+ '鎾ら攢鎴戝垰鍒氬娴佺▼缂栧彿 XXX 鐨勫鎵规搷浣溿��',
+ '甯垜淇敼娴佺▼缂栧彿 XXX 鐨勫娉ㄤ负鈥滃凡琛ュ厖闄勪欢鈥濄��',
+ '鍒犻櫎鎴戝彂璧风殑娴佺▼缂栧彿 XXX銆�'
+ ],
+ purchase: [
+ '鏈湀閲囪喘閲戦鎺掑悕鍓嶅崄鐨勭墿鏂欐湁鍝簺锛�',
+ '鍝簺閲囪喘璁㈠崟杩樻湭鍏ュ簱锛�',
+ '鏈�杩�7澶╀緵搴斿晢鍒拌揣寮傚父鏈夊摢浜涳紵',
+ '甯垜缁熻寰呬粯娆鹃噰璐崟',
+ '鍒楀嚭鏈湀閲囪喘閫�璐ф儏鍐�'
+ ]
+}
+const quickPromptLimit = 3
+const quickPromptStart = ref(0)
+const quickPrompts = computed(() => {
+ const assistant = currentAssistant.value || {}
+ if (Array.isArray(assistant.quickPrompts) && assistant.quickPrompts.length) {
+ return assistant.quickPrompts
+ }
+ return assistantQuickPromptMap[assistant.key] || assistantQuickPromptMap.general
+})
+const displayedQuickPrompts = computed(() => {
+ const prompts = quickPrompts.value || []
+ if (prompts.length <= quickPromptLimit) return prompts
+
+ const result = []
+ for (let i = 0; i < quickPromptLimit; i++) {
+ result.push(prompts[(quickPromptStart.value + i) % prompts.length])
+ }
+ return result
+})
+const hasMessages = computed(() => messages.value.length > 0)
+
+const visible = ref(false)
+const windowWidth = ref(window.innerWidth)
+const drawerSize = computed(() => {
+ if (windowWidth.value < 768) return '100%'
+ if (windowWidth.value < 1200) return '500px'
+ return '600px'
+})
+const messageListRef = ref(null)
+const isSending = ref(false)
+const currentAbortController = ref(null)
+const inputMessage = ref('')
+const selectedFile = ref(null)
+const messages = ref([])
+const uuid = ref('')
+const chartInstances = ref({})
+const resizeHandlers = ref([])
+const outputState = ref({})
+
+// 鍘嗗彶浼氳瘽鐩稿叧
+const showHistory = ref(false)
+const sessions = ref([])
+const loadingSessions = ref(false)
+
+const abortCurrentRequest = () => {
+ if (!currentAbortController.value) return
+
+ currentAbortController.value.abort()
+ currentAbortController.value = null
+ isSending.value = false
+
+ const lastMsg = messages.value[messages.value.length - 1]
+ if (lastMsg && !lastMsg.isUser) {
+ lastMsg.isTyping = false
+ }
+}
+
+const toggleHistory = () => {
+ showHistory.value = !showHistory.value
+ if (showHistory.value) {
+ loadSessions()
+ }
+}
+
+const handleToggleHistory = () => {
+ if (isSending.value) {
+ abortCurrentRequest()
+ }
+ toggleHistory()
+}
+
+const loadSessions = async () => {
+ loadingSessions.value = true
+ try {
+ const res = await request.get(`${currentAssistant.value.apiBase}/history/sessions`)
+ if (res.code === 200) {
+ sessions.value = res.data || []
+ }
+ } catch (err) {
+ console.error('Failed to load sessions', err)
+ } finally {
+ loadingSessions.value = false
+ }
+}
+
+const selectSession = async (session) => {
+ showHistory.value = false
+ uuid.value = session.memoryId
+ localStorage.setItem(currentAssistant.value.storageKey, uuid.value)
+
+ // 鍔犺浇浼氳瘽娑堟伅
+ try {
+ const res = await request.get(`${currentAssistant.value.apiBase}/history/messages/${uuid.value}`)
+ if (res.code === 200) {
+ disposeCharts()
+ messages.value = []
+ const historyMsgs = res.data || []
+
+ // 閲嶆柊鏋勯�犳秷鎭垪琛ㄥ苟瑙f瀽
+ historyMsgs.forEach((msg, idx) => {
+ const isUser = msg.role === 'user'
+ const botMsgIndex = messages.value.length
+
+ const messageObj = {
+ isUser,
+ content: msg.content,
+ htmlContent: '',
+ isTyping: false,
+ chartOptions: null,
+ chartRenderReady: false,
+ type: '',
+ tableData: null
+ }
+
+ messages.value.push(messageObj)
+
+ if (!isUser) {
+ outputState.value[botMsgIndex] = {
+ isPaused: false,
+ jsonBlockStartPos: -1,
+ jsBlockStartPos: -1,
+ blockEndPos: -1,
+ hasRenderedChart: false
+ }
+
+ // 瑙f瀽鍘嗗彶娑堟伅涓殑 JSON
+ const extracted = extractEmbeddedSuccessJson(msg.content)
+ if (extracted) {
+ applyStructuredMessageData(messageObj, extracted.data, botMsgIndex)
+ }
+
+ updateOutputState(msg.content, botMsgIndex)
+ messageObj.htmlContent = convertStreamOutput(msg.content, botMsgIndex)
+ } else {
+ messageObj.htmlContent = convertTextToHtml(msg.content)
+ }
+ })
+ scrollToBottom()
+ }
+ } catch (err) {
+ console.error('Failed to load messages', err)
+ }
+}
+
+const handleDeleteSession = async (memoryId) => {
+ try {
+ const res = await request.delete(`${currentAssistant.value.apiBase}/history/${memoryId}`)
+ if (res.code === 200) {
+ loadSessions()
+ if (uuid.value === memoryId) {
+ newChat()
+ }
+ }
+ } catch (err) {
+ console.error('Failed to delete session', err)
+ }
+}
+
+const columnLabelMap = {
+ approveId: '瀹℃壒缂栧彿',
+ approveType: '瀹℃壒绫诲瀷',
+ approveUserName: '瀹℃壒浜�',
+ approveUserCurrentName: '褰撳墠澶勭悊浜�',
+ approveReason: '瀹℃壒鍘熷洜',
+ approveStatus: '瀹℃壒鐘舵��',
+ createTime: '鍒涘缓鏃堕棿'
+}
+
+onMounted(() => {
+ initUUID()
+ // 鍒濆娆㈣繋
+ if (messages.value.length === 0) {
+ hello()
+ }
+ window.addEventListener('resize', handleWindowResize)
+})
+
+onUnmounted(() => {
+ disposeCharts()
+ window.removeEventListener('resize', handleWindowResize)
+})
+
+watch(selectedAssistantKey, (nextKey, prevKey) => {
+ if (!prevKey || nextKey === prevKey) return
+
+ abortCurrentRequest()
+ disposeCharts()
+ messages.value = []
+ outputState.value = {}
+ sessions.value = []
+ showHistory.value = false
+ selectedFile.value = null
+ inputMessage.value = ''
+ quickPromptStart.value = 0
+ initUUID()
+ hello()
+})
+
+const handleWindowResize = () => {
+ windowWidth.value = window.innerWidth
+}
+
+const toggleSidebar = () => {
+ visible.value = !visible.value
+ if (visible.value) {
+ scrollToBottom()
+ }
+}
+
+const handleClose = () => {
+ visible.value = false
+}
+
+const handleManualClose = () => {
+ if (isSending.value) {
+ abortCurrentRequest()
+ }
+ handleClose()
+}
+
+const initUUID = () => {
+ let storedUUID = localStorage.getItem(currentAssistant.value.storageKey)
+ if (!storedUUID) {
+ storedUUID = Math.random().toString(36).substring(2, 10) + Date.now().toString(36).substring(4)
+ localStorage.setItem(currentAssistant.value.storageKey, storedUUID)
+ }
+ uuid.value = storedUUID
+}
+
+const hello = () => {
+ sendRequest(currentAssistant.value.welcomeMessage || '浣犲ソ')
+}
+
+const newChat = () => {
+ disposeCharts()
+ messages.value = []
+ outputState.value = {}
+ sessions.value = []
+ showHistory.value = false
+ selectedFile.value = null
+ quickPromptStart.value = 0
+ localStorage.removeItem(currentAssistant.value.storageKey)
+ initUUID()
+ hello()
+}
+
+const handleNewChat = () => {
+ if (isSending.value) {
+ abortCurrentRequest()
+ }
+ newChat()
+}
+
+const sendQuickPrompt = (prompt) => {
+ if (!prompt || isSending.value) return
+ inputMessage.value = prompt
+ sendMessage()
+}
+
+const refreshQuickPrompts = () => {
+ const prompts = quickPrompts.value || []
+ if (prompts.length <= quickPromptLimit) return
+ quickPromptStart.value = (quickPromptStart.value + quickPromptLimit) % prompts.length
+}
+
+const disposeCharts = () => {
+ Object.values(chartInstances.value).forEach(chart => chart.dispose())
+ resizeHandlers.value.forEach(handler => window.removeEventListener('resize', handler))
+ chartInstances.value = {}
+ resizeHandlers.value = []
+}
+
+const extractEmbeddedSuccessJson = (text) => {
+ if (!text || typeof text !== 'string') return null
+
+ const startIdx = text.indexOf('{"success"')
+ if (startIdx === -1) return null
+
+ for (let i = startIdx; i < text.length; i++) {
+ if (text[i] !== '{') continue
+
+ let depth = 0
+ let inString = false
+ let escaped = false
+
+ for (let j = i; j < text.length; j++) {
+ const char = text[j]
+
+ if (inString) {
+ if (escaped) {
+ escaped = false
+ } else if (char === '\\') {
+ escaped = true
+ } else if (char === '"') {
+ inString = false
+ }
+ continue
+ }
+
+ if (char === '"') {
+ inString = true
+ continue
+ }
+
+ if (char === '{') {
+ depth++
+ } else if (char === '}') {
+ depth--
+ if (depth === 0) {
+ const candidate = text.slice(i, j + 1)
+ try {
+ const parsed = JSON.parse(candidate)
+ if (parsed?.success === true) {
+ return {
+ data: parsed,
+ startIdx: i,
+ endIdx: j + 1
+ }
+ }
+ } catch (err) {
+ continue
+ }
+ }
+ }
+ }
+ }
+
+ return null
+}
+
+const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => {
+ if (!messageObj || !parsedData?.success) return
+
+ messageObj.type = parsedData.type || ''
+
+ if (messageObj.type === 'todo_list' && parsedData.data) {
+ messageObj.tableData = parsedData.data
+ }
+
+ if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
+ messageObj.chartOptions = parsedData.charts
+ messageObj.chartRenderReady = true
+
+ if (shouldRenderCharts) {
+ renderCharts(msgIndex, messageObj.chartOptions)
+ if (outputState.value[msgIndex]) {
+ outputState.value[msgIndex].hasRenderedChart = true
+ }
+ }
+ }
+}
+
+const scrollToBottom = () => {
+ nextTick(() => {
+ if (messageListRef.value) {
+ messageListRef.value.scrollTop = messageListRef.value.scrollHeight
+ }
+ })
+}
+
+const handleFileChange = (file) => {
+ if (!file) return
+ const rawFile = file.raw
+ if (rawFile) {
+ // 闄愬埗鏂囦欢澶у皬锛屼緥濡� 10MB
+ const isLt10M = rawFile.size / 1024 / 1024 < 10
+ if (!isLt10M) {
+ ElMessage.error('鏂囦欢澶у皬涓嶈兘瓒呰繃 10MB!')
+ return
+ }
+ selectedFile.value = rawFile
+ }
+}
+
+const removeSelectedFile = () => {
+ selectedFile.value = null
+}
+
+const analyzeFile = async (file, message = '') => {
+ if (isSending.value) return
+ isSending.value = true
+ currentAbortController.value = new AbortController()
+
+ const userMsg = message ? `${message}\n[涓婁紶鏂囦欢鍒嗘瀽] ${file.name}` : `[涓婁紶鏂囦欢鍒嗘瀽] ${file.name}`
+ messages.value.push({
+ isUser: true,
+ content: userMsg,
+ htmlContent: convertTextToHtml(userMsg),
+ isTyping: false
+ })
+
+ const botMsgIndex = messages.value.length
+ messages.value.push({
+ isUser: false,
+ content: '',
+ htmlContent: '',
+ isTyping: true,
+ chartOptions: null,
+ chartRenderReady: false,
+ type: '',
+ tableData: null
+ })
+
+ outputState.value[botMsgIndex] = {
+ isPaused: false,
+ jsonBlockStartPos: -1,
+ jsBlockStartPos: -1,
+ blockEndPos: -1,
+ hasRenderedChart: false
+ }
+
+ scrollToBottom()
+
+ const formData = new FormData()
+ formData.append('file', file)
+ formData.append('memoryId', uuid.value)
+ if (message.trim()) {
+ formData.append('message', message.trim())
+ }
+
+ request.post(`${currentAssistant.value.apiBase}/analyze-file`, formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data'
+ },
+ signal: currentAbortController.value.signal,
+ onDownloadProgress: (e) => {
+ const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
+ if (!fullText) return
+
+ const currentMsg = messages.value[botMsgIndex]
+ if (!currentMsg) return
+
+ currentMsg.content = fullText
+
+ // 瑙f瀽 JSON 鏁版嵁锛堥拡瀵瑰祵鍏ュ紡 JSON锛�
+ const extracted = extractEmbeddedSuccessJson(fullText)
+ if (extracted) {
+ applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart)
+ }
+
+ updateOutputState(fullText, botMsgIndex)
+ currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
+ scrollToBottom()
+ }
+ }).then(() => {
+ const currentMsg = messages.value[botMsgIndex]
+ currentMsg.isTyping = false
+ isSending.value = false
+ currentAbortController.value = null
+
+ // 鏈�缁堣В鏋愮‘淇濆浘琛ㄦ覆鏌�
+ if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) {
+ renderCharts(botMsgIndex, currentMsg.chartOptions)
+ outputState.value[botMsgIndex].hasRenderedChart = true
+ }
+ }).catch(err => {
+ if (err.name === 'CanceledError' || err.name === 'AbortError') {
+ console.log('Analysis aborted by user')
+ return
+ }
+ console.error('File analysis error:', err)
+ const errorMsg = '鎶辨瓑锛屾枃浠跺垎鏋愯繃绋嬩腑閬囧埌浜嗕竴鐐归棶棰橈紝璇风◢鍚庡啀璇曘��'
+ if (messages.value[botMsgIndex]) {
+ messages.value[botMsgIndex].content = errorMsg
+ messages.value[botMsgIndex].htmlContent = convertTextToHtml(errorMsg)
+ messages.value[botMsgIndex].isTyping = false
+ }
+ isSending.value = false
+ currentAbortController.value = null
+ })
+}
+
+const sendMessage = () => {
+ const msg = inputMessage.value?.trim() || ''
+ if ((msg || selectedFile.value) && !isSending.value) {
+ if (selectedFile.value) {
+ analyzeFile(selectedFile.value, msg)
+ selectedFile.value = null
+ } else {
+ sendRequest(msg)
+ }
+ inputMessage.value = ''
+ }
+}
+
+const stopGeneration = () => {
+ abortCurrentRequest()
+}
+
+const sendRequest = (message) => {
+ isSending.value = true
+ currentAbortController.value = new AbortController()
+
+ // 鐢ㄦ埛娑堟伅
+ messages.value.push({
+ isUser: true,
+ content: message,
+ htmlContent: convertTextToHtml(message),
+ isTyping: false
+ })
+
+ // 鏈哄櫒浜哄崰浣�
+ const botMsgIndex = messages.value.length
+ const botMsg = {
+ isUser: false,
+ content: '',
+ htmlContent: '',
+ isTyping: true,
+ chartOptions: null,
+ chartRenderReady: false,
+ type: '',
+ tableData: null
+ }
+ messages.value.push(botMsg)
+
+ outputState.value[botMsgIndex] = {
+ isPaused: false,
+ jsonBlockStartPos: -1,
+ jsBlockStartPos: -1,
+ blockEndPos: -1,
+ hasRenderedChart: false
+ }
+
+ scrollToBottom()
+
+ request.post(`${currentAssistant.value.apiBase}/chat`,
+ { memoryId: uuid.value, message },
+ {
+ signal: currentAbortController.value.signal,
+ onDownloadProgress: (e) => {
+ // 鍏煎涓嶅悓鐗堟湰鐨� axios 鑾峰彇鍝嶅簲鏂囨湰鐨勬柟寮�
+ const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
+ if (!fullText) return
+
+ const currentMsg = messages.value[botMsgIndex]
+ if (!currentMsg) return
+
+ currentMsg.content = fullText
+
+ // 灏濊瘯鎻愬彇骞惰В鏋� JSON
+ const extracted = extractEmbeddedSuccessJson(fullText)
+ if (extracted) {
+ applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
+ } else {
+ const extractJson = (text) => {
+ const startIdx = text.indexOf('{"success": true')
+ if (startIdx === -1) return null
+
+ // 浠庡悗寰�鍓嶆壘鏈�鍚庝竴涓� '}'
+ const lastBraceIdx = text.lastIndexOf('}')
+ if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
+
+ const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
+ try {
+ return JSON.parse(potentialJson)
+ } catch (err) {
+ return null
+ }
+ }
+
+ const parsedData = extractJson(fullText)
+ if (parsedData) {
+ currentMsg.type = parsedData.type || ''
+ if (currentMsg.type === 'todo_list' && parsedData.data) {
+ currentMsg.tableData = parsedData.data
+ }
+ if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
+ currentMsg.chartOptions = parsedData.charts
+ currentMsg.chartRenderReady = true
+ // 姣忔瑙f瀽鎴愬姛閮藉皾璇曟覆鏌�/鏇存柊鍥捐〃锛屼互鏀寔娴佸紡鏇存柊
+ renderCharts(botMsgIndex, currentMsg.chartOptions)
+ }
+ }
+
+ }
+
+ updateOutputState(fullText, botMsgIndex)
+ currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
+ scrollToBottom()
+ }
+ }
+ ).then(() => {
+ const currentMsg = messages.value[botMsgIndex]
+ currentMsg.isTyping = false
+ isSending.value = false
+ currentAbortController.value = null
+
+ // 鏈�缁堣В鏋�
+ const extracted = extractEmbeddedSuccessJson(currentMsg.content)
+ if (extracted) {
+ applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
+ } else {
+ const extractJson = (text) => {
+ const startIdx = text.indexOf('{"success": true')
+ if (startIdx === -1) return null
+ const lastBraceIdx = text.lastIndexOf('}')
+ if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
+ const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
+ try {
+ return JSON.parse(potentialJson)
+ } catch (err) {
+ return null
+ }
+ }
+
+ const finalParsed = extractJson(currentMsg.content)
+ if (finalParsed) {
+ currentMsg.type = finalParsed.type || ''
+ if (currentMsg.type === 'todo_list' && finalParsed.data) {
+ currentMsg.tableData = finalParsed.data
+ }
+ if (finalParsed.charts && Object.keys(finalParsed.charts).length > 0) {
+ currentMsg.chartOptions = finalParsed.charts
+ currentMsg.chartRenderReady = true
+ renderCharts(botMsgIndex, currentMsg.chartOptions)
+ }
+ }
+ }
+ }).catch(err => {
+ if (err.name === 'CanceledError' || err.name === 'AbortError') {
+ console.log('Request aborted by user')
+ return
+ }
+ console.error('AI Chat Error:', err)
+ const errorMsg = '鎶辨瓑锛屾垜鐜板湪閬囧埌浜嗕竴鐐归棶棰橈紝璇风◢鍚庡啀璇曘��'
+ if (messages.value[botMsgIndex]) {
+ messages.value[botMsgIndex].content = errorMsg
+ messages.value[botMsgIndex].htmlContent = convertTextToHtml(errorMsg)
+ messages.value[botMsgIndex].isTyping = false
+ }
+ isSending.value = false
+ currentAbortController.value = null
+ })
+}
+
+const updateOutputState = (text, msgIndex) => {
+ const state = outputState.value[msgIndex]
+ if (state.jsonBlockStartPos === -1) {
+ const pos = text.indexOf('```json')
+ if (pos !== -1) { state.jsonBlockStartPos = pos; state.isPaused = true }
+ }
+ if (state.jsBlockStartPos === -1) {
+ const pos = text.indexOf('```javascript') !== -1 ? text.indexOf('```javascript') : text.indexOf('```js')
+ if (pos !== -1) { state.jsBlockStartPos = pos; state.isPaused = true }
+ }
+ if ((state.jsonBlockStartPos !== -1 || state.jsBlockStartPos !== -1) && state.blockEndPos === -1) {
+ const startCheck = state.jsonBlockStartPos !== -1 ? state.jsonBlockStartPos + 7 : state.jsBlockStartPos + (text.includes('javascript') ? 13 : 5)
+ const endPos = text.indexOf('```', startCheck)
+ if (endPos !== -1) { state.blockEndPos = endPos + 3; state.isPaused = false }
+ }
+}
+
+const convertTextToHtml = (text) => {
+ if (!text) return ''
+ return text
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/\n/g, '<br>')
+}
+
+const convertStreamOutput = (output, msgIndex) => {
+ if (!output) return ''
+ const state = outputState.value[msgIndex]
+ let display = output
+
+ // 灏濊瘯鎻愬彇 JSON 閮ㄥ垎
+ const extracted = extractEmbeddedSuccessJson(output)
+ const startIdx = extracted ? extracted.startIdx : output.indexOf('{"success"')
+
+ // 濡傛灉杩樺湪浠g爜鍧椾腑涓旀湭缁撴潫锛屾樉绀烘彁绀烘枃瀛�
+ if (state && ((state.jsonBlockStartPos !== -1) || (state.jsBlockStartPos !== -1)) && state.blockEndPos === -1) {
+ const startPos = state.jsonBlockStartPos !== -1 ? state.jsonBlockStartPos : state.jsBlockStartPos
+ const textBeforeBlock = display.substring(0, startPos).trim()
+ display = textBeforeBlock || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
+ return convertTextToHtml(display)
+ }
+
+ if (extracted) {
+ const parsed = extracted.data
+ display = (output.substring(0, extracted.startIdx) + output.substring(extracted.endIdx)).trim()
+
+ if (/^[\s}\],锛屻��.:锛�;锛沒+$/.test(display)) {
+ display = ''
+ }
+
+ if (parsed.description) {
+ display = parsed.description
+ }
+
+ if (!display) {
+ if (parsed.type === 'todo_list') {
+ display = '宸蹭负鎮ㄦ暣鐞嗗ソ鐩稿叧鏁版嵁銆�'
+ } else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
+ display = '宸蹭负鎮ㄧ敓鎴愬垎鏋愬浘琛ㄣ��'
+ } else {
+ display = '姝e湪涓烘偍灞曠ず鍒嗘瀽缁撴灉...'
+ }
+ }
+ } else if (startIdx !== -1) {
+ const lastBraceIdx = output.lastIndexOf('}')
+ if (lastBraceIdx !== -1 && lastBraceIdx > startIdx) {
+ const potentialJson = output.substring(startIdx, lastBraceIdx + 1)
+ try {
+ const parsed = JSON.parse(potentialJson)
+ // 鎴愬姛瑙f瀽锛岀Щ闄� JSON 閮ㄥ垎鏄剧ず鏂囧瓧
+ display = (output.substring(0, startIdx) + output.substring(lastBraceIdx + 1)).trim()
+
+ if (/^[\s}\],锛屻��.:锛�;锛沒+$/.test(display)) {
+ display = ''
+ }
+
+ if (parsed.description) {
+ display = parsed.description
+ }
+
+ if (!display) {
+ if (parsed.type === 'todo_list') {
+ display = '宸蹭负鎮ㄦ暣鐞嗗ソ鐩稿叧鏁版嵁锛�'
+ } else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
+ display = '宸蹭负鎮ㄧ敓鎴愬垎鏋愬浘琛細'
+ } else {
+ display = '姝e湪涓烘偍灞曠ず鍒嗘瀽缁撴灉...'
+ }
+ }
+ } catch (e) {
+ // 瑙f瀽澶辫触锛岃鏄� JSON 杩樺湪浼犺緭涓垨鏍煎紡涓嶆纭�
+ display = output.substring(0, startIdx).trim() || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
+ }
+ } else {
+ // 鎵惧埌浜嗗紑濮嬩絾杩樻病鎵惧埌缁撴潫
+ display = output.substring(0, startIdx).trim() || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
+ }
+ }
+
+ let html = convertTextToHtml(display)
+
+ // 杩樺師浠g爜鍧�
+ html = html.replace(/```(javascript|js)([\s\S]*?)```/g, '<pre class="code-block js-code">$2</pre>')
+ html = html.replace(/```([\s\S]*?)```/g, '<pre class="code-block">$1</pre>')
+
+ return html || '...'
+}
+
+const renderCharts = (msgIndex, chartOptions) => {
+ nextTick(() => {
+ Object.keys(chartOptions).forEach(key => {
+ const id = `ai-chart-${msgIndex}-${key}`
+ const tryInit = (count = 0) => {
+ const dom = document.getElementById(id)
+ if (dom) {
+ if (chartInstances.value[id]) {
+ // 濡傛灉宸茬粡鍒濆鍖栬繃锛岀洿鎺ユ洿鏂版暟鎹�
+ const chart = chartInstances.value[id]
+ const option = normalizeAiChartOption(chartOptions[key])
+ if (option) chart.setOption(option)
+ return
+ }
+
+ const chart = echarts.init(dom)
+ chartInstances.value[id] = chart
+ const option = normalizeAiChartOption(chartOptions[key])
+ if (option) {
+ chart.setOption(option)
+ } else {
+ console.warn('Invalid chart option for:', id, chartOptions[key])
+ }
+
+ const handler = () => chart.resize()
+ resizeHandlers.value.push(handler)
+ window.addEventListener('resize', handler)
+ } else if (count < 15) { // 绋嶅井澧炲姞閲嶈瘯娆℃暟
+ setTimeout(() => tryInit(count + 1), 200)
+ }
+ }
+ tryInit()
+ })
+ })
+}
+
+// 鏍煎紡鍖� AI 杩斿洖鐨勫浘琛ㄩ厤缃紝灏嗗叾杞崲涓烘爣鍑嗙殑 ECharts 閰嶇疆
+const formatChartOption = (rawOption) => {
+ if (!rawOption) return null
+
+ // 濡傛灉宸茬粡鏄爣鍑� ECharts 閰嶇疆锛堝寘鍚� series锛夛紝鍒欑洿鎺ヨ繑鍥�
+ const hasSeries = rawOption.series && Array.isArray(rawOption.series)
+
+ // 灏濊瘯杞崲绠�鏄撴牸寮�
+ try {
+ const isPie = rawOption.type === 'pie' || (rawOption.title && rawOption.title.includes('鍗犳瘮'))
+
+ const option = {
+ title: {
+ text: rawOption.title || '',
+ left: 'center',
+ textStyle: { fontSize: 14 }
+ },
+ tooltip: {
+ trigger: isPie ? 'item' : 'axis'
+ },
+ legend: {
+ bottom: '0'
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '15%',
+ containLabel: true
+ },
+ xAxis: isPie ? undefined : {
+ type: 'category',
+ data: rawOption.xAxisData || (Array.isArray(rawOption.xAxis) ? rawOption.xAxis : []),
+ name: typeof rawOption.xAxis === 'string' ? rawOption.xAxis : ''
+ },
+ yAxis: isPie ? undefined : {
+ type: 'value',
+ name: typeof rawOption.yAxis === 'string' ? rawOption.yAxis : ''
+ },
+ series: rawOption.series || [{
+ name: rawOption.title || '鏁板��',
+ type: rawOption.type || 'line',
+ data: rawOption.seriesData || (Array.isArray(rawOption.data) ? rawOption.data : []),
+ smooth: true,
+ radius: isPie ? '50%' : undefined
+ }]
+ }
+
+ // 閽堝楗煎浘鐨勭壒娈婂鐞�
+ if (isPie && !option.series[0].data && Array.isArray(rawOption.data)) {
+ option.series[0].data = rawOption.data
+ }
+
+ return option
+ } catch (err) {
+ console.error('Chart option conversion failed:', err)
+ return null
+ }
+}
+
+const normalizeAiChartOption = (rawOption) => {
+ if (!rawOption) return null
+
+ try {
+ const hasSeries = Array.isArray(rawOption.series) && rawOption.series.length > 0
+ const firstSeriesType = hasSeries ? rawOption.series[0]?.type : rawOption.type
+ const titleConfig = rawOption.title && typeof rawOption.title === 'object'
+ ? rawOption.title
+ : null
+ const tooltipConfig = rawOption.tooltip && typeof rawOption.tooltip === 'object'
+ ? rawOption.tooltip
+ : null
+ const legendConfig = rawOption.legend && typeof rawOption.legend === 'object'
+ ? rawOption.legend
+ : null
+ const rawXAxisConfig = rawOption.xAxis && typeof rawOption.xAxis === 'object' && !Array.isArray(rawOption.xAxis)
+ ? rawOption.xAxis
+ : null
+ const rawYAxisConfig = rawOption.yAxis && typeof rawOption.yAxis === 'object' && !Array.isArray(rawOption.yAxis)
+ ? rawOption.yAxis
+ : null
+ const titleText = typeof rawOption.title === 'string' ? rawOption.title : rawOption.title?.text || ''
+ const isPie = firstSeriesType === 'pie' || titleText.includes('鍗犳瘮')
+ const baseXAxisData = Array.isArray(rawOption.xAxisData)
+ ? rawOption.xAxisData
+ : (Array.isArray(rawOption.xAxis) ? rawOption.xAxis : (Array.isArray(rawXAxisConfig?.data) ? rawXAxisConfig.data : []))
+ const fallbackSeries = [{
+ name: titleText || '鏁版嵁',
+ type: rawOption.type || 'line',
+ data: Array.isArray(rawOption.seriesData) ? rawOption.seriesData : (Array.isArray(rawOption.data) ? rawOption.data : [])
+ }]
+ const normalizedSeries = (hasSeries ? rawOption.series : fallbackSeries).map((seriesItem, index) => {
+ const seriesType = seriesItem?.type || rawOption.type || 'line'
+ const nextSeries = {
+ ...seriesItem,
+ name: seriesItem?.name || titleText || `绯诲垪${index + 1}`,
+ type: seriesType
+ }
+
+ if (isPie) {
+ nextSeries.radius = nextSeries.radius || '55%'
+ nextSeries.data = Array.isArray(nextSeries.data) ? nextSeries.data : (Array.isArray(rawOption.data) ? rawOption.data : [])
+ } else {
+ nextSeries.smooth = typeof nextSeries.smooth === 'boolean' ? nextSeries.smooth : seriesType === 'line'
+ nextSeries.data = Array.isArray(nextSeries.data) ? nextSeries.data : []
+ }
+
+ return nextSeries
+ })
+ const categorySource = !isPie
+ ? normalizedSeries.find(seriesItem => Array.isArray(seriesItem.data) && seriesItem.data.every(item => item && typeof item === 'object' && 'name' in item && 'value' in item))
+ : null
+ const xAxisData = categorySource
+ ? categorySource.data.map(item => item.name)
+ : baseXAxisData
+ const finalSeries = !isPie && categorySource
+ ? normalizedSeries.map(seriesItem => ({
+ ...seriesItem,
+ data: Array.isArray(seriesItem.data)
+ ? seriesItem.data.map(item => (item && typeof item === 'object' && 'value' in item ? item.value : item))
+ : []
+ }))
+ : normalizedSeries
+
+ return {
+ title: titleConfig || {
+ text: titleText,
+ left: 'center',
+ textStyle: { fontSize: 14 }
+ },
+ tooltip: tooltipConfig || {
+ trigger: isPie ? 'item' : 'axis'
+ },
+ legend: legendConfig || {
+ bottom: '0'
+ },
+ grid: isPie ? undefined : {
+ left: '3%',
+ right: '4%',
+ bottom: '15%',
+ containLabel: true
+ },
+ xAxis: isPie ? undefined : {
+ ...(rawXAxisConfig || {}),
+ type: 'category',
+ data: xAxisData,
+ name: typeof rawOption.xAxis === 'string' ? rawOption.xAxis : (rawXAxisConfig?.name || '')
+ },
+ yAxis: isPie ? undefined : (rawYAxisConfig || {
+ type: 'value',
+ name: typeof rawOption.yAxis === 'string' ? rawOption.yAxis : ''
+ }),
+ series: finalSeries
+ }
+ } catch (err) {
+ console.error('AI chart normalization failed:', err, rawOption)
+ return formatChartOption(rawOption)
+ }
+}
+
+watch(messages, () => scrollToBottom(), { deep: true })
+</script>
+
+<style lang="scss">
+.ai-chat-overlay {
+ pointer-events: none !important;
+ background: transparent !important;
+}
+
+.ai-chat-overlay .el-drawer {
+ pointer-events: auto;
+}
+</style>
+
+<style scoped lang="scss">
+$primary-blue: #0055d4;
+$secondary-blue: #2e8ce0;
+$light-blue: #7ab8ff;
+$pale-blue: #c5dcff;
+$ice-white: #e8f2ff;
+$deep-blue: #003b8e;
+$deepest-blue: #002b66;
+$gradient-blue: linear-gradient(145deg, #004fc7 0%, #0066e0 40%, #2580e8 70%, #5a9fe0 100%);
+$gradient-dark: linear-gradient(145deg, #003b8e 0%, #0055d4 50%, #0077e8 100%);
+$gradient-ice: linear-gradient(180deg, #e0ecff 0%, #d4e5ff 50%, #e8f0ff 100%);
+$shadow-blue: 0 8px 40px rgba(0, 85, 212, 0.35);
+$shadow-deep: 0 12px 48px rgba(0, 40, 120, 0.4);
+$shadow-card: 0 6px 24px rgba(0, 51, 136, 0.12);
+
+.ai-chat-sidebar-wrapper {
+ position: static;
+ z-index: 2000;
+ pointer-events: auto;
+
+ :deep(.el-drawer) {
+ pointer-events: auto;
+ }
+}
+
+.ai-chat-trigger {
+ pointer-events: auto;
+ position: fixed;
+ right: 24px;
+ bottom: 100px;
+ width: 56px;
+ height: 56px;
+ background: $gradient-dark;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ box-shadow: $shadow-deep, 0 0 0 2px rgba(0, 85, 212, 0.3) inset, 0 0 30px rgba(0, 119, 232, 0.2);
+ transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ z-index: 2001;
+ animation: triggerPulse 3s ease-in-out infinite;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: -6px;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.4), rgba(0, 136, 232, 0.3), rgba(90, 159, 224, 0.2));
+ border-radius: 50%;
+ z-index: -1;
+ filter: blur(16px);
+ animation: glowPulse 2s ease-in-out infinite alternate;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.3) 0%, transparent 50%);
+ pointer-events: none;
+ }
+
+ &:hover {
+ transform: scale(1.12) translateY(-4px);
+ box-shadow: $shadow-deep, 0 0 0 3px rgba(0, 136, 232, 0.4) inset, 0 0 50px rgba(0, 136, 232, 0.3);
+
+ &::before {
+ animation: glowPulse 1s ease-in-out infinite alternate;
+ }
+
+ .trigger-icon {
+ transform: rotate(-8deg) scale(1.05);
+ filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.5));
+ }
+ }
+
+ .trigger-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ color: #fff;
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
+ }
+}
+
+@keyframes triggerPulse {
+ 0%, 100% {
+ box-shadow: $shadow-blue, 0 0 0 2px rgba(0, 85, 212, 0.25) inset, 0 0 20px rgba(0, 119, 232, 0.15);
+ }
+ 50% {
+ box-shadow: $shadow-deep, 0 0 0 3px rgba(0, 136, 232, 0.35) inset, 0 0 40px rgba(0, 136, 232, 0.25);
+ }
+}
+
+@keyframes glowPulse {
+ 0% {
+ opacity: 0.6;
+ transform: scale(1);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1.1);
+ }
+}
+
+.ai-chat-drawer {
+ :deep(.el-drawer__body) {
+ padding: 0;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+ :deep(.el-drawer__header) {
+ margin-bottom: 0;
+ padding: 0;
+ background: $gradient-dark;
+ color: #fff;
+ }
+}
+
+.drawer-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ padding: 18px 20px;
+ background: $gradient-dark;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: -60%;
+ right: -25%;
+ width: 250px;
+ height: 250px;
+ background: radial-gradient(circle, rgba(0, 136, 232, 0.4) 0%, transparent 70%);
+ pointer-events: none;
+ animation: headerGlow 4s ease-in-out infinite alternate;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: -40%;
+ left: -15%;
+ width: 200px;
+ height: 200px;
+ background: radial-gradient(circle, rgba(0, 85, 212, 0.3) 0%, transparent 70%);
+ pointer-events: none;
+ animation: headerGlow 5s ease-in-out infinite alternate-reverse;
+ }
+
+ .header-left {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ position: relative;
+ z-index: 1;
+
+ .header-icon {
+ color: rgba(255, 255, 255, 0.95);
+ filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.2));
+ animation: iconFloat 3s ease-in-out infinite;
+ }
+
+ .title {
+ font-size: 17px;
+ font-weight: 600;
+ color: #fff;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
+ letter-spacing: 0.5px;
+ }
+ }
+
+ .header-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ position: relative;
+ z-index: 1;
+
+ .action-divider {
+ width: 1px;
+ height: 16px;
+ background: rgba(255, 255, 255, 0.2);
+ margin: 0 2px;
+ }
+
+ :deep(.el-button) {
+ color: rgba(255, 255, 255, 0.85);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ background: rgba(255, 255, 255, 0.12);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 8px;
+ padding: 8px;
+ height: 32px;
+ width: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ color: #fff;
+ background: rgba(255, 255, 255, 0.25);
+ border-color: rgba(255, 255, 255, 0.3);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+
+ &.close-btn {
+ background: rgba(255, 255, 255, 0.1);
+ &:hover {
+ background: rgba(245, 108, 108, 0.8);
+ border-color: rgba(245, 108, 108, 0.5);
+ }
+ }
+ }
+
+ :deep(.header-action-btn) {
+ position: relative;
+ overflow: hidden;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.08));
+ border: 1px solid rgba(255, 255, 255, 0.16);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 10px 18px rgba(0, 0, 0, 0.12);
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), transparent 55%);
+ pointer-events: none;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: -120%;
+ left: -40%;
+ width: 60%;
+ height: 260%;
+ background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.28), transparent);
+ transform: rotate(24deg);
+ opacity: 0;
+ transition: all 0.35s ease;
+ }
+
+ &:hover::after {
+ left: 100%;
+ opacity: 1;
+ }
+ }
+ }
+
+ .assistant-switcher {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ padding: 0 12px;
+ position: relative;
+ z-index: 1;
+
+ :deep(.el-radio-group) {
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+ justify-content: center;
+ padding: 4px;
+ border-radius: 999px;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.08));
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 10px 18px rgba(0, 0, 0, 0.1);
+ }
+
+ :deep(.el-radio-button__inner) {
+ border-radius: 999px;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ background: rgba(255, 255, 255, 0.12);
+ color: rgba(255, 255, 255, 0.86);
+ box-shadow: none;
+ padding: 7px 14px;
+ font-weight: 500;
+ }
+
+ :deep(.el-radio-button:first-child .el-radio-button__inner),
+ :deep(.el-radio-button:last-child .el-radio-button__inner) {
+ border-radius: 999px;
+ }
+
+ :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+ background: #fff;
+ color: $primary-blue;
+ border-color: #fff;
+ box-shadow: 0 6px 14px rgba(0, 40, 120, 0.16);
+ }
+ }
+}
+
+@keyframes headerGlow {
+ 0% {
+ opacity: 0.6;
+ transform: scale(1);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1.15);
+ }
+}
+
+@keyframes iconFloat {
+ 0%, 100% {
+ transform: translateY(0) rotate(0);
+ }
+ 50% {
+ transform: translateY(-2px) rotate(3deg);
+ }
+}
+
+.chat-container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 100%;
+ background: $ice-white;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 240px;
+ background: linear-gradient(180deg, rgba(0, 85, 212, 0.06) 0%, transparent 100%);
+ pointer-events: none;
+ }
+}
+
+.history-panel {
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(180deg, #fff 0%, $ice-white 100%);
+ z-index: 10;
+ display: flex;
+ flex-direction: column;
+ box-shadow: -8px 0 32px rgba(0, 85, 212, 0.15);
+
+ .history-header {
+ padding: 18px 20px;
+ border-bottom: 1px solid rgba(0, 85, 212, 0.12);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-weight: 600;
+ font-size: 14px;
+ color: $deep-blue;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.08) 0%, rgba(0, 136, 232, 0.05) 100%);
+ position: relative;
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(0, 85, 212, 0.2), transparent);
+ }
+ }
+
+ .session-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 12px 16px;
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: linear-gradient(180deg, $secondary-blue, $primary-blue);
+ border-radius: 4px;
+ box-shadow: 0 0 6px rgba(0, 85, 212, 0.25);
+ }
+
+ .session-item {
+ display: flex;
+ align-items: center;
+ padding: 14px 16px;
+ margin-bottom: 6px;
+ border-radius: 12px;
+ cursor: pointer;
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ gap: 12px;
+ position: relative;
+ border: 1px solid transparent;
+ background: #fff;
+ animation: sessionSlideIn 0.35s ease;
+
+ @keyframes sessionSlideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-15px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ }
+
+ &:hover {
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.06) 0%, rgba(0, 136, 232, 0.08) 100%);
+ border-color: rgba(0, 85, 212, 0.12);
+ box-shadow: 0 4px 16px rgba(0, 85, 212, 0.1);
+ transform: translateX(4px);
+
+ .delete-btn {
+ opacity: 1;
+ transform: scale(1);
+ }
+ }
+
+ &.active {
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.12) 0%, rgba(0, 136, 232, 0.15) 100%);
+ border-color: rgba(0, 85, 212, 0.25);
+ color: $primary-blue;
+ box-shadow: 0 4px 16px rgba(0, 85, 212, 0.15);
+
+ .el-icon {
+ color: $primary-blue;
+ }
+ }
+
+ .el-icon {
+ font-size: 18px;
+ flex-shrink: 0;
+ color: $secondary-blue;
+ transition: color 0.2s;
+ }
+
+ .session-name {
+ flex: 1;
+ font-size: 13px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: #1a1a2e;
+ font-weight: 500;
+ }
+
+ .delete-btn {
+ opacity: 0;
+ transform: scale(0.8);
+ transition: all 0.25s ease;
+ padding: 6px;
+ border-radius: 6px;
+ color: #c0c4cc;
+
+ &:hover {
+ color: #fff;
+ background: rgba(245, 108, 108, 0.85);
+ transform: scale(1.1) rotate(8deg);
+ }
+ }
+ }
+ }
+}
+
+.chat-main {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ flex: 1;
+ overflow: hidden;
+}
+
+.message-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 24px 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ background: linear-gradient(180deg, transparent 0%, rgba(0, 85, 212, 0.02) 100%);
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: linear-gradient(180deg, $secondary-blue, $primary-blue);
+ border-radius: 4px;
+ box-shadow: 0 0 8px rgba(0, 85, 212, 0.3);
+ }
+}
+
+.message-item {
+ display: flex;
+ gap: 14px;
+ width: 100%;
+ animation: messageSlideIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+
+ @keyframes messageSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY(20px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+ }
+
+ .avatar {
+ width: 42px;
+ height: 42px;
+ border-radius: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ font-size: 24px;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: inherit;
+ filter: blur(10px);
+ opacity: 0.5;
+ z-index: -1;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: linear-gradient(45deg, transparent 40%, rgba(255, 255, 255, 0.2) 50%, transparent 60%);
+ animation: shimmer 3s infinite;
+ }
+ }
+
+ .message-content {
+ flex: 1;
+ overflow-x: hidden;
+ display: flex;
+ flex-direction: column;
+ max-width: calc(100% - 56px);
+
+ .text-box {
+ padding: 14px 20px;
+ border-radius: 18px;
+ font-size: 14px;
+ line-height: 1.7;
+ word-break: break-word;
+ max-width: 100%;
+ width: fit-content;
+ overflow-x: auto;
+ transition: all 0.3s ease;
+ position: relative;
+
+ &::-webkit-scrollbar {
+ height: 4px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: rgba(0, 85, 212, 0.25);
+ border-radius: 2px;
+ }
+ }
+ }
+
+ &.bot-message {
+ .message-content {
+ align-items: flex-start;
+ }
+ .avatar {
+ background: $gradient-dark;
+ color: #fff;
+ box-shadow: 0 6px 20px rgba(0, 85, 212, 0.35);
+ }
+ .text-box {
+ background: #fff;
+ color: #1a1a2e;
+ box-shadow: $shadow-card;
+ border: 1px solid rgba(0, 85, 212, 0.08);
+ border-top-left-radius: 6px;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, rgba(0, 85, 212, 0.15), transparent);
+ }
+ }
+ }
+
+ &.user-message {
+ flex-direction: row-reverse;
+ .message-content {
+ align-items: flex-end;
+ }
+ .avatar {
+ background: linear-gradient(145deg, #5a9fe0, #3d8bd4);
+ color: #fff;
+ box-shadow: 0 6px 20px rgba(0, 85, 212, 0.4);
+ }
+ .text-box {
+ background: $gradient-dark;
+ color: #fff;
+ border-top-right-radius: 6px;
+ box-shadow: 0 6px 24px rgba(0, 85, 212, 0.3);
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3));
+ }
+ }
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ transform: translateX(-100%) rotate(45deg);
+ }
+ 100% {
+ transform: translateX(100%) rotate(45deg);
+ }
+}
+
+.charts-wrapper {
+ margin-top: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ overflow-x: auto;
+ width: 100%;
+ padding-bottom: 8px;
+
+ &::-webkit-scrollbar {
+ height: 6px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: linear-gradient(90deg, $light-blue, $secondary-blue);
+ border-radius: 3px;
+ }
+}
+
+.chart-item {
+ width: 100%;
+ min-width: 300px;
+ height: 300px;
+ border-radius: 12px;
+ padding: 12px;
+ margin-bottom: 12px;
+}
+
+.table-wrapper {
+ margin-top: 12px;
+ background: #fff;
+ border-radius: 12px;
+ overflow: hidden;
+ overflow-x: auto;
+ width: 100%;
+ box-shadow: $shadow-card;
+ border: 1px solid rgba(0, 122, 255, 0.06);
+
+ &::-webkit-scrollbar {
+ height: 6px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: linear-gradient(90deg, $light-blue, $secondary-blue);
+ border-radius: 3px;
+ }
+
+ .el-table {
+ min-width: 300px;
+ --el-table-border-color: rgba(0, 122, 255, 0.08);
+ --el-table-header-bg-color: $ice-white;
+ }
+}
+
+.input-area {
+ padding: 18px 20px;
+ background: linear-gradient(180deg, rgba(232, 242, 255, 0.95) 0%, #fff 100%);
+ border-top: 1px solid rgba(0, 85, 212, 0.1);
+ position: relative;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 20px;
+ right: 20px;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(0, 85, 212, 0.15), transparent);
+ }
+
+ .input-actions {
+ display: flex;
+ gap: 14px;
+ margin-bottom: 12px;
+ align-items: center;
+
+ .file-upload-trigger {
+ display: inline-flex;
+ align-items: center;
+ }
+
+ :deep(.utility-action-btn) {
+ position: relative;
+ height: 34px;
+ padding: 0 14px;
+ border-radius: 999px;
+ border: 1px solid rgba(92, 119, 255, 0.18);
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(236, 243, 255, 0.98));
+ color: $primary-blue;
+ font-weight: 600;
+ box-shadow: 0 10px 20px rgba(0, 85, 212, 0.08);
+ transition: all 0.25s ease;
+
+ .el-icon {
+ margin-right: 5px;
+ }
+
+ &:hover:not(.is-disabled) {
+ color: #fff;
+ border-color: transparent;
+ background: linear-gradient(135deg, #1f6dff 0%, #6b38ef 100%);
+ box-shadow: 0 14px 24px rgba(64, 90, 255, 0.2);
+ transform: translateY(-1px);
+ }
+ }
+
+ :deep(.stop-action-btn) {
+ border-color: rgba(255, 99, 123, 0.18);
+ color: #d33e5e;
+
+ &:hover:not(.is-disabled) {
+ background: linear-gradient(135deg, #f5536e 0%, #a33cff 100%);
+ }
+ }
+ }
+
+ .input-box {
+ padding: 16px;
+ position: relative;
+ background: #fff;
+ border: 2px solid rgba(0, 85, 212, 0.12);
+ border-radius: 16px;
+ margin: 0 4px;
+ transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+
+ &:focus-within {
+ border-color: $primary-blue;
+ box-shadow: 0 0 0 4px rgba(0, 85, 212, 0.12), $shadow-deep;
+ transform: translateY(-2px);
+ background: #fff;
+ }
+
+ .selected-file-tag {
+ display: flex;
+ align-items: center;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.1) 0%, rgba(0, 136, 232, 0.15) 100%);
+ border: 1px solid rgba(0, 85, 212, 0.2);
+ border-radius: 10px;
+ padding: 8px 12px;
+ margin-bottom: 12px;
+ gap: 10px;
+ width: fit-content;
+ max-width: 100%;
+ animation: tagSlideIn 0.3s ease;
+
+ @keyframes tagSlideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ }
+
+ .el-icon {
+ color: $primary-blue;
+ font-size: 18px;
+ }
+
+ .file-name {
+ font-size: 13px;
+ color: $deep-blue;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-weight: 600;
+ }
+
+ .remove-file {
+ cursor: pointer;
+ color: $secondary-blue;
+ transition: all 0.2s;
+ padding: 4px;
+ border-radius: 50%;
+
+ &:hover {
+ color: #fff;
+ background: rgba(245, 108, 108, 0.8);
+ transform: scale(1.1) rotate(90deg);
+ }
+ }
+ }
+
+ :deep(.el-textarea__inner) {
+ padding: 0;
+ padding-bottom: 35px;
+ border: none;
+ box-shadow: none;
+ background: transparent;
+ font-family: inherit;
+ font-size: 14px;
+ line-height: 1.6;
+ color: #1a1a2e;
+
+ &::placeholder {
+ color: #7ab8ff;
+ }
+
+ &:focus {
+ box-shadow: none;
+ }
+ }
+
+ .send-btn {
+ position: absolute;
+ right: 16px;
+ bottom: 16px;
+ padding: 10px 22px;
+ background: $gradient-dark;
+ border: none;
+ border-radius: 10px;
+ font-weight: 600;
+ font-size: 14px;
+ color: #fff;
+ box-shadow: 0 6px 20px rgba(0, 85, 212, 0.4);
+ transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ overflow: hidden;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ letter-spacing: 0.3px;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+ transition: left 0.5s;
+ }
+
+ &:hover:not(:disabled) {
+ transform: translateY(-3px) scale(1.02);
+ box-shadow: 0 10px 30px rgba(0, 85, 212, 0.5);
+
+ &::before {
+ left: 100%;
+ }
+ }
+
+ &:active:not(:disabled) {
+ transform: translateY(-1px) scale(0.98);
+ }
+
+ &:disabled {
+ background: linear-gradient(145deg, #b0b0b0, #c5c5c5);
+ box-shadow: none;
+ cursor: not-allowed;
+ }
+
+ .el-icon {
+ font-size: 15px;
+ transform: translateY(-1px);
+ }
+ }
+ }
+}
+
+.typing-indicator {
+ display: flex;
+ gap: 5px;
+ padding: 10px 14px;
+ background: #fff;
+ border-radius: 14px;
+ width: fit-content;
+ box-shadow: $shadow-card;
+ margin-top: 6px;
+ border: 1px solid rgba(0, 122, 255, 0.06);
+ border-top-left-radius: 4px;
+
+ .dot {
+ width: 7px;
+ height: 7px;
+ background: $secondary-blue;
+ border-radius: 50%;
+ animation: typing 1.4s infinite ease-in-out;
+
+ &:nth-child(2) {
+ animation-delay: 0.2s;
+ background: $primary-blue;
+ }
+ &:nth-child(3) {
+ animation-delay: 0.4s;
+ background: $deep-blue;
+ }
+ }
+}
+
+@keyframes typing {
+ 0%, 80%, 100% {
+ transform: scale(0.6);
+ opacity: 0.4;
+ }
+ 40% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+.code-block {
+ background: linear-gradient(145deg, #1a1a2e, #16213e);
+ color: #a8d8ff;
+ padding: 14px;
+ border-radius: 10px;
+ font-family: 'Fira Code', 'Consolas', monospace;
+ margin: 10px 0;
+ overflow-x: auto;
+ border: 1px solid rgba(90, 200, 250, 0.15);
+ box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.2);
+
+ &.js-code {
+ color: #5ac8fa;
+ }
+}
+
+.chat-main {
+ background:
+ radial-gradient(circle at top left, rgba(46, 140, 224, 0.12) 0%, transparent 34%),
+ linear-gradient(180deg, #fff 0%, #f7fbff 46%, #fff 100%);
+}
+
+.chat-hero {
+ display: grid;
+ grid-template-columns: 164px minmax(0, 1fr);
+ gap: 18px;
+ align-items: start;
+ padding: 14px 18px 6px;
+
+ &.compact {
+ grid-template-columns: 122px minmax(0, 1fr);
+ gap: 12px;
+ padding: 8px 18px 2px;
+ }
+}
+
+.assistant-stand {
+ position: relative;
+ min-height: 252px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ padding-top: 18px;
+ overflow: hidden;
+
+ &.compact {
+ min-height: 176px;
+ padding-top: 8px;
+ }
+
+ &.thinking {
+ .assistant-halo {
+ opacity: 1;
+ transform: scale(1.08);
+ filter: blur(8px);
+ }
+
+ .assistant-scan-ring {
+ opacity: 1;
+ animation-duration: 1.6s;
+ }
+
+ .assistant-orbit {
+ opacity: 1;
+ }
+
+ .assistant-bot {
+ transform: translateY(-4px) scale(1.02);
+ }
+
+ .assistant-bot-head {
+ box-shadow: 0 0 30px rgba(80, 157, 255, 0.36);
+ }
+
+ .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-status {
+ color: #6a3bee;
+ box-shadow: 0 10px 22px rgba(106, 59, 238, 0.14);
+ }
+
+ .assistant-status-dot {
+ background: #6a3bee;
+ box-shadow: 0 0 12px rgba(106, 59, 238, 0.9);
+ animation: thinkingDot 1s ease-in-out infinite;
+ }
+
+ .assistant-base-sm {
+ box-shadow: 0 0 24px rgba(255, 93, 122, 0.48);
+ }
+ }
+}
+
+.assistant-halo {
+ position: absolute;
+ top: 22px;
+ width: 130px;
+ height: 130px;
+ 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%);
+ filter: blur(6px);
+ opacity: 0.82;
+ transition: all 0.35s ease;
+}
+
+.assistant-scan-ring {
+ position: absolute;
+ top: 40px;
+ width: 132px;
+ height: 132px;
+ border-radius: 50%;
+ border: 1px solid rgba(90, 159, 224, 0.22);
+ box-shadow: inset 0 0 16px rgba(255, 255, 255, 0.25);
+ opacity: 0.55;
+ animation: scanRing 4s linear infinite;
+}
+
+.assistant-orbit {
+ position: absolute;
+ top: 52px;
+ width: 150px;
+ height: 150px;
+ border-radius: 50%;
+ border: 1px dashed rgba(92, 135, 255, 0.22);
+ opacity: 0.45;
+}
+
+.assistant-orbit-a {
+ animation: orbitRotate 8s linear infinite;
+}
+
+.assistant-orbit-b {
+ width: 118px;
+ height: 118px;
+ top: 68px;
+ border-color: rgba(255, 108, 150, 0.22);
+ animation: orbitRotateReverse 5.6s linear infinite;
+}
+
+.assistant-bot {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ margin-top: 12px;
+ 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%;
+ transform: translateX(-50%);
+ background: linear-gradient(135deg, #54bfff, #7a41ff);
+ box-shadow: 0 0 14px rgba(84, 191, 255, 0.65);
+ }
+}
+
+.assistant-bot-antenna-left {
+ left: 36px;
+ transform: rotate(-14deg);
+}
+
+.assistant-bot-antenna-right {
+ right: 36px;
+ transform: rotate(14deg);
+}
+
+.assistant-bot-head {
+ position: relative;
+ 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);
+}
+
+.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 {
+ position: absolute;
+ left: 50%;
+ bottom: 16px;
+ width: 22px;
+ height: 4px;
+ 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);
+}
+
+.assistant-status {
+ position: relative;
+ z-index: 1;
+ margin-top: 14px;
+ padding: 6px 12px;
+ border-radius: 999px;
+ font-size: 12px;
+ font-weight: 600;
+ color: $deep-blue;
+ background: rgba(255, 255, 255, 0.95);
+ border: 1px solid rgba(0, 85, 212, 0.12);
+ box-shadow: 0 8px 20px rgba(0, 85, 212, 0.08);
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.assistant-status-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: #2e8ce0;
+ box-shadow: 0 0 10px rgba(46, 140, 224, 0.72);
+}
+
+.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;
+}
+
+.assistant-base-md {
+ bottom: 6px;
+ width: 88px;
+ height: 20px;
+ border-color: rgba(255, 93, 122, 0.34);
+}
+
+.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));
+ 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);
+ }
+}
+
+@keyframes orbitRotate {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes orbitRotateReverse {
+ from {
+ transform: rotate(360deg);
+ }
+ to {
+ transform: rotate(0deg);
+ }
+}
+
+@keyframes scanRing {
+ 0%, 100% {
+ transform: scale(0.96);
+ opacity: 0.42;
+ }
+ 50% {
+ transform: scale(1.04);
+ opacity: 0.86;
+ }
+}
+
+@keyframes thinkingDot {
+ 0%, 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.35);
+ }
+}
+
+.welcome-card {
+ position: relative;
+ padding: 14px 14px 12px;
+ border-radius: 16px;
+ background:
+ linear-gradient(#fff, #fff) padding-box,
+ linear-gradient(135deg, rgba(255, 64, 96, 0.85), rgba(117, 65, 255, 0.9)) border-box;
+ border: 1px solid transparent;
+ box-shadow: 0 16px 36px rgba(0, 85, 212, 0.12);
+
+ &.compact {
+ padding: 10px 12px;
+ border-radius: 12px;
+ box-shadow: 0 8px 16px rgba(0, 85, 212, 0.07);
+
+ .welcome-eyebrow {
+ margin-bottom: 4px;
+ }
+
+ .welcome-title {
+ font-size: 17px;
+ line-height: 1.3;
+
+ br {
+ display: none;
+ }
+ }
+
+ .welcome-desc {
+ margin-top: 6px;
+ font-size: 12px;
+ line-height: 1.55;
+ }
+
+ .quick-prompt-list {
+ margin-top: 10px;
+ gap: 6px;
+ }
+
+ .quick-prompt-btn {
+ padding: 8px 10px;
+ font-size: 12px;
+ border-radius: 7px;
+ }
+
+ .more-prompts-btn {
+ margin-top: 8px;
+ font-size: 12px;
+ }
+ }
+}
+
+.welcome-eyebrow {
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 2px;
+ color: rgba(0, 85, 212, 0.58);
+ margin-bottom: 8px;
+}
+
+.welcome-title {
+ margin: 0;
+ font-size: 26px;
+ line-height: 1.2;
+ font-weight: 800;
+ color: #172033;
+}
+
+.welcome-desc {
+ margin: 10px 0 0;
+ font-size: 13px;
+ line-height: 1.7;
+ color: #5f6980;
+}
+
+.quick-prompt-list {
+ display: grid;
+ gap: 8px;
+ margin-top: 14px;
+}
+
+.quick-prompt-btn {
+ width: 100%;
+ border: none;
+ border-radius: 10px;
+ padding: 11px 14px;
+ text-align: left;
+ font-size: 13px;
+ font-weight: 600;
+ color: #fff;
+ cursor: pointer;
+ background: linear-gradient(90deg, #ff4c55 0%, #7c38ef 100%);
+ box-shadow: 0 12px 22px rgba(124, 56, 239, 0.18);
+ transition: transform 0.25s ease, box-shadow 0.25s ease, opacity 0.2s ease;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), transparent 56%);
+ pointer-events: none;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: -120%;
+ left: -30%;
+ width: 45%;
+ height: 260%;
+ background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+ transform: rotate(22deg);
+ opacity: 0;
+ transition: all 0.35s ease;
+ }
+
+ &:hover:not(:disabled) {
+ transform: translateY(-2px) scale(1.01);
+ box-shadow: 0 16px 28px rgba(124, 56, 239, 0.24);
+
+ &::after {
+ left: 100%;
+ opacity: 1;
+ }
+ }
+
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.65;
+ }
+}
+
+.more-prompts-btn {
+ margin-top: 10px;
+ padding: 0 12px;
+ height: 32px;
+ 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-weight: 600;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ box-shadow: 0 10px 18px rgba(208, 65, 81, 0.08);
+ transition: all 0.25s ease;
+
+ &:hover {
+ transform: translateY(-1px);
+ background: linear-gradient(135deg, #ff5570 0%, #8a3df6 100%);
+ 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));
+ }
+}
+
+.message-list {
+ padding: 8px 18px 18px;
+ gap: 16px;
+ background: transparent;
+}
+
+.input-area {
+ padding: 12px 18px 16px;
+ background: #fff;
+ border-top: none;
+
+ &::before {
+ display: none;
+ }
+
+ .input-box {
+ padding: 14px 16px 16px;
+ border: 1px solid rgba(123, 56, 239, 0.9);
+ border-radius: 22px;
+ margin: 0;
+ transition: all 0.25s ease;
+ box-shadow: 0 14px 34px rgba(0, 85, 212, 0.08);
+
+ &:focus-within {
+ border-color: #7c38ef;
+ box-shadow: 0 0 0 3px rgba(124, 56, 239, 0.1), 0 18px 40px rgba(0, 85, 212, 0.12);
+ transform: none;
+ }
+
+ :deep(.el-textarea__inner) {
+ padding-right: 58px;
+ padding-bottom: 0;
+ min-height: 104px;
+
+ &::placeholder {
+ color: #a0a9bc;
+ }
+ }
+
+ .send-btn {
+ right: 25px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 36px;
+ min-width: 36px;
+ height: 36px;
+ padding: 0;
+ background: linear-gradient(135deg, #ff5570 0%, #7a36f2 58%, #2d79ff 100%);
+ border-radius: 50%;
+ box-shadow: 0 12px 24px rgba(109, 50, 236, 0.24);
+ transition: all 0.25s ease;
+ gap: 0;
+
+ &:hover:not(:disabled) {
+ transform: translateY(calc(-50% - 1px)) scale(1.04);
+ box-shadow: 0 16px 28px rgba(109, 50, 236, 0.3);
+ }
+
+ &:active:not(:disabled) {
+ transform: translateY(-50%) scale(0.96);
+ }
+
+ .el-icon {
+ margin: 0;
+ font-size: 16px;
+ transform: translate(0, -1px);
+ }
+ }
+ }
+}
+
+@media (max-width: 767px) {
+ .chat-hero {
+ grid-template-columns: 1fr;
+ gap: 10px;
+ padding: 14px 14px 6px;
+
+ &.compact {
+ padding: 8px 14px 4px;
+ }
+ }
+
+ .assistant-stand {
+ min-height: 184px;
+ }
+
+ .welcome-card {
+ padding: 12px 12px 10px;
+ }
+
+ .welcome-title {
+ font-size: 21px;
+ }
+
+ .hero-dot-grid {
+ grid-template-columns: repeat(12, 1fr);
+ gap: 6px;
+ padding: 0 14px 12px;
+ }
+
+ .message-list {
+ padding: 8px 14px 14px;
+ }
+
+ .input-area {
+ padding: 10px 14px 14px;
+ }
+
+ .input-area .input-actions {
+ gap: 10px;
+ flex-wrap: wrap;
+ }
+}
+</style>
diff --git a/src/components/Dialog/FileList.vue b/src/components/Dialog/FileList.vue
new file mode 100644
index 0000000..e373c27
--- /dev/null
+++ b/src/components/Dialog/FileList.vue
@@ -0,0 +1,253 @@
+<template>
+ <el-dialog
+ v-model="isShow"
+ :title="title"
+ :width="width"
+ @close="handleClose"
+ class="attachment-dialog"
+ >
+ <!-- 宸ュ叿鏍� -->
+ <div class="toolbar">
+ <el-button
+ type="primary"
+ size="small"
+ @click="handleUpload"
+ >
+ 涓婁紶闄勪欢
+ </el-button>
+ </div>
+
+ <!-- 涓婁紶缁勪欢寮圭獥 -->
+ <el-dialog
+ v-model="uploadDialogVisible"
+ title="涓婁紶闄勪欢"
+ width="50%"
+ @close="handleUploadClose"
+ >
+ <AttachmentUpload
+ v-model:file-list="newFileList"
+ />
+ <template #footer>
+ <el-button @click="handleUploadClose">鍏抽棴</el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 鏂囦欢鍒楄〃琛ㄦ牸 -->
+ <div class="table-container">
+ <el-table
+ :data="tableData"
+ border
+ class="attachment-table"
+ :height="tableData.length > 0 ? 'auto' : '120px'"
+ >
+ <el-table-column
+ label="闄勪欢鍚嶇О"
+ prop="originalFilename"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ v-if="showActions"
+ fixed="right"
+ label="鎿嶄綔"
+ :width="120"
+ align="center"
+ >
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ size="small"
+ :href="scope.row.downloadURL"
+ class="download-link"
+ >
+ 涓嬭浇
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ size="small"
+ @click="handleDelete(scope.row)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </el-dialog>
+</template>
+
+<script setup>
+import { ref, computed, getCurrentInstance, onMounted, watch } from 'vue'
+import AttachmentUpload from '@/components/AttachmentUpload/file/index.vue'
+import {attachmentList, deleteAttachment, createAttachment} from "@/api/basicData/storageAttachment.js";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ recordType: {
+ type: String,
+ default: '',
+ required: true
+ },
+ recordId: {
+ type: Number,
+ default: 0,
+ required: true
+ },
+ title: {
+ type: String,
+ default: '闄勪欢'
+ },
+ width: {
+ type: String,
+ default: '50%'
+ },
+ showActions: {
+ type: Boolean,
+ default: true
+ }
+})
+
+const emit = defineEmits([
+ 'close',
+ 'download',
+ 'upload',
+ 'delete'
+])
+
+const { proxy } = getCurrentInstance()
+const tableData = ref([])
+const uploadDialogVisible = ref(false)
+const newFileList = ref([])
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit("update:visible", val);
+ },
+});
+
+const handleClose = () => {
+ isShow.value = false
+}
+
+const handleUpload = () => {
+ uploadDialogVisible.value = true
+}
+
+const handleUploadClose = async () => {
+ // 妫�鏌ユ槸鍚︽湁鏂颁笂浼犵殑鏂囦欢
+ if (newFileList.value.length > 0) {
+ try {
+ await createAttachment({
+ application: 'file',
+ recordType: props.recordType,
+ recordId: props.recordId,
+ storageBlobDTOs: [...newFileList.value, ...tableData.value]
+ })
+ newFileList.value = []
+ // 鍒锋柊鍒楄〃
+ setList()
+ } catch (error) {
+ proxy?.$modal?.msgError('涓婁紶澶辫触')
+ }
+ }
+ uploadDialogVisible.value = false
+}
+
+
+
+const handleDelete = async (row, index) => {
+ try {
+ await deleteAttachment([row.storageAttachmentId])
+ proxy?.$modal?.msgSuccess('鍒犻櫎鎴愬姛')
+ setList()
+ } catch (error) {
+ proxy?.$modal?.msgError('鍒犻櫎澶辫触')
+ }
+}
+
+const setList = () => {
+ attachmentList({
+ recordType: props.recordType,
+ recordId: props.recordId,
+ }).then(res => {
+ if (res && res.data) {
+ tableData.value = res.data || []
+ }
+ })
+}
+
+onMounted(() => {
+ setList()
+})
+</script>
+
+<style scoped>
+.attachment-dialog {
+ border-radius: 12px;
+}
+
+.toolbar {
+ margin-bottom: 16px;
+ text-align: right;
+}
+
+.table-container {
+ max-height: 40vh;
+ overflow-y: auto;
+ min-height: 120px;
+ padding-bottom: 16px;
+ box-sizing: border-box;
+ will-change: scroll-position;
+ transform: translateZ(0);
+ -webkit-overflow-scrolling: touch;
+}
+
+:deep(.el-table) {
+ margin-bottom: 0;
+}
+
+:deep(.el-table__body-wrapper) {
+ overflow-y: auto;
+ will-change: transform;
+ transform: translateZ(0);
+}
+
+:deep(.el-table__body tr) {
+ transition: none;
+}
+
+:deep(.el-dialog__footer) {
+ padding-top: 12px;
+ border-top: 1px solid #e9ecef;
+}
+
+.attachment-table {
+ border-radius: 8px;
+}
+
+:deep(.el-dialog__header) {
+ background-color: #f8f9fa;
+ border-bottom: 1px solid #e9ecef;
+ padding: 16px 20px;
+}
+
+:deep(.el-dialog__title) {
+ font-size: 16px;
+ font-weight: 600;
+}
+
+:deep(.el-dialog__body) {
+ padding: 16px 20px;
+}
+
+:deep(.el-table__empty-text) {
+ color: #999;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/PurchaseAIChatSidebar/index.vue b/src/components/PurchaseAIChatSidebar/index.vue
new file mode 100644
index 0000000..f97d521
--- /dev/null
+++ b/src/components/PurchaseAIChatSidebar/index.vue
@@ -0,0 +1,24 @@
+<template>
+ <AIChatSidebar :assistants="assistants" default-assistant="purchase" />
+</template>
+
+<script setup>
+import { ShoppingCart } from '@element-plus/icons-vue'
+import AIChatSidebar from '@/components/AIChatSidebar/index.vue'
+
+const assistants = [
+ {
+ key: 'purchase',
+ label: '閲囪喘鍔╃悊',
+ title: '閲囪喘鏅鸿兘鍔╃悊',
+ tooltip: '閲囪喘鏅鸿兘鍔╃悊',
+ icon: ShoppingCart,
+ apiBase: '/purchase-ai',
+ storageKey: 'purchase_ai_chat_uuid',
+ placeholder: '璇疯緭鍏ラ噰璐棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)',
+ welcomeMessage: '浣犲ソ',
+ allowFileUpload: false,
+ emptySessionText: '鏆傛棤閲囪喘浼氳瘽'
+ }
+]
+</script>
diff --git a/src/layout/index.vue b/src/layout/index.vue
index d3580d0..a1bb724 100644
--- a/src/layout/index.vue
+++ b/src/layout/index.vue
@@ -1,131 +1,133 @@
-<template>
- <div :class="classObj"
- class="app-wrapper"
- :style="{ '--current-color': theme }">
- <div v-if="device === 'mobile' && sidebar.opened"
- class="drawer-bg"
- @click="handleClickOutside" />
- <sidebar v-if="!sidebar.hide"
- class="sidebar-container" />
- <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }"
- class="main-container">
- <div :class="{ 'fixed-header': fixedHeader }">
- <navbar @setLayout="setLayout" />
- <tags-view v-if="needTagsView" />
- </div>
- <app-main />
- <settings ref="settingRef" />
- </div>
- </div>
-</template>
-
-<script setup>
- import { useWindowSize } from "@vueuse/core";
- import Sidebar from "./components/Sidebar/index.vue";
- import { AppMain, Navbar, Settings, TagsView } from "./components";
- import defaultSettings from "@/settings";
-
- import useAppStore from "@/store/modules/app";
- import useSettingsStore from "@/store/modules/settings";
-
- const settingsStore = useSettingsStore();
- const theme = computed(() => settingsStore.theme);
- const sideTheme = computed(() => settingsStore.sideTheme);
- const sidebar = computed(() => useAppStore().sidebar);
- const device = computed(() => useAppStore().device);
- const needTagsView = computed(() => settingsStore.tagsView);
- const fixedHeader = computed(() => settingsStore.fixedHeader);
-
- const classObj = computed(() => ({
- hideSidebar: !sidebar.value.opened,
- openSidebar: sidebar.value.opened,
- withoutAnimation: sidebar.value.withoutAnimation,
- mobile: device.value === "mobile",
- }));
-
- const { width, height } = useWindowSize();
- const WIDTH = 992; // refer to Bootstrap's responsive design
-
- watch(
- () => device.value,
- () => {
- if (device.value === "mobile" && sidebar.value.opened) {
- useAppStore().closeSideBar({ withoutAnimation: false });
- }
- }
- );
-
- watchEffect(() => {
- if (width.value - 1 < WIDTH) {
- useAppStore().toggleDevice("mobile");
- useAppStore().closeSideBar({ withoutAnimation: true });
- } else {
- useAppStore().toggleDevice("desktop");
- }
- });
-
- function handleClickOutside() {
- useAppStore().closeSideBar({ withoutAnimation: false });
- }
-
- const settingRef = ref(null);
- function setLayout() {
- settingRef.value.openSetting();
- }
-</script>
-
-<style lang="scss" scoped>
- @import "@/assets/styles/mixin.scss";
- @import "@/assets/styles/variables.module.scss";
-
- .app-wrapper {
- @include clearfix;
- position: relative;
- height: 100%;
- width: 100%;
- background: radial-gradient(
- circle at top,
- rgba(223, 232, 226, 0.95),
- transparent 32%
- ),
- linear-gradient(180deg, #f7faf8 0%, var(--app-bg) 100%);
-
- &.mobile.openSidebar {
- position: fixed;
- top: 0;
- }
- }
-
- .drawer-bg {
- background: #000;
- opacity: 0.3;
- width: 100%;
- top: 0;
- height: 100%;
- position: absolute;
- z-index: 999;
- }
-
- .fixed-header {
- position: fixed;
- top: 0px;
- padding-top: 12px;
- right: 16px;
- z-index: 9;
- width: calc(100% - #{$base-sidebar-width} - 32px);
- transition: width 0.28s, right 0.28s;
- padding-bottom: 8px;
- background-color: #f3f6f4;
- }
- .hideSidebar .fixed-header {
- width: calc(100% - 100px);
- }
-
- .sidebarHide .fixed-header {
- width: calc(100% - 32px);
- }
-
- .mobile .fixed-header {
- width: 100%;
- }
-</style>
+<template>
+ <div :class="classObj"
+ class="app-wrapper"
+ :style="{ '--current-color': theme }">
+ <div v-if="device === 'mobile' && sidebar.opened"
+ class="drawer-bg"
+ @click="handleClickOutside" />
+ <sidebar v-if="!sidebar.hide"
+ class="sidebar-container" />
+ <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }"
+ class="main-container">
+ <div :class="{ 'fixed-header': fixedHeader }">
+ <navbar @setLayout="setLayout" />
+ <tags-view v-if="needTagsView" />
+ </div>
+ <app-main />
+ <settings ref="settingRef" />
+ </div>
+ <AIChatSidebar />
+ </div>
+</template>
+
+<script setup>
+ import { useWindowSize } from "@vueuse/core";
+ import Sidebar from "./components/Sidebar/index.vue";
+ import { AppMain, Navbar, Settings, TagsView } from "./components";
+ import AIChatSidebar from "@/components/AIChatSidebar/index.vue";
+ import defaultSettings from "@/settings";
+
+ import useAppStore from "@/store/modules/app";
+ import useSettingsStore from "@/store/modules/settings";
+
+ const settingsStore = useSettingsStore();
+ const theme = computed(() => settingsStore.theme);
+ const sideTheme = computed(() => settingsStore.sideTheme);
+ const sidebar = computed(() => useAppStore().sidebar);
+ const device = computed(() => useAppStore().device);
+ const needTagsView = computed(() => settingsStore.tagsView);
+ const fixedHeader = computed(() => settingsStore.fixedHeader);
+
+ const classObj = computed(() => ({
+ hideSidebar: !sidebar.value.opened,
+ openSidebar: sidebar.value.opened,
+ withoutAnimation: sidebar.value.withoutAnimation,
+ mobile: device.value === "mobile",
+ }));
+
+ const { width, height } = useWindowSize();
+ const WIDTH = 992; // refer to Bootstrap's responsive design
+
+ watch(
+ () => device.value,
+ () => {
+ if (device.value === "mobile" && sidebar.value.opened) {
+ useAppStore().closeSideBar({ withoutAnimation: false });
+ }
+ }
+ );
+
+ watchEffect(() => {
+ if (width.value - 1 < WIDTH) {
+ useAppStore().toggleDevice("mobile");
+ useAppStore().closeSideBar({ withoutAnimation: true });
+ } else {
+ useAppStore().toggleDevice("desktop");
+ }
+ });
+
+ function handleClickOutside() {
+ useAppStore().closeSideBar({ withoutAnimation: false });
+ }
+
+ const settingRef = ref(null);
+ function setLayout() {
+ settingRef.value.openSetting();
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "@/assets/styles/mixin.scss";
+ @import "@/assets/styles/variables.module.scss";
+
+ .app-wrapper {
+ @include clearfix;
+ position: relative;
+ height: 100%;
+ width: 100%;
+ background: radial-gradient(
+ circle at top,
+ rgba(223, 232, 226, 0.95),
+ transparent 32%
+ ),
+ linear-gradient(180deg, #f7faf8 0%, var(--app-bg) 100%);
+
+ &.mobile.openSidebar {
+ position: fixed;
+ top: 0;
+ }
+ }
+
+ .drawer-bg {
+ background: #000;
+ opacity: 0.3;
+ width: 100%;
+ top: 0;
+ height: 100%;
+ position: absolute;
+ z-index: 999;
+ }
+
+ .fixed-header {
+ position: fixed;
+ top: 0px;
+ padding-top: 12px;
+ right: 16px;
+ z-index: 9;
+ width: calc(100% - #{$base-sidebar-width} - 32px);
+ transition: width 0.28s, right 0.28s;
+ padding-bottom: 8px;
+ background-color: #f3f6f4;
+ }
+ .hideSidebar .fixed-header {
+ width: calc(100% - 100px);
+ }
+
+ .sidebarHide .fixed-header {
+ width: calc(100% - 32px);
+ }
+
+ .mobile .fixed-header {
+ width: 100%;
+ }
+</style>
diff --git a/src/views/equipmentManagement/repair/Modal/RepairModal.vue b/src/views/equipmentManagement/repair/Modal/RepairModal.vue
index 1728b37..5e31943 100644
--- a/src/views/equipmentManagement/repair/Modal/RepairModal.vue
+++ b/src/views/equipmentManagement/repair/Modal/RepairModal.vue
@@ -49,8 +49,8 @@
</el-form-item>
</el-col>
<el-col :span="12">
- <el-form-item label="绫荤洰">
- <el-input v-model="form.machineryCategory" placeholder="璇疯緭鍏ョ被鐩�" />
+ <el-form-item label="椤圭洰">
+ <el-input v-model="form.machineryCategory" placeholder="璇疯緭鍏ラ」鐩�" />
</el-form-item>
</el-col>
</el-row>
@@ -77,12 +77,20 @@
</el-form-item>
</el-col>
</el-row>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="闄勪欢" prop="attachmentIds">
+ <FileUpload v-model:file-list="form.storageBlobDTOs" />
+ </el-form-item>
+ </el-col>
+ </el-row>
</el-form>
</FormDialog>
</template>
<script setup>
import FormDialog from "@/components/Dialog/FormDialog.vue";
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
import {
addRepair,
editRepair,
@@ -106,6 +114,7 @@
const userStore = useUserStore();
const deviceOptions = ref([]);
+const fileList = ref([]);
const loadDeviceName = async () => {
const { data } = await getDeviceLedger();
@@ -121,6 +130,7 @@
remark: undefined, // 鏁呴殰鐜拌薄
status: 0, // 鎶ヤ慨鐘舵��
machineryCategory: undefined,
+ storageBlobDTOs: [],
});
const setDeviceModel = (deviceId) => {
@@ -137,6 +147,7 @@
form.remark = data.remark;
form.status = data.status;
form.machineryCategory = data.machineryCategory;
+ form.storageBlobDTOs = data.storageBlobVOs || [];
};
const sendForm = async () => {
@@ -168,6 +179,7 @@
const openAdd = async () => {
id.value = undefined;
visible.value = true;
+ fileList.value = [];
await nextTick();
await loadDeviceName();
};
diff --git a/src/views/equipmentManagement/repair/index.vue b/src/views/equipmentManagement/repair/index.vue
index 27d0acb..f3a4330 100644
--- a/src/views/equipmentManagement/repair/index.vue
+++ b/src/views/equipmentManagement/repair/index.vue
@@ -127,22 +127,31 @@
>
鍒犻櫎
</el-button>
+ <el-button
+ type="primary"
+ link
+ @click="openFileDialog(row)"
+ >
+ 闄勪欢
+ </el-button>
</template>
</PIMTable>
</div>
<RepairModal ref="repairModalRef" @ok="getTableData"/>
<MaintainModal ref="maintainModalRef" @ok="getTableData"/>
+ <FileList v-if="fileDialogVisible" v-model:visible="fileDialogVisible" :record-type="'device_repair'" :record-id="recordId" />
</div>
</template>
<script setup>
-import { onMounted, getCurrentInstance, computed } from "vue";
+import {onMounted, getCurrentInstance, computed, ref, defineAsyncComponent} from "vue";
import {usePaginationApi} from "@/hooks/usePaginationApi";
import {getRepairPage, delRepair} from "@/api/equipmentManagement/repair";
import RepairModal from "./Modal/RepairModal.vue";
import {ElMessageBox, ElMessage} from "element-plus";
import dayjs from "dayjs";
import MaintainModal from "./Modal/MaintainModal.vue";
+const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
defineOptions({
name: "璁惧鎶ヤ慨",
@@ -188,7 +197,7 @@
prop: "deviceModel",
},
{
- label: "绫荤洰",
+ label: "椤圭洰",
align: "center",
prop: "machineryCategory",
},
@@ -258,6 +267,15 @@
getTableData();
};
+// 鎵撳紑闄勪欢寮圭獥
+const recordId =ref(0)
+const fileDialogVisible = ref(false)
+
+const openFileDialog = async (row) => {
+ recordId.value = row.id
+ fileDialogVisible.value = true
+}
+
// 澶氶�夊悗鍋氫粈涔�
const handleSelectionChange = (selectionList) => {
multipleList.value = selectionList;
diff --git a/src/views/equipmentManagement/upkeep/Form/PlanModal.vue b/src/views/equipmentManagement/upkeep/Form/PlanModal.vue
index 6fa6595..ee59ce2 100644
--- a/src/views/equipmentManagement/upkeep/Form/PlanModal.vue
+++ b/src/views/equipmentManagement/upkeep/Form/PlanModal.vue
@@ -32,10 +32,10 @@
disabled
/>
</el-form-item>
- <el-form-item label="绫荤洰">
+ <el-form-item label="椤圭洰">
<el-input
v-model="form.machineryCategory"
- placeholder="璇疯緭鍏ョ被鐩�"
+ placeholder="璇疯緭鍏ラ」鐩�"
/>
</el-form-item>
<el-form-item label="褰曞叆浜�">
@@ -73,6 +73,13 @@
clearable
/>
</el-form-item>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="闄勪欢" prop="attachmentIds">
+ <FileUpload v-model:file-list="form.storageBlobDTOs" />
+ </el-form-item>
+ </el-col>
+ </el-row>
</el-form>
</FormDialog>
</template>
@@ -90,6 +97,7 @@
import { onMounted } from "vue";
import dayjs from "dayjs";
import { userListNoPage } from "@/api/system/user.js";
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
defineOptions({
name: "璁惧淇濆吇鏂板璁″垝",
@@ -115,6 +123,7 @@
createUser: undefined, // 褰曞叆浜�
status: 0, //淇濅慨鐘舵��
machineryCategory: undefined,
+ storageBlobDTOs: [],
});
const setDeviceModel = (deviceId) => {
@@ -133,9 +142,12 @@
form.createUser = Number(data.createUser);
form.status = data.status;
form.machineryCategory = data.machineryCategory;
- form.maintenancePlanTime = dayjs(data.maintenancePlanTime).format(
- "YYYY-MM-DD HH:mm:ss"
- );
+ if (data.maintenancePlanTime) {
+ form.maintenancePlanTime = dayjs(data.maintenancePlanTime).format(
+ "YYYY-MM-DD HH:mm:ss"
+ );
+ }
+ form.storageBlobDTOs = data.storageBlobVOs || [];
};
// 鐢ㄦ埛鍒楄〃
diff --git a/src/views/equipmentManagement/upkeep/index.vue b/src/views/equipmentManagement/upkeep/index.vue
index 1e65663..3b771bf 100644
--- a/src/views/equipmentManagement/upkeep/index.vue
+++ b/src/views/equipmentManagement/upkeep/index.vue
@@ -218,40 +218,27 @@
<PlanModal ref="planModalRef" @ok="getTableData" />
<MaintenanceModal ref="maintainModalRef" @ok="getTableData" />
<FormDia ref="formDiaRef" @closeDia="getScheduledTableData" />
- <FileListDialog
- ref="fileListDialogRef"
- v-model="fileDialogVisible"
- :show-upload-button="true"
- :show-delete-button="true"
- :delete-method="handleAttachmentDelete"
- :name-column-label="'闄勪欢鍚嶇О'"
- :rulesRegulationsManagementId="currentMaintenanceTaskId"
- @upload="handleAttachmentUpload" />
+ <FileList v-if="fileDialogVisible" v-model:visible="fileDialogVisible" :record-type="'device_maintenance'" :record-id="currentMaintenanceTaskId" />
</div>
</template>
<script setup>
-import { ref, onMounted, reactive, getCurrentInstance, nextTick, computed } from 'vue'
+import {ref, onMounted, reactive, getCurrentInstance, nextTick, computed, defineAsyncComponent} from 'vue'
import { Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import PlanModal from './Form/PlanModal.vue'
import MaintenanceModal from './Form/MaintenanceModal.vue'
import FormDia from './Form/formDia.vue'
-import FileListDialog from '@/components/Dialog/FileListDialog.vue'
import {
getUpkeepPage,
delUpkeep,
deviceMaintenanceTaskList,
deviceMaintenanceTaskDel,
} from '@/api/equipmentManagement/upkeep'
-import {
- listMaintenanceTaskFiles,
- addMaintenanceTaskFile,
- delMaintenanceTaskFile,
-} from '@/api/equipmentManagement/maintenanceTaskFile'
import dayjs from 'dayjs'
const { proxy } = getCurrentInstance()
+const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
// Tab鐩稿叧
const activeTab = ref('scheduled')
@@ -373,7 +360,7 @@
prop: "createUserName",
},
{
- label: "绫荤洰",
+ label: "椤圭洰",
align: "center",
prop: "machineryCategory",
},
@@ -602,77 +589,10 @@
getTableData()
}
-// 闄勪欢鐩稿叧鏂规硶
-// 鏌ヨ闄勪欢鍒楄〃
-const fetchMaintenanceTaskFiles = async (deviceMaintenanceId) => {
- try {
- const params = {
- current: 1,
- size: 100,
- deviceMaintenanceId,
- rulesRegulationsManagementId:deviceMaintenanceId
- }
- const res = await listMaintenanceTaskFiles(params)
- const records = res?.data?.records || []
- const mapped = records.map(item => ({
- id: item.id,
- name: item.fileName || item.name,
- url: item.fileUrl || item.url,
- raw: item,
- }))
- fileListDialogRef.value?.setList(mapped)
- } catch (error) {
- ElMessage.error('鑾峰彇闄勪欢鍒楄〃澶辫触')
- }
-}
-
// 鎵撳紑闄勪欢寮圭獥
const openFileDialog = async (row) => {
currentMaintenanceTaskId.value = row.id
fileDialogVisible.value = true
- await fetchMaintenanceTaskFiles(row.id)
-}
-
-// 鍒锋柊闄勪欢鍒楄〃
-const refreshFileList = async () => {
- if (!currentMaintenanceTaskId.value) return
- await fetchMaintenanceTaskFiles(currentMaintenanceTaskId.value)
-}
-
-// 涓婁紶闄勪欢
-const handleAttachmentUpload = async (filePayload) => {
- if (!currentMaintenanceTaskId.value) return
- try {
- const payload = {
- name: filePayload?.fileName || filePayload?.name,
- url: filePayload?.fileUrl || filePayload?.url,
- deviceMaintenanceId: currentMaintenanceTaskId.value,
- }
- await addMaintenanceTaskFile(payload)
- ElMessage.success('鏂囦欢涓婁紶鎴愬姛')
- await refreshFileList()
- } catch (error) {
- ElMessage.error('鏂囦欢涓婁紶澶辫触')
- }
-}
-
-// 鍒犻櫎闄勪欢
-const handleAttachmentDelete = async (row) => {
- if (!row?.id) return false
- try {
- await ElMessageBox.confirm('纭鍒犻櫎璇ラ檮浠讹紵', '鎻愮ず', { type: 'warning' })
- } catch {
- return false
- }
- try {
- await delMaintenanceTaskFile(row.id)
- ElMessage.success('鍒犻櫎鎴愬姛')
- await refreshFileList()
- return true
- } catch (error) {
- ElMessage.error('鍒犻櫎澶辫触')
- return false
- }
}
onMounted(() => {
diff --git a/src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue b/src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue
index a83ff6a..e9b2646 100644
--- a/src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue
+++ b/src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue
@@ -35,16 +35,30 @@
<el-button type="primary"
link
@click="handleViewSupplementRecord(row)">
- {{ row.supplementQty ?? 0 }}
+ {{ row.feedingQty ?? 0 }}
</el-button>
</template>
</el-table-column>
<el-table-column label="閫�鏂欐暟閲�"
- prop="returnQty"
- min-width="110" />
+ min-width="110">
+ <template #default="{ row }">
+ {{ row.returnQty ?? 0 }}
+ </template>
+ </el-table-column>
<el-table-column label="瀹為檯鏁伴噺"
- prop="actualQty"
- min-width="110" />
+ min-width="140">
+ <template #default="{ row }">
+ <el-input-number v-model="row.actualQty"
+ :min="0"
+ :precision="3"
+ :step="1"
+ controls-position="right"
+ placeholder="杈撳叆瀹為檯鏁伴噺"
+ style="width: 100%;"
+ :disabled="row.returned"
+ @change="val => handleActualQtyChange(row, val)" />
+ </template>
+ </el-table-column>
</el-table>
<template #footer>
<span class="dialog-footer">
@@ -66,7 +80,7 @@
border
row-key="id">
<el-table-column label="琛ユ枡鏁伴噺"
- prop="supplementQty"
+ prop="pickQuantity"
min-width="120" />
<el-table-column label="琛ユ枡浜�"
prop="supplementUserName"
@@ -75,7 +89,7 @@
prop="supplementTime"
min-width="160" />
<el-table-column label="琛ユ枡鍘熷洜"
- prop="supplementReason"
+ prop="feedingReason"
min-width="200" />
</el-table>
<template #footer>
@@ -121,7 +135,7 @@
import {
listMaterialPickingDetail,
listMaterialSupplementRecord,
- confirmMaterialReturn,
+ updateMaterialPickingLedger,
} from "@/api/productionManagement/productionOrder.js";
const props = defineProps({
@@ -145,10 +159,12 @@
const returnSummaryList = ref([]);
const calcReturnQty = item =>
Number(item.pickQuantity || 0) +
- Number(item.supplementQty || 0) -
+ Number(item.feedingQty || 0) -
Number(item.actualQty || 0);
const canOpenReturnSummary = computed(() =>
- materialDetailTableData.value.some(item => calcReturnQty(item) > 0)
+ materialDetailTableData.value.some(
+ item => item.returned !== true && calcReturnQty(item) > 0
+ )
);
const loadDetailList = async () => {
@@ -157,7 +173,13 @@
materialDetailTableData.value = [];
try {
const res = await listMaterialPickingDetail(props.orderRow.id);
- materialDetailTableData.value = res.data || [];
+ materialDetailTableData.value = (res.data || []).map(item => ({
+ ...item,
+ actualQty:
+ item.actualQty ??
+ Number(item.pickQuantity || 0) + Number(item.feedingQty || 0),
+ returnQty: item.returnQty ?? 0,
+ }));
} finally {
materialDetailLoading.value = false;
}
@@ -176,6 +198,10 @@
materialDetailTableData.value = [];
};
+ const handleActualQtyChange = (row, val) => {
+ row.returnQty = calcReturnQty(row);
+ };
+
const handleViewSupplementRecord = async row => {
if (!row?.id) return;
supplementRecordDialogVisible.value = true;
@@ -183,7 +209,8 @@
supplementRecordTableData.value = [];
try {
const res = await listMaterialSupplementRecord({
- materialDetailId: row.id,
+ pickId: row.id,
+ productionOrderId: props.orderRow.id,
});
supplementRecordTableData.value = res.data || [];
} finally {
@@ -225,9 +252,24 @@
if (!props.orderRow?.id) return;
materialReturnConfirming.value = true;
try {
- await confirmMaterialReturn({
- orderId: props.orderRow.id,
- returnSummaryList: returnSummaryList.value,
+ await updateMaterialPickingLedger({
+ productionOrderId: props.orderRow.id,
+ productionOrderPickDto: materialDetailTableData.value.map(item => ({
+ id: item.id,
+ technologyOperationId: item.technologyOperationId,
+ operationName: item.operationName,
+ bom: item.bom === true,
+ productModelId: item.productModelId,
+ demandedQuantity: item.demandedQuantity,
+ unit: item.unit,
+ pickQuantity: item.pickQuantity,
+ batchNo: item.batchNo,
+ feedingQty: item.feedingQty,
+ returnQty: item.returnQty,
+ actualQty: item.actualQty,
+ feedingReason: item.feedingReason,
+ returned: true,
+ })),
});
returnSummaryDialogVisible.value = false;
dialogVisible.value = false;
diff --git a/src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue b/src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue
new file mode 100644
index 0000000..67e44f1
--- /dev/null
+++ b/src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue
@@ -0,0 +1,159 @@
+<template>
+ <el-dialog v-model="dialogVisible"
+ title="琛ユ枡"
+ width="1200px"
+ @close="handleClose">
+ <el-table v-loading="loading"
+ :data="tableData"
+ border
+ row-key="id">
+ <el-table-column label="宸ュ簭鍚嶇О"
+ prop="operationName"
+ min-width="140" />
+ <el-table-column label="鍘熸枡鍚嶇О"
+ prop="productName"
+ min-width="140" />
+ <el-table-column label="鍘熸枡鍨嬪彿"
+ prop="model"
+ min-width="140" />
+ <el-table-column label="璁¢噺鍗曚綅"
+ prop="unit"
+ width="100" />
+ <el-table-column label="棰嗙敤鏁伴噺"
+ prop="pickQuantity"
+ width="100" />
+ <el-table-column label="琛ユ枡鏁伴噺"
+ min-width="150">
+ <template #default="{ row }">
+ <el-input-number v-model="row.newSupplementQty"
+ :min="0"
+ :precision="3"
+ :step="1"
+ controls-position="right"
+ placeholder="杈撳叆琛ユ枡鏁伴噺"
+ style="width: 100%;" />
+ </template>
+ </el-table-column>
+ <el-table-column label="琛ユ枡鍘熷洜"
+ min-width="200">
+ <template #default="{ row }">
+ <el-input v-model="row.newSupplementReason"
+ placeholder="杈撳叆琛ユ枡鍘熷洜"
+ maxlength="200"
+ show-word-limit />
+ </template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ :loading="submitting"
+ @click="handleSubmit">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </span>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+ import { computed, ref, watch } from "vue";
+ import { ElMessage } from "element-plus";
+ import {
+ listMaterialPickingDetail,
+ updateMaterialPickingLedger,
+ } from "@/api/productionManagement/productionOrder.js";
+
+ const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ orderRow: { type: Object, default: null },
+ });
+ const emit = defineEmits(["update:modelValue", "saved"]);
+
+ const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: val => emit("update:modelValue", val),
+ });
+
+ const loading = ref(false);
+ const submitting = ref(false);
+ const tableData = ref([]);
+
+ const loadData = async () => {
+ if (!props.orderRow?.id) return;
+ loading.value = true;
+ try {
+ const res = await listMaterialPickingDetail(props.orderRow.id);
+ tableData.value = (res.data || []).map(item => ({
+ ...item,
+ newSupplementQty: 0,
+ newSupplementReason: "",
+ }));
+ } catch (e) {
+ console.error("鑾峰彇鐗╂枡鏄庣粏澶辫触锛�", e);
+ ElMessage.error("鑾峰彇鐗╂枡鏄庣粏澶辫触");
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ watch(
+ () => dialogVisible.value,
+ visible => {
+ if (visible) {
+ loadData();
+ }
+ }
+ );
+
+ const handleClose = () => {
+ tableData.value = [];
+ };
+
+ const handleSubmit = async () => {
+ const supplementList = tableData.value.filter(
+ item => item.newSupplementQty > 0
+ );
+ if (supplementList.length === 0) {
+ ElMessage.warning("璇疯嚦灏戣緭鍏ヤ竴鏉¤ˉ鏂欐暟閲�");
+ return;
+ }
+
+ const invalidRow = supplementList.find(item => !item.newSupplementReason);
+ if (invalidRow) {
+ ElMessage.warning("璇疯緭鍏ヨˉ鏂欏師鍥�");
+ return;
+ }
+
+ submitting.value = true;
+ try {
+ await updateMaterialPickingLedger({
+ productionOrderId: props.orderRow.id,
+ productionOrderPickDto: tableData.value.map(item => ({
+ id: item.id,
+ technologyOperationId: item.technologyOperationId,
+ operationName: item.operationName,
+ bom: item.bom === true,
+ productModelId: item.productModelId,
+ demandedQuantity: item.demandedQuantity,
+ unit: item.unit,
+ pickQuantity: item.pickQuantity,
+ batchNo: item.batchNo,
+ feedingQuantity: item.newSupplementQty || 0,
+ feedingReason: item.newSupplementReason || "",
+ pickType: 2,
+ })),
+ });
+ ElMessage.success("琛ユ枡鎴愬姛");
+ dialogVisible.value = false;
+ emit("saved");
+ } catch (e) {
+ console.error("琛ユ枡澶辫触锛�", e);
+ ElMessage.error("琛ユ枡澶辫触");
+ } finally {
+ submitting.value = false;
+ }
+ };
+</script>
+
+<style scoped lang="scss">
+</style>
diff --git a/src/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue b/src/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue
new file mode 100644
index 0000000..d1aa267
--- /dev/null
+++ b/src/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue
@@ -0,0 +1,225 @@
+<template>
+ <div class="print-container"
+ id="print-requisition">
+ <div class="print-content">
+ <div class="bill-title">鐢熶骇棰嗘枡鍗�</div>
+ <div class="info-grid">
+ <div class="info-row">
+ <div class="info-item">
+ <span class="label">鍒涘缓鏃ユ湡锛�</span>
+ <span class="value">{{ formatDate(orderRow?.createTime) }}</span>
+ </div>
+ <div class="info-item">
+ <span class="label">棰嗘枡鍗曞彿锛�</span>
+ <span class="value">{{ orderRow?.npsNo }}</span>
+ </div>
+ <div class="info-item">
+ <span class="label">鐢宠浜猴細</span>
+ <span class="value">{{ userName }}</span>
+ </div>
+ </div>
+ <div class="info-row">
+ <div class="info-item"
+ style="width: 50%;">
+ <span class="label">浜у搧鍚嶇О/鍨嬪彿锛�</span>
+ <span class="value">{{ orderRow?.productName }} / {{ orderRow?.model }}</span>
+ </div>
+ <div class="info-item"
+ style="width: 25%;">
+ <span class="label">鐢熶骇鏁伴噺锛�</span>
+ <span class="value">{{ orderRow?.quantity }}</span>
+ </div>
+ <div class="info-item"
+ style="width: 25%;">
+ <span class="label">闇�姹傛棩鏈燂細</span>
+ <span class="value">{{ formatDate(orderRow?.planCompleteTime) }}</span>
+ </div>
+ </div>
+ </div>
+ <table class="material-table">
+ <thead>
+ <tr>
+ <th width="50">搴忓彿</th>
+ <th>宸ュ簭鍚嶇О</th>
+ <th>瑙勬牸/鍚嶇О</th>
+ <th>鎵瑰彿</th>
+ <th width="80">闇�姹傛暟閲�</th>
+ <th width="80">棰嗘枡鏁伴噺</th>
+ <th width="60">鍗曚綅</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(item, index) in materialList"
+ :key="index">
+ <td align="center">{{ index + 1 }}</td>
+ <td>{{ item.operationName || '-' }}</td>
+ <td>{{ item.materialName || item.productName }} {{ item.materialModel || item.model }}</td>
+ <td>{{ item.batchNo || '-' }}</td>
+ <td align="right">{{ item.demandedQuantity }}</td>
+ <td align="right">{{ item.pickQuantity || item.pickQty || 0 }}</td>
+ <td align="center">{{ item.unit }}</td>
+ </tr>
+ </tbody>
+ </table>
+ <div class="print-footer">
+ <div class="footer-item">棰嗘枡锛歘_______________</div>
+ <div class="footer-item">鍙戞枡锛歘_______________</div>
+ <div class="footer-item">瀹℃牳锛歘_______________</div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import dayjs from "dayjs";
+ import useUserStore from "@/store/modules/user";
+ import { computed } from "vue";
+
+ const props = defineProps({
+ orderRow: {
+ type: Object,
+ default: () => ({}),
+ },
+ materialList: {
+ type: Array,
+ default: () => [],
+ },
+ });
+
+ const userStore = useUserStore();
+ const userName = computed(() => userStore.nickName || userStore.name || "-");
+
+ const formatDate = date => {
+ return date ? dayjs(date).format("YYYY骞碝M鏈圖D鏃�") : "-";
+ };
+</script>
+
+<style lang="scss">
+ /* 灞忓箷鏄剧ず鏍峰紡 */
+ .print-requisition-wrapper {
+ display: none;
+ }
+
+ /* 鎵撳嵃涓撶敤鏍峰紡 */
+ @media print {
+ @page {
+ size: landscape;
+ margin: 10mm;
+ }
+
+ /* 鍩虹鎵撳嵃璁剧疆 */
+ html,
+ body {
+ visibility: hidden;
+ height: auto !important;
+ overflow: visible !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ width: 100%;
+ }
+
+ /* 鏄惧紡鏄剧ず鎵撳嵃瀹瑰櫒鍙婂叾鎵�鏈夊瓙鍏冪礌 */
+ .print-requisition-wrapper,
+ .print-requisition-wrapper * {
+ visibility: visible !important;
+ }
+
+ /* 纭繚鎵撳嵃瀹瑰櫒鍗犳嵁鏁翠釜椤甸潰骞剁Щ闄ょ粷瀵瑰畾浣嶅共鎵� */
+ .print-requisition-wrapper {
+ display: block !important;
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: auto;
+ background: white;
+ margin: 0 !important;
+ padding: 0 !important;
+ z-index: 99999;
+ }
+
+ .print-container {
+ width: 100% !important;
+ padding: 0 10mm; /* 浣跨敤瀵圭О鐨勫乏鍙冲唴杈硅窛纭繚灞呬腑 */
+ box-sizing: border-box;
+ height: auto;
+ overflow: visible;
+ color: #000;
+ font-family: "SimSun", "STSong", serif;
+ page-break-inside: avoid;
+ display: block;
+ .print-content {
+ width: 100%;
+ text-align: center;
+ }
+ .bill-title {
+ font-size: 20px;
+ font-weight: bold;
+ text-align: center;
+ margin-bottom: 20px;
+ letter-spacing: 5px;
+ text-decoration: underline;
+ }
+
+ .info-grid {
+ margin-bottom: 10px;
+ font-size: 14px;
+
+ .info-row {
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 8px;
+
+ .info-item {
+ width: 33.33%;
+ display: flex;
+ align-items: flex-end;
+
+ .label {
+ font-weight: bold;
+ white-space: nowrap;
+ }
+ .value {
+ border-bottom: 1px solid #000;
+ padding: 0 5px;
+ flex: 1;
+ min-height: 20px;
+ }
+ }
+ }
+ }
+
+ .material-table {
+ width: 100%;
+ border-collapse: collapse;
+ border: 2px solid #000;
+ font-size: 13px;
+
+ th,
+ td {
+ border: 1px solid #000 !important;
+ padding: 6px 4px;
+ height: 25px;
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+ }
+
+ th {
+ background-color: #f2f2f2 !important;
+ font-weight: bold;
+ }
+ }
+
+ .print-footer {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 30px;
+ padding: 0 10px;
+
+ .footer-item {
+ font-size: 14px;
+ }
+ }
+ }
+ }
+</style>
diff --git a/src/views/productionManagement/productionOrder/index.vue b/src/views/productionManagement/productionOrder/index.vue
index f348aae..28356dd 100644
--- a/src/views/productionManagement/productionOrder/index.vue
+++ b/src/views/productionManagement/productionOrder/index.vue
@@ -175,9 +175,18 @@
<MaterialDetailDialog v-model="materialDetailDialogVisible"
:order-row="currentMaterialDetailOrder"
@confirmed="getList" />
+ <MaterialSupplementDialog v-model="materialSupplementDialogVisible"
+ :order-row="currentMaterialSupplementOrder"
+ @saved="getList" />
<new-product-order v-if="isShowNewModal"
v-model:visible="isShowNewModal"
@completed="handleQuery" />
+ <!-- 鎵撳嵃棰嗘枡鍗曠粍浠� -->
+ <div class="print-requisition-wrapper">
+ <PrintMaterialRequisition ref="printRef"
+ :order-row="printOrderRow"
+ :material-list="printMaterialList" />
+ </div>
</div>
</template>
@@ -205,8 +214,14 @@
import { listMain as getOrderProcessRouteMain } from "@/api/productionManagement/productProcessRoute.js";
import MaterialLedgerDialog from "@/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue";
import MaterialDetailDialog from "@/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue";
+ import MaterialSupplementDialog from "@/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue";
+ import PrintMaterialRequisition from "@/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import { listPage } from "@/api/productionManagement/processRoute.js";
+ import {
+ listMaterialPickingDetail,
+ listMaterialPickingBom,
+ } from "@/api/productionManagement/productionOrder.js";
const NewProductOrder = defineAsyncComponent(() =>
import("@/views/productionManagement/productionOrder/New.vue")
);
@@ -304,7 +319,7 @@
label: "鎿嶄綔",
align: "center",
fixed: "right",
- width: 360,
+ width: 260,
operation: [
{
name: "宸ヨ壓璺嚎",
@@ -340,15 +355,33 @@
{
name: "棰嗘枡",
type: "text",
+ color: "#5EC7AB",
clickFun: row => {
openMaterialDialog(row);
},
},
{
+ name: "琛ユ枡",
+ type: "text",
+ color: "#5EC7AB",
+ clickFun: row => {
+ openMaterialSupplementDialog(row);
+ },
+ },
+ {
name: "棰嗘枡璇︽儏",
type: "text",
+ color: "#5EC7AB",
clickFun: row => {
openMaterialDetailDialog(row);
+ },
+ },
+ {
+ name: "鎵撳嵃棰嗘枡鍗�",
+ type: "text",
+ color: "#409eff",
+ clickFun: row => {
+ handlePrint(row);
},
},
],
@@ -423,6 +456,44 @@
const currentMaterialOrder = ref(null);
const materialDetailDialogVisible = ref(false);
const currentMaterialDetailOrder = ref(null);
+ const materialSupplementDialogVisible = ref(false);
+ const currentMaterialSupplementOrder = ref(null);
+
+ // 鎵撳嵃鐩稿叧
+ const printOrderRow = ref(null);
+ const printMaterialList = ref([]);
+ const handlePrint = async row => {
+ printOrderRow.value = row;
+ proxy.$modal.loading("姝e湪鑾峰彇棰嗘枡鏁版嵁...");
+ try {
+ printMaterialList.value = [];
+ const detailRes = await listMaterialPickingDetail(row.id);
+ const detailList = Array.isArray(detailRes?.data)
+ ? detailRes.data
+ : detailRes?.data?.records || [];
+
+ if (detailList.length > 0) {
+ printMaterialList.value = detailList;
+ }
+
+ if (printMaterialList.value.length === 0) {
+ proxy.$modal.msgWarning("鏆傛棤棰嗘枡鏁版嵁");
+ return;
+ }
+
+ // 绛夊緟 DOM 鏇存柊鍚庢墽琛屾墦鍗�
+ proxy.$nextTick(() => {
+ setTimeout(() => {
+ window.print();
+ }, 800);
+ });
+ } catch (e) {
+ console.error("鑾峰彇棰嗘枡鏁版嵁澶辫触锛�", e);
+ proxy.$modal.msgError("鑾峰彇棰嗘枡鏁版嵁澶辫触");
+ } finally {
+ proxy.$modal.closeLoading();
+ }
+ };
const openBindRouteDialog = async (row, type) => {
bindForm.orderId = row.id;
@@ -478,6 +549,11 @@
materialDetailDialogVisible.value = true;
};
+ const openMaterialSupplementDialog = row => {
+ currentMaterialSupplementOrder.value = row;
+ materialSupplementDialogVisible.value = true;
+ };
+
const handleReset = () => {
searchForm.value = {
...searchForm.value,
diff --git a/src/views/productionManagement/productionProcess/Edit.vue b/src/views/productionManagement/productionProcess/Edit.vue
index e69de29..28077b6 100644
--- a/src/views/productionManagement/productionProcess/Edit.vue
+++ b/src/views/productionManagement/productionProcess/Edit.vue
@@ -0,0 +1,168 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ title="缂栬緫宸ュ簭"
+ width="400"
+ @close="closeModal"
+ >
+ <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
+ <el-form-item
+ label="宸ュ簭鍚嶇О锛�"
+ prop="name"
+ :rules="[
+ {
+ required: true,
+ message: '璇疯緭鍏ュ伐搴忓悕绉�',
+ },
+ {
+ max: 100,
+ message: '鏈�澶�100涓瓧绗�',
+ }
+ ]">
+ <el-input v-model="formState.name" />
+ </el-form-item>
+ <el-form-item label="宸ュ簭缂栧彿" prop="no">
+ <el-input v-model="formState.no" />
+ </el-form-item>
+ <el-form-item
+ label="宸ュ簭绫诲瀷"
+ prop="type"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨宸ュ簭绫诲瀷',
+ }
+ ]"
+ >
+ <el-select v-model="formState.type" placeholder="璇烽�夋嫨宸ュ簭绫诲瀷">
+ <el-option label="璁℃椂" :value="0" />
+ <el-option label="璁′欢" :value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="宸ヨ祫瀹氶" prop="salaryQuota">
+ <el-input v-model="formState.salaryQuota" type="number" :step="0.001" />
+ </el-form-item>
+ <el-form-item label="鏄惁璐ㄦ" prop="isQuality">
+ <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="鏄惁鍏ュ簱" prop="inbound">
+ <el-switch v-model="formState.inbound" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="鏄惁鎶ュ伐" prop="reportWork">
+ <el-switch v-model="formState.reportWork" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formState.remark" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, getCurrentInstance, watch } from "vue";
+import {update} from "@/api/productionManagement/productionProcess.js";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+
+ record: {
+ type: Object,
+ required: true,
+ }
+});
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+// 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
+const formState = ref({
+ id: props.record.id,
+ name: props.record.name,
+ type: props.record.type,
+ no: props.record.no,
+ remark: props.record.remark,
+ salaryQuota: props.record.salaryQuota,
+ isQuality: props.record.isQuality,
+ inbound: props.record.inbound,
+ reportWork: props.record.reportWork,
+});
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+// 鐩戝惉 record 鍙樺寲锛屾洿鏂拌〃鍗曟暟鎹�
+watch(() => props.record, (newRecord) => {
+ if (newRecord && isShow.value) {
+ formState.value = {
+ id: newRecord.id,
+ name: newRecord.name || '',
+ no: newRecord.no || '',
+ type: newRecord.type,
+ remark: newRecord.remark || '',
+ salaryQuota: newRecord.salaryQuota || '',
+ isQuality: props.record.isQuality,
+ inbound: newRecord.inbound,
+ reportWork: newRecord.reportWork,
+ };
+ }
+}, { immediate: true, deep: true });
+
+// 鐩戝惉寮圭獥鎵撳紑锛岄噸鏂板垵濮嬪寲琛ㄥ崟鏁版嵁
+watch(() => props.visible, (visible) => {
+ if (visible && props.record) {
+ formState.value = {
+ id: props.record.id,
+ name: props.record.name || '',
+ no: props.record.no || '',
+ type: props.record.type,
+ remark: props.record.remark || '',
+ salaryQuota: props.record.salaryQuota || '',
+ isQuality: props.record.isQuality,
+ inbound: props.record.inbound,
+ reportWork: props.record.reportWork,
+ };
+ }
+});
+
+let { proxy } = getCurrentInstance()
+
+const closeModal = () => {
+ isShow.value = false;
+};
+
+const handleSubmit = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ update(formState.value).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ })
+ }
+ })
+};
+
+defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow,
+});
+</script>
diff --git a/src/views/productionManagement/productionProcess/New.vue b/src/views/productionManagement/productionProcess/New.vue
index e69de29..0b3fd47 100644
--- a/src/views/productionManagement/productionProcess/New.vue
+++ b/src/views/productionManagement/productionProcess/New.vue
@@ -0,0 +1,129 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ title="鏂板宸ュ簭"
+ width="400"
+ @close="closeModal"
+ >
+ <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
+ <el-form-item
+ label="宸ュ簭鍚嶇О锛�"
+ prop="name"
+ :rules="[
+ {
+ required: true,
+ message: '璇疯緭鍏ュ伐搴忓悕绉�',
+ },
+ {
+ max: 100,
+ message: '鏈�澶�100涓瓧绗�',
+ }
+ ]">
+ <el-input v-model="formState.name" />
+ </el-form-item>
+ <el-form-item label="宸ュ簭缂栧彿" prop="no">
+ <el-input v-model="formState.no" />
+ </el-form-item>
+ <el-form-item
+ label="宸ュ簭绫诲瀷"
+ prop="type"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨宸ュ簭绫诲瀷',
+ }
+ ]"
+ >
+ <el-select v-model="formState.type" placeholder="璇烽�夋嫨宸ュ簭绫诲瀷">
+ <el-option label="璁℃椂" :value="0" />
+ <el-option label="璁′欢" :value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="宸ヨ祫瀹氶" prop="salaryQuota">
+ <el-input v-model="formState.salaryQuota" type="number" :step="0.001">
+ <template #append>鍏�</template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="鏄惁璐ㄦ" prop="isQuality">
+ <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="鏄惁鍏ュ簱" prop="inbound">
+ <el-switch v-model="formState.inbound" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="鏄惁鎶ュ伐" prop="reportWork">
+ <el-switch v-model="formState.reportWork" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formState.remark" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, getCurrentInstance } from "vue";
+import {add} from "@/api/productionManagement/productionProcess.js";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+});
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+// 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
+const formState = ref({
+ name: '',
+ type: undefined,
+ remark: '',
+ salaryQuota: '',
+ isQuality: false,
+ inbound: false,
+ reportWork: false,
+});
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+let { proxy } = getCurrentInstance()
+
+const closeModal = () => {
+ isShow.value = false;
+};
+
+const handleSubmit = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ add(formState.value).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ })
+ }
+ })
+};
+
+defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow,
+});
+</script>
diff --git a/src/views/productionManagement/productionReporting/index.vue b/src/views/productionManagement/productionReporting/index.vue
index 8490b85..aff050f 100644
--- a/src/views/productionManagement/productionReporting/index.vue
+++ b/src/views/productionManagement/productionReporting/index.vue
@@ -99,8 +99,7 @@
style="width: 100%" />
</template>
</el-table-column>
- <el-table-column label="鎿嶄綔"
- >
+ <el-table-column label="鎿嶄綔">
<template #default="scope">
<el-button link
type="primary"
@@ -124,11 +123,36 @@
<input-modal v-if="isShowInput"
v-model:visible="isShowInput"
:production-product-main-id="isShowingId" />
+ <!-- 鍙傛暟璇︽儏寮圭獥 -->
+ <el-dialog v-model="paramDetailVisible"
+ title="鍙傛暟璇︽儏"
+ width="600px">
+ <div v-if="currentParams && currentParams.length > 0"
+ class="param-detail-list">
+ <el-descriptions :column="1"
+ border>
+ <el-descriptions-item v-for="param in currentParams"
+ :key="param.id"
+ :label="param.paramName">
+ {{ param.inputValue }}
+ <span v-if="param.unit && param.unit !== '/'"
+ class="unit-text">({{ param.unit }})</span>
+ </el-descriptions-item>
+ </el-descriptions>
+ </div>
+ <el-empty v-else
+ description="鏆傛棤鍙傛暟鏁版嵁" />
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="paramDetailVisible = false">鍏抽棴</el-button>
+ </span>
+ </template>
+ </el-dialog>
</div>
</template>
<script setup>
- import { onMounted, ref } from "vue";
+ import { onMounted, ref, reactive, toRefs, getCurrentInstance } from "vue";
import FormDia from "@/views/productionManagement/productionReporting/components/formDia.vue";
import { ElMessageBox } from "element-plus";
import {
@@ -202,7 +226,7 @@
prop: "unit",
width: 120,
},
-
+
{
label: "鍒涘缓鏃堕棿",
prop: "createTime",
@@ -213,12 +237,20 @@
label: "鎿嶄綔",
align: "center",
fixed: "right",
+ width: 250,
operation: [
{
name: "鏌ョ湅鎶曞叆",
type: "text",
clickFun: row => {
showInput(row);
+ },
+ },
+ {
+ name: "鍙傛暟璇︽儏",
+ type: "text",
+ clickFun: row => {
+ showParamDetail(row);
},
},
{
@@ -232,6 +264,13 @@
},
]);
const tableData = ref([]);
+ const paramDetailVisible = ref(false);
+ const currentParams = ref([]);
+
+ const showParamDetail = row => {
+ currentParams.value = row.productionOperationParamList || [];
+ paramDetailVisible.value = true;
+ };
const selectedRows = ref([]);
const tableLoading = ref(false);
const childrenLoading = ref(false);
@@ -418,7 +457,15 @@
</script>
<style scoped>
-.table_list {
- margin-top: unset;
-}
+ .unit-text {
+ margin-left: 5px;
+ color: #909399;
+ font-size: 12px;
+ }
+ .param-detail-list {
+ padding: 10px;
+ }
+ .table_list {
+ margin-top: unset;
+ }
</style>
diff --git a/src/views/productionManagement/workOrderEdit/index.vue b/src/views/productionManagement/workOrderEdit/index.vue
index 52a36cd..b919973 100644
--- a/src/views/productionManagement/workOrderEdit/index.vue
+++ b/src/views/productionManagement/workOrderEdit/index.vue
@@ -4,74 +4,66 @@
<div class="search-row">
<div class="search-item">
<span class="search_title">宸ュ崟缂栧彿锛�</span>
- <el-input
- v-model="searchForm.workOrderNo"
- style="width: 240px"
- placeholder="璇疯緭鍏�"
- @change="handleQuery"
- clearable
- prefix-icon="Search"
- />
+ <el-input v-model="searchForm.workOrderNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ prefix-icon="Search" />
</div>
<div class="search-item">
<span class="search_title">鐢熶骇璁㈠崟鍙凤細</span>
- <el-input
- v-model="searchForm.productOrderNpsNo"
- style="width: 240px"
- placeholder="璇疯緭鍏�"
- @change="handleQuery"
- clearable
- prefix-icon="Search"
- />
+ <el-input v-model="searchForm.productOrderNpsNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ prefix-icon="Search" />
</div>
<div class="search-item">
- <el-button type="primary" @click="handleQuery">鎼滅储</el-button>
+ <el-button type="primary"
+ @click="handleQuery">鎼滅储</el-button>
</div>
</div>
</div>
<div class="table_list">
- <PIMTable
- rowKey="id"
- :column="tableColumn"
- :tableData="tableData"
- :page="page"
- :tableLoading="tableLoading"
- @pagination="pagination"
- >
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :tableLoading="tableLoading"
+ @pagination="pagination">
<template #completionStatus="{ row }">
- <el-progress
- :percentage="toProgressPercentage(row?.completionStatus)"
- :color="progressColor(toProgressPercentage(row?.completionStatus))"
- :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''"
- />
+ <el-progress :percentage="toProgressPercentage(row?.completionStatus)"
+ :color="progressColor(toProgressPercentage(row?.completionStatus))"
+ :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" />
</template>
</PIMTable>
</div>
-
- <el-dialog v-model="editDialogVisible" title="缂栬緫璁″垝鏃堕棿" width="500px">
- <el-form :model="editrow" label-width="120px">
+ <el-dialog v-model="editDialogVisible"
+ title="缂栬緫璁″垝鏃堕棿"
+ width="500px">
+ <el-form :model="editrow"
+ label-width="120px">
<el-form-item label="璁″垝寮�濮嬫椂闂�">
- <el-date-picker
- v-model="editrow.planStartTime"
- type="date"
- placeholder="璇烽�夋嫨"
- value-format="YYYY-MM-DD"
- style="width: 300px"
- />
+ <el-date-picker v-model="editrow.planStartTime"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ value-format="YYYY-MM-DD"
+ style="width: 300px" />
</el-form-item>
<el-form-item label="璁″垝缁撴潫鏃堕棿">
- <el-date-picker
- v-model="editrow.planEndTime"
- type="date"
- placeholder="璇烽�夋嫨"
- value-format="YYYY-MM-DD"
- style="width: 300px"
- />
+ <el-date-picker v-model="editrow.planEndTime"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ value-format="YYYY-MM-DD"
+ style="width: 300px" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
- <el-button type="primary" @click="handleUpdate">纭畾</el-button>
+ <el-button type="primary"
+ @click="handleUpdate">纭畾</el-button>
<el-button @click="editDialogVisible = false">鍙栨秷</el-button>
</span>
</template>
@@ -80,200 +72,201 @@
</template>
<script setup>
-import { getCurrentInstance, onMounted, reactive, ref, toRefs } from "vue";
-import { ElMessageBox } from "element-plus";
-import {
- productWorkOrderPage,
- updateProductWorkOrder,
-} from "@/api/productionManagement/workOrder.js";
+ import { getCurrentInstance, onMounted, reactive, ref, toRefs } from "vue";
+ import { ElMessageBox } from "element-plus";
+ import {
+ productWorkOrderPage,
+ updateProductWorkOrder,
+ } from "@/api/productionManagement/workOrder.js";
-const { proxy } = getCurrentInstance();
+ const { proxy } = getCurrentInstance();
-const tableColumn = ref([
- {
- label: "宸ュ崟绫诲瀷",
- prop: "workOrderType",
- width: "80",
- },
- {
- label: "宸ュ崟缂栧彿",
- prop: "workOrderNo",
- width: "140",
- },
- {
- label: "鐢熶骇璁㈠崟鍙�",
- prop: "productOrderNpsNo",
- width: "140",
- },
- {
- label: "浜у搧鍚嶇О",
- prop: "productName",
- width: "140",
- },
- {
- label: "瑙勬牸",
- prop: "model",
- },
- {
- label: "鍗曚綅",
- prop: "unit",
- },
- {
- label: "宸ュ簭鍚嶇О",
- prop: "processName",
- },
- {
- label: "闇�姹傛暟閲�",
- prop: "planQuantity",
- width: "140",
- },
- {
- label: "瀹屾垚鏁伴噺",
- prop: "completeQuantity",
- width: "140",
- },
- {
- label: "瀹屾垚杩涘害",
- prop: "completionStatus",
- dataType: "slot",
- slot: "completionStatus",
- width: "140",
- },
- {
- label: "璁″垝寮�濮嬫椂闂�",
- prop: "planStartTime",
- width: "140",
- },
- {
- label: "璁″垝缁撴潫鏃堕棿",
- prop: "planEndTime",
- width: "140",
- },
- {
- label: "瀹為檯寮�濮嬫椂闂�",
- prop: "actualStartTime",
- width: "140",
- },
- {
- label: "瀹為檯缁撴潫鏃堕棿",
- prop: "actualEndTime",
- width: "140",
- },
- {
- label: "鎿嶄綔",
- width: "100",
- align: "center",
- dataType: "action",
- fixed: "right",
- operation: [
- {
- name: "璁″垝鏃堕棿",
- clickFun: row => {
- handleEdit(row);
+ const tableColumn = ref([
+ {
+ label: "宸ュ崟绫诲瀷",
+ prop: "workOrderType",
+ width: "80",
+ },
+ {
+ label: "宸ュ崟缂栧彿",
+ prop: "workOrderNo",
+ width: "140",
+ },
+ {
+ label: "鐢熶骇璁㈠崟鍙�",
+ prop: "npsNo",
+ width: "140",
+ },
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+ width: "140",
+ },
+ {
+ label: "瑙勬牸",
+ prop: "model",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "宸ュ簭鍚嶇О",
+ prop: "operationName",
+ width: "100",
+ },
+ {
+ label: "闇�姹傛暟閲�",
+ prop: "planQuantity",
+ width: "140",
+ },
+ {
+ label: "瀹屾垚鏁伴噺",
+ prop: "completeQuantity",
+ width: "140",
+ },
+ {
+ label: "瀹屾垚杩涘害",
+ prop: "completionStatus",
+ dataType: "slot",
+ slot: "completionStatus",
+ width: "140",
+ },
+ {
+ label: "璁″垝寮�濮嬫椂闂�",
+ prop: "planStartTime",
+ width: "140",
+ },
+ {
+ label: "璁″垝缁撴潫鏃堕棿",
+ prop: "planEndTime",
+ width: "140",
+ },
+ {
+ label: "瀹為檯寮�濮嬫椂闂�",
+ prop: "actualStartTime",
+ width: "140",
+ },
+ {
+ label: "瀹為檯缁撴潫鏃堕棿",
+ prop: "actualEndTime",
+ width: "140",
+ },
+ {
+ label: "鎿嶄綔",
+ width: "100",
+ align: "center",
+ dataType: "action",
+ fixed: "right",
+ operation: [
+ {
+ name: "璁″垝鏃堕棿",
+ clickFun: row => {
+ handleEdit(row);
+ },
},
- },
- ],
- },
-]);
+ ],
+ },
+ ]);
-const tableData = ref([]);
-const tableLoading = ref(false);
-const editDialogVisible = ref(false);
-const editrow = ref(null);
-const page = reactive({
- current: 1,
- size: 100,
- total: 0,
-});
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const editDialogVisible = ref(false);
+ const editrow = ref(null);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
-const data = reactive({
- searchForm: {
- workOrderNo: "",
- productOrderNpsNo: "",
- },
-});
-const { searchForm } = toRefs(data);
+ const data = reactive({
+ searchForm: {
+ workOrderNo: "",
+ productOrderNpsNo: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
-const toProgressPercentage = val => {
- const n = Number(val);
- if (!Number.isFinite(n)) return 0;
- if (n <= 0) return 0;
- if (n >= 100) return 100;
- return Math.round(n);
-};
+ const toProgressPercentage = val => {
+ const n = Number(val);
+ if (!Number.isFinite(n)) return 0;
+ if (n <= 0) return 0;
+ if (n >= 100) return 100;
+ return Math.round(n);
+ };
-const progressColor = percentage => {
- const p = toProgressPercentage(percentage);
- if (p < 30) return "#f56c6c";
- if (p < 50) return "#e6a23c";
- if (p < 80) return "#409eff";
- return "#67c23a";
-};
+ const progressColor = percentage => {
+ const p = toProgressPercentage(percentage);
+ if (p < 30) return "#f56c6c";
+ if (p < 50) return "#e6a23c";
+ if (p < 80) return "#409eff";
+ return "#67c23a";
+ };
-const handleQuery = () => {
- page.current = 1;
- getList();
-};
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
-const pagination = obj => {
- page.current = obj.page;
- page.size = obj.limit;
- getList();
-};
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
-const getList = () => {
- tableLoading.value = true;
- const params = { ...searchForm.value, ...page };
- productWorkOrderPage(params)
- .then(res => {
- tableLoading.value = false;
- tableData.value = res.data.records;
- page.total = res.data.total;
- })
- .catch(() => {
- tableLoading.value = false;
- });
-};
-
-const handleEdit = row => {
- editrow.value = JSON.parse(JSON.stringify(row));
- editDialogVisible.value = true;
-};
-
-const handleUpdate = () => {
- updateProductWorkOrder(editrow.value)
- .then(() => {
- proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
- editDialogVisible.value = false;
- getList();
- })
- .catch(() => {
- ElMessageBox.alert("淇敼澶辫触", "鎻愮ず", {
- confirmButtonText: "纭畾",
+ const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ productWorkOrderPage(params)
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+ .catch(() => {
+ tableLoading.value = false;
});
- });
-};
+ };
-onMounted(() => {
- getList();
-});
+ const handleEdit = row => {
+ editrow.value = JSON.parse(JSON.stringify(row));
+ editDialogVisible.value = true;
+ };
+
+ const handleUpdate = () => {
+ updateProductWorkOrder(editrow.value)
+ .then(() => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ editDialogVisible.value = false;
+ getList();
+ })
+ .catch(() => {
+ ElMessageBox.alert("淇敼澶辫触", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ });
+ };
+
+ onMounted(() => {
+ getList();
+ });
</script>
<style scoped lang="scss">
-.search-row {
- display: flex;
- align-items: center;
- gap: 12px;
-}
+ .search-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
-.search-item {
- display: flex;
- align-items: center;
-}
+ .search-item {
+ display: flex;
+ align-items: center;
+ }
-.search_title {
- margin-right: 8px;
- font-size: 14px;
- color: #606266;
-}
+ .search_title {
+ margin-right: 8px;
+ font-size: 14px;
+ color: #606266;
+ }
</style>
diff --git a/src/views/productionManagement/workOrderManagement/index.vue b/src/views/productionManagement/workOrderManagement/index.vue
index 48f8839..95fb703 100644
--- a/src/views/productionManagement/workOrderManagement/index.vue
+++ b/src/views/productionManagement/workOrderManagement/index.vue
@@ -40,7 +40,6 @@
</template>
</PIMTable>
</div>
-
<!-- 娴佽浆鍗″脊绐� -->
<el-dialog v-model="transferCardVisible"
title="娴佽浆鍗�"
@@ -116,7 +115,6 @@
@click="printTransferCard">鎵撳嵃娴佽浆鍗�</el-button>
</div>
</el-dialog>
-
<!-- 鎶ュ伐寮圭獥 -->
<el-dialog v-model="reportDialogVisible"
title="鎶ュ伐"
@@ -163,6 +161,75 @@
:value="user.userId" />
</el-select>
</el-form-item>
+ <div v-if="params.length > 0"
+ class="param-grid"
+ v-loading="paramLoading">
+ <el-form-item v-for="param in params"
+ :key="param.id"
+ :label="param.paramName"
+ :label-width="120"
+ class="param-item">
+ <template v-if="param.paramType == '1'">
+ <div class="param-input-group">
+ <el-input-number v-model="reportForm.paramGroups[param.id]"
+ controls-position="right"
+ :key="param.id"
+ style="width: 250px"
+ class="param-input" />
+ <span v-if="param.unit && param.unit != '/'"
+ class="param-unit">{{ param.unit }}</span>
+ </div>
+ </template>
+ <template v-else-if="param.paramType == '2'">
+ <div class="param-input-group">
+ <el-input v-model="reportForm.paramGroups[param.id]"
+ :key="param.id"
+ style="width: 250px"
+ class="param-input" />
+ <span v-if="param.unit && param.unit != '/'"
+ class="param-unit">{{ param.unit }}</span>
+ </div>
+ </template>
+ <template v-else-if="param.paramType == '3'">
+ <div class="param-input-group">
+ <el-select v-model="reportForm.paramGroups[param.id]"
+ placeholder="璇烽�夋嫨"
+ :key="param.id"
+ class="param-select"
+ style="width: 250px">
+ <el-option v-for="option in dictOptions[param.paramFormat] || []"
+ :key="option.dictLabel"
+ :label="option.dictLabel"
+ :value="option.dictLabel" />
+ </el-select>
+ <span v-if="param.unit && param.unit != '/'"
+ class="param-unit">{{ param.unit }}</span>
+ </div>
+ </template>
+ <template v-else-if="param.paramType == '4'">
+ <div class="param-input-group">
+ <el-date-picker :value-format="param.paramFormat.replace('yyyy', 'YYYY').replace('dd', 'DD')"
+ :format="param.paramFormat.replace('yyyy', 'YYYY').replace('dd', 'DD')"
+ :key="param.id"
+ :type="param.paramFormat=='yyyy-MM-dd'?'date':'datetime'"
+ v-model="reportForm.paramGroups[param.id]"
+ class="param-input"
+ style="width: 250px" />
+ <span v-if="param.unit && param.unit != '/'"
+ class="param-unit">{{ param.unit }}</span>
+ </div>
+ </template>
+ <template v-else>
+ <div class="param-input-group">
+ <el-input v-model="reportForm.paramGroups[param.id]"
+ :key="param.id"
+ class="param-input" />
+ <span v-if="param.unit && param.unit != '/'"
+ class="param-unit">{{ param.unit }}</span>
+ </div>
+ </template>
+ </el-form-item>
+ </div>
</el-form>
<template #footer>
<span class="dialog-footer">
@@ -172,13 +239,9 @@
</span>
</template>
</el-dialog>
-
- <MaterialDialog
- v-model="materialDialogVisible"
- :row-data="currentMaterialOrderRow"
- @refresh="getList"
- />
-
+ <MaterialDialog v-model="materialDialogVisible"
+ :row-data="currentMaterialOrderRow"
+ @refresh="getList" />
<FilesDia ref="workOrderFilesRef" />
</div>
</template>
@@ -192,7 +255,9 @@
addProductMain,
downProductWorkOrder,
} from "@/api/productionManagement/workOrder.js";
+ import { findProcessParamListOrder } from "@/api/productionManagement/productProcessRoute.js";
import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js";
+ import { getDicts } from "@/api/system/dict/data";
import QRCode from "qrcode";
import { getCurrentInstance, reactive, toRefs } from "vue";
import FilesDia from "./components/filesDia.vue";
@@ -212,7 +277,7 @@
},
{
label: "鐢熶骇璁㈠崟鍙�",
- prop: "productOrderNpsNo",
+ prop: "npsNo",
width: "140",
},
{
@@ -230,7 +295,8 @@
},
{
label: "宸ュ簭鍚嶇О",
- prop: "processName",
+ prop: "operationName",
+ width: "100",
},
{
label: "闇�姹傛暟閲�",
@@ -288,12 +354,12 @@
openWorkOrderFiles(row);
},
},
- {
- name: "鐗╂枡",
- clickFun: row => {
- openMaterialDialog(row);
- },
- },
+ // {
+ // name: "鐗╂枡",
+ // clickFun: row => {
+ // openMaterialDialog(row);
+ // },
+ // },
{
name: "鎶ュ伐",
clickFun: row => {
@@ -304,7 +370,7 @@
],
},
]);
-
+
const tableData = ref([]);
const tableLoading = ref(false);
const transferCardVisible = ref(false);
@@ -325,7 +391,14 @@
productProcessRouteItemId: "",
userId: "",
productMainId: null,
+ productionOrderRoutingOperationId: "",
+ productionOrderId: "",
+ paramGroups: {},
});
+
+ const params = ref({});
+ const dictOptions = ref({});
+ const paramLoading = ref(false);
// 鏈鐢熶骇鏁伴噺楠岃瘉瑙勫垯
const validateQuantity = (rule, value, callback) => {
@@ -416,7 +489,7 @@
// 鏈夋晥鐨勯潪璐熸暣鏁帮紙鍖呮嫭0锛�
reportForm.scrapQty = num;
};
-
+
const currentReportRowData = ref(null);
const materialDialogVisible = ref(false);
const currentMaterialOrderRow = ref(null);
@@ -454,13 +527,13 @@
page.current = 1;
getList();
};
-
+
const pagination = obj => {
page.current = obj.page;
page.size = obj.limit;
getList();
};
-
+
const getList = () => {
tableLoading.value = true;
const params = { ...searchForm.value, ...page };
@@ -554,10 +627,15 @@
reportForm.productMainId = row.productMainId;
reportForm.scrapQty =
row.scrapQty !== undefined && row.scrapQty !== null ? row.scrapQty : null;
+ reportForm.productionOrderRoutingOperationId =
+ row.productionOrderRoutingOperationId;
+ reportForm.productionOrderId = row.productionOrderId;
nextTick(() => {
reportFormRef.value?.clearValidate();
+ if (row.productionOrderRoutingOperationId && row.productionOrderId) {
+ loadParams(row.productionOrderRoutingOperationId, row.productionOrderId);
+ }
});
- // 鑾峰彇褰撳墠鐧诲綍鐢ㄦ埛淇℃伅锛岃缃负榛樿閫変腑
getUserProfile()
.then(res => {
if (res.code === 200) {
@@ -634,18 +712,27 @@
return;
}
- const params = {
+ const productionOperationParamList = params.value.map(param => ({
+ ...param,
+ inputValue: reportForm.paramGroups[param.id] ?? "",
+ }));
+
+ const submitParams = {
quantity: quantity,
scrapQty: isNaN(scrapQty) ? 0 : scrapQty,
userId: reportForm.userId,
userName: reportForm.userName,
- workOrderId: reportForm.workOrderId,
+ productionOperationTaskId: reportForm.workOrderId,
productProcessRouteItemId: reportForm.productProcessRouteItemId,
reportWork: reportForm.reportWork,
productMainId: reportForm.productMainId,
+ productionOrderRoutingOperationId:
+ reportForm.productionOrderRoutingOperationId,
+ productionOrderId: reportForm.productionOrderId,
+ productionOperationParamList: productionOperationParamList,
};
- addProductMain(params)
+ addProductMain(submitParams)
.then(res => {
proxy.$modal.msgSuccess("鎶ュ伐鎴愬姛");
reportDialogVisible.value = false;
@@ -662,6 +749,51 @@
const handleUserChange = val => {
const user = userOptions.value.find(item => item.userId === val);
reportForm.userName = user ? user.nickName : "";
+ };
+
+ const getDictOptions = async dictType => {
+ if (!dictType) return [];
+ if (dictOptions.value[dictType]) return dictOptions.value[dictType];
+ try {
+ const res = await getDicts(dictType);
+ if (res.code === 200) {
+ dictOptions.value[dictType] = res.data;
+ return res.data;
+ }
+ return [];
+ } catch (error) {
+ console.error("鑾峰彇瀛楀吀鏁版嵁澶辫触:", error);
+ return [];
+ }
+ };
+
+ const loadParams = (productionOrderRoutingOperationId, productionOrderId) => {
+ paramLoading.value = true;
+ findProcessParamListOrder({
+ productionOrderRoutingOperationId,
+ productionOrderId,
+ })
+ .then(res => {
+ if (res.code === 200) {
+ const paramList = res.data || [];
+ params.value = paramList;
+ reportForm.paramGroups = {};
+ paramList.forEach(param => {
+ if (!reportForm.paramGroups[param.id]) {
+ reportForm.paramGroups[param.id] = "";
+ }
+ if (param.paramType == "3" && param.paramFormat) {
+ getDictOptions(param.paramFormat);
+ }
+ });
+ }
+ })
+ .catch(err => {
+ console.error("鑾峰彇宸ュ簭鍙傛暟澶辫触:", err);
+ })
+ .finally(() => {
+ paramLoading.value = false;
+ });
};
onMounted(() => {
@@ -734,6 +866,30 @@
text-align: center;
margin-top: 20px;
}
+ .param-grid {
+ margin-top: 10px;
+ border-top: 1px solid #ebe9f3;
+ padding-top: 10px;
+ }
+ .param-item {
+ margin-bottom: 12px;
+ }
+ .param-input-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+ .param-input {
+ flex: 1;
+ }
+ .param-select {
+ flex: 1;
+ }
+ .param-unit {
+ color: #909399;
+ font-size: 12px;
+ min-width: 30px;
+ }
</style>
<style lang="scss">
diff --git a/vite.config.js b/vite.config.js
index dc687a8..ac18ec5 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -8,7 +8,7 @@
const { VITE_APP_ENV } = env;
const baseUrl =
env.VITE_APP_ENV === "development"
- ? "http://1.15.17.182:9003"
+ ? "http://localhost:7005"
: env.VITE_BASE_API;
const javaUrl =
env.VITE_APP_ENV === "development"
--
Gitblit v1.9.3