huminmin
2 天以前 4a407279f0c9757f0714eaf385fdd5cd68c038c2
Merge branch 'dev_NEW_pro' of http://114.132.189.42:9002/r/product-inventory-management into dev_NEW_pro
已添加1个文件
已修改10个文件
1559 ■■■■ 文件已修改
src/api/viewIndex.js 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/purchaseAssistant.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/index.vue 171 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/user.js 133 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue 680 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue 335 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/index.vue 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/viewIndex.js
@@ -347,18 +347,30 @@
  });
};
const HOME_PROGRESS_STATUS_LIST = ["all", "waiting", "inProgress", "completed", "paused", "1", "2", "3", "4"];
const HOME_PROGRESS_TAB_LIST = ["all", "inProgress", "completed", "paused"];
const HOME_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
const normalizeDateParam = (value) => {
  const dateText = typeof value === "string" ? value.trim() : "";
  return HOME_DATE_PATTERN.test(dateText) ? dateText : undefined;
};
export const productionOrderProgress = (params = {}) => {
  const safePageNum = Math.max(1, Number(params.pageNum || 1));
  const safePageSize = Math.min(50, Math.max(1, Number(params.pageSize || 10)));
  const safeTab = ["all", "inProgress", "completed", "paused"].includes(params.tab)
    ? params.tab
    : "all";
  const rawStatus = String(params.status ?? "").trim();
  const safeStatus = HOME_PROGRESS_STATUS_LIST.includes(rawStatus) ? rawStatus : undefined;
  const safeTab = HOME_PROGRESS_TAB_LIST.includes(params.tab) ? params.tab : "all";
  const normalizedTab = safeStatus && HOME_PROGRESS_TAB_LIST.includes(safeStatus) ? safeStatus : safeTab;
  return request({
    url: "/home/productionOrderProgress",
    method: "get",
    params: {
      ...params,
      tab: safeTab,
      status: safeStatus,
      tab: normalizedTab,
      bizDate: normalizeDateParam(params.bizDate),
      pageNum: safePageNum,
      pageSize: safePageSize,
    },
@@ -376,6 +388,7 @@
    params: {
      ...params,
      limit: safeLimit,
      planDate: normalizeDateParam(params.planDate),
    },
    headers: {
      handleAuthError: false,
src/components/AIChatSidebar/assistants/purchaseAssistant.js
@@ -19,7 +19,7 @@
    '本月采购金额排名前十的物料有哪些?',
    '哪些采购订单还未入库?',
    '最近7天供应商到货异常有哪些?',
    '帮我统计待付款采购单',
    '帮我统计待付款采购单!',
    '列出本月采购退货情况'
  ]
}
src/components/AIChatSidebar/index.vue
@@ -489,6 +489,56 @@
                  </div>
                </div>
                <div v-if="message.purchaseData" class="sales-structured-card">
                  <div class="sales-structured-card__title">{{ getPurchaseTypeLabel(message.type) }}</div>
                  <div v-if="message.purchaseData.summaryEntries?.length" class="sales-summary-grid">
                    <div
                        v-for="(entry, entryIndex) in message.purchaseData.summaryEntries"
                        :key="`purchase-summary-${entry.key}-${entryIndex}`"
                        class="sales-summary-item"
                    >
                      <span class="sales-summary-label">{{ entry.label }}</span>
                      <strong class="sales-summary-value">{{ entry.value }}</strong>
                    </div>
                  </div>
                  <div
                      v-if="message.purchaseData.listItems?.length && message.purchaseData.columns?.length"
                      class="table-wrapper manufacturing-table-wrapper"
                  >
                    <el-table :data="message.purchaseData.listItems" border stripe size="small" style="width: 100%">
                      <el-table-column
                          v-for="col in message.purchaseData.columns"
                          :key="col"
                          :label="getStructuredFieldLabel(col)"
                          min-width="140"
                          show-overflow-tooltip
                      >
                        <template #default="{ row }">
                          {{ formatStructuredValue(row[col]) }}
                        </template>
                      </el-table-column>
                    </el-table>
                  </div>
                </div>
                <div v-if="message.purchaseIntentData?.quickPrompts?.length" class="purchase-intent-quick-prompt-wrap">
                  <div class="purchase-intent-quick-prompt-title">试试以下提问</div>
                  <div class="quick-prompt-list purchase-intent-quick-prompt-list">
                    <button
                        v-for="prompt in message.purchaseIntentData.quickPrompts"
                        :key="`purchase-intent-${prompt}`"
                        type="button"
                        class="quick-prompt-btn"
                        :disabled="isSending"
                        @click="sendQuickPrompt(prompt)"
                    >
                      {{ prompt }}
                    </button>
                  </div>
                </div>
                <div v-if="message.purchaseAnalysisData" class="purchase-confirm-card">
                  <div class="purchase-confirm-header">
                    <span>{{ businessTypeLabelMap[message.purchaseAnalysisData.businessType] || message.purchaseAnalysisData.businessType || '采购业务' }}</span>
@@ -906,6 +956,10 @@
  sales_customer_churn_risk: '客户流失风险分析',
  sales_collection_quote_strategy: '回款与报价策略建议'
}
const purchaseTypeLabelMap = {
  purchase_material_rank: '采购物料金额排行',
  purchase_pending_payment_list: '待付款采购单'
}
const manufacturingStructuredTypeSet = new Set([
  'manufacturing_site_snapshot',
  'manufacturing_plan_list',
@@ -991,7 +1045,11 @@
  contractAmountTotal: '合同总额',
  receivedAmountTotal: '已回款金额',
  pendingAmountTotal: '待回款总额',
  shipRate: '发货率'
  shipRate: '发货率',
  pendingOrderCount: '待付款订单数',
  totalContractAmount: '待付款合同总额',
  totalPaidAmount: '已付款总额',
  totalPendingAmount: '待付款总额'
})
const purchasePayloadFieldLabelMap = {
  purchaseLedgers: '采购台账',
@@ -1415,6 +1473,25 @@
const getSalesTypeLabel = (type = '') => salesTypeLabelMap[String(type || '')] || '销售查询结果'
const buildPurchaseStructuredData = (parsedData) => {
  if (parsedData?.success !== true) return null
  const type = String(parsedData?.type || '')
  if (!type.startsWith('purchase_')) return null
  const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
  const listItems = normalizeSalesListItems(rawData.items)
  return {
    type,
    summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
    listItems,
    columns: inferSalesColumns(listItems)
  }
}
const getPurchaseTypeLabel = (type = '') => purchaseTypeLabelMap[String(type || '')] || '采购查询结果'
const isSalesFocusType = (type = '') => salesFocusTypeSet.has(String(type || ''))
const getSalesLevelTagType = (level = '') => {
@@ -1446,6 +1523,13 @@
      .filter(Boolean)
  }
  return []
}
const normalizePurchaseIntentNotRecognizedData = (parsedData) => {
  if (String(parsedData?.type || '') !== 'purchase_intent_not_recognized') return null
  const quickPrompts = toStructuredStringArray(parsedData?.data?.quickPrompts)
  if (!quickPrompts.length) return null
  return { quickPrompts }
}
const getManufacturingWarningLevelType = (level = '') => {
@@ -1827,6 +1911,8 @@
          purchaseAnalysisData: null,
          manufacturingData: null,
          salesData: null,
          purchaseData: null,
          purchaseIntentData: null,
          localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
        }
@@ -2045,7 +2131,7 @@
          const candidate = text.slice(i, j + 1)
          try {
            const parsed = JSON.parse(candidate)
            if (parsed?.success === true) {
            if (typeof parsed?.success === 'boolean') {
              return {
                data: parsed,
                startIdx: i,
@@ -2064,7 +2150,8 @@
}
const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => {
  if (!messageObj || !parsedData?.success) return
  const isPurchaseIntentNotRecognized = String(parsedData?.type || '') === 'purchase_intent_not_recognized'
  if (!messageObj || (parsedData?.success !== true && !isPurchaseIntentNotRecognized)) return
  const previousManufacturingData = messageObj.manufacturingData
  messageObj.type = parsedData.type || ''
@@ -2072,6 +2159,15 @@
  messageObj.purchaseAnalysisData = null
  messageObj.manufacturingData = null
  messageObj.salesData = null
  messageObj.purchaseData = null
  messageObj.purchaseIntentData = null
  if (isPurchaseIntentNotRecognized) {
    messageObj.purchaseIntentData = normalizePurchaseIntentNotRecognizedData(parsedData)
    messageObj.chartOptions = null
    messageObj.chartRenderReady = false
    return
  }
  if (messageObj.type === 'todo_list' && parsedData.data) {
    messageObj.tableData = parsedData.data
@@ -2087,11 +2183,17 @@
    messageObj.manufacturingData = manufacturingData
  }
  const purchaseData = buildPurchaseStructuredData(parsedData)
  if (purchaseData) {
    messageObj.purchaseData = purchaseData
  }
  if (parsedData.action === 'confirm_required' && parsedData.businessType) {
    messageObj.type = 'purchase_analysis_confirm'
    messageObj.purchaseAnalysisData = parsedData
    messageObj.manufacturingData = null
    messageObj.salesData = null
    messageObj.purchaseData = null
    if (!Array.isArray(messageObj.payloadTreeData) || !messageObj.payloadTreeData.length) {
      initializePurchasePayloadTree(messageObj, parsedData.payload || {})
    }
@@ -2136,6 +2238,7 @@
const getStructuredFallbackText = (parsedData) => {
  if (!parsedData) return '正在为您展示分析结果...'
  if (parsedData.type === 'todo_list') return '已为您整理好相关数据。'
  if (parsedData.type === 'purchase_intent_not_recognized') return '未识别到可执行的采购查询条件,请补充查询条件后再试。'
  if (salesStructuredTypeSet.has(parsedData.type)) {
    if (parsedData.type === 'sales_customer_churn_risk') return '已为您生成客户流失风险分析。'
    if (parsedData.type === 'sales_collection_quote_strategy') return '已为您生成回款与报价策略建议。'
@@ -2148,6 +2251,7 @@
    if (parsedData.type === 'manufacturing_analysis') return '已为您生成制造分析结果。'
    return '已返回制造查询结果。'
  }
  if (String(parsedData.type || '').startsWith('purchase_')) return '已返回采购查询结果。'
  if (parsedData.charts && Object.keys(parsedData.charts).length > 0) return '已为您生成分析图表。'
  return '正在为您展示分析结果...'
}
@@ -3278,7 +3382,9 @@
    payloadHiddenData: null,
    purchaseAnalysisData: null,
    manufacturingData: null,
    salesData: null
    salesData: null,
    purchaseData: null,
    purchaseIntentData: null
  })
  outputState.value[botMsgIndex] = {
@@ -3404,7 +3510,9 @@
    payloadHiddenData: null,
    purchaseAnalysisData: null,
    manufacturingData: null,
    salesData: null
    salesData: null,
    purchaseData: null,
    purchaseIntentData: null
  }
  messages.value.push(botMsg)
@@ -3436,28 +3544,6 @@
          const extracted = extractEmbeddedSuccessJson(fullText)
          if (extracted) {
            applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
          } else {
            const extractJson = (text) => {
              const startIdx = text.indexOf('{"success": true')
              if (startIdx === -1) return null
              // ä»ŽåŽå¾€å‰æ‰¾æœ€åŽä¸€ä¸ª '}'
              const lastBraceIdx = text.lastIndexOf('}')
              if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
              const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
              try {
                return JSON.parse(potentialJson)
              } catch (err) {
                return null
              }
            }
            const parsedData = extractJson(fullText)
            if (parsedData) {
              applyStructuredMessageData(currentMsg, parsedData, botMsgIndex, true)
            }
          }
          updateOutputState(fullText, botMsgIndex)
@@ -3475,24 +3561,6 @@
    const extracted = extractEmbeddedSuccessJson(currentMsg.content)
    if (extracted) {
      applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
    } else {
      const extractJson = (text) => {
        const startIdx = text.indexOf('{"success": true')
        if (startIdx === -1) return null
        const lastBraceIdx = text.lastIndexOf('}')
        if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
        const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
        try {
          return JSON.parse(potentialJson)
        } catch (err) {
          return null
        }
      }
      const finalParsed = extractJson(currentMsg.content)
      if (finalParsed) {
        applyStructuredMessageData(currentMsg, finalParsed, botMsgIndex)
      }
    }
  }).catch(err => {
    if (err.name === 'CanceledError' || err.name === 'AbortError') {
@@ -4889,6 +4957,19 @@
  color: $deep-blue;
}
.purchase-intent-quick-prompt-wrap {
  margin-top: 12px;
}
.purchase-intent-quick-prompt-title {
  font-size: 12px;
  color: #4b5563;
}
.purchase-intent-quick-prompt-list {
  margin-top: 8px;
}
.purchase-confirm-card {
  margin-top: 12px;
  width: 100%;
src/store/modules/user.js
@@ -1,12 +1,12 @@
import {login, logout, getInfo, loginCheck, loginCheckFactory,tideLogin} from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { isHttp, isEmpty } from "@/utils/validate"
import defAva from '@/assets/images/profile.jpg'
import { defineStore } from 'pinia'
const useUserStore = defineStore(
  'user',
  {
import {login, logout, getInfo, loginCheck, loginCheckFactory,tideLogin} from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { isHttp, isEmpty } from "@/utils/validate"
import defAva from '@/assets/images/profile.jpg'
import { defineStore } from 'pinia'
const useUserStore = defineStore(
  'user',
  {
    state: () => ({
      token: getToken(),
      id: '',
@@ -15,9 +15,9 @@
      roles: [],
      permissions: [],
      aiEnabled: 0
    }),
    actions: {
      // ç™»å½•
    }),
    actions: {
      // ç™»å½•
      login(userInfo) {
        const username = userInfo.username.trim()
        const password = userInfo.password
@@ -37,33 +37,34 @@
            reject(error)
          })
        })
      },
      getCurrentTime() {
        const now = new Date();
        const year = now.getFullYear();       // èŽ·å–å¹´ä»½
        const month = String(now.getMonth() + 1).padStart(2, '0');  // æœˆä»½ä»Ž0开始,要+1,并补零
        const day = String(now.getDate()).padStart(2, '0');         // æ—¥æœŸè¡¥é›¶
        const hours = String(now.getHours()).padStart(2, '0');      // å°æ—¶è¡¥é›¶
        const minutes = String(now.getMinutes()).padStart(2, '0');  // åˆ†é’Ÿè¡¥é›¶
        const seconds = String(now.getSeconds()).padStart(2, '0');  // ç§’数补零
        return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
      },
      // èŽ·å–ç”¨æˆ·ä¿¡æ¯
      getInfo() {
        return new Promise((resolve, reject) => {
          getInfo().then(res => {
      },
      getCurrentTime() {
        const now = new Date();
        const year = now.getFullYear();       // èŽ·å–å¹´ä»½
        const month = String(now.getMonth() + 1).padStart(2, '0');  // æœˆä»½ä»Ž0开始,要+1,并补零
        const day = String(now.getDate()).padStart(2, '0');         // æ—¥æœŸè¡¥é›¶
        const hours = String(now.getHours()).padStart(2, '0');      // å°æ—¶è¡¥é›¶
        const minutes = String(now.getMinutes()).padStart(2, '0');  // åˆ†é’Ÿè¡¥é›¶
        const seconds = String(now.getSeconds()).padStart(2, '0');  // ç§’数补零
        return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
      },
      // èŽ·å–ç”¨æˆ·ä¿¡æ¯
      getInfo() {
        return new Promise((resolve, reject) => {
          getInfo().then(res => {
            res  = res.data
            const user = res.user || {}
            let avatar = user.avatar || ""
            avatar = import.meta.env.VITE_APP_BASE_API + '/profile/' + avatar
            if (res.roles && res.roles.length > 0) { // éªŒè¯è¿”回的roles是否是一个非空数组
              this.roles = res.roles
              this.permissions = res.permissions
            } else {
              this.roles = ['ROLE_DEFAULT']
            }
            let avatar = user.avatar || ""
            avatar = import.meta.env.VITE_APP_BASE_API + '/profile/' + avatar
            if (res.roles && res.roles.length > 0) { // éªŒè¯è¿”回的roles是否是一个非空数组
              this.roles = res.roles
              this.permissions = res.permissions
            } else {
              this.roles = ['ROLE_DEFAULT']
            }
            this.id = user.userId || ''
            this.name = user.userName || ''
            this.avatar = avatar
            this.avatar = avatar
            this.currentFactoryName = user.currentFactoryName || ''
            this.nickName = user.nickName || ''
            this.roleName = Array.isArray(user.roles) && user.roles.length > 0 ? (user.roles[0].roleName || '') : ''
@@ -75,11 +76,11 @@
            reject(error)
          })
        })
      },
      // é€€å‡ºç³»ç»Ÿ
      logOut() {
        return new Promise((resolve, reject) => {
          logout(this.token).then(() => {
      },
      // é€€å‡ºç³»ç»Ÿ
      logOut() {
        return new Promise((resolve, reject) => {
          logout(this.token).then(() => {
            this.token = ''
            this.roles = []
            this.permissions = []
@@ -89,21 +90,21 @@
          }).catch(error => {
            reject(error)
          })
        })
      },
      // ç™»å½•校验
      loginCheck(userInfo) {
        const username = userInfo.username.trim()
        const password = userInfo.password
        return new Promise((resolve, reject) => {
          loginCheck(username, password).then(res => {
            resolve(res)
          }).catch(error => {
            reject(error)
          })
        })
      },
      // éƒ¨é—¨ç™»å½•
        })
      },
      // ç™»å½•校验
      loginCheck(userInfo) {
        const username = userInfo.username.trim()
        const password = userInfo.password
        return new Promise((resolve, reject) => {
          loginCheck(username, password).then(res => {
            resolve(res)
          }).catch(error => {
            reject(error)
          })
        })
      },
      // éƒ¨é—¨ç™»å½•
      loginCheckFactory(userInfo) {
        const username = userInfo.username.trim()
        const password = userInfo.password
@@ -121,7 +122,7 @@
            reject(error)
          })
        })
      },
      },
      TideLogin(code) {
        return new Promise((resolve, reject) => {
          tideLogin(code)
@@ -138,12 +139,12 @@
                };
                resolve();
              })
              .catch((error) => {
                reject(error);
              });
        });
      },
    }
  })
