<template>
  <div class="mod-config">
    <el-form :inline="true" label-position="top" class="query-form">
      <el-form-item label="物料号">
        <el-input v-model="searchData.modelNo" clearable placeholder="请输入物料号" style="width: 160px"></el-input>
      </el-form-item>
      <el-form-item label="物料名称">
        <el-input v-model="searchData.materialName" clearable placeholder="请输入物料名称" style="width: 160px"></el-input>
      </el-form-item>
      <el-form-item label="规格">
        <el-input v-model="searchData.materialSpec" clearable placeholder="请输入规格" style="width: 160px"></el-input>
      </el-form-item>
      <el-form-item label="状态">
        <el-select v-model="searchData.status" clearable placeholder="全部" style="width: 120px">
          <el-option label="已排产" value="已排产"></el-option>
          <el-option label="进行中" value="进行中"></el-option>
          <el-option label="已完成" value="已完成"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="计划完工日期">
        <el-date-picker v-model="searchData.planStartDate" type="date" value-format="yyyy-MM-dd" placeholder="开始" style="width: 130px"></el-date-picker>
      </el-form-item>
      <el-form-item label="至">
        <el-date-picker v-model="searchData.planEndDate" type="date" value-format="yyyy-MM-dd" placeholder="结束" style="width: 130px"></el-date-picker>
      </el-form-item>
      <el-form-item label=" " style="margin-top: -11px">
        <el-button @click="searchTable('Y')" plain class="search-btn">查询</el-button>
        <el-button @click="resetQuery()" plain class="reset-btn">重置</el-button>
        <el-button @click="openEditDialog()" plain class="add-btn">新增任务单</el-button>
      </el-form-item>
      <el-form-item label=" " class="query-finish-item">
        <el-button
          @click="finishTask()"
          plain
          class="search-btn"
          :disabled="!selectedOrder.orderNo || selectedOrder.status === '已完成'">
          完工
        </el-button>
      </el-form-item>
    </el-form>

    <el-table
      ref="orderTable"
      class="data-table"
      :data="dataList"
      :height="tableHeight"
      border
      highlight-current-row
      v-loading="dataListLoading"
      style="width: 100%"
      @current-change="onCurrentRowChange">
      <el-table-column label="操作" width="200" align="center">
        <template slot-scope="scope">
          <a type="text" @click="openEditDialog(scope.row)">修改</a>
          <a type="text" @click="openAssignDialog(scope.row)" v-if="scope.row.status !== '已完成' && scope.row.currentNode!=='全部完成'">分配人员</a>
          <a type="text" style="color:#F56C6C" @click="deleteTask(scope.row)" v-if="scope.row.status !== '已完成'">删除</a>
        </template>
      </el-table-column>
      <el-table-column type="index" label="#" width="50" align="center"></el-table-column>
