5 天以前 5dbc69b4e792dcce998038822b92402366bec47f
Merge remote-tracking branch 'origin/fix/hsy/20260812' into dev_pro2.0
已修改14个文件
627 ■■■■■ 文件已修改
src/api/mes/pro/task/index.ts 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/workorder/index.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user/index.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/data.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/data.ts 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/index.vue 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/modules/schedule-form.vue 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/modules/task-list.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/data.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/index.vue 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/TaskListPanel.vue 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/TaskDetailTab.vue 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/task/index.ts
@@ -1,8 +1,21 @@
import type { PageParam, PageResult } from '@vben/request';
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProTaskApi {
  export interface PauseRecord {
    id: number;
    pauseTime: string;
    pauseUserId: number;
    pauseUserName: string;
    reason: string;
    remark?: string;
    resumeTime?: string;
    resumeUserId?: number;
    resumeUserName?: string;
    taskId: number;
  }
  /** MES 生产任务 */
  export interface Task {
    id?: number;
@@ -41,6 +54,9 @@
    checkFlag?: boolean; // 是否质检(派生自工艺路线工序)
    backflushFlag?: boolean; // 是否倒冲(派生自工艺路线工序)
    remark?: string; // 备注
    pauseRecords?: PauseRecord[]; // 停工记录
    reportableQuantity?: number; // 可报工数量
    assigneeUserIds?: number[]; // 报工人
  }
  /** MES 生产任务分页查询参数 */
@@ -174,3 +190,20 @@
    { params: { workstationId, quantity } },
  );
}
/** 生产任务停工请求参数 */
export interface MesProTaskPauseReqVO {
  id: number;
  reason: string;
  remark?: string;
}
/** 生产任务停工 */
export function pauseTask(data: MesProTaskPauseReqVO) {
  return requestClient.put('/mes/pro/task/pause', data);
}
/** 生产任务复工 */
export function resumeTask(data: { id: number }) {
  return requestClient.put('/mes/pro/task/resume', data);
}
src/api/mes/pro/workorder/index.ts
@@ -1,4 +1,4 @@
import type { PageParam, PageResult } from '@vben/request';
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
@@ -34,6 +34,10 @@
    finishDate?: number; // 完成时间
    cancelDate?: number; // 取消时间
    status?: number; // 订单状态
    scheduleStatus?: number; // 排产状态
    productionStatus?: number; // 生产状态
    processProgressList?: ProcessProgress[]; // 工序生产进度
    completionProgress?: number; // 完成进度
    remark?: string; // 备注
    createTime?: number; // 创建时间
  }
@@ -59,6 +63,14 @@
  );
}
/** 获得生产工单调度状态分页 */
export function getWorkOrderSchedulePage(params: MesProWorkOrderApi.PageParams) {
  return requestClient.get<PageResult<MesProWorkOrderApi.WorkOrder>>(
      '/mes/pro/work-order/schedule-page',
      { params },
  );
}
/** 查询生产订单详情 */
export function getWorkOrder(id: number) {
  return requestClient.get<MesProWorkOrderApi.WorkOrder>(
src/api/system/user/index.ts
@@ -114,3 +114,10 @@
    params: { nickname },
  });
}
/** 通过角色标识获取用户精简信息列表 */
export function getSimpleUserListByRoleCode(code: string) {
  return requestClient.get<SystemUserApi.UserSimple[]>('/system/user/list-by-role-code', {
    params: { code },
  });
}
src/packages/constants/src/dict-enum.ts
@@ -239,6 +239,7 @@
  MES_DV_METERING_CHECK_RESULT: 'mes_dv_metering_check_result', // MES 计量检测结论
  MES_PRO_LINK_TYPE: 'mes_pro_link_type', // MES 工序关系类型
  MES_PRO_WORK_ORDER_STATUS: 'mes_pro_work_order_status', // MES 生产工单状态
  ORDER_PRODUCTION_STATUS: 'order_production_status', // 生产订单状态
  MES_PRO_WORK_ORDER_TYPE: 'mes_pro_work_order_type', // MES 工单类型
  MES_PRO_WORK_ORDER_SOURCE_TYPE: 'mes_pro_work_order_source_type', // MES 工单来源类型
  MES_PRO_TASK_STATUS: 'mes_pro_task_status', // MES 生产任务状态
