优化页面、修复BUG

This commit is contained in:
李琦
2026-06-24 16:50:04 +08:00
parent 88ba9be318
commit 3f653bc336
36 changed files with 1672 additions and 2401 deletions

View File

@@ -1,86 +1,107 @@
<template>
<Teleport to="body">
<div v-if="show" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" @click.self="handleClose">
<div class="admin-card max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-serif italic text-white">附件详情</h2>
<button @click="handleClose" class="text-white/40 hover:text-white transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div v-if="attachment" class="space-y-6">
<!-- 图片预览 -->
<div v-if="attachment.fileType === 'image'" class="w-full rounded-lg overflow-hidden bg-white/5">
<img :src="attachment.fileUrl" :alt="attachment.originalName" class="w-full h-auto max-h-96 object-contain" />
<div
v-if="show"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
@click.self="handleClose"
>
<div class="admin-card max-w-3xl w-full mx-4 max-h-[92vh] overflow-hidden flex flex-col border border-white/10 shadow-2xl">
<!-- Header -->
<div class="flex items-start justify-between gap-4 px-6 py-4 border-b border-white/10 shrink-0">
<div class="min-w-0">
<h2 class="text-xl font-serif italic text-white">附件详情</h2>
<p v-if="attachment" class="text-sm text-art-muted mt-1 truncate" :title="attachment.originalName">
{{ attachment.originalName }}
</p>
</div>
<button @click="handleClose" class="text-white/40 hover:text-white transition-colors shrink-0 p-1">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<!-- 文件信息 -->
<div class="space-y-4">
<div>
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider mb-2">文件名</label>
<p class="text-white">{{ attachment.originalName }}</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider mb-2">文件大小</label>
<p class="text-white">{{ formatFileSize(attachment.fileSize) }}</p>
</div>
<div>
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider mb-2">文件类型</label>
<p class="text-white">{{ getFileTypeLabel(attachment.fileType) }}</p>
</div>
</div>
<div v-if="attachment.mimeType">
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider mb-2">MIME类型</label>
<p class="text-white font-mono text-sm">{{ attachment.mimeType }}</p>
</div>
<div>
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider mb-2">存储类型</label>
<p class="text-white">{{ getStorageTypeLabel(attachment.storageType) }}</p>
</div>
<div>
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider mb-2">分类</label>
<CustomSelect
v-model="localCategoryId"
:options="categoryOptions"
placeholder="无分类"
/>
</div>
<div>
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider mb-2">文件URL</label>
<div class="flex gap-2">
<input
type="text"
:value="attachment.fileUrl"
readonly
class="admin-input flex-1 font-mono text-sm"
<div v-if="attachment" class="overflow-y-auto custom-scrollbar flex-1">
<!-- Media preview -->
<div class="px-6 pt-6">
<div class="rounded-xl overflow-hidden bg-black/40 border border-white/10 min-h-[200px] flex items-center justify-center">
<img
v-if="attachment.fileType === 'image'"
:src="attachment.fileUrl"
:alt="attachment.originalName"
class="w-full max-h-[420px] object-contain"
/>
<button
@click="handleCopyUrl"
class="admin-btn-secondary px-4 whitespace-nowrap"
>
复制
</button>
<video
v-else-if="attachment.fileType === 'video'"
:src="attachment.fileUrl"
controls
preload="metadata"
class="w-full max-h-[420px] bg-black"
/>
<div v-else class="flex flex-col items-center justify-center py-16 px-6 text-center">
<div class="w-20 h-20 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="text-art-muted">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
</div>
<p class="text-white font-medium mb-1">{{ attachment.originalName }}</p>
<p class="text-sm text-art-muted">此文件类型暂不支持预览</p>
</div>
</div>
</div>
<div v-if="attachment.createdAt" class="text-xs text-art-muted">
创建时间: {{ formatDate(attachment.createdAt) }}
<!-- Badges -->
<div class="px-6 pt-4 flex flex-wrap gap-2">
<span class="px-3 py-1 rounded-full text-xs font-medium border" :class="typeBadgeClass">
{{ getFileTypeLabel(attachment.fileType) }}
</span>
<span class="px-3 py-1 rounded-full text-xs bg-white/5 text-art-muted border border-white/10">
{{ formatFileSize(attachment.fileSize) }}
</span>
<span class="px-3 py-1 rounded-full text-xs bg-white/5 text-art-muted border border-white/10">
{{ getStorageTypeLabel(attachment.storageType) }}
</span>
</div>
<!-- Meta grid -->
<div class="px-6 py-5 grid sm:grid-cols-2 gap-4">
<div class="rounded-lg bg-white/[0.03] border border-white/5 p-4">
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider mb-2">MIME 类型</label>
<p class="text-white font-mono text-sm break-all">{{ attachment.mimeType || '—' }}</p>
</div>
<div class="rounded-lg bg-white/[0.03] border border-white/5 p-4">
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider mb-2">创建时间</label>
<p class="text-white text-sm">{{ attachment.createdAt ? formatDate(attachment.createdAt) : '—' }}</p>
</div>
<div class="sm:col-span-2 rounded-lg bg-white/[0.03] border border-white/5 p-4">
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider mb-2">分类</label>
<CustomSelect
v-model="localCategoryId"
:options="categoryOptions"
placeholder="无分类"
/>
</div>
<div class="sm:col-span-2 rounded-lg bg-white/[0.03] border border-white/5 p-4">
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider mb-2">文件 URL</label>
<div class="flex gap-2">
<input
type="text"
:value="attachment.fileUrl"
readonly
class="admin-input flex-1 font-mono text-sm"
/>
<button @click="handleCopyUrl" class="admin-btn-secondary px-4 whitespace-nowrap">
复制
</button>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex gap-3 pt-4 border-t border-white/10">
<!-- Footer -->
<div class="flex gap-3 px-6 py-4 border-t border-white/10 shrink-0">
<button @click="handleClose" class="admin-btn-secondary flex-1">
取消
</button>
@@ -90,7 +111,6 @@
</div>
</div>
</div>
</div>
</Teleport>
</template>
@@ -132,7 +152,6 @@ const emit = defineEmits<{
const localCategoryId = ref<number>(0)
const saving = ref(false)
// 计算属性:分类选项
const categoryOptions = computed(() => {
return [
{ value: 0, label: '无分类' },
@@ -140,7 +159,16 @@ const categoryOptions = computed(() => {
]
})
// 监听attachment变化更新本地分类ID
const typeBadgeClass = computed(() => {
const map: Record<string, string> = {
image: 'bg-emerald-500/10 text-emerald-300 border-emerald-500/30',
video: 'bg-violet-500/10 text-violet-300 border-violet-500/30',
document: 'bg-blue-500/10 text-blue-300 border-blue-500/30',
other: 'bg-white/5 text-art-muted border-white/10'
}
return map[props.attachment?.fileType || 'other'] || map.other
})
watch(() => props.attachment, (newAttachment) => {
if (newAttachment) {
localCategoryId.value = newAttachment.categoryId || 0
@@ -155,7 +183,6 @@ const handleSave = () => {
saving.value = true
const categoryId = localCategoryId.value === 0 ? null : localCategoryId.value
emit('save', { categoryId })
// saving状态由父组件控制
}
const handleCopyUrl = () => {
@@ -183,8 +210,8 @@ const getFileTypeLabel = (fileType: string): string => {
const getStorageTypeLabel = (storageType?: string): string => {
const labels: Record<string, string> = {
local: '本地存储',
qcloud: '腾讯云COS',
aliyun: '阿里云OSS',
qcloud: '腾讯云 COS',
aliyun: '阿里云 OSS',
qiniu: '七牛云'
}
return labels[storageType || 'local'] || storageType || '本地存储'
@@ -195,12 +222,15 @@ const formatDate = (timestamp: string | number): string => {
return date.toLocaleString('zh-CN')
}
// 暴露saving状态给父组件
defineExpose({
saving
})
defineExpose({ saving })
</script>
<style scoped>
/* 使用全局admin样式无需额外样式 */
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
</style>