collabdocchat 2.4.4 → 2.4.6

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 (82) hide show
  1. package/bin/cli.js +1 -1
  2. package/package.json +4 -2
  3. package/scripts/cleanup-scripts.js +140 -0
  4. package/scripts/fix-startup-issues.js +136 -0
  5. package/scripts/start-app.js +11 -5
  6. package/scripts/start-simple.js +96 -0
  7. package/server/index.js +4 -0
  8. package/server/index.js.bak +97 -0
  9. package/server/models/Document.js +5 -0
  10. package/server/models/KnowledgeBase.js +259 -254
  11. package/server/models/Poll.js +97 -0
  12. package/server/routes/ai.js +391 -327
  13. package/server/routes/audit.js +61 -0
  14. package/server/routes/documents.js +74 -5
  15. package/server/routes/export.js +171 -10
  16. package/server/routes/files.js +27 -4
  17. package/server/routes/knowledge.js +31 -22
  18. package/server/routes/messages.js +142 -0
  19. package/server/routes/polls.js +241 -0
  20. package/server/routes/tasks.js +1 -0
  21. package/server/routes/workflows.js +27 -0
  22. package/server/utils/auditLogger.js +268 -238
  23. package/src/pages/admin-dashboard.js +1431 -335
  24. package/src/pages/admin-dashboard.js.audit-optimize.bak +4134 -0
  25. package/src/pages/admin-dashboard.js.bak +4041 -0
  26. package/src/pages/admin-dashboard.js.broken.bak +4099 -0
  27. package/src/pages/admin-dashboard.js.comprehensive.bak +4099 -0
  28. package/src/pages/admin-dashboard.js.escape.bak +4099 -0
  29. package/src/pages/admin-dashboard.js.final-final-fix.bak +4099 -0
  30. package/src/pages/admin-dashboard.js.final-fix.bak +4099 -0
  31. package/src/pages/admin-dashboard.js.final.bak +4099 -0
  32. package/src/pages/admin-dashboard.js.indent-fix.bak +4099 -0
  33. package/src/pages/admin-dashboard.js.last-fix.bak +4099 -0
  34. package/src/pages/admin-dashboard.js.line595-fix.bak +4099 -0
  35. package/src/pages/admin-dashboard.js.pre-manual-fix.bak +4099 -0
  36. package/src/pages/admin-dashboard.js.syntax.bak +4099 -0
  37. package/src/pages/admin-dashboard.js.test.bak +4099 -0
  38. package/src/pages/optimized-task-detail-original.js +838 -0
  39. package/src/pages/optimized-task-detail.js +324 -22
  40. package/src/pages/optimized-task-detail.js.bak +1162 -0
  41. package/src/pages/poll-detail-enhanced.js +394 -0
  42. package/src/pages/update-poll-display.js +380 -0
  43. package/src/pages/user-dashboard.js +1860 -1006
  44. package/src/services/api.js +326 -265
  45. package/src/services/auth.js +54 -54
  46. package/src/services/websocket.js +88 -80
  47. package/scripts/add-button-hover.js +0 -56
  48. package/scripts/add-missing-functions.js +0 -66
  49. package/scripts/add-more-features.js +0 -427
  50. package/scripts/add-user-functions.js +0 -201
  51. package/scripts/auto-publish.js +0 -63
  52. package/scripts/beautify-buttons.js +0 -45
  53. package/scripts/beautify-ui.js +0 -267
  54. package/scripts/check-encoding.js +0 -41
  55. package/scripts/check-syntax.js +0 -54
  56. package/scripts/find-buttons.js +0 -20
  57. package/scripts/find-duplicate.js +0 -35
  58. package/scripts/find-sidebar-buttons.js +0 -21
  59. package/scripts/fix-help.js +0 -274
  60. package/scripts/fix-issues-step1.js +0 -73
  61. package/scripts/fix-issues-step2.js +0 -93
  62. package/scripts/fix-issues-step3.js +0 -155
  63. package/scripts/fix-issues-step4.js +0 -150
  64. package/scripts/fix-optimized-views.js +0 -37
  65. package/scripts/fix-ports.js +0 -77
  66. package/scripts/fix-settings.js +0 -258
  67. package/scripts/fix-user-dashboard.js +0 -62
  68. package/scripts/fix-workflow.js +0 -110
  69. package/scripts/refactor-step1.js +0 -32
  70. package/scripts/refactor-step2.js +0 -255
  71. package/scripts/refactor-step3.js +0 -137
  72. package/scripts/refactor-step4.js +0 -183
  73. package/scripts/refactor-step5.js +0 -181
  74. package/scripts/refactor-step6.js +0 -254
  75. package/scripts/refactor-step7.js +0 -291
  76. package/scripts/remove-bom.js +0 -69
  77. package/scripts/remove-quill-from-user-dashboard.js +0 -49
  78. package/scripts/remove-quill-imports-only.js +0 -32
  79. package/scripts/update-port-user.js +0 -21
  80. package/scripts/update-port.js +0 -22
  81. package/src/pages/simplified-workflows.js +0 -652
  82. package/src/utils/ai-assistant.js +0 -1384