src/views/mes/pro/feedback/data.ts
@@ -1,4 +1,4 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
import type { MesProTaskApi } from '#/api/mes/pro/task';
@@ -264,6 +264,7 @@
            taskId: undefined,
            unitMeasureName: undefined,
            workstationId: undefined,
            reportableQuantity: undefined,
          });
        },
      },
@@ -298,6 +299,7 @@
            routeId: task.routeId,
            unitMeasureName: task.unitMeasureName,
            workstationId: task.workstationId,
            reportableQuantity: task.reportableQuantity,
          });
          // 工艺路线工序的 checkFlag 决定数量区域展示
          if (task.routeId && task.processId) {
@@ -377,6 +379,20 @@
      },
    },
    {
      fieldName: 'reportableQuantity',
      label: '可报工数量',
      component: 'InputNumber',
      componentProps: {
        class: 'w-full',
        disabled: true,
        precision: 2,
      },
      dependencies: {
        triggerFields: ['taskId'],
        show: (values) => !!values.taskId && ['create', 'update'].includes(formType),
      },
    },
    {
      fieldName: 'feedbackQuantity',
      label: '报工数量',
      component: 'InputNumber',
src/views/mes/pro/task/data.ts
@@ -1,5 +1,7 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import { getSimpleUserListByRoleCode } from '#/api/system/user';
import type { MesProTaskApi } from '#/api/mes/pro/task';
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
@@ -130,15 +132,33 @@
      width: 80,
    },
    {
      field: 'status',
      title: '订单状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_STATUS },
      },
    },
    {
      field: 'productionStatus',
      title: '生产状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.ORDER_PRODUCTION_STATUS },
      },
    },
    {
      field: 'quantity',
      title: '订单数量',
      width: 100,
    },
    {
      field: 'quantityChanged',
      title: '调整数量',
      width: 100,
    },
    // {
    //   field: 'quantityChanged',
    //   title: '调整数量',
    //   width: 100,
    // },
    {
      field: 'quantityProduced',
      title: '已生产数量',
@@ -160,15 +180,7 @@
      width: 120,
      formatter: 'formatDate',
    },
    {
      field: 'status',
      title: '排产状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_STATUS },
      },
    },
    {
      title: '操作',
      width: 160,
@@ -458,6 +470,20 @@
      },
    },
    {
      fieldName: 'assigneeUserIds',
      label: '报工人',
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请选择报工人',
        api: getSimpleUserListByRoleCode,
        params: { code: 'production_submit_work' },
        labelField: 'nickname',
        valueField: 'id',
        mode: 'multiple',
      },
      rules: z.array(z.number()).min(1, '请选择报工人').default([]),
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
src/views/mes/pro/task/index.vue
@@ -1,6 +1,6 @@
<script lang="ts" setup>
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import {getWorkOrderSchedulePage, type MesProWorkOrderApi} from '#/api/mes/pro/workorder';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
@@ -11,7 +11,7 @@
  MesProWorkOrderTypeEnum,
} from '@vben/constants';
import { Button, Card, message } from 'ant-design-vue';
import { Button, Card, message, Tabs } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getGanttTaskList, previewAutoGenerate, autoGenerateTasks } from '#/api/mes/pro/task';
@@ -27,6 +27,8 @@
const ganttTasks = ref<any[]>([]); // 甘特图预览数据
const archiveVisible = ref(false);
const archiveProductCode = ref('');
const activeTab = ref('PENDING'); // 默认选中待排产
const [ScheduleModal, scheduleModalApi] = useVbenModal({
  connectedComponent: ScheduleForm,
@@ -106,10 +108,19 @@
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getWorkOrderPage({
          let scheduleStatus: number | undefined = undefined;
          if (activeTab.value === 'PENDING') {
            scheduleStatus = 0; // 待排产
          } else if (activeTab.value === 'FINISHED') {
            scheduleStatus = 1; // 已完成
          }
          // activeTab === 'ALL' 时不传
          return await getWorkOrderSchedulePage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            status: MesProWorkOrderStatusEnum.CONFIRMED,
            scheduleStatus,
            type: MesProWorkOrderTypeEnum.SELF,
            ...formValues,
          });
@@ -147,7 +158,17 @@
      <GanttChart :height="350" :readonly="true" :tasks="ganttTasks" />
    </Card>
    <Grid table-title="待排产订单">
    <Grid>
      <template #table-title>
        <div class="flex flex-col gap-2">
          <span class="text-base font-medium">生产订单列表</span>
          <Tabs v-model:activeKey="activeTab" @change="handleRefresh" :tabBarStyle="{ margin: 0, borderBottom: 0 }">
            <Tabs.TabPane key="ALL" tab="全部" />
            <Tabs.TabPane key="PENDING" tab="未完成" />
            <Tabs.TabPane key="FINISHED" tab="已完成" />
          </Tabs>
        </div>
      </template>
      <template #toolbar-tools>
        <TableAction
          :actions="[
src/views/mes/pro/task/modules/schedule-form.vue
@@ -1,4 +1,4 @@
<script lang="ts" setup>
<script lang="ts" setup>
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
@@ -50,16 +50,19 @@
    message.warning('当前订单未配置工序,请先在工单中维护工序信息');
    return;
  }
  // 确定工艺路线ID:工单路线 → 产品路线
  let tempRouteId = 0;
  if (workOrder.value?.routeId) {
    tempRouteId = workOrder.value.routeId;
  } else {
    const processes = await getRouteProcessListByProduct(productId);
    tempRouteId = processes?.[0]?.routeId ?? 0;
  }
  currentRouteId.value = tempRouteId;
  routeProcessList.value = [...woProcesses].toSorted(
    (a, b) => (a.sort ?? 0) - (b.sort ?? 0),
  );
  // 确定工艺路线ID:工单路线 → 产品路线
  if (workOrder.value?.routeId) {
    currentRouteId.value = workOrder.value.routeId;
  } else {
    const processes = await getRouteProcessListByProduct(productId);
    currentRouteId.value = processes?.[0]?.routeId ?? 0;
  }
}
/** 完成订单 */
@@ -131,22 +134,22 @@
          :title="rp.processName"
        />
      </Steps>
      <Card
        v-for="(rp, index) in routeProcessList"
        v-show="activeProcessStep === index"
        :key="rp.processId"
        class="mx-4"
      >
        <TaskList
          :color-code="rp.colorCode"
          :disabled="isReadonly"
          :item-id="rp.outputItemId || workOrder.productId"
          :process-id="rp.processId!"
          :route-id="currentRouteId"
          :work-order-id="workOrder.id!"
          :work-order-quantity="workOrder.quantity"
        />
      </Card>
      <template v-for="(rp, index) in routeProcessList" :key="rp.processId">
        <Card
          v-if="activeProcessStep === index"
          class="mx-4"
        >
          <TaskList
            :color-code="rp.colorCode"
            :disabled="isReadonly"
            :item-id="rp.outputItemId || workOrder.productId"
            :process-id="rp.processId!"
            :route-id="currentRouteId"
            :work-order-id="workOrder.id!"
            :work-order-quantity="workOrder.quantity"
          />
        </Card>
      </template>
    </template>
    <template #prepend-footer>
      <div class="flex flex-auto items-center gap-2">
