用户三个问题:
一、软件名/分类/状态三个标签挤在标题前面,把标题推到右边很难看
列表页:item-top 里 badges 在前、标题在后 → 标题被挤到右边
详情页:同一个毛病
改法(两处一致):
行1 标题(顶头、独占一行)
行2 标签(分类/状态;详情页还有软件名)
行3 摘要 / 作者信息
列表页新增 .item-head + .item-tags;详情页新增 .subject-head + .subject-tags。
⚠️ .item-top 保留 —— admin.php 有三处内部列表还在用,删了后台会塌。
二、顶栏「XX 反馈系统」点完跳首页
用户:「点完之后应该还是进入这个反馈系统,而不是进到首页。首页没有太大作用,
只是演示说明用的。正常使用都是每个软件独立,要让用户觉得
这套反馈系统就是专门为这个软件做的。」
改法:brand-mini 的 href 从 "./" 改为 fb_software_list_url()。
三、软件内部不该引导去其他软件
用户:「在一个反馈系统里面,我要反馈、问题列表、左上角那个链接,
全部都是这个软件相关的内容,跟其他软件没关系。」
- 提交成功页删掉整个「也可以去其他软件反馈」区块及其 CSS
- 删掉成功页的「返回首页」按钮(已有「看该软件其他反馈」,重复且无意义)
- 跨软件只剩顶栏三横线菜单一个入口
验证:列表页/详情页 DOM 顺序均为 标题→标签→作者(实测 item-title 58 < item-tags 171;
subject-title 49 < subject-tags 151 < item-meta 592);
浏览器实测标题 top=282 标签 top=315(列表)、155→194(详情),标签确实在下方;
标题左边缘距卡片边 18/23px,就是卡片自身 padding,没有额外缩进;
顶栏标题 href=index.php?sw=synccalendar,不再指向根路径;全站 8 个路由 200。
362 lines
18 KiB
PHP
362 lines
18 KiB
PHP
<?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);
|
||
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) {
|
||
$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; ?>&t=<?php echo e(rawurlencode((string)$token)); ?>">
|
||
查看这条反馈
|
||
</a>
|
||
<a class="btn" href="new.php<?php echo $sw ? '?sw=' . e(urlencode((string)$sw['slug'])) : ''; ?>">继续提交一条</a>
|
||
<?php /* 不再放「返回首页」—— 在某个软件的反馈系统里,首页(全局介绍页)
|
||
对用户没有意义。要回就回这个软件的问题列表。
|
||
想看别的软件,只有右上角三横线一个入口。 */ ?>
|
||
<a class="btn" href="<?php echo e(fb_software_list_url($sw)); ?>">
|
||
<?php echo $swName !== '' ? '看看「' . e($swName) . '」的其他反馈' : '返回问题列表'; ?>
|
||
</a>
|
||
</div>
|
||
|
||
<?php
|
||
/* ⚠️ 这里原来有一个「也可以去其他软件反馈」的列表,已删除。
|
||
*
|
||
* 用户 2026-11-03:「在某个软件的反馈系统里面,什么我要反馈、问题列表、
|
||
* 还有左上角的那个链接,全部都是这个软件相关的内容,跟其他软件没关系 ——
|
||
* 相对来讲它是独立的。他如果想进别的软件,只有右上角三个横线点开。」
|
||
*
|
||
* 所以在软件内部(列表 / 详情 / 提交页 / 成功页)都不引导用户去别的软件。
|
||
* 唯一的跨软件入口就是顶栏那个三横线菜单。
|
||
*/
|
||
?>
|
||
|
||
<?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();
|
||
}
|