sql-assistant 1.0.0__py3-none-any.whl
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.
- sql_assistant/__init__.py +3 -0
- sql_assistant/api/__init__.py +1 -0
- sql_assistant/api/backup.py +116 -0
- sql_assistant/api/config.py +183 -0
- sql_assistant/api/conversation.py +71 -0
- sql_assistant/api/dependencies.py +22 -0
- sql_assistant/api/history.py +61 -0
- sql_assistant/api/models.py +221 -0
- sql_assistant/api/query.py +275 -0
- sql_assistant/api/routes.py +19 -0
- sql_assistant/api/schema.py +21 -0
- sql_assistant/config.py +144 -0
- sql_assistant/database/__init__.py +1 -0
- sql_assistant/database/backup.py +568 -0
- sql_assistant/database/connectors/__init__.py +1 -0
- sql_assistant/database/connectors/base.py +185 -0
- sql_assistant/database/connectors/exceptions.py +88 -0
- sql_assistant/database/connectors/mongodb.py +194 -0
- sql_assistant/database/connectors/mysql.py +110 -0
- sql_assistant/database/connectors/postgresql.py +133 -0
- sql_assistant/database/connectors/redis.py +132 -0
- sql_assistant/database/connectors/sqlserver.py +140 -0
- sql_assistant/database/history.py +290 -0
- sql_assistant/database/manager.py +178 -0
- sql_assistant/database/security.py +230 -0
- sql_assistant/llm/__init__.py +1 -0
- sql_assistant/llm/base.py +28 -0
- sql_assistant/llm/exceptions.py +96 -0
- sql_assistant/llm/manager.py +82 -0
- sql_assistant/llm/prompts.py +29 -0
- sql_assistant/llm/providers/__init__.py +1 -0
- sql_assistant/llm/providers/claude.py +132 -0
- sql_assistant/llm/providers/gemini.py +127 -0
- sql_assistant/llm/providers/openai_compatible.py +103 -0
- sql_assistant/llm/retry.py +88 -0
- sql_assistant/main.py +94 -0
- sql_assistant/settings.py +219 -0
- sql_assistant/web/__init__.py +1 -0
- sql_assistant/web/static/css/base.css +25 -0
- sql_assistant/web/static/css/components/backup.css +146 -0
- sql_assistant/web/static/css/components/chat.css +465 -0
- sql_assistant/web/static/css/components/modal.css +143 -0
- sql_assistant/web/static/css/components/settings.css +358 -0
- sql_assistant/web/static/css/components/sidebar.css +235 -0
- sql_assistant/web/static/css/components/toast.css +30 -0
- sql_assistant/web/static/css/style.css +10 -0
- sql_assistant/web/static/css/theme.css +200 -0
- sql_assistant/web/static/js/api.js +38 -0
- sql_assistant/web/static/js/app.js +161 -0
- sql_assistant/web/static/js/backup.js +216 -0
- sql_assistant/web/static/js/chat.js +238 -0
- sql_assistant/web/static/js/color-theme-manager.js +121 -0
- sql_assistant/web/static/js/confirm.js +95 -0
- sql_assistant/web/static/js/conversations.js +182 -0
- sql_assistant/web/static/js/settings.js +425 -0
- sql_assistant/web/static/js/state.js +43 -0
- sql_assistant/web/static/js/theme-manager.js +64 -0
- sql_assistant/web/static/js/ui.js +53 -0
- sql_assistant/web/templates/index.html +373 -0
- sql_assistant-1.0.0.dist-info/METADATA +24 -0
- sql_assistant-1.0.0.dist-info/RECORD +64 -0
- sql_assistant-1.0.0.dist-info/WHEEL +4 -0
- sql_assistant-1.0.0.dist-info/entry_points.txt +2 -0
- sql_assistant-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/* === Theme Variables === */
|
|
2
|
+
:root,
|
|
3
|
+
[data-theme="dark"] {
|
|
4
|
+
--bg-primary: #0d1117;
|
|
5
|
+
--bg-secondary: #161b22;
|
|
6
|
+
--bg-tertiary: #21262d;
|
|
7
|
+
--bg-hover: #30363d;
|
|
8
|
+
--border-color: #30363d;
|
|
9
|
+
--text-primary: #e6edf3;
|
|
10
|
+
--text-secondary: #8b949e;
|
|
11
|
+
--text-muted: #6e7681;
|
|
12
|
+
--accent: #58a6ff;
|
|
13
|
+
--accent-hover: #79c0ff;
|
|
14
|
+
--success: #3fb950;
|
|
15
|
+
--warning: #d29922;
|
|
16
|
+
--danger: #f85149;
|
|
17
|
+
--font-mono: '宋体', 'SimSun', 'Times New Roman', 'Fira Code', 'Consolas', monospace;
|
|
18
|
+
--font-sans: '宋体', 'SimSun', 'Times New Roman', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
19
|
+
--radius: 8px;
|
|
20
|
+
--radius-sm: 6px;
|
|
21
|
+
--transition: 150ms ease;
|
|
22
|
+
--shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
23
|
+
|
|
24
|
+
--hl-keyword: #ff7b72;
|
|
25
|
+
--hl-string: #a5d6ff;
|
|
26
|
+
--hl-number: #79c0ff;
|
|
27
|
+
--hl-comment: #8b949e;
|
|
28
|
+
--hl-function: #d2a8ff;
|
|
29
|
+
--hl-operator: #ff7b72;
|
|
30
|
+
--hl-built_in: #ffa657;
|
|
31
|
+
--hl-type: #79c0ff;
|
|
32
|
+
--hl-params: #e6edf3;
|
|
33
|
+
--hl-literal: #79c0ff;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
[data-theme="light"] {
|
|
37
|
+
--bg-primary: #ffffff;
|
|
38
|
+
--bg-secondary: #f6f8fa;
|
|
39
|
+
--bg-tertiary: #eaeef2;
|
|
40
|
+
--bg-hover: #d0d7de;
|
|
41
|
+
--border-color: #d0d7de;
|
|
42
|
+
--text-primary: #1f2328;
|
|
43
|
+
--text-secondary: #656d76;
|
|
44
|
+
--text-muted: #8b949e;
|
|
45
|
+
--accent: #0969da;
|
|
46
|
+
--accent-hover: #0550ae;
|
|
47
|
+
--success: #1a7f37;
|
|
48
|
+
--warning: #9a6700;
|
|
49
|
+
--danger: #cf222e;
|
|
50
|
+
--font-mono: '宋体', 'SimSun', 'Times New Roman', 'Fira Code', 'Consolas', monospace;
|
|
51
|
+
--font-sans: '宋体', 'SimSun', 'Times New Roman', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
52
|
+
--shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
53
|
+
|
|
54
|
+
--hl-keyword: #cf222e;
|
|
55
|
+
--hl-string: #0a3069;
|
|
56
|
+
--hl-number: #0550ae;
|
|
57
|
+
--hl-comment: #6e7781;
|
|
58
|
+
--hl-function: #8250df;
|
|
59
|
+
--hl-operator: #cf222e;
|
|
60
|
+
--hl-built_in: #953800;
|
|
61
|
+
--hl-type: #0550ae;
|
|
62
|
+
--hl-params: #1f2328;
|
|
63
|
+
--hl-literal: #0550ae;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* === Header Actions === */
|
|
67
|
+
.header-actions {
|
|
68
|
+
display: flex;
|
|
69
|
+
gap: 8px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* === Theme Section === */
|
|
73
|
+
.theme-section {
|
|
74
|
+
margin-top: 20px;
|
|
75
|
+
padding: 16px;
|
|
76
|
+
background: var(--bg-tertiary);
|
|
77
|
+
border-radius: var(--radius);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.theme-section h4 {
|
|
81
|
+
font-size: 14px;
|
|
82
|
+
margin-bottom: 12px;
|
|
83
|
+
color: var(--text-primary);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.theme-toggle {
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: 12px;
|
|
90
|
+
margin-bottom: 16px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.theme-toggle span {
|
|
94
|
+
font-size: 13px;
|
|
95
|
+
color: var(--text-secondary);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.switch {
|
|
99
|
+
position: relative;
|
|
100
|
+
display: inline-block;
|
|
101
|
+
width: 44px;
|
|
102
|
+
height: 24px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.switch input {
|
|
106
|
+
opacity: 0;
|
|
107
|
+
width: 0;
|
|
108
|
+
height: 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.slider {
|
|
112
|
+
position: absolute;
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
top: 0;
|
|
115
|
+
left: 0;
|
|
116
|
+
right: 0;
|
|
117
|
+
bottom: 0;
|
|
118
|
+
background-color: var(--bg-hover);
|
|
119
|
+
transition: 0.3s;
|
|
120
|
+
border-radius: 24px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.slider:before {
|
|
124
|
+
position: absolute;
|
|
125
|
+
content: "";
|
|
126
|
+
height: 18px;
|
|
127
|
+
width: 18px;
|
|
128
|
+
left: 3px;
|
|
129
|
+
bottom: 3px;
|
|
130
|
+
background-color: white;
|
|
131
|
+
transition: 0.3s;
|
|
132
|
+
border-radius: 50%;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
input:checked + .slider {
|
|
136
|
+
background-color: var(--accent);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
input:checked + .slider:before {
|
|
140
|
+
transform: translateX(20px);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* === Color Picker === */
|
|
144
|
+
.color-picker {
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
gap: 12px;
|
|
148
|
+
flex-wrap: wrap;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.color-picker > span {
|
|
152
|
+
font-size: 13px;
|
|
153
|
+
color: var(--text-secondary);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.color-options {
|
|
157
|
+
display: flex;
|
|
158
|
+
gap: 8px;
|
|
159
|
+
flex-wrap: wrap;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.color-btn {
|
|
163
|
+
width: 28px;
|
|
164
|
+
height: 28px;
|
|
165
|
+
border-radius: 50%;
|
|
166
|
+
border: 2px solid transparent;
|
|
167
|
+
cursor: pointer;
|
|
168
|
+
transition: all var(--transition);
|
|
169
|
+
position: relative;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.color-btn:hover {
|
|
173
|
+
transform: scale(1.1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.color-btn.active {
|
|
177
|
+
border-color: var(--text-primary);
|
|
178
|
+
box-shadow: 0 0 0 2px var(--bg-primary);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.color-btn[data-color="blue"] { background: #58a6ff; }
|
|
182
|
+
.color-btn[data-color="purple"] { background: #a371f7; }
|
|
183
|
+
.color-btn[data-color="pink"] { background: #f778ba; }
|
|
184
|
+
.color-btn[data-color="red"] { background: #f85149; }
|
|
185
|
+
.color-btn[data-color="orange"] { background: #d29922; }
|
|
186
|
+
.color-btn[data-color="yellow"] { background: #d4a72c; }
|
|
187
|
+
.color-btn[data-color="green"] { background: #3fb950; }
|
|
188
|
+
.color-btn[data-color="cyan"] { background: #39c5cf; }
|
|
189
|
+
|
|
190
|
+
.color-btn.active::after {
|
|
191
|
+
content: '✓';
|
|
192
|
+
position: absolute;
|
|
193
|
+
top: 50%;
|
|
194
|
+
left: 50%;
|
|
195
|
+
transform: translate(-50%, -50%);
|
|
196
|
+
color: white;
|
|
197
|
+
font-size: 14px;
|
|
198
|
+
font-weight: bold;
|
|
199
|
+
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
|
|
200
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Helper Module
|
|
3
|
+
*/
|
|
4
|
+
const API = {
|
|
5
|
+
async post(url, data) {
|
|
6
|
+
const res = await fetch(url, {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9
|
+
body: JSON.stringify(data),
|
|
10
|
+
});
|
|
11
|
+
if (!res.ok) {
|
|
12
|
+
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
|
13
|
+
throw new Error(err.detail || '请求失败');
|
|
14
|
+
}
|
|
15
|
+
return res.json();
|
|
16
|
+
},
|
|
17
|
+
async get(url) {
|
|
18
|
+
const res = await fetch(url);
|
|
19
|
+
if (!res.ok) throw new Error('请求失败');
|
|
20
|
+
return res.json();
|
|
21
|
+
},
|
|
22
|
+
async del(url) {
|
|
23
|
+
const res = await fetch(url, { method: 'DELETE' });
|
|
24
|
+
if (!res.ok) throw new Error('请求失败');
|
|
25
|
+
return res.json();
|
|
26
|
+
},
|
|
27
|
+
async put(url, data) {
|
|
28
|
+
const res = await fetch(url, {
|
|
29
|
+
method: 'PUT',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
body: JSON.stringify(data || {}),
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) throw new Error('请求失败');
|
|
34
|
+
return res.json();
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
window.API = API;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL 智能助手 - 主入口文件
|
|
3
|
+
* 负责初始化和事件绑定
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============================================================
|
|
7
|
+
// Event Listeners
|
|
8
|
+
// ============================================================
|
|
9
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
10
|
+
// 初始化主题管理器
|
|
11
|
+
ThemeManager.init();
|
|
12
|
+
ColorThemeManager.init();
|
|
13
|
+
|
|
14
|
+
// 加载流式响应设置
|
|
15
|
+
state.streamingEnabled = false;
|
|
16
|
+
|
|
17
|
+
// 加载设置和对话列表
|
|
18
|
+
loadSettings().then(() => {
|
|
19
|
+
if (state.activeDB) refreshSchema();
|
|
20
|
+
});
|
|
21
|
+
loadConversations();
|
|
22
|
+
|
|
23
|
+
// Query form
|
|
24
|
+
dom.queryForm.addEventListener('submit', (e) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
sendQuery(dom.queryInput.value);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
dom.queryInput.addEventListener('keydown', (e) => {
|
|
30
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
sendQuery(dom.queryInput.value);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Auto-resize textarea
|
|
37
|
+
dom.queryInput.addEventListener('input', () => {
|
|
38
|
+
dom.queryInput.style.height = 'auto';
|
|
39
|
+
dom.queryInput.style.height = Math.min(dom.queryInput.scrollHeight, 120) + 'px';
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Example tags
|
|
43
|
+
document.querySelectorAll('.example-tag').forEach(tag => {
|
|
44
|
+
tag.addEventListener('click', () => {
|
|
45
|
+
const question = tag.dataset.question;
|
|
46
|
+
dom.queryInput.value = question;
|
|
47
|
+
sendQuery(question);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Settings
|
|
52
|
+
$('btn-settings').addEventListener('click', openSettings);
|
|
53
|
+
$('btn-close-modal').addEventListener('click', closeSettings);
|
|
54
|
+
dom.settingsModal.addEventListener('click', (e) => {
|
|
55
|
+
if (e.target === dom.settingsModal) closeSettings();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Tabs
|
|
59
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
60
|
+
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// LLM form
|
|
64
|
+
$('btn-add-llm').addEventListener('click', () => {
|
|
65
|
+
state.editingLLM = null;
|
|
66
|
+
$('llm-form-title').textContent = '添加 LLM 提供商';
|
|
67
|
+
$('llm-api-key').placeholder = 'sk-...';
|
|
68
|
+
$('llm-name').value = '';
|
|
69
|
+
$('llm-provider').value = 'deepseek';
|
|
70
|
+
$('llm-api-key').value = '';
|
|
71
|
+
$('llm-base-url').value = '';
|
|
72
|
+
updateModelSelect('deepseek');
|
|
73
|
+
updateLLMFormByProvider('deepseek');
|
|
74
|
+
dom.llmForm.style.display = 'block';
|
|
75
|
+
dom.llmForm.scrollIntoView({ behavior: 'smooth' });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Provider change - update model list and form fields
|
|
79
|
+
$('llm-provider').addEventListener('change', async (e) => {
|
|
80
|
+
const provider = e.target.value;
|
|
81
|
+
if (!state.llmModels[provider] || state.llmModels[provider].length === 0) {
|
|
82
|
+
await loadLLMModels(provider);
|
|
83
|
+
}
|
|
84
|
+
updateModelSelect(provider);
|
|
85
|
+
updateLLMFormByProvider(provider);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// LLM form events
|
|
89
|
+
$('btn-save-llm').addEventListener('click', saveLLM);
|
|
90
|
+
$('btn-test-llm').addEventListener('click', testNewLLM);
|
|
91
|
+
|
|
92
|
+
// DB form
|
|
93
|
+
$('btn-add-db').addEventListener('click', () => {
|
|
94
|
+
state.editingDB = null;
|
|
95
|
+
$('db-form-title').textContent = '添加数据库连接';
|
|
96
|
+
$('db-password').placeholder = '数据库密码';
|
|
97
|
+
dom.dbForm.style.display = 'block';
|
|
98
|
+
dom.dbForm.scrollIntoView({ behavior: 'smooth' });
|
|
99
|
+
});
|
|
100
|
+
$('btn-save-db').addEventListener('click', saveDB);
|
|
101
|
+
$('btn-cancel-db').addEventListener('click', cancelDBForm);
|
|
102
|
+
$('btn-test-db').addEventListener('click', testNewDB);
|
|
103
|
+
|
|
104
|
+
// New conversation
|
|
105
|
+
$('btn-new-chat').addEventListener('click', createNewConversation);
|
|
106
|
+
|
|
107
|
+
// Refresh schema button
|
|
108
|
+
const refreshBtn = $('btn-refresh-schema');
|
|
109
|
+
if (refreshBtn) {
|
|
110
|
+
refreshBtn.addEventListener('click', refreshSchema);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Backup
|
|
114
|
+
const backupBtn = $('btn-create-backup');
|
|
115
|
+
if (backupBtn) {
|
|
116
|
+
backupBtn.addEventListener('click', createBackup);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const confirmRestoreBtn = $('btn-confirm-restore');
|
|
120
|
+
if (confirmRestoreBtn) {
|
|
121
|
+
confirmRestoreBtn.addEventListener('click', confirmRestore);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Load backup list when backup tab is shown
|
|
125
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
126
|
+
btn.addEventListener('click', () => {
|
|
127
|
+
if (btn.dataset.tab === 'backup') {
|
|
128
|
+
loadBackupList();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// SQL 确认对话框
|
|
134
|
+
$('btn-close-sql-confirm').addEventListener('click', hideSQLConfirmDialog);
|
|
135
|
+
$('btn-cancel-sql').addEventListener('click', hideSQLConfirmDialog);
|
|
136
|
+
$('btn-confirm-sql').addEventListener('click', confirmAndExecuteSQL);
|
|
137
|
+
|
|
138
|
+
$('sql-confirm-modal').addEventListener('click', (e) => {
|
|
139
|
+
if (e.target === $('sql-confirm-modal')) hideSQLConfirmDialog();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
$('btn-copy-confirm-sql').addEventListener('click', () => {
|
|
143
|
+
const code = document.getElementById('sql-confirm-code').textContent;
|
|
144
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
145
|
+
showToast('已复制到剪贴板', 'success');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Keyboard shortcut: Escape to close modal
|
|
150
|
+
document.addEventListener('keydown', (e) => {
|
|
151
|
+
if (e.key === 'Escape' && dom.settingsModal.classList.contains('active')) {
|
|
152
|
+
closeSettings();
|
|
153
|
+
}
|
|
154
|
+
if (e.key === 'Escape' && $('restore-modal').classList.contains('active')) {
|
|
155
|
+
closeRestoreModal();
|
|
156
|
+
}
|
|
157
|
+
if (e.key === 'Escape' && $('sql-confirm-modal').classList.contains('active')) {
|
|
158
|
+
hideSQLConfirmDialog();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup Module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ============================================================
|
|
6
|
+
// Backup Functions
|
|
7
|
+
// ============================================================
|
|
8
|
+
function toggleTableSelection() {
|
|
9
|
+
const scope = $('backup-scope').value;
|
|
10
|
+
const group = $('table-selection-group');
|
|
11
|
+
if (scope === 'selected') {
|
|
12
|
+
group.style.display = 'block';
|
|
13
|
+
loadTablesForBackup();
|
|
14
|
+
} else {
|
|
15
|
+
group.style.display = 'none';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function loadTablesForBackup() {
|
|
20
|
+
try {
|
|
21
|
+
const schema = await API.get('/api/schema');
|
|
22
|
+
const tables = schema.tables || [];
|
|
23
|
+
const select = $('backup-tables');
|
|
24
|
+
select.innerHTML = tables.map(t => `
|
|
25
|
+
<option value="${escapeHtml(t.name)}">${escapeHtml(t.name)}</option>
|
|
26
|
+
`).join('');
|
|
27
|
+
} catch (err) {
|
|
28
|
+
showToast('加载表列表失败: ' + err.message, 'error');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function createBackup() {
|
|
33
|
+
const backupType = $('backup-type').value;
|
|
34
|
+
const scope = $('backup-scope').value;
|
|
35
|
+
const includeSchema = $('backup-include-schema').checked;
|
|
36
|
+
const includeData = $('backup-include-data').checked;
|
|
37
|
+
|
|
38
|
+
let tables = null;
|
|
39
|
+
if (scope === 'selected') {
|
|
40
|
+
const select = $('backup-tables');
|
|
41
|
+
tables = Array.from(select.selectedOptions).map(opt => opt.value);
|
|
42
|
+
if (tables.length === 0) {
|
|
43
|
+
return showToast('请至少选择一个表', 'error');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const btn = $('btn-create-backup');
|
|
48
|
+
const originalText = btn.textContent;
|
|
49
|
+
btn.textContent = '备份中...';
|
|
50
|
+
btn.disabled = true;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
showToast('正在创建备份...', 'info');
|
|
54
|
+
const result = await API.post('/api/backup', {
|
|
55
|
+
backup_type: backupType,
|
|
56
|
+
tables,
|
|
57
|
+
include_schema: includeSchema,
|
|
58
|
+
include_data: includeData
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (result.success) {
|
|
62
|
+
showToast(result.message, 'success');
|
|
63
|
+
await loadBackupList();
|
|
64
|
+
} else {
|
|
65
|
+
showToast(result.message, 'error');
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
showToast('备份失败: ' + err.message, 'error');
|
|
69
|
+
} finally {
|
|
70
|
+
btn.textContent = originalText;
|
|
71
|
+
btn.disabled = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function loadBackupList() {
|
|
76
|
+
try {
|
|
77
|
+
const result = await API.get('/api/backup/list');
|
|
78
|
+
const backups = result.backups || [];
|
|
79
|
+
const list = $('backup-list');
|
|
80
|
+
|
|
81
|
+
if (backups.length === 0) {
|
|
82
|
+
list.innerHTML = '<div class="empty-state">暂无备份记录</div>';
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
list.innerHTML = backups.map(b => `
|
|
87
|
+
<div class="backup-item">
|
|
88
|
+
<div class="backup-item-header">
|
|
89
|
+
<span class="backup-item-id">${escapeHtml(b.backup_id)}</span>
|
|
90
|
+
<span class="backup-item-type ${b.backup_type}">${b.backup_type === 'full' ? '全量备份' : '增量备份'}</span>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="backup-item-info">数据库: ${escapeHtml(b.db_name || '未知')}</div>
|
|
93
|
+
<div class="backup-item-meta">
|
|
94
|
+
<span>表数: ${b.tables.length}</span>
|
|
95
|
+
<span>记录数: ${b.record_count}</span>
|
|
96
|
+
<span>大小: ${formatFileSize(b.file_size)}</span>
|
|
97
|
+
<span>${b.backup_time ? new Date(b.backup_time).toLocaleString('zh-CN') : ''}</span>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="backup-item-actions">
|
|
100
|
+
<button onclick="viewBackupDetail('${escapeHtml(b.backup_id)}')">详情</button>
|
|
101
|
+
<button onclick="openRestoreModal('${escapeHtml(b.backup_id)}')">恢复</button>
|
|
102
|
+
<button class="danger" onclick="deleteBackup('${escapeHtml(b.backup_id)}')">删除</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
`).join('');
|
|
106
|
+
} catch (err) {
|
|
107
|
+
showToast('加载备份列表失败: ' + err.message, 'error');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatFileSize(bytes) {
|
|
112
|
+
if (bytes === 0) return '0 B';
|
|
113
|
+
const k = 1024;
|
|
114
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
115
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
116
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function viewBackupDetail(backupId) {
|
|
120
|
+
try {
|
|
121
|
+
const result = await API.get(`/api/backup/${encodeURIComponent(backupId)}`);
|
|
122
|
+
const info = [
|
|
123
|
+
`备份ID: ${result.backup_id}`,
|
|
124
|
+
`类型: ${result.backup_type === 'full' ? '全量备份' : '增量备份'}`,
|
|
125
|
+
`数据库: ${result.db_name}`,
|
|
126
|
+
`数据库类型: ${result.db_type}`,
|
|
127
|
+
`表数量: ${result.tables.length}`,
|
|
128
|
+
`记录数: ${result.record_count}`,
|
|
129
|
+
`大小: ${formatFileSize(result.file_size)}`,
|
|
130
|
+
`时间: ${result.backup_time ? new Date(result.backup_time).toLocaleString('zh-CN') : ''}`
|
|
131
|
+
];
|
|
132
|
+
alert(info.join('\n'));
|
|
133
|
+
} catch (err) {
|
|
134
|
+
showToast('获取备份详情失败: ' + err.message, 'error');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function deleteBackup(backupId) {
|
|
139
|
+
if (!confirm(`确定删除备份 "${backupId}" 吗?`)) return;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await API.del(`/api/backup/${encodeURIComponent(backupId)}`);
|
|
143
|
+
showToast('备份已删除', 'success');
|
|
144
|
+
await loadBackupList();
|
|
145
|
+
} catch (err) {
|
|
146
|
+
showToast('删除备份失败: ' + err.message, 'error');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let currentRestoreBackupId = null;
|
|
151
|
+
|
|
152
|
+
function openRestoreModal(backupId) {
|
|
153
|
+
currentRestoreBackupId = backupId;
|
|
154
|
+
$('restore-backup-info').innerHTML = `
|
|
155
|
+
<div style="font-weight: 600; margin-bottom: 8px;">备份ID: ${escapeHtml(backupId)}</div>
|
|
156
|
+
<div style="font-size: 12px; color: var(--text-secondary);">
|
|
157
|
+
恢复操作会将备份数据写入当前激活的数据库。
|
|
158
|
+
</div>
|
|
159
|
+
`;
|
|
160
|
+
$('restore-schema').checked = false;
|
|
161
|
+
$('restore-data').checked = true;
|
|
162
|
+
$('restore-warning').style.display = 'block';
|
|
163
|
+
$('restore-modal').classList.add('active');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function closeRestoreModal() {
|
|
167
|
+
$('restore-modal').classList.remove('active');
|
|
168
|
+
currentRestoreBackupId = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function confirmRestore() {
|
|
172
|
+
if (!currentRestoreBackupId) return;
|
|
173
|
+
|
|
174
|
+
const restoreSchema = $('restore-schema').checked;
|
|
175
|
+
const restoreData = $('restore-data').checked;
|
|
176
|
+
|
|
177
|
+
if (!restoreData && !restoreSchema) {
|
|
178
|
+
return showToast('请至少选择一个恢复选项', 'error');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const btn = $('btn-confirm-restore');
|
|
182
|
+
const originalText = btn.textContent;
|
|
183
|
+
btn.textContent = '恢复中...';
|
|
184
|
+
btn.disabled = true;
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
showToast('正在恢复备份...', 'info');
|
|
188
|
+
const result = await API.post('/api/backup/restore', {
|
|
189
|
+
backup_id: currentRestoreBackupId,
|
|
190
|
+
restore_schema: restoreSchema,
|
|
191
|
+
restore_data: restoreData
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (result.success) {
|
|
195
|
+
showToast(result.message, 'success');
|
|
196
|
+
closeRestoreModal();
|
|
197
|
+
} else {
|
|
198
|
+
showToast(result.message, 'error');
|
|
199
|
+
}
|
|
200
|
+
} catch (err) {
|
|
201
|
+
showToast('恢复失败: ' + err.message, 'error');
|
|
202
|
+
} finally {
|
|
203
|
+
btn.textContent = originalText;
|
|
204
|
+
btn.disabled = false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Expose functions
|
|
209
|
+
window.toggleTableSelection = toggleTableSelection;
|
|
210
|
+
window.createBackup = createBackup;
|
|
211
|
+
window.loadBackupList = loadBackupList;
|
|
212
|
+
window.viewBackupDetail = viewBackupDetail;
|
|
213
|
+
window.deleteBackup = deleteBackup;
|
|
214
|
+
window.openRestoreModal = openRestoreModal;
|
|
215
|
+
window.closeRestoreModal = closeRestoreModal;
|
|
216
|
+
window.confirmRestore = confirmRestore;
|