2026-08-04 21:01:39 +08:00
< script setup lang = "ts" >
2026-08-07 14:47:11 +08:00
import { computed , onMounted , ref , watch } from 'vue'
2026-09-11 18:44:01 +08:00
import { CircleX , Download , Eye , 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 } 将不能再确认生效;版本记录和原始文件会继续保留。` ,
'作废待确认价格表' ,
{
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-08-10 16:24:54 +08:00
< AdminPage as = "div" class = "price-book-page" spacing = "normal" >
< ListPageLayout aria -label =" 承运商报价版本管理 " >
< template # toolbar >
< QueryToolbar
v-model = "filters"
class = "surface-panel"
ariaLabel = "船期价格表筛选"
:fields = "queryFields"
preference -key =" price -book-query "
@search ="applyQuery"
/>
2026-08-07 14:47:11 +08:00
</ template >
2026-08-04 21:01:39 +08:00
2026-08-10 16:24:54 +08:00
< template v-if = "loadError" # state >
< LoadErrorState
title = "船期价格表版本加载失败"
:message = "loadErrorMessage"
:loading = "loading"
@retry ="loadPriceBooks()"
>
< template # details >< ApiErrorMeta :details = "loadError" /></ template >
</ LoadErrorState >
</ template >
2026-08-04 21:01:39 +08:00
2026-08-10 16:24:54 +08:00
< section v-if = "!loadError" class="surface-panel table-panel" aria-live="polite" >
2026-08-04 21:01:39 +08:00
< DataTable
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
stripe
2026-08-07 14:47:11 +08:00
aria -label =" 船期价格表版本列表 "
empty -text =" 暂无船期价格表版本 "
2026-08-04 21:01:39 +08:00
>
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-08-04 21:01:39 +08:00
< template # actions = "{ row }" >
< div class = "table-actions" >
2026-08-12 18:00:26 +08:00
< el-button size = "small" plain type = "primary" @click ="openDetail(row as PlatformPriceBookVersion)" >< Eye :size = "15" />{{ detailActionLabel ( row as PlatformPriceBookVersion ) }}</ el-button >
2026-08-04 21:01:39 +08:00
< el-button
2026-08-12 18:00:26 +08:00
size = "small"
plain
2026-08-07 14:47:11 +08:00
: loading = "downloadingIds.has((row as PlatformPriceBookVersion).id)"
@click ="downloadSource(row as PlatformPriceBookVersion)"
>< Download :size = "15" /> 原文件 </ el-button >
2026-09-12 11:45:57 +08:00
< el-button
v-if = "canEdit && (row as PlatformPriceBookVersion).status === 'PENDING_CONFIRMATION'"
size = "small"
plain
type = "danger"
: loading = "voidingIds.has((row as PlatformPriceBookVersion).id)"
@click ="voidPending(row as PlatformPriceBookVersion)"
>< CircleX :size = "15" /> 作废 </ el-button >
2026-08-04 21:01:39 +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-08-04 21:01:39 +08:00
</ DataTable >
2026-08-10 16:24:54 +08:00
</ section >
</ ListPageLayout >
2026-08-04 21:01:39 +08:00
2026-08-07 14:47:11 +08:00
< AppDialog
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
>
2026-08-07 14:47:11 +08:00
< div class = "upload-content" >
2026-09-08 17:43:04 +08:00
< el-alert title = "上传后先展示解析结果和仓库映射;确认后只替换同一报价范围的当前生效版本。客户专属报价生效后,计价时优先于平台公共报价。" type = "info" :closable = "false" show -icon />
< label class = "upload-scope-field" >< span > 报价范围 </ span >< 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 ></ label >
2026-08-07 14:47:11 +08:00
< 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 >
< div class = "upload-file-name" : class = "{ 'is-empty': !uploadFile }" >
< strong >{{ uploadFile ? . name || '尚未选择文件' }}</ strong >
< span v-if = "uploadFile" >{{ ( uploadFile.size / 1024 / 1024 ) .toFixed ( 2 ) }} MB </ span >
< span v-else > 最大 10MB </ span >
2026-08-04 21:01:39 +08:00
</ div >
2026-08-07 14:47:11 +08:00
</ div >
< p v-if = "uploadFileError" class="upload-file-error" role="alert" >{{ uploadFileError }}</ p >
</ 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 ; }
. table - actions { display : flex ; min - height : 32 px ; align - items : center ; gap : 8 px ; }
. table - actions : deep (. el - button + . el - button ) { margin - left : 0 ; }
. version - cell { display : flex ; min - width : 0 ; flex - direction : column ; gap : 3 px ; }
. version - cell strong { color : var ( -- cc - text ); }
. version - cell span , . metric - cell span { color : var ( -- cc - muted ); font - size : 11 px ; }
. metric - cell , . issue - counts { display : flex ; flex - wrap : wrap ; align - items : baseline ; gap : 4 px 8 px ; }
. metric - cell strong { color : var ( -- cc - text ); font - size : 14 px ; }
. issue - counts { color : var ( -- cc - muted ); font - size : 12 px ; }
. issue - counts . is - blocking , . is - danger { color : var ( -- el - color - danger ); font - weight : 700 ; }
. upload - content { display : grid ; gap : 18 px ; }
2026-09-08 17:43:04 +08:00
. upload - scope - field { display : grid ; gap : 6 px ; color : var ( -- cc - muted ); font - size : 12 px ; }
. upload - scope - field : deep (. el - select ) { width : 100 % ; }
2026-08-07 14:47:11 +08:00
. visually - hidden - input { position : absolute ; width : 1 px ; height : 1 px ; overflow : hidden ; clip : rect ( 0 0 0 0 ); white - space : nowrap ; clip - path : inset ( 50 % ); }
. upload - file - control { display : flex ; align - items : center ; gap : 14 px ; }
. upload - file - name { display : grid ; min - width : 0 ; gap : 3 px ; color : var ( -- cc - text ); font - size : 13 px ; }
. upload - file - name strong { overflow - wrap : anywhere ; }
. upload - file - name span { color : var ( -- cc - muted ); font - size : 12 px ; }
. upload - file - name . is - empty strong { color : var ( -- cc - muted ); font - weight : 400 ; }
. upload - file - error { margin : - 8 px 0 0 ; color : var ( -- el - color - danger ); font - size : 12 px ; }
2026-08-04 21:01:39 +08:00
@ media ( max - width : 720 px ) {
2026-08-07 14:47:11 +08:00
. table - actions : deep (. el - button ) { min - height : 40 px ; }
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 >