2 * 1024 * 1024) { return [false, '图标不能超过 2 MB。', '']; } // 真实类型(不信扩展名、不信前端上报的 mime) $mime = ''; if (function_exists('finfo_open')) { $fi = @finfo_open(FILEINFO_MIME_TYPE); if ($fi) { $mime = strtolower(trim((string)@finfo_file($fi, $tmp))); @finfo_close($fi); } } $mimeMap = [ 'image/png' => 'png', 'image/jpeg' => 'jpg', 'image/webp' => 'webp', ]; if (!isset($mimeMap[$mime])) { return [false, '只支持 PNG / JPG / WEBP 格式的图片(检测到 ' . ($mime === '' ? '未知' : $mime) . ')。', '']; } $ext = $mimeMap[$mime]; // 尺寸下限:太小的图放大会糊,直接拒绝比事后难看更好 if (function_exists('getimagesize')) { $wh = @getimagesize($tmp); if ($wh && isset($wh[0], $wh[1])) { if ($wh[0] < 32 || $wh[1] < 32) { return [false, '图片太小(' . (int)$wh[0] . '×' . (int)$wh[1] . '),至少 32×32。', '']; } } } $mode = fb_store_mode(); if ($mode === 'off') { return [false, '附件存储未启用,无法保存图标。请先在「站点设置」里启用存储。', '']; } $key = fb_icon_make_key($ext); /* 断言:键的前缀必须与当前存储方式一致。 * 这两者一旦脱钩,就会出现「文件写进 uploads/ 但 media.php 去 OSS 找」的 503 —— * 早期把键硬编码成 icons/… 就是这么坏的。宁可当场报错,也不要静默存错位置。 */ if ($mode === 'local' && !fb_attach_is_local($key)) { return [false, '内部错误:本地模式下生成了非本机路径的图标键。', '']; } if ($mode === 'oss' && fb_attach_is_local($key)) { return [false, '内部错误:OSS 模式下生成了本机路径的图标键。', '']; } if ($mode === 'local') { if (!fb_local_writable()) { return [false, '本机附件目录不可写(uploads/),无法保存图标。', '']; } $path = fb_local_path_of($key); $dir = dirname($path); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } if (!is_dir($dir) || !@move_uploaded_file($tmp, $path)) { return [false, '保存图片失败,请稍后重试。', '']; } @chmod($path, 0644); } else { if (!fb_oss_enabled()) { return [false, '对象存储未配置完整,无法保存图标。', '']; } list($ok, $err) = fb_oss_put_file($key, $tmp, $mime); if (!$ok) { return [false, '写入对象存储失败:' . $err, '']; } } return [true, $key, $mime]; } /** * 删除一个图标实体(换成新图 / 清空图标时调用,避免孤儿文件堆积)。 * * @return bool 是否已确认删除(文件不存在也算成功) * * 🔴 不要用 @ 把错误吞掉:删失败却返回「已保存」, * 用户会以为旧图清掉了,实际 OSS 上还留着一个可访问的孤儿图。 * 这里失败就返回 false,调用方据此提示。 */ function fb_icon_delete($key) { $key = trim((string)$key); if ($key === '' || !fb_icon_key_ok($key)) { return true; } // 没东西可删 if (fb_attach_is_local($key)) { $p = fb_local_path_of($key); if (!is_file($p)) { return true; } return (bool)@unlink($p); } if (!fb_oss_enabled()) { return true; } // OSS 关掉了,对象已不可达,不必纠结 if (!function_exists('fb_oss_delete')) { return false; } try { return (bool)fb_oss_delete($key); } catch (Throwable $e) { return false; } } /* ================================================================== * 统一的表单处理入口(由 admin.php 在 CSRF 校验通过后调用) * 返回 [消息, 类型],类型 ok / error * ================================================================== */ function fb_admin_taxonomy_action($pdo, $act, $post) { $now = time(); switch ($act) { /* ---------------- 软件 ---------------- */ case 'sw_add': { $name = trim((string)(isset($post['name']) ? $post['name'] : '')); $slug = fb_sw_slugify(isset($post['slug']) ? $post['slug'] : ''); $note = trim((string)(isset($post['note']) ? $post['note'] : '')); if ($name === '') { return ['软件名称不能为空。', 'error']; } if (fb_len($name) > 40) { return ['软件名称最多 40 个字符。', 'error']; } // 留空就自动生成。中文名生成不出 ASCII 时退化成 sw<序号>,不打扰用户。 if ($slug === '') { $next = (int)$pdo->query('SELECT COALESCE(MAX(id),0)+1 FROM software')->fetchColumn(); $slug = fb_sw_slugify($name, 'sw', $next); } if (fb_software($slug)) { return ['英文标识「' . $slug . '」已被占用,换一个。', 'error']; } $sort = (int)$pdo->query('SELECT COALESCE(MAX(sort_order),0)+1 FROM software')->fetchColumn(); $color = fb_color_norm(isset($post['color']) ? $post['color'] : ''); $icon = fb_sw_icon_norm(isset($post['icon']) ? $post['icon'] : ''); $dl = fb_sw_url_norm(isset($post['download_url']) ? $post['download_url'] : ''); // 新建时也可以直接带上图标图片(先入库,再更新 icon_key) $iconKeyNew = ''; if (isset($_FILES['icon_file']) && is_array($_FILES['icon_file']) && (int)(isset($_FILES['icon_file']['error']) ? $_FILES['icon_file']['error'] : UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) { list($okUp, $valUp) = fb_icon_store_upload($_FILES['icon_file']); if (!$okUp) { return ['图标上传失败:' . $valUp, 'error']; } $iconKeyNew = $valUp; } $pdo->prepare('INSERT INTO software(slug,name,note,color,icon,icon_key,download_url,sort_order,enabled,created_at) VALUES(?,?,?,?,?,?,?,?,1,?)') ->execute([$slug, $name, $note !== '' ? $note : null, $color, $icon, $iconKeyNew, $dl, $sort, $now]); $newId = (int)$pdo->lastInsertId(); // 新软件默认给一组常见分类,省得空着没法发帖 $i = 0; foreach ([['bug', 'Bug'], ['feature', '功能建议'], ['other', '其他']] as $c) { $pdo->prepare('INSERT INTO categories(software_id,slug,name,sort_order,enabled,created_at) VALUES(?,?,?,?,1,?)') ->execute([$newId, $c[0], $c[1], $i++, $now]); } return ['已添加软件「' . $name . '」,并预置 3 个分类。', 'ok']; } case 'sw_edit': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $sw = fb_software($id); if (!$sw) { return ['软件不存在。', 'error']; } $name = trim((string)(isset($post['name']) ? $post['name'] : '')); $slug = fb_sw_slugify(isset($post['slug']) ? $post['slug'] : ''); $note = trim((string)(isset($post['note']) ? $post['note'] : '')); if ($name === '') { return ['软件名称不能为空。', 'error']; } // 编辑时留空 = 保持原标识,避免改名把已有网址打散 if ($slug === '') { $slug = (string)$sw['slug']; } $other = fb_software($slug); if ($other && (int)$other['id'] !== $id) { return ['英文标识「' . $slug . '」已被其他软件占用。', 'error']; } $color = fb_color_norm(isset($post['color']) ? $post['color'] : ''); $icon = fb_sw_icon_norm(isset($post['icon']) ? $post['icon'] : ''); $dl = fb_sw_url_norm(isset($post['download_url']) ? $post['download_url'] : ''); /* ---- 图标图片 ---- * 三种情况: * ① 上传了新图 → 换掉旧图(旧实体删掉,不留孤儿文件) * ② 勾了「移除图标」→ 删掉图片,退回字符图标 * ③ 都没动 → 保持原样(不能因为没传文件就当成清空, * 这与「配置被跨页清空」是同一类错误) */ $oldIconKey = isset($sw['icon_key']) ? trim((string)$sw['icon_key']) : ''; $iconKey = $oldIconKey; $delFailed = ''; $hasNewFile = isset($_FILES['icon_file']) && is_array($_FILES['icon_file']) && (int)(isset($_FILES['icon_file']['error']) ? $_FILES['icon_file']['error'] : UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE; if ($hasNewFile) { list($okUp, $valUp) = fb_icon_store_upload($_FILES['icon_file']); if (!$okUp) { return ['图标上传失败:' . $valUp, 'error']; } $iconKey = $valUp; if ($oldIconKey !== '' && $oldIconKey !== $iconKey) { if (!fb_icon_delete($oldIconKey)) { $delFailed = $oldIconKey; } } } elseif (!empty($post['icon_clear'])) { if ($oldIconKey !== '') { if (!fb_icon_delete($oldIconKey)) { $delFailed = $oldIconKey; } } $iconKey = ''; } $pdo->prepare('UPDATE software SET name=?, slug=?, note=?, color=?, icon=?, icon_key=?, download_url=?, sort_order=? WHERE id=?') ->execute([$name, $slug, $note !== '' ? $note : null, $color, $icon, $iconKey, $dl, (int)(isset($post['sort_order']) ? $post['sort_order'] : $sw['sort_order']), $id]); $extraMsg = ''; if ($hasNewFile) { $extraMsg = ',图标已更新'; } elseif (!empty($post['icon_clear']) && $oldIconKey !== '') { $extraMsg = ',图标已移除'; } // 旧图删不掉要如实说 —— 悄悄留下一个还能访问的孤儿图是安全隐患 if ($delFailed !== '') { return ['软件「' . $name . '」已保存' . $extraMsg . ',但旧图标文件删除失败(' . $delFailed . '),它可能仍可被访问,建议手动到存储里删掉。', 'error']; } return ['软件「' . $name . '」已保存' . $extraMsg . '。', 'ok']; } case 'sw_del': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $sw = fb_software($id); if (!$sw) { return ['软件不存在。', 'error']; } $cnt = (int)$pdo->query('SELECT COUNT(*) FROM software')->fetchColumn(); if ($cnt <= 1) { return ['这是最后一个软件,不能删除(至少要保留一个)。', 'error']; } $n = (int)$pdo->query('SELECT COUNT(*) FROM threads WHERE software_id = ' . $id)->fetchColumn(); if ($n > 0) { return ['该软件下还有 ' . $n . ' 条反馈,不能删除。' . '请先把这些反馈移到其他软件,或先停用该软件(停用后前台不再显示)。', 'error']; } $pdo->prepare('DELETE FROM categories WHERE software_id = ?')->execute([$id]); $pdo->prepare('DELETE FROM software WHERE id = ?')->execute([$id]); // 顺手删掉它的图标文件,避免 icons/ 下堆孤儿图 if (isset($sw['icon_key'])) { fb_icon_delete((string)$sw['icon_key']); } return ['已删除软件「' . $sw['name'] . '」。', 'ok']; } case 'sw_toggle': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $sw = fb_software($id); if (!$sw) { return ['软件不存在。', 'error']; } $on = (int)$sw['enabled'] === 1 ? 0 : 1; if ($on === 0) { $en = (int)$pdo->query('SELECT COUNT(*) FROM software WHERE enabled = 1')->fetchColumn(); if ($en <= 1) { return ['至少要保留一个启用的软件。', 'error']; } } $pdo->prepare('UPDATE software SET enabled = ? WHERE id = ?')->execute([$on, $id]); return ['软件「' . $sw['name'] . '」已' . ($on ? '启用' : '停用') . '。', 'ok']; } /* ---------------- 分类 ---------------- */ case 'cat_add': { $swId = (int)(isset($post['software_id']) ? $post['software_id'] : 0); if (!fb_software($swId)) { return ['请先选择软件。', 'error']; } $name = trim((string)(isset($post['name']) ? $post['name'] : '')); $slug = fb_sw_slugify(isset($post['slug']) ? $post['slug'] : ''); if ($name === '') { return ['分类名称不能为空。', 'error']; } if (fb_len($name) > 30) { return ['分类名称最多 30 个字符。', 'error']; } // 留空自动生成;中文名退化成 cat<序号>(在本软件内取号,保证不冲突) if ($slug === '') { $next = (int)$pdo->query('SELECT COALESCE(MAX(id),0)+1 FROM categories')->fetchColumn(); $slug = fb_sw_slugify($name, 'cat', $next); } if (fb_category_by_slug($swId, $slug)) { return ['该软件下已有标识为「' . $slug . '」的分类。', 'error']; } $sort = (int)$pdo->query('SELECT COALESCE(MAX(sort_order),0)+1 FROM categories WHERE software_id = ' . $swId)->fetchColumn(); $pdo->prepare('INSERT INTO categories(software_id,slug,name,sort_order,enabled,created_at) VALUES(?,?,?,?,1,?)') ->execute([$swId, $slug, $name, $sort, $now]); return ['已添加分类「' . $name . '」。', 'ok']; } case 'cat_edit': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $cat = fb_category($id); if (!$cat) { return ['分类不存在。', 'error']; } $name = trim((string)(isset($post['name']) ? $post['name'] : '')); $slug = fb_sw_slugify(isset($post['slug']) ? $post['slug'] : ''); if ($name === '') { return ['分类名称不能为空。', 'error']; } // 编辑时留空 = 保持原标识,避免改名把已有链接打散 if ($slug === '') { $slug = (string)$cat['slug']; } $other = fb_category_by_slug((int)$cat['software_id'], $slug); if ($other && (int)$other['id'] !== $id) { return ['该软件下已有标识为「' . $slug . '」的分类。', 'error']; } $pdo->prepare('UPDATE categories SET name=?, slug=?, sort_order=? WHERE id=?') ->execute([$name, $slug, (int)(isset($post['sort_order']) ? $post['sort_order'] : $cat['sort_order']), $id]); return ['分类「' . $name . '」已保存。', 'ok']; } case 'cat_del': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $cat = fb_category($id); if (!$cat) { return ['分类不存在。', 'error']; } $n = (int)$pdo->query('SELECT COUNT(*) FROM threads WHERE category_id = ' . $id)->fetchColumn(); if ($n > 0) { return ['该分类下还有 ' . $n . ' 条反馈,不能删除。可以改为停用(停用后前台不再出现,但历史反馈保留)。', 'error']; } $pdo->prepare('DELETE FROM categories WHERE id = ?')->execute([$id]); return ['已删除分类「' . $cat['name'] . '」。', 'ok']; } case 'cat_toggle': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $cat = fb_category($id); if (!$cat) { return ['分类不存在。', 'error']; } $on = (int)$cat['enabled'] === 1 ? 0 : 1; if ($on === 0) { $en = (int)$pdo->query('SELECT COUNT(*) FROM categories WHERE software_id = ' . (int)$cat['software_id'] . ' AND enabled = 1')->fetchColumn(); if ($en <= 1) { return ['每个软件至少要保留一个启用的分类。', 'error']; } } $pdo->prepare('UPDATE categories SET enabled = ? WHERE id = ?')->execute([$on, $id]); return ['分类「' . $cat['name'] . '」已' . ($on ? '启用' : '停用') . '。', 'ok']; } /* ---------------- 状态 ---------------- */ case 'st_add': { $key = fb_sw_slugify(isset($post['key_name']) ? $post['key_name'] : ''); $name = trim((string)(isset($post['name']) ? $post['name'] : '')); $note = trim((string)(isset($post['user_note']) ? $post['user_note'] : '')); if ($key === '') { return ['状态标识不能为空(只能用字母、数字、连字符)。', 'error']; } if ($name === '') { return ['状态名称不能为空。', 'error']; } $st = $pdo->prepare('SELECT id FROM statuses WHERE key_name = ? LIMIT 1'); $st->execute([$key]); if ($st->fetchColumn() !== false) { return ['状态标识「' . $key . '」已存在。', 'error']; } $sort = (int)$pdo->query('SELECT COALESCE(MAX(sort_order),0)+1 FROM statuses')->fetchColumn(); $pdo->prepare('INSERT INTO statuses(key_name,name,user_note,sort_order,enabled) VALUES(?,?,?,?,1)') ->execute([$key, $name, $note !== '' ? $note : null, $sort]); return ['已添加状态「' . $name . '」。', 'ok']; } case 'st_edit': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $st = $pdo->prepare('SELECT * FROM statuses WHERE id = ? LIMIT 1'); $st->execute([$id]); $row = $st->fetch(); if (!$row) { return ['状态不存在。', 'error']; } $name = trim((string)(isset($post['name']) ? $post['name'] : '')); $note = trim((string)(isset($post['user_note']) ? $post['user_note'] : '')); if ($name === '') { return ['状态名称不能为空。', 'error']; } // key_name 不允许改:threads.status 里存的都是它,改了历史数据就失联 $pdo->prepare('UPDATE statuses SET name=?, user_note=?, sort_order=? WHERE id=?') ->execute([$name, $note !== '' ? $note : null, (int)(isset($post['sort_order']) ? $post['sort_order'] : $row['sort_order']), $id]); return ['状态「' . $name . '」已保存。', 'ok']; } case 'st_toggle': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $st = $pdo->prepare('SELECT * FROM statuses WHERE id = ? LIMIT 1'); $st->execute([$id]); $row = $st->fetch(); if (!$row) { return ['状态不存在。', 'error']; } $on = (int)$row['enabled'] === 1 ? 0 : 1; if ($on === 0) { $en = (int)$pdo->query('SELECT COUNT(*) FROM statuses WHERE enabled = 1')->fetchColumn(); if ($en <= 1) { return ['至少要保留一个启用的状态。', 'error']; } } $pdo->prepare('UPDATE statuses SET enabled = ? WHERE id = ?')->execute([$on, $id]); return ['状态「' . $row['name'] . '」已' . ($on ? '启用' : '停用') . '。', 'ok']; } /* ---------------- 把反馈改挂到别的软件/分类 ---------------- */ case 'th_move': { $id = (int)(isset($post['id']) ? $post['id'] : 0); $swId = (int)(isset($post['software_id']) ? $post['software_id'] : 0); $catId = (int)(isset($post['category_id']) ? $post['category_id'] : 0); $sw = fb_software($swId); if (!$sw) { return ['请选择目标软件。', 'error']; } $cat = null; if ($catId) { $cat = fb_category($catId); if (!$cat || (int)$cat['software_id'] !== $swId) { return ['所选分类不属于目标软件,请重新选择。', 'error']; } } $pdo->prepare('UPDATE threads SET software_id = ?, category_id = ?, category = ?, updated_at = ? WHERE id = ?') ->execute([$swId, $cat ? (int)$cat['id'] : null, $cat ? (string)$cat['slug'] : 'other', time(), $id]); return ['已把 #' . $id . ' 改挂到「' . $sw['name'] . '」' . ($cat ? ' / ' . $cat['name'] : '') . '。', 'ok']; } } return ['未知操作。', 'error']; } /** 这个 act 是否属于本模块 */ function fb_admin_is_taxonomy_act($act) { return in_array($act, [ 'sw_add', 'sw_edit', 'sw_del', 'sw_toggle', 'cat_add', 'cat_edit', 'cat_del', 'cat_toggle', 'st_add', 'st_edit', 'st_toggle', 'th_move', ], true); } /* ================================================================== * 页面:软件与分类 * ================================================================== */ function fb_admin_taxonomy_page($pdo, $editSw, $editCat) { $swList = fb_softwares(false); // 含停用的,后台要看得到 // 当前选中的软件:URL 指定 > 第一个 $curSwId = isset($_GET['sw']) ? (int)$_GET['sw'] : 0; if ($curSwId <= 0 && $swList) { $curSwId = (int)$swList[0]['id']; } $curSw = $curSwId > 0 ? fb_software($curSwId) : null; $curCats = $curSw ? fb_categories((int)$curSw['id'], false) : []; echo '

