src/components/AIChatSidebar/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,1206 @@ <template> <div class="ai-chat-sidebar-wrapper"> <!-- æ¬æµ®å¾æ --> <div class="ai-chat-trigger" @click="toggleSidebar" v-show="!visible"> <el-tooltip content="AI 婿" placement="left"> <div class="trigger-icon"> <el-icon :size="30" color="#fff"><Cpu /></el-icon> </div> </el-tooltip> </div> <!-- ä¾§è¾¹æ å¯¹è¯æ¡ --> <el-drawer v-model="visible" :size="drawerSize" direction="rtl" :with-header="true" class="ai-chat-drawer" :modal="false" :show-close="true" :append-to-body="false" @close="handleClose" > <template #header> <div class="drawer-header"> <div class="header-left"> <el-icon :size="20" class="header-icon"><Cpu /></el-icon> <span class="title">AI æºè½å©æ</span> </div> <div class="header-actions"> <el-tooltip content="ä¼è¯åå²" placement="bottom"> <el-button link @click="toggleHistory"> <el-icon :size="18"><Timer /></el-icon> </el-button> </el-tooltip> <el-tooltip content="å¼å¯æ°ä¼è¯" placement="bottom"> <el-button link @click="newChat"> <el-icon :size="18"><Plus /></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="ææ åå²ä¼è¯" /> </div> </el-skeleton> </div> <div v-else class="chat-main"> <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 type="primary" size="small" @click="newChat"> <el-icon><Plus /></el-icon>æ°ä¼è¯ </el-button> <el-button v-if="isSending" link type="danger" size="small" @click="stopGeneration"> <el-icon><VideoPause /></el-icon>åæ¢çæ </el-button> <el-upload class="file-upload-trigger" action="#" :auto-upload="false" :show-file-list="false" :on-change="handleFileChange" :disabled="isSending" > <el-button link 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="请è¾å ¥æ¨çé®é¢... (Enter åé, Shift+Enter æ¢è¡)" resize="none" @keydown.enter.exact.prevent="sendMessage" /> <el-button type="primary" class="send-btn" :disabled="isSending || (!inputMessage.trim() && !selectedFile)" @click="sendMessage" > åé </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, Loading, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close } from '@element-plus/icons-vue' import { ElMessage } from 'element-plus' 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 toggleHistory = () => { showHistory.value = !showHistory.value if (showHistory.value) { loadSessions() } } const loadSessions = async () => { loadingSessions.value = true try { const res = await request.get('/xiaozhi/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('ai_chat_uuid', uuid.value) // å è½½ä¼è¯æ¶æ¯ try { const res = await request.get(`/xiaozhi/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 jsonRegex = /\{"success":\s*true,[\s\S]*\}/ const jsonMatch = msg.content.match(jsonRegex) if (jsonMatch) { try { const parsedData = JSON.parse(jsonMatch[0]) if (parsedData.success) { 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 renderCharts(botMsgIndex, messageObj.chartOptions) } } } catch (err) {} } 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(`/xiaozhi/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) }) const handleWindowResize = () => { windowWidth.value = window.innerWidth } const toggleSidebar = () => { visible.value = !visible.value if (visible.value) { scrollToBottom() } } const handleClose = () => { visible.value = false } const initUUID = () => { let storedUUID = localStorage.getItem('ai_chat_uuid') if (!storedUUID) { storedUUID = Math.random().toString(36).substring(2, 10) + Date.now().toString(36).substring(4) localStorage.setItem('ai_chat_uuid', storedUUID) } uuid.value = storedUUID } const hello = () => { sendRequest('ä½ å¥½') } const newChat = () => { disposeCharts() messages.value = [] outputState.value = {} localStorage.removeItem('ai_chat_uuid') initUUID() hello() } const disposeCharts = () => { Object.values(chartInstances.value).forEach(chart => chart.dispose()) resizeHandlers.value.forEach(handler => window.removeEventListener('resize', handler)) chartInstances.value = {} resizeHandlers.value = [] } 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('/xiaozhi/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 jsonRegex = /\{"success":\s*true,[\s\S]*\}/ const jsonMatch = fullText.match(jsonRegex) if (jsonMatch) { try { const parsedData = JSON.parse(jsonMatch[0]) if (parsedData.success) { 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 if (!outputState.value[botMsgIndex].hasRenderedChart) { renderCharts(botMsgIndex, currentMsg.chartOptions) outputState.value[botMsgIndex].hasRenderedChart = true } } } } catch (err) {} } 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 = () => { if (currentAbortController.value) { 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 sendRequest = (message) => { isSending.value = true currentAbortController.value = new AbortController() // ç¨æ·æ¶æ¯ if (messages.value.length > 0) { 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('/xiaozhi/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 æ°æ®ï¼é对åµå ¥å¼ JSONï¼ const jsonRegex = /\{"success":\s*true,[\s\S]*\}/ const jsonMatch = fullText.match(jsonRegex) if (jsonMatch) { try { const parsedData = JSON.parse(jsonMatch[0]) if (parsedData.success) { 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 if (!outputState.value[botMsgIndex].hasRenderedChart) { renderCharts(botMsgIndex, currentMsg.chartOptions) outputState.value[botMsgIndex].hasRenderedChart = true } } } } catch (err) {} } 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 fullText = currentMsg.content const jsonRegex = /\{"success":\s*true,[\s\S]*\}/ const jsonMatch = fullText.match(jsonRegex) if (jsonMatch) { try { const parsedData = JSON.parse(jsonMatch[0]) if (parsedData.success) { 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 if (!outputState.value[botMsgIndex].hasRenderedChart) { renderCharts(botMsgIndex, currentMsg.chartOptions) outputState.value[botMsgIndex].hasRenderedChart = true } } currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex) } } catch (err) {} } // å åºæ¸²æ if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) { 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 const jsonRegex = /\{"success":\s*true,[\s\S]*\}/ const jsonMatch = output.match(jsonRegex) if (jsonMatch) { try { const parsed = JSON.parse(jsonMatch[0]) display = output.replace(jsonMatch[0], '').trim() if (!display && parsed.description) display = parsed.description } catch (e) { const start = output.search(/\{"success":\s*true/) display = output.substring(0, start) + '... (æ£å¨çææ°æ®å¾è¡¨)' } } if (state.jsonBlockStartPos !== -1 && state.blockEndPos === -1) { display = display.substring(0, state.jsonBlockStartPos) } else if (state.jsBlockStartPos !== -1 && state.blockEndPos === -1) { display = display.substring(0, state.jsBlockStartPos) } display = display.replace(/```(javascript|js)([\s\S]*?)```/g, '<pre class="code-block js-code">$2</pre>') display = display.replace(/```([\s\S]*?)```/g, '<pre class="code-block">$1</pre>') return convertTextToHtml(display) } 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]) chartInstances.value[id].dispose() const chart = echarts.init(dom) chartInstances.value[id] = chart chart.setOption(chartOptions[key]) const handler = () => chart.resize() resizeHandlers.value.push(handler) window.addEventListener('resize', handler) } else if (count < 10) { setTimeout(() => tryInit(count + 1), 200) } } tryInit() }) }) } watch(messages, () => scrollToBottom(), { deep: true }) </script> <style scoped lang="scss"> .ai-chat-sidebar-wrapper { position: fixed; inset: 0; z-index: 2000; pointer-events: none; :deep(.el-drawer__container) { pointer-events: none; } :deep(.el-drawer) { pointer-events: auto; } } .ai-chat-trigger { pointer-events: auto; position: fixed; right: 20px; bottom: 100px; width: 60px; height: 60px; background: linear-gradient(135deg, #409eff 0%, #007aff 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 12px rgba(0, 122, 255, 0.4); transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); z-index: 2001; &:hover { transform: scale(1.1) translateY(-5px); box-shadow: 0 8px 20px rgba(0, 122, 255, 0.5); } .trigger-icon { display: flex; align-items: center; justify-content: center; } } .ai-chat-drawer { :deep(.el-drawer__body) { padding: 0; overflow: hidden; } :deep(.el-drawer__header) { margin-bottom: 0; padding: 12px 16px; background: #fff; border-bottom: 1px solid #ebeef5; color: #303133; } } .drawer-header { display: flex; justify-content: space-between; align-items: center; width: 100%; padding-right: 32px; .header-left { display: flex; align-items: center; gap: 8px; .header-icon { color: #409eff; } .title { font-size: 16px; font-weight: 600; color: #303133; } } .header-actions { display: flex; gap: 8px; } } .chat-container { display: flex; flex-direction: column; height: 100%; background-color: #f5f7fa; position: relative; } .history-panel { position: absolute; inset: 0; background: #fff; z-index: 10; display: flex; flex-direction: column; .history-header { padding: 16px; border-bottom: 1px solid #ebeef5; display: flex; justify-content: space-between; align-items: center; font-weight: 600; font-size: 14px; } .session-list { flex: 1; overflow-y: auto; padding: 8px; .session-item { display: flex; align-items: center; padding: 12px; margin-bottom: 4px; border-radius: 8px; cursor: pointer; transition: all 0.2s; gap: 10px; position: relative; border: 1px solid transparent; &:hover { background-color: #f5f7fa; .delete-btn { opacity: 1; } } &.active { background-color: #ecf5ff; border-color: #d9ecff; color: #409eff; } .el-icon { font-size: 16px; flex-shrink: 0; } .session-name { flex: 1; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .delete-btn { opacity: 0; transition: opacity 0.2s; padding: 4px; &:hover { color: #f56c6c; } } } } } .chat-main { display: flex; flex-direction: column; height: 100%; flex: 1; overflow: hidden; } .message-list { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 20px; &::-webkit-scrollbar { width: 6px; } &::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 3px; } } .message-item { display: flex; gap: 12px; width: 100%; .avatar { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 20px; } .message-content { flex: 1; overflow-x: hidden; // ä¿®æ¹ä¸º hiddenï¼å é¨å®¹å¨å¤çæ»å¨ display: flex; flex-direction: column; max-width: calc(100% - 48px); // åå»å¤´ååé´è· .text-box { padding: 12px 16px; border-radius: 12px; font-size: 14px; line-height: 1.6; word-break: break-word; max-width: 100%; width: fit-content; overflow-x: auto; &::-webkit-scrollbar { height: 6px; } &::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 3px; } } } &.bot-message { .message-content { align-items: flex-start; } .avatar { background-color: #409eff; color: #fff; } .text-box { background-color: #fff; color: #303133; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); } } &.user-message { flex-direction: row-reverse; .message-content { align-items: flex-end; } .avatar { background-color: #95d475; color: #fff; } .text-box { background-color: #409eff; color: #fff; } } } .charts-wrapper { margin-top: 10px; display: flex; flex-direction: column; gap: 10px; overflow-x: auto; width: 100%; padding-bottom: 8px; &::-webkit-scrollbar { height: 6px; } &::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 3px; } } .chart-item { width: 100%; min-width: 300px; height: 300px; background: #fff; border-radius: 8px; padding: 10px; } .table-wrapper { margin-top: 10px; background: #fff; border-radius: 8px; overflow: hidden; overflow-x: auto; width: 100%; &::-webkit-scrollbar { height: 6px; } &::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 3px; } .el-table { min-width: 300px; } } .input-area { padding: 16px; background-color: #fff; border-top: 1px solid #dcdfe6; .input-actions { display: flex; gap: 12px; margin-bottom: 8px; align-items: center; .file-upload-trigger { display: inline-flex; align-items: center; } } .input-box { padding: 12px; position: relative; background: #fff; border: 1px solid #dcdfe6; border-radius: 8px; margin: 0 16px 16px; transition: border-color 0.2s; &:focus-within { border-color: #409eff; } .selected-file-tag { display: flex; align-items: center; background: #f0f7ff; border: 1px solid #d9ecff; border-radius: 4px; padding: 4px 8px; margin-bottom: 8px; gap: 6px; width: fit-content; max-width: 100%; .el-icon { color: #409eff; font-size: 14px; } .file-name { font-size: 12px; color: #606266; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .remove-file { cursor: pointer; color: #909399; transition: color 0.2s; &:hover { color: #f56c6c; } } } :deep(.el-textarea__inner) { padding: 0; border: none; box-shadow: none; background: transparent; font-family: inherit; font-size: 14px; line-height: 1.5; color: #303133; &::placeholder { color: #c0c4cc; } } .send-btn { position: absolute; right: 12px; bottom: 12px; padding: 8px 16px; } } } .typing-indicator { display: flex; gap: 4px; padding: 8px 12px; background: #fff; border-radius: 12px; width: fit-content; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); margin-top: 4px; .dot { width: 6px; height: 6px; background-color: #909399; border-radius: 50%; animation: typing 1.4s infinite ease-in-out; &:nth-child(2) { animation-delay: 0.2s; } &:nth-child(3) { animation-delay: 0.4s; } } } @keyframes typing { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } } .code-block { background: #2d2d2d; color: #ccc; padding: 12px; border-radius: 6px; font-family: monospace; margin: 8px 0; overflow-x: auto; &.js-code { color: #f08d49; } } </style> src/layout/index.vue
@@ -16,6 +16,7 @@ <app-main /> <settings ref="settingRef" /> </div> <AIChatSidebar /> </div> </template> @@ -23,6 +24,7 @@ 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"; src/views/fileManagement/borrow/index.vue
@@ -100,16 +100,14 @@ </el-col> <el-col :span="12"> <el-form-item label="åé 书ç±ï¼" prop="documentationId"> <!-- <el-select v-model="borrowForm.documentationId" placeholder="è¯·éæ©åé 书ç±" style="width: 100%" @change="handleScanContent"> <el-option v-for="item in documentList" :key="item.id" :label="item.docName || item.name" :value="item.id" /> </el-select> --> <div style="display: flex; gap: 10px;"> <el-select v-model="borrowForm.documentationId" placeholder="è¯·éæ©åé 书ç±" style="flex: 1;width: 100px;" @change="handleSelectChange"> <el-select v-if="borrowOperationType !== 'edit'" v-model="borrowForm.documentationId" placeholder="è¯·éæ©åé 书ç±" style="flex: 1;width: 100px;" @change="handleSelectChange" > <el-option v-for="item in documentList" :key="item.id" @@ -118,6 +116,13 @@ /> </el-select> <el-input v-else v-model="currentEditDocName" style="flex: 1;width: 100px;" disabled /> <el-input v-if="borrowOperationType !== 'edit'" v-model="scanContent" placeholder="æ«ç è¾å ¥" style="width: 100px;" @@ -205,6 +210,7 @@ const selectedRows = ref([]); const documentList = ref([]); // ææ¡£å表ï¼ç¨äºåé 书ç±éæ© const scanContent = ref() // æ«ç å 容 const currentEditDocName = ref(''); // ç¼è¾æ¶åå¨çææ¡£åç§° // å页ç¸å ³ const pagination = reactive({ currentPage: 1, @@ -282,6 +288,7 @@ { name: "ç¼è¾", type: "text", disabled: (row) => row.borrowStatus === 'å½è¿', clickFun: (row) => { openBorrowDia('edit', row) }, @@ -428,11 +435,14 @@ if (type === "edit") { // ç¼è¾æ¨¡å¼ï¼å è½½ç°ææ°æ® Object.assign(borrowForm, data); // åå¨ææ¡£åç§°ç¨äºæ¾ç¤º currentEditDocName.value = data.docName || ''; } else { // æ°å¢æ¨¡å¼ï¼æ¸ 空表å Object.keys(borrowForm).forEach(key => { borrowForm[key] = ""; }); currentEditDocName.value = ''; // æ¸ ç©ºç¼è¾æ¶çææ¡£åç§° // 设置é»è®¤ç¶æ borrowForm.borrowStatus = "åé "; // 设置å½åæ¥æä¸ºåé æ¥æ @@ -445,6 +455,7 @@ proxy.$refs.borrowFormRef.resetFields(); borrowDia.value = false; scanContent.value = ''; // æ¸ ç©ºæ«ç å 容 currentEditDocName.value = ''; // æ¸ ç©ºç¼è¾æ¶çææ¡£åç§° }; // æäº¤åé 表å src/views/fileManagement/document/index.vue
@@ -862,12 +862,14 @@ documentForm[key] = ""; }); documentForm.attachments = []; // æ°å¢æ¨¡å¼ä¸ä¹æ¸ 空éä»¶ // 设置é»è®¤å¼ - 使ç¨åå ¸æ°æ®ç第ä¸ä¸ªé项ä½ä¸ºé»è®¤å¼ // 设置é»è®¤å¼ - ææ¡£ç¶æé»è®¤è®¾ç½®ä¸º"æ£å¸¸" if (document_status.value && document_status.value.length > 0) { documentForm.docStatus = document_status.value[0].value; const normalStatus = document_status.value.find(item => item.label === 'æ£å¸¸'); documentForm.docStatus = normalStatus ? normalStatus.value : document_status.value[0].value; } if (document_urgency.value && document_urgency.value.length > 0) { documentForm.urgencyLevel = document_urgency.value[0].value; const normalUrgency = document_urgency.value.find(item => item.label === 'æ®é'); documentForm.urgencyLevel = normalUrgency ? normalUrgency.value : document_urgency.value[0].value; } } }; src/views/fileManagement/return/index.vue
@@ -103,16 +103,14 @@ <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="ææ¡£ï¼" prop="borrowId"> <!-- <el-select v-model="returnForm.borrowId" placeholder="è¯·éæ©ææ¡£" style="flex: 1;" @change="handleDocumentChange"> <el-option v-for="item in documentList" :key="item.id" :label="item.docName || item.name" :value="item.id" /> </el-select> --> <div style="display: flex; gap: 10px;"> <el-select v-model="returnForm.borrowId" placeholder="è¯·éæ©ææ¡£" style="width: 120px;" @change="handleDocumentChange"> <el-select v-if="returnOperationType !== 'edit'" v-model="returnForm.borrowId" placeholder="è¯·éæ©ææ¡£" style="width: 120px;" @change="handleDocumentChange" > <el-option v-for="item in documentList" :key="item.id" @@ -121,6 +119,13 @@ /> </el-select> <el-input v-else v-model="currentEditDocName" style="width: 120px;" disabled /> <el-input v-if="returnOperationType !== 'edit'" v-model="scanContent" placeholder="æ«ç è¾å ¥" style="flex: 1;" @@ -215,6 +220,7 @@ const documentList = ref([]); // ææ¡£å表 const borrowInfoList = ref([]); // åé ä¿¡æ¯å表 const scanContent = ref(); // æ«ç å 容 const currentEditDocName = ref(''); // ç¼è¾æ¶åå¨çææ¡£åç§° // å页ç¸å ³ const pagination = reactive({ @@ -286,6 +292,7 @@ { name: "ç¼è¾", type: "text", disabled: (row) => row.borrowStatus === 'å½è¿', clickFun: (row) => { openReturnDia('edit', row) }, @@ -396,15 +403,14 @@ if (type === "edit") { // ç¼è¾æ¨¡å¼ï¼å è½½ç°ææ°æ® Object.assign(returnForm, data); // ç¼è¾æ¨¡å¼ä¸ï¼ææ¡£éæ©åèªå¨å¡«å åé 人ååºå½è¿æ¥æ if (returnForm.borrowId) { handleDocumentChange(returnForm.borrowId); } // åå¨ææ¡£åç§°ç¨äºæ¾ç¤º currentEditDocName.value = data.docName || ''; } else { // æ°å¢æ¨¡å¼ï¼æ¸ 空表å Object.keys(returnForm).forEach(key => { returnForm[key] = ""; }); currentEditDocName.value = ''; // æ¸ ç©ºç¼è¾æ¶çææ¡£åç§° // 设置é»è®¤ç¶æ returnForm.borrowStatus = "å½è¿"; // 设置å½åæ¥æä¸ºå½è¿æ¥æ @@ -418,6 +424,7 @@ returnDia.value = false; scanContent.value = ''; // æ¸ ç©ºæ«ç å 容 borrowInfoList.value = []; // æ¸ ç©ºåé ä¿¡æ¯å表 currentEditDocName.value = ''; // æ¸ ç©ºç¼è¾æ¶çææ¡£åç§° }; // æäº¤å½è¿è¡¨å src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue
@@ -35,16 +35,30 @@ <el-button type="primary" link @click="handleViewSupplementRecord(row)"> {{ row.supplementQty ?? 0 }} {{ row.feedingQty ?? 0 }} </el-button> </template> </el-table-column> <el-table-column label="éææ°é" prop="returnQty" min-width="110" /> min-width="110"> <template #default="{ row }"> {{ row.returnQty ?? 0 }} </template> </el-table-column> <el-table-column label="å®é æ°é" prop="actualQty" min-width="110" /> min-width="140"> <template #default="{ row }"> <el-input-number v-model="row.actualQty" :min="0" :precision="3" :step="1" controls-position="right" placeholder="è¾å ¥å®é æ°é" style="width: 100%;" :disabled="row.returned" @change="val => handleActualQtyChange(row, val)" /> </template> </el-table-column> </el-table> <template #footer> <span class="dialog-footer"> @@ -66,7 +80,7 @@ border row-key="id"> <el-table-column label="è¡¥ææ°é" prop="supplementQty" prop="pickQuantity" min-width="120" /> <el-table-column label="è¡¥æäºº" prop="supplementUserName" @@ -75,7 +89,7 @@ prop="supplementTime" min-width="160" /> <el-table-column label="è¡¥æåå " prop="supplementReason" prop="feedingReason" min-width="200" /> </el-table> <template #footer> @@ -121,7 +135,7 @@ import { listMaterialPickingDetail, listMaterialSupplementRecord, confirmMaterialReturn, updateMaterialPickingLedger, } from "@/api/productionManagement/productionOrder.js"; const props = defineProps({ @@ -145,10 +159,12 @@ const returnSummaryList = ref([]); const calcReturnQty = item => Number(item.pickQuantity || 0) + Number(item.supplementQty || 0) - Number(item.feedingQty || 0) - Number(item.actualQty || 0); const canOpenReturnSummary = computed(() => materialDetailTableData.value.some(item => calcReturnQty(item) > 0) materialDetailTableData.value.some( item => item.returned !== true && calcReturnQty(item) > 0 ) ); const loadDetailList = async () => { @@ -157,7 +173,13 @@ materialDetailTableData.value = []; try { const res = await listMaterialPickingDetail(props.orderRow.id); materialDetailTableData.value = res.data || []; materialDetailTableData.value = (res.data || []).map(item => ({ ...item, actualQty: item.actualQty ?? Number(item.pickQuantity || 0) + Number(item.feedingQty || 0), returnQty: item.returnQty ?? 0, })); } finally { materialDetailLoading.value = false; } @@ -176,6 +198,10 @@ materialDetailTableData.value = []; }; const handleActualQtyChange = (row, val) => { row.returnQty = calcReturnQty(row); }; const handleViewSupplementRecord = async row => { if (!row?.id) return; supplementRecordDialogVisible.value = true; @@ -183,7 +209,8 @@ supplementRecordTableData.value = []; try { const res = await listMaterialSupplementRecord({ materialDetailId: row.id, pickId: row.id, productionOrderId: props.orderRow.id, }); supplementRecordTableData.value = res.data || []; } finally { @@ -225,9 +252,24 @@ if (!props.orderRow?.id) return; materialReturnConfirming.value = true; try { await confirmMaterialReturn({ orderId: props.orderRow.id, returnSummaryList: returnSummaryList.value, await updateMaterialPickingLedger({ productionOrderId: props.orderRow.id, productionOrderPickDto: materialDetailTableData.value.map(item => ({ id: item.id, technologyOperationId: item.technologyOperationId, operationName: item.operationName, bom: item.bom === true, productModelId: item.productModelId, demandedQuantity: item.demandedQuantity, unit: item.unit, pickQuantity: item.pickQuantity, batchNo: item.batchNo, feedingQty: item.feedingQty, returnQty: item.returnQty, actualQty: item.actualQty, feedingReason: item.feedingReason, returned: true, })), }); returnSummaryDialogVisible.value = false; dialogVisible.value = false; src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,159 @@ <template> <el-dialog v-model="dialogVisible" title="è¡¥æ" width="1200px" @close="handleClose"> <el-table v-loading="loading" :data="tableData" border row-key="id"> <el-table-column label="å·¥åºåç§°" prop="operationName" min-width="140" /> <el-table-column label="åæåç§°" prop="productName" min-width="140" /> <el-table-column label="åæåå·" prop="model" min-width="140" /> <el-table-column label="计éåä½" prop="unit" width="100" /> <el-table-column label="é¢ç¨æ°é" prop="pickQuantity" width="100" /> <el-table-column label="è¡¥ææ°é" min-width="150"> <template #default="{ row }"> <el-input-number v-model="row.newSupplementQty" :min="0" :precision="3" :step="1" controls-position="right" placeholder="è¾å ¥è¡¥ææ°é" style="width: 100%;" /> </template> </el-table-column> <el-table-column label="è¡¥æåå " min-width="200"> <template #default="{ row }"> <el-input v-model="row.newSupplementReason" placeholder="è¾å ¥è¡¥æåå " maxlength="200" show-word-limit /> </template> </el-table-column> </el-table> <template #footer> <span class="dialog-footer"> <el-button type="primary" :loading="submitting" @click="handleSubmit">ç¡® å®</el-button> <el-button @click="dialogVisible = false">å æ¶</el-button> </span> </template> </el-dialog> </template> <script setup> import { computed, ref, watch } from "vue"; import { ElMessage } from "element-plus"; import { listMaterialPickingDetail, updateMaterialPickingLedger, } from "@/api/productionManagement/productionOrder.js"; const props = defineProps({ modelValue: { type: Boolean, default: false }, orderRow: { type: Object, default: null }, }); const emit = defineEmits(["update:modelValue", "saved"]); const dialogVisible = computed({ get: () => props.modelValue, set: val => emit("update:modelValue", val), }); const loading = ref(false); const submitting = ref(false); const tableData = ref([]); const loadData = async () => { if (!props.orderRow?.id) return; loading.value = true; try { const res = await listMaterialPickingDetail(props.orderRow.id); tableData.value = (res.data || []).map(item => ({ ...item, newSupplementQty: 0, newSupplementReason: "", })); } catch (e) { console.error("è·åç©ææç»å¤±è´¥ï¼", e); ElMessage.error("è·åç©ææç»å¤±è´¥"); } finally { loading.value = false; } }; watch( () => dialogVisible.value, visible => { if (visible) { loadData(); } } ); const handleClose = () => { tableData.value = []; }; const handleSubmit = async () => { const supplementList = tableData.value.filter( item => item.newSupplementQty > 0 ); if (supplementList.length === 0) { ElMessage.warning("请è³å°è¾å ¥ä¸æ¡è¡¥ææ°é"); return; } const invalidRow = supplementList.find(item => !item.newSupplementReason); if (invalidRow) { ElMessage.warning("请è¾å ¥è¡¥æåå "); return; } submitting.value = true; try { await updateMaterialPickingLedger({ productionOrderId: props.orderRow.id, productionOrderPickDto: tableData.value.map(item => ({ id: item.id, technologyOperationId: item.technologyOperationId, operationName: item.operationName, bom: item.bom === true, productModelId: item.productModelId, demandedQuantity: item.demandedQuantity, unit: item.unit, pickQuantity: item.pickQuantity, batchNo: item.batchNo, feedingQuantity: item.newSupplementQty || 0, feedingReason: item.newSupplementReason || "", pickType: 2, })), }); ElMessage.success("è¡¥ææå"); dialogVisible.value = false; emit("saved"); } catch (e) { console.error("è¡¥æå¤±è´¥ï¼", e); ElMessage.error("è¡¥æå¤±è´¥"); } finally { submitting.value = false; } }; </script> <style scoped lang="scss"> </style> src/views/productionManagement/productionOrder/index.vue
@@ -175,6 +175,9 @@ <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" /> @@ -205,6 +208,7 @@ 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 PIMTable from "@/components/PIMTable/PIMTable.vue"; import { listPage } from "@/api/productionManagement/processRoute.js"; const NewProductOrder = defineAsyncComponent(() => @@ -304,7 +308,7 @@ label: "æä½", align: "center", fixed: "right", width: 360, width: 260, operation: [ { name: "å·¥èºè·¯çº¿", @@ -340,13 +344,23 @@ { 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); }, @@ -423,6 +437,8 @@ const currentMaterialOrder = ref(null); const materialDetailDialogVisible = ref(false); const currentMaterialDetailOrder = ref(null); const materialSupplementDialogVisible = ref(false); const currentMaterialSupplementOrder = ref(null); const openBindRouteDialog = async (row, type) => { bindForm.orderId = row.id; @@ -478,6 +494,11 @@ materialDetailDialogVisible.value = true; }; const openMaterialSupplementDialog = row => { currentMaterialSupplementOrder.value = row; materialSupplementDialogVisible.value = true; }; const handleReset = () => { searchForm.value = { ...searchForm.value, src/views/productionManagement/productionProcess/Edit.vue
@@ -0,0 +1,168 @@ <template> <div> <el-dialog v-model="isShow" title="ç¼è¾å·¥åº" width="400" @close="closeModal" > <el-form label-width="140px" :model="formState" label-position="top" ref="formRef"> <el-form-item label="å·¥åºåç§°ï¼" prop="name" :rules="[ { required: true, message: '请è¾å ¥å·¥åºåç§°', }, { max: 100, message: 'æå¤100个å符', } ]"> <el-input v-model="formState.name" /> </el-form-item> <el-form-item label="å·¥åºç¼å·" prop="no"> <el-input v-model="formState.no" /> </el-form-item> <el-form-item label="å·¥åºç±»å" prop="type" :rules="[ { required: true, message: 'è¯·éæ©å·¥åºç±»å', } ]" > <el-select v-model="formState.type" placeholder="è¯·éæ©å·¥åºç±»å"> <el-option label="计æ¶" :value="0" /> <el-option label="计件" :value="1" /> </el-select> </el-form-item> <el-form-item label="å·¥èµå®é¢" prop="salaryQuota"> <el-input v-model="formState.salaryQuota" type="number" :step="0.001" /> </el-form-item> <el-form-item label="æ¯å¦è´¨æ£" prop="isQuality"> <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/> </el-form-item> <el-form-item label="æ¯å¦å ¥åº" prop="inbound"> <el-switch v-model="formState.inbound" :active-value="true" inactive-value="false"/> </el-form-item> <el-form-item label="æ¯å¦æ¥å·¥" prop="reportWork"> <el-switch v-model="formState.reportWork" :active-value="true" inactive-value="false"/> </el-form-item> <el-form-item label="夿³¨" prop="remark"> <el-input v-model="formState.remark" type="textarea" /> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="handleSubmit">确认</el-button> <el-button @click="closeModal">åæ¶</el-button> </div> </template> </el-dialog> </div> </template> <script setup> import { ref, computed, getCurrentInstance, watch } from "vue"; import {update} from "@/api/productionManagement/productionProcess.js"; const props = defineProps({ visible: { type: Boolean, required: true, }, record: { type: Object, required: true, } }); const emit = defineEmits(['update:visible', 'completed']); // ååºå¼æ°æ®ï¼æ¿ä»£é项å¼ç dataï¼ const formState = ref({ id: props.record.id, name: props.record.name, type: props.record.type, no: props.record.no, remark: props.record.remark, salaryQuota: props.record.salaryQuota, isQuality: props.record.isQuality, inbound: props.record.inbound, reportWork: props.record.reportWork, }); const isShow = computed({ get() { return props.visible; }, set(val) { emit('update:visible', val); }, }); // çå¬ record ååï¼æ´æ°è¡¨åæ°æ® watch(() => props.record, (newRecord) => { if (newRecord && isShow.value) { formState.value = { id: newRecord.id, name: newRecord.name || '', no: newRecord.no || '', type: newRecord.type, remark: newRecord.remark || '', salaryQuota: newRecord.salaryQuota || '', isQuality: props.record.isQuality, inbound: newRecord.inbound, reportWork: newRecord.reportWork, }; } }, { immediate: true, deep: true }); // çå¬å¼¹çªæå¼ï¼éæ°åå§åè¡¨åæ°æ® watch(() => props.visible, (visible) => { if (visible && props.record) { formState.value = { id: props.record.id, name: props.record.name || '', no: props.record.no || '', type: props.record.type, remark: props.record.remark || '', salaryQuota: props.record.salaryQuota || '', isQuality: props.record.isQuality, inbound: props.record.inbound, reportWork: props.record.reportWork, }; } }); let { proxy } = getCurrentInstance() const closeModal = () => { isShow.value = false; }; const handleSubmit = () => { proxy.$refs["formRef"].validate(valid => { if (valid) { update(formState.value).then(res => { // å ³éæ¨¡ææ¡ isShow.value = false; // åç¥ç¶ç»ä»¶å·²å®æ emit('completed'); proxy.$modal.msgSuccess("æäº¤æå"); }) } }) }; defineExpose({ closeModal, handleSubmit, isShow, }); </script> src/views/productionManagement/productionProcess/New.vue
@@ -0,0 +1,129 @@ <template> <div> <el-dialog v-model="isShow" title="æ°å¢å·¥åº" width="400" @close="closeModal" > <el-form label-width="140px" :model="formState" label-position="top" ref="formRef"> <el-form-item label="å·¥åºåç§°ï¼" prop="name" :rules="[ { required: true, message: '请è¾å ¥å·¥åºåç§°', }, { max: 100, message: 'æå¤100个å符', } ]"> <el-input v-model="formState.name" /> </el-form-item> <el-form-item label="å·¥åºç¼å·" prop="no"> <el-input v-model="formState.no" /> </el-form-item> <el-form-item label="å·¥åºç±»å" prop="type" :rules="[ { required: true, message: 'è¯·éæ©å·¥åºç±»å', } ]" > <el-select v-model="formState.type" placeholder="è¯·éæ©å·¥åºç±»å"> <el-option label="计æ¶" :value="0" /> <el-option label="计件" :value="1" /> </el-select> </el-form-item> <el-form-item label="å·¥èµå®é¢" prop="salaryQuota"> <el-input v-model="formState.salaryQuota" type="number" :step="0.001"> <template #append>å </template> </el-input> </el-form-item> <el-form-item label="æ¯å¦è´¨æ£" prop="isQuality"> <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/> </el-form-item> <el-form-item label="æ¯å¦å ¥åº" prop="inbound"> <el-switch v-model="formState.inbound" :active-value="true" inactive-value="false"/> </el-form-item> <el-form-item label="æ¯å¦æ¥å·¥" prop="reportWork"> <el-switch v-model="formState.reportWork" :active-value="true" inactive-value="false"/> </el-form-item> <el-form-item label="夿³¨" prop="remark"> <el-input v-model="formState.remark" type="textarea" /> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="handleSubmit">确认</el-button> <el-button @click="closeModal">åæ¶</el-button> </div> </template> </el-dialog> </div> </template> <script setup> import { ref, computed, getCurrentInstance } from "vue"; import {add} from "@/api/productionManagement/productionProcess.js"; const props = defineProps({ visible: { type: Boolean, required: true, }, }); const emit = defineEmits(['update:visible', 'completed']); // ååºå¼æ°æ®ï¼æ¿ä»£é项å¼ç dataï¼ const formState = ref({ name: '', type: undefined, remark: '', salaryQuota: '', isQuality: false, inbound: false, reportWork: false, }); const isShow = computed({ get() { return props.visible; }, set(val) { emit('update:visible', val); }, }); let { proxy } = getCurrentInstance() const closeModal = () => { isShow.value = false; }; const handleSubmit = () => { proxy.$refs["formRef"].validate(valid => { if (valid) { add(formState.value).then(res => { // å ³éæ¨¡ææ¡ isShow.value = false; // åç¥ç¶ç»ä»¶å·²å®æ emit('completed'); proxy.$modal.msgSuccess("æäº¤æå"); }) } }) }; defineExpose({ closeModal, handleSubmit, isShow, }); </script> src/views/productionManagement/workOrderEdit/index.vue
@@ -4,74 +4,66 @@ <div class="search-row"> <div class="search-item"> <span class="search_title">å·¥åç¼å·ï¼</span> <el-input v-model="searchForm.workOrderNo" <el-input v-model="searchForm.workOrderNo" style="width: 240px" placeholder="请è¾å ¥" @change="handleQuery" clearable prefix-icon="Search" /> prefix-icon="Search" /> </div> <div class="search-item"> <span class="search_title">ç产订åå·ï¼</span> <el-input v-model="searchForm.productOrderNpsNo" <el-input v-model="searchForm.productOrderNpsNo" style="width: 240px" placeholder="请è¾å ¥" @change="handleQuery" clearable prefix-icon="Search" /> 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" <PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :tableLoading="tableLoading" @pagination="pagination" > @pagination="pagination"> <template #completionStatus="{ row }"> <el-progress :percentage="toProgressPercentage(row?.completionStatus)" <el-progress :percentage="toProgressPercentage(row?.completionStatus)" :color="progressColor(toProgressPercentage(row?.completionStatus))" :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" /> :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" <el-date-picker v-model="editrow.planStartTime" type="date" placeholder="è¯·éæ©" value-format="YYYY-MM-DD" style="width: 300px" /> style="width: 300px" /> </el-form-item> <el-form-item label="计åç»ææ¶é´"> <el-date-picker v-model="editrow.planEndTime" <el-date-picker v-model="editrow.planEndTime" type="date" placeholder="è¯·éæ©" value-format="YYYY-MM-DD" style="width: 300px" /> 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> @@ -102,7 +94,7 @@ }, { label: "ç产订åå·", prop: "productOrderNpsNo", prop: "npsNo", width: "140", }, { @@ -120,7 +112,8 @@ }, { label: "å·¥åºåç§°", prop: "processName", prop: "operationName", width: "100", }, { label: "éæ±æ°é", src/views/productionManagement/workOrderManagement/index.vue
@@ -40,7 +40,6 @@ </template> </PIMTable> </div> <!-- æµè½¬å¡å¼¹çª --> <el-dialog v-model="transferCardVisible" title="æµè½¬å¡" @@ -116,7 +115,6 @@ @click="printTransferCard">æå°æµè½¬å¡</el-button> </div> </el-dialog> <!-- æ¥å·¥å¼¹çª --> <el-dialog v-model="reportDialogVisible" title="æ¥å·¥" @@ -172,13 +170,9 @@ </span> </template> </el-dialog> <MaterialDialog v-model="materialDialogVisible" <MaterialDialog v-model="materialDialogVisible" :row-data="currentMaterialOrderRow" @refresh="getList" /> @refresh="getList" /> <FilesDia ref="workOrderFilesRef" /> </div> </template> @@ -212,7 +206,7 @@ }, { label: "ç产订åå·", prop: "productOrderNpsNo", prop: "npsNo", width: "140", }, { @@ -230,7 +224,7 @@ }, { label: "å·¥åºåç§°", prop: "processName", prop: "operationName", }, { label: "éæ±æ°é", @@ -288,12 +282,12 @@ openWorkOrderFiles(row); }, }, { name: "ç©æ", clickFun: row => { openMaterialDialog(row); }, }, // { // name: "ç©æ", // clickFun: row => { // openMaterialDialog(row); // }, // }, { name: "æ¥å·¥", clickFun: row => { vite.config.js
@@ -8,7 +8,7 @@ const { VITE_APP_ENV } = env; const baseUrl = env.VITE_APP_ENV === "development" ? "http://1.15.17.182:9003" ? "http://localhost:7005" : env.VITE_BASE_API; const javaUrl = env.VITE_APP_ENV === "development"