collabdocchat 1.2.12 → 2.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.
Files changed (61) hide show
  1. package/README.md +219 -218
  2. package/index.html +2 -0
  3. package/install-and-start.bat +5 -0
  4. package/install-and-start.sh +5 -0
  5. package/package.json +9 -2
  6. package/scripts/pre-publish-check.js +213 -0
  7. package/scripts/start-app.js +15 -15
  8. package/server/index.js +38 -6
  9. package/server/middleware/cache.js +115 -0
  10. package/server/middleware/errorHandler.js +209 -0
  11. package/server/models/Document.js +66 -59
  12. package/server/models/File.js +49 -43
  13. package/server/models/Group.js +6 -0
  14. package/server/models/KnowledgeBase.js +254 -0
  15. package/server/models/Message.js +43 -0
  16. package/server/models/Task.js +87 -55
  17. package/server/models/User.js +67 -60
  18. package/server/models/Workflow.js +249 -0
  19. package/server/routes/ai.js +327 -0
  20. package/server/routes/audit.js +245 -210
  21. package/server/routes/backup.js +108 -0
  22. package/server/routes/chunked-upload.js +343 -0
  23. package/server/routes/export.js +440 -0
  24. package/server/routes/files.js +294 -218
  25. package/server/routes/groups.js +182 -0
  26. package/server/routes/knowledge.js +509 -0
  27. package/server/routes/tasks.js +257 -110
  28. package/server/routes/workflows.js +380 -0
  29. package/server/utils/backup.js +439 -0
  30. package/server/utils/cache.js +223 -0
  31. package/server/utils/workflow-engine.js +479 -0
  32. package/server/websocket/enhanced.js +509 -0
  33. package/server/websocket/index.js +233 -1
  34. package/src/components/knowledge-modal.js +485 -0
  35. package/src/components/optimized-poll-detail.js +724 -0
  36. package/src/main.js +5 -0
  37. package/src/pages/admin-dashboard.js +2248 -44
  38. package/src/pages/optimized-backup-view.js +616 -0
  39. package/src/pages/optimized-knowledge-view.js +803 -0
  40. package/src/pages/optimized-task-detail.js +843 -0
  41. package/src/pages/optimized-workflow-view.js +806 -0
  42. package/src/pages/simplified-workflows.js +651 -0
  43. package/src/pages/user-dashboard.js +677 -58
  44. package/src/services/api.js +65 -1
  45. package/src/services/websocket.js +124 -16
  46. package/src/styles/collaboration-modern.js +708 -0
  47. package/src/styles/enhancements.css +392 -0
  48. package/src/styles/main.css +620 -1420
  49. package/src/styles/responsive.css +1000 -0
  50. package/src/styles/sidebar-fix.css +60 -0
  51. package/src/utils/ai-assistant.js +1398 -0
  52. package/src/utils/chat-enhancements.js +509 -0
  53. package/src/utils/collaboration-enhancer.js +1151 -0
  54. package/src/utils/feature-integrator.js +1724 -0
  55. package/src/utils/onboarding-guide.js +734 -0
  56. package/src/utils/performance.js +394 -0
  57. package/src/utils/permission-manager.js +890 -0
  58. package/src/utils/responsive-handler.js +491 -0
  59. package/src/utils/theme-manager.js +811 -0
  60. package/src/utils/ui-enhancements-loader.js +329 -0
  61. package/USAGE.md +0 -298
@@ -1,14 +1,24 @@
1
1
  import { ApiService } from '../services/api.js';
2
2
  import { AuthService } from '../services/auth.js';
3
+ import { FeatureIntegrator } from '../utils/feature-integrator.js';
4
+ import { uiEnhancementsLoader } from '../utils/ui-enhancements-loader.js';
5
+ import { renderOptimizedTaskDetail } from './optimized-task-detail.js';
6
+ import { renderOptimizedKnowledgeView } from './optimized-knowledge-view.js';
3
7
  // import Quill from 'quill/dist/quill.js';
4
8
  // import 'quill/dist/quill.snow.css';
5
9
  import 'emoji-picker-element';
6
10
 