src/views/mes/pro/task/modules/task-list.vue
@@ -110,6 +110,8 @@
  () => props.processId,
  () => handleRefresh(),
);
</script>
<template>
src/views/mes/pro/workorder/data.ts
@@ -1,4 +1,4 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import type { MesProWorkOrderBomApi } from '#/api/mes/pro/workorder/bom';
@@ -413,6 +413,27 @@
      },
    },
    {
      field: 'productionStatus',
      title: '生产状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.ORDER_PRODUCTION_STATUS },
      },
    },
    {
      field: 'processProgressList',
      title: '工序生产进度',
      width: 350,
      slots: { default: 'processProgressList' },
    },
    {
      field: 'completionProgress',
      title: '完成进度',
      width: 200,
      slots: { default: 'completionProgress' },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
src/views/mes/pro/workorder/index.vue
@@ -11,7 +11,7 @@
  } from "@vben/constants";
  import { downloadFileFromBlobPart } from "@vben/utils";
  import { Button, message } from "ant-design-vue";
  import { Button, message, Progress, Steps, Popover, Modal } from "ant-design-vue";
  import { ACTION_ICON, TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
  import {
@@ -63,6 +63,21 @@
  /** 完成订单 */
  function handleFinish(row: MesProWorkOrderApi.WorkOrder) {
    formModalApi.setData({ formType: "finish", id: row.id }).open();
  }
  /** 完成订单前校验 */
  function handleCheckFinish(row: MesProWorkOrderApi.WorkOrder) {
    if (row.productionStatus !== 2) {
      Modal.confirm({
        title: '操作确认',
        content: '该订单未完成,是否确认要继续完成?',
        onOk() {
          handleFinish(row);
        },
      });
    } else {
      handleFinish(row);
    }
  }
  /** 删除订单 */
@@ -139,6 +154,13 @@
      },
    } as VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>,
  });
  /** 获取工序节点状态样式类 */
  function getProcessNodeStatus(percent: number) {
    if (percent >= 100) return 'completed';
    if (percent > 0) return 'in-progress';
    return 'pending';
  }