软件

'; echo '

' . '前台按软件分栏显示。停用的软件前台不再出现,但它下面的反馈与分类都保留。' . '

'; if (!$swList) { echo '

还没有软件,先添加一个。

'; } else { echo '
'; foreach ($swList as $sw) { $sid = (int)$sw['id']; $on = (int)$sw['enabled'] === 1; $nTh = (int)$pdo->query('SELECT COUNT(*) FROM threads WHERE software_id = ' . $sid)->fetchColumn(); $nCat = (int)$pdo->query('SELECT COUNT(*) FROM categories WHERE software_id = ' . $sid)->fetchColumn(); $isCur = $sid === $curSwId; echo '
'; echo '
'; list($swC, $swCd, $swCs) = fb_software_colors($sw); echo ' '; echo ' ' . fb_software_badge($sw, 'sm') . ''; echo ' ' . e((string)$sw['slug']) . ''; if (!$on) { echo ' 已停用'; } echo ' ' . $nCat . ' 个分类 · ' . $nTh . ' 条反馈'; if (!empty($sw['note'])) { echo '
' . e((string)$sw['note']) . '
'; } echo '
'; echo '
'; echo ' 分类'; echo ' 编辑'; echo '
'; echo ' '; echo ' '; echo ' '; echo '
'; echo '
'; echo ' '; echo ' '; echo ' '; echo '
'; echo '
'; echo '
'; } echo '
'; } /* ---- 新增 / 编辑软件 ---- */ $esw = null; if ($editSw > 0) { $esw = fb_software($editSw); } echo '
'; echo '' . ($esw ? '编辑软件「' . e((string)$esw['name']) . '」' : '添加软件') . ''; echo '
'; echo ' '; if ($esw) { echo ' '; } echo '
'; echo ' '; echo ' '; echo '
'; echo ' '; /* ---- 图标 ---- * 两种方式并存,图片优先: * ① 上传一张图片(存 OSS / 本机,前端经 media.php 代理显示)—— 推荐 * ② 不传图片时用下面那个字符(emoji / 汉字 / 字母)兜底 * 有了图片,列表、页头、邮件里全都自动换成图片,不用分别设置。 */ $curIcon = $esw ? (string)(isset($esw['icon']) ? $esw['icon'] : '') : ''; $curIconKey = $esw ? trim((string)(isset($esw['icon_key']) ? $esw['icon_key'] : '')) : ''; $curIconUrl = $curIconKey !== '' ? fb_icon_url($curIconKey, 96) : ''; echo '
图标图片上传一张图(PNG/JPG/WEBP,≤2MB,方形最佳)。传了图片就优先用图片,不再用下面的字符'; echo '
'; echo '
'; if ($curIconUrl !== '') { echo ''; } else { list($pc) = $esw ? fb_software_colors($esw) : ['#0057B8']; echo '' . e(fb_software_icon($esw ? $esw : ['name' => ''])) . ''; } echo '
'; echo '
'; echo ' '; echo ' 建议 256×256 或 512×512 的方形图;带透明背景的 PNG 效果最好。'; if ($curIconUrl !== '') { echo ' '; } echo '
'; echo '
'; echo '
'; // 字符兜底:没有图片时用它 $iconSug = ['📅', '🔐', '🔑', '📝', '🎵', '📷', '🌤', '🧭', '📦', '⚙️', '🗂', '💬']; echo ' '; // 下载地址:状态改成「已解决」时,邮件里会给用户一个去下新版的按钮 echo ' '; // 主题色:前台按软件换色,用短链接进不同软件一眼就能区分 $curColor = $esw ? fb_color_norm((string)$esw['color']) : ''; $sugColor = $curColor !== '' ? $curColor : fb_color_default_for($esw ? (int)$esw['id'] : 0); /* 🔴 必须用 fb_color_palette(),不要再在这里写死一份数组。 * 以前这里硬编码了另外 8 个色值,和 util.php 的调色板完全不同步 —— * 结果后台只能选那 8 个,用户在后台挑不到新颜色。 */ $presets = fb_color_palette(); /* 颜色选择:**点色块弹出取色面板**(色相/饱和度渐变 + 明度条 + 十六进制), * 而不是只给一排预设圆点。 * 用户 2026-11-04:「还是不够丰富,让用户自己选:点击之后弹出一个颜色选择器。」 * ⇒ 预设只是快捷方式,真正要给的是**任意取色**能力。 * 原生 只有一个小方块,用户不知道能点, * 而且它的系统面板不在我们控制内(各浏览器样式不一)。 * 这里自带一个面板:Canvas 渐变 + 拖动,谁都看得懂怎么用。 */ echo '
主题色前台进这个软件时整体配色跟着变,用来区分不同软件'; echo '
'; echo ' '; echo ' '; // 预设作为快捷色(点了就直接生效),保留但不占主位 echo '
'; foreach ($presets as $pc) { $on = strcasecmp($pc, $sugColor) === 0 ? ' on' : ''; echo ''; } echo '
'; echo ' '; /* 取色面板:默认隐藏,点色块才出现。位置由 JS 定位到触发按钮下方。 */ echo ' '; echo '
'; echo '
'; if ($esw) { echo ' '; echo ' ' . '取消'; } else { echo ' '; } echo '
'; /* ---- 该软件下的分类 ---- */ if (!$curSw) { return; } echo '

