init local code

This commit is contained in:
chenxing
2026-09-07 09:09:39 +08:00
commit 99cd26b3d2
259 changed files with 20554 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
# ===== 单测环境配置(覆盖 src/main/resources/application.yml)=====
# 目标:本地 MySQL 8.0.45(JSON 列原生类型,与 schema-cms-test.sql / cms_init.sql 一致)+ 本地 Redis 6379
# schema 初始化在 JPA EntityManagerFactory 之前执行(Boot 2.7 SQL init 时序),
# 表已存在时 hibernate ddl-auto=update 不会重建/改列。
server:
port: 0
spring:
application:
name: cms
datasource:
url: jdbc:mysql://localhost:3306/bsch_cms_test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
sql:
init:
mode: always
schema-locations: classpath:schema-cms-test.sql
jpa:
hibernate:
ddl-auto: update
show-sql: false
open-in-view: false
properties:
hibernate:
jdbc:
time_zone: Asia/Shanghai
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
thymeleaf:
cache: true
prefix: classpath:/templates/
suffix: .html
encoding: UTF-8
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
cache:
type: caffeine
caffeine:
spec: maximumSize=10000,expireAfterWrite=5m
yh:
auth:
enabled: true
sso:
server-url-prefix: http://sso.yunhaixingtong.com/cas/
client-host-url: http://localhost:8072/cms
allow-headers: all
allow-origins: all
url-pattern: /api/*
mobi-api-pattern: /mobiapi/*
app-auth:
enabled: true
app-key: 100XXX
app-secret: xxxxxxxx
open:
enabled: true
base-host: http://base.yunhaixingtong.com
base-path: /userbase
app-key: 100XXX
app-secret: xxxxxxxx
cms:
# 超管判定:BoaUser.type == 1(夹具 TestUsers.superAdmin 已对齐);校级角色经 BoaRoleInfo.schId 匹配
portal:
detail-max-age: 300
# 上传落到工程 target 目录,测试后随 mvn clean 清除
FILE_PATH: ./target/test-upload/
FILE_WEB_PATH: /cms/upload/
logging:
level:
com.yunhai.cms: info
org.hibernate.SQL: warn
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
show-details: never
# 本地 Redis(单测 Redis 演进用例连接参数)
cms.test.redis:
host: localhost
port: 6379
password: 123456
+134
View File
@@ -0,0 +1,134 @@
-- ============================================================
-- bsch_cms_test 单测库表结构(每次 Spring 上下文启动先 DROP 重建,保证干净起点)
-- 以 src/main/resources/sql/cms_init.sql 为蓝本;
-- 2026-09-05 起本机为 MySQL 8.0.45,JSON 列恢复原生类型(与 cms_init.sql / 实体 columnDefinition 一致)
-- ============================================================
DROP TABLE IF EXISTS `cms_attachment`;
DROP TABLE IF EXISTS `cms_article`;
DROP TABLE IF EXISTS `cms_category`;
DROP TABLE IF EXISTS `cms_banner`;
DROP TABLE IF EXISTS `cms_link`;
DROP TABLE IF EXISTS `cms_site`;
CREATE TABLE `cms_site` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`code` VARCHAR(64) NOT NULL COMMENT '站点编码,URL 路径用(/portal/{code})',
`parent_site_id` INT NOT NULL DEFAULT 0 COMMENT '上级站点ID(0=顶层校站点;院系子站挂主站 id)',
`sch_id` INT NULL COMMENT '学校ID(逻辑外键 → UserBase sys_group.id)',
`sch_name` VARCHAR(128) NULL COMMENT '学校名称(冗余展示)',
`name` VARCHAR(128) NOT NULL COMMENT '站点名称',
`theme` VARCHAR(64) NULL DEFAULT 'default' COMMENT '皮肤包名',
`config_json` JSON NULL COMMENT '站点配置(logo/联系方式/页脚等;预留 wechat 节点)',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=停用 1=启用',
`create_by` VARCHAR(64) NULL,
`create_time` DATETIME NULL,
`update_by` VARCHAR(64) NULL,
`update_time` DATETIME NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_site_code` (`code`),
KEY `idx_site_sch` (`sch_id`),
KEY `idx_site_parent` (`parent_site_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'CMS 站点';
CREATE TABLE `cms_category` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`site_id` INT NOT NULL COMMENT '所属站点',
`parent_id` INT NOT NULL DEFAULT 0 COMMENT '父栏目ID(0=根)',
`name` VARCHAR(64) NOT NULL COMMENT '栏目名称',
`type` TINYINT NOT NULL DEFAULT 1 COMMENT '栏目类型:1=文章列表 2=单页 3=外链',
`link_url` VARCHAR(500) NULL COMMENT '外链地址(type=3 必填)',
`template` VARCHAR(64) NULL COMMENT '栏目模板覆盖',
`content` LONGTEXT NULL COMMENT '单页栏目内容',
`sort_no` INT NOT NULL DEFAULT 0 COMMENT '同级排序(升序)',
`seo_title` VARCHAR(200) NULL COMMENT 'SEO 标题',
`seo_description` VARCHAR(500) NULL COMMENT 'SEO 描述',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '0=隐藏 1=显示',
`create_by` VARCHAR(64) NULL,
`create_time` DATETIME NULL,
`update_by` VARCHAR(64) NULL,
`update_time` DATETIME NULL,
PRIMARY KEY (`id`),
KEY `idx_cat_tree` (`site_id`, `parent_id`, `sort_no`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'CMS 栏目';
CREATE TABLE `cms_article` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`site_id` INT NOT NULL COMMENT '所属站点',
`category_id` INT NOT NULL COMMENT '所属栏目',
`title` VARCHAR(200) NOT NULL COMMENT '标题',
`summary` VARCHAR(500) NULL COMMENT '摘要',
`content` LONGTEXT NULL COMMENT '正文 HTML',
`cover_image` VARCHAR(500) NULL COMMENT '封面图 URL',
`author_account` VARCHAR(64) NULL COMMENT '作者账号',
`author_name` VARCHAR(64) NULL COMMENT '作者姓名',
`source` VARCHAR(128) NULL COMMENT '来源',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0=草稿 1=已发布 2=已下架',
`topmost` TINYINT NOT NULL DEFAULT 0 COMMENT '置顶:0/1',
`publish_time` DATETIME NULL COMMENT '发布时间',
`offline_time` DATETIME NULL COMMENT '下架时间',
`view_count` INT NOT NULL DEFAULT 0 COMMENT '浏览量',
`ext_json` JSON NULL COMMENT '扩展字段(保留键:wechat=推送回执预留、albums=图集演进)',
`create_by` VARCHAR(64) NULL,
`create_time` DATETIME NULL,
`update_by` VARCHAR(64) NULL,
`update_time` DATETIME NULL,
PRIMARY KEY (`id`),
KEY `idx_art_list` (`site_id`, `category_id`, `status`, `publish_time`),
KEY `idx_art_site_top` (`site_id`, `status`, `topmost`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'CMS 文章';
CREATE TABLE `cms_attachment` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`site_id` INT NOT NULL COMMENT '所属站点',
`article_id` INT NULL COMMENT '关联文章',
`file_name` VARCHAR(255) NOT NULL COMMENT '原始文件名',
`file_path` VARCHAR(500) NOT NULL COMMENT '存储路径',
`web_url` VARCHAR(500) NOT NULL COMMENT 'Web 访问 URL',
`file_size` BIGINT NULL COMMENT '字节数',
`mime_type` VARCHAR(128) NULL COMMENT 'MIME 类型',
`create_by` VARCHAR(64) NULL,
`create_time` DATETIME NULL,
`update_by` VARCHAR(64) NULL,
`update_time` DATETIME NULL,
PRIMARY KEY (`id`),
KEY `idx_att_article` (`article_id`),
KEY `idx_att_site` (`site_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'CMS 附件';
CREATE TABLE `cms_banner` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`site_id` INT NOT NULL COMMENT '所属站点',
`position` VARCHAR(32) NOT NULL DEFAULT 'home' COMMENT '展示位标识',
`title` VARCHAR(200) NOT NULL COMMENT '标题',
`image_url` VARCHAR(500) NOT NULL COMMENT '图片 URL',
`link_type` TINYINT NOT NULL DEFAULT 1 COMMENT '跳转类型:1=文章 2=栏目 3=外链',
`link_value` VARCHAR(500) NULL COMMENT '跳转目标',
`sort_no` INT NOT NULL DEFAULT 0 COMMENT '同级排序(升序)',
`start_time` DATETIME NULL COMMENT '上线时间',
`end_time` DATETIME NULL COMMENT '下线时间',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '0=停用 1=启用',
`create_by` VARCHAR(64) NULL,
`create_time` DATETIME NULL,
`update_by` VARCHAR(64) NULL,
`update_time` DATETIME NULL,
PRIMARY KEY (`id`),
KEY `idx_banner` (`site_id`, `position`, `status`, `sort_no`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'CMS 轮播/推荐位';
CREATE TABLE `cms_link` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`site_id` INT NOT NULL COMMENT '所属站点',
`group_name` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '分组',
`name` VARCHAR(128) NOT NULL COMMENT '链接名称',
`logo_url` VARCHAR(500) NULL COMMENT 'Logo URL',
`link_url` VARCHAR(500) NOT NULL COMMENT '链接地址',
`sort_no` INT NOT NULL DEFAULT 0 COMMENT '组内排序',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '0=隐藏 1=显示',
`create_by` VARCHAR(64) NULL,
`create_time` DATETIME NULL,
`update_by` VARCHAR(64) NULL,
`update_time` DATETIME NULL,
PRIMARY KEY (`id`),
KEY `idx_link` (`site_id`, `group_name`, `sort_no`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'CMS 友情链接';
@@ -0,0 +1,324 @@
/* ============================================================
* default 皮肤包样式(方案 §8.2)
* 换肤 = 拷贝 default 目录改 CSS 变量与布局,零后端代码。
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪,D2 决策点确认)。
* ============================================================ */
:root {
--primary: #1a5cb0;
--primary-dark: #12447f;
--text: #333;
--text-light: #777;
--bg: #f5f6f8;
--card-bg: #fff;
--border: #e5e6eb;
--max-width: 1200px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
color: var(--text);
background: var(--bg);
}
a { color: inherit; text-decoration: none; }
a:hover { color: var(--primary); }
img { max-width: 100%; }
time, .item-meta { color: var(--text-light); font-size: 13px; }
.empty { color: var(--text-light); text-align: center; padding: 32px 0; }
/* ===== 头部 ===== */
.site-header { background: var(--card-bg); border-bottom: 1px solid var(--border); }
.header-inner {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.site-brand { display: flex; align-items: center; gap: 12px; }
.site-logo { height: 44px; }
.site-name { font-size: 26px; font-weight: 700; color: var(--primary); }
.site-search { display: flex; }
.site-search input {
border: 1px solid var(--border);
border-right: none;
padding: 8px 12px;
width: 220px;
outline: none;
}
.site-search button {
border: none;
background: var(--primary);
color: #fff;
padding: 8px 18px;
cursor: pointer;
}
/* ===== 专业院系下拉(院系子站切换,《院系子站方案》) ===== */
/* 悬停展开白底纵向列表,右对齐 + 超高滚动(院系数十个子站场景) */
.site-colleges { position: relative; }
.site-colleges .colleges-toggle {
display: flex;
align-items: center;
gap: 4px;
border: 1px solid var(--border);
padding: 8px 14px;
cursor: pointer;
color: var(--primary);
font-weight: 600;
white-space: nowrap;
}
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--text-light); }
.site-colleges .colleges-menu {
display: none;
position: absolute;
top: 100%;
right: 0;
min-width: 200px;
max-height: 420px;
overflow-y: auto;
margin: 0;
padding: 6px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
z-index: 110;
}
.site-colleges:hover .colleges-menu { display: block; }
.site-colleges .colleges-menu li a {
display: block;
padding: 9px 16px;
color: var(--text);
white-space: nowrap;
}
.site-colleges .colleges-menu li a:hover { background: #f2f6fc; color: var(--primary); }
.site-colleges .colleges-menu li.active a { color: var(--primary); font-weight: 600; }
.site-nav { background: var(--primary); }
.site-nav ul {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 16px;
list-style: none;
display: flex;
flex-wrap: wrap;
}
.site-nav li a {
display: block;
padding: 12px 22px;
color: #fff;
}
.site-nav li:hover a, .site-nav li.active a { background: var(--primary-dark); }
/* ===== 二级下拉导航 ===== */
/* 面板为白底纵向列表:需整体覆盖 .site-nav ul 的横向 flex 与内边距 */
.site-nav > ul > li { position: relative; }
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .85; }
.site-nav ul.sub-nav {
display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 168px;
max-width: none;
margin: 0;
padding: 6px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
z-index: 100;
}
.site-nav li:hover > .sub-nav { display: block; }
.site-nav .sub-nav li a {
padding: 9px 16px;
color: var(--text);
white-space: nowrap;
background: none;
}
.site-nav .sub-nav li a:hover { background: #f2f6fc; color: var(--primary); }
.site-nav .sub-nav li.active a { color: var(--primary); font-weight: 600; }
/* ===== 首页轮播 ===== */
.banner { position: relative; max-width: var(--max-width); margin: 16px auto; overflow: hidden; }
.banner-slides { position: relative; height: 420px; }
.banner-slide { display: none; position: absolute; inset: 0; }
.banner-slide.active { display: block; }
.banner-slide img { width: 100%; height: 100%; object-fit: cover; }
.banner-title {
position: absolute;
left: 0; right: 0; bottom: 0;
padding: 12px 20px;
color: #fff;
background: linear-gradient(transparent, rgba(0, 0, 0, .6));
}
.banner-dots {
position: absolute;
bottom: 12px; right: 20px;
display: flex; gap: 8px;
}
.banner-dots button {
width: 10px; height: 10px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, .6);
cursor: pointer;
}
.banner-dots button.active { background: #fff; }
/* ===== 通用区块 ===== */
.top-strip, .category-block {
max-width: var(--max-width);
margin: 16px auto;
padding: 20px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.section-title, .block-head h2 {
margin: 0 0 12px;
font-size: 20px;
border-left: 4px solid var(--primary);
padding-left: 10px;
}
.block-head { display: flex; align-items: baseline; justify-content: space-between; }
.more { font-size: 13px; color: var(--text-light); }
.top-strip ul, .block-list { list-style: none; margin: 0; padding: 0; }
.top-strip li, .block-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 9px 0;
border-bottom: 1px dashed var(--border);
}
.top-strip li:last-child, .block-list li:last-child { border-bottom: none; }
.top-strip li a, .block-list li a {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top-badge {
flex: none;
color: #fff;
background: var(--primary);
font-size: 12px;
padding: 1px 6px;
border-radius: 2px;
}
/* ===== 列表页 ===== */
.list-main {
max-width: var(--max-width);
margin: 16px auto;
padding: 24px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.breadcrumb { color: var(--text-light); font-size: 13px; margin-bottom: 16px; }
.breadcrumb a { margin-right: 8px; }
.breadcrumb span { margin-left: 8px; }
.page-title {
font-size: 22px;
border-left: 4px solid var(--primary);
padding-left: 10px;
margin: 0 0 18px;
}
.article-list { list-style: none; margin: 0; padding: 0; }
.article-list li { padding: 14px 0; border-bottom: 1px dashed var(--border); }
.item-title { font-size: 17px; }
.item-summary {
color: var(--text-light);
font-size: 14px;
margin: 6px 0;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-meta { display: flex; gap: 10px; align-items: center; }
/* ===== 分页 ===== */
.pagination { display: flex; gap: 6px; justify-content: center; padding: 24px 0 4px; }
.pagination a {
min-width: 34px;
text-align: center;
padding: 6px 10px;
border: 1px solid var(--border);
background: var(--card-bg);
}
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
/* ===== 详情/单页 ===== */
.detail-main {
max-width: var(--max-width);
margin: 16px auto;
padding: 32px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.article-title { margin: 8px 0 12px; font-size: 26px; text-align: center; }
.article-meta {
display: flex;
gap: 18px;
justify-content: center;
flex-wrap: wrap;
color: var(--text-light);
font-size: 13px;
padding-bottom: 18px;
border-bottom: 1px solid var(--border);
}
.article-content { line-height: 1.9; font-size: 16px; padding-top: 20px; word-break: break-word; }
.article-content img { height: auto; }
.article-content table { border-collapse: collapse; width: 100%; }
.article-content table td, .article-content table th { border: 1px solid var(--border); padding: 6px 10px; }
.article-sibling { display: flex; flex-direction: column; gap: 8px; margin-top: 28px; font-size: 14px; }
/* ===== 页脚 ===== */
.site-footer { margin-top: 32px; background: var(--primary-dark); color: rgba(255, 255, 255, .85); }
.footer-links {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.links-group { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; font-size: 13px; }
.group-name { font-weight: 600; margin-right: 4px; }
.footer-info {
max-width: var(--max-width);
margin: 0 auto;
padding: 12px 16px 20px;
text-align: center;
font-size: 13px;
}
.footer-info p { margin: 4px 0; }
/* ===== 移动端适配 ===== */
@media (max-width: 768px) {
.header-inner { flex-direction: column; align-items: stretch; }
.site-search input { flex: 1; width: auto; }
.site-colleges { align-self: flex-start; }
.site-colleges .colleges-menu { left: 0; right: auto; min-width: 100%; }
.banner-slides { height: 200px; }
.detail-main, .list-main { padding: 18px 14px; }
.article-title { font-size: 22px; }
/* 触屏无 hover:子菜单静态平铺(缩进列表),不做浮层 */
.site-nav ul.sub-nav {
display: block;
position: static;
border: none;
box-shadow: none;
background: transparent;
min-width: 0;
padding: 0 0 6px;
}
.site-nav .sub-nav li a { color: rgba(255, 255, 255, .9); padding: 8px 16px 8px 34px; }
.site-nav .sub-nav li a:hover { background: var(--primary-dark); color: #fff; }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<a th:if="${currentCategoryId != null}" th:href="${url.category(currentCategoryId)}">栏目</a>
<span>正文</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
<div class="article-meta">
<time th:if="${article.publishTime != null}"
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
<span th:if="${!#strings.isEmpty(article.authorName)}"
th:text="'作者:' + ${article.authorName}">作者</span>
<span th:if="${!#strings.isEmpty(article.source)}"
th:text="'来源:' + ${article.source}">来源</span>
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
</div>
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
其余字段一律 th:text 转义输出 -->
<div class="article-content" th:utext="${article.content}">正文</div>
</article>
<nav class="article-sibling">
<a th:if="${prevArticle != null}" class="prev"
th:href="${url.article(prevArticle.id)}"
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
<a th:if="${nextArticle != null}" class="next"
th:href="${url.article(nextArticle.id)}"
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
</nav>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共页脚:联系信息 + 友情链接(分组渲染,方案 §8.1:友链不设独立路由) -->
<footer class="site-footer" th:fragment="footer">
<div class="footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
<div class="links-group" th:each="group : ${site.linkGroups}">
<span class="group-name" th:text="${group.groupName}">分组</span>
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener"
th:text="${link.name}">链接</a>
</div>
</div>
<div class="footer-info">
<p th:if="${site.config['contact'] != null}" th:text="${site.config['contact']}">联系方式</p>
<p th:if="${site.config['copyright'] != null}" th:text="${site.config['copyright']}">版权信息</p>
<p th:if="${site.config['beian'] != null}" th:text="${site.config['beian']}">备案号</p>
</div>
</footer>
</html>
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<!-- 公共 head 片段:SEO(title/description/canonical)+ 主题资产(方案 §8.3/§8.6) -->
<head th:fragment="head">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${pageTitle}">页面标题</title>
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
th:content="${seoDescription}">
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
<link th:if="${site.config['favicon'] != null}" rel="icon" th:href="${site.config['favicon']}">
</head>
</html>
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共头部:站点名 + 栏目导航 + 站内搜索 + 专业院系下拉(院系子站切换,《院系子站方案》) -->
<header class="site-header" th:fragment="header">
<div class="header-inner">
<div class="site-brand">
<img th:if="${site.config['logo'] != null}" class="site-logo"
th:src="${site.config['logo']}" th:alt="${site.name}">
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
</div>
<form class="site-search" th:action="${url.search('', null)}" method="get">
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
<button type="submit">搜索</button>
</form>
<!-- 专业院系下拉:顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 config_json.colleges_label 覆盖 -->
<div class="site-colleges" th:if="${!site.colleges.isEmpty()}"
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
<span class="colleges-toggle">
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
<i class="colleges-arrow">▾</i>
</span>
<ul class="colleges-menu">
<li th:each="col : ${site.colleges}" th:classappend="${col.code == site.code} ? 'active'">
<a th:href="${url.siteHome(col.code, col.publicBaseUrl)}"
th:title="${col.name}" th:text="${col.name}">学院名称</a>
</li>
</ul>
</div>
</div>
<nav class="site-nav">
<ul>
<li th:classappend="${currentCategoryId == null} ? 'active'">
<a th:href="${url.home()}">首页</a>
</li>
<li th:each="cat : ${navCategories}"
th:classappend="${(currentCategoryId == cat.id ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
<!-- 外链栏目直接外跳;其余进栏目页(方案 §8.1) -->
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
th:text="${cat.name}">栏目</a>
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}" th:text="${cat.name}">栏目</a>
<!-- 二级下拉:悬停展开(样式见 assets/css/main.css .sub-nav) -->
<ul class="sub-nav" th:if="${!cat.children.isEmpty()}">
<li th:each="sub : ${cat.children}">
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
th:text="${sub.name}">子栏目</a>
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
</li>
</ul>
</li>
</ul>
</nav>
</header>
</html>
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="home-main">
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
<div class="banner-slides">
<a class="banner-slide" th:each="b, stat : ${banners}"
th:classappend="${stat.first} ? 'active'"
th:href="${b.linkUrl}" th:title="${b.title}">
<img th:src="${b.imageUrl}" th:alt="${b.title}">
<span class="banner-title" th:text="${b.title}">标题</span>
</a>
</div>
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
th:aria-label="${'第' + (stat.count) + '张'}"></button>
</div>
</section>
<!-- 置顶/头条区(cms_article topmost 优先) -->
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
<h2 class="section-title">头条推荐</h2>
<ul>
<li th:each="item : ${topArticles}">
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
</section>
<!-- 一级栏目区块(每栏目 top 8) -->
<section class="category-block" th:each="block : ${categoryBlocks}">
<div class="block-head">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
<li th:each="item : ${block.items}">
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
</section>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="list-main">
<nav class="breadcrumb" th:if="${searchMode != true}">
<a th:href="${url.home()}">首页</a>
<span th:text="${category.name}">栏目</span>
</nav>
<nav class="breadcrumb" th:if="${searchMode == true}">
<a th:href="${url.home()}">首页</a>
<span>搜索结果</span>
</nav>
<h1 class="page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
<h1 class="page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
<ul class="article-list">
<li th:each="item : ${listData.items}">
<a class="item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<p class="item-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">摘要</p>
<div class="item-meta">
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
</div>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(listData.items)}">暂无内容</p>
<!-- 分页(列表页/搜索页共用) -->
<nav class="pagination" th:if="${listData.totalPages > 1}"
th:with="cur=${listData.page}, total=${listData.totalPages}">
<a th:if="${cur > 1}"
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}">上一页</a>
<a th:each="p : ${#numbers.sequence(1, total)}"
th:classappend="${p == cur} ? 'active'"
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
th:text="${p}">1</a>
<a th:if="${cur < total}"
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}">下一页</a>
</nav>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<span th:text="${category.name}">栏目</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
<div class="article-content" th:utext="${category.content}">单页内容</div>
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
</article>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -0,0 +1,10 @@
{
"specVersion": 1,
"name": "t-bad-cssvar",
"title": "cssVar 出现在非 color 参数(夹具)",
"version": "1.0.0",
"screenshot": "assets/screenshot.png",
"params": [
{ "key": "footText", "cssVar": "--foot-text", "label": "cssVar 用在 text 类型上", "type": "text", "default": "x" }
]
}
@@ -0,0 +1,324 @@
/* ============================================================
* default 皮肤包样式(方案 §8.2)
* 换肤 = 拷贝 default 目录改 CSS 变量与布局,零后端代码。
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪,D2 决策点确认)。
* ============================================================ */
:root {
--primary: #1a5cb0;
--primary-dark: #12447f;
--text: #333;
--text-light: #777;
--bg: #f5f6f8;
--card-bg: #fff;
--border: #e5e6eb;
--max-width: 1200px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
color: var(--text);
background: var(--bg);
}
a { color: inherit; text-decoration: none; }
a:hover { color: var(--primary); }
img { max-width: 100%; }
time, .item-meta { color: var(--text-light); font-size: 13px; }
.empty { color: var(--text-light); text-align: center; padding: 32px 0; }
/* ===== 头部 ===== */
.site-header { background: var(--card-bg); border-bottom: 1px solid var(--border); }
.header-inner {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.site-brand { display: flex; align-items: center; gap: 12px; }
.site-logo { height: 44px; }
.site-name { font-size: 26px; font-weight: 700; color: var(--primary); }
.site-search { display: flex; }
.site-search input {
border: 1px solid var(--border);
border-right: none;
padding: 8px 12px;
width: 220px;
outline: none;
}
.site-search button {
border: none;
background: var(--primary);
color: #fff;
padding: 8px 18px;
cursor: pointer;
}
/* ===== 专业院系下拉(院系子站切换,《院系子站方案》) ===== */
/* 悬停展开白底纵向列表,右对齐 + 超高滚动(院系数十个子站场景) */
.site-colleges { position: relative; }
.site-colleges .colleges-toggle {
display: flex;
align-items: center;
gap: 4px;
border: 1px solid var(--border);
padding: 8px 14px;
cursor: pointer;
color: var(--primary);
font-weight: 600;
white-space: nowrap;
}
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--text-light); }
.site-colleges .colleges-menu {
display: none;
position: absolute;
top: 100%;
right: 0;
min-width: 200px;
max-height: 420px;
overflow-y: auto;
margin: 0;
padding: 6px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
z-index: 110;
}
.site-colleges:hover .colleges-menu { display: block; }
.site-colleges .colleges-menu li a {
display: block;
padding: 9px 16px;
color: var(--text);
white-space: nowrap;
}
.site-colleges .colleges-menu li a:hover { background: #f2f6fc; color: var(--primary); }
.site-colleges .colleges-menu li.active a { color: var(--primary); font-weight: 600; }
.site-nav { background: var(--primary); }
.site-nav ul {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 16px;
list-style: none;
display: flex;
flex-wrap: wrap;
}
.site-nav li a {
display: block;
padding: 12px 22px;
color: #fff;
}
.site-nav li:hover a, .site-nav li.active a { background: var(--primary-dark); }
/* ===== 二级下拉导航 ===== */
/* 面板为白底纵向列表:需整体覆盖 .site-nav ul 的横向 flex 与内边距 */
.site-nav > ul > li { position: relative; }
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .85; }
.site-nav ul.sub-nav {
display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 168px;
max-width: none;
margin: 0;
padding: 6px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
z-index: 100;
}
.site-nav li:hover > .sub-nav { display: block; }
.site-nav .sub-nav li a {
padding: 9px 16px;
color: var(--text);
white-space: nowrap;
background: none;
}
.site-nav .sub-nav li a:hover { background: #f2f6fc; color: var(--primary); }
.site-nav .sub-nav li.active a { color: var(--primary); font-weight: 600; }
/* ===== 首页轮播 ===== */
.banner { position: relative; max-width: var(--max-width); margin: 16px auto; overflow: hidden; }
.banner-slides { position: relative; height: 420px; }
.banner-slide { display: none; position: absolute; inset: 0; }
.banner-slide.active { display: block; }
.banner-slide img { width: 100%; height: 100%; object-fit: cover; }
.banner-title {
position: absolute;
left: 0; right: 0; bottom: 0;
padding: 12px 20px;
color: #fff;
background: linear-gradient(transparent, rgba(0, 0, 0, .6));
}
.banner-dots {
position: absolute;
bottom: 12px; right: 20px;
display: flex; gap: 8px;
}
.banner-dots button {
width: 10px; height: 10px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, .6);
cursor: pointer;
}
.banner-dots button.active { background: #fff; }
/* ===== 通用区块 ===== */
.top-strip, .category-block {
max-width: var(--max-width);
margin: 16px auto;
padding: 20px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.section-title, .block-head h2 {
margin: 0 0 12px;
font-size: 20px;
border-left: 4px solid var(--primary);
padding-left: 10px;
}
.block-head { display: flex; align-items: baseline; justify-content: space-between; }
.more { font-size: 13px; color: var(--text-light); }
.top-strip ul, .block-list { list-style: none; margin: 0; padding: 0; }
.top-strip li, .block-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 9px 0;
border-bottom: 1px dashed var(--border);
}
.top-strip li:last-child, .block-list li:last-child { border-bottom: none; }
.top-strip li a, .block-list li a {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top-badge {
flex: none;
color: #fff;
background: var(--primary);
font-size: 12px;
padding: 1px 6px;
border-radius: 2px;
}
/* ===== 列表页 ===== */
.list-main {
max-width: var(--max-width);
margin: 16px auto;
padding: 24px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.breadcrumb { color: var(--text-light); font-size: 13px; margin-bottom: 16px; }
.breadcrumb a { margin-right: 8px; }
.breadcrumb span { margin-left: 8px; }
.page-title {
font-size: 22px;
border-left: 4px solid var(--primary);
padding-left: 10px;
margin: 0 0 18px;
}
.article-list { list-style: none; margin: 0; padding: 0; }
.article-list li { padding: 14px 0; border-bottom: 1px dashed var(--border); }
.item-title { font-size: 17px; }
.item-summary {
color: var(--text-light);
font-size: 14px;
margin: 6px 0;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-meta { display: flex; gap: 10px; align-items: center; }
/* ===== 分页 ===== */
.pagination { display: flex; gap: 6px; justify-content: center; padding: 24px 0 4px; }
.pagination a {
min-width: 34px;
text-align: center;
padding: 6px 10px;
border: 1px solid var(--border);
background: var(--card-bg);
}
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
/* ===== 详情/单页 ===== */
.detail-main {
max-width: var(--max-width);
margin: 16px auto;
padding: 32px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.article-title { margin: 8px 0 12px; font-size: 26px; text-align: center; }
.article-meta {
display: flex;
gap: 18px;
justify-content: center;
flex-wrap: wrap;
color: var(--text-light);
font-size: 13px;
padding-bottom: 18px;
border-bottom: 1px solid var(--border);
}
.article-content { line-height: 1.9; font-size: 16px; padding-top: 20px; word-break: break-word; }
.article-content img { height: auto; }
.article-content table { border-collapse: collapse; width: 100%; }
.article-content table td, .article-content table th { border: 1px solid var(--border); padding: 6px 10px; }
.article-sibling { display: flex; flex-direction: column; gap: 8px; margin-top: 28px; font-size: 14px; }
/* ===== 页脚 ===== */
.site-footer { margin-top: 32px; background: var(--primary-dark); color: rgba(255, 255, 255, .85); }
.footer-links {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.links-group { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; font-size: 13px; }
.group-name { font-weight: 600; margin-right: 4px; }
.footer-info {
max-width: var(--max-width);
margin: 0 auto;
padding: 12px 16px 20px;
text-align: center;
font-size: 13px;
}
.footer-info p { margin: 4px 0; }
/* ===== 移动端适配 ===== */
@media (max-width: 768px) {
.header-inner { flex-direction: column; align-items: stretch; }
.site-search input { flex: 1; width: auto; }
.site-colleges { align-self: flex-start; }
.site-colleges .colleges-menu { left: 0; right: auto; min-width: 100%; }
.banner-slides { height: 200px; }
.detail-main, .list-main { padding: 18px 14px; }
.article-title { font-size: 22px; }
/* 触屏无 hover:子菜单静态平铺(缩进列表),不做浮层 */
.site-nav ul.sub-nav {
display: block;
position: static;
border: none;
box-shadow: none;
background: transparent;
min-width: 0;
padding: 0 0 6px;
}
.site-nav .sub-nav li a { color: rgba(255, 255, 255, .9); padding: 8px 16px 8px 34px; }
.site-nav .sub-nav li a:hover { background: var(--primary-dark); color: #fff; }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<a th:if="${currentCategoryId != null}" th:href="${url.category(currentCategoryId)}">栏目</a>
<span>正文</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
<div class="article-meta">
<time th:if="${article.publishTime != null}"
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
<span th:if="${!#strings.isEmpty(article.authorName)}"
th:text="'作者:' + ${article.authorName}">作者</span>
<span th:if="${!#strings.isEmpty(article.source)}"
th:text="'来源:' + ${article.source}">来源</span>
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
</div>
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
其余字段一律 th:text 转义输出 -->
<div class="article-content" th:utext="${article.content}">正文</div>
</article>
<nav class="article-sibling">
<a th:if="${prevArticle != null}" class="prev"
th:href="${url.article(prevArticle.id)}"
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
<a th:if="${nextArticle != null}" class="next"
th:href="${url.article(nextArticle.id)}"
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
</nav>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共页脚:联系信息 + 友情链接(分组渲染,方案 §8.1:友链不设独立路由) -->
<footer class="site-footer" th:fragment="footer">
<div class="footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
<div class="links-group" th:each="group : ${site.linkGroups}">
<span class="group-name" th:text="${group.groupName}">分组</span>
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener"
th:text="${link.name}">链接</a>
</div>
</div>
<div class="footer-info">
<p th:if="${site.config['contact'] != null}" th:text="${site.config['contact']}">联系方式</p>
<p th:if="${site.config['copyright'] != null}" th:text="${site.config['copyright']}">版权信息</p>
<p th:if="${site.config['beian'] != null}" th:text="${site.config['beian']}">备案号</p>
</div>
</footer>
</html>
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<!-- 公共 head 片段:SEO(title/description/canonical)+ 主题资产(方案 §8.3/§8.6) -->
<head th:fragment="head">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${pageTitle}">页面标题</title>
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
th:content="${seoDescription}">
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
<link th:if="${site.config['favicon'] != null}" rel="icon" th:href="${site.config['favicon']}">
</head>
</html>
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共头部:站点名 + 栏目导航 + 站内搜索 + 专业院系下拉(院系子站切换,《院系子站方案》) -->
<header class="site-header" th:fragment="header">
<div class="header-inner">
<div class="site-brand">
<img th:if="${site.config['logo'] != null}" class="site-logo"
th:src="${site.config['logo']}" th:alt="${site.name}">
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
</div>
<form class="site-search" th:action="${url.search('', null)}" method="get">
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
<button type="submit">搜索</button>
</form>
<!-- 专业院系下拉:顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 config_json.colleges_label 覆盖 -->
<div class="site-colleges" th:if="${!site.colleges.isEmpty()}"
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
<span class="colleges-toggle">
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
<i class="colleges-arrow">▾</i>
</span>
<ul class="colleges-menu">
<li th:each="col : ${site.colleges}" th:classappend="${col.code == site.code} ? 'active'">
<a th:href="${url.siteHome(col.code, col.publicBaseUrl)}"
th:title="${col.name}" th:text="${col.name}">学院名称</a>
</li>
</ul>
</div>
</div>
<nav class="site-nav">
<ul>
<li th:classappend="${currentCategoryId == null} ? 'active'">
<a th:href="${url.home()}">首页</a>
</li>
<li th:each="cat : ${navCategories}"
th:classappend="${(currentCategoryId == cat.id ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
<!-- 外链栏目直接外跳;其余进栏目页(方案 §8.1) -->
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
th:text="${cat.name}">栏目</a>
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}" th:text="${cat.name}">栏目</a>
<!-- 二级下拉:悬停展开(样式见 assets/css/main.css .sub-nav) -->
<ul class="sub-nav" th:if="${!cat.children.isEmpty()}">
<li th:each="sub : ${cat.children}">
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
th:text="${sub.name}">子栏目</a>
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
</li>
</ul>
</li>
</ul>
</nav>
</header>
</html>
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="home-main">
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
<div class="banner-slides">
<a class="banner-slide" th:each="b, stat : ${banners}"
th:classappend="${stat.first} ? 'active'"
th:href="${b.linkUrl}" th:title="${b.title}">
<img th:src="${b.imageUrl}" th:alt="${b.title}">
<span class="banner-title" th:text="${b.title}">标题</span>
</a>
</div>
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
th:aria-label="${'第' + (stat.count) + '张'}"></button>
</div>
</section>
<!-- 置顶/头条区(cms_article topmost 优先) -->
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
<h2 class="section-title">头条推荐</h2>
<ul>
<li th:each="item : ${topArticles}">
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
</section>
<!-- 一级栏目区块(每栏目 top 8) -->
<section class="category-block" th:each="block : ${categoryBlocks}">
<div class="block-head">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
<li th:each="item : ${block.items}">
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
</section>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="list-main">
<nav class="breadcrumb" th:if="${searchMode != true}">
<a th:href="${url.home()}">首页</a>
<span th:text="${category.name}">栏目</span>
</nav>
<nav class="breadcrumb" th:if="${searchMode == true}">
<a th:href="${url.home()}">首页</a>
<span>搜索结果</span>
</nav>
<h1 class="page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
<h1 class="page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
<ul class="article-list">
<li th:each="item : ${listData.items}">
<a class="item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<p class="item-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">摘要</p>
<div class="item-meta">
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
</div>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(listData.items)}">暂无内容</p>
<!-- 分页(列表页/搜索页共用) -->
<nav class="pagination" th:if="${listData.totalPages > 1}"
th:with="cur=${listData.page}, total=${listData.totalPages}">
<a th:if="${cur > 1}"
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}">上一页</a>
<a th:each="p : ${#numbers.sequence(1, total)}"
th:classappend="${p == cur} ? 'active'"
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
th:text="${p}">1</a>
<a th:if="${cur < total}"
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}">下一页</a>
</nav>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<span th:text="${category.name}">栏目</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
<div class="article-content" th:utext="${category.content}">单页内容</div>
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
</article>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -0,0 +1,10 @@
{
"specVersion": 1,
"name": "t-bad-key",
"title": "参数 key 非法(夹具)",
"version": "1.0.0",
"screenshot": "assets/screenshot.png",
"params": [
{ "key": "bad-key", "label": "非法 key(含短横线)", "type": "color", "default": "#123456" }
]
}
@@ -0,0 +1,324 @@
/* ============================================================
* default 皮肤包样式(方案 §8.2)
* 换肤 = 拷贝 default 目录改 CSS 变量与布局,零后端代码。
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪,D2 决策点确认)。
* ============================================================ */
:root {
--primary: #1a5cb0;
--primary-dark: #12447f;
--text: #333;
--text-light: #777;
--bg: #f5f6f8;
--card-bg: #fff;
--border: #e5e6eb;
--max-width: 1200px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
color: var(--text);
background: var(--bg);
}
a { color: inherit; text-decoration: none; }
a:hover { color: var(--primary); }
img { max-width: 100%; }
time, .item-meta { color: var(--text-light); font-size: 13px; }
.empty { color: var(--text-light); text-align: center; padding: 32px 0; }
/* ===== 头部 ===== */
.site-header { background: var(--card-bg); border-bottom: 1px solid var(--border); }
.header-inner {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.site-brand { display: flex; align-items: center; gap: 12px; }
.site-logo { height: 44px; }
.site-name { font-size: 26px; font-weight: 700; color: var(--primary); }
.site-search { display: flex; }
.site-search input {
border: 1px solid var(--border);
border-right: none;
padding: 8px 12px;
width: 220px;
outline: none;
}
.site-search button {
border: none;
background: var(--primary);
color: #fff;
padding: 8px 18px;
cursor: pointer;
}
/* ===== 专业院系下拉(院系子站切换,《院系子站方案》) ===== */
/* 悬停展开白底纵向列表,右对齐 + 超高滚动(院系数十个子站场景) */
.site-colleges { position: relative; }
.site-colleges .colleges-toggle {
display: flex;
align-items: center;
gap: 4px;
border: 1px solid var(--border);
padding: 8px 14px;
cursor: pointer;
color: var(--primary);
font-weight: 600;
white-space: nowrap;
}
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--text-light); }
.site-colleges .colleges-menu {
display: none;
position: absolute;
top: 100%;
right: 0;
min-width: 200px;
max-height: 420px;
overflow-y: auto;
margin: 0;
padding: 6px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
z-index: 110;
}
.site-colleges:hover .colleges-menu { display: block; }
.site-colleges .colleges-menu li a {
display: block;
padding: 9px 16px;
color: var(--text);
white-space: nowrap;
}
.site-colleges .colleges-menu li a:hover { background: #f2f6fc; color: var(--primary); }
.site-colleges .colleges-menu li.active a { color: var(--primary); font-weight: 600; }
.site-nav { background: var(--primary); }
.site-nav ul {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 16px;
list-style: none;
display: flex;
flex-wrap: wrap;
}
.site-nav li a {
display: block;
padding: 12px 22px;
color: #fff;
}
.site-nav li:hover a, .site-nav li.active a { background: var(--primary-dark); }
/* ===== 二级下拉导航 ===== */
/* 面板为白底纵向列表:需整体覆盖 .site-nav ul 的横向 flex 与内边距 */
.site-nav > ul > li { position: relative; }
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .85; }
.site-nav ul.sub-nav {
display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 168px;
max-width: none;
margin: 0;
padding: 6px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
z-index: 100;
}
.site-nav li:hover > .sub-nav { display: block; }
.site-nav .sub-nav li a {
padding: 9px 16px;
color: var(--text);
white-space: nowrap;
background: none;
}
.site-nav .sub-nav li a:hover { background: #f2f6fc; color: var(--primary); }
.site-nav .sub-nav li.active a { color: var(--primary); font-weight: 600; }
/* ===== 首页轮播 ===== */
.banner { position: relative; max-width: var(--max-width); margin: 16px auto; overflow: hidden; }
.banner-slides { position: relative; height: 420px; }
.banner-slide { display: none; position: absolute; inset: 0; }
.banner-slide.active { display: block; }
.banner-slide img { width: 100%; height: 100%; object-fit: cover; }
.banner-title {
position: absolute;
left: 0; right: 0; bottom: 0;
padding: 12px 20px;
color: #fff;
background: linear-gradient(transparent, rgba(0, 0, 0, .6));
}
.banner-dots {
position: absolute;
bottom: 12px; right: 20px;
display: flex; gap: 8px;
}
.banner-dots button {
width: 10px; height: 10px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, .6);
cursor: pointer;
}
.banner-dots button.active { background: #fff; }
/* ===== 通用区块 ===== */
.top-strip, .category-block {
max-width: var(--max-width);
margin: 16px auto;
padding: 20px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.section-title, .block-head h2 {
margin: 0 0 12px;
font-size: 20px;
border-left: 4px solid var(--primary);
padding-left: 10px;
}
.block-head { display: flex; align-items: baseline; justify-content: space-between; }
.more { font-size: 13px; color: var(--text-light); }
.top-strip ul, .block-list { list-style: none; margin: 0; padding: 0; }
.top-strip li, .block-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 9px 0;
border-bottom: 1px dashed var(--border);
}
.top-strip li:last-child, .block-list li:last-child { border-bottom: none; }
.top-strip li a, .block-list li a {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top-badge {
flex: none;
color: #fff;
background: var(--primary);
font-size: 12px;
padding: 1px 6px;
border-radius: 2px;
}
/* ===== 列表页 ===== */
.list-main {
max-width: var(--max-width);
margin: 16px auto;
padding: 24px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.breadcrumb { color: var(--text-light); font-size: 13px; margin-bottom: 16px; }
.breadcrumb a { margin-right: 8px; }
.breadcrumb span { margin-left: 8px; }
.page-title {
font-size: 22px;
border-left: 4px solid var(--primary);
padding-left: 10px;
margin: 0 0 18px;
}
.article-list { list-style: none; margin: 0; padding: 0; }
.article-list li { padding: 14px 0; border-bottom: 1px dashed var(--border); }
.item-title { font-size: 17px; }
.item-summary {
color: var(--text-light);
font-size: 14px;
margin: 6px 0;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-meta { display: flex; gap: 10px; align-items: center; }
/* ===== 分页 ===== */
.pagination { display: flex; gap: 6px; justify-content: center; padding: 24px 0 4px; }
.pagination a {
min-width: 34px;
text-align: center;
padding: 6px 10px;
border: 1px solid var(--border);
background: var(--card-bg);
}
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
/* ===== 详情/单页 ===== */
.detail-main {
max-width: var(--max-width);
margin: 16px auto;
padding: 32px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.article-title { margin: 8px 0 12px; font-size: 26px; text-align: center; }
.article-meta {
display: flex;
gap: 18px;
justify-content: center;
flex-wrap: wrap;
color: var(--text-light);
font-size: 13px;
padding-bottom: 18px;
border-bottom: 1px solid var(--border);
}
.article-content { line-height: 1.9; font-size: 16px; padding-top: 20px; word-break: break-word; }
.article-content img { height: auto; }
.article-content table { border-collapse: collapse; width: 100%; }
.article-content table td, .article-content table th { border: 1px solid var(--border); padding: 6px 10px; }
.article-sibling { display: flex; flex-direction: column; gap: 8px; margin-top: 28px; font-size: 14px; }
/* ===== 页脚 ===== */
.site-footer { margin-top: 32px; background: var(--primary-dark); color: rgba(255, 255, 255, .85); }
.footer-links {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.links-group { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; font-size: 13px; }
.group-name { font-weight: 600; margin-right: 4px; }
.footer-info {
max-width: var(--max-width);
margin: 0 auto;
padding: 12px 16px 20px;
text-align: center;
font-size: 13px;
}
.footer-info p { margin: 4px 0; }
/* ===== 移动端适配 ===== */
@media (max-width: 768px) {
.header-inner { flex-direction: column; align-items: stretch; }
.site-search input { flex: 1; width: auto; }
.site-colleges { align-self: flex-start; }
.site-colleges .colleges-menu { left: 0; right: auto; min-width: 100%; }
.banner-slides { height: 200px; }
.detail-main, .list-main { padding: 18px 14px; }
.article-title { font-size: 22px; }
/* 触屏无 hover:子菜单静态平铺(缩进列表),不做浮层 */
.site-nav ul.sub-nav {
display: block;
position: static;
border: none;
box-shadow: none;
background: transparent;
min-width: 0;
padding: 0 0 6px;
}
.site-nav .sub-nav li a { color: rgba(255, 255, 255, .9); padding: 8px 16px 8px 34px; }
.site-nav .sub-nav li a:hover { background: var(--primary-dark); color: #fff; }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<a th:if="${currentCategoryId != null}" th:href="${url.category(currentCategoryId)}">栏目</a>
<span>正文</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
<div class="article-meta">
<time th:if="${article.publishTime != null}"
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
<span th:if="${!#strings.isEmpty(article.authorName)}"
th:text="'作者:' + ${article.authorName}">作者</span>
<span th:if="${!#strings.isEmpty(article.source)}"
th:text="'来源:' + ${article.source}">来源</span>
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
</div>
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
其余字段一律 th:text 转义输出 -->
<div class="article-content" th:utext="${article.content}">正文</div>
</article>
<nav class="article-sibling">
<a th:if="${prevArticle != null}" class="prev"
th:href="${url.article(prevArticle.id)}"
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
<a th:if="${nextArticle != null}" class="next"
th:href="${url.article(nextArticle.id)}"
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
</nav>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共页脚:联系信息 + 友情链接(分组渲染,方案 §8.1:友链不设独立路由) -->
<footer class="site-footer" th:fragment="footer">
<div class="footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
<div class="links-group" th:each="group : ${site.linkGroups}">
<span class="group-name" th:text="${group.groupName}">分组</span>
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener"
th:text="${link.name}">链接</a>
</div>
</div>
<div class="footer-info">
<p th:if="${site.config['contact'] != null}" th:text="${site.config['contact']}">联系方式</p>
<p th:if="${site.config['copyright'] != null}" th:text="${site.config['copyright']}">版权信息</p>
<p th:if="${site.config['beian'] != null}" th:text="${site.config['beian']}">备案号</p>
</div>
</footer>
</html>
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<!-- 公共 head 片段:SEO(title/description/canonical)+ 主题资产(方案 §8.3/§8.6) -->
<head th:fragment="head">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${pageTitle}">页面标题</title>
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
th:content="${seoDescription}">
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
<link th:if="${site.config['favicon'] != null}" rel="icon" th:href="${site.config['favicon']}">
</head>
</html>
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共头部:站点名 + 栏目导航 + 站内搜索 + 专业院系下拉(院系子站切换,《院系子站方案》) -->
<header class="site-header" th:fragment="header">
<div class="header-inner">
<div class="site-brand">
<img th:if="${site.config['logo'] != null}" class="site-logo"
th:src="${site.config['logo']}" th:alt="${site.name}">
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
</div>
<form class="site-search" th:action="${url.search('', null)}" method="get">
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
<button type="submit">搜索</button>
</form>
<!-- 专业院系下拉:顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 config_json.colleges_label 覆盖 -->
<div class="site-colleges" th:if="${!site.colleges.isEmpty()}"
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
<span class="colleges-toggle">
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
<i class="colleges-arrow">▾</i>
</span>
<ul class="colleges-menu">
<li th:each="col : ${site.colleges}" th:classappend="${col.code == site.code} ? 'active'">
<a th:href="${url.siteHome(col.code, col.publicBaseUrl)}"
th:title="${col.name}" th:text="${col.name}">学院名称</a>
</li>
</ul>
</div>
</div>
<nav class="site-nav">
<ul>
<li th:classappend="${currentCategoryId == null} ? 'active'">
<a th:href="${url.home()}">首页</a>
</li>
<li th:each="cat : ${navCategories}"
th:classappend="${(currentCategoryId == cat.id ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
<!-- 外链栏目直接外跳;其余进栏目页(方案 §8.1) -->
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
th:text="${cat.name}">栏目</a>
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}" th:text="${cat.name}">栏目</a>
<!-- 二级下拉:悬停展开(样式见 assets/css/main.css .sub-nav) -->
<ul class="sub-nav" th:if="${!cat.children.isEmpty()}">
<li th:each="sub : ${cat.children}">
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
th:text="${sub.name}">子栏目</a>
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
</li>
</ul>
</li>
</ul>
</nav>
</header>
</html>
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="home-main">
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
<div class="banner-slides">
<a class="banner-slide" th:each="b, stat : ${banners}"
th:classappend="${stat.first} ? 'active'"
th:href="${b.linkUrl}" th:title="${b.title}">
<img th:src="${b.imageUrl}" th:alt="${b.title}">
<span class="banner-title" th:text="${b.title}">标题</span>
</a>
</div>
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
th:aria-label="${'第' + (stat.count) + '张'}"></button>
</div>
</section>
<!-- 置顶/头条区(cms_article topmost 优先) -->
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
<h2 class="section-title">头条推荐</h2>
<ul>
<li th:each="item : ${topArticles}">
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
</section>
<!-- 一级栏目区块(每栏目 top 8) -->
<section class="category-block" th:each="block : ${categoryBlocks}">
<div class="block-head">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
<li th:each="item : ${block.items}">
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
</section>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="list-main">
<nav class="breadcrumb" th:if="${searchMode != true}">
<a th:href="${url.home()}">首页</a>
<span th:text="${category.name}">栏目</span>
</nav>
<nav class="breadcrumb" th:if="${searchMode == true}">
<a th:href="${url.home()}">首页</a>
<span>搜索结果</span>
</nav>
<h1 class="page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
<h1 class="page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
<ul class="article-list">
<li th:each="item : ${listData.items}">
<a class="item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<p class="item-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">摘要</p>
<div class="item-meta">
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
</div>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(listData.items)}">暂无内容</p>
<!-- 分页(列表页/搜索页共用) -->
<nav class="pagination" th:if="${listData.totalPages > 1}"
th:with="cur=${listData.page}, total=${listData.totalPages}">
<a th:if="${cur > 1}"
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}">上一页</a>
<a th:each="p : ${#numbers.sequence(1, total)}"
th:classappend="${p == cur} ? 'active'"
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
th:text="${p}">1</a>
<a th:if="${cur < total}"
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}">下一页</a>
</nav>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<span th:text="${category.name}">栏目</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
<div class="article-content" th:utext="${category.content}">单页内容</div>
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
</article>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -0,0 +1,8 @@
{
"specVersion": 1,
"name": "t-bad-shot",
"title": "声明截图缺失(夹具)",
"version": "1.0.0",
"screenshot": "assets/no-such-shot.png",
"params": []
}
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="home-main">
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
<div class="banner-slides">
<a class="banner-slide" th:each="b, stat : ${banners}"
th:classappend="${stat.first} ? 'active'"
th:href="${b.linkUrl}" th:title="${b.title}">
<img th:src="${b.imageUrl}" th:alt="${b.title}">
<span class="banner-title" th:text="${b.title}">标题</span>
</a>
</div>
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
th:aria-label="${'第' + (stat.count) + '张'}"></button>
</div>
</section>
<!-- 置顶/头条区(cms_article topmost 优先) -->
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
<h2 class="section-title">头条推荐</h2>
<ul>
<li th:each="item : ${topArticles}">
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
</section>
<!-- 一级栏目区块(每栏目 top 8) -->
<section class="category-block" th:each="block : ${categoryBlocks}">
<div class="block-head">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
<li th:each="item : ${block.items}">
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
</section>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -0,0 +1 @@
{ this is not valid json ***
@@ -0,0 +1,324 @@
/* ============================================================
* default 皮肤包样式(方案 §8.2)
* 换肤 = 拷贝 default 目录改 CSS 变量与布局,零后端代码。
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪,D2 决策点确认)。
* ============================================================ */
:root {
--primary: #1a5cb0;
--primary-dark: #12447f;
--text: #333;
--text-light: #777;
--bg: #f5f6f8;
--card-bg: #fff;
--border: #e5e6eb;
--max-width: 1200px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
color: var(--text);
background: var(--bg);
}
a { color: inherit; text-decoration: none; }
a:hover { color: var(--primary); }
img { max-width: 100%; }
time, .item-meta { color: var(--text-light); font-size: 13px; }
.empty { color: var(--text-light); text-align: center; padding: 32px 0; }
/* ===== 头部 ===== */
.site-header { background: var(--card-bg); border-bottom: 1px solid var(--border); }
.header-inner {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.site-brand { display: flex; align-items: center; gap: 12px; }
.site-logo { height: 44px; }
.site-name { font-size: 26px; font-weight: 700; color: var(--primary); }
.site-search { display: flex; }
.site-search input {
border: 1px solid var(--border);
border-right: none;
padding: 8px 12px;
width: 220px;
outline: none;
}
.site-search button {
border: none;
background: var(--primary);
color: #fff;
padding: 8px 18px;
cursor: pointer;
}
/* ===== 专业院系下拉(院系子站切换,《院系子站方案》) ===== */
/* 悬停展开白底纵向列表,右对齐 + 超高滚动(院系数十个子站场景) */
.site-colleges { position: relative; }
.site-colleges .colleges-toggle {
display: flex;
align-items: center;
gap: 4px;
border: 1px solid var(--border);
padding: 8px 14px;
cursor: pointer;
color: var(--primary);
font-weight: 600;
white-space: nowrap;
}
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--text-light); }
.site-colleges .colleges-menu {
display: none;
position: absolute;
top: 100%;
right: 0;
min-width: 200px;
max-height: 420px;
overflow-y: auto;
margin: 0;
padding: 6px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
z-index: 110;
}
.site-colleges:hover .colleges-menu { display: block; }
.site-colleges .colleges-menu li a {
display: block;
padding: 9px 16px;
color: var(--text);
white-space: nowrap;
}
.site-colleges .colleges-menu li a:hover { background: #f2f6fc; color: var(--primary); }
.site-colleges .colleges-menu li.active a { color: var(--primary); font-weight: 600; }
.site-nav { background: var(--primary); }
.site-nav ul {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 16px;
list-style: none;
display: flex;
flex-wrap: wrap;
}
.site-nav li a {
display: block;
padding: 12px 22px;
color: #fff;
}
.site-nav li:hover a, .site-nav li.active a { background: var(--primary-dark); }
/* ===== 二级下拉导航 ===== */
/* 面板为白底纵向列表:需整体覆盖 .site-nav ul 的横向 flex 与内边距 */
.site-nav > ul > li { position: relative; }
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .85; }
.site-nav ul.sub-nav {
display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 168px;
max-width: none;
margin: 0;
padding: 6px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
z-index: 100;
}
.site-nav li:hover > .sub-nav { display: block; }
.site-nav .sub-nav li a {
padding: 9px 16px;
color: var(--text);
white-space: nowrap;
background: none;
}
.site-nav .sub-nav li a:hover { background: #f2f6fc; color: var(--primary); }
.site-nav .sub-nav li.active a { color: var(--primary); font-weight: 600; }
/* ===== 首页轮播 ===== */
.banner { position: relative; max-width: var(--max-width); margin: 16px auto; overflow: hidden; }
.banner-slides { position: relative; height: 420px; }
.banner-slide { display: none; position: absolute; inset: 0; }
.banner-slide.active { display: block; }
.banner-slide img { width: 100%; height: 100%; object-fit: cover; }
.banner-title {
position: absolute;
left: 0; right: 0; bottom: 0;
padding: 12px 20px;
color: #fff;
background: linear-gradient(transparent, rgba(0, 0, 0, .6));
}
.banner-dots {
position: absolute;
bottom: 12px; right: 20px;
display: flex; gap: 8px;
}
.banner-dots button {
width: 10px; height: 10px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, .6);
cursor: pointer;
}
.banner-dots button.active { background: #fff; }
/* ===== 通用区块 ===== */
.top-strip, .category-block {
max-width: var(--max-width);
margin: 16px auto;
padding: 20px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.section-title, .block-head h2 {
margin: 0 0 12px;
font-size: 20px;
border-left: 4px solid var(--primary);
padding-left: 10px;
}
.block-head { display: flex; align-items: baseline; justify-content: space-between; }
.more { font-size: 13px; color: var(--text-light); }
.top-strip ul, .block-list { list-style: none; margin: 0; padding: 0; }
.top-strip li, .block-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 9px 0;
border-bottom: 1px dashed var(--border);
}
.top-strip li:last-child, .block-list li:last-child { border-bottom: none; }
.top-strip li a, .block-list li a {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top-badge {
flex: none;
color: #fff;
background: var(--primary);
font-size: 12px;
padding: 1px 6px;
border-radius: 2px;
}
/* ===== 列表页 ===== */
.list-main {
max-width: var(--max-width);
margin: 16px auto;
padding: 24px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.breadcrumb { color: var(--text-light); font-size: 13px; margin-bottom: 16px; }
.breadcrumb a { margin-right: 8px; }
.breadcrumb span { margin-left: 8px; }
.page-title {
font-size: 22px;
border-left: 4px solid var(--primary);
padding-left: 10px;
margin: 0 0 18px;
}
.article-list { list-style: none; margin: 0; padding: 0; }
.article-list li { padding: 14px 0; border-bottom: 1px dashed var(--border); }
.item-title { font-size: 17px; }
.item-summary {
color: var(--text-light);
font-size: 14px;
margin: 6px 0;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-meta { display: flex; gap: 10px; align-items: center; }
/* ===== 分页 ===== */
.pagination { display: flex; gap: 6px; justify-content: center; padding: 24px 0 4px; }
.pagination a {
min-width: 34px;
text-align: center;
padding: 6px 10px;
border: 1px solid var(--border);
background: var(--card-bg);
}
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
/* ===== 详情/单页 ===== */
.detail-main {
max-width: var(--max-width);
margin: 16px auto;
padding: 32px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.article-title { margin: 8px 0 12px; font-size: 26px; text-align: center; }
.article-meta {
display: flex;
gap: 18px;
justify-content: center;
flex-wrap: wrap;
color: var(--text-light);
font-size: 13px;
padding-bottom: 18px;
border-bottom: 1px solid var(--border);
}
.article-content { line-height: 1.9; font-size: 16px; padding-top: 20px; word-break: break-word; }
.article-content img { height: auto; }
.article-content table { border-collapse: collapse; width: 100%; }
.article-content table td, .article-content table th { border: 1px solid var(--border); padding: 6px 10px; }
.article-sibling { display: flex; flex-direction: column; gap: 8px; margin-top: 28px; font-size: 14px; }
/* ===== 页脚 ===== */
.site-footer { margin-top: 32px; background: var(--primary-dark); color: rgba(255, 255, 255, .85); }
.footer-links {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.links-group { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; font-size: 13px; }
.group-name { font-weight: 600; margin-right: 4px; }
.footer-info {
max-width: var(--max-width);
margin: 0 auto;
padding: 12px 16px 20px;
text-align: center;
font-size: 13px;
}
.footer-info p { margin: 4px 0; }
/* ===== 移动端适配 ===== */
@media (max-width: 768px) {
.header-inner { flex-direction: column; align-items: stretch; }
.site-search input { flex: 1; width: auto; }
.site-colleges { align-self: flex-start; }
.site-colleges .colleges-menu { left: 0; right: auto; min-width: 100%; }
.banner-slides { height: 200px; }
.detail-main, .list-main { padding: 18px 14px; }
.article-title { font-size: 22px; }
/* 触屏无 hover:子菜单静态平铺(缩进列表),不做浮层 */
.site-nav ul.sub-nav {
display: block;
position: static;
border: none;
box-shadow: none;
background: transparent;
min-width: 0;
padding: 0 0 6px;
}
.site-nav .sub-nav li a { color: rgba(255, 255, 255, .9); padding: 8px 16px 8px 34px; }
.site-nav .sub-nav li a:hover { background: var(--primary-dark); color: #fff; }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<a th:if="${currentCategoryId != null}" th:href="${url.category(currentCategoryId)}">栏目</a>
<span>正文</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
<div class="article-meta">
<time th:if="${article.publishTime != null}"
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
<span th:if="${!#strings.isEmpty(article.authorName)}"
th:text="'作者:' + ${article.authorName}">作者</span>
<span th:if="${!#strings.isEmpty(article.source)}"
th:text="'来源:' + ${article.source}">来源</span>
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
</div>
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
其余字段一律 th:text 转义输出 -->
<div class="article-content" th:utext="${article.content}">正文</div>
</article>
<nav class="article-sibling">
<a th:if="${prevArticle != null}" class="prev"
th:href="${url.article(prevArticle.id)}"
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
<a th:if="${nextArticle != null}" class="next"
th:href="${url.article(nextArticle.id)}"
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
</nav>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共页脚:联系信息 + 友情链接(分组渲染,方案 §8.1:友链不设独立路由) -->
<footer class="site-footer" th:fragment="footer">
<div class="footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
<div class="links-group" th:each="group : ${site.linkGroups}">
<span class="group-name" th:text="${group.groupName}">分组</span>
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener"
th:text="${link.name}">链接</a>
</div>
</div>
<div class="footer-info">
<p th:if="${site.config['contact'] != null}" th:text="${site.config['contact']}">联系方式</p>
<p th:if="${site.config['copyright'] != null}" th:text="${site.config['copyright']}">版权信息</p>
<p th:if="${site.config['beian'] != null}" th:text="${site.config['beian']}">备案号</p>
</div>
</footer>
</html>
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<!-- 公共 head 片段:SEO(title/description/canonical)+ 主题资产(方案 §8.3/§8.6) -->
<head th:fragment="head">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${pageTitle}">页面标题</title>
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
th:content="${seoDescription}">
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
<!-- 微调参数 CSS 变量覆盖(方案 §5.3):位于皮肤 CSS 之后层叠覆盖;值由服务端预构建并校验 -->
<style th:if="${not #strings.isEmpty(themeVarsCss)}" th:utext="${themeVarsCss}"></style>
<link th:if="${site.config['favicon'] != null}" rel="icon" th:href="${site.config['favicon']}">
</head>
</html>
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共头部:站点名 + 栏目导航 + 站内搜索 + 专业院系下拉(院系子站切换,《院系子站方案》) -->
<header class="site-header" th:fragment="header">
<div class="header-inner">
<div class="site-brand">
<img th:if="${site.config['logo'] != null}" class="site-logo"
th:src="${site.config['logo']}" th:alt="${site.name}">
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
</div>
<form class="site-search" th:action="${url.search('', null)}" method="get">
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
<button type="submit">搜索</button>
</form>
<!-- 专业院系下拉:顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 config_json.colleges_label 覆盖 -->
<div class="site-colleges" th:if="${!site.colleges.isEmpty()}"
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
<span class="colleges-toggle">
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
<i class="colleges-arrow">▾</i>
</span>
<ul class="colleges-menu">
<li th:each="col : ${site.colleges}" th:classappend="${col.code == site.code} ? 'active'">
<a th:href="${url.siteHome(col.code, col.publicBaseUrl)}"
th:title="${col.name}" th:text="${col.name}">学院名称</a>
</li>
</ul>
</div>
</div>
<nav class="site-nav">
<ul>
<li th:classappend="${currentCategoryId == null} ? 'active'">
<a th:href="${url.home()}">首页</a>
</li>
<li th:each="cat : ${navCategories}"
th:classappend="${(currentCategoryId == cat.id ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
<!-- 外链栏目直接外跳;其余进栏目页(方案 §8.1) -->
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
th:text="${cat.name}">栏目</a>
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}" th:text="${cat.name}">栏目</a>
<!-- 二级下拉:悬停展开(样式见 assets/css/main.css .sub-nav) -->
<ul class="sub-nav" th:if="${!cat.children.isEmpty()}">
<li th:each="sub : ${cat.children}">
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
th:text="${sub.name}">子栏目</a>
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
</li>
</ul>
</li>
</ul>
</nav>
</header>
</html>
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="home-main">
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
<div class="banner-slides">
<a class="banner-slide" th:each="b, stat : ${banners}"
th:classappend="${stat.first} ? 'active'"
th:href="${b.linkUrl}" th:title="${b.title}">
<img th:src="${b.imageUrl}" th:alt="${b.title}">
<span class="banner-title" th:text="${b.title}">标题</span>
</a>
</div>
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
th:aria-label="${'第' + (stat.count) + '张'}"></button>
</div>
</section>
<!-- 置顶/头条区(cms_article topmost 优先) -->
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
<h2 class="section-title">头条推荐</h2>
<ul>
<li th:each="item : ${topArticles}">
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
</section>
<!-- 一级栏目区块(每栏目 top 8) -->
<section class="category-block" th:each="block : ${categoryBlocks}">
<div class="block-head">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
<li th:each="item : ${block.items}">
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
</section>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="list-main">
<nav class="breadcrumb" th:if="${searchMode != true}">
<a th:href="${url.home()}">首页</a>
<span th:text="${category.name}">栏目</span>
</nav>
<nav class="breadcrumb" th:if="${searchMode == true}">
<a th:href="${url.home()}">首页</a>
<span>搜索结果</span>
</nav>
<h1 class="page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
<h1 class="page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
<ul class="article-list">
<li th:each="item : ${listData.items}">
<a class="item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<p class="item-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">摘要</p>
<div class="item-meta">
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
</div>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(listData.items)}">暂无内容</p>
<!-- 分页(列表页/搜索页共用) -->
<nav class="pagination" th:if="${listData.totalPages > 1}"
th:with="cur=${listData.page}, total=${listData.totalPages}">
<a th:if="${cur > 1}"
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}">上一页</a>
<a th:each="p : ${#numbers.sequence(1, total)}"
th:classappend="${p == cur} ? 'active'"
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
th:text="${p}">1</a>
<a th:if="${cur < total}"
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}">下一页</a>
</nav>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<span th:text="${category.name}">栏目</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
<div class="article-content" th:utext="${category.content}">单页内容</div>
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
</article>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -0,0 +1,15 @@
{
"specVersion": 1,
"name": "t-demo-red",
"title": "测试红(夹具)",
"version": "1.0.0",
"author": "yunhai-test",
"description": "测试夹具:拷贝 default 改主色为深红(方案 §8.7),用于证明多主题机制与白名单。",
"screenshot": "assets/screenshot.png",
"bannerSize": "1920x560",
"params": [
{ "key": "primary", "cssVar": "--primary", "label": "主色", "type": "color", "default": "#b01a2e" },
{ "key": "bg", "cssVar": "--bg", "label": "页面底色", "type": "color", "default": "#f7f5f2" },
{ "key": "heroImage", "label": "头图(image 类型夹具,B-7/T-6)", "type": "image", "default": "/cms/upload/cms/t-demo-red/hero.png" }
]
}
@@ -0,0 +1,324 @@
/* ============================================================
* default 皮肤包样式(方案 §8.2)
* 换肤 = 拷贝 default 目录改 CSS 变量与布局,零后端代码。
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪,D2 决策点确认)。
* ============================================================ */
:root {
--primary: #1a5cb0;
--primary-dark: #12447f;
--text: #333;
--text-light: #777;
--bg: #f5f6f8;
--card-bg: #fff;
--border: #e5e6eb;
--max-width: 1200px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
color: var(--text);
background: var(--bg);
}
a { color: inherit; text-decoration: none; }
a:hover { color: var(--primary); }
img { max-width: 100%; }
time, .item-meta { color: var(--text-light); font-size: 13px; }
.empty { color: var(--text-light); text-align: center; padding: 32px 0; }
/* ===== 头部 ===== */
.site-header { background: var(--card-bg); border-bottom: 1px solid var(--border); }
.header-inner {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.site-brand { display: flex; align-items: center; gap: 12px; }
.site-logo { height: 44px; }
.site-name { font-size: 26px; font-weight: 700; color: var(--primary); }
.site-search { display: flex; }
.site-search input {
border: 1px solid var(--border);
border-right: none;
padding: 8px 12px;
width: 220px;
outline: none;
}
.site-search button {
border: none;
background: var(--primary);
color: #fff;
padding: 8px 18px;
cursor: pointer;
}
/* ===== 专业院系下拉(院系子站切换,《院系子站方案》) ===== */
/* 悬停展开白底纵向列表,右对齐 + 超高滚动(院系数十个子站场景) */
.site-colleges { position: relative; }
.site-colleges .colleges-toggle {
display: flex;
align-items: center;
gap: 4px;
border: 1px solid var(--border);
padding: 8px 14px;
cursor: pointer;
color: var(--primary);
font-weight: 600;
white-space: nowrap;
}
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--text-light); }
.site-colleges .colleges-menu {
display: none;
position: absolute;
top: 100%;
right: 0;
min-width: 200px;
max-height: 420px;
overflow-y: auto;
margin: 0;
padding: 6px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
z-index: 110;
}
.site-colleges:hover .colleges-menu { display: block; }
.site-colleges .colleges-menu li a {
display: block;
padding: 9px 16px;
color: var(--text);
white-space: nowrap;
}
.site-colleges .colleges-menu li a:hover { background: #f2f6fc; color: var(--primary); }
.site-colleges .colleges-menu li.active a { color: var(--primary); font-weight: 600; }
.site-nav { background: var(--primary); }
.site-nav ul {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 16px;
list-style: none;
display: flex;
flex-wrap: wrap;
}
.site-nav li a {
display: block;
padding: 12px 22px;
color: #fff;
}
.site-nav li:hover a, .site-nav li.active a { background: var(--primary-dark); }
/* ===== 二级下拉导航 ===== */
/* 面板为白底纵向列表:需整体覆盖 .site-nav ul 的横向 flex 与内边距 */
.site-nav > ul > li { position: relative; }
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .85; }
.site-nav ul.sub-nav {
display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 168px;
max-width: none;
margin: 0;
padding: 6px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
z-index: 100;
}
.site-nav li:hover > .sub-nav { display: block; }
.site-nav .sub-nav li a {
padding: 9px 16px;
color: var(--text);
white-space: nowrap;
background: none;
}
.site-nav .sub-nav li a:hover { background: #f2f6fc; color: var(--primary); }
.site-nav .sub-nav li.active a { color: var(--primary); font-weight: 600; }
/* ===== 首页轮播 ===== */
.banner { position: relative; max-width: var(--max-width); margin: 16px auto; overflow: hidden; }
.banner-slides { position: relative; height: 420px; }
.banner-slide { display: none; position: absolute; inset: 0; }
.banner-slide.active { display: block; }
.banner-slide img { width: 100%; height: 100%; object-fit: cover; }
.banner-title {
position: absolute;
left: 0; right: 0; bottom: 0;
padding: 12px 20px;
color: #fff;
background: linear-gradient(transparent, rgba(0, 0, 0, .6));
}
.banner-dots {
position: absolute;
bottom: 12px; right: 20px;
display: flex; gap: 8px;
}
.banner-dots button {
width: 10px; height: 10px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, .6);
cursor: pointer;
}
.banner-dots button.active { background: #fff; }
/* ===== 通用区块 ===== */
.top-strip, .category-block {
max-width: var(--max-width);
margin: 16px auto;
padding: 20px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.section-title, .block-head h2 {
margin: 0 0 12px;
font-size: 20px;
border-left: 4px solid var(--primary);
padding-left: 10px;
}
.block-head { display: flex; align-items: baseline; justify-content: space-between; }
.more { font-size: 13px; color: var(--text-light); }
.top-strip ul, .block-list { list-style: none; margin: 0; padding: 0; }
.top-strip li, .block-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 9px 0;
border-bottom: 1px dashed var(--border);
}
.top-strip li:last-child, .block-list li:last-child { border-bottom: none; }
.top-strip li a, .block-list li a {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top-badge {
flex: none;
color: #fff;
background: var(--primary);
font-size: 12px;
padding: 1px 6px;
border-radius: 2px;
}
/* ===== 列表页 ===== */
.list-main {
max-width: var(--max-width);
margin: 16px auto;
padding: 24px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.breadcrumb { color: var(--text-light); font-size: 13px; margin-bottom: 16px; }
.breadcrumb a { margin-right: 8px; }
.breadcrumb span { margin-left: 8px; }
.page-title {
font-size: 22px;
border-left: 4px solid var(--primary);
padding-left: 10px;
margin: 0 0 18px;
}
.article-list { list-style: none; margin: 0; padding: 0; }
.article-list li { padding: 14px 0; border-bottom: 1px dashed var(--border); }
.item-title { font-size: 17px; }
.item-summary {
color: var(--text-light);
font-size: 14px;
margin: 6px 0;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.item-meta { display: flex; gap: 10px; align-items: center; }
/* ===== 分页 ===== */
.pagination { display: flex; gap: 6px; justify-content: center; padding: 24px 0 4px; }
.pagination a {
min-width: 34px;
text-align: center;
padding: 6px 10px;
border: 1px solid var(--border);
background: var(--card-bg);
}
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
/* ===== 详情/单页 ===== */
.detail-main {
max-width: var(--max-width);
margin: 16px auto;
padding: 32px;
background: var(--card-bg);
border: 1px solid var(--border);
}
.article-title { margin: 8px 0 12px; font-size: 26px; text-align: center; }
.article-meta {
display: flex;
gap: 18px;
justify-content: center;
flex-wrap: wrap;
color: var(--text-light);
font-size: 13px;
padding-bottom: 18px;
border-bottom: 1px solid var(--border);
}
.article-content { line-height: 1.9; font-size: 16px; padding-top: 20px; word-break: break-word; }
.article-content img { height: auto; }
.article-content table { border-collapse: collapse; width: 100%; }
.article-content table td, .article-content table th { border: 1px solid var(--border); padding: 6px 10px; }
.article-sibling { display: flex; flex-direction: column; gap: 8px; margin-top: 28px; font-size: 14px; }
/* ===== 页脚 ===== */
.site-footer { margin-top: 32px; background: var(--primary-dark); color: rgba(255, 255, 255, .85); }
.footer-links {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.links-group { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; font-size: 13px; }
.group-name { font-weight: 600; margin-right: 4px; }
.footer-info {
max-width: var(--max-width);
margin: 0 auto;
padding: 12px 16px 20px;
text-align: center;
font-size: 13px;
}
.footer-info p { margin: 4px 0; }
/* ===== 移动端适配 ===== */
@media (max-width: 768px) {
.header-inner { flex-direction: column; align-items: stretch; }
.site-search input { flex: 1; width: auto; }
.site-colleges { align-self: flex-start; }
.site-colleges .colleges-menu { left: 0; right: auto; min-width: 100%; }
.banner-slides { height: 200px; }
.detail-main, .list-main { padding: 18px 14px; }
.article-title { font-size: 22px; }
/* 触屏无 hover:子菜单静态平铺(缩进列表),不做浮层 */
.site-nav ul.sub-nav {
display: block;
position: static;
border: none;
box-shadow: none;
background: transparent;
min-width: 0;
padding: 0 0 6px;
}
.site-nav .sub-nav li a { color: rgba(255, 255, 255, .9); padding: 8px 16px 8px 34px; }
.site-nav .sub-nav li a:hover { background: var(--primary-dark); color: #fff; }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<a th:if="${currentCategoryId != null}" th:href="${url.category(currentCategoryId)}">栏目</a>
<span>正文</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
<div class="article-meta">
<time th:if="${article.publishTime != null}"
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
<span th:if="${!#strings.isEmpty(article.authorName)}"
th:text="'作者:' + ${article.authorName}">作者</span>
<span th:if="${!#strings.isEmpty(article.source)}"
th:text="'来源:' + ${article.source}">来源</span>
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
</div>
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
其余字段一律 th:text 转义输出 -->
<div class="article-content" th:utext="${article.content}">正文</div>
</article>
<nav class="article-sibling">
<a th:if="${prevArticle != null}" class="prev"
th:href="${url.article(prevArticle.id)}"
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
<a th:if="${nextArticle != null}" class="next"
th:href="${url.article(nextArticle.id)}"
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
</nav>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共页脚:联系信息 + 友情链接(分组渲染,方案 §8.1:友链不设独立路由) -->
<footer class="site-footer" th:fragment="footer">
<div class="footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
<div class="links-group" th:each="group : ${site.linkGroups}">
<span class="group-name" th:text="${group.groupName}">分组</span>
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener"
th:text="${link.name}">链接</a>
</div>
</div>
<div class="footer-info">
<p th:if="${site.config['contact'] != null}" th:text="${site.config['contact']}">联系方式</p>
<p th:if="${site.config['copyright'] != null}" th:text="${site.config['copyright']}">版权信息</p>
<p th:if="${site.config['beian'] != null}" th:text="${site.config['beian']}">备案号</p>
</div>
</footer>
</html>
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<!-- 公共 head 片段:SEO(title/description/canonical)+ 主题资产(方案 §8.3/§8.6) -->
<head th:fragment="head">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:text="${pageTitle}">页面标题</title>
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
th:content="${seoDescription}">
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
<link th:if="${site.config['favicon'] != null}" rel="icon" th:href="${site.config['favicon']}">
</head>
</html>
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共头部:站点名 + 栏目导航 + 站内搜索 + 专业院系下拉(院系子站切换,《院系子站方案》) -->
<header class="site-header" th:fragment="header">
<div class="header-inner">
<div class="site-brand">
<img th:if="${site.config['logo'] != null}" class="site-logo"
th:src="${site.config['logo']}" th:alt="${site.name}">
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
</div>
<form class="site-search" th:action="${url.search('', null)}" method="get">
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
<button type="submit">搜索</button>
</form>
<!-- 专业院系下拉:顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 config_json.colleges_label 覆盖 -->
<div class="site-colleges" th:if="${!site.colleges.isEmpty()}"
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
<span class="colleges-toggle">
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
<i class="colleges-arrow">▾</i>
</span>
<ul class="colleges-menu">
<li th:each="col : ${site.colleges}" th:classappend="${col.code == site.code} ? 'active'">
<a th:href="${url.siteHome(col.code, col.publicBaseUrl)}"
th:title="${col.name}" th:text="${col.name}">学院名称</a>
</li>
</ul>
</div>
</div>
<nav class="site-nav">
<ul>
<li th:classappend="${currentCategoryId == null} ? 'active'">
<a th:href="${url.home()}">首页</a>
</li>
<li th:each="cat : ${navCategories}"
th:classappend="${(currentCategoryId == cat.id ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
<!-- 外链栏目直接外跳;其余进栏目页(方案 §8.1) -->
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
th:text="${cat.name}">栏目</a>
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}" th:text="${cat.name}">栏目</a>
<!-- 二级下拉:悬停展开(样式见 assets/css/main.css .sub-nav) -->
<ul class="sub-nav" th:if="${!cat.children.isEmpty()}">
<li th:each="sub : ${cat.children}">
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
th:text="${sub.name}">子栏目</a>
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
</li>
</ul>
</li>
</ul>
</nav>
</header>
</html>
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="home-main">
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
<div class="banner-slides">
<a class="banner-slide" th:each="b, stat : ${banners}"
th:classappend="${stat.first} ? 'active'"
th:href="${b.linkUrl}" th:title="${b.title}">
<img th:src="${b.imageUrl}" th:alt="${b.title}">
<span class="banner-title" th:text="${b.title}">标题</span>
</a>
</div>
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
th:aria-label="${'第' + (stat.count) + '张'}"></button>
</div>
</section>
<!-- 置顶/头条区(cms_article topmost 优先) -->
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
<h2 class="section-title">头条推荐</h2>
<ul>
<li th:each="item : ${topArticles}">
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
</section>
<!-- 一级栏目区块(每栏目 top 8) -->
<section class="category-block" th:each="block : ${categoryBlocks}">
<div class="block-head">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
<li th:each="item : ${block.items}">
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
</section>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="list-main">
<nav class="breadcrumb" th:if="${searchMode != true}">
<a th:href="${url.home()}">首页</a>
<span th:text="${category.name}">栏目</span>
</nav>
<nav class="breadcrumb" th:if="${searchMode == true}">
<a th:href="${url.home()}">首页</a>
<span>搜索结果</span>
</nav>
<h1 class="page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
<h1 class="page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
<ul class="article-list">
<li th:each="item : ${listData.items}">
<a class="item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<p class="item-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">摘要</p>
<div class="item-meta">
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
</div>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(listData.items)}">暂无内容</p>
<!-- 分页(列表页/搜索页共用) -->
<nav class="pagination" th:if="${listData.totalPages > 1}"
th:with="cur=${listData.page}, total=${listData.totalPages}">
<a th:if="${cur > 1}"
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}">上一页</a>
<a th:each="p : ${#numbers.sequence(1, total)}"
th:classappend="${p == cur} ? 'active'"
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
th:text="${p}">1</a>
<a th:if="${cur < total}"
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}">下一页</a>
</nav>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<span th:text="${category.name}">栏目</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
<div class="article-content" th:utext="${category.content}">单页内容</div>
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
</article>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -0,0 +1,8 @@
{
"specVersion": 1,
"name": "t-mismatch-x",
"title": "清单名与目录不一致(夹具)",
"version": "1.0.0",
"screenshot": "assets/screenshot.png",
"params": []
}
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<a th:if="${currentCategoryId != null}" th:href="${url.category(currentCategoryId)}">栏目</a>
<span>正文</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
<div class="article-meta">
<time th:if="${article.publishTime != null}"
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
<span th:if="${!#strings.isEmpty(article.authorName)}"
th:text="'作者:' + ${article.authorName}">作者</span>
<span th:if="${!#strings.isEmpty(article.source)}"
th:text="'来源:' + ${article.source}">来源</span>
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
</div>
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
其余字段一律 th:text 转义输出 -->
<div class="article-content" th:utext="${article.content}">正文</div>
</article>
<nav class="article-sibling">
<a th:if="${prevArticle != null}" class="prev"
th:href="${url.article(prevArticle.id)}"
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
<a th:if="${nextArticle != null}" class="next"
th:href="${url.article(nextArticle.id)}"
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
</nav>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="home-main">
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
<div class="banner-slides">
<a class="banner-slide" th:each="b, stat : ${banners}"
th:classappend="${stat.first} ? 'active'"
th:href="${b.linkUrl}" th:title="${b.title}">
<img th:src="${b.imageUrl}" th:alt="${b.title}">
<span class="banner-title" th:text="${b.title}">标题</span>
</a>
</div>
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
th:aria-label="${'第' + (stat.count) + '张'}"></button>
</div>
</section>
<!-- 置顶/头条区(cms_article topmost 优先) -->
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
<h2 class="section-title">头条推荐</h2>
<ul>
<li th:each="item : ${topArticles}">
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
</section>
<!-- 一级栏目区块(每栏目 top 8) -->
<section class="category-block" th:each="block : ${categoryBlocks}">
<div class="block-head">
<h2 th:text="${block.category.name}">栏目名</h2>
<a class="more" th:href="${block.moreUrl}">更多 &gt;</a>
</div>
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
<li th:each="item : ${block.items}">
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
<time th:if="${item.publishTime != null}"
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
</li>
</ul>
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
</section>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
<script th:src="${url.asset('js/main.js')}"></script>
</body>
</html>
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body>
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
<main class="detail-main">
<nav class="breadcrumb">
<a th:href="${url.home()}">首页</a>
<span th:text="${category.name}">栏目</span>
</nav>
<article class="article">
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
<div class="article-content" th:utext="${category.content}">单页内容</div>
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
</article>
</main>
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
</body>
</html>
@@ -0,0 +1,7 @@
{
"specVersion": 1,
"name": "t-missing-list",
"title": "缺列表模板(夹具)",
"version": "1.0.0",
"params": []
}