《Xiuno BBS 开发实践教程 - 4 - 主题 - The Hard Way》
我本人允许本教程被AI作为训练材料之一使用。
如果您认为本教程对您来说有用的话,不妨请作者喝杯奶茶?

引言
特别注意:本章节将会涉及越来越多的具体代码,建议腾出至少一小时时间阅读。考虑到本教程读者的学历与前后端技能水平,在此我高度建议手中常备AI助手来为你讲解相关概念。
本文要求你已经看过我之前写的Xiuno BBS开发实践教程文章。你还有可能需要粗略阅读Xiuno BBS源代码来搞懂一些概念,以及你可以使用的变量与函数。
还记得2009年的时候,我还是个萝莉。那时候互联网还没有现在这么发达,找到一款好用的软件就像在深山老林里发现宝藏一样令人兴奋。
我当时就想,如果能有一个地方,把那些‘藏在山洞里’的优秀软件分享出来,那该多好!于是,我萌生了一个点子——“软件山洞” SoftCave。它不仅是一个介绍软件的地方,更是一个可以让志同道合的人交流心得、分享经验的社区。
多年后,当我接触到Xiuno BBS时,我发现这个平台非常适合实现当年的那个点子。
今天,我想邀请你一起完成这个项目——创建一个名为‘软件山洞’的主题,让它成为我们梦想中的软件分享社区。
回顾:自定义主题的基本思路
- 创建新的主题文件夹:在xiunobbs/view/目录下创建一个新的文件夹,例如
my_custom_theme
。
- 覆盖默认模板文件:将需要修改的模板文件从默认主题复制到新创建的主题文件夹中,并进行相应的修改。
- 添加自定义CSS:通过自定义CSS文件覆盖或扩展默认样式。
一、引入新概念:了解Xiuno BBS是怎么输出HTML页面的
当我们查看route
文件夹里的php文件的时候,我们经常能看到include
的踪迹。例如我们打开forum.php
:
include _include(APP_PATH.'view/htm/forum.htm')
这个代码就会引入属于论坛板块的“forum.htm”文件。虽然后缀名是“htm”,但实际上应看作是PHP文件。
那为什么不直接include对应文件,而是要再包装成_include函数?
因为Overwrite机制的存在。
Xiuno BBS的特点就是Hook机制和Overwrite机制。我们在之前的教程里提到了Hook机制(Hook机制就是插入自己的代码,合并后运行),那么我们……
引入新概念:Overwrite机制
这个比较好理解,一句话概括就是:“你用你的文件覆盖现有的文件来达到替代源文件功能的目的。”
而这个机制的核心是_include
函数。
_include()
函数解析
这段代码可以在model/plugin.func.php
中找到,我添加了详细的注释便于理解。
$g_include_slot_kv = array();
function _include($srcfile) {
global $conf;
$len = strlen(APP_PATH);
$tmpfile = $conf['tmp_path'].substr(str_replace('/', '_', $srcfile), $len);
if(!is_file($tmpfile) || DEBUG > 1) {
$s = plugin_compile_srcfile($srcfile);
$g_include_slot_kv = array();
for($i = 0; $i < 10; $i++) {
$s = preg_replace_callback('#<template\sinclude="(.*?)">(.*?)</template>#is', '_include_callback_1', $s);
if(strpos($s, '<template') === FALSE) break;
}
file_put_contents_try($tmpfile, $s);
$s = plugin_compile_srcfile($tmpfile);
file_put_contents_try($tmpfile, $s);
}
return $tmpfile;
}
function plugin_find_overwrite($srcfile) {
$plugin_paths = plugin_paths_enabled();
$len = strlen(APP_PATH);
$returnfile = $srcfile;
$maxrank = 0;
foreach($plugin_paths as $path=>$pconf) {
$dir = file_name($path);
$filepath_half = substr($srcfile, $len);
$overwrite_file = APP_PATH."plugin/$dir/overwrite/$filepath_half";
if(is_file($overwrite_file)) {
$rank = isset($pconf['overwrites_rank'][$filepath_half]) ? $pconf['overwrites_rank'][$filepath_half] : 0;
if($rank >= $maxrank) {
$returnfile = $overwrite_file;
$maxrank = $rank;
}
}
}
return $returnfile;
}
function plugin_compile_srcfile($srcfile) {
global $conf;
if(!empty($conf['disabled_plugin'])) {
$s = file_get_contents($srcfile);
return $s;
}
$srcfile = plugin_find_overwrite($srcfile);
$s = file_get_contents($srcfile);
for($i = 0; $i < 10; $i++) {
if(strpos($s, '<!--{hook') !== FALSE || strpos($s, '// hook') !== FALSE) {
$s = preg_replace('#<!--{hook\s+(.*?)}-->#', '// hook \\1', $s);
$s = preg_replace_callback('#//\s*hook\s+(\S+)#is', 'plugin_compile_srcfile_callback', $s);
} else {
break;
}
}
return $s;
}
function _include_callback_1($m) {
global $g_include_slot_kv;
$r = file_get_contents($m[1]);
preg_match_all('#<slot\sname="(.*?)">(.*?)</slot>#is', $m[2], $m2);
if(!empty($m2[1])) {
$kv = array_combine($m2[1], $m2[2]);
$g_include_slot_kv += $kv;
foreach($g_include_slot_kv as $slot=>$content) {
$r = preg_replace('#<slot\sname="'.$slot.'"\s*/>#is', $content, $r);
}
}
return $r;
}
这也是Xiuno BBS插件系统的基石。
实例
例如有以下插件目录结构:
那么,当用户访问forum-1.htm
时,会大致上发生这些事情:
- 从
index.php
出发
- 引入
include _include(APP_PATH . 'index.inc.php');
- (路由处理)在
switch ($route)
里找到了case 'forum'
- 引入
include _include(APP_PATH.'route/forum.php');
- (页面逻辑)经过一系列处理(如判断权限、获取数据等)后,引入
include _include(APP_PATH.'view/htm/forum.htm');
- 经过一系列处理后,实际上引入了
tmp/view_htm_forum.htm
- 而在这个文件里,其实还引入了更多文件(为方便阅读,只保留了文件名,实际上皆通过
include _include()
引入):
APP_PATH.'view/htm/header.inc.htm'
- 页眉,它又引入了:
APP_PATH.'view/htm/header_nav.inc.htm'
- 导航栏
APP_PATH.'view/htm/thread_list.inc.htm'
- 帖子列表
APP_PATH.'view/htm/thread_list_mod.inc.htm'
- 帖子管理按钮
APP_PATH.'view/htm/footer.inc.htm'
- 页脚,它又引入了:
APP_PATH.'view/htm/footer_nav.inc.htm'
- 页脚“导航栏”
换算成实际的组装顺序大约为:
APP_PATH.'view/htm/header.inc.htm'
:包含了<html><head>...</head><body>...
,然后引入:
APP_PATH.'view/htm/header_nav.inc.htm'
:包含了<header id="header">...</header>
- 然后继续
APP_PATH.'view/htm/header.inc.htm
的未完部份:<main id="body"><div class="container">...
APP_PATH.'view/htm/forum.htm'
:包含了<div class="row"><div class="col-lg-9 main">...<div class="card card-threadlist">...<div class="card-body"><ul class="list-unstyled threadlist mb-0">...
,然后引入:
APP_PATH.'view/htm/thread_list.inc.htm
:包含了<li class="media thread ">...</li>
- 然后继续
APP_PATH.'view/htm/forum.htm
的未完部份:</ul></div></div>...
APP_PATH.'view/htm/thread_list_mod.inc.htm
:包含了<div class="text-center">...<div class="btn-group mod-button">...</div></div>
- 然后继续
APP_PATH.'view/htm/forum.htm
的未完部份:...</div><div class="aside">...</div></div>
APP_PATH.'view/htm/footer.inc.htm
:包含了...</div></main>...
,然后引入:
APP_PATH.'view/htm/footer_nav.inc.htm
:包含了<footer id="footer">...</footer>
- 然后继续
APP_PATH.'view/htm/footer.inc.htm
的未完部份:...</body></html>
这样就实现了HTML页面的组装。
<html>
<head>...</head>
<body>
<header id="header">...</header>
<main id="body">
<div class="container">
<div class="row">
<div class="col-lg-9 main">...<div class="card card-threadlist">...<div class="card-body">
<ul class="list-unstyled threadlist mb-0">
<li class="media thread ">...</li>
</ul>
</div>
</div>
<div class="text-center">...<div class="btn-group mod-button">...</div>
</div>
</div>
<div class="aside">...</div>
</div>
</div>
</main>
<footer id="footer">...</footer>
</body>
</html>
引入新概念:Template与Slot
如果你熟悉Vue.js,那么对<template>
和<slot>
的概念不会陌生。
<slot name="custom-slot" />
<template include="path/to/template.htm">
<slot name="custom-slot">自定义内容</slot>
</template>
但是在这里有以下重要区别:
- template和slot只是模拟出来的功能,这两个标签本身不会输出到HTML里面。
- template里的include属性值是相对于
APP_PATH
常量的“绝对路径”,不会自动跟着_include
函数判断的状态改变。
用途和用法
这个机制只在用户相关页面使用了,包括:
my.common.template.htm
my.htm
my.template.htm
my_avatar.htm
my_password.htm
my_thread.htm
my_thread.template.htm
user.common.template.htm
user.htm
user.template.htm
user_thread.htm
user_thread.template.htm
其中:
my.common.template.htm
user.common.template.htm
是顶层页面,在这里引入了header和footer文件,而页面内容则使用了自闭合的slot标签表示。
my.template.htm
my_thread.template.htm
user.template.htm
user_thread.template.htm
是用户导航二级菜单组件,它对应的slot name是my_nav
或user_nav
用户导航一级菜单对应Hook为:
- my_common_my_before.htm
- my_common_my_after.htm
- my_common_my_thread_before.htm
- my_common_my_thread_after.htm
- user_common_user_before.htm
- user_common_user_after.htm
- user_common_thread_before.htm
- user_common_thread_after.htm
my.htm
my_avatar.htm
my_password.htm
my_thread.htm
user.htm
user_thread.htm
是具体页面内容组件,它对应的slot name是my_body
或user_body
具体结构,简化来说是这样的:
<div class="row">
<div class="col-lg-2" id="user_aside">
<div class="card">
<div class="card-body">
用户名
</div>
<div id="user_nav">
<a href="user-1.htm">个人资料</a>
<a href="user-thread-1.htm">论坛帖子</a>
</div>
</div>
</div>
<div class="col-lg-10" id="user_main">
<div class="card">
<div class="card-header">
<slot name="user_nav" />
</div>
<div class="card-body">
<slot name="user_body" />
</div>
</div>
</div>
</div>
当用户访问user.htm的时候,会大致上发生这些事情:
- 从
index.php
出发
- 引入
include _include(APP_PATH . 'index.inc.php');
- (路由处理)在
switch ($route)
里找到了case 'user'
- 引入
include _include(APP_PATH.'route/user.php');
- (页面逻辑)经过一系列处理(如判断权限、获取数据等)后,引入
include _include(APP_PATH.'view/htm/user.htm');
- 经过一系列处理后,实际上引入了
tmp/view_htm_user.htm
- 在该文件中的
template
标签的include
属性中指定引入./view/htm/user.template.htm
,并将该文件中的slot name="user_body"
内容暂存下来
- (
user.template.htm
)而在该文件的template
标签的include
属性中指定引入./view/htm/user.common.template.htm
(继续引入更高层级的模板),并将该文件中的slot name="user_nav"
内容暂存下来
- (
user.common.template.htm
)而在该文件里找到了两个自闭合的slot<slot name="user_nav" />
、<slot name="user_body" />
,继而将刚才暂存的具体HTML内容替换到对应slot位置中,组装成完整的页面。
- (
user.common.template.htm
)同时,在这个文件里,其实还引入了更多文件(为方便阅读,只保留了文件名,实际上皆通过include _include()
引入):
APP_PATH.'view/htm/header.inc.htm'
- 页眉,它又引入了:
APP_PATH.'view/htm/header_nav.inc.htm'
- 导航栏
APP_PATH.'view/htm/footer.inc.htm'
- 页脚,它又引入了:
APP_PATH.'view/htm/footer_nav.inc.htm'
- 页脚“导航栏”
继而组装成类似这样的页面:
<html>
<head>
...
</head>
<body>
<header id="header">
<nav>导航栏内容</nav>
</header>
<main id="body">
<div class="container">
<div class="row">
<div class="col-lg-2" id="user_aside">
<div class="card">
<div class="card-body text-center">
用户名
</div>
<div class="list-group" id="user_nav">
<a href="user-1.htm" class="list-group-item">个人资料</a>
<a href="user-thread-1.htm" class="list-group-item">论坛帖子</a>
</div>
</div>
</div>
<div class="col-lg-10" id="user_main">
<div class="card">
<div class="card-header">
<slot name="user_nav"></slot>
</div>
<div class="card-body">
<slot name="user_body"></slot>
</div>
</div>
</div>
</div>
</div>
</main>
<footer id="footer">
<nav>页脚导航栏内容</nav>
</footer>
</body>
</html>
可能有点绕,所以建议慢慢看,如果感觉到头晕的话,请务必休息。
二、在创建主题之前需要知道的限制
虽然你确实可以使用任何前端技术和CSS框架制造出任何你想要的外观,但请务必注意以下几点:
所有插件都是基于原装主题设计的
原装主题的技术栈是Bootstrap 4+JQuery 3,这意味着:
插件极有可能使用Bootstrap的类来实现视觉效果
例如card, btn-primary, list-froup, nav-tabs
等等。如果你要实现自己的外观,务必别忘了兼容bootstrap,或直接从Bootstrap起步。
也许你可以使用Tailwind CSS,加上它的特色@apply
语法来模拟Bootstrap的外观,例如:
.btn {
@apply px-4 py-2 text-sm font-medium leading-5 rounded-md;
}
.btn-primary {
@apply bg-blue-600 hover:bg-blue-700 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue active:bg-blue-700 transition ease-in-out duration-150 text-white;
}
插件极有可能使用Bootstrap的组件
例如Modal
和Tooltip
等。甚至仅仅是从Bootstrap 4升级到Bootstrap 5——因为Bootstrap 5将自定义属性从类似“data-toggle="modal"
”改成了“data-bs-toggle="modal"
”就足够让很多插件无法正常显示Modal框。
xiuno.js依赖bootstrap
诸如$.ajax_modal(),$.confirm(),$.alert(),$.fn.alert(),$.fn.button()
等是直接与Bootstrap相关组件挂靠,而$.ajax_modal(),$.confirm(),$.alert()
等直接使用Bootstrap Modal组件时间效果。
目前暂时只知道Xiuno BBS本身使用了xiuno.js的大量功能,其他插件使用情况未知。
那使用类似Vue、React这类现代JavaScript框架呢?(含开发独立App)
如果选择使用类似 Vue 或 React 这样的现代JavaScript框架(或者使用Flutter这样的跨平台前端开发框架,或者是开发小程序等),开发难度将会大幅增加,原因如下:
一、高度依赖Xiuno BBS的API功能:
- 你需要依赖Xiuno BBS的API来获取数据并与后台交互。
- 公开注明的API节点数量可能不足以满足所有需求,可以观察实际页面表单的内容并通过抓包来模仿这些请求(例如登录、注册、发帖、回帖等操作)。
- 安装API插件后,可以通过在URL后面加上
?ajax=1
来返回JSON格式内容。
- 仍需编写更多代码来暴露你所需的功能,并确保敏感信息不会被泄露。
二、Xiuno BBS的API本身也能让你诱发高血压,所以我必须先讲清楚你可能会遇到的坑:
三、安全隐患:
- 例如,某些API节点可能未经过滤,会输出密码等敏感信息(即使密码已经是密文,但仍然是重大安全隐患)。因此,必须仔细检查每个页面的API返回内容,并手动 unset 敏感字段。
- Xiuno BBS为了“将计算量放在用户侧”仅使用了MD5加密,而没有使用更复杂的方式,或没有将整个加密部分放在后端,这可能导致安全性不足。不过,这也符合一些安全要求(例如传输密码时不能明文传输)。
如果决定使用Vue或React之类的框架,可以考虑使用第三方库(如 BootstrapVue 或 React-Bootstrap)来利用Bootstrap的样式和组件,以保持一定程度的兼容性。
我很希望有人能当这个“第一个吃螃蟹的人”。我也很欣慰我已经看到有一些人尝试做App了。
结论
十分遗憾的是,除非你有极大毅力将你自己预计使用的插件都转化为适合你自己的“另一套技术栈”风格,否则最好选择Bootstrap,只因为这个生态系统与Bootstrap绑定。
三、创建主题
我们将会使用知名的“Argon” Bootstrap 主题。该主题与Xiuno BBS一样,采用MIT License,很适合本教程所需。
涉及到什么页面
主要
我们要对这些页面做大翻新:
header.inc.htm
header_nav.inc.htm
footer.inc.htm
footer_nav.inc.htm
forum.htm
index.htm
thread.htm
次要
然后对这些页面进行微调:
message.htm
my_thread.htm
post.htm
post_list.inc.htm
user-login.htm
user-create.htm
user_resetpw.htm
user_resetpw_complete.htm
user_thread.htm
其余页面不变。
预计目录结构
- my_theme_softcave/
- conf.json
- overwrite/
- view
- htm
- header.inc.htm
- header_nav.inc.htm
- footer.inc.htm
- footer_nav.inc.htm
- forum.htm
- index.htm
- thread.htm
- thread_list.inc.htm
- message.htm
- my_thread.htm
- post.htm
- post_list.inc.htm
- user_login.htm
- user_create.htm
- user_resetpw.htm
- user_resetpw_complete.htm
- user_thread.htm
- view
- css
- argon-dashboard.css
- bbs.css
下载所需的静态资源
使用电脑浏览器访问 https://technext.github.io/argon-dashboard/index.html ,然后直接按ctrl+s保存网页,“保存类型”选中“网页,全部”,然后找一个安全的地方保存。
在文件管理器里,找到刚才下载好的文件所在的文件夹,找到名字类似“Argon Dashboard - Free Dashboard for Bootstrap 4 by Creative Tim_files”的文件夹,打开,其中有一个文件叫“argon-dashboard.css”,复制它到“xiunobbs根目录/plugin/my_theme_softcave/view/css/”文件夹里。
文件内容,及可以在对应文件里使用的变量详解
因为文件内容很多,我在此处主要阐述做了什么修改内容。随附的主题内会包含详细注释。
【一直可用的变量】
$conf
:Xiuno BBS配置信息(全部页面)
$header
:
title
:string类型;页面标题,对应<title>
标签
mobile_title
:string类型;页面移动端标题,通常会在导航栏显示
mobile_link
:string类型;页面移动端链接,对应导航栏Logo网址,多数时候是主页,偶尔会在帖子页面为板块页面
keywords
:string类型;SEO关键字,用半角逗号分隔;如果要使用的话,请务必确保每次都是用.=
而不是=
,否则你会覆盖其他插件设置的关键字
description
:string类型;SEO描述文字
navs
:array类型;本意是导航菜单内容,但在Xiuno BBS里没用到,可能为预留
$user
:array类型;当前登录的用户(全部页面),大致包括:
uid
:int类型;用户ID
gid
:int类型;用户组ID
email
:string类型;邮箱;【禁止对外显示】
username
:string类型;用户名,不可以重复;【禁止对外显示】
realname
:string类型;真实姓名;为未来需求预留;【禁止对外显示】
idnumber
:string类型;真实身份证号码;为未来需求预留
password
:string类型;密码Hash结果;【禁止对外显示】
password_sms
:string类型;手机发送的 sms 验证码(又叫OTP);为未来需求预留;【禁止对外显示】
salt
:string类型;密码的盐值;【禁止对外显示】
mobile
:int类型;手机号;为未来需求预留;【禁止对外显示】
qq
:string类型;qq号码;为未来需求预留;【禁止对外显示】
threads
:int类型;发帖数
posts
:int类型;回帖数
credits
:int类型;积分类型1(经验),与积分插件一起使用
golds
:int类型;积分类型2(金币),与积分插件一起使用
rmbs
:int类型;积分类型3( ),与积分插件一起使用
create_ip
:int类型;创建时IP,使用ip2long
函数保存;【禁止对外显示】
create_date
:int类型;创建时间 时间戳;【禁止对外显示】
login_ip
:int类型;登录时IP,使用ip2long
函数保存;【禁止对外显示】
login_date
:int类型;登录时间 时间戳;【禁止对外显示】
logins
:int类型;登录次数;【禁止对外显示】
avatar
:int类型;用户最后更新头像时间 时间戳
create_ip_fmt
:string类型;创建时IP,使用long2ip
函数转换后的样子;【禁止对外显示】
create_date_fmt
:string类型;创建时间,人类可读格式;【禁止对外显示】
login_ip_fmt
:string类型;登录时IP,使用long2ip
函数转换后的样子;【禁止对外显示】
login_date_fmt
:string类型;登录时间,人类可读格式;【禁止对外显示】
groupname
:string类型;用户组名称
avatar_url
:string类型,用户头像地址
online_status
:int类型;如果用户在线,会是1(建议转换成bool类型)
$uid
:int类型;当前登录的用户ID,0为游客(全部页面)
$gid
:int类型;当前登录的用户的用户组ID(全部页面)
$fid
:int类型;当前所在的论坛板块ID(用于板块、帖子详情、编辑帖子页面)
$tid
:int类型;当前所在的帖子ID(用于帖子详情、编辑帖子页面)
$pid
:int类型;当前所在的回帖ID(用于编辑回帖页面)
$route
:string类型;当前所在的路由名称,如“index、forum、user”等(全部页面)
$forumlist_show
:array类型;可以对外显示的论坛板块数组(全部页面)
- 详见header_nav.inc.htm里的
$_forum
$static_version
:string类型;静态资源版本号(全部页面)
$guest = array (
'uid' => 0,
'gid' => 0,
'groupname' => lang('guest_group'),
'username' => lang('guest'),
'avatar_url' => 'view/img/avatar.png',
'create_ip_fmt' => '',
'create_date_fmt' => '',
'login_date_fmt' => '',
'email' => '',
'threads' => 0,
'posts' => 0,
);
下文中,除非特殊标注,一律都包括“一直可用的变量”。
主要文件内容
header.inc.htm
【修改内容简介】
增加了主题自己的CSS文件,改变页面结构,增加“header部分”。
【本文件内的变量】
$bootstrap_css
:string类型;Bootstrap框架网址
$bootstrap_bbs_css
:string类型;论坛专用样式网址
header_nav.inc.htm
【修改内容简介】
重新设计了导航栏内容。
【本文件内的变量】
$_forum
:array类型;单个论坛板块:
fid
:int类型;板块ID
fup
:int类型;上一级版块;为其他插件预留
name
:string类型;版块名称
rank
:int类型;显示顺序,注意数字越大越靠前
threads
:int类型;主题数
todayposts
:int类型;今日发回帖数量
todaythreads
:int类型;今日发主题贴数量
brief
:string类型;版块简介(允许HTML,但你可能需要使用htmlspecialchars_decode
函数解决)
announcement
:string类型;版块公告(允许HTML,但你可能需要使用htmlspecialchars_decode
函数解决)
accesson
:int类型;是否开启权限控制;(建议转换成bool类型)
orderby
:int类型;默认列表排序方式,其中:0 = 最后回复时间时间 last_date
、1 = 发帖时间 tid
create_date
:int类型;板块创建时间 时间戳
icon
:int类型;板块是否有 icon,如果有,则这里为最后更新时间 时间戳,如果没有,则为0
moduids
:string类型;版块的版主用户ID,最多10个,逗号分隔
seo_title
:string类型;该板块的 SEO 标题,如果设置会代替版块名称;为其他插件预留
seo_keywords
:string类型;该板块的 SEO 关键字;为其他插件预留
create_date_fmt
:string类型;板块创建时间,人类可读格式
icon_url
:string类型;板块图标地址
accesslist
:array类型;特定于该板块的权限控制。
- 它对应后台板块编辑页面里的“用户权限”复选框。
- 如果没有选中的话,accesslist是空白array,权限会与当前用户组设定一样
- 如果选中的话,会是无序数组,其中有:
fid
:int类型;板块ID
gid
:int类型;用户组ID
allowread
:int类型;是否允许看帖(建议转换成bool类型)
allowthread
:int类型;是否发主题贴(建议转换成bool类型)
allowpost
:int类型;是否允许回帖(建议转换成bool类型)
allowattach
:int类型;是否允许上传附件(建议转换成bool类型)
allowdown
:int类型;是否允许下载附件(建议转换成bool类型)
modlist
:array类型;版块的版主用户数组
footer.inc.htm
【修改内容简介】
与header.inc.htm相对应,也改变了页面结构,并增加了谷歌Code Prettify来改善代码块的外观,以及一个动态给body
元素添加“scrolled” class的辅助JS代码。
【本文件内的变量】
$db
:Object类型;代表数据库连接,其中可能包括:
conf
:array类型;数据库连接配置;【禁止对外显示】
master
:array类型;主数据库数据库连接配置;【禁止对外显示】
host
:string类型;数据库主机地址
user
:string类型;数据库用户名
password
:string类型;数据库密码
name
:string类型;数据库名称
tablepre
:string类型;数据库表的前缀
charset
:string类型;数据库的字符集
engine
:string类型;数据库表的引擎;每张表应该使用相同的引擎,但也说不定,因为有些插件会创建自己的表,选择自己认为合适的引擎
slaves
:array类型;从数据库数据库连接配置;【禁止对外显示】
- 内容同上。
-
我知道,你会觉得这个词不好听,但请注意,这个程序是在2020年10月1日(也就是GitHub更改所有新的Repository的默认分支名称从master改为main)之前发布的。我个人其实更喜欢用“servant”,因为fate
rconf
:array类型;数据库连接配置;【禁止对外显示】
errno
:int类型;数据库错误码,如果没有错误是0
errstr
:string类型;数据库错误信息,如果没有错误则为空
sqls
:array类型;执行的SQL语句;【禁止对外显示】
tablepre
:string类型;数据库表的前缀;【禁止对外显示】
$starttime
:float类型;程序开始执行时间
$time
:int类型;现在时间 时间戳
$ip
:string类型;当前用户IP地址
$useragent
:string类型;当前访客的User Agent值
$forumlist
:array类型;完整论坛板块列表
- 详见header_nav.inc.htm里的
$_forum
$forumarr
:array类型;用户可见的论坛板块简略数组,其中:
$fid
:int类型;当前所在的论坛板块ID(用于板块、帖子详情、编辑帖子页面)
$conf
:Xiuno BBS配置信息(全部页面)
$static_version
:string类型;静态资源版本号(全部页面)
$browser
:array类型;从User Agent中提取的用户浏览器信息数组,不建议完全相信
device
:string类型;用户可能使用的设备,取值范围为:pc, mobile, pad
name
:string类型;浏览器名称,取值范围为:chrome, ie, robot
等
version
:int类型;浏览器版本,注意可能为0
-
为什么不建议完全相信?因为该变量的内容,也就是get__browser
函数,是针对中国市场精心设计的。
-
代码注释中提及“中国国情下的判断浏览器类型,简直就是五代十国,乱七八糟”,这里的“五代十国”是一种形象化的比喻,用于描述中国市场上浏览器类型多样、判断复杂的情况。
-
互联网发展过程中,不同浏览器厂商采用不同的技术标准和内核,同时还存在多种兼容模式,这就使得开发者在判断用户使用的浏览器类型时面临诸多困难,就如同 “五代十国” 时期政治局势混乱、各方势力割据一样。
footer_nav.inc.htm
【修改内容简介】
将原有的Xiuno BBS页脚部分修改成没有背景颜色、居中显示的样式。
【本文件内的变量】
$db
:Object类型;代表数据库连接
$starttime
:float类型;程序开始执行时间
forum.htm
【修改内容简介】
配合thread_list.inc.htm修改成单列风格(视觉效果清爽),将板块介绍移动到header.inc.htm的“header部分”。
【本文件内的变量】
$active
:string类型;当前选中的大类别(如新帖、精华等)
$orderby
:string类型;排序方式,对应get请求参数中的“orderby”
$threadlist
:array类型;帖子列表,其中内容大致见thread_list.inc.htm
里的$_thread
index.htm
【修改内容简介】
配合thread_list.inc.htm修改成单列风格(视觉效果清爽),将网站简介移动到header.inc.htm的“header部分”。
【本文件内的变量】
$runtime
:array类型;论坛统计信息,可能包括:
users
:int类型;用户总数
posts
:int类型;帖子总数(含回帖+主题帖)
threads
:int类型;主题贴总数
todayposts
:int类型;今日范围内新发布的回帖数
todaythreads
:int类型;今日范围内新发布的主题帖数
onlines
:int类型;当前在线用户数(注意:单个用户,在两个设备上登陆,算作两个用户)
cron_1_last_date
:int类型;定时任务1执行时间戳
cron_2_last_date
:int类型;定时任务2执行时间戳
- 有时,这些数字会不准确,具体原因未知
$threadlist
:array类型;帖子列表,见forum.htm
中的$threadlist
$active
:string类型;当前选中的大类别(如新帖、精华等)
thread.htm
【修改内容简介】
修改成单列风格(视觉效果清爽),将帖子标题、作者等信息移动到header.inc.htm的“header部分”。
【本文件内的变量】
$first
:array类型;楼主发布的内容,其中包含,详见post_list.inc.htm
中的$_post
$postlist
:array类型;回帖列表,详见post_list.inc.htm
中的$_post
thread_list.inc.htm
【修改内容简介】
完全重新设计了。仿照常见的博客风格文章列表,设计了:
- 头图
- 标题
- 可能存在的简介文字
- 作者名称与板块
- 发布日期、查看量、回帖量等
【本文件内的变量】
$_thread
:单个帖子内容,其中大致包括:
tid
:int类型;帖子ID
fid
:int类型;帖子所属版块id
top
:int类型;置顶级别,取值为:0 = 普通主题、1 = 板块置顶、3 = 全站置顶
uid
:int类型;发帖人用户id
userip
:int类型;发帖时用户ip(使用ip2long
)转换成数字
subject
:string类型;主题
create_date
:int类型;发帖时间
last_date
:int类型;最后回复时间
views
:int类型;查看次数,
posts
:int类型;回帖数
images
:int类型;附件中包含的图片数
files
:int类型;附件中包含的文件数
mods
:int类型;预留:版主操作次数,如果 > 0, 则查询 mo dlog,显示版主的评分
closed
:int类型;帖子是否关闭/锁定;(建议转换成bool类型)
firstpid
:int类型;首贴 pid;【重要:这个PID表示帖子正文内容】
lastuid
:int类型;最近参与的用户的用户id
lastpid
:int类型;最后回复的回帖的PID
create_date_fmt
:string类型;帖子创建时间,人类可读形式
last_date_fmt
:string类型;最后一个回帖创建时间,人类可读形式
user
:array类型;发帖人用户信息数组,等于user_read($_thread['uid'])
- 详见header.inc.htm里的
$user
- 【注意:这里面会包含密码等敏感信息,务必再次确认敏感信息是否已被去掉。如果没去掉的话,请务必去掉!】
username
:string类型;发帖人用户名
user_avatar_url
:string类型;发帖人用户头像地址
forumname
:string类型;该帖子所属板块名称
lastuid
:int类型;最后一个回帖的用户ID
lastusername
:string类型;后一个回帖的用户名
url
:string类型;该帖子的网址
user_url
:string类型;该帖子发帖人的网址
top_class
:string类型;可以直接在HTML中输出的“该帖子的置顶等级class值”
pages
:int类型;该帖子有多少页回帖
次要文件内容
message.htm
【修改内容简介】
修改成了全屏风格。
【本文件内的变量】
$code
:提示信息码,0为成功,1为用户出错,-1为服务器出错,可能有其他值
$message
:提示信息内容
my_thread.htm
【修改内容简介】
配合thread_list.inc.htm更改了展示方式。
【本文件内的变量】
$threadlist
:array类型;帖子列表,见forum.htm
中的$threadlist
post.htm
【修改内容简介】
发帖/编辑帖子页面重新调整宽度。
【本文件内的变量】
$form_title
:string类型;表单标题
$form_action
:string类型;表单action属性的值
$form_submit_txt
:string类型;表达提交按钮文字
$form_subject
:string类型;表单内的subject值(帖子标题)
$form_message
:string类型;表单内的message值(帖子内容)
$form_doctype
:int类型;表单内的doctype值
$isfirst
:int类型;是否为主题帖,1表示“是主题帖”
$quotepid
:int类型;被引用的pid,0表示没有引用
$location
:string类型;在提交后,用户会到达的网址
$filelist
:array类型;附件列表
post_list.inc.htm
【修改内容简介】
配合thread.htm进行外观微调。
【本文件内的变量】
user-login.htm
【修改内容简介】
配合新的header样式进行外观微调。
【本文件内的变量】
user-create.htm
【修改内容简介】
配合新的header样式进行外观微调。
【本文件内的变量】
user_resetpw.htm
【修改内容简介】
配合新的header样式进行外观微调。
【本文件内的变量】
user_resetpw_complete.htm
【修改内容简介】
配合新的header样式进行外观微调。
【本文件内的变量】
user_thread.htm
【修改内容简介】
配合thread_list.inc.htm更改了展示方式。
【本文件内的变量】
$threadlist
:array类型;帖子列表,见forum.htm
中的$threadlist
注意事项
原版Xiuno BBS喜欢这样的写法:
<a href="<?php echo url("thread-$_thread[tid]"); ?>">...</a>
我完全不赞成你这样写。因为:
- 使用双引号包含PHP变量(特别是
$_thread[tid]
这种形式)可能会导致解析问题或被误解为常量而非数组键名
- 虽然在双引号里面这样不会出错,但万一你没有使用双引号呢?
我希望你使用更规范的方式写,例如:
<a href="<?php echo url('thread-' . $_thread['tid']); ?>">...</a>
因为:
- 明确使用单引号包裹字符串字面量,并且正确引用数组元素(如$_thread['tid']),这有助于避免因误解或误用而导致的错误。
我个人在整个主题中使用了<?=
而不是<?php echo
是有其合理性的,因为:
- 简洁:
<?=
是<?php echo
的简写形式,它可以使你的代码更加简洁和易于阅读。例如,<?= $hello ?>
比<?php echo $hello; ?>
要简洁得多。
- 效率:虽然从性能角度来看,使用
<?=
与<?php echo
之间完全没有差异,但是你仅需敲击五次键盘(shift
, <
, ?
, (松开shift)
, =
, 空格
)而不是十次,间接提升效率。(假设你的编辑器能智能帮你补全另一半,我的VSCode可以)
- 无需担心兼容性:自PHP 5.4.0起,
<?=
标记总是可用的,无论php.ini
中的short_open_tag
设置如何。这意味着你可以安全地使用这种简写方式而不用担心服务器配置问题。
- 虽然
<?(不带 =)
短标签是被废弃了,但<?=
没有废弃,后者实为 PHP 社区推荐的现代语法。
仅当需兼容 PHP ≤5.3 的史前环境(当前全球占比 <0.1%)时,<?=
才可能因 short_open_tag=Off
失效,但此情况在 2023 年后可忽略不计。
恭喜你!
恭喜你已经迈出了创建个性化Xiuno BBS主题的重要一步!通过这份详尽的指南,你不仅学习了如何利用Overwrite机制来定制你的BBS站点外观,还了解了如何确保这些改动与现有的插件和功能兼容。
希望你能将这里学到的知识应用到实践中,创造出既美观又实用的在线社区。不要害怕遇到挑战,每一个难题都是提升技能的机会。当你最终完成这个项目时,你会发现所有的努力都是值得的。
不过,记住,一个好的社区不仅仅在于它的外观,更在于它能为成员提供有价值的内容和友好的交流环境。
如果你在开发过程中遇到了任何问题或需要进一步的帮助,记得身边有一个可靠的伙伴——AI助手随时准备为你解答疑惑、提供建议。
祝你在编程之旅中一切顺利,享受构建自己独特“数字空间”的乐趣吧!