11
+ // 自动加载所有UI美化增强
12
+ uiEnhancementsLoader.init();
13
+
7
14
  export function renderAdminDashboard(user, wsService) {
8
15
  const app = document.getElementById('app');
9
16
  const apiService = new ApiService();
10
17
  const authService = new AuthService();
11
18
  const currentUserId = user.id || user._id;
19
+
20
+ // 初始化功能集成器
21
+ const features = new FeatureIntegrator(apiService, wsService, user);
12
22
 
13
23
  let currentGroup = null;
14
24
  let groups = [];
@@ -39,12 +49,21 @@ export function renderAdminDashboard(user, wsService) {
39
49
  <button class="nav-item" data-view="documents">
40
50
  <span class="icon">📄</span> 文档管理
41
51
  </button>
52
+ <button class="nav-item" data-view="knowledge">
53
+ <span class="icon">📚</span> 知识库
54
+ </button>
55
+ <button class="nav-item" data-view="workflows">
56
+ <span class="icon">🔄</span> 工作流
57
+ </button>
42
58
  <button class="nav-item" data-view="files">
43
59
  <span class="icon">📎</span> 文件管理
44
60
  </button>
45
61
  <button class="nav-item" data-view="chat">
46
62
  <span class="icon">💬</span> 群聊
47
63
  </button>
64
+ <button class="nav-item" data-view="backup">
65
+ <span class="icon">💾</span> 备份管理
66
+ </button>
48
67
  <button class="nav-item" data-view="search">
49
68
  <span class="icon">🔍</span> 搜索
50
69
  </button>
@@ -56,6 +75,15 @@ export function renderAdminDashboard(user, wsService) {
56
75
  </button>
57
76
  </nav>
58
77
 
78
+ <div class="sidebar-footer">
79
+ <button class="nav-item" data-view="settings">
80
+ <span class="icon">⚙️</span> 设置
81
+ </button>
82
+ <button class="nav-item" data-view="help">
83
+ <span class="icon">❓</span> 帮助
84
+ </button>
85
+ </div>
86
+
59
87
  <button class="btn-logout" id="logoutBtn">退出登录</button>
60
88
  </aside>
61
89
 
@@ -93,12 +121,26 @@ export function renderAdminDashboard(user, wsService) {
93
121
  case 'documents':
94
122
  await renderDocumentsView(contentArea);
95
123
  break;
124
+ case 'knowledge':
125
+ await renderOptimizedKnowledgeView(contentArea, currentGroup, apiService);
126
+ break;
127
+ case 'workflows':
128
+ // 使用优化后的工作流视图(可视化创建,无需JSON代码)
129
+ if (typeof window.renderOptimizedWorkflowView === 'function') {
130
+ await window.renderOptimizedWorkflowView(contentArea, currentGroup, apiService);
131
+ } else {
132
+ await renderWorkflowsView(contentArea);
133
+ }
134
+ break;
96
135
  case 'chat':
97
136
  await renderChatView(contentArea);
98
137
  break;
99
138
  case 'files':
100
139
  await renderFilesView(contentArea);
101
140
  break;
141
+ case 'backup':
142
+ await renderOptimizedBackupView(contentArea);
143
+ break;
102
144
  case 'search':
103
145
  await renderSearchView(contentArea);
104
146
  break;
@@ -108,6 +150,12 @@ export function renderAdminDashboard(user, wsService) {
108
150
  case 'audit':
109
151
  await renderAuditView(contentArea);
110
152
  break;
153
+ case 'settings':
154
+ features.renderSettingsView(contentArea);
155
+ break;
156
+ case 'help':
157
+ features.renderHelpView(contentArea);
158
+ break;
111
159
  }
112
160
  }
113
161
 
@@ -353,6 +401,17 @@ export function renderAdminDashboard(user, wsService) {
353
401
  </form>
354
402
  </div>
355
403
  </div>
404
+ <div id="taskDetailModal" class="modal hidden">
405
+ <div class="modal-content" style="max-width: 900px;">
406
+ <div class="modal-header">
407
+ <h3>任务完成情况</h3>
408
+ <button class="modal-close" id="closeTaskDetailModal">&times;</button>
409
+ </div>
410
+ <div class="modal-body" id="taskDetailContent">
411
+ <div class="loading">加载中...</div>
412
+ </div>
413
+ </div>
414
+ </div>
356
415
  `;
357
416
 
358
417
  const tasksList = document.getElementById('tasksList');
@@ -362,6 +421,12 @@ export function renderAdminDashboard(user, wsService) {
362
421
  result.tasks.forEach(task => {
363
422
  const taskCard = document.createElement('div');
364
423
  taskCard.className = `task-card status-${task.status}`;
424
+
425
+ // 计算完成情况
426
+ const totalMembers = task.assignedTo ? task.assignedTo.length : 0;
427
+ const completedMembers = task.completedBy ? task.completedBy.length : 0;
428
+ const completionRate = totalMembers > 0 ? Math.round((completedMembers / totalMembers) * 100) : 0;
429
+
365
430
  taskCard.innerHTML = `
366
431
  <div style="display: flex; justify-content: space-between; align-items: start;">
367
432
  <div style="flex: 1;">
@@ -370,14 +435,27 @@ export function renderAdminDashboard(user, wsService) {
370
435
  <div class="task-meta">
371
436
  <span class="status-badge">${getStatusText(task.status)}</span>
372
437
  <span>截止: ${task.deadline ? new Date(task.deadline).toLocaleDateString() : '无'}</span>
438
+ <span>完成率: ${completionRate}% (${completedMembers}/${totalMembers})</span>
373
439
  </div>
374
440
  </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>
441
+ <div style="display: flex; gap: 10px;">
442
+ <button class="btn-primary btn-sm" data-id="${task._id}" data-action="view-detail" title="查看详细">📊 详情</button>
443
+ <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>
444
+ </div>
376
445
  </div>
377
446
  `;
378
447
  tasksList.appendChild(taskCard);
379
448
  });
380
449
 
450
+ // 添加查看详情事件
451
+ document.querySelectorAll('[data-action="view-detail"]').forEach(btn => {
452
+ btn.addEventListener('click', async (e) => {
453
+ e.stopPropagation();
454
+ const taskId = btn.dataset.id;
455
+ await showTaskDetail(taskId);
456
+ });
457
+ });
458
+
381
459
  // 添加删除任务事件
382
460
  document.querySelectorAll('[data-action="delete-task"]').forEach(btn => {
383
461
  btn.addEventListener('click', async (e) => {
@@ -430,6 +508,105 @@ export function renderAdminDashboard(user, wsService) {
430
508
  });
431
509
  }
432
510
 
511
+ // 显示任务详情(使用美化后的组件)
512
+ async function showTaskDetail(taskId) {
513
+ const modal = document.getElementById('taskDetailModal');
514
+ const content = document.getElementById('taskDetailContent');
515
+
516
+ // 先设置关闭事件(只设置一次,避免重复)
517
+ const closeBtn = document.getElementById('closeTaskDetailModal');
518
+ if (closeBtn && !closeBtn.dataset.listenerSet) {
519
+ closeBtn.addEventListener('click', () => {
520
+ modal.classList.add('hidden');
521
+ });
522
+ closeBtn.dataset.listenerSet = 'true';
523
+ }
524
+
525
+ // 点击模态框外部关闭(只设置一次)
526
+ if (!modal.dataset.listenerSet) {
527
+ modal.addEventListener('click', (e) => {
528
+ if (e.target === modal) {
529
+ modal.classList.add('hidden');
530
+ }
531
+ });
532
+ modal.dataset.listenerSet = 'true';
533
+ }
534
+
535
+ try {
536
+ modal.classList.remove('hidden');
537
+ content.innerHTML = '<div class="loading">加载中...</div>';
538
+
539
+ // 获取任务详情
540
+ const taskResult = await apiService.getTask(taskId);
541
+ const task = taskResult.task;
542
+
543
+ // 验证必要数据
544
+ if (!task) {
545
+ throw new Error('任务数据不存在');
546
+ }
547
+
548
+ // 群组信息已经在task.group中(后端已populate)
549
+ const group = task.group;
550
+ if (!group || !group.members) {
551
+ throw new Error('群组数据不完整');
552
+ }
553
+
554
+ // 计算完成情况
555
+ const totalMembers = task.assignedTo ? task.assignedTo.length : 0;
556
+ const completedMembers = task.completedBy ? task.completedBy.length : 0;
557
+
558
+ // 创建成员完成情况列表 - 安全处理所有可能的undefined
559
+ const completedIds = new Set((task.completedBy || []).map(m => {
560
+ if (!m || !m.user) return null;
561
+ const userId = m.user._id ? String(m.user._id) : String(m.user);
562
+ return userId;
563
+ }).filter(id => id !== null));
564
+
565
+ const assignedMembers = (task.assignedTo || []).map(assignedId => {
566
+ if (!assignedId) return null;
567
+ const memberId = assignedId._id ? String(assignedId._id) : String(assignedId);
568
+ const member = group.members.find(m => m && m._id && String(m._id) === memberId);
569
+
570
+ // 查找完成时间
571
+ const completedInfo = task.completedBy ? task.completedBy.find(c => {
572
+ if (!c || !c.user) return false;
573
+ const userId = c.user._id ? String(c.user._id) : String(c.user);
574
+ return userId === memberId;
575
+ }) : null;
576
+
577
+ return {
578
+ id: memberId,
579
+ username: member ? member.username : '未知用户',
580
+ completed: completedIds.has(memberId),
581
+ completedAt: completedInfo && completedInfo.completedAt ? completedInfo.completedAt : null,
582
+ status: completedIds.has(memberId) ? 'completed' : 'pending'
583
+ };
584
+ }).filter(m => m !== null);
585
+
586
+ // 准备任务数据给美化组件
587
+ const taskData = {
588
+ ...task,
589
+ group: group.name,
590
+ assignedTo: task.assignedTo && task.assignedTo.length > 0 && task.assignedTo[0].username ?
591
+ task.assignedTo[0].username : '未分配',
592
+ members: assignedMembers,
593
+ completedCount: completedMembers
594
+ };
595
+
596
+ // 使用美化后的任务详情组件
597
+ renderOptimizedTaskDetail(taskData, content);
598
+
599
+ } catch (error) {
600
+ console.error('加载任务详情失败:', error);
601
+ content.innerHTML = `
602
+ <div class="error-state">
603
+ <h3>❌ 加载失败</h3>
604
+ <p>${error.message || '未知错误'}</p>
605
+ <button class="btn-primary" onclick="document.getElementById('taskDetailModal').classList.add('hidden')">关闭</button>
606
+ </div>
607
+ `;
608
+ }
609
+ }
433
610
  async function renderDocumentsView(container) {
434
611
  if (!currentGroup) {
435
612
  container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
@@ -722,13 +899,53 @@ export function renderAdminDashboard(user, wsService) {
722
899
  ${file.description ? `<p class="file-description">${file.description}</p>` : ''}
723
900
  </div>
724
901
  <div class="file-actions">
725
- <a href="${apiService.getFileDownloadUrl(file._id)}" class="btn-primary" download>下载</a>
902
+ <button class="btn-primary" data-id="${file._id}" data-name="${file.originalName}" data-action="download-file">下载</button>
726
903
  <button class="btn-danger" data-id="${file._id}" data-action="delete-file">删除</button>
727
904
  </div>
728
905
  `;
729
906
  filesList.appendChild(fileCard);
730
907
  });
731
908
 
909
+ // 下载文件事件
910
+ document.querySelectorAll('[data-action="download-file"]').forEach(btn => {
911
+ btn.addEventListener('click', async () => {
912
+ try {
913
+ const fileId = btn.dataset.id;
914
+ const fileName = btn.dataset.name;
915
+ const token = localStorage.getItem('token');
916
+
917
+ // 使用 fetch 下载文件
918
+ const response = await fetch(`http://localhost:8765/api/files/${fileId}/download`, {
919
+ method: 'GET',
920
+ headers: {
921
+ 'Authorization': `Bearer ${token}`
922
+ }
923
+ });
924
+
925
+ if (!response.ok) {
926
+ throw new Error('下载失败');
927
+ }
928
+
929
+ // 获取文件内容
930
+ const blob = await response.blob();
931
+
932
+ // 创建下载链接
933
+ const url = window.URL.createObjectURL(blob);
934
+ const a = document.createElement('a');
935
+ a.href = url;
936
+ a.download = fileName;
937
+ document.body.appendChild(a);
938
+ a.click();
939
+
940
+ // 清理
941
+ window.URL.revokeObjectURL(url);
942
+ document.body.removeChild(a);
943
+ } catch (error) {
944
+ alert('下载失败: ' + error.message);
945
+ }
946
+ });
947
+ });
948
+
732
949
  // 删除文件事件
733
950
  document.querySelectorAll('[data-action="delete-file"]').forEach(btn => {
734
951
  btn.addEventListener('click', async () => {
@@ -801,11 +1018,13 @@ export function renderAdminDashboard(user, wsService) {
801
1018
  }
802
1019
 
803
1020
  function formatFileSize(bytes) {
804
- if (bytes === 0) return '0 Bytes';
1021
+ const n = Number(bytes);
1022
+ if (!Number.isFinite(n) || n < 0) return '-';
1023
+ if (n === 0) return '0 Bytes';
805
1024
  const k = 1024;
806
1025
  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];
1026
+ const i = Math.floor(Math.log(n) / Math.log(k));
1027
+ return Math.round(n / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
809
1028
  }
810
1029
 
811
1030
  async function renderChatView(container) {
@@ -820,16 +1039,26 @@ export function renderAdminDashboard(user, wsService) {
820
1039
  container.innerHTML = `
821
1040
  <div class="view-header">
822
1041
  <h2>群聊 - ${currentGroup.name}</h2>
823
- <div style="display: flex; gap: 10px;">
1042
+ <div style="display: flex; gap: 10px; flex-wrap: wrap;">
1043
+ <button class="btn-secondary" id="showAIAssistant" title="AI 助手">
1044
+ 🤖 AI
1045
+ </button>
1046
+ <button class="btn-secondary" id="showCollabTools" title="协作工具">
1047
+ 🛠️ 工具
1048
+ </button>
1049
+ <button class="btn-secondary" id="markAllRead" title="全部标记为已读">
1050
+ ✓ 已读
1051
+ </button>
824
1052
  <button class="btn-secondary" id="muteAllBtn">全体禁言</button>
825
1053
  <button class="btn-secondary" id="manageMuteBtn">个人禁言</button>
1054
+ <button class="btn-danger" id="clearChatBtn">清除聊天记录</button>
826
1055
  </div>
827
1056
  </div>
828
1057
  <div class="chat-container">
829
1058
  <div class="messages" id="messages"></div>
830
1059
  <div class="chat-input">
831
1060
  <button class="btn-emoji" id="emojiBtn">😊</button>
832
- <input type="text" id="messageInput" placeholder="输入消息...">
1061
+ <input type="text" id="messageInput" placeholder="输入消息... (使用 @ 提及用户)">
833
1062
  <button class="btn-primary" id="sendBtn">发送</button>
834
1063
  </div>
835
1064
  <emoji-picker id="emojiPicker" class="hidden"></emoji-picker>
@@ -841,6 +1070,40 @@ export function renderAdminDashboard(user, wsService) {
841
1070
  <button type="button" class="btn-secondary" id="closeMuteModal">关闭</button>
842
1071
  </div>
843
1072
  </div>
1073
+ <div id="clearChatModal" class="modal hidden">
1074
+ <div class="modal-content">
1075
+ <h3>清除聊天记录</h3>
1076
+ <div style="padding: 20px;">
1077
+ <p style="margin-bottom: 20px; color: var(--danger);">⚠️ 警告:此操作不可恢复!</p>
1078
+ <div class="form-group">
1079
+ <label>
1080
+ <input type="radio" name="clearType" value="all" checked>
1081
+ 清除所有聊天记录
1082
+ </label>
1083
+ </div>
1084
+ <div class="form-group">
1085
+ <label>
1086
+ <input type="radio" name="clearType" value="before">
1087
+ 清除指定日期之前的记录
1088
+ </label>
1089
+ <input type="date" id="clearBeforeDate" style="margin-left: 10px; margin-top: 10px;" disabled>
1090
+ </div>
1091
+ <div class="form-group">
1092
+ <label>
1093
+ <input type="radio" name="clearType" value="user">
1094
+ 清除指定用户的消息
1095
+ </label>
1096
+ <select id="clearUserId" style="margin-left: 10px; margin-top: 10px;" disabled>
1097
+ <option value="">选择用户</option>
1098
+ </select>
1099
+ </div>
1100
+ </div>
1101
+ <div style="display: flex; gap: 10px; justify-content: flex-end; padding: 0 20px 20px;">
1102
+ <button type="button" class="btn-secondary" id="closeClearModal">取消</button>
1103
+ <button type="button" class="btn-danger" id="confirmClearBtn">确认清除</button>
1104
+ </div>
1105
+ </div>
1106
+ </div>
844
1107
  `;
845
1108
 
846
1109
  const messagesDiv = document.getElementById('messages');
@@ -851,6 +1114,44 @@ export function renderAdminDashboard(user, wsService) {
851
1114
  let mutedUsers = new Set((group.mutedUsers || []).map(String));
852
1115
  let isMutedAll = Boolean(group.mutedAll);
853
1116
 
1117
+ // 设置聊天增强功能
1118
+ features.chatEnhancements.setCurrentGroup(currentGroup._id);
1119
+
1120
+ // 设置@提及功能
1121
+ if (group.members) {
1122
+ features.chatEnhancements.setupMentionInput(messageInput, group.members);
1123
+ }
1124
+
1125
+ // AI 助手按钮
1126
+ document.getElementById('showAIAssistant').addEventListener('click', () => {
1127
+ const panel = document.createElement('div');
1128
+ panel.className = 'modal';
1129
+ panel.innerHTML = '<div class="modal-content" style="max-width: 800px;"></div>';
1130
+ document.body.appendChild(panel);
1131
+
1132
+ features.aiAssistant.renderAssistantPanel(panel.querySelector('.modal-content'));
1133
+
1134
+ panel.addEventListener('click', (e) => {
1135
+ if (e.target === panel) panel.remove();
1136
+ });
1137
+ });
1138
+
1139
+ // 协作工具按钮
1140
+ document.getElementById('showCollabTools').addEventListener('click', () => {
1141
+ features.showCollaborationTools(currentGroup._id);
1142
+ });
1143
+
1144
+ // 标记全部已读
1145
+ document.getElementById('markAllRead').addEventListener('click', async () => {
1146
+ const messages = Array.from(messagesDiv.querySelectorAll('.message[data-message-id]'))
1147
+ .map(el => el.dataset.messageId);
1148
+
1149
+ if (messages.length > 0) {
1150
+ await features.chatEnhancements.markAllAsRead(messages);
1151
+ alert('已标记所有消息为已读');
1152
+ }
1153
+ });
1154
+
854
1155
  // 表情包功能
855
1156
  emojiBtn.addEventListener('click', () => {
856
1157
  emojiPicker.classList.toggle('hidden');
@@ -891,16 +1192,8 @@ export function renderAdminDashboard(user, wsService) {
891
1192
  const messagesResult = await apiService.getGroupMessages(currentGroup._id);
892
1193
  if (messagesResult.messages) {
893
1194
  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);
1195
+ // 使用增强的消息渲染
1196
+ features.chatEnhancements.renderMessage(msg, messagesDiv, group.members);
904
1197
  });
905
1198
  messagesDiv.scrollTop = messagesDiv.scrollHeight;
906
1199
  }
@@ -979,20 +1272,131 @@ export function renderAdminDashboard(user, wsService) {
979
1272
  }
980
1273
  };
981
1274
 
1275
+ // 清除聊天记录功能
1276
+ document.getElementById('clearChatBtn').addEventListener('click', async () => {
1277
+ // 加载群组成员到下拉列表
1278
+ const clearUserId = document.getElementById('clearUserId');
1279
+ clearUserId.innerHTML = '<option value="">选择用户</option>';
1280
+ group.members.forEach(member => {
1281
+ clearUserId.innerHTML += `<option value="${member._id}">${member.username}</option>`;
1282
+ });
1283
+
1284
+ document.getElementById('clearChatModal').classList.remove('hidden');
1285
+ });
1286
+
1287
+ document.getElementById('closeClearModal').addEventListener('click', () => {
1288
+ document.getElementById('clearChatModal').classList.add('hidden');
1289
+ });
1290
+
1291
+ // 清除类型切换
1292
+ document.querySelectorAll('input[name="clearType"]').forEach(radio => {
1293
+ radio.addEventListener('change', (e) => {
1294
+ document.getElementById('clearBeforeDate').disabled = e.target.value !== 'before';
1295
+ document.getElementById('clearUserId').disabled = e.target.value !== 'user';
1296
+ });
1297
+ });
1298
+
1299
+ // 确认清除
1300
+ document.getElementById('confirmClearBtn').addEventListener('click', async () => {
1301
+ const clearType = document.querySelector('input[name="clearType"]:checked').value;
1302
+
1303
+ let confirmMsg = '';
1304
+ if (clearType === 'all') {
1305
+ confirmMsg = '确定要清除所有聊天记录吗?此操作不可恢复!';
1306
+ } else if (clearType === 'before') {
1307
+ const date = document.getElementById('clearBeforeDate').value;
1308
+ if (!date) {
1309
+ alert('请选择日期');
1310
+ return;
1311
+ }
1312
+ confirmMsg = `确定要清除 ${date} 之前的所有聊天记录吗?此操作不可恢复!`;
1313
+ } else if (clearType === 'user') {
1314
+ const userId = document.getElementById('clearUserId').value;
1315
+ if (!userId) {
1316
+ alert('请选择用户');
1317
+ return;
1318
+ }
1319
+ const username = group.members.find(m => m._id === userId)?.username;
1320
+ confirmMsg = `确定要清除用户 ${username} 的所有消息吗?此操作不可恢复!`;
1321
+ }
1322
+
1323
+ if (!confirm(confirmMsg)) {
1324
+ return;
1325
+ }
1326
+
1327
+ try {
1328
+ let result;
1329
+ if (clearType === 'all') {
1330
+ result = await apiService.clearChatMessages(currentGroup._id, { deleteAll: true });
1331
+ } else if (clearType === 'before') {
1332
+ const beforeDate = document.getElementById('clearBeforeDate').value;
1333
+ result = await apiService.clearChatMessages(currentGroup._id, { beforeDate });
1334
+ } else if (clearType === 'user') {
1335
+ const userId = document.getElementById('clearUserId').value;
1336
+ result = await apiService.clearUserMessages(currentGroup._id, userId);
1337
+ }
1338
+
1339
+ alert(`清除成功!共删除 ${result.deletedCount} 条消息`);
1340
+ document.getElementById('clearChatModal').classList.add('hidden');
1341
+
1342
+ // 刷新消息列表
1343
+ messagesDiv.innerHTML = '';
1344
+ const messagesResult = await apiService.getGroupMessages(currentGroup._id);
1345
+ if (messagesResult.messages) {
1346
+ messagesResult.messages.forEach(msg => {
1347
+ // 使用增强的消息渲染
1348
+ features.chatEnhancements.renderMessage(msg, messagesDiv, group.members);
1349
+ });
1350
+ }
1351
+ } catch (error) {
1352
+ alert('清除失败: ' + error.message);
1353
+ }
1354
+ });
1355
+
982
1356
  // 监听消息
983
1357
  wsService.on('chat_message', (data) => {
984
1358
  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);
1359
+ // 使用增强的消息渲染
1360
+ features.chatEnhancements.renderMessage(data, messagesDiv, group.members);
995
1361
  messagesDiv.scrollTop = messagesDiv.scrollHeight;
1362
+
1363
+ // 显示通知(如果不是自己发送的消息)
1364
+ if (data.userId !== currentUserId) {
1365
+ features.notifications.showNewMessageNotification(data.username, data.content);
1366
+
1367
+ // 检查是否@了当前用户
1368
+ if (features.chatEnhancements.checkMentionsMe(data.content, group.members)) {
1369
+ features.notifications.showMentionNotification(data.username, data.content);
1370
+ }
1371
+ }
1372
+ }
1373
+ });
1374
+
1375
+ // 监听消息撤回
1376
+ features.chatEnhancements.onMessageRecalled((data) => {
1377
+ if (data.groupId === currentGroup._id) {
1378
+ const messageEl = messagesDiv.querySelector(`[data-message-id="${data.messageId}"]`);
1379
+ if (messageEl) {
1380
+ const contentEl = messageEl.querySelector('.message-content');
1381
+ contentEl.textContent = '[消息已撤回]';
1382
+ contentEl.classList.add('recalled');
1383
+
1384
+ const recallBtn = messageEl.querySelector('.btn-recall');
1385
+ if (recallBtn) recallBtn.remove();
1386
+ }
1387
+ }
1388
+ });
1389
+
1390
+ // 监听已读回执
1391
+ features.chatEnhancements.onMessageRead((data) => {
1392
+ if (data.groupId === currentGroup._id) {
1393
+ const messageEl = messagesDiv.querySelector(`[data-message-id="${data.messageId}"]`);
1394
+ if (messageEl) {
1395
+ const statusEl = messageEl.querySelector('.message-status');
1396
+ if (statusEl) {
1397
+ statusEl.innerHTML = `<span class="read-status read">已读 ${data.readCount}</span>`;
1398
+ }
1399
+ }
996
1400
  }
997
1401
  });
998
1402
 
@@ -1011,8 +1415,19 @@ export function renderAdminDashboard(user, wsService) {
1011
1415
  const sendMessage = () => {
1012
1416
  const content = messageInput.value.trim();
1013
1417
  if (content) {
1014
- wsService.sendChatMessage(currentGroup._id, user.username, content);
1418
+ // 解析@提及
1419
+ const mentions = features.chatEnhancements.parseMentions(content, group.members);
1420
+
1421
+ wsService.sendChatMessage(currentGroup._id, user.username, content, mentions);
1015
1422
  messageInput.value = '';
1423
+
1424
+ // 显示AI智能回复建议(如果启用)
1425
+ if (localStorage.getItem('ai_smartReplies') !== 'false') {
1426
+ features.aiAssistant.getSmartReplies(content).then(replies => {
1427
+ // 可以在这里显示智能回复建议
1428
+ console.log('智能回复建议:', replies);
1429
+ });
1430
+ }
1016
1431
  }
1017
1432
  };
1018
1433
 
@@ -1228,24 +1643,119 @@ export function renderAdminDashboard(user, wsService) {
1228
1643
  // 显示操作详情
1229
1644
  window.showAuditDetail = async (logId) => {
1230
1645
  try {
1231
- // 这里我们需要从已加载的日志中找到对应的记录
1232
- // 在实际应用中可能需要单独的API来获取详情
1233
1646
  const modal = document.getElementById('auditDetailModal');
1234
1647
  const content = document.getElementById('auditDetailContent');
1235
1648
 
1236
- // 简单实现:显示基本信息
1649
+ modal.classList.remove('hidden');
1650
+ content.innerHTML = '<div class="loading">加载中...</div>';
1651
+
1652
+ // 获取审计日志详情
1653
+ const result = await apiService.getAuditLogDetail(logId);
1654
+ const log = result.log;
1655
+
1656
+ // 格式化详细信息
1657
+ let detailsHtml = '';
1658
+ if (log.details) {
1659
+ detailsHtml = Object.entries(log.details).map(([key, value]) => {
1660
+ let displayValue = value;
1661
+
1662
+ // 特殊处理某些字段
1663
+ if (key === 'oldContent' || key === 'newContent') {
1664
+ // 截断过长的内容
1665
+ if (typeof value === 'string' && value.length > 200) {
1666
+ displayValue = value.substring(0, 200) + '...';
1667
+ }
1668
+ } else if (typeof value === 'object') {
1669
+ displayValue = JSON.stringify(value, null, 2);
1670
+ }
1671
+
1672
+ return `
1673
+ <div class="info-row">
1674
+ <span class="info-label">${formatFieldName(key)}:</span>
1675
+ <span class="info-value">${displayValue || '-'}</span>
1676
+ </div>
1677
+ `;
1678
+ }).join('');
1679
+ }
1680
+
1237
1681
  content.innerHTML = `
1238
1682
  <div class="audit-detail">
1239
- <p>操作ID: ${logId}</p>
1240
- <p>详细信息加载中...</p>
1683
+ <div class="task-detail-info">
1684
+ <div class="info-row">
1685
+ <span class="info-label">操作时间:</span>
1686
+ <span class="info-value">${new Date(log.createdAt).toLocaleString()}</span>
1687
+ </div>
1688
+ <div class="info-row">
1689
+ <span class="info-label">操作用户:</span>
1690
+ <span class="info-value">${log.user?.username || '未知用户'}</span>
1691
+ </div>
1692
+ <div class="info-row">
1693
+ <span class="info-label">用户角色:</span>
1694
+ <span class="info-value">${log.user?.role === 'admin' ? '管理员' : '普通用户'}</span>
1695
+ </div>
1696
+ <div class="info-row">
1697
+ <span class="info-label">操作类型:</span>
1698
+ <span class="info-value">
1699
+ <span class="action-badge action-${log.action}">${getActionText(log.action)}</span>
1700
+ </span>
1701
+ </div>
1702
+ <div class="info-row">
1703
+ <span class="info-label">所属群组:</span>
1704
+ <span class="info-value">${log.metadata?.groupId?.name || '-'}</span>
1705
+ </div>
1706
+ <div class="info-row">
1707
+ <span class="info-label">资源类型:</span>
1708
+ <span class="info-value">${log.resourceType || '-'}</span>
1709
+ </div>
1710
+ <div class="info-row">
1711
+ <span class="info-label">资源标题:</span>
1712
+ <span class="info-value">${log.resourceTitle || log.resourceId || '-'}</span>
1713
+ </div>
1714
+ <div class="info-row">
1715
+ <span class="info-label">IP地址:</span>
1716
+ <span class="info-value">${log.metadata?.ipAddress || '-'}</span>
1717
+ </div>
1718
+ </div>
1719
+
1720
+ ${detailsHtml ? `
1721
+ <div class="task-detail-info" style="margin-top: 20px;">
1722
+ <h4 style="margin-bottom: 15px; color: var(--primary);">详细信息</h4>
1723
+ ${detailsHtml}
1724
+ </div>
1725
+ ` : ''}
1726
+
1727
+ ${log.details?.description ? `
1728
+ <div class="task-detail-info" style="margin-top: 20px;">
1729
+ <h4 style="margin-bottom: 15px; color: var(--primary);">操作描述</h4>
1730
+ <p style="color: var(--text-primary); line-height: 1.6;">${log.details.description}</p>
1731
+ </div>
1732
+ ` : ''}
1241
1733
  </div>
1242
1734
  `;
1243
1735
 
1244
- modal.classList.remove('hidden');
1245
1736
  } catch (error) {
1246
- alert('加载详情失败: ' + error.message);
1737
+ console.error('加载审计详情失败:', error);
1738
+ const content = document.getElementById('auditDetailContent');
1739
+ content.innerHTML = `<div class="error-state">加载失败: ${error.message}</div>`;
1247
1740
  }
1248
1741
  };
1742
+
1743
+ // 格式化字段名
1744
+ function formatFieldName(key) {
1745
+ const fieldNames = {
1746
+ 'description': '描述',
1747
+ 'oldTitle': '原标题',
1748
+ 'newTitle': '新标题',
1749
+ 'oldContent': '原内容',
1750
+ 'newContent': '新内容',
1751
+ 'oldPermission': '原权限',
1752
+ 'newPermission': '新权限',
1753
+ 'contentLength': '内容长度',
1754
+ 'changes': '变更内容',
1755
+ 'reason': '原因'
1756
+ };
1757
+ return fieldNames[key] || key;
1758
+ }
1249
1759
 
1250
1760
  // 事件监听
1251
1761
  document.getElementById('applyFilters').addEventListener('click', () => {
@@ -1478,16 +1988,1710 @@ export function renderAdminDashboard(user, wsService) {
1478
1988
  return actionMap[action] || action;
1479
1989
  }
1480
1990
 
1481
- function getStatusText(status) {
1482
- const statusMap = {
1483
- 'pending': '待处理',
1484
- 'in_progress': '进行中',
1485
- 'completed': '已完成',
1486
- 'terminated': '已终止'
1487
- };
1488
- return statusMap[status] || status;
1489
- }
1991
+ // 备份管理视图
1992
+ async function renderBackupView(container) {
1993
+ try {
1994
+ const token = localStorage.getItem('token');
1995
+
1996
+ // 获取备份列表
1997
+ const backupsResponse = await fetch('http://localhost:8765/api/backup/list', {
1998
+ headers: { 'Authorization': `Bearer ${token}` }
1999
+ });
2000
+ const backupsResult = await backupsResponse.json();
2001
+
2002
+ // 获取备份配置
2003
+ const configResponse = await fetch('http://localhost:8765/api/backup/config', {
2004
+ headers: { 'Authorization': `Bearer ${token}` }
2005
+ });
2006
+ const configResult = await configResponse.json();
2007
+
2008
+ container.innerHTML = `
2009
+ <div class="view-header">
2010
+ <h2>💾 备份管理</h2>
2011
+ <div style="display: flex; gap: 10px;">
2012
+ <button class="btn-primary" id="createBackupBtn">立即备份</button>
2013
+ <button class="btn-secondary" id="configBackupBtn">备份设置</button>
2014
+ </div>
2015
+ </div>
2016
+
2017
+ <div class="backup-stats">
2018
+ <div class="stat-card">
2019
+ <h3>备份总数</h3>
2020
+ <div class="stat-number">${backupsResult.data?.backups?.length || 0}</div>
2021
+ </div>
2022
+ <div class="stat-card">
2023
+ <h3>自动备份</h3>
2024
+ <div class="stat-number">${configResult.data?.config?.autoBackup ? '✅ 已启用' : '❌ 已禁用'}</div>
2025
+ </div>
2026
+ <div class="stat-card">
2027
+ <h3>备份频率</h3>
2028
+ <div class="stat-number">${configResult.data?.config?.schedule || '未设置'}</div>
2029
+ </div>
2030
+ <div class="stat-card">
2031
+ <h3>保留天数</h3>
2032
+ <div class="stat-number">${configResult.data?.config?.retention || 30} 天</div>
2033
+ </div>
2034
+ </div>
2035
+
2036
+ <div class="backup-list" id="backupList">
2037
+ ${backupsResult.data?.backups?.length > 0 ? `
2038
+ <div class="backups-table">
2039
+ <div class="backup-header">
2040
+ <div>备份时间</div>
2041
+ <div>类型</div>
2042
+ <div>大小</div>
2043
+ <div>状态</div>
2044
+ <div>操作</div>
2045
+ </div>
2046
+ ${backupsResult.data.backups.map(backup => `
2047
+ <div class="backup-row">
2048
+ <div>${backup.createdAt ? new Date(backup.createdAt).toLocaleString() : '-'}</div>
2049
+ <div>
2050
+ <span class="backup-type-badge ${backup.type}">
2051
+ ${backup.type === 'manual' ? '手动' : backup.type === 'scheduled' ? '定时' : '自动'}
2052
+ </span>
2053
+ </div>
2054
+ <div>${formatFileSize(backup.size)}</div>
2055
+ <div>
2056
+ <span class="status-badge status-${backup.status}">
2057
+ ${backup.status === 'completed' ? '✅ 完成' : backup.status === 'failed' ? '❌ 失败' : '⏳ 进行中'}
2058
+ </span>
2059
+ </div>
2060
+ <div class="backup-actions">
2061
+ ${backup.status === 'completed' ? `
2062
+ <button class="btn-primary btn-sm" data-id="${backup._id}" data-action="download-backup">下载</button>
2063
+ <button class="btn-secondary btn-sm" data-id="${backup._id}" data-action="restore-backup">恢复</button>
2064
+ ` : ''}
2065
+ <button class="btn-danger btn-sm" data-id="${backup._id}" data-action="delete-backup">删除</button>
2066
+ </div>
2067
+ </div>
2068
+ `).join('')}
2069
+ </div>
2070
+ ` : '<div class="empty-state">暂无备份记录<br>点击"立即备份"创建第一个备份</div>'}
2071
+ </div>
2072
+
2073
+ <!-- 创建备份模态框 -->
2074
+ <div class="modal hidden" id="createBackupModal">
2075
+ <div class="modal-content">
2076
+ <div class="modal-header">
2077
+ <h3>创建备份</h3>
2078
+ <button class="modal-close" id="closeCreateBackup">&times;</button>
2079
+ </div>
2080
+ <form id="createBackupForm">
2081
+ <div class="form-group">
2082
+ <label>备份类型</label>
2083
+ <select id="backupType" required>
2084
+ <option value="full">完整备份(所有数据)</option>
2085
+ <option value="incremental">增量备份(仅变更)</option>
2086
+ </select>
2087
+ </div>
2088
+ <div class="form-group">
2089
+ <label>备份说明(可选)</label>
2090
+ <textarea id="backupDescription" rows="3" placeholder="备份说明..."></textarea>
2091
+ </div>
2092
+ <div class="form-actions">
2093
+ <button type="button" class="btn-secondary" id="cancelCreateBackup">取消</button>
2094
+ <button type="submit" class="btn-primary">开始备份</button>
2095
+ </div>
2096
+ </form>
2097
+ </div>
2098
+ </div>
2099
+
2100
+ <!-- 备份配置模态框 -->
2101
+ <div class="modal hidden" id="configBackupModal">
2102
+ <div class="modal-content">
2103
+ <div class="modal-header">
2104
+ <h3>备份设置</h3>
2105
+ <button class="modal-close" id="closeConfigBackup">&times;</button>
2106
+ </div>
2107
+ <form id="configBackupForm">
2108
+ <div class="form-group">
2109
+ <label>
2110
+ <input type="checkbox" id="autoBackup" ${configResult.data?.config?.autoBackup ? 'checked' : ''}>
2111
+ 启用自动备份
2112
+ </label>
2113
+ </div>
2114
+ <div class="form-group">
2115
+ <label>备份频率(Cron表达式)</label>
2116
+ <input type="text" id="backupSchedule" value="${configResult.data?.config?.schedule || '0 2 * * *'}" placeholder="0 2 * * * (每天凌晨2点)">
2117
+ <small>示例:0 2 * * * (每天凌晨2点),0 */6 * * * (每6小时)</small>
2118
+ </div>
2119
+ <div class="form-group">
2120
+ <label>保留天数</label>
2121
+ <input type="number" id="backupRetention" value="${configResult.data?.config?.retention || 30}" min="1" max="365">
2122
+ <small>超过此天数的备份将自动删除</small>
2123
+ </div>
2124
+ <div class="form-group">
2125
+ <label>最大备份数量</label>
2126
+ <input type="number" id="maxBackups" value="${configResult.data?.config?.maxBackups || 10}" min="1" max="100">
2127
+ </div>
2128
+ <div class="form-actions">
2129
+ <button type="button" class="btn-secondary" id="cancelConfigBackup">取消</button>
2130
+ <button type="submit" class="btn-primary">保存设置</button>
2131
+ </div>
2132
+ </form>
2133
+ </div>
2134
+ </div>
2135
+ `;
2136
+
2137
+ // 立即备份
2138
+ document.getElementById('createBackupBtn').addEventListener('click', () => {
2139
+ document.getElementById('createBackupModal').classList.remove('hidden');
2140
+ });
2141
+
2142
+ document.getElementById('closeCreateBackup').addEventListener('click', () => {
2143
+ document.getElementById('createBackupModal').classList.add('hidden');
2144
+ });
2145
+
2146
+ document.getElementById('cancelCreateBackup').addEventListener('click', () => {
2147
+ document.getElementById('createBackupModal').classList.add('hidden');
2148
+ });
2149
+
2150
+ document.getElementById('createBackupForm').addEventListener('submit', async (e) => {
2151
+ e.preventDefault();
2152
+ const type = document.getElementById('backupType').value;
2153
+ const description = document.getElementById('backupDescription').value;
2154
+
2155
+ try {
2156
+ const response = await fetch('http://localhost:8765/api/backup/create', {
2157
+ method: 'POST',
2158
+ headers: {
2159
+ 'Content-Type': 'application/json',
2160
+ 'Authorization': `Bearer ${token}`
2161
+ },
2162
+ body: JSON.stringify({ type, description })
2163
+ });
2164
+
2165
+ const result = await response.json();
2166
+ if (result.success) {
2167
+ alert('备份创建成功!');
2168
+ document.getElementById('createBackupModal').classList.add('hidden');
2169
+ renderBackupView(container);
2170
+ } else {
2171
+ alert('备份失败: ' + result.error.message);
2172
+ }
2173
+ } catch (error) {
2174
+ alert('备份失败: ' + error.message);
2175
+ }
2176
+ });
2177
+
2178
+ // 备份设置
2179
+ document.getElementById('configBackupBtn').addEventListener('click', () => {
2180
+ document.getElementById('configBackupModal').classList.remove('hidden');
2181
+ });
2182
+
2183
+ document.getElementById('closeConfigBackup').addEventListener('click', () => {
2184
+ document.getElementById('configBackupModal').classList.add('hidden');
2185
+ });
2186
+
2187
+ document.getElementById('cancelConfigBackup').addEventListener('click', () => {
2188
+ document.getElementById('configBackupModal').classList.add('hidden');
2189
+ });
2190
+
2191
+ document.getElementById('configBackupForm').addEventListener('submit', async (e) => {
2192
+ e.preventDefault();
2193
+ const config = {
2194
+ autoBackup: document.getElementById('autoBackup').checked,
2195
+ schedule: document.getElementById('backupSchedule').value,
2196
+ retention: parseInt(document.getElementById('backupRetention').value),
2197
+ maxBackups: parseInt(document.getElementById('maxBackups').value)
2198
+ };
2199
+
2200
+ try {
2201
+ const response = await fetch('http://localhost:8765/api/backup/config', {
2202
+ method: 'PUT',
2203
+ headers: {
2204
+ 'Content-Type': 'application/json',
2205
+ 'Authorization': `Bearer ${token}`
2206
+ },
2207
+ body: JSON.stringify(config)
2208
+ });
2209
+
2210
+ const result = await response.json();
2211
+ if (result.success) {
2212
+ alert('设置保存成功!');
2213
+ document.getElementById('configBackupModal').classList.add('hidden');
2214
+ renderBackupView(container);
2215
+ } else {
2216
+ alert('保存失败: ' + result.error.message);
2217
+ }
2218
+ } catch (error) {
2219
+ alert('保存失败: ' + error.message);
2220
+ }
2221
+ });
2222
+
2223
+ // 下载备份
2224
+ document.querySelectorAll('[data-action="download-backup"]').forEach(btn => {
2225
+ btn.addEventListener('click', async () => {
2226
+ const backupId = btn.dataset.id;
2227
+ window.location.href = `http://localhost:8765/api/backup/${backupId}/download?token=${token}`;
2228
+ });
2229
+ });
2230
+
2231
+ // 恢复备份
2232
+ document.querySelectorAll('[data-action="restore-backup"]').forEach(btn => {
2233
+ btn.addEventListener('click', async () => {
2234
+ if (!confirm('确定要恢复此备份吗?这将覆盖当前数据!')) {
2235
+ return;
2236
+ }
2237
+
2238
+ const backupId = btn.dataset.id;
2239
+ try {
2240
+ const response = await fetch(`http://localhost:8765/api/backup/${backupId}/restore`, {
2241
+ method: 'POST',
2242
+ headers: { 'Authorization': `Bearer ${token}` }
2243
+ });
2244
+
2245
+ const result = await response.json();
2246
+ if (result.success) {
2247
+ alert('备份恢复成功!页面将刷新...');
2248
+ setTimeout(() => window.location.reload(), 2000);
2249
+ } else {
2250
+ alert('恢复失败: ' + result.error.message);
2251
+ }
2252
+ } catch (error) {
2253
+ alert('恢复失败: ' + error.message);
2254
+ }
2255
+ });
2256
+ });
2257
+
2258
+ // 删除备份
2259
+ document.querySelectorAll('[data-action="delete-backup"]').forEach(btn => {
2260
+ btn.addEventListener('click', async () => {
2261
+ if (!confirm('确定要删除此备份吗?')) {
2262
+ return;
2263
+ }
2264
+
2265
+ const backupId = btn.dataset.id;
2266
+ try {
2267
+ const response = await fetch(`http://localhost:8765/api/backup/${backupId}`, {
2268
+ method: 'DELETE',
2269
+ headers: { 'Authorization': `Bearer ${token}` }
2270
+ });
2271
+
2272
+ const result = await response.json();
2273
+ if (result.success) {
2274
+ alert('备份删除成功!');
2275
+ renderBackupView(container);
2276
+ } else {
2277
+ alert('删除失败: ' + result.error.message);
2278
+ }
2279
+ } catch (error) {
2280
+ alert('删除失败: ' + error.message);
2281
+ }
2282
+ });
2283
+ });
2284
+
2285
+ } catch (error) {
2286
+ console.error('加载备份管理失败:', error);
2287
+ container.innerHTML = `
2288
+ <div class="view-header">
2289
+ <h2>💾 备份管理</h2>
2290
+ </div>
2291
+ <div class="empty-state">加载失败: ${error.message}</div>
2292
+ `;
2293
+ }
2294
+ }
2295
+
2296
+ // 知识库视图(管理员)
2297
+ async function renderKnowledgeView(container) {
2298
+ if (!currentGroup) {
2299
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
2300
+ return;
2301
+ }
2302
+
2303
+ try {
2304
+ const token = localStorage.getItem('token');
2305
+ const response = await fetch(`http://localhost:8765/api/knowledge/group/${currentGroup._id}`, {
2306
+ headers: { 'Authorization': `Bearer ${token}` }
2307
+ });
2308
+ const result = await response.json();
2309
+
2310
+ container.innerHTML = `
2311
+ <div class="view-header">
2312
+ <h2>📚 知识库管理 - ${currentGroup.name}</h2>
2313
+ <button class="btn-primary" id="createKnowledgeBtn">+ 新建文档</button>
2314
+ </div>
2315
+ <div class="knowledge-list" id="knowledgeList"></div>
2316
+ `;
2317
+
2318
+ const knowledgeList = document.getElementById('knowledgeList');
2319
+
2320
+ if (!result.data || !result.data.knowledgeList || result.data.knowledgeList.length === 0) {
2321
+ knowledgeList.innerHTML = '<div class="empty-state">暂无知识库文档</div>';
2322
+ } else {
2323
+ result.data.knowledgeList.forEach(kb => {
2324
+ const kbCard = document.createElement('div');
2325
+ kbCard.className = 'knowledge-card';
2326
+ kbCard.innerHTML = `
2327
+ <div class="kb-header">
2328
+ <h3>${kb.title}</h3>
2329
+ <span class="kb-status ${kb.status}">${kb.status === 'published' ? '已发布' : kb.status === 'draft' ? '草稿' : '已归档'}</span>
2330
+ </div>
2331
+ <div class="kb-meta">
2332
+ <span>📁 ${kb.category || '未分类'}</span>
2333
+ <span>👁️ ${kb.views || 0} 浏览</span>
2334
+ <span>👍 ${kb.likes ? kb.likes.length : 0} 点赞</span>
2335
+ </div>
2336
+ <div class="kb-actions">
2337
+ <button class="btn-primary btn-sm" data-id="${kb._id}" data-action="view-kb">查看</button>
2338
+ <button class="btn-secondary btn-sm" data-id="${kb._id}" data-action="edit-kb">编辑</button>
2339
+ <button class="btn-danger btn-sm" data-id="${kb._id}" data-action="delete-kb">删除</button>
2340
+ </div>
2341
+ `;
2342
+ knowledgeList.appendChild(kbCard);
2343
+ });
2344
+
2345
+ // 查看、编辑、删除事件
2346
+ document.querySelectorAll('[data-action="view-kb"]').forEach(btn => {
2347
+ btn.addEventListener('click', async () => {
2348
+ await showKnowledgeDetail(btn.dataset.id);
2349
+ });
2350
+ });
2351
+
2352
+ document.querySelectorAll('[data-action="edit-kb"]').forEach(btn => {
2353
+ btn.addEventListener('click', async () => {
2354
+ await showEditKnowledgeModal(btn.dataset.id);
2355
+ });
2356
+ });
2357
+
2358
+ document.querySelectorAll('[data-action="delete-kb"]').forEach(btn => {
2359
+ btn.addEventListener('click', async () => {
2360
+ if (confirm('确定要删除这个知识库文档吗?')) {
2361
+ try {
2362
+ await fetch(`http://localhost:8765/api/knowledge/${btn.dataset.id}`, {
2363
+ method: 'DELETE',
2364
+ headers: { 'Authorization': `Bearer ${token}` }
2365
+ });
2366
+ alert('删除成功!');
2367
+ renderKnowledgeView(container);
2368
+ } catch (error) {
2369
+ alert('删除失败: ' + error.message);
2370
+ }
2371
+ }
2372
+ });
2373
+ });
2374
+ }
2375
+
2376
+ document.getElementById('createKnowledgeBtn').addEventListener('click', () => {
2377
+ showCreateKnowledgeModal();
2378
+ });
2379
+
2380
+ } catch (error) {
2381
+ console.error('加载知识库失败:', error);
2382
+ container.innerHTML = `
2383
+ <div class="view-header">
2384
+ <h2>📚 知识库管理</h2>
2385
+ </div>
2386
+ <div class="empty-state">加载失败: ${error.message}</div>
2387
+ `;
2388
+ }
2389
+ }
2390
+
2391
+ // 显示创建知识库文档模态框
2392
+ function showCreateKnowledgeModal() {
2393
+ const modal = document.createElement('div');
2394
+ modal.className = 'modal';
2395
+ modal.id = 'createKnowledgeModal';
2396
+ modal.innerHTML = `
2397
+ <div class="modal-content" style="max-width: 800px;">
2398
+ <div class="modal-header">
2399
+ <h3>📚 创建知识库文档</h3>
2400
+ <button class="close-btn" id="closeCreateKnowledge">&times;</button>
2401
+ </div>
2402
+ <form id="createKnowledgeForm">
2403
+ <div class="form-group">
2404
+ <label>标题 *</label>
2405
+ <input type="text" id="kbTitle" required placeholder="请输入文档标题">
2406
+ </div>
2407
+ <div class="form-group">
2408
+ <label>分类</label>
2409
+ <input type="text" id="kbCategory" placeholder="例如:技术文档、产品说明等">
2410
+ </div>
2411
+ <div class="form-group">
2412
+ <label>标签(用逗号分隔)</label>
2413
+ <input type="text" id="kbTags" placeholder="例如:前端,React,教程">
2414
+ </div>
2415
+ <div class="form-group">
2416
+ <label>内容 *</label>
2417
+ <textarea id="kbContent" rows="10" required placeholder="请输入文档内容"></textarea>
2418
+ </div>
2419
+ <div class="form-group">
2420
+ <label>状态</label>
2421
+ <select id="kbStatus">
2422
+ <option value="draft">草稿</option>
2423
+ <option value="published">发布</option>
2424
+ </select>
2425
+ </div>
2426
+ <div class="form-group">
2427
+ <label>阅读权限</label>
2428
+ <select id="kbReadPermission">
2429
+ <option value="group">群组成员可见</option>
2430
+ <option value="public">公开</option>
2431
+ <option value="private">仅自己可见</option>
2432
+ </select>
2433
+ </div>
2434
+ <div class="form-group">
2435
+ <label>编辑权限</label>
2436
+ <select id="kbWritePermission">
2437
+ <option value="author">仅作者</option>
2438
+ <option value="admin">管理员</option>
2439
+ <option value="all">所有成员</option>
2440
+ </select>
2441
+ </div>
2442
+ <div style="display: flex; gap: 10px; margin-top: 20px;">
2443
+ <button type="submit" class="btn-primary">创建文档</button>
2444
+ <button type="button" class="btn-secondary" id="cancelCreateKnowledge">取消</button>
2445
+ </div>
2446
+ </form>
2447
+ </div>
2448
+ `;
2449
+
2450
+ document.body.appendChild(modal);
2451
+
2452
+ // 关闭按钮
2453
+ document.getElementById('closeCreateKnowledge').addEventListener('click', () => {
2454
+ modal.remove();
2455
+ });
2456
+
2457
+ document.getElementById('cancelCreateKnowledge').addEventListener('click', () => {
2458
+ modal.remove();
2459
+ });
2460
+
2461
+ // 点击背景关闭
2462
+ modal.addEventListener('click', (e) => {
2463
+ if (e.target === modal) {
2464
+ modal.remove();
2465
+ }
2466
+ });
2467
+
2468
+ // 提交表单
2469
+ document.getElementById('createKnowledgeForm').addEventListener('submit', async (e) => {
2470
+ e.preventDefault();
2471
+
2472
+ const title = document.getElementById('kbTitle').value.trim();
2473
+ const category = document.getElementById('kbCategory').value.trim();
2474
+ const tagsInput = document.getElementById('kbTags').value.trim();
2475
+ const content = document.getElementById('kbContent').value.trim();
2476
+ const status = document.getElementById('kbStatus').value;
2477
+ const readPermission = document.getElementById('kbReadPermission').value;
2478
+ const writePermission = document.getElementById('kbWritePermission').value;
2479
+
2480
+ if (!title || !content) {
2481
+ alert('标题和内容不能为空!');
2482
+ return;
2483
+ }
2484
+
2485
+ const tags = tagsInput ? tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag) : [];
2486
+
2487
+ try {
2488
+ const token = localStorage.getItem('token');
2489
+ const response = await fetch('http://localhost:8765/api/knowledge', {
2490
+ method: 'POST',
2491
+ headers: {
2492
+ 'Content-Type': 'application/json',
2493
+ 'Authorization': `Bearer ${token}`
2494
+ },
2495
+ body: JSON.stringify({
2496
+ title,
2497
+ content,
2498
+ category: category || '未分类',
2499
+ tags,
2500
+ groupId: currentGroup._id,
2501
+ status,
2502
+ permissions: {
2503
+ read: readPermission,
2504
+ write: writePermission
2505
+ }
2506
+ })
2507
+ });
2508
+
2509
+ const result = await response.json();
2510
+
2511
+ if (result.success) {
2512
+ alert('知识库文档创建成功!');
2513
+ modal.remove();
2514
+ // 重新加载知识库列表
2515
+ const contentArea = document.getElementById('contentArea');
2516
+ renderKnowledgeView(contentArea);
2517
+ } else {
2518
+ alert('创建失败: ' + (result.error?.message || result.message || '未知错误'));
2519
+ }
2520
+ } catch (error) {
2521
+ console.error('创建知识库文档失败:', error);
2522
+ alert('创建失败: ' + error.message);
2523
+ }
2524
+ });
2525
+ }
2526
+
2527
+ // 显示知识库文档详情
2528
+ async function showKnowledgeDetail(knowledgeId) {
2529
+ try {
2530
+ const token = localStorage.getItem('token');
2531
+ const response = await fetch(`http://localhost:8765/api/knowledge/${knowledgeId}`, {
2532
+ headers: { 'Authorization': `Bearer ${token}` }
2533
+ });
2534
+ const result = await response.json();
2535
+
2536
+ if (!result.success) {
2537
+ alert('加载失败: ' + (result.error?.message || result.message));
2538
+ return;
2539
+ }
2540
+
2541
+ const kb = result.data.knowledge;
2542
+ const modal = document.createElement('div');
2543
+ modal.className = 'modal';
2544
+ modal.innerHTML = `
2545
+ <div class="modal-content" style="max-width: 900px;">
2546
+ <div class="modal-header">
2547
+ <h3>📚 ${kb.title}</h3>
2548
+ <button class="close-btn" id="closeKnowledgeDetail">&times;</button>
2549
+ </div>
2550
+ <div style="padding: 20px;">
2551
+ <div style="display: flex; gap: 20px; margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid var(--border);">
2552
+ <div><strong>分类:</strong>${kb.category}</div>
2553
+ <div><strong>状态:</strong><span class="status-badge status-${kb.status}">${kb.status === 'published' ? '已发布' : kb.status === 'draft' ? '草稿' : '已归档'}</span></div>
2554
+ <div><strong>浏览:</strong>${kb.views || 0}</div>
2555
+ <div><strong>点赞:</strong>${kb.likes?.length || 0}</div>
2556
+ </div>
2557
+ ${kb.tags && kb.tags.length > 0 ? `
2558
+ <div style="margin-bottom: 20px;">
2559
+ <strong>标签:</strong>
2560
+ ${kb.tags.map(tag => `<span class="kb-tag" style="background: var(--bg-hover); padding: 4px 12px; border-radius: 12px; margin-right: 8px; font-size: 12px;">${tag}</span>`).join('')}
2561
+ </div>
2562
+ ` : ''}
2563
+ <div style="background: var(--bg-dark); padding: 20px; border-radius: 12px; line-height: 1.8; white-space: pre-wrap;">
2564
+ ${kb.content}
2565
+ </div>
2566
+ <div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border); color: var(--text-secondary); font-size: 13px;">
2567
+ <div>作者:${kb.author?.username || '未知'}</div>
2568
+ <div>创建时间:${new Date(kb.createdAt).toLocaleString()}</div>
2569
+ <div>更新时间:${new Date(kb.updatedAt).toLocaleString()}</div>
2570
+ </div>
2571
+ </div>
2572
+ </div>
2573
+ `;
2574
+
2575
+ document.body.appendChild(modal);
2576
+
2577
+ document.getElementById('closeKnowledgeDetail').addEventListener('click', () => {
2578
+ modal.remove();
2579
+ });
2580
+
2581
+ modal.addEventListener('click', (e) => {
2582
+ if (e.target === modal) {
2583
+ modal.remove();
2584
+ }
2585
+ });
2586
+ } catch (error) {
2587
+ console.error('加载知识库详情失败:', error);
2588
+ alert('加载失败: ' + error.message);
2589
+ }
2590
+ }
2591
+
2592
+ // 显示编辑知识库文档模态框
2593
+ async function showEditKnowledgeModal(knowledgeId) {
2594
+ try {
2595
+ const token = localStorage.getItem('token');
2596
+ const response = await fetch(`http://localhost:8765/api/knowledge/${knowledgeId}`, {
2597
+ headers: { 'Authorization': `Bearer ${token}` }
2598
+ });
2599
+ const result = await response.json();
2600
+
2601
+ if (!result.success) {
2602
+ alert('加载失败: ' + (result.error?.message || result.message));
2603
+ return;
2604
+ }
2605
+
2606
+ const kb = result.data.knowledge;
2607
+ const modal = document.createElement('div');
2608
+ modal.className = 'modal';
2609
+ modal.innerHTML = `
2610
+ <div class="modal-content" style="max-width: 800px;">
2611
+ <div class="modal-header">
2612
+ <h3>✏️ 编辑知识库文档</h3>
2613
+ <button class="close-btn" id="closeEditKnowledge">&times;</button>
2614
+ </div>
2615
+ <form id="editKnowledgeForm">
2616
+ <div class="form-group">
2617
+ <label>标题 *</label>
2618
+ <input type="text" id="editKbTitle" required value="${kb.title}">
2619
+ </div>
2620
+ <div class="form-group">
2621
+ <label>分类</label>
2622
+ <input type="text" id="editKbCategory" value="${kb.category || ''}">
2623
+ </div>
2624
+ <div class="form-group">
2625
+ <label>标签(用逗号分隔)</label>
2626
+ <input type="text" id="editKbTags" value="${kb.tags ? kb.tags.join(', ') : ''}">
2627
+ </div>
2628
+ <div class="form-group">
2629
+ <label>内容 *</label>
2630
+ <textarea id="editKbContent" rows="10" required>${kb.content}</textarea>
2631
+ </div>
2632
+ <div class="form-group">
2633
+ <label>状态</label>
2634
+ <select id="editKbStatus">
2635
+ <option value="draft" ${kb.status === 'draft' ? 'selected' : ''}>草稿</option>
2636
+ <option value="published" ${kb.status === 'published' ? 'selected' : ''}>发布</option>
2637
+ <option value="archived" ${kb.status === 'archived' ? 'selected' : ''}>归档</option>
2638
+ </select>
2639
+ </div>
2640
+ <div class="form-group">
2641
+ <label>阅读权限</label>
2642
+ <select id="editKbReadPermission">
2643
+ <option value="group" ${kb.permissions?.read === 'group' ? 'selected' : ''}>群组成员可见</option>
2644
+ <option value="public" ${kb.permissions?.read === 'public' ? 'selected' : ''}>公开</option>
2645
+ <option value="private" ${kb.permissions?.read === 'private' ? 'selected' : ''}>仅自己可见</option>
2646
+ </select>
2647
+ </div>
2648
+ <div class="form-group">
2649
+ <label>编辑权限</label>
2650
+ <select id="editKbWritePermission">
2651
+ <option value="author" ${kb.permissions?.write === 'author' ? 'selected' : ''}>仅作者</option>
2652
+ <option value="admin" ${kb.permissions?.write === 'admin' ? 'selected' : ''}>管理员</option>
2653
+ <option value="all" ${kb.permissions?.write === 'all' ? 'selected' : ''}>所有成员</option>
2654
+ </select>
2655
+ </div>
2656
+ <div style="display: flex; gap: 10px; margin-top: 20px;">
2657
+ <button type="submit" class="btn-primary">保存修改</button>
2658
+ <button type="button" class="btn-secondary" id="cancelEditKnowledge">取消</button>
2659
+ </div>
2660
+ </form>
2661
+ </div>
2662
+ `;
2663
+
2664
+ document.body.appendChild(modal);
2665
+
2666
+ document.getElementById('closeEditKnowledge').addEventListener('click', () => {
2667
+ modal.remove();
2668
+ });
2669
+
2670
+ document.getElementById('cancelEditKnowledge').addEventListener('click', () => {
2671
+ modal.remove();
2672
+ });
2673
+
2674
+ modal.addEventListener('click', (e) => {
2675
+ if (e.target === modal) {
2676
+ modal.remove();
2677
+ }
2678
+ });
2679
+
2680
+ // 提交表单
2681
+ document.getElementById('editKnowledgeForm').addEventListener('submit', async (e) => {
2682
+ e.preventDefault();
2683
+
2684
+ const title = document.getElementById('editKbTitle').value.trim();
2685
+ const category = document.getElementById('editKbCategory').value.trim();
2686
+ const tagsInput = document.getElementById('editKbTags').value.trim();
2687
+ const content = document.getElementById('editKbContent').value.trim();
2688
+ const status = document.getElementById('editKbStatus').value;
2689
+ const readPermission = document.getElementById('editKbReadPermission').value;
2690
+ const writePermission = document.getElementById('editKbWritePermission').value;
2691
+
2692
+ if (!title || !content) {
2693
+ alert('标题和内容不能为空!');
2694
+ return;
2695
+ }
2696
+
2697
+ const tags = tagsInput ? tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag) : [];
2698
+
2699
+ try {
2700
+ const updateResponse = await fetch(`http://localhost:8765/api/knowledge/${knowledgeId}`, {
2701
+ method: 'PUT',
2702
+ headers: {
2703
+ 'Content-Type': 'application/json',
2704
+ 'Authorization': `Bearer ${token}`
2705
+ },
2706
+ body: JSON.stringify({
2707
+ title,
2708
+ content,
2709
+ category: category || '未分类',
2710
+ tags,
2711
+ status,
2712
+ permissions: {
2713
+ read: readPermission,
2714
+ write: writePermission
2715
+ },
2716
+ changeLog: '文档更新'
2717
+ })
2718
+ });
2719
+
2720
+ const updateResult = await updateResponse.json();
2721
+
2722
+ if (updateResult.success) {
2723
+ alert('知识库文档更新成功!');
2724
+ modal.remove();
2725
+ // 重新加载知识库列表
2726
+ const contentArea = document.getElementById('contentArea');
2727
+ renderKnowledgeView(contentArea);
2728
+ } else {
2729
+ alert('更新失败: ' + (updateResult.error?.message || updateResult.message || '未知错误'));
2730
+ }
2731
+ } catch (error) {
2732
+ console.error('更新知识库文档失败:', error);
2733
+ alert('更新失败: ' + error.message);
2734
+ }
2735
+ });
2736
+ } catch (error) {
2737
+ console.error('加载知识库文档失败:', error);
2738
+ alert('加载失败: ' + error.message);
2739
+ }
2740
+ }
2741
+
2742
+ // 工作流视图(管理员)
2743
+ async function renderWorkflowsView(container) {
2744
+ if (!currentGroup) {
2745
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
2746
+ return;
2747
+ }
2748
+
2749
+ try {
2750
+ const token = localStorage.getItem('token');
2751
+ const response = await fetch(`http://localhost:8765/api/workflows/group/${currentGroup._id}`, {
2752
+ headers: { 'Authorization': `Bearer ${token}` }
2753
+ });
2754
+ const result = await response.json();
2755
+ const workflows = (result.data && result.data.workflows) || [];
2756
+
2757
+ container.innerHTML = `
2758
+ <div class="view-header">
2759
+ <h2>🔄 工作流管理 - ${currentGroup.name}</h2>
2760
+ <button class="btn-primary" id="createWorkflowBtn">+ 创建工作流</button>
2761
+ </div>
2762
+
2763
+ <div class="info-box" style="background: var(--bg-card); padding: 20px; border-radius: 12px; margin-bottom: 20px; border-left: 4px solid var(--primary);">
2764
+ <h3 style="margin-bottom: 10px;">💡 什么是工作流?</h3>
2765
+ <p style="color: var(--text-secondary); line-height: 1.6; margin-bottom: 15px;">
2766
+ 工作流是一个<strong>自动化流程引擎</strong>,可以自动执行一系列预定义的操作,提高团队协作效率。
2767
+ </p>
2768
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin-top: 15px;">
2769
+ <div style="background: var(--bg-dark); padding: 15px; border-radius: 8px;">
2770
+ <div style="font-size: 24px; margin-bottom: 8px;">📋</div>
2771
+ <strong>文档审批流程</strong>
2772
+ <p style="font-size: 13px; color: var(--text-secondary); margin-top: 5px;">
2773
+ 自动通知审批人 → 等待审批 → 审批通过后发布
2774
+ </p>
2775
+ </div>
2776
+ <div style="background: var(--bg-dark); padding: 15px; border-radius: 8px;">
2777
+ <div style="font-size: 24px; margin-bottom: 8px;">✅</div>
2778
+ <strong>任务自动分配</strong>
2779
+ <p style="font-size: 13px; color: var(--text-secondary); margin-top: 5px;">
2780
+ 新任务创建 → 自动分配成员 → 发送通知
2781
+ </p>
2782
+ </div>
2783
+ <div style="background: var(--bg-dark); padding: 15px; border-radius: 8px;">
2784
+ <div style="font-size: 24px; margin-bottom: 8px;">⏰</div>
2785
+ <strong>定时报告生成</strong>
2786
+ <p style="font-size: 13px; color: var(--text-secondary); margin-top: 5px;">
2787
+ 每天凌晨2点 → 收集数据 → 生成报告 → 发送邮件
2788
+ </p>
2789
+ </div>
2790
+ </div>
2791
+ </div>
2792
+
2793
+ <div class="workflows-list" id="workflowsList"></div>
2794
+
2795
+ <!-- 工作流配置模态框 -->
2796
+ <div class="modal hidden" id="workflowModal">
2797
+ <div class="modal-content" style="max-width: 720px;">
2798
+ <div class="modal-header">
2799
+ <h3 id="workflowModalTitle">创建工作流</h3>
2800
+ <button class="modal-close" id="closeWorkflowModal">&times;</button>
2801
+ </div>
2802
+ <form id="workflowForm">
2803
+ <div class="form-group">
2804
+ <label>名称 *</label>
2805
+ <input type="text" id="wfName" required placeholder="例如:文档审批流程">
2806
+ </div>
2807
+ <div class="form-group">
2808
+ <label>描述</label>
2809
+ <textarea id="wfDescription" rows="2" placeholder="说明此工作流的用途"></textarea>
2810
+ </div>
2811
+ <div class="form-group">
2812
+ <label>触发方式</label>
2813
+ <select id="wfTriggerType">
2814
+ <option value="manual">手动触发</option>
2815
+ <option value="scheduled">定时触发</option>
2816
+ </select>
2817
+ </div>
2818
+ <div class="form-group" id="wfScheduleGroup">
2819
+ <label>定时表达式(可选)</label>
2820
+ <input type="text" id="wfSchedule" placeholder="例如:0 2 * * * 表示每天凌晨2点">
2821
+ <small>使用 Cron 表达式配置定时触发时间</small>
2822
+ </div>
2823
+ <div class="form-group">
2824
+ <label>步骤配置(JSON 数组)</label>
2825
+ <textarea id="wfSteps" rows="6" placeholder='例如:[{"name":"通知审批人","type":"notification","config":{"message":"有新文档需要审批"}}]'></textarea>
2826
+ <small>高级用法:直接编辑 JSON,字段与后端 Workflow.steps 一致</small>
2827
+ </div>
2828
+ <div class="form-actions">
2829
+ <button type="button" class="btn-secondary" id="cancelWorkflow">取消</button>
2830
+ <button type="submit" class="btn-primary">保存工作流</button>
2831
+ </div>
2832
+ </form>
2833
+ </div>
2834
+ </div>
2835
+ `;
2836
+
2837
+ const workflowsList = document.getElementById('workflowsList');
2838
+
2839
+ if (workflows.length === 0) {
2840
+ workflowsList.innerHTML = '<div class="empty-state">暂无工作流<br><small style="color: var(--text-secondary);">点击"创建工作流"开始自动化您的工作流程</small></div>';
2841
+ } else {
2842
+ workflows.forEach(wf => {
2843
+ const wfCard = document.createElement('div');
2844
+ wfCard.className = 'workflow-card';
2845
+ const triggerType = wf.trigger && wf.trigger.type ? wf.trigger.type : 'manual';
2846
+ const triggerLabel = triggerType === 'manual'
2847
+ ? '🖱️ 手动'
2848
+ : triggerType === 'scheduled'
2849
+ ? '⏰ 定时'
2850
+ : '⚡ 事件';
2851
+ const stepsCount = Array.isArray(wf.steps) ? wf.steps.length : 0;
2852
+ const totalExec = wf.stats && typeof wf.stats.totalExecutions === 'number'
2853
+ ? wf.stats.totalExecutions
2854
+ : 0;
2855
+
2856
+ wfCard.innerHTML = `
2857
+ <div class="wf-header">
2858
+ <h3>${wf.name}</h3>
2859
+ <span class="wf-status ${wf.status}">${wf.status === 'active' ? '✅ 已激活' : wf.status === 'inactive' ? '⏸️ 已停用' : '📝 草稿'}</span>
2860
+ </div>
2861
+ <p>${wf.description || '暂无描述'}</p>
2862
+ <div class="wf-meta">
2863
+ <span>触发器: ${triggerLabel}</span>
2864
+ <span>步骤: ${stepsCount}</span>
2865
+ <span>执行: ${totalExec} 次</span>
2866
+ </div>
2867
+ <div class="wf-actions">
2868
+ <button class="btn-primary btn-sm" data-id="${wf._id}" data-action="view-wf">查看详情</button>
2869
+ <button class="btn-secondary btn-sm" data-id="${wf._id}" data-action="edit-wf">配置</button>
2870
+ ${wf.status === 'active' ? `
2871
+ <button class="btn-secondary btn-sm" data-id="${wf._id}" data-action="trigger-wf">手动触发</button>
2872
+ <button class="btn-warning btn-sm" data-id="${wf._id}" data-action="deactivate-wf">停用</button>
2873
+ ` : `
2874
+ <button class="btn-success btn-sm" data-id="${wf._id}" data-action="activate-wf">激活</button>
2875
+ `}
2876
+ <button class="btn-danger btn-sm" data-id="${wf._id}" data-action="delete-wf">删除</button>
2877
+ </div>
2878
+ `;
2879
+ workflowsList.appendChild(wfCard);
2880
+ });
2881
+ }
2882
+
2883
+ // 详情查看(暂时简单提示,可后续扩展为真正详情面板)
2884
+ document.querySelectorAll('[data-action="view-wf"]').forEach(btn => {
2885
+ const wf = workflows.find(w => w._id === btn.dataset.id);
2886
+ btn.addEventListener('click', () => {
2887
+ alert(`工作流详情:\n\n名称:${wf.name}\n状态:${wf.status}\n触发方式:${wf.trigger?.type || 'manual'}\n步骤数:${(wf.steps || []).length}`);
2888
+ });
2889
+ });
2890
+
2891
+ // 手动触发
2892
+ document.querySelectorAll('[data-action="trigger-wf"]').forEach(btn => {
2893
+ btn.addEventListener('click', async () => {
2894
+ if (confirm('确定要手动触发此工作流吗?')) {
2895
+ try {
2896
+ await fetch(`http://localhost:8765/api/workflows/${btn.dataset.id}/trigger`, {
2897
+ method: 'POST',
2898
+ headers: {
2899
+ 'Content-Type': 'application/json',
2900
+ 'Authorization': `Bearer ${token}`
2901
+ },
2902
+ body: JSON.stringify({ triggerData: {} })
2903
+ });
2904
+ alert('工作流已触发!');
2905
+ } catch (error) {
2906
+ alert('触发失败: ' + error.message);
2907
+ }
2908
+ }
2909
+ });
2910
+ });
2911
+
2912
+ // 激活/停用
2913
+ document.querySelectorAll('[data-action="activate-wf"], [data-action="deactivate-wf"]').forEach(btn => {
2914
+ btn.addEventListener('click', async () => {
2915
+ const action = btn.dataset.action === 'activate-wf' ? 'activate' : 'deactivate';
2916
+ try {
2917
+ await fetch(`http://localhost:8765/api/workflows/${btn.dataset.id}/${action}`, {
2918
+ method: 'POST',
2919
+ headers: { 'Authorization': `Bearer ${token}` }
2920
+ });
2921
+ alert(action === 'activate' ? '已激活工作流' : '已停用工作流');
2922
+ renderWorkflowsView(container);
2923
+ } catch (error) {
2924
+ alert('操作失败: ' + error.message);
2925
+ }
2926
+ });
2927
+ });
2928
+
2929
+ // 删除工作流
2930
+ document.querySelectorAll('[data-action="delete-wf"]').forEach(btn => {
2931
+ btn.addEventListener('click', async () => {
2932
+ if (confirm('确定要删除这个工作流吗?删除后无法恢复!')) {
2933
+ try {
2934
+ await fetch(`http://localhost:8765/api/workflows/${btn.dataset.id}`, {
2935
+ method: 'DELETE',
2936
+ headers: { 'Authorization': `Bearer ${token}` }
2937
+ });
2938
+ alert('工作流删除成功!');
2939
+ renderWorkflowsView(container);
2940
+ } catch (error) {
2941
+ alert('删除失败: ' + error.message);
2942
+ }
2943
+ }
2944
+ });
2945
+ });
2946
+
2947
+ // --- 工作流配置模态框逻辑 ---
2948
+ const workflowModal = document.getElementById('workflowModal');
2949
+ const workflowModalTitle = document.getElementById('workflowModalTitle');
2950
+ const workflowForm = document.getElementById('workflowForm');
2951
+ const nameInput = document.getElementById('wfName');
2952
+ const descInput = document.getElementById('wfDescription');
2953
+ const triggerSelect = document.getElementById('wfTriggerType');
2954
+ const scheduleGroup = document.getElementById('wfScheduleGroup');
2955
+ const scheduleInput = document.getElementById('wfSchedule');
2956
+ const stepsTextarea = document.getElementById('wfSteps');
2957
+
2958
+ let editingWorkflow = null;
2959
+
2960
+ function updateScheduleVisibility() {
2961
+ const type = triggerSelect.value;
2962
+ scheduleGroup.style.display = type === 'scheduled' ? 'block' : 'none';
2963
+ }
2964
+
2965
+ function openWorkflowModal(workflow) {
2966
+ editingWorkflow = workflow || null;
2967
+ workflowModalTitle.textContent = workflow ? '编辑工作流' : '创建工作流';
2968
+ nameInput.value = workflow?.name || '';
2969
+ descInput.value = workflow?.description || '';
2970
+ const type = workflow?.trigger?.type || 'manual';
2971
+ triggerSelect.value = type;
2972
+ scheduleInput.value = workflow?.trigger?.schedule || '';
2973
+ stepsTextarea.value = workflow ? JSON.stringify(workflow.steps || [], null, 2) : '';
2974
+ updateScheduleVisibility();
2975
+ workflowModal.classList.remove('hidden');
2976
+ }
2977
+
2978
+ function closeWorkflowModal() {
2979
+ workflowModal.classList.add('hidden');
2980
+ }
2981
+
2982
+ triggerSelect.addEventListener('change', updateScheduleVisibility);
2983
+
2984
+ document.getElementById('createWorkflowBtn').addEventListener('click', () => openWorkflowModal(null));
2985
+ document.getElementById('closeWorkflowModal').addEventListener('click', closeWorkflowModal);
2986
+ document.getElementById('cancelWorkflow').addEventListener('click', closeWorkflowModal);
2987
+ workflowModal.addEventListener('click', (e) => {
2988
+ if (e.target === workflowModal) {
2989
+ closeWorkflowModal();
2990
+ }
2991
+ });
2992
+
2993
+ // 编辑按钮
2994
+ document.querySelectorAll('[data-action="edit-wf"]').forEach(btn => {
2995
+ const wf = workflows.find(w => w._id === btn.dataset.id);
2996
+ btn.addEventListener('click', () => openWorkflowModal(wf));
2997
+ });
2998
+
2999
+ // 保存工作流
3000
+ workflowForm.addEventListener('submit', async (e) => {
3001
+ e.preventDefault();
3002
+ const name = nameInput.value.trim();
3003
+ if (!name) {
3004
+ alert('请填写工作流名称');
3005
+ return;
3006
+ }
3007
+ const description = descInput.value.trim();
3008
+ const triggerType = triggerSelect.value;
3009
+ const schedule = scheduleInput.value.trim();
3010
+ const stepsText = stepsTextarea.value.trim();
3011
+
3012
+ let steps = [];
3013
+ if (stepsText) {
3014
+ try {
3015
+ const parsed = JSON.parse(stepsText);
3016
+ if (!Array.isArray(parsed)) {
3017
+ throw new Error('步骤配置必须是 JSON 数组');
3018
+ }
3019
+ steps = parsed;
3020
+ } catch (err) {
3021
+ alert('步骤配置必须是合法的 JSON 数组:' + err.message);
3022
+ return;
3023
+ }
3024
+ }
3025
+
3026
+ const body = {
3027
+ name,
3028
+ description,
3029
+ groupId: currentGroup._id,
3030
+ trigger: { type: triggerType },
3031
+ steps
3032
+ };
3033
+ if (triggerType === 'scheduled' && schedule) {
3034
+ body.trigger.schedule = schedule;
3035
+ }
3036
+
3037
+ const url = editingWorkflow
3038
+ ? `http://localhost:8765/api/workflows/${editingWorkflow._id}`
3039
+ : 'http://localhost:8765/api/workflows';
3040
+ const method = editingWorkflow ? 'PUT' : 'POST';
3041
+
3042
+ try {
3043
+ const resp = await fetch(url, {
3044
+ method,
3045
+ headers: {
3046
+ 'Content-Type': 'application/json',
3047
+ 'Authorization': `Bearer ${token}`
3048
+ },
3049
+ body: JSON.stringify(body)
3050
+ });
3051
+ const data = await resp.json().catch(() => ({}));
3052
+ if (!resp.ok || data.success === false) {
3053
+ throw new Error(data.error?.message || data.message || `HTTP ${resp.status}`);
3054
+ }
3055
+ alert('工作流已保存');
3056
+ closeWorkflowModal();
3057
+ renderWorkflowsView(container);
3058
+ } catch (err) {
3059
+ alert('保存失败: ' + err.message);
3060
+ }
3061
+ });
3062
+
3063
+ } catch (error) {
3064
+ console.error('加载工作流失败:', error);
3065
+ container.innerHTML = `
3066
+ <div class="view-header">
3067
+ <h2>🔄 工作流管理</h2>
3068
+ </div>
3069
+ <div class="empty-state">加载失败: ${error.message}</div>
3070
+ `;
3071
+ }
3072
+ }
3073
+
3074
+ function getStatusText(status) {
3075
+ const statusMap = {
3076
+ 'pending': '待处理',
3077
+ 'in_progress': '进行中',
3078
+ 'completed': '已完成',
3079
+ 'terminated': '已终止'
3080
+ };
3081
+ return statusMap[status] || status;
3082
+ }
3083
+
3084
+ // ==================== 优化后的备份管理界面 ====================
3085
+
3086
+ async function renderOptimizedBackupView(container) {
3087
+ try {
3088
+ const token = localStorage.getItem('token');
3089
+
3090
+ // 获取备份列表
3091
+ const backupsResponse = await fetch('http://localhost:8765/api/backup/list', {
3092
+ headers: { 'Authorization': `Bearer ${token}` }
3093
+ });
3094
+ const backupsResult = await backupsResponse.json();
3095
+
3096
+ // 获取备份配置
3097
+ const configResponse = await fetch('http://localhost:8765/api/backup/config', {
3098
+ headers: { 'Authorization': `Bearer ${token}` }
3099
+ });
3100
+ const configResult = await configResponse.json();
3101
+
3102
+ const backups = backupsResult.data?.backups || [];
3103
+ const config = configResult.data?.config || {};
3104
+
3105
+ container.innerHTML = `
3106
+ <div class="view-header">
3107
+ <h2>💾 备份管理</h2>
3108
+ <div style="display: flex; gap: 10px;">
3109
+ <button class="btn-primary" id="createBackupBtn">
3110
+ <span style="font-size: 18px;">➕</span> 立即备份
3111
+ </button>
3112
+ <button class="btn-secondary" id="configBackupBtn">
3113
+ <span style="font-size: 18px;">⚙️</span> 备份设置
3114
+ </button>
3115
+ </div>
3116
+ </div>
3117
+
3118
+ <!-- 统计卡片区域 -->
3119
+ <div class="backup-stats-grid">
3120
+ <div class="stat-card-modern">
3121
+ <div class="stat-icon" style="background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);">
3122
+ 📊
3123
+ </div>
3124
+ <div class="stat-content">
3125
+ <div class="stat-label">备份总数</div>
3126
+ <div class="stat-value">${backups.length}</div>
3127
+ </div>
3128
+ </div>
3129
+
3130
+ <div class="stat-card-modern">
3131
+ <div class="stat-icon" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%);">
3132
+ ${config.autoBackup ? '✅' : '❌'}
3133
+ </div>
3134
+ <div class="stat-content">
3135
+ <div class="stat-label">自动备份</div>
3136
+ <div class="stat-value">${config.autoBackup ? '已启用' : '已禁用'}</div>
3137
+ </div>
3138
+ </div>
3139
+
3140
+ <div class="stat-card-modern">
3141
+ <div class="stat-icon" style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);">
3142
+ 🕐
3143
+ </div>
3144
+ <div class="stat-content">
3145
+ <div class="stat-label">备份频率</div>
3146
+ <div class="stat-value">${config.schedule || '未设置'}</div>
3147
+ </div>
3148
+ </div>
3149
+
3150
+ <div class="stat-card-modern">
3151
+ <div class="stat-icon" style="background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);">
3152
+ 📅
3153
+ </div>
3154
+ <div class="stat-content">
3155
+ <div class="stat-label">保留天数</div>
3156
+ <div class="stat-value">${config.retention || 30} 天</div>
3157
+ </div>
3158
+ </div>
3159
+ </div>
3160
+
3161
+ <!-- 备份列表 -->
3162
+ <div class="backup-list-section">
3163
+ <h3 style="margin: 30px 0 20px; color: var(--text-primary); font-size: 20px;">
3164
+ 📦 最近备份
3165
+ </h3>
3166
+ <div class="backup-cards-grid" id="backupCards">
3167
+ ${backups.length === 0 ? `
3168
+ <div class="empty-state-modern">
3169
+ <div class="empty-icon">📭</div>
3170
+ <h3>暂无备份记录</h3>
3171
+ <p>点击"立即备份"创建第一个备份</p>
3172
+ <button class="btn-primary" onclick="document.getElementById('createBackupBtn').click()">
3173
+ 立即备份
3174
+ </button>
3175
+ </div>
3176
+ ` : backups.map(backup => createBackupCard(backup)).join('')}
3177
+ </div>
3178
+ </div>
3179
+
3180
+ ${createBackupModals(config)}
3181
+ `;
3182
+
3183
+ // 添加样式
3184
+ addBackupStyles();
3185
+
3186
+ // 绑定事件
3187
+ setupBackupEvents(token, container);
3188
+
3189
+ } catch (error) {
3190
+ console.error('加载备份管理失败:', error);
3191
+ container.innerHTML = `
3192
+ <div class="view-header">
3193
+ <h2>💾 备份管理</h2>
3194
+ </div>
3195
+ <div class="empty-state">加载失败: ${error.message}</div>
3196
+ `;
3197
+ }
3198
+ }
3199
+
3200
+ function createBackupCard(backup) {
3201
+ const typeConfig = {
3202
+ manual: { icon: '📦', label: '手动备份', color: '#6366f1' },
3203
+ scheduled: { icon: '🔄', label: '定时备份', color: '#10b981' },
3204
+ auto: { icon: '⚡', label: '自动备份', color: '#f59e0b' }
3205
+ };
3206
+
3207
+ const config = typeConfig[backup.type] || typeConfig.manual;
3208
+ const statusIcon = backup.status === 'completed' ? '✅' : backup.status === 'failed' ? '❌' : '⏳';
3209
+
3210
+ return `
3211
+ <div class="backup-card-modern">
3212
+ <div class="backup-card-header">
3213
+ <div class="backup-type-icon" style="background: ${config.color}20; color: ${config.color};">
3214
+ ${config.icon}
3215
+ </div>
3216
+ <div class="backup-card-title">
3217
+ <h4>${config.label}</h4>
3218
+ <span class="backup-time">${new Date(backup.createdAt).toLocaleString()}</span>
3219
+ </div>
3220
+ <div class="backup-status-icon">${statusIcon}</div>
3221
+ </div>
3222
+
3223
+ <div class="backup-card-body">
3224
+ <div class="backup-info-row">
3225
+ <span class="info-label">大小</span>
3226
+ <span class="info-value">${formatFileSize(backup.size)}</span>
3227
+ </div>
3228
+ <div class="backup-info-row">
3229
+ <span class="info-label">状态</span>
3230
+ <span class="info-value">${backup.status === 'completed' ? '完成' : backup.status === 'failed' ? '失败' : '进行中'}</span>
3231
+ </div>
3232
+ </div>
3233
+
3234
+ ${backup.status === 'completed' ? `
3235
+ <div class="backup-card-actions">
3236
+ <button class="btn-action btn-download" data-id="${backup._id}" data-action="download">
3237
+ <span>⬇️</span> 下载
3238
+ </button>
3239
+ <button class="btn-action btn-restore" data-id="${backup._id}" data-action="restore">
3240
+ <span>🔄</span> 恢复
3241
+ </button>
3242
+ <button class="btn-action btn-delete" data-id="${backup._id}" data-action="delete">
3243
+ <span>🗑️</span> 删除
3244
+ </button>
3245
+ </div>
3246
+ ` : `
3247
+ <div class="backup-card-actions">
3248
+ <button class="btn-action btn-delete" data-id="${backup._id}" data-action="delete">
3249
+ <span>🗑️</span> 删除
3250
+ </button>
3251
+ </div>
3252
+ `}
3253
+ </div>
3254
+ `;
3255
+ }
3256
+
3257
+ function createBackupModals(config) {
3258
+ return `
3259
+ <!-- 创建备份模态框 -->
3260
+ <div class="modal hidden" id="createBackupModal">
3261
+ <div class="modal-content">
3262
+ <div class="modal-header">
3263
+ <h3>创建备份</h3>
3264
+ <button class="close-btn" id="closeCreateBackup">&times;</button>
3265
+ </div>
3266
+ <form id="createBackupForm">
3267
+ <div class="form-group">
3268
+ <label>备份类型</label>
3269
+ <select id="backupType" required>
3270
+ <option value="full">完整备份(所有数据)</option>
3271
+ <option value="incremental">增量备份(仅变更)</option>
3272
+ </select>
3273
+ </div>
3274
+ <div class="form-group">
3275
+ <label>备份说明(可选)</label>
3276
+ <textarea id="backupDescription" rows="3" placeholder="备份说明..."></textarea>
3277
+ </div>
3278
+ <div style="display: flex; gap: 10px; margin-top: 20px;">
3279
+ <button type="submit" class="btn-primary">开始备份</button>
3280
+ <button type="button" class="btn-secondary" id="cancelCreateBackup">取消</button>
3281
+ </div>
3282
+ </form>
3283
+ </div>
3284
+ </div>
3285
+
3286
+ <!-- 备份配置模态框 -->
3287
+ <div class="modal hidden" id="configBackupModal">
3288
+ <div class="modal-content">
3289
+ <div class="modal-header">
3290
+ <h3>备份设置</h3>
3291
+ <button class="close-btn" id="closeConfigBackup">&times;</button>
3292
+ </div>
3293
+ <form id="configBackupForm">
3294
+ <div class="form-group">
3295
+ <label>
3296
+ <input type="checkbox" id="autoBackup" ${config.autoBackup ? 'checked' : ''}>
3297
+ 启用自动备份
3298
+ </label>
3299
+ </div>
3300
+ <div class="form-group">
3301
+ <label>备份频率(Cron表达式)</label>
3302
+ <input type="text" id="backupSchedule" value="${config.schedule || '0 2 * * *'}" placeholder="0 2 * * * (每天凌晨2点)">
3303
+ <small>示例:0 2 * * * (每天凌晨2点),0 */6 * * * (每6小时)</small>
3304
+ </div>
3305
+ <div class="form-group">
3306
+ <label>保留天数</label>
3307
+ <input type="number" id="backupRetention" value="${config.retention || 30}" min="1" max="365">
3308
+ <small>超过此天数的备份将自动删除</small>
3309
+ </div>
3310
+ <div class="form-group">
3311
+ <label>最大备份数量</label>
3312
+ <input type="number" id="maxBackups" value="${config.maxBackups || 10}" min="1" max="100">
3313
+ </div>
3314
+ <div style="display: flex; gap: 10px; margin-top: 20px;">
3315
+ <button type="submit" class="btn-primary">保存设置</button>
3316
+ <button type="button" class="btn-secondary" id="cancelConfigBackup">取消</button>
3317
+ </div>
3318
+ </form>
3319
+ </div>
3320
+ </div>
3321
+ `;
3322
+ }
3323
+
3324
+ function addBackupStyles() {
3325
+ if (document.getElementById('backup-modern-styles')) return;
3326
+
3327
+ const style = document.createElement('style');
3328
+ style.id = 'backup-modern-styles';
3329
+ style.textContent = `
3330
+ .backup-stats-grid {
3331
+ display: grid;
3332
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
3333
+ gap: 20px;
3334
+ margin: 20px 0 30px;
3335
+ }
3336
+
3337
+ .stat-card-modern {
3338
+ background: linear-gradient(135deg, var(--bg-card) 0%, rgba(99,102,241,0.05) 100%);
3339
+ border: 1px solid var(--border);
3340
+ border-radius: 16px;
3341
+ padding: 20px;
3342
+ display: flex;
3343
+ align-items: center;
3344
+ gap: 16px;
3345
+ transition: all 0.3s ease;
3346
+ animation: fadeInUp 0.5s ease;
3347
+ }
3348
+
3349
+ .stat-card-modern:hover {
3350
+ transform: translateY(-5px);
3351
+ box-shadow: 0 12px 32px rgba(99,102,241,0.2);
3352
+ border-color: var(--primary);
3353
+ }
3354
+
3355
+ .stat-icon {
3356
+ width: 60px;
3357
+ height: 60px;
3358
+ border-radius: 12px;
3359
+ display: flex;
3360
+ align-items: center;
3361
+ justify-content: center;
3362
+ font-size: 28px;
3363
+ flex-shrink: 0;
3364
+ }
3365
+
3366
+ .stat-content {
3367
+ flex: 1;
3368
+ }
3369
+
3370
+ .stat-label {
3371
+ font-size: 13px;
3372
+ color: var(--text-secondary);
3373
+ margin-bottom: 6px;
3374
+ font-weight: 600;
3375
+ text-transform: uppercase;
3376
+ letter-spacing: 0.5px;
3377
+ }
3378
+
3379
+ .stat-value {
3380
+ font-size: 24px;
3381
+ font-weight: 800;
3382
+ color: var(--text-primary);
3383
+ }
3384
+
3385
+ .backup-cards-grid {
3386
+ display: grid;
3387
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
3388
+ gap: 20px;
3389
+ }
3390
+
3391
+ .backup-card-modern {
3392
+ background: var(--bg-card);
3393
+ border: 1px solid var(--border);
3394
+ border-radius: 16px;
3395
+ padding: 20px;
3396
+ transition: all 0.3s ease;
3397
+ animation: fadeInUp 0.5s ease;
3398
+ }
3399
+
3400
+ .backup-card-modern:hover {
3401
+ transform: translateY(-5px);
3402
+ box-shadow: 0 8px 24px rgba(99,102,241,0.15);
3403
+ border-color: var(--primary);
3404
+ }
3405
+
3406
+ .backup-card-header {
3407
+ display: flex;
3408
+ align-items: center;
3409
+ gap: 12px;
3410
+ margin-bottom: 16px;
3411
+ padding-bottom: 16px;
3412
+ border-bottom: 1px solid var(--border);
3413
+ }
3414
+
3415
+ .backup-type-icon {
3416
+ width: 48px;
3417
+ height: 48px;
3418
+ border-radius: 12px;
3419
+ display: flex;
3420
+ align-items: center;
3421
+ justify-content: center;
3422
+ font-size: 24px;
3423
+ flex-shrink: 0;
3424
+ }
3425
+
3426
+ .backup-card-title {
3427
+ flex: 1;
3428
+ }
3429
+
3430
+ .backup-card-title h4 {
3431
+ margin: 0 0 4px;
3432
+ font-size: 16px;
3433
+ color: var(--text-primary);
3434
+ }
3435
+
3436
+ .backup-time {
3437
+ font-size: 12px;
3438
+ color: var(--text-secondary);
3439
+ }
3440
+
3441
+ .backup-status-icon {
3442
+ font-size: 24px;
3443
+ }
3444
+
3445
+ .backup-card-body {
3446
+ margin-bottom: 16px;
3447
+ }
3448
+
3449
+ .backup-info-row {
3450
+ display: flex;
3451
+ justify-content: space-between;
3452
+ padding: 8px 0;
3453
+ font-size: 14px;
3454
+ }
3455
+
3456
+ .info-label {
3457
+ color: var(--text-secondary);
3458
+ }
3459
+
3460
+ .info-value {
3461
+ color: var(--text-primary);
3462
+ font-weight: 600;
3463
+ }
3464
+
3465
+ .backup-card-actions {
3466
+ display: flex;
3467
+ gap: 8px;
3468
+ }
3469
+
3470
+ .btn-action {
3471
+ flex: 1;
3472
+ padding: 10px;
3473
+ border: none;
3474
+ border-radius: 8px;
3475
+ font-size: 13px;
3476
+ font-weight: 600;
3477
+ cursor: pointer;
3478
+ transition: all 0.3s ease;
3479
+ display: flex;
3480
+ align-items: center;
3481
+ justify-content: center;
3482
+ gap: 6px;
3483
+ }
3484
+
3485
+ .btn-download {
3486
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
3487
+ color: white;
3488
+ }
3489
+
3490
+ .btn-restore {
3491
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
3492
+ color: white;
3493
+ }
3494
+
3495
+ .btn-delete {
3496
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
3497
+ color: white;
3498
+ }
3499
+
3500
+ .btn-action:hover {
3501
+ transform: translateY(-2px);
3502
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
3503
+ }
3504
+
3505
+ .empty-state-modern {
3506
+ grid-column: 1 / -1;
3507
+ text-align: center;
3508
+ padding: 60px 20px;
3509
+ background: linear-gradient(135deg, var(--bg-dark) 0%, rgba(99,102,241,0.03) 100%);
3510
+ border-radius: 16px;
3511
+ border: 2px dashed var(--border);
3512
+ }
3513
+
3514
+ .empty-icon {
3515
+ font-size: 64px;
3516
+ margin-bottom: 20px;
3517
+ animation: bounce 2s infinite;
3518
+ }
3519
+
3520
+ .empty-state-modern h3 {
3521
+ margin: 0 0 10px;
3522
+ color: var(--text-primary);
3523
+ font-size: 20px;
3524
+ }
3525
+
3526
+ .empty-state-modern p {
3527
+ margin: 0 0 20px;
3528
+ color: var(--text-secondary);
3529
+ font-size: 14px;
3530
+ }
3531
+ `;
3532
+ document.head.appendChild(style);
3533
+ }
3534
+
3535
+ function setupBackupEvents(token, container) {
3536
+ // 创建备份按钮
3537
+ document.getElementById('createBackupBtn')?.addEventListener('click', () => {
3538
+ document.getElementById('createBackupModal').classList.remove('hidden');
3539
+ });
3540
+
3541
+ // 配置备份按钮
3542
+ document.getElementById('configBackupBtn')?.addEventListener('click', () => {
3543
+ document.getElementById('configBackupModal').classList.remove('hidden');
3544
+ });
3545
+
3546
+ // 关闭按钮
3547
+ document.getElementById('closeCreateBackup')?.addEventListener('click', () => {
3548
+ document.getElementById('createBackupModal').classList.add('hidden');
3549
+ });
3550
+
3551
+ document.getElementById('cancelCreateBackup')?.addEventListener('click', () => {
3552
+ document.getElementById('createBackupModal').classList.add('hidden');
3553
+ });
3554
+
3555
+ document.getElementById('closeConfigBackup')?.addEventListener('click', () => {
3556
+ document.getElementById('configBackupModal').classList.add('hidden');
3557
+ });
3558
+
3559
+ document.getElementById('cancelConfigBackup')?.addEventListener('click', () => {
3560
+ document.getElementById('configBackupModal').classList.add('hidden');
3561
+ });
3562
+
3563
+ // 创建备份表单提交
3564
+ document.getElementById('createBackupForm')?.addEventListener('submit', async (e) => {
3565
+ e.preventDefault();
3566
+ const type = document.getElementById('backupType').value;
3567
+ const description = document.getElementById('backupDescription').value;
3568
+
3569
+ try {
3570
+ const response = await fetch('http://localhost:8765/api/backup/create', {
3571
+ method: 'POST',
3572
+ headers: {
3573
+ 'Content-Type': 'application/json',
3574
+ 'Authorization': `Bearer ${token}`
3575
+ },
3576
+ body: JSON.stringify({ type, description })
3577
+ });
3578
+
3579
+ const result = await response.json();
3580
+ if (result.success) {
3581
+ alert('备份创建成功!');
3582
+ document.getElementById('createBackupModal').classList.add('hidden');
3583
+ renderOptimizedBackupView(container);
3584
+ } else {
3585
+ alert('备份失败: ' + (result.error?.message || '未知错误'));
3586
+ }
3587
+ } catch (error) {
3588
+ alert('备份失败: ' + error.message);
3589
+ }
3590
+ });
3591
+
3592
+ // 配置备份表单提交
3593
+ document.getElementById('configBackupForm')?.addEventListener('submit', async (e) => {
3594
+ e.preventDefault();
3595
+ const config = {
3596
+ autoBackup: document.getElementById('autoBackup').checked,
3597
+ schedule: document.getElementById('backupSchedule').value,
3598
+ retention: parseInt(document.getElementById('backupRetention').value),
3599
+ maxBackups: parseInt(document.getElementById('maxBackups').value)
3600
+ };
3601
+
3602
+ try {
3603
+ const response = await fetch('http://localhost:8765/api/backup/config', {
3604
+ method: 'PUT',
3605
+ headers: {
3606
+ 'Content-Type': 'application/json',
3607
+ 'Authorization': `Bearer ${token}`
3608
+ },
3609
+ body: JSON.stringify(config)
3610
+ });
3611
+
3612
+ const result = await response.json();
3613
+ if (result.success) {
3614
+ alert('设置保存成功!');
3615
+ document.getElementById('configBackupModal').classList.add('hidden');
3616
+ renderOptimizedBackupView(container);
3617
+ } else {
3618
+ alert('保存失败: ' + (result.error?.message || '未知错误'));
3619
+ }
3620
+ } catch (error) {
3621
+ alert('保存失败: ' + error.message);
3622
+ }
3623
+ });
3624
+
3625
+ // 备份操作按钮
3626
+ document.querySelectorAll('[data-action="download"]').forEach(btn => {
3627
+ btn.addEventListener('click', () => {
3628
+ const backupId = btn.dataset.id;
3629
+ window.location.href = `http://localhost:8765/api/backup/${backupId}/download?token=${token}`;
3630
+ });
3631
+ });
3632
+
3633
+ document.querySelectorAll('[data-action="restore"]').forEach(btn => {
3634
+ btn.addEventListener('click', async () => {
3635
+ if (!confirm('确定要恢复此备份吗?这将覆盖当前数据!')) return;
3636
+
3637
+ const backupId = btn.dataset.id;
3638
+ try {
3639
+ const response = await fetch(`http://localhost:8765/api/backup/${backupId}/restore`, {
3640
+ method: 'POST',
3641
+ headers: { 'Authorization': `Bearer ${token}` }
3642
+ });
3643
+
3644
+ const result = await response.json();
3645
+ if (result.success) {
3646
+ alert('备份恢复成功!页面将刷新...');
3647
+ setTimeout(() => window.location.reload(), 2000);
3648
+ } else {
3649
+ alert('恢复失败: ' + (result.error?.message || '未知错误'));
3650
+ }
3651
+ } catch (error) {
3652
+ alert('恢复失败: ' + error.message);
3653
+ }
3654
+ });
3655
+ });
3656
+
3657
+ document.querySelectorAll('[data-action="delete"]').forEach(btn => {
3658
+ btn.addEventListener('click', async () => {
3659
+ if (!confirm('确定要删除此备份吗?')) return;
3660
+
3661
+ const backupId = btn.dataset.id;
3662
+ try {
3663
+ const response = await fetch(`http://localhost:8765/api/backup/${backupId}`, {
3664
+ method: 'DELETE',
3665
+ headers: { 'Authorization': `Bearer ${token}` }
3666
+ });
3667
+
3668
+ const result = await response.json();
3669
+ if (result.success) {
3670
+ alert('备份删除成功!');
3671
+ renderOptimizedBackupView(container);
3672
+ } else {
3673
+ alert('删除失败: ' + (result.error?.message || '未知错误'));
3674
+ }
3675
+ } catch (error) {
3676
+ alert('删除失败: ' + error.message);
3677
+ }
3678
+ });
3679
+ });
3680
+ }
3681
+
3682
+ function formatFileSize(bytes) {
3683
+ if (!bytes) return '0 B';
3684
+ const k = 1024;
3685
+ const sizes = ['B', 'KB', 'MB', 'GB'];
3686
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
3687
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
3688
+ }
3689
+
3690
+ // ==================== 备份管理界面优化完成 ====================
1490
3691
 
1491
3692
  renderView('groups');
3693
+
3694
+ // 显示新手引导
3695
+ features.showOnboarding();
1492
3696
  }
1493
3697