<!--      <el-table-column prop="orderNo" label="任务单号" width="180" align="center"></el-table-column>-->
      <el-table-column prop="modelNo" label="物料号" width="160" align="center"></el-table-column>
      <el-table-column prop="materialName" label="物料名称" width="160" align="center" show-overflow-tooltip></el-table-column>
      <el-table-column prop="materialSpec" label="规格" width="160" align="center" show-overflow-tooltip></el-table-column>
      <el-table-column prop="taskQty" label="计划数量" width="100" align="center"></el-table-column>
      <el-table-column prop="reportQty" label="累计报工数量" width="120" align="center"></el-table-column>
      <el-table-column prop="currentNode" label="当前节点" width="120" align="center"></el-table-column>
      <el-table-column prop="assigneeSummary" label="节点负责人" min-width="160" show-overflow-tooltip></el-table-column>
      <el-table-column label="节点进度" width="100" align="center">
        <template slot-scope="scope">
          <el-tag size="small" type="info">{{ scope.row.nodeDoneCount }}/{{ scope.row.nodeTotalCount }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="status" label="状态" width="90" align="center">
        <template slot-scope="scope">
          <el-tag :type="getStatusType(scope.row.status)" size="small">{{ scope.row.status }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="planFinishDate" label="计划完工日期" width="120" align="center"></el-table-column>
      <el-table-column prop="finishDate" label="完工日期" width="120" align="center">
        <template slot-scope="scope">{{ scope.row.finishDate || '-' }}</template>
      </el-table-column>

    </el-table>

    <el-pagination
      @size-change="sizeChangeHandle"
      @current-change="currentChangeHandle"
      :current-page="pageIndex"
      :page-sizes="[10, 20, 50, 100]"
      :page-size="pageSize"
      :total="totalPage"
      layout="total, sizes, prev, pager, next, jumper"
      style="margin-top: 20px; text-align: right">
    </el-pagination>

    <div class="detail-tabs-wrap">
      <el-tabs v-model="detailTabName" type="border-card">
        <el-tab-pane label="节点状态和日志" name="statusLogs">
          <div v-if="selectedOrder.orderNo" class="two-column-layout">
            <div class="stages-column">
              <div class="column-header">
                <i class="el-icon-s-order"></i>
                <span>节点流程</span>
                <span class="progress-badge">{{ selectedOrderProgressPercent }}%</span>
              </div>
              <div class="stages-list">
                <div
                  v-for="(stage, index) in selectedOrderNodeList"
                  :key="stage.nodeCode || index"
                  class="stage-item"
                  :class="'stage-' + getStageClass(stage.status)">
                  <div class="stage-icon">
                    <i :class="getStageIcon(stage.status)"></i>
                  </div>
                  <div class="stage-content">
                    <div class="stage-name">{{ stage.nodeName }}</div>
                    <div class="stage-meta">
                      <el-tag :type="getStageTagType(stage.status)" size="mini" effect="plain">{{ stage.status || '未开始' }}</el-tag>
                      <span class="stage-owner">负责人：{{ stage.assigneeUserName || '-' }}</span>
                    </div>
                  </div>
                </div>
              </div>
            </div>
            <div class="logs-column">
              <div class="column-header">
                <i class="el-icon-tickets"></i>
                <span>操作日志</span>
                <span class="logs-count">{{ selectedOrderLogList.length }}条</span>
              </div>
              <div class="logs-table-wrapper">
                <el-table :data="selectedOrderLogList" border size="small" class="detail-table" v-loading="detailLogLoading" height="295px">
                  <el-table-column prop="logTime" label="时间" width="160" align="center"></el-table-column>
                  <el-table-column prop="action" label="操作" width="95" align="center"></el-table-column>
                  <el-table-column prop="nodeName" label="节点" width="130" align="center">
                    <template slot-scope="scope">{{ scope.row.nodeName || scope.row.nodeCode || '-' }}</template>
                  </el-table-column>
                  <el-table-column prop="operatorName" label="操作人" width="100" align="center"></el-table-column>
                  <el-table-column prop="comment" label="备注" min-width="220" show-overflow-tooltip>
                    <template slot-scope="scope">{{ scope.row.comment || '-' }}</template>
                  </el-table-column>
                  <el-table-column label="影像" width="90" align="center">
                    <template slot-scope="scope">
                      <a
                        v-if="isMediaNodeLog(scope.row)"
                        type="text"
                        size="mini"
                        @click="openMediaFileDialog(scope.row)">
                        查看文件
                      </a>
                      <span v-else>-</span>
                    </template>
                  </el-table-column>
                </el-table>
              </div>
            </div>
          </div>
          <el-empty v-else description="请先选择一条任务记录"></el-empty>
        </el-tab-pane>
      </el-tabs>
    </div>

    <el-dialog :title="saveHeaderData.orderNo ? '修改机加工生产任务单' : '新增机加工生产任务单'" :visible.sync="setUp.reviewFlag" width="550px" :close-on-click-modal="false" v-drag>
      <el-form
        ref="editForm"
        :model="saveHeaderData"
        label-position="top"
        class="edit-form">
        <el-row :gutter="20">
          <el-col :span="12"><el-form-item label="物料号" required><el-input v-model="saveHeaderData.modelNo" :disabled="isDateOnlyEditMode()"></el-input></el-form-item></el-col>
          <el-col :span="12"><el-form-item label="物料名称" required><el-input v-model="saveHeaderData.materialName" :disabled="isDateOnlyEditMode()"></el-input></el-form-item></el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12"><el-form-item label="规格" required><el-input v-model="saveHeaderData.materialSpec" :disabled="isDateOnlyEditMode()"></el-input></el-form-item></el-col>
          <el-col :span="12"><el-form-item label="计划数量" required><el-input v-model="saveHeaderData.taskQty" :min="1" :max="999999" style="width: 100%" :disabled="isDateOnlyEditMode()"></el-input></el-form-item></el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12"><el-form-item label="计划完工日期" required><el-date-picker v-model="saveHeaderData.planFinishDate" type="date" value-format="yyyy-MM-dd" style="width: 100%"></el-date-picker></el-form-item></el-col>
          <el-col :span="12"><el-form-item label="状态"><el-select disabled v-model="saveHeaderData.status" style="width: 100%"><el-option label="已排产" value="已排产"></el-option><el-option label="进行中" value="进行中"></el-option><el-option label="已完成" value="已完成"></el-option></el-select></el-form-item></el-col>
        </el-row>
<!--        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item>
              <template slot="label">
                人员分配策略
                <el-tooltip effect="dark" placement="top">
                  <div slot="content">
                    默认分配：创建订单后，系统会按节点角色自动分配该角色下全部人员。<br>
                    手动分配：创建订单后不自动分配，需要在“分配人员”里手工选择负责人。
                  </div>
                  <i class="el-icon-question" style="margin-left:6px;color:#909399;cursor:pointer;"></i>
                </el-tooltip>
              </template>
              <el-radio-group v-model="saveHeaderData.autoAssignAllUsers">
                <el-radio :label="true">默认分配</el-radio>
                <el-radio :label="false">手动分配</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="节点报工模式">
              <el-radio-group v-model="saveHeaderData.nodeReportMode">
                <el-radio label="PARALLEL">并行</el-radio>
                <el-radio label="SEQUENTIAL">串行</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>-->
      </el-form>
      <el-footer style="height: 40px; margin-top: 50px; text-align: center">
        <el-button plain class="reset-btn" @click="setUp.reviewFlag = false">取消</el-button>
        <el-button plain class="add-btn" :loading="setUp.saveButton" @click="saveTask">保存</el-button>
      </el-footer>
    </el-dialog>

    <el-dialog title="机加工节点报工" :visible.sync="setUp.reportFlag" width="460px" :close-on-click-modal="false" @close="handleReportDialogClose" v-drag>
      <el-form :model="reportData" label-width="110px" label-position="top">
        <el-form-item label="任务单号"><el-input v-model="reportData.taskNo" disabled></el-input></el-form-item>
        <el-form-item label="报工节点">
          <el-select v-model="reportData.nodeCode" placeholder="请选择节点" style="width: 100%" @change="handleReportNodeChange">
            <el-option v-for="item in reportNodeOptions" :key="item.nodeCode" :label="item.nodeName" :value="item.nodeCode"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item v-if="isMachiningProductionNode(reportData.nodeCode)" label="本次报工数量">
          <el-input v-model="reportData.reportQty" :min="1" :max="999999" style="width: 100%"></el-input>
        </el-form-item>
        <el-form-item v-if="isMachiningInspectionNode(reportData.nodeCode)" label="检验影像上传">
          <div class="inspection-upload-wrap">
            <el-button plain type="primary" size="mini" @click="triggerInspectionImagePicker">选择图片</el-button>
            <span class="inspection-upload-tip">支持图片格式，可多选上传</span>
            <input
              ref="inspectionImageInput"
              class="inspection-file-input"
              type="file"
              accept="image/*"
              multiple
              @change="handleInspectionImageSelect">
            <div v-if="inspectionImageList.length > 0" class="inspection-file-list">
              <div v-for="(item, index) in inspectionImageList" :key="item.uid" class="inspection-file-item">
                <span class="inspection-file-name">{{ item.name }}</span>
                <el-button type="text" style="color:#F56C6C" @click="removeInspectionImage(index)">删除</el-button>
              </div>
            </div>
            <div v-else class="inspection-upload-empty">请至少上传1张检验图片</div>
          </div>
        </el-form-item>
        <el-form-item label="报工备注"><el-input v-model="reportData.remark" type="textarea" :rows="2"></el-input></el-form-item>
      </el-form>
      <el-footer style="height: 40px; margin-top: 50px; text-align: center">
        <el-button plain class="reset-btn" @click="setUp.reportFlag = false">取消</el-button>
        <el-button plain class="search-btn" :loading="setUp.reportButton" @click="submitNodeReport">提交报工</el-button>
      </el-footer>
    </el-dialog>

    <el-dialog title="节点负责人分配" :visible.sync="setUp.assignFlag" width="550px" :close-on-click-modal="false" v-drag>
      <el-table :data="assignNodeList" border class="data-table">
        <el-table-column prop="nodeName" label="节点" width="150"></el-table-column>
        <el-table-column label="负责人" width="370">
          <template slot-scope="scope">
            <el-select v-model="scope.row.assigneeUserIdList" filterable clearable multiple placeholder="请选择负责人" style="width: 100%">
              <el-option v-for="user in scope.row.userOptions" :key="user.userId" :label="user.displayName || user.username" :value="user.userId"></el-option>
            </el-select>
          </template>
        </el-table-column>
      </el-table>
      <el-footer style="height: 40px; margin-top: 20px; text-align: center">
        <el-button plain class="reset-btn" @click="setUp.assignFlag = false">取消</el-button>
        <el-button plain class="add-btn" :loading="setUp.assignButton" @click="saveNodeAssigneeAction">保存分配</el-button>
      </el-footer>
    </el-dialog>

    <el-dialog
      title="报工影像文件"
      :visible.sync="mediaDialogVisible"
      width="680px"
      :close-on-click-modal="false"
      @close="handleMediaDialogClose"
      v-drag>
      <div v-loading="mediaDialogLoading" class="media-dialog-body">
        <div class="media-dialog-meta">
          <span>节点：{{ mediaDialogLog.nodeName || mediaDialogLog.nodeCode || '-' }}</span>
          <span>报工时间：{{ mediaDialogLog.logTime || '-' }}</span>
        </div>
        <el-table v-if="mediaFileList.length > 0" class="file-table" :data="mediaFileList" border size="small" height="360px">
          <el-table-column type="index" label="#" width="55" align="center"></el-table-column>
          <el-table-column label="缩略图" width="150" align="center">
            <template slot-scope="scope">
              <div class="media-thumb-cell">
                <el-button v-if="resolveMediaKind(scope.row) === 'other'" type="text" size="mini" disabled>不支持</el-button>
                <span v-else-if="scope.row.previewLoading" class="media-thumb-loading">加载中...</span>
                <img
                  v-else-if="resolveMediaKind(scope.row) === 'image' && scope.row.previewUrl"
                  :src="scope.row.previewUrl"
                  class="media-thumb media-thumb-image"
                  alt="media-thumb"
                  @click="previewMediaFile(scope.row)">
                <video
                  v-else-if="resolveMediaKind(scope.row) === 'video'"
                  :src="getVideoStreamUrl(scope.row)"
                  class="media-thumb media-thumb-video"
                  muted
                  playsinline
                  preload="metadata"
                  @click="previewMediaFile(scope.row)">
                </video>
                <el-button v-else type="text" size="mini" @click="previewMediaFile(scope.row)">加载预览</el-button>
              </div>
            </template>
          </el-table-column>
          <el-table-column label="类型" width="90" align="center">
            <template slot-scope="scope">
              <el-tag size="mini" :type="resolveMediaKind(scope.row) === 'video' ? 'warning' : (resolveMediaKind(scope.row) === 'image' ? 'success' : 'info')">
                {{ getMediaKindLabel(scope.row) }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="步骤" width="120" align="center">
            <template slot-scope="scope">{{ getMediaStepLabel(scope.row) }}</template>
          </el-table-column>
          <el-table-column label="上传时间" min-width="170" align="center">
            <template slot-scope="scope">{{ scope.row.createDate || '-' }}</template>
          </el-table-column>
          <el-table-column label="操作" width="60" align="center">
            <template slot-scope="scope">
              <el-button
                type="text"
                style="color:#F56C6C"
                :loading="scope.row.deleting"
                @click="deleteMediaFile(scope.row)">
                删除
              </el-button>
            </template>
          </el-table-column>
        </el-table>
        <el-empty v-else description="当前节点暂无影像文件"></el-empty>
      </div>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="mediaDialogVisible = false">关闭</el-button>
      </div>
    </el-dialog>

    <div v-if="mediaPreviewVisible" class="media-preview-overlay" @click="closeMediaPreview">
      <i class="el-icon-close media-preview-close" @click.stop="closeMediaPreview"></i>
      <img
        v-if="mediaPreviewType === 'image'"
        :src="mediaPreviewUrl"
        :alt="mediaPreviewName || 'media-preview'"
        class="media-preview-image"
        @click.stop="closeMediaPreview">
      <video
        v-else-if="mediaPreviewType === 'video'"
        ref="mediaPreviewVideo"
        :key="mediaPreviewUrl"
        :src="mediaPreviewUrl"
        class="media-preview-video"
        controls
        autoplay
        playsinline
        preload="auto"
        @error="handleMediaPreviewError"
        @click.stop>
      </video>
    </div>
  </div>
</template>

<script>
import { deleteMachiningTask, finishMachiningTask, getMachiningTaskList, getNodeAssigneeList, getNodeAssigneeUsers, getReportLogList, reportMachiningTaskNode, reportWorkNodeWithMedia, saveMachiningTask, saveNodeAssignee } from '@/api/longchuang/productionPlan'
import { getOssVideoStreamUrl, previewOssFileById2, queryOssFilePlus, removeOss } from '@/api/oss/oss'

export default {
  name: 'ProductionPlanMachiningTask',
  data() {
    return {
      searchData: { modelNo: '', materialName: '', materialSpec: '', status: '', planStartDate: '', planEndDate: '', page: 1, limit: 20 },
      saveHeaderData: {},
      reportData: { orderNo: '', taskNo: '', nodeCode: 'machiningProduction', reportQty: '', remark: '' },
      reportNodeOptions: [],
      inspectionImageList: [],
      setUp: { reviewFlag: false, reportFlag: false, assignFlag: false, saveButton: false, reportButton: false, assignButton: false },
      dataList: [],
      currentAssignOrder: { orderNo: '', orderType: 'MACHINING' },
      assignNodeList: [],
      selectedOrder: {},
      detailTabName: 'statusLogs',
      selectedOrderLogList: [],
      detailLogLoading: false,
      mediaDialogVisible: false,
      mediaDialogLoading: false,
      mediaDialogLog: {},
      mediaFileList: [],
      mediaPreviewVisible: false,
      mediaPreviewUrl: '',
      mediaPreviewType: '',
      mediaPreviewName: '',
      pageIndex: 1,
      pageSize: 20,
      totalPage: 0,
      dataListLoading: false,
      tableHeight: (window.innerHeight - 320) / 2
    }
  },
  activated() {
    this.searchTable()
  },
  beforeDestroy() {
    this.releaseMediaFileUrls()
  },
  methods: {
    isDateOnlyEditMode() {
      const status = String(this.saveHeaderData.status || '').trim()
      return !!this.saveHeaderData.orderNo && !!status && status !== '已排产'
    },
    searchTable(flag) {
      if (flag === 'Y') this.pageIndex = 1
      this.searchData.page = this.pageIndex
      this.searchData.limit = this.pageSize
      this.dataListLoading = true
      getMachiningTaskList(this.searchData).then(({ data }) => {
        this.dataListLoading = false
        if (data && data.code === 0) {
          this.dataList = (data.page.list || []).map(this.normalizeRow)
          this.totalPage = data.page.totalCount || 0
          this.syncSelectedOrder()
        } else this.loadMockData()
      }).catch(() => {
        this.dataListLoading = false
        this.loadMockData()
      })
    },
    normalizeRow(row) {
      const list = row.nodeList || []
      const done = list.filter(item => item.status === '已完成').length
      const currentNode = (list.find(item => item.status !== '已完成') || {}).nodeName || '全部完成'
      const assigneeSummary = list.filter(item => item.assigneeUserName).map(item => `${item.nodeName}:${item.assigneeUserName}`).join('；')
      return { ...row, autoAssignAllUsers: !!row.autoAssignAllUsers, nodeReportMode: row.nodeReportMode || 'SEQUENTIAL', nodeList: list, nodeDoneCount: done, nodeTotalCount: list.length, currentNode: row.currentNode || currentNode, reportQty: row.reportQty || 0, assigneeSummary: assigneeSummary || '-' }
    },
    getMachiningDefaultNodeList() {
      return [
        { nodeCode: 'machiningProduction', nodeName: '机加工生产', status: '未开始' },
        { nodeCode: 'machiningInspection', nodeName: '机加工检验', status: '未开始' }
      ]
    },
    isMachiningProductionNode(nodeCode) {
      return nodeCode === 'machiningProduction'
    },
    isMachiningInspectionNode(nodeCode) {
      return nodeCode === 'machiningInspection'
    },
    loadMockData() {
      this.dataList = [
        {
          orderNo: 'MOCK-MACH-001',
          projectNo: 'MAT-120001',
          modelNo: 'MAT-120001',
          materialName: '导轨压板',
          materialSpec: 'DG-80-12',
          taskQty: 120,
          reportQty: 30,
          autoAssignAllUsers: true,
          nodeReportMode: 'SEQUENTIAL',
          planFinishDate: '2026-05-25',
          finishDate: '',
          status: '进行中',
          nodeList: [
            { nodeCode: 'machiningProduction', nodeName: '机加工生产', status: '已完成' },
            { nodeCode: 'machiningInspection', nodeName: '机加工检验', status: '进行中' }
          ]
        },
        {
          orderNo: 'MOCK-MACH-002',
          projectNo: 'MAT-120188',
          modelNo: 'MAT-120188',
          materialName: '轿底连接件',
          materialSpec: 'JD-LJ-06',
          taskQty: 240,
          reportQty: 0,
          autoAssignAllUsers: true,
          nodeReportMode: 'SEQUENTIAL',
          planFinishDate: '2026-05-27',
          finishDate: '',
          status: '已排产',
          nodeList: this.getMachiningDefaultNodeList()
        }
      ].map(this.normalizeRow)
      this.totalPage = this.dataList.length
      this.syncSelectedOrder()
    },
    syncSelectedOrder() {
      if (!this.dataList.length) {
        this.selectedOrder = {}
        this.selectedOrderLogList = []
        return
      }
      const selectedOrderNo = this.selectedOrder && this.selectedOrder.orderNo
      const matched = selectedOrderNo ? this.dataList.find(item => item.orderNo === selectedOrderNo) : null
      const current = matched || this.dataList[0]
      this.selectedOrder = current
      this.$nextTick(() => {
        if (this.$refs.orderTable && current) {
          this.$refs.orderTable.setCurrentRow(current)
        }
      })
      this.loadSelectedOrderLogList(current)
    },
    onCurrentRowChange(row) {
      if (!row || !row.orderNo) {
        this.selectedOrder = {}
        this.selectedOrderLogList = []
        return
      }
      this.selectedOrder = row
      this.loadSelectedOrderLogList(row)
    },
    loadSelectedOrderLogList(row) {
      if (!row || !row.orderNo) {
        this.selectedOrderLogList = []
        return
      }
      this.detailLogLoading = true
      getReportLogList({ orderNo: row.orderNo, orderType: 'MACHINING' }).then(({ data }) => {
        this.detailLogLoading = false
        this.selectedOrderLogList = data && data.code === 0 ? (data.rows || []) : []
      }).catch(() => {
        this.detailLogLoading = false
        this.selectedOrderLogList = []
      })
    },
    isMediaNodeLog(row) {
      if (!row || !row.logNo) {
        return false
      }
      if (row.action && row.action !== '报工完成') {
        return false
      }
      const codeSet = ['machiningInspection']
      const nameSet = ['机加工检验', '检验']
      const nodeCode = String(row.nodeCode || '').trim()
      const nodeName = String(row.nodeName || '').replace(/\s+/g, '')
      return codeSet.includes(nodeCode) || nameSet.some(item => item.replace(/\s+/g, '') === nodeName)
    },
    async openMediaFileDialog(row) {
      if (!row || !row.logNo || !row.nodeCode) {
        this.$message.warning('当前日志未关联影像文件')
        return
      }
      this.releaseMediaFileUrls()
      this.closeMediaPreview()
      this.mediaFileList = []
      this.mediaDialogLog = { ...row }
      this.mediaDialogVisible = true
      this.mediaDialogLoading = true
      try {
        const { data } = await queryOssFilePlus({
          orderRef1: this.selectedOrder.orderNo || row.orderNo,
          orderRef2: row.nodeCode
        })
        this.mediaFileList = (data && data.code === 0 ? (data.rows || []) : []).map(item => ({
          ...item,
          previewUrl: '',
          previewLoading: false,
          deleting: false
        }))
        this.mediaFileList.forEach(item => {
          const kind = this.resolveMediaKind(item)
          if (kind === 'image') {
            this.loadMediaPreviewUrl(item, kind, false)
          }
        })
      } catch (e) {
        this.mediaFileList = []
        this.$message.error('影像文件加载失败')
      } finally {
        this.mediaDialogLoading = false
      }
    },
    handleMediaDialogClose() {
      this.mediaDialogLoading = false
      this.mediaDialogLog = {}
      this.releaseMediaFileUrls()
      this.mediaFileList = []
      this.closeMediaPreview()
    },
    releaseMediaFileUrls() {
      this.mediaFileList.forEach(item => {
        if (item && item.previewUrl) {
          URL.revokeObjectURL(item.previewUrl)
        }
      })
    },
    getMediaExt(fileRow) {
      if (!fileRow) {
        return ''
      }
      const ext = fileRow.fileType || fileRow.fileSuffix || this.getExtFromFileName(fileRow.fileName || fileRow.newFileName || '')
      return String(ext).replace(/^\./, '').toLowerCase()
    },
    getExtFromFileName(fileName) {
      const name = String(fileName || '')
      const dotIndex = name.lastIndexOf('.')
      if (dotIndex < 0 || dotIndex >= name.length - 1) {
        return ''
      }
      return name.slice(dotIndex + 1)
    },
    resolveMediaKind(fileRow) {
      const ext = this.getMediaExt(fileRow)
      const imageExtList = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
      const videoExtList = ['mp4', 'webm', 'mov', 'avi', 'm4v', '3gp']
      if (imageExtList.includes(ext)) {
        return 'image'
      }
      if (videoExtList.includes(ext)) {
        return 'video'
      }
      return 'other'
    },
    getMediaKindLabel(fileRow) {
      const kind = this.resolveMediaKind(fileRow)
      if (kind === 'image') {
        return '照片'
      }
      if (kind === 'video') {
        return '视频'
      }
      return '其他'
    },
    getMediaStepLabel(fileRow) {
      const step = String((fileRow && fileRow.orderRef6) || '').replace(/\s+/g, '')
      const stepList = ['1平台箱', '2门箱', '3背景墙箱', '4框架箱', '5钣金箱', '6铝型材箱']
      return stepList.includes(step) ? step : '-'
    },
    buildPreviewMimeType(fileRow, kind) {
      const ext = this.getMediaExt(fileRow)
      if (kind === 'image') {
        const imageMimeMap = {
          jpg: 'image/jpeg',
          jpeg: 'image/jpeg',
          png: 'image/png',
          gif: 'image/gif',
          bmp: 'image/bmp',
          webp: 'image/webp'
        }
        return imageMimeMap[ext] || 'image/jpeg'
      }
      if (kind === 'video') {
        const videoMimeMap = {
          mp4: 'video/mp4',
          m4v: 'video/mp4',
          webm: 'video/webm',
          mov: 'video/quicktime',
          avi: 'video/x-msvideo',
          '3gp': 'video/3gpp'
        }
        return videoMimeMap[ext] || 'video/mp4'
      }
      return 'application/octet-stream'
    },
    async loadMediaPreviewUrl(fileRow, fixedKind = '', showError = true) {
      if (!fileRow || !fileRow.id) {
        return false
      }
      const kind = fixedKind || this.resolveMediaKind(fileRow)
      if (kind === 'other') {
        return false
      }
      if (fileRow.previewUrl) {
        return true
      }
      if (fileRow.previewLoading) {
        return false
      }
      this.$set(fileRow, 'previewLoading', true)
      const mimeType = this.buildPreviewMimeType(fileRow, kind)
      try {
        const { data } = await previewOssFileById2({ id: fileRow.id, fileType: mimeType })
        const sourceBlob = data instanceof Blob ? data : new Blob([data])
        const blobType = mimeType || sourceBlob.type || 'application/octet-stream'
        const blob = new Blob([sourceBlob], { type: blobType })
        this.$set(fileRow, 'previewUrl', URL.createObjectURL(blob))
        return true
      } catch (e) {
        if (showError) {
          this.$message.error('文件预览加载失败')
        }
        return false
      } finally {
        this.$set(fileRow, 'previewLoading', false)
      }
    },
    getVideoStreamUrl(fileRow) {
      if (!fileRow || !fileRow.id) {
        return ''
      }
      return getOssVideoStreamUrl(fileRow.id)
    },
    async previewMediaFile(fileRow) {
      const kind = this.resolveMediaKind(fileRow)
      if (kind === 'other') {
        this.$message.warning('当前文件暂不支持预览')
        return
      }
      if (!fileRow || !fileRow.id) {
        this.$message.warning('文件信息不完整，无法预览')
        return
      }
      if (kind === 'video') {
        const videoUrl = this.getVideoStreamUrl(fileRow)
        if (!videoUrl) {
          this.$message.warning('视频地址无效，无法预览')
          return
        }
        this.mediaPreviewType = 'video'
        this.mediaPreviewName = fileRow.fileName || fileRow.newFileName || ''
        this.mediaPreviewUrl = videoUrl
        this.mediaPreviewVisible = true
        this.$nextTick(() => {
          const previewVideo = this.$refs.mediaPreviewVideo
          if (previewVideo && typeof previewVideo.play === 'function') {
            const playPromise = previewVideo.play()
            if (playPromise && typeof playPromise.catch === 'function') {
              playPromise.catch(() => {})
            }
          }
        })
        return
      }
      const loaded = await this.loadMediaPreviewUrl(fileRow, kind, true)
      if (!loaded) {
        return
      }
      this.mediaPreviewType = kind
      this.mediaPreviewName = fileRow.fileName || fileRow.newFileName || ''
      this.mediaPreviewUrl = fileRow.previewUrl
      this.mediaPreviewVisible = true
    },
    closeMediaPreview() {
      const previewVideo = this.$refs.mediaPreviewVideo
      if (previewVideo && typeof previewVideo.pause === 'function') {
        previewVideo.pause()
      }
      this.mediaPreviewVisible = false
      this.mediaPreviewType = ''
      this.mediaPreviewName = ''
      this.mediaPreviewUrl = ''
    },
    handleMediaPreviewError() {
      this.$message.warning('视频播放失败，请稍后重试')
    },
    async deleteMediaFile(fileRow) {
      const fileId = Number(fileRow && fileRow.id)
      if (!Number.isFinite(fileId) || fileId <= 0) {
        this.$message.warning('文件ID无效，无法删除')
        return
      }
      try {
        await this.$confirm('确定删除该影像文件吗？删除后不可恢复。', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
      } catch (e) {
        return
      }
      this.$set(fileRow, 'deleting', true)
      try {
        const { data } = await removeOss([fileId])
        if (!data || data.code !== 0) {
          this.$message.error((data && data.msg) || '删除失败')
          return
        }
        const currentPreviewUrl = this.mediaPreviewUrl || ''
        const isCurrentImage = !!(fileRow.previewUrl && currentPreviewUrl && fileRow.previewUrl === currentPreviewUrl)
        const isCurrentVideo = !!(currentPreviewUrl && currentPreviewUrl.indexOf(`/video/${fileId}`) > -1)
        if (isCurrentImage || isCurrentVideo) {
          this.closeMediaPreview()
        }
        if (fileRow.previewUrl) {
          URL.revokeObjectURL(fileRow.previewUrl)
        }
        this.mediaFileList = this.mediaFileList.filter(item => Number(item.id) !== fileId)
        this.$message.success('删除成功')
      } catch (e) {
        this.$message.error('删除失败')
      } finally {
        this.$set(fileRow, 'deleting', false)
      }
    },
    resetQuery() {
      this.searchData = { modelNo: '', materialName: '', materialSpec: '', status: '', planStartDate: '', planEndDate: '', page: 1, limit: 20 }
      this.searchTable('Y')
    },
    openEditDialog(row) {
      this.saveHeaderData = row
        ? { ...row, autoAssignAllUsers: !!row.autoAssignAllUsers, nodeReportMode: row.nodeReportMode || 'SEQUENTIAL' }
        : { orderNo: '', modelNo: '', materialName: '', materialSpec: '', taskQty: 1, reportQty: 0, planFinishDate: '', status: '已排产', autoAssignAllUsers: true, nodeReportMode: 'SEQUENTIAL', nodeList: [] }
      this.setUp.reviewFlag = true
    },
    saveTask() {
      if (!this.saveHeaderData.modelNo) return this.$message.warning('请先填写物料号')
      if (!this.saveHeaderData.materialName) return this.$message.warning('请先填写物料名称')
      if (!this.saveHeaderData.materialSpec) return this.$message.warning('请先填写规格')
      if (!this.saveHeaderData.taskQty || Number(this.saveHeaderData.taskQty) <= 0) return this.$message.warning('请输入有效的计划数量')
      if (!this.saveHeaderData.planFinishDate) return this.$message.warning('请选择计划完工日期')
      const payload = {
        ...this.saveHeaderData,
        projectNo: this.saveHeaderData.modelNo,
        nodeList: this.saveHeaderData.nodeList && this.saveHeaderData.nodeList.length
          ? this.saveHeaderData.nodeList
          : this.getMachiningDefaultNodeList()
      }
      this.setUp.saveButton = true
      saveMachiningTask(payload).then(({ data }) => {
        this.setUp.saveButton = false
        if (data && data.code === 0) {
          this.$message.success(data.msg || '保存成功')
          this.setUp.reviewFlag = false
          this.searchTable()
        } else this.$message.error(data.msg || '保存失败')
      }).catch(() => {
        this.setUp.saveButton = false
        const orderNo = this.saveHeaderData.orderNo || String(Date.now())
        const index = this.dataList.findIndex(item => item.orderNo === orderNo)
        const saveData = this.normalizeRow({
          ...payload,
          orderNo: orderNo
        })
        if (index > -1) this.$set(this.dataList, index, saveData)
        else this.dataList.unshift(saveData)
        this.totalPage = this.dataList.length
        this.setUp.reviewFlag = false
        this.$message.success('后端未完成，已在前端演示保存')
      })
    },
    openReportDialog(row) {
      this.reportNodeOptions = (row.nodeList || []).filter(item => item.status !== '已完成')
      const defaultNode = this.reportNodeOptions[0] || {}
      const nodeCode = defaultNode.nodeCode || 'machiningProduction'
      this.reportData = {
        orderNo: row.orderNo,
        taskNo: row.orderNo,
        nodeCode: nodeCode,
        reportQty: this.isMachiningProductionNode(nodeCode) ? '' : 1,
        remark: ''
      }
      this.clearInspectionImageList()
      this.setUp.reportFlag = true
    },
    handleReportNodeChange(nodeCode) {
      if (this.isMachiningProductionNode(nodeCode)) {
        this.reportData.reportQty = ''
      } else {
        this.reportData.reportQty = 1
      }
      if (!this.isMachiningInspectionNode(nodeCode)) {
        this.clearInspectionImageList()
      }
    },
    handleReportDialogClose() {
      this.clearInspectionImageList()
    },
    triggerInspectionImagePicker() {
      if (this.$refs.inspectionImageInput) {
        this.$refs.inspectionImageInput.click()
      }
    },
    handleInspectionImageSelect(event) {
      const fileList = Array.from((event && event.target && event.target.files) || [])
      if (!fileList.length) {
        return
      }
      let invalidCount = 0
      fileList.forEach(file => {
        const fileType = String((file && file.type) || '').toLowerCase()
        const fileName = String((file && file.name) || '').toLowerCase()
        const isImage = fileType.indexOf('image/') === 0 || /\.(jpg|jpeg|png|gif|bmp|webp)$/.test(fileName)
        if (!file || !isImage) {
          invalidCount++
          return
        }
        this.inspectionImageList.push({
          uid: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
          name: file.name || 'inspection-image',
          raw: file
        })
      })
      if (invalidCount > 0) {
        this.$message.warning('仅支持上传图片文件')
      }
      if (event && event.target) {
        event.target.value = ''
      }
    },
    removeInspectionImage(index) {
      this.inspectionImageList.splice(index, 1)
    },
    clearInspectionImageList() {
      this.inspectionImageList = []
      if (this.$refs.inspectionImageInput) {
        this.$refs.inspectionImageInput.value = ''
      }
    },
    openAssignDialog(row) {
      if (!row.orderNo) return this.$message.warning('请先保存任务单后再分配人员')
      this.currentAssignOrder = { orderNo: row.orderNo, orderType: 'MACHINING' }
      getNodeAssigneeList({ orderNo: row.orderNo, orderType: 'MACHINING' }).then(({ data }) => {
        const assignRows = data && data.code === 0 ? (data.rows || []) : []
        this.assignNodeList = assignRows.map(item => ({ ...item, assigneeUserIdList: item.assigneeUserIdList || [], userOptions: [] }))
        const requests = this.assignNodeList.map(item =>
          getNodeAssigneeUsers({ orderType: 'MACHINING', nodeCode: item.nodeCode }).then(({ data: userData }) => {
            item.userOptions = userData && userData.code === 0 ? (userData.rows || []) : []
          }).catch(() => { item.userOptions = [] })
        )
        Promise.all(requests).finally(() => { this.setUp.assignFlag = true })
      }).catch(() => {
        this.$message.error('加载节点分配信息失败')
      })
    },
    saveNodeAssigneeAction() {
      this.setUp.assignButton = true
      const assigneeList = this.assignNodeList.map(item => ({
        nodeCode: item.nodeCode,
        nodeName: item.nodeName,
        assigneeUserIdList: item.assigneeUserIdList || []
      }))
      saveNodeAssignee({
        orderNo: this.currentAssignOrder.orderNo,
        orderType: this.currentAssignOrder.orderType,
        assigneeList: assigneeList
      }).then(({ data }) => {
        this.setUp.assignButton = false
        if (data && data.code === 0) {
          this.$message.success(data.msg || '分配成功')
          this.setUp.assignFlag = false
          this.searchTable()
        } else this.$message.error(data.msg || '分配失败')
      }).catch(() => {
        this.setUp.assignButton = false
        this.$message.error('分配失败')
      })
    },
    submitNodeReport() {
      if (!this.reportData.nodeCode) this.reportData.nodeCode = 'machiningProduction'
      if (this.isMachiningProductionNode(this.reportData.nodeCode)) {
        const qty = Number(this.reportData.reportQty)
        if (!Number.isFinite(qty) || qty <= 0) return this.$message.warning('请输入有效报工数量')
        this.submitMachiningProductionReport(qty)
        return
      }
      if (this.isMachiningInspectionNode(this.reportData.nodeCode)) {
        this.submitMachiningInspectionReport()
        return
      }
      this.$message.warning('当前节点暂不支持报工')
    },
    submitMachiningProductionReport(reportQty) {
      const payload = {
        ...this.reportData,
        reportQty: reportQty
      }
      this.setUp.reportButton = true
      reportMachiningTaskNode(payload).then(({ data }) => {
        this.setUp.reportButton = false
        if (data && data.code === 0) {
          this.$message.success(data.msg || '报工成功')
          this.setUp.reportFlag = false
          this.searchTable()
        } else this.$message.error(data.msg || '报工失败')
      }).catch(() => {
        this.setUp.reportButton = false
        this.simulateNodeReport(this.reportData.orderNo, this.reportData.nodeCode, reportQty)
        this.setUp.reportFlag = false
        this.$message.success('后端未完成，已在前端演示节点报工')
      })
    },
    submitMachiningInspectionReport() {
      const uploadFileList = this.inspectionImageList.map(item => item.raw).filter(item => !!item)
      if (!uploadFileList.length) {
        return this.$message.warning('请先上传检验图片')
      }
      const formData = new FormData()
      formData.append('orderNo', this.reportData.orderNo)
      formData.append('orderType', 'MACHINING')
      formData.append('nodeCode', this.reportData.nodeCode)
      formData.append('reportQty', '1')
      formData.append('remark', this.reportData.remark || '')
      formData.append('reportNode', 'true')
      uploadFileList.forEach(file => {
        formData.append('file', file, file.name || 'inspection-image')
      })
      this.setUp.reportButton = true
      reportWorkNodeWithMedia(formData).then(({ data }) => {
        this.setUp.reportButton = false
        if (data && data.code === 0) {
          this.$message.success(data.msg || '检验报工成功')
          this.setUp.reportFlag = false
          this.clearInspectionImageList()
          this.searchTable()
        } else {
          this.$message.error(data.msg || '检验报工失败')
        }
      }).catch(() => {
        this.setUp.reportButton = false
        this.simulateNodeReport(this.reportData.orderNo, this.reportData.nodeCode, 0)
        this.setUp.reportFlag = false
        this.clearInspectionImageList()
        this.$message.success('后端未完成，已在前端演示检验报工')
      })
    },
    simulateNodeReport(orderNo, nodeCode, qty) {
      const row = this.dataList.find(item => item.orderNo === orderNo)
      if (!row) return
      const node = row.nodeList.find(item => item.nodeCode === nodeCode)
      if (!node) return
      node.status = '已完成'
      if (this.isMachiningProductionNode(nodeCode)) {
        row.reportQty = Number(row.reportQty || 0) + Number(qty || 0)
      }
      row.status = '进行中'
      const nextNode = row.nodeList.find(item => item.status !== '已完成')
      row.currentNode = nextNode ? nextNode.nodeName : '全部完成'
      row.nodeDoneCount = row.nodeList.filter(item => item.status === '已完成').length
      row.finishDate = ''
    },
    finishTask(row) {
      const targetRow = row || this.selectedOrder
      if (!targetRow || !targetRow.orderNo) {
        this.$message.warning('请先选择一条任务记录')
        return
      }
      if (targetRow.status === '已完成') {
        this.$message.warning('该任务单已完工')
        return
      }
      this.$confirm('确定将该任务单标记为完工吗？', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
        finishMachiningTask({ orderNo: targetRow.orderNo }).then(({ data }) => {
          if (data && data.code === 0) {
            this.$message.success(data.msg || '完工成功')
            this.searchTable()
          } else this.$message.error(data.msg || '完工失败')
        }).catch(() => {
          targetRow.status = '已完成'
          targetRow.finishDate = this.dayjs().format('YYYY-MM-DD')
          targetRow.nodeList = targetRow.nodeList.map(item => ({ ...item, status: '已完成' }))
          targetRow.currentNode = '全部完成'
          targetRow.nodeDoneCount = targetRow.nodeTotalCount
          this.$message.success('后端未完成，已在前端演示完工')
        })
      }).catch(() => {})
    },
    deleteTask(row) {
      this.$confirm('确定删除该任务单吗？', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
        deleteMachiningTask({ orderNo: row.orderNo }).then(({ data }) => {
          if (data && data.code === 0) {
            this.$message.success(data.msg || '删除成功')
            this.searchTable()
          } else this.$message.error(data.msg || '删除失败')
        }).catch(() => {
          this.dataList = this.dataList.filter(item => item.orderNo !== row.orderNo)
          this.totalPage = this.dataList.length
          this.$message.success('后端未完成，已在前端演示删除')
        })
      }).catch(() => {})
    },
    getStatusType(status) {
      const map = { 已排产: 'info', 进行中: 'warning', 已完成: 'success' }
      return map[status] || 'info'
    },
    getStageClass(status) {
      if (status === '已完成') return 'done'
      if (status === '进行中') return 'processing'
      return 'pending'
    },
    getStageIcon(status) {
      if (status === '已完成') return 'el-icon-check'
      if (status === '进行中') return 'el-icon-loading'
      return 'el-icon-time'
    },
    getStageTagType(status) {
      if (status === '已完成') return 'success'
      if (status === '进行中') return 'warning'
      return 'info'
    },
    sizeChangeHandle(val) {
      this.pageSize = val
      this.pageIndex = 1
      this.searchTable()
    },
    currentChangeHandle(val) {
      this.pageIndex = val
      this.searchTable()
    }
  },
  computed: {
    selectedOrderNodeList() {
      return (this.selectedOrder && this.selectedOrder.nodeList) ? this.selectedOrder.nodeList : []
    },
    selectedOrderProgressPercent() {
      const total = this.selectedOrderNodeList.length
      if (!total) {
        return 0
      }
      const done = this.selectedOrderNodeList.filter(item => item.status === '已完成').length
      return Math.round((done * 100) / total)
    }
  }
}
</script>

<style scoped>
.data-table {
  background-color: #fff;
  border-radius: 4px;
}
.data-table >>> .cell {
  line-height: 20px;
  height: 20px;
}
.data-table >>> .el-table__header-wrapper th,
.data-table >>> .el-table__fixed-header-wrapper th {
  background-color: #f5f7fa !important;
  color: #333;
  font-weight: 600;
  border-color: #ebeef5;
  padding: 8px 0;
}

.data-table >>> .el-table__header-wrapper .cell,
.data-table >>> .el-table__fixed-header-wrapper .cell,
.data-table >>> .el-table__body-wrapper .cell,
.data-table >>> .el-table__fixed-body-wrapper .cell {
  padding: 0 10px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px !important;
}

.data-table >>> .el-table__body tr:hover > td {
  background-color: #f5f7fa !important;
}

.data-table >>> .el-table__body tr.current-row > td {
  background-color: #ecf5ff !important;
}

.query-form {
  background-color: #fff;
  padding: 15px 15px 5px 15px;
  border-radius: 4px;
  margin-bottom: 12px;
}

.query-finish-item {
  margin-top: -11px;
  float: right;
  margin-right: 0 !important;
}

.query-form >>> .el-form-item__label {
  color: #333;
  font-size: 13px;
  padding-bottom: 5px;
}

.query-form >>> .el-input__inner {
  height: 32px;
  line-height: 32px;
  border-radius: 4px;
  font-size: 13px;
}

.query-form >>> .el-button {
  height: 32px;
  padding: 0 15px;
  font-size: 13px;
  border-radius: 4px;
}

.search-btn {
  background-color: #ecf5ff;
  border-color: #b3d8ff;
  color: #409eff;
}

.search-btn:hover {
  background-color: #409eff;
  border-color: #409eff;
  color: #fff;
}

.reset-btn {
  background-color: #f5f7fa;
  border-color: #d3d4d6;
  color: #606266;
}

.reset-btn:hover {
  background-color: #909399;
  border-color: #909399;
  color: #fff;
}

.add-btn {
  background-color: #f0f9eb;
  border-color: #c2e7b0;
  color: #67c23a;
}

.add-btn:hover {
  background-color: #67c23a;
  border-color: #67c23a;
  color: #fff;
}

.dialog-footer {
  text-align: right;
}

.edit-form {
  margin-left: 5px;
  margin-top: -5px;
}

.detail-tabs-wrap {
  margin-top: 12px;
  background-color: #fff;
  border-radius: 4px;
}

.detail-table {
  width: 100%;
}

.detail-table >>> .el-table__header-wrapper th,
.detail-table >>> .el-table__fixed-header-wrapper th {
  background-color: #f5f7fa !important;
  color: #333;
  font-weight: 600;
  border-color: #ebeef5;
}

.two-column-layout {
  display: flex;
  gap: 12px;
}

.stages-column {
  width: 38%;
  min-width: 320px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  background: #fff;
}

.logs-column {
  flex: 1;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  background: #fff;
}

.column-header {
  height: 24px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 12px;
  border-bottom: 1px solid #ebeef5;
  font-weight: 600;
  color: #303133;
}

.progress-badge,
.logs-count {
  margin-left: auto;
  color: #409eff;
  font-size: 12px;
  font-weight: 500;
}

.stages-list {
  max-height: 295px;
  overflow-y: auto;
  padding: 10px;
}

.stage-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 3px 8px;
  border-radius: 4px;
}

.stage-item + .stage-item {
  margin-top: 2px;
}

.stage-item.stage-done {
  background: #f0f9eb;
}

.stage-item.stage-processing {
  background: #fdf6ec;
}

.stage-item.stage-pending {
  background: #f5f7fa;
}

.stage-icon {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  background: #fff;
  border: 1px solid #dcdfe6;
  color: #606266;
}

.stage-content {
  flex: 1;
}

.stage-name {
  font-size: 13px;
  color: #303133;
  line-height: 20px;
}

.stage-meta {
  margin-top: 6px;
  display: flex;
  align-items: center;
  gap: 8px;
}

.stage-owner {
  color: #606266;
  font-size: 12px;
}

.logs-table-wrapper {
  padding: 8px;
}

.inspection-upload-wrap {
  border: 1px dashed #dcdfe6;
  border-radius: 4px;
  padding: 10px;
}

.inspection-upload-tip {
  margin-left: 8px;
  color: #909399;
  font-size: 12px;
}

.inspection-file-input {
  display: none;
}

.inspection-file-list {
  margin-top: 10px;
  max-height: 150px;
  overflow-y: auto;
}

.inspection-file-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid #f0f2f5;
  padding: 4px 0;
}

