Compare commits
2
Commits
8a850ca0e5
...
738c6c09ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
738c6c09ff | ||
|
|
e43f2ce67a |
@@ -280,21 +280,41 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) {
|
||||
$msg = '#' . $id . ' 已删除。';
|
||||
|
||||
} elseif ($act === 'mail_test') {
|
||||
// 发送测试邮件:只验证 SMTP 通不通,不改任何配置
|
||||
// 发送测试邮件:只验证 SMTP 通不通,不改任何配置。
|
||||
// 可选 sw=<软件id>:按那个软件的样子渲染(配色 / 图标 / 软件名),
|
||||
// 便于在后台直接预览「不同软件的邮件长什么样」。
|
||||
$to = trim((string)(isset($_POST['to']) ? $_POST['to'] : ''));
|
||||
if ($to === '') { $to = trim((string)fb_cfg('admin_email', '')); }
|
||||
if ($to === '') { $to = trim((string)fb_cfg('smtp.user', '')); }
|
||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
$msg = '请填写有效的收件邮箱。';
|
||||
$msgType = 'error';
|
||||
} else {
|
||||
$sc = (array)fb_cfg('smtp', []);
|
||||
$site = (string)fb_cfg('site_name', '轻反馈');
|
||||
$body = '<p>这是一封来自 <b>' . htmlspecialchars($site, ENT_QUOTES, 'UTF-8')
|
||||
. '</b> 的测试邮件。</p>'
|
||||
. '<p>如果你收到了它,说明 SMTP 配置正确,回复通知可以正常发出。</p>'
|
||||
. '<p style="color:#888;font-size:12px">发送时间:'
|
||||
. date('Y-m-d H:i:s') . '</p>';
|
||||
list($ok, $err) = fb_smtp_send($sc, $to, '【' . $site . '】邮件测试', $body);
|
||||
|
||||
// 选中的软件(没选就用默认软件,保证测试邮件也带软件标识)
|
||||
$swId = (int)(isset($_POST['sw']) ? $_POST['sw'] : 0);
|
||||
$swRow = $swId > 0 ? fb_software($swId) : fb_default_software();
|
||||
|
||||
// 用真实的回执模板发,比一句「测试邮件」更能看出实际效果
|
||||
$fake = [
|
||||
'id' => 0,
|
||||
'software_id' => $swRow ? (int)$swRow['id'] : 0,
|
||||
'title' => '这是一封测试邮件,用来检查排版与配色',
|
||||
'body' => "这是测试用的反馈正文。\n如果你收到这封信,说明 SMTP 配置正确,配色与图标也应当对应「"
|
||||
. ($swRow ? (string)$swRow['name'] : '默认软件') . "」。\n\n发送时间:" . date('Y-m-d H:i:s'),
|
||||
'author_name' => '测试',
|
||||
'author_email' => $to,
|
||||
'author_phone' => '',
|
||||
'status' => 'open',
|
||||
'visible' => 1,
|
||||
];
|
||||
list($subject, $html) = fb_notify_render('receipt', $fake,
|
||||
['software' => $swRow], $to);
|
||||
$subject = '【测试】' . $subject;
|
||||
|
||||
list($ok, $err) = fb_smtp_send($sc, $to, $subject, $html);
|
||||
if ($ok) {
|
||||
$msg = '测试邮件已发出,请查收 ' . $to . '(也看一下垃圾箱)。';
|
||||
$msgType = 'ok';
|
||||
@@ -574,6 +594,7 @@ function fb_admin_save_settings() {
|
||||
$save('notify.enabled', (int)(bool)$post('notify.enabled'));
|
||||
$save('notify.on_every_reply', (int)(bool)$post('notify.on_every_reply'));
|
||||
$save('notify.admin_new_thread', (int)(bool)$post('notify.admin_new_thread'));
|
||||
$save('notify.receipt_enabled', (int)(bool)$post('notify.receipt_enabled'));
|
||||
|
||||
// 冷却秒数留空 = 用默认 300(config 里的出厂值)。注意 0 是合法值(表示不限制),
|
||||
// 所以要用「空串」而不是「0」来区分"没填"和"填了 0"
|
||||
@@ -1317,17 +1338,38 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
||||
<div class="mail-test">
|
||||
<span class="muted" style="font-size:13.5px">配好后先发一封测试邮件确认能通:</span>
|
||||
<input type="email" id="mailTestTo" placeholder="收件邮箱,默认发给自己"
|
||||
value="<?php echo e((string)fb_cfg('admin_email', '')); ?>">
|
||||
value="<?php echo e((string)fb_cfg('admin_email', '') !== '' ? (string)fb_cfg('admin_email', '') : (string)fb_cfg('smtp.user', '')); ?>">
|
||||
<select id="mailTestSw" title="按哪个软件的样子发送">
|
||||
<?php foreach (fb_softwares(true) as $swOne): ?>
|
||||
<option value="<?php echo (int)$swOne['id']; ?>"><?php echo e((string)$swOne['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="button" class="btn" id="mailTestBtn" data-csrf="<?php echo e(fb_csrf_token()); ?>">发送测试邮件</button>
|
||||
<span id="mailTestOut" class="mail-test-out"></span>
|
||||
</div>
|
||||
<p class="hint" style="margin:6px 0 0">测试邮件用的是「提交回执」模板,会带上所选软件的图标与配色 —— 可以直接看出不同软件的邮件长什么样。</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3 style="margin:20px 0 8px;font-size:15px">通知规则</h3>
|
||||
<label class="check"><input type="checkbox" name="notify_receipt_enabled" value="1"<?php echo fb_cfg('notify.receipt_enabled', true) ? ' checked' : ''; ?>> 提交后立即给提交人发「已收到」回执
|
||||
<span class="hint" style="display:block;margin-left:24px">告诉对方反馈已送达、正在等审核,请耐心等待。提交完就让用户干等着是最容易丢信任的环节。</span></label>
|
||||
<label class="check"><input type="checkbox" name="notify_enabled" value="1"<?php echo fb_cfg('notify.enabled', true) ? ' checked' : ''; ?>> 有人回复时通知提交人</label>
|
||||
<label class="check"><input type="checkbox" name="notify_on_every_reply" value="1"<?php echo fb_cfg('notify.on_every_reply', false) ? ' checked' : ''; ?>> 每次回复都通知(默认只在官方回复时通知,避免刷屏)</label>
|
||||
<label class="check"><input type="checkbox" name="notify_admin_new_thread" value="1"<?php echo fb_cfg('notify.admin_new_thread', false) ? ' checked' : ''; ?>> 有新反馈时通知管理员
|
||||
<span class="hint" style="display:block;margin-left:24px">发到「站点信息」里填的官方邮箱。</span></label>
|
||||
<?php
|
||||
// 勾了「有新反馈时通知管理员」却没填官方邮箱 —— 这条通知会发不出去。
|
||||
// 之前这个组合完全是静默失效的,用户在后台看不出任何异常。
|
||||
$adminMailNow = trim((string)fb_cfg('admin_email', ''));
|
||||
$smtpUserNow = trim((string)fb_cfg('smtp.user', ''));
|
||||
$newNotifyOn = (bool)fb_cfg('notify.admin_new_thread', false);
|
||||
$mailMissing = ($adminMailNow === '' && $smtpUserNow === '');
|
||||
?>
|
||||
<label class="check"><input type="checkbox" name="notify_admin_new_thread" value="1"<?php echo $newNotifyOn ? ' checked' : ''; ?>> 有新反馈时通知管理员
|
||||
<span class="hint" style="display:block;margin-left:24px">发到「站点信息」里填的官方邮箱;没填官方邮箱时退而发到 SMTP 发件地址。</span>
|
||||
<?php if ($newNotifyOn && $mailMissing): ?>
|
||||
<span class="hint" style="display:block;margin-left:24px;color:#b4553c;font-weight:600">⚠️ 官方邮箱和 SMTP 发件地址都是空的,这条通知目前发不出去。请在上方「官方邮箱」处填写。</span>
|
||||
<?php elseif ($newNotifyOn && $adminMailNow === ''): ?>
|
||||
<span class="hint" style="display:block;margin-left:24px;color:#8a6a15">当前会发到 SMTP 发件地址 <?php echo e($smtpUserNow); ?>。若想收到别的信箱,请在「站点信息」填写官方邮箱。</span>
|
||||
<?php endif; ?></label>
|
||||
<label class="field" style="max-width:280px"><span>同一帖同一收件人的最短发信间隔(秒)</span>
|
||||
<?php /* 出厂默认是 300(见 config.example.php 的 notify.cooldown_seconds) */ ?>
|
||||
<input type="number" name="notify_cooldown_seconds" min="0" max="86400" placeholder="300"
|
||||
|
||||
@@ -772,12 +772,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 图标快选(后台软件编辑) ---------- */
|
||||
function bindIconPicker() {
|
||||
var boxes = document.querySelectorAll('.icon-presets');
|
||||
for (var i = 0; i < boxes.length; i++) {
|
||||
(function (box) {
|
||||
// 图标输入框是「图标快选」的前一个兄弟里的 input[name=icon]
|
||||
var field = box.parentNode;
|
||||
var input = field ? field.querySelector('input[name=icon]') : null;
|
||||
if (!input) { return; }
|
||||
var btns = box.querySelectorAll('.icon-preset');
|
||||
|
||||
function syncOn(v) {
|
||||
for (var k = 0; k < btns.length; k++) {
|
||||
var same = (btns[k].getAttribute('data-icon') || '') === v;
|
||||
if (same) { btns[k].classList.add('on'); } else { btns[k].classList.remove('on'); }
|
||||
}
|
||||
}
|
||||
for (var j = 0; j < btns.length; j++) {
|
||||
btns[j].addEventListener('click', function () {
|
||||
input.value = this.getAttribute('data-icon') || '';
|
||||
syncOn(input.value);
|
||||
});
|
||||
}
|
||||
input.addEventListener('input', function () { syncOn(this.value); });
|
||||
syncOn(input.value);
|
||||
})(boxes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 邮件测试发送(后台设置页) ---------- */
|
||||
function bindMailTest() {
|
||||
var btn = document.getElementById('mailTestBtn');
|
||||
if (!btn) { return; }
|
||||
var out = document.getElementById('mailTestOut');
|
||||
var to = document.getElementById('mailTestTo');
|
||||
var swSel = document.getElementById('mailTestSw');
|
||||
btn.addEventListener('click', function () {
|
||||
var addr = to ? (to.value || '').trim() : '';
|
||||
if (!addr) {
|
||||
@@ -793,6 +823,7 @@
|
||||
fd.append('csrf', btn.getAttribute('data-csrf') || '');
|
||||
fd.append('act', 'mail_test');
|
||||
fd.append('to', addr);
|
||||
if (swSel && swSel.value) { fd.append('sw', swSel.value); }
|
||||
|
||||
fetch('admin.php', { method: 'POST', body: fd, credentials: 'same-origin' })
|
||||
.then(function (r) { return r.text(); })
|
||||
@@ -821,6 +852,7 @@
|
||||
bindAdminToggle();
|
||||
bindWontfixPanel();
|
||||
bindColorPicker();
|
||||
bindIconPicker();
|
||||
bindMailTest();
|
||||
bindInlineValidate(); // ★ 必须在 bindNoDoubleSubmit 之前
|
||||
bindNoDoubleSubmit();
|
||||
|
||||
@@ -116,6 +116,44 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
|
||||
margin-top:14px; padding-top:14px; border-top:1px dashed var(--line);
|
||||
}
|
||||
|
||||
/* ---------- 软件标识:图标 + 名字(多软件区分的主视觉) ----------
|
||||
* 邮件模板里也有一套等价的 table 写法,两边的观感要对得上:
|
||||
* 圆角色块 + 居中字符 + 紧跟软件名。 */
|
||||
.sw-badge{display:inline-flex; align-items:center; gap:7px; min-width:0; vertical-align:middle}
|
||||
.sw-badge-ic{
|
||||
display:inline-flex; align-items:center; justify-content:center;
|
||||
border-radius:7px; color:#fff; font-weight:600; flex:none;
|
||||
line-height:1; letter-spacing:0; box-shadow:0 1px 2px rgba(15,25,45,.18);
|
||||
}
|
||||
.sw-badge-nm{
|
||||
font-weight:600; font-size:14px; color:var(--fg);
|
||||
overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
|
||||
}
|
||||
.sw-badge-sm .sw-badge-nm{font-size:13px}
|
||||
.sw-badge-lg .sw-badge-nm{font-size:17px}
|
||||
/* 页头大标题位置放软件标识:清掉 h1 的行高影响,让图标与文字垂直居中 */
|
||||
.page-h1-sw{margin:0; font-size:0; line-height:0}
|
||||
.page-h1-sw .sw-badge-nm{font-size:22px; font-weight:700; color:var(--fg)}
|
||||
/* 提交页顶部的软件上下文条 */
|
||||
.sw-context{
|
||||
display:flex; align-items:center; gap:10px; flex-wrap:wrap;
|
||||
margin:0 0 16px; padding:11px 14px; border-radius:11px;
|
||||
background:var(--brand-soft); border:1px solid var(--line);
|
||||
}
|
||||
.sw-context .sw-badge-nm{color:var(--fg)}
|
||||
/* 后台软件编辑:图标快选 */
|
||||
.icon-presets{display:flex; gap:4px; flex-wrap:wrap; margin-top:8px}
|
||||
.icon-preset{
|
||||
width:34px; height:34px; border:1px solid var(--line-strong); border-radius:9px;
|
||||
background:var(--card); cursor:pointer; font-size:17px; line-height:1; padding:0;
|
||||
display:inline-flex; align-items:center; justify-content:center;
|
||||
}
|
||||
.icon-preset:hover{border-color:var(--brand); background:var(--brand-soft)}
|
||||
/* 选中态的 chip 里,图标底衬白、字符取软件色,避免同色糊在一起 */
|
||||
.chip.on .sw-badge-ic{background:#fff !important; color:var(--brand) !important; box-shadow:none}
|
||||
.chip.on .sw-badge-nm{color:#fff}
|
||||
.chip .sw-badge-ic{box-shadow:none}
|
||||
|
||||
/* ---------- 主题色选择器 ---------- */
|
||||
.sw-dot{
|
||||
display:inline-block; width:10px; height:10px; border-radius:50%;
|
||||
|
||||
@@ -82,7 +82,11 @@ fb_layout_head($sw ? ((string)$sw['name']) : $headTitle, ['software' => $sw]);
|
||||
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1><?php echo e($sw ? (string)$sw['name'] : '全部问题'); ?></h1>
|
||||
<?php if ($sw): ?>
|
||||
<h1 class="page-h1-sw"><?php echo fb_software_badge($sw, 'lg'); ?></h1>
|
||||
<?php else: ?>
|
||||
<h1>全部问题</h1>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$siteDesc = (string)fb_cfg('site_desc');
|
||||
$swNote = ($sw && !empty($sw['note'])) ? (string)$sw['note'] : '';
|
||||
@@ -98,7 +102,7 @@ fb_layout_head($sw ? ((string)$sw['name']) : $headTitle, ['software' => $sw]);
|
||||
<div class="chips">
|
||||
<?php foreach ($swList as $s): ?>
|
||||
<a class="chip <?php echo ($sw && (int)$s['id'] === (int)$sw['id']) ? 'on' : ''; ?>"
|
||||
href="<?php echo e(fb_qs(['sw' => (string)$s['slug'], 'cat' => null, 'page' => null]) ?: ('?sw=' . e(urlencode((string)$s['slug'])))); ?>"><?php echo e((string)$s['name']); ?></a>
|
||||
href="<?php echo e(fb_qs(['sw' => (string)$s['slug'], 'cat' => null, 'page' => null]) ?: ('?sw=' . e(urlencode((string)$s['slug'])))); ?>"><?php echo fb_software_badge($s, 'sm'); ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+27
-5
@@ -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);
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ if (!defined('FB_ROOT')) {
|
||||
define('FB_ROOT', dirname(__DIR__));
|
||||
// 静态资源版本号:改 assets/ 下的 css/js 后把这个数字 +1,
|
||||
// 让浏览器与 CDN 缓存失效。全站只此一处,别再往页面里写死数字。
|
||||
if (!defined('FB_ASSET_V')) { define('FB_ASSET_V', 16); }
|
||||
if (!defined('FB_ASSET_V')) { define('FB_ASSET_V', 17); }
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
|
||||
+11
@@ -324,6 +324,17 @@ SQL
|
||||
if (!isset($swNames['color'])) {
|
||||
$pdo->exec("ALTER TABLE software ADD COLUMN color TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
/* ---- 软件增加图标(多软件并存时靠「图标 + 名字 + 颜色」三重区分)----
|
||||
* 存一个字符即可:优先 emoji(日历 📅 / 锁 🔐 / 密码 🔑),也接受单个汉字或字母。
|
||||
* 不存图片路径,避免再管一套上传 / 存储 / 权限 —— 邮件里也用同一个字符渲染,
|
||||
* 前端与邮件天然一致,不会出现「页面有图标、邮件没有」的割裂。 */
|
||||
if (!isset($swNames['icon'])) {
|
||||
$pdo->exec("ALTER TABLE software ADD COLUMN icon TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
/* ---- 软件增加下载地址(状态「已解决」的邮件里给用户一个去下新版的入口)---- */
|
||||
if (!isset($swNames['download_url'])) {
|
||||
$pdo->exec("ALTER TABLE software ADD COLUMN download_url TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
|
||||
/* ---- threads 增加两列(老 category 文本列保留,不删)---- */
|
||||
$cols = $pdo->query('PRAGMA table_info(threads)')->fetchAll();
|
||||
|
||||
@@ -51,10 +51,22 @@ function fb_layout_head($title, array $opt = []) {
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="wrap">
|
||||
<?php
|
||||
/* 顶栏品牌位:当前在哪個软件下,就显示那个软件的「图标 + 名字」,
|
||||
* 而不是一直挂着站点大标题 —— 多软件共用一套反馈系统,
|
||||
* 用户要一眼看清「我现在是在给哪个软件提问题」。 */
|
||||
$curSw = isset($opt['software']) && is_array($opt['software']) ? $opt['software'] : null;
|
||||
?>
|
||||
<?php if ($curSw): ?>
|
||||
<a class="brand-mini" href="./" title="<?php echo e($site); ?>">
|
||||
<?php echo fb_software_badge($curSw, 'md'); ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a class="brand-mini" href="./" title="<?php echo e($site); ?>">
|
||||
<span class="logo-mark" role="img" aria-label="轻反馈图标"></span>
|
||||
<span class="brand-text"><?php echo e($site); ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<div class="spacer"></div>
|
||||
<a class="navlink hide-sm" href="./">全部问题</a>
|
||||
<?php if (fb_admin_logged_in()): ?><a class="navlink" href="admin.php">管理后台</a><?php endif; ?>
|
||||
|
||||
+212
-32
@@ -144,14 +144,67 @@ if (!function_exists('fb_mail_esc')) {
|
||||
}
|
||||
|
||||
if (!function_exists('fb_mail_brand')) {
|
||||
/** 取站点主色,用于邮件头与按钮。跟随「软件主题色」体系,但邮件里只用主色与浅底 */
|
||||
/**
|
||||
* 邮件主色。
|
||||
*
|
||||
* 🔴 多软件站点的核心要求:邮件颜色必须跟「这条反馈属于哪个软件」一致,
|
||||
* 否则用户在收件箱里根本分不清是哪个软件的来信。
|
||||
* 取值顺序:
|
||||
* ① 调用方显式传入的软件行(fb_mail_layout(['software' => ...]))
|
||||
* ② 当前全局软件($GLOBALS['__fb_mail_sw'],由 fb_notify_render 设置)
|
||||
* ③ 配置里的 mail_brand_color(单软件站点的老配置,保持兼容)
|
||||
* ④ 兜底蓝
|
||||
*/
|
||||
function fb_mail_brand() {
|
||||
$sw = null;
|
||||
if (isset($GLOBALS['__fb_mail_sw']) && is_array($GLOBALS['__fb_mail_sw'])) {
|
||||
$sw = $GLOBALS['__fb_mail_sw'];
|
||||
}
|
||||
if ($sw) {
|
||||
list($c) = fb_software_colors($sw);
|
||||
if ($c !== '') { return $c; }
|
||||
}
|
||||
$c = fb_color_norm((string)fb_cfg('mail_brand_color', ''));
|
||||
if ($c === '') { $c = '#0057B8'; }
|
||||
return $c;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('fb_mail_sw_ctx')) {
|
||||
/** 设置/读取「当前邮件所属软件」,返回之前的上下文(便于嵌套调用后还原) */
|
||||
function fb_mail_sw_ctx($sw = null, $set = false) {
|
||||
$prev = isset($GLOBALS['__fb_mail_sw']) ? $GLOBALS['__fb_mail_sw'] : null;
|
||||
if ($set) { $GLOBALS['__fb_mail_sw'] = $sw; }
|
||||
return $prev;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('fb_mail_sw_header')) {
|
||||
/**
|
||||
* 邮件头部的「软件标识」:色块图标 + 软件名。
|
||||
* 用 table + 内联样式写,兼容 Outlook 等不认 flex 的客户端。
|
||||
*/
|
||||
function fb_mail_sw_header($sw) {
|
||||
if (!is_array($sw) || empty($sw['name'])) { return ''; }
|
||||
list($c) = fb_software_colors($sw);
|
||||
if ($c === '') { $c = '#0057B8'; }
|
||||
$icon = fb_software_icon($sw);
|
||||
$name = (string)$sw['name'];
|
||||
|
||||
return '<table role="presentation" cellpadding="0" cellspacing="0" border="0"'
|
||||
. ' style="border-collapse:collapse"><tr>'
|
||||
. '<td width="30" style="width:30px;height:30px;background:' . fb_mail_esc($c) . ';'
|
||||
. 'border-radius:8px;text-align:center;vertical-align:middle;'
|
||||
. 'font-size:15px;line-height:30px;color:#ffffff;font-weight:700">'
|
||||
. '<span style="display:inline-block;line-height:30px">' . fb_mail_esc($icon) . '</span>'
|
||||
. '</td>'
|
||||
. '<td style="padding-left:9px;vertical-align:middle;font-size:15px;'
|
||||
. 'font-weight:700;color:#1b2430;letter-spacing:.2px">'
|
||||
. fb_mail_esc($name) . '</td>'
|
||||
. '</tr></table>';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('fb_mail_block')) {
|
||||
/**
|
||||
* 渲染一个内容块。
|
||||
@@ -242,6 +295,10 @@ if (!function_exists('fb_mail_layout')) {
|
||||
* @return string 完整 HTML(可直接作为 text/html 正文)
|
||||
*/
|
||||
function fb_mail_layout(array $o) {
|
||||
// 软件上下文:显式传入优先,否则用全局上下文(fb_notify_render 会设好)
|
||||
$sw = isset($o['software']) && is_array($o['software']) ? $o['software'] : null;
|
||||
if ($sw) { fb_mail_sw_ctx($sw, true); } // 让本函数内 fb_mail_brand() 取到软件色
|
||||
|
||||
$brand = fb_mail_brand();
|
||||
$site = (string)fb_cfg('site_name', '轻反馈');
|
||||
$base = fb_base_url();
|
||||
@@ -330,11 +387,18 @@ if (!function_exists('fb_mail_layout')) {
|
||||
. '<tr><td style="height:4px;background:' . fb_mail_esc($brand) . ';font-size:0;'
|
||||
. 'line-height:0"> </td></tr>'
|
||||
|
||||
// 站名
|
||||
// 站名 / 软件标识
|
||||
// 有软件上下文时:上排是「图标 + 软件名」(这是用户最需要一眼认出的信息),
|
||||
// 下排再补一行小字站点名。没有软件时退回原来的纯站名写法。
|
||||
. '<tr><td style="padding:22px 32px 0">'
|
||||
. '<div style="font-size:13px;font-weight:700;letter-spacing:.6px;'
|
||||
. (isset($o['software']) && is_array($o['software'])
|
||||
? fb_mail_sw_header($o['software'])
|
||||
. '<div style="margin-top:7px;font-size:11.5px;letter-spacing:.4px;color:#9aa2ad">'
|
||||
. fb_mail_esc($site) . '</div>'
|
||||
: '<div style="font-size:13px;font-weight:700;letter-spacing:.6px;'
|
||||
. 'color:' . fb_mail_esc($brand) . ';text-transform:uppercase">'
|
||||
. fb_mail_esc($site) . '</div></td></tr>'
|
||||
. fb_mail_esc($site) . '</div>')
|
||||
. '</td></tr>'
|
||||
|
||||
// 标题
|
||||
. '<tr><td style="padding:12px 32px 4px">' . $badgeHtml
|
||||
@@ -512,6 +576,7 @@ if (!function_exists('fb_notify_reason_label')) {
|
||||
/** 事件的中文名,用于日志与模板徽标 */
|
||||
function fb_notify_reason_label($reason) {
|
||||
$map = [
|
||||
'receipt' => '已收到反馈',
|
||||
'reply' => '有新回复',
|
||||
'status' => '状态更新',
|
||||
'approve' => '已发布',
|
||||
@@ -533,9 +598,15 @@ if (!function_exists('fb_notify_enabled_for')) {
|
||||
* 它独立于前者 —— 就算关了给用户的信,管理员也可以想收通知
|
||||
*/
|
||||
function fb_notify_enabled_for($reason) {
|
||||
// 「新反馈」按 admin_new_thread 独立开关;「提交回执」按 receipt_enabled 独立开关
|
||||
// (回执是发给提问人的,跟「有人回复时通知提交人」是两件不同的事,
|
||||
// 有人可能只想要回执不想要回复通知,所以不能共用一个开关)
|
||||
if ($reason === 'new') {
|
||||
return (bool)fb_cfg('notify.admin_new_thread', false);
|
||||
}
|
||||
if ($reason === 'receipt') {
|
||||
return (bool)fb_cfg('notify.receipt_enabled', true);
|
||||
}
|
||||
return (bool)fb_cfg('notify.enabled', true);
|
||||
}
|
||||
}
|
||||
@@ -581,8 +652,20 @@ if (!function_exists('fb_notify_event')) {
|
||||
function fb_notify_event(PDO $pdo, $reason, array $thread, array $extra = []) {
|
||||
$reason = (string)$reason;
|
||||
|
||||
/* 🔴 铁律:任何一条退出路径都必须写 notify_log。
|
||||
*
|
||||
* 原先前 4 个判断(开关 / 收件人 / 自己回自己 / 冷却)直接 return 不记日志,
|
||||
* 结果就是「用户说没收到信」时后台查不到任何记录 —— 因为压根就没进发信那一步。
|
||||
* 2026-11-03 排查时 notify_log 全表为空,一度被误判成「发信链路坏了」,
|
||||
* 实际是这些静默 return 造成的黑箱。日志必须回答「为什么没发」。
|
||||
*/
|
||||
$log = function ($to, $ok, $err) use ($pdo, $thread, $reason) {
|
||||
fb_notify_log2($pdo, $thread, $to, $reason, $ok, $err);
|
||||
};
|
||||
|
||||
// ---- 1. 开关 ----
|
||||
if (!fb_notify_enabled_for($reason)) {
|
||||
$log('', 0, '通知未启用(该事件的开关是关的)');
|
||||
return [false, '通知未启用'];
|
||||
}
|
||||
|
||||
@@ -591,30 +674,41 @@ if (!function_exists('fb_notify_event')) {
|
||||
if ($isAdminNotice) {
|
||||
$to = trim((string)fb_cfg('admin_email', ''));
|
||||
if ($to === '') { $to = trim((string)fb_cfg('admin.email', '')); }
|
||||
// 还没填官方邮箱时回落到 SMTP 发件地址 —— 至少信能到管理员手上,
|
||||
// 而不是整个「新反馈通知」功能静默失效(实际就发生过:管理员从不收信,
|
||||
// 后台也没有任何提示,直到来测才发现)。
|
||||
if ($to === '') { $to = trim((string)fb_cfg('smtp.user', '')); }
|
||||
if ($to === '') { $to = trim((string)fb_cfg('smtp.from', '')); }
|
||||
} else {
|
||||
$to = trim((string)(isset($thread['author_email']) ? $thread['author_email'] : ''));
|
||||
}
|
||||
if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
return [false, '收件邮箱无效'];
|
||||
$why = $isAdminNotice
|
||||
? '收件邮箱无效:官方邮箱与 SMTP 发件地址都没填,新反馈通知发不出去'
|
||||
: '收件邮箱无效:提交人没留邮箱或格式不对';
|
||||
$log($to, 0, $why);
|
||||
return [false, $why];
|
||||
}
|
||||
|
||||
// ---- 3. 自己通知自己就别发了 ----
|
||||
if (!$isAdminNotice && !empty($extra['is_admin']) === false
|
||||
&& !empty($extra['reply']['author_email'])) {
|
||||
if (strcasecmp(trim((string)$extra['reply']['author_email']), $to) === 0) {
|
||||
$log($to, 0, '自己回复自己,不发');
|
||||
return [false, '自己回复自己,不发'];
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 4. 节流 ----
|
||||
if (fb_notify_throttled($pdo, (int)$thread['id'], $to, $reason)) {
|
||||
$log($to, 0, '冷却中跳过(notify.cooldown_seconds=' . (int)fb_cfg('notify.cooldown_seconds', 300) . ')');
|
||||
return [false, '冷却中,跳过'];
|
||||
}
|
||||
|
||||
// ---- 5. SMTP 就绪吗 ----
|
||||
$smtp = (array)fb_cfg('smtp', []);
|
||||
if (empty($smtp['enabled']) || trim((string)$smtp['host']) === '') {
|
||||
fb_notify_log2($pdo, $thread, $to, $reason, 0, 'SMTP 未启用或未配置服务器');
|
||||
$log($to, 0, 'SMTP 未启用或未配置服务器');
|
||||
return [false, 'SMTP 未启用或未配置服务器'];
|
||||
}
|
||||
|
||||
@@ -623,7 +717,7 @@ if (!function_exists('fb_notify_event')) {
|
||||
|
||||
// ---- 7. 发送 ----
|
||||
list($ok, $err) = fb_smtp_send($smtp, $to, $subject, $html);
|
||||
fb_notify_log2($pdo, $thread, $to, $reason, $ok ? 1 : 0, $ok ? null : $err);
|
||||
$log($to, $ok ? 1 : 0, $ok ? null : $err);
|
||||
return [$ok, $ok ? '' : (string)$err];
|
||||
}
|
||||
}
|
||||
@@ -691,10 +785,10 @@ if (!function_exists('fb_notify_status_meta')) {
|
||||
'tip' => '感谢你的耐心等待,暂时不需要你做任何操作。',
|
||||
],
|
||||
'resolved' => [
|
||||
'badge' => '已解决', 'color' => '#1e7b3c', 'tone' => 'ok',
|
||||
'title' => '你反馈的问题已解决',
|
||||
'intro' => '好消息,你反馈的问题我们已经处理完成。',
|
||||
'tip' => '如果问题仍然存在,直接在本页回复即可,我们会继续跟进。',
|
||||
'badge' => '已完成', 'color' => '#1e7b3c', 'tone' => 'ok',
|
||||
'title' => '你反馈的问题已完成并上线',
|
||||
'intro' => '好消息:你反馈的内容我们已经处理完成,并已发布上线。',
|
||||
'tip' => '建议更新到最新版本后试一下。如果问题仍然存在,直接在本页回复即可,我们会继续跟进。',
|
||||
],
|
||||
'closed' => [
|
||||
'badge' => '已关闭', 'color' => '#8a6a15', 'tone' => 'warn',
|
||||
@@ -731,6 +825,24 @@ if (!function_exists('fb_notify_render')) {
|
||||
$short = fb_excerpt($title, 42);
|
||||
$isAdmin = !empty($extra['is_admin']);
|
||||
|
||||
/* ---- 软件上下文:整封邮件的配色、头部标识、主题前缀都靠它 ----
|
||||
* 每条反馈都属于某个软件。邮件必须让用户一眼看出「这是哪个软件的来信」,
|
||||
* 所以这里解析一次,后面所有分支共用:
|
||||
* $swName 软件名(进主题行前缀)
|
||||
* $sw 软件行(进邮件头标识与配色)
|
||||
*
|
||||
* 🔴 允许 $extra['software'] 显式指定软件行:
|
||||
* ① 后台「发送测试邮件」可以指定「按某个软件的样子发」,便于预览配色;
|
||||
* ② 测试脚本能在不往库里插数据的前提下验证配色切换;
|
||||
* ③ 万一 threads.software_id 指向已被删除的软件,调用方仍能给出正确上下文。
|
||||
* 没有显式指定时,才按 software_id 回查、再回落默认软件。 */
|
||||
$sw = (isset($extra['software']) && is_array($extra['software'])) ? $extra['software'] : null;
|
||||
if (!$sw) { $sw = fb_software_of_thread($thread); }
|
||||
$swName = $sw ? (string)$sw['name'] : '';
|
||||
fb_mail_sw_ctx($sw, true);
|
||||
// 主题统一加软件名前缀,收件箱列表里不点开就能分辨
|
||||
$pfx = ($swName !== '') ? '【' . $swName . '】' : '';
|
||||
|
||||
$subject = '';
|
||||
$layout = [];
|
||||
|
||||
@@ -741,21 +853,30 @@ if (!function_exists('fb_notify_render')) {
|
||||
$rbody = (string)(isset($reply['body']) ? $reply['body'] : '');
|
||||
$isOff = $isAdmin ? '官方回复' : '新回复';
|
||||
|
||||
$subject = '【' . $site . '】' . $isOff . ':' . $short;
|
||||
$subject = $pfx . $isOff . ':' . $short;
|
||||
|
||||
/* 像博客评论通知那样带上上下文:
|
||||
* 用户可能同时提过好几条反馈,只给「回复内容」根本想不起来是哪一条。
|
||||
* 所以固定三段:① 你反馈了什么 ② 谁回复了什么 ③ 去哪里继续。 */
|
||||
$origBody = (string)(isset($thread['body']) ? $thread['body'] : '');
|
||||
$blocks = [
|
||||
['type' => 'text', 'text' => '你在「' . $site . '」提交的反馈有新的回复。'],
|
||||
['type' => 'text', 'text' => ($swName !== '' ? '你在「' . $swName . '」提交的反馈' : '你提交的反馈')
|
||||
. '有新的回复。'],
|
||||
['type' => 'field', 'rows' => ['反馈标题' => $title]],
|
||||
['type' => 'quote', 'who' => $rname . ' 回复:', 'text' => $rbody],
|
||||
];
|
||||
if (trim($origBody) !== '') {
|
||||
$blocks[] = ['type' => 'quote', 'who' => '你当时反馈的内容:', 'text' => fb_excerpt($origBody, 220)];
|
||||
}
|
||||
$blocks[] = ['type' => 'quote', 'who' => $rname . ' 回复:', 'text' => $rbody];
|
||||
if ($isAdmin) {
|
||||
$blocks[] = ['type' => 'callout', 'tone' => 'info',
|
||||
'text' => '这是官方回复。如果还有疑问,直接在本页继续回复即可。'];
|
||||
}
|
||||
|
||||
$layout = [
|
||||
'software' => $sw,
|
||||
'preheader' => $rname . ':' . fb_excerpt($rbody, 60),
|
||||
'badge' => ['text' => $isOff, 'color' => $isAdmin ? '#0057B8' : '#5c6572'],
|
||||
'badge' => ['text' => $isOff, 'color' => $isAdmin ? fb_mail_brand() : '#5c6572'],
|
||||
'title' => $isAdmin ? '官方回复了你的反馈' : '有人回复了你的反馈',
|
||||
'intro' => ($name !== '' ? $name . ',你好:' : '你好:'),
|
||||
'blocks' => $blocks,
|
||||
@@ -775,9 +896,10 @@ if (!function_exists('fb_notify_render')) {
|
||||
$label = fb_status_label($newKey);
|
||||
if ($label === '') { $label = $meta['badge']; }
|
||||
|
||||
$subject = '【' . $site . '】' . $short . ' —— ' . $label;
|
||||
$subject = $pfx . $short . ' —— ' . $label;
|
||||
|
||||
$rows = ['反馈标题' => $title];
|
||||
if ($swName !== '') { $rows['所属软件'] = $swName; }
|
||||
if ($oldKey !== '' && $oldKey !== $newKey) {
|
||||
$oldLabel = fb_status_label($oldKey);
|
||||
if ($oldLabel !== '') { $rows['原状态'] = $oldLabel; }
|
||||
@@ -795,33 +917,50 @@ if (!function_exists('fb_notify_render')) {
|
||||
$blocks[] = ['type' => 'callout', 'tone' => $meta['tone'], 'text' => $meta['tip']];
|
||||
}
|
||||
|
||||
/* 「已解决」要真的推用户一把去验证:
|
||||
* 软件配了下载地址就给一个下载按钮 —— 只说「修好了」用户不知道去哪拿新版本。 */
|
||||
$dlUrl = $sw ? trim((string)(isset($sw['download_url']) ? $sw['download_url'] : '')) : '';
|
||||
$btnText = '查看详情';
|
||||
$btnUrl = $url;
|
||||
if ($newKey === 'resolved' && $dlUrl !== '') {
|
||||
$blocks[] = ['type' => 'callout', 'tone' => 'ok',
|
||||
'text' => '新版本已经发布,建议下载最新版后验证一下。'];
|
||||
$btnText = '下载最新版本';
|
||||
$btnUrl = $dlUrl;
|
||||
}
|
||||
|
||||
$layout = [
|
||||
'software' => $sw,
|
||||
'preheader' => '状态更新为「' . $label . '」',
|
||||
'badge' => ['text' => $meta['badge'], 'color' => $meta['color']],
|
||||
'title' => $meta['title'],
|
||||
'intro' => ($name !== '' ? $name . ',你好:' : '你好:'),
|
||||
'blocks' => $blocks,
|
||||
'button' => ['text' => '查看详情', 'url' => $url],
|
||||
'link' => $url,
|
||||
'button' => ['text' => $btnText, 'url' => $btnUrl],
|
||||
// 下载按钮之外,始终保留「查看这条反馈」的原文链接
|
||||
'link' => ($btnUrl === $url) ? $url
|
||||
: ('下载地址:' . $dlUrl . "\n" . '查看反馈:' . $url),
|
||||
'recipient' => $to,
|
||||
];
|
||||
}
|
||||
|
||||
/* ---------------- 3. 审核通过 ---------------- */
|
||||
elseif ($reason === 'approve') {
|
||||
$subject = '【' . $site . '】你的反馈已发布:' . $short;
|
||||
$subject = $pfx . '你的反馈已发布:' . $short;
|
||||
$layout = [
|
||||
'software' => $sw,
|
||||
'preheader' => '你的反馈已经公开可见',
|
||||
'badge' => ['text' => '已发布', 'color' => '#1e7b3c'],
|
||||
'title' => '你的反馈已通过审核并公开',
|
||||
'intro' => ($name !== '' ? $name . ',你好:' : '你好:'),
|
||||
'blocks' => [
|
||||
['type' => 'text', 'text' => '你提交的反馈已通过审核,现在可以在公开列表中看到了。'],
|
||||
['type' => 'text', 'text' => '你提交的反馈已通过审核,现在可以在'
|
||||
. ($swName !== '' ? '「' . $swName . '」的' : '') . '公开列表中看到了。'],
|
||||
['type' => 'field', 'rows' => ['反馈标题' => $title]],
|
||||
['type' => 'callout', 'tone' => 'info',
|
||||
'text' => '后续的处理进展与回复,我们会通过邮件通知你。'],
|
||||
],
|
||||
'button' => ['text' => '查看这条反馈', 'url' => $url],
|
||||
'button' => ['text' => '查看并跟踪这条反馈', 'url' => $url],
|
||||
'link' => $url,
|
||||
'recipient' => $to,
|
||||
];
|
||||
@@ -830,7 +969,7 @@ if (!function_exists('fb_notify_render')) {
|
||||
/* ---------------- 4. 转为不公开 ---------------- */
|
||||
elseif ($reason === 'hide') {
|
||||
$note = trim((string)(isset($extra['note']) ? $extra['note'] : ''));
|
||||
$subject = '【' . $site . '】你的反馈已转为不公开:' . $short;
|
||||
$subject = $pfx . '你的反馈已转为不公开:' . $short;
|
||||
|
||||
$blocks = [
|
||||
['type' => 'text', 'text' => '你提交的反馈已从公开列表中撤下,不再对其他人可见。'],
|
||||
@@ -843,6 +982,7 @@ if (!function_exists('fb_notify_render')) {
|
||||
'text' => '这条反馈本身仍然保留,你依然可以通过原链接访问和处理进展。'];
|
||||
|
||||
$layout = [
|
||||
'software' => $sw,
|
||||
'preheader' => '这条反馈已不对其他人可见',
|
||||
'badge' => ['text' => '不公开', 'color' => '#5c6572'],
|
||||
'title' => '你的反馈已转为不公开',
|
||||
@@ -860,11 +1000,12 @@ if (!function_exists('fb_notify_render')) {
|
||||
$body = (string)$thread['body'];
|
||||
$mail = trim((string)fb_cfg('admin_email', ''));
|
||||
|
||||
$subject = '【' . $site . '】关于你反馈的问题《' . $short . '》';
|
||||
$subject = $pfx . '关于你反馈的问题《' . $short . '》';
|
||||
|
||||
$blocks = [
|
||||
['type' => 'text',
|
||||
'text' => '感谢你的反馈。我们已经核实过,这个情况并不是应用本身的问题,因此这条反馈已关闭(不会公开显示)。'],
|
||||
'text' => '感谢你的反馈。我们已经核实过,这个情况并不是'
|
||||
. ($swName !== '' ? '「' . $swName . '」' : '应用') . '本身的问题,因此这条反馈已关闭(不会公开显示)。'],
|
||||
['type' => 'field', 'rows' => ['反馈标题' => $title]],
|
||||
];
|
||||
if ($note !== '') {
|
||||
@@ -879,6 +1020,7 @@ if (!function_exists('fb_notify_render')) {
|
||||
];
|
||||
|
||||
$layout = [
|
||||
'software' => $sw,
|
||||
'preheader' => '关于你反馈的问题,我们的说明',
|
||||
'badge' => ['text' => '已关闭', 'color' => '#8a6a15'],
|
||||
'title' => '关于你反馈的问题,我们的说明',
|
||||
@@ -893,18 +1035,24 @@ if (!function_exists('fb_notify_render')) {
|
||||
$body = (string)$thread['body'];
|
||||
$mail = (string)(isset($thread['author_email']) ? $thread['author_email'] : '');
|
||||
$phone = trim((string)(isset($thread['author_phone']) ? $thread['author_phone'] : ''));
|
||||
$swName = (string)(isset($extra['software_name']) ? $extra['software_name'] : '');
|
||||
// 软件名优先取实际软件行(比 extra 里传进来的更可靠)
|
||||
$swName = $swName !== '' ? $swName
|
||||
: (string)(isset($extra['software_name']) ? $extra['software_name'] : '');
|
||||
|
||||
$subject = '【' . $site . '】新反馈:' . $short;
|
||||
$subject = $pfx . '新反馈:' . $short;
|
||||
|
||||
$rows = ['标题' => $title];
|
||||
$rows = [];
|
||||
if ($swName !== '') { $rows['所属软件'] = $swName; }
|
||||
$rows['标题'] = $title;
|
||||
$rows['提交人'] = $name !== '' ? $name : '(未留名)';
|
||||
if ($mail !== '') { $rows['邮箱'] = $mail; }
|
||||
if ($phone !== '') { $rows['电话'] = $phone; }
|
||||
|
||||
$adminUrl = fb_base_url() . '/admin.php?tab=thread&id=' . (int)$thread['id'];
|
||||
|
||||
$blocks = [
|
||||
['type' => 'text', 'text' => '有新的反馈提交,请及时处理。'],
|
||||
['type' => 'text', 'text' => ($swName !== '' ? '「' . $swName . '」' : '')
|
||||
. '有一条新的反馈提交,请到管理后台处理。'],
|
||||
['type' => 'field', 'rows' => $rows],
|
||||
['type' => 'quote', 'who' => '反馈内容:', 'text' => $body],
|
||||
['type' => 'callout', 'tone' => 'warn',
|
||||
@@ -912,22 +1060,54 @@ if (!function_exists('fb_notify_render')) {
|
||||
];
|
||||
|
||||
$layout = [
|
||||
'software' => $sw,
|
||||
'preheader' => $name . ':' . fb_excerpt($body, 60),
|
||||
'badge' => ['text' => '新反馈', 'color' => '#b4553c'],
|
||||
'title' => '收到一条新反馈',
|
||||
'title' => ($swName !== '' ? $swName . ' 收到一条新反馈' : '收到一条新反馈'),
|
||||
'intro' => '你好:',
|
||||
'blocks' => $blocks,
|
||||
'button' => ['text' => '去后台处理', 'url' => fb_base_url() . '/admin.php?tab=thread&id=' . (int)$thread['id']],
|
||||
'link' => fb_base_url() . '/admin.php?tab=thread&id=' . (int)$thread['id'],
|
||||
'button' => ['text' => '去后台处理这条反馈', 'url' => $adminUrl],
|
||||
'link' => $adminUrl,
|
||||
'recipient' => $to,
|
||||
'footer' => '此邮件由系统自动发送。想调整接收规则,请到后台「邮件通知」页修改。',
|
||||
];
|
||||
}
|
||||
|
||||
/* ---------------- 7. 提交回执(发给提问人) ---------------- */
|
||||
elseif ($reason === 'receipt') {
|
||||
$body = (string)$thread['body'];
|
||||
|
||||
$subject = $pfx . '已收到你的反馈:' . $short;
|
||||
|
||||
$blocks = [
|
||||
['type' => 'text', 'text' => '我们已收到你在'
|
||||
. ($swName !== '' ? '「' . $swName . '」' : '本站')
|
||||
. '提交的反馈,感谢你的反馈!'],
|
||||
['type' => 'field', 'rows' => ['反馈标题' => $title]],
|
||||
['type' => 'quote', 'who' => '你提交的内容:', 'text' => fb_excerpt($body, 400)],
|
||||
['type' => 'callout', 'tone' => 'info',
|
||||
'text' => '这条反馈正在等待管理员审核,请耐心等待。审核通过后会在列表中公开显示。'],
|
||||
['type' => 'text', 'text' => '审核结果与后续处理进展,我们都会通过邮件通知你,无需反复回来查看。'],
|
||||
];
|
||||
|
||||
$layout = [
|
||||
'software' => $sw,
|
||||
'preheader' => '已收到你的反馈,正在等待审核',
|
||||
'badge' => ['text' => '已收到', 'color' => fb_mail_brand()],
|
||||
'title' => '已收到你的反馈',
|
||||
'intro' => ($name !== '' ? $name . ',你好:' : '你好:'),
|
||||
'blocks' => $blocks,
|
||||
'button' => ['text' => '查看这条反馈', 'url' => $url],
|
||||
'link' => $url,
|
||||
'recipient' => $to,
|
||||
];
|
||||
}
|
||||
|
||||
/* ---------------- 兜底 ---------------- */
|
||||
else {
|
||||
$subject = '【' . $site . '】' . $short;
|
||||
$subject = $pfx . $short;
|
||||
$layout = [
|
||||
'software' => $sw,
|
||||
'preheader' => '你有一条新的通知',
|
||||
'title' => '你有一条新的通知',
|
||||
'intro' => ($name !== '' ? $name . ',你好:' : '你好:'),
|
||||
|
||||
@@ -171,6 +171,20 @@ function fb_default_software() {
|
||||
return $list ? $list[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 由帖子行取它所属的软件行(邮件/页面要显示「这是对哪个软件提交的」都用它)。
|
||||
* threads 只存 software_id,这里统一解析,避免各处自己写 SQL 漏掉。
|
||||
* 取不到时回落到默认软件,保证邮件里永远有软件名可显示。
|
||||
*/
|
||||
function fb_software_of_thread(array $thread) {
|
||||
$id = isset($thread['software_id']) ? (int)$thread['software_id'] : 0;
|
||||
if ($id > 0) {
|
||||
$sw = fb_software($id);
|
||||
if ($sw) { return $sw; }
|
||||
}
|
||||
return fb_default_software();
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单回显用:如果当前值等于「出厂默认」,就返回空串。
|
||||
*
|
||||
@@ -263,6 +277,74 @@ function fb_software_colors($software) {
|
||||
return fb_color_variants($hex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化软件图标字符。
|
||||
* 只留前几个字符,去掉换行与 HTML 尖括号 —— 这个值会直接输出到页面和邮件里,
|
||||
* 放进 <span> 之间。虽然后面统一走 e() 转义,但源头收窄一层更稳。
|
||||
*/
|
||||
function fb_sw_icon_norm($s) {
|
||||
$s = trim(strip_tags((string)$s));
|
||||
$s = preg_replace('/[\x00-\x1F\x7F]+/u', '', $s);
|
||||
if ($s === null || $s === '') { return ''; }
|
||||
return mb_substr($s, 0, 4, 'UTF-8');
|
||||
}
|
||||
|
||||
/** 规范化软件的下载地址:只接受 http/https,其它一律丢空 */
|
||||
function fb_sw_url_norm($s) {
|
||||
$s = trim((string)$s);
|
||||
if ($s === '') { return ''; }
|
||||
if (!preg_match('~^https?://~i', $s)) { return ''; }
|
||||
return mb_substr($s, 0, 300, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 某软件的图标字符(单字符:emoji / 汉字 / 字母)。
|
||||
* 没配就按软件名首字兜底 —— 多软件并存时,任何地方都不该出现「无图标」的空档。
|
||||
*/
|
||||
function fb_software_icon($software) {
|
||||
$ic = '';
|
||||
if (is_array($software)) {
|
||||
$ic = trim((string)(isset($software['icon']) ? $software['icon'] : ''));
|
||||
if ($ic === '') {
|
||||
// 兜底:取名称首字(中文取第一个字,英文取首字母大写)
|
||||
$nm = trim((string)(isset($software['name']) ? $software['name'] : ''));
|
||||
if ($nm !== '') {
|
||||
if (preg_match('/^[A-Za-z]/', $nm)) { $ic = strtoupper($nm[0]); }
|
||||
else { $ic = mb_substr($nm, 0, 1, 'UTF-8'); }
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($ic === '') { $ic = '·'; }
|
||||
// 只允许极短内容,防止把整段 HTML 塞进图标位
|
||||
return mb_substr($ic, 0, 4, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 图标 + 名字的组合标签(HTML)。
|
||||
* 前端所有「这是哪个软件」的地方都用它,保证视觉一致。
|
||||
*
|
||||
* @param array $sw 软件行
|
||||
* @param string $size 'sm' | 'md' | 'lg'
|
||||
*/
|
||||
function fb_software_badge($software, $size = 'md') {
|
||||
if (!is_array($software) || empty($software['name'])) { return ''; }
|
||||
list($c) = fb_software_colors($software);
|
||||
if ($c === '') { $c = '#0057B8'; }
|
||||
$icon = fb_software_icon($software);
|
||||
$name = (string)$software['name'];
|
||||
|
||||
$px = ['sm' => 20, 'md' => 26, 'lg' => 34];
|
||||
$fs = ['sm' => 11, 'md' => 14, 'lg' => 17];
|
||||
$box = isset($px[$size]) ? $px[$size] : 26;
|
||||
$fnt = isset($fs[$size]) ? $fs[$size] : 14;
|
||||
|
||||
return '<span class="sw-badge sw-badge-' . e($size) . '">'
|
||||
. '<span class="sw-badge-ic" aria-hidden="true" style="width:' . $box . 'px;height:' . $box
|
||||
. 'px;background:' . e($c) . ';font-size:' . $fnt . 'px">' . e($icon) . '</span>'
|
||||
. '<span class="sw-badge-nm">' . e($name) . '</span>'
|
||||
. '</span>';
|
||||
}
|
||||
|
||||
/** 某软件下的分类;不传软件则返回空数组 */
|
||||
function fb_categories($softwareId = null, $onlyEnabled = true) {
|
||||
if ($softwareId === null) { return []; }
|
||||
@@ -490,6 +572,7 @@ function fb_setting_keys() {
|
||||
'smtp.timeout', 'smtp.verify_ssl',
|
||||
'notify.enabled', 'notify.on_every_reply', 'notify.cooldown_seconds',
|
||||
'notify.admin_new_thread',
|
||||
'notify.receipt_enabled',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -119,6 +119,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$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) {
|
||||
@@ -137,6 +144,10 @@ fb_layout_head('我要反馈', ['robots' => 'noindex,follow', 'software' => $sw]
|
||||
|
||||
<section class="narrow">
|
||||
<h1>我要反馈</h1>
|
||||
<?php if ($sw): ?>
|
||||
<div class="sw-context"><?php echo fb_software_badge($sw, 'md'); ?>
|
||||
<span class="muted small">你正在为这个软件提交反馈</span></div>
|
||||
<?php endif; ?>
|
||||
<p class="muted">请描述你遇到的问题或想要的功能。提交后会先进入审核,确认为有效问题后才会公开显示;<b>若经确认不是应用的问题,我们会把结论与说明通过邮件发送给你</b>(请填写常用邮箱)。</p>
|
||||
|
||||
<?php fb_flash_box($err, 'error'); ?>
|
||||
@@ -155,7 +166,7 @@ fb_layout_head('我要反馈', ['robots' => 'noindex,follow', 'software' => $sw]
|
||||
<div class="chips">
|
||||
<?php foreach ($swList as $one): ?>
|
||||
<a class="chip <?php echo ((int)$one['id'] === $swId) ? 'on' : ''; ?>"
|
||||
href="new.php?sw=<?php echo e(urlencode((string)$one['slug'])); ?>"><?php echo e((string)$one['name']); ?></a>
|
||||
href="new.php?sw=<?php echo e(urlencode((string)$one['slug'])); ?>"><?php echo fb_software_badge($one, 'sm'); ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -161,7 +161,7 @@ fb_layout_head(fb_excerpt($thread['title'], 50), ['software' => $thSwForColor]);
|
||||
$thSw = !empty($thread['software_id']) ? fb_software((int)$thread['software_id']) : null;
|
||||
$thCat = !empty($thread['category_id']) ? fb_category((int)$thread['category_id']) : null;
|
||||
?>
|
||||
<?php if ($thSw): ?><span class="badge sw"><?php echo e((string)$thSw['name']); ?></span><?php endif; ?>
|
||||
<?php if ($thSw): ?><span class="badge sw"><?php echo fb_software_badge($thSw, 'sm'); ?></span><?php endif; ?>
|
||||
<?php if ($thCat): ?><span class="badge cat"><?php echo e((string)$thCat['name']); ?></span><?php endif; ?>
|
||||
<span class="badge st st-<?php echo e($thread['status']); ?>"><?php echo e(fb_status_label($thread['status'])); ?></span>
|
||||
<h1 class="subject-title"><?php echo e($thread['title']); ?></h1>
|
||||
|
||||
Reference in New Issue
Block a user