Files
admin-web/src/modules/unified-orders/workflows/components/SmartLoadingWorkbench.vue
T

389 lines
22 KiB
Vue
Raw Normal View History

2026-08-10 15:36:53 +08:00
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
2026-08-10 16:24:54 +08:00
import { Check, Pencil, Plus, RefreshCw, Sparkles, Trash2 } from '@lucide/vue'
2026-08-10 15:36:53 +08:00
import { ElMessage } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
2026-08-10 16:24:54 +08:00
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
2026-08-10 15:36:53 +08:00
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import {
adjustSmartLoadingItem,
assignSmartLoadingItem,
createSmartLoadingPlan,
deleteSmartLoadingItem,
getContainerBooking,
getSmartLoadingPlanByBooking,
listSmartLoadingCandidates,
pushSmartLoadingPreload,
recommendSmartLoading,
type ContainerBooking,
type SmartLoadingCandidate,
type SmartLoadingPlan,
type SmartLoadingPlanItem,
} from '@/modules/unified-orders/workflows/api'
const props = defineProps<{
bookingId: string
highlightInboundId?: string | number | null
readOnly?: boolean
}>()
const emit = defineEmits<{ updated: [] }>()
const access = useAccessPreviewStore()
const container = ref<ContainerBooking | null>(null)
const plan = ref<SmartLoadingPlan | null>(null)
const candidates = ref<SmartLoadingCandidate[]>([])
const loading = ref(false)
const action = ref('')
const loadError = ref<ApiErrorDetails | null>(null)
const actionError = ref<ApiErrorDetails | null>(null)
const conflictRefreshed = ref(false)
const assignOpen = ref(false)
const adjustOpen = ref(false)
const removeOpen = ref(false)
const pushOpen = ref(false)
const selectedCandidate = ref<SmartLoadingCandidate | null>(null)
const selectedItem = ref<SmartLoadingPlanItem | null>(null)
const packageCount = ref(1)
const filters = ref<QueryModel>({ keyword: '', eligibility: '' })
const appliedFilters = ref<QueryModel>({ ...filters.value })
const canView = computed(() => access.can('platform:firstleg:smart-loading:list'))
const canCreate = computed(() => !props.readOnly && access.can('platform:firstleg:smart-loading:create'))
const canAssign = computed(() => !props.readOnly && access.can('platform:firstleg:smart-loading:assign'))
const canDelete = computed(() => !props.readOnly && access.can('platform:firstleg:smart-loading:delete'))
const canPush = computed(() => !props.readOnly && access.can('platform:firstleg:smart-loading:push'))
const editable = computed(() => plan.value?.status === 'EDITING')
const usedVolume = computed(() => plan.value?.items.reduce((sum, item) => sum + item.volumeCbm, 0) ?? 0)
const usedWeight = computed(() => plan.value?.items.reduce((sum, item) => sum + item.grossWeightKg, 0) ?? 0)
const totalPackages = computed(() => plan.value?.items.reduce((sum, item) => sum + item.packageCount, 0) ?? 0)
const volumeRate = computed(() => utilization(usedVolume.value, container.value?.capacityCbm))
const weightRate = computed(() => utilization(usedWeight.value, container.value?.maxWeightKg))
const queryFields: QueryField[] = [
2026-08-14 19:59:32 +08:00
{ key: 'keyword', type: 'input', label: '候选货物', placeholder: '入仓码、客户或品名', props: { 'aria-label': '候选货物关键词' } },
2026-08-10 15:36:53 +08:00
{
key: 'eligibility', type: 'select', label: '可装状态', placeholder: '全部状态',
options: [{ label: '可加入', value: 'ELIGIBLE' }, { label: '存在冲突', value: 'CONFLICT' }],
},
]
const candidateColumns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 92, fixed: 'left', slotName: 'actions' },
{ key: 'priority', label: '推荐顺序', width: 92, slotName: 'priority' },
{ key: 'order', label: '订单/入仓码', minWidth: 180, slotName: 'order' },
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 140 },
{ key: 'goodsName', prop: 'goodsName', label: '品名', minWidth: 160 },
{ key: 'destination', label: '目的仓', minWidth: 150, slotName: 'destination' },
{ key: 'availablePackageCount', prop: 'availablePackageCount', label: '可用箱数', width: 100 },
{ key: 'unitVolumeCbm', label: '单箱体积(m³)', width: 120, slotName: 'unitVolume' },
{ key: 'unitWeightKg', label: '单箱重量(kg)', width: 120, slotName: 'unitWeight' },
{ key: 'explanation', label: '建议与冲突说明', minWidth: 270, slotName: 'explanation' },
]
const itemColumns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 150, fixed: 'left', slotName: 'actions' },
{ key: 'loadSequence', prop: 'loadSequence', label: '装柜顺序', width: 100 },
{ key: 'order', label: '订单/入仓码', minWidth: 180, slotName: 'order' },
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 140 },
{ key: 'productNameZh', prop: 'productNameZh', label: '品名', minWidth: 160 },
{ key: 'skuCode', prop: 'skuCode', label: 'SKU', minWidth: 130 },
{ key: 'packageCount', prop: 'packageCount', label: '箱数', width: 90 },
{ key: 'grossWeightKg', label: '毛重(kg)', width: 110, slotName: 'weight' },
{ key: 'volumeCbm', label: '体积(m³)', width: 110, slotName: 'volume' },
{ key: 'destination', label: '目的仓', minWidth: 150, slotName: 'destination' },
]
function utilization(used: number, capacity: number | null | undefined) {
return capacity && capacity > 0 ? Math.round(used / capacity * 1000) / 10 : null
}
function formatNumber(value: number, precision = 3) {
return Number.isFinite(value) ? value.toLocaleString('zh-CN', { maximumFractionDigits: precision }) : '—'
}
2026-08-11 18:43:56 +08:00
function planStatusLabel(status?: string | null) {
return ({ EDITING: '预装柜中', LOADING: '拣货装柜中', COMPLETED: '装柜完成' } as Record<string, string>)[status || '']
|| (status ? '未知状态' : '尚未创建')
}
2026-08-10 15:36:53 +08:00
function routeMatches(candidate: SmartLoadingCandidate) {
const target = container.value?.destinationWarehouseCode
return !target || candidate.destinationWarehouseCode === target
}
function fittingCount(candidate: SmartLoadingCandidate) {
if (!routeMatches(candidate) || candidate.availablePackageCount <= 0) return 0
const volumeLeft = (container.value?.capacityCbm ?? 0) - usedVolume.value
const weightLeft = (container.value?.maxWeightKg ?? 0) - usedWeight.value
if (!container.value?.capacityCbm || !container.value.maxWeightKg) return 0
const byVolume = candidate.unitVolumeCbm > 0 ? Math.floor(Math.max(volumeLeft, 0) / candidate.unitVolumeCbm) : candidate.availablePackageCount
const byWeight = candidate.unitWeightKg > 0 ? Math.floor(Math.max(weightLeft, 0) / candidate.unitWeightKg) : candidate.availablePackageCount
return Math.max(0, Math.min(candidate.availablePackageCount, byVolume, byWeight))
}
function candidateExplanation(candidate: SmartLoadingCandidate) {
if (!routeMatches(candidate)) return `不可装:目的仓与柜次 ${container.value?.destinationWarehouseName || container.value?.destinationWarehouseCode || '目的仓'} 不一致`
if (!container.value?.capacityCbm || !container.value.maxWeightKg) return '不可装:柜次容量或最大承重尚未配置'
const count = fittingCount(candidate)
if (count <= 0) return '不可装:剩余容积或承重不足'
return `可装 ${count} 箱;同仓同目的仓,按入库完成时间先进先出推荐`
}
const filteredCandidates = computed(() => {
const keyword = String(appliedFilters.value.keyword || '').trim().toLowerCase()
const eligibility = String(appliedFilters.value.eligibility || '')
return candidates.value.filter((candidate) => {
2026-08-14 19:59:32 +08:00
const searchable = [candidate.inboundCode, candidate.customerName, candidate.goodsName].join(' ').toLowerCase()
2026-08-10 15:36:53 +08:00
if (keyword && !searchable.includes(keyword)) return false
const eligible = fittingCount(candidate) > 0
return !eligibility || (eligibility === 'ELIGIBLE' ? eligible : !eligible)
})
})
function isConflict(error: unknown) {
const details = getApiErrorDetails(error)
return error instanceof ApiResponseError && error.code === 'A0409'
|| details.code === 'A0409'
|| /版本|并发|已被其他人修改/.test(details.message)
}
async function load() {
if (!canView.value || !props.bookingId) return
loading.value = true
loadError.value = null
try {
const nextContainer = await getContainerBooking(props.bookingId)
container.value = nextContainer
try {
plan.value = await getSmartLoadingPlanByBooking(props.bookingId)
} catch (error) {
const details = getApiErrorDetails(error)
if (error instanceof ApiResponseError && ['A0404', 'NOT_FOUND'].includes(error.code)) plan.value = null
else throw Object.assign(error instanceof Error ? error : new Error(details.message), { details })
}
candidates.value = nextContainer.warehouseId ? await listSmartLoadingCandidates(nextContainer.id, nextContainer.warehouseId) : []
2026-08-10 15:36:53 +08:00
} catch (error) {
container.value = null
plan.value = null
candidates.value = []
loadError.value = getApiErrorDetails(error, '智能排柜数据加载失败,请稍后重试')
} finally {
loading.value = false
}
}
async function runAction(name: string, operation: () => Promise<SmartLoadingPlan>, success: string) {
action.value = name
actionError.value = null
conflictRefreshed.value = false
try {
plan.value = await operation()
ElMessage.success(success)
emit('updated')
if (container.value?.warehouseId) candidates.value = await listSmartLoadingCandidates(container.value.id, container.value.warehouseId)
2026-08-10 15:36:53 +08:00
return true
} catch (error) {
actionError.value = getApiErrorDetails(error, `${success}失败,请重试`)
if (isConflict(error)) {
await load()
conflictRefreshed.value = true
}
return false
} finally {
action.value = ''
}
}
async function createPlan() {
await runAction('create', () => createSmartLoadingPlan(props.bookingId), '装柜草稿已创建')
}
async function recommend() {
if (!plan.value) return
await runAction('recommend', () => recommendSmartLoading(plan.value!.id, plan.value!.version), '智能建议已生成')
}
function openAssign(candidate: SmartLoadingCandidate) {
selectedCandidate.value = candidate
packageCount.value = Math.max(1, fittingCount(candidate))
assignOpen.value = true
}
async function confirmAssign() {
if (!plan.value || !selectedCandidate.value) return
const candidate = selectedCandidate.value
const success = await runAction('assign', () => assignSmartLoadingItem(plan.value!.id, {
inboundId: candidate.inboundId,
inboundDetailId: candidate.inboundDetailId,
packageCount: packageCount.value,
expectedVersion: plan.value!.version,
}), '候选货物已加入装柜计划')
if (success) assignOpen.value = false
}
function openAdjust(item: SmartLoadingPlanItem) {
selectedItem.value = item
packageCount.value = item.packageCount
adjustOpen.value = true
}
async function confirmAdjust() {
if (!plan.value || !selectedItem.value) return
const item = selectedItem.value
const success = await runAction('adjust', () => adjustSmartLoadingItem(plan.value!.id, item.id, {
packageCount: packageCount.value,
expectedPlanVersion: plan.value!.version,
expectedItemVersion: item.version,
}), '装柜箱数已调整')
if (success) adjustOpen.value = false
}
function openRemove(item: SmartLoadingPlanItem) {
selectedItem.value = item
removeOpen.value = true
}
async function confirmRemove() {
if (!plan.value || !selectedItem.value) return
const item = selectedItem.value
const success = await runAction('remove', () => deleteSmartLoadingItem(plan.value!.id, item.id, {
expectedPlanVersion: plan.value!.version,
expectedItemVersion: item.version,
}), '装柜明细已移除')
if (success) removeOpen.value = false
}
async function confirmPush() {
if (!plan.value) return
const success = await runAction('push', () => pushSmartLoadingPreload(plan.value!.id, plan.value!.version), '装柜计划已推送预装')
if (success) pushOpen.value = false
}
function applyFilters(model: QueryModel) {
appliedFilters.value = { keyword: model.keyword || '', eligibility: model.eligibility || '' }
}
watch(() => [props.bookingId, access.permissionSignature], () => { void load() })
onMounted(() => { void load() })
</script>
<template>
2026-08-10 16:24:54 +08:00
<section class="smart-loading" aria-label="智能排柜工作台">
2026-08-10 15:36:53 +08:00
<header class="smart-loading__heading">
2026-08-10 16:24:54 +08:00
<div class="smart-loading__context"><span>当前柜次</span><strong>{{ container?.cabinetNo || container?.containerNo || `柜次 ${bookingId}` }}</strong><small>{{ container?.warehouseName || '仓库待加载' }}</small></div>
2026-08-10 15:36:53 +08:00
<div class="smart-loading__actions">
<el-button circle :loading="loading" aria-label="刷新智能排柜" @click="load"><RefreshCw :size="16" /></el-button>
<el-button v-if="!plan" type="primary" :disabled="!canCreate || loading" :loading="action === 'create'" @click="createPlan"><Plus :size="15" />创建装柜草稿</el-button>
<el-button v-if="plan" :disabled="!canAssign || !editable || Boolean(action)" :loading="action === 'recommend'" @click="recommend"><Sparkles :size="15" />生成智能建议</el-button>
2026-08-14 16:06:30 +08:00
<el-button v-if="plan" type="primary" :disabled="!canPush || !editable || !plan.items.length || Boolean(action)" @click="pushOpen = true"><Check :size="15" />推送预装</el-button>
2026-08-10 15:36:53 +08:00
</div>
</header>
<el-alert v-if="!canView" type="warning" :closable="false" show-icon>当前账号无智能排柜查询权限。</el-alert>
<LoadErrorState v-else-if="loadError" title="智能排柜暂时无法加载" :message="loadError.message" :loading="loading" @retry="load">
<template #details><ApiErrorMeta :details="loadError" /></template>
</LoadErrorState>
<template v-else>
<el-alert v-if="actionError" class="smart-loading__error" type="error" :closable="false" show-icon>
<template #title>{{ actionError.message }}</template>
<p v-if="conflictRefreshed">系统已刷新最新计划,当前输入仍保留,请核对后重新提交。</p>
<ApiErrorMeta :details="actionError" />
</el-alert>
<div v-if="container" class="smart-loading__summary">
2026-08-11 18:43:56 +08:00
<div><span>计划状态</span><strong>{{ planStatusLabel(plan?.status) }}</strong></div>
2026-08-10 15:36:53 +08:00
<div><span>已装箱数</span><strong>{{ totalPackages }}</strong></div>
<div><span>容积</span><strong>{{ formatNumber(usedVolume) }} / {{ formatNumber(container.capacityCbm || 0) }} m³</strong><small>{{ volumeRate == null ? '容量待配置' : `${volumeRate}%` }}</small></div>
<div><span>重量</span><strong>{{ formatNumber(usedWeight) }} / {{ formatNumber(container.maxWeightKg || 0) }} kg</strong><small>{{ weightRate == null ? '承重待配置' : `${weightRate}%` }}</small></div>
</div>
2026-08-10 16:24:54 +08:00
<section v-if="plan" class="smart-loading__section surface-panel table-panel">
2026-08-10 15:36:53 +08:00
<div class="smart-loading__section-title"><h4>当前装柜方案</h4><span>版本 {{ plan.version }} · {{ plan.planNo || '—' }}</span></div>
<DataTable :data="plan.items" :columns="itemColumns" :loading="loading" empty-text="装柜草稿暂无明细">
<template #actions="{ row }">
2026-08-12 18:16:44 +08:00
<el-button size="small" plain type="primary" :disabled="!canAssign || !editable || Boolean(action)" @click="openAdjust(row as SmartLoadingPlanItem)"><Pencil :size="14" />调整</el-button>
<el-button size="small" plain type="danger" :disabled="!canDelete || !editable || Boolean(action)" @click="openRemove(row as SmartLoadingPlanItem)"><Trash2 :size="13" />移除</el-button>
2026-08-10 15:36:53 +08:00
</template>
2026-08-14 19:59:32 +08:00
<template #order="{ row }"><strong>{{ (row as SmartLoadingPlanItem).inboundCode || '—' }}</strong></template>
2026-08-10 15:36:53 +08:00
<template #weight="{ row }">{{ formatNumber((row as SmartLoadingPlanItem).grossWeightKg) }}</template>
<template #volume="{ row }">{{ formatNumber((row as SmartLoadingPlanItem).volumeCbm) }}</template>
<template #destination="{ row }">{{ (row as SmartLoadingPlanItem).destinationWarehouseName || (row as SmartLoadingPlanItem).destinationWarehouseCode || '—' }}</template>
</DataTable>
</section>
<section class="smart-loading__section">
<div class="smart-loading__section-title"><h4>候选货物</h4><span>后端按入库完成时间排序</span></div>
2026-08-10 16:24:54 +08:00
<ListPageLayout ariaLabel="智能排柜候选货物">
2026-08-10 18:12:23 +08:00
<template #toolbar><QueryToolbar v-model="filters" ariaLabel="候选货物筛选" :fields="queryFields" @search="applyFilters" /></template>
2026-08-10 16:24:54 +08:00
<section class="surface-panel table-panel">
2026-08-10 15:36:53 +08:00
<DataTable :data="filteredCandidates" :columns="candidateColumns" :loading="loading" empty-text="当前柜次仓库暂无可用候选货物">
2026-08-12 18:16:44 +08:00
<template #actions="{ row }"><el-button size="small" plain type="primary" :disabled="!plan || !canAssign || !editable || fittingCount(row as SmartLoadingCandidate) <= 0 || Boolean(action)" @click="openAssign(row as SmartLoadingCandidate)"><Plus :size="14" />加入</el-button></template>
2026-08-10 15:36:53 +08:00
<template #priority="{ $index }"><span class="priority">{{ Number($index) + 1 }}</span></template>
2026-08-14 19:59:32 +08:00
<template #order="{ row }"><strong>{{ (row as SmartLoadingCandidate).inboundCode || '—' }}</strong></template>
2026-08-10 15:36:53 +08:00
<template #destination="{ row }">{{ (row as SmartLoadingCandidate).destinationWarehouseName || (row as SmartLoadingCandidate).destinationWarehouseCode || '—' }}</template>
<template #unitVolume="{ row }">{{ formatNumber((row as SmartLoadingCandidate).unitVolumeCbm) }}</template>
<template #unitWeight="{ row }">{{ formatNumber((row as SmartLoadingCandidate).unitWeightKg) }}</template>
<template #explanation="{ row }"><el-tag size="small" :type="fittingCount(row as SmartLoadingCandidate) > 0 ? 'success' : 'danger'">{{ fittingCount(row as SmartLoadingCandidate) > 0 ? '可加入' : '冲突' }}</el-tag><span>{{ candidateExplanation(row as SmartLoadingCandidate) }}</span></template>
</DataTable>
2026-08-10 16:24:54 +08:00
</section>
</ListPageLayout>
2026-08-10 15:36:53 +08:00
</section>
</template>
<AppDialog v-model="assignOpen" title="加入装柜方案" :confirm-loading="action === 'assign'" :confirm-disabled="packageCount < 1 || packageCount > (selectedCandidate ? fittingCount(selectedCandidate) : 0)" confirm-text="确认加入" @confirm="confirmAssign">
2026-08-14 19:59:32 +08:00
<p>{{ selectedCandidate?.inboundCode || '入仓码暂缺' }} · 最多可加入 {{ selectedCandidate ? fittingCount(selectedCandidate) : 0 }} 箱</p>
2026-08-10 15:36:53 +08:00
<el-input-number v-model="packageCount" :min="1" :max="selectedCandidate ? fittingCount(selectedCandidate) : 1" controls-position="right" aria-label="加入箱数" />
<ApiErrorMeta v-if="actionError" :details="actionError" />
</AppDialog>
<AppDialog v-model="adjustOpen" title="调整装柜箱数" :confirm-loading="action === 'adjust'" :confirm-disabled="packageCount < 1" confirm-text="保存调整" @confirm="confirmAdjust">
2026-08-14 19:59:32 +08:00
<p>{{ selectedItem?.inboundCode || '入仓码暂缺' }} · 当前 {{ selectedItem?.packageCount || 0 }} 箱</p>
2026-08-10 15:36:53 +08:00
<el-input-number v-model="packageCount" :min="1" controls-position="right" aria-label="调整后箱数" />
<ApiErrorMeta v-if="actionError" :details="actionError" />
</AppDialog>
<AppDialog v-model="removeOpen" title="移除装柜明细" confirm-type="danger" confirm-text="确认移除" :confirm-loading="action === 'remove'" @confirm="confirmRemove">
2026-08-14 19:59:32 +08:00
<p>将从当前草稿移除 {{ selectedItem?.inboundCode || '该入仓单' }} 的 {{ selectedItem?.packageCount || 0 }} 箱货物。此操作会写入审计历史。</p>
2026-08-10 15:36:53 +08:00
<ApiErrorMeta v-if="actionError" :details="actionError" />
</AppDialog>
<AppDialog v-model="pushOpen" title="推送预装清单" confirm-type="primary" confirm-text="确认推送" :confirm-loading="action === 'push'" @confirm="confirmPush">
2026-08-14 16:06:30 +08:00
<p>推送后当前草稿明细不可再增删调整。真实集装箱号可在订舱放舱后补录,现场 PDA 任务将在条件齐备后开放。</p>
2026-08-10 15:36:53 +08:00
<ApiErrorMeta v-if="actionError" :details="actionError" />
</AppDialog>
</section>
</template>
<style scoped>
.smart-loading { display: grid; gap: 14px; }
.smart-loading__heading, .smart-loading__actions, .smart-loading__section-title { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
.smart-loading__heading, .smart-loading__section-title { justify-content: space-between; }
2026-08-10 16:24:54 +08:00
.smart-loading__section-title h4 { margin: 0; font-size: 15px; }
.smart-loading__context { display: grid; min-width: 0; gap: 3px; }
.smart-loading__context span { color: var(--cc-text-secondary, #64748b); font-size: 11px; }
.smart-loading__context strong { overflow: hidden; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
.smart-loading__section-title span, .smart-loading small { display: block; margin: 3px 0 0; color: var(--cc-text-secondary, #64748b); font-size: 12px; }
2026-08-10 15:36:53 +08:00
.smart-loading__summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
.smart-loading__summary > div { min-width: 0; padding: 10px; border: 1px solid var(--cc-border); border-radius: 5px; background: var(--el-fill-color-extra-light); }
.smart-loading__summary span { display: block; margin-bottom: 4px; color: var(--cc-text-secondary, #64748b); font-size: 12px; }
.smart-loading__summary strong { display: block; overflow-wrap: anywhere; font-size: 13px; }
2026-08-10 16:24:54 +08:00
.smart-loading__section { display: grid; min-width: 0; gap: 10px; padding-top: 4px; overflow: hidden; }
2026-08-10 15:36:53 +08:00
.smart-loading__error p { margin: 6px 0 0; }
.smart-loading :deep(.el-table__cell small), .smart-loading :deep(.el-table__cell strong) { display: block; }
.smart-loading :deep(.el-table__cell small) { margin-top: 2px; color: var(--cc-text-secondary, #64748b); }
.smart-loading :deep(.el-table__cell .el-tag + span) { margin-left: 6px; }
.priority { display: inline-grid; width: 26px; height: 26px; place-items: center; border-radius: 50%; background: var(--el-color-primary-light-9); color: var(--el-color-primary); font-weight: 700; }
@media (max-width: 760px) {
.smart-loading__summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.smart-loading__actions { width: 100%; }
.smart-loading__actions :deep(.el-button:not(.is-circle)) { flex: 1 1 150px; margin-left: 0; }
}
@media (max-width: 420px) { .smart-loading__summary { grid-template-columns: 1fr; } }
2026-08-12 18:16:44 +08:00
@media (max-width: 360px) { .smart-loading :deep(.el-table__cell .el-button) { width: 100%; min-height: 40px; } }
2026-08-10 15:36:53 +08:00
</style>