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

用户要求:
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
+64 -49
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; ?>
<?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>
<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>
<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>
<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($t['author_email']); ?></span>
<span class="dot">·</span>
<span><?php echo e(fb_fmt_time($t['created_at'])); ?></span>
</div>
</div>
<div class="item-ex" style="white-space:normal"><?php echo nl2br(e($t['body'])); ?></div>
<div class="item-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>
</div>
<div class="row2" style="margin-top:14px">
<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>
<button class="btn primary" type="submit" name="act" value="approve">通过并公开</button>
</form>
</div>
</div>