mes-vue/src/views/biz/orderProcessPlan/board.vue

1661 lines
53 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="plan-board-page">
<!-- 顶部工具栏 -->
<div class="board-toolbar">
<n-space :size="8" align="center">
<n-button type="primary" size="small" @click="handleAdd">
<template #icon><n-icon><AddOutline /></n-icon></template>
新增工序
</n-button>
<n-button size="small" type="success" ghost disabled title="待后端接口支持">
<template #icon><n-icon><CheckmarkCircleOutline /></n-icon></template>
齐套自动下达
</n-button>
<n-button size="small" type="error" ghost disabled title="待后端接口支持">批量删除</n-button>
<n-button size="small" type="primary" ghost disabled title="待后端接口支持">批量下达</n-button>
<n-button size="small" quaternary disabled title="待后端接口支持">合并为组工单</n-button>
<n-dropdown :options="batchOptions" trigger="click" @select="onBatchSelect">
<n-button size="small" quaternary>
批量操作
<n-icon style="margin-left: 4px"><ChevronDownOutline /></n-icon>
</n-button>
</n-dropdown>
</n-space>
<n-space :size="8" align="center">
<n-button size="small" quaternary @click="importModalVisible = true">
<template #icon><n-icon><CloudUploadOutline /></n-icon></template>
导入
</n-button>
<n-button size="small" quaternary @click="handleExport">
<template #icon><n-icon><DownloadOutline /></n-icon></template>
导出
</n-button>
<n-button size="small" quaternary @click="loadData">
<template #icon><n-icon><RefreshOutline /></n-icon></template>
同步
</n-button>
</n-space>
</div>
<!-- 筛选 -->
<div class="board-filter">
<n-form inline :model="searchForm" label-placement="left" size="small">
<n-form-item label="工序名称">
<n-input v-model:value="searchForm.name" placeholder="工序名称" clearable style="width: 140px" />
</n-form-item>
<n-form-item label="工序编码">
<n-input v-model:value="searchForm.code" placeholder="工序编码" clearable style="width: 140px" />
</n-form-item>
<n-form-item>
<n-space>
<n-button type="primary" size="small" @click="handleSearch">
<template #icon><n-icon><SearchOutline /></n-icon></template>
搜索
</n-button>
<n-button size="small" @click="handleReset">重置</n-button>
</n-space>
</n-form-item>
</n-form>
</div>
<div class="board-table-wrap">
<!-- 表头 -->
<div class="board-table-head">
<div class="col-check" />
<div class="col-product">产品 / 工单编号</div>
<div class="col-qty">数量</div>
<div class="col-mode">模式</div>
<div class="col-kit">齐套率</div>
<div class="col-flow">工序</div>
<div class="col-plan">计划开始 / 结束</div>
<div class="col-life">生命周期</div>
<div class="col-actions">操作</div>
</div>
<!-- 列表 -->
<n-spin :show="loading">
<div v-if="!tableData.length && !loading" class="board-empty">
<n-empty description="暂无工序计划数据" />
</div>
<div v-for="order in tableData" :key="orderRowKey(order)" class="order-block">
<div class="order-row" :class="{ expanded: isExpanded(order) }">
<div class="col-check">
<n-checkbox
:checked="selectedOrderIds.includes(orderRowKey(order))"
@update:checked="(v: boolean) => toggleSelect(orderRowKey(order), v)"
/>
</div>
<div class="col-product">
<div class="product-name">{{ order.materialName || '-' }}</div>
<div class="product-code">
<span>{{ formatWorkOrderCodes(order) }}</span>
<n-tag v-if="isOverdue(order)" size="tiny" type="error" :bordered="false">逾期</n-tag>
<n-tag v-if="hasOutsource(order)" size="tiny" type="warning" :bordered="false">委外</n-tag>
</div>
</div>
<div class="col-qty">{{ order.quantity ?? '-' }}</div>
<div class="col-mode">
<n-tag v-if="order.routeCode" size="tiny" :bordered="false">工艺型</n-tag>
<n-tag v-if="order.workshopName" size="tiny" type="info" :bordered="false">{{ order.workshopName }}</n-tag>
</div>
<div class="col-kit">-</div>
<div class="col-flow" :class="{ 'flow-expanded': isExpanded(order) }">
<ProcessFlowStepper
:list="order.processList"
clickable
@click="toggleExpand(order)"
/>
</div>
<div class="col-plan">
<div>{{ formatShortTime(order.beginTime) }}</div>
<div class="sub-text">{{ formatShortTime(order.endTime) }}</div>
</div>
<div class="col-life">
<div class="life-cell">
<n-icon :color="lifeIcon(order).color" size="18">
<component :is="lifeIcon(order).icon" />
</n-icon>
<span>{{ lifeLabel(order) }}</span>
</div>
</div>
<div class="col-actions">
<n-button text type="primary" size="tiny" @click="openDetail(order)">详情</n-button>
<n-button text type="info" size="tiny" @click="openPlmModel(order)">模型</n-button>
<!-- 订单级编辑:打开工序顺序重排弹窗(不进单工序编辑) -->
<n-button
text
type="primary"
size="tiny"
:disabled="!hasPermission('biz:orderProcessPlan:edit')"
@click="openReorder(order)"
>编辑</n-button>
<n-dropdown :options="moreOptions" trigger="click" @select="(k) => onMoreSelect(k, order)">
<n-button text size="tiny">更多</n-button>
</n-dropdown>
</div>
</div>
<div v-if="isExpanded(order)" class="order-expand">
<div v-if="!(order.processList?.length)" class="expand-empty">暂无工序明细</div>
<div v-else class="card-flow-scroll">
<ProcessCard
v-for="(proc, idx) in sortedProcessList(order.processList)"
:key="proc.planId ?? idx"
:process="proc"
:material-code="order.materialCode"
:order-code="order.orderCode"
:order-begin-time="order.beginTime"
:order-end-time="order.endTime"
:workshop-name="order.workshopName"
:is-last="idx === (order.processList?.length ?? 0) - 1"
:can-edit="hasPermission('biz:orderItem:edit')"
:can-assign="hasPermission('biz:orderItem:assign')"
:can-outsource="hasPermission('biz:orderProcessPlan:add')"
@edit="(p) => handleEdit(p, order)"
@assign="(p) => disphand(p, order)"
@outsource="(p) => openOutsource(order, p)"
@view-model="(p) => openPlmModel(order, p)"
@submit-detail = "(p) => openDetailModel(order,p)"
/>
</div>
</div>
</div>
</n-spin>
</div>
<!--模型抽屉 -->
<PlmModelDrawer ref="plmModelDrawerRef" />
<div class="board-pagination">
<n-pagination
v-model:page="pagination.page"
v-model:page-size="pagination.pageSize"
:item-count="pagination.itemCount"
:page-sizes="[10, 20, 50, 100]"
show-size-picker
show-quick-jumper
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
>
<template #prefix>共 {{ pagination.itemCount }} 条</template>
</n-pagination>
</div>
<!-- 详情弹窗 -->
<n-modal v-model:show="detailVisible" preset="card" title="工序计划详情" style="width: 900px">
<n-spin :show="detailLoading">
<template v-if="detailData">
<n-descriptions :column="3" bordered size="small" label-placement="left">
<n-descriptions-item label="生产令号">{{ detailData.mainCode || '-' }}</n-descriptions-item>
<n-descriptions-item label="工单号">{{ formatWorkOrderCodes(detailData) }}</n-descriptions-item>
<n-descriptions-item label="物料">{{ detailData.materialName || '-' }}</n-descriptions-item>
<n-descriptions-item label="数量">{{ detailData.quantity ?? '-' }}</n-descriptions-item>
<n-descriptions-item label="工艺路线">{{ detailData.routeCode || '-' }}</n-descriptions-item>
<n-descriptions-item label="车间">{{ detailData.workshopName || '-' }}</n-descriptions-item>
<n-descriptions-item label="计划开工">{{ detailData.beginTime || '-' }}</n-descriptions-item>
<n-descriptions-item label="计划完工">{{ detailData.endTime || '-' }}</n-descriptions-item>
<n-descriptions-item label="订单状态">{{ lifeLabel(detailData) }}</n-descriptions-item>
</n-descriptions>
<div class="detail-cards">
<ProcessCard
v-for="(proc, idx) in (detailData.processList)"
:key="proc.planId ?? idx"
:process="proc"
:material-code="detailData.materialCode"
:order-code="detailData.orderCode"
:order-begin-time="detailData.beginTime"
:order-end-time="detailData.endTime"
:workshop-name="detailData.workshopName"
:is-last="idx === (detailData.processList?.length ?? 0) - 1"
:can-edit="hasPermission('biz:orderItem:edit')"
:can-assign="hasPermission('biz:orderItem:assign')"
:can-outsource="hasPermission('biz:orderProcessPlan:add')"
:can-submitDetail="hasPermission('biz:submitLog:detail')"
@edit="(p) => handleEdit(p, detailData!)"
@assign="(p) => disphand(p, detailData!)"
@outsource="(p) => openOutsource(detailData!, p)"
@view-model="(p) => openPlmModel(detailData!, p)"
/>
</div>
</template>
</n-spin>
</n-modal>
<!-- 订单级工序顺序重排:拖拽 / 上移下移,只提交 planId 顺序 -->
<n-modal
v-model:show="reorderVisible"
preset="card"
title="调整工序顺序"
style="width: 640px"
:mask-closable="false"
@after-leave="resetReorder"
>
<n-spin :show="reorderLoading">
<n-alert type="info" style="margin-bottom: 12px">
拖拽行或使用上移/下移调整顺序;保存后按 10、20、30… 重写工序号。存在派工工单或已有转出数量的工序不可调整。
</n-alert>
<div v-if="reorderOrderMeta" class="reorder-meta">
<span>令号:{{ reorderOrderMeta.mainCode || '-' }}</span>
<span>物料:{{ reorderOrderMeta.materialName || '-' }}</span>
<span>数量:{{ reorderOrderMeta.quantity ?? '-' }}</span>
</div>
<div v-if="!reorderList.length && !reorderLoading" class="reorder-empty">暂无工序可调整</div>
<VueDraggable
v-else
v-model="reorderList"
:animation="150"
handle=".reorder-drag-handle"
class="reorder-list"
>
<div
v-for="(proc, idx) in reorderList"
:key="proc.planId ?? idx"
class="reorder-item"
>
<n-icon class="reorder-drag-handle" size="18" :depth="3">
<MenuOutline />
</n-icon>
<span class="reorder-index">{{ idx + 1 }}</span>
<div class="reorder-body">
<div class="reorder-title">
<em v-if="proc.operNumber != null">{{ proc.operNumber }}</em>
{{ proc.processName || '-' }}
</div>
<div class="reorder-sub">
{{ proc.processCode || '-' }} · {{ proc.workOrderCode || '-' }}
</div>
</div>
<n-space :size="4">
<n-button
size="tiny"
quaternary
:disabled="idx === 0"
@click="moveReorderItem(idx, -1)"
>上移</n-button>
<n-button
size="tiny"
quaternary
:disabled="idx === reorderList.length - 1"
@click="moveReorderItem(idx, 1)"
>下移</n-button>
</n-space>
</div>
</VueDraggable>
</n-spin>
<template #footer>
<n-space justify="end">
<n-button @click="reorderVisible = false">取消</n-button>
<n-button
type="primary"
:loading="reorderSubmitLoading"
:disabled="!reorderList.length"
@click="submitReorder"
>保存顺序</n-button>
</n-space>
</template>
</n-modal>
<!-- 新增/编辑(单工序字段;顺序请用上方重排弹窗) -->
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 800px">
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="120px">
<n-grid :cols="2">
<n-gi>
<n-form-item label="工序名称" path="name">
<n-input v-model:value="formData.name" placeholder="请输入工序名称" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="工序编码" path="code">
<n-input v-model:value="formData.code" placeholder="请输入工序编码" />
</n-form-item>
</n-gi>
<n-gi>
<!-- 顺序请走订单行「编辑」重排,避免单条改 sort 撞号断链 -->
<n-form-item label="工序顺序" path="sort">
<n-input v-model:value="formData.sort" placeholder="请通过订单编辑调整顺序" disabled />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="生产订单id" path="orderItemId">
<n-input v-model:value="formData.orderItemId" placeholder="请输入生产订单id" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="生产开始时间" path="beginTime">
<n-date-picker v-model:value="formData.beginTime" type="datetime" clearable style="width: 100%" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="生产结束时间" path="endTime">
<n-date-picker v-model:value="formData.endTime" type="datetime" clearable style="width: 100%" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="生产总数" path="quantity">
<n-input v-model:value="formData.quantity" placeholder="请输入生产总数" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="转入数量" path="transferNum">
<n-input v-model:value="formData.transferNum" placeholder="请输入转入数量" />
</n-form-item>
</n-gi>
</n-grid>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="modalVisible = false">取消</n-button>
<n-button type="primary" @click="handleSubmit">确定</n-button>
</n-space>
</template>
</n-modal>
<!-- 工序工单拆单(整单:全部工序一起按数量拆) -->
<SplitProcessDialog
v-model:show="splitVisible"
:plan-id="splitPlanId"
@success="onSplitSuccess"
/>
<!-- 派工 -->
<n-modal v-model:show="dispatchmodal" title="工序派工" preset="card" style="width: 800px" :mask-closable="false">
<n-divider />
<n-card>
<n-grid :cols="2">
<n-gi>
<span class="pgClass">当前工序:</span>
<span>{{dispatchform.name}}</span>
</n-gi>
<n-gi>
<span class="pgClass">计划数量:</span>
<span>{{dispatchform.quantity}}</span>
</n-gi>
</n-grid>
<n-grid :cols="2">
<n-gi>
<span class="pgClass">已完成:</span>
<span>{{dispatchform.completedQty ?? 0}}</span>
</n-gi>
<n-gi>
<span class="pgClass">本次待派:</span>
<span style="color: #2080f0; font-weight: 600">{{dispatchform.remainQty}}</span>
</n-gi>
</n-grid>
<n-grid :cols="2">
<n-gi>
<span class="pgClass">开始时间:</span>
<span>{{dispatchform.beginTime}}</span>
</n-gi>
<n-gi>
<span class="pgClass">结束时间:</span>
<span>{{dispatchform.endTime}}</span>
</n-gi>
</n-grid>
</n-card>
<n-form ref="dispatchformRef" :model="dispatchform"
label-placement="left"
:rules="dispatchrules"
style="margin-top: 10px;"
label-width="80">
<n-grid :cols="2">
<n-gi :span="24">
<Pgitem
v-for="(n, i) in dispatchform.list"
:key="i"
:obj="n"
:index="i"
:section-id="dispatchform.sectionId"
:workshop-id="dispatchform.workshopId"
listname="list"
@addhand="addpgnum"
@delehand="deletenum(i)"
/>
</n-gi>
</n-grid>
</n-form>
<template #footer>
<n-space justify="end">
<n-button size="small" @click="dispatchmodal = false">取消</n-button>
<n-button size="small" type="primary" :loading="dispatchLoading" @click="dispatchSubmit">确定</n-button>
</n-space>
</template>
</n-modal>
<!-- 工序委外 -->
<n-modal
v-model:show="outsourceModalVisible"
preset="card"
:title="outsourceStep === 1 ? '创建工序委外 - 选择工序' : '创建工序委外 - 填写信息'"
style="width: 720px"
:mask-closable="false"
@after-leave="resetOutsourceForm"
>
<n-spin :show="outsourceOptionsLoading">
<template v-if="outsourceStep === 1 && outsourceOptions">
<n-descriptions :column="2" bordered size="small" label-placement="left" style="margin-bottom: 16px">
<n-descriptions-item label="生产令号">{{ outsourceOptions.mainCode || '-' }}</n-descriptions-item>
<n-descriptions-item label="订单编码">{{ outsourceOptions.orderCode || '-' }}</n-descriptions-item>
<n-descriptions-item label="物料编码">{{ outsourceOptions.materialCode || '-' }}</n-descriptions-item>
<n-descriptions-item label="物料名称">{{ outsourceOptions.materialName || '-' }}</n-descriptions-item>
<n-descriptions-item label="生产数量">{{ outsourceOptions.quantity ?? '-' }}</n-descriptions-item>
</n-descriptions>
<n-radio-group v-model:value="outsourceSelectedPlanId" style="width: 100%">
<n-space vertical :size="8" style="width: 100%">
<div
v-for="proc in outsourceOptions.processList"
:key="proc.planId"
class="outsource-process-item"
:class="{ active: outsourceSelectedPlanId === proc.planId }"
@click="outsourceSelectedPlanId = proc.planId!"
>
<n-radio :value="proc.planId" />
<div class="outsource-process-body">
<div class="outsource-process-title">
<span>{{ proc.processName || '-' }}</span>
<n-tag v-if="proc.current" size="tiny" type="info" :bordered="false">当前</n-tag>
<n-tag v-if="proc.outsource === 1" size="tiny" type="warning" :bordered="false">已委外</n-tag>
</div>
<div class="outsource-process-meta">
<span>工单号{{ proc.workOrderCode || '-' }}</span>
<span>工序号{{ proc.operNumber ?? '-' }}</span>
<span>数量{{ proc.quantity ?? '-' }}</span>
</div>
</div>
</div>
</n-space>
</n-radio-group>
</template>
<template v-else-if="outsourceStep === 2">
<n-card size="small" style="margin-bottom: 16px">
<n-grid :cols="2" :x-gap="12">
<n-gi>
<span class="pgClass">委外工序</span>
<span>{{ selectedOutsourceProcess?.processName || '-' }}</span>
</n-gi>
<n-gi>
<span class="pgClass">工单号</span>
<span>{{ selectedOutsourceProcess?.workOrderCode || '-' }}</span>
</n-gi>
<n-gi>
<span class="pgClass">物料</span>
<span>{{ outsourceOptions?.materialName || '-' }}</span>
</n-gi>
<n-gi>
<span class="pgClass">工序数量</span>
<span>{{ selectedOutsourceProcess?.quantity ?? '-' }}</span>
</n-gi>
</n-grid>
</n-card>
<n-form
ref="outsourceFormRef"
:model="outsourceForm"
:rules="outsourceRules"
label-placement="left"
label-width="110"
>
<n-form-item label="供应商" path="supplierName">
<n-input v-model:value="outsourceForm.supplierName" placeholder="请输入供应商名称" />
</n-form-item>
<n-form-item label="委外数量" path="quantity">
<n-input-number
v-model:value="outsourceForm.quantity"
:min="1"
:max="selectedOutsourceProcess?.quantity ?? undefined"
:precision="0"
placeholder="请输入委外数量"
style="width: 100%"
/>
</n-form-item>
<n-form-item label="单价" path="unitPrice">
<n-input-number
v-model:value="outsourceForm.unitPrice"
:min="0"
:precision="2"
placeholder="请输入单价(选填)"
style="width: 100%"
/>
</n-form-item>
<n-form-item label="计划开始时间" path="planStartTime">
<n-date-picker
v-model:value="outsourceForm.planStartTime"
type="datetime"
clearable
style="width: 100%"
/>
</n-form-item>
<n-form-item label="计划结束时间" path="planEndTime">
<n-date-picker
v-model:value="outsourceForm.planEndTime"
type="datetime"
clearable
style="width: 100%"
/>
</n-form-item>
<n-form-item label="备注" path="remark">
<n-input
v-model:value="outsourceForm.remark"
type="textarea"
placeholder="请输入备注(选填)"
:autosize="{ minRows: 2, maxRows: 4 }"
/>
</n-form-item>
</n-form>
</template>
</n-spin>
<template #footer>
<n-space justify="end">
<n-button @click="outsourceModalVisible = false">取消</n-button>
<n-button v-if="outsourceStep === 2" @click="outsourceStep = 1">上一步</n-button>
<n-button
v-if="outsourceStep === 1"
type="primary"
:disabled="!outsourceSelectedPlanId"
@click="goOutsourceStep2"
>下一步</n-button>
<n-button
v-else
type="primary"
:loading="outsourceSubmitLoading"
@click="submitOutsource"
>确认创建</n-button>
</n-space>
</template>
</n-modal>
<!-- 导入 -->
<n-modal v-model:show="importModalVisible" preset="card" title="导入工序计划" style="width: 500px">
<n-space vertical>
<n-alert type="info">
<template #header>导入说明</template>
请先下载模板,按格式填写后上传 .xlsx / .xls 文件。
</n-alert>
<n-button type="primary" @click="handleDownloadTemplate">
<template #icon><n-icon><DownloadOutline /></n-icon></template>
下载模板
</n-button>
<n-upload :max="1" accept=".xlsx,.xls" :custom-request="handleImportUpload">
<n-upload-dragger>
<n-text>点击或拖拽文件到此处上传</n-text>
</n-upload-dragger>
</n-upload>
</n-space>
</n-modal>
<!-- 汇报详情弹窗 -->
<n-modal v-model:show="submitLogModalVisible" preset="card" title="工序汇报详情" style="width: 2000px">
<ReportDetailPage
:planId="processId"
/>
<template #footer>
<n-space justify="end">
<n-button size="small" @click="submitLogModalVisible = false">取消</n-button>
</n-space>
</template>
</n-modal>
<n-modal v-model:show="submitModalVisible" preset="card" title="工单汇报详情" style="width: 2000px">
<OrderReportDetailPage
:orderItemId="orderItemId"
/>
<template #footer>
<n-space justify="end">
<n-button size="small" @click="submitLogModalVisible = false">取消</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, h, computed, type Component } from 'vue'
import {
NButton, NSpace, NIcon, NTag, NSpin, NModal, NForm, NFormItem, NGrid, NGi,
NInput, NInputNumber, NDatePicker, NCheckbox, NEmpty, NPagination, NDropdown, NDescriptions,
NDescriptionsItem, NAlert, NUpload, NText, NCard, NRadioGroup, NRadio, NDivider, useMessage, useDialog,
type UploadCustomRequestOptions, type FormRules,
} from 'naive-ui'
import {
AddOutline, SearchOutline, RefreshOutline, CloudUploadOutline,
DownloadOutline, ChevronDownOutline, CheckmarkCircleOutline, CheckmarkCircle,
EllipseOutline, TimeOutline, PrintOutline, PeopleOutline,
GitBranchOutline, CloseCircleOutline, LockClosedOutline, TrashOutline, EyeSharp,
MenuOutline,
} from '@vicons/ionicons5'
import { VueDraggable } from 'vue-draggable-plus'
import {
orderProcessPlanApi,
toOrderProcessPlanPayload,
processItemToEntity,
ORDER_STATUS_MAP,
type OrderProcessPlanVO,
type ProcessPlanItemVO,
type OutsourcingOptions,
type OutsourcingProcessOption,
} from '@/api/orderProcessPlan'
import { useUserStore } from '@/stores/user'
import ProcessFlowStepper from './components/ProcessFlowStepper.vue'
import ProcessCard from './components/ProcessCard.vue'
import SplitProcessDialog from './components/SplitProcessDialog.vue'
import PlmModelDrawer from '@/components/PlmModelDrawer.vue'
import type { PlmModelOpenContext } from '@/api/plmModel'
import Pgitem from './pgitem.vue'
import ReportDetailPage from "@/views/biz/orderProcessPlan/components/ReportDetail.vue";
import OrderReportDetailPage from "@/views/biz/orderProcessPlan/components/OrderReportDetail.vue";
const message = useMessage()
const dialog = useDialog()
const userStore = useUserStore()
const hasPermission = (p: string) => userStore.hasPermission(p)
const searchForm = reactive({
name: '',
code: '',
})
const tableData = ref<OrderProcessPlanVO[]>([])
const expandedIds = ref<Set<string>>(new Set())
const selectedOrderIds = ref<string[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, itemCount: 0 })
const detailVisible = ref(false)
const detailLoading = ref(false)
const detailData = ref<OrderProcessPlanVO | null>(null)
/** 工序工单拆单 */
const splitVisible = ref(false)
const splitPlanId = ref<number | null>(null)
/** 订单级工序重排弹窗状态 */
const reorderVisible = ref(false)
const reorderLoading = ref(false)
const reorderSubmitLoading = ref(false)
const reorderOrderItemId = ref<number | null>(null)
const reorderOrderMeta = ref<OrderProcessPlanVO | null>(null)
/** 弹窗内可拖拽的工序列表(按当前展示顺序) */
const reorderList = ref<ProcessPlanItemVO[]>([])
const modalVisible = ref(false)
const modalTitle = ref('')
const formRef = ref()
const defaultFormData = {
name: '', code: '', sort: '', orderItemId: '',
beginTime: null as number | null, endTime: null as number | null,
quantity: '', transferNum: '', transferOutNum: '',
procurement: '', outsource: '', qualityInspection: '', storageEntry: '',
}
const formData = reactive<any>({ ...defaultFormData })
const formRules = {}
const dispatchmodal = ref(false)
const dispatchLoading = ref(false)
const dispatchformRef = ref()
const dispatchform = reactive<any>({
id: '', name: '', beginTime: '', endTime: '', quantity: '', sectionId: null as number | null, workshopId: null as number | null,
list: [{ sectionId:'',deviceId:'', quantity: '', ncId:'' }],
})
const dispatchrules = {}
const importModalVisible = ref(false)
const plmModelDrawerRef = ref<InstanceType<typeof PlmModelDrawer> | null>(null)
const submitModalVisible = ref<Boolean>(false)
const outsourceModalVisible = ref(false)
const outsourceOptionsLoading = ref(false)
const outsourceSubmitLoading = ref(false)
const outsourceStep = ref(1)
const outsourceOptions = ref<OutsourcingOptions | null>(null)
const outsourceSelectedPlanId = ref<number | null>(null)
const outsourceFormRef = ref()
const outsourceForm = reactive({
supplierName: '',
quantity: null as number | null,
unitPrice: null as number | null,
planStartTime: null as number | null,
planEndTime: null as number | null,
remark: '',
})
const outsourceRules: FormRules = {
supplierName: [{ required: true, message: '请输入供应商', trigger: 'blur' }],
quantity: [{ required: true, type: 'number', message: '请输入委外数量', trigger: ['blur', 'change'] }],
}
const selectedOutsourceProcess = computed<OutsourcingProcessOption | null>(() => {
if (!outsourceSelectedPlanId.value) return null
return outsourceOptions.value?.processList?.find(p => p.planId === outsourceSelectedPlanId.value) ?? null
})
function openPlmModel(order: OrderProcessPlanVO, process?: ProcessPlanItemVO) {
const ctx: PlmModelOpenContext = {
orderItemId: order.orderItemId,
materialCode: order.materialCode,
materialName: order.materialName,
drawingNo: order.materialCode,
title: process
? `三维模型 · ${process.processName || process.processCode || ''}`
: `三维模型 · ${order.materialName || order.materialCode || ''}`,
}
if (process?.planId) ctx.processPlanId = process.planId
plmModelDrawerRef.value?.open(ctx)
}
//汇报记录详情
const submitLogModalVisible = ref<Boolean>(false)
const processId = ref<number | null>(null)
//汇报详情
function openDetailModel(order:ProcessPlanItemVO,process?:OrderProcessPlanVO) {
submitLogModalVisible.value = true
console.log(process.planId)
processId.value = process.planId
}
const batchOptions = [
{ label: '全部展开', key: 'expandAll' },
{ label: '全部收起', key: 'collapseAll' },
]
function dropdownIcon(icon: Component) {
return () => h(NIcon, null, { default: () => h(icon) })
}
function dangerDropdownItem(label: string, key: string, icon: Component) {
return {
label,
key,
icon: () => h(NIcon, { color: '#d03050' }, { default: () => h(icon) }),
props: { style: { color: '#d03050' } },
}
}
const moreOptions = [
{
type: 'group',
label: '查看与编辑',
key: 'group-view-edit',
children: [
{ label: '打印', key: 'print', icon: dropdownIcon(PrintOutline) },
],
},
{
type: 'group',
label: '派生操作',
key: 'group-derived',
children: [
{ label: '创建工序委外', key: 'createOutsource', icon: dropdownIcon(PeopleOutline) },
{ label: '拆分工单', key: 'splitWorkOrder', icon: dropdownIcon(GitBranchOutline) },
{ label: '汇报详情', key: 'reportDetailView', icon: dropdownIcon(EyeSharp) },
],
},
{
type: 'group',
label: '状态控制',
key: 'group-status',
children: [
dangerDropdownItem('撤回', 'withdraw', CloseCircleOutline),
dangerDropdownItem('冻结工单', 'freeze', LockClosedOutline),
],
},
{
type: 'group',
label: '危险操作',
key: 'group-danger',
children: [
dangerDropdownItem('删除', 'delete', TrashOutline),
],
},
]
/** 列表行主键:按工单号(拆单后同订单多工单分行) */
function orderRowKey(row: OrderProcessPlanVO) {
if (row.workOrderCode) return row.workOrderCode
return `oi-${row.orderItemId ?? 'x'}`
}
/** 订单级工单号展示 */
function formatWorkOrderCodes(order?: OrderProcessPlanVO | null) {
if (order?.workOrderCode) return order.workOrderCode
const codes = (order?.processList ?? [])
.map((p) => p.workOrderCode)
.filter((c): c is string => !!c)
const unique = [...new Set(codes)]
return unique.length ? unique.join('、') : '-'
}
function sortedProcessList(list?: ProcessPlanItemVO[]) {
return [...(list ?? [])].sort((a, b) => {
const oa = a.operNumber ?? 0
const ob = b.operNumber ?? 0
if (oa !== ob) return oa - ob
return (a.planId ?? 0) - (b.planId ?? 0)
})
}
function isExpanded(order: OrderProcessPlanVO) {
return expandedIds.value.has(orderRowKey(order))
}
function toggleExpand(order: OrderProcessPlanVO) {
const key = orderRowKey(order)
const next = new Set(expandedIds.value)
if (next.has(key)) next.delete(key)
else next.add(key)
expandedIds.value = next
}
/** 整单拆分入口:有计划数量且未全部完工即可点开 */
function canSplitWorkOrder(order: OrderProcessPlanVO) {
const qty = order.quantity ?? 0
if (qty <= 0) return false
if (order.orderStatus === 2) return false
return (order.processList?.length ?? 0) > 0
}
function openSplitFromOrder(order: OrderProcessPlanVO) {
if (!hasPermission('biz:orderProcessPlan:edit')) {
message.warning('无拆单权限')
return
}
const list = sortedProcessList(order.processList)
const seed = list.find((p) => p.planId != null)
if (!seed?.planId) {
message.warning('当前工单暂无工序,无法拆单')
return
}
if (!canSplitWorkOrder(order)) {
message.warning('该工单不可拆(无数量或已完工)')
return
}
splitPlanId.value = seed.planId
splitVisible.value = true
}
function onSplitSuccess() {
loadData()
if (detailVisible.value && detailData.value?.orderItemId) {
openDetail(detailData.value)
}
}
function toggleSelect(id: string, checked: boolean) {
if (checked) selectedOrderIds.value = [...selectedOrderIds.value, id]
else selectedOrderIds.value = selectedOrderIds.value.filter(v => v !== id)
}
function formatShortTime(val?: string | null) {
if (!val) return '-'
return val.length > 16 ? val.slice(0, 16) : val
}
function isOverdue(order: OrderProcessPlanVO) {
if (order.orderStatus === 2 || !order.endTime) return false
return new Date(order.endTime.replace(' ', 'T')).getTime() < Date.now()
}
function hasOutsource(order: OrderProcessPlanVO) {
return order.processList?.some(p => p.outsource === 1)
}
function lifeLabel(order: OrderProcessPlanVO) {
const s = order.orderStatus
if (s == null) return '-'
return ORDER_STATUS_MAP[s]?.label ?? String(s)
}
function lifeIcon(order: OrderProcessPlanVO) {
const s = order.orderStatus
if (s === 2) return { icon: CheckmarkCircle, color: '#18a058' }
if (s === 1) return { icon: TimeOutline, color: '#2080f0' }
return { icon: EllipseOutline, color: '#8c8c8c' }
}
async function loadData() {
loading.value = true
try {
const res = await orderProcessPlanApi.page({
page: pagination.page,
pageSize: pagination.pageSize,
name: searchForm.name || undefined,
code: searchForm.code || undefined,
})
tableData.value = res.list ?? []
pagination.itemCount = res.total
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.name = ''
searchForm.code = ''
handleSearch()
}
function handlePageChange(page: number) {
pagination.page = page
loadData()
}
function handlePageSizeChange(pageSize: number) {
pagination.pageSize = pageSize
pagination.page = 1
loadData()
}
function onBatchSelect(key: string) {
if (key === 'expandAll') {
expandedIds.value = new Set(tableData.value.map(o => orderRowKey(o)))
} else if (key === 'collapseAll') {
expandedIds.value = new Set()
}
}
function onMoreSelect(key: string, order: OrderProcessPlanVO) {
if (key === 'createOutsource') {
openOutsource(order)
return
}
if (key === 'reportDetailView') {
openReportDetail(order)
return;
}
if (key === 'splitWorkOrder') {
openSplitFromOrder(order)
return
}
const labels: Record<string, string> = {
print: '打印',
withdraw: '撤回',
freeze: '冻结工单',
delete: '删除',
}
if (labels[key]) message.info(`${labels[key]}功能待后端接口支持`)
}
function resolveDefaultPlanId(order: OrderProcessPlanVO, process?: ProcessPlanItemVO) {
if (process?.planId) return process.planId
const list = sortedProcessList(order.processList)
if (!list.length) return null
const current = list.find(p => p.operNumber === order.currentOperNumber)
return current?.planId ?? list[0].planId ?? null
}
function resetOutsourceForm() {
outsourceStep.value = 1
outsourceOptions.value = null
outsourceSelectedPlanId.value = null
outsourceForm.supplierName = ''
outsourceForm.quantity = null
outsourceForm.unitPrice = null
outsourceForm.planStartTime = null
outsourceForm.planEndTime = null
outsourceForm.remark = ''
outsourceFormRef.value?.restoreValidation()
}
function fillOutsourceFormFromProcess(proc?: OutsourcingProcessOption | null) {
if (!proc) return
outsourceForm.quantity = proc.quantity ?? null
outsourceForm.planStartTime = parseDateTime(proc.planStartTime)
outsourceForm.planEndTime = parseDateTime(proc.planFinishTime)
}
async function openOutsource(order: OrderProcessPlanVO, process?: ProcessPlanItemVO) {
const planId = resolveDefaultPlanId(order, process)
if (!planId) {
message.warning('当前订单暂无可委外工序')
return
}
resetOutsourceForm()
outsourceModalVisible.value = true
outsourceOptionsLoading.value = true
try {
const options = await orderProcessPlanApi.getOutsourcingOptions(planId)
outsourceOptions.value = options
const preferredId = process?.planId ?? options.currentProcessPlanId ?? options.processList?.[0]?.planId ?? null
outsourceSelectedPlanId.value = preferredId
if (process?.planId) {
outsourceStep.value = 2
fillOutsourceFormFromProcess(selectedOutsourceProcess.value)
}
} catch {
outsourceModalVisible.value = false
} finally {
outsourceOptionsLoading.value = false
}
}
function goOutsourceStep2() {
if (!outsourceSelectedPlanId.value) {
message.warning('请选择委外工序')
return
}
fillOutsourceFormFromProcess(selectedOutsourceProcess.value)
outsourceStep.value = 2
}
function formatSubmitDateTime(ts: number | null) {
if (!ts) return undefined
return new Date(ts).toISOString().slice(0, 19).replace('T', ' ')
}
async function submitOutsource() {
await outsourceFormRef.value?.validate()
if (!outsourceSelectedPlanId.value) {
message.warning('请选择委外工序')
return
}
const maxQty = selectedOutsourceProcess.value?.quantity
if (maxQty != null && (outsourceForm.quantity ?? 0) > maxQty) {
message.error(`委外数量不能大于工序数量 ${maxQty}`)
return
}
if (outsourceForm.planStartTime && outsourceForm.planEndTime
&& outsourceForm.planEndTime < outsourceForm.planStartTime) {
message.error('计划结束时间不能早于计划开始时间')
return
}
outsourceSubmitLoading.value = true
try {
await orderProcessPlanApi.createOutsourcing({
processPlanId: outsourceSelectedPlanId.value,
supplierName: outsourceForm.supplierName.trim(),
quantity: outsourceForm.quantity!,
unitPrice: outsourceForm.unitPrice ?? undefined,
planStartTime: formatSubmitDateTime(outsourceForm.planStartTime),
planEndTime: formatSubmitDateTime(outsourceForm.planEndTime),
remark: outsourceForm.remark || undefined,
})
message.success('委外单创建成功')
outsourceModalVisible.value = false
loadData()
} finally {
outsourceSubmitLoading.value = false
}
}
async function openDetail(order: OrderProcessPlanVO) {
if (!order.orderItemId) return
detailVisible.value = true
detailLoading.value = true
detailData.value = null
try {
detailData.value = await orderProcessPlanApi.detail(order.orderItemId, order.workOrderCode)
} catch {
detailData.value = order
} finally {
detailLoading.value = false
}
}
/** 打开订单级工序顺序调整(拉详情,按 operNumber 排序后供拖拽) */
async function openReorder(order: OrderProcessPlanVO) {
if (!order.orderItemId) {
message.warning('缺少生产订单id')
return
}
reorderVisible.value = true
reorderLoading.value = true
reorderOrderItemId.value = order.orderItemId
reorderOrderMeta.value = order
reorderList.value = []
try {
const detail = await orderProcessPlanApi.detail(order.orderItemId, order.workOrderCode)
reorderOrderMeta.value = detail
// 仅当前工单工序,按 operNumber 升序
reorderList.value = sortedProcessList(detail.processList).map((p) => ({ ...p }))
if (!reorderList.value.length) {
message.warning('当前工单暂无工序计划')
}
} catch {
// 详情失败时降级用列表行数据,仍可本地排序后提交
reorderList.value = sortedProcessList(order.processList).map((p) => ({ ...p }))
} finally {
reorderLoading.value = false
}
}
function resetReorder() {
reorderOrderItemId.value = null
reorderOrderMeta.value = null
reorderList.value = []
reorderSubmitLoading.value = false
}
/** 上移 / 下移delta: -1 上移,+1 下移) */
function moveReorderItem(index: number, delta: number) {
const next = index + delta
if (next < 0 || next >= reorderList.value.length) return
const list = [...reorderList.value]
const [item] = list.splice(index, 1)
list.splice(next, 0, item)
reorderList.value = list
}
/** 提交重排:只传 orderItemId + planId 顺序 */
async function submitReorder() {
if (!reorderOrderItemId.value) {
message.warning('缺少生产订单id')
return
}
if (!reorderList.value.length) {
message.warning('暂无工序可保存')
return
}
const missing = reorderList.value.some((p) => p.planId == null)
if (missing) {
message.error('存在缺少 planId 的工序,请刷新后重试')
return
}
reorderSubmitLoading.value = true
try {
await orderProcessPlanApi.reorder({
orderItemId: reorderOrderItemId.value,
items: reorderList.value.map((p) => ({ planId: p.planId! })),
})
message.success('工序顺序已更新')
reorderVisible.value = false
loadData()
} finally {
reorderSubmitLoading.value = false
}
}
function handleAdd() {
modalTitle.value = '新增工序计划'
Object.assign(formData, defaultFormData)
modalVisible.value = true
}
function parseDateTime(val: string | null | undefined) {
if (!val) return null
const ts = new Date(val.replace(' ', 'T')).getTime()
return Number.isNaN(ts) ? null : ts
}
function handleEdit(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
modalTitle.value = '编辑工序计划'
const entity = processItemToEntity(process, order?.orderItemId)
Object.assign(formData, {
...defaultFormData,
...entity,
sort: entity.sort != null ? String(entity.sort) : '',
orderItemId: entity.orderItemId != null ? String(entity.orderItemId) : '',
quantity: entity.quantity != null ? String(entity.quantity) : '',
transferNum: entity.transferNum != null ? String(entity.transferNum) : '',
beginTime: parseDateTime(entity.beginTime),
endTime: parseDateTime(entity.endTime),
})
modalVisible.value = true
}
async function handleSubmit() {
await formRef.value?.validate()
const submitData = toOrderProcessPlanPayload({ ...formData })
if (typeof submitData.beginTime === 'number') {
submitData.beginTime = new Date(submitData.beginTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (typeof submitData.endTime === 'number') {
submitData.endTime = new Date(submitData.endTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (submitData.id) {
await orderProcessPlanApi.update(submitData)
message.success('修改成功')
} else {
await orderProcessPlanApi.create(submitData)
message.success('新增成功')
}
modalVisible.value = false
loadData()
}
function disphand(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
const entity = processItemToEntity(process, order?.orderItemId)
const planQty = Number(entity.quantity) || 0
const completedQty = Number(process.completedQty) || 0
const remainQty = Math.max(0, planQty - completedQty)
dispatchmodal.value = true
dispatchformRef.value?.restoreValidation()
dispatchform.id = entity.id
dispatchform.name = entity.name
// 工序计划时间优先,没有则用订单头 beginTime/endTime
dispatchform.beginTime = entity.beginTime || order?.beginTime || ''
dispatchform.endTime = entity.endTime || order?.endTime || ''
// quantity 保持工序计划总数,提交时不能改掉计划数量
dispatchform.quantity = entity.quantity
dispatchform.completedQty = completedQty
dispatchform.remainQty = remainQty
dispatchform.orderItemId = entity.orderItemId
dispatchform.sort = entity.sort
dispatchform.sectionId = entity.sectionId ?? process.sectionId ?? null
dispatchform.workshopId = entity.workshopId ?? process.workshopId ?? order?.workshopId ?? null
dispatchform.list = [{ sectionId: '', deviceId: '', quantity: remainQty || '',ncId:'' }]
}
function addpgnum() {
dispatchform.list.push({ sectionId: '', deviceId: '', quantity: '',ncId:'' })
}
function deletenum(index: number) {
dispatchform.list.splice(index, 1)
}
function dispatchSubmit() {
let total = 0
dispatchform.list.forEach((n: any) => {
total += Number(n.quantity) || 0
})
const remainQty = Number(dispatchform.remainQty)
if (remainQty <= 0) {
message.warning('当前工序已全部完成,无需再派工')
return
}
dispatchformRef.value?.validate((errors: any) => {
// Naive UI有 errors 表示校验失败
if (errors) return
if (total !== remainQty) {
message.error(`派工总数量需等于待派数量 ${remainQty}(计划 ${dispatchform.quantity},已完成 ${dispatchform.completedQty ?? 0}`, {
duration: 4000,
})
return
}
dispatchLoading.value = true
// 提交时 quantity 仍传计划总数,避免把工序计划数量改成待派数
orderProcessPlanApi.assignWork(dispatchform).then(() => {
message.success('派工成功')
dispatchmodal.value = false
loadData()
}).finally(() => { dispatchLoading.value = false })
})
}
async function handleExport() {
const blob = await orderProcessPlanApi.export({
name: searchForm.name || undefined,
code: searchForm.code || undefined,
})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '工序计划数据.xlsx'
link.click()
window.URL.revokeObjectURL(url)
}
async function handleDownloadTemplate() {
const blob = await orderProcessPlanApi.downloadTemplate()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '工序计划导入模板.xlsx'
link.click()
window.URL.revokeObjectURL(url)
}
async function handleImportUpload({ file }: UploadCustomRequestOptions) {
if (!file.file) return
const result = await orderProcessPlanApi.importData(file.file)
if (result.fail > 0) {
dialog.warning({
title: '导入结果',
content: `成功 ${result.success} 条,失败 ${result.fail}`,
positiveText: '确定',
})
} else {
message.success(`导入成功,共 ${result.success}`)
importModalVisible.value = false
}
loadData()
}
const orderItemId = ref<number>(null)
function openReportDetail(order) {
submitModalVisible.value = true
orderItemId.value = order.orderItemId
}
onMounted(loadData)
</script>
<style scoped>
.plan-board-page {
padding: 0;
background: #f5f7fa;
min-height: 100%;
}
.board-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
padding: 12px 16px;
background: #fff;
border-radius: 8px 8px 0 0;
border-bottom: 1px solid #eef0f3;
}
.board-filter {
padding: 12px 16px;
background: #fff;
border-bottom: 1px solid #eef0f3;
}
.board-table-wrap {
overflow-x: auto;
background: #fff;
}
.board-table-head,
.order-row {
display: grid;
/* 勾选 | 产品工单 | 数量 | 模式 | 齐套率 | 工序轴 | 计划时间 | 生命周期 | 操作 */
grid-template-columns:
40px
minmax(220px, 1.6fr)
64px
108px
72px
minmax(200px, 1.4fr)
168px
96px
140px;
align-items: center;
gap: 8px;
padding: 0 16px;
min-width: 1280px;
}
.board-table-head {
height: 40px;
background: #fafbfc;
border-bottom: 1px solid #e8eaed;
font-size: 12px;
font-weight: 600;
color: #8c8c8c;
}
.order-block {
background: #fff;
border-bottom: 1px solid #eef0f3;
}
.order-row {
min-height: 64px;
transition: background 0.15s;
}
.order-row:hover {
background: #f8faff;
}
.order-row.expanded {
background: rgba(32, 128, 240, 0.04);
}
.col-check {
display: flex;
justify-content: center;
flex-shrink: 0;
}
.col-product {
min-width: 0;
}
.col-qty,
.col-kit {
text-align: center;
font-size: 13px;
white-space: nowrap;
}
.product-name {
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-code {
display: flex;
align-items: center;
gap: 6px;
margin-top: 4px;
font-size: 12px;
color: #8c8c8c;
}
.col-mode {
display: flex;
flex-direction: column;
gap: 4px;
align-items: flex-start;
}
.col-plan {
font-size: 13px;
line-height: 1.4;
white-space: nowrap;
}
.col-plan .sub-text {
font-size: 12px;
color: #8c8c8c;
margin-top: 2px;
}
.col-life {
white-space: nowrap;
}
.col-flow {
min-width: 0;
overflow: hidden;
}
.col-flow.flow-expanded :deep(.flow-stepper) {
background: rgba(32, 128, 240, 0.1);
}
.life-cell {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.col-actions {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: nowrap;
white-space: nowrap;
}
.order-expand {
padding: 12px 16px 16px 56px;
background: linear-gradient(180deg, rgba(32, 128, 240, 0.03) 0%, #fff 100%);
border-top: 1px dashed #e8eaed;
}
.card-flow-scroll,
.detail-cards {
display: flex;
align-items: flex-start;
gap: 0;
overflow-x: auto;
padding-bottom: 8px;
}
.detail-cards {
margin-top: 16px;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
}
.expand-empty {
font-size: 13px;
color: #8c8c8c;
padding: 8px 0;
}
.board-empty {
padding: 48px;
background: #fff;
}
.board-pagination {
display: flex;
justify-content: flex-end;
padding: 12px 16px;
background: #fff;
border-radius: 0 0 8px 8px;
}
.pgClass {
font-weight: bold;
}
.outsource-process-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 12px;
border: 1px solid #e8e8e8;
border-radius: 8px;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.outsource-process-item:hover,
.outsource-process-item.active {
border-color: #2080f0;
background: rgba(32, 128, 240, 0.04);
}
.outsource-process-body {
flex: 1;
min-width: 0;
}
.outsource-process-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
}
.outsource-process-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 6px;
font-size: 12px;
color: #8c8c8c;
}
.reorder-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 12px;
font-size: 13px;
color: #595959;
}
.reorder-empty {
padding: 24px 0;
text-align: center;
color: #8c8c8c;
font-size: 13px;
}
.reorder-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 420px;
overflow-y: auto;
}
.reorder-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: #fafbfc;
border: 1px solid #eef0f3;
border-radius: 8px;
}
.reorder-drag-handle {
cursor: grab;
flex-shrink: 0;
color: #8c8c8c;
}
.reorder-drag-handle:active {
cursor: grabbing;
}
.reorder-index {
width: 22px;
text-align: center;
font-size: 13px;
font-weight: 600;
color: #2080f0;
flex-shrink: 0;
}
.reorder-body {
flex: 1;
min-width: 0;
}
.reorder-title {
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reorder-title em {
font-style: normal;
margin-right: 6px;
color: #2080f0;
}
.reorder-sub {
margin-top: 2px;
font-size: 12px;
color: #8c8c8c;
}
</style>