collabdocchat 2.1.0 → 2.1.1

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.
@@ -1,581 +1,581 @@
1
- import { ApiService } from '../services/api.js';
2
- import { AuthService } from '../services/auth.js';
3
- import 'emoji-picker-element';
4
-
5
- export function renderAdminDashboard(user, wsService) {
6
- const app = document.getElementById('app');
7
- const apiService = new ApiService();
8
- const authService = new AuthService();
9
- const currentUserId = user.id || user._id;
10
-
11
- let currentGroup = null;
12
- let groups = [];
13
-
14
- app.innerHTML = `
15
- <div class="dashboard">
16
- <aside class="sidebar">
17
- <div class="sidebar-header">
18
- <h2>CollabDocChat</h2>
19
- <span class="badge-admin">绠$悊鍛?/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="tasks">
35
- <span class="icon">馃搵</span> 浠诲姟绠$悊
36
- </button>
37
- <button class="nav-item" data-view="documents">
38
- <span class="icon">馃搫</span> 鏂囨。绠$悊
39
- </button>
40
- <button class="nav-item" data-view="files">
41
- <span class="icon">馃搸</span> 鏂囦欢绠$悊
42
- </button>
43
- <button class="nav-item" data-view="chat">
44
- <span class="icon">馃挰</span> 缇よ亰
45
- </button>
46
- <button class="nav-item" data-view="search">
47
- <span class="icon">馃攳</span> 鎼滅储
48
- </button>
49
- <button class="nav-item" data-view="call">
50
- <span class="icon">馃幉</span> 闅忔満鐐瑰悕
51
- </button>
52
- <button class="nav-item" data-view="audit">
53
- <span class="icon">馃搳</span> 鎿嶄綔璁板綍
54
- </button>
55
- </nav>
56
-
57
- <button class="btn-logout" id="logoutBtn">閫€鍑虹櫥褰?/button>
58
- </aside>
59
-
60
- <main class="main-content">
61
- <div id="contentArea"></div>
62
- </main>
63
- </div>
64
- `;
65
-
66
- // 瀵艰埅鍒囨崲
67
- document.querySelectorAll('.nav-item').forEach(item => {
68
- item.addEventListener('click', () => {
69
- document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
70
- item.classList.add('active');
71
- const view = item.dataset.view;
72
- renderView(view);
73
- });
74
- });
75
-
76
- // 閫€鍑虹櫥褰?
77
- document.getElementById('logoutBtn').addEventListener('click', () => {
78
- authService.logout();
79
- });
80
-
81
- async function renderView(view) {
82
- const contentArea = document.getElementById('contentArea');
83
-
84
- switch(view) {
85
- case 'groups':
86
- await renderGroupsView(contentArea);
87
- break;
88
- case 'tasks':
89
- await renderTasksView(contentArea);
90
- break;
91
- case 'documents':
92
- await renderDocumentsView(contentArea);
93
- break;
94
- case 'chat':
95
- await renderChatView(contentArea);
96
- break;
97
- case 'files':
98
- await renderFilesView(contentArea);
99
- break;
100
- case 'search':
101
- await renderSearchView(contentArea);
102
- break;
103
- case 'call':
104
- await renderCallView(contentArea);
105
- break;
106
- case 'audit':
107
- await renderAuditView(contentArea);
108
- break;
109
- }
110
- }
111
-
112
- async function renderGroupsView(container) {
113
- const result = await apiService.getGroups();
114
- groups = result.groups;
115
-
116
- container.innerHTML = `
117
- <div class="view-header">
118
- <h2>缇ょ粍绠$悊</h2>
119
- <button class="btn-primary" id="createGroupBtn">鍒涘缓缇ょ粍</button>
120
- </div>
121
- <div class="groups-grid" id="groupsList"></div>
122
- <div id="createGroupModal" class="modal hidden">
123
- <div class="modal-content">
124
- <h3>鍒涘缓鏂扮兢缁?/h3>
125
- <form id="createGroupForm">
126
- <div class="form-group">
127
- <label>缇ょ粍鍚嶇О</label>
128
- <input type="text" name="name" placeholder="璇疯緭鍏ョ兢缁勫悕绉? required>
129
- </div>
130
- <div class="form-group">
131
- <label>缇ょ粍鎻忚堪</label>
132
- <textarea name="description" placeholder="璇疯緭鍏ョ兢缁勬弿杩帮紙鍙€夛級"></textarea>
133
- </div>
134
- <div class="form-group">
135
- <label>娣诲姞鎴愬憳锛堝彲閫夛級</label>
136
- <div id="usersList" style="max-height: 200px; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 10px;">
137
- <p>鍔犺浇涓?..</p>
138
- </div>
139
- </div>
140
- <div style="display: flex; gap: 10px;">
141
- <button type="submit" class="btn-primary">鍒涘缓</button>
142
- <button type="button" class="btn-secondary" id="closeModal">鍙栨秷</button>
143
- </div>
144
- </form>
145
- </div>
146
- </div>
147
- <div id="manageMembersModal" class="modal hidden">
148
- <div class="modal-content">
149
- <h3>绠$悊鎴愬憳</h3>
150
- <div id="currentMembers"></div>
151
- <div class="form-group">
152
- <label>娣诲姞鏂版垚鍛?/label>
153
- <div id="availableUsers"></div>
154
- </div>
155
- <button type="button" class="btn-secondary" id="closeMembersModal">鍏抽棴</button>
156
- </div>
157
- </div>
158
- `;
159
-
160
- const groupsList = document.getElementById('groupsList');
161
- groups.forEach(group => {
162
- const groupCard = document.createElement('div');
163
- groupCard.className = 'group-card';
164
- groupCard.innerHTML = `
165
- <h3>${group.name}</h3>
166
- <p>${group.description || '鏆傛棤鎻忚堪'}</p>
167
- <div class="group-stats">
168
- <span>馃懃 ${group.members.length} 鎴愬憳</span>
169
- <span>馃搫 ${group.documents.length} 鏂囨。</span>
170
- </div>
171
- <div style="display: flex; gap: 10px; margin-top: 10px;">
172
- <button class="btn-select" data-id="${group._id}">閫夋嫨</button>
173
- <button class="btn-secondary" data-id="${group._id}" data-action="manage">绠$悊鎴愬憳</button>
174
- </div>
175
- `;
176
- groupsList.appendChild(groupCard);
177
- });
178
-
179
- document.querySelectorAll('.btn-select').forEach(btn => {
180
- btn.addEventListener('click', () => {
181
- currentGroup = groups.find(g => g._id === btn.dataset.id);
182
- wsService.joinGroup(currentGroup._id);
183
- alert(`宸插姞鍏ョ兢缁? ${currentGroup.name}`);
184
- });
185
- });
186
-
187
- document.querySelectorAll('[data-action="manage"]').forEach(btn => {
188
- btn.addEventListener('click', async () => {
189
- const groupId = btn.dataset.id;
190
- await showManageMembersModal(groupId);
191
- });
192
- });
193
-
194
- document.getElementById('createGroupBtn').addEventListener('click', async () => {
195
- document.getElementById('createGroupModal').classList.remove('hidden');
196
- await loadUsers();
197
- });
198
-
199
- document.getElementById('closeModal').addEventListener('click', () => {
200
- document.getElementById('createGroupModal').classList.add('hidden');
201
- });
202
-
203
- document.getElementById('closeMembersModal').addEventListener('click', () => {
204
- document.getElementById('manageMembersModal').classList.add('hidden');
205
- });
206
-
207
- document.getElementById('createGroupForm').addEventListener('submit', async (e) => {
208
- e.preventDefault();
209
- const formData = new FormData(e.target);
210
- const selectedUsers = Array.from(document.querySelectorAll('#usersList input:checked')).map(cb => cb.value);
211
-
212
- try {
213
- const result = await apiService.createGroup(
214
- formData.get('name'),
215
- formData.get('description'),
216
- selectedUsers
217
- );
218
- alert('缇ょ粍鍒涘缓鎴愬姛锛?);
219
- await renderGroupsView(container);
220
- document.getElementById('createGroupModal').classList.add('hidden');
221
- } catch (error) {
222
- console.error('鍒涘缓缇ょ粍閿欒:', error);
223
- alert('鍒涘缓澶辫触: ' + error.message);
224
- }
225
- });
226
- }
227
-
228
- async function loadUsers() {
229
- try {
230
- const result = await apiService.getAllUsers();
231
- const usersList = document.getElementById('usersList');
232
- usersList.innerHTML = result.users.map(u => `
233
- <label style="display: flex; align-items: center; gap: 10px; padding: 8px; cursor: pointer;">
234
- <input type="checkbox" value="${u._id}">
235
- <div class="avatar" style="width: 30px; height: 30px; font-size: 14px;">${u.username[0].toUpperCase()}</div>
236
- <span>${u.username} (${u.role === 'admin' ? '绠$悊鍛? : '鐢ㄦ埛'})</span>
237
- </label>
238
- `).join('');
239
- } catch (error) {
240
- console.error('鍔犺浇鐢ㄦ埛澶辫触:', error);
241
- document.getElementById('usersList').innerHTML = '<p style="color: var(--danger);">鍔犺浇澶辫触</p>';
242
- }
243
- }
244
-
245
- async function showManageMembersModal(groupId) {
246
- try {
247
- const groupResult = await apiService.getGroup(groupId);
248
- const usersResult = await apiService.getAllUsers();
249
- const group = groupResult.group;
250
-
251
- const currentMembers = document.getElementById('currentMembers');
252
- currentMembers.innerHTML = `
253
- <h4>褰撳墠鎴愬憳 (${group.members.length})</h4>
254
- <div style="max-height: 200px; overflow-y: auto;">
255
- ${group.members.map(member => `
256
- <div style="display: flex; align-items: center; justify-content: space-between; padding: 8px; border-bottom: 1px solid var(--border);">
257
- <div style="display: flex; align-items: center; gap: 10px;">
258
- <div class="avatar" style="width: 30px; height: 30px; font-size: 14px;">${member.username[0].toUpperCase()}</div>
259
- <span>${member.username} ${member._id.toString() === group.admin._id.toString() ? '(绠$悊鍛?' : ''}</span>
260
- </div>
261
- ${member._id.toString() !== group.admin._id.toString() ?
262
- `<button class="btn-secondary btn-sm" onclick="removeMember('${groupId}', '${member._id}')">绉婚櫎</button>` :
263
- ''}
264
- </div>
265
- `).join('')}
266
- </div>
267
- `;
268
-
269
- const memberIds = group.members.map(m => m._id.toString());
270
- const availableUsers = usersResult.users.filter(u => !memberIds.includes(u._id));
271
-
272
- const availableUsersDiv = document.getElementById('availableUsers');
273
- if (availableUsers.length === 0) {
274
- availableUsersDiv.innerHTML = '<p>鎵€鏈夌敤鎴烽兘宸插湪缇ょ粍涓?/p>';
275
- } else {
276
- availableUsersDiv.innerHTML = availableUsers.map(u => `
277
- <div style="display: flex; align-items: center; justify-content: space-between; padding: 8px; border-bottom: 1px solid var(--border);">
278
- <div style="display: flex; align-items: center; gap: 10px;">
279
- <div class="avatar" style="width: 30px; height: 30px; font-size: 14px;">${u.username[0].toUpperCase()}</div>
280
- <span>${u.username}</span>
281
- </div>
282
- <button class="btn-primary btn-sm" onclick="addMember('${groupId}', '${u._id}')">娣诲姞</button>
283
- </div>
284
- `).join('');
285
- }
286
-
287
- document.getElementById('manageMembersModal').classList.remove('hidden');
288
- } catch (error) {
289
- console.error('鍔犺浇鎴愬憳澶辫触:', error);
290
- alert('鍔犺浇澶辫触: ' + error.message);
291
- }
292
- }
293
-
294
- // 鍏ㄥ眬鍑芥暟渚涙寜閽皟鐢?
295
- window.addMember = async (groupId, userId) => {
296
- try {
297
- await apiService.addMember(groupId, userId);
298
- alert('鎴愬憳娣诲姞鎴愬姛锛?);
299
- await showManageMembersModal(groupId);
300
- } catch (error) {
301
- alert('娣诲姞澶辫触: ' + error.message);
302
- }
303
- };
304
-
305
- window.removeMember = async (groupId, userId) => {
306
- if (confirm('纭畾瑕佺Щ闄よ鎴愬憳鍚楋紵')) {
307
- try {
308
- await apiService.removeMember(groupId, userId);
309
- alert('鎴愬憳绉婚櫎鎴愬姛锛?);
310
- await showManageMembersModal(groupId);
311
- } catch (error) {
312
- alert('绉婚櫎澶辫触: ' + error.message);
313
- }
314
- }
315
- };
316
-
317
- async function renderTasksView(container) {
318
- if (!currentGroup) {
319
- container.innerHTML = '<div class="empty-state">璇峰厛閫夋嫨涓€涓兢缁?/div>';
320
- return;
321
- }
322
-
323
- const result = await apiService.getTasks(currentGroup._id);
324
-
325
- container.innerHTML = `
326
- <div class="view-header">
327
- <h2>浠诲姟绠$悊 - ${currentGroup.name}</h2>
328
- <button class="btn-primary" id="createTaskBtn">鍒涘缓浠诲姟</button>
329
- </div>
330
- <div class="tasks-list" id="tasksList"></div>
331
- <div id="createTaskModal" class="modal hidden">
332
- <div class="modal-content">
333
- <h3>鍒涘缓鏂颁换鍔?/h3>
334
- <form id="createTaskForm">
335
- <div class="form-group">
336
- <label>浠诲姟鏍囬</label>
337
- <input type="text" name="title" placeholder="璇疯緭鍏ヤ换鍔℃爣棰? required>
338
- </div>
339
- <div class="form-group">
340
- <label>浠诲姟鎻忚堪</label>
341
- <textarea name="description" placeholder="璇疯緭鍏ヤ换鍔℃弿杩?></textarea>
342
- </div>
343
- <div class="form-group">
344
- <label>鎴鏃ユ湡</label>
345
- <input type="date" name="deadline">
346
- </div>
347
- <div style="display: flex; gap: 10px;">
348
- <button type="submit" class="btn-primary">鍒涘缓</button>
349
- <button type="button" class="btn-secondary" id="closeTaskModal">鍙栨秷</button>
350
- </div>
351
- </form>
352
- </div>
353
- </div>
354
- `;
355
-
356
- const tasksList = document.getElementById('tasksList');
357
- if (result.tasks.length === 0) {
358
- tasksList.innerHTML = '<div class="empty-state">鏆傛棤浠诲姟</div>';
359
- } else {
360
- result.tasks.forEach(task => {
361
- const taskCard = document.createElement('div');
362
- taskCard.className = `task-card status-${task.status}`;
363
- taskCard.innerHTML = `
364
- <div style="display: flex; justify-content: space-between; align-items: start;">
365
- <div style="flex: 1;">
366
- <h3>${task.title}</h3>
367
- <p>${task.description || '鏃犳弿杩?}</p>
368
- <div class="task-meta">
369
- <span class="status-badge">${getStatusText(task.status)}</span>
370
- <span>鎴: ${task.deadline ? new Date(task.deadline).toLocaleDateString() : '鏃?}</span>
371
- </div>
372
- </div>
373
- <button class="btn-danger btn-sm" data-id="${task._id}" data-action="delete-task" title="鍒犻櫎浠诲姟" style="min-width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;">馃棏锔?鍒犻櫎</button>
374
- </div>
375
- `;
376
- tasksList.appendChild(taskCard);
377
- });
378
-
379
- // 娣诲姞鍒犻櫎浠诲姟浜嬩欢
380
- document.querySelectorAll('[data-action="delete-task"]').forEach(btn => {
381
- btn.addEventListener('click', async (e) => {
382
- e.stopPropagation();
383
- const taskId = btn.dataset.id;
384
- if (confirm('纭畾瑕佸垹闄よ繖涓换鍔″悧锛熷垹闄ゅ悗鏃犳硶鎭㈠锛?)) {
385
- try {
386
- await apiService.deleteTask(taskId);
387
- alert('浠诲姟鍒犻櫎鎴愬姛锛?);
388
- await renderTasksView(container);
389
- } catch (error) {
390
- console.error('鍒犻櫎浠诲姟閿欒:', error);
391
- alert('鍒犻櫎澶辫触: ' + (error.message || '鏈煡閿欒'));
392
- }
393
- }
394
- });
395
- });
396
- }
397
-
398
- document.getElementById('createTaskBtn').addEventListener('click', () => {
399
- document.getElementById('createTaskModal').classList.remove('hidden');
400
- });
401
-
402
- document.getElementById('closeTaskModal').addEventListener('click', () => {
403
- document.getElementById('createTaskModal').classList.add('hidden');
404
- });
405
-
406
- document.getElementById('createTaskForm').addEventListener('submit', async (e) => {
407
- e.preventDefault();
408
- const formData = new FormData(e.target);
409
- try {
410
- // 鑾峰彇缇ょ粍淇℃伅锛岃嚜鍔ㄥ垎閰嶇粰鎵€鏈夋垚鍛?
411
- const groupResult = await apiService.getGroup(currentGroup._id);
412
- const memberIds = groupResult.group.members.map(m => m._id);
413
-
414
- await apiService.createTask({
415
- title: formData.get('title'),
416
- description: formData.get('description'),
417
- groupId: currentGroup._id,
418
- assignedTo: memberIds, // 鍒嗛厤缁欐墍鏈夋垚鍛?
419
- deadline: formData.get('deadline') || null
420
- });
421
- alert('浠诲姟鍒涘缓鎴愬姛锛佸凡鍒嗛厤缁欐墍鏈夌兢缁勬垚鍛?);
422
- await renderTasksView(container);
423
- document.getElementById('createTaskModal').classList.add('hidden');
424
- } catch (error) {
425
- console.error('鍒涘缓浠诲姟閿欒:', error);
426
- alert('鍒涘缓澶辫触: ' + error.message);
427
- }
428
- });
429
- }
430
-
431
- async function renderDocumentsView(container) {
432
- if (!currentGroup) {
433
- container.innerHTML = '<div class="empty-state">璇峰厛閫夋嫨涓€涓兢缁?/div>';
434
- return;
435
- }
436
-
437
- const result = await apiService.getDocuments(currentGroup._id);
438
-
439
- container.innerHTML = `
440
- <div class="view-header">
441
- <h2>鏂囨。绠$悊 - ${currentGroup.name}</h2>
442
- <button class="btn-primary" id="createDocBtn">鍒涘缓鏂囨。</button>
443
- </div>
444
- <div class="documents-list" id="docsList"></div>
445
- <div id="createDocModal" class="modal hidden">
446
- <div class="modal-content">
447
- <h3>鍒涘缓鏂版枃妗?/h3>
448
- <form id="createDocForm">
449
- <div class="form-group">
450
- <label>鏂囨。鏍囬</label>
451
- <input type="text" name="title" placeholder="璇疯緭鍏ユ枃妗f爣棰? required>
452
- </div>
453
- <div class="form-group">
454
- <label>鏂囨。鍐呭</label>
455
- <textarea name="content" placeholder="璇疯緭鍏ユ枃妗e唴瀹? rows="6"></textarea>
456
- </div>
457
- <div class="form-group">
458
- <label>鏉冮檺璁剧疆</label>
459
- <select name="permission">
460
- <option value="editable">鍙紪杈?/option>
461
- <option value="readonly">鍙</option>
462
- </select>
463
- </div>
464
- <div style="display: flex; gap: 10px;">
465
- <button type="submit" class="btn-primary">鍒涘缓</button>
466
- <button type="button" class="btn-secondary" id="closeDocModal">鍙栨秷</button>
467
- </div>
468
- </form>
469
- </div>
470
- </div>
471
- `;
472
-
473
- const docsList = document.getElementById('docsList');
474
- if (result.documents.length === 0) {
475
- docsList.innerHTML = '<div class="empty-state">鏆傛棤鏂囨。</div>';
476
- } else {
477
- result.documents.forEach(doc => {
478
- const docCard = document.createElement('div');
479
- docCard.className = 'document-card';
480
- docCard.innerHTML = `
481
- <div style="display: flex; justify-content: space-between; align-items: start;">
482
- <div style="flex: 1;">
483
- <h3>馃搫 ${doc.title}</h3>
484
- <div class="doc-meta">
485
- <span>鍒涘缓鑰? ${doc.creator.username}</span>
486
- <span>${doc.permission === 'readonly' ? '馃敀 鍙' : '鉁忥笍 鍙紪杈?}</span>
487
- </div>
488
- </div>
489
- <div style="display: flex; gap: 10px; align-items: center;">
490
- <button class="btn-edit" data-id="${doc._id}">缂栬緫</button>
491
- <button class="btn-danger btn-sm" data-id="${doc._id}" data-action="delete-doc" title="鍒犻櫎鏂囨。" style="min-width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;">馃棏锔?鍒犻櫎</button>
492
- </div>
493
- </div>
494
- `;
495
- docsList.appendChild(docCard);
496
- });
497
-
498
- // 娣诲姞缂栬緫鎸夐挳浜嬩欢
499
- document.querySelectorAll('.btn-edit').forEach(btn => {
500
- btn.addEventListener('click', () => {
501
- renderDocumentEditor(container, btn.dataset.id);
502
- });
503
- });
504
-
505
- // 娣诲姞鍒犻櫎鏂囨。浜嬩欢
506
- document.querySelectorAll('[data-action="delete-doc"]').forEach(btn => {
507
- btn.addEventListener('click', async (e) => {
508
- e.stopPropagation();
509
- const docId = btn.dataset.id;
510
- if (confirm('纭畾瑕佸垹闄よ繖涓枃妗e悧锛熷垹闄ゅ悗鏃犳硶鎭㈠锛?)) {
511
- try {
512
- await apiService.deleteDocument(docId);
513
- alert('鏂囨。鍒犻櫎鎴愬姛锛?);
514
- await renderDocumentsView(container);
515
- } catch (error) {
516
- console.error('鍒犻櫎鏂囨。閿欒:', error);
517
- alert('鍒犻櫎澶辫触: ' + (error.message || '鏈煡閿欒'));
518
- }
519
- }
520
- });
521
- });
522
- }
523
-
524
- document.getElementById('createDocBtn').addEventListener('click', () => {
525
- document.getElementById('createDocModal').classList.remove('hidden');
526
- });
527
-
528
- document.getElementById('closeDocModal').addEventListener('click', () => {
529
- document.getElementById('createDocModal').classList.add('hidden');
530
- });
531
-
532
- document.getElementById('createDocForm').addEventListener('submit', async (e) => {
533
- e.preventDefault();
534
- const formData = new FormData(e.target);
535
- try {
536
- await apiService.createDocument(
537
- formData.get('title'),
538
- formData.get('content'),
539
- currentGroup._id,
540
- formData.get('permission')
541
- );
542
- alert('鏂囨。鍒涘缓鎴愬姛锛?);
543
- await renderDocumentsView(container);
544
- document.getElementById('createDocModal').classList.add('hidden');
545
- } catch (error) {
546
- console.error('鍒涘缓鏂囨。閿欒:', error);
547
- alert('鍒涘缓澶辫触: ' + error.message);
548
- }
549
- });
550
- }
551
-
1
+ import { ApiService } from '../services/api.js';
2
+ import { AuthService } from '../services/auth.js';
3
+ import 'emoji-picker-element';
4
+
5
+ export function renderAdminDashboard(user, wsService) {
6
+ const app = document.getElementById('app');
7
+ const apiService = new ApiService();
8
+ const authService = new AuthService();
9
+ const currentUserId = user.id || user._id;
10
+
11
+ let currentGroup = null;
12
+ let groups = [];
13
+
14
+ app.innerHTML = `
15
+ <div class="dashboard">
16
+ <aside class="sidebar">
17
+ <div class="sidebar-header">
18
+ <h2>CollabDocChat</h2>
19
+ <span class="badge-admin">管理员</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="tasks">
35
+ <span class="icon">📋</span> 任务管理
36
+ </button>
37
+ <button class="nav-item" data-view="documents">
38
+ <span class="icon">📄</span> 文档管理
39
+ </button>
40
+ <button class="nav-item" data-view="files">
41
+ <span class="icon">📎</span> 文件管理
42
+ </button>
43
+ <button class="nav-item" data-view="chat">
44
+ <span class="icon">💬</span> 群聊
45
+ </button>
46
+ <button class="nav-item" data-view="search">
47
+ <span class="icon">🔍</span> 搜索
48
+ </button>
49
+ <button class="nav-item" data-view="call">
50
+ <span class="icon">🎲</span> 随机点名
51
+ </button>
52
+ <button class="nav-item" data-view="audit">
53
+ <span class="icon">📊</span> 操作记录
54
+ </button>
55
+ </nav>
56
+
57
+ <button class="btn-logout" id="logoutBtn">退出登录</button>
58
+ </aside>
59
+
60
+ <main class="main-content">
61
+ <div id="contentArea"></div>
62
+ </main>
63
+ </div>
64
+ `;
65
+
66
+ // 导航切换
67
+ document.querySelectorAll('.nav-item').forEach(item => {
68
+ item.addEventListener('click', () => {
69
+ document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
70
+ item.classList.add('active');
71
+ const view = item.dataset.view;
72
+ renderView(view);
73
+ });
74
+ });
75
+
76
+ // 退出登录
77
+ document.getElementById('logoutBtn').addEventListener('click', () => {
78
+ authService.logout();
79
+ });
80
+
81
+ async function renderView(view) {
82
+ const contentArea = document.getElementById('contentArea');
83
+
84
+ switch(view) {
85
+ case 'groups':
86
+ await renderGroupsView(contentArea);
87
+ break;
88
+ case 'tasks':
89
+ await renderTasksView(contentArea);
90
+ break;
91
+ case 'documents':
92
+ await renderDocumentsView(contentArea);
93
+ break;
94
+ case 'chat':
95
+ await renderChatView(contentArea);
96
+ break;
97
+ case 'files':
98
+ await renderFilesView(contentArea);
99
+ break;
100
+ case 'search':
101
+ await renderSearchView(contentArea);
102
+ break;
103
+ case 'call':
104
+ await renderCallView(contentArea);
105
+ break;
106
+ case 'audit':
107
+ await renderAuditView(contentArea);
108
+ break;
109
+ }
110
+ }
111
+
112
+ async function renderGroupsView(container) {
113
+ const result = await apiService.getGroups();
114
+ groups = result.groups;
115
+
116
+ container.innerHTML = `
117
+ <div class="view-header">
118
+ <h2>群组管理</h2>
119
+ <button class="btn-primary" id="createGroupBtn">创建群组</button>
120
+ </div>
121
+ <div class="groups-grid" id="groupsList"></div>
122
+ <div id="createGroupModal" class="modal hidden">
123
+ <div class="modal-content">
124
+ <h3>创建新群组</h3>
125
+ <form id="createGroupForm">
126
+ <div class="form-group">
127
+ <label>群组名称</label>
128
+ <input type="text" name="name" placeholder="请输入群组名称" required>
129
+ </div>
130
+ <div class="form-group">
131
+ <label>群组描述</label>
132
+ <textarea name="description" placeholder="请输入群组描述(可选)"></textarea>
133
+ </div>
134
+ <div class="form-group">
135
+ <label>添加成员(可选)</label>
136
+ <div id="usersList" style="max-height: 200px; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 10px;">
137
+ <p>加载中...</p>
138
+ </div>
139
+ </div>
140
+ <div style="display: flex; gap: 10px;">
141
+ <button type="submit" class="btn-primary">创建</button>
142
+ <button type="button" class="btn-secondary" id="closeModal">取消</button>
143
+ </div>
144
+ </form>
145
+ </div>
146
+ </div>
147
+ <div id="manageMembersModal" class="modal hidden">
148
+ <div class="modal-content">
149
+ <h3>管理成员</h3>
150
+ <div id="currentMembers"></div>
151
+ <div class="form-group">
152
+ <label>添加新成员</label>
153
+ <div id="availableUsers"></div>
154
+ </div>
155
+ <button type="button" class="btn-secondary" id="closeMembersModal">关闭</button>
156
+ </div>
157
+ </div>
158
+ `;
159
+
160
+ const groupsList = document.getElementById('groupsList');
161
+ groups.forEach(group => {
162
+ const groupCard = document.createElement('div');
163
+ groupCard.className = 'group-card';
164
+ groupCard.innerHTML = `
165
+ <h3>${group.name}</h3>
166
+ <p>${group.description || '暂无描述'}</p>
167
+ <div class="group-stats">
168
+ <span>👥 ${group.members.length} 成员</span>
169
+ <span>📄 ${group.documents.length} 文档</span>
170
+ </div>
171
+ <div style="display: flex; gap: 10px; margin-top: 10px;">
172
+ <button class="btn-select" data-id="${group._id}">选择</button>
173
+ <button class="btn-secondary" data-id="${group._id}" data-action="manage">管理成员</button>
174
+ </div>
175
+ `;
176
+ groupsList.appendChild(groupCard);
177
+ });
178
+
179
+ document.querySelectorAll('.btn-select').forEach(btn => {
180
+ btn.addEventListener('click', () => {
181
+ currentGroup = groups.find(g => g._id === btn.dataset.id);
182
+ wsService.joinGroup(currentGroup._id);
183
+ alert(`已加入群组: ${currentGroup.name}`);
184
+ });
185
+ });
186
+
187
+ document.querySelectorAll('[data-action="manage"]').forEach(btn => {
188
+ btn.addEventListener('click', async () => {
189
+ const groupId = btn.dataset.id;
190
+ await showManageMembersModal(groupId);
191
+ });
192
+ });
193
+
194
+ document.getElementById('createGroupBtn').addEventListener('click', async () => {
195
+ document.getElementById('createGroupModal').classList.remove('hidden');
196
+ await loadUsers();
197
+ });
198
+
199
+ document.getElementById('closeModal').addEventListener('click', () => {
200
+ document.getElementById('createGroupModal').classList.add('hidden');
201
+ });
202
+
203
+ document.getElementById('closeMembersModal').addEventListener('click', () => {
204
+ document.getElementById('manageMembersModal').classList.add('hidden');
205
+ });
206
+
207
+ document.getElementById('createGroupForm').addEventListener('submit', async (e) => {
208
+ e.preventDefault();
209
+ const formData = new FormData(e.target);
210
+ const selectedUsers = Array.from(document.querySelectorAll('#usersList input:checked')).map(cb => cb.value);
211
+
212
+ try {
213
+ const result = await apiService.createGroup(
214
+ formData.get('name'),
215
+ formData.get('description'),
216
+ selectedUsers
217
+ );
218
+ alert('群组创建成功!');
219
+ await renderGroupsView(container);
220
+ document.getElementById('createGroupModal').classList.add('hidden');
221
+ } catch (error) {
222
+ console.error('创建群组错误:', error);
223
+ alert('创建失败: ' + error.message);
224
+ }
225
+ });
226
+ }
227
+
228
+ async function loadUsers() {
229
+ try {
230
+ const result = await apiService.getAllUsers();
231
+ const usersList = document.getElementById('usersList');
232
+ usersList.innerHTML = result.users.map(u => `
233
+ <label style="display: flex; align-items: center; gap: 10px; padding: 8px; cursor: pointer;">
234
+ <input type="checkbox" value="${u._id}">
235
+ <div class="avatar" style="width: 30px; height: 30px; font-size: 14px;">${u.username[0].toUpperCase()}</div>
236
+ <span>${u.username} (${u.role === 'admin' ? '管理员' : '用户'})</span>
237
+ </label>
238
+ `).join('');
239
+ } catch (error) {
240
+ console.error('加载用户失败:', error);
241
+ document.getElementById('usersList').innerHTML = '<p style="color: var(--danger);">加载失败</p>';
242
+ }
243
+ }
244
+
245
+ async function showManageMembersModal(groupId) {
246
+ try {
247
+ const groupResult = await apiService.getGroup(groupId);
248
+ const usersResult = await apiService.getAllUsers();
249
+ const group = groupResult.group;
250
+
251
+ const currentMembers = document.getElementById('currentMembers');
252
+ currentMembers.innerHTML = `
253
+ <h4>当前成员 (${group.members.length})</h4>
254
+ <div style="max-height: 200px; overflow-y: auto;">
255
+ ${group.members.map(member => `
256
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 8px; border-bottom: 1px solid var(--border);">
257
+ <div style="display: flex; align-items: center; gap: 10px;">
258
+ <div class="avatar" style="width: 30px; height: 30px; font-size: 14px;">${member.username[0].toUpperCase()}</div>
259
+ <span>${member.username} ${member._id.toString() === group.admin._id.toString() ? '(管理员)' : ''}</span>
260
+ </div>
261
+ ${member._id.toString() !== group.admin._id.toString() ?
262
+ `<button class="btn-secondary btn-sm" onclick="removeMember('${groupId}', '${member._id}')">移除</button>` :
263
+ ''}
264
+ </div>
265
+ `).join('')}
266
+ </div>
267
+ `;
268
+
269
+ const memberIds = group.members.map(m => m._id.toString());
270
+ const availableUsers = usersResult.users.filter(u => !memberIds.includes(u._id));
271
+
272
+ const availableUsersDiv = document.getElementById('availableUsers');
273
+ if (availableUsers.length === 0) {
274
+ availableUsersDiv.innerHTML = '<p>所有用户都已在群组中</p>';
275
+ } else {
276
+ availableUsersDiv.innerHTML = availableUsers.map(u => `
277
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 8px; border-bottom: 1px solid var(--border);">
278
+ <div style="display: flex; align-items: center; gap: 10px;">
279
+ <div class="avatar" style="width: 30px; height: 30px; font-size: 14px;">${u.username[0].toUpperCase()}</div>
280
+ <span>${u.username}</span>
281
+ </div>
282
+ <button class="btn-primary btn-sm" onclick="addMember('${groupId}', '${u._id}')">添加</button>
283
+ </div>
284
+ `).join('');
285
+ }
286
+
287
+ document.getElementById('manageMembersModal').classList.remove('hidden');
288
+ } catch (error) {
289
+ console.error('加载成员失败:', error);
290
+ alert('加载失败: ' + error.message);
291
+ }
292
+ }
293
+
294
+ // 全局函数供按钮调用
295
+ window.addMember = async (groupId, userId) => {
296
+ try {
297
+ await apiService.addMember(groupId, userId);
298
+ alert('成员添加成功!');
299
+ await showManageMembersModal(groupId);
300
+ } catch (error) {
301
+ alert('添加失败: ' + error.message);
302
+ }
303
+ };
304
+
305
+ window.removeMember = async (groupId, userId) => {
306
+ if (confirm('确定要移除该成员吗?')) {
307
+ try {
308
+ await apiService.removeMember(groupId, userId);
309
+ alert('成员移除成功!');
310
+ await showManageMembersModal(groupId);
311
+ } catch (error) {
312
+ alert('移除失败: ' + error.message);
313
+ }
314
+ }
315
+ };
316
+
317
+ async function renderTasksView(container) {
318
+ if (!currentGroup) {
319
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
320
+ return;
321
+ }
322
+
323
+ const result = await apiService.getTasks(currentGroup._id);
324
+
325
+ container.innerHTML = `
326
+ <div class="view-header">
327
+ <h2>任务管理 - ${currentGroup.name}</h2>
328
+ <button class="btn-primary" id="createTaskBtn">创建任务</button>
329
+ </div>
330
+ <div class="tasks-list" id="tasksList"></div>
331
+ <div id="createTaskModal" class="modal hidden">
332
+ <div class="modal-content">
333
+ <h3>创建新任务</h3>
334
+ <form id="createTaskForm">
335
+ <div class="form-group">
336
+ <label>任务标题</label>
337
+ <input type="text" name="title" placeholder="请输入任务标题" required>
338
+ </div>
339
+ <div class="form-group">
340
+ <label>任务描述</label>
341
+ <textarea name="description" placeholder="请输入任务描述"></textarea>
342
+ </div>
343
+ <div class="form-group">
344
+ <label>截止日期</label>
345
+ <input type="date" name="deadline">
346
+ </div>
347
+ <div style="display: flex; gap: 10px;">
348
+ <button type="submit" class="btn-primary">创建</button>
349
+ <button type="button" class="btn-secondary" id="closeTaskModal">取消</button>
350
+ </div>
351
+ </form>
352
+ </div>
353
+ </div>
354
+ `;
355
+
356
+ const tasksList = document.getElementById('tasksList');
357
+ if (result.tasks.length === 0) {
358
+ tasksList.innerHTML = '<div class="empty-state">暂无任务</div>';
359
+ } else {
360
+ result.tasks.forEach(task => {
361
+ const taskCard = document.createElement('div');
362
+ taskCard.className = `task-card status-${task.status}`;
363
+ taskCard.innerHTML = `
364
+ <div style="display: flex; justify-content: space-between; align-items: start;">
365
+ <div style="flex: 1;">
366
+ <h3>${task.title}</h3>
367
+ <p>${task.description || '无描述'}</p>
368
+ <div class="task-meta">
369
+ <span class="status-badge">${getStatusText(task.status)}</span>
370
+ <span>截止: ${task.deadline ? new Date(task.deadline).toLocaleDateString() : '无'}</span>
371
+ </div>
372
+ </div>
373
+ <button class="btn-danger btn-sm" data-id="${task._id}" data-action="delete-task" title="删除任务" style="min-width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;">🗑️ 删除</button>
374
+ </div>
375
+ `;
376
+ tasksList.appendChild(taskCard);
377
+ });
378
+
379
+ // 添加删除任务事件
380
+ document.querySelectorAll('[data-action="delete-task"]').forEach(btn => {
381
+ btn.addEventListener('click', async (e) => {
382
+ e.stopPropagation();
383
+ const taskId = btn.dataset.id;
384
+ if (confirm('确定要删除这个任务吗?删除后无法恢复!')) {
385
+ try {
386
+ await apiService.deleteTask(taskId);
387
+ alert('任务删除成功!');
388
+ await renderTasksView(container);
389
+ } catch (error) {
390
+ console.error('删除任务错误:', error);
391
+ alert('删除失败: ' + (error.message || '未知错误'));
392
+ }
393
+ }
394
+ });
395
+ });
396
+ }
397
+
398
+ document.getElementById('createTaskBtn').addEventListener('click', () => {
399
+ document.getElementById('createTaskModal').classList.remove('hidden');
400
+ });
401
+
402
+ document.getElementById('closeTaskModal').addEventListener('click', () => {
403
+ document.getElementById('createTaskModal').classList.add('hidden');
404
+ });
405
+
406
+ document.getElementById('createTaskForm').addEventListener('submit', async (e) => {
407
+ e.preventDefault();
408
+ const formData = new FormData(e.target);
409
+ try {
410
+ // 获取群组信息,自动分配给所有成员
411
+ const groupResult = await apiService.getGroup(currentGroup._id);
412
+ const memberIds = groupResult.group.members.map(m => m._id);
413
+
414
+ await apiService.createTask({
415
+ title: formData.get('title'),
416
+ description: formData.get('description'),
417
+ groupId: currentGroup._id,
418
+ assignedTo: memberIds, // 分配给所有成员
419
+ deadline: formData.get('deadline') || null
420
+ });
421
+ alert('任务创建成功!已分配给所有群组成员');
422
+ await renderTasksView(container);
423
+ document.getElementById('createTaskModal').classList.add('hidden');
424
+ } catch (error) {
425
+ console.error('创建任务错误:', error);
426
+ alert('创建失败: ' + error.message);
427
+ }
428
+ });
429
+ }
430
+
431
+ async function renderDocumentsView(container) {
432
+ if (!currentGroup) {
433
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
434
+ return;
435
+ }
436
+
437
+ const result = await apiService.getDocuments(currentGroup._id);
438
+
439
+ container.innerHTML = `
440
+ <div class="view-header">
441
+ <h2>文档管理 - ${currentGroup.name}</h2>
442
+ <button class="btn-primary" id="createDocBtn">创建文档</button>
443
+ </div>
444
+ <div class="documents-list" id="docsList"></div>
445
+ <div id="createDocModal" class="modal hidden">
446
+ <div class="modal-content">
447
+ <h3>创建新文档</h3>
448
+ <form id="createDocForm">
449
+ <div class="form-group">
450
+ <label>文档标题</label>
451
+ <input type="text" name="title" placeholder="请输入文档标题" required>
452
+ </div>
453
+ <div class="form-group">
454
+ <label>文档内容</label>
455
+ <textarea name="content" placeholder="请输入文档内容" rows="6"></textarea>
456
+ </div>
457
+ <div class="form-group">
458
+ <label>权限设置</label>
459
+ <select name="permission">
460
+ <option value="editable">可编辑</option>
461
+ <option value="readonly">只读</option>
462
+ </select>
463
+ </div>
464
+ <div style="display: flex; gap: 10px;">
465
+ <button type="submit" class="btn-primary">创建</button>
466
+ <button type="button" class="btn-secondary" id="closeDocModal">取消</button>
467
+ </div>
468
+ </form>
469
+ </div>
470
+ </div>
471
+ `;
472
+
473
+ const docsList = document.getElementById('docsList');
474
+ if (result.documents.length === 0) {
475
+ docsList.innerHTML = '<div class="empty-state">暂无文档</div>';
476
+ } else {
477
+ result.documents.forEach(doc => {
478
+ const docCard = document.createElement('div');
479
+ docCard.className = 'document-card';
480
+ docCard.innerHTML = `
481
+ <div style="display: flex; justify-content: space-between; align-items: start;">
482
+ <div style="flex: 1;">
483
+ <h3>📄 ${doc.title}</h3>
484
+ <div class="doc-meta">
485
+ <span>创建者: ${doc.creator.username}</span>
486
+ <span>${doc.permission === 'readonly' ? '🔒 只读' : '✏️ 可编辑'}</span>
487
+ </div>
488
+ </div>
489
+ <div style="display: flex; gap: 10px; align-items: center;">
490
+ <button class="btn-edit" data-id="${doc._id}">编辑</button>
491
+ <button class="btn-danger btn-sm" data-id="${doc._id}" data-action="delete-doc" title="删除文档" style="min-width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;">🗑️ 删除</button>
492
+ </div>
493
+ </div>
494
+ `;
495
+ docsList.appendChild(docCard);
496
+ });
497
+
498
+ // 添加编辑按钮事件
499
+ document.querySelectorAll('.btn-edit').forEach(btn => {
500
+ btn.addEventListener('click', () => {
501
+ renderDocumentEditor(container, btn.dataset.id);
502
+ });
503
+ });
504
+
505
+ // 添加删除文档事件
506
+ document.querySelectorAll('[data-action="delete-doc"]').forEach(btn => {
507
+ btn.addEventListener('click', async (e) => {
508
+ e.stopPropagation();
509
+ const docId = btn.dataset.id;
510
+ if (confirm('确定要删除这个文档吗?删除后无法恢复!')) {
511
+ try {
512
+ await apiService.deleteDocument(docId);
513
+ alert('文档删除成功!');
514
+ await renderDocumentsView(container);
515
+ } catch (error) {
516
+ console.error('删除文档错误:', error);
517
+ alert('删除失败: ' + (error.message || '未知错误'));
518
+ }
519
+ }
520
+ });
521
+ });
522
+ }
523
+
524
+ document.getElementById('createDocBtn').addEventListener('click', () => {
525
+ document.getElementById('createDocModal').classList.remove('hidden');
526
+ });
527
+
528
+ document.getElementById('closeDocModal').addEventListener('click', () => {
529
+ document.getElementById('createDocModal').classList.add('hidden');
530
+ });
531
+
532
+ document.getElementById('createDocForm').addEventListener('submit', async (e) => {
533
+ e.preventDefault();
534
+ const formData = new FormData(e.target);
535
+ try {
536
+ await apiService.createDocument(
537
+ formData.get('title'),
538
+ formData.get('content'),
539
+ currentGroup._id,
540
+ formData.get('permission')
541
+ );
542
+ alert('文档创建成功!');
543
+ await renderDocumentsView(container);
544
+ document.getElementById('createDocModal').classList.add('hidden');
545
+ } catch (error) {
546
+ console.error('创建文档错误:', error);
547
+ alert('创建失败: ' + error.message);
548
+ }
549
+ });
550
+ }
551
+
552
552
  async function renderDocumentEditor(container, documentId) {
553
553
  const result = await apiService.getDocument(documentId);
554
554
  const doc = result.document;
555
555
 
556
556
  container.innerHTML = `
557
557
  <div class="view-header">
558
- <button class="btn-back" id="backBtn">鈫?杩斿洖</button>
558
+ <button class="btn-back" id="backBtn">← 返回</button>
559
559
  <h2>${doc.title}</h2>
560
- <span class="doc-status">${doc.permission === 'readonly' ? '馃敀 鍙妯″紡' : '鉁忥笍 缂栬緫妯″紡'}</span>
560
+ <span class="doc-status">${doc.permission === 'readonly' ? '🔒 只读模式' : '✏️ 编辑模式'}</span>
561
561
  </div>
562
562
  <div class="editor-container">
563
563
  <div class="editor-toolbar">
564
564
  <div class="online-users" id="onlineUsers">
565
- <span class="user-badge">馃懁 ${user.username}</span>
565
+ <span class="user-badge">👤 ${user.username}</span>
566
566
  </div>
567
- <button class="btn-primary" id="saveBtn">淇濆瓨</button>
567
+ <button class="btn-primary" id="saveBtn">保存</button>
568
568
  </div>
569
- <textarea id="editor" style="width: 100%; min-height: 500px; padding: 20px; font-size: 16px; border: 1px solid var(--border); border-radius: 8px; resize: vertical;">${doc.content || ''}</textarea>
569
+ <textarea id="editor" style="width: 100%; min-height: 400px; padding: 10px; font-family: monospace;">${doc.content || ''}</textarea>
570
570
  <div class="editor-footer">
571
- <span>鏈€鍚庣紪杈? ${new Date(doc.updatedAt).toLocaleString()}</span>
571
+ <span>最后编辑: ${new Date(doc.updatedAt).toLocaleString()}</span>
572
572
  </div>
573
573
  </div>
574
574
  `;
575
575
 
576
576
  const editor = document.getElementById('editor');
577
577
 
578
- // 瀹炴椂鍚屾
578
+ // 实时同步
579
579
  let typingTimeout;
580
580
  let saveTimeout;
581
581
 
@@ -588,13 +588,13 @@ export function renderAdminDashboard(user, wsService) {
588
588
  wsService.sendTyping(documentId, user.username, false);
589
589
  }, 1000);
590
590
 
591
- // 鑷姩淇濆瓨
591
+ // 自动保存
592
592
  saveTimeout = setTimeout(async () => {
593
593
  const content = editor.value;
594
594
  try {
595
595
  await apiService.updateDocument(documentId, content);
596
596
  } catch (error) {
597
- console.error('鑷姩淇濆瓨澶辫触:', error);
597
+ console.error('自动保存失败:', error);
598
598
  }
599
599
  }, 2000);
600
600
  });
@@ -603,13 +603,13 @@ export function renderAdminDashboard(user, wsService) {
603
603
  try {
604
604
  const content = editor.value;
605
605
  await apiService.updateDocument(documentId, content);
606
- alert('淇濆瓨鎴愬姛锛?);
606
+ alert('保存成功!');
607
607
  } catch (error) {
608
- alert('淇濆瓨澶辫触: ' + error.message);
608
+ alert('保存失败: ' + error.message);
609
609
  }
610
610
  });
611
611
 
612
- // 鐩戝惉鏂囨。鏇存柊
612
+ // 监听文档更新
613
613
  wsService.on('document_update', (data) => {
614
614
  if (data.documentId === documentId && data.userId !== user.id) {
615
615
  const cursorPos = editor.selectionStart;
@@ -618,11 +618,12 @@ export function renderAdminDashboard(user, wsService) {
618
618
  }
619
619
  });
620
620
 
621
- // 鐩戝惉鎵撳瓧鐘舵€? wsService.on('typing', (data) => {
621
+ // 监听打字状态
622
+ wsService.on('typing', (data) => {
622
623
  if (data.documentId === documentId && data.userId !== user.id) {
623
624
  const onlineUsers = document.getElementById('onlineUsers');
624
625
  if (data.isTyping) {
625
- onlineUsers.innerHTML += `<span class="user-badge typing" data-user="${data.userId}">鉁忥笍 ${data.username}</span>`;
626
+ onlineUsers.innerHTML += `<span class="user-badge typing" data-user="${data.userId}">✏️ ${data.username}</span>`;
626
627
  } else {
627
628
  const badge = onlineUsers.querySelector(`[data-user="${data.userId}"]`);
628
629
  if (badge) badge.remove();
@@ -633,841 +634,840 @@ export function renderAdminDashboard(user, wsService) {
633
634
  document.getElementById('backBtn').addEventListener('click', () => {
634
635
  renderDocumentsView(container);
635
636
  });
636
- }
637
-
638
- async function renderFilesView(container) {
639
- if (!currentGroup) {
640
- container.innerHTML = '<div class="empty-state">璇峰厛閫夋嫨涓€涓兢缁?/div>';
641
- return;
642
- }
643
-
644
- try {
645
- const result = await apiService.getGroupFiles(currentGroup._id);
646
-
647
- container.innerHTML = `
648
- <div class="view-header">
649
- <h2>鏂囦欢绠$悊 - ${currentGroup.name}</h2>
650
- <button class="btn-primary" id="uploadFileBtn">馃摛 涓婁紶鏂囦欢</button>
651
- </div>
652
- <div class="files-list" id="filesList"></div>
653
-
654
- <!-- 鏂囦欢涓婁紶妯℃€佹 -->
655
- <div class="modal hidden" id="uploadFileModal">
656
- <div class="modal-content">
657
- <div class="modal-header">
658
- <h3>涓婁紶鏂囦欢</h3>
659
- <button class="modal-close" id="closeUploadModal">&times;</button>
660
- </div>
661
- <form id="uploadFileForm">
662
- <div class="form-group">
663
- <label>閫夋嫨鏂囦欢</label>
664
- <input type="file" id="fileInput" required>
665
- <small>鏀寔鍥剧墖銆丳DF銆乄ord銆丒xcel绛夛紝鏈€澶?0MB</small>
666
- </div>
667
- <div class="form-group">
668
- <label>鎻忚堪锛堝彲閫夛級</label>
669
- <textarea id="fileDescription" rows="3" placeholder="鏂囦欢鎻忚堪..."></textarea>
670
- </div>
671
- <div class="form-actions">
672
- <button type="button" class="btn-secondary" id="cancelUpload">鍙栨秷</button>
673
- <button type="submit" class="btn-primary">涓婁紶</button>
674
- </div>
675
- </form>
676
- </div>
677
- </div>
678
- `;
679
-
680
- const filesList = document.getElementById('filesList');
681
-
682
- if (!result.files || result.files.length === 0) {
683
- filesList.innerHTML = '<div class="empty-state">鏆傛棤鏂囦欢</div>';
684
- } else {
685
- result.files.forEach(file => {
686
- const fileCard = document.createElement('div');
687
- fileCard.className = 'file-card';
688
-
689
- const fileIcon = getFileIcon(file.mimetype);
690
- const fileSize = formatFileSize(file.size);
691
-
692
- fileCard.innerHTML = `
693
- <div class="file-icon">${fileIcon}</div>
694
- <div class="file-info">
695
- <h4>${file.originalName}</h4>
696
- <div class="file-meta">
697
- <span>涓婁紶鑰? ${file.uploader.username}</span>
698
- <span>澶у皬: ${fileSize}</span>
699
- <span>鏃堕棿: ${new Date(file.createdAt).toLocaleString()}</span>
700
- </div>
701
- ${file.description ? `<p class="file-description">${file.description}</p>` : ''}
702
- </div>
703
- <div class="file-actions">
704
- <a href="${apiService.getFileDownloadUrl(file._id)}" class="btn-primary" download>涓嬭浇</a>
705
- <button class="btn-danger" data-id="${file._id}" data-action="delete-file">鍒犻櫎</button>
706
- </div>
707
- `;
708
- filesList.appendChild(fileCard);
709
- });
710
-
711
- // 鍒犻櫎鏂囦欢浜嬩欢
712
- document.querySelectorAll('[data-action="delete-file"]').forEach(btn => {
713
- btn.addEventListener('click', async () => {
714
- if (confirm('纭畾瑕佸垹闄よ繖涓枃浠跺悧锛?)) {
715
- try {
716
- await apiService.deleteFile(btn.dataset.id);
717
- alert('鏂囦欢鍒犻櫎鎴愬姛锛?);
718
- await renderFilesView(container);
719
- } catch (error) {
720
- alert('鍒犻櫎澶辫触: ' + error.message);
721
- }
722
- }
723
- });
724
- });
725
- }
726
-
727
- // 鏂囦欢涓婁紶鍔熻兘
728
- document.getElementById('uploadFileBtn').addEventListener('click', () => {
729
- document.getElementById('uploadFileModal').classList.remove('hidden');
730
- });
731
-
732
- document.getElementById('closeUploadModal').addEventListener('click', () => {
733
- document.getElementById('uploadFileModal').classList.add('hidden');
734
- document.getElementById('uploadFileForm').reset();
735
- });
736
-
737
- document.getElementById('cancelUpload').addEventListener('click', () => {
738
- document.getElementById('uploadFileModal').classList.add('hidden');
739
- document.getElementById('uploadFileForm').reset();
740
- });
741
-
742
- document.getElementById('uploadFileForm').addEventListener('submit', async (e) => {
743
- e.preventDefault();
744
- const fileInput = document.getElementById('fileInput');
745
- const description = document.getElementById('fileDescription').value;
746
-
747
- if (!fileInput.files[0]) {
748
- alert('璇烽€夋嫨鏂囦欢');
749
- return;
750
- }
751
-
752
- try {
753
- await apiService.uploadFile(currentGroup._id, fileInput.files[0], description);
754
- alert('鏂囦欢涓婁紶鎴愬姛锛?);
755
- document.getElementById('uploadFileModal').classList.add('hidden');
756
- document.getElementById('uploadFileForm').reset();
757
- await renderFilesView(container);
758
- } catch (error) {
759
- alert('涓婁紶澶辫触: ' + error.message);
760
- }
761
- });
762
- } catch (error) {
763
- console.error('鑾峰彇鏂囦欢鍒楄〃澶辫触:', error);
764
- container.innerHTML = `
765
- <div class="view-header">
766
- <h2>鏂囦欢绠$悊</h2>
767
- </div>
768
- <div class="empty-state">鍔犺浇鏂囦欢澶辫触: ${error.message}</div>
769
- `;
770
- }
771
- }
772
-
773
- function getFileIcon(mimetype) {
774
- if (mimetype.startsWith('image/')) return '馃柤锔?;
775
- if (mimetype === 'application/pdf') return '馃摃';
776
- if (mimetype.includes('word') || mimetype.includes('document')) return '馃摌';
777
- if (mimetype.includes('excel') || mimetype.includes('spreadsheet')) return '馃摋';
778
- if (mimetype.includes('zip') || mimetype.includes('compressed')) return '馃摝';
779
- return '馃搫';
780
- }
781
-
782
- function formatFileSize(bytes) {
783
- if (bytes === 0) return '0 Bytes';
784
- const k = 1024;
785
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
786
- const i = Math.floor(Math.log(bytes) / Math.log(k));
787
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
788
- }
789
-
790
- async function renderChatView(container) {
791
- if (!currentGroup) {
792
- container.innerHTML = '<div class="empty-state">璇峰厛閫夋嫨涓€涓兢缁?/div>';
793
- return;
794
- }
795
-
796
- const groupResult = await apiService.getGroup(currentGroup._id);
797
- let group = groupResult.group;
798
-
799
- container.innerHTML = `
800
- <div class="view-header">
801
- <h2>缇よ亰 - ${currentGroup.name}</h2>
802
- <div style="display: flex; gap: 10px;">
803
- <button class="btn-secondary" id="muteAllBtn">鍏ㄤ綋绂佽█</button>
804
- <button class="btn-secondary" id="manageMuteBtn">涓汉绂佽█</button>
805
- </div>
806
- </div>
807
- <div class="chat-container">
808
- <div class="messages" id="messages"></div>
809
- <div class="chat-input">
810
- <button class="btn-emoji" id="emojiBtn">馃槉</button>
811
- <input type="text" id="messageInput" placeholder="杈撳叆娑堟伅...">
812
- <button class="btn-primary" id="sendBtn">鍙戦€?/button>
813
- </div>
814
- <emoji-picker id="emojiPicker" class="hidden"></emoji-picker>
815
- </div>
816
- <div id="manageMuteModal" class="modal hidden">
817
- <div class="modal-content">
818
- <h3>涓汉绂佽█</h3>
819
- <div id="membersList" style="max-height: 400px; overflow-y: auto;"></div>
820
- <button type="button" class="btn-secondary" id="closeMuteModal">鍏抽棴</button>
821
- </div>
822
- </div>
823
- `;
824
-
825
- const messagesDiv = document.getElementById('messages');
826
- const messageInput = document.getElementById('messageInput');
827
- const sendBtn = document.getElementById('sendBtn');
828
- const emojiBtn = document.getElementById('emojiBtn');
829
- const emojiPicker = document.getElementById('emojiPicker');
830
- let mutedUsers = new Set((group.mutedUsers || []).map(String));
831
- let isMutedAll = Boolean(group.mutedAll);
832
-
833
- // 琛ㄦ儏鍖呭姛鑳?
834
- emojiBtn.addEventListener('click', () => {
835
- emojiPicker.classList.toggle('hidden');
836
- });
837
-
838
- emojiPicker.addEventListener('emoji-click', (event) => {
839
- messageInput.value += event.detail.unicode;
840
- messageInput.focus();
841
- emojiPicker.classList.add('hidden');
842
- });
843
-
844
- // 鐐瑰嚮澶栭儴鍏抽棴琛ㄦ儏閫夋嫨鍣?
845
- document.addEventListener('click', (e) => {
846
- if (!emojiBtn.contains(e.target) && !emojiPicker.contains(e.target)) {
847
- emojiPicker.classList.add('hidden');
848
- }
849
- });
850
-
851
- // 娑堟伅閫氱煡绯荤粺
852
- function showNotification(title, body, icon = '馃挰') {
853
- if ('Notification' in window && Notification.permission === 'granted') {
854
- new Notification(title, {
855
- body: body,
856
- icon: '/icon.png',
857
- badge: '/icon.png',
858
- tag: 'chat-message'
859
- });
860
- }
861
- }
862
-
863
- // 璇锋眰閫氱煡鏉冮檺
864
- if ('Notification' in window && Notification.permission === 'default') {
865
- Notification.requestPermission();
866
- }
867
-
868
- // 鍔犺浇鍘嗗彶娑堟伅
869
- try {
870
- const messagesResult = await apiService.getGroupMessages(currentGroup._id);
871
- if (messagesResult.messages) {
872
- messagesResult.messages.forEach(msg => {
873
- const messageEl = document.createElement('div');
874
- messageEl.className = `message ${msg.sender === currentUserId ? 'own' : ''}`;
875
- messageEl.innerHTML = `
876
- <div class="message-header">
877
- <span class="message-user">${msg.username}</span>
878
- <span class="message-time">${new Date(msg.timestamp).toLocaleTimeString()}</span>
879
- </div>
880
- <div class="message-content">${msg.content}</div>
881
- `;
882
- messagesDiv.appendChild(messageEl);
883
- });
884
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
885
- }
886
- } catch (err) {
887
- console.error('鍔犺浇鍘嗗彶娑堟伅澶辫触:', err);
888
- }
889
-
890
- const refreshMuteButtons = () => {
891
- const btn = document.getElementById('muteAllBtn');
892
- btn.textContent = isMutedAll ? '鍙栨秷鍏ㄤ綋绂佽█' : '鍏ㄤ綋绂佽█';
893
- btn.style.background = isMutedAll ? 'var(--danger)' : '';
894
- };
895
- refreshMuteButtons();
896
-
897
- // 鍏ㄤ綋绂佽█锛堟湇鍔$鐢熸晥锛?
898
- document.getElementById('muteAllBtn').addEventListener('click', async () => {
899
- try {
900
- const next = !isMutedAll;
901
- const res = await apiService.setMuteAll(currentGroup._id, next);
902
- isMutedAll = Boolean(res.mutedAll);
903
- refreshMuteButtons();
904
-
905
- const notification = document.createElement('div');
906
- notification.className = 'notification';
907
- notification.textContent = isMutedAll ? '宸插紑鍚叏浣撶瑷€锛堟垚鍛樻棤娉曞彂瑷€锛? : '宸插彇娑堝叏浣撶瑷€';
908
- messagesDiv.appendChild(notification);
909
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
910
- } catch (e) {
911
- alert('璁剧疆澶辫触: ' + e.message);
912
- }
913
- });
914
-
915
- // 涓汉绂佽█锛堟湇鍔$鐢熸晥锛?
916
- document.getElementById('manageMuteBtn').addEventListener('click', async () => {
917
- // 閲嶆柊鎷夊彇鏈€鏂?group锛堥伩鍏嶆垚鍛樺彉鍔ㄤ笉鍚屾锛?
918
- const latest = await apiService.getGroup(currentGroup._id);
919
- group = latest.group;
920
- mutedUsers = new Set((group.mutedUsers || []).map(String));
921
-
922
- const membersList = document.getElementById('membersList');
923
- membersList.innerHTML = group.members
924
- .filter(m => m._id.toString() !== currentUserId)
925
- .map(member => {
926
- const isMuted = mutedUsers.has(member._id.toString());
927
- return `
928
- <div style="display: flex; align-items: center; justify-content: space-between; padding: 12px; border-bottom: 1px solid var(--border);">
929
- <div style="display: flex; align-items: center; gap: 10px;">
930
- <div class="avatar" style="width: 35px; height: 35px;">${member.username[0].toUpperCase()}</div>
931
- <span>${member.username}</span>
932
- </div>
933
- <button class="btn-secondary btn-sm" onclick="toggleMute('${member._id}')" id="mute-${member._id}">
934
- ${isMuted ? '鍙栨秷绂佽█' : '绂佽█'}
935
- </button>
936
- </div>
937
- `;
938
- }).join('');
939
- document.getElementById('manageMuteModal').classList.remove('hidden');
940
- });
941
-
942
- document.getElementById('closeMuteModal').addEventListener('click', () => {
943
- document.getElementById('manageMuteModal').classList.add('hidden');
944
- });
945
-
946
- // 鍒囨崲涓汉绂佽█鐘舵€侊紙鏈嶅姟绔敓鏁堬級
947
- window.toggleMute = async (userId) => {
948
- try {
949
- const nextMuted = !mutedUsers.has(userId);
950
- const res = await apiService.setUserMute(currentGroup._id, userId, nextMuted);
951
- mutedUsers = new Set((res.mutedUsers || []).map(String));
952
-
953
- const btn = document.getElementById(`mute-${userId}`);
954
- btn.textContent = mutedUsers.has(userId) ? '鍙栨秷绂佽█' : '绂佽█';
955
- btn.style.background = mutedUsers.has(userId) ? 'var(--danger)' : '';
956
- } catch (e) {
957
- alert('鎿嶄綔澶辫触: ' + e.message);
958
- }
959
- };
960
-
961
- // 鐩戝惉娑堟伅
962
- wsService.on('chat_message', (data) => {
963
- if (data.groupId === currentGroup._id) {
964
- const messageEl = document.createElement('div');
965
- messageEl.className = `message ${data.userId === currentUserId ? 'own' : ''}`;
966
- messageEl.innerHTML = `
967
- <div class="message-header">
968
- <span class="message-user">${data.username}</span>
969
- <span class="message-time">${new Date(data.timestamp).toLocaleTimeString()}</span>
970
- </div>
971
- <div class="message-content">${data.content}</div>
972
- `;
973
- messagesDiv.appendChild(messageEl);
974
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
975
- }
976
- });
977
-
978
- // 鍙戦€佽鎷︽埅鎻愮ず锛堟潵鑷湇鍔$锛?
979
- wsService.on('chat_blocked', (data) => {
980
- if (data.groupId === currentGroup._id) {
981
- const notification = document.createElement('div');
982
- notification.className = 'notification';
983
- notification.textContent = data.message || '娑堟伅鍙戦€佸け璐?;
984
- messagesDiv.appendChild(notification);
985
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
986
- }
987
- });
988
-
989
- // 鍙戦€佹秷鎭?
990
- const sendMessage = () => {
991
- const content = messageInput.value.trim();
992
- if (content) {
993
- wsService.sendChatMessage(currentGroup._id, user.username, content);
994
- messageInput.value = '';
995
- }
996
- };
997
-
998
- sendBtn.addEventListener('click', sendMessage);
999
- messageInput.addEventListener('keypress', (e) => {
1000
- if (e.key === 'Enter') sendMessage();
1001
- });
1002
- }
1003
-
1004
- async function renderCallView(container) {
1005
- if (!currentGroup) {
1006
- container.innerHTML = '<div class="empty-state">璇峰厛閫夋嫨涓€涓兢缁?/div>';
1007
- return;
1008
- }
1009
-
1010
- container.innerHTML = `
1011
- <div class="view-header">
1012
- <h2>闅忔満鐐瑰悕 - ${currentGroup.name}</h2>
1013
- </div>
1014
- <div class="call-panel">
1015
- <div class="call-controls">
1016
- <label>鐐瑰悕浜烘暟:</label>
1017
- <input type="number" id="callCount" value="1" min="1" max="10">
1018
- <button class="btn-primary btn-large" id="randomCallBtn">馃幉 寮€濮嬬偣鍚?/button>
1019
- </div>
1020
- <div id="callResult" class="call-result"></div>
1021
- </div>
1022
- `;
1023
-
1024
- document.getElementById('randomCallBtn').addEventListener('click', async () => {
1025
- const count = parseInt(document.getElementById('callCount').value);
1026
- try {
1027
- const result = await apiService.randomCall(currentGroup._id, count);
1028
- const callResult = document.getElementById('callResult');
1029
- callResult.innerHTML = `
1030
- <h3>鐐瑰悕缁撴灉:</h3>
1031
- <div class="called-members">
1032
- ${result.calledMembers.map(member => `
1033
- <div class="member-card">
1034
- <div class="avatar">${member.username[0].toUpperCase()}</div>
1035
- <div class="member-name">${member.username}</div>
1036
- </div>
1037
- `).join('')}
1038
- </div>
1039
- `;
1040
- } catch (error) {
1041
- alert('鐐瑰悕澶辫触: ' + error.message);
1042
- }
1043
- });
1044
- }
1045
-
1046
- async function renderAuditView(container) {
1047
- container.innerHTML = `
1048
- <div class="view-header">
1049
- <h2>鎿嶄綔璁板綍</h2>
1050
- <div style="display: flex; gap: 10px;">
1051
- <select id="auditGroupFilter" class="form-select">
1052
- <option value="">鍏ㄩ儴缇ょ粍</option>
1053
- </select>
1054
- <select id="auditActionFilter" class="form-select">
1055
- <option value="">鍏ㄩ儴鎿嶄綔</option>
1056
- <option value="document_create">鏂囨。鍒涘缓</option>
1057
- <option value="document_update">鏂囨。鏇存柊</option>
1058
- <option value="document_delete">鏂囨。鍒犻櫎</option>
1059
- <option value="content_edit">鍐呭缂栬緫</option>
1060
- <option value="title_edit">鏍囬缂栬緫</option>
1061
- <option value="document_permission_change">鏉冮檺淇敼</option>
1062
- </select>
1063
- <input type="date" id="startDate" class="form-input" title="寮€濮嬫棩鏈?>
1064
- <input type="date" id="endDate" class="form-input" title="缁撴潫鏃ユ湡">
1065
- <button class="btn-primary" id="applyFilters">绛涢€?/button>
1066
- <button class="btn-secondary" id="exportLogs">瀵煎嚭</button>
1067
- </div>
1068
- </div>
1069
-
1070
- <div class="audit-stats" id="auditStats">
1071
- <div class="stat-card">
1072
- <h3>浠婃棩鎿嶄綔</h3>
1073
- <div class="stat-number" id="todayCount">-</div>
1074
- </div>
1075
- <div class="stat-card">
1076
- <h3>鏈懆鎿嶄綔</h3>
1077
- <div class="stat-number" id="weekCount">-</div>
1078
- </div>
1079
- <div class="stat-card">
1080
- <h3>娲昏穬鐢ㄦ埛</h3>
1081
- <div class="stat-number" id="activeUsers">-</div>
1082
- </div>
1083
- </div>
1084
-
1085
- <div class="audit-logs" id="auditLogs">
1086
- <div class="loading">鍔犺浇涓?..</div>
1087
- </div>
1088
-
1089
- <div class="pagination" id="auditPagination" style="display: none;">
1090
- <button class="btn-secondary" id="prevPage">涓婁竴椤?/button>
1091
- <span id="pageInfo">绗?1 椤碉紝鍏?1 椤?/span>
1092
- <button class="btn-secondary" id="nextPage">涓嬩竴椤?/button>
1093
- </div>
1094
-
1095
- <div id="auditDetailModal" class="modal hidden">
1096
- <div class="modal-content" style="max-width: 800px;">
1097
- <div class="modal-header">
1098
- <h3>鎿嶄綔璇︽儏</h3>
1099
- <button class="close-btn" id="closeAuditDetail">&times;</button>
1100
- </div>
1101
- <div class="modal-body" id="auditDetailContent">
1102
- </div>
1103
- </div>
1104
- </div>
1105
- `;
1106
-
1107
- let currentPage = 1;
1108
- let currentFilters = {};
1109
-
1110
- // 鍔犺浇缇ょ粍鍒楄〃鍒扮瓫閫夊櫒
1111
- try {
1112
- const groupsResult = await apiService.getGroups();
1113
- const groupFilter = document.getElementById('auditGroupFilter');
1114
- groupsResult.groups.forEach(group => {
1115
- const option = document.createElement('option');
1116
- option.value = group._id;
1117
- option.textContent = group.name;
1118
- groupFilter.appendChild(option);
1119
- });
1120
- } catch (error) {
1121
- console.error('鍔犺浇缇ょ粍鍒楄〃澶辫触:', error);
1122
- }
1123
-
1124
- async function loadAuditLogs(page = 1, filters = {}) {
1125
- try {
1126
- const auditLogsDiv = document.getElementById('auditLogs');
1127
- auditLogsDiv.innerHTML = '<div class="loading">鍔犺浇涓?..</div>';
1128
-
1129
- const options = { page, limit: 20 };
1130
- const result = await apiService.getAuditLogs(filters, options);
1131
-
1132
- if (result.logs.length === 0) {
1133
- auditLogsDiv.innerHTML = '<div class="empty-state">鏆傛棤鎿嶄綔璁板綍</div>';
1134
- document.getElementById('auditPagination').style.display = 'none';
1135
- return;
1136
- }
1137
-
1138
- auditLogsDiv.innerHTML = `
1139
- <div class="audit-table">
1140
- <div class="audit-header">
1141
- <div>鏃堕棿</div>
1142
- <div>鐢ㄦ埛</div>
1143
- <div>鎿嶄綔</div>
1144
- <div>璧勬簮</div>
1145
- <div>璇︽儏</div>
1146
- </div>
1147
- ${result.logs.map(log => `
1148
- <div class="audit-row" onclick="showAuditDetail('${log._id}')">
1149
- <div class="audit-time">${new Date(log.createdAt).toLocaleString()}</div>
1150
- <div class="audit-user">
1151
- <div class="avatar">${log.user?.username?.[0]?.toUpperCase() || '?'}</div>
1152
- <span>${log.user?.username || '鏈煡鐢ㄦ埛'}</span>
1153
- </div>
1154
- <div class="audit-action">
1155
- <span class="action-badge action-${log.action}">${getActionText(log.action)}</span>
1156
- </div>
1157
- <div class="audit-resource">${log.resourceTitle || log.resourceId}</div>
1158
- <div class="audit-description">${log.details?.description || '-'}</div>
1159
- </div>
1160
- `).join('')}
1161
- </div>
1162
- `;
1163
-
1164
- // 鏇存柊鍒嗛〉
1165
- const pagination = document.getElementById('auditPagination');
1166
- const pageInfo = document.getElementById('pageInfo');
1167
- pageInfo.textContent = `绗?${result.pagination.page} 椤碉紝鍏?${result.pagination.pages} 椤礰;
1168
-
1169
- document.getElementById('prevPage').disabled = result.pagination.page <= 1;
1170
- document.getElementById('nextPage').disabled = result.pagination.page >= result.pagination.pages;
1171
-
1172
- pagination.style.display = result.pagination.pages > 1 ? 'flex' : 'none';
1173
-
1174
- } catch (error) {
1175
- console.error('鍔犺浇瀹¤鏃ュ織澶辫触:', error);
1176
- document.getElementById('auditLogs').innerHTML =
1177
- '<div class="error-state">鍔犺浇澶辫触: ' + error.message + '</div>';
1178
- }
1179
- }
1180
-
1181
- async function loadStats() {
1182
- try {
1183
- const today = new Date();
1184
- const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
1185
-
1186
- // 鑾峰彇浠婃棩缁熻
1187
- const todayStats = await apiService.getAuditSummary({
1188
- startDate: today.toISOString().split('T')[0],
1189
- endDate: today.toISOString().split('T')[0]
1190
- });
1191
-
1192
- // 鑾峰彇鏈懆缁熻
1193
- const weekStats = await apiService.getAuditSummary({
1194
- startDate: weekAgo.toISOString().split('T')[0],
1195
- endDate: today.toISOString().split('T')[0]
1196
- });
1197
-
1198
- document.getElementById('todayCount').textContent = todayStats.summary.totalLogs;
1199
- document.getElementById('weekCount').textContent = weekStats.summary.totalLogs;
1200
- document.getElementById('activeUsers').textContent = weekStats.summary.topUsers.length;
1201
-
1202
- } catch (error) {
1203
- console.error('鍔犺浇缁熻淇℃伅澶辫触:', error);
1204
- }
1205
- }
1206
-
1207
- // 鏄剧ず鎿嶄綔璇︽儏
1208
- window.showAuditDetail = async (logId) => {
1209
- try {
1210
- // 杩欓噷鎴戜滑闇€瑕佷粠宸插姞杞界殑鏃ュ織涓壘鍒板搴旂殑璁板綍
1211
- // 鍦ㄥ疄闄呭簲鐢ㄤ腑鍙兘闇€瑕佸崟鐙殑API鏉ヨ幏鍙栬鎯?
1212
- const modal = document.getElementById('auditDetailModal');
1213
- const content = document.getElementById('auditDetailContent');
1214
-
1215
- // 绠€鍗曞疄鐜帮細鏄剧ず鍩烘湰淇℃伅
1216
- content.innerHTML = `
1217
- <div class="audit-detail">
1218
- <p>鎿嶄綔ID: ${logId}</p>
1219
- <p>璇︾粏淇℃伅鍔犺浇涓?..</p>
1220
- </div>
1221
- `;
1222
-
1223
- modal.classList.remove('hidden');
1224
- } catch (error) {
1225
- alert('鍔犺浇璇︽儏澶辫触: ' + error.message);
1226
- }
1227
- };
1228
-
1229
- // 浜嬩欢鐩戝惉
1230
- document.getElementById('applyFilters').addEventListener('click', () => {
1231
- currentFilters = {
1232
- groupId: document.getElementById('auditGroupFilter').value,
1233
- action: document.getElementById('auditActionFilter').value,
1234
- startDate: document.getElementById('startDate').value,
1235
- endDate: document.getElementById('endDate').value
1236
- };
1237
-
1238
- // 绉婚櫎绌哄€?
1239
- Object.keys(currentFilters).forEach(key => {
1240
- if (!currentFilters[key]) {
1241
- delete currentFilters[key];
1242
- }
1243
- });
1244
-
1245
- currentPage = 1;
1246
- loadAuditLogs(currentPage, currentFilters);
1247
- });
1248
-
1249
- document.getElementById('prevPage').addEventListener('click', () => {
1250
- if (currentPage > 1) {
1251
- currentPage--;
1252
- loadAuditLogs(currentPage, currentFilters);
1253
- }
1254
- });
1255
-
1256
- document.getElementById('nextPage').addEventListener('click', () => {
1257
- currentPage++;
1258
- loadAuditLogs(currentPage, currentFilters);
1259
- });
1260
-
1261
- document.getElementById('exportLogs').addEventListener('click', () => {
1262
- alert('瀵煎嚭鍔熻兘寮€鍙戜腑...');
1263
- });
1264
-
1265
- document.getElementById('closeAuditDetail').addEventListener('click', () => {
1266
- document.getElementById('auditDetailModal').classList.add('hidden');
1267
- });
1268
-
1269
- // 鍒濆鍔犺浇
1270
- loadStats();
1271
- loadAuditLogs();
1272
- }
1273
-
1274
- async function renderSearchView(container) {
1275
- container.innerHTML = `
1276
- <div class="view-header">
1277
- <h2>馃攳 鎼滅储</h2>
1278
- </div>
1279
- <div class="search-container">
1280
- <div class="search-box">
1281
- <input type="text" id="searchInput" placeholder="鎼滅储娑堟伅銆佹枃妗c€佷换鍔?..">
1282
- <button class="btn-primary" id="searchBtn">鎼滅储</button>
1283
- </div>
1284
- <div class="search-filters">
1285
- <label>
1286
- <input type="checkbox" id="filterMessages" checked> 娑堟伅
1287
- </label>
1288
- <label>
1289
- <input type="checkbox" id="filterDocuments" checked> 鏂囨。
1290
- </label>
1291
- <label>
1292
- <input type="checkbox" id="filterTasks" checked> 浠诲姟
1293
- </label>
1294
- </div>
1295
- <div class="search-results" id="searchResults"></div>
1296
- </div>
1297
- `;
1298
-
1299
- const searchInput = document.getElementById('searchInput');
1300
- const searchBtn = document.getElementById('searchBtn');
1301
- const searchResults = document.getElementById('searchResults');
1302
-
1303
- const performSearch = async () => {
1304
- const query = searchInput.value.trim();
1305
- if (!query) {
1306
- searchResults.innerHTML = '<div class="empty-state">璇疯緭鍏ユ悳绱㈠叧閿瘝</div>';
1307
- return;
1308
- }
1309
-
1310
- const filters = {
1311
- messages: document.getElementById('filterMessages').checked,
1312
- documents: document.getElementById('filterDocuments').checked,
1313
- tasks: document.getElementById('filterTasks').checked
1314
- };
1315
-
1316
- searchResults.innerHTML = '<div class="loading">鎼滅储涓?..</div>';
1317
-
1318
- try {
1319
- const results = [];
1320
-
1321
- // 鎼滅储娑堟伅
1322
- if (filters.messages && currentGroup) {
1323
- try {
1324
- const messagesResult = await apiService.getGroupMessages(currentGroup._id);
1325
- if (messagesResult.messages) {
1326
- const matchedMessages = messagesResult.messages.filter(msg =>
1327
- msg.content.toLowerCase().includes(query.toLowerCase())
1328
- );
1329
- matchedMessages.forEach(msg => {
1330
- results.push({
1331
- type: 'message',
1332
- title: `娑堟伅 - ${msg.username}`,
1333
- content: msg.content,
1334
- time: msg.timestamp,
1335
- group: currentGroup.name
1336
- });
1337
- });
1338
- }
1339
- } catch (err) {
1340
- console.error('鎼滅储娑堟伅澶辫触:', err);
1341
- }
1342
- }
1343
-
1344
- // 鎼滅储鏂囨。
1345
- if (filters.documents) {
1346
- try {
1347
- if (currentGroup) {
1348
- const docsResult = await apiService.getDocuments(currentGroup._id);
1349
- if (docsResult.documents) {
1350
- const matchedDocs = docsResult.documents.filter(doc =>
1351
- doc.title.toLowerCase().includes(query.toLowerCase()) ||
1352
- doc.content.toLowerCase().includes(query.toLowerCase())
1353
- );
1354
- matchedDocs.forEach(doc => {
1355
- results.push({
1356
- type: 'document',
1357
- title: doc.title,
1358
- content: doc.content.substring(0, 200),
1359
- time: doc.updatedAt,
1360
- id: doc._id,
1361
- group: currentGroup.name
1362
- });
1363
- });
1364
- }
1365
- }
1366
- } catch (err) {
1367
- console.error('鎼滅储鏂囨。澶辫触:', err);
1368
- }
1369
- }
1370
-
1371
- // 鎼滅储浠诲姟
1372
- if (filters.tasks && currentGroup) {
1373
- try {
1374
- const tasksResult = await apiService.getTasks(currentGroup._id);
1375
- if (tasksResult.tasks) {
1376
- const matchedTasks = tasksResult.tasks.filter(task =>
1377
- task.title.toLowerCase().includes(query.toLowerCase()) ||
1378
- (task.description && task.description.toLowerCase().includes(query.toLowerCase()))
1379
- );
1380
- matchedTasks.forEach(task => {
1381
- results.push({
1382
- type: 'task',
1383
- title: task.title,
1384
- content: task.description || '',
1385
- time: task.updatedAt,
1386
- id: task._id,
1387
- status: task.status,
1388
- group: currentGroup.name
1389
- });
1390
- });
1391
- }
1392
- } catch (err) {
1393
- console.error('鎼滅储浠诲姟澶辫触:', err);
1394
- }
1395
- }
1396
-
1397
- // 鏄剧ず缁撴灉
1398
- if (results.length === 0) {
1399
- searchResults.innerHTML = '<div class="empty-state">鏈壘鍒扮浉鍏崇粨鏋?/div>';
1400
- } else {
1401
- searchResults.innerHTML = results.map(result => {
1402
- const typeIcon = {
1403
- message: '馃挰',
1404
- document: '馃搫',
1405
- task: '馃搵'
1406
- };
1407
- return `
1408
- <div class="search-result-item">
1409
- <div class="result-header">
1410
- <span class="result-type">${typeIcon[result.type]} ${result.type === 'message' ? '娑堟伅' : result.type === 'document' ? '鏂囨。' : '浠诲姟'}</span>
1411
- <span class="result-time">${new Date(result.time).toLocaleString()}</span>
1412
- </div>
1413
- <h4>${highlightText(result.title, query)}</h4>
1414
- <p>${highlightText(result.content, query)}</p>
1415
- ${result.group ? `<span class="result-group">缇ょ粍: ${result.group}</span>` : ''}
1416
- ${result.status ? `<span class="result-status">鐘舵€? ${getStatusText(result.status)}</span>` : ''}
1417
- </div>
1418
- `;
1419
- }).join('');
1420
- }
1421
- } catch (error) {
1422
- searchResults.innerHTML = `<div class="empty-state">鎼滅储澶辫触: ${error.message}</div>`;
1423
- }
1424
- };
1425
-
1426
- searchBtn.addEventListener('click', performSearch);
1427
- searchInput.addEventListener('keypress', (e) => {
1428
- if (e.key === 'Enter') performSearch();
1429
- });
1430
- }
1431
-
1432
- function highlightText(text, query) {
1433
- if (!query) return text;
1434
- const regex = new RegExp(`(${query})`, 'gi');
1435
- return text.replace(regex, '<mark>$1</mark>');
1436
- }
1437
-
1438
- function getStatusText(status) {
1439
- const statusMap = {
1440
- 'pending': '寰呭鐞?,
1441
- 'in_progress': '杩涜涓?,
1442
- 'completed': '宸插畬鎴?,
1443
- 'terminated': '宸茬粓姝?
1444
- };
1445
- return statusMap[status] || status;
1446
- }
1447
-
1448
- function getActionText(action) {
1449
- const actionMap = {
1450
- 'document_create': '鍒涘缓鏂囨。',
1451
- 'document_update': '鏇存柊鏂囨。',
1452
- 'document_delete': '鍒犻櫎鏂囨。',
1453
- 'content_edit': '缂栬緫鍐呭',
1454
- 'title_edit': '淇敼鏍囬',
1455
- 'document_permission_change': '鏉冮檺淇敼'
1456
- };
1457
- return actionMap[action] || action;
1458
- }
1459
-
1460
- function getStatusText(status) {
1461
- const statusMap = {
1462
- 'pending': '寰呭鐞?,
1463
- 'in_progress': '杩涜涓?,
1464
- 'completed': '宸插畬鎴?,
1465
- 'terminated': '宸茬粓姝?
1466
- };
1467
- return statusMap[status] || status;
1468
- }
1469
-
1470
- renderView('groups');
1471
- }
1472
-
1473
-
637
+ }
638
+
639
+ async function renderFilesView(container) {
640
+ if (!currentGroup) {
641
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
642
+ return;
643
+ }
644
+
645
+ try {
646
+ const result = await apiService.getGroupFiles(currentGroup._id);
647
+
648
+ container.innerHTML = `
649
+ <div class="view-header">
650
+ <h2>文件管理 - ${currentGroup.name}</h2>
651
+ <button class="btn-primary" id="uploadFileBtn">📤 上传文件</button>
652
+ </div>
653
+ <div class="files-list" id="filesList"></div>
654
+
655
+ <!-- 文件上传模态框 -->
656
+ <div class="modal hidden" id="uploadFileModal">
657
+ <div class="modal-content">
658
+ <div class="modal-header">
659
+ <h3>上传文件</h3>
660
+ <button class="modal-close" id="closeUploadModal">&times;</button>
661
+ </div>
662
+ <form id="uploadFileForm">
663
+ <div class="form-group">
664
+ <label>选择文件</label>
665
+ <input type="file" id="fileInput" required>
666
+ <small>支持图片、PDF、Word、Excel等,最大10MB</small>
667
+ </div>
668
+ <div class="form-group">
669
+ <label>描述(可选)</label>
670
+ <textarea id="fileDescription" rows="3" placeholder="文件描述..."></textarea>
671
+ </div>
672
+ <div class="form-actions">
673
+ <button type="button" class="btn-secondary" id="cancelUpload">取消</button>
674
+ <button type="submit" class="btn-primary">上传</button>
675
+ </div>
676
+ </form>
677
+ </div>
678
+ </div>
679
+ `;
680
+
681
+ const filesList = document.getElementById('filesList');
682
+
683
+ if (!result.files || result.files.length === 0) {
684
+ filesList.innerHTML = '<div class="empty-state">暂无文件</div>';
685
+ } else {
686
+ result.files.forEach(file => {
687
+ const fileCard = document.createElement('div');
688
+ fileCard.className = 'file-card';
689
+
690
+ const fileIcon = getFileIcon(file.mimetype);
691
+ const fileSize = formatFileSize(file.size);
692
+
693
+ fileCard.innerHTML = `
694
+ <div class="file-icon">${fileIcon}</div>
695
+ <div class="file-info">
696
+ <h4>${file.originalName}</h4>
697
+ <div class="file-meta">
698
+ <span>上传者: ${file.uploader.username}</span>
699
+ <span>大小: ${fileSize}</span>
700
+ <span>时间: ${new Date(file.createdAt).toLocaleString()}</span>
701
+ </div>
702
+ ${file.description ? `<p class="file-description">${file.description}</p>` : ''}
703
+ </div>
704
+ <div class="file-actions">
705
+ <a href="${apiService.getFileDownloadUrl(file._id)}" class="btn-primary" download>下载</a>
706
+ <button class="btn-danger" data-id="${file._id}" data-action="delete-file">删除</button>
707
+ </div>
708
+ `;
709
+ filesList.appendChild(fileCard);
710
+ });
711
+
712
+ // 删除文件事件
713
+ document.querySelectorAll('[data-action="delete-file"]').forEach(btn => {
714
+ btn.addEventListener('click', async () => {
715
+ if (confirm('确定要删除这个文件吗?')) {
716
+ try {
717
+ await apiService.deleteFile(btn.dataset.id);
718
+ alert('文件删除成功!');
719
+ await renderFilesView(container);
720
+ } catch (error) {
721
+ alert('删除失败: ' + error.message);
722
+ }
723
+ }
724
+ });
725
+ });
726
+ }
727
+
728
+ // 文件上传功能
729
+ document.getElementById('uploadFileBtn').addEventListener('click', () => {
730
+ document.getElementById('uploadFileModal').classList.remove('hidden');
731
+ });
732
+
733
+ document.getElementById('closeUploadModal').addEventListener('click', () => {
734
+ document.getElementById('uploadFileModal').classList.add('hidden');
735
+ document.getElementById('uploadFileForm').reset();
736
+ });
737
+
738
+ document.getElementById('cancelUpload').addEventListener('click', () => {
739
+ document.getElementById('uploadFileModal').classList.add('hidden');
740
+ document.getElementById('uploadFileForm').reset();
741
+ });
742
+
743
+ document.getElementById('uploadFileForm').addEventListener('submit', async (e) => {
744
+ e.preventDefault();
745
+ const fileInput = document.getElementById('fileInput');
746
+ const description = document.getElementById('fileDescription').value;
747
+
748
+ if (!fileInput.files[0]) {
749
+ alert('请选择文件');
750
+ return;
751
+ }
752
+
753
+ try {
754
+ await apiService.uploadFile(currentGroup._id, fileInput.files[0], description);
755
+ alert('文件上传成功!');
756
+ document.getElementById('uploadFileModal').classList.add('hidden');
757
+ document.getElementById('uploadFileForm').reset();
758
+ await renderFilesView(container);
759
+ } catch (error) {
760
+ alert('上传失败: ' + error.message);
761
+ }
762
+ });
763
+ } catch (error) {
764
+ console.error('获取文件列表失败:', error);
765
+ container.innerHTML = `
766
+ <div class="view-header">
767
+ <h2>文件管理</h2>
768
+ </div>
769
+ <div class="empty-state">加载文件失败: ${error.message}</div>
770
+ `;
771
+ }
772
+ }
773
+
774
+ function getFileIcon(mimetype) {
775
+ if (mimetype.startsWith('image/')) return '🖼️';
776
+ if (mimetype === 'application/pdf') return '📕';
777
+ if (mimetype.includes('word') || mimetype.includes('document')) return '📘';
778
+ if (mimetype.includes('excel') || mimetype.includes('spreadsheet')) return '📗';
779
+ if (mimetype.includes('zip') || mimetype.includes('compressed')) return '📦';
780
+ return '📄';
781
+ }
782
+
783
+ function formatFileSize(bytes) {
784
+ if (bytes === 0) return '0 Bytes';
785
+ const k = 1024;
786
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
787
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
788
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
789
+ }
790
+
791
+ async function renderChatView(container) {
792
+ if (!currentGroup) {
793
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
794
+ return;
795
+ }
796
+
797
+ const groupResult = await apiService.getGroup(currentGroup._id);
798
+ let group = groupResult.group;
799
+
800
+ container.innerHTML = `
801
+ <div class="view-header">
802
+ <h2>群聊 - ${currentGroup.name}</h2>
803
+ <div style="display: flex; gap: 10px;">
804
+ <button class="btn-secondary" id="muteAllBtn">全体禁言</button>
805
+ <button class="btn-secondary" id="manageMuteBtn">个人禁言</button>
806
+ </div>
807
+ </div>
808
+ <div class="chat-container">
809
+ <div class="messages" id="messages"></div>
810
+ <div class="chat-input">
811
+ <button class="btn-emoji" id="emojiBtn">😊</button>
812
+ <input type="text" id="messageInput" placeholder="输入消息...">
813
+ <button class="btn-primary" id="sendBtn">发送</button>
814
+ </div>
815
+ <emoji-picker id="emojiPicker" class="hidden"></emoji-picker>
816
+ </div>
817
+ <div id="manageMuteModal" class="modal hidden">
818
+ <div class="modal-content">
819
+ <h3>个人禁言</h3>
820
+ <div id="membersList" style="max-height: 400px; overflow-y: auto;"></div>
821
+ <button type="button" class="btn-secondary" id="closeMuteModal">关闭</button>
822
+ </div>
823
+ </div>
824
+ `;
825
+
826
+ const messagesDiv = document.getElementById('messages');
827
+ const messageInput = document.getElementById('messageInput');
828
+ const sendBtn = document.getElementById('sendBtn');
829
+ const emojiBtn = document.getElementById('emojiBtn');
830
+ const emojiPicker = document.getElementById('emojiPicker');
831
+ let mutedUsers = new Set((group.mutedUsers || []).map(String));
832
+ let isMutedAll = Boolean(group.mutedAll);
833
+
834
+ // 表情包功能
835
+ emojiBtn.addEventListener('click', () => {
836
+ emojiPicker.classList.toggle('hidden');
837
+ });
838
+
839
+ emojiPicker.addEventListener('emoji-click', (event) => {
840
+ messageInput.value += event.detail.unicode;
841
+ messageInput.focus();
842
+ emojiPicker.classList.add('hidden');
843
+ });
844
+
845
+ // 点击外部关闭表情选择器
846
+ document.addEventListener('click', (e) => {
847
+ if (!emojiBtn.contains(e.target) && !emojiPicker.contains(e.target)) {
848
+ emojiPicker.classList.add('hidden');
849
+ }
850
+ });
851
+
852
+ // 消息通知系统
853
+ function showNotification(title, body, icon = '💬') {
854
+ if ('Notification' in window && Notification.permission === 'granted') {
855
+ new Notification(title, {
856
+ body: body,
857
+ icon: '/icon.png',
858
+ badge: '/icon.png',
859
+ tag: 'chat-message'
860
+ });
861
+ }
862
+ }
863
+
864
+ // 请求通知权限
865
+ if ('Notification' in window && Notification.permission === 'default') {
866
+ Notification.requestPermission();
867
+ }
868
+
869
+ // 加载历史消息
870
+ try {
871
+ const messagesResult = await apiService.getGroupMessages(currentGroup._id);
872
+ if (messagesResult.messages) {
873
+ messagesResult.messages.forEach(msg => {
874
+ const messageEl = document.createElement('div');
875
+ messageEl.className = `message ${msg.sender === currentUserId ? 'own' : ''}`;
876
+ messageEl.innerHTML = `
877
+ <div class="message-header">
878
+ <span class="message-user">${msg.username}</span>
879
+ <span class="message-time">${new Date(msg.timestamp).toLocaleTimeString()}</span>
880
+ </div>
881
+ <div class="message-content">${msg.content}</div>
882
+ `;
883
+ messagesDiv.appendChild(messageEl);
884
+ });
885
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
886
+ }
887
+ } catch (err) {
888
+ console.error('加载历史消息失败:', err);
889
+ }
890
+
891
+ const refreshMuteButtons = () => {
892
+ const btn = document.getElementById('muteAllBtn');
893
+ btn.textContent = isMutedAll ? '取消全体禁言' : '全体禁言';
894
+ btn.style.background = isMutedAll ? 'var(--danger)' : '';
895
+ };
896
+ refreshMuteButtons();
897
+
898
+ // 全体禁言(服务端生效)
899
+ document.getElementById('muteAllBtn').addEventListener('click', async () => {
900
+ try {
901
+ const next = !isMutedAll;
902
+ const res = await apiService.setMuteAll(currentGroup._id, next);
903
+ isMutedAll = Boolean(res.mutedAll);
904
+ refreshMuteButtons();
905
+
906
+ const notification = document.createElement('div');
907
+ notification.className = 'notification';
908
+ notification.textContent = isMutedAll ? '已开启全体禁言(成员无法发言)' : '已取消全体禁言';
909
+ messagesDiv.appendChild(notification);
910
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
911
+ } catch (e) {
912
+ alert('设置失败: ' + e.message);
913
+ }
914
+ });
915
+
916
+ // 个人禁言(服务端生效)
917
+ document.getElementById('manageMuteBtn').addEventListener('click', async () => {
918
+ // 重新拉取最新 group(避免成员变动不同步)
919
+ const latest = await apiService.getGroup(currentGroup._id);
920
+ group = latest.group;
921
+ mutedUsers = new Set((group.mutedUsers || []).map(String));
922
+
923
+ const membersList = document.getElementById('membersList');
924
+ membersList.innerHTML = group.members
925
+ .filter(m => m._id.toString() !== currentUserId)
926
+ .map(member => {
927
+ const isMuted = mutedUsers.has(member._id.toString());
928
+ return `
929
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 12px; border-bottom: 1px solid var(--border);">
930
+ <div style="display: flex; align-items: center; gap: 10px;">
931
+ <div class="avatar" style="width: 35px; height: 35px;">${member.username[0].toUpperCase()}</div>
932
+ <span>${member.username}</span>
933
+ </div>
934
+ <button class="btn-secondary btn-sm" onclick="toggleMute('${member._id}')" id="mute-${member._id}">
935
+ ${isMuted ? '取消禁言' : '禁言'}
936
+ </button>
937
+ </div>
938
+ `;
939
+ }).join('');
940
+ document.getElementById('manageMuteModal').classList.remove('hidden');
941
+ });
942
+
943
+ document.getElementById('closeMuteModal').addEventListener('click', () => {
944
+ document.getElementById('manageMuteModal').classList.add('hidden');
945
+ });
946
+
947
+ // 切换个人禁言状态(服务端生效)
948
+ window.toggleMute = async (userId) => {
949
+ try {
950
+ const nextMuted = !mutedUsers.has(userId);
951
+ const res = await apiService.setUserMute(currentGroup._id, userId, nextMuted);
952
+ mutedUsers = new Set((res.mutedUsers || []).map(String));
953
+
954
+ const btn = document.getElementById(`mute-${userId}`);
955
+ btn.textContent = mutedUsers.has(userId) ? '取消禁言' : '禁言';
956
+ btn.style.background = mutedUsers.has(userId) ? 'var(--danger)' : '';
957
+ } catch (e) {
958
+ alert('操作失败: ' + e.message);
959
+ }
960
+ };
961
+
962
+ // 监听消息
963
+ wsService.on('chat_message', (data) => {
964
+ if (data.groupId === currentGroup._id) {
965
+ const messageEl = document.createElement('div');
966
+ messageEl.className = `message ${data.userId === currentUserId ? 'own' : ''}`;
967
+ messageEl.innerHTML = `
968
+ <div class="message-header">
969
+ <span class="message-user">${data.username}</span>
970
+ <span class="message-time">${new Date(data.timestamp).toLocaleTimeString()}</span>
971
+ </div>
972
+ <div class="message-content">${data.content}</div>
973
+ `;
974
+ messagesDiv.appendChild(messageEl);
975
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
976
+ }
977
+ });
978
+
979
+ // 发送被拦截提示(来自服务端)
980
+ wsService.on('chat_blocked', (data) => {
981
+ if (data.groupId === currentGroup._id) {
982
+ const notification = document.createElement('div');
983
+ notification.className = 'notification';
984
+ notification.textContent = data.message || '消息发送失败';
985
+ messagesDiv.appendChild(notification);
986
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
987
+ }
988
+ });
989
+
990
+ // 发送消息
991
+ const sendMessage = () => {
992
+ const content = messageInput.value.trim();
993
+ if (content) {
994
+ wsService.sendChatMessage(currentGroup._id, user.username, content);
995
+ messageInput.value = '';
996
+ }
997
+ };
998
+
999
+ sendBtn.addEventListener('click', sendMessage);
1000
+ messageInput.addEventListener('keypress', (e) => {
1001
+ if (e.key === 'Enter') sendMessage();
1002
+ });
1003
+ }
1004
+
1005
+ async function renderCallView(container) {
1006
+ if (!currentGroup) {
1007
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
1008
+ return;
1009
+ }
1010
+
1011
+ container.innerHTML = `
1012
+ <div class="view-header">
1013
+ <h2>随机点名 - ${currentGroup.name}</h2>
1014
+ </div>
1015
+ <div class="call-panel">
1016
+ <div class="call-controls">
1017
+ <label>点名人数:</label>
1018
+ <input type="number" id="callCount" value="1" min="1" max="10">
1019
+ <button class="btn-primary btn-large" id="randomCallBtn">🎲 开始点名</button>
1020
+ </div>
1021
+ <div id="callResult" class="call-result"></div>
1022
+ </div>
1023
+ `;
1024
+
1025
+ document.getElementById('randomCallBtn').addEventListener('click', async () => {
1026
+ const count = parseInt(document.getElementById('callCount').value);
1027
+ try {
1028
+ const result = await apiService.randomCall(currentGroup._id, count);
1029
+ const callResult = document.getElementById('callResult');
1030
+ callResult.innerHTML = `
1031
+ <h3>点名结果:</h3>
1032
+ <div class="called-members">
1033
+ ${result.calledMembers.map(member => `
1034
+ <div class="member-card">
1035
+ <div class="avatar">${member.username[0].toUpperCase()}</div>
1036
+ <div class="member-name">${member.username}</div>
1037
+ </div>
1038
+ `).join('')}
1039
+ </div>
1040
+ `;
1041
+ } catch (error) {
1042
+ alert('点名失败: ' + error.message);
1043
+ }
1044
+ });
1045
+ }
1046
+
1047
+ async function renderAuditView(container) {
1048
+ container.innerHTML = `
1049
+ <div class="view-header">
1050
+ <h2>操作记录</h2>
1051
+ <div style="display: flex; gap: 10px;">
1052
+ <select id="auditGroupFilter" class="form-select">
1053
+ <option value="">全部群组</option>
1054
+ </select>
1055
+ <select id="auditActionFilter" class="form-select">
1056
+ <option value="">全部操作</option>
1057
+ <option value="document_create">文档创建</option>
1058
+ <option value="document_update">文档更新</option>
1059
+ <option value="document_delete">文档删除</option>
1060
+ <option value="content_edit">内容编辑</option>
1061
+ <option value="title_edit">标题编辑</option>
1062
+ <option value="document_permission_change">权限修改</option>
1063
+ </select>
1064
+ <input type="date" id="startDate" class="form-input" title="开始日期">
1065
+ <input type="date" id="endDate" class="form-input" title="结束日期">
1066
+ <button class="btn-primary" id="applyFilters">筛选</button>
1067
+ <button class="btn-secondary" id="exportLogs">导出</button>
1068
+ </div>
1069
+ </div>
1070
+
1071
+ <div class="audit-stats" id="auditStats">
1072
+ <div class="stat-card">
1073
+ <h3>今日操作</h3>
1074
+ <div class="stat-number" id="todayCount">-</div>
1075
+ </div>
1076
+ <div class="stat-card">
1077
+ <h3>本周操作</h3>
1078
+ <div class="stat-number" id="weekCount">-</div>
1079
+ </div>
1080
+ <div class="stat-card">
1081
+ <h3>活跃用户</h3>
1082
+ <div class="stat-number" id="activeUsers">-</div>
1083
+ </div>
1084
+ </div>
1085
+
1086
+ <div class="audit-logs" id="auditLogs">
1087
+ <div class="loading">加载中...</div>
1088
+ </div>
1089
+
1090
+ <div class="pagination" id="auditPagination" style="display: none;">
1091
+ <button class="btn-secondary" id="prevPage">上一页</button>
1092
+ <span id="pageInfo">第 1 页,共 1 页</span>
1093
+ <button class="btn-secondary" id="nextPage">下一页</button>
1094
+ </div>
1095
+
1096
+ <div id="auditDetailModal" class="modal hidden">
1097
+ <div class="modal-content" style="max-width: 800px;">
1098
+ <div class="modal-header">
1099
+ <h3>操作详情</h3>
1100
+ <button class="close-btn" id="closeAuditDetail">&times;</button>
1101
+ </div>
1102
+ <div class="modal-body" id="auditDetailContent">
1103
+ </div>
1104
+ </div>
1105
+ </div>
1106
+ `;
1107
+
1108
+ let currentPage = 1;
1109
+ let currentFilters = {};
1110
+
1111
+ // 加载群组列表到筛选器
1112
+ try {
1113
+ const groupsResult = await apiService.getGroups();
1114
+ const groupFilter = document.getElementById('auditGroupFilter');
1115
+ groupsResult.groups.forEach(group => {
1116
+ const option = document.createElement('option');
1117
+ option.value = group._id;
1118
+ option.textContent = group.name;
1119
+ groupFilter.appendChild(option);
1120
+ });
1121
+ } catch (error) {
1122
+ console.error('加载群组列表失败:', error);
1123
+ }
1124
+
1125
+ async function loadAuditLogs(page = 1, filters = {}) {
1126
+ try {
1127
+ const auditLogsDiv = document.getElementById('auditLogs');
1128
+ auditLogsDiv.innerHTML = '<div class="loading">加载中...</div>';
1129
+
1130
+ const options = { page, limit: 20 };
1131
+ const result = await apiService.getAuditLogs(filters, options);
1132
+
1133
+ if (result.logs.length === 0) {
1134
+ auditLogsDiv.innerHTML = '<div class="empty-state">暂无操作记录</div>';
1135
+ document.getElementById('auditPagination').style.display = 'none';
1136
+ return;
1137
+ }
1138
+
1139
+ auditLogsDiv.innerHTML = `
1140
+ <div class="audit-table">
1141
+ <div class="audit-header">
1142
+ <div>时间</div>
1143
+ <div>用户</div>
1144
+ <div>操作</div>
1145
+ <div>资源</div>
1146
+ <div>详情</div>
1147
+ </div>
1148
+ ${result.logs.map(log => `
1149
+ <div class="audit-row" onclick="showAuditDetail('${log._id}')">
1150
+ <div class="audit-time">${new Date(log.createdAt).toLocaleString()}</div>
1151
+ <div class="audit-user">
1152
+ <div class="avatar">${log.user?.username?.[0]?.toUpperCase() || '?'}</div>
1153
+ <span>${log.user?.username || '未知用户'}</span>
1154
+ </div>
1155
+ <div class="audit-action">
1156
+ <span class="action-badge action-${log.action}">${getActionText(log.action)}</span>
1157
+ </div>
1158
+ <div class="audit-resource">${log.resourceTitle || log.resourceId}</div>
1159
+ <div class="audit-description">${log.details?.description || '-'}</div>
1160
+ </div>
1161
+ `).join('')}
1162
+ </div>
1163
+ `;
1164
+
1165
+ // 更新分页
1166
+ const pagination = document.getElementById('auditPagination');
1167
+ const pageInfo = document.getElementById('pageInfo');
1168
+ pageInfo.textContent = `第 ${result.pagination.page} 页,共 ${result.pagination.pages} 页`;
1169
+
1170
+ document.getElementById('prevPage').disabled = result.pagination.page <= 1;
1171
+ document.getElementById('nextPage').disabled = result.pagination.page >= result.pagination.pages;
1172
+
1173
+ pagination.style.display = result.pagination.pages > 1 ? 'flex' : 'none';
1174
+
1175
+ } catch (error) {
1176
+ console.error('加载审计日志失败:', error);
1177
+ document.getElementById('auditLogs').innerHTML =
1178
+ '<div class="error-state">加载失败: ' + error.message + '</div>';
1179
+ }
1180
+ }
1181
+
1182
+ async function loadStats() {
1183
+ try {
1184
+ const today = new Date();
1185
+ const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
1186
+
1187
+ // 获取今日统计
1188
+ const todayStats = await apiService.getAuditSummary({
1189
+ startDate: today.toISOString().split('T')[0],
1190
+ endDate: today.toISOString().split('T')[0]
1191
+ });
1192
+
1193
+ // 获取本周统计
1194
+ const weekStats = await apiService.getAuditSummary({
1195
+ startDate: weekAgo.toISOString().split('T')[0],
1196
+ endDate: today.toISOString().split('T')[0]
1197
+ });
1198
+
1199
+ document.getElementById('todayCount').textContent = todayStats.summary.totalLogs;
1200
+ document.getElementById('weekCount').textContent = weekStats.summary.totalLogs;
1201
+ document.getElementById('activeUsers').textContent = weekStats.summary.topUsers.length;
1202
+
1203
+ } catch (error) {
1204
+ console.error('加载统计信息失败:', error);
1205
+ }
1206
+ }
1207
+
1208
+ // 显示操作详情
1209
+ window.showAuditDetail = async (logId) => {
1210
+ try {
1211
+ // 这里我们需要从已加载的日志中找到对应的记录
1212
+ // 在实际应用中可能需要单独的API来获取详情
1213
+ const modal = document.getElementById('auditDetailModal');
1214
+ const content = document.getElementById('auditDetailContent');
1215
+
1216
+ // 简单实现:显示基本信息
1217
+ content.innerHTML = `
1218
+ <div class="audit-detail">
1219
+ <p>操作ID: ${logId}</p>
1220
+ <p>详细信息加载中...</p>
1221
+ </div>
1222
+ `;
1223
+
1224
+ modal.classList.remove('hidden');
1225
+ } catch (error) {
1226
+ alert('加载详情失败: ' + error.message);
1227
+ }
1228
+ };
1229
+
1230
+ // 事件监听
1231
+ document.getElementById('applyFilters').addEventListener('click', () => {
1232
+ currentFilters = {
1233
+ groupId: document.getElementById('auditGroupFilter').value,
1234
+ action: document.getElementById('auditActionFilter').value,
1235
+ startDate: document.getElementById('startDate').value,
1236
+ endDate: document.getElementById('endDate').value
1237
+ };
1238
+
1239
+ // 移除空值
1240
+ Object.keys(currentFilters).forEach(key => {
1241
+ if (!currentFilters[key]) {
1242
+ delete currentFilters[key];
1243
+ }
1244
+ });
1245
+
1246
+ currentPage = 1;
1247
+ loadAuditLogs(currentPage, currentFilters);
1248
+ });
1249
+
1250
+ document.getElementById('prevPage').addEventListener('click', () => {
1251
+ if (currentPage > 1) {
1252
+ currentPage--;
1253
+ loadAuditLogs(currentPage, currentFilters);
1254
+ }
1255
+ });
1256
+
1257
+ document.getElementById('nextPage').addEventListener('click', () => {
1258
+ currentPage++;
1259
+ loadAuditLogs(currentPage, currentFilters);
1260
+ });
1261
+
1262
+ document.getElementById('exportLogs').addEventListener('click', () => {
1263
+ alert('导出功能开发中...');
1264
+ });
1265
+
1266
+ document.getElementById('closeAuditDetail').addEventListener('click', () => {
1267
+ document.getElementById('auditDetailModal').classList.add('hidden');
1268
+ });
1269
+
1270
+ // 初始加载
1271
+ loadStats();
1272
+ loadAuditLogs();
1273
+ }
1274
+
1275
+ async function renderSearchView(container) {
1276
+ container.innerHTML = `
1277
+ <div class="view-header">
1278
+ <h2>🔍 搜索</h2>
1279
+ </div>
1280
+ <div class="search-container">
1281
+ <div class="search-box">
1282
+ <input type="text" id="searchInput" placeholder="搜索消息、文档、任务...">
1283
+ <button class="btn-primary" id="searchBtn">搜索</button>
1284
+ </div>
1285
+ <div class="search-filters">
1286
+ <label>
1287
+ <input type="checkbox" id="filterMessages" checked> 消息
1288
+ </label>
1289
+ <label>
1290
+ <input type="checkbox" id="filterDocuments" checked> 文档
1291
+ </label>
1292
+ <label>
1293
+ <input type="checkbox" id="filterTasks" checked> 任务
1294
+ </label>
1295
+ </div>
1296
+ <div class="search-results" id="searchResults"></div>
1297
+ </div>
1298
+ `;
1299
+
1300
+ const searchInput = document.getElementById('searchInput');
1301
+ const searchBtn = document.getElementById('searchBtn');
1302
+ const searchResults = document.getElementById('searchResults');
1303
+
1304
+ const performSearch = async () => {
1305
+ const query = searchInput.value.trim();
1306
+ if (!query) {
1307
+ searchResults.innerHTML = '<div class="empty-state">请输入搜索关键词</div>';
1308
+ return;
1309
+ }
1310
+
1311
+ const filters = {
1312
+ messages: document.getElementById('filterMessages').checked,
1313
+ documents: document.getElementById('filterDocuments').checked,
1314
+ tasks: document.getElementById('filterTasks').checked
1315
+ };
1316
+
1317
+ searchResults.innerHTML = '<div class="loading">搜索中...</div>';
1318
+
1319
+ try {
1320
+ const results = [];
1321
+
1322
+ // 搜索消息
1323
+ if (filters.messages && currentGroup) {
1324
+ try {
1325
+ const messagesResult = await apiService.getGroupMessages(currentGroup._id);
1326
+ if (messagesResult.messages) {
1327
+ const matchedMessages = messagesResult.messages.filter(msg =>
1328
+ msg.content.toLowerCase().includes(query.toLowerCase())
1329
+ );
1330
+ matchedMessages.forEach(msg => {
1331
+ results.push({
1332
+ type: 'message',
1333
+ title: `消息 - ${msg.username}`,
1334
+ content: msg.content,
1335
+ time: msg.timestamp,
1336
+ group: currentGroup.name
1337
+ });
1338
+ });
1339
+ }
1340
+ } catch (err) {
1341
+ console.error('搜索消息失败:', err);
1342
+ }
1343
+ }
1344
+
1345
+ // 搜索文档
1346
+ if (filters.documents) {
1347
+ try {
1348
+ if (currentGroup) {
1349
+ const docsResult = await apiService.getDocuments(currentGroup._id);
1350
+ if (docsResult.documents) {
1351
+ const matchedDocs = docsResult.documents.filter(doc =>
1352
+ doc.title.toLowerCase().includes(query.toLowerCase()) ||
1353
+ doc.content.toLowerCase().includes(query.toLowerCase())
1354
+ );
1355
+ matchedDocs.forEach(doc => {
1356
+ results.push({
1357
+ type: 'document',
1358
+ title: doc.title,
1359
+ content: doc.content.substring(0, 200),
1360
+ time: doc.updatedAt,
1361
+ id: doc._id,
1362
+ group: currentGroup.name
1363
+ });
1364
+ });
1365
+ }
1366
+ }
1367
+ } catch (err) {
1368
+ console.error('搜索文档失败:', err);
1369
+ }
1370
+ }
1371
+
1372
+ // 搜索任务
1373
+ if (filters.tasks && currentGroup) {
1374
+ try {
1375
+ const tasksResult = await apiService.getTasks(currentGroup._id);
1376
+ if (tasksResult.tasks) {
1377
+ const matchedTasks = tasksResult.tasks.filter(task =>
1378
+ task.title.toLowerCase().includes(query.toLowerCase()) ||
1379
+ (task.description && task.description.toLowerCase().includes(query.toLowerCase()))
1380
+ );
1381
+ matchedTasks.forEach(task => {
1382
+ results.push({
1383
+ type: 'task',
1384
+ title: task.title,
1385
+ content: task.description || '',
1386
+ time: task.updatedAt,
1387
+ id: task._id,
1388
+ status: task.status,
1389
+ group: currentGroup.name
1390
+ });
1391
+ });
1392
+ }
1393
+ } catch (err) {
1394
+ console.error('搜索任务失败:', err);
1395
+ }
1396
+ }
1397
+
1398
+ // 显示结果
1399
+ if (results.length === 0) {
1400
+ searchResults.innerHTML = '<div class="empty-state">未找到相关结果</div>';
1401
+ } else {
1402
+ searchResults.innerHTML = results.map(result => {
1403
+ const typeIcon = {
1404
+ message: '💬',
1405
+ document: '📄',
1406
+ task: '📋'
1407
+ };
1408
+ return `
1409
+ <div class="search-result-item">
1410
+ <div class="result-header">
1411
+ <span class="result-type">${typeIcon[result.type]} ${result.type === 'message' ? '消息' : result.type === 'document' ? '文档' : '任务'}</span>
1412
+ <span class="result-time">${new Date(result.time).toLocaleString()}</span>
1413
+ </div>
1414
+ <h4>${highlightText(result.title, query)}</h4>
1415
+ <p>${highlightText(result.content, query)}</p>
1416
+ ${result.group ? `<span class="result-group">群组: ${result.group}</span>` : ''}
1417
+ ${result.status ? `<span class="result-status">状态: ${getStatusText(result.status)}</span>` : ''}
1418
+ </div>
1419
+ `;
1420
+ }).join('');
1421
+ }
1422
+ } catch (error) {
1423
+ searchResults.innerHTML = `<div class="empty-state">搜索失败: ${error.message}</div>`;
1424
+ }
1425
+ };
1426
+
1427
+ searchBtn.addEventListener('click', performSearch);
1428
+ searchInput.addEventListener('keypress', (e) => {
1429
+ if (e.key === 'Enter') performSearch();
1430
+ });
1431
+ }
1432
+
1433
+ function highlightText(text, query) {
1434
+ if (!query) return text;
1435
+ const regex = new RegExp(`(${query})`, 'gi');
1436
+ return text.replace(regex, '<mark>$1</mark>');
1437
+ }
1438
+
1439
+ function getStatusText(status) {
1440
+ const statusMap = {
1441
+ 'pending': '待处理',
1442
+ 'in_progress': '进行中',
1443
+ 'completed': '已完成',
1444
+ 'terminated': '已终止'
1445
+ };
1446
+ return statusMap[status] || status;
1447
+ }
1448
+
1449
+ function getActionText(action) {
1450
+ const actionMap = {
1451
+ 'document_create': '创建文档',
1452
+ 'document_update': '更新文档',
1453
+ 'document_delete': '删除文档',
1454
+ 'content_edit': '编辑内容',
1455
+ 'title_edit': '修改标题',
1456
+ 'document_permission_change': '权限修改'
1457
+ };
1458
+ return actionMap[action] || action;
1459
+ }
1460
+
1461
+ function getStatusText(status) {
1462
+ const statusMap = {
1463
+ 'pending': '待处理',
1464
+ 'in_progress': '进行中',
1465
+ 'completed': '已完成',
1466
+ 'terminated': '已终止'
1467
+ };
1468
+ return statusMap[status] || status;
1469
+ }
1470
+
1471
+ renderView('groups');
1472
+ }
1473
+