fdb2 1.0.0
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 -0
- package/.editorconfig +11 -0
- package/.eslintrc.cjs +14 -0
- package/.eslintrc.json +7 -0
- package/.prettierrc.js +3 -0
- package/.tpl.env +22 -0
- package/README.md +260 -0
- package/bin/build.sh +28 -0
- package/bin/deploy.sh +8 -0
- package/bin/dev.sh +10 -0
- package/bin/docker/.env +4 -0
- package/bin/docker/dev-docker-compose.yml +43 -0
- package/bin/docker/dev.Dockerfile +24 -0
- package/bin/docker/prod-docker-compose.yml +17 -0
- package/bin/docker/prod.Dockerfile +29 -0
- package/bin/fdb2.js +142 -0
- package/data/connections.demo.json +32 -0
- package/env.d.ts +1 -0
- package/nw-build.js +120 -0
- package/nw-dev.js +65 -0
- package/package.json +114 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +9 -0
- package/public/modules/header.tpl +14 -0
- package/public/modules/initial_state.tpl +55 -0
- package/server/index.ts +677 -0
- package/server/model/connection.entity.ts +66 -0
- package/server/model/database.entity.ts +246 -0
- package/server/service/connection.service.ts +334 -0
- package/server/service/database/base.service.ts +363 -0
- package/server/service/database/database.service.ts +510 -0
- package/server/service/database/index.ts +7 -0
- package/server/service/database/mssql.service.ts +723 -0
- package/server/service/database/mysql.service.ts +761 -0
- package/server/service/database/oracle.service.ts +839 -0
- package/server/service/database/postgres.service.ts +744 -0
- package/server/service/database/sqlite.service.ts +559 -0
- package/server/service/session.service.ts +158 -0
- package/server.js +128 -0
- package/src/adapter/ajax.ts +135 -0
- package/src/assets/base.css +1 -0
- package/src/assets/database.css +950 -0
- package/src/assets/images/collapse.png +0 -0
- package/src/assets/images/no-login.png +0 -0
- package/src/assets/images/svg/illustrations/illustration-1.svg +1 -0
- package/src/assets/images/svg/illustrations/illustration-2.svg +2 -0
- package/src/assets/images/svg/illustrations/illustration-3.svg +50 -0
- package/src/assets/images/svg/illustrations/illustration-4.svg +1 -0
- package/src/assets/images/svg/illustrations/illustration-5.svg +73 -0
- package/src/assets/images/svg/illustrations/illustration-6.svg +89 -0
- package/src/assets/images/svg/illustrations/illustration-7.svg +39 -0
- package/src/assets/images/svg/illustrations/illustration-8.svg +1 -0
- package/src/assets/images/svg/separators/curve-2.svg +3 -0
- package/src/assets/images/svg/separators/curve.svg +3 -0
- package/src/assets/images/svg/separators/line.svg +3 -0
- package/src/assets/images/theme/light/screen-1-1000x800.jpg +0 -0
- package/src/assets/images/theme/light/screen-2-1000x800.jpg +0 -0
- package/src/assets/login/bg.jpg +0 -0
- package/src/assets/login/bg.png +0 -0
- package/src/assets/login/left.jpg +0 -0
- package/src/assets/logo.svg +73 -0
- package/src/assets/logo.webp +0 -0
- package/src/assets/main.css +1 -0
- package/src/base/config.ts +20 -0
- package/src/base/detect.ts +134 -0
- package/src/base/entity.ts +92 -0
- package/src/base/eventBus.ts +37 -0
- package/src/base//345/237/272/347/241/200/345/261/202.md +7 -0
- package/src/components/connection-editor/index.vue +590 -0
- package/src/components/dataGrid/index.vue +105 -0
- package/src/components/dataGrid/pagination.vue +106 -0
- package/src/components/loading/index.vue +43 -0
- package/src/components/modal/index.ts +181 -0
- package/src/components/modal/index.vue +560 -0
- package/src/components/toast/index.ts +44 -0
- package/src/components/toast/toast.vue +58 -0
- package/src/components/user/name.vue +104 -0
- package/src/components/user/selector.vue +416 -0
- package/src/domain/SysConfig.ts +74 -0
- package/src/platform/App.vue +8 -0
- package/src/platform/database/components/connection-detail.vue +1154 -0
- package/src/platform/database/components/data-editor.vue +478 -0
- package/src/platform/database/components/data-import-export.vue +1602 -0
- package/src/platform/database/components/database-detail.vue +1173 -0
- package/src/platform/database/components/database-monitor.vue +1086 -0
- package/src/platform/database/components/db-tools.vue +577 -0
- package/src/platform/database/components/query-history.vue +1349 -0
- package/src/platform/database/components/sql-executor.vue +738 -0
- package/src/platform/database/components/sql-query-editor.vue +1046 -0
- package/src/platform/database/components/table-detail.vue +1376 -0
- package/src/platform/database/components/table-editor.vue +690 -0
- package/src/platform/database/explorer.vue +1840 -0
- package/src/platform/database/index.vue +1193 -0
- package/src/platform/database/layout.vue +367 -0
- package/src/platform/database/router.ts +37 -0
- package/src/platform/database/styles/common.scss +602 -0
- package/src/platform/database/types/common.ts +445 -0
- package/src/platform/database/utils/export.ts +232 -0
- package/src/platform/database/utils/helpers.ts +437 -0
- package/src/platform/index.ts +33 -0
- package/src/platform/router.ts +41 -0
- package/src/service/base.ts +128 -0
- package/src/service/database.ts +500 -0
- package/src/service/login.ts +121 -0
- package/src/shims-vue.d.ts +7 -0
- package/src/stores/connection.ts +266 -0
- package/src/stores/session.ts +87 -0
- package/src/typings/database-types.ts +413 -0
- package/src/typings/database.ts +364 -0
- package/src/typings/global.d.ts +58 -0
- package/src/typings/pinia.d.ts +8 -0
- package/src/utils/clipboard.ts +30 -0
- package/src/utils/database-types.ts +243 -0
- package/src/utils/modal.ts +124 -0
- package/src/utils/request.ts +55 -0
- package/src/utils/sleep.ts +4 -0
- package/src/utils/toast.ts +73 -0
- package/src/utils/util.ts +171 -0
- package/src/utils/xlsx.ts +228 -0
- package/tsconfig.json +33 -0
- package/tsconfig.server.json +19 -0
- package/view/index.html +9 -0
- package/view/modules/header.tpl +14 -0
- package/view/modules/initial_state.tpl +20 -0
- package/vite.config.ts +384 -0
|
@@ -0,0 +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;
|
|
437
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createApp } from 'vue';
|
|
2
|
+
import App from './App.vue';
|
|
3
|
+
import router from './router';
|
|
4
|
+
import { createPinia } from 'pinia';
|
|
5
|
+
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
|
6
|
+
|
|
7
|
+
// 引入 vconsole 进行调试
|
|
8
|
+
import VConsole from 'vconsole';
|
|
9
|
+
new VConsole();
|
|
10
|
+
|
|
11
|
+
import 'bootstrap/dist/css/bootstrap.css';
|
|
12
|
+
import 'bootstrap-icons/font/bootstrap-icons.css';
|
|
13
|
+
import '@/assets/database.css';
|
|
14
|
+
import 'bootstrap';
|
|
15
|
+
|
|
16
|
+
import toastPlugin from '@/components/toast/index';
|
|
17
|
+
import Modal from '@/components/modal/index.vue';
|
|
18
|
+
import DataGrid from '@/components/dataGrid/index.vue';
|
|
19
|
+
import modalPlugin from '@/components/modal/index';
|
|
20
|
+
|
|
21
|
+
const app = createApp(App);
|
|
22
|
+
const pinia = createPinia();
|
|
23
|
+
pinia.use(piniaPluginPersistedstate);
|
|
24
|
+
|
|
25
|
+
app.use(router);
|
|
26
|
+
app.use(pinia);
|
|
27
|
+
app.use(toastPlugin);
|
|
28
|
+
app.use(modalPlugin);
|
|
29
|
+
|
|
30
|
+
app.component('Modal', Modal)
|
|
31
|
+
app.component('DataGrid', DataGrid)
|
|
32
|
+
|
|
33
|
+
app.mount('#app');
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createRouter, createWebHistory } from 'vue-router';
|
|
2
|
+
import { useTitle } from '@vueuse/core';
|
|
3
|
+
import { abortAllPending } from '@/adapter/ajax';
|
|
4
|
+
import { checkLogin, initLoginState } from '@/service/login';
|
|
5
|
+
|
|
6
|
+
import databaseRouters from './database/router';
|
|
7
|
+
|
|
8
|
+
export const router = createRouter({
|
|
9
|
+
history: createWebHistory(import.meta.env.BASE_URL),
|
|
10
|
+
routes: [
|
|
11
|
+
// 数据库管理
|
|
12
|
+
...databaseRouters,
|
|
13
|
+
{
|
|
14
|
+
path: '/:pathMatch(.*)*',
|
|
15
|
+
name: 'not-found',
|
|
16
|
+
redirect: '/database/index',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
router.beforeEach(async (to, from, next) => {
|
|
22
|
+
// 切换路由取消请求
|
|
23
|
+
abortAllPending();
|
|
24
|
+
|
|
25
|
+
const ret = await initLoginState(to);
|
|
26
|
+
if(ret === false) return;
|
|
27
|
+
|
|
28
|
+
// 校验登陆态
|
|
29
|
+
if(to.meta?.needAuth) {
|
|
30
|
+
await checkLogin();
|
|
31
|
+
}
|
|
32
|
+
next();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
router.afterEach((to) => {
|
|
36
|
+
if (to.meta?.title) {
|
|
37
|
+
useTitle(to.meta.title as string);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export default router;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
|
|
2
|
+
import * as requestHelper from '@/utils/request';
|
|
3
|
+
import type { AxiosRequestConfig } from 'axios';
|
|
4
|
+
import * as eventBus from '../base/eventBus';
|
|
5
|
+
import config from '../base/config';
|
|
6
|
+
import { isNWjs } from '@/base/detect';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function getRequestUrl(api: string) {
|
|
11
|
+
if(/^(http(s)?:)?\/\//.test(api)) return api;
|
|
12
|
+
const apiUrl = config.apiUrl || `${location.protocol}//${location.hostname}${[80,443].includes(Number(location.port))?'':(':'+location.port)}`;
|
|
13
|
+
return `${apiUrl.trim()}${config.prefix}${api}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function requestServer(url: string, data?: any, option?: AxiosRequestConfig) {
|
|
17
|
+
// 检查是否在 nw.js 环境中运行
|
|
18
|
+
if (isNWjs) {
|
|
19
|
+
// 在 nw.js 环境中,直接 require 后端服务代码并执行
|
|
20
|
+
try {
|
|
21
|
+
// 提取 API 路径,去掉前缀和协议等
|
|
22
|
+
const apiPath = url.replace(/^(http(s)?:)?\/\//, '').replace(/.*?\/api\//, '/api/');
|
|
23
|
+
|
|
24
|
+
// 在 nw.js 环境中,使用 Node.js 的 require 方法加载服务端代码
|
|
25
|
+
// 服务端代码现在是 CommonJS 模块,使用 .cjs 扩展名
|
|
26
|
+
let server;
|
|
27
|
+
|
|
28
|
+
// 尝试使用 Node.js 的 require 方法
|
|
29
|
+
try {
|
|
30
|
+
// 路径 1: 直接从项目根目录加载
|
|
31
|
+
server = require('../server/index.js');
|
|
32
|
+
} catch (e) {
|
|
33
|
+
try {
|
|
34
|
+
// 路径 2: 使用 Node.js 的 path 模块获取正确路径
|
|
35
|
+
const path = require('path');
|
|
36
|
+
const fs = require('fs');
|
|
37
|
+
const appPath = process.cwd();
|
|
38
|
+
const serverFilePath = path.join(appPath, 'server', 'index.js');
|
|
39
|
+
|
|
40
|
+
console.log('尝试加载服务端代码:', serverFilePath);
|
|
41
|
+
|
|
42
|
+
if (fs.existsSync(serverFilePath)) {
|
|
43
|
+
server = require(serverFilePath);
|
|
44
|
+
} else {
|
|
45
|
+
// 路径 3: 尝试相对路径
|
|
46
|
+
server = require('./server/index.js');
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('所有路径尝试失败:', err);
|
|
50
|
+
throw new Error('无法加载服务端代码');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const res = await server.handleDatabaseRoutes(apiPath, data);
|
|
55
|
+
// 模拟 HTTP 响应格式
|
|
56
|
+
return {
|
|
57
|
+
status: 200,
|
|
58
|
+
statusText: 'OK',
|
|
59
|
+
data: {
|
|
60
|
+
ret: 0,
|
|
61
|
+
msg: 'success',
|
|
62
|
+
data: res
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('NW.js 环境下执行后端代码失败:', error);
|
|
67
|
+
// 模拟错误响应
|
|
68
|
+
return {
|
|
69
|
+
status: 500,
|
|
70
|
+
statusText: 'Internal Server Error',
|
|
71
|
+
data: {
|
|
72
|
+
ret: 500,
|
|
73
|
+
// @ts-ignore
|
|
74
|
+
msg: error.message || '执行失败'
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
// 在浏览器环境中使用 HTTP 请求
|
|
80
|
+
url = getRequestUrl(url);
|
|
81
|
+
const res = await requestHelper.request(url, data, option);
|
|
82
|
+
return res;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 请求服务
|
|
87
|
+
export async function request<T = any>(url: string, data?: any, option?: AxiosRequestConfig) {
|
|
88
|
+
const res = await requestServer(url, data, option);
|
|
89
|
+
|
|
90
|
+
// 处理服务器直接返回的数据(如数组)
|
|
91
|
+
if (!res || res instanceof Array) {
|
|
92
|
+
return res as T;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 确保res是对象类型
|
|
96
|
+
if (typeof res !== 'object') {
|
|
97
|
+
return res as T;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 检查HTTP状态码
|
|
101
|
+
if(res.status !== 200) {
|
|
102
|
+
throw {
|
|
103
|
+
ret: res.status,
|
|
104
|
+
msg: res.statusText,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 处理新的响应格式 {ret:0,msg:'',data:any}
|
|
109
|
+
const responseData = res.data;
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
// 特殊处理登录态失效的情况
|
|
113
|
+
if(responseData.ret === 50001) {
|
|
114
|
+
console.error('登陆态失效,请重新登陆后再试');
|
|
115
|
+
eventBus.publish(eventBus.AUTHTIMEOUT, { message: responseData.msg || '登陆态失效,请重新登陆后再试' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 返回data字段内容
|
|
119
|
+
return responseData as T;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 请求管理端接口代理
|
|
123
|
+
export async function requestBaseServerApi(api: string, data?: any): Promise<any> {
|
|
124
|
+
return request('/admin/requestBaseServer', {
|
|
125
|
+
api,
|
|
126
|
+
data,
|
|
127
|
+
});
|
|
128
|
+
}
|