Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_NEW_pro
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // é件页颿¥å£ |
| | | 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 |
| | | }) |
| | | } |
| | |
| | | // ç产订å-è¡¥æè®°å½å表 |
| | | export function listMaterialSupplementRecord(query) { |
| | | return request({ |
| | | url: "/productOrderMaterial/supplementRecord", |
| | | url: "/productionOrderPickRecord/feeding", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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 }}åæè§£è¯»å©æ |
| | | </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: 'æå¯ä»¥åçä½ çé®é¢ï¼ä¸ºä½ æä¾ä¸å¡æ°æ®è§£è¯»ä¿¡æ¯ãå¤ç建议åè¾
å©å³çæ¯æã', |
| | | 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 || [] |
| | | |
| | | // éæ°æé æ¶æ¯å表并解æ |
| | | 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 |
| | | } |
| | | |
| | | // è§£æå岿¶æ¯ä¸ç 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 |
| | | |
| | | // è§£æ 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 |
| | | // æ¯æ¬¡è§£ææåé½å°è¯æ¸²æ/æ´æ°å¾è¡¨ï¼ä»¥æ¯ææµå¼æ´æ° |
| | | 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"') |
| | | |
| | | // 妿è¿å¨ä»£ç åä¸ä¸æªç»æï¼æ¾ç¤ºæç¤ºæå |
| | | 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 || 'æ£å¨åææ°æ®å¹¶çæå¾è¡¨...' |
| | | 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 = 'æ£å¨ä¸ºæ¨å±ç¤ºåæç»æ...' |
| | | } |
| | | } |
| | | } 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) |
| | | // æåè§£æï¼ç§»é¤ 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 = 'æ£å¨ä¸ºæ¨å±ç¤ºåæç»æ...' |
| | | } |
| | | } |
| | | } catch (e) { |
| | | // è§£æå¤±è´¥ï¼è¯´æ JSON è¿å¨ä¼ è¾ä¸ææ ¼å¼ä¸æ£ç¡® |
| | | display = output.substring(0, startIdx).trim() || 'æ£å¨åææ°æ®å¹¶çæå¾è¡¨...' |
| | | } |
| | | } else { |
| | | // æ¾å°äºå¼å§ä½è¿æ²¡æ¾å°ç»æ |
| | | display = output.substring(0, startIdx).trim() || 'æ£å¨åææ°æ®å¹¶çæå¾è¡¨...' |
| | | } |
| | | } |
| | | |
| | | let html = convertTextToHtml(display) |
| | | |
| | | // è¿å代ç å |
| | | 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | </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> |
| | |
| | | </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, |
| | |
| | | |
| | | const userStore = useUserStore(); |
| | | const deviceOptions = ref([]); |
| | | const fileList = ref([]); |
| | | |
| | | const loadDeviceName = async () => { |
| | | const { data } = await getDeviceLedger(); |
| | |
| | | remark: undefined, // æ
éç°è±¡ |
| | | status: 0, // æ¥ä¿®ç¶æ |
| | | machineryCategory: undefined, |
| | | storageBlobDTOs: [], |
| | | }); |
| | | |
| | | const setDeviceModel = (deviceId) => { |
| | |
| | | form.remark = data.remark; |
| | | form.status = data.status; |
| | | form.machineryCategory = data.machineryCategory; |
| | | form.storageBlobDTOs = data.storageBlobVOs || []; |
| | | }; |
| | | |
| | | const sendForm = async () => { |
| | |
| | | const openAdd = async () => { |
| | | id.value = undefined; |
| | | visible.value = true; |
| | | fileList.value = []; |
| | | await nextTick(); |
| | | await loadDeviceName(); |
| | | }; |
| | |
| | | > |
| | | å é¤ |
| | | </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: "è®¾å¤æ¥ä¿®", |
| | |
| | | prop: "deviceModel", |
| | | }, |
| | | { |
| | | label: "ç±»ç®", |
| | | label: "项ç®", |
| | | align: "center", |
| | | prop: "machineryCategory", |
| | | }, |
| | |
| | | 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; |
| | |
| | | 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="å½å
¥äºº"> |
| | |
| | | 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> |
| | |
| | | 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: "设å¤ä¿å
»æ°å¢è®¡å", |
| | |
| | | createUser: undefined, // å½å
¥äºº |
| | | status: 0, //ä¿ä¿®ç¶æ |
| | | machineryCategory: undefined, |
| | | storageBlobDTOs: [], |
| | | }); |
| | | |
| | | const setDeviceModel = (deviceId) => { |
| | |
| | | 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 || []; |
| | | }; |
| | | |
| | | // ç¨æ·å表 |
| | |
| | | <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') |
| | |
| | | prop: "createUserName", |
| | | }, |
| | | { |
| | | label: "ç±»ç®", |
| | | label: "项ç®", |
| | | align: "center", |
| | | prop: "machineryCategory", |
| | | }, |
| | |
| | | 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(() => { |
| | |
| | | <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"> |
| | |
| | | border |
| | | row-key="id"> |
| | | <el-table-column label="è¡¥ææ°é" |
| | | prop="supplementQty" |
| | | prop="pickQuantity" |
| | | min-width="120" /> |
| | | <el-table-column label="è¡¥æäºº" |
| | | prop="supplementUserName" |
| | |
| | | prop="supplementTime" |
| | | min-width="160" /> |
| | | <el-table-column label="è¡¥æåå " |
| | | prop="supplementReason" |
| | | prop="feedingReason" |
| | | min-width="200" /> |
| | | </el-table> |
| | | <template #footer> |
| | |
| | | import { |
| | | listMaterialPickingDetail, |
| | | listMaterialSupplementRecord, |
| | | confirmMaterialReturn, |
| | | updateMaterialPickingLedger, |
| | | } from "@/api/productionManagement/productionOrder.js"; |
| | | |
| | | const props = defineProps({ |
| | |
| | | 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 () => { |
| | |
| | | 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; |
| | | } |
| | |
| | | materialDetailTableData.value = []; |
| | | }; |
| | | |
| | | const handleActualQtyChange = (row, val) => { |
| | | row.returnQty = calcReturnQty(row); |
| | | }; |
| | | |
| | | const handleViewSupplementRecord = async row => { |
| | | if (!row?.id) return; |
| | | supplementRecordDialogVisible.value = true; |
| | |
| | | supplementRecordTableData.value = []; |
| | | try { |
| | | const res = await listMaterialSupplementRecord({ |
| | | materialDetailId: row.id, |
| | | pickId: row.id, |
| | | productionOrderId: props.orderRow.id, |
| | | }); |
| | | supplementRecordTableData.value = res.data || []; |
| | | } finally { |
| | |
| | | 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; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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å¹´MMæDDæ¥") : "-"; |
| | | }; |
| | | </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> |
| | |
| | | <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> |
| | | |
| | |
| | | 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") |
| | | ); |
| | |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 360, |
| | | width: 260, |
| | | operation: [ |
| | | { |
| | | name: "å·¥èºè·¯çº¿", |
| | |
| | | { |
| | | 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); |
| | | }, |
| | | }, |
| | | ], |
| | |
| | | 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("æ£å¨è·åé¢ææ°æ®..."); |
| | | 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; |
| | |
| | | materialDetailDialogVisible.value = true; |
| | | }; |
| | | |
| | | const openMaterialSupplementDialog = row => { |
| | | currentMaterialSupplementOrder.value = row; |
| | | materialSupplementDialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleReset = () => { |
| | | searchForm.value = { |
| | | ...searchForm.value, |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | style="width: 100%" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" |
| | | > |
| | | <el-table-column label="æä½"> |
| | | <template #default="scope"> |
| | | <el-button link |
| | | type="primary" |
| | |
| | | <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 { |
| | |
| | | prop: "unit", |
| | | width: 120, |
| | | }, |
| | | |
| | | |
| | | { |
| | | label: "å建æ¶é´", |
| | | prop: "createTime", |
| | |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 250, |
| | | operation: [ |
| | | { |
| | | name: "æ¥çæå
¥", |
| | | type: "text", |
| | | clickFun: row => { |
| | | showInput(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "åæ°è¯¦æ
", |
| | | type: "text", |
| | | clickFun: row => { |
| | | showParamDetail(row); |
| | | }, |
| | | }, |
| | | { |
| | |
| | | }, |
| | | ]); |
| | | 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); |
| | |
| | | </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> |
| | |
| | | <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> |
| | |
| | | </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> |
| | |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | |
| | | <!-- æµè½¬å¡å¼¹çª --> |
| | | <el-dialog v-model="transferCardVisible" |
| | | title="æµè½¬å¡" |
| | |
| | | @click="printTransferCard">æå°æµè½¬å¡</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | |
| | | <!-- æ¥å·¥å¼¹çª --> |
| | | <el-dialog v-model="reportDialogVisible" |
| | | title="æ¥å·¥" |
| | |
| | | :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"> |
| | |
| | | </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> |
| | |
| | | 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"; |
| | |
| | | }, |
| | | { |
| | | label: "ç产订åå·", |
| | | prop: "productOrderNpsNo", |
| | | prop: "npsNo", |
| | | width: "140", |
| | | }, |
| | | { |
| | |
| | | }, |
| | | { |
| | | label: "å·¥åºåç§°", |
| | | prop: "processName", |
| | | prop: "operationName", |
| | | width: "100", |
| | | }, |
| | | { |
| | | label: "éæ±æ°é", |
| | |
| | | openWorkOrderFiles(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "ç©æ", |
| | | clickFun: row => { |
| | | openMaterialDialog(row); |
| | | }, |
| | | }, |
| | | // { |
| | | // name: "ç©æ", |
| | | // clickFun: row => { |
| | | // openMaterialDialog(row); |
| | | // }, |
| | | // }, |
| | | { |
| | | name: "æ¥å·¥", |
| | | clickFun: row => { |
| | |
| | | ], |
| | | }, |
| | | ]); |
| | | |
| | | |
| | | const tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | const transferCardVisible = ref(false); |
| | |
| | | productProcessRouteItemId: "", |
| | | userId: "", |
| | | productMainId: null, |
| | | productionOrderRoutingOperationId: "", |
| | | productionOrderId: "", |
| | | paramGroups: {}, |
| | | }); |
| | | |
| | | const params = ref({}); |
| | | const dictOptions = ref({}); |
| | | const paramLoading = ref(false); |
| | | |
| | | // æ¬æ¬¡ç产æ°ééªè¯è§å |
| | | const validateQuantity = (rule, value, callback) => { |
| | |
| | | // ææçéè´æ´æ°ï¼å
æ¬0ï¼ |
| | | reportForm.scrapQty = num; |
| | | }; |
| | | |
| | | |
| | | const currentReportRowData = ref(null); |
| | | const materialDialogVisible = ref(false); |
| | | const currentMaterialOrderRow = ref(null); |
| | |
| | | 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 }; |
| | |
| | | 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) { |
| | |
| | | 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; |
| | |
| | | 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(() => { |
| | |
| | | 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"> |
| | |
| | | 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" |