.inspection-file-name {
  flex: 1;
  padding-right: 10px;
  font-size: 12px;
  color: #606266;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.inspection-upload-empty {
  margin-top: 8px;
  font-size: 12px;
  color: #909399;
}

.media-dialog-body {
  min-height: 360px;
}

.media-dialog-meta {
  display: flex;
  justify-content: space-between;
  margin-bottom: 10px;
  font-size: 13px;
  color: #606266;
}

.media-thumb-cell {
  min-height: 70px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.media-thumb-loading {
  color: #909399;
  font-size: 12px;
}

.media-thumb {
  width: 110px;
  height: 68px;
  border-radius: 4px;
  object-fit: cover;
  cursor: pointer;
  border: 1px solid #ebeef5;
  background: #f5f7fa;
}

.media-thumb-video {
  background: #000;
}

.media-preview-overlay {
  position: fixed;
  inset: 0;
  z-index: 2100;
  background: rgba(0, 0, 0, 0.82);
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 30px;
}

.media-preview-close {
  position: absolute;
  top: 18px;
  right: 22px;
  color: #fff;
  font-size: 30px;
  cursor: pointer;
}

.media-preview-image,
.media-preview-video {
  max-width: 92vw;
  max-height: 88vh;
  object-fit: contain;
  border-radius: 6px;
  background: #000;
}

.file-table {
  background-color: #fff;
  border-radius: 4px;
}
.file-table >>> .cell {
  line-height: 55px;
  height: 55px;
}
.file-table >>> .el-table__header-wrapper th,
.file-table >>> .el-table__fixed-header-wrapper th {
  background-color: #f5f7fa !important;
  color: #333;
  font-weight: 600;
  border-color: #ebeef5;
  padding: 8px 0;
}

.file-table >>> .el-table__header-wrapper .cell,
.file-table >>> .el-table__fixed-header-wrapper .cell{
  padding: 0 10px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px !important;
  line-height: 20px;
  height: 20px;
}

.file-table >>> .el-table__body tr:hover > td {
  background-color: #f5f7fa !important;
}

.file-table >>> .el-table__body tr.current-row > td {
  background-color: #ecf5ff !important;
}

.el-icon-check {
  color: #67c23a;
  font-weight: 1000;
}
</style>
