后台管理界面:软件/分类/状态可配,反馈可改挂归属 (#1)
## 新增 lib/admin_taxonomy.php - 软件:增删改、排序、启停;新增时自动预置 Bug/功能建议/其他 三个分类 - 分类:在选定软件下增删改、排序、启停 - 状态:改名、改「给用户看的话」、排序、启停;标识不可改(历史数据靠它对应) - 反馈改挂:th_move,可把反馈移到别的软件/分类,跨软件的非法分类会被拒 ## 安全与边界 - 所有动作走 CSRF 校验(沿用现有机制) - 不能删最后一个软件 / 没有分类的软件至少要留一个启用分类 - 有反馈挂着的软件或分类不能删,提示改为停用(停用后前台消失、数据保留) - th_move 加入 fb_admin_is_taxonomy_act 白名单,否则会被「问题不存在」静默吞掉 ## admin.php 接线 - 引入模块 + 动作分发 + tab 白名单加 taxonomy/statuses + 导航两项 - 前置检查放行不依赖 $t 的动作(否则新增软件报「问题不存在」) - 全部问题列表:加软件筛选、分类跟着软件走;修掉三处仍在用旧 fb_statuses()/fb_categories() 的遍历(那会渲染出错) - 详情页加「归属」表单,软件徽章可显示 ## 其他文件 - api.php:状态/分类改数据库;新增 ?sw= / ?software_id= / ?cid= 过滤, 老的 ?category=<slug> 仍可用(在所属软件内解析) - thread.php:详情页显示软件与分类徽章 - config.example.php:移除 categories / statuses,改由数据库管理 ## 修掉两个真实 bug 1. 移除配置后出厂分类兜底只剩 4 个,漏了「同步问题」—— 建库后核对数量才发现 2. th_move 不在白名单,POST 后返回 200 却静默不生效 验证:后台 e2e 29/29;改挂与联动 13/13;PHP 8.5 全部入口 0 弃用; 19 个 PHP 文件语法全过。
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
* 入口:/feedback/admin.php
|
||||
*/
|
||||
require __DIR__ . '/lib/bootstrap.php';
|
||||
require __DIR__ . '/lib/admin_taxonomy.php';
|
||||
|
||||
$pdo = fb_db();
|
||||
|
||||
@@ -93,7 +94,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) {
|
||||
$act = (string)$_POST['act'];
|
||||
$t = fb_admin_thread($pdo, $id);
|
||||
|
||||
if (!$t && $act !== 'settings' && $act !== 'password') {
|
||||
// 这些动作不针对某一条反馈(不依赖 $t),单独放行
|
||||
$noThread = in_array($act, ['settings', 'password'], true)
|
||||
|| fb_admin_is_taxonomy_act($act);
|
||||
|
||||
if (!$t && !$noThread) {
|
||||
$msg = '问题不存在。';
|
||||
$msgType = 'error';
|
||||
|
||||
@@ -182,6 +187,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) {
|
||||
fb_mod_log('delete', $id, '删除问题:' . fb_excerpt((string)$t['title'], 40));
|
||||
$msg = '#' . $id . ' 已删除。';
|
||||
|
||||
} elseif (fb_admin_is_taxonomy_act($act)) {
|
||||
list($msg, $msgType) = fb_admin_taxonomy_action($pdo, $act, $_POST);
|
||||
|
||||
} elseif ($act === 'settings') {
|
||||
$msg = fb_admin_save_settings();
|
||||
|
||||
@@ -300,7 +308,7 @@ function fb_admin_save_settings() {
|
||||
* 页面
|
||||
* ================================================================== */
|
||||
$tab = (string)(isset($_GET['tab']) ? $_GET['tab'] : 'pending');
|
||||
if (!in_array($tab, ['pending', 'all', 'thread', 'settings', 'log'], true)) { $tab = 'pending'; }
|
||||
if (!in_array($tab, ['pending', 'all', 'thread', 'taxonomy', 'statuses', 'settings', 'log'], true)) { $tab = 'pending'; }
|
||||
|
||||
fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
||||
?>
|
||||
@@ -315,6 +323,8 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
||||
<nav class="tabs">
|
||||
<a class="tab<?php echo $tab === 'pending' ? ' on' : ''; ?>" href="admin.php?tab=pending">待审核</a>
|
||||
<a class="tab<?php echo $tab === 'all' ? ' on' : ''; ?>" href="admin.php?tab=all">全部问题</a>
|
||||
<a class="tab<?php echo $tab === 'taxonomy' ? ' on' : ''; ?>" href="admin.php?tab=taxonomy">软件与分类</a>
|
||||
<a class="tab<?php echo $tab === 'statuses' ? ' on' : ''; ?>" href="admin.php?tab=statuses">处理状态</a>
|
||||
<a class="tab<?php echo $tab === 'settings' ? ' on' : ''; ?>" href="admin.php?tab=settings">站点设置</a>
|
||||
<a class="tab<?php echo $tab === 'log' ? ' on' : ''; ?>" href="admin.php?tab=log">操作日志</a>
|
||||
</nav>
|
||||
@@ -343,9 +353,12 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
||||
$visF = (string)(isset($_GET['vis']) ? $_GET['vis'] : '');
|
||||
$catF = (string)(isset($_GET['cat']) ? $_GET['cat'] : '');
|
||||
$qF = trim((string)(isset($_GET['q']) ? $_GET['q'] : ''));
|
||||
$swF = isset($_GET['sw']) ? (int)$_GET['sw'] : 0;
|
||||
$cidF = isset($_GET['cid']) ? (int)$_GET['cid'] : 0;
|
||||
if ($stF !== '') { $where[] = 'status = ?'; $args[] = $stF; }
|
||||
if ($visF === '1' || $visF === '0') { $where[] = 'visible = ?'; $args[] = (int)$visF; }
|
||||
if ($catF !== '') { $where[] = 'category = ?'; $args[] = $catF; }
|
||||
if ($swF > 0) { $where[] = 'software_id = ?'; $args[] = $swF; }
|
||||
if ($cidF > 0) { $where[] = 'category_id = ?'; $args[] = $cidF; }
|
||||
if ($qF !== '') { $where[] = '(title LIKE ? OR body LIKE ?)'; $args[] = '%' . $qF . '%'; $args[] = '%' . $qF . '%'; }
|
||||
$wSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
|
||||
|
||||
@@ -364,17 +377,29 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
||||
<form class="filters" method="get" action="admin.php">
|
||||
<input type="hidden" name="tab" value="all">
|
||||
<input type="text" name="q" value="<?php echo e($qF); ?>" placeholder="搜索标题或正文">
|
||||
<select name="sw" onchange="this.form.submit()">
|
||||
<option value="0">全部软件</option>
|
||||
<?php foreach (fb_softwares(false) as $sw1): ?>
|
||||
<option value="<?php echo (int)$sw1['id']; ?>"<?php echo $swF === (int)$sw1['id'] ? ' selected' : ''; ?>><?php echo e((string)$sw1['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="status">
|
||||
<option value="">全部状态</option>
|
||||
<?php foreach (fb_statuses() as $k => $label): ?>
|
||||
<?php foreach (fb_status_keys() as $k => $label): ?>
|
||||
<option value="<?php echo e($k); ?>"<?php echo $stF === $k ? ' selected' : ''; ?>><?php echo e($label); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="cat">
|
||||
<option value="">全部分类</option>
|
||||
<?php foreach (fb_categories() as $k => $label): ?>
|
||||
<option value="<?php echo e($k); ?>"<?php echo $catF === $k ? ' selected' : ''; ?>><?php echo e($label); ?></option>
|
||||
<?php endforeach; ?>
|
||||
<select name="cid">
|
||||
<option value="0">全部分类</option>
|
||||
<?php
|
||||
// 选了某个软件就只列它的分类;否则把所有软件的分类按软件分组列出
|
||||
$catSwList = $swF > 0 ? [fb_software($swF)] : fb_softwares(false);
|
||||
foreach ($catSwList as $csw1):
|
||||
if (!$csw1) { continue; }
|
||||
foreach (fb_categories((int)$csw1['id'], false) as $c1):
|
||||
?>
|
||||
<option value="<?php echo (int)$c1['id']; ?>"<?php echo $cidF === (int)$c1['id'] ? ' selected' : ''; ?>><?php echo e(($swF > 0 ? '' : (string)$csw1['name'] . ' · ') . (string)$c1['name']); ?></option>
|
||||
<?php endforeach; endforeach; ?>
|
||||
</select>
|
||||
<select name="vis">
|
||||
<option value="">公开与否不限</option>
|
||||
@@ -437,7 +462,12 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
||||
|
||||
<div class="card-form" style="margin:0 0 16px">
|
||||
<div class="item-top">
|
||||
<span class="badge cat"><?php echo e(fb_category_label($t['category'])); ?></span>
|
||||
<?php
|
||||
$dSw = !empty($t['software_id']) ? fb_software((int)$t['software_id']) : null;
|
||||
$dCat = !empty($t['category_id']) ? fb_category((int)$t['category_id']) : null;
|
||||
?>
|
||||
<?php if ($dSw): ?><span class="badge sw"><?php echo e((string)$dSw['name']); ?></span><?php endif; ?>
|
||||
<?php if ($dCat): ?><span class="badge cat"><?php echo e((string)$dCat['name']); ?></span><?php endif; ?>
|
||||
<span class="badge st st-<?php echo e($t['status']); ?>"><?php echo e(fb_status_label($t['status'])); ?></span>
|
||||
<span class="item-title"><?php echo e($t['title']); ?></span>
|
||||
<span class="item-id">#<?php echo (int)$t['id']; ?></span>
|
||||
@@ -469,6 +499,40 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<h2 style="margin:24px 0 10px">归属</h2>
|
||||
<form method="post" action="admin.php?tab=thread&id=<?php echo (int)$id; ?>" class="card-form">
|
||||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||||
<input type="hidden" name="id" value="<?php echo (int)$id; ?>">
|
||||
<p class="muted" style="margin:0 0 10px;font-size:13.5px">
|
||||
挂错软件或分类时在这里改正。换软件后分类选项会跟着变,需重新选择。
|
||||
</p>
|
||||
<div class="row2">
|
||||
<label class="field">
|
||||
<span>所属软件</span>
|
||||
<select name="software_id" onchange="location.href='admin.php?tab=thread&id=<?php echo (int)$id; ?>&sw='+this.value">
|
||||
<?php foreach (fb_softwares(false) as $m1): ?>
|
||||
<option value="<?php echo (int)$m1['id']; ?>"<?php echo (int)(isset($t['software_id']) ? $t['software_id'] : 0) === (int)$m1['id'] ? ' selected' : ''; ?>><?php echo e((string)$m1['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>分类</span>
|
||||
<select name="category_id">
|
||||
<option value="0">(不指定)</option>
|
||||
<?php
|
||||
$mvSw = isset($_GET['sw']) ? (int)$_GET['sw']
|
||||
: (int)(isset($t['software_id']) ? $t['software_id'] : 0);
|
||||
if ($mvSw <= 0) { $d0 = fb_default_software(); $mvSw = $d0 ? (int)$d0['id'] : 0; }
|
||||
foreach (fb_categories($mvSw, false) as $m2):
|
||||
?>
|
||||
<option value="<?php echo (int)$m2['id']; ?>"<?php echo (int)(isset($t['category_id']) ? $t['category_id'] : 0) === (int)$m2['id'] ? ' selected' : ''; ?>><?php echo e((string)$m2['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn" name="act" value="th_move" type="submit">保存归属</button>
|
||||
</form>
|
||||
|
||||
<h2 style="margin:24px 0 10px">以官方身份回复</h2>
|
||||
<form method="post" action="admin.php?tab=thread&id=<?php echo (int)$id; ?>" class="card-form">
|
||||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||||
@@ -481,7 +545,7 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
||||
<span>同时把状态改为</span>
|
||||
<select name="status">
|
||||
<option value="">(不改)</option>
|
||||
<?php foreach (fb_statuses() as $k => $label): ?>
|
||||
<?php foreach (fb_status_keys() as $k => $label): ?>
|
||||
<option value="<?php echo e($k); ?>"><?php echo e($label); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
@@ -499,7 +563,7 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
||||
<label class="field">
|
||||
<span>处理状态</span>
|
||||
<select name="status">
|
||||
<?php foreach (fb_statuses() as $k => $label): ?>
|
||||
<?php foreach (fb_status_keys() as $k => $label): ?>
|
||||
<option value="<?php echo e($k); ?>"<?php echo ((string)$t['status'] === $k) ? ' selected' : ''; ?>><?php echo e($label); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
@@ -542,6 +606,14 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php elseif ($tab === 'taxonomy'): ?>
|
||||
<?php fb_admin_taxonomy_page($pdo,
|
||||
isset($_GET['edit_sw']) ? (int)$_GET['edit_sw'] : 0,
|
||||
isset($_GET['edit_cat']) ? (int)$_GET['edit_cat'] : 0); ?>
|
||||
|
||||
<?php elseif ($tab === 'statuses'): ?>
|
||||
<?php fb_admin_status_page($pdo, isset($_GET['edit_st']) ? (int)$_GET['edit_st'] : 0); ?>
|
||||
|
||||
<?php elseif ($tab === 'settings'): ?>
|
||||
<?php
|
||||
$mode = fb_store_mode();
|
||||
@@ -761,7 +833,11 @@ function fb_admin_thread_card(array $t) {
|
||||
ob_start(); ?>
|
||||
<div class="card-form" style="margin:0 0 16px">
|
||||
<div class="item-top">
|
||||
<span class="badge cat"><?php echo e(fb_category_label($t['category'])); ?></span>
|
||||
<?php $cSw = !empty($t['software_id']) ? fb_software((int)$t['software_id']) : null; ?>
|
||||
<?php if ($cSw): ?><span class="badge sw"><?php echo e((string)$cSw['name']); ?></span><?php endif; ?>
|
||||
<?php if (!empty($t['category_id'])): ?>
|
||||
<span class="badge cat"><?php echo e(fb_category_label((int)$t['category_id'])); ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="badge st st-<?php echo e($t['status']); ?>"><?php echo e(fb_status_label($t['status'])); ?></span>
|
||||
<span class="item-title"><?php echo e($t['title']); ?></span>
|
||||
<span class="item-id" title="问题编号">#<?php echo (int)$t['id']; ?></span>
|
||||
|
||||
Reference in New Issue
Block a user