mes-vue/src/views/biz/orderProcessPlan/components/SplitProcessDialog.vue
2026-08-07 15:06:23 +08:00

930 lines
28 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>
<n-modal
v-model:show="visible"
preset="card"
title="拆分工单"
:style="{ width: '1120px' }"
:content-style="{ paddingTop: '8px', paddingBottom: '4px' }"
:mask-closable="false"
@after-leave="resetState"
>
<n-spin :show="loading">
<div class="split-scroll">
<n-descriptions v-if="preview" :column="3" label-placement="left" size="small" class="split-desc">
<n-descriptions-item label="原工单编号">{{ preview.workOrderCode || '-' }}</n-descriptions-item>
<n-descriptions-item label="物料编码">{{ preview.materialCode || '-' }}</n-descriptions-item>
<n-descriptions-item label="物料/订单">{{ preview.orderItemName || '-' }}</n-descriptions-item>
<n-descriptions-item label="计划数量">{{ preview.quantity ?? 0 }}</n-descriptions-item>
<n-descriptions-item label="已进入生产">{{ preview.investedQty ?? 0 }}</n-descriptions-item>
<n-descriptions-item label="可拆数量">{{ preview.splittableQty ?? 0 }}</n-descriptions-item>
<n-descriptions-item label="原工序" :span="3">
{{ preview.processCount ?? 0 }} 道
<span v-if="preview.processNames?.length" class="process-names">
{{ preview.processNames.join('、') }}
</span>
</n-descriptions-item>
</n-descriptions>
<n-alert v-if="preview && !preview.canSplit" type="warning" :bordered="false" style="margin: 8px 0">
{{ preview.reason || '当前工单不允许拆单' }}
</n-alert>
<template v-if="preview?.canSplit">
<div class="split-section">
<div class="split-section-title">数量分配</div>
<div class="field-row">
<span class="field-label-inline">拆分方式</span>
<n-radio-group v-model:value="mode" name="splitMode" size="small">
<n-space>
<n-radio value="equal">等量拆分</n-radio>
<n-radio value="custom">指定数量</n-radio>
</n-space>
</n-radio-group>
</div>
<!-- 等量按总份数均分计划数量第1份留原单其余新建 -->
<div v-if="mode === 'equal'" class="split-body">
<div class="field-row">
<span class="field-label-inline">拆成份数</span>
<n-input-number
v-model:value="equalCount"
:min="2"
:max="Math.max(2, maxEqualParts)"
:precision="0"
size="small"
style="width: 140px"
/>
<span class="hint-inline">含原工单第1份留原单</span>
</div>
<div class="hint-inline" style="margin-top: 6px">{{ equalHint }}</div>
</div>
<!-- 指定:手动设原单保留 + 各新工单数量 -->
<div v-else class="split-body">
<div class="field-row">
<span class="field-label-inline">原单保留</span>
<n-input-number
v-model:value="keepQty"
:min="unsplittableQty"
:max="Math.max(unsplittableQty, (preview.quantity ?? 0) - 1)"
:precision="0"
size="small"
style="width: 140px"
@update:value="onKeepQtyUpdate"
/>
<span class="hint-inline">≥不可拆 {{ unsplittableQty }},拆出 {{ splitOutQty }}</span>
</div>
<div v-for="(row, idx) in customRows" :key="idx" class="custom-row">
<n-input-number
v-model:value="customRows[idx]"
:min="1"
:precision="0"
size="small"
placeholder="新工单数量"
style="flex: 1"
/>
<n-button text type="error" :disabled="customRows.length <= 1" @click="removeCustomRow(idx)">
删除
</n-button>
</div>
<n-button dashed block size="small" style="margin-top: 6px" @click="addCustomRow">+ 添加工单</n-button>
<div class="sum-line" :class="{ error: customSum !== splitOutQty }">
拆出合计: {{ customSum }} / {{ splitOutQty }}
</div>
</div>
</div>
<div class="split-section">
<div class="split-section-title">
新工单工艺路线
<n-radio-group
v-model:value="routeMode"
name="routeMode"
size="small"
style="margin-left: 12px; font-weight: 400"
>
<n-space>
<n-radio value="kingdee">金蝶工艺路线</n-radio>
<n-radio value="assemble">创建新工艺路线</n-radio>
</n-space>
</n-radio-group>
</div>
<div v-if="routeMode === 'kingdee'" class="split-body">
<div v-if="!preview?.materialCode" class="hint">当前订单无物料编码,无法查询金蝶工艺路线</div>
<template v-else>
<div class="kingdee-layout">
<div class="kingdee-left">
<div class="field-label">路线列表({{ routeList.length }}</div>
<n-spin :show="routeLoading">
<div v-if="!routeLoading && routeList.length === 0" class="hint">未查询到工艺路线</div>
<div v-else class="route-card-list">
<div
v-for="route in routeList"
:key="route.routingNo"
class="route-card"
:class="{ active: routingNo === route.routingNo }"
@click="routingNo = route.routingNo || null"
>
<div class="route-card-head">
<span class="route-code">{{ route.routingNo }}</span>
<span class="route-step-count">{{ route.stepCount ?? route.steps?.length ?? 0 }}道</span>
</div>
<div class="route-flow">
{{ (route.processNames || []).join(' → ') || '暂无工序' }}
</div>
</div>
</div>
</n-spin>
</div>
<div class="kingdee-right">
<div class="field-label">
工序明细
<span v-if="selectedRoute" class="route-detail-sub">
{{ selectedRoute.routingNo }} · {{ selectedSteps.length }} 道
</span>
</div>
<n-data-table
v-if="selectedRoute"
size="small"
:bordered="true"
:single-line="true"
:columns="kingdeeStepColumns"
:data="selectedSteps"
:scroll-x="900"
:max-height="260"
:pagination="false"
/>
<div v-else class="route-detail-empty">点击左侧路线查看工序</div>
</div>
</div>
</template>
</div>
<div v-else class="split-body">
<div class="assemble-toolbar">
<span class="hint-inline">新增工艺路线方;说明为空时默认用工序名称</span>
<n-button size="small" type="primary" secondary @click="addAssembleRow">+ 添加工序</n-button>
</div>
<n-data-table
size="small"
:bordered="true"
:single-line="false"
:columns="assembleColumns"
:data="assembleRows"
:scroll-x="1080"
:max-height="280"
:pagination="false"
/>
</div>
</div>
</template>
</div>
</n-spin>
<template #footer>
<n-space justify="end">
<n-button @click="visible = false">取消</n-button>
<n-button type="primary" :loading="submitting" :disabled="!canConfirm" @click="confirmSplit">
确认拆分
</n-button>
</n-space>
</template>
</n-modal>
</template>
<script setup lang="ts">
/**
* 拆单弹窗
* - 已进入生产数量不可拆留原单只拆可拆数量
* - 等量拆分拆成份数均分计划数量第1份留原单其余新建 30÷3 原10 + 新10 + 新10
* - 指定数量手动设原单保留 + 各新工单数量
* - 金蝶工艺路线 / 工序表组装对齐工艺路线字段
*/
import { computed, h, ref, watch } from 'vue'
import {
NAlert,
NButton,
NDataTable,
NDescriptions,
NDescriptionsItem,
NInput,
NInputNumber,
NModal,
NRadio,
NRadioGroup,
NSelect,
NSpace,
NSpin,
NSwitch,
useMessage,
type DataTableColumns,
type SelectOption,
} from 'naive-ui'
import {
orderProcessPlanApi,
type ProcessPlanSplitPreview,
type ProcessPlanSplitStep,
} from '@/api/orderProcessPlan'
import {
processRouteApi,
type KingdeeProcessRouteStep,
type ProcessRouteSelectOption,
} from '@/api/processRoute'
import { basicProcessPlanApi, type BasicProcessPlan } from '@/api/basicProcessPlan'
interface AssembleRow extends ProcessPlanSplitStep {
_key: string
}
const props = defineProps<{
show: boolean
planId: number | null
}>()
const emit = defineEmits<{
'update:show': [value: boolean]
success: []
}>()
const message = useMessage()
const loading = ref(false)
const submitting = ref(false)
const preview = ref<ProcessPlanSplitPreview | null>(null)
const mode = ref<'equal' | 'custom'>('equal')
const keepQty = ref(0)
/** 等量拆分:总份数(含原单),至少 2 = 原单 + 1 新单 */
const equalCount = ref(2)
const customRows = ref<(number | null)[]>([null])
const routeMode = ref<'kingdee' | 'assemble'>('kingdee')
const routingNo = ref<string | null>(null)
const routeList = ref<ProcessRouteSelectOption[]>([])
const routeLoading = ref(false)
const processLoading = ref(false)
const processOptions = ref<SelectOption[]>([])
const processMap = ref<Record<number, BasicProcessPlan>>({})
const assembleRows = ref<AssembleRow[]>([])
const ctrlCodeOptions: SelectOption[] = [
{ label: '汇报+免检', value: '汇报+免检' },
{ label: '汇报+质量', value: '汇报+质量' },
{ label: '委外+质量', value: '委外+质量' },
{ label: '委外+汇报', value: '委外+汇报' },
]
const activityUnitOptions = ref<SelectOption[]>([
{ label: '小时', value: '小时' },
{ label: '分钟', value: '分钟' },
{ label: '秒', value: '秒' },
])
const visible = computed({
get: () => props.show,
set: (v: boolean) => emit('update:show', v),
})
const unsplittableQty = computed(() => preview.value?.investedQty ?? 0)
const planQty = computed(() => preview.value?.quantity ?? 0)
const splitOutQty = computed(() => Math.max(planQty.value - (keepQty.value ?? 0), 0))
function onKeepQtyUpdate(v: number | null) {
const min = unsplittableQty.value
const max = Math.max(min, (preview.value?.quantity ?? 0) - 1)
if (v == null || v < min) {
keepQty.value = min
return
}
if (v > max) keepQty.value = max
}
/**
* 等量拆分:把整单计划数量均分成 N 份(含原单)。
* quantities = [原单保留, 新单1, 新单2, ...]
*/
const equalQuantities = computed(() => {
const q = planQty.value
const n = equalCount.value
if (!q || !n || n < 2 || q < n) return [] as number[]
const base = Math.floor(q / n)
const rem = q % n
const list = Array.from({ length: n }, () => base)
// 余数加在最后一份,避免中间份变成 0
list[n - 1] = base + rem
if (list.some((v) => v <= 0)) return []
// 第1份必须盖住不可拆已进入生产数量
if (list[0] < unsplittableQty.value) return []
return list
})
/** 在「第1份 ≥ 不可拆」前提下,最多能拆成几份 */
const maxEqualParts = computed(() => {
const q = planQty.value
const inv = unsplittableQty.value
if (q < 2) return 2
let max = Math.min(q, Math.max(2, q - inv)) // 至少留 1 给某新单时可拆上限粗估
// 收紧floor(q/n) >= inv余数在末份时首份=floor
while (max > 2 && Math.floor(q / max) < inv) {
max -= 1
}
// 还要保证至少能拆出 1 个新单:首份 < q
while (max > 2) {
const base = Math.floor(q / max)
if (base >= Math.max(1, inv) && base < q) break
max -= 1
}
return Math.max(2, max)
})
const equalHint = computed(() => {
const list = equalQuantities.value
if (!list.length) {
if (planQty.value < equalCount.value) return `计划数量不足以分成 ${equalCount.value}`
if (equalCount.value >= 2 && Math.floor(planQty.value / equalCount.value) < unsplittableQty.value) {
return `第1份无法覆盖不可拆数量 ${unsplittableQty.value},请减少份数`
}
return ''
}
const keep = list[0]
const news = list.slice(1)
const newCount = news.length
if (news.every((v) => v === news[0])) {
return `结果:原单保留 ${keep},新建 ${newCount} 张各 ${news[0]}(共 ${list.length} 张)`
}
return `结果:原单保留 ${keep},新建 ${newCount} 张(${news.join('、')}`
})
const customSum = computed(() =>
customRows.value.reduce((s, v) => s + (typeof v === 'number' && v > 0 ? v : 0), 0),
)
const selectedRoute = computed(() =>
routingNo.value ? routeList.value.find((r) => r.routingNo === routingNo.value) ?? null : null,
)
const selectedSteps = computed<KingdeeProcessRouteStep[]>(() => selectedRoute.value?.steps ?? [])
const kingdeeStepColumns: DataTableColumns<KingdeeProcessRouteStep> = [
{ title: '工序号', key: 'operNumber', width: 70, render: (r) => r.operNumber ?? '-' },
{ title: '工序编码', key: 'processCode', width: 110, ellipsis: { tooltip: true }, render: (r) => r.processCode || '-' },
{ title: '工序名称', key: 'processName', width: 120, ellipsis: { tooltip: true }, render: (r) => r.processName || '-' },
{ title: '工序说明', key: 'operDescription', minWidth: 140, ellipsis: { tooltip: true }, render: (r) => r.operDescription || '-' },
{ title: '工作中心', key: 'workCenterName', width: 110, ellipsis: { tooltip: true }, render: (r) => r.workCenterName || '-' },
{ title: '车间', key: 'departmentName', width: 100, ellipsis: { tooltip: true }, render: (r) => r.departmentName || '-' },
{ title: '控制码', key: 'optCtrlCodeName', width: 100, ellipsis: { tooltip: true }, render: (r) => r.optCtrlCodeName || '-' },
{
title: '标准工时',
key: 'activityQty',
width: 100,
render: (r) => (r.activityQty == null ? '-' : `${r.activityQty}${r.activityUnit ? ' ' + r.activityUnit : ''}`),
},
{ title: '质检', key: 'qualityInspection', width: 56, render: (r) => (r.qualityInspection === 1 ? '是' : '否') },
{ title: '入库', key: 'storageEntry', width: 56, render: (r) => (r.storageEntry === 1 ? '是' : '否') },
]
function createEmptyAssembleRow(operNumber = 10): AssembleRow {
return {
_key: `a-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
processId: null,
operNumber,
processCode: null,
processName: null,
operDescription: null,
sectionId: null,
workshopId: null,
activityUnit: '小时',
optCtrlCodeName: null,
activityQty: null,
qualityInspection: 0,
storageEntry: 0,
procurement: 0,
isOutsource: 0,
}
}
function renumberAssembleRows() {
assembleRows.value.forEach((row, i) => {
row.operNumber = (i + 1) * 10
})
}
function addAssembleRow() {
assembleRows.value.push(createEmptyAssembleRow((assembleRows.value.length + 1) * 10))
}
function removeAssembleRow(key: string) {
if (assembleRows.value.length <= 1) return
assembleRows.value = assembleRows.value.filter((r) => r._key !== key)
renumberAssembleRows()
}
function onAssembleProcessChange(row: AssembleRow, processId: number | null) {
row.processId = processId
if (processId == null) {
row.processCode = null
row.processName = null
row.operDescription = null
row.sectionId = null
row.workshopId = null
row.qualityInspection = 0
row.storageEntry = 0
row.procurement = 0
row.isOutsource = 0
return
}
const basic = processMap.value[processId]
if (!basic) return
row.processCode = basic.code || null
row.processName = basic.name || null
if (!row.operDescription) {
row.operDescription = basic.operDescription || basic.name || null
}
row.sectionId = basic.sectionId ?? null
row.workshopId = basic.workShopId ?? null
row.qualityInspection = basic.qualityInspection ?? 0
row.storageEntry = basic.storageEntry ?? 0
row.procurement = basic.procurement ?? 0
row.isOutsource = basic.outsource ?? 0
}
function onCtrlCodeChange(row: AssembleRow, val: string | null) {
row.optCtrlCodeName = val
if (!val) return
// 含「质量」→ 质检;含「委外」→ 委外;「免检」→ 不质检
row.qualityInspection = val.includes('质量') ? 1 : 0
row.isOutsource = val.includes('委外') ? 1 : 0
}
const assembleColumns = computed<DataTableColumns<AssembleRow>>(() => [
{
title: '工序号',
key: 'operNumber',
width: 64,
render: (row) => row.operNumber ?? '-',
},
{
title: '工序',
key: 'processId',
width: 180,
render: (row) =>
h(NSelect, {
value: row.processId,
size: 'small',
filterable: true,
clearable: true,
options: processOptions.value,
loading: processLoading.value,
placeholder: '选择工序',
style: 'width: 100%',
onUpdateValue: (v: number | null) => onAssembleProcessChange(row, v),
}),
},
{
title: '工序说明',
key: 'operDescription',
width: 150,
render: (row) =>
h(NInput, {
value: row.operDescription ?? '',
size: 'small',
clearable: true,
placeholder: row.processName || '默认工序名称',
onUpdateValue: (v: string) => {
row.operDescription = v
},
}),
},
{
title: '标准工时',
key: 'activityQty',
width: 100,
render: (row) =>
h(NInputNumber, {
value: row.activityQty,
size: 'small',
min: 0,
precision: 3,
style: 'width: 90px',
placeholder: '工时',
onUpdateValue: (v: number | null) => {
row.activityQty = v
},
}),
},
{
title: '单位',
key: 'activityUnit',
width: 100,
render: (row) =>
h(NSelect, {
value: row.activityUnit,
size: 'small',
filterable: true,
tag: true,
options: activityUnitOptions.value,
placeholder: '单位',
style: 'width: 100%',
onUpdateValue: (v: string | null) => {
row.activityUnit = v
},
}),
},
{
title: '控制码',
key: 'optCtrlCodeName',
width: 140,
render: (row) =>
h(NSelect, {
value: row.optCtrlCodeName,
size: 'small',
filterable: true,
clearable: true,
options: ctrlCodeOptions,
placeholder: '选择控制码',
style: 'width: 100%',
onUpdateValue: (v: string | null) => onCtrlCodeChange(row, v),
}),
},
{
title: '质检',
key: 'qualityInspection',
width: 70,
render: (row) =>
h(NSwitch, {
value: row.qualityInspection === 1,
size: 'small',
onUpdateValue: (v: boolean) => {
row.qualityInspection = v ? 1 : 0
},
}),
},
{
title: '入库',
key: 'storageEntry',
width: 70,
render: (row) =>
h(NSwitch, {
value: row.storageEntry === 1,
size: 'small',
onUpdateValue: (v: boolean) => {
row.storageEntry = v ? 1 : 0
},
}),
},
{
title: '操作',
key: 'actions',
width: 64,
fixed: 'right',
render: (row) =>
h(
NButton,
{
text: true,
type: 'error',
size: 'small',
disabled: assembleRows.value.length <= 1,
onClick: () => removeAssembleRow(row._key),
},
{ default: () => '删除' },
),
},
])
const assembleValid = computed(() => {
if (!assembleRows.value.length) return false
return assembleRows.value.every((r) => r.processId != null || !!r.processName || !!r.processCode)
})
const canConfirm = computed(() => {
if (!preview.value?.canSplit || submitting.value) return false
if (mode.value === 'equal') {
// 等量:整单均分,列表本身已含原单保留
if (!equalQuantities.value.length) return false
} else {
if (keepQty.value == null || keepQty.value < unsplittableQty.value) return false
if (splitOutQty.value <= 0) return false
if (customRows.value.length < 1) return false
if (customRows.value.some((v) => v == null || v <= 0)) return false
if (customSum.value !== splitOutQty.value) return false
}
if (routeMode.value === 'kingdee') return !!routingNo.value
return assembleValid.value
})
watch(
() => [props.show, props.planId] as const,
async ([show, planId]) => {
if (!show || planId == null) return
await loadPreview(planId)
await Promise.all([
loadRouteOptions(preview.value?.materialCode),
loadProcessOptions(),
])
},
)
watch(keepQty, () => {
if (mode.value === 'custom') {
customRows.value = [splitOutQty.value > 0 ? splitOutQty.value : null]
}
})
async function loadPreview(planId: number) {
loading.value = true
preview.value = null
try {
preview.value = await orderProcessPlanApi.splitPreview(planId)
const unsplittable = preview.value?.investedQty ?? 0
keepQty.value = unsplittable
// 默认拆成 2 份(原单 + 1 新单);数量够时也可改成更多
equalCount.value = 2
customRows.value = [preview.value?.splittableQty ?? null]
mode.value = 'equal'
routeMode.value = 'kingdee'
routingNo.value = null
assembleRows.value = [createEmptyAssembleRow(10)]
} catch (e: any) {
message.error(e?.message || '加载拆单预览失败')
visible.value = false
} finally {
loading.value = false
}
}
async function loadRouteOptions(materialCode?: string | null) {
routeLoading.value = true
routeList.value = []
routingNo.value = null
try {
if (!materialCode) return
const list = (await processRouteApi.kingdeeSelect(materialCode)) as ProcessRouteSelectOption[]
routeList.value = (Array.isArray(list) ? list : []).filter((r) => !!r?.routingNo)
} catch (e: any) {
routeList.value = []
message.error(e?.message || '加载金蝶工艺路线失败')
} finally {
routeLoading.value = false
}
}
async function loadProcessOptions() {
processLoading.value = true
try {
const page: any = await basicProcessPlanApi.page({ page: 1, pageSize: 500 })
const rows: BasicProcessPlan[] = page?.records ?? page?.list ?? (Array.isArray(page) ? page : [])
const map: Record<number, BasicProcessPlan> = {}
processOptions.value = rows
.filter((r) => r?.id != null)
.map((r) => {
const id = Number(r.id)
map[id] = r
return {
label: `${r.code || ''} ${r.name || ''}`.trim() || String(id),
value: id,
}
})
processMap.value = map
} catch {
processOptions.value = []
processMap.value = {}
} finally {
processLoading.value = false
}
}
function addCustomRow() {
customRows.value.push(null)
}
function removeCustomRow(idx: number) {
if (customRows.value.length <= 1) return
customRows.value.splice(idx, 1)
}
function resetState() {
preview.value = null
mode.value = 'equal'
keepQty.value = 0
equalCount.value = 2
customRows.value = [null]
routeMode.value = 'kingdee'
routingNo.value = null
routeList.value = []
assembleRows.value = [createEmptyAssembleRow(10)]
}
function buildAssembleSteps(): ProcessPlanSplitStep[] {
return assembleRows.value.map((row, i) => {
const name = row.processName || processMap.value[row.processId!]?.name || null
const desc = (row.operDescription || '').trim() || name
return {
processId: row.processId,
operNumber: row.operNumber ?? (i + 1) * 10,
processCode: row.processCode,
processName: name,
operDescription: desc,
sectionId: row.sectionId,
workshopId: row.workshopId,
activityUnit: row.activityUnit,
optCtrlCodeName: row.optCtrlCodeName,
activityQty: row.activityQty,
qualityInspection: row.qualityInspection ?? 0,
storageEntry: row.storageEntry ?? 0,
procurement: row.procurement ?? 0,
isOutsource: row.isOutsource ?? 0,
}
})
}
async function confirmSplit() {
if (!props.planId || !canConfirm.value) return
// 等量:列表已含原单;指定:拼上 keepQty
const quantities =
mode.value === 'equal'
? equalQuantities.value
: [keepQty.value!, ...customRows.value.map((v) => Number(v))]
submitting.value = true
try {
await orderProcessPlanApi.split({
planId: props.planId,
quantities,
routeMode: routeMode.value,
routingNo: routeMode.value === 'kingdee' ? routingNo.value : null,
assembleSteps: routeMode.value === 'assemble' ? buildAssembleSteps() : undefined,
})
message.success('拆单成功')
visible.value = false
emit('success')
} catch (e: any) {
message.error(e?.message || '拆单失败')
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.split-scroll {
max-height: min(72vh, 680px);
overflow-y: auto;
padding-right: 4px;
}
.split-desc {
margin-bottom: 4px;
}
.process-names {
color: #666;
font-size: 12px;
}
.split-section {
margin-top: 10px;
padding-top: 8px;
border-top: 1px dashed #e8e8e8;
}
.split-section-title {
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 13px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.field-label {
font-size: 12px;
color: #666;
margin-bottom: 6px;
}
.field-label-inline {
font-size: 13px;
color: #666;
min-width: 64px;
}
.split-body {
margin-top: 6px;
}
.field-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.hint,
.hint-inline {
font-size: 12px;
color: #999;
}
.hint {
margin-top: 4px;
}
.kingdee-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 12px;
min-height: 0;
}
.kingdee-left,
.kingdee-right {
min-width: 0;
}
.route-card-list {
display: flex;
flex-direction: column;
gap: 6px;
max-height: 280px;
overflow-y: auto;
padding: 1px;
}
.route-card {
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 8px 10px;
cursor: pointer;
background: #fafafa;
transition: border-color 0.15s, background 0.15s;
}
.route-card:hover {
border-color: #93c5fd;
background: #f8fbff;
}
.route-card.active {
border-color: #2080f0;
background: #eff6ff;
}
.route-card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.route-code {
font-weight: 600;
color: #1f2937;
font-size: 12px;
}
.route-step-count {
font-size: 11px;
color: #6b7280;
white-space: nowrap;
}
.route-flow {
margin-top: 4px;
font-size: 11px;
color: #6b7280;
line-height: 1.35;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.route-detail-sub {
margin-left: 6px;
font-weight: 400;
font-size: 12px;
color: #6b7280;
}
.route-detail-empty {
height: 260px;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #e5e7eb;
border-radius: 6px;
color: #9ca3af;
font-size: 12px;
background: #fafafa;
}
.assemble-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.custom-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.sum-line {
margin-top: 6px;
font-size: 12px;
color: #666;
text-align: right;
}
.sum-line.error {
color: #d03050;
}
@media (max-width: 1100px) {
.kingdee-layout {
grid-template-columns: 1fr;
}
.route-card-list {
max-height: 140px;
}
}
</style>