40) { return ['软件名称最多 40 个字符。', 'error']; } if ($slug === '') { $slug = fb_sw_slugify($name); } if ($slug === '') { return ['请填写英文标识(用于网址)。', 'error']; } if (fb_software($slug)) { return ['英文标识「' . $slug . '」已被占用,换一个。', 'error']; } $sort = (int)$pdo->query('SELECT COALESCE(MAX(sort_order),0)+1 FROM software')->fetchColumn(); $pdo->prepare('INSERT INTO software(slug,name,note,sort_order,enabled,created_at) VALUES(?,?,?,?,1,?)') ->execute([$slug, $name, $note !== '' ? $note : null, $sort, $now]); $newId = (int)$pdo->lastInsertId(); // 新软件默认给一组常见分类,省得空着没法发帖 $i = 0; foreach ([['bug', 'Bug'], ['feature', '功能建议'], ['other', '其他']] as $c) { $pdo->prepare('INSERT INTO categories(software_id,slug,name,sort_order,enabled,created_at) VALUES(?,?,?,?,1,?)') ->execute([$newId, $c[0], $c[1], $i++, $now]); } return ['已添加软件「' . $name . '」,并预置 3 个分类。', 'ok']; } case 'sw_edit': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $sw = fb_software($id); if (!$sw) { return ['软件不存在。', 'error']; } $name = trim((string)(isset($post['name']) ? $post['name'] : '')); $slug = fb_sw_slugify(isset($post['slug']) ? $post['slug'] : ''); $note = trim((string)(isset($post['note']) ? $post['note'] : '')); if ($name === '') { return ['软件名称不能为空。', 'error']; } if ($slug === '') { $slug = fb_sw_slugify($name); } if ($slug === '') { return ['请填写英文标识。', 'error']; } $other = fb_software($slug); if ($other && (int)$other['id'] !== $id) { return ['英文标识「' . $slug . '」已被其他软件占用。', 'error']; } $pdo->prepare('UPDATE software SET name=?, slug=?, note=?, sort_order=? WHERE id=?') ->execute([$name, $slug, $note !== '' ? $note : null, (int)(isset($post['sort_order']) ? $post['sort_order'] : $sw['sort_order']), $id]); return ['软件「' . $name . '」已保存。', 'ok']; } case 'sw_del': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $sw = fb_software($id); if (!$sw) { return ['软件不存在。', 'error']; } $cnt = (int)$pdo->query('SELECT COUNT(*) FROM software')->fetchColumn(); if ($cnt <= 1) { return ['这是最后一个软件,不能删除(至少要保留一个)。', 'error']; } $n = (int)$pdo->query('SELECT COUNT(*) FROM threads WHERE software_id = ' . $id)->fetchColumn(); if ($n > 0) { return ['该软件下还有 ' . $n . ' 条反馈,不能删除。' . '请先把这些反馈移到其他软件,或先停用该软件(停用后前台不再显示)。', 'error']; } $pdo->prepare('DELETE FROM categories WHERE software_id = ?')->execute([$id]); $pdo->prepare('DELETE FROM software WHERE id = ?')->execute([$id]); return ['已删除软件「' . $sw['name'] . '」。', 'ok']; } case 'sw_toggle': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $sw = fb_software($id); if (!$sw) { return ['软件不存在。', 'error']; } $on = (int)$sw['enabled'] === 1 ? 0 : 1; if ($on === 0) { $en = (int)$pdo->query('SELECT COUNT(*) FROM software WHERE enabled = 1')->fetchColumn(); if ($en <= 1) { return ['至少要保留一个启用的软件。', 'error']; } } $pdo->prepare('UPDATE software SET enabled = ? WHERE id = ?')->execute([$on, $id]); return ['软件「' . $sw['name'] . '」已' . ($on ? '启用' : '停用') . '。', 'ok']; } /* ---------------- 分类 ---------------- */ case 'cat_add': { $swId = (int)(isset($post['software_id']) ? $post['software_id'] : 0); if (!fb_software($swId)) { return ['请先选择软件。', 'error']; } $name = trim((string)(isset($post['name']) ? $post['name'] : '')); $slug = fb_sw_slugify(isset($post['slug']) ? $post['slug'] : ''); if ($name === '') { return ['分类名称不能为空。', 'error']; } if (fb_len($name) > 30) { return ['分类名称最多 30 个字符。', 'error']; } if ($slug === '') { $slug = fb_sw_slugify($name); } if ($slug === '') { return ['请填写英文标识。', 'error']; } if (fb_category_by_slug($swId, $slug)) { return ['该软件下已有标识为「' . $slug . '」的分类。', 'error']; } $sort = (int)$pdo->query('SELECT COALESCE(MAX(sort_order),0)+1 FROM categories WHERE software_id = ' . $swId)->fetchColumn(); $pdo->prepare('INSERT INTO categories(software_id,slug,name,sort_order,enabled,created_at) VALUES(?,?,?,?,1,?)') ->execute([$swId, $slug, $name, $sort, $now]); return ['已添加分类「' . $name . '」。', 'ok']; } case 'cat_edit': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $cat = fb_category($id); if (!$cat) { return ['分类不存在。', 'error']; } $name = trim((string)(isset($post['name']) ? $post['name'] : '')); $slug = fb_sw_slugify(isset($post['slug']) ? $post['slug'] : ''); if ($name === '') { return ['分类名称不能为空。', 'error']; } if ($slug === '') { $slug = fb_sw_slugify($name); } if ($slug === '') { return ['请填写英文标识。', 'error']; } $other = fb_category_by_slug((int)$cat['software_id'], $slug); if ($other && (int)$other['id'] !== $id) { return ['该软件下已有标识为「' . $slug . '」的分类。', 'error']; } $pdo->prepare('UPDATE categories SET name=?, slug=?, sort_order=? WHERE id=?') ->execute([$name, $slug, (int)(isset($post['sort_order']) ? $post['sort_order'] : $cat['sort_order']), $id]); return ['分类「' . $name . '」已保存。', 'ok']; } case 'cat_del': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $cat = fb_category($id); if (!$cat) { return ['分类不存在。', 'error']; } $n = (int)$pdo->query('SELECT COUNT(*) FROM threads WHERE category_id = ' . $id)->fetchColumn(); if ($n > 0) { return ['该分类下还有 ' . $n . ' 条反馈,不能删除。可以改为停用(停用后前台不再出现,但历史反馈保留)。', 'error']; } $pdo->prepare('DELETE FROM categories WHERE id = ?')->execute([$id]); return ['已删除分类「' . $cat['name'] . '」。', 'ok']; } case 'cat_toggle': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $cat = fb_category($id); if (!$cat) { return ['分类不存在。', 'error']; } $on = (int)$cat['enabled'] === 1 ? 0 : 1; if ($on === 0) { $en = (int)$pdo->query('SELECT COUNT(*) FROM categories WHERE software_id = ' . (int)$cat['software_id'] . ' AND enabled = 1')->fetchColumn(); if ($en <= 1) { return ['每个软件至少要保留一个启用的分类。', 'error']; } } $pdo->prepare('UPDATE categories SET enabled = ? WHERE id = ?')->execute([$on, $id]); return ['分类「' . $cat['name'] . '」已' . ($on ? '启用' : '停用') . '。', 'ok']; } /* ---------------- 状态 ---------------- */ case 'st_add': { $key = fb_sw_slugify(isset($post['key_name']) ? $post['key_name'] : ''); $name = trim((string)(isset($post['name']) ? $post['name'] : '')); $note = trim((string)(isset($post['user_note']) ? $post['user_note'] : '')); if ($key === '') { return ['状态标识不能为空(只能用字母、数字、连字符)。', 'error']; } if ($name === '') { return ['状态名称不能为空。', 'error']; } $st = $pdo->prepare('SELECT id FROM statuses WHERE key_name = ? LIMIT 1'); $st->execute([$key]); if ($st->fetchColumn() !== false) { return ['状态标识「' . $key . '」已存在。', 'error']; } $sort = (int)$pdo->query('SELECT COALESCE(MAX(sort_order),0)+1 FROM statuses')->fetchColumn(); $pdo->prepare('INSERT INTO statuses(key_name,name,user_note,sort_order,enabled) VALUES(?,?,?,?,1)') ->execute([$key, $name, $note !== '' ? $note : null, $sort]); return ['已添加状态「' . $name . '」。', 'ok']; } case 'st_edit': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $st = $pdo->prepare('SELECT * FROM statuses WHERE id = ? LIMIT 1'); $st->execute([$id]); $row = $st->fetch(); if (!$row) { return ['状态不存在。', 'error']; } $name = trim((string)(isset($post['name']) ? $post['name'] : '')); $note = trim((string)(isset($post['user_note']) ? $post['user_note'] : '')); if ($name === '') { return ['状态名称不能为空。', 'error']; } // key_name 不允许改:threads.status 里存的都是它,改了历史数据就失联 $pdo->prepare('UPDATE statuses SET name=?, user_note=?, sort_order=? WHERE id=?') ->execute([$name, $note !== '' ? $note : null, (int)(isset($post['sort_order']) ? $post['sort_order'] : $row['sort_order']), $id]); return ['状态「' . $name . '」已保存。', 'ok']; } case 'st_toggle': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $st = $pdo->prepare('SELECT * FROM statuses WHERE id = ? LIMIT 1'); $st->execute([$id]); $row = $st->fetch(); if (!$row) { return ['状态不存在。', 'error']; } $on = (int)$row['enabled'] === 1 ? 0 : 1; if ($on === 0) { $en = (int)$pdo->query('SELECT COUNT(*) FROM statuses WHERE enabled = 1')->fetchColumn(); if ($en <= 1) { return ['至少要保留一个启用的状态。', 'error']; } } $pdo->prepare('UPDATE statuses SET enabled = ? WHERE id = ?')->execute([$on, $id]); return ['状态「' . $row['name'] . '」已' . ($on ? '启用' : '停用') . '。', 'ok']; } /* ---------------- 把反馈改挂到别的软件/分类 ---------------- */ case 'th_move': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $swId = (int)(isset($post['software_id']) ? $post['software_id'] : 0); $catId = (int)(isset($post['category_id']) ? $post['category_id'] : 0); $sw = fb_software($swId); if (!$sw) { return ['请选择目标软件。', 'error']; } $cat = null; if ($catId) { $cat = fb_category($catId); if (!$cat || (int)$cat['software_id'] !== $swId) { return ['所选分类不属于目标软件,请重新选择。', 'error']; } } $pdo->prepare('UPDATE threads SET software_id = ?, category_id = ?, category = ?, updated_at = ? WHERE id = ?') ->execute([$swId, $cat ? (int)$cat['id'] : null, $cat ? (string)$cat['slug'] : 'other', time(), $id]); return ['已把 #' . $id . ' 改挂到「' . $sw['name'] . '」' . ($cat ? ' / ' . $cat['name'] : '') . '。', 'ok']; } } return ['未知操作。', 'error']; } /** 这个 act 是否属于本模块 */ function fb_admin_is_taxonomy_act($act) { return in_array($act, [ 'sw_add', 'sw_edit', 'sw_del', 'sw_toggle', 'cat_add', 'cat_edit', 'cat_del', 'cat_toggle', 'st_add', 'st_edit', 'st_toggle', 'th_move', ], true); } /* ================================================================== * 页面:软件与分类 * ================================================================== */ function fb_admin_taxonomy_page($pdo, $editSw, $editCat) { $swList = fb_softwares(false); // 含停用的,后台要看得到 // 当前选中的软件:URL 指定 > 第一个 $curSwId = isset($_GET['sw']) ? (int)$_GET['sw'] : 0; if ($curSwId <= 0 && $swList) { $curSwId = (int)$swList[0]['id']; } $curSw = $curSwId > 0 ? fb_software($curSwId) : null; $curCats = $curSw ? fb_categories((int)$curSw['id'], false) : []; echo '

