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
+6 -3
View File
@@ -16,7 +16,10 @@ COPY web/ ./
RUN npm run build RUN npm run build
# Stage 2: Build backend # Stage 2: Build backend
FROM golang:1.24-alpine AS backend-builder FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS backend-builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app WORKDIR /app
@@ -30,10 +33,10 @@ COPY . .
# Copy built frontend to embed location # Copy built frontend to embed location
COPY --from=frontend-builder /app/web/dist ./internal/static/dist COPY --from=frontend-builder /app/web/dist ./internal/static/dist
# Build Go binary (no CGO needed, auto-detect target arch) # Build Go binary with cross-compilation (native speed, no QEMU)
ARG VERSION=dev ARG VERSION=dev
ARG BUILD_TIME ARG BUILD_TIME
RUN CGO_ENABLED=0 go build -ldflags="-s -w -X baihu/internal/constant.Version=${VERSION} -X 'baihu/internal/constant.BuildTime=${BUILD_TIME}'" -o baihu . RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-s -w -X baihu/internal/constant.Version=${VERSION} -X 'baihu/internal/constant.BuildTime=${BUILD_TIME}'" -o baihu .
# Stage 3: Final image based on Dockerfile.debian # Stage 3: Final image based on Dockerfile.debian
FROM debian:bookworm-slim FROM debian:bookworm-slim
+8
View File
@@ -26,6 +26,7 @@ require (
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect
@@ -42,14 +43,21 @@ require (
github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.46.0 // indirect golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.47.0 // indirect
+21
View File
@@ -24,6 +24,8 @@ github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9g
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -41,6 +43,7 @@ github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
@@ -71,6 +74,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -82,6 +87,8 @@ github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZ
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@@ -89,6 +96,10 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -102,10 +113,16 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
@@ -115,9 +132,13 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+6
View File
@@ -62,6 +62,12 @@ func (a *App) initDatabase() {
if err := database.Migrate(); err != nil { if err := database.Migrate(); err != nil {
logger.Fatalf("Failed to migrate database: %v", err) logger.Fatalf("Failed to migrate database: %v", err)
} }
// 初始化默认设置
settingsService := services.NewSettingsService()
if err := settingsService.InitSettings(); err != nil {
logger.Warnf("Failed to init settings: %v", err)
}
} }
func (a *App) initRouter() { func (a *App) initRouter() {
+11
View File
@@ -46,3 +46,14 @@ var TablePrefix = DefaultTablePrefix
// JWTSecret JWT 密钥,可通过配置文件设置 // JWTSecret JWT 密钥,可通过配置文件设置
var JWTSecret = DefaultJWTSecret var JWTSecret = DefaultJWTSecret
// DefaultSettings 默认系统设置
var DefaultSettings = map[string]map[string]string{
"site": {
"title": "白虎面板",
"subtitle": "轻量级定时任务管理系统",
"icon": "<svg t=\"1766107903919\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"1942\" width=\"200\" height=\"200\"><path d=\"M884.992 273.05984c4.10624 0 5.0688-2.36544 2.10944-5.25312 0 0-64.28672-65.55648-111.7696-75.55072-47.48288-9.984-45.47584-59.37152-80.56832-75.02848-72.0896-32.16384-158.6176-34.3552-158.6176-34.3552s-91.37152-4.92544-138.752-9.89184c-19.46624-2.03776-54.46656-9.58464-54.46656-9.58464-4.0448-0.84992-10.63936-1.19808-14.66368-0.44032 0 0-30.63808-0.07168-44.30848 43.35616-8.8576 28.11904 1.792 104.79616 1.792 104.79616 1.46432 12.27776-3.42016 30.21824-10.72128 40.20224 0 0-36.77184 46.03904-58.9312 100.34176-22.15936 54.30272 118.15936 145.05984 208.98816 205.27104C507.82208 613.89824 502.03648 743.424 502.03648 743.424s-74.19904-96.75776-194.00704-156.9792C188.2112 526.22336 150.30272 442.23488 150.30272 442.23488c-2.89792-5.45792-5.94944-4.94592-6.71744 1.21856 0 0-15.1552 91.61728 16.25088 147.8144 70.92224 126.88384 141.74208 112.88576 197.03808 183.27552s54.272 164.9152 54.272 164.9152-97.62816-141.29152-235.66336-205.55776c-91.53536-61.27616-74.7008-125.91104-85.49376-101.85728-10.79296 24.05376 26.73664 192.41984 65.40288 222.2592 80.57856 62.18752 94.16704 101.98016 94.16704 101.98016h175.53408S544.512 814.85824 572.928 725.73952c44.41088-139.30496 40.20224-191.26272 40.20224-191.26272 0.08192-8.22272 6.81984-14.19264 14.98112-13.29152 0 0 46.45888 4.64896 66.64192 9.68704 23.53152 5.86752 55.35744 26.20416 55.35744 26.20416 3.49184 2.14016 7.39328 0.68608 8.69376-3.1744l34.4576-102.77888c1.30048-3.8912-0.63488-5.51936-4.352-3.75808 0 0-45.37344 25.09824-88.17664 12.1856-20.15232-6.08256-59.60704-14.82752-74.69056-32.6656-16.95744-20.03968-15.59552-71.3728 26.66496-79.21664 48.0256-8.9088 33.13664 15.14496 65.91488 24.64768 27.42272 7.95648 22.29248-1.69984 26.69568 5.21216 1.67936 2.63168 0.38912 32.65536 0.38912 32.65536-0.21504 6.144 3.39968 7.84384 8.0384 3.79904l41.89184-36.46464c17.37728-11.24352 30.86336-0.57344 48.24064-11.81696 14.73536-9.53344 29.58336-43.66336 29.58336-43.66336 4.48512-9.24672 0.60416-20.41856-8.63232-24.92416l-42.58816-20.80768c-3.69664-1.80224-3.38944-3.26656 0.74752-3.26656h62.0032zM422.54336 123.87328s-49.88928 50.7392-74.5472 50.66752c-24.65792-0.07168-33.24928-56.12544-3.92192-56.12544 18.00192 0 76.32896 0.21504 76.32896 0.21504 4.13696 0.02048 5.0688 2.36544 2.14016 5.24288z m123.09504 249.64096s-3.31776-25.53856-33.16736-40.05888c-29.8496-14.52032-52.5312-41.13408-59.648-68.95616-12.1856-54.8864 48.29184-104.192 48.29184-104.192s-30.1056 73.6768 3.5328 106.60864c54.272 53.12512 40.99072 106.5984 40.99072 106.5984z m155.56608-164.46464c-7.94624 10.32192-9.60512 37.92896-59.2384 20.736-49.63328-17.2032-71.00416-54.5792-71.00416-54.5792-2.27328-3.40992-0.79872-6.49216 3.328-6.8096 0 0 43.55072-5.03808 80.06656 6.44096 24.91392 7.82336 54.79424 23.88992 46.848 34.21184z\" fill=\"#272636\" p-id=\"1943\"></path><path d=\"M366.30528 259.26656c1.05472-1.76128 1.51552-1.57696 1.19808 0.43008 0 0-7.55712 19.0464 22.8864 87.63392 27.0848 61.02016 87.49056 68.7104 118.66112 98.23232 55.808 52.82816 53.52448 123.45344 53.52448 123.45344s-25.82528-49.85856-85.98528-78.83776-92.6208-50.52416-127.7952-85.77024c-45.02528-45.12768 17.5104-145.14176 17.5104-145.14176zM500.0704 961.44384h134.49216s95.8464-138.07616 46.68416-291.25632c-16.19968-50.46272-43.45856-80.00512-43.45856-80.00512-5.18144-6.38976-8.89856-4.88448-8.448 3.34848 0 0 9.15456 99.80928-19.44576 194.00704-28.60032 94.208-109.824 173.90592-109.824 173.90592zM681.61536 956.8768h105.24672s22.38464-73.5232 16.61952-130.21184c-9.30816-91.57632-48.31232-132.72064-48.31232-132.72064-3.80928-4.77184-6.0928-3.67616-5.2224 2.38592 0 0 16.75264 89.1904-14.4896 150.1184-30.9248 60.30336-53.84192 110.42816-53.84192 110.42816zM869.30432 811.35616c-2.88768-2.9184-4.80256-1.95584-4.38272 2.10944 0 0 6.79936 44.99456-7.76192 81.22368-10.12736 25.1904-28.91776 58.60352-28.91776 58.60352h107.17184s5.34528-50.31936-19.89632-83.97824c-11.56096-23.99232-46.21312-57.9584-46.21312-57.9584z\" fill=\"#272636\" p-id=\"1944\"></path></svg>",
"page_size": "10",
"cookie_days": "7",
},
}
+68
View File
@@ -0,0 +1,68 @@
package constant
import (
"bufio"
"encoding/json"
"math/rand"
"os"
"sync"
)
type Sentence struct {
Name string `json:"name"`
From string `json:"from"`
}
const sentenceFile = "internal/constant/sentence1-10000.json"
var (
lineCount int
lineCountOnce sync.Once
)
// countLines 统计文件行数(只执行一次)
func countLines() {
lineCountOnce.Do(func() {
file, err := os.Open(sentenceFile)
if err != nil {
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lineCount++
}
})
}
// GetRandomSentence 随机获取一条古诗词
func GetRandomSentence() string {
countLines()
if lineCount == 0 {
return "欢迎使用白虎面板"
}
targetLine := rand.Intn(lineCount)
file, err := os.Open(sentenceFile)
if err != nil {
return "欢迎使用白虎面板"
}
defer file.Close()
scanner := bufio.NewScanner(file)
currentLine := 0
for scanner.Scan() {
if currentLine == targetLine {
var s Sentence
if err := json.Unmarshal(scanner.Bytes(), &s); err == nil && s.Name != "" {
return s.Name
}
break
}
currentLine++
}
return "欢迎使用白虎面板"
}
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,7 @@
package controllers package controllers
import ( import (
"baihu/internal/constant"
"baihu/internal/database" "baihu/internal/database"
"baihu/internal/models" "baihu/internal/models"
"baihu/internal/services" "baihu/internal/services"
@@ -49,3 +50,10 @@ func (dc *DashboardController) GetStats(c *gin.Context) {
utils.Success(c, stats) utils.Success(c, stats)
} }
// GetSentence 获取随机古诗词
func (dc *DashboardController) GetSentence(c *gin.Context) {
utils.Success(c, gin.H{
"sentence": constant.GetRandomSentence(),
})
}
+55 -16
View File
@@ -7,18 +7,23 @@ import (
"baihu/internal/services" "baihu/internal/services"
"baihu/internal/utils" "baihu/internal/utils"
"fmt" "fmt"
"runtime" "os"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/v3/process"
) )
type SettingsController struct { type SettingsController struct {
userService *services.UserService userService *services.UserService
settingsService *services.SettingsService
} }
func NewSettingsController(userService *services.UserService) *SettingsController { func NewSettingsController(userService *services.UserService) *SettingsController {
return &SettingsController{userService: userService} return &SettingsController{
userService: userService,
settingsService: services.NewSettingsService(),
}
} }
// ChangePassword 修改密码 // ChangePassword 修改密码
@@ -74,19 +79,50 @@ func (sc *SettingsController) CleanLogs(c *gin.Context) {
// GetSiteSettings 获取站点设置 // GetSiteSettings 获取站点设置
func (sc *SettingsController) GetSiteSettings(c *gin.Context) { func (sc *SettingsController) GetSiteSettings(c *gin.Context) {
config := services.Config settings := sc.settingsService.GetSection("site")
if config == nil { utils.Success(c, settings)
utils.Success(c, gin.H{ }
"site_name": "白虎面板",
"port": 8080, // GetPublicSiteSettings 获取公开的站点设置(无需认证)
}) func (sc *SettingsController) GetPublicSiteSettings(c *gin.Context) {
settings := sc.settingsService.GetSection("site")
// 只返回公开信息
utils.Success(c, gin.H{
"title": settings["title"],
"subtitle": settings["subtitle"],
"icon": settings["icon"],
})
}
// UpdateSiteSettings 更新站点设置
func (sc *SettingsController) UpdateSiteSettings(c *gin.Context) {
var req struct {
Title string `json:"title"`
Subtitle string `json:"subtitle"`
Icon string `json:"icon"`
PageSize string `json:"page_size"`
CookieDays string `json:"cookie_days"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误")
return return
} }
utils.Success(c, gin.H{ values := map[string]string{
"site_name": config.Server.SiteName, "title": req.Title,
"port": config.Server.Port, "subtitle": req.Subtitle,
}) "icon": req.Icon,
"page_size": req.PageSize,
"cookie_days": req.CookieDays,
}
if err := sc.settingsService.SetSection("site", values); err != nil {
utils.ServerError(c, "保存失败")
return
}
utils.SuccessMsg(c, "保存成功")
} }
// GetAbout 获取关于信息 // GetAbout 获取关于信息
@@ -97,9 +133,12 @@ func (sc *SettingsController) GetAbout(c *gin.Context) {
database.DB.Model(&models.EnvironmentVariable{}).Count(&envCount) database.DB.Model(&models.EnvironmentVariable{}).Count(&envCount)
// 内存使用 // 内存使用
var m runtime.MemStats memUsage := "N/A"
runtime.ReadMemStats(&m) if p, err := process.NewProcess(int32(os.Getpid())); err == nil {
memUsage := formatBytes(m.Alloc) if memInfo, err := p.MemoryInfo(); err == nil {
memUsage = formatBytes(memInfo.RSS)
}
}
// 运行时间 // 运行时间
uptime := formatDuration(time.Since(constant.StartTime)) uptime := formatDuration(time.Since(constant.StartTime))
+5
View File
@@ -89,6 +89,9 @@ func Setup(c *Controllers) *gin.Engine {
auth.POST("/register", c.Auth.Register) auth.POST("/register", c.Auth.Register)
} }
// 公开的站点设置(无需认证)
api.GET("/settings/public", c.Settings.GetPublicSiteSettings)
// 需要认证的路由 // 需要认证的路由
authorized := api.Group("") authorized := api.Group("")
authorized.Use(middleware.AuthRequired()) authorized.Use(middleware.AuthRequired())
@@ -98,6 +101,7 @@ func Setup(c *Controllers) *gin.Engine {
// Dashboard stats // Dashboard stats
authorized.GET("/stats", c.Dashboard.GetStats) authorized.GET("/stats", c.Dashboard.GetStats)
authorized.GET("/sentence", c.Dashboard.GetSentence)
// Task routes // Task routes
tasks := authorized.Group("/tasks") tasks := authorized.Group("/tasks")
@@ -167,6 +171,7 @@ func Setup(c *Controllers) *gin.Engine {
settings.POST("/password", c.Settings.ChangePassword) settings.POST("/password", c.Settings.ChangePassword)
settings.POST("/clean-logs", c.Settings.CleanLogs) settings.POST("/clean-logs", c.Settings.CleanLogs)
settings.GET("/site", c.Settings.GetSiteSettings) settings.GET("/site", c.Settings.GetSiteSettings)
settings.PUT("/site", c.Settings.UpdateSiteSettings)
settings.GET("/about", c.Settings.GetAbout) settings.GET("/about", c.Settings.GetAbout)
} }
} }
+12 -7
View File
@@ -24,19 +24,24 @@ func NewInitService(settingsService *SettingsService, userService *UserService)
// Initialize 执行初始化,如果已初始化则跳过 // Initialize 执行初始化,如果已初始化则跳过
func (s *InitService) Initialize() { func (s *InitService) Initialize() {
if s.IsInitialized() { //if s.IsInitialized() {
logger.Info("系统已初始化,跳过") // logger.Info("系统已初始化,跳过")
return // return
} //}
logger.Info("开始初始化系统...") logger.Info("开始初始化系统...")
// 创建管理员账号 // 创建管理员账号
s.createAdminUser() s.createAdminUser()
// 标记为已初始化 // 初始化默认设置(每次启动都检查)
s.settingsService.Set(InitSection, InitKey, InitValue) if err := s.settingsService.InitSettings(); err != nil {
logger.Info("系统初始化完成") logger.Warnf("初始化设置失败: %v", err)
}
//// 标记为已初始化
//s.settingsService.Set(InitSection, InitKey, InitValue)
//logger.Info("系统初始化完成")
} }
// IsInitialized 检查是否已初始化 // IsInitialized 检查是否已初始化
+47 -28
View File
@@ -1,6 +1,7 @@
package services package services
import ( import (
"baihu/internal/constant"
"baihu/internal/database" "baihu/internal/database"
"baihu/internal/models" "baihu/internal/models"
) )
@@ -11,59 +12,77 @@ func NewSettingsService() *SettingsService {
return &SettingsService{} return &SettingsService{}
} }
// Get 获取设置 // InitSettings 初始化默认设置
func (s *SettingsService) InitSettings() error {
for section, keys := range constant.DefaultSettings {
for key, value := range keys {
var count int64
database.DB.Model(&models.Setting{}).
Where("section = ? AND key = ?", section, key).
Count(&count)
if count == 0 {
setting := &models.Setting{
Section: section,
Key: key,
Value: value,
}
if err := database.DB.Create(setting).Error; err != nil {
return err
}
}
}
}
return nil
}
// Get 获取单个设置
func (s *SettingsService) Get(section, key string) string { func (s *SettingsService) Get(section, key string) string {
var setting models.Setting var setting models.Setting
if err := database.DB.Where("section = ? AND key = ?", section, key).First(&setting).Error; err != nil { if err := database.DB.Where("section = ? AND key = ?", section, key).First(&setting).Error; err != nil {
if def, ok := constant.DefaultSettings[section][key]; ok {
return def
}
return "" return ""
} }
return setting.Value return setting.Value
} }
// GetWithDefault 获取设置值,如果不存在则返回默认 // Set 设置单个
func (s *SettingsService) GetWithDefault(section, key, defaultValue string) string {
value := s.Get(section, key)
if value == "" {
return defaultValue
}
return value
}
// Set 设置值
func (s *SettingsService) Set(section, key, value string) error { func (s *SettingsService) Set(section, key, value string) error {
var setting models.Setting var setting models.Setting
result := database.DB.Where("section = ? AND key = ?", section, key).First(&setting) result := database.DB.Where("section = ? AND key = ?", section, key).First(&setting)
if result.Error != nil { if result.Error != nil {
// 不存在则创建 setting = models.Setting{Section: section, Key: key, Value: value}
setting = models.Setting{
Section: section,
Key: key,
Value: value,
}
return database.DB.Create(&setting).Error return database.DB.Create(&setting).Error
} }
// 存在则更新
return database.DB.Model(&setting).Update("value", value).Error return database.DB.Model(&setting).Update("value", value).Error
} }
// GetBySection 获取个 section 下的所有设置 // GetSection 获取个 section 设置
func (s *SettingsService) GetBySection(section string) map[string]string { func (s *SettingsService) GetSection(section string) map[string]string {
var settings []models.Setting var settings []models.Setting
database.DB.Where("section = ?", section).Find(&settings) database.DB.Where("section = ?", section).Find(&settings)
result := make(map[string]string) result := make(map[string]string)
// 先填充默认值
if defaults, ok := constant.DefaultSettings[section]; ok {
for k, v := range defaults {
result[k] = v
}
}
// 覆盖数据库值
for _, s := range settings { for _, s := range settings {
result[s.Key] = s.Value result[s.Key] = s.Value
} }
return result return result
} }
// Delete 删除设置 // SetSection 批量设置
func (s *SettingsService) Delete(section, key string) error { func (s *SettingsService) SetSection(section string, values map[string]string) error {
return database.DB.Where("section = ? AND key = ?", section, key).Delete(&models.Setting{}).Error for key, value := range values {
} if err := s.Set(section, key, value); err != nil {
return err
// DeleteBySection 删除某个 section 下的所有设置 }
func (s *SettingsService) DeleteBySection(section string) error { }
return database.DB.Where("section = ?", section).Delete(&models.Setting{}).Error return nil
} }
-7
View File
@@ -16,7 +16,6 @@
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"jinrishici": "^1.0.6",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"lucide-vue-next": "^0.562.0", "lucide-vue-next": "^0.562.0",
"pako": "^2.1.0", "pako": "^2.1.0",
@@ -1771,12 +1770,6 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/jinrishici": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/jinrishici/-/jinrishici-1.0.6.tgz",
"integrity": "sha512-4Bk5VHo6xg9DspGS8y8I7D6kftA6DYy4rSvwpdtxFvh9DidXMgf+NSZKAXrcLTi8S5NjqmwkpAKYPHXRURH64Q==",
"license": "Apache-2.0"
},
"node_modules/jiti": { "node_modules/jiti": {
"version": "2.6.1", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
-1
View File
@@ -17,7 +17,6 @@
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"jinrishici": "^1.0.6",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"lucide-vue-next": "^0.562.0", "lucide-vue-next": "^0.562.0",
"pako": "^2.1.0", "pako": "^2.1.0",
+14 -2
View File
@@ -101,14 +101,18 @@ export const api = {
detail: (id: number) => request<LogDetail>(`/logs/${id}`) detail: (id: number) => request<LogDetail>(`/logs/${id}`)
}, },
dashboard: { dashboard: {
stats: () => request<Stats>('/stats') stats: () => request<Stats>('/stats'),
sentence: () => request<{ sentence: string }>('/sentence')
}, },
settings: { settings: {
changePassword: (data: { old_password: string; new_password: string }) => changePassword: (data: { old_password: string; new_password: string }) =>
request('/settings/password', { method: 'POST', body: JSON.stringify(data) }), request('/settings/password', { method: 'POST', body: JSON.stringify(data) }),
cleanLogs: (days: number) => cleanLogs: (days: number) =>
request<{ deleted: number }>('/settings/clean-logs', { method: 'POST', body: JSON.stringify({ days }) }), request<{ deleted: number }>('/settings/clean-logs', { method: 'POST', body: JSON.stringify({ days }) }),
getSite: () => request<{ site_name: string; port: number }>('/settings/site'), getSite: () => request<SiteSettings>('/settings/site'),
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')
}, },
files: { files: {
@@ -252,3 +256,11 @@ export interface AboutInfo {
log_count: number log_count: number
env_count: number env_count: number
} }
export interface SiteSettings {
title: string
subtitle: string
icon: string
page_size: string
cookie_days: string
}
+1 -1
View File
@@ -24,7 +24,7 @@ const modelValue = useVModel(props, "modelValue", emits, {
v-model="modelValue" v-model="modelValue"
data-slot="input" data-slot="input"
:class="cn( :class="cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-7 w-full min-w-0 rounded-md border bg-transparent px-2 py-1 text-xs shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50', 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
props.class, props.class,
+38
View File
@@ -0,0 +1,38 @@
import { ref } from 'vue'
import { api, type SiteSettings } from '@/api'
const siteSettings = ref<SiteSettings>({
title: '白虎面板',
subtitle: '轻量级定时任务管理系统',
icon: "<svg t=\"1766107903919\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" p-id=\"1942\" width=\"200\" height=\"200\"><path d=\"M884.992 273.05984c4.10624 0 5.0688-2.36544 2.10944-5.25312 0 0-64.28672-65.55648-111.7696-75.55072-47.48288-9.984-45.47584-59.37152-80.56832-75.02848-72.0896-32.16384-158.6176-34.3552-158.6176-34.3552s-91.37152-4.92544-138.752-9.89184c-19.46624-2.03776-54.46656-9.58464-54.46656-9.58464-4.0448-0.84992-10.63936-1.19808-14.66368-0.44032 0 0-30.63808-0.07168-44.30848 43.35616-8.8576 28.11904 1.792 104.79616 1.792 104.79616 1.46432 12.27776-3.42016 30.21824-10.72128 40.20224 0 0-36.77184 46.03904-58.9312 100.34176-22.15936 54.30272 118.15936 145.05984 208.98816 205.27104C507.82208 613.89824 502.03648 743.424 502.03648 743.424s-74.19904-96.75776-194.00704-156.9792C188.2112 526.22336 150.30272 442.23488 150.30272 442.23488c-2.89792-5.45792-5.94944-4.94592-6.71744 1.21856 0 0-15.1552 91.61728 16.25088 147.8144 70.92224 126.88384 141.74208 112.88576 197.03808 183.27552s54.272 164.9152 54.272 164.9152-97.62816-141.29152-235.66336-205.55776c-91.53536-61.27616-74.7008-125.91104-85.49376-101.85728-10.79296 24.05376 26.73664 192.41984 65.40288 222.2592 80.57856 62.18752 94.16704 101.98016 94.16704 101.98016h175.53408S544.512 814.85824 572.928 725.73952c44.41088-139.30496 40.20224-191.26272 40.20224-191.26272 0.08192-8.22272 6.81984-14.19264 14.98112-13.29152 0 0 46.45888 4.64896 66.64192 9.68704 23.53152 5.86752 55.35744 26.20416 55.35744 26.20416 3.49184 2.14016 7.39328 0.68608 8.69376-3.1744l34.4576-102.77888c1.30048-3.8912-0.63488-5.51936-4.352-3.75808 0 0-45.37344 25.09824-88.17664 12.1856-20.15232-6.08256-59.60704-14.82752-74.69056-32.6656-16.95744-20.03968-15.59552-71.3728 26.66496-79.21664 48.0256-8.9088 33.13664 15.14496 65.91488 24.64768 27.42272 7.95648 22.29248-1.69984 26.69568 5.21216 1.67936 2.63168 0.38912 32.65536 0.38912 32.65536-0.21504 6.144 3.39968 7.84384 8.0384 3.79904l41.89184-36.46464c17.37728-11.24352 30.86336-0.57344 48.24064-11.81696 14.73536-9.53344 29.58336-43.66336 29.58336-43.66336 4.48512-9.24672 0.60416-20.41856-8.63232-24.92416l-42.58816-20.80768c-3.69664-1.80224-3.38944-3.26656 0.74752-3.26656h62.0032zM422.54336 123.87328s-49.88928 50.7392-74.5472 50.66752c-24.65792-0.07168-33.24928-56.12544-3.92192-56.12544 18.00192 0 76.32896 0.21504 76.32896 0.21504 4.13696 0.02048 5.0688 2.36544 2.14016 5.24288z m123.09504 249.64096s-3.31776-25.53856-33.16736-40.05888c-29.8496-14.52032-52.5312-41.13408-59.648-68.95616-12.1856-54.8864 48.29184-104.192 48.29184-104.192s-30.1056 73.6768 3.5328 106.60864c54.272 53.12512 40.99072 106.5984 40.99072 106.5984z m155.56608-164.46464c-7.94624 10.32192-9.60512 37.92896-59.2384 20.736-49.63328-17.2032-71.00416-54.5792-71.00416-54.5792-2.27328-3.40992-0.79872-6.49216 3.328-6.8096 0 0 43.55072-5.03808 80.06656 6.44096 24.91392 7.82336 54.79424 23.88992 46.848 34.21184z\" fill=\"#272636\" p-id=\"1943\"></path><path d=\"M366.30528 259.26656c1.05472-1.76128 1.51552-1.57696 1.19808 0.43008 0 0-7.55712 19.0464 22.8864 87.63392 27.0848 61.02016 87.49056 68.7104 118.66112 98.23232 55.808 52.82816 53.52448 123.45344 53.52448 123.45344s-25.82528-49.85856-85.98528-78.83776-92.6208-50.52416-127.7952-85.77024c-45.02528-45.12768 17.5104-145.14176 17.5104-145.14176zM500.0704 961.44384h134.49216s95.8464-138.07616 46.68416-291.25632c-16.19968-50.46272-43.45856-80.00512-43.45856-80.00512-5.18144-6.38976-8.89856-4.88448-8.448 3.34848 0 0 9.15456 99.80928-19.44576 194.00704-28.60032 94.208-109.824 173.90592-109.824 173.90592zM681.61536 956.8768h105.24672s22.38464-73.5232 16.61952-130.21184c-9.30816-91.57632-48.31232-132.72064-48.31232-132.72064-3.80928-4.77184-6.0928-3.67616-5.2224 2.38592 0 0 16.75264 89.1904-14.4896 150.1184-30.9248 60.30336-53.84192 110.42816-53.84192 110.42816zM869.30432 811.35616c-2.88768-2.9184-4.80256-1.95584-4.38272 2.10944 0 0 6.79936 44.99456-7.76192 81.22368-10.12736 25.1904-28.91776 58.60352-28.91776 58.60352h107.17184s5.34528-50.31936-19.89632-83.97824c-11.56096-23.99232-46.21312-57.9584-46.21312-57.9584z\" fill=\"#272636\" p-id=\"1944\"></path></svg>",
page_size: '10',
cookie_days: '7'
})
let loaded = false
export function useSiteSettings() {
async function loadSettings() {
if (loaded) return
try {
const res = await api.settings.getSite()
siteSettings.value = res
document.title = res.title || '白虎面板'
loaded = true
} catch {
// 使用默认值
}
}
function refreshSettings() {
loaded = false
return loadSettings()
}
return {
siteSettings,
loadSettings,
refreshSettings
}
}
+15 -13
View File
@@ -5,10 +5,11 @@ import { LayoutDashboard, ListTodo, FileCode, Settings, LogOut, ScrollText, Term
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import ThemeToggle from '@/components/ThemeToggle.vue' import ThemeToggle from '@/components/ThemeToggle.vue'
import { api } from '@/api' import { api } from '@/api'
import * as jinrishici from 'jinrishici' import { useSiteSettings } from '@/composables/useSiteSettings'
const route = useRoute() const route = useRoute()
const sentence = ref('欢迎使用白虎面板') const sentence = ref('欢迎使用白虎面板')
const { siteSettings, loadSettings } = useSiteSettings()
const navItems = [ const navItems = [
{ to: '/', icon: LayoutDashboard, label: '数据仪表', exact: true }, { to: '/', icon: LayoutDashboard, label: '数据仪表', exact: true },
@@ -20,7 +21,7 @@ const navItems = [
{ to: '/settings', icon: Settings, label: '系统设置', exact: true }, { to: '/settings', icon: Settings, label: '系统设置', exact: true },
] ]
function isItemActive(item: typeof navItems[0]) { function isItemActive(item: (typeof navItems)[0]) {
if (item.exact) { if (item.exact) {
return route.path === item.to return route.path === item.to
} }
@@ -36,18 +37,19 @@ async function logout() {
window.location.href = '/login' window.location.href = '/login'
} }
function loadSentence() { async function loadSentence() {
jinrishici.load( try {
(result: { data: { content: string } }) => { const res = await api.dashboard.sentence()
sentence.value = result.data.content sentence.value = res.sentence
}, } catch {
() => { // 加载失败保持默认
// 加载失败保持默认 }
}
)
} }
onMounted(loadSentence) onMounted(() => {
loadSettings()
loadSentence()
})
</script> </script>
<template> <template>
@@ -55,7 +57,7 @@ onMounted(loadSentence)
<!-- Sidebar --> <!-- Sidebar -->
<aside class="w-56 border-r bg-background flex flex-col"> <aside class="w-56 border-r bg-background flex flex-col">
<div class="h-14 flex items-center px-10 font-semibold text-lg border-b"> <div class="h-14 flex items-center px-10 font-semibold text-lg border-b">
白虎面板 {{ siteSettings.title }}
</div> </div>
<nav class="flex-1 px-6 py-9 space-y-1"> <nav class="flex-1 px-6 py-9 space-y-1">
<RouterLink <RouterLink
+19 -3
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -13,6 +13,20 @@ const username = ref('')
const password = ref('') const password = ref('')
const loading = ref(false) 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() { async function handleLogin() {
loading.value = true loading.value = true
try { try {
@@ -25,6 +39,8 @@ async function handleLogin() {
loading.value = false loading.value = false
} }
} }
onMounted(loadSiteSettings)
</script> </script>
<template> <template>
@@ -40,8 +56,8 @@ async function handleLogin() {
<div class="w-96 p-10"> <div class="w-96 p-10">
<div class="space-y-8"> <div class="space-y-8">
<div class="space-y-2"> <div class="space-y-2">
<h1 class="text-2xl font-bold tracking-tight">白虎面板</h1> <h1 class="text-2xl font-bold tracking-tight">{{ siteTitle }}</h1>
<p class="text-muted-foreground">轻量级定时任务管理系统</p> <p class="text-muted-foreground">{{ siteSubtitle }}</p>
</div> </div>
<form @submit.prevent="handleLogin" class="space-y-5"> <form @submit.prevent="handleLogin" class="space-y-5">
<div class="space-y-2"> <div class="space-y-2">
+17 -17
View File
@@ -21,36 +21,36 @@ onMounted(loadAbout)
<template> <template>
<div> <div>
<!-- 站点关于 --> <!-- 站点关于 -->
<div class="mb-8"> <div class="mb-6">
<h3 class="text-xl font-semibold mb-2">白虎面板</h3> <h3 class="text-lg font-semibold mb-1">白虎面板</h3>
<p class="text-muted-foreground">一个轻量级的定时任务管理系统支持脚本管理定时执行和日志追踪</p> <p class="text-sm text-muted-foreground">一个轻量级的定时任务管理系统支持脚本管理定时执行和日志追踪</p>
</div> </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> <div>
<h4 class="text-sm font-medium mb-4">技术栈</h4> <h4 class="text-sm font-medium mb-2">技术栈</h4>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-1.5">
<Badge v-for="tech in techStack" :key="tech" variant="secondary">{{ tech }}</Badge> <Badge v-for="tech in techStack" :key="tech" class="text-xs bg-primary/15 text-primary border-0">{{ tech }}</Badge>
</div> </div>
</div> </div>
<div> <div>
<h4 class="text-sm font-medium mb-4">功能特性</h4> <h4 class="text-sm font-medium mb-2">功能特性</h4>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-1.5">
<Badge v-for="feature in features" :key="feature" variant="outline">{{ feature }}</Badge> <Badge v-for="feature in features" :key="feature" class="text-xs bg-accent text-accent-foreground">{{ feature }}</Badge>
</div> </div>
</div> </div>
</div> </div>
<!-- 右侧系统信息 --> <!-- 右侧系统信息 -->
<div> <div>
<h4 class="text-sm font-medium mb-4">系统信息</h4> <h4 class="text-sm font-medium mb-2">系统信息</h4>
<div class="space-y-3"> <div class="space-y-2">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-muted-foreground text-sm">系统版本:</span> <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>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-muted-foreground text-sm">构建时间:</span> <span class="text-muted-foreground text-sm">构建时间:</span>
@@ -69,9 +69,9 @@ onMounted(loadAbout)
</div> </div>
<!-- 底部版权和链接 --> <!-- 底部版权和链接 -->
<div class="mt-10 pt-6 border-t flex items-center justify-center gap-2 text-sm text-muted-foreground"> <div class="mt-6 pt-4 border-t flex items-center justify-center gap-2 text-sm text-muted-foreground">
<span>© 2025 保留所有权利</span> <span>© {{ new Date().getFullYear() }} 保留所有权利</span>
<a href="https://github.com" target="_blank" class="inline-flex items-center gap-1 text-primary hover:underline"> <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" /> <ExternalLink class="h-3.5 w-3.5" />
GitHub 仓库 GitHub 仓库
</a> </a>
+3 -3
View File
@@ -50,10 +50,10 @@ const activeTab = ref('password')
</TabsContent> </TabsContent>
<TabsContent value="site" class="mt-6"> <TabsContent value="site" class="mt-6">
<Card class="max-w-lg"> <Card class="max-w-xl">
<CardHeader> <CardHeader>
<CardTitle>站点设置</CardTitle> <CardTitle>站点设置</CardTitle>
<CardDescription>查看当前站点配置信息</CardDescription> <CardDescription>配置站点标题图标和系统参数</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<SiteSettings /> <SiteSettings />
@@ -62,7 +62,7 @@ const activeTab = ref('password')
</TabsContent> </TabsContent>
<TabsContent value="about" class="mt-6"> <TabsContent value="about" class="mt-6">
<Card class="max-w-3xl"> <Card class="max-w-xl">
<CardContent class="pt-6"> <CardContent class="pt-6">
<AboutSettings /> <AboutSettings />
</CardContent> </CardContent>
+71 -15
View File
@@ -1,33 +1,89 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' 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 { refreshSettings } = useSiteSettings()
const sitePort = ref(0)
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 { try {
const res = await api.settings.getSite() const res = await api.settings.getSite()
siteName.value = res.site_name || '白虎面板' form.value = res
sitePort.value = res.port
} catch {} } 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> </script>
<template> <template>
<div class="space-y-4"> <div class="space-y-4">
<div class="space-y-2"> <div class="grid grid-cols-4 items-center gap-4">
<Label>站点名称</Label> <Label class="text-right">站点标题</Label>
<Input v-model="siteName" disabled class="max-w-sm" /> <Input v-model="form.title" placeholder="白虎面板" class="col-span-3" />
</div> </div>
<div class="space-y-2"> <div class="grid grid-cols-4 items-center gap-4">
<Label>端口</Label> <Label class="text-right">站点标语</Label>
<Input v-model.number="sitePort" type="number" class="w-32" disabled /> <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> </div>
<p class="text-sm text-muted-foreground">站点设置需要修改配置文件后重启服务</p>
</div> </div>
</template> </template>
+37 -14
View File
@@ -23,6 +23,16 @@ const pageSize = ref(10)
const total = ref(0) const total = ref(0)
let searchTimer: ReturnType<typeof setTimeout> | null = null 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() { async function loadTasks() {
try { try {
const res = await api.tasks.list({ page: currentPage.value, page_size: pageSize.value, name: filterName.value || undefined }) const res = await api.tasks.list({ page: currentPage.value, page_size: pageSize.value, name: filterName.value || undefined })
@@ -168,26 +178,39 @@ onMounted(loadTasks)
</div> </div>
<Dialog v-model:open="showDialog"> <Dialog v-model:open="showDialog">
<DialogContent class="max-w-md"> <DialogContent class="sm:max-w-[425px]">
<DialogHeader> <DialogHeader>
<DialogTitle>{{ isEdit ? '编辑任务' : '新建任务' }}</DialogTitle> <DialogTitle>{{ isEdit ? '编辑任务' : '新建任务' }}</DialogTitle>
</DialogHeader> </DialogHeader>
<div class="space-y-4 py-2"> <div class="grid gap-4 py-4">
<div class="space-y-2"> <div class="grid grid-cols-4 items-center gap-4">
<Label>任务名称</Label> <Label class="text-right">任务名称</Label>
<Input v-model="editingTask.name" placeholder="任务名称" /> <Input v-model="editingTask.name" placeholder="我的任务" class="col-span-3" />
</div> </div>
<div class="space-y-2"> <div class="grid grid-cols-4 items-center gap-4">
<Label>执行命令</Label> <Label class="text-right">执行命令</Label>
<Input v-model="editingTask.command" class="font-mono" placeholder="node script.js" /> <Input v-model="editingTask.command" placeholder="node script.js" class="col-span-3 font-mono" />
</div> </div>
<div class="space-y-2"> <div class="grid grid-cols-4 items-center gap-4">
<Label>定时规则 (Cron)</Label> <Label class="text-right">定时规则</Label>
<Input v-model="editingTask.schedule" class="font-mono" placeholder="0 * * * *" /> <Input v-model="editingTask.schedule" placeholder="0 * * * *" class="col-span-3 font-mono" />
</div> </div>
<div class="space-y-2"> <div class="grid grid-cols-4 items-start gap-4">
<Label>超时时间 (分钟)</Label> <span></span>
<Input v-model.number="editingTask.timeout" type="number" placeholder="30" /> <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>
</div> </div>
<DialogFooter> <DialogFooter>