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 = () => {
}
+ icon={}
onClick={handleGitHubLogin}
className={`h-10 rounded-xl border-slate-200 text-slate-700 font-medium hover:border-slate-300 hover:text-slate-900 ${!oidcEnabled ? 'col-span-2' : ''}`}
>
@@ -157,7 +160,7 @@ const Login = () => {
} // 换了一个更通用的图标
+ icon={} // 换了一个更通用的图标
onClick={handleOIDCLogin}
className={`h-10 rounded-xl border-slate-200 text-slate-700 font-medium hover:border-slate-300 hover:text-slate-900 ${!githubEnabled ? 'col-span-2' : ''}`}
>