后台管理界面:软件/分类/状态可配,反馈可改挂归属 (#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
|
* 入口:/feedback/admin.php
|
||||||
*/
|
*/
|
||||||
require __DIR__ . '/lib/bootstrap.php';
|
require __DIR__ . '/lib/bootstrap.php';
|
||||||
|
require __DIR__ . '/lib/admin_taxonomy.php';
|
||||||
|
|
||||||
$pdo = fb_db();
|
$pdo = fb_db();
|
||||||
|
|
||||||
@@ -93,7 +94,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) {
|
|||||||
$act = (string)$_POST['act'];
|
$act = (string)$_POST['act'];
|
||||||
$t = fb_admin_thread($pdo, $id);
|
$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 = '问题不存在。';
|
$msg = '问题不存在。';
|
||||||
$msgType = 'error';
|
$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));
|
fb_mod_log('delete', $id, '删除问题:' . fb_excerpt((string)$t['title'], 40));
|
||||||
$msg = '#' . $id . ' 已删除。';
|
$msg = '#' . $id . ' 已删除。';
|
||||||
|
|
||||||
|
} elseif (fb_admin_is_taxonomy_act($act)) {
|
||||||
|
list($msg, $msgType) = fb_admin_taxonomy_action($pdo, $act, $_POST);
|
||||||
|
|
||||||
} elseif ($act === 'settings') {
|
} elseif ($act === 'settings') {
|
||||||
$msg = fb_admin_save_settings();
|
$msg = fb_admin_save_settings();
|
||||||
|
|
||||||
@@ -300,7 +308,7 @@ function fb_admin_save_settings() {
|
|||||||
* 页面
|
* 页面
|
||||||
* ================================================================== */
|
* ================================================================== */
|
||||||
$tab = (string)(isset($_GET['tab']) ? $_GET['tab'] : 'pending');
|
$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']);
|
fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
||||||
?>
|
?>
|
||||||
@@ -315,6 +323,8 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
|||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<a class="tab<?php echo $tab === 'pending' ? ' on' : ''; ?>" href="admin.php?tab=pending">待审核</a>
|
<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 === '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 === 'settings' ? ' on' : ''; ?>" href="admin.php?tab=settings">站点设置</a>
|
||||||
<a class="tab<?php echo $tab === 'log' ? ' on' : ''; ?>" href="admin.php?tab=log">操作日志</a>
|
<a class="tab<?php echo $tab === 'log' ? ' on' : ''; ?>" href="admin.php?tab=log">操作日志</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -343,9 +353,12 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
|||||||
$visF = (string)(isset($_GET['vis']) ? $_GET['vis'] : '');
|
$visF = (string)(isset($_GET['vis']) ? $_GET['vis'] : '');
|
||||||
$catF = (string)(isset($_GET['cat']) ? $_GET['cat'] : '');
|
$catF = (string)(isset($_GET['cat']) ? $_GET['cat'] : '');
|
||||||
$qF = trim((string)(isset($_GET['q']) ? $_GET['q'] : ''));
|
$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 ($stF !== '') { $where[] = 'status = ?'; $args[] = $stF; }
|
||||||
if ($visF === '1' || $visF === '0') { $where[] = 'visible = ?'; $args[] = (int)$visF; }
|
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 . '%'; }
|
if ($qF !== '') { $where[] = '(title LIKE ? OR body LIKE ?)'; $args[] = '%' . $qF . '%'; $args[] = '%' . $qF . '%'; }
|
||||||
$wSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
|
$wSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
|
||||||
|
|
||||||
@@ -364,17 +377,29 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
|||||||
<form class="filters" method="get" action="admin.php">
|
<form class="filters" method="get" action="admin.php">
|
||||||
<input type="hidden" name="tab" value="all">
|
<input type="hidden" name="tab" value="all">
|
||||||
<input type="text" name="q" value="<?php echo e($qF); ?>" placeholder="搜索标题或正文">
|
<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">
|
<select name="status">
|
||||||
<option value="">全部状态</option>
|
<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>
|
<option value="<?php echo e($k); ?>"<?php echo $stF === $k ? ' selected' : ''; ?>><?php echo e($label); ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
<select name="cat">
|
<select name="cid">
|
||||||
<option value="">全部分类</option>
|
<option value="0">全部分类</option>
|
||||||
<?php foreach (fb_categories() as $k => $label): ?>
|
<?php
|
||||||
<option value="<?php echo e($k); ?>"<?php echo $catF === $k ? ' selected' : ''; ?>><?php echo e($label); ?></option>
|
// 选了某个软件就只列它的分类;否则把所有软件的分类按软件分组列出
|
||||||
<?php endforeach; ?>
|
$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>
|
||||||
<select name="vis">
|
<select name="vis">
|
||||||
<option value="">公开与否不限</option>
|
<option value="">公开与否不限</option>
|
||||||
@@ -437,7 +462,12 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
|||||||
|
|
||||||
<div class="card-form" style="margin:0 0 16px">
|
<div class="card-form" style="margin:0 0 16px">
|
||||||
<div class="item-top">
|
<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="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-title"><?php echo e($t['title']); ?></span>
|
||||||
<span class="item-id">#<?php echo (int)$t['id']; ?></span>
|
<span class="item-id">#<?php echo (int)$t['id']; ?></span>
|
||||||
@@ -469,6 +499,40 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
|||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?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>
|
<h2 style="margin:24px 0 10px">以官方身份回复</h2>
|
||||||
<form method="post" action="admin.php?tab=thread&id=<?php echo (int)$id; ?>" class="card-form">
|
<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="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||||||
@@ -481,7 +545,7 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
|||||||
<span>同时把状态改为</span>
|
<span>同时把状态改为</span>
|
||||||
<select name="status">
|
<select name="status">
|
||||||
<option value="">(不改)</option>
|
<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>
|
<option value="<?php echo e($k); ?>"><?php echo e($label); ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
@@ -499,7 +563,7 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
|||||||
<label class="field">
|
<label class="field">
|
||||||
<span>处理状态</span>
|
<span>处理状态</span>
|
||||||
<select name="status">
|
<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>
|
<option value="<?php echo e($k); ?>"<?php echo ((string)$t['status'] === $k) ? ' selected' : ''; ?>><?php echo e($label); ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
@@ -542,6 +606,14 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
|||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?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 elseif ($tab === 'settings'): ?>
|
||||||
<?php
|
<?php
|
||||||
$mode = fb_store_mode();
|
$mode = fb_store_mode();
|
||||||
@@ -761,7 +833,11 @@ function fb_admin_thread_card(array $t) {
|
|||||||
ob_start(); ?>
|
ob_start(); ?>
|
||||||
<div class="card-form" style="margin:0 0 16px">
|
<div class="card-form" style="margin:0 0 16px">
|
||||||
<div class="item-top">
|
<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="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-title"><?php echo e($t['title']); ?></span>
|
||||||
<span class="item-id" title="问题编号">#<?php echo (int)$t['id']; ?></span>
|
<span class="item-id" title="问题编号">#<?php echo (int)$t['id']; ?></span>
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ $action = isset($_GET['action']) ? (string)$_GET['action'] : 'list';
|
|||||||
$contacts = !empty($_GET['contacts']);
|
$contacts = !empty($_GET['contacts']);
|
||||||
$limit = isset($_GET['limit']) ? max(1, min(500, (int)$_GET['limit'])) : 100;
|
$limit = isset($_GET['limit']) ? max(1, min(500, (int)$_GET['limit'])) : 100;
|
||||||
|
|
||||||
$cats = fb_categories();
|
$statuses = fb_status_keys();
|
||||||
$statuses = fb_statuses();
|
|
||||||
|
|
||||||
/** 过滤条件 */
|
/** 过滤条件 */
|
||||||
function fb_api_where(&$args, $cats, $statuses) {
|
function fb_api_where(&$args, $cats, $statuses) {
|
||||||
@@ -43,8 +42,32 @@ function fb_api_where(&$args, $cats, $statuses) {
|
|||||||
if (!empty($_GET['status']) && isset($statuses[(string)$_GET['status']])) {
|
if (!empty($_GET['status']) && isset($statuses[(string)$_GET['status']])) {
|
||||||
$w[] = 'status = ?'; $args[] = (string)$_GET['status'];
|
$w[] = 'status = ?'; $args[] = (string)$_GET['status'];
|
||||||
}
|
}
|
||||||
if (!empty($_GET['category']) && isset($cats[(string)$_GET['category']])) {
|
// 软件:?sw=<slug> 或 ?software_id=<id>
|
||||||
$w[] = 'category = ?'; $args[] = (string)$_GET['category'];
|
$swArg = '';
|
||||||
|
if (!empty($_GET['sw'])) {
|
||||||
|
$sw = fb_software((string)$_GET['sw']);
|
||||||
|
if ($sw) { $w[] = 'software_id = ?'; $args[] = (int)$sw['id']; $swArg = 'sw'; }
|
||||||
|
} elseif (!empty($_GET['software_id'])) {
|
||||||
|
$sw = fb_software((int)$_GET['software_id']);
|
||||||
|
if ($sw) { $w[] = 'software_id = ?'; $args[] = (int)$sw['id']; $swArg = 'id'; }
|
||||||
|
}
|
||||||
|
// 分类:?cid=<id> 优先;兼容老的 ?category=<slug>,在其所属软件内解析
|
||||||
|
if (!empty($_GET['cid'])) {
|
||||||
|
$c = fb_category((int)$_GET['cid']);
|
||||||
|
if ($c) { $w[] = 'category_id = ?'; $args[] = (int)$c['id']; }
|
||||||
|
} elseif (!empty($_GET['category'])) {
|
||||||
|
$slug = (string)$_GET['category'];
|
||||||
|
$swId = 0;
|
||||||
|
if (!empty($_GET['sw'])) {
|
||||||
|
$sw0 = fb_software((string)$_GET['sw']);
|
||||||
|
if ($sw0) { $swId = (int)$sw0['id']; }
|
||||||
|
}
|
||||||
|
if ($swId <= 0) {
|
||||||
|
$d = fb_default_software();
|
||||||
|
if ($d) { $swId = (int)$d['id']; }
|
||||||
|
}
|
||||||
|
$c = fb_category_by_slug($swId, $slug);
|
||||||
|
if ($c) { $w[] = 'category_id = ?'; $args[] = (int)$c['id']; }
|
||||||
}
|
}
|
||||||
if (!empty($_GET['since'])) {
|
if (!empty($_GET['since'])) {
|
||||||
$w[] = 'updated_at >= ?'; $args[] = (int)$_GET['since'];
|
$w[] = 'updated_at >= ?'; $args[] = (int)$_GET['since'];
|
||||||
@@ -58,7 +81,9 @@ function fb_api_thread(array $t, $contacts) {
|
|||||||
'id' => (int)$t['id'],
|
'id' => (int)$t['id'],
|
||||||
'title' => $t['title'],
|
'title' => $t['title'],
|
||||||
'category' => $t['category'],
|
'category' => $t['category'],
|
||||||
'category_cn' => fb_category_label($t['category']),
|
'category_id' => isset($t['category_id']) ? (int)$t['category_id'] : null,
|
||||||
|
'category_cn' => fb_category_label(isset($t['category_id']) ? $t['category_id'] : 0),
|
||||||
|
'software_id' => isset($t['software_id']) ? (int)$t['software_id'] : null,
|
||||||
'status' => $t['status'],
|
'status' => $t['status'],
|
||||||
'status_cn' => fb_status_label($t['status']),
|
'status_cn' => fb_status_label($t['status']),
|
||||||
'author' => $t['author_name'],
|
'author' => $t['author_name'],
|
||||||
|
|||||||
@@ -81,6 +81,43 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
|
|||||||
.page-head h1{margin-bottom:4px}
|
.page-head h1{margin-bottom:4px}
|
||||||
.page-head .muted{font-size:14px; margin:0}
|
.page-head .muted{font-size:14px; margin:0}
|
||||||
|
|
||||||
|
/* ---------- 后台:软件 / 分类 / 状态 ---------- */
|
||||||
|
.card-list{display:flex; flex-direction:column; gap:8px; margin-bottom:16px}
|
||||||
|
.tax-row{
|
||||||
|
display:flex; align-items:flex-start; gap:12px; flex-wrap:wrap;
|
||||||
|
padding:12px 14px; background:var(--card); border:1px solid var(--line);
|
||||||
|
border-radius:11px;
|
||||||
|
}
|
||||||
|
.tax-row.on{border-color:var(--brand); box-shadow:0 0 0 1px var(--brand) inset}
|
||||||
|
.tax-row.off{opacity:.62}
|
||||||
|
.tax-main{flex:1 1 320px; min-width:0}
|
||||||
|
.tax-name{font-weight:600; font-size:15.5px; text-decoration:none; color:var(--fg)}
|
||||||
|
.tax-name:hover{color:var(--brand)}
|
||||||
|
.tax-name.plain{font-weight:600}
|
||||||
|
.tax-slug{
|
||||||
|
display:inline-block; margin-left:8px; font-size:12px; color:var(--muted);
|
||||||
|
font-family:ui-monospace,Consolas,monospace;
|
||||||
|
}
|
||||||
|
.tax-meta{margin-left:10px; font-size:12.5px; color:var(--muted)}
|
||||||
|
.tax-note{margin-top:5px; font-size:13px; color:var(--muted); line-height:1.5}
|
||||||
|
.tax-acts{display:flex; flex-wrap:wrap; gap:6px; align-items:center}
|
||||||
|
.inline-form{display:inline}
|
||||||
|
.btn.sm{padding:5px 11px; font-size:13px; border-radius:8px}
|
||||||
|
.btn.danger{border-color:#e0b4b4; color:#b4242c}
|
||||||
|
.btn.danger:hover{background:#fdf2f2; border-color:#b4242c}
|
||||||
|
.tax-form-box{
|
||||||
|
margin:18px 0 8px; border:1px solid var(--line); border-radius:11px;
|
||||||
|
background:var(--card); overflow:hidden;
|
||||||
|
}
|
||||||
|
.tax-form-box>summary{
|
||||||
|
cursor:pointer; padding:12px 14px; font-weight:600; font-size:14.5px;
|
||||||
|
background:var(--bg-soft, #f7f8fa); user-select:none;
|
||||||
|
}
|
||||||
|
.tax-form-box>summary:hover{color:var(--brand)}
|
||||||
|
.tax-form-box[open]>summary{border-bottom:1px solid var(--line)}
|
||||||
|
.sm-form{padding:14px; margin:0}
|
||||||
|
.sm-form .field{margin-bottom:10px}
|
||||||
|
|
||||||
/* ---------- 软件切换(一级) ---------- */
|
/* ---------- 软件切换(一级) ---------- */
|
||||||
.software-bar{
|
.software-bar{
|
||||||
display:flex; flex-wrap:wrap; align-items:center; gap:8px;
|
display:flex; flex-wrap:wrap; align-items:center; gap:8px;
|
||||||
@@ -136,6 +173,9 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
|
|||||||
.badge.st-resolved{background:var(--ok-soft); color:var(--ok)}
|
.badge.st-resolved{background:var(--ok-soft); color:var(--ok)}
|
||||||
.badge.st-closed{background:#f1f3f5; color:#8a929c}
|
.badge.st-closed{background:#f1f3f5; color:#8a929c}
|
||||||
.badge.official{background:var(--brand); color:#fff}
|
.badge.official{background:var(--brand); color:#fff}
|
||||||
|
/* 软件徽章:比分类更"重",一眼看出属于哪个软件 */
|
||||||
|
.badge.sw{background:#eef2f8; color:#2b4a74; border:1px solid #cfdcee; font-weight:600}
|
||||||
|
.badge.st-wontfix{background:#f6eef0; color:#a13c53}
|
||||||
|
|
||||||
/* ---------- 分页 ---------- */
|
/* ---------- 分页 ---------- */
|
||||||
.pager{display:flex; align-items:center; gap:12px; margin:22px 0 0; justify-content:center}
|
.pager{display:flex; align-items:center; gap:12px; margin:22px 0 0; justify-content:center}
|
||||||
|
|||||||
+8
-16
@@ -50,22 +50,14 @@ return [
|
|||||||
// 对访客 IP 做加盐哈希(数据库只保存哈希,不保存明文 IP)。★ 请修改为随机值 ★
|
// 对访客 IP 做加盐哈希(数据库只保存哈希,不保存明文 IP)。★ 请修改为随机值 ★
|
||||||
'ip_salt' => 'CHANGE_ME_random_salt',
|
'ip_salt' => 'CHANGE_ME_random_salt',
|
||||||
|
|
||||||
/* ---------------- 问题分类 ---------------- */
|
/* ---------------- 问题分类与处理状态:已移入数据库 ----------------
|
||||||
'categories' => [
|
* 不再写在这里。安装时会把出厂值写进 data/ 的数据库,
|
||||||
'usage' => '使用问题',
|
* 之后在「管理后台 → 软件与分类 / 处理状态」里增删改,改这里无效。
|
||||||
'bug' => 'Bug 反馈',
|
*
|
||||||
'feature' => '功能建议',
|
* 出厂分类(首个软件下):使用问题 / Bug 反馈 / 功能建议 / 同步问题 / 其他
|
||||||
'sync' => '同步问题',
|
* 出厂状态(5 个):待处理 / 处理中 / 已解决 / 已关闭 / 不予考虑
|
||||||
'other' => '其他',
|
* 每个状态都带一句「给用户看的话」,会在通知邮件里发给提交人。
|
||||||
],
|
*/
|
||||||
|
|
||||||
/* ---------------- 处理状态 ---------------- */
|
|
||||||
'statuses' => [
|
|
||||||
'open' => '待处理',
|
|
||||||
'doing' => '处理中',
|
|
||||||
'resolved' => '已解决',
|
|
||||||
'closed' => '已关闭',
|
|
||||||
],
|
|
||||||
|
|
||||||
/* ---------------- 邮件通知(SMTP) ---------------- */
|
/* ---------------- 邮件通知(SMTP) ---------------- */
|
||||||
'smtp' => [
|
'smtp' => [
|
||||||
|
|||||||
@@ -0,0 +1,456 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 后台:软件 / 分类 / 状态 管理
|
||||||
|
* 全部存数据库,管理员在这里增删改排序启停,不用碰配置文件。
|
||||||
|
*/
|
||||||
|
if (!defined('FB_ROOT')) { http_response_code(403); exit('forbidden'); }
|
||||||
|
|
||||||
|
/* ==================================================================
|
||||||
|
* 统一的表单处理入口(由 admin.php 在 CSRF 校验通过后调用)
|
||||||
|
* 返回 [消息, 类型],类型 ok / error
|
||||||
|
* ================================================================== */
|
||||||
|
function fb_admin_taxonomy_action($pdo, $act, $post) {
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
switch ($act) {
|
||||||
|
|
||||||
|
/* ---------------- 软件 ---------------- */
|
||||||
|
case 'sw_add': {
|
||||||
|
$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 (fb_len($name) > 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 '<h2 style="margin:22px 0 10px">软件</h2>';
|
||||||
|
echo '<p class="muted" style="margin-top:-4px;font-size:13.5px">'
|
||||||
|
. '前台按软件分栏显示。停用的软件前台不再出现,但它下面的反馈与分类都保留。'
|
||||||
|
. '</p>';
|
||||||
|
|
||||||
|
if (!$swList) {
|
||||||
|
echo '<div class="empty"><p>还没有软件,先添加一个。</p></div>';
|
||||||
|
} else {
|
||||||
|
echo '<div class="card-list">';
|
||||||
|
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 '<div class="tax-row' . ($isCur ? ' on' : '') . ($on ? '' : ' off') . '">';
|
||||||
|
echo ' <div class="tax-main">';
|
||||||
|
echo ' <a class="tax-name" href="admin.php?tab=taxonomy&sw=' . $sid . '">' . e((string)$sw['name']) . '</a>';
|
||||||
|
echo ' <span class="tax-slug">' . e((string)$sw['slug']) . '</span>';
|
||||||
|
if (!$on) { echo ' <span class="badge st">已停用</span>'; }
|
||||||
|
echo ' <span class="tax-meta">' . $nCat . ' 个分类 · ' . $nTh . ' 条反馈</span>';
|
||||||
|
if (!empty($sw['note'])) { echo ' <div class="tax-note">' . e((string)$sw['note']) . '</div>'; }
|
||||||
|
echo ' </div>';
|
||||||
|
echo ' <div class="tax-acts">';
|
||||||
|
echo ' <a class="btn sm" href="admin.php?tab=taxonomy&sw=' . $sid . '">分类</a>';
|
||||||
|
echo ' <a class="btn sm" href="admin.php?tab=taxonomy&edit_sw=' . $sid . '">编辑</a>';
|
||||||
|
echo ' <form method="post" action="admin.php?tab=taxonomy&sw=' . $sid . '" class="inline-form">';
|
||||||
|
echo ' <input type="hidden" name="csrf" value="' . e(fb_csrf_token()) . '">';
|
||||||
|
echo ' <input type="hidden" name="id" value="' . $sid . '">';
|
||||||
|
echo ' <button class="btn sm" name="act" value="sw_toggle" type="submit">' . ($on ? '停用' : '启用') . '</button>';
|
||||||
|
echo ' </form>';
|
||||||
|
echo ' <form method="post" action="admin.php?tab=taxonomy&sw=' . $sid . '" class="inline-form"'
|
||||||
|
. ' onsubmit="return confirm(\'确定删除软件「' . e((string)$sw['name']) . '」?\n只有在该软件下没有反馈时才能删除。\');">';
|
||||||
|
echo ' <input type="hidden" name="csrf" value="' . e(fb_csrf_token()) . '">';
|
||||||
|
echo ' <input type="hidden" name="id" value="' . $sid . '">';
|
||||||
|
echo ' <button class="btn sm danger" name="act" value="sw_del" type="submit">删除</button>';
|
||||||
|
echo ' </form>';
|
||||||
|
echo ' </div>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 新增 / 编辑软件 ---- */
|
||||||
|
$esw = null;
|
||||||
|
if ($editSw > 0) { $esw = fb_software($editSw); }
|
||||||
|
echo '<details class="tax-form-box"' . ($esw ? ' open' : '') . '>';
|
||||||
|
echo '<summary>' . ($esw ? '编辑软件「' . e((string)$esw['name']) . '」' : '添加软件') . '</summary>';
|
||||||
|
echo '<form method="post" action="admin.php?tab=taxonomy" class="card-form sm-form">';
|
||||||
|
echo ' <input type="hidden" name="csrf" value="' . e(fb_csrf_token()) . '">';
|
||||||
|
if ($esw) { echo ' <input type="hidden" name="id" value="' . (int)$esw['id'] . '">'; }
|
||||||
|
echo ' <div class="row2">';
|
||||||
|
echo ' <label class="field"><span>名称 <b class="req">*</b></span>'
|
||||||
|
. '<input type="text" name="name" maxlength="40" required value="'
|
||||||
|
. e($esw ? (string)$esw['name'] : '') . '" placeholder="例如:同步日历"></label>';
|
||||||
|
echo ' <label class="field"><span>英文标识<i class="hint">网址里用,留空自动生成</i></span>'
|
||||||
|
. '<input type="text" name="slug" maxlength="40" value="'
|
||||||
|
. e($esw ? (string)$esw['slug'] : '') . '" placeholder="synccalendar"></label>';
|
||||||
|
echo ' </div>';
|
||||||
|
echo ' <label class="field"><span>一句话说明<i class="hint">显示在列表页标题下,可留空</i></span>'
|
||||||
|
. '<input type="text" name="note" maxlength="120" value="'
|
||||||
|
. e($esw ? (string)($esw['note'] === null ? '' : $esw['note']) : '') . '"></label>';
|
||||||
|
if ($esw) {
|
||||||
|
echo ' <label class="field"><span>排序<i class="hint">数字小的排前面</i></span>'
|
||||||
|
. '<input type="number" name="sort_order" value="' . (int)$esw['sort_order'] . '"></label>';
|
||||||
|
echo ' <button class="btn primary" name="act" value="sw_edit" type="submit">保存</button> '
|
||||||
|
. '<a class="btn" href="admin.php?tab=taxonomy">取消</a>';
|
||||||
|
} else {
|
||||||
|
echo ' <button class="btn primary" name="act" value="sw_add" type="submit">添加软件</button>';
|
||||||
|
}
|
||||||
|
echo '</form></details>';
|
||||||
|
|
||||||
|
/* ---- 该软件下的分类 ---- */
|
||||||
|
if (!$curSw) { return; }
|
||||||
|
|
||||||
|
echo '<h2 style="margin:30px 0 10px">「' . e((string)$curSw['name']) . '」的分类</h2>';
|
||||||
|
echo '<p class="muted" style="margin-top:-4px;font-size:13.5px">'
|
||||||
|
. '每个软件有自己的一套分类,名称可以和其他软件重复。'
|
||||||
|
. '停用的分类前台不再出现,历史反馈照旧保留。'
|
||||||
|
. '</p>';
|
||||||
|
|
||||||
|
if (!$curCats) {
|
||||||
|
echo '<div class="empty"><p>这个软件还没有分类,先加一个。</p></div>';
|
||||||
|
} else {
|
||||||
|
echo '<div class="card-list">';
|
||||||
|
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 '<div class="tax-row' . ($on ? '' : ' off') . '">';
|
||||||
|
echo ' <div class="tax-main">';
|
||||||
|
echo ' <span class="tax-name plain">' . e((string)$c['name']) . '</span>';
|
||||||
|
echo ' <span class="tax-slug">' . e((string)$c['slug']) . '</span>';
|
||||||
|
if (!$on) { echo ' <span class="badge st">已停用</span>'; }
|
||||||
|
echo ' <span class="tax-meta">' . $n . ' 条反馈</span>';
|
||||||
|
echo ' </div>';
|
||||||
|
echo ' <div class="tax-acts">';
|
||||||
|
echo ' <a class="btn sm" href="admin.php?tab=taxonomy&sw=' . (int)$curSw['id'] . '&edit_cat=' . $cid . '">编辑</a>';
|
||||||
|
echo ' <form method="post" action="admin.php?tab=taxonomy&sw=' . (int)$curSw['id'] . '" class="inline-form">';
|
||||||
|
echo ' <input type="hidden" name="csrf" value="' . e(fb_csrf_token()) . '">';
|
||||||
|
echo ' <input type="hidden" name="id" value="' . $cid . '">';
|
||||||
|
echo ' <button class="btn sm" name="act" value="cat_toggle" type="submit">' . ($on ? '停用' : '启用') . '</button>';
|
||||||
|
echo ' </form>';
|
||||||
|
echo ' <form method="post" action="admin.php?tab=taxonomy&sw=' . (int)$curSw['id'] . '" class="inline-form"'
|
||||||
|
. ' onsubmit="return confirm(\'确定删除分类「' . e((string)$c['name']) . '」?\n只有在该分类下没有反馈时才能删除。\');">';
|
||||||
|
echo ' <input type="hidden" name="csrf" value="' . e(fb_csrf_token()) . '">';
|
||||||
|
echo ' <input type="hidden" name="id" value="' . $cid . '">';
|
||||||
|
echo ' <button class="btn sm danger" name="act" value="cat_del" type="submit">删除</button>';
|
||||||
|
echo ' </form>';
|
||||||
|
echo ' </div>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 新增 / 编辑分类 ---- */
|
||||||
|
$ecat = $editCat > 0 ? fb_category($editCat) : null;
|
||||||
|
echo '<details class="tax-form-box"' . ($ecat ? ' open' : '') . '>';
|
||||||
|
echo '<summary>' . ($ecat ? '编辑分类「' . e((string)$ecat['name']) . '」' : '添加分类') . '</summary>';
|
||||||
|
echo '<form method="post" action="admin.php?tab=taxonomy&sw=' . (int)$curSw['id'] . '" class="card-form sm-form">';
|
||||||
|
echo ' <input type="hidden" name="csrf" value="' . e(fb_csrf_token()) . '">';
|
||||||
|
echo ' <input type="hidden" name="software_id" value="' . (int)$curSw['id'] . '">';
|
||||||
|
if ($ecat) { echo ' <input type="hidden" name="id" value="' . (int)$ecat['id'] . '">'; }
|
||||||
|
echo ' <div class="row2">';
|
||||||
|
echo ' <label class="field"><span>分类名称 <b class="req">*</b></span>'
|
||||||
|
. '<input type="text" name="name" maxlength="30" required value="'
|
||||||
|
. e($ecat ? (string)$ecat['name'] : '') . '" placeholder="例如:Bug"></label>';
|
||||||
|
echo ' <label class="field"><span>英文标识<i class="hint">网址里用,留空自动生成</i></span>'
|
||||||
|
. '<input type="text" name="slug" maxlength="40" value="'
|
||||||
|
. e($ecat ? (string)$ecat['slug'] : '') . '" placeholder="bug"></label>';
|
||||||
|
echo ' </div>';
|
||||||
|
if ($ecat) {
|
||||||
|
echo ' <label class="field"><span>排序<i class="hint">数字小的排前面</i></span>'
|
||||||
|
. '<input type="number" name="sort_order" value="' . (int)$ecat['sort_order'] . '"></label>';
|
||||||
|
echo ' <button class="btn primary" name="act" value="cat_edit" type="submit">保存</button> '
|
||||||
|
. '<a class="btn" href="admin.php?tab=taxonomy&sw=' . (int)$curSw['id'] . '">取消</a>';
|
||||||
|
} else {
|
||||||
|
echo ' <button class="btn primary" name="act" value="cat_add" type="submit">添加分类</button>';
|
||||||
|
}
|
||||||
|
echo '</form></details>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================================================================
|
||||||
|
* 页面:状态
|
||||||
|
* ================================================================== */
|
||||||
|
function fb_admin_status_page($pdo, $editId) {
|
||||||
|
$rows = fb_statuses(false);
|
||||||
|
|
||||||
|
echo '<h2 style="margin:22px 0 10px">处理状态</h2>';
|
||||||
|
echo '<p class="muted" style="margin-top:-4px;font-size:13.5px">'
|
||||||
|
. '「给用户看的话」会写进发给提交人的邮件正文。改这里的文字,前台与邮件都会跟着变。'
|
||||||
|
. '</p>';
|
||||||
|
|
||||||
|
echo '<div class="card-list">';
|
||||||
|
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 '<div class="tax-row' . ($on ? '' : ' off') . '">';
|
||||||
|
echo ' <div class="tax-main">';
|
||||||
|
echo ' <span class="badge st st-' . e((string)$r['key_name']) . '">' . e((string)$r['name']) . '</span>';
|
||||||
|
echo ' <span class="tax-slug">' . e((string)$r['key_name']) . '</span>';
|
||||||
|
if (!$on) { echo ' <span class="badge st">已停用</span>'; }
|
||||||
|
echo ' <span class="tax-meta">' . $n . ' 条反馈</span>';
|
||||||
|
$note = (string)($r['user_note'] === null ? '' : $r['user_note']);
|
||||||
|
echo ' <div class="tax-note">' . ($note !== '' ? e($note) : '<i>(没有给用户看的说明)</i>') . '</div>';
|
||||||
|
echo ' </div>';
|
||||||
|
echo ' <div class="tax-acts">';
|
||||||
|
echo ' <a class="btn sm" href="admin.php?tab=statuses&edit_st=' . $rid . '">编辑</a>';
|
||||||
|
echo ' <form method="post" action="admin.php?tab=statuses" class="inline-form">';
|
||||||
|
echo ' <input type="hidden" name="csrf" value="' . e(fb_csrf_token()) . '">';
|
||||||
|
echo ' <input type="hidden" name="id" value="' . $rid . '">';
|
||||||
|
echo ' <button class="btn sm" name="act" value="st_toggle" type="submit">' . ($on ? '停用' : '启用') . '</button>';
|
||||||
|
echo ' </form>';
|
||||||
|
echo ' </div>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
/* ---- 新增 / 编辑状态 ---- */
|
||||||
|
$est = null;
|
||||||
|
if ($editId > 0) {
|
||||||
|
$q = $pdo->prepare('SELECT * FROM statuses WHERE id = ? LIMIT 1');
|
||||||
|
$q->execute([$editId]);
|
||||||
|
$est = $q->fetch();
|
||||||
|
}
|
||||||
|
echo '<details class="tax-form-box"' . ($est ? ' open' : '') . '>';
|
||||||
|
echo '<summary>' . ($est ? '编辑状态「' . e((string)$est['name']) . '」' : '添加状态') . '</summary>';
|
||||||
|
echo '<form method="post" action="admin.php?tab=statuses" class="card-form sm-form">';
|
||||||
|
echo ' <input type="hidden" name="csrf" value="' . e(fb_csrf_token()) . '">';
|
||||||
|
if ($est) { echo ' <input type="hidden" name="id" value="' . (int)$est['id'] . '">'; }
|
||||||
|
echo ' <div class="row2">';
|
||||||
|
if ($est) {
|
||||||
|
echo ' <label class="field"><span>标识<i class="hint">不可修改(历史数据靠它对应)</i></span>'
|
||||||
|
. '<input type="text" value="' . e((string)$est['key_name']) . '" disabled></label>';
|
||||||
|
} else {
|
||||||
|
echo ' <label class="field"><span>标识 <b class="req">*</b><i class="hint">字母数字连字符</i></span>'
|
||||||
|
. '<input type="text" name="key_name" maxlength="30" required placeholder="wontfix"></label>';
|
||||||
|
}
|
||||||
|
echo ' <label class="field"><span>显示名称 <b class="req">*</b></span>'
|
||||||
|
. '<input type="text" name="name" maxlength="20" required value="'
|
||||||
|
. e($est ? (string)$est['name'] : '') . '" placeholder="不予考虑"></label>';
|
||||||
|
echo ' </div>';
|
||||||
|
echo ' <label class="field"><span>给用户看的话<i class="hint">会写进通知邮件</i></span>'
|
||||||
|
. '<textarea name="user_note" rows="2" maxlength="200">'
|
||||||
|
. e($est ? (string)($est['user_note'] === null ? '' : $est['user_note']) : '') . '</textarea></label>';
|
||||||
|
if ($est) {
|
||||||
|
echo ' <label class="field"><span>排序</span>'
|
||||||
|
. '<input type="number" name="sort_order" value="' . (int)$est['sort_order'] . '"></label>';
|
||||||
|
echo ' <button class="btn primary" name="act" value="st_edit" type="submit">保存</button> '
|
||||||
|
. '<a class="btn" href="admin.php?tab=statuses">取消</a>';
|
||||||
|
} else {
|
||||||
|
echo ' <button class="btn primary" name="act" value="st_add" type="submit">添加状态</button>';
|
||||||
|
}
|
||||||
|
echo '</form></details>';
|
||||||
|
}
|
||||||
+8
-1
@@ -293,7 +293,14 @@ SQL
|
|||||||
$old = [];
|
$old = [];
|
||||||
try { $old = (array)fb_cfg('categories', []); } catch (\Throwable $e) { $old = []; }
|
try { $old = (array)fb_cfg('categories', []); } catch (\Throwable $e) { $old = []; }
|
||||||
if (!$old) {
|
if (!$old) {
|
||||||
$old = ['usage' => '使用问题', 'bug' => 'Bug', 'feature' => '功能建议', 'other' => '其他'];
|
// 出厂分类:与改造前配置里的五个完全一致,一个都不能少
|
||||||
|
$old = [
|
||||||
|
'usage' => '使用问题',
|
||||||
|
'bug' => 'Bug 反馈',
|
||||||
|
'feature' => '功能建议',
|
||||||
|
'sync' => '同步问题',
|
||||||
|
'other' => '其他',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
$i = 0;
|
$i = 0;
|
||||||
$st = $pdo->prepare('INSERT INTO categories(software_id, slug, name, sort_order, enabled, created_at)
|
$st = $pdo->prepare('INSERT INTO categories(software_id, slug, name, sort_order, enabled, created_at)
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ function fb_layout_head($title, array $opt = []) {
|
|||||||
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon-16x16.png">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon.png">
|
||||||
<link rel="manifest" href="assets/site.webmanifest">
|
<link rel="manifest" href="assets/site.webmanifest">
|
||||||
<link rel="stylesheet" href="assets/style.css?v=8">
|
<link rel="stylesheet" href="assets/style.css?v=10">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
|
|||||||
+7
-2
@@ -42,7 +42,7 @@ if (!fb_thread_public($thread)) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$statuses = fb_statuses();
|
$statuses = fb_status_keys();
|
||||||
$err = '';
|
$err = '';
|
||||||
$atts = [];
|
$atts = [];
|
||||||
$form = ['name' => '', 'email' => '', 'body' => '', 'captcha' => ''];
|
$form = ['name' => '', 'email' => '', 'body' => '', 'captcha' => ''];
|
||||||
@@ -156,7 +156,12 @@ fb_layout_head(fb_excerpt($thread['title'], 50));
|
|||||||
|
|
||||||
<article class="subject">
|
<article class="subject">
|
||||||
<div class="item-top">
|
<div class="item-top">
|
||||||
<span class="badge cat"><?php echo e(fb_category_label($thread['category'])); ?></span>
|
<?php
|
||||||
|
$thSw = !empty($thread['software_id']) ? fb_software((int)$thread['software_id']) : null;
|
||||||
|
$thCat = !empty($thread['category_id']) ? fb_category((int)$thread['category_id']) : null;
|
||||||
|
?>
|
||||||
|
<?php if ($thSw): ?><span class="badge sw"><?php echo e((string)$thSw['name']); ?></span><?php endif; ?>
|
||||||
|
<?php if ($thCat): ?><span class="badge cat"><?php echo e((string)$thCat['name']); ?></span><?php endif; ?>
|
||||||
<span class="badge st st-<?php echo e($thread['status']); ?>"><?php echo e(fb_status_label($thread['status'])); ?></span>
|
<span class="badge st st-<?php echo e($thread['status']); ?>"><?php echo e(fb_status_label($thread['status'])); ?></span>
|
||||||
<h1 class="subject-title"><?php echo e($thread['title']); ?></h1>
|
<h1 class="subject-title"><?php echo e($thread['title']); ?></h1>
|
||||||
<span class="item-id" title="问题编号">#<?php echo (int)$id; ?></span>
|
<span class="item-id" title="问题编号">#<?php echo (int)$id; ?></span>
|
||||||
|
|||||||
Reference in New Issue
Block a user