chore: opt settings code

This commit is contained in:
engigu
2025-12-21 10:23:25 +08:00
parent 432c57e862
commit c06030fe27
28 changed files with 535 additions and 187 deletions
+26 -1
View File
@@ -113,7 +113,14 @@ export const api = {
getPublicSite: () => request<{ title: string; subtitle: string; icon: string }>('/settings/public'),
updateSite: (data: SiteSettings) =>
request('/settings/site', { method: 'PUT', body: JSON.stringify(data) }),
getAbout: () => request<AboutInfo>('/settings/about')
getAbout: () => request<AboutInfo>('/settings/about'),
getLoginLogs: (params?: { page?: number; page_size?: number; username?: string }) => {
const query = new URLSearchParams()
if (params?.page) query.set('page', String(params.page))
if (params?.page_size) query.set('page_size', String(params.page_size))
if (params?.username) query.set('username', params.username)
return request<LoginLogListResponse>(`/settings/login-logs?${query}`)
}
},
files: {
tree: () => request<FileNode[]>('/files/tree'),
@@ -264,3 +271,21 @@ export interface SiteSettings {
page_size: string
cookie_days: string
}
export interface LoginLog {
id: number
username: string
ip: string
user_agent: string
status: string
message: string
created_at: string
}
export interface LoginLogListResponse {
data: LoginLog[]
total: number
page: number
page_size: number
}
+2 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import { LayoutDashboard, ListTodo, FileCode, Settings, LogOut, ScrollText, Terminal, Variable } from 'lucide-vue-next'
import { LayoutDashboard, ListTodo, FileCode, Settings, LogOut, ScrollText, Terminal, Variable, KeyRound } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import ThemeToggle from '@/components/ThemeToggle.vue'
import { api } from '@/api'
@@ -18,6 +18,7 @@ const navItems = [
{ to: '/history', icon: ScrollText, label: '执行历史', exact: true },
{ to: '/environments', icon: Variable, label: '环境变量', exact: true },
{ to: '/terminal', icon: Terminal, label: '终端命令', exact: true },
{ to: '/login-logs', icon: KeyRound, label: '登录日志', exact: true },
{ to: '/settings', icon: Settings, label: '系统设置', exact: true },
]
+1
View File
@@ -20,6 +20,7 @@ const router = createRouter({
{ path: 'editor/:path(.*)?', name: 'editor', component: () => import('@/views/editor/Editor.vue') },
{ path: 'environments', name: 'environments', component: () => import('@/views/environments/Environments.vue') },
{ path: 'history', name: 'history', component: () => import('@/views/history/History.vue') },
{ path: 'login-logs', name: 'login-logs', component: () => import('@/views/login-logs/LoginLogs.vue') },
{ path: 'terminal', name: 'terminal', component: () => import('@/views/terminal/Terminal.vue') },
{ path: 'settings', name: 'settings', component: () => import('@/views/settings/Settings.vue') }
]
+132
View File
@@ -0,0 +1,132 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import Pagination from '@/components/Pagination.vue'
import { RefreshCw, Search } from 'lucide-vue-next'
import { api } from '@/api'
import { toast } from 'vue-sonner'
import { useSiteSettings } from '@/composables/useSiteSettings'
const { pageSize } = useSiteSettings()
interface LoginLog {
id: number
username: string
ip: string
user_agent: string
status: string
message: string
created_at: string
}
const logs = ref<LoginLog[]>([])
const filterUsername = ref('')
const currentPage = ref(1)
const total = ref(0)
const loading = ref(false)
let searchTimer: ReturnType<typeof setTimeout> | null = null
async function loadLogs() {
loading.value = true
try {
const res = await api.settings.getLoginLogs({
page: currentPage.value,
page_size: pageSize.value,
username: filterUsername.value || undefined
})
logs.value = res.data
total.value = res.total
} catch {
toast.error('加载登录日志失败')
} finally {
loading.value = false
}
}
function handleSearch() {
if (searchTimer) clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
currentPage.value = 1
loadLogs()
}, 300)
}
function handlePageChange(page: number) {
currentPage.value = page
loadLogs()
}
function getBrowserInfo(userAgent: string): string {
if (!userAgent) return '未知'
if (userAgent.includes('Chrome')) return 'Chrome'
if (userAgent.includes('Firefox')) return 'Firefox'
if (userAgent.includes('Safari')) return 'Safari'
if (userAgent.includes('Edge')) return 'Edge'
return '其他'
}
onMounted(loadLogs)
</script>
<template>
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">登录日志</h2>
<p class="text-muted-foreground">查看系统登录记录</p>
</div>
<div class="flex items-center gap-2">
<div class="relative">
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
v-model="filterUsername"
placeholder="搜索用户名..."
class="h-9 pl-9 w-56 text-sm"
@input="handleSearch"
/>
</div>
<Button variant="outline" size="icon" class="h-9 w-9" @click="loadLogs" :disabled="loading">
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
</Button>
</div>
</div>
<div class="rounded-lg border bg-card">
<!-- 表头 -->
<div class="flex items-center gap-4 px-4 py-2 border-b bg-muted/50 text-sm text-muted-foreground font-medium">
<span class="w-24 shrink-0">用户名</span>
<span class="w-32 shrink-0">IP 地址</span>
<span class="w-20 shrink-0">浏览器</span>
<span class="w-16 shrink-0 text-center">状态</span>
<span class="flex-1">消息</span>
<span class="w-40 shrink-0 text-right">时间</span>
</div>
<!-- 列表 -->
<div class="divide-y">
<div v-if="logs.length === 0" class="text-sm text-muted-foreground text-center py-8">
暂无登录日志
</div>
<div
v-for="log in logs"
:key="log.id"
class="flex items-center gap-4 px-4 py-2 hover:bg-muted/50 transition-colors"
>
<span class="w-24 shrink-0 font-medium text-sm truncate">{{ log.username }}</span>
<code class="w-32 shrink-0 text-xs text-muted-foreground bg-muted px-2 py-1 rounded">{{ log.ip }}</code>
<span class="w-20 shrink-0 text-xs text-muted-foreground">{{ getBrowserInfo(log.user_agent) }}</span>
<span class="w-16 shrink-0 flex justify-center">
<Badge :variant="log.status === 'success' ? 'default' : 'destructive'" class="text-xs">
{{ log.status === 'success' ? '成功' : '失败' }}
</Badge>
</span>
<span class="flex-1 text-sm text-muted-foreground truncate">{{ log.message }}</span>
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground">{{ log.created_at }}</span>
</div>
</div>
<!-- 分页 -->
<Pagination :total="total" :page="currentPage" @update:page="handlePageChange" />
</div>
</div>
</template>