</script>
<template>
@@ -168,6 +190,29 @@
                @click="handleDetail(row)">
          {{ row.code }}
        </Button>
      </template>
      <template #processProgressList="{ row }">
        <div v-if="row.processProgressList && row.processProgressList.length > 0" class="process-node-list">
          <div
            v-for="(item, index) in row.processProgressList"
            :key="index"
            class="process-node-item"
          >
            <div class="process-node-dot" :class="getProcessNodeStatus(item.progressPercent || item.progress || 0)"></div>
            <div class="process-node-info">
              <span class="process-node-name" :title="item.processName ">{{ item.processName  }}</span>
              <span class="process-node-percent">{{ item.progress || 0 }}%</span>
            </div>
            <div v-if="index !== row.processProgressList.length - 1" class="process-node-line"></div>
          </div>
        </div>
        <span v-else>-</span>
      </template>
      <template #completionProgress="{ row }">
        <div style="display: flex; align-items: center; gap: 8px; width: 100%;">
          <Progress :percent="row.completionProgress || 0" :show-info="false" size="small" style="flex: 1; margin: 0;" />
          <span style="white-space: nowrap; min-width: 40px; text-align: left;">{{ row.completionProgress || 0 }}%</span>
        </div>
      </template>
      <template #actions="{ row }">
        <TableAction :actions="[
