From 58bbc394e9221b087705aed878a016a7cb581948 Mon Sep 17 00:00:00 2001
From: "Logan.Li"
Date: Tue, 7 Jul 2026 10:39:33 +0800
Subject: [PATCH] feat: enhance redis and production IA
---
backend/internal/handler/connection_tester.go | 21 +-
backend/internal/handler/preferences.go | 2 +-
backend/internal/handler/redis.go | 188 +++++--
backend/internal/model/model.go | 2 +-
backend/router/router.go | 10 +-
frontend/src/App.tsx | 6 +-
frontend/src/api/auth.ts | 35 +-
frontend/src/api/auth/auth.ts | 34 ++
frontend/src/api/auth/index.ts | 2 +
frontend/src/api/dashboard.ts | 6 +-
frontend/src/api/dashboard/dashboard.ts | 5 +
frontend/src/api/dashboard/index.ts | 2 +
frontend/src/api/datasource/db-types.ts | 51 ++
frontend/src/api/datasource/dictionary.ts | 22 +
frontend/src/api/datasource/index.ts | 8 +
.../src/api/datasource/instance-detail.ts | 89 ++++
frontend/src/api/datasource/instance-group.ts | 38 ++
frontend/src/api/datasource/instance.ts | 15 +
frontend/src/api/datasource/mongo.ts | 24 +
frontend/src/api/datasource/redis.ts | 47 ++
frontend/src/api/dbTypes.ts | 51 +-
frontend/src/api/dictionary.ts | 22 +-
frontend/src/api/index.ts | 8 +
frontend/src/api/instance.ts | 15 +-
frontend/src/api/instanceDetail.ts | 89 +---
frontend/src/api/instanceGroup.ts | 38 +-
frontend/src/api/mongo.ts | 24 +-
frontend/src/api/notification.ts | 9 +-
frontend/src/api/query.ts | 16 +-
frontend/src/api/query/index.ts | 2 +
frontend/src/api/query/query.ts | 15 +
frontend/src/api/redis.ts | 43 +-
frontend/src/api/system/index.ts | 4 +
frontend/src/api/system/notification.ts | 9 +
frontend/src/api/system/user.ts | 25 +
frontend/src/api/system/workflow.ts | 11 +
frontend/src/api/ticket.ts | 38 +-
frontend/src/api/ticket/index.ts | 2 +
frontend/src/api/ticket/ticket.ts | 37 ++
frontend/src/api/user.ts | 25 +-
frontend/src/api/workflow.ts | 11 +-
.../src/components/DataDictionaryPanel.tsx | 474 ++++++++++++++++++
.../src/components/EditInstanceDialog.tsx | 14 +-
frontend/src/components/Layout.tsx | 7 +-
frontend/src/components/PageHeader.tsx | 8 +-
frontend/src/components/login/AuroraView.tsx | 2 +-
frontend/src/components/login/CosmosView.tsx | 4 +-
.../src/components/login/GradientMeshView.tsx | 2 +-
frontend/src/components/login/InkWashView.tsx | 2 +-
frontend/src/components/login/MinimalDark.tsx | 2 +-
.../src/components/login/NeonCityView.tsx | 2 +-
frontend/src/components/login/SidePanel.tsx | 6 +-
frontend/src/constants/menu.ts | 47 +-
frontend/src/hooks/useAuth.tsx | 28 +-
frontend/src/hooks/useLogin.tsx | 4 +-
frontend/src/hooks/useTheme.tsx | 31 +-
frontend/src/index.css | 36 +-
frontend/src/lib/constants.ts | 52 +-
frontend/src/pages/AppearanceSettings.tsx | 16 +-
frontend/src/pages/AuditLogsPage.tsx | 4 +-
frontend/src/pages/AuditRulesPage.tsx | 6 +-
frontend/src/pages/ConfigPlatformPage.tsx | 14 +-
frontend/src/pages/Dashboard.tsx | 4 +-
frontend/src/pages/DataDictionaryPage.tsx | 10 +-
frontend/src/pages/InstanceDetailPage.tsx | 48 +-
frontend/src/pages/InstancePage.tsx | 14 +-
frontend/src/pages/LoginPage.tsx | 12 +-
frontend/src/pages/MongoDBDetailPage.tsx | 2 +-
frontend/src/pages/RedisDetailPage.tsx | 406 ++++++++++++---
frontend/src/pages/RolesPage.tsx | 16 +-
frontend/src/pages/TicketCreatePage.tsx | 2 +-
frontend/src/pages/TicketDetailPage.tsx | 12 +-
frontend/src/pages/TicketsPage.tsx | 10 +-
frontend/src/pages/UsersPage.tsx | 6 +-
frontend/src/pages/WorkflowPage.tsx | 6 +-
run.sh | 13 +-
76 files changed, 1702 insertions(+), 721 deletions(-)
create mode 100644 frontend/src/api/auth/auth.ts
create mode 100644 frontend/src/api/auth/index.ts
create mode 100644 frontend/src/api/dashboard/dashboard.ts
create mode 100644 frontend/src/api/dashboard/index.ts
create mode 100644 frontend/src/api/datasource/db-types.ts
create mode 100644 frontend/src/api/datasource/dictionary.ts
create mode 100644 frontend/src/api/datasource/index.ts
create mode 100644 frontend/src/api/datasource/instance-detail.ts
create mode 100644 frontend/src/api/datasource/instance-group.ts
create mode 100644 frontend/src/api/datasource/instance.ts
create mode 100644 frontend/src/api/datasource/mongo.ts
create mode 100644 frontend/src/api/datasource/redis.ts
create mode 100644 frontend/src/api/index.ts
create mode 100644 frontend/src/api/query/index.ts
create mode 100644 frontend/src/api/query/query.ts
create mode 100644 frontend/src/api/system/index.ts
create mode 100644 frontend/src/api/system/notification.ts
create mode 100644 frontend/src/api/system/user.ts
create mode 100644 frontend/src/api/system/workflow.ts
create mode 100644 frontend/src/api/ticket/index.ts
create mode 100644 frontend/src/api/ticket/ticket.ts
create mode 100644 frontend/src/components/DataDictionaryPanel.tsx
diff --git a/backend/internal/handler/connection_tester.go b/backend/internal/handler/connection_tester.go
index 5abb406..5b81b20 100644
--- a/backend/internal/handler/connection_tester.go
+++ b/backend/internal/handler/connection_tester.go
@@ -141,12 +141,13 @@ func testRedis(ctx context.Context, host string, port int, username, pass string
redisUsername = ""
}
rdb := redis.NewClient(&redis.Options{
- Addr: addr,
- Username: redisUsername,
- Password: pass,
- DB: 0,
- DialTimeout: 10 * time.Second,
- ReadTimeout: 10 * time.Second,
+ Addr: addr,
+ Username: redisUsername,
+ Password: pass,
+ DB: 0,
+ DialTimeout: 10 * time.Second,
+ ReadTimeout: 10 * time.Second,
+ WriteTimeout: 10 * time.Second,
})
defer rdb.Close()
@@ -169,6 +170,14 @@ func testRedis(ctx context.Context, host string, port int, username, pass string
}
}
}
+ if clusterInfo, err := rdb.Do(ctx, "CLUSTER", "INFO").Text(); err == nil {
+ for _, line := range strings.Split(clusterInfo, "\n") {
+ line = strings.TrimRight(line, "\r")
+ if strings.HasPrefix(line, "cluster_state:") {
+ isCluster = strings.TrimSpace(strings.TrimPrefix(line, "cluster_state:")) == "ok"
+ }
+ }
+ }
mode := "standalone"
if isCluster {
diff --git a/backend/internal/handler/preferences.go b/backend/internal/handler/preferences.go
index bf2cb66..48d1f94 100644
--- a/backend/internal/handler/preferences.go
+++ b/backend/internal/handler/preferences.go
@@ -69,7 +69,7 @@ func UpdateUserPreferences(c *gin.Context) {
// GET /api/v1/public/login-style (no auth required)
func GetPublicLoginStyle(c *gin.Context) {
style := "split"
- theme := "dark"
+ theme := "system"
var s model.SystemSetting
if model.DB.Where("`key` = ?", "login_style").Take(&s).Error == nil && s.Value != "" {
diff --git a/backend/internal/handler/redis.go b/backend/internal/handler/redis.go
index 285f864..d0042c7 100644
--- a/backend/internal/handler/redis.go
+++ b/backend/internal/handler/redis.go
@@ -184,6 +184,13 @@ func (w *redisClientWrapper) HSet(ctx context.Context, key string, values ...int
return w.Client.HSet(ctx, key, values...)
}
+func (w *redisClientWrapper) ZAdd(ctx context.Context, key string, members ...redis.Z) *redis.IntCmd {
+ if w.isCluster && w.ClusterClient != nil {
+ return w.ClusterClient.ZAdd(ctx, key, members...)
+ }
+ return w.Client.ZAdd(ctx, key, members...)
+}
+
func (w *redisClientWrapper) Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd {
if w.isCluster && w.ClusterClient != nil {
return w.ClusterClient.Expire(ctx, key, expiration)
@@ -219,6 +226,79 @@ func (w *redisClientWrapper) SScan(ctx context.Context, key string, cursor uint6
return w.Client.SScan(ctx, key, cursor, match, count)
}
+func writeRedisKeyValue(ctx context.Context, client *redisClientWrapper, key string, keyType string, value interface{}) error {
+ switch keyType {
+ case "string":
+ return client.Set(ctx, key, fmt.Sprintf("%v", value), 0).Err()
+ case "list":
+ values, ok := value.([]interface{})
+ if !ok {
+ return fmt.Errorf("list value must be an array")
+ }
+ if len(values) == 0 {
+ return nil
+ }
+ items := make([]interface{}, 0, len(values))
+ for _, v := range values {
+ items = append(items, fmt.Sprintf("%v", v))
+ }
+ return client.RPush(ctx, key, items...).Err()
+ case "set":
+ values, ok := value.([]interface{})
+ if !ok {
+ return fmt.Errorf("set value must be an array")
+ }
+ if len(values) == 0 {
+ return nil
+ }
+ items := make([]interface{}, 0, len(values))
+ for _, v := range values {
+ items = append(items, fmt.Sprintf("%v", v))
+ }
+ return client.SAdd(ctx, key, items...).Err()
+ case "hash":
+ values, ok := value.(map[string]interface{})
+ if !ok {
+ return fmt.Errorf("hash value must be an object")
+ }
+ if len(values) == 0 {
+ return nil
+ }
+ return client.HSet(ctx, key, values).Err()
+ case "zset":
+ values, ok := value.([]interface{})
+ if !ok {
+ return fmt.Errorf("zset value must be an array")
+ }
+ members := make([]redis.Z, 0, len(values))
+ for _, row := range values {
+ m, ok := row.(map[string]interface{})
+ if !ok {
+ return fmt.Errorf("zset rows must be objects with member and score")
+ }
+ member, ok := m["member"]
+ if !ok {
+ member = m["Member"]
+ }
+ scoreRaw, ok := m["score"]
+ if !ok {
+ scoreRaw = m["Score"]
+ }
+ score, err := strconv.ParseFloat(fmt.Sprintf("%v", scoreRaw), 64)
+ if err != nil {
+ return fmt.Errorf("invalid zset score: %v", scoreRaw)
+ }
+ members = append(members, redis.Z{Member: fmt.Sprintf("%v", member), Score: score})
+ }
+ if len(members) == 0 {
+ return nil
+ }
+ return client.ZAdd(ctx, key, members...).Err()
+ default:
+ return fmt.Errorf("unsupported redis type: %s", keyType)
+ }
+}
+
func getRedisClient(instance *model.Instance, db int) (*redisClientWrapper, error) {
password := instance.Password
// Decrypt if encrypted
@@ -237,12 +317,16 @@ func getRedisClient(instance *model.Instance, db int) (*redisClientWrapper, erro
}
client := redis.NewClient(&redis.Options{
- Addr: addr,
- Username: redisUsername,
- Password: password,
- DB: db,
+ Addr: addr,
+ Username: redisUsername,
+ Password: password,
+ DB: db,
+ DialTimeout: 5 * time.Second,
+ ReadTimeout: 5 * time.Second,
+ WriteTimeout: 5 * time.Second,
})
- ctx := context.Background()
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
if err := client.Ping(ctx).Err(); err != nil {
client.Close()
return nil, fmt.Errorf("Redis connection failed: %v", err)
@@ -250,16 +334,9 @@ func getRedisClient(instance *model.Instance, db int) (*redisClientWrapper, erro
// Check cluster mode flag for wrapper behavior (ScanAllKeys needs to know)
isCluster := instance.ClusterMode != nil && *instance.ClusterMode
- wrapper := &redisClientWrapper{Client: client, isCluster: isCluster}
- if !isCluster {
- return wrapper, nil
- }
-
- // Redis Cluster routes key commands by slot. Build the address list from
- // CLUSTER SLOTS, but replace advertised internal node IPs with the configured
- // host so Docker/NAT deployments can still reach the mapped node ports.
addrs := []string{addr}
if slots, err := client.ClusterSlots(ctx).Result(); err == nil && len(slots) > 0 {
+ isCluster = true
seen := map[string]bool{}
addrs = addrs[:0]
for _, slot := range slots {
@@ -280,10 +357,18 @@ func getRedisClient(instance *model.Instance, db int) (*redisClientWrapper, erro
}
}
+ wrapper := &redisClientWrapper{Client: client, isCluster: isCluster}
+ if !isCluster {
+ return wrapper, nil
+ }
+
clusterClient := redis.NewClusterClient(&redis.ClusterOptions{
- Addrs: addrs,
- Username: redisUsername,
- Password: password,
+ Addrs: addrs,
+ Username: redisUsername,
+ Password: password,
+ DialTimeout: 5 * time.Second,
+ ReadTimeout: 5 * time.Second,
+ WriteTimeout: 5 * time.Second,
})
if err := clusterClient.Ping(ctx).Err(); err != nil {
clusterClient.Close()
@@ -710,25 +795,9 @@ func RedisCreateKey(c *gin.Context) {
ctx := context.Background()
- switch input.Type {
- case "string":
- client.Set(ctx, input.Key, input.Value, 0)
- case "list":
- if vals, ok := input.Value.([]interface{}); ok {
- for _, v := range vals {
- client.RPush(ctx, input.Key, fmt.Sprintf("%v", v))
- }
- }
- case "set":
- if vals, ok := input.Value.([]interface{}); ok {
- for _, v := range vals {
- client.SAdd(ctx, input.Key, fmt.Sprintf("%v", v))
- }
- }
- case "hash":
- if vals, ok := input.Value.(map[string]interface{}); ok {
- client.HSet(ctx, input.Key, vals)
- }
+ if err := writeRedisKeyValue(ctx, client, input.Key, input.Type, input.Value); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
+ return
}
if input.TTL > 0 {
@@ -738,6 +807,55 @@ func RedisCreateKey(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Key created"})
}
+// PUT /api/v1/redis/:id/keys/:key
+func RedisUpdateKey(c *gin.Context) {
+ id := c.Param("id")
+ key := c.Param("key")
+ dbParam := c.Query("db")
+ db := 0
+ if dbParam != "" {
+ db, _ = strconv.Atoi(dbParam)
+ }
+
+ var input struct {
+ Type string `json:"type" binding:"required"`
+ Value interface{} `json:"value"`
+ TTL int `json:"ttl"`
+ }
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
+ return
+ }
+
+ var instance model.Instance
+ if err := model.DB.First(&instance, id).Error; err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"})
+ return
+ }
+
+ client, err := getRedisClient(&instance, db)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
+ return
+ }
+ defer client.Close()
+
+ ctx := context.Background()
+ if err := client.Del(ctx, key).Err(); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
+ return
+ }
+ if err := writeRedisKeyValue(ctx, client, key, input.Type, input.Value); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
+ return
+ }
+ if input.TTL > 0 {
+ client.Expire(ctx, key, time.Duration(input.TTL)*time.Second)
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Key updated"})
+}
+
// DELETE /api/v1/redis/:id/keys/:key
func RedisDeleteKey(c *gin.Context) {
id := c.Param("id")
diff --git a/backend/internal/model/model.go b/backend/internal/model/model.go
index 6d42621..6ae0e01 100644
--- a/backend/internal/model/model.go
+++ b/backend/internal/model/model.go
@@ -19,7 +19,7 @@ type User struct {
AuthSource string `gorm:"size:20;default:'local'" json:"auth_source"` // local / ldap
Role string `gorm:"size:20;not null" json:"role"` // admin, dba, developer, auditor
IsActive *bool `gorm:"default:true" json:"is_active"`
- Preferences *string `gorm:"type:text" json:"preferences"` // JSON: {"theme":"dark","login_style":"split"}
+ Preferences *string `gorm:"type:text" json:"preferences"` // JSON: {"theme":"system","login_style":"split"}
LastLogin *time.Time `json:"last_login"`
FailedLoginAttempts int `gorm:"default:0" json:"failed_login_attempts"`
LockedUntil *time.Time `json:"locked_until"`
diff --git a/backend/router/router.go b/backend/router/router.go
index 5df20e8..28fbe87 100644
--- a/backend/router/router.go
+++ b/backend/router/router.go
@@ -47,11 +47,11 @@ func Setup(r *gin.Engine) {
auth.GET("/instances/:id", handler.GetInstance)
auth.GET("/instances/:id/databases", handler.ListDatabases)
auth.GET("/instances/:id/databases/:db/tables", handler.ListTables)
- auth.POST("/instances/:id/test-connection", handler.TestConnection)
- auth.POST("/instances/test-connection", handler.TestNewConnection)
- auth.GET("/instances/:id/metrics", handler.InstanceMetrics)
+ auth.POST("/instances/:id/test-connection", handler.TestConnection)
+ auth.POST("/instances/test-connection", handler.TestNewConnection)
+ auth.GET("/instances/:id/metrics", handler.InstanceMetrics)
- // Instance Detail (databases/tables/schema/processlist/status/users)
+ // Instance Detail (databases/tables/schema/processlist/status/users)
auth.GET("/instances/:id/detail/databases", handler.DetailListDatabases)
auth.GET("/instances/:id/detail/tables", handler.DetailListTables)
auth.GET("/instances/:id/detail/db-schema", handler.DetailDbSchema)
@@ -97,6 +97,7 @@ func Setup(r *gin.Engine) {
auth.GET("/redis/:id/clients", handler.RedisClients)
auth.POST("/redis/:id/command", handler.RedisExecuteCommand)
auth.POST("/redis/:id/keys", handler.RedisCreateKey)
+ auth.PUT("/redis/:id/keys/:key", handler.RedisUpdateKey)
auth.DELETE("/redis/:id/keys/:key", handler.RedisDeleteKey)
// Users (admin only)
@@ -162,7 +163,6 @@ func Setup(r *gin.Engine) {
queryGroup.POST("/query/export", handler.ExportQueryCSV)
}
-
// SQL Assistant
auth.POST("/sql-assistant/format", handler.SQLFormat)
auth.POST("/sql-assistant/analyze", handler.SQLAnalyze)
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 278f917..0c5deb9 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -17,7 +17,8 @@ const WorkflowPage = lazy(() => import('@/pages/WorkflowPage'));
const AutoTaskPage = lazy(() => import('@/pages/AutoTaskPage'));
const SettingsPage = lazy(() => import('@/pages/SettingsPage'));
const AuditLogsPage = lazy(() => import('@/pages/AuditLogsPage'));
-const DataDictionaryPage = lazy(() => import('@/pages/DataDictionaryPage'));
+const AnnouncementPage = lazy(() => import('@/pages/AnnouncementPage'));
+const NotificationPage = lazy(() => import('@/pages/NotificationPage'));
const InstanceDetailPage = lazy(() => import('@/pages/InstanceDetailPage'));
const RedisDetailPage = lazy(() => import('@/pages/RedisDetailPage'));
const MongoDBDetailPage = lazy(() => import('@/pages/MongoDBDetailPage'));
@@ -73,7 +74,6 @@ function AppRoutes() {
} />
} />
} />
- } />
} />
} />
} />
@@ -84,6 +84,8 @@ function AppRoutes() {
} />
} />
} />
+ } />
+ } />
} />
} />
diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts
index f3a5e87..f68792e 100644
--- a/frontend/src/api/auth.ts
+++ b/frontend/src/api/auth.ts
@@ -1,34 +1 @@
-import request from './request';
-
-export interface LoginResponse {
- access_token: string;
- token_type: string;
- user: {
- id: number;
- username: string;
- display_name: string;
- role: string;
- email: string | null;
- data_source_permissions?: string;
- custom_permissions?: string;
- group_ids?: number[];
- };
-}
-
-export interface MeResponse {
- id: number;
- username: string;
- display_name: string;
- role: string;
- email: string | null;
- data_source_permissions?: string;
- custom_permissions?: string;
- group_ids?: number[];
-}
-
-export const authApi = {
- login: (username: string, password: string) =>
- request.post('/auth/login', { username, password }),
- getMe: () => request.get('/auth/me'),
- logout: () => request.post('/auth/logout'),
-};
+export * from './auth/auth';
diff --git a/frontend/src/api/auth/auth.ts b/frontend/src/api/auth/auth.ts
new file mode 100644
index 0000000..6326c8f
--- /dev/null
+++ b/frontend/src/api/auth/auth.ts
@@ -0,0 +1,34 @@
+import request from '../request';
+
+export interface LoginResponse {
+ access_token: string;
+ token_type: string;
+ user: {
+ id: number;
+ username: string;
+ display_name: string;
+ role: string;
+ email: string | null;
+ data_source_permissions?: string;
+ custom_permissions?: string;
+ group_ids?: number[];
+ };
+}
+
+export interface MeResponse {
+ id: number;
+ username: string;
+ display_name: string;
+ role: string;
+ email: string | null;
+ data_source_permissions?: string;
+ custom_permissions?: string;
+ group_ids?: number[];
+}
+
+export const authApi = {
+ login: (username: string, password: string) =>
+ request.post('/auth/login', { username, password }),
+ getMe: () => request.get('/auth/me'),
+ logout: () => request.post('/auth/logout'),
+};
diff --git a/frontend/src/api/auth/index.ts b/frontend/src/api/auth/index.ts
new file mode 100644
index 0000000..e632c6f
--- /dev/null
+++ b/frontend/src/api/auth/index.ts
@@ -0,0 +1,2 @@
+export * from './auth';
+
diff --git a/frontend/src/api/dashboard.ts b/frontend/src/api/dashboard.ts
index 7e8fd42..bc27bc4 100644
--- a/frontend/src/api/dashboard.ts
+++ b/frontend/src/api/dashboard.ts
@@ -1,5 +1 @@
-import request from './request';
-
-export const dashboardApi = {
- getStats: () => request.get('/dashboard/stats'),
-};
+export * from './dashboard/dashboard';
diff --git a/frontend/src/api/dashboard/dashboard.ts b/frontend/src/api/dashboard/dashboard.ts
new file mode 100644
index 0000000..49c0f8c
--- /dev/null
+++ b/frontend/src/api/dashboard/dashboard.ts
@@ -0,0 +1,5 @@
+import request from '../request';
+
+export const dashboardApi = {
+ getStats: () => request.get('/dashboard/stats'),
+};
diff --git a/frontend/src/api/dashboard/index.ts b/frontend/src/api/dashboard/index.ts
new file mode 100644
index 0000000..b56c17a
--- /dev/null
+++ b/frontend/src/api/dashboard/index.ts
@@ -0,0 +1,2 @@
+export * from './dashboard';
+
diff --git a/frontend/src/api/datasource/db-types.ts b/frontend/src/api/datasource/db-types.ts
new file mode 100644
index 0000000..e2d22d6
--- /dev/null
+++ b/frontend/src/api/datasource/db-types.ts
@@ -0,0 +1,51 @@
+import request from '../request';
+
+export interface DBTypeOverview {
+ type: string;
+ name: string;
+ icon: string;
+ description: string;
+ category: string;
+ version: string;
+ supported_operations: number;
+ security_rules_count: number;
+}
+
+export interface DBTypeQueryType {
+ type: string;
+ description: string;
+ example: string;
+}
+
+export interface DBTypeDetail {
+ type: string;
+ name: string;
+ icon: string;
+ description: string;
+ version: string;
+ category: string;
+ supported_query_types: DBTypeQueryType[];
+ security_rules: string[];
+ tips: string[];
+ common_commands: Record;
+ limitations: string[];
+}
+
+export interface DBTypeCommand {
+ name: string;
+ command: string;
+}
+
+export const dbTypesApi = {
+ list: () =>
+ request.get('/db-types'),
+
+ getDetail: (dbType: string) =>
+ request.get('/db-types/' + dbType),
+
+ getCommands: (dbType: string) =>
+ request.get('/db-types/' + dbType + '/commands'),
+
+ getTips: (dbType: string) =>
+ request.get('/db-types/' + dbType + '/tips'),
+};
diff --git a/frontend/src/api/datasource/dictionary.ts b/frontend/src/api/datasource/dictionary.ts
new file mode 100644
index 0000000..8ff27e2
--- /dev/null
+++ b/frontend/src/api/datasource/dictionary.ts
@@ -0,0 +1,22 @@
+import request from '../request';
+
+export var dictionaryApi = {
+ listDatabases: function (instanceId: number) {
+ return request.get('/dictionary/instances/' + instanceId + '/databases');
+ },
+ listTables: function (instanceId: number, database: string) {
+ return request.get('/dictionary/instances/' + instanceId + '/tables', {
+ params: { database: database },
+ });
+ },
+ getTableDetail: function (instanceId: number, tableName: string, database: string) {
+ return request.get('/dictionary/instances/' + instanceId + '/tables/' + tableName, {
+ params: { database: database },
+ });
+ },
+ exportDictionary: function (instanceId: number, database: string) {
+ return request.get('/dictionary/instances/' + instanceId + '/export', {
+ params: { database: database },
+ });
+ },
+};
diff --git a/frontend/src/api/datasource/index.ts b/frontend/src/api/datasource/index.ts
new file mode 100644
index 0000000..bfa6ee0
--- /dev/null
+++ b/frontend/src/api/datasource/index.ts
@@ -0,0 +1,8 @@
+export * from './db-types';
+export * from './dictionary';
+export * from './instance';
+export * from './instance-detail';
+export * from './instance-group';
+export * from './mongo';
+export * from './redis';
+
diff --git a/frontend/src/api/datasource/instance-detail.ts b/frontend/src/api/datasource/instance-detail.ts
new file mode 100644
index 0000000..fc25fcc
--- /dev/null
+++ b/frontend/src/api/datasource/instance-detail.ts
@@ -0,0 +1,89 @@
+import request from '../request';
+
+/** 实例详情 API — 数据库/表/字段/索引/Redis Key */
+export const instanceDetailApi = {
+ /** 获取 MySQL 数据库列表 */
+ listDatabases: (instanceId: number) =>
+ request.get(`/instances/${instanceId}/detail/databases`),
+
+ /** 获取表列表(含引擎、行数等) */
+ listTables: (instanceId: number, database: string) =>
+ request.get(`/instances/${instanceId}/detail/tables`, {
+ params: { database },
+ }),
+
+ /** 获取整库表+列结构(用于 SQL 自动补全) */
+ getDbSchema: (instanceId: number, database: string) =>
+ request.get>(`/instances/${instanceId}/detail/db-schema`, {
+ params: { database },
+ }),
+
+ /** 获取表详情(DDL + 字段 + 索引) */
+ getTableDetail: (instanceId: number, database: string, table: string) =>
+ request.get(`/instances/${instanceId}/detail/table-detail`, {
+ params: { database, table },
+ }),
+
+ /** SHOW PROCESSLIST */
+ getProcesslist: (instanceId: number) =>
+ request.get(`/instances/${instanceId}/detail/processlist`),
+
+ /** SHOW GLOBAL STATUS */
+ getStatus: (instanceId: number) =>
+ request.get; all_status: any[] }>(
+ `/instances/${instanceId}/detail/status`
+ ),
+
+ /** 终止进程 */
+ killProcess: (instanceId: number, pid: number) =>
+ request.post(`/instances/${instanceId}/detail/processlist/${pid}/kill`),
+
+ /** 获取数据库用户列表 */
+ listUsers: (instanceId: number) =>
+ request.get(`/instances/${instanceId}/detail/users`),
+};
+
+/** 表元信息 */
+export interface TableMeta {
+ name: string;
+ engine: string;
+ rows: number;
+ data_length: number;
+ index_length: number;
+ comment: string;
+ collation: string;
+ create_time: string;
+ update_time: string;
+}
+
+/** 表详情 */
+export interface TableDetail {
+ ddl: string;
+ columns: ColumnInfo[];
+ indexes: IndexInfo[];
+}
+
+export interface ColumnInfo {
+ Field: string;
+ Type: string;
+ Null: string;
+ Key: string;
+ Default: string | null;
+ Extra: string;
+}
+
+export interface IndexInfo {
+ Table: string;
+ Non_unique: number;
+ Key_name: string;
+ Seq_in_index: number;
+ Column_name: string;
+ Collation: string;
+ Cardinality: number;
+ Sub_part: string | null;
+ Packed: string | null;
+ Null: string;
+ Index_type: string;
+ Comment: string;
+ Index_comment: string;
+}
diff --git a/frontend/src/api/datasource/instance-group.ts b/frontend/src/api/datasource/instance-group.ts
new file mode 100644
index 0000000..b153677
--- /dev/null
+++ b/frontend/src/api/datasource/instance-group.ts
@@ -0,0 +1,38 @@
+import request from '../request';
+
+export interface InstanceGroup {
+ id: number;
+ name: string;
+ description?: string;
+ db_type: string;
+ permissions?: string[];
+ allowed_databases?: string[];
+ is_active: boolean;
+ created_at?: string;
+ updated_at?: string;
+ readonly_instance_ids?: number[];
+ write_instance_ids?: number[];
+ readonly_instance_count: number;
+ write_instance_count: number;
+ user_count: number;
+ readonly_instances?: { id: number; name: string; db_type: string; host: string; port: number; env: string; status: string }[];
+ write_instances?: { id: number; name: string; db_type: string; host: string; port: number; env: string; status: string }[];
+ users?: { id: number; username: string; display_name: string; role: string }[];
+}
+
+export const instanceGroupApi = {
+ list: (params?: any) => request.get('/instance-groups', { params }),
+ get: (id: number) => request.get(`/instance-groups/${id}`),
+ create: (data: any) => request.post('/instance-groups', data),
+ update: (id: number, data: any) => request.put(`/instance-groups/${id}`, data),
+ delete: (id: number) => request.delete(`/instance-groups/${id}`),
+ assignInstances: (groupId: number, instanceIds: number[], accessType: string = 'readonly') =>
+ request.post(`/instance-groups/${groupId}/instances`, { instance_ids: instanceIds, access_type: accessType }),
+ removeInstance: (groupId: number, instanceId: number, accessType?: string) =>
+ request.delete(`/instance-groups/${groupId}/instances/${instanceId}`, { params: accessType ? { access_type: accessType } : undefined }),
+ listUsers: (groupId: number) => request.get(`/instance-groups/${groupId}/users`),
+ assignUsers: (groupId: number, userIds: number[], permissions?: string[]) =>
+ request.post(`/instance-groups/${groupId}/users`, { user_ids: userIds, permissions }),
+ updateUserGroups: (userId: number, groupIds: number[]) =>
+ request.put(`/instance-groups/users/${userId}/groups`, { group_ids: groupIds }),
+};
diff --git a/frontend/src/api/datasource/instance.ts b/frontend/src/api/datasource/instance.ts
new file mode 100644
index 0000000..b3a935a
--- /dev/null
+++ b/frontend/src/api/datasource/instance.ts
@@ -0,0 +1,15 @@
+import request from '../request';
+
+export const instanceApi = {
+ list: (params?: any) => request.get('/instances', { params }),
+ accessible: (accessType: 'readonly' | 'write') => request.get('/instances/accessible', { params: { access_type: accessType } }),
+ get: (id: number) => request.get(`/instances/${id}`),
+ create: (data: any) => request.post('/instances', data),
+ update: (id: number, data: any) => request.put(`/instances/${id}`, data),
+ delete: (id: number) => request.delete(`/instances/${id}`),
+ testConnection: (id: number) => request.post(`/instances/${id}/test-connection`),
+ testNewConnection: (data: any) => request.post('/instances/test-connection', data),
+ listDatabases: (id: number) => request.get(`/instances/${id}/databases`),
+ listTables: (id: number, db: string) => request.get(`/instances/${id}/databases/${db}/tables`),
+ metrics: (id: number) => request.get(`/instances/${id}/metrics`),
+};
diff --git a/frontend/src/api/datasource/mongo.ts b/frontend/src/api/datasource/mongo.ts
new file mode 100644
index 0000000..4d0fb21
--- /dev/null
+++ b/frontend/src/api/datasource/mongo.ts
@@ -0,0 +1,24 @@
+import request from '../request';
+
+export var mongoApi = {
+ getDatabases: function (id: number) {
+ return request.get('/mongo/' + id + '/databases');
+ },
+ getCollections: function (id: number, database: string) {
+ return request.get('/mongo/' + id + '/databases/' + encodeURIComponent(database) + '/collections');
+ },
+ getCollectionStats: function (id: number, collection: string, database?: string) {
+ var params: Record = {};
+ if (database) { params.database = database; }
+ return request.get('/mongo/' + id + '/collections/' + encodeURIComponent(collection) + '/stats', { params: params });
+ },
+ query: function (id: number, data: Record) {
+ return request.post('/mongo/' + id + '/query', data);
+ },
+ execute: function (id: number, data: Record) {
+ return request.post('/mongo/' + id + '/execute', data);
+ },
+ testConnection: function (id: number) {
+ return request.post('/mongo/' + id + '/test-connection');
+ },
+};
diff --git a/frontend/src/api/datasource/redis.ts b/frontend/src/api/datasource/redis.ts
new file mode 100644
index 0000000..960c7df
--- /dev/null
+++ b/frontend/src/api/datasource/redis.ts
@@ -0,0 +1,47 @@
+import request from '../request';
+
+export const redisApi = {
+ // Redis 信息概览
+ getInfo: (instanceId: number) =>
+ request.get('/redis/' + instanceId + '/info'),
+
+ // DBSIZE
+ getDbsize: (instanceId: number, db?: number) =>
+ request.get('/redis/' + instanceId + '/dbsize', { params: { db: db ?? 0 } }),
+
+ // 键列表
+ scanKeys: (instanceId: number, params: { db?: number; pattern?: string; cursor?: number; count?: number; type?: string }) =>
+ request.get('/redis/' + instanceId + '/keys', { params }),
+
+ // 键详情
+ getKeyDetail: (instanceId: number, key: string, db?: number) =>
+ request.get('/redis/' + instanceId + '/keys/' + encodeURIComponent(key), { params: { db: db ?? 0 } }),
+
+ // 慢日志
+ getSlowlog: (instanceId: number, count?: number) =>
+ request.get('/redis/' + instanceId + '/slowlog', { params: { count: count ?? 10 } }),
+
+ // 客户端列表
+ getClients: (instanceId: number) =>
+ request.get('/redis/' + instanceId + '/clients'),
+
+ // 执行命令
+ executeCommand: (instanceId: number, data: { db: number; command: string }) =>
+ request.post('/redis/' + instanceId + '/command', data),
+
+ // 获取键列表(兼容旧接口)
+ getKeys: (instanceId: number, params?: { db?: number; pattern?: string; count?: number; type?: string; cursor?: number }) =>
+ request.get('/redis/' + instanceId + '/keys', { params }),
+
+ // 创建键
+ createKey: (instanceId: number, data: { db?: number; key: string; type?: string; value: any; ttl?: number }) =>
+ request.post('/redis/' + instanceId + '/keys', data),
+
+ // 更新键值(整体替换 value,并可设置 TTL)
+ updateKey: (instanceId: number, key: string, data: { db?: number; type: string; value: any; ttl?: number }) =>
+ request.put('/redis/' + instanceId + '/keys/' + encodeURIComponent(key), data, { params: { db: data.db ?? 0 } }),
+
+ // 删除键
+ deleteKey: (instanceId: number, key: string, db?: number) =>
+ request.delete('/redis/' + instanceId + '/keys/' + encodeURIComponent(key), { params: { db: db ?? 0 } }),
+};
diff --git a/frontend/src/api/dbTypes.ts b/frontend/src/api/dbTypes.ts
index f8143ad..8d91908 100644
--- a/frontend/src/api/dbTypes.ts
+++ b/frontend/src/api/dbTypes.ts
@@ -1,51 +1,2 @@
-import request from './request';
+export * from './datasource/db-types';
-export interface DBTypeOverview {
- type: string;
- name: string;
- icon: string;
- description: string;
- category: string;
- version: string;
- supported_operations: number;
- security_rules_count: number;
-}
-
-export interface DBTypeQueryType {
- type: string;
- description: string;
- example: string;
-}
-
-export interface DBTypeDetail {
- type: string;
- name: string;
- icon: string;
- description: string;
- version: string;
- category: string;
- supported_query_types: DBTypeQueryType[];
- security_rules: string[];
- tips: string[];
- common_commands: Record;
- limitations: string[];
-}
-
-export interface DBTypeCommand {
- name: string;
- command: string;
-}
-
-export const dbTypesApi = {
- list: () =>
- request.get('/db-types'),
-
- getDetail: (dbType: string) =>
- request.get('/db-types/' + dbType),
-
- getCommands: (dbType: string) =>
- request.get('/db-types/' + dbType + '/commands'),
-
- getTips: (dbType: string) =>
- request.get('/db-types/' + dbType + '/tips'),
-};
diff --git a/frontend/src/api/dictionary.ts b/frontend/src/api/dictionary.ts
index 4105404..b4fe7c4 100644
--- a/frontend/src/api/dictionary.ts
+++ b/frontend/src/api/dictionary.ts
@@ -1,22 +1,2 @@
-import request from './request';
+export * from './datasource/dictionary';
-export var dictionaryApi = {
- listDatabases: function (instanceId: number) {
- return request.get('/dictionary/instances/' + instanceId + '/databases');
- },
- listTables: function (instanceId: number, database: string) {
- return request.get('/dictionary/instances/' + instanceId + '/tables', {
- params: { database: database },
- });
- },
- getTableDetail: function (instanceId: number, tableName: string, database: string) {
- return request.get('/dictionary/instances/' + instanceId + '/tables/' + tableName, {
- params: { database: database },
- });
- },
- exportDictionary: function (instanceId: number, database: string) {
- return request.get('/dictionary/instances/' + instanceId + '/export', {
- params: { database: database },
- });
- },
-};
diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts
new file mode 100644
index 0000000..b731b4b
--- /dev/null
+++ b/frontend/src/api/index.ts
@@ -0,0 +1,8 @@
+// API unified exports. Prefer importing domain APIs from "@/api" or "@/api/".
+export * from './auth';
+export * from './dashboard';
+export * from './datasource';
+export * from './query';
+export * from './ticket';
+export * from './system';
+
diff --git a/frontend/src/api/instance.ts b/frontend/src/api/instance.ts
index 7aafae6..d1c7499 100644
--- a/frontend/src/api/instance.ts
+++ b/frontend/src/api/instance.ts
@@ -1,15 +1,2 @@
-import request from './request';
+export * from './datasource/instance';
-export const instanceApi = {
- list: (params?: any) => request.get('/instances', { params }),
- accessible: (accessType: 'readonly' | 'write') => request.get('/instances/accessible', { params: { access_type: accessType } }),
- get: (id: number) => request.get(`/instances/${id}`),
- create: (data: any) => request.post('/instances', data),
- update: (id: number, data: any) => request.put(`/instances/${id}`, data),
- delete: (id: number) => request.delete(`/instances/${id}`),
- testConnection: (id: number) => request.post(`/instances/${id}/test-connection`),
- testNewConnection: (data: any) => request.post('/instances/test-connection', data),
- listDatabases: (id: number) => request.get(`/instances/${id}/databases`),
- listTables: (id: number, db: string) => request.get(`/instances/${id}/databases/${db}/tables`),
- metrics: (id: number) => request.get(`/instances/${id}/metrics`),
-};
diff --git a/frontend/src/api/instanceDetail.ts b/frontend/src/api/instanceDetail.ts
index 8f8ddea..ff508f3 100644
--- a/frontend/src/api/instanceDetail.ts
+++ b/frontend/src/api/instanceDetail.ts
@@ -1,89 +1,2 @@
-import request from './request';
+export * from './datasource/instance-detail';
-/** 实例详情 API — 数据库/表/字段/索引/Redis Key */
-export const instanceDetailApi = {
- /** 获取 MySQL 数据库列表 */
- listDatabases: (instanceId: number) =>
- request.get(`/instances/${instanceId}/detail/databases`),
-
- /** 获取表列表(含引擎、行数等) */
- listTables: (instanceId: number, database: string) =>
- request.get(`/instances/${instanceId}/detail/tables`, {
- params: { database },
- }),
-
- /** 获取整库表+列结构(用于 SQL 自动补全) */
- getDbSchema: (instanceId: number, database: string) =>
- request.get>(`/instances/${instanceId}/detail/db-schema`, {
- params: { database },
- }),
-
- /** 获取表详情(DDL + 字段 + 索引) */
- getTableDetail: (instanceId: number, database: string, table: string) =>
- request.get(`/instances/${instanceId}/detail/table-detail`, {
- params: { database, table },
- }),
-
- /** SHOW PROCESSLIST */
- getProcesslist: (instanceId: number) =>
- request.get(`/instances/${instanceId}/detail/processlist`),
-
- /** SHOW GLOBAL STATUS */
- getStatus: (instanceId: number) =>
- request.get; all_status: any[] }>(
- `/instances/${instanceId}/detail/status`
- ),
-
- /** 终止进程 */
- killProcess: (instanceId: number, pid: number) =>
- request.post(`/instances/${instanceId}/detail/processlist/${pid}/kill`),
-
- /** 获取数据库用户列表 */
- listUsers: (instanceId: number) =>
- request.get(`/instances/${instanceId}/detail/users`),
-};
-
-/** 表元信息 */
-export interface TableMeta {
- name: string;
- engine: string;
- rows: number;
- data_length: number;
- index_length: number;
- comment: string;
- collation: string;
- create_time: string;
- update_time: string;
-}
-
-/** 表详情 */
-export interface TableDetail {
- ddl: string;
- columns: ColumnInfo[];
- indexes: IndexInfo[];
-}
-
-export interface ColumnInfo {
- Field: string;
- Type: string;
- Null: string;
- Key: string;
- Default: string | null;
- Extra: string;
-}
-
-export interface IndexInfo {
- Table: string;
- Non_unique: number;
- Key_name: string;
- Seq_in_index: number;
- Column_name: string;
- Collation: string;
- Cardinality: number;
- Sub_part: string | null;
- Packed: string | null;
- Null: string;
- Index_type: string;
- Comment: string;
- Index_comment: string;
-}
diff --git a/frontend/src/api/instanceGroup.ts b/frontend/src/api/instanceGroup.ts
index baccea7..06ba681 100644
--- a/frontend/src/api/instanceGroup.ts
+++ b/frontend/src/api/instanceGroup.ts
@@ -1,38 +1,2 @@
-import request from './request';
+export * from './datasource/instance-group';
-export interface InstanceGroup {
- id: number;
- name: string;
- description?: string;
- db_type: string;
- permissions?: string[];
- allowed_databases?: string[];
- is_active: boolean;
- created_at?: string;
- updated_at?: string;
- readonly_instance_ids?: number[];
- write_instance_ids?: number[];
- readonly_instance_count: number;
- write_instance_count: number;
- user_count: number;
- readonly_instances?: { id: number; name: string; db_type: string; host: string; port: number; env: string; status: string }[];
- write_instances?: { id: number; name: string; db_type: string; host: string; port: number; env: string; status: string }[];
- users?: { id: number; username: string; display_name: string; role: string }[];
-}
-
-export const instanceGroupApi = {
- list: (params?: any) => request.get('/instance-groups', { params }),
- get: (id: number) => request.get(`/instance-groups/${id}`),
- create: (data: any) => request.post('/instance-groups', data),
- update: (id: number, data: any) => request.put(`/instance-groups/${id}`, data),
- delete: (id: number) => request.delete(`/instance-groups/${id}`),
- assignInstances: (groupId: number, instanceIds: number[], accessType: string = 'readonly') =>
- request.post(`/instance-groups/${groupId}/instances`, { instance_ids: instanceIds, access_type: accessType }),
- removeInstance: (groupId: number, instanceId: number, accessType?: string) =>
- request.delete(`/instance-groups/${groupId}/instances/${instanceId}`, { params: accessType ? { access_type: accessType } : undefined }),
- listUsers: (groupId: number) => request.get(`/instance-groups/${groupId}/users`),
- assignUsers: (groupId: number, userIds: number[], permissions?: string[]) =>
- request.post(`/instance-groups/${groupId}/users`, { user_ids: userIds, permissions }),
- updateUserGroups: (userId: number, groupIds: number[]) =>
- request.put(`/instance-groups/users/${userId}/groups`, { group_ids: groupIds }),
-};
diff --git a/frontend/src/api/mongo.ts b/frontend/src/api/mongo.ts
index 7448fe4..018e6ca 100644
--- a/frontend/src/api/mongo.ts
+++ b/frontend/src/api/mongo.ts
@@ -1,24 +1,2 @@
-import request from './request';
+export * from './datasource/mongo';
-export var mongoApi = {
- getDatabases: function (id: number) {
- return request.get('/mongo/' + id + '/databases');
- },
- getCollections: function (id: number, database: string) {
- return request.get('/mongo/' + id + '/databases/' + encodeURIComponent(database) + '/collections');
- },
- getCollectionStats: function (id: number, collection: string, database?: string) {
- var params: Record = {};
- if (database) { params.database = database; }
- return request.get('/mongo/' + id + '/collections/' + encodeURIComponent(collection) + '/stats', { params: params });
- },
- query: function (id: number, data: Record) {
- return request.post('/mongo/' + id + '/query', data);
- },
- execute: function (id: number, data: Record) {
- return request.post('/mongo/' + id + '/execute', data);
- },
- testConnection: function (id: number) {
- return request.post('/mongo/' + id + '/test-connection');
- },
-};
diff --git a/frontend/src/api/notification.ts b/frontend/src/api/notification.ts
index 36918f5..24d9928 100644
--- a/frontend/src/api/notification.ts
+++ b/frontend/src/api/notification.ts
@@ -1,9 +1,2 @@
-import request from './request';
+export * from './system/notification';
-export const notificationApi = {
- list: () => request.get('/notifications'),
- create: (data: any) => request.post('/notifications', data),
- update: (id: number, data: any) => request.put(`/notifications/${id}`, data),
- delete: (id: number) => request.delete(`/notifications/${id}`),
- test: (id: number) => request.post(`/notifications/${id}/test`),
-};
diff --git a/frontend/src/api/query.ts b/frontend/src/api/query.ts
index 16d772f..9f9abc0 100644
--- a/frontend/src/api/query.ts
+++ b/frontend/src/api/query.ts
@@ -1,15 +1 @@
-import request from './request';
-
-export const queryApi = {
- execute: (data: { instance_id: number; database?: string; sql: string; limit?: number; offset?: number }) =>
- request.post('/query', data),
-
- formatSQL: (sql: string) =>
- request.post('/sql-assistant/format', { sql }),
-
- analyzeSQL: (sql: string, db_type: string) =>
- request.post('/sql-assistant/analyze', { sql, db_type }),
-
- getDBTypeDetail: (dbType: string) =>
- request.get('/db-types/' + dbType),
-};
+export * from './query/query';
diff --git a/frontend/src/api/query/index.ts b/frontend/src/api/query/index.ts
new file mode 100644
index 0000000..1082fc1
--- /dev/null
+++ b/frontend/src/api/query/index.ts
@@ -0,0 +1,2 @@
+export * from './query';
+
diff --git a/frontend/src/api/query/query.ts b/frontend/src/api/query/query.ts
new file mode 100644
index 0000000..afe9738
--- /dev/null
+++ b/frontend/src/api/query/query.ts
@@ -0,0 +1,15 @@
+import request from '../request';
+
+export const queryApi = {
+ execute: (data: { instance_id: number; database?: string; sql: string; limit?: number; offset?: number }) =>
+ request.post('/query', data),
+
+ formatSQL: (sql: string) =>
+ request.post('/sql-assistant/format', { sql }),
+
+ analyzeSQL: (sql: string, db_type: string) =>
+ request.post('/sql-assistant/analyze', { sql, db_type }),
+
+ getDBTypeDetail: (dbType: string) =>
+ request.get('/db-types/' + dbType),
+};
diff --git a/frontend/src/api/redis.ts b/frontend/src/api/redis.ts
index 71f8eb2..1bd99d3 100644
--- a/frontend/src/api/redis.ts
+++ b/frontend/src/api/redis.ts
@@ -1,43 +1,2 @@
-import request from './request';
+export * from './datasource/redis';
-export const redisApi = {
- // Redis 信息概览
- getInfo: (instanceId: number) =>
- request.get('/redis/' + instanceId + '/info'),
-
- // DBSIZE
- getDbsize: (instanceId: number, db?: number) =>
- request.get('/redis/' + instanceId + '/dbsize', { params: { db: db ?? 0 } }),
-
- // 键列表
- scanKeys: (instanceId: number, params: { db?: number; pattern?: string; cursor?: number; count?: number; type?: string }) =>
- request.get('/redis/' + instanceId + '/keys', { params }),
-
- // 键详情
- getKeyDetail: (instanceId: number, key: string, db?: number) =>
- request.get('/redis/' + instanceId + '/keys/' + encodeURIComponent(key), { params: { db: db ?? 0 } }),
-
- // 慢日志
- getSlowlog: (instanceId: number, count?: number) =>
- request.get('/redis/' + instanceId + '/slowlog', { params: { count: count ?? 10 } }),
-
- // 客户端列表
- getClients: (instanceId: number) =>
- request.get('/redis/' + instanceId + '/clients'),
-
- // 执行命令
- executeCommand: (instanceId: number, data: { db: number; command: string }) =>
- request.post('/redis/' + instanceId + '/command', data),
-
- // 获取键列表(兼容旧接口)
- getKeys: (instanceId: number, params?: { db?: number; pattern?: string; count?: number; type?: string; cursor?: number }) =>
- request.get('/redis/' + instanceId + '/keys', { params }),
-
- // 创建键
- createKey: (instanceId: number, data: { db?: number; key: string; type?: string; value: any; ttl?: number }) =>
- request.post('/redis/' + instanceId + '/keys', data),
-
- // 删除键
- deleteKey: (instanceId: number, key: string, db?: number) =>
- request.delete('/redis/' + instanceId + '/keys/' + encodeURIComponent(key), { params: { db: db ?? 0 } }),
-};
diff --git a/frontend/src/api/system/index.ts b/frontend/src/api/system/index.ts
new file mode 100644
index 0000000..e0b4306
--- /dev/null
+++ b/frontend/src/api/system/index.ts
@@ -0,0 +1,4 @@
+export * from './notification';
+export * from './user';
+export * from './workflow';
+
diff --git a/frontend/src/api/system/notification.ts b/frontend/src/api/system/notification.ts
new file mode 100644
index 0000000..afcd664
--- /dev/null
+++ b/frontend/src/api/system/notification.ts
@@ -0,0 +1,9 @@
+import request from '../request';
+
+export const notificationApi = {
+ list: () => request.get('/notifications'),
+ create: (data: any) => request.post('/notifications', data),
+ update: (id: number, data: any) => request.put(`/notifications/${id}`, data),
+ delete: (id: number) => request.delete(`/notifications/${id}`),
+ test: (id: number) => request.post(`/notifications/${id}/test`),
+};
diff --git a/frontend/src/api/system/user.ts b/frontend/src/api/system/user.ts
new file mode 100644
index 0000000..5471a34
--- /dev/null
+++ b/frontend/src/api/system/user.ts
@@ -0,0 +1,25 @@
+import request from '../request';
+
+export const userApi = {
+ list: (params?: any) => request.get('/users', { params }),
+ get: (id: number) => request.get(`/users/${id}`),
+ create: (data: any) => request.post('/users', data),
+ update: (id: number, data: any) => request.put(`/users/${id}`, data),
+ delete: (id: number) => request.delete(`/users/${id}`),
+ resetPassword: (id: number, data: { new_password: string }) =>
+ request.post(`/users/${id}/reset-password`, data),
+ batchImport: (data: any) => request.post('/users/batch-import', data),
+ batchImportLdap: (data: any) => request.post('/users/batch-import-ldap', data),
+ downloadTemplate: () => request.get('/users/import-template', { responseType: 'blob' }),
+};
+
+export const ldapApi = {
+ getConfig: () => request.get('/ldap-config'),
+ updateConfig: (data: any) => request.put('/ldap-config', data),
+ testConnection: (data: any) => request.post('/ldap-config/test', data),
+ listOus: () => request.post('/ldap-config/ou-list'),
+ listUsers: (data?: any) => request.post('/ldap-config/user-list', data || {}),
+ createOu: (data: any) => request.post('/ldap-config/create-ou', data),
+ createUser: (data: any) => request.post('/ldap-config/create-user', data),
+ batchImportToLdap: (data: any) => request.post('/ldap-config/batch-import-ldap', data),
+};
diff --git a/frontend/src/api/system/workflow.ts b/frontend/src/api/system/workflow.ts
new file mode 100644
index 0000000..54d925f
--- /dev/null
+++ b/frontend/src/api/system/workflow.ts
@@ -0,0 +1,11 @@
+import request from '../request';
+
+export const workflowApi = {
+ list: () => request.get('/workflows'),
+ get: (id: number) => request.get(`/workflows/${id}`),
+ create: (data: any) => request.post('/workflows', data),
+ update: (id: number, data: any) => request.put(`/workflows/${id}`, data),
+ delete: (id: number) => request.delete(`/workflows/${id}`),
+ setDefault: (id: number) => request.post(`/workflows/${id}/set-default`),
+ clone: (id: number) => request.post(`/workflows/${id}/clone`),
+};
diff --git a/frontend/src/api/ticket.ts b/frontend/src/api/ticket.ts
index 84776cc..0ffc24c 100644
--- a/frontend/src/api/ticket.ts
+++ b/frontend/src/api/ticket.ts
@@ -1,37 +1 @@
-import request from './request';
-
-export var ticketApi = {
- list: function (params?: any) {
- return request.get('/tickets', { params: params });
- },
- get: function (id: number) {
- return request.get('/tickets/' + id);
- },
- create: function (data: any) {
- return request.post('/tickets', data);
- },
- review: function (id: number, data: { action: 'approve' | 'reject'; comment: string }) {
- return request.post('/tickets/' + id + '/review', data);
- },
- execute: function (id: number) {
- return request.post('/tickets/' + id + '/execute');
- },
- statistics: function () {
- return request.get('/tickets/statistics');
- },
- timeline: function (id: number) {
- return request.get('/tickets/' + id + '/timeline');
- },
- approvals: function (id: number) {
- return request.get('/tickets/' + id + '/approvals');
- },
- cancel: function (id: number) {
- return request.post('/tickets/' + id + '/cancel');
- },
- rollback: function (id: number) {
- return request.post('/tickets/' + id + '/rollback');
- },
- audit: function (data: { sql_content: string; instance_id: number; database: string }) {
- return request.post('/tickets/audit', data);
- },
-};
+export * from './ticket/ticket';
diff --git a/frontend/src/api/ticket/index.ts b/frontend/src/api/ticket/index.ts
new file mode 100644
index 0000000..dcfb11d
--- /dev/null
+++ b/frontend/src/api/ticket/index.ts
@@ -0,0 +1,2 @@
+export * from './ticket';
+
diff --git a/frontend/src/api/ticket/ticket.ts b/frontend/src/api/ticket/ticket.ts
new file mode 100644
index 0000000..16d4152
--- /dev/null
+++ b/frontend/src/api/ticket/ticket.ts
@@ -0,0 +1,37 @@
+import request from '../request';
+
+export var ticketApi = {
+ list: function (params?: any) {
+ return request.get('/tickets', { params: params });
+ },
+ get: function (id: number) {
+ return request.get('/tickets/' + id);
+ },
+ create: function (data: any) {
+ return request.post('/tickets', data);
+ },
+ review: function (id: number, data: { action: 'approve' | 'reject'; comment: string }) {
+ return request.post('/tickets/' + id + '/review', data);
+ },
+ execute: function (id: number) {
+ return request.post('/tickets/' + id + '/execute');
+ },
+ statistics: function () {
+ return request.get('/tickets/statistics');
+ },
+ timeline: function (id: number) {
+ return request.get('/tickets/' + id + '/timeline');
+ },
+ approvals: function (id: number) {
+ return request.get('/tickets/' + id + '/approvals');
+ },
+ cancel: function (id: number) {
+ return request.post('/tickets/' + id + '/cancel');
+ },
+ rollback: function (id: number) {
+ return request.post('/tickets/' + id + '/rollback');
+ },
+ audit: function (data: { sql_content: string; instance_id: number; database: string }) {
+ return request.post('/tickets/audit', data);
+ },
+};
diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts
index ddbe4fe..af15242 100644
--- a/frontend/src/api/user.ts
+++ b/frontend/src/api/user.ts
@@ -1,25 +1,2 @@
-import request from './request';
+export * from './system/user';
-export const userApi = {
- list: (params?: any) => request.get('/users', { params }),
- get: (id: number) => request.get(`/users/${id}`),
- create: (data: any) => request.post('/users', data),
- update: (id: number, data: any) => request.put(`/users/${id}`, data),
- delete: (id: number) => request.delete(`/users/${id}`),
- resetPassword: (id: number, data: { new_password: string }) =>
- request.post(`/users/${id}/reset-password`, data),
- batchImport: (data: any) => request.post('/users/batch-import', data),
- batchImportLdap: (data: any) => request.post('/users/batch-import-ldap', data),
- downloadTemplate: () => request.get('/users/import-template', { responseType: 'blob' }),
-};
-
-export const ldapApi = {
- getConfig: () => request.get('/ldap-config'),
- updateConfig: (data: any) => request.put('/ldap-config', data),
- testConnection: (data: any) => request.post('/ldap-config/test', data),
- listOus: () => request.post('/ldap-config/ou-list'),
- listUsers: (data?: any) => request.post('/ldap-config/user-list', data || {}),
- createOu: (data: any) => request.post('/ldap-config/create-ou', data),
- createUser: (data: any) => request.post('/ldap-config/create-user', data),
- batchImportToLdap: (data: any) => request.post('/ldap-config/batch-import-ldap', data),
-};
diff --git a/frontend/src/api/workflow.ts b/frontend/src/api/workflow.ts
index 1b5b830..1a37897 100644
--- a/frontend/src/api/workflow.ts
+++ b/frontend/src/api/workflow.ts
@@ -1,11 +1,2 @@
-import request from './request';
+export * from './system/workflow';
-export const workflowApi = {
- list: () => request.get('/workflows'),
- get: (id: number) => request.get(`/workflows/${id}`),
- create: (data: any) => request.post('/workflows', data),
- update: (id: number, data: any) => request.put(`/workflows/${id}`, data),
- delete: (id: number) => request.delete(`/workflows/${id}`),
- setDefault: (id: number) => request.post(`/workflows/${id}/set-default`),
- clone: (id: number) => request.post(`/workflows/${id}/clone`),
-};
diff --git a/frontend/src/components/DataDictionaryPanel.tsx b/frontend/src/components/DataDictionaryPanel.tsx
new file mode 100644
index 0000000..7faa518
--- /dev/null
+++ b/frontend/src/components/DataDictionaryPanel.tsx
@@ -0,0 +1,474 @@
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import {
+ AlignLeft,
+ Code2,
+ Columns3,
+ Database,
+ FileJson,
+ Gauge,
+ Hash,
+ Info,
+ Key,
+ RefreshCw,
+ Search,
+ Table2,
+} from 'lucide-react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { instanceDetailApi, type IndexInfo, type TableMeta } from '@/api/datasource/instance-detail';
+
+interface ColumnInfo {
+ Field: string;
+ Type: string;
+ Null: string;
+ Key: string;
+ Default: string | null;
+ Extra: string;
+ Comment?: string;
+}
+
+interface NormalizedTableDetail {
+ columns: ColumnInfo[];
+ indexes: IndexInfo[];
+ ddl: string;
+}
+
+type DetailTab = 'columns' | 'indexes' | 'ddl';
+
+function getTypeIcon(type: string): { icon: React.ElementType; color: string } {
+ const lower = type.toLowerCase();
+ if (lower.includes('int') || lower.includes('float') || lower.includes('decimal') || lower.includes('double')) {
+ return { icon: Hash, color: 'text-blue-400' };
+ }
+ if (lower.includes('char') || lower.includes('text') || lower.includes('blob')) {
+ return { icon: AlignLeft, color: 'text-green-400' };
+ }
+ if (lower.includes('date') || lower.includes('time') || lower.includes('timestamp')) {
+ return { icon: Info, color: 'text-cyan-400' };
+ }
+ if (lower.includes('json')) {
+ return { icon: FileJson, color: 'text-yellow-400' };
+ }
+ return { icon: Hash, color: 'text-[var(--dg-text-muted)]' };
+}
+
+function formatBytes(value?: number): string {
+ if (!value || value <= 0) return '-';
+ if (value >= 1024 * 1024 * 1024) return `${(value / 1024 / 1024 / 1024).toFixed(2)} GB`;
+ if (value >= 1024 * 1024) return `${(value / 1024 / 1024).toFixed(2)} MB`;
+ if (value >= 1024) return `${(value / 1024).toFixed(2)} KB`;
+ return `${value} B`;
+}
+
+function highlightDDL(ddl: string): string {
+ const keywords = [
+ 'CREATE', 'TABLE', 'IF', 'NOT', 'EXISTS', 'PRIMARY', 'KEY', 'UNIQUE', 'INDEX',
+ 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'DEFAULT', 'NULL', 'AUTO_INCREMENT',
+ 'CHARSET', 'COLLATE', 'ENGINE', 'COMMENT', 'INT', 'VARCHAR', 'TEXT', 'DATETIME',
+ 'TIMESTAMP', 'BOOLEAN', 'DECIMAL', 'BIGINT', 'UNSIGNED', 'SIGNED', 'ON', 'DELETE',
+ 'UPDATE', 'CASCADE', 'SET', 'DROP', 'ALTER', 'ADD', 'COLUMN', 'MODIFY', 'CHANGE',
+ ];
+ let highlighted = ddl
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+
+ keywords.forEach(kw => {
+ highlighted = highlighted.replace(new RegExp(`\\b${kw}\\b`, 'gi'), `${kw}`);
+ });
+ highlighted = highlighted.replace(/'[^']*'/g, '$&');
+ highlighted = highlighted.replace(/`[^`]*`/g, '$&');
+ highlighted = highlighted.replace(/\b\d+\b/g, '$&');
+ return highlighted;
+}
+
+function normalizeTableName(table: TableMeta | string): string {
+ return typeof table === 'string' ? table : table.name || '';
+}
+
+function groupIndexes(indexes: IndexInfo[]) {
+ const grouped = new Map();
+ indexes.forEach(index => {
+ const key = index.Key_name || 'UNKNOWN';
+ grouped.set(key, [...(grouped.get(key) || []), index]);
+ });
+ return Array.from(grouped.entries()).map(([name, rows]) => ({
+ name,
+ columns: rows
+ .sort((a, b) => (a.Seq_in_index || 0) - (b.Seq_in_index || 0))
+ .map(row => row.Column_name)
+ .filter(Boolean),
+ nonUnique: rows[0]?.Non_unique,
+ type: rows[0]?.Index_type || '-',
+ comment: rows[0]?.Index_comment || rows[0]?.Comment || '-',
+ }));
+}
+
+export default function DataDictionaryPanel({ instanceId }: { instanceId: number }) {
+ const [databases, setDatabases] = useState([]);
+ const [currentDb, setCurrentDb] = useState('');
+ const [tables, setTables] = useState([]);
+ const [selectedTable, setSelectedTable] = useState('');
+ const [tableDetail, setTableDetail] = useState(null);
+ const [searchFilter, setSearchFilter] = useState('');
+ const [activeTab, setActiveTab] = useState('columns');
+ const [loadingDatabases, setLoadingDatabases] = useState(false);
+ const [loadingTables, setLoadingTables] = useState(false);
+ const [loadingDetail, setLoadingDetail] = useState(false);
+
+ const loadDatabases = useCallback(() => {
+ setLoadingDatabases(true);
+ instanceDetailApi.listDatabases(instanceId)
+ .then((res: any) => {
+ const list = Array.isArray(res) ? res : (res?.databases || []);
+ setDatabases(list);
+ setCurrentDb(prev => prev && list.includes(prev) ? prev : (list[0] || ''));
+ })
+ .catch(() => {
+ setDatabases([]);
+ setCurrentDb('');
+ })
+ .finally(() => setLoadingDatabases(false));
+ }, [instanceId]);
+
+ const loadTables = useCallback(() => {
+ if (!currentDb) {
+ setTables([]);
+ setSelectedTable('');
+ return;
+ }
+ setLoadingTables(true);
+ instanceDetailApi.listTables(instanceId, currentDb)
+ .then((res: any) => {
+ const list = Array.isArray(res) ? res : (res?.tables || []);
+ setTables(list);
+ const tableNames = list.map(normalizeTableName).filter(Boolean);
+ setSelectedTable(prev => prev && tableNames.includes(prev) ? prev : (tableNames[0] || ''));
+ })
+ .catch(() => {
+ setTables([]);
+ setSelectedTable('');
+ })
+ .finally(() => setLoadingTables(false));
+ }, [currentDb, instanceId]);
+
+ const loadTableDetail = useCallback(() => {
+ if (!currentDb || !selectedTable) {
+ setTableDetail(null);
+ return;
+ }
+ setLoadingDetail(true);
+ instanceDetailApi.getTableDetail(instanceId, currentDb, selectedTable)
+ .then((res: any) => {
+ setTableDetail({
+ columns: res?.columns || [],
+ indexes: res?.indexes || [],
+ ddl: res?.ddl || res?.create_ddl || '',
+ });
+ })
+ .catch(() => setTableDetail(null))
+ .finally(() => setLoadingDetail(false));
+ }, [currentDb, instanceId, selectedTable]);
+
+ useEffect(() => {
+ loadDatabases();
+ }, [loadDatabases]);
+
+ useEffect(() => {
+ loadTables();
+ }, [loadTables]);
+
+ useEffect(() => {
+ loadTableDetail();
+ }, [loadTableDetail]);
+
+ const filteredTables = useMemo(() => {
+ const keyword = searchFilter.trim().toLowerCase();
+ if (!keyword) return tables;
+ return tables.filter(table => normalizeTableName(table).toLowerCase().includes(keyword));
+ }, [searchFilter, tables]);
+
+ const selectedMeta = useMemo(
+ () => tables.find(table => normalizeTableName(table) === selectedTable),
+ [selectedTable, tables]
+ );
+
+ const indexes = useMemo(() => groupIndexes(tableDetail?.indexes || []), [tableDetail?.indexes]);
+
+ const tabs: { key: DetailTab; label: string; icon: React.ElementType }[] = [
+ { key: 'columns', label: '字段', icon: Columns3 },
+ { key: 'indexes', label: '索引', icon: Key },
+ { key: 'ddl', label: 'DDL', icon: Code2 },
+ ];
+
+ return (
+
+
+
+
+ {!selectedTable ? (
+
+
+
+ 选择一个表查看字段、索引和 DDL
+
+
+ ) : (
+
+
+
+
+
{selectedTable}
+ {selectedMeta?.comment && (
+
{selectedMeta.comment}
+ )}
+
+
+
+ {Number(selectedMeta?.rows || 0).toLocaleString()} 行
+
+
+
+ {formatBytes((selectedMeta?.data_length || 0) + (selectedMeta?.index_length || 0))}
+
+ {selectedMeta?.engine && (
+
+ {selectedMeta.engine}
+
+ )}
+
+
+
+
+
+
+ {tabs.map(tab => {
+ const Icon = tab.icon;
+ const isActive = activeTab === tab.key;
+ return (
+
+ );
+ })}
+
+
+
+ {loadingDetail ? (
+
+
+ 加载结构...
+
+ ) : activeTab === 'columns' ? (
+
+
+
+
+ | 字段 |
+ 类型 |
+ 可空 |
+ 键 |
+ 默认值 |
+ 额外 |
+ 注释 |
+
+
+
+ {(tableDetail?.columns || []).map(col => {
+ const { icon: TypeIcon, color } = getTypeIcon(col.Type || '');
+ return (
+
+ |
+
+
+ {col.Field}
+
+ |
+ {col.Type} |
+
+
+ {col.Null === 'YES' ? 'YES' : 'NO'}
+
+ |
+ {col.Key || '-'} |
+ {col.Default ?? 'NULL'} |
+ {col.Extra || '-'} |
+ {col.Comment || '-'} |
+
+ );
+ })}
+ {(!tableDetail?.columns || tableDetail.columns.length === 0) && (
+
+ | 暂无字段信息 |
+
+ )}
+
+
+
+ ) : activeTab === 'indexes' ? (
+
+
+
+
+ | 索引名 |
+ 字段 |
+ 唯一 |
+ 类型 |
+ 备注 |
+
+
+
+ {indexes.map(index => (
+
+ | {index.name} |
+ {index.columns.join(', ') || '-'} |
+
+
+ {index.nonUnique === 0 ? '唯一' : '普通'}
+
+ |
+ {index.type} |
+ {index.comment} |
+
+ ))}
+ {indexes.length === 0 && (
+
+ | 暂无索引信息 |
+
+ )}
+
+
+
+ ) : (
+
+
+
+
+ CREATE TABLE 语句
+
+
+
+
+ {tableDetail?.ddl ? (
+
+ ) : (
+
-- 暂无 DDL 信息
+ )}
+
+
+ )}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/components/EditInstanceDialog.tsx b/frontend/src/components/EditInstanceDialog.tsx
index 420df95..0d247d5 100644
--- a/frontend/src/components/EditInstanceDialog.tsx
+++ b/frontend/src/components/EditInstanceDialog.tsx
@@ -38,6 +38,10 @@ interface Props {
mode?: 'edit' | 'clone';
}
+function toBool(value: unknown): boolean {
+ return value === true || value === 'true' || value === 1 || value === '1';
+}
+
/* ------------------------------------------------------------------ */
/* Reusable Edit Instance Dialog */
/* ------------------------------------------------------------------ */
@@ -50,7 +54,7 @@ export default function EditInstanceDialog({ instance, open, onClose, onSaved, m
const [database, setDatabase] = useState(instance.database || '');
const [env, setEnv] = useState(instance.env || 'development');
const [accessMode, setAccessMode] = useState(instance.access_mode || 'readwrite');
- const [clusterMode, setClusterMode] = useState(instance.cluster_mode === true);
+ const [clusterMode, setClusterMode] = useState(toBool(instance.cluster_mode));
const [workflowId, setWorkflowId] = useState(instance.workflow_id || null);
const [workflows, setWorkflows] = useState([]);
// excluded_databases: comma-separated input
@@ -74,7 +78,7 @@ export default function EditInstanceDialog({ instance, open, onClose, onSaved, m
setDatabase(instance.database || '');
setEnv(instance.env || 'development');
setAccessMode(instance.access_mode || 'readwrite');
- setClusterMode(instance.cluster_mode === true);
+ setClusterMode(toBool(instance.cluster_mode));
setWorkflowId(instance.workflow_id || null);
setExcludedDbs(
Array.isArray(instance.excluded_databases) ? (instance.excluded_databases as string[]).join(', ') : (instance.excluded_databases || '')
@@ -155,7 +159,11 @@ export default function EditInstanceDialog({ instance, open, onClose, onSaved, m
const res = mode === 'edit' && !password
? await instanceApi.testConnection(instance.id)
: await instanceApi.testNewConnection(payload);
- setTestResult(res as { success: boolean; message: string; version?: string; cluster_mode?: boolean });
+ const result = res as { success: boolean; message: string; version?: string; cluster_mode?: boolean };
+ if (instance.db_type === 'redis' && result.success) {
+ setClusterMode(toBool(result.cluster_mode));
+ }
+ setTestResult(result);
} catch (e: unknown) {
const msg = (e as { response?: { data?: { detail?: string; message?: string } } })?.response?.data?.detail
|| (e as { response?: { data?: { message?: string } } })?.response?.data?.message
diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx
index cc56ee2..773d4ce 100644
--- a/frontend/src/components/Layout.tsx
+++ b/frontend/src/components/Layout.tsx
@@ -47,6 +47,7 @@ export default function Layout() {
const themeOption = useMemo(() => THEME_OPTIONS.find(o => o.id === theme), [theme]);
const themeIcon = useMemo(() => {
if (!themeOption) return ;
+ if (themeOption.isDark === 'system') return ;
if (themeOption.isDark) return ;
return ;
}, [themeOption]);
@@ -94,13 +95,13 @@ export default function Layout() {
+ 'hover:ml-0.5')
}
>
- {/* Active indicator bar - gradient with glow */}
+ {/* Active indicator bar */}
{isActive && (
)}
diff --git a/frontend/src/components/PageHeader.tsx b/frontend/src/components/PageHeader.tsx
index 1038e13..cf6dc02 100644
--- a/frontend/src/components/PageHeader.tsx
+++ b/frontend/src/components/PageHeader.tsx
@@ -1,4 +1,4 @@
-import { useAuth, actionPermissionMap } from '@/hooks/useAuth';
+import { useAuth } from '@/hooks/useAuth';
import type { ReactNode } from 'react';
interface ActionConfig {
@@ -17,11 +17,11 @@ interface PageHeaderProps {
}
export default function PageHeader({ title, description, action, secondaryAction, children }: PageHeaderProps) {
- const { user, hasPermission } = useAuth();
+ const { hasPermission, hasActionPermission } = useAuth();
const canShowAction = !action?.permission
|| hasPermission(action.permission)
- || (actionPermissionMap[action.permission] || []).includes(user.role);
+ || hasActionPermission(action.permission);
return (
@@ -43,7 +43,7 @@ export default function PageHeader({ title, description, action, secondaryAction
{canShowAction && action && (
diff --git a/frontend/src/components/login/CosmosView.tsx b/frontend/src/components/login/CosmosView.tsx
index e9b9b59..969c9b2 100644
--- a/frontend/src/components/login/CosmosView.tsx
+++ b/frontend/src/components/login/CosmosView.tsx
@@ -373,7 +373,7 @@ export default function CosmosView() {
letterSpacing: '0.3px',
}}
>
- 企业级数据库审核与合规管理
+ 企业级数据审核与变更管理平台
@@ -527,7 +527,7 @@ export default function CosmosView() {
letterSpacing: '0.3px',
}}
>
- © 2025 OpsxSQL — 企业级数据库安全合规
+ © 2025 OpsxSQL — 企业级数据审核与变更管理平台
diff --git a/frontend/src/components/login/GradientMeshView.tsx b/frontend/src/components/login/GradientMeshView.tsx
index 087da06..df6a652 100644
--- a/frontend/src/components/login/GradientMeshView.tsx
+++ b/frontend/src/components/login/GradientMeshView.tsx
@@ -117,7 +117,7 @@ export default function GradientMeshView(): React.JSX.Element {
OpsxSQL
- 企业级数据库审核与合规管理
+ 企业级数据审核与变更管理平台
diff --git a/frontend/src/components/login/InkWashView.tsx b/frontend/src/components/login/InkWashView.tsx
index fe1295e..398c65d 100644
--- a/frontend/src/components/login/InkWashView.tsx
+++ b/frontend/src/components/login/InkWashView.tsx
@@ -288,7 +288,7 @@ export default function InkWashView() {
letterSpacing: 0.5,
}}
>
- 企业级数据库审核与合规管理
+ 企业级数据审核与变更管理平台
diff --git a/frontend/src/components/login/MinimalDark.tsx b/frontend/src/components/login/MinimalDark.tsx
index a431b34..2ecc962 100644
--- a/frontend/src/components/login/MinimalDark.tsx
+++ b/frontend/src/components/login/MinimalDark.tsx
@@ -195,7 +195,7 @@ export default function MinimalDark() {
OpsxSQL
- 企业级数据库审核与合规管理
+ 企业级数据审核与变更管理平台
{/* Form */}
diff --git a/frontend/src/components/login/NeonCityView.tsx b/frontend/src/components/login/NeonCityView.tsx
index e5d6a78..3e3240c 100644
--- a/frontend/src/components/login/NeonCityView.tsx
+++ b/frontend/src/components/login/NeonCityView.tsx
@@ -79,7 +79,7 @@ export default function NeonCityView() {
OpsxSQL
- 企业级数据库审核与合规管理
+ 企业级数据审核与变更管理平台
diff --git a/frontend/src/components/login/SidePanel.tsx b/frontend/src/components/login/SidePanel.tsx
index 0994fb9..525d688 100644
--- a/frontend/src/components/login/SidePanel.tsx
+++ b/frontend/src/components/login/SidePanel.tsx
@@ -1,6 +1,6 @@
import { useLogin } from '@/hooks/useLogin';
import { useTheme } from '@/hooks/useTheme';
-import { Shield, Database, Code, GitBranch, Bell, History, UserCog, Sparkles, Sun, Moon } from 'lucide-react';
+import { Shield, Database, Code, GitBranch, Bell, History, UserCog, Sparkles, Sun, Moon, Monitor } from 'lucide-react';
export default function SidePanel() {
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
@@ -11,7 +11,7 @@ export default function SidePanel() {
{ icon: Code, title: '自助查询', desc: '在线SQL编辑器,语法高亮,查询历史' },
{ icon: GitBranch, title: '数据变更审批', desc: '工单提交、多级审核、执行确认' },
{ icon: Bell, title: '审核规则引擎', desc: '语法/性能/安全/命名,9大类规则' },
- { icon: History, title: '完整审计追踪', desc: '操作可追溯,查询历史、慢日志分析' },
+ { icon: History, title: '完整审计追踪', desc: '操作可追溯,查询历史、执行记录' },
{ icon: UserCog, title: '角色权限管理', desc: 'RBAC 权限体系,admin/DBA/开发者/审计' },
];
@@ -26,7 +26,7 @@ export default function SidePanel() {
onClick={toggleTheme}
className="absolute top-5 right-5 z-20 w-10 h-10 rounded-xl bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] flex items-center justify-center text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] transition-colors"
>
- {(theme as string) === 'dark' ? : }
+ {theme === 'system' ? : theme === 'ocean-deep' || theme === 'pine-green' ? : }
{/* 左侧 */}
diff --git a/frontend/src/constants/menu.ts b/frontend/src/constants/menu.ts
index 588bd34..56e9053 100644
--- a/frontend/src/constants/menu.ts
+++ b/frontend/src/constants/menu.ts
@@ -1,24 +1,39 @@
export const menuSections = [
{ id: 'workspace', label: '工作台' },
- { id: 'admin', label: '管理' },
- { id: 'system', label: '系统' },
+ { id: 'change', label: '数据变更' },
+ { id: 'query', label: '数据查询' },
+ { id: 'asset', label: '数据资产' },
+ { id: 'governance', label: '审批治理' },
+ { id: 'access', label: '权限与组织' },
+ { id: 'system', label: '系统治理' },
];
export const menuItems = [
- // 工作台 — 日常操作
- { id: 'dashboard', label: '仪表盘', icon: 'LayoutDashboard', section: 'workspace', roles: ['admin', 'dba', 'developer', 'auditor'], permission: 'dashboard.view' },
- { id: 'queries', label: '查询数据', icon: 'Code2', section: 'workspace', roles: ['admin', 'dba', 'developer'], permission: 'queries.execute' },
- { id: 'tickets', label: '数据变更', icon: 'FileEdit', section: 'workspace', roles: ['admin', 'dba', 'developer'], permission: 'tickets.manage' },
+ // 工作台
+ { id: 'dashboard', label: '运营总览', icon: 'LayoutDashboard', section: 'workspace', roles: ['admin', 'dba', 'developer', 'auditor'], permission: 'dashboard' },
- // 管理 — 数据源 + 权限
- { id: 'instances', label: '数据源', icon: 'Database', section: 'admin', roles: ['admin', 'dba'], permission: 'instances.manage' },
- { id: 'workflow', label: '审批流程', icon: 'GitBranch', section: 'admin', roles: ['admin', 'dba'], permission: 'workflow.manage' },
- { id: 'users', label: '用户管理', icon: 'Users', section: 'admin', roles: ['admin'], permission: 'users.manage' },
- { id: 'roles', label: '角色权限', icon: 'ShieldCheck', section: 'admin', roles: ['admin'], permission: 'roles.manage' },
- { id: 'instance-groups', label: '权限组', icon: 'FolderLock', section: 'admin', roles: ['admin'], permission: 'instance-groups.manage' },
+ // 数据变更
+ { id: 'tickets', label: '变更工单', icon: 'FileEdit', section: 'change', roles: ['admin', 'dba', 'developer'], permission: 'tickets' },
+ { id: 'auto-task', label: '自动执行', icon: 'Timer', section: 'change', roles: ['admin'], permission: 'auto-task' },
- // 系统
- { id: 'audit-logs', label: '审计日志', icon: 'ScrollText', section: 'system', roles: ['admin', 'dba', 'auditor'], permission: 'audit-logs.view' },
- { id: 'auto-task', label: '自动任务', icon: 'Timer', section: 'system', roles: ['admin'], permission: 'auto-task.manage' },
- { id: 'settings', label: '系统设置', icon: 'Settings', section: 'system', roles: ['admin'], permission: 'settings.manage' },
+ // 数据查询
+ { id: 'queries', label: 'SQL 查询', icon: 'Code2', section: 'query', roles: ['admin', 'dba', 'developer'], permission: 'queries' },
+
+ // 数据资产
+ { id: 'instances', label: '数据源实例', icon: 'Database', section: 'asset', roles: ['admin', 'dba'], permission: 'instances' },
+
+ // 审批治理
+ { id: 'workflow', label: '审批流程', icon: 'GitBranch', section: 'governance', roles: ['admin', 'dba', 'developer', 'auditor'], permission: 'workflow' },
+ { id: 'audit-rules', label: '审核规则', icon: 'ShieldCheck', section: 'governance', roles: ['admin', 'dba', 'auditor'], permission: 'audit-rules' },
+
+ // 权限与组织
+ { id: 'users', label: '用户管理', icon: 'Users', section: 'access', roles: ['admin'], permission: 'users' },
+ { id: 'roles', label: '角色权限', icon: 'ShieldCheck', section: 'access', roles: ['admin'], permission: 'roles' },
+ { id: 'instance-groups', label: '权限组', icon: 'FolderLock', section: 'access', roles: ['admin'], permission: 'instance-groups' },
+
+ // 系统治理
+ { id: 'audit-logs', label: '审计日志', icon: 'ScrollText', section: 'system', roles: ['admin', 'dba', 'auditor'], permission: 'audit-logs' },
+ { id: 'notifications', label: '通知渠道', icon: 'Bell', section: 'system', roles: ['admin'], permission: 'notifications' },
+ { id: 'announcement', label: '公告管理', icon: 'Megaphone', section: 'system', roles: ['admin'], permission: 'announcement' },
+ { id: 'settings', label: '系统设置', icon: 'Settings', section: 'system', roles: ['admin'], permission: 'settings' },
];
diff --git a/frontend/src/hooks/useAuth.tsx b/frontend/src/hooks/useAuth.tsx
index 3165dbc..2992e01 100644
--- a/frontend/src/hooks/useAuth.tsx
+++ b/frontend/src/hooks/useAuth.tsx
@@ -113,17 +113,24 @@ const routePermissionMap: Record = {
dashboard: ['admin', 'dba', 'developer', 'auditor'],
instances: ['admin', 'dba'],
'instance-groups': ['admin'],
- 'data-dictionary': ['admin', 'dba', 'developer'],
tickets: ['admin', 'dba', 'developer'],
queries: ['admin', 'dba', 'developer'],
roles: ['admin'],
users: ['admin'],
+ 'audit-rules': ['admin', 'dba', 'auditor'],
'audit-logs': ['admin', 'dba', 'auditor'],
workflow: ['admin', 'dba', 'developer', 'auditor'],
'auto-task': ['admin'],
+ notifications: ['admin'],
+ announcement: ['admin'],
settings: ['admin'],
};
+const permissionAliasMap: Record = {
+ instances: ['datasource'],
+ settings: ['config-platform'],
+};
+
export const actionPermissionMap: Record = {
'ticket.create': ['admin', 'dba', 'developer'],
'ticket.approve': ['admin', 'dba', 'developer'],
@@ -314,17 +321,26 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
return cache.menu.get(menuId)!;
}
- const allowedRoles = routePermissionMap[menuId];
+ const menuItem = menuItems.find(item => item.id === menuId || item.permission === menuId);
+ const allowedRoles = menuItem?.roles || routePermissionMap[menuId];
if (!allowedRoles) {
cache.menu.set(menuId, false);
return false;
}
- const result = allowedRoles.includes(user.role);
+ const permissionKeys = [
+ menuId,
+ menuItem?.permission,
+ ...((permissionAliasMap[menuId] || [])),
+ ...((menuItem?.permission && permissionAliasMap[menuItem.permission]) || []),
+ ].filter(Boolean) as string[];
+ const customPermission = permissionKeys.find(key => user.customPermissions && key in user.customPermissions);
+ const result = allowedRoles.includes(user.role)
+ && (customPermission ? Boolean(user.customPermissions?.[customPermission]) : true);
cache.menu.set(menuId, result);
cache.timestamp = Date.now();
return result;
- }, [user.role, isLoggedIn, isCacheValid]);
+ }, [user.role, user.customPermissions, isLoggedIn, isCacheValid]);
/** 操作权限检查(带缓存,支持自定义权限覆盖) */
const hasActionPermission = useCallback((action: PermissionAction | string): boolean => {
@@ -446,8 +462,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}, [user, clearPermissionCache, emitPermissionChange]);
const visibleMenuItems = useMemo(() => {
- return menuItems.filter(item => item.roles.includes(user.role));
- }, [user.role]);
+ return menuItems.filter(item => item.roles.includes(user.role) && hasPermission(item.id));
+ }, [user.role, hasPermission]);
/** 检查是否拥有指定角色或以上级别权限 */
const hasRoleLevel = useCallback((minRole: UserRole): boolean => {
diff --git a/frontend/src/hooks/useLogin.tsx b/frontend/src/hooks/useLogin.tsx
index e0806d6..66e42d5 100644
--- a/frontend/src/hooks/useLogin.tsx
+++ b/frontend/src/hooks/useLogin.tsx
@@ -35,7 +35,9 @@ export function useLogin(): LoginState {
const { data: prefs } = await api.get<{ theme?: string; login_style?: string }>('/user/preferences');
if (prefs?.theme) {
const t = prefs.theme;
- if (t === 'dark' || t === 'light') {
+ if (t === 'system') {
+ setTheme('system');
+ } else if (t === 'dark' || t === 'light') {
setTheme(t === 'dark' ? 'ocean-deep' : 'light-professional');
} else if (THEME_OPTIONS.some(o => o.id === t)) {
setTheme(t as Theme);
diff --git a/frontend/src/hooks/useTheme.tsx b/frontend/src/hooks/useTheme.tsx
index 5848369..7ad42ad 100644
--- a/frontend/src/hooks/useTheme.tsx
+++ b/frontend/src/hooks/useTheme.tsx
@@ -1,30 +1,30 @@
import { createContext, useContext, useState, useCallback, useMemo, useEffect } from 'react';
-export type Theme = 'ocean-deep' | 'light-professional' | 'midnight-purple' | 'pine-green' | 'orange-vitality';
+export type Theme = 'system' | 'light-professional' | 'ocean-deep' | 'pine-green';
export interface ThemeOption {
id: Theme;
name: string;
description: string;
- isDark: boolean;
+ isDark: boolean | 'system';
primaryColor: string;
}
export const THEME_OPTIONS: ThemeOption[] = [
- { id: 'ocean-deep', name: '深海蓝', description: '专业稳重的深蓝色调', isDark: true, primaryColor: '#3b82f6' },
- { id: 'light-professional', name: '明亮白', description: '清爽干净的浅色主题', isDark: false, primaryColor: '#2563eb' },
- { id: 'midnight-purple', name: '午夜紫', description: '神秘优雅的紫色调', isDark: true, primaryColor: '#a855f7' },
- { id: 'pine-green', name: '松林绿', description: '自然清新的绿色主题', isDark: true, primaryColor: '#22c55e' },
- { id: 'orange-vitality', name: '活力橙', description: '温暖活力的橙色主题', isDark: true, primaryColor: '#f97316' },
+ { id: 'system', name: '跟随系统', description: '自动匹配系统深色或灰白浅色', isDark: 'system', primaryColor: '#2563eb' },
+ { id: 'light-professional', name: '灰白', description: '克制清爽的生产浅色主题', isDark: false, primaryColor: '#2563eb' },
+ { id: 'ocean-deep', name: '深色', description: '低亮度、高对比的专业暗色主题', isDark: true, primaryColor: '#3b82f6' },
+ { id: 'pine-green', name: '墨绿', description: '偏运维场景的深色备用主题', isDark: true, primaryColor: '#22c55e' },
];
const THEME_ORDER: Theme[] = THEME_OPTIONS.map(o => o.id);
-const DEFAULT_THEME: Theme = 'ocean-deep';
+const DEFAULT_THEME: Theme = 'system';
// Backward compatibility: map old theme names to new ones
function migrateLegacyTheme(value: string | null): Theme | null {
if (value === 'dark') return 'ocean-deep';
if (value === 'light') return 'light-professional';
+ if (value === 'midnight-purple' || value === 'orange-vitality') return 'ocean-deep';
return null;
}
@@ -51,8 +51,15 @@ function getInitialTheme(): Theme {
return DEFAULT_THEME;
}
+function resolveSystemTheme(): Exclude {
+ if (typeof window === 'undefined') return 'ocean-deep';
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'ocean-deep' : 'light-professional';
+}
+
function applyTheme(theme: Theme) {
- document.documentElement.setAttribute('data-theme', theme);
+ const resolvedTheme = theme === 'system' ? resolveSystemTheme() : theme;
+ document.documentElement.setAttribute('data-theme', resolvedTheme);
+ document.documentElement.setAttribute('data-theme-choice', theme);
}
export function ThemeProvider({ children }: { children: React.ReactNode }) {
@@ -60,6 +67,12 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
applyTheme(theme);
+ if (theme !== 'system') return;
+
+ const media = window.matchMedia('(prefers-color-scheme: dark)');
+ const handleChange = () => applyTheme('system');
+ media.addEventListener('change', handleChange);
+ return () => media.removeEventListener('change', handleChange);
}, [theme]);
const toggleTheme = useCallback(() => {
diff --git a/frontend/src/index.css b/frontend/src/index.css
index b28ccb4..76ba3d8 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -46,14 +46,14 @@
--dg-green: #3fb950;
--dg-red: #f85149;
--dg-orange: #f0883e;
- --dg-purple: #8957e5;
+ --dg-purple: #7c8aa5;
--dg-cyan: #39c5cf;
--dg-yellow: #d29922;
--dg-card-hover-border: rgba(88, 166, 255, 0.5);
--dg-gradient-start: #2563eb;
- --dg-gradient-end: #9333ea;
+ --dg-gradient-end: #0ea5e9;
--dg-gradient-text-start: #60a5fa;
- --dg-gradient-text-end: #c084fc;
+ --dg-gradient-text-end: #93c5fd;
--dg-input-focus-ring: rgba(96, 165, 250, 0.2);
}
@@ -99,21 +99,21 @@
--dg-green: #3fb950;
--dg-red: #f85149;
--dg-orange: #f0883e;
- --dg-purple: #8957e5;
+ --dg-purple: #7c8aa5;
--dg-cyan: #39c5cf;
--dg-yellow: #d29922;
--dg-card-hover-border: rgba(88, 166, 255, 0.5);
--dg-gradient-start: #2563eb;
- --dg-gradient-end: #9333ea;
+ --dg-gradient-end: #0ea5e9;
--dg-gradient-text-start: #60a5fa;
- --dg-gradient-text-end: #c084fc;
+ --dg-gradient-text-end: #93c5fd;
--dg-input-focus-ring: rgba(96, 165, 250, 0.2);
}
/* ===== Theme 2: Light Professional (浅色商务) ===== */
[data-theme="light-professional"],
[data-theme="light"] {
- --background: 0 0% 100%;
+ --background: 220 14% 97%;
--foreground: 220 14% 10%;
--card: 0 0% 100%;
--card-foreground: 220 14% 10%;
@@ -133,7 +133,7 @@
--input: 220 13% 91%;
--ring: 217 91% 60%;
--radius: 0.625rem;
- --sidebar-background: 0 0% 98%;
+ --sidebar-background: 0 0% 100%;
--sidebar-foreground: 220 14% 20%;
--sidebar-primary: 217 91% 60%;
--sidebar-primary-foreground: 0 0% 100%;
@@ -141,12 +141,12 @@
--sidebar-accent-foreground: 220 14% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217 91% 60%;
- --dg-bg-primary: #ffffff;
- --dg-bg-secondary: #f8f9fb;
- --dg-bg-tertiary: #f0f1f5;
- --dg-bg-hover: #e8eaef;
- --dg-border: #e2e5eb;
- --dg-border-light: #eaecf0;
+ --dg-bg-primary: #f5f6f8;
+ --dg-bg-secondary: #ffffff;
+ --dg-bg-tertiary: #eef1f5;
+ --dg-bg-hover: #e6e9ef;
+ --dg-border: #dfe3ea;
+ --dg-border-light: #edf0f4;
--dg-text-primary: #1a1d23;
--dg-text-secondary: #5c6370;
--dg-text-muted: #9ca3af;
@@ -154,14 +154,14 @@
--dg-green: #16a34a;
--dg-red: #dc2626;
--dg-orange: #ea580c;
- --dg-purple: #7c3aed;
+ --dg-purple: #64748b;
--dg-cyan: #0891b2;
--dg-yellow: #ca8a04;
--dg-card-hover-border: rgba(37, 99, 235, 0.3);
--dg-gradient-start: #2563eb;
- --dg-gradient-end: #7c3aed;
+ --dg-gradient-end: #0ea5e9;
--dg-gradient-text-start: #1d4ed8;
- --dg-gradient-text-end: #6d28d9;
+ --dg-gradient-text-end: #0369a1;
--dg-input-focus-ring: rgba(37, 99, 235, 0.15);
}
@@ -455,7 +455,7 @@
@apply dg-badge bg-yellow-500/10 text-yellow-400;
}
.dg-badge-purple {
- @apply dg-badge bg-purple-500/10 text-purple-400;
+ @apply dg-badge bg-cyan-500/10 text-cyan-400;
}
.dg-badge-orange {
@apply dg-badge bg-orange-500/10 text-orange-400;
diff --git a/frontend/src/lib/constants.ts b/frontend/src/lib/constants.ts
index 060ba5d..b78e1b2 100644
--- a/frontend/src/lib/constants.ts
+++ b/frontend/src/lib/constants.ts
@@ -1,39 +1,45 @@
import type { PermissionNode } from '@/types';
export const permissionTreeData: PermissionNode[] = [
- { id: 'dashboard', label: '仪表盘(查看统计、图表)' },
- { id: 'datasource', label: '数据源管理', children: [
+ { id: 'workspace', label: '工作台', children: [
+ { id: 'dashboard', label: '运营总览(查看统计、待办、风险趋势)' },
+ ]},
+ { id: 'change', label: '数据变更', children: [
+ { id: 'tickets', label: '变更工单(创建、查看、审批、执行)' },
+ { id: 'ticket_create', label: '创建工单' },
+ { id: 'ticket_approve', label: '审批工单' },
+ { id: 'ticket_execute', label: '执行工单' },
+ { id: 'ticket_rollback', label: '回滚工单' },
+ { id: 'auto-task', label: '自动执行任务' },
+ ]},
+ { id: 'query', label: '数据查询', children: [
+ { id: 'queries', label: 'SQL 查询(查看日志、新建查询、导出)' },
+ { id: 'query_create', label: '执行查询' },
+ { id: 'query_export', label: '导出结果' },
+ { id: 'query_view_all', label: '查看全部用户查询' },
+ ]},
+ { id: 'datasource', label: '数据资产', children: [
+ { id: 'instances', label: '数据源实例(查看、添加、编辑、删除)' },
{ id: 'mysql', label: 'MySQL 实例(查看、添加、编辑、删除)' },
{ id: 'postgres', label: 'PostgreSQL 实例(查看、添加、编辑、删除)' },
{ id: 'redis', label: 'Redis 实例(查看、添加、编辑、删除)' },
{ id: 'mongo', label: 'MongoDB 实例(查看、添加、编辑、删除)' },
]},
- { id: 'tickets', label: '数据变更工单(创建、查看、审批、执行)', children: [
- { id: 'ticket_create', label: '创建工单' },
- { id: 'ticket_approve', label: '审批工单' },
- { id: 'ticket_execute', label: '执行工单' },
- { id: 'ticket_rollback', label: '回滚工单' },
+ { id: 'governance', label: '审批治理', children: [
+ { id: 'workflow', label: '审批流程(创建、编辑流程模板)' },
+ { id: 'audit-rules', label: '审核规则(风险规则、禁用语句、影响行数)' },
]},
- { id: 'queries', label: '查询数据(查看日志、新建查询、导出)', children: [
- { id: 'query_create', label: '执行查询' },
- { id: 'query_export', label: '导出结果' },
- { id: 'query_view_all', label: '查看全部用户查询' },
- ]},
- { id: 'permission', label: '权限管理', children: [
- { id: 'roles', label: '角色权限(创建、编辑、分配权限)' },
+ { id: 'permission', label: '权限与组织', children: [
{ id: 'users', label: '用户管理(创建、编辑、禁用)' },
+ { id: 'roles', label: '角色权限(创建、编辑、分配权限)' },
{ id: 'instance-groups', label: '权限组管理(创建、编辑、分配)' },
]},
- { id: 'system', label: '系统配置', children: [
- { id: 'audit-rules', label: '审核规则(启用/禁用、添加规则)' },
+ { id: 'system', label: '系统治理', children: [
{ id: 'audit-logs', label: '审计日志(查看操作记录)' },
- { id: 'workflow', label: '审批流程(创建、编辑流程模板)' },
- { id: 'auto-task', label: '自动任务' },
- { id: 'announcement', label: '公告管理' },
- ]},
- { id: 'config', label: '配置平台', children: [
- { id: 'config-platform', label: '全局配置(消息推送/LDAP/查询限制/工单规则)' },
{ id: 'notifications', label: '通知渠道(配置 Webhook、测试)' },
+ { id: 'announcement', label: '公告管理' },
+ { id: 'settings', label: '系统设置(外观、LDAP、平台参数)' },
+ { id: 'config-platform', label: '全局配置(消息推送/LDAP/查询限制/工单规则)' },
]},
];
@@ -53,7 +59,7 @@ export const statusColorMap: Record = {
reviewing: 'bg-blue-500/15 text-blue-400',
approved: 'bg-green-500/15 text-green-400',
rejected: 'bg-red-500/15 text-red-400',
- executing: 'bg-purple-500/15 text-purple-400',
+ executing: 'bg-cyan-500/15 text-cyan-400',
success: 'bg-green-500/15 text-green-400',
failed: 'bg-red-500/15 text-red-400',
rollback: 'bg-orange-500/15 text-orange-400',
diff --git a/frontend/src/pages/AppearanceSettings.tsx b/frontend/src/pages/AppearanceSettings.tsx
index 948ea68..92ba75c 100644
--- a/frontend/src/pages/AppearanceSettings.tsx
+++ b/frontend/src/pages/AppearanceSettings.tsx
@@ -32,7 +32,9 @@ export default function AppearanceSettings() {
const prefs = res.data;
if (prefs?.theme) {
const t = prefs.theme;
- if (t === 'dark' || t === 'light') {
+ if (t === 'system') {
+ setTheme('system');
+ } else if (t === 'dark' || t === 'light') {
setTheme(t === 'dark' ? 'ocean-deep' : 'light-professional');
} else if (THEME_OPTIONS.some(o => o.id === t)) {
setTheme(t as Theme);
@@ -87,8 +89,8 @@ export default function AppearanceSettings() {
{/* ===== Theme Mode Section ===== */}
界面配色
-
选择界面配色方案,即时预览效果
-
+
推荐使用跟随系统;浅色为灰白基调,暗色降低眩光,适合长期值守
+
{THEME_OPTIONS.map(opt => (