export default useUserStore
              .catch((error) => {
                reject(error);
              });
        });
      },
    }
  })
export default useUserStore
src/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,680 @@
<template>
  <FormDialog
    v-model="dialogVisible"
    title="上传巡检记录"
    width="980px"
    @close="handleClose"
    @cancel="handleClose"
  >
    <main class="upload-content">
      <el-card v-if="taskInfo" class="section-card">
        <el-descriptions :column="1" border>
          <el-descriptions-item label="巡检任务名称">
            {{ taskInfo.taskName || "-" }}
          </el-descriptions-item>
          <el-descriptions-item label="巡检项目">
            {{ taskInfo.inspectionProject || "-" }}
          </el-descriptions-item>
          <el-descriptions-item label="备注">
            {{ taskInfo.remarks || "-" }}
          </el-descriptions-item>
        </el-descriptions>
      </el-card>
      <el-card class="section-card">
        <h3>巡检状态</h3>
        <el-radio-group v-model="hasException">
          <el-radio-button :value="false">正常</el-radio-button>
          <el-radio-button :value="true">存在异常</el-radio-button>
        </el-radio-group>
      </el-card>
      <el-card v-if="hasException === true" class="section-card">
        <h3>异常描述</h3>
        <el-input
          v-model="abnormalDescription"
          type="textarea"
          maxlength="500"
          show-word-limit
          :rows="4"
          placeholder="请描述异常情况..."
        />
      </el-card>
      <el-card v-if="hasException === true" class="section-card">
        <el-tabs v-model="currentUploadType">
          <el-tab-pane label="生产前" name="before" />
          <el-tab-pane label="生产中" name="after" />
          <el-tab-pane label="生产后" name="issue" />
        </el-tabs>
        <div class="upload-buttons">
          <el-upload
            :show-file-list="false"
            :http-request="uploadFile"
            :disabled="getCurrentFiles().length >= uploadConfig.limit || uploading"
            accept="image/*"
          >
            <el-button type="primary" :loading="uploading">
              <el-icon><Camera /></el-icon>
              é€‰æ‹©å›¾ç‰‡
            </el-button>
          </el-upload>
          <el-upload
            :show-file-list="false"
            :http-request="uploadFile"
            :disabled="getCurrentFiles().length >= uploadConfig.limit || uploading"
            accept="video/*"
          >
            <el-button type="success" :loading="uploading">
              <el-icon><VideoCamera /></el-icon>
              é€‰æ‹©è§†é¢‘
            </el-button>
          </el-upload>
        </div>
        <el-progress
          v-if="uploading"
          :percentage="uploadProgress"
          class="upload-progress"
        />
        <div v-if="getCurrentFiles().length" class="file-list">
          <div
            v-for="(file, index) in getCurrentFiles()"
            :key="file.uid || file.id || index"
            class="file-item"
          >
            <div class="file-preview-container">
              <el-image
                v-if="file.type === 'image' || !file.type"
                :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                fit="cover"
                class="file-preview"
                :preview-src-list="[file.url || file.tempFilePath || file.path || file.downloadUrl]"
                preview-teleported
              />
              <div v-else class="video-preview" @click="previewVideo(file)">
                <el-icon><VideoCamera /></el-icon>
                <span>视频</span>
              </div>
              <el-button
                class="delete-btn"
                type="danger"
                circle
                size="small"
                @click="removeFile(index)"
              >
                <el-icon><Close /></el-icon>
              </el-button>
            </div>
            <div class="file-info">
              <div class="file-name">
                {{ file.bucketFilename || file.name || (file.type === "image" ? "图片" : "视频") }}
              </div>
              <div class="file-size">{{ formatFileSize(file.size) }}</div>
            </div>
          </div>
        </div>
        <el-empty
          v-else
          :description="`请选择要上传的${getUploadTypeText()}图片或视频`"
        />
        <el-alert
          class="upload-summary"
          type="info"
          :closable="false"
          :title="`生产前:${beforeModelValue.length}个文件 | ç”Ÿäº§ä¸­ï¼š${afterModelValue.length}个文件 | ç”Ÿäº§åŽï¼š${issueModelValue.length}个文件`"
        />
      </el-card>
      <el-result
        v-if="hasException === false"
        icon="success"
        title="设备运行正常"
        sub-title="无需上传照片"
      />
    </main>
    <template #footer>
      <footer class="footer-buttons">
        <el-button type="primary" @click="submitUpload">提交</el-button>
        <el-button v-if="hasException === true" type="warning" @click="goToRepair">
          æ–°å¢žæŠ¥ä¿®
        </el-button>
        <el-button @click="handleClose">取消</el-button>
      </footer>
    </template>
  </FormDialog>
  <el-dialog
    v-model="showVideoDialog"
    :title="currentVideoFile?.originalFilename || currentVideoFile?.name || '视频预览'"
    width="720px"
  >
    <video
      v-if="currentVideoFile"
      :src="currentVideoFile.url || currentVideoFile.downloadUrl"
      class="video-player"
      controls
      autoplay
    />
  </el-dialog>
