Files
FeedLite/new.php
T
laoyang bec5b09bd6 提交成功后改为原地显示回执页(不再跳转);修提交页右侧缺块
用户两个问题:

一、「我要反馈」页面比正常页面窄很多、右侧缺一块
  根因:.narrow 只有 max-width:640px,**没有居中**(缺 margin auto)。
  在 880px 的内容区里左对齐,右侧空出 240px 白,看起来像页面被抠掉一块。
  改法:max-width 放到 720px + margin-left/right auto 居中。
  实测:容器 880px,表单 720px,左右间隙各 80px,完全对称。

二、提交完直接跳转太突兀
  用户:「应该给它一个提示,说反馈我们已经收到…里面有一个标志码,
        是你以后跟踪它的唯一凭据…下面再列出其他功能(返回首页、继续提交、
        其他软件链接、开源链接),不能直接跳转,这给人感觉很突兀。」
  改法:不再 302 跳转,改为原地渲染成功页 fb_render_submitted():
    ✓ 您的反馈我们已经收到
    → 详细信息已发送到 xxx@xxx(并提示看垃圾邮件)
    → 醒目的标志码区块(等宽字 + 虚线框 + 可整体选中)
      并说明:凭它可查看进度,未通过审核/已关闭也能看;每封邮件也会带
    → 勿外传提醒(等同于身份凭证)
    → 四个去处:查看这条反馈 / 继续提交 / 看该软件其他反馈 / 返回首页
    → 其他软件入口(排除当前软件,避免重复)
    → 开源仓库链接
  为什么要把 token 当场亮出来而不只是说已发到邮箱:
    邮件可能进垃圾箱或被误删,码亮出来用户至少能复制存档;
    这是唯一凭据,丢了找不回来(我们不做登录,不认人)。

验证(只在 feedlitetest 软件里做,不碰其他正式软件):
  真实提交一条 → 返回 200 且 URL 仍在 new.php(没跳走);
  成功页含回执/邮箱/唯一凭据/勿外传/四个按钮/其他软件/开源链接;
  提取到 32 位 hex 标志码,带它访问详情页 200,不带则 404。
  测试数据已清理,其他软件数据一条未动。
2026-11-03 13:19:14 +08:00

