collabdocchat 2.1.2 → 2.1.4
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.
- package/package.json +1 -1
- package/scripts/fix-optimized-views.js +37 -0
- package/scripts/fix-user-dashboard.js +62 -0
- package/scripts/remove-quill-from-user-dashboard.js +3 -0
- package/scripts/remove-quill-imports-only.js +32 -0
- package/src/pages/admin-dashboard.js +21 -0
- package/src/pages/optimized-backup-view.js +1 -1
- package/src/pages/optimized-workflow-view.js +2 -2
- package/src/pages/user-dashboard.js +155 -155
- package/src/utils/theme-manager.js +799 -799
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { ApiService } from '../services/api.js';
|
|
2
2
|
import { AuthService } from '../services/auth.js';
|
|
3
3
|
import 'emoji-picker-element';
|
|
4
4
|
|
|
@@ -16,42 +16,42 @@ export function renderUserDashboard(user, wsService) {
|
|
|
16
16
|
<aside class="sidebar">
|
|
17
17
|
<div class="sidebar-header">
|
|
18
18
|
<h2>CollabDocChat</h2>
|
|
19
|
-
<span class="badge-user"
|
|
19
|
+
<span class="badge-user">用户</span>
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
22
|
<div class="user-info">
|
|
23
23
|
<div class="avatar">${user.username[0].toUpperCase()}</div>
|
|
24
24
|
<div>
|
|
25
25
|
<div class="username">${user.username}</div>
|
|
26
|
-
<div class="user-role"
|
|
26
|
+
<div class="user-role">普通用户</div>
|
|
27
27
|
</div>
|
|
28
28
|
</div>
|
|
29
29
|
|
|
30
30
|
<nav class="nav-menu">
|
|
31
31
|
<button class="nav-item active" data-view="groups">
|
|
32
|
-
<span class="icon"
|
|
32
|
+
<span class="icon">👥</span> 我的群组
|
|
33
33
|
</button>
|
|
34
34
|
<button class="nav-item" data-view="allgroups">
|
|
35
|
-
<span class="icon"
|
|
35
|
+
<span class="icon">🌐</span> 所有群组
|
|
36
36
|
</button>
|
|
37
37
|
<button class="nav-item" data-view="tasks">
|
|
38
|
-
<span class="icon"
|
|
38
|
+
<span class="icon">📋</span> 我的任务
|
|
39
39
|
</button>
|
|
40
40
|
<button class="nav-item" data-view="documents">
|
|
41
|
-
<span class="icon"
|
|
41
|
+
<span class="icon">📄</span> 共享文档
|
|
42
42
|
</button>
|
|
43
43
|
<button class="nav-item" data-view="files">
|
|
44
|
-
<span class="icon"
|
|
44
|
+
<span class="icon">📎</span> 文件共享
|
|
45
45
|
</button>
|
|
46
46
|
<button class="nav-item" data-view="chat">
|
|
47
|
-
<span class="icon"
|
|
47
|
+
<span class="icon">💬</span> 群聊
|
|
48
48
|
</button>
|
|
49
49
|
<button class="nav-item" data-view="search">
|
|
50
|
-
<span class="icon"
|
|
50
|
+
<span class="icon">🔍</span> 搜索
|
|
51
51
|
</button>
|
|
52
52
|
</nav>
|
|
53
53
|
|
|
54
|
-
<button class="btn-logout" id="logoutBtn"
|
|
54
|
+
<button class="btn-logout" id="logoutBtn">退出登录</button>
|
|
55
55
|
</aside>
|
|
56
56
|
|
|
57
57
|
<main class="main-content">
|
|
@@ -60,7 +60,7 @@ export function renderUserDashboard(user, wsService) {
|
|
|
60
60
|
</div>
|
|
61
61
|
`;
|
|
62
62
|
|
|
63
|
-
//
|
|
63
|
+
// 导航切换
|
|
64
64
|
document.querySelectorAll('.nav-item').forEach(item => {
|
|
65
65
|
item.addEventListener('click', () => {
|
|
66
66
|
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
|
@@ -70,7 +70,7 @@ export function renderUserDashboard(user, wsService) {
|
|
|
70
70
|
});
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
//
|
|
73
|
+
// 退出登录
|
|
74
74
|
document.getElementById('logoutBtn').addEventListener('click', () => {
|
|
75
75
|
authService.logout();
|
|
76
76
|
});
|
|
@@ -109,14 +109,14 @@ export function renderUserDashboard(user, wsService) {
|
|
|
109
109
|
|
|
110
110
|
container.innerHTML = `
|
|
111
111
|
<div class="view-header">
|
|
112
|
-
<h2
|
|
112
|
+
<h2>我的群组</h2>
|
|
113
113
|
</div>
|
|
114
114
|
<div class="groups-grid" id="groupsList"></div>
|
|
115
115
|
`;
|
|
116
116
|
|
|
117
117
|
const groupsList = document.getElementById('groupsList');
|
|
118
118
|
if (groups.length === 0) {
|
|
119
|
-
groupsList.innerHTML = '<div class="empty-state"
|
|
119
|
+
groupsList.innerHTML = '<div class="empty-state">您还没有加入任何群组<br>请前往"所有群组"查看并加入</div>';
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -125,15 +125,15 @@ export function renderUserDashboard(user, wsService) {
|
|
|
125
125
|
groupCard.className = 'group-card';
|
|
126
126
|
groupCard.innerHTML = `
|
|
127
127
|
<h3>${group.name}</h3>
|
|
128
|
-
<p>${group.description || '
|
|
128
|
+
<p>${group.description || '暂无描述'}</p>
|
|
129
129
|
<div class="group-stats">
|
|
130
|
-
<span
|
|
131
|
-
<span
|
|
132
|
-
<span
|
|
130
|
+
<span>👥 ${group.members.length} 成员</span>
|
|
131
|
+
<span>📄 ${group.documents.length} 文档</span>
|
|
132
|
+
<span>📋 ${group.tasks.length} 任务</span>
|
|
133
133
|
</div>
|
|
134
134
|
<div style="display: flex; gap: 10px; margin-top: 10px;">
|
|
135
|
-
<button class="btn-select" data-id="${group._id}"
|
|
136
|
-
<button class="btn-secondary" data-id="${group._id}" data-action="leave"
|
|
135
|
+
<button class="btn-select" data-id="${group._id}">进入群组</button>
|
|
136
|
+
<button class="btn-secondary" data-id="${group._id}" data-action="leave">退出群组</button>
|
|
137
137
|
</div>
|
|
138
138
|
`;
|
|
139
139
|
groupsList.appendChild(groupCard);
|
|
@@ -143,19 +143,19 @@ export function renderUserDashboard(user, wsService) {
|
|
|
143
143
|
btn.addEventListener('click', () => {
|
|
144
144
|
currentGroup = groups.find(g => g._id === btn.dataset.id);
|
|
145
145
|
wsService.joinGroup(currentGroup._id);
|
|
146
|
-
alert(
|
|
146
|
+
alert(`已进入群组: ${currentGroup.name}`);
|
|
147
147
|
});
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
document.querySelectorAll('[data-action="leave"]').forEach(btn => {
|
|
151
151
|
btn.addEventListener('click', async () => {
|
|
152
|
-
if (confirm('
|
|
152
|
+
if (confirm('确定要退出该群组吗?')) {
|
|
153
153
|
try {
|
|
154
154
|
await apiService.leaveGroup(btn.dataset.id);
|
|
155
|
-
alert('
|
|
155
|
+
alert('已退出群组');
|
|
156
156
|
await renderGroupsView(container);
|
|
157
157
|
} catch (error) {
|
|
158
|
-
alert('
|
|
158
|
+
alert('退出失败: ' + error.message);
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
});
|
|
@@ -169,7 +169,7 @@ export function renderUserDashboard(user, wsService) {
|
|
|
169
169
|
|
|
170
170
|
container.innerHTML = `
|
|
171
171
|
<div class="view-header">
|
|
172
|
-
<h2
|
|
172
|
+
<h2>所有群组</h2>
|
|
173
173
|
</div>
|
|
174
174
|
<div class="groups-grid" id="allGroupsList"></div>
|
|
175
175
|
`;
|
|
@@ -181,14 +181,14 @@ export function renderUserDashboard(user, wsService) {
|
|
|
181
181
|
groupCard.className = 'group-card';
|
|
182
182
|
groupCard.innerHTML = `
|
|
183
183
|
<h3>${group.name}</h3>
|
|
184
|
-
<p>${group.description || '
|
|
184
|
+
<p>${group.description || '暂无描述'}</p>
|
|
185
185
|
<div class="group-stats">
|
|
186
|
-
<span
|
|
187
|
-
<span
|
|
186
|
+
<span>👥 ${group.members.length} 成员</span>
|
|
187
|
+
<span>📄 ${group.documents.length} 文档</span>
|
|
188
188
|
</div>
|
|
189
189
|
${isJoined ?
|
|
190
|
-
'<div style="color: var(--success); margin-top: 10px;"
|
|
191
|
-
`<button class="btn-primary" data-id="${group._id}" data-action="join"
|
|
190
|
+
'<div style="color: var(--success); margin-top: 10px;">✓ 已加入</div>' :
|
|
191
|
+
`<button class="btn-primary" data-id="${group._id}" data-action="join">加入群组</button>`
|
|
192
192
|
}
|
|
193
193
|
`;
|
|
194
194
|
allGroupsList.appendChild(groupCard);
|
|
@@ -198,10 +198,10 @@ export function renderUserDashboard(user, wsService) {
|
|
|
198
198
|
btn.addEventListener('click', async () => {
|
|
199
199
|
try {
|
|
200
200
|
await apiService.joinGroup(btn.dataset.id);
|
|
201
|
-
alert('
|
|
201
|
+
alert('加入成功!');
|
|
202
202
|
await renderAllGroupsView(container);
|
|
203
203
|
} catch (error) {
|
|
204
|
-
alert('
|
|
204
|
+
alert('加入失败: ' + error.message);
|
|
205
205
|
}
|
|
206
206
|
});
|
|
207
207
|
});
|
|
@@ -213,7 +213,7 @@ export function renderUserDashboard(user, wsService) {
|
|
|
213
213
|
|
|
214
214
|
container.innerHTML = `
|
|
215
215
|
<div class="view-header">
|
|
216
|
-
<h2
|
|
216
|
+
<h2>我的任务</h2>
|
|
217
217
|
</div>
|
|
218
218
|
<div class="tasks-list" id="tasksList"></div>
|
|
219
219
|
`;
|
|
@@ -221,7 +221,7 @@ export function renderUserDashboard(user, wsService) {
|
|
|
221
221
|
const tasksList = document.getElementById('tasksList');
|
|
222
222
|
|
|
223
223
|
if (result.tasks.length === 0) {
|
|
224
|
-
tasksList.innerHTML = '<div class="empty-state"
|
|
224
|
+
tasksList.innerHTML = '<div class="empty-state">暂无任务</div>';
|
|
225
225
|
return;
|
|
226
226
|
}
|
|
227
227
|
|
|
@@ -233,19 +233,19 @@ export function renderUserDashboard(user, wsService) {
|
|
|
233
233
|
<p>${task.description}</p>
|
|
234
234
|
<div class="task-meta">
|
|
235
235
|
<span class="status-badge">${getStatusText(task.status)}</span>
|
|
236
|
-
<span
|
|
237
|
-
${task.deadline ? `<span
|
|
236
|
+
<span>群组: ${task.group.name}</span>
|
|
237
|
+
${task.deadline ? `<span>截止: ${new Date(task.deadline).toLocaleDateString()}</span>` : ''}
|
|
238
238
|
</div>
|
|
239
|
-
${task.relatedDocument ? `<a href="#" class="doc-link" data-id="${task.relatedDocument._id}"
|
|
239
|
+
${task.relatedDocument ? `<a href="#" class="doc-link" data-id="${task.relatedDocument._id}">📄 查看相关文档</a>` : ''}
|
|
240
240
|
<div class="task-actions">
|
|
241
|
-
${task.status === 'pending' ? `<button class="btn-primary btn-sm" data-id="${task._id}" data-action="start"
|
|
242
|
-
${task.status === 'in_progress' ? `<button class="btn-success btn-sm" data-id="${task._id}" data-action="complete"
|
|
241
|
+
${task.status === 'pending' ? `<button class="btn-primary btn-sm" data-id="${task._id}" data-action="start">开始任务</button>` : ''}
|
|
242
|
+
${task.status === 'in_progress' ? `<button class="btn-success btn-sm" data-id="${task._id}" data-action="complete">完成任务</button>` : ''}
|
|
243
243
|
</div>
|
|
244
244
|
`;
|
|
245
245
|
tasksList.appendChild(taskCard);
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
-
//
|
|
248
|
+
// 任务操作
|
|
249
249
|
document.querySelectorAll('[data-action]').forEach(btn => {
|
|
250
250
|
btn.addEventListener('click', async () => {
|
|
251
251
|
const taskId = btn.dataset.id;
|
|
@@ -256,24 +256,24 @@ export function renderUserDashboard(user, wsService) {
|
|
|
256
256
|
await apiService.updateTaskStatus(taskId, status);
|
|
257
257
|
await renderTasksView(container);
|
|
258
258
|
} catch (error) {
|
|
259
|
-
alert('
|
|
259
|
+
alert('操作失败: ' + error.message);
|
|
260
260
|
}
|
|
261
261
|
});
|
|
262
262
|
});
|
|
263
263
|
} catch (error) {
|
|
264
|
-
console.error('
|
|
264
|
+
console.error('获取任务失败:', error);
|
|
265
265
|
container.innerHTML = `
|
|
266
266
|
<div class="view-header">
|
|
267
|
-
<h2
|
|
267
|
+
<h2>我的任务</h2>
|
|
268
268
|
</div>
|
|
269
|
-
<div class="empty-state"
|
|
269
|
+
<div class="empty-state">加载任务失败: ${error.message}</div>
|
|
270
270
|
`;
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
async function renderDocumentsView(container) {
|
|
275
275
|
if (!currentGroup) {
|
|
276
|
-
container.innerHTML = '<div class="empty-state"
|
|
276
|
+
container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
|
|
277
277
|
return;
|
|
278
278
|
}
|
|
279
279
|
|
|
@@ -281,7 +281,7 @@ export function renderUserDashboard(user, wsService) {
|
|
|
281
281
|
|
|
282
282
|
container.innerHTML = `
|
|
283
283
|
<div class="view-header">
|
|
284
|
-
<h2
|
|
284
|
+
<h2>共享文档 - ${currentGroup.name}</h2>
|
|
285
285
|
</div>
|
|
286
286
|
<div class="documents-list" id="docsList"></div>
|
|
287
287
|
`;
|
|
@@ -289,7 +289,7 @@ export function renderUserDashboard(user, wsService) {
|
|
|
289
289
|
const docsList = document.getElementById('docsList');
|
|
290
290
|
|
|
291
291
|
if (result.documents.length === 0) {
|
|
292
|
-
docsList.innerHTML = '<div class="empty-state"
|
|
292
|
+
docsList.innerHTML = '<div class="empty-state">暂无文档</div>';
|
|
293
293
|
return;
|
|
294
294
|
}
|
|
295
295
|
|
|
@@ -297,14 +297,14 @@ export function renderUserDashboard(user, wsService) {
|
|
|
297
297
|
const docCard = document.createElement('div');
|
|
298
298
|
docCard.className = 'document-card';
|
|
299
299
|
docCard.innerHTML = `
|
|
300
|
-
<h3
|
|
300
|
+
<h3>📄 ${doc.title}</h3>
|
|
301
301
|
<div class="doc-meta">
|
|
302
|
-
<span
|
|
303
|
-
<span>${doc.permission === 'readonly' ? '
|
|
304
|
-
<span
|
|
302
|
+
<span>创建者: ${doc.creator.username}</span>
|
|
303
|
+
<span>${doc.permission === 'readonly' ? '🔒 只读' : '✏️ 可编辑'}</span>
|
|
304
|
+
<span>更新: ${new Date(doc.updatedAt).toLocaleString()}</span>
|
|
305
305
|
</div>
|
|
306
306
|
<button class="btn-edit" data-id="${doc._id}">
|
|
307
|
-
${doc.permission === 'readonly' ? '
|
|
307
|
+
${doc.permission === 'readonly' ? '查看' : '编辑'}
|
|
308
308
|
</button>
|
|
309
309
|
`;
|
|
310
310
|
docsList.appendChild(docCard);
|
|
@@ -323,25 +323,25 @@ export function renderUserDashboard(user, wsService) {
|
|
|
323
323
|
|
|
324
324
|
container.innerHTML = `
|
|
325
325
|
<div class="view-header">
|
|
326
|
-
<button class="btn-back" id="backBtn"
|
|
326
|
+
<button class="btn-back" id="backBtn">← 返回</button>
|
|
327
327
|
<h2>${doc.title}</h2>
|
|
328
|
-
<span class="doc-status">${doc.permission === 'readonly' ? '
|
|
328
|
+
<span class="doc-status">${doc.permission === 'readonly' ? '🔒 只读模式' : '✏️ 编辑模式'}</span>
|
|
329
329
|
</div>
|
|
330
330
|
<div class="editor-container">
|
|
331
331
|
<div class="editor-toolbar">
|
|
332
332
|
<div class="online-users" id="onlineUsers">
|
|
333
|
-
<span class="user-badge"
|
|
333
|
+
<span class="user-badge">👤 ${user.username}</span>
|
|
334
334
|
</div>
|
|
335
|
-
${doc.permission === 'editable' ? '<button class="btn-primary" id="saveBtn"
|
|
335
|
+
${doc.permission === 'editable' ? '<button class="btn-primary" id="saveBtn">保存</button>' : ''}
|
|
336
336
|
</div>
|
|
337
337
|
<div id="editor" ${doc.permission === 'readonly' ? 'class="readonly"' : ''}></div>
|
|
338
338
|
<div class="editor-footer">
|
|
339
|
-
<span
|
|
339
|
+
<span>最后编辑: ${new Date(doc.updatedAt).toLocaleString()}</span>
|
|
340
340
|
</div>
|
|
341
341
|
</div>
|
|
342
342
|
`;
|
|
343
343
|
|
|
344
|
-
//
|
|
344
|
+
// 初始化 Quill 编辑器
|
|
345
345
|
const quill = new Quill('#editor', {
|
|
346
346
|
theme: 'snow',
|
|
347
347
|
modules: {
|
|
@@ -357,15 +357,15 @@ export function renderUserDashboard(user, wsService) {
|
|
|
357
357
|
readOnly: doc.permission === 'readonly'
|
|
358
358
|
});
|
|
359
359
|
|
|
360
|
-
//
|
|
361
|
-
|
|
360
|
+
// 设置初始内容
|
|
361
|
+
quill.root.innerHTML = doc.content || '';
|
|
362
362
|
|
|
363
|
-
//
|
|
363
|
+
// 实时同步
|
|
364
364
|
if (doc.permission === 'editable') {
|
|
365
365
|
let typingTimeout;
|
|
366
366
|
let saveTimeout;
|
|
367
367
|
|
|
368
|
-
|
|
368
|
+
quill.on('text-change', () => {
|
|
369
369
|
clearTimeout(typingTimeout);
|
|
370
370
|
clearTimeout(saveTimeout);
|
|
371
371
|
wsService.sendTyping(documentId, user.username, true);
|
|
@@ -374,45 +374,45 @@ export function renderUserDashboard(user, wsService) {
|
|
|
374
374
|
wsService.sendTyping(documentId, user.username, false);
|
|
375
375
|
}, 1000);
|
|
376
376
|
|
|
377
|
-
//
|
|
377
|
+
// 自动保存
|
|
378
378
|
saveTimeout = setTimeout(async () => {
|
|
379
|
-
const content =
|
|
379
|
+
const content = quill.root.innerHTML;
|
|
380
380
|
try {
|
|
381
381
|
await apiService.updateDocument(documentId, content);
|
|
382
382
|
} catch (error) {
|
|
383
|
-
console.error('
|
|
383
|
+
console.error('自动保存失败:', error);
|
|
384
384
|
}
|
|
385
385
|
}, 2000);
|
|
386
386
|
});
|
|
387
387
|
|
|
388
388
|
document.getElementById('saveBtn').addEventListener('click', async () => {
|
|
389
389
|
try {
|
|
390
|
-
const content =
|
|
390
|
+
const content = quill.root.innerHTML;
|
|
391
391
|
await apiService.updateDocument(documentId, content);
|
|
392
|
-
alert('
|
|
392
|
+
alert('保存成功!');
|
|
393
393
|
} catch (error) {
|
|
394
|
-
alert('
|
|
394
|
+
alert('保存失败: ' + error.message);
|
|
395
395
|
}
|
|
396
396
|
});
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
-
//
|
|
399
|
+
// 监听文档更新
|
|
400
400
|
wsService.on('document_update', (data) => {
|
|
401
401
|
if (data.documentId === documentId && data.userId !== user.id) {
|
|
402
402
|
const selection = quill.getSelection();
|
|
403
|
-
|
|
403
|
+
quill.root.innerHTML = data.content;
|
|
404
404
|
if (selection) {
|
|
405
405
|
quill.setSelection(selection);
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
408
|
});
|
|
409
409
|
|
|
410
|
-
//
|
|
410
|
+
// 监听打字状态
|
|
411
411
|
wsService.on('typing', (data) => {
|
|
412
412
|
if (data.documentId === documentId && data.userId !== user.id) {
|
|
413
413
|
const onlineUsers = document.getElementById('onlineUsers');
|
|
414
414
|
if (data.isTyping) {
|
|
415
|
-
onlineUsers.innerHTML += `<span class="user-badge typing" data-user="${data.userId}"
|
|
415
|
+
onlineUsers.innerHTML += `<span class="user-badge typing" data-user="${data.userId}">✏️ ${data.username}</span>`;
|
|
416
416
|
} else {
|
|
417
417
|
const badge = onlineUsers.querySelector(`[data-user="${data.userId}"]`);
|
|
418
418
|
if (badge) badge.remove();
|
|
@@ -427,7 +427,7 @@ export function renderUserDashboard(user, wsService) {
|
|
|
427
427
|
|
|
428
428
|
async function renderFilesView(container) {
|
|
429
429
|
if (!currentGroup) {
|
|
430
|
-
container.innerHTML = '<div class="empty-state"
|
|
430
|
+
container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
|
|
431
431
|
return;
|
|
432
432
|
}
|
|
433
433
|
|
|
@@ -436,31 +436,31 @@ export function renderUserDashboard(user, wsService) {
|
|
|
436
436
|
|
|
437
437
|
container.innerHTML = `
|
|
438
438
|
<div class="view-header">
|
|
439
|
-
<h2
|
|
440
|
-
<button class="btn-primary" id="uploadFileBtn"
|
|
439
|
+
<h2>文件共享 - ${currentGroup.name}</h2>
|
|
440
|
+
<button class="btn-primary" id="uploadFileBtn">📤 上传文件</button>
|
|
441
441
|
</div>
|
|
442
442
|
<div class="files-list" id="filesList"></div>
|
|
443
443
|
|
|
444
|
-
<!--
|
|
444
|
+
<!-- 文件上传模态框 -->
|
|
445
445
|
<div class="modal hidden" id="uploadFileModal">
|
|
446
446
|
<div class="modal-content">
|
|
447
447
|
<div class="modal-header">
|
|
448
|
-
<h3
|
|
448
|
+
<h3>上传文件</h3>
|
|
449
449
|
<button class="modal-close" id="closeUploadModal">×</button>
|
|
450
450
|
</div>
|
|
451
451
|
<form id="uploadFileForm">
|
|
452
452
|
<div class="form-group">
|
|
453
|
-
<label
|
|
453
|
+
<label>选择文件</label>
|
|
454
454
|
<input type="file" id="fileInput" required>
|
|
455
|
-
<small
|
|
455
|
+
<small>支持图片、PDF、Word、Excel等,最大10MB</small>
|
|
456
456
|
</div>
|
|
457
457
|
<div class="form-group">
|
|
458
|
-
<label
|
|
459
|
-
<textarea id="fileDescription" rows="3" placeholder="
|
|
458
|
+
<label>描述(可选)</label>
|
|
459
|
+
<textarea id="fileDescription" rows="3" placeholder="文件描述..."></textarea>
|
|
460
460
|
</div>
|
|
461
461
|
<div class="form-actions">
|
|
462
|
-
<button type="button" class="btn-secondary" id="cancelUpload"
|
|
463
|
-
<button type="submit" class="btn-primary"
|
|
462
|
+
<button type="button" class="btn-secondary" id="cancelUpload">取消</button>
|
|
463
|
+
<button type="submit" class="btn-primary">上传</button>
|
|
464
464
|
</div>
|
|
465
465
|
</form>
|
|
466
466
|
</div>
|
|
@@ -470,7 +470,7 @@ export function renderUserDashboard(user, wsService) {
|
|
|
470
470
|
const filesList = document.getElementById('filesList');
|
|
471
471
|
|
|
472
472
|
if (!result.files || result.files.length === 0) {
|
|
473
|
-
filesList.innerHTML = '<div class="empty-state"
|
|
473
|
+
filesList.innerHTML = '<div class="empty-state">暂无文件</div>';
|
|
474
474
|
} else {
|
|
475
475
|
result.files.forEach(file => {
|
|
476
476
|
const fileCard = document.createElement('div');
|
|
@@ -484,37 +484,37 @@ export function renderUserDashboard(user, wsService) {
|
|
|
484
484
|
<div class="file-info">
|
|
485
485
|
<h4>${file.originalName}</h4>
|
|
486
486
|
<div class="file-meta">
|
|
487
|
-
<span
|
|
488
|
-
<span
|
|
489
|
-
<span
|
|
487
|
+
<span>上传者: ${file.uploader.username}</span>
|
|
488
|
+
<span>大小: ${fileSize}</span>
|
|
489
|
+
<span>时间: ${new Date(file.createdAt).toLocaleString()}</span>
|
|
490
490
|
</div>
|
|
491
491
|
${file.description ? `<p class="file-description">${file.description}</p>` : ''}
|
|
492
492
|
</div>
|
|
493
493
|
<div class="file-actions">
|
|
494
|
-
<a href="${apiService.getFileDownloadUrl(file._id)}" class="btn-primary" download
|
|
495
|
-
${file.uploader._id === currentUserId ? `<button class="btn-danger" data-id="${file._id}" data-action="delete-file"
|
|
494
|
+
<a href="${apiService.getFileDownloadUrl(file._id)}" class="btn-primary" download>下载</a>
|
|
495
|
+
${file.uploader._id === currentUserId ? `<button class="btn-danger" data-id="${file._id}" data-action="delete-file">删除</button>` : ''}
|
|
496
496
|
</div>
|
|
497
497
|
`;
|
|
498
498
|
filesList.appendChild(fileCard);
|
|
499
499
|
});
|
|
500
500
|
|
|
501
|
-
//
|
|
501
|
+
// 删除文件事件
|
|
502
502
|
document.querySelectorAll('[data-action="delete-file"]').forEach(btn => {
|
|
503
503
|
btn.addEventListener('click', async () => {
|
|
504
|
-
if (confirm('
|
|
504
|
+
if (confirm('确定要删除这个文件吗?')) {
|
|
505
505
|
try {
|
|
506
506
|
await apiService.deleteFile(btn.dataset.id);
|
|
507
|
-
alert('
|
|
507
|
+
alert('文件删除成功!');
|
|
508
508
|
await renderFilesView(container);
|
|
509
509
|
} catch (error) {
|
|
510
|
-
alert('
|
|
510
|
+
alert('删除失败: ' + error.message);
|
|
511
511
|
}
|
|
512
512
|
}
|
|
513
513
|
});
|
|
514
514
|
});
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
-
//
|
|
517
|
+
// 文件上传功能
|
|
518
518
|
document.getElementById('uploadFileBtn').addEventListener('click', () => {
|
|
519
519
|
document.getElementById('uploadFileModal').classList.remove('hidden');
|
|
520
520
|
});
|
|
@@ -535,38 +535,38 @@ export function renderUserDashboard(user, wsService) {
|
|
|
535
535
|
const description = document.getElementById('fileDescription').value;
|
|
536
536
|
|
|
537
537
|
if (!fileInput.files[0]) {
|
|
538
|
-
alert('
|
|
538
|
+
alert('请选择文件');
|
|
539
539
|
return;
|
|
540
540
|
}
|
|
541
541
|
|
|
542
542
|
try {
|
|
543
543
|
await apiService.uploadFile(currentGroup._id, fileInput.files[0], description);
|
|
544
|
-
alert('
|
|
544
|
+
alert('文件上传成功!');
|
|
545
545
|
document.getElementById('uploadFileModal').classList.add('hidden');
|
|
546
546
|
document.getElementById('uploadFileForm').reset();
|
|
547
547
|
await renderFilesView(container);
|
|
548
548
|
} catch (error) {
|
|
549
|
-
alert('
|
|
549
|
+
alert('上传失败: ' + error.message);
|
|
550
550
|
}
|
|
551
551
|
});
|
|
552
552
|
} catch (error) {
|
|
553
|
-
console.error('
|
|
553
|
+
console.error('获取文件列表失败:', error);
|
|
554
554
|
container.innerHTML = `
|
|
555
555
|
<div class="view-header">
|
|
556
|
-
<h2
|
|
556
|
+
<h2>文件共享</h2>
|
|
557
557
|
</div>
|
|
558
|
-
<div class="empty-state"
|
|
558
|
+
<div class="empty-state">加载文件失败: ${error.message}</div>
|
|
559
559
|
`;
|
|
560
560
|
}
|
|
561
561
|
}
|
|
562
562
|
|
|
563
563
|
function getFileIcon(mimetype) {
|
|
564
|
-
if (mimetype.startsWith('image/')) return '
|
|
565
|
-
if (mimetype === 'application/pdf') return '
|
|
566
|
-
if (mimetype.includes('word') || mimetype.includes('document')) return '
|
|
567
|
-
if (mimetype.includes('excel') || mimetype.includes('spreadsheet')) return '
|
|
568
|
-
if (mimetype.includes('zip') || mimetype.includes('compressed')) return '
|
|
569
|
-
return '
|
|
564
|
+
if (mimetype.startsWith('image/')) return '🖼️';
|
|
565
|
+
if (mimetype === 'application/pdf') return '📕';
|
|
566
|
+
if (mimetype.includes('word') || mimetype.includes('document')) return '📘';
|
|
567
|
+
if (mimetype.includes('excel') || mimetype.includes('spreadsheet')) return '📗';
|
|
568
|
+
if (mimetype.includes('zip') || mimetype.includes('compressed')) return '📦';
|
|
569
|
+
return '📄';
|
|
570
570
|
}
|
|
571
571
|
|
|
572
572
|
function formatFileSize(bytes) {
|
|
@@ -579,7 +579,7 @@ export function renderUserDashboard(user, wsService) {
|
|
|
579
579
|
|
|
580
580
|
async function renderChatView(container) {
|
|
581
581
|
if (!currentGroup) {
|
|
582
|
-
container.innerHTML = '<div class="empty-state"
|
|
582
|
+
container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
|
|
583
583
|
return;
|
|
584
584
|
}
|
|
585
585
|
|
|
@@ -591,14 +591,14 @@ export function renderUserDashboard(user, wsService) {
|
|
|
591
591
|
|
|
592
592
|
container.innerHTML = `
|
|
593
593
|
<div class="view-header">
|
|
594
|
-
<h2
|
|
594
|
+
<h2>群聊 - ${currentGroup.name}</h2>
|
|
595
595
|
</div>
|
|
596
596
|
<div class="chat-container">
|
|
597
597
|
<div class="messages" id="messages"></div>
|
|
598
598
|
<div class="chat-input">
|
|
599
|
-
<button class="btn-emoji" id="emojiBtn" ${canSpeak ? '' : 'disabled'}
|
|
600
|
-
<input type="text" id="messageInput" placeholder="${canSpeak ? '
|
|
601
|
-
<button class="btn-primary" id="sendBtn" ${canSpeak ? '' : 'disabled'}
|
|
599
|
+
<button class="btn-emoji" id="emojiBtn" ${canSpeak ? '' : 'disabled'}>😊</button>
|
|
600
|
+
<input type="text" id="messageInput" placeholder="${canSpeak ? '输入消息...' : (isMutedAll ? '全体禁言中,无法发言' : '你已被禁言')}" ${canSpeak ? '' : 'disabled'}>
|
|
601
|
+
<button class="btn-primary" id="sendBtn" ${canSpeak ? '' : 'disabled'}>发送</button>
|
|
602
602
|
</div>
|
|
603
603
|
<emoji-picker id="emojiPicker" class="hidden"></emoji-picker>
|
|
604
604
|
</div>
|
|
@@ -608,7 +608,7 @@ export function renderUserDashboard(user, wsService) {
|
|
|
608
608
|
const messageInput = document.getElementById('messageInput');
|
|
609
609
|
const sendBtn = document.getElementById('sendBtn');
|
|
610
610
|
|
|
611
|
-
//
|
|
611
|
+
// 加载历史消息
|
|
612
612
|
try {
|
|
613
613
|
const messagesResult = await apiService.getGroupMessages(currentGroup._id);
|
|
614
614
|
if (messagesResult.messages) {
|
|
@@ -627,10 +627,10 @@ export function renderUserDashboard(user, wsService) {
|
|
|
627
627
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
628
628
|
}
|
|
629
629
|
} catch (err) {
|
|
630
|
-
console.error('
|
|
630
|
+
console.error('加载历史消息失败:', err);
|
|
631
631
|
}
|
|
632
632
|
|
|
633
|
-
//
|
|
633
|
+
// 表情包功能
|
|
634
634
|
const emojiBtn = document.getElementById('emojiBtn');
|
|
635
635
|
const emojiPicker = document.getElementById('emojiPicker');
|
|
636
636
|
|
|
@@ -644,15 +644,15 @@ export function renderUserDashboard(user, wsService) {
|
|
|
644
644
|
emojiPicker.classList.add('hidden');
|
|
645
645
|
});
|
|
646
646
|
|
|
647
|
-
//
|
|
647
|
+
// 点击外部关闭表情选择器
|
|
648
648
|
document.addEventListener('click', (e) => {
|
|
649
649
|
if (!emojiBtn.contains(e.target) && !emojiPicker.contains(e.target)) {
|
|
650
650
|
emojiPicker.classList.add('hidden');
|
|
651
651
|
}
|
|
652
652
|
});
|
|
653
653
|
|
|
654
|
-
//
|
|
655
|
-
function showNotification(title, body, icon = '
|
|
654
|
+
// 消息通知系统
|
|
655
|
+
function showNotification(title, body, icon = '💬') {
|
|
656
656
|
if ('Notification' in window && Notification.permission === 'granted') {
|
|
657
657
|
new Notification(title, {
|
|
658
658
|
body: body,
|
|
@@ -663,12 +663,12 @@ export function renderUserDashboard(user, wsService) {
|
|
|
663
663
|
}
|
|
664
664
|
}
|
|
665
665
|
|
|
666
|
-
//
|
|
666
|
+
// 请求通知权限
|
|
667
667
|
if ('Notification' in window && Notification.permission === 'default') {
|
|
668
668
|
Notification.requestPermission();
|
|
669
669
|
}
|
|
670
670
|
|
|
671
|
-
//
|
|
671
|
+
// 监听消息
|
|
672
672
|
wsService.on('chat_message', (data) => {
|
|
673
673
|
if (data.groupId === currentGroup._id) {
|
|
674
674
|
const messageEl = document.createElement('div');
|
|
@@ -683,35 +683,35 @@ export function renderUserDashboard(user, wsService) {
|
|
|
683
683
|
messagesDiv.appendChild(messageEl);
|
|
684
684
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
685
685
|
|
|
686
|
-
//
|
|
686
|
+
// 显示通知(如果不是自己发送的消息)
|
|
687
687
|
if (data.userId !== currentUserId) {
|
|
688
|
-
showNotification(`${data.username}
|
|
688
|
+
showNotification(`${data.username} 在 ${currentGroup.name}`, data.content);
|
|
689
689
|
}
|
|
690
690
|
}
|
|
691
691
|
});
|
|
692
692
|
|
|
693
|
-
//
|
|
693
|
+
// 服务端拦截提示(比如被禁言/未加入群组)
|
|
694
694
|
wsService.on('chat_blocked', (data) => {
|
|
695
695
|
if (data.groupId === currentGroup._id) {
|
|
696
696
|
const notificationEl = document.createElement('div');
|
|
697
697
|
notificationEl.className = 'notification';
|
|
698
|
-
notificationEl.textContent = data.message || '
|
|
698
|
+
notificationEl.textContent = data.message || '消息发送失败';
|
|
699
699
|
messagesDiv.appendChild(notificationEl);
|
|
700
700
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
701
701
|
}
|
|
702
702
|
});
|
|
703
703
|
|
|
704
|
-
//
|
|
704
|
+
// 监听点名通知
|
|
705
705
|
wsService.on('call_response', (data) => {
|
|
706
706
|
if (data.groupId === currentGroup._id) {
|
|
707
707
|
const notificationEl = document.createElement('div');
|
|
708
708
|
notificationEl.className = 'notification';
|
|
709
|
-
notificationEl.textContent = `${data.username}
|
|
709
|
+
notificationEl.textContent = `${data.username} 已响应点名`;
|
|
710
710
|
messagesDiv.appendChild(notificationEl);
|
|
711
711
|
}
|
|
712
712
|
});
|
|
713
713
|
|
|
714
|
-
//
|
|
714
|
+
// 发送消息
|
|
715
715
|
const sendMessage = () => {
|
|
716
716
|
const content = messageInput.value.trim();
|
|
717
717
|
if (content) {
|
|
@@ -729,22 +729,22 @@ export function renderUserDashboard(user, wsService) {
|
|
|
729
729
|
async function renderSearchView(container) {
|
|
730
730
|
container.innerHTML = `
|
|
731
731
|
<div class="view-header">
|
|
732
|
-
<h2
|
|
732
|
+
<h2>🔍 搜索</h2>
|
|
733
733
|
</div>
|
|
734
734
|
<div class="search-container">
|
|
735
735
|
<div class="search-box">
|
|
736
|
-
<input type="text" id="searchInput" placeholder="
|
|
737
|
-
<button class="btn-primary" id="searchBtn"
|
|
736
|
+
<input type="text" id="searchInput" placeholder="搜索消息、文档、任务...">
|
|
737
|
+
<button class="btn-primary" id="searchBtn">搜索</button>
|
|
738
738
|
</div>
|
|
739
739
|
<div class="search-filters">
|
|
740
740
|
<label>
|
|
741
|
-
<input type="checkbox" id="filterMessages" checked>
|
|
741
|
+
<input type="checkbox" id="filterMessages" checked> 消息
|
|
742
742
|
</label>
|
|
743
743
|
<label>
|
|
744
|
-
<input type="checkbox" id="filterDocuments" checked>
|
|
744
|
+
<input type="checkbox" id="filterDocuments" checked> 文档
|
|
745
745
|
</label>
|
|
746
746
|
<label>
|
|
747
|
-
<input type="checkbox" id="filterTasks" checked>
|
|
747
|
+
<input type="checkbox" id="filterTasks" checked> 任务
|
|
748
748
|
</label>
|
|
749
749
|
</div>
|
|
750
750
|
<div class="search-results" id="searchResults"></div>
|
|
@@ -758,7 +758,7 @@ export function renderUserDashboard(user, wsService) {
|
|
|
758
758
|
const performSearch = async () => {
|
|
759
759
|
const query = searchInput.value.trim();
|
|
760
760
|
if (!query) {
|
|
761
|
-
searchResults.innerHTML = '<div class="empty-state"
|
|
761
|
+
searchResults.innerHTML = '<div class="empty-state">请输入搜索关键词</div>';
|
|
762
762
|
return;
|
|
763
763
|
}
|
|
764
764
|
|
|
@@ -768,12 +768,12 @@ export function renderUserDashboard(user, wsService) {
|
|
|
768
768
|
tasks: document.getElementById('filterTasks').checked
|
|
769
769
|
};
|
|
770
770
|
|
|
771
|
-
searchResults.innerHTML = '<div class="loading"
|
|
771
|
+
searchResults.innerHTML = '<div class="loading">搜索中...</div>';
|
|
772
772
|
|
|
773
773
|
try {
|
|
774
774
|
const results = [];
|
|
775
775
|
|
|
776
|
-
//
|
|
776
|
+
// 搜索消息
|
|
777
777
|
if (filters.messages && currentGroup) {
|
|
778
778
|
try {
|
|
779
779
|
const messagesResult = await apiService.getGroupMessages(currentGroup._id);
|
|
@@ -784,7 +784,7 @@ export function renderUserDashboard(user, wsService) {
|
|
|
784
784
|
matchedMessages.forEach(msg => {
|
|
785
785
|
results.push({
|
|
786
786
|
type: 'message',
|
|
787
|
-
title:
|
|
787
|
+
title: `消息 - ${msg.username}`,
|
|
788
788
|
content: msg.content,
|
|
789
789
|
time: msg.timestamp,
|
|
790
790
|
group: currentGroup.name
|
|
@@ -792,11 +792,11 @@ export function renderUserDashboard(user, wsService) {
|
|
|
792
792
|
});
|
|
793
793
|
}
|
|
794
794
|
} catch (err) {
|
|
795
|
-
console.error('
|
|
795
|
+
console.error('搜索消息失败:', err);
|
|
796
796
|
}
|
|
797
797
|
}
|
|
798
798
|
|
|
799
|
-
//
|
|
799
|
+
// 搜索文档
|
|
800
800
|
if (filters.documents) {
|
|
801
801
|
try {
|
|
802
802
|
if (currentGroup) {
|
|
@@ -819,11 +819,11 @@ export function renderUserDashboard(user, wsService) {
|
|
|
819
819
|
}
|
|
820
820
|
}
|
|
821
821
|
} catch (err) {
|
|
822
|
-
console.error('
|
|
822
|
+
console.error('搜索文档失败:', err);
|
|
823
823
|
}
|
|
824
824
|
}
|
|
825
825
|
|
|
826
|
-
//
|
|
826
|
+
// 搜索任务
|
|
827
827
|
if (filters.tasks) {
|
|
828
828
|
try {
|
|
829
829
|
const tasksResult = await apiService.getMyTasks();
|
|
@@ -844,36 +844,36 @@ export function renderUserDashboard(user, wsService) {
|
|
|
844
844
|
});
|
|
845
845
|
}
|
|
846
846
|
} catch (err) {
|
|
847
|
-
console.error('
|
|
847
|
+
console.error('搜索任务失败:', err);
|
|
848
848
|
}
|
|
849
849
|
}
|
|
850
850
|
|
|
851
|
-
//
|
|
851
|
+
// 显示结果
|
|
852
852
|
if (results.length === 0) {
|
|
853
|
-
searchResults.innerHTML = '<div class="empty-state"
|
|
853
|
+
searchResults.innerHTML = '<div class="empty-state">未找到相关结果</div>';
|
|
854
854
|
} else {
|
|
855
855
|
searchResults.innerHTML = results.map(result => {
|
|
856
856
|
const typeIcon = {
|
|
857
|
-
message: '
|
|
858
|
-
document: '
|
|
859
|
-
task: '
|
|
857
|
+
message: '💬',
|
|
858
|
+
document: '📄',
|
|
859
|
+
task: '📋'
|
|
860
860
|
};
|
|
861
861
|
return `
|
|
862
862
|
<div class="search-result-item">
|
|
863
863
|
<div class="result-header">
|
|
864
|
-
<span class="result-type">${typeIcon[result.type]} ${result.type === 'message' ? '
|
|
864
|
+
<span class="result-type">${typeIcon[result.type]} ${result.type === 'message' ? '消息' : result.type === 'document' ? '文档' : '任务'}</span>
|
|
865
865
|
<span class="result-time">${new Date(result.time).toLocaleString()}</span>
|
|
866
866
|
</div>
|
|
867
867
|
<h4>${highlightText(result.title, query)}</h4>
|
|
868
868
|
<p>${highlightText(result.content, query)}</p>
|
|
869
|
-
${result.group ? `<span class="result-group"
|
|
870
|
-
${result.status ? `<span class="result-status"
|
|
869
|
+
${result.group ? `<span class="result-group">群组: ${result.group}</span>` : ''}
|
|
870
|
+
${result.status ? `<span class="result-status">状态: ${getStatusText(result.status)}</span>` : ''}
|
|
871
871
|
</div>
|
|
872
872
|
`;
|
|
873
873
|
}).join('');
|
|
874
874
|
}
|
|
875
875
|
} catch (error) {
|
|
876
|
-
searchResults.innerHTML = `<div class="empty-state"
|
|
876
|
+
searchResults.innerHTML = `<div class="empty-state">搜索失败: ${error.message}</div>`;
|
|
877
877
|
}
|
|
878
878
|
};
|
|
879
879
|
|
|
@@ -891,10 +891,10 @@ export function renderUserDashboard(user, wsService) {
|
|
|
891
891
|
|
|
892
892
|
function getStatusText(status) {
|
|
893
893
|
const statusMap = {
|
|
894
|
-
'pending': '
|
|
895
|
-
'in_progress': '
|
|
896
|
-
'completed': '
|
|
897
|
-
'terminated': '
|
|
894
|
+
'pending': '待处理',
|
|
895
|
+
'in_progress': '进行中',
|
|
896
|
+
'completed': '已完成',
|
|
897
|
+
'terminated': '已终止'
|
|
898
898
|
};
|
|
899
899
|
return statusMap[status] || status;
|
|
900
900
|
}
|