@@ -207,7 +252,7 @@
              ifShow:
                row.status === MesProWorkOrderStatusEnum.CONFIRMED &&
                !!row.quantityProduced,
              onClick: handleFinish.bind(null, row),
              onClick: handleCheckFinish.bind(null, row),
            },
            {
              label: '取消',
@@ -238,3 +283,79 @@
    <ArchiveDetail ref="archiveDetailRef" />
  </Page>
</template>
<style scoped>
.process-node-list {
  display: flex;
  align-items: center;
  overflow-x: auto;
  overflow-y: hidden;
  max-width: 100%;
  padding-bottom: 4px;
}
/* Custom scrollbar to make it sleek */
.process-node-list::-webkit-scrollbar {
  height: 4px;
}
.process-node-list::-webkit-scrollbar-thumb {
  background-color: #d9d9d9;
  border-radius: 4px;
}
.process-node-list::-webkit-scrollbar-track {
  background: transparent;
}
.process-node-item {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}
.process-node-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: 6px;
}
.process-node-dot.completed {
  background-color: #52c41a;
}
.process-node-dot.in-progress {
  background-color: #1890ff;
}
.process-node-dot.pending {
  background-color: #d9d9d9;
}
.process-node-info {
  display: flex;
  flex-direction: column;
  justify-content: center;
  margin-right: 12px;
  font-size: 12px;
  line-height: 1.2;
}
.process-node-name {
  color: #333;
  max-width: 80px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.process-node-percent {
  color: #888;
  font-size: 11px;
}
.process-node-line {
  width: 20px;
  height: 1px;
  background-color: #e8e8e8;
  margin-right: 12px;
}
</style>
src/views/mes/workbench/components/TaskListPanel.vue
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { PendingTask } from '../../types';
import { computed, ref } from 'vue';
@@ -15,7 +15,13 @@
const emit = defineEmits<{
  select: [task: PendingTask];
  refresh: [];
}>();
import { pauseTask, resumeTask } from '#/api/mes/pro/task';
import { Form, FormItem, Input, Modal, message, Button } from 'ant-design-vue';
import { confirm } from '@vben/common-ui';
import type {PendingTask} from "#/views/mes/workbench/types";
const searchKeyword = ref('');
@@ -41,6 +47,50 @@
  if (rate >= 100) return '#52c41a';
  if (rate >= 60) return '#1890ff';
  return '#faad14';
}
const pauseModalVisible = ref(false);
const pauseFormRef = ref();
const pauseFormState = ref({ id: 0, reason: '', remark: '' });
const pauseLoading = ref(false);
const pauseRules = {
  reason: [{ required: true, message: '请输入停工原因', trigger: 'blur' }],
};
function handlePause(item: PendingTask, event: Event) {
  event.stopPropagation();
  pauseFormState.value = { id: item.id!, reason: '', remark: '' };
  pauseModalVisible.value = true;
}
async function submitPause() {
  await pauseFormRef.value?.validate();
  pauseLoading.value = true;
  try {
    await pauseTask(pauseFormState.value);
    message.success('停工成功');
    pauseModalVisible.value = false;
    emit('refresh');
  } finally {
    pauseLoading.value = false;
  }
}
async function handleResume(item: PendingTask, event: Event) {
  event.stopPropagation();
  try {
    await confirm('确认要复工该任务吗?');
  } catch {
    return;
  }
  try {
    await resumeTask({ id: item.id! });
    message.success('复工成功');
    emit('refresh');
  } catch (error: any) {
    // Error is handled by request interceptor
  }
}
</script>
@@ -99,6 +149,27 @@
            >
              {{ item.feedbackRate ?? 0 }}%
            </span>
            <div class="task-card__actions">
              <Button
                v-if="item.status === 9"
                type="primary"
                size="small"
                class="task-card__action-btn resume-btn"
                @click="handleResume(item, $event)"
              >
                复工
              </Button>
              <Button
                v-else
                type="primary"
                danger
                size="small"
                class="task-card__action-btn pause-btn"
                @click="handlePause(item, $event)"
              >
                停工
              </Button>
            </div>
          </div>
          <div class="task-card__progress-row">
@@ -144,6 +215,42 @@
        </div>
      </div>
    </div>
    <Modal
      v-model:open="pauseModalVisible"
      title="生产任务停工"
      @ok="submitPause"
      :confirm-loading="pauseLoading"
      destroy-on-close
    >
      <Form
        ref="pauseFormRef"
        :model="pauseFormState"
        :rules="pauseRules"
        :label-col="{ span: 4 }"
        :wrapper-col="{ span: 20 }"
        style="margin-top: 20px"
      >
        <FormItem label="停工原因" name="reason">
          <Input.TextArea
            v-model:value="pauseFormState.reason"
            placeholder="请输入停工原因(如:设备故障、缺料等)"
            :rows="3"
            :maxlength="100"
            show-count
          />
        </FormItem>
        <FormItem label="备注" name="remark">
          <Input.TextArea
            v-model:value="pauseFormState.remark"
            placeholder="请输入备注说明"
            :rows="2"
            :maxlength="200"
            show-count
          />
        </FormItem>
      </Form>
    </Modal>
  </section>
