EnovelCms 2.x.x 主题模板开发教程
本文基于你提供的 EnovelCms 源码整理。目标:让你能独立开发一个 EnovelCms 主题,掌握模板位置、语言包、全局变量/函数、每页变量、JS 交互、自定义榜单/广告、SEO、PHP 模板写法,并完成一个可运行的简单主题。具体开发行为可参考默认主题!
函数、变量列表:https://www.enovelcms.cn/pages/doc_detail.php?id=9
一、主题目录与文件存放位置
EnovelCms 前台主题统一放在:
templates/
当前使用主题由数据库 settings 表的 active_theme 决定,代码里通过:
getActiveTheme()
获取。默认主题是 default。
一个完整主题推荐结构:
templates/ └── yourtheme/ ├── template.xml # 主题信息,必须 ├── screenshot.png # 后台预览图,可选 ├── css/ │ └── style.css ├── js/ │ └── main.js ├── lang/ │ ├── zh-cn.php │ └── en-us.php ├── index/ │ ├── header.php │ ├── footer.php │ ├── home.php │ ├── library.php │ ├── rank.php │ ├── author.php │ ├── novel_detail.php │ ├── read.php │ └── search.php ├── user/ │ ├── sidebar.php │ ├── index.php │ ├── profile.php │ ├── bookshelf.php │ ├── history.php │ ├── gold.php │ ├── sign.php │ ├── vip.php │ ├── login.php │ ├── register.php │ ├── forgot.php │ └── recharge.php ├── author/ │ ├── apply_author.php │ ├── author_works.php │ ├── novel_add.php │ ├── novel_edit.php │ ├── chapter_list.php │ ├── chapter_add.php │ ├── chapter_edit.php │ ├── volume_list.php │ ├── volume_add.php │ ├── volume_edit.php │ ├── income.php │ └── withdraw.php ├── 404.php └── 500.php
主题安装方式:
手动把主题文件夹放到 /templates/。
或后台“主题管理”上传 ZIP。
ZIP 内第一层应为主题文件夹,文件夹内必须有 template.xml。
后台启用主题后,settings.active_theme 会更新。
二、template.xml 主题信息
每个主题必须包含 template.xml:
<?xml version="1.0" encoding="UTF-8"?> <theme> <name>Demo 主题</name> <author>你的名字</author> <website>https://example.com</website> <version>1.0.0</version> <description>EnovelCms 示例主题</description> </theme>
后台主题管理会读取这些字段显示名称、作者、版本、描述和预览图。
三、路由与模板文件对应关系
EnovelCms 的 includes/Router.php 负责把 URL 映射到 handler 和模板文件。模板文件必须按约定命名。
前台主路由
| URL | Handler | 模板文件 |
| /home | home_handler() | index/home.php |
| /library | library_handler() | index/library.php |
| /rank | rank_handler() | index/rank.php |
| /authors | author_handler() | index/author.php |
| /search | search_handler() | index/search.php |
| /novel/{id} | novel_detail_handler() | index/novel_detail.php |
| /read/{novel_id}/{chapter_id} | read_handler() | index/read.php |
| 404 | - | 404.php |
| 500 | - | 500.php |
用户中心路由
| URL | 模板文件 |
| /user | user/index.php |
| /user/profile | user/profile.php |
| /user/bookshelf | user/bookshelf.php |
| /user/history | user/history.php |
| /user/gold | user/gold.php |
| /user/sign | user/sign.php |
| /user/vip | user/vip.php |
| /user/login | user/login.php |
| /user/register | user/register.php |
| /user/forgot | user/forgot.php |
| /user/recharge | user/recharge.php |
| /user/logout | 无模板,直接退出 |
| /user/apply_author | author/apply_author.php |
| /user/author_works | author/author_works.php |
| /user/novel_add | author/novel_add.php |
| /user/novel_edit | author/novel_edit.php |
| /user/chapter_list | author/chapter_list.php |
| /user/chapter_add | author/chapter_add.php |
| /user/chapter_edit | author/chapter_edit.php |
| /user/volume_list | author/volume_list.php |
| /user/volume_add | author/volume_add.php |
| /user/volume_edit | author/volume_edit.php |
| /user/income | author/income.php |
| /user/withdraw | author/withdraw.php |
公共模板:
index/header.php:前台公共头部
index/footer.php:前台公共底部,定义 window.lang,引入 main.js
user/sidebar.php:用户中心/作者中心侧边栏
lang/zh-cn.php:简体中文语言包
lang/en-us.php:英文语言包
css/style.css:主题样式
js/main.js:主题脚本
四、语言包的使用与设计
1. 语言包位置
前台语言包只放主题目录下:
templates/yourtheme/lang/zh-cn.php templates/yourtheme/lang/en-us.php
后台语言包在 includes/lang/,主题开发主要关心主题语言包。
2. 语言包格式
语言文件必须返回数组:
<?php return [ 'home' => '首页', 'library' => '书库', 'rank' => '排行榜', 'login' => '登录', 'register' => '注册', 'sign_button' => '立即签到', 'network_error' => '网络错误,请重试', ];
英文:
<?php return [ 'home' => 'Home', 'library' => 'Library', 'rank' => 'Ranking', 'login' => 'Login', 'register' => 'Register', 'sign_button' => 'Sign Now', 'network_error' => 'Network error, please retry', ];
3. 模板中调用
<?= $lang->get('home') ?>
<?= $lang->get('sign_button') ?>
<?= $lang->get('network_error') ?>支持默认值:
<?= $lang->get('custom_key', '默认文字') ?>当前语言:
<?= $current_lang ?> <?= $lang->current() ?>
4. 加载优先级
includes/Language.php 的加载顺序:
当前活动主题的 lang/{code}.php
默认主题 templates/default/lang/{code}.php
默认主题的 templates/default/lang/zh-cn.php
所以你的主题如果缺少某个语言包,会自动回退到 default 主题。
5. 语言切换
前台语言切换接口:
/api/ajax/switch_lang.php?lang=zh-cn /api/ajax/switch_lang.php?lang=en-us
源码中允许 zh-cn、en-us。如果你要增加更多语言,需要同时改 switch_lang.php 的白名单,或自行扩展。
6. 设计建议
键名用英文小写加下划线,如 sign_button。
不要把中文当键名。
公共词、导航、按钮、提示信息都放语言包。
后台可配置的文字用 getSetting(),前台固定文案用 $lang->get()。
五、全局变量、常量、常用函数
1. 全局常量
| 常量 | 说明 |
| ROOT_PATH | 网站根目录绝对路径,带末尾 / |
| BASE_URL | 网站基础 URL |
| THEME_PATH | 当前主题绝对路径 |
| THEME_URL | 当前主题 URL |
| ENOVELCMS_VERSION | 当前版本 |
模板中引用 CSS/JS:
<link rel="stylesheet" href="<?= THEME_URL ?>css/style.css"> <script src="<?= THEME_URL ?>js/main.js"></script>
引用公共资源:
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
2. 模板中全局可用变量
index.php 会 extract($viewData),所以模板中可直接用:
| 变量 | 说明 |
| $db | 数据库对象 |
| $lang | 语言对象 |
| $current_lang | 当前语言代码 |
| $siteLogo | 网站 Logo |
| $userGold | 当前用户金币,未登录为 0 |
| $seo | SEO 数组:title、keywords、description |
| $pageTitle | 页面标题 |
| $data 中的键 | 每个 handler 返回的独有变量 |
3. 常用全局函数
| 函数 | 说明 |
| h($str) | HTML 转义,输出用户数据必须用 |
| getSetting($key, $db) | 读取系统设置 |
| isLoggedIn() | 是否登录 |
| isAdmin() | 是否管理员 |
| isAuthor($userId = null) | 是否作者 |
| getActiveTheme() | 当前主题 |
| getThemePath() | 主题绝对路径 |
| getThemeUrl() | 主题 URL |
| csrf_token() | 生成 CSRF token |
| csrf_field() | 输出 CSRF hidden input |
| input($key, $default, $method, $htmlspecialchars) | 安全获取输入 |
| safe_nl2br($str) | 安全换行 |
| showAd($position) | 输出广告位 HTML |
| DiyString($blockName) | 获取自定义榜单数据 |
| DiyStrTitle($blockName) | 获取自定义榜单标题 |
| getPointName() | 积分名称,如“金币” |
| formatNumberShort($num) | 数字简写:千/万/亿 |
| applyAdLinkReplacements($content) | 章节内容关键词链接替换 |
| getSeoMeta($page, $variables) | 获取 SEO |
| updateNovelTotalWords($novelId) | 更新小说总字数 |
| getChapterFilePath($novelId) | 生成章节文件路径 |
| smartWordCount($content) | 智能字数统计 |
| batch_latest_chapters($novelIds) | 批量取最新章节 |
| checkAuthorPermission($novelId, $userId) | 作者权限校验 |
| getChapterPrice($chapterId) | 获取章节价格 |
| checkChapterReadPermission($userId, $chapterId) | 阅读权限 |
| purchaseChapter($userId, $chapterId) | 购买章节 |
| isChapterPurchased($userId, $chapterId) | 是否已购买 |
| getRechargeRate() | 充值汇率 |
| get_user_withdraw_info($user_id) | 获取提现信息 |
| save_user_withdraw_info($user_id, $data) | 保存提现信息 |
| apply_withdraw($user_id, $amount, $withdraw_info) | 申请提现 |
| get_author_income_stats($user_id) | 作者收入统计 |
| get_author_income_by_novel($user_id) | 按作品统计收入 |
| get_author_income_by_chapter($user_id, $novel_id) | 按章节统计收入 |
六、每页独有变量列表
以下变量由对应 handler 返回,模板中可直接使用。
前台页面
| 页面模板 | 独有变量 |
| 首页 index/home.php | banners, latestUpdates, topViewsWeek, topUpdate, seoPage, seoVariables, pageTitle |
| 书库 index/library.php | categories, novels, categoryId, status, orderBy, minWords, maxWords, vipFilter, page, totalPages, limit, total, pageTitle, seoPage, seoVariables |
| 排行榜 index/rank.php | rankType, novels, seoPage, seoVariables, pageTitle |
| 作者列表 index/author.php | authors, seoPage, seoVariables, pageTitle |
| 搜索 index/search.php | categories, novels, keyword, author, categoryId, status, orderBy, minWords, maxWords, page, totalPages, limit, total, needCaptcha, captchaError, searchExecuted, pageTitle, seoPage, seoVariables |
| 小说详情 index/novel_detail.php | novel, chaptersByVolume, totalWords, seoPage, seoVariables, pageTitle |
| 阅读页 index/read.php | novel, chapter, content, prev, next, novelId, needPay, needLogin, price, userGold, error, seoPage, seoVariables, pageTitle |
用户中心
| 页面模板 | 独有变量 |
| 个人中心 user/index.php | user, vipActive, bookshelfCount, historyCount, todaySigned, pageTitle |
| 个人资料 user/profile.php | user, vipActive, message, pageTitle |
| 书架 user/bookshelf.php | user, vipActive, books, pageTitle |
| 阅读历史 user/history.php | user, vipActive, history, page, totalPages, total, limit, pageTitle |
| 金币明细 user/gold.php | user, vipActive, logs, pageTitle |
| 签到 user/sign.php | user, vipActive, message, todaySigned, rewardPreview, pageTitle |
| VIP 中心 user/vip.php | user, vipActive, message, pricePerMonth, goldPerMonth, pageTitle |
| 登录 user/login.php | error, pageTitle |
| 注册 user/register.php | error, pageTitle |
| 忘记密码 user/forgot.php | step, message, error, pageTitle |
| 充值 user/recharge.php | user, vipActive, message, error, rate, pointName, rechargeLogs, pageTitle |
作者中心
| 页面模板 | 独有变量 |
| 申请作者 author/apply_author.php | user, vipActive, message, error, applied, pageTitle |
| 我的作品 author/author_works.php | user, vipActive, novels, pageTitle |
| 发布作品 author/novel_add.php | user, vipActive, categories, penName, isAuthor, pageTitle |
| 编辑作品 author/novel_edit.php | user, vipActive, novel, categories, pageTitle |
| 章节列表 author/chapter_list.php | user, vipActive, novel, novelId, chapters, page, totalPages, pageTitle |
| 添加章节 author/chapter_add.php | user, vipActive, novel, novelId, volumes, pageTitle |
| 编辑章节 author/chapter_edit.php | user, vipActive, novel, novelId, chapter, volumes, content, pageTitle |
| 分卷列表 author/volume_list.php | user, vipActive, novel, novelId, volumes, pageTitle |
| 添加分卷 author/volume_add.php | user, vipActive, novel, novelId, pageTitle |
| 编辑分卷 author/volume_edit.php | user, vipActive, novel, novelId, volume, pageTitle |
| 收入统计 author/income.php | user, vipActive, stats, novelList, novelId, timeRange, chapterStats, detailList, page, totalPages, total, pageTitle |
| 提现 author/withdraw.php | user, vipActive, message, error, withdrawInfo, withdraws, page, totalPages, pageTitle |
七、文件引用与公共模板
1. header 和 footer
每个前台模板通常这样写:
<?php defined('ROOT_PATH') or die('URLError'); ?>
<?php require_once THEME_PATH . 'index/header.php'; ?>
<div class="container">
<!-- 页面内容 -->
</div>
<?php require_once THEME_PATH . 'index/footer.php'; ?>header.php 里一般包含:
<!DOCTYPE html> <head> SEO title/keywords/description FontAwesome 主题 style.css 导航 搜索弹窗 <main class="site-main">
footer.php 里一般包含:
</main> 页脚版权 window.lang 主题 main.js 统计代码
2. 用户中心侧边栏
用户中心和作者中心模板通常引入:
<?php include THEME_PATH . 'user/sidebar.php'; ?>
它依赖 $user、$vipActive、$lang。
3. 资源路径
主题资源:
<link rel="stylesheet" href="<?= THEME_URL ?>css/style.css"> <script src="<?= THEME_URL ?>js/main.js"></script> <img src="<?= THEME_URL ?>images/logo.png">
公共资源:
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
封面图:
<img src="/<?= h($novel['cover'] ?: 'assets/images/default_cover.jpg') ?>">
八、PHP 代码在模板中的使用
模板就是 PHP 文件,可以直接写 PHP。
1. 输出与转义
<h1><?= h($novel['title']) ?></h1> <p><?= h($novel['author']) ?></p>
用户输入、数据库内容输出必须 h()。
2. 条件
<?php if ($vipActive): ?> <span class="vip">VIP会员</span> <?php else: ?> <span>普通用户</span> <?php endif; ?>
3. 循环
<?php foreach ($novels as $n): ?> <a href="/novel/<?= $n['id'] ?>"><?= h($n['title']) ?></a> <?php endforeach; ?>
4. 数据库查询
<?php $hot = $db->fetchAll($db->query("SELECT id, title FROM novels ORDER BY views DESC LIMIT 5"));
foreach ($hot as $n) {
echo '<a href="/novel/' . $n['id'] . '">' . h($n['title']) . '</a>';
} ?>模板里可以查询,但不建议写复杂业务,复杂逻辑放 handler 或函数。
5. 语言包
<h1><?= $lang->get('library') ?></h1>
<button><?= $lang->get('sign_button') ?></button>九、JS 数据交互
EnovelCms 前台 AJAX 接口主要在 /api/ajax/ 和 /api/。
常用接口
| 接口 | 方法 | 参数 | 返回 |
| /api/ajax/add_bookshelf.php | POST | novel_id | {code, message, favorites} |
| /api/ajax/remove_bookshelf.php | POST | novel_id | {code, message, favorites} |
| /api/ajax/sign.php | POST/GET | 无 | {code, msg} |
| /api/ajax/novel.php | POST | action=... | {code, msg, data} |
| /api/ajax/get_chapter_content.php | GET | id | {code, content} 仅管理员 |
| /api/ajax/switch_lang.php | GET | lang | 跳转 |
| /api/apply_author.php | POST | FormData | {code, msg} |
| /api/captcha.php | GET | 无 | PNG 验证码 |
| /api/crawler/publish.php | POST | api_key, action | {code, message, data} |
| /api/pay_notify.php | POST/GET | 支付回调 success/fail | |
| /api/pay_return.php | GET | 支付同步HTML |
返回格式
大部分 AJAX 返回:
{
"code": 1,
"msg": "操作成功",
"data": {}
}或:
{
"code": 0,
"message": "请先登录"
}前端示例:签到
<button id="signBtn">立即签到</button>
<script>
document.getElementById('signBtn').addEventListener('click', function () {
fetch('/api/ajax/sign.php', {
method: 'POST'
})
.then(res => res.json())
.then(data => {
alert(data.msg || data.message);
})
.catch(() => {
alert(window.lang.network_error || '网络错误');
});
});
</script>前端示例:加入书架
fetch('/api/ajax/add_bookshelf.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'novel_id=' + novelId
})
.then(res => res.json())
.then(data => {
alert(data.message);
if (data.code === 1) {
// 更新收藏数
document.getElementById('favoritesDisplay').innerText = data.favorites;
}
});window.lang
footer.php 会输出:
<script>
window.lang = {
already_in_bookshelf: '<?= $lang->get('already_in_bookshelf') ?>',
confirm_delete: '<?= $lang->get('confirm_delete') ?>',
network_error: '<?= $lang->get('network_error') ?>',
// ...
};
</script>main.js 里通过 window.lang.xxx 使用。
十、自定义绑定:DIY 榜单、广告、SEO
1. DIY 榜单
后台“自定义榜单”创建榜单:
标识符:hot_novels
显示标题:热门推荐
排序:点击最多
数量:6
模板中调用:
<?php $hot = DiyString('hot_novels'); ?>
<h2><?= DiyStrTitle('hot_novels') ?: '热门推荐' ?></h2>
<?php if (!empty($hot)): ?>
<div class="grid">
<?php foreach ($hot as $n): ?>
<a href="/novel/<?= $n['id'] ?>">
<img src="/<?= h($n['cover'] ?: 'assets/images/default_cover.jpg') ?>">
<h3><?= h($n['title']) ?></h3>
<p><?= h($n['author']) ?></p>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>DiyString() 返回字段包括:
id, title, author, category_id, cover, description, status, views, total_words, favorites, created_at, updated_at, is_vip, category_name, latest_chapter_id, latest_chapter_title, latest_chapter_is_vip, total_chapters, vip_sales_amount, vip_sales_count。
2. 广告位
后台广告管理设置位置,如:
home_banner
home_sidebar_top
home_sidebar_bottom
novel_detail_top
novel_detail_bottom
read_top
read_bottom
library_top
rank_top
模板输出:
<?= showAd('home_banner') ?>
<?= showAd('novel_detail_top') ?>VIP 用户会自动不显示广告。
3. SEO
Router 会自动调用 getSeoMeta($seoPage, $seoVariables),模板中直接用:
<title><?= h($seo['title']) ?></title> <meta name="keywords" content="<?= h($seo['keywords']) ?>"> <meta name="description" content="<?= h($seo['description']) ?>">
后台 SEO 设置里支持变量,如 {site_name}、{novel_title}、{author} 等。
十一、完整简单案例:开发一个 Demo 主题
目标:创建一个 templates/demo/ 主题,首页显示欢迎语、热门推荐、最新小说、签到按钮,并支持中英文。
1. 创建目录
templates/demo/ ├── template.xml ├── css/style.css ├── js/main.js ├── lang/zh-cn.php ├── lang/en-us.php └── index/ ├── header.php ├── footer.php └── home.php
2. template.xml
<?xml version="1.0" encoding="UTF-8"?> <theme> <name>Demo 主题</name> <author>你的名字</author> <website>https://example.com</website> <version>1.0.0</version> <description>EnovelCms 示例主题</description> </theme>
3. 语言包
lang/zh-cn.php:
<?php return [ 'demo_welcome' => '欢迎来到 Demo 主题', 'hot_novels' => '热门推荐', 'latest_novels' => '最新小说', 'sign_button' => '立即签到', 'network_error' => '网络错误,请重试', 'home' => '首页', 'library' => '书库', 'rank' => '排行榜', 'login' => '登录', 'register' => '注册', ];
lang/en-us.php:
<?php return [ 'demo_welcome' => 'Welcome to Demo Theme', 'hot_novels' => 'Hot Novels', 'latest_novels' => 'Latest Novels', 'sign_button' => 'Sign Now', 'network_error' => 'Network error, please retry', 'home' => 'Home', 'library' => 'Library', 'rank' => 'Ranking', 'login' => 'Login', 'register' => 'Register', ];
4. index/header.php
<?php defined('ROOT_PATH') or die('URLError'); ?>
<!DOCTYPE html>
<html lang="<?= $current_lang == 'zh-cn' ? 'zh-CN' : 'en' ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= h($seo['title'] ?? $pageTitle ?? getSetting('site_name', $db)) ?></title>
<meta name="keywords" content="<?= h($seo['keywords'] ?? '') ?>">
<meta name="description" content="<?= h($seo['description'] ?? '') ?>">
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
<link rel="stylesheet" href="<?= THEME_URL ?>css/style.css">
</head>
<body>
<header class="site-header">
<div class="container">
<a class="logo" href="/"><?= h(getSetting('site_name', $db) ?: 'Novel Site') ?></a>
<nav>
<a href="/"><?= $lang->get('home') ?></a>
<a href="/library"><?= $lang->get('library') ?></a>
<a href="/rank"><?= $lang->get('rank') ?></a>
<?php if (isLoggedIn()): ?>
<a href="/user/">个人中心</a>
<?php else: ?>
<a href="/user/login"><?= $lang->get('login') ?></a>
<a href="/user/register"><?= $lang->get('register') ?></a>
<?php endif; ?>
<a href="/api/ajax/switch_lang.php?lang=zh-cn">中文</a>
<a href="/api/ajax/switch_lang.php?lang=en-us">EN</a>
</nav>
</div>
</header>
<main class="site-main">5. index/footer.php
<?php defined('ROOT_PATH') or die('URLError'); ?>
</main>
<footer class="site-footer">
<div class="container">
<p>© <?= date('Y') ?> <?= h(getSetting('site_name', $db)) ?>. Powered By EnovelCms</p>
</div>
</footer>
<script>
window.lang = {
network_error: '<?= $lang->get('network_error') ?>',
sign_success: '<?= $lang->get('sign_success', '签到成功') ?>'
};
</script>
<script src="<?= THEME_URL ?>js/main.js"></script>
<?= getSetting('analytics_code', $db) ?>
</body>
</html>6. index/home.php
<?php defined('ROOT_PATH') or die('URLError'); ?>
<?php require_once THEME_PATH . 'index/header.php'; ?>
<div class="container">
<h1><?= $lang->get('demo_welcome') ?></h1>
<?= showAd('home_banner') ?>
<h2><?= DiyStrTitle('hot_novels') ?: $lang->get('hot_novels') ?></h2>
<?php $hot = DiyString('hot_novels'); ?>
<?php if (!empty($hot)): ?>
<div class="novel-grid">
<?php foreach ($hot as $n): ?>
<a class="novel-card" href="/novel/<?= $n['id'] ?>">
<img src="/<?= h($n['cover'] ?: 'assets/images/default_cover.jpg') ?>" alt="<?= h($n['title']) ?>">
<h3><?= h($n['title']) ?></h3>
<p><?= h($n['author']) ?></p>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<h2><?= $lang->get('latest_novels') ?></h2>
<ul class="latest-list">
<?php foreach ($latestUpdates as $n): ?>
<li>
<a href="/novel/<?= $n['id'] ?>"><?= h($n['title']) ?></a>
<span><?= h($n['author']) ?></span>
</li>
<?php endforeach; ?>
</ul>
<?php if (isLoggedIn()): ?>
<button id="signBtn" class="btn-primary"><?= $lang->get('sign_button') ?></button>
<?php else: ?>
<a href="/user/login" class="btn-primary"><?= $lang->get('login') ?>后签到</a>
<?php endif; ?>
</div>
<?php require_once THEME_PATH . 'index/footer.php'; ?>7. js/main.js
document.addEventListener('DOMContentLoaded', function () {
const signBtn = document.getElementById('signBtn');
if (!signBtn) return;
signBtn.addEventListener('click', function () {
signBtn.disabled = true;
fetch('/api/ajax/sign.php', {
method: 'POST'
})
.then(res => res.json())
.then(data => {
alert(data.msg || data.message || '操作完成');
if (data.code === 1) {
signBtn.innerText = '已签到';
} else {
signBtn.disabled = false;
}
})
.catch(() => {
alert(window.lang.network_error || '网络错误');
signBtn.disabled = false;
});
});
});8. css/style.css
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f5f7fb;
color: #1e293b;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 16px;
}
.site-header {
background: #fff;
border-bottom: 1px solid #e2e8f0;
padding: 14px 0;
}
.site-header .container {
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
font-size: 20px;
font-weight: 700;
color: #3b82f6;
text-decoration: none;
}
.site-header nav a {
margin-left: 12px;
color: #475569;
text-decoration: none;
}
.site-main {
min-height: 70vh;
padding: 24px 0;
}
.novel-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 16px;
}
.novel-card {
background: #fff;
border-radius: 12px;
padding: 10px;
text-decoration: none;
color: inherit;
box-shadow: 0 1px 3px rgba(0,0,0,.05);
}
.novel-card img {
width: 100%;
aspect-ratio: 2/3;
object-fit: cover;
border-radius: 8px;
}
.latest-list {
background: #fff;
border-radius: 12px;
padding: 16px;
list-style: none;
}
.latest-list li {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #f1f5f9;
}
.btn-primary {
display: inline-block;
margin-top: 16px;
padding: 10px 20px;
background: #3b82f6;
color: #fff;
border: none;
border-radius: 30px;
cursor: pointer;
text-decoration: none;
}
.site-footer {
padding: 20px 0;
text-align: center;
color: #94a3b8;
font-size: 14px;
}9. 后台配置
后台“主题管理”启用 demo 主题。
后台“自定义榜单”创建:
标识符:hot_novels
显示标题:热门推荐
排序:点击最多
数量:6
后台“广告管理”给 home_banner 添加广告。
前台访问首页,即可看到 Demo 主题效果。
点击“立即签到”会调用 /api/ajax/sign.php。
十二、开发注意事项
不要修改核心文件 尽量只改 templates/yourtheme/,否则升级会覆盖。
用户数据必须转义 输出数据库或用户内容用 h()。
语言包键名统一 主题语言包放 templates/yourtheme/lang/,缺失会自动回退 default。
AJAX 返回 JSON 前端用 res.json(),判断 code === 1。
写操作建议加 CSRF 后台已用 verify_admin_csrf(),前台部分 AJAX 未统一验证。如果你开发写操作,建议自己加 csrf_token()。
主题必须包含 template.xml 否则后台无法识别。
用户中心/作者中心依赖变量 如 $user、$vipActive,不要随便删除。
SEO 由后台控制 模板中输出 $seo['title']、$seo['keywords']、$seo['description']。
自定义榜单缓存 DiyString() 会根据后台缓存时间缓存,后台可刷新缓存。
调试 可看 data/ 下日志;生产环境建议关闭 DEBUG_MODE。
十三、快速对照表:模板文件与调用重点
| 页面模板 | 重点变量/函数 |
| 首页 index/home.php | $latestUpdates, DiyString(), DiyStrTitle(), showAd('home_banner') |
| 书库 index/library.php | $categories, $novels, $page, $totalPages, $ |