feat(theme): 新增 cherry-bloom/violet-iris 两套门户皮肤并应用于视觉种子院系子站
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# cherry-bloom 皮肤包
|
||||
|
||||
由 mint-breeze 派生的温柔樱粉风皮肤(docs/10 契约 §5 工作流)。**升级随版本发布,不在站点间复制目录**。
|
||||
|
||||
|
||||
## 风格定位
|
||||
|
||||
温柔、甜美,适合幼儿园与小学门户:樱花粉品牌色、大圆角 16px、卡片纯白+暖粉柔和阴影、细线顶栏+居中 logo(桌面吸顶,移动端取消)、列表卡片网格(一行 3 张)、花瓣式圆点装饰与暖白留白;无纹理、无通栏大图、无复杂动效。
|
||||
|
||||
## 数据契约(模板可取用的 model 属性全集,契约 §3)
|
||||
|
||||
| 页面 | 属性 |
|
||||
| --- | --- |
|
||||
| 全部 | `site`(快照:config/分类树/轮播/友链组/院系子站)、`url`(PortalUrlHelper:home/category/article/asset/search/siteHome)、`themeBase`(=`themes/cherry-bloom`)、`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,616 @@
|
||||
/* ============================================================
|
||||
* cherry-bloom 皮肤包样式(mint-breeze 派生,docs/10 皮肤包契约 §5)
|
||||
* 温柔樱粉风:樱花粉品牌色 + 大圆角 16px + 柔和暖粉阴影 + 卡片网格列表
|
||||
* + 花瓣式圆点装饰与暖白留白;无纹理、无通栏大图、无复杂动效。
|
||||
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪)。
|
||||
* ============================================================ */
|
||||
:root {
|
||||
--primary: #DB5A8B;
|
||||
--primary-dark: #B23A68;
|
||||
--primary-light: #FDECF3;
|
||||
--text: #43333B;
|
||||
--text-light: #9C828C;
|
||||
--bg: #FFFAFB;
|
||||
--card-bg: #FFFFFF;
|
||||
--border: #F7DDE7;
|
||||
--radius: 16px;
|
||||
--radius-sm: 10px;
|
||||
--radius-pill: 999px;
|
||||
--shadow-soft: 0 6px 18px rgba(178, 58, 104, .08);
|
||||
--shadow-hover: 0 12px 28px rgba(178, 58, 104, .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); }
|
||||
}
|
||||
|
||||
|
||||
/* ===== 左侧栏目导航(docs/11):有二级节点的模块,栏目/文章页左侧展示兄弟目录 ===== */
|
||||
.list-main.has-sidenav, .detail-main.has-sidenav { display: flex; align-items: flex-start; gap: 24px; }
|
||||
.has-sidenav .list-body { flex: 1; min-width: 0; }
|
||||
.side-nav { flex: none; width: 220px; background: var(--card-bg, #fff); border: 1px solid var(--border); overflow: hidden; }
|
||||
.side-nav-head { background: var(--primary); color: #fff; font-size: 20px; font-weight: 700; text-align: center; padding: 18px 12px; letter-spacing: 2px; }
|
||||
.side-nav-list { list-style: none; margin: 0; padding: 0; }
|
||||
.side-nav-item + .side-nav-item { border-top: 1px dashed var(--border); }
|
||||
.side-nav-item a { display: block; padding: 12px 16px; color: var(--text); text-decoration: none; font-size: 15px; transition: background .15s ease, color .15s ease; }
|
||||
.side-nav-item a:hover { color: var(--primary); background: var(--bg); }
|
||||
.side-nav-item.active a { background: var(--primary); color: #fff; font-weight: 600; text-decoration: none; }
|
||||
/* 顶导航二级下拉当前项高亮(docs/11 顺带) */
|
||||
.site-nav ul.sub-nav li.active a { color: var(--primary); }
|
||||
@media (max-width: 768px) {
|
||||
.list-main.has-sidenav, .detail-main.has-sidenav { flex-direction: column; gap: 12px; }
|
||||
.side-nav { width: 100%; }
|
||||
.side-nav-list { display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||
.side-nav-item { flex: none; }
|
||||
.side-nav-item + .side-nav-item { border-top: none; border-left: 1px dashed var(--border); }
|
||||
.side-nav-item a { white-space: nowrap; }
|
||||
}
|
||||
@media print {
|
||||
.side-nav { display: none; }
|
||||
.list-main.has-sidenav, .detail-main.has-sidenav { display: block; }
|
||||
}
|
||||
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="#EE7FA9"/><stop offset="1" stop-color="#B23A68"/>
|
||||
</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: 435 B |
@@ -0,0 +1,40 @@
|
||||
/* cherry-bloom 皮肤包脚本:仅首页轮播自动播放与圆点切换,无任何外部依赖 */
|
||||
(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,58 @@
|
||||
<!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" th:classappend="${sideNav != null} ? 'has-sidenav'">
|
||||
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
|
||||
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
|
||||
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
|
||||
<ul class="side-nav-list">
|
||||
<li class="side-nav-item" th:each="item : ${sideNav.items}"
|
||||
th:classappend="${item.id == sideNav.activeId} ? 'active'">
|
||||
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${item.name}">子栏目</a>
|
||||
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div class="list-body">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<th:block th:each="node : ${categoryPath}">
|
||||
<a class="crumb-parent" th:href="${url.category(node.id)}" th:text="${node.name}">栏目</a>
|
||||
</th:block>
|
||||
<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>
|
||||
</div>
|
||||
</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,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<!-- 公共头部(cherry-bloom):细线顶栏(院系切换+搜索)→ 居中品牌行 → 白底导航;th:* 数据绑定与 mint-breeze 基准逐字一致 -->
|
||||
<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 and navServiceActive == null} ? 'active'">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
</li>
|
||||
<li th:each="cat : ${navCategories}"
|
||||
th:classappend="${((currentCategoryId == cat.id or navTopId == 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}"
|
||||
th:classappend="${sub.id == currentCategoryId} ? 'active'">
|
||||
<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,68 @@
|
||||
<!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>
|
||||
|
||||
<!-- 首页布局模式(docs/16 §4.4):home_layout 落库后按区块序列渲染(common 兜底骨架,本主题不另设形态) -->
|
||||
<th:block th:if="${layoutBlocks != null}">
|
||||
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: style}" />
|
||||
<th:block th:each="block : ${layoutBlocks}">
|
||||
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: render(${block})}" />
|
||||
</th:block>
|
||||
</th:block>
|
||||
|
||||
<!-- 回落分支(docs/16 D2):无 home_layout 时按现状渲染 -->
|
||||
<th:block th:unless="${layoutBlocks != null}">
|
||||
<!-- 置顶/头条区(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}">更多 ></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>
|
||||
</th:block>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,69 @@
|
||||
<!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' : '') + (${sideNav != null} ? ' has-sidenav' : '')">
|
||||
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
|
||||
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
|
||||
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
|
||||
<ul class="side-nav-list">
|
||||
<li class="side-nav-item" th:each="item : ${sideNav.items}"
|
||||
th:classappend="${item.id == sideNav.activeId} ? 'active'">
|
||||
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${item.name}">子栏目</a>
|
||||
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div class="list-body">
|
||||
<nav class="breadcrumb" th:if="${searchMode != true}">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<th:block th:each="node, st : ${categoryPath}">
|
||||
<a th:if="${!st.last}" class="crumb-parent" th:href="${url.category(node.id)}"
|
||||
th:text="${node.name}">父栏目</a>
|
||||
<span th:if="${st.last}" th:text="${node.name}">栏目</span>
|
||||
</th:block>
|
||||
</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>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,41 @@
|
||||
<!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" th:classappend="${sideNav != null} ? 'has-sidenav'">
|
||||
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
|
||||
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
|
||||
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
|
||||
<ul class="side-nav-list">
|
||||
<li class="side-nav-item" th:each="item : ${sideNav.items}"
|
||||
th:classappend="${item.id == sideNav.activeId} ? 'active'">
|
||||
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${item.name}">子栏目</a>
|
||||
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div class="list-body">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<th:block th:each="node, st : ${categoryPath}">
|
||||
<a th:if="${!st.last}" class="crumb-parent" th:href="${url.category(node.id)}"
|
||||
th:text="${node.name}">父栏目</a>
|
||||
<span th:if="${st.last}" th:text="${node.name}">栏目</span>
|
||||
</th:block>
|
||||
</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>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"specVersion": 1,
|
||||
"name": "cherry-bloom",
|
||||
"title": "樱语粉韵",
|
||||
"version": "1.0.0",
|
||||
"author": "yunhai",
|
||||
"description": "温柔樱粉风门户:细线顶栏+居中 logo 吸顶、樱花粉品牌色、大圆角 16px 卡片网格列表、花瓣式圆点装饰与暖白留白,甜美亲和,适合幼儿园与小学门户。",
|
||||
"screenshot": "assets/screenshot.png",
|
||||
"bannerSize": "1920x560",
|
||||
"params": [
|
||||
{ "key": "primary", "cssVar": "--primary", "label": "主色", "type": "color", "default": "#DB5A8B" },
|
||||
{ "key": "primaryDark", "cssVar": "--primary-dark", "label": "主色加深(强调色)", "type": "color", "default": "#B23A68" },
|
||||
{ "key": "primaryLight", "cssVar": "--primary-light", "label": "主色提亮", "type": "color", "default": "#FDECF3" },
|
||||
{ "key": "bg", "cssVar": "--bg", "label": "页面底色", "type": "color", "default": "#FFFAFB" },
|
||||
{ "key": "cardBg", "cssVar": "--card-bg", "label": "卡片底色", "type": "color", "default": "#FFFFFF" },
|
||||
{ "key": "border", "cssVar": "--border", "label": "边框色", "type": "color", "default": "#F7DDE7" },
|
||||
{ "key": "text", "cssVar": "--text", "label": "正文文字色", "type": "color", "default": "#43333B" },
|
||||
{ "key": "textLight", "cssVar": "--text-light", "label": "次要文字色", "type": "color", "default": "#9C828C" },
|
||||
{ "key": "listLayout", "label": "列表布局", "type": "select", "default": "grid",
|
||||
"options": [ { "value": "grid", "label": "卡片网格(一行 3 张)" }, { "value": "row", "label": "传统行列表" } ] }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# violet-iris 皮肤包
|
||||
|
||||
由 mint-breeze 派生的雅致鸢尾紫皮肤(docs/10 契约 §5 工作流)。**升级随版本发布,不在站点间复制目录**。
|
||||
|
||||
|
||||
## 风格定位
|
||||
|
||||
沉静、书卷气,适合高校与教科研机构门户:鸢尾紫品牌色、圆角 12px、卡片纯白+淡紫柔和阴影、细线顶栏+居中 logo(桌面吸顶,移动端取消)、列表卡片网格(一行 3 张)、几何圆点装饰与大留白;无纹理、无通栏大图、无复杂动效。
|
||||
|
||||
## 数据契约(模板可取用的 model 属性全集,契约 §3)
|
||||
|
||||
| 页面 | 属性 |
|
||||
| --- | --- |
|
||||
| 全部 | `site`(快照:config/分类树/轮播/友链组/院系子站)、`url`(PortalUrlHelper:home/category/article/asset/search/siteHome)、`themeBase`(=`themes/violet-iris`)、`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,616 @@
|
||||
/* ============================================================
|
||||
* violet-iris 皮肤包样式(mint-breeze 派生,docs/10 皮肤包契约 §5)
|
||||
* 雅致鸢尾紫风:鸢尾紫品牌色 + 圆角 12px + 柔和紫阴影 + 卡片网格列表
|
||||
* + 几何圆点装饰与淡薰衣草留白;无纹理、无通栏大图、无复杂动效。
|
||||
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪)。
|
||||
* ============================================================ */
|
||||
:root {
|
||||
--primary: #6552D9;
|
||||
--primary-dark: #4A39B0;
|
||||
--primary-light: #EFECFC;
|
||||
--text: #332F45;
|
||||
--text-light: #847E9C;
|
||||
--bg: #F8F7FD;
|
||||
--card-bg: #FFFFFF;
|
||||
--border: #E4DFF6;
|
||||
--radius: 12px;
|
||||
--radius-sm: 8px;
|
||||
--radius-pill: 999px;
|
||||
--shadow-soft: 0 6px 18px rgba(74, 57, 176, .08);
|
||||
--shadow-hover: 0 12px 28px rgba(74, 57, 176, .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); }
|
||||
}
|
||||
|
||||
|
||||
/* ===== 左侧栏目导航(docs/11):有二级节点的模块,栏目/文章页左侧展示兄弟目录 ===== */
|
||||
.list-main.has-sidenav, .detail-main.has-sidenav { display: flex; align-items: flex-start; gap: 24px; }
|
||||
.has-sidenav .list-body { flex: 1; min-width: 0; }
|
||||
.side-nav { flex: none; width: 220px; background: var(--card-bg, #fff); border: 1px solid var(--border); overflow: hidden; }
|
||||
.side-nav-head { background: var(--primary); color: #fff; font-size: 20px; font-weight: 700; text-align: center; padding: 18px 12px; letter-spacing: 2px; }
|
||||
.side-nav-list { list-style: none; margin: 0; padding: 0; }
|
||||
.side-nav-item + .side-nav-item { border-top: 1px dashed var(--border); }
|
||||
.side-nav-item a { display: block; padding: 12px 16px; color: var(--text); text-decoration: none; font-size: 15px; transition: background .15s ease, color .15s ease; }
|
||||
.side-nav-item a:hover { color: var(--primary); background: var(--bg); }
|
||||
.side-nav-item.active a { background: var(--primary); color: #fff; font-weight: 600; text-decoration: none; }
|
||||
/* 顶导航二级下拉当前项高亮(docs/11 顺带) */
|
||||
.site-nav ul.sub-nav li.active a { color: var(--primary); }
|
||||
@media (max-width: 768px) {
|
||||
.list-main.has-sidenav, .detail-main.has-sidenav { flex-direction: column; gap: 12px; }
|
||||
.side-nav { width: 100%; }
|
||||
.side-nav-list { display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||
.side-nav-item { flex: none; }
|
||||
.side-nav-item + .side-nav-item { border-top: none; border-left: 1px dashed var(--border); }
|
||||
.side-nav-item a { white-space: nowrap; }
|
||||
}
|
||||
@media print {
|
||||
.side-nav { display: none; }
|
||||
.list-main.has-sidenav, .detail-main.has-sidenav { display: block; }
|
||||
}
|
||||
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="#8574E8"/><stop offset="1" stop-color="#4A39B0"/>
|
||||
</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: 435 B |
@@ -0,0 +1,40 @@
|
||||
/* violet-iris 皮肤包脚本:仅首页轮播自动播放与圆点切换,无任何外部依赖 */
|
||||
(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,58 @@
|
||||
<!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" th:classappend="${sideNav != null} ? 'has-sidenav'">
|
||||
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
|
||||
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
|
||||
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
|
||||
<ul class="side-nav-list">
|
||||
<li class="side-nav-item" th:each="item : ${sideNav.items}"
|
||||
th:classappend="${item.id == sideNav.activeId} ? 'active'">
|
||||
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${item.name}">子栏目</a>
|
||||
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div class="list-body">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<th:block th:each="node : ${categoryPath}">
|
||||
<a class="crumb-parent" th:href="${url.category(node.id)}" th:text="${node.name}">栏目</a>
|
||||
</th:block>
|
||||
<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>
|
||||
</div>
|
||||
</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,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<!-- 公共头部(violet-iris):细线顶栏(院系切换+搜索)→ 居中品牌行 → 白底导航;th:* 数据绑定与 mint-breeze 基准逐字一致 -->
|
||||
<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 and navServiceActive == null} ? 'active'">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
</li>
|
||||
<li th:each="cat : ${navCategories}"
|
||||
th:classappend="${((currentCategoryId == cat.id or navTopId == 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}"
|
||||
th:classappend="${sub.id == currentCategoryId} ? 'active'">
|
||||
<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,68 @@
|
||||
<!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>
|
||||
|
||||
<!-- 首页布局模式(docs/16 §4.4):home_layout 落库后按区块序列渲染(common 兜底骨架,本主题不另设形态) -->
|
||||
<th:block th:if="${layoutBlocks != null}">
|
||||
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: style}" />
|
||||
<th:block th:each="block : ${layoutBlocks}">
|
||||
<th:block th:replace="~{'themes/common/fragments/home-blocks' :: render(${block})}" />
|
||||
</th:block>
|
||||
</th:block>
|
||||
|
||||
<!-- 回落分支(docs/16 D2):无 home_layout 时按现状渲染 -->
|
||||
<th:block th:unless="${layoutBlocks != null}">
|
||||
<!-- 置顶/头条区(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}">更多 ></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>
|
||||
</th:block>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,69 @@
|
||||
<!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' : '') + (${sideNav != null} ? ' has-sidenav' : '')">
|
||||
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
|
||||
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
|
||||
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
|
||||
<ul class="side-nav-list">
|
||||
<li class="side-nav-item" th:each="item : ${sideNav.items}"
|
||||
th:classappend="${item.id == sideNav.activeId} ? 'active'">
|
||||
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${item.name}">子栏目</a>
|
||||
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div class="list-body">
|
||||
<nav class="breadcrumb" th:if="${searchMode != true}">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<th:block th:each="node, st : ${categoryPath}">
|
||||
<a th:if="${!st.last}" class="crumb-parent" th:href="${url.category(node.id)}"
|
||||
th:text="${node.name}">父栏目</a>
|
||||
<span th:if="${st.last}" th:text="${node.name}">栏目</span>
|
||||
</th:block>
|
||||
</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>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,41 @@
|
||||
<!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" th:classappend="${sideNav != null} ? 'has-sidenav'">
|
||||
<!-- 左侧栏目导航(docs/11):模块有二级子栏目时展示"模块名 + 全部兄弟" -->
|
||||
<aside class="side-nav" th:if="${sideNav != null}" aria-label="栏目导航">
|
||||
<div class="side-nav-head" th:text="${sideNav.top.name}">模块名</div>
|
||||
<ul class="side-nav-list">
|
||||
<li class="side-nav-item" th:each="item : ${sideNav.items}"
|
||||
th:classappend="${item.id == sideNav.activeId} ? 'active'">
|
||||
<a th:if="${item.type == 3}" th:href="${item.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${item.name}">子栏目</a>
|
||||
<a th:unless="${item.type == 3}" th:href="${url.category(item.id)}" th:text="${item.name}">子栏目</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div class="list-body">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<th:block th:each="node, st : ${categoryPath}">
|
||||
<a th:if="${!st.last}" class="crumb-parent" th:href="${url.category(node.id)}"
|
||||
th:text="${node.name}">父栏目</a>
|
||||
<span th:if="${st.last}" th:text="${node.name}">栏目</span>
|
||||
</th:block>
|
||||
</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>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"specVersion": 1,
|
||||
"name": "violet-iris",
|
||||
"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": "#6552D9" },
|
||||
{ "key": "primaryDark", "cssVar": "--primary-dark", "label": "主色加深(强调色)", "type": "color", "default": "#4A39B0" },
|
||||
{ "key": "primaryLight", "cssVar": "--primary-light", "label": "主色提亮", "type": "color", "default": "#EFECFC" },
|
||||
{ "key": "bg", "cssVar": "--bg", "label": "页面底色", "type": "color", "default": "#F8F7FD" },
|
||||
{ "key": "cardBg", "cssVar": "--card-bg", "label": "卡片底色", "type": "color", "default": "#FFFFFF" },
|
||||
{ "key": "border", "cssVar": "--border", "label": "边框色", "type": "color", "default": "#E4DFF6" },
|
||||
{ "key": "text", "cssVar": "--text", "label": "正文文字色", "type": "color", "default": "#332F45" },
|
||||
{ "key": "textLight", "cssVar": "--text-light", "label": "次要文字色", "type": "color", "default": "#847E9C" },
|
||||
{ "key": "listLayout", "label": "列表布局", "type": "select", "default": "grid",
|
||||
"options": [ { "value": "grid", "label": "卡片网格(一行 3 张)" }, { "value": "row", "label": "传统行列表" } ] }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user