huminmin
2026-04-28 27af440fbce012aefbd1023f23762cf925e16fab
Merge branch 'dev_NEW_pro' of http://114.132.189.42:9002/r/product-inventory-management into dev_NEW_pro
已添加2个文件
已修改11个文件
2580 ■■■■ 文件已修改
src/components/AIChatSidebar/index.vue 1206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/index.vue 264 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fileManagement/borrow/index.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fileManagement/document/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fileManagement/return/index.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue 72 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/Edit.vue 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/New.vue 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderEdit/index.vue 447 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/index.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .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
@@ -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>
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 = ''; // æ¸…空编辑时的文档名称
};
// æäº¤å€Ÿé˜…表单
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"
            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>
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 };
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"