从零开始手把手教你开发一个融合 PHP + 现代前端的二次元 WordPress 主题,包含完整代码示例和动漫专属功能
前言:为什么二次元网站需要定制主题?
作为二次元爱好者,你是否也曾幻想过拥有一个完全属于自己的动漫空间?在这个空间里,不仅有精美的番剧推荐、角色图鉴,还能让初音未来、时崎狂三这样的虚拟角色陪伴你浏览每一篇文章。
WordPress 作为全球最流行的 CMS,其强大的扩展性让这一切成为可能。本文将结合 PHP + Tailwind CSS + Alpine.js + Live2D,带你开发一个功能完整的二次元主题。
一、技术栈选型与开发环境
1.1 核心技术栈
技术 用途 二次元特色
PHP 8.x WordPress 核心逻辑 自定义动漫文章类型、角色分类
Tailwind CSS 样式系统 樱花粉、霓虹渐变等动漫配色
Alpine.js 前端交互 轻量级动态效果,不拖慢速度
Live2D 看板娘 二次元网站灵魂组件
AOS 滚动动画 让页面像动漫 OP 一样动感
1.2 二次元特色功能规划
功能清单:
├── 看板娘系统(Live2D)
│ ├── 多模型切换(初音、狂三、蕾姆等)
│ ├── 语音互动(点击触发语音)
│ └── 节日换装(自动切换季节/节日服装)
├── 动漫内容管理
│ ├── 自定义文章类型:番剧、角色、资讯
│ ├── 番剧评分系统(5星制)
│ ├── 角色关系图(类似萌娘百科)
│ └── 季度番剧归档(冬/春/夏/秋)
├── 视觉特效
│ ├── 樱花飘落背景
│ ├── 霓虹光效按钮
│ ├── 渐变滚动进度条
│ └── 图片灯箱画廊
└── 社区功能
├── 二次元表情评论
├── CP 投票系统
└── 角色应援榜
1.3 项目结构
anime-theme/
├── style.css # 主题信息
├── functions.php # 主题核心功能
├── index.php # 主模板
├── header.php # 头部(含看板娘容器)
├── footer.php # 底部
├── single.php # 文章详情
├── page.php # 页面模板
├── archive.php # 归档
├── 404.php # 404页面
├── template-anime-list.php # 番剧列表模板
├── template-characters.php # 角色图鉴模板
├── assets/
│ ├── css/
│ │ ├── tailwind.css # 编译后的样式
│ │ └── live2d.css # 看板娘样式
│ ├── js/
│ │ ├── main.js # 主脚本
│ │ ├── live2d.js # 看板娘控制
│ │ └── sakura.js # 樱花飘落效果
│ └── models/ # Live2D 模型文件
│ ├── miku/
│ ├── kurumi/
│ └── rem/
├── inc/
│ ├── setup.php # 主题设置
│ ├── post-types.php # 自定义文章类型
│ ├── shortcodes.php # 短代码
│ └── ajax.php # AJAX 功能
└── template-parts/
├── content-anime.php # 番剧卡片
└── content-character.php # 角色卡片
二、WordPress 二次元核心功能实现
2.1 主题信息文件(style.css)
/*
Theme Name: 二次元主题 AnimeWP
Theme URI: https://example.com/animewp
Author: 你的名字
Author URI: https://example.com
Description: 专为二次元爱好者打造的 WordPress 主题,支持 Live2D 看板娘、番剧评分、角色图鉴等功能
Version: 1.0.0
License: GPL v2 or later
Text Domain: animewp
Tags: anime, manga, japanese, colorful, live2d, blog, portfolio
【二次元特色】
- Live2D 看板娘,支持多模型切换
- 樱花飘落特效
- 番剧季度归档系统
- 角色评分与应援榜
- 二次元风格表情包支持
*/
/* 样式由 Tailwind 处理,此文件仅用于主题信息 */
2.2 主题核心功能(functions.php)
<?php
/**
* AnimeWP 二次元主题核心功能
*
* @package AnimeWP
*/
// 定义主题常量
define( 'ANIMEWP_VERSION', '1.0.0' );
define( 'ANIMEWP_DIR', get_template_directory() );
define( 'ANIMEWP_URI', get_template_directory_uri() );
/**
* 主题初始化设置
*/
function animewp_setup() {
// 基础支持
add_theme_support( 'post-thumbnails' );
add_theme_support( 'title-tag' );
add_theme_support( 'html5', array( 'search-form', 'comment-form', 'comment-list', 'gallery', 'caption' ) );
// 注册导航菜单
register_nav_menus( array(
'primary' => esc_html__( '主菜单', 'animewp' ),
'anime' => esc_html__( '番剧菜单', 'animewp' ),
'footer' => esc_html__( '底部菜单', 'animewp' ),
) );
// 自定义缩略图尺寸(二次元卡片专用)
add_image_size( 'anime-card', 400, 560, true ); // 番剧卡片
add_image_size( 'character-card', 300, 300, true ); // 角色头像
add_image_size( 'anime-banner', 1200, 400, true ); // 番剧头图
// 支持文章格式(二次元专用)
add_theme_support( 'post-formats', array(
'gallery', // 番剧截图集
'video', // PV/MAD 视频
'quote', // 经典台词
'status' // 新番速报
) );
}
add_action( 'after_setup_theme', 'animewp_setup' );
/**
* 引入功能模块
*/
require_once ANIMEWP_DIR . '/inc/post-types.php'; // 自定义文章类型
require_once ANIMEWP_DIR . '/inc/shortcodes.php'; // 短代码
require_once ANIMEWP_DIR . '/inc/ajax.php'; // AJAX 功能
require_once ANIMEWP_DIR . '/inc/live2d.php'; // 看板娘配置
require_once ANIMEWP_DIR . '/inc/customizer.php'; // 自定义器
/**
* 注册侧边栏
*/
function animewp_widgets_init() {
register_sidebar( array(
'name' => esc_html__( '番剧侧边栏', 'animewp' ),
'id' => 'anime-sidebar',
'description' => esc_html__( '显示在番剧详情页的侧边栏,可放置新番推荐、角色投票等小工具', 'animewp' ),
'before_widget' => '<div class="widget %2$s bg-white/80 backdrop-blur-sm rounded-2xl p-5 mb-6 shadow-lg border border-purple-100">',
'after_widget' => '</div>',
'before_title' => '<h3 class="widget-title text-lg font-bold mb-4 pb-2 border-b-2 border-pink-400 text-pink-600">✨ ',
'after_title' => '</h3>',
) );
register_sidebar( array(
'name' => esc_html__( '角色侧边栏', 'animewp' ),
'id' => 'character-sidebar',
'description' => esc_html__( '角色图鉴页侧边栏', 'animewp' ),
'before_widget' => '<div class="widget %2$s bg-gradient-to-br from-purple-50 to-pink-50 rounded-2xl p-5 mb-6 shadow-md">',
'after_widget' => '</div>',
'before_title' => '<h3 class="text-md font-bold mb-3 text-purple-700">🎭 ',
'after_title' => '</h3>',
) );
}
add_action( 'widgets_init', 'animewp_widgets_init' );
2.3 自定义文章类型(inc/post-types.php)
<?php
/**
* 二次元自定义文章类型
*
* @package AnimeWP
*/
/**
* 注册番剧文章类型
*/
function animewp_register_anime_post_type() {
$labels = array(
'name' => '番剧',
'singular_name' => '番剧',
'menu_name' => '番剧管理',
'add_new' => '添加新番',
'add_new_item' => '添加新番剧',
'edit_item' => '编辑番剧',
'new_item' => '新番剧',
'view_item' => '查看番剧',
'search_items' => '搜索番剧',
'not_found' => '未找到番剧',
'not_found_in_trash' => '回收站中没有番剧',
);
$args = array(
'labels' => $labels,
'public' => true,
'has_archive' => true,
'rewrite' => array( 'slug' => 'anime' ),
'menu_icon' => 'dashicons-format-video',
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'comments', 'custom-fields' ),
'show_in_rest' => true, // 支持古腾堡编辑器
'taxonomies' => array( 'anime_season', 'anime_genre' ),
);
register_post_type( 'anime', $args );
}
add_action( 'init', 'animewp_register_anime_post_type' );
/**
* 注册角色文章类型
*/
function animewp_register_character_post_type() {
$labels = array(
'name' => '角色',
'singular_name' => '角色',
'menu_name' => '角色图鉴',
'add_new' => '添加角色',
'add_new_item' => '添加新角色',
'edit_item' => '编辑角色',
'new_item' => '新角色',
'view_item' => '查看角色',
'search_items' => '搜索角色',
);
$args = array(
'labels' => $labels,
'public' => true,
'has_archive' => true,
'rewrite' => array( 'slug' => 'character' ),
'menu_icon' => 'dashicons-admin-users',
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
'show_in_rest' => true,
);
register_post_type( 'character', $args );
}
add_action( 'init', 'animewp_register_character_post_type' );
/**
* 注册季度分类(冬/春/夏/秋)
*/
function animewp_register_season_taxonomy() {
$labels = array(
'name' => '播出季度',
'singular_name' => '季度',
'search_items' => '搜索季度',
'all_items' => '所有季度',
'edit_item' => '编辑季度',
'update_item' => '更新季度',
'add_new_item' => '添加新季度',
'new_item_name' => '新季度名称',
'menu_name' => '播出季度',
);
$args = array(
'labels' => $labels,
'hierarchical' => true,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'season' ),
'show_in_rest' => true,
);
register_taxonomy( 'anime_season', array( 'anime' ), $args );
// 预设季度数据
$seasons = array( '2025年冬季番', '2025年春季番', '2025年夏季番', '2025年秋季番' );
foreach ( $seasons as $season ) {
if ( ! term_exists( $season, 'anime_season' ) ) {
wp_insert_term( $season, 'anime_season' );
}
}
}
add_action( 'init', 'animewp_register_season_taxonomy' );
/**
* 注册番剧类型分类(热血、恋爱、科幻等)
*/
function animewp_register_genre_taxonomy() {
$labels = array(
'name' => '番剧类型',
'singular_name' => '类型',
'search_items' => '搜索类型',
'all_items' => '所有类型',
'edit_item' => '编辑类型',
'update_item' => '更新类型',
'add_new_item' => '添加新类型',
'new_item_name' => '新类型名称',
'menu_name' => '番剧类型',
);
$args = array(
'labels' => $labels,
'hierarchical' => true,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'genre' ),
'show_in_rest' => true,
);
register_taxonomy( 'anime_genre', array( 'anime' ), $args );
// 预设类型
$genres = array( '热血', '恋爱', '科幻', '奇幻', '日常', '悬疑', '搞笑', '治愈', '致郁', '战斗' );
foreach ( $genres as $genre ) {
if ( ! term_exists( $genre, 'anime_genre' ) ) {
wp_insert_term( $genre, 'anime_genre' );
}
}
}
add_action( 'init', 'animewp_register_genre_taxonomy' );
/**
* 添加番剧自定义字段(评分、话数、原作等)
*/
function animewp_add_anime_meta_boxes() {
add_meta_box(
'anime_details',
'📺 番剧详细信息',
'animewp_render_anime_meta_box',
'anime',
'normal',
'high'
);
}
add_action( 'add_meta_boxes', 'animewp_add_anime_meta_boxes' );
function animewp_render_anime_meta_box( $post ) {
wp_nonce_field( 'animewp_anime_meta', 'animewp_anime_meta_nonce' );
$rating = get_post_meta( $post->ID, '_anime_rating', true );
$episodes = get_post_meta( $post->ID, '_anime_episodes', true );
$studio = get_post_meta( $post->ID, '_anime_studio', true );
$original = get_post_meta( $post->ID, '_anime_original', true );
$air_date = get_post_meta( $post->ID, '_anime_air_date', true );
?>
<div class="anime-meta-box" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px;">
<p>
<label style="font-weight: bold;">⭐ 评分(1-5):</label>
<input type="number" name="anime_rating" value="<?php echo esc_attr( $rating ); ?>" step="0.1" min="0" max="5" style="width: 100%;" />
</p>
<p>
<label style="font-weight: bold;">📺 总话数:</label>
<input type="number" name="anime_episodes" value="<?php echo esc_attr( $episodes ); ?>" style="width: 100%;" />
</p>
<p>
<label style="font-weight: bold;">🏢 制作公司:</label>
<input type="text" name="anime_studio" value="<?php echo esc_attr( $studio ); ?>" style="width: 100%;" />
</p>
<p>
<label style="font-weight: bold;">📖 原作类型:</label>
<select name="anime_original" style="width: 100%;">
<option value="">请选择</option>
<option value="漫画" <?php selected( $original, '漫画' ); ?>>漫画</option>
<option value="轻小说" <?php selected( $original, '轻小说' ); ?>>轻小说</option>
<option value="游戏" <?php selected( $original, '游戏' ); ?>>游戏</option>
<option value="原创" <?php selected( $original, '原创' ); ?>>原创</option>
</select>
</p>
<p>
<label style="font-weight: bold;">📅 首播日期:</label>
<input type="date" name="anime_air_date" value="<?php echo esc_attr( $air_date ); ?>" style="width: 100%;" />
</p>
</div>
<?php
}
function animewp_save_anime_meta( $post_id ) {
if ( ! isset( $_POST['animewp_anime_meta_nonce'] ) ) return;
if ( ! wp_verify_nonce( $_POST['animewp_anime_meta_nonce'], 'animewp_anime_meta' ) ) return;
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
$fields = array( 'anime_rating', 'anime_episodes', 'anime_studio', 'anime_original', 'anime_air_date' );
foreach ( $fields as $field ) {
if ( isset( $_POST[$field] ) ) {
update_post_meta( $post_id, '_' . $field, sanitize_text_field( $_POST[$field] ) );
}
}
}
add_action( 'save_post_anime', 'animewp_save_anime_meta' );
三、二次元前端样式与动画
3.1 Tailwind 配置(tailwind.config.js)
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./**/*.php',
'./assets/src/js/**/*.js',
'./template-parts/**/*.php',
],
theme: {
extend: {
colors: {
// 二次元专属配色
sakura: {
50: '#fff5f7',
100: '#ffe4e9',
200: '#ffc2ce',
300: '#ff9fb3',
400: '#ff7d98',
500: '#ff5a7d',
600: '#e63e62',
700: '#c22a4a',
},
neon: {
pink: '#ff00e5',
blue: '#00f2ff',
purple: '#bf00ff',
},
anime: {
dark: '#1a1a2e',
card: 'rgba(255, 255, 255, 0.1)',
}
},
fontFamily: {
'anime': ['"Zen Maru Gothic"', '"Noto Sans CJK JP"', 'system-ui'],
'title': ['"Poppins"', '"Zen Maru Gothic"', 'sans-serif'],
},
backgroundImage: {
'gradient-anime': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'gradient-sakura': 'linear-gradient(135deg, #f5af19 0%, #f12711 100%)',
'gradient-neon': 'linear-gradient(45deg, #ff00e5, #00f2ff)',
},
animation: {
'float': 'float 3s ease-in-out infinite',
'glow': 'glow 2s ease-in-out infinite',
'sakura-fall': 'sakuraFall 8s linear infinite',
'bounce-slow': 'bounce 2s infinite',
'pulse-fast': 'pulse 1s infinite',
'shimmer': 'shimmer 2s infinite',
'slide-up': 'slideUp 0.5s ease-out',
'slide-down': 'slideDown 0.3s ease-out',
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0px)' },
'50%': { transform: 'translateY(-10px)' },
},
glow: {
'0%, 100%': { textShadow: '0 0 5px rgba(255,90,125,0.5)' },
'50%': { textShadow: '0 0 20px rgba(255,90,125,0.8)' },
},
sakuraFall: {
'0%': { transform: 'translateY(-10vh) rotate(0deg)', opacity: 1 },
'100%': { transform: 'translateY(100vh) rotate(360deg)', opacity: 0 },
},
shimmer: {
'100%': { transform: 'translateX(100%)' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
slideDown: {
'0%': { opacity: '0', transform: 'translateY(-20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
},
},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
require('@tailwindcss/aspect-ratio'),
],
}
3.2 主样式文件(assets/src/css/main.css)
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
/* 自定义滚动条(二次元风格) */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
@apply bg-gray-100 rounded-full;
}
::-webkit-scrollbar-thumb {
@apply bg-gradient-to-b from-pink-400 to-purple-500 rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply from-pink-500 to-purple-600;
}
/* 全局样式 */
body {
@apply bg-gradient-to-br from-gray-50 to-purple-50 font-anime text-gray-800;
}
/* 选中文本样式 */
::selection {
@apply bg-pink-400 text-white;
}
}
@layer components {
/* 霓虹卡片效果 */
.neon-card {
@apply relative bg-white/90 backdrop-blur-sm rounded-2xl shadow-lg overflow-hidden transition-all duration-300;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.02);
}
.neon-card:hover {
@apply transform -translate-y-2 shadow-2xl;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2), 0 0 20px rgba(255, 90, 125, 0.3);
}
/* 二次元按钮 */
.anime-btn {
@apply relative px-6 py-3 rounded-full font-bold text-white overflow-hidden transition-all duration-300;
background: linear-gradient(135deg, #ff5a7d, #e63e62);
}
.anime-btn::before {
content: '';
@apply absolute top-0 left-[-100%] w-full h-full bg-gradient-to-r from-transparent via-white/30 to-transparent transition-all duration-500;
}
.anime-btn:hover::before {
left: 100%;
}
.anime-btn:active {
transform: scale(0.95);
}
/* 樱花飘落容器 */
.sakura-container {
@apply fixed top-0 left-0 w-full h-full pointer-events-none overflow-hidden z-0;
}
.sakura {
@apply absolute pointer-events-none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ff9fb3'%3E%3Cpath d='M12 2L13.5 8.5L20 9L15 12.5L17 19L12 15L7 19L9 12.5L4 9L10.5 8.5L12 2Z'/%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
opacity: 0.6;
width: 20px;
height: 20px;
animation: sakuraFall linear infinite;
}
/* 评分星星 */
.star-rating {
@apply inline-flex gap-0.5;
}
.star {
@apply text-gray-300 transition-colors duration-200;
}
.star.filled {
@apply text-yellow-400;
}
/* 角色卡片悬浮特效 */
.character-card {
@apply relative overflow-hidden rounded-2xl transition-all duration-500;
}
.character-card::after {
content: '';
@apply absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 transition-opacity duration-300;
}
.character-card:hover::after {
@apply opacity-100;
}
.character-card .character-info {
@apply absolute bottom-0 left-0 right-0 p-4 text-white transform translate-y-full transition-transform duration-300 z-10;
}
.character-card:hover .character-info {
transform: translateY(0);
}
}
@layer utilities {
/* 文字霓虹效果 */
.text-neon {
text-shadow: 0 0 5px currentColor, 0 0 10px currentColor;
}
/* 毛玻璃效果 */
.glass {
@apply bg-white/70 backdrop-blur-md border border-white/20;
}
/* 渐变文字 */
.gradient-text {
@apply bg-clip-text text-transparent bg-gradient-to-r from-pink-500 to-purple-500;
}
}
3.3 樱花飘落特效(assets/src/js/sakura.js)
/**
* 樱花飘落特效
* 为二次元网站增加浪漫氛围
*/
class SakuraEffect {
constructor(options = {}) {
this.options = {
count: 30, // 樱花数量
minSize: 15, // 最小尺寸
maxSize: 30, // 最大尺寸
minDuration: 5, // 最小飘落时间(秒)
maxDuration: 12, // 最大飘落时间
colors: ['#ff9fb3', '#ffb7c5', '#ffc2ce', '#ffe4e9'], // 樱花颜色
...options
}
this.container = null
this.sakuras = []
this.animationId = null
}
init() {
// 创建容器
this.container = document.createElement('div')
this.container.className = 'sakura-container'
document.body.appendChild(this.container)
// 生成樱花
for (let i = 0; i < this.options.count; i++) {
this.createSakura()
}
// 持续生成新樱花
setInterval(() => {
if (this.sakuras.length < this.options.count) {
this.createSakura()
}
}, 2000)
}
createSakura() {
const sakura = document.createElement('div')
sakura.className = 'sakura'
// 随机属性
const size = Math.random() * (this.options.maxSize - this.options.minSize) + this.options.minSize
const left = Math.random() * 100
const duration = Math.random() * (this.options.maxDuration - this.options.minDuration) + this.options.minDuration
const delay = Math.random() * 5
const color = this.options.colors[Math.floor(Math.random() * this.options.colors.length)]
sakura.style.width = `${size}px`
sakura.style.height = `${size}px`
sakura.style.left = `${left}%`
sakura.style.animationDuration = `${duration}s`
sakura.style.animationDelay = `${delay}s`
sakura.style.backgroundColor = color
sakura.style.opacity = Math.random() * 0.5 + 0.3
// 随机旋转角度
const rotate = Math.random() * 360
sakura.style.transform = `rotate(${rotate}deg)`
this.container.appendChild(sakura)
this.sakuras.push(sakura)
// 动画结束后移除
sakura.addEventListener('animationend', () => {
sakura.remove()
const index = this.sakuras.indexOf(sakura)
if (index > -1) this.sakuras.splice(index, 1)
})
}
destroy() {
if (this.container) {
this.container.remove()
this.container = null
}
this.sakuras = []
}
}
// 自动初始化(可在设置中关闭)
if (typeof window !== 'undefined' && !window.disableSakura) {
const sakura = new SakuraEffect({ count: 40 })
sakura.init()
}
3.4 看板娘组件(inc/live2d.php)
<?php
/**
* Live2D 看板娘配置
*
* @package AnimeWP
*/
/**
* 在页脚添加看板娘容器
*/
function animewp_add_live2d_container() {
?>
<div id="live2d-widget" class="fixed bottom-0 <?php echo get_theme_mod( 'live2d_position', 'right' ) === 'left' ? 'left-0' : 'right-0'; ?> z-50">
<canvas id="live2d-canvas" width="300" height="400" style="width: 300px; height: 400px;"></canvas>
<div class="live2d-control absolute -top-10 <?php echo get_theme_mod( 'live2d_position', 'right' ) === 'left' ? 'left-0' : 'right-0'; ?> flex gap-2">
<button id="live2d-change" class="bg-pink-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-pink-600 transition-colors" title="换装">
👗
</button>
<button id="live2d-speak" class="bg-purple-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-purple-600 transition-colors" title="对话">
💬
</button>
<button id="live2d-hide" class="bg-gray-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-gray-600 transition-colors" title="隐藏">
✖️
</button>
</div>
</div>
<?php
}
add_action( 'wp_footer', 'animewp_add_live2d_container' );
/**
* 加载 Live2D 脚本
*/
function animewp_enqueue_live2d_scripts() {
wp_enqueue_script( 'live2d-cubism', 'https://cdn.jsdelivr.net/npm/live2d-widget@3.1.4/lib/L2Dwidget.min.js', array(), '3.1.4', true );
wp_enqueue_script( 'animewp-live2d', ANIMEWP_URI . '/assets/js/live2d.js', array( 'live2d-cubism' ), ANIMEWP_VERSION, true );
// 传递 PHP 变量到 JS
wp_localize_script( 'animewp-live2d', 'live2dConfig', array(
'modelPath' => ANIMEWP_URI . '/assets/models/' . get_theme_mod( 'live2d_model', 'miku' ) . '/',
'position' => get_theme_mod( 'live2d_position', 'right' ),
'messages' => array(
'morning' => '早上好呀,今天也要元气满满哦!',
'evening' => '晚上了呢,记得早点休息~',
'click' => '诶?戳我干嘛啦~',
'greeting' => '欢迎来到我的动漫小窝!',
),
) );
}
add_action( 'wp_enqueue_scripts', 'animewp_enqueue_live2d_scripts' );
3.5 看板娘控制脚本(assets/js/live2d.js)
/**
* Live2D 看板娘控制脚本
*/
document.addEventListener('DOMContentLoaded', function() {
let live2dWidget = null
let currentModel = 'miku'
let models = ['miku', 'kurumi', 'rem']
// 初始化 Live2D
function initLive2D() {
if (typeof L2Dwidget === 'undefined') return
L2Dwidget.init({
model: {
jsonPath: live2dConfig.modelPath + 'model.json',
scale: 0.5,
},
display: {
position: live2dConfig.position,
width: 300,
height: 400,
hOffset: live2dConfig.position === 'right' ? -50 : 50,
vOffset: -20,
},
mobile: {
show: true,
scale: 0.4,
},
react: {
opacity: 0.9,
},
dialog: {
enable: true,
script: {
'早上好': live2dConfig.messages.morning,
'晚上好': live2dConfig.messages.evening,
'hello': live2dConfig.messages.greeting,
}
}
})
live2dWidget = L2Dwidget
}
// 切换模型
function switchModel() {
let nextIndex = (models.indexOf(currentModel) + 1) % models.length
currentModel = models[nextIndex]
if (live2dWidget) {
live2dWidget.init({
model: {
jsonPath: `${window.location.origin}/wp-content/themes/animewp/assets/models/${currentModel}/model.json`,
scale: 0.5,
}
})
}
// 播放切换语音
speak('换上新衣服啦,好看吗?')
}
// 对话功能
function speak(message) {
if (live2dWidget && live2dWidget.dialog) {
live2dWidget.dialog.showMessage(message)
}
}
// 随机说话
function randomSpeak() {
const messages = [
'今天想看什么番呢?',
'最近的新番都超好看!',
'要记得吃饭哦~',
'一起看动漫吧!',
live2dConfig.messages.greeting
]
const randomMsg = messages[Math.floor(Math.random() * messages.length)]
speak(randomMsg)
}
// 点击互动
function onClick() {
speak(live2dConfig.messages.click)
}
// 绑定事件
document.getElementById('live2d-change')?.addEventListener('click', switchModel)
document.getElementById('live2d-speak')?.addEventListener('click', () => randomSpeak())
document.getElementById('live2d-hide')?.addEventListener('click', () => {
const widget = document.getElementById('live2d-widget')
if (widget) widget.style.display = 'none'
})
document.getElementById('live2d-canvas')?.addEventListener('click', onClick)
// 定时随机说话
setInterval(randomSpeak, 60000)
// 根据时间问候
const hour = new Date().getHours()
if (hour < 12) speak(live2dConfig.messages.morning)
else if (hour > 18) speak(live2dConfig.messages.evening)
// 初始化
initLive2D()
})
四、二次元模板文件
4.1 头部模板(header.php)
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<?php wp_head(); ?>
</head>
<body <?php body_class( 'bg-gradient-to-br from-gray-50 to-purple-50' ); ?>>
<?php wp_body_open(); ?>
<!-- 导航栏 -->
<nav class="glass sticky top-0 z-50 border-b border-white/20">
<div class="container mx-auto px-4 py-3">
<div class="flex items-center justify-between">
<!-- Logo -->
<div class="flex items-center space-x-2">
<?php if ( has_custom_logo() ) : ?>
<?php the_custom_logo(); ?>
<?php else : ?>
<span class="text-2xl font-bold gradient-text">✨ AnimeWP</span>
<?php endif; ?>
</div>
<!-- 主菜单 -->
<?php
wp_nav_menu( array(
'theme_location' => 'primary',
'container' => false,
'menu_class' => 'hidden md:flex space-x-6 text-gray-700 font-medium',
'fallback_cb' => false,
'link_before' => '<span class="hover:text-pink-500 transition-colors">',
'link_after' => '</span>',
) );
?>
<!-- 移动端菜单按钮 -->
<button id="mobile-menu-btn" class="md:hidden p-2 rounded-lg hover:bg-pink-100 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
</div>
</nav>
<!-- 移动端侧边菜单 -->
<div id="mobile-menu" class="fixed inset-0 bg-black/50 z-50 hidden transition-opacity" x-data="{ open: false }" x-show="open" x-transition.opacity>
<div class="absolute right-0 top-0 w-64 h-full bg-white shadow-xl p-6" x-show="open" x-transition.duration.300>
<button id="close-menu" class="absolute top-4 right-4 text-gray-500 hover:text-pink-500">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
<?php
wp_nav_menu( array(
'theme_location' => 'primary',
'container' => false,
'menu_class' => 'flex flex-col space-y-4 mt-8',
'fallback_cb' => false,
) );
?>
</div>
</div>
<main class="min-h-screen">
4.2 番剧列表模板(template-anime-list.php)
<?php
/**
* Template Name: 番剧列表
* Template Post Type: page
*/
get_header();
?>
<div class="container mx-auto px-4 py-8">
<!-- 页面标题 -->
<div class="text-center mb-12 animate-slide-up">
<h1 class="text-4xl md:text-5xl font-bold gradient-text mb-4">🎬 新番推荐</h1>
<p class="text-gray-600">精选每一季度的优质动漫,带你畅游二次元世界</p>
</div>
<!-- 季度筛选 -->
<div class="flex flex-wrap justify-center gap-3 mb-10">
<?php
$seasons = get_terms( array(
'taxonomy' => 'anime_season',
'hide_empty' => false,
) );
foreach ( $seasons as $season ) :
$is_current = isset( $_GET['season'] ) && $_GET['season'] == $season->slug;
?>
<a href="?season=<?php echo $season->slug; ?>"
class="px-5 py-2 rounded-full transition-all <?php echo $is_current ? 'bg-pink-500 text-white shadow-lg' : 'bg-white text-gray-700 hover:bg-pink-100'; ?>">
<?php echo $season->name; ?>
</a>
<?php endforeach; ?>
</div>
<!-- 番剧卡片网格 -->
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
<?php
$args = array(
'post_type' => 'anime',
'posts_per_page' => 20,
'paged' => get_query_var( 'paged' ) ?: 1,
);
if ( isset( $_GET['season'] ) && ! empty( $_GET['season'] ) ) {
$args['tax_query'] = array( array(
'taxonomy' => 'anime_season',
'field' => 'slug',
'terms' => sanitize_text_field( $_GET['season'] ),
) );
}
$anime_query = new WP_Query( $args );
if ( $anime_query->have_posts() ) :
while ( $anime_query->have_posts() ) : $anime_query->the_post();
$rating = get_post_meta( get_the_ID(), '_anime_rating', true );
$episodes = get_post_meta( get_the_ID(), '_anime_episodes', true );
?>
<div class="neon-card group">
<a href="<?php the_permalink(); ?>" class="block">
<!-- 封面图 -->
<div class="relative overflow-hidden rounded-t-2xl">
<?php if ( has_post_thumbnail() ) : ?>
<?php the_post_thumbnail( 'anime-card', array( 'class' => 'w-full h-64 object-cover group-hover:scale-110 transition-transform duration-500' ) ); ?>
<?php else : ?>
<div class="w-full h-64 bg-gradient-to-br from-pink-200 to-purple-200 flex items-center justify-center">
<span class="text-4xl">🎬</span>
</div>
<?php endif; ?>
<!-- 评分徽章 -->
<?php if ( $rating ) : ?>
<div class="absolute top-2 right-2 bg-black/70 backdrop-blur-sm rounded-full px-2 py-1 text-xs text-yellow-400">
⭐ <?php echo number_format( $rating, 1 ); ?>
</div>
<?php endif; ?>
<!-- 话数 -->
<?php if ( $episodes ) : ?>
<div class="absolute bottom-2 left-2 bg-black/70 backdrop-blur-sm rounded-full px-2 py-1 text-xs text-white">
📺 <?php echo $episodes; ?>话
</div>
<?php endif; ?>
</div>
<!-- 信息 -->
<div class="p-4">
<h3 class="font-bold text-lg line-clamp-1 group-hover:text-pink-500 transition-colors">
<?php the_title(); ?>
</h3>
<?php
$genres = get_the_terms( get_the_ID(), 'anime_genre' );
if ( $genres && ! is_wp_error( $genres ) ) :
?>
<div class="flex flex-wrap gap-1 mt-2">
<?php foreach ( array_slice( $genres, 0, 2 ) as $genre ) : ?>
<span class="text-xs px-2 py-0.5 bg-pink-100 text-pink-600 rounded-full">
<?php echo $genre->name; ?>
</span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</a>
</div>
<?php
endwhile;
wp_reset_postdata();
else :
?>
<div class="col-span-full text-center py-20">
<div class="text-6xl mb-4">😢</div>
<p class="text-gray-500">暂时还没有番剧,敬请期待~</p>
</div>
<?php endif; ?>
</div>
<!-- 分页 -->
<div class="mt-12">
<?php
echo paginate_links( array(
'total' => $anime_query->max_num_pages,
'current' => max( 1, get_query_var( 'paged' ) ),
'prev_text' => '← 上一页',
'next_text' => '下一页 →',
'type' => 'list',
'before_page_number' => '<span class="sr-only">第</span>',
'after_page_number' => '<span class="sr-only">页</span>',
) );
?>
</div>
</div>
<?php get_footer(); ?>
4.3 角色图鉴模板(template-characters.php)
<?php
/**
* Template Name: 角色图鉴
*/
get_header();
?>
<div class="container mx-auto px-4 py-8">
<div class="text-center mb-12 animate-slide-up">
<h1 class="text-4xl md:text-5xl font-bold gradient-text mb-4">🎭 角色图鉴</h1>
<p class="text-gray-600">收录各大热门动漫的经典角色,谁是你心中的本命?</p>
</div>
<!-- 角色卡片网格 -->
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
<?php
$character_query = new WP_Query( array(
'post_type' => 'character',
'posts_per_page' => -1,
) );
if ( $character_query->have_posts() ) :
while ( $character_query->have_posts() ) : $character_query->the_post();
$anime = get_post_meta( get_the_ID(), '_character_anime', true );
$voice_actor = get_post_meta( get_the_ID(), '_character_voice_actor', true );
?>
<div class="character-card group cursor-pointer" onclick="location.href='<?php the_permalink(); ?>'">
<div class="relative rounded-2xl overflow-hidden bg-white shadow-lg">
<?php if ( has_post_thumbnail() ) : ?>
<?php the_post_thumbnail( 'character-card', array( 'class' => 'w-full aspect-square object-cover group-hover:scale-110 transition-transform duration-500' ) ); ?>
<?php else : ?>
<div class="w-full aspect-square bg-gradient-to-br from-pink-200 to-purple-200 flex items-center justify-center">
<span class="text-5xl">👤</span>
</div>
<?php endif; ?>
<div class="character-info absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/80 via-black/40 to-transparent">
<h3 class="text-white font-bold text-lg"><?php the_title(); ?></h3>
<?php if ( $anime ) : ?>
<p class="text-white/80 text-sm">🎬 <?php echo esc_html( $anime ); ?></p>
<?php endif; ?>
<?php if ( $voice_actor ) : ?>
<p class="text-white/60 text-xs mt-1">🎙️ <?php echo esc_html( $voice_actor ); ?></p>
<?php endif; ?>
</div>
</div>
<!-- 应援按钮 -->
<button class="support-btn mt-3 w-full py-2 rounded-full bg-gradient-to-r from-pink-500 to-purple-500 text-white font-medium opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-y-2 group-hover:translate-y-0"
data-id="<?php echo get_the_ID(); ?>">
❤️ 应援 (<span class="support-count"><?php echo get_post_meta( get_the_ID(), '_support_count', true ) ?: 0; ?></span>)
</button>
</div>
<?php
endwhile;
wp_reset_postdata();
else :
?>
<div class="col-span-full text-center py-20">
<div class="text-6xl mb-4">😿</div>
<p class="text-gray-500">角色图鉴正在制作中...</p>
</div>
<?php endif; ?>
</div>
</div>
<script>
// 应援功能
document.querySelectorAll('.support-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation()
const postId = btn.dataset.id
const countSpan = btn.querySelector('.support-count')
const response = await fetch('<?php echo admin_url("admin-ajax.php"); ?>', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `action=character_support&id=${postId}`
})
const result = await response.json()
if (result.success) {
countSpan.textContent = result.count
btn.style.transform = 'scale(1.1)'
setTimeout(() => btn.style.transform = '', 200)
}
})
})
</script>
<?php get_footer(); ?>
五、AJAX 功能实现(inc/ajax.php)
<?php
/**
* AJAX 功能处理
*/
/**
* 角色应援功能
*/
function animewp_character_support() {
$post_id = isset( $_POST['id'] ) ? intval( $_POST['id'] ) : 0;
if ( ! $post_id || get_post_type( $post_id ) !== 'character' ) {
wp_send_json_error( '无效的角色' );
}
$count = get_post_meta( $post_id, '_support_count', true );
$count = $count ? intval( $count ) + 1 : 1;
update_post_meta( $post_id, '_support_count', $count );
wp_send_json_success( array( 'count' => $count ) );
}
add_action( 'wp_ajax_character_support', 'animewp_character_support' );
add_action( 'wp_ajax_nopriv_character_support', 'animewp_character_support' );
/**
* 番剧评分功能
*/
function animewp_anime_rating() {
$post_id = isset( $_POST['id'] ) ? intval( $_POST['id'] ) : 0;
$rating = isset( $_POST['rating'] ) ? floatval( $_POST['rating'] ) : 0;
if ( ! $post_id || get_post_type( $post_id ) !== 'anime' ) {
wp_send_json_error( '无效的番剧' );
}
if ( $rating < 0 || $rating > 5 ) {
wp_send_json_error( '评分无效' );
}
// 记录用户评分(简单实现,可扩展为复杂逻辑)
$current_rating = get_post_meta( $post_id, '_anime_rating', true );
$rating_count = get_post_meta( $post_id, '_anime_rating_count', true );
$rating_count = $rating_count ? intval( $rating_count ) + 1 : 1;
if ( $current_rating ) {
$new_rating = ( $current_rating * ( $rating_count - 1 ) + $rating ) / $rating_count;
} else {
$new_rating = $rating;
}
update_post_meta( $post_id, '_anime_rating', number_format( $new_rating, 1 ) );
update_post_meta( $post_id, '_anime_rating_count', $rating_count );
wp_send_json_success( array(
'rating' => number_format( $new_rating, 1 ),
'count' => $rating_count,
) );
}
add_action( 'wp_ajax_anime_rating', 'animewp_anime_rating' );
add_action( 'wp_ajax_nopriv_anime_rating', 'animewp_anime_rating' );
六、主题自定义器(inc/customizer.php)
<?php
/**
* 主题自定义器
*/
function animewp_customize_register( $wp_customize ) {
// 二次元配色面板
$wp_customize->add_panel( 'animewp_colors', array(
'title' => '🎨 二次元配色',
'priority' => 30,
) );
// 主色调
$wp_customize->add_setting( 'primary_color', array(
'default' => '#ff5a7d',
'sanitize_callback' => 'sanitize_hex_color',
) );
$wp_customize->add_control( new WP_Customize_Color_Control( $wp_customize, 'primary_color', array(
'label' => '主色调',
'section' => 'colors',
'settings' => 'primary_color',
) ) );
// 看板娘设置面板
$wp_customize->add_section( 'animewp_live2d', array(
'title' => '🎭 看板娘设置',
'priority' => 40,
) );
// 看板娘位置
$wp_customize->add_setting( 'live2d_position', array(
'default' => 'right',
'sanitize_callback' => 'sanitize_text_field',
) );
$wp_customize->add_control( 'live2d_position', array(
'label' => '看板娘位置',
'section' => 'animewp_live2d',
'type' => 'select',
'choices' => array(
'left' => '左侧',
'right' => '右侧',
),
) );
// 看板娘模型
$wp_customize->add_setting( 'live2d_model', array(
'default' => 'miku',
'sanitize_callback' => 'sanitize_text_field',
) );
$wp_customize->add_control( 'live2d_model', array(
'label' => '看板娘模型',
'section' => 'animewp_live2d',
'type' => 'select',
'choices' => array(
'miku' => '初音未来',
'kurumi' => '时崎狂三',
'rem' => '蕾姆',
),
) );
// 樱花特效开关
$wp_customize->add_setting( 'enable_sakura', array(
'default' => true,
'sanitize_callback' => 'wp_validate_boolean',
) );
$wp_customize->add_control( 'enable_sakura', array(
'label' => '🌸 开启樱花飘落特效',
'section' => 'animewp_live2d',
'type' => 'checkbox',
) );
}
add_action( 'customize_register', 'animewp_customize_register' );
七、部署与发布
7.1 打包主题
# 进入主题目录
cd wp-content/themes/animewp
# 清理不必要的文件
rm -rf node_modules .git
# 打包
zip -r animewp.zip . -x "*.git*" "node_modules/*" "*.log"
7.2 安装到 WordPress
- 在 WordPress 后台「外观」→「主题」→「添加」→「上传主题」
- 上传 animewp.zip 并激活
- 访问「自定义」进行个性化设置
八、总结
至此,我们完成了一个功能完整的二次元 WordPress 主题开发。这个主题包含了:
✅ 核心功能
· 番剧、角色自定义文章类型
· 季度/类型分类系统
· 评分与应援系统
· AJAX 动态交互
✅ 视觉特效
· Live2D 看板娘
· 樱花飘落特效
· 霓虹渐变卡片
· 滚动动画
✅ 现代化开发
· Tailwind CSS 原子化样式
· Alpine.js 轻量交互
· Vite 构建工具
这个主题为你提供了一个坚实的基础,你可以继续扩展:
· 添加番剧排行榜
· 集成 Bangumi 数据 API
· 开发手机端 PWA 版本
· 添加二次元表情包评论系统
希望这份教程能帮助你打造出属于自己的主题
特殊声明: 这篇文章的问题 很抱歉 在后续检测后发现有一些问题 请你在部署前看看这个
一、英文语法问题
- functions.php 中的函数名
animewp_widgets_init 函数内,侧边栏的 before_widget 参数中使用了 %2$s(用于输出CSS类名),但在后面的 register_sidebar 数组中,有两处 %2$s 前后缺少空格或使用了中文引号?实际上代码本身没问题,但注释中混用了中文引号,不过不影响运行。
- live2d.js 中的变量
live2dConfig.messages 在 inc/live2d.php 中通过 wp_localize_script 传递,但在 JS 中访问 live2dConfig.messages.morning 等属性时,如果 PHP 端没有定义 morning、evening 等键(实际已定义),会出现 undefined。这不是语法错误,但属于逻辑严谨性问题,可能导致控制台报错。
二、中文标点与格式问题
- 中文引号混用
在多处 PHP 注释和 HTML 属性中,混用了中文全角引号(如 ‘、’、“、”)和英文半角引号。例如:
'before_title' => '<h3 class="widget-title text-lg font-bold mb-4 pb-2 border-b-2 border-pink-400 text-pink-600">✨ ',
这里的 ‘ 是英文单引号,但注释里用了中文引号,虽然不影响执行,但不符合 WordPress 编码规范。
- 一处缺失分号
在 inc/post-types.php 中,注册季度分类的代码块末尾缺少一个闭合的 }?我核对了一下,实际上代码是完整的,但在文章呈现时,register_taxonomy 调用后的注释和预设数据部分,格式被压缩了,容易误以为缺少括号。实际上没有语法错误。
- Markdown 代码块语言标记
文中多处使用了
、
js 等标记,但有些地方(如 sakura.js 的 destroy() 方法后)出现了多余的代码块闭合标记,可能是复制时产生的格式问题,不影响实际代码。
三、技术细节上的小偏差
- Live2D 模型路径
在 live2d.js 中切换模型时,硬编码了路径:
jsonPath: `${window.location.origin}/wp-content/themes/animewp/assets/models/${currentModel}/model.json`,
如果主题目录名不是 animewp,或 WordPress 安装在子目录下,路径会失效。更稳妥的做法是使用 PHP 传递的 live2dConfig.modelPath 前缀。
- 樱花特效 SVG 背景
在 main.css 中,.sakura 的 background-image 使用了内联 SVG,但该 SVG 是一朵花的形状,并非标准的樱花花瓣。不过作为示例效果是可行的。











评论(0)
暂无评论