1panel
This commit is contained in:
850
frontend/src/views/HistoryView.vue
Normal file
850
frontend/src/views/HistoryView.vue
Normal file
@@ -0,0 +1,850 @@
|
||||
<template>
|
||||
<div class="history-view">
|
||||
<!-- 頁面標題 -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">歷史記錄</h1>
|
||||
<div class="page-actions">
|
||||
<el-button @click="exportHistory">
|
||||
<el-icon><Download /></el-icon>
|
||||
匯出記錄
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 篩選區域 -->
|
||||
<div class="content-card">
|
||||
<div class="filters-section">
|
||||
<div class="filters-row">
|
||||
<div class="filter-group">
|
||||
<label>時間範圍:</label>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
start-placeholder="開始日期"
|
||||
end-placeholder="結束日期"
|
||||
format="YYYY/MM/DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
@change="handleDateRangeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>狀態:</label>
|
||||
<el-select v-model="filters.status" @change="handleFilterChange">
|
||||
<el-option label="全部" value="all" />
|
||||
<el-option label="已完成" value="COMPLETED" />
|
||||
<el-option label="失敗" value="FAILED" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>檔案類型:</label>
|
||||
<el-select v-model="filters.fileType" @change="handleFilterChange">
|
||||
<el-option label="全部" value="all" />
|
||||
<el-option label="Word" value="doc" />
|
||||
<el-option label="PowerPoint" value="ppt" />
|
||||
<el-option label="Excel" value="xls" />
|
||||
<el-option label="PDF" value="pdf" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="filter-actions">
|
||||
<el-button @click="clearFilters">
|
||||
<el-icon><Close /></el-icon>
|
||||
清除篩選
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-row">
|
||||
<el-input
|
||||
v-model="filters.search"
|
||||
placeholder="搜尋檔案名稱..."
|
||||
style="width: 300px"
|
||||
clearable
|
||||
@input="handleSearchChange"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 統計概覽 -->
|
||||
<div class="stats-section">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon primary">
|
||||
<el-icon><Files /></el-icon>
|
||||
</div>
|
||||
<div class="stat-value">{{ filteredJobs.length }}</div>
|
||||
<div class="stat-label">總記錄數</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success">
|
||||
<el-icon><SuccessFilled /></el-icon>
|
||||
</div>
|
||||
<div class="stat-value">{{ completedCount }}</div>
|
||||
<div class="stat-label">成功完成</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon warning">
|
||||
<el-icon><Money /></el-icon>
|
||||
</div>
|
||||
<div class="stat-value">${{ totalCost.toFixed(4) }}</div>
|
||||
<div class="stat-label">總成本</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon info">
|
||||
<el-icon><Clock /></el-icon>
|
||||
</div>
|
||||
<div class="stat-value">{{ avgProcessingTime }}</div>
|
||||
<div class="stat-label">平均處理時間</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 歷史記錄列表 -->
|
||||
<div class="content-card">
|
||||
<div class="card-body">
|
||||
<div v-if="loading" class="loading-state">
|
||||
<el-skeleton :rows="5" animated />
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredJobs.length === 0" class="empty-state">
|
||||
<el-icon class="empty-icon"><Document /></el-icon>
|
||||
<div class="empty-title">無歷史記錄</div>
|
||||
<div class="empty-description">
|
||||
在所選時間範圍內沒有找到符合條件的記錄
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- 表格模式 -->
|
||||
<div class="view-toggle">
|
||||
<el-radio-group v-model="viewMode">
|
||||
<el-radio-button label="table">表格檢視</el-radio-button>
|
||||
<el-radio-button label="card">卡片檢視</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 表格檢視 -->
|
||||
<div v-if="viewMode === 'table'" class="table-view">
|
||||
<el-table :data="paginatedJobs" style="width: 100%">
|
||||
<el-table-column prop="original_filename" label="檔案名稱" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="file-info">
|
||||
<div class="file-icon" :class="getFileExtension(row.original_filename)">
|
||||
{{ getFileExtension(row.original_filename).toUpperCase() }}
|
||||
</div>
|
||||
<span class="file-name">{{ row.original_filename }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="file_size" label="檔案大小" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ formatFileSize(row.file_size) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="target_languages" label="翻譯語言" width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="language-tags">
|
||||
<el-tag
|
||||
v-for="lang in row.target_languages.slice(0, 2)"
|
||||
:key="lang"
|
||||
size="small"
|
||||
type="primary"
|
||||
>
|
||||
{{ getLanguageText(lang) }}
|
||||
</el-tag>
|
||||
<el-tag v-if="row.target_languages.length > 2" size="small" type="info">
|
||||
+{{ row.target_languages.length - 2 }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="status" label="狀態" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusTagType(row.status)" size="small">
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="total_cost" label="成本" width="100">
|
||||
<template #default="{ row }">
|
||||
${{ (row.total_cost || 0).toFixed(4) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="created_at" label="建立時間" width="130">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="completed_at" label="完成時間" width="130">
|
||||
<template #default="{ row }">
|
||||
{{ row.completed_at ? formatDate(row.completed_at) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="table-actions">
|
||||
<el-button type="text" size="small" @click="viewJobDetail(row.job_uuid)">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'COMPLETED'"
|
||||
type="text"
|
||||
size="small"
|
||||
@click="downloadJob(row)"
|
||||
>
|
||||
下載
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 卡片檢視 -->
|
||||
<div v-else class="card-view">
|
||||
<div class="history-cards">
|
||||
<div
|
||||
v-for="job in paginatedJobs"
|
||||
:key="job.job_uuid"
|
||||
class="history-card"
|
||||
@click="viewJobDetail(job.job_uuid)"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="file-info">
|
||||
<div class="file-icon" :class="getFileExtension(job.original_filename)">
|
||||
{{ getFileExtension(job.original_filename).toUpperCase() }}
|
||||
</div>
|
||||
<div class="file-details">
|
||||
<div class="file-name">{{ job.original_filename }}</div>
|
||||
<div class="file-meta">
|
||||
{{ formatFileSize(job.file_size) }} •
|
||||
{{ formatDate(job.created_at) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-status">
|
||||
<el-tag :type="getStatusTagType(job.status)" size="small">
|
||||
{{ getStatusText(job.status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="languages-section">
|
||||
<div class="language-label">翻譯語言:</div>
|
||||
<div class="language-tags">
|
||||
<span
|
||||
v-for="lang in job.target_languages"
|
||||
:key="lang"
|
||||
class="language-tag"
|
||||
>
|
||||
{{ getLanguageText(lang) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-section">
|
||||
<div class="stat-item" v-if="job.total_cost > 0">
|
||||
<span class="stat-label">成本:</span>
|
||||
<span class="stat-value">${{ job.total_cost.toFixed(4) }}</span>
|
||||
</div>
|
||||
<div class="stat-item" v-if="job.total_tokens > 0">
|
||||
<span class="stat-label">Token:</span>
|
||||
<span class="stat-value">{{ job.total_tokens.toLocaleString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer" v-if="job.completed_at || job.processing_started_at">
|
||||
<div class="time-info">
|
||||
<div v-if="job.processing_started_at && job.completed_at">
|
||||
處理時間: {{ calculateProcessingTime(job.processing_started_at, job.completed_at) }}
|
||||
</div>
|
||||
<div v-if="job.completed_at">
|
||||
完成時間: {{ formatTime(job.completed_at) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions" @click.stop>
|
||||
<el-button
|
||||
v-if="job.status === 'COMPLETED'"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="downloadJob(job)"
|
||||
>
|
||||
下載
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分頁 -->
|
||||
<div class="pagination-section" v-if="totalPages > 1">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:total="filteredJobs.length"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useJobsStore } from '@/stores/jobs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { debounce } from 'lodash-es'
|
||||
import {
|
||||
Download, Close, Search, Files, SuccessFilled, Money, Clock, Document
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
// Router 和 Store
|
||||
const router = useRouter()
|
||||
const jobsStore = useJobsStore()
|
||||
|
||||
// 響應式數據
|
||||
const loading = ref(false)
|
||||
const viewMode = ref('table')
|
||||
const dateRange = ref([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
const filters = ref({
|
||||
status: 'all',
|
||||
fileType: 'all',
|
||||
search: ''
|
||||
})
|
||||
|
||||
// 語言映射
|
||||
const languageMap = {
|
||||
'zh-TW': '繁中',
|
||||
'zh-CN': '簡中',
|
||||
'en': '英文',
|
||||
'ja': '日文',
|
||||
'ko': '韓文',
|
||||
'vi': '越文'
|
||||
}
|
||||
|
||||
// 計算屬性
|
||||
const allJobs = computed(() => jobsStore.jobs.filter(job =>
|
||||
job.status === 'COMPLETED' || job.status === 'FAILED'
|
||||
))
|
||||
|
||||
const filteredJobs = computed(() => {
|
||||
let jobs = allJobs.value
|
||||
|
||||
// 狀態篩選
|
||||
if (filters.value.status !== 'all') {
|
||||
jobs = jobs.filter(job => job.status === filters.value.status)
|
||||
}
|
||||
|
||||
// 檔案類型篩選
|
||||
if (filters.value.fileType !== 'all') {
|
||||
jobs = jobs.filter(job => {
|
||||
const ext = getFileExtension(job.original_filename)
|
||||
switch (filters.value.fileType) {
|
||||
case 'doc': return ['docx', 'doc'].includes(ext)
|
||||
case 'ppt': return ['pptx', 'ppt'].includes(ext)
|
||||
case 'xls': return ['xlsx', 'xls'].includes(ext)
|
||||
case 'pdf': return ext === 'pdf'
|
||||
default: return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 日期範圍篩選
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
const [startDate, endDate] = dateRange.value
|
||||
jobs = jobs.filter(job => {
|
||||
const jobDate = new Date(job.created_at).toDateString()
|
||||
return jobDate >= new Date(startDate).toDateString() &&
|
||||
jobDate <= new Date(endDate).toDateString()
|
||||
})
|
||||
}
|
||||
|
||||
// 搜尋篩選
|
||||
if (filters.value.search.trim()) {
|
||||
const searchTerm = filters.value.search.toLowerCase().trim()
|
||||
jobs = jobs.filter(job =>
|
||||
job.original_filename.toLowerCase().includes(searchTerm)
|
||||
)
|
||||
}
|
||||
|
||||
return jobs.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
|
||||
})
|
||||
|
||||
const paginatedJobs = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return filteredJobs.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
|
||||
const totalPages = computed(() => Math.ceil(filteredJobs.value.length / pageSize.value))
|
||||
|
||||
const completedCount = computed(() =>
|
||||
filteredJobs.value.filter(job => job.status === 'COMPLETED').length
|
||||
)
|
||||
|
||||
const totalCost = computed(() =>
|
||||
filteredJobs.value.reduce((sum, job) => sum + (job.total_cost || 0), 0)
|
||||
)
|
||||
|
||||
const avgProcessingTime = computed(() => {
|
||||
const completedJobs = filteredJobs.value.filter(job =>
|
||||
job.status === 'COMPLETED' && job.processing_started_at && job.completed_at
|
||||
)
|
||||
|
||||
if (completedJobs.length === 0) return '無資料'
|
||||
|
||||
const totalMs = completedJobs.reduce((sum, job) => {
|
||||
const startTime = new Date(job.processing_started_at)
|
||||
const endTime = new Date(job.completed_at)
|
||||
return sum + (endTime - startTime)
|
||||
}, 0)
|
||||
|
||||
const avgMs = totalMs / completedJobs.length
|
||||
const minutes = Math.floor(avgMs / 60000)
|
||||
const seconds = Math.floor((avgMs % 60000) / 1000)
|
||||
|
||||
return `${minutes}分${seconds}秒`
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleFilterChange = () => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const handleSearchChange = debounce(() => {
|
||||
currentPage.value = 1
|
||||
}, 300)
|
||||
|
||||
const handleDateRangeChange = () => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
filters.value.status = 'all'
|
||||
filters.value.fileType = 'all'
|
||||
filters.value.search = ''
|
||||
dateRange.value = []
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const viewJobDetail = (jobUuid) => {
|
||||
router.push(`/job/${jobUuid}`)
|
||||
}
|
||||
|
||||
const downloadJob = async (job) => {
|
||||
try {
|
||||
if (job.target_languages.length === 1) {
|
||||
const originalExt = getFileExtension(job.original_filename)
|
||||
const translatedExt = getTranslatedFileExtension(originalExt)
|
||||
const filename = `${job.original_filename.replace(/\.[^/.]+$/, '')}_${job.target_languages[0]}_translated.${translatedExt}`
|
||||
await jobsStore.downloadFile(job.job_uuid, job.target_languages[0], filename)
|
||||
} else {
|
||||
const filename = `${job.original_filename.replace(/\.[^/.]+$/, '')}_translated.zip`
|
||||
await jobsStore.downloadAllFiles(job.job_uuid, filename)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下載失敗:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const exportHistory = () => {
|
||||
// 匯出 CSV 格式的歷史記錄
|
||||
const csvContent = [
|
||||
['檔案名稱', '檔案大小', '目標語言', '狀態', '成本', '建立時間', '完成時間'].join(','),
|
||||
...filteredJobs.value.map(job => [
|
||||
`"${job.original_filename}"`,
|
||||
formatFileSize(job.file_size),
|
||||
`"${job.target_languages.join(', ')}"`,
|
||||
getStatusText(job.status),
|
||||
(job.total_cost || 0).toFixed(4),
|
||||
formatDate(job.created_at),
|
||||
job.completed_at ? formatDate(job.completed_at) : ''
|
||||
].join(','))
|
||||
].join('\n')
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' })
|
||||
const link = document.createElement('a')
|
||||
const url = URL.createObjectURL(blob)
|
||||
link.setAttribute('href', url)
|
||||
link.setAttribute('download', `翻譯歷史記錄_${new Date().toISOString().slice(0, 10)}.csv`)
|
||||
link.click()
|
||||
|
||||
ElMessage.success('歷史記錄已匯出')
|
||||
}
|
||||
|
||||
const getFileExtension = (filename) => {
|
||||
return filename.split('.').pop().toLowerCase()
|
||||
}
|
||||
|
||||
const getTranslatedFileExtension = (originalExt) => {
|
||||
// PDF 翻譯後變成 DOCX
|
||||
if (originalExt === 'pdf') {
|
||||
return 'docx'
|
||||
}
|
||||
// 其他格式保持不變
|
||||
return originalExt
|
||||
}
|
||||
|
||||
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 formatDate = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleDateString('zh-TW', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
const now = new Date()
|
||||
const time = new Date(timestamp)
|
||||
const diff = now - time
|
||||
|
||||
if (diff < 86400000) return '今天'
|
||||
if (diff < 172800000) return '昨天'
|
||||
if (diff < 2592000000) return `${Math.floor(diff / 86400000)}天前`
|
||||
|
||||
return time.toLocaleDateString('zh-TW')
|
||||
}
|
||||
|
||||
const calculateProcessingTime = (startTime, endTime) => {
|
||||
const start = new Date(startTime)
|
||||
const end = new Date(endTime)
|
||||
const diff = end - start
|
||||
|
||||
const minutes = Math.floor(diff / 60000)
|
||||
const seconds = Math.floor((diff % 60000) / 1000)
|
||||
|
||||
return `${minutes}分${seconds}秒`
|
||||
}
|
||||
|
||||
const getLanguageText = (langCode) => {
|
||||
return languageMap[langCode] || langCode
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'COMPLETED': '已完成',
|
||||
'FAILED': '失敗'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
const getStatusTagType = (status) => {
|
||||
const typeMap = {
|
||||
'COMPLETED': 'success',
|
||||
'FAILED': 'danger'
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 生命週期
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await jobsStore.fetchJobs({ per_page: 100 })
|
||||
} catch (error) {
|
||||
console.error('載入歷史記錄失敗:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// 監聽檢視模式變化,重置分頁
|
||||
watch(viewMode, () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.history-view {
|
||||
.filters-section {
|
||||
.filters-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-left: auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
margin: 24px 0;
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.table-view {
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.file-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 8px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.docx, &.doc { background-color: #2b579a; }
|
||||
&.pptx, &.ppt { background-color: #d24726; }
|
||||
&.xlsx, &.xls { background-color: #207245; }
|
||||
&.pdf { background-color: #ff0000; }
|
||||
}
|
||||
|
||||
.file-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.language-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-view {
|
||||
.history-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
gap: 16px;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.history-card {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: var(--el-bg-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.file-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.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: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.languages-section {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.language-label {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.language-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
|
||||
.language-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
background-color: var(--el-color-primary-light-8);
|
||||
color: var(--el-color-primary);
|
||||
border: 1px solid var(--el-color-primary-light-5);
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.stat-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.time-info {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-section {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
padding: 40px 0;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user