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
@@ -1,1058 +1,1912 @@
1
- import { ApiService } from '../services/api.js';
2
- import { AuthService } from '../services/auth.js';
3
- import 'emoji-picker-element';
4
-
5
- export function renderUserDashboard(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
- app.innerHTML = `
15
- <div class="dashboard">
16
- <aside class="sidebar">
17
- <div class="sidebar-header">
18
- <h2>CollabDocChat</h2>
19
- <span class="badge-user">用户</span>
20
- </div>
21
-
22
- <div class="user-info">
23
- <div class="avatar">${user.username[0].toUpperCase()}</div>
24
- <div>
25
- <div class="username">${user.username}</div>
26
- <div class="user-role">普通用户</div>
27
- </div>
28
- </div>
29
-
30
- <nav class="nav-menu">
31
- <button class="nav-item active" data-view="groups">
32
- <span class="icon">👥</span> 我的群组
33
- </button>
34
- <button class="nav-item" data-view="allgroups">
35
- <span class="icon">🌐</span> 所有群组
36
- </button>
37
- <button class="nav-item" data-view="tasks">
38
- <span class="icon">📋</span> 我的任务
39
- </button>
40
- <button class="nav-item" data-view="documents">
41
- <span class="icon">📄</span> 共享文档
42
- </button>
43
- <button class="nav-item" data-view="files">
44
- <span class="icon">📎</span> 文件共享
45
- </button>
46
- <button class="nav-item" data-view="chat">
47
- <span class="icon">💬</span> 群聊
48
- </button>
49
- <button class="nav-item" data-view="search">
50
- <span class="icon">🔍</span> 搜索
1
+ import { ApiService } from '../services/api.js';
2
+ import { AuthService } from '../services/auth.js';
3
+ import 'emoji-picker-element';
4
+
5
+ export function renderUserDashboard(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
+ function formatMessageContent(content) {
16
+ // 处理白板作品
17
+ if (content.startsWith('[白板作品]')) {
18
+ const imageUrl = content.replace('[白板作品]', '').trim();
19
+
20
+ // 检查是否是文件URL(需要token)
21
+ const isFileUrl = imageUrl.includes('/api/files/') && imageUrl.includes('/download');
22
+
23
+ // 如果是文件URL但没有token,添加token
24
+ let finalUrl = imageUrl;
25
+ if (isFileUrl && !imageUrl.includes('token=')) {
26
+ const token = localStorage.getItem('token');
27
+ finalUrl = imageUrl.includes('?')
28
+ ? `${imageUrl}&token=${token}`
29
+ : `${imageUrl}?token=${token}`;
30
+ }
31
+
32
+ return `
33
+ <div style="background: linear-gradient(135deg, rgb(99, 102, 241) 0%, rgb(139, 92, 246) 100%); padding: 16px; border-radius: 12px; box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);">
34
+ <div style="margin-bottom: 12px; font-weight: 600; color: white; display: flex; align-items: center; gap: 6px; font-size: 15px;">
35
+ <span style="font-size: 18px;">🎨</span>
36
+ <span>白板作品</span>
37
+ </div>
38
+ <div style="position: relative; display: inline-block;">
39
+ <img src="${finalUrl}" alt="白板作品"
40
+ style="max-width: 400px; max-height: 400px; border-radius: 8px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.2); background: rgba(255,255,255,0.1);"
41
+ onclick="window.open('${finalUrl}', '_blank')"
42
+ onerror="this.style.display='none'; this.nextElementSibling.style.display='block'; console.error('白板图片加载失败:', '${finalUrl}');"
43
+ onload="console.log('白板图片加载成功:', '${finalUrl}');">
44
+ <div style="display: none; padding: 20px; background: rgba(255,255,255,0.1); border: 2px dashed rgba(255,255,255,0.3); border-radius: 8px; text-align: center; color: white;">
45
+ <div style="font-size: 48px; margin-bottom: 10px;">⚠️</div>
46
+ <div style="font-weight: 600; margin-bottom: 5px;">图片加载失败</div>
47
+ <div style="font-size: 12px;">图片可能已被删除或URL无效</div>
48
+ <button onclick="navigator.clipboard.writeText('${imageUrl}'); alert('图片URL已复制到剪贴板');" style="margin-top: 10px; padding: 6px 12px; background: rgba(255,255,255,0.2); color: white; border: none; border-radius: 6px; cursor: pointer;">复制图片URL</button>
49
+ </div>
50
+ <div style="margin-top: 10px; font-size: 12px; color: rgba(255,255,255,0.8);">点击图片查看大图</div>
51
+ </div>
52
+ </div>
53
+ `;
54
+ }
55
+
56
+ // 处理投票
57
+ if (content.startsWith('[投票]')) {
58
+ const pollId = content.replace('[投票]', '').trim();
59
+ return `
60
+ <div class="poll-card" data-poll-id="${pollId}" style="background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); border: 2px solid rgba(99, 102, 241, 0.3); border-radius: 12px; padding: 16px; cursor: pointer; transition: all 0.3s;" 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'" onclick="viewPollDetail('${pollId}')">
61
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
62
+ <span style="font-size: 32px;">📊</span>
63
+ <div style="flex: 1;">
64
+ <div style="font-weight: 600; font-size: 16px; color: var(--text-primary); margin-bottom: 4px;">投票</div>
65
+ <div style="font-size: 13px; color: var(--text-secondary);">点击查看详情并参与投票</div>
66
+ </div>
67
+ </div>
68
+ <div style="padding: 8px 12px; background: rgba(99, 102, 241, 0.1); border-radius: 6px; font-size: 12px; color: var(--primary); text-align: center;">
69
+ 📋 查看投票详情
70
+ </div>
71
+ </div>
72
+ `;
73
+ }
74
+
75
+ // 普通消息
76
+ return content;
77
+ }
78
+
79
+ // 全局函数:查看投票详情
80
+ window.viewPollDetail = async (pollId) => {
81
+ try {
82
+ const result = await apiService.getPoll(pollId);
83
+ const poll = result.poll;
84
+
85
+ const totalVotes = poll.options.reduce((sum, opt) => sum + opt.votes.length, 0);
86
+ const hasVoted = poll.options.some(opt => opt.votes.includes(currentUserId));
87
+ const isEnded = poll.status === 'ended' || (poll.endTime && new Date(poll.endTime) < new Date());
88
+
89
+ let optionsHtml = '';
90
+ poll.options.forEach((option, index) => {
91
+ const percentage = totalVotes > 0 ? (option.votes.length / totalVotes * 100).toFixed(1) : 0;
92
+ const isSelected = option.votes.includes(currentUserId);
93
+
94
+ optionsHtml += `
95
+ <div class="poll-option" style="margin-bottom: 12px; padding: 12px; background: var(--bg-tertiary); border-radius: 8px; border: 2px solid ${isSelected ? 'var(--primary)' : 'var(--border)'};">
96
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
97
+ <div style="display: flex; align-items: center; gap: 8px;">
98
+ <span style="font-weight: 500; color: var(--text-primary);">${option.text}</span>
99
+ ${isSelected ? '<span style="color: var(--primary); font-size: 12px;">✓ 已投票</span>' : ''}
100
+ </div>
101
+ <span style="font-weight: 600; color: var(--primary);">${option.votes.length} 票 (${percentage}%)</span>
102
+ </div>
103
+ <div style="height: 8px; background: var(--bg-secondary); border-radius: 4px; overflow: hidden;">
104
+ <div style="height: 100%; background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%); width: ${percentage}%; transition: width 0.3s;"></div>
105
+ </div>
106
+ ${!poll.anonymous && option.votes.length > 0 ? `
107
+ <div style="margin-top: 8px; font-size: 12px; color: var(--text-secondary);">
108
+ 投票者: ${option.voterNames ? option.voterNames.join(', ') : ''}
109
+ </div>
110
+ ` : ''}
111
+ </div>
112
+ `;
113
+ });
114
+
115
+ const modalHtml = `
116
+ <div id="pollDetailModal" class="modal" style="display: flex;">
117
+ <div class="modal-content" style="max-width: 700px; max-height: 90vh; overflow-y: auto;">
118
+ <div class="modal-header">
119
+ <h3>📊 投票详情</h3>
120
+ <button class="modal-close" id="closePollDetailModal">&times;</button>
121
+ </div>
122
+ <div class="modal-body" style="padding: 24px;">
123
+ <div style="margin-bottom: 24px;">
124
+ <h2 style="margin: 0 0 12px 0; color: var(--text-primary);">${poll.title}</h2>
125
+ ${poll.description ? `<p style="color: var(--text-secondary); margin: 0 0 16px 0;">${poll.description}</p>` : ''}
126
+
127
+ <div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px;">
128
+ <span style="font-size: 13px; padding: 6px 12px; background: var(--bg-tertiary); border-radius: 14px; color: var(--text-secondary);">
129
+ ${poll.allowMultiple ? '✓ 多选投票' : '○ 单选投票'}
130
+ </span>
131
+ <span style="font-size: 13px; padding: 6px 12px; background: var(--bg-tertiary); border-radius: 14px; color: var(--text-secondary);">
132
+ ${poll.anonymous ? '🔒 匿名投票' : '👤 实名投票'}
133
+ </span>
134
+ ${isEnded ? '<span style="font-size: 13px; padding: 6px 12px; background: var(--danger); border-radius: 14px; color: white;">已结束</span>' : '<span style="font-size: 13px; padding: 6px 12px; background: var(--success); border-radius: 14px; color: white;">进行中</span>'}
135
+ </div>
136
+
137
+ <div style="padding: 16px; background: var(--bg-secondary); border-radius: 12px; border-left: 4px solid var(--primary); margin-bottom: 24px;">
138
+ <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
139
+ <div>
140
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">创建者</div>
141
+ <div style="font-weight: 600; color: var(--text-primary);">👤 ${poll.creatorName}</div>
142
+ </div>
143
+ <div>
144
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">总投票数</div>
145
+ <div style="font-weight: 600; color: var(--primary);">👥 ${totalVotes} 人</div>
146
+ </div>
147
+ <div>
148
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">创建时间</div>
149
+ <div style="font-weight: 600; color: var(--text-primary);">⏰ ${new Date(poll.createdAt).toLocaleString('zh-CN')}</div>
150
+ </div>
151
+ ${poll.endTime ? `
152
+ <div>
153
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">截止时间</div>
154
+ <div style="font-weight: 600; color: var(--text-primary);">⏰ ${new Date(poll.endTime).toLocaleString('zh-CN')}</div>
155
+ </div>
156
+ ` : ''}
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+ <div style="margin-bottom: 24px;">
162
+ <h3 style="margin-bottom: 16px; color: var(--text-primary);">投票选项</h3>
163
+ ${!isEnded && !hasVoted ? `
164
+ <form id="voteForm">
165
+ ${poll.options.map((option, index) => {
166
+ const percentage = totalVotes > 0 ? (option.votes.length / totalVotes * 100).toFixed(1) : 0;
167
+ return `
168
+ <div class="poll-option" style="margin-bottom: 12px; padding: 12px; background: var(--bg-tertiary); border-radius: 8px; border: 2px solid var(--border); cursor: pointer;" onmouseover="this.style.borderColor='var(--primary)'" onmouseout="this.style.borderColor='var(--border)'">
169
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
170
+ <div style="display: flex; align-items: center; gap: 8px;">
171
+ <input type="${poll.allowMultiple ? 'checkbox' : 'radio'}" name="poll-option" value="${index}" style="width: 18px; height: 18px; cursor: pointer;">
172
+ <span style="font-weight: 500; color: var(--text-primary);">${option.text}</span>
173
+ </div>
174
+ <span style="font-weight: 600; color: var(--primary);">${option.votes.length} 票 (${percentage}%)</span>
175
+ </div>
176
+ <div style="height: 8px; background: var(--bg-secondary); border-radius: 4px; overflow: hidden;">
177
+ <div style="height: 100%; background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%); width: ${percentage}%; transition: width 0.3s;"></div>
178
+ </div>
179
+ </div>
180
+ `;
181
+ }).join('')}
182
+ <button type="submit" class="btn-primary" style="width: 100%; padding: 12px; margin-top: 16px;">提交投票</button>
183
+ <div style="text-align: center; color: var(--warning); margin-top: 12px; font-size: 13px;">⚠️ 提交后不可修改</div>
184
+ </form>
185
+ ` : optionsHtml}
186
+ ${hasVoted ? '<div style="text-align: center; color: var(--success); margin-top: 16px; padding: 12px; background: rgba(34, 197, 94, 0.1); border-radius: 8px; font-weight: 600;">✓ 您已参与投票,投票后不可修改</div>' : ''}
187
+ ${isEnded ? '<div style="text-align: center; color: var(--text-secondary); margin-top: 16px; padding: 12px; background: var(--bg-tertiary); border-radius: 8px;">投票已结束</div>' : ''}
188
+ </div>
189
+
190
+ <div style="display: flex; gap: 10px;">
191
+ <button class="btn-secondary" id="closePollDetailBtn" style="flex: 1;">关闭</button>
192
+ </div>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ `;
197
+
198
+ // 移除旧的模态框
199
+ const oldModal = document.getElementById('pollDetailModal');
200
+ if (oldModal) oldModal.remove();
201
+
202
+ // 添加新模态框
203
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
204
+
205
+ // 绑定事件
206
+ document.getElementById('closePollDetailModal').addEventListener('click', () => {
207
+ document.getElementById('pollDetailModal').remove();
208
+ });
209
+
210
+ document.getElementById('closePollDetailBtn').addEventListener('click', () => {
211
+ document.getElementById('pollDetailModal').remove();
212
+ });
213
+
214
+ // 投票表单提交
215
+ const voteForm = document.getElementById('voteForm');
216
+ if (voteForm && !isEnded && !hasVoted) {
217
+ voteForm.addEventListener('submit', async (e) => {
218
+ e.preventDefault();
219
+
220
+ const selectedOptions = Array.from(document.querySelectorAll('input[name="poll-option"]:checked'))
221
+ .map(input => parseInt(input.value));
222
+
223
+ if (selectedOptions.length === 0) {
224
+ alert('请选择至少一个选项!');
225
+ return;
226
+ }
227
+
228
+ try {
229
+ await apiService.vote(pollId, selectedOptions);
230
+ alert('投票成功!');
231
+ document.getElementById('pollDetailModal').remove();
232
+ // 刷新当前视图以更新投票状态
233
+ const activeView = document.querySelector('.nav-item.active');
234
+ if (activeView && activeView.dataset.view === 'tasks') {
235
+ const contentArea = document.getElementById('contentArea');
236
+ await renderTasksView(contentArea);
237
+ }
238
+ } catch (error) {
239
+ console.error('投票失败:', error);
240
+ alert('投票失败:' + error.message);
241
+ }
242
+ });
243
+ }
244
+
245
+ } catch (error) {
246
+ console.error('加载投票详情失败:', error);
247
+ alert('加载投票详情失败:' + error.message);
248
+ }
249
+ };
250
+
251
+ app.innerHTML = `
252
+ <div class="dashboard">
253
+ <aside class="sidebar">
254
+ <div class="sidebar-header">
255
+ <h2>CollabDocChat</h2>
256
+ <span class="badge-user">用户</span>
257
+ </div>
258
+
259
+ <div class="user-info">
260
+ <div class="avatar">${user.username[0].toUpperCase()}</div>
261
+ <div>
262
+ <div class="username">${user.username}</div>
263
+ <div class="user-role">普通用户</div>
264
+ </div>
265
+ </div>
266
+
267
+ <nav class="nav-menu">
268
+ <button class="nav-item active" data-view="groups">
269
+ <span class="icon">👥</span> 我的群组
270
+ </button>
271
+ <button class="nav-item" data-view="allgroups">
272
+ <span class="icon">🌐</span> 所有群组
273
+ </button>
274
+ <button class="nav-item" data-view="tasks">
275
+ <span class="icon">📋</span> 我的任务
276
+ </button>
277
+ <button class="nav-item" data-view="documents">
278
+ <span class="icon">📄</span> 共享文档
279
+ </button>
280
+ <button class="nav-item" data-view="files">
281
+ <span class="icon">📎</span> 文件共享
282
+ </button>
283
+ <button class="nav-item" data-view="chat">
284
+ <span class="icon">💬</span> 群聊
285
+ </button>
286
+ <button class="nav-item" data-view="search">
287
+ <span class="icon">🔍</span> 搜索
51
288
  </button>
52
289
  <button class="nav-item" data-view="knowledge">
53
290
  <span class="icon">📚</span> 知识库
54
291
  </button>
55
- <button class="nav-item" data-view="ai">
56
- <span class="icon">🤖</span> AI助手
57
- </button>
58
- </nav>
59
-
60
- <button class="btn-logout" id="logoutBtn">退出登录</button>
61
- </aside>
62
-
63
- <main class="main-content">
64
- <div id="contentArea"></div>
65
- </main>
66
- </div>
67
- `;
68
-
69
- // 导航切换
70
- document.querySelectorAll('.nav-item').forEach(item => {
71
- item.addEventListener('click', () => {
72
- document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
73
- item.classList.add('active');
74
- const view = item.dataset.view;
75
- renderView(view);
76
- });
77
- });
78
-
79
- // 退出登录
80
- document.getElementById('logoutBtn').addEventListener('click', () => {
81
- authService.logout();
82
- });
83
-
84
- async function renderView(view) {
85
- const contentArea = document.getElementById('contentArea');
86
-
87
- switch(view) {
88
- case 'groups':
89
- await renderGroupsView(contentArea);
90
- break;
91
- case 'allgroups':
92
- await renderAllGroupsView(contentArea);
93
- break;
94
- case 'tasks':
95
- await renderTasksView(contentArea);
96
- break;
97
- case 'documents':
98
- await renderDocumentsView(contentArea);
99
- break;
100
- case 'files':
101
- await renderFilesView(contentArea);
102
- break;
103
- case 'chat':
104
- await renderChatView(contentArea);
105
- break;
106
- case 'search':
107
- await renderSearchView(contentArea);
292
+ </nav>
293
+
294
+ <button class="btn-logout" id="logoutBtn">退出登录</button>
295
+ </aside>
296
+
297
+ <main class="main-content">
298
+ <div id="contentArea"></div>
299
+ </main>
300
+ </div>
301
+ `;
302
+
303
+ // 导航切换
304
+ document.querySelectorAll('.nav-item').forEach(item => {
305
+ item.addEventListener('click', () => {
306
+ document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
307
+ item.classList.add('active');
308
+ const view = item.dataset.view;
309
+ renderView(view);
310
+ });
311
+ });
312
+
313
+ // 退出登录
314
+ document.getElementById('logoutBtn').addEventListener('click', () => {
315
+ authService.logout();
316
+ });
317
+
318
+ async function renderView(view) {
319
+ const contentArea = document.getElementById('contentArea');
320
+
321
+ switch(view) {
322
+ case 'groups':
323
+ await renderGroupsView(contentArea);
324
+ break;
325
+ case 'allgroups':
326
+ await renderAllGroupsView(contentArea);
327
+ break;
328
+ case 'tasks':
329
+ await renderTasksView(contentArea);
330
+ break;
331
+ case 'documents':
332
+ await renderDocumentsView(contentArea);
333
+ break;
334
+ case 'files':
335
+ await renderFilesView(contentArea);
336
+ break;
337
+ case 'chat':
338
+ await renderChatView(contentArea);
339
+ break;
340
+ case 'search':
341
+ await renderSearchView(contentArea);
108
342
  break;
109
343
  case 'knowledge':
110
344
  await renderKnowledgeView(contentArea);
111
345
  break;
112
- case 'ai':
113
- await renderAIView(contentArea);
114
- break;
115
- }
116
- }
117
-
118
- async function renderGroupsView(container) {
119
- const result = await apiService.getGroups();
120
- groups = result.groups;
121
-
122
- container.innerHTML = `
123
- <div class="view-header">
124
- <h2>我的群组</h2>
125
- </div>
126
- <div class="groups-grid" id="groupsList"></div>
127
- `;
128
-
129
- const groupsList = document.getElementById('groupsList');
130
- if (groups.length === 0) {
131
- groupsList.innerHTML = '<div class="empty-state">您还没有加入任何群组<br>请前往"所有群组"查看并加入</div>';
132
- return;
133
- }
134
-
135
- groups.forEach(group => {
136
- const groupCard = document.createElement('div');
137
- groupCard.className = 'group-card';
138
- groupCard.innerHTML = `
139
- <h3>${group.name}</h3>
140
- <p>${group.description || '暂无描述'}</p>
141
- <div class="group-stats">
142
- <span>👥 ${group.members.length} 成员</span>
143
- <span>📄 ${group.documents.length} 文档</span>
144
- <span>📋 ${group.tasks.length} 任务</span>
145
- </div>
146
- <div style="display: flex; gap: 10px; margin-top: 10px;">
147
- <button class="btn-select" data-id="${group._id}">进入群组</button>
148
- <button class="btn-secondary" data-id="${group._id}" data-action="leave">退出群组</button>
149
- </div>
150
- `;
151
- groupsList.appendChild(groupCard);
152
- });
153
-
154
- document.querySelectorAll('.btn-select').forEach(btn => {
155
- btn.addEventListener('click', () => {
156
- currentGroup = groups.find(g => g._id === btn.dataset.id);
157
- wsService.joinGroup(currentGroup._id);
158
- alert(`已进入群组: ${currentGroup.name}`);
159
- });
160
- });
161
-
162
- document.querySelectorAll('[data-action="leave"]').forEach(btn => {
163
- btn.addEventListener('click', async () => {
164
- if (confirm('确定要退出该群组吗?')) {
165
- try {
166
- await apiService.leaveGroup(btn.dataset.id);
167
- alert('已退出群组');
168
- await renderGroupsView(container);
169
- } catch (error) {
170
- alert('退出失败: ' + error.message);
171
- }
172
- }
173
- });
174
- });
175
- }
176
-
177
- async function renderAllGroupsView(container) {
178
- const allGroupsResult = await apiService.getAllGroups();
179
- const myGroupsResult = await apiService.getGroups();
180
- const myGroupIds = myGroupsResult.groups.map(g => g._id);
181
-
182
- container.innerHTML = `
183
- <div class="view-header">
184
- <h2>所有群组</h2>
185
- </div>
186
- <div class="groups-grid" id="allGroupsList"></div>
187
- `;
188
-
189
- const allGroupsList = document.getElementById('allGroupsList');
190
- allGroupsResult.groups.forEach(group => {
191
- const isJoined = myGroupIds.includes(group._id);
192
- const groupCard = document.createElement('div');
193
- groupCard.className = 'group-card';
194
- groupCard.innerHTML = `
195
- <h3>${group.name}</h3>
196
- <p>${group.description || '暂无描述'}</p>
197
- <div class="group-stats">
198
- <span>👥 ${group.members.length} 成员</span>
199
- <span>📄 ${group.documents.length} 文档</span>
200
- </div>
201
- ${isJoined ?
202
- '<div style="color: var(--success); margin-top: 10px;">✓ 已加入</div>' :
203
- `<button class="btn-primary" data-id="${group._id}" data-action="join">加入群组</button>`
204
- }
205
- `;
206
- allGroupsList.appendChild(groupCard);
207
- });
208
-
209
- document.querySelectorAll('[data-action="join"]').forEach(btn => {
210
- btn.addEventListener('click', async () => {
211
- try {
212
- await apiService.joinGroup(btn.dataset.id);
213
- alert('加入成功!');
214
- await renderAllGroupsView(container);
215
- } catch (error) {
216
- alert('加入失败: ' + error.message);
217
- }
218
- });
219
- });
220
- }
221
-
222
- async function renderTasksView(container) {
223
- try {
224
- const result = await apiService.getMyTasks();
225
-
226
- container.innerHTML = `
227
- <div class="view-header">
228
- <h2>我的任务</h2>
229
- </div>
230
- <div class="tasks-list" id="tasksList"></div>
231
- `;
232
-
233
- const tasksList = document.getElementById('tasksList');
234
-
235
- if (result.tasks.length === 0) {
236
- tasksList.innerHTML = '<div class="empty-state">暂无任务</div>';
237
- return;
238
- }
239
-
240
- result.tasks.forEach(task => {
241
- const taskCard = document.createElement('div');
242
- taskCard.className = `task-card status-${task.status}`;
243
- taskCard.innerHTML = `
244
- <h3>${task.title}</h3>
245
- <p>${task.description}</p>
246
- <div class="task-meta">
247
- <span class="status-badge">${getStatusText(task.status)}</span>
248
- <span>群组: ${task.group.name}</span>
249
- ${task.deadline ? `<span>截止: ${new Date(task.deadline).toLocaleDateString()}</span>` : ''}
250
- </div>
251
- ${task.relatedDocument ? `<a href="#" class="doc-link" data-id="${task.relatedDocument._id}">📄 查看相关文档</a>` : ''}
252
- <div class="task-actions">
253
- ${task.status === 'pending' ? `<button class="btn-primary btn-sm" data-id="${task._id}" data-action="start">开始任务</button>` : ''}
254
- ${task.status === 'in_progress' ? `<button class="btn-success btn-sm" data-id="${task._id}" data-action="complete">完成任务</button>` : ''}
255
- </div>
256
- `;
257
- tasksList.appendChild(taskCard);
258
- });
259
-
260
- // 任务操作
261
- document.querySelectorAll('[data-action]').forEach(btn => {
262
- btn.addEventListener('click', async () => {
263
- const taskId = btn.dataset.id;
264
- const action = btn.dataset.action;
265
- const status = action === 'start' ? 'in_progress' : 'completed';
266
-
267
- try {
268
- await apiService.updateTaskStatus(taskId, status);
269
- await renderTasksView(container);
270
- } catch (error) {
271
- alert('操作失败: ' + error.message);
272
- }
273
- });
274
- });
275
- } catch (error) {
276
- console.error('获取任务失败:', error);
277
- container.innerHTML = `
278
- <div class="view-header">
279
- <h2>我的任务</h2>
280
- </div>
281
- <div class="empty-state">加载任务失败: ${error.message}</div>
282
- `;
283
- }
284
- }
285
-
286
- async function renderDocumentsView(container) {
287
- if (!currentGroup) {
288
- container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
289
- return;
290
- }
291
-
292
- const result = await apiService.getDocuments(currentGroup._id);
293
-
294
- container.innerHTML = `
295
- <div class="view-header">
296
- <h2>共享文档 - ${currentGroup.name}</h2>
297
- </div>
298
- <div class="documents-list" id="docsList"></div>
299
- `;
300
-
301
- const docsList = document.getElementById('docsList');
302
-
303
- if (result.documents.length === 0) {
304
- docsList.innerHTML = '<div class="empty-state">暂无文档</div>';
305
- return;
306
- }
307
-
308
- result.documents.forEach(doc => {
309
- const docCard = document.createElement('div');
310
- docCard.className = 'document-card';
311
- docCard.innerHTML = `
312
- <h3>📄 ${doc.title}</h3>
313
- <div class="doc-meta">
314
- <span>创建者: ${doc.creator.username}</span>
315
- <span>${doc.permission === 'readonly' ? '🔒 只读' : '✏️ 可编辑'}</span>
316
- <span>更新: ${new Date(doc.updatedAt).toLocaleString()}</span>
317
- </div>
318
- <button class="btn-edit" data-id="${doc._id}">
319
- ${doc.permission === 'readonly' ? '查看' : '编辑'}
320
- </button>
321
- `;
322
- docsList.appendChild(docCard);
323
- });
324
-
325
- document.querySelectorAll('.btn-edit').forEach(btn => {
326
- btn.addEventListener('click', () => {
327
- renderDocumentEditor(container, btn.dataset.id);
328
- });
329
- });
330
- }
331
-
332
- async function renderDocumentEditor(container, documentId) {
333
- const result = await apiService.getDocument(documentId);
334
- const doc = result.document;
335
-
336
- container.innerHTML = `
337
- <div class="view-header">
338
- <button class="btn-back" id="backBtn">← 返回</button>
339
- <h2>${doc.title}</h2>
340
- <span class="doc-status">${doc.permission === 'readonly' ? '🔒 只读模式' : '✏️ 编辑模式'}</span>
341
- </div>
342
- <div class="editor-container">
343
- <div class="editor-toolbar">
344
- <div class="online-users" id="onlineUsers">
345
- <span class="user-badge">👤 ${user.username}</span>
346
- </div>
347
- ${doc.permission === 'editable' ? '<button class="btn-primary" id="saveBtn">保存</button>' : ''}
348
- </div>
349
- <div id="editor" ${doc.permission === 'readonly' ? 'class="readonly"' : ''}></div>
350
- <div class="editor-footer">
351
- <span>最后编辑: ${new Date(doc.updatedAt).toLocaleString()}</span>
352
- </div>
353
- </div>
354
- `;
355
-
356
- // 初始化 Quill 编辑器
357
- const quill = new Quill('#editor', {
358
- theme: 'snow',
359
- modules: {
360
- toolbar: doc.permission === 'readonly' ? false : [
361
- [{ 'header': [1, 2, 3, false] }],
362
- ['bold', 'italic', 'underline', 'strike'],
363
- [{ 'list': 'ordered'}, { 'list': 'bullet' }],
364
- [{ 'color': [] }, { 'background': [] }],
365
- ['link', 'image', 'code-block'],
366
- ['clean']
367
- ]
368
- },
369
- readOnly: doc.permission === 'readonly'
370
- });
371
-
372
- // 设置初始内容
373
- quill.root.innerHTML = doc.content || '';
374
-
375
- // 实时同步
376
- if (doc.permission === 'editable') {
377
- let typingTimeout;
378
- let saveTimeout;
379
-
380
- quill.on('text-change', () => {
381
- clearTimeout(typingTimeout);
382
- clearTimeout(saveTimeout);
383
- wsService.sendTyping(documentId, user.username, true);
384
-
385
- typingTimeout = setTimeout(() => {
386
- wsService.sendTyping(documentId, user.username, false);
387
- }, 1000);
388
-
389
- // 自动保存
390
- saveTimeout = setTimeout(async () => {
391
- const content = quill.root.innerHTML;
392
- try {
393
- await apiService.updateDocument(documentId, content);
394
- } catch (error) {
395
- console.error('自动保存失败:', error);
396
- }
397
- }, 2000);
398
- });
399
-
400
- document.getElementById('saveBtn').addEventListener('click', async () => {
401
- try {
402
- const content = quill.root.innerHTML;
403
- await apiService.updateDocument(documentId, content);
404
- alert('保存成功!');
405
- } catch (error) {
406
- alert('保存失败: ' + error.message);
407
- }
408
- });
409
- }
410
-
411
- // 监听文档更新
412
- wsService.on('document_update', (data) => {
413
- if (data.documentId === documentId && data.userId !== user.id) {
414
- const selection = quill.getSelection();
415
- quill.root.innerHTML = data.content;
416
- if (selection) {
417
- quill.setSelection(selection);
418
- }
419
- }
420
- });
421
-
422
- // 监听打字状态
423
- wsService.on('typing', (data) => {
424
- if (data.documentId === documentId && data.userId !== user.id) {
425
- const onlineUsers = document.getElementById('onlineUsers');
426
- if (data.isTyping) {
427
- onlineUsers.innerHTML += `<span class="user-badge typing" data-user="${data.userId}">✏️ ${data.username}</span>`;
428
- } else {
429
- const badge = onlineUsers.querySelector(`[data-user="${data.userId}"]`);
430
- if (badge) badge.remove();
431
- }
432
- }
433
- });
434
-
435
- document.getElementById('backBtn').addEventListener('click', () => {
436
- renderDocumentsView(container);
437
- });
438
- }
439
-
440
- async function renderFilesView(container) {
441
- if (!currentGroup) {
442
- container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
443
- return;
444
- }
445
-
446
- try {
447
- const result = await apiService.getGroupFiles(currentGroup._id);
448
-
449
- container.innerHTML = `
450
- <div class="view-header">
451
- <h2>文件共享 - ${currentGroup.name}</h2>
452
- <button class="btn-primary" id="uploadFileBtn">📤 上传文件</button>
453
- </div>
454
- <div class="files-list" id="filesList"></div>
455
-
456
- <!-- 文件上传模态框 -->
457
- <div class="modal hidden" id="uploadFileModal">
458
- <div class="modal-content">
459
- <div class="modal-header">
460
- <h3>上传文件</h3>
461
- <button class="modal-close" id="closeUploadModal">&times;</button>
462
- </div>
463
- <form id="uploadFileForm">
464
- <div class="form-group">
465
- <label>选择文件</label>
466
- <input type="file" id="fileInput" required>
467
- <small>支持图片、PDF、Word、Excel等,最大10MB</small>
468
- </div>
469
- <div class="form-group">
470
- <label>描述(可选)</label>
471
- <textarea id="fileDescription" rows="3" placeholder="文件描述..."></textarea>
472
- </div>
473
- <div class="form-actions">
474
- <button type="button" class="btn-secondary" id="cancelUpload">取消</button>
475
- <button type="submit" class="btn-primary">上传</button>
476
- </div>
477
- </form>
478
- </div>
479
- </div>
480
- `;
481
-
482
- const filesList = document.getElementById('filesList');
483
-
484
- if (!result.files || result.files.length === 0) {
485
- filesList.innerHTML = '<div class="empty-state">暂无文件</div>';
486
- } else {
487
- result.files.forEach(file => {
488
- const fileCard = document.createElement('div');
489
- fileCard.className = 'file-card';
490
-
491
- const fileIcon = getFileIcon(file.mimetype);
492
- const fileSize = formatFileSize(file.size);
493
-
494
- fileCard.innerHTML = `
495
- <div class="file-icon">${fileIcon}</div>
496
- <div class="file-info">
497
- <h4>${file.originalName}</h4>
498
- <div class="file-meta">
499
- <span>上传者: ${file.uploader.username}</span>
500
- <span>大小: ${fileSize}</span>
501
- <span>时间: ${new Date(file.createdAt).toLocaleString()}</span>
502
- </div>
503
- ${file.description ? `<p class="file-description">${file.description}</p>` : ''}
504
- </div>
505
- <div class="file-actions">
506
- <a href="${apiService.getFileDownloadUrl(file._id)}" class="btn-primary" download>下载</a>
507
- ${file.uploader._id === currentUserId ? `<button class="btn-danger" data-id="${file._id}" data-action="delete-file">删除</button>` : ''}
508
- </div>
509
- `;
510
- filesList.appendChild(fileCard);
511
- });
512
-
513
- // 删除文件事件
514
- document.querySelectorAll('[data-action="delete-file"]').forEach(btn => {
515
- btn.addEventListener('click', async () => {
516
- if (confirm('确定要删除这个文件吗?')) {
517
- try {
518
- await apiService.deleteFile(btn.dataset.id);
519
- alert('文件删除成功!');
520
- await renderFilesView(container);
521
- } catch (error) {
522
- alert('删除失败: ' + error.message);
523
- }
524
- }
525
- });
526
- });
527
- }
528
-
529
- // 文件上传功能
530
- document.getElementById('uploadFileBtn').addEventListener('click', () => {
531
- document.getElementById('uploadFileModal').classList.remove('hidden');
532
- });
533
-
534
- document.getElementById('closeUploadModal').addEventListener('click', () => {
535
- document.getElementById('uploadFileModal').classList.add('hidden');
536
- document.getElementById('uploadFileForm').reset();
537
- });
538
-
539
- document.getElementById('cancelUpload').addEventListener('click', () => {
540
- document.getElementById('uploadFileModal').classList.add('hidden');
541
- document.getElementById('uploadFileForm').reset();
542
- });
543
-
544
- document.getElementById('uploadFileForm').addEventListener('submit', async (e) => {
545
- e.preventDefault();
546
- const fileInput = document.getElementById('fileInput');
547
- const description = document.getElementById('fileDescription').value;
548
-
549
- if (!fileInput.files[0]) {
550
- alert('请选择文件');
551
- return;
552
- }
553
-
554
- try {
555
- await apiService.uploadFile(currentGroup._id, fileInput.files[0], description);
556
- alert('文件上传成功!');
557
- document.getElementById('uploadFileModal').classList.add('hidden');
558
- document.getElementById('uploadFileForm').reset();
559
- await renderFilesView(container);
560
- } catch (error) {
561
- alert('上传失败: ' + error.message);
562
- }
563
- });
564
- } catch (error) {
565
- console.error('获取文件列表失败:', error);
566
- container.innerHTML = `
567
- <div class="view-header">
568
- <h2>文件共享</h2>
569
- </div>
570
- <div class="empty-state">加载文件失败: ${error.message}</div>
571
- `;
572
- }
573
- }
574
-
575
- function getFileIcon(mimetype) {
576
- if (mimetype.startsWith('image/')) return '🖼️';
577
- if (mimetype === 'application/pdf') return '📕';
578
- if (mimetype.includes('word') || mimetype.includes('document')) return '📘';
579
- if (mimetype.includes('excel') || mimetype.includes('spreadsheet')) return '📗';
580
- if (mimetype.includes('zip') || mimetype.includes('compressed')) return '📦';
581
- return '📄';
582
- }
583
-
584
- function formatFileSize(bytes) {
585
- if (bytes === 0) return '0 Bytes';
586
- const k = 1024;
587
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
588
- const i = Math.floor(Math.log(bytes) / Math.log(k));
589
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
590
- }
591
-
592
- async function renderChatView(container) {
593
- if (!currentGroup) {
594
- container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
595
- return;
596
- }
597
-
598
- const groupResult = await apiService.getGroup(currentGroup._id);
599
- const group = groupResult.group;
600
- const isMutedAll = Boolean(group.mutedAll);
601
- const isMutedMe = (group.mutedUsers || []).map(String).includes(String(currentUserId));
602
- const canSpeak = !isMutedAll && !isMutedMe;
603
-
604
- container.innerHTML = `
605
- <div class="view-header">
606
- <h2>群聊 - ${currentGroup.name}</h2>
607
- </div>
608
- <div class="chat-container">
609
- <div class="messages" id="messages"></div>
610
- <div class="chat-input">
611
- <button class="btn-emoji" id="emojiBtn" ${canSpeak ? '' : 'disabled'}>😊</button>
612
- <input type="text" id="messageInput" placeholder="${canSpeak ? '输入消息...' : (isMutedAll ? '全体禁言中,无法发言' : '你已被禁言')}" ${canSpeak ? '' : 'disabled'}>
613
- <button class="btn-primary" id="sendBtn" ${canSpeak ? '' : 'disabled'}>发送</button>
614
- </div>
615
- <emoji-picker id="emojiPicker" class="hidden"></emoji-picker>
616
- </div>
617
- `;
618
-
619
- const messagesDiv = document.getElementById('messages');
620
- const messageInput = document.getElementById('messageInput');
621
- const sendBtn = document.getElementById('sendBtn');
622
-
623
- // 加载历史消息
624
- try {
625
- const messagesResult = await apiService.getGroupMessages(currentGroup._id);
626
- if (messagesResult.messages) {
627
- messagesResult.messages.forEach(msg => {
628
- const messageEl = document.createElement('div');
629
- messageEl.className = `message ${msg.sender === currentUserId ? 'own' : ''}`;
630
- messageEl.innerHTML = `
631
- <div class="message-header">
632
- <span class="message-user">${msg.username}</span>
633
- <span class="message-time">${new Date(msg.timestamp).toLocaleTimeString()}</span>
634
- </div>
635
- <div class="message-content">${msg.content}</div>
636
- `;
637
- messagesDiv.appendChild(messageEl);
638
- });
639
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
640
- }
641
- } catch (err) {
642
- console.error('加载历史消息失败:', err);
643
- }
644
-
645
- // 表情包功能
646
- const emojiBtn = document.getElementById('emojiBtn');
647
- const emojiPicker = document.getElementById('emojiPicker');
648
-
649
- emojiBtn.addEventListener('click', () => {
650
- emojiPicker.classList.toggle('hidden');
651
- });
652
-
653
- emojiPicker.addEventListener('emoji-click', (event) => {
654
- messageInput.value += event.detail.unicode;
655
- messageInput.focus();
656
- emojiPicker.classList.add('hidden');
657
- });
658
-
659
- // 点击外部关闭表情选择器
660
- document.addEventListener('click', (e) => {
661
- if (!emojiBtn.contains(e.target) && !emojiPicker.contains(e.target)) {
662
- emojiPicker.classList.add('hidden');
663
- }
664
- });
665
-
666
- // 消息通知系统
667
- function showNotification(title, body, icon = '💬') {
668
- if ('Notification' in window && Notification.permission === 'granted') {
669
- new Notification(title, {
670
- body: body,
671
- icon: '/icon.png',
672
- badge: '/icon.png',
673
- tag: 'chat-message'
674
- });
675
- }
676
- }
677
-
678
- // 请求通知权限
679
- if ('Notification' in window && Notification.permission === 'default') {
680
- Notification.requestPermission();
681
- }
682
-
683
- // 监听消息
684
- wsService.on('chat_message', (data) => {
685
- if (data.groupId === currentGroup._id) {
686
- const messageEl = document.createElement('div');
687
- messageEl.className = `message ${data.userId === currentUserId ? 'own' : ''}`;
688
- messageEl.innerHTML = `
689
- <div class="message-header">
690
- <span class="message-user">${data.username}</span>
691
- <span class="message-time">${new Date(data.timestamp).toLocaleTimeString()}</span>
692
- </div>
693
- <div class="message-content">${data.content}</div>
694
- `;
695
- messagesDiv.appendChild(messageEl);
696
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
697
-
698
- // 显示通知(如果不是自己发送的消息)
699
- if (data.userId !== currentUserId) {
700
- showNotification(`${data.username} 在 ${currentGroup.name}`, data.content);
701
- }
702
- }
703
- });
704
-
705
- // 服务端拦截提示(比如被禁言/未加入群组)
706
- wsService.on('chat_blocked', (data) => {
707
- if (data.groupId === currentGroup._id) {
708
- const notificationEl = document.createElement('div');
709
- notificationEl.className = 'notification';
710
- notificationEl.textContent = data.message || '消息发送失败';
711
- messagesDiv.appendChild(notificationEl);
712
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
713
- }
714
- });
715
-
716
- // 监听点名通知
717
- wsService.on('call_response', (data) => {
718
- if (data.groupId === currentGroup._id) {
719
- const notificationEl = document.createElement('div');
720
- notificationEl.className = 'notification';
721
- notificationEl.textContent = `${data.username} 已响应点名`;
722
- messagesDiv.appendChild(notificationEl);
723
- }
724
- });
725
-
726
- // 发送消息
727
- const sendMessage = () => {
728
- const content = messageInput.value.trim();
729
- if (content) {
730
- wsService.sendChatMessage(currentGroup._id, user.username, content);
731
- messageInput.value = '';
732
- }
733
- };
734
-
735
- sendBtn.addEventListener('click', sendMessage);
736
- messageInput.addEventListener('keypress', (e) => {
737
- if (e.key === 'Enter') sendMessage();
738
- });
739
- }
740
-
741
- async function renderSearchView(container) {
742
- container.innerHTML = `
743
- <div class="view-header">
744
- <h2>🔍 搜索</h2>
745
- </div>
746
- <div class="search-container">
747
- <div class="search-box">
748
- <input type="text" id="searchInput" placeholder="搜索消息、文档、任务...">
749
- <button class="btn-primary" id="searchBtn">搜索</button>
750
- </div>
751
- <div class="search-filters">
752
- <label>
753
- <input type="checkbox" id="filterMessages" checked> 消息
754
- </label>
755
- <label>
756
- <input type="checkbox" id="filterDocuments" checked> 文档
757
- </label>
758
- <label>
759
- <input type="checkbox" id="filterTasks" checked> 任务
760
- </label>
761
- </div>
762
- <div class="search-results" id="searchResults"></div>
763
- </div>
764
- `;
765
-
766
- const searchInput = document.getElementById('searchInput');
767
- const searchBtn = document.getElementById('searchBtn');
768
- const searchResults = document.getElementById('searchResults');
769
-
770
- const performSearch = async () => {
771
- const query = searchInput.value.trim();
772
- if (!query) {
773
- searchResults.innerHTML = '<div class="empty-state">请输入搜索关键词</div>';
774
- return;
775
- }
776
-
777
- const filters = {
778
- messages: document.getElementById('filterMessages').checked,
779
- documents: document.getElementById('filterDocuments').checked,
780
- tasks: document.getElementById('filterTasks').checked
781
- };
782
-
783
- searchResults.innerHTML = '<div class="loading">搜索中...</div>';
784
-
785
- try {
786
- const results = [];
787
-
788
- // 搜索消息
789
- if (filters.messages && currentGroup) {
790
- try {
791
- const messagesResult = await apiService.getGroupMessages(currentGroup._id);
792
- if (messagesResult.messages) {
793
- const matchedMessages = messagesResult.messages.filter(msg =>
794
- msg.content.toLowerCase().includes(query.toLowerCase())
795
- );
796
- matchedMessages.forEach(msg => {
797
- results.push({
798
- type: 'message',
799
- title: `消息 - ${msg.username}`,
800
- content: msg.content,
801
- time: msg.timestamp,
802
- group: currentGroup.name
803
- });
804
- });
805
- }
806
- } catch (err) {
807
- console.error('搜索消息失败:', err);
808
- }
809
- }
810
-
811
- // 搜索文档
812
- if (filters.documents) {
813
- try {
814
- if (currentGroup) {
815
- const docsResult = await apiService.getDocuments(currentGroup._id);
816
- if (docsResult.documents) {
817
- const matchedDocs = docsResult.documents.filter(doc =>
818
- doc.title.toLowerCase().includes(query.toLowerCase()) ||
819
- doc.content.toLowerCase().includes(query.toLowerCase())
820
- );
821
- matchedDocs.forEach(doc => {
822
- results.push({
823
- type: 'document',
824
- title: doc.title,
825
- content: doc.content.substring(0, 200),
826
- time: doc.updatedAt,
827
- id: doc._id,
828
- group: currentGroup.name
829
- });
830
- });
831
- }
832
- }
833
- } catch (err) {
834
- console.error('搜索文档失败:', err);
835
- }
836
- }
837
-
838
- // 搜索任务
839
- if (filters.tasks) {
840
- try {
841
- const tasksResult = await apiService.getMyTasks();
842
- if (tasksResult.tasks) {
843
- const matchedTasks = tasksResult.tasks.filter(task =>
844
- task.title.toLowerCase().includes(query.toLowerCase()) ||
845
- (task.description && task.description.toLowerCase().includes(query.toLowerCase()))
846
- );
847
- matchedTasks.forEach(task => {
848
- results.push({
849
- type: 'task',
850
- title: task.title,
851
- content: task.description || '',
852
- time: task.updatedAt,
853
- id: task._id,
854
- status: task.status
855
- });
856
- });
857
- }
858
- } catch (err) {
859
- console.error('搜索任务失败:', err);
860
- }
861
- }
862
-
863
- // 显示结果
864
- if (results.length === 0) {
865
- searchResults.innerHTML = '<div class="empty-state">未找到相关结果</div>';
866
- } else {
867
- searchResults.innerHTML = results.map(result => {
868
- const typeIcon = {
869
- message: '💬',
870
- document: '📄',
871
- task: '📋'
872
- };
873
- return `
874
- <div class="search-result-item">
875
- <div class="result-header">
876
- <span class="result-type">${typeIcon[result.type]} ${result.type === 'message' ? '消息' : result.type === 'document' ? '文档' : '任务'}</span>
877
- <span class="result-time">${new Date(result.time).toLocaleString()}</span>
878
- </div>
879
- <h4>${highlightText(result.title, query)}</h4>
880
- <p>${highlightText(result.content, query)}</p>
881
- ${result.group ? `<span class="result-group">群组: ${result.group}</span>` : ''}
882
- ${result.status ? `<span class="result-status">状态: ${getStatusText(result.status)}</span>` : ''}
883
- </div>
884
- `;
885
- }).join('');
886
- }
887
- } catch (error) {
888
- searchResults.innerHTML = `<div class="empty-state">搜索失败: ${error.message}</div>`;
889
- }
890
- };
891
-
892
- searchBtn.addEventListener('click', performSearch);
893
- searchInput.addEventListener('keypress', (e) => {
894
- if (e.key === 'Enter') performSearch();
895
- });
896
- }
897
-
898
- function highlightText(text, query) {
899
- if (!query) return text;
900
- const regex = new RegExp(`(${query})`, 'gi');
901
- return text.replace(regex, '<mark>$1</mark>');
902
- }
903
-
904
- function getStatusText(status) {
905
- const statusMap = {
906
- 'pending': '待处理',
907
- 'in_progress': '进行中',
908
- 'completed': '已完成',
909
- 'terminated': '已终止'
910
- };
911
- return statusMap[status] || status;
912
- }
913
-
914
-
915
- // 知识库(用户版)
916
- async function renderKnowledgeView(container) {
346
+ }
347
+ }
348
+
349
+ async function renderGroupsView(container) {
350
+ const result = await apiService.getGroups();
351
+ groups = result.groups;
352
+
353
+ container.innerHTML = `
354
+ <div class="view-header">
355
+ <h2>我的群组</h2>
356
+ </div>
357
+ <div class="groups-grid" id="groupsList"></div>
358
+ `;
359
+
360
+ const groupsList = document.getElementById('groupsList');
361
+ if (groups.length === 0) {
362
+ groupsList.innerHTML = '<div class="empty-state">您还没有加入任何群组<br>请前往"所有群组"查看并加入</div>';
363
+ return;
364
+ }
365
+
366
+ groups.forEach(group => {
367
+ const groupCard = document.createElement('div');
368
+ groupCard.className = 'group-card';
369
+ groupCard.innerHTML = `
370
+ <h3>${group.name}</h3>
371
+ <p>${group.description || '暂无描述'}</p>
372
+ <div class="group-stats">
373
+ <span>👥 ${group.members.length} 成员</span>
374
+ <span>📄 ${group.documents.length} 文档</span>
375
+ <span>📋 ${group.tasks.length} 任务</span>
376
+ </div>
377
+ <div style="display: flex; gap: 10px; margin-top: 10px;">
378
+ <button class="btn-select" data-id="${group._id}">进入群组</button>
379
+ <button class="btn-secondary" data-id="${group._id}" data-action="leave">退出群组</button>
380
+ </div>
381
+ `;
382
+ groupsList.appendChild(groupCard);
383
+ });
384
+
385
+ document.querySelectorAll('.btn-select').forEach(btn => {
386
+ btn.addEventListener('click', () => {
387
+ currentGroup = groups.find(g => g._id === btn.dataset.id);
388
+ wsService.joinGroup(currentGroup._id);
389
+ alert(`已进入群组: ${currentGroup.name}`);
390
+ });
391
+ });
392
+
393
+ document.querySelectorAll('[data-action="leave"]').forEach(btn => {
394
+ btn.addEventListener('click', async () => {
395
+ if (confirm('确定要退出该群组吗?')) {
396
+ try {
397
+ await apiService.leaveGroup(btn.dataset.id);
398
+ alert('已退出群组');
399
+ await renderGroupsView(container);
400
+ } catch (error) {
401
+ alert('退出失败: ' + error.message);
402
+ }
403
+ }
404
+ });
405
+ });
406
+ }
407
+
408
+ async function renderAllGroupsView(container) {
409
+ const allGroupsResult = await apiService.getAllGroups();
410
+ const myGroupsResult = await apiService.getGroups();
411
+ const myGroupIds = myGroupsResult.groups.map(g => g._id);
412
+
413
+ container.innerHTML = `
414
+ <div class="view-header">
415
+ <h2>所有群组</h2>
416
+ </div>
417
+ <div class="groups-grid" id="allGroupsList"></div>
418
+ `;
419
+
420
+ const allGroupsList = document.getElementById('allGroupsList');
421
+ allGroupsResult.groups.forEach(group => {
422
+ const isJoined = myGroupIds.includes(group._id);
423
+ const groupCard = document.createElement('div');
424
+ groupCard.className = 'group-card';
425
+ groupCard.innerHTML = `
426
+ <h3>${group.name}</h3>
427
+ <p>${group.description || '暂无描述'}</p>
428
+ <div class="group-stats">
429
+ <span>👥 ${group.members.length} 成员</span>
430
+ <span>📄 ${group.documents.length} 文档</span>
431
+ </div>
432
+ ${isJoined ?
433
+ '<div style="color: var(--success); margin-top: 10px;">✓ 已加入</div>' :
434
+ `<button class="btn-primary" data-id="${group._id}" data-action="join">加入群组</button>`
435
+ }
436
+ `;
437
+ allGroupsList.appendChild(groupCard);
438
+ });
439
+
440
+ document.querySelectorAll('[data-action="join"]').forEach(btn => {
441
+ btn.addEventListener('click', async () => {
442
+ try {
443
+ await apiService.joinGroup(btn.dataset.id);
444
+ alert('加入成功!');
445
+ await renderAllGroupsView(container);
446
+ } catch (error) {
447
+ alert('加入失败: ' + error.message);
448
+ }
449
+ });
450
+ });
451
+ }
452
+
453
+ async function renderTasksView(container) {
454
+ try {
455
+ const result = await apiService.getMyTasks();
456
+
457
+ // 获取用户所在群组的投票
458
+ let polls = [];
459
+ try {
460
+ const groupsResult = await apiService.getGroups();
461
+ const myGroups = groupsResult.groups;
462
+
463
+ // 获取所有群组的投票
464
+ for (const group of myGroups) {
465
+ try {
466
+ const pollsResult = await apiService.getGroupPolls(group._id);
467
+ if (pollsResult.polls && Array.isArray(pollsResult.polls)) {
468
+ polls = polls.concat(pollsResult.polls.map(poll => ({
469
+ ...poll,
470
+ groupName: group.name
471
+ })));
472
+ }
473
+ } catch (err) {
474
+ console.error(`获取群组 ${group.name} 的投票失败:`, err);
475
+ }
476
+ }
477
+ } catch (err) {
478
+ console.error('获取投票失败:', err);
479
+ }
480
+
481
+ container.innerHTML = `
482
+ <div class="view-header">
483
+ <h2>我的任务</h2>
484
+ </div>
485
+ <div class="tasks-list" id="tasksList"></div>
486
+ `;
487
+
488
+ const tasksList = document.getElementById('tasksList');
489
+
490
+ if (result.tasks.length === 0 && polls.length === 0) {
491
+ tasksList.innerHTML = '<div class="empty-state">暂无任务</div>';
492
+ return;
493
+ }
494
+
495
+ // 渲染普通任务
496
+ result.tasks.forEach(task => {
497
+ const taskCard = document.createElement('div');
498
+ taskCard.className = `task-card status-${task.status}`;
499
+ taskCard.innerHTML = `
500
+ <h3>${task.title}</h3>
501
+ <p>${task.description}</p>
502
+ <div class="task-meta">
503
+ <span class="status-badge">${getStatusText(task.status)}</span>
504
+ <span>群组: ${task.group.name}</span>
505
+ ${task.deadline ? `<span>截止: ${new Date(task.deadline).toLocaleDateString()}</span>` : ''}
506
+ </div>
507
+ ${task.relatedDocument ? `<a href="#" class="doc-link" data-id="${task.relatedDocument._id}">📄 查看相关文档</a>` : ''}
508
+ <div class="task-actions">
509
+ ${task.status === 'pending' ? `<button class="btn-primary btn-sm" data-id="${task._id}" data-action="start">开始任务</button>` : ''}
510
+ ${task.status === 'in_progress' ? `<button class="btn-success btn-sm" data-id="${task._id}" data-action="complete">完成任务</button>` : ''}
511
+ </div>
512
+ `;
513
+ tasksList.appendChild(taskCard);
514
+ });
515
+
516
+ // 渲染投票任务
517
+ polls.forEach(poll => {
518
+ const totalVotes = poll.options.reduce((sum, opt) => sum + opt.votes.length, 0);
519
+ const hasVoted = poll.options.some(opt => opt.votes.includes(currentUserId));
520
+ const isEnded = poll.status === 'ended' || (poll.endTime && new Date(poll.endTime) < new Date());
521
+
522
+ const pollCard = document.createElement('div');
523
+ pollCard.className = 'task-card poll-task';
524
+ pollCard.style.cssText = 'background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(168, 85, 247, 0.05) 100%); border-left: 4px solid var(--primary);';
525
+ pollCard.innerHTML = `
526
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
527
+ <span style="font-size: 32px;">📊</span>
528
+ <h3 style="margin: 0;">${poll.title}</h3>
529
+ </div>
530
+ <p>${poll.description || '暂无描述'}</p>
531
+ <div class="task-meta">
532
+ <span class="status-badge" style="background: ${isEnded ? 'var(--danger)' : 'var(--success)'};">${isEnded ? '已结束' : '进行中'}</span>
533
+ <span>群组: ${poll.groupName}</span>
534
+ <span>👥 ${totalVotes} 人投票</span>
535
+ ${hasVoted ? '<span style="color: var(--success);">✓ 已投票</span>' : '<span style="color: var(--warning);">⏳ 待投票</span>'}
536
+ </div>
537
+ <div class="task-actions">
538
+ <button class="btn-primary btn-sm" data-poll-id="${poll._id}" data-action="view-poll">查看详情</button>
539
+ </div>
540
+ `;
541
+ tasksList.appendChild(pollCard);
542
+ });
543
+
544
+ // 任务操作
545
+ document.querySelectorAll('[data-action="start"], [data-action="complete"]').forEach(btn => {
546
+ btn.addEventListener('click', async () => {
547
+ const taskId = btn.dataset.id;
548
+ const action = btn.dataset.action;
549
+ const status = action === 'start' ? 'in_progress' : 'completed';
550
+
551
+ try {
552
+ await apiService.updateTaskStatus(taskId, status);
553
+ await renderTasksView(container);
554
+ } catch (error) {
555
+ alert('操作失败: ' + error.message);
556
+ }
557
+ });
558
+ });
559
+
560
+ // 投票查看操作
561
+ document.querySelectorAll('[data-action="view-poll"]').forEach(btn => {
562
+ btn.addEventListener('click', () => {
563
+ const pollId = btn.dataset.pollId;
564
+ window.viewPollDetail(pollId);
565
+ });
566
+ });
567
+ } catch (error) {
568
+ console.error('获取任务失败:', error);
569
+ container.innerHTML = `
570
+ <div class="view-header">
571
+ <h2>我的任务</h2>
572
+ </div>
573
+ <div class="empty-state">加载任务失败: ${error.message}</div>
574
+ `;
575
+ }
576
+ }
577
+
578
+ async function renderDocumentsView(container) {
917
579
  if (!currentGroup) {
918
580
  container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
919
581
  return;
920
582
  }
921
583
 
922
- try {
923
- const token = localStorage.getItem('token');
924
- const response = await fetch(`http://localhost:8765/api/knowledge/group/${currentGroup._id}`, {
925
- headers: { 'Authorization': `Bearer ${token}` }
584
+ const result = await apiService.getDocuments(currentGroup._id);
585
+
586
+ container.innerHTML = `
587
+ <div class="view-header">
588
+ <h2>共享文档 - ${currentGroup.name}</h2>
589
+ </div>
590
+ <div class="documents-list" id="docsList"></div>
591
+ `;
592
+
593
+ const docsList = document.getElementById('docsList');
594
+
595
+ if (result.documents.length === 0) {
596
+ docsList.innerHTML = '<div class="empty-state">暂无文档</div>';
597
+ return;
598
+ }
599
+
600
+ result.documents.forEach(doc => {
601
+ const docCard = document.createElement('div');
602
+ docCard.className = 'document-card';
603
+ docCard.innerHTML = `
604
+ <h3>📄 ${doc.title}</h3>
605
+ <div class="doc-meta">
606
+ <span>创建者: ${doc.creator.username}</span>
607
+ <span>${doc.permission === 'readonly' ? '🔒 只读' : '✏️ 可编辑'}</span>
608
+ <span>更新: ${new Date(doc.updatedAt).toLocaleString()}</span>
609
+ </div>
610
+ <button class="btn-edit" data-id="${doc._id}">
611
+ ${doc.permission === 'readonly' ? '查看' : '编辑'}
612
+ </button>
613
+ `;
614
+ docsList.appendChild(docCard);
615
+ });
616
+
617
+ document.querySelectorAll('.btn-edit').forEach(btn => {
618
+ btn.addEventListener('click', () => {
619
+ renderDocumentEditor(container, btn.dataset.id);
926
620
  });
927
- const result = await response.json();
928
- const knowledgeItems = result.data || [];
621
+ });
622
+ }
623
+
624
+ async function renderDocumentEditor(container, documentId) {
625
+ const result = await apiService.getDocument(documentId);
626
+ const doc = result.document;
627
+
628
+ container.innerHTML = `
629
+ <div class="view-header">
630
+ <button class="btn-back" id="backBtn">← 返回</button>
631
+ <h2>${doc.title}</h2>
632
+ <span class="doc-status">${doc.permission === 'readonly' ? '🔒 只读模式' : '✏️ 编辑模式'}</span>
633
+ </div>
634
+ <div class="editor-container">
635
+ <div class="editor-toolbar">
636
+ <div class="online-users" id="onlineUsers">
637
+ <span class="user-badge">👤 ${user.username}</span>
638
+ </div>
639
+ ${doc.permission === 'editable' ? '<button class="btn-primary" id="saveBtn">保存</button>' : ''}
640
+ </div>
641
+ <div id="editor" ${doc.permission === 'readonly' ? 'class="readonly"' : ''}></div>
642
+ <div class="editor-footer">
643
+ <span>最后编辑: ${new Date(doc.updatedAt).toLocaleString()}</span>
644
+ </div>
645
+ </div>
646
+ `;
647
+
648
+ // 初始化 Quill 编辑器
649
+ const quill = new Quill('#editor', {
650
+ theme: 'snow',
651
+ modules: {
652
+ toolbar: doc.permission === 'readonly' ? false : [
653
+ [{ 'header': [1, 2, 3, false] }],
654
+ ['bold', 'italic', 'underline', 'strike'],
655
+ [{ 'list': 'ordered'}, { 'list': 'bullet' }],
656
+ [{ 'color': [] }, { 'background': [] }],
657
+ ['link', 'image', 'code-block'],
658
+ ['clean']
659
+ ]
660
+ },
661
+ readOnly: doc.permission === 'readonly'
662
+ });
663
+
664
+ // 设置初始内容
665
+ quill.root.innerHTML = doc.content || '';
666
+
667
+ // 实时同步
668
+ if (doc.permission === 'editable') {
669
+ let typingTimeout;
670
+ let saveTimeout;
671
+
672
+ quill.on('text-change', () => {
673
+ clearTimeout(typingTimeout);
674
+ clearTimeout(saveTimeout);
675
+ wsService.sendTyping(documentId, user.username, true);
676
+
677
+ typingTimeout = setTimeout(() => {
678
+ wsService.sendTyping(documentId, user.username, false);
679
+ }, 1000);
680
+
681
+ // 自动保存
682
+ saveTimeout = setTimeout(async () => {
683
+ const content = quill.root.innerHTML;
684
+ try {
685
+ await apiService.updateDocument(documentId, content);
686
+ } catch (error) {
687
+ console.error('自动保存失败:', error);
688
+ }
689
+ }, 2000);
690
+ });
691
+
692
+ document.getElementById('saveBtn').addEventListener('click', async () => {
693
+ try {
694
+ const content = quill.root.innerHTML;
695
+ await apiService.updateDocument(documentId, content);
696
+ alert('保存成功!');
697
+ } catch (error) {
698
+ alert('保存失败: ' + error.message);
699
+ }
700
+ });
701
+ }
702
+
703
+ // 监听文档更新
704
+ wsService.on('document_update', (data) => {
705
+ if (data.documentId === documentId && data.userId !== user.id) {
706
+ const selection = quill.getSelection();
707
+ quill.root.innerHTML = data.content;
708
+ if (selection) {
709
+ quill.setSelection(selection);
710
+ }
711
+ }
712
+ });
929
713
 
714
+ // 监听打字状态
715
+ wsService.on('typing', (data) => {
716
+ if (data.documentId === documentId && data.userId !== user.id) {
717
+ const onlineUsers = document.getElementById('onlineUsers');
718
+ if (data.isTyping) {
719
+ onlineUsers.innerHTML += `<span class="user-badge typing" data-user="${data.userId}">✏️ ${data.username}</span>`;
720
+ } else {
721
+ const badge = onlineUsers.querySelector(`[data-user="${data.userId}"]`);
722
+ if (badge) badge.remove();
723
+ }
724
+ }
725
+ });
726
+
727
+ document.getElementById('backBtn').addEventListener('click', () => {
728
+ renderDocumentsView(container);
729
+ });
730
+ }
731
+
732
+ async function renderFilesView(container) {
733
+ if (!currentGroup) {
734
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
735
+ return;
736
+ }
737
+
738
+ try {
739
+ const result = await apiService.getGroupFiles(currentGroup._id);
740
+
930
741
  container.innerHTML = `
931
742
  <div class="view-header">
932
- <h2>知识库 - ${currentGroup.name}</h2>
933
- <input type="text" id="knowledgeSearch" placeholder="搜索知识..." style="padding: 8px; border: 1px solid var(--border); border-radius: 4px;">
743
+ <h2>文件共享 - ${currentGroup.name}</h2>
744
+ <button class="btn-primary" id="uploadFileBtn">📤 上传文件</button>
745
+ </div>
746
+ <div class="files-list" id="filesList"></div>
747
+
748
+ <!-- 文件上传模态框 -->
749
+ <div class="modal hidden" id="uploadFileModal">
750
+ <div class="modal-content">
751
+ <div class="modal-header">
752
+ <h3>上传文件</h3>
753
+ <button class="modal-close" id="closeUploadModal">&times;</button>
754
+ </div>
755
+ <form id="uploadFileForm">
756
+ <div class="form-group">
757
+ <label>选择文件</label>
758
+ <input type="file" id="fileInput" required>
759
+ <small>支持图片、PDF、Word、Excel等,最大10MB</small>
760
+ </div>
761
+ <div class="form-group">
762
+ <label>描述(可选)</label>
763
+ <textarea id="fileDescription" rows="3" placeholder="文件描述..."></textarea>
764
+ </div>
765
+ <div class="form-actions">
766
+ <button type="button" class="btn-secondary" id="cancelUpload">取消</button>
767
+ <button type="submit" class="btn-primary">上传</button>
768
+ </div>
769
+ </form>
770
+ </div>
934
771
  </div>
935
- <div class="knowledge-list" id="knowledgeList"></div>
936
772
  `;
937
773
 
938
- const knowledgeList = document.getElementById('knowledgeList');
939
- if (knowledgeItems.length === 0) {
940
- knowledgeList.innerHTML = '<div class="empty-state">暂无知识条目</div>';
774
+ const filesList = document.getElementById('filesList');
775
+
776
+ if (!result.files || result.files.length === 0) {
777
+ filesList.innerHTML = '<div class="empty-state">暂无文件</div>';
941
778
  } else {
942
- const renderItems = (items) => {
943
- knowledgeList.innerHTML = items.map(item => `
944
- <div class="knowledge-card">
945
- <h3>${item.title}</h3>
946
- <p>${item.content}</p>
947
- <div class="knowledge-meta">
948
- <span>创建者: ${item.creator?.username || '未知'}</span>
949
- <span>创建时间: ${new Date(item.createdAt).toLocaleDateString()}</span>
779
+ result.files.forEach(file => {
780
+ const fileCard = document.createElement('div');
781
+ fileCard.className = 'file-card';
782
+
783
+ const fileIcon = getFileIcon(file.mimetype);
784
+ const fileSize = formatFileSize(file.size);
785
+
786
+ fileCard.innerHTML = `
787
+ <div class="file-icon">${fileIcon}</div>
788
+ <div class="file-info">
789
+ <h4>${file.originalName}</h4>
790
+ <div class="file-meta">
791
+ <span>上传者: ${file.uploader.username}</span>
792
+ <span>大小: ${fileSize}</span>
793
+ <span>时间: ${new Date(file.createdAt).toLocaleString()}</span>
950
794
  </div>
951
- ${item.tags && item.tags.length > 0 ? `
952
- <div class="tags">
953
- ${item.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
954
- </div>
955
- ` : ''}
795
+ ${file.description ? `<p class="file-description">${file.description}</p>` : ''}
956
796
  </div>
957
- `).join('');
958
- };
797
+ <div class="file-actions">
798
+ <a href="${apiService.getFileDownloadUrl(file._id)}" class="btn-primary" download>下载</a>
799
+ ${String(file.uploader._id) === String(currentUserId) ? `<button class="btn-danger" data-id="${file._id}" data-action="delete-file">删除</button>` : ''}
800
+ </div>
801
+ `;
802
+ filesList.appendChild(fileCard);
803
+ });
959
804
 
960
- renderItems(knowledgeItems);
961
-
962
- // 搜索功能
963
- document.getElementById('knowledgeSearch').addEventListener('input', (e) => {
964
- const query = e.target.value.toLowerCase();
965
- const filtered = knowledgeItems.filter(item =>
966
- item.title.toLowerCase().includes(query) ||
967
- item.content.toLowerCase().includes(query) ||
968
- (item.tags && item.tags.some(tag => tag.toLowerCase().includes(query)))
969
- );
970
- renderItems(filtered);
805
+ // 删除文件事件
806
+ document.querySelectorAll('[data-action="delete-file"]').forEach(btn => {
807
+ btn.addEventListener('click', async () => {
808
+ if (confirm('确定要删除这个文件吗?')) {
809
+ try {
810
+ await apiService.deleteFile(btn.dataset.id);
811
+ alert('文件删除成功!');
812
+ await renderFilesView(container);
813
+ } catch (error) {
814
+ alert('删除失败: ' + error.message);
815
+ }
816
+ }
817
+ });
971
818
  });
972
819
  }
820
+
821
+ // 文件上传功能
822
+ document.getElementById('uploadFileBtn').addEventListener('click', () => {
823
+ document.getElementById('uploadFileModal').classList.remove('hidden');
824
+ });
825
+
826
+ document.getElementById('closeUploadModal').addEventListener('click', () => {
827
+ document.getElementById('uploadFileModal').classList.add('hidden');
828
+ document.getElementById('uploadFileForm').reset();
829
+ });
830
+
831
+ document.getElementById('cancelUpload').addEventListener('click', () => {
832
+ document.getElementById('uploadFileModal').classList.add('hidden');
833
+ document.getElementById('uploadFileForm').reset();
834
+ });
835
+
836
+ document.getElementById('uploadFileForm').addEventListener('submit', async (e) => {
837
+ e.preventDefault();
838
+ const fileInput = document.getElementById('fileInput');
839
+ const description = document.getElementById('fileDescription').value;
840
+
841
+ if (!fileInput.files[0]) {
842
+ alert('请选择文件');
843
+ return;
844
+ }
845
+
846
+ try {
847
+ await apiService.uploadFile(currentGroup._id, fileInput.files[0], description);
848
+ alert('文件上传成功!');
849
+ document.getElementById('uploadFileModal').classList.add('hidden');
850
+ document.getElementById('uploadFileForm').reset();
851
+ await renderFilesView(container);
852
+ } catch (error) {
853
+ alert('上传失败: ' + error.message);
854
+ }
855
+ });
973
856
  } catch (error) {
974
- container.innerHTML = `<div class="empty-state">加载失败: ${error.message}</div>`;
857
+ console.error('获取文件列表失败:', error);
858
+ container.innerHTML = `
859
+ <div class="view-header">
860
+ <h2>文件共享</h2>
861
+ </div>
862
+ <div class="empty-state">加载文件失败: ${error.message}</div>
863
+ `;
975
864
  }
976
865
  }
977
866
 
978
- // AI助手(用户版)
979
- async function renderAIView(container) {
867
+ function getFileIcon(mimetype) {
868
+ if (mimetype.startsWith('image/')) return '🖼️';
869
+ if (mimetype === 'application/pdf') return '📕';
870
+ if (mimetype.includes('word') || mimetype.includes('document')) return '📘';
871
+ if (mimetype.includes('excel') || mimetype.includes('spreadsheet')) return '📗';
872
+ if (mimetype.includes('zip') || mimetype.includes('compressed')) return '📦';
873
+ return '📄';
874
+ }
875
+
876
+ function formatFileSize(bytes) {
877
+ if (bytes === 0) return '0 Bytes';
878
+ const k = 1024;
879
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
880
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
881
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
882
+ }
883
+
884
+ async function renderChatView(container) {
885
+ if (!currentGroup) {
886
+ container.innerHTML = `
887
+ <div class="empty-state" style="text-align: center; padding: 60px 20px; background: var(--bg-secondary); border-radius: 16px; border: 2px dashed var(--border);">
888
+ <div style="font-size: 64px; margin-bottom: 20px;">💬</div>
889
+ <h3 style="font-size: 24px; margin-bottom: 12px; color: var(--text-primary);">群聊</h3>
890
+ <p style="color: var(--text-secondary); margin-bottom: 24px; font-size: 16px;">请先在"我的群组"中选择一个群组</p>
891
+ <button class="btn-primary" onclick="document.querySelector('[data-view=\\"groups\\"]').click()" style="padding: 12px 32px; font-size: 16px;">
892
+ 前往我的群组
893
+ </button>
894
+ </div>
895
+ `;
896
+ return;
897
+ }
898
+
899
+ try {
900
+ const groupResult = await apiService.getGroup(currentGroup._id);
901
+ const group = groupResult.group;
902
+ const isMutedAll = Boolean(group.mutedAll);
903
+ const isMutedMe = (group.mutedUsers || []).map(String).includes(String(currentUserId));
904
+ const canSpeak = !isMutedAll && !isMutedMe;
905
+
980
906
  container.innerHTML = `
981
- <div class="view-header">
982
- <h2>🤖 AI助手</h2>
907
+ <div class="view-header" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 12px; margin-bottom: 20px;">
908
+ <h2 style="margin: 0; display: flex; align-items: center; gap: 12px;">
909
+ <span style="font-size: 32px;">💬</span>
910
+ <span>群聊 - ${currentGroup.name}</span>
911
+ </h2>
912
+ ${!canSpeak ? `
913
+ <div style="margin-top: 12px; padding: 12px; background: rgba(255,255,255,0.2); border-radius: 8px; font-size: 14px;">
914
+ ⚠️ ${isMutedAll ? '全体禁言中,无法发言' : '你已被禁言'}
983
915
  </div>
984
- <div class="ai-container">
985
- <div class="ai-chat" id="aiChat">
986
- <div class="ai-message ai">
987
- <p>你好!我是AI助手,有什么可以帮助你的吗?</p>
988
- <p>你可以问我关于文档、任务、群组的问题。</p>
916
+ ` : ''}
917
+ </div>
918
+
919
+ <!-- 标签页切换 -->
920
+ <div class="chat-tabs" style="display: flex; gap: 8px; margin-bottom: 16px; background: var(--bg-secondary); padding: 8px; border-radius: 12px;">
921
+ <button class="chat-tab active" data-tab="chat" style="flex: 1; padding: 12px 20px; background: var(--primary); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; transition: all 0.3s;">
922
+ 💬 聊天
923
+ </button>
924
+ <button class="chat-tab" data-tab="whiteboard" style="flex: 1; padding: 12px 20px; background: transparent; color: var(--text-primary); border: none; border-radius: 8px; cursor: pointer; font-weight: 600; transition: all 0.3s;">
925
+ 🎨 白板
926
+ </button>
927
+ <button class="chat-tab" data-tab="ai" style="flex: 1; padding: 12px 20px; background: transparent; color: var(--text-primary); border: none; border-radius: 8px; cursor: pointer; font-weight: 600; transition: all 0.3s;">
928
+ 🤖 AI助手
929
+ </button>
930
+ </div>
931
+
932
+ <!-- 聊天内容 -->
933
+ <div class="tab-content active" data-content="chat">
934
+ <div class="chat-container" style="display: flex; flex-direction: column; height: calc(100vh - 350px); background: var(--bg-secondary); border-radius: 12px; overflow: hidden;">
935
+ <div class="messages" id="messages" style="flex: 1; overflow-y: auto; padding: 20px;"></div>
936
+ <div class="chat-input" style="display: flex; gap: 10px; padding: 16px; background: var(--bg-tertiary); border-top: 1px solid var(--border);">
937
+ <button class="btn-emoji" id="emojiBtn" ${canSpeak ? '' : 'disabled'} style="padding: 10px 16px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; cursor: ${canSpeak ? 'pointer' : 'not-allowed'}; font-size: 20px;">😊</button>
938
+ <input type="text" id="messageInput" placeholder="${canSpeak ? '输入消息...' : (isMutedAll ? '全体禁言中,无法发言' : '你已被禁言')}" ${canSpeak ? '' : 'disabled'} style="flex: 1; padding: 10px 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-primary);">
939
+ <button class="btn-primary" id="sendBtn" ${canSpeak ? '' : 'disabled'} style="padding: 10px 24px; border-radius: 8px; cursor: ${canSpeak ? 'pointer' : 'not-allowed'};">发送</button>
940
+ </div>
941
+ <emoji-picker id="emojiPicker" class="hidden" style="position: absolute; bottom: 80px; left: 20px; z-index: 1000;"></emoji-picker>
989
942
  </div>
990
943
  </div>
991
- <div class="ai-input">
992
- <textarea id="aiInput" placeholder="向AI助手提问..." rows="3"></textarea>
993
- <button class="btn-primary" id="aiSendBtn">发送</button>
944
+
945
+ <!-- 白板内容 -->
946
+ <div class="tab-content" data-content="whiteboard" style="display: none;">
947
+ <div class="whiteboard-container" style="background: var(--bg-secondary); border-radius: 12px; padding: 16px; height: calc(100vh - 350px);">
948
+ <div class="whiteboard-toolbar" style="display: flex; gap: 10px; margin-bottom: 16px; padding: 12px; background: var(--bg-tertiary); border-radius: 8px;">
949
+ <button class="tool-btn active" data-tool="pen" style="padding: 8px 16px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer;">✏️ 画笔</button>
950
+ <button class="tool-btn" data-tool="eraser" style="padding: 8px 16px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 6px; cursor: pointer;">🧹 橡皮</button>
951
+ <input type="color" id="colorPicker" value="#667eea" style="width: 50px; height: 36px; border: none; border-radius: 6px; cursor: pointer;">
952
+ <input type="range" id="brushSize" min="1" max="20" value="3" style="width: 120px;">
953
+ <button class="btn-secondary" id="clearCanvas" style="padding: 8px 16px; border-radius: 6px;">🗑️ 清空</button>
954
+ <button class="btn-primary" id="sendWhiteboardBtn" style="padding: 8px 16px; border-radius: 6px; background: var(--success); border: none; color: white; cursor: pointer;">📤 发送到群聊</button>
955
+ </div>
956
+ <canvas id="whiteboard" style="width: 100%; height: calc(100% - 80px); background: white; border-radius: 8px; cursor: crosshair;"></canvas>
957
+ </div>
994
958
  </div>
959
+
960
+ <!-- AI助手内容 -->
961
+ <div class="tab-content" data-content="ai" style="display: none;">
962
+ <div class="ai-container" style="display: flex; flex-direction: column; height: calc(100vh - 350px); background: var(--bg-secondary); border-radius: 12px; overflow: hidden;">
963
+ <div class="ai-chat" id="aiChat" style="flex: 1; overflow-y: auto; padding: 20px;">
964
+ <div class="ai-message ai" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 16px; border-radius: 12px; margin-bottom: 16px;">
965
+ <p style="margin: 0 0 8px 0; font-weight: 600;">🤖 AI助手</p>
966
+ <p style="margin: 0;">你好!我是AI助手,有什么可以帮助你的吗?</p>
967
+ <p style="margin: 8px 0 0 0; font-size: 14px; opacity: 0.9;">你可以问我关于文档、任务、群组的问题。</p>
968
+ </div>
969
+ </div>
970
+ <div class="ai-input" style="display: flex; gap: 10px; padding: 16px; background: var(--bg-tertiary); border-top: 1px solid var(--border);">
971
+ <textarea id="aiInput" placeholder="向AI助手提问..." rows="2" style="flex: 1; padding: 10px 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-primary); resize: none;"></textarea>
972
+ <button class="btn-primary" id="aiSendBtn" style="padding: 10px 24px; border-radius: 8px; align-self: flex-end;">发送</button>
973
+ </div>
974
+ </div>
995
975
  </div>
996
976
  `;
997
977
 
998
- const aiChat = document.getElementById('aiChat');
999
- const aiInput = document.getElementById('aiInput');
1000
- const aiSendBtn = document.getElementById('aiSendBtn');
978
+ const messagesDiv = document.getElementById('messages');
979
+ const messageInput = document.getElementById('messageInput');
980
+ const sendBtn = document.getElementById('sendBtn');
981
+
982
+ // 加载历史消息
983
+ try {
984
+ const messagesResult = await apiService.getGroupMessages(currentGroup._id);
985
+ if (messagesResult.messages && Array.isArray(messagesResult.messages)) {
986
+ if (messagesResult.messages.length === 0) {
987
+ messagesDiv.innerHTML = `
988
+ <div style="text-align: center; padding: 40px; color: var(--text-tertiary);">
989
+ <div style="font-size: 48px; margin-bottom: 16px;">💬</div>
990
+ <p>还没有消息,开始聊天吧!</p>
991
+ </div>
992
+ `;
993
+ } else {
994
+ messagesResult.messages.forEach(msg => {
995
+ const messageEl = document.createElement('div');
996
+ const isOwn = String(msg.sender) === String(currentUserId) || msg.username === user.username;
997
+
998
+ // 格式化消息内容(处理白板、投票等)
999
+ const formattedContent = formatMessageContent(msg.content);
1000
+ const isSpecialMessage = msg.content.startsWith('[白板作品]') || msg.content.startsWith('[投票]');
1001
+ const isWhiteboard = msg.content.startsWith('[白板作品]');
1002
+
1003
+ messageEl.className = `message ${isOwn ? 'own' : ''}`;
1004
+ messageEl.style.cssText = `
1005
+ margin-bottom: 16px;
1006
+ display: flex;
1007
+ flex-direction: column;
1008
+ align-items: ${isOwn ? 'flex-end' : 'flex-start'};
1009
+ `;
1010
+
1011
+ messageEl.innerHTML = `
1012
+ <div class="message-header" style="display: flex; gap: 8px; margin-bottom: 4px; font-size: 12px; color: var(--text-tertiary);">
1013
+ <span class="message-user">${msg.username}</span>
1014
+ <span class="message-time">${new Date(msg.timestamp).toLocaleTimeString('zh-CN')}</span>
1015
+ </div>
1016
+ <div class="message-content" style="background: ${isWhiteboard ? 'transparent' : (isSpecialMessage ? 'transparent' : (isOwn ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'var(--bg-tertiary)'))}; color: ${isOwn && !isSpecialMessage ? 'white' : 'var(--text-primary)'}; padding: ${isSpecialMessage ? '0' : '12px 16px'}; border-radius: 16px; max-width: ${isSpecialMessage ? '90%' : '70%'}; word-wrap: break-word; box-shadow: ${isOwn && !isSpecialMessage ? '0 4px 12px rgba(102, 126, 234, 0.3)' : '0 2px 8px rgba(0,0,0,0.05)'};">${formattedContent}</div>
1017
+ `;
1018
+ messagesDiv.appendChild(messageEl);
1019
+ });
1020
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1021
+ }
1022
+ }
1023
+ } catch (err) {
1024
+ console.error('加载历史消息失败:', err);
1025
+ messagesDiv.innerHTML = `
1026
+ <div style="text-align: center; padding: 40px; color: var(--danger);">
1027
+ <div style="font-size: 48px; margin-bottom: 16px;">⚠️</div>
1028
+ <p>加载历史消息失败</p>
1029
+ <p style="font-size: 14px; color: var(--text-tertiary);">${err.message}</p>
1030
+ </div>
1031
+ `;
1032
+ }
1033
+
1034
+ // 表情包功能
1035
+ const emojiBtn = document.getElementById('emojiBtn');
1036
+ const emojiPicker = document.getElementById('emojiPicker');
1037
+
1038
+ if (canSpeak) {
1039
+ emojiBtn.addEventListener('click', () => {
1040
+ emojiPicker.classList.toggle('hidden');
1041
+ });
1001
1042
 
1002
- const sendMessage = async () => {
1003
- const question = aiInput.value.trim();
1004
- if (!question) return;
1043
+ emojiPicker.addEventListener('emoji-click', (event) => {
1044
+ messageInput.value += event.detail.unicode;
1045
+ messageInput.focus();
1046
+ emojiPicker.classList.add('hidden');
1047
+ });
1005
1048
 
1006
- // 显示用户消息
1007
- const userMsg = document.createElement('div');
1008
- userMsg.className = 'ai-message user';
1009
- userMsg.textContent = question;
1010
- aiChat.appendChild(userMsg);
1011
- aiInput.value = '';
1049
+ // 点击外部关闭表情选择器
1050
+ document.addEventListener('click', (e) => {
1051
+ if (!emojiBtn.contains(e.target) && !emojiPicker.contains(e.target)) {
1052
+ emojiPicker.classList.add('hidden');
1053
+ }
1054
+ });
1055
+ }
1012
1056
 
1013
- // 显示加载中
1014
- const loadingMsg = document.createElement('div');
1015
- loadingMsg.className = 'ai-message ai loading';
1016
- loadingMsg.textContent = '思考中...';
1017
- aiChat.appendChild(loadingMsg);
1018
- aiChat.scrollTop = aiChat.scrollHeight;
1057
+ // 消息通知系统
1058
+ function showNotification(title, body, icon = '💬') {
1059
+ if ('Notification' in window && Notification.permission === 'granted') {
1060
+ new Notification(title, {
1061
+ body: body,
1062
+ icon: '/icon.png',
1063
+ badge: '/icon.png',
1064
+ tag: 'chat-message'
1065
+ });
1066
+ }
1067
+ }
1019
1068
 
1020
- try {
1021
- const token = localStorage.getItem('token');
1022
- const response = await fetch('http://localhost:8765/api/ai/ask', {
1023
- method: 'POST',
1024
- headers: {
1025
- 'Content-Type': 'application/json',
1026
- 'Authorization': `Bearer ${token}`
1027
- },
1028
- body: JSON.stringify({ question, groupId: currentGroup?._id })
1069
+ // 请求通知权限
1070
+ if ('Notification' in window && Notification.permission === 'default') {
1071
+ Notification.requestPermission();
1072
+ }
1073
+
1074
+ // 监听消息
1075
+ wsService.on('chat_message', (data) => {
1076
+ if (data.groupId === currentGroup._id) {
1077
+ const messageEl = document.createElement('div');
1078
+ const isOwn = String(data.userId) === String(currentUserId) || data.username === user.username;
1079
+ messageEl.className = `message ${isOwn ? 'own' : ''}`;
1080
+ messageEl.style.cssText = `
1081
+ margin-bottom: 16px;
1082
+ display: flex;
1083
+ flex-direction: column;
1084
+ align-items: ${isOwn ? 'flex-end' : 'flex-start'};
1085
+ `;
1086
+
1087
+ // 格式化消息内容(处理白板、投票等)
1088
+ const formattedContent = formatMessageContent(data.content);
1089
+ const isSpecialMessage = data.content.startsWith('[白板作品]') || data.content.startsWith('[投票]');
1090
+ const isWhiteboard = data.content.startsWith('[白板作品]');
1091
+
1092
+ messageEl.innerHTML = `
1093
+ <div class="message-header" style="display: flex; gap: 8px; margin-bottom: 4px; font-size: 12px; color: var(--text-tertiary);">
1094
+ <span class="message-user">${data.username}</span>
1095
+ <span class="message-time">${new Date(data.timestamp).toLocaleTimeString('zh-CN')}</span>
1096
+ </div>
1097
+ <div class="message-content" style="background: ${isWhiteboard ? 'transparent' : (isSpecialMessage ? 'transparent' : (isOwn ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'var(--bg-tertiary)'))}; color: ${isOwn && !isSpecialMessage ? 'white' : 'var(--text-primary)'}; padding: ${isSpecialMessage ? '0' : '12px 16px'}; border-radius: 16px; max-width: ${isSpecialMessage ? '90%' : '70%'}; word-wrap: break-word; box-shadow: ${isOwn && !isSpecialMessage ? '0 4px 12px rgba(102, 126, 234, 0.3)' : '0 2px 8px rgba(0,0,0,0.05)'};">${formattedContent}</div>
1098
+ `;
1099
+ messagesDiv.appendChild(messageEl);
1100
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1101
+
1102
+ // 显示通知(如果不是自己发送的消息)
1103
+ if (!isOwn) {
1104
+ showNotification(`${data.username} 在 ${currentGroup.name}`, data.content.startsWith('[') ? '发送了特殊消息' : data.content);
1105
+ }
1106
+ }
1107
+ });
1108
+
1109
+ // 服务端拦截提示(比如被禁言/未加入群组)
1110
+ wsService.on('chat_blocked', (data) => {
1111
+ if (data.groupId === currentGroup._id) {
1112
+ const notificationEl = document.createElement('div');
1113
+ notificationEl.style.cssText = `
1114
+ text-align: center;
1115
+ padding: 12px;
1116
+ margin: 16px auto;
1117
+ background: var(--danger);
1118
+ color: white;
1119
+ border-radius: 8px;
1120
+ max-width: 80%;
1121
+ `;
1122
+ notificationEl.textContent = data.message || '消息发送失败';
1123
+ messagesDiv.appendChild(notificationEl);
1124
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1125
+ }
1126
+ });
1127
+
1128
+ // 监听点名通知
1129
+ wsService.on('call_response', (data) => {
1130
+ if (data.groupId === currentGroup._id) {
1131
+ const notificationEl = document.createElement('div');
1132
+ notificationEl.style.cssText = `
1133
+ text-align: center;
1134
+ padding: 12px;
1135
+ margin: 16px auto;
1136
+ background: var(--success);
1137
+ color: white;
1138
+ border-radius: 8px;
1139
+ max-width: 80%;
1140
+ `;
1141
+ notificationEl.textContent = `${data.username} 已响应点名`;
1142
+ messagesDiv.appendChild(notificationEl);
1143
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1144
+ }
1145
+ });
1146
+
1147
+ // 发送消息
1148
+ const sendMessage = () => {
1149
+ if (!canSpeak) {
1150
+ alert(isMutedAll ? '全体禁言中,无法发言' : '你已被禁言');
1151
+ return;
1152
+ }
1153
+
1154
+ const content = messageInput.value.trim();
1155
+ if (content) {
1156
+ try {
1157
+ wsService.sendChatMessage(currentGroup._id, user.username, content);
1158
+ messageInput.value = '';
1159
+ } catch (error) {
1160
+ console.error('发送消息失败:', error);
1161
+ alert('发送失败: ' + error.message);
1162
+ }
1163
+ }
1164
+ };
1165
+
1166
+ if (canSpeak) {
1167
+ sendBtn.addEventListener('click', sendMessage);
1168
+ messageInput.addEventListener('keypress', (e) => {
1169
+ if (e.key === 'Enter' && !e.shiftKey) {
1170
+ e.preventDefault();
1171
+ sendMessage();
1172
+ }
1173
+ });
1174
+ }
1175
+
1176
+ // 标签页切换功能
1177
+ const tabs = document.querySelectorAll('.chat-tab');
1178
+ const tabContents = document.querySelectorAll('.tab-content');
1179
+
1180
+ tabs.forEach(tab => {
1181
+ tab.addEventListener('click', () => {
1182
+ const targetTab = tab.dataset.tab;
1183
+
1184
+ // 更新标签样式
1185
+ tabs.forEach(t => {
1186
+ if (t.dataset.tab === targetTab) {
1187
+ t.style.background = 'var(--primary)';
1188
+ t.style.color = 'white';
1189
+ } else {
1190
+ t.style.background = 'transparent';
1191
+ t.style.color = 'var(--text-primary)';
1192
+ }
1193
+ });
1194
+
1195
+ // 切换内容
1196
+ tabContents.forEach(content => {
1197
+ if (content.dataset.content === targetTab) {
1198
+ content.style.display = 'block';
1199
+ } else {
1200
+ content.style.display = 'none';
1201
+ }
1202
+ });
1203
+
1204
+ // 初始化白板
1205
+ if (targetTab === 'whiteboard') {
1206
+ initWhiteboard();
1207
+ }
1208
+ });
1209
+ });
1210
+
1211
+ // 白板功能
1212
+ function initWhiteboard() {
1213
+ const canvas = document.getElementById('whiteboard');
1214
+ if (!canvas || canvas.dataset.initialized) return;
1215
+
1216
+ canvas.dataset.initialized = 'true';
1217
+ const ctx = canvas.getContext('2d');
1218
+
1219
+ // 设置画布大小
1220
+ canvas.width = canvas.offsetWidth;
1221
+ canvas.height = canvas.offsetHeight;
1222
+
1223
+ let isDrawing = false;
1224
+ let currentTool = 'pen';
1225
+ let currentColor = '#667eea';
1226
+ let brushSize = 3;
1227
+
1228
+ // 工具按钮
1229
+ document.querySelectorAll('.tool-btn').forEach(btn => {
1230
+ btn.addEventListener('click', () => {
1231
+ currentTool = btn.dataset.tool;
1232
+ document.querySelectorAll('.tool-btn').forEach(b => {
1233
+ b.style.background = 'var(--bg-secondary)';
1234
+ b.style.color = 'var(--text-primary)';
1235
+ b.style.border = '1px solid var(--border)';
1236
+ });
1237
+ btn.style.background = 'var(--primary)';
1238
+ btn.style.color = 'white';
1239
+ btn.style.border = 'none';
1240
+ });
1241
+ });
1242
+
1243
+ // 颜色选择器
1244
+ document.getElementById('colorPicker').addEventListener('change', (e) => {
1245
+ currentColor = e.target.value;
1246
+ });
1247
+
1248
+ // 画笔大小
1249
+ document.getElementById('brushSize').addEventListener('input', (e) => {
1250
+ brushSize = e.target.value;
1251
+ });
1252
+
1253
+ // 清空画布
1254
+ document.getElementById('clearCanvas').addEventListener('click', () => {
1255
+ if (confirm('确定要清空画布吗?')) {
1256
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1257
+ wsService.sendWhiteboardClear(currentGroup._id);
1258
+ }
1259
+ });
1260
+
1261
+ // 发送到群聊
1262
+ document.getElementById('sendWhiteboardBtn').addEventListener('click', async () => {
1263
+ try {
1264
+ // 获取Base64图片数据
1265
+ const dataURL = canvas.toDataURL('image/png');
1266
+
1267
+ // 检查画布是否为空
1268
+ const emptyCanvas = document.createElement('canvas');
1269
+ emptyCanvas.width = canvas.width;
1270
+ emptyCanvas.height = canvas.height;
1271
+ const emptyDataURL = emptyCanvas.toDataURL('image/png');
1272
+
1273
+ if (dataURL === emptyDataURL) {
1274
+ alert('画布是空的,请先绘制内容!');
1275
+ return;
1276
+ }
1277
+
1278
+ // 方案1: 直接发送Base64(适合小图片)
1279
+ // 如果图片大小不超过1MB,直接发送Base64
1280
+ const base64Size = dataURL.length * 0.75 / 1024 / 1024; // 转换为MB
1281
+
1282
+ if (base64Size < 1) {
1283
+ // 直接发送Base64
1284
+ wsService.sendChatMessage(currentGroup._id, user.username, `[白板作品]${dataURL}`);
1285
+ alert('白板作品已发送到群聊!');
1286
+ } else {
1287
+ // 方案2: 上传到服务器(适合大图片)
1288
+ const blob = await fetch(dataURL).then(r => r.blob());
1289
+
1290
+ // 创建 FormData 上传图片
1291
+ const formData = new FormData();
1292
+ formData.append('file', blob, `whiteboard-${Date.now()}.png`);
1293
+ formData.append('groupId', currentGroup._id);
1294
+ formData.append('description', '协作白板作品');
1295
+
1296
+ const token = localStorage.getItem('token');
1297
+ const response = await fetch('http://localhost:8765/api/files/upload', {
1298
+ method: 'POST',
1299
+ headers: {
1300
+ 'Authorization': `Bearer ${token}`
1301
+ },
1302
+ body: formData
1303
+ });
1304
+
1305
+ if (response.ok) {
1306
+ const result = await response.json();
1307
+ const fileId = result.file._id;
1308
+ const fileUrl = `http://localhost:8765/api/files/${fileId}/download?token=${token}`;
1309
+
1310
+ // 发送包含图片链接的消息到群聊
1311
+ wsService.sendChatMessage(currentGroup._id, user.username, `[白板作品]${fileUrl}`);
1312
+ alert('白板作品已发送到群聊!');
1313
+ } else {
1314
+ throw new Error('上传失败');
1315
+ }
1316
+ }
1317
+ } catch (error) {
1318
+ console.error('发送白板失败:', error);
1319
+ alert('发送失败,请重试!');
1320
+ }
1321
+ });
1322
+
1323
+ // 绘图事件
1324
+ canvas.addEventListener('mousedown', (e) => {
1325
+ isDrawing = true;
1326
+ const rect = canvas.getBoundingClientRect();
1327
+ const x = e.clientX - rect.left;
1328
+ const y = e.clientY - rect.top;
1329
+
1330
+ ctx.beginPath();
1331
+ ctx.moveTo(x, y);
1332
+ });
1333
+
1334
+ canvas.addEventListener('mousemove', (e) => {
1335
+ if (!isDrawing) return;
1336
+
1337
+ const rect = canvas.getBoundingClientRect();
1338
+ const x = e.clientX - rect.left;
1339
+ const y = e.clientY - rect.top;
1340
+
1341
+ ctx.lineWidth = brushSize;
1342
+ ctx.lineCap = 'round';
1343
+
1344
+ if (currentTool === 'pen') {
1345
+ ctx.strokeStyle = currentColor;
1346
+ ctx.globalCompositeOperation = 'source-over';
1347
+ } else if (currentTool === 'eraser') {
1348
+ ctx.globalCompositeOperation = 'destination-out';
1349
+ }
1350
+
1351
+ ctx.lineTo(x, y);
1352
+ ctx.stroke();
1353
+
1354
+ // 发送绘图数据到其他用户
1355
+ wsService.sendWhiteboardDraw(currentGroup._id, {
1356
+ tool: currentTool,
1357
+ color: currentColor,
1358
+ size: brushSize,
1359
+ x: x,
1360
+ y: y
1361
+ });
1362
+ });
1363
+
1364
+ canvas.addEventListener('mouseup', () => {
1365
+ isDrawing = false;
1366
+ });
1367
+
1368
+ canvas.addEventListener('mouseleave', () => {
1369
+ isDrawing = false;
1029
1370
  });
1030
- const result = await response.json();
1371
+
1372
+ // 监听其他用户的绘图
1373
+ wsService.on('whiteboard_draw', (data) => {
1374
+ if (data.groupId === currentGroup._id) {
1375
+ ctx.lineWidth = data.size;
1376
+ ctx.lineCap = 'round';
1377
+
1378
+ if (data.tool === 'pen') {
1379
+ ctx.strokeStyle = data.color;
1380
+ ctx.globalCompositeOperation = 'source-over';
1381
+ } else if (data.tool === 'eraser') {
1382
+ ctx.globalCompositeOperation = 'destination-out';
1383
+ }
1384
+
1385
+ ctx.lineTo(data.x, data.y);
1386
+ ctx.stroke();
1387
+ }
1388
+ });
1389
+
1390
+ // 监听清空画布
1391
+ wsService.on('whiteboard_clear', (data) => {
1392
+ if (data.groupId === currentGroup._id) {
1393
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1394
+ }
1395
+ });
1396
+ }
1397
+
1398
+ // AI助手功能
1399
+ const aiChat = document.getElementById('aiChat');
1400
+ const aiInput = document.getElementById('aiInput');
1401
+ const aiSendBtn = document.getElementById('aiSendBtn');
1031
1402
 
1032
- loadingMsg.remove();
1033
- const aiMsg = document.createElement('div');
1034
- aiMsg.className = 'ai-message ai';
1035
- aiMsg.textContent = result.answer || '抱歉,我无法回答这个问题。';
1036
- aiChat.appendChild(aiMsg);
1403
+ const sendAIMessage = async () => {
1404
+ const question = aiInput.value.trim();
1405
+ if (!question) return;
1406
+
1407
+ // 显示用户消息
1408
+ const userMsg = document.createElement('div');
1409
+ userMsg.style.cssText = `
1410
+ background: var(--primary);
1411
+ color: white;
1412
+ padding: 12px 16px;
1413
+ border-radius: 12px;
1414
+ margin-bottom: 16px;
1415
+ max-width: 80%;
1416
+ margin-left: auto;
1417
+ word-wrap: break-word;
1418
+ `;
1419
+ userMsg.textContent = question;
1420
+ aiChat.appendChild(userMsg);
1421
+ aiInput.value = '';
1422
+
1423
+ // 显示加载中
1424
+ const loadingMsg = document.createElement('div');
1425
+ loadingMsg.style.cssText = `
1426
+ background: var(--bg-tertiary);
1427
+ color: var(--text-secondary);
1428
+ padding: 12px 16px;
1429
+ border-radius: 12px;
1430
+ margin-bottom: 16px;
1431
+ max-width: 80%;
1432
+ font-style: italic;
1433
+ `;
1434
+ loadingMsg.textContent = '🤔 思考中...';
1435
+ aiChat.appendChild(loadingMsg);
1037
1436
  aiChat.scrollTop = aiChat.scrollHeight;
1437
+
1438
+ try {
1439
+ const token = localStorage.getItem('token');
1440
+ const response = await fetch('http://localhost:8765/api/ai/ask', {
1441
+ method: 'POST',
1442
+ headers: {
1443
+ 'Content-Type': 'application/json',
1444
+ 'Authorization': `Bearer ${token}`
1445
+ },
1446
+ body: JSON.stringify({ question, groupId: currentGroup?._id })
1447
+ });
1448
+ const result = await response.json();
1449
+
1450
+ loadingMsg.remove();
1451
+ const aiMsg = document.createElement('div');
1452
+ aiMsg.style.cssText = `
1453
+ background: var(--bg-tertiary);
1454
+ color: var(--text-primary);
1455
+ padding: 12px 16px;
1456
+ border-radius: 12px;
1457
+ margin-bottom: 16px;
1458
+ max-width: 80%;
1459
+ line-height: 1.6;
1460
+ word-wrap: break-word;
1461
+ `;
1462
+ aiMsg.textContent = result.answer || '抱歉,我无法回答这个问题。';
1463
+ aiChat.appendChild(aiMsg);
1464
+ aiChat.scrollTop = aiChat.scrollHeight;
1465
+ } catch (error) {
1466
+ loadingMsg.remove();
1467
+ const errorMsg = document.createElement('div');
1468
+ errorMsg.style.cssText = `
1469
+ background: var(--danger);
1470
+ color: white;
1471
+ padding: 12px 16px;
1472
+ border-radius: 12px;
1473
+ margin-bottom: 16px;
1474
+ max-width: 80%;
1475
+ `;
1476
+ errorMsg.textContent = '抱歉,发生了错误: ' + error.message;
1477
+ aiChat.appendChild(errorMsg);
1478
+ aiChat.scrollTop = aiChat.scrollHeight;
1479
+ }
1480
+ };
1481
+
1482
+ aiSendBtn.addEventListener('click', sendAIMessage);
1483
+ aiInput.addEventListener('keypress', (e) => {
1484
+ if (e.key === 'Enter' && !e.shiftKey) {
1485
+ e.preventDefault();
1486
+ sendAIMessage();
1487
+ }
1488
+ });
1489
+
1490
+ } catch (error) {
1491
+ console.error('加载群聊失败:', error);
1492
+ container.innerHTML = `
1493
+ <div class="empty-state" style="text-align: center; padding: 60px 20px;">
1494
+ <div style="font-size: 64px; margin-bottom: 20px;">⚠️</div>
1495
+ <h3 style="font-size: 24px; margin-bottom: 12px; color: var(--danger);">加载失败</h3>
1496
+ <p style="color: var(--text-secondary); margin-bottom: 24px;">${error.message}</p>
1497
+ <button class="btn-primary" onclick="location.reload()">重新加载</button>
1498
+ </div>
1499
+ `;
1500
+ }
1501
+ }
1502
+
1503
+ async function renderSearchView(container) {
1504
+ container.innerHTML = `
1505
+ <div class="view-header">
1506
+ <h2>🔍 搜索</h2>
1507
+ </div>
1508
+ <div class="search-container">
1509
+ <div class="search-box">
1510
+ <input type="text" id="searchInput" placeholder="搜索消息、文档、任务...">
1511
+ <button class="btn-primary" id="searchBtn">搜索</button>
1512
+ </div>
1513
+ <div class="search-filters">
1514
+ <label>
1515
+ <input type="checkbox" id="filterMessages" checked> 消息
1516
+ </label>
1517
+ <label>
1518
+ <input type="checkbox" id="filterDocuments" checked> 文档
1519
+ </label>
1520
+ <label>
1521
+ <input type="checkbox" id="filterTasks" checked> 任务
1522
+ </label>
1523
+ </div>
1524
+ <div class="search-results" id="searchResults"></div>
1525
+ </div>
1526
+ `;
1527
+
1528
+ const searchInput = document.getElementById('searchInput');
1529
+ const searchBtn = document.getElementById('searchBtn');
1530
+ const searchResults = document.getElementById('searchResults');
1531
+
1532
+ const performSearch = async () => {
1533
+ const query = searchInput.value.trim();
1534
+ if (!query) {
1535
+ searchResults.innerHTML = '<div class="empty-state">请输入搜索关键词</div>';
1536
+ return;
1537
+ }
1538
+
1539
+ const filters = {
1540
+ messages: document.getElementById('filterMessages').checked,
1541
+ documents: document.getElementById('filterDocuments').checked,
1542
+ tasks: document.getElementById('filterTasks').checked
1543
+ };
1544
+
1545
+ searchResults.innerHTML = '<div class="loading">搜索中...</div>';
1546
+
1547
+ try {
1548
+ const results = [];
1549
+
1550
+ // 搜索消息
1551
+ if (filters.messages && currentGroup) {
1552
+ try {
1553
+ const messagesResult = await apiService.getGroupMessages(currentGroup._id);
1554
+ if (messagesResult.messages) {
1555
+ const matchedMessages = messagesResult.messages.filter(msg =>
1556
+ msg.content.toLowerCase().includes(query.toLowerCase())
1557
+ );
1558
+ matchedMessages.forEach(msg => {
1559
+ results.push({
1560
+ type: 'message',
1561
+ title: `消息 - ${msg.username}`,
1562
+ content: msg.content,
1563
+ time: msg.timestamp,
1564
+ group: currentGroup.name
1565
+ });
1566
+ });
1567
+ }
1568
+ } catch (err) {
1569
+ console.error('搜索消息失败:', err);
1570
+ }
1571
+ }
1572
+
1573
+ // 搜索文档
1574
+ if (filters.documents) {
1575
+ try {
1576
+ if (currentGroup) {
1577
+ const docsResult = await apiService.getDocuments(currentGroup._id);
1578
+ if (docsResult.documents) {
1579
+ const matchedDocs = docsResult.documents.filter(doc =>
1580
+ doc.title.toLowerCase().includes(query.toLowerCase()) ||
1581
+ doc.content.toLowerCase().includes(query.toLowerCase())
1582
+ );
1583
+ matchedDocs.forEach(doc => {
1584
+ results.push({
1585
+ type: 'document',
1586
+ title: doc.title,
1587
+ content: doc.content.substring(0, 200),
1588
+ time: doc.updatedAt,
1589
+ id: doc._id,
1590
+ group: currentGroup.name
1591
+ });
1592
+ });
1593
+ }
1594
+ }
1595
+ } catch (err) {
1596
+ console.error('搜索文档失败:', err);
1597
+ }
1598
+ }
1599
+
1600
+ // 搜索任务
1601
+ if (filters.tasks) {
1602
+ try {
1603
+ const tasksResult = await apiService.getMyTasks();
1604
+ if (tasksResult.tasks) {
1605
+ const matchedTasks = tasksResult.tasks.filter(task =>
1606
+ task.title.toLowerCase().includes(query.toLowerCase()) ||
1607
+ (task.description && task.description.toLowerCase().includes(query.toLowerCase()))
1608
+ );
1609
+ matchedTasks.forEach(task => {
1610
+ results.push({
1611
+ type: 'task',
1612
+ title: task.title,
1613
+ content: task.description || '',
1614
+ time: task.updatedAt,
1615
+ id: task._id,
1616
+ status: task.status
1617
+ });
1618
+ });
1619
+ }
1620
+ } catch (err) {
1621
+ console.error('搜索任务失败:', err);
1622
+ }
1623
+ }
1624
+
1625
+ // 显示结果
1626
+ if (results.length === 0) {
1627
+ searchResults.innerHTML = '<div class="empty-state">未找到相关结果</div>';
1628
+ } else {
1629
+ searchResults.innerHTML = results.map(result => {
1630
+ const typeIcon = {
1631
+ message: '💬',
1632
+ document: '📄',
1633
+ task: '📋'
1634
+ };
1635
+ return `
1636
+ <div class="search-result-item">
1637
+ <div class="result-header">
1638
+ <span class="result-type">${typeIcon[result.type]} ${result.type === 'message' ? '消息' : result.type === 'document' ? '文档' : '任务'}</span>
1639
+ <span class="result-time">${new Date(result.time).toLocaleString()}</span>
1640
+ </div>
1641
+ <h4>${highlightText(result.title, query)}</h4>
1642
+ <p>${highlightText(result.content, query)}</p>
1643
+ ${result.group ? `<span class="result-group">群组: ${result.group}</span>` : ''}
1644
+ ${result.status ? `<span class="result-status">状态: ${getStatusText(result.status)}</span>` : ''}
1645
+ </div>
1646
+ `;
1647
+ }).join('');
1648
+ }
1038
1649
  } catch (error) {
1039
- loadingMsg.remove();
1040
- const errorMsg = document.createElement('div');
1041
- errorMsg.className = 'ai-message ai error';
1042
- errorMsg.textContent = '抱歉,发生了错误: ' + error.message;
1043
- aiChat.appendChild(errorMsg);
1650
+ searchResults.innerHTML = `<div class="empty-state">搜索失败: ${error.message}</div>`;
1044
1651
  }
1045
1652
  };
1046
1653
 
1047
- aiSendBtn.addEventListener('click', sendMessage);
1048
- aiInput.addEventListener('keypress', (e) => {
1049
- if (e.key === 'Enter' && !e.shiftKey) {
1050
- e.preventDefault();
1051
- sendMessage();
1052
- }
1654
+ searchBtn.addEventListener('click', performSearch);
1655
+ searchInput.addEventListener('keypress', (e) => {
1656
+ if (e.key === 'Enter') performSearch();
1053
1657
  });
1054
1658
  }
1055
1659
 
1056
- renderView('groups');
1057
- }
1058
-
1660
+ function highlightText(text, query) {
1661
+ if (!query) return text;
1662
+ const regex = new RegExp(`(${query})`, 'gi');
1663
+ return text.replace(regex, '<mark>$1</mark>');
1664
+ }
1665
+
1666
+ function getStatusText(status) {
1667
+ const statusMap = {
1668
+ 'pending': '待处理',
1669
+ 'in_progress': '进行中',
1670
+ 'completed': '已完成',
1671
+ 'terminated': '已终止'
1672
+ };
1673
+ return statusMap[status] || status;
1674
+ }
1675
+
1676
+
1677
+ // 知识库(用户版 - 完整功能)
1678
+ async function renderKnowledgeView(container) {
1679
+ if (!currentGroup) {
1680
+ container.innerHTML = `
1681
+ <div class="empty-state" style="text-align: center; padding: 60px 20px; background: var(--bg-secondary); border-radius: 16px; border: 2px dashed var(--border);">
1682
+ <div style="font-size: 64px; margin-bottom: 20px;">📚</div>
1683
+ <h3 style="font-size: 24px; margin-bottom: 12px; color: var(--text-primary);">知识库</h3>
1684
+ <p style="color: var(--text-secondary); margin-bottom: 24px; font-size: 16px;">请先在"我的群组"中选择一个群组</p>
1685
+ <button class="btn-primary" onclick="document.querySelector('[data-view=\\"groups\\"]').click()" style="padding: 12px 32px; font-size: 16px;">
1686
+ 前往我的群组
1687
+ </button>
1688
+ </div>
1689
+ `;
1690
+ return;
1691
+ }
1692
+
1693
+ try {
1694
+ const token = localStorage.getItem('token');
1695
+ const response = await fetch(`http://localhost:8765/api/knowledge/group/${currentGroup._id}`, {
1696
+ headers: { 'Authorization': `Bearer ${token}` }
1697
+ });
1698
+
1699
+ if (!response.ok) {
1700
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1701
+ }
1702
+
1703
+ const result = await response.json();
1704
+
1705
+ // 确保 knowledgeItems 是数组
1706
+ let knowledgeItems = [];
1707
+ if (Array.isArray(result)) {
1708
+ knowledgeItems = result;
1709
+ } else if (result.data && Array.isArray(result.data)) {
1710
+ knowledgeItems = result.data;
1711
+ } else if (result.data && result.data.knowledgeList && Array.isArray(result.data.knowledgeList)) {
1712
+ knowledgeItems = result.data.knowledgeList;
1713
+ } else if (result.items && Array.isArray(result.items)) {
1714
+ knowledgeItems = result.items;
1715
+ } else if (result.knowledge && Array.isArray(result.knowledge)) {
1716
+ knowledgeItems = result.knowledge;
1717
+ }
1718
+
1719
+ container.innerHTML = `
1720
+ <div class="view-header">
1721
+ <h2>📚 知识库 - ${currentGroup.name}</h2>
1722
+ <button class="btn-primary" id="createKnowledgeBtn">📝 创建知识条目</button>
1723
+ </div>
1724
+ <div class="knowledge-grid" id="knowledgeList" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; padding: 20px;"></div>
1725
+
1726
+ <!-- 知识库模态框 -->
1727
+ <div id="knowledgeModal" class="modal hidden">
1728
+ <div class="modal-content">
1729
+ <div class="modal-header">
1730
+ <h3 id="modalTitle">创建知识条目</h3>
1731
+ <button class="modal-close" id="closeKnowledgeModal">&times;</button>
1732
+ </div>
1733
+ <form id="knowledgeForm">
1734
+ <div class="form-group">
1735
+ <label>📌 标题</label>
1736
+ <input type="text" name="title" required style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;">
1737
+ </div>
1738
+ <div class="form-group">
1739
+ <label>📝 内容</label>
1740
+ <textarea name="content" rows="6" required style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;"></textarea>
1741
+ </div>
1742
+ <div class="form-group">
1743
+ <label>🏷️ 标签(用逗号分隔)</label>
1744
+ <input type="text" name="tags" placeholder="例如: 学习,文档,教程" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;">
1745
+ </div>
1746
+ <div class="form-group" style="display: flex; align-items: center; gap: 10px; padding: 15px; background: var(--bg-tertiary); border-radius: 8px; margin-top: 15px;">
1747
+ <input type="checkbox" name="isShared" id="isSharedCheckbox" style="width: 20px; height: 20px; cursor: pointer;">
1748
+ <label for="isSharedCheckbox" style="margin: 0; cursor: pointer; display: flex; align-items: center; gap: 8px;">
1749
+ <span style="font-size: 18px;">🌐</span>
1750
+ <div>
1751
+ <div style="font-weight: 600; color: var(--text-primary);">共享到所有群组</div>
1752
+ <div style="font-size: 12px; color: var(--text-secondary); margin-top: 2px;">开启后,此知识条目将对所有群组可见</div>
1753
+ </div>
1754
+ </label>
1755
+ </div>
1756
+ <div style="display: flex; gap: 10px; margin-top: 20px;">
1757
+ <button type="submit" class="btn-primary" style="flex: 1;">保存</button>
1758
+ <button type="button" class="btn-secondary" id="cancelKnowledgeModal" style="flex: 1;">取消</button>
1759
+ </div>
1760
+ </form>
1761
+ </div>
1762
+ </div>
1763
+ `;
1764
+
1765
+ const knowledgeList = document.getElementById('knowledgeList');
1766
+
1767
+ if (knowledgeItems.length === 0) {
1768
+ knowledgeList.innerHTML = '<div class="empty-state" style="grid-column: 1/-1;">暂无知识条目</div>';
1769
+ } else {
1770
+ knowledgeItems.forEach(item => {
1771
+ const card = document.createElement('div');
1772
+ card.className = 'knowledge-card';
1773
+ 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;';
1774
+ card.innerHTML = `
1775
+ ${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>' : ''}
1776
+ <h3 style="margin: 0 0 10px 0; font-size: 18px; ${item.isShared ? 'padding-right: 80px;' : ''}">${item.title}</h3>
1777
+ <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>
1778
+ <div class="knowledge-meta" style="font-size: 12px; color: var(--text-tertiary); margin-bottom: 10px;">
1779
+ <span>👤 ${item.author?.username || item.creator?.username || '未知'}</span>
1780
+ <span style="margin-left: 15px;">📅 ${new Date(item.createdAt).toLocaleDateString()}</span>
1781
+ </div>
1782
+ ${item.tags && item.tags.length > 0 ? `
1783
+ <div class="tags" style="display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 15px;">
1784
+ ${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('')}
1785
+ </div>
1786
+ ` : ''}
1787
+ <div style="display: flex; gap: 10px;">
1788
+ <button class="btn-secondary btn-sm" data-id="${item._id}" data-action="edit" style="flex: 1;">✏️ 编辑</button>
1789
+ <button class="btn-danger btn-sm" data-id="${item._id}" data-action="delete" style="flex: 1;">🗑️ 删除</button>
1790
+ </div>
1791
+ `;
1792
+ card.onmouseenter = () => {
1793
+ card.style.transform = 'translateY(-4px)';
1794
+ card.style.boxShadow = '0 8px 16px rgba(0,0,0,0.1)';
1795
+ };
1796
+ card.onmouseleave = () => {
1797
+ card.style.transform = 'translateY(0)';
1798
+ card.style.boxShadow = 'none';
1799
+ };
1800
+ knowledgeList.appendChild(card);
1801
+ });
1802
+
1803
+ // 编辑按钮
1804
+ document.querySelectorAll('[data-action="edit"]').forEach(btn => {
1805
+ btn.addEventListener('click', async () => {
1806
+ const item = knowledgeItems.find(k => k._id === btn.dataset.id);
1807
+ document.getElementById('modalTitle').textContent = '编辑知识条目';
1808
+ document.querySelector('[name="title"]').value = item.title;
1809
+ document.querySelector('[name="content"]').value = item.content;
1810
+ document.querySelector('[name="tags"]').value = item.tags?.join(', ') || '';
1811
+ document.getElementById('isSharedCheckbox').checked = item.isShared || false;
1812
+ document.getElementById('knowledgeForm').dataset.editId = item._id;
1813
+ document.getElementById('knowledgeModal').classList.remove('hidden');
1814
+ });
1815
+ });
1816
+
1817
+ // 删除按钮
1818
+ document.querySelectorAll('[data-action="delete"]').forEach(btn => {
1819
+ btn.addEventListener('click', async () => {
1820
+ if (confirm('确定要删除这个知识条目吗?')) {
1821
+ try {
1822
+ await fetch(`http://localhost:8765/api/knowledge/${btn.dataset.id}`, {
1823
+ method: 'DELETE',
1824
+ headers: { 'Authorization': `Bearer ${token}` }
1825
+ });
1826
+ alert('删除成功!');
1827
+ await renderKnowledgeView(container);
1828
+ } catch (error) {
1829
+ alert('删除失败: ' + error.message);
1830
+ }
1831
+ }
1832
+ });
1833
+ });
1834
+ }
1835
+
1836
+ // 创建按钮
1837
+ document.getElementById('createKnowledgeBtn').addEventListener('click', () => {
1838
+ document.getElementById('modalTitle').textContent = '创建知识条目';
1839
+ document.getElementById('knowledgeForm').reset();
1840
+ delete document.getElementById('knowledgeForm').dataset.editId;
1841
+ document.getElementById('knowledgeModal').classList.remove('hidden');
1842
+ });
1843
+
1844
+ // 关闭模态框
1845
+ document.getElementById('closeKnowledgeModal').addEventListener('click', () => {
1846
+ document.getElementById('knowledgeModal').classList.add('hidden');
1847
+ });
1848
+
1849
+ document.getElementById('cancelKnowledgeModal').addEventListener('click', () => {
1850
+ document.getElementById('knowledgeModal').classList.add('hidden');
1851
+ });
1852
+
1853
+ // 表单提交
1854
+ document.getElementById('knowledgeForm').addEventListener('submit', async (e) => {
1855
+ e.preventDefault();
1856
+ const formData = new FormData(e.target);
1857
+ const data = {
1858
+ title: formData.get('title'),
1859
+ content: formData.get('content'),
1860
+ tags: formData.get('tags').split(',').map(t => t.trim()).filter(t => t),
1861
+ groupId: currentGroup._id,
1862
+ isShared: document.getElementById('isSharedCheckbox').checked
1863
+ };
1864
+
1865
+ try {
1866
+ const editId = e.target.dataset.editId;
1867
+ const url = editId
1868
+ ? `http://localhost:8765/api/knowledge/${editId}`
1869
+ : 'http://localhost:8765/api/knowledge';
1870
+ const method = editId ? 'PUT' : 'POST';
1871
+
1872
+ const response = await fetch(url, {
1873
+ method,
1874
+ headers: {
1875
+ 'Content-Type': 'application/json',
1876
+ 'Authorization': `Bearer ${token}`
1877
+ },
1878
+ body: JSON.stringify(data)
1879
+ });
1880
+
1881
+ if (!response.ok) {
1882
+ throw new Error('操作失败');
1883
+ }
1884
+
1885
+ alert(editId ? '更新成功!' : '创建成功!');
1886
+ document.getElementById('knowledgeModal').classList.add('hidden');
1887
+ await renderKnowledgeView(container);
1888
+ } catch (error) {
1889
+ alert('操作失败: ' + error.message);
1890
+ }
1891
+ });
1892
+
1893
+ } catch (error) {
1894
+ console.error('加载知识库失败:', error);
1895
+ container.innerHTML = `
1896
+ <div class="view-header">
1897
+ <h2>📚 知识库 - ${currentGroup.name}</h2>
1898
+ </div>
1899
+ <div class="empty-state" style="text-align: center; padding: 40px 20px;">
1900
+ <div style="font-size: 48px; margin-bottom: 16px;">⚠️</div>
1901
+ <h3 style="margin-bottom: 8px; color: var(--danger);">加载失败</h3>
1902
+ <p style="color: var(--text-secondary); margin-bottom: 16px;">${error.message}</p>
1903
+ <button class="btn-primary" onclick="location.reload()">重新加载</button>
1904
+ </div>
1905
+ `;
1906
+ }
1907
+ }
1908
+
1909
+
1910
+ renderView('groups');
1911
+ }
1912
+