软件

'; echo '

' . '前台按软件分栏显示。停用的软件前台不再出现,但它下面的反馈与分类都保留。' . '

'; if (!$swList) { echo '

还没有软件,先添加一个。

'; } else { echo '
'; foreach ($swList as $sw) { $sid = (int)$sw['id']; $on = (int)$sw['enabled'] === 1; $nTh = (int)$pdo->query('SELECT COUNT(*) FROM threads WHERE software_id = ' . $sid)->fetchColumn(); $nCat = (int)$pdo->query('SELECT COUNT(*) FROM categories WHERE software_id = ' . $sid)->fetchColumn(); $isCur = $sid === $curSwId; echo '
'; echo '
'; echo ' ' . e((string)$sw['name']) . ''; echo ' ' . e((string)$sw['slug']) . ''; if (!$on) { echo ' 已停用'; } echo ' ' . $nCat . ' 个分类 · ' . $nTh . ' 条反馈'; if (!empty($sw['note'])) { echo '
' . e((string)$sw['note']) . '
'; } echo '
'; echo '
'; echo ' 分类'; echo ' 编辑'; echo '
'; echo ' '; echo ' '; echo ' '; echo '
'; echo '
'; echo ' '; echo ' '; echo ' '; echo '
'; echo '
'; echo '
'; } echo '
'; } /* ---- 新增 / 编辑软件 ---- */ $esw = null; if ($editSw > 0) { $esw = fb_software($editSw); } echo '
'; echo '' . ($esw ? '编辑软件「' . e((string)$esw['name']) . '」' : '添加软件') . ''; echo '
'; echo ' '; if ($esw) { echo ' '; } echo '
'; echo ' '; echo ' '; echo '
'; echo ' '; if ($esw) { echo ' '; echo ' ' . '取消'; } else { echo ' '; } echo '
'; /* ---- 该软件下的分类 ---- */ if (!$curSw) { return; } echo '

