新增「未公开」状态;待审核改列表式,只留通过并公开

用户要求:
1. 待审核/全部问题都用列表形式
2. 待审核去掉「关闭说明」整块,后面只加一个「通过并公开」按钮
3. 标题本身就是进管理页的链接,去掉「进入管理」
4. 「未公开」应当是一种状态:提交后初始状态不是「待处理」而是「未公开」

状态流转(用户定义):
  提交 → 未公开
    ├─ 通过并公开 → 待处理 → 处理中 → 已解决 → 已关闭
    └─ 直接关闭   → 已关闭(不经过待处理)
  每次状态变更、每次有人回复,都给提问人发信

实现:
- statuses 新增 hidden「未公开」,sort=0 排最前,其余顺延
  幂等迁移:老库检测到没有 hidden 就整体后移一位再插入,
  并把 visible=0 且 status=open 的存量数据归到 hidden
- new.php 初始状态按可见性决定:未公开→hidden,公开→open
  (以前一律写 open,后台「待审核」里显示的全是「待处理」,状态反映不出事实)
- approve 动作:visible=1 且 hidden→open,并用更新后的行渲染邮件
- 详情页「通过并公开」按钮原来传原状态(未公开时通过后仍是未公开),改为固定传 open
- 待审核查询改为 status=hidden(兼容老数据 visible=0 且 open),
  不再用 visible=0 AND status<>closed —— 那个条件会把「已关闭但未公开」也收进来
- 新增 fb_admin_pending_row() 列表行渲染:标题即链接、右侧单个按钮
- 删除 fb_admin_thread_card() 及「关闭说明」表单
- 新增 .plist/.prow 列表样式与 .st-hidden 状态徽标(中性灰)
- mailer 补 hidden 状态的文案,避免落到兜底

