diff --git a/backend/db.js b/backend/db.js index 7c0498c..d50b077 100644 --- a/backend/db.js +++ b/backend/db.js @@ -224,6 +224,15 @@ async function initSchema(connection) { INDEX idx_item (item_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); + await createIfNotExists('feature_comments', `CREATE TABLE feature_comments ( + id INT AUTO_INCREMENT PRIMARY KEY, + item_id INT NOT NULL, + user_id INT NOT NULL, + content TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_item (item_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); + await createIfNotExists('server_groups', `CREATE TABLE server_groups ( id INT AUTO_INCREMENT PRIMARY KEY, group_name VARCHAR(100) NOT NULL, diff --git a/backend/routes/features.js b/backend/routes/features.js index 601caa3..140ab1a 100644 --- a/backend/routes/features.js +++ b/backend/routes/features.js @@ -5,19 +5,31 @@ const { authenticate, requireRole } = require('../middleware/auth'); const router = express.Router(); router.get('/', authenticate, async (req, res) => { - const { group_name, server_name, sort } = req.query; + const { group_name, server_name, sort, status } = req.query; let orderClause = 'ORDER BY fi.created_at DESC'; if (sort === 'votes') orderClause = 'ORDER BY votes_count DESC, fi.created_at DESC'; const items = await query(`SELECT fi.*, (SELECT COUNT(*) FROM feature_votes WHERE item_id = fi.id) as votes_count, - (SELECT COUNT(*) FROM feature_votes WHERE item_id = fi.id AND user_id = ?) as my_vote + (SELECT COUNT(*) FROM feature_votes WHERE item_id = fi.id AND user_id = ?) as my_vote, + (SELECT COUNT(*) FROM feature_comments WHERE item_id = fi.id) as comments_count FROM feature_items fi WHERE (fi.group_name = ? OR ? = '') AND (fi.server_name = ? OR ? = '') - ${orderClause}`, [req.user.id, group_name||'', group_name||'', server_name||'', server_name||'']); + AND (fi.status = ? OR ? = '' OR ? = 'all') + ${orderClause}`, [req.user.id, group_name||'', group_name||'', server_name||'', server_name||'', status||'', status||'', status||'']); res.json(items); }); +router.get('/:id', authenticate, async (req, res) => { + const item = await getRow(`SELECT fi.*, + (SELECT COUNT(*) FROM feature_votes WHERE item_id = fi.id) as votes_count, + (SELECT COUNT(*) FROM feature_votes WHERE item_id = fi.id AND user_id = ?) as my_vote + FROM feature_items fi WHERE fi.id = ?`, [req.user.id, req.params.id]); + if (!item) return res.status(404).json({ error: '不存在' }); + item.comments = await query(`SELECT fc.*, u.username, u.role FROM feature_comments fc LEFT JOIN users u ON fc.user_id = u.id WHERE fc.item_id = ? ORDER BY fc.created_at ASC`, [req.params.id]); + res.json(item); +}); + router.post('/', authenticate, requireRole('owner'), async (req, res) => { const { title, description, group_name, server_name } = req.body; if (!title) return res.status(400).json({ error: '标题为必填' }); @@ -39,6 +51,7 @@ router.put('/:id', authenticate, requireRole('owner'), async (req, res) => { }); router.delete('/:id', authenticate, requireRole('owner'), async (req, res) => { + await query('DELETE FROM feature_comments WHERE item_id = ?', [req.params.id]); await query('DELETE FROM feature_votes WHERE item_id = ?', [req.params.id]); await query('DELETE FROM feature_items WHERE id = ?', [req.params.id]); res.json({ message: '已删除' }); @@ -56,4 +69,13 @@ router.post('/:id/vote', authenticate, async (req, res) => { res.json({ message: '投票成功' }); }); +router.post('/:id/comment', authenticate, async (req, res) => { + const { content } = req.body; + if (!content) return res.status(400).json({ error: '内容不能为空' }); + const item = await getRow('SELECT id FROM feature_items WHERE id = ?', [req.params.id]); + if (!item) return res.status(404).json({ error: '不存在' }); + await query('INSERT INTO feature_comments(item_id, user_id, content) VALUES (?,?,?)', [req.params.id, req.user.id, content]); + res.status(201).json({ message: '评论成功' }); +}); + module.exports = router; diff --git a/public/js/pages/features.js b/public/js/pages/features.js index 8760652..0c9280c 100644 --- a/public/js/pages/features.js +++ b/public/js/pages/features.js @@ -2,6 +2,7 @@ const FeaturesPage = { sort: 'time', filterGroup: '', filterServer: '', + filterStatus: '', async render() { return '
暂无
暂无内容
${U.esc(i.description)}
` : ''} -${U.esc(i.description)}
` : ''} +暂无评论
' + : item.comments.map(c => `加载失败
'; } + }, + + bindComment(i) { + const form = document.getElementById('comment-form-'+i.id); + if (!form) return; + form.onsubmit = async e => { + e.preventDefault(); + const input = document.getElementById('comment-input-'+i.id); + const content = input.value.trim(); + if (!content) return; + try { await API.post(`/features/${i.id}/comment`, { content }); input.value = ''; this.loadComments(i.id); this.load(); } catch(ex){alert(ex.message);} + }; + }, + bindVote(i) { const card = document.getElementById('feat-'+i.id); if (!card) return; - card.querySelector('.feat-vote').addEventListener('click', async () => { + const btn = card.querySelector('.feat-vote'); + if (!btn) return; + btn.addEventListener('click', async () => { try { await API.post(`/features/${i.id}/vote`); this.load(); } catch (ex) { alert(ex.message); } }); }, @@ -102,11 +150,11 @@ const FeaturesPage = { async showCreate() { const groups = await this.loadGroupsForForm(); const opts = groups.map(g => ``).join(''); - U.modal('添加项目', ` + U.modal('添加', ` `); @@ -125,7 +173,7 @@ const FeaturesPage = {