优化页面、修复BUG

This commit is contained in:
李琦
2026-06-26 13:37:16 +08:00
parent 87fa638b24
commit 396272908b
19 changed files with 1105 additions and 410 deletions

View File

@@ -1,6 +1,6 @@
<template>
<div class="admin-table-filter admin-card mb-6 p-4">
<div class="filter-row">
<div class="filter-main">
<div v-if="showKeyword" class="filter-keyword">
<div class="relative flex-1 min-w-[200px]">
<input
@@ -26,27 +26,46 @@
</div>
</div>
<div class="filter-extra">
<slot name="filters" />
<div v-if="hasPrimaryFilters" class="filter-primary">
<slot name="primaryFilters" />
</div>
<button
v-if="hasFilters"
type="button"
class="filter-toggle admin-btn-secondary text-xs"
@click="expanded = !expanded"
>
{{ expanded ? '收起筛选 ▲' : '展开筛选 ▼' }}
</button>
<div class="filter-actions">
<button type="button" class="admin-btn-secondary" @click="emit('search')">查询</button>
<button type="button" class="admin-btn-secondary opacity-70" @click="emit('reset')">重置</button>
</div>
</div>
<Transition name="filter-panel">
<div v-if="hasFilters && expanded" class="filter-panel">
<slot name="filters" />
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
import { computed, ref, useSlots, watch } from 'vue'
const props = withDefaults(defineProps<{
keyword?: string
keywordPlaceholder?: string
showKeyword?: boolean
defaultExpanded?: boolean
}>(), {
keyword: '',
keywordPlaceholder: '关键词搜索...',
showKeyword: true,
defaultExpanded: false,
})
const emit = defineEmits<{
@@ -55,6 +74,15 @@ const emit = defineEmits<{
reset: []
}>()
const slots = useSlots()
const hasFilters = computed(() => !!slots.filters)
const hasPrimaryFilters = computed(() => !!slots.primaryFilters)
const expanded = ref(props.defaultExpanded)
watch(() => props.defaultExpanded, (value) => {
expanded.value = value
})
const onKeywordInput = (e: Event) => {
emit('update:keyword', (e.target as HTMLInputElement).value)
}
@@ -66,7 +94,7 @@ const clearKeyword = () => {
</script>
<style scoped>
.filter-row {
.filter-main {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
@@ -74,21 +102,58 @@ const clearKeyword = () => {
}
.filter-keyword {
flex: 1 1 220px;
flex: 1 1 240px;
min-width: 200px;
max-width: 100%;
}
.filter-extra {
.filter-primary {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 0.75rem 1rem;
flex: 0 0 auto;
}
.filter-toggle {
flex-shrink: 0;
white-space: nowrap;
}
.filter-actions {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
margin-left: auto;
}
.filter-panel {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 0.75rem 1rem;
width: 100%;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.filter-panel-enter-active,
.filter-panel-leave-active {
transition: opacity 0.2s ease, max-height 0.25s ease;
overflow: hidden;
}
.filter-panel-enter-from,
.filter-panel-leave-to {
opacity: 0;
max-height: 0;
}
.filter-panel-enter-to,
.filter-panel-leave-from {
opacity: 1;
max-height: 500px;
}
.search-icon {
@@ -125,6 +190,8 @@ const clearKeyword = () => {
display: flex;
flex-direction: column;
gap: 0.35rem;
flex: 0 0 auto;
flex-shrink: 0;
min-width: 120px;
}

View File

@@ -5,7 +5,7 @@
class="fixed inset-0 z-[9998] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
@click.self="emit('close')"
>
<div class="w-full max-w-5xl max-h-[85vh] bg-[#121214] border border-white/10 rounded-xl shadow-2xl flex flex-col overflow-hidden">
<div class="w-full max-w-6xl max-h-[90vh] bg-[#121214] border border-white/10 rounded-xl shadow-2xl flex flex-col overflow-hidden">
<div class="flex items-center justify-between px-6 py-4 border-b border-white/10 shrink-0">
<div>
<h3 class="text-lg font-medium text-white">历史版本</h3>
@@ -28,6 +28,7 @@
</div>
<div class="flex-1 overflow-hidden flex min-h-0">
<!-- 左侧版本列表 -->
<div class="w-72 border-r border-white/10 overflow-y-auto custom-scrollbar shrink-0">
<div v-if="loading" class="p-6 text-center text-art-muted text-sm">加载中...</div>
<div v-else-if="historyList.length === 0" class="p-6 text-center text-art-muted text-sm">暂无历史版本</div>
@@ -56,80 +57,222 @@
</button>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar p-6">
<div v-if="!activeVersion" class="h-full flex items-center justify-center text-sm text-art-muted">
<!-- 右侧 Tab 面板 -->
<div class="flex-1 flex flex-col min-h-0 overflow-hidden">
<div v-if="!activeVersion" class="flex-1 flex items-center justify-center text-sm text-art-muted">
点击左侧版本查看内容与变更对比
</div>
<div v-else-if="detailLoading" class="h-full flex items-center justify-center text-sm text-art-muted">
<div v-else-if="detailLoading" class="flex-1 flex items-center justify-center text-sm text-art-muted">
加载版本详情...
</div>
<template v-else-if="previewData">
<section class="mb-8">
<h4 class="text-white font-medium mb-4">版本 v{{ previewData.version }} 内容</h4>
<div class="space-y-3 text-sm">
<div><span class="text-art-muted">标题</span><span class="text-white">{{ previewData.title }}</span></div>
<div><span class="text-art-muted">分类</span><span class="text-white">{{ previewData.categoryName || '—' }}</span></div>
<div><span class="text-art-muted">专栏</span><span class="text-white">{{ previewData.columnName || '—' }}</span></div>
<div><span class="text-art-muted">标签</span><span class="text-white">{{ previewData.tagNames?.join(', ') || '—' }}</span></div>
<div><span class="text-art-muted">状态</span><span class="text-white">{{ previewData.isPublished === 1 ? '已发布' : '草稿' }}</span></div>
<div class="flex items-start gap-2">
<span class="text-art-muted shrink-0">操作人</span>
<AuthorInfo
:name="previewData.userName || previewData.modifiedByName"
:email="previewData.userEmail"
:avatar="previewData.userAvatar"
:user-id="previewData.userId || previewData.modifiedBy"
variant="compact"
size="sm"
/>
<!-- Tab -->
<div class="flex border-b border-white/10 shrink-0">
<button
type="button"
class="px-5 py-2.5 text-sm transition-colors"
:class="activeMainTab === 'preview'
? 'text-art-accent bg-white/5 border-b-2 border-art-accent'
: 'text-white/40 hover:text-white/70'"
@click="activeMainTab = 'preview'"
>
版本内容
</button>
<button
type="button"
class="px-5 py-2.5 text-sm transition-colors flex items-center gap-2"
:class="activeMainTab === 'diff'
? 'text-art-accent bg-white/5 border-b-2 border-art-accent'
: 'text-white/40 hover:text-white/70'"
:disabled="activeVersion === 1"
@click="activeMainTab = 'diff'"
>
变更对比
<span
v-if="changedCount > 0"
class="text-[10px] px-1.5 py-0.5 rounded-full bg-art-accent/20 text-art-accent font-medium"
>
{{ changedCount }}
</span>
</button>
</div>
<!-- Tab: 版本内容 -->
<div
v-show="activeMainTab === 'preview'"
class="flex-1 flex flex-col min-h-0 overflow-hidden"
>
<div class="flex-1 overflow-y-auto custom-scrollbar p-6 flex flex-col min-h-0">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-3 text-sm shrink-0 mb-4">
<div class="md:col-span-2">
<span class="text-art-muted">标题</span>
<span class="text-white">{{ previewData.title }}</span>
</div>
<div>
<span class="text-art-muted">分类</span>
<span class="text-white">{{ previewData.categoryName || '—' }}</span>
</div>
<div>
<span class="text-art-muted">专栏</span>
<span class="text-white">{{ previewData.columnName || '—' }}</span>
</div>
<div>
<span class="text-art-muted">标签</span>
<span class="text-white">{{ previewData.tagNames?.join(', ') || '—' }}</span>
</div>
<div>
<span class="text-art-muted">状态</span>
<span class="text-white">{{ previewData.isPublished === 1 ? '已发布' : '草稿' }}</span>
</div>
<div class="flex items-start gap-2 md:col-span-2">
<span class="text-art-muted shrink-0">操作人</span>
<AuthorInfo
:name="previewData.userName || previewData.modifiedByName"
:email="previewData.userEmail"
:avatar="previewData.userAvatar"
:user-id="previewData.userId || previewData.modifiedBy"
variant="compact"
size="sm"
/>
</div>
<div class="md:col-span-2">
<span class="text-art-muted">摘要</span>
<span class="text-white">{{ previewData.excerpt || '—' }}</span>
</div>
</div>
<div><span class="text-art-muted">摘要</span><span class="text-white">{{ previewData.excerpt || '—' }}</span></div>
<div>
<span class="text-art-muted block mb-2">正文</span>
<div class="bg-black/30 rounded-lg p-4 max-h-80 overflow-y-auto prose prose-invert prose-sm max-w-none">
<div class="flex-1 flex flex-col min-h-0">
<span class="text-art-muted text-sm mb-2 shrink-0">正文</span>
<div class="flex-1 min-h-[200px] bg-black/30 rounded-lg p-4 overflow-y-auto custom-scrollbar prose prose-invert prose-sm max-w-none">
<MdPreview :model-value="previewData.content || ''" theme="dark" />
</div>
</div>
</div>
</section>
</div>
<section>
<h4 class="text-white font-medium mb-4">
<template v-if="activeVersion > 1">与上一版本对比v{{ activeVersion - 1 }} v{{ activeVersion }}</template>
<template v-else>版本对比</template>
</h4>
<div v-if="activeVersion === 1" class="text-sm text-art-muted rounded-lg border border-white/10 p-4 bg-white/[0.02]">
这是首个版本无上一版本可对比
</div>
<div v-else-if="diffData" class="space-y-4">
<div
v-for="(field, key) in diffData.fields"
:key="key"
class="rounded-lg border p-4"
:class="field.changed ? 'border-art-accent/30 bg-art-accent/5' : 'border-white/5 bg-white/[0.02]'"
>
<div class="text-xs uppercase tracking-wider text-art-muted mb-2">{{ fieldLabel(key) }}</div>
<template v-if="field.changed">
<div class="grid md:grid-cols-2 gap-3 text-xs">
<div>
<div class="text-art-muted mb-1">旧版</div>
<pre class="whitespace-pre-wrap bg-black/30 rounded p-2 text-white/70 max-h-40 overflow-y-auto">{{ field.from || '—' }}</pre>
</div>
<div>
<div class="text-art-muted mb-1">新版</div>
<pre class="whitespace-pre-wrap bg-black/30 rounded p-2 text-white/70 max-h-40 overflow-y-auto">{{ field.to || '—' }}</pre>
</div>
</div>
<pre v-if="field.diff && key === 'content'" class="mt-2 text-xs text-art-accent/80 whitespace-pre-wrap max-h-48 overflow-y-auto">{{ field.diff }}</pre>
</template>
<div v-else class="text-xs text-art-muted">无变化</div>
<!-- Tab: 变更对比 -->
<div
v-show="activeMainTab === 'diff'"
class="flex-1 flex flex-col min-h-0 overflow-hidden"
>
<!-- v1 empty state -->
<div
v-if="activeVersion === 1"
class="flex-1 flex items-center justify-center p-6"
>
<div class="text-sm text-art-muted rounded-lg border border-white/10 p-6 bg-white/[0.02] text-center max-w-md">
这是首个版本无上一版本可对比
</div>
</div>
</section>
<template v-else-if="diffData">
<!-- 摘要条 -->
<div class="px-6 py-3 border-b border-white/10 shrink-0 bg-white/[0.02] flex items-center gap-3">
<span class="text-sm text-white">
v{{ diffData.fromVersion }} v{{ diffData.toVersion }}
</span>
<span
v-if="changedCount > 0"
class="text-xs px-2 py-0.5 rounded-full bg-art-accent/15 text-art-accent"
>
{{ changedCount }} 项变更
</span>
<span v-else class="text-xs text-art-muted">无变更</span>
</div>
<!-- 无变更 empty state -->
<div
v-if="changedCount === 0"
class="flex-1 flex items-center justify-center p-6"
>
<div class="text-sm text-art-muted rounded-lg border border-white/10 p-6 bg-white/[0.02] text-center max-w-md">
与上一版本相比所有字段均无变化
</div>
</div>
<template v-else>
<!-- 字段子 Tab -->
<div class="flex gap-1 px-4 py-2 border-b border-white/10 shrink-0 overflow-x-auto custom-scrollbar">
<button
v-for="[key] in changedFields"
:key="key"
type="button"
class="shrink-0 px-3 py-1.5 text-xs rounded-md transition-colors"
:class="activeDiffField === key
? 'bg-art-accent/15 text-art-accent border border-art-accent/30'
: 'text-white/50 hover:text-white/80 hover:bg-white/5 border border-transparent'"
@click="activeDiffField = key"
>
{{ fieldLabel(key) }}
</button>
</div>
<!-- 双栏对比 -->
<div
v-if="activeDiffFieldData"
class="flex-1 flex flex-col md:flex-row min-h-0 overflow-hidden"
>
<!-- 旧版 -->
<div class="flex-1 flex flex-col min-h-0 md:border-r border-white/10">
<div class="px-4 py-2 text-xs text-art-muted border-b border-white/5 shrink-0 bg-black/20">
旧版 v{{ diffData.fromVersion }}
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar p-4 min-h-0">
<template v-if="activeDiffField === 'content'">
<div class="prose prose-invert prose-sm max-w-none">
<MdPreview :model-value="activeDiffFieldData.from || ''" theme="dark" />
</div>
</template>
<p v-else class="text-sm text-white/80 whitespace-pre-wrap break-words">
{{ activeDiffFieldData.from || '—' }}
</p>
</div>
</div>
<!-- 新版 -->
<div class="flex-1 flex flex-col min-h-0">
<div class="px-4 py-2 text-xs text-art-muted border-b border-white/5 shrink-0 bg-art-accent/5">
新版 v{{ diffData.toVersion }}
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar p-4 min-h-0">
<template v-if="activeDiffField === 'content'">
<div class="prose prose-invert prose-sm max-w-none">
<MdPreview :model-value="activeDiffFieldData.to || ''" theme="dark" />
</div>
</template>
<p v-else class="text-sm text-white/80 whitespace-pre-wrap break-words">
{{ activeDiffFieldData.to || '—' }}
</p>
</div>
</div>
</div>
<!-- 无变化字段折叠区 -->
<div v-if="unchangedCount > 0" class="shrink-0 border-t border-white/10">
<button
type="button"
class="w-full px-6 py-2.5 text-xs text-art-muted hover:text-white/70 hover:bg-white/[0.02] transition-colors flex items-center gap-2"
@click="showUnchanged = !showUnchanged"
>
<span>{{ showUnchanged ? '收起' : '展开' }}</span>
<span>{{ unchangedCount }} 项无变化</span>
<span class="ml-auto">{{ showUnchanged ? '▲' : '▼' }}</span>
</button>
<div v-if="showUnchanged" class="px-6 pb-4 flex flex-wrap gap-2">
<span
v-for="[key] in unchangedFields"
:key="key"
class="text-xs px-2 py-1 rounded bg-white/[0.03] border border-white/5 text-art-muted"
>
{{ fieldLabel(key) }}
</span>
</div>
</div>
</template>
</template>
</div>
</template>
</div>
</div>
@@ -139,7 +282,7 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { MdPreview } from 'md-editor-v3'
import 'md-editor-v3/lib/preview.css'
import {
@@ -148,7 +291,8 @@ import {
getPostHistoryDiff,
restorePostHistory,
type PostHistory,
type PostHistoryDiff
type PostHistoryDiff,
type PostHistoryFieldDiff
} from '../../services/api'
import { useToast } from '../../composables/useToast'
import AuthorInfo from '../AuthorInfo.vue'
@@ -180,9 +324,40 @@ const historyList = ref<PostHistory[]>([])
const activeVersion = ref<number | null>(null)
const previewData = ref<PostHistory | null>(null)
const diffData = ref<PostHistoryDiff | null>(null)
const activeMainTab = ref<'preview' | 'diff'>('preview')
const activeDiffField = ref<string | null>(null)
const showUnchanged = ref(false)
const fieldLabel = (key: string | number) => FIELD_LABELS[String(key)] || String(key)
const changedFields = computed(() =>
Object.entries(diffData.value?.fields ?? {}).filter(([, f]) => f.changed)
)
const unchangedFields = computed(() =>
Object.entries(diffData.value?.fields ?? {}).filter(([, f]) => !f.changed)
)
const changedCount = computed(() => changedFields.value.length)
const unchangedCount = computed(() => unchangedFields.value.length)
const activeDiffFieldData = computed((): PostHistoryFieldDiff | null => {
if (!activeDiffField.value || !diffData.value) return null
return diffData.value.fields[activeDiffField.value] ?? null
})
const applyTabDefaults = (version: number) => {
if (version > 1 && changedCount.value > 0) {
activeMainTab.value = 'diff'
activeDiffField.value = changedFields.value[0]?.[0] ?? null
} else {
activeMainTab.value = 'preview'
activeDiffField.value = null
}
showUnchanged.value = false
}
const loadHistory = async () => {
if (!props.postId) return
loading.value = true
@@ -211,6 +386,7 @@ const selectVersion = async (version: number) => {
])
previewData.value = detail
diffData.value = diff
applyTabDefaults(version)
} catch (error: any) {
toast.showToast(error.message || '加载版本详情失败', 'error')
} finally {
@@ -237,6 +413,9 @@ watch(
activeVersion.value = null
previewData.value = null
diffData.value = null
activeMainTab.value = 'preview'
activeDiffField.value = null
showUnchanged.value = false
loadHistory()
}
}
@@ -244,6 +423,6 @@ watch(
</script>
<style scoped>
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
.custom-scrollbar::-webkit-scrollbar { width: 4px; height: 4px; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 2px; }
</style>