375 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 发布新问题
*/
require __DIR__ . '/lib/bootstrap.php';
$pdo = fb_db();
$err = '';
$atts = []; // 校验失败时保留已上传的附件
/* ---- 软件:URL 带 sw 就用它 ----
* 🔴 不再「没带 sw 就默认落到第一个软件」:
* 首页现在是介绍页,直接访问 new.php 没有软件上下文,
* 默默替他选一个软件会让反馈进错地方、发错通知邮件。
* 没有软件时给一句引导,让用户先选软件。 */
$swList = fb_softwares(true);
$sw = null;
$swKey = isset($_GET['sw']) ? (string)$_GET['sw'] : '';
if ($swKey !== '') {
$sw = fb_software($swKey);
if (!$sw || (int)$sw['enabled'] !== 1) { $sw = null; }
}
if ($sw === null) {
fb_layout_head('请先选择软件', ['robots' => 'noindex,follow']);
?>
<div class="empty">
<h2 style="margin:0 0 12px">请先选择要给哪个软件提反馈</h2>
<p class="muted" style="margin:0 0 16px">不同软件的问题列表是分开的,请先选一个。</p>
<div class="home-sw-list" style="max-width:420px;margin:0 auto 18px">
<?php foreach ($swList as $s): ?>
<?php
list($c) = fb_software_colors($s);
if ($c === '') { $c = '#0057B8'; }
?>
<a class="home-sw" href="new.php?sw=<?php echo e(urlencode((string)$s['slug'])); ?>"
style="--swc:<?php echo e($c); ?>">
<span class="home-sw-nm"><?php echo e((string)$s['name']); ?></span>
<span class="home-sw-go">去反馈 →</span>
</a>
<?php endforeach; ?>
</div>
<p><a class="btn" href="index.php">返回首页</a></p>
</div>
<?php
fb_layout_foot();
exit;
}
$swId = (int)$sw['id'];
$cats = fb_categories($swId);
$catKeys = [];
foreach ($cats as $c) { $catKeys[] = (int)$c['id']; }
$form = [
'category_id' => $catKeys ? (string)$catKeys[0] : '',
'title' => '',
'body' => '',
'name' => '',
'email' => '',
'phone' => '',
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
foreach (['category_id', 'title', 'body', 'name', 'email', 'phone'] as $k) {
$form[$k] = trim(isset($_POST[$k]) ? (string)$_POST[$k] : '');
}
// POST 里带了 software_id 时以它为准(切换软件后表单会跟着变)
if (isset($_POST['software_id']) && ctype_digit((string)$_POST['software_id'])) {
$swPost = fb_software((int)$_POST['software_id']);
if ($swPost && (int)$swPost['enabled'] === 1) {
$sw = $swPost;
$swId = (int)$swPost['id'];
}
}
$capInput = isset($_POST['captcha']) ? (string)$_POST['captcha'] : '';
$hp = trim(isset($_POST['website']) ? (string)$_POST['website'] : '');
$t0 = isset($_POST['t0']) ? (int)$_POST['t0'] : 0;
list($attOk, $attList, $attErr) = fb_attach_parse(isset($_POST['attachments']) ? $_POST['attachments'] : '');
// 分类必须属于这个软件,否则视为未选
$curCatId = ctype_digit($form['category_id']) ? (int)$form['category_id'] : 0;
$curCat = $curCatId ? fb_category($curCatId) : null;
if (!$curCat || (int)$curCat['software_id'] !== $swId) { $curCat = null; $form['category_id'] = ''; }
if (!fb_csrf_ok(isset($_POST['csrf']) ? $_POST['csrf'] : '')) {
$err = '会话已过期,请重新提交。';
} elseif ($hp !== '') {
$err = '提交未通过校验,请重试。';
} elseif ($t0 > 0 && (time() - $t0) < max(0, (int)fb_cfg('post_min_seconds', 3))) {
$err = '提交过于频繁,请稍候再试。';
} elseif (fb_len($form['title']) < 4 || fb_len($form['title']) > 80) {
$err = '标题请填写 4~80 个字符。';
} elseif (!fb_valid_body($form['body'])) {
$err = '问题描述请填写 2~5000 个字符。';
} elseif (!fb_valid_name($form['name'])) {
$err = '请填写昵称(1~24 个字符)。';
} elseif (!filter_var($form['email'], FILTER_VALIDATE_EMAIL)) {
$err = '请填写有效的邮箱地址(用于接收回复通知)。';
} elseif ($form['phone'] !== '' && !preg_match('/^[0-9+\-\s()]{5,30}$/', $form['phone'])) {
$err = '手机号格式不正确(可留空)。';
} elseif (!$attOk) {
$err = $attErr;
} elseif (!fb_rate_check('thread', (int)fb_cfg('rate_limit.thread_per_hour', 5))) {
$err = '发帖过于频繁,请稍后再试。';
} else {
// 附件回源复核 + 全站总量检查(放在人机验证之前,避免验证码被无效消耗)
list($vOk, $vList, $vErr) = fb_attach_verify($attList);
if (!$vOk) {
$err = $vErr;
$atts = $attList;
} elseif (!fb_attach_cap_ok($pdo, $vList)) {
$err = '附件存储空间已满,请减少附件后重试。';
$atts = $vList;
} elseif (!fb_captcha_check($capInput)) {
$err = '人机验证未通过,请重新作答。';
$atts = $vList;
} else {
$now = time();
/* 审核:开启后新提交默认不公开(visible=0),管理员通过后才出现在列表。
*
* 🔴 状态与可见性必须同时设,两者是同一件事的两个侧面:
* 未公开 → status='hidden'(未公开),公开 → status='open'(待处理)。
* 以前一律写 'open',导致后台「待审核」里全是「待处理」,
* 状态根本反映不出"还没公开"这个事实。 */
$visible = fb_moderation_on() ? 0 : 1;
$initStatus = $visible === 1 ? 'open' : 'hidden';
/* track_token:提交人的私人追踪钥匙,只发到他的邮箱。
* 有了它,即使这条还没审核通过,他也能凭链接看到进度。 */
$trackToken = fb_track_new_token();
$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, track_token) VALUES(?,?,?,?,?,?,?,?,?,?,0,?,?,?,?)');
$ins->execute([
$form['title'], $form['body'],
$curCat ? (string)$curCat['slug'] : 'other', // 老列留个值,便于回退与检索
$curCat ? (int)$curCat['id'] : null,
$swId > 0 ? $swId : null,
$form['name'], $form['email'],
$form['phone'] !== '' ? $form['phone'] : null,
$initStatus, $visible, $now, $now, fb_ip_hash(), $trackToken,
]);
$newId = (int)$pdo->lastInsertId();
fb_attach_save($pdo, $newId, null, $vList);
fb_rate_hit('thread');
// 通知管理员「有新反馈」(工单 #2)
// 此前 notify.admin_new_thread 这个设置项是**假的** ——
// 白名单里有、后台有勾选框,但没有任何代码实现它。
// 用户反馈「提交了反馈却收不到邮件」正是这个原因。
try {
$stNt = $pdo->prepare('SELECT * FROM threads WHERE id = ?');
$stNt->execute([$newId]);
$newThread = $stNt->fetch(PDO::FETCH_ASSOC);
if ($newThread) {
$swName = '';
if (!empty($swId)) {
$swRow = fb_software((int)$swId);
if ($swRow) { $swName = (string)$swRow['name']; }
}
/* ① 先给提交人发「已收到」回执 —— 让他知道东西送达了、正在等审核。
* 这一步是用户最在意的:提交完页面跳走了,没有回执就等于石沉大海。
* 放在管理员通知之前,是因为回执对人的价值更高,且失败互不影响。 */
fb_notify_event($pdo, 'receipt', $newThread, ['software_name' => $swName]);
/* ② 再通知管理员「有新反馈」 */
fb_notify_event($pdo, 'new', $newThread, ['software_name' => $swName]);
}
} catch (Throwable $e) {
// 发信失败绝不能让用户提交失败 —— 反馈已经存库了
}
/* 提交完成 → 原地渲染「已收到」页面,不跳转。
*
* 用户 2026-11-03:「提交完怎么直接跳转到首页去了?应该给它一个提示,
* 说你反馈我们已经收到…里面有一个标志码,是你以后跟踪它的唯一凭据…
* 然后下面再列出来其他功能(返回首页、继续提交、其他软件链接、开源链接),
* 不能直接跳转,这给人感觉很突兀。」
*
* 🔴 关键取舍:**不跳转、不用 302**。跳转会让用户失去"我刚提交成功"这一帧,
* 而且详情页此刻还是「未公开」状态,跳过去反而像出错了。
* 这里直接把成功页渲染出来,把 token 明明白白摆在眼前。 */
fb_render_submitted($sw, $newId, $trackToken, $form, $swList);
exit;
}
}
}
fb_layout_head('我要反馈', ['robots' => 'noindex,follow', 'software' => $sw]);
?>
<div class="crumbs"><a href="<?php echo e(fb_software_list_url($sw)); ?>">← 返回问题列表</a></div>
<section class="narrow">
<h1>我要反馈</h1>
<?php if ($sw): ?>
<div class="sw-context"><?php echo fb_software_badge($sw, 'md'); ?>
<span class="muted small">您正在为「<?php echo e(fb_software_site_title($sw)); ?>」提交反馈</span></div>
<?php endif; ?>
<p class="muted">请描述您遇到的问题或想要的功能。提交后会先进入审核,确认为有效问题后才会公开显示;<b>若经确认不是应用的问题,我们会把结论与说明通过邮件发送给您</b>(请填写常用邮箱)。</p>
<?php fb_flash_box($err, 'error'); ?>
<form method="post" action="new.php" class="card-form">
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
<input type="hidden" name="t0" value="<?php echo time(); ?>">
<div class="hp-field" aria-hidden="true">
<label>请勿填写此栏<input type="text" name="website" tabindex="-1" autocomplete="off"></label>
</div>
<?php if ($sw): ?>
<input type="hidden" name="software_id" value="<?php echo (int)$sw['id']; ?>">
<?php /* 软件切换统一收到顶栏的汉堡菜单里,表单内不再平铺一排 chip ——
免得同一个选择在两个地方出现,用户不知道该点哪个。 */ ?>
<?php endif; ?>
<?php if ($cats): ?>
<label class="field">
<span>问题分类 <b class="req">*</b></span>
<select name="category_id" required>
<?php foreach ($cats as $c): ?>
<option value="<?php echo (int)$c['id']; ?>" <?php echo $form['category_id'] === (string)$c['id'] ? 'selected' : ''; ?>><?php echo e((string)$c['name']); ?></option>
<?php endforeach; ?>
</select>
</label>
<?php endif; ?>
<label class="field">
<span>标题 <b class="req">*</b></span>
<input type="text" name="title" maxlength="80" value="<?php echo e($form['title']); ?>" placeholder="一句话概括您的问题" required>
</label>
<label class="field">
<span>详细描述 <b class="req">*</b></span>
<textarea name="body" rows="9" maxlength="5000" placeholder="请尽量说明:您做了什么、期望的结果、实际的结果;如是同步问题,可说明使用的邮箱 / 服务器类型。" required><?php echo e($form['body']); ?></textarea>
</label>
<div class="row2">
<label class="field">
<span>昵称 <b class="req">*</b></span>
<input type="text" name="name" maxlength="24" value="<?php echo e($form['name']); ?>" placeholder="怎么称呼您" required>
</label>
<label class="field">
<span>邮箱 <b class="req">*</b><i class="hint">仅用于接收回复通知,不会公开</i></span>
<input type="email" name="email" maxlength="120" value="<?php echo e($form['email']); ?>" placeholder="you@example.com" required>
</label>
</div>
<label class="field">
<span>手机号 <i class="hint">选填;仅用于必要时联系,不会公开</i></span>
<input type="tel" name="phone" maxlength="30" value="<?php echo e($form['phone']); ?>" placeholder="选填">
</label>
<?php fb_uploaders($atts); ?>
<div class="captcha">
<label class="field">
<span>人机验证 <b class="req">*</b> <i class="hint">防机器人,随手答一下</i></span>
<div class="captcha-line">
<span class="c-q"><?php echo e(fb_captcha_question()); ?></span>
<input type="text" name="captcha" value="" placeholder="答案" autocomplete="off" required>
<a class="reload" href="captcha.php" data-captcha-refresh>换一题</a>
</div>
</label>
</div>
<div class="form-actions">
<button class="btn primary" type="submit">提交反馈</button>
</div>
</form>
<p class="muted small">提示:本页面不会公开展示您的邮箱与手机号。请勿在内容中填写密码等敏感信息。可上传截图或录屏帮助说明问题;文件会直接传到对象存储,不占用本站服务器空间。</p>
</section>
<?php
fb_layout_foot();
/* ==================================================================
* 提交成功页
* ------------------------------------------------------------------
* 用户 2026-11-03 要求:提交完不要直接跳走,要给一个明确回执。
* 页面结构(按他的话组织):
* ① 你的反馈我们已经收到
* ② 详细信息已发到你邮箱
* ③ 下面这个标志码是**你以后跟踪它的唯一凭据**,请保存好
* ④ 我们会尽快处理,你可以凭它持续关注进展
* ⑤ 再往下给出路:查看这条反馈 / 继续提交 / 返回列表 / 其他软件 / 开源仓库
*
* 为什么要显式展示 token(而不只是说"已发到邮箱"):
* 邮件可能进垃圾箱、也可能被误删。把码当场亮出来,用户至少能复制存档,
* 不至于彻底失去跟踪能力。这是"唯一凭据",丢了就找不回来(我们不认人)。
* ================================================================== */
function fb_render_submitted($sw, $newId, $token, array $form, array $swList) {
$swName = $sw ? (string)$sw['name'] : '';
fb_layout_head('反馈已提交', ['robots' => 'noindex,nofollow', 'software' => $sw]);
?>
<section class="narrow">
<div class="submitted">
<div class="submitted-ok" aria-hidden="true">✓</div>
<h1>您的反馈我们已经收到</h1>
<p class="submitted-lead">
我们已经把这条反馈存下来了,会尽快安排查看与处理。
<?php if (trim((string)$form['email']) !== ''): ?>
这条反馈的详细信息也已发送到 <b><?php echo e((string)$form['email']); ?></b>,
请留意收件箱(如果没看到,可以看一下垃圾邮件)。
<?php endif; ?>
</p>
<div class="token-box">
<div class="token-box-hd">您跟踪这条反馈的唯一凭据</div>
<div class="token-code" id="trackToken"><?php echo e((string)$token); ?></div>
<p class="token-box-tip">
请把这段标志码保存好。今后凭它就能查看这条反馈的处理进度 ——
即使它还没有通过审核、甚至最终被关闭,也依然能看。
我们也会在每封通知邮件里带上它,所以务必留意邮箱。
</p>
<p class="token-box-warn">
这段码等同于您的身份凭证:谁拿到它,谁就能查看这条反馈。
请不要发给别人,也不要发到公开的地方。
</p>
</div>
<p class="submitted-next">接下来您可以:</p>
<div class="submitted-actions">
<a class="btn primary" href="thread.php?id=<?php echo (int)$newId; ?>&amp;t=<?php echo e(rawurlencode((string)$token)); ?>">
查看这条反馈
</a>
<a class="btn" href="new.php<?php echo $sw ? '?sw=' . e(urlencode((string)$sw['slug'])) : ''; ?>">继续提交一条</a>
<a class="btn" href="<?php echo e(fb_software_list_url($sw)); ?>">
<?php echo $swName !== '' ? '看看「' . e($swName) . '」的其他反馈' : '返回问题列表'; ?>
</a>
<a class="btn" href="index.php">返回首页</a>
</div>
<?php
/* 其他软件的入口:只列除了当前这个之外的,避免重复 */
$others = [];
foreach ($swList as $s) {
if ($sw && (int)$s['id'] === (int)$sw['id']) { continue; }
$others[] = $s;
}
if ($others):
?>
<div class="submitted-others">
<div class="submitted-others-hd">也可以去其他软件反馈</div>
<div class="submitted-others-list">
<?php foreach ($others as $s): ?>
<?php
list($c) = fb_software_colors($s);
if ($c === '') { $c = '#0057B8'; }
?>
<a class="submitted-sw" href="new.php?sw=<?php echo e(urlencode((string)$s['slug'])); ?>"
style="--swc:<?php echo e($c); ?>">
<?php echo fb_software_chip($s); ?>
<span class="submitted-sw-go">去反馈 →</span>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php
$creditUrl = (string)fb_cfg('footer.credit_url', fb_credit_url_default());
if ($creditUrl !== ''):
?>
<p class="submitted-credit">
这套反馈系统是开源的:<a href="<?php echo e($creditUrl); ?>" target="_blank" rel="noopener"><?php echo e($creditUrl); ?></a>
</p>
<?php endif; ?>
</div>
</section>
<?php
fb_layout_foot();
}