初始化

This commit is contained in:
李琦
2026-01-15 13:51:44 +08:00
commit b7b6d3e39e
156 changed files with 38913 additions and 0 deletions

View File

@@ -0,0 +1,378 @@
<template>
<div class="admin-logs">
<h1 class="page-title">操作日志管理</h1>
<div class="toolbar">
<div class="filter-section">
<div class="filter-group">
<label for="pageSize">每页显示:</label>
<CustomSelect
v-model.number="pageSize"
:options="pageSizeOptions"
@update:modelValue="fetchLogs"
style="width: 80px;"
/>
</div>
</div>
</div>
<div class="table-container">
<table class="admin-table">
<thead>
<tr>
<th>ID</th>
<th>操作人</th>
<th>IP地址</th>
<th>路径</th>
<th>方法</th>
<th>状态</th>
<th>耗时(ms)</th>
<th>操作时间</th>
</tr>
</thead>
<tbody>
<tr v-for="log in logs.list" :key="log.id">
<td>{{ log.id }}</td>
<td>{{ log.username }}</td>
<td>{{ log.ip }}</td>
<td class="log-path">{{ log.path }}</td>
<td>
<span :class="['method-badge', `method-${log.method.toLowerCase()}`]">
{{ log.method }}
</span>
</td>
<td>
<span :class="['status-badge', getStatusClass(log.status)]">
{{ log.status }}
</span>
</td>
<td>{{ log.duration }}</td>
<td>{{ formatDate(log.createdAt) }}</td>
</tr>
</tbody>
</table>
</div>
<div v-if="logs.list.length === 0" class="empty-state">
<p>暂无操作日志</p>
</div>
<!-- Pagination -->
<div v-if="logs.list.length > 0" class="pagination">
<button
class="btn btn-sm btn-secondary"
:disabled="currentPage === 1"
@click="changePage(currentPage - 1)"
>
上一页
</button>
<span class="page-info">
{{ currentPage }} {{ totalPages }} 总计 {{ logs.total }} 条记录
</span>
<button
class="btn btn-sm btn-secondary"
:disabled="currentPage === totalPages"
@click="changePage(currentPage + 1)"
>
下一页
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { getOperationLogs, PaginationResponse, OperationLog } from '../../services/api'
import { useToast } from '../../composables/useToast'
import CustomSelect from '../../components/CustomSelect.vue'
const toast = useToast()
const logs = ref<PaginationResponse<OperationLog>>({
list: [],
total: 0,
page: 1,
size: 10
})
const currentPage = ref(1)
const pageSize = ref(10)
// Select options
const pageSizeOptions = [
{ value: 10, label: '10' },
{ value: 20, label: '20' },
{ value: 50, label: '50' },
{ value: 100, label: '100' }
]
const totalPages = computed(() => {
return Math.ceil(logs.value.total / pageSize.value)
})
const fetchLogs = async () => {
try {
logs.value = await getOperationLogs(currentPage.value, pageSize.value)
} catch (error) {
console.error('Error fetching operation logs:', error)
toast.error('获取操作日志失败')
}
}
const changePage = (page: number) => {
currentPage.value = page
fetchLogs()
}
const getStatusClass = (status: number) => {
if (status >= 200 && status < 300) {
return 'success'
} else if (status >= 400 && status < 500) {
return 'warning'
} else if (status >= 500) {
return 'error'
}
return ''
}
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN')
}
onMounted(() => {
fetchLogs()
})
</script>
<style scoped>
.admin-logs {
max-width: 1200px;
margin: 0 auto;
}
.page-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1.5rem;
color: #d4b383;
font-family: 'Inter', sans-serif;
}
.toolbar {
margin-bottom: 1rem;
display: flex;
justify-content: flex-end;
}
.filter-section {
display: flex;
gap: 1rem;
}
.filter-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-group label {
font-weight: 500;
color: rgba(255, 255, 255, 0.8);
font-size: 0.875rem;
font-family: 'Inter', sans-serif;
}
.form-select {
padding: 0.5rem;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 0.375rem;
font-size: 0.875rem;
background: rgba(255, 255, 255, 0.05);
color: white;
font-family: 'Inter', sans-serif;
}
.table-container {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow-x: auto;
margin-bottom: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.admin-table {
width: 100%;
border-collapse: collapse;
min-width: 800px;
color: white;
}
.admin-table th,
.admin-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
font-size: 0.875rem;
font-family: 'Inter', sans-serif;
}
.admin-table th {
background: rgba(255, 255, 255, 0.08);
font-weight: 600;
color: #d4b383;
white-space: nowrap;
}
.admin-table tr:hover {
background: rgba(212, 179, 131, 0.05);
}
.log-path {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.method-badge {
display: inline-block;
padding: 0.125rem 0.375rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
color: white;
min-width: 40px;
text-align: center;
font-family: 'Inter', sans-serif;
}
.method-get {
background-color: #3b82f6;
}
.method-post {
background-color: #10b981;
}
.method-put {
background-color: #f59e0b;
}
.method-delete {
background-color: #ef4444;
}
.method-patch {
background-color: #8b5cf6;
}
.method-options {
background-color: #6b7280;
}
.status-badge {
display: inline-block;
padding: 0.125rem 0.375rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
min-width: 40px;
text-align: center;
font-family: 'Inter', sans-serif;
}
.status-badge.success {
background-color: rgba(16, 185, 129, 0.2);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.status-badge.warning {
background-color: rgba(251, 191, 36, 0.2);
color: #f59e0b;
border: 1px solid rgba(251, 191, 36, 0.3);
}
.status-badge.error {
background-color: rgba(239, 68, 68, 0.2);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.btn {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.25rem;
border: 1px solid transparent;
font-family: 'Inter', sans-serif;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.btn-primary {
background-color: #d4b383;
color: #050505;
border-color: #d4b383;
}
.btn-primary:hover {
background-color: transparent;
color: #d4b383;
box-shadow: 0 0 15px rgba(212, 179, 131, 0.3);
}
.btn-secondary {
background-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.8);
border-color: rgba(255, 255, 255, 0.2);
}
.btn-secondary:hover:not(:disabled) {
background-color: rgba(212, 179, 131, 0.1);
border-color: #d4b383;
color: #d4b383;
}
.btn-secondary:disabled {
background-color: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.4);
border-color: rgba(255, 255, 255, 0.1);
cursor: not-allowed;
}
.empty-state {
text-align: center;
padding: 2rem;
color: rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.05);
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
font-family: 'Inter', sans-serif;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-top: 1rem;
}
.page-info {
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.6);
font-family: 'Inter', sans-serif;
}
</style>