138 lines
3.3 KiB
Vue
138 lines
3.3 KiB
Vue
<!-- Author: fruit.com.cn -->
|
||
<!-- 批量上传队列单行:序号、文件名、大小、进度、状态与操作 -->
|
||
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
import { useI18n } from '@/hooks/web/useI18n'
|
||
import type { UploadQueueItem } from './useUploadQueue'
|
||
|
||
const props = defineProps<{
|
||
item: UploadQueueItem
|
||
/** 自然排序后的展示序号(从 1 起) */
|
||
index: number
|
||
/** 选择阶段才允许移除 */
|
||
allowRemove: boolean
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
retry: [id: string]
|
||
remove: [id: string]
|
||
}>()
|
||
|
||
const { t } = useI18n()
|
||
|
||
const BYTES_PER_KB = 1024
|
||
const BYTES_PER_MB = 1024 * 1024
|
||
|
||
const sizeLabel = computed(() => {
|
||
const size = props.item.fileSize
|
||
if (size >= BYTES_PER_MB) {
|
||
return `${(size / BYTES_PER_MB).toFixed(1)} MB`
|
||
}
|
||
return `${Math.max(1, Math.round(size / BYTES_PER_KB))} KB`
|
||
})
|
||
|
||
const statusLabel = computed(() => t(`episodeBatch.status.${props.item.status}`))
|
||
|
||
const statusType = computed(() => {
|
||
switch (props.item.status) {
|
||
case 'done':
|
||
return 'success'
|
||
case 'failed':
|
||
return 'danger'
|
||
case 'paused':
|
||
return 'warning'
|
||
case 'merging':
|
||
case 'uploading':
|
||
return ''
|
||
default:
|
||
return 'info'
|
||
}
|
||
})
|
||
|
||
const canRetry = computed(() => props.item.status === 'failed')
|
||
</script>
|
||
|
||
<template>
|
||
<div class="upload-task-row" :class="{ 'is-failed': item.status === 'failed' }">
|
||
<div class="upload-task-row__meta">
|
||
<span class="upload-task-row__index">{{ index }}</span>
|
||
<b class="upload-task-row__name" :title="item.fileName">{{ item.fileName }}</b>
|
||
<small>{{ sizeLabel }}</small>
|
||
<el-tag :type="statusType" effect="light" size="small">{{ statusLabel }}</el-tag>
|
||
</div>
|
||
<el-progress
|
||
:percentage="item.percent"
|
||
:status="
|
||
item.status === 'failed' ? 'exception' : item.status === 'done' ? 'success' : undefined
|
||
"
|
||
:stroke-width="10"
|
||
/>
|
||
<p v-if="item.error" class="upload-task-row__error">{{ item.error }}</p>
|
||
<div class="upload-task-row__actions">
|
||
<el-button v-if="canRetry" size="small" type="primary" @click="emit('retry', item.id)">
|
||
{{ t('episodeBatch.retry') }}
|
||
</el-button>
|
||
<el-button
|
||
v-if="allowRemove"
|
||
size="small"
|
||
type="danger"
|
||
text
|
||
@click="emit('remove', item.id)"
|
||
>
|
||
{{ t('episodeBatch.remove') }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.upload-task-row {
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
}
|
||
|
||
.upload-task-row__meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.upload-task-row__index {
|
||
flex: 0 0 auto;
|
||
min-width: 1.5em;
|
||
color: var(--el-text-color-secondary);
|
||
font-size: 13px;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.upload-task-row__name {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.upload-task-row__error {
|
||
margin: 6px 0 0;
|
||
color: var(--el-color-danger);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.upload-task-row__actions {
|
||
margin-top: 8px;
|
||
display: flex;
|
||
gap: 8px;
|
||
justify-content: flex-end;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.upload-task-row.is-failed {
|
||
background: color-mix(in srgb, var(--el-color-danger) 6%, transparent);
|
||
border-radius: 6px;
|
||
padding-left: 8px;
|
||
padding-right: 8px;
|
||
}
|
||
</style>
|