</template>
<script setup>
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { ElLoading, ElMessage, ElMessageBox } from "element-plus";
import { Camera, Close, VideoCamera } from "@element-plus/icons-vue";
import axios from "axios";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { uploadInspectionTask } from "@/api/inspectionManagement/index.js";
import { getToken } from "@/utils/auth";
const emit = defineEmits(["closeDia", "success"]);
const router = useRouter();
const dialogVisible = ref(false);
const taskInfo = ref(null);
const uploading = ref(false);
const uploadProgress = ref(0);
const beforeModelValue = ref([]);
const afterModelValue = ref([]);
const issueModelValue = ref([]);
const currentUploadType = ref("before");
const hasException = ref(null);
const abnormalDescription = ref("");
const showVideoDialog = ref(false);
const currentVideoFile = ref(null);
const uploadConfig = {
  action: "/common/upload",
  limit: 10,
  fileSize: 50,
  fileType: ["jpg", "jpeg", "png", "mp4", "mov"],
};
const uploadFileUrl = computed(
  () => `${import.meta.env.VITE_APP_BASE_API}${uploadConfig.action}`
);
const processFileUrl = fileUrl => {
  if (!fileUrl) return "";
  let currentUrl = String(fileUrl);
  if (currentUrl.includes("\\")) {
    const uploadsIndex = currentUrl.toLowerCase().indexOf("uploads");
    if (uploadsIndex > -1) {
      currentUrl = `/${currentUrl.substring(uploadsIndex).replace(/\\/g, "/")}`;
    } else {
      const fileName = currentUrl.split("\\").pop();
      currentUrl = `/uploads/${fileName}`;
    }
  }
  if (currentUrl && !currentUrl.startsWith("http")) {
    if (!currentUrl.startsWith("/")) {
      currentUrl = `/${currentUrl}`;
    }
    currentUrl = __BASE_API__ + currentUrl;
  }
  return currentUrl;
};
const normalizeList = (list, fileType) => {
  if (!Array.isArray(list)) return [];
  return list.filter(Boolean).map(item => {
    let currentType = item.type;
    if (!currentType && item.contentType) {
      currentType = item.contentType.startsWith("video") ? "video" : "image";
    } else if (!currentType) {
      currentType = fileType || "image";
    }
    return {
      ...item,
      url: processFileUrl(item.url || item.previewURL || item.downloadUrl || item.path || ""),
      downloadUrl: processFileUrl(
        item.downloadUrl || item.url || item.previewURL || item.path || ""
      ),
      name: item.name || item.originalFilename || item.bucketFilename,
      tempId: item.tempId || item.id || item.tempFileId,
      tempFileId: item.tempFileId || item.tempId || item.id,
      size: item.size || item.byteSize || 0,
      type: currentType,
      status: "success",
      uid: item.uid || `${Date.now()}-${Math.random()}`,
    };
  });
};
const resetState = () => {
  taskInfo.value = null;
  beforeModelValue.value = [];
  afterModelValue.value = [];
  issueModelValue.value = [];
  currentUploadType.value = "before";
  hasException.value = null;
  abnormalDescription.value = "";
  uploading.value = false;
  uploadProgress.value = 0;
  showVideoDialog.value = false;
  currentVideoFile.value = null;
};
const openDialog = row => {
  const raw = JSON.parse(JSON.stringify(row?.__raw || row || {}));
  taskInfo.value = raw;
  beforeModelValue.value = normalizeList(
    raw.commonFileListBeforeVO || raw.commonFileListBefore || [],
    "image"
  );
  afterModelValue.value = normalizeList(
    raw.commonFileListVO || raw.commonFileList || [],
    "image"
  );
  issueModelValue.value = normalizeList(
    raw.commonFileListAfterVO || raw.commonFileListAfter || [],
    "image"
  );
  abnormalDescription.value = raw.abnormalDescription || "";
  if (raw.hasException !== undefined && raw.hasException !== null) {
    hasException.value = raw.hasException;
  } else if (raw.inspectionResult !== undefined && raw.inspectionResult !== null) {
    hasException.value = String(raw.inspectionResult) === "0";
  } else {
    hasException.value = null;
  }
  if (
    hasException.value !== true &&
    (beforeModelValue.value.length || afterModelValue.value.length || issueModelValue.value.length)
  ) {
    hasException.value = true;
  }
  dialogVisible.value = true;
};
const handleClose = () => {
  dialogVisible.value = false;
  resetState();
  emit("closeDia");
};
const getCurrentFiles = () => {
  if (currentUploadType.value === "before") return beforeModelValue.value;
  if (currentUploadType.value === "after") return afterModelValue.value;
  if (currentUploadType.value === "issue") return issueModelValue.value;
  return [];
};
const getUploadTypeText = () => {
  if (currentUploadType.value === "before") return "生产前";
  if (currentUploadType.value === "after") return "生产中";
  if (currentUploadType.value === "issue") return "生产后";
  return "";
};
const getTabType = () => {
  if (currentUploadType.value === "before") return 10;
  if (currentUploadType.value === "after") return 11;
  if (currentUploadType.value === "issue") return 12;
  return 10;
};
const previewVideo = file => {
  currentVideoFile.value = file;
  showVideoDialog.value = true;
};
const uploadFile = async uploadRequest => {
  const rawFile = uploadRequest.file;
  if (getCurrentFiles().length >= uploadConfig.limit) {
    ElMessage.warning(`最多只能选择${uploadConfig.limit}个文件`);
    return;
  }
  const ext = rawFile.name.split(".").pop()?.toLowerCase();
  if (!uploadConfig.fileType.includes(ext)) {
    ElMessage.warning(`文件格式不支持,请上传 ${uploadConfig.fileType.join("/")} æ ¼å¼`);
    return;
  }
  if (rawFile.size > uploadConfig.fileSize * 1024 * 1024) {
    ElMessage.warning(`文件大小不能超过 ${uploadConfig.fileSize}MB`);
    return;
  }
  const token = getToken();
  if (!token) {
    ElMessage.warning("用户未登录");
    return;
  }
  const formData = new FormData();
  formData.append("files", rawFile);
  formData.append("type", getTabType());
  uploading.value = true;
  uploadProgress.value = 0;
  try {
    const { data } = await axios.post(uploadFileUrl.value, formData, {
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "multipart/form-data",
      },
      onUploadProgress: event => {
        if (event.total) {
          uploadProgress.value = Math.round((event.loaded / event.total) * 100);
        }
      },
    });
    if (data.code !== 200) {
      ElMessage.error(data.msg || "上传失败");
      return;
    }
    const resultData = Array.isArray(data.data) ? data.data[0] : data.data;
    const finalUrl = processFileUrl(
      resultData.url || resultData.previewURL || resultData.downloadUrl || ""
    );
    const finalName = resultData.name || resultData.originalFilename || resultData.bucketFilename;
    const finalId = resultData.tempId || resultData.id || resultData.tempFileId;
    const uploadedFile = {
      ...resultData,
      url: finalUrl,
      downloadUrl: finalUrl,
      name: finalName,
      tempId: finalId,
      tempFileId: resultData.tempFileId || finalId,
      size: rawFile.size || resultData.size || resultData.byteSize || 0,
      type: rawFile.type?.startsWith("video") ? "video" : "image",
      status: "success",
      uid: `${Date.now()}-${Math.random()}`,
    };
    getCurrentFiles().push(uploadedFile);
    ElMessage.success("上传成功");
  } catch (error) {
    ElMessage.error(error?.message || "上传失败");
  } finally {
    uploading.value = false;
  }
};
const buildFileItem = item => ({
  id: item?.id,
  tempId: item?.tempId,
  tempFileId: item?.tempFileId,
  url: item?.downloadUrl || item?.url || "",
  downloadUrl: item?.downloadUrl || item?.url || "",
  name: item?.name,
  bucketFilename: item?.bucketFilename || item?.name,
  originalFilename: item?.originalFilename || item?.name,
  size: item?.size || 0,
  byteSize: item?.byteSize || item?.size || 0,
  contentType: item?.contentType || "",
  type: item?.type,
});
const submitUpload = async () => {
  if (hasException.value === null) {
    ElMessage.warning("请选择巡检状态");
    return;
  }
  if (hasException.value === true) {
    const totalFiles =
      beforeModelValue.value.length +
      afterModelValue.value.length +
      issueModelValue.value.length;
    if (!totalFiles) {
      ElMessage.warning("请上传异常照片或视频");
      return;
    }
    if (!abnormalDescription.value.trim()) {
      ElMessage.warning("请填写异常描述");
      return;
    }
  }
  const loading = ElLoading.service({
    text: "提交中...",
    background: "rgba(0, 0, 0, 0.3)",
  });
  try {
    const allFiles = [
      ...beforeModelValue.value,
      ...afterModelValue.value,
      ...issueModelValue.value,
    ];
    const tempFileIds = allFiles
      .map(item => item?.tempId ?? item?.tempFileId ?? item?.id)
      .filter(Boolean);
    const {
      createTime,
      updateTime,
      storageBlobDTO,
      commonFileListAfterVO,
      commonFileListVO,
      commonFileListBeforeVO,
      commonFileListAfter,
      commonFileList,
      commonFileListBefore,
      __raw,
      ...baseTaskInfo
    } = taskInfo.value || {};
    const submitData = {
      ...baseTaskInfo,
      commonFileListBeforeDTO: beforeModelValue.value.map(buildFileItem),
      commonFileListDTO: afterModelValue.value.map(buildFileItem),
      commonFileListAfterDTO: issueModelValue.value.map(buildFileItem),
      hasException: hasException.value,
      inspectionResult: hasException.value ? 0 : 1,
      abnormalDescription: abnormalDescription.value,
      tempFileIds,
    };
    const result = await uploadInspectionTask(submitData);
    if (result && (result.code === 200 || result.success)) {
      ElMessage.success("提交成功");
      dialogVisible.value = false;
      resetState();
      emit("success");
      emit("closeDia");
    } else {
      ElMessage.error(result?.msg || result?.message || "提交失败");
    }
  } catch (error) {
    ElMessage.error(error?.message || "提交失败");
  } finally {
    loading.close();
  }
};
const removeFile = async index => {
  try {
    await ElMessageBox.confirm("确定要删除这个文件吗?", "确认删除", {
      type: "warning",
    });
    getCurrentFiles().splice(index, 1);
  } catch {}
};
const goToRepair = () => {
  const taskData = {
    taskId: taskInfo.value?.taskId || taskInfo.value?.id,
    taskName: taskInfo.value?.taskName,
    inspectionLocation: taskInfo.value?.inspectionLocation,
    inspector: taskInfo.value?.inspector,
    hasException: hasException.value,
    inspectionResult: hasException.value ? 0 : 1,
    commonFileListBeforeDTO: beforeModelValue.value.map(buildFileItem),
    commonFileListDTO: afterModelValue.value.map(buildFileItem),
    commonFileListAfterDTO: issueModelValue.value.map(buildFileItem),
    uploadedFiles: {
      before: beforeModelValue.value,
      after: afterModelValue.value,
      issue: issueModelValue.value,
    },
  };
  sessionStorage.setItem("repairTaskInfo", JSON.stringify(taskData));
  router.push("/equipmentManagement/repair/add");
};
const formatFileSize = size => {
  if (!size) return "0 B";
  const units = ["B", "KB", "MB", "GB"];
  let index = 0;
  let fileSize = size;
  while (fileSize >= 1024 && index < units.length - 1) {
    fileSize /= 1024;
    index += 1;
  }
  return `${fileSize.toFixed(2)} ${units[index]}`;
};
defineExpose({
  openDialog,
});
</script>
<style scoped>
.inspection-upload-page {
  min-height: 70vh;
  background: #f5f7fa;
  padding: 20px 20px 90px;
  box-sizing: border-box;
}
.upload-content {
  max-width: 960px;
  margin: 20px auto 0;
}
.section-card {
  margin-bottom: 16px;
}
.section-card h3 {
  margin: 0 0 16px;
  font-size: 16px;
}
.upload-buttons {
  display: flex;
  gap: 12px;
  margin: 16px 0;
}
.upload-progress {
  margin-bottom: 16px;
}
.file-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 12px;
}
.file-preview-container {
  position: relative;
  aspect-ratio: 1;
  border-radius: 8px;
  overflow: hidden;
  background: #f2f3f5;
}
.file-preview {
  width: 100%;
  height: 100%;
}
.video-preview {
  width: 100%;
  height: 100%;
  background: #303133;
  color: #fff;
  display: flex;
  gap: 6px;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.delete-btn {
  position: absolute;
  top: 6px;
  right: 6px;
}
.file-info {
  margin-top: 6px;
  font-size: 12px;
}
.file-name {
  color: #606266;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.file-size {
  color: #909399;
  margin-top: 2px;
}
.upload-summary {
  margin-top: 16px;
}
.footer-buttons {
  position: sticky;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 14px 20px 0;
  background: #f5f7fa;
  display: flex;
  justify-content: center;
  gap: 12px;
}
.video-player {
  width: 100%;
  max-height: 70vh;
  background: #000;
}
</style>
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue
@@ -1,210 +1,226 @@
<template>
  <div>
    <el-dialog title="查看附件"
               v-model="dialogVisitable" width="800px" @close="cancel">
    <el-dialog title="查看附件" v-model="dialogVisitable" width="800px" @close="cancel">
      <div class="upload-container">
        <!-- ç”Ÿäº§å‰ -->
        <div class="form-container">
          <div class="title">生产前</div>
          <!-- å›¾ç‰‡åˆ—表 -->
          <div style="display: flex; flex-wrap: wrap;">
            <img v-for="(item, index) in beforeProductionImgs" :key="index"
                 @click="showMedia(beforeProductionImgs, index, 'image')"
                 :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt="">
          <div class="media-list">
            <img
              v-for="(item, index) in beforeProductionImgs"
              :key="`before-img-${index}`"
              :src="item"
              alt=""
              class="media-image"
              @click="showMedia(beforeProductionImgs, index, 'image')"
            />
          </div>
          <!-- è§†é¢‘列表 -->
          <div style="display: flex; flex-wrap: wrap;">
          <div class="media-list">
            <div
                v-for="(videoUrl, index) in beforeProductionVideos"
                :key="index"
                @click="showMedia(beforeProductionVideos, index, 'video')"
                style="position: relative; margin: 10px; cursor: pointer;"
              v-for="(videoUrl, index) in beforeProductionVideos"
              :key="`before-video-${index}`"
              class="video-item"
              @click="showMedia(beforeProductionVideos, index, 'video')"
            >
              <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;">
                <img src="@/assets/images/video.png" alt="播放" style="width: 30px; height: 30px; opacity: 0.8;" />
              <div class="video-thumb">
                <img src="@/assets/images/video.png" alt="播放" class="video-icon" />
              </div>
              <div style="text-align: center; font-size: 12px; color: #666;">点击播放</div>
              <div class="video-text">点击播放</div>
            </div>
          </div>
        </div>
        <!-- ç”Ÿäº§åŽ -->
        <div class="form-container">
          <div class="title">生产中</div>
          <div class="media-list">
            <img
              v-for="(item, index) in afterProductionImgs"
              :key="`during-img-${index}`"
              :src="item"
              alt=""
              class="media-image"
              @click="showMedia(afterProductionImgs, index, 'image')"
            />
          </div>
          <div class="media-list">
            <div
              v-for="(videoUrl, index) in afterProductionVideos"
              :key="`during-video-${index}`"
              class="video-item"
              @click="showMedia(afterProductionVideos, index, 'video')"
            >
              <div class="video-thumb">
                <img src="@/assets/images/video.png" alt="播放" class="video-icon" />
              </div>
              <div class="video-text">点击播放</div>
            </div>
          </div>
        </div>
        <div class="form-container">
          <div class="title">生产后</div>
          <!-- å›¾ç‰‡åˆ—表 -->
          <div style="display: flex; flex-wrap: wrap;">
            <img v-for="(item, index) in afterProductionImgs" :key="index"
                 @click="showMedia(afterProductionImgs, index, 'image')"
                 :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt="">
          <div class="media-list">
            <img
              v-for="(item, index) in productionIssuesImgs"
              :key="`after-img-${index}`"
              :src="item"
              alt=""
              class="media-image"
              @click="showMedia(productionIssuesImgs, index, 'image')"
            />
          </div>
          <!-- è§†é¢‘列表 -->
          <div style="display: flex; flex-wrap: wrap;">
          <div class="media-list">
            <div
                v-for="(videoUrl, index) in afterProductionVideos"
                :key="index"
                @click="showMedia(afterProductionVideos, index, 'video')"
                style="position: relative; margin: 10px; cursor: pointer;"
              v-for="(videoUrl, index) in productionIssuesVideos"
              :key="`after-video-${index}`"
              class="video-item"
              @click="showMedia(productionIssuesVideos, index, 'video')"
            >
              <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;">
                <img src="@/assets/images/video.png" alt="播放" style="width: 30px; height: 30px; opacity: 0.8;" />
              <div class="video-thumb">
                <img src="@/assets/images/video.png" alt="播放" class="video-icon" />
              </div>
              <div style="text-align: center; font-size: 12px; color: #666;">点击播放</div>
            </div>
          </div>
        </div>
        <!-- ç”Ÿäº§é—®é¢˜ -->
        <div class="form-container">
          <div class="title">生产问题</div>
          <!-- å›¾ç‰‡åˆ—表 -->
          <div style="display: flex; flex-wrap: wrap;">
            <img v-for="(item, index) in productionIssuesImgs" :key="index"
                 @click="showMedia(productionIssuesImgs, index, 'image')"
                 :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt="">
          </div>
          <!-- è§†é¢‘列表 -->
          <div style="display: flex; flex-wrap: wrap;">
            <div
                v-for="(videoUrl, index) in productionIssuesVideos"
                :key="index"
                @click="showMedia(productionIssuesVideos, index, 'video')"
                style="position: relative; margin: 10px; cursor: pointer;"
            >
              <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;">
                <img src="@/assets/images/video.png" alt="播放" style="width: 30px; height: 30px; opacity: 0.8;" />
              </div>
              <div style="text-align: center; font-size: 12px; color: #666;">点击播放</div>
              <div class="video-text">点击播放</div>
            </div>
          </div>
        </div>
      </div>
    </el-dialog>
    <!-- ç»Ÿä¸€åª’体查看器 -->
    <div v-if="isMediaViewerVisible" class="media-viewer-overlay" @click.self="closeMediaViewer">
      <div class="media-viewer-content" @click.stop>
        <!-- å›¾ç‰‡ -->
        <vue-easy-lightbox
            v-if="mediaType === 'image'"
            :visible="isMediaViewerVisible"
            :imgs="mediaList"
            :index="currentMediaIndex"
            @hide="closeMediaViewer"
        ></vue-easy-lightbox>
        <!-- è§†é¢‘ -->
        <div v-else-if="mediaType === 'video'" style="position: relative;">
          <video
              :src="mediaList[currentMediaIndex]"
              autoplay
              controls
              style="max-width: 90vw; max-height: 80vh;"
          />
          v-if="mediaType === 'image'"
          :visible="isMediaViewerVisible"
          :imgs="mediaList"
          :index="currentMediaIndex"
          @hide="closeMediaViewer"
        />
        <div v-else-if="mediaType === 'video'" class="video-player-wrap">
          <video :src="mediaList[currentMediaIndex]" autoplay controls class="video-player" />
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref } from 'vue';
import VueEasyLightbox from 'vue-easy-lightbox';
const { proxy } = getCurrentInstance();
// æŽ§åˆ¶å¼¹çª—显示
<script setup>
import { ref } from "vue";
import VueEasyLightbox from "vue-easy-lightbox";
const dialogVisitable = ref(false);
// å›¾ç‰‡æ•°ç»„
const beforeProductionImgs = ref([]);
const afterProductionImgs = ref([]);
const productionIssuesImgs = ref([]);
// è§†é¢‘数组
const beforeProductionVideos = ref([]);
const afterProductionVideos = ref([]);
const productionIssuesVideos = ref([]);
// åª’体查看器状态
const isMediaViewerVisible = ref(false);
const currentMediaIndex = ref(0);
const mediaList = ref([]); // å­˜å‚¨å½“前要查看的媒体列表(含图片和视频对象)
const mediaType = ref('image'); // image | video
const mediaList = ref([]);
const mediaType = ref("image");
// å¤„理每一类数据:分离图片和视频
function processItems(items) {
const processFileUrl = fileUrl => {
  if (!fileUrl) return "";
  let currentUrl = String(fileUrl);
  if (currentUrl.includes("\\")) {
    const uploadsIndex = currentUrl.toLowerCase().indexOf("uploads");
    if (uploadsIndex > -1) {
      currentUrl = `/${currentUrl.substring(uploadsIndex).replace(/\\/g, "/")}`;
    } else {
      const fileName = currentUrl.split("\\").pop();
      currentUrl = `/uploads/${fileName}`;
    }
  }
  if (currentUrl && !currentUrl.startsWith("http")) {
    if (!currentUrl.startsWith("/")) {
      currentUrl = `/${currentUrl}`;
    }
    currentUrl = __BASE_API__ + currentUrl;
  }
  return currentUrl;
};
const processItems = items => {
  const images = [];
  const videos = [];
  // æ£€æŸ¥ items æ˜¯å¦å­˜åœ¨ä¸”为数组
  if (!items || !Array.isArray(items)) {
  if (!Array.isArray(items)) {
    return { images, videos };
  }
  items.forEach(item => {
    if (!item || !item.previewURL || !item.contentType) return;
    if (!item) return;
    // å¤„理文件 URL
    const fileUrl = item.previewURL;
    const contentType = String(item.contentType).toLowerCase();
    const fileUrl = processFileUrl(
      item.previewURL || item.url || item.downloadUrl || item.path || ""
    );
    const contentType = String(item.contentType || "").toLowerCase();
    // æ ¹æ® contentType åˆ¤æ–­æ˜¯å›¾ç‰‡è¿˜æ˜¯è§†é¢‘
    if (contentType.startsWith('image/')) {
      images.push(fileUrl);
    } else if (contentType.startsWith('video/')) {
    if (!fileUrl) return;
    if (contentType.startsWith("video/")) {
      videos.push(fileUrl);
      return;
    }
  });
  return { images, videos };
}
// æ‰“开弹窗并加载数据
const openDialog = async (row) => {
  // ä½¿ç”¨æ­£ç¡®çš„字段名:commonFileListBefore, commonFileListAfter
  const { images: beforeImgs, videos: beforeVids } = processItems(row.commonFileListBeforeVO || []);
  const { images: afterImgs, videos: afterVids } = processItems(row.commonFileListAfterVO || []);
  const { images: issueImgs, videos: issueVids } = processItems(row.commonFileListVO || []);
    images.push(fileUrl);
  });
  return { images, videos };
};
const openDialog = row => {
  const { images: beforeImgs, videos: beforeVids } = processItems(
    row.commonFileListBeforeVO || []
  );
  const { images: afterImgs, videos: afterVids } = processItems(
    row.commonFileListVO || []
  );
  const { images: issueImgs, videos: issueVids } = processItems(
    row.commonFileListAfterVO || []
  );
  beforeProductionImgs.value = beforeImgs;
  beforeProductionVideos.value = beforeVids;
  afterProductionImgs.value = afterImgs;
  afterProductionVideos.value = afterVids;
  productionIssuesImgs.value = issueImgs;
  productionIssuesVideos.value = issueVids;
  dialogVisitable.value = true;
};
// æ˜¾ç¤ºåª’体(图片 or è§†é¢‘)
function showMedia(mediaArray, index, type) {
  mediaList.value = mediaArray;
const showMedia = (items, index, type) => {
  mediaList.value = items;
  currentMediaIndex.value = index;
  mediaType.value = type;
  isMediaViewerVisible.value = true;
}
};
// å…³é—­åª’体查看器
function closeMediaViewer() {
const closeMediaViewer = () => {
  isMediaViewerVisible.value = false;
  mediaList.value = [];
  mediaType.value = 'image';
}
  mediaType.value = "image";
};
// è¡¨å•关闭方法
const cancel = () => {
  dialogVisitable.value = false;
};
defineExpose({ openDialog });
</script>
<style scoped lang="scss">
.upload-container {
  display: flex;
@@ -213,7 +229,7 @@
  padding: 20px;
  border: 1px solid #dcdfe6;
  box-sizing: border-box;
  .form-container {
    flex: 1;
    width: 100%;
@@ -229,7 +245,7 @@
  padding-left: 10px;
  position: relative;
  margin: 6px 0;
  &::before {
    content: "";
    position: absolute;
@@ -241,12 +257,48 @@
  }
}
.media-list {
  display: flex;
  flex-wrap: wrap;
}
.media-image {
  max-width: 100px;
  height: 100px;
  margin: 5px;
  cursor: pointer;
}
.video-item {
  position: relative;
  margin: 10px;
  cursor: pointer;
}
.video-thumb {
  width: 160px;
  height: 90px;
  background-color: #333;
  display: flex;
  align-items: center;
  justify-content: center;
}
.video-icon {
  width: 30px;
  height: 30px;
  opacity: 0.8;
}
.video-text {
  text-align: center;
  font-size: 12px;
  color: #666;
}
.media-viewer-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.8);
  z-index: 9999;
  display: flex;
@@ -260,4 +312,13 @@
  max-height: 90vh;
  overflow: hidden;
}
</style>
.video-player-wrap {
  position: relative;
}
.video-player {
  max-width: 90vw;
  max-height: 80vh;
}
</style>
src/views/equipmentManagement/inspectionManagement/index.vue
@@ -82,6 +82,9 @@
    <form-dia ref="formDia"
              @closeDia="handleQuery"></form-dia>
    <view-files ref="viewFiles"></view-files>
    <upload-files ref="uploadFiles"
                  @success="handleQuery"
                  @closeDia="handleQuery"></upload-files>
  </div>
