feat(theme): 皮肤包管理收敛与命名批次-兜底catch经wrapFailure先log.error记原始堆栈再包装(upload/derive/updateMeta三处,extractZip/deleteRecursively静默吞改warn/debug);内置包改显示名走.meta覆盖层(ThemeRegistry加载后覆写title/description,updateMeta放开内置:全空=恢复默认,params拒改);derive/upload支持表单命名(theme.json改写regex换ObjectMapper防误替换,upload落位前改staging失败不落位);resolveTheme回退warn补站点编码;ThemeVo.editable语义改为可编辑元数据;附内置包mint-breeze/fresh-mint2与R1学校主站编码迁移SQL;308/308全绿
build / build-push (push) Successful in 48s

This commit is contained in:
chenxing
2026-09-08 20:25:43 +08:00
parent 12a492fc55
commit a666e13f56
39 changed files with 2343 additions and 72 deletions
@@ -0,0 +1,36 @@
# fresh-mint2 皮肤包
由 blue-white 派生的清新薄荷风皮肤(**卡片封面版**,docs/10 契约 §5 工作流)。**升级随版本发布,不在站点间复制目录**。
## 风格定位
清爽、亲和,适合职业院校门户:主色薄荷青、圆角 12px、卡片纯白+柔和阴影、细线吸顶顶栏+居中 logo、列表卡片网格(一行 3 张)支持封面图、几何圆点装饰与大留白;无纹理、无通栏大图、无复杂动效。
与 `fresh-mint`(同色系基础版)的差异:列表为带封面的卡片网格(`listLayout=grid`,`decoration=dots`),header 为 3 列网格(左:专业院系 / 中:品牌 / 右:搜索);`fresh-mint` 采用 top-bar + brand-row 顶栏与纯文字列表卡片。
## 数据契约(模板可取用的 model 属性全集,契约 §3)
| 页面 | 属性 |
| --- | --- |
| 全部 | `site`(快照:config/分类树/轮播/友链组/院系子站)、`url`(PortalUrlHelper:home/category/article/upload/asset/search/siteHome)、`themeBase`(=`themes/fresh-mint2`)、`navCategories`、`pageTitle`、`seoDescription`、`canonical`、`themeParams`(微调参数,仅 theme.json 声明过的 key)、`themeVarsCss`(服务端预构建 `:root{}` 覆盖串,无参数为空) |
| home | `banners`(时间窗内 home 位)、`topArticles`(置顶+最新 6)、`categoryBlocks`(LIST 型根栏目块,各 top 8) |
| list / search | `listData`(分页 + `items[].coverImage`)、`category`(list)、`q` + `searchMode`(search) |
| detail | `article`、`prevArticle`、`nextArticle`、`currentCategoryId` |
| single | `category`(含 `content`)、`currentCategoryId` |
原则:皮肤不得假设"首页一定有轮播"、"列表一定有封面",对空集合优雅降级。
## 轮播图尺寸约定(bannerSize)
1920x560(管理端上传端按此引导裁剪)。
## 可调参数
见 `theme.json` 的 `params`:
- **color**(进 CSS 变量):`primary` 主色 / `primaryDark` 主色加深(用于品牌文字、悬停加深)/ `primarySoft` 主色浅调(装饰、点缀背景)/ `bg` 页面底色 / `border` 边框色。
- **select**:`listLayout` = `grid`(默认,卡片网格一行 3 张,支持封面)或 `list`(简约列表,封面靠左缩略);`decoration` = `dots`(默认,背景几何圆点)或 `plain`(纯净无装饰)。
换色建议:主色系三值同步调整(primary / primaryDark / primarySoft),bg 与 border、primarySoft 保持同族浅色调,避免底色与卡片对比失衡。
优先级:`custom_css` > `themeVarsCss` > `main.css`。
@@ -0,0 +1,624 @@
/* ============================================================
* fresh-mint2 皮肤包样式(blue-white 派生,docs/10 皮肤包契约 §5)
* 清新薄荷风(卡片封面版):主色 #17A2A2、浅薄荷底、白卡 12px 圆角 + 柔和阴影;
* 细线吸顶顶栏 + 居中 logo;列表为带封面的卡片网格(一行 3 张);几何圆点装饰。
* 无纹理、无通栏大图、无复杂动效。
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪)。
* ============================================================ */
:root {
--primary: #17A2A2;
--primary-dark: #0F7C7C;
--primary-soft: #E6F6F6;
--bg: #F5FBFA;
--card-bg: #FFFFFF;
--border: #DCEDEB;
--text: #1F2D2D;
--text-light: #6B8382;
--radius: 12px;
--radius-sm: 8px;
--max-width: 1200px;
--shadow-card: 0 6px 18px rgba(15, 124, 124, .06);
--shadow-hover: 0 14px 30px rgba(15, 124, 124, .13);
--shadow-pop: 0 10px 26px rgba(15, 124, 124, .12);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
font-size: 15px;
line-height: 1.7;
color: var(--text);
background-color: var(--bg);
background-image: radial-gradient(rgba(23, 162, 162, .10) 1.6px, transparent 1.6px);
background-size: 28px 28px;
background-attachment: fixed;
}
body.decor-plain { background-image: none; }
h1, h2, h3 { font-weight: 600; letter-spacing: .01em; }
a { color: inherit; text-decoration: none; }
a:hover { color: var(--primary-dark); }
img { max-width: 100%; }
time, .item-meta { color: var(--text-light); font-size: 13px; }
.empty {
color: var(--text-light);
text-align: center;
padding: 36px 0;
background: var(--card-bg);
border: 1px dashed var(--border);
border-radius: var(--radius);
}
/* ===== 头部:细线顶栏 + 居中 logo + 吸顶 ===== */
.site-header {
position: sticky;
top: 0;
z-index: 200;
background: rgba(255, 255, 255, .94);
border-bottom: 1px solid var(--border);
box-shadow: 0 2px 12px rgba(15, 124, 124, .05);
}
.header-inner {
max-width: var(--max-width);
margin: 0 auto;
padding: 12px 16px;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 16px;
}
.header-aside { display: flex; align-items: center; gap: 10px; min-width: 0; }
.header-aside-left { justify-self: start; }
.header-aside-right { justify-self: end; }
.site-brand { display: flex; align-items: center; gap: 12px; justify-self: center; }
.site-logo { height: 40px; width: auto; }
.site-name {
font-size: 22px;
font-weight: 600;
color: var(--primary-dark);
letter-spacing: .02em;
white-space: nowrap;
}
.site-search { display: flex; gap: 8px; }
.site-search input {
border: 1px solid var(--border);
background: var(--primary-soft);
border-radius: 999px;
padding: 8px 16px;
width: 190px;
font-size: 14px;
color: var(--text);
outline: none;
}
.site-search input::placeholder { color: #9bb6b5; }
.site-search input:focus { border-color: var(--primary); background: #fff; }
.site-search button {
border: none;
border-radius: 999px;
background: var(--primary);
color: #fff;
font-size: 14px;
padding: 8px 20px;
cursor: pointer;
}
.site-search button:hover { background: var(--primary-dark); color: #fff; }
/* ===== 专业院系下拉(院系子站切换,《院系子站方案》) ===== */
.site-colleges { position: relative; }
.site-colleges .colleges-toggle {
display: flex;
align-items: center;
gap: 4px;
border: 1px solid var(--border);
border-radius: 999px;
background: #fff;
padding: 7px 16px;
cursor: pointer;
color: var(--primary-dark);
font-size: 14px;
font-weight: 500;
white-space: nowrap;
}
.site-colleges .colleges-toggle:hover { border-color: var(--primary); background: var(--primary-soft); }
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--text-light); }
.site-colleges .colleges-menu {
display: none;
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 200px;
max-height: 420px;
overflow-y: auto;
margin: 0;
padding: 6px;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-pop);
z-index: 210;
}
.site-colleges:hover .colleges-menu { display: block; }
.site-colleges .colleges-menu li a {
display: block;
padding: 9px 14px;
border-radius: var(--radius-sm);
color: var(--text);
white-space: nowrap;
}
.site-colleges .colleges-menu li a:hover { background: var(--primary-soft); color: var(--primary-dark); }
.site-colleges .colleges-menu li.active a { color: var(--primary-dark); font-weight: 600; background: var(--primary-soft); }
/* ===== 主导航:细线文字导航 + 薄荷下划线激活态 ===== */
.site-nav { background: transparent; border-top: 1px solid var(--border); }
.site-nav ul {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 16px;
list-style: none;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.site-nav > ul > li { position: relative; }
.site-nav li > a {
display: block;
padding: 13px 22px;
color: var(--text);
font-weight: 500;
position: relative;
}
.site-nav > ul > li > a::before {
content: "";
position: absolute;
left: 22px;
right: 22px;
bottom: 8px;
height: 2px;
border-radius: 2px;
background: var(--primary);
transform: scaleX(0);
transition: transform .18s ease;
}
.site-nav > ul > li:hover > a::before,
.site-nav > ul > li.active > a::before { transform: scaleX(1); }
.site-nav > ul > li:hover > a,
.site-nav > ul > li.active > a { color: var(--primary-dark); }
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .7; }
/* ===== 二级下拉导航 ===== */
.site-nav ul.sub-nav {
display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 178px;
max-width: none;
margin: 0;
padding: 6px;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-pop);
z-index: 205;
}
.site-nav li:hover > .sub-nav { display: block; }
.site-nav .sub-nav li a {
padding: 9px 16px;
border-radius: var(--radius-sm);
color: var(--text);
white-space: nowrap;
background: none;
}
.site-nav .sub-nav li a::before { display: none; }
.site-nav .sub-nav li a:hover { background: var(--primary-soft); color: var(--primary-dark); }
.site-nav .sub-nav li.active a { color: var(--primary-dark); font-weight: 600; }
/* ===== 首页轮播(收窄 + 圆角,不做通栏大图) ===== */
.banner {
position: relative;
max-width: var(--max-width);
margin: 24px auto 4px;
padding: 0 16px;
}
.banner-slides {
position: relative;
height: 380px;
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow-card);
}
.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: 16px;
right: auto;
bottom: 16px;
max-width: 68%;
padding: 8px 18px;
border-radius: 999px;
background: rgba(255, 255, 255, .92);
color: var(--text);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.banner-dots {
position: absolute;
bottom: 24px;
right: 32px;
display: flex;
gap: 8px;
}
.banner-dots button {
width: 9px;
height: 9px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, .75);
cursor: pointer;
}
.banner-dots button.active { background: var(--primary); transform: scale(1.25); }
/* ===== 通用区块:白卡 + 柔和阴影 ===== */
.top-strip, .category-block {
max-width: var(--max-width);
margin: 20px auto;
padding: 22px 24px 24px;
background: var(--card-bg);
border: none;
border-radius: var(--radius);
box-shadow: var(--shadow-card);
}
.section-title, .block-head h2 {
margin: 0 0 16px;
font-size: 19px;
font-weight: 600;
display: flex;
align-items: center;
}
.section-title::before, .block-head h2::before {
content: "";
flex: none;
width: 7px;
height: 7px;
margin-right: 26px;
border-radius: 50%;
background: var(--primary);
box-shadow: 12px 0 0 rgba(23, 162, 162, .32), 24px 0 0 rgba(23, 162, 162, .14);
}
.block-head { display: flex; align-items: baseline; justify-content: space-between; }
.more {
font-size: 13px;
color: var(--primary-dark);
background: var(--primary-soft);
border-radius: 999px;
padding: 4px 14px;
flex: none;
}
/* 头条推荐:卡片网格(listLayout=list 时回落为行列表) */
.top-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.top-grid li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 18px;
background: var(--primary-soft);
border-radius: var(--radius-sm);
}
.top-grid li a {
flex: 1;
font-weight: 500;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top-grid li time { flex: none; }
.top-grid.is-list { display: block; }
.top-grid.is-list li { background: transparent; padding: 11px 0; border-bottom: 1px solid var(--border); }
.top-grid.is-list li:last-child { border-bottom: none; }
/* 栏目区块:行列表 + 圆点标记 */
.block-list { list-style: none; margin: 0; padding: 0; }
.block-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 11px 0;
border-bottom: 1px solid var(--border);
}
.block-list li:last-child { border-bottom: none; }
.block-list li a {
flex: 1;
display: flex;
align-items: center;
gap: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.block-list li a::before {
content: "";
flex: none;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--primary);
opacity: .5;
}
.top-badge {
flex: none;
color: #fff;
background: var(--primary);
font-size: 12px;
font-weight: 500;
padding: 2px 10px;
border-radius: 999px;
}
/* ===== 列表页:卡片网格(一行三张,支持封面) ===== */
.list-main {
max-width: var(--max-width);
margin: 0 auto;
padding: 24px 16px 48px;
background: transparent;
border: none;
}
.home-main { padding-bottom: 8px; }
.breadcrumb { color: var(--text-light); font-size: 13px; margin: 0 0 14px; }
.breadcrumb a { margin-right: 8px; }
.breadcrumb span { margin-left: 8px; color: var(--primary-dark); }
.page-title {
font-size: 24px;
font-weight: 600;
margin: 0 0 22px;
padding-bottom: 14px;
border-bottom: 1px solid var(--border);
}
.article-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.article-list li {
background: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow-card);
overflow: hidden;
transition: transform .2s ease, box-shadow .2s ease;
}
.article-list li:hover { transform: translateY(-4px); box-shadow: var(--shadow-hover); }
.item-card { display: flex; flex-direction: column; height: 100%; }
.item-cover {
display: block;
aspect-ratio: 16 / 9;
overflow: hidden;
background: var(--primary-soft);
}
.item-cover img { display: block; width: 100%; height: 100%; object-fit: cover; }
.item-body { display: flex; flex-direction: column; gap: 8px; padding: 16px 18px 18px; flex: 1; }
.item-title {
font-size: 16px;
font-weight: 600;
line-height: 1.5;
color: var(--text);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.item-summary {
color: var(--text-light);
font-size: 14px;
line-height: 1.7;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.item-meta { display: flex; gap: 10px; align-items: center; margin-top: auto; padding-top: 4px; }
.article-list.is-list { display: block; }
.article-list.is-list li { margin-bottom: 14px; }
.article-list.is-list .item-card { flex-direction: row; align-items: center; gap: 18px; padding: 16px 20px; }
.article-list.is-list .item-cover { width: 190px; flex: none; aspect-ratio: 16 / 10; border-radius: var(--radius-sm); }
.article-list.is-list .item-body { padding: 0; }
.article-list.is-list .item-meta { margin-top: 4px; }
/* ===== 分页 ===== */
.pagination { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; padding: 36px 0 8px; }
.pagination a {
min-width: 36px;
text-align: center;
padding: 7px 14px;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--card-bg);
color: var(--text-light);
}
.pagination a:hover { border-color: var(--primary); color: var(--primary-dark); }
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
/* ===== 详情 / 单页 ===== */
.detail-main {
max-width: 1000px;
margin: 24px auto 48px;
padding: 36px 40px 40px;
background: var(--card-bg);
border: none;
border-radius: var(--radius);
box-shadow: var(--shadow-card);
}
.article-title { margin: 8px 0 14px; font-size: 27px; font-weight: 600; text-align: center; line-height: 1.45; }
.article-meta {
display: flex;
gap: 0;
justify-content: center;
flex-wrap: wrap;
color: var(--text-light);
font-size: 13px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
}
.article-meta > * { padding: 0 12px; position: relative; }
.article-meta > * + *::before {
content: "";
position: absolute;
left: 0;
top: 50%;
width: 4px;
height: 4px;
margin-top: -2px;
border-radius: 50%;
background: var(--primary);
opacity: .5;
}
.article-content { line-height: 1.95; font-size: 16px; padding-top: 24px; word-break: break-word; color: var(--text); }
.article-content p { margin: 0 0 16px; }
.article-content h1, .article-content h2, .article-content h3 { margin: 26px 0 12px; }
.article-content img { height: auto; border-radius: var(--radius-sm); }
.article-content a { color: var(--primary-dark); text-decoration: underline; text-underline-offset: 3px; }
.article-content blockquote {
margin: 18px 0;
padding: 12px 18px;
background: var(--primary-soft);
border-left: 3px solid var(--primary);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.article-content table { border-collapse: collapse; width: 100%; }
.article-content table td, .article-content table th { border: 1px solid var(--border); padding: 8px 12px; }
.article-sibling { display: flex; flex-direction: column; gap: 10px; margin-top: 32px; font-size: 14px; }
.article-sibling a {
padding: 12px 18px;
border-radius: var(--radius-sm);
background: var(--card-bg);
border: 1px solid var(--border);
color: var(--text-light);
}
.article-sibling a:hover { border-color: var(--primary); color: var(--primary-dark); }
/* ===== 页脚(浅薄荷底) ===== */
.site-footer {
margin-top: 40px;
background: var(--primary-soft);
color: var(--text-light);
border-top: 1px solid var(--border);
}
.footer-decor {
height: 26px;
display: flex;
justify-content: center;
align-items: center;
}
.footer-decor::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--primary);
box-shadow: 14px 0 0 rgba(23, 162, 162, .32), -14px 0 0 rgba(23, 162, 162, .32);
}
.footer-links {
max-width: var(--max-width);
margin: 0 auto;
padding: 8px 16px 4px;
display: flex;
flex-direction: column;
gap: 10px;
}
.links-group { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; font-size: 13px; }
.group-name {
font-weight: 600;
color: var(--primary-dark);
margin-right: 6px;
padding-right: 16px;
position: relative;
}
.group-name::after {
content: "";
position: absolute;
right: 0;
top: 50%;
width: 4px;
height: 4px;
margin-top: -2px;
border-radius: 50%;
background: var(--primary);
opacity: .5;
}
.links-group a { color: var(--text-light); }
.links-group a:hover { color: var(--primary-dark); }
.footer-info {
max-width: var(--max-width);
margin: 0 auto;
padding: 14px 16px 26px;
text-align: center;
font-size: 13px;
}
.footer-info p { margin: 4px 0; }
.links-group a .link-logo,
.sp-links-row a .link-logo {
height: 20px;
width: auto;
max-width: 64px;
object-fit: contain;
vertical-align: middle;
margin-right: 4px;
border-radius: 4px;
}
/* ===== 移动端适配 ===== */
@media (max-width: 768px) {
.header-inner { grid-template-columns: 1fr; justify-items: center; gap: 12px; padding: 12px 14px; }
.site-brand { order: 1; justify-self: center; }
.header-aside-right { order: 2; justify-self: stretch; }
.header-aside-left { order: 3; justify-self: center; }
.site-search { flex: 1; }
.site-search input { flex: 1; width: auto; min-width: 0; }
.site-colleges .colleges-menu { left: 50%; right: auto; transform: translateX(-50%); min-width: 200px; }
.site-nav ul { flex-wrap: nowrap; overflow-x: auto; justify-content: flex-start; }
.site-nav > ul > li > a { padding: 12px 16px; white-space: nowrap; }
.site-nav > ul > li > a::before { left: 16px; right: 16px; }
.banner { margin-top: 16px; }
.banner-slides { height: 190px; }
.banner-title { font-size: 13px; padding: 6px 14px; }
.banner-dots { bottom: 14px; right: 20px; }
.top-grid { grid-template-columns: 1fr; }
.article-list { grid-template-columns: 1fr; }
.article-list.is-list .item-card { flex-direction: column; align-items: stretch; }
.article-list.is-list .item-cover { width: 100%; aspect-ratio: 16 / 9; }
.top-strip, .category-block { padding: 18px 16px 20px; margin: 16px auto; }
.detail-main { margin: 16px auto 32px; padding: 22px 18px 26px; }
.article-title { font-size: 22px; }
.article-content { font-size: 15px; }
.site-nav ul.sub-nav {
display: block;
position: static;
border: none;
border-radius: var(--radius-sm);
box-shadow: none;
background: var(--primary-soft);
min-width: 0;
padding: 6px;
}
.site-nav .sub-nav li a { color: var(--text); padding: 8px 14px; }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs><linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#5486df"/><stop offset="1" stop-color="#214b97"/>
</linearGradient></defs>
<rect width="64" height="64" fill="url(#bg)"/>
<circle cx="32" cy="25" r="10" fill="#ffffff"/>
<path d="M 16 49.3 Q 32 30.7 48 49.3 Z" fill="#ffffff" opacity="0.92"/>
</svg>

After

Width:  |  Height:  |  Size: 434 B

@@ -0,0 +1,40 @@
/* fresh-mint2 皮肤包脚本:仅首页轮播自动播放与圆点切换,无任何外部依赖 */
(function () {
var slides = document.querySelectorAll('.banner-slide');
var dots = document.querySelectorAll('.banner-dots button');
if (!slides.length) {
return;
}
var current = 0;
var timer = null;
function show(index) {
slides[current].classList.remove('active');
if (dots[current]) {
dots[current].classList.remove('active');
}
current = (index + slides.length) % slides.length;
slides[current].classList.add('active');
if (dots[current]) {
dots[current].classList.add('active');
}
}
function start() {
if (slides.length > 1) {
timer = window.setInterval(function () {
show(current + 1);
}, 5000);
}
}
dots.forEach(function (dot, index) {
dot.addEventListener('click', function () {
window.clearInterval(timer);
show(index);
start();
});
});
start();
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 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 th:classappend="${themeParams['decoration'] == 'plain'} ? 'decor-plain'">
<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,23 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共页脚:浅薄荷底 + 圆点装饰 + 友情链接(分组渲染,方案 §8.1:友链不设独立路由) -->
<footer class="site-footer" th:fragment="footer">
<!-- 纯装饰几何圆点(无数据绑定,aria-hidden) -->
<div class="footer-decor" aria-hidden="true"></div>
<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">
<img th:if="${link.logoUrl != null and !#strings.isEmpty(link.logoUrl)}" class="link-logo"
th:src="${url.upload(link.logoUrl)}" th:alt="${link.name}" loading="lazy">
<span th:text="${link.name}">链接</span>
</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,25 @@
<!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 name="theme-color" content="#17A2A2">
<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}">
<!-- og 标签基本三件(C-7,方案 §8.6 canonical/og 同基准) -->
<meta property="og:title" th:if="${pageTitle != null}" th:content="${pageTitle}">
<meta property="og:description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}" th:content="${seoDescription}">
<meta property="og:image" th:if="${ogImage != null}" th:content="${ogImage}">
<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>
<!-- 站点级自定义样式(B3):位于皮肤 CSS 与微调变量之后层叠覆盖;内容经服务端黑名单强校验后直出 -->
<style th:if="${not #strings.isEmpty(customCss)}" th:utext="${customCss}"></style>
<!-- favicon:管理端配置了 favicon 键用上传图,未配置输出主题内置兜底 icon(assets/favicon.png) -->
<link rel="icon" type="image/png"
th:href="${site.config['favicon'] != null ? url.upload(site.config['favicon']) : url.asset('favicon.png')}">
</head>
</html>
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共头部:细线顶栏 + 居中品牌行(左:专业院系 / 中:品牌 / 右:站内搜索)+ 栏目导航(《院系子站方案》) -->
<header class="site-header" th:fragment="header">
<div class="header-inner">
<!-- 左:专业院系下拉;顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 config_json.colleges_label 覆盖 -->
<div class="header-aside header-aside-left">
<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.path == site.path} ? 'active'">
<a th:href="${url.siteHome(col.path, col.publicBaseUrl)}"
th:title="${col.name}" th:text="${col.name}">学院名称</a>
</li>
</ul>
</div>
</div>
<!-- 中:居中品牌(logo + 站名) -->
<div class="site-brand">
<img th:if="${site.config['logo'] != null}" class="site-logo"
th:src="${url.upload(site.config['logo'])}" th:alt="${site.name}">
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
</div>
<!-- 右:站内搜索 -->
<div class="header-aside header-aside-right">
<form class="site-search" th:action="${url.search('', null)}" method="get">
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
<button type="submit">搜索</button>
</form>
</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 th:classappend="${themeParams['decoration'] == 'plain'} ? 'decor-plain'">
<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 class="top-grid" th:classappend="${themeParams['listLayout'] == 'list'} ? 'is-list'">
<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,58 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
<body th:classappend="${themeParams['decoration'] == 'plain'} ? 'decor-plain'">
<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" th:classappend="${themeParams['listLayout'] == 'list'} ? 'is-list'">
<li th:each="item : ${listData.items}">
<a class="item-card" th:href="${url.article(item.id)}">
<!-- 封面(契约 §3.3 listData.items[].coverImage;上传图走 url.upload) -->
<span class="item-cover" th:if="${item.coverImage != null and !#strings.isEmpty(item.coverImage)}">
<img th:src="${url.upload(item.coverImage)}" th:alt="${item.title}" loading="lazy">
</span>
<span class="item-body">
<span class="item-title" th:text="${item.title}">文章标题</span>
<span class="item-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">摘要</span>
<span 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>
</span>
</span>
</a>
</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 th:classappend="${themeParams['decoration'] == 'plain'} ? 'decor-plain'">
<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,21 @@
{
"specVersion": 1,
"name": "fresh-mint2",
"title": "清新薄荷·II",
"version": "1.0.0",
"author": "yunhai",
"description": "清新薄荷风(卡片封面版):主色 #17A2A2、浅薄荷底、白卡 12px 圆角;细线吸顶顶栏 + 居中 logo;列表为带封面的卡片网格(一行 3 张);几何圆点装饰与大留白;适合职业院校。",
"screenshot": "assets/screenshot.png",
"bannerSize": "1920x560",
"params": [
{ "key": "primary", "cssVar": "--primary", "label": "主色(薄荷绿)", "type": "color", "default": "#17A2A2" },
{ "key": "primaryDark", "cssVar": "--primary-dark", "label": "主色加深", "type": "color", "default": "#0F7C7C" },
{ "key": "primarySoft", "cssVar": "--primary-soft", "label": "主色浅调(装饰)", "type": "color", "default": "#E6F6F6" },
{ "key": "bg", "cssVar": "--bg", "label": "页面底色", "type": "color", "default": "#F5FBFA" },
{ "key": "border", "cssVar": "--border", "label": "边框色", "type": "color", "default": "#DCEDEB" },
{ "key": "listLayout", "label": "列表形态", "type": "select", "default": "grid",
"options": [ { "value": "grid", "label": "卡片网格(一行三张)" }, { "value": "list", "label": "简约列表" } ] },
{ "key": "decoration", "label": "背景装饰", "type": "select", "default": "dots",
"options": [ { "value": "dots", "label": "几何圆点" }, { "value": "plain", "label": "纯净无装饰" } ] }
]
}
@@ -0,0 +1,36 @@
# mint-breeze 皮肤包
由 blue-white 派生的清新薄荷风皮肤(docs/10 契约 §5 工作流)。**升级随版本发布,不在站点间复制目录**。
> 注:本包与 fresh-mint 为同源并存的两个车道产物(并发会话分道,2026-09-08),互不依赖;站点二选一即可。
## 风格定位
清爽、亲和,适合职业院校门户:薄荷青品牌色、圆角 12px、卡片纯白+柔和阴影、细线顶栏+居中 logo(桌面吸顶,移动端取消)、列表卡片网格(一行 3 张)、几何圆点装饰与大留白;无纹理、无通栏大图、无复杂动效。
## 数据契约(模板可取用的 model 属性全集,契约 §3)
| 页面 | 属性 |
| --- | --- |
| 全部 | `site`(快照:config/分类树/轮播/友链组/院系子站)、`url`(PortalUrlHelper:home/category/article/asset/search/siteHome)、`themeBase`(=`themes/mint-breeze`)、`navCategories`、`pageTitle`、`seoDescription`、`canonical`、`themeParams`(微调参数,仅 theme.json 声明过的 key)、`themeVarsCss`(服务端预构建 `:root{}` 覆盖串,无参数为空) |
| home | `banners`(时间窗内 home 位)、`topArticles`(置顶+最新 6)、`categoryBlocks`(LIST 型根栏目块,各 top 8) |
| list / search | `listData`(分页)、`category`(list)、`q` + `searchMode`(search) |
| detail | `article`、`prevArticle`、`nextArticle`、`currentCategoryId` |
| single | `category`(含 `content`)、`currentCategoryId` |
原则:皮肤不得假设"首页一定有轮播",对空集合优雅降级。
## 轮播图尺寸约定(bannerSize)
1920x560(管理端上传端按此引导裁剪,D2 决策点)。
## 可调参数
见 `theme.json` 的 `params`:
- **color**(进 CSS 变量):`primary` 主色 / `primaryDark` 主色加深(即强调色,用于品牌文字与悬停加深)/ `primaryLight` 主色提亮 / `bg` 页面底色 / `cardBg` 卡片底色 / `border` 边框色 / `text` 正文文字 / `textLight` 次要文字。
- **select**:`listLayout` = `grid`(默认,卡片网格一行 3 张)或 `row`(传统行列表)。
换色建议:主色系三值同步调整(primary / primaryDark / primaryLight),bg 与 border、primaryLight 保持同族浅色调,避免底色与卡片对比失衡。
优先级:`custom_css` > `themeVarsCss` > `main.css`。
@@ -0,0 +1,590 @@
/* ============================================================
* mint-breeze 皮肤包样式(blue-white 派生,docs/10 皮肤包契约 §5)
* 清新薄荷风:薄荷青品牌色 + 圆角 12px + 柔和阴影 + 卡片网格列表
* + 几何圆点装饰与大留白;无纹理、无通栏大图、无复杂动效。
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪)。
* ============================================================ */
:root {
--primary: #17A2A2;
--primary-dark: #0F7C7C;
--primary-light: #E6F6F6;
--text: #2F3E3C;
--text-light: #7D8F8C;
--bg: #F5FBFA;
--card-bg: #FFFFFF;
--border: #DDF0EE;
--radius: 12px;
--radius-sm: 8px;
--radius-pill: 999px;
--shadow-soft: 0 6px 18px rgba(15, 124, 124, .08);
--shadow-hover: 0 12px 28px rgba(15, 124, 124, .14);
--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; transition: color .15s ease; }
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 {
position: sticky;
top: 0;
z-index: 90;
background: var(--card-bg);
box-shadow: var(--shadow-soft);
}
/* 细线顶栏:院系切换在左、站内搜索在右;院系未配置时搜索仍靠右 */
.top-bar {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 16px;
padding: 7px max(16px, calc((100% - var(--max-width)) / 2 + 16px));
border-bottom: 1px solid var(--border);
font-size: 13px;
}
.top-bar .site-colleges { margin-right: auto; }
/* 居中品牌行:两侧几何圆点装饰(纯 CSS,无纹理) */
.brand-row {
position: relative;
display: flex;
justify-content: center;
padding: 18px 16px 14px;
}
.brand-row::before, .brand-row::after {
content: "";
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 46px;
height: 10px;
opacity: .45;
background-image:
radial-gradient(circle, var(--primary) 2.2px, transparent 2.8px),
radial-gradient(circle, var(--primary) 1.6px, transparent 2.2px),
radial-gradient(circle, var(--primary) 1.1px, transparent 1.7px);
background-repeat: no-repeat;
background-position: left center, center center, right center;
}
.brand-row::before { left: 6%; }
.brand-row::after { right: 6%; }
.site-brand { display: flex; align-items: center; gap: 14px; }
.site-logo { height: 52px; }
.site-name {
font-size: 28px;
font-weight: 600;
letter-spacing: .02em;
color: var(--primary-dark);
}
.site-name:hover { color: var(--primary); }
.site-search { display: flex; align-items: center; }
.site-search input {
border: 1px solid var(--border);
border-right: none;
border-radius: var(--radius-pill) 0 0 var(--radius-pill);
padding: 7px 16px;
width: 220px;
outline: none;
background: var(--card-bg);
color: var(--text);
transition: border-color .15s ease, box-shadow .15s ease;
}
.site-search input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
}
.site-search button {
border: none;
background: var(--primary);
color: #fff;
padding: 7px 20px;
border-radius: 0 var(--radius-pill) var(--radius-pill) 0;
cursor: pointer;
transition: background .15s ease;
}
.site-search button:hover { background: var(--primary-dark); }
/* ===== 专业院系下拉(院系子站切换):胶囊开关 + 圆角白卡面板 ===== */
.site-colleges { position: relative; }
.site-colleges .colleges-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid var(--border);
border-radius: var(--radius-pill);
padding: 5px 14px;
cursor: pointer;
color: var(--primary-dark);
font-weight: 600;
white-space: nowrap;
transition: border-color .15s ease, background .15s ease;
}
.site-colleges:hover .colleges-toggle {
border-color: var(--primary);
background: var(--primary-light);
}
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--primary); }
.site-colleges .colleges-menu {
display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 200px;
max-height: 420px;
overflow-y: auto;
margin: 0;
padding: 8px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-hover);
z-index: 110;
}
.site-colleges:hover .colleges-menu { display: block; }
.site-colleges .colleges-menu li a {
display: block;
margin: 0 8px;
padding: 9px 12px;
border-radius: var(--radius-sm);
color: var(--text);
white-space: nowrap;
}
.site-colleges .colleges-menu li a:hover { background: var(--primary-light); color: var(--primary-dark); }
.site-colleges .colleges-menu li.active a { color: var(--primary); font-weight: 600; }
/* ===== 主导航:白底 + 柔和文字层级,当前项圆点标记 ===== */
.site-nav { background: var(--card-bg); border-top: 1px solid var(--border); }
.site-nav ul {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 16px;
list-style: none;
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.site-nav li a {
position: relative;
display: block;
padding: 13px 20px;
color: var(--text);
font-weight: 500;
}
.site-nav li a:hover { color: var(--primary); }
.site-nav li.active a { color: var(--primary); font-weight: 600; }
/* 几何圆点:当前栏目下方的小圆点 */
.site-nav li.active > a::before {
content: "";
position: absolute;
left: 50%;
bottom: 5px;
width: 5px;
height: 5px;
transform: translateX(-50%);
border-radius: 50%;
background: var(--primary);
}
/* ===== 二级下拉导航:圆角白卡面板 ===== */
.site-nav > ul > li { position: relative; }
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .7; }
.site-nav ul.sub-nav {
display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 168px;
max-width: none;
margin: 0;
padding: 8px 0;
list-style: none;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-hover);
z-index: 100;
}
.site-nav li:hover > .sub-nav { display: block; }
.site-nav .sub-nav li a {
margin: 0 8px;
padding: 9px 12px;
border-radius: var(--radius-sm);
color: var(--text);
font-weight: 400;
white-space: nowrap;
background: none;
}
.site-nav .sub-nav li a:hover { background: var(--primary-light); color: var(--primary-dark); }
.site-nav .sub-nav li.active a { color: var(--primary); font-weight: 600; }
/* ===== 首页轮播:容器内圆角卡片(非通栏),几何圆点指示器 ===== */
.banner {
position: relative;
width: min(var(--max-width), calc(100% - 32px));
margin: 24px auto 0;
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow-soft);
}
.banner-slides { position: relative; height: 400px; }
.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: 14px 20px;
color: #fff;
font-weight: 500;
background: linear-gradient(transparent, rgba(20, 63, 61, .62));
}
.banner-dots {
position: absolute;
bottom: 14px; right: 20px;
display: flex; gap: 8px;
}
.banner-dots button {
width: 9px; height: 9px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, .55);
cursor: pointer;
transition: background .15s ease, transform .15s ease;
}
.banner-dots button.active { background: #fff; transform: scale(1.25); }
/* ===== 首页区块:卡片网格(头条通栏 + 栏目块一行 3 张) ===== */
.home-main {
max-width: var(--max-width);
margin: 0 auto;
padding: 28px 16px 56px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 24px;
}
.top-strip, .category-block {
background: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow-soft);
padding: 24px;
}
.top-strip { grid-column: 1 / -1; }
/* 栏目标题:几何圆点标记(环形点),标题中等粗细、层级柔和 */
.section-title, .block-head h2 {
margin: 0 0 14px;
font-size: 19px;
font-weight: 600;
letter-spacing: .02em;
display: flex;
align-items: center;
gap: 10px;
}
.section-title::before, .block-head h2::before {
content: "";
flex: none;
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--primary);
box-shadow: 0 0 0 3.5px var(--primary-light);
}
.block-head { display: flex; align-items: center; justify-content: space-between; }
.more {
flex: none;
font-size: 13px;
color: var(--text-light);
padding: 4px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-pill);
transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.more:hover {
color: var(--primary-dark);
border-color: var(--primary);
background: var(--primary-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: 10px 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: var(--primary-dark);
background: var(--primary-light);
font-size: 12px;
font-weight: 500;
padding: 2px 10px;
border-radius: var(--radius-pill);
}
/* ===== 列表页:卡片网格(默认一行 3 张;listLayout=row 切换行列表) ===== */
.list-main {
max-width: var(--max-width);
margin: 0 auto;
padding: 28px 16px 56px;
}
.breadcrumb { color: var(--text-light); font-size: 13px; margin-bottom: 14px; }
.breadcrumb a { margin-right: 8px; }
.breadcrumb span { margin-left: 8px; }
.page-title {
font-size: 24px;
font-weight: 600;
letter-spacing: .02em;
margin: 0 0 22px;
display: flex;
align-items: center;
gap: 10px;
}
.page-title::before {
content: "";
flex: none;
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--primary);
box-shadow: 0 0 0 3.5px var(--primary-light);
}
.article-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 20px;
}
.article-list li {
background: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow-soft);
padding: 22px 20px;
display: flex;
flex-direction: column;
gap: 10px;
transition: transform .18s ease, box-shadow .18s ease;
}
.article-list li:hover { transform: translateY(-3px); box-shadow: var(--shadow-hover); }
.item-title {
font-size: 16px;
font-weight: 600;
line-height: 1.55;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.article-list li:hover .item-title { color: var(--primary); }
.item-summary {
color: var(--text-light);
font-size: 13.5px;
line-height: 1.7;
margin: 0;
flex: 1;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.item-meta {
display: flex;
gap: 10px;
align-items: center;
margin-top: auto;
padding-top: 10px;
border-top: 1px dashed var(--border);
}
/* 布局变体:传统行列表(themeParams listLayout=row,见 list.html classappend) */
.list-rows .article-list { display: block; }
.list-rows .article-list li { display: block; margin-bottom: 16px; }
.list-rows .item-summary { -webkit-line-clamp: 2; flex: none; }
.list-rows .item-meta { border-top: none; padding-top: 0; margin-top: 6px; }
/* ===== 分页:胶囊页码 ===== */
.pagination {
display: flex;
gap: 8px;
justify-content: center;
flex-wrap: wrap;
padding: 28px 0 4px;
}
.pagination a {
min-width: 36px;
text-align: center;
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-pill);
background: var(--card-bg);
font-size: 14px;
transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.pagination a:hover { color: var(--primary-dark); border-color: var(--primary); }
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
/* ===== 详情/单页:整卡承载,正文舒适行距 ===== */
.detail-main {
max-width: var(--max-width);
margin: 0 auto;
padding: 28px 16px 56px;
}
.detail-main .article {
background: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow-soft);
padding: 36px 40px;
}
.article-title {
margin: 4px 0 14px;
font-size: 26px;
font-weight: 600;
letter-spacing: .02em;
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: 22px; word-break: break-word; }
.article-content img { height: auto; border-radius: var(--radius-sm); }
.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-wrap: wrap; gap: 12px; margin-top: 20px; font-size: 14px; }
.article-sibling a {
flex: 1 1 320px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow-soft);
padding: 14px 18px;
transition: transform .18s ease, box-shadow .18s ease, color .15s ease;
}
.article-sibling a:hover {
color: var(--primary-dark);
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
}
/* ===== 页脚:浅薄荷底 + 圆点分隔条(三态渲染逻辑不动) ===== */
.site-footer {
margin-top: 40px;
background: var(--primary-light);
color: var(--text);
border-top: 1px solid var(--border);
}
.site-footer::before {
content: "";
display: block;
height: 8px;
width: min(var(--max-width), calc(100% - 32px));
margin: 18px auto 0;
background-image: radial-gradient(circle, var(--primary) 1.6px, transparent 2.2px);
background-size: 16px 8px;
background-repeat: repeat-x;
opacity: .4;
}
.footer-links {
max-width: var(--max-width);
margin: 0 auto;
padding: 18px 16px 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.links-group { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; font-size: 13px; }
.group-name { font-weight: 600; margin-right: 4px; color: var(--primary-dark); }
.links-group a {
padding: 3px 10px;
border-radius: var(--radius-pill);
transition: background .15s ease, color .15s ease;
}
.links-group a:hover { background: rgba(255, 255, 255, .75); color: var(--primary-dark); }
.footer-info {
max-width: var(--max-width);
margin: 0 auto;
padding: 14px 16px 24px;
text-align: center;
font-size: 13px;
color: var(--text-light);
}
.footer-info p { margin: 4px 0; }
/* B4 友链 Logo:与名称并排小图(高度 ~20px,垂直居中;过宽截断不撑破页脚) */
.links-group a .link-logo {
height: 20px;
width: auto;
max-width: 64px;
object-fit: contain;
vertical-align: middle;
margin-right: 4px;
border-radius: 4px;
}
/* ===== 平板(≤992px):网格降为两列,隐藏品牌行圆点装饰 ===== */
@media (max-width: 992px) {
.home-main { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.article-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.brand-row::before, .brand-row::after { display: none; }
}
/* ===== 移动端(≤768px):取消吸顶,网格单列,导航横向滑动 ===== */
@media (max-width: 768px) {
.site-header { position: static; }
.top-bar { padding: 8px 14px; justify-content: flex-start; flex-wrap: wrap; }
.top-bar .site-colleges { margin-right: 0; }
.site-search { flex: 1; }
.site-search input { flex: 1; width: auto; min-width: 0; }
.brand-row { padding: 14px 16px 10px; }
.site-logo { height: 40px; }
.site-name { font-size: 22px; }
.site-nav ul {
justify-content: flex-start;
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.banner-slides { height: 200px; }
.home-main { grid-template-columns: 1fr; padding: 18px 14px 40px; }
.article-list { grid-template-columns: 1fr; }
.detail-main .article { padding: 22px 16px; }
.article-title { font-size: 21px; }
/* 触屏无 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 { margin: 0; padding: 8px 16px 8px 34px; border-radius: 0; color: var(--text-light); }
.site-nav .sub-nav li a:hover { background: var(--primary-light); color: var(--primary-dark); }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs><linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#5486df"/><stop offset="1" stop-color="#214b97"/>
</linearGradient></defs>
<rect width="64" height="64" fill="url(#bg)"/>
<circle cx="32" cy="25" r="10" fill="#ffffff"/>
<path d="M 16 49.3 Q 32 30.7 48 49.3 Z" fill="#ffffff" opacity="0.92"/>
</svg>

After

Width:  |  Height:  |  Size: 434 B

@@ -0,0 +1,40 @@
/* mint-breeze 皮肤包脚本:仅首页轮播自动播放与圆点切换,无任何外部依赖 */
(function () {
var slides = document.querySelectorAll('.banner-slide');
var dots = document.querySelectorAll('.banner-dots button');
if (!slides.length) {
return;
}
var current = 0;
var timer = null;
function show(index) {
slides[current].classList.remove('active');
if (dots[current]) {
dots[current].classList.remove('active');
}
current = (index + slides.length) % slides.length;
slides[current].classList.add('active');
if (dots[current]) {
dots[current].classList.add('active');
}
}
function start() {
if (slides.length > 1) {
timer = window.setInterval(function () {
show(current + 1);
}, 5000);
}
}
dots.forEach(function (dot, index) {
dot.addEventListener('click', function () {
window.clearInterval(timer);
show(index);
start();
});
});
start();
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 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,21 @@
<!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">
<img th:if="${link.logoUrl != null and !#strings.isEmpty(link.logoUrl)}" class="link-logo"
th:src="${url.upload(link.logoUrl)}" th:alt="${link.name}" loading="lazy">
<span th:text="${link.name}">链接</span>
</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,24 @@
<!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}">
<!-- og 标签基本三件(C-7,方案 §8.6 canonical/og 同基准) -->
<meta property="og:title" th:if="${pageTitle != null}" th:content="${pageTitle}">
<meta property="og:description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}" th:content="${seoDescription}">
<meta property="og:image" th:if="${ogImage != null}" th:content="${ogImage}">
<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>
<!-- 站点级自定义样式(B3):位于皮肤 CSS 与微调变量之后层叠覆盖;内容经服务端黑名单强校验后直出 -->
<style th:if="${not #strings.isEmpty(customCss)}" th:utext="${customCss}"></style>
<!-- favicon:管理端配置了 favicon 键用上传图,未配置输出主题内置兜底 icon(assets/favicon.png) -->
<link rel="icon" type="image/png"
th:href="${site.config['favicon'] != null ? url.upload(site.config['favicon']) : url.asset('favicon.png')}">
</head>
</html>
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- 公共头部(mint-breeze):细线顶栏(院系切换+搜索)→ 居中品牌行 → 白底导航;th:* 数据绑定与 blue-white 基准逐字一致 -->
<header class="site-header" th:fragment="header">
<div class="top-bar">
<!-- 专业院系下拉:顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 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.path == site.path} ? 'active'">
<a th:href="${url.siteHome(col.path, col.publicBaseUrl)}"
th:title="${col.name}" th:text="${col.name}">学院名称</a>
</li>
</ul>
</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>
</div>
<div class="brand-row">
<div class="site-brand">
<img th:if="${site.config['logo'] != null}" class="site-logo"
th:src="${url.upload(site.config['logo'])}" th:alt="${site.name}">
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
</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,51 @@
<!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>
<!-- listLayout 布局开关(theme.json select 参数):row=传统行列表,默认/未知值=卡片网格 -->
<main class="list-main" th:classappend="${themeParams['listLayout'] != null and themeParams['listLayout'] == 'row'} ? 'list-rows'">
<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,22 @@
{
"specVersion": 1,
"name": "mint-breeze",
"title": "薄荷沁风",
"version": "1.0.0",
"author": "yunhai",
"description": "清新薄荷风门户:细线顶栏+居中 logo 吸顶、薄荷青品牌色、圆角 12px 卡片网格列表、几何圆点装饰与大留白,清爽亲和,适合职业院校。",
"screenshot": "assets/screenshot.png",
"bannerSize": "1920x560",
"params": [
{ "key": "primary", "cssVar": "--primary", "label": "主色", "type": "color", "default": "#17A2A2" },
{ "key": "primaryDark", "cssVar": "--primary-dark", "label": "主色加深(强调色)", "type": "color", "default": "#0F7C7C" },
{ "key": "primaryLight", "cssVar": "--primary-light", "label": "主色提亮", "type": "color", "default": "#E6F6F6" },
{ "key": "bg", "cssVar": "--bg", "label": "页面底色", "type": "color", "default": "#F5FBFA" },
{ "key": "cardBg", "cssVar": "--card-bg", "label": "卡片底色", "type": "color", "default": "#FFFFFF" },
{ "key": "border", "cssVar": "--border", "label": "边框色", "type": "color", "default": "#DDF0EE" },
{ "key": "text", "cssVar": "--text", "label": "正文文字色", "type": "color", "default": "#2F3E3C" },
{ "key": "textLight", "cssVar": "--text-light", "label": "次要文字色", "type": "color", "default": "#7D8F8C" },
{ "key": "listLayout", "label": "列表布局", "type": "select", "default": "grid",
"options": [ { "value": "grid", "label": "卡片网格(一行 3 张)" }, { "value": "row", "label": "传统行列表" } ] }
]
}