多软件归属:软件→分类两级,配置全部入库 (#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:
+9
-1
@@ -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{
|
||||
|
||||
@@ -5,15 +5,33 @@
|
||||
require __DIR__ . '/lib/bootstrap.php';
|
||||
|
||||
$pdo = fb_db();
|
||||
$cats = fb_categories();
|
||||
$statuses = fb_statuses();
|
||||
|
||||
/* ---- 软件切换:只配一个软件时自动落到它,不暴露软件选择器 ---- */
|
||||
$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,7 +39,8 @@ $where = [];
|
||||
$args = [];
|
||||
// 审核(先审后显):访客只看已通过审核的;管理员登录审核后台后可看全部
|
||||
if (fb_moderation_on() && !fb_is_admin()) { $where[] = 'visible = 1'; }
|
||||
if ($cat !== '') { $where[] = 'category = ?'; $args[] = $cat; }
|
||||
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, '%_\\') . '%';
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 */
|
||||
|
||||
@@ -5,12 +5,26 @@
|
||||
require __DIR__ . '/lib/bootstrap.php';
|
||||
|
||||
$pdo = fb_db();
|
||||
$cats = fb_categories();
|
||||
$err = '';
|
||||
$atts = []; // 校验失败时保留已上传的附件
|
||||
$catKeys = array_keys($cats);
|
||||
|
||||
/* ---- 软件: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' => $catKeys ? (string)$catKeys[0] : 'other',
|
||||
'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>
|
||||
|
||||
Reference in New Issue
Block a user