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.
Files changed (64) hide show
  1. sql_assistant/__init__.py +3 -0
  2. sql_assistant/api/__init__.py +1 -0
  3. sql_assistant/api/backup.py +116 -0
  4. sql_assistant/api/config.py +183 -0
  5. sql_assistant/api/conversation.py +71 -0
  6. sql_assistant/api/dependencies.py +22 -0
  7. sql_assistant/api/history.py +61 -0
  8. sql_assistant/api/models.py +221 -0
  9. sql_assistant/api/query.py +275 -0
  10. sql_assistant/api/routes.py +19 -0
  11. sql_assistant/api/schema.py +21 -0
  12. sql_assistant/config.py +144 -0
  13. sql_assistant/database/__init__.py +1 -0
  14. sql_assistant/database/backup.py +568 -0
  15. sql_assistant/database/connectors/__init__.py +1 -0
  16. sql_assistant/database/connectors/base.py +185 -0
  17. sql_assistant/database/connectors/exceptions.py +88 -0
  18. sql_assistant/database/connectors/mongodb.py +194 -0
  19. sql_assistant/database/connectors/mysql.py +110 -0
  20. sql_assistant/database/connectors/postgresql.py +133 -0
  21. sql_assistant/database/connectors/redis.py +132 -0
  22. sql_assistant/database/connectors/sqlserver.py +140 -0
  23. sql_assistant/database/history.py +290 -0
  24. sql_assistant/database/manager.py +178 -0
  25. sql_assistant/database/security.py +230 -0
  26. sql_assistant/llm/__init__.py +1 -0
  27. sql_assistant/llm/base.py +28 -0
  28. sql_assistant/llm/exceptions.py +96 -0
  29. sql_assistant/llm/manager.py +82 -0
  30. sql_assistant/llm/prompts.py +29 -0
  31. sql_assistant/llm/providers/__init__.py +1 -0
  32. sql_assistant/llm/providers/claude.py +132 -0
  33. sql_assistant/llm/providers/gemini.py +127 -0
  34. sql_assistant/llm/providers/openai_compatible.py +103 -0
  35. sql_assistant/llm/retry.py +88 -0
  36. sql_assistant/main.py +94 -0
  37. sql_assistant/settings.py +219 -0
  38. sql_assistant/web/__init__.py +1 -0
  39. sql_assistant/web/static/css/base.css +25 -0
  40. sql_assistant/web/static/css/components/backup.css +146 -0
  41. sql_assistant/web/static/css/components/chat.css +465 -0
  42. sql_assistant/web/static/css/components/modal.css +143 -0
  43. sql_assistant/web/static/css/components/settings.css +358 -0
  44. sql_assistant/web/static/css/components/sidebar.css +235 -0
  45. sql_assistant/web/static/css/components/toast.css +30 -0
  46. sql_assistant/web/static/css/style.css +10 -0
  47. sql_assistant/web/static/css/theme.css +200 -0
  48. sql_assistant/web/static/js/api.js +38 -0
  49. sql_assistant/web/static/js/app.js +161 -0
  50. sql_assistant/web/static/js/backup.js +216 -0
  51. sql_assistant/web/static/js/chat.js +238 -0
  52. sql_assistant/web/static/js/color-theme-manager.js +121 -0
  53. sql_assistant/web/static/js/confirm.js +95 -0
  54. sql_assistant/web/static/js/conversations.js +182 -0
  55. sql_assistant/web/static/js/settings.js +425 -0
  56. sql_assistant/web/static/js/state.js +43 -0
  57. sql_assistant/web/static/js/theme-manager.js +64 -0
  58. sql_assistant/web/static/js/ui.js +53 -0
  59. sql_assistant/web/templates/index.html +373 -0
  60. sql_assistant-1.0.0.dist-info/METADATA +24 -0
  61. sql_assistant-1.0.0.dist-info/RECORD +64 -0
  62. sql_assistant-1.0.0.dist-info/WHEEL +4 -0
  63. sql_assistant-1.0.0.dist-info/entry_points.txt +2 -0
  64. 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;