AI摘要
一个简洁优雅的 Typecho 微语(说说/动态)插件,记录生活中的点滴想法。
前言
最近快年底了,公司不是很忙,终于有时间折腾自己的博客了。前几天收到 @流情 的留言,于是去他的博客回访了一下,发现了很多有趣的创意。其中最吸引我的就是微语这个模块,类似朋友圈或者说说,可以随时记录一些简短的想法和心情。
看着流情博客上那些简洁优雅的微语卡片,我心想:这个功能太棒了!虽然市面上可能有类似的插件,但作为一个爱折腾的开发者,我决定自己动手实现一个,顺便加入一些自己的想法。
经过几天的开发和调试(以及无数次的样式调整😂),终于完成了这个插件。从最初的基础功能,到后来加入的图片上传、表情选择、点赞功能,每一个细节都经过了反复打磨。现在已经开源到 GitHub 了,欢迎大家使用和 Star!
项目地址: GitHub - Whisper
在线演示: 我的微语页面
功能规划
在开始开发之前,我先梳理了一下想要实现的功能:
核心功能
- ✅ 发表微语(支持多行文本)
- ✅ 图片上传(支持多图)
- ✅ 表情选择
- ✅ 点赞功能
- ✅ 删除管理
界面设计
- ✅ 清新简洁的卡片式设计
- ✅ 暗黑模式适配
- ✅ 响应式布局
- ✅ 流畅的动画效果
用户体验
- ✅ 图片灯箱预览
- ✅ 智能时间显示
- ✅ 设备信息识别
- ✅ 分页浏览
开发过程
第一步:搭建基础框架
作为一个 Typecho 插件,首先要遵循它的开发规范。Typecho 插件需要实现 Typecho_Plugin_Interface 接口,这是最基本的要求:
class Whisper_Plugin implements Typecho_Plugin_Interface
{
public static function activate() {
// 插件激活时执行:创建数据表、注册路由等
}
public static function deactivate() {
// 插件禁用时执行:清理路由等
}
public static function config(Typecho_Widget_Helper_Form $form) {
// 插件配置界面:添加各种配置项
}
}这个接口定义了插件的生命周期,让我们可以在激活、禁用时执行特定的操作。
第二步:设计数据库
微语需要存储的信息包括:内容、图片、点赞数、发表时间等。经过思考,我设计了这样的表结构:
CREATE TABLE `typecho_whispers` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`content` text NOT NULL, -- 微语内容
`images` text, -- 图片地址(逗号分隔)
`os_info` varchar(100), -- 操作系统信息
`likes` int(11) DEFAULT 0, -- 点赞数
`created_at` int(11) NOT NULL, -- 发表时间戳
`user_id` int(11) NOT NULL, -- 用户ID
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;在插件激活时自动创建表,并且做了兼容处理:如果表已存在,会检查是否缺少 likes 字段(这是后来加的功能),如果缺少就自动添加。这样即使用户升级插件,也不会出现字段缺失的问题。
public static function activate()
{
$db = Typecho_Db::get();
$prefix = $db->getPrefix();
// 创建表
$sql = "CREATE TABLE IF NOT EXISTS `{$prefix}whispers` (...)";
$db->query($sql);
// 检查并添加 likes 字段(兼容旧版本)
$hasLikes = false;
$columns = $db->fetchAll($db->query("SHOW COLUMNS FROM `{$prefix}whispers`"));
foreach ($columns as $column) {
if ($column['Field'] == 'likes') {
$hasLikes = true;
break;
}
}
if (!$hasLikes) {
$db->query("ALTER TABLE `{$prefix}whispers` ADD `likes` int(11) DEFAULT 0");
}
// 注册 Action 路由
Helper::addAction('whisper', 'Whisper_Action');
}这里有个小插曲:最初我忘记添加 likes 字段了,后来加点赞功能时才发现。于是我加了这个检查逻辑,让用户重新启用插件就能自动添加字段,避免手动修改数据库的麻烦。
第三步:实现 API 接口
创建 Action.php 处理各种操作:
class Whisper_Action extends Typecho_Widget implements Widget_Interface_Do
{
public function action()
{
$this->on($this->request->is('do=publish'))->publish();
$this->on($this->request->is('do=delete'))->delete();
$this->on($this->request->is('do=list'))->getList();
$this->on($this->request->is('do=upload'))->uploadImage();
$this->on($this->request->is('do=like'))->likeWhisper();
}
}发表微语
public function publish()
{
$user = Typecho_Widget::widget('Widget_User');
if (!$user->hasLogin()) {
$this->response->throwJson(['success' => false, 'message' => '请先登录']);
}
$content = $this->request->get('content');
$images = $this->request->get('images', '');
$data = [
'content' => $content,
'images' => $images,
'os_info' => $this->getOSInfo(),
'created_at' => time(),
'user_id' => $user->uid
];
$this->db->query($this->db->insert('table.whispers')->rows($data));
}图片上传
图片上传是个重点,需要考虑安全性和用户体验。最初我想直接用 Typecho 自带的上传接口,结果发现会返回 302 重定向,因为需要 CSRF 令牌验证。于是我决定自己实现一个上传接口:
public function uploadImage()
{
header('Content-Type: application/json');
$user = Typecho_Widget::widget('Widget_User');
if (!$user->hasLogin()) {
echo json_encode(['success' => false, 'message' => '请先登录']);
exit;
}
$file = $_FILES['file'];
// 1. 检查文件大小(限制5MB)
if ($file['size'] > 5 * 1024 * 1024) {
echo json_encode(['success' => false, 'message' => '文件大小不能超过5MB']);
exit;
}
// 2. 检查文件类型(使用 MIME 检测,更安全)
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) {
echo json_encode(['success' => false, 'message' => '只支持 JPG、PNG、GIF、WebP 格式']);
exit;
}
// 3. 生成唯一文件名,避免冲突
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = date('YmdHis') . '_' . uniqid() . '.' . $ext;
// 4. 保存文件
$uploadDir = __TYPECHO_ROOT_DIR__ . '/usr/uploads/whisper/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if (move_uploaded_file($file['tmp_name'], $uploadDir . $filename)) {
$url = Typecho_Common::url('/usr/uploads/whisper/' . $filename, $this->options->siteUrl);
echo json_encode([
['url' => $url]
]);
} else {
echo json_encode(['success' => false, 'message' => '文件保存失败']);
}
exit;
}这里我特别注意了几点:
- 使用
finfo_file检测真实的 MIME 类型,而不是仅仅检查文件扩展名,防止恶意文件上传 - 文件名使用时间戳 + uniqid 生成,确保唯一性
- 自动创建上传目录,避免首次使用时报错
- 返回完整的 URL 地址,方便前端直接使用
点赞功能
点赞功能需要防止重复点赞,我使用 Cookie 来记录用户的点赞状态。虽然 Cookie 可以被清除,但对于一个博客来说,这个方案已经足够了:
public function likeWhisper()
{
$id = $this->request->get('id');
$likedKey = 'whisper_liked_' . $id;
// 1. 检查是否已点赞
if (isset($_COOKIE[$likedKey])) {
echo json_encode(['success' => false, 'message' => '您已经点赞过了']);
exit;
}
// 2. 获取当前点赞数
$whisper = $this->db->fetchRow(
$this->db->select()->from('table.whispers')->where('id = ?', $id)
);
if (!$whisper) {
echo json_encode(['success' => false, 'message' => '微语不存在']);
exit;
}
// 3. 增加点赞数
$newLikes = intval($whisper['likes']) + 1;
$this->db->query(
$this->db->update('table.whispers')
->rows(['likes' => $newLikes])
->where('id = ?', $id)
);
// 4. 设置 Cookie(30天有效)
setcookie($likedKey, '1', time() + 2592000, '/');
echo json_encode([
'success' => true,
'message' => '点赞成功',
'likes' => $newLikes
]);
exit;
}前端配合心跳动画,点赞时爱心会跳动一下,视觉效果很棒:
@keyframes heartBeat {
0%, 100% { transform: scale(1); }
25% { transform: scale(1.3); }
50% { transform: scale(1.1); }
75% { transform: scale(1.2); }
}这个动画调了好几次才找到最舒服的节奏,太快显得急促,太慢又不够灵动。
第四步:前端页面开发
前端是最花时间的部分,因为我的博客使用的是 Jasmine 主题,需要完美适配它的布局结构。
适配 Jasmine 主题
Jasmine 主题使用了三栏布局(左侧边栏、主内容区、右侧边栏),我需要让微语页面保持一致的风格:
<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
$this->need('header.php');
?>
<div class="jasmine-container grid grid-cols-12">
<!-- 左侧边栏 -->
<?php $this->need('component/sidebar-left.php'); ?>
<!-- 主内容区 -->
<div class="flex col-span-12 lg:col-span-8 flex-col lg:border-x-2 border-stone-100 dark:border-neutral-600 lg:pt-0 lg:px-6 pb-10 px-3">
<?php $this->need('component/menu.php'); ?>
<!-- 微语内容 -->
<div class="whisper-container">
<!-- 发表框、微语列表等 -->
</div>
</div>
<!-- 右侧边栏 -->
<?php $this->need('component/sidebar.php'); ?>
</div>
<?php $this->need('footer.php'); ?>这样微语页面就和博客的其他页面保持了一致的布局,看起来就像是主题自带的功能一样。
发表框设计
发表框是用户最常接触的部分,我希望它简洁但功能完整:
<div class="whisper-publish">
<textarea id="whisper-content" class="whisper-textarea"
placeholder="发表您的新鲜事儿..."></textarea>
<div class="whisper-actions">
<div style="display: flex; gap: 12px;">
<!-- 表情按钮 -->
<button class="whisper-emoji-btn" onclick="toggleEmojiPicker(event)">
😊 表情
</button>
<!-- 图片上传 -->
<label class="whisper-image-btn">
📷 图片
<input type="file" id="image-upload" accept="image/*"
multiple style="display: none;"
onchange="handleImageUpload(event)">
</label>
</div>
<button class="whisper-submit-btn" onclick="publishWhisper()">
立即发表
</button>
</div>
<!-- 图片预览区 -->
<div id="image-preview" class="image-preview"></div>
</div>这里有个小技巧:图片上传按钮使用 <label> 包裹隐藏的 <input>,这样可以自定义按钮样式,同时保持原生的文件选择功能。
第五步:样式优化
样式是最考验耐心的部分,我反复调整了很多次才达到满意的效果。
1. 卡片设计
参考了流情博客的样式,我设计了清新的卡片效果:
.whisper-item {
background: white;
border: 1px solid #e1e4e8;
border-radius: 16px;
padding: 20px;
transition: all 0.3s;
animation: fadeIn 0.5s ease-in; /* 淡入动画 */
}
.whisper-item:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
transform: translateY(-2px); /* 悬停时轻微上浮 */
}
/* 淡入动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}每条微语加载时都会有一个淡入动画,让页面显得更加生动。
2. 图片布局
图片布局是个难点,我希望不同数量的图片能有不同的展示效果:
/* 基础网格布局 */
.whisper-images {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
}
.whisper-image {
width: 100%;
height: 200px; /* 固定高度,统一整齐 */
object-fit: cover; /* 自动裁剪,保持比例 */
border-radius: 12px;
cursor: pointer;
transition: transform 0.3s;
}
.whisper-image:hover {
transform: scale(1.05); /* 悬停时放大 */
}
/* 单张图片:更大的展示 */
.whisper-images:has(:only-child) {
grid-template-columns: 1fr;
max-width: 400px;
}
.whisper-images:has(:only-child) .whisper-image {
height: 300px;
}
/* 两张图片:并排显示 */
.whisper-images:has(:nth-child(2):last-child) {
grid-template-columns: repeat(2, 1fr);
}
/* 三张图片:第一张占满,后两张并排 */
.whisper-images:has(:nth-child(3):last-child) {
grid-template-columns: repeat(2, 1fr);
}
.whisper-images:has(:nth-child(3):last-child) .whisper-image:first-child {
grid-column: 1 / -1;
height: 250px;
}这里使用了 CSS 的 :has() 伪类,可以根据子元素数量应用不同的样式。这是一个相对较新的特性,但现代浏览器都已支持。
3. 点赞按钮
点赞按钮的样式经过了几次迭代。最初我给它加了背景色,但看起来太重了。后来改成了无背景的简洁样式:
.whisper-like-btn {
background: none; /* 无背景,更简洁 */
border: none;
color: #6b7280;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px; /* 图标和数字之间的间距 */
padding: 0;
transition: all 0.3s;
}
.whisper-like-btn:hover {
color: #ef4444; /* 悬停时变红 */
}
.whisper-like-btn.liked {
color: #ef4444; /* 已点赞状态 */
}
.like-icon {
font-size: 18px;
line-height: 1;
}
.like-count {
font-size: 14px;
font-weight: 500;
line-height: 1;
}布局上也调整了好几次。最初点赞按钮和设备信息在同一行,显得很拥挤。后来改成了垂直布局,设备信息在上,点赞按钮在下,看起来舒服多了。
4. 暗黑模式
Jasmine 主题支持暗黑模式,我也为微语页面适配了暗色样式:
.dark .whisper-item {
background: #1f2937;
border-color: #374151;
}
.dark .whisper-content {
color: #d1d5db;
}
.dark .whisper-textarea {
background: #111827;
border-color: #374151;
color: #e5e7eb;
}所有元素都有对应的暗色样式,切换主题时不会有任何违和感。
第六步:交互功能
1. 表情选择器
表情选择器最初是10列布局,但在移动端显得太挤了,后来改成了响应式:桌面端10列,移动端8列。
function toggleEmojiPicker(event) {
event.stopPropagation(); // 阻止事件冒泡
const picker = document.getElementById('emoji-picker');
// 第一次打开时初始化表情列表
if (picker.innerHTML === '') {
const emojis = [
'😊', '😂', '🤣', '😍', '😘',
'😭', '😢', '😤', '😡', '🤔',
'😎', '🥰', '😜', '🤗', '🤩',
'❤️', '💕', '💖', '💗', '💙',
'👍', '👎', '👏', '🙏', '💪',
'🎉', '🎊', '🎈', '🎁', '🔥',
'✨', '⭐', '🌟', '💫', '🌈',
'☀️', '🌙', '⚡', '💧', '🌸'
];
emojis.forEach(emoji => {
const item = document.createElement('span');
item.className = 'emoji-item';
item.textContent = emoji;
item.onclick = () => insertEmoji(emoji);
picker.appendChild(item);
});
}
picker.classList.toggle('show');
}
// 点击其他地方关闭表情选择器
document.addEventListener('click', function(event) {
const picker = document.getElementById('emoji-picker');
if (picker && !event.target.closest('.whisper-emoji-btn')) {
picker.classList.remove('show');
}
});这里有个细节:表情列表只在第一次打开时创建,之后只是显示/隐藏,避免重复创建 DOM 元素。
2. 图片灯箱
图片灯箱支持多种操作:点击放大、缩放控制、ESC 关闭等。
// 查看图片
function viewImage(url) {
const lightbox = document.getElementById('image-lightbox');
const image = document.getElementById('lightbox-image');
image.src = url;
image.style.transform = 'scale(1)';
lightbox.classList.add('show');
document.body.style.overflow = 'hidden'; // 禁止背景滚动
}
// 放大
function zoomIn(event) {
event.stopPropagation();
const image = document.getElementById('lightbox-image');
const currentScale = parseFloat(
image.style.transform.replace('scale(', '').replace(')', '') || '1'
);
const newScale = Math.min(currentScale + 0.2, 3); // 最大3倍
image.style.transform = `scale(${newScale})`;
}
// 缩小
function zoomOut(event) {
event.stopPropagation();
const image = document.getElementById('lightbox-image');
const currentScale = parseFloat(
image.style.transform.replace('scale(', '').replace(')', '') || '1'
);
const newScale = Math.max(currentScale - 0.2, 0.5); // 最小0.5倍
image.style.transform = `scale(${newScale})`;
}
// ESC键关闭
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
const lightbox = document.getElementById('image-lightbox');
if (lightbox.classList.contains('show')) {
lightbox.classList.remove('show');
document.body.style.overflow = '';
}
}
});灯箱打开时会禁止背景滚动,关闭时恢复,这样用户体验更好。
3. 智能时间显示
时间显示格式经过了精心设计,让用户一眼就能看懂:
private function timeAgo($timestamp)
{
$diff = time() - $timestamp;
$today = strtotime(date('Y-m-d'));
$postDate = strtotime(date('Y-m-d', $timestamp));
// 1分钟内:刚刚
if ($diff < 60) return '刚刚';
// 1小时内:X分钟前
if ($diff < 3600) return floor($diff / 60) . '分钟前';
// 今天:今天 HH:mm
if ($postDate == $today) return '今天 ' . date('H:i', $timestamp);
// 昨天:昨天 HH:mm
if ($postDate == $today - 86400) return '昨天 ' . date('H:i', $timestamp);
// 一周内:星期X HH:mm
if ($diff < 604800) {
$days = ['日', '一', '二', '三', '四', '五', '六'];
return '星期' . $days[date('w', $timestamp)] . ' ' . date('H:i', $timestamp);
}
// 今年内:m月d日 HH:mm
if (date('Y', $timestamp) == date('Y')) {
return date('m月d日 H:i', $timestamp);
}
// 更早:Y年m月d日 HH:mm
return date('Y年m月d日 H:i', $timestamp);
}这个函数考虑了各种时间场景,让时间显示更加人性化。
踩过的坑
开发过程中遇到了不少问题,这里分享一下解决方案,希望能帮到遇到类似问题的朋友。
坑1:点赞功能 500 错误
现象: 点赞功能开发完成后,点击按钮时浏览器控制台报 500 错误,返回的是 HTML 错误页面而不是 JSON。
排查过程:
- 检查 Action.php 的代码逻辑,没有问题
- 检查路由注册,也正常
- 最后发现是数据库查询报错
原因: 原来是我后来才加的点赞功能,数据库表中没有 likes 字段!因为表是在插件激活时创建的,而我是在开发过程中才想到加点赞功能的。
解决方案: 在插件激活时增加字段检查逻辑,如果缺少 likes 字段就自动添加:
// 检查是否需要添加 likes 字段(兼容旧版本)
$hasLikes = false;
$columns = $db->fetchAll($db->query("SHOW COLUMNS FROM `{$prefix}whispers`"));
foreach ($columns as $column) {
if ($column['Field'] == 'likes') {
$hasLikes = true;
break;
}
}
if (!$hasLikes) {
$db->query("ALTER TABLE `{$prefix}whispers` ADD `likes` int(11) DEFAULT 0");
}这样用户只需要重新启用一下插件,就能自动添加字段,不需要手动修改数据库。
坑2:图片上传返回 302
现象: 最初我想偷懒,直接用 Typecho 自带的上传接口 /action/upload,结果一上传就返回 302 重定向。
原因: Typecho 的上传接口有 CSRF 令牌验证,需要在 URL 中带上 _ 参数(一个随机生成的令牌)。虽然可以通过 JavaScript 获取这个令牌,但感觉太麻烦了。
解决方案: 干脆自己实现一个上传接口,反正逻辑也不复杂。在 Action.php 中添加 uploadImage 方法,自己处理文件上传、类型检查、大小限制等。这样还能更好地控制上传逻辑,比如自定义保存路径、文件命名规则等。
坑3:点赞按钮布局问题
现象: 最初我把点赞按钮和设备信息放在同一行,用 justify-content: space-between 让它们分居两端。结果发现在移动端显得很拥挤,而且视觉上不够清晰。
迭代过程:
- 第一版:点赞按钮和设备信息在同一行 → 太挤
- 第二版:改为垂直布局,但间距太大 → 不够紧凑
- 第三版:调整间距为 8px,完美!
最终方案:
.whisper-footer {
display: flex;
flex-direction: column; /* 垂直布局 */
gap: 8px; /* 适中的间距 */
}有时候设计就是这样,需要反复调整才能找到最舒服的状态。
坑4:表情选择器超出屏幕
现象: 表情选择器最初是固定宽度,在移动端会超出屏幕,而且表情太小不好点击。
解决方案: 使用响应式设计,桌面端 10 列,移动端 8 列,并且调整表情大小:
.emoji-picker {
grid-template-columns: repeat(10, 1fr);
min-width: 400px;
}
@media (max-width: 768px) {
.emoji-picker {
grid-template-columns: repeat(8, 1fr);
min-width: 320px;
}
.emoji-item {
font-size: 18px;
padding: 4px;
}
}坑5:图片固定高度的选择
现象: 最初图片是自适应高度的,结果不同尺寸的图片混在一起,整个页面看起来很乱。
解决方案: 统一设置固定高度 200px,使用 object-fit: cover 自动裁剪。这样所有图片都整整齐齐,视觉效果好多了。虽然会裁剪掉部分内容,但点击可以查看完整图片,所以不影响使用。
.whisper-image {
width: 100%;
height: 200px;
object-fit: cover; /* 关键属性 */
}功能亮点
经过这么多天的打磨,插件有了一些我比较满意的功能点,这里重点介绍一下。
1. 智能图片布局
这是我花了不少心思的地方。不同数量的图片,会自动应用不同的布局策略:
- 1张图片:单独显示,高度 300px,最大宽度 400px,突出展示
- 2张图片:左右并排,每张宽度 50%
- 3张图片:第一张占满一行(高度 250px),后两张并排显示
- 4-9张图片:网格布局,自动换行,每张高度统一 200px
所有图片都使用 object-fit: cover 自动裁剪,保持整齐美观。点击任意图片可以查看完整内容,支持缩放操作。
这种布局方式参考了微信朋友圈和微博的设计,既美观又实用。
2. 防重复点赞机制
点赞功能使用 Cookie 记录用户的点赞状态,有效期 30 天。虽然 Cookie 可以被清除,但对于个人博客来说,这个方案已经足够了。
点赞后的体验也做了优化:
- 按钮立即变红并禁用,防止重复点击
- 爱心图标播放心跳动画(0.5秒)
- 点赞数实时更新
- 提示"您已经点赞过了"
如果未来需要更严格的限制,可以考虑加入 IP 限制或者用户登录验证。
3. 图片灯箱预览
点击图片会弹出全屏灯箱,支持丰富的操作:
- 放大/缩小:点击 +/- 按钮,每次缩放 0.2 倍
- 快速切换:点击图片在 1 倍和 1.5 倍之间切换
- 重置:点击 ⟲ 按钮恢复原始大小
- 关闭:ESC 键、点击背景、点击 × 按钮都可以关闭
- 防止滚动:灯箱打开时禁止背景滚动
缩放范围限制在 0.5 倍到 3 倍之间,既能看清细节,又不会过度放大导致模糊。
4. 完美的暗黑模式
作为一个强迫症患者,我不能容忍暗黑模式下有任何违和感。所以每个元素都有对应的暗色样式:
- 卡片背景:
#1f2937 - 边框颜色:
#374151 - 文字颜色:
#e5e7eb/#d1d5db - 输入框背景:
#111827
切换主题时,所有元素都会平滑过渡,没有任何闪烁或突兀感。
5. 响应式设计
移动端体验也做了专门优化:
- 图片网格:从 3-4 列改为 2 列,避免图片太小
- 表情选择器:从 10 列改为 8 列,表情大小也相应调整
- 按钮间距:适当增大,方便手指点击
- 字体大小:保持可读性
在 iPhone 和 Android 上测试过,体验都很流畅。
6. 智能时间显示
时间显示采用了人性化的格式,让用户一眼就能理解:
- 1 分钟内:刚刚
- 1 小时内:5分钟前
- 今天:今天 14:30
- 昨天:昨天 09:15
- 一周内:星期三 18:20
- 今年内:10月15日 12:00
- 更早:2023年8月20日 16:45
这种渐进式的时间显示,既精确又友好。
7. 设备信息识别
每条微语会自动记录发表时的操作系统信息,显示为"来自 Windows 10"、"来自 macOS"等。这个小细节让微语更有生活气息。
识别逻辑基于 User-Agent,支持:
- Windows(包括版本号)
- macOS
- Linux
- Android
- iOS
虽然不是核心功能,但这种小细节能让产品更有温度。
技术栈与架构
在开始开发之前,先来看看整个项目的技术选型和架构设计。
技术选型
- 后端框架: Typecho 1.0+(PHP 5.4+)
- 数据库: MySQL 5.5+
- 前端技术: 原生 JavaScript(无依赖)
- 样式方案: CSS3(Flexbox + Grid)
- 主题适配: Jasmine 主题
选择原生 JavaScript 而不是 jQuery 或其他框架,主要是考虑到:
- 现代浏览器对原生 API 的支持已经很好
- 减少依赖,降低插件体积
- 提高加载速度
架构设计
插件采用了经典的 MVC 架构:
Whisper/
├── Plugin.php # 插件主文件(Controller)
├── Action.php # API 接口处理(Controller)
└── whisper.php # 页面模板(View)
├── HTML 结构
├── CSS 样式
└── JavaScript 交互数据流向:
用户操作 → JavaScript → Action API → 数据库 → 返回 JSON → 更新界面这种架构清晰简单,易于维护和扩展。
文件结构
Whisper/
├── Plugin.php # 插件主文件
├── Action.php # API 接口
├── README.md # 项目说明
├── LICENSE # MIT 协议
├── CHANGELOG.md # 更新日志
├── CONTRIBUTING.md # 贡献指南
└── .gitignore # Git 忽略文件
主题目录/
└── whisper.php # 页面模板
上传目录/
└── usr/uploads/whisper/ # 图片存储(自动创建)开源说明
这个插件已经在 GitHub 开源,采用 MIT 协议。
项目地址: https://github.com/lovisnd/Whisper
功能清单
- [x] 发表微语
- [x] 图片上传(多图)
- [x] 表情选择器
- [x] 点赞功能
- [x] 图片灯箱预览
- [x] 分页浏览
- [x] 暗黑模式
- [x] 响应式设计
- [ ] 评论功能(计划中)
- [ ] 标签分类(计划中)
- [ ] 视频上传(计划中)
如何使用
下载插件
cd /usr/plugins/ git clone https://github.com/lovisnd/Whisper.git启用插件
- 后台 → 插件 → 启用"微语"
创建页面
- 将
whisper.php复制到主题目录 - 创建独立页面,选择"微语"模板
- 将
开始使用
- 登录后访问微语页面
- 发表你的第一条微语!
总结与展望
开发感悟
这次开发微语插件的过程很有趣,从最初看到流情博客的创意,到自己动手实现,再到不断优化细节,整个过程充满了成就感。
特别是在调整样式的时候,虽然有时候为了几个像素的间距反复调整,但看到最终效果时,会觉得一切都值得。这就是前端开发的魅力吧——每一个细节都能影响用户体验。
通过这个项目,我也更深入地理解了 Typecho 的插件机制,包括:
- 插件生命周期管理
- 数据库操作和字段动态添加
- Action 路由注册和处理
- 主题模板适配
- 配置项的设计
这些经验对以后开发其他插件也很有帮助。
未来计划
目前插件已经具备了基本的功能,但还有一些想法可以继续完善:
短期计划:
- [ ] 评论功能:允许访客对微语进行评论互动
- [ ] 标签分类:给微语添加标签,方便分类浏览
- [ ] 搜索功能:支持搜索微语内容
长期计划:
- [ ] 视频上传:支持上传短视频,像真正的朋友圈一样
- [ ] Markdown 支持:支持 Markdown 语法,方便写代码片段
- [ ] RSS 订阅:生成微语的 RSS 源,方便订阅
- [ ] 导出功能:支持导出为 JSON 或 Markdown 格式
- [ ] 多用户权限:细化权限控制,支持多人协作
这些功能会根据实际使用情况和用户反馈逐步添加。如果你有好的建议,欢迎在 GitHub 提 Issue!
致谢
最后要特别感谢:
- @流情:感谢你的创意启发,让我有了开发这个插件的想法
- Typecho 社区:提供了优秀的博客系统和完善的文档
- Jasmine 主题作者:清新的主题设计给了我很多灵感
- 所有测试用户:感谢你们的反馈和建议
开源地址
如果你也在使用 Typecho,欢迎试用这个插件。如果觉得不错,给个 Star 支持一下吧!
项目地址: https://github.com/lovisnd/Whisper
在线演示: https://blog.zhangmingrui.top/index.php/whisper.html
我的博客: https://blog.zhangmingrui.top
有任何问题或建议,欢迎:
- 在 GitHub 提 Issue
- 在博客留言
- 通过邮件联系我
让我们一起把这个插件做得更好!
我用的是1.3的typecho,好像不行?提示500错误?
好的 我的版本是1.21是正常。可能是1.3最新版有些兼容问题,我来升级测试下。
大神看看那里出问题了。1.3版本提示无法启用插件
厉害,后续要用用,然后反馈一下
厉害,我也是用的别人的插件微调后拿来用的,你这是从零纯手动,尽善尽美了
嘿嘿 我也是各种学习+问AI 捣鼓出来的