From a551dda28b58016beec82f26a5013c1fe28fe451 Mon Sep 17 00:00:00 2001 From: yumusb <43062104+yumusb@users.noreply.github.com> Date: Wed, 24 Dec 2025 21:42:11 +0800 Subject: [PATCH 1/2] feat: Refactor system configuration injection to use a JSON script tag and client-side `init.ts`, adding support for custom CSS and JS. --- internal/app.go | 8 ++- internal/models/property.go | 2 + web/index.html | 31 +++++----- web/src/api/property.ts | 2 + web/src/init.ts | 52 +++++++++++++++++ web/src/main.tsx | 9 +-- web/src/pages/Settings/SystemConfig.tsx | 76 ++++++++++++++++++------- 7 files changed, 136 insertions(+), 44 deletions(-) create mode 100644 web/src/init.ts diff --git a/internal/app.go b/internal/app.go index e02d517..314099a 100644 --- a/internal/app.go +++ b/internal/app.go @@ -3,6 +3,7 @@ package internal import ( "bytes" "context" + "encoding/json" "html/template" "log" "net/http" @@ -109,7 +110,12 @@ func setupApi(app *orz.App, components *AppComponents) { e.Use(middleware.Recover()) e.Use(ErrorHandler(logger)) - indexTemplate, err := template.New("index").Parse(web.IndexHtml()) + indexTemplate, err := template.New("index").Funcs(template.FuncMap{ + "json": func(v interface{}) template.JS { + b, _ := json.Marshal(v) + return template.JS(b) + }, + }).Parse(web.IndexHtml()) if err != nil { logger.Fatal("failed to parse index.html", zap.Error(err)) } diff --git a/internal/models/property.go b/internal/models/property.go index 15284d3..5bc283b 100644 --- a/internal/models/property.go +++ b/internal/models/property.go @@ -60,6 +60,8 @@ type SystemConfig struct { LogoBase64 string `json:"logoBase64"` // 系统logo(base64编码) ICPCode string `json:"icpCode"` // ICP备案号 DefaultView string `json:"defaultView"` // 默认视图 grid | list + CustomCSS string `json:"customCSS"` // 自定义 CSS + CustomJS string `json:"customJS"` // 自定义 JS } // AlertConfig 全局告警配置 diff --git a/web/index.html b/web/index.html index 47de1bb..8b1b620 100644 --- a/web/index.html +++ b/web/index.html @@ -1,22 +1,17 @@ - - - - - {{.SystemNameZh}} | {{.SystemNameEn}} - - - -
- + + + + + {{.SystemNameZh}} | {{.SystemNameEn}} + + + + +
+ - + + \ No newline at end of file diff --git a/web/src/api/property.ts b/web/src/api/property.ts index 0ad6fa8..5616c05 100644 --- a/web/src/api/property.ts +++ b/web/src/api/property.ts @@ -86,6 +86,8 @@ export interface SystemConfig { logoBase64: string; // Logo 的 base64 编码 icpCode: string; // ICP 备案号 defaultView: string; // 默认视图 grid,list + customCSS: string; // 自定义 CSS + customJS: string; // 自定义 JS } // 获取系统配置(管理后台使用) diff --git a/web/src/init.ts b/web/src/init.ts new file mode 100644 index 0000000..656568e --- /dev/null +++ b/web/src/init.ts @@ -0,0 +1,52 @@ + +// Default configuration +const defaultConfig = { + SystemNameZh: "Pika", + SystemNameEn: "Monitor", + ICPCode: "", + DefaultView: "grid", +}; + +// Initialize SystemConfig from injected JSON +try { + const el = document.getElementById('app-config'); + if (el && el.textContent) { + const config = JSON.parse(el.textContent); + + // Merge with default config and expose to window + window.SystemConfig = { + SystemNameZh: config.systemNameZh || defaultConfig.SystemNameZh, + SystemNameEn: config.systemNameEn || defaultConfig.SystemNameEn, + ICPCode: config.icpCode || defaultConfig.ICPCode, + DefaultView: config.defaultView || defaultConfig.DefaultView, + }; + + // Inject Custom CSS + if (config.customCSS) { + const style = document.createElement('style'); + style.innerHTML = config.customCSS; + document.head.appendChild(style); + } + + // Inject Custom JS + if (config.customJS) { + const injectJS = () => { + const script = document.createElement('script'); + script.innerHTML = config.customJS; + document.body.appendChild(script); + }; + + if (document.body) { + injectJS(); + } else { + window.addEventListener('DOMContentLoaded', injectJS); + } + } + } else { + // Fallback if no config element found + window.SystemConfig = defaultConfig; + } +} catch (e) { + console.error('Failed to parse app config:', e); + window.SystemConfig = defaultConfig; +} diff --git a/web/src/main.tsx b/web/src/main.tsx index 38180b9..c172597 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,6 +1,7 @@ -import {StrictMode} from 'react' -import {createRoot} from 'react-dom/client' -import {QueryClient, QueryClientProvider} from '@tanstack/react-query' +import './init' +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import './index.css' import App from './App.tsx' @@ -16,7 +17,7 @@ const queryClient = new QueryClient({ createRoot(document.getElementById('root')!).render( - + , ) diff --git a/web/src/pages/Settings/SystemConfig.tsx b/web/src/pages/Settings/SystemConfig.tsx index 88479d5..963cb65 100644 --- a/web/src/pages/Settings/SystemConfig.tsx +++ b/web/src/pages/Settings/SystemConfig.tsx @@ -1,21 +1,21 @@ -import {useEffect, useState} from 'react'; -import {App, Button, Card, Form, Input, Radio, Space, Spin, Upload} from 'antd'; -import {Upload as UploadIcon, Grid3x3, List} from 'lucide-react'; -import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; -import type {SystemConfig} from '@/api/property.ts'; -import {getSystemConfig, saveSystemConfig} from '@/api/property.ts'; -import {getErrorMessage} from '@/lib/utils.ts'; -import type {RcFile} from 'antd/es/upload/interface'; +import { useEffect, useState } from 'react'; +import { App, Button, Card, Form, Input, Radio, Space, Spin, Upload } from 'antd'; +import { Upload as UploadIcon, Grid3x3, List } from 'lucide-react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import type { SystemConfig } from '@/api/property.ts'; +import { getSystemConfig, saveSystemConfig } from '@/api/property.ts'; +import { getErrorMessage } from '@/lib/utils.ts'; +import type { RcFile } from 'antd/es/upload/interface'; const SystemConfigComponent = () => { const [form] = Form.useForm(); - const {message: messageApi} = App.useApp(); + const { message: messageApi } = App.useApp(); const queryClient = useQueryClient(); const [logoPreview, setLogoPreview] = useState(''); const [uploading, setUploading] = useState(false); // 获取系统配置 - const {data: config, isLoading} = useQuery({ + const { data: config, isLoading } = useQuery({ queryKey: ['systemConfig'], queryFn: getSystemConfig, }); @@ -25,7 +25,7 @@ const SystemConfigComponent = () => { mutationFn: saveSystemConfig, onSuccess: () => { messageApi.success('保存成功'); - queryClient.invalidateQueries({queryKey: ['systemConfig']}); + queryClient.invalidateQueries({ queryKey: ['systemConfig'] }); // 刷新页面以应用新的系统配置 window.location.reload(); }, @@ -42,6 +42,8 @@ const SystemConfigComponent = () => { systemNameZh: config.systemNameZh, icpCode: config.icpCode, defaultView: config.defaultView ?? true, // 默认为 grid 视图 + customCSS: config.customCSS, + customJS: config.customJS, }); if (config.logoBase64) { setLogoPreview(config.logoBase64); @@ -99,6 +101,8 @@ const SystemConfigComponent = () => { logoBase64: logoPreview, icpCode: values.icpCode || '', defaultView: values.defaultView ?? true, + customCSS: values.customCSS || '', + customJS: values.customJS || '', } as SystemConfig); } catch (error) { // 表单验证失败 @@ -113,6 +117,8 @@ const SystemConfigComponent = () => { systemNameZh: config.systemNameZh, icpCode: config.icpCode, defaultView: config.defaultView ?? true, + customCSS: config.customCSS, + customJS: config.customJS, }); setLogoPreview(config.logoBase64 || ''); } @@ -153,22 +159,22 @@ const SystemConfigComponent = () => { label="系统英文名称" name="systemNameEn" rules={[ - {required: true, message: '请输入系统英文名称'}, - {max: 50, message: '系统名称不能超过 50 个字符'}, + { required: true, message: '请输入系统英文名称' }, + { max: 50, message: '系统名称不能超过 50 个字符' }, ]} > - + - + @@ -176,11 +182,11 @@ const SystemConfigComponent = () => { label="ICP 备案号" name="icpCode" rules={[ - {max: 50, message: 'ICP 备案号不能超过 50 个字符'}, + { max: 50, message: 'ICP 备案号不能超过 50 个字符' }, ]} tooltip="ICP 备案号将显示在公共页面底部,例如:京ICP备12345678号" > - + { beforeUpload={beforeUpload} disabled={uploading} > - @@ -223,13 +229,41 @@ const SystemConfigComponent = () => { + + + + + + + + + + - {({getFieldValue}) => { + {({ getFieldValue }) => { const systemNameEn = getFieldValue('systemNameEn') || ''; const systemNameZh = getFieldValue('systemNameZh') || ''; return ( From 68ecd706c46e9e50b5d62f1c05b1e67971f17eca Mon Sep 17 00:00:00 2001 From: yumusb <43062104+yumusb@users.noreply.github.com> Date: Fri, 26 Dec 2025 19:34:47 +0800 Subject: [PATCH 2/2] feat: Add `passwordEnabled` configuration to conditionally display the username/password login form. --- internal/service/account_service.go | 10 ++- web/src/api/auth.ts | 1 + web/src/pages/Login/index.tsx | 111 ++++++++++++++-------------- 3 files changed, 64 insertions(+), 58 deletions(-) diff --git a/internal/service/account_service.go b/internal/service/account_service.go index 5c32e1f..0500ffc 100644 --- a/internal/service/account_service.go +++ b/internal/service/account_service.go @@ -167,15 +167,17 @@ func (s *AccountService) ValidateToken(tokenString string) (*JWTClaims, error) { // AuthConfig 认证配置 type AuthConfig struct { - OIDCEnabled bool `json:"oidcEnabled"` - GitHubEnabled bool `json:"githubEnabled"` + OIDCEnabled bool `json:"oidcEnabled"` + GitHubEnabled bool `json:"githubEnabled"` + PasswordEnabled bool `json:"passwordEnabled"` } // GetAuthConfig 获取认证配置 func (s *AccountService) GetAuthConfig() *AuthConfig { return &AuthConfig{ - OIDCEnabled: s.oidcService.IsEnabled(), - GitHubEnabled: s.githubService.IsEnabled(), + OIDCEnabled: s.oidcService.IsEnabled(), + GitHubEnabled: s.githubService.IsEnabled(), + PasswordEnabled: s.userService.IsEnabled(), } } diff --git a/web/src/api/auth.ts b/web/src/api/auth.ts index 8d13392..f355905 100644 --- a/web/src/api/auth.ts +++ b/web/src/api/auth.ts @@ -5,6 +5,7 @@ import type { LoginRequest, LoginResponse } from '../types'; export interface AuthConfig { oidcEnabled: boolean; githubEnabled: boolean; + passwordEnabled: boolean; } // OIDC 认证 URL diff --git a/web/src/pages/Login/index.tsx b/web/src/pages/Login/index.tsx index a215646..a73bf1d 100644 --- a/web/src/pages/Login/index.tsx +++ b/web/src/pages/Login/index.tsx @@ -1,18 +1,19 @@ -import {useEffect, useState} from 'react'; -import {useNavigate} from 'react-router-dom'; -import {App, Button, Form, Input} from 'antd'; -import {GithubOutlined, GlobalOutlined, LockOutlined, UserOutlined} from '@ant-design/icons'; -import {getAuthConfig, getGitHubAuthURL, getOIDCAuthURL, login} from '@/api/auth.ts'; -import type {LoginRequest} from '@/types'; +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { App, Button, Form, Input } from 'antd'; +import { GithubOutlined, GlobalOutlined, LockOutlined, UserOutlined } from '@ant-design/icons'; +import { getAuthConfig, getGitHubAuthURL, getOIDCAuthURL, login } from '@/api/auth.ts'; +import type { LoginRequest } from '@/types'; const Login = () => { const [loading, setLoading] = useState(false); const [oidcEnabled, setOidcEnabled] = useState(false); const [githubEnabled, setGithubEnabled] = useState(false); + const [passwordEnabled, setPasswordEnabled] = useState(true); const [oidcLoading, setOidcLoading] = useState(false); const [githubLoading, setGithubLoading] = useState(false); const navigate = useNavigate(); - const {message: messageApi} = App.useApp(); + const { message: messageApi } = App.useApp(); useEffect(() => { fetchAuthConfig(); @@ -23,6 +24,7 @@ const Login = () => { const response = await getAuthConfig(); setOidcEnabled(response.data.oidcEnabled); setGithubEnabled(response.data.githubEnabled); + setPasswordEnabled(response.data.passwordEnabled); } catch (error) { console.error('获取认证配置失败:', error); } @@ -32,7 +34,7 @@ const Login = () => { setLoading(true); try { const response = await login(values); - const {token, user} = response.data; + const { token, user } = response.data; localStorage.setItem('token', token); localStorage.setItem('userInfo', JSON.stringify(user)); messageApi.success('欢迎回来'); @@ -83,60 +85,61 @@ const Login = () => {

-
- - } - placeholder="用户名" - className="rounded-xl px-4 py-2.5 bg-slate-50 border-slate-200 hover:bg-white focus:bg-white transition-all" - /> - - - - } - placeholder="密码" - className="rounded-xl px-4 py-2.5 bg-slate-50 border-slate-200 hover:bg-white focus:bg-white transition-all" - /> - - - - - -
+ } + placeholder="用户名" + className="rounded-xl px-4 py-2.5 bg-slate-50 border-slate-200 hover:bg-white focus:bg-white transition-all" + /> +
+ + + } + placeholder="密码" + className="rounded-xl px-4 py-2.5 bg-slate-50 border-slate-200 hover:bg-white focus:bg-white transition-all" + /> + + + + + + + )} {/* 5. 第三方登录区域 */} {(oidcEnabled || githubEnabled) && (
- +
- 或者 + {passwordEnabled ? '或者' : '使用第三方登录'}
@@ -146,7 +149,7 @@ const Login = () => {