Преглед изворни кода

feat(production): add production task execution page

zlwu пре 5 месеци
родитељ
комит
eec59de5dc
1 измењених фајлова са 1071 додато и 0 уклоњено
  1. 1071 0
      web-admin/src/views/production/production-task/index.vue

+ 1071 - 0
web-admin/src/views/production/production-task/index.vue

@@ -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>