用户:「管理后台的待审核和全部问题那个列表里面也是一样的。
标题应该顶头,然后在标题下面另起一行,写上来自于哪一个软件、
它的状态是什么、它是什么分类。点到一个具体的反馈里面也是一样的,
标题顶头,然后把同步日历、bug、待处理往下一行。管理后台也是全部按照这种方式去处理。」
改三处:
1. fb_admin_pending_row(待审核列表)
prow-top(标签在前+标题)→ prow-head(标题独占一行)+ prow-tags(软件/分类/状态)
2. 全部问题列表
item-top → item-head + item-tags
🔴 同时补上软件名(fb_software_chip)—— 后台是跨软件视图,
前台某个软件内部不需要显示软件名(满页同一个),后台必须显示。
3. 后台详情页头部
item-top → item-head + item-tags,标题字号 19px 略大于列表
样式新增 .prow-head / .prow-tags(.prow-top 保留给操作日志,
日志条目只有「动作+编号」,单行反而合适)。
验证(真实渲染 + 浏览器测量,不登录后台、不动数据):
待审核行 标题 top=87 left=222 / 标签 top=119 left=222 → 标签在下方、左对齐 0px 偏差
全部问题 标题 top=318 left=223 / 标签 top=351 left=223 → 同上
详情头 标题 top=541 left=228 / 标签 top=575 left=228 → 同上
另外静态检查:三处的 head 段内均无状态标签,tags 段内均含软件/分类/状态。
前台 5 个路由 200,未受影响。
1501 lines
84 KiB
PHP
1501 lines
84 KiB
PHP
<?php
|
||
/**
|
||
* 反馈管理后台
|
||
* ------------------------------------------------------------------
|
||
* 管理员用口令登录后可以:
|
||
* · 审核:通过(公开)/ 关闭(判定不是问题,邮件告知提问人)/ 撤下
|
||
* · 管理:以官方身份回复、改处理状态与可见性、删除问题
|
||
* · 设置:附件数量与大小上限、附件存储方式(关闭 / 本机硬盘 / 阿里云 OSS)、
|
||
* OSS 桶信息与鉴权方式(ECS 实例 RAM 角色 或 静态 AccessKey)
|
||
* · 修改自己的登录口令
|
||
*
|
||
* 入口:/feedback/admin.php
|
||
*/
|
||
require __DIR__ . '/lib/bootstrap.php';
|
||
require __DIR__ . '/lib/admin_taxonomy.php';
|
||
|
||
$pdo = fb_db();
|
||
|
||
/* ---------------- 退出 ---------------- */
|
||
if (isset($_GET['logout'])) {
|
||
unset($_SESSION['fb_admin'], $_SESSION['fb_admin_at']);
|
||
fb_redirect('admin.php');
|
||
}
|
||
|
||
/* ---------------- 登录 ---------------- */
|
||
$loginErr = '';
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
|
||
$user = isset($_POST['admin_user']) ? (string)$_POST['admin_user'] : '';
|
||
$pw = isset($_POST['admin_password']) ? (string)$_POST['admin_password'] : '';
|
||
$cs = isset($_POST['csrf']) ? (string)$_POST['csrf'] : '';
|
||
if (!fb_csrf_ok($cs)) {
|
||
$loginErr = '会话已过期,请重试。';
|
||
} elseif (!fb_rate_check('admin_login', 10)) {
|
||
$loginErr = '尝试次数过多,请一小时后再试。';
|
||
} else {
|
||
$rc = fb_admin_verify_login($user, $pw);
|
||
if ($rc === 0) {
|
||
fb_admin_login();
|
||
fb_mod_log('login');
|
||
fb_redirect('admin.php');
|
||
} elseif ($rc === 2) {
|
||
$loginErr = '本系统还没有配置管理员账号,请先运行安装向导(install.php)完成设置。';
|
||
} else {
|
||
fb_rate_hit('admin_login');
|
||
// 不区分「用户名错」还是「口令错」,避免被用来探测账号
|
||
$loginErr = '用户名或口令不正确。';
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!fb_admin_logged_in()) {
|
||
fb_layout_head('管理登录', ['robots' => 'noindex,nofollow']);
|
||
?>
|
||
<div class="crumbs"><a href="index.php">← 返回问题列表</a></div>
|
||
<section class="narrow">
|
||
<h1>管理登录</h1>
|
||
<p class="muted">请输入管理员用户名与口令<?php if (fb_admin_password_source() === 'file'): ?>(当前口令仍是 <code>config.php</code> 里的明文,登录后请到「站点设置」改成加密存储)<?php endif; ?>。</p>
|
||
<?php if (fb_admin_password_source() === 'none'): ?>
|
||
<div class="flash error">尚未配置管理员账号,且出厂占位口令已被禁用。请运行 <a href="install.php">install.php</a> 完成初始化。</div>
|
||
<?php endif; ?>
|
||
<?php if (fb_admin_username() === '' && fb_admin_password_is_set()): ?>
|
||
<div class="flash error">本站只设了口令、没有用户名(旧版安装)。请运行 <a href="install.php">install.php</a> 补一个用户名;在那之前用户名可留空、只填口令。</div>
|
||
<?php endif; ?>
|
||
<?php fb_flash_box($loginErr, 'error'); ?>
|
||
<form method="post" action="admin.php" class="card-form">
|
||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||
<label class="field">
|
||
<span>管理员用户名</span>
|
||
<input type="text" name="admin_user" autocomplete="username" required
|
||
value="<?php echo e(isset($_POST['admin_user']) ? (string)$_POST['admin_user'] : ''); ?>">
|
||
</label>
|
||
<label class="field">
|
||
<span>管理员口令</span>
|
||
<input type="password" name="admin_password" autocomplete="current-password" required>
|
||
</label>
|
||
<div class="form-actions">
|
||
<button class="btn primary" type="submit" name="login" value="1">登录</button>
|
||
</div>
|
||
</form>
|
||
</section>
|
||
<?php
|
||
fb_layout_foot();
|
||
exit;
|
||
}
|
||
|
||
/* ==================================================================
|
||
* 已登录:处理各种操作
|
||
* ================================================================== */
|
||
$msg = '';
|
||
$msgType = 'ok';
|
||
|
||
/** 取一条主题 */
|
||
function fb_admin_thread(PDO $pdo, $id) {
|
||
$st = $pdo->prepare('SELECT * FROM threads WHERE id = ?');
|
||
$st->execute([(int)$id]);
|
||
$t = $st->fetch();
|
||
return is_array($t) ? $t : null;
|
||
}
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) {
|
||
$cs = isset($_POST['csrf']) ? (string)$_POST['csrf'] : '';
|
||
if (!fb_csrf_ok($cs)) {
|
||
$msg = '会话已过期,请重试。';
|
||
$msgType = 'error';
|
||
} else {
|
||
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
|
||
$act = (string)$_POST['act'];
|
||
$t = fb_admin_thread($pdo, $id);
|
||
|
||
// 这些动作不针对某一条反馈(不依赖 $t),单独放行
|
||
$noThread = in_array($act, ['settings', 'password', 'mail_test'], true)
|
||
|| fb_admin_is_taxonomy_act($act);
|
||
|
||
if (!$t && !$noThread) {
|
||
$msg = '问题不存在。';
|
||
$msgType = 'error';
|
||
|
||
} elseif ($act === 'approve') {
|
||
/* 「通过并公开」= 两件事一起做:
|
||
* ① 可见性: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_status_label($newStatus) . ')');
|
||
|
||
// 通知提交人「已发布」(工单 #2)
|
||
$errA = '';
|
||
try {
|
||
// 用更新后的状态渲染邮件,否则信里还写着「未公开」
|
||
$fresh = fb_admin_thread($pdo, $id);
|
||
list($okA, $errA) = fb_notify_event($pdo, 'approve', $fresh ? $fresh : $t);
|
||
} catch (Throwable $e) { $errA = '发信异常'; }
|
||
$msg = '#' . $id . ' 已通过审核并公开,状态为「' . fb_status_label($newStatus) . '」。'
|
||
. ($errA === '' ? '已发邮件通知提交人。' : '(邮件未发出:' . $errA . ')');
|
||
|
||
} elseif ($act === 'close') {
|
||
$note = trim(isset($_POST['note']) ? (string)$_POST['note'] : '');
|
||
if ($note === '') {
|
||
$msg = '关闭前请先填写给提问人的说明(这段内容会随邮件发出)。';
|
||
$msgType = 'error';
|
||
} else {
|
||
$pdo->prepare("UPDATE threads SET status = 'closed', visible = 0, mod_note = ?, moderated_at = ? WHERE id = ?")
|
||
->execute([$note, time(), $id]);
|
||
list($sent, $err) = fb_notify_closed($pdo, $t, $note);
|
||
fb_mod_log('close', $id, '关闭并邮件告知:' . ($sent ? '已发信' : '未发信(' . $err . ')'));
|
||
$msg = $sent
|
||
? ('#' . $id . ' 已关闭,说明邮件已发送给 ' . (string)$t['author_email'] . '。')
|
||
: ('#' . $id . ' 已关闭,但邮件未发出:' . $err);
|
||
$msgType = $sent ? 'ok' : 'error';
|
||
}
|
||
|
||
} elseif ($act === 'hide') {
|
||
$pdo->prepare('UPDATE threads SET visible = 0, moderated_at = ? WHERE id = ?')->execute([time(), $id]);
|
||
fb_mod_log('hide', $id, '撤下(转为不公开)');
|
||
|
||
$noteH = trim((string)(isset($_POST['note']) ? $_POST['note'] : ''));
|
||
$errH = '';
|
||
try {
|
||
list($okH, $errH) = fb_notify_event($pdo, 'hide', $t, ['note' => $noteH]);
|
||
} catch (Throwable $e) { $errH = '发信异常'; }
|
||
$msg = '#' . $id . ' 已撤下(转为不公开)。'
|
||
. ($errH === '' ? '已发邮件通知提交人。' : '(邮件未发出:' . $errH . ')');
|
||
|
||
} elseif ($act === 'setstatus') {
|
||
/* 问题详情页顶部那一排按钮走的动作。
|
||
*
|
||
* 与 status 的区别:
|
||
* - status 是「状态下拉框 + 公开勾选」的完整表单,需要手动选
|
||
* - setstatus 是「点一下立刻生效」,状态与可见性由按钮本身决定
|
||
*
|
||
* 「通过并公开」= 可见 + 状态保持;其余状态只改状态不动可见性,
|
||
* 避免误把已公开的问题撤下来(用户没这个意图)。
|
||
*/
|
||
$statuses = fb_statuses();
|
||
$new = (string)(isset($_POST['status']) ? $_POST['status'] : '');
|
||
$note = trim((string)(isset($_POST['note']) ? $_POST['note'] : ''));
|
||
$alsoShow = !empty($_POST['also_show']); // 「通过并公开」置 1
|
||
|
||
if (!isset($statuses[$new])) {
|
||
$msg = '未知的状态。';
|
||
$msgType = 'error';
|
||
} else {
|
||
$old = (string)$t['status'];
|
||
$vis = $alsoShow ? 1 : (int)$t['visible'];
|
||
$pdo->prepare('UPDATE threads SET status = ?, visible = ?, updated_at = ? WHERE id = ?')
|
||
->execute([$new, $vis, time(), $id]);
|
||
fb_mod_log('status', $id,
|
||
'一键改为 ' . $statuses[$new] . ($alsoShow ? ',并公开' : '')
|
||
. ($note !== '' ? ',说明:' . fb_excerpt($note, 60) : ''));
|
||
|
||
$errS = '';
|
||
if ($old !== $new || $alsoShow) {
|
||
try {
|
||
// 「通过并公开」用 approve 事件(文案是「已发布」)
|
||
$reason = $alsoShow ? 'approve' : 'status';
|
||
list($okS, $errS) = fb_notify_event($pdo, $reason, $t, [
|
||
'old_status' => $old,
|
||
'new_status' => $new,
|
||
'note' => $note,
|
||
]);
|
||
} catch (Throwable $e) { $errS = '发信异常'; }
|
||
}
|
||
$msg = '#' . $id . ' 已改为「' . $statuses[$new] . '」'
|
||
. ($alsoShow ? '并公开显示' : '') . '。'
|
||
. ($old !== $new || $alsoShow
|
||
? ($errS === '' ? '已发邮件通知提交人。' : '(邮件未发出:' . $errS . ')')
|
||
: '状态未变化,未发信。');
|
||
$msgType = $errS === '' ? 'ok' : 'error';
|
||
}
|
||
|
||
} elseif ($act === 'status') {
|
||
$statuses = fb_statuses();
|
||
$new = (string)(isset($_POST['status']) ? $_POST['status'] : '');
|
||
if (!isset($statuses[$new])) {
|
||
$msg = '未知的状态。';
|
||
$msgType = 'error';
|
||
} else {
|
||
$vis = isset($_POST['visible']) ? 1 : 0;
|
||
$old = (string)$t['status'];
|
||
$note = trim((string)(isset($_POST['note']) ? $_POST['note'] : ''));
|
||
$pdo->prepare('UPDATE threads SET status = ?, visible = ?, updated_at = ? WHERE id = ?')
|
||
->execute([$new, $vis, time(), $id]);
|
||
fb_mod_log('status', $id, '状态改为 ' . $statuses[$new] . ',公开=' . $vis);
|
||
|
||
// 状态变了才发信(没变就别打扰用户)
|
||
$errS = '';
|
||
if ($old !== $new) {
|
||
try {
|
||
list($okS, $errS) = fb_notify_event($pdo, 'status', $t, [
|
||
'old_status' => $old,
|
||
'new_status' => $new,
|
||
'note' => $note,
|
||
]);
|
||
} catch (Throwable $e) { $errS = '发信异常'; }
|
||
}
|
||
$msg = '#' . $id . ' 已更新:状态「' . $statuses[$new] . '」,'
|
||
. ($vis ? '公开' : '不公开') . '。'
|
||
. ($old !== $new
|
||
? ($errS === '' ? '已发邮件通知提交人。' : '(邮件未发出:' . $errS . ')')
|
||
: '状态未变化,未发信。');
|
||
}
|
||
|
||
} elseif ($act === 'reply') {
|
||
$body = trim(isset($_POST['body']) ? (string)$_POST['body'] : '');
|
||
if (!fb_valid_body($body)) {
|
||
$msg = '回复内容需要 2~5000 个字符。';
|
||
$msgType = 'error';
|
||
} else {
|
||
$now = time();
|
||
$statuses = fb_statuses();
|
||
$new = (string)(isset($_POST['status']) ? $_POST['status'] : '');
|
||
$setStatus = isset($statuses[$new]) ? $new : null;
|
||
|
||
$ins = $pdo->prepare('INSERT INTO replies(thread_id, body, author_name, author_email, is_admin, created_at, ip_hash) VALUES(?,?,?,?,?,?,?)');
|
||
$ins->execute([$id, $body, '官方', null, 1, $now, fb_ip_hash()]);
|
||
$replyId = (int)$pdo->lastInsertId();
|
||
|
||
if ($setStatus !== null) {
|
||
$pdo->prepare('UPDATE threads SET reply_count = reply_count + 1, updated_at = ?, status = ? WHERE id = ?')
|
||
->execute([$now, $setStatus, $id]);
|
||
} else {
|
||
$pdo->prepare('UPDATE threads SET reply_count = reply_count + 1, updated_at = ? WHERE id = ?')
|
||
->execute([$now, $id]);
|
||
}
|
||
|
||
$replyRow = [
|
||
'id' => $replyId,
|
||
'body' => $body,
|
||
'author_name' => '官方',
|
||
'author_email' => null,
|
||
'is_admin' => 1,
|
||
];
|
||
$err2 = fb_notify_reply($pdo, $t, $replyRow, true);
|
||
fb_mod_log('reply', $id, '官方回复' . ($setStatus !== null ? ',状态改为 ' . $statuses[$setStatus] : ''));
|
||
$msg = '#' . $id . ' 已以官方身份回复' . ($err2 === '' ? '' : '(邮件:' . $err2 . ')') . '。';
|
||
}
|
||
|
||
} elseif ($act === 'delete') {
|
||
$st = $pdo->prepare('SELECT oss_key FROM attachments WHERE thread_id = ?');
|
||
$st->execute([$id]);
|
||
foreach ($st->fetchAll() as $r) { fb_attach_delete((string)$r['oss_key']); }
|
||
$pdo->prepare('DELETE FROM threads WHERE id = ?')->execute([$id]);
|
||
fb_mod_log('delete', $id, '删除问题:' . fb_excerpt((string)$t['title'], 40));
|
||
$msg = '#' . $id . ' 已删除。';
|
||
|
||
} elseif ($act === 'mail_test') {
|
||
// 发送测试邮件:只验证 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', '轻反馈');
|
||
|
||
// 选中的软件(没选就用默认软件,保证测试邮件也带软件标识)
|
||
$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';
|
||
} else {
|
||
$msg = '发送失败:' . $err;
|
||
$msgType = 'error';
|
||
}
|
||
}
|
||
|
||
} elseif (fb_admin_is_taxonomy_act($act)) {
|
||
list($msg, $msgType) = fb_admin_taxonomy_action($pdo, $act, $_POST);
|
||
|
||
} elseif ($act === 'settings') {
|
||
$msg = fb_admin_save_settings();
|
||
|
||
} elseif ($act === 'password') {
|
||
$old = (string)(isset($_POST['old_password']) ? $_POST['old_password'] : '');
|
||
$n1 = (string)(isset($_POST['new_password']) ? $_POST['new_password'] : '');
|
||
$n2 = (string)(isset($_POST['new_password2']) ? $_POST['new_password2'] : '');
|
||
// 从没设过口令(或仍是出厂占位值)时,跳过"当前口令"校验 —— 否则谁也进不来、又改不了
|
||
$firstSet = !fb_admin_password_is_set();
|
||
if (!$firstSet && !fb_admin_verify_password($old)) {
|
||
$msg = '当前口令不正确。';
|
||
$msgType = 'error';
|
||
} elseif (fb_len($n1) < 8) {
|
||
$msg = '新口令至少 8 个字符。';
|
||
$msgType = 'error';
|
||
} elseif ($n1 !== $n2) {
|
||
$msg = '两次输入的新口令不一致。';
|
||
$msgType = 'error';
|
||
} else {
|
||
fb_admin_set_password($n1);
|
||
fb_mod_log('password', null, $firstSet ? '首次设定登录口令(bcrypt 哈希存库)' : '修改登录口令');
|
||
$msg = $firstSet ? '口令已设定,下次登录请使用它。' : '口令已修改,下次登录请使用新口令。';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存站点设置(把表单值写进 settings 表;config.php 仍是出厂默认值)
|
||
* @return string 提示语
|
||
*/
|
||
function fb_admin_save_settings() {
|
||
// PHP 会把表单字段名里的「.」替换成「_」,所以两种写法都读一遍
|
||
$post = function ($k, $def = null) {
|
||
if (isset($_POST[$k])) { return $_POST[$k]; }
|
||
$alt = str_replace('.', '_', $k);
|
||
if (isset($_POST[$alt])) { return $_POST[$alt]; }
|
||
return $def;
|
||
};
|
||
|
||
/**
|
||
* 这个字段本次请求到底有没有被提交?
|
||
*
|
||
* 🔴 为什么必须有这个:后台有多个 tab(站点设置 / 邮件通知),
|
||
* 但它们**共用同一个保存处理**(都提交 act=settings)。
|
||
* 如果在站点设置页点保存,该表单里根本没有 smtp_* 字段,
|
||
* `$post('smtp.host', '')` 会取到空串,然后被写进数据库 ——
|
||
* **把用户之前填好的邮件配置清空**。
|
||
*
|
||
* 同类事故还会发生在 checkbox 上:没提交 = 视为取消勾选,
|
||
* 于是另一个 tab 保存一次就把通知开关全关掉。
|
||
*
|
||
* 所以写库前一律先问 has():没提交就跳过,绝不写空值。
|
||
*/
|
||
$has = function ($k) {
|
||
if (array_key_exists($k, $_POST)) { return true; }
|
||
return array_key_exists(str_replace('.', '_', $k), $_POST);
|
||
};
|
||
|
||
/**
|
||
* 🔴🔴 受守卫的写库:**字段没提交就一个字都不写**。
|
||
*
|
||
* 这是本文件唯一允许写设置的入口(新增设置项也必须走它)。
|
||
* 为什么要把这件事做成强制:
|
||
* 这个 bug 已经犯过三次 —— 每次都是「某个 tab 保存时,
|
||
* 另一个 tab 的字段没提交,被当成空值写回去」。
|
||
* 根因不是某个字段忘了判断,而是**写入入口本身没有约束**:
|
||
* 32 处 fb_set_setting 只有 8 处手写了 has(),漏一处就是一次事故。
|
||
*
|
||
* 用法:
|
||
* $save('oss.bucket', trim((string)$post('oss.bucket', '')));
|
||
* $save('oss.enabled', $storage === 'oss' ? 1 : 0);
|
||
* 字段没提交 → 直接 return false,数据库不动。
|
||
*
|
||
* ⚠️ 少数场景确实要在「字段没提交」时也写(例如用户主动清空某项),
|
||
* 那时用 $saveForce(),但必须在注释里写清楚为什么。
|
||
*/
|
||
$save = function ($k, $v) use ($has) {
|
||
if (!$has($k)) { return false; } // ← 铁律:没提交就不动
|
||
return fb_set_setting($k, $v);
|
||
};
|
||
|
||
/** 显式强制写入(字段没提交也写),仅在「清空某项」这类确有必要时用 */
|
||
$saveForce = function ($k, $v) {
|
||
return fb_set_setting($k, $v);
|
||
};
|
||
|
||
// ============ 站点设置组(含站点信息/页脚/附件/OSS)============
|
||
// 🔴 整组包在 has() 判断里:邮件页只提交邮件字段,没有 site_*/oss_*/upload_*,
|
||
// 无条件写入会把它们全清空。反过来也一样。
|
||
$isSiteForm = $has('oss_endpoint') || $has('site_name')
|
||
|| $has('attachment_storage') || $has('site_desc');
|
||
if ($isSiteForm) {
|
||
|
||
// 站点信息(对外展示,用于说明「本系统为哪个软件提供反馈服务」)
|
||
// 留空 = 删除该设置项,回落到 config.php 的出厂值,便于恢复默认。
|
||
foreach (['site_name', 'site_desc'] as $k) {
|
||
if (!$has($k)) { continue; }
|
||
$v = trim((string)$post($k, ''));
|
||
if ($v === '') { fb_unset_setting($k); }
|
||
else { $save($k, $v); }
|
||
}
|
||
|
||
// 页脚:是否显示「由 FeedLite 驱动」这句版权,以及链接指向的开源仓库地址
|
||
$save('footer.show_credit', $post('footer_show_credit') ? 1 : 0);
|
||
$curl = trim((string)$post('footer_credit_url', ''));
|
||
// 只接受 http/https:这条会被直接渲染成 <a href>,不能让 javascript:/data: 混进来
|
||
if ($curl !== '' && !preg_match('~^https?://~i', $curl)) {
|
||
return '页脚链接地址需以 http:// 或 https:// 开头。';
|
||
}
|
||
if ($curl === '' || $curl === fb_credit_url_default()) { fb_unset_setting('footer.credit_url'); }
|
||
else { $save('footer.credit_url', $curl); }
|
||
|
||
$ints = [
|
||
// 区间下限统一改 0:0 = 不限制(见 #4)。
|
||
// 只有 image_max_side 例外 —— 边长 0 没有意义,它是压缩目标不是限制。
|
||
'upload.image_max_per_post' => [0, 1000],
|
||
'upload.video_max_per_post' => [0, 1000],
|
||
'upload.retention_days' => [0, 36500],
|
||
'upload.upload_per_hour' => [0, 100000],
|
||
'upload.image_max_side' => [480, 4096],
|
||
];
|
||
foreach ($ints as $k => $range) {
|
||
if (!$has($k)) { continue; }
|
||
$v = (int)$post($k, 0);
|
||
$v = max($range[0], min($range[1], $v));
|
||
$save($k, $v);
|
||
}
|
||
// 图片/视频上传总开关(0=不限个数后,开关必须独立存在)
|
||
$save('upload.image_on', (int)(bool)$post('upload.image_on'));
|
||
$save('upload.video_on', (int)(bool)$post('upload.video_on'));
|
||
|
||
// MB → 字节
|
||
// 下限改 0 = 不限制(见 #4)
|
||
$mbs = [
|
||
'upload.image_max_bytes' => [0, 1024],
|
||
'upload.video_max_bytes' => [0, 10240],
|
||
'upload.total_cap_bytes' => [0, 1048576],
|
||
];
|
||
foreach ($mbs as $k => $range) {
|
||
if (!$has($k)) { continue; } // 没提交就不动(防跨 tab 清空)
|
||
$mb = (float)$post($k, 0);
|
||
$mb = max($range[0], min($range[1], $mb));
|
||
$save($k, (int)round($mb * 1048576));
|
||
}
|
||
|
||
// 附件存储方式:只有表单真的带了这个字段才处理。
|
||
// 没有它(比如只提交了 site_name 的局部表单)就整段跳过 ——
|
||
// 否则下面 $post(..., 'off') 的默认值会把存储方式误改成「关闭」。
|
||
if ($has('attachment.storage')) {
|
||
$storage = (string)$post('attachment.storage', 'off');
|
||
if (!in_array($storage, ['off', 'local', 'oss'], true)) { $storage = 'off'; }
|
||
$save('attachment.storage', $storage);
|
||
|
||
// OSS 模式下 oss.enabled 必须跟着开;其它模式关掉,避免误判。
|
||
// 🔴 这里必须用 $saveForce,不能用 $save:
|
||
// oss.enabled 是【派生字段】—— 表单里根本没有名为 oss_enabled 的 input,
|
||
// $save 的 $has() 守卫会判定"没提交"而直接 return,于是 storage 存成了 oss、
|
||
// enabled 却永远停在旧值 0。media.php 因此报「对象存储未配置」,全站图片 503。
|
||
// (2026-11-03 图片全挂的直接原因:守卫修好了跨 tab 清空,却把派生写入也挡掉了。)
|
||
// 又因为它处在 $has('attachment.storage') 分支内,不会出现「没提交却强制写 0」的反向问题。
|
||
$saveForce('oss.enabled', $storage === 'oss' ? 1 : 0);
|
||
}
|
||
|
||
$texts = ['oss.endpoint', 'oss.bucket', 'oss.instance_role', 'oss.domain', 'oss.prefix', 'moderation.close_mail_subject'];
|
||
foreach ($texts as $k) {
|
||
if (!$has($k)) { continue; } // 没提交就不动
|
||
$v = trim((string)$post($k, ''));
|
||
// 这两个留空都有"出厂默认"兜底,必须删键而不是存空串:
|
||
// - oss.prefix 留空会让对象键变成 "/2026/...",默认是 'fb'
|
||
// - close_mail_subject 留空会让邮件标题变成空字符串,
|
||
// sprintf('') 出来的主题是空的,必须回落到默认模板
|
||
if (($k === 'oss.prefix' || $k === 'moderation.close_mail_subject') && $v === '') {
|
||
fb_unset_setting($k);
|
||
continue;
|
||
}
|
||
// RAM 角色留空 = 让程序自己去元数据服务探测,不写死一个空值
|
||
if ($k === 'oss.instance_role' && $v === '') {
|
||
list($rOk, $rVal) = fb_oss_detect_instance_role();
|
||
if ($rOk) { $save($k, (string)$rVal); }
|
||
continue; // 探测不到就保持原值不动
|
||
}
|
||
$save($k, $v);
|
||
}
|
||
|
||
$src = (string)$post('oss.credential_source', 'instance_role');
|
||
$save('oss.credential_source', $src === 'static' ? 'static' : 'instance_role');
|
||
$save('oss.access_key', trim((string)$post('oss.access_key', '')));
|
||
$sk = trim((string)$post('oss.secret_key', ''));
|
||
if ($sk !== '') { $save('oss.secret_key', $sk); }
|
||
|
||
$save('oss.visibility', ((string)$post('oss.visibility', 'public')) === 'private' ? 'private' : 'public');
|
||
$save('oss.url_scheme', ((string)$post('oss.url_scheme', 'https')) === 'http' ? 'http' : 'https');
|
||
$save('oss.verify', $post('oss.verify') ? 1 : 0);
|
||
$save('oss.image_process', $post('oss.image_process') ? 1 : 0);
|
||
$save('oss.use_internal_endpoint', $post('oss.use_internal_endpoint') ? 1 : 0);
|
||
|
||
} // ← end if ($isSiteForm)
|
||
|
||
/* ---------------- 邮件通知(SMTP) ----------------
|
||
*
|
||
* 🔴🔴 必须整段包在「本页到底有没有提交邮件表单」的判断里。
|
||
*
|
||
* 事故:站点设置页和邮件通知页共用 act=settings。在站点设置页点保存时,
|
||
* 该表单里没有 smtp_* 字段,$post() 取到空串,被无条件写进数据库 ——
|
||
* **把用户已经填好的 SMTP 服务器/账号/发件地址全清空了**。
|
||
* 用户看到的现象就是「我明明配过邮件,怎么全没了、信也发不出」。
|
||
*
|
||
* 判据用 smtp_enabled:它是邮件表单里的总开关,一定存在。
|
||
* 站点设置页没有这个字段 ⇒ 整段跳过,一个键都不动。
|
||
*/
|
||
$isMailForm = $has('smtp_enabled') || $has('smtp.host') || $has('smtp_host');
|
||
if ($isMailForm) {
|
||
$smtpText = ['smtp.host', 'smtp.user', 'smtp.from', 'smtp.from_name'];
|
||
foreach ($smtpText as $k) {
|
||
if (!$has($k)) { continue; } // 没提交就不动它
|
||
$save($k, trim((string)$post($k, '')));
|
||
}
|
||
// 口令留空 = 不修改(避免每次保存都得重填)
|
||
$sp = (string)(isset($_POST['smtp_pass']) ? $_POST['smtp_pass'] : '');
|
||
if (trim($sp) !== '') { $save('smtp.pass', $sp); }
|
||
|
||
// 端口留空 = 用默认 465。删键回落,不要存成 0(0 端口根本连不上)
|
||
$portRaw = trim((string)$post('smtp.port', ''));
|
||
if ($portRaw === '') {
|
||
fb_unset_setting('smtp.port');
|
||
} else {
|
||
$smtpPort = (int)$portRaw;
|
||
if ($smtpPort <= 0 || $smtpPort > 65535) { $smtpPort = 465; }
|
||
$save('smtp.port', $smtpPort);
|
||
}
|
||
|
||
$sec = strtolower(trim((string)$post('smtp.secure', 'ssl')));
|
||
if (!in_array($sec, ['ssl', 'tls', ''], true)) { $sec = 'ssl'; }
|
||
$save('smtp.secure', $sec);
|
||
|
||
// 超时留空 = 用默认 15
|
||
$tmoRaw = trim((string)$post('smtp.timeout', ''));
|
||
if ($tmoRaw === '') {
|
||
fb_unset_setting('smtp.timeout');
|
||
} else {
|
||
$save('smtp.timeout', max(5, min(60, (int)$tmoRaw)));
|
||
}
|
||
$save('smtp.verify_ssl', (int)(bool)$post('smtp.verify_ssl'));
|
||
|
||
// 没填 from 时用 user 兜底(绝大多数 SMTP 要求两者一致)
|
||
if ($has('smtp.from') && trim((string)$post('smtp.from', '')) === '') {
|
||
$save('smtp.from', trim((string)fb_cfg('smtp.user', '')));
|
||
}
|
||
|
||
// 只有 host 填了才允许开启,否则开着一个空配置只会每次发信都失败
|
||
$wantOn = (bool)$post('smtp.enabled');
|
||
$hostNow = trim((string)fb_cfg('smtp.host', ''));
|
||
$save('smtp.enabled', ($wantOn && $hostNow !== '') ? 1 : 0);
|
||
if ($wantOn && $hostNow === '') {
|
||
$msg = '邮件通知未开启:请先填写 SMTP 服务器地址。';
|
||
$msgType = 'error';
|
||
}
|
||
|
||
/* ---------------- 通知开关 ----------------
|
||
* 全是 checkbox:未提交会被当成"取消勾选"。
|
||
* 所以只有本页确实提交了邮件表单时才写,否则在站点设置页保存一次
|
||
* 就会把「有人回复时通知」这些开关全部关掉。
|
||
*/
|
||
$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"
|
||
if ($has('notify_cooldown_seconds')) {
|
||
$cdRaw = trim((string)$post('notify.cooldown_seconds', ''));
|
||
if ($cdRaw === '') {
|
||
fb_unset_setting('notify.cooldown_seconds');
|
||
} else {
|
||
$save('notify.cooldown_seconds', max(0, min(86400, (int)$cdRaw)));
|
||
}
|
||
}
|
||
|
||
} // ← end if ($isMailForm)
|
||
|
||
// 审核开关属于「站点设置」,只在站点设置页提交时才写
|
||
if ($has('moderation_enabled')) {
|
||
$save('moderation.enabled', $post('moderation.enabled') ? 1 : 0);
|
||
}
|
||
|
||
fb_mod_log('settings', null, '保存站点设置:存储=' . $storage);
|
||
return '设置已保存。';
|
||
}
|
||
|
||
/* ==================================================================
|
||
* 页面
|
||
* ================================================================== */
|
||
$tab = (string)(isset($_GET['tab']) ? $_GET['tab'] : 'pending');
|
||
if (!in_array($tab, ['pending', 'all', 'thread', 'taxonomy', 'statuses', 'settings', 'mail', 'log'], true)) { $tab = 'pending'; }
|
||
|
||
fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
||
?>
|
||
<div class="page-head">
|
||
<div>
|
||
<h1>管理后台</h1>
|
||
<p class="muted">登录状态 <?php echo (int)floor(fb_admin_idle_ttl() / 3600); ?> 小时内有效,超时自动退出。</p>
|
||
</div>
|
||
<a class="btn" href="admin.php?logout=1">退出登录</a>
|
||
</div>
|
||
|
||
<nav class="tabs">
|
||
<a class="tab<?php echo $tab === 'pending' ? ' on' : ''; ?>" href="admin.php?tab=pending">待审核</a>
|
||
<a class="tab<?php echo $tab === 'all' ? ' on' : ''; ?>" href="admin.php?tab=all">全部问题</a>
|
||
<a class="tab<?php echo $tab === 'taxonomy' ? ' on' : ''; ?>" href="admin.php?tab=taxonomy">软件与分类</a>
|
||
<a class="tab<?php echo $tab === 'statuses' ? ' on' : ''; ?>" href="admin.php?tab=statuses">处理状态</a>
|
||
<a class="tab<?php echo $tab === 'mail' ? ' on' : ''; ?>" href="admin.php?tab=mail">邮件通知</a>
|
||
<a class="tab<?php echo $tab === 'settings' ? ' on' : ''; ?>" href="admin.php?tab=settings">站点设置</a>
|
||
<a class="tab<?php echo $tab === 'log' ? ' on' : ''; ?>" href="admin.php?tab=log">操作日志</a>
|
||
</nav>
|
||
|
||
<?php fb_flash_box($msg, $msgType === 'ok' ? 'ok' : 'error'); ?>
|
||
|
||
<?php if ($tab === 'pending'): ?>
|
||
<?php
|
||
/* 「待审核」= 还没公开过的条目。
|
||
* 判据用 status='hidden'(未公开),不再用 visible=0 这种隐式条件 ——
|
||
* 已关闭的条目 visible 也是 0,但它是处理完了,不该出现在待审核里。
|
||
* 顺便兼容老数据:visible=0 且状态仍是 'open' 的也一并算待审核。 */
|
||
$pending = $pdo->query(
|
||
"SELECT * FROM threads
|
||
WHERE status = 'hidden' OR (visible = 0 AND status = 'open')
|
||
ORDER BY created_at DESC"
|
||
)->fetchAll();
|
||
?>
|
||
<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: ?>
|
||
<div class="plist">
|
||
<?php foreach ($pending as $t) { echo fb_admin_pending_row($t); } ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php elseif ($tab === 'all'): ?>
|
||
<?php
|
||
$where = [];
|
||
$args = [];
|
||
$stF = (string)(isset($_GET['status']) ? $_GET['status'] : '');
|
||
$visF = (string)(isset($_GET['vis']) ? $_GET['vis'] : '');
|
||
$catF = (string)(isset($_GET['cat']) ? $_GET['cat'] : '');
|
||
$qF = trim((string)(isset($_GET['q']) ? $_GET['q'] : ''));
|
||
$swF = isset($_GET['sw']) ? (int)$_GET['sw'] : 0;
|
||
$cidF = isset($_GET['cid']) ? (int)$_GET['cid'] : 0;
|
||
if ($stF !== '') { $where[] = 'status = ?'; $args[] = $stF; }
|
||
if ($visF === '1' || $visF === '0') { $where[] = 'visible = ?'; $args[] = (int)$visF; }
|
||
if ($swF > 0) { $where[] = 'software_id = ?'; $args[] = $swF; }
|
||
if ($cidF > 0) { $where[] = 'category_id = ?'; $args[] = $cidF; }
|
||
if ($qF !== '') { $where[] = '(title LIKE ? OR body LIKE ?)'; $args[] = '%' . $qF . '%'; $args[] = '%' . $qF . '%'; }
|
||
$wSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
|
||
|
||
$cntSt = $pdo->prepare('SELECT COUNT(*) FROM threads ' . $wSql);
|
||
$cntSt->execute($args);
|
||
$total = (int)$cntSt->fetchColumn();
|
||
$pageSize = max(5, (int)fb_cfg('page_size', 20));
|
||
$pages = max(1, (int)ceil($total / $pageSize));
|
||
$page = min(max(1, (int)(isset($_GET['page']) ? $_GET['page'] : 1)), $pages);
|
||
$offset = ($page - 1) * $pageSize;
|
||
|
||
$lst = $pdo->prepare('SELECT * FROM threads ' . $wSql . ' ORDER BY updated_at DESC LIMIT ' . (int)$pageSize . ' OFFSET ' . (int)$offset);
|
||
$lst->execute($args);
|
||
$rows = $lst->fetchAll();
|
||
?>
|
||
<form class="filters" method="get" action="admin.php">
|
||
<input type="hidden" name="tab" value="all">
|
||
<input type="text" name="q" value="<?php echo e($qF); ?>" placeholder="搜索标题或正文">
|
||
<select name="sw" onchange="this.form.submit()">
|
||
<option value="0">全部软件</option>
|
||
<?php foreach (fb_softwares(false) as $sw1): ?>
|
||
<option value="<?php echo (int)$sw1['id']; ?>"<?php echo $swF === (int)$sw1['id'] ? ' selected' : ''; ?>><?php echo e((string)$sw1['name']); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<select name="status">
|
||
<option value="">全部状态</option>
|
||
<?php foreach (fb_status_keys() as $k => $label): ?>
|
||
<option value="<?php echo e($k); ?>"<?php echo $stF === $k ? ' selected' : ''; ?>><?php echo e($label); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<select name="cid">
|
||
<option value="0">全部分类</option>
|
||
<?php
|
||
// 选了某个软件就只列它的分类;否则把所有软件的分类按软件分组列出
|
||
$catSwList = $swF > 0 ? [fb_software($swF)] : fb_softwares(false);
|
||
foreach ($catSwList as $csw1):
|
||
if (!$csw1) { continue; }
|
||
foreach (fb_categories((int)$csw1['id'], false) as $c1):
|
||
?>
|
||
<option value="<?php echo (int)$c1['id']; ?>"<?php echo $cidF === (int)$c1['id'] ? ' selected' : ''; ?>><?php echo e(($swF > 0 ? '' : (string)$csw1['name'] . ' · ') . (string)$c1['name']); ?></option>
|
||
<?php endforeach; endforeach; ?>
|
||
</select>
|
||
<select name="vis">
|
||
<option value="">公开与否不限</option>
|
||
<option value="1"<?php echo $visF === '1' ? ' selected' : ''; ?>>已公开</option>
|
||
<option value="0"<?php echo $visF === '0' ? ' selected' : ''; ?>>未公开</option>
|
||
</select>
|
||
<button class="btn" type="submit">筛选</button>
|
||
<a class="btn" href="admin.php?tab=all">重置</a>
|
||
</form>
|
||
|
||
<h2 style="margin:16px 0 10px">全部问题 <span class="muted">(<?php echo $total; ?> 条,第 <?php echo $page; ?>/<?php echo $pages; ?> 页)</span></h2>
|
||
|
||
<?php if (!$rows): ?>
|
||
<div class="empty"><p>没有符合条件的问题。</p></div>
|
||
<?php else: ?>
|
||
<div class="list">
|
||
<?php foreach ($rows as $t): ?>
|
||
<div class="item" style="cursor:default">
|
||
<?php
|
||
/* 标题顶头独占一行,标签(软件 / 分类 / 状态)另起一行。
|
||
* 后台是跨软件视图,所以这里必须带软件名 —— 前台某个软件内部
|
||
* 不需要显示软件名(满页都是同一个),后台需要。 */
|
||
$aSw = !empty($t['software_id']) ? fb_software((int)$t['software_id']) : null;
|
||
?>
|
||
<div class="item-head">
|
||
<a class="item-title" href="admin.php?tab=thread&id=<?php echo (int)$t['id']; ?>"><?php echo e($t['title']); ?></a>
|
||
<span class="item-id">#<?php echo (int)$t['id']; ?></span>
|
||
</div>
|
||
<div class="item-tags">
|
||
<?php if ($aSw): ?><?php echo fb_software_chip($aSw); ?><?php endif; ?>
|
||
<span class="badge cat"><?php echo e(fb_category_label($t['category'])); ?></span>
|
||
<span class="badge st st-<?php echo e($t['status']); ?>"><?php echo e(fb_status_label($t['status'])); ?></span>
|
||
<?php if ((int)$t['visible'] !== 1): ?><span class="badge st st-closed">未公开</span><?php endif; ?>
|
||
</div>
|
||
<p class="item-ex"><?php echo e(fb_excerpt((string)$t['body'], 120)); ?></p>
|
||
<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 (int)$t['reply_count']; ?> 条回复</span>
|
||
<span class="dot">·</span>
|
||
<a href="admin.php?tab=thread&id=<?php echo (int)$t['id']; ?>">管理</a>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php if ($pages > 1): ?>
|
||
<div class="pager">
|
||
<?php if ($page > 1): ?><a class="btn" href="admin.php?<?php echo e(http_build_query(array_merge($_GET, ['tab' => 'all', 'page' => $page - 1]))); ?>">上一页</a><?php endif; ?>
|
||
<?php if ($page < $pages): ?><a class="btn" href="admin.php?<?php echo e(http_build_query(array_merge($_GET, ['tab' => 'all', 'page' => $page + 1]))); ?>">下一页</a><?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
|
||
<?php elseif ($tab === 'thread'): ?>
|
||
<?php
|
||
$id = (int)(isset($_GET['id']) ? $_GET['id'] : 0);
|
||
$t = fb_admin_thread($pdo, $id);
|
||
if (!$t): ?>
|
||
<div class="empty"><p>问题不存在。<a href="admin.php?tab=all">返回列表</a></p></div>
|
||
<?php else:
|
||
$rs = $pdo->prepare('SELECT * FROM replies WHERE thread_id = ? ORDER BY created_at ASC, id ASC');
|
||
$rs->execute([$id]);
|
||
$replies = $rs->fetchAll();
|
||
$atts = fb_attach_for_thread($pdo, $id);
|
||
?>
|
||
<div class="crumbs"><a href="admin.php?tab=all">← 返回全部问题</a></div>
|
||
|
||
<div class="card-form" style="margin:0 0 16px">
|
||
<?php
|
||
$dSw = !empty($t['software_id']) ? fb_software((int)$t['software_id']) : null;
|
||
$dCat = !empty($t['category_id']) ? fb_category((int)$t['category_id']) : null;
|
||
?>
|
||
<?php /* 标题顶头独占一行,标签另起一行 —— 与列表页、前台详情页统一。
|
||
用户 2026-11-03:「点到一个具体的反馈里面也是一样的,标题顶头,
|
||
然后把同步日历、bug、待处理往下一行,标签写到下面去。」 */ ?>
|
||
<div class="item-head">
|
||
<span class="item-title" style="font-size:19px"><?php echo e($t['title']); ?></span>
|
||
<span class="item-id">#<?php echo (int)$t['id']; ?></span>
|
||
</div>
|
||
<div class="item-tags">
|
||
<?php if ($dSw): ?><?php echo fb_software_chip($dSw); ?><?php endif; ?>
|
||
<?php if ($dCat): ?><span class="badge cat"><?php echo e((string)$dCat['name']); ?></span><?php endif; ?>
|
||
<span class="badge st st-<?php echo e($t['status']); ?>"><?php echo e(fb_status_label($t['status'])); ?></span>
|
||
</div>
|
||
<div class="item-ex" style="white-space:normal"><?php echo nl2br(e($t['body'])); ?></div>
|
||
<?php if (!empty($atts['main'])) { echo fb_attach_render($atts['main']); } ?>
|
||
<div class="item-meta">
|
||
<span class="who"><?php echo e($t['author_name']); ?></span>
|
||
<span class="dot">·</span>
|
||
<span><?php echo e($t['author_email']); ?></span>
|
||
<?php if (!empty($t['author_phone'])): ?><span class="dot">·</span><span><?php echo e($t['author_phone']); ?></span><?php endif; ?>
|
||
<span class="dot">·</span>
|
||
<span><?php echo e(fb_fmt_time($t['created_at'])); ?></span>
|
||
<span class="dot">·</span>
|
||
<span><?php echo ((int)$t['visible'] === 1) ? '已公开' : '未公开'; ?></span>
|
||
</div>
|
||
</div>
|
||
|
||
<?php
|
||
/* ---------- 处理动作:一排按钮,点一下立刻生效 ----------
|
||
* 用户原话:「第一个就是通过并显示…另外一个再加一个按钮,就是说现在正在处理中。
|
||
* 还有比方说不处理,都是在那个问题后边列几个按钮不就行了吗?」
|
||
*
|
||
* 设计取舍:
|
||
* - 「通过并公开」单独成一类(它改的是可见性,不只是状态)
|
||
* - 「不予考虑」必须填说明(会随邮件发出,不能发一封没理由的信)
|
||
* - 其余状态点一下就走,不弹框 —— 高频操作不该被对话框拖慢
|
||
*/
|
||
$curStatus = (string)$t['status'];
|
||
$isVisible = ((int)$t['visible'] === 1);
|
||
|
||
// 按钮表:key=状态, label=按钮文字, cls=样式
|
||
$quickBtns = [
|
||
['key' => 'doing', 'label' => '处理中', 'cls' => ''],
|
||
['key' => 'resolved', 'label' => '已解决', 'cls' => 'ok'],
|
||
['key' => 'wontfix', 'label' => '不予考虑', 'cls' => 'warn'],
|
||
];
|
||
?>
|
||
<div class="act-bar">
|
||
<div class="act-bar-title">处理</div>
|
||
<div class="act-bar-body">
|
||
<?php if (!$isVisible): ?>
|
||
<form method="post" action="admin.php?tab=thread&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; ?>">
|
||
<?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>
|
||
<?php else: ?>
|
||
<span class="act-state on">已公开显示</span>
|
||
<?php endif; ?>
|
||
|
||
<?php foreach ($quickBtns as $qb):
|
||
$isCur = ($curStatus === $qb['key']);
|
||
if ($qb['key'] === 'wontfix'): ?>
|
||
<button type="button" class="btn act-btn <?php echo e($qb['cls']); ?><?php echo $isCur ? ' is-cur' : ''; ?>"
|
||
data-wontfix-open <?php echo $isCur ? 'disabled' : ''; ?>><?php echo e($qb['label']); ?></button>
|
||
<?php else: ?>
|
||
<form method="post" action="admin.php?tab=thread&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($qb['key']); ?>">
|
||
<button class="btn act-btn <?php echo e($qb['cls']); ?><?php echo $isCur ? ' is-cur' : ''; ?>"
|
||
type="submit" name="act" value="setstatus"<?php echo $isCur ? ' disabled' : ''; ?>><?php echo e($qb['label']); ?></button>
|
||
</form>
|
||
<?php endif; ?>
|
||
<?php endforeach; ?>
|
||
|
||
<?php if ($isVisible): ?>
|
||
<form method="post" action="admin.php?tab=thread&id=<?php echo (int)$id; ?>" class="act-inline"
|
||
onsubmit="return confirm('确定把这条件反馈从公开列表中撤下?');">
|
||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||
<input type="hidden" name="id" value="<?php echo (int)$id; ?>">
|
||
<button class="btn act-btn" type="submit" name="act" value="hide">撤下</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
|
||
<span class="act-hint">点一下立刻生效并自动发邮件通知提交人</span>
|
||
</div>
|
||
|
||
<?php /* 不予考虑:需要填说明,展开一个小面板而不是弹 window.prompt */ ?>
|
||
<form method="post" action="admin.php?tab=thread&id=<?php echo (int)$id; ?>" class="act-note" data-wontfix-panel hidden>
|
||
<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="wontfix">
|
||
<label class="field">
|
||
<span>为什么不处理 <b class="req">*</b>
|
||
<i class="hint">会作为邮件正文发给 <?php echo e((string)$t['author_email']); ?>,请写清楚原因</i></span>
|
||
<textarea name="note" rows="3" maxlength="2000" required
|
||
placeholder="例如:这个现象来自服务器端的 CalDAV 限制,并非应用本身的问题;建议在客户端调整同步间隔。"></textarea>
|
||
</label>
|
||
<div class="form-actions">
|
||
<button class="btn warn" type="submit" name="act" value="setstatus">确认不予考虑并邮件告知</button>
|
||
<button class="btn" type="button" data-wontfix-close>取消</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<?php foreach ($replies as $r): ?>
|
||
<div class="reply<?php echo ((int)$r['is_admin'] === 1) ? ' admin' : ''; ?>">
|
||
<div class="reply-head">
|
||
<span class="who"><?php echo e($r['author_name']); ?></span>
|
||
<?php if ((int)$r['is_admin'] === 1): ?><span class="badge official">官方</span><?php endif; ?>
|
||
<span class="dot">·</span>
|
||
<span><?php echo e(fb_fmt_time($r['created_at'])); ?></span>
|
||
</div>
|
||
<div class="reply-body"><?php echo nl2br(e($r['body'])); ?></div>
|
||
<?php if (isset($atts['replies'][(int)$r['id']])) { echo fb_attach_render($atts['replies'][(int)$r['id']], ['compact' => true]); } ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
|
||
<h2 style="margin:24px 0 10px">归属</h2>
|
||
<form method="post" action="admin.php?tab=thread&id=<?php echo (int)$id; ?>" class="card-form">
|
||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||
<input type="hidden" name="id" value="<?php echo (int)$id; ?>">
|
||
<p class="muted" style="margin:0 0 10px;font-size:13.5px">
|
||
挂错软件或分类时在这里改正。换软件后分类选项会跟着变,需重新选择。
|
||
</p>
|
||
<div class="row2">
|
||
<label class="field">
|
||
<span>所属软件</span>
|
||
<select name="software_id" onchange="location.href='admin.php?tab=thread&id=<?php echo (int)$id; ?>&sw='+this.value">
|
||
<?php foreach (fb_softwares(false) as $m1): ?>
|
||
<option value="<?php echo (int)$m1['id']; ?>"<?php echo (int)(isset($t['software_id']) ? $t['software_id'] : 0) === (int)$m1['id'] ? ' selected' : ''; ?>><?php echo e((string)$m1['name']); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</label>
|
||
<label class="field">
|
||
<span>分类</span>
|
||
<select name="category_id">
|
||
<option value="0">(不指定)</option>
|
||
<?php
|
||
$mvSw = isset($_GET['sw']) ? (int)$_GET['sw']
|
||
: (int)(isset($t['software_id']) ? $t['software_id'] : 0);
|
||
if ($mvSw <= 0) { $d0 = fb_default_software(); $mvSw = $d0 ? (int)$d0['id'] : 0; }
|
||
foreach (fb_categories($mvSw, false) as $m2):
|
||
?>
|
||
<option value="<?php echo (int)$m2['id']; ?>"<?php echo (int)(isset($t['category_id']) ? $t['category_id'] : 0) === (int)$m2['id'] ? ' selected' : ''; ?>><?php echo e((string)$m2['name']); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
<button class="btn" name="act" value="th_move" type="submit">保存归属</button>
|
||
</form>
|
||
|
||
<h2 style="margin:24px 0 10px">以官方身份回复</h2>
|
||
<form method="post" action="admin.php?tab=thread&id=<?php echo (int)$id; ?>" class="card-form">
|
||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||
<input type="hidden" name="id" value="<?php echo (int)$id; ?>">
|
||
<label class="field">
|
||
<span>回复内容</span>
|
||
<textarea name="body" rows="5" maxlength="5000" placeholder="写给提问人的答复;提交后会给对方发一封通知邮件"></textarea>
|
||
</label>
|
||
<label class="field">
|
||
<span>同时把状态改为</span>
|
||
<select name="status">
|
||
<option value="">(不改)</option>
|
||
<?php foreach (fb_status_keys() as $k => $label): ?>
|
||
<option value="<?php echo e($k); ?>"><?php echo e($label); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</label>
|
||
<div class="form-actions">
|
||
<button class="btn primary" type="submit" name="act" value="reply">发布官方回复</button>
|
||
</div>
|
||
</form>
|
||
|
||
<h2 style="margin:24px 0 10px">状态与可见性</h2>
|
||
<form method="post" action="admin.php?tab=thread&id=<?php echo (int)$id; ?>" class="card-form">
|
||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||
<input type="hidden" name="id" value="<?php echo (int)$id; ?>">
|
||
<div class="row2">
|
||
<label class="field">
|
||
<span>处理状态</span>
|
||
<select name="status">
|
||
<?php foreach (fb_status_keys() as $k => $label): ?>
|
||
<option value="<?php echo e($k); ?>"<?php echo ((string)$t['status'] === $k) ? ' selected' : ''; ?>><?php echo e($label); ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</label>
|
||
<label class="field">
|
||
<span>公开显示</span>
|
||
<label class="check"><input type="checkbox" name="visible" value="1"<?php echo ((int)$t['visible'] === 1) ? ' checked' : ''; ?>> 在公开列表中显示</label>
|
||
</label>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button class="btn primary" type="submit" name="act" value="status">保存</button>
|
||
<?php if ((int)$t['visible'] !== 1): ?>
|
||
<button class="btn" type="submit" name="act" value="approve">通过并公开</button>
|
||
<?php else: ?>
|
||
<button class="btn" type="submit" name="act" value="hide">撤下</button>
|
||
<?php endif; ?>
|
||
</div>
|
||
</form>
|
||
|
||
<h2 style="margin:24px 0 10px">关闭(判定不是应用问题,会发邮件给提问人)</h2>
|
||
<form method="post" action="admin.php?tab=thread&id=<?php echo (int)$id; ?>" class="card-form">
|
||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||
<input type="hidden" name="id" value="<?php echo (int)$id; ?>">
|
||
<label class="field">
|
||
<span>关闭说明 <b class="req">*</b> <i class="hint">会作为邮件正文发给 <?php echo e($t['author_email']); ?></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>
|
||
</form>
|
||
|
||
<form method="post" action="admin.php?tab=all" class="card-form" style="margin-top:24px"
|
||
onsubmit="return confirm('确定删除 #<?php echo (int)$id; ?>?该问题的回复与附件会一并删除,且不可恢复。');">
|
||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||
<input type="hidden" name="id" value="<?php echo (int)$id; ?>">
|
||
<div class="form-actions">
|
||
<button class="btn danger" type="submit" name="act" value="delete">删除这条问题</button>
|
||
</div>
|
||
</form>
|
||
<?php endif; ?>
|
||
|
||
<?php elseif ($tab === 'taxonomy'): ?>
|
||
<?php fb_admin_taxonomy_page($pdo,
|
||
isset($_GET['edit_sw']) ? (int)$_GET['edit_sw'] : 0,
|
||
isset($_GET['edit_cat']) ? (int)$_GET['edit_cat'] : 0); ?>
|
||
|
||
<?php elseif ($tab === 'statuses'): ?>
|
||
<?php fb_admin_status_page($pdo, isset($_GET['edit_st']) ? (int)$_GET['edit_st'] : 0); ?>
|
||
|
||
<?php elseif ($tab === 'settings'): ?>
|
||
<?php
|
||
$mode = fb_store_mode();
|
||
$ossStat = fb_oss_cred_status();
|
||
$mb = function ($bytes) { return round(((int)$bytes) / 1048576, 1); };
|
||
?>
|
||
<form method="post" action="admin.php?tab=settings" class="card-form">
|
||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||
|
||
<h2 style="margin:8px 0 10px">站点信息</h2>
|
||
<label class="field"><span>站点标题</span>
|
||
<input type="text" name="site_name" maxlength="80" placeholder="例如:我的软件 · 问题反馈"
|
||
value="<?php echo e(fb_form_val(fb_cfg('site_name', ''), '轻反馈', 'site_name')); ?>">
|
||
<i class="hint">显示在浏览器标签、页面顶栏与页脚。用它说明「本系统为哪个软件提供反馈服务」,如「XX 软件 · 问题反馈」。</i></label>
|
||
<label class="field"><span>站点描述</span>
|
||
<textarea name="site_desc" rows="3" maxlength="300"
|
||
placeholder="欢迎在这里反馈使用问题、提出功能建议。"><?php echo e((string)fb_cfg('site_desc', '')); ?></textarea>
|
||
<i class="hint">显示在首页标题下方,一句话说明这里可以反馈什么。两项都留空则恢复为出厂默认。</i></label>
|
||
|
||
<h2 style="margin:24px 0 10px">页脚</h2>
|
||
<label class="check"><input type="checkbox" name="footer_show_credit" value="1"<?php echo fb_cfg('footer.show_credit', true) ? ' checked' : ''; ?>> 在页脚显示「由 FeedLite 驱动」
|
||
<span class="hint" style="display:block;margin:2px 0 0 24px">取消勾选后,这句话连同链接都不再出现在页脚,只保留「管理后台」入口。</span></label>
|
||
<label class="field"><span>FeedLite 链接地址</span>
|
||
<input type="text" name="footer_credit_url" maxlength="300"
|
||
value="<?php echo e((string)fb_cfg('footer.credit_url', '') !== '' ? (string)fb_cfg('footer.credit_url') : fb_credit_url_default()); ?>">
|
||
<i class="hint">页脚「FeedLite」二字指向的地址,默认是本项目开源仓库。只接受 http:// 或 https:// 开头的网址;填回默认值即恢复。</i></label>
|
||
|
||
<h2 style="margin:24px 0 10px">附件存储</h2>
|
||
<label class="field">
|
||
<span>附件保存在哪</span>
|
||
<select name="attachment_storage">
|
||
<option value="off"<?php echo $mode === 'off' ? ' selected' : ''; ?>>关闭(不开放上传)</option>
|
||
<option value="local"<?php echo $mode === 'local' ? ' selected' : ''; ?>>本机硬盘(站点 uploads/ 目录)</option>
|
||
<option value="oss"<?php echo $mode === 'oss' ? ' selected' : ''; ?>>阿里云 OSS(私有桶 · 服务器代理)</option>
|
||
</select>
|
||
<i class="hint">本机:文件落在站点 uploads/ 目录;OSS:文件经服务器代理写入私有桶(走内网 Endpoint),公网无法直连桶。两种方式都需要服务器磁盘能放下单个上传文件。<b>前台展示一律经 media.php 代理</b>——无论存哪,页面上的图片/视频地址都是 <code>media.php?k=…</code>,不暴露 uploads/ 真实路径,也不出现任何 OSS 桶信息。</i>
|
||
</label>
|
||
|
||
<?php
|
||
// 所有「限制类」输入框:min=0,填 0 = 不限制(见 #4)。
|
||
// 只有 image_max_side 例外 —— 它是压缩目标边长,0 没有意义。
|
||
$zero = '<i class="hint">填 0 表示不限制。</i>';
|
||
?>
|
||
<label class="check"><input type="checkbox" name="upload_image_on" value="1"<?php echo fb_upload_cfg('image_on', true) ? ' checked' : ''; ?>> 允许上传图片
|
||
<span class="hint" style="display:block;margin-left:24px">关掉后前台不再显示图片上传入口。与下面「填 0 不限个数」互不干扰。</span></label>
|
||
<label class="check"><input type="checkbox" name="upload_video_on" value="1"<?php echo fb_upload_cfg('video_on', true) ? ' checked' : ''; ?>> 允许上传视频</label>
|
||
|
||
<div class="grid2" style="margin-top:10px">
|
||
<label class="field"><span>每个问题最多几张图片</span>
|
||
<input type="number" name="upload_image_max_per_post" min="0" max="1000"
|
||
value="<?php echo (int)fb_upload_cfg('image_max_per_post', 4); ?>">
|
||
<?php echo $zero; ?></label>
|
||
<label class="field"><span>单张图片上限(MB)</span>
|
||
<input type="number" step="any" min="0" max="1024" name="upload_image_max_bytes"
|
||
value="<?php echo $mb(fb_upload_cfg('image_max_bytes', 5242880)); ?>">
|
||
<?php echo $zero; ?><i class="hint">填 0 时注意:服务器磁盘可能被大图写满。</i></label>
|
||
<label class="field"><span>每个问题最多几个视频</span>
|
||
<input type="number" name="upload_video_max_per_post" min="0" max="1000"
|
||
value="<?php echo (int)fb_upload_cfg('video_max_per_post', 1); ?>">
|
||
<?php echo $zero; ?></label>
|
||
<label class="field"><span>单个视频上限(MB)</span>
|
||
<input type="number" step="any" min="0" max="10240" name="upload_video_max_bytes"
|
||
value="<?php echo $mb(fb_upload_cfg('video_max_bytes', 52428800)); ?>">
|
||
<?php echo $zero; ?><i class="hint">填 0 时注意:服务器磁盘可能被大视频写满。</i></label>
|
||
<label class="field"><span>全站附件总量上限(MB)</span>
|
||
<input type="number" step="any" min="0" max="1048576" name="upload_total_cap_bytes"
|
||
value="<?php echo $mb(fb_upload_cfg('total_cap_bytes', 1073741824)); ?>">
|
||
<?php echo $zero; ?><i class="hint">填 0 表示不限总量,磁盘写满后上传会失败。</i></label>
|
||
<label class="field"><span>附件保留天数</span>
|
||
<input type="number" name="upload_retention_days" min="0" max="36500"
|
||
value="<?php echo (int)fb_upload_cfg('retention_days', 365); ?>">
|
||
<?php echo $zero; ?><i class="hint">填 0 = 永不自动清理。</i></label>
|
||
<label class="field"><span>每 IP 每小时最多上传几次</span>
|
||
<input type="number" name="upload_upload_per_hour" min="0" max="100000"
|
||
value="<?php echo (int)fb_upload_cfg('upload_per_hour', 60); ?>">
|
||
<?php echo $zero; ?></label>
|
||
<label class="field"><span>图片压缩后的最长边(像素)</span>
|
||
<input type="number" name="upload_image_max_side" min="480" max="4096"
|
||
value="<?php echo (int)fb_upload_cfg('image_max_side', 1600); ?>">
|
||
<i class="hint">这是压缩目标边长,不是限制项,最小 480。</i></label>
|
||
</div>
|
||
|
||
<h2 style="margin:24px 0 10px">阿里云 OSS(选「OSS」时才需要填)</h2>
|
||
<p class="muted small" style="margin:-4px 0 12px">私有桶 + 服务器代理:浏览器不直连桶,附件读写都经本站 upload.php / media.php 中转。Endpoint 填公网或内网形式都可以,程序按下方开关自动转换。</p>
|
||
<div class="grid2">
|
||
<label class="field"><span>Endpoint</span>
|
||
<input type="text" name="oss_endpoint" placeholder="oss-cn-qingdao.aliyuncs.com" value="<?php echo e((string)fb_cfg('oss.endpoint', '')); ?>"></label>
|
||
<label class="field"><span>Bucket 名</span>
|
||
<input type="text" name="oss_bucket" value="<?php echo e((string)fb_cfg('oss.bucket', '')); ?>"></label>
|
||
<label class="field"><span>鉴权方式</span>
|
||
<select name="oss_credential_source">
|
||
<option value="instance_role"<?php echo fb_oss_credential_source() === 'instance_role' ? ' selected' : ''; ?>>ECS 实例 RAM 角色(推荐)</option>
|
||
<option value="static"<?php echo fb_oss_credential_source() === 'static' ? ' selected' : ''; ?>>静态 AccessKey</option>
|
||
</select></label>
|
||
<?php
|
||
// RAM 角色名不该让用户去控制台查 —— 直接问 ECS 元数据服务。
|
||
// 已配置的显示配置值;没配置就现场探测一次作为占位提示。
|
||
$curRole = trim((string)fb_cfg('oss.instance_role', ''));
|
||
$detect = null;
|
||
if ($curRole === '') {
|
||
list($dOk, $dVal) = fb_oss_detect_instance_role();
|
||
$detect = [$dOk, $dVal];
|
||
}
|
||
?>
|
||
<div class="field">
|
||
<span>ECS 实例 RAM 角色</span>
|
||
<?php if ($curRole !== ''): ?>
|
||
<div class="role-ok">已绑定:<b><?php echo e($curRole); ?></b>
|
||
<i class="hint" style="display:inline;margin-left:6px">(自动从实例元数据读取,无需手填)</i>
|
||
</div>
|
||
<?php elseif ($detect && $detect[0]): ?>
|
||
<div class="role-ok">自动探测到:<b><?php echo e((string)$detect[1]); ?></b>
|
||
<i class="hint" style="display:inline;margin-left:6px">保存时若留空就自动用这个</i>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="role-warn">
|
||
<?php echo e($detect ? (string)$detect[1] : '未能自动探测'); ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
<details style="margin-top:6px">
|
||
<summary style="cursor:pointer;font-size:13px;color:var(--muted)">手动指定角色名(一般不需要)</summary>
|
||
<input type="text" name="oss_instance_role" style="margin-top:6px"
|
||
placeholder="留空即自动探测"
|
||
value="<?php echo e($curRole); ?>">
|
||
</details>
|
||
</div>
|
||
<label class="field"><span>AccessKeyId(静态鉴权用)</span>
|
||
<input type="text" name="oss_access_key" value="<?php echo e((string)fb_cfg('oss.access_key', '')); ?>"></label>
|
||
<label class="field"><span>AccessKeySecret(留空=不修改)</span>
|
||
<input type="password" name="oss_secret_key" autocomplete="new-password"
|
||
placeholder="<?php echo (string)fb_cfg('oss.secret_key', '') !== '' ? '已设置,留空保持不变' : '未设置'; ?>"></label>
|
||
<label class="field"><span>对象键前缀</span>
|
||
<input type="text" name="oss_prefix" placeholder="留空即用 fb"
|
||
value="<?php echo e(fb_form_val(fb_cfg('oss.prefix', 'fb'), 'fb', 'oss.prefix')); ?>"></label>
|
||
<label class="field"><span>公网访问协议(走公网 Endpoint 时)</span>
|
||
<select name="oss_url_scheme">
|
||
<option value="https"<?php echo ((string)fb_cfg('oss.url_scheme', 'https')) === 'https' ? ' selected' : ''; ?>>https</option>
|
||
<option value="http"<?php echo ((string)fb_cfg('oss.url_scheme', 'https')) === 'http' ? ' selected' : ''; ?>>http</option>
|
||
</select></label>
|
||
</div>
|
||
<label class="check"><input type="checkbox" name="oss_use_internal_endpoint" value="1"<?php echo fb_oss_use_internal() ? ' checked' : ''; ?>> 使用内网 Endpoint(<code>*-internal.aliyuncs.com</code>,免公网流量费、延迟低)
|
||
<span class="hint" style="display:block;margin:2px 0 0 24px">⚠️ 仅当【桶与本机 ECS 同地域】时可用;跨地域内网不可达,请取消勾选回落公网,或改用同地域的桶。保存后可在下方「存储体检」查看连通性。</span></label>
|
||
<label class="check"><input type="checkbox" name="oss_verify" value="1"<?php echo fb_cfg('oss.verify', true) ? ' checked' : ''; ?>> 提交时回源复核附件(防伪造)</label>
|
||
<label class="check"><input type="checkbox" name="oss_image_process" value="1"<?php echo fb_cfg('oss.image_process', true) ? ' checked' : ''; ?>> 缩略图使用 OSS 图片处理(服务端缩放后再转发,省下行流量)</label>
|
||
<input type="hidden" name="oss_visibility" value="<?php echo e((string)fb_cfg('oss.visibility', 'private')); ?>">
|
||
|
||
<h2 style="margin:24px 0 10px">审核</h2>
|
||
<label class="check"><input type="checkbox" name="moderation_enabled" value="1"<?php echo fb_moderation_on() ? ' checked' : ''; ?>> 先审后显(新提交的问题默认不公开,需您通过后才显示)</label>
|
||
<label class="field"><span>关闭通知邮件标题</span>
|
||
<input type="text" name="moderation_close_mail_subject"
|
||
placeholder="留空即用:关于您反馈的问题《%s》"
|
||
value="<?php echo e(fb_form_val(fb_cfg('moderation.close_mail_subject', '关于您反馈的问题《%s》'), '关于您反馈的问题《%s》', 'moderation.close_mail_subject')); ?>">
|
||
<i class="hint">%s 会替换成问题标题。留空则用默认模板。</i></label>
|
||
|
||
<div class="form-actions">
|
||
<button class="btn primary" type="submit" name="act" value="settings">保存设置</button>
|
||
</div>
|
||
</form>
|
||
|
||
<h2 style="margin:26px 0 10px">存储体检</h2>
|
||
<div class="card-form">
|
||
<div style="display:flex;flex-direction:column;gap:6px">
|
||
<span>当前存储方式:<b><?php echo $mode === 'oss' ? '阿里云 OSS' : ($mode === 'local' ? '本机硬盘' : '已关闭'); ?></b></span>
|
||
<?php if ($mode === 'local'): ?>
|
||
<?php $ld = fb_local_diag(); ?>
|
||
<span>上传目录:<code><?php echo e($ld['dir']); ?></code> — <?php echo $ld['ok'] ? '可写 ✅' : '不可写 ❌'; ?></span>
|
||
<?php if (!$ld['ok']): ?>
|
||
<span class="muted small"><?php echo e($ld['tip']); ?></span>
|
||
<?php endif; ?>
|
||
<?php elseif ($mode === 'oss'): ?>
|
||
<span>鉴权方式:<b><?php echo e($ossStat['label']); ?></b></span>
|
||
<span>凭证状态:<?php echo !empty($ossStat['ok']) ? ('已取得 AccessKey(尾号 ' . e($ossStat['ak_tail']) . ')✅') : ('未取得 ❌ ' . e((string)$ossStat['error'])); ?></span>
|
||
<span>Bucket:<?php echo e((string)fb_cfg('oss.bucket', '')); ?></span>
|
||
<span>Endpoint(配置):<?php echo e((string)fb_cfg('oss.endpoint', '')); ?></span>
|
||
<span>Endpoint(实际连接):<code><?php echo e(fb_oss_host()); ?></code> — <?php echo fb_oss_use_internal() ? '内网' : '公网'; ?></span>
|
||
<?php if (isset($_GET['probe']) && $_GET['probe'] === '1'): ?>
|
||
<?php $pb = fb_oss_probe(); ?>
|
||
<span>连通性:<?php echo $pb['ok'] ? '✅' : '❌'; ?> <?php echo e($pb['detail']); ?><?php echo $pb['status'] ? ('(HTTP ' . (int)$pb['status'] . ')') : ''; ?></span>
|
||
<?php if (!$pb['ok'] && $pb['internal'] && $pb['status'] === 0): ?>
|
||
<span class="muted small">↳ 内网 Endpoint 跨地域不可达:请取消勾选上方「使用内网 Endpoint」后保存,或把桶换到与本机同地域。</span>
|
||
<?php endif; ?>
|
||
<?php else: ?>
|
||
<span><a class="btn" style="padding:4px 10px" href="admin.php?tab=settings&probe=1">测一下桶连通性</a>
|
||
<span class="muted small">(会真实访问一次 OSS,跨地域不通时可能等待数秒)</span></span>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
<span>已用附件:<?php echo e(fb_size_h(fb_attach_total_bytes($pdo))); ?> / 上限 <?php echo e(fb_size_h(fb_upload_cfg('total_cap_bytes', 0))); ?></span>
|
||
</div>
|
||
</div>
|
||
|
||
<h2 style="margin:26px 0 10px">登录口令</h2>
|
||
<div class="card-form" style="margin:0 0 16px">
|
||
<?php $pwSrc = fb_admin_password_source(); ?>
|
||
<div style="display:flex;flex-direction:column;gap:6px">
|
||
<span>存储位置:
|
||
<?php if ($pwSrc === 'db'): ?>
|
||
数据库 <code>settings</code> 表(键名 <code>admin.password_hash</code>)
|
||
<?php elseif ($pwSrc === 'file'): ?>
|
||
<code>config.php</code> 的 <code>admin_password</code>
|
||
<?php else: ?>
|
||
<b style="color:#b45309">尚未设置</b>
|
||
<?php endif; ?>
|
||
</span>
|
||
<span>是否加密:
|
||
<?php if ($pwSrc === 'db'): ?>
|
||
<b>是</b> —— 存的是 <code>password_hash()</code> 生成的 bcrypt 单向哈希(<code>$2y$…</code>),
|
||
不可逆、无法还原明文;校验用 <code>password_verify()</code>。
|
||
<?php elseif ($pwSrc === 'file'): ?>
|
||
<b>否</b> —— 目前是明文写在 <code>config.php</code> 里。
|
||
<a href="#pwform">在下面改一次口令</a>后即会转存为数据库中的 bcrypt 哈希。
|
||
<?php else: ?>
|
||
—— 还没有可用口令,当前后台<strong>无法登录</strong>。请用下方表单设定一个。
|
||
<?php endif; ?>
|
||
</span>
|
||
<?php if ($pwSrc === 'db'): ?>
|
||
<span class="muted small">数据库文件在 <code>data/feedback.db</code>(已由 .htaccess 拒绝外部访问),且只存哈希,明文不落盘。</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="pwform">
|
||
<form method="post" action="admin.php?tab=settings" class="card-form">
|
||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||
<?php if ($pwSrc !== 'none'): ?>
|
||
<div class="grid2">
|
||
<label class="field"><span>当前口令</span><input type="password" name="old_password" autocomplete="current-password"></label>
|
||
<label class="field"><span>新口令(至少 8 位)</span><input type="password" name="new_password" autocomplete="new-password"></label>
|
||
<label class="field"><span>再输入一次新口令</span><input type="password" name="new_password2" autocomplete="new-password"></label>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="grid2">
|
||
<label class="field"><span>设定登录口令(至少 8 位)</span><input type="password" name="new_password" autocomplete="new-password"></label>
|
||
<label class="field"><span>再输入一次</span><input type="password" name="new_password2" autocomplete="new-password"></label>
|
||
</div>
|
||
<?php endif; ?>
|
||
<div class="form-actions">
|
||
<button class="btn primary" type="submit" name="act" value="password"><?php echo $pwSrc === 'none' ? '设定口令' : '修改口令'; ?></button>
|
||
</div>
|
||
<p class="muted small">新口令以 bcrypt 哈希写入数据库,优先级高于 <code>config.php</code> 的 <code>admin_password</code>;改完立即生效,明文不会留在任何文件里。</p>
|
||
</form>
|
||
</div>
|
||
|
||
<?php elseif ($tab === 'mail'): ?>
|
||
<div class="page-head">
|
||
<div>
|
||
<h1>邮件通知</h1>
|
||
<p class="muted">配置 SMTP 后,反馈有变动会自动发信通知提交人。</p>
|
||
</div>
|
||
</div>
|
||
<?php fb_flash_box($msg, $msgType); ?>
|
||
<form method="post" action="admin.php?tab=mail" class="card-form" data-validate>
|
||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||
<input type="hidden" name="act" value="settings">
|
||
<h2 style="margin:24px 0 10px">邮件通知</h2>
|
||
<?php
|
||
// ⚠️ config.example.php 的 smtp 段有全套出厂示例值
|
||
// (smtp.example.com / noreply@example.com / 轻反馈 …),
|
||
// fb_cfg() 合并后表单会把这些当成"已填内容"显示出来,
|
||
// 用户点进去得先删一遍。所以回显一律过 fb_form_val():
|
||
// 数据库里没设过 = 没自定义 = 显示空,示例只留在 placeholder。
|
||
$smOn = (bool)fb_cfg('smtp.enabled', false);
|
||
$smHost = fb_form_val(fb_cfg('smtp.host', ''), 'smtp.example.com', 'smtp.host');
|
||
$smUser = fb_form_val(fb_cfg('smtp.user', ''), 'noreply@example.com', 'smtp.user');
|
||
$smFrom = fb_form_val(fb_cfg('smtp.from', ''), 'noreply@example.com', 'smtp.from');
|
||
$smName = fb_form_val(fb_cfg('smtp.from_name', ''), '轻反馈', 'smtp.from_name');
|
||
// 端口出厂 465,没设过就留空让 placeholder 提示,别预填数字
|
||
$smPortRaw = fb_form_val((string)(int)fb_cfg('smtp.port', 465), '465', 'smtp.port');
|
||
$smPort = $smPortRaw === '' ? '' : (string)(int)$smPortRaw;
|
||
$smSec = (string)fb_cfg('smtp.secure', 'ssl');
|
||
$smHasPw = (string)fb_cfg('smtp.pass', '') !== '';
|
||
$smTmo = (int)fb_cfg('smtp.timeout', 15);
|
||
$smVerify= (bool)fb_cfg('smtp.verify_ssl', true);
|
||
?>
|
||
<label class="check"><input type="checkbox" name="smtp_enabled" value="1"<?php echo $smOn ? ' checked' : ''; ?>> 开启邮件通知
|
||
<span class="hint" style="display:block;margin:2px 0 0 24px">开启后,有人回复您反馈的问题会发信通知提交人。填好下方参数并用「发送测试邮件」验证通了再勾。</span></label>
|
||
|
||
<div class="grid2">
|
||
<label class="field"><span>SMTP 服务器</span>
|
||
<input type="text" name="smtp_host" maxlength="120" placeholder="smtp.qiye.aliyun.com"
|
||
value="<?php echo e($smHost); ?>">
|
||
<i class="hint">阿里云企业邮 smtp.qiye.aliyun.com;腾讯企业邮 smtp.exmail.qq.com;QQ 邮箱 smtp.qq.com。</i></label>
|
||
<label class="field"><span>端口与加密</span>
|
||
<div style="display:flex;gap:8px">
|
||
<input type="number" name="smtp_port" min="1" max="65535" style="flex:0 0 110px"
|
||
placeholder="465"
|
||
value="<?php echo e($smPort); ?>">
|
||
<select name="smtp_secure" style="flex:1">
|
||
<option value="ssl"<?php echo $smSec === 'ssl' ? ' selected' : ''; ?>>SSL(通常 465)</option>
|
||
<option value="tls"<?php echo $smSec === 'tls' ? ' selected' : ''; ?>>STARTTLS(通常 587)</option>
|
||
<option value=""<?php echo $smSec === '' ? ' selected' : ''; ?>>不加密(通常 25,不推荐)</option>
|
||
</select>
|
||
</div></label>
|
||
</div>
|
||
|
||
<div class="grid2">
|
||
<label class="field"><span>登录账号</span>
|
||
<input type="text" name="smtp_user" maxlength="120" placeholder="noreply@example.com"
|
||
value="<?php echo e($smUser); ?>"></label>
|
||
<label class="field"><span>密码 / 授权码 <i class="hint" style="display:inline"><?php echo $smHasPw ? '已设置,留空不修改' : '未设置'; ?></i></span>
|
||
<input type="password" name="smtp_pass" autocomplete="new-password"
|
||
placeholder="<?php echo $smHasPw ? '留空保持不变' : '邮箱密码或授权码'; ?>"></label>
|
||
</div>
|
||
|
||
<div class="grid2">
|
||
<label class="field"><span>发件地址</span>
|
||
<input type="email" name="smtp_from" maxlength="120" placeholder="留空则同登录账号"
|
||
value="<?php echo e($smFrom); ?>"></label>
|
||
<label class="field"><span>发件人显示名称</span>
|
||
<input type="text" name="smtp_from_name" maxlength="60"
|
||
placeholder="<?php echo e((string)fb_cfg('site_name', '轻反馈')); ?>"
|
||
value="<?php echo e($smName); ?>"></label>
|
||
</div>
|
||
|
||
<details style="margin:6px 0 12px">
|
||
<summary style="cursor:pointer;font-size:13px;color:var(--muted)">高级选项</summary>
|
||
<div class="grid2" style="margin-top:8px">
|
||
<label class="field"><span>超时(秒)</span>
|
||
<input type="number" name="smtp_timeout" min="5" max="60" placeholder="15"
|
||
value="<?php echo e(fb_form_val((string)(int)$smTmo, '15', 'smtp.timeout')); ?>"></label>
|
||
<label class="check" style="align-self:end"><input type="checkbox" name="smtp_verify_ssl" value="1"<?php echo $smVerify ? ' checked' : ''; ?>> 校验服务器 SSL 证书
|
||
<span class="hint" style="display:block;margin-left:24px">服务器缺 CA 证书时可临时取消,但不建议。</span></label>
|
||
</div>
|
||
</details>
|
||
|
||
<?php if ($smHost !== ''): ?>
|
||
<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', '') !== '' ? (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>
|
||
<?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"
|
||
value="<?php echo e(fb_form_val((string)(int)fb_cfg('notify.cooldown_seconds', 300), '300', 'notify.cooldown_seconds')); ?>">
|
||
<i class="hint">0 = 不限制。</i></label>
|
||
<div class="form-actions">
|
||
<button class="btn primary" type="submit">保存邮件设置</button>
|
||
<a class="btn" href="admin.php?tab=settings">← 回站点设置</a>
|
||
</div>
|
||
</form>
|
||
|
||
|
||
<?php else: ?>
|
||
<?php
|
||
$logs = $pdo->query('SELECT * FROM mod_log ORDER BY created_at DESC, id DESC LIMIT 100')->fetchAll();
|
||
?>
|
||
<h2 style="margin:8px 0 10px">最近操作 <span class="muted">(<?php echo count($logs); ?> 条)</span></h2>
|
||
<?php if (!$logs): ?>
|
||
<div class="empty"><p>还没有操作记录。</p></div>
|
||
<?php else: ?>
|
||
<div class="list">
|
||
<?php foreach ($logs as $l): ?>
|
||
<div class="item" style="cursor:default">
|
||
<div class="item-top">
|
||
<span class="badge cat"><?php echo e((string)$l['action']); ?></span>
|
||
<?php if (!empty($l['thread_id'])): ?>
|
||
<a class="item-title" href="admin.php?tab=thread&id=<?php echo (int)$l['thread_id']; ?>">#<?php echo (int)$l['thread_id']; ?></a>
|
||
<?php endif; ?>
|
||
</div>
|
||
<p class="item-ex"><?php echo e((string)$l['detail']); ?></p>
|
||
<div class="item-meta"><span><?php echo e(fb_fmt_time($l['created_at'])); ?></span></div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
|
||
<?php
|
||
fb_layout_foot();
|
||
|
||
/**
|
||
* 待审核条目(列表式)
|
||
*
|
||
* 用户 2026-11-03 明确要求:
|
||
* ① 列表形式显示,不要每张卡片塞满表单
|
||
* ② 去掉「关闭说明」整块 —— 想关闭就点标题进管理页再填说明,
|
||
* 列表页不承担这个职责
|
||
* ③ 后面只留一个「通过并公开」按钮,点一下就通过
|
||
* ④ 标题自己就是进管理页的链接,别再额外挂「进入管理」
|
||
*/
|
||
function fb_admin_pending_row(array $t) {
|
||
$url = 'admin.php?tab=thread&id=' . (int)$t['id'];
|
||
ob_start(); ?>
|
||
<div class="prow">
|
||
<div class="prow-main">
|
||
<?php $cSw = !empty($t['software_id']) ? fb_software((int)$t['software_id']) : null; ?>
|
||
<?php /* 标题顶头独占一行,标签(软件/分类/状态)另起一行放下面。
|
||
用户 2026-11-03:后台待审核、全部问题、详情页全都按这个方式处理 ——
|
||
标签放在标题前面会把标题挤到右边。 */ ?>
|
||
<div class="prow-head">
|
||
<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>
|
||
<div class="prow-tags">
|
||
<?php if ($cSw): ?><?php echo fb_software_chip($cSw); ?><?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>
|
||
</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="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']; ?>">
|
||
<button class="btn primary" type="submit" name="act" value="approve">通过并公开</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<?php
|
||
return ob_get_clean();
|
||
}
|