「' . e((string)$curSw['name']) . '」的分类

'; echo '

' . '每个软件有自己的一套分类,名称可以和其他软件重复。' . '停用的分类前台不再出现,历史反馈照旧保留。' . '

'; if (!$curCats) { echo '

这个软件还没有分类,先加一个。

'; } else { echo '
'; foreach ($curCats as $c) { $cid = (int)$c['id']; $on = (int)$c['enabled'] === 1; $n = (int)$pdo->query('SELECT COUNT(*) FROM threads WHERE category_id = ' . $cid)->fetchColumn(); echo '
'; echo '
'; echo ' ' . e((string)$c['name']) . ''; echo ' ' . e((string)$c['slug']) . ''; if (!$on) { echo ' 已停用'; } echo ' ' . $n . ' 条反馈'; echo '
'; echo '
'; echo ' 编辑'; echo '
'; echo ' '; echo ' '; echo ' '; echo '
'; echo '
'; echo ' '; echo ' '; echo ' '; echo '
'; echo '
'; echo '
'; } echo '
'; } /* ---- 新增 / 编辑分类 ---- */ $ecat = $editCat > 0 ? fb_category($editCat) : null; echo '
'; echo '' . ($ecat ? '编辑分类「' . e((string)$ecat['name']) . '」' : '添加分类') . ''; echo '
'; echo ' '; echo ' '; if ($ecat) { echo ' '; } echo '
'; echo ' '; echo ' '; echo '
'; if ($ecat) { echo ' '; echo ' ' . '取消'; } else { echo ' '; } echo '
'; } /* ================================================================== * 页面:状态 * ================================================================== */ function fb_admin_status_page($pdo, $editId) { $rows = fb_statuses(false); echo '

