From d61ad2109f14e46e8bea36d23bf7eb817742c6b4 Mon Sep 17 00:00:00 2001 From: canglan Date: Tue, 14 Jul 2026 03:11:26 +0800 Subject: [PATCH] refactor: remove polls+features, move to standalone mc-vote repo --- backend/routes/features.js | 51 ----------------- backend/routes/polls.js | 72 ------------------------ backend/server.js | 2 - public/index.html | 2 - public/js/app.js | 7 --- public/js/pages/features.js | 89 ----------------------------- public/js/pages/polls.js | 108 ------------------------------------ 7 files changed, 331 deletions(-) delete mode 100644 backend/routes/features.js delete mode 100644 backend/routes/polls.js delete mode 100644 public/js/pages/features.js delete mode 100644 public/js/pages/polls.js diff --git a/backend/routes/features.js b/backend/routes/features.js deleted file mode 100644 index d92f3a9..0000000 --- a/backend/routes/features.js +++ /dev/null @@ -1,51 +0,0 @@ -const express = require('express'); -const { query, getRow } = require('../db'); -const { authenticate, requireRole } = require('../middleware/auth'); - -const router = express.Router(); - -router.get('/', authenticate, async (req, res) => { - 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 - FROM feature_items fi ORDER BY votes_count DESC, fi.created_at DESC`, [req.user.id]); - res.json(items); -}); - -router.post('/', authenticate, requireRole('owner'), async (req, res) => { - const { title, description } = req.body; - if (!title) return res.status(400).json({ error: '标题为必填' }); - const r = await query('INSERT INTO feature_items(title, description) VALUES (?,?)', [title, description||'']); - res.status(201).json({ id: r.insertId }); -}); - -router.put('/:id', authenticate, requireRole('owner'), async (req, res) => { - const fields = {}; - if (req.body.title) fields.title = req.body.title; - if (req.body.description !== undefined) fields.description = req.body.description; - if (req.body.status) fields.status = req.body.status; - if (!Object.keys(fields).length) return res.status(400).json({ error: '无更新内容' }); - const sets = Object.keys(fields).map(k => `${k} = ?`).join(', '); - await query(`UPDATE feature_items SET ${sets} WHERE id = ?`, [...Object.values(fields), req.params.id]); - res.json({ message: '更新成功' }); -}); - -router.delete('/:id', authenticate, requireRole('owner'), async (req, res) => { - 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: '已删除' }); -}); - -router.post('/:id/vote', authenticate, async (req, res) => { - const item = await getRow('SELECT id FROM feature_items WHERE id = ?', [req.params.id]); - if (!item) return res.status(404).json({ error: '不存在' }); - const voted = await getRow('SELECT id FROM feature_votes WHERE item_id = ? AND user_id = ?', [item.id, req.user.id]); - if (voted) { - await query('DELETE FROM feature_votes WHERE item_id = ? AND user_id = ?', [item.id, req.user.id]); - return res.json({ message: '已取消投票' }); - } - await query('INSERT INTO feature_votes(item_id, user_id) VALUES (?,?)', [item.id, req.user.id]); - res.json({ message: '投票成功' }); -}); - -module.exports = router; diff --git a/backend/routes/polls.js b/backend/routes/polls.js deleted file mode 100644 index 34d7722..0000000 --- a/backend/routes/polls.js +++ /dev/null @@ -1,72 +0,0 @@ -const express = require('express'); -const { query, getRow } = require('../db'); -const { authenticate, requireRole } = require('../middleware/auth'); - -const router = express.Router(); - -router.get('/active', authenticate, async (req, res) => { - const polls = await query('SELECT id, title, description, active, created_at FROM polls WHERE active = 1 ORDER BY created_at DESC'); - for (const p of polls) { - p.options = JSON.parse(p.options || '[]'); - const rows = await query(`SELECT pv.option_index, - SUM(CASE WHEN u.role IN ('owner','admin') THEN 1.5 ELSE 1 END) as total - FROM poll_votes pv JOIN users u ON pv.user_id = u.id - WHERE pv.poll_id = ? GROUP BY pv.option_index`, [p.id]); - p.votes = {}; - let total = 0; - for (const r of rows) { p.votes[r.option_index] = Math.round(r.total * 10) / 10; total += r.total; } - p.total = Math.round(total * 10) / 10; - const myVote = await getRow('SELECT option_index FROM poll_votes WHERE poll_id = ? AND user_id = ?', [p.id, req.user.id]); - p.my_vote = myVote ? myVote.option_index : null; - p.voted = !!myVote; - } - res.json(polls); -}); - -router.get('/:id', authenticate, requireRole('owner'), async (req, res) => { - const p = await getRow('SELECT * FROM polls WHERE id = ?', [req.params.id]); - if (!p) return res.status(404).json({ error: '不存在' }); - res.json(p); -}); - -router.post('/', authenticate, requireRole('owner'), async (req, res) => { - const { title, description, options } = req.body; - if (!title || !options || !Array.isArray(options) || options.length < 2) return res.status(400).json({ error: '标题和至少2个选项为必填' }); - const r = await query('INSERT INTO polls(title, description, options, created_by) VALUES (?,?,?,?)', [title, description||'', JSON.stringify(options), req.user.id]); - res.status(201).json({ id: r.insertId, message: '创建成功' }); -}); - -router.put('/:id', authenticate, requireRole('owner'), async (req, res) => { - const p = await getRow('SELECT * FROM polls WHERE id = ?', [req.params.id]); - if (!p) return res.status(404).json({ error: '投票不存在' }); - const fields = {}; - if (req.body.title) fields.title = req.body.title; - if (req.body.description !== undefined) fields.description = req.body.description; - if (req.body.active !== undefined) fields.active = req.body.active ? 1 : 0; - if (req.body.options) fields.options = JSON.stringify(req.body.options); - if (!Object.keys(fields).length) return res.status(400).json({ error: '无更新内容' }); - const sets = Object.keys(fields).map(k => `${k} = ?`).join(', '); - await query(`UPDATE polls SET ${sets} WHERE id = ?`, [...Object.values(fields), req.params.id]); - res.json({ message: '更新成功' }); -}); - -router.delete('/:id', authenticate, requireRole('owner'), async (req, res) => { - await query('DELETE FROM poll_votes WHERE poll_id = ?', [req.params.id]); - await query('DELETE FROM polls WHERE id = ?', [req.params.id]); - res.json({ message: '已删除' }); -}); - -router.post('/:id/vote', authenticate, async (req, res) => { - const p = await getRow('SELECT * FROM polls WHERE id = ? AND active = 1', [req.params.id]); - if (!p) return res.status(404).json({ error: '投票不存在或已关闭' }); - const voted = await getRow('SELECT id FROM poll_votes WHERE poll_id = ? AND user_id = ?', [p.id, req.user.id]); - if (voted) return res.status(400).json({ error: '您已投过票' }); - const { option_index } = req.body; - if (option_index === undefined || option_index === null) return res.status(400).json({ error: '请选择选项' }); - const opts = JSON.parse(p.options || '[]'); - if (option_index < 0 || option_index >= opts.length) return res.status(400).json({ error: '无效选项' }); - await query('INSERT INTO poll_votes(poll_id, user_id, option_index) VALUES (?,?,?)', [req.params.id, req.user.id, option_index]); - res.json({ message: '投票成功' }); -}); - -module.exports = router; diff --git a/backend/server.js b/backend/server.js index ddb6b41..710e2a6 100644 --- a/backend/server.js +++ b/backend/server.js @@ -104,8 +104,6 @@ if (isInstalled()) { app.use('/api/export', methodGuard(['GET']), generalLimiter, require('./routes/export')); app.use('/api/uploads', methodGuard(['GET']), generalLimiter, require('./routes/uploads')); app.use('/api/notifications', methodGuard(['GET','POST','PUT','DELETE']), generalLimiter, require('./routes/notifications')); - app.use('/api/polls', methodGuard(['GET','POST','PUT','DELETE']), generalLimiter, require('./routes/polls')); - app.use('/api/features', methodGuard(['GET','POST','PUT','DELETE']), generalLimiter, require('./routes/features')); app.use('/api/external', require('./routes/external')); app.get('/api/verify', (req, res) => res.redirect(`/#/verify?token=${req.query.token}`)); diff --git a/public/index.html b/public/index.html index c3401fd..0bda9a2 100644 --- a/public/index.html +++ b/public/index.html @@ -69,8 +69,6 @@ - - diff --git a/public/js/app.js b/public/js/app.js index 72aba60..9ea2788 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -9,8 +9,6 @@ const App = { { id: 'templates', label: '邮件模板', icon: 'fa-envelope', roles: ['owner','admin'] }, { id: 'settings', label: '系统设置', icon: 'fa-cog', roles: ['owner','admin'] }, { id: 'export', label: '数据导出', icon: 'fa-download', roles: ['owner'] }, - { id: 'polls', label: '投票', icon: 'fa-poll', roles: ['owner','admin','player'] }, - { id: 'features', label: '更新内容', icon: 'fa-lightbulb', roles: ['owner','admin','player'] }, ], async init() { @@ -78,8 +76,6 @@ const App = { case 'templates': this.renderMain('邮件模板', TemplatesPage, param); break; case 'settings': this.renderMain('系统设置', SettingsPage, param); break; case 'export': this.renderMain('数据导出', ExportPage, param); break; - case 'polls': this.renderMain('投票', PollsPage, param); break; - case 'features': this.renderMain('更新内容', FeaturesPage, param); break; default: this.renderMain('控制台', Dashboard, param); } }, @@ -102,7 +98,6 @@ const App = { const u = Auth.user(); el.innerHTML = `${U.esc(u.game_name||u.username)} 控制台 - ${Auth.isAdmin() ? `待处理`:''} `; } else { @@ -149,8 +144,6 @@ window.TemplatesPage = TemplatesPage; window.NotificationsPage = NotificationsPage; window.SettingsPage = SettingsPage; window.ExportPage = ExportPage; -window.PollsPage = PollsPage; -window.FeaturesPage = FeaturesPage; window.LoginPage = LoginPage; window.RegisterPage = RegisterPage; window.HomePage = HomePage; diff --git a/public/js/pages/features.js b/public/js/pages/features.js deleted file mode 100644 index 06310e2..0000000 --- a/public/js/pages/features.js +++ /dev/null @@ -1,89 +0,0 @@ -const FeaturesPage = { - async render() { return '
'; }, - - async mount() { - document.getElementById('page-title').textContent = '更新内容'; - document.getElementById('page-actions').innerHTML = Auth.isOwner() - ? '' - : ''; - await this.load(); - }, - - async load() { - const ct = document.getElementById('page-content'); - try { - const items = await API.get('/features'); - ct.innerHTML = items.length === 0 - ? '

暂无更新项目

' - : items.map(i => this.renderItem(i)).join(''); - for (const i of items) if (Auth.logged()) this.bindVote(i); - } catch (e) { ct.innerHTML = `
${e.message}
`; } - }, - - renderItem(i) { - const statusColor = i.status === 'done' ? 'var(--s)' : i.status === 'planned' ? 'var(--p)' : 'var(--g5)'; - const statusLabel = i.status === 'done' ? '已完成' : i.status === 'planned' ? '计划中' : '待定'; - return `
-
- ${U.esc(i.title)} -
- ● ${statusLabel} - ${Auth.isOwner() ? `` : ''} -
-
-
- ${i.description ? `

${U.esc(i.description)}

` : ''} -
- - ${i.my_vote ? '已投票 (点击取消)' : '点击投票'} -
-
-
`; - }, - - bindVote(i) { - const card = document.getElementById('feat-'+i.id); - if (!card) return; - card.querySelector('.feat-vote').addEventListener('click', async () => { - try { await API.post(`/features/${i.id}/vote`); this.load(); } catch (ex) { alert(ex.message); } - }); - }, - - showCreate() { - U.modal('添加项目', ` -
-
-
- -
- `); - document.getElementById('feat-form').onsubmit = async e => { e.preventDefault(); - try { await API.post('/features', { title: document.getElementById('ft-title').value, description: document.getElementById('ft-desc').value }); App.closeModal(); this.load(); } catch(ex){alert(ex.message);} - }; - }, - - async showEdit(id, title, desc, status) { - U.modal('编辑', ` -
-
-
-
- -
- `); - document.getElementById('fe-edit').onsubmit = async e => { e.preventDefault(); - try { await API.put('/features/'+id, { title: document.getElementById('fe-title').value, description: document.getElementById('fe-desc').value, status: document.getElementById('fe-status').value }); App.closeModal(); this.load(); } catch(ex){alert(ex.message);} - }; - }, - - async del(id) { - if (!await U.confirm('确认删除?')) return; - try { await API.req('DELETE', '/features/'+id); App.closeModal(); this.load(); } catch(ex){alert(ex.message);} - } -}; diff --git a/public/js/pages/polls.js b/public/js/pages/polls.js deleted file mode 100644 index 6c3f51f..0000000 --- a/public/js/pages/polls.js +++ /dev/null @@ -1,108 +0,0 @@ -const PollsPage = { - async render() { return '
'; }, - - async mount() { - document.getElementById('page-title').textContent = '投票'; - document.getElementById('page-actions').innerHTML = Auth.isOwner() - ? '' - : ''; - await this.load(); - }, - - async load() { - const ct = document.getElementById('page-content'); - try { - const polls = await API.get('/polls/active'); - ct.innerHTML = polls.length === 0 - ? '

暂无进行中的投票

' - : polls.map(p => this.renderPoll(p)).join(''); - for (const p of polls) this.bindVote(p); - } catch (e) { ct.innerHTML = `
${e.message}
`; } - }, - - renderPoll(p) { - const opts = p.options || []; - const max = Math.max(1, ...Object.values(p.votes || {})); - const showResult = !p.voted; - return `
-
${U.esc(p.title)}${p.total||0} 票 - ${Auth.isOwner() ? `` : ''} -
-
- ${p.description?`

${U.esc(p.description)}

`:''} - ${p.voted && !showResult ? `
已投票,结果仅在投票结束后向服主公开
` : ''} -
- ${opts.map((o,i) => { - const v = showResult ? (p.votes?.[i] || 0) : 0; - const pct = showResult && p.total ? Math.round(v/p.total*100) : 0; - const my = p.my_vote === i; - return `
- ${showResult ? `
` : ''} -
- ${my?' ':''}${U.esc(o)} - ${showResult ? `${v}票 ${pct}%` : ''} -
-
`; - }).join('')} -
-
-
`; - }, - - bindVote(p) { - if (p.voted) return; - const card = document.getElementById('poll-'+p.id); - if (!card) return; - card.querySelectorAll('.poll-opt').forEach(el => { - el.addEventListener('click', async () => { - try { - await API.post(`/polls/${p.id}/vote`, { option_index: parseInt(el.dataset.idx) }); - this.load(); - } catch (ex) { alert(ex.message); } - }); - }); - }, - - showCreate() { - U.modal('创建投票', ` -
-
-
-
- -
- `); - document.getElementById('poll-form').onsubmit = async e => { - e.preventDefault(); - const opts = document.getElementById('pl-opts').value.split('\n').map(s=>s.trim()).filter(s=>s); - if (opts.length < 2) return alert('至少需要2个选项'); - try { await API.post('/polls', { title: document.getElementById('pl-title').value, description: document.getElementById('pl-desc').value, options: opts }); App.closeModal(); this.load(); } catch (ex) { alert(ex.message); } - }; - }, - - async showEdit(id) { - U.modal('编辑投票', '
'); - const p = await API.get('/polls/' + id); - document.getElementById('modal-body').innerHTML = ` -
-
-
-
- -
- `; - document.getElementById('pe-form').onsubmit = async e => { - e.preventDefault(); - const opts = document.getElementById('pe-opts').value.split('\n').map(s=>s.trim()).filter(s=>s); - try { - await API.put('/polls/'+id, { title: document.getElementById('pe-title').value, active: document.getElementById('pe-active').value==='1', options: opts.length>=2?opts:undefined }); - App.closeModal(); this.load(); - } catch (ex) { alert(ex.message); } - }; - }, - - async deletePoll(id) { - if (!await U.confirm('确认删除此投票?')) return; - try { await API.req('DELETE', '/polls/'+id); App.closeModal(); this.load(); } catch(ex){alert(ex.message);} - } -};