feat: add settings pages

This commit is contained in:
engigu
2025-12-20 12:00:08 +08:00
parent 70cc7c0f8c
commit c299ed67d6
23 changed files with 10462 additions and 130 deletions
+19 -3
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
@@ -13,6 +13,20 @@ const username = ref('')
const password = ref('')
const loading = ref(false)
const siteTitle = ref('白虎面板')
const siteSubtitle = ref('轻量级定时任务管理系统')
async function loadSiteSettings() {
try {
const res = await api.settings.getPublicSite()
siteTitle.value = res.title || '白虎面板'
siteSubtitle.value = res.subtitle || '轻量级定时任务管理系统'
document.title = siteTitle.value
} catch {
// 使用默认值
}
}
async function handleLogin() {
loading.value = true
try {
@@ -25,6 +39,8 @@ async function handleLogin() {
loading.value = false
}
}
onMounted(loadSiteSettings)
</script>
<template>
@@ -40,8 +56,8 @@ async function handleLogin() {
<div class="w-96 p-10">
<div class="space-y-8">
<div class="space-y-2">
<h1 class="text-2xl font-bold tracking-tight">白虎面板</h1>
<p class="text-muted-foreground">轻量级定时任务管理系统</p>
<h1 class="text-2xl font-bold tracking-tight">{{ siteTitle }}</h1>
<p class="text-muted-foreground">{{ siteSubtitle }}</p>
</div>
<form @submit.prevent="handleLogin" class="space-y-5">
<div class="space-y-2">
+17 -17
View File
@@ -21,36 +21,36 @@ onMounted(loadAbout)
<template>
<div>
<!-- 站点关于 -->
<div class="mb-8">
<h3 class="text-xl font-semibold mb-2">白虎面板</h3>
<p class="text-muted-foreground">一个轻量级的定时任务管理系统支持脚本管理定时执行和日志追踪</p>
<div class="mb-6">
<h3 class="text-lg font-semibold mb-1">白虎面板</h3>
<p class="text-sm text-muted-foreground">一个轻量级的定时任务管理系统支持脚本管理定时执行和日志追踪</p>
</div>
<div class="grid md:grid-cols-2 gap-x-16 gap-y-8">
<div class="grid md:grid-cols-2 gap-x-8 gap-y-5">
<!-- 左侧技术栈和功能特性 -->
<div class="space-y-8">
<div class="space-y-5">
<div>
<h4 class="text-sm font-medium mb-4">技术栈</h4>
<div class="flex flex-wrap gap-2">
<Badge v-for="tech in techStack" :key="tech" variant="secondary">{{ tech }}</Badge>
<h4 class="text-sm font-medium mb-2">技术栈</h4>
<div class="flex flex-wrap gap-1.5">
<Badge v-for="tech in techStack" :key="tech" class="text-xs bg-primary/15 text-primary border-0">{{ tech }}</Badge>
</div>
</div>
<div>
<h4 class="text-sm font-medium mb-4">功能特性</h4>
<div class="flex flex-wrap gap-2">
<Badge v-for="feature in features" :key="feature" variant="outline">{{ feature }}</Badge>
<h4 class="text-sm font-medium mb-2">功能特性</h4>
<div class="flex flex-wrap gap-1.5">
<Badge v-for="feature in features" :key="feature" class="text-xs bg-accent text-accent-foreground">{{ feature }}</Badge>
</div>
</div>
</div>
<!-- 右侧系统信息 -->
<div>
<h4 class="text-sm font-medium mb-4">系统信息</h4>
<div class="space-y-3">
<h4 class="text-sm font-medium mb-2">系统信息</h4>
<div class="space-y-2">
<div class="flex justify-between items-center">
<span class="text-muted-foreground text-sm">系统版本:</span>
<Badge variant="outline" class="font-mono">{{ aboutInfo?.version || 'dev' }}</Badge>
<Badge variant="outline" class="font-mono text-xs">{{ aboutInfo?.version || 'dev' }}</Badge>
</div>
<div class="flex justify-between items-center">
<span class="text-muted-foreground text-sm">构建时间:</span>
@@ -69,9 +69,9 @@ onMounted(loadAbout)
</div>
<!-- 底部版权和链接 -->
<div class="mt-10 pt-6 border-t flex items-center justify-center gap-2 text-sm text-muted-foreground">
<span>© 2025 保留所有权利</span>
<a href="https://github.com" target="_blank" class="inline-flex items-center gap-1 text-primary hover:underline">
<div class="mt-6 pt-4 border-t flex items-center justify-center gap-2 text-sm text-muted-foreground">
<span>© {{ new Date().getFullYear() }} 保留所有权利</span>
<a href="https://github.com/engigu/BaiHu/" target="_blank" class="inline-flex items-center gap-1 text-primary hover:underline">
<ExternalLink class="h-3.5 w-3.5" />
GitHub 仓库
</a>
+3 -3
View File
@@ -50,10 +50,10 @@ const activeTab = ref('password')
</TabsContent>
<TabsContent value="site" class="mt-6">
<Card class="max-w-lg">
<Card class="max-w-xl">
<CardHeader>
<CardTitle>站点设置</CardTitle>
<CardDescription>查看当前站点配置信息</CardDescription>
<CardDescription>配置站点标题图标和系统参数</CardDescription>
</CardHeader>
<CardContent>
<SiteSettings />
@@ -62,7 +62,7 @@ const activeTab = ref('password')
</TabsContent>
<TabsContent value="about" class="mt-6">
<Card class="max-w-3xl">
<Card class="max-w-xl">
<CardContent class="pt-6">
<AboutSettings />
</CardContent>
+71 -15
View File
@@ -1,33 +1,89 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { api } from '@/api'
import { Button } from '@/components/ui/button'
import { api, type SiteSettings } from '@/api'
import { toast } from 'vue-sonner'
import { useSiteSettings } from '@/composables/useSiteSettings'
const siteName = ref('')
const sitePort = ref(0)
const { refreshSettings } = useSiteSettings()
async function loadSiteSettings() {
const form = ref<SiteSettings>({
title: '',
subtitle: '',
icon: '',
page_size: '10',
cookie_days: '7'
})
const loading = ref(false)
const iconPreview = computed(() => {
if (!form.value.icon) return ''
// 简单验证是否是 SVG
if (form.value.icon.trim().startsWith('<svg')) {
return form.value.icon
}
return ''
})
async function loadSettings() {
try {
const res = await api.settings.getSite()
siteName.value = res.site_name || '白虎面板'
sitePort.value = res.port
form.value = res
} catch {}
}
onMounted(loadSiteSettings)
async function saveSettings() {
loading.value = true
try {
await api.settings.updateSite(form.value)
await refreshSettings()
toast.success('保存成功')
} catch {
toast.error('保存失败')
} finally {
loading.value = false
}
}
onMounted(loadSettings)
</script>
<template>
<div class="space-y-4">
<div class="space-y-2">
<Label>站点名称</Label>
<Input v-model="siteName" disabled class="max-w-sm" />
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">站点标题</Label>
<Input v-model="form.title" placeholder="白虎面板" class="col-span-3" />
</div>
<div class="space-y-2">
<Label>端口</Label>
<Input v-model.number="sitePort" type="number" class="w-32" disabled />
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">站点标语</Label>
<Input v-model="form.subtitle" placeholder="轻量级定时任务管理系统" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">站点图标</Label>
<div class="col-span-3 flex items-center gap-2">
<Input v-model="form.icon" placeholder="<svg>...</svg>" class="flex-1 font-mono text-xs" />
<div v-if="iconPreview" class="p-1.5 border rounded bg-muted/50 w-8 h-8 flex items-center justify-center shrink-0 [&>svg]:w-5 [&>svg]:h-5" v-html="iconPreview" />
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">分页/Cookie</Label>
<div class="col-span-3 flex items-center gap-4">
<div class="flex items-center gap-2">
<Input v-model="form.page_size" type="number" class="w-20" />
<span class="text-sm text-muted-foreground">/</span>
</div>
<div class="flex items-center gap-2">
<Input v-model="form.cookie_days" type="number" class="w-20" />
<span class="text-sm text-muted-foreground">天过期</span>
</div>
</div>
</div>
<div class="flex justify-end pt-2">
<Button @click="saveSettings" :disabled="loading">
{{ loading ? '保存中...' : '保存设置' }}
</Button>
</div>
<p class="text-sm text-muted-foreground">站点设置需要修改配置文件后重启服务</p>
</div>
</template>
+37 -14
View File
@@ -23,6 +23,16 @@ const pageSize = ref(10)
const total = ref(0)
let searchTimer: ReturnType<typeof setTimeout> | null = null
const cronPresets = [
{ label: '每分钟', value: '* * * * *' },
{ label: '每5分钟', value: '*/5 * * * *' },
{ label: '每小时', value: '0 * * * *' },
{ label: '每天0点', value: '0 0 * * *' },
{ label: '每天8点', value: '0 8 * * *' },
{ label: '每周一', value: '0 0 * * 1' },
{ label: '每月1号', value: '0 0 1 * *' },
]
async function loadTasks() {
try {
const res = await api.tasks.list({ page: currentPage.value, page_size: pageSize.value, name: filterName.value || undefined })
@@ -168,26 +178,39 @@ onMounted(loadTasks)
</div>
<Dialog v-model:open="showDialog">
<DialogContent class="max-w-md">
<DialogContent class="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{{ isEdit ? '编辑任务' : '新建任务' }}</DialogTitle>
</DialogHeader>
<div class="space-y-4 py-2">
<div class="space-y-2">
<Label>任务名称</Label>
<Input v-model="editingTask.name" placeholder="任务名称" />
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">任务名称</Label>
<Input v-model="editingTask.name" placeholder="我的任务" class="col-span-3" />
</div>
<div class="space-y-2">
<Label>执行命令</Label>
<Input v-model="editingTask.command" class="font-mono" placeholder="node script.js" />
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">执行命令</Label>
<Input v-model="editingTask.command" placeholder="node script.js" class="col-span-3 font-mono" />
</div>
<div class="space-y-2">
<Label>定时规则 (Cron)</Label>
<Input v-model="editingTask.schedule" class="font-mono" placeholder="0 * * * *" />
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">定时规则</Label>
<Input v-model="editingTask.schedule" placeholder="0 * * * *" class="col-span-3 font-mono" />
</div>
<div class="space-y-2">
<Label>超时时间 (分钟)</Label>
<Input v-model.number="editingTask.timeout" type="number" placeholder="30" />
<div class="grid grid-cols-4 items-start gap-4">
<span></span>
<div class="col-span-3 flex flex-wrap gap-1.5">
<span
v-for="preset in cronPresets"
:key="preset.value"
class="px-2 py-0.5 text-xs rounded-md bg-muted hover:bg-accent cursor-pointer transition-colors"
@click="editingTask.schedule = preset.value"
>
{{ preset.label }}
</span>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">超时(分钟)</Label>
<Input v-model.number="editingTask.timeout" type="number" placeholder="30" class="col-span-3" />
</div>
</div>
<DialogFooter>