多软件区分:邮件按软件换色+图标+软件名;补提交回执邮件

用户要求:多软件并存时,用「颜色 + 图标 + 名字」三重区分,
邮件也要跟着所属软件变,一眼能看出是哪款软件的来信。

1. software 表加 icon / download_url 字段(幂等迁移)
   - icon 存单个字符(emoji / 汉字 / 字母),不存图片路径:
     前端与邮件用同一个字符渲染,天然一致,不用再管一套上传与权限
   - 没配 icon 时取名首字兜底,任何地方都不出现空图标
   - 后台软件编辑加图标快选(12 个常用 emoji)

2. 邮件配色改为跟随软件(fb_mail_brand 取 fb_software_colors)
   - 头部色条、按钮、引用块色条、徽标全部跟着软件走
   - 邮件头部改为「图标色块 + 软件名」,下面再小字标站点名
   - 主题行统一加【软件名】前缀,不点开就能分辨

3. 新增 receipt 事件:提交后立即给提问人发「已收到、待审核、请耐心等待」
   - 独立开关 notify.receipt_enabled(与「回复通知」是两件事)
   - 新增 7 处调用前的静默 return 也一并写日志

4. reply 邮件补上下文:原提问摘要 + 本次回复,像博客评论通知
5. status=resolved 文案改为「已完成并上线」,配了下载地址时给下载按钮

验证:多软件配色/图标/前缀全部通过(含反向验证),XSS 转义通过,
空图标兜底通过;页面顶栏、页头、选择器图标渲染正确。
This commit is contained in:
2026-11-03 08:00:50 +08:00
parent e43f2ce67a
commit 738c6c09ff
12 changed files with 451 additions and 52 deletions
+27 -5
View File
@@ -29,8 +29,10 @@ function fb_admin_taxonomy_action($pdo, $act, $post) {
if (fb_software($slug)) { return ['英文标识「' . $slug . '」已被占用,换一个。', 'error']; }
$sort = (int)$pdo->query('SELECT COALESCE(MAX(sort_order),0)+1 FROM software')->fetchColumn();
$color = fb_color_norm(isset($post['color']) ? $post['color'] : '');
$pdo->prepare('INSERT INTO software(slug,name,note,color,sort_order,enabled,created_at) VALUES(?,?,?,?,?,1,?)')
->execute([$slug, $name, $note !== '' ? $note : null, $color, $sort, $now]);
$icon = fb_sw_icon_norm(isset($post['icon']) ? $post['icon'] : '');
$dl = fb_sw_url_norm(isset($post['download_url']) ? $post['download_url'] : '');
$pdo->prepare('INSERT INTO software(slug,name,note,color,icon,download_url,sort_order,enabled,created_at) VALUES(?,?,?,?,?,?,?,1,?)')
->execute([$slug, $name, $note !== '' ? $note : null, $color, $icon, $dl, $sort, $now]);
$newId = (int)$pdo->lastInsertId();
// 新软件默认给一组常见分类,省得空着没法发帖
$i = 0;
@@ -54,8 +56,10 @@ function fb_admin_taxonomy_action($pdo, $act, $post) {
$other = fb_software($slug);
if ($other && (int)$other['id'] !== $id) { return ['英文标识「' . $slug . '」已被其他软件占用。', 'error']; }
$color = fb_color_norm(isset($post['color']) ? $post['color'] : '');
$pdo->prepare('UPDATE software SET name=?, slug=?, note=?, color=?, sort_order=? WHERE id=?')
->execute([$name, $slug, $note !== '' ? $note : null, $color,
$icon = fb_sw_icon_norm(isset($post['icon']) ? $post['icon'] : '');
$dl = fb_sw_url_norm(isset($post['download_url']) ? $post['download_url'] : '');
$pdo->prepare('UPDATE software SET name=?, slug=?, note=?, color=?, icon=?, download_url=?, sort_order=? WHERE id=?')
->execute([$name, $slug, $note !== '' ? $note : null, $color, $icon, $dl,
(int)(isset($post['sort_order']) ? $post['sort_order'] : $sw['sort_order']), $id]);
return ['软件「' . $name . '」已保存。', 'ok'];
}
@@ -264,7 +268,8 @@ function fb_admin_taxonomy_page($pdo, $editSw, $editCat) {
echo ' <div class="tax-main">';
list($swC, $swCd, $swCs) = fb_software_colors($sw);
echo ' <span class="sw-dot" style="background:' . e($swC) . '" title="' . e($swC) . '"></span>';
echo ' <a class="tax-name" href="admin.php?tab=taxonomy&sw=' . $sid . '">' . e((string)$sw['name']) . '</a>';
echo ' <a class="tax-name" href="admin.php?tab=taxonomy&sw=' . $sid . '">'
. fb_software_badge($sw, 'sm') . '</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>';
@@ -310,6 +315,23 @@ function fb_admin_taxonomy_page($pdo, $editSw, $editCat) {
. '<input type="text" name="note" maxlength="120" value="'
. e($esw ? (string)($esw['note'] === null ? '' : $esw['note']) : '') . '"></label>';
// 图标:一个字符即可(emoji / 汉字 / 字母),配合主题色一起区分软件
$curIcon = $esw ? (string)(isset($esw['icon']) ? $esw['icon'] : '') : '';
$iconSug = ['📅', '🔐', '🔑', '📝', '🎵', '📷', '🌤', '🧭', '📦', '⚙️', '🗂', '💬'];
echo ' <label class="field"><span>图标<i class="hint">一个字符:emoji 或汉字/字母。列表、页头与邮件里都会带上它</i></span>'
. '<input type="text" name="icon" maxlength="8" value="' . e($curIcon) . '" placeholder="例如 📅">'
. '<span class="icon-presets">';
foreach ($iconSug as $ic) {
echo '<button type="button" class="icon-preset" data-icon="' . e($ic) . '">' . e($ic) . '</button>';
}
echo '</span></label>';
// 下载地址:状态改成「已解决」时,邮件里会给用户一个去下新版的按钮
echo ' <label class="field"><span>下载地址<i class="hint">可留空。填了之后「已解决」的通知邮件里会多一个「下载最新版」按钮</i></span>'
. '<input type="url" name="download_url" maxlength="300" value="'
. e($esw ? (string)(isset($esw['download_url']) ? $esw['download_url'] : '') : '')
. '" placeholder="https://..."></label>';
// 主题色:前台按软件换色,用短链接进不同软件一眼就能区分
$curColor = $esw ? fb_color_norm((string)$esw['color']) : '';
$sugColor = $curColor !== '' ? $curColor : fb_color_default_for($esw ? (int)$esw['id'] : 0);