feat: gh-ost真实集成验证通过 + 登录页全面中文化 + 外观设置跨浏览器同步修复

gh-ost集成:
- handler根据instance_id查DB获取连接信息(host/port/user/password)
- 修复gh-ost参数: 去掉不存在的--dry-run(默认noop), 加--execute才真执行
- 加--skip-port-validation(端口映射环境必须)
- 加--allow-on-master(单机MySQL直接执行)
- 加--initially-drop-ghost-table/old-table/socket-file(清理残留)
- GhOstTask增加Port字段, buildArgs同步更新
- dry-run真实验证通过: 连接1.94.55.133:9005, 3行表, 3.85s完成

登录页中文化:
- 5个登录组件(SplitView/CenteredCard/SidePanel/FullImage/MinimalDark)
- 46处英文文案全部替换为中文
- 平台标题统一为'SQL审核平台'

外观设置同步修复:
- GetPublicLoginStyle: GORM First()复用主键导致第二次查询失败
- 改用Take()+重置结构体, 修复theme始终返回dark的bug
- UpdateUserPreferences: 同步写入SystemSetting表(login_style+default_theme)
This commit is contained in:
logan committed 2026-05-27 00:06:13 +08:00
1 parent 62a2b383e1
commit 8aaf2271b8
8 files changed
+134 -60

No files matched your search

