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,590 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- 统一modal组件 - 添加/编辑连接 -->
|
|
3
|
+
<Modal
|
|
4
|
+
ref="connectionModal"
|
|
5
|
+
:title="editingConnection ? '编辑数据库连接' : '新增数据库连接'"
|
|
6
|
+
:closeButton="{ text: '取消', show: true }"
|
|
7
|
+
:confirmButton="{ text: '', show: false }"
|
|
8
|
+
:isFullScreen="true"
|
|
9
|
+
:style="{ maxWidth: '800px', width: '100%' }"
|
|
10
|
+
@onClose="handleModalClose"
|
|
11
|
+
>
|
|
12
|
+
<form @submit.prevent="saveConnection" class="connection-form-modern">
|
|
13
|
+
<!-- 基本信息 -->
|
|
14
|
+
<div class="form-section">
|
|
15
|
+
<div class="section-header">
|
|
16
|
+
<div class="section-icon">
|
|
17
|
+
<i class="bi bi-info-circle"></i>
|
|
18
|
+
</div>
|
|
19
|
+
<h3 class="section-title">基本信息</h3>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="section-content">
|
|
22
|
+
<div class="form-grid">
|
|
23
|
+
<div class="form-group-modern">
|
|
24
|
+
<label class="form-label-modern">
|
|
25
|
+
<i class="bi bi-tag me-2"></i>连接名称 <span class="required">*</span>
|
|
26
|
+
</label>
|
|
27
|
+
<input type="text" class="form-control-modern" v-model="connectionForm.name"
|
|
28
|
+
placeholder="为连接起一个易记的名称" required>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="form-group-modern">
|
|
31
|
+
<label class="form-label-modern">
|
|
32
|
+
<i class="bi bi-diagram-3 me-2"></i>数据库类型 <span class="required">*</span>
|
|
33
|
+
</label>
|
|
34
|
+
<select class="form-select-modern" v-model="connectionForm.type" @change="onTypeChange" required>
|
|
35
|
+
<option value="">请选择数据库类型</option>
|
|
36
|
+
<option v-for="dbType in databaseTypes" :key="dbType.value" :value="dbType.value">
|
|
37
|
+
{{ dbType.label }}
|
|
38
|
+
</option>
|
|
39
|
+
</select>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- 连接配置 -->
|
|
46
|
+
<div class="form-section" v-if="connectionForm.type !== 'sqlite'">
|
|
47
|
+
<div class="section-header">
|
|
48
|
+
<div class="section-icon">
|
|
49
|
+
<i class="bi bi-hdd-network"></i>
|
|
50
|
+
</div>
|
|
51
|
+
<h3 class="section-title">连接配置</h3>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="section-content">
|
|
54
|
+
<div class="form-grid">
|
|
55
|
+
<div class="form-group-modern">
|
|
56
|
+
<label class="form-label-modern">
|
|
57
|
+
<i class="bi bi-server me-2"></i>主机地址 <span class="required">*</span>
|
|
58
|
+
</label>
|
|
59
|
+
<input type="text" class="form-control-modern" v-model="connectionForm.host"
|
|
60
|
+
placeholder="数据库服务器地址" required>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="form-group-modern">
|
|
63
|
+
<label class="form-label-modern">
|
|
64
|
+
<i class="bi bi-door-closed me-2"></i>端口 <span class="required">*</span>
|
|
65
|
+
</label>
|
|
66
|
+
<input type="number" class="form-control-modern" v-model.number="connectionForm.port"
|
|
67
|
+
placeholder="数据库端口号" min="1" max="65535" required>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="form-group-modern">
|
|
70
|
+
<label class="form-label-modern">
|
|
71
|
+
<i class="bi bi-database me-2"></i>数据库名 <span class="required">*</span>
|
|
72
|
+
</label>
|
|
73
|
+
<input type="text" class="form-control-modern" v-model="connectionForm.database"
|
|
74
|
+
placeholder="要连接的数据库名" required>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="form-group-modern">
|
|
77
|
+
<label class="form-label-modern">
|
|
78
|
+
<i class="bi bi-clock me-2"></i>连接超时
|
|
79
|
+
</label>
|
|
80
|
+
<input type="number" class="form-control-modern" v-model.number="connectionForm.options.timeout"
|
|
81
|
+
placeholder="连接超时时间(秒)" min="1">
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- SQLite配置 -->
|
|
88
|
+
<div class="form-section" v-if="connectionForm.type === 'sqlite'">
|
|
89
|
+
<div class="section-header">
|
|
90
|
+
<div class="section-icon">
|
|
91
|
+
<i class="bi bi-file-earmark-text"></i>
|
|
92
|
+
</div>
|
|
93
|
+
<h3 class="section-title">SQLite配置</h3>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="section-content">
|
|
96
|
+
<div class="form-grid">
|
|
97
|
+
<div class="form-group-modern">
|
|
98
|
+
<label class="form-label-modern">
|
|
99
|
+
<i class="bi bi-file-earmark me-2"></i>数据库文件 <span class="required">*</span>
|
|
100
|
+
</label>
|
|
101
|
+
<input type="text" class="form-control-modern" v-model="connectionForm.database"
|
|
102
|
+
placeholder="SQLite数据库文件路径" required>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<!-- 认证信息 -->
|
|
109
|
+
<div class="form-section">
|
|
110
|
+
<div class="section-header">
|
|
111
|
+
<div class="section-icon">
|
|
112
|
+
<i class="bi bi-shield-lock"></i>
|
|
113
|
+
</div>
|
|
114
|
+
<h3 class="section-title">认证信息</h3>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="section-content">
|
|
117
|
+
<div class="form-grid">
|
|
118
|
+
<div class="form-group-modern">
|
|
119
|
+
<label class="form-label-modern">
|
|
120
|
+
<i class="bi bi-person me-2"></i>用户名
|
|
121
|
+
</label>
|
|
122
|
+
<input type="text" class="form-control-modern" v-model="connectionForm.username"
|
|
123
|
+
placeholder="数据库用户名">
|
|
124
|
+
</div>
|
|
125
|
+
<div class="form-group-modern">
|
|
126
|
+
<label class="form-label-modern">
|
|
127
|
+
<i class="bi bi-key me-2"></i>密码
|
|
128
|
+
</label>
|
|
129
|
+
<input type="password" class="form-control-modern" v-model="connectionForm.password"
|
|
130
|
+
placeholder="数据库密码">
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- 高级选项 -->
|
|
137
|
+
<div class="form-section">
|
|
138
|
+
<div class="section-header">
|
|
139
|
+
<div class="section-icon">
|
|
140
|
+
<i class="bi bi-gear"></i>
|
|
141
|
+
</div>
|
|
142
|
+
<h3 class="section-title">其他选项</h3>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="section-content">
|
|
145
|
+
<div class="form-group-modern">
|
|
146
|
+
<label class="form-label-modern">
|
|
147
|
+
<i class="bi bi-toggle-on me-2"></i>连接状态
|
|
148
|
+
</label>
|
|
149
|
+
<div class="form-check-modern">
|
|
150
|
+
<input class="form-check-input-modern" type="checkbox" v-model="connectionForm.enabled" id="enabled">
|
|
151
|
+
<label class="form-check-label-modern" for="enabled">
|
|
152
|
+
<span class="check-text">启用此连接</span>
|
|
153
|
+
<span class="check-description">创建后将自动启用连接</span>
|
|
154
|
+
</label>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</form>
|
|
160
|
+
|
|
161
|
+
<!-- 自定义footer -->
|
|
162
|
+
<template #footer>
|
|
163
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
164
|
+
<i class="bi bi-x-circle me-1"></i>取消
|
|
165
|
+
</button>
|
|
166
|
+
<button type="button" class="btn btn-outline-primary" @click="testConnection(connectionForm)">
|
|
167
|
+
<i class="bi bi-wifi me-1"></i>测试连接
|
|
168
|
+
</button>
|
|
169
|
+
<button type="button" class="btn btn-primary" @click="saveConnection">
|
|
170
|
+
<i class="bi bi-save me-1"></i>{{ editingConnection ? '更新配置' : '保存配置' }}
|
|
171
|
+
</button>
|
|
172
|
+
<button type="button" class="btn btn-success" @click="saveAndTestConnection">
|
|
173
|
+
<i class="bi bi-check-circle me-1"></i>保存并测试
|
|
174
|
+
</button>
|
|
175
|
+
</template>
|
|
176
|
+
</Modal>
|
|
177
|
+
|
|
178
|
+
<!-- 错误提示模态框 -->
|
|
179
|
+
<Modal
|
|
180
|
+
ref="errorModal"
|
|
181
|
+
title="错误提示"
|
|
182
|
+
:closeButton="{ text: '确定', show: true }"
|
|
183
|
+
:confirmButton="{ text: '', show: false }"
|
|
184
|
+
:isFullScreen="false"
|
|
185
|
+
:style="{ maxWidth: '400px!important', width: '100%' }"
|
|
186
|
+
>
|
|
187
|
+
<div class="error-content">
|
|
188
|
+
<div class="error-icon">
|
|
189
|
+
<i class="bi bi-exclamation-triangle"></i>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="error-message">{{ errorMessage }}</div>
|
|
192
|
+
</div>
|
|
193
|
+
</Modal>
|
|
194
|
+
</template>
|
|
195
|
+
|
|
196
|
+
<script lang="ts" setup>
|
|
197
|
+
import { ref, computed, watch } from 'vue';
|
|
198
|
+
import { ConnectionService } from '@/service/database';
|
|
199
|
+
import type { ConnectionEntity } from '@/typings/database';
|
|
200
|
+
import Modal from '@/components/modal/index.vue';
|
|
201
|
+
import Toast from '@/components/toast/toast.vue';
|
|
202
|
+
|
|
203
|
+
// Props
|
|
204
|
+
interface Props {
|
|
205
|
+
modelValue?: boolean;
|
|
206
|
+
connection?: ConnectionEntity | null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
210
|
+
modelValue: false,
|
|
211
|
+
connection: null
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Emits
|
|
215
|
+
const emit = defineEmits<{
|
|
216
|
+
'update:modelValue': [value: boolean];
|
|
217
|
+
'saved': [connection: ConnectionEntity];
|
|
218
|
+
}>();
|
|
219
|
+
|
|
220
|
+
// 组件实例
|
|
221
|
+
const connectionModal = ref();
|
|
222
|
+
const toastRef = ref();
|
|
223
|
+
const errorModal = ref();
|
|
224
|
+
const errorMessage = ref('');
|
|
225
|
+
|
|
226
|
+
// 响应式数据
|
|
227
|
+
const editingConnection = ref<ConnectionEntity | null>(null);
|
|
228
|
+
const connectionForm = ref<ConnectionEntity>({
|
|
229
|
+
id: '',
|
|
230
|
+
name: '',
|
|
231
|
+
type: '',
|
|
232
|
+
host: 'localhost',
|
|
233
|
+
port: 3306,
|
|
234
|
+
database: '',
|
|
235
|
+
username: '',
|
|
236
|
+
password: '',
|
|
237
|
+
options: {},
|
|
238
|
+
enabled: true,
|
|
239
|
+
createdAt: new Date(),
|
|
240
|
+
updatedAt: new Date()
|
|
241
|
+
});
|
|
242
|
+
const databaseTypes = ref<any[]>([]);
|
|
243
|
+
|
|
244
|
+
// 计算属性
|
|
245
|
+
const enabledConnections = computed(() => connections.value.filter(conn => conn.enabled));
|
|
246
|
+
|
|
247
|
+
// 验证连接配置(前端验证)
|
|
248
|
+
function validateConnection(connection: ConnectionEntity): { isValid: boolean; message: string } {
|
|
249
|
+
if (!connection.name?.trim()) {
|
|
250
|
+
return { isValid: false, message: '连接名称不能为空' };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!connection.type?.trim()) {
|
|
254
|
+
return { isValid: false, message: '数据库类型不能为空' };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (connection.type !== 'sqlite') {
|
|
258
|
+
if (!connection.host?.trim()) {
|
|
259
|
+
return { isValid: false, message: '主机地址不能为空' };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!connection.port || connection.port <= 0 || connection.port > 65535) {
|
|
263
|
+
return { isValid: false, message: '端口号必须在1-65535之间' };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 对于某些数据库类型,数据库名是必需的
|
|
268
|
+
if (['mysql', 'postgres', 'mssql'].includes(connection.type) && !connection.database?.trim()) {
|
|
269
|
+
return { isValid: false, message: `${connection.type.toUpperCase()} 数据库名不能为空` };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// SQLite需要数据库文件路径
|
|
273
|
+
if (connection.type === 'sqlite' && !connection.database?.trim()) {
|
|
274
|
+
return { isValid: false, message: 'SQLite数据库文件路径不能为空' };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { isValid: true, message: '配置验证通过' };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 显示模态框
|
|
281
|
+
function show() {
|
|
282
|
+
connectionModal.value?.show();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 隐藏模态框
|
|
286
|
+
function hide() {
|
|
287
|
+
connectionModal.value?.hide();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 显示添加模态框
|
|
291
|
+
function showAddModal() {
|
|
292
|
+
editingConnection.value = null;
|
|
293
|
+
connectionForm.value = {
|
|
294
|
+
id: '',
|
|
295
|
+
name: '',
|
|
296
|
+
type: '',
|
|
297
|
+
host: 'localhost',
|
|
298
|
+
port: 3306,
|
|
299
|
+
database: '',
|
|
300
|
+
username: '',
|
|
301
|
+
password: '',
|
|
302
|
+
options: {},
|
|
303
|
+
enabled: true,
|
|
304
|
+
createdAt: new Date(),
|
|
305
|
+
updatedAt: new Date()
|
|
306
|
+
};
|
|
307
|
+
show();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 显示编辑模态框
|
|
311
|
+
function showEditModal(connection: ConnectionEntity) {
|
|
312
|
+
editingConnection.value = connection;
|
|
313
|
+
connectionForm.value = { ...connection };
|
|
314
|
+
show();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 保存连接配置(不测试连接)
|
|
318
|
+
async function saveConnection(closeModal = true) {
|
|
319
|
+
try {
|
|
320
|
+
debugger
|
|
321
|
+
// 先进行前端验证
|
|
322
|
+
const validation = validateConnection(connectionForm.value);
|
|
323
|
+
if (!validation.isValid) {
|
|
324
|
+
errorMessage.value = validation.message;
|
|
325
|
+
errorModal.value?.show();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const connectionService = new ConnectionService();
|
|
330
|
+
if (editingConnection.value) {
|
|
331
|
+
await connectionService.updateConnection(editingConnection.value.id!, connectionForm.value);
|
|
332
|
+
} else {
|
|
333
|
+
await connectionService.addConnection(connectionForm.value);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (closeModal) {
|
|
337
|
+
hide();
|
|
338
|
+
}
|
|
339
|
+
emit('saved', connectionForm.value);
|
|
340
|
+
showToast('', editingConnection.value ? '连接配置更新成功' : '连接配置添加成功');
|
|
341
|
+
} catch (error) {
|
|
342
|
+
console.error('保存连接配置失败:', error);
|
|
343
|
+
let errorMsg = '保存配置失败';
|
|
344
|
+
if (error.message) {
|
|
345
|
+
if (error.message.includes('连接') && error.message.includes('失败')) {
|
|
346
|
+
errorMsg = '配置保存失败,请检查服务器状态';
|
|
347
|
+
} else {
|
|
348
|
+
errorMsg = `保存配置失败: ${error.message}`;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
errorMessage.value = errorMsg;
|
|
352
|
+
errorModal.value?.show();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 保存并测试连接
|
|
357
|
+
async function saveAndTestConnection() {
|
|
358
|
+
try {
|
|
359
|
+
// 先保存配置(不关闭模态框)
|
|
360
|
+
await saveConnection(false);
|
|
361
|
+
|
|
362
|
+
// 然后测试连接
|
|
363
|
+
await testConnection(connectionForm.value);
|
|
364
|
+
|
|
365
|
+
// 测试成功后关闭模态框
|
|
366
|
+
hide();
|
|
367
|
+
emit('saved', connectionForm.value);
|
|
368
|
+
showToast('', editingConnection.value ? '连接配置更新并测试成功' : '连接配置添加并测试成功');
|
|
369
|
+
} catch (error) {
|
|
370
|
+
console.error('保存并测试连接失败:', error);
|
|
371
|
+
// 如果是保存失败,错误已经在 saveConnection 中处理了
|
|
372
|
+
// 如果是测试失败,显示警告
|
|
373
|
+
if (error.message && error.message.includes('连接测试失败')) {
|
|
374
|
+
showToast('警告', '配置已保存,但连接测试失败', 'warning');
|
|
375
|
+
hide();
|
|
376
|
+
emit('saved', connectionForm.value);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 测试连接
|
|
382
|
+
async function testConnection(connection: ConnectionEntity) {
|
|
383
|
+
try {
|
|
384
|
+
const connectionService = new ConnectionService();
|
|
385
|
+
const response = await connectionService.testConnection(connection);
|
|
386
|
+
|
|
387
|
+
if (response) {
|
|
388
|
+
showToast('', `"${connection.name}" 连接测试成功`, 'success');
|
|
389
|
+
} else {
|
|
390
|
+
showToast('', `"${connection.name}" 连接测试失败`, 'error');
|
|
391
|
+
}
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.error('测试连接失败:', error);
|
|
394
|
+
showToast('', `"${connection.name}" 连接测试失败: ${error.message || '未知错误'}`, 'error');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 处理模态框关闭
|
|
399
|
+
function handleModalClose() {
|
|
400
|
+
hide();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 数据库类型改变
|
|
404
|
+
function onTypeChange() {
|
|
405
|
+
const selectedType = databaseTypes.value.find(t => t.value === connectionForm.value.type);
|
|
406
|
+
if (selectedType?.defaultPort) {
|
|
407
|
+
connectionForm.value.port = selectedType.defaultPort;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Toast 提示
|
|
412
|
+
function showToast(title: string, message: string, type?: string) {
|
|
413
|
+
toastRef.value?.show(title, message, type);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 加载数据库类型
|
|
417
|
+
async function loadDatabaseTypes() {
|
|
418
|
+
try {
|
|
419
|
+
const connectionService = new ConnectionService();
|
|
420
|
+
const response = await connectionService.getDatabaseTypes();
|
|
421
|
+
databaseTypes.value = response?.data || [];
|
|
422
|
+
} catch (error) {
|
|
423
|
+
console.error('加载数据库类型失败:', error);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 监听 props 变化
|
|
428
|
+
watch(() => props.modelValue, (newValue) => {
|
|
429
|
+
if (newValue) {
|
|
430
|
+
show();
|
|
431
|
+
} else {
|
|
432
|
+
hide();
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
watch(() => props.connection, (newConnection) => {
|
|
437
|
+
if (newConnection) {
|
|
438
|
+
showEditModal(newConnection);
|
|
439
|
+
}
|
|
440
|
+
}, { immediate: true });
|
|
441
|
+
|
|
442
|
+
// 暴露方法给父组件
|
|
443
|
+
defineExpose({
|
|
444
|
+
showAddModal,
|
|
445
|
+
showEditModal,
|
|
446
|
+
show,
|
|
447
|
+
hide
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// 生命周期
|
|
451
|
+
loadDatabaseTypes();
|
|
452
|
+
</script>
|
|
453
|
+
|
|
454
|
+
<style scoped>
|
|
455
|
+
/* 继承原有的样式,这里可以添加组件特定的样式 */
|
|
456
|
+
.connection-form-modern {
|
|
457
|
+
padding: 0;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.form-section {
|
|
461
|
+
margin-bottom: 2rem;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.section-header {
|
|
465
|
+
display: flex;
|
|
466
|
+
align-items: center;
|
|
467
|
+
margin-bottom: 1.5rem;
|
|
468
|
+
padding-bottom: 0.75rem;
|
|
469
|
+
border-bottom: 2px solid #f0f0f0;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.section-icon {
|
|
473
|
+
width: 40px;
|
|
474
|
+
height: 40px;
|
|
475
|
+
border-radius: 8px;
|
|
476
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
477
|
+
display: flex;
|
|
478
|
+
align-items: center;
|
|
479
|
+
justify-content: center;
|
|
480
|
+
color: white;
|
|
481
|
+
margin-right: 1rem;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.section-title {
|
|
485
|
+
font-size: 1.1rem;
|
|
486
|
+
font-weight: 600;
|
|
487
|
+
margin: 0;
|
|
488
|
+
color: #333;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.section-content {
|
|
492
|
+
padding-left: 56px;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.form-grid {
|
|
496
|
+
display: grid;
|
|
497
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
498
|
+
gap: 1.5rem;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.form-group-modern {
|
|
502
|
+
margin-bottom: 1rem;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.form-label-modern {
|
|
506
|
+
display: flex;
|
|
507
|
+
align-items: center;
|
|
508
|
+
font-weight: 500;
|
|
509
|
+
color: #555;
|
|
510
|
+
margin-bottom: 0.5rem;
|
|
511
|
+
font-size: 0.9rem;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.form-control-modern,
|
|
515
|
+
.form-select-modern {
|
|
516
|
+
width: 100%;
|
|
517
|
+
padding: 0.75rem;
|
|
518
|
+
border: 2px solid #e9ecef;
|
|
519
|
+
border-radius: 8px;
|
|
520
|
+
font-size: 0.95rem;
|
|
521
|
+
transition: all 0.3s ease;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.form-control-modern:focus,
|
|
525
|
+
.form-select-modern:focus {
|
|
526
|
+
outline: none;
|
|
527
|
+
border-color: #667eea;
|
|
528
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.form-check-modern {
|
|
532
|
+
display: flex;
|
|
533
|
+
align-items: flex-start;
|
|
534
|
+
padding: 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.form-check-input-modern {
|
|
538
|
+
margin-right: 0.75rem;
|
|
539
|
+
margin-top: 0.25rem;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.form-check-label-modern {
|
|
543
|
+
display: flex;
|
|
544
|
+
flex-direction: column;
|
|
545
|
+
cursor: pointer;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.check-text {
|
|
549
|
+
font-weight: 500;
|
|
550
|
+
color: #333;
|
|
551
|
+
margin-bottom: 0.25rem;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.check-description {
|
|
555
|
+
font-size: 0.85rem;
|
|
556
|
+
color: #666;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.required {
|
|
560
|
+
color: #dc3545;
|
|
561
|
+
margin-left: 0.25rem;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.error-content {
|
|
565
|
+
display: flex;
|
|
566
|
+
align-items: center;
|
|
567
|
+
padding: 1.5rem;
|
|
568
|
+
gap: 1rem;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.error-icon {
|
|
572
|
+
width: 48px;
|
|
573
|
+
height: 48px;
|
|
574
|
+
border-radius: 50%;
|
|
575
|
+
background: linear-gradient(135deg, #dc3545 0%, #ff6b6b 100%);
|
|
576
|
+
display: flex;
|
|
577
|
+
align-items: center;
|
|
578
|
+
justify-content: center;
|
|
579
|
+
color: white;
|
|
580
|
+
font-size: 1.5rem;
|
|
581
|
+
flex-shrink: 0;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.error-message {
|
|
585
|
+
flex: 1;
|
|
586
|
+
color: #333;
|
|
587
|
+
font-size: 0.95rem;
|
|
588
|
+
line-height: 1.6;
|
|
589
|
+
}
|
|
590
|
+
</style>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="datagrid-container">
|
|
3
|
+
<div class="datagrid-inner">
|
|
4
|
+
<table class="table table-light table-striped table-hover">
|
|
5
|
+
<thead class="table-light">
|
|
6
|
+
<tr>
|
|
7
|
+
<th v-for="column in props.columns" :key="column.name" scope="col" class="datagrid-th" :style="column.headerStyle||''" >
|
|
8
|
+
<slot :name="column.name+'_header'" :column="column">
|
|
9
|
+
<span>{{column.text||column.name||''}}</span>
|
|
10
|
+
</slot>
|
|
11
|
+
</th>
|
|
12
|
+
</tr>
|
|
13
|
+
</thead>
|
|
14
|
+
<tbody class="table-group-divider">
|
|
15
|
+
<tr v-for="(row, index) in props.data" :key="index" @click="emits('rowClicked', row, index)" style="cursor: pointer;">
|
|
16
|
+
<td v-for="column in props.columns" :key="column.name">
|
|
17
|
+
<slot :name="column.name||column" :row="row" :column="column">
|
|
18
|
+
<component v-if="column.component" :is="column.component"></component>
|
|
19
|
+
<span v-else>{{ renderDataItem(row, column) }}</span>
|
|
20
|
+
</slot>
|
|
21
|
+
</td>
|
|
22
|
+
</tr>
|
|
23
|
+
</tbody>
|
|
24
|
+
<tfoot>
|
|
25
|
+
<slot name="footer" :columns="props.columns"></slot>
|
|
26
|
+
</tfoot>
|
|
27
|
+
</table>
|
|
28
|
+
<Loading :isLoading="props.isLoading" :message="props.loadingMessage"></Loading>
|
|
29
|
+
</div>
|
|
30
|
+
<!-- 分页组件 -->
|
|
31
|
+
<Pagination v-if="props.showPagination" :currentPage="props.currentPage" :totalPages="props.totalPages" @pageChanged="pageChanged"></Pagination>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup lang="ts">
|
|
36
|
+
import { ref, computed, defineComponent, type Component } from 'vue';
|
|
37
|
+
import Loading from '../loading/index.vue';
|
|
38
|
+
import Pagination from './pagination.vue';
|
|
39
|
+
|
|
40
|
+
type ColumnType = {
|
|
41
|
+
name: string;
|
|
42
|
+
text?: string;
|
|
43
|
+
component?: Component;
|
|
44
|
+
headerStyle?: string;
|
|
45
|
+
style?: string;
|
|
46
|
+
formatter?: (value: any) => string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const props = defineProps({
|
|
50
|
+
isLoading: {
|
|
51
|
+
type: Boolean,
|
|
52
|
+
default: false
|
|
53
|
+
},
|
|
54
|
+
loadingMessage: {
|
|
55
|
+
type: String,
|
|
56
|
+
default: '加载中...'
|
|
57
|
+
},
|
|
58
|
+
// 总页数
|
|
59
|
+
totalPages: {
|
|
60
|
+
type: Number,
|
|
61
|
+
default: 1
|
|
62
|
+
},
|
|
63
|
+
currentPage: {
|
|
64
|
+
type: Number,
|
|
65
|
+
default: 1,
|
|
66
|
+
},
|
|
67
|
+
showPagination: {
|
|
68
|
+
type: Boolean,
|
|
69
|
+
default: true
|
|
70
|
+
},
|
|
71
|
+
data: {
|
|
72
|
+
type: Array<any>,
|
|
73
|
+
default: []
|
|
74
|
+
},
|
|
75
|
+
columns: {
|
|
76
|
+
type: Array<ColumnType>,
|
|
77
|
+
default: []
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const emits = defineEmits(['pageChanged', 'rowClicked']);
|
|
82
|
+
|
|
83
|
+
function renderDataItem(row: any, column: any) {
|
|
84
|
+
if(typeof column === 'string') return row[column];
|
|
85
|
+
if(column.formatter) {
|
|
86
|
+
return column.formatter(row, column);
|
|
87
|
+
}
|
|
88
|
+
return row[column.name];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function pageChanged(page: number) {
|
|
92
|
+
emits('pageChanged', page);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<style scoped>
|
|
98
|
+
.datagrid-inner {
|
|
99
|
+
overflow: auto;
|
|
100
|
+
margin-bottom: 10px;
|
|
101
|
+
}
|
|
102
|
+
.datagrid-th {
|
|
103
|
+
min-width: 80px;
|
|
104
|
+
}
|
|
105
|
+
</style>
|