From 27af440fbce012aefbd1023f23762cf925e16fab Mon Sep 17 00:00:00 2001
From: huminmin <mac@MacBook-Pro.local>
Date: 星期二, 28 四月 2026 14:25:51 +0800
Subject: [PATCH] Merge branch 'dev_NEW_pro' of http://114.132.189.42:9002/r/product-inventory-management into dev_NEW_pro
---
src/views/fileManagement/document/index.vue | 8
src/layout/index.vue | 264 +++---
src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue | 159 +++
src/components/AIChatSidebar/index.vue | 1206 ++++++++++++++++++++++++++++
src/views/productionManagement/productionProcess/Edit.vue | 168 +++
src/views/productionManagement/workOrderEdit/index.vue | 447 +++++-----
src/views/productionManagement/workOrderManagement/index.vue | 36
src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue | 72 +
src/views/fileManagement/return/index.vue | 33
src/views/fileManagement/borrow/index.vue | 33
src/views/productionManagement/productionOrder/index.vue | 23
vite.config.js | 2
src/views/productionManagement/productionProcess/New.vue | 129 +++
13 files changed, 2,157 insertions(+), 423 deletions(-)
diff --git a/src/components/AIChatSidebar/index.vue b/src/components/AIChatSidebar/index.vue
new file mode 100644
index 0000000..d14978c
--- /dev/null
+++ b/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 || []
+
+ // 閲嶆柊鏋勯�犳秷鎭垪琛ㄥ苟瑙f瀽
+ historyMsgs.forEach((msg, idx) => {
+ const isUser = msg.role === 'user'
+ const botMsgIndex = messages.value.length
+
+ const messageObj = {
+ isUser,
+ content: msg.content,
+ htmlContent: '',
+ isTyping: false,
+ chartOptions: null,
+ chartRenderReady: false,
+ type: '',
+ tableData: null
+ }
+
+ messages.value.push(messageObj)
+
+ if (!isUser) {
+ outputState.value[botMsgIndex] = {
+ isPaused: false,
+ jsonBlockStartPos: -1,
+ jsBlockStartPos: -1,
+ blockEndPos: -1,
+ hasRenderedChart: false
+ }
+
+ // 瑙f瀽鍘嗗彶娑堟伅涓殑 JSON
+ const 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
+
+ // 瑙f瀽 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
+
+ // 瑙f瀽 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) + '... (姝e湪鐢熸垚鏁版嵁鍥捐〃)'
+ }
+ }
+
+ 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>
diff --git a/src/layout/index.vue b/src/layout/index.vue
index d3580d0..a1bb724 100644
--- a/src/layout/index.vue
+++ b/src/layout/index.vue
@@ -1,131 +1,133 @@
-<template>
- <div :class="classObj"
- class="app-wrapper"
- :style="{ '--current-color': theme }">
- <div v-if="device === 'mobile' && sidebar.opened"
- class="drawer-bg"
- @click="handleClickOutside" />
- <sidebar v-if="!sidebar.hide"
- class="sidebar-container" />
- <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }"
- class="main-container">
- <div :class="{ 'fixed-header': fixedHeader }">
- <navbar @setLayout="setLayout" />
- <tags-view v-if="needTagsView" />
- </div>
- <app-main />
- <settings ref="settingRef" />
- </div>
- </div>
-</template>
-
-<script setup>
- import { useWindowSize } from "@vueuse/core";
- import Sidebar from "./components/Sidebar/index.vue";
- import { AppMain, Navbar, Settings, TagsView } from "./components";
- import defaultSettings from "@/settings";
-
- import useAppStore from "@/store/modules/app";
- import useSettingsStore from "@/store/modules/settings";
-
- const settingsStore = useSettingsStore();
- const theme = computed(() => settingsStore.theme);
- const sideTheme = computed(() => settingsStore.sideTheme);
- const sidebar = computed(() => useAppStore().sidebar);
- const device = computed(() => useAppStore().device);
- const needTagsView = computed(() => settingsStore.tagsView);
- const fixedHeader = computed(() => settingsStore.fixedHeader);
-
- const classObj = computed(() => ({
- hideSidebar: !sidebar.value.opened,
- openSidebar: sidebar.value.opened,
- withoutAnimation: sidebar.value.withoutAnimation,
- mobile: device.value === "mobile",
- }));
-
- const { width, height } = useWindowSize();
- const WIDTH = 992; // refer to Bootstrap's responsive design
-
- watch(
- () => device.value,
- () => {
- if (device.value === "mobile" && sidebar.value.opened) {
- useAppStore().closeSideBar({ withoutAnimation: false });
- }
- }
- );
-
- watchEffect(() => {
- if (width.value - 1 < WIDTH) {
- useAppStore().toggleDevice("mobile");
- useAppStore().closeSideBar({ withoutAnimation: true });
- } else {
- useAppStore().toggleDevice("desktop");
- }
- });
-
- function handleClickOutside() {
- useAppStore().closeSideBar({ withoutAnimation: false });
- }
-
- const settingRef = ref(null);
- function setLayout() {
- settingRef.value.openSetting();
- }
-</script>
-
-<style lang="scss" scoped>
- @import "@/assets/styles/mixin.scss";
- @import "@/assets/styles/variables.module.scss";
-
- .app-wrapper {
- @include clearfix;
- position: relative;
- height: 100%;
- width: 100%;
- background: radial-gradient(
- circle at top,
- rgba(223, 232, 226, 0.95),
- transparent 32%
- ),
- linear-gradient(180deg, #f7faf8 0%, var(--app-bg) 100%);
-
- &.mobile.openSidebar {
- position: fixed;
- top: 0;
- }
- }
-
- .drawer-bg {
- background: #000;
- opacity: 0.3;
- width: 100%;
- top: 0;
- height: 100%;
- position: absolute;
- z-index: 999;
- }
-
- .fixed-header {
- position: fixed;
- top: 0px;
- padding-top: 12px;
- right: 16px;
- z-index: 9;
- width: calc(100% - #{$base-sidebar-width} - 32px);
- transition: width 0.28s, right 0.28s;
- padding-bottom: 8px;
- background-color: #f3f6f4;
- }
- .hideSidebar .fixed-header {
- width: calc(100% - 100px);
- }
-
- .sidebarHide .fixed-header {
- width: calc(100% - 32px);
- }
-
- .mobile .fixed-header {
- width: 100%;
- }
-</style>
+<template>
+ <div :class="classObj"
+ class="app-wrapper"
+ :style="{ '--current-color': theme }">
+ <div v-if="device === 'mobile' && sidebar.opened"
+ class="drawer-bg"
+ @click="handleClickOutside" />
+ <sidebar v-if="!sidebar.hide"
+ class="sidebar-container" />
+ <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }"
+ class="main-container">
+ <div :class="{ 'fixed-header': fixedHeader }">
+ <navbar @setLayout="setLayout" />
+ <tags-view v-if="needTagsView" />
+ </div>
+ <app-main />
+ <settings ref="settingRef" />
+ </div>
+ <AIChatSidebar />
+ </div>
+</template>
+
+<script setup>
+ import { useWindowSize } from "@vueuse/core";
+ import Sidebar from "./components/Sidebar/index.vue";
+ import { AppMain, Navbar, Settings, TagsView } from "./components";
+ import AIChatSidebar from "@/components/AIChatSidebar/index.vue";
+ import defaultSettings from "@/settings";
+
+ import useAppStore from "@/store/modules/app";
+ import useSettingsStore from "@/store/modules/settings";
+
+ const settingsStore = useSettingsStore();
+ const theme = computed(() => settingsStore.theme);
+ const sideTheme = computed(() => settingsStore.sideTheme);
+ const sidebar = computed(() => useAppStore().sidebar);
+ const device = computed(() => useAppStore().device);
+ const needTagsView = computed(() => settingsStore.tagsView);
+ const fixedHeader = computed(() => settingsStore.fixedHeader);
+
+ const classObj = computed(() => ({
+ hideSidebar: !sidebar.value.opened,
+ openSidebar: sidebar.value.opened,
+ withoutAnimation: sidebar.value.withoutAnimation,
+ mobile: device.value === "mobile",
+ }));
+
+ const { width, height } = useWindowSize();
+ const WIDTH = 992; // refer to Bootstrap's responsive design
+
+ watch(
+ () => device.value,
+ () => {
+ if (device.value === "mobile" && sidebar.value.opened) {
+ useAppStore().closeSideBar({ withoutAnimation: false });
+ }
+ }
+ );
+
+ watchEffect(() => {
+ if (width.value - 1 < WIDTH) {
+ useAppStore().toggleDevice("mobile");
+ useAppStore().closeSideBar({ withoutAnimation: true });
+ } else {
+ useAppStore().toggleDevice("desktop");
+ }
+ });
+
+ function handleClickOutside() {
+ useAppStore().closeSideBar({ withoutAnimation: false });
+ }
+
+ const settingRef = ref(null);
+ function setLayout() {
+ settingRef.value.openSetting();
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "@/assets/styles/mixin.scss";
+ @import "@/assets/styles/variables.module.scss";
+
+ .app-wrapper {
+ @include clearfix;
+ position: relative;
+ height: 100%;
+ width: 100%;
+ background: radial-gradient(
+ circle at top,
+ rgba(223, 232, 226, 0.95),
+ transparent 32%
+ ),
+ linear-gradient(180deg, #f7faf8 0%, var(--app-bg) 100%);
+
+ &.mobile.openSidebar {
+ position: fixed;
+ top: 0;
+ }
+ }
+
+ .drawer-bg {
+ background: #000;
+ opacity: 0.3;
+ width: 100%;
+ top: 0;
+ height: 100%;
+ position: absolute;
+ z-index: 999;
+ }
+
+ .fixed-header {
+ position: fixed;
+ top: 0px;
+ padding-top: 12px;
+ right: 16px;
+ z-index: 9;
+ width: calc(100% - #{$base-sidebar-width} - 32px);
+ transition: width 0.28s, right 0.28s;
+ padding-bottom: 8px;
+ background-color: #f3f6f4;
+ }
+ .hideSidebar .fixed-header {
+ width: calc(100% - 100px);
+ }
+
+ .sidebarHide .fixed-header {
+ width: calc(100% - 32px);
+ }
+
+ .mobile .fixed-header {
+ width: 100%;
+ }
+</style>
diff --git a/src/views/fileManagement/borrow/index.vue b/src/views/fileManagement/borrow/index.vue
index 8983665..c7d8e0d 100644
--- a/src/views/fileManagement/borrow/index.vue
+++ b/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,13 +435,16 @@
if (type === "edit") {
// 缂栬緫妯″紡锛屽姞杞界幇鏈夋暟鎹�
Object.assign(borrowForm, data);
+ // 瀛樺偍鏂囨。鍚嶇О鐢ㄤ簬鏄剧ず
+ currentEditDocName.value = data.docName || '';
} else {
// 鏂板妯″紡锛屾竻绌鸿〃鍗�
Object.keys(borrowForm).forEach(key => {
borrowForm[key] = "";
});
- // 璁剧疆榛樿鐘舵��
- borrowForm.borrowStatus = "鍊熼槄";
+ currentEditDocName.value = ''; // 娓呯┖缂栬緫鏃剁殑鏂囨。鍚嶇О
+ // 璁剧疆榛樿鐘舵��
+ borrowForm.borrowStatus = "鍊熼槄";
// 璁剧疆褰撳墠鏃ユ湡涓哄�熼槄鏃ユ湡
borrowForm.borrowDate = new Date().toISOString().split('T')[0];
}
@@ -445,6 +455,7 @@
proxy.$refs.borrowFormRef.resetFields();
borrowDia.value = false;
scanContent.value = ''; // 娓呯┖鎵爜鍐呭
+ currentEditDocName.value = ''; // 娓呯┖缂栬緫鏃剁殑鏂囨。鍚嶇О
};
// 鎻愪氦鍊熼槄琛ㄥ崟
diff --git a/src/views/fileManagement/document/index.vue b/src/views/fileManagement/document/index.vue
index c31b044..f4eac2d 100644
--- a/src/views/fileManagement/document/index.vue
+++ b/src/views/fileManagement/document/index.vue
@@ -862,12 +862,14 @@
documentForm[key] = "";
});
documentForm.attachments = []; // 鏂板妯″紡涓嬩篃娓呯┖闄勪欢
- // 璁剧疆榛樿鍊� - 浣跨敤瀛楀吀鏁版嵁鐨勭涓�涓�夐」浣滀负榛樿鍊�
+ // 璁剧疆榛樿鍊� - 鏂囨。鐘舵�侀粯璁よ缃负"姝e父"
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 === '姝e父');
+ 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;
}
}
};
diff --git a/src/views/fileManagement/return/index.vue b/src/views/fileManagement/return/index.vue
index bc86c56..097ab29 100644
--- a/src/views/fileManagement/return/index.vue
+++ b/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 = ''; // 娓呯┖缂栬緫鏃剁殑鏂囨。鍚嶇О
};
// 鎻愪氦褰掕繕琛ㄥ崟
diff --git a/src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue b/src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue
index a83ff6a..e9b2646 100644
--- a/src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue
+++ b/src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue
@@ -35,16 +35,30 @@
<el-button type="primary"
link
@click="handleViewSupplementRecord(row)">
- {{ row.supplementQty ?? 0 }}
+ {{ row.feedingQty ?? 0 }}
</el-button>
</template>
</el-table-column>
<el-table-column label="閫�鏂欐暟閲�"
- prop="returnQty"
- min-width="110" />
+ min-width="110">
+ <template #default="{ row }">
+ {{ row.returnQty ?? 0 }}
+ </template>
+ </el-table-column>
<el-table-column label="瀹為檯鏁伴噺"
- prop="actualQty"
- min-width="110" />
+ min-width="140">
+ <template #default="{ row }">
+ <el-input-number v-model="row.actualQty"
+ :min="0"
+ :precision="3"
+ :step="1"
+ controls-position="right"
+ placeholder="杈撳叆瀹為檯鏁伴噺"
+ style="width: 100%;"
+ :disabled="row.returned"
+ @change="val => handleActualQtyChange(row, val)" />
+ </template>
+ </el-table-column>
</el-table>
<template #footer>
<span class="dialog-footer">
@@ -66,7 +80,7 @@
border
row-key="id">
<el-table-column label="琛ユ枡鏁伴噺"
- prop="supplementQty"
+ prop="pickQuantity"
min-width="120" />
<el-table-column label="琛ユ枡浜�"
prop="supplementUserName"
@@ -75,7 +89,7 @@
prop="supplementTime"
min-width="160" />
<el-table-column label="琛ユ枡鍘熷洜"
- prop="supplementReason"
+ prop="feedingReason"
min-width="200" />
</el-table>
<template #footer>
@@ -121,7 +135,7 @@
import {
listMaterialPickingDetail,
listMaterialSupplementRecord,
- confirmMaterialReturn,
+ updateMaterialPickingLedger,
} from "@/api/productionManagement/productionOrder.js";
const props = defineProps({
@@ -145,10 +159,12 @@
const returnSummaryList = ref([]);
const calcReturnQty = item =>
Number(item.pickQuantity || 0) +
- Number(item.supplementQty || 0) -
+ Number(item.feedingQty || 0) -
Number(item.actualQty || 0);
const canOpenReturnSummary = computed(() =>
- materialDetailTableData.value.some(item => calcReturnQty(item) > 0)
+ materialDetailTableData.value.some(
+ item => item.returned !== true && calcReturnQty(item) > 0
+ )
);
const loadDetailList = async () => {
@@ -157,7 +173,13 @@
materialDetailTableData.value = [];
try {
const res = await listMaterialPickingDetail(props.orderRow.id);
- materialDetailTableData.value = res.data || [];
+ materialDetailTableData.value = (res.data || []).map(item => ({
+ ...item,
+ actualQty:
+ item.actualQty ??
+ Number(item.pickQuantity || 0) + Number(item.feedingQty || 0),
+ returnQty: item.returnQty ?? 0,
+ }));
} finally {
materialDetailLoading.value = false;
}
@@ -176,6 +198,10 @@
materialDetailTableData.value = [];
};
+ const handleActualQtyChange = (row, val) => {
+ row.returnQty = calcReturnQty(row);
+ };
+
const handleViewSupplementRecord = async row => {
if (!row?.id) return;
supplementRecordDialogVisible.value = true;
@@ -183,7 +209,8 @@
supplementRecordTableData.value = [];
try {
const res = await listMaterialSupplementRecord({
- materialDetailId: row.id,
+ pickId: row.id,
+ productionOrderId: props.orderRow.id,
});
supplementRecordTableData.value = res.data || [];
} finally {
@@ -225,9 +252,24 @@
if (!props.orderRow?.id) return;
materialReturnConfirming.value = true;
try {
- await confirmMaterialReturn({
- orderId: props.orderRow.id,
- returnSummaryList: returnSummaryList.value,
+ await updateMaterialPickingLedger({
+ productionOrderId: props.orderRow.id,
+ productionOrderPickDto: materialDetailTableData.value.map(item => ({
+ id: item.id,
+ technologyOperationId: item.technologyOperationId,
+ operationName: item.operationName,
+ bom: item.bom === true,
+ productModelId: item.productModelId,
+ demandedQuantity: item.demandedQuantity,
+ unit: item.unit,
+ pickQuantity: item.pickQuantity,
+ batchNo: item.batchNo,
+ feedingQty: item.feedingQty,
+ returnQty: item.returnQty,
+ actualQty: item.actualQty,
+ feedingReason: item.feedingReason,
+ returned: true,
+ })),
});
returnSummaryDialogVisible.value = false;
dialogVisible.value = false;
diff --git a/src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue b/src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue
new file mode 100644
index 0000000..67e44f1
--- /dev/null
+++ b/src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue
@@ -0,0 +1,159 @@
+<template>
+ <el-dialog v-model="dialogVisible"
+ title="琛ユ枡"
+ width="1200px"
+ @close="handleClose">
+ <el-table v-loading="loading"
+ :data="tableData"
+ border
+ row-key="id">
+ <el-table-column label="宸ュ簭鍚嶇О"
+ prop="operationName"
+ min-width="140" />
+ <el-table-column label="鍘熸枡鍚嶇О"
+ prop="productName"
+ min-width="140" />
+ <el-table-column label="鍘熸枡鍨嬪彿"
+ prop="model"
+ min-width="140" />
+ <el-table-column label="璁¢噺鍗曚綅"
+ prop="unit"
+ width="100" />
+ <el-table-column label="棰嗙敤鏁伴噺"
+ prop="pickQuantity"
+ width="100" />
+ <el-table-column label="琛ユ枡鏁伴噺"
+ min-width="150">
+ <template #default="{ row }">
+ <el-input-number v-model="row.newSupplementQty"
+ :min="0"
+ :precision="3"
+ :step="1"
+ controls-position="right"
+ placeholder="杈撳叆琛ユ枡鏁伴噺"
+ style="width: 100%;" />
+ </template>
+ </el-table-column>
+ <el-table-column label="琛ユ枡鍘熷洜"
+ min-width="200">
+ <template #default="{ row }">
+ <el-input v-model="row.newSupplementReason"
+ placeholder="杈撳叆琛ユ枡鍘熷洜"
+ maxlength="200"
+ show-word-limit />
+ </template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ :loading="submitting"
+ @click="handleSubmit">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </span>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+ import { computed, ref, watch } from "vue";
+ import { ElMessage } from "element-plus";
+ import {
+ listMaterialPickingDetail,
+ updateMaterialPickingLedger,
+ } from "@/api/productionManagement/productionOrder.js";
+
+ const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ orderRow: { type: Object, default: null },
+ });
+ const emit = defineEmits(["update:modelValue", "saved"]);
+
+ const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: val => emit("update:modelValue", val),
+ });
+
+ const loading = ref(false);
+ const submitting = ref(false);
+ const tableData = ref([]);
+
+ const loadData = async () => {
+ if (!props.orderRow?.id) return;
+ loading.value = true;
+ try {
+ const res = await listMaterialPickingDetail(props.orderRow.id);
+ tableData.value = (res.data || []).map(item => ({
+ ...item,
+ newSupplementQty: 0,
+ newSupplementReason: "",
+ }));
+ } catch (e) {
+ console.error("鑾峰彇鐗╂枡鏄庣粏澶辫触锛�", e);
+ ElMessage.error("鑾峰彇鐗╂枡鏄庣粏澶辫触");
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ watch(
+ () => dialogVisible.value,
+ visible => {
+ if (visible) {
+ loadData();
+ }
+ }
+ );
+
+ const handleClose = () => {
+ tableData.value = [];
+ };
+
+ const handleSubmit = async () => {
+ const supplementList = tableData.value.filter(
+ item => item.newSupplementQty > 0
+ );
+ if (supplementList.length === 0) {
+ ElMessage.warning("璇疯嚦灏戣緭鍏ヤ竴鏉¤ˉ鏂欐暟閲�");
+ return;
+ }
+
+ const invalidRow = supplementList.find(item => !item.newSupplementReason);
+ if (invalidRow) {
+ ElMessage.warning("璇疯緭鍏ヨˉ鏂欏師鍥�");
+ return;
+ }
+
+ submitting.value = true;
+ try {
+ await updateMaterialPickingLedger({
+ productionOrderId: props.orderRow.id,
+ productionOrderPickDto: tableData.value.map(item => ({
+ id: item.id,
+ technologyOperationId: item.technologyOperationId,
+ operationName: item.operationName,
+ bom: item.bom === true,
+ productModelId: item.productModelId,
+ demandedQuantity: item.demandedQuantity,
+ unit: item.unit,
+ pickQuantity: item.pickQuantity,
+ batchNo: item.batchNo,
+ feedingQuantity: item.newSupplementQty || 0,
+ feedingReason: item.newSupplementReason || "",
+ pickType: 2,
+ })),
+ });
+ ElMessage.success("琛ユ枡鎴愬姛");
+ dialogVisible.value = false;
+ emit("saved");
+ } catch (e) {
+ console.error("琛ユ枡澶辫触锛�", e);
+ ElMessage.error("琛ユ枡澶辫触");
+ } finally {
+ submitting.value = false;
+ }
+ };
+</script>
+
+<style scoped lang="scss">
+</style>
diff --git a/src/views/productionManagement/productionOrder/index.vue b/src/views/productionManagement/productionOrder/index.vue
index f348aae..5f4fa61 100644
--- a/src/views/productionManagement/productionOrder/index.vue
+++ b/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,
diff --git a/src/views/productionManagement/productionProcess/Edit.vue b/src/views/productionManagement/productionProcess/Edit.vue
index e69de29..28077b6 100644
--- a/src/views/productionManagement/productionProcess/Edit.vue
+++ b/src/views/productionManagement/productionProcess/Edit.vue
@@ -0,0 +1,168 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ title="缂栬緫宸ュ簭"
+ width="400"
+ @close="closeModal"
+ >
+ <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
+ <el-form-item
+ label="宸ュ簭鍚嶇О锛�"
+ prop="name"
+ :rules="[
+ {
+ required: true,
+ message: '璇疯緭鍏ュ伐搴忓悕绉�',
+ },
+ {
+ max: 100,
+ message: '鏈�澶�100涓瓧绗�',
+ }
+ ]">
+ <el-input v-model="formState.name" />
+ </el-form-item>
+ <el-form-item label="宸ュ簭缂栧彿" prop="no">
+ <el-input v-model="formState.no" />
+ </el-form-item>
+ <el-form-item
+ label="宸ュ簭绫诲瀷"
+ prop="type"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨宸ュ簭绫诲瀷',
+ }
+ ]"
+ >
+ <el-select v-model="formState.type" placeholder="璇烽�夋嫨宸ュ簭绫诲瀷">
+ <el-option label="璁℃椂" :value="0" />
+ <el-option label="璁′欢" :value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="宸ヨ祫瀹氶" prop="salaryQuota">
+ <el-input v-model="formState.salaryQuota" type="number" :step="0.001" />
+ </el-form-item>
+ <el-form-item label="鏄惁璐ㄦ" prop="isQuality">
+ <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="鏄惁鍏ュ簱" prop="inbound">
+ <el-switch v-model="formState.inbound" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="鏄惁鎶ュ伐" prop="reportWork">
+ <el-switch v-model="formState.reportWork" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formState.remark" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, getCurrentInstance, watch } from "vue";
+import {update} from "@/api/productionManagement/productionProcess.js";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+
+ record: {
+ type: Object,
+ required: true,
+ }
+});
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+// 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
+const formState = ref({
+ id: props.record.id,
+ name: props.record.name,
+ type: props.record.type,
+ no: props.record.no,
+ remark: props.record.remark,
+ salaryQuota: props.record.salaryQuota,
+ isQuality: props.record.isQuality,
+ inbound: props.record.inbound,
+ reportWork: props.record.reportWork,
+});
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+// 鐩戝惉 record 鍙樺寲锛屾洿鏂拌〃鍗曟暟鎹�
+watch(() => props.record, (newRecord) => {
+ if (newRecord && isShow.value) {
+ formState.value = {
+ id: newRecord.id,
+ name: newRecord.name || '',
+ no: newRecord.no || '',
+ type: newRecord.type,
+ remark: newRecord.remark || '',
+ salaryQuota: newRecord.salaryQuota || '',
+ isQuality: props.record.isQuality,
+ inbound: newRecord.inbound,
+ reportWork: newRecord.reportWork,
+ };
+ }
+}, { immediate: true, deep: true });
+
+// 鐩戝惉寮圭獥鎵撳紑锛岄噸鏂板垵濮嬪寲琛ㄥ崟鏁版嵁
+watch(() => props.visible, (visible) => {
+ if (visible && props.record) {
+ formState.value = {
+ id: props.record.id,
+ name: props.record.name || '',
+ no: props.record.no || '',
+ type: props.record.type,
+ remark: props.record.remark || '',
+ salaryQuota: props.record.salaryQuota || '',
+ isQuality: props.record.isQuality,
+ inbound: props.record.inbound,
+ reportWork: props.record.reportWork,
+ };
+ }
+});
+
+let { proxy } = getCurrentInstance()
+
+const closeModal = () => {
+ isShow.value = false;
+};
+
+const handleSubmit = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ update(formState.value).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ })
+ }
+ })
+};
+
+defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow,
+});
+</script>
diff --git a/src/views/productionManagement/productionProcess/New.vue b/src/views/productionManagement/productionProcess/New.vue
index e69de29..0b3fd47 100644
--- a/src/views/productionManagement/productionProcess/New.vue
+++ b/src/views/productionManagement/productionProcess/New.vue
@@ -0,0 +1,129 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ title="鏂板宸ュ簭"
+ width="400"
+ @close="closeModal"
+ >
+ <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
+ <el-form-item
+ label="宸ュ簭鍚嶇О锛�"
+ prop="name"
+ :rules="[
+ {
+ required: true,
+ message: '璇疯緭鍏ュ伐搴忓悕绉�',
+ },
+ {
+ max: 100,
+ message: '鏈�澶�100涓瓧绗�',
+ }
+ ]">
+ <el-input v-model="formState.name" />
+ </el-form-item>
+ <el-form-item label="宸ュ簭缂栧彿" prop="no">
+ <el-input v-model="formState.no" />
+ </el-form-item>
+ <el-form-item
+ label="宸ュ簭绫诲瀷"
+ prop="type"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨宸ュ簭绫诲瀷',
+ }
+ ]"
+ >
+ <el-select v-model="formState.type" placeholder="璇烽�夋嫨宸ュ簭绫诲瀷">
+ <el-option label="璁℃椂" :value="0" />
+ <el-option label="璁′欢" :value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="宸ヨ祫瀹氶" prop="salaryQuota">
+ <el-input v-model="formState.salaryQuota" type="number" :step="0.001">
+ <template #append>鍏�</template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="鏄惁璐ㄦ" prop="isQuality">
+ <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="鏄惁鍏ュ簱" prop="inbound">
+ <el-switch v-model="formState.inbound" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="鏄惁鎶ュ伐" prop="reportWork">
+ <el-switch v-model="formState.reportWork" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formState.remark" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, getCurrentInstance } from "vue";
+import {add} from "@/api/productionManagement/productionProcess.js";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+});
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+// 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
+const formState = ref({
+ name: '',
+ type: undefined,
+ remark: '',
+ salaryQuota: '',
+ isQuality: false,
+ inbound: false,
+ reportWork: false,
+});
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+let { proxy } = getCurrentInstance()
+
+const closeModal = () => {
+ isShow.value = false;
+};
+
+const handleSubmit = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ add(formState.value).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ })
+ }
+ })
+};
+
+defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow,
+});
+</script>
diff --git a/src/views/productionManagement/workOrderEdit/index.vue b/src/views/productionManagement/workOrderEdit/index.vue
index 52a36cd..b919973 100644
--- a/src/views/productionManagement/workOrderEdit/index.vue
+++ b/src/views/productionManagement/workOrderEdit/index.vue
@@ -4,74 +4,66 @@
<div class="search-row">
<div class="search-item">
<span class="search_title">宸ュ崟缂栧彿锛�</span>
- <el-input
- v-model="searchForm.workOrderNo"
- style="width: 240px"
- placeholder="璇疯緭鍏�"
- @change="handleQuery"
- clearable
- prefix-icon="Search"
- />
+ <el-input v-model="searchForm.workOrderNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ prefix-icon="Search" />
</div>
<div class="search-item">
<span class="search_title">鐢熶骇璁㈠崟鍙凤細</span>
- <el-input
- v-model="searchForm.productOrderNpsNo"
- style="width: 240px"
- placeholder="璇疯緭鍏�"
- @change="handleQuery"
- clearable
- prefix-icon="Search"
- />
+ <el-input v-model="searchForm.productOrderNpsNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ prefix-icon="Search" />
</div>
<div class="search-item">
- <el-button type="primary" @click="handleQuery">鎼滅储</el-button>
+ <el-button type="primary"
+ @click="handleQuery">鎼滅储</el-button>
</div>
</div>
</div>
<div class="table_list">
- <PIMTable
- rowKey="id"
- :column="tableColumn"
- :tableData="tableData"
- :page="page"
- :tableLoading="tableLoading"
- @pagination="pagination"
- >
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :tableLoading="tableLoading"
+ @pagination="pagination">
<template #completionStatus="{ row }">
- <el-progress
- :percentage="toProgressPercentage(row?.completionStatus)"
- :color="progressColor(toProgressPercentage(row?.completionStatus))"
- :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''"
- />
+ <el-progress :percentage="toProgressPercentage(row?.completionStatus)"
+ :color="progressColor(toProgressPercentage(row?.completionStatus))"
+ :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" />
</template>
</PIMTable>
</div>
-
- <el-dialog v-model="editDialogVisible" title="缂栬緫璁″垝鏃堕棿" width="500px">
- <el-form :model="editrow" label-width="120px">
+ <el-dialog v-model="editDialogVisible"
+ title="缂栬緫璁″垝鏃堕棿"
+ width="500px">
+ <el-form :model="editrow"
+ label-width="120px">
<el-form-item label="璁″垝寮�濮嬫椂闂�">
- <el-date-picker
- v-model="editrow.planStartTime"
- type="date"
- placeholder="璇烽�夋嫨"
- value-format="YYYY-MM-DD"
- style="width: 300px"
- />
+ <el-date-picker v-model="editrow.planStartTime"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ value-format="YYYY-MM-DD"
+ style="width: 300px" />
</el-form-item>
<el-form-item label="璁″垝缁撴潫鏃堕棿">
- <el-date-picker
- v-model="editrow.planEndTime"
- type="date"
- placeholder="璇烽�夋嫨"
- value-format="YYYY-MM-DD"
- style="width: 300px"
- />
+ <el-date-picker v-model="editrow.planEndTime"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ value-format="YYYY-MM-DD"
+ style="width: 300px" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
- <el-button type="primary" @click="handleUpdate">纭畾</el-button>
+ <el-button type="primary"
+ @click="handleUpdate">纭畾</el-button>
<el-button @click="editDialogVisible = false">鍙栨秷</el-button>
</span>
</template>
@@ -80,200 +72,201 @@
</template>
<script setup>
-import { getCurrentInstance, onMounted, reactive, ref, toRefs } from "vue";
-import { ElMessageBox } from "element-plus";
-import {
- productWorkOrderPage,
- updateProductWorkOrder,
-} from "@/api/productionManagement/workOrder.js";
+ import { getCurrentInstance, onMounted, reactive, ref, toRefs } from "vue";
+ import { ElMessageBox } from "element-plus";
+ import {
+ productWorkOrderPage,
+ updateProductWorkOrder,
+ } from "@/api/productionManagement/workOrder.js";
-const { proxy } = getCurrentInstance();
+ const { proxy } = getCurrentInstance();
-const tableColumn = ref([
- {
- label: "宸ュ崟绫诲瀷",
- prop: "workOrderType",
- width: "80",
- },
- {
- label: "宸ュ崟缂栧彿",
- prop: "workOrderNo",
- width: "140",
- },
- {
- label: "鐢熶骇璁㈠崟鍙�",
- prop: "productOrderNpsNo",
- width: "140",
- },
- {
- label: "浜у搧鍚嶇О",
- prop: "productName",
- width: "140",
- },
- {
- label: "瑙勬牸",
- prop: "model",
- },
- {
- label: "鍗曚綅",
- prop: "unit",
- },
- {
- label: "宸ュ簭鍚嶇О",
- prop: "processName",
- },
- {
- label: "闇�姹傛暟閲�",
- prop: "planQuantity",
- width: "140",
- },
- {
- label: "瀹屾垚鏁伴噺",
- prop: "completeQuantity",
- width: "140",
- },
- {
- label: "瀹屾垚杩涘害",
- prop: "completionStatus",
- dataType: "slot",
- slot: "completionStatus",
- width: "140",
- },
- {
- label: "璁″垝寮�濮嬫椂闂�",
- prop: "planStartTime",
- width: "140",
- },
- {
- label: "璁″垝缁撴潫鏃堕棿",
- prop: "planEndTime",
- width: "140",
- },
- {
- label: "瀹為檯寮�濮嬫椂闂�",
- prop: "actualStartTime",
- width: "140",
- },
- {
- label: "瀹為檯缁撴潫鏃堕棿",
- prop: "actualEndTime",
- width: "140",
- },
- {
- label: "鎿嶄綔",
- width: "100",
- align: "center",
- dataType: "action",
- fixed: "right",
- operation: [
- {
- name: "璁″垝鏃堕棿",
- clickFun: row => {
- handleEdit(row);
+ const tableColumn = ref([
+ {
+ label: "宸ュ崟绫诲瀷",
+ prop: "workOrderType",
+ width: "80",
+ },
+ {
+ label: "宸ュ崟缂栧彿",
+ prop: "workOrderNo",
+ width: "140",
+ },
+ {
+ label: "鐢熶骇璁㈠崟鍙�",
+ prop: "npsNo",
+ width: "140",
+ },
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+ width: "140",
+ },
+ {
+ label: "瑙勬牸",
+ prop: "model",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "宸ュ簭鍚嶇О",
+ prop: "operationName",
+ width: "100",
+ },
+ {
+ label: "闇�姹傛暟閲�",
+ prop: "planQuantity",
+ width: "140",
+ },
+ {
+ label: "瀹屾垚鏁伴噺",
+ prop: "completeQuantity",
+ width: "140",
+ },
+ {
+ label: "瀹屾垚杩涘害",
+ prop: "completionStatus",
+ dataType: "slot",
+ slot: "completionStatus",
+ width: "140",
+ },
+ {
+ label: "璁″垝寮�濮嬫椂闂�",
+ prop: "planStartTime",
+ width: "140",
+ },
+ {
+ label: "璁″垝缁撴潫鏃堕棿",
+ prop: "planEndTime",
+ width: "140",
+ },
+ {
+ label: "瀹為檯寮�濮嬫椂闂�",
+ prop: "actualStartTime",
+ width: "140",
+ },
+ {
+ label: "瀹為檯缁撴潫鏃堕棿",
+ prop: "actualEndTime",
+ width: "140",
+ },
+ {
+ label: "鎿嶄綔",
+ width: "100",
+ align: "center",
+ dataType: "action",
+ fixed: "right",
+ operation: [
+ {
+ name: "璁″垝鏃堕棿",
+ clickFun: row => {
+ handleEdit(row);
+ },
},
- },
- ],
- },
-]);
+ ],
+ },
+ ]);
-const tableData = ref([]);
-const tableLoading = ref(false);
-const editDialogVisible = ref(false);
-const editrow = ref(null);
-const page = reactive({
- current: 1,
- size: 100,
- total: 0,
-});
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const editDialogVisible = ref(false);
+ const editrow = ref(null);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
-const data = reactive({
- searchForm: {
- workOrderNo: "",
- productOrderNpsNo: "",
- },
-});
-const { searchForm } = toRefs(data);
+ const data = reactive({
+ searchForm: {
+ workOrderNo: "",
+ productOrderNpsNo: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
-const toProgressPercentage = val => {
- const n = Number(val);
- if (!Number.isFinite(n)) return 0;
- if (n <= 0) return 0;
- if (n >= 100) return 100;
- return Math.round(n);
-};
+ const toProgressPercentage = val => {
+ const n = Number(val);
+ if (!Number.isFinite(n)) return 0;
+ if (n <= 0) return 0;
+ if (n >= 100) return 100;
+ return Math.round(n);
+ };
-const progressColor = percentage => {
- const p = toProgressPercentage(percentage);
- if (p < 30) return "#f56c6c";
- if (p < 50) return "#e6a23c";
- if (p < 80) return "#409eff";
- return "#67c23a";
-};
+ const progressColor = percentage => {
+ const p = toProgressPercentage(percentage);
+ if (p < 30) return "#f56c6c";
+ if (p < 50) return "#e6a23c";
+ if (p < 80) return "#409eff";
+ return "#67c23a";
+ };
-const handleQuery = () => {
- page.current = 1;
- getList();
-};
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
-const pagination = obj => {
- page.current = obj.page;
- page.size = obj.limit;
- getList();
-};
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
-const getList = () => {
- tableLoading.value = true;
- const params = { ...searchForm.value, ...page };
- productWorkOrderPage(params)
- .then(res => {
- tableLoading.value = false;
- tableData.value = res.data.records;
- page.total = res.data.total;
- })
- .catch(() => {
- tableLoading.value = false;
- });
-};
-
-const handleEdit = row => {
- editrow.value = JSON.parse(JSON.stringify(row));
- editDialogVisible.value = true;
-};
-
-const handleUpdate = () => {
- updateProductWorkOrder(editrow.value)
- .then(() => {
- proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
- editDialogVisible.value = false;
- getList();
- })
- .catch(() => {
- ElMessageBox.alert("淇敼澶辫触", "鎻愮ず", {
- confirmButtonText: "纭畾",
+ const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ productWorkOrderPage(params)
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+ .catch(() => {
+ tableLoading.value = false;
});
- });
-};
+ };
-onMounted(() => {
- getList();
-});
+ const handleEdit = row => {
+ editrow.value = JSON.parse(JSON.stringify(row));
+ editDialogVisible.value = true;
+ };
+
+ const handleUpdate = () => {
+ updateProductWorkOrder(editrow.value)
+ .then(() => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ editDialogVisible.value = false;
+ getList();
+ })
+ .catch(() => {
+ ElMessageBox.alert("淇敼澶辫触", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ });
+ };
+
+ onMounted(() => {
+ getList();
+ });
</script>
<style scoped lang="scss">
-.search-row {
- display: flex;
- align-items: center;
- gap: 12px;
-}
+ .search-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
-.search-item {
- display: flex;
- align-items: center;
-}
+ .search-item {
+ display: flex;
+ align-items: center;
+ }
-.search_title {
- margin-right: 8px;
- font-size: 14px;
- color: #606266;
-}
+ .search_title {
+ margin-right: 8px;
+ font-size: 14px;
+ color: #606266;
+ }
</style>
diff --git a/src/views/productionManagement/workOrderManagement/index.vue b/src/views/productionManagement/workOrderManagement/index.vue
index 48f8839..819e3fd 100644
--- a/src/views/productionManagement/workOrderManagement/index.vue
+++ b/src/views/productionManagement/workOrderManagement/index.vue
@@ -40,7 +40,6 @@
</template>
</PIMTable>
</div>
-
<!-- 娴佽浆鍗″脊绐� -->
<el-dialog v-model="transferCardVisible"
title="娴佽浆鍗�"
@@ -116,7 +115,6 @@
@click="printTransferCard">鎵撳嵃娴佽浆鍗�</el-button>
</div>
</el-dialog>
-
<!-- 鎶ュ伐寮圭獥 -->
<el-dialog v-model="reportDialogVisible"
title="鎶ュ伐"
@@ -172,13 +170,9 @@
</span>
</template>
</el-dialog>
-
- <MaterialDialog
- v-model="materialDialogVisible"
- :row-data="currentMaterialOrderRow"
- @refresh="getList"
- />
-
+ <MaterialDialog v-model="materialDialogVisible"
+ :row-data="currentMaterialOrderRow"
+ @refresh="getList" />
<FilesDia ref="workOrderFilesRef" />
</div>
</template>
@@ -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 => {
@@ -304,7 +298,7 @@
],
},
]);
-
+
const tableData = ref([]);
const tableLoading = ref(false);
const transferCardVisible = ref(false);
@@ -416,7 +410,7 @@
// 鏈夋晥鐨勯潪璐熸暣鏁帮紙鍖呮嫭0锛�
reportForm.scrapQty = num;
};
-
+
const currentReportRowData = ref(null);
const materialDialogVisible = ref(false);
const currentMaterialOrderRow = ref(null);
@@ -454,13 +448,13 @@
page.current = 1;
getList();
};
-
+
const pagination = obj => {
page.current = obj.page;
page.size = obj.limit;
getList();
};
-
+
const getList = () => {
tableLoading.value = true;
const params = { ...searchForm.value, ...page };
diff --git a/vite.config.js b/vite.config.js
index dc687a8..ac18ec5 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -8,7 +8,7 @@
const { VITE_APP_ENV } = env;
const baseUrl =
env.VITE_APP_ENV === "development"
- ? "http://1.15.17.182:9003"
+ ? "http://localhost:7005"
: env.VITE_BASE_API;
const javaUrl =
env.VITE_APP_ENV === "development"
--
Gitblit v1.9.3