数据结构优化
This commit is contained in:
182
client/src/components/admin/AccessLogModal.vue
Normal file
182
client/src/components/admin/AccessLogModal.vue
Normal 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>
|
||||
@@ -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: '🌐' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user