增加豆瓣、Steam、装备管理适配页面

This commit is contained in:
nxxy335top
2026-05-19 11:07:22 +08:00
parent 0021045c5f
commit 1bf7107966
13 changed files with 1620 additions and 251 deletions
+36 -19
View File
@@ -1,8 +1,20 @@
# WarmIsland 暖屿
一座深夜里温暖、安静、治愈的小岛 — 具有独特气质的生活博客主题。
一座深夜里温暖、安静、治愈的小岛 — 具有独特气质的生活博客主题,适用于 [Halo CMS](https://github.com/halo-dev/halo)
> 本项目由AI开发完成,人工仅进行部分功能测试及问题找寻
> 本项目由 AI 开发完成,人工仅进行部分功能测试及问题找寻
## 特点
1. **响应式设计** — 适配桌面端与移动端,支持暗色模式
2. **插件兼容性** — 适配 8 款常用 Halo 插件,安装即用
3. **内置灯箱** — 主题内置 LightGallery 灯箱,支持 EXIF 信息展示;安装 lightgallery.js 插件后自动禁用内置灯箱
4. **可配置性** — 丰富的后台设置项,支持自定义首页模块、文章卡片、导航栏样式等
5. **性能优化** — 图片懒加载、CSS/HTML 压缩、requestAnimationFrame 节流、Vue 组件按需加载
## 预览
![WarmIsland Preview](https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=A%20warm%20and%20cozy%20blog%20theme%20preview%2C%20soft%20colors%2C%20clean%20layout%2C%20minimalist%20design%2C%20dark%20mode%20support&image_size=landscape_16_9)
## 已适配插件
@@ -12,9 +24,29 @@
| [图库](https://halo.run/store/apps/app-BmQJW) | `PluginPhotos` | `/photos` | 图片展示页面,支持分组、灯箱预览 |
| [友情链接](https://halo.run/store/apps/app-hqbe) | `PluginLinks` | `/links` | 友链展示页面,支持分组展示 |
| [朋友圈](https://github.com/halo-sigs/plugin-friends) | `plugin-friends` | `/friends` | RSS 订阅朋友圈页面 |
| [我的装备](https://github.com/halo-sigs/plugin-equipment) | `equipment` | `/equipment` | 装备展示页面,支持分组展示 |
| [装备管理](https://github.com/chengzhongxue/plugin-equipment) | `equipment` | `/equipments` | 装备展示页面,分组展示,支持封面图 |
| [豆瓣](https://github.com/chengzhongxue/plugin-douban) | `plugin-douban` | `/douban` | 豆瓣记录页面,支持类型/状态/标签筛选,默认电影 |
| [Steam 信息展示](https://github.com/Tim0x0/halo-plugin-steam) | `PluginSteam` | `/steam` | Steam 游戏库页面,含用户资料、统计数据、最近游玩、游戏库分页 |
| [足迹](https://github.com/acanyo/halo-plugin-footprint) | `footprint` | `/footprints` | 足迹地图页面,保留插件原有样式 + 主题导航栏 |
> **注意**:插件需要单独安装,安装后主题会自动适配对应页面。若未安装某插件,对应页面会显示提示信息
> **注意**:插件需要单独安装,安装后主题会自动适配对应页面。若未安装某插件,对应页面不会出现
## 灯箱集成
主题内置 LightGallery 灯箱,为图片提供放大预览功能,并支持 EXIF 信息展示。
### lightgallery.js 插件兼容
如安装 [lightgallery.js](https://www.lightgalleryjs.com/) 灯箱插件,主题内置灯箱将自动禁用,改由插件接管灯箱功能。以下为 lightgallery.js 插件的路径匹配规则和 DOM 节点选择器:
| 页面 | 路径匹配规则 | 匹配区域 DOM 节点 |
|------|-------------|-------------------|
| 文章详情页 | `/archives/*` | `.wi-post__body` |
| 瞬间页 | `/moments` | `.wi-moments-page__content` |
| 图库页 | `/photos` | `.wi-photos-page__grid` |
| 自定义页面 | 用户自定义 | `.wi-page__body` |
> **注意**:自定义页面的路径匹配由用户自行设定,此处仅提供 DOM 节点选择器 `.wi-page__body`。
## 安装
@@ -33,21 +65,6 @@ npm run build
构建产物在 `templates/` 目录下,将其复制到 Halo 主题目录即可。
## lightgallery.js 灯箱插件集成
如果你使用 [lightgallery.js](https://www.lightgalleryjs.com/) 灯箱插件来为图片添加放大预览功能,以下是各页面的路径匹配规则和 DOM 节点选择器:
### 路径匹配与 DOM 节点
| 页面 | 路径匹配规则 | 匹配区域 DOM 节点 |
|------|-------------|-------------------|
| 文章详情页 | `/archives/*` | `.wi-post__body` |
| 瞬间页 | `/moments` | `.wi-moments-page__content` |
| 图库页 | `/photos` | `.wi-photos-page__grid` |
| 自定义页面 | 用户自定义 | `.wi-page__body` |
> **注意**:自定义页面的路径匹配由用户自行设定,此处仅提供 DOM 节点选择器 `.wi-page__body`。
## 开发
```bash
-7
View File
@@ -386,13 +386,6 @@ spec:
name: friends_page_title
label: 页面标题
value: 朋友圈
- group: equipment
label: 我的装备
formSchema:
- $formkit: text
name: equipment_page_title
label: 页面标题
value: 装备
- group: messageboard
label: 留言板
formSchema:
+85 -5
View File
@@ -35,12 +35,16 @@ const today = new Date();
</div>
</div>
<div th:if="${theme.config?.footer?.footer_custom_html}" th:utext="${theme.config?.footer?.footer_custom_html}" class="footer-custom"></div>
<halo:footer />
<div class="footer-injected">
<halo:footer />
</div>
</div>
</footer>
<style>
footer {
position: relative;
z-index: 1;
border-top: 1px solid var(--rule);
background: linear-gradient(
to bottom,
@@ -53,11 +57,11 @@ const today = new Date();
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 0 1.25rem;
min-height: 50px;
padding: 1.25rem;
display: flex;
flex-direction: column;
align-items: center;
flex-wrap: wrap;
gap: 0.75rem;
}
.footer-bottom {
@@ -142,9 +146,75 @@ const today = new Date();
}
.footer-custom {
margin-left: auto;
width: 100%;
text-align: center;
font-size: 0.85rem;
color: var(--ink-3);
line-height: 1.8;
}
.footer-custom :global(a),
.footer-custom :global(p),
.footer-custom :global(span) {
display: inline;
margin-inline: 0.25em;
}
.footer-custom :global(a) {
color: var(--ink-3);
text-decoration: none;
transition: color 0.15s ease;
}
.footer-custom :global(a):hover {
color: var(--ink-2);
}
.footer-custom :global(img) {
display: inline-block;
max-height: 26px;
vertical-align: middle;
border-radius: 4px;
}
.footer-custom :global(div) {
text-align: center;
}
.footer-injected {
width: 100%;
text-align: center;
font-size: 0.85rem;
color: var(--ink-3);
line-height: 1.8;
}
.footer-injected :global(a),
.footer-injected :global(p),
.footer-injected :global(span) {
display: inline;
margin-inline: 0.25em;
}
.footer-injected :global(a) {
color: var(--ink-3);
text-decoration: none;
transition: color 0.15s ease;
}
.footer-injected :global(a):hover {
color: var(--ink-2);
}
.footer-injected :global(img) {
display: inline-block;
max-height: 26px;
vertical-align: middle;
border-radius: 4px;
}
.footer-injected :global(div) {
text-align: center;
}
@media (max-width: 768px) {
@@ -161,5 +231,15 @@ const today = new Date();
.footer-bottom__right {
justify-content: center;
}
.footer-custom,
.footer-injected {
font-size: 0.75rem;
}
.footer-custom :global(img),
.footer-injected :global(img) {
max-height: 20px;
}
}
</style>
+22 -8
View File
@@ -3,6 +3,7 @@
<section
class="hero"
id="wi-hero"
th:if="${theme.config?.hero?.hero_enabled != false}"
th:classappend="${theme.config?.animation?.animation_breath == false} ? 'hero--no-breath'"
>
@@ -17,7 +18,9 @@
></div>
</div>
<div class="hero__content">
<div class="hero__blur-overlay" id="wi-hero-blur-overlay"></div>
<div class="hero__content" id="wi-hero-content">
<h1
class="hero__title"
th:text="${theme.config?.hero?.hero_title != null and !#strings.isEmpty(theme.config?.hero?.hero_title) ? theme.config?.hero?.hero_title : site.title}"
@@ -43,7 +46,7 @@
></p>
</div>
<div class="hero__scroll-indicator" aria-hidden="true">
<div class="hero__scroll-indicator" id="wi-hero-scroll-indicator" aria-hidden="true">
<svg
width="24"
height="24"
@@ -105,9 +108,9 @@
<style>
.hero {
position: relative;
height: calc(100vh + 80px);
height: calc(100dvh + 80px);
position: fixed;
inset: 0;
z-index: 0;
display: flex;
align-items: center;
justify-content: center;
@@ -119,8 +122,6 @@
color-mix(in srgb, var(--bg) 90%, var(--mist-pink) 10%) 70%,
var(--bg) 100%
);
margin-top: -80px;
padding-top: 80px;
width: 100%;
}
@@ -130,6 +131,16 @@
pointer-events: none;
}
.hero__blur-overlay {
position: absolute;
inset: 0;
z-index: 1;
backdrop-filter: blur(0px);
-webkit-backdrop-filter: blur(0px);
will-change: backdrop-filter;
pointer-events: none;
}
.hero__orb {
position: absolute;
border-radius: 50%;
@@ -190,13 +201,14 @@
.hero__content {
position: relative;
z-index: 1;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: var(--space-xl);
gap: var(--space-lg);
transition: opacity 0.4s ease, transform 0.4s ease;
}
.hero__title {
@@ -230,8 +242,10 @@
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
z-index: 2;
color: var(--ink-3);
animation: scrollHint 2s ease-in-out infinite;
transition: opacity 0.4s ease;
}
.hero--no-breath .hero__orb {
+407
View File
@@ -0,0 +1,407 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout wide>
<Fragment slot="head">
<title th:text="|${title} - ${site.title}|"></title>
</Fragment>
<section class="wi-douban">
<div class="wi-container">
<div class="wi-douban__header">
<h1 class="wi-douban__title" th:text="${title}">豆瓣</h1>
<span class="wi-page-accent"></span>
</div>
<div class="wi-douban__filters">
<th:block th:with="urlType = ${param.type}, isDefaultType = ${urlType == null or #strings.isEmpty(urlType.toString())}, currentType = ${isDefaultType ? 'movie' : urlType.toString()}">
<div class="wi-douban__filter-group" th:if="${types != null and !#lists.isEmpty(types)}">
<a
th:each="t : ${types}"
class="wi-douban__filter-tag"
th:href="@{/douban(type=${t.key})}"
th:text="${t.name}"
th:classappend="${(isDefaultType and t.key == 'movie') or (!isDefaultType and urlType.toString() == t.key) ? 'wi-douban__filter-tag--active' : ''}"
></a>
</div>
<div class="wi-douban__filter-group wi-douban__status-filters">
<th:block th:if="${currentType == 'movie'}">
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type='movie',status='wish')}"
th:classappend="${param.status != null and param.status.toString() == 'wish' ? 'wi-douban__filter-tag--active' : ''}"
>想看</a>
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type='movie',status='done')}"
th:classappend="${param.status != null and param.status.toString() == 'done' ? 'wi-douban__filter-tag--active' : ''}"
>看过</a>
</th:block>
<th:block th:if="${currentType == 'book'}">
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type='book',status='wish')}"
th:classappend="${param.status != null and param.status.toString() == 'wish' ? 'wi-douban__filter-tag--active' : ''}"
>想看</a>
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type='book',status='doing')}"
th:classappend="${param.status != null and param.status.toString() == 'doing' ? 'wi-douban__filter-tag--active' : ''}"
>在看</a>
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type='book',status='done')}"
th:classappend="${param.status != null and param.status.toString() == 'done' ? 'wi-douban__filter-tag--active' : ''}"
>看过</a>
</th:block>
</div>
<div class="wi-douban__filter-group" th:if="${genres != null and !#lists.isEmpty(genres)}">
<th:block th:each="g : ${genres}" th:with="urlStatus = ${param.status}, hasStatus = ${urlStatus != null and !#strings.isEmpty(urlStatus.toString())}">
<a
th:if="${hasStatus}"
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type=${currentType}, status=${urlStatus.toString()}, genre=${g.name})}"
th:text="|${g.name}(${g.doubanCount})|"
th:classappend="${param.genre != null and param.genre.toString() == g.name ? 'wi-douban__filter-tag--active' : ''}"
></a>
<a
th:unless="${hasStatus}"
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type=${currentType}, genre=${g.name})}"
th:text="|${g.name}(${g.doubanCount})|"
th:classappend="${param.genre != null and param.genre.toString() == g.name ? 'wi-douban__filter-tag--active' : ''}"
></a>
</th:block>
</div>
</th:block>
</div>
<th:block th:with="urlType = ${param.type}, isDefaultType = ${urlType == null or #strings.isEmpty(urlType.toString())}, currentType = ${isDefaultType ? 'movie' : urlType.toString()}, urlStatus = ${param.status}, hasStatus = ${urlStatus != null and !#strings.isEmpty(urlStatus.toString())}, currentStatus = ${hasStatus ? urlStatus.toString() : null}, urlGenre = ${param.genre}, hasGenre = ${urlGenre != null and !#strings.isEmpty(urlGenre.toString())}, currentGenre = ${hasGenre ? urlGenre.toString() : null}, doubans = ${hasStatus ? doubanFinder.list(1, 100, currentType, currentStatus) : doubanFinder.listByType(1, 100, currentType)}">
<div class="wi-douban__grid">
<article
class="wi-douban__card"
th:each="item : ${doubans.items}"
th:if="${!hasGenre or #sets.contains(item.spec?.genres, currentGenre)}"
>
<a
class="wi-douban__card-link"
th:if="${!#strings.isEmpty(item.spec?.link)}"
th:href="${item.spec?.link}"
target="_blank"
rel="noopener noreferrer"
tabindex="-1"
aria-hidden="true"
></a>
<div class="wi-douban__poster" th:if="${!#strings.isEmpty(item.spec?.poster)}">
<img
th:src="${item.spec?.poster}"
th:alt="${item.spec?.name}"
class="wi-douban__poster-img"
loading="lazy"
decoding="async"
/>
</div>
<div class="wi-douban__poster wi-douban__poster--placeholder" th:if="${#strings.isEmpty(item.spec?.poster)}">
<span th:text="${#strings.substring(item.spec?.name ?: '', 0, 1)}"></span>
</div>
<div class="wi-douban__body">
<h3 class="wi-douban__name" th:text="${item.spec?.name}"></h3>
<p
class="wi-douban__subtitle"
th:if="${!#strings.isEmpty(item.spec?.cardSubtitle)}"
th:text="${item.spec?.cardSubtitle}"
></p>
<div class="wi-douban__meta">
<span
class="wi-douban__score"
th:if="${!#strings.isEmpty(item.spec?.score) and item.spec?.score != '0'}"
>
<svg class="wi-douban__star" viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
<th:block th:text="${item.spec?.score}"></th:block>
</span>
<span
class="wi-douban__my-score"
th:if="${!#strings.isEmpty(item.faves?.score)}"
>
我的:<th:block th:text="${item.faves?.score}"></th:block>
</span>
<span
class="wi-douban__year"
th:if="${!#strings.isEmpty(item.spec?.year)}"
th:text="${item.spec?.year}"
></span>
<span
class="wi-douban__type-badge"
th:if="${!#strings.isEmpty(item.spec?.type)}"
th:text="${item.spec?.type}"
></span>
</div>
<div class="wi-douban__tags" th:if="${item.spec?.genres != null and !#sets.isEmpty(item.spec?.genres)}">
<span
class="wi-douban__tag"
th:each="genre : ${item.spec?.genres}"
th:text="${genre}"
></span>
</div>
<p
class="wi-douban__remark"
th:if="${!#strings.isEmpty(item.faves?.remark)}"
th:text="${item.faves?.remark}"
></p>
<span
class="wi-douban__status"
th:if="${!#strings.isEmpty(item.faves?.status)}"
th:text="${item.faves?.status == 'done' ? '看过' : (item.faves?.status == 'doing' ? '在看' : '想看')}"
></span>
</div>
</article>
</div>
</th:block>
</div>
</section>
</Layout>
<style>
.wi-douban {
padding-block: 1rem;
}
.wi-douban__header {
text-align: center;
margin-bottom: var(--space-3xl);
}
.wi-douban__title {
font-size: var(--text-4xl);
margin-bottom: var(--space-sm);
}
.wi-page-accent {
display: block;
width: 48px;
height: 3px;
margin: var(--space-sm) auto 0;
border-radius: 9999px;
background: var(--accent);
}
.wi-douban__filters {
display: flex;
flex-direction: column;
gap: var(--space-md);
margin-bottom: var(--space-2xl);
}
.wi-douban__filter-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
}
.wi-douban__filter-tag {
display: inline-block;
padding: 6px 16px;
border-radius: 20px;
font-size: var(--text-sm);
color: var(--ink-2);
background: var(--bg-raised);
border: 1px solid var(--rule);
text-decoration: none;
transition: color var(--duration-fast) var(--ease-out-quart),
background var(--duration-fast) var(--ease-out-quart),
border-color var(--duration-fast) var(--ease-out-quart);
}
.wi-douban__filter-tag--sm {
padding: 4px 12px;
font-size: var(--text-xs);
}
.wi-douban__filter-tag:hover {
color: var(--accent);
border-color: var(--accent);
}
.wi-douban__filter-tag--active {
color: #fff;
background: var(--accent);
border-color: var(--accent);
}
.wi-douban__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--space-lg);
}
.wi-douban__card {
position: relative;
display: flex;
gap: var(--space-md);
padding: var(--space-lg);
background: var(--bg-raised);
border: 1px solid var(--rule);
border-radius: 16px;
transition:
transform var(--duration-normal) var(--ease-out-expo),
box-shadow var(--duration-normal) var(--ease-out-expo),
border-color var(--duration-normal) var(--ease-out-expo);
}
.wi-douban__card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--accent);
}
.wi-douban__card-link {
position: absolute;
inset: 0;
z-index: 1;
}
.wi-douban__poster {
flex-shrink: 0;
width: 80px;
height: 112px;
border-radius: 8px;
overflow: hidden;
background: var(--bg);
}
.wi-douban__poster-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.wi-douban__poster--placeholder {
display: flex;
align-items: center;
justify-content: center;
background: var(--accent-bg);
color: var(--accent);
font-size: var(--text-2xl);
font-weight: 700;
}
.wi-douban__body {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
flex: 1;
}
.wi-douban__name {
font-size: var(--text-md);
font-weight: 600;
color: var(--ink);
line-height: var(--leading-tight);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wi-douban__subtitle {
font-size: var(--text-xs);
color: var(--ink-3);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wi-douban__meta {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
font-size: var(--text-xs);
color: var(--ink-3);
}
.wi-douban__score {
display: inline-flex;
align-items: center;
gap: 2px;
color: #f59e0b;
font-weight: 600;
}
.wi-douban__star {
color: #f59e0b;
}
.wi-douban__my-score {
color: var(--accent);
font-weight: 500;
}
.wi-douban__type-badge {
padding: 1px 6px;
border-radius: 4px;
background: var(--accent-bg);
color: var(--accent);
font-weight: 500;
}
.wi-douban__tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.wi-douban__tag {
font-size: 11px;
color: var(--ink-3);
padding: 1px 6px;
border-radius: 4px;
background: var(--bg);
border: 1px solid var(--rule);
}
.wi-douban__remark {
font-size: var(--text-sm);
color: var(--ink-2);
line-height: var(--leading-relaxed);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-top: 4px;
position: relative;
z-index: 2;
}
.wi-douban__status {
display: inline-block;
font-size: 11px;
padding: 2px 8px;
border-radius: 4px;
background: color-mix(in srgb, var(--accent) 10%, transparent);
color: var(--accent);
font-weight: 500;
width: fit-content;
}
@media (max-width: 640px) {
.wi-douban__grid {
grid-template-columns: 1fr;
}
.wi-douban__card {
padding: var(--space-md);
}
.wi-douban__poster {
width: 64px;
height: 90px;
}
}
</style>
-208
View File
@@ -1,208 +0,0 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout wide>
<Fragment slot="head">
<title th:text="|${theme.config?.equipment?.equipment_page_title ?: '装备'} - ${site.title}|"></title>
</Fragment>
<section
class="wi-equipment"
th:if="${pluginFinder.available('equipment')}"
>
<div class="wi-container">
<div class="wi-equipment__header">
<h1
class="wi-equipment__title"
th:text="${theme.config?.equipment?.equipment_page_title ?: '装备'}"
>
装备
</h1>
<span class="wi-page-accent"></span>
</div>
<th:block th:if="${equipmentFinder != null}">
<th:block th:each="group : ${equipmentFinder.listByGroup()}">
<h2
class="wi-equipment__group"
th:if="${group.spec?.displayName != null and !#strings.isEmpty(group.spec?.displayName)}"
th:text="${group.spec?.displayName}"
></h2>
<div class="wi-equipment__grid">
<div
class="wi-equipment__card"
th:each="equip : ${group.equipments}"
>
<img
th:if="${equip.spec?.cover}"
th:src="${equip.spec?.cover}"
th:alt="${equip.spec?.displayName}"
class="wi-equipment__avatar"
/>
<div
th:unless="${equip.spec?.cover}"
class="wi-equipment__avatar wi-equipment__avatar--placeholder"
>
<span
th:text="${#strings.substring(equip.spec?.displayName ?: '', 0, 1)}"
></span>
</div>
<div class="wi-equipment__info">
<span
class="wi-equipment__name"
th:text="${equip.spec?.displayName}"
></span>
</div>
</div>
</div>
</th:block>
</th:block>
<th:block th:if="${equipmentFinder == null}">
<div class="wi-equipment__empty">
<p>装备插件加载异常,请检查插件状态</p>
</div>
</th:block>
</div>
</section>
<section
class="wi-equipment"
th:unless="${pluginFinder.available('equipment')}"
>
<div class="wi-container">
<div class="wi-equipment__header">
<h1 class="wi-equipment__title">装备</h1>
<span class="wi-page-accent"></span>
</div>
<div class="wi-equipment__empty">
<p>装备功能需要安装「装备管理」插件,<a href="https://www.halo.run/store/apps/app-ytygyqml">前往应用市场安装</a></p>
</div>
</div>
</section>
</Layout>
<style>
.wi-equipment {
padding-block: 1rem;
}
.wi-equipment__header {
text-align: center;
margin-bottom: var(--space-3xl);
}
.wi-equipment__title {
font-size: var(--text-4xl);
margin-bottom: var(--space-sm);
}
.wi-page-accent {
display: block;
width: 48px;
height: 3px;
margin: var(--space-sm) auto 0;
border-radius: 9999px;
background: var(--accent);
}
.wi-equipment__empty {
text-align: center;
padding: var(--space-4xl) var(--space-xl);
color: var(--ink-3);
font-size: var(--text-md);
}
.wi-equipment__empty a {
color: var(--accent);
}
.wi-equipment__group {
font-size: var(--text-xl);
color: var(--ink-2);
margin-bottom: var(--space-lg);
padding-bottom: var(--space-sm);
border-bottom: 1px solid var(--rule);
}
.wi-equipment__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--space-lg);
margin-bottom: var(--space-2xl);
}
.wi-equipment__card {
display: flex;
align-items: center;
gap: var(--space-md);
padding: var(--space-lg);
background: var(--glass-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
border-radius: 16px;
text-decoration: none;
color: inherit;
transition:
transform var(--duration-normal) var(--ease-out-expo),
box-shadow var(--duration-normal) var(--ease-out-expo),
border-color var(--duration-normal) var(--ease-out-expo);
}
.wi-equipment__card:hover {
transform: translateY(-6px);
box-shadow: var(--shadow-lg);
border-color: var(--accent);
color: inherit;
}
.wi-equipment__avatar {
width: 52px;
height: 52px;
border-radius: 16px;
object-fit: cover;
flex-shrink: 0;
box-shadow: var(--shadow-sm);
}
.wi-equipment__avatar--placeholder {
display: flex;
align-items: center;
justify-content: center;
background: var(--accent-bg);
color: var(--accent);
font-family: var(--font-sans);
font-size: var(--text-lg);
font-weight: 700;
box-shadow: none;
}
.wi-equipment__info {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.wi-equipment__name {
font-size: var(--text-md);
font-weight: 600;
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 640px) {
.wi-equipment__grid {
grid-template-columns: 1fr;
gap: var(--space-md);
}
.wi-equipment__card {
padding: var(--space-md);
}
}
</style>
+219
View File
@@ -0,0 +1,219 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout wide>
<Fragment slot="head">
<title th:text="|${title} - ${site.title}|"></title>
</Fragment>
<section class="wi-equipments">
<div class="wi-container">
<div class="wi-equipments__header">
<h1 class="wi-equipments__title" th:text="${title}">装备</h1>
<span class="wi-page-accent"></span>
</div>
<div class="wi-equipments__groups">
<th:block th:each="group : ${equipmentFinder.groupBy()}">
<div class="wi-equipments__group">
<h2 class="wi-equipments__group-title" th:text="${group.spec?.displayName}"></h2>
<div class="wi-equipments__grid">
<article
class="wi-equipments__card"
th:each="equip : ${group.equipments}"
>
<a
class="wi-equipments__card-link"
th:if="${!#strings.isEmpty(equip.spec?.url)}"
th:href="${equip.spec?.url}"
target="_blank"
rel="noopener noreferrer"
tabindex="-1"
aria-hidden="true"
></a>
<div class="wi-equipments__cover" th:if="${!#strings.isEmpty(equip.spec?.cover)}">
<img
th:src="${equip.spec?.cover}"
th:alt="${equip.spec?.displayName}"
class="wi-equipments__cover-img"
loading="lazy"
decoding="async"
/>
</div>
<div
class="wi-equipments__cover wi-equipments__cover--placeholder"
th:if="${#strings.isEmpty(equip.spec?.cover)}"
>
<span th:text="${#strings.substring(equip.spec?.displayName ?: '', 0, 1)}"></span>
</div>
<div class="wi-equipments__body">
<h3
class="wi-equipments__name"
th:text="${equip.spec?.displayName}"
></h3>
<p
class="wi-equipments__spec"
th:if="${!#strings.isEmpty(equip.spec?.specification)}"
th:text="${equip.spec?.specification}"
></p>
<p
class="wi-equipments__desc"
th:if="${!#strings.isEmpty(equip.spec?.description)}"
th:text="${equip.spec?.description}"
></p>
</div>
</article>
</div>
</div>
</th:block>
</div>
</div>
</section>
</Layout>
<style>
.wi-equipments {
padding-block: 1rem;
}
.wi-equipments__header {
text-align: center;
margin-bottom: var(--space-3xl);
}
.wi-equipments__title {
font-size: var(--text-4xl);
margin-bottom: var(--space-sm);
}
.wi-page-accent {
display: block;
width: 48px;
height: 3px;
margin: var(--space-sm) auto 0;
border-radius: 9999px;
background: var(--accent);
}
.wi-equipments__groups {
display: flex;
flex-direction: column;
gap: var(--space-3xl);
}
.wi-equipments__group-title {
font-size: var(--text-xl);
font-weight: 700;
color: var(--ink);
padding-bottom: var(--space-sm);
border-bottom: 2px solid var(--accent);
display: inline-block;
margin-bottom: var(--space-lg);
}
.wi-equipments__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: var(--space-lg);
}
.wi-equipments__card {
position: relative;
display: flex;
flex-direction: column;
background: var(--bg-raised);
border: 1px solid var(--rule);
border-radius: 16px;
overflow: hidden;
transition:
transform var(--duration-normal) var(--ease-out-expo),
box-shadow var(--duration-normal) var(--ease-out-expo),
border-color var(--duration-normal) var(--ease-out-expo);
}
.wi-equipments__card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--accent);
}
.wi-equipments__card-link {
position: absolute;
inset: 0;
z-index: 1;
}
.wi-equipments__cover {
width: 100%;
aspect-ratio: 1 / 1;
overflow: hidden;
background: var(--bg);
}
.wi-equipments__cover-img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform var(--duration-normal) var(--ease-out-expo);
}
.wi-equipments__card:hover .wi-equipments__cover-img {
transform: scale(1.05);
}
.wi-equipments__cover--placeholder {
display: flex;
align-items: center;
justify-content: center;
background: var(--accent-bg);
color: var(--accent);
font-size: var(--text-4xl);
font-weight: 700;
}
.wi-equipments__body {
display: flex;
flex-direction: column;
gap: 4px;
padding: var(--space-md) var(--space-lg) var(--space-lg);
min-width: 0;
}
.wi-equipments__name {
font-size: var(--text-md);
font-weight: 600;
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wi-equipments__spec {
font-size: var(--text-sm);
color: var(--accent);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wi-equipments__desc {
font-size: var(--text-xs);
color: var(--ink-3);
line-height: var(--leading-relaxed);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
@media (max-width: 640px) {
.wi-equipments__grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
.wi-equipments__body {
padding: var(--space-sm) var(--space-md) var(--space-md);
}
}
</style>
+161
View File
@@ -0,0 +1,161 @@
---
import Navbar from "../components/Navbar.astro";
import MobileMenu from "../components/MobileMenu.astro";
import "../styles/main.scss";
---
<!doctype html>
<html lang="zh" th:lang="${theme.config?.basic?.language ?: #locale.language}" th:classappend="${theme.config?.animation?.animation_enabled == false} ? 'wi-no-animations'">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script is:inline th:attr="data-scheme=${theme.config?.style?.color_scheme}">
(function () {
var stored = localStorage.getItem("wi-theme");
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
var scheme = null;
if (document.currentScript && document.currentScript.getAttribute("data-scheme")) {
scheme = document.currentScript.getAttribute("data-scheme");
}
var isDark = false;
if (stored === "dark") { isDark = true; }
else if (stored === "light") { isDark = false; }
else if (scheme === "dark") { isDark = true; }
else if (scheme === "light") { isDark = false; }
else if (!stored && prefersDark) { isDark = true; }
if (isDark) {
document.documentElement.classList.add("dark");
document.documentElement.setAttribute("data-color-scheme", "dark");
} else {
document.documentElement.classList.remove("dark");
document.documentElement.setAttribute("data-color-scheme", "light");
}
})();
</script>
<title th:text="|${settings.title} - ${site.title}|"></title>
<link rel="stylesheet" type="text/css" th:href="@{/plugins/footprint/assets/static/css/footprint.css(version=${version})}" />
<link rel="stylesheet" type="text/css" th:href="@{/plugins/footprint/assets/static/font/result.css(version=${version})}" />
<script type="text/javascript" th:src="@{/plugins/footprint/assets/static/js/jquery-3.6.0.min.js}"></script>
<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode: /*[[${settings.gaoDeKey}]]*/''
}
</script>
<script type="text/javascript" th:src="|https://webapi.amap.com/maps?v=2.0&key=${settings.gaoDeKey}|"></script>
<script type="text/javascript" th:src="@{/plugins/footprint/assets/static/js/footprint.js(version=${version})}"></script>
<script th:inline="javascript">
window.FOOTPRINT_CONFIG = {
amapKey: /*[[${settings.gaoDeKey}]]*/'',
footprints: [],
title: /*[[${settings.title}]]*/ '足迹',
logoName: /*[[${settings.logoName}]]*/ '足迹',
describe: /*[[${settings.describe}]]*/ '描述',
hsla: /*[[${settings.hsla}]]*/ '200, 100%, 50%',
mapStyle: /*[[${settings.mapStyle}]]*/ 'amap://styles/normal'
};
</script>
</head>
<body id="footprint-page">
<Navbar />
<MobileMenu />
<div class="footprint-container">
<div id="footprint-map"></div>
<div class="logo-container">
<h1 class="footprint-logo" th:text="${settings.logoName}">足迹<span class="logo-version">2024</span></h1>
<p class="footprint-slogan" th:text="${settings.describe}">每一处足迹都充满了故事,那是对人生的思考和无限的风光。</p>
</div>
<div class="map-controls">
<div class="likcc-layer-selector">
<button class="likcc-layer-btn active" data-type="normal">
<svg class="likcc-layer-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<path d="M9 9h6v6H9z"></path>
</svg>
<span>标准图</span>
</button>
<button class="likcc-layer-btn" data-type="satellite">
<svg class="likcc-layer-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
</svg>
<span>卫星图</span>
</button>
</div>
<div class="likcc-feature-toggles">
<label class="likcc-toggle-item">
<input type="checkbox" data-type="road">
<div class="likcc-toggle-switch">
<div class="likcc-toggle-slider"></div>
</div>
<div class="likcc-toggle-content">
<svg class="likcc-toggle-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 7v10a4 4 0 004 4h10a4 4 0 004-4V7a4 4 0 00-4-4H7a4 4 0 00-4 4z"></path>
<path d="M9 12h6"></path>
</svg>
<span class="likcc-toggle-label">路网</span>
</div>
</label>
<label class="likcc-toggle-item">
<input type="checkbox" data-type="traffic">
<div class="likcc-toggle-switch">
<div class="likcc-toggle-slider"></div>
</div>
<div class="likcc-toggle-content">
<svg class="likcc-toggle-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
</svg>
<span class="likcc-toggle-label">路况</span>
</div>
</label>
</div>
<div class="likcc-zoom-controls">
<button class="likcc-zoom-btn" id="zoom-out">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<path d="M21 21l-4.35-4.35"></path>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
</button>
<button class="likcc-zoom-btn" id="zoom-in">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<path d="M21 21l-4.35-4.35"></path>
<line x1="11" y1="8" x2="11" y2="14"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
</button>
</div>
<div class="likcc-scale-display">
<div class="likcc-scale-bar"></div>
<span class="likcc-scale-text" id="scale-text">1000 公里</span>
</div>
</div>
</div>
</body>
</html>
<style>
#footprint-page {
margin: 0;
padding: 0;
}
#footprint-page .footprint-container {
position: relative;
width: 100%;
height: calc(100vh - 72px);
margin-top: 72px;
}
#footprint-page #footprint-map {
width: 100%;
height: 100%;
}
</style>
+62
View File
@@ -10,6 +10,10 @@ import HeroSection from "../components/HeroSection.astro";
<HeroSection />
<div
class="wi-home-content"
th:classappend="${theme.config?.hero?.hero_enabled != false} ? 'wi-home-content--with-hero'"
>
<section
class="wi-home-pinned"
th:if="${posts != null and !#lists.isEmpty(posts.items)}"
@@ -181,6 +185,7 @@ import HeroSection from "../components/HeroSection.astro";
</div>
</div>
</section>
</div>
</Layout>
<script is:inline>
@@ -252,7 +257,64 @@ import HeroSection from "../components/HeroSection.astro";
})();
</script>
<script is:inline>
(function () {
var hero = document.getElementById("wi-hero");
var blurOverlay = document.getElementById("wi-hero-blur-overlay");
var heroContent = document.getElementById("wi-hero-content");
var scrollIndicator = document.getElementById("wi-hero-scroll-indicator");
if (!hero || !blurOverlay) return;
var maxBlur = 20;
var triggerDistance = 300;
var fadeStart = 200;
var fadeEnd = 500;
var ticking = false;
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(function () {
ticking = false;
var scrollY = window.scrollY || window.pageYOffset;
var currentBlur = Math.min(scrollY / triggerDistance * maxBlur, maxBlur);
blurOverlay.style.backdropFilter = "blur(" + currentBlur + "px)";
blurOverlay.style.webkitBackdropFilter = "blur(" + currentBlur + "px)";
if (heroContent) {
var progress = Math.max(0, Math.min(1, (scrollY - fadeStart) / (fadeEnd - fadeStart)));
heroContent.style.opacity = String(1 - progress);
heroContent.style.transform = "translateY(" + (-20 * progress) + "px)";
}
if (scrollIndicator) {
var indicatorProgress = Math.max(0, Math.min(1, (scrollY - fadeStart) / (fadeEnd - fadeStart)));
scrollIndicator.style.opacity = String(1 - indicatorProgress);
}
});
}
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
})();
</script>
<style>
.wi-home-content {
position: relative;
z-index: 1;
background: var(--bg);
min-height: 100vh;
padding-bottom: 2rem;
}
.wi-home-content--with-hero {
margin-top: 100vh;
margin-top: 100dvh;
}
.wi-home-pinned {
padding-block: 2rem;
background: var(--bg);
+626
View File
@@ -0,0 +1,626 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout wide>
<Fragment slot="head">
<title th:text="|${title} - ${site.title}|"></title>
</Fragment>
<section class="wi-steam">
<div class="wi-container">
<div class="wi-steam__header">
<h1 class="wi-steam__title" th:text="${title}">Steam</h1>
<span class="wi-page-accent"></span>
</div>
<th:block th:with="profile = ${steamFinder.getProfile()}, stats = ${steamFinder.getStats()}, badges = ${steamFinder.getBadges()}, recentGames = ${steamFinder.getRecentGames(recentGamesLimit)}">
<div class="wi-steam__profile" th:if="${profile != null and profile.summary != null}">
<div class="wi-steam__avatar">
<img
th:src="${profile.summary?.avatarFull}"
th:alt="${profile.summary?.personaName}"
class="wi-steam__avatar-img"
loading="lazy"
decoding="async"
/>
<span
class="wi-steam__status-dot wi-steam__status-dot--playing"
th:if="${profile.playing}"
></span>
<span
class="wi-steam__status-dot wi-steam__status-dot--online"
th:if="${!profile.playing and profile.summary?.personaState != null and profile.summary?.personaState > 0}"
></span>
<span
class="wi-steam__status-dot wi-steam__status-dot--offline"
th:if="${!profile.playing and (profile.summary?.personaState == null or profile.summary?.personaState == 0)}"
></span>
</div>
<div class="wi-steam__info">
<h2 class="wi-steam__name" th:text="${profile.summary?.personaName}"></h2>
<span class="wi-steam__status-text" th:text="${profile.statusText}"></span>
<a
class="wi-steam__profile-link"
th:if="${!#strings.isEmpty(profile.summary?.profileUrl)}"
th:href="${profile.summary?.profileUrl}"
target="_blank"
rel="noopener noreferrer"
>Steam 主页 →</a>
</div>
<div class="wi-steam__level" th:if="${profile.steamLevel != null}">
<span class="wi-steam__level-num" th:text="${profile.steamLevel}"></span>
<span class="wi-steam__level-label">等级</span>
</div>
</div>
<div class="wi-steam__stats" th:if="${stats != null}">
<div class="wi-steam__stat-card">
<span class="wi-steam__stat-value" th:text="${stats.totalGames}"></span>
<span class="wi-steam__stat-label">拥有游戏</span>
</div>
<div class="wi-steam__stat-card">
<span class="wi-steam__stat-value" th:text="${stats.totalPlaytimeFormatted}"></span>
<span class="wi-steam__stat-label">总游玩时长</span>
</div>
<div class="wi-steam__stat-card">
<span class="wi-steam__stat-value" th:text="${stats.recentPlaytimeFormatted}"></span>
<span class="wi-steam__stat-label">近两周</span>
</div>
<div class="wi-steam__stat-card" th:if="${badges != null}">
<span class="wi-steam__stat-value" th:text="${badges.totalBadges}"></span>
<span class="wi-steam__stat-label">徽章</span>
</div>
</div>
<div class="wi-steam__section" th:if="${recentGames != null and !recentGames.isEmpty()}">
<h3 class="wi-steam__section-title">最近游玩</h3>
<div class="wi-steam__recent-scroll">
<article class="wi-steam__recent-card" th:each="game : ${recentGames}">
<a
class="wi-steam__card-link"
th:if="${enableGameLink}"
th:href="@{'https://store.steampowered.com/app/' + ${game.appId}}"
target="_blank"
rel="noopener noreferrer"
tabindex="-1"
aria-hidden="true"
></a>
<div class="wi-steam__recent-cover">
<img
th:src="${game.headerImageUrl}"
th:alt="${game.name}"
class="wi-steam__recent-img"
loading="lazy"
decoding="async"
/>
</div>
<div class="wi-steam__recent-body">
<h4 class="wi-steam__recent-name" th:text="${game.name}"></h4>
<div class="wi-steam__recent-meta">
<span class="wi-steam__recent-time" th:text="${game.playtime2WeeksFormatted}"></span>
<span
class="wi-steam__recent-achievements"
th:if="${game.achievementProgressText != null}"
th:text="${game.achievementProgressText}"
></span>
</div>
</div>
</article>
</div>
</div>
</th:block>
<div class="wi-steam__section">
<h3 class="wi-steam__section-title">游戏库</h3>
<div class="wi-steam__games-grid">
<article class="wi-steam__game-card" th:each="game : ${games.items}">
<a
class="wi-steam__card-link"
th:if="${enableGameLink}"
th:href="@{'https://store.steampowered.com/app/' + ${game.appId}}"
target="_blank"
rel="noopener noreferrer"
tabindex="-1"
aria-hidden="true"
></a>
<div class="wi-steam__game-cover">
<img
th:src="${game.headerImageUrl}"
th:alt="${game.name}"
class="wi-steam__game-img"
loading="lazy"
decoding="async"
/>
</div>
<div class="wi-steam__game-body">
<h4 class="wi-steam__game-name" th:text="${game.name}"></h4>
<div class="wi-steam__game-meta">
<span class="wi-steam__game-time" th:text="${game.playtimeFormatted}"></span>
<span
class="wi-steam__game-last"
th:if="${!#strings.isEmpty(game.lastPlayedFormatted)}"
th:text="|最后游玩 ${game.lastPlayedFormatted}|"
></span>
</div>
</div>
</article>
</div>
</div>
<nav
class="wi-steam__pagination"
th:if="${games.hasPrevious() or games.hasNext()}"
>
<a
class="wi-steam__page-btn"
th:if="${games.hasPrevious()}"
th:href="${games.prevUrl}"
>上一页</a>
<span class="wi-steam__page-spacer" th:if="${games.hasPrevious() and games.hasNext()}"></span>
<a
class="wi-steam__page-btn"
th:if="${games.hasNext()}"
th:href="${games.nextUrl}"
>下一页</a>
</nav>
</div>
</section>
</Layout>
<style>
.wi-steam {
padding-block: 1rem;
}
.wi-steam__header {
text-align: center;
margin-bottom: var(--space-3xl);
}
.wi-steam__title {
font-size: var(--text-4xl);
margin-bottom: var(--space-sm);
}
.wi-page-accent {
display: block;
width: 48px;
height: 3px;
margin: var(--space-sm) auto 0;
border-radius: 9999px;
background: var(--accent);
}
.wi-steam__profile {
display: flex;
align-items: center;
gap: var(--space-lg);
padding: var(--space-xl);
background: var(--bg-raised);
border: 1px solid var(--rule);
border-radius: 16px;
margin-bottom: var(--space-2xl);
}
.wi-steam__avatar {
position: relative;
flex-shrink: 0;
}
.wi-steam__avatar-img {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 3px solid var(--rule);
}
.wi-steam__status-dot {
position: absolute;
bottom: 4px;
right: 4px;
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid var(--bg-raised);
}
.wi-steam__status-dot--online {
background: #22c55e;
}
.wi-steam__status-dot--playing {
background: #3b82f6;
}
.wi-steam__status-dot--offline {
background: #6b7280;
}
.wi-steam__info {
flex: 1;
min-width: 0;
}
.wi-steam__name {
font-size: var(--text-xl);
font-weight: 700;
color: var(--ink);
}
.wi-steam__status-text {
font-size: var(--text-sm);
color: var(--ink-3);
display: block;
margin-top: 2px;
}
.wi-steam__profile-link {
font-size: var(--text-xs);
color: var(--accent);
text-decoration: none;
display: inline-block;
margin-top: 4px;
}
.wi-steam__profile-link:hover {
text-decoration: underline;
}
.wi-steam__level {
display: flex;
flex-direction: column;
align-items: center;
padding: var(--space-sm) var(--space-md);
background: var(--accent-bg);
border-radius: 12px;
flex-shrink: 0;
}
.wi-steam__level-num {
font-size: var(--text-2xl);
font-weight: 800;
color: var(--accent);
line-height: 1;
}
.wi-steam__level-label {
font-size: 11px;
color: var(--ink-3);
margin-top: 2px;
}
.wi-steam__stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-md);
margin-bottom: var(--space-2xl);
}
.wi-steam__stat-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-lg);
background: var(--bg-raised);
border: 1px solid var(--rule);
border-radius: 12px;
min-height: 80px;
}
.wi-steam__stat-value {
font-size: var(--text-2xl);
font-weight: 800;
color: var(--accent);
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
container-type: inline-size;
}
@container (max-width: 120px) {
.wi-steam__stat-value {
font-size: var(--text-lg);
}
}
@container (max-width: 80px) {
.wi-steam__stat-value {
font-size: var(--text-md);
}
}
.wi-steam__stat-label {
font-size: var(--text-xs);
color: var(--ink-3);
margin-top: 4px;
}
.wi-steam__section {
margin-bottom: var(--space-2xl);
}
.wi-steam__section-title {
font-size: var(--text-xl);
font-weight: 700;
color: var(--ink);
margin-bottom: var(--space-lg);
padding-bottom: var(--space-sm);
border-bottom: 2px solid var(--accent);
display: inline-block;
}
.wi-steam__recent-scroll {
display: flex;
gap: var(--space-md);
overflow-x: auto;
padding-bottom: var(--space-sm);
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
}
.wi-steam__recent-scroll::-webkit-scrollbar {
height: 6px;
}
.wi-steam__recent-scroll::-webkit-scrollbar-track {
background: var(--bg);
border-radius: 3px;
}
.wi-steam__recent-scroll::-webkit-scrollbar-thumb {
background: var(--rule);
border-radius: 3px;
}
.wi-steam__recent-card {
position: relative;
display: flex;
flex-direction: column;
min-width: 220px;
max-width: 260px;
background: var(--bg-raised);
border: 1px solid var(--rule);
border-radius: 12px;
overflow: hidden;
scroll-snap-align: start;
flex-shrink: 0;
transition:
transform var(--duration-normal) var(--ease-out-expo),
box-shadow var(--duration-normal) var(--ease-out-expo),
border-color var(--duration-normal) var(--ease-out-expo);
}
.wi-steam__recent-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
border-color: var(--accent);
}
.wi-steam__recent-cover {
width: 100%;
overflow: hidden;
}
.wi-steam__recent-img {
width: 100%;
aspect-ratio: 460 / 215;
object-fit: cover;
}
.wi-steam__recent-body {
display: flex;
flex-direction: column;
gap: 4px;
padding: var(--space-sm) var(--space-md) var(--space-md);
min-width: 0;
}
.wi-steam__recent-name {
font-size: var(--text-sm);
font-weight: 600;
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wi-steam__recent-meta {
display: flex;
gap: 8px;
font-size: var(--text-xs);
color: var(--ink-3);
}
.wi-steam__recent-time {
color: var(--accent);
font-weight: 500;
}
.wi-steam__recent-achievements {
color: #f59e0b;
}
.wi-steam__games-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: var(--space-md);
}
.wi-steam__game-card {
position: relative;
background: var(--bg-raised);
border: 1px solid var(--rule);
border-radius: 12px;
overflow: hidden;
transition:
transform var(--duration-normal) var(--ease-out-expo),
box-shadow var(--duration-normal) var(--ease-out-expo),
border-color var(--duration-normal) var(--ease-out-expo);
}
.wi-steam__game-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--accent);
}
.wi-steam__card-link {
position: absolute;
inset: 0;
z-index: 1;
}
.wi-steam__game-cover {
width: 100%;
overflow: hidden;
}
.wi-steam__game-img {
width: 100%;
aspect-ratio: 460 / 215;
object-fit: cover;
transition: transform var(--duration-normal) var(--ease-out-expo);
}
.wi-steam__game-card:hover .wi-steam__game-img {
transform: scale(1.05);
}
.wi-steam__game-body {
padding: var(--space-sm) var(--space-md) var(--space-md);
display: flex;
flex-direction: column;
gap: 4px;
}
.wi-steam__game-name {
font-size: var(--text-sm);
font-weight: 600;
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wi-steam__game-meta {
display: flex;
justify-content: space-between;
font-size: var(--text-xs);
color: var(--ink-3);
}
.wi-steam__game-time {
color: var(--accent);
font-weight: 500;
}
.wi-steam__pagination {
display: flex;
justify-content: center;
align-items: center;
gap: var(--space-lg);
margin-top: var(--space-3xl);
}
.wi-steam__page-btn {
display: inline-block;
padding: 10px 24px;
border-radius: 12px;
background: var(--bg-raised);
border: 1px solid var(--rule);
color: var(--ink);
font-size: var(--text-sm);
text-decoration: none;
transition: border-color var(--duration-fast) var(--ease-out-quart),
color var(--duration-fast) var(--ease-out-quart);
}
.wi-steam__page-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
.wi-steam__page-spacer {
flex: 1;
}
@media (max-width: 1024px) {
.wi-steam__stats {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.wi-steam__profile {
flex-direction: column;
text-align: center;
padding: var(--space-lg);
}
.wi-steam__avatar-img {
width: 64px;
height: 64px;
}
.wi-steam__stats {
grid-template-columns: repeat(2, 1fr);
gap: var(--space-sm);
}
.wi-steam__stat-card {
padding: var(--space-md);
}
.wi-steam__stat-value {
font-size: var(--text-xl);
}
.wi-steam__games-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: var(--space-sm);
}
.wi-steam__game-body {
padding: var(--space-xs) var(--space-sm) var(--space-sm);
}
.wi-steam__game-name {
font-size: var(--text-xs);
}
.wi-steam__game-meta {
font-size: 10px;
}
.wi-steam__recent-card {
min-width: 180px;
}
}
@media (max-width: 480px) {
.wi-steam__games-grid {
grid-template-columns: repeat(2, 1fr);
gap: var(--space-xs);
}
.wi-steam__stat-card {
padding: var(--space-sm);
}
.wi-steam__stat-value {
font-size: var(--text-lg);
}
.wi-steam__stat-label {
font-size: 10px;
}
}
</style>
+2
View File
@@ -188,6 +188,7 @@ table {
position: relative;
z-index: 1;
background-color: var(--bg);
width: 100%;
}
.wi-main--wide.wi-main--home {
@@ -212,5 +213,6 @@ table {
.wi-main--wide {
max-width: 1400px;
width: 100%;
margin: 0 auto;
}
-4
View File
@@ -19,10 +19,6 @@ spec:
description: 留言板
screenshot:
file: page_messageboard.html
- name: Equipment
description: 装备
screenshot:
file: equipment.html
version: "1.0.0"
requires: ">=2.24.0"
license:
BIN
View File
Binary file not shown.