|
|
@@ -0,0 +1,1071 @@
|
|
|
+<!-- 生产管理 — 生产任务 -->
|
|
|
+<!-- eslint-disable vue/no-v-model-argument -->
|
|
|
+<template>
|
|
|
+ <div class="page-container">
|
|
|
+ <header class="page-head">
|
|
|
+ <h1>生产任务</h1>
|
|
|
+ <p class="page-desc">管理生产工单下的任务执行,跟踪工艺步骤及物料消耗。</p>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <section class="panel">
|
|
|
+ <!-- ========== 列表视图 ========== -->
|
|
|
+ <template v-if="currentView === 'list'">
|
|
|
+ <div class="panel-top">
|
|
|
+ <h2 class="title-bar-heading">任务列表</h2>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="filter-block">
|
|
|
+ <el-form :model="searchForm" class="filter-form" @submit.prevent>
|
|
|
+ <div class="filter-grid">
|
|
|
+ <el-form-item label="任务号">
|
|
|
+ <el-input v-model="searchForm.keyword" placeholder="任务号" clearable @keyup.enter="handleSearch" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="任务状态">
|
|
|
+ <el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 100%">
|
|
|
+ <el-option label="待配置" value="dpz" />
|
|
|
+ <el-option label="进行中" value="jxz" />
|
|
|
+ <el-option label="已完成" value="ywc" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="工单号">
|
|
|
+ <el-input v-model="searchForm.workOrderNo" placeholder="工单号" clearable @keyup.enter="handleSearch" />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ <div class="filter-actions-row">
|
|
|
+ <span class="filter-hint">按任务号、状态、工单号筛选。</span>
|
|
|
+ <div class="filter-actions-end">
|
|
|
+ <el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
|
|
|
+ <el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="list-body list-wrap">
|
|
|
+ <el-table
|
|
|
+ class="list-table"
|
|
|
+ :data="tableData"
|
|
|
+ v-loading="loading"
|
|
|
+ stripe
|
|
|
+ @row-dblclick="handleViewDetail"
|
|
|
+ >
|
|
|
+ <el-table-column type="index" label="序号" width="68" align="center" :index="(i: number) => (pagination.page - 1) * pagination.pageSize + i + 1" />
|
|
|
+ <el-table-column prop="taskNo" label="任务号" min-width="150" align="center" />
|
|
|
+ <el-table-column prop="workOrderNo" label="所属工单号" min-width="140" align="center" />
|
|
|
+ <el-table-column prop="taskName" label="任务名称" min-width="160" align="center" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="routeName" label="工艺路线" min-width="140" align="center" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">{{ row.routeName || '—' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="taskStatus" label="任务状态" width="110" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="taskStatusTag(row.taskStatus).type" size="small">{{ taskStatusTag(row.taskStatus).label }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="startedTime" label="开始时间" min-width="170" align="center">
|
|
|
+ <template #default="{ row }">{{ row.startedTime || '—' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="completedTime" label="完成时间" min-width="170" align="center">
|
|
|
+ <template #default="{ row }">{{ row.completedTime || '—' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="260" fixed="right" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button class="btn-op-item primary" @click="handleEnterExecution(row)"><el-icon><VideoPlay /></el-icon>进入执行</el-button>
|
|
|
+ <el-button class="btn-op-item" @click="handleViewDetail(row)"><el-icon><View /></el-icon>查看详情</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <div class="pagination list-pager-bar">
|
|
|
+ <el-pagination
|
|
|
+ :current-page="pagination.page"
|
|
|
+ :page-size="pagination.pageSize"
|
|
|
+ :total="pagination.total"
|
|
|
+ :page-sizes="[10, 20, 50]"
|
|
|
+ layout="total, sizes, prev, pager, next"
|
|
|
+ @size-change="(val: number) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
|
|
+ @current-change="(val: number) => ((pagination.page = val), loadData())"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- ========== 执行详情视图 ========== -->
|
|
|
+ <template v-if="currentView === 'detail'">
|
|
|
+ <div v-loading="detailLoading" class="task-detail-wrap">
|
|
|
+ <!-- 顶部:基本信息卡片 -->
|
|
|
+ <div class="task-detail-header">
|
|
|
+ <div class="task-detail-header-info">
|
|
|
+ <div class="task-detail-title-row">
|
|
|
+ <h2 class="title-bar-heading">任务执行</h2>
|
|
|
+ <el-tag v-if="taskDetail" :type="taskStatusTag(taskDetail.taskStatus).type" size="large">
|
|
|
+ {{ taskStatusTag(taskDetail.taskStatus).label }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <div v-if="taskDetail" class="task-detail-meta">
|
|
|
+ <span>任务号:<b>{{ taskDetail.taskNo }}</b></span>
|
|
|
+ <span>工单号:{{ taskDetail.workOrderNo || '—' }}</span>
|
|
|
+ <span>任务名称:{{ taskDetail.taskName || '—' }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="taskDetail?.remark" class="task-detail-desc">{{ taskDetail.remark }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="task-detail-header-actions">
|
|
|
+ <el-button class="btn-ghost" @click="handleBackToList"><el-icon><Back /></el-icon>返回列表</el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="taskDetail?.taskStatus === 'dpz'"
|
|
|
+ class="btn-main"
|
|
|
+ type="primary"
|
|
|
+ @click="handleStartTask"
|
|
|
+ >
|
|
|
+ <el-icon><VideoPlay /></el-icon>开始执行
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="taskDetail?.taskStatus === 'jxz'"
|
|
|
+ class="btn-main"
|
|
|
+ type="success"
|
|
|
+ @click="handleCompleteTask"
|
|
|
+ >
|
|
|
+ <el-icon><Check /></el-icon>完成任务
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Section 1: 任务输入 -->
|
|
|
+ <div class="task-detail-section">
|
|
|
+ <div class="task-detail-section-header">
|
|
|
+ <h3><span class="section-bar"></span>任务输入</h3>
|
|
|
+ <div class="task-detail-section-actions">
|
|
|
+ <el-button class="btn-main" type="primary" size="small" @click="addInputRow"><el-icon><Plus /></el-icon>添加输入</el-button>
|
|
|
+ <el-button class="btn-main" type="primary" size="small" :loading="inputSaving" @click="saveInputs"><el-icon><Check /></el-icon>保存输入</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-table :data="taskInputs" stripe class="inner-table" max-height="300">
|
|
|
+ <el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
|
|
|
+ <el-table-column label="输入类型" min-width="140" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-select v-model="row.inputType" placeholder="选择类型" class="inner-cell-select">
|
|
|
+ <el-option label="样本" value="sample" />
|
|
|
+ <el-option label="细胞产品" value="cell_product" />
|
|
|
+ </el-select>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="输入编码" min-width="150" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.inputCode" placeholder="输入编码" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="输入名称" min-width="150" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.inputName" placeholder="输入名称" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="80" align="center">
|
|
|
+ <template #default="{ $index }">
|
|
|
+ <el-button link type="danger" size="small" @click="taskInputs.splice($index, 1)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-empty v-if="taskInputs.length === 0" description="暂无输入项" :image-size="48" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Section 2: 工艺步骤 -->
|
|
|
+ <div class="task-detail-section">
|
|
|
+ <div class="task-detail-section-header">
|
|
|
+ <h3><span class="section-bar"></span>工艺步骤</h3>
|
|
|
+ <div class="task-detail-section-actions">
|
|
|
+ <el-button class="btn-main" type="primary" size="small" @click="handleAddStep"><el-icon><Plus /></el-icon>新增步骤</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="taskSteps.length === 0" class="task-detail-empty">
|
|
|
+ <el-empty description="暂无工艺步骤" :image-size="48" />
|
|
|
+ </div>
|
|
|
+ <el-collapse v-else v-model="expandedSteps" class="task-step-collapse">
|
|
|
+ <el-collapse-item
|
|
|
+ v-for="(step, stepIdx) in taskSteps"
|
|
|
+ :key="step.id || stepIdx"
|
|
|
+ :name="step.id || stepIdx"
|
|
|
+ >
|
|
|
+ <template #title>
|
|
|
+ <div class="task-step-card-header">
|
|
|
+ <span class="task-step-order">{{ step.stepOrder || stepIdx + 1 }}</span>
|
|
|
+ <span class="task-step-name">{{ step.processName || '未命名步骤' }}</span>
|
|
|
+ <el-tag :type="stepStatusTag(step.stepStatus).type" size="small">{{ stepStatusTag(step.stepStatus).label }}</el-tag>
|
|
|
+ <span class="task-step-equipment">{{ step.equipmentName || '' }}</span>
|
|
|
+ <div class="task-step-header-actions" @click.stop>
|
|
|
+ <el-button
|
|
|
+ v-if="step.stepStatus === 'dzx'"
|
|
|
+ class="btn-op-item primary"
|
|
|
+ size="small"
|
|
|
+ @click="handleStartStep(step)"
|
|
|
+ >
|
|
|
+ <el-icon><VideoPlay /></el-icon>开始执行
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="step.stepStatus === 'jxz'"
|
|
|
+ class="btn-op-item"
|
|
|
+ size="small"
|
|
|
+ type="success"
|
|
|
+ @click="handleCompleteStep(step)"
|
|
|
+ >
|
|
|
+ <el-icon><Check /></el-icon>完成步骤
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ class="btn-op-item"
|
|
|
+ size="small"
|
|
|
+ @click="handleDeleteStep(step)"
|
|
|
+ >
|
|
|
+ <el-icon><Delete /></el-icon>删除
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 步骤展开内容 -->
|
|
|
+ <div class="task-step-body">
|
|
|
+ <el-tabs v-model="step.activeTab" class="task-step-tabs">
|
|
|
+ <!-- 试剂 -->
|
|
|
+ <el-tab-pane label="试剂" name="reagent">
|
|
|
+ <div class="task-step-tab-toolbar">
|
|
|
+ <el-button class="btn-main" type="primary" size="small" @click="addStepReagent(step)"><el-icon><Plus /></el-icon>添加试剂</el-button>
|
|
|
+ <el-button class="btn-main" type="primary" size="small" @click="saveStepReagents(step)"><el-icon><Check /></el-icon>保存试剂</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="step.reagents" stripe class="inner-table" max-height="260">
|
|
|
+ <el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
|
|
|
+ <el-table-column label="试剂编码" min-width="110" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.reagentCode" placeholder="编码" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="试剂名称" min-width="120" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.reagentName" placeholder="名称" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="规格" min-width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.spec" placeholder="规格" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="标准用量" min-width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.standardUsage" placeholder="标准" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="实际用量" min-width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.actualUsage" placeholder="实际" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="单位" min-width="80" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.unit" placeholder="单位" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="关键物料" width="90" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-switch v-model="row.isKeyMaterial" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="80" align="center">
|
|
|
+ <template #default="{ $index }">
|
|
|
+ <el-button link type="danger" size="small" @click="step.reagents.splice($index, 1)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-empty v-if="!step.reagents || step.reagents.length === 0" description="暂无试剂" :image-size="48" />
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- 耗材 -->
|
|
|
+ <el-tab-pane label="耗材" name="consumable">
|
|
|
+ <div class="task-step-tab-toolbar">
|
|
|
+ <el-button class="btn-main" type="primary" size="small" @click="addStepConsumable(step)"><el-icon><Plus /></el-icon>添加耗材</el-button>
|
|
|
+ <el-button class="btn-main" type="primary" size="small" @click="saveStepConsumables(step)"><el-icon><Check /></el-icon>保存耗材</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="step.consumables" stripe class="inner-table" max-height="260">
|
|
|
+ <el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
|
|
|
+ <el-table-column label="耗材编码" min-width="110" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.consumableCode" placeholder="编码" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="耗材名称" min-width="120" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.consumableName" placeholder="名称" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="规格型号" min-width="110" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.specModel" placeholder="规格型号" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="标准用量" min-width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.standardUsage" placeholder="标准" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="实际用量" min-width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.actualUsage" placeholder="实际" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="单位" min-width="80" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.unit" placeholder="单位" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="必需" width="80" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-switch v-model="row.isRequired" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="80" align="center">
|
|
|
+ <template #default="{ $index }">
|
|
|
+ <el-button link type="danger" size="small" @click="step.consumables.splice($index, 1)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-empty v-if="!step.consumables || step.consumables.length === 0" description="暂无耗材" :image-size="48" />
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- 产出物 -->
|
|
|
+ <el-tab-pane label="产出物" name="output">
|
|
|
+ <div class="task-step-tab-toolbar">
|
|
|
+ <el-button class="btn-main" type="primary" size="small" @click="addStepOutput(step)"><el-icon><Plus /></el-icon>添加产出物</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="step.outputs" stripe class="inner-table" max-height="260">
|
|
|
+ <el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
|
|
|
+ <el-table-column label="产出类型" min-width="140" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.outputTypeId" placeholder="类型ID" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="产出名称" min-width="140" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.outputName" placeholder="名称" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="产出条码" min-width="140" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.outputBarcode" placeholder="扫码或输入" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="80" align="center">
|
|
|
+ <template #default="{ $index }">
|
|
|
+ <el-button link type="danger" size="small" @click="step.outputs.splice($index, 1)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-empty v-if="!step.outputs || step.outputs.length === 0" description="暂无产出物" :image-size="48" />
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <!-- 数据采集 -->
|
|
|
+ <el-tab-pane label="数据采集" name="dataCollection">
|
|
|
+ <div class="task-step-tab-toolbar">
|
|
|
+ <el-button class="btn-main" type="primary" size="small" @click="addStepDataCollection(step)"><el-icon><Plus /></el-icon>添加采集项</el-button>
|
|
|
+ <el-button class="btn-main" type="primary" size="small" @click="saveStepDataCollection(step)"><el-icon><Check /></el-icon>保存采集</el-button>
|
|
|
+ </div>
|
|
|
+ <el-table :data="step.dataCollections" stripe class="inner-table" max-height="260">
|
|
|
+ <el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
|
|
|
+ <el-table-column label="采集名称" min-width="160" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.collectionName" placeholder="采集项名称" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="单位" min-width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.unit" placeholder="如 °C、mL" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="采集值" min-width="140" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.collectionValue" placeholder="输入采集值" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="80" align="center">
|
|
|
+ <template #default="{ $index }">
|
|
|
+ <el-button link type="danger" size="small" @click="step.dataCollections.splice($index, 1)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-empty v-if="!step.dataCollections || step.dataCollections.length === 0" description="暂无采集项" :image-size="48" />
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </div>
|
|
|
+ </el-collapse-item>
|
|
|
+ </el-collapse>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Section 3: 任务输出 -->
|
|
|
+ <div class="task-detail-section">
|
|
|
+ <div class="task-detail-section-header">
|
|
|
+ <h3><span class="section-bar"></span>任务输出</h3>
|
|
|
+ <div class="task-detail-section-actions">
|
|
|
+ <el-button class="btn-main" type="primary" size="small" @click="addOutputRow"><el-icon><Plus /></el-icon>添加输出</el-button>
|
|
|
+ <el-button class="btn-main" type="primary" size="small" :loading="outputSaving" @click="saveOutputs"><el-icon><Check /></el-icon>保存输出</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-table :data="taskOutputs" stripe class="inner-table" max-height="300">
|
|
|
+ <el-table-column type="index" label="序号" width="56" align="center" :index="(i: number) => i + 1" />
|
|
|
+ <el-table-column label="细胞产品" min-width="150" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.cellProductId" placeholder="产品ID" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="储物格" min-width="120" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.storageSlotId" placeholder="储物格ID" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="输出名称" min-width="140" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.outputName" placeholder="输出名称" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="输出条码" min-width="140" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.outputBarcode" placeholder="扫码或输入" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="数量" min-width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input-number v-model="row.quantity" :min="0" :precision="2" controls-position="right" class="inner-cell-input" style="width: 100%" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="单位" min-width="80" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.unit" placeholder="单位" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="备注" min-width="120" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="80" align="center">
|
|
|
+ <template #default="{ $index }">
|
|
|
+ <el-button link type="danger" size="small" @click="taskOutputs.splice($index, 1)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-empty v-if="taskOutputs.length === 0" description="暂无输出项" :image-size="48" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+ import {
|
|
|
+ RefreshLeft,
|
|
|
+ Search,
|
|
|
+ VideoPlay,
|
|
|
+ View,
|
|
|
+ Back,
|
|
|
+ Check,
|
|
|
+ Plus,
|
|
|
+ Delete
|
|
|
+ } from '@element-plus/icons-vue'
|
|
|
+ import {
|
|
|
+ fetchProductionTaskList,
|
|
|
+ fetchProductionTaskDetail,
|
|
|
+ fetchUpdateProductionTaskStatus,
|
|
|
+ fetchAddTaskStep,
|
|
|
+ fetchUpdateTaskStep,
|
|
|
+ fetchDeleteTaskStep,
|
|
|
+ fetchReplaceStepReagents,
|
|
|
+ fetchReplaceStepConsumables,
|
|
|
+ fetchReplaceStepDataCollection,
|
|
|
+ fetchSetTaskInputs,
|
|
|
+ fetchSetTaskOutputs
|
|
|
+ } from '@/api/production-task'
|
|
|
+
|
|
|
+ defineOptions({ name: 'ProductionProductionTask' })
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ // 状态标签映射
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ function taskStatusTag(status: string) {
|
|
|
+ const map: Record<string, { label: string; type: string }> = {
|
|
|
+ dpz: { label: '待配置', type: 'info' },
|
|
|
+ jxz: { label: '进行中', type: 'warning' },
|
|
|
+ ywc: { label: '已完成', type: 'success' }
|
|
|
+ }
|
|
|
+ return map[status] || { label: status, type: 'info' }
|
|
|
+ }
|
|
|
+
|
|
|
+ function stepStatusTag(status: string) {
|
|
|
+ const map: Record<string, { label: string; type: string }> = {
|
|
|
+ dzx: { label: '待执行', type: 'info' },
|
|
|
+ jxz: { label: '进行中', type: 'warning' },
|
|
|
+ ywc: { label: '已完成', type: 'success' }
|
|
|
+ }
|
|
|
+ return map[status] || { label: status, type: 'info' }
|
|
|
+ }
|
|
|
+
|
|
|
+ function toApiFlag(val: boolean | number | string | undefined): string {
|
|
|
+ if (val === true || val === 1 || val === '1') return '1'
|
|
|
+ return '0'
|
|
|
+ }
|
|
|
+
|
|
|
+ function fromApiFlag(val: boolean | number | string | undefined): boolean {
|
|
|
+ return val === true || val === 1 || val === '1'
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ // 列表视图
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ const currentView = ref<'list' | 'detail'>('list')
|
|
|
+ const searchForm = reactive({ keyword: '', status: '', workOrderNo: '' })
|
|
|
+ const tableData = ref<Record<string, any>[]>([])
|
|
|
+ const loading = ref(false)
|
|
|
+ const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
|
|
+
|
|
|
+ async function loadData() {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const res = await fetchProductionTaskList({
|
|
|
+ page: pagination.page,
|
|
|
+ pageSize: pagination.pageSize,
|
|
|
+ keyword: searchForm.keyword || undefined,
|
|
|
+ status: searchForm.status || undefined,
|
|
|
+ workOrderNo: searchForm.workOrderNo || undefined
|
|
|
+ })
|
|
|
+ tableData.value = res.list || []
|
|
|
+ pagination.total = res.total ?? 0
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleSearch() {
|
|
|
+ pagination.page = 1
|
|
|
+ loadData()
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleReset() {
|
|
|
+ searchForm.keyword = ''
|
|
|
+ searchForm.status = ''
|
|
|
+ searchForm.workOrderNo = ''
|
|
|
+ pagination.page = 1
|
|
|
+ loadData()
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ // 详情视图
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ const detailLoading = ref(false)
|
|
|
+ const taskDetail = ref<Record<string, any> | null>(null)
|
|
|
+ const taskInputs = ref<Record<string, any>[]>([])
|
|
|
+ const taskOutputs = ref<Record<string, any>[]>([])
|
|
|
+ const taskSteps = ref<Record<string, any>[]>([])
|
|
|
+ const expandedSteps = ref<(string | number)[]>([])
|
|
|
+ const inputSaving = ref(false)
|
|
|
+ const outputSaving = ref(false)
|
|
|
+
|
|
|
+ function normalizeStep(step: Record<string, any>): Record<string, any> {
|
|
|
+ return {
|
|
|
+ ...step,
|
|
|
+ activeTab: 'reagent',
|
|
|
+ reagents: (step.reagents || []).map((r: Record<string, any>) => ({
|
|
|
+ ...r,
|
|
|
+ isKeyMaterial: fromApiFlag(r.isKeyMaterial)
|
|
|
+ })),
|
|
|
+ consumables: (step.consumables || []).map((c: Record<string, any>) => ({
|
|
|
+ ...c,
|
|
|
+ isRequired: fromApiFlag(c.isRequired)
|
|
|
+ })),
|
|
|
+ outputs: step.outputs || [],
|
|
|
+ dataCollections: step.dataCollections || []
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function loadTaskDetail(id: string) {
|
|
|
+ detailLoading.value = true
|
|
|
+ try {
|
|
|
+ const detail = await fetchProductionTaskDetail(id)
|
|
|
+ taskDetail.value = detail
|
|
|
+ taskInputs.value = (detail.inputs || []).map((i: Record<string, any>) => ({ ...i }))
|
|
|
+ taskOutputs.value = (detail.outputs || []).map((o: Record<string, any>) => ({ ...o }))
|
|
|
+ taskSteps.value = (detail.steps || []).map((s: Record<string, any>) => normalizeStep(s))
|
|
|
+ if (taskSteps.value.length > 0) {
|
|
|
+ expandedSteps.value = [taskSteps.value[0].id || 0]
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ taskDetail.value = null
|
|
|
+ taskInputs.value = []
|
|
|
+ taskOutputs.value = []
|
|
|
+ taskSteps.value = []
|
|
|
+ } finally {
|
|
|
+ detailLoading.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function refreshDetail() {
|
|
|
+ if (!taskDetail.value?.id) return
|
|
|
+ await loadTaskDetail(taskDetail.value.id)
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleEnterExecution(row: Record<string, any>) {
|
|
|
+ currentView.value = 'detail'
|
|
|
+ loadTaskDetail(row.id)
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleViewDetail(row: Record<string, any>) {
|
|
|
+ currentView.value = 'detail'
|
|
|
+ loadTaskDetail(row.id)
|
|
|
+ }
|
|
|
+
|
|
|
+ function handleBackToList() {
|
|
|
+ currentView.value = 'list'
|
|
|
+ taskDetail.value = null
|
|
|
+ loadData()
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ // 任务状态操作
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ async function handleStartTask() {
|
|
|
+ if (!taskDetail.value?.id) return
|
|
|
+ await ElMessageBox.confirm('确定开始执行该任务吗?', '提示', { type: 'warning' })
|
|
|
+ await fetchUpdateProductionTaskStatus(taskDetail.value.id, { taskStatus: 'jxz' })
|
|
|
+ ElMessage.success('任务已开始执行')
|
|
|
+ await refreshDetail()
|
|
|
+ }
|
|
|
+
|
|
|
+ async function handleCompleteTask() {
|
|
|
+ if (!taskDetail.value?.id) return
|
|
|
+ await ElMessageBox.confirm('确定完成该任务吗?完成后不可恢复。', '提示', { type: 'warning' })
|
|
|
+ await fetchUpdateProductionTaskStatus(taskDetail.value.id, { taskStatus: 'ywc' })
|
|
|
+ ElMessage.success('任务已完成')
|
|
|
+ await refreshDetail()
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ // 任务输入
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ function addInputRow() {
|
|
|
+ taskInputs.value.push({
|
|
|
+ inputType: 'sample',
|
|
|
+ inputCode: '',
|
|
|
+ inputName: ''
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ async function saveInputs() {
|
|
|
+ if (!taskDetail.value?.id) return
|
|
|
+ inputSaving.value = true
|
|
|
+ try {
|
|
|
+ await fetchSetTaskInputs(taskDetail.value.id, { items: taskInputs.value })
|
|
|
+ ElMessage.success('输入已保存')
|
|
|
+ } catch {
|
|
|
+ /* 接口错误已由全局处理 */
|
|
|
+ } finally {
|
|
|
+ inputSaving.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ // 任务输出
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ function addOutputRow() {
|
|
|
+ taskOutputs.value.push({
|
|
|
+ cellProductId: '',
|
|
|
+ storageSlotId: '',
|
|
|
+ outputName: '',
|
|
|
+ outputBarcode: '',
|
|
|
+ quantity: 0,
|
|
|
+ unit: '',
|
|
|
+ remark: ''
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ async function saveOutputs() {
|
|
|
+ if (!taskDetail.value?.id) return
|
|
|
+ outputSaving.value = true
|
|
|
+ try {
|
|
|
+ await fetchSetTaskOutputs(taskDetail.value.id, { items: taskOutputs.value })
|
|
|
+ ElMessage.success('输出已保存')
|
|
|
+ } catch {
|
|
|
+ /* 接口错误已由全局处理 */
|
|
|
+ } finally {
|
|
|
+ outputSaving.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ // 工艺步骤
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ async function handleAddStep() {
|
|
|
+ if (!taskDetail.value?.id) return
|
|
|
+ try {
|
|
|
+ await fetchAddTaskStep(taskDetail.value.id, { processName: '新步骤', stepOrder: taskSteps.value.length + 1 })
|
|
|
+ ElMessage.success('步骤已添加')
|
|
|
+ await refreshDetail()
|
|
|
+ } catch {
|
|
|
+ /* 接口错误已由全局处理 */
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function handleStartStep(step: Record<string, any>) {
|
|
|
+ if (!taskDetail.value?.id || !step.id) return
|
|
|
+ await fetchUpdateTaskStep(taskDetail.value.id, step.id, { stepStatus: 'jxz' })
|
|
|
+ ElMessage.success('步骤已开始执行')
|
|
|
+ await refreshDetail()
|
|
|
+ }
|
|
|
+
|
|
|
+ async function handleCompleteStep(step: Record<string, any>) {
|
|
|
+ if (!taskDetail.value?.id || !step.id) return
|
|
|
+ await ElMessageBox.confirm('确定完成该步骤吗?', '提示', { type: 'warning' })
|
|
|
+ await fetchUpdateTaskStep(taskDetail.value.id, step.id, { stepStatus: 'ywc' })
|
|
|
+ ElMessage.success('步骤已完成')
|
|
|
+ await refreshDetail()
|
|
|
+ }
|
|
|
+
|
|
|
+ async function handleDeleteStep(step: Record<string, any>) {
|
|
|
+ if (!taskDetail.value?.id || !step.id) return
|
|
|
+ await ElMessageBox.confirm('确定删除该步骤吗?删除后不可恢复。', '提示', { type: 'warning' })
|
|
|
+ await fetchDeleteTaskStep(taskDetail.value.id, step.id)
|
|
|
+ ElMessage.success('步骤已删除')
|
|
|
+ await refreshDetail()
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ // 步骤子表 — 试剂
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ function addStepReagent(step: Record<string, any>) {
|
|
|
+ if (!step.reagents) step.reagents = []
|
|
|
+ step.reagents.push({
|
|
|
+ reagentCode: '',
|
|
|
+ reagentName: '',
|
|
|
+ spec: '',
|
|
|
+ standardUsage: '',
|
|
|
+ actualUsage: '',
|
|
|
+ unit: '',
|
|
|
+ isKeyMaterial: false
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ async function saveStepReagents(step: Record<string, any>) {
|
|
|
+ if (!taskDetail.value?.id || !step.id) return
|
|
|
+ try {
|
|
|
+ await fetchReplaceStepReagents(
|
|
|
+ taskDetail.value.id,
|
|
|
+ step.id,
|
|
|
+ {
|
|
|
+ items: (step.reagents || []).map((r: Record<string, any>) => ({
|
|
|
+ reagentCode: r.reagentCode,
|
|
|
+ reagentName: r.reagentName,
|
|
|
+ spec: r.spec,
|
|
|
+ standardUsage: r.standardUsage,
|
|
|
+ actualUsage: r.actualUsage,
|
|
|
+ unit: r.unit,
|
|
|
+ isKeyMaterial: toApiFlag(r.isKeyMaterial)
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ )
|
|
|
+ ElMessage.success('试剂已保存')
|
|
|
+ } catch {
|
|
|
+ /* 接口错误已由全局处理 */
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ // 步骤子表 — 耗材
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ function addStepConsumable(step: Record<string, any>) {
|
|
|
+ if (!step.consumables) step.consumables = []
|
|
|
+ step.consumables.push({
|
|
|
+ consumableCode: '',
|
|
|
+ consumableName: '',
|
|
|
+ specModel: '',
|
|
|
+ standardUsage: '',
|
|
|
+ actualUsage: '',
|
|
|
+ unit: '',
|
|
|
+ isRequired: false
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ async function saveStepConsumables(step: Record<string, any>) {
|
|
|
+ if (!taskDetail.value?.id || !step.id) return
|
|
|
+ try {
|
|
|
+ await fetchReplaceStepConsumables(
|
|
|
+ taskDetail.value.id,
|
|
|
+ step.id,
|
|
|
+ {
|
|
|
+ items: (step.consumables || []).map((c: Record<string, any>) => ({
|
|
|
+ consumableCode: c.consumableCode,
|
|
|
+ consumableName: c.consumableName,
|
|
|
+ specModel: c.specModel,
|
|
|
+ standardUsage: c.standardUsage,
|
|
|
+ actualUsage: c.actualUsage,
|
|
|
+ unit: c.unit,
|
|
|
+ isRequired: toApiFlag(c.isRequired)
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ )
|
|
|
+ ElMessage.success('耗材已保存')
|
|
|
+ } catch {
|
|
|
+ /* 接口错误已由全局处理 */
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ // 步骤子表 — 产出物
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ function addStepOutput(step: Record<string, any>) {
|
|
|
+ if (!step.outputs) step.outputs = []
|
|
|
+ step.outputs.push({
|
|
|
+ outputTypeId: '',
|
|
|
+ outputName: '',
|
|
|
+ outputBarcode: ''
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ // 步骤子表 — 数据采集
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ function addStepDataCollection(step: Record<string, any>) {
|
|
|
+ if (!step.dataCollections) step.dataCollections = []
|
|
|
+ step.dataCollections.push({
|
|
|
+ collectionName: '',
|
|
|
+ unit: '',
|
|
|
+ collectionValue: ''
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ async function saveStepDataCollection(step: Record<string, any>) {
|
|
|
+ if (!taskDetail.value?.id || !step.id) return
|
|
|
+ try {
|
|
|
+ await fetchReplaceStepDataCollection(
|
|
|
+ taskDetail.value.id,
|
|
|
+ step.id,
|
|
|
+ {
|
|
|
+ items: (step.dataCollections || []).map((d: Record<string, any>) => ({
|
|
|
+ collectionName: d.collectionName,
|
|
|
+ unit: d.unit,
|
|
|
+ collectionValue: d.collectionValue
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ )
|
|
|
+ ElMessage.success('采集数据已保存')
|
|
|
+ } catch {
|
|
|
+ /* 接口错误已由全局处理 */
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ // 初始化
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ onMounted(() => loadData())
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+ /* ===== 详情视图整体 ===== */
|
|
|
+ .task-detail-wrap {
|
|
|
+ padding: 0 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ===== 顶部信息卡片 ===== */
|
|
|
+ .task-detail-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: flex-start;
|
|
|
+ padding: 20px 24px;
|
|
|
+ background: #f0fdf4;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ border: 1px solid #d1fae5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-detail-header-info {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-detail-title-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-detail-meta {
|
|
|
+ display: flex;
|
|
|
+ gap: 24px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #475569;
|
|
|
+ flex-wrap: wrap;
|
|
|
+
|
|
|
+ b {
|
|
|
+ color: #1e293b;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-detail-desc {
|
|
|
+ margin-top: 8px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #64748b;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-detail-header-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-left: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ===== Section 通用 ===== */
|
|
|
+ .task-detail-section {
|
|
|
+ margin-bottom: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-detail-section-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 12px;
|
|
|
+
|
|
|
+ h3 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1e293b;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .section-bar {
|
|
|
+ display: inline-block;
|
|
|
+ width: 3px;
|
|
|
+ height: 16px;
|
|
|
+ background: #10b981;
|
|
|
+ border-radius: 2px;
|
|
|
+ margin-right: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-detail-section-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ===== 步骤折叠面板 ===== */
|
|
|
+ .task-step-collapse {
|
|
|
+ border: none;
|
|
|
+
|
|
|
+ :deep(.el-collapse-item__header) {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 8px 8px 0 0;
|
|
|
+ padding: 0 16px;
|
|
|
+ height: auto;
|
|
|
+ min-height: 48px;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-collapse-item__wrap) {
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-top: none;
|
|
|
+ border-radius: 0 0 8px 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-collapse-item__content) {
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-collapse-item) {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ border: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-step-card-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ width: 100%;
|
|
|
+ padding: 4px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-step-order {
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #10b981;
|
|
|
+ color: #fff;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-step-name {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #1e293b;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-step-equipment {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #94a3b8;
|
|
|
+
|
|
|
+ &:empty {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-step-header-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 4px;
|
|
|
+ margin-left: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ===== 步骤内容 Tabs ===== */
|
|
|
+ .task-step-body {
|
|
|
+ min-height: 120px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-step-tabs {
|
|
|
+ :deep(.el-tabs__header) {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-step-tab-toolbar {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .task-detail-empty {
|
|
|
+ padding: 24px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ===== 内嵌表格样式 ===== */
|
|
|
+ .inner-table {
|
|
|
+ :deep(.el-input__wrapper) {
|
|
|
+ box-shadow: none;
|
|
|
+ border: 1px solid transparent;
|
|
|
+ transition: border-color 0.15s;
|
|
|
+
|
|
|
+ &:hover,
|
|
|
+ &:focus-within {
|
|
|
+ border-color: #10b981;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .inner-cell-input {
|
|
|
+ :deep(.el-input__wrapper) {
|
|
|
+ padding: 0 8px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .inner-cell-select {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+</style>
|