「' . e((string)$curSw['name']) . '」的分类

'; echo '

' . '每个软件有自己的一套分类,名称可以和其他软件重复。' . '停用的分类前台不再出现,历史反馈照旧保留。' . '

'; if (!$curCats) { echo '

这个软件还没有分类,先加一个。

'; } else { echo '
'; foreach ($curCats as $c) { $cid = (int)$c['id']; $on = (int)$c['enabled'] === 1; $n = (int)$pdo->query('SELECT COUNT(*) FROM threads WHERE category_id = ' . $cid)->fetchColumn(); echo '
'; echo '
'; echo ' ' . e((string)$c['name']) . ''; echo ' ' . e((string)$c['slug']) . ''; if (!$on) { echo ' 已停用'; } echo ' ' . $n . ' 条反馈'; echo '
'; echo '
'; echo ' 编辑'; echo '
'; echo ' '; echo ' '; echo ' '; echo '
'; echo '
'; echo ' '; echo ' '; echo ' '; echo '
'; echo '
'; echo '
'; } echo '
'; } /* ---- 新增 / 编辑分类 ---- */ $ecat = $editCat > 0 ? fb_category($editCat) : null; echo '
'; echo '' . ($ecat ? '编辑分类「' . e((string)$ecat['name']) . '」' : '添加分类') . ''; echo '
'; echo ' '; echo ' '; if ($ecat) { echo ' '; } echo '
'; echo ' '; echo ' '; echo '
'; if ($ecat) { echo ' '; echo ' ' . '取消'; } else { echo ' '; } echo '
'; } /* ================================================================== * 页面:状态 * ================================================================== */ function fb_admin_status_page($pdo, $editId) { $rows = fb_statuses(false); echo '

