850 lines
25 KiB
Vue
850 lines
25 KiB
Vue
<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> |