865 lines
24 KiB
Vue
865 lines
24 KiB
Vue
<template>
|
||
<div class="upload-view">
|
||
<!-- 頁面標題 -->
|
||
<div class="page-header">
|
||
<h1 class="page-title">檔案上傳</h1>
|
||
<div class="page-actions">
|
||
<el-button @click="$router.push('/jobs')">
|
||
<el-icon><List /></el-icon>
|
||
查看任務列表
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="upload-content">
|
||
<!-- 上傳區域 -->
|
||
<div class="content-card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">選擇要翻譯的檔案</h3>
|
||
<div class="card-subtitle">
|
||
支援 DOCX、DOC、PPTX、XLSX、XLS、PDF 格式,單檔最大 25MB
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-body">
|
||
<!-- 檔案上傳器 -->
|
||
<el-upload
|
||
ref="uploadRef"
|
||
class="upload-dragger"
|
||
:class="{ disabled: uploading }"
|
||
drag
|
||
:multiple="true"
|
||
:show-file-list="false"
|
||
:before-upload="handleBeforeUpload"
|
||
:http-request="() => {}"
|
||
:disabled="uploading"
|
||
>
|
||
<div class="upload-content-inner">
|
||
<el-icon class="upload-icon">
|
||
<UploadFilled />
|
||
</el-icon>
|
||
<div class="upload-text">
|
||
<div class="upload-title">拖拽檔案至此或點擊選擇檔案</div>
|
||
<div class="upload-hint">
|
||
支援 .docx, .doc, .pptx, .xlsx, .xls, .pdf 格式
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-upload>
|
||
|
||
<!-- 已選擇的檔案列表 -->
|
||
<div v-if="selectedFiles.length > 0" class="selected-files">
|
||
<div class="files-header">
|
||
<h4>已選擇的檔案 ({{ selectedFiles.length }})</h4>
|
||
<el-button type="text" @click="clearFiles" :disabled="uploading">
|
||
<el-icon><Delete /></el-icon>
|
||
清空
|
||
</el-button>
|
||
</div>
|
||
|
||
<div class="files-list">
|
||
<div
|
||
v-for="(file, index) in selectedFiles"
|
||
:key="index"
|
||
class="file-item"
|
||
>
|
||
<div class="file-icon">
|
||
<div class="file-type" :class="getFileExtension(file.name)">
|
||
{{ getFileExtension(file.name).toUpperCase() }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="file-info">
|
||
<div class="file-name">{{ file.name }}</div>
|
||
<div class="file-details">
|
||
<span class="file-size">{{ formatFileSize(file.size) }}</span>
|
||
<span class="file-type-text">{{ getFileTypeText(file.name) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="file-actions">
|
||
<el-button
|
||
type="text"
|
||
size="small"
|
||
@click="removeFile(index)"
|
||
:disabled="uploading"
|
||
>
|
||
<el-icon><Close /></el-icon>
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 翻譯設定 -->
|
||
<div class="content-card" v-if="selectedFiles.length > 0">
|
||
<div class="card-header">
|
||
<h3 class="card-title">翻譯設定</h3>
|
||
</div>
|
||
|
||
<div class="card-body">
|
||
<el-form
|
||
ref="translationFormRef"
|
||
:model="translationForm"
|
||
:rules="translationRules"
|
||
label-width="120px"
|
||
size="large"
|
||
>
|
||
<el-form-item label="來源語言" prop="sourceLanguage">
|
||
<el-select
|
||
v-model="translationForm.sourceLanguage"
|
||
placeholder="請選擇來源語言"
|
||
style="width: 100%"
|
||
:disabled="uploading"
|
||
>
|
||
<el-option label="自動偵測" value="auto" />
|
||
<el-option label="繁體中文" value="zh-TW" />
|
||
<el-option label="簡體中文" value="zh-CN" />
|
||
<el-option label="英文" value="en" />
|
||
<el-option label="日文" value="ja" />
|
||
<el-option label="韓文" value="ko" />
|
||
<el-option label="越南文" value="vi" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="目標語言" prop="targetLanguages">
|
||
<el-select
|
||
v-model="translationForm.targetLanguages"
|
||
multiple
|
||
placeholder="請選擇目標語言(可多選)"
|
||
style="width: 100%"
|
||
:disabled="uploading"
|
||
collapse-tags
|
||
collapse-tags-tooltip
|
||
>
|
||
<el-option label="英文" value="en" />
|
||
<el-option label="越南文" value="vi" />
|
||
<el-option label="繁體中文" value="zh-TW" />
|
||
<el-option label="簡體中文" value="zh-CN" />
|
||
<el-option label="日文" value="ja" />
|
||
<el-option label="韓文" value="ko" />
|
||
<el-option label="泰文" value="th" />
|
||
<el-option label="印尼文" value="id" />
|
||
<el-option label="馬來文" value="ms" />
|
||
</el-select>
|
||
<div class="form-tip">
|
||
<el-icon><InfoFilled /></el-icon>
|
||
可以同時選擇多個目標語言,系統會分別生成對應的翻譯檔案
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-form-item>
|
||
<div class="translation-actions">
|
||
<el-button
|
||
type="primary"
|
||
size="large"
|
||
:loading="uploading"
|
||
:disabled="selectedFiles.length === 0 || translationForm.targetLanguages.length === 0"
|
||
@click="startTranslation"
|
||
>
|
||
<el-icon><Upload /></el-icon>
|
||
{{ uploading ? '上傳中...' : `開始翻譯 (${selectedFiles.length} 個檔案)` }}
|
||
</el-button>
|
||
|
||
<el-button size="large" @click="resetForm" :disabled="uploading">
|
||
重置
|
||
</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 上傳進度 -->
|
||
<div class="content-card" v-if="uploading || uploadResults.length > 0">
|
||
<div class="card-header">
|
||
<h3 class="card-title">上傳進度</h3>
|
||
</div>
|
||
|
||
<div class="card-body">
|
||
<div class="upload-progress">
|
||
<!-- 總體進度 -->
|
||
<div class="overall-progress" v-if="uploading">
|
||
<div class="progress-info">
|
||
<span>整體進度: {{ currentFileIndex + 1 }} / {{ selectedFiles.length }}</span>
|
||
<span>{{ Math.round(overallProgress) }}%</span>
|
||
</div>
|
||
<el-progress
|
||
:percentage="overallProgress"
|
||
:stroke-width="8"
|
||
:show-text="false"
|
||
status="success"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 個別檔案進度 -->
|
||
<div class="files-progress">
|
||
<div
|
||
v-for="(result, index) in uploadResults"
|
||
:key="index"
|
||
class="file-progress-item"
|
||
:class="result.status"
|
||
>
|
||
<div class="file-info">
|
||
<div class="file-icon">
|
||
<div class="file-type" :class="getFileExtension(result.filename)">
|
||
{{ getFileExtension(result.filename).toUpperCase() }}
|
||
</div>
|
||
</div>
|
||
<div class="file-details">
|
||
<div class="file-name">{{ result.filename }}</div>
|
||
<div class="file-status">
|
||
<el-icon v-if="result.status === 'success'">
|
||
<SuccessFilled />
|
||
</el-icon>
|
||
<el-icon v-else-if="result.status === 'error'">
|
||
<CircleCloseFilled />
|
||
</el-icon>
|
||
<el-icon v-else>
|
||
<Loading />
|
||
</el-icon>
|
||
<span>{{ getUploadStatusText(result.status) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="file-progress" v-if="result.status === 'uploading'">
|
||
<el-progress
|
||
:percentage="result.progress || 0"
|
||
:stroke-width="4"
|
||
:show-text="false"
|
||
/>
|
||
</div>
|
||
|
||
<div class="file-actions" v-if="result.status === 'success'">
|
||
<el-button
|
||
type="text"
|
||
size="small"
|
||
@click="viewJob(result.jobUuid)"
|
||
>
|
||
查看任務
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 完成後的操作 -->
|
||
<div class="upload-complete-actions" v-if="!uploading && uploadResults.length > 0">
|
||
<el-button type="primary" @click="$router.push('/jobs')">
|
||
<el-icon><List /></el-icon>
|
||
查看所有任務
|
||
</el-button>
|
||
<el-button @click="resetUpload">
|
||
<el-icon><RefreshRight /></el-icon>
|
||
重新上傳
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { useJobsStore } from '@/stores/jobs'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import {
|
||
List, UploadFilled, Delete, Close, Upload, InfoFilled,
|
||
SuccessFilled, CircleCloseFilled, Loading, RefreshRight
|
||
} from '@element-plus/icons-vue'
|
||
|
||
// Router 和 Stores
|
||
const router = useRouter()
|
||
const jobsStore = useJobsStore()
|
||
|
||
// 組件引用
|
||
const uploadRef = ref()
|
||
const translationFormRef = ref()
|
||
|
||
// 響應式數據
|
||
const selectedFiles = ref([])
|
||
const uploading = ref(false)
|
||
const currentFileIndex = ref(0)
|
||
const uploadResults = ref([])
|
||
|
||
// 表單數據
|
||
const translationForm = reactive({
|
||
sourceLanguage: 'auto',
|
||
targetLanguages: []
|
||
})
|
||
|
||
// 表單驗證規則
|
||
const translationRules = {
|
||
targetLanguages: [
|
||
{ required: true, message: '請至少選擇一個目標語言', trigger: 'change' },
|
||
{
|
||
type: 'array',
|
||
min: 1,
|
||
message: '請至少選擇一個目標語言',
|
||
trigger: 'change'
|
||
}
|
||
]
|
||
}
|
||
|
||
// 支援的檔案類型
|
||
const supportedTypes = {
|
||
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||
'doc': 'application/msword',
|
||
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||
'ppt': 'application/vnd.ms-powerpoint',
|
||
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
'xls': 'application/vnd.ms-excel',
|
||
'pdf': 'application/pdf'
|
||
}
|
||
|
||
// 計算屬性
|
||
const overallProgress = computed(() => {
|
||
if (uploadResults.value.length === 0) return 0
|
||
|
||
const totalProgress = uploadResults.value.reduce((sum, result) => {
|
||
if (result.status === 'success') return sum + 100
|
||
if (result.status === 'error') return sum + 100
|
||
return sum + (result.progress || 0)
|
||
}, 0)
|
||
|
||
return (totalProgress / selectedFiles.value.length)
|
||
})
|
||
|
||
// 方法
|
||
const handleBeforeUpload = (file) => {
|
||
// 檢查檔案類型
|
||
const extension = getFileExtension(file.name)
|
||
if (!supportedTypes[extension]) {
|
||
ElMessage.error(`不支援的檔案類型: ${extension}`)
|
||
return false
|
||
}
|
||
|
||
// 檢查檔案大小
|
||
const maxSize = 25 * 1024 * 1024 // 25MB
|
||
if (file.size > maxSize) {
|
||
ElMessage.error(`檔案大小不能超過 25MB,當前檔案: ${formatFileSize(file.size)}`)
|
||
return false
|
||
}
|
||
|
||
// 檢查是否已存在
|
||
const exists = selectedFiles.value.some(f => f.name === file.name)
|
||
if (exists) {
|
||
ElMessage.warning('檔案已存在於列表中')
|
||
return false
|
||
}
|
||
|
||
// 添加到選擇列表
|
||
selectedFiles.value.push(file)
|
||
ElMessage.success(`已添加檔案: ${file.name}`)
|
||
|
||
return false // 阻止自動上傳
|
||
}
|
||
|
||
const removeFile = (index) => {
|
||
const filename = selectedFiles.value[index].name
|
||
selectedFiles.value.splice(index, 1)
|
||
ElMessage.info(`已移除檔案: ${filename}`)
|
||
}
|
||
|
||
const clearFiles = async () => {
|
||
try {
|
||
await ElMessageBox.confirm('確定要清空所有已選檔案嗎?', '確認清空', {
|
||
confirmButtonText: '確定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
|
||
selectedFiles.value = []
|
||
ElMessage.success('已清空檔案列表')
|
||
} catch (error) {
|
||
// 用戶取消
|
||
}
|
||
}
|
||
|
||
const startTranslation = async () => {
|
||
try {
|
||
// 驗證表單
|
||
const valid = await translationFormRef.value.validate()
|
||
if (!valid) {
|
||
return
|
||
}
|
||
|
||
if (selectedFiles.value.length === 0) {
|
||
ElMessage.warning('請先選擇要翻譯的檔案')
|
||
return
|
||
}
|
||
|
||
// 開始上傳
|
||
uploading.value = true
|
||
currentFileIndex.value = 0
|
||
uploadResults.value = []
|
||
|
||
// 為每個檔案創建上傳記錄
|
||
selectedFiles.value.forEach(file => {
|
||
uploadResults.value.push({
|
||
filename: file.name,
|
||
status: 'waiting',
|
||
progress: 0,
|
||
jobUuid: null,
|
||
error: null
|
||
})
|
||
})
|
||
|
||
// 逐個上傳檔案
|
||
for (let i = 0; i < selectedFiles.value.length; i++) {
|
||
currentFileIndex.value = i
|
||
const file = selectedFiles.value[i]
|
||
const resultIndex = i
|
||
|
||
try {
|
||
// 更新狀態為上傳中
|
||
uploadResults.value[resultIndex].status = 'uploading'
|
||
|
||
// 創建 FormData
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
formData.append('source_language', translationForm.sourceLanguage)
|
||
formData.append('target_languages', JSON.stringify(translationForm.targetLanguages))
|
||
|
||
// 上傳檔案
|
||
const result = await jobsStore.uploadFile(formData, (progress) => {
|
||
uploadResults.value[resultIndex].progress = progress
|
||
})
|
||
|
||
// 上傳成功
|
||
uploadResults.value[resultIndex].status = 'success'
|
||
uploadResults.value[resultIndex].progress = 100
|
||
uploadResults.value[resultIndex].jobUuid = result.job_uuid
|
||
|
||
} catch (error) {
|
||
console.error(`檔案 ${file.name} 上傳失敗:`, error)
|
||
uploadResults.value[resultIndex].status = 'error'
|
||
uploadResults.value[resultIndex].error = error.message || '上傳失敗'
|
||
ElMessage.error(`檔案 ${file.name} 上傳失敗: ${error.message || '未知錯誤'}`)
|
||
}
|
||
}
|
||
|
||
// 檢查上傳結果
|
||
const successCount = uploadResults.value.filter(r => r.status === 'success').length
|
||
const failCount = uploadResults.value.filter(r => r.status === 'error').length
|
||
|
||
if (successCount > 0) {
|
||
ElMessage.success(`成功上傳 ${successCount} 個檔案`)
|
||
}
|
||
|
||
if (failCount > 0) {
|
||
ElMessage.error(`${failCount} 個檔案上傳失敗`)
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('批量上傳失敗:', error)
|
||
ElMessage.error('批量上傳失敗')
|
||
} finally {
|
||
uploading.value = false
|
||
}
|
||
}
|
||
|
||
const resetForm = () => {
|
||
selectedFiles.value = []
|
||
translationForm.sourceLanguage = 'auto'
|
||
translationForm.targetLanguages = []
|
||
uploadResults.value = []
|
||
translationFormRef.value?.resetFields()
|
||
}
|
||
|
||
const resetUpload = () => {
|
||
uploadResults.value = []
|
||
currentFileIndex.value = 0
|
||
}
|
||
|
||
const viewJob = (jobUuid) => {
|
||
router.push(`/job/${jobUuid}`)
|
||
}
|
||
|
||
const getFileExtension = (filename) => {
|
||
return filename.split('.').pop().toLowerCase()
|
||
}
|
||
|
||
const getFileTypeText = (filename) => {
|
||
const ext = getFileExtension(filename)
|
||
const typeMap = {
|
||
'docx': 'Word 文件',
|
||
'doc': 'Word 文件',
|
||
'pptx': 'PowerPoint 簡報',
|
||
'ppt': 'PowerPoint 簡報',
|
||
'xlsx': 'Excel 試算表',
|
||
'xls': 'Excel 試算表',
|
||
'pdf': 'PDF 文件'
|
||
}
|
||
return typeMap[ext] || ext.toUpperCase()
|
||
}
|
||
|
||
const formatFileSize = (bytes) => {
|
||
if (bytes === 0) return '0 B'
|
||
|
||
const k = 1024
|
||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||
}
|
||
|
||
const getUploadStatusText = (status) => {
|
||
const statusMap = {
|
||
'waiting': '等待中',
|
||
'uploading': '上傳中',
|
||
'success': '上傳成功',
|
||
'error': '上傳失敗'
|
||
}
|
||
return statusMap[status] || status
|
||
}
|
||
|
||
// 生命週期
|
||
onMounted(() => {
|
||
// 載入使用者偏好設定(如果有的話)
|
||
const savedSettings = localStorage.getItem('translation_settings')
|
||
if (savedSettings) {
|
||
try {
|
||
const settings = JSON.parse(savedSettings)
|
||
translationForm.sourceLanguage = settings.sourceLanguage || 'auto'
|
||
translationForm.targetLanguages = settings.targetLanguages || []
|
||
} catch (error) {
|
||
console.error('載入設定失敗:', error)
|
||
}
|
||
}
|
||
})
|
||
|
||
// 監聽表單變化,保存設定
|
||
watch([() => translationForm.sourceLanguage, () => translationForm.targetLanguages], () => {
|
||
const settings = {
|
||
sourceLanguage: translationForm.sourceLanguage,
|
||
targetLanguages: translationForm.targetLanguages
|
||
}
|
||
localStorage.setItem('translation_settings', JSON.stringify(settings))
|
||
}, { deep: true })
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.upload-view {
|
||
.upload-content {
|
||
.content-card {
|
||
&:not(:last-child) {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.card-subtitle {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
margin-top: 4px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.upload-dragger {
|
||
:deep(.el-upload-dragger) {
|
||
border: 2px dashed var(--el-border-color);
|
||
border-radius: 8px;
|
||
width: 100%;
|
||
height: 200px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
border-color: var(--el-color-primary);
|
||
background-color: var(--el-color-primary-light-9);
|
||
}
|
||
|
||
&.is-dragover {
|
||
border-color: var(--el-color-primary);
|
||
background-color: var(--el-color-primary-light-8);
|
||
}
|
||
}
|
||
|
||
&.disabled :deep(.el-upload-dragger) {
|
||
cursor: not-allowed;
|
||
opacity: 0.6;
|
||
|
||
&:hover {
|
||
border-color: var(--el-border-color);
|
||
background-color: transparent;
|
||
}
|
||
}
|
||
|
||
.upload-content-inner {
|
||
text-align: center;
|
||
|
||
.upload-icon {
|
||
font-size: 48px;
|
||
color: var(--el-color-primary);
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.upload-title {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
color: var(--el-text-color-primary);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.upload-hint {
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
}
|
||
}
|
||
|
||
.selected-files {
|
||
margin-top: 24px;
|
||
|
||
.files-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
|
||
h4 {
|
||
margin: 0;
|
||
color: var(--el-text-color-primary);
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
.files-list {
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
|
||
.file-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
transition: background-color 0.3s ease;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
&:hover {
|
||
background-color: var(--el-fill-color-light);
|
||
}
|
||
|
||
.file-icon {
|
||
margin-right: 12px;
|
||
|
||
.file-type {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 10px;
|
||
font-weight: bold;
|
||
color: white;
|
||
|
||
&.docx, &.doc {
|
||
background-color: #2b579a;
|
||
}
|
||
|
||
&.pptx, &.ppt {
|
||
background-color: #d24726;
|
||
}
|
||
|
||
&.xlsx, &.xls {
|
||
background-color: #207245;
|
||
}
|
||
|
||
&.pdf {
|
||
background-color: #ff0000;
|
||
}
|
||
}
|
||
}
|
||
|
||
.file-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
|
||
.file-name {
|
||
font-weight: 500;
|
||
color: var(--el-text-color-primary);
|
||
margin-bottom: 2px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.file-details {
|
||
display: flex;
|
||
gap: 16px;
|
||
font-size: 13px;
|
||
color: var(--el-text-color-secondary);
|
||
|
||
@media (max-width: 480px) {
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.form-tip {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 6px;
|
||
margin-top: 8px;
|
||
padding: 8px 12px;
|
||
background-color: var(--el-color-info-light-9);
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
color: var(--el-color-info);
|
||
line-height: 1.4;
|
||
|
||
.el-icon {
|
||
margin-top: 1px;
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
.translation-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
|
||
@media (max-width: 480px) {
|
||
flex-direction: column;
|
||
|
||
.el-button {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
|
||
.upload-progress {
|
||
.overall-progress {
|
||
margin-bottom: 24px;
|
||
|
||
.progress-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
font-size: 14px;
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
}
|
||
|
||
.files-progress {
|
||
.file-progress-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
&.success {
|
||
.file-status {
|
||
color: var(--el-color-success);
|
||
}
|
||
}
|
||
|
||
&.error {
|
||
.file-status {
|
||
color: var(--el-color-danger);
|
||
}
|
||
}
|
||
|
||
&.uploading {
|
||
.file-status {
|
||
color: var(--el-color-primary);
|
||
}
|
||
}
|
||
|
||
.file-info {
|
||
display: flex;
|
||
align-items: center;
|
||
flex: 1;
|
||
min-width: 0;
|
||
|
||
.file-icon {
|
||
margin-right: 12px;
|
||
|
||
.file-type {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 9px;
|
||
font-weight: bold;
|
||
color: white;
|
||
|
||
&.docx, &.doc { background-color: #2b579a; }
|
||
&.pptx, &.ppt { background-color: #d24726; }
|
||
&.xlsx, &.xls { background-color: #207245; }
|
||
&.pdf { background-color: #ff0000; }
|
||
}
|
||
}
|
||
|
||
.file-details {
|
||
flex: 1;
|
||
min-width: 0;
|
||
|
||
.file-name {
|
||
font-weight: 500;
|
||
color: var(--el-text-color-primary);
|
||
margin-bottom: 2px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.file-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 13px;
|
||
|
||
.el-icon {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.file-progress {
|
||
width: 120px;
|
||
margin: 0 16px;
|
||
}
|
||
|
||
.file-actions {
|
||
margin-left: 16px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.upload-complete-actions {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
margin-top: 24px;
|
||
|
||
@media (max-width: 480px) {
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style> |