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