From 49139843912fa7fd7da349625401565061a25796 Mon Sep 17 00:00:00 2001 From: canglan Date: Tue, 14 Jul 2026 03:17:13 +0800 Subject: [PATCH] feat: poll groups/servers, timed polls, features sort+filter by group --- backend/db.js | 16 +++- backend/routes/features.js | 59 +++++++++++++++ backend/routes/polls.js | 107 +++++++++++++++++++++++++++ backend/server.js | 2 + public/index.html | 2 + public/js/app.js | 7 ++ public/js/pages/features.js | 140 +++++++++++++++++++++++++++++++++++ public/js/pages/polls.js | 142 ++++++++++++++++++++++++++++++++++++ 8 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 backend/routes/features.js create mode 100644 backend/routes/polls.js create mode 100644 public/js/pages/features.js create mode 100644 public/js/pages/polls.js diff --git a/backend/db.js b/backend/db.js index 2bcc470..7c0498c 100644 --- a/backend/db.js +++ b/backend/db.js @@ -184,10 +184,15 @@ async function initSchema(connection) { title VARCHAR(200) NOT NULL, description TEXT, options JSON NOT NULL, + group_name VARCHAR(100) NOT NULL DEFAULT '', + server_name VARCHAR(100) NOT NULL DEFAULT '', + start_time DATETIME, + end_time DATETIME, active TINYINT(1) NOT NULL DEFAULT 1, created_by INT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - INDEX idx_active (active) + INDEX idx_active (active), + INDEX idx_group_server (group_name, server_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); await createIfNotExists('poll_votes', `CREATE TABLE poll_votes ( @@ -204,6 +209,8 @@ async function initSchema(connection) { id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, description TEXT, + group_name VARCHAR(100) NOT NULL DEFAULT '', + server_name VARCHAR(100) NOT NULL DEFAULT '', status ENUM('pending','planned','done') NOT NULL DEFAULT 'pending', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); @@ -217,6 +224,13 @@ async function initSchema(connection) { 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, + server_name VARCHAR(100) NOT NULL, + UNIQUE KEY uk_group_server (group_name, server_name) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); + await createIfNotExists('audit_logs', `CREATE TABLE audit_logs ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT, diff --git a/backend/routes/features.js b/backend/routes/features.js new file mode 100644 index 0000000..192d87d --- /dev/null +++ b/backend/routes/features.js @@ -0,0 +1,59 @@ +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 { group_name, server_name, sort } = 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 + 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||'']); + res.json(items); +}); + +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: '标题为必填' }); + await query('INSERT INTO feature_items(title, description, group_name, server_name) VALUES (?,?,?,?)', [title, description||'', group_name||'', server_name||'']); + res.status(201).json({ id: 0 }); +}); + +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 (req.body.group_name) fields.group_name = req.body.group_name; + if (req.body.server_name) fields.server_name = req.body.server_name; + 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 new file mode 100644 index 0000000..5a8ce4d --- /dev/null +++ b/backend/routes/polls.js @@ -0,0 +1,107 @@ +const express = require('express'); +const { query, getRow } = require('../db'); +const { authenticate, requireRole } = require('../middleware/auth'); + +const router = express.Router(); + +router.get('/groups', authenticate, async (req, res) => { + res.json(await query('SELECT * FROM server_groups ORDER BY group_name, server_name')); +}); + +router.post('/groups', authenticate, requireRole('owner'), async (req, res) => { + const { group_name, server_name } = req.body; + if (!group_name || !server_name) return res.status(400).json({ error: '分组名和子服名为必填' }); + await query('INSERT IGNORE INTO server_groups(group_name, server_name) VALUES (?,?)', [group_name, server_name]); + res.status(201).json({ message: '已添加' }); +}); + +router.delete('/groups/:id', authenticate, requireRole('owner'), async (req, res) => { + await query('DELETE FROM server_groups WHERE id = ?', [req.params.id]); + res.json({ message: '已删除' }); +}); + +router.get('/active', authenticate, async (req, res) => { + const groups = await query('SELECT DISTINCT group_name, server_name FROM server_groups ORDER BY group_name, server_name'); + const result = {}; + for (const g of groups) { + const polls = await query(`SELECT id, title, description, group_name, server_name, start_time, end_time, active, created_at FROM polls + WHERE active = 1 AND group_name = ? AND server_name = ? AND (end_time IS NULL OR end_time > NOW()) + ORDER BY created_at DESC LIMIT 1`, [g.group_name, g.server_name]); + if (polls.length === 0) continue; + const p = polls[0]; + 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; + if (!result[g.group_name]) result[g.group_name] = []; + result[g.group_name].push({ ...p, server_name: g.server_name }); + } + res.json(result); +}); + +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, group_name, server_name, start_time, end_time } = req.body; + if (!title || !options || !Array.isArray(options) || options.length < 2) return res.status(400).json({ error: '标题和至少2个选项为必填' }); + if (!group_name || !server_name) return res.status(400).json({ error: '请选择分组和子服' }); + + const existing = await getRow(`SELECT id FROM polls WHERE active = 1 AND group_name = ? AND server_name = ? AND (end_time IS NULL OR end_time > NOW())`, [group_name, server_name]); + if (existing) return res.status(400).json({ error: '该子服已有进行中的投票' }); + + await query(`INSERT INTO polls(title, description, options, group_name, server_name, start_time, end_time, created_by) VALUES (?,?,?,?,?,?,?,?)`, + [title, description||'', JSON.stringify(options), group_name, server_name, start_time||null, end_time||null, req.user.id]); + res.status(201).json({ 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 (req.body.start_time !== undefined) fields.start_time = req.body.start_time || null; + if (req.body.end_time !== undefined) fields.end_time = req.body.end_time || null; + if (req.body.group_name) fields.group_name = req.body.group_name; + if (req.body.server_name) fields.server_name = req.body.server_name; + 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: '投票不存在或已关闭' }); + if (p.end_time && new Date(p.end_time) < new Date()) return res.status(400).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 710e2a6..ddb6b41 100644 --- a/backend/server.js +++ b/backend/server.js @@ -104,6 +104,8 @@ 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 0bda9a2..c3401fd 100644 --- a/public/index.html +++ b/public/index.html @@ -69,6 +69,8 @@ + + diff --git a/public/js/app.js b/public/js/app.js index 9ea2788..72aba60 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -9,6 +9,8 @@ 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() { @@ -76,6 +78,8 @@ 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); } }, @@ -98,6 +102,7 @@ const App = { const u = Auth.user(); el.innerHTML = `${U.esc(u.game_name||u.username)} 控制台 + ${Auth.isAdmin() ? `待处理`:''} `; } else { @@ -144,6 +149,8 @@ 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 new file mode 100644 index 0000000..8760652 --- /dev/null +++ b/public/js/pages/features.js @@ -0,0 +1,140 @@ +const FeaturesPage = { + sort: 'time', + filterGroup: '', + filterServer: '', + + async render() { return '
'; }, + + async mount() { + document.getElementById('page-title').textContent = '更新内容'; + document.getElementById('page-actions').innerHTML = Auth.isOwner() + ? '' + : ''; + this.renderLayout(); + await this.load(); + }, + + renderLayout() { + const ct = document.getElementById('page-content'); + ct.innerHTML = `
+ + + + + +
+
`; + this.loadGroupOptions(); + }, + + async loadGroupOptions() { + try { + const groups = await API.get('/polls/groups'); + const groupSel = document.getElementById('feat-group'); + const serverSel = document.getElementById('feat-server'); + const uniqueGroups = [...new Set(groups.map(g => g.group_name))]; + groupSel.innerHTML = '' + uniqueGroups.map(g => ``).join(''); + const servers = this.filterGroup ? groups.filter(g => g.group_name === this.filterGroup) : groups; + const uniqueServers = [...new Set(servers.map(g => g.server_name))]; + serverSel.innerHTML = '' + uniqueServers.map(s => ``).join(''); + } catch {} + }, + + setFilter() { + this.filterGroup = document.getElementById('feat-group').value; + this.filterServer = document.getElementById('feat-server').value; + this.loadGroupOptions(); + this.load(); + }, + + setSort(s) { + this.sort = s; + document.getElementById('sort-time').className = 'btn btn-o btn-sm ' + (s==='time'?'btn-p':''); + document.getElementById('sort-votes').className = 'btn btn-o btn-sm ' + (s==='votes'?'btn-p':''); + this.load(); + }, + + async load() { + const ct = document.getElementById('feat-list'); + U.loading(ct); + try { + const params = new URLSearchParams({ sort: this.sort, group_name: this.filterGroup, server_name: this.filterServer }); + const items = await API.get('/features?' + params); + 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 sc = i.status==='done'?'var(--s)':i.status==='planned'?'var(--p)':'var(--g5)'; + const sl = i.status==='done'?'已完成':i.status==='planned'?'计划中':'待定'; + return `
+
+ ${U.esc(i.title)} ${i.group_name ? `— ${U.esc(i.group_name)}/${U.esc(i.server_name)}` : ''} +
+ ● ${sl} + ${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); } + }); + }, + + async loadGroupsForForm() { try { return await API.get('/polls/groups'); } catch { return []; } }, + + async showCreate() { + const groups = await this.loadGroupsForForm(); + const opts = groups.map(g => ``).join(''); + U.modal('添加项目', ` +
+
+
+
+ +
+ `); + document.getElementById('feat-form').onsubmit = async e => { e.preventDefault(); + const gs = document.getElementById('ft-gs').value; + const [group_name, server_name] = gs === '::' ? ['',''] : gs.split('::'); + try { await API.post('/features', { title: document.getElementById('ft-title').value, description: document.getElementById('ft-desc').value, group_name, server_name }); App.closeModal(); this.load(); } catch(ex){alert(ex.message);} + }; + }, + + async showEdit(id, title, desc, status, gn, sn) { + const groups = await this.loadGroupsForForm(); + const opts = groups.map(g => ``).join(''); + U.modal('编辑', ` +
+
+
+
+
+ +
+ `); + document.getElementById('fe-edit').onsubmit = async e => { e.preventDefault(); + const gs = document.getElementById('fe-gs').value; + const [group_name, server_name] = gs === '::' ? ['',''] : gs.split('::'); + try { await API.put('/features/'+id, { title: document.getElementById('fe-title').value, description: document.getElementById('fe-desc').value, status: document.getElementById('fe-status').value, group_name, server_name }); 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 new file mode 100644 index 0000000..b7422e7 --- /dev/null +++ b/public/js/pages/polls.js @@ -0,0 +1,142 @@ +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 groups = await API.get('/polls/active'); + const entries = Object.entries(groups); + if (entries.length === 0) { + ct.innerHTML = '

暂无进行中的投票

'; + return; + } + ct.innerHTML = entries.map(([group, polls]) => ` +
+

${U.esc(group)}

+ ${polls.map(p => this.renderPoll(p)).join('')} +
+ `).join(''); + for (const [group, polls] of entries) for (const p of polls) this.bindVote(p); + } catch (e) { ct.innerHTML = `
${e.message}
`; } + }, + + renderPoll(p) { + const opts = p.options || []; + const timed = p.start_time || p.end_time; + return `
+
${U.esc(p.server_name)} — ${U.esc(p.title)} + + ${timed ? `${p.start_time?'起: '+U.date(p.start_time):''} ${p.end_time?'至: '+U.date(p.end_time):''}`:''} + ${p.total||0} 票 + ${Auth.isOwner() ? `` : ''} + +
+
+ ${p.description?`

${U.esc(p.description)}

`:''} + ${p.voted ? `
已投票
` : ''} +
+ ${opts.map((o,i) => { + const v = p.voted ? 0 : (p.votes?.[i] || 0); + const pct = p.voted ? 0 : (p.total ? Math.round(v/p.total*100) : 0); + const my = p.my_vote === i; + return `
+ ${!p.voted ? `
` : ''} +
+ ${my?' ':''}${U.esc(o)} + ${!p.voted ? `${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() { + this.loadGroups().then(groups => { + const opts = groups.map(g => ``).join(''); + U.modal('创建投票', ` +
+
+
+
+
+
+
+ +
+ `); + document.getElementById('poll-form').onsubmit = async e => { + e.preventDefault(); + const [group_name, server_name] = document.getElementById('pl-gs').value.split('::'); + 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, group_name, server_name, start_time: document.getElementById('pl-start').value||null, end_time: document.getElementById('pl-end').value||null }); + App.closeModal(); this.load(); + } catch (ex) { alert(ex.message); } + }; + }); + }, + + async loadGroups() { try { return await API.get('/polls/groups'); } catch { return []; } }, + + showGroupMgr() { + this.loadGroups().then(groups => { + const list = groups.map(g => `
+ ${U.esc(g.group_name)} / ${U.esc(g.server_name)} + +
`).join(''); + U.modal('管理分组', ` +
+
+
${list||'

暂无

'}
+ `); + document.getElementById('gm-form').onsubmit = async e => { e.preventDefault(); + try { await API.post('/polls/groups', { group_name: document.getElementById('gm-group').value, server_name: document.getElementById('gm-server').value }); App.closeModal(); this.showGroupMgr(); } catch(ex){alert(ex.message);} + }; + }); + }, + + async delGroup(id) { try { await API.req('DELETE', '/polls/groups/'+id); App.closeModal(); this.showGroupMgr(); } catch(ex){alert(ex.message);} }, + + async showEdit(id) { + const p = await API.get('/polls/'+id); + document.getElementById('modal-title').textContent = '编辑投票'; + document.getElementById('modal-body').innerHTML = ` +
+
+
+
+ +
+ `; + document.getElementById('modal-overlay').classList.remove('hidden'); + 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);} } +};