init local code
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# ===== 单测环境配置(覆盖 src/main/resources/application.yml)=====
|
||||
# 目标:本地 MySQL 8.0.45(JSON 列原生类型,与 schema-cms-test.sql / cms_init.sql 一致)+ 本地 Redis 6379
|
||||
# schema 初始化在 JPA EntityManagerFactory 之前执行(Boot 2.7 SQL init 时序),
|
||||
# 表已存在时 hibernate ddl-auto=update 不会重建/改列。
|
||||
server:
|
||||
port: 0
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: cms
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/bsch_cms_test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
sql:
|
||||
init:
|
||||
mode: always
|
||||
schema-locations: classpath:schema-cms-test.sql
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: false
|
||||
open-in-view: false
|
||||
properties:
|
||||
hibernate:
|
||||
jdbc:
|
||||
time_zone: Asia/Shanghai
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: Asia/Shanghai
|
||||
thymeleaf:
|
||||
cache: true
|
||||
prefix: classpath:/templates/
|
||||
suffix: .html
|
||||
encoding: UTF-8
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 100MB
|
||||
max-request-size: 100MB
|
||||
cache:
|
||||
type: caffeine
|
||||
caffeine:
|
||||
spec: maximumSize=10000,expireAfterWrite=5m
|
||||
|
||||
yh:
|
||||
auth:
|
||||
enabled: true
|
||||
sso:
|
||||
server-url-prefix: http://sso.yunhaixingtong.com/cas/
|
||||
client-host-url: http://localhost:8072/cms
|
||||
allow-headers: all
|
||||
allow-origins: all
|
||||
url-pattern: /api/*
|
||||
mobi-api-pattern: /mobiapi/*
|
||||
app-auth:
|
||||
enabled: true
|
||||
app-key: 100XXX
|
||||
app-secret: xxxxxxxx
|
||||
open:
|
||||
enabled: true
|
||||
base-host: http://base.yunhaixingtong.com
|
||||
base-path: /userbase
|
||||
app-key: 100XXX
|
||||
app-secret: xxxxxxxx
|
||||
|
||||
cms:
|
||||
# 超管判定:BoaUser.type == 1(夹具 TestUsers.superAdmin 已对齐);校级角色经 BoaRoleInfo.schId 匹配
|
||||
portal:
|
||||
detail-max-age: 300
|
||||
|
||||
# 上传落到工程 target 目录,测试后随 mvn clean 清除
|
||||
FILE_PATH: ./target/test-upload/
|
||||
FILE_WEB_PATH: /cms/upload/
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.yunhai.cms: info
|
||||
org.hibernate.SQL: warn
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health
|
||||
endpoint:
|
||||
health:
|
||||
show-details: never
|
||||
|
||||
# 本地 Redis(单测 Redis 演进用例连接参数)
|
||||
cms.test.redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: 123456
|
||||
@@ -0,0 +1,134 @@
|
||||
-- ============================================================
|
||||
-- bsch_cms_test 单测库表结构(每次 Spring 上下文启动先 DROP 重建,保证干净起点)
|
||||
-- 以 src/main/resources/sql/cms_init.sql 为蓝本;
|
||||
-- 2026-09-05 起本机为 MySQL 8.0.45,JSON 列恢复原生类型(与 cms_init.sql / 实体 columnDefinition 一致)
|
||||
-- ============================================================
|
||||
|
||||
DROP TABLE IF EXISTS `cms_attachment`;
|
||||
DROP TABLE IF EXISTS `cms_article`;
|
||||
DROP TABLE IF EXISTS `cms_category`;
|
||||
DROP TABLE IF EXISTS `cms_banner`;
|
||||
DROP TABLE IF EXISTS `cms_link`;
|
||||
DROP TABLE IF EXISTS `cms_site`;
|
||||
|
||||
CREATE TABLE `cms_site` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`code` VARCHAR(64) NOT NULL COMMENT '站点编码,URL 路径用(/portal/{code})',
|
||||
`parent_site_id` INT NOT NULL DEFAULT 0 COMMENT '上级站点ID(0=顶层校站点;院系子站挂主站 id)',
|
||||
`sch_id` INT NULL COMMENT '学校ID(逻辑外键 → UserBase sys_group.id)',
|
||||
`sch_name` VARCHAR(128) NULL COMMENT '学校名称(冗余展示)',
|
||||
`name` VARCHAR(128) NOT NULL COMMENT '站点名称',
|
||||
`theme` VARCHAR(64) NULL DEFAULT 'default' COMMENT '皮肤包名',
|
||||
`config_json` JSON NULL COMMENT '站点配置(logo/联系方式/页脚等;预留 wechat 节点)',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=停用 1=启用',
|
||||
`create_by` VARCHAR(64) NULL,
|
||||
`create_time` DATETIME NULL,
|
||||
`update_by` VARCHAR(64) NULL,
|
||||
`update_time` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_site_code` (`code`),
|
||||
KEY `idx_site_sch` (`sch_id`),
|
||||
KEY `idx_site_parent` (`parent_site_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'CMS 站点';
|
||||
|
||||
CREATE TABLE `cms_category` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`site_id` INT NOT NULL COMMENT '所属站点',
|
||||
`parent_id` INT NOT NULL DEFAULT 0 COMMENT '父栏目ID(0=根)',
|
||||
`name` VARCHAR(64) NOT NULL COMMENT '栏目名称',
|
||||
`type` TINYINT NOT NULL DEFAULT 1 COMMENT '栏目类型:1=文章列表 2=单页 3=外链',
|
||||
`link_url` VARCHAR(500) NULL COMMENT '外链地址(type=3 必填)',
|
||||
`template` VARCHAR(64) NULL COMMENT '栏目模板覆盖',
|
||||
`content` LONGTEXT NULL COMMENT '单页栏目内容',
|
||||
`sort_no` INT NOT NULL DEFAULT 0 COMMENT '同级排序(升序)',
|
||||
`seo_title` VARCHAR(200) NULL COMMENT 'SEO 标题',
|
||||
`seo_description` VARCHAR(500) NULL COMMENT 'SEO 描述',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '0=隐藏 1=显示',
|
||||
`create_by` VARCHAR(64) NULL,
|
||||
`create_time` DATETIME NULL,
|
||||
`update_by` VARCHAR(64) NULL,
|
||||
`update_time` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_cat_tree` (`site_id`, `parent_id`, `sort_no`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'CMS 栏目';
|
||||
|
||||
CREATE TABLE `cms_article` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`site_id` INT NOT NULL COMMENT '所属站点',
|
||||
`category_id` INT NOT NULL COMMENT '所属栏目',
|
||||
`title` VARCHAR(200) NOT NULL COMMENT '标题',
|
||||
`summary` VARCHAR(500) NULL COMMENT '摘要',
|
||||
`content` LONGTEXT NULL COMMENT '正文 HTML',
|
||||
`cover_image` VARCHAR(500) NULL COMMENT '封面图 URL',
|
||||
`author_account` VARCHAR(64) NULL COMMENT '作者账号',
|
||||
`author_name` VARCHAR(64) NULL COMMENT '作者姓名',
|
||||
`source` VARCHAR(128) NULL COMMENT '来源',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0=草稿 1=已发布 2=已下架',
|
||||
`topmost` TINYINT NOT NULL DEFAULT 0 COMMENT '置顶:0/1',
|
||||
`publish_time` DATETIME NULL COMMENT '发布时间',
|
||||
`offline_time` DATETIME NULL COMMENT '下架时间',
|
||||
`view_count` INT NOT NULL DEFAULT 0 COMMENT '浏览量',
|
||||
`ext_json` JSON NULL COMMENT '扩展字段(保留键:wechat=推送回执预留、albums=图集演进)',
|
||||
`create_by` VARCHAR(64) NULL,
|
||||
`create_time` DATETIME NULL,
|
||||
`update_by` VARCHAR(64) NULL,
|
||||
`update_time` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_art_list` (`site_id`, `category_id`, `status`, `publish_time`),
|
||||
KEY `idx_art_site_top` (`site_id`, `status`, `topmost`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'CMS 文章';
|
||||
|
||||
CREATE TABLE `cms_attachment` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`site_id` INT NOT NULL COMMENT '所属站点',
|
||||
`article_id` INT NULL COMMENT '关联文章',
|
||||
`file_name` VARCHAR(255) NOT NULL COMMENT '原始文件名',
|
||||
`file_path` VARCHAR(500) NOT NULL COMMENT '存储路径',
|
||||
`web_url` VARCHAR(500) NOT NULL COMMENT 'Web 访问 URL',
|
||||
`file_size` BIGINT NULL COMMENT '字节数',
|
||||
`mime_type` VARCHAR(128) NULL COMMENT 'MIME 类型',
|
||||
`create_by` VARCHAR(64) NULL,
|
||||
`create_time` DATETIME NULL,
|
||||
`update_by` VARCHAR(64) NULL,
|
||||
`update_time` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_att_article` (`article_id`),
|
||||
KEY `idx_att_site` (`site_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'CMS 附件';
|
||||
|
||||
CREATE TABLE `cms_banner` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`site_id` INT NOT NULL COMMENT '所属站点',
|
||||
`position` VARCHAR(32) NOT NULL DEFAULT 'home' COMMENT '展示位标识',
|
||||
`title` VARCHAR(200) NOT NULL COMMENT '标题',
|
||||
`image_url` VARCHAR(500) NOT NULL COMMENT '图片 URL',
|
||||
`link_type` TINYINT NOT NULL DEFAULT 1 COMMENT '跳转类型:1=文章 2=栏目 3=外链',
|
||||
`link_value` VARCHAR(500) NULL COMMENT '跳转目标',
|
||||
`sort_no` INT NOT NULL DEFAULT 0 COMMENT '同级排序(升序)',
|
||||
`start_time` DATETIME NULL COMMENT '上线时间',
|
||||
`end_time` DATETIME NULL COMMENT '下线时间',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '0=停用 1=启用',
|
||||
`create_by` VARCHAR(64) NULL,
|
||||
`create_time` DATETIME NULL,
|
||||
`update_by` VARCHAR(64) NULL,
|
||||
`update_time` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_banner` (`site_id`, `position`, `status`, `sort_no`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'CMS 轮播/推荐位';
|
||||
|
||||
CREATE TABLE `cms_link` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`site_id` INT NOT NULL COMMENT '所属站点',
|
||||
`group_name` VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '分组',
|
||||
`name` VARCHAR(128) NOT NULL COMMENT '链接名称',
|
||||
`logo_url` VARCHAR(500) NULL COMMENT 'Logo URL',
|
||||
`link_url` VARCHAR(500) NOT NULL COMMENT '链接地址',
|
||||
`sort_no` INT NOT NULL DEFAULT 0 COMMENT '组内排序',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '0=隐藏 1=显示',
|
||||
`create_by` VARCHAR(64) NULL,
|
||||
`create_time` DATETIME NULL,
|
||||
`update_by` VARCHAR(64) NULL,
|
||||
`update_time` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_link` (`site_id`, `group_name`, `sort_no`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'CMS 友情链接';
|
||||
@@ -0,0 +1,324 @@
|
||||
/* ============================================================
|
||||
* default 皮肤包样式(方案 §8.2)
|
||||
* 换肤 = 拷贝 default 目录改 CSS 变量与布局,零后端代码。
|
||||
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪,D2 决策点确认)。
|
||||
* ============================================================ */
|
||||
:root {
|
||||
--primary: #1a5cb0;
|
||||
--primary-dark: #12447f;
|
||||
--text: #333;
|
||||
--text-light: #777;
|
||||
--bg: #f5f6f8;
|
||||
--card-bg: #fff;
|
||||
--border: #e5e6eb;
|
||||
--max-width: 1200px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
}
|
||||
a { color: inherit; text-decoration: none; }
|
||||
a:hover { color: var(--primary); }
|
||||
img { max-width: 100%; }
|
||||
time, .item-meta { color: var(--text-light); font-size: 13px; }
|
||||
.empty { color: var(--text-light); text-align: center; padding: 32px 0; }
|
||||
|
||||
/* ===== 头部 ===== */
|
||||
.site-header { background: var(--card-bg); border-bottom: 1px solid var(--border); }
|
||||
.header-inner {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 18px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
.site-brand { display: flex; align-items: center; gap: 12px; }
|
||||
.site-logo { height: 44px; }
|
||||
.site-name { font-size: 26px; font-weight: 700; color: var(--primary); }
|
||||
.site-search { display: flex; }
|
||||
.site-search input {
|
||||
border: 1px solid var(--border);
|
||||
border-right: none;
|
||||
padding: 8px 12px;
|
||||
width: 220px;
|
||||
outline: none;
|
||||
}
|
||||
.site-search button {
|
||||
border: none;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
padding: 8px 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ===== 专业院系下拉(院系子站切换,《院系子站方案》) ===== */
|
||||
/* 悬停展开白底纵向列表,右对齐 + 超高滚动(院系数十个子站场景) */
|
||||
.site-colleges { position: relative; }
|
||||
.site-colleges .colleges-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--text-light); }
|
||||
.site-colleges .colleges-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
min-width: 200px;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
list-style: none;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-top: 2px solid var(--primary);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
|
||||
z-index: 110;
|
||||
}
|
||||
.site-colleges:hover .colleges-menu { display: block; }
|
||||
.site-colleges .colleges-menu li a {
|
||||
display: block;
|
||||
padding: 9px 16px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.site-colleges .colleges-menu li a:hover { background: #f2f6fc; color: var(--primary); }
|
||||
.site-colleges .colleges-menu li.active a { color: var(--primary); font-weight: 600; }
|
||||
.site-nav { background: var(--primary); }
|
||||
.site-nav ul {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.site-nav li a {
|
||||
display: block;
|
||||
padding: 12px 22px;
|
||||
color: #fff;
|
||||
}
|
||||
.site-nav li:hover a, .site-nav li.active a { background: var(--primary-dark); }
|
||||
|
||||
/* ===== 二级下拉导航 ===== */
|
||||
/* 面板为白底纵向列表:需整体覆盖 .site-nav ul 的横向 flex 与内边距 */
|
||||
.site-nav > ul > li { position: relative; }
|
||||
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .85; }
|
||||
.site-nav ul.sub-nav {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 168px;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
list-style: none;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-top: 2px solid var(--primary);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
|
||||
z-index: 100;
|
||||
}
|
||||
.site-nav li:hover > .sub-nav { display: block; }
|
||||
.site-nav .sub-nav li a {
|
||||
padding: 9px 16px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
background: none;
|
||||
}
|
||||
.site-nav .sub-nav li a:hover { background: #f2f6fc; color: var(--primary); }
|
||||
.site-nav .sub-nav li.active a { color: var(--primary); font-weight: 600; }
|
||||
|
||||
/* ===== 首页轮播 ===== */
|
||||
.banner { position: relative; max-width: var(--max-width); margin: 16px auto; overflow: hidden; }
|
||||
.banner-slides { position: relative; height: 420px; }
|
||||
.banner-slide { display: none; position: absolute; inset: 0; }
|
||||
.banner-slide.active { display: block; }
|
||||
.banner-slide img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.banner-title {
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
padding: 12px 20px;
|
||||
color: #fff;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, .6));
|
||||
}
|
||||
.banner-dots {
|
||||
position: absolute;
|
||||
bottom: 12px; right: 20px;
|
||||
display: flex; gap: 8px;
|
||||
}
|
||||
.banner-dots button {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, .6);
|
||||
cursor: pointer;
|
||||
}
|
||||
.banner-dots button.active { background: #fff; }
|
||||
|
||||
/* ===== 通用区块 ===== */
|
||||
.top-strip, .category-block {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 20px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.section-title, .block-head h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 20px;
|
||||
border-left: 4px solid var(--primary);
|
||||
padding-left: 10px;
|
||||
}
|
||||
.block-head { display: flex; align-items: baseline; justify-content: space-between; }
|
||||
.more { font-size: 13px; color: var(--text-light); }
|
||||
.top-strip ul, .block-list { list-style: none; margin: 0; padding: 0; }
|
||||
.top-strip li, .block-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 9px 0;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
}
|
||||
.top-strip li:last-child, .block-list li:last-child { border-bottom: none; }
|
||||
.top-strip li a, .block-list li a {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.top-badge {
|
||||
flex: none;
|
||||
color: #fff;
|
||||
background: var(--primary);
|
||||
font-size: 12px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ===== 列表页 ===== */
|
||||
.list-main {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 24px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.breadcrumb { color: var(--text-light); font-size: 13px; margin-bottom: 16px; }
|
||||
.breadcrumb a { margin-right: 8px; }
|
||||
.breadcrumb span { margin-left: 8px; }
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
border-left: 4px solid var(--primary);
|
||||
padding-left: 10px;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
.article-list { list-style: none; margin: 0; padding: 0; }
|
||||
.article-list li { padding: 14px 0; border-bottom: 1px dashed var(--border); }
|
||||
.item-title { font-size: 17px; }
|
||||
.item-summary {
|
||||
color: var(--text-light);
|
||||
font-size: 14px;
|
||||
margin: 6px 0;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.item-meta { display: flex; gap: 10px; align-items: center; }
|
||||
|
||||
/* ===== 分页 ===== */
|
||||
.pagination { display: flex; gap: 6px; justify-content: center; padding: 24px 0 4px; }
|
||||
.pagination a {
|
||||
min-width: 34px;
|
||||
text-align: center;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card-bg);
|
||||
}
|
||||
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
|
||||
|
||||
/* ===== 详情/单页 ===== */
|
||||
.detail-main {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 32px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.article-title { margin: 8px 0 12px; font-size: 26px; text-align: center; }
|
||||
.article-meta {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
color: var(--text-light);
|
||||
font-size: 13px;
|
||||
padding-bottom: 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.article-content { line-height: 1.9; font-size: 16px; padding-top: 20px; word-break: break-word; }
|
||||
.article-content img { height: auto; }
|
||||
.article-content table { border-collapse: collapse; width: 100%; }
|
||||
.article-content table td, .article-content table th { border: 1px solid var(--border); padding: 6px 10px; }
|
||||
.article-sibling { display: flex; flex-direction: column; gap: 8px; margin-top: 28px; font-size: 14px; }
|
||||
|
||||
/* ===== 页脚 ===== */
|
||||
.site-footer { margin-top: 32px; background: var(--primary-dark); color: rgba(255, 255, 255, .85); }
|
||||
.footer-links {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 18px 16px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.links-group { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; font-size: 13px; }
|
||||
.group-name { font-weight: 600; margin-right: 4px; }
|
||||
.footer-info {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 12px 16px 20px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.footer-info p { margin: 4px 0; }
|
||||
|
||||
/* ===== 移动端适配 ===== */
|
||||
@media (max-width: 768px) {
|
||||
.header-inner { flex-direction: column; align-items: stretch; }
|
||||
.site-search input { flex: 1; width: auto; }
|
||||
.site-colleges { align-self: flex-start; }
|
||||
.site-colleges .colleges-menu { left: 0; right: auto; min-width: 100%; }
|
||||
.banner-slides { height: 200px; }
|
||||
.detail-main, .list-main { padding: 18px 14px; }
|
||||
.article-title { font-size: 22px; }
|
||||
/* 触屏无 hover:子菜单静态平铺(缩进列表),不做浮层 */
|
||||
.site-nav ul.sub-nav {
|
||||
display: block;
|
||||
position: static;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
min-width: 0;
|
||||
padding: 0 0 6px;
|
||||
}
|
||||
.site-nav .sub-nav li a { color: rgba(255, 255, 255, .9); padding: 8px 16px 8px 34px; }
|
||||
.site-nav .sub-nav li a:hover { background: var(--primary-dark); color: #fff; }
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="detail-main">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<a th:if="${currentCategoryId != null}" th:href="${url.category(currentCategoryId)}">栏目</a>
|
||||
<span>正文</span>
|
||||
</nav>
|
||||
|
||||
<article class="article">
|
||||
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
|
||||
<div class="article-meta">
|
||||
<time th:if="${article.publishTime != null}"
|
||||
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
|
||||
<span th:if="${!#strings.isEmpty(article.authorName)}"
|
||||
th:text="'作者:' + ${article.authorName}">作者</span>
|
||||
<span th:if="${!#strings.isEmpty(article.source)}"
|
||||
th:text="'来源:' + ${article.source}">来源</span>
|
||||
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
|
||||
</div>
|
||||
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
|
||||
其余字段一律 th:text 转义输出 -->
|
||||
<div class="article-content" th:utext="${article.content}">正文</div>
|
||||
</article>
|
||||
|
||||
<nav class="article-sibling">
|
||||
<a th:if="${prevArticle != null}" class="prev"
|
||||
th:href="${url.article(prevArticle.id)}"
|
||||
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
|
||||
<a th:if="${nextArticle != null}" class="next"
|
||||
th:href="${url.article(nextArticle.id)}"
|
||||
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<!-- 公共页脚:联系信息 + 友情链接(分组渲染,方案 §8.1:友链不设独立路由) -->
|
||||
<footer class="site-footer" th:fragment="footer">
|
||||
<div class="footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
|
||||
<div class="links-group" th:each="group : ${site.linkGroups}">
|
||||
<span class="group-name" th:text="${group.groupName}">分组</span>
|
||||
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${link.name}">链接</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-info">
|
||||
<p th:if="${site.config['contact'] != null}" th:text="${site.config['contact']}">联系方式</p>
|
||||
<p th:if="${site.config['copyright'] != null}" th:text="${site.config['copyright']}">版权信息</p>
|
||||
<p th:if="${site.config['beian'] != null}" th:text="${site.config['beian']}">备案号</p>
|
||||
</div>
|
||||
</footer>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<!-- 公共 head 片段:SEO(title/description/canonical)+ 主题资产(方案 §8.3/§8.6) -->
|
||||
<head th:fragment="head">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title th:text="${pageTitle}">页面标题</title>
|
||||
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
|
||||
th:content="${seoDescription}">
|
||||
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
|
||||
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
|
||||
<link th:if="${site.config['favicon'] != null}" rel="icon" th:href="${site.config['favicon']}">
|
||||
</head>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<!-- 公共头部:站点名 + 栏目导航 + 站内搜索 + 专业院系下拉(院系子站切换,《院系子站方案》) -->
|
||||
<header class="site-header" th:fragment="header">
|
||||
<div class="header-inner">
|
||||
<div class="site-brand">
|
||||
<img th:if="${site.config['logo'] != null}" class="site-logo"
|
||||
th:src="${site.config['logo']}" th:alt="${site.name}">
|
||||
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
|
||||
</div>
|
||||
<form class="site-search" th:action="${url.search('', null)}" method="get">
|
||||
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
|
||||
<button type="submit">搜索</button>
|
||||
</form>
|
||||
<!-- 专业院系下拉:顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 config_json.colleges_label 覆盖 -->
|
||||
<div class="site-colleges" th:if="${!site.colleges.isEmpty()}"
|
||||
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
|
||||
<span class="colleges-toggle">
|
||||
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
|
||||
<i class="colleges-arrow">▾</i>
|
||||
</span>
|
||||
<ul class="colleges-menu">
|
||||
<li th:each="col : ${site.colleges}" th:classappend="${col.code == site.code} ? 'active'">
|
||||
<a th:href="${url.siteHome(col.code, col.publicBaseUrl)}"
|
||||
th:title="${col.name}" th:text="${col.name}">学院名称</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="site-nav">
|
||||
<ul>
|
||||
<li th:classappend="${currentCategoryId == null} ? 'active'">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
</li>
|
||||
<li th:each="cat : ${navCategories}"
|
||||
th:classappend="${(currentCategoryId == cat.id ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
|
||||
<!-- 外链栏目直接外跳;其余进栏目页(方案 §8.1) -->
|
||||
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${cat.name}">栏目</a>
|
||||
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}" th:text="${cat.name}">栏目</a>
|
||||
<!-- 二级下拉:悬停展开(样式见 assets/css/main.css .sub-nav) -->
|
||||
<ul class="sub-nav" th:if="${!cat.children.isEmpty()}">
|
||||
<li th:each="sub : ${cat.children}">
|
||||
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${sub.name}">子栏目</a>
|
||||
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="home-main">
|
||||
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
|
||||
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
|
||||
<div class="banner-slides">
|
||||
<a class="banner-slide" th:each="b, stat : ${banners}"
|
||||
th:classappend="${stat.first} ? 'active'"
|
||||
th:href="${b.linkUrl}" th:title="${b.title}">
|
||||
<img th:src="${b.imageUrl}" th:alt="${b.title}">
|
||||
<span class="banner-title" th:text="${b.title}">标题</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
|
||||
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
|
||||
th:aria-label="${'第' + (stat.count) + '张'}"></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 置顶/头条区(cms_article topmost 优先) -->
|
||||
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
|
||||
<h2 class="section-title">头条推荐</h2>
|
||||
<ul>
|
||||
<li th:each="item : ${topArticles}">
|
||||
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 一级栏目区块(每栏目 top 8) -->
|
||||
<section class="category-block" th:each="block : ${categoryBlocks}">
|
||||
<div class="block-head">
|
||||
<h2 th:text="${block.category.name}">栏目名</h2>
|
||||
<a class="more" th:href="${block.moreUrl}">更多 ></a>
|
||||
</div>
|
||||
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
|
||||
<li th:each="item : ${block.items}">
|
||||
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="list-main">
|
||||
<nav class="breadcrumb" th:if="${searchMode != true}">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span th:text="${category.name}">栏目</span>
|
||||
</nav>
|
||||
<nav class="breadcrumb" th:if="${searchMode == true}">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span>搜索结果</span>
|
||||
</nav>
|
||||
|
||||
<h1 class="page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
|
||||
<h1 class="page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
|
||||
|
||||
<ul class="article-list">
|
||||
<li th:each="item : ${listData.items}">
|
||||
<a class="item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<p class="item-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">摘要</p>
|
||||
<div class="item-meta">
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="empty" th:if="${#lists.isEmpty(listData.items)}">暂无内容</p>
|
||||
|
||||
<!-- 分页(列表页/搜索页共用) -->
|
||||
<nav class="pagination" th:if="${listData.totalPages > 1}"
|
||||
th:with="cur=${listData.page}, total=${listData.totalPages}">
|
||||
<a th:if="${cur > 1}"
|
||||
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}">上一页</a>
|
||||
<a th:each="p : ${#numbers.sequence(1, total)}"
|
||||
th:classappend="${p == cur} ? 'active'"
|
||||
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
|
||||
th:text="${p}">1</a>
|
||||
<a th:if="${cur < total}"
|
||||
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}">下一页</a>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="detail-main">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span th:text="${category.name}">栏目</span>
|
||||
</nav>
|
||||
|
||||
<article class="article">
|
||||
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
|
||||
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
|
||||
<div class="article-content" th:utext="${category.content}">单页内容</div>
|
||||
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"specVersion": 1,
|
||||
"name": "t-bad-cssvar",
|
||||
"title": "cssVar 出现在非 color 参数(夹具)",
|
||||
"version": "1.0.0",
|
||||
"screenshot": "assets/screenshot.png",
|
||||
"params": [
|
||||
{ "key": "footText", "cssVar": "--foot-text", "label": "cssVar 用在 text 类型上", "type": "text", "default": "x" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
/* ============================================================
|
||||
* default 皮肤包样式(方案 §8.2)
|
||||
* 换肤 = 拷贝 default 目录改 CSS 变量与布局,零后端代码。
|
||||
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪,D2 决策点确认)。
|
||||
* ============================================================ */
|
||||
:root {
|
||||
--primary: #1a5cb0;
|
||||
--primary-dark: #12447f;
|
||||
--text: #333;
|
||||
--text-light: #777;
|
||||
--bg: #f5f6f8;
|
||||
--card-bg: #fff;
|
||||
--border: #e5e6eb;
|
||||
--max-width: 1200px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
}
|
||||
a { color: inherit; text-decoration: none; }
|
||||
a:hover { color: var(--primary); }
|
||||
img { max-width: 100%; }
|
||||
time, .item-meta { color: var(--text-light); font-size: 13px; }
|
||||
.empty { color: var(--text-light); text-align: center; padding: 32px 0; }
|
||||
|
||||
/* ===== 头部 ===== */
|
||||
.site-header { background: var(--card-bg); border-bottom: 1px solid var(--border); }
|
||||
.header-inner {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 18px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
.site-brand { display: flex; align-items: center; gap: 12px; }
|
||||
.site-logo { height: 44px; }
|
||||
.site-name { font-size: 26px; font-weight: 700; color: var(--primary); }
|
||||
.site-search { display: flex; }
|
||||
.site-search input {
|
||||
border: 1px solid var(--border);
|
||||
border-right: none;
|
||||
padding: 8px 12px;
|
||||
width: 220px;
|
||||
outline: none;
|
||||
}
|
||||
.site-search button {
|
||||
border: none;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
padding: 8px 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ===== 专业院系下拉(院系子站切换,《院系子站方案》) ===== */
|
||||
/* 悬停展开白底纵向列表,右对齐 + 超高滚动(院系数十个子站场景) */
|
||||
.site-colleges { position: relative; }
|
||||
.site-colleges .colleges-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--text-light); }
|
||||
.site-colleges .colleges-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
min-width: 200px;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
list-style: none;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-top: 2px solid var(--primary);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
|
||||
z-index: 110;
|
||||
}
|
||||
.site-colleges:hover .colleges-menu { display: block; }
|
||||
.site-colleges .colleges-menu li a {
|
||||
display: block;
|
||||
padding: 9px 16px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.site-colleges .colleges-menu li a:hover { background: #f2f6fc; color: var(--primary); }
|
||||
.site-colleges .colleges-menu li.active a { color: var(--primary); font-weight: 600; }
|
||||
.site-nav { background: var(--primary); }
|
||||
.site-nav ul {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.site-nav li a {
|
||||
display: block;
|
||||
padding: 12px 22px;
|
||||
color: #fff;
|
||||
}
|
||||
.site-nav li:hover a, .site-nav li.active a { background: var(--primary-dark); }
|
||||
|
||||
/* ===== 二级下拉导航 ===== */
|
||||
/* 面板为白底纵向列表:需整体覆盖 .site-nav ul 的横向 flex 与内边距 */
|
||||
.site-nav > ul > li { position: relative; }
|
||||
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .85; }
|
||||
.site-nav ul.sub-nav {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 168px;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
list-style: none;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-top: 2px solid var(--primary);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
|
||||
z-index: 100;
|
||||
}
|
||||
.site-nav li:hover > .sub-nav { display: block; }
|
||||
.site-nav .sub-nav li a {
|
||||
padding: 9px 16px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
background: none;
|
||||
}
|
||||
.site-nav .sub-nav li a:hover { background: #f2f6fc; color: var(--primary); }
|
||||
.site-nav .sub-nav li.active a { color: var(--primary); font-weight: 600; }
|
||||
|
||||
/* ===== 首页轮播 ===== */
|
||||
.banner { position: relative; max-width: var(--max-width); margin: 16px auto; overflow: hidden; }
|
||||
.banner-slides { position: relative; height: 420px; }
|
||||
.banner-slide { display: none; position: absolute; inset: 0; }
|
||||
.banner-slide.active { display: block; }
|
||||
.banner-slide img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.banner-title {
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
padding: 12px 20px;
|
||||
color: #fff;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, .6));
|
||||
}
|
||||
.banner-dots {
|
||||
position: absolute;
|
||||
bottom: 12px; right: 20px;
|
||||
display: flex; gap: 8px;
|
||||
}
|
||||
.banner-dots button {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, .6);
|
||||
cursor: pointer;
|
||||
}
|
||||
.banner-dots button.active { background: #fff; }
|
||||
|
||||
/* ===== 通用区块 ===== */
|
||||
.top-strip, .category-block {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 20px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.section-title, .block-head h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 20px;
|
||||
border-left: 4px solid var(--primary);
|
||||
padding-left: 10px;
|
||||
}
|
||||
.block-head { display: flex; align-items: baseline; justify-content: space-between; }
|
||||
.more { font-size: 13px; color: var(--text-light); }
|
||||
.top-strip ul, .block-list { list-style: none; margin: 0; padding: 0; }
|
||||
.top-strip li, .block-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 9px 0;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
}
|
||||
.top-strip li:last-child, .block-list li:last-child { border-bottom: none; }
|
||||
.top-strip li a, .block-list li a {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.top-badge {
|
||||
flex: none;
|
||||
color: #fff;
|
||||
background: var(--primary);
|
||||
font-size: 12px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ===== 列表页 ===== */
|
||||
.list-main {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 24px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.breadcrumb { color: var(--text-light); font-size: 13px; margin-bottom: 16px; }
|
||||
.breadcrumb a { margin-right: 8px; }
|
||||
.breadcrumb span { margin-left: 8px; }
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
border-left: 4px solid var(--primary);
|
||||
padding-left: 10px;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
.article-list { list-style: none; margin: 0; padding: 0; }
|
||||
.article-list li { padding: 14px 0; border-bottom: 1px dashed var(--border); }
|
||||
.item-title { font-size: 17px; }
|
||||
.item-summary {
|
||||
color: var(--text-light);
|
||||
font-size: 14px;
|
||||
margin: 6px 0;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.item-meta { display: flex; gap: 10px; align-items: center; }
|
||||
|
||||
/* ===== 分页 ===== */
|
||||
.pagination { display: flex; gap: 6px; justify-content: center; padding: 24px 0 4px; }
|
||||
.pagination a {
|
||||
min-width: 34px;
|
||||
text-align: center;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card-bg);
|
||||
}
|
||||
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
|
||||
|
||||
/* ===== 详情/单页 ===== */
|
||||
.detail-main {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 32px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.article-title { margin: 8px 0 12px; font-size: 26px; text-align: center; }
|
||||
.article-meta {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
color: var(--text-light);
|
||||
font-size: 13px;
|
||||
padding-bottom: 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.article-content { line-height: 1.9; font-size: 16px; padding-top: 20px; word-break: break-word; }
|
||||
.article-content img { height: auto; }
|
||||
.article-content table { border-collapse: collapse; width: 100%; }
|
||||
.article-content table td, .article-content table th { border: 1px solid var(--border); padding: 6px 10px; }
|
||||
.article-sibling { display: flex; flex-direction: column; gap: 8px; margin-top: 28px; font-size: 14px; }
|
||||
|
||||
/* ===== 页脚 ===== */
|
||||
.site-footer { margin-top: 32px; background: var(--primary-dark); color: rgba(255, 255, 255, .85); }
|
||||
.footer-links {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 18px 16px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.links-group { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; font-size: 13px; }
|
||||
.group-name { font-weight: 600; margin-right: 4px; }
|
||||
.footer-info {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 12px 16px 20px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.footer-info p { margin: 4px 0; }
|
||||
|
||||
/* ===== 移动端适配 ===== */
|
||||
@media (max-width: 768px) {
|
||||
.header-inner { flex-direction: column; align-items: stretch; }
|
||||
.site-search input { flex: 1; width: auto; }
|
||||
.site-colleges { align-self: flex-start; }
|
||||
.site-colleges .colleges-menu { left: 0; right: auto; min-width: 100%; }
|
||||
.banner-slides { height: 200px; }
|
||||
.detail-main, .list-main { padding: 18px 14px; }
|
||||
.article-title { font-size: 22px; }
|
||||
/* 触屏无 hover:子菜单静态平铺(缩进列表),不做浮层 */
|
||||
.site-nav ul.sub-nav {
|
||||
display: block;
|
||||
position: static;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
min-width: 0;
|
||||
padding: 0 0 6px;
|
||||
}
|
||||
.site-nav .sub-nav li a { color: rgba(255, 255, 255, .9); padding: 8px 16px 8px 34px; }
|
||||
.site-nav .sub-nav li a:hover { background: var(--primary-dark); color: #fff; }
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="detail-main">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<a th:if="${currentCategoryId != null}" th:href="${url.category(currentCategoryId)}">栏目</a>
|
||||
<span>正文</span>
|
||||
</nav>
|
||||
|
||||
<article class="article">
|
||||
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
|
||||
<div class="article-meta">
|
||||
<time th:if="${article.publishTime != null}"
|
||||
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
|
||||
<span th:if="${!#strings.isEmpty(article.authorName)}"
|
||||
th:text="'作者:' + ${article.authorName}">作者</span>
|
||||
<span th:if="${!#strings.isEmpty(article.source)}"
|
||||
th:text="'来源:' + ${article.source}">来源</span>
|
||||
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
|
||||
</div>
|
||||
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
|
||||
其余字段一律 th:text 转义输出 -->
|
||||
<div class="article-content" th:utext="${article.content}">正文</div>
|
||||
</article>
|
||||
|
||||
<nav class="article-sibling">
|
||||
<a th:if="${prevArticle != null}" class="prev"
|
||||
th:href="${url.article(prevArticle.id)}"
|
||||
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
|
||||
<a th:if="${nextArticle != null}" class="next"
|
||||
th:href="${url.article(nextArticle.id)}"
|
||||
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<!-- 公共页脚:联系信息 + 友情链接(分组渲染,方案 §8.1:友链不设独立路由) -->
|
||||
<footer class="site-footer" th:fragment="footer">
|
||||
<div class="footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
|
||||
<div class="links-group" th:each="group : ${site.linkGroups}">
|
||||
<span class="group-name" th:text="${group.groupName}">分组</span>
|
||||
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${link.name}">链接</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-info">
|
||||
<p th:if="${site.config['contact'] != null}" th:text="${site.config['contact']}">联系方式</p>
|
||||
<p th:if="${site.config['copyright'] != null}" th:text="${site.config['copyright']}">版权信息</p>
|
||||
<p th:if="${site.config['beian'] != null}" th:text="${site.config['beian']}">备案号</p>
|
||||
</div>
|
||||
</footer>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<!-- 公共 head 片段:SEO(title/description/canonical)+ 主题资产(方案 §8.3/§8.6) -->
|
||||
<head th:fragment="head">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title th:text="${pageTitle}">页面标题</title>
|
||||
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
|
||||
th:content="${seoDescription}">
|
||||
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
|
||||
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
|
||||
<link th:if="${site.config['favicon'] != null}" rel="icon" th:href="${site.config['favicon']}">
|
||||
</head>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<!-- 公共头部:站点名 + 栏目导航 + 站内搜索 + 专业院系下拉(院系子站切换,《院系子站方案》) -->
|
||||
<header class="site-header" th:fragment="header">
|
||||
<div class="header-inner">
|
||||
<div class="site-brand">
|
||||
<img th:if="${site.config['logo'] != null}" class="site-logo"
|
||||
th:src="${site.config['logo']}" th:alt="${site.name}">
|
||||
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
|
||||
</div>
|
||||
<form class="site-search" th:action="${url.search('', null)}" method="get">
|
||||
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
|
||||
<button type="submit">搜索</button>
|
||||
</form>
|
||||
<!-- 专业院系下拉:顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 config_json.colleges_label 覆盖 -->
|
||||
<div class="site-colleges" th:if="${!site.colleges.isEmpty()}"
|
||||
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
|
||||
<span class="colleges-toggle">
|
||||
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
|
||||
<i class="colleges-arrow">▾</i>
|
||||
</span>
|
||||
<ul class="colleges-menu">
|
||||
<li th:each="col : ${site.colleges}" th:classappend="${col.code == site.code} ? 'active'">
|
||||
<a th:href="${url.siteHome(col.code, col.publicBaseUrl)}"
|
||||
th:title="${col.name}" th:text="${col.name}">学院名称</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="site-nav">
|
||||
<ul>
|
||||
<li th:classappend="${currentCategoryId == null} ? 'active'">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
</li>
|
||||
<li th:each="cat : ${navCategories}"
|
||||
th:classappend="${(currentCategoryId == cat.id ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
|
||||
<!-- 外链栏目直接外跳;其余进栏目页(方案 §8.1) -->
|
||||
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${cat.name}">栏目</a>
|
||||
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}" th:text="${cat.name}">栏目</a>
|
||||
<!-- 二级下拉:悬停展开(样式见 assets/css/main.css .sub-nav) -->
|
||||
<ul class="sub-nav" th:if="${!cat.children.isEmpty()}">
|
||||
<li th:each="sub : ${cat.children}">
|
||||
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${sub.name}">子栏目</a>
|
||||
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="home-main">
|
||||
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
|
||||
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
|
||||
<div class="banner-slides">
|
||||
<a class="banner-slide" th:each="b, stat : ${banners}"
|
||||
th:classappend="${stat.first} ? 'active'"
|
||||
th:href="${b.linkUrl}" th:title="${b.title}">
|
||||
<img th:src="${b.imageUrl}" th:alt="${b.title}">
|
||||
<span class="banner-title" th:text="${b.title}">标题</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
|
||||
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
|
||||
th:aria-label="${'第' + (stat.count) + '张'}"></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 置顶/头条区(cms_article topmost 优先) -->
|
||||
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
|
||||
<h2 class="section-title">头条推荐</h2>
|
||||
<ul>
|
||||
<li th:each="item : ${topArticles}">
|
||||
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 一级栏目区块(每栏目 top 8) -->
|
||||
<section class="category-block" th:each="block : ${categoryBlocks}">
|
||||
<div class="block-head">
|
||||
<h2 th:text="${block.category.name}">栏目名</h2>
|
||||
<a class="more" th:href="${block.moreUrl}">更多 ></a>
|
||||
</div>
|
||||
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
|
||||
<li th:each="item : ${block.items}">
|
||||
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="list-main">
|
||||
<nav class="breadcrumb" th:if="${searchMode != true}">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span th:text="${category.name}">栏目</span>
|
||||
</nav>
|
||||
<nav class="breadcrumb" th:if="${searchMode == true}">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span>搜索结果</span>
|
||||
</nav>
|
||||
|
||||
<h1 class="page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
|
||||
<h1 class="page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
|
||||
|
||||
<ul class="article-list">
|
||||
<li th:each="item : ${listData.items}">
|
||||
<a class="item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<p class="item-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">摘要</p>
|
||||
<div class="item-meta">
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="empty" th:if="${#lists.isEmpty(listData.items)}">暂无内容</p>
|
||||
|
||||
<!-- 分页(列表页/搜索页共用) -->
|
||||
<nav class="pagination" th:if="${listData.totalPages > 1}"
|
||||
th:with="cur=${listData.page}, total=${listData.totalPages}">
|
||||
<a th:if="${cur > 1}"
|
||||
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}">上一页</a>
|
||||
<a th:each="p : ${#numbers.sequence(1, total)}"
|
||||
th:classappend="${p == cur} ? 'active'"
|
||||
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
|
||||
th:text="${p}">1</a>
|
||||
<a th:if="${cur < total}"
|
||||
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}">下一页</a>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="detail-main">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span th:text="${category.name}">栏目</span>
|
||||
</nav>
|
||||
|
||||
<article class="article">
|
||||
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
|
||||
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
|
||||
<div class="article-content" th:utext="${category.content}">单页内容</div>
|
||||
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"specVersion": 1,
|
||||
"name": "t-bad-key",
|
||||
"title": "参数 key 非法(夹具)",
|
||||
"version": "1.0.0",
|
||||
"screenshot": "assets/screenshot.png",
|
||||
"params": [
|
||||
{ "key": "bad-key", "label": "非法 key(含短横线)", "type": "color", "default": "#123456" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
/* ============================================================
|
||||
* default 皮肤包样式(方案 §8.2)
|
||||
* 换肤 = 拷贝 default 目录改 CSS 变量与布局,零后端代码。
|
||||
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪,D2 决策点确认)。
|
||||
* ============================================================ */
|
||||
:root {
|
||||
--primary: #1a5cb0;
|
||||
--primary-dark: #12447f;
|
||||
--text: #333;
|
||||
--text-light: #777;
|
||||
--bg: #f5f6f8;
|
||||
--card-bg: #fff;
|
||||
--border: #e5e6eb;
|
||||
--max-width: 1200px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
}
|
||||
a { color: inherit; text-decoration: none; }
|
||||
a:hover { color: var(--primary); }
|
||||
img { max-width: 100%; }
|
||||
time, .item-meta { color: var(--text-light); font-size: 13px; }
|
||||
.empty { color: var(--text-light); text-align: center; padding: 32px 0; }
|
||||
|
||||
/* ===== 头部 ===== */
|
||||
.site-header { background: var(--card-bg); border-bottom: 1px solid var(--border); }
|
||||
.header-inner {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 18px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
.site-brand { display: flex; align-items: center; gap: 12px; }
|
||||
.site-logo { height: 44px; }
|
||||
.site-name { font-size: 26px; font-weight: 700; color: var(--primary); }
|
||||
.site-search { display: flex; }
|
||||
.site-search input {
|
||||
border: 1px solid var(--border);
|
||||
border-right: none;
|
||||
padding: 8px 12px;
|
||||
width: 220px;
|
||||
outline: none;
|
||||
}
|
||||
.site-search button {
|
||||
border: none;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
padding: 8px 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ===== 专业院系下拉(院系子站切换,《院系子站方案》) ===== */
|
||||
/* 悬停展开白底纵向列表,右对齐 + 超高滚动(院系数十个子站场景) */
|
||||
.site-colleges { position: relative; }
|
||||
.site-colleges .colleges-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--text-light); }
|
||||
.site-colleges .colleges-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
min-width: 200px;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
list-style: none;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-top: 2px solid var(--primary);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
|
||||
z-index: 110;
|
||||
}
|
||||
.site-colleges:hover .colleges-menu { display: block; }
|
||||
.site-colleges .colleges-menu li a {
|
||||
display: block;
|
||||
padding: 9px 16px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.site-colleges .colleges-menu li a:hover { background: #f2f6fc; color: var(--primary); }
|
||||
.site-colleges .colleges-menu li.active a { color: var(--primary); font-weight: 600; }
|
||||
.site-nav { background: var(--primary); }
|
||||
.site-nav ul {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.site-nav li a {
|
||||
display: block;
|
||||
padding: 12px 22px;
|
||||
color: #fff;
|
||||
}
|
||||
.site-nav li:hover a, .site-nav li.active a { background: var(--primary-dark); }
|
||||
|
||||
/* ===== 二级下拉导航 ===== */
|
||||
/* 面板为白底纵向列表:需整体覆盖 .site-nav ul 的横向 flex 与内边距 */
|
||||
.site-nav > ul > li { position: relative; }
|
||||
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .85; }
|
||||
.site-nav ul.sub-nav {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 168px;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
list-style: none;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-top: 2px solid var(--primary);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
|
||||
z-index: 100;
|
||||
}
|
||||
.site-nav li:hover > .sub-nav { display: block; }
|
||||
.site-nav .sub-nav li a {
|
||||
padding: 9px 16px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
background: none;
|
||||
}
|
||||
.site-nav .sub-nav li a:hover { background: #f2f6fc; color: var(--primary); }
|
||||
.site-nav .sub-nav li.active a { color: var(--primary); font-weight: 600; }
|
||||
|
||||
/* ===== 首页轮播 ===== */
|
||||
.banner { position: relative; max-width: var(--max-width); margin: 16px auto; overflow: hidden; }
|
||||
.banner-slides { position: relative; height: 420px; }
|
||||
.banner-slide { display: none; position: absolute; inset: 0; }
|
||||
.banner-slide.active { display: block; }
|
||||
.banner-slide img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.banner-title {
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
padding: 12px 20px;
|
||||
color: #fff;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, .6));
|
||||
}
|
||||
.banner-dots {
|
||||
position: absolute;
|
||||
bottom: 12px; right: 20px;
|
||||
display: flex; gap: 8px;
|
||||
}
|
||||
.banner-dots button {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, .6);
|
||||
cursor: pointer;
|
||||
}
|
||||
.banner-dots button.active { background: #fff; }
|
||||
|
||||
/* ===== 通用区块 ===== */
|
||||
.top-strip, .category-block {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 20px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.section-title, .block-head h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 20px;
|
||||
border-left: 4px solid var(--primary);
|
||||
padding-left: 10px;
|
||||
}
|
||||
.block-head { display: flex; align-items: baseline; justify-content: space-between; }
|
||||
.more { font-size: 13px; color: var(--text-light); }
|
||||
.top-strip ul, .block-list { list-style: none; margin: 0; padding: 0; }
|
||||
.top-strip li, .block-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 9px 0;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
}
|
||||
.top-strip li:last-child, .block-list li:last-child { border-bottom: none; }
|
||||
.top-strip li a, .block-list li a {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.top-badge {
|
||||
flex: none;
|
||||
color: #fff;
|
||||
background: var(--primary);
|
||||
font-size: 12px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ===== 列表页 ===== */
|
||||
.list-main {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 24px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.breadcrumb { color: var(--text-light); font-size: 13px; margin-bottom: 16px; }
|
||||
.breadcrumb a { margin-right: 8px; }
|
||||
.breadcrumb span { margin-left: 8px; }
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
border-left: 4px solid var(--primary);
|
||||
padding-left: 10px;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
.article-list { list-style: none; margin: 0; padding: 0; }
|
||||
.article-list li { padding: 14px 0; border-bottom: 1px dashed var(--border); }
|
||||
.item-title { font-size: 17px; }
|
||||
.item-summary {
|
||||
color: var(--text-light);
|
||||
font-size: 14px;
|
||||
margin: 6px 0;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.item-meta { display: flex; gap: 10px; align-items: center; }
|
||||
|
||||
/* ===== 分页 ===== */
|
||||
.pagination { display: flex; gap: 6px; justify-content: center; padding: 24px 0 4px; }
|
||||
.pagination a {
|
||||
min-width: 34px;
|
||||
text-align: center;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card-bg);
|
||||
}
|
||||
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
|
||||
|
||||
/* ===== 详情/单页 ===== */
|
||||
.detail-main {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 32px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.article-title { margin: 8px 0 12px; font-size: 26px; text-align: center; }
|
||||
.article-meta {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
color: var(--text-light);
|
||||
font-size: 13px;
|
||||
padding-bottom: 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.article-content { line-height: 1.9; font-size: 16px; padding-top: 20px; word-break: break-word; }
|
||||
.article-content img { height: auto; }
|
||||
.article-content table { border-collapse: collapse; width: 100%; }
|
||||
.article-content table td, .article-content table th { border: 1px solid var(--border); padding: 6px 10px; }
|
||||
.article-sibling { display: flex; flex-direction: column; gap: 8px; margin-top: 28px; font-size: 14px; }
|
||||
|
||||
/* ===== 页脚 ===== */
|
||||
.site-footer { margin-top: 32px; background: var(--primary-dark); color: rgba(255, 255, 255, .85); }
|
||||
.footer-links {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 18px 16px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.links-group { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; font-size: 13px; }
|
||||
.group-name { font-weight: 600; margin-right: 4px; }
|
||||
.footer-info {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 12px 16px 20px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.footer-info p { margin: 4px 0; }
|
||||
|
||||
/* ===== 移动端适配 ===== */
|
||||
@media (max-width: 768px) {
|
||||
.header-inner { flex-direction: column; align-items: stretch; }
|
||||
.site-search input { flex: 1; width: auto; }
|
||||
.site-colleges { align-self: flex-start; }
|
||||
.site-colleges .colleges-menu { left: 0; right: auto; min-width: 100%; }
|
||||
.banner-slides { height: 200px; }
|
||||
.detail-main, .list-main { padding: 18px 14px; }
|
||||
.article-title { font-size: 22px; }
|
||||
/* 触屏无 hover:子菜单静态平铺(缩进列表),不做浮层 */
|
||||
.site-nav ul.sub-nav {
|
||||
display: block;
|
||||
position: static;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
min-width: 0;
|
||||
padding: 0 0 6px;
|
||||
}
|
||||
.site-nav .sub-nav li a { color: rgba(255, 255, 255, .9); padding: 8px 16px 8px 34px; }
|
||||
.site-nav .sub-nav li a:hover { background: var(--primary-dark); color: #fff; }
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="detail-main">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<a th:if="${currentCategoryId != null}" th:href="${url.category(currentCategoryId)}">栏目</a>
|
||||
<span>正文</span>
|
||||
</nav>
|
||||
|
||||
<article class="article">
|
||||
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
|
||||
<div class="article-meta">
|
||||
<time th:if="${article.publishTime != null}"
|
||||
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
|
||||
<span th:if="${!#strings.isEmpty(article.authorName)}"
|
||||
th:text="'作者:' + ${article.authorName}">作者</span>
|
||||
<span th:if="${!#strings.isEmpty(article.source)}"
|
||||
th:text="'来源:' + ${article.source}">来源</span>
|
||||
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
|
||||
</div>
|
||||
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
|
||||
其余字段一律 th:text 转义输出 -->
|
||||
<div class="article-content" th:utext="${article.content}">正文</div>
|
||||
</article>
|
||||
|
||||
<nav class="article-sibling">
|
||||
<a th:if="${prevArticle != null}" class="prev"
|
||||
th:href="${url.article(prevArticle.id)}"
|
||||
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
|
||||
<a th:if="${nextArticle != null}" class="next"
|
||||
th:href="${url.article(nextArticle.id)}"
|
||||
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<!-- 公共页脚:联系信息 + 友情链接(分组渲染,方案 §8.1:友链不设独立路由) -->
|
||||
<footer class="site-footer" th:fragment="footer">
|
||||
<div class="footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
|
||||
<div class="links-group" th:each="group : ${site.linkGroups}">
|
||||
<span class="group-name" th:text="${group.groupName}">分组</span>
|
||||
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${link.name}">链接</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-info">
|
||||
<p th:if="${site.config['contact'] != null}" th:text="${site.config['contact']}">联系方式</p>
|
||||
<p th:if="${site.config['copyright'] != null}" th:text="${site.config['copyright']}">版权信息</p>
|
||||
<p th:if="${site.config['beian'] != null}" th:text="${site.config['beian']}">备案号</p>
|
||||
</div>
|
||||
</footer>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<!-- 公共 head 片段:SEO(title/description/canonical)+ 主题资产(方案 §8.3/§8.6) -->
|
||||
<head th:fragment="head">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title th:text="${pageTitle}">页面标题</title>
|
||||
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
|
||||
th:content="${seoDescription}">
|
||||
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
|
||||
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
|
||||
<link th:if="${site.config['favicon'] != null}" rel="icon" th:href="${site.config['favicon']}">
|
||||
</head>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<!-- 公共头部:站点名 + 栏目导航 + 站内搜索 + 专业院系下拉(院系子站切换,《院系子站方案》) -->
|
||||
<header class="site-header" th:fragment="header">
|
||||
<div class="header-inner">
|
||||
<div class="site-brand">
|
||||
<img th:if="${site.config['logo'] != null}" class="site-logo"
|
||||
th:src="${site.config['logo']}" th:alt="${site.name}">
|
||||
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
|
||||
</div>
|
||||
<form class="site-search" th:action="${url.search('', null)}" method="get">
|
||||
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
|
||||
<button type="submit">搜索</button>
|
||||
</form>
|
||||
<!-- 专业院系下拉:顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 config_json.colleges_label 覆盖 -->
|
||||
<div class="site-colleges" th:if="${!site.colleges.isEmpty()}"
|
||||
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
|
||||
<span class="colleges-toggle">
|
||||
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
|
||||
<i class="colleges-arrow">▾</i>
|
||||
</span>
|
||||
<ul class="colleges-menu">
|
||||
<li th:each="col : ${site.colleges}" th:classappend="${col.code == site.code} ? 'active'">
|
||||
<a th:href="${url.siteHome(col.code, col.publicBaseUrl)}"
|
||||
th:title="${col.name}" th:text="${col.name}">学院名称</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="site-nav">
|
||||
<ul>
|
||||
<li th:classappend="${currentCategoryId == null} ? 'active'">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
</li>
|
||||
<li th:each="cat : ${navCategories}"
|
||||
th:classappend="${(currentCategoryId == cat.id ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
|
||||
<!-- 外链栏目直接外跳;其余进栏目页(方案 §8.1) -->
|
||||
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${cat.name}">栏目</a>
|
||||
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}" th:text="${cat.name}">栏目</a>
|
||||
<!-- 二级下拉:悬停展开(样式见 assets/css/main.css .sub-nav) -->
|
||||
<ul class="sub-nav" th:if="${!cat.children.isEmpty()}">
|
||||
<li th:each="sub : ${cat.children}">
|
||||
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${sub.name}">子栏目</a>
|
||||
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="home-main">
|
||||
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
|
||||
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
|
||||
<div class="banner-slides">
|
||||
<a class="banner-slide" th:each="b, stat : ${banners}"
|
||||
th:classappend="${stat.first} ? 'active'"
|
||||
th:href="${b.linkUrl}" th:title="${b.title}">
|
||||
<img th:src="${b.imageUrl}" th:alt="${b.title}">
|
||||
<span class="banner-title" th:text="${b.title}">标题</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
|
||||
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
|
||||
th:aria-label="${'第' + (stat.count) + '张'}"></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 置顶/头条区(cms_article topmost 优先) -->
|
||||
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
|
||||
<h2 class="section-title">头条推荐</h2>
|
||||
<ul>
|
||||
<li th:each="item : ${topArticles}">
|
||||
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 一级栏目区块(每栏目 top 8) -->
|
||||
<section class="category-block" th:each="block : ${categoryBlocks}">
|
||||
<div class="block-head">
|
||||
<h2 th:text="${block.category.name}">栏目名</h2>
|
||||
<a class="more" th:href="${block.moreUrl}">更多 ></a>
|
||||
</div>
|
||||
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
|
||||
<li th:each="item : ${block.items}">
|
||||
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="list-main">
|
||||
<nav class="breadcrumb" th:if="${searchMode != true}">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span th:text="${category.name}">栏目</span>
|
||||
</nav>
|
||||
<nav class="breadcrumb" th:if="${searchMode == true}">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span>搜索结果</span>
|
||||
</nav>
|
||||
|
||||
<h1 class="page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
|
||||
<h1 class="page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
|
||||
|
||||
<ul class="article-list">
|
||||
<li th:each="item : ${listData.items}">
|
||||
<a class="item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<p class="item-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">摘要</p>
|
||||
<div class="item-meta">
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="empty" th:if="${#lists.isEmpty(listData.items)}">暂无内容</p>
|
||||
|
||||
<!-- 分页(列表页/搜索页共用) -->
|
||||
<nav class="pagination" th:if="${listData.totalPages > 1}"
|
||||
th:with="cur=${listData.page}, total=${listData.totalPages}">
|
||||
<a th:if="${cur > 1}"
|
||||
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}">上一页</a>
|
||||
<a th:each="p : ${#numbers.sequence(1, total)}"
|
||||
th:classappend="${p == cur} ? 'active'"
|
||||
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
|
||||
th:text="${p}">1</a>
|
||||
<a th:if="${cur < total}"
|
||||
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}">下一页</a>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="detail-main">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span th:text="${category.name}">栏目</span>
|
||||
</nav>
|
||||
|
||||
<article class="article">
|
||||
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
|
||||
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
|
||||
<div class="article-content" th:utext="${category.content}">单页内容</div>
|
||||
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"specVersion": 1,
|
||||
"name": "t-bad-shot",
|
||||
"title": "声明截图缺失(夹具)",
|
||||
"version": "1.0.0",
|
||||
"screenshot": "assets/no-such-shot.png",
|
||||
"params": []
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="home-main">
|
||||
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
|
||||
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
|
||||
<div class="banner-slides">
|
||||
<a class="banner-slide" th:each="b, stat : ${banners}"
|
||||
th:classappend="${stat.first} ? 'active'"
|
||||
th:href="${b.linkUrl}" th:title="${b.title}">
|
||||
<img th:src="${b.imageUrl}" th:alt="${b.title}">
|
||||
<span class="banner-title" th:text="${b.title}">标题</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
|
||||
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
|
||||
th:aria-label="${'第' + (stat.count) + '张'}"></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 置顶/头条区(cms_article topmost 优先) -->
|
||||
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
|
||||
<h2 class="section-title">头条推荐</h2>
|
||||
<ul>
|
||||
<li th:each="item : ${topArticles}">
|
||||
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 一级栏目区块(每栏目 top 8) -->
|
||||
<section class="category-block" th:each="block : ${categoryBlocks}">
|
||||
<div class="block-head">
|
||||
<h2 th:text="${block.category.name}">栏目名</h2>
|
||||
<a class="more" th:href="${block.moreUrl}">更多 ></a>
|
||||
</div>
|
||||
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
|
||||
<li th:each="item : ${block.items}">
|
||||
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
{ this is not valid json ***
|
||||
@@ -0,0 +1,324 @@
|
||||
/* ============================================================
|
||||
* default 皮肤包样式(方案 §8.2)
|
||||
* 换肤 = 拷贝 default 目录改 CSS 变量与布局,零后端代码。
|
||||
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪,D2 决策点确认)。
|
||||
* ============================================================ */
|
||||
:root {
|
||||
--primary: #1a5cb0;
|
||||
--primary-dark: #12447f;
|
||||
--text: #333;
|
||||
--text-light: #777;
|
||||
--bg: #f5f6f8;
|
||||
--card-bg: #fff;
|
||||
--border: #e5e6eb;
|
||||
--max-width: 1200px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
}
|
||||
a { color: inherit; text-decoration: none; }
|
||||
a:hover { color: var(--primary); }
|
||||
img { max-width: 100%; }
|
||||
time, .item-meta { color: var(--text-light); font-size: 13px; }
|
||||
.empty { color: var(--text-light); text-align: center; padding: 32px 0; }
|
||||
|
||||
/* ===== 头部 ===== */
|
||||
.site-header { background: var(--card-bg); border-bottom: 1px solid var(--border); }
|
||||
.header-inner {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 18px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
.site-brand { display: flex; align-items: center; gap: 12px; }
|
||||
.site-logo { height: 44px; }
|
||||
.site-name { font-size: 26px; font-weight: 700; color: var(--primary); }
|
||||
.site-search { display: flex; }
|
||||
.site-search input {
|
||||
border: 1px solid var(--border);
|
||||
border-right: none;
|
||||
padding: 8px 12px;
|
||||
width: 220px;
|
||||
outline: none;
|
||||
}
|
||||
.site-search button {
|
||||
border: none;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
padding: 8px 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ===== 专业院系下拉(院系子站切换,《院系子站方案》) ===== */
|
||||
/* 悬停展开白底纵向列表,右对齐 + 超高滚动(院系数十个子站场景) */
|
||||
.site-colleges { position: relative; }
|
||||
.site-colleges .colleges-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--text-light); }
|
||||
.site-colleges .colleges-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
min-width: 200px;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
list-style: none;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-top: 2px solid var(--primary);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
|
||||
z-index: 110;
|
||||
}
|
||||
.site-colleges:hover .colleges-menu { display: block; }
|
||||
.site-colleges .colleges-menu li a {
|
||||
display: block;
|
||||
padding: 9px 16px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.site-colleges .colleges-menu li a:hover { background: #f2f6fc; color: var(--primary); }
|
||||
.site-colleges .colleges-menu li.active a { color: var(--primary); font-weight: 600; }
|
||||
.site-nav { background: var(--primary); }
|
||||
.site-nav ul {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.site-nav li a {
|
||||
display: block;
|
||||
padding: 12px 22px;
|
||||
color: #fff;
|
||||
}
|
||||
.site-nav li:hover a, .site-nav li.active a { background: var(--primary-dark); }
|
||||
|
||||
/* ===== 二级下拉导航 ===== */
|
||||
/* 面板为白底纵向列表:需整体覆盖 .site-nav ul 的横向 flex 与内边距 */
|
||||
.site-nav > ul > li { position: relative; }
|
||||
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .85; }
|
||||
.site-nav ul.sub-nav {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 168px;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
list-style: none;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-top: 2px solid var(--primary);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
|
||||
z-index: 100;
|
||||
}
|
||||
.site-nav li:hover > .sub-nav { display: block; }
|
||||
.site-nav .sub-nav li a {
|
||||
padding: 9px 16px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
background: none;
|
||||
}
|
||||
.site-nav .sub-nav li a:hover { background: #f2f6fc; color: var(--primary); }
|
||||
.site-nav .sub-nav li.active a { color: var(--primary); font-weight: 600; }
|
||||
|
||||
/* ===== 首页轮播 ===== */
|
||||
.banner { position: relative; max-width: var(--max-width); margin: 16px auto; overflow: hidden; }
|
||||
.banner-slides { position: relative; height: 420px; }
|
||||
.banner-slide { display: none; position: absolute; inset: 0; }
|
||||
.banner-slide.active { display: block; }
|
||||
.banner-slide img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.banner-title {
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
padding: 12px 20px;
|
||||
color: #fff;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, .6));
|
||||
}
|
||||
.banner-dots {
|
||||
position: absolute;
|
||||
bottom: 12px; right: 20px;
|
||||
display: flex; gap: 8px;
|
||||
}
|
||||
.banner-dots button {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, .6);
|
||||
cursor: pointer;
|
||||
}
|
||||
.banner-dots button.active { background: #fff; }
|
||||
|
||||
/* ===== 通用区块 ===== */
|
||||
.top-strip, .category-block {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 20px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.section-title, .block-head h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 20px;
|
||||
border-left: 4px solid var(--primary);
|
||||
padding-left: 10px;
|
||||
}
|
||||
.block-head { display: flex; align-items: baseline; justify-content: space-between; }
|
||||
.more { font-size: 13px; color: var(--text-light); }
|
||||
.top-strip ul, .block-list { list-style: none; margin: 0; padding: 0; }
|
||||
.top-strip li, .block-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 9px 0;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
}
|
||||
.top-strip li:last-child, .block-list li:last-child { border-bottom: none; }
|
||||
.top-strip li a, .block-list li a {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.top-badge {
|
||||
flex: none;
|
||||
color: #fff;
|
||||
background: var(--primary);
|
||||
font-size: 12px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ===== 列表页 ===== */
|
||||
.list-main {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 24px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.breadcrumb { color: var(--text-light); font-size: 13px; margin-bottom: 16px; }
|
||||
.breadcrumb a { margin-right: 8px; }
|
||||
.breadcrumb span { margin-left: 8px; }
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
border-left: 4px solid var(--primary);
|
||||
padding-left: 10px;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
.article-list { list-style: none; margin: 0; padding: 0; }
|
||||
.article-list li { padding: 14px 0; border-bottom: 1px dashed var(--border); }
|
||||
.item-title { font-size: 17px; }
|
||||
.item-summary {
|
||||
color: var(--text-light);
|
||||
font-size: 14px;
|
||||
margin: 6px 0;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.item-meta { display: flex; gap: 10px; align-items: center; }
|
||||
|
||||
/* ===== 分页 ===== */
|
||||
.pagination { display: flex; gap: 6px; justify-content: center; padding: 24px 0 4px; }
|
||||
.pagination a {
|
||||
min-width: 34px;
|
||||
text-align: center;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card-bg);
|
||||
}
|
||||
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
|
||||
|
||||
/* ===== 详情/单页 ===== */
|
||||
.detail-main {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 32px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.article-title { margin: 8px 0 12px; font-size: 26px; text-align: center; }
|
||||
.article-meta {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
color: var(--text-light);
|
||||
font-size: 13px;
|
||||
padding-bottom: 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.article-content { line-height: 1.9; font-size: 16px; padding-top: 20px; word-break: break-word; }
|
||||
.article-content img { height: auto; }
|
||||
.article-content table { border-collapse: collapse; width: 100%; }
|
||||
.article-content table td, .article-content table th { border: 1px solid var(--border); padding: 6px 10px; }
|
||||
.article-sibling { display: flex; flex-direction: column; gap: 8px; margin-top: 28px; font-size: 14px; }
|
||||
|
||||
/* ===== 页脚 ===== */
|
||||
.site-footer { margin-top: 32px; background: var(--primary-dark); color: rgba(255, 255, 255, .85); }
|
||||
.footer-links {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 18px 16px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.links-group { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; font-size: 13px; }
|
||||
.group-name { font-weight: 600; margin-right: 4px; }
|
||||
.footer-info {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 12px 16px 20px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.footer-info p { margin: 4px 0; }
|
||||
|
||||
/* ===== 移动端适配 ===== */
|
||||
@media (max-width: 768px) {
|
||||
.header-inner { flex-direction: column; align-items: stretch; }
|
||||
.site-search input { flex: 1; width: auto; }
|
||||
.site-colleges { align-self: flex-start; }
|
||||
.site-colleges .colleges-menu { left: 0; right: auto; min-width: 100%; }
|
||||
.banner-slides { height: 200px; }
|
||||
.detail-main, .list-main { padding: 18px 14px; }
|
||||
.article-title { font-size: 22px; }
|
||||
/* 触屏无 hover:子菜单静态平铺(缩进列表),不做浮层 */
|
||||
.site-nav ul.sub-nav {
|
||||
display: block;
|
||||
position: static;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
min-width: 0;
|
||||
padding: 0 0 6px;
|
||||
}
|
||||
.site-nav .sub-nav li a { color: rgba(255, 255, 255, .9); padding: 8px 16px 8px 34px; }
|
||||
.site-nav .sub-nav li a:hover { background: var(--primary-dark); color: #fff; }
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="detail-main">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<a th:if="${currentCategoryId != null}" th:href="${url.category(currentCategoryId)}">栏目</a>
|
||||
<span>正文</span>
|
||||
</nav>
|
||||
|
||||
<article class="article">
|
||||
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
|
||||
<div class="article-meta">
|
||||
<time th:if="${article.publishTime != null}"
|
||||
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
|
||||
<span th:if="${!#strings.isEmpty(article.authorName)}"
|
||||
th:text="'作者:' + ${article.authorName}">作者</span>
|
||||
<span th:if="${!#strings.isEmpty(article.source)}"
|
||||
th:text="'来源:' + ${article.source}">来源</span>
|
||||
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
|
||||
</div>
|
||||
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
|
||||
其余字段一律 th:text 转义输出 -->
|
||||
<div class="article-content" th:utext="${article.content}">正文</div>
|
||||
</article>
|
||||
|
||||
<nav class="article-sibling">
|
||||
<a th:if="${prevArticle != null}" class="prev"
|
||||
th:href="${url.article(prevArticle.id)}"
|
||||
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
|
||||
<a th:if="${nextArticle != null}" class="next"
|
||||
th:href="${url.article(nextArticle.id)}"
|
||||
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<!-- 公共页脚:联系信息 + 友情链接(分组渲染,方案 §8.1:友链不设独立路由) -->
|
||||
<footer class="site-footer" th:fragment="footer">
|
||||
<div class="footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
|
||||
<div class="links-group" th:each="group : ${site.linkGroups}">
|
||||
<span class="group-name" th:text="${group.groupName}">分组</span>
|
||||
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${link.name}">链接</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-info">
|
||||
<p th:if="${site.config['contact'] != null}" th:text="${site.config['contact']}">联系方式</p>
|
||||
<p th:if="${site.config['copyright'] != null}" th:text="${site.config['copyright']}">版权信息</p>
|
||||
<p th:if="${site.config['beian'] != null}" th:text="${site.config['beian']}">备案号</p>
|
||||
</div>
|
||||
</footer>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<!-- 公共 head 片段:SEO(title/description/canonical)+ 主题资产(方案 §8.3/§8.6) -->
|
||||
<head th:fragment="head">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title th:text="${pageTitle}">页面标题</title>
|
||||
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
|
||||
th:content="${seoDescription}">
|
||||
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
|
||||
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
|
||||
<!-- 微调参数 CSS 变量覆盖(方案 §5.3):位于皮肤 CSS 之后层叠覆盖;值由服务端预构建并校验 -->
|
||||
<style th:if="${not #strings.isEmpty(themeVarsCss)}" th:utext="${themeVarsCss}"></style>
|
||||
<link th:if="${site.config['favicon'] != null}" rel="icon" th:href="${site.config['favicon']}">
|
||||
</head>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<!-- 公共头部:站点名 + 栏目导航 + 站内搜索 + 专业院系下拉(院系子站切换,《院系子站方案》) -->
|
||||
<header class="site-header" th:fragment="header">
|
||||
<div class="header-inner">
|
||||
<div class="site-brand">
|
||||
<img th:if="${site.config['logo'] != null}" class="site-logo"
|
||||
th:src="${site.config['logo']}" th:alt="${site.name}">
|
||||
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
|
||||
</div>
|
||||
<form class="site-search" th:action="${url.search('', null)}" method="get">
|
||||
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
|
||||
<button type="submit">搜索</button>
|
||||
</form>
|
||||
<!-- 专业院系下拉:顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 config_json.colleges_label 覆盖 -->
|
||||
<div class="site-colleges" th:if="${!site.colleges.isEmpty()}"
|
||||
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
|
||||
<span class="colleges-toggle">
|
||||
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
|
||||
<i class="colleges-arrow">▾</i>
|
||||
</span>
|
||||
<ul class="colleges-menu">
|
||||
<li th:each="col : ${site.colleges}" th:classappend="${col.code == site.code} ? 'active'">
|
||||
<a th:href="${url.siteHome(col.code, col.publicBaseUrl)}"
|
||||
th:title="${col.name}" th:text="${col.name}">学院名称</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="site-nav">
|
||||
<ul>
|
||||
<li th:classappend="${currentCategoryId == null} ? 'active'">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
</li>
|
||||
<li th:each="cat : ${navCategories}"
|
||||
th:classappend="${(currentCategoryId == cat.id ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
|
||||
<!-- 外链栏目直接外跳;其余进栏目页(方案 §8.1) -->
|
||||
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${cat.name}">栏目</a>
|
||||
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}" th:text="${cat.name}">栏目</a>
|
||||
<!-- 二级下拉:悬停展开(样式见 assets/css/main.css .sub-nav) -->
|
||||
<ul class="sub-nav" th:if="${!cat.children.isEmpty()}">
|
||||
<li th:each="sub : ${cat.children}">
|
||||
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${sub.name}">子栏目</a>
|
||||
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="home-main">
|
||||
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
|
||||
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
|
||||
<div class="banner-slides">
|
||||
<a class="banner-slide" th:each="b, stat : ${banners}"
|
||||
th:classappend="${stat.first} ? 'active'"
|
||||
th:href="${b.linkUrl}" th:title="${b.title}">
|
||||
<img th:src="${b.imageUrl}" th:alt="${b.title}">
|
||||
<span class="banner-title" th:text="${b.title}">标题</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
|
||||
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
|
||||
th:aria-label="${'第' + (stat.count) + '张'}"></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 置顶/头条区(cms_article topmost 优先) -->
|
||||
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
|
||||
<h2 class="section-title">头条推荐</h2>
|
||||
<ul>
|
||||
<li th:each="item : ${topArticles}">
|
||||
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 一级栏目区块(每栏目 top 8) -->
|
||||
<section class="category-block" th:each="block : ${categoryBlocks}">
|
||||
<div class="block-head">
|
||||
<h2 th:text="${block.category.name}">栏目名</h2>
|
||||
<a class="more" th:href="${block.moreUrl}">更多 ></a>
|
||||
</div>
|
||||
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
|
||||
<li th:each="item : ${block.items}">
|
||||
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="list-main">
|
||||
<nav class="breadcrumb" th:if="${searchMode != true}">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span th:text="${category.name}">栏目</span>
|
||||
</nav>
|
||||
<nav class="breadcrumb" th:if="${searchMode == true}">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span>搜索结果</span>
|
||||
</nav>
|
||||
|
||||
<h1 class="page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
|
||||
<h1 class="page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
|
||||
|
||||
<ul class="article-list">
|
||||
<li th:each="item : ${listData.items}">
|
||||
<a class="item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<p class="item-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">摘要</p>
|
||||
<div class="item-meta">
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="empty" th:if="${#lists.isEmpty(listData.items)}">暂无内容</p>
|
||||
|
||||
<!-- 分页(列表页/搜索页共用) -->
|
||||
<nav class="pagination" th:if="${listData.totalPages > 1}"
|
||||
th:with="cur=${listData.page}, total=${listData.totalPages}">
|
||||
<a th:if="${cur > 1}"
|
||||
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}">上一页</a>
|
||||
<a th:each="p : ${#numbers.sequence(1, total)}"
|
||||
th:classappend="${p == cur} ? 'active'"
|
||||
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
|
||||
th:text="${p}">1</a>
|
||||
<a th:if="${cur < total}"
|
||||
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}">下一页</a>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="detail-main">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span th:text="${category.name}">栏目</span>
|
||||
</nav>
|
||||
|
||||
<article class="article">
|
||||
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
|
||||
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
|
||||
<div class="article-content" th:utext="${category.content}">单页内容</div>
|
||||
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"specVersion": 1,
|
||||
"name": "t-demo-red",
|
||||
"title": "测试红(夹具)",
|
||||
"version": "1.0.0",
|
||||
"author": "yunhai-test",
|
||||
"description": "测试夹具:拷贝 default 改主色为深红(方案 §8.7),用于证明多主题机制与白名单。",
|
||||
"screenshot": "assets/screenshot.png",
|
||||
"bannerSize": "1920x560",
|
||||
"params": [
|
||||
{ "key": "primary", "cssVar": "--primary", "label": "主色", "type": "color", "default": "#b01a2e" },
|
||||
{ "key": "bg", "cssVar": "--bg", "label": "页面底色", "type": "color", "default": "#f7f5f2" },
|
||||
{ "key": "heroImage", "label": "头图(image 类型夹具,B-7/T-6)", "type": "image", "default": "/cms/upload/cms/t-demo-red/hero.png" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
/* ============================================================
|
||||
* default 皮肤包样式(方案 §8.2)
|
||||
* 换肤 = 拷贝 default 目录改 CSS 变量与布局,零后端代码。
|
||||
* 轮播图设计尺寸约定:1920×560(管理端上传端按此引导裁剪,D2 决策点确认)。
|
||||
* ============================================================ */
|
||||
:root {
|
||||
--primary: #1a5cb0;
|
||||
--primary-dark: #12447f;
|
||||
--text: #333;
|
||||
--text-light: #777;
|
||||
--bg: #f5f6f8;
|
||||
--card-bg: #fff;
|
||||
--border: #e5e6eb;
|
||||
--max-width: 1200px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
}
|
||||
a { color: inherit; text-decoration: none; }
|
||||
a:hover { color: var(--primary); }
|
||||
img { max-width: 100%; }
|
||||
time, .item-meta { color: var(--text-light); font-size: 13px; }
|
||||
.empty { color: var(--text-light); text-align: center; padding: 32px 0; }
|
||||
|
||||
/* ===== 头部 ===== */
|
||||
.site-header { background: var(--card-bg); border-bottom: 1px solid var(--border); }
|
||||
.header-inner {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 18px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
.site-brand { display: flex; align-items: center; gap: 12px; }
|
||||
.site-logo { height: 44px; }
|
||||
.site-name { font-size: 26px; font-weight: 700; color: var(--primary); }
|
||||
.site-search { display: flex; }
|
||||
.site-search input {
|
||||
border: 1px solid var(--border);
|
||||
border-right: none;
|
||||
padding: 8px 12px;
|
||||
width: 220px;
|
||||
outline: none;
|
||||
}
|
||||
.site-search button {
|
||||
border: none;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
padding: 8px 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ===== 专业院系下拉(院系子站切换,《院系子站方案》) ===== */
|
||||
/* 悬停展开白底纵向列表,右对齐 + 超高滚动(院系数十个子站场景) */
|
||||
.site-colleges { position: relative; }
|
||||
.site-colleges .colleges-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.site-colleges .colleges-arrow { font-style: normal; font-size: 12px; color: var(--text-light); }
|
||||
.site-colleges .colleges-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
min-width: 200px;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
list-style: none;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-top: 2px solid var(--primary);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
|
||||
z-index: 110;
|
||||
}
|
||||
.site-colleges:hover .colleges-menu { display: block; }
|
||||
.site-colleges .colleges-menu li a {
|
||||
display: block;
|
||||
padding: 9px 16px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.site-colleges .colleges-menu li a:hover { background: #f2f6fc; color: var(--primary); }
|
||||
.site-colleges .colleges-menu li.active a { color: var(--primary); font-weight: 600; }
|
||||
.site-nav { background: var(--primary); }
|
||||
.site-nav ul {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.site-nav li a {
|
||||
display: block;
|
||||
padding: 12px 22px;
|
||||
color: #fff;
|
||||
}
|
||||
.site-nav li:hover a, .site-nav li.active a { background: var(--primary-dark); }
|
||||
|
||||
/* ===== 二级下拉导航 ===== */
|
||||
/* 面板为白底纵向列表:需整体覆盖 .site-nav ul 的横向 flex 与内边距 */
|
||||
.site-nav > ul > li { position: relative; }
|
||||
.site-nav li.has-sub > a::after { content: " ▾"; font-size: 12px; opacity: .85; }
|
||||
.site-nav ul.sub-nav {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 168px;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
list-style: none;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-top: 2px solid var(--primary);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, .12);
|
||||
z-index: 100;
|
||||
}
|
||||
.site-nav li:hover > .sub-nav { display: block; }
|
||||
.site-nav .sub-nav li a {
|
||||
padding: 9px 16px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
background: none;
|
||||
}
|
||||
.site-nav .sub-nav li a:hover { background: #f2f6fc; color: var(--primary); }
|
||||
.site-nav .sub-nav li.active a { color: var(--primary); font-weight: 600; }
|
||||
|
||||
/* ===== 首页轮播 ===== */
|
||||
.banner { position: relative; max-width: var(--max-width); margin: 16px auto; overflow: hidden; }
|
||||
.banner-slides { position: relative; height: 420px; }
|
||||
.banner-slide { display: none; position: absolute; inset: 0; }
|
||||
.banner-slide.active { display: block; }
|
||||
.banner-slide img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.banner-title {
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
padding: 12px 20px;
|
||||
color: #fff;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, .6));
|
||||
}
|
||||
.banner-dots {
|
||||
position: absolute;
|
||||
bottom: 12px; right: 20px;
|
||||
display: flex; gap: 8px;
|
||||
}
|
||||
.banner-dots button {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, .6);
|
||||
cursor: pointer;
|
||||
}
|
||||
.banner-dots button.active { background: #fff; }
|
||||
|
||||
/* ===== 通用区块 ===== */
|
||||
.top-strip, .category-block {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 20px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.section-title, .block-head h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 20px;
|
||||
border-left: 4px solid var(--primary);
|
||||
padding-left: 10px;
|
||||
}
|
||||
.block-head { display: flex; align-items: baseline; justify-content: space-between; }
|
||||
.more { font-size: 13px; color: var(--text-light); }
|
||||
.top-strip ul, .block-list { list-style: none; margin: 0; padding: 0; }
|
||||
.top-strip li, .block-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 9px 0;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
}
|
||||
.top-strip li:last-child, .block-list li:last-child { border-bottom: none; }
|
||||
.top-strip li a, .block-list li a {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.top-badge {
|
||||
flex: none;
|
||||
color: #fff;
|
||||
background: var(--primary);
|
||||
font-size: 12px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ===== 列表页 ===== */
|
||||
.list-main {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 24px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.breadcrumb { color: var(--text-light); font-size: 13px; margin-bottom: 16px; }
|
||||
.breadcrumb a { margin-right: 8px; }
|
||||
.breadcrumb span { margin-left: 8px; }
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
border-left: 4px solid var(--primary);
|
||||
padding-left: 10px;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
.article-list { list-style: none; margin: 0; padding: 0; }
|
||||
.article-list li { padding: 14px 0; border-bottom: 1px dashed var(--border); }
|
||||
.item-title { font-size: 17px; }
|
||||
.item-summary {
|
||||
color: var(--text-light);
|
||||
font-size: 14px;
|
||||
margin: 6px 0;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.item-meta { display: flex; gap: 10px; align-items: center; }
|
||||
|
||||
/* ===== 分页 ===== */
|
||||
.pagination { display: flex; gap: 6px; justify-content: center; padding: 24px 0 4px; }
|
||||
.pagination a {
|
||||
min-width: 34px;
|
||||
text-align: center;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card-bg);
|
||||
}
|
||||
.pagination a.active { background: var(--primary); color: #fff; border-color: var(--primary); }
|
||||
|
||||
/* ===== 详情/单页 ===== */
|
||||
.detail-main {
|
||||
max-width: var(--max-width);
|
||||
margin: 16px auto;
|
||||
padding: 32px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.article-title { margin: 8px 0 12px; font-size: 26px; text-align: center; }
|
||||
.article-meta {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
color: var(--text-light);
|
||||
font-size: 13px;
|
||||
padding-bottom: 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.article-content { line-height: 1.9; font-size: 16px; padding-top: 20px; word-break: break-word; }
|
||||
.article-content img { height: auto; }
|
||||
.article-content table { border-collapse: collapse; width: 100%; }
|
||||
.article-content table td, .article-content table th { border: 1px solid var(--border); padding: 6px 10px; }
|
||||
.article-sibling { display: flex; flex-direction: column; gap: 8px; margin-top: 28px; font-size: 14px; }
|
||||
|
||||
/* ===== 页脚 ===== */
|
||||
.site-footer { margin-top: 32px; background: var(--primary-dark); color: rgba(255, 255, 255, .85); }
|
||||
.footer-links {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 18px 16px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.links-group { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; font-size: 13px; }
|
||||
.group-name { font-weight: 600; margin-right: 4px; }
|
||||
.footer-info {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 12px 16px 20px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.footer-info p { margin: 4px 0; }
|
||||
|
||||
/* ===== 移动端适配 ===== */
|
||||
@media (max-width: 768px) {
|
||||
.header-inner { flex-direction: column; align-items: stretch; }
|
||||
.site-search input { flex: 1; width: auto; }
|
||||
.site-colleges { align-self: flex-start; }
|
||||
.site-colleges .colleges-menu { left: 0; right: auto; min-width: 100%; }
|
||||
.banner-slides { height: 200px; }
|
||||
.detail-main, .list-main { padding: 18px 14px; }
|
||||
.article-title { font-size: 22px; }
|
||||
/* 触屏无 hover:子菜单静态平铺(缩进列表),不做浮层 */
|
||||
.site-nav ul.sub-nav {
|
||||
display: block;
|
||||
position: static;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
min-width: 0;
|
||||
padding: 0 0 6px;
|
||||
}
|
||||
.site-nav .sub-nav li a { color: rgba(255, 255, 255, .9); padding: 8px 16px 8px 34px; }
|
||||
.site-nav .sub-nav li a:hover { background: var(--primary-dark); color: #fff; }
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="detail-main">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<a th:if="${currentCategoryId != null}" th:href="${url.category(currentCategoryId)}">栏目</a>
|
||||
<span>正文</span>
|
||||
</nav>
|
||||
|
||||
<article class="article">
|
||||
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
|
||||
<div class="article-meta">
|
||||
<time th:if="${article.publishTime != null}"
|
||||
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
|
||||
<span th:if="${!#strings.isEmpty(article.authorName)}"
|
||||
th:text="'作者:' + ${article.authorName}">作者</span>
|
||||
<span th:if="${!#strings.isEmpty(article.source)}"
|
||||
th:text="'来源:' + ${article.source}">来源</span>
|
||||
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
|
||||
</div>
|
||||
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
|
||||
其余字段一律 th:text 转义输出 -->
|
||||
<div class="article-content" th:utext="${article.content}">正文</div>
|
||||
</article>
|
||||
|
||||
<nav class="article-sibling">
|
||||
<a th:if="${prevArticle != null}" class="prev"
|
||||
th:href="${url.article(prevArticle.id)}"
|
||||
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
|
||||
<a th:if="${nextArticle != null}" class="next"
|
||||
th:href="${url.article(nextArticle.id)}"
|
||||
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<!-- 公共页脚:联系信息 + 友情链接(分组渲染,方案 §8.1:友链不设独立路由) -->
|
||||
<footer class="site-footer" th:fragment="footer">
|
||||
<div class="footer-links" th:if="${!#lists.isEmpty(site.linkGroups)}">
|
||||
<div class="links-group" th:each="group : ${site.linkGroups}">
|
||||
<span class="group-name" th:text="${group.groupName}">分组</span>
|
||||
<a th:each="link : ${group.links}" th:href="${link.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${link.name}">链接</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-info">
|
||||
<p th:if="${site.config['contact'] != null}" th:text="${site.config['contact']}">联系方式</p>
|
||||
<p th:if="${site.config['copyright'] != null}" th:text="${site.config['copyright']}">版权信息</p>
|
||||
<p th:if="${site.config['beian'] != null}" th:text="${site.config['beian']}">备案号</p>
|
||||
</div>
|
||||
</footer>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<!-- 公共 head 片段:SEO(title/description/canonical)+ 主题资产(方案 §8.3/§8.6) -->
|
||||
<head th:fragment="head">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title th:text="${pageTitle}">页面标题</title>
|
||||
<meta name="description" th:if="${seoDescription != null and !#strings.isEmpty(seoDescription)}"
|
||||
th:content="${seoDescription}">
|
||||
<link rel="canonical" th:if="${canonical != null}" th:href="${canonical}">
|
||||
<link rel="stylesheet" th:href="${url.asset('css/main.css')}">
|
||||
<link th:if="${site.config['favicon'] != null}" rel="icon" th:href="${site.config['favicon']}">
|
||||
</head>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<!-- 公共头部:站点名 + 栏目导航 + 站内搜索 + 专业院系下拉(院系子站切换,《院系子站方案》) -->
|
||||
<header class="site-header" th:fragment="header">
|
||||
<div class="header-inner">
|
||||
<div class="site-brand">
|
||||
<img th:if="${site.config['logo'] != null}" class="site-logo"
|
||||
th:src="${site.config['logo']}" th:alt="${site.name}">
|
||||
<a class="site-name" th:href="${url.home()}" th:text="${site.name}">站点名称</a>
|
||||
</div>
|
||||
<form class="site-search" th:action="${url.search('', null)}" method="get">
|
||||
<input type="text" name="q" placeholder="站内搜索" aria-label="站内搜索">
|
||||
<button type="submit">搜索</button>
|
||||
</form>
|
||||
<!-- 专业院系下拉:顶层站取其子站、院系子站取兄弟站(含自身);无子站不渲染;文案经 config_json.colleges_label 覆盖 -->
|
||||
<div class="site-colleges" th:if="${!site.colleges.isEmpty()}"
|
||||
th:with="collegesLabel=${site.config['colleges_label'] ?: '专业院系'}">
|
||||
<span class="colleges-toggle">
|
||||
<a href="javascript:void(0)" th:text="${collegesLabel}">专业院系</a>
|
||||
<i class="colleges-arrow">▾</i>
|
||||
</span>
|
||||
<ul class="colleges-menu">
|
||||
<li th:each="col : ${site.colleges}" th:classappend="${col.code == site.code} ? 'active'">
|
||||
<a th:href="${url.siteHome(col.code, col.publicBaseUrl)}"
|
||||
th:title="${col.name}" th:text="${col.name}">学院名称</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="site-nav">
|
||||
<ul>
|
||||
<li th:classappend="${currentCategoryId == null} ? 'active'">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
</li>
|
||||
<li th:each="cat : ${navCategories}"
|
||||
th:classappend="${(currentCategoryId == cat.id ? 'active' : '') + (!cat.children.isEmpty() ? ' has-sub' : '')}">
|
||||
<!-- 外链栏目直接外跳;其余进栏目页(方案 §8.1) -->
|
||||
<a th:if="${cat.type == 3}" th:href="${cat.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${cat.name}">栏目</a>
|
||||
<a th:unless="${cat.type == 3}" th:href="${url.category(cat.id)}" th:text="${cat.name}">栏目</a>
|
||||
<!-- 二级下拉:悬停展开(样式见 assets/css/main.css .sub-nav) -->
|
||||
<ul class="sub-nav" th:if="${!cat.children.isEmpty()}">
|
||||
<li th:each="sub : ${cat.children}">
|
||||
<a th:if="${sub.type == 3}" th:href="${sub.linkUrl}" target="_blank" rel="noopener"
|
||||
th:text="${sub.name}">子栏目</a>
|
||||
<a th:unless="${sub.type == 3}" th:href="${url.category(sub.id)}" th:text="${sub.name}">子栏目</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="home-main">
|
||||
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
|
||||
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
|
||||
<div class="banner-slides">
|
||||
<a class="banner-slide" th:each="b, stat : ${banners}"
|
||||
th:classappend="${stat.first} ? 'active'"
|
||||
th:href="${b.linkUrl}" th:title="${b.title}">
|
||||
<img th:src="${b.imageUrl}" th:alt="${b.title}">
|
||||
<span class="banner-title" th:text="${b.title}">标题</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
|
||||
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
|
||||
th:aria-label="${'第' + (stat.count) + '张'}"></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 置顶/头条区(cms_article topmost 优先) -->
|
||||
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
|
||||
<h2 class="section-title">头条推荐</h2>
|
||||
<ul>
|
||||
<li th:each="item : ${topArticles}">
|
||||
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 一级栏目区块(每栏目 top 8) -->
|
||||
<section class="category-block" th:each="block : ${categoryBlocks}">
|
||||
<div class="block-head">
|
||||
<h2 th:text="${block.category.name}">栏目名</h2>
|
||||
<a class="more" th:href="${block.moreUrl}">更多 ></a>
|
||||
</div>
|
||||
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
|
||||
<li th:each="item : ${block.items}">
|
||||
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="list-main">
|
||||
<nav class="breadcrumb" th:if="${searchMode != true}">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span th:text="${category.name}">栏目</span>
|
||||
</nav>
|
||||
<nav class="breadcrumb" th:if="${searchMode == true}">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span>搜索结果</span>
|
||||
</nav>
|
||||
|
||||
<h1 class="page-title" th:if="${searchMode != true}" th:text="${category.name}">栏目名</h1>
|
||||
<h1 class="page-title" th:if="${searchMode == true}" th:text="'搜索:' + ${q}">搜索词</h1>
|
||||
|
||||
<ul class="article-list">
|
||||
<li th:each="item : ${listData.items}">
|
||||
<a class="item-title" th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<p class="item-summary" th:if="${!#strings.isEmpty(item.summary)}" th:text="${item.summary}">摘要</p>
|
||||
<div class="item-meta">
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="empty" th:if="${#lists.isEmpty(listData.items)}">暂无内容</p>
|
||||
|
||||
<!-- 分页(列表页/搜索页共用) -->
|
||||
<nav class="pagination" th:if="${listData.totalPages > 1}"
|
||||
th:with="cur=${listData.page}, total=${listData.totalPages}">
|
||||
<a th:if="${cur > 1}"
|
||||
th:href="${searchMode == true} ? ${url.search(q, cur - 1)} : ${url.category(listData.categoryId) + '?page=' + (cur - 1)}">上一页</a>
|
||||
<a th:each="p : ${#numbers.sequence(1, total)}"
|
||||
th:classappend="${p == cur} ? 'active'"
|
||||
th:href="${searchMode == true} ? ${url.search(q, p)} : ${url.category(listData.categoryId) + '?page=' + p}"
|
||||
th:text="${p}">1</a>
|
||||
<a th:if="${cur < total}"
|
||||
th:href="${searchMode == true} ? ${url.search(q, cur + 1)} : ${url.category(listData.categoryId) + '?page=' + (cur + 1)}">下一页</a>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="detail-main">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span th:text="${category.name}">栏目</span>
|
||||
</nav>
|
||||
|
||||
<article class="article">
|
||||
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
|
||||
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
|
||||
<div class="article-content" th:utext="${category.content}">单页内容</div>
|
||||
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"specVersion": 1,
|
||||
"name": "t-mismatch-x",
|
||||
"title": "清单名与目录不一致(夹具)",
|
||||
"version": "1.0.0",
|
||||
"screenshot": "assets/screenshot.png",
|
||||
"params": []
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="detail-main">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<a th:if="${currentCategoryId != null}" th:href="${url.category(currentCategoryId)}">栏目</a>
|
||||
<span>正文</span>
|
||||
</nav>
|
||||
|
||||
<article class="article">
|
||||
<h1 class="article-title" th:text="${article.title}">文章标题</h1>
|
||||
<div class="article-meta">
|
||||
<time th:if="${article.publishTime != null}"
|
||||
th:text="${'发布时间:' + #temporals.format(article.publishTime, 'yyyy-MM-dd HH:mm')}">2026-01-01 10:00</time>
|
||||
<span th:if="${!#strings.isEmpty(article.authorName)}"
|
||||
th:text="'作者:' + ${article.authorName}">作者</span>
|
||||
<span th:if="${!#strings.isEmpty(article.source)}"
|
||||
th:text="'来源:' + ${article.source}">来源</span>
|
||||
<span th:text="'浏览量:' + ${article.viewCount}">浏览量</span>
|
||||
</div>
|
||||
<!-- 正文:入库前已经 jsoup 白名单清洗(方案 §8.5),此处允许 utext;
|
||||
其余字段一律 th:text 转义输出 -->
|
||||
<div class="article-content" th:utext="${article.content}">正文</div>
|
||||
</article>
|
||||
|
||||
<nav class="article-sibling">
|
||||
<a th:if="${prevArticle != null}" class="prev"
|
||||
th:href="${url.article(prevArticle.id)}"
|
||||
th:text="'上一篇:' + ${prevArticle.title}">上一篇</a>
|
||||
<a th:if="${nextArticle != null}" class="next"
|
||||
th:href="${url.article(nextArticle.id)}"
|
||||
th:text="'下一篇:' + ${nextArticle.title}">下一篇</a>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="home-main">
|
||||
<!-- 首页轮播位(cms_banner position=home;设计尺寸约定 1920×560,见 assets/css 注释) -->
|
||||
<section class="banner" th:if="${!#lists.isEmpty(banners)}">
|
||||
<div class="banner-slides">
|
||||
<a class="banner-slide" th:each="b, stat : ${banners}"
|
||||
th:classappend="${stat.first} ? 'active'"
|
||||
th:href="${b.linkUrl}" th:title="${b.title}">
|
||||
<img th:src="${b.imageUrl}" th:alt="${b.title}">
|
||||
<span class="banner-title" th:text="${b.title}">标题</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="banner-dots" th:if="${#lists.size(banners) > 1}">
|
||||
<button th:each="b, stat : ${banners}" th:classappend="${stat.first} ? 'active'"
|
||||
th:aria-label="${'第' + (stat.count) + '张'}"></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 置顶/头条区(cms_article topmost 优先) -->
|
||||
<section class="top-strip" th:if="${!#lists.isEmpty(topArticles)}">
|
||||
<h2 class="section-title">头条推荐</h2>
|
||||
<ul>
|
||||
<li th:each="item : ${topArticles}">
|
||||
<span class="top-badge" th:if="${item.topmost == 1}">置顶</span>
|
||||
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 一级栏目区块(每栏目 top 8) -->
|
||||
<section class="category-block" th:each="block : ${categoryBlocks}">
|
||||
<div class="block-head">
|
||||
<h2 th:text="${block.category.name}">栏目名</h2>
|
||||
<a class="more" th:href="${block.moreUrl}">更多 ></a>
|
||||
</div>
|
||||
<ul class="block-list" th:if="${!#lists.isEmpty(block.items)}">
|
||||
<li th:each="item : ${block.items}">
|
||||
<a th:href="${url.article(item.id)}" th:text="${item.title}">文章标题</a>
|
||||
<time th:if="${item.publishTime != null}"
|
||||
th:text="${#temporals.format(item.publishTime, 'yyyy-MM-dd')}">2026-01-01</time>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="empty" th:if="${#lists.isEmpty(block.items)}">暂无内容</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
<script th:src="${url.asset('js/main.js')}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
||||
<head th:replace="~{${themeBase} + '/fragments/head' :: head}"></head>
|
||||
<body>
|
||||
<header th:replace="~{${themeBase} + '/fragments/header' :: header}"></header>
|
||||
|
||||
<main class="detail-main">
|
||||
<nav class="breadcrumb">
|
||||
<a th:href="${url.home()}">首页</a>
|
||||
<span th:text="${category.name}">栏目</span>
|
||||
</nav>
|
||||
|
||||
<article class="article">
|
||||
<h1 class="article-title" th:text="${category.name}">栏目名</h1>
|
||||
<!-- 单页栏目内容(type=2,入库前已 jsoup 清洗) -->
|
||||
<div class="article-content" th:utext="${category.content}">单页内容</div>
|
||||
<p class="empty" th:if="${#strings.isEmpty(category.content)}">暂无内容</p>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer th:replace="~{${themeBase} + '/fragments/footer' :: footer}"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"specVersion": 1,
|
||||
"name": "t-missing-list",
|
||||
"title": "缺列表模板(夹具)",
|
||||
"version": "1.0.0",
|
||||
"params": []
|
||||
}
|
||||
Reference in New Issue
Block a user