From ac498c7ba2ffa6cbbbc76563c3a72c9a29b04996 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Mon, 26 Oct 2026 17:54:15 +0800 Subject: [PATCH] refactor(frontend): split 7 oversized pages into feature co-location Per guide ch.7 page split pattern (skeleton orchestrates, private components live in features//): - InstancePage 1716->328, UsersPage 1309->240, TicketsPage 1076->195, TicketDetailPage 911->287, QueriesPage 883->479, LdapConfigPage 860->393, WorkflowPage 852->194 - new feature modules: datasource/ rbac/ ticket/ query/ system/ldap/ governance/workflow/ (components + hooks + types + utils) - stable keys replace index keys in dynamic lists; polling intervals and toast call sites preserved 1:1 (verified by agents) - filter/pagination state moved to URL query via new useQueryState hook (Instance/Users/Tickets/AuditLogs pages): refresh/share/back-button restore state; composite updates applied atomically - permission pure functions extracted to lib/permissions.ts (used by useAuth + useLogin); page-size audit: 0 pages over 700-line budget --- frontend/.prettierignore | 4 + frontend/.prettierrc | 7 + frontend/commitlint.config.js | 13 + frontend/eslint.config.js | 6 +- frontend/package.json | 14 + .../components/CreateInstanceForm.tsx | 552 ++++++ .../datasource/components/DbTypeIcons.tsx | 36 + .../components/InstanceBatchBar.tsx | 48 + .../datasource/components/InstanceDetail.tsx | 270 +++ .../components/InstanceFilterBar.tsx | 71 + .../datasource/components/InstanceGrid.tsx | 160 ++ .../components/InstanceStatsOverview.tsx | 66 + .../datasource/components/InstanceTable.tsx | 136 ++ .../components/QuickImportDialog.tsx | 160 ++ .../datasource/hooks/useInstanceList.ts | 30 + .../datasource/hooks/useInstanceSelection.ts | 31 + frontend/src/features/datasource/types.ts | 40 + frontend/src/features/datasource/utils.ts | 120 ++ .../workflow/components/UserDropdown.tsx | 131 ++ .../workflow/components/WorkflowCard.tsx | 161 ++ .../components/WorkflowEditorModal.tsx | 292 +++ .../components/WorkflowPreviewModal.tsx | 75 + .../workflow/components/WorkflowStatsGrid.tsx | 38 + .../features/governance/workflow/constants.ts | 42 + .../src/features/governance/workflow/types.ts | 44 + .../query/components/QueryResultPanel.tsx | 268 +++ .../features/query/components/QueryTopBar.tsx | 126 ++ .../query/components/RedisKeyValuePanel.tsx | 170 ++ .../query/components/SqlEditorSection.tsx | 52 + frontend/src/features/query/constants.ts | 43 + frontend/src/features/query/types.ts | 49 + .../rbac/components/BatchActionBar.tsx | 54 + .../rbac/components/BatchImportModal.tsx | 407 ++++ .../rbac/components/CreateUserModal.tsx | 109 ++ .../rbac/components/EditUserModal.tsx | 131 ++ .../rbac/components/ResetPasswordModal.tsx | 73 + .../rbac/components/UserDetailModal.tsx | 87 + .../features/rbac/components/UserFilters.tsx | 78 + .../features/rbac/components/UserTable.tsx | 222 +++ frontend/src/features/rbac/constants.ts | 52 + .../features/rbac/hooks/useUserSelection.ts | 35 + .../src/features/rbac/hooks/useUsersList.ts | 47 + frontend/src/features/rbac/types.ts | 30 + frontend/src/features/rbac/utils.ts | 44 + .../ldap/components/LdapAccountTestDialog.tsx | 128 ++ .../ldap/components/LdapAddUserDialog.tsx | 105 ++ .../system/ldap/components/LdapConfigForm.tsx | 197 ++ .../ldap/components/LdapCreateOuDialog.tsx | 83 + .../ldap/components/LdapManagePanel.tsx | 133 ++ .../system/ldap/components/LdapUserTable.tsx | 98 + frontend/src/features/system/ldap/types.ts | 56 + .../ticket/components/DetailApprovalFlow.tsx | 104 ++ .../ticket/components/DetailApprovalPanel.tsx | 173 ++ .../ticket/components/DetailAuditResult.tsx | 99 + .../ticket/components/DetailExecuteResult.tsx | 120 ++ .../components/DetailRelatedTickets.tsx | 57 + .../components/DetailRollbackSqlCard.tsx | 25 + .../ticket/components/DetailSqlCard.tsx | 25 + .../components/DetailTicketInfoCard.tsx | 64 + .../ticket/components/DetailTimeline.tsx | 83 + .../features/ticket/components/DetailTypes.ts | 57 + .../ticket/components/TicketDetailDialog.tsx | 367 ++++ .../ticket/components/TicketFilterBar.tsx | 65 + .../ticket/components/TicketMobileList.tsx | 62 + .../ticket/components/TicketPagination.tsx | 59 + .../ticket/components/TicketRowActions.tsx | 82 + .../ticket/components/TicketStatsCards.tsx | 55 + .../ticket/components/TicketStatusBadge.tsx | 20 + .../ticket/components/TicketTable.tsx | 164 ++ frontend/src/features/ticket/constants.ts | 52 + frontend/src/features/ticket/types.ts | 92 + .../features/ticket/useTicketListParams.ts | 59 + frontend/src/features/ticket/utils.ts | 21 + frontend/src/hooks/useAuth.tsx | 58 +- frontend/src/hooks/useLogin.tsx | 7 +- frontend/src/hooks/useQueryState.ts | 59 + .../src/lib/__tests__/permissions.test.ts | 79 + frontend/src/lib/__tests__/storage.test.ts | 43 + frontend/src/lib/permissions.ts | 64 + frontend/src/pages/AuditLogsPage.tsx | 19 +- frontend/src/pages/InstancePage.tsx | 1632 ++--------------- frontend/src/pages/LdapConfigPage.tsx | 725 ++------ frontend/src/pages/QueriesPage.tsx | 587 +----- frontend/src/pages/SettingsPage.tsx | 5 + frontend/src/pages/TicketDetailPage.tsx | 728 +------- frontend/src/pages/TicketsPage.tsx | 1057 +---------- frontend/src/pages/UsersPage.tsx | 1226 +------------ frontend/src/pages/WorkflowPage.tsx | 719 +------- frontend/src/vite-env.d.ts | 4 + frontend/tsconfig.app.json | 1 + frontend/vite.config.ts | 14 + frontend/vitest.config.ts | 14 + 92 files changed, 8224 insertions(+), 6146 deletions(-) create mode 100644 frontend/.prettierignore create mode 100644 frontend/.prettierrc create mode 100644 frontend/commitlint.config.js create mode 100644 frontend/src/features/datasource/components/CreateInstanceForm.tsx create mode 100644 frontend/src/features/datasource/components/DbTypeIcons.tsx create mode 100644 frontend/src/features/datasource/components/InstanceBatchBar.tsx create mode 100644 frontend/src/features/datasource/components/InstanceDetail.tsx create mode 100644 frontend/src/features/datasource/components/InstanceFilterBar.tsx create mode 100644 frontend/src/features/datasource/components/InstanceGrid.tsx create mode 100644 frontend/src/features/datasource/components/InstanceStatsOverview.tsx create mode 100644 frontend/src/features/datasource/components/InstanceTable.tsx create mode 100644 frontend/src/features/datasource/components/QuickImportDialog.tsx create mode 100644 frontend/src/features/datasource/hooks/useInstanceList.ts create mode 100644 frontend/src/features/datasource/hooks/useInstanceSelection.ts create mode 100644 frontend/src/features/datasource/types.ts create mode 100644 frontend/src/features/datasource/utils.ts create mode 100644 frontend/src/features/governance/workflow/components/UserDropdown.tsx create mode 100644 frontend/src/features/governance/workflow/components/WorkflowCard.tsx create mode 100644 frontend/src/features/governance/workflow/components/WorkflowEditorModal.tsx create mode 100644 frontend/src/features/governance/workflow/components/WorkflowPreviewModal.tsx create mode 100644 frontend/src/features/governance/workflow/components/WorkflowStatsGrid.tsx create mode 100644 frontend/src/features/governance/workflow/constants.ts create mode 100644 frontend/src/features/governance/workflow/types.ts create mode 100644 frontend/src/features/query/components/QueryResultPanel.tsx create mode 100644 frontend/src/features/query/components/QueryTopBar.tsx create mode 100644 frontend/src/features/query/components/RedisKeyValuePanel.tsx create mode 100644 frontend/src/features/query/components/SqlEditorSection.tsx create mode 100644 frontend/src/features/query/constants.ts create mode 100644 frontend/src/features/rbac/components/BatchActionBar.tsx create mode 100644 frontend/src/features/rbac/components/BatchImportModal.tsx create mode 100644 frontend/src/features/rbac/components/CreateUserModal.tsx create mode 100644 frontend/src/features/rbac/components/EditUserModal.tsx create mode 100644 frontend/src/features/rbac/components/ResetPasswordModal.tsx create mode 100644 frontend/src/features/rbac/components/UserDetailModal.tsx create mode 100644 frontend/src/features/rbac/components/UserFilters.tsx create mode 100644 frontend/src/features/rbac/components/UserTable.tsx create mode 100644 frontend/src/features/rbac/constants.ts create mode 100644 frontend/src/features/rbac/hooks/useUserSelection.ts create mode 100644 frontend/src/features/rbac/hooks/useUsersList.ts create mode 100644 frontend/src/features/rbac/types.ts create mode 100644 frontend/src/features/rbac/utils.ts create mode 100644 frontend/src/features/system/ldap/components/LdapAccountTestDialog.tsx create mode 100644 frontend/src/features/system/ldap/components/LdapAddUserDialog.tsx create mode 100644 frontend/src/features/system/ldap/components/LdapConfigForm.tsx create mode 100644 frontend/src/features/system/ldap/components/LdapCreateOuDialog.tsx create mode 100644 frontend/src/features/system/ldap/components/LdapManagePanel.tsx create mode 100644 frontend/src/features/system/ldap/components/LdapUserTable.tsx create mode 100644 frontend/src/features/system/ldap/types.ts create mode 100644 frontend/src/features/ticket/components/DetailApprovalFlow.tsx create mode 100644 frontend/src/features/ticket/components/DetailApprovalPanel.tsx create mode 100644 frontend/src/features/ticket/components/DetailAuditResult.tsx create mode 100644 frontend/src/features/ticket/components/DetailExecuteResult.tsx create mode 100644 frontend/src/features/ticket/components/DetailRelatedTickets.tsx create mode 100644 frontend/src/features/ticket/components/DetailRollbackSqlCard.tsx create mode 100644 frontend/src/features/ticket/components/DetailSqlCard.tsx create mode 100644 frontend/src/features/ticket/components/DetailTicketInfoCard.tsx create mode 100644 frontend/src/features/ticket/components/DetailTimeline.tsx create mode 100644 frontend/src/features/ticket/components/DetailTypes.ts create mode 100644 frontend/src/features/ticket/components/TicketDetailDialog.tsx create mode 100644 frontend/src/features/ticket/components/TicketFilterBar.tsx create mode 100644 frontend/src/features/ticket/components/TicketMobileList.tsx create mode 100644 frontend/src/features/ticket/components/TicketPagination.tsx create mode 100644 frontend/src/features/ticket/components/TicketRowActions.tsx create mode 100644 frontend/src/features/ticket/components/TicketStatsCards.tsx create mode 100644 frontend/src/features/ticket/components/TicketStatusBadge.tsx create mode 100644 frontend/src/features/ticket/components/TicketTable.tsx create mode 100644 frontend/src/features/ticket/constants.ts create mode 100644 frontend/src/features/ticket/types.ts create mode 100644 frontend/src/features/ticket/useTicketListParams.ts create mode 100644 frontend/src/features/ticket/utils.ts create mode 100644 frontend/src/hooks/useQueryState.ts create mode 100644 frontend/src/lib/__tests__/permissions.test.ts create mode 100644 frontend/src/lib/__tests__/storage.test.ts create mode 100644 frontend/src/lib/permissions.ts create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/vitest.config.ts diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..74b2199 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,4 @@ +dist +node_modules +package-lock.json +src/components/ui diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..0f2bddd --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "semi": true, + "trailingComma": "all", + "printWidth": 110, + "tabWidth": 2 +} diff --git a/frontend/commitlint.config.js b/frontend/commitlint.config.js new file mode 100644 index 0000000..54a3672 --- /dev/null +++ b/frontend/commitlint.config.js @@ -0,0 +1,13 @@ +/** Conventional Commits(scope 用功能域) */ +export default { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [ + 2, + 'always', + ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'revert'], + ], + 'subject-empty': [2, 'never'], + 'type-empty': [2, 'never'], + }, +}; diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 5e6b472..e837cdc 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -17,7 +17,11 @@ export default defineConfig([ ], languageOptions: { ecmaVersion: 2020, - globals: globals.browser, + globals: { ...globals.browser, ...globals.node }, + }, + rules: { + // 生产禁 console.log(允许 warn/error 上报保留) + 'no-console': ['warn', { allow: ['warn', 'error'] }], }, }, ]) diff --git a/frontend/package.json b/frontend/package.json index 346d76e..32d2167 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,11 @@ "dev": "vite", "build": "vite build", "lint": "eslint .", + "lint:fix": "eslint . --fix", + "typecheck": "tsc --noEmit -p tsconfig.app.json", + "format": "prettier --write \"src/**/*.{ts,tsx,css}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,css}\"", + "test": "vitest run", "audit:pages": "node scripts/audit-page-size.mjs 700", "preview": "vite preview" }, @@ -50,5 +55,14 @@ "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4" + }, + "lint-staged": { + "*.{ts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.css": [ + "prettier --write" + ] } } diff --git a/frontend/src/features/datasource/components/CreateInstanceForm.tsx b/frontend/src/features/datasource/components/CreateInstanceForm.tsx new file mode 100644 index 0000000..535d7cd --- /dev/null +++ b/frontend/src/features/datasource/components/CreateInstanceForm.tsx @@ -0,0 +1,552 @@ +/** + * 新建数据源四步表单(类型选择/基本信息/连接信息/高级配置),须先测试连接成功才可提交。 + * 必需 props:defaultType 初始数据库类型、onCancel 取消回调、onCreated 创建成功回调。 + * 已知限制:未通过「测试连接」前提交按钮不可用;步骤指示器仅允许回退已完成的步骤。 + */ +import { useState, useCallback } from 'react'; +import { + Database, Server, Zap, Activity, AlertTriangle, CheckCircle, XCircle, + ChevronRight, Eye, EyeOff, Loader2, +} from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; +import { ModalOverlay } from '@/components/ModalOverlay'; +import { instanceApi } from '@/api/instance'; +import { DB_DEFAULT_PORTS } from '../utils'; +import { MySQLIcon, PostgreSQLIcon, RedisIcon, MongoDBIcon } from './DbTypeIcons'; + +/** 预置脱敏字段集合(高级配置一键填入用) */ +const PRESET_MASK_FIELDS = [ + 'mobile', 'concat_mobile', 'email_alias', 'email', 'password', + 'ga_key', 'private_key', 'secret', 'secret_key', 'key_snow', + 'telephone', 'username', 'real_name', 'token', 'two_factor_secret', 'content', +]; + +/** 创建向导的四个步骤(静态序列) */ +const formSteps = [ + { label: '类型选择', icon: Database }, + { label: '基本信息', icon: Server }, + { label: '连接信息', icon: Zap }, + { label: '高级配置', icon: Activity }, +]; + +/** 宽松布尔解析(后端测试连接返回的 cluster_mode 可能是字符串) */ +function toBool(value: unknown): boolean { + return value === true || value === 'true' || value === 1 || value === '1'; +} + +interface CreateInstanceFormProps { + defaultType: string; + onCancel: () => void; + onCreated: () => void; +} + +export function CreateInstanceForm({ defaultType, onCancel, onCreated }: CreateInstanceFormProps) { + const [currentStep, setCurrentStep] = useState(0); + const [dbType, setDbType] = useState(defaultType); + const [name, setName] = useState(''); + const [host, setHost] = useState(''); + const [port, setPort] = useState(String(DB_DEFAULT_PORTS[defaultType] || 3306)); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [showPwd, setShowPwd] = useState(false); + const [database, setDatabase] = useState(''); + const [redisDb, setRedisDb] = useState('0'); + const [redisClusterMode, setRedisClusterMode] = useState(false); + const [env, setEnv] = useState('development'); + const [customEnv, setCustomEnv] = useState(''); + const [accessMode, setAccessMode] = useState<'readwrite' | 'read' | 'write'>('readwrite'); + const [excludedDbs, setExcludedDbs] = useState(''); + const [maskFields, setMaskFields] = useState(''); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(''); + const [testing, setTesting] = useState(false); + const [testResult, setTestResult] = useState<{ success: boolean; message: string; version?: string; cluster_mode?: boolean } | null>(null); + const isRedis = dbType === 'redis'; + const needUsername = !isRedis; // Redis username is optional (6.0+ ACL) + const needDatabase = !isRedis; + const finalEnv = env === '__custom__' ? customEnv.trim() : env; + const canSubmit = testResult?.success === true; + + const handleTypeChange = useCallback((t: string) => { + setDbType(t); + setPort(String(DB_DEFAULT_PORTS[t] || 3306)); + setTestResult(null); + if (t === 'redis') { setDatabase(''); } else { setRedisClusterMode(false); } + }, []); + + const handleSubmit = useCallback(async () => { + if (!name) { setError('请填写名称'); return; } + if (!host || !port) { setError('请填写地址和端口'); return; } + if (needUsername && !username) { setError('请填写用户名'); return; } + setSubmitting(true); setError(''); + try { + await instanceApi.create({ + name, db_type: dbType, host, port: Number(port), + username: isRedis ? (username || undefined) : (username || 'default'), + password: password || 'default', + database: database || undefined, + cluster_mode: isRedis ? redisClusterMode : undefined, + env: (finalEnv || 'development') as string, + access_mode: accessMode, + excluded_databases: excludedDbs ? excludedDbs.split(',').map(s => s.trim()).filter(Boolean) : undefined, + mask_fields: maskFields ? maskFields.split('\n').filter(Boolean).map(line => { + const parts = line.split(':'); + return { table: parts[0]?.trim() || '', field: parts[1]?.trim() || parts[0]?.trim() || '', rule: parts[2]?.trim() || 'mask' }; + }) : undefined, + }); + onCreated(); + } catch (e: unknown) { + const msg = (e as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '创建失败'; + setError(msg); + } finally { + setSubmitting(false); + } + }, [name, host, port, dbType, username, password, database, redisClusterMode, finalEnv, accessMode, excludedDbs, maskFields, needUsername, isRedis, onCreated]); + + const handleTestConnection = useCallback(async () => { + if (!host || !port) { setError('请填写地址和端口'); return; } + if (needUsername && !username) { setError('请填写用户名'); return; } + setTesting(true); setError(''); setTestResult(null); + try { + const res = await instanceApi.testNewConnection({ + name: name || '__test__', db_type: dbType, host, port: Number(port), + username: isRedis ? (username || undefined) : (username || 'default'), + password: password || 'default', + database: database || undefined, + cluster_mode: isRedis ? redisClusterMode : undefined, + env: (finalEnv || 'development') as string, access_mode: accessMode, + }); + const result = res as { success: boolean; message: string; version?: string; cluster_mode?: boolean }; + if (isRedis && result.success) { + setRedisClusterMode(toBool(result.cluster_mode)); + } + setTestResult(result); + } catch (e: unknown) { + const msg = (e as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '连接失败'; + setTestResult({ success: false, message: msg }); + } finally { + setTesting(false); + } + }, [host, port, dbType, username, password, database, redisClusterMode, finalEnv, accessMode, needUsername, name, isRedis]); + + const validateStep = useCallback((step: number): boolean => { + setError(''); + if (step === 1) { + if (!name) { setError('请填写实例名称'); return false; } + } + if (step === 2) { + if (!host || !port) { setError('请填写地址和端口'); return false; } + if (needUsername && !username) { setError('请填写用户名'); return false; } + } + return true; + }, [name, host, port, needUsername, username]); + + const goNext = useCallback(() => { + if (validateStep(currentStep)) { + setCurrentStep(s => Math.min(s + 1, formSteps.length - 1)); + } + }, [currentStep, validateStep]); + + const goPrev = useCallback(() => { + setError(''); + setCurrentStep(s => Math.max(s - 1, 0)); + }, []); + + const inputCls = 'w-full px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none transition-colors'; + const labelCls = 'block text-sm font-medium mb-1.5'; + + return ( + <> + +
+ {/* Header */} +
+
+

添加数据源

+

步骤 {currentStep + 1} / {formSteps.length}: {formSteps[currentStep].label}

+
+ +
+ + {/* Step Indicator */} +
+
+ {formSteps.map((step, idx) => ( +
+ + {idx < formSteps.length - 1 && ( + + )} +
+ ))} +
+
+ +
+ {/* Step 0: Type Selection */} + {currentStep === 0 && ( + + +

选择数据源类型

+
+ {[ + { value: 'mysql', label: 'MySQL', Icon: MySQLIcon }, + { value: 'postgres', label: 'PostgreSQL', Icon: PostgreSQLIcon }, + { value: 'redis', label: 'Redis', Icon: RedisIcon }, + { value: 'mongo', label: 'MongoDB', Icon: MongoDBIcon }, + ].map(t => ( + + ))} +
+
+
+ )} + + {/* Step 1: Basic Info */} + {currentStep === 1 && ( + + +

基本信息

+
+
+ + { setName(e.target.value); setTestResult(null); }} + className={inputCls} + placeholder="例如:prod-mysql-只读库" + /> +
+
+ +
+ + {env === '__custom__' && ( + setCustomEnv(e.target.value)} + className={inputCls} + placeholder="输入自定义环境名" + /> + )} +
+
+
+ {isRedis && ( +
+

+ + Redis 6.0+ ACL 支持用户名+密码认证;传统模式仅填密码即可 +

+
+ )} +
+
+ )} + + {/* Step 2: Connection Info */} + {currentStep === 2 && ( + + +

连接信息

+
+
+ + { setHost(e.target.value); setTestResult(null); }} + className={inputCls} + placeholder="例如:mysql.example.com" + /> +
+
+ + { setPort(e.target.value); setTestResult(null); }} + className={inputCls} + placeholder={String(DB_DEFAULT_PORTS[dbType] || 3306)} + /> +
+
+ + { setUsername(e.target.value); setTestResult(null); }} + className={inputCls} + placeholder={isRedis ? 'ACL 用户名,留空则仅密码认证' : 'root'} + /> +
+
+
+
+ +
+ { setPassword(e.target.value); setTestResult(null); }} + className={inputCls + ' pr-10'} + placeholder={isRedis ? '可选,Redis 无密码可留空' : '数据库密码'} + /> + +
+
+ {needDatabase ? ( +
+ + { setDatabase(e.target.value); setTestResult(null); }} + className={inputCls} + placeholder="可选,留空则连接默认库" + /> +
+ ) : ( +
+ + setRedisDb(e.target.value)} + className={inputCls} + placeholder="默认 0" + /> +
+ )} +
+ {isRedis && ( +
+ +
+ {[ + { value: false, label: '单节点', desc: '普通 Redis 或哨兵入口,查询只访问当前实例' }, + { value: true, label: '集群', desc: 'Redis Cluster,按 slot 路由并扫描所有 master' }, + ].map(mode => ( + + ))} +
+
+ )} + {/* Connection Mode */} +
+ +
+ {[ + { value: 'readwrite' as const, label: '读写' }, + { value: 'write' as const, label: '写' }, + { value: 'read' as const, label: '读' }, + ].map(m => ( + + ))} +
+
+ {/* Test Result */} + {testResult && ( +
+ {testResult.success + ? <> 连接成功{testResult.version ? ` · ${testResult.version}` : ''}{isRedis ? ` · ${testResult.cluster_mode ? '集群' : '单节点'}` : ''} + : <> {testResult.message} + } +
+ )} +
+
+ )} + + {/* Step 3: Advanced Config */} + {currentStep === 3 && ( + + +

高级配置

+ {!isRedis && ( +
+ + setExcludedDbs(e.target.value)} + className={inputCls} + placeholder="以逗号分隔,如:sys, information_schema, mysql" + /> +
+ )} +
+
+ +
+ + +
+
+
+ {PRESET_MASK_FIELDS.map(f => { + const isSelected = maskFields.split(/[\n,:]/).some(s => s.trim() === f); + return ( + + ); + })} +
+