Xiuno BBS 开发实践教程 - 网站大事记
本文要求你已经阅读并理解了《Xiuno BBS 开发实践教程 - 单页》一文,以及了解PHP、HTML、MySQL等基础知识。
并额外期待你了解JavaScript Ajax请求(尤其是使用jQuery发送Ajax请求)相关知识。
注意:本文可能枯燥无味(因为涉及更多代码),但如果你愿意耐心阅读,那么你将会收获很多。
本文将会更加聚焦于代码本身,辅佐适当的注释来解释代码的细节。
需求分析
你盯着之前制作好的单页插件,心想:“嘿,要是有个地方能记录我们网站成长的每一步,岂不是更酷?”这不仅仅是给新用户一个了解网站历史的机会,也是老用户回忆往昔的温馨角落。于是,你决定开始新的冒险——创建一个“网站大事记”。
于是你就找到了核心需求:你想要将网站发展过程中的大事件记录下来,放在单独的页面里。
因为“网站发展过程中的大事件”的数据结构比单页复杂一些,所以数据结构选择创建数据库表。
表的名字是bbs_events_log
,字段有:
字段名 | 数据类型 | 是否为空 | 默认值 | 备注 |
---|---|---|---|---|
id | int | NOT NULL | AUTO_INCREMENT | 主键 |
title | varchar(255) | NOT NULL | 标题 | |
content | text | NOT NULL | 内容 | |
create_time | int | NOT NULL | 发布时间 |
通过这样的设计,我们可以轻松地记录每一次重要的更新或活动,并以友好的方式向用户展示这些信息。接下来,我们将一步步实现这个功能。
你知道吗?在古代,人们用石碑来记录重大事件,而今天,我们则用代码和数据库。技术进步的速度,真让人惊叹!
了解新概念
Xiuno BBS的数据库函数
Xiuno BBS将PDO等数据库操作函数再次封装成db
系列函数,开发者无需关心PDO等底层细节,也方便用户使用不同的数据库引擎,如MySQL、SQLite等。
函数包括:
/**
* 执行 SQL 查询并返回单条记录。
*
* @param string $sql SQL 查询语句
* @param object|null $d 数据库连接对象,默认为NULL,将使用全局默认连接
* @return array|bool 成功返回查询结果数组,失败返回 FALSE
*/
function db_sql_find_one($sql, $d = NULL);
/**
* 执行 SQL 查询并返回多条记录。
*
* @param string $sql SQL 查询语句
* @param string|null $key 指定返回数组的键名,默认为NULL
* @param object|null $d 数据库连接对象,默认为NULL,将使用全局默认连接
* @return array|bool 成功返回查询结果数组,失败返回 FALSE
*/
function db_sql_find($sql, $key = NULL, $d = NULL);
/**
* 执行 SQL 语句。
*
* @param string $sql SQL 语句
* @param object|null $d 数据库连接对象,默认为NULL,将使用全局默认连接
* @return mixed 根据执行的 SQL 类型返回相应结果
*/
function db_exec($sql, $d = NULL);
/**
* 计算指定表中满足条件的记录数。
*
* @param string $table 表名
* @param array $cond 条件数组
* @param object|null $d 数据库连接对象,默认为NULL,将使用全局默认连接
* @return int|FALSE 成功返回计数值,失败返回 FALSE
*/
function db_count($table, $cond = array(), $d = NULL);
/**
* 获取指定表中的最大ID值。
*
* @param string $table 表名
* @param string $field 字段名
* @param array $cond 条件数组
* @param object|null $d 数据库连接对象,默认为NULL,将使用全局默认连接
* @return int|FALSE 成功返回最大ID值,失败返回 FALSE
*/
function db_maxid($table, $field, $cond = array(), $d = NULL);
/**
* 插入一条新记录到指定表。
*
* @param string $table 表名
* @param array $arr 要插入的数据数组
* @param object|null $d 数据库连接对象,默认为NULL,将使用全局默认连接
* @return int|FALSE 成功返回插入的ID或受影响的行数,失败返回 FALSE
*/
function db_insert($table, $arr, $d = NULL);
/**
* 替换一条记录到指定表,如果存在则更新,不存在则插入。
*
* @param string $table 表名
* @param array $arr 要替换的数据数组
* @param object|null $d 数据库连接对象,默认为NULL,将使用全局默认连接
* @return int|FALSE 成功返回插入的ID或受影响的行数,失败返回 FALSE
*/
function db_replace($table, $arr, $d = NULL);
/**
* 更新指定表中的记录。
*
* @param string $table 表名
* @param array $cond 条件数组
* @param array $update 要更新的数据数组
* @param object|null $d 数据库连接对象,默认为NULL,将使用全局默认连接
* @return int|FALSE 成功返回受影响的行数,失败返回 FALSE
*/
function db_update($table, $cond, $update, $d = NULL);
/**
* 删除指定表中的记录。
* 慎用!
* @param string $table 表名
* @param array $cond 条件数组
* @param object|null $d 数据库连接对象,默认为NULL,将使用全局默认连接
* @return int|FALSE 成功返回受影响的行数,失败返回 FALSE
*/
function db_delete($table, $cond, $d = NULL);
/**
* 清空指定表中的所有数据。
* 慎用!
* @param string $table 表名
* @param object|null $d 数据库连接对象,默认为NULL,将使用全局默认连接
* @return bool 成功返回 TRUE,否则返回 FALSE
*/
function db_truncate($table, $d = NULL);
/**
* 读取指定表中的一条记录。
*
* @param string $table 表名
* @param array $cond 条件数组
* @param object|null $d 数据库连接对象,默认为NULL,将使用全局默认连接
* @return array|bool 成功返回查询结果数组,失败返回 FALSE
*/
function db_read($table, $cond, $d = NULL);
/**
* 查找指定表中的记录,可分页。
*
* @param string $table 表名
* @param array $cond 条件数组
* @param array $orderby 排序数组
* @param int $page 页码
* @param int $pagesize 每页显示数量
* @param string $key 返回数组的键名
* @param array $col 要查询的字段列表
* @param object|null $d 数据库连接对象,默认为NULL,将使用全局默认连接
* @return array|bool 成功返回查询结果数组,失败返回 FALSE
*/
function db_find($table, $cond = array(), $orderby = array(), $page = 1, $pagesize = 10, $key = '', $col = array(), $d = NULL);
/**
* 查找指定表中的一条记录。
*
* @param string $table 表名
* @param array $cond 条件数组
* @param array $orderby 排序数组
* @param array $col 要查询的字段列表
* @param object|null $d 数据库连接对象,默认为NULL,将使用全局默认连接
* @return array|bool 成功返回查询结果数组,失败返回 FALSE
*/
function db_find_one($table, $cond = array(), $orderby = array(), $col = array(), $d = NULL);
这将是你操作数据库的基石。
cond 条件数组详解
在使用上述提供的数据库操作函数时,条件数组 $cond
用于构建 SQL 查询的 WHERE 子句。
条件数组的键对应数据库中的字段名称,而值则可以采用多种形式来表达查询条件:
- 简单等值匹配
- 当值不是数组时,表示对字段进行等值匹配。例如,
['id' => 123]
将生成WHERE id = 123
。
- 当值不是数组时,表示对字段进行等值匹配。例如,
- 多值匹配
- 当值是一个包含多个元素的数组时,表示字段的值需要匹配这些元素中的任何一个。例如,
['id' => array(1, 2, 3)]
将生成WHERE (id=1 OR id=2 OR id=3)
。
- 当值是一个包含多个元素的数组时,表示字段的值需要匹配这些元素中的任何一个。例如,
- 范围匹配
- 当值是一个关联数组,其中键为比较运算符(如
>
、<
、=
等),值为比较值时,表示字段的值需要满足特定的范围或比较条件。例如,array('age' => array('>' => 18, '<=' => 30))
将生成WHERE age > 18 AND age <= 30
。 - 支持小于、大于、小于等于、大于等于等比较运算符。
- 当值是一个关联数组,其中键为比较运算符(如
- 模糊匹配
- 当值是一个关联数组,且键为
LIKE
(全大写) 时,表示字段的值需要部分匹配给定的字符串。例如,['name' => array('LIKE' => 'John')]
将生成WHERE name LIKE '%John%'
。你不需要特意写百分号,它会自动添加。
- 当值是一个关联数组,且键为
orderby 排序数组详解
排序数组 $orderby
用于构建 SQL 查询的 ORDER BY 子句,以确定查询结果的排序方式。数组的键对应数据库中的字段名称,而值决定了排序的方向:
- 如果值为 1,则表示该字段按升序排列
ASC
。例如,['name' => 1]
将对用ORDER BY name ASC
。 - 如果值不是 1(通常是0或-1),则表示该字段按降序排列
DESC
。例如,['date' => -1]
将对用ORDER BY date DESC
。- 务必注意这一点,如果不是1,则表示降序排列。
- 因为这个确实是有误导性,因为只用数字是看不出来排序方向的。如果你愿意的话,请自行定义两个常量,比如
const ASC = 1; const DESC = 0;
来消除误会。- 再次强调:如果不是1,则表示降序排列。
排序数组可以包含多个字段,以实现多级排序。例如,['name' => 1, 'date' => -1]
将首先按name字段升序排序,如果name字段值相同,则按date字段降序排序。
- 排序的顺序将按照数组中的顺序进行。
key参数详解(用于db_find、db_sql_find)
假设
bbs_thread
表中有以下数据(简化例子):
tid subject content username create_time 80 第一主题 这是第一个帖子的内容 用户A 2023-01-01 81 第二主题 这是第二个帖子的内容 用户B 2023-01-02 82 第三主题 这是第三个帖子的内容 用户C 2023-01-03
当指定了$key
参数时,返回数组的键名则为$key
参数指定的字段内容。例如,db_find('bbs_thread', array(), array(), 1, 20, 'tid')
返回的数组的键将会是tid
的内容。在foreach循环等场合可能有用。
返回的数组将是:
array(
80 => array(
'tid' => 80,
'subject' => '第一主题',
'content' => '这是第一个帖子的内容',
'username' => '用户A',
'create_time' => '2023-01-01'
),
81 => array(
'tid' => 81,
'subject' => '第二主题',
'content' => '这是第二个帖子的内容',
'username' => '用户B',
'create_time' => '2023-01-02'
),
82 => array(
'tid' => 82,
'subject' => '第三主题',
'content' => '这是第三个帖子的内容',
'username' => '用户C',
'create_time' => '2023-01-03'
)
)
col参数详解(用于db_find、db_find_one)
用于指定要查询的字段列表,默认为空数组,表示查询所有字段。例如:db_find('bbs_thread', array(), array(), 1, 20, '', array('tid','subject'))
将只会返回tid和subject字段的内容。适合“我不想要那么多数据”的场景。
返回的数组将是:
array(
0 => array(
'tid' => 80,
'subject' => '第一主题'
),
1 => array(
'tid' => 81,
'subject' => '第二主题'
),
2 => array(
'tid' => 82,
'subject' => '第三主题'
)
)
我应该获取所有字段的内容还是只获取一部分字段的内容?
虽然这是一个相对主观的决策,但Xiuno BBS的立场为我们提供了一些有价值的见解。
Xiuno BBS的观点:
在Xiuno BBS的架构中,由于缓存层的存在,即使使用了 SELECT *
,也不会对性能造成太大的影响,因为缓存能够减少数据库的直接访问次数。
另外我们坚持用 SELECT * 而不写长条字段,也是有原因的,因为我们可以在中间加入缓存。比如用户数据,我们按条去,按条缓存,在开启 memcached, yac 后,中间的这些 SQL 都消失了。
权衡利弊:
使用 SELECT *
的优点
- 不需要列出所有所需的字段名,减少了代码维护的工作量。
- 当数据库表结构发生变化时(如增加新字段),不需要修改查询代码。
- 在缓存机制完善的情况下,可以减少数据库的直接访问。
使用 SELECT *
的缺点
- 在某些情况下,可能会暴露敏感信息,如密码等。
- 所以xiuno bbs选择在获取之后进行处理,如
user_format
函数会删掉密码字段。
- 所以xiuno bbs选择在获取之后进行处理,如
- 在没有缓存或故意不使用缓存的情况下,查询大量数据会影响性能。
- 一些开发者往往忽视缓存机制,导致查询大量数据时对性能的影响。我就不在此点出是谁了。
指定字段查询的优点
- 只查询需要的字段,减少了数据传输和处理的开销。
- 避免暴露不必要的字段信息,如密码等。
- 在数据库层面减少了数据的读取和处理量。
- 但是缓存机制的存在,已经可以减少数据的读取和处理量了。
指定字段查询的缺点
- 当数据库表结构发生变化时,需要更新查询代码。
- 但万一你忘了呢?如果你不自己看数据库里有哪些列,那你可能会不知道新的结构是什么。用
SELECT *
可以让你一直知道最新的结构。
- 但万一你忘了呢?如果你不自己看数据库里有哪些列,那你可能会不知道新的结构是什么。用
- 如果遗漏了某些必要的字段,可能会导致程序出错。
- 例如你的插件写死了“获取某些字段”,但未来数据库表结构发生变化,你没有将新的字段加入查询代码中,就相当于你遮住了自己的眼睛。
你的服务器硬盘
- 如果你使用的是SSD,那么指定字段查询所节约的开销并不明显。
- 如果你使用的是机械硬盘,那么缓存机制会填补这部分性能损失。
- 你可能会认为PHP这种“过时的语言”是导致网站性能低的原因之一,但我不这么认为。哪怕你用PHP 7,只要开启OPCache,也能有不错的性能;而使用PHP 8再开启JIT,性能会进一步提升。
- 因此,无论你是使用PHP 7还是PHP 8,只要合理配置并充分利用其优化特性,都可以获得出色的性能表现。
- 此外,结合良好的数据库设计和缓存策略,你完全不必担心PHP会成为瓶颈。
综上所述
你应该放心地使用 SELECT *
。
jQuery Ajax请求
为了让你能更方便地与服务器进行数据交互,Xiuno BBS提供了jQuery Ajax请求的封装函数。当然,jQuery自身的Ajax相关函数保持原样,只是Xiuno BBS再次封装了一遍。
为什么要用Ajax?
- 页面不刷新,用户体验更好。
- 不是所有的操作都必须刷新页面的。
- 减轻服务器压力,提高性能。
- 因为Xiuno BBS的设计哲学之一就是“将一些任务交给客户端来做,而不是让服务器来处理”。
- 解耦代码,提高可维护性。
- 虽然Xiuno BBS依旧是很传统的那种PHP+HTML的架构,但Xiuno BBS是支持API输出的,所以用Ajax是非常明智的选择。
标准jQuery Ajax请求
Get
$.get()
方法用于发送GET请求,获取数据。基本语法如下:
$.get('your-url-here', { key: 'value' }, function(response) {
console.log(response);
}, 'json');
- 第一个参数是请求的URL。
- 第二个参数是发送到服务器的数据。
- 第三个参数是请求成功时的回调函数。
- 第四个参数是预期的返回数据类型。
Post
$.post()
方法用于发送POST请求,通常用于提交表单数据。基本语法如下:
$.post('your-url-here', { key: 'value' }, function(response) {
console.log(response);
}, 'json');
参数与$.get()
相同,但请求类型为POST。
Xiuno BBS的Ajax请求
XGet
与标准的 $.get()
方法不同,$.xget()
在处理非 JSON 响应时能够返回错误回调,并且支持重试机制。
参数:
- url (
string
):请求的 URL。 - callback (
function|callable
):请求完成后的回调函数。回调函数接收两个参数:code
和message
。code
(number
):0 表示成功,非 0 表示失败。- 通常1是用户出错,-1是服务器出错。
- 但很奇葩的是,Xiuno BBS允许在
code
字段里写string
类型的数据,用于指示具体哪个表单控件产生错误。你不要这样写啊。 - 这要求服务器必须以这个格式返回数据:
{code:0, message:'具体内容'}
。
message
(any
):服务器返回的数据。可能是string或object。
- retry (
number
):重试次数,默认为 0(不重试)。
用法:
$.xget('your-url-here', function(code, message) {
if (code === 0) {
alert('成功');
} else {
alert('错误:' + message);
}
}, 3); // 最多重试3次
XPost
与标准的 $.post()
方法不同,$.xpost()
在处理非 JSON 响应时能够返回错误回调,并且支持进度回调函数。
参数:
- url (
string
):请求的 URL。 - postdata (
object|string
):发送到服务器的数据。 - callback (
function|callable
):请求完成后的回调函数。回调函数接收两个参数:code
和message
。- 这要求服务器必须以这个格式返回数据:
{code:0, message:'具体内容'}
。
- 这要求服务器必须以这个格式返回数据:
- progress_callback (
function
):进度回调函数,参数为一个数值(0-100),表示请求的进度。- 对于上传文件来说很有用。
用法:
$.xpost('your-url-here', "key=value", function(code, message) {
// 请求完成后的回调
if (code === 0) {
alert('成功');
} else {
alert('出现错误');
console.warn('错误:' + message);
}
}, function(progress) {
// 进度回调
console.log('上传进度:' + progress.toFixed(2) + '%');
// 在这里你就可以改变进度条读数了
document.querySelector('progress#upload_progress').value = progress.toFixed(0);
});
开始制作插件
自述文件 conf.json
创建文件夹my_events_log
,在文件夹内创建conf.json
文件。
内容如下:
{
"name":"网站大事记",
"brief":"教程插件",
"version":"1.0.0",
"bbs_version":"4.0.4",
"installed":0,
"enable":0,
"hooks_rank":[],
"overwrites_rank":[],
"dependencies":[]
}
安装插件文件 install.php
在安装插件的时候,我们需要往数据库里创建表,才能让插件正常工作。
在文件夹my_events_log
内创建install.php
文件。
<?php
// 防止直接访问
!defined('DEBUG') AND exit('Forbidden');
// 数据库表前缀
$tablepre = $db->tablepre;
$sql = "CREATE TABLE IF NOT EXISTS {$tablepre}events_log (
id INT NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
create_time INT NOT NULL,
PRIMARY KEY (id)
);";
// 执行创建表的SQL
$r = db_exec($sql);
卸载插件文件 unstall.php
卸载的时候可以将之前创建的表删掉。
在文件夹my_events_log
内创建unstall.php
文件。
<?php
// 防止直接访问
!defined('DEBUG') AND exit('Forbidden');
// 数据库表前缀
$tablepre = $db->tablepre;
$sql = "DROP TABLE IF EXISTS {$tablepre}events_log;";
// 执行
$r = db_exec($sql);
业务函数
在my_events_log/
文件夹内创建model
文件夹,然后进入model
文件夹,创建文件events_log.func.php
,内容如下:
<?php
// 【R】根据条件查询网站大事记列表。
function events_log_find($cond = [], $sort = ['id'=>0], $page = 1, $pagesize = 20) {
$r = db_find('events_log', $cond, $sort, $page, $pagesize);
return $r;
}
// 【R】根据ID读取网站大事记内容。
function events_log_read($id) {
$r = db_find_one('events_log', array('id'=>$id));
return $r;
}
// 【C】根据ID创建网站大事记内容。
function events_log_create($title, $content) {
$arr = array(
'title'=>$title,
'content'=>$content,
'create_time'=>time(),
);
$r = db_insert('events_log', $arr);
return $r;
}
// 【U】根据ID更新网站大事记内容。
function events_log_update($id,$title,$content) {
$arr = array(
'id' => $id,
'title'=>$title,
'content'=>$content,
'create_time'=>time(),
);
$r = db_replace('events_log', $arr );
return $r;
}
// 【D】根据ID删除网站大事记内容。
function events_log_delete($id) {
$r = db_delete('events_log', array('id'=>$id));
return $r;
}
这就是最基础的CRUD(Create、Read、Update和Delete)操作。
然后将业务函数注册到Xiuno BBS中
刚才的文件需要注册到Xiuno BBS中,才能被Xiuno BBS调用。
在my_events_log/
文件夹内创建hook
文件夹,然后进入hook
文件夹,创建文件model_inc_file.php
,内容如下:
APP_PATH.'plugin/my_events_log/model/events_log.func.php',
我们可以观察Xiuno BBS目录/model.inc.php
,在这个文件中首先是$include_model_files
,注册了大量文件,这些文件都是Xiuno BBS内置的“Model层”业务代码。而最后一个model文件后面有个注释// hook model_inc_file.php
,我们将自己的model文件路径放在这个hook中,就能让Xiuno BBS加载我们的model文件。
然后继续看$include_model_files
的后面,有一个foreach
循环,用于注册Xiuno BBS内置的Model层文件。简化版本为:
foreach ($include_model_files as $model_files) {
include _include($model_files);
}
这个应该很好理解,就是Xiuno BBS加载所有注册的model文件。这样,我们就能在xiuno bbs的任何地方调用我们的model文件中的函数。
你甚至可以用
$include_model_files
数组来判断某插件是否已经安装,因为有些插件会注册自己的model文件到这里。因为$include_model_files
是个全局变量,所以可以随处用foreach等手段来遍历。
// 判断某个插件是否已经安装的Flag变量 $is_some_plugin_installed = false; //开始遍历 foreach ($include_model_files as $filename) { if(str_contains($filename, 'some_plugin')) { $is_some_plugin_installed = true; //我们要找的插件已经安装 break; //找到后就立刻跳出循环 } else { continue; //继续遍历;用continue加速循环 } } if (!function_exists('str_contains')) { // 因为是php 8函数所以需要写一个polyfill function str_contains($haystack, $needle) { return $needle !== '' && mb_strpos($haystack, $needle) !== false; //意思是$haystack中是否包含$needle } }
创建路由并编写业务逻辑
进入my_events_log/hook
文件夹创建文件index_route_case_end.php
,内容如下:
case 'events_log':
include APP_PATH.'plugin/my_events_log/route/events_log.php';
break;
接着,我们需要创建处理页面请求的具体逻辑。
在my_events_log
文件夹里创建route
文件夹。进入route
文件夹,然后创建名为events_log.php
的文件。此文件将包含处理展示、添加、编辑和删除大事记条目的逻辑。
<?php
// 获取操作类型
// 这样会对应events_log-{action}.htm的请求
$action = param(1, 'list');
switch ($action) {
case 'add':
// 添加新事件
if (isset($_POST['submit'])) {
// 更新的数据
$data = [
'title' => strip_tags(param('title', '')), // 标题不需要HTML标签
'content' => param('content', ''),
];
// 在前端虽然用required可以保证用户输入对应框的内容,但用户不一定会用网页提交请求,所以在后端进行检查是有必要的
if (empty($data['title'])) {
message(2, "请输入标题!");
}
if (empty($data['content'])) {
message(2, "请输入内容!");
}
$r = events_log_create($data["title"], $data['content']);
if ($r) {
message(0, "添加成功!");
} else {
message(1, "添加失败,请重试!");
}
} else {
include APP_PATH . 'plugin/my_events_log/view/htm/add.htm';
}
break;
case 'edit':
// 编辑现有事件
$id = param('id', 0);
$events_log = events_log_read($id);
if ($events_log) {
// 当有数据时才能编辑
if (isset($_POST['submit'])) {
// 更新的数据
$data = [
'title' => strip_tags(param('title', '')), // 标题不需要HTML标签
'content' => param('content', ''),
];
// 在前端虽然用required可以保证用户输入对应框的内容,但用户不一定会用网页提交请求,所以在后端进行检查是有必要的
if (empty($data['title'])) {
message(2, "请输入标题!");
}
if (empty($data['content'])) {
message(2, "请输入内容!");
}
$r = events_log_update($id, $data["title"], $data['content']);
if ($r) {
message(0, "更新成功!");
} else {
message(1, "更新失败,请重试!");
}
} else {
$id = param('id', 0);
$event = events_log_read($id);
include APP_PATH . 'plugin/my_events_log/view/htm/edit.htm';
}
} else {
// 否则立刻提示错误
message(1, '事件记录不存在');
}
break;
case 'delete':
// 删除事件
$r = events_log_delete(param('id', 0));
if ($r) {
message(0, "删除成功!");
} else {
message(1, "删除失败,可能已经删掉了");
}
break;
case 'list':
// 列出所有事件
// 当前页码
$page = param('page', 1);
// 每页显示多少条
$pagesize = 10;
// 事件总数
$tmp_events_result = events_log_find([], ['create_time' => 0], 1, 1000);
if (is_array($tmp_events_result)) {
$total = count($tmp_events_result);
} else {
$total = 0;
}
// 事件数据
$events = events_log_find([], ['create_time' => 0], $page, $pagesize);
// 分页HTML
$pagination = pagination(url("events_log-list-{page}"), $total, $page, $pagesize);
include APP_PATH . 'plugin/my_events_log/view/htm/list.htm';
break;
case 'view':
// 查看事件
$id = param('id', 0);
$events_log = events_log_read($id);
if ($events_log) {
// 当有数据时才能查看
include APP_PATH . 'plugin/my_events_log/view/htm/view.htm';
} else {
// 否则立刻提示错误
message(1, '事件记录不存在');
}
break;
default:
// 当未提供任何操作时,立刻结束;我们不能惯着用户,以为“这能访问所有事件列表”
// 因为Xiuno BBS会在论坛版块等场合默认执行类似于list的操作
message(1, '未知的操作');
break;
}
创建前台页面
为了显示和管理大事记条目,我们需要创建一些页面。在my_events_log
文件夹中创建view
文件夹。进入view
文件夹,在里面创建htm
子文件夹,然后在htm
文件夹里,创建以下文件:
add.htm
——创建edit.htm
——编辑list.htm
——查看(列表)view.htm
——查看(详情)
add.htm
<?php include _include(APP_PATH.'view/htm/header.inc.htm');?>
<div class="container">
<h2 class="page-header">添加新事件</h2>
<a href="<?= url('events_log-list') ?>" class="btn btn-default">返回列表</a>
<form method="post" action="<?= url('events_log-add') ?>">
<div class="form-group">
<label>标题</label>
<input type="text" name="title" class="form-control" required>
</div>
<div class="form-group">
<label>内容</label>
<textarea name="content" class="form-control" rows="6" required></textarea>
</div>
<button type="submit" name="submit" class="btn btn-primary">提交</button>
</form>
</div>
<?php include _include(APP_PATH.'view/htm/footer.inc.htm');?>
edit.htm
其实和add.htm
差不多,只是每个input都预先填充了内容。
<?php include _include(APP_PATH.'view/htm/header.inc.htm');?>
<div class="container">
<h2 class="page-header">编辑事件 <?= $event['id'] ?></h2>
<a href="<?= url('events_log-list') ?>" class="btn btn-default">返回列表</a>
<form method="post" action="<?= url('events_log-edit') ?>">
<input type="hidden" name="id" value="<?= $event['id'] ?>">
<div class="form-group">
<label>标题</label>
<input type="text" name="title" class="form-control" value="<?= $event['title'] ?>" required>
</div>
<div class="form-group">
<label>内容</label>
<textarea name="content" class="form-control" rows="6" required><?= $event['content'] ?></textarea>
</div>
<button type="submit" name="submit" class="btn btn-primary">更新</button>
</form>
</div>
<?php include _include(APP_PATH.'view/htm/footer.inc.htm');?>
list.htm
<?php include _include(APP_PATH.'view/htm/header.inc.htm');?>
<div class="container">
<h2 class="page-header">网站大事记列表</h2>
<!-- 事件列表 -->
<section class="timeline">
<?php /* 只有管理员可用的操作 */ if($uid && intval($gid) === 1): ?>
<!-- 添加新事件按钮 -->
<article class="content card">
<div class="card-body">
<a href="<?= url('events_log-add') ?>" class="btn btn-success">+ 添加事件</a>
</div>
</article>
<?php endif; ?>
<?php /* 如果有事件的话 */ if($events): ?>
<?php /* 遍历每个事件 */ foreach($events as $event): ?>
<!-- 单个事件 -->
<article class="content card">
<div class="card-body">
<!-- 标题 -->
<h3 class="card-title"><?= $event['title'] ?></h3>
<!-- 时间 -->
<p><?= date('Y-m-d H:i', $event['create_time']) ?></p>
<!-- 查看详情按钮 -->
<a href="<?= url('events_log-view', ['id' => $event['id']]) ?>" class="btn btn-link">查看详情</a>
</div>
<?php /* 只有管理员可用的操作 */ if($uid && intval($gid) === 1): ?>
<div class="card-footer">
<a href="<?= url('events_log-edit', ['id' => $event['id']]) ?>" class="btn btn-xs btn-warning">编辑</a>
<a href="<?= url('events_log-delete', ['id' => $event['id']]) ?>" class="btn btn-xs btn-danger" onclick="return confirm('确定删除?')">删除</a>
</div>
<?php endif; ?>
</article>
<?php endforeach; ?>
<?php endif; ?>
</section>
<!-- 分页导航 -->
<div class="text-center">
<?= $pagination ?>
</div>
</div>
<!-- 提前引入时间线样式,我们之后会提到的,相信我 -->
<link rel="stylesheet" href="./plugin/my_events_log/view/css/timeline.css">
<?php include _include(APP_PATH.'view/htm/footer.inc.htm');?>
view.htm
<?php include _include(APP_PATH.'view/htm/header.inc.htm');?>
<div class="container">
<div class="panel card-default">
<div class="card-body">
<h3 class="card-title"><?= $events_log['title'] ?></h3>
<small class="text-muted">发布时间:<?= date('Y-m-d H:i', $events_log['create_time']) ?></small>
</div>
<div class="card-body">
<?= $events_log['content'] ?>
</div>
<div class="card-footer">
<a href="<?= url('events_log-list') ?>" class="btn btn-default">返回列表</a>
</div>
</div>
</div>
<?php include _include(APP_PATH.'view/htm/footer.inc.htm');?>
在导航菜单里添加链接
我们还差一步就可以测试了!我们需要在导航菜单里添加这个新的页面,别人才能方便的找到这个页面并查阅。
进入hook
文件夹,创建文件header_nav_forum_end.htm
,内容如下:
<li class="nav-item">
<a class="nav-link" href="<?= url('events_log-list') ?>">
<i class="icon-clock-o"></i> 大事记
</a>
</li>
亲自测试
实际上,到这一步,整个插件的功能已经完整了。
点击网站顶部导航菜单里的“大事记”,访问我们新制作的“网站大事记”页面。
然后点击“添加事件”按钮,填写如下内容:
- 标题:网站上线
- 内容:我们很高兴地宣布,经过数月的紧张开发与测试,我们的新网站终于正式上线了!感谢所有参与和支持我们项目的朋友们。
然后点击“提交”按钮。如果一切正常,你会看到提示消息:“添加成功!”
你也可以故意不填写某一项,如果一切正常,你会看到提示信息“请输入标题!”或“请输入内容!”
然后,再次点击网站顶部导航菜单里的“大事记”,回到大事记列表页面。现在你应该能看到新的卡片出现,标题为“网站上线”。
点击这个卡片的“查看详情”按钮,应该会看到具体的内容。
然后,再次点击网站顶部导航菜单里的“大事记”,回到大事记列表页面。点击标题为“网站上线”的卡片里的“编辑”按钮。填入以下新的内容:
- 标题:新增评论系统
- 内容:为了增强用户的互动体验,我们推出了一套全新的评论系统。现在,用户可以更方便地分享他们的观点和反馈。
然后点击“提交”按钮。如果一切正常,你会看到提示消息:“更新成功!”
你也可以故意不填写某一项,如果一切正常,你会看到提示信息“请输入标题!”或“请输入内容!”
然后,再次点击网站顶部导航菜单里的“大事记”,回到大事记列表页面。点击标题为“网站上线”的卡片里的“删除”按钮。确认删除后,对应的卡片应该会消失不见。
不过,我们可以更进一步
完成了上述功能测试后,你就已经验证了“网站大事记”插件的基本增删改查功能。恭喜!
界面美化
接下来,为了让用户体验更佳,我们可以对界面进行一些美化。一个美观的界面不仅能提升用户的使用体验,还能让您的网站看起来更加专业和吸引人。
在my_events_log
文件夹的view
文件夹中创建css
文件夹。进入css
文件夹,然后创建文件timeline.css
。内容如下(你可以直接复制):
/* 定义时间线的容器样式 */
.timeline {
/* 设置相对定位,以便其内部元素可以使用绝对定位进行布局 */
position: relative;
}
/* 使用伪元素:before在时间线左侧创建一条竖直的线条 */
.timeline::before {
/* 确保内容为空,因为我们只需要背景色作为竖线 */
content: '';
/* 绝对定位,相对于.timeline容器 */
position: absolute;
/* 竖线宽度设置为6px */
width: 6px;
/* 背景颜色设置为灰色 */
background-color: var(--gray);
/* 从顶部到底部全高 */
top: 0;
bottom: 0;
/* 竖线位于容器的最左侧 */
left: 0%;
/* 向左移动3px以精确对齐 */
margin-left: -3px;
}
/* 定义时间线上每个事件的内容区域样式 */
.timeline .content {
/* 左边距设置为16px,确保文本不会紧贴竖线 */
margin-left: 16px;
}
/* 使用伪元素:after在每个事件内容区域前添加一个小圆点 */
.timeline .content::after {
/* 确保内容为空,因为我们只需要圆形形状 */
content: '';
/* 绝对定位,相对于.content容器 */
position: absolute;
/* 圆形大小设置为25px x 25px */
width: 25px;
height: 25px;
/* 将圆点放置在左边距之外 */
left: -30px;
/* 圆点背景颜色设置为白色 */
background-color: var(--white);
/* 圆点周围添加橙色边框 */
border: 4px solid var(--orange);
/* 圆点垂直居中对齐 */
top: 15px;
/* 圆角半径设置为50%,使其成为圆形 */
border-radius: 50%;
/* 提高堆叠顺序,确保圆点显示在竖线之上 */
z-index: 1;
}
现在,回到大事记列表页面。你应该能看到每一个卡片的左侧会有橙色的圆点和灰色的竖线,来表示“时间线”的概念,使事件展示得更为美观和直观。
Ajax请求
为了进一步提升用户体验,我们可以使用Ajax技术来实现无刷新页面操作。
这样用户在添加或删除事件时,无需重新加载整个页面,从而提供更加流畅的交互体验。
打开文件my_events_log/view/htm/list.htm
,然后作出修改。新的文件内容如下。
<?php include _include(APP_PATH.'view/htm/header.inc.htm');?>
<div class="container">
<h2 class="page-header">网站大事记列表</h2>
<!-- 事件列表 -->
<section class="timeline">
<?php /* 只有管理员可用的操作 */ if($uid && intval($gid) === 1): ?>
<!-- 添加新事件按钮 -->
<article class="content card">
<div class="card-body">
<!-- 这次使用了模态框来提升用户体验 -->
<button type="button" class="btn btn-success" data-toggle="modal" data-target="#addEventModal">+ 添加事件</button>
</div>
</article>
<!-- 添加新事件表单模态框 -->
<div class="modal fade" id="addEventModal" tabindex="-1" role="dialog" aria-labelledby="addEventModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addEventModalLabel">添加新事件</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<!-- 你会发现这个表单和add.htm的如出一辙 -->
<form id="add_event_form" method="post" action="<?= url('events_log-add') ?>">
<input type="hidden" name="submit" value="1">
<div class="form-group">
<label for="title">标题</label>
<input type="text" id="title" name="title" class="form-control" required>
</div>
<div class="form-group">
<label for="content">内容</label>
<textarea id="content" name="content" class="form-control" rows="6" required></textarea>
</div>
<button type="submit" class="btn btn-primary">提交</button>
<button type="reset" class="btn btn-secondary" data-dismiss="modal">取消</button>
</form>
</div>
</div>
</div>
</div>
<?php endif; ?>
<?php /* 如果有事件的话 */ if($events): ?>
<?php /* 遍历每个事件 */ foreach($events as $event): ?>
<!-- 单个事件 -->
<article class="content card" data-id="<?= $event['id'] ?>">
<div class="card-body">
<!-- 标题 -->
<h3 class="card-title"><?= $event['title'] ?></h3>
<!-- 时间 -->
<p><?= date('Y-m-d H:i', $event['create_time']) ?></p>
<!-- 查看详情按钮 -->
<a href="<?= url('events_log-view', ['id' => $event['id']]) ?>" class="btn btn-link">查看详情</a>
</div>
<?php /* 只有管理员可用的操作 */ if($uid && intval($gid) === 1): ?>
<div class="card-footer">
<a href="<?= url('events_log-edit', ['id' => $event['id']]) ?>" class="btn btn-xs btn-warning">编辑</a>
<button type="button" class="btn btn-xs btn-danger" onclick="processDelete(<?= $event['id'] ?>)">删除</button>
</div>
<?php endif; ?>
</article>
<?php endforeach; ?>
<?php endif; ?>
</section>
<!-- 分页导航 -->
<div class="text-center">
<?= $pagination ?>
</div>
</div>
<!-- 引入提前写好的时间线样式 -->
<link rel="stylesheet" href="./plugin/my_events_log/view/css/timeline.css">
<?php include _include(APP_PATH.'view/htm/footer.inc.htm');?>
<script>
// 将表单固定成jQuery实例
let add_event_form = $('#add_event_form');
// 提交事件
add_event_form.on('submit', function () {
event.preventDefault();
add_event_form.reset();
var postdata = add_event_form.serialize();
$.xpost(add_event_form.attr('action'), postdata, function (code, message) {
if (code == 0) {
window.location.reload();
} else {
$.alert(message);
}
});
});
// 删除事件
function processDelete(id) {
if (confirm('确定删除?')) {
$.xpost(xn.url("events_log-delete"), "id=" + id, function (code, message) {
if (code == 0) {
document.querySelector('[data-id="' + id + '"]').remove();
} else {
$.alert(message);
}
});
}
}
</script>
现在,点击“添加事件”按钮后,会显示一个弹窗(模态框),填写好内容后,点击“提交”按钮,如果一切正常,页面将会刷新,并显示刚刚提交的内容。
恭喜你!
你已经完成了为Xiuno BBS开发一款功能更复杂一些的插件!你应该出门为你自己买一杯奶茶犒劳自己了。
我知道本文可能确实不那么容易理解,但还是感谢你能坚持看完。
- xiuno bbs 后台Getshell漏洞 2021-11-6
- 二开xiuno-三叶草社区: http://lemonbbs.rf.gd/ 2022-2-27
- 怎样提高xiuno图片网站在各个搜索引擎的权重和收录量? 2021-2-22
- 求这两个插件 8月前