@@ -0,0 +1,4099 @@
1
+ import { ApiService } from '../services/api.js';
2
+ import { AuthService } from '../services/auth.js';
3
+ import 'emoji-picker-element';
4
+
5
+ export function renderAdminDashboard(user, wsService) {
6
+ const app = document.getElementById('app');
7
+ const apiService = new ApiService();
8
+ const authService = new AuthService();
9
+ const currentUserId = user.id || user._id;
10
+
11
+ let currentGroup = null;
12
+ let groups = [];
13
+
14
+ // 初始化主题
15
+ const savedTheme = localStorage.getItem('currentTheme') || 'dark';
16
+ applyThemeOnLoad(savedTheme);
17
+
18
+ app.innerHTML = `
19
+ <div class="dashboard">
20
+ <aside class="sidebar">
21
+ <div class="sidebar-header">
22
+ <h2>CollabDocChat</h2>
23
+ <span class="badge-admin">管理员</span>
24
+ </div>
25
+
26
+ <div class="user-info">
27
+ <div class="avatar">${user.username[0].toUpperCase()}</div>
28
+ <div>
29
+ <div class="username">${user.username}</div>
30
+ <div class="user-role">管理员</div>
31
+ </div>
32
+ </div>
33
+
34
+ <nav class="nav-menu">
35
+ <button class="nav-item active" data-view="groups">
36
+ <span class="icon">👥</span> 群组管理
37
+ </button>
38
+ <button class="nav-item" data-view="tasks">
39
+ <span class="icon">📋</span> 任务管理
40
+ </button>
41
+ <button class="nav-item" data-view="documents">
42
+ <span class="icon">📄</span> 共享文档
43
+ </button>
44
+ <button class="nav-item" data-view="files">
45
+ <span class="icon">📎</span> 文件管理
46
+ </button>
47
+ <button class="nav-item" data-view="chat">
48
+ <span class="icon">💬</span> 群聊
49
+ </button>
50
+ <button class="nav-item" data-view="search">
51
+ <span class="icon">🔍</span> 搜索
52
+ </button>
53
+ <button class="nav-item" data-view="call">
54
+ <span class="icon">🎲</span> 随机点名
55
+ </button>
56
+ <button class="nav-item" data-view="audit">
57
+ <span class="icon">📊</span> 操作记录
58
+ </button>
59
+ <button class="nav-item" data-view="knowledge">
60
+ <span class="icon">📚</span> 知识库
61
+ </button>
62
+ <button class="nav-item" data-view="workflow">
63
+ <span class="icon">⚙️</span> 工作流
64
+ </button>
65
+ <button class="nav-item" data-view="backup">
66
+ <span class="icon">💾</span> 备份管理
67
+ </button>
68
+ <button class="nav-item" data-view="export">
69
+ <span class="icon">📤</span> 数据导出
70
+ </button>
71
+ </nav>
72
+
73
+ <div class="sidebar-footer" style="display: flex; gap: 8px; padding: 12px 16px;">
74
+ <button class="sidebar-footer-btn" id="settingsBtn" style="flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px 16px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 10px; color: var(--text-primary); font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.3s;" onmouseover="this.style.background='linear-gradient(135deg, rgba(99, 102, 241, 0.2) 0%, rgba(168, 85, 247, 0.2) 100%)'; this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(99, 102, 241, 0.2)'" onmouseout="this.style.background='linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%)'; this.style.transform='translateY(0)'; this.style.boxShadow='none'">
75
+ <span style="font-size: 16px;">⚙️</span>
76
+ <span>设置</span>
77
+ </button>
78
+ <button class="sidebar-footer-btn" id="helpBtn" style="flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px 16px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 10px; color: var(--text-primary); font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.3s;" onmouseover="this.style.background='linear-gradient(135deg, rgba(99, 102, 241, 0.2) 0%, rgba(168, 85, 247, 0.2) 100%)'; this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(99, 102, 241, 0.2)'" onmouseout="this.style.background='linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%)'; this.style.transform='translateY(0)'; this.style.boxShadow='none'">
79
+ <span style="font-size: 16px;">❓</span>
80
+ <span>帮助</span>
81
+ </button>
82
+ </div>
83
+ <button class="btn-logout" id="logoutBtn">退出登录</button>
84
+ </aside>
85
+
86
+ <main class="main-content">
87
+ <div id="contentArea"></div>
88
+ </main>
89
+ </div>
90
+ `;
91
+
92
+ // 导航切换
93
+ document.querySelectorAll('.nav-item').forEach(item => {
94
+ item.addEventListener('click', () => {
95
+ document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
96
+ item.classList.add('active');
97
+ const view = item.dataset.view;
98
+ renderView(view);
99
+ });
100
+ });
101
+
102
+ // 退出登录
103
+ document.getElementById('logoutBtn').addEventListener('click', () => {
104
+ authService.logout();
105
+ });
106
+
107
+ // 设置按钮
108
+ document.getElementById('settingsBtn').addEventListener('click', () => {
109
+ renderView('settings');
110
+ });
111
+
112
+ // 帮助按钮
113
+ document.getElementById('helpBtn').addEventListener('click', () => {
114
+ renderView('help');
115
+ });
116
+
117
+ async function renderGroupsView(container) {
118
+ const result = await apiService.getGroups();
119
+ groups = result.groups;
120
+
121
+ container.innerHTML = `
122
+ <div class="view-header">
123
+ <h2>群组管理</h2>
124
+ <button class="btn-primary" id="createGroupBtn">创建群组</button>
125
+ </div>
126
+ <div class="groups-grid" id="groupsList"></div>
127
+ <div id="createGroupModal" class="modal hidden">
128
+ <div class="modal-content">
129
+ <h3>创建新群组</h3>
130
+ <form id="createGroupForm">
131
+ <div class="form-group">
132
+ <label>群组名称</label>
133
+ <input type="text" name="name" placeholder="请输入群组名称" required>
134
+ </div>
135
+ <div class="form-group">
136
+ <label>群组描述</label>
137
+ <textarea name="description" placeholder="请输入群组描述(可选)"></textarea>
138
+ </div>
139
+ <div class="form-group">
140
+ <label>添加成员(可选)</label>
141
+ <div id="usersList" style="max-height: 200px; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 10px;">
142
+ <p>加载中...</p>
143
+ </div>
144
+ </div>
145
+ <div style="display: flex; gap: 10px;">
146
+ <button type="submit" class="btn-primary">创建</button>
147
+ <button type="button" class="btn-secondary" id="closeModal">取消</button>
148
+ </div>
149
+ </form>
150
+ </div>
151
+ </div>
152
+ <div id="manageMembersModal" class="modal hidden">
153
+ <div class="modal-content">
154
+ <h3>管理成员</h3>
155
+ <div id="currentMembers"></div>
156
+ <div class="form-group">
157
+ <label>添加新成员</label>
158
+ <div id="availableUsers"></div>
159
+ </div>
160
+ <button type="button" class="btn-secondary" id="closeMembersModal">关闭</button>
161
+ </div>
162
+ </div>
163
+ `;
164
+
165
+ const groupsList = document.getElementById('groupsList');
166
+ groups.forEach(group => {
167
+ const groupCard = document.createElement('div');
168
+ groupCard.className = 'group-card';
169
+ groupCard.innerHTML = `
170
+ <h3>${group.name}</h3>
171
+ <p>${group.description || '暂无描述'}</p>
172
+ <div class="group-stats">
173
+ <span>👥 ${group.members.length} 成员</span>
174
+ <span>📄 ${group.documents.length} 文档</span>
175
+ </div>
176
+ <div style="display: flex; gap: 10px; margin-top: 10px;">
177
+ <button class="btn-select" data-id="${group._id}">选择</button>
178
+ <button class="btn-secondary" data-id="${group._id}" data-action="manage">管理成员</button>
179
+ </div>
180
+ `;
181
+ groupsList.appendChild(groupCard);
182
+ });
183
+
184
+ document.querySelectorAll('.btn-select').forEach(btn => {
185
+ btn.addEventListener('click', () => {
186
+ currentGroup = groups.find(g => g._id === btn.dataset.id);
187
+ wsService.joinGroup(currentGroup._id);
188
+ alert(`已加入群组: ${currentGroup.name}`);
189
+ });
190
+ });
191
+
192
+ document.querySelectorAll('[data-action="manage"]').forEach(btn => {
193
+ btn.addEventListener('click', async () => {
194
+ const groupId = btn.dataset.id;
195
+ await showManageMembersModal(groupId);
196
+ });
197
+ });
198
+
199
+ document.getElementById('createGroupBtn').addEventListener('click', async () => {
200
+ document.getElementById('createGroupModal').classList.remove('hidden');
201
+ await loadUsers();
202
+ });
203
+
204
+ document.getElementById('closeModal').addEventListener('click', () => {
205
+ document.getElementById('createGroupModal').classList.add('hidden');
206
+ });
207
+
208
+ document.getElementById('closeMembersModal').addEventListener('click', () => {
209
+ document.getElementById('manageMembersModal').classList.add('hidden');
210
+ });
211
+
212
+ document.getElementById('createGroupForm').addEventListener('submit', async (e) => {
213
+ e.preventDefault();
214
+ const formData = new FormData(e.target);
215
+ const selectedUsers = Array.from(document.querySelectorAll('#usersList input:checked')).map(cb => cb.value);
216
+
217
+ try {
218
+ const result = await apiService.createGroup(
219
+ formData.get('name'),
220
+ formData.get('description'),
221
+ selectedUsers
222
+ );
223
+ alert('群组创建成功!');
224
+ await renderGroupsView(container);
225
+ document.getElementById('createGroupModal').classList.add('hidden');
226
+ } catch (error) {
227
+ console.error('创建群组错误:', error);
228
+ alert('创建失败: ' + error.message);
229
+ }
230
+ });
231
+ }
232
+
233
+ async function loadUsers() {
234
+ try {
235
+ const result = await apiService.getAllUsers();
236
+ const usersList = document.getElementById('usersList');
237
+ usersList.innerHTML = result.users.map(u => `
238
+ <label style="display: flex; align-items: center; gap: 10px; padding: 8px; cursor: pointer;">
239
+ <input type="checkbox" value="${u._id}">
240
+ <div class="avatar" style="width: 30px; height: 30px; font-size: 14px;">${u.username[0].toUpperCase()}</div>
241
+ <span>${u.username} (${u.role === 'admin' ? '管理员' : '用户'})</span>
242
+ </label>
243
+ `).join('');
244
+ } catch (error) {
245
+ console.error('加载用户失败:', error);
246
+ document.getElementById('usersList').innerHTML = '<p style="color: var(--danger);">加载失败</p>';
247
+ }
248
+ }
249
+
250
+ async function showManageMembersModal(groupId) {
251
+ try {
252
+ const groupResult = await apiService.getGroup(groupId);
253
+ const usersResult = await apiService.getAllUsers();
254
+ const group = groupResult.group;
255
+
256
+ const currentMembers = document.getElementById('currentMembers');
257
+ currentMembers.innerHTML = `
258
+ <h4>当前成员 (${group.members.length})</h4>
259
+ <div style="max-height: 200px; overflow-y: auto;">
260
+ ${group.members.map(member => `
261
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 8px; border-bottom: 1px solid var(--border);">
262
+ <div style="display: flex; align-items: center; gap: 10px;">
263
+ <div class="avatar" style="width: 30px; height: 30px; font-size: 14px;">${member.username[0].toUpperCase()}</div>
264
+ <span>${member.username} ${member._id.toString() === group.admin._id.toString() ? '(管理员)' : ''}</span>
265
+ </div>
266
+ ${member._id.toString() !== group.admin._id.toString() ?
267
+ `<button class="btn-secondary btn-sm" onclick="removeMember('${groupId}', '${member._id}')">移除</button>` :
268
+ ''}
269
+ </div>
270
+ `).join('')}
271
+ </div>
272
+ `;
273
+
274
+ const memberIds = group.members.map(m => m._id.toString());
275
+ const availableUsers = usersResult.users.filter(u => !memberIds.includes(u._id));
276
+
277
+ const availableUsersDiv = document.getElementById('availableUsers');
278
+ if (availableUsers.length === 0) {
279
+ availableUsersDiv.innerHTML = '<p>所有用户都已在群组中</p>';
280
+ } else {
281
+ availableUsersDiv.innerHTML = availableUsers.map(u => `
282
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 8px; border-bottom: 1px solid var(--border);">
283
+ <div style="display: flex; align-items: center; gap: 10px;">
284
+ <div class="avatar" style="width: 30px; height: 30px; font-size: 14px;">${u.username[0].toUpperCase()}</div>
285
+ <span>${u.username}</span>
286
+ </div>
287
+ <button class="btn-primary btn-sm" onclick="addMember('${groupId}', '${u._id}')">添加</button>
288
+ </div>
289
+ `).join('');
290
+ }
291
+
292
+ document.getElementById('manageMembersModal').classList.remove('hidden');
293
+ } catch (error) {
294
+ console.error('加载成员失败:', error);
295
+ alert('加载失败: ' + error.message);
296
+ }
297
+ }
298
+
299
+ // 全局函数供按钮调用
300
+ window.addMember = async (groupId, userId) => {
301
+ try {
302
+ await apiService.addMember(groupId, userId);
303
+ alert('成员添加成功!');
304
+ await showManageMembersModal(groupId);
305
+ } catch (error) {
306
+ alert('添加失败: ' + error.message);
307
+ }
308
+ };
309
+
310
+ window.removeMember = async (groupId, userId) => {
311
+ if (confirm('确定要移除该成员吗?')) {
312
+ try {
313
+ await apiService.removeMember(groupId, userId);
314
+ alert('成员移除成功!');
315
+ await showManageMembersModal(groupId);
316
+ } catch (error) {
317
+ alert('移除失败: ' + error.message);
318
+ }
319
+ }
320
+ };
321
+
322
+ async function renderTasksView(container) {
323
+ if (!currentGroup) {
324
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
325
+ return;
326
+ }
327
+
328
+ const result = await apiService.getTasks(currentGroup._id);
329
+
330
+ container.innerHTML = `
331
+ <div class="view-header">
332
+ <h2>任务管理 - ${currentGroup.name}</h2>
333
+ <button class="btn-primary" id="createTaskBtn">创建任务</button>
334
+ </div>
335
+ <div class="tasks-list" id="tasksList"></div>
336
+ <div id="createTaskModal" class="modal hidden">
337
+ <div class="modal-content">
338
+ <h3>创建新任务</h3>
339
+ <form id="createTaskForm">
340
+ <div class="form-group">
341
+ <label>任务标题</label>
342
+ <input type="text" name="title" placeholder="请输入任务标题" required>
343
+ </div>
344
+ <div class="form-group">
345
+ <label>任务描述</label>
346
+ <textarea name="description" placeholder="请输入任务描述"></textarea>
347
+ </div>
348
+ <div class="form-group">
349
+ <label>截止日期</label>
350
+ <input type="date" name="deadline">
351
+ </div>
352
+ <div style="display: flex; gap: 10px;">
353
+ <button type="submit" class="btn-primary">创建</button>
354
+ <button type="button" class="btn-secondary" id="closeTaskModal">取消</button>
355
+ </div>
356
+ </form>
357
+ </div>
358
+ </div>
359
+
360
+ <!-- 任务详情模态框 -->
361
+ <div id="taskDetailModal" class="modal hidden">
362
+ <div class="modal-content" style="max-width: 800px;">
363
+ <div class="modal-header">
364
+ <h3>📋 任务详情</h3>
365
+ <button class="modal-close" id="closeTaskDetailModal">&times;</button>
366
+ </div>
367
+ <div class="modal-body" id="taskDetailContent" style="padding: 20px;">
368
+ <!-- 任务详情内容 -->
369
+ </div>
370
+ </div>
371
+ </div>
372
+ `;
373
+
374
+ const tasksList = document.getElementById('tasksList');
375
+ if (result.tasks.length === 0) {
376
+ tasksList.innerHTML = '<div class="empty-state">暂无任务</div>';
377
+ } else {
378
+ result.tasks.forEach(task => {
379
+ const taskCard = document.createElement('div');
380
+ taskCard.className = `task-card status-${task.status}`;
381
+ taskCard.innerHTML = `
382
+ <div style="display: flex; justify-content: space-between; align-items: start;">
383
+ <div style="flex: 1;">
384
+ <h3>${task.title}</h3>
385
+ <p>${task.description || '无描述'}</p>
386
+ <div class="task-meta">
387
+ <span class="status-badge">${getStatusText(task.status)}</span>
388
+ <span>截止: ${task.deadline ? new Date(task.deadline).toLocaleDateString() : '无'}</span>
389
+ </div>
390
+ </div>
391
+ <div style="display: flex; gap: 10px;">
392
+ <button class="btn-primary btn-sm" data-id="${task._id}" data-action="view-task" title="查看详细">📋 查看详细</button>
393
+ <button class="btn-danger btn-sm" data-id="${task._id}" data-action="delete-task" title="删除任务" style="min-width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;">🗑️ 删除</button>
394
+ </div>
395
+ `;
396
+ tasksList.appendChild(taskCard);
397
+ });
398
+
399
+ // 添加删除任务事件
400
+ document.querySelectorAll('[data-action="delete-task"]').forEach(btn => {
401
+ btn.addEventListener('click', async (e) => {
402
+ e.stopPropagation();
403
+ const taskId = btn.dataset.id;
404
+ if (confirm('确定要删除这个任务吗?删除后无法恢复!')) {
405
+ try {
406
+ await apiService.deleteTask(taskId);
407
+ alert('任务删除成功!');
408
+ await renderTasksView(container);
409
+ } catch (error) {
410
+ console.error('删除任务错误:', error);
411
+ alert('删除失败: ' + (error.message || '未知错误'));
412
+ }
413
+ }
414
+ });
415
+ });
416
+
417
+ // 添加查看详细事件
418
+ document.querySelectorAll('[data-action="view-task"]').forEach(btn => {
419
+ btn.addEventListener('click', async (e) => {
420
+ e.stopPropagation();
421
+ const taskId = btn.dataset.id;
422
+ const task = result.tasks.find(t => t._id === taskId);
423
+
424
+ if (task) {
425
+ const detailContent = document.getElementById('taskDetailContent');
426
+
427
+ // 获取状态颜色
428
+ const getStatusColor = (status) => {
429
+ const colors = {
430
+ 'pending': '#6366f1',
431
+ 'in-progress': '#f59e0b',
432
+ 'completed': '#10b981',
433
+ 'cancelled': '#ef4444'
434
+ };
435
+ return colors[status] || '#6366f1';
436
+ };
437
+
438
+ detailContent.innerHTML = `
439
+ <div class="task-detail" style="background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(168, 85, 247, 0.05) 100%); border-radius: 16px; padding: 24px;">
440
+
441
+ <!-- 任务标题 -->
442
+ <div class="detail-section" style="background: var(--bg-secondary); border-radius: 12px; padding: 20px; margin-bottom: 16px; border-left: 4px solid ${getStatusColor(task.status)};">
443
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px;">
444
+ <span style="font-size: 24px;">📌</span>
445
+ <h4 style="margin: 0; color: var(--text-secondary); font-size: 14px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">任务标题</h4>
446
+ </div>
447
+ <p style="font-size: 22px; font-weight: 700; margin: 0; color: var(--text-primary); line-height: 1.4;">${task.title}</p>
448
+ </div>
449
+
450
+ <!-- 任务描述 -->
451
+ <div class="detail-section" style="background: var(--bg-secondary); border-radius: 12px; padding: 20px; margin-bottom: 16px;">
452
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
453
+ <span style="font-size: 24px;">📝</span>
454
+ <h4 style="margin: 0; color: var(--text-secondary); font-size: 14px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">任务描述</h4>
455
+ </div>
456
+ <p style="margin: 0; line-height: 1.8; color: var(--text-primary); font-size: 15px; white-space: pre-wrap;">${task.description || '<span style="color: var(--text-secondary); font-style: italic;">暂无描述</span>'}</p>
457
+ </div>
458
+
459
+ <!-- 状态和日期网格 -->
460
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 16px;">
461
+
462
+ <!-- 任务状态 -->
463
+ <div class="detail-section" style="background: var(--bg-secondary); border-radius: 12px; padding: 20px;">
464
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
465
+ <span style="font-size: 24px;">📊</span>
466
+ <h4 style="margin: 0; color: var(--text-secondary); font-size: 14px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">任务状态</h4>
467
+ </div>
468
+ <span style="display: inline-flex; align-items: center; gap: 8px; padding: 10px 16px; border-radius: 8px; background: ${getStatusColor(task.status)}; color: white; font-weight: 600; font-size: 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.15);">
469
+ ${task.status === 'completed' ? '✓' : task.status === 'in-progress' ? '⟳' : task.status === 'cancelled' ? '✕' : '○'}
470
+ ${getStatusText(task.status)}
471
+ </span>
472
+ </div>
473
+
474
+ <!-- 截止日期 -->
475
+ <div class="detail-section" style="background: var(--bg-secondary); border-radius: 12px; padding: 20px;">
476
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
477
+ <span style="font-size: 24px;">📅</span>
478
+ <h4 style="margin: 0; color: var(--text-secondary); font-size: 14px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">截止日期</h4>
479
+ </div>
480
+ <p style="margin: 0; font-size: 15px; font-weight: 600; color: var(--text-primary);">
481
+ ${task.deadline ? new Date(task.deadline).toLocaleString('zh-CN', {
482
+ year: 'numeric',
483
+ month: 'long',
484
+ day: 'numeric',
485
+ hour: '2-digit',
486
+ minute: '2-digit'
487
+ }) : '<span style="color: var(--text-secondary); font-style: italic;">无截止日期</span>'}
488
+ </p>
489
+ </div>
490
+
491
+ </div>
492
+
493
+ <!-- 分配成员 -->
494
+ <div class="detail-section" style="background: var(--bg-secondary); border-radius: 12px; padding: 20px; margin-bottom: 16px;">
495
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
496
+ <span style="font-size: 24px;">👥</span>
497
+ <h4 style="margin: 0; color: var(--text-secondary); font-size: 14px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">分配给</h4>
498
+ ${task.assignedTo && task.assignedTo.length > 0 ? `<span style="background: var(--primary); color: white; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 600;">${task.assignedTo.length} 人</span>` : ''}
499
+ </div>
500
+ <div style="display: flex; flex-wrap: wrap; gap: 12px;">
501
+ ${task.assignedTo && task.assignedTo.length > 0 ?
502
+ task.assignedTo.map(user => `
503
+ <div style="display: flex; align-items: center; gap: 10px; padding: 10px 16px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border-radius: 10px; border: 1px solid rgba(99, 102, 241, 0.2); transition: all 0.3s ease;">
504
+ <div class="avatar" style="width: 36px; height: 36px; font-size: 16px; background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%); box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);">${user.username?.[0]?.toUpperCase() || '?'}</div>
505
+ <span style="font-weight: 600; color: var(--text-primary);">${user.username || '未知用户'}</span>
506
+ </div>
507
+ `).join('') :
508
+ '<p style="margin: 0; color: var(--text-secondary); font-style: italic;">未分配给任何人</p>'
509
+ }
510
+ </div>
511
+ </div>
512
+
513
+ <!-- 时间信息 -->
514
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
515
+
516
+ <!-- 创建时间 -->
517
+ <div class="detail-section" style="background: var(--bg-secondary); border-radius: 12px; padding: 20px;">
518
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
519
+ <span style="font-size: 24px;">🕐</span>
520
+ <h4 style="margin: 0; color: var(--text-secondary); font-size: 14px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">创建时间</h4>
521
+ </div>
522
+ <p style="margin: 0; font-size: 14px; color: var(--text-primary);">
523
+ ${new Date(task.createdAt).toLocaleString('zh-CN', {
524
+ year: 'numeric',
525
+ month: 'long',
526
+ day: 'numeric',
527
+ hour: '2-digit',
528
+ minute: '2-digit',
529
+ second: '2-digit'
530
+ })}
531
+ </p>
532
+ </div>
533
+
534
+ <!-- 更新时间 -->
535
+ <div class="detail-section" style="background: var(--bg-secondary); border-radius: 12px; padding: 20px;">
536
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
537
+ <span style="font-size: 24px;">🔄</span>
538
+ <h4 style="margin: 0; color: var(--text-secondary); font-size: 14px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">更新时间</h4>
539
+ </div>
540
+ <p style="margin: 0; font-size: 14px; color: var(--text-primary);">
541
+ ${new Date(task.updatedAt).toLocaleString('zh-CN', {
542
+ year: 'numeric',
543
+ month: 'long',
544
+ day: 'numeric',
545
+ hour: '2-digit',
546
+ minute: '2-digit',
547
+ second: '2-digit'
548
+ })}
549
+ </p>
550
+ </div>
551
+
552
+ </div>
553
+
554
+ <!-- 投票信息 -->
555
+ ${task.poll ? `
556
+ <div class="detail-section" style="background: var(--bg-secondary); border-radius: 12px; padding: 20px; margin-top: 16px;">
557
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
558
+ <span style="font-size: 24px;">📊</span>
559
+ <h4 style="margin: 0; color: var(--text-secondary); font-size: 14px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">投票信息</h4>
560
+ </div>
561
+
562
+ <div style="background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(168, 85, 247, 0.05) 100%); border-radius: 12px; padding: 20px; border: 1px solid rgba(99, 102, 241, 0.2);">
563
+ <h5 style="margin: 0 0 16px 0; font-size: 16px; font-weight: 600; color: var(--text-primary);">${task.poll.question || '投票'}</h5>
564
+
565
+ <!-- 投票统计 -->
566
+ <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px;">
567
+ <div style="background: var(--bg-tertiary); padding: 12px; border-radius: 8px; text-align: center;">
568
+ <div style="font-size: 24px; font-weight: 700; color: var(--primary);">${task.poll.votedCount || 0}</div>
569
+ <div style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">已投票</div>
570
+ </div>
571
+ <div style="background: var(--bg-tertiary); padding: 12px; border-radius: 8px; text-align: center;">
572
+ <div style="font-size: 24px; font-weight: 700; color: var(--warning);">${(task.poll.totalMembers || 0) - (task.poll.votedCount || 0)}</div>
573
+ <div style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">未投票</div>
574
+ </div>
575
+ <div style="background: var(--bg-tertiary); padding: 12px; border-radius: 8px; text-align: center;">
576
+ <div style="font-size: 24px; font-weight: 700; color: var(--success);">${task.poll.totalVotes || 0}</div>
577
+ <div style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">总票数</div>
578
+ </div>
579
+ </div>
580
+
581
+ <!-- 投票选项 -->
582
+ <div style="display: flex; flex-direction: column; gap: 12px;">
583
+ ${task.poll.options?.map(option => {
584
+ const percentage = task.poll.totalVotes > 0 ? Math.round((option.votes || 0) / task.poll.totalVotes * 100) : 0;
585
+ const voters = option.voters || [];
586
+ return \`
587
+ <div style="background: var(--bg-tertiary); border-radius: 10px; padding: 16px; border: 1px solid var(--border);">
588
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
589
+ <span style="font-weight: 600; color: var(--text-primary);">\${option.text}</span>
590
+ <span style="font-weight: 700; color: var(--primary);">\${option.votes || 0} 票 (\${percentage}%)</span>
591
+ </div>
592
+ <div style="height: 8px; background: var(--bg-secondary); border-radius: 4px; overflow: hidden; margin-bottom: 12px;">
593
+ <div style="height: 100%; background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%); width: \${percentage}%; transition: width 0.3s;"></div>
594
+ </div>
595
+ \${voters.length > 0 ? \`
596
+ <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border);">
597
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 8px;">投票成员 (\${voters.length}人):</div>
598
+ <div style="display: flex; flex-wrap: wrap; gap: 6px;">
599
+ ${voters.map(voter => {
600
+ const voterName = typeof voter === 'string' ? voter : voter.username;
601
+ return `
602
+ <span style="display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 12px; font-size: 12px;">
603
+ <span style="width: 20px; height: 20px; border-radius: 50%; background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 10px; font-weight: 700;">${voterName ? voterName[0].toUpperCase() : '?'}</span>
604
+ <span style="font-weight: 600; color: var(--text-primary);">${voterName || '未知用户'}</span>
605
+ </span>
606
+ `;
607
+ }).join('')}
608
+ </div>
609
+ </div>
610
+ \` : \`
611
+ <div style="text-align: center; padding: 8px; color: var(--text-secondary); font-size: 12px; font-style: italic;">暂无人投票</div>
612
+ \`}
613
+ </div>
614
+ \`;
615
+ }).join('') || '<p style="color: var(--text-secondary); font-style: italic;">暂无投票选项</p>'}
616
+ </div>
617
+
618
+ <!-- 未投票成员 -->
619
+ ${task.poll.unvotedMembers && task.poll.unvotedMembers.length > 0 ? \`
620
+ <div style="margin-top: 16px; padding: 16px; background: linear-gradient(135deg, rgba(239, 68, 68, 0.05) 0%, rgba(220, 38, 38, 0.05) 100%); border: 1px solid rgba(239, 68, 68, 0.2); border-radius: 10px;">
621
+ <div style="font-size: 14px; font-weight: 600; color: var(--danger); margin-bottom: 12px; display: flex; align-items: center; gap: 8px;">
622
+ <span>⚠️</span>
623
+ <span>未投票成员 (\${task.poll.unvotedMembers.length}人):</span>
624
+ </div>
625
+ <div style="display: flex; flex-wrap: wrap; gap: 8px;">
626
+ \${task.poll.unvotedMembers.map(member => {
627
+ const memberName = typeof member === 'string' ? member : member.username;
628
+ return \\`
629
+ <span style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: var(--bg-tertiary); border: 2px solid rgba(239, 68, 68, 0.3); border-radius: 12px; font-size: 12px;">
630
+ <span style="width: 22px; height: 22px; border-radius: 50%; background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 11px; font-weight: 700;">\\${memberName ? memberName[0].toUpperCase() : '?'}</span>
631
+ <span style="font-weight: 600; color: var(--text-primary);">\\${memberName || '未知用户'}</span>
632
+ <span style="font-size: 10px; color: var(--danger); background: rgba(239, 68, 68, 0.1); padding: 2px 6px; border-radius: 8px;">未投票</span>
633
+ </span>
634
+ \\`;
635
+ }).join('')}
636
+ </div>
637
+ </div>
638
+ \` : task.poll.totalMembers > 0 ? \`
639
+ <div style="margin-top: 16px; padding: 12px; background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, rgba(5, 150, 105, 0.05) 100%); border: 1px solid rgba(16, 185, 129, 0.3); border-radius: 10px; text-align: center; color: var(--success); font-weight: 600;">
640
+ ✅ 所有成员已完成投票
641
+ </div>
642
+ \` : ''}
643
+ </div>
644
+ </div>
645
+ ` : ''}
646
+
647
+ </div>
648
+ `;
649
+
650
+ document.getElementById('taskDetailModal').classList.remove('hidden');
651
+ }
652
+ });
653
+ });
654
+ }
655
+
656
+ document.getElementById('createTaskBtn').addEventListener('click', () => {
657
+ document.getElementById('createTaskModal').classList.remove('hidden');
658
+ });
659
+
660
+ document.getElementById('closeTaskModal').addEventListener('click', () => {
661
+ document.getElementById('createTaskModal').classList.add('hidden');
662
+ });
663
+
664
+ document.getElementById('closeTaskDetailModal').addEventListener('click', () => {
665
+ document.getElementById('taskDetailModal').classList.add('hidden');
666
+ });
667
+
668
+ document.getElementById('createTaskForm').addEventListener('submit', async (e) => {
669
+ e.preventDefault();
670
+ const formData = new FormData(e.target);
671
+ try {
672
+ // 获取群组信息,自动分配给所有成员
673
+ const groupResult = await apiService.getGroup(currentGroup._id);
674
+ const memberIds = groupResult.group.members.map(m => m._id);
675
+
676
+ await apiService.createTask({
677
+ title: formData.get('title'),
678
+ description: formData.get('description'),
679
+ groupId: currentGroup._id,
680
+ assignedTo: memberIds, // 分配给所有成员
681
+ deadline: formData.get('deadline') || null
682
+ });
683
+ alert('任务创建成功!已分配给所有群组成员');
684
+ await renderTasksView(container);
685
+ document.getElementById('createTaskModal').classList.add('hidden');
686
+ } catch (error) {
687
+ console.error('创建任务错误:', error);
688
+ alert('创建失败: ' + error.message);
689
+ }
690
+ });
691
+ }
692
+
693
+ async function renderDocumentsView(container) {
694
+ if (!currentGroup) {
695
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
696
+ return;
697
+ }
698
+
699
+ const result = await apiService.getDocuments(currentGroup._id);
700
+ const groupResult = await apiService.getGroup(currentGroup._id);
701
+ const group = groupResult.group;
702
+
703
+ container.innerHTML = `
704
+ <div class="view-header">
705
+ <h2>📄 共享文档 - ${currentGroup.name}</h2>
706
+ <button class="btn-primary" id="createDocBtn">➕ 创建文档</button>
707
+ </div>
708
+ <div class="documents-list" id="docsList"></div>
709
+
710
+ <!-- 创建文档模态框 -->
711
+ <div id="createDocModal" class="modal hidden">
712
+ <div class="modal-content" style="max-width: 700px;">
713
+ <div class="modal-header">
714
+ <h3>创建共享文档</h3>
715
+ <button class="modal-close" id="closeDocModal">&times;</button>
716
+ </div>
717
+ <form id="createDocForm" style="padding: 20px;">
718
+ <div class="form-group">
719
+ <label>📌 文档标题</label>
720
+ <input type="text" name="title" placeholder="请输入文档标题" required style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;">
721
+ </div>
722
+ <div class="form-group">
723
+ <label>📝 文档内容</label>
724
+ <textarea name="content" placeholder="请输入文档内容" rows="6" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;"></textarea>
725
+ </div>
726
+ <div class="form-group">
727
+ <label>👥 成员编辑权限</label>
728
+ <div style="max-height: 200px; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 10px;">
729
+ ${group.members.map(member => `
730
+ <div style="display: flex; align-items: center; gap: 10px; padding: 8px; background: var(--bg-secondary); border-radius: 6px; margin-bottom: 8px;">
731
+ <div class="avatar" style="width: 32px; height: 32px; font-size: 14px;">${member.username[0].toUpperCase()}</div>
732
+ <span style="flex: 1;">${member.username}</span>
733
+ <label style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
734
+ <input type="checkbox" name="editableMembers" value="${member._id}" ${member._id === group.admin.toString() ? 'checked disabled' : 'checked'} style="width: 18px; height: 18px; cursor: pointer;">
735
+ <span style="font-size: 13px;">${member._id === group.admin.toString() ? '管理员' : '可编辑'}</span>
736
+ </label>
737
+ </div>
738
+ `).join('')}
739
+ </div>
740
+ <p style="font-size: 12px; color: var(--text-tertiary); margin-top: 8px;">💡 提示:所有成员都可以查看文档,勾选的成员可以编辑文档</p>
741
+ </div>
742
+ <div style="display: flex; gap: 10px; margin-top: 20px;">
743
+ <button type="submit" class="btn-primary" style="flex: 1;">创建</button>
744
+ <button type="button" class="btn-secondary" id="closeDocModalBtn" style="flex: 1;">取消</button>
745
+ </div>
746
+ </form>
747
+ </div>
748
+ </div>
749
+
750
+ <!-- 编辑权限模态框 -->
751
+ <div id="editPermissionModal" class="modal hidden">
752
+ <div class="modal-content" style="max-width: 600px;">
753
+ <div class="modal-header">
754
+ <h3>管理编辑权限</h3>
755
+ <button class="modal-close" id="closePermissionModal">&times;</button>
756
+ </div>
757
+ <div id="permissionContent" style="padding: 20px;">
758
+ </div>
759
+ </div>
760
+ </div>
761
+ `;
762
+
763
+ const docsList = document.getElementById('docsList');
764
+ if (result.documents.length === 0) {
765
+ docsList.innerHTML = '<div class="empty-state">暂无共享文档</div>';
766
+ } else {
767
+ result.documents.forEach(doc => {
768
+ const docCard = document.createElement('div');
769
+ docCard.className = 'document-card';
770
+ const editableCount = doc.editableMembers?.length || 0;
771
+ docCard.innerHTML = `
772
+ <div style="display: flex; justify-content: space-between; align-items: start;">
773
+ <div style="flex: 1;">
774
+ <h3>📄 ${doc.title}</h3>
775
+ <div class="doc-meta" style="display: flex; gap: 15px; margin-top: 8px; font-size: 13px; color: var(--text-secondary);">
776
+ <span>👤 创建者: ${doc.creator.username}</span>
777
+ <span>✏️ ${editableCount} 人可编辑</span>
778
+ <span>📅 ${new Date(doc.createdAt).toLocaleDateString()}</span>
779
+ </div>
780
+ </div>
781
+ <div style="display: flex; gap: 10px; align-items: center;">
782
+ <button class="btn-secondary btn-sm" data-id="${doc._id}" data-action="manage-permission" title="管理权限" style="padding: 8px 16px;">⚙️ 权限</button>
783
+ <button class="btn-primary btn-sm" data-id="${doc._id}" data-action="edit-doc" title="编辑文档" style="padding: 8px 16px;">✏️ 编辑</button>
784
+ <button class="btn-danger btn-sm" data-id="${doc._id}" data-action="delete-doc" title="删除文档" style="padding: 8px 16px;">🗑️ 删除</button>
785
+ </div>
786
+ </div>
787
+ `;
788
+ docsList.appendChild(docCard);
789
+ });
790
+
791
+ // 编辑文档按钮
792
+ document.querySelectorAll('[data-action="edit-doc"]').forEach(btn => {
793
+ btn.addEventListener('click', () => {
794
+ renderDocumentEditor(container, btn.dataset.id);
795
+ });
796
+ });
797
+
798
+ // 管理权限按钮
799
+ document.querySelectorAll('[data-action="manage-permission"]').forEach(btn => {
800
+ btn.addEventListener('click', async () => {
801
+ const docId = btn.dataset.id;
802
+ const doc = result.documents.find(d => d._id === docId);
803
+ showPermissionModal(doc, group);
804
+ });
805
+ });
806
+
807
+ // 删除文档按钮
808
+ document.querySelectorAll('[data-action="delete-doc"]').forEach(btn => {
809
+ btn.addEventListener('click', async (e) => {
810
+ e.stopPropagation();
811
+ const docId = btn.dataset.id;
812
+ if (confirm('确定要删除这个文档吗?删除后无法恢复!')) {
813
+ try {
814
+ await apiService.deleteDocument(docId);
815
+ alert('文档删除成功!');
816
+ await renderDocumentsView(container);
817
+ } catch (error) {
818
+ console.error('删除文档错误:', error);
819
+ alert('删除失败: ' + (error.message || '未知错误'));
820
+ }
821
+ }
822
+ });
823
+ });
824
+ }
825
+
826
+ document.getElementById('createDocBtn').addEventListener('click', () => {
827
+ document.getElementById('createDocModal').classList.remove('hidden');
828
+ });
829
+
830
+ document.getElementById('closeDocModal').addEventListener('click', () => {
831
+ document.getElementById('createDocModal').classList.add('hidden');
832
+ });
833
+
834
+ document.getElementById('closeDocModalBtn').addEventListener('click', () => {
835
+ document.getElementById('createDocModal').classList.add('hidden');
836
+ });
837
+
838
+ // 权限管理模态框函数
839
+ function showPermissionModal(doc, group) {
840
+ const modal = document.getElementById('editPermissionModal');
841
+ const content = document.getElementById('permissionContent');
842
+
843
+ content.innerHTML = `
844
+ <div style="margin-bottom: 16px;">
845
+ <h4 style="margin: 0 0 8px 0;">📄 ${doc.title}</h4>
846
+ <p style="font-size: 13px; color: var(--text-secondary); margin: 0;">管理哪些成员可以编辑此文档</p>
847
+ </div>
848
+ <form id="updatePermissionForm">
849
+ <div style="max-height: 300px; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 10px;">
850
+ ${group.members.map(member => {
851
+ const isEditable = doc.editableMembers?.includes(member._id) || member._id === group.admin.toString();
852
+ const isAdmin = member._id === group.admin.toString();
853
+ return `
854
+ <div style="display: flex; align-items: center; gap: 10px; padding: 8px; background: var(--bg-secondary); border-radius: 6px; margin-bottom: 8px;">
855
+ <div class="avatar" style="width: 32px; height: 32px; font-size: 14px;">${member.username[0].toUpperCase()}</div>
856
+ <span style="flex: 1;">${member.username}</span>
857
+ <label style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
858
+ <input type="checkbox" name="editableMembers" value="${member._id}" ${isEditable ? 'checked' : ''} ${isAdmin ? 'disabled' : ''} style="width: 18px; height: 18px; cursor: pointer;">
859
+ <span style="font-size: 13px;">${isAdmin ? '管理员' : '可编辑'}</span>
860
+ </label>
861
+ </div>
862
+ `;
863
+ }).join('')}
864
+ </div>
865
+ <p style="font-size: 12px; color: var(--text-tertiary); margin-top: 12px;">💡 提示:所有成员都可以查看文档,只有勾选的成员可以编辑</p>
866
+ <div style="display: flex; gap: 10px; margin-top: 20px;">
867
+ <button type="submit" class="btn-primary" style="flex: 1;">保存</button>
868
+ <button type="button" class="btn-secondary" id="cancelPermissionBtn" style="flex: 1;">取消</button>
869
+ </div>
870
+ </form>
871
+ `;
872
+
873
+ modal.classList.remove('hidden');
874
+
875
+ document.getElementById('cancelPermissionBtn').addEventListener('click', () => {
876
+ modal.classList.add('hidden');
877
+ });
878
+
879
+ document.getElementById('updatePermissionForm').addEventListener('submit', async (e) => {
880
+ e.preventDefault();
881
+ const formData = new FormData(e.target);
882
+ const editableMembers = formData.getAll('editableMembers');
883
+
884
+ try {
885
+ await apiService.updateDocumentPermissions(doc._id, editableMembers);
886
+ alert('权限更新成功!');
887
+ modal.classList.add('hidden');
888
+ await renderDocumentsView(container);
889
+ } catch (error) {
890
+ console.error('更新权限错误:', error);
891
+ alert('更新失败: ' + error.message);
892
+ }
893
+ });
894
+ }
895
+
896
+ document.getElementById('closePermissionModal').addEventListener('click', () => {
897
+ document.getElementById('editPermissionModal').classList.add('hidden');
898
+ });
899
+
900
+ document.getElementById('createDocForm').addEventListener('submit', async (e) => {
901
+ e.preventDefault();
902
+ const formData = new FormData(e.target);
903
+ const editableMembers = formData.getAll('editableMembers');
904
+
905
+ try {
906
+ await apiService.createDocument(
907
+ formData.get('title'),
908
+ formData.get('content'),
909
+ currentGroup._id,
910
+ editableMembers
911
+ );
912
+ alert('文档创建成功!');
913
+ await renderDocumentsView(container);
914
+ document.getElementById('createDocModal').classList.add('hidden');
915
+ } catch (error) {
916
+ console.error('创建文档错误:', error);
917
+ alert('创建失败: ' + error.message);
918
+ }
919
+ });
920
+ }
921
+
922
+ async function renderDocumentEditor(container, documentId) {
923
+ const result = await apiService.getDocument(documentId);
924
+ const doc = result.document;
925
+
926
+ container.innerHTML = `
927
+ <div class="view-header">
928
+ <button class="btn-back" id="backBtn">← 返回</button>
929
+ <h2>${doc.title}</h2>
930
+ <span class="doc-status">${doc.permission === 'readonly' ? '🔒 只读模式' : '✏️ 编辑模式'}</span>
931
+ </div>
932
+ <div class="editor-container">
933
+ <div class="editor-toolbar">
934
+ <div class="online-users" id="onlineUsers">
935
+ <span class="user-badge">👤 ${user.username}</span>
936
+ </div>
937
+ <button class="btn-primary" id="saveBtn">保存</button>
938
+ </div>
939
+ <textarea id="editor" style="width: 100%; min-height: 400px; padding: 10px; font-family: monospace;">${doc.content || ''}</textarea>
940
+ <div class="editor-footer">
941
+ <span>最后编辑: ${new Date(doc.updatedAt).toLocaleString()}</span>
942
+ </div>
943
+ </div>
944
+ `;
945
+
946
+ const editor = document.getElementById('editor');
947
+
948
+ // 实时同步
949
+ let typingTimeout;
950
+ let saveTimeout;
951
+
952
+ editor.addEventListener('input', () => {
953
+ clearTimeout(typingTimeout);
954
+ clearTimeout(saveTimeout);
955
+ wsService.sendTyping(documentId, user.username, true);
956
+
957
+ typingTimeout = setTimeout(() => {
958
+ wsService.sendTyping(documentId, user.username, false);
959
+ }, 1000);
960
+
961
+ // 自动保存
962
+ saveTimeout = setTimeout(async () => {
963
+ const content = editor.value;
964
+ try {
965
+ await apiService.updateDocument(documentId, content);
966
+ } catch (error) {
967
+ console.error('自动保存失败:', error);
968
+ }
969
+ }, 2000);
970
+ });
971
+
972
+ document.getElementById('saveBtn').addEventListener('click', async () => {
973
+ try {
974
+ const content = editor.value;
975
+ await apiService.updateDocument(documentId, content);
976
+ alert('保存成功!');
977
+ } catch (error) {
978
+ alert('保存失败: ' + error.message);
979
+ }
980
+ });
981
+
982
+ // 监听文档更新
983
+ wsService.on('document_update', (data) => {
984
+ if (data.documentId === documentId && data.userId !== user.id) {
985
+ const cursorPos = editor.selectionStart;
986
+ editor.value = data.content;
987
+ editor.setSelectionRange(cursorPos, cursorPos);
988
+ }
989
+ });
990
+
991
+ // 监听打字状态
992
+ wsService.on('typing', (data) => {
993
+ if (data.documentId === documentId && data.userId !== user.id) {
994
+ const onlineUsers = document.getElementById('onlineUsers');
995
+ if (data.isTyping) {
996
+ onlineUsers.innerHTML += `<span class="user-badge typing" data-user="${data.userId}">✏️ ${data.username}</span>`;
997
+ } else {
998
+ const badge = onlineUsers.querySelector(`[data-user="${data.userId}"]`);
999
+ if (badge) badge.remove();
1000
+ }
1001
+ }
1002
+ });
1003
+
1004
+ document.getElementById('backBtn').addEventListener('click', () => {
1005
+ renderDocumentsView(container);
1006
+ });
1007
+ }
1008
+
1009
+ async function renderFilesView(container) {
1010
+ if (!currentGroup) {
1011
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
1012
+ return;
1013
+ }
1014
+
1015
+ try {
1016
+ const result = await apiService.getGroupFiles(currentGroup._id);
1017
+
1018
+ container.innerHTML = `
1019
+ <div class="view-header">
1020
+ <h2>文件管理 - ${currentGroup.name}</h2>
1021
+ <button class="btn-primary" id="uploadFileBtn">📤 上传文件</button>
1022
+ </div>
1023
+ <div class="files-list" id="filesList"></div>
1024
+
1025
+ <!-- 文件上传模态框 -->
1026
+ <div class="modal hidden" id="uploadFileModal">
1027
+ <div class="modal-content">
1028
+ <div class="modal-header">
1029
+ <h3>上传文件</h3>
1030
+ <button class="modal-close" id="closeUploadModal">&times;</button>
1031
+ </div>
1032
+ <form id="uploadFileForm">
1033
+ <div class="form-group">
1034
+ <label>选择文件</label>
1035
+ <input type="file" id="fileInput" required>
1036
+ <small>支持图片、PDF、Word、Excel等,最大10MB</small>
1037
+ </div>
1038
+ <div class="form-group">
1039
+ <label>描述(可选)</label>
1040
+ <textarea id="fileDescription" rows="3" placeholder="文件描述..."></textarea>
1041
+ </div>
1042
+ <div class="form-actions">
1043
+ <button type="button" class="btn-secondary" id="cancelUpload">取消</button>
1044
+ <button type="submit" class="btn-primary">上传</button>
1045
+ </div>
1046
+ </form>
1047
+ </div>
1048
+ </div>
1049
+ `;
1050
+
1051
+ const filesList = document.getElementById('filesList');
1052
+
1053
+ if (!result.files || result.files.length === 0) {
1054
+ filesList.innerHTML = '<div class="empty-state">暂无文件</div>';
1055
+ } else {
1056
+ result.files.forEach(file => {
1057
+ const fileCard = document.createElement('div');
1058
+ fileCard.className = 'file-card';
1059
+
1060
+ const fileIcon = getFileIcon(file.mimetype);
1061
+ const fileSize = formatFileSize(file.size);
1062
+
1063
+ fileCard.innerHTML = `
1064
+ <div class="file-icon">${fileIcon}</div>
1065
+ <div class="file-info">
1066
+ <h4>${file.originalName}</h4>
1067
+ <div class="file-meta">
1068
+ <span>上传者: ${file.uploader.username}</span>
1069
+ <span>大小: ${fileSize}</span>
1070
+ <span>时间: ${new Date(file.createdAt).toLocaleString()}</span>
1071
+ </div>
1072
+ ${file.description ? `<p class="file-description">${file.description}</p>` : ''}
1073
+ </div>
1074
+ <div class="file-actions">
1075
+ <a href="${apiService.getFileDownloadUrl(file._id)}" class="btn-primary" download>下载</a>
1076
+ <button class="btn-danger" data-id="${file._id}" data-action="delete-file">删除</button>
1077
+ </div>
1078
+ `;
1079
+ filesList.appendChild(fileCard);
1080
+ });
1081
+
1082
+ // 删除文件事件
1083
+ document.querySelectorAll('[data-action="delete-file"]').forEach(btn => {
1084
+ btn.addEventListener('click', async () => {
1085
+ if (confirm('确定要删除这个文件吗?')) {
1086
+ try {
1087
+ await apiService.deleteFile(btn.dataset.id);
1088
+ alert('文件删除成功!');
1089
+ await renderFilesView(container);
1090
+ } catch (error) {
1091
+ alert('删除失败: ' + error.message);
1092
+ }
1093
+ }
1094
+ });
1095
+ });
1096
+ }
1097
+
1098
+ // 文件上传功能
1099
+ document.getElementById('uploadFileBtn').addEventListener('click', () => {
1100
+ document.getElementById('uploadFileModal').classList.remove('hidden');
1101
+ });
1102
+
1103
+ document.getElementById('closeUploadModal').addEventListener('click', () => {
1104
+ document.getElementById('uploadFileModal').classList.add('hidden');
1105
+ document.getElementById('uploadFileForm').reset();
1106
+ });
1107
+
1108
+ document.getElementById('cancelUpload').addEventListener('click', () => {
1109
+ document.getElementById('uploadFileModal').classList.add('hidden');
1110
+ document.getElementById('uploadFileForm').reset();
1111
+ });
1112
+
1113
+ document.getElementById('uploadFileForm').addEventListener('submit', async (e) => {
1114
+ e.preventDefault();
1115
+ const fileInput = document.getElementById('fileInput');
1116
+ const description = document.getElementById('fileDescription').value;
1117
+
1118
+ if (!fileInput.files[0]) {
1119
+ alert('请选择文件');
1120
+ return;
1121
+ }
1122
+
1123
+ try {
1124
+ await apiService.uploadFile(currentGroup._id, fileInput.files[0], description);
1125
+ alert('文件上传成功!');
1126
+ document.getElementById('uploadFileModal').classList.add('hidden');
1127
+ document.getElementById('uploadFileForm').reset();
1128
+ await renderFilesView(container);
1129
+ } catch (error) {
1130
+ alert('上传失败: ' + error.message);
1131
+ }
1132
+ });
1133
+ } catch (error) {
1134
+ console.error('获取文件列表失败:', error);
1135
+ container.innerHTML = `
1136
+ <div class="view-header">
1137
+ <h2>文件管理</h2>
1138
+ </div>
1139
+ <div class="empty-state">加载文件失败: ${error.message}</div>
1140
+ `;
1141
+ }
1142
+ }
1143
+
1144
+ function getFileIcon(mimetype) {
1145
+ if (mimetype.startsWith('image/')) return '🖼️';
1146
+ if (mimetype === 'application/pdf') return '📕';
1147
+ if (mimetype.includes('word') || mimetype.includes('document')) return '📘';
1148
+ if (mimetype.includes('excel') || mimetype.includes('spreadsheet')) return '📗';
1149
+ if (mimetype.includes('zip') || mimetype.includes('compressed')) return '📦';
1150
+ return '📄';
1151
+ }
1152
+
1153
+ function formatFileSize(bytes) {
1154
+ if (bytes === 0) return '0 Bytes';
1155
+ const k = 1024;
1156
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
1157
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1158
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
1159
+ }
1160
+
1161
+ async function renderChatView(container) {
1162
+ if (!currentGroup) {
1163
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
1164
+ return;
1165
+ }
1166
+
1167
+ const groupResult = await apiService.getGroup(currentGroup._id);
1168
+ let group = groupResult.group;
1169
+
1170
+ container.innerHTML = `
1171
+ <div class="view-header">
1172
+ <h2>群聊 - ${currentGroup.name}</h2>
1173
+ <div style="display: flex; gap: 10px;">
1174
+ <button class="btn-secondary" id="muteAllBtn">全体禁言</button>
1175
+ <button class="btn-secondary" id="manageMuteBtn">个人禁言</button>
1176
+ <button class="btn-danger" id="clearChatBtn" style="background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); color: white; border: none;">🗑️ 清除记录</button>
1177
+ <button class="btn-secondary" id="openAIBtn">🤖 AI助手</button>
1178
+ <button class="btn-secondary" id="openWhiteboardBtn">🎨 协作白板</button>
1179
+ </div>
1180
+ </div>
1181
+ <div class="chat-container">
1182
+ <div class="messages" id="messages"></div>
1183
+ <div class="chat-input">
1184
+ <button class="btn-emoji" id="emojiBtn">😊</button>
1185
+ <input type="text" id="messageInput" placeholder="输入消息...">
1186
+ <button class="btn-primary" id="sendBtn">发送</button>
1187
+ </div>
1188
+ <emoji-picker id="emojiPicker" class="hidden"></emoji-picker>
1189
+ </div>
1190
+ <div id="manageMuteModal" class="modal hidden">
1191
+ <div class="modal-content">
1192
+ <h3>个人禁言</h3>
1193
+ <div id="membersList" style="max-height: 400px; overflow-y: auto;"></div>
1194
+ <button type="button" class="btn-secondary" id="closeMuteModal">关闭</button>
1195
+ </div>
1196
+ </div>
1197
+
1198
+ <!-- AI助手模态框 -->
1199
+ <div id="aiModal" class="modal hidden">
1200
+ <div class="modal-content" style="max-width: 950px; height: 88vh; display: flex; flex-direction: column; background: var(--bg-card); border-radius: 16px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.3);">
1201
+
1202
+ <!-- 头部 -->
1203
+ <div class="modal-header" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 24px 28px; position: relative; overflow: hidden;">
1204
+ <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; opacity: 0.1; background-image: repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(255,255,255,0.1) 10px, rgba(255,255,255,0.1) 20px);"></div>
1205
+ <div style="display: flex; align-items: center; justify-content: space-between; position: relative; z-index: 1;">
1206
+ <div style="display: flex; align-items: center; gap: 18px;">
1207
+ <div style="width: 56px; height: 56px; background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border-radius: 14px; display: flex; align-items: center; justify-content: center; font-size: 32px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);">🤖</div>
1208
+ <div>
1209
+ <h3 style="margin: 0; font-size: 24px; font-weight: 700; letter-spacing: -0.5px;">AI 智能助手</h3>
1210
+ <p style="margin: 6px 0 0 0; font-size: 14px; opacity: 0.9; font-weight: 400;">为您提供智能问答和协助服务 ✨</p>
1211
+ </div>
1212
+ </div>
1213
+ <button class="modal-close" id="closeAIModal" style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border: none; color: white; width: 40px; height: 40px; border-radius: 10px; cursor: pointer; font-size: 22px; transition: all 0.3s; display: flex; align-items: center; justify-content: center;" onmouseover="this.style.background='rgba(255,255,255,0.25)'" onmouseout="this.style.background='rgba(255,255,255,0.15)'">&times;</button>
1214
+ </div>
1215
+ </div>
1216
+
1217
+ <div style="flex: 1; display: flex; flex-direction: column; overflow: hidden;">
1218
+
1219
+ <!-- 快捷问题 -->
1220
+ <div style="padding: 18px 24px; background: linear-gradient(to bottom, var(--bg-secondary), var(--bg-card)); border-bottom: 1px solid var(--border);">
1221
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">
1222
+ <span style="font-size: 16px;">💡</span>
1223
+ <span style="font-size: 13px; color: var(--text-secondary); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">快捷问题</span>
1224
+ </div>
1225
+ <div style="display: flex; gap: 10px; flex-wrap: wrap;">
1226
+ <button class="quick-question-btn" data-question="如何创建一个新文档?" style="padding: 8px 16px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.3s; color: var(--text-primary); font-weight: 500;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(99, 102, 241, 0.2)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">📄 如何创建文档?</button>
1227
+ <button class="quick-question-btn" data-question="如何邀请成员加入群组?" style="padding: 8px 16px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.3s; color: var(--text-primary); font-weight: 500;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(99, 102, 241, 0.2)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">👥 如何邀请成员?</button>
1228
+ <button class="quick-question-btn" data-question="如何使用工作流功能?" style="padding: 8px 16px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.3s; color: var(--text-primary); font-weight: 500;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(99, 102, 241, 0.2)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">⚙️ 工作流使用?</button>
1229
+ <button class="quick-question-btn" data-question="如何备份数据?" style="padding: 8px 16px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.3s; color: var(--text-primary); font-weight: 500;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(99, 102, 241, 0.2)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">💾 如何备份?</button>
1230
+ </div>
1231
+ </div>
1232
+
1233
+ <!-- 聊天区域 -->
1234
+ <div class="ai-chat" id="aiChatMessages" style="flex: 1; overflow-y: auto; padding: 24px; background: var(--bg-dark); background-image: radial-gradient(circle at 20% 50%, rgba(99, 102, 241, 0.03) 0%, transparent 50%), radial-gradient(circle at 80% 80%, rgba(168, 85, 247, 0.03) 0%, transparent 50%);">
1235
+ <div class="ai-message ai" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 18px 22px; border-radius: 20px 20px 20px 6px; margin: 12px 0; max-width: 85%; box-shadow: 0 4px 16px rgba(102, 126, 234, 0.25);">
1236
+ <div style="display: flex; align-items: start; gap: 14px;">
1237
+ <div style="font-size: 28px; line-height: 1;">🤖</div>
1238
+ <div style="flex: 1;">
1239
+ <p style="margin: 0 0 10px 0; font-weight: 700; font-size: 16px;">你好!我是AI智能助手</p>
1240
+ <p style="margin: 0 0 12px 0; opacity: 0.95; line-height: 1.7; font-size: 14px;">我可以帮助你:</p>
1241
+ <ul style="margin: 0; padding-left: 22px; opacity: 0.95; line-height: 2; font-size: 14px;">
1242
+ <li style="margin-bottom: 4px;">解答关于平台功能的问题</li>
1243
+ <li style="margin-bottom: 4px;">提供操作指导和建议</li>
1244
+ <li>帮助你更好地使用各项功能</li>
1245
+ </ul>
1246
+ </div>
1247
+ </div>
1248
+ </div>
1249
+ </div>
1250
+
1251
+ <!-- 输入区域 -->
1252
+ <div class="ai-input-container" style="padding: 20px 24px 24px; border-top: 1px solid var(--border); background: linear-gradient(to top, var(--bg-secondary), var(--bg));">
1253
+ <div style="display: flex; gap: 14px; align-items: end;">
1254
+ <div style="flex: 1; position: relative;">
1255
+ <textarea id="aiInputText" placeholder="输入你的问题..." rows="1" style="width: 100%; padding: 14px 18px; border: 2px solid var(--border); border-radius: 14px; resize: none; font-size: 15px; transition: all 0.3s; max-height: 140px; background: var(--bg); box-shadow: 0 2px 8px rgba(0,0,0,0.05);" onfocus="this.style.borderColor='#667eea'; this.style.boxShadow='0 4px 16px rgba(102, 126, 234, 0.15)'" onblur="this.style.borderColor='var(--border)'; this.style.boxShadow='0 2px 8px rgba(0,0,0,0.05)'"></textarea>
1256
+ </div>
1257
+ <button class="btn-primary" id="aiSendBtnModal" style="padding: 14px 28px; border-radius: 14px; font-size: 15px; font-weight: 600; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; cursor: pointer; transition: all 0.3s; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); min-width: 100px;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 20px rgba(102, 126, 234, 0.4)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 4px 12px rgba(102, 126, 234, 0.3)'">
1258
+ <span style="display: flex; align-items: center; gap: 8px; justify-content: center;">
1259
+ <span>发送</span>
1260
+ <span style="font-size: 16px;">🚀</span>
1261
+ </span>
1262
+ </button>
1263
+ </div>
1264
+ <div style="margin-top: 12px; font-size: 12px; color: var(--text-tertiary); text-align: center; display: flex; align-items: center; justify-content: center; gap: 6px;">
1265
+ <span style="opacity: 0.8;">💡</span>
1266
+ <span>提示:按 <kbd style="padding: 2px 6px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 4px; font-size: 11px;">Enter</kbd> 发送,<kbd style="padding: 2px 6px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 4px; font-size: 11px;">Shift + Enter</kbd> 换行</span>
1267
+ </div>
1268
+ </div>
1269
+ </div>
1270
+ </div>
1271
+ </div>
1272
+
1273
+ <!-- 协作白板模态框 -->
1274
+ <div id="whiteboardModal" class="modal hidden">
1275
+ <div class="modal-content" style="max-width: 95vw; max-height: 90vh; width: 1400px;">
1276
+ <div class="modal-header">
1277
+ <h3>🎨 协作白板</h3>
1278
+ <div style="display: flex; gap: 10px;">
1279
+ <button class="btn-secondary" id="clearCanvasBtn">清空画布</button>
1280
+ <button class="btn-primary" id="saveCanvasBtn">保存白板</button>
1281
+ <button class="btn-success" id="sendToGroupBtn" style="background: var(--success); color: white;">📤 发送到群聊</button>
1282
+ <button class="modal-close" id="closeWhiteboardModal">&times;</button>
1283
+ </div>
1284
+ </div>
1285
+ <div class="whiteboard-container" style="padding: 15px;">
1286
+ <div class="whiteboard-toolbar" style="display: flex; gap: 10px; margin-bottom: 15px; padding: 10px; background: var(--bg-secondary); border-radius: 8px;">
1287
+ <button class="tool-btn active" data-tool="pen" style="padding: 8px 15px; border: 2px solid var(--primary); border-radius: 6px; background: var(--primary); color: white;">✏️ 画笔</button>
1288
+ <button class="tool-btn" data-tool="eraser" style="padding: 8px 15px; border: 2px solid var(--border); border-radius: 6px; background: transparent;">🧹 橡皮擦</button>
1289
+ <input type="color" id="colorPickerCanvas" value="#000000" title="颜色" style="width: 50px; height: 40px; border: none; border-radius: 6px; cursor: pointer;">
1290
+ <input type="range" id="brushSizeCanvas" min="1" max="20" value="3" title="画笔大小" style="width: 150px;">
1291
+ <span id="brushSizeLabel" style="padding: 8px 15px;">大小: 3</span>
1292
+ </div>
1293
+ <canvas id="whiteboardCanvas" width="1300" height="600" style="border: 2px solid var(--border); background: white; cursor: crosshair; border-radius: 8px; display: block;"></canvas>
1294
+ </div>
1295
+ </div>
1296
+ </div>
1297
+ `;
1298
+
1299
+ const messagesDiv = document.getElementById('messages');
1300
+ const messageInput = document.getElementById('messageInput');
1301
+ const sendBtn = document.getElementById('sendBtn');
1302
+ const emojiBtn = document.getElementById('emojiBtn');
1303
+ const emojiPicker = document.getElementById('emojiPicker');
1304
+ let mutedUsers = new Set((group.mutedUsers || []).map(String));
1305
+ let isMutedAll = Boolean(group.mutedAll);
1306
+
1307
+ // 表情包功能
1308
+ emojiBtn.addEventListener('click', () => {
1309
+ emojiPicker.classList.toggle('hidden');
1310
+ });
1311
+
1312
+ emojiPicker.addEventListener('emoji-click', (event) => {
1313
+ messageInput.value += event.detail.unicode;
1314
+ messageInput.focus();
1315
+ emojiPicker.classList.add('hidden');
1316
+ });
1317
+
1318
+ // 点击外部关闭表情选择器
1319
+ document.addEventListener('click', (e) => {
1320
+ if (!emojiBtn.contains(e.target) && !emojiPicker.contains(e.target)) {
1321
+ emojiPicker.classList.add('hidden');
1322
+ }
1323
+ });
1324
+
1325
+ // 消息通知系统
1326
+ function showNotification(title, body, icon = '💬') {
1327
+ if ('Notification' in window && Notification.permission === 'granted') {
1328
+ new Notification(title, {
1329
+ body: body,
1330
+ icon: '/icon.png',
1331
+ badge: '/icon.png',
1332
+ tag: 'chat-message'
1333
+ });
1334
+ }
1335
+ }
1336
+
1337
+ // 请求通知权限
1338
+ if ('Notification' in window && Notification.permission === 'default') {
1339
+ Notification.requestPermission();
1340
+ }
1341
+
1342
+ // 加载历史消息
1343
+ try {
1344
+ const messagesResult = await apiService.getGroupMessages(currentGroup._id);
1345
+ if (messagesResult.messages) {
1346
+ messagesResult.messages.forEach(msg => {
1347
+ const messageEl = document.createElement('div');
1348
+ messageEl.className = `message ${msg.sender === currentUserId ? 'own' : ''}`;
1349
+
1350
+ // 检查是否是白板作品消息
1351
+ let messageContent = msg.content;
1352
+ if (msg.content.startsWith('[白板作品]')) {
1353
+ const imageUrl = msg.content.replace('[白板作品]', '').trim();
1354
+ console.log('白板作品URL:', imageUrl.substring(0, 100)); // 调试:打印URL前100个字符
1355
+ messageContent = `
1356
+ <div style="margin-bottom: 8px; font-weight: 600; color: var(--primary);">🎨 白板作品</div>
1357
+ <img src="${imageUrl}" alt="白板作品" style="max-width: 400px; max-height: 300px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); cursor: pointer; transition: transform 0.2s;" onclick="window.open('${imageUrl}', '_blank')" onmouseover="this.style.transform='scale(1.02)'" onmouseout="this.style.transform='scale(1)'">
1358
+ <div style="margin-top: 8px; font-size: 12px; color: var(--text-tertiary);">点击查看大图</div>
1359
+ `;
1360
+ }
1361
+
1362
+ messageEl.innerHTML = `
1363
+ <div class="message-header">
1364
+ <span class="message-user">${msg.username}</span>
1365
+ <span class="message-time">${new Date(msg.timestamp).toLocaleTimeString()}</span>
1366
+ </div>
1367
+ <div class="message-content">${messageContent}</div>
1368
+ `;
1369
+ messagesDiv.appendChild(messageEl);
1370
+ });
1371
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1372
+ }
1373
+ } catch (err) {
1374
+ console.error('加载历史消息失败:', err);
1375
+ }
1376
+
1377
+ const refreshMuteButtons = () => {
1378
+ const btn = document.getElementById('muteAllBtn');
1379
+ btn.textContent = isMutedAll ? '取消全体禁言' : '全体禁言';
1380
+ btn.style.background = isMutedAll ? 'var(--danger)' : '';
1381
+ };
1382
+ refreshMuteButtons();
1383
+
1384
+ // 全体禁言(服务端生效)
1385
+
1386
+ // 清除聊天记录
1387
+ document.getElementById('clearChatBtn').addEventListener('click', async () => {
1388
+ const confirmText = '⚠️ 警告:此操作将永久删除该群组的所有聊天记录!\n\n确定要清除吗?';
1389
+
1390
+ if (!confirm(confirmText)) {
1391
+ return;
1392
+ }
1393
+
1394
+ // 二次确认
1395
+ const doubleConfirm = prompt('请输入群组名称以确认删除:\n\n群组名称:' + currentGroup.name);
1396
+
1397
+ if (doubleConfirm !== currentGroup.name) {
1398
+ alert('❌ 群组名称不匹配,操作已取消');
1399
+ return;
1400
+ }
1401
+
1402
+ try {
1403
+ const btn = document.getElementById('clearChatBtn');
1404
+ const originalText = btn.innerHTML;
1405
+ btn.innerHTML = '⏳ 清除中...';
1406
+ btn.disabled = true;
1407
+
1408
+ const token = localStorage.getItem('token');
1409
+ const response = await fetch(`http://localhost:8765/api/messages/group/${currentGroup._id}/clear`, {
1410
+ method: 'DELETE',
1411
+ headers: {
1412
+ 'Authorization': `Bearer ${token}`,
1413
+ 'Content-Type': 'application/json'
1414
+ }
1415
+ });
1416
+
1417
+ if (!response.ok) {
1418
+ const error = await response.json();
1419
+ throw new Error(error.message || '清除失败');
1420
+ }
1421
+
1422
+ const result = await response.json();
1423
+
1424
+ // 清空消息显示
1425
+ messagesDiv.innerHTML = '<div class="empty-state" style="padding: 40px; text-align: center; color: var(--text-secondary);">✨ 聊天记录已清空</div>';
1426
+
1427
+ alert(`✅ 成功清除 ${result.deletedCount || 0} 条聊天记录!`);
1428
+
1429
+ btn.innerHTML = originalText;
1430
+ btn.disabled = false;
1431
+
1432
+ } catch (error) {
1433
+ console.error('清除聊天记录失败:', error);
1434
+ alert('❌ 清除失败: ' + error.message);
1435
+
1436
+ const btn = document.getElementById('clearChatBtn');
1437
+ btn.innerHTML = '🗑️ 清除记录';
1438
+ btn.disabled = false;
1439
+ }
1440
+ });
1441
+
1442
+ document.getElementById('muteAllBtn').addEventListener('click', async () => {
1443
+ try {
1444
+ const next = !isMutedAll;
1445
+ const res = await apiService.setMuteAll(currentGroup._id, next);
1446
+ isMutedAll = Boolean(res.mutedAll);
1447
+ refreshMuteButtons();
1448
+
1449
+ const notification = document.createElement('div');
1450
+ notification.className = 'notification';
1451
+ notification.textContent = isMutedAll ? '已开启全体禁言(成员无法发言)' : '已取消全体禁言';
1452
+ messagesDiv.appendChild(notification);
1453
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1454
+ } catch (e) {
1455
+ alert('设置失败: ' + e.message);
1456
+ }
1457
+ });
1458
+
1459
+ // 个人禁言(服务端生效)
1460
+ document.getElementById('manageMuteBtn').addEventListener('click', async () => {
1461
+ // 重新拉取最新 group(避免成员变动不同步)
1462
+ const latest = await apiService.getGroup(currentGroup._id);
1463
+ group = latest.group;
1464
+ mutedUsers = new Set((group.mutedUsers || []).map(String));
1465
+
1466
+ const membersList = document.getElementById('membersList');
1467
+ membersList.innerHTML = group.members
1468
+ .filter(m => m._id.toString() !== currentUserId)
1469
+ .map(member => {
1470
+ const isMuted = mutedUsers.has(member._id.toString());
1471
+ return `
1472
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 12px; border-bottom: 1px solid var(--border);">
1473
+ <div style="display: flex; align-items: center; gap: 10px;">
1474
+ <div class="avatar" style="width: 35px; height: 35px;">${member.username[0].toUpperCase()}</div>
1475
+ <span>${member.username}</span>
1476
+ </div>
1477
+ <button class="btn-secondary btn-sm" onclick="toggleMute('${member._id}')" id="mute-${member._id}">
1478
+ ${isMuted ? '取消禁言' : '禁言'}
1479
+ </button>
1480
+ </div>
1481
+ `;
1482
+ }).join('');
1483
+ document.getElementById('manageMuteModal').classList.remove('hidden');
1484
+ });
1485
+
1486
+ document.getElementById('closeMuteModal').addEventListener('click', () => {
1487
+ document.getElementById('manageMuteModal').classList.add('hidden');
1488
+ });
1489
+
1490
+ // 切换个人禁言状态(服务端生效)
1491
+ window.toggleMute = async (userId) => {
1492
+ try {
1493
+ const nextMuted = !mutedUsers.has(userId);
1494
+ const res = await apiService.setUserMute(currentGroup._id, userId, nextMuted);
1495
+ mutedUsers = new Set((res.mutedUsers || []).map(String));
1496
+
1497
+ const btn = document.getElementById(`mute-${userId}`);
1498
+ btn.textContent = mutedUsers.has(userId) ? '取消禁言' : '禁言';
1499
+ btn.style.background = mutedUsers.has(userId) ? 'var(--danger)' : '';
1500
+ } catch (e) {
1501
+ alert('操作失败: ' + e.message);
1502
+ }
1503
+ };
1504
+
1505
+ // 监听消息
1506
+ wsService.on('chat_message', (data) => {
1507
+ if (data.groupId === currentGroup._id) {
1508
+ const messageEl = document.createElement('div');
1509
+ messageEl.className = `message ${data.userId === currentUserId ? 'own' : ''}`;
1510
+
1511
+ // 检查是否是白板作品消息
1512
+ let messageContent = data.content;
1513
+ if (data.content.startsWith('[白板作品]')) {
1514
+ const imageUrl = data.content.replace('[白板作品]', '').trim();
1515
+ console.log('实时白板作品URL:', imageUrl.substring(0, 100)); // 调试:打印URL前100个字符
1516
+ messageContent = `
1517
+ <div style="margin-bottom: 8px; font-weight: 600; color: var(--primary);">🎨 白板作品</div>
1518
+ <img src="${imageUrl}" alt="白板作品" style="max-width: 400px; max-height: 300px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); cursor: pointer; transition: transform 0.2s;" onclick="window.open('${imageUrl}', '_blank')" onmouseover="this.style.transform='scale(1.02)'" onmouseout="this.style.transform='scale(1)'">
1519
+ <div style="margin-top: 8px; font-size: 12px; color: var(--text-tertiary);">点击查看大图</div>
1520
+ `;
1521
+ }
1522
+
1523
+ messageEl.innerHTML = `
1524
+ <div class="message-header">
1525
+ <span class="message-user">${data.username}</span>
1526
+ <span class="message-time">${new Date(data.timestamp).toLocaleTimeString()}</span>
1527
+ </div>
1528
+ <div class="message-content">${messageContent}</div>
1529
+ `;
1530
+ messagesDiv.appendChild(messageEl);
1531
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1532
+ }
1533
+ });
1534
+
1535
+ // 发送被拦截提示(来自服务端)
1536
+ wsService.on('chat_blocked', (data) => {
1537
+ if (data.groupId === currentGroup._id) {
1538
+ const notification = document.createElement('div');
1539
+ notification.className = 'notification';
1540
+ notification.textContent = data.message || '消息发送失败';
1541
+ messagesDiv.appendChild(notification);
1542
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1543
+ }
1544
+ });
1545
+
1546
+ // 发送消息
1547
+ const sendMessage = () => {
1548
+ const content = messageInput.value.trim();
1549
+ if (content) {
1550
+ wsService.sendChatMessage(currentGroup._id, user.username, content);
1551
+ messageInput.value = '';
1552
+ }
1553
+ };
1554
+
1555
+ sendBtn.addEventListener('click', sendMessage);
1556
+ messageInput.addEventListener('keypress', (e) => {
1557
+ if (e.key === 'Enter') sendMessage();
1558
+ });
1559
+
1560
+ // AI助手按钮
1561
+ document.getElementById('openAIBtn').addEventListener('click', () => {
1562
+ document.getElementById('aiModal').classList.remove('hidden');
1563
+ });
1564
+
1565
+ document.getElementById('closeAIModal').addEventListener('click', () => {
1566
+ document.getElementById('aiModal').classList.add('hidden');
1567
+ });
1568
+
1569
+ // 快捷问题按钮
1570
+ document.querySelectorAll('.quick-question-btn').forEach(btn => {
1571
+ btn.addEventListener('click', () => {
1572
+ document.getElementById('aiInputText').value = btn.dataset.question;
1573
+ document.getElementById('aiSendBtnModal').click();
1574
+ });
1575
+ btn.addEventListener('mouseenter', (e) => {
1576
+ e.target.style.background = 'var(--primary)';
1577
+ e.target.style.color = 'white';
1578
+ e.target.style.transform = 'translateY(-2px)';
1579
+ });
1580
+ btn.addEventListener('mouseleave', (e) => {
1581
+ e.target.style.background = 'var(--bg)';
1582
+ e.target.style.color = 'inherit';
1583
+ e.target.style.transform = 'translateY(0)';
1584
+ });
1585
+ });
1586
+
1587
+ // AI输入框自动调整高度
1588
+ const aiInput = document.getElementById('aiInputText');
1589
+ aiInput.addEventListener('input', () => {
1590
+ aiInput.style.height = 'auto';
1591
+ aiInput.style.height = aiInput.scrollHeight + 'px';
1592
+ });
1593
+
1594
+ // 支持 Enter 发送,Shift+Enter 换行
1595
+ aiInput.addEventListener('keydown', (e) => {
1596
+ if (e.key === 'Enter' && !e.shiftKey) {
1597
+ e.preventDefault();
1598
+ document.getElementById('aiSendBtnModal').click();
1599
+ }
1600
+ });
1601
+
1602
+ // 输入框聚焦效果
1603
+ aiInput.addEventListener('focus', () => {
1604
+ aiInput.style.borderColor = 'var(--primary)';
1605
+ });
1606
+ aiInput.addEventListener('blur', () => {
1607
+ aiInput.style.borderColor = 'var(--border)';
1608
+ });
1609
+
1610
+ // 发送按钮悬停效果
1611
+ const aiSendBtn = document.getElementById('aiSendBtnModal');
1612
+ aiSendBtn.addEventListener('mouseenter', () => {
1613
+ aiSendBtn.style.transform = 'scale(1.05)';
1614
+ });
1615
+ aiSendBtn.addEventListener('mouseleave', () => {
1616
+ aiSendBtn.style.transform = 'scale(1)';
1617
+ });
1618
+
1619
+ document.getElementById('aiSendBtnModal').addEventListener('click', async () => {
1620
+ const input = document.getElementById('aiInputText');
1621
+ const question = input.value.trim();
1622
+ if (!question) return;
1623
+
1624
+ const chatMessages = document.getElementById('aiChatMessages');
1625
+
1626
+ // 显示用户消息
1627
+ const userMsg = document.createElement('div');
1628
+ userMsg.className = 'ai-message user';
1629
+ userMsg.style.cssText = 'background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px 18px; border-radius: 18px 18px 4px 18px; margin: 10px 0; max-width: 75%; margin-left: auto; text-align: right; box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); animation: slideInRight 0.3s ease;';
1630
+ userMsg.textContent = question;
1631
+ chatMessages.appendChild(userMsg);
1632
+ input.value = '';
1633
+
1634
+ // 显示加载中
1635
+ const loadingMsg = document.createElement('div');
1636
+ loadingMsg.className = 'ai-message ai loading';
1637
+ loadingMsg.style.cssText = 'background: var(--bg-tertiary); padding: 12px 16px; border-radius: 12px; margin: 10px 0; max-width: 70%;';
1638
+ loadingMsg.textContent = '思考中...';
1639
+ chatMessages.appendChild(loadingMsg);
1640
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1641
+
1642
+ try {
1643
+ const token = localStorage.getItem('token');
1644
+ const response = await fetch('http://localhost:8765/api/ai/ask', {
1645
+ method: 'POST',
1646
+ headers: {
1647
+ 'Content-Type': 'application/json',
1648
+ 'Authorization': `Bearer ${token}`
1649
+ },
1650
+ body: JSON.stringify({ question, groupId: currentGroup?._id })
1651
+ });
1652
+ const result = await response.json();
1653
+
1654
+ loadingMsg.remove();
1655
+ const aiMsg = document.createElement('div');
1656
+ aiMsg.className = 'ai-message ai';
1657
+ aiMsg.style.cssText = 'background: var(--bg-secondary); padding: 15px 18px; border-radius: 18px 18px 18px 4px; margin: 10px 0; max-width: 75%; border: 1px solid var(--border); box-shadow: 0 2px 4px rgba(0,0,0,0.05); animation: slideInLeft 0.3s ease; line-height: 1.6;';
1658
+ aiMsg.textContent = result.answer || '抱歉,我无法回答这个问题。';
1659
+ chatMessages.appendChild(aiMsg);
1660
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1661
+ } catch (error) {
1662
+ loadingMsg.remove();
1663
+ const errorMsg = document.createElement('div');
1664
+ errorMsg.className = 'ai-message ai error';
1665
+ errorMsg.style.cssText = 'background: var(--danger); color: white; padding: 12px 16px; border-radius: 12px; margin: 10px 0; max-width: 70%;';
1666
+ errorMsg.textContent = '抱歉,发生了错误: ' + error.message;
1667
+ chatMessages.appendChild(errorMsg);
1668
+ chatMessages.scrollTop = chatMessages.scrollHeight;
1669
+ }
1670
+ });
1671
+
1672
+ // 协作白板按钮
1673
+ document.getElementById('openWhiteboardBtn').addEventListener('click', () => {
1674
+ document.getElementById('whiteboardModal').classList.remove('hidden');
1675
+ initWhiteboard();
1676
+ });
1677
+
1678
+ document.getElementById('closeWhiteboardModal').addEventListener('click', () => {
1679
+ document.getElementById('whiteboardModal').classList.add('hidden');
1680
+ });
1681
+
1682
+ function initWhiteboard() {
1683
+ const canvas = document.getElementById('whiteboardCanvas');
1684
+ if (!canvas) return;
1685
+
1686
+ const ctx = canvas.getContext('2d');
1687
+ let isDrawing = false;
1688
+ let currentTool = 'pen';
1689
+ let currentColor = '#000000';
1690
+ let brushSize = 3;
1691
+ let lastX = 0;
1692
+ let lastY = 0;
1693
+
1694
+ // 工具切换
1695
+ document.querySelectorAll('.tool-btn').forEach(btn => {
1696
+ btn.onclick = () => {
1697
+ document.querySelectorAll('.tool-btn').forEach(b => {
1698
+ b.style.background = 'transparent';
1699
+ b.style.borderColor = 'var(--border)';
1700
+ b.style.color = 'inherit';
1701
+ b.classList.remove('active');
1702
+ });
1703
+ btn.style.background = 'var(--primary)';
1704
+ btn.style.borderColor = 'var(--primary)';
1705
+ btn.style.color = 'white';
1706
+ btn.classList.add('active');
1707
+ currentTool = btn.dataset.tool;
1708
+ };
1709
+ });
1710
+
1711
+ const colorPicker = document.getElementById('colorPickerCanvas');
1712
+ if (colorPicker) {
1713
+ colorPicker.onchange = (e) => {
1714
+ currentColor = e.target.value;
1715
+ };
1716
+ }
1717
+
1718
+ const brushSizeInput = document.getElementById('brushSizeCanvas');
1719
+ const brushSizeLabel = document.getElementById('brushSizeLabel');
1720
+ if (brushSizeInput && brushSizeLabel) {
1721
+ brushSizeInput.oninput = (e) => {
1722
+ brushSize = e.target.value;
1723
+ brushSizeLabel.textContent = `大小: ${brushSize}`;
1724
+ };
1725
+ }
1726
+
1727
+ // 绘画功能
1728
+ canvas.onmousedown = (e) => {
1729
+ isDrawing = true;
1730
+ const rect = canvas.getBoundingClientRect();
1731
+ lastX = e.clientX - rect.left;
1732
+ lastY = e.clientY - rect.top;
1733
+ };
1734
+
1735
+ canvas.onmousemove = (e) => {
1736
+ if (!isDrawing) return;
1737
+
1738
+ const rect = canvas.getBoundingClientRect();
1739
+ const x = e.clientX - rect.left;
1740
+ const y = e.clientY - rect.top;
1741
+
1742
+ ctx.beginPath();
1743
+ ctx.moveTo(lastX, lastY);
1744
+ ctx.lineTo(x, y);
1745
+ ctx.strokeStyle = currentTool === 'eraser' ? '#ffffff' : currentColor;
1746
+ ctx.lineWidth = brushSize;
1747
+ ctx.lineCap = 'round';
1748
+ ctx.stroke();
1749
+
1750
+ lastX = x;
1751
+ lastY = y;
1752
+ };
1753
+
1754
+ canvas.onmouseup = () => {
1755
+ isDrawing = false;
1756
+ };
1757
+
1758
+ canvas.onmouseleave = () => {
1759
+ isDrawing = false;
1760
+ };
1761
+
1762
+ // 清空画布
1763
+ document.getElementById('clearCanvasBtn').onclick = () => {
1764
+ if (confirm('确定要清空画布吗?')) {
1765
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1766
+ }
1767
+ };
1768
+
1769
+ // 保存白板
1770
+ document.getElementById('saveCanvasBtn').onclick = () => {
1771
+ const dataURL = canvas.toDataURL('image/png');
1772
+ const link = document.createElement('a');
1773
+ link.download = `whiteboard-${Date.now()}.png`;
1774
+ link.href = dataURL;
1775
+ link.click();
1776
+ alert('白板已保存!');
1777
+ };
1778
+
1779
+ // 发送到群聊
1780
+ document.getElementById('sendToGroupBtn').onclick = async () => {
1781
+ try {
1782
+ const dataURL = canvas.toDataURL('image/png');
1783
+ const blob = await fetch(dataURL).then(r => r.blob());
1784
+
1785
+ // 创建 FormData 上传图片
1786
+ const formData = new FormData();
1787
+ formData.append('file', blob, `whiteboard-${Date.now()}.png`);
1788
+ formData.append('groupId', currentGroup._id);
1789
+ formData.append('description', '协作白板作品');
1790
+
1791
+ const token = localStorage.getItem('token');
1792
+ const response = await fetch('http://localhost:8765/api/files/upload', {
1793
+ method: 'POST',
1794
+ headers: {
1795
+ 'Authorization': `Bearer ${token}`
1796
+ },
1797
+ body: formData
1798
+ });
1799
+
1800
+ if (response.ok) {
1801
+ const result = await response.json();
1802
+ const fileId = result.file._id;
1803
+ const fileUrl = `http://localhost:8765/api/files/${fileId}/download?token=${token}`;
1804
+
1805
+ // 发送包含图片的消息到群聊
1806
+ wsService.sendChatMessage(currentGroup._id, user.username, `[白板作品]${fileUrl}`);
1807
+ alert('白板作品已发送到群聊!');
1808
+ document.getElementById('whiteboardModal').classList.add('hidden');
1809
+ } else {
1810
+ throw new Error('上传失败');
1811
+ }
1812
+ } catch (error) {
1813
+ console.error('发送白板作品错误:', error);
1814
+ alert('发送失败: ' + error.message);
1815
+ }
1816
+ };
1817
+ }
1818
+ }
1819
+
1820
+ async function renderCallView(container) {
1821
+ if (!currentGroup) {
1822
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
1823
+ return;
1824
+ }
1825
+
1826
+ container.innerHTML = `
1827
+ <div class="view-header">
1828
+ <h2>随机点名 - ${currentGroup.name}</h2>
1829
+ </div>
1830
+ <div class="call-panel">
1831
+ <div class="call-controls">
1832
+ <label>点名人数:</label>
1833
+ <input type="number" id="callCount" value="1" min="1" max="10">
1834
+ <button class="btn-primary btn-large" id="randomCallBtn">🎲 开始点名</button>
1835
+ </div>
1836
+ <div id="callResult" class="call-result"></div>
1837
+ </div>
1838
+ `;
1839
+
1840
+ document.getElementById('randomCallBtn').addEventListener('click', async () => {
1841
+ const count = parseInt(document.getElementById('callCount').value);
1842
+ try {
1843
+ const result = await apiService.randomCall(currentGroup._id, count);
1844
+ const callResult = document.getElementById('callResult');
1845
+ callResult.innerHTML = `
1846
+ <h3>点名结果:</h3>
1847
+ <div class="called-members">
1848
+ ${result.calledMembers.map(member => `
1849
+ <div class="member-card">
1850
+ <div class="avatar">${member.username[0].toUpperCase()}</div>
1851
+ <div class="member-name">${member.username}</div>
1852
+ </div>
1853
+ `).join('')}
1854
+ </div>
1855
+ `;
1856
+ } catch (error) {
1857
+ alert('点名失败: ' + error.message);
1858
+ }
1859
+ });
1860
+ }
1861
+
1862
+ async function renderAuditView(container) {
1863
+ container.innerHTML = `
1864
+ <div class="view-header">
1865
+ <h2>操作记录</h2>
1866
+ <div style="display: flex; gap: 10px;">
1867
+ <select id="auditGroupFilter" class="form-select">
1868
+ <option value="">全部群组</option>
1869
+ </select>
1870
+ <select id="auditActionFilter" class="form-select">
1871
+ <option value="">全部操作</option>
1872
+ <option value="document_create">文档创建</option>
1873
+ <option value="document_update">文档更新</option>
1874
+ <option value="document_delete">文档删除</option>
1875
+ <option value="content_edit">内容编辑</option>
1876
+ <option value="title_edit">标题编辑</option>
1877
+ <option value="document_permission_change">权限修改</option>
1878
+ </select>
1879
+ <input type="date" id="startDate" class="form-input" title="开始日期">
1880
+ <input type="date" id="endDate" class="form-input" title="结束日期">
1881
+ <button class="btn-primary" id="applyFilters">筛选</button>
1882
+ <button class="btn-secondary" id="exportLogs">导出</button>
1883
+ </div>
1884
+ </div>
1885
+
1886
+ <div class="audit-stats" id="auditStats">
1887
+ <div class="stat-card">
1888
+ <h3>今日操作</h3>
1889
+ <div class="stat-number" id="todayCount">-</div>
1890
+ </div>
1891
+ <div class="stat-card">
1892
+ <h3>本周操作</h3>
1893
+ <div class="stat-number" id="weekCount">-</div>
1894
+ </div>
1895
+ <div class="stat-card">
1896
+ <h3>活跃用户</h3>
1897
+ <div class="stat-number" id="activeUsers">-</div>
1898
+ </div>
1899
+ </div>
1900
+
1901
+ <div class="audit-logs" id="auditLogs">
1902
+ <div class="loading">加载中...</div>
1903
+ </div>
1904
+
1905
+ <div class="pagination" id="auditPagination" style="display: none;">
1906
+ <button class="btn-secondary" id="prevPage">上一页</button>
1907
+ <span id="pageInfo">第 1 页,共 1 页</span>
1908
+ <button class="btn-secondary" id="nextPage">下一页</button>
1909
+ </div>
1910
+
1911
+ <div id="auditDetailModal" class="modal hidden">
1912
+ <div class="modal-content" style="max-width: 850px; border-radius: 16px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.3);">
1913
+ <div class="modal-header" style="background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%); color: white; padding: 24px 28px; position: relative; overflow: hidden;">
1914
+ <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: url('data:image/svg+xml,<svg width=\"100\" height=\"100\" xmlns=\"http://www.w3.org/2000/svg\"><defs><pattern id=\"grid\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\"><path d=\"M 20 0 L 0 0 0 20\" fill=\"none\" stroke=\"rgba(255,255,255,0.05)\" stroke-width=\"1\"/></pattern></defs><rect width=\"100\" height=\"100\" fill=\"url(%23grid)\" /></svg>'); opacity: 0.3;"></div>
1915
+ <div style="display: flex; align-items: center; justify-content: space-between; position: relative; z-index: 1;">
1916
+ <div style="display: flex; align-items: center; gap: 16px;">
1917
+ <div style="width: 48px; height: 48px; background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);">📋</div>
1918
+ <h3 style="margin: 0; font-size: 22px; font-weight: 700; letter-spacing: -0.5px;">操作详情</h3>
1919
+ </div>
1920
+ <button class="close-btn" id="closeAuditDetail" style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border: none; color: white; width: 40px; height: 40px; border-radius: 10px; cursor: pointer; font-size: 24px; transition: all 0.3s; display: flex; align-items: center; justify-content: center;" onmouseover="this.style.background='rgba(255,255,255,0.25)'" onmouseout="this.style.background='rgba(255,255,255,0.15)'">&times;</button>
1921
+ </div>
1922
+ </div>
1923
+ <div class="modal-body" id="auditDetailContent">
1924
+ </div>
1925
+ </div>
1926
+ </div>
1927
+ `;
1928
+
1929
+ let currentPage = 1;
1930
+ let currentFilters = {};
1931
+
1932
+ // 加载群组列表到筛选器
1933
+ try {
1934
+ const groupsResult = await apiService.getGroups();
1935
+ const groupFilter = document.getElementById('auditGroupFilter');
1936
+ groupsResult.groups.forEach(group => {
1937
+ const option = document.createElement('option');
1938
+ option.value = group._id;
1939
+ option.textContent = group.name;
1940
+ groupFilter.appendChild(option);
1941
+ });
1942
+ } catch (error) {
1943
+ console.error('加载群组列表失败:', error);
1944
+ }
1945
+
1946
+ async function loadAuditLogs(page = 1, filters = {}) {
1947
+ try {
1948
+ const auditLogsDiv = document.getElementById('auditLogs');
1949
+ auditLogsDiv.innerHTML = '<div class="loading">加载中...</div>';
1950
+
1951
+ const options = { page, limit: 20 };
1952
+ const result = await apiService.getAuditLogs(filters, options);
1953
+
1954
+ if (result.logs.length === 0) {
1955
+ auditLogsDiv.innerHTML = '<div class="empty-state">暂无操作记录</div>';
1956
+ document.getElementById('auditPagination').style.display = 'none';
1957
+ return;
1958
+ }
1959
+
1960
+ auditLogsDiv.innerHTML = `
1961
+ <div class="audit-table">
1962
+ <div class="audit-header">
1963
+ <div>时间</div>
1964
+ <div>用户</div>
1965
+ <div>操作</div>
1966
+ <div>资源</div>
1967
+ <div>详情</div>
1968
+ </div>
1969
+ ${result.logs.map(log => `
1970
+ <div class="audit-row" onclick="showAuditDetail('${log._id}')">
1971
+ <div class="audit-time">${new Date(log.createdAt).toLocaleString()}</div>
1972
+ <div class="audit-user">
1973
+ <div class="avatar">${log.user?.username?.[0]?.toUpperCase() || '?'}</div>
1974
+ <span>${log.user?.username || '未知用户'}</span>
1975
+ </div>
1976
+ <div class="audit-action">
1977
+ <span class="action-badge action-${log.action}">${getActionText(log.action)}</span>
1978
+ </div>
1979
+ <div class="audit-resource">${log.resourceTitle || log.resourceId}</div>
1980
+ <div class="audit-description">${log.details?.description || '-'}</div>
1981
+ </div>
1982
+ `).join('')}
1983
+ </div>
1984
+ `;
1985
+
1986
+ // 更新分页
1987
+ const pagination = document.getElementById('auditPagination');
1988
+ const pageInfo = document.getElementById('pageInfo');
1989
+ pageInfo.textContent = `第 ${result.pagination.page} 页,共 ${result.pagination.pages} 页`;
1990
+
1991
+ document.getElementById('prevPage').disabled = result.pagination.page <= 1;
1992
+ document.getElementById('nextPage').disabled = result.pagination.page >= result.pagination.pages;
1993
+
1994
+ pagination.style.display = result.pagination.pages > 1 ? 'flex' : 'none';
1995
+
1996
+ } catch (error) {
1997
+ console.error('加载审计日志失败:', error);
1998
+ document.getElementById('auditLogs').innerHTML =
1999
+ '<div class="error-state">加载失败: ' + error.message + '</div>';
2000
+ }
2001
+ }
2002
+
2003
+ async function loadStats() {
2004
+ try {
2005
+ const today = new Date();
2006
+ const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
2007
+
2008
+ // 获取今日统计
2009
+ const todayStats = await apiService.getAuditSummary({
2010
+ startDate: today.toISOString().split('T')[0],
2011
+ endDate: today.toISOString().split('T')[0]
2012
+ });
2013
+
2014
+ // 获取本周统计
2015
+ const weekStats = await apiService.getAuditSummary({
2016
+ startDate: weekAgo.toISOString().split('T')[0],
2017
+ endDate: today.toISOString().split('T')[0]
2018
+ });
2019
+
2020
+ document.getElementById('todayCount').textContent = todayStats.summary.totalLogs;
2021
+ document.getElementById('weekCount').textContent = weekStats.summary.totalLogs;
2022
+ document.getElementById('activeUsers').textContent = weekStats.summary.topUsers.length;
2023
+
2024
+ } catch (error) {
2025
+ console.error('加载统计信息失败:', error);
2026
+ }
2027
+ }
2028
+
2029
+
2030
+ // 格式化审计描述
2031
+ function formatAuditDescription(log) {
2032
+ const username = log.user?.username || '未知用户';
2033
+ const action = getActionText(log.action);
2034
+ const resourceTitle = log.resourceTitle || log.resourceId;
2035
+
2036
+ let description = '<strong style="color: #6366f1;">' + username + '</strong> ';
2037
+
2038
+ switch(log.action) {
2039
+ case 'document_create':
2040
+ description += '创建了文档 <strong>"' + resourceTitle + '"</strong>';
2041
+ break;
2042
+ case 'document_update':
2043
+ description += '更新了文档 <strong>"' + resourceTitle + '"</strong>';
2044
+ if (log.details?.field) {
2045
+ description += ' 的 <strong>' + getFieldName(log.details.field) + '</strong>';
2046
+ }
2047
+ break;
2048
+ case 'document_delete':
2049
+ description += '删除了文档 <strong>"' + resourceTitle + '"</strong>';
2050
+ break;
2051
+ case 'content_edit':
2052
+ description += '编辑了文档 <strong>"' + resourceTitle + '"</strong> 的内容';
2053
+ if (log.changes) {
2054
+ const insertions = log.changes.insertions?.reduce((sum, ins) => sum + ins.length, 0) || 0;
2055
+ const deletions = log.changes.deletions?.reduce((sum, del) => sum + del.length, 0) || 0;
2056
+ if (insertions > 0 || deletions > 0) {
2057
+ description += ' (<span style="color: #10b981;">+' + insertions + '</span> / <span style="color: #ef4444;">-' + deletions + '</span> 字符)';
2058
+ }
2059
+ }
2060
+ break;
2061
+ case 'title_edit':
2062
+ description += '修改了文档 <strong>"' + resourceTitle + '"</strong> 的标题';
2063
+ if (log.details?.oldValue && log.details?.newValue) {
2064
+ description += ' 从 <strong>"' + log.details.oldValue + '"</strong> 改为 <strong>"' + log.details.newValue + '"</strong>';
2065
+ }
2066
+ break;
2067
+ case 'document_permission_change':
2068
+ description += '修改了文档 <strong>"' + resourceTitle + '"</strong> 的权限设置';
2069
+ break;
2070
+ default:
2071
+ description += '执行了 <strong>' + action + '</strong> 操作';
2072
+ }
2073
+
2074
+ return description;
2075
+ }
2076
+
2077
+ // 获取字段中文名称
2078
+ function getFieldName(field) {
2079
+ const fieldMap = {
2080
+ 'title': '标题',
2081
+ 'content': '内容',
2082
+ 'permissions': '权限',
2083
+ 'status': '状态',
2084
+ 'tags': '标签',
2085
+ 'category': '分类',
2086
+ 'description': '描述'
2087
+ };
2088
+ return fieldMap[field] || field;
2089
+ }
2090
+
2091
+ // 格式化值显示
2092
+ function formatValue(value) {
2093
+ if (value === null || value === undefined) {
2094
+ return '<span style="color: var(--text-tertiary); font-style: italic;">空</span>';
2095
+ }
2096
+
2097
+ if (typeof value === 'object') {
2098
+ return JSON.stringify(value, null, 2);
2099
+ }
2100
+
2101
+ const strValue = String(value);
2102
+
2103
+ // 如果内容太长,截断显示
2104
+ if (strValue.length > 500) {
2105
+ return strValue.substring(0, 500) + '... <span style="color: var(--text-tertiary); font-style: italic;">(内容过长,已截断)</span>';
2106
+ }
2107
+
2108
+ // HTML转义
2109
+ return strValue.replace(/</g, '&lt;').replace(/>/g, '&gt;');
2110
+ }
2111
+
2112
+ // 显示操作详情
2113
+ window.showAuditDetail = async (logId) => {
2114
+ try {
2115
+ const token = localStorage.getItem('token');
2116
+ const response = await fetch(`http://localhost:8765/api/audit/${logId}`, {
2117
+ headers: { 'Authorization': `Bearer ${token}` }
2118
+ });
2119
+ const result = await response.json();
2120
+ const log = result.log;
2121
+
2122
+ const modal = document.getElementById('auditDetailModal');
2123
+ const content = document.getElementById('auditDetailContent');
2124
+
2125
+ // 获取操作类型颜色
2126
+ const getActionColor = (action) => {
2127
+ const colors = {
2128
+ 'create': '#10b981',
2129
+ 'update': '#f59e0b',
2130
+ 'delete': '#ef4444',
2131
+ 'login': '#6366f1',
2132
+ 'logout': '#8b5cf6'
2133
+ };
2134
+ return colors[action] || '#6366f1';
2135
+ };
2136
+
2137
+ content.innerHTML = `
2138
+ <div class="audit-detail" style="padding: 24px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.03) 0%, rgba(168, 85, 247, 0.03) 100%);">
2139
+
2140
+ <!-- 操作信息 -->
2141
+ <div class="detail-section" style="margin-bottom: 18px; padding: 20px; background: var(--bg-secondary); border-radius: 12px; border-left: 4px solid ${getActionColor(log.action)}; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
2142
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
2143
+ <span style="font-size: 24px;">📋</span>
2144
+ <h4 style="margin: 0; color: var(--text-primary); font-size: 16px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">操作信息</h4>
2145
+ </div>
2146
+ <div style="display: grid; grid-template-columns: 140px 1fr; gap: 14px; font-size: 15px;">
2147
+ <span style="color: var(--text-tertiary); font-weight: 500;">操作类型:</span>
2148
+ <span style="display: inline-flex; align-items: center; gap: 8px;">
2149
+ <span style="display: inline-block; padding: 6px 14px; background: ${getActionColor(log.action)}; color: white; border-radius: 8px; font-weight: 600; font-size: 13px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);">${getActionText(log.action)}</span>
2150
+ </span>
2151
+ <span style="color: var(--text-tertiary); font-weight: 500;">操作时间:</span>
2152
+ <span style="font-weight: 600; color: var(--text-primary);">${new Date(log.createdAt).toLocaleString('zh-CN', {
2153
+ year: 'numeric',
2154
+ month: 'long',
2155
+ day: 'numeric',
2156
+ hour: '2-digit',
2157
+ minute: '2-digit',
2158
+ second: '2-digit'
2159
+ })}</span>
2160
+ <span style="color: var(--text-tertiary); font-weight: 500;">操作用户:</span>
2161
+ <span style="display: flex; align-items: center; gap: 10px;">
2162
+ <div class="avatar" style="width: 32px; height: 32px; font-size: 14px; background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);">${log.user?.username?.[0]?.toUpperCase() || '?'}</div>
2163
+ <span style="font-weight: 600; color: var(--text-primary);">${log.user?.username || '未知用户'}</span>
2164
+ </span>
2165
+ </div>
2166
+ </div>
2167
+
2168
+ <!-- 资源信息 -->
2169
+ <div class="detail-section" style="margin-bottom: 18px; padding: 20px; background: var(--bg-secondary); border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
2170
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
2171
+ <span style="font-size: 24px;">📄</span>
2172
+ <h4 style="margin: 0; color: var(--text-primary); font-size: 16px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">资源信息</h4>
2173
+ </div>
2174
+ <div style="display: grid; grid-template-columns: 140px 1fr; gap: 14px; font-size: 15px;">
2175
+ <span style="color: var(--text-tertiary); font-weight: 500;">资源类型:</span>
2176
+ <span style="font-weight: 600; color: var(--text-primary);">${log.resourceType || '未知'}</span>
2177
+ <span style="color: var(--text-tertiary); font-weight: 500;">资源ID:</span>
2178
+ <span style="font-family: 'Courier New', monospace; font-size: 13px; padding: 6px 12px; background: var(--bg); border-radius: 6px; color: var(--text-primary); border: 1px solid var(--border);">${log.resourceId}</span>
2179
+ <span style="color: var(--text-tertiary); font-weight: 500;">资源标题:</span>
2180
+ <span style="font-weight: 600; color: var(--text-primary);">${log.resourceTitle || '<span style="color: var(--text-tertiary); font-style: italic;">无</span>'}</span>
2181
+ </div>
2182
+ </div>
2183
+
2184
+ ${log.details ? `
2185
+ <!-- 详细信息 -->
2186
+ <div class="detail-section" style="margin-bottom: 18px; padding: 20px; background: var(--bg-secondary); border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
2187
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
2188
+ <span style="font-size: 24px;">📝</span>
2189
+ <h4 style="margin: 0; color: var(--text-primary); font-size: 16px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">详细总结</h4>
2190
+ </div>
2191
+ <div style="font-size: 15px; line-height: 1.8; color: var(--text-primary); padding: 16px 20px; background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(168, 85, 247, 0.05) 100%); border-radius: 8px; border-left: 4px solid #6366f1;">
2192
+ <div style="font-weight: 600; margin-bottom: 8px; font-size: 16px;" id="auditDescriptionText">
2193
+ </div>
2194
+ </div>
2195
+
2196
+ <div id="auditDetailsSection" style="margin-top: 16px; padding: 16px; background: var(--bg); border-radius: 8px; border: 1px solid var(--border);">
2197
+ <h5 style="margin: 0 0 12px 0; font-size: 14px; color: var(--text-secondary); font-weight: 600; display: flex; align-items: center; gap: 8px;">
2198
+ <span>ℹ️</span>
2199
+ <span>操作详情</span>
2200
+ </h5>
2201
+ <div id="auditDetailsContent" style="display: grid; grid-template-columns: 120px 1fr; gap: 10px; font-size: 14px;">
2202
+ </div>
2203
+ </div>
2204
+
2205
+ <div id="auditChangesSection" style="margin-top: 16px; padding: 16px; background: var(--bg); border-radius: 8px; border: 1px solid var(--border); display: none;">
2206
+ <h5 style="margin: 0 0 12px 0; font-size: 14px; color: var(--text-secondary); font-weight: 600; display: flex; align-items: center; gap: 8px;">
2207
+ <span>🔄</span>
2208
+ <span>文本变更统计</span>
2209
+ </h5>
2210
+ <div id="auditChangesContent" style="display: flex; gap: 20px; font-size: 14px;">
2211
+ </div>
2212
+ </div>
2213
+ </div>
2214
+
2215
+ <!-- 请求信息 -->
2216
+ <div class="detail-section" style="padding: 20px; background: var(--bg-secondary); border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
2217
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
2218
+ <span style="font-size: 24px;">🌐</span>
2219
+ <h4 style="margin: 0; color: var(--text-primary); font-size: 16px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">请求信息</h4>
2220
+ </div>
2221
+ <div style="display: grid; grid-template-columns: 140px 1fr; gap: 14px; font-size: 15px;">
2222
+ <span style="color: var(--text-tertiary); font-weight: 500;">IP地址:</span>
2223
+ <span style="font-family: 'Courier New', monospace; font-weight: 600; color: var(--text-primary);">${log.ipAddress || '<span style="color: var(--text-tertiary); font-style: italic;">未记录</span>'}</span>
2224
+ <span style="color: var(--text-tertiary); font-weight: 500;">用户代理:</span>
2225
+ <span style="font-size: 13px; word-break: break-all; color: var(--text-secondary); line-height: 1.6;">${log.userAgent || '<span style="color: var(--text-tertiary); font-style: italic;">未记录</span>'}</span>
2226
+ </div>
2227
+ </div>
2228
+ </div>
2229
+ `;
2230
+
2231
+
2232
+ // 填充详细描述
2233
+ document.getElementById('auditDescriptionText').innerHTML = formatAuditDescription(log);
2234
+
2235
+ // 填充操作详情
2236
+ const detailsContent = document.getElementById('auditDetailsContent');
2237
+ let detailsHTML = '';
2238
+
2239
+ if (log.details) {
2240
+ if (log.details.field) {
2241
+ detailsHTML += '<span style="color: var(--text-tertiary); font-weight: 500;">修改字段:</span>';
2242
+ detailsHTML += '<span style="font-weight: 600; color: var(--text-primary);">' + getFieldName(log.details.field) + '</span>';
2243
+ }
2244
+
2245
+ if (log.details.oldValue !== undefined && log.details.oldValue !== null) {
2246
+ detailsHTML += '<span style="color: var(--text-tertiary); font-weight: 500;">原始值:</span>';
2247
+ detailsHTML += '<div style="padding: 8px 12px; background: rgba(239, 68, 68, 0.1); border-radius: 6px; border-left: 3px solid #ef4444; font-family: \'Courier New\', monospace; font-size: 13px; word-break: break-all; max-height: 200px; overflow-y: auto;">' + formatValue(log.details.oldValue) + '</div>';
2248
+ }
2249
+
2250
+ if (log.details.newValue !== undefined && log.details.newValue !== null) {
2251
+ detailsHTML += '<span style="color: var(--text-tertiary); font-weight: 500;">新值:</span>';
2252
+ detailsHTML += '<div style="padding: 8px 12px; background: rgba(16, 185, 129, 0.1); border-radius: 6px; border-left: 3px solid #10b981; font-family: \'Courier New\', monospace; font-size: 13px; word-break: break-all; max-height: 200px; overflow-y: auto;">' + formatValue(log.details.newValue) + '</div>';
2253
+ }
2254
+ }
2255
+
2256
+ if (detailsHTML) {
2257
+ detailsContent.innerHTML = detailsHTML;
2258
+ document.getElementById('auditDetailsSection').style.display = 'block';
2259
+ } else {
2260
+ document.getElementById('auditDetailsSection').style.display = 'none';
2261
+ }
2262
+
2263
+ // 填充变更统计
2264
+ if (log.changes && (log.changes.insertions?.length > 0 || log.changes.deletions?.length > 0)) {
2265
+ const changesContent = document.getElementById('auditChangesContent');
2266
+ let changesHTML = '';
2267
+
2268
+ if (log.changes.insertions && log.changes.insertions.length > 0) {
2269
+ const insertCount = log.changes.insertions.reduce((sum, ins) => sum + ins.length, 0);
2270
+ changesHTML += '<div style="flex: 1; padding: 12px; background: rgba(16, 185, 129, 0.1); border-radius: 8px; border-left: 3px solid #10b981;">';
2271
+ changesHTML += '<div style="font-weight: 600; color: #10b981; margin-bottom: 4px;">✅ 新增内容</div>';
2272
+ changesHTML += '<div style="color: var(--text-secondary);">共 ' + insertCount + ' 个字符</div>';
2273
+ changesHTML += '</div>';
2274
+ }
2275
+
2276
+ if (log.changes.deletions && log.changes.deletions.length > 0) {
2277
+ const deleteCount = log.changes.deletions.reduce((sum, del) => sum + del.length, 0);
2278
+ changesHTML += '<div style="flex: 1; padding: 12px; background: rgba(239, 68, 68, 0.1); border-radius: 8px; border-left: 3px solid #ef4444;">';
2279
+ changesHTML += '<div style="font-weight: 600; color: #ef4444; margin-bottom: 4px;">❌ 删除内容</div>';
2280
+ changesHTML += '<div style="color: var(--text-secondary);">共 ' + deleteCount + ' 个字符</div>';
2281
+ changesHTML += '</div>';
2282
+ }
2283
+
2284
+ changesContent.innerHTML = changesHTML;
2285
+ document.getElementById('auditChangesSection').style.display = 'block';
2286
+ } else {
2287
+ document.getElementById('auditChangesSection').style.display = 'none';
2288
+ }
2289
+
2290
+ modal.classList.remove('hidden');
2291
+ } catch (error) {
2292
+ alert('加载详情失败: ' + error.message);
2293
+ }
2294
+ };
2295
+
2296
+ // 事件监听
2297
+ document.getElementById('applyFilters').addEventListener('click', () => {
2298
+ currentFilters = {
2299
+ groupId: document.getElementById('auditGroupFilter').value,
2300
+ action: document.getElementById('auditActionFilter').value,
2301
+ startDate: document.getElementById('startDate').value,
2302
+ endDate: document.getElementById('endDate').value
2303
+ };
2304
+
2305
+ // 移除空值
2306
+ Object.keys(currentFilters).forEach(key => {
2307
+ if (!currentFilters[key]) {
2308
+ delete currentFilters[key];
2309
+ }
2310
+ });
2311
+
2312
+ currentPage = 1;
2313
+ loadAuditLogs(currentPage, currentFilters);
2314
+ });
2315
+
2316
+ document.getElementById('prevPage').addEventListener('click', () => {
2317
+ if (currentPage > 1) {
2318
+ currentPage--;
2319
+ loadAuditLogs(currentPage, currentFilters);
2320
+ }
2321
+ });
2322
+
2323
+ document.getElementById('nextPage').addEventListener('click', () => {
2324
+ currentPage++;
2325
+ loadAuditLogs(currentPage, currentFilters);
2326
+ });
2327
+
2328
+ document.getElementById('exportLogs').addEventListener('click', () => {
2329
+ alert('导出功能开发中...');
2330
+ });
2331
+
2332
+ document.getElementById('closeAuditDetail').addEventListener('click', () => {
2333
+ document.getElementById('auditDetailModal').classList.add('hidden');
2334
+ });
2335
+
2336
+ // 初始加载
2337
+ loadStats();
2338
+ loadAuditLogs();
2339
+ }
2340
+
2341
+ async function renderSearchView(container) {
2342
+ container.innerHTML = `
2343
+ <div class="view-header">
2344
+ <h2>🔍 搜索</h2>
2345
+ </div>
2346
+ <div class="search-container">
2347
+ <div class="search-box">
2348
+ <input type="text" id="searchInput" placeholder="搜索消息、文档、任务...">
2349
+ <button class="btn-primary" id="searchBtn">搜索</button>
2350
+ </div>
2351
+ <div class="search-filters">
2352
+ <label>
2353
+ <input type="checkbox" id="filterMessages" checked> 消息
2354
+ </label>
2355
+ <label>
2356
+ <input type="checkbox" id="filterDocuments" checked> 文档
2357
+ </label>
2358
+ <label>
2359
+ <input type="checkbox" id="filterTasks" checked> 任务
2360
+ </label>
2361
+ </div>
2362
+ <div class="search-results" id="searchResults"></div>
2363
+ </div>
2364
+ `;
2365
+
2366
+ const searchInput = document.getElementById('searchInput');
2367
+ const searchBtn = document.getElementById('searchBtn');
2368
+ const searchResults = document.getElementById('searchResults');
2369
+
2370
+ const performSearch = async () => {
2371
+ const query = searchInput.value.trim();
2372
+ if (!query) {
2373
+ searchResults.innerHTML = '<div class="empty-state">请输入搜索关键词</div>';
2374
+ return;
2375
+ }
2376
+
2377
+ const filters = {
2378
+ messages: document.getElementById('filterMessages').checked,
2379
+ documents: document.getElementById('filterDocuments').checked,
2380
+ tasks: document.getElementById('filterTasks').checked
2381
+ };
2382
+
2383
+ searchResults.innerHTML = '<div class="loading">搜索中...</div>';
2384
+
2385
+ try {
2386
+ const results = [];
2387
+
2388
+ // 搜索消息
2389
+ if (filters.messages && currentGroup) {
2390
+ try {
2391
+ const messagesResult = await apiService.getGroupMessages(currentGroup._id);
2392
+ if (messagesResult.messages) {
2393
+ const matchedMessages = messagesResult.messages.filter(msg =>
2394
+ msg.content.toLowerCase().includes(query.toLowerCase())
2395
+ );
2396
+ matchedMessages.forEach(msg => {
2397
+ results.push({
2398
+ type: 'message',
2399
+ title: `消息 - ${msg.username}`,
2400
+ content: msg.content,
2401
+ time: msg.timestamp,
2402
+ group: currentGroup.name
2403
+ });
2404
+ });
2405
+ }
2406
+ } catch (err) {
2407
+ console.error('搜索消息失败:', err);
2408
+ }
2409
+ }
2410
+
2411
+ // 搜索文档
2412
+ if (filters.documents) {
2413
+ try {
2414
+ if (currentGroup) {
2415
+ const docsResult = await apiService.getDocuments(currentGroup._id);
2416
+ if (docsResult.documents) {
2417
+ const matchedDocs = docsResult.documents.filter(doc =>
2418
+ doc.title.toLowerCase().includes(query.toLowerCase()) ||
2419
+ doc.content.toLowerCase().includes(query.toLowerCase())
2420
+ );
2421
+ matchedDocs.forEach(doc => {
2422
+ results.push({
2423
+ type: 'document',
2424
+ title: doc.title,
2425
+ content: doc.content.substring(0, 200),
2426
+ time: doc.updatedAt,
2427
+ id: doc._id,
2428
+ group: currentGroup.name
2429
+ });
2430
+ });
2431
+ }
2432
+ }
2433
+ } catch (err) {
2434
+ console.error('搜索文档失败:', err);
2435
+ }
2436
+ }
2437
+
2438
+ // 搜索任务
2439
+ if (filters.tasks && currentGroup) {
2440
+ try {
2441
+ const tasksResult = await apiService.getTasks(currentGroup._id);
2442
+ if (tasksResult.tasks) {
2443
+ const matchedTasks = tasksResult.tasks.filter(task =>
2444
+ task.title.toLowerCase().includes(query.toLowerCase()) ||
2445
+ (task.description && task.description.toLowerCase().includes(query.toLowerCase()))
2446
+ );
2447
+ matchedTasks.forEach(task => {
2448
+ results.push({
2449
+ type: 'task',
2450
+ title: task.title,
2451
+ content: task.description || '',
2452
+ time: task.updatedAt,
2453
+ id: task._id,
2454
+ status: task.status,
2455
+ group: currentGroup.name
2456
+ });
2457
+ });
2458
+ }
2459
+ } catch (err) {
2460
+ console.error('搜索任务失败:', err);
2461
+ }
2462
+ }
2463
+
2464
+ // 显示结果
2465
+ if (results.length === 0) {
2466
+ searchResults.innerHTML = '<div class="empty-state">未找到相关结果</div>';
2467
+ } else {
2468
+ searchResults.innerHTML = results.map(result => {
2469
+ const typeIcon = {
2470
+ message: '💬',
2471
+ document: '📄',
2472
+ task: '📋'
2473
+ };
2474
+ return `
2475
+ <div class="search-result-item">
2476
+ <div class="result-header">
2477
+ <span class="result-type">${typeIcon[result.type]} ${result.type === 'message' ? '消息' : result.type === 'document' ? '文档' : '任务'}</span>
2478
+ <span class="result-time">${new Date(result.time).toLocaleString()}</span>
2479
+ </div>
2480
+ <h4>${highlightText(result.title, query)}</h4>
2481
+ <p>${highlightText(result.content, query)}</p>
2482
+ ${result.group ? `<span class="result-group">群组: ${result.group}</span>` : ''}
2483
+ ${result.status ? `<span class="result-status">状态: ${getStatusText(result.status)}</span>` : ''}
2484
+ </div>
2485
+ `;
2486
+ }).join('');
2487
+ }
2488
+ } catch (error) {
2489
+ searchResults.innerHTML = `<div class="empty-state">搜索失败: ${error.message}</div>`;
2490
+ }
2491
+ };
2492
+
2493
+ searchBtn.addEventListener('click', performSearch);
2494
+ searchInput.addEventListener('keypress', (e) => {
2495
+ if (e.key === 'Enter') performSearch();
2496
+ });
2497
+ }
2498
+
2499
+ function highlightText(text, query) {
2500
+ if (!query) return text;
2501
+ const regex = new RegExp(`(${query})`, 'gi');
2502
+ return text.replace(regex, '<mark>$1</mark>');
2503
+ }
2504
+
2505
+ function getStatusText(status) {
2506
+ const statusMap = {
2507
+ 'pending': '待处理',
2508
+ 'in_progress': '进行中',
2509
+ 'completed': '已完成',
2510
+ 'terminated': '已终止'
2511
+ };
2512
+ return statusMap[status] || status;
2513
+ }
2514
+
2515
+ function getActionText(action) {
2516
+ const actionMap = {
2517
+ 'document_create': '创建文档',
2518
+ 'document_update': '更新文档',
2519
+ 'document_delete': '删除文档',
2520
+ 'content_edit': '编辑内容',
2521
+ 'title_edit': '修改标题',
2522
+ 'document_permission_change': '权限修改'
2523
+ };
2524
+ return actionMap[action] || action;
2525
+ }
2526
+
2527
+ function getStatusText(status) {
2528
+ const statusMap = {
2529
+ 'pending': '待处理',
2530
+ 'in_progress': '进行中',
2531
+ 'completed': '已完成',
2532
+ 'terminated': '已终止'
2533
+ };
2534
+ return statusMap[status] || status;
2535
+ }
2536
+
2537
+
2538
+ // 知识库管理
2539
+ async function renderKnowledgeView(container) {
2540
+ if (!currentGroup) {
2541
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
2542
+ return;
2543
+ }
2544
+
2545
+ try {
2546
+ const token = localStorage.getItem('token');
2547
+ const response = await fetch(`http://localhost:8765/api/knowledge/group/${currentGroup._id}`, {
2548
+ headers: { 'Authorization': `Bearer ${token}` }
2549
+ });
2550
+
2551
+ if (!response.ok) {
2552
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
2553
+ }
2554
+
2555
+ const result = await response.json();
2556
+ console.log('知识库数据:', result);
2557
+
2558
+ // 后端返回的是 { success: true, data: { knowledgeList: [...], pagination: {...} } }
2559
+ const knowledgeItems = result.data?.knowledgeList || [];
2560
+ console.log('知识库条目数量:', knowledgeItems.length);
2561
+
2562
+ container.innerHTML = `
2563
+ <div class="view-header">
2564
+ <h2>📚 知识库管理 - ${currentGroup.name}</h2>
2565
+ <button class="btn-primary" id="createKnowledgeBtn">➕ 创建知识条目</button>
2566
+ </div>
2567
+ <div class="knowledge-grid" id="knowledgeList" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; padding: 20px;"></div>
2568
+ <div id="knowledgeModal" class="modal hidden">
2569
+ <div class="modal-content">
2570
+ <h3 id="modalTitle">创建知识条目</h3>
2571
+ <form id="knowledgeForm">
2572
+ <div class="form-group">
2573
+ <label>📌 标题</label>
2574
+ <input type="text" name="title" required style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;">
2575
+ </div>
2576
+ <div class="form-group">
2577
+ <label>📝 内容</label>
2578
+ <textarea name="content" rows="6" required style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;"></textarea>
2579
+ </div>
2580
+ <div class="form-group">
2581
+ <label>🏷️ 标签(用逗号分隔)</label>
2582
+ <input type="text" name="tags" placeholder="例如: 技术,文档,教程" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;">
2583
+ </div>
2584
+ <div class="form-group" style="display: flex; align-items: center; gap: 10px; padding: 15px; background: var(--bg-tertiary); border-radius: 8px; margin-top: 15px;">
2585
+ <input type="checkbox" name="isShared" id="isSharedCheckbox" style="width: 20px; height: 20px; cursor: pointer;">
2586
+ <label for="isSharedCheckbox" style="margin: 0; cursor: pointer; display: flex; align-items: center; gap: 8px;">
2587
+ <span style="font-size: 18px;">🌐</span>
2588
+ <div>
2589
+ <div style="font-weight: 600; color: var(--text-primary);">共享到所有群组</div>
2590
+ <div style="font-size: 12px; color: var(--text-secondary); margin-top: 2px;">开启后,此知识条目将对所有群组可见</div>
2591
+ </div>
2592
+ </label>
2593
+ </div>
2594
+ <div style="display: flex; gap: 10px; margin-top: 20px;">
2595
+ <button type="submit" class="btn-primary" style="flex: 1;">保存</button>
2596
+ <button type="button" class="btn-secondary" id="closeKnowledgeModal" style="flex: 1;">取消</button>
2597
+ </div>
2598
+ </form>
2599
+ </div>
2600
+ </div>
2601
+ `;
2602
+
2603
+ const knowledgeList = document.getElementById('knowledgeList');
2604
+ if (knowledgeItems.length === 0) {
2605
+ knowledgeList.innerHTML = '<div class="empty-state" style="grid-column: 1/-1;">暂无知识条目</div>';
2606
+ } else {
2607
+ knowledgeItems.forEach(item => {
2608
+ const card = document.createElement('div');
2609
+ card.className = 'knowledge-card';
2610
+ card.style.cssText = 'background: var(--bg-secondary); padding: 20px; border-radius: 12px; border: 1px solid var(--border); transition: transform 0.2s, box-shadow 0.2s; position: relative;';
2611
+ card.innerHTML = `
2612
+ ${item.isShared ? '<div style="position: absolute; top: 15px; right: 15px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; display: flex; align-items: center; gap: 4px;"><span>🌐</span><span>已共享</span></div>' : ''}
2613
+ <h3 style="margin: 0 0 10px 0; font-size: 18px; ${item.isShared ? 'padding-right: 80px;' : ''}">${item.title}</h3>
2614
+ <p style="color: var(--text-secondary); margin: 0 0 15px 0; line-height: 1.6;">${item.content.substring(0, 150)}${item.content.length > 150 ? '...' : ''}</p>
2615
+ <div class="knowledge-meta" style="font-size: 12px; color: var(--text-tertiary); margin-bottom: 10px;">
2616
+ <span>👤 ${item.author?.username || '未知'}</span>
2617
+ <span style="margin-left: 15px;">📅 ${new Date(item.createdAt).toLocaleDateString()}</span>
2618
+ </div>
2619
+ ${item.tags && item.tags.length > 0 ? `
2620
+ <div class="tags" style="display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 15px;">
2621
+ ${item.tags.map(tag => `<span class="tag" style="background: var(--primary); color: white; padding: 4px 10px; border-radius: 12px; font-size: 12px;">${tag}</span>`).join('')}
2622
+ </div>
2623
+ ` : ''}
2624
+ <div style="display: flex; gap: 10px;">
2625
+ <button class="btn-secondary btn-sm" data-id="${item._id}" data-action="edit" style="flex: 1;">✏️ 编辑</button>
2626
+ <button class="btn-danger btn-sm" data-id="${item._id}" data-action="delete" style="flex: 1;">🗑️ 删除</button>
2627
+ </div>
2628
+ `;
2629
+ card.onmouseenter = () => {
2630
+ card.style.transform = 'translateY(-4px)';
2631
+ card.style.boxShadow = '0 8px 16px rgba(0,0,0,0.1)';
2632
+ };
2633
+ card.onmouseleave = () => {
2634
+ card.style.transform = 'translateY(0)';
2635
+ card.style.boxShadow = 'none';
2636
+ };
2637
+ knowledgeList.appendChild(card);
2638
+ });
2639
+
2640
+ document.querySelectorAll('[data-action="edit"]').forEach(btn => {
2641
+ btn.addEventListener('click', async () => {
2642
+ const item = knowledgeItems.find(k => k._id === btn.dataset.id);
2643
+ document.getElementById('modalTitle').textContent = '编辑知识条目';
2644
+ document.querySelector('[name="title"]').value = item.title;
2645
+ document.querySelector('[name="content"]').value = item.content;
2646
+ document.querySelector('[name="tags"]').value = item.tags?.join(', ') || '';
2647
+ document.getElementById('isSharedCheckbox').checked = item.isShared || false;
2648
+ document.getElementById('knowledgeForm').dataset.editId = item._id;
2649
+ document.getElementById('knowledgeModal').classList.remove('hidden');
2650
+ });
2651
+ });
2652
+
2653
+ document.querySelectorAll('[data-action="download"]').forEach(btn => {
2654
+ btn.addEventListener('click', async () => {
2655
+ try {
2656
+ const response = await fetch(`http://localhost:8765/api/backup/download/${btn.dataset.filename}`, {
2657
+ method: 'GET',
2658
+ headers: { 'Authorization': `Bearer ${token}` }
2659
+ });
2660
+
2661
+ if (!response.ok) {
2662
+ throw new Error('下载失败');
2663
+ }
2664
+
2665
+ const blob = await response.blob();
2666
+ const url = window.URL.createObjectURL(blob);
2667
+ const a = document.createElement('a');
2668
+ a.href = url;
2669
+ a.download = btn.dataset.filename;
2670
+ document.body.appendChild(a);
2671
+ a.click();
2672
+ window.URL.revokeObjectURL(url);
2673
+ document.body.removeChild(a);
2674
+ } catch (error) {
2675
+ alert('下载失败: ' + error.message);
2676
+ }
2677
+ });
2678
+ });
2679
+
2680
+ document.querySelectorAll('[data-action="delete"]').forEach(btn => {
2681
+ btn.addEventListener('click', async () => {
2682
+ if (confirm('确定要删除这个知识条目吗?')) {
2683
+ try {
2684
+ await fetch(`http://localhost:8765/api/knowledge/${btn.dataset.id}`, {
2685
+ method: 'DELETE',
2686
+ headers: { 'Authorization': `Bearer ${token}` }
2687
+ });
2688
+ alert('删除成功!');
2689
+ await renderKnowledgeView(container);
2690
+ } catch (error) {
2691
+ alert('删除失败: ' + error.message);
2692
+ }
2693
+ }
2694
+ });
2695
+ });
2696
+ }
2697
+
2698
+ document.getElementById('createKnowledgeBtn').addEventListener('click', () => {
2699
+ document.getElementById('modalTitle').textContent = '创建知识条目';
2700
+ document.getElementById('knowledgeForm').reset();
2701
+ delete document.getElementById('knowledgeForm').dataset.editId;
2702
+ document.getElementById('knowledgeModal').classList.remove('hidden');
2703
+ });
2704
+
2705
+ document.getElementById('closeKnowledgeModal').addEventListener('click', () => {
2706
+ document.getElementById('knowledgeModal').classList.add('hidden');
2707
+ });
2708
+
2709
+ document.getElementById('knowledgeForm').addEventListener('submit', async (e) => {
2710
+ e.preventDefault();
2711
+ const formData = new FormData(e.target);
2712
+ const data = {
2713
+ title: formData.get('title'),
2714
+ content: formData.get('content'),
2715
+ tags: formData.get('tags').split(',').map(t => t.trim()).filter(t => t),
2716
+ groupId: currentGroup._id,
2717
+ isShared: document.getElementById('isSharedCheckbox').checked
2718
+ };
2719
+
2720
+ try {
2721
+ const editId = e.target.dataset.editId;
2722
+ const url = editId
2723
+ ? `http://localhost:8765/api/knowledge/${editId}`
2724
+ : 'http://localhost:8765/api/knowledge';
2725
+ const method = editId ? 'PUT' : 'POST';
2726
+
2727
+ const response = await fetch(url, {
2728
+ method,
2729
+ headers: {
2730
+ 'Content-Type': 'application/json',
2731
+ 'Authorization': `Bearer ${token}`
2732
+ },
2733
+ body: JSON.stringify(data)
2734
+ });
2735
+
2736
+ if (!response.ok) {
2737
+ const errorData = await response.json();
2738
+ throw new Error(errorData.message || '操作失败');
2739
+ }
2740
+
2741
+ const result = await response.json();
2742
+ console.log('知识库操作结果:', result);
2743
+
2744
+ alert(editId ? '更新成功!' : '创建成功!');
2745
+ document.getElementById('knowledgeModal').classList.add('hidden');
2746
+
2747
+ // 刷新知识库列表
2748
+ await renderKnowledgeView(container);
2749
+ } catch (error) {
2750
+ console.error('知识库操作错误:', error);
2751
+ alert('操作失败: ' + error.message);
2752
+ }
2753
+ });
2754
+ } catch (error) {
2755
+ container.innerHTML = `<div class="empty-state">加载失败: ${error.message}</div>`;
2756
+ }
2757
+ }
2758
+
2759
+ // 工作流管理
2760
+
2761
+ // 格式化工作流触发条件
2762
+ function formatWorkflowTrigger(trigger) {
2763
+ if (!trigger) return '未设置';
2764
+
2765
+ // 如果是字符串,直接转换
2766
+ if (typeof trigger === 'string') {
2767
+ const triggerNames = {
2768
+ 'document_create': '📄 文档创建时',
2769
+ 'document_update': '✏️ 文档更新时',
2770
+ 'document_delete': '🗑️ 文档删除时',
2771
+ 'task_create': '📋 任务创建时',
2772
+ 'task_complete': '✅ 任务完成时',
2773
+ 'task_overdue': '⏰ 任务逾期时',
2774
+ 'member_join': '👥 成员加入时',
2775
+ 'group_create': '🏢 群组创建时',
2776
+ 'scheduled': '⏱️ 定时触发',
2777
+ 'manual': '🖱️ 手动触发'
2778
+ };
2779
+ return triggerNames[trigger] || trigger;
2780
+ }
2781
+
2782
+ // 如果是对象,解析详细信息
2783
+ const parts = [];
2784
+
2785
+ if (trigger.event) {
2786
+ const eventNames = {
2787
+ 'document_created': '📄 文档创建',
2788
+ 'document_updated': '✏️ 文档更新',
2789
+ 'document_deleted': '🗑️ 文档删除',
2790
+ 'task_created': '📋 任务创建',
2791
+ 'task_completed': '✅ 任务完成',
2792
+ 'task_overdue': '⏰ 任务逾期',
2793
+ 'member_joined': '👥 成员加入',
2794
+ 'group_created': '🏢 群组创建',
2795
+ 'message_sent': '💬 消息发送',
2796
+ 'file_uploaded': '📎 文件上传'
2797
+ };
2798
+ parts.push(eventNames[trigger.event] || trigger.event);
2799
+ }
2800
+
2801
+ if (trigger.conditions && Object.keys(trigger.conditions).length > 0) {
2802
+ const conditions = [];
2803
+ for (const [key, value] of Object.entries(trigger.conditions)) {
2804
+ const conditionNames = {
2805
+ 'group': '群组',
2806
+ 'user': '用户',
2807
+ 'keyword': '关键词',
2808
+ 'status': '状态',
2809
+ 'priority': '优先级'
2810
+ };
2811
+ const conditionName = conditionNames[key] || key;
2812
+ conditions.push(`${conditionName}=${value}`);
2813
+ }
2814
+ if (conditions.length > 0) {
2815
+ parts.push(`(条件: ${conditions.join(', ')})`);
2816
+ }
2817
+ }
2818
+
2819
+ if (trigger.schedule) {
2820
+ parts.push(`⏱️ 定时: ${trigger.schedule}`);
2821
+ }
2822
+
2823
+ return parts.length > 0 ? parts.join(' ') : '自定义触发条件';
2824
+ }
2825
+
2826
+ async function renderWorkflowView(container) {
2827
+ if (!currentGroup) {
2828
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
2829
+ return;
2830
+ }
2831
+
2832
+ try {
2833
+ const token = localStorage.getItem('token');
2834
+ const response = await fetch(`http://localhost:8765/api/workflows/group/${currentGroup._id}`, {
2835
+ headers: { 'Authorization': `Bearer ${token}` }
2836
+ });
2837
+ const result = await response.json();
2838
+ const workflows = result.data?.workflows || [];
2839
+
2840
+ container.innerHTML = `
2841
+ <div class="view-header">
2842
+ <h2>⚙️ 工作流管理 - ${currentGroup.name}</h2>
2843
+ <button class="btn-primary" id="createWorkflowBtn">➕ 创建工作流</button>
2844
+ </div>
2845
+ <div class="workflow-grid" id="workflowList" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; padding: 20px;"></div>
2846
+ <div id="workflowModal" class="modal hidden">
2847
+ <div class="modal-content" style="max-width: 700px;">
2848
+ <div class="modal-header" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; padding: 20px; border-radius: 12px 12px 0 0; margin: -20px -20px 20px -20px;">
2849
+ <h3 style="margin: 0; display: flex; align-items: center; gap: 10px;">
2850
+ <span style="font-size: 24px;">⚙️</span>
2851
+ <span>创建工作流</span>
2852
+ </h3>
2853
+ </div>
2854
+ <form id="workflowForm">
2855
+ <div class="form-group" style="margin-bottom: 20px;">
2856
+ <label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary);">⚙️ 工作流名称</label>
2857
+ <input type="text" name="name" required placeholder="例如:文档审批流程" style="width: 100%; padding: 12px; border: 2px solid var(--border); border-radius: 8px; font-size: 14px; transition: border-color 0.2s;">
2858
+ </div>
2859
+ <div class="form-group" style="margin-bottom: 20px;">
2860
+ <label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary);">📝 描述</label>
2861
+ <textarea name="description" rows="3" placeholder="描述这个工作流的用途..." style="width: 100%; padding: 12px; border: 2px solid var(--border); border-radius: 8px; font-size: 14px; resize: vertical; transition: border-color 0.2s;"></textarea>
2862
+ </div>
2863
+ <div class="form-group" style="margin-bottom: 20px;">
2864
+ <label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary);">⚡ 触发条件</label>
2865
+ <select name="trigger" style="width: 100%; padding: 12px; border: 2px solid var(--border); border-radius: 8px; font-size: 14px; cursor: pointer; transition: border-color 0.2s;">
2866
+ <option value="document_create">📄 文档创建时</option>
2867
+ <option value="document_update">✏️ 文档更新时</option>
2868
+ <option value="document_delete">🗑️ 文档删除时</option>
2869
+ <option value="task_create">📋 任务创建时</option>
2870
+ <option value="task_complete">✅ 任务完成时</option>
2871
+ <option value="task_overdue">⏰ 任务逾期时</option>
2872
+ <option value="member_join">👥 成员加入时</option>
2873
+ <option value="group_create">🏢 群组创建时</option>
2874
+ <option value="scheduled">⏱️ 定时触发</option>
2875
+ <option value="manual">🖱️ 手动触发</option>
2876
+ </select>
2877
+ </div>
2878
+ <div class="form-group" style="margin-bottom: 20px;">
2879
+ <label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary);">🎯 执行操作</label>
2880
+ <select name="action" style="width: 100%; padding: 12px; border: 2px solid var(--border); border-radius: 8px; font-size: 14px; cursor: pointer; transition: border-color 0.2s;">
2881
+ <option value="send_notification">📧 发送通知</option>
2882
+ <option value="send_email">✉️ 发送邮件</option>
2883
+ <option value="create_task">📋 创建任务</option>
2884
+ <option value="update_status">🔄 更新状态</option>
2885
+ <option value="assign_permission">🔐 分配权限</option>
2886
+ <option value="backup_data">💾 备份数据</option>
2887
+ <option value="generate_report">📊 生成报告</option>
2888
+ <option value="call_api">🔗 调用外部API</option>
2889
+ </select>
2890
+ </div>
2891
+ <div class="form-group" style="margin-bottom: 20px;">
2892
+ <label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary);">⚙️ 操作参数 (JSON格式)</label>
2893
+ <textarea name="actionParams" rows="4" placeholder='{"message": "任务已完成", "recipients": ["user1"]}' style="width: 100%; padding: 12px; border: 2px solid var(--border); border-radius: 8px; font-size: 13px; font-family: monospace; resize: vertical; transition: border-color 0.2s;"></textarea>
2894
+ <div style="margin-top: 8px; font-size: 12px; color: var(--text-tertiary);">
2895
+ 💡 提示:使用 JSON 格式配置操作参数
2896
+ </div>
2897
+ </div>
2898
+ <div style="display: flex; gap: 12px; margin-top: 25px;">
2899
+ <button type="submit" class="btn-primary" style="flex: 1; padding: 12px; border-radius: 8px; font-weight: 600; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border: none; cursor: pointer; transition: transform 0.2s;">创建</button>
2900
+ <button type="button" class="btn-secondary" id="closeWorkflowModal" style="flex: 1; padding: 12px; border-radius: 8px; font-weight: 600;">取消</button>
2901
+ </div>
2902
+ </form>
2903
+ </div>
2904
+ </div>
2905
+ `;
2906
+
2907
+ const workflowList = document.getElementById('workflowList');
2908
+ if (workflows.length === 0) {
2909
+ workflowList.innerHTML = '<div class="empty-state" style="grid-column: 1/-1;">暂无工作流</div>';
2910
+ } else {
2911
+ workflows.forEach(workflow => {
2912
+ const card = document.createElement('div');
2913
+ card.className = 'workflow-card';
2914
+ card.style.cssText = 'background: var(--bg-secondary); padding: 20px; border-radius: 12px; border: 1px solid var(--border); transition: transform 0.2s, box-shadow 0.2s;';
2915
+ const isActive = workflow.status === 'active';
2916
+ card.innerHTML = `
2917
+ <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;">
2918
+ <h3 style="margin: 0; font-size: 18px;">${workflow.name}</h3>
2919
+ <span style="padding: 4px 10px; border-radius: 12px; font-size: 12px; background: ${isActive ? 'var(--success)' : 'var(--warning)'}; color: white;">${isActive ? '✅ 活跃' : '⏸️ 暂停'}</span>
2920
+ </div>
2921
+ <p style="color: var(--text-secondary); margin: 10px 0; line-height: 1.6;">${workflow.description || '无描述'}</p>
2922
+ <div class="workflow-meta" style="font-size: 12px; color: var(--text-tertiary); margin: 10px 0;">
2923
+ <span>🔔 触发条件: ${formatWorkflowTrigger(workflow.trigger)}</span>
2924
+ </div>
2925
+ <div style="display: flex; gap: 10px; margin-top: 15px;">
2926
+ <button class="btn-secondary btn-sm" data-id="${workflow._id}" data-action="toggle" style="flex: 1;">
2927
+ ${isActive ? '⏸️ 暂停' : '▶️ 启用'}
2928
+ </button>
2929
+ <button class="btn-danger btn-sm" data-id="${workflow._id}" data-action="delete" style="flex: 1;">🗑️ 删除</button>
2930
+ </div>
2931
+ `;
2932
+ card.onmouseenter = () => {
2933
+ card.style.transform = 'translateY(-4px)';
2934
+ card.style.boxShadow = '0 8px 16px rgba(0,0,0,0.1)';
2935
+ };
2936
+ card.onmouseleave = () => {
2937
+ card.style.transform = 'translateY(0)';
2938
+ card.style.boxShadow = 'none';
2939
+ };
2940
+ workflowList.appendChild(card);
2941
+ });
2942
+
2943
+ document.querySelectorAll('[data-action="toggle"]').forEach(btn => {
2944
+ btn.addEventListener('click', async () => {
2945
+ try {
2946
+ await fetch(`http://localhost:8765/api/workflows/${btn.dataset.id}/toggle`, {
2947
+ method: 'POST',
2948
+ headers: { 'Authorization': `Bearer ${token}` }
2949
+ });
2950
+ await renderWorkflowView(container);
2951
+ } catch (error) {
2952
+ alert('操作失败: ' + error.message);
2953
+ }
2954
+ });
2955
+ });
2956
+
2957
+ document.querySelectorAll('[data-action="delete"]').forEach(btn => {
2958
+ btn.addEventListener('click', async () => {
2959
+ if (confirm('确定要删除这个工作流吗?')) {
2960
+ try {
2961
+ await fetch(`http://localhost:8765/api/workflows/${btn.dataset.id}`, {
2962
+ method: 'DELETE',
2963
+ headers: { 'Authorization': `Bearer ${token}` }
2964
+ });
2965
+ alert('删除成功!');
2966
+ await renderWorkflowView(container);
2967
+ } catch (error) {
2968
+ alert('删除失败: ' + error.message);
2969
+ }
2970
+ }
2971
+ });
2972
+ });
2973
+ }
2974
+
2975
+ document.getElementById('createWorkflowBtn').addEventListener('click', () => {
2976
+ document.getElementById('workflowModal').classList.remove('hidden');
2977
+ });
2978
+
2979
+ document.getElementById('closeWorkflowModal').addEventListener('click', () => {
2980
+ document.getElementById('workflowModal').classList.add('hidden');
2981
+ });
2982
+
2983
+ document.getElementById('workflowForm').addEventListener('submit', async (e) => {
2984
+ e.preventDefault();
2985
+ const formData = new FormData(e.target);
2986
+ const data = {
2987
+ name: formData.get('name'),
2988
+ description: formData.get('description'),
2989
+ trigger: formData.get('trigger'),
2990
+ groupId: currentGroup._id,
2991
+ actions: []
2992
+ };
2993
+
2994
+ try {
2995
+ await fetch('http://localhost:8765/api/workflows', {
2996
+ method: 'POST',
2997
+ headers: {
2998
+ 'Content-Type': 'application/json',
2999
+ 'Authorization': `Bearer ${token}`
3000
+ },
3001
+ body: JSON.stringify(data)
3002
+ });
3003
+ alert('创建成功!');
3004
+ document.getElementById('workflowModal').classList.add('hidden');
3005
+ await renderWorkflowView(container);
3006
+ } catch (error) {
3007
+ alert('创建失败: ' + error.message);
3008
+ }
3009
+ });
3010
+ } catch (error) {
3011
+ container.innerHTML = `<div class="empty-state">加载失败: ${error.message}</div>`;
3012
+ }
3013
+ }
3014
+
3015
+ // 备份管理
3016
+ async function renderBackupView(container) {
3017
+ try {
3018
+ const token = localStorage.getItem('token');
3019
+ const response = await fetch('http://localhost:8765/api/backup/list', {
3020
+ headers: { 'Authorization': `Bearer ${token}` }
3021
+ });
3022
+ const result = await response.json();
3023
+ const backups = result.data?.backups || [];
3024
+
3025
+ container.innerHTML = `
3026
+ <div class="view-header">
3027
+ <h2>💾 备份管理</h2>
3028
+ <button class="btn-primary" id="createBackupBtn">➕ 创建备份</button>
3029
+ </div>
3030
+ <div class="backup-grid" id="backupList" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; padding: 20px;"></div>
3031
+ `;
3032
+
3033
+ const backupList = document.getElementById('backupList');
3034
+ if (backups.length === 0) {
3035
+ backupList.innerHTML = '<div class="empty-state" style="grid-column: 1/-1;">暂无备份</div>';
3036
+ } else {
3037
+ backups.forEach(backup => {
3038
+ const card = document.createElement('div');
3039
+ card.className = 'backup-card';
3040
+ card.style.cssText = 'background: var(--bg-secondary); padding: 20px; border-radius: 12px; border: 1px solid var(--border); transition: transform 0.2s, box-shadow 0.2s;';
3041
+ const size = (backup.size / 1024 / 1024).toFixed(2);
3042
+ card.innerHTML = `
3043
+ <div style="display: flex; align-items: center; gap: 15px; margin-bottom: 15px;">
3044
+ <div style="font-size: 48px;">📦</div>
3045
+ <div style="flex: 1;">
3046
+ <h3 style="margin: 0 0 5px 0; font-size: 16px;">${backup.filename}</h3>
3047
+ <p style="margin: 0; font-size: 14px; color: var(--text-secondary);">${size} MB</p>
3048
+ </div>
3049
+ </div>
3050
+ <div class="backup-meta" style="font-size: 12px; color: var(--text-tertiary); margin-bottom: 15px;">
3051
+ <span>📅 ${new Date(backup.createdAt).toLocaleString()}</span>
3052
+ </div>
3053
+ <div style="display: flex; gap: 10px;">
3054
+ <button class="btn-primary btn-sm" data-backup-name="${backup.name}" data-filename="${backup.filename}" data-action="download" style="flex: 1;">⬇️ 下载</button>
3055
+ <button class="btn-danger btn-sm" data-backup-name="${backup.name}" data-action="delete" style="flex: 1;">🗑️ 删除</button>
3056
+ </div>
3057
+ `;
3058
+ card.onmouseenter = () => {
3059
+ card.style.transform = 'translateY(-4px)';
3060
+ card.style.boxShadow = '0 8px 16px rgba(0,0,0,0.1)';
3061
+ };
3062
+ card.onmouseleave = () => {
3063
+ card.style.transform = 'translateY(0)';
3064
+ card.style.boxShadow = 'none';
3065
+ };
3066
+ backupList.appendChild(card);
3067
+ });
3068
+
3069
+
3070
+ // 下载按钮事件
3071
+ document.querySelectorAll('[data-action="download"]').forEach(btn => {
3072
+ btn.addEventListener('click', async () => {
3073
+ try {
3074
+ const backupName = btn.dataset.backupName; // 使用 name 而不是 filename
3075
+ const filename = btn.dataset.filename;
3076
+ const token = localStorage.getItem('token');
3077
+
3078
+ console.log('开始下载备份:', { backupName, filename });
3079
+
3080
+ // 使用 fetch 下载,带 Authorization header
3081
+ const response = await fetch(`http://localhost:8765/api/backup/download/${backupName}`, {
3082
+ method: 'GET',
3083
+ headers: {
3084
+ 'Authorization': `Bearer ${token}`
3085
+ }
3086
+ });
3087
+
3088
+ if (!response.ok) {
3089
+ throw new Error(`下载失败: ${response.status} ${response.statusText}`);
3090
+ }
3091
+
3092
+ // 获取文件内容
3093
+ const blob = await response.blob();
3094
+ console.log('文件下载成功,大小:', blob.size, 'bytes');
3095
+
3096
+ // 创建下载链接
3097
+ const url = window.URL.createObjectURL(blob);
3098
+ const link = document.createElement('a');
3099
+ link.href = url;
3100
+ link.download = filename;
3101
+ link.style.display = 'none';
3102
+ document.body.appendChild(link);
3103
+ link.click();
3104
+
3105
+ // 清理
3106
+ setTimeout(() => {
3107
+ document.body.removeChild(link);
3108
+ window.URL.revokeObjectURL(url);
3109
+ }, 100);
3110
+
3111
+ // 显示提示
3112
+ const originalText = btn.textContent;
3113
+ btn.textContent = '✅ 下载成功';
3114
+ btn.disabled = true;
3115
+
3116
+ setTimeout(() => {
3117
+ btn.textContent = originalText;
3118
+ btn.disabled = false;
3119
+ }, 2000);
3120
+
3121
+ } catch (error) {
3122
+ console.error('下载失败:', error);
3123
+ alert('下载失败: ' + error.message);
3124
+ btn.textContent = '⬇️ 下载';
3125
+ btn.disabled = false;
3126
+ }
3127
+ });
3128
+ });
3129
+
3130
+ // 删除按钮事件
3131
+ document.querySelectorAll('[data-action="delete"]').forEach(btn => {
3132
+ btn.addEventListener('click', async () => {
3133
+ if (confirm('确定要删除这个备份吗?')) {
3134
+ try {
3135
+ const backupName = btn.dataset.backupName; // 使用 name 而不是 filename
3136
+ console.log('删除备份:', backupName);
3137
+
3138
+ const response = await fetch(`http://localhost:8765/api/backup/${backupName}`, {
3139
+ method: 'DELETE',
3140
+ headers: { 'Authorization': `Bearer ${token}` }
3141
+ });
3142
+
3143
+ if (!response.ok) {
3144
+ throw new Error(`删除失败: ${response.status}`);
3145
+ }
3146
+
3147
+ alert('删除成功!');
3148
+ await renderBackupView(container);
3149
+ } catch (error) {
3150
+ console.error('删除失败:', error);
3151
+ alert('删除失败: ' + error.message);
3152
+ }
3153
+ }
3154
+ });
3155
+ });
3156
+ }
3157
+
3158
+ document.getElementById('createBackupBtn').addEventListener('click', async () => {
3159
+ if (confirm('确定要创建新备份吗?这可能需要一些时间。')) {
3160
+ const btn = document.getElementById('createBackupBtn');
3161
+ btn.disabled = true;
3162
+ btn.textContent = '⏳ 创建中...';
3163
+ try {
3164
+ await fetch('http://localhost:8765/api/backup/create', {
3165
+ method: 'POST',
3166
+ headers: { 'Authorization': `Bearer ${token}` }
3167
+ });
3168
+ alert('备份创建成功!');
3169
+ await renderBackupView(container);
3170
+ } catch (error) {
3171
+ alert('创建失败: ' + error.message);
3172
+ } finally {
3173
+ btn.disabled = false;
3174
+ btn.textContent = '➕ 创建备份';
3175
+ }
3176
+ }
3177
+ });
3178
+ } catch (error) {
3179
+ container.innerHTML = `<div class="empty-state">加载失败: ${error.message}</div>`;
3180
+ }
3181
+ }
3182
+
3183
+ // AI助手
3184
+ async function renderAIView(container) {
3185
+ container.innerHTML = '<div class="empty-state">AI助手功能开发中...</div>';
3186
+ }
3187
+
3188
+ // 数据导出
3189
+ async function renderExportView(container) {
3190
+ container.innerHTML = `
3191
+ <div class="view-header">
3192
+ <h2>📤 数据导出</h2>
3193
+ </div>
3194
+ <div class="export-container" style="padding: 20px;">
3195
+ <div class="export-options" style="background: var(--bg-secondary); padding: 30px; border-radius: 12px; margin-bottom: 20px;">
3196
+ <h3 style="margin: 0 0 20px 0;">选择导出内容</h3>
3197
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; margin-bottom: 25px;">
3198
+ <label style="display: flex; align-items: center; gap: 10px; padding: 12px; background: var(--bg); border-radius: 8px; cursor: pointer;">
3199
+ <input type="checkbox" id="exportGroups" checked style="width: 18px; height: 18px;">
3200
+ <span>👥 群组信息</span>
3201
+ </label>
3202
+ <label style="display: flex; align-items: center; gap: 10px; padding: 12px; background: var(--bg); border-radius: 8px; cursor: pointer;">
3203
+ <input type="checkbox" id="exportDocuments" checked style="width: 18px; height: 18px;">
3204
+ <span>📄 文档</span>
3205
+ </label>
3206
+ <label style="display: flex; align-items: center; gap: 10px; padding: 12px; background: var(--bg); border-radius: 8px; cursor: pointer;">
3207
+ <input type="checkbox" id="exportTasks" checked style="width: 18px; height: 18px;">
3208
+ <span>📋 任务</span>
3209
+ </label>
3210
+ <label style="display: flex; align-items: center; gap: 10px; padding: 12px; background: var(--bg); border-radius: 8px; cursor: pointer;">
3211
+ <input type="checkbox" id="exportMessages" checked style="width: 18px; height: 18px;">
3212
+ <span>💬 消息</span>
3213
+ </label>
3214
+ <label style="display: flex; align-items: center; gap: 10px; padding: 12px; background: var(--bg); border-radius: 8px; cursor: pointer;">
3215
+ <input type="checkbox" id="exportFiles" style="width: 18px; height: 18px;">
3216
+ <span>📎 文件</span>
3217
+ </label>
3218
+ </div>
3219
+
3220
+ <h3 style="margin: 25px 0 15px 0;">导出格式</h3>
3221
+ <select id="exportFormat" style="width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 25px;">
3222
+ <option value="json">JSON</option>
3223
+ <option value="csv">CSV</option>
3224
+ <option value="excel">Excel</option>
3225
+ </select>
3226
+
3227
+ <button class="btn-primary" id="exportBtn" style="width: 100%; padding: 15px; font-size: 16px;">🚀 开始导出</button>
3228
+ </div>
3229
+
3230
+ <div class="export-history" style="background: var(--bg-secondary); padding: 30px; border-radius: 12px;">
3231
+ <h3 style="margin: 0 0 20px 0;">📜 导出历史</h3>
3232
+ <div id="historyList">加载中...</div>
3233
+ </div>
3234
+ </div>
3235
+ `;
3236
+
3237
+ // 加载导出历史
3238
+ try {
3239
+ const token = localStorage.getItem('token');
3240
+ const response = await fetch('http://localhost:8765/api/export/history', {
3241
+ headers: { 'Authorization': `Bearer ${token}` }
3242
+ });
3243
+ const result = await response.json();
3244
+ const historyList = document.getElementById('historyList');
3245
+
3246
+ if (result.exports && result.exports.length > 0) {
3247
+ historyList.innerHTML = result.exports.map(exp => `
3248
+ <div class="export-item" style="display: flex; justify-content: space-between; align-items: center; padding: 15px; background: var(--bg); border-radius: 8px; margin-bottom: 10px;">
3249
+ <div>
3250
+ <div style="font-weight: 600; margin-bottom: 5px;">📦 ${exp.format.toUpperCase()} 导出</div>
3251
+ <div style="font-size: 12px; color: var(--text-secondary);">📅 ${new Date(exp.createdAt).toLocaleString()}</div>
3252
+ </div>
3253
+ <a href="http://localhost:8765/api/export/download/${exp.filename}" class="btn-sm btn-primary" download style="text-decoration: none;">⬇️ 下载</a>
3254
+ </div>
3255
+ `).join('');
3256
+ } else {
3257
+ historyList.innerHTML = '<div class="empty-state">暂无导出记录</div>';
3258
+ }
3259
+ } catch (error) {
3260
+ document.getElementById('historyList').innerHTML = '<div class="empty-state">加载失败</div>';
3261
+ }
3262
+
3263
+ document.getElementById('exportBtn').addEventListener('click', async () => {
3264
+ const options = {
3265
+ groups: document.getElementById('exportGroups').checked,
3266
+ documents: document.getElementById('exportDocuments').checked,
3267
+ tasks: document.getElementById('exportTasks').checked,
3268
+ messages: document.getElementById('exportMessages').checked,
3269
+ files: document.getElementById('exportFiles').checked,
3270
+ format: document.getElementById('exportFormat').value
3271
+ };
3272
+
3273
+ const btn = document.getElementById('exportBtn');
3274
+ btn.disabled = true;
3275
+ btn.textContent = '⏳ 导出中...';
3276
+
3277
+ try {
3278
+ const token = localStorage.getItem('token');
3279
+ const response = await fetch('http://localhost:8765/api/export', {
3280
+ method: 'POST',
3281
+ headers: {
3282
+ 'Content-Type': 'application/json',
3283
+ 'Authorization': `Bearer ${token}`
3284
+ },
3285
+ body: JSON.stringify(options)
3286
+ });
3287
+
3288
+ if (response.ok) {
3289
+ const blob = await response.blob();
3290
+ const url = window.URL.createObjectURL(blob);
3291
+ const a = document.createElement('a');
3292
+ a.href = url;
3293
+ a.download = `export-${Date.now()}.${options.format}`;
3294
+ a.click();
3295
+ alert('导出成功!');
3296
+ await renderExportView(container);
3297
+ } else {
3298
+ throw new Error('导出失败');
3299
+ }
3300
+ } catch (error) {
3301
+ alert('导出失败: ' + error.message);
3302
+ } finally {
3303
+ btn.disabled = false;
3304
+ btn.textContent = '🚀 开始导出';
3305
+ }
3306
+ });
3307
+ }
3308
+
3309
+
3310
+ // 协作白板
3311
+ async function renderWhiteboardView(container) {
3312
+ if (!currentGroup) {
3313
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
3314
+ return;
3315
+ }
3316
+
3317
+ container.innerHTML = `
3318
+ <div class="view-header">
3319
+ <h2>🎨 协作白板 - ${currentGroup.name}</h2>
3320
+ <div style="display: flex; gap: 10px;">
3321
+ <button class="btn-secondary" id="clearCanvas">清空画布</button>
3322
+ <button class="btn-primary" id="saveCanvas">保存白板</button>
3323
+ </div>
3324
+ </div>
3325
+ <div class="whiteboard-container">
3326
+ <div class="whiteboard-toolbar">
3327
+ <button class="tool-btn active" data-tool="pen">✏️ 画笔</button>
3328
+ <button class="tool-btn" data-tool="eraser">🧹 橡皮擦</button>
3329
+ <button class="tool-btn" data-tool="text">📝 文字</button>
3330
+ <button class="tool-btn" data-tool="shape">⬜ 形状</button>
3331
+ <input type="color" id="colorPicker" value="#000000" title="颜色">
3332
+ <input type="range" id="brushSize" min="1" max="20" value="3" title="画笔大小">
3333
+ </div>
3334
+ <canvas id="whiteboard" width="1200" height="600" style="border: 1px solid var(--border); background: white; cursor: crosshair;"></canvas>
3335
+ <div class="whiteboard-users" id="whiteboardUsers">
3336
+ <span class="user-badge">👤 ${user.username}</span>
3337
+ </div>
3338
+ </div>
3339
+ `;
3340
+
3341
+ const canvas = document.getElementById('whiteboard');
3342
+ const ctx = canvas.getContext('2d');
3343
+ let isDrawing = false;
3344
+ let currentTool = 'pen';
3345
+ let currentColor = '#000000';
3346
+ let brushSize = 3;
3347
+
3348
+ // 工具切换
3349
+ document.querySelectorAll('.tool-btn').forEach(btn => {
3350
+ btn.addEventListener('click', () => {
3351
+ document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active'));
3352
+ btn.classList.add('active');
3353
+ currentTool = btn.dataset.tool;
3354
+ });
3355
+ });
3356
+
3357
+ document.getElementById('colorPicker').addEventListener('change', (e) => {
3358
+ currentColor = e.target.value;
3359
+ });
3360
+
3361
+ document.getElementById('brushSize').addEventListener('input', (e) => {
3362
+ brushSize = e.target.value;
3363
+ });
3364
+
3365
+ // 绘画功能
3366
+ let lastX = 0;
3367
+ let lastY = 0;
3368
+
3369
+ canvas.addEventListener('mousedown', (e) => {
3370
+ isDrawing = true;
3371
+ const rect = canvas.getBoundingClientRect();
3372
+ lastX = e.clientX - rect.left;
3373
+ lastY = e.clientY - rect.top;
3374
+ });
3375
+
3376
+ canvas.addEventListener('mousemove', (e) => {
3377
+ if (!isDrawing) return;
3378
+
3379
+ const rect = canvas.getBoundingClientRect();
3380
+ const x = e.clientX - rect.left;
3381
+ const y = e.clientY - rect.top;
3382
+
3383
+ ctx.beginPath();
3384
+ ctx.moveTo(lastX, lastY);
3385
+ ctx.lineTo(x, y);
3386
+ ctx.strokeStyle = currentTool === 'eraser' ? '#ffffff' : currentColor;
3387
+ ctx.lineWidth = brushSize;
3388
+ ctx.lineCap = 'round';
3389
+ ctx.stroke();
3390
+
3391
+ lastX = x;
3392
+ lastY = y;
3393
+
3394
+ // 发送绘画数据到其他用户
3395
+ wsService.sendWhiteboardData(currentGroup._id, {
3396
+ tool: currentTool,
3397
+ color: currentColor,
3398
+ size: brushSize,
3399
+ from: { x: lastX, y: lastY },
3400
+ to: { x, y }
3401
+ });
3402
+ });
3403
+
3404
+ canvas.addEventListener('mouseup', () => {
3405
+ isDrawing = false;
3406
+ });
3407
+
3408
+ canvas.addEventListener('mouseleave', () => {
3409
+ isDrawing = false;
3410
+ });
3411
+
3412
+ // 清空画布
3413
+ document.getElementById('clearCanvas').addEventListener('click', () => {
3414
+ if (confirm('确定要清空画布吗?')) {
3415
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
3416
+ }
3417
+ });
3418
+
3419
+ // 保存白板
3420
+ document.getElementById('saveCanvas').addEventListener('click', () => {
3421
+ const dataURL = canvas.toDataURL('image/png');
3422
+ const link = document.createElement('a');
3423
+ link.download = `whiteboard-${Date.now()}.png`;
3424
+ link.href = dataURL;
3425
+ link.click();
3426
+ alert('白板已保存!');
3427
+ });
3428
+
3429
+ // 监听其他用户的绘画
3430
+ wsService.on('whiteboard_draw', (data) => {
3431
+ if (data.groupId === currentGroup._id && data.userId !== currentUserId) {
3432
+ ctx.beginPath();
3433
+ ctx.moveTo(data.from.x, data.from.y);
3434
+ ctx.lineTo(data.to.x, data.to.y);
3435
+ ctx.strokeStyle = data.tool === 'eraser' ? '#ffffff' : data.color;
3436
+ ctx.lineWidth = data.size;
3437
+ ctx.lineCap = 'round';
3438
+ ctx.stroke();
3439
+ }
3440
+ });
3441
+ }
3442
+
3443
+ // 集成管理
3444
+ async function renderIntegrationsView(container) {
3445
+ container.innerHTML = `
3446
+ <div class="view-header">
3447
+ <h2>🔌 集成管理</h2>
3448
+ <button class="btn-primary" id="addIntegrationBtn">添加集成</button>
3449
+ </div>
3450
+ <div class="integrations-grid">
3451
+ <div class="integration-card">
3452
+ <div class="integration-icon">📧</div>
3453
+ <h3>邮件通知</h3>
3454
+ <p>接收重要事件的邮件通知</p>
3455
+ <button class="btn-secondary">配置</button>
3456
+ </div>
3457
+ <div class="integration-card">
3458
+ <div class="integration-icon">🔔</div>
3459
+ <h3>Webhook</h3>
3460
+ <p>自定义 Webhook 集成</p>
3461
+ <button class="btn-secondary">配置</button>
3462
+ </div>
3463
+ <div class="integration-card">
3464
+ <div class="integration-icon">📱</div>
3465
+ <h3>钉钉机器人</h3>
3466
+ <p>发送消息到钉钉群</p>
3467
+ <button class="btn-secondary">配置</button>
3468
+ </div>
3469
+ <div class="integration-card">
3470
+ <div class="integration-icon">💬</div>
3471
+ <h3>企业微信</h3>
3472
+ <p>发送消息到企业微信</p>
3473
+ <button class="btn-secondary">配置</button>
3474
+ </div>
3475
+ <div class="integration-card">
3476
+ <div class="integration-icon">🤖</div>
3477
+ <h3>Slack</h3>
3478
+ <p>连接到 Slack 工作区</p>
3479
+ <button class="btn-secondary">配置</button>
3480
+ </div>
3481
+ <div class="integration-card">
3482
+ <div class="integration-icon">📊</div>
3483
+ <h3>数据分析</h3>
3484
+ <p>导出数据到分析平台</p>
3485
+ <button class="btn-secondary">配置</button>
3486
+ </div>
3487
+ </div>
3488
+ `;
3489
+
3490
+ document.getElementById('addIntegrationBtn').addEventListener('click', () => {
3491
+ alert('添加自定义集成功能开发中...');
3492
+ });
3493
+ }
3494
+
3495
+ // 设置
3496
+ async function renderSettingsView(container) {
3497
+ container.innerHTML = `
3498
+ <div class="view-header" style="margin-bottom: 30px;">
3499
+ <h2 style="display: flex; align-items: center; gap: 12px; font-size: 28px;">
3500
+ <span style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">⚙️ 设置中心</span>
3501
+ </h2>
3502
+ <p style="color: var(--text-tertiary); margin-top: 8px;">管理您的个人偏好和系统配置</p>
3503
+ </div>
3504
+ <div class="settings-container" style="display: grid; gap: 24px; max-width: 900px;">
3505
+ <!-- 个人设置卡片 -->
3506
+ <div class="settings-card" style="background: var(--bg-secondary); padding: 28px; border-radius: 16px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s;">
3507
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 24px;">
3508
+ <div style="width: 48px; height: 48px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">👤</div>
3509
+ <h3 style="margin: 0; font-size: 20px; font-weight: 600;">个人设置</h3>
3510
+ </div>
3511
+ <div class="setting-item" style="margin-bottom: 20px;">
3512
+ <label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary); font-size: 14px;">👤 用户名</label>
3513
+ <input type="text" value="${user.username}" disabled style="width: 100%; padding: 12px 16px; border: 2px solid var(--border); border-radius: 10px; font-size: 14px; background: var(--bg-tertiary); cursor: not-allowed;">
3514
+ </div>
3515
+ <div class="setting-item" style="margin-bottom: 20px;">
3516
+ <label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary); font-size: 14px;">📧 邮箱</label>
3517
+ <input type="email" id="userEmail" value="${user.email || ''}" placeholder="请输入邮箱地址" style="width: 100%; padding: 12px 16px; border: 2px solid var(--border); border-radius: 10px; font-size: 14px; transition: border-color 0.2s;">
3518
+ </div>
3519
+ <div class="setting-item">
3520
+ <label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary); font-size: 14px;">🔐 密码</label>
3521
+ <button class="btn-secondary" id="changePasswordBtn" style="padding: 10px 20px; border-radius: 8px; font-weight: 600; transition: all 0.2s;">修改密码</button>
3522
+ </div>
3523
+ </div>
3524
+
3525
+ <!-- 通知设置卡片 -->
3526
+ <div class="settings-card" style="background: var(--bg-secondary); padding: 28px; border-radius: 16px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s;">
3527
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 24px;">
3528
+ <div style="width: 48px; height: 48px; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">🔔</div>
3529
+ <h3 style="margin: 0; font-size: 20px; font-weight: 600;">通知设置</h3>
3530
+ </div>
3531
+ <div class="setting-item" style="margin-bottom: 16px;">
3532
+ <label style="display: flex; align-items: center; gap: 12px; cursor: pointer; padding: 12px; border-radius: 8px; transition: background 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'" onmouseleave="this.style.background='transparent'">
3533
+ <input type="checkbox" id="emailNotifications" checked style="width: 20px; height: 20px; cursor: pointer;">
3534
+ <div>
3535
+ <div style="font-weight: 600; color: var(--text-primary);">📧 邮件通知</div>
3536
+ <div style="font-size: 12px; color: var(--text-tertiary); margin-top: 2px;">接收重要事件的邮件提醒</div>
3537
+ </div>
3538
+ </label>
3539
+ </div>
3540
+ <div class="setting-item" style="margin-bottom: 16px;">
3541
+ <label style="display: flex; align-items: center; gap: 12px; cursor: pointer; padding: 12px; border-radius: 8px; transition: background 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'" onmouseleave="this.style.background='transparent'">
3542
+ <input type="checkbox" id="desktopNotifications" checked style="width: 20px; height: 20px; cursor: pointer;">
3543
+ <div>
3544
+ <div style="font-weight: 600; color: var(--text-primary);">🖥️ 桌面通知</div>
3545
+ <div style="font-size: 12px; color: var(--text-tertiary); margin-top: 2px;">在桌面显示通知消息</div>
3546
+ </div>
3547
+ </label>
3548
+ </div>
3549
+ <div class="setting-item">
3550
+ <label style="display: flex; align-items: center; gap: 12px; cursor: pointer; padding: 12px; border-radius: 8px; transition: background 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'" onmouseleave="this.style.background='transparent'">
3551
+ <input type="checkbox" id="soundNotifications" checked style="width: 20px; height: 20px; cursor: pointer;">
3552
+ <div>
3553
+ <div style="font-weight: 600; color: var(--text-primary);">🔊 声音提示</div>
3554
+ <div style="font-size: 12px; color: var(--text-tertiary); margin-top: 2px;">播放通知提示音</div>
3555
+ </div>
3556
+ </label>
3557
+ </div>
3558
+ </div>
3559
+
3560
+ <!-- 系统设置卡片 -->
3561
+ <div class="settings-card" style="background: var(--bg-secondary); padding: 28px; border-radius: 16px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s;">
3562
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 24px;">
3563
+ <div style="width: 48px; height: 48px; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">🎨</div>
3564
+ <h3 style="margin: 0; font-size: 20px; font-weight: 600;">系统设置</h3>
3565
+ </div>
3566
+ <div class="setting-item" style="margin-bottom: 20px;">
3567
+ <label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary); font-size: 14px;">🎨 主题</label>
3568
+ <select id="themeSelect" style="width: 100%; padding: 12px 16px; border: 2px solid var(--border); border-radius: 10px; font-size: 14px; cursor: pointer; transition: border-color 0.2s;">
3569
+ <option value="dark">🌙 深色模式(默认紫色)</option>
3570
+ <option value="blue">💙 深色蓝色</option>
3571
+ <option value="green">💚 深色绿色</option>
3572
+ <option value="orange">🧡 深色橙色</option>
3573
+ <option value="pink">💗 深色粉色</option>
3574
+ <option value="light">☀️ 浅色模式</option>
3575
+ </select>
3576
+ </div>
3577
+ <div class="setting-item">
3578
+ <label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-secondary); font-size: 14px;">🌐 语言</label>
3579
+ <select id="languageSelect" style="width: 100%; padding: 12px 16px; border: 2px solid var(--border); border-radius: 10px; font-size: 14px; cursor: pointer; transition: border-color 0.2s;">
3580
+ <option value="zh-CN" selected>🇨🇳 简体中文</option>
3581
+ <option value="en-US">🇺🇸 English</option>
3582
+ <option value="ja-JP">🇯🇵 日本語</option>
3583
+ </select>
3584
+ </div>
3585
+ </div>
3586
+
3587
+ <!-- 保存按钮 -->
3588
+ <div class="settings-actions" style="display: flex; gap: 12px; justify-content: flex-end;">
3589
+ <button class="btn-secondary" id="resetSettingsBtn" style="padding: 12px 24px; border-radius: 10px; font-weight: 600; transition: all 0.2s;">重置</button>
3590
+ <button class="btn-primary" id="saveSettingsBtn" style="padding: 12px 32px; border-radius: 10px; font-weight: 600; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; cursor: pointer; transition: transform 0.2s;">💾 保存设置</button>
3591
+ </div>
3592
+ </div>
3593
+ `;
3594
+
3595
+ // 添加卡片悬停效果
3596
+ document.querySelectorAll('.settings-card').forEach(card => {
3597
+ card.addEventListener('mouseenter', () => {
3598
+ card.style.transform = 'translateY(-4px)';
3599
+ card.style.boxShadow = '0 8px 24px rgba(0,0,0,0.12)';
3600
+ });
3601
+ card.addEventListener('mouseleave', () => {
3602
+ card.style.transform = 'translateY(0)';
3603
+ card.style.boxShadow = '0 2px 8px rgba(0,0,0,0.05)';
3604
+ });
3605
+ });
3606
+
3607
+ // 输入框聚焦效果
3608
+ document.querySelectorAll('input[type="email"], select').forEach(input => {
3609
+ input.addEventListener('focus', () => {
3610
+ input.style.borderColor = 'var(--primary)';
3611
+ });
3612
+ input.addEventListener('blur', () => {
3613
+ input.style.borderColor = 'var(--border)';
3614
+ });
3615
+ });
3616
+
3617
+ document.getElementById('changePasswordBtn').addEventListener('click', () => {
3618
+ const newPassword = prompt('请输入新密码:');
3619
+ if (newPassword) {
3620
+ alert('密码修改功能开发中...');
3621
+ }
3622
+ });
3623
+
3624
+ document.getElementById('resetSettingsBtn').addEventListener('click', () => {
3625
+ if (confirm('确定要重置所有设置吗?')) {
3626
+ location.reload();
3627
+ }
3628
+ });
3629
+
3630
+ document.getElementById('saveSettingsBtn').addEventListener('click', () => {
3631
+ const settings = {
3632
+ email: document.getElementById('userEmail').value,
3633
+ emailNotifications: document.getElementById('emailNotifications').checked,
3634
+ desktopNotifications: document.getElementById('desktopNotifications').checked,
3635
+ soundNotifications: document.getElementById('soundNotifications').checked,
3636
+ theme: document.getElementById('themeSelect').value,
3637
+ language: document.getElementById('languageSelect').value
3638
+ };
3639
+ localStorage.setItem('userSettings', JSON.stringify(settings));
3640
+
3641
+ // 应用主题
3642
+ applyTheme(settings.theme);
3643
+
3644
+ alert('✅ 设置已保存!');
3645
+ });
3646
+
3647
+ // 主题切换监听
3648
+ document.getElementById('themeSelect').addEventListener('change', (e) => {
3649
+ applyTheme(e.target.value);
3650
+ });
3651
+
3652
+ // 加载保存的设置
3653
+ const savedSettings = localStorage.getItem('userSettings');
3654
+ if (savedSettings) {
3655
+ const settings = JSON.parse(savedSettings);
3656
+ if (settings.email) document.getElementById('userEmail').value = settings.email;
3657
+ document.getElementById('emailNotifications').checked = settings.emailNotifications !== false;
3658
+ document.getElementById('desktopNotifications').checked = settings.desktopNotifications !== false;
3659
+ document.getElementById('soundNotifications').checked = settings.soundNotifications !== false;
3660
+ if (settings.theme) {
3661
+ document.getElementById('themeSelect').value = settings.theme;
3662
+ applyTheme(settings.theme);
3663
+ }
3664
+ if (settings.language) document.getElementById('languageSelect').value = settings.language;
3665
+ }
3666
+ }
3667
+
3668
+ // 主题应用函数
3669
+ function applyTheme(theme) {
3670
+ const root = document.documentElement;
3671
+
3672
+ // 主题配置
3673
+ const themes = {
3674
+ dark: {
3675
+ primary: '#6366f1',
3676
+ primaryDark: '#4f46e5',
3677
+ secondary: '#8b5cf6',
3678
+ bgDark: '#0f172a',
3679
+ bgCard: '#1e293b',
3680
+ bgHover: '#334155',
3681
+ textPrimary: '#f1f5f9',
3682
+ textSecondary: '#94a3b8',
3683
+ border: '#334155'
3684
+ },
3685
+ blue: {
3686
+ primary: '#3b82f6',
3687
+ primaryDark: '#2563eb',
3688
+ secondary: '#06b6d4',
3689
+ bgDark: '#0c1222',
3690
+ bgCard: '#1a2332',
3691
+ bgHover: '#2a3442',
3692
+ textPrimary: '#e0f2fe',
3693
+ textSecondary: '#7dd3fc',
3694
+ border: '#2a3442'
3695
+ },
3696
+ green: {
3697
+ primary: '#10b981',
3698
+ primaryDark: '#059669',
3699
+ secondary: '#34d399',
3700
+ bgDark: '#0a1f1a',
3701
+ bgCard: '#1a2f2a',
3702
+ bgHover: '#2a3f3a',
3703
+ textPrimary: '#d1fae5',
3704
+ textSecondary: '#6ee7b7',
3705
+ border: '#2a3f3a'
3706
+ },
3707
+ orange: {
3708
+ primary: '#f59e0b',
3709
+ primaryDark: '#d97706',
3710
+ secondary: '#fb923c',
3711
+ bgDark: '#1f1a0a',
3712
+ bgCard: '#2f2a1a',
3713
+ bgHover: '#3f3a2a',
3714
+ textPrimary: '#fef3c7',
3715
+ textSecondary: '#fcd34d',
3716
+ border: '#3f3a2a'
3717
+ },
3718
+ pink: {
3719
+ primary: '#ec4899',
3720
+ primaryDark: '#db2777',
3721
+ secondary: '#f472b6',
3722
+ bgDark: '#1f0a1a',
3723
+ bgCard: '#2f1a2a',
3724
+ bgHover: '#3f2a3a',
3725
+ textPrimary: '#fce7f3',
3726
+ textSecondary: '#f9a8d4',
3727
+ border: '#3f2a3a'
3728
+ },
3729
+ light: {
3730
+ primary: '#6366f1',
3731
+ primaryDark: '#4f46e5',
3732
+ secondary: '#8b5cf6',
3733
+ bgDark: '#ffffff',
3734
+ bgCard: '#f8fafc',
3735
+ bgHover: '#e2e8f0',
3736
+ textPrimary: '#0f172a',
3737
+ textSecondary: '#475569',
3738
+ border: '#cbd5e1'
3739
+ }
3740
+ };
3741
+
3742
+ const selectedTheme = themes[theme] || themes.dark;
3743
+
3744
+ // 应用CSS变量
3745
+ root.style.setProperty('--primary', selectedTheme.primary);
3746
+ root.style.setProperty('--primary-dark', selectedTheme.primaryDark);
3747
+ root.style.setProperty('--secondary', selectedTheme.secondary);
3748
+ root.style.setProperty('--bg-dark', selectedTheme.bgDark);
3749
+ root.style.setProperty('--bg-card', selectedTheme.bgCard);
3750
+ root.style.setProperty('--bg-hover', selectedTheme.bgHover);
3751
+ root.style.setProperty('--text-primary', selectedTheme.textPrimary);
3752
+ root.style.setProperty('--text-secondary', selectedTheme.textSecondary);
3753
+ root.style.setProperty('--border', selectedTheme.border);
3754
+
3755
+ // 保存到localStorage
3756
+ localStorage.setItem('currentTheme', theme);
3757
+ }
3758
+
3759
+ // 帮助
3760
+ async function renderHelpView(container) {
3761
+ container.innerHTML = `
3762
+ <div class="view-header" style="margin-bottom: 30px;">
3763
+ <h2 style="display: flex; align-items: center; gap: 12px; font-size: 28px;">
3764
+ <span style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">❓ 帮助中心</span>
3765
+ </h2>
3766
+ <p style="color: var(--text-tertiary); margin-top: 8px;">快速找到您需要的帮助和支持</p>
3767
+ </div>
3768
+
3769
+ <div class="help-container" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 24px; max-width: 1200px;">
3770
+ <!-- 快速开始卡片 -->
3771
+ <div class="help-card" style="background: var(--bg-secondary); padding: 28px; border-radius: 16px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s;">
3772
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
3773
+ <div style="width: 48px; height: 48px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">📖</div>
3774
+ <h3 style="margin: 0; font-size: 20px; font-weight: 600;">快速开始</h3>
3775
+ </div>
3776
+ <ul style="list-style: none; padding: 0; margin: 0;">
3777
+ <li style="margin-bottom: 12px;">
3778
+ <a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
3779
+ <span style="color: var(--primary);">▶</span>
3780
+ <span>如何创建群组?</span>
3781
+ </a>
3782
+ </li>
3783
+ <li style="margin-bottom: 12px;">
3784
+ <a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
3785
+ <span style="color: var(--primary);">▶</span>
3786
+ <span>如何邀请成员?</span>
3787
+ </a>
3788
+ </li>
3789
+ <li style="margin-bottom: 12px;">
3790
+ <a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
3791
+ <span style="color: var(--primary);">▶</span>
3792
+ <span>如何创建文档?</span>
3793
+ </a>
3794
+ </li>
3795
+ <li>
3796
+ <a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
3797
+ <span style="color: var(--primary);">▶</span>
3798
+ <span>如何使用协作白板?</span>
3799
+ </a>
3800
+ </li>
3801
+ </ul>
3802
+ </div>
3803
+
3804
+ <!-- 功能说明卡片 -->
3805
+ <div class="help-card" style="background: var(--bg-secondary); padding: 28px; border-radius: 16px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s;">
3806
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
3807
+ <div style="width: 48px; height: 48px; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">🔧</div>
3808
+ <h3 style="margin: 0; font-size: 20px; font-weight: 600;">功能说明</h3>
3809
+ </div>
3810
+ <ul style="list-style: none; padding: 0; margin: 0;">
3811
+ <li style="margin-bottom: 12px;">
3812
+ <a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
3813
+ <span style="color: var(--primary);">▶</span>
3814
+ <span>群组管理</span>
3815
+ </a>
3816
+ </li>
3817
+ <li style="margin-bottom: 12px;">
3818
+ <a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
3819
+ <span style="color: var(--primary);">▶</span>
3820
+ <span>任务管理</span>
3821
+ </a>
3822
+ </li>
3823
+ <li style="margin-bottom: 12px;">
3824
+ <a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
3825
+ <span style="color: var(--primary);">▶</span>
3826
+ <span>文档协作</span>
3827
+ </a>
3828
+ </li>
3829
+ <li style="margin-bottom: 12px;">
3830
+ <a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
3831
+ <span style="color: var(--primary);">▶</span>
3832
+ <span>知识库</span>
3833
+ </a>
3834
+ </li>
3835
+ <li style="margin-bottom: 12px;">
3836
+ <a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
3837
+ <span style="color: var(--primary);">▶</span>
3838
+ <span>工作流引擎</span>
3839
+ </a>
3840
+ </li>
3841
+ <li>
3842
+ <a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
3843
+ <span style="color: var(--primary);">▶</span>
3844
+ <span>AI 智能助手</span>
3845
+ </a>
3846
+ </li>
3847
+ </ul>
3848
+ </div>
3849
+
3850
+ <!-- 常见问题卡片 -->
3851
+ <div class="help-card" style="background: var(--bg-secondary); padding: 28px; border-radius: 16px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s;">
3852
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
3853
+ <div style="width: 48px; height: 48px; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">❓</div>
3854
+ <h3 style="margin: 0; font-size: 20px; font-weight: 600;">常见问题</h3>
3855
+ </div>
3856
+ <ul style="list-style: none; padding: 0; margin: 0;">
3857
+ <li style="margin-bottom: 12px;">
3858
+ <a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
3859
+ <span style="color: var(--primary);">▶</span>
3860
+ <span>如何重置密码?</span>
3861
+ </a>
3862
+ </li>
3863
+ <li style="margin-bottom: 12px;">
3864
+ <a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
3865
+ <span style="color: var(--primary);">▶</span>
3866
+ <span>如何导出数据?</span>
3867
+ </a>
3868
+ </li>
3869
+ <li style="margin-bottom: 12px;">
3870
+ <a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
3871
+ <span style="color: var(--primary);">▶</span>
3872
+ <span>如何备份数据?</span>
3873
+ </a>
3874
+ </li>
3875
+ <li>
3876
+ <a href="#" class="help-link" style="display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; text-decoration: none; color: var(--text-primary); transition: all 0.2s;" onmouseenter="this.style.background='var(--bg-tertiary)'; this.style.paddingLeft='16px';" onmouseleave="this.style.background='transparent'; this.style.paddingLeft='10px';">
3877
+ <span style="color: var(--primary);">▶</span>
3878
+ <span>如何联系技术支持?</span>
3879
+ </a>
3880
+ </li>
3881
+ </ul>
3882
+ </div>
3883
+
3884
+ <!-- 联系我们卡片 -->
3885
+ <div class="help-card" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 28px; border-radius: 16px; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); transition: transform 0.2s, box-shadow 0.2s; color: white;">
3886
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
3887
+ <div style="width: 48px; height: 48px; background: rgba(255,255,255,0.2); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">📞</div>
3888
+ <h3 style="margin: 0; font-size: 20px; font-weight: 600;">联系我们</h3>
3889
+ </div>
3890
+ <p style="margin: 0 0 16px 0; opacity: 0.95; line-height: 1.6;">如果您有任何问题或建议,请通过以下方式联系我们:</p>
3891
+ <ul style="list-style: none; padding: 0; margin: 0;">
3892
+ <li style="margin-bottom: 12px; display: flex; align-items: center; gap: 10px; opacity: 0.95;">
3893
+ <span>📧</span>
3894
+ <span>support@collabdocchat.com</span>
3895
+ </li>
3896
+ <li style="margin-bottom: 12px;">
3897
+ <a href="https://github.com/shijinghao/collabdocchat" target="_blank" style="display: flex; align-items: center; gap: 10px; color: white; text-decoration: none; opacity: 0.95; transition: opacity 0.2s;" onmouseenter="this.style.opacity='1'" onmouseleave="this.style.opacity='0.95'">
3898
+ <span>🌐</span>
3899
+ <span>GitHub</span>
3900
+ </a>
3901
+ </li>
3902
+ <li>
3903
+ <a href="https://www.npmjs.com/package/collabdocchat" target="_blank" style="display: flex; align-items: center; gap: 10px; color: white; text-decoration: none; opacity: 0.95; transition: opacity 0.2s;" onmouseenter="this.style.opacity='1'" onmouseleave="this.style.opacity='0.95'">
3904
+ <span>📦</span>
3905
+ <span>npm Package</span>
3906
+ </a>
3907
+ </li>
3908
+ </ul>
3909
+ </div>
3910
+
3911
+ <!-- 关于卡片 -->
3912
+ <div class="help-card" style="background: var(--bg-secondary); padding: 28px; border-radius: 16px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s; grid-column: span 2;">
3913
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
3914
+ <div style="width: 48px; height: 48px; background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px;">ℹ️</div>
3915
+ <h3 style="margin: 0; font-size: 20px; font-weight: 600;">关于 CollabDocChat</h3>
3916
+ </div>
3917
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
3918
+ <div>
3919
+ <p style="margin: 0 0 12px 0; font-size: 18px; font-weight: 600; color: var(--primary);">CollabDocChat v2.3.0</p>
3920
+ <p style="margin: 0 0 12px 0; color: var(--text-secondary); line-height: 1.6;">开源的实时协作文档聊天平台,提供强大的团队协作功能。</p>
3921
+ <p style="margin: 0; color: var(--text-tertiary); font-size: 14px;">© 2026 CollabDocChat. All rights reserved.</p>
3922
+ </div>
3923
+ <div style="background: var(--bg-tertiary); padding: 20px; border-radius: 12px;">
3924
+ <h4 style="margin: 0 0 12px 0; font-size: 16px;">核心特性</h4>
3925
+ <ul style="margin: 0; padding-left: 20px; color: var(--text-secondary); line-height: 1.8; font-size: 14px;">
3926
+ <li>实时协作编辑</li>
3927
+ <li>智能 AI 助手</li>
3928
+ <li>工作流自动化</li>
3929
+ <li>知识库管理</li>
3930
+ <li>协作白板</li>
3931
+ <li>数据备份导出</li>
3932
+ </ul>
3933
+ </div>
3934
+ </div>
3935
+ </div>
3936
+ </div>
3937
+ `;
3938
+
3939
+ // 添加卡片悬停效果
3940
+ document.querySelectorAll('.help-card').forEach(card => {
3941
+ card.addEventListener('mouseenter', () => {
3942
+ card.style.transform = 'translateY(-4px)';
3943
+ card.style.boxShadow = '0 8px 24px rgba(0,0,0,0.12)';
3944
+ });
3945
+ card.addEventListener('mouseleave', () => {
3946
+ card.style.transform = 'translateY(0)';
3947
+ const isGradient = card.style.background.includes('gradient');
3948
+ card.style.boxShadow = isGradient ? '0 4px 12px rgba(102, 126, 234, 0.3)' : '0 2px 8px rgba(0,0,0,0.05)';
3949
+ });
3950
+ });
3951
+ }
3952
+
3953
+ async function renderView(view) {
3954
+ const contentArea = document.getElementById('contentArea');
3955
+
3956
+ switch(view) {
3957
+ case 'groups':
3958
+ await renderGroupsView(contentArea);
3959
+ break;
3960
+ case 'tasks':
3961
+ await renderTasksView(contentArea);
3962
+ break;
3963
+ case 'documents':
3964
+ await renderDocumentsView(contentArea);
3965
+ break;
3966
+ case 'chat':
3967
+ await renderChatView(contentArea);
3968
+ break;
3969
+ case 'files':
3970
+ await renderFilesView(contentArea);
3971
+ break;
3972
+ case 'search':
3973
+ await renderSearchView(contentArea);
3974
+ break;
3975
+ case 'call':
3976
+ await renderCallView(contentArea);
3977
+ break;
3978
+ case 'audit':
3979
+ await renderAuditView(contentArea);
3980
+ break;
3981
+ case 'knowledge':
3982
+ await renderKnowledgeView(contentArea);
3983
+ break;
3984
+ case 'workflow':
3985
+ await renderWorkflowView(contentArea);
3986
+ break;
3987
+ case 'backup':
3988
+ await renderBackupView(contentArea);
3989
+ break;
3990
+ case 'export':
3991
+ await renderExportView(contentArea);
3992
+ break;
3993
+ case 'ai':
3994
+ await renderAIView(contentArea);
3995
+ break;
3996
+ case 'export':
3997
+ await renderExportView(contentArea);
3998
+ break;
3999
+ case 'whiteboard':
4000
+ await renderWhiteboardView(contentArea);
4001
+ break;
4002
+ case 'settings':
4003
+ await renderSettingsView(contentArea);
4004
+ break;
4005
+ case 'help':
4006
+ await renderHelpView(contentArea);
4007
+ break;
4008
+ }
4009
+ }
4010
+
4011
+ renderView('groups');
4012
+ }
4013
+
4014
+ // 主题初始化函数(在页面加载时调用)
4015
+ function applyThemeOnLoad(theme) {
4016
+ const root = document.documentElement;
4017
+
4018
+ const themes = {
4019
+ dark: {
4020
+ primary: '#6366f1',
4021
+ primaryDark: '#4f46e5',
4022
+ secondary: '#8b5cf6',
4023
+ bgDark: '#0f172a',
4024
+ bgCard: '#1e293b',
4025
+ bgHover: '#334155',
4026
+ textPrimary: '#f1f5f9',
4027
+ textSecondary: '#94a3b8',
4028
+ border: '#334155'
4029
+ },
4030
+ blue: {
4031
+ primary: '#3b82f6',
4032
+ primaryDark: '#2563eb',
4033
+ secondary: '#06b6d4',
4034
+ bgDark: '#0c1222',
4035
+ bgCard: '#1a2332',
4036
+ bgHover: '#2a3442',
4037
+ textPrimary: '#e0f2fe',
4038
+ textSecondary: '#7dd3fc',
4039
+ border: '#2a3442'
4040
+ },
4041
+ green: {
4042
+ primary: '#10b981',
4043
+ primaryDark: '#059669',
4044
+ secondary: '#34d399',
4045
+ bgDark: '#0a1f1a',
4046
+ bgCard: '#1a2f2a',
4047
+ bgHover: '#2a3f3a',
4048
+ textPrimary: '#d1fae5',
4049
+ textSecondary: '#6ee7b7',
4050
+ border: '#2a3f3a'
4051
+ },
4052
+ orange: {
4053
+ primary: '#f59e0b',
4054
+ primaryDark: '#d97706',
4055
+ secondary: '#fb923c',
4056
+ bgDark: '#1f1a0a',
4057
+ bgCard: '#2f2a1a',
4058
+ bgHover: '#3f3a2a',
4059
+ textPrimary: '#fef3c7',
4060
+ textSecondary: '#fcd34d',
4061
+ border: '#3f3a2a'
4062
+ },
4063
+ pink: {
4064
+ primary: '#ec4899',
4065
+ primaryDark: '#db2777',
4066
+ secondary: '#f472b6',
4067
+ bgDark: '#1f0a1a',
4068
+ bgCard: '#2f1a2a',
4069
+ bgHover: '#3f2a3a',
4070
+ textPrimary: '#fce7f3',
4071
+ textSecondary: '#f9a8d4',
4072
+ border: '#3f2a3a'
4073
+ },
4074
+ light: {
4075
+ primary: '#6366f1',
4076
+ primaryDark: '#4f46e5',
4077
+ secondary: '#8b5cf6',
4078
+ bgDark: '#ffffff',
4079
+ bgCard: '#f8fafc',
4080
+ bgHover: '#e2e8f0',
4081
+ textPrimary: '#0f172a',
4082
+ textSecondary: '#475569',
4083
+ border: '#cbd5e1'
4084
+ }
4085
+ };
4086
+
4087
+ const selectedTheme = themes[theme] || themes.dark;
4088
+
4089
+ root.style.setProperty('--primary', selectedTheme.primary);
4090
+ root.style.setProperty('--primary-dark', selectedTheme.primaryDark);
4091
+ root.style.setProperty('--secondary', selectedTheme.secondary);
4092
+ root.style.setProperty('--bg-dark', selectedTheme.bgDark);
4093
+ root.style.setProperty('--bg-card', selectedTheme.bgCard);
4094
+ root.style.setProperty('--bg-hover', selectedTheme.bgHover);
4095
+ root.style.setProperty('--text-primary', selectedTheme.textPrimary);
4096
+ root.style.setProperty('--text-secondary', selectedTheme.textSecondary);
4097
+ root.style.setProperty('--border', selectedTheme.border);
4098
+ }
4099
+