验证(线上真实数据):6 个状态齐全、提交落 hidden、
待审核能查到且不含已公开、通过后转 open 并移出待审核、
后续 doing/resolved/closed 推进正常、6 个状态邮件均能渲染、
待审核行结构 10 项断言全过(无关闭说明/无 note/无进入管理/标题可点)。
测试数据已清理,用户原有 4 条数据完好。
This commit is contained in:
2026-11-03 09:09:40 +08:00
parent db8c1969af
commit 314a1cf2a5
5 changed files with 143 additions and 61 deletions
+52 -37
View File
@@ -116,17 +116,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) {
$msgType = 'error';
} elseif ($act === 'approve') {
$newStatus = ((string)$t['status'] === 'closed') ? 'open' : (string)$t['status'];
/* 「通过并公开」= 两件事一起做:
* ① 可见性:visible=1
* ② 状态:『未公开』→『待处理』
*
* 状态流转是用户在 2026-11-03 明确要求的:提交后先落「未公开」,
* 通过了才变「待处理」;如果判定不是问题,直接走 close 变「已关闭」,
* 中间不经过「待处理」。
*
* 边界:如果这条本来就已经是别的状态(比如以前误操作改成了「处理中」),
* 通过审核不该把状态倒退回去,所以只在「未公开」时才推进。
* 老数据可能还是 'open' + visible=0,也一并视作待推进。 */
$oldSt = (string)$t['status'];
$newStatus = ($oldSt === 'hidden' || $oldSt === 'open') ? 'open' : $oldSt;
$pdo->prepare('UPDATE threads SET visible = 1, status = ?, mod_note = NULL, moderated_at = ? WHERE id = ?')
->execute([$newStatus, time(), $id]);
fb_mod_log('approve', $id, '通过审核并公开');
fb_mod_log('approve', $id, '通过审核并公开(状态:' . fb_status_label($newStatus) . ')');
// 通知提交人「已发布」(工单 #2)
$errA = '';
try {
list($okA, $errA) = fb_notify_event($pdo, 'approve', $t);
// 用更新后的状态渲染邮件,否则信里还写着「未公开」
$fresh = fb_admin_thread($pdo, $id);
list($okA, $errA) = fb_notify_event($pdo, 'approve', $fresh ? $fresh : $t);
} catch (Throwable $e) { $errA = '发信异常'; }
$msg = '#' . $id . ' 已通过审核,现已公开显示。'
$msg = '#' . $id . ' 已通过审核并公开,状态为「' . fb_status_label($newStatus) . '」。'
. ($errA === '' ? '已发邮件通知提交人。' : '(邮件未发出:' . $errA . ')');
} elseif ($act === 'close') {
@@ -648,16 +662,23 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
<?php if ($tab === 'pending'): ?>
<?php
/* 「待审核」= 还没公开过的条目。
* 判据用 status='hidden'(未公开),不再用 visible=0 这种隐式条件 ——
* 已关闭的条目 visible 也是 0,但它是处理完了,不该出现在待审核里。
* 顺便兼容老数据:visible=0 且状态仍是 'open' 的也一并算待审核。 */
$pending = $pdo->query(
"SELECT id, title, body, category, status, author_name, author_email, created_at
FROM threads WHERE visible = 0 AND status <> 'closed' ORDER BY created_at DESC"
"SELECT * FROM threads
WHERE status = 'hidden' OR (visible = 0 AND status = 'open')
ORDER BY created_at DESC"
)->fetchAll();
?>
<h2 style="margin:22px 0 10px">待审核 <span class="muted">(<?php echo count($pending); ?>)</span></h2>
<h2 style="margin:22px 0 12px">待审核 <span class="muted">(<?php echo count($pending); ?>)</span></h2>
<?php if (!$pending): ?>
<div class="empty"><p>当前没有待审核的反馈。</p></div>
<?php else: ?>
<?php foreach ($pending as $t) { echo fb_admin_thread_card($t); } ?>
<div class="plist">
<?php foreach ($pending as $t) { echo fb_admin_pending_row($t); } ?>
</div>
<?php endif; ?>
<?php elseif ($tab === 'all'): ?>
@@ -828,7 +849,10 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
<form method="post" action="admin.php?tab=thread&amp;id=<?php echo (int)$id; ?>" class="act-inline">
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
<input type="hidden" name="id" value="<?php echo (int)$id; ?>">
<input type="hidden" name="status" value="<?php echo e($curStatus === 'closed' ? 'open' : $curStatus); ?>">
<?php /* 通过并公开 = 可见 + 状态转「待处理」。
不能沿用原状态(未公开时原状态是 hidden,通过后还是未公开,
状态和可见性就对不上了)。 */ ?>
<input type="hidden" name="status" value="open">
<input type="hidden" name="also_show" value="1">
<button class="btn primary act-btn" type="submit" name="act" value="setstatus">通过并公开</button>
</form>
@@ -1411,53 +1435,44 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
fb_layout_foot();
/**
* 待审核卡片(含「通过」与「关闭并邮件告知」两个操作)
* 待审核条目(列表式)
*
* 用户 2026-11-03 明确要求:
* ① 列表形式显示,不要每张卡片塞满表单
* ② 去掉「关闭说明」整块 —— 想关闭就点标题进管理页再填说明,
* 列表页不承担这个职责
* ③ 后面只留一个「通过并公开」按钮,点一下就通过
* ④ 标题自己就是进管理页的链接,别再额外挂「进入管理」
*/
function fb_admin_thread_card(array $t) {
function fb_admin_pending_row(array $t) {
$url = 'admin.php?tab=thread&id=' . (int)$t['id'];
ob_start(); ?>
<div class="card-form" style="margin:0 0 16px">
<div class="item-top">
<div class="prow">
<div class="prow-main">
<?php $cSw = !empty($t['software_id']) ? fb_software((int)$t['software_id']) : null; ?>
<div class="prow-top">
<?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>
<a class="prow-title" href="<?php echo e($url); ?>"><?php echo e($t['title']); ?></a>
<span class="item-id" title="问题编号">#<?php echo (int)$t['id']; ?></span>
</div>
<div class="item-ex" style="white-space:normal"><?php echo nl2br(e($t['body'])); ?></div>
<div class="item-meta">
<p class="prow-ex"><?php echo e(fb_excerpt((string)$t['body'], 120)); ?></p>
<div class="prow-meta">
<span class="who"><?php echo e($t['author_name']); ?></span>
<span class="dot">·</span>
<span><?php echo e(fb_fmt_time($t['created_at'])); ?></span>
<span class="dot">·</span>
<span><?php echo e($t['author_email']); ?></span>
<span class="dot">·</span>
<a href="admin.php?tab=thread&amp;id=<?php echo (int)$t['id']; ?>">进入管理</a>
<span><?php echo e(fb_fmt_time($t['created_at'])); ?></span>
</div>
<div class="row2" style="margin-top:14px">
</div>
<div class="prow-act">
<form method="post" action="admin.php?tab=pending">
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
<input type="hidden" name="id" value="<?php echo (int)$t['id']; ?>">
<div class="form-actions">
<button class="btn primary" type="submit" name="act" value="approve">通过并公开</button>
</div>
</form>
<form method="post" action="admin.php?tab=pending">
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
<input type="hidden" name="id" value="<?php echo (int)$t['id']; ?>">
<label class="field">
<span>关闭说明 <b class="req">*</b> <i class="hint">判定"不是问题"时填写,会随邮件发给提问人</i></span>
<textarea name="note" rows="4" maxlength="4000" placeholder="例如:该现象是服务器端的 CalDAV 限制,并非应用问题;建议改用…"></textarea>
</label>
<div class="form-actions">
<button class="btn" type="submit" name="act" value="close">关闭并邮件告知</button>
</div>
</form>
</div>
</div>
+38 -4
View File
@@ -189,10 +189,11 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.sw-badge-sm .sw-badge-nm{font-size:14px}
.sw-badge-md .sw-badge-nm{font-size:19px}
.sw-badge-lg .sw-badge-nm{font-size:22px; font-weight:700}
/* 顶栏主标题:比「选择软件」标签(13.5px)大两号 = 17px。
* 之前给到 26px 反而比内容区标题还小,主次颠倒;这里保持克制。 */
.sw-badge-md .sw-badge-nm{font-size:16px; font-weight:600}
/* 内容区:只是软件标识,字号刻意小于顶栏标题(17px),
* 避免「上面的字比下面的小」这种主次颠倒。 */
.sw-badge-lg .sw-badge-nm{font-size:15px; font-weight:600}
/* 顶栏主标题:比「选择软件」标签(13.5px)大两号 = 17px,且大于内容区的 15px */
.sw-badge-brand .sw-badge-nm{font-size:17px; font-weight:700; letter-spacing:.2px}
/* 页头副行:图标 + 软件名 */
.page-sw-id{margin:0; display:flex; align-items:center}
@@ -368,6 +369,37 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
.item-ex{color:var(--muted); font-size:14px; margin:8px 0 8px; word-break:break-word}
.item-meta{display:flex; flex-wrap:wrap; align-items:center; gap:6px; font-size:13px; color:var(--muted)}
.item-meta .who{color:#3a424d; font-weight:600}
/* ---------- 后台「待审核」列表 ----------
* 列表式:一屏能看多条,右侧固定一个「通过并公开」。
* 详情/关闭说明留在管理页里填,列表页不塞表单。 */
.plist{display:flex; flex-direction:column; gap:10px}
.prow{
display:flex; align-items:flex-start; gap:16px;
background:var(--card); border:1px solid var(--line);
border-radius:12px; padding:13px 16px;
}
.prow:hover{border-color:var(--line-strong)}
.prow-main{flex:1; min-width:0}
.prow-top{display:flex; align-items:center; flex-wrap:wrap; gap:8px}
.prow-title{
font-weight:600; font-size:15.5px; color:var(--fg);
flex:1; min-width:160px; word-break:break-word;
}
.prow-title:hover{color:var(--brand); text-decoration:underline}
.prow-ex{
color:var(--muted); font-size:13.5px; margin:6px 0;
word-break:break-word; display:-webkit-box; -webkit-line-clamp:2;
-webkit-box-orient:vertical; overflow:hidden;
}
.prow-meta{display:flex; flex-wrap:wrap; align-items:center; gap:6px; font-size:12.5px; color:var(--muted)}
.prow-meta .who{color:#3a424d; font-weight:600}
.prow-act{flex:none; padding-top:2px}
.prow-act .btn{white-space:nowrap}
@media (max-width:600px){
.prow{flex-direction:column; gap:10px}
.prow-act{align-self:flex-start}
}
/* 问题编号:便于按 id 逐条跟进与引用 */
.item-id{
flex:none; font-size:12.5px; font-weight:600; color:var(--muted);
@@ -383,6 +415,8 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
}
.badge.st{background:#f1f3f5; color:#5b636e}
.badge.st-open{background:var(--warn-soft); color:var(--warn)}
/* 「未公开」= 还没审过,用中性灰,不要和「已关闭」混淆 */
.badge.st-hidden{background:#eef0f3; color:#6b7684}
.badge.st-doing{background:var(--brand-soft); color:var(--brand)}
.badge.st-resolved{background:var(--ok-soft); color:var(--ok)}
.badge.st-closed{background:#f1f3f5; color:#8a929c}
+24 -6
View File
@@ -399,18 +399,36 @@ SQL
}
}
// 3) 状态:空表时灌 5 个(4 个原有 + 新增「不予考虑」)
// 3) 状态:空表时灌全部状态
if ((int)$pdo->query('SELECT COUNT(*) FROM statuses')->fetchColumn() === 0) {
$rows = [
['open', '待处理', '您提交的反馈已收到,我们正在处理中。', 0],
['doing', '处理中', '我们正在跟进这个问题,有进展会在这里同步。', 1],
['resolved', '已解决', '这个问题已经处理好了,欢迎回来验证一下。', 2],
['closed', '已关闭', '这个反馈已关闭,如有新情况可以重新提交。', 3],
['wontfix', '不予考虑', '经评估这个反馈暂不处理,感谢你的建议。', 4],
// key_name 名称 给用户看的说明(进邮件) 排序
['hidden', '未公开', '您的反馈已提交,正在等待管理员查看。', 0],
['open', '待处理', '您的反馈已公开,我们已收到,会尽快安排处理。', 1],
['doing', '处理中', '我们正在跟进这个问题,有进展会在这里同步。', 2],
['resolved', '已解决', '这个问题已经处理好了,欢迎回来验证一下。', 3],
['closed', '已关闭', '这个反馈已关闭,如有新情况可以重新提交。', 4],
['wontfix', '不予考虑', '经评估这个反馈暂不处理,感谢您的建议。', 5],
];
$st = $pdo->prepare('INSERT INTO statuses(key_name, name, user_note, sort_order, enabled)
VALUES(?,?,?,?,1)');
foreach ($rows as $r) { $st->execute($r); }
} else {
/* 老库补「未公开」状态。
* 2026-11-03 新增:用户提交后先落「未公开」,审核通过才转「待处理」——
* 「未公开」本身就是一种状态,不该只靠 threads.visible=0 这个附加字段隐式表达。 */
$hasHidden = (int)$pdo->query("SELECT COUNT(*) FROM statuses WHERE key_name = 'hidden'")->fetchColumn();
if ($hasHidden === 0) {
// 先把现有状态整体后移一位,给「未公开」腾出 0 号位
$pdo->exec('UPDATE statuses SET sort_order = sort_order + 1');
$pdo->prepare('INSERT INTO statuses(key_name, name, user_note, sort_order, enabled)
VALUES(?,?,?,?,1)')
->execute(['hidden', '未公开', '您的反馈已提交,正在等待管理员查看。', 0]);
/* 存量数据:visible=0 且状态还是「待处理」的,语义上就是「没公开」,
* 归到「未公开」;visible=1 的保持「待处理」不动。 */
$pdo->exec("UPDATE threads SET status = 'hidden' WHERE visible = 0 AND status = 'open'");
}
}
// 4) 回填老帖子:software_id / category_id(只填 NULL 的,重复执行无副作用)
+9
View File
@@ -786,6 +786,15 @@ if (!function_exists('fb_notify_status_meta')) {
*/
function fb_notify_status_meta($key) {
$map = [
/* 「未公开」:用户刚提交时就是这个状态。
* 一般不单独发信(提交回执已经说了「等待审核」),
* 但万一从别的状态退回来,也要有话可说,不能落到兜底文案。 */
'hidden' => [
'badge' => '未公开', 'color' => '#6b7684', 'tone' => 'muted',
'title' => '您的反馈已提交,正在等待审核',
'intro' => '管理员会尽快查看您的反馈,确认后才会公开显示。',
'tip' => '审核结果我们会通过邮件通知您,无需反复回来查看。',
],
'open' => [
'badge' => '待处理', 'color' => '#5c6572', 'tone' => 'muted',
'title' => '您的反馈已收到,正在等待处理',
+8 -2
View File
@@ -89,8 +89,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$atts = $vList;
} else {
$now = time();
// 审核:开启后新提交默认不公开(visible=0),管理员通过后才会出现在列表
/* 审核:开启后新提交默认不公开(visible=0),管理员通过后才出现在列表。
*
* 🔴 状态与可见性必须同时设,两者是同一件事的两个侧面:
* 未公开 → status='hidden'(未公开),公开 → status='open'(待处理)。
* 以前一律写 'open',导致后台「待审核」里全是「待处理」,
* 状态根本反映不出"还没公开"这个事实。 */
$visible = fb_moderation_on() ? 0 : 1;
$initStatus = $visible === 1 ? 'open' : 'hidden';
$ins = $pdo->prepare('INSERT INTO threads(title, body, category, category_id, software_id, author_name, author_email, author_phone, status, visible, reply_count, created_at, updated_at, ip_hash) VALUES(?,?,?,?,?,?,?,?,?,?,0,?,?,?)');
$ins->execute([
$form['title'], $form['body'],
@@ -99,7 +105,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$swId > 0 ? $swId : null,
$form['name'], $form['email'],
$form['phone'] !== '' ? $form['phone'] : null,
'open', $visible, $now, $now, fb_ip_hash(),
$initStatus, $visible, $now, $now, fb_ip_hash(),
]);
$newId = (int)$pdo->lastInsertId();
fb_attach_save($pdo, $newId, null, $vList);