fdb2 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +21 -21
- package/.editorconfig +11 -11
- package/.eslintrc.cjs +14 -14
- package/.eslintrc.json +7 -7
- package/.prettierrc.js +3 -3
- package/.tpl.env +21 -21
- package/.vscodeignore +45 -45
- package/README.md +312 -312
- package/bin/build.sh +28 -28
- package/bin/deploy.sh +8 -8
- package/bin/dev.sh +10 -10
- package/bin/docker/dev-docker-compose.yml +43 -43
- package/bin/docker/dev.Dockerfile +24 -24
- package/bin/docker/prod-docker-compose.yml +17 -17
- package/bin/docker/prod.Dockerfile +29 -29
- package/bin/fdb2.js +220 -220
- package/dist/package.json +29 -29
- package/dist/pnpm-lock.yaml +1042 -354
- package/dist/public/explorer.css +1464 -1437
- package/dist/public/explorer.js +764 -226
- package/dist/public/index.css +1026 -1026
- package/dist/public/index.js +15 -9
- package/dist/public/layout.css +221 -221
- package/dist/public/layout.js +1 -1
- package/dist/public/vue.js +8 -2
- package/dist/scripts/preinstall.js +112 -112
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.ts +680 -671
- package/dist/server/model/connection.entity.ts +65 -65
- package/dist/server/model/database.entity.ts +245 -245
- package/dist/server/service/connection.service.d.ts +6 -1
- package/dist/server/service/connection.service.d.ts.map +1 -1
- package/dist/server/service/connection.service.js +15 -0
- package/dist/server/service/connection.service.js.map +1 -1
- package/dist/server/service/connection.service.ts +356 -341
- package/dist/server/service/database/base.service.d.ts +27 -0
- package/dist/server/service/database/base.service.d.ts.map +1 -1
- package/dist/server/service/database/base.service.js +17 -0
- package/dist/server/service/database/base.service.js.map +1 -1
- package/dist/server/service/database/base.service.ts +406 -367
- package/dist/server/service/database/cockroachdb.service.d.ts +16 -0
- package/dist/server/service/database/cockroachdb.service.d.ts.map +1 -1
- package/dist/server/service/database/cockroachdb.service.js +220 -154
- package/dist/server/service/database/cockroachdb.service.js.map +1 -1
- package/dist/server/service/database/cockroachdb.service.ts +871 -782
- package/dist/server/service/database/database.service.d.ts +4 -0
- package/dist/server/service/database/database.service.d.ts.map +1 -1
- package/dist/server/service/database/database.service.js +123 -0
- package/dist/server/service/database/database.service.js.map +1 -1
- package/dist/server/service/database/database.service.ts +775 -638
- package/dist/server/service/database/index.ts +6 -6
- package/dist/server/service/database/mongodb.service.d.ts +16 -0
- package/dist/server/service/database/mongodb.service.d.ts.map +1 -1
- package/dist/server/service/database/mongodb.service.js +35 -0
- package/dist/server/service/database/mongodb.service.js.map +1 -1
- package/dist/server/service/database/mongodb.service.ts +39 -1
- package/dist/server/service/database/mssql.service.d.ts +16 -0
- package/dist/server/service/database/mssql.service.d.ts.map +1 -1
- package/dist/server/service/database/mssql.service.js +168 -96
- package/dist/server/service/database/mssql.service.js.map +1 -1
- package/dist/server/service/database/mssql.service.ts +931 -840
- package/dist/server/service/database/mysql.service.d.ts +16 -0
- package/dist/server/service/database/mysql.service.d.ts.map +1 -1
- package/dist/server/service/database/mysql.service.js +189 -80
- package/dist/server/service/database/mysql.service.js.map +1 -1
- package/dist/server/service/database/mysql.service.ts +1025 -890
- package/dist/server/service/database/oracle.service.d.ts +16 -0
- package/dist/server/service/database/oracle.service.d.ts.map +1 -1
- package/dist/server/service/database/oracle.service.js +182 -120
- package/dist/server/service/database/oracle.service.js.map +1 -1
- package/dist/server/service/database/oracle.service.ts +1035 -959
- package/dist/server/service/database/postgres.service.d.ts +16 -0
- package/dist/server/service/database/postgres.service.d.ts.map +1 -1
- package/dist/server/service/database/postgres.service.js +154 -88
- package/dist/server/service/database/postgres.service.js.map +1 -1
- package/dist/server/service/database/postgres.service.ts +960 -871
- package/dist/server/service/database/sap.service.d.ts +16 -0
- package/dist/server/service/database/sap.service.d.ts.map +1 -1
- package/dist/server/service/database/sap.service.js +66 -0
- package/dist/server/service/database/sap.service.js.map +1 -1
- package/dist/server/service/database/sap.service.ts +89 -0
- package/dist/server/service/database/sqlite.service.d.ts +16 -0
- package/dist/server/service/database/sqlite.service.d.ts.map +1 -1
- package/dist/server/service/database/sqlite.service.js +77 -18
- package/dist/server/service/database/sqlite.service.js.map +1 -1
- package/dist/server/service/database/sqlite.service.ts +787 -708
- package/dist/server/service/session.service.ts +158 -158
- package/dist/view/index.html +38 -38
- package/env.d.ts +1 -1
- package/package.json +1 -1
- package/packages/vscode/.vscodeignore +44 -44
- package/packages/vscode/README.md +62 -62
- package/packages/vscode/out/database-services/cockroachdb.service.js +154 -154
- package/packages/vscode/out/database-services/mssql.service.js +96 -96
- package/packages/vscode/out/database-services/mysql.service.js +80 -80
- package/packages/vscode/out/database-services/oracle.service.js +120 -120
- package/packages/vscode/out/database-services/postgres.service.js +88 -88
- package/packages/vscode/out/database-services/sqlite.service.js +18 -18
- package/packages/vscode/out/provider/WebViewProvider.js +32 -32
- package/packages/vscode/package.json +142 -142
- package/packages/vscode/resources/icon.svg +5 -5
- package/packages/vscode/resources/webview/connection.css +41 -41
- package/packages/vscode/resources/webview/database.css +163 -163
- package/packages/vscode/resources/webview/index.html +9 -9
- package/packages/vscode/resources/webview/modules/header.tpl +13 -13
- package/packages/vscode/resources/webview/modules/initial_state.tpl +54 -54
- package/packages/vscode/resources/webview/query.css +104 -104
- package/packages/vscode/src/database-services/base.service.ts +362 -362
- package/packages/vscode/src/database-services/cockroachdb.service.ts +659 -659
- package/packages/vscode/src/database-services/connection.service.ts +340 -340
- package/packages/vscode/src/database-services/database.service.ts +629 -629
- package/packages/vscode/src/database-services/index.ts +6 -6
- package/packages/vscode/src/database-services/model/connection.entity.ts +65 -65
- package/packages/vscode/src/database-services/model/database.entity.ts +245 -245
- package/packages/vscode/src/database-services/mssql.service.ts +722 -722
- package/packages/vscode/src/database-services/mysql.service.ts +760 -760
- package/packages/vscode/src/database-services/oracle.service.ts +831 -831
- package/packages/vscode/src/database-services/postgres.service.ts +740 -740
- package/packages/vscode/src/database-services/sqlite.service.ts +558 -558
- package/packages/vscode/src/extension.ts +76 -76
- package/packages/vscode/src/provider/DatabaseTreeProvider.ts +167 -167
- package/packages/vscode/src/provider/WebViewProvider.ts +277 -277
- package/packages/vscode/src/service/DatabaseServiceBridge.ts +414 -414
- package/packages/vscode/src/typings/connection.ts +90 -90
- package/packages/vscode/tsconfig.json +21 -21
- package/public/index.html +9 -9
- package/public/modules/header.tpl +13 -13
- package/public/modules/initial_state.tpl +54 -54
- package/scripts/preinstall.js +112 -112
- package/server/index.ts +680 -671
- package/server/model/connection.entity.ts +65 -65
- package/server/model/database.entity.ts +245 -245
- package/server/service/connection.service.ts +356 -341
- package/server/service/database/base.service.ts +406 -367
- package/server/service/database/cockroachdb.service.ts +871 -782
- package/server/service/database/database.service.ts +775 -638
- package/server/service/database/index.ts +6 -6
- package/server/service/database/mongodb.service.ts +39 -1
- package/server/service/database/mssql.service.ts +931 -840
- package/server/service/database/mysql.service.ts +1025 -890
- package/server/service/database/oracle.service.ts +1035 -959
- package/server/service/database/postgres.service.ts +960 -871
- package/server/service/database/sap.service.ts +89 -0
- package/server/service/database/sqlite.service.ts +787 -708
- package/server/service/session.service.ts +158 -158
- package/server/tsconfig.json +20 -20
- package/server.js +149 -149
- package/server.pid +1 -0
- package/src/adapter/ajax.ts +135 -135
- package/src/assets/base.css +1 -1
- package/src/assets/database.css +949 -949
- package/src/assets/images/svg/illustrations/illustration-1.svg +1 -1
- package/src/assets/images/svg/illustrations/illustration-2.svg +2 -2
- package/src/assets/images/svg/illustrations/illustration-3.svg +50 -50
- package/src/assets/images/svg/illustrations/illustration-4.svg +1 -1
- package/src/assets/images/svg/illustrations/illustration-5.svg +73 -73
- package/src/assets/images/svg/illustrations/illustration-6.svg +89 -89
- package/src/assets/images/svg/illustrations/illustration-7.svg +39 -39
- package/src/assets/images/svg/separators/curve-2.svg +3 -3
- package/src/assets/images/svg/separators/curve.svg +3 -3
- package/src/assets/images/svg/separators/line.svg +3 -3
- package/src/assets/logo.svg +73 -73
- package/src/assets/main.css +1 -1
- package/src/base/config.ts +20 -20
- package/src/base/detect.ts +134 -134
- package/src/base/entity.ts +92 -92
- package/src/base/eventBus.ts +36 -36
- package/src/components/connection-editor/index.vue +588 -588
- package/src/components/dataGrid/index.vue +104 -104
- package/src/components/dataGrid/pagination.vue +105 -105
- package/src/components/loading/index.vue +42 -42
- package/src/components/modal/index.ts +180 -180
- package/src/components/modal/index.vue +560 -560
- package/src/components/toast/index.ts +43 -43
- package/src/components/toast/toast.vue +57 -57
- package/src/components/user/name.vue +103 -103
- package/src/components/user/selector.vue +416 -416
- package/src/domain/SysConfig.ts +74 -74
- package/src/platform/App.vue +7 -7
- package/src/platform/database/components/connection-detail.vue +1153 -1154
- package/src/platform/database/components/data-editor.vue +477 -477
- package/src/platform/database/components/database-detail.vue +1173 -1172
- package/src/platform/database/components/database-monitor.vue +1085 -1085
- package/src/platform/database/components/db-tools.vue +1264 -816
- package/src/platform/database/components/query-history.vue +1348 -1348
- package/src/platform/database/components/sql-executor.vue +737 -737
- package/src/platform/database/components/sql-query-editor.vue +1045 -1045
- package/src/platform/database/components/table-detail.vue +1375 -1376
- package/src/platform/database/components/table-editor.vue +916 -916
- package/src/platform/database/explorer.vue +1839 -1839
- package/src/platform/database/index.vue +1192 -1192
- package/src/platform/database/layout.vue +366 -366
- package/src/platform/database/router.ts +36 -36
- package/src/platform/database/styles/common.scss +601 -601
- package/src/platform/database/types/common.ts +444 -444
- package/src/platform/database/utils/export.ts +231 -231
- package/src/platform/database/utils/helpers.ts +436 -436
- package/src/platform/index.ts +32 -32
- package/src/platform/router.ts +40 -40
- package/src/platform/vscode/bridge.ts +121 -121
- package/src/platform/vscode/components/ConnectionPanel.vue +272 -272
- package/src/platform/vscode/components/DatabasePanel.vue +532 -532
- package/src/platform/vscode/components/QueryPanel.vue +371 -371
- package/src/platform/vscode/entry/connection.ts +13 -13
- package/src/platform/vscode/entry/database.ts +13 -13
- package/src/platform/vscode/entry/query.ts +13 -13
- package/src/platform/vscode/index.ts +5 -5
- package/src/service/base.ts +133 -127
- package/src/service/database.ts +505 -495
- package/src/service/login.ts +120 -120
- package/src/shims-vue.d.ts +6 -6
- package/src/stores/connection.ts +266 -266
- package/src/stores/session.ts +87 -87
- package/src/typings/database-types.ts +412 -412
- package/src/typings/database.ts +363 -363
- package/src/typings/global.d.ts +58 -58
- package/src/typings/pinia.d.ts +7 -7
- package/src/utils/clipboard.ts +29 -29
- package/src/utils/database-types.ts +242 -242
- package/src/utils/modal.ts +123 -123
- package/src/utils/request.ts +55 -55
- package/src/utils/sleep.ts +3 -3
- package/src/utils/toast.ts +73 -73
- package/src/utils/util.ts +171 -171
- package/src/utils/xlsx.ts +228 -228
- package/tsconfig.json +33 -33
- package/view/index.html +9 -9
- package/view/modules/header.tpl +13 -13
- package/view/modules/initial_state.tpl +19 -19
- package/vite.config.ts +424 -424
- package/vite.config.vscode.ts +47 -47
- package/server/backups/db_ai_breakout_2026-03-11T08-38-48-677Z.sql +0 -0
|
@@ -1,437 +1,437 @@
|
|
|
1
|
-
// 数据库平台通用工具函数
|
|
2
|
-
|
|
3
|
-
// 格式化文件大小
|
|
4
|
-
export function formatFileSize(bytes: number): string {
|
|
5
|
-
if (bytes === 0) return '0 B';
|
|
6
|
-
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
7
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
8
|
-
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// 格式化时间
|
|
12
|
-
export function formatTime(timestamp: Date): string {
|
|
13
|
-
const now = new Date();
|
|
14
|
-
const diff = now.getTime() - timestamp.getTime();
|
|
15
|
-
const minutes = Math.floor(diff / 60000);
|
|
16
|
-
const hours = Math.floor(diff / 3600000);
|
|
17
|
-
const days = Math.floor(diff / 86400000);
|
|
18
|
-
|
|
19
|
-
if (minutes < 1) return '刚刚';
|
|
20
|
-
if (minutes < 60) return `${minutes}分钟前`;
|
|
21
|
-
if (hours < 24) return `${hours}小时前`;
|
|
22
|
-
if (days < 7) return `${days}天前`;
|
|
23
|
-
|
|
24
|
-
return timestamp.toLocaleDateString('zh-CN');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// 格式化日期时间
|
|
28
|
-
export function formatDateTime(date: Date): string {
|
|
29
|
-
return date.toLocaleString('zh-CN', {
|
|
30
|
-
year: 'numeric',
|
|
31
|
-
month: '2-digit',
|
|
32
|
-
day: '2-digit',
|
|
33
|
-
hour: '2-digit',
|
|
34
|
-
minute: '2-digit',
|
|
35
|
-
second: '2-digit'
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// 格式化执行时间
|
|
40
|
-
export function formatExecutionTime(ms: number): string {
|
|
41
|
-
if (ms < 1000) return `${ms}ms`;
|
|
42
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
|
|
43
|
-
return `${(ms / 60000).toFixed(2)}min`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// 获取执行时间状态类
|
|
47
|
-
export function getExecutionTimeClass(time: number): string {
|
|
48
|
-
if (time < 100) return 'fast';
|
|
49
|
-
if (time < 1000) return 'normal';
|
|
50
|
-
if (time < 2000) return 'slow';
|
|
51
|
-
return 'very-slow';
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// 截断SQL语句
|
|
55
|
-
export function truncateSql(sql: string, maxLength = 80): string {
|
|
56
|
-
if (sql.length <= maxLength) return sql;
|
|
57
|
-
return sql.substring(0, maxLength) + '...';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// 格式化SQL语句
|
|
61
|
-
export function formatSql(sql: string): string {
|
|
62
|
-
return sql
|
|
63
|
-
.replace(/\s+/g, ' ')
|
|
64
|
-
.replace(/,/g, ',\n ')
|
|
65
|
-
.replace(/\bFROM\b/gi, '\nFROM')
|
|
66
|
-
.replace(/\bWHERE\b/gi, '\nWHERE')
|
|
67
|
-
.replace(/\bORDER BY\b/gi, '\nORDER BY')
|
|
68
|
-
.replace(/\bGROUP BY\b/gi, '\nGROUP BY')
|
|
69
|
-
.replace(/\bHAVING\b/gi, '\nHAVING')
|
|
70
|
-
.replace(/\bLIMIT\b/gi, '\nLIMIT')
|
|
71
|
-
.trim();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 获取SQL类型
|
|
75
|
-
export function getSqlType(sql: string): string {
|
|
76
|
-
const trimmed = sql.trim().toUpperCase();
|
|
77
|
-
if (trimmed.startsWith('SELECT')) return 'SELECT';
|
|
78
|
-
if (trimmed.startsWith('INSERT')) return 'INSERT';
|
|
79
|
-
if (trimmed.startsWith('UPDATE')) return 'UPDATE';
|
|
80
|
-
if (trimmed.startsWith('DELETE')) return 'DELETE';
|
|
81
|
-
if (trimmed.startsWith('CREATE')) return 'CREATE';
|
|
82
|
-
if (trimmed.startsWith('ALTER')) return 'ALTER';
|
|
83
|
-
if (trimmed.startsWith('DROP')) return 'DROP';
|
|
84
|
-
if (trimmed.startsWith('TRUNCATE')) return 'TRUNCATE';
|
|
85
|
-
return 'OTHER';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// 验证SQL语句
|
|
89
|
-
export function validateSql(sql: string): { valid: boolean; error?: string } {
|
|
90
|
-
if (!sql || sql.trim() === '') {
|
|
91
|
-
return { valid: false, error: 'SQL语句不能为空' };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 基本的SQL注入检查
|
|
95
|
-
const dangerousPatterns = [
|
|
96
|
-
/DROP\s+DATABASE/i,
|
|
97
|
-
/DROP\s+TABLE/i,
|
|
98
|
-
/TRUNCATE/i,
|
|
99
|
-
/DELETE\s+FROM.*WHERE\s+1\s*=\s*1/i,
|
|
100
|
-
/UPDATE.*SET.*WHERE\s+1\s*=\s*1/i
|
|
101
|
-
];
|
|
102
|
-
|
|
103
|
-
for (const pattern of dangerousPatterns) {
|
|
104
|
-
if (pattern.test(sql)) {
|
|
105
|
-
return { valid: false, error: '检测到潜在的危险SQL操作' };
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return { valid: true };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// 生成UUID
|
|
113
|
-
export function generateUUID(): string {
|
|
114
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
115
|
-
const r = Math.random() * 16 | 0;
|
|
116
|
-
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
117
|
-
return v.toString(16);
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// 深拷贝对象
|
|
122
|
-
export function deepClone<T>(obj: T): T {
|
|
123
|
-
if (obj === null || typeof obj !== 'object') return obj;
|
|
124
|
-
if (obj instanceof Date) return new Date(obj.getTime()) as unknown as T;
|
|
125
|
-
if (obj instanceof Array) return obj.map(item => deepClone(item)) as unknown as T;
|
|
126
|
-
if (typeof obj === 'object') {
|
|
127
|
-
const cloned = {} as T;
|
|
128
|
-
for (const key in obj) {
|
|
129
|
-
if (obj.hasOwnProperty(key)) {
|
|
130
|
-
cloned[key] = deepClone(obj[key]);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return cloned;
|
|
134
|
-
}
|
|
135
|
-
return obj;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// 防抖函数
|
|
139
|
-
export function debounce<T extends (...args: any[]) => any>(
|
|
140
|
-
func: T,
|
|
141
|
-
wait: number
|
|
142
|
-
): (...args: Parameters<T>) => void {
|
|
143
|
-
let timeout: NodeJS.Timeout;
|
|
144
|
-
return (...args: Parameters<T>) => {
|
|
145
|
-
clearTimeout(timeout);
|
|
146
|
-
timeout = setTimeout(() => func(...args), wait);
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// 节流函数
|
|
151
|
-
export function throttle<T extends (...args: any[]) => any>(
|
|
152
|
-
func: T,
|
|
153
|
-
wait: number
|
|
154
|
-
): (...args: Parameters<T>) => void {
|
|
155
|
-
let inThrottle: boolean;
|
|
156
|
-
return (...args: Parameters<T>) => {
|
|
157
|
-
if (!inThrottle) {
|
|
158
|
-
func(...args);
|
|
159
|
-
inThrottle = true;
|
|
160
|
-
setTimeout(() => inThrottle = false, wait);
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// 复制到剪贴板
|
|
166
|
-
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
167
|
-
try {
|
|
168
|
-
await navigator.clipboard.writeText(text);
|
|
169
|
-
return true;
|
|
170
|
-
} catch (error) {
|
|
171
|
-
// 降级方案
|
|
172
|
-
const textArea = document.createElement('textarea');
|
|
173
|
-
textArea.value = text;
|
|
174
|
-
textArea.style.position = 'fixed';
|
|
175
|
-
textArea.style.opacity = '0';
|
|
176
|
-
document.body.appendChild(textArea);
|
|
177
|
-
textArea.focus();
|
|
178
|
-
textArea.select();
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
document.execCommand('copy');
|
|
182
|
-
document.body.removeChild(textArea);
|
|
183
|
-
return true;
|
|
184
|
-
} catch (error) {
|
|
185
|
-
document.body.removeChild(textArea);
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// 下载文件
|
|
192
|
-
export function downloadFile(content: string, filename: string, contentType = 'text/plain') {
|
|
193
|
-
const blob = new Blob([content], { type: contentType });
|
|
194
|
-
const url = URL.createObjectURL(blob);
|
|
195
|
-
const link = document.createElement('a');
|
|
196
|
-
link.href = url;
|
|
197
|
-
link.download = filename;
|
|
198
|
-
document.body.appendChild(link);
|
|
199
|
-
link.click();
|
|
200
|
-
document.body.removeChild(link);
|
|
201
|
-
URL.revokeObjectURL(url);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// 导出为CSV
|
|
205
|
-
export function exportToCSV(data: any[], filename: string) {
|
|
206
|
-
if (data.length === 0) return;
|
|
207
|
-
|
|
208
|
-
const headers = Object.keys(data[0]);
|
|
209
|
-
const csvContent = [
|
|
210
|
-
headers.join(','),
|
|
211
|
-
...data.map(row =>
|
|
212
|
-
headers.map(header => {
|
|
213
|
-
const value = row[header];
|
|
214
|
-
if (value === null || value === undefined) return '';
|
|
215
|
-
if (typeof value === 'object') return JSON.stringify(value);
|
|
216
|
-
return `"${String(value).replace(/"/g, '""')}"`;
|
|
217
|
-
}).join(',')
|
|
218
|
-
)
|
|
219
|
-
].join('\n');
|
|
220
|
-
|
|
221
|
-
downloadFile(csvContent, filename, 'text/csv;charset=utf-8;');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// 导出为JSON
|
|
225
|
-
export function exportToJSON(data: any[], filename: string) {
|
|
226
|
-
const jsonContent = JSON.stringify(data, null, 2);
|
|
227
|
-
downloadFile(jsonContent, filename, 'application/json;charset=utf-8;');
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// 获取数据库类型图标
|
|
231
|
-
export function getDbTypeIcon(type: string): string {
|
|
232
|
-
const iconMap: Record<string, string> = {
|
|
233
|
-
mysql: 'bi-database',
|
|
234
|
-
postgres: 'bi-database',
|
|
235
|
-
postgresql: 'bi-database',
|
|
236
|
-
sqlite: 'bi-database',
|
|
237
|
-
mssql: 'bi-database',
|
|
238
|
-
sqlserver: 'bi-database',
|
|
239
|
-
oracle: 'bi-database',
|
|
240
|
-
mongodb: 'bi-diagram-3'
|
|
241
|
-
};
|
|
242
|
-
return iconMap[type.toLowerCase()] || 'bi-database';
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// 获取数据库类型标签
|
|
246
|
-
export function getDbTypeLabel(type: string): string {
|
|
247
|
-
const labelMap: Record<string, string> = {
|
|
248
|
-
mysql: 'MySQL',
|
|
249
|
-
postgres: 'PostgreSQL',
|
|
250
|
-
postgresql: 'PostgreSQL',
|
|
251
|
-
sqlite: 'SQLite',
|
|
252
|
-
mssql: 'SQL Server',
|
|
253
|
-
sqlserver: 'SQL Server',
|
|
254
|
-
oracle: 'Oracle',
|
|
255
|
-
mongodb: 'MongoDB'
|
|
256
|
-
};
|
|
257
|
-
return labelMap[type.toLowerCase()] || type;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// 获取数据库类型样式类
|
|
261
|
-
export function getDbTypeClass(type: string): string {
|
|
262
|
-
const classMap: Record<string, string> = {
|
|
263
|
-
mysql: 'db-mysql',
|
|
264
|
-
postgres: 'db-postgres',
|
|
265
|
-
postgresql: 'db-postgres',
|
|
266
|
-
sqlite: 'db-sqlite',
|
|
267
|
-
mssql: 'db-mssql',
|
|
268
|
-
sqlserver: 'db-mssql',
|
|
269
|
-
oracle: 'db-oracle',
|
|
270
|
-
mongodb: 'db-mongodb'
|
|
271
|
-
};
|
|
272
|
-
return classMap[type.toLowerCase()] || 'db-default';
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// 计算百分比
|
|
276
|
-
export function calculatePercentage(value: number, total: number): number {
|
|
277
|
-
if (total === 0) return 0;
|
|
278
|
-
return Math.round((value / total) * 100);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// 格式化数字
|
|
282
|
-
export function formatNumber(num: number): string {
|
|
283
|
-
if (num >= 1000000) {
|
|
284
|
-
return (num / 1000000).toFixed(1) + 'M';
|
|
285
|
-
}
|
|
286
|
-
if (num >= 1000) {
|
|
287
|
-
return (num / 1000).toFixed(1) + 'K';
|
|
288
|
-
}
|
|
289
|
-
return num.toString();
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// 获取颜色值
|
|
293
|
-
export function getColorByValue(value: number, ranges: { threshold: number; color: string }[]): string {
|
|
294
|
-
for (const range of ranges) {
|
|
295
|
-
if (value <= range.threshold) {
|
|
296
|
-
return range.color;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return ranges[ranges.length - 1].color;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// 创建Toast通知
|
|
303
|
-
export function createToast(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info', duration = 3000) {
|
|
304
|
-
const toast = document.createElement('div');
|
|
305
|
-
toast.className = `toast toast-${type}`;
|
|
306
|
-
toast.innerHTML = `
|
|
307
|
-
<div class="toast-content">
|
|
308
|
-
<i class="bi bi-${getToastIcon(type)}"></i>
|
|
309
|
-
<span>${message}</span>
|
|
310
|
-
</div>
|
|
311
|
-
`;
|
|
312
|
-
|
|
313
|
-
Object.assign(toast.style, {
|
|
314
|
-
position: 'fixed',
|
|
315
|
-
top: '20px',
|
|
316
|
-
right: '20px',
|
|
317
|
-
background: 'white',
|
|
318
|
-
border: '1px solid #e5e7eb',
|
|
319
|
-
borderRadius: '8px',
|
|
320
|
-
padding: '1rem',
|
|
321
|
-
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
322
|
-
zIndex: '9999',
|
|
323
|
-
display: 'flex',
|
|
324
|
-
alignItems: 'center',
|
|
325
|
-
gap: '0.5rem',
|
|
326
|
-
minWidth: '250px',
|
|
327
|
-
transform: 'translateX(100%)',
|
|
328
|
-
transition: 'transform 0.3s ease'
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
document.body.appendChild(toast);
|
|
332
|
-
|
|
333
|
-
// 触发动画
|
|
334
|
-
setTimeout(() => {
|
|
335
|
-
toast.style.transform = 'translateX(0)';
|
|
336
|
-
}, 100);
|
|
337
|
-
|
|
338
|
-
// 自动移除
|
|
339
|
-
setTimeout(() => {
|
|
340
|
-
toast.style.transform = 'translateX(100%)';
|
|
341
|
-
setTimeout(() => {
|
|
342
|
-
if (toast.parentNode) {
|
|
343
|
-
toast.parentNode.removeChild(toast);
|
|
344
|
-
}
|
|
345
|
-
}, 300);
|
|
346
|
-
}, duration);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function getToastIcon(type: string): string {
|
|
350
|
-
const iconMap: Record<string, string> = {
|
|
351
|
-
success: 'check-circle',
|
|
352
|
-
error: 'x-circle',
|
|
353
|
-
warning: 'exclamation-triangle',
|
|
354
|
-
info: 'info-circle'
|
|
355
|
-
};
|
|
356
|
-
return iconMap[type] || 'info-circle';
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// 数组去重
|
|
360
|
-
export function uniqueArray<T>(array: T[], key?: keyof T): T[] {
|
|
361
|
-
if (!key) return [...new Set(array)];
|
|
362
|
-
|
|
363
|
-
const seen = new Set();
|
|
364
|
-
return array.filter(item => {
|
|
365
|
-
const value = item[key];
|
|
366
|
-
if (seen.has(value)) {
|
|
367
|
-
return false;
|
|
368
|
-
}
|
|
369
|
-
seen.add(value);
|
|
370
|
-
return true;
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// 数组分组
|
|
375
|
-
export function groupBy<T>(array: T[], key: keyof T): Record<string, T[]> {
|
|
376
|
-
return array.reduce((groups, item) => {
|
|
377
|
-
const groupKey = String(item[key]);
|
|
378
|
-
if (!groups[groupKey]) {
|
|
379
|
-
groups[groupKey] = [];
|
|
380
|
-
}
|
|
381
|
-
groups[groupKey].push(item);
|
|
382
|
-
return groups;
|
|
383
|
-
}, {} as Record<string, T[]>);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// 数组排序
|
|
387
|
-
export function sortBy<T>(array: T[], key: keyof T, direction: 'asc' | 'desc' = 'asc'): T[] {
|
|
388
|
-
return [...array].sort((a, b) => {
|
|
389
|
-
const aVal = a[key];
|
|
390
|
-
const bVal = b[key];
|
|
391
|
-
|
|
392
|
-
if (aVal === null || aVal === undefined) return direction === 'asc' ? 1 : -1;
|
|
393
|
-
if (bVal === null || bVal === undefined) return direction === 'asc' ? -1 : 1;
|
|
394
|
-
|
|
395
|
-
let comparison = 0;
|
|
396
|
-
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
397
|
-
comparison = aVal - bVal;
|
|
398
|
-
} else {
|
|
399
|
-
comparison = String(aVal).localeCompare(String(bVal));
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return direction === 'asc' ? comparison : -comparison;
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// 检查对象是否为空
|
|
407
|
-
export function isEmpty(obj: any): boolean {
|
|
408
|
-
if (obj == null) return true;
|
|
409
|
-
if (Array.isArray(obj)) return obj.length === 0;
|
|
410
|
-
if (typeof obj === 'object') return Object.keys(obj).length === 0;
|
|
411
|
-
return false;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// 合并对象
|
|
415
|
-
export function mergeObjects<T extends Record<string, any>>(...objects: Partial<T>[]): T {
|
|
416
|
-
return objects.reduce((result, obj) => {
|
|
417
|
-
return { ...result, ...obj };
|
|
418
|
-
}, {} as T);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// 获取嵌套对象属性
|
|
422
|
-
export function getNestedProperty(obj: any, path: string): any {
|
|
423
|
-
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// 设置嵌套对象属性
|
|
427
|
-
export function setNestedProperty(obj: any, path: string, value: any): void {
|
|
428
|
-
const keys = path.split('.');
|
|
429
|
-
const lastKey = keys.pop()!;
|
|
430
|
-
const target = keys.reduce((current, key) => {
|
|
431
|
-
if (!current[key] || typeof current[key] !== 'object') {
|
|
432
|
-
current[key] = {};
|
|
433
|
-
}
|
|
434
|
-
return current[key];
|
|
435
|
-
}, obj);
|
|
436
|
-
target[lastKey] = value;
|
|
1
|
+
// 数据库平台通用工具函数
|
|
2
|
+
|
|
3
|
+
// 格式化文件大小
|
|
4
|
+
export function formatFileSize(bytes: number): string {
|
|
5
|
+
if (bytes === 0) return '0 B';
|
|
6
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
7
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
8
|
+
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 格式化时间
|
|
12
|
+
export function formatTime(timestamp: Date): string {
|
|
13
|
+
const now = new Date();
|
|
14
|
+
const diff = now.getTime() - timestamp.getTime();
|
|
15
|
+
const minutes = Math.floor(diff / 60000);
|
|
16
|
+
const hours = Math.floor(diff / 3600000);
|
|
17
|
+
const days = Math.floor(diff / 86400000);
|
|
18
|
+
|
|
19
|
+
if (minutes < 1) return '刚刚';
|
|
20
|
+
if (minutes < 60) return `${minutes}分钟前`;
|
|
21
|
+
if (hours < 24) return `${hours}小时前`;
|
|
22
|
+
if (days < 7) return `${days}天前`;
|
|
23
|
+
|
|
24
|
+
return timestamp.toLocaleDateString('zh-CN');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 格式化日期时间
|
|
28
|
+
export function formatDateTime(date: Date): string {
|
|
29
|
+
return date.toLocaleString('zh-CN', {
|
|
30
|
+
year: 'numeric',
|
|
31
|
+
month: '2-digit',
|
|
32
|
+
day: '2-digit',
|
|
33
|
+
hour: '2-digit',
|
|
34
|
+
minute: '2-digit',
|
|
35
|
+
second: '2-digit'
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 格式化执行时间
|
|
40
|
+
export function formatExecutionTime(ms: number): string {
|
|
41
|
+
if (ms < 1000) return `${ms}ms`;
|
|
42
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
|
|
43
|
+
return `${(ms / 60000).toFixed(2)}min`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 获取执行时间状态类
|
|
47
|
+
export function getExecutionTimeClass(time: number): string {
|
|
48
|
+
if (time < 100) return 'fast';
|
|
49
|
+
if (time < 1000) return 'normal';
|
|
50
|
+
if (time < 2000) return 'slow';
|
|
51
|
+
return 'very-slow';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 截断SQL语句
|
|
55
|
+
export function truncateSql(sql: string, maxLength = 80): string {
|
|
56
|
+
if (sql.length <= maxLength) return sql;
|
|
57
|
+
return sql.substring(0, maxLength) + '...';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 格式化SQL语句
|
|
61
|
+
export function formatSql(sql: string): string {
|
|
62
|
+
return sql
|
|
63
|
+
.replace(/\s+/g, ' ')
|
|
64
|
+
.replace(/,/g, ',\n ')
|
|
65
|
+
.replace(/\bFROM\b/gi, '\nFROM')
|
|
66
|
+
.replace(/\bWHERE\b/gi, '\nWHERE')
|
|
67
|
+
.replace(/\bORDER BY\b/gi, '\nORDER BY')
|
|
68
|
+
.replace(/\bGROUP BY\b/gi, '\nGROUP BY')
|
|
69
|
+
.replace(/\bHAVING\b/gi, '\nHAVING')
|
|
70
|
+
.replace(/\bLIMIT\b/gi, '\nLIMIT')
|
|
71
|
+
.trim();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 获取SQL类型
|
|
75
|
+
export function getSqlType(sql: string): string {
|
|
76
|
+
const trimmed = sql.trim().toUpperCase();
|
|
77
|
+
if (trimmed.startsWith('SELECT')) return 'SELECT';
|
|
78
|
+
if (trimmed.startsWith('INSERT')) return 'INSERT';
|
|
79
|
+
if (trimmed.startsWith('UPDATE')) return 'UPDATE';
|
|
80
|
+
if (trimmed.startsWith('DELETE')) return 'DELETE';
|
|
81
|
+
if (trimmed.startsWith('CREATE')) return 'CREATE';
|
|
82
|
+
if (trimmed.startsWith('ALTER')) return 'ALTER';
|
|
83
|
+
if (trimmed.startsWith('DROP')) return 'DROP';
|
|
84
|
+
if (trimmed.startsWith('TRUNCATE')) return 'TRUNCATE';
|
|
85
|
+
return 'OTHER';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 验证SQL语句
|
|
89
|
+
export function validateSql(sql: string): { valid: boolean; error?: string } {
|
|
90
|
+
if (!sql || sql.trim() === '') {
|
|
91
|
+
return { valid: false, error: 'SQL语句不能为空' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 基本的SQL注入检查
|
|
95
|
+
const dangerousPatterns = [
|
|
96
|
+
/DROP\s+DATABASE/i,
|
|
97
|
+
/DROP\s+TABLE/i,
|
|
98
|
+
/TRUNCATE/i,
|
|
99
|
+
/DELETE\s+FROM.*WHERE\s+1\s*=\s*1/i,
|
|
100
|
+
/UPDATE.*SET.*WHERE\s+1\s*=\s*1/i
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
for (const pattern of dangerousPatterns) {
|
|
104
|
+
if (pattern.test(sql)) {
|
|
105
|
+
return { valid: false, error: '检测到潜在的危险SQL操作' };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { valid: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 生成UUID
|
|
113
|
+
export function generateUUID(): string {
|
|
114
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
115
|
+
const r = Math.random() * 16 | 0;
|
|
116
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
117
|
+
return v.toString(16);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 深拷贝对象
|
|
122
|
+
export function deepClone<T>(obj: T): T {
|
|
123
|
+
if (obj === null || typeof obj !== 'object') return obj;
|
|
124
|
+
if (obj instanceof Date) return new Date(obj.getTime()) as unknown as T;
|
|
125
|
+
if (obj instanceof Array) return obj.map(item => deepClone(item)) as unknown as T;
|
|
126
|
+
if (typeof obj === 'object') {
|
|
127
|
+
const cloned = {} as T;
|
|
128
|
+
for (const key in obj) {
|
|
129
|
+
if (obj.hasOwnProperty(key)) {
|
|
130
|
+
cloned[key] = deepClone(obj[key]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return cloned;
|
|
134
|
+
}
|
|
135
|
+
return obj;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 防抖函数
|
|
139
|
+
export function debounce<T extends (...args: any[]) => any>(
|
|
140
|
+
func: T,
|
|
141
|
+
wait: number
|
|
142
|
+
): (...args: Parameters<T>) => void {
|
|
143
|
+
let timeout: NodeJS.Timeout;
|
|
144
|
+
return (...args: Parameters<T>) => {
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
timeout = setTimeout(() => func(...args), wait);
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 节流函数
|
|
151
|
+
export function throttle<T extends (...args: any[]) => any>(
|
|
152
|
+
func: T,
|
|
153
|
+
wait: number
|
|
154
|
+
): (...args: Parameters<T>) => void {
|
|
155
|
+
let inThrottle: boolean;
|
|
156
|
+
return (...args: Parameters<T>) => {
|
|
157
|
+
if (!inThrottle) {
|
|
158
|
+
func(...args);
|
|
159
|
+
inThrottle = true;
|
|
160
|
+
setTimeout(() => inThrottle = false, wait);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 复制到剪贴板
|
|
166
|
+
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
167
|
+
try {
|
|
168
|
+
await navigator.clipboard.writeText(text);
|
|
169
|
+
return true;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
// 降级方案
|
|
172
|
+
const textArea = document.createElement('textarea');
|
|
173
|
+
textArea.value = text;
|
|
174
|
+
textArea.style.position = 'fixed';
|
|
175
|
+
textArea.style.opacity = '0';
|
|
176
|
+
document.body.appendChild(textArea);
|
|
177
|
+
textArea.focus();
|
|
178
|
+
textArea.select();
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
document.execCommand('copy');
|
|
182
|
+
document.body.removeChild(textArea);
|
|
183
|
+
return true;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
document.body.removeChild(textArea);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 下载文件
|
|
192
|
+
export function downloadFile(content: string, filename: string, contentType = 'text/plain') {
|
|
193
|
+
const blob = new Blob([content], { type: contentType });
|
|
194
|
+
const url = URL.createObjectURL(blob);
|
|
195
|
+
const link = document.createElement('a');
|
|
196
|
+
link.href = url;
|
|
197
|
+
link.download = filename;
|
|
198
|
+
document.body.appendChild(link);
|
|
199
|
+
link.click();
|
|
200
|
+
document.body.removeChild(link);
|
|
201
|
+
URL.revokeObjectURL(url);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 导出为CSV
|
|
205
|
+
export function exportToCSV(data: any[], filename: string) {
|
|
206
|
+
if (data.length === 0) return;
|
|
207
|
+
|
|
208
|
+
const headers = Object.keys(data[0]);
|
|
209
|
+
const csvContent = [
|
|
210
|
+
headers.join(','),
|
|
211
|
+
...data.map(row =>
|
|
212
|
+
headers.map(header => {
|
|
213
|
+
const value = row[header];
|
|
214
|
+
if (value === null || value === undefined) return '';
|
|
215
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
216
|
+
return `"${String(value).replace(/"/g, '""')}"`;
|
|
217
|
+
}).join(',')
|
|
218
|
+
)
|
|
219
|
+
].join('\n');
|
|
220
|
+
|
|
221
|
+
downloadFile(csvContent, filename, 'text/csv;charset=utf-8;');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 导出为JSON
|
|
225
|
+
export function exportToJSON(data: any[], filename: string) {
|
|
226
|
+
const jsonContent = JSON.stringify(data, null, 2);
|
|
227
|
+
downloadFile(jsonContent, filename, 'application/json;charset=utf-8;');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 获取数据库类型图标
|
|
231
|
+
export function getDbTypeIcon(type: string): string {
|
|
232
|
+
const iconMap: Record<string, string> = {
|
|
233
|
+
mysql: 'bi-database',
|
|
234
|
+
postgres: 'bi-database',
|
|
235
|
+
postgresql: 'bi-database',
|
|
236
|
+
sqlite: 'bi-database',
|
|
237
|
+
mssql: 'bi-database',
|
|
238
|
+
sqlserver: 'bi-database',
|
|
239
|
+
oracle: 'bi-database',
|
|
240
|
+
mongodb: 'bi-diagram-3'
|
|
241
|
+
};
|
|
242
|
+
return iconMap[type.toLowerCase()] || 'bi-database';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 获取数据库类型标签
|
|
246
|
+
export function getDbTypeLabel(type: string): string {
|
|
247
|
+
const labelMap: Record<string, string> = {
|
|
248
|
+
mysql: 'MySQL',
|
|
249
|
+
postgres: 'PostgreSQL',
|
|
250
|
+
postgresql: 'PostgreSQL',
|
|
251
|
+
sqlite: 'SQLite',
|
|
252
|
+
mssql: 'SQL Server',
|
|
253
|
+
sqlserver: 'SQL Server',
|
|
254
|
+
oracle: 'Oracle',
|
|
255
|
+
mongodb: 'MongoDB'
|
|
256
|
+
};
|
|
257
|
+
return labelMap[type.toLowerCase()] || type;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 获取数据库类型样式类
|
|
261
|
+
export function getDbTypeClass(type: string): string {
|
|
262
|
+
const classMap: Record<string, string> = {
|
|
263
|
+
mysql: 'db-mysql',
|
|
264
|
+
postgres: 'db-postgres',
|
|
265
|
+
postgresql: 'db-postgres',
|
|
266
|
+
sqlite: 'db-sqlite',
|
|
267
|
+
mssql: 'db-mssql',
|
|
268
|
+
sqlserver: 'db-mssql',
|
|
269
|
+
oracle: 'db-oracle',
|
|
270
|
+
mongodb: 'db-mongodb'
|
|
271
|
+
};
|
|
272
|
+
return classMap[type.toLowerCase()] || 'db-default';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 计算百分比
|
|
276
|
+
export function calculatePercentage(value: number, total: number): number {
|
|
277
|
+
if (total === 0) return 0;
|
|
278
|
+
return Math.round((value / total) * 100);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 格式化数字
|
|
282
|
+
export function formatNumber(num: number): string {
|
|
283
|
+
if (num >= 1000000) {
|
|
284
|
+
return (num / 1000000).toFixed(1) + 'M';
|
|
285
|
+
}
|
|
286
|
+
if (num >= 1000) {
|
|
287
|
+
return (num / 1000).toFixed(1) + 'K';
|
|
288
|
+
}
|
|
289
|
+
return num.toString();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 获取颜色值
|
|
293
|
+
export function getColorByValue(value: number, ranges: { threshold: number; color: string }[]): string {
|
|
294
|
+
for (const range of ranges) {
|
|
295
|
+
if (value <= range.threshold) {
|
|
296
|
+
return range.color;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return ranges[ranges.length - 1].color;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 创建Toast通知
|
|
303
|
+
export function createToast(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info', duration = 3000) {
|
|
304
|
+
const toast = document.createElement('div');
|
|
305
|
+
toast.className = `toast toast-${type}`;
|
|
306
|
+
toast.innerHTML = `
|
|
307
|
+
<div class="toast-content">
|
|
308
|
+
<i class="bi bi-${getToastIcon(type)}"></i>
|
|
309
|
+
<span>${message}</span>
|
|
310
|
+
</div>
|
|
311
|
+
`;
|
|
312
|
+
|
|
313
|
+
Object.assign(toast.style, {
|
|
314
|
+
position: 'fixed',
|
|
315
|
+
top: '20px',
|
|
316
|
+
right: '20px',
|
|
317
|
+
background: 'white',
|
|
318
|
+
border: '1px solid #e5e7eb',
|
|
319
|
+
borderRadius: '8px',
|
|
320
|
+
padding: '1rem',
|
|
321
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
322
|
+
zIndex: '9999',
|
|
323
|
+
display: 'flex',
|
|
324
|
+
alignItems: 'center',
|
|
325
|
+
gap: '0.5rem',
|
|
326
|
+
minWidth: '250px',
|
|
327
|
+
transform: 'translateX(100%)',
|
|
328
|
+
transition: 'transform 0.3s ease'
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
document.body.appendChild(toast);
|
|
332
|
+
|
|
333
|
+
// 触发动画
|
|
334
|
+
setTimeout(() => {
|
|
335
|
+
toast.style.transform = 'translateX(0)';
|
|
336
|
+
}, 100);
|
|
337
|
+
|
|
338
|
+
// 自动移除
|
|
339
|
+
setTimeout(() => {
|
|
340
|
+
toast.style.transform = 'translateX(100%)';
|
|
341
|
+
setTimeout(() => {
|
|
342
|
+
if (toast.parentNode) {
|
|
343
|
+
toast.parentNode.removeChild(toast);
|
|
344
|
+
}
|
|
345
|
+
}, 300);
|
|
346
|
+
}, duration);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function getToastIcon(type: string): string {
|
|
350
|
+
const iconMap: Record<string, string> = {
|
|
351
|
+
success: 'check-circle',
|
|
352
|
+
error: 'x-circle',
|
|
353
|
+
warning: 'exclamation-triangle',
|
|
354
|
+
info: 'info-circle'
|
|
355
|
+
};
|
|
356
|
+
return iconMap[type] || 'info-circle';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 数组去重
|
|
360
|
+
export function uniqueArray<T>(array: T[], key?: keyof T): T[] {
|
|
361
|
+
if (!key) return [...new Set(array)];
|
|
362
|
+
|
|
363
|
+
const seen = new Set();
|
|
364
|
+
return array.filter(item => {
|
|
365
|
+
const value = item[key];
|
|
366
|
+
if (seen.has(value)) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
seen.add(value);
|
|
370
|
+
return true;
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 数组分组
|
|
375
|
+
export function groupBy<T>(array: T[], key: keyof T): Record<string, T[]> {
|
|
376
|
+
return array.reduce((groups, item) => {
|
|
377
|
+
const groupKey = String(item[key]);
|
|
378
|
+
if (!groups[groupKey]) {
|
|
379
|
+
groups[groupKey] = [];
|
|
380
|
+
}
|
|
381
|
+
groups[groupKey].push(item);
|
|
382
|
+
return groups;
|
|
383
|
+
}, {} as Record<string, T[]>);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 数组排序
|
|
387
|
+
export function sortBy<T>(array: T[], key: keyof T, direction: 'asc' | 'desc' = 'asc'): T[] {
|
|
388
|
+
return [...array].sort((a, b) => {
|
|
389
|
+
const aVal = a[key];
|
|
390
|
+
const bVal = b[key];
|
|
391
|
+
|
|
392
|
+
if (aVal === null || aVal === undefined) return direction === 'asc' ? 1 : -1;
|
|
393
|
+
if (bVal === null || bVal === undefined) return direction === 'asc' ? -1 : 1;
|
|
394
|
+
|
|
395
|
+
let comparison = 0;
|
|
396
|
+
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
397
|
+
comparison = aVal - bVal;
|
|
398
|
+
} else {
|
|
399
|
+
comparison = String(aVal).localeCompare(String(bVal));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return direction === 'asc' ? comparison : -comparison;
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 检查对象是否为空
|
|
407
|
+
export function isEmpty(obj: any): boolean {
|
|
408
|
+
if (obj == null) return true;
|
|
409
|
+
if (Array.isArray(obj)) return obj.length === 0;
|
|
410
|
+
if (typeof obj === 'object') return Object.keys(obj).length === 0;
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 合并对象
|
|
415
|
+
export function mergeObjects<T extends Record<string, any>>(...objects: Partial<T>[]): T {
|
|
416
|
+
return objects.reduce((result, obj) => {
|
|
417
|
+
return { ...result, ...obj };
|
|
418
|
+
}, {} as T);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 获取嵌套对象属性
|
|
422
|
+
export function getNestedProperty(obj: any, path: string): any {
|
|
423
|
+
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 设置嵌套对象属性
|
|
427
|
+
export function setNestedProperty(obj: any, path: string, value: any): void {
|
|
428
|
+
const keys = path.split('.');
|
|
429
|
+
const lastKey = keys.pop()!;
|
|
430
|
+
const target = keys.reduce((current, key) => {
|
|
431
|
+
if (!current[key] || typeof current[key] !== 'object') {
|
|
432
|
+
current[key] = {};
|
|
433
|
+
}
|
|
434
|
+
return current[key];
|
|
435
|
+
}, obj);
|
|
436
|
+
target[lastKey] = value;
|
|
437
437
|
}
|