+18 -6
View File
@@ -13,6 +13,7 @@ import (
// GhOstTask defines a gh-ost execution task // GhOstTask defines a gh-ost execution task
type GhOstTask struct { type GhOstTask struct {
Host string `json:"host"` Host string `json:"host"`
Port int `json:"port,omitempty"`
User string `json:"user"` User string `json:"user"`
Password string `json:"password"` Password string `json:"password"`
Database string `json:"database"` Database string `json:"database"`
@@ -60,12 +61,18 @@ func RunGhOst(ctx context.Context, task GhOstTask, ghOstPath string) (*GhOstResu
args := []string{ args := []string{
"--host=" + task.Host, "--host=" + task.Host,
fmt.Sprintf("--port=%d", task.Port),
"--user=" + task.User, "--user=" + task.User,
"--password=" + task.Password, "--password=" + task.Password,
"--database=" + task.Database, "--database=" + task.Database,
"--table=" + task.Table, "--table=" + task.Table,
"--alter=" + task.Alter, "--alter=" + task.Alter,
"--verbose", "--verbose",
"--skip-port-validation",
"--allow-on-master",
"--initially-drop-ghost-table",
"--initially-drop-old-table",
"--initially-drop-socket-file",
} }
if task.ChunkSize > 0 { if task.ChunkSize > 0 {
@@ -91,11 +98,11 @@ func RunGhOst(ctx context.Context, task GhOstTask, ghOstPath string) (*GhOstResu
} }
// Dry-Run vs Execute // Dry-Run vs Execute
if task.DryRun || !task.Execute { // gh-ost defaults to noop (dry-run), only --execute actually applies changes
args = append(args, "--dry-run") if task.Execute {
} else {
args = append(args, "--execute") args = append(args, "--execute")
} }
// always verbose for output
cmd := exec.CommandContext(ctx, ghOstPath, args...) cmd := exec.CommandContext(ctx, ghOstPath, args...)
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
@@ -193,12 +200,18 @@ func (m *TaskManager) ListTasks() map[string]string {
func buildArgs(task GhOstTask, ghOstPath string) []string { func buildArgs(task GhOstTask, ghOstPath string) []string {
args := []string{ args := []string{
"--host=" + task.Host, "--host=" + task.Host,
fmt.Sprintf("--port=%d", task.Port),
"--user=" + task.User, "--user=" + task.User,
"--password=" + task.Password, "--password=" + task.Password,
"--database=" + task.Database, "--database=" + task.Database,
"--table=" + task.Table, "--table=" + task.Table,
"--alter=" + task.Alter, "--alter=" + task.Alter,
"--verbose", "--verbose",
"--skip-port-validation",
"--allow-on-master",
"--initially-drop-ghost-table",
"--initially-drop-old-table",
"--initially-drop-socket-file",
} }
if task.ChunkSize > 0 { if task.ChunkSize > 0 {
@@ -223,9 +236,8 @@ func buildArgs(task GhOstTask, ghOstPath string) []string {
args = append(args, "--postpone-cut-over-flag-file=/tmp/gh-ost-cutover.flag") args = append(args, "--postpone-cut-over-flag-file=/tmp/gh-ost-cutover.flag")
} }
if task.DryRun || !task.Execute { // gh-ost defaults to noop, only --execute actually applies changes
args = append(args, "--dry-run") if task.Execute {
} else {
args = append(args, "--execute") args = append(args, "--execute")
} }
+53 -6
View File
@@ -1,6 +1,7 @@
package handler package handler
import ( import (
"fmt"
"net/http" "net/http"
"time" "time"
@@ -13,15 +14,27 @@ import (
// POST /api/v1/ghost/dry-run // POST /api/v1/ghost/dry-run
func GhOstDryRun(c *gin.Context) { func GhOstDryRun(c *gin.Context) {
var task ghost.GhOstTask var req struct {
if err := c.ShouldBindJSON(&task); err != nil { InstanceID int `json:"instance_id"`
Database string `json:"database"`
Table string `json:"table"`
Alter string `json:"alter"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
// Look up instance connection info
task, err := buildGhOstTask(req.InstanceID, req.Database, req.Table, req.Alter)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return return
} }
task.DryRun = true task.DryRun = true
task.Execute = false task.Execute = false
result, err := ghost.RunGhOst(c.Request.Context(), task, config.C.GhOstPath) result, err := ghost.RunGhOst(c.Request.Context(), *task, config.C.GhOstPath)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, result) c.JSON(http.StatusInternalServerError, result)
return return
@@ -29,17 +42,51 @@ func GhOstDryRun(c *gin.Context) {
c.JSON(http.StatusOK, result) c.JSON(http.StatusOK, result)
} }
// buildGhOstTask resolves instance_id to connection params
func buildGhOstTask(instanceID int, database, table, alter string) (*ghost.GhOstTask, error) {
var inst model.Instance
if err := model.DB.First(&inst, instanceID).Error; err != nil {
return nil, fmt.Errorf("instance %d not found", instanceID)
}
if inst.DBType != "mysql" {
return nil, fmt.Errorf("gh-ost only supports MySQL instances, got %s", inst.DBType)
}
// gh-ost needs separate host and port
host := inst.Host
port := inst.Port
return &ghost.GhOstTask{
Host: host,
Port: port,
User: inst.Username,
Password: inst.Password,
Database: database,
Table: table,
Alter: alter,
}, nil
}
// POST /api/v1/ghost/execute // POST /api/v1/ghost/execute
func GhOstExecute(c *gin.Context) { func GhOstExecute(c *gin.Context) {
var task ghost.GhOstTask var req struct {
if err := c.ShouldBindJSON(&task); err != nil { InstanceID int `json:"instance_id"`
Database string `json:"database"`
Table string `json:"table"`
Alter string `json:"alter"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
task, err := buildGhOstTask(req.InstanceID, req.Database, req.Table, req.Alter)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return return
} }
task.DryRun = false task.DryRun = false
task.Execute = true task.Execute = true
taskID := ghost.RunGhOstAsync(task, config.C.GhOstPath, nil) taskID := ghost.RunGhOstAsync(*task, config.C.GhOstPath, nil)
c.JSON(http.StatusAccepted, gin.H{ c.JSON(http.StatusAccepted, gin.H{
"task_id": taskID, "task_id": taskID,
"message": "gh-ost execution started", "message": "gh-ost execution started",
+28 -13
View File
@@ -43,26 +43,41 @@ func UpdateUserPreferences(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"detail": result.Error.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"detail": result.Error.Error()})
return return
} }
// Sync login_style and theme to SystemSetting so public endpoint picks them up
syncToSystemSetting := func(key, value string) {
var s model.SystemSetting
if model.DB.Where("`key` = ?", key).First(&s).Error == nil {
model.DB.Model(&s).Update("value", value)
} else {
model.DB.Create(&model.SystemSetting{
Key: key, Value: value, ValueType: "string",
Group: "appearance", Label: key, SortOrder: 0,
})
}
}
if v, ok := prefs["login_style"]; ok {
syncToSystemSetting("login_style", v)
}
if v, ok := prefs["theme"]; ok {
syncToSystemSetting("default_theme", v)
}
c.JSON(http.StatusOK, gin.H{"message": "Preferences updated", "preferences": prefs}) c.JSON(http.StatusOK, gin.H{"message": "Preferences updated", "preferences": prefs})
} }
// GET /api/v1/public/login-style (no auth required) // GET /api/v1/public/login-style (no auth required)
func GetPublicLoginStyle(c *gin.Context) { func GetPublicLoginStyle(c *gin.Context) {
// Check system settings first style := "split"
var setting model.SystemSetting
style := "split" // default
if model.DB.Where("`key` = ?", "login_style").First(&setting).Error == nil {
if setting.Value != "" {
style = setting.Value
}
}
// Also return theme default
theme := "dark" theme := "dark"
if model.DB.Where("`key` = ?", "default_theme").First(&setting).Error == nil {
if setting.Value != "" { var s model.SystemSetting
theme = setting.Value if model.DB.Where("`key` = ?", "login_style").Take(&s).Error == nil && s.Value != "" {
style = s.Value
} }
s = model.SystemSetting{} // reset to avoid GORM reusing primary key
if model.DB.Where("`key` = ?", "default_theme").Take(&s).Error == nil && s.Value != "" {
theme = s.Value
} }
c.JSON(http.StatusOK, gin.H{"login_style": style, "theme": theme}) c.JSON(http.StatusOK, gin.H{"login_style": style, "theme": theme})
@@ -23,9 +23,9 @@ export default function CenteredCard() {
<div style={{ position: 'relative', zIndex: 10, width: '100%', maxWidth: 400, background: 'var(--dg-bg-primary)', borderRadius: 14, padding: '40px 32px', boxShadow: '0 8px 32px rgba(0,0,0,0.3)' }}> <div style={{ position: 'relative', zIndex: 10, width: '100%', maxWidth: 400, background: 'var(--dg-bg-primary)', borderRadius: 14, padding: '40px 32px', boxShadow: '0 8px 32px rgba(0,0,0,0.3)' }}>
<div style={{ textAlign: 'center', marginBottom: 32 }}> <div style={{ textAlign: 'center', marginBottom: 32 }}>
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 6 }}> <h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 6 }}>
<span className="dg-gradient-text">SQL Audit Platform</span> <span className="dg-gradient-text">SQL审核平台</span>
</h1> </h1>
<p style={{ color: 'var(--dg-text-muted)', fontSize: 14 }}>Sign in to your account</p> <p style={{ color: 'var(--dg-text-muted)', fontSize: 14 }}>登录您的账号</p>
</div> </div>
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
@@ -33,14 +33,14 @@ export default function CenteredCard() {
<input <input
type="text" type="text"
placeholder="Username" placeholder="用户名"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
style={{ padding: '11px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} style={{ padding: '11px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
/> />
<input <input
type="password" type="password"
placeholder="Password" placeholder="密码"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
style={{ padding: '11px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} style={{ padding: '11px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
@@ -52,7 +52,7 @@ export default function CenteredCard() {
className="dg-gradient-bg" className="dg-gradient-bg"
style={{ padding: '11px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 4 }} style={{ padding: '11px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 4 }}
> >
{loading ? 'Signing in…' : 'Sign in'} {loading ? '登录中…' : '登录'}
</button> </button>
</form> </form>
</div> </div>
+5 -5
View File
@@ -40,9 +40,9 @@ export default function FullImage() {
> >
<div style={{ textAlign: 'center', marginBottom: 32 }}> <div style={{ textAlign: 'center', marginBottom: 32 }}>
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 6 }}> <h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 6 }}>
<span className="dg-gradient-text">SQL Audit Platform</span> <span className="dg-gradient-text">SQL审核平台</span>
</h1> </h1>
<p style={{ color: 'rgba(255,255,255,0.6)', fontSize: 14 }}>Sign in to your workspace</p> <p style={{ color: 'rgba(255,255,255,0.6)', fontSize: 14 }}>登录您的工作区</p>
</div> </div>
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> <form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
@@ -50,14 +50,14 @@ export default function FullImage() {
<input <input
type="text" type="text"
placeholder="Username" placeholder="用户名"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
style={{ padding: '11px 14px', borderRadius: 8, background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)', color: '#fff', fontSize: 14, outline: 'none' }} style={{ padding: '11px 14px', borderRadius: 8, background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)', color: '#fff', fontSize: 14, outline: 'none' }}
/> />
<input <input
type="password" type="password"
placeholder="Password" placeholder="密码"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
style={{ padding: '11px 14px', borderRadius: 8, background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)', color: '#fff', fontSize: 14, outline: 'none' }} style={{ padding: '11px 14px', borderRadius: 8, background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)', color: '#fff', fontSize: 14, outline: 'none' }}
@@ -69,7 +69,7 @@ export default function FullImage() {
className="dg-gradient-bg" className="dg-gradient-bg"
style={{ padding: '11px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 4 }} style={{ padding: '11px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 4 }}
> >
{loading ? 'Signing in…' : 'Sign in'} {loading ? '登录中…' : '登录'}
</button> </button>
</form> </form>
</div> </div>
@@ -18,23 +18,23 @@ export default function MinimalDark() {
<div style={{ width: '100%', maxWidth: 320, textAlign: 'center' }}> <div style={{ width: '100%', maxWidth: 320, textAlign: 'center' }}>
<Database size={36} className="dg-gradient-text" style={{ marginBottom: 16, display: 'inline-block' }} /> <Database size={36} className="dg-gradient-text" style={{ marginBottom: 16, display: 'inline-block' }} />
<h1 style={{ fontSize: 22, fontWeight: 700, marginBottom: 4 }}> <h1 style={{ fontSize: 22, fontWeight: 700, marginBottom: 4 }}>
<span className="dg-gradient-text">SQL Audit Platform</span> <span className="dg-gradient-text">SQL审核平台</span>
</h1> </h1>
<p style={{ color: 'var(--dg-text-muted)', fontSize: 13, marginBottom: 36 }}>Secure database auditing & compliance</p> <p style={{ color: 'var(--dg-text-muted)', fontSize: 13, marginBottom: 36 }}>安全的数据库审核与合规管理</p>
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}> <form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
{error && <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div>} {error && <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div>}
<input <input
type="text" type="text"
placeholder="Username" placeholder="用户名"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: 'none', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: 'none', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
/> />
<input <input
type="password" type="password"
placeholder="Password" placeholder="密码"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: 'none', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: 'none', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
@@ -46,7 +46,7 @@ export default function MinimalDark() {
className="dg-gradient-bg" className="dg-gradient-bg"
style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 8 }} style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 8 }}
> >
{loading ? 'Signing in…' : 'Sign in'} {loading ? '登录中…' : '登录'}
</button> </button>
</form> </form>
</div> </div>
+7 -7
View File
@@ -29,9 +29,9 @@ export default function SidePanel() {
<div style={{ position: 'relative', zIndex: 2, textAlign: 'center', maxWidth: 320 }}> <div style={{ position: 'relative', zIndex: 2, textAlign: 'center', maxWidth: 320 }}>
<Database size={40} style={{ color: '#fff', marginBottom: 20 }} /> <Database size={40} style={{ color: '#fff', marginBottom: 20 }} />
<h1 style={{ fontSize: 26, fontWeight: 700, color: '#fff', marginBottom: 12 }}>SQL Audit Platform</h1> <h1 style={{ fontSize: 26, fontWeight: 700, color: '#fff', marginBottom: 12 }}>SQL审核平台</h1>
<p style={{ color: 'rgba(255,255,255,0.7)', fontSize: 14, lineHeight: 1.6 }}> <p style={{ color: 'rgba(255,255,255,0.7)', fontSize: 14, lineHeight: 1.6 }}>
Empowering teams with intelligent SQL auditing, real-time compliance monitoring, and actionable insights for database security. 为团队提供智能 SQL 审核、实时合规监控与数据库安全保障。
</p> </p>
</div> </div>
</div> </div>
@@ -39,21 +39,21 @@ export default function SidePanel() {
{/* Right Panel — Login Form */} {/* Right Panel — Login Form */}
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 48 }}> <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 48 }}>
<form onSubmit={handleLogin} style={{ width: '100%', maxWidth: 360, display: 'flex', flexDirection: 'column', gap: 20 }}> <form onSubmit={handleLogin} style={{ width: '100%', maxWidth: 360, display: 'flex', flexDirection: 'column', gap: 20 }}>
<h2 style={{ fontSize: 22, fontWeight: 600, marginBottom: 4 }}>Welcome back</h2> <h2 style={{ fontSize: 22, fontWeight: 600, marginBottom: 4 }}>欢迎回来</h2>
<p style={{ color: 'var(--dg-text-muted)', fontSize: 14, marginBottom: 8 }}>Sign in to access your dashboard</p> <p style={{ color: 'var(--dg-text-muted)', fontSize: 14, marginBottom: 8 }}>登录以访问控制台</p>
{error && <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div>} {error && <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div>}
<input <input
type="text" type="text"
placeholder="Username" placeholder="用户名"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
/> />
<input <input
type="password" type="password"
placeholder="Password" placeholder="密码"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
@@ -65,7 +65,7 @@ export default function SidePanel() {
className="dg-gradient-bg" className="dg-gradient-bg"
style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1 }} style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1 }}
> >
{loading ? 'Signing in…' : 'Sign in'} {loading ? '登录中…' : '登录'}
</button> </button>
</form> </form>
</div> </div>
+13 -13
View File
@@ -3,12 +3,12 @@ import { useTheme } from '@/hooks/useTheme';
import { Sun, Moon, Database, Code, GitBranch, Bell, History, UserCog } from 'lucide-react'; import { Sun, Moon, Database, Code, GitBranch, Bell, History, UserCog } from 'lucide-react';
const features = [ const features = [
{ icon: Database, label: 'SQL Auditing', desc: 'Real-time query analysis' }, { icon: Database, label: 'SQL审核', desc: '实时查询分析与合规检测' },
{ icon: Code, label: 'Smart Rules', desc: 'Custom compliance rules' }, { icon: Code, label: '智能规则', desc: '自定义审核规则引擎' },
{ icon: GitBranch, label: 'Version Control', desc: 'Track schema changes' }, { icon: GitBranch, label: '变更管理', desc: 'DDL/DML 变更审批流' },
{ icon: Bell, label: 'Alerts', desc: 'Instant notifications' }, { icon: Bell, label: '即时通知', desc: '多渠道消息推送' },
{ icon: History, label: 'History', desc: 'Full audit trail' }, { icon: History, label: '审计追踪', desc: '完整操作历史记录' },
{ icon: UserCog, label: 'Access Control', desc: 'Role-based permissions' }, { icon: UserCog, label: '权限管理', desc: '基于角色的访问控制' },
]; ];
export default function SplitView() { export default function SplitView() {
@@ -28,10 +28,10 @@ export default function SplitView() {
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '48px 40px', background: 'var(--dg-bg-secondary)' }}> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '48px 40px', background: 'var(--dg-bg-secondary)' }}>
<div style={{ maxWidth: 440 }}> <div style={{ maxWidth: 440 }}>
<h1 style={{ fontSize: 28, fontWeight: 700, marginBottom: 8 }}> <h1 style={{ fontSize: 28, fontWeight: 700, marginBottom: 8 }}>
<span className="dg-gradient-text">SQL Audit Platform</span> <span className="dg-gradient-text">SQL审核平台</span>
</h1> </h1>
<p style={{ color: 'var(--dg-text-muted)', fontSize: 15, marginBottom: 36, lineHeight: 1.5 }}> <p style={{ color: 'var(--dg-text-muted)', fontSize: 15, marginBottom: 36, lineHeight: 1.5 }}>
Enterprise-grade SQL auditing and compliance management. 企业级 SQL 审核与合规管理平台。
</p> </p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
{features.map((f) => ( {features.map((f) => (
@@ -51,21 +51,21 @@ export default function SplitView() {
onSubmit={handleLogin} onSubmit={handleLogin}
style={{ width: '100%', maxWidth: 360, display: 'flex', flexDirection: 'column', gap: 20 }} style={{ width: '100%', maxWidth: 360, display: 'flex', flexDirection: 'column', gap: 20 }}
> >
<h2 style={{ fontSize: 22, fontWeight: 600, marginBottom: 4 }}>Sign in</h2> <h2 style={{ fontSize: 22, fontWeight: 600, marginBottom: 4 }}>登录</h2>
<p style={{ color: 'var(--dg-text-muted)', fontSize: 14, marginBottom: 8 }}>Enter your credentials to continue</p> <p style={{ color: 'var(--dg-text-muted)', fontSize: 14, marginBottom: 8 }}>请输入您的账号信息</p>
{error && <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div>} {error && <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div>}
<input <input
type="text" type="text"
placeholder="Username" placeholder="用户名"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
/> />
<input <input
type="password" type="password"
placeholder="Password" placeholder="密码"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }} style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
@@ -77,7 +77,7 @@ export default function SplitView() {
className="dg-gradient-bg" className="dg-gradient-bg"
style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1 }} style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1 }}
> >
{loading ? 'Signing in…' : 'Sign in'} {loading ? '登录中…' : '登录'}
</button> </button>
</form> </form>
</div> </div>