多软件归属:软件→分类两级,配置全部入库 (#1)

原先分类写死在 config.example.php,后台改不了;也没有「软件」概念。
现在改为数据库驱动的两级结构,后台可配(管理界面待做)。

## 数据层(lib/db.php)
- 新增 software / categories / statuses 三张表
- categories.software_id 外键:每个软件有自己的分类集合,可同名互不干扰
- threads 加 software_id / category_id;老 category 文本列保留不删(可回退)
- fb_db_migrate_taxonomy() 幂等迁移:建表 → 空表灌初始数据 → 回填老帖子

## 迁移实测
- 老 5 个分类(使用问题/Bug 反馈/功能建议/同步问题/其他)原样落进默认软件
- 老帖子按 category slug 回填 category_id,匹配不到的留 NULL

## 状态(5 个)
待处理 / 处理中 / 已解决 / 已关闭 / 不予考虑(wontfix)
每个状态带 user_note「给用户看的话」,后台可改,将进邮件正文。
待处理 = 刚提交:您提交的反馈已收到,我们正在处理中。

## 前台
- index.php:软件切换条 + 分类 chip(一级软件、二级分类)
- new.php:选软件、分类下拉随软件变、服务端校验分类归属
- URL:?sw=<slug>&cat=<slug>;老链接 ?cat=xxx 解析不到时退回全部,200 不 404

## 界面一致性
按用户要求去掉「单软件时隐藏软件选择器」的逻辑:
所有界面一致,配一个就列一个,配多个就列多个。

## 兼容
fb_categories() 现在必须传软件 id;fb_statuses() 返回行数组,
另有 fb_status_keys() 取 key=>name 映射。

验证:WSL 内临时实例实测——单软件显示 1 个软件条;两软件各显示
自己的分类集合与帖子,互不串;无 PHP 警告/致命错误。
This commit is contained in:
2026-11-01 11:35:55 +08:00
parent 690228e7c1
commit 59f7466ae4
6 changed files with 378 additions and 39 deletions
+9 -1
View File
@@ -81,7 +81,15 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
.page-head h1{margin-bottom:4px}
.page-head .muted{font-size:14px; margin:0}
/* ---------- 筛选 ---------- */
/* ---------- 软件切换(一级) ---------- */
.software-bar{
display:flex; flex-wrap:wrap; align-items:center; gap:8px;
margin-bottom:14px; padding-bottom:14px; border-bottom:1px solid var(--line);
}
.software-bar .sw-label{font-size:13.5px; color:var(--muted); font-weight:600}
.software-bar .chips{margin-bottom:0}
/* ---------- 筛选(二级:分类 / 状态) ---------- */
.filters{margin-bottom:16px}
.chips{display:flex; flex-wrap:wrap; gap:8px; margin-bottom:12px}
.chip{
+50 -15
View File
@@ -4,16 +4,34 @@
*/
require __DIR__ . '/lib/bootstrap.php';
$pdo = fb_db();
$cats = fb_categories();
$statuses = fb_statuses();
$pdo = fb_db();
/* ---- 软件切换:只配一个软件时自动落到它,不暴露软件选择器 ---- */
$swList = fb_softwares(true);
$swKey = isset($_GET['sw']) ? (string)$_GET['sw'] : '';
$sw = null;
if ($swKey !== '') {
$sw = fb_software($swKey);
// 指定的软件不存在或已停用 → 退回默认,不报错(老链接要能打开)
if (!$sw || (int)$sw['enabled'] !== 1) { $sw = null; }
}
if ($sw === null && $swList) { $sw = $swList[0]; }
$swId = $sw ? (int)$sw['id'] : 0;
$cats = fb_categories($swId);
$statuses = fb_status_keys();
$cat = isset($_GET['cat']) ? (string)$_GET['cat'] : '';
$st = isset($_GET['status']) ? (string)$_GET['status'] : '';
$q = isset($_GET['q']) ? trim((string)$_GET['q']) : '';
$sort = isset($_GET['sort']) ? (string)$_GET['sort'] : 'active';
if ($cat !== '' && !isset($cats[$cat])) { $cat = ''; }
// 分类按 slug 在当前软件内解析;解析不到退回「全部」,不许 404
$curCat = null;
if ($cat !== '') {
$curCat = fb_category_by_slug($swId, $cat);
if (!$curCat) { $cat = ''; }
}
if ($st !== '' && !isset($statuses[$st])) { $st = ''; }
if ($sort !== 'new') { $sort = 'active'; }
@@ -21,8 +39,9 @@ $where = [];
$args = [];
// 审核(先审后显):访客只看已通过审核的;管理员登录审核后台后可看全部
if (fb_moderation_on() && !fb_is_admin()) { $where[] = 'visible = 1'; }
if ($cat !== '') { $where[] = 'category = ?'; $args[] = $cat; }
if ($st !== '') { $where[] = 'status = ?'; $args[] = $st; }
if ($swId > 0) { $where[] = 'software_id = ?'; $args[] = $swId; }
if ($curCat) { $where[] = 'category_id = ?'; $args[] = (int)$curCat['id']; }
if ($st !== '') { $where[] = 'status = ?'; $args[] = $st; }
if ($q !== '') {
$like = '%' . addcslashes($q, '%_\\') . '%';
$where[] = "(title LIKE ? ESCAPE '\\' OR body LIKE ? ESCAPE '\\')";
@@ -43,7 +62,7 @@ if ($page < 1) { $page = 1; }
if ($page > $pages){ $page = $pages; }
$offset = ($page - 1) * $pageSize;
$sql = 'SELECT id, title, category, status, author_name, reply_count, created_at, updated_at,'
$sql = 'SELECT id, title, category, category_id, software_id, status, author_name, reply_count, created_at, updated_at,'
. ' substr(body, 1, 300) AS excerpt FROM threads'
. $wsql . ' ORDER BY ' . $order . ' LIMIT ' . $pageSize . ' OFFSET ' . $offset;
$sth = $pdo->prepare($sql);
@@ -56,25 +75,41 @@ foreach ($threads as $t) { $tids[] = (int)$t['id']; }
$attImgs = $tids ? fb_attach_map_images($pdo, $tids, 3) : [];
$attCount = $tids ? fb_attach_count_map($pdo, $tids) : [];
fb_layout_head('全部问题');
$siteName = (string)fb_cfg('site_name');
$headTitle = $siteName !== '' ? $siteName . ' · 问题反馈' : '问题反馈';
fb_layout_head($sw ? ((string)$sw['name']) : $headTitle);
?>
<div class="page-head">
<div>
<h1>全部问题</h1>
<p class="muted"><?php echo e((string)fb_cfg('site_desc')); ?></p>
<h1><?php echo e($sw ? (string)$sw['name'] : '全部问题'); ?></h1>
<p class="muted"><?php
$desc = $sw && !empty($sw['note']) ? (string)$sw['note'] : (string)fb_cfg('site_desc');
echo e($desc);
?></p>
</div>
<a class="btn primary" href="new.php<?php echo $sw ? '?sw=' . e(urlencode((string)$sw['slug'])) : ''; ?>">我要反馈</a>
</div>
<div class="software-bar">
<span class="sw-label">选择软件:</span>
<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>
<?php endforeach; ?>
</div>
<a class="btn primary" href="new.php">我要反馈</a>
</div>
<div class="filters">
<div class="chips">
<a class="chip <?php echo $cat === '' ? 'on' : ''; ?>" href="<?php echo e(fb_qs(['cat' => null, 'page' => null]) ?: '?'); ?>">全部</a>
<?php foreach ($cats as $k => $v): ?>
<a class="chip <?php echo $cat === $k ? 'on' : ''; ?>" href="<?php echo e(fb_qs(['cat' => $k, 'page' => null])); ?>"><?php echo e($v); ?></a>
<?php foreach ($cats as $c): ?>
<a class="chip <?php echo $cat === (string)$c['slug'] ? 'on' : ''; ?>" href="<?php echo e(fb_qs(['cat' => (string)$c['slug'], 'page' => null])); ?>"><?php echo e((string)$c['name']); ?></a>
<?php endforeach; ?>
</div>
<form class="filter-row" method="get" action="">
<?php if ($sw): ?><input type="hidden" name="sw" value="<?php echo e((string)$sw['slug']); ?>"><?php endif; ?>
<input type="hidden" name="cat" value="<?php echo e($cat); ?>">
<select name="status" onchange="this.form.submit()">
<option value="">全部状态</option>
@@ -95,13 +130,13 @@ fb_layout_head('全部问题');
<?php if (!$threads): ?>
<div class="empty">
<p>还没有符合条件的问题。</p>
<p><a class="btn primary" href="new.php">来提第一个吧</a></p>
<p><a class="btn primary" href="new.php<?php echo $sw ? '?sw=' . e(urlencode((string)$sw['slug'])) : ''; ?>">来提第一个吧</a></p>
</div>
<?php else: ?>
<?php foreach ($threads as $t): ?>
<a class="item" href="thread.php?id=<?php echo (int)$t['id']; ?>">
<div class="item-top">
<span class="badge cat"><?php echo e(fb_category_label($t['category'])); ?></span>
<span class="badge cat"><?php echo e(fb_category_label($t['category_id'])); ?></span>
<span class="badge st st-<?php echo e($t['status']); ?>"><?php echo e(fb_status_label($t['status'])); ?></span>
<span class="item-title"><?php echo e($t['title']); ?></span>
<span class="item-id" title="问题编号">#<?php echo (int)$t['id']; ?></span>
+146
View File
@@ -199,4 +199,150 @@ SQL
$pdo->exec('ALTER TABLE threads ADD COLUMN moderated_at INTEGER');
}
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_threads_visible ON threads(visible, created_at DESC)');
fb_db_migrate_taxonomy($pdo);
}
/**
* 软件 / 分类 / 状态 三张表(全部可在后台配置,不再写死在配置文件里)
*
* 结构:软件 → 分类(两级)。分类属于某个软件,不同软件可以有同名但互不相干的分类。
* 幂等:每次请求都会跑,靠 IF NOT EXISTS 与「空表才灌初始数据」保证重复执行结果一致。
*/
function fb_db_migrate_taxonomy(PDO $pdo, $cfg = null) {
/* ---- 软件 ---- */
$pdo->exec(<<<'SQL'
CREATE TABLE IF NOT EXISTS software (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL,
name TEXT NOT NULL,
note TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
enabled INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL
);
SQL
);
$pdo->exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_sw_slug ON software(slug)');
/* ---- 分类(属于某个软件)---- */
$pdo->exec(<<<'SQL'
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
software_id INTEGER NOT NULL,
slug TEXT NOT NULL,
name TEXT NOT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
enabled INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
FOREIGN KEY(software_id) REFERENCES software(id) ON DELETE CASCADE
);
SQL
);
$pdo->exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_cat_sw_slug ON categories(software_id, slug)');
/* ---- 状态 ---- */
// key_name 是 threads.status 里存的值;user_note 是发给用户看的解释(进邮件正文)
$pdo->exec(<<<'SQL'
CREATE TABLE IF NOT EXISTS statuses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key_name TEXT NOT NULL,
name TEXT NOT NULL,
user_note TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
enabled INTEGER NOT NULL DEFAULT 1
);
SQL
);
$pdo->exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_st_key ON statuses(key_name)');
/* ---- threads 增加两列(老 category 文本列保留,不删)---- */
$cols = $pdo->query('PRAGMA table_info(threads)')->fetchAll();
$names = [];
foreach ($cols as $c) { if (isset($c['name'])) { $names[(string)$c['name']] = true; } }
if (!isset($names['software_id'])) {
// 不用 REFERENCES:SQLite 加列时外键约束对既有行不友好,改由应用层保证
$pdo->exec('ALTER TABLE threads ADD COLUMN software_id INTEGER');
}
if (!isset($names['category_id'])) {
$pdo->exec('ALTER TABLE threads ADD COLUMN category_id INTEGER');
}
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_threads_sw ON threads(software_id)');
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_threads_cid ON threads(category_id)');
/* ================= 初始数据(空表才灌,幂等) ================= */
$now = time();
$swId = 0;
// 1) 软件:空表时建一个默认软件
if ((int)$pdo->query('SELECT COUNT(*) FROM software')->fetchColumn() === 0) {
$siteName = '';
try { $siteName = (string)fb_cfg('site_name', ''); } catch (\Throwable $e) { $siteName = ''; }
if (trim($siteName) === '') { $siteName = '我的软件'; }
$st = $pdo->prepare('INSERT INTO software(slug, name, note, sort_order, enabled, created_at)
VALUES(?,?,?,?,1,?)');
$st->execute(['default', $siteName, '默认软件,可在后台改名', 0, $now]);
$swId = (int)$pdo->lastInsertId();
} else {
$swId = (int)$pdo->query('SELECT id FROM software ORDER BY sort_order, id LIMIT 1')->fetchColumn();
}
// 2) 分类:空表时把配置文件里的老 categories 原样搬进来(保住既有五个分类)
if ((int)$pdo->query('SELECT COUNT(*) FROM categories')->fetchColumn() === 0 && $swId > 0) {
$old = [];
try { $old = (array)fb_cfg('categories', []); } catch (\Throwable $e) { $old = []; }
if (!$old) {
$old = ['usage' => '使用问题', 'bug' => 'Bug', 'feature' => '功能建议', 'other' => '其他'];
}
$i = 0;
$st = $pdo->prepare('INSERT INTO categories(software_id, slug, name, sort_order, enabled, created_at)
VALUES(?,?,?,?,1,?)');
foreach ($old as $k => $label) {
$slug = fb_sw_slugify($k);
if ($slug === '') { $slug = 'c' . $i; }
$st->execute([$swId, $slug, (string)$label, $i, $now]);
$i++;
}
}
// 3) 状态:空表时灌 5 个(4 个原有 + 新增「不予考虑」)
if ((int)$pdo->query('SELECT COUNT(*) FROM statuses')->fetchColumn() === 0) {
$rows = [
['open', '待处理', '您提交的反馈已收到,我们正在处理中。', 0],
['doing', '处理中', '我们正在跟进这个问题,有进展会在这里同步。', 1],
['resolved', '已解决', '这个问题已经处理好了,欢迎回来验证一下。', 2],
['closed', '已关闭', '这个反馈已关闭,如有新情况可以重新提交。', 3],
['wontfix', '不予考虑', '经评估这个反馈暂不处理,感谢你的建议。', 4],
];
$st = $pdo->prepare('INSERT INTO statuses(key_name, name, user_note, sort_order, enabled)
VALUES(?,?,?,?,1)');
foreach ($rows as $r) { $st->execute($r); }
}
// 4) 回填老帖子:software_id / category_id(只填 NULL 的,重复执行无副作用)
if ($swId > 0) {
$pdo->prepare('UPDATE threads SET software_id = ? WHERE software_id IS NULL')->execute([$swId]);
}
$need = $pdo->prepare('SELECT id, category FROM threads WHERE category_id IS NULL AND software_id = ?');
$need->execute([$swId]);
$rows = $need->fetchAll();
if ($rows) {
$find = $pdo->prepare('SELECT id FROM categories WHERE software_id = ? AND slug = ? LIMIT 1');
$set = $pdo->prepare('UPDATE threads SET category_id = ? WHERE id = ?');
foreach ($rows as $r) {
$slug = fb_sw_slugify((string)$r['category']);
$find->execute([$swId, $slug]);
$cid = $find->fetchColumn();
if ($cid !== false && $cid !== null) { $set->execute([(int)$cid, (int)$r['id']]); }
}
}
}
/** 生成 URL 友好的 slug(只留小写字母数字和连字符) */
function fb_sw_slugify($s) {
$s = strtolower(trim((string)$s));
$s = preg_replace('/[^a-z0-9_-]+/', '-', $s);
$s = trim((string)$s, '-');
return $s === '' ? '' : substr($s, 0, 40);
}
+1 -1
View File
@@ -27,7 +27,7 @@ function fb_layout_head($title, array $opt = []) {
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon.png">
<link rel="manifest" href="assets/site.webmanifest">
<link rel="stylesheet" href="assets/style.css?v=7">
<link rel="stylesheet" href="assets/style.css?v=8">
</head>
<body>
<header class="topbar">
+113 -8
View File
@@ -58,16 +58,121 @@ function fb_redirect($url) {
exit;
}
/** 分类 / 状态 */
function fb_categories() { return (array)fb_cfg('categories', []); }
function fb_category_label($k) {
$c = fb_categories();
return isset($c[$k]) ? $c[$k] : '其他';
/** ================= 软件 / 分类 / 状态 =================
* 三者全部存数据库、后台可配,不再读配置文件。
* 结构:软件 → 分类(两级)。分类属于某个软件,不同软件可有同名分类。 */
/** 全部软件(按排序),$onlyEnabled=true 时只取启用的 */
function fb_softwares($onlyEnabled = true) {
$sql = 'SELECT * FROM software';
if ($onlyEnabled) { $sql .= ' WHERE enabled = 1'; }
$sql .= ' ORDER BY sort_order, id';
$rows = fb_db()->query($sql)->fetchAll();
$out = [];
foreach ($rows as $r) { $out[] = $r; }
return $out;
}
function fb_statuses() { return (array)fb_cfg('statuses', []); }
/** 按 slug 或 id 取一个软件;取不到返回 null */
function fb_software($key) {
if ($key === null || $key === '') { return null; }
$pdo = fb_db();
if (ctype_digit((string)$key)) {
$st = $pdo->prepare('SELECT * FROM software WHERE id = ? LIMIT 1');
$st->execute([(int)$key]);
} else {
$st = $pdo->prepare('SELECT * FROM software WHERE slug = ? LIMIT 1');
$st->execute([(string)$key]);
}
$r = $st->fetch();
return $r ? $r : null;
}
/** 默认软件:排序最前的启用软件;一个都没有时返回 null */
function fb_default_software() {
$list = fb_softwares(true);
return $list ? $list[0] : null;
}
/** 某软件下的分类;不传软件则返回空数组 */
function fb_categories($softwareId = null, $onlyEnabled = true) {
if ($softwareId === null) { return []; }
$sql = 'SELECT * FROM categories WHERE software_id = ?';
$args = [(int)$softwareId];
if ($onlyEnabled) { $sql .= ' AND enabled = 1'; }
$sql .= ' ORDER BY sort_order, id';
$st = fb_db()->prepare($sql);
$st->execute($args);
return $st->fetchAll();
}
/** 按 id 取分类(含禁用的,后台编辑用) */
function fb_category($id) {
if (!$id) { return null; }
$st = fb_db()->prepare('SELECT * FROM categories WHERE id = ? LIMIT 1');
$st->execute([(int)$id]);
$r = $st->fetch();
return $r ? $r : null;
}
/** 在某软件内按 slug 取分类 */
function fb_category_by_slug($softwareId, $slug) {
if (!$softwareId || $slug === '') { return null; }
$st = fb_db()->prepare('SELECT * FROM categories WHERE software_id = ? AND slug = ? LIMIT 1');
$st->execute([(int)$softwareId, (string)$slug]);
$r = $st->fetch();
return $r ? $r : null;
}
/** 分类显示名:只给 id 就能拿到名字(列表渲染用) */
function fb_category_label($id) {
if (!$id) { return ''; }
static $cache = [];
$id = (int)$id;
if (!isset($cache[$id])) {
$c = fb_category($id);
$cache[$id] = $c ? (string)$c['name'] : '';
}
return $cache[$id];
}
/** 全部状态(按排序) */
function fb_statuses($onlyEnabled = true) {
$sql = 'SELECT * FROM statuses';
if ($onlyEnabled) { $sql .= ' WHERE enabled = 1'; }
$sql .= ' ORDER BY sort_order, id';
return fb_db()->query($sql)->fetchAll();
}
/** 状态 key 列表(校验用) */
function fb_status_keys() {
$out = [];
foreach (fb_statuses(true) as $s) { $out[(string)$s['key_name']] = (string)$s['name']; }
return $out;
}
/** 状态显示名 */
function fb_status_label($k) {
$s = fb_statuses();
return isset($s[$k]) ? $s[$k] : (string)$k;
static $cache = null;
if ($cache === null) {
$cache = [];
foreach (fb_statuses(false) as $s) { $cache[(string)$s['key_name']] = (string)$s['name']; }
}
$k = (string)$k;
return isset($cache[$k]) ? $cache[$k] : $k;
}
/** 状态的「给用户看的话」——进邮件正文,后台可改 */
function fb_status_user_note($k) {
static $cache = null;
if ($cache === null) {
$cache = [];
foreach (fb_statuses(false) as $s) {
$cache[(string)$s['key_name']] = (string)($s['user_note'] === null ? '' : $s['user_note']);
}
}
$k = (string)$k;
return isset($cache[$k]) ? $cache[$k] : '';
}
/** CSRF */
+59 -14
View File
@@ -4,13 +4,27 @@
*/
require __DIR__ . '/lib/bootstrap.php';
$pdo = fb_db();
$cats = fb_categories();
$err = '';
$atts = []; // 校验失败时保留已上传的附件
$catKeys = array_keys($cats);
$form = [
'category' => $catKeys ? (string)$catKeys[0] : 'other',
$pdo = fb_db();
$err = '';
$atts = []; // 校验失败时保留已上传的附件
/* ---- 软件:URL 带 sw 就用它,否则落到默认软件 ---- */
$swList = fb_softwares(true);
$sw = null;
$swKey = isset($_GET['sw']) ? (string)$_GET['sw'] : '';
if ($swKey !== '') {
$sw = fb_software($swKey);
if (!$sw || (int)$sw['enabled'] !== 1) { $sw = null; }
}
if ($sw === null && $swList) { $sw = $swList[0]; }
$swId = $sw ? (int)$sw['id'] : 0;
$cats = fb_categories($swId);
$catKeys = [];
foreach ($cats as $c) { $catKeys[] = (int)$c['id']; }
$form = [
'category_id' => $catKeys ? (string)$catKeys[0] : '',
'title' => '',
'body' => '',
'name' => '',
@@ -19,16 +33,27 @@ $form = [
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
foreach (['category', 'title', 'body', 'name', 'email', 'phone'] as $k) {
foreach (['category_id', 'title', 'body', 'name', 'email', 'phone'] as $k) {
$form[$k] = trim(isset($_POST[$k]) ? (string)$_POST[$k] : '');
}
// POST 里带了 software_id 时以它为准(切换软件后表单会跟着变)
if (isset($_POST['software_id']) && ctype_digit((string)$_POST['software_id'])) {
$swPost = fb_software((int)$_POST['software_id']);
if ($swPost && (int)$swPost['enabled'] === 1) {
$sw = $swPost;
$swId = (int)$swPost['id'];
}
}
$capInput = isset($_POST['captcha']) ? (string)$_POST['captcha'] : '';
$hp = trim(isset($_POST['website']) ? (string)$_POST['website'] : '');
$t0 = isset($_POST['t0']) ? (int)$_POST['t0'] : 0;
list($attOk, $attList, $attErr) = fb_attach_parse(isset($_POST['attachments']) ? $_POST['attachments'] : '');
if (!isset($cats[$form['category']])) { $form['category'] = 'other'; }
// 分类必须属于这个软件,否则视为未选
$curCatId = ctype_digit($form['category_id']) ? (int)$form['category_id'] : 0;
$curCat = $curCatId ? fb_category($curCatId) : null;
if (!$curCat || (int)$curCat['software_id'] !== $swId) { $curCat = null; $form['category_id'] = ''; }
if (!fb_csrf_ok(isset($_POST['csrf']) ? $_POST['csrf'] : '')) {
$err = '会话已过期,请重新提交。';
@@ -66,9 +91,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$now = time();
// 审核:开启后新提交默认不公开(visible=0),管理员通过后才会出现在列表
$visible = fb_moderation_on() ? 0 : 1;
$ins = $pdo->prepare('INSERT INTO threads(title, body, category, author_name, author_email, author_phone, status, visible, reply_count, created_at, updated_at, ip_hash) VALUES(?,?,?,?,?,?,?,?,0,?,?,?)');
$ins = $pdo->prepare('INSERT INTO threads(title, body, category, category_id, software_id, author_name, author_email, author_phone, status, visible, reply_count, created_at, updated_at, ip_hash) VALUES(?,?,?,?,?,?,?,?,?,?,0,?,?,?)');
$ins->execute([
$form['title'], $form['body'], $form['category'],
$form['title'], $form['body'],
$curCat ? (string)$curCat['slug'] : 'other', // 老列留个值,便于回退与检索
$curCat ? (int)$curCat['id'] : null,
$swId > 0 ? $swId : null,
$form['name'], $form['email'],
$form['phone'] !== '' ? $form['phone'] : null,
'open', $visible, $now, $now, fb_ip_hash(),
@@ -99,14 +127,31 @@ fb_layout_head('我要反馈', ['robots' => 'noindex,follow']);
<label>请勿填写此栏<input type="text" name="website" tabindex="-1" autocomplete="off"></label>
</div>
<?php if ($sw): ?>
<input type="hidden" name="software_id" value="<?php echo (int)$sw['id']; ?>">
<div class="field">
<span>所属软件 <b class="req">*</b><i class="hint">换软件会刷新下面的分类</i></span>
<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>
<?php endforeach; ?>
</div>
</div>
<?php elseif ($sw): ?>
<input type="hidden" name="software_id" value="<?php echo (int)$sw['id']; ?>">
<?php endif; ?>
<?php if ($cats): ?>
<label class="field">
<span>问题分类 <b class="req">*</b></span>
<select name="category">
<?php foreach ($cats as $k => $v): ?>
<option value="<?php echo e($k); ?>" <?php echo $form['category'] === $k ? 'selected' : ''; ?>><?php echo e($v); ?></option>
<select name="category_id" required>
<?php foreach ($cats as $c): ?>
<option value="<?php echo (int)$c['id']; ?>" <?php echo $form['category_id'] === (string)$c['id'] ? 'selected' : ''; ?>><?php echo e((string)$c['name']); ?></option>
<?php endforeach; ?>
</select>
</label>
<?php endif; ?>
<label class="field">
<span>标题 <b class="req">*</b></span>