</template>
@@ -290,6 +397,32 @@
  font-variant-numeric: tabular-nums;
}
.task-card__actions {
  display: flex;
  align-items: center;
  margin-left: auto;
}
.task-card__action-btn {
  font-size: 13px;
  border-radius: 4px;
}
.pause-btn {
  box-shadow: 0 2px 4px rgb(255 77 79 / 20%);
}
.resume-btn {
  background-color: #52c41a;
  border-color: #52c41a;
  box-shadow: 0 2px 4px rgb(82 196 26 / 20%);
}
.resume-btn:hover,
.resume-btn:focus {
  background-color: #73d13d;
  border-color: #73d13d;
}
.task-card__progress-row {
  display: flex;
  align-items: baseline;
src/views/mes/workbench/components/tabs/TaskDetailTab.vue
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { PendingTask } from '../../../types';
import { Descriptions, Progress, Tag } from 'ant-design-vue';
import { Descriptions, Progress, Tag, Timeline, TimelineItem } from 'ant-design-vue';
defineOptions({ name: 'WorkbenchTaskDetailTab' });
@@ -91,6 +91,48 @@
        </Descriptions.Item>
        <Descriptions.Item label="备注" :span="2">{{ task.remark ?? '—' }}</Descriptions.Item>
      </Descriptions>
      <!-- 停工记录 -->
      <div v-if="task.pauseRecords && task.pauseRecords.length > 0" class="detail-pause-records">
        <div class="detail-pause-records__title">停工历史记录</div>
        <Timeline class="pause-timeline">
          <TimelineItem
            v-for="record in task.pauseRecords"
            :key="record.id"
            :color="record.resumeTime ? 'green' : 'red'"
          >
            <div class="pause-timeline-item">
              <div class="pause-timeline-time">
                <span class="time-label">{{ record.pauseTime }}</span>
                <span class="time-separator">至</span>
                <span class="time-label" :class="{ 'is-ongoing': !record.resumeTime }">
                  {{ record.resumeTime || '正在停工' }}
                </span>
              </div>
              <div class="pause-timeline-content">
                <div class="pause-info-row">
                  <span class="info-label">操作人员:</span>
                  <span class="info-value">
                    {{ record.pauseUserName }} (停工)
                    <template v-if="record.resumeTime">
                      <span style="margin: 0 4px; color: #d9d9d9">|</span>
                      {{ record.resumeUserName }} (复工)
                    </template>
                  </span>
                </div>
                <div class="pause-info-row">
                  <span class="info-label">停工原因:</span>
                  <span class="info-value">{{ record.reason }}</span>
                </div>
                <div class="pause-info-row" v-if="record.remark">
                  <span class="info-label">备注:</span>
                  <span class="info-value">{{ record.remark }}</span>
                </div>
              </div>
            </div>
          </TimelineItem>
        </Timeline>
      </div>
    </template>
  </div>
</template>
@@ -187,4 +229,86 @@
    font-size: 14px;
  }
}
.detail-pause-records {
  margin-top: 24px;
}
.detail-pause-records__title {
  font-size: 16px;
  font-weight: 600;
  color: #1a1a1a;
  margin-bottom: 20px;
}
.pause-timeline {
  padding-top: 6px;
  padding-left: 4px;
}
.pause-timeline-item {
  background: #fafafa;
  border: 1px solid #f0f0f0;
  border-radius: 8px;
  padding: 14px 18px;
  margin-top: -6px;
  box-shadow: 0 1px 2px rgb(0 0 0 / 2%);
  transition: all 0.3s ease;
  &:hover {
    box-shadow: 0 4px 12px rgb(0 0 0 / 5%);
    border-color: #e6d8d8;
  }
}
.pause-timeline-time {
  font-size: 13px;
  color: #595959;
  font-weight: 500;
  margin-bottom: 10px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.time-label {
  background: #e6f7ff;
  color: #1890ff;
  padding: 2px 8px;
  border-radius: 4px;
  font-variant-numeric: tabular-nums;
}
.time-label.is-ongoing {
  background: #fff1f0;
  color: #f5222d;
}
.time-separator {
  color: #bfbfbf;
  font-size: 12px;
}
.pause-timeline-content {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.pause-info-row {
  font-size: 13px;
  display: flex;
  line-height: 1.5;
}
.pause-info-row .info-label {
  color: #8c8c8c;
  width: 70px;
  flex-shrink: 0;
}
.pause-info-row .info-value {
  color: #262626;
  flex: 1;
}
</style>
src/views/mes/workbench/index.vue
@@ -215,6 +215,7 @@
          :selected-id="selectedTask?.id"
          :loading="taskLoading"
          @select="handleSelectTask"
          @refresh="loadTaskList"
        />
      </aside>