collabdocchat 2.2.0 → 2.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "collabdocchat",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "开源的实时协作文档聊天平台 - 集成任务管理、多人文档编辑、智能点名功能",
5
5
  "main": "./server/index.js",
6
6
  "type": "module",
@@ -0,0 +1,32 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ const filePath = path.join(__dirname, '../src/pages/admin-dashboard.js');
9
+
10
+ console.log('读取文件...');
11
+ let content = fs.readFileSync(filePath, 'utf8');
12
+
13
+ // 1. 从导航菜单中删除 AI助手、数据导出、协作白板、集成管理
14
+ console.log('删除菜单项...');
15
+ content = content.replace(/<button class="nav-item" data-view="ai">[\s\S]*?AI助手[\s\S]*?<\/button>\s*/g, '');
16
+ content = content.replace(/<button class="nav-item" data-view="export">[\s\S]*?数据导出[\s\S]*?<\/button>\s*/g, '');
17
+ content = content.replace(/<button class="nav-item" data-view="whiteboard">[\s\S]*?协作白板[\s\S]*?<\/button>\s*/g, '');
18
+ content = content.replace(/<button class="nav-item" data-view="integrations">[\s\S]*?集成管理[\s\S]*?<\/button>\s*/g, '');
19
+
20
+ // 2. 从 switch 语句中删除对应的 case
21
+ console.log('删除 switch case...');
22
+ content = content.replace(/case 'integrations':[\s\S]*?await renderIntegrationsView\(contentArea\);[\s\S]*?break;\s*/g, '');
23
+
24
+ // 3. 删除 renderIntegrationsView 函数
25
+ console.log('删除集成管理函数...');
26
+ content = content.replace(/\/\/ 集成管理[\s\S]*?async function renderIntegrationsView\(container\)[\s\S]*?}\s*}\s*;[\s\S]*?}\s*}\s*;/g, '');
27
+
28
+ console.log('写入文件...');
29
+ fs.writeFileSync(filePath, content, 'utf8');
30
+
31
+ console.log('✅ 第一步完成!已删除不需要的菜单项');
32
+
@@ -0,0 +1,255 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ const filePath = path.join(__dirname, '../src/pages/admin-dashboard.js');
9
+
10
+ console.log('读取文件...');
11
+ let content = fs.readFileSync(filePath, 'utf8');
12
+
13
+ // 在群聊视图的按钮组中添加 AI助手 和 协作白板
14
+ console.log('在群聊中添加 AI助手 和 协作白板按钮...');
15
+ const chatHeaderPattern = /(<h2>群聊 - \$\{currentGroup\.name\}<\/h2>\s*<div style="display: flex; gap: 10px;">[\s\S]*?<button class="btn-secondary" id="manageMuteBtn">个人禁言<\/button>)/;
16
+ const chatHeaderReplacement = `$1
17
+ <button class="btn-secondary" id="openAIBtn">🤖 AI助手</button>
18
+ <button class="btn-secondary" id="openWhiteboardBtn">🎨 协作白板</button>`;
19
+
20
+ content = content.replace(chatHeaderPattern, chatHeaderReplacement);
21
+
22
+ // 在群聊视图中添加 AI助手和白板的模态框
23
+ console.log('添加 AI助手和白板模态框...');
24
+ const chatContainerPattern = /(<div id="manageMuteModal" class="modal hidden">[\s\S]*?<\/div>\s*<\/div>)/;
25
+ const chatContainerReplacement = `$1
26
+
27
+ <!-- AI助手模态框 -->
28
+ <div id="aiModal" class="modal hidden">
29
+ <div class="modal-content" style="max-width: 800px; height: 80vh;">
30
+ <div class="modal-header">
31
+ <h3>🤖 AI助手</h3>
32
+ <button class="modal-close" id="closeAIModal">&times;</button>
33
+ </div>
34
+ <div class="ai-chat-container" style="display: flex; flex-direction: column; height: calc(100% - 60px);">
35
+ <div class="ai-chat" id="aiChatMessages" style="flex: 1; overflow-y: auto; padding: 20px; background: var(--bg-secondary);">
36
+ <div class="ai-message ai">
37
+ <p>你好!我是AI助手,有什么可以帮助你的吗?</p>
38
+ <p>你可以问我关于文档、任务、群组的问题。</p>
39
+ </div>
40
+ </div>
41
+ <div class="ai-input-container" style="padding: 15px; border-top: 1px solid var(--border);">
42
+ <textarea id="aiInputText" placeholder="向AI助手提问..." rows="3" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px; resize: none;"></textarea>
43
+ <button class="btn-primary" id="aiSendBtnModal" style="margin-top: 10px; width: 100%;">发送</button>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+
49
+ <!-- 协作白板模态框 -->
50
+ <div id="whiteboardModal" class="modal hidden">
51
+ <div class="modal-content" style="max-width: 95vw; max-height: 90vh; width: 1400px;">
52
+ <div class="modal-header">
53
+ <h3>🎨 协作白板</h3>
54
+ <div style="display: flex; gap: 10px;">
55
+ <button class="btn-secondary" id="clearCanvasBtn">清空画布</button>
56
+ <button class="btn-primary" id="saveCanvasBtn">保存白板</button>
57
+ <button class="modal-close" id="closeWhiteboardModal">&times;</button>
58
+ </div>
59
+ </div>
60
+ <div class="whiteboard-container" style="padding: 15px;">
61
+ <div class="whiteboard-toolbar" style="display: flex; gap: 10px; margin-bottom: 15px; padding: 10px; background: var(--bg-secondary); border-radius: 8px;">
62
+ <button class="tool-btn active" data-tool="pen" style="padding: 8px 15px; border: 2px solid var(--primary); border-radius: 6px; background: var(--primary); color: white;">✏️ 画笔</button>
63
+ <button class="tool-btn" data-tool="eraser" style="padding: 8px 15px; border: 2px solid var(--border); border-radius: 6px; background: transparent;">🧹 橡皮擦</button>
64
+ <input type="color" id="colorPickerCanvas" value="#000000" title="颜色" style="width: 50px; height: 40px; border: none; border-radius: 6px; cursor: pointer;">
65
+ <input type="range" id="brushSizeCanvas" min="1" max="20" value="3" title="画笔大小" style="width: 150px;">
66
+ <span id="brushSizeLabel" style="padding: 8px 15px;">大小: 3</span>
67
+ </div>
68
+ <canvas id="whiteboardCanvas" width="1300" height="600" style="border: 2px solid var(--border); background: white; cursor: crosshair; border-radius: 8px; display: block;"></canvas>
69
+ </div>
70
+ </div>
71
+ </div>`;
72
+
73
+ content = content.replace(chatContainerPattern, chatContainerReplacement);
74
+
75
+ // 在群聊视图函数末尾添加按钮事件处理
76
+ console.log('添加按钮事件处理...');
77
+ const chatEndPattern = /(sendBtn\.addEventListener\('click', sendMessage\);[\s\S]*?messageInput\.addEventListener\('keypress', \(e\) => \{[\s\S]*?if \(e\.key === 'Enter'\) sendMessage\(\);[\s\S]*?\}\);)/;
78
+ const chatEndReplacement = `$1
79
+
80
+ // AI助手按钮
81
+ document.getElementById('openAIBtn').addEventListener('click', () => {
82
+ document.getElementById('aiModal').classList.remove('hidden');
83
+ });
84
+
85
+ document.getElementById('closeAIModal').addEventListener('click', () => {
86
+ document.getElementById('aiModal').classList.add('hidden');
87
+ });
88
+
89
+ document.getElementById('aiSendBtnModal').addEventListener('click', async () => {
90
+ const input = document.getElementById('aiInputText');
91
+ const question = input.value.trim();
92
+ if (!question) return;
93
+
94
+ const chatMessages = document.getElementById('aiChatMessages');
95
+
96
+ // 显示用户消息
97
+ const userMsg = document.createElement('div');
98
+ userMsg.className = 'ai-message user';
99
+ userMsg.style.cssText = 'background: var(--primary); color: white; padding: 12px 16px; border-radius: 12px; margin: 10px 0; max-width: 70%; margin-left: auto; text-align: right;';
100
+ userMsg.textContent = question;
101
+ chatMessages.appendChild(userMsg);
102
+ input.value = '';
103
+
104
+ // 显示加载中
105
+ const loadingMsg = document.createElement('div');
106
+ loadingMsg.className = 'ai-message ai loading';
107
+ loadingMsg.style.cssText = 'background: var(--bg-tertiary); padding: 12px 16px; border-radius: 12px; margin: 10px 0; max-width: 70%;';
108
+ loadingMsg.textContent = '思考中...';
109
+ chatMessages.appendChild(loadingMsg);
110
+ chatMessages.scrollTop = chatMessages.scrollHeight;
111
+
112
+ try {
113
+ const token = localStorage.getItem('token');
114
+ const response = await fetch('http://localhost:3000/api/ai/ask', {
115
+ method: 'POST',
116
+ headers: {
117
+ 'Content-Type': 'application/json',
118
+ 'Authorization': \`Bearer \${token}\`
119
+ },
120
+ body: JSON.stringify({ question, groupId: currentGroup?._id })
121
+ });
122
+ const result = await response.json();
123
+
124
+ loadingMsg.remove();
125
+ const aiMsg = document.createElement('div');
126
+ aiMsg.className = 'ai-message ai';
127
+ aiMsg.style.cssText = 'background: var(--bg-tertiary); padding: 12px 16px; border-radius: 12px; margin: 10px 0; max-width: 70%;';
128
+ aiMsg.textContent = result.answer || '抱歉,我无法回答这个问题。';
129
+ chatMessages.appendChild(aiMsg);
130
+ chatMessages.scrollTop = chatMessages.scrollHeight;
131
+ } catch (error) {
132
+ loadingMsg.remove();
133
+ const errorMsg = document.createElement('div');
134
+ errorMsg.className = 'ai-message ai error';
135
+ errorMsg.style.cssText = 'background: var(--danger); color: white; padding: 12px 16px; border-radius: 12px; margin: 10px 0; max-width: 70%;';
136
+ errorMsg.textContent = '抱歉,发生了错误: ' + error.message;
137
+ chatMessages.appendChild(errorMsg);
138
+ }
139
+ });
140
+
141
+ // 协作白板按钮
142
+ document.getElementById('openWhiteboardBtn').addEventListener('click', () => {
143
+ document.getElementById('whiteboardModal').classList.remove('hidden');
144
+ initWhiteboard();
145
+ });
146
+
147
+ document.getElementById('closeWhiteboardModal').addEventListener('click', () => {
148
+ document.getElementById('whiteboardModal').classList.add('hidden');
149
+ });
150
+
151
+ function initWhiteboard() {
152
+ const canvas = document.getElementById('whiteboardCanvas');
153
+ if (!canvas) return;
154
+
155
+ const ctx = canvas.getContext('2d');
156
+ let isDrawing = false;
157
+ let currentTool = 'pen';
158
+ let currentColor = '#000000';
159
+ let brushSize = 3;
160
+ let lastX = 0;
161
+ let lastY = 0;
162
+
163
+ // 工具切换
164
+ document.querySelectorAll('.tool-btn').forEach(btn => {
165
+ btn.onclick = () => {
166
+ document.querySelectorAll('.tool-btn').forEach(b => {
167
+ b.style.background = 'transparent';
168
+ b.style.borderColor = 'var(--border)';
169
+ b.style.color = 'inherit';
170
+ b.classList.remove('active');
171
+ });
172
+ btn.style.background = 'var(--primary)';
173
+ btn.style.borderColor = 'var(--primary)';
174
+ btn.style.color = 'white';
175
+ btn.classList.add('active');
176
+ currentTool = btn.dataset.tool;
177
+ };
178
+ });
179
+
180
+ const colorPicker = document.getElementById('colorPickerCanvas');
181
+ if (colorPicker) {
182
+ colorPicker.onchange = (e) => {
183
+ currentColor = e.target.value;
184
+ };
185
+ }
186
+
187
+ const brushSizeInput = document.getElementById('brushSizeCanvas');
188
+ const brushSizeLabel = document.getElementById('brushSizeLabel');
189
+ if (brushSizeInput && brushSizeLabel) {
190
+ brushSizeInput.oninput = (e) => {
191
+ brushSize = e.target.value;
192
+ brushSizeLabel.textContent = \`大小: \${brushSize}\`;
193
+ };
194
+ }
195
+
196
+ // 绘画功能
197
+ canvas.onmousedown = (e) => {
198
+ isDrawing = true;
199
+ const rect = canvas.getBoundingClientRect();
200
+ lastX = e.clientX - rect.left;
201
+ lastY = e.clientY - rect.top;
202
+ };
203
+
204
+ canvas.onmousemove = (e) => {
205
+ if (!isDrawing) return;
206
+
207
+ const rect = canvas.getBoundingClientRect();
208
+ const x = e.clientX - rect.left;
209
+ const y = e.clientY - rect.top;
210
+
211
+ ctx.beginPath();
212
+ ctx.moveTo(lastX, lastY);
213
+ ctx.lineTo(x, y);
214
+ ctx.strokeStyle = currentTool === 'eraser' ? '#ffffff' : currentColor;
215
+ ctx.lineWidth = brushSize;
216
+ ctx.lineCap = 'round';
217
+ ctx.stroke();
218
+
219
+ lastX = x;
220
+ lastY = y;
221
+ };
222
+
223
+ canvas.onmouseup = () => {
224
+ isDrawing = false;
225
+ };
226
+
227
+ canvas.onmouseleave = () => {
228
+ isDrawing = false;
229
+ };
230
+
231
+ // 清空画布
232
+ document.getElementById('clearCanvasBtn').onclick = () => {
233
+ if (confirm('确定要清空画布吗?')) {
234
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
235
+ }
236
+ };
237
+
238
+ // 保存白板
239
+ document.getElementById('saveCanvasBtn').onclick = () => {
240
+ const dataURL = canvas.toDataURL('image/png');
241
+ const link = document.createElement('a');
242
+ link.download = \`whiteboard-\${Date.now()}.png\`;
243
+ link.href = dataURL;
244
+ link.click();
245
+ alert('白板已保存!');
246
+ };
247
+ }`;
248
+
249
+ content = content.replace(chatEndPattern, chatEndReplacement);
250
+
251
+ console.log('写入文件...');
252
+ fs.writeFileSync(filePath, content, 'utf8');
253
+
254
+ console.log('✅ 第二步完成!已在群聊中添加 AI助手 和 协作白板');
255
+
@@ -0,0 +1,137 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ const filePath = path.join(__dirname, '../src/pages/admin-dashboard.js');
9
+
10
+ console.log('读取文件...');
11
+ let content = fs.readFileSync(filePath, 'utf8');
12
+
13
+ // 在任务卡片中添加"查看详细"按钮
14
+ console.log('添加查看详细按钮...');
15
+ const taskCardPattern = /(taskCard\.innerHTML = `[\s\S]*?<button class="btn-danger btn-sm" data-id="\$\{task\._id\}" data-action="delete-task")/;
16
+ const taskCardReplacement = `taskCard.innerHTML = \`
17
+ <div style="display: flex; justify-content: space-between; align-items: start;">
18
+ <div style="flex: 1;">
19
+ <h3>\${task.title}</h3>
20
+ <p>\${task.description || '无描述'}</p>
21
+ <div class="task-meta">
22
+ <span class="status-badge">\${getStatusText(task.status)}</span>
23
+ <span>截止: \${task.deadline ? new Date(task.deadline).toLocaleDateString() : '无'}</span>
24
+ </div>
25
+ </div>
26
+ <div style="display: flex; gap: 10px;">
27
+ <button class="btn-primary btn-sm" data-id="\${task._id}" data-action="view-task" title="查看详细">📋 查看详细</button>
28
+ <button class="btn-danger btn-sm" data-id="\${task._id}" data-action="delete-task"`;
29
+
30
+ content = content.replace(taskCardPattern, taskCardReplacement);
31
+
32
+ // 在任务列表后添加任务详情模态框
33
+ console.log('添加任务详情模态框...');
34
+ const taskModalPattern = /(<div id="createTaskModal" class="modal hidden">[\s\S]*?<\/div>\s*<\/div>)/;
35
+ const taskModalReplacement = `$1
36
+
37
+ <!-- 任务详情模态框 -->
38
+ <div id="taskDetailModal" class="modal hidden">
39
+ <div class="modal-content" style="max-width: 800px;">
40
+ <div class="modal-header">
41
+ <h3>📋 任务详情</h3>
42
+ <button class="modal-close" id="closeTaskDetailModal">&times;</button>
43
+ </div>
44
+ <div class="modal-body" id="taskDetailContent" style="padding: 20px;">
45
+ <!-- 任务详情内容 -->
46
+ </div>
47
+ </div>
48
+ </div>`;
49
+
50
+ content = content.replace(taskModalPattern, taskModalReplacement);
51
+
52
+ // 在删除任务事件后添加查看详细事件
53
+ console.log('添加查看详细事件...');
54
+ const deleteTaskPattern = /(\/\/ 添加删除任务事件[\s\S]*?document\.querySelectorAll\('\[data-action="delete-task"\]'\)\.forEach\(btn => \{[\s\S]*?\}\);[\s\S]*?\}\);)/;
55
+ const deleteTaskReplacement = `$1
56
+
57
+ // 添加查看详细事件
58
+ document.querySelectorAll('[data-action="view-task"]').forEach(btn => {
59
+ btn.addEventListener('click', async (e) => {
60
+ e.stopPropagation();
61
+ const taskId = btn.dataset.id;
62
+ const task = result.tasks.find(t => t._id === taskId);
63
+
64
+ if (task) {
65
+ const detailContent = document.getElementById('taskDetailContent');
66
+ detailContent.innerHTML = \`
67
+ <div class="task-detail">
68
+ <div class="detail-section">
69
+ <h4>📌 任务标题</h4>
70
+ <p style="font-size: 18px; font-weight: 600; margin: 10px 0;">\${task.title}</p>
71
+ </div>
72
+
73
+ <div class="detail-section" style="margin-top: 20px;">
74
+ <h4>📝 任务描述</h4>
75
+ <p style="margin: 10px 0; line-height: 1.6;">\${task.description || '无描述'}</p>
76
+ </div>
77
+
78
+ <div class="detail-section" style="margin-top: 20px;">
79
+ <h4>📊 任务状态</h4>
80
+ <span class="status-badge" style="display: inline-block; margin: 10px 0; padding: 6px 12px; border-radius: 6px; background: var(--primary); color: white;">\${getStatusText(task.status)}</span>
81
+ </div>
82
+
83
+ <div class="detail-section" style="margin-top: 20px;">
84
+ <h4>📅 截止日期</h4>
85
+ <p style="margin: 10px 0;">\${task.deadline ? new Date(task.deadline).toLocaleString() : '无截止日期'}</p>
86
+ </div>
87
+
88
+ <div class="detail-section" style="margin-top: 20px;">
89
+ <h4>👥 分配给</h4>
90
+ <div style="display: flex; flex-wrap: wrap; gap: 10px; margin: 10px 0;">
91
+ \${task.assignedTo && task.assignedTo.length > 0 ?
92
+ task.assignedTo.map(user => \`
93
+ <div style="display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--bg-secondary); border-radius: 8px;">
94
+ <div class="avatar" style="width: 30px; height: 30px; font-size: 14px;">\${user.username?.[0]?.toUpperCase() || '?'}</div>
95
+ <span>\${user.username || '未知用户'}</span>
96
+ </div>
97
+ \`).join('') :
98
+ '<p>未分配</p>'
99
+ }
100
+ </div>
101
+ </div>
102
+
103
+ <div class="detail-section" style="margin-top: 20px;">
104
+ <h4>🕐 创建时间</h4>
105
+ <p style="margin: 10px 0;">\${new Date(task.createdAt).toLocaleString()}</p>
106
+ </div>
107
+
108
+ <div class="detail-section" style="margin-top: 20px;">
109
+ <h4>🔄 更新时间</h4>
110
+ <p style="margin: 10px 0;">\${new Date(task.updatedAt).toLocaleString()}</p>
111
+ </div>
112
+ </div>
113
+ \`;
114
+
115
+ document.getElementById('taskDetailModal').classList.remove('hidden');
116
+ }
117
+ });
118
+ });`;
119
+
120
+ content = content.replace(deleteTaskPattern, deleteTaskReplacement);
121
+
122
+ // 添加关闭任务详情模态框的事件
123
+ console.log('添加关闭模态框事件...');
124
+ const closeTaskModalPattern = /(document\.getElementById\('closeTaskModal'\)\.addEventListener\('click', \(\) => \{[\s\S]*?document\.getElementById\('createTaskModal'\)\.classList\.add\('hidden'\);[\s\S]*?\}\);)/;
125
+ const closeTaskModalReplacement = `$1
126
+
127
+ document.getElementById('closeTaskDetailModal').addEventListener('click', () => {
128
+ document.getElementById('taskDetailModal').classList.add('hidden');
129
+ });`;
130
+
131
+ content = content.replace(closeTaskModalPattern, closeTaskModalReplacement);
132
+
133
+ console.log('写入文件...');
134
+ fs.writeFileSync(filePath, content, 'utf8');
135
+
136
+ console.log('✅ 第三步完成!已为任务管理添加查看详细功能');
137
+
@@ -0,0 +1,183 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ const filePath = path.join(__dirname, '../src/pages/admin-dashboard.js');
9
+
10
+ console.log('读取文件...');
11
+ let content = fs.readFileSync(filePath, 'utf8');
12
+
13
+ // 替换知识库功能为完整实现
14
+ console.log('完善知识库功能...');
15
+ const knowledgePattern = /\/\/ 知识库管理[\s\S]*?async function renderKnowledgeView\(container\)[\s\S]*?container\.innerHTML = '<div class="empty-state">知识库功能开发中\.\.\.<\/div>';[\s\S]*?}/;
16
+ const knowledgeReplacement = `// 知识库管理
17
+ async function renderKnowledgeView(container) {
18
+ if (!currentGroup) {
19
+ container.innerHTML = '<div class="empty-state">请先选择一个群组</div>';
20
+ return;
21
+ }
22
+
23
+ try {
24
+ const token = localStorage.getItem('token');
25
+ const response = await fetch(\`http://localhost:3000/api/knowledge/group/\${currentGroup._id}\`, {
26
+ headers: { 'Authorization': \`Bearer \${token}\` }
27
+ });
28
+ const result = await response.json();
29
+ const knowledgeItems = result.data || [];
30
+
31
+ container.innerHTML = \`
32
+ <div class="view-header">
33
+ <h2>📚 知识库管理 - \${currentGroup.name}</h2>
34
+ <button class="btn-primary" id="createKnowledgeBtn">➕ 创建知识条目</button>
35
+ </div>
36
+ <div class="knowledge-grid" id="knowledgeList" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; padding: 20px;"></div>
37
+ <div id="knowledgeModal" class="modal hidden">
38
+ <div class="modal-content">
39
+ <h3 id="modalTitle">创建知识条目</h3>
40
+ <form id="knowledgeForm">
41
+ <div class="form-group">
42
+ <label>📌 标题</label>
43
+ <input type="text" name="title" required style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;">
44
+ </div>
45
+ <div class="form-group">
46
+ <label>📝 内容</label>
47
+ <textarea name="content" rows="6" required style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;"></textarea>
48
+ </div>
49
+ <div class="form-group">
50
+ <label>🏷️ 标签(用逗号分隔)</label>
51
+ <input type="text" name="tags" placeholder="例如: 技术,文档,教程" style="width: 100%; padding: 10px; border: 1px solid var(--border); border-radius: 8px;">
52
+ </div>
53
+ <div style="display: flex; gap: 10px; margin-top: 20px;">
54
+ <button type="submit" class="btn-primary" style="flex: 1;">保存</button>
55
+ <button type="button" class="btn-secondary" id="closeKnowledgeModal" style="flex: 1;">取消</button>
56
+ </div>
57
+ </form>
58
+ </div>
59
+ </div>
60
+ \`;
61
+
62
+ const knowledgeList = document.getElementById('knowledgeList');
63
+ if (knowledgeItems.length === 0) {
64
+ knowledgeList.innerHTML = '<div class="empty-state" style="grid-column: 1/-1;">暂无知识条目</div>';
65
+ } else {
66
+ knowledgeItems.forEach(item => {
67
+ const card = document.createElement('div');
68
+ card.className = 'knowledge-card';
69
+ card.style.cssText = 'background: var(--bg-secondary); padding: 20px; border-radius: 12px; border: 1px solid var(--border); transition: transform 0.2s, box-shadow 0.2s;';
70
+ card.innerHTML = \`
71
+ <h3 style="margin: 0 0 10px 0; font-size: 18px;">\${item.title}</h3>
72
+ <p style="color: var(--text-secondary); margin: 0 0 15px 0; line-height: 1.6;">\${item.content.substring(0, 150)}\${item.content.length > 150 ? '...' : ''}</p>
73
+ <div class="knowledge-meta" style="font-size: 12px; color: var(--text-tertiary); margin-bottom: 10px;">
74
+ <span>👤 \${item.creator?.username || '未知'}</span>
75
+ <span style="margin-left: 15px;">📅 \${new Date(item.createdAt).toLocaleDateString()}</span>
76
+ </div>
77
+ \${item.tags && item.tags.length > 0 ? \`
78
+ <div class="tags" style="display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 15px;">
79
+ \${item.tags.map(tag => \`<span class="tag" style="background: var(--primary); color: white; padding: 4px 10px; border-radius: 12px; font-size: 12px;">\${tag}</span>\`).join('')}
80
+ </div>
81
+ \` : ''}
82
+ <div style="display: flex; gap: 10px;">
83
+ <button class="btn-secondary btn-sm" data-id="\${item._id}" data-action="edit" style="flex: 1;">✏️ 编辑</button>
84
+ <button class="btn-danger btn-sm" data-id="\${item._id}" data-action="delete" style="flex: 1;">🗑️ 删除</button>
85
+ </div>
86
+ \`;
87
+ card.onmouseenter = () => {
88
+ card.style.transform = 'translateY(-4px)';
89
+ card.style.boxShadow = '0 8px 16px rgba(0,0,0,0.1)';
90
+ };
91
+ card.onmouseleave = () => {
92
+ card.style.transform = 'translateY(0)';
93
+ card.style.boxShadow = 'none';
94
+ };
95
+ knowledgeList.appendChild(card);
96
+ });
97
+
98
+ document.querySelectorAll('[data-action="edit"]').forEach(btn => {
99
+ btn.addEventListener('click', async () => {
100
+ const item = knowledgeItems.find(k => k._id === btn.dataset.id);
101
+ document.getElementById('modalTitle').textContent = '编辑知识条目';
102
+ document.querySelector('[name="title"]').value = item.title;
103
+ document.querySelector('[name="content"]').value = item.content;
104
+ document.querySelector('[name="tags"]').value = item.tags?.join(', ') || '';
105
+ document.getElementById('knowledgeForm').dataset.editId = item._id;
106
+ document.getElementById('knowledgeModal').classList.remove('hidden');
107
+ });
108
+ });
109
+
110
+ document.querySelectorAll('[data-action="delete"]').forEach(btn => {
111
+ btn.addEventListener('click', async () => {
112
+ if (confirm('确定要删除这个知识条目吗?')) {
113
+ try {
114
+ await fetch(\`http://localhost:3000/api/knowledge/\${btn.dataset.id}\`, {
115
+ method: 'DELETE',
116
+ headers: { 'Authorization': \`Bearer \${token}\` }
117
+ });
118
+ alert('删除成功!');
119
+ await renderKnowledgeView(container);
120
+ } catch (error) {
121
+ alert('删除失败: ' + error.message);
122
+ }
123
+ }
124
+ });
125
+ });
126
+ }
127
+
128
+ document.getElementById('createKnowledgeBtn').addEventListener('click', () => {
129
+ document.getElementById('modalTitle').textContent = '创建知识条目';
130
+ document.getElementById('knowledgeForm').reset();
131
+ delete document.getElementById('knowledgeForm').dataset.editId;
132
+ document.getElementById('knowledgeModal').classList.remove('hidden');
133
+ });
134
+
135
+ document.getElementById('closeKnowledgeModal').addEventListener('click', () => {
136
+ document.getElementById('knowledgeModal').classList.add('hidden');
137
+ });
138
+
139
+ document.getElementById('knowledgeForm').addEventListener('submit', async (e) => {
140
+ e.preventDefault();
141
+ const formData = new FormData(e.target);
142
+ const data = {
143
+ title: formData.get('title'),
144
+ content: formData.get('content'),
145
+ tags: formData.get('tags').split(',').map(t => t.trim()).filter(t => t),
146
+ groupId: currentGroup._id
147
+ };
148
+
149
+ try {
150
+ const editId = e.target.dataset.editId;
151
+ const url = editId
152
+ ? \`http://localhost:3000/api/knowledge/\${editId}\`
153
+ : 'http://localhost:3000/api/knowledge';
154
+ const method = editId ? 'PUT' : 'POST';
155
+
156
+ await fetch(url, {
157
+ method,
158
+ headers: {
159
+ 'Content-Type': 'application/json',
160
+ 'Authorization': \`Bearer \${token}\`
161
+ },
162
+ body: JSON.stringify(data)
163
+ });
164
+
165
+ alert(editId ? '更新成功!' : '创建成功!');
166
+ document.getElementById('knowledgeModal').classList.add('hidden');
167
+ await renderKnowledgeView(container);
168
+ } catch (error) {
169
+ alert('操作失败: ' + error.message);
170
+ }
171
+ });
172
+ } catch (error) {
173
+ container.innerHTML = \`<div class="empty-state">加载失败: \${error.message}</div>\`;
174
+ }
175
+ }`;
176
+
177
+ content = content.replace(knowledgePattern, knowledgeReplacement);
178
+
179
+ console.log('写入文件...');
180
+ fs.writeFileSync(filePath, content, 'utf8');
181
+
182
+ console.log('✅ 第四步完成!已完善知识库功能');
183
+