EnovelCms 2.x.x 主题模板开发教程

教程文章 2026年09月30日 EnovelCms官网

本文基于你提供的 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 和模板文件。模板文件必须按约定命名。

前台主路由

URLHandler模板文件
/homehome_handler()index/home.php
/librarylibrary_handler()index/library.php
/rankrank_handler()index/rank.php
/authorsauthor_handler()index/author.php
/searchsearch_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模板文件
/useruser/index.php
/user/profileuser/profile.php
/user/bookshelfuser/bookshelf.php
/user/historyuser/history.php
/user/golduser/gold.php
/user/signuser/sign.php
/user/vipuser/vip.php
/user/loginuser/login.php
/user/registeruser/register.php
/user/forgotuser/forgot.php
/user/rechargeuser/recharge.php
/user/logout无模板,直接退出
/user/apply_authorauthor/apply_author.php
/user/author_worksauthor/author_works.php
/user/novel_addauthor/novel_add.php
/user/novel_editauthor/novel_edit.php
/user/chapter_listauthor/chapter_list.php
/user/chapter_addauthor/chapter_add.php
/user/chapter_editauthor/chapter_edit.php
/user/volume_listauthor/volume_list.php
/user/volume_addauthor/volume_add.php
/user/volume_editauthor/volume_edit.php
/user/incomeauthor/income.php
/user/withdrawauthor/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 的加载顺序:

  1. 当前活动主题的 lang/{code}.php

  2. 默认主题 templates/default/lang/{code}.php

  3. 默认主题的 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
$seoSEO 数组: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.phpbanners, latestUpdates, topViewsWeek, topUpdate, seoPage, seoVariables, pageTitle
书库 index/library.phpcategories, novels, categoryId, status, orderBy, minWords, maxWords, vipFilter, page, totalPages, limit, total, pageTitle, seoPage, seoVariables
排行榜 index/rank.phprankType, novels, seoPage, seoVariables, pageTitle
作者列表 index/author.phpauthors, seoPage, seoVariables, pageTitle
搜索 index/search.phpcategories, novels, keyword, author, categoryId, status, orderBy, minWords, maxWords, page, totalPages, limit, total, needCaptcha, captchaError, searchExecuted, pageTitle, seoPage, seoVariables
小说详情 index/novel_detail.phpnovel, chaptersByVolume, totalWords, seoPage, seoVariables, pageTitle
阅读页 index/read.phpnovel, chapter, content, prev, next, novelId, needPay, needLogin, price, userGold, error, seoPage, seoVariables, pageTitle

用户中心

页面模板独有变量
个人中心 user/index.phpuser, vipActive, bookshelfCount, historyCount, todaySigned, pageTitle
个人资料 user/profile.phpuser, vipActive, message, pageTitle
书架 user/bookshelf.phpuser, vipActive, books, pageTitle
阅读历史 user/history.phpuser, vipActive, history, page, totalPages, total, limit, pageTitle
金币明细 user/gold.phpuser, vipActive, logs, pageTitle
签到 user/sign.phpuser, vipActive, message, todaySigned, rewardPreview, pageTitle
VIP 中心 user/vip.phpuser, vipActive, message, pricePerMonth, goldPerMonth, pageTitle
登录 user/login.phperror, pageTitle
注册 user/register.phperror, pageTitle
忘记密码 user/forgot.phpstep, message, error, pageTitle
充值 user/recharge.phpuser, vipActive, message, error, rate, pointName, rechargeLogs, pageTitle

作者中心

页面模板独有变量
申请作者 author/apply_author.phpuser, vipActive, message, error, applied, pageTitle
我的作品 author/author_works.phpuser, vipActive, novels, pageTitle
发布作品 author/novel_add.phpuser, vipActive, categories, penName, isAuthor, pageTitle
编辑作品 author/novel_edit.phpuser, vipActive, novel, categories, pageTitle
章节列表 author/chapter_list.phpuser, vipActive, novel, novelId, chapters, page, totalPages, pageTitle
添加章节 author/chapter_add.phpuser, vipActive, novel, novelId, volumes, pageTitle
编辑章节 author/chapter_edit.phpuser, vipActive, novel, novelId, chapter, volumes, content, pageTitle
分卷列表 author/volume_list.phpuser, vipActive, novel, novelId, volumes, pageTitle
添加分卷 author/volume_add.phpuser, vipActive, novel, novelId, pageTitle
编辑分卷 author/volume_edit.phpuser, vipActive, novel, novelId, volume, pageTitle
收入统计 author/income.phpuser, vipActive, stats, novelList, novelId, timeRange, chapterStats, detailList, page, totalPages, total, pageTitle
提现 author/withdraw.phpuser, 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.phpPOSTnovel_id{code, message, favorites}
/api/ajax/remove_bookshelf.phpPOSTnovel_id{code, message, favorites}
/api/ajax/sign.phpPOST/GET无{code, msg}
/api/ajax/novel.phpPOSTaction=...{code, msg, data}
/api/ajax/get_chapter_content.phpGETid{code, content} 仅管理员
/api/ajax/switch_lang.phpGETlang跳转
/api/apply_author.phpPOSTFormData{code, msg}
/api/captcha.phpGET无PNG 验证码
/api/crawler/publish.phpPOSTapi_key, action{code, message, data}
/api/pay_notify.phpPOST/GET
支付回调 success/fail
/api/pay_return.phpGET
支付同步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>&copy; <?= 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, $