backup
This commit is contained in:
865
frontend/src/views/UploadView.vue
Normal file
865
frontend/src/views/UploadView.vue
Normal file
@@ -0,0 +1,865 @@
|
||||
<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>
|
Reference in New Issue
Block a user