手把手教学:为你的WordPress网站添加在线白板与团队头脑风暴功能
引言:为什么你的网站需要协作工具?
在当今数字化工作环境中,远程协作已成为常态。无论是教育机构、创意团队还是企业项目组,都需要高效的在线协作工具来促进沟通和创意产出。然而,许多专业协作工具价格昂贵,且难以与现有网站无缝集成。
本文将指导你通过WordPress代码二次开发,为你的网站添加在线白板和团队头脑风暴功能。这不仅能为你的用户提供价值,还能显著提升网站的互动性和专业性。我们将从零开始,逐步构建一个功能完整的协作系统。
第一部分:准备工作与环境搭建
1.1 理解WordPress开发基础
在开始之前,你需要具备以下基础知识:
- 基本的PHP编程能力
- HTML/CSS/JavaScript前端知识
- WordPress主题或插件开发经验
- 对REST API的基本了解
如果你已经熟悉这些技术,可以直接进入下一部分。如果你是初学者,建议先学习WordPress官方开发文档。
1.2 开发环境配置
首先,确保你有一个本地开发环境:
- 安装XAMPP、MAMP或Local by Flywheel
- 下载最新版WordPress并安装
- 启用调试模式(在wp-config.php中添加
define('WP_DEBUG', true);) - 安装代码编辑器(如VS Code、Sublime Text或PHPStorm)
1.3 创建自定义插件
我们将创建一个独立插件来实现功能,而不是修改主题文件,这样可以确保功能独立且易于维护。
创建插件目录结构:
wp-content/plugins/team-whiteboard/
├── team-whiteboard.php
├── includes/
│ ├── class-database.php
│ ├── class-whiteboard.php
│ └── class-brainstorm.php
├── assets/
│ ├── css/
│ ├── js/
│ └── images/
├── templates/
└── vendor/
第二部分:构建在线白板核心功能
2.1 数据库设计与实现
首先,我们需要设计数据库表来存储白板数据。在includes/class-database.php中:
<?php
class TeamWhiteboard_Database {
private static $instance = null;
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action('init', array($this, 'create_tables'));
}
public function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$table_name = $wpdb->prefix . 'team_whiteboards';
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id mediumint(9) NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
content longtext,
created_by bigint(20) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
status varchar(20) DEFAULT 'active',
settings text,
PRIMARY KEY (id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
// 创建白板元素表
$elements_table = $wpdb->prefix . 'whiteboard_elements';
$sql_elements = "CREATE TABLE IF NOT EXISTS $elements_table (
id mediumint(9) NOT NULL AUTO_INCREMENT,
whiteboard_id mediumint(9) NOT NULL,
element_type varchar(50) NOT NULL,
element_data text NOT NULL,
created_by bigint(20) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
position_x float DEFAULT 0,
position_y float DEFAULT 0,
z_index int DEFAULT 0,
PRIMARY KEY (id),
FOREIGN KEY (whiteboard_id) REFERENCES $table_name(id) ON DELETE CASCADE
) $charset_collate;";
dbDelta($sql_elements);
}
}
?>
2.2 白板前端界面开发
接下来,我们创建白板的HTML结构和CSS样式。在templates/whiteboard.php中:
<div class="team-whiteboard-container">
<div class="whiteboard-header">
<h2 id="whiteboard-title">新白板</h2>
<div class="whiteboard-actions">
<button id="save-whiteboard" class="btn btn-primary">保存</button>
<button id="clear-whiteboard" class="btn btn-secondary">清空</button>
<button id="export-whiteboard" class="btn btn-success">导出</button>
</div>
</div>
<div class="whiteboard-toolbar">
<div class="tool-group">
<button class="tool-btn active" data-tool="select" title="选择工具">
<i class="fas fa-mouse-pointer"></i>
</button>
<button class="tool-btn" data-tool="pen" title="画笔">
<i class="fas fa-pen"></i>
</button>
<button class="tool-btn" data-tool="line" title="直线">
<i class="fas fa-minus"></i>
</button>
<button class="tool-btn" data-tool="rectangle" title="矩形">
<i class="fas fa-square"></i>
</button>
<button class="tool-btn" data-tool="circle" title="圆形">
<i class="fas fa-circle"></i>
</button>
<button class="tool-btn" data-tool="text" title="文本">
<i class="fas fa-font"></i>
</button>
</div>
<div class="tool-group">
<input type="color" id="color-picker" value="#000000">
<input type="range" id="brush-size" min="1" max="50" value="3">
<span id="brush-size-value">3px</span>
</div>
<div class="tool-group">
<button class="tool-btn" data-action="undo" title="撤销">
<i class="fas fa-undo"></i>
</button>
<button class="tool-btn" data-action="redo" title="重做">
<i class="fas fa-redo"></i>
</button>
<button class="tool-btn" data-action="delete" title="删除选中">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="whiteboard-wrapper">
<canvas id="whiteboard-canvas"></canvas>
<div class="whiteboard-grid"></div>
</div>
<div class="whiteboard-sidebar">
<div class="sidebar-section">
<h4>参与者</h4>
<ul id="participants-list">
<!-- 参与者列表将通过JavaScript动态生成 -->
</ul>
</div>
<div class="sidebar-section">
<h4>聊天</h4>
<div id="chat-messages"></div>
<div class="chat-input">
<input type="text" id="chat-input" placeholder="输入消息...">
<button id="send-chat">发送</button>
</div>
</div>
</div>
</div>
2.3 白板画布与绘图功能实现
现在,我们使用JavaScript和Canvas API实现白板的绘图功能。在assets/js/whiteboard.js中:
class Whiteboard {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.isDrawing = false;
this.currentTool = 'pen';
this.currentColor = '#000000';
this.brushSize = 3;
this.lastX = 0;
this.lastY = 0;
this.history = [];
this.historyIndex = -1;
this.elements = [];
this.selectedElement = null;
this.initCanvas();
this.setupEventListeners();
this.setupWebSocket();
}
initCanvas() {
// 设置画布尺寸
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
// 设置初始样式
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.strokeStyle = this.currentColor;
this.ctx.lineWidth = this.brushSize;
// 绘制网格背景
this.drawGrid();
}
resizeCanvas() {
const container = this.canvas.parentElement;
this.canvas.width = container.clientWidth;
this.canvas.height = container.clientHeight;
this.drawGrid();
this.redraw();
}
drawGrid() {
const gridSize = 20;
const width = this.canvas.width;
const height = this.canvas.height;
this.ctx.save();
this.ctx.strokeStyle = '#f0f0f0';
this.ctx.lineWidth = 1;
// 绘制垂直线
for (let x = 0; x <= width; x += gridSize) {
this.ctx.beginPath();
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, height);
this.ctx.stroke();
}
// 绘制水平线
for (let y = 0; y <= height; y += gridSize) {
this.ctx.beginPath();
this.ctx.moveTo(0, y);
this.ctx.lineTo(width, y);
this.ctx.stroke();
}
this.ctx.restore();
}
setupEventListeners() {
// 鼠标事件
this.canvas.addEventListener('mousedown', (e) => this.startDrawing(e));
this.canvas.addEventListener('mousemove', (e) => this.draw(e));
this.canvas.addEventListener('mouseup', () => this.stopDrawing());
this.canvas.addEventListener('mouseout', () => this.stopDrawing());
// 触摸事件(移动设备支持)
this.canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
this.startDrawing(touch);
});
this.canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
const touch = e.touches[0];
this.draw(touch);
});
this.canvas.addEventListener('touchend', () => this.stopDrawing());
// 工具选择
document.querySelectorAll('.tool-btn[data-tool]').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.tool-btn[data-tool]').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
this.currentTool = e.target.dataset.tool;
});
});
// 颜色选择
document.getElementById('color-picker').addEventListener('change', (e) => {
this.currentColor = e.target.value;
this.ctx.strokeStyle = this.currentColor;
});
// 笔刷大小
document.getElementById('brush-size').addEventListener('input', (e) => {
this.brushSize = e.target.value;
document.getElementById('brush-size-value').textContent = `${this.brushSize}px`;
this.ctx.lineWidth = this.brushSize;
});
// 动作按钮
document.querySelector('[data-action="undo"]').addEventListener('click', () => this.undo());
document.querySelector('[data-action="redo"]').addEventListener('click', () => this.redo());
document.querySelector('[data-action="delete"]').addEventListener('click', () => this.deleteSelected());
}
startDrawing(e) {
this.isDrawing = true;
const rect = this.canvas.getBoundingClientRect();
this.lastX = e.clientX - rect.left;
this.lastY = e.clientY - rect.top;
// 保存当前状态到历史记录
this.saveState();
// 根据工具类型执行不同操作
if (this.currentTool === 'text') {
this.addTextElement(this.lastX, this.lastY);
}
}
draw(e) {
if (!this.isDrawing) return;
const rect = this.canvas.getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
switch (this.currentTool) {
case 'pen':
this.drawFreehand(currentX, currentY);
break;
case 'line':
this.drawLine(currentX, currentY);
break;
case 'rectangle':
this.drawRectangle(currentX, currentY);
break;
case 'circle':
this.drawCircle(currentX, currentY);
break;
}
this.lastX = currentX;
this.lastY = currentY;
}
drawFreehand(x, y) {
this.ctx.beginPath();
this.ctx.moveTo(this.lastX, this.lastY);
this.ctx.lineTo(x, y);
this.ctx.stroke();
// 发送绘图数据到服务器
this.sendDrawingData({
type: 'freehand',
from: {x: this.lastX, y: this.lastY},
to: {x, y},
color: this.currentColor,
size: this.brushSize
});
}
stopDrawing() {
this.isDrawing = false;
this.ctx.beginPath();
}
saveState() {
// 只保留最近50个状态
if (this.historyIndex < this.history.length - 1) {
this.history = this.history.slice(0, this.historyIndex + 1);
}
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
this.history.push(imageData);
this.historyIndex++;
// 限制历史记录数量
if (this.history.length > 50) {
this.history.shift();
this.historyIndex--;
}
}
undo() {
if (this.historyIndex > 0) {
this.historyIndex--;
const imageData = this.history[this.historyIndex];
this.ctx.putImageData(imageData, 0, 0);
}
}
redo() {
if (this.historyIndex < this.history.length - 1) {
this.historyIndex++;
const imageData = this.history[this.historyIndex];
this.ctx.putImageData(imageData, 0, 0);
}
}
setupWebSocket() {
// 这里将实现WebSocket连接,用于实时协作
// 实际实现需要服务器端WebSocket支持
console.log('WebSocket连接将在服务器端配置后实现');
}
sendDrawingData(data) {
// 发送绘图数据到服务器,以便其他参与者可以看到
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'drawing',
data: data,
timestamp: Date.now(),
userId: window.userId || 'anonymous'
}));
}
}
redraw() {
// 重绘画布上的所有元素
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.drawGrid();
// 重绘所有保存的元素
this.elements.forEach(element => {
this.drawElement(element);
});
}
drawElement(element) {
// 根据元素类型绘制
switch (element.type) {
case 'freehand':
this.ctx.beginPath();
this.ctx.moveTo(element.from.x, element.from.y);
this.ctx.lineTo(element.to.x, element.to.y);
this.ctx.strokeStyle = element.color;
this.ctx.lineWidth = element.size;
this.ctx.stroke();
break;
// 其他元素类型的绘制逻辑
}
}
}
// 初始化白板
document.addEventListener('DOMContentLoaded', () => {
const whiteboard = new Whiteboard('whiteboard-canvas');
});
第三部分:实现团队头脑风暴功能
3.1 头脑风暴数据库设计
在includes/class-brainstorm.php中,我们扩展数据库设计:
<?php
class TeamWhiteboard_Brainstorm {
public function create_brainstorm_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
// 头脑风暴会话表
$sessions_table = $wpdb->prefix . 'brainstorm_sessions';
$sql_sessions = "CREATE TABLE IF NOT EXISTS $sessions_table (
id mediumint(9) NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
description text,
created_by bigint(20) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
status varchar(20) DEFAULT 'active',
settings text,
PRIMARY KEY (id)
) $charset_collate;";
// 想法/便签表
$ideas_table = $wpdb->prefix . 'brainstorm_ideas';
$sql_ideas = "CREATE TABLE IF NOT EXISTS $ideas_table (
id mediumint(9) NOT NULL AUTO_INCREMENT,
session_id mediumint(9) NOT NULL,
content text NOT NULL,
created_by bigint(20) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
category varchar(50),
votes int DEFAULT 0,
position_x float DEFAULT 0,
position_y float DEFAULT 0,
color varchar(20) DEFAULT '#FFFF99',
PRIMARY KEY (id),
FOREIGN KEY (session_id) REFERENCES $sessions_table(id) ON DELETE CASCADE
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql_sessions);
dbDelta($sql_ideas);
}
}
?>
3.2 头脑风暴前端界面
创建templates/brainstorm.php:
<div class="brainstorm-container">
<div class="brainstorm-header">
手把手教学:为你的WordPress网站添加在线白板与团队头脑风暴功能(续)
第三部分:实现团队头脑风暴功能(续)
3.2 头脑风暴前端界面(续)
<div class="brainstorm-container">
<div class="brainstorm-header">
<h2 id="session-title">头脑风暴会议</h2>
<div class="session-info">
<span id="participant-count">0 参与者</span>
<span id="idea-count">0 个想法</span>
</div>
<div class="session-controls">
<button id="new-idea-btn" class="btn btn-primary">
<i class="fas fa-plus"></i> 添加想法
</button>
<button id="timer-toggle" class="btn btn-secondary">
<i class="fas fa-clock"></i> 计时器
</button>
<button id="export-ideas" class="btn btn-success">
<i class="fas fa-download"></i> 导出
</button>
</div>
</div>
<div class="brainstorm-main">
<!-- 左侧工具栏 -->
<div class="brainstorm-sidebar">
<div class="sidebar-section">
<h4>分类</h4>
<div class="category-list">
<div class="category-item active" data-category="all">
<span class="category-color" style="background:#f0f0f0"></span>
<span>全部想法</span>
</div>
<div class="category-item" data-category="feature">
<span class="category-color" style="background:#FF9999"></span>
<span>功能建议</span>
</div>
<div class="category-item" data-category="improvement">
<span class="category-color" style="background:#99FF99"></span>
<span>改进建议</span>
</div>
<div class="category-item" data-category="question">
<span class="category-color" style="background:#9999FF"></span>
<span>问题反馈</span>
</div>
</div>
<div class="add-category">
<input type="text" id="new-category-name" placeholder="新分类名称">
<input type="color" id="new-category-color" value="#CCCCCC">
<button id="add-category-btn">添加</button>
</div>
</div>
<div class="sidebar-section">
<h4>投票设置</h4>
<div class="voting-settings">
<label>每人票数:</label>
<input type="number" id="votes-per-user" min="1" max="20" value="5">
<div class="voting-status">
<p>已用票数:<span id="used-votes">0</span>/<span id="total-votes">5</span></p>
</div>
</div>
</div>
<div class="sidebar-section">
<h4>计时器</h4>
<div class="timer-container">
<div class="timer-display" id="timer-display">10:00</div>
<div class="timer-controls">
<button id="start-timer">开始</button>
<button id="pause-timer">暂停</button>
<button id="reset-timer">重置</button>
</div>
<div class="timer-presets">
<button class="timer-preset" data-time="300">5分钟</button>
<button class="timer-preset" data-time="600">10分钟</button>
<button class="timer-preset" data-time="900">15分钟</button>
</div>
</div>
</div>
</div>
<!-- 主工作区 -->
<div class="brainstorm-workspace">
<div class="workspace-tools">
<button class="workspace-tool" data-action="arrange" title="自动排列">
<i class="fas fa-th"></i>
</button>
<button class="workspace-tool" data-action="cluster" title="按分类分组">
<i class="fas fa-object-group"></i>
</button>
<button class="workspace-tool" data-action="clear" title="清空工作区">
<i class="fas fa-broom"></i>
</button>
</div>
<div class="ideas-container" id="ideas-container">
<!-- 想法卡片将通过JavaScript动态生成 -->
</div>
</div>
<!-- 右侧聊天和参与者面板 -->
<div class="brainstorm-participants">
<div class="participants-section">
<h4>参与者 (<span id="active-participants">0</span>)</h4>
<ul id="participants-list">
<!-- 参与者列表 -->
</ul>
</div>
<div class="chat-section">
<h4>讨论区</h4>
<div class="chat-messages" id="brainstorm-chat">
<!-- 聊天消息 -->
</div>
<div class="chat-input">
<input type="text" id="brainstorm-chat-input" placeholder="输入消息...">
<button id="send-brainstorm-chat">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</div>
</div>
</div>
3.3 头脑风暴JavaScript实现
创建assets/js/brainstorm.js:
class BrainstormSession {
constructor(sessionId) {
this.sessionId = sessionId;
this.ideas = [];
this.categories = [];
this.participants = [];
this.currentUser = null;
this.votesUsed = 0;
this.votesTotal = 5;
this.timer = null;
this.timerSeconds = 600; // 默认10分钟
this.isTimerRunning = false;
this.init();
this.loadSessionData();
this.setupEventListeners();
this.connectWebSocket();
}
init() {
// 初始化用户信息
this.currentUser = {
id: window.userId || 'user_' + Math.random().toString(36).substr(2, 9),
name: window.userName || '匿名用户',
color: this.getRandomColor()
};
// 初始化默认分类
this.categories = [
{ id: 'all', name: '全部想法', color: '#f0f0f0' },
{ id: 'feature', name: '功能建议', color: '#FF9999' },
{ id: 'improvement', name: '改进建议', color: '#99FF99' },
{ id: 'question', name: '问题反馈', color: '#9999FF' }
];
// 更新UI
this.updateParticipantCount();
this.renderCategories();
}
loadSessionData() {
// 从服务器加载会话数据
fetch(`/wp-json/team-whiteboard/v1/brainstorm/${this.sessionId}`)
.then(response => response.json())
.then(data => {
this.ideas = data.ideas || [];
this.categories = data.categories || this.categories;
this.participants = data.participants || [];
this.renderIdeas();
this.renderParticipants();
})
.catch(error => {
console.error('加载会话数据失败:', error);
});
}
setupEventListeners() {
// 添加想法按钮
document.getElementById('new-idea-btn').addEventListener('click', () => {
this.showIdeaForm();
});
// 分类点击事件
document.querySelectorAll('.category-item').forEach(item => {
item.addEventListener('click', (e) => {
const category = e.currentTarget.dataset.category;
this.filterIdeasByCategory(category);
// 更新活动状态
document.querySelectorAll('.category-item').forEach(i => {
i.classList.remove('active');
});
e.currentTarget.classList.add('active');
});
});
// 添加分类
document.getElementById('add-category-btn').addEventListener('click', () => {
this.addCategory();
});
// 投票设置
document.getElementById('votes-per-user').addEventListener('change', (e) => {
this.votesTotal = parseInt(e.target.value);
document.getElementById('total-votes').textContent = this.votesTotal;
});
// 计时器控制
document.getElementById('start-timer').addEventListener('click', () => {
this.startTimer();
});
document.getElementById('pause-timer').addEventListener('click', () => {
this.pauseTimer();
});
document.getElementById('reset-timer').addEventListener('click', () => {
this.resetTimer();
});
// 预设时间按钮
document.querySelectorAll('.timer-preset').forEach(btn => {
btn.addEventListener('click', (e) => {
const seconds = parseInt(e.currentTarget.dataset.time);
this.setTimer(seconds);
});
});
// 工作区工具
document.querySelectorAll('.workspace-tool').forEach(tool => {
tool.addEventListener('click', (e) => {
const action = e.currentTarget.dataset.action;
this.handleWorkspaceAction(action);
});
});
// 聊天发送
document.getElementById('send-brainstorm-chat').addEventListener('click', () => {
this.sendChatMessage();
});
document.getElementById('brainstorm-chat-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendChatMessage();
}
});
}
showIdeaForm() {
// 创建想法表单模态框
const modal = document.createElement('div');
modal.className = 'idea-form-modal';
modal.innerHTML = `
<div class="modal-content">
<h3>添加新想法</h3>
<div class="form-group">
<label>想法内容</label>
<textarea id="idea-content" rows="4" placeholder="输入你的想法..."></textarea>
</div>
<div class="form-group">
<label>分类</label>
<select id="idea-category">
${this.categories.filter(c => c.id !== 'all').map(c =>
`<option value="${c.id}">${c.name}</option>`
).join('')}
</select>
</div>
<div class="form-group">
<label>颜色</label>
<input type="color" id="idea-color" value="#FFFF99">
</div>
<div class="modal-actions">
<button id="cancel-idea" class="btn btn-secondary">取消</button>
<button id="submit-idea" class="btn btn-primary">提交</button>
</div>
</div>
`;
document.body.appendChild(modal);
// 事件监听
document.getElementById('cancel-idea').addEventListener('click', () => {
document.body.removeChild(modal);
});
document.getElementById('submit-idea').addEventListener('click', () => {
this.submitIdea();
document.body.removeChild(modal);
});
}
submitIdea() {
const content = document.getElementById('idea-content').value;
const category = document.getElementById('idea-category').value;
const color = document.getElementById('idea-color').value;
if (!content.trim()) {
alert('请输入想法内容');
return;
}
const idea = {
id: 'idea_' + Date.now(),
content: content,
category: category,
color: color,
createdBy: this.currentUser,
createdAt: new Date().toISOString(),
votes: 0,
position: {
x: Math.random() * 600,
y: Math.random() * 400
}
};
this.ideas.push(idea);
this.renderIdea(idea);
this.updateIdeaCount();
// 发送到服务器
this.saveIdea(idea);
// 广播给其他参与者
this.broadcast({
type: 'new_idea',
data: idea
});
}
renderIdeas() {
const container = document.getElementById('ideas-container');
container.innerHTML = '';
this.ideas.forEach(idea => {
this.renderIdea(idea);
});
this.updateIdeaCount();
}
renderIdea(idea) {
const container = document.getElementById('ideas-container');
const ideaElement = document.createElement('div');
ideaElement.className = 'idea-card';
ideaElement.dataset.id = idea.id;
ideaElement.dataset.category = idea.category;
ideaElement.style.left = idea.position.x + 'px';
ideaElement.style.top = idea.position.y + 'px';
ideaElement.style.backgroundColor = idea.color;
ideaElement.innerHTML = `
<div class="idea-header">
<span class="idea-author">${idea.createdBy.name}</span>
<span class="idea-time">${this.formatTime(idea.createdAt)}</span>
</div>
<div class="idea-content">${this.escapeHtml(idea.content)}</div>
<div class="idea-footer">
<button class="vote-btn" data-id="${idea.id}">
<i class="fas fa-thumbs-up"></i>
<span class="vote-count">${idea.votes}</span>
</button>
<button class="delete-btn" data-id="${idea.id}">
<i class="fas fa-trash"></i>
</button>
</div>
`;
container.appendChild(ideaElement);
// 添加拖拽功能
this.makeDraggable(ideaElement, idea);
// 投票按钮事件
ideaElement.querySelector('.vote-btn').addEventListener('click', (e) => {
e.stopPropagation();
this.voteForIdea(idea.id);
});
// 删除按钮事件
ideaElement.querySelector('.delete-btn').addEventListener('click', (e) => {
e.stopPropagation();
if (confirm('确定要删除这个想法吗?')) {
this.deleteIdea(idea.id);
}
});
}
makeDraggable(element, idea) {
let isDragging = false;
let offsetX, offsetY;
element.addEventListener('mousedown', startDrag);
element.addEventListener('touchstart', startDrag);
function startDrag(e) {
isDragging = true;
if (e.type === 'mousedown') {
offsetX = e.clientX - element.offsetLeft;
offsetY = e.clientY - element.offsetTop;
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', stopDrag);
} else {
const touch = e.touches[0];
offsetX = touch.clientX - element.offsetLeft;
offsetY = touch.clientY - element.offsetTop;
document.addEventListener('touchmove', drag);
document.addEventListener('touchend', stopDrag);
}
e.preventDefault();
}
const drag = (e) => {
if (!isDragging) return;
let clientX, clientY;
if (e.type === 'mousemove') {
clientX = e.clientX;
clientY = e.clientY;
} else {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
}
const container = document.getElementById('ideas-container');
const maxX = container.clientWidth - element.offsetWidth;
const maxY = container.clientHeight - element.offsetHeight;
let x = clientX - offsetX;
let y = clientY - offsetY;
// 限制在容器内
x = Math.max(0, Math.min(x, maxX));
y = Math.max(0, Math.min(y, maxY));
element.style.left = x + 'px';
element.style.top = y + 'px';
// 更新想法位置
idea.position.x = x;
idea.position.y = y;
// 广播位置更新
this.broadcast({
type: 'move_idea',
data: {
id: idea.id,
position: idea.position
}
});
}.bind(this);
function stopDrag() {
isDragging = false;
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', stopDrag);
document.removeEventListener('touchmove', drag);
document.removeEventListener('touchend', stopDrag);
}
}
voteForIdea(ideaId) {
if (this.votesUsed >= this.votesTotal) {
alert('你的投票次数已用完!');
return;
}
const idea = this.ideas.find(i => i.id === ideaId);
if (!idea) return;
idea.votes++;
this.votesUsed++;
// 更新UI
const voteBtn = document.querySelector(`.vote-btn[data-id="${ideaId}"] .vote-count`);
if (voteBtn) {
voteBtn.textContent = idea.votes;
}
document.getElementById('used-votes').textContent = this.votesUsed;
// 发送到服务器
this.updateIdeaVotes(ideaId, idea.votes);
// 广播投票
this.broadcast({
type: 'vote',
data: {
ideaId: ideaId,
votes: idea.votes
}
});
}
filterIdeasByCategory(category) {
const ideas = document.querySelectorAll('.idea-card');
ideas.forEach(idea => {
if (category === 'all' || idea.dataset.category === category) {
idea.style.display = 'block';
} else {
idea.style.display = 'none';
}
});
}
addCategory() {
const nameInput = document.getElementById('new-category-name');
const colorInput = document.getElementById('new-category-color');
const name = nameInput.value.trim();
const color = colorInput.value;
if (!name) {
alert('请输入分类名称');
return;
}