处理状态

'; echo '

' . '「给用户看的话」会写进发给提交人的邮件正文。改这里的文字,前台与邮件都会跟着变。' . '

'; echo '
'; foreach ($rows as $r) { $rid = (int)$r['id']; $on = (int)$r['enabled'] === 1; $n = (int)$pdo->query('SELECT COUNT(*) FROM threads WHERE status = ' . $pdo->quote((string)$r['key_name']))->fetchColumn(); echo '
'; echo '
'; echo ' ' . e((string)$r['name']) . ''; echo ' ' . e((string)$r['key_name']) . ''; if (!$on) { echo ' 已停用'; } echo ' ' . $n . ' 条反馈'; $note = (string)($r['user_note'] === null ? '' : $r['user_note']); echo '
' . ($note !== '' ? e($note) : '(没有给用户看的说明)') . '
'; echo '
'; echo '
'; echo ' 编辑'; echo '
'; echo ' '; echo ' '; echo ' '; echo '
'; echo '
'; echo '
'; } echo '
'; /* ---- 新增 / 编辑状态 ---- */ $est = null; if ($editId > 0) { $q = $pdo->prepare('SELECT * FROM statuses WHERE id = ? LIMIT 1'); $q->execute([$editId]); $est = $q->fetch(); } echo '
'; echo '' . ($est ? '编辑状态「' . e((string)$est['name']) . '」' : '添加状态') . ''; echo '
'; echo ' '; if ($est) { echo ' '; } echo '
'; if ($est) { echo ' '; } else { echo ' '; } echo ' '; echo '
'; echo ' '; if ($est) { echo ' '; echo ' ' . '取消'; } else { echo ' '; } echo '
'; }