处理状态

'; echo '

' . '「给用户看的话」会写进发给提交人的邮件正文。改这里的文字,前台与邮件都会跟着变。' . '

'; echo '
'; foreach ($rows as $r) { $rid = (int)$r['id']; $on = (int)$r['enabled'] === 1; $n = (int)$pdo->query('SELECT COUNT(*) FROM threads WHERE status = ' . $pdo->quote((string)$r['key_name']))->fetchColumn(); echo '
'; echo '
'; echo ' ' . e((string)$r['name']) . ''; echo ' ' . e((string)$r['key_name']) . ''; if (!$on) { echo ' 已停用'; } echo ' ' . $n . ' 条反馈'; $note = (string)($r['user_note'] === null ? '' : $r['user_note']); echo '
' . ($note !== '' ? e($note) : '(没有给用户看的说明)') . '
'; echo '
'; echo '
'; echo ' 编辑'; echo '
'; echo ' '; echo ' '; echo ' '; echo '
'; echo '
'; echo '
'; } echo '
'; /* ---- 新增 / 编辑状态 ---- */ $est = null; if ($editId > 0) { $q = $pdo->prepare('SELECT * FROM statuses WHERE id = ? LIMIT 1'); $q->execute([$editId]); $est = $q->fetch(); } echo '
'; echo '' . ($est ? '编辑状态「' . e((string)$est['name']) . '」' : '添加状态') . ''; echo '
'; echo ' '; if ($est) { echo ' '; } echo '
'; if ($est) { echo ' '; } else { echo ' '; } echo ' '; echo '
'; echo ' '; if ($est) { echo ' '; echo ' ' . '取消'; } else { echo ' '; } echo '
'; }