</template>
@@ -93,6 +96,7 @@
  // ç»„件引入
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  import FormDia from "@/views/equipmentManagement/inspectionManagement/components/formDia.vue";
  import UploadFiles from "@/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue";
  import ViewFiles from "@/views/equipmentManagement/inspectionManagement/components/viewFiles.vue";
  // æŽ¥å£å¼•å…¥
@@ -106,6 +110,7 @@
  const { proxy } = getCurrentInstance();
  const formDia = ref();
  const viewFiles = ref();
  const uploadFiles = ref();
  // æŸ¥è¯¢å‚æ•°
  const queryParams = reactive({
@@ -211,8 +216,9 @@
    const operationConfig = {
      label: "操作",
      width: 130,
      width: operations.length > 1 ? 180 : 130,
      fixed: "right",
            align: 'center',
      dataType: "action",
      operation: operations
        .map(op => {
@@ -221,6 +227,12 @@
              return {
                name: "编辑",
                clickFun: handleAdd,
                color: "#409EFF",
              };
            case "upload":
              return {
                name: "上传",
                clickFun: openUploadDialog,
                color: "#409EFF",
              };
            case "viewFile":
@@ -253,14 +265,14 @@
      ];
      operationsArr.value = ["edit"];
    } else if (value === "task") {
      const operationColumn = getOperationColumn(["viewFile"]);
      const operationColumn = getOperationColumn(["upload", "viewFile"]);
      // å®šæ—¶ä»»åŠ¡è®°å½•ä¸å±•ç¤º"是否启用"列
      const taskColumns = columns.value.filter(col => col.prop !== "isEnabled");
      tableColumns.value = [
        ...taskColumns,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["viewFile"];
      operationsArr.value = ["upload", "viewFile"];
    }
    pageNum.value = 1;
    pageSize.value = 10;
@@ -302,6 +314,7 @@
        // å¤„理 inspector å­—段,将字符串转换为数组(适用于所有情况)
        tableData.value = rawData.map(item => {
          const processedItem = { ...item };
          processedItem.__raw = { ...item };
          // å¤„理 inspector å­—段
          if (processedItem.inspector) {
@@ -351,6 +364,12 @@
  const viewFile = row => {
    nextTick(() => {
      viewFiles.value?.openDialog(row);
    });
  };
  const openUploadDialog = row => {
    nextTick(() => {
      uploadFiles.value?.openDialog(row);
    });
  };
@@ -419,4 +438,4 @@
    color: #909399;
    font-size: 14px;
  }
</style>
</style>
src/views/index.vue
@@ -114,6 +114,7 @@
            <div class="panel-title">生产订单进度</div>
            <el-radio-group v-model="orderFilter" size="small">
              <el-radio-button label="all">全部({{ orderProgressMeta.total }})</el-radio-button>
              <el-radio-button label="waiting">待开始({{ orderProgressMeta.waitingCount }})</el-radio-button>
              <el-radio-button label="inProgress">进行中({{ orderProgressMeta.inProgressCount }})</el-radio-button>
              <el-radio-button label="completed">已完成({{ orderProgressMeta.completedCount }})</el-radio-button>
              <el-radio-button label="paused">已暂停({{ orderProgressMeta.pausedCount }})</el-radio-button>
@@ -454,10 +455,13 @@
});
const orderProgressMeta = ref({
  status: "all",
  tab: "all",
  bizDate: null,
  total: 0,
  pageNum: 1,
  pageSize: 10,
  waitingCount: 0,
  inProgressCount: 0,
  completedCount: 0,
  pausedCount: 0,
@@ -832,8 +836,36 @@
const productionOrders = ref([]);
const orderFilterOptions = ["all", "waiting", "inProgress", "completed", "paused"];
const orderFilterAliasMap = {
  1: "waiting",
  2: "inProgress",
  3: "completed",
  4: "paused",
};
const orderFilter = ref("all");
const filteredOrders = computed(() => productionOrders.value);
const normalizeOrderFilter = (value, fallback = "all") => {
  const safeFallback = orderFilterOptions.includes(fallback) ? fallback : "all";
  const text = String(value ?? "").trim();
  if (orderFilterAliasMap[text]) {
    return orderFilterAliasMap[text];
  }
  return orderFilterOptions.includes(text) ? text : safeFallback;
};
const parseCount = (value) => {
  if (value === null || value === undefined || value === "") return null;
  const num = Number(value);
  return Number.isFinite(num) ? num : null;
};
const resolveProgressCount = (rawValue, currentStatus, targetStatus, total) => {
  const count = parseCount(rawValue);
  if (count !== null) return count;
  return currentStatus === targetStatus ? total : 0;
};
const getCompareTrend = (value) => {
  const num = Number(value || 0);
@@ -1198,27 +1230,36 @@
const refreshProductionOrderProgress = async () => {
  try {
    const res = await productionOrderProgress({
      status: orderFilter.value,
      tab: orderFilter.value,
      pageNum: 1,
      pageSize: 10,
    });
    const data = res?.data || {};
    const statusValue = normalizeOrderFilter(data.status, orderFilter.value);
    const total = Number(data.total || 0);
    orderProgressMeta.value = {
      status: statusValue,
      tab: data.tab || orderFilter.value,
      total: Number(data.total || 0),
      bizDate: data.bizDate || null,
      total,
      pageNum: Number(data.pageNum || 1),
      pageSize: Number(data.pageSize || 10),
      inProgressCount: Number(data.inProgressCount || 0),
      completedCount: Number(data.completedCount || 0),
      pausedCount: Number(data.pausedCount || 0),
      waitingCount: resolveProgressCount(data.waitingCount, statusValue, "waiting", total),
      inProgressCount: resolveProgressCount(data.inProgressCount, statusValue, "inProgress", total),
      completedCount: resolveProgressCount(data.completedCount, statusValue, "completed", total),
      pausedCount: resolveProgressCount(data.pausedCount, statusValue, "paused", total),
    };
    productionOrders.value = (data.records || []).map(mapOrderProgressRecord);
  } catch {
    orderProgressMeta.value = {
      status: orderFilter.value,
      tab: orderFilter.value,
      bizDate: null,
      total: 0,
      pageNum: 1,
      pageSize: 10,
      waitingCount: 0,
      inProgressCount: 0,
      completedCount: 0,
      pausedCount: 0,
@@ -1229,7 +1270,10 @@
const refreshTodayProductionPlan = async () => {
  try {
    const res = await todayProductionPlan({ limit: 4 });
    const res = await todayProductionPlan({
      limit: 4,
      planDate: nowDate.value,
    });
    const data = res?.data || {};
    todayPlanTotal.value = Number(data.total || 0);
    todayPlanList.value = (data.records || []).map(mapTodayPlanRecord);
src/views/productionManagement/processRoute/index.vue
@@ -61,7 +61,9 @@
  import EditProcess from "@/views/productionManagement/processRoute/Edit.vue";
  import RouteItemForm from "@/views/productionManagement/processRoute/ItemsForm.vue";
  import { listPage, del } from "@/api/productionManagement/processRoute.js";
  const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
  const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
  );
  import { useRouter } from "vue-router";
  import { ElMessage, ElMessageBox } from "element-plus";
src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -47,16 +47,45 @@
            <span class="info-value">{{ routeInfo.quantity || '-' }}</span>
          </div>
        </div>
        <div class="info-item full-width"
             v-if="routeInfo.description">
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">描述</span>
            <span class="info-label">备注</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.description }}</span>
          </div>
        </div>
      </div>
    </el-card>
    <!-- é™„件模块 -->
    <div v-if="pageType === 'order'"
         class="section-header">
      <div class="section-title">附件</div>
    </div>
    <el-card v-if="pageType === 'order'"
             class="attachment-card"
             shadow="hover"
             style="margin-top: 10px; margin-bottom: 20px;">
      <el-table :data="attachmentTableData"
                border
                class="attachment-table">
        <el-table-column label="附件名称"
                         prop="originalFilename"
                         show-overflow-tooltip />
        <el-table-column fixed="right"
                         label="操作"
                         width="200"
                         align="center">
          <template #default="scope">
            <el-button link
                       type="primary"
                       size="small"
                       @click="downloadAttachmentFile(scope.row.downloadURL)">
              ä¸‹è½½
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <!-- è¡¨æ ¼è§†å›¾ -->
    <div v-if="viewMode === 'table'"
@@ -382,6 +411,18 @@
                         v-model="bomDataValue.showProductDialog"
                         :single="true"
                         @confirm="handleBomProduct" />
    <!-- ä¸Šä¼ ç»„件弹窗 -->
    <el-dialog v-model="uploadDialogVisible"
               title="上传附件"
               width="50%"
               @close="closeAttachmentUpload">
      <AttachmentUpload v-model:file-list="newFileList" />
      <template #footer>
        <el-button @click="saveAttachmentUpload"
                   type="primary">保存</el-button>
        <el-button @click="closeAttachmentUpload">关闭</el-button>
      </template>
    </el-dialog>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog v-model="dialogVisible"
               :title="operationType === 'add' ? '新增工艺路线项目' : '编辑工艺路线项目'"
@@ -518,6 +559,12 @@
    queryList2,
    add2,
  } from "@/api/productionManagement/productStructure.js";
  import AttachmentUpload from "@/components/AttachmentUpload/file/index.vue";
  import {
    attachmentList,
    deleteAttachment,
    createAttachment,
  } from "@/api/basicData/storageAttachment.js";
  import { useRoute } from "vue-router";
  import { ElMessageBox, ElMessage } from "element-plus";
@@ -530,6 +577,7 @@
  const orderId = computed(() => route.query.orderId);
  const pageType = computed(() => route.query.type);
  const editable = computed(() => route.query.editable !== "false");
  const technologyRoutingId = computed(() => route.query.technologyRoutingId);
  const tableLoading = ref(false);
  const tableData = ref([]);
@@ -548,7 +596,66 @@
    bomNo: "",
    description: "",
    quantity: 0,
    technologyRoutingId: "",
  });
  // é™„件相关
  const attachmentTableData = ref([]);
  const uploadDialogVisible = ref(false);
  const newFileList = ref([]);
  const getAttachmentList = () => {
    if (!technologyRoutingId.value) return;
    attachmentList({
      recordType: "technology_routing",
      recordId: technologyRoutingId.value,
    }).then(res => {
      attachmentTableData.value = (res && res.data) || [];
    });
  };
  const handleUploadAttachment = () => {
    uploadDialogVisible.value = true;
  };
  const saveAttachmentUpload = async () => {
    if (newFileList.value.length > 0) {
      createAttachment({
        application: "file",
        recordType: "technology_routing",
        recordId: technologyRoutingId.value,
        storageBlobDTOs: [...newFileList.value, ...attachmentTableData.value],
      })
        .then(res => {
          if (res && res.code === 200) {
            proxy?.$modal?.msgSuccess("上传成功");
            newFileList.value = [];
            getAttachmentList();
          }
        })
        .finally(() => {
          uploadDialogVisible.value = false;
        });
    }
  };
  const closeAttachmentUpload = () => {
    newFileList.value = [];
    uploadDialogVisible.value = false;
  };
  const handleDeleteAttachment = async row => {
    deleteAttachment([row.storageAttachmentId]).then(res => {
      if (res && res.code === 200) {
        proxy?.$modal?.msgSuccess("删除成功");
        getAttachmentList();
      }
    });
  };
  const downloadAttachmentFile = url => {
    window.open(url, "_blank");
  };
  const processOptions = ref([]);
  const showProductSelectDialog = ref(false);
@@ -680,6 +787,7 @@
      bomId: route.query.bomId || "",
      description: route.query.description || "",
      quantity: route.query.quantity || 0,
      technologyRoutingId: route.query.technologyRoutingId || "",
      status: !(route.query.status == 1 || route.query.status === "false"),
    };
    bomTableData.value[0].productName = routeInfo.value.productName;
@@ -1165,12 +1273,16 @@
  const handleBomProcessChange = (row, value) => {
    row.processId = value || "";
    syncProcessOperationFields(row);
    // æ£€æŸ¥åŒä¸€å±‚级是否已经有其他不同的工序被选中
    const siblings = findSiblings(bomDataValue.value.dataList, row.tempId);
    if (siblings && value) {
      const hasDifferentProcess = siblings.some(sibling => {
        return sibling.tempId !== row.tempId && sibling.processId && sibling.processId !== value;
        return (
          sibling.tempId !== row.tempId &&
          sibling.processId &&
          sibling.processId !== value
        );
      });
      if (hasDifferentProcess) {
        ElMessage.warning("同一层级已存在不同的工序,请先统一工序后再进行修改");
@@ -1392,11 +1504,13 @@
    };
    // æ ¡éªŒåŒä¸€å±‚级的工序是否一致
    const validateProcessConsistency = (items) => {
    const validateProcessConsistency = items => {
      if (!items || items.length === 0) return;
      // æ£€æŸ¥å½“前层级
      const processes = items.filter(item => item.processId).map(item => item.processId);
      const processes = items
        .filter(item => item.processId)
        .map(item => item.processId);
      if (processes.length > 1) {
        const uniqueProcesses = [...new Set(processes)];
        if (uniqueProcesses.length > 1) {
@@ -1474,6 +1588,9 @@
    getList();
    getProcessList();
    fetchBomData();
    if (pageType.value === "order") {
      getAttachmentList();
    }
  };
  onMounted(() => {
src/views/productionManagement/productionOrder/index.vue
@@ -724,6 +724,7 @@
          bomNo: row.bomNo || "",
          description: data.description || "",
          quantity: row.quantity || 0,
          technologyRoutingId: data.technologyRoutingId,
          orderId,
          type: "order",
          editable: !row.endOrder,