2026-08-03 18:15:41 +08:00
< script setup lang = "ts" >
2026-09-15 09:58:15 +08:00
import '@/styles/adminDensity.css'
2026-08-03 18:15:41 +08:00
import { computed , onBeforeUnmount , onMounted , ref , watch } from 'vue'
import { ElMessage } from 'element-plus'
2026-08-03 18:49:18 +08:00
import { getApiErrorDetails , getApiErrorMessage , type ApiErrorDetails } from '@/shared/api/http'
2026-08-03 18:15:41 +08:00
import {
downloadPlatformExportTask ,
2026-08-04 09:05:19 +08:00
getPlatformExportTask ,
2026-08-03 18:15:41 +08:00
listPlatformExportTasks ,
retryPlatformExportTask ,
type ExportTaskStatus ,
type PlatformExportTask ,
2026-08-03 18:49:18 +08:00
} from '@/modules/downloads/api/platformExportTasks'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
2026-08-10 16:24:54 +08:00
import AdminPage from '@/shared/components/AdminPage.vue'
2026-08-13 11:57:44 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-08-03 18:49:18 +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-03 18:49:18 +08:00
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
2026-08-03 18:15:41 +08:00
type DownloadFilters = QueryModel & {
exportType : string
status : ExportTaskStatus | ''
requestedRange : string []
}
2026-08-17 17:49:10 +08:00
const exportTypeMeta : Record < string , { label : string ; retryPermission : string }> = {
2026-10-08 17:08:03 +08:00
LAST_MILE_ORDER_LIST : { label : '尾程订单' , retryPermission : 'platform:overseas-warehouse:last-mile-order:export' },
OVERSEAS_INVENTORY_BALANCE : { label : '库存余额' , retryPermission : 'platform:overseas-warehouse:inventory:export' },
OVERSEAS_INVENTORY_LEDGER : { label : '进销存记录' , retryPermission : 'platform:overseas-warehouse:inventory:export' },
OVERSEAS_QUOTE_LIST : { label : '海外报价' , retryPermission : 'platform:overseas-master-data:quote:export' },
MONTHLY_EXCHANGE_RATE_LIST : { label : '月度汇率' , retryPermission : 'platform:overseas-master-data:exchange-rate:export' },
VEHICLE_LIST : { label : '车辆' , retryPermission : 'platform:overseas-master-data:vehicle:export' },
FLEET_QUOTE_LIST : { label : '车队报价' , retryPermission : 'platform:fleet:quote:export' },
TRIP_OPERATIONS_LIST : { label : '出车记录' , retryPermission : 'platform:fleet:operations:export' },
FINANCE_RECEIVABLE_STATISTICS : { label : '应收统计' , retryPermission : 'platform:finance:receivable-statistics:export' },
LOADING_LIST_BUY_DECLARATION_ITEMS : { label : '买单报关明细' , retryPermission : 'platform:firstleg:customs:manage' },
2026-08-17 17:49:10 +08:00
CUSTOMER_LIST : { label : '客户列表' , retryPermission : 'platform:customer:export' },
2026-09-01 21:09:39 +08:00
DOMESTIC_WAREHOUSE_ORDER_LIST : { label : '国内仓订单列表' , retryPermission : 'platform:order:export' },
2026-08-17 17:49:10 +08:00
FINANCE_CUSTOMER_ACCOUNT_LIST : { label : '客户账户' , retryPermission : 'platform:finance:customer-account:export' },
FINANCE_CUSTOMER_ACCOUNT_LEDGER : { label : '客户账户流水' , retryPermission : 'platform:finance:customer-account:export' },
FINANCE_FEE_SOURCE_LIST : { label : '客户应收应付费用来源' , retryPermission : 'platform:finance:customer-arap:export' },
FINANCE_RECONCILIATION_STATEMENT_LIST : { label : '客户对账单' , retryPermission : 'platform:finance:customer-statement:export' },
FINANCE_RECEIVABLE_SETTLEMENT_LIST : { label : '应收结算单列表' , retryPermission : 'platform:finance:settlement-ar:export' },
FINANCE_RECEIVABLE_SETTLEMENT_DETAIL : { label : '应收结算单详情' , retryPermission : 'platform:finance:settlement-ar:export' },
FINANCE_PAYABLE_SETTLEMENT_LIST : { label : '应付结算单列表' , retryPermission : 'platform:finance:settlement-ap:export' },
FINANCE_PAYABLE_SETTLEMENT_DETAIL : { label : '应付结算单详情' , retryPermission : 'platform:finance:settlement-ap:export' },
}
2026-08-03 18:15:41 +08:00
const access = useAccessPreviewStore ()
const page = ref ( 1 )
const pageSize = ref ( 20 )
const loading = ref ( false )
const downloadingId = ref ( '' )
const retryingId = ref ( '' )
const loadError = ref < ApiErrorDetails | null >( null )
2026-08-04 09:05:19 +08:00
const syncWarning = ref < ApiErrorDetails | null >( null )
const detailOpen = ref ( false )
const detailLoading = ref ( false )
const detailError = ref < ApiErrorDetails | null >( null )
const detailTask = ref < PlatformExportTask | null >( null )
2026-08-03 18:15:41 +08:00
const result = ref ({ records : [] as PlatformExportTask [], total : 0 , pages : 0 })
const filters = ref < DownloadFilters >({ exportType : '' , status : '' , requestedRange : [] })
let refreshTimer : number | undefined
2026-08-03 18:49:18 +08:00
let disposed = false
2026-08-04 09:05:19 +08:00
let loadRequestId = 0
let detailRequestId = 0
2026-08-03 18:15:41 +08:00
const queryFields : QueryField [] = [
2026-08-06 19:03:13 +08:00
{ key : 'exportType' , type : 'select' , label : '任务类型' , placeholder : '全部任务类型' , options : [
2026-08-17 17:49:10 +08:00
... Object . entries ( exportTypeMeta ). map (([ value , meta ]) => ({ label : meta . label , value })),
2026-08-03 18:15:41 +08:00
] },
2026-08-06 19:03:13 +08:00
{ key : 'status' , type : 'select' , label : '任务状态' , placeholder : '全部任务状态' , options : [
2026-08-03 18:15:41 +08:00
{ label : '等待处理' , value : 'PENDING' }, { label : '处理中' , value : 'RUNNING' },
{ label : '已完成' , value : 'SUCCEEDED' }, { label : '失败' , value : 'FAILED' },
{ label : '已过期' , value : 'EXPIRED' },
] },
2026-08-06 19:03:13 +08:00
{ key : 'requestedRange' , type : 'daterange' , label : '申请日期' , placeholder : '请选择申请日期' },
2026-08-03 18:15:41 +08:00
]
const columns : DataTableColumn [] = [
2026-08-04 09:05:19 +08:00
{ key : 'actions' , label : '操作' , width : 210 , fixed : 'left' , slotName : 'actions' },
2026-08-06 19:03:13 +08:00
{ key : 'taskName' , prop : 'taskName' , label : '任务名称' , minWidth : 180 , showOverflowTooltip : true },
2026-08-03 18:15:41 +08:00
{ key : 'type' , label : '任务类型' , width : 120 , slotName : 'type' },
{ key : 'status' , label : '状态' , width : 110 , slotName : 'status' },
{ key : 'progress' , label : '数据量' , width : 120 , slotName : 'progress' },
2026-08-17 19:52:04 +08:00
{ key : 'format' , prop : 'format' , label : '格式' , width : 80 },
2026-08-03 18:15:41 +08:00
{ key : 'requester' , label : '申请人' , minWidth : 130 , slotName : 'requester' },
{ key : 'requestedAt' , label : '申请时间' , width : 170 , slotName : 'requestedAt' },
2026-08-17 19:52:04 +08:00
{ key : 'completedAt' , label : '完成时间' , width : 170 , slotName : 'completedAt' },
{ key : 'expiresAt' , label : '过期时间' , width : 170 , slotName : 'expiresAt' },
2026-08-03 18:15:41 +08:00
]
const hasActiveTasks = computed (() => result . value . records . some (( task ) => [ 'PENDING' , 'RUNNING' ]. includes ( task . status )))
function clearRefreshTimer () {
if ( refreshTimer !== undefined ) window . clearTimeout ( refreshTimer )
refreshTimer = undefined
}
function scheduleRefresh () {
clearRefreshTimer ()
2026-08-04 09:05:19 +08:00
if ( ! disposed && hasActiveTasks . value ) refreshTimer = window . setTimeout (() => { void loadTasks ( true ) }, 5000 )
2026-08-03 18:15:41 +08:00
}
async function loadTasks ( background = false ) {
2026-08-04 09:05:19 +08:00
const requestId = ++ loadRequestId
2026-08-03 18:15:41 +08:00
clearRefreshTimer ()
if ( ! background ) loading . value = true
2026-08-04 09:05:19 +08:00
if ( ! background ) loadError . value = null
2026-08-03 18:15:41 +08:00
try {
const range = filters . value . requestedRange
const response = await listPlatformExportTasks ({
page : page . value ,
size : pageSize . value ,
exportType : filters . value . exportType || undefined ,
status : filters . value . status || undefined ,
requestedFrom : range ? .[ 0 ] || undefined ,
requestedTo : range ? .[ 1 ] || undefined ,
})
2026-08-04 09:05:19 +08:00
if ( disposed || requestId !== loadRequestId ) return
2026-08-03 18:15:41 +08:00
result . value = { records : response . records , total : response . total , pages : response . pages }
2026-08-04 09:05:19 +08:00
loadError . value = null
syncWarning . value = null
if ( detailOpen . value && detailTask . value ) {
const freshDetail = response . records . find (( task ) => task . id === detailTask . value ? . id )
if ( freshDetail ) detailTask . value = freshDetail
}
2026-08-03 18:15:41 +08:00
} catch ( error ) {
2026-08-04 09:05:19 +08:00
if ( disposed || requestId !== loadRequestId ) return
const details = getApiErrorDetails ( error , '下载任务加载失败' )
if ( background && result . value . records . length > 0 ) syncWarning . value = details
else loadError . value = details
2026-08-03 18:15:41 +08:00
} finally {
2026-08-04 09:05:19 +08:00
if ( ! disposed && requestId === loadRequestId ) {
if ( ! background ) loading . value = false
scheduleRefresh ()
}
2026-08-03 18:15:41 +08:00
}
}
function applyFilters () {
if ( page . value === 1 ) void loadTasks ()
else page . value = 1
}
2026-08-04 09:05:19 +08:00
async function openDetail ( task : PlatformExportTask ) {
if ( ! task ? . id ) return
const requestId = ++ detailRequestId
detailTask . value = task
detailError . value = null
detailOpen . value = true
detailLoading . value = true
try {
const response = await getPlatformExportTask ( task . id )
if ( disposed || requestId !== detailRequestId ) return
detailTask . value = response
} catch ( error ) {
if ( disposed || requestId !== detailRequestId ) return
detailError . value = getApiErrorDetails ( error , '下载任务详情加载失败' )
} finally {
if ( ! disposed && requestId === detailRequestId ) detailLoading . value = false
}
}
function closeDetail () {
detailRequestId += 1
detailTask . value = null
detailError . value = null
detailLoading . value = false
}
2026-08-13 11:57:44 +08:00
function downloadDetailTask () {
if ( detailTask . value ? . status === 'SUCCEEDED' ) void downloadTask ( detailTask . value )
}
2026-08-03 18:15:41 +08:00
async function downloadTask ( task : PlatformExportTask ) {
downloadingId . value = task . id
try {
const blob = await downloadPlatformExportTask ( task . id )
const url = URL . createObjectURL ( blob )
const anchor = document . createElement ( 'a' )
2026-08-04 09:05:19 +08:00
try {
anchor . href = url
anchor . download = safeFileName ( task . fileName || ` ${ task . taskName } .xlsx` )
document . body . append ( anchor )
anchor . click ()
} finally {
anchor . remove ()
window . setTimeout (() => URL . revokeObjectURL ( url ), 0 )
}
2026-08-03 18:15:41 +08:00
} catch ( error ) {
2026-08-04 09:05:19 +08:00
const details = getApiErrorDetails ( error , '文件下载失败,请刷新任务状态后重试' )
const trace = [ details . code , details . requestId && `requestId: ${ details . requestId } ` , details . traceId && `traceId: ${ details . traceId } ` ]
. filter ( Boolean )
. join ( ', ' )
ElMessage . error ( trace ? ` ${ details . message } ( ${ trace } ) ` : details . message )
2026-08-03 18:15:41 +08:00
} finally {
downloadingId . value = ''
}
}
async function retryTask ( task : PlatformExportTask ) {
retryingId . value = task . id
try {
await retryPlatformExportTask ( task . id )
ElMessage . success ( '导出任务已重新提交' )
await loadTasks ()
} catch ( error ) {
ElMessage . error ( getApiErrorMessage ( error , '任务重试失败' ))
} finally {
retryingId . value = ''
}
}
function safeFileName ( value : string ) {
const safe = value . replace ( /[\\/:*?"<>|\u0000-\u001f]/g , '_' ). trim ()
return safe || '导出文件.xlsx'
}
2026-08-17 17:49:10 +08:00
function typeLabel ( type : string ) { return exportTypeMeta [ type ] ? . label || type }
2026-08-03 18:15:41 +08:00
function statusMeta ( status : ExportTaskStatus ) {
const values = {
PENDING : { label : '等待处理' , type : 'info' as const },
RUNNING : { label : '处理中' , type : 'warning' as const },
SUCCEEDED : { label : '已完成' , type : 'success' as const },
FAILED : { label : '失败' , type : 'danger' as const },
EXPIRED : { label : '已过期' , type : 'info' as const },
}
2026-08-04 09:05:19 +08:00
return values [ status ] ?? { label : status || '未知状态' , type : 'info' as const }
2026-08-03 18:15:41 +08:00
}
2026-09-03 10:39:55 +08:00
function taskStatusMeta ( task : PlatformExportTask ) {
if ( task . status === 'PENDING' && task . attemptCount > 0 ) {
return { label : '等待重试' , type : 'warning' as const }
}
return statusMeta ( task . status )
}
2026-08-03 18:15:41 +08:00
function formatDate ( value : string | null ) {
if ( ! value ) return '—'
const date = new Date ( value )
return Number . isNaN ( date . getTime ()) ? value : date . toLocaleString ( 'zh-CN' , { hour12 : false })
}
function formatSize ( bytes : number | null ) {
if ( bytes === null ) return ''
if ( bytes < 1024 ) return ` ${ bytes } B`
if ( bytes < 1024 * 1024 ) return ` ${ ( bytes / 1024 ). toFixed ( 1 ) } KB`
return ` ${ ( bytes / 1024 / 1024 ). toFixed ( 1 ) } MB`
}
function canRetry ( task : PlatformExportTask ) {
2026-08-17 17:49:10 +08:00
const permission = exportTypeMeta [ task . exportType ] ? . retryPermission
2026-09-03 10:39:55 +08:00
const retryableStatus = task . status === 'FAILED' || ( task . status === 'PENDING' && task . attemptCount > 0 )
return retryableStatus && permission !== undefined && access . can ( permission )
}
function retryLabel ( task : PlatformExportTask ) {
return task . status === 'PENDING' ? '立即重试' : '重试'
2026-08-03 18:15:41 +08:00
}
watch ([ page , pageSize ], () => { void loadTasks () })
onMounted ( loadTasks )
2026-08-03 18:49:18 +08:00
onBeforeUnmount (() => {
disposed = true
2026-08-04 09:05:19 +08:00
loadRequestId += 1
detailRequestId += 1
2026-08-03 18:49:18 +08:00
clearRefreshTimer ()
})
2026-08-03 18:15:41 +08:00
</ script >
< template >
2026-09-15 09:58:15 +08:00
< AdminPage class = "admin-density" >
2026-08-03 18:15:41 +08:00
< div class = "business-page download-center-page" >
2026-08-10 16:24:54 +08:00
< ListPageLayout ariaLabel = "下载任务列表" >
2026-08-03 18:15:41 +08:00
2026-09-30 14:01:58 +08:00
< section :aria-busy = "loading" >< DataTable appearance = "integrated" :stripe = "false" column -settings-mode =" show " :results-visible = "!loadError" v -model:page = "page" v -model:page-size = "pageSize" :data = "result.records" :columns = "columns" preference -key =" downloads -table " :loading = "loading" :total = "result.total" row -key =" id " empty -text =" 暂无符合筛选条件的下载任务 " aria -label =" 下载任务列表 " >
< template # toolbar -start >< h1 > 下载中心 </ h1 ></ template >
< template # filters >
< QueryToolbar v-model = "filters" ariaLabel="下载任务筛选" :fields="queryFields" preference-key="downloads-query" @search="applyFilters" />
</ template >
< template # notices >< template v-if = "loadError" >
2026-08-10 16:24:54 +08:00
< section class = "surface-panel download-error" role = "alert" >
< div >< strong > 下载任务暂时无法加载 </ strong >< p >{{ loadError . message }}</ p >< ApiErrorMeta :details = "loadError" /></ div >
< el-button type = "primary" :loading = "loading" @click ="loadTasks()" > 重新加载 </ el -button >
</ section >
2026-09-30 14:01:58 +08:00
</ template ></ template >
< template # before -results >< div v-if = "syncWarning" class="download-sync-warning" role="status" >
2026-08-04 09:05:19 +08:00
< span > 自动刷新暂时失败 , 正在保留当前任务并继续重试 : {{ syncWarning . message }}</ span >
< ApiErrorMeta :details = "syncWarning" />
2026-09-30 14:01:58 +08:00
</ div ></ template >
2026-08-10 16:24:54 +08:00
< template # pagination -left =" { total : tableTotal } " >< span > 共 {{ tableTotal }} 个任务 </ span ></ 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" v-if = "(row as PlatformExportTask)?.id" @click="openDetail(row as PlatformExportTask)" > 详情 </ el -button >
< el-button data -tone =" neutral " class = "density-action" v-if = "(row as PlatformExportTask).status === 'SUCCEEDED'" :loading="downloadingId === (row as PlatformExportTask).id" @click="downloadTask(row as PlatformExportTask)" > 下载 </ el -button >
< el-button data -tone =" neutral " class = "density-action" v-if = "canRetry(row as PlatformExportTask)" :loading="retryingId === (row as PlatformExportTask).id" @click="retryTask(row as PlatformExportTask)" >{{ retryLabel ( row as PlatformExportTask ) }} </ el -button >
2026-08-03 18:15:41 +08:00
</ div ></ template >
< template # type = "{ row }" >{{ typeLabel (( row as PlatformExportTask ). exportType ) }}</ template >
2026-09-03 10:39:55 +08:00
< template # status = "{ row }" >< el-tag : type = "taskStatusMeta(row as PlatformExportTask).type" effect = "light" >{{ taskStatusMeta ( row as PlatformExportTask ). label }}</ el-tag ></ template >
2026-08-03 18:15:41 +08:00
< template # progress = "{ row }" >< span >{{ ( row as PlatformExportTask ). processedRows }}< template v-if = "(row as PlatformExportTask).totalRows !== null"> / {{ (row as PlatformExportTask).totalRows }}</template></span><small v-if="(row as PlatformExportTask).sizeBytes !== null" >{{ formatSize (( row as PlatformExportTask ) .sizeBytes ) }}</ small ></ template >
< template # requester = "{ row }" >{{ ( row as PlatformExportTask ). requestedUsername || ( row as PlatformExportTask ). requestedBy }}</ template >
< template # requestedAt = "{ row }" >{{ formatDate (( row as PlatformExportTask ). requestedAt ) }}</ template >
< template # completedAt = "{ row }" >{{ formatDate (( row as PlatformExportTask ). completedAt ) }}</ template >
< template # expiresAt = "{ row }" >{{ formatDate (( row as PlatformExportTask ). expiresAt ) }}</ template >
2026-09-30 14:01:58 +08:00
</ DataTable ></ section >
2026-08-10 16:24:54 +08:00
</ ListPageLayout >
2026-08-04 09:05:19 +08:00
2026-09-15 09:58:15 +08:00
< AppDialog class = "admin-density-dialog"
2026-08-04 09:05:19 +08:00
v-model = "detailOpen"
: title = "detailTask ? `任务详情 · ${detailTask.taskNo}` : '任务详情'"
2026-08-13 11:57:44 +08:00
width = "min(720px, calc(100vw - 28px))"
2026-08-04 09:05:19 +08:00
:loading = "detailLoading"
2026-08-13 11:57:44 +08:00
:show-confirm = "false"
2026-08-04 09:05:19 +08:00
destroy -on-close
@closed ="closeDetail"
>
< section v-if = "detailError" class="detail-load-error" role="alert" >
< strong > 任务详情暂时无法加载 </ strong >
< p >{{ detailError . message }}</ p >
< ApiErrorMeta :details = "detailError" />
< el-button type = "primary" @click ="detailTask && openDetail(detailTask)" > 重新加载 </ el -button >
</ section >
< div v-else-if = "detailTask" class="task-detail" >
< div class = "task-detail-summary" >
< div >
< h3 >{{ detailTask . taskName }}</ h3 >
< p >{{ typeLabel ( detailTask . exportType ) }} · {{ detailTask . format }}</ p >
</ div >
2026-09-03 10:39:55 +08:00
< el-tag :type = "taskStatusMeta(detailTask).type" effect = "light" >{{ taskStatusMeta ( detailTask ). label }}</ el-tag >
2026-08-04 09:05:19 +08:00
</ div >
2026-09-15 09:58:15 +08:00
< dl class = "task-detail-grid density-fields" >
2026-08-04 09:05:19 +08:00
< div >< dt > 任务编号 </ dt >< dd >{{ detailTask . taskNo }}</ dd ></ div >
< div >< dt > 申请人 </ dt >< dd >{{ detailTask . requestedUsername || detailTask . requestedBy }}</ dd ></ div >
< div >< dt > 申请时间 </ dt >< dd >{{ formatDate ( detailTask . requestedAt ) }}</ dd ></ div >
< div >< dt > 开始时间 </ dt >< dd >{{ formatDate ( detailTask . startedAt ) }}</ dd ></ div >
< div >< dt > 完成时间 </ dt >< dd >{{ formatDate ( detailTask . completedAt ) }}</ dd ></ div >
2026-09-03 10:39:55 +08:00
< div >< dt > 执行次数 </ dt >< dd >{{ detailTask . attemptCount }} / {{ detailTask . maxAttempts }}</ dd ></ div >
< div >< dt > 下次重试 </ dt >< dd >{{ detailTask . status === 'PENDING' && detailTask . attemptCount > 0 ? formatDate ( detailTask . nextRetryAt ) : '—' }}</ dd ></ div >
2026-08-04 09:05:19 +08:00
< div >< dt > 过期时间 </ dt >< dd >{{ formatDate ( detailTask . expiresAt ) }}</ dd ></ div >
< div >< dt > 处理数据 </ dt >< dd >{{ detailTask . processedRows }}< template v-if = "detailTask.totalRows !== null" > / {{ detailTask.totalRows }}</ template > 行 </ dd ></ div >
< div >< dt > 文件大小 </ dt >< dd >{{ formatSize ( detailTask . sizeBytes ) || '—' }}</ dd ></ div >
< div class = "is-wide" >< dt > 文件名称 </ dt >< dd >{{ detailTask . fileName || '—' }}</ dd ></ div >
</ dl >
< section v-if = "detailTask.failureMessage" class="task-failure" role="status" >
< strong > 失败原因 </ strong >
< p >{{ detailTask . failureMessage }}</ p >
</ section >
</ div >
2026-08-13 11:57:44 +08:00
< template # footer = "{ close }" >
< div class = "app-dialog__footer" >
< el-button @click ="close" > 关闭 </ el -button >
2026-09-03 10:39:55 +08:00
< el-button
v-if = "detailTask && canRetry(detailTask)"
: loading = "retryingId === detailTask.id"
@click ="retryTask(detailTask)"
>{{ retryLabel ( detailTask ) }}</ el-button >
2026-08-13 11:57:44 +08:00
< el-button
type = "primary"
: loading = "downloadingId === detailTask?.id"
: disabled = "detailTask?.status !== 'SUCCEEDED'"
@click ="downloadDetailTask"
> 下载文件 </ el-button >
</ div >
</ template >
</ AppDialog >
2026-08-03 18:15:41 +08:00
</ div >
2026-08-10 16:24:54 +08:00
</ AdminPage >
2026-08-03 18:15:41 +08:00
</ template >
< style scoped >
2026-09-15 09:58:15 +08:00
. task - detail - grid . density - fields { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); }
. task - detail - grid . is - wide { grid - column : 1 / - 1 ; }
2026-08-03 18:15:41 +08:00
. download - center - page { min - width : 0 ; }
2026-09-15 09:58:15 +08:00
. download - error { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 6 ); }
. download - error { padding : var ( -- admin - space - 6 ); }
. download - error strong { color : var ( -- cc - danger ); font - size : var ( -- admin - font - body ); }
. download - error p { margin : 5 px 0 ; color : var ( -- cc - text ); font - size : var ( -- admin - font - body ); }
. download - sync - warning { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 5 ); padding : var ( -- admin - space - 4 ) var ( -- admin - space - 6 ); border - bottom : 1 px solid # fedf89 ; background : # fffaeb ; color : # 93370 d ; font - size : var ( -- admin - font - caption ); }
2026-08-03 18:49:18 +08:00
. failure - cell { display : block ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. failure - message { color : var ( -- cc - danger ); }
2026-08-04 09:05:19 +08:00
. detail - load - error { display : flex ; min - height : 220 px ; align - items : flex - start ; justify - content : center ; flex - direction : column ; }
2026-09-15 09:58:15 +08:00
. detail - load - error strong { color : var ( -- cc - danger ); font - size : var ( -- admin - font - section ); }
. detail - load - error p { margin : var ( -- admin - space - 2 ) 0 ; color : var ( -- cc - text ); font - size : var ( -- admin - font - body ); }
. detail - load - error : deep (. el - button ) { margin - top : var ( -- admin - space - 6 ); }
2026-08-04 09:05:19 +08:00
. task - detail { min - width : 0 ; }
2026-09-15 09:58:15 +08:00
. task - detail - summary { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : var ( -- admin - space - 6 ); padding - bottom : var ( -- admin - space - 6 ); border - bottom : 1 px solid var ( -- cc - border ); }
. task - detail - summary h3 { margin : 0 ; overflow - wrap : anywhere ; color : var ( -- cc - text ); font - size : var ( -- admin - font - section ); }
. task - detail - summary p { margin : var ( -- admin - space - 2 ) 0 0 ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption ); }
. task - failure { margin - top : var ( -- admin - space - 6 ); padding : var ( -- admin - space - 5 ); border : 1 px solid color - mix ( in srgb , var ( -- cc - danger ) 24 % , var ( -- cc - border )); border - radius : var ( -- admin - radius ); background : color - mix ( in srgb , var ( -- cc - danger ) 5 % , # fff ); }
. task - failure strong { color : var ( -- cc - danger ); font - size : var ( -- admin - font - body ); }
. task - failure p { margin : var ( -- admin - space - 2 ) 0 0 ; overflow - wrap : anywhere ; color : var ( -- cc - text ); font - size : var ( -- admin - font - body ); line - height : 1.6 ; }
small { display : block ; margin - top : 3 px ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption ); }
2026-08-03 18:15:41 +08:00
@ media ( max - width : 720 px ) {
2026-08-06 20:14:56 +08:00
. download - error , . download - sync - warning { align - items : stretch ; flex - direction : column ; }
. table - actions : deep (. el - button ) { min - height : 44 px ; }
2026-08-04 09:05:19 +08:00
. table - actions : deep (. el - button ) { padding - right : 8 px ; padding - left : 8 px ; }
}
@ media ( max - width : 480 px ) {
. task - detail - summary { flex - direction : column ; }
. task - detail - grid { grid - template - columns : 1 fr ; }
. task - detail - grid > div : nth - child ( odd ) : not (. is - wide ) { border - right : 0 ; }
. task - detail - grid . is - wide { grid - column : 1 ; }
2026-08-03 18:15:41 +08:00
}
</ style >