feat(theme): S5 学院蓝主题改造——home-blocks/service/guide页+区块CSS+ThemeRegistry增强
build / build-push (push) Successful in 53s
build / build-push (push) Successful in 53s
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
样式随 fragment 内联携带——themes/common 不注册为皮肤包(无 theme.json),主题资产通道不服务该目录;
|
||||
颜色回落站点主题变量(--primary 等 themeVarsCss 注入),无变量时用内置缺省。
|
||||
契约:block 为渲染端组装的同构 Map——type/title/category.name/homeStyle/items/moreUrl/subtitle/
|
||||
background/source/linkGroups,与回落模式 categoryBlocks 的块字段兼容。
|
||||
background/source/linkGroups/quickLinks/noticeText/noticeUrl/noticeLevel,与回落模式 categoryBlocks 的块字段兼容。
|
||||
-->
|
||||
<th:block th:fragment="style">
|
||||
<style>
|
||||
@@ -19,6 +19,10 @@
|
||||
--plb-band-bg: var(--bg, #f5f7fb);
|
||||
--plb-border: var(--border, rgba(0, 0, 0, .08));
|
||||
--plb-radius: var(--radius, 10px);
|
||||
/* 公告条 warn 档:皮肤契约没有告警色变量,取骨架自持的暖色(不跟随皮肤) */
|
||||
--plb-warn-fg: #d46b08;
|
||||
--plb-warn-bg: #fff7e6;
|
||||
--plb-warn-border: #ffd591;
|
||||
}
|
||||
.plb-section{padding:26px 16px}
|
||||
.plb-container{max-width:1200px;margin:0 auto}
|
||||
@@ -67,15 +71,28 @@
|
||||
.plb-links{display:flex;flex-wrap:wrap;gap:10px;list-style:none;margin:0;padding:0}
|
||||
.plb-links a{display:inline-block;font-size:13px;color:var(--plb-muted);text-decoration:none;border:1px solid var(--plb-border);border-radius:16px;padding:4px 14px;background:var(--plb-card-bg)}
|
||||
.plb-links a:hover{color:var(--plb-primary);border-color:var(--plb-primary)}
|
||||
.plb-quick-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:12px;list-style:none;margin:0;padding:0}
|
||||
.plb-quick-link{display:flex;align-items:center;justify-content:center;min-height:62px;padding:10px 12px;font-size:14px;line-height:1.4;text-align:center;color:var(--plb-text);text-decoration:none;background:var(--plb-page-bg);border:1px solid var(--plb-border);border-radius:var(--plb-radius);overflow-wrap:anywhere}
|
||||
.plb-quick-link:hover{color:var(--plb-primary);border-color:var(--plb-primary)}
|
||||
.plb-notice-bar{background:var(--plb-band-bg);border-top:1px solid var(--plb-border);border-bottom:1px solid var(--plb-border);padding:10px 16px;font-size:14px}
|
||||
.plb-notice-inner{max-width:1200px;margin:0 auto;display:flex;align-items:center;gap:12px}
|
||||
.plb-notice-tag{flex:none;font-weight:600;color:var(--plb-primary)}
|
||||
/* 中文可按字断行,flex 项不锁 min-width:0 就会被长公告挤破容器 */
|
||||
.plb-notice-link,.plb-notice-text{flex:1;min-width:0;color:var(--plb-text);text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.plb-notice-link:hover{text-decoration:underline;color:var(--plb-primary)}
|
||||
.plb-notice-warn{background:var(--plb-warn-bg);border-color:var(--plb-warn-border)}
|
||||
.plb-notice-warn .plb-notice-tag{color:var(--plb-warn-fg)}
|
||||
/* 全宽区块必须跨满整行:网格型主题(mint-breeze 系 .home-main 是三列)会把它们塞进单列,
|
||||
非网格父容器下 grid-column 无副作用,故由骨架自带。 */
|
||||
main > .plb-ticker{grid-column:1 / -1}
|
||||
main > .plb-newshot{grid-column:1 / -1}
|
||||
main > .plb-section{grid-column:1 / -1}
|
||||
main > .plb-notice-bar{grid-column:1 / -1}
|
||||
@media (max-width:768px){
|
||||
.plb-grid2{columns:1}
|
||||
.plb-imagewall{grid-template-columns:repeat(2,1fr)}
|
||||
.plb-gallery-grid{grid-template-columns:repeat(2,1fr)}
|
||||
.plb-quick-grid{grid-template-columns:repeat(3,1fr)}
|
||||
.plb-feat img{width:120px;height:80px}
|
||||
.plb-ticker-item{max-width:150px}
|
||||
}
|
||||
@@ -99,6 +116,8 @@ main > .plb-section{grid-column:1 / -1}
|
||||
<th:block th:case="'ticker'"><th:block th:replace="~{'themes/common/fragments/home-blocks' :: plbTicker(${block})}" /></th:block>
|
||||
<th:block th:case="'gallery'"><th:block th:replace="~{'themes/common/fragments/home-blocks' :: plbGallery(${block})}" /></th:block>
|
||||
<th:block th:case="'links'"><th:block th:replace="~{'themes/common/fragments/home-blocks' :: plbLinks(${block})}" /></th:block>
|
||||
<th:block th:case="'quick_links'"><th:block th:replace="~{'themes/common/fragments/home-blocks' :: plbQuickLinks(${block})}" /></th:block>
|
||||
<th:block th:case="'notice_bar'"><th:block th:replace="~{'themes/common/fragments/home-blocks' :: plbNoticeBar(${block})}" /></th:block>
|
||||
<th:block th:case="*"><th:block th:replace="~{'themes/common/fragments/home-blocks' :: plbCard(${block})}" /></th:block>
|
||||
</th:block>
|
||||
</th:block>
|
||||
@@ -274,3 +293,36 @@ main > .plb-section{grid-column:1 / -1}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 区块:快捷入口宫格(quick_links,v2 手册 §2.8 类型 A):区块内联数组,不读友情链接表;
|
||||
非法 url 项已由渲染端丢弃,这里只消费 block['quickLinks']=[{name,url}] -->
|
||||
<section class="plb-section plb-quick-links" th:fragment="plbQuickLinks(block)" aria-label="快捷入口">
|
||||
<div class="plb-container">
|
||||
<div class="plb-card">
|
||||
<div th:replace="~{'themes/common/fragments/home-blocks' :: head(${block})}"></div>
|
||||
<ul class="plb-quick-grid" th:if="${!#lists.isEmpty(block['quickLinks'])}">
|
||||
<li th:each="l : ${block['quickLinks']}">
|
||||
<a class="plb-quick-link" th:href="${l.url}" th:text="${l.name}">入口名</a>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- 空态守卫必须包在 th:block 外层(坑①:th:replace 优先级 100 先于 th:if 300 求值) -->
|
||||
<th:block th:if="${#lists.isEmpty(block['quickLinks'])}">
|
||||
<div th:replace="~{'themes/common/fragments/home-blocks' :: empty(${block})}"></div>
|
||||
</th:block>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 区块:公告条(notice_bar,v2 手册 §2.8 类型 B):单条手填通告,非滚动非文章列表;
|
||||
level 缺省 info,warn 走告警配色;url 非法时渲染端置空 → 退化为纯文本 -->
|
||||
<div class="plb-notice-bar" th:fragment="plbNoticeBar(block)" role="note"
|
||||
th:if="${!#strings.isEmpty(block['noticeText'])}"
|
||||
th:classappend="${block['noticeLevel'] == 'warn' ? 'plb-notice-warn' : 'plb-notice-info'}">
|
||||
<div class="plb-notice-inner">
|
||||
<span class="plb-notice-tag" th:text="${block['title']}">公告</span>
|
||||
<a class="plb-notice-link" th:if="${!#strings.isEmpty(block['noticeUrl'])}"
|
||||
th:href="${block['noticeUrl']}" th:text="${block['noticeText']}">公告正文</a>
|
||||
<span class="plb-notice-text" th:unless="${!#strings.isEmpty(block['noticeUrl'])}"
|
||||
th:text="${block['noticeText']}">公告正文</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1371,3 +1371,395 @@ input, button, select, textarea { font-size: inherit; color: inherit; }
|
||||
.sp-side-nav { display: none; }
|
||||
.sp-cols { display: block; }
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
16. 首页布局模式区块(fragments/home-blocks.html · 手册 §A.2)
|
||||
站点配了 home_layout 时首页正文由本包 fragment 渲染(不再落 themes/common 骨架)。
|
||||
只补布局模式才出现的排布件(双栏要闻、速递条、图墙、链接胶囊、快捷宫格、公告条);
|
||||
标题、白卡、日期方块、置顶徽标、空态沿用第 7/9 节既有语汇。
|
||||
颜色一律走 --sp-* / 契约别名变量(换肤必须穿透到这里)。
|
||||
--------------------------------------------------------------------- */
|
||||
/* 坑③:自研 fragment 后 common 骨架不再兜 grid-column,区块根类必须自跨整行
|
||||
(本包 main 目前非 grid 父容器,该声明无副作用;皮肤改成网格布局时不至于塌列) */
|
||||
main > .sp-section,
|
||||
main > .sp-ticker,
|
||||
main > .sp-notice-bar { grid-column: 1 / -1; }
|
||||
|
||||
.sp-section { position: relative; }
|
||||
.sp-sec-bg {
|
||||
position: absolute; inset: 0; z-index: 0;
|
||||
background-size: cover; background-position: center;
|
||||
opacity: .45; pointer-events: none;
|
||||
}
|
||||
/* 定位元素默认画在普通流之上:不抬 z-index,新闻背景带会糊在卡片正面(academic-navy 实测整卡泛蓝) */
|
||||
.sp-sec-bg + .sp-block { position: relative; z-index: 1; background: var(--sp-card-bg); }
|
||||
|
||||
/* 整宽区块卡:不参与第 9 节三列网格,故不抬起 hover 位移 */
|
||||
.sp-block--flow { padding: 20px 26px 12px; }
|
||||
.sp-block--flow:hover { transform: none; box-shadow: var(--sp-shadow-sm); }
|
||||
|
||||
/* 双栏要闻:grid 轨道必须 minmax(0,1fr),否则 nowrap 省略行的 min-content 会撑破轨道(中文尤其明显) */
|
||||
.sp-split {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
|
||||
gap: 26px;
|
||||
padding-top: 14px;
|
||||
}
|
||||
.sp-split > * { min-width: 0; }
|
||||
/* 左图限高 + 右列行均分:否则 16/9 大图把左栏撑到 400px,右列 5 行挤在顶部留大片空白 */
|
||||
.sp-split .sp-feat-cover { max-height: 300px; }
|
||||
.sp-split .sp-block-list { display: flex; flex-direction: column; justify-content: space-between; }
|
||||
.sp-split .sp-block-item { padding: 12px 0; }
|
||||
|
||||
.sp-block-list--two { columns: 2; column-gap: 34px; }
|
||||
.sp-block-list--two .sp-block-item { break-inside: avoid; }
|
||||
|
||||
.sp-feat-cover--row { aspect-ratio: 16 / 7; }
|
||||
|
||||
.sp-item-date {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
color: var(--sp-text-light);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
/* 来源徽标:细描边小 tag,跟随主色(媒体聚焦形态与 news_grid/card 元信息共用) */
|
||||
.sp-src {
|
||||
flex: none;
|
||||
font-size: 11px; line-height: 18px; padding: 0 6px;
|
||||
color: var(--sp-primary);
|
||||
border: 1px solid var(--sp-primary);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 图片墙 / 线上展厅:四列格 + 底部渐变题注(渐变端点用主色深变量,不写死色值) */
|
||||
.sp-wall {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin: 16px 0 0;
|
||||
}
|
||||
.sp-wall-item {
|
||||
position: relative;
|
||||
border-radius: var(--sp-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--sp-shadow-sm);
|
||||
}
|
||||
.sp-wall-item img { width: 100%; height: 158px; object-fit: cover; transition: transform .5s ease; }
|
||||
.sp-wall-item:hover img { transform: scale(1.05); }
|
||||
.sp-wall-cap {
|
||||
position: absolute; left: 0; right: 0; bottom: 0;
|
||||
padding: 26px 10px 9px;
|
||||
font-size: 13px;
|
||||
color: var(--sp-ink-on-dark);
|
||||
background: linear-gradient(transparent, var(--sp-primary-dark));
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
/* 线上展厅:整幅居中(本包签名式双线标题带),去掉紧贴区块上沿的那道顶边线 */
|
||||
.sp-gallery .sp-block-head { border-top: 0; padding-top: 0; }
|
||||
.sp-gallery-sub {
|
||||
margin: -4px 0 4px;
|
||||
text-align: center;
|
||||
font-size: 12px; letter-spacing: .18em; text-transform: uppercase;
|
||||
color: var(--sp-text-light);
|
||||
}
|
||||
|
||||
/* 要闻速递横条:flex 项自动最小尺寸 = min-content,中文可按字断行 → 不锁 nowrap / 不让出收缩权就塌成一字一列 */
|
||||
.sp-ticker {
|
||||
position: relative;
|
||||
background: var(--sp-primary);
|
||||
color: var(--sp-ink-on-dark);
|
||||
padding: 11px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
.sp-ticker-inner { display: flex; align-items: center; gap: 16px; min-width: 0; }
|
||||
.sp-ticker-label {
|
||||
flex: none;
|
||||
font-weight: 700; letter-spacing: .06em;
|
||||
padding-right: 14px;
|
||||
border-right: 1px solid var(--sp-ink-on-dark-faint);
|
||||
}
|
||||
.sp-ticker-list {
|
||||
display: flex; gap: 28px;
|
||||
flex: 1; min-width: 0;
|
||||
overflow: hidden; white-space: nowrap;
|
||||
}
|
||||
.sp-ticker-list li { flex: none; min-width: 0; }
|
||||
.sp-ticker-item {
|
||||
display: block; flex: none;
|
||||
max-width: 320px;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
color: var(--sp-ink-on-dark-soft);
|
||||
}
|
||||
.sp-ticker-item:hover { color: var(--sp-ink-on-dark); text-decoration: underline; }
|
||||
|
||||
/* 友情链接胶囊行 */
|
||||
.sp-links-group {
|
||||
margin: 12px 0 0;
|
||||
font-size: 13px; letter-spacing: .08em;
|
||||
color: var(--sp-text-light);
|
||||
}
|
||||
.sp-links { display: flex; flex-wrap: wrap; gap: 10px; margin: 8px 0 0; }
|
||||
.sp-links a {
|
||||
font-size: 13px;
|
||||
color: var(--sp-text-light);
|
||||
padding: 4px 14px;
|
||||
background: var(--sp-card-bg);
|
||||
border: 1px solid var(--sp-border);
|
||||
border-radius: 999px;
|
||||
}
|
||||
.sp-links a:hover { color: var(--sp-primary); border-color: var(--sp-primary); }
|
||||
|
||||
/* 快捷通道(quick_links):六列入口格,主蓝细边、hover 反色;名称超长省略不撑格 */
|
||||
.sp-quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin: 16px 0 6px;
|
||||
}
|
||||
.sp-quick-link {
|
||||
display: block;
|
||||
padding: 14px 10px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: var(--sp-primary-dark);
|
||||
background: var(--sp-bg);
|
||||
border: 1px solid var(--sp-border);
|
||||
border-radius: var(--sp-radius);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
transition: color var(--sp-speed) ease, background var(--sp-speed) ease, border-color var(--sp-speed) ease;
|
||||
}
|
||||
.sp-quick-link:hover {
|
||||
color: var(--sp-ink-on-dark);
|
||||
background: var(--sp-primary);
|
||||
border-color: var(--sp-primary);
|
||||
}
|
||||
|
||||
/* 公告条(notice_bar):单条整宽提示。warn 档是语义色、不属于皮肤可配调色板,
|
||||
故与 themes/common 骨架同一处置——自持三个 --sp-warn-* 令牌,不假装皮肤契约有告警变量 */
|
||||
.sp-notice-bar {
|
||||
--sp-warn-fg: #d46b08;
|
||||
--sp-warn-bg: #fff7e6;
|
||||
--sp-warn-border: #ffd591;
|
||||
position: relative;
|
||||
padding: 12px 0;
|
||||
font-size: 14px;
|
||||
background: var(--sp-card-bg);
|
||||
border-top: 1px solid var(--sp-border);
|
||||
border-bottom: 1px solid var(--sp-border);
|
||||
}
|
||||
.sp-notice-inner { display: flex; align-items: baseline; gap: 14px; min-width: 0; }
|
||||
.sp-notice-tag {
|
||||
flex: none;
|
||||
font-weight: 700; letter-spacing: .06em;
|
||||
color: var(--sp-primary);
|
||||
}
|
||||
.sp-notice-link,
|
||||
.sp-notice-text {
|
||||
flex: 1; min-width: 0;
|
||||
color: var(--sp-text);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.sp-notice-link:hover { color: var(--sp-primary); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.sp-notice--info { border-top-color: var(--sp-primary); border-bottom-color: var(--sp-primary); }
|
||||
.sp-notice--warn {
|
||||
background: var(--sp-warn-bg);
|
||||
border-top-color: var(--sp-warn-border);
|
||||
border-bottom-color: var(--sp-warn-border);
|
||||
}
|
||||
.sp-notice--warn .sp-notice-tag { color: var(--sp-warn-fg); }
|
||||
.sp-notice--warn .sp-notice-link { color: var(--sp-warn-fg); }
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.sp-split { grid-template-columns: minmax(0, 1fr); }
|
||||
.sp-wall { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.sp-block-list--two { columns: 1; }
|
||||
.sp-ticker-item { max-width: 180px; }
|
||||
.sp-block--flow { padding: 16px 16px 8px; }
|
||||
.sp-quick-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.sp-notice-inner { flex-direction: column; gap: 4px; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sp-quick-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
17. 服务中心 / 事项指南(service.html · guide.html,svc 页族)
|
||||
页面卡/面包屑/页题/分页沿用第 10 节既有语汇,这里只补事项特有的件。
|
||||
--------------------------------------------------------------------- */
|
||||
.sp-svc-page { min-width: 0; }
|
||||
|
||||
.sp-svc-filters {
|
||||
border: 1px solid var(--sp-border);
|
||||
border-radius: var(--sp-radius);
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.sp-svc-filter-group { display: flex; align-items: flex-start; gap: 12px; padding: 6px 0; }
|
||||
.sp-svc-filter-group + .sp-svc-filter-group { border-top: 1px dashed var(--sp-border); }
|
||||
.sp-svc-filter-label { flex: none; font-size: 13px; font-weight: 600; color: var(--sp-text); padding-top: 3px; }
|
||||
.sp-svc-filter-options { display: flex; flex-wrap: wrap; gap: 8px; min-width: 0; }
|
||||
|
||||
.sp-chip {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 3px 8px;
|
||||
font-size: 13px;
|
||||
color: var(--sp-text);
|
||||
background: var(--sp-card-bg);
|
||||
border: 1px solid var(--sp-border);
|
||||
border-radius: var(--sp-radius);
|
||||
transition: color var(--sp-speed) ease, border-color var(--sp-speed) ease, background var(--sp-speed) ease;
|
||||
}
|
||||
.sp-chip em { font-style: normal; font-size: 12px; color: var(--sp-text-light); }
|
||||
.sp-chip:hover { color: var(--sp-primary); border-color: var(--sp-primary); }
|
||||
.sp-chip.active { background: var(--sp-primary); border-color: var(--sp-primary); color: var(--sp-ink-on-dark); }
|
||||
.sp-chip.active em { color: var(--sp-ink-on-dark-soft); }
|
||||
|
||||
.sp-svc-toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; }
|
||||
.sp-svc-search {
|
||||
display: flex;
|
||||
width: min(420px, 100%);
|
||||
border: 1px solid var(--sp-border);
|
||||
border-radius: var(--sp-radius);
|
||||
overflow: hidden;
|
||||
transition: border-color var(--sp-speed) ease, box-shadow var(--sp-speed) ease;
|
||||
}
|
||||
.sp-svc-search:focus-within { border-color: var(--sp-primary); box-shadow: var(--sp-shadow-sm); }
|
||||
.sp-svc-search input { flex: 1; min-width: 0; border: 0; padding: 8px 12px; font-size: 14px; }
|
||||
.sp-svc-search input:focus { outline: none; }
|
||||
.sp-svc-search button {
|
||||
border: 0;
|
||||
background: var(--sp-primary);
|
||||
color: var(--sp-ink-on-dark);
|
||||
font-size: 14px;
|
||||
padding: 8px 22px;
|
||||
transition: background var(--sp-speed) ease;
|
||||
}
|
||||
.sp-svc-search button:hover { background: var(--sp-primary-light); }
|
||||
.sp-svc-clear { font-size: 13px; color: var(--sp-text-light); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.sp-svc-clear:hover { color: var(--sp-primary); }
|
||||
|
||||
/* 事项卡列表 */
|
||||
.sp-svc-list > li + li { border-top: 1px solid var(--sp-border); }
|
||||
.sp-svc-item { display: flex; gap: 18px; padding: 16px 0; min-width: 0; }
|
||||
.sp-svc-item-cover {
|
||||
flex: none; width: 120px; aspect-ratio: 16 / 9;
|
||||
border-radius: var(--sp-radius);
|
||||
overflow: hidden;
|
||||
background: var(--sp-bg);
|
||||
}
|
||||
.sp-svc-item-cover img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.sp-svc-item-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
|
||||
.sp-svc-item-title { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; min-width: 0; }
|
||||
.sp-svc-item-name { font-size: 16px; font-weight: 600; color: var(--sp-text); transition: color var(--sp-speed) ease; }
|
||||
.sp-svc-item:hover .sp-svc-item-name { color: var(--sp-primary); }
|
||||
/* 事项徽标:本包调色板没有第二语义色,线上办理态改用主色深 + 描边区分(不新增裸色) */
|
||||
.sp-tag {
|
||||
flex: none;
|
||||
font-size: 12px; line-height: 18px; padding: 0 6px;
|
||||
color: var(--sp-primary);
|
||||
background: var(--sp-bg);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.sp-tag.online { color: var(--sp-primary-dark); border: 1px solid var(--sp-primary-light); }
|
||||
.sp-svc-item-summary {
|
||||
font-size: 13px; color: var(--sp-text-light); line-height: 1.7;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||
}
|
||||
.sp-svc-item-meta { display: flex; flex-wrap: wrap; gap: 14px; font-size: 12px; color: var(--sp-text-light); }
|
||||
|
||||
/* 事项指南页 */
|
||||
.sp-svc-guide,
|
||||
.sp-svc-guide-body,
|
||||
.sp-svc-unavailable { min-width: 0; }
|
||||
.sp-svc-apply { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||
.sp-svc-apply-btn {
|
||||
display: inline-flex; align-items: center;
|
||||
padding: 8px 28px;
|
||||
font-size: 16px; font-weight: 600;
|
||||
color: var(--sp-ink-on-dark);
|
||||
background: var(--sp-primary);
|
||||
border-radius: var(--sp-radius);
|
||||
box-shadow: var(--sp-shadow-sm);
|
||||
transition: background var(--sp-speed) ease, box-shadow var(--sp-speed) ease, transform var(--sp-speed) ease;
|
||||
}
|
||||
.sp-svc-apply-btn:hover {
|
||||
color: var(--sp-ink-on-dark);
|
||||
background: var(--sp-primary-dark);
|
||||
box-shadow: var(--sp-shadow-md);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.sp-svc-apply-hint { font-size: 13px; color: var(--sp-text-light); }
|
||||
.sp-svc-summary {
|
||||
margin-bottom: 18px;
|
||||
padding: 10px 16px;
|
||||
font-size: 13px; color: var(--sp-text-light); line-height: 1.7;
|
||||
background: var(--sp-bg);
|
||||
border-left: 4px solid var(--sp-primary-light);
|
||||
border-radius: 0 var(--sp-radius) var(--sp-radius) 0;
|
||||
}
|
||||
.sp-svc-info { width: 100%; border-collapse: collapse; margin: 12px 0 24px; font-size: 13px; }
|
||||
.sp-svc-info th,
|
||||
.sp-svc-info td {
|
||||
border: 1px solid var(--sp-border);
|
||||
padding: 8px 12px;
|
||||
text-align: left; vertical-align: top;
|
||||
}
|
||||
.sp-svc-info th { width: 120px; background: var(--sp-bg); color: var(--sp-text); font-weight: 600; white-space: nowrap; }
|
||||
.sp-svc-section { margin-top: 24px; }
|
||||
.sp-svc-section h2 {
|
||||
margin-bottom: 12px;
|
||||
font-size: 18px; line-height: 1.5;
|
||||
color: var(--sp-text);
|
||||
padding-left: 12px;
|
||||
border-left: 4px solid var(--sp-primary);
|
||||
}
|
||||
.sp-svc-attachments ul { display: flex; flex-direction: column; gap: 8px; }
|
||||
.sp-svc-attachments a { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--sp-primary); }
|
||||
.sp-svc-attachments a:hover { text-decoration: underline; text-underline-offset: 3px; }
|
||||
.sp-svc-att-size { font-size: 12px; color: var(--sp-text-light); margin-left: 8px; }
|
||||
.sp-svc-links {
|
||||
display: flex; flex-wrap: wrap; gap: 12px;
|
||||
margin-top: 24px; padding-top: 16px;
|
||||
border-top: 1px dashed var(--sp-border);
|
||||
}
|
||||
.sp-svc-links a {
|
||||
font-size: 13px;
|
||||
color: var(--sp-primary);
|
||||
padding: 4px 16px;
|
||||
border: 1px solid var(--sp-primary);
|
||||
border-radius: var(--sp-radius);
|
||||
transition: background var(--sp-speed) ease, color var(--sp-speed) ease;
|
||||
}
|
||||
.sp-svc-links a:hover { background: var(--sp-primary); color: var(--sp-ink-on-dark); }
|
||||
.sp-svc-related { margin-top: 32px; }
|
||||
.sp-svc-related-title {
|
||||
margin-bottom: 12px;
|
||||
font-size: 18px; line-height: 1.5;
|
||||
padding-left: 12px;
|
||||
border-left: 4px solid var(--sp-primary);
|
||||
}
|
||||
.sp-svc-related-list > li + li { border-top: 1px dashed var(--sp-border); }
|
||||
.sp-svc-related-list a {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
||||
padding: 8px 0; min-width: 0;
|
||||
}
|
||||
.sp-svc-related-name {
|
||||
flex: 1; min-width: 0;
|
||||
font-size: 14px; color: var(--sp-text);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.sp-svc-related-list a:hover .sp-svc-related-name { color: var(--sp-primary); }
|
||||
.sp-svc-related-meta {
|
||||
flex: none; display: inline-flex; align-items: center; gap: 8px;
|
||||
font-size: 12px; color: var(--sp-text-light);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
18. headerStyle=plain 变体(theme.json select 参数,header 根 class 切换)
|
||||
banner(缺省)= 现形态;plain = 收起深蓝顶栏、压低品牌行——简洁顶栏。
|
||||
--------------------------------------------------------------------- */
|
||||
.sp-header--plain .sp-topbar { display: none; }
|
||||
.sp-header--plain .sp-brandbar-inner { padding-top: 10px; padding-bottom: 10px; }
|
||||
.sp-header--plain .sp-brand-badge { width: 44px; height: 44px; }
|
||||
.sp-header--plain .sp-site-name { font-size: 22px; }
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<!-- scholar-blue 公共头部:深蓝顶栏(日期+院系切换) + 深蓝品牌行(徽标+站名+圆角搜索) + 白底主导航(吸顶反蓝、
|
||||
主蓝半透明下拉面板)。数据绑定与 default 契约一致,仅换皮。 -->
|
||||
<header class="sp-header" th:fragment="header">
|
||||
主蓝半透明下拉面板)。数据绑定与 default 契约一致,仅换皮。
|
||||
headerStyle=plain(theme.json select 参数)时收起深蓝顶栏、压低品牌行——root class 切换,CSS 第 18 节消费。 -->
|
||||
<header class="sp-header" th:fragment="header"
|
||||
th:classappend="${themeParams['headerStyle'] == 'plain'} ? 'sp-header--plain'">
|
||||
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
||||
|
||||
<!-- 顶栏 -->
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
<!--
|
||||
scholar-blue(学院蓝)首页区块 fragment(v2 手册 §A.2 · P-A1 定型模式):
|
||||
站点配了 home_layout 时,区块正文必须由本文件渲染,不能落到 themes/common 的通用骨架——
|
||||
否则换肤只换了页头页脚,首页正文仍是骨架内置蓝灰卡(线上事故根因,见手册 §2.2)。
|
||||
结构复用本包 main.css 第 7/9 节既有语汇(sp-block / sp-block-head--bar / sp-block-list /
|
||||
sp-block-item / sp-block-item-title / sp-date-chip / sp-feat-* / sp-top-badge / sp-empty),
|
||||
布局模式特有的排布(双栏要闻、速递条、图墙、链接行、快捷宫格、公告条)另起 main.css 第 16 节。
|
||||
契约:block 为渲染端装配的同构 Map(type/title/category.name/homeStyle/items/moreUrl/background/
|
||||
quickLinks/noticeText/noticeUrl/noticeLevel),与回落模式 categoryBlocks 的块字段兼容;
|
||||
区块标题一律取 category.name(baseBlock 已把 title 合成进 category.name,两种模式同构)。
|
||||
三条铁律(手册 §2.5):类名全 sp- 前缀;Map 可选键一律 block['key'] 括号访问(坑②,点号访问缺键抛错);
|
||||
引用 empty 的空态守卫必须包在 th:block 外层(坑①,th:replace 优先级 100 先于 th:if 300 求值)。
|
||||
-->
|
||||
<!-- 形态一:双栏要闻(homeStyle=news_grid)——左 3 份大图文头条、右 2 份紧凑行;区块背景取站点 news_bg_image -->
|
||||
<section class="sp-section sp-container" th:fragment="news_grid(block)" aria-label="栏目区块"
|
||||
th:with="newsBg=${block['background'] != null ? (block['background'] ? site.config['news_bg_image'] : null) : site.config['news_bg_image']}">
|
||||
<div class="sp-sec-bg" th:if="${!#strings.isEmpty(newsBg)}"
|
||||
th:style="|background-image:url(${url.upload(newsBg)})|"></div>
|
||||
<div class="sp-block sp-block--flow"
|
||||
th:with="lead=${!#lists.isEmpty(block.items) ? block.items[0] : null},
|
||||
rest=${#lists.size(block.items) > 1 ? block.items.subList(1, block.items.size()) : null}">
|
||||
<div class="sp-block-head sp-block-head--bar">
|
||||
<h2 th:text="${block.category.name}">校园要闻</h2>
|
||||
<a class="sp-more" th:href="${block.moreUrl}">更多 ›</a>
|
||||
</div>
|
||||
<div class="sp-split" th:if="${lead != null}">
|
||||
<div class="sp-block-featured">
|
||||
<a th:href="${url.article(lead.id)}">
|
||||
<span class="sp-feat-cover" th:if="${lead.coverImage != null}">
|
||||
<img th:src="${url.upload(lead.coverImage)}" th:alt="${lead.title}" loading="lazy">
|
||||
</span>
|
||||
<span class="sp-feat-title" th:text="${lead.title}">头条标题</span>
|
||||
<span class="sp-feat-summary" th:if="${!#strings.isEmpty(lead.summary)}"
|
||||
th:text="${lead.summary}">摘要</span>
|
||||
<span class="sp-feat-meta">
|
||||
<span class="sp-top-badge" th:if="${lead.topmost == 1}">置顶</span>
|
||||
<span class="sp-src" th:if="${!#strings.isEmpty(lead.source)}" th:text="${lead.source}">来源</span>
|
||||
<time th:if="${lead.publishTime != null}"
|
||||
th:text="${#temporals.format(lead.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<ul class="sp-block-list" th:if="${rest != null}">
|
||||
<li class="sp-block-item" th:each="item : ${rest}">
|
||||
<a class="sp-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<span class="sp-top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
<time class="sp-item-date" th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'MM-dd')}">01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<th:block th:if="${#lists.isEmpty(block.items)}">
|
||||
<div th:replace="~{'themes/scholar-blue/fragments/home-blocks' :: empty(${block})}"></div>
|
||||
</th:block>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 形态二:默认卡(homeStyle 未设/未知)——整宽头条(左图右文)+ 行式列表 -->
|
||||
<section class="sp-section sp-container" th:fragment="card(block)" aria-label="栏目区块">
|
||||
<div class="sp-block sp-block--flow">
|
||||
<div class="sp-block-head sp-block-head--bar">
|
||||
<h2 th:text="${block.category.name}">栏目名</h2>
|
||||
<a class="sp-more" th:href="${block.moreUrl}">更多 ›</a>
|
||||
</div>
|
||||
<div class="sp-block-featured" th:if="${!#lists.isEmpty(block.items)}" th:with="feat=${block.items[0]}">
|
||||
<a th:href="${url.article(feat.id)}">
|
||||
<span class="sp-feat-cover sp-feat-cover--row" th:if="${feat.coverImage != null}">
|
||||
<img th:src="${url.upload(feat.coverImage)}" th:alt="${feat.title}" loading="lazy">
|
||||
</span>
|
||||
<span class="sp-feat-title" th:text="${feat.title}">头条标题</span>
|
||||
<span class="sp-feat-summary" th:if="${!#strings.isEmpty(feat.summary)}" th:text="${feat.summary}">摘要</span>
|
||||
<span class="sp-feat-meta">
|
||||
<span class="sp-top-badge" th:if="${feat.topmost == 1}">置顶</span>
|
||||
<span class="sp-src" th:if="${!#strings.isEmpty(feat.source)}" th:text="${feat.source}">来源</span>
|
||||
<time th:if="${feat.publishTime != null}"
|
||||
th:text="${#temporals.format(feat.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<ul class="sp-block-list" th:if="${#lists.size(block.items) > 1}">
|
||||
<li class="sp-block-item" th:each="item, stat : ${block.items}" th:if="${stat.index > 0}">
|
||||
<a class="sp-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<span class="sp-top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
<time class="sp-item-date" th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
<th:block th:if="${#lists.isEmpty(block.items)}">
|
||||
<div th:replace="~{'themes/scholar-blue/fragments/home-blocks' :: empty(${block})}"></div>
|
||||
</th:block>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 形态三:日期方块行(homeStyle=compact)——学院蓝最典型的「方块日期 + 标题」行式(复用第 9 节 sp-date-chip) -->
|
||||
<section class="sp-section sp-container" th:fragment="compact(block)" aria-label="栏目区块">
|
||||
<div class="sp-block sp-block--flow">
|
||||
<div class="sp-block-head sp-block-head--bar">
|
||||
<h2 th:text="${block.category.name}">通知公告</h2>
|
||||
<a class="sp-more" th:href="${block.moreUrl}">更多 ›</a>
|
||||
</div>
|
||||
<ul class="sp-block-list" th:if="${!#lists.isEmpty(block.items)}">
|
||||
<li class="sp-block-item" th:each="item : ${block.items}">
|
||||
<span class="sp-date-chip" th:if="${item.publishTime != null}" th:with="d=${item.publishTime}">
|
||||
<b th:text="${#temporals.format(d, 'dd')}">01</b>
|
||||
<i th:text="${#temporals.format(d, 'yyyy-MM')}">2026-01</i>
|
||||
</span>
|
||||
<a class="sp-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<span class="sp-top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
</li>
|
||||
</ul>
|
||||
<th:block th:if="${#lists.isEmpty(block.items)}">
|
||||
<div th:replace="~{'themes/scholar-blue/fragments/home-blocks' :: empty(${block})}"></div>
|
||||
</th:block>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 形态四:媒体聚焦(homeStyle=media_focus)——来源徽标 + 双列行式 -->
|
||||
<section class="sp-section sp-container" th:fragment="media_focus(block)" aria-label="栏目区块">
|
||||
<div class="sp-block sp-block--flow">
|
||||
<div class="sp-block-head sp-block-head--bar">
|
||||
<h2 th:text="${block.category.name}">媒体聚焦</h2>
|
||||
<a class="sp-more" th:href="${block.moreUrl}">更多 ›</a>
|
||||
</div>
|
||||
<ul class="sp-block-list sp-block-list--two" th:if="${!#lists.isEmpty(block.items)}">
|
||||
<li class="sp-block-item" th:each="item : ${block.items}">
|
||||
<span class="sp-src" th:if="${!#strings.isEmpty(item.source)}" th:text="${item.source}">媒体来源</span>
|
||||
<a class="sp-block-item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time class="sp-item-date" th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'MM-dd')}">01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
<th:block th:if="${#lists.isEmpty(block.items)}">
|
||||
<div th:replace="~{'themes/scholar-blue/fragments/home-blocks' :: empty(${block})}"></div>
|
||||
</th:block>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 形态五:图片墙(homeStyle=image_wall)——仅取有封面的条目,四列格 + 底部渐变题注 -->
|
||||
<section class="sp-section sp-container" th:fragment="image_wall(block)" aria-label="栏目区块">
|
||||
<div class="sp-block-head sp-block-head--bar">
|
||||
<h2 th:text="${block.category.name}">图片新闻</h2>
|
||||
<a class="sp-more" th:href="${block.moreUrl}">更多 ›</a>
|
||||
</div>
|
||||
<ul class="sp-wall" th:if="${!#lists.isEmpty(block.items)}">
|
||||
<li class="sp-wall-item" th:each="item : ${block.items}" th:if="${item.coverImage != null}">
|
||||
<a th:href="${url.article(item.id)}" th:title="${item.title}">
|
||||
<img th:src="${url.upload(item.coverImage)}" th:alt="${item.title}" loading="lazy">
|
||||
<span class="sp-wall-cap" th:text="${item.title}">图片标题</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<th:block th:if="${#lists.isEmpty(block.items)}">
|
||||
<div th:replace="~{'themes/scholar-blue/fragments/home-blocks' :: empty(${block})}"></div>
|
||||
</th:block>
|
||||
</section>
|
||||
|
||||
<!-- 布局模式:要闻速递横条(block.items 由渲染端按置顶+最新装配;单行不换行,中文不塌列) -->
|
||||
<section class="sp-ticker" th:fragment="ticker(block)" aria-label="要闻速递"
|
||||
th:if="${!#lists.isEmpty(block.items)}">
|
||||
<div class="sp-container sp-ticker-inner">
|
||||
<span class="sp-ticker-label" th:text="${block.title}">要闻速递</span>
|
||||
<ul class="sp-ticker-list">
|
||||
<li th:each="item : ${block.items}">
|
||||
<a class="sp-ticker-item" th:href="${url.article(item.id)}" th:text="${item.title}">要闻标题</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 布局模式:线上展厅(block.items = 轮播位 gallery)——居中双线标题是本包签名,故不用 --bar 变体 -->
|
||||
<section class="sp-section sp-container sp-gallery" th:fragment="gallery(block)" aria-label="线上展厅"
|
||||
th:if="${!#lists.isEmpty(block.items)}">
|
||||
<div class="sp-block-head">
|
||||
<h2 th:text="${block.title}">线上展厅</h2>
|
||||
<i class="sp-block-tick" aria-hidden="true"></i>
|
||||
</div>
|
||||
<p class="sp-gallery-sub" th:if="${!#strings.isEmpty(block['subtitle'])}" th:text="${block['subtitle']}">
|
||||
Online Exhibition Hall
|
||||
</p>
|
||||
<ul class="sp-wall">
|
||||
<li class="sp-wall-item" th:each="g : ${block.items}">
|
||||
<a th:href="${g.linkUrl}" th:title="${g.title}">
|
||||
<img th:src="${g.imageUrl}" th:alt="${g.title}" loading="lazy">
|
||||
<span class="sp-wall-cap" th:if="${!#strings.isEmpty(g.title)}" th:text="${g.title}">作品标题</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 布局模式:友情链接分组(block.linkGroups 由渲染端按 group 过滤)——胶囊行 -->
|
||||
<section class="sp-section sp-container" th:fragment="links(block)" aria-label="友情链接">
|
||||
<div class="sp-block sp-block--flow">
|
||||
<div class="sp-block-head sp-block-head--bar">
|
||||
<h2 th:text="${block.title}">友情链接</h2>
|
||||
</div>
|
||||
<th:block th:each="grp : ${block.linkGroups}">
|
||||
<p class="sp-links-group" th:if="${!#strings.isEmpty(grp.groupName)}" th:text="${grp.groupName}">分组</p>
|
||||
<ul class="sp-links">
|
||||
<li th:each="l : ${grp.links}">
|
||||
<a th:href="${l.linkUrl}" th:text="${l.name}" target="_blank" rel="noopener">链接名</a>
|
||||
</li>
|
||||
</ul>
|
||||
</th:block>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 布局模式:快捷通道(quick_links,手册 §2.8 类型 A)——区块内联入口数组,不读友情链接表。
|
||||
渲染端已把 name 空/url 非法的项丢弃,这里只消费 block['quickLinks']=[{name,url}](括号访问,坑②) -->
|
||||
<section class="sp-section sp-container" th:fragment="quick_links(block)" aria-label="快捷通道">
|
||||
<div class="sp-block sp-block--flow">
|
||||
<div class="sp-block-head sp-block-head--bar">
|
||||
<h2 th:text="${block.title}">快捷通道</h2>
|
||||
</div>
|
||||
<ul class="sp-quick-grid" th:if="${!#lists.isEmpty(block['quickLinks'])}">
|
||||
<li th:each="l : ${block['quickLinks']}">
|
||||
<a class="sp-quick-link" th:href="${l.url}" th:text="${l.name}">入口名</a>
|
||||
</li>
|
||||
</ul>
|
||||
<th:block th:if="${#lists.isEmpty(block['quickLinks'])}">
|
||||
<div th:replace="~{'themes/scholar-blue/fragments/home-blocks' :: empty(${block})}"></div>
|
||||
</th:block>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 布局模式:公告条(notice_bar,手册 §2.8 类型 B)——单条手填通告,非滚动非文章列表。
|
||||
noticeLevel 渲染端已归一为 info|warn;noticeUrl 非法时为空串 → 退化成纯文本 -->
|
||||
<div class="sp-notice-bar" th:fragment="notice_bar(block)" role="note"
|
||||
th:if="${!#strings.isEmpty(block['noticeText'])}"
|
||||
th:classappend="${block['noticeLevel'] == 'warn' ? 'sp-notice--warn' : 'sp-notice--info'}">
|
||||
<div class="sp-container sp-notice-inner">
|
||||
<span class="sp-notice-tag" th:text="${block.title}">公告</span>
|
||||
<a class="sp-notice-link" th:if="${!#strings.isEmpty(block['noticeUrl'])}"
|
||||
th:href="${block['noticeUrl']}" th:text="${block['noticeText']}">公告正文</a>
|
||||
<span class="sp-notice-text" th:unless="${!#strings.isEmpty(block['noticeUrl'])}"
|
||||
th:text="${block['noticeText']}">公告正文</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空态(各形态引用时守卫必须包在 th:block 外层:th:replace 优先级 100 先于 th:if 300 求值) -->
|
||||
<div class="sp-empty" th:fragment="empty(block)">
|
||||
<p>栏目暂无内容</p>
|
||||
<p class="sp-empty-hint">内容发布后将在此展示</p>
|
||||
</div>
|
||||
@@ -0,0 +1,134 @@
|
||||
<!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>
|
||||
|
||||
<!-- 事项指南页(事项库 §6.2 契约):item / attachments[] / relatedItems[] /
|
||||
faqCategoryUrl·noticeCategoryUrl·ruleCategoryUrl(可空)/ applyUrl(可空,空则不渲染"我要办理")。
|
||||
三个富文本字段已在保存期过 jsoup 白名单、渲染期补 URL 基准,此处允许 utext。
|
||||
svcUnavailable=true(P1-4 停机降级):只渲染骨架+空态,不渲染事项内容与"我要办理"。
|
||||
数据绑定与 default 同构,仅换皮(样式见本包 main.css 第 17 节) -->
|
||||
<main class="sp-page sp-container" id="main-content">
|
||||
<div class="sp-page-card sp-svc-guide">
|
||||
<nav class="sp-crumb" aria-label="面包屑">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span class="sp-crumb-sep" aria-hidden="true">/</span>
|
||||
<a th:href="${url.serviceList()}">服务中心</a>
|
||||
<span class="sp-crumb-sep" aria-hidden="true">/</span>
|
||||
<span class="sp-crumb-current" th:text="${svcUnavailable} ? '服务中心' : ${item.title}">事项名称</span>
|
||||
</nav>
|
||||
|
||||
<!-- P1-4 停机空态:svc-hall 不可达时降级渲染(200),不出 500 错误页 -->
|
||||
<div class="sp-empty sp-svc-unavailable" th:if="${svcUnavailable}">
|
||||
<p>事项服务暂时不可用,请稍后再试</p>
|
||||
<p class="sp-empty-hint">事项数据服务暂不可达,稍后刷新即可恢复</p>
|
||||
</div>
|
||||
|
||||
<article class="sp-svc-guide-body" th:if="${!svcUnavailable}">
|
||||
<h1 class="sp-article-title" th:text="${item.title}">事项名称</h1>
|
||||
<div class="sp-article-meta">
|
||||
<span th:if="${!#strings.isEmpty(item.deptName)}"
|
||||
th:text="${'办理部门:' + item.deptName}">办理部门</span>
|
||||
<span th:text="${item.handleMode == 1 ? '线上办理' : (item.handleMode == 2 ? '线下办理' : '线上线下均可办理')}">办理方式</span>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${'发布时间:' + #temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
<span th:text="${'浏览量:' + item.viewCount}">浏览量</span>
|
||||
</div>
|
||||
|
||||
<!-- 我要办理:三态(手填外链 > svc_hall_base 生成 > 不渲染),按钮外链新窗口打开 -->
|
||||
<div class="sp-svc-apply" th:if="${applyUrl != null}">
|
||||
<a class="sp-svc-apply-btn" th:href="${applyUrl}" target="_blank" rel="noopener">我要办理</a>
|
||||
<span class="sp-svc-apply-hint">进入服务大厅在线办理</span>
|
||||
</div>
|
||||
|
||||
<p class="sp-svc-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">简介</p>
|
||||
|
||||
<table class="sp-svc-info">
|
||||
<caption class="sp-visually-hidden">事项基本信息</caption>
|
||||
<tbody>
|
||||
<tr th:if="${!#strings.isEmpty(item.objectName)}">
|
||||
<th scope="row">服务对象</th>
|
||||
<td th:text="${item.objectName}">服务对象</td>
|
||||
</tr>
|
||||
<tr th:if="${!#strings.isEmpty(item.categoryName)}">
|
||||
<th scope="row">事项类别</th>
|
||||
<td th:text="${item.categoryName}">事项类别</td>
|
||||
</tr>
|
||||
<tr th:if="${!#strings.isEmpty(item.handlePlace)}">
|
||||
<th scope="row">办理地点</th>
|
||||
<td th:text="${item.handlePlace}">办理地点</td>
|
||||
</tr>
|
||||
<tr th:if="${!#strings.isEmpty(item.handleTimeLimit)}">
|
||||
<th scope="row">办理时限</th>
|
||||
<td th:text="${item.handleTimeLimit}">办理时限</td>
|
||||
</tr>
|
||||
<tr th:if="${!#strings.isEmpty(item.promiseTimeLimit)}">
|
||||
<th scope="row">承诺时限</th>
|
||||
<td th:text="${item.promiseTimeLimit}">承诺时限</td>
|
||||
</tr>
|
||||
<tr th:if="${!#strings.isEmpty(item.contactPhone)}">
|
||||
<th scope="row">咨询电话</th>
|
||||
<td th:text="${item.contactPhone}">咨询电话</td>
|
||||
</tr>
|
||||
<tr th:if="${!#strings.isEmpty(item.supervisePhone)}">
|
||||
<th scope="row">监督投诉</th>
|
||||
<td th:text="${item.supervisePhone}">监督投诉电话</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<section class="sp-svc-section" th:if="${!#strings.isEmpty(item.processHtml)}">
|
||||
<h2>办理流程</h2>
|
||||
<div class="sp-article-content" th:utext="${item.processHtml}">办理流程</div>
|
||||
</section>
|
||||
<section class="sp-svc-section" th:if="${!#strings.isEmpty(item.materialHtml)}">
|
||||
<h2>所需材料</h2>
|
||||
<div class="sp-article-content" th:utext="${item.materialHtml}">所需材料</div>
|
||||
</section>
|
||||
<section class="sp-svc-section" th:if="${!#strings.isEmpty(item.noticeHtml)}">
|
||||
<h2>注意事项</h2>
|
||||
<div class="sp-article-content" th:utext="${item.noticeHtml}">注意事项</div>
|
||||
</section>
|
||||
|
||||
<section class="sp-svc-section sp-svc-attachments" th:if="${!#lists.isEmpty(attachments)}">
|
||||
<h2>相关材料下载</h2>
|
||||
<ul>
|
||||
<li th:each="att : ${attachments}">
|
||||
<a th:href="${url.upload(att.webUrl)}" th:download
|
||||
th:text="${att.fileName}">材料文件</a>
|
||||
<span class="sp-svc-att-size" th:if="${att.fileSize != null}"
|
||||
th:text="${#numbers.formatDecimal(att.fileSize / 1024.0, 1, 1) + ' KB'}">12.3 KB</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<nav class="sp-svc-links" th:if="${faqCategoryUrl != null or noticeCategoryUrl != null or ruleCategoryUrl != null}">
|
||||
<a th:if="${faqCategoryUrl != null}" th:href="${faqCategoryUrl}">常见问题</a>
|
||||
<a th:if="${noticeCategoryUrl != null}" th:href="${noticeCategoryUrl}">公告信息</a>
|
||||
<a th:if="${ruleCategoryUrl != null}" th:href="${ruleCategoryUrl}">文件规定</a>
|
||||
</nav>
|
||||
</article>
|
||||
|
||||
<aside class="sp-svc-related" th:if="${!svcUnavailable and !#lists.isEmpty(relatedItems)}">
|
||||
<h2 class="sp-svc-related-title">相关服务</h2>
|
||||
<ul class="sp-svc-related-list">
|
||||
<li th:each="rel : ${relatedItems}">
|
||||
<a th:href="${rel.url}">
|
||||
<span class="sp-svc-related-name" th:text="${rel.title}">相关事项</span>
|
||||
<span class="sp-svc-related-meta">
|
||||
<span th:if="${!#strings.isEmpty(rel.deptName)}" th:text="${rel.deptName}">部门</span>
|
||||
<span class="sp-tag"
|
||||
th:text="${rel.handleMode == 1 ? '线上' : (rel.handleMode == 2 ? '线下' : '线上线下')}">方式</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -23,11 +23,31 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 首页布局模式(docs/16 §4.4):home_layout 落库后按区块序列渲染(common 兜底骨架,本主题不另设形态) -->
|
||||
<!-- 首页布局模式(docs/16 §4.4 · 手册 §A.2):home_layout 落库后按区块序列渲染。
|
||||
本包为 A 档:8 形态 + quick_links/notice_bar 全部自持(fragments/home-blocks.html),
|
||||
只有白名单外的未知类型才落 themes/common 兜底骨架(故 common 的 :: style 仍要引入)。
|
||||
注意坑④:th:replace(优先级 100)先于 th:case(300)执行——case 必须放父元素、replace 放子元素,
|
||||
否则未命中的 case 分支也会求值 fragment 表达式(缺键 Map 访问直接抛错整页 500)。 -->
|
||||
<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:switch="${block.type}">
|
||||
<th:block th:case="'article_list'">
|
||||
<th:block th:switch="${block.homeStyle}">
|
||||
<th:block th:case="'news_grid'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: news_grid(${block})}" /></th:block>
|
||||
<th:block th:case="'media_focus'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: media_focus(${block})}" /></th:block>
|
||||
<th:block th:case="'compact'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: compact(${block})}" /></th:block>
|
||||
<th:block th:case="'image_wall'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: image_wall(${block})}" /></th:block>
|
||||
<th:block th:case="*"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: card(${block})}" /></th:block>
|
||||
</th:block>
|
||||
</th:block>
|
||||
<th:block th:case="'ticker'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: ticker(${block})}" /></th:block>
|
||||
<th:block th:case="'gallery'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: gallery(${block})}" /></th:block>
|
||||
<th:block th:case="'links'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: links(${block})}" /></th:block>
|
||||
<th:block th:case="'quick_links'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: quick_links(${block})}" /></th:block>
|
||||
<th:block th:case="'notice_bar'"><th:block th:replace="~{${themeBase} + '/fragments/home-blocks' :: notice_bar(${block})}" /></th:block>
|
||||
<th:block th:case="*"><th:block th:replace="~{'themes/common/fragments/home-blocks' :: render(${block})}" /></th:block>
|
||||
</th:block>
|
||||
</th:block>
|
||||
</th:block>
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<!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>
|
||||
|
||||
<!-- 服务中心(事项库 §6.1 契约):serviceList / serviceFilters / activeFilters / q;
|
||||
筛选与分页 URL 全部后端预生成(facets[].url / pageUrls[].url),模板禁止手拼 query
|
||||
(数据绑定与 default 同构,仅换皮;样式见本包 main.css 第 17 节) -->
|
||||
<main class="sp-page sp-container" id="main-content">
|
||||
<div class="sp-page-card sp-svc-page">
|
||||
<nav class="sp-crumb" aria-label="面包屑">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span class="sp-crumb-sep" aria-hidden="true">/</span>
|
||||
<span class="sp-crumb-current">服务中心</span>
|
||||
</nav>
|
||||
|
||||
<h1 class="sp-page-title">服务中心</h1>
|
||||
|
||||
<!-- P1-4 停机空态:svc-hall 不可达时降级渲染(200),不出 500 错误页 -->
|
||||
<div class="sp-empty sp-svc-unavailable" th:if="${svcUnavailable}">
|
||||
<p>事项服务暂时不可用,请稍后再试</p>
|
||||
<p class="sp-empty-hint">事项数据服务暂不可达,稍后刷新即可恢复</p>
|
||||
</div>
|
||||
|
||||
<div class="sp-svc-filters"
|
||||
th:if="${!svcUnavailable and (!#lists.isEmpty(serviceFilters.objects) or !#lists.isEmpty(serviceFilters.categories) or !#lists.isEmpty(serviceFilters.depts))}">
|
||||
<div class="sp-svc-filter-group" th:if="${!#lists.isEmpty(serviceFilters.objects)}">
|
||||
<span class="sp-svc-filter-label">服务对象</span>
|
||||
<div class="sp-svc-filter-options">
|
||||
<a class="sp-chip" th:each="opt : ${serviceFilters.objects}"
|
||||
th:classappend="${opt.active} ? 'active'" th:href="${opt.url}">
|
||||
<span th:text="${opt.name}">对象</span><em th:text="${opt.count}">0</em>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sp-svc-filter-group" th:if="${!#lists.isEmpty(serviceFilters.categories)}">
|
||||
<span class="sp-svc-filter-label">事项类别</span>
|
||||
<div class="sp-svc-filter-options">
|
||||
<a class="sp-chip" th:each="opt : ${serviceFilters.categories}"
|
||||
th:classappend="${opt.active} ? 'active'" th:href="${opt.url}">
|
||||
<span th:text="${opt.name}">类别</span><em th:text="${opt.count}">0</em>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sp-svc-filter-group" th:if="${!#lists.isEmpty(serviceFilters.depts)}">
|
||||
<span class="sp-svc-filter-label">服务部门</span>
|
||||
<div class="sp-svc-filter-options">
|
||||
<a class="sp-chip" th:each="opt : ${serviceFilters.depts}"
|
||||
th:classappend="${opt.active} ? 'active'" th:href="${opt.url}">
|
||||
<span th:text="${opt.name}">部门</span><em th:text="${opt.count}">0</em>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sp-svc-toolbar" th:if="${!svcUnavailable}">
|
||||
<form class="sp-svc-search" th:action="${url.serviceList()}" method="get" role="search">
|
||||
<input type="search" name="q" th:value="${q}" placeholder="搜索事项名称 / 简介" maxlength="64">
|
||||
<button type="submit">搜索</button>
|
||||
</form>
|
||||
<a class="sp-svc-clear" th:if="${activeFilters.hasAny}" th:href="${activeFilters.clearUrl}">清除筛选</a>
|
||||
</div>
|
||||
|
||||
<ul class="sp-svc-list" th:if="${!svcUnavailable}">
|
||||
<li th:each="it : ${serviceList.items}">
|
||||
<a class="sp-svc-item" th:href="${url.serviceItem(it.id)}">
|
||||
<span class="sp-svc-item-cover" th:if="${it.coverImage != null}">
|
||||
<img th:src="${url.upload(it.coverImage)}" th:alt="${it.title}">
|
||||
</span>
|
||||
<span class="sp-svc-item-body">
|
||||
<span class="sp-svc-item-title">
|
||||
<span class="sp-svc-item-name" th:text="${it.title}">事项名称</span>
|
||||
<span class="sp-tag online" th:if="${it.onlineFlag == 1}">可在线办理</span>
|
||||
<span class="sp-tag"
|
||||
th:text="${it.handleMode == 1 ? '线上办理' : (it.handleMode == 2 ? '线下办理' : '线上线下')}">办理方式</span>
|
||||
</span>
|
||||
<span class="sp-svc-item-summary" th:if="${!#strings.isEmpty(it.summary)}"
|
||||
th:text="${it.summary}">简介</span>
|
||||
<span class="sp-svc-item-meta">
|
||||
<span th:if="${!#strings.isEmpty(it.deptName)}" th:text="${it.deptName}">部门</span>
|
||||
<span th:if="${!#strings.isEmpty(it.categoryName)}" th:text="${it.categoryName}">类别</span>
|
||||
<time th:if="${it.publishTime != null}"
|
||||
th:text="${#temporals.format(it.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="sp-empty" th:if="${!svcUnavailable and #lists.isEmpty(serviceList.items)}">
|
||||
<p th:if="${activeFilters.hasAny}">未找到符合条件的服务事项</p>
|
||||
<p th:if="${!activeFilters.hasAny}">服务中心暂未上架事项</p>
|
||||
<p class="sp-empty-hint" th:if="${activeFilters.hasAny}">换个筛选条件或关键词试试</p>
|
||||
<p class="sp-empty-hint" th:if="${!activeFilters.hasAny}">事项发布后将在此展示</p>
|
||||
</div>
|
||||
|
||||
<!-- 分页:URL 由后端预生成(携带当前筛选,pageUrls[].active 标记当前页);停机态 totalPages=0 自然隐藏 -->
|
||||
<nav class="sp-pagination" th:if="${!svcUnavailable and serviceList.totalPages > 1}" aria-label="分页">
|
||||
<a th:each="p : ${serviceList.pageUrls}"
|
||||
th:classappend="${p.active} ? 'active'"
|
||||
th:href="${p.url}"
|
||||
th:aria-label="${'第' + p.page + '页'}"
|
||||
th:aria-current="${p.active} ? 'page'"
|
||||
th:text="${p.page}">1</a>
|
||||
</nav>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,6 +17,11 @@
|
||||
{ "key": "textLight", "cssVar": "--sp-text-light", "label": "次要文字色", "type": "color", "default": "#777777" },
|
||||
{ "key": "bg", "cssVar": "--sp-bg", "label": "页面底色", "type": "color", "default": "#eff2f6" },
|
||||
{ "key": "cardBg", "cssVar": "--sp-card-bg", "label": "卡片底色", "type": "color", "default": "#ffffff" },
|
||||
{ "key": "border", "cssVar": "--sp-border", "label": "边框色", "type": "color", "default": "#e3e7ee" }
|
||||
{ "key": "border", "cssVar": "--sp-border", "label": "边框色", "type": "color", "default": "#e3e7ee" },
|
||||
{ "key": "headerStyle", "label": "头部样式", "type": "select", "default": "banner",
|
||||
"options": [
|
||||
{ "value": "banner", "label": "深蓝顶栏+品牌行+白底导航(完整学院派)" },
|
||||
{ "value": "plain", "label": "简洁顶栏(收起深蓝细条、压低品牌行)" }
|
||||
] }
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user