Files
admin-web/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue
T

438 lines
19 KiB
Vue
Raw Normal View History

2026-08-04 21:01:39 +08:00
<script setup lang="ts">
2026-10-20 15:48:23 +08:00
import InlineHelp from '@/shared/components/InlineHelp.vue'
2026-09-15 09:58:15 +08:00
import '@/styles/adminDensity.css'
2026-10-20 23:34:11 +08:00
import '@/styles/adminWorkbench.css'
2026-08-07 14:47:11 +08:00
import { computed, onMounted, ref, watch } from 'vue'
2026-09-15 09:58:15 +08:00
import { FileSpreadsheet, FileUp } from '@lucide/vue'
2026-08-07 14:47:11 +08:00
import { ElMessage, ElMessageBox } from 'element-plus'
2026-09-11 18:44:01 +08:00
import { useRouter } from 'vue-router'
2026-08-04 21:01:39 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
2026-09-08 17:43:04 +08:00
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
2026-08-04 21:01:39 +08:00
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
2026-08-10 16:24:54 +08:00
import AdminPage from '@/shared/components/AdminPage.vue'
2026-08-07 14:47:11 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-08-04 21:01:39 +08:00
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-04 21:01:39 +08:00
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import {
2026-08-07 14:47:11 +08:00
downloadPlatformPriceBookSource,
listPlatformPriceBooks,
uploadPlatformPriceBook,
2026-09-11 18:44:01 +08:00
voidPlatformPriceBook,
2026-08-07 14:47:11 +08:00
type PlatformPriceBookVersion,
type PriceBookStatus,
} from '../api/platformPriceBooks'
2026-08-04 21:01:39 +08:00
2026-08-07 14:47:11 +08:00
type PriceBookFilters = QueryModel & {
2026-09-08 17:43:04 +08:00
customerId: string
2026-08-07 14:47:11 +08:00
status: PriceBookStatus | ''
2026-08-04 21:01:39 +08:00
}
const access = useAccessPreviewStore()
2026-09-11 18:44:01 +08:00
const router = useRouter()
2026-08-07 14:47:11 +08:00
const rows = ref<PlatformPriceBookVersion[]>([])
2026-08-04 21:01:39 +08:00
const page = ref(1)
const pageSize = ref(20)
2026-08-07 14:47:11 +08:00
const loading = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const uploadOpen = ref(false)
const uploadFile = ref<File | null>(null)
const uploadFileError = ref('')
2026-09-08 17:43:04 +08:00
const uploadCustomerId = ref('0')
2026-08-07 14:47:11 +08:00
const uploadInput = ref<HTMLInputElement>()
const uploading = ref(false)
const downloadingIds = ref(new Set<string>())
2026-09-11 18:44:01 +08:00
const voidingIds = ref(new Set<string>())
2026-09-08 17:43:04 +08:00
const customerOptions = ref<PlatformTenantResource[]>([])
const customersLoading = ref(false)
const filters = ref<PriceBookFilters>({ customerId: '0', status: '' })
2026-08-07 14:47:11 +08:00
const appliedFilters = ref<PriceBookFilters>({ ...filters.value })
2026-08-04 21:01:39 +08:00
let loadSequence = 0
2026-09-08 17:43:04 +08:00
const queryFields = computed<QueryField[]>(() => [
{
key: 'customerId',
type: 'select',
width: 260,
label: '报价范围',
placeholder: '请选择报价范围',
props: { 'aria-label': '报价范围', filterable: true, loading: customersLoading.value },
options: [
{ label: '平台公共报价', value: '0' },
...customerOptions.value.map((customer) => ({ label: `客户专属 · ${customer.tenantName || '未命名客户'}`, value: String(customer.id) })),
],
},
2026-08-04 21:01:39 +08:00
{
2026-08-07 14:47:11 +08:00
key: 'status',
type: 'select',
width: 190,
label: '版本状态',
placeholder: '全部版本状态',
props: { 'aria-label': '版本状态' },
options: [
{ label: '待确认', value: 'PENDING_CONFIRMATION' },
2026-08-04 21:01:39 +08:00
{ label: '生效中', value: 'ACTIVE' },
2026-08-07 14:47:11 +08:00
{ label: '已替换', value: 'SUPERSEDED' },
2026-09-12 11:45:57 +08:00
{ label: '已作废', value: 'VOIDED' },
2026-08-07 14:47:11 +08:00
{ label: '解析失败', value: 'FAILED' },
2026-08-04 21:01:39 +08:00
],
},
2026-09-08 17:43:04 +08:00
])
2026-08-04 21:01:39 +08:00
2026-08-07 14:47:11 +08:00
const columns: DataTableColumn[] = [
2026-09-12 11:45:57 +08:00
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
2026-09-08 17:43:04 +08:00
{ key: 'scope', label: '报价范围', minWidth: 190, slotName: 'scope' },
2026-08-07 14:47:11 +08:00
{ key: 'version', label: '价格表版本', minWidth: 180, slotName: 'version' },
2026-08-12 12:19:02 +08:00
{ key: 'originalFileName', prop: 'originalFileName', label: '原始文件', minWidth: 250, showOverflowTooltip: true, defaultVisible: false },
2026-08-07 14:47:11 +08:00
{ key: 'status', label: '状态', width: 110, slotName: 'status' },
{ key: 'contents', label: '解析结果', minWidth: 155, slotName: 'contents' },
{ key: 'issues', label: '校验结果', minWidth: 150, slotName: 'issues' },
{ key: 'createdAt', label: '上传时间', minWidth: 165, formatter: (row: PlatformPriceBookVersion) => formatDateTime(row.createdAt) },
{ key: 'effectivePeriod', label: '实际生效区间', minWidth: 290, formatter: (row: PlatformPriceBookVersion) => formatEffectivePeriod(row) },
]
const canEdit = computed(() => access.can('firstleg:fba-pricing:rate:edit'))
const pagedRows = computed(() => rows.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value))
2026-08-04 21:01:39 +08:00
const activeCount = computed(() => rows.value.filter((row) => row.status === 'ACTIVE').length)
2026-08-07 14:47:11 +08:00
const pendingCount = computed(() => rows.value.filter((row) => row.status === 'PENDING_CONFIRMATION').length)
2026-08-04 21:01:39 +08:00
const loadErrorMessage = computed(() => loadError.value?.message || '')
function formatDateTime(value: string | null | undefined) {
return value ? value.replace('T', ' ').slice(0, 16) : '-'
}
2026-08-07 14:47:11 +08:00
function formatEffectivePeriod(version: PlatformPriceBookVersion) {
if (!version.publishedAt) return '尚未生效'
const start = formatDateTime(version.publishedAt)
return version.supersededAt ? `${start} 至 ${formatDateTime(version.supersededAt)}` : `${start} 起`
}
2026-09-08 17:43:04 +08:00
function priceBookScopeLabel(customerId: string | null | undefined) {
if (!customerId || customerId === '0') return '平台公共报价'
const customer = customerOptions.value.find((item) => String(item.id) === String(customerId))
return customer ? `客户专属 · ${customer.tenantName || '未命名客户'}` : '客户专属报价'
}
async function loadCustomerOptions() {
customersLoading.value = true
try {
const result = await listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })
customerOptions.value = result.records.filter((customer) => customer.status === 'ACTIVE')
} catch {
customerOptions.value = []
ElMessage.warning('客户列表加载失败,仍可维护平台公共报价')
} finally {
customersLoading.value = false
}
}
2026-08-07 14:47:11 +08:00
function priceBookStatusMeta(status: PriceBookStatus) {
const values = {
PENDING_CONFIRMATION: { label: '待确认', type: 'warning' as const },
ACTIVE: { label: '生效中', type: 'success' as const },
SUPERSEDED: { label: '已替换', type: 'info' as const },
2026-09-12 11:45:57 +08:00
VOIDED: { label: '已作废', type: 'info' as const },
2026-08-07 14:47:11 +08:00
FAILED: { label: '解析失败', type: 'danger' as const },
}
return values[status]
2026-08-04 21:01:39 +08:00
}
2026-08-07 14:47:11 +08:00
function apiErrorMessage(error: unknown, fallback: string) {
const details = getApiErrorDetails(error, fallback)
return details.requestId ? `${details.message}(requestId: ${details.requestId})` : details.message
}
function safeFileName(value: string) {
const safe = value.replace(/[\\/:*?"<>|\u0000-\u001f]/g, '_').trim()
return safe || '船期价格表.xlsx'
}
function saveBlob(blob: Blob, fileName: string) {
const url = URL.createObjectURL(blob)
const anchor = document.createElement('a')
try {
anchor.href = url
anchor.download = safeFileName(fileName)
document.body.append(anchor)
anchor.click()
} finally {
anchor.remove()
window.setTimeout(() => URL.revokeObjectURL(url), 0)
}
}
async function loadPriceBooks() {
2026-08-04 21:01:39 +08:00
const sequence = ++loadSequence
loading.value = true
loadError.value = null
try {
2026-09-08 17:43:04 +08:00
const result = await listPlatformPriceBooks({ customerId: appliedFilters.value.customerId, status: appliedFilters.value.status })
2026-08-04 21:01:39 +08:00
if (sequence !== loadSequence) return
2026-08-07 14:47:11 +08:00
rows.value = result
2026-08-04 21:01:39 +08:00
} catch (error) {
if (sequence !== loadSequence) return
rows.value = []
2026-08-07 14:47:11 +08:00
loadError.value = getApiErrorDetails(error, '船期价格表版本加载失败,请稍后重试')
2026-08-04 21:01:39 +08:00
} finally {
2026-08-07 14:47:11 +08:00
if (sequence === loadSequence) loading.value = false
2026-08-04 21:01:39 +08:00
}
}
async function applyQuery(model: QueryModel) {
2026-09-08 17:43:04 +08:00
appliedFilters.value = {
customerId: String(model.customerId || '0'),
status: (model.status || '') as PriceBookStatus | '',
}
2026-08-04 21:01:39 +08:00
page.value = 1
2026-08-07 14:47:11 +08:00
await loadPriceBooks()
2026-08-04 21:01:39 +08:00
}
2026-08-07 14:47:11 +08:00
function resetUpload() {
uploadFile.value = null
uploadFileError.value = ''
if (uploadInput.value) uploadInput.value.value = ''
2026-08-04 21:01:39 +08:00
}
2026-08-07 14:47:11 +08:00
function openUpload() {
resetUpload()
2026-09-08 17:43:04 +08:00
uploadCustomerId.value = appliedFilters.value.customerId || '0'
2026-08-07 14:47:11 +08:00
uploadOpen.value = true
2026-08-04 21:01:39 +08:00
}
2026-08-07 14:47:11 +08:00
function selectUploadFile(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0] || null
uploadFile.value = null
uploadFileError.value = ''
if (!file) return
if (!file.name.toLowerCase().endsWith('.xlsx')) {
uploadFileError.value = '仅支持 XLSX 格式的船期价格表。'
input.value = ''
2026-08-04 21:01:39 +08:00
return
}
2026-08-07 14:47:11 +08:00
if (file.size > 10 * 1024 * 1024) {
uploadFileError.value = '价格表文件不能超过 10MB。'
input.value = ''
return
}
uploadFile.value = file
}
2026-09-12 11:45:57 +08:00
function openDetail(row: PlatformPriceBookVersion) {
void router.push({ name: 'fba-carrier-rate-detail', params: { versionId: row.id } })
2026-08-07 14:47:11 +08:00
}
async function submitUpload() {
const file = uploadFile.value
if (!file) {
ElMessage.error('请选择需要上传的 XLSX 价格表。')
return
}
uploading.value = true
try {
2026-09-08 17:43:04 +08:00
const result = await uploadPlatformPriceBook(file, uploadCustomerId.value)
2026-08-07 14:47:11 +08:00
uploadOpen.value = false
resetUpload()
await loadPriceBooks()
2026-09-12 11:45:57 +08:00
ElMessage.success('价格表已解析,请在详情页核对结果并确认仓库映射。')
await router.push({ name: 'fba-carrier-rate-detail', params: { versionId: result.version.id } })
2026-08-07 14:47:11 +08:00
} catch (error) {
ElMessage.error(apiErrorMessage(error, '价格表上传或解析失败,请检查文件后重试'))
await loadPriceBooks()
} finally {
uploading.value = false
}
}
async function downloadSource(row: PlatformPriceBookVersion) {
const next = new Set(downloadingIds.value)
next.add(row.id)
downloadingIds.value = next
try {
const blob = await downloadPlatformPriceBookSource(row.id)
if (!blob.size) throw new Error('原始价格表内容为空')
saveBlob(blob, row.originalFileName)
} catch (error) {
ElMessage.error(apiErrorMessage(error, '原始价格表下载失败,请稍后重试'))
} finally {
const current = new Set(downloadingIds.value)
current.delete(row.id)
downloadingIds.value = current
}
2026-08-04 21:01:39 +08:00
}
2026-09-12 11:45:57 +08:00
async function voidPending(row: PlatformPriceBookVersion) {
if (row.status !== 'PENDING_CONFIRMATION' || !canEdit.value) return
let reason = ''
try {
const result = await ElMessageBox.prompt(
`作废后,版本 V${row.versionNo} 将不能再确认生效;版本记录和原始文件会继续保留。`,
`作废待确认价格表 · V${row.versionNo} · ${priceBookScopeLabel(row.customerId)}`,
2026-09-12 11:45:57 +08:00
{
2026-09-15 09:58:15 +08:00
customClass: 'admin-density-confirm',
2026-09-12 11:45:57 +08:00
type: 'warning',
confirmButtonText: '确认作废',
cancelButtonText: '取消',
inputPlaceholder: '请输入作废原因',
inputValidator: (value) => value.trim() ? true : '请输入作废原因',
inputPattern: /^.{1,500}$/s,
inputErrorMessage: '作废原因不能超过 500 个字符',
},
)
reason = result.value.trim()
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error('作废操作未完成,请重试。')
return
}
const next = new Set(voidingIds.value)
next.add(row.id)
voidingIds.value = next
try {
await voidPlatformPriceBook(row.id, reason)
ElMessage.success(`价格表 V${row.versionNo} 已作废`)
await loadPriceBooks()
} catch (error) {
ElMessage.error(apiErrorMessage(error, '价格表作废失败,请稍后重试'))
} finally {
const current = new Set(voidingIds.value)
current.delete(row.id)
voidingIds.value = current
}
}
2026-08-07 14:47:11 +08:00
function detailActionLabel(row: PlatformPriceBookVersion) {
return row.status === 'PENDING_CONFIRMATION' && canEdit.value ? '查看并确认' : '查看详情'
}
watch(uploadOpen, (open) => {
if (!open && !uploading.value) resetUpload()
})
2026-08-04 21:01:39 +08:00
onMounted(() => {
2026-09-08 17:43:04 +08:00
void loadCustomerOptions()
2026-08-07 14:47:11 +08:00
void loadPriceBooks()
2026-08-04 21:01:39 +08:00
})
</script>
<template>
2026-10-20 23:34:11 +08:00
<AdminPage as="div" class="price-book-page admin-density admin-workbench" spacing="normal">
2026-08-10 16:24:54 +08:00
<ListPageLayout aria-label="承运商报价版本管理">
2026-08-04 21:01:39 +08:00
2026-09-30 14:01:58 +08:00
2026-10-20 23:34:11 +08:00
<section aria-live="polite"><div class="workbench-table is-page-surface"><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!loadError"
2026-08-04 21:01:39 +08:00
v-model:page="page"
v-model:page-size="pageSize"
:data="pagedRows"
:columns="columns"
2026-08-07 14:47:11 +08:00
preference-key="price-book-versions-table"
2026-08-04 21:01:39 +08:00
:loading="loading"
2026-08-07 14:47:11 +08:00
:total="rows.length"
2026-08-04 21:01:39 +08:00
:page-sizes="[10, 20, 50]"
row-key="id"
2026-08-12 18:00:26 +08:00
border
2026-08-07 14:47:11 +08:00
aria-label="船期价格表版本列表"
empty-text="暂无船期价格表版本"
2026-08-04 21:01:39 +08:00
>
2026-09-30 14:01:58 +08:00
<template #toolbar-start><h1>船期价格表</h1></template>
<template #filters>
<QueryToolbar
v-model="filters"
ariaLabel="船期价格表筛选"
:fields="queryFields"
preference-key="price-book-query"
@search="applyQuery"
/>
</template>
<template #notices><template v-if="loadError">
<LoadErrorState
title="船期价格表版本加载失败"
:message="loadErrorMessage"
:loading="loading"
@retry="loadPriceBooks()"
>
<template #details><ApiErrorMeta :details="loadError" /></template>
</LoadErrorState>
</template></template>
2026-08-10 16:24:54 +08:00
<template #toolbar-actions><el-button v-if="canEdit" type="primary" @click="openUpload"><FileUp :size="16" />上传价格表</el-button></template>
2026-09-30 14:01:58 +08:00
<template #actions="{ row }"><div class="table-actions density-actions row-actions admin-text-actions">
2026-09-15 09:58:15 +08:00
<el-button data-tone="neutral" class="density-action" size="small" @click="openDetail(row as PlatformPriceBookVersion)">{{ detailActionLabel(row as PlatformPriceBookVersion) }}</el-button>
<el-button data-tone="neutral" class="density-action"
2026-08-12 18:00:26 +08:00
size="small"
2026-09-15 09:58:15 +08:00
2026-08-07 14:47:11 +08:00
:loading="downloadingIds.has((row as PlatformPriceBookVersion).id)"
@click="downloadSource(row as PlatformPriceBookVersion)"
2026-09-15 09:58:15 +08:00
>原文件</el-button>
<el-button data-tone="danger" class="density-action"
2026-09-12 11:45:57 +08:00
v-if="canEdit && (row as PlatformPriceBookVersion).status === 'PENDING_CONFIRMATION'"
size="small"
2026-09-15 09:58:15 +08:00
2026-09-12 11:45:57 +08:00
:loading="voidingIds.has((row as PlatformPriceBookVersion).id)"
@click="voidPending(row as PlatformPriceBookVersion)"
2026-09-15 09:58:15 +08:00
>作废</el-button>
2026-09-30 14:01:58 +08:00
</div></template>
2026-08-12 10:59:49 +08:00
<template #version="{ row }"><div class="version-cell"><strong>V{{ (row as PlatformPriceBookVersion).versionNo }}</strong></div></template>
2026-09-08 17:43:04 +08:00
<template #scope="{ row }"><el-tag :type="(row as PlatformPriceBookVersion).customerId === '0' ? 'info' : 'warning'" effect="light">{{ priceBookScopeLabel((row as PlatformPriceBookVersion).customerId) }}</el-tag></template>
2026-08-07 14:47:11 +08:00
<template #status="{ row }"><el-tag :type="priceBookStatusMeta((row as PlatformPriceBookVersion).status).type" effect="light">{{ priceBookStatusMeta((row as PlatformPriceBookVersion).status).label }}</el-tag></template>
<template #contents="{ row }"><div class="metric-cell"><strong>{{ (row as PlatformPriceBookVersion).channelCount }}</strong><span>渠道</span><strong>{{ (row as PlatformPriceBookVersion).rateCount }}</strong><span>价格行</span></div></template>
<template #issues="{ row }"><div class="issue-counts"><span :class="{ 'is-blocking': (row as PlatformPriceBookVersion).blockingErrorCount }">阻断 {{ (row as PlatformPriceBookVersion).blockingErrorCount }}</span><span>警告 {{ (row as PlatformPriceBookVersion).warningCount }}</span></div></template>
<template #empty><el-empty description="暂无船期价格表版本" /></template>
2026-08-10 16:24:54 +08:00
<template #pagination-left>共 {{ rows.length }} 个版本 · {{ activeCount }} 个生效中 · {{ pendingCount }} 个待确认</template>
2026-10-20 23:34:11 +08:00
</DataTable></div></section>
2026-08-10 16:24:54 +08:00
</ListPageLayout>
2026-08-04 21:01:39 +08:00
2026-10-20 23:34:11 +08:00
<AppDialog class="admin-density-dialog admin-workbench is-dialog"
2026-08-07 14:47:11 +08:00
v-model="uploadOpen"
title="上传船期价格表"
width="min(560px, calc(100vw - 28px))"
:confirm-loading="uploading"
:confirm-disabled="!uploadFile || Boolean(uploadFileError)"
confirm-text="上传并解析"
@confirm="submitUpload"
2026-08-04 21:01:39 +08:00
>
<div class="density-dialog-sections">
2026-10-20 15:48:27 +08:00
<el-form class="density-form" label-position="left" label-width="90px" :disabled="uploading">
2026-10-20 23:34:11 +08:00
<div class="workbench-evidence"><table class="density-table density-table--editable" aria-label="上传价格表资料"><tbody>
2026-10-20 15:48:23 +08:00
<tr><td><el-form-item label="报价范围"><template #label><span>报价范围<InlineHelp title="报价上传规则"><p>上传后先展示解析结果和仓库映射;确认后只替换同一报价范围的当前生效版本。客户专属报价生效后,计价时优先于平台公共报价。</p></InlineHelp></span></template><el-select v-model="uploadCustomerId" filterable :disabled="uploading" aria-label="上传报价范围"><el-option label="平台公共报价" value="0" /><el-option v-for="customer in customerOptions" :key="String(customer.id)" :label="`客户专属 · ${customer.tenantName || '未命名客户'}`" :value="String(customer.id)" /></el-select></el-form-item></td></tr>
<tr><td><el-form-item label="价格表文件">
<input ref="uploadInput" class="visually-hidden-input" type="file" accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" aria-label="选择 XLSX 船期价格表" :disabled="uploading" @change="selectUploadFile" />
<div class="upload-file-control"><el-button :disabled="uploading" @click="uploadInput?.click()"><FileSpreadsheet :size="16" />选择 XLSX 文件</el-button><span class="upload-file-name">{{ uploadFile?.name || '尚未选择文件' }}</span></div>
</el-form-item></td></tr>
2026-10-20 23:34:11 +08:00
</tbody></table></div>
</el-form>
<p class="density-dialog-note">{{ uploadFile ? (uploadFile.size / 1024 / 1024).toFixed(2) + ' MB' : '最大 10MB' }}</p>
<el-alert v-if="uploadFileError" :title="uploadFileError" type="error" :closable="false" show-icon />
2026-08-07 14:47:11 +08:00
</div>
</AppDialog>
2026-08-04 21:01:39 +08:00
2026-08-10 16:24:54 +08:00
</AdminPage>
2026-08-04 21:01:39 +08:00
</template>
<style scoped>
2026-08-07 14:47:11 +08:00
.price-book-page { min-width: 0; }
.version-cell { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
2026-10-20 23:34:11 +08:00
.version-cell strong { color: var(--workbench-text); }
.version-cell span, .metric-cell span { color: var(--workbench-muted); font-size: var(--admin-font-caption); }
2026-09-15 09:58:15 +08:00
.metric-cell, .issue-counts { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--admin-space-1) var(--admin-space-3); }
2026-10-20 23:34:11 +08:00
.metric-cell strong { color: var(--workbench-text); font-size: var(--admin-font-body); }
.issue-counts { color: var(--workbench-muted); font-size: var(--admin-font-caption); }
2026-08-07 14:47:11 +08:00
.issue-counts .is-blocking, .is-danger { color: var(--el-color-danger); font-weight: 700; }
.visually-hidden-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; clip-path: inset(50%); }
.upload-file-control { display: flex; width: 100%; min-width: 0; align-items: center; flex-wrap: wrap; gap: var(--admin-space-5); }
.upload-file-control > :deep(.el-button) { flex: 0 0 auto; }
.upload-file-name { min-width: 0; flex: 1 1 180px; overflow-wrap: anywhere; }
2026-08-04 21:01:39 +08:00
@media (max-width: 720px) {
2026-08-07 14:47:11 +08:00
.table-actions :deep(.el-button) { min-height: 40px; }
2026-09-12 11:45:57 +08:00
.upload-file-control { align-items: stretch; flex-direction: column; }
2026-08-04 21:01:39 +08:00
}
</style>