数据结构优化

This commit is contained in:
李琦
2026-01-20 11:00:42 +08:00
parent 29a642c285
commit b088ba48be
17 changed files with 1073 additions and 49 deletions

View File

@@ -0,0 +1,182 @@
<template>
<Teleport to="body">
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" @click.self="close">
<div class="bg-[#1a1a1a] border border-white/10 rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-hidden animate-reveal flex flex-col">
<div class="p-4 border-b border-white/10 flex justify-between items-center shrink-0">
<h3 class="text-lg font-serif italic text-white">访问记录 - {{ postTitle }}</h3>
<button @click="close" class="text-white/50 hover:text-white">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="p-6 overflow-y-auto flex-1">
<div v-if="loading" class="flex items-center justify-center py-12">
<div class="text-white/40">加载中...</div>
</div>
<div v-else-if="logs.list.length === 0" class="flex flex-col items-center justify-center py-12">
<div class="text-4xl mb-4">📊</div>
<p class="text-white/60">暂无访问记录</p>
</div>
<div v-else>
<div class="mb-4 text-sm text-white/60">
{{ logs.total }} 条记录当前第 {{ currentPage }} / {{ totalPages }}
</div>
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="border-b border-white/10">
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">IP地址</th>
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">归属地</th>
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">访问路径</th>
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">状态码</th>
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">响应时间</th>
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">访问时间</th>
</tr>
</thead>
<tbody>
<tr v-for="log in logs.list" :key="log.id" class="border-b border-white/5 hover:bg-white/5 transition-colors">
<td class="py-2 px-3 text-sm text-white/90 font-mono">{{ log.ip }}</td>
<td class="py-2 px-3 text-sm text-white/80">{{ log.region || 'Unknown' }}</td>
<td class="py-2 px-3 text-sm text-white/80 font-mono">{{ log.path }}</td>
<td class="py-2 px-3 text-sm">
<span :class="getStatusClass(log.statusCode)">
{{ log.statusCode }}
</span>
</td>
<td class="py-2 px-3 text-sm text-white/80">{{ log.responseTime }}ms</td>
<td class="py-2 px-3 text-sm text-white/80">{{ log.createdAt }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div v-if="totalPages > 1" class="flex items-center justify-between mt-4 pt-4 border-t border-white/10">
<button
@click="changePage(currentPage - 1)"
:disabled="currentPage === 1"
class="px-3 py-1 text-sm bg-white/5 border border-white/10 rounded text-white/60 hover:text-white hover:border-white/20 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
上一页
</button>
<span class="text-sm text-white/60">
{{ currentPage }} / {{ totalPages }}
</span>
<button
@click="changePage(currentPage + 1)"
:disabled="currentPage === totalPages"
class="px-3 py-1 text-sm bg-white/5 border border-white/10 rounded text-white/60 hover:text-white hover:border-white/20 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
下一页
</button>
</div>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { getPostAccessLogs, AccessLog, PaginationResponse } from '../../services/api'
import { useToast } from '../../composables/useToast'
interface Props {
isOpen: boolean
postId: number | null
postTitle?: string
}
const props = withDefaults(defineProps<Props>(), {
postTitle: '文章'
})
const emit = defineEmits<{
'update:isOpen': [value: boolean]
}>()
const toast = useToast()
const loading = ref(false)
const logs = ref<PaginationResponse<AccessLog>>({
list: [],
total: 0,
page: 1,
size: 20
})
const currentPage = ref(1)
const pageSize = ref(20)
const totalPages = computed(() => {
return Math.ceil(logs.value.total / pageSize.value)
})
const close = () => {
emit('update:isOpen', false)
}
const fetchLogs = async () => {
if (!props.postId) return
loading.value = true
try {
logs.value = await getPostAccessLogs(props.postId, currentPage.value, pageSize.value)
} catch (error) {
console.error('Error fetching access logs:', error)
toast.error('获取访问记录失败')
} finally {
loading.value = false
}
}
const changePage = (page: number) => {
if (page < 1 || page > totalPages.value) return
currentPage.value = page
fetchLogs()
}
const getStatusClass = (status: number) => {
if (status >= 200 && status < 300) {
return 'text-green-400'
} else if (status >= 400 && status < 500) {
return 'text-yellow-400'
} else if (status >= 500) {
return 'text-red-400'
}
return 'text-white/60'
}
watch(() => props.isOpen, (newVal) => {
if (newVal && props.postId) {
currentPage.value = 1
fetchLogs()
}
})
watch(() => props.postId, () => {
if (props.isOpen && props.postId) {
currentPage.value = 1
fetchLogs()
}
})
</script>
<style scoped>
.animate-reveal {
animation: reveal 0.3s ease-out;
}
@keyframes reveal {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -263,7 +263,8 @@ const menuItems = ref<MenuItem[]>([
isOpen: false,
children: [
{ title: '全局配置', path: '/admin/settings', icon: '🛠️' },
{ title: '操作日志', path: '/admin/logs', icon: '📋' }
{ title: '操作日志', path: '/admin/logs', icon: '📋' },
{ title: '访问日志', path: '/admin/access-logs', icon: '🌐' }
]
}
])