fix: 修复升级后整站无样式与点击特效失效
- 旧哈希静态资源随版本升级被删除,被缓存的 HTML 仍请求旧文件名导致 FileNotFoundException 500,补 compat/assets 历史资源并在构建时合并 - postbuild 增加静态资源引用校验,缺失即构建失败 - ClickEffect 改为常驻配置节点 + data 属性驱动,避免配置未落库时静默失效; 强制回流修正 transition 不触发;特效层挂到 body 下避免被导航栏遮挡 - 新增 scripts: postbuild/snapshot-assets/package/smoke-clickfx - 版本 1.2.1
This commit is contained in:
@@ -97,6 +97,11 @@
|
|||||||
2. 进入 Halo 后台 → 主题管理 → 安装主题 → 上传 ZIP 包
|
2. 进入 Halo 后台 → 主题管理 → 安装主题 → 上传 ZIP 包
|
||||||
3. 安装完成后点击「启用」
|
3. 安装完成后点击「启用」
|
||||||
|
|
||||||
|
> **升级后如果整站没了样式**:Halo 页面缓存或前置反向代理里可能还留着上一版本的 HTML,
|
||||||
|
> 它引用的 CSS 文件名带内容哈希,升级后已被删除,请求会直接 500。
|
||||||
|
> 先在 Halo 后台执行一次「清理缓存」(并刷新 CDN / Nginx 缓存)即可。
|
||||||
|
> 主题从 v1.2.1 起会把历史版本的哈希资源一并发进安装包,旧 HTML 也不会请求失败。
|
||||||
|
|
||||||
或从源码构建:
|
或从源码构建:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -117,10 +122,21 @@ pnpm install
|
|||||||
# 开发模式(监听文件变化,自动构建到 templates/)
|
# 开发模式(监听文件变化,自动构建到 templates/)
|
||||||
pnpm dev
|
pnpm dev
|
||||||
|
|
||||||
# 构建
|
# 构建(输出到 templates/,并自动合并历史版本的静态资源)
|
||||||
pnpm build
|
pnpm build
|
||||||
|
|
||||||
|
# 把本次构建的哈希资源快照进 compat/assets(发版前必做)
|
||||||
|
pnpm snapshot
|
||||||
|
|
||||||
|
# 打包 dist/warm-island-<version>.zip
|
||||||
|
pnpm package
|
||||||
|
|
||||||
|
# 点击特效脚本冒烟测试
|
||||||
|
pnpm smoke
|
||||||
```
|
```
|
||||||
|
|
||||||
|
发版流程:`pnpm build` → `pnpm snapshot` → `pnpm package`,并把 `compat/assets` 的新增文件一起提交。
|
||||||
|
|
||||||
### 目录结构
|
### 目录结构
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -129,16 +145,24 @@ pnpm build
|
|||||||
│ ├── layouts/ # Layout.astro(含页面布局契约渲染模式)
|
│ ├── layouts/ # Layout.astro(含页面布局契约渲染模式)
|
||||||
│ ├── pages/ # 页面模板(构建后输出到 templates/)
|
│ ├── pages/ # 页面模板(构建后输出到 templates/)
|
||||||
│ └── styles/ # SCSS 样式(正文样式在 _content.scss,全局生效)
|
│ └── styles/ # SCSS 样式(正文样式在 _content.scss,全局生效)
|
||||||
|
├── compat/
|
||||||
|
│ └── assets/ # 历史版本哈希静态资源,构建时合并进 templates/assets
|
||||||
|
├── scripts/ # 构建后置、资源快照、打包、冒烟测试
|
||||||
├── templates/ # 构建产物,Halo 实际加载的模板
|
├── templates/ # 构建产物,Halo 实际加载的模板
|
||||||
├── settings.yaml # 后台设置项定义(16 分组 / 117 项)
|
├── settings.yaml # 后台设置项定义
|
||||||
└── theme.yaml # 主题清单与自定义页面模板注册
|
└── theme.yaml # 主题清单与自定义页面模板注册
|
||||||
```
|
```
|
||||||
|
|
||||||
### 主题开发注意事项
|
### 主题开发注意事项
|
||||||
|
|
||||||
|
- **静态资源文件名带内容哈希**:升级后旧文件被删除,被缓存的 HTML 仍会请求旧文件名并触发 500,
|
||||||
|
所以每个版本的哈希文件都要留一份(见 `compat/assets` 与 `pnpm snapshot`)
|
||||||
- **Thymeleaf 字符串字面量必须加单引号**:`th:text="'文本'"`,裸中文遇到逗号/空格/emoji 会导致页面 500
|
- **Thymeleaf 字符串字面量必须加单引号**:`th:text="'文本'"`,裸中文遇到逗号/空格/emoji 会导致页面 500
|
||||||
|
- **`th:attr` 只用简单表达式**(取属性、Elvis 默认值、三元、拼接、等值比较),表达式对象方法调用会被受限求值器拒绝
|
||||||
|
- **凡是与后台开关绑定的模板表达式都要给默认值**:`?: '默认值'`,配置未落库时属性会被整体移除,功能会静默失效
|
||||||
- **正文样式必须写在全局样式表**(`src/styles/_content.scss`):Astro scoped 样式对 Thymeleaf 运行时注入的内容无效
|
- **正文样式必须写在全局样式表**(`src/styles/_content.scss`):Astro scoped 样式对 Thymeleaf 运行时注入的内容无效
|
||||||
- **内联脚本统一加 `th:inline="none"`**:避免 Thymeleaf 解析脚本中的 `[[` / `[(` / `${}`
|
- **内联脚本统一加 `th:inline="none"`**:避免 Thymeleaf 解析脚本中的 `[[` / `[(` / `${}`
|
||||||
|
- **新插入的动画元素要强制回流**(读一次 `offsetWidth`):否则初态与终态在同一帧被合并,transition 不触发
|
||||||
- 修改主题外观无需单独维护 `layout.astro`:页面布局契约复用 `Layout.astro`,构建时自动生成
|
- 修改主题外观无需单独维护 `layout.astro`:页面布局契约复用 `Layout.astro`,构建时自动生成
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
.wi-douban[data-astro-cid-nevwzhq2]{padding-block:1rem}.wi-douban__header[data-astro-cid-nevwzhq2]{text-align:center;margin-bottom:var(--space-3xl)}.wi-douban__title[data-astro-cid-nevwzhq2]{font-size:var(--text-4xl);margin-bottom:var(--space-sm)}.wi-page-accent[data-astro-cid-nevwzhq2]{display:block;width:48px;height:3px;margin:var(--space-sm) auto 0;border-radius:9999px;background:var(--accent)}.wi-douban__filters[data-astro-cid-nevwzhq2]{display:flex;flex-direction:column;gap:var(--space-md);margin-bottom:var(--space-2xl)}.wi-douban__filter-group[data-astro-cid-nevwzhq2]{display:flex;flex-wrap:wrap;gap:8px;justify-content:center}.wi-douban__filter-tag[data-astro-cid-nevwzhq2]{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[data-astro-cid-nevwzhq2]{padding:4px 12px;font-size:var(--text-xs)}.wi-douban__filter-tag[data-astro-cid-nevwzhq2]:hover{color:var(--accent);border-color:var(--accent)}.wi-douban__filter-tag--active[data-astro-cid-nevwzhq2]{color:#fff;background:var(--accent);border-color:var(--accent)}.wi-douban__grid[data-astro-cid-nevwzhq2]{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:var(--space-lg)}.wi-douban__card[data-astro-cid-nevwzhq2]{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[data-astro-cid-nevwzhq2]:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg);border-color:var(--accent)}.wi-douban__card-link[data-astro-cid-nevwzhq2]{position:absolute;inset:0;z-index:1}.wi-douban__poster[data-astro-cid-nevwzhq2]{flex-shrink:0;width:80px;height:112px;border-radius:8px;overflow:hidden;background:var(--bg)}.wi-douban__card[data-astro-cid-nevwzhq2][data-type=music] .wi-douban__poster[data-astro-cid-nevwzhq2]{width:80px;height:80px;border-radius:8px}.wi-douban__poster-img[data-astro-cid-nevwzhq2]{width:100%;height:100%;object-fit:cover}.wi-douban__poster--placeholder[data-astro-cid-nevwzhq2]{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[data-astro-cid-nevwzhq2]{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1}.wi-douban__name[data-astro-cid-nevwzhq2]{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[data-astro-cid-nevwzhq2]{font-size:var(--text-xs);color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wi-douban__meta[data-astro-cid-nevwzhq2]{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:var(--text-xs);color:var(--ink-3)}.wi-douban__score[data-astro-cid-nevwzhq2]{display:inline-flex;align-items:center;gap:2px;color:#f59e0b;font-weight:600}.wi-douban__star[data-astro-cid-nevwzhq2]{color:#f59e0b}.wi-douban__my-score[data-astro-cid-nevwzhq2]{color:var(--accent);font-weight:500}.wi-douban__tags[data-astro-cid-nevwzhq2]{display:flex;flex-wrap:wrap;gap:4px}.wi-douban__tag[data-astro-cid-nevwzhq2]{font-size:11px;color:var(--ink-3);padding:1px 6px;border-radius:4px;background:var(--bg);border:1px solid var(--rule)}.wi-douban__remark[data-astro-cid-nevwzhq2]{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}@media(max-width:640px){.wi-douban__grid[data-astro-cid-nevwzhq2]{grid-template-columns:1fr}.wi-douban__card[data-astro-cid-nevwzhq2]{padding:var(--space-md)}.wi-douban__poster[data-astro-cid-nevwzhq2]{width:64px;height:90px}.wi-douban__card[data-astro-cid-nevwzhq2][data-type=music] .wi-douban__poster[data-astro-cid-nevwzhq2]{width:64px;height:64px}}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<metadata>
|
||||||
|
<json>
|
||||||
|
<![CDATA[
|
||||||
|
{
|
||||||
|
"fontFamily": "lg",
|
||||||
|
"majorVersion": 2,
|
||||||
|
"minorVersion": 0,
|
||||||
|
"fontURL": "",
|
||||||
|
"copyright": "",
|
||||||
|
"license": "",
|
||||||
|
"licenseURL": "",
|
||||||
|
"description": "Font generated by IcoMoon.",
|
||||||
|
"version": "Version 2.0",
|
||||||
|
"fontId": "lg",
|
||||||
|
"psName": "lg",
|
||||||
|
"subFamily": "Regular",
|
||||||
|
"fullName": "lg"
|
||||||
|
}
|
||||||
|
]]>
|
||||||
|
</json>
|
||||||
|
</metadata>
|
||||||
|
<defs>
|
||||||
|
<font id="lg" horiz-adv-x="1024">
|
||||||
|
<font-face units-per-em="1024" ascent="960" descent="-64" />
|
||||||
|
<missing-glyph horiz-adv-x="1024" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||||
|
<glyph unicode="" glyph-name="pause_circle_outline" data-tags="pause_circle_outline" d="M554 256.667v340h86v-340h-86zM512 84.667q140 0 241 101t101 241-101 241-241 101-241-101-101-241 101-241 241-101zM512 852.667q176 0 301-125t125-301-125-301-301-125-301 125-125 301 125 301 301 125zM384 256.667v340h86v-340h-86z" />
|
||||||
|
<glyph unicode="" glyph-name="play_circle_outline" data-tags="play_circle_outline" d="M512 84.667q140 0 241 101t101 241-101 241-241 101-241-101-101-241 101-241 241-101zM512 852.667q176 0 301-125t125-301-125-301-301-125-301 125-125 301 125 301 301 125zM426 234.667v384l256-192z" />
|
||||||
|
<glyph unicode="" glyph-name="stack-2" data-tags="stack-2" d="M384 853.334h426.667q53 0 90.5-37.5t37.5-90.5v-426.667q0-53-37.5-90.5t-90.5-37.5h-426.667q-53 0-90.5 37.5t-37.5 90.5v426.667q0 53 37.5 90.5t90.5 37.5zM170.667 675.334v-547.333q0-17.667 12.5-30.167t30.167-12.5h547.333q-13.333-37.667-46.333-61.5t-74.333-23.833h-426.667q-53 0-90.5 37.5t-37.5 90.5v426.667q0 41.333 23.833 74.333t61.5 46.333zM810.667 768h-426.667q-17.667 0-30.167-12.5t-12.5-30.167v-426.667q0-17.667 12.5-30.167t30.167-12.5h426.667q17.667 0 30.167 12.5t12.5 30.167v426.667q0 17.667-12.5 30.167t-30.167 12.5z" />
|
||||||
|
<glyph unicode="" glyph-name="clear" data-tags="clear" d="M810 664.667l-238-238 238-238-60-60-238 238-238-238-60 60 238 238-238 238 60 60 238-238 238 238z" />
|
||||||
|
<glyph unicode="" glyph-name="arrow-left" data-tags="arrow-left" d="M426.667 768q17.667 0 30.167-12.5t12.5-30.167q0-18-12.667-30.333l-225.667-225.667h665q17.667 0 30.167-12.5t12.5-30.167-12.5-30.167-30.167-12.5h-665l225.667-225.667q12.667-12.333 12.667-30.333 0-17.667-12.5-30.167t-30.167-12.5q-18 0-30.333 12.333l-298.667 298.667q-12.333 13-12.333 30.333t12.333 30.333l298.667 298.667q12.667 12.333 30.333 12.333z" />
|
||||||
|
<glyph unicode="" glyph-name="arrow-right" data-tags="arrow-right" d="M597.333 768q18 0 30.333-12.333l298.667-298.667q12.333-12.333 12.333-30.333t-12.333-30.333l-298.667-298.667q-12.333-12.333-30.333-12.333-18.333 0-30.5 12.167t-12.167 30.5q0 18 12.333 30.333l226 225.667h-665q-17.667 0-30.167 12.5t-12.5 30.167 12.5 30.167 30.167 12.5h665l-226 225.667q-12.333 12.333-12.333 30.333 0 18.333 12.167 30.5t30.5 12.167z" />
|
||||||
|
<glyph unicode="" glyph-name="vertical_align_bottom" data-tags="vertical_align_bottom" d="M170 128.667h684v-86h-684v86zM682 384.667l-170-172-170 172h128v426h84v-426h128z" />
|
||||||
|
<glyph unicode="" glyph-name="apps" data-tags="apps" d="M682 84.667v172h172v-172h-172zM682 340.667v172h172v-172h-172zM426 596.667v172h172v-172h-172zM682 768.667h172v-172h-172v172zM426 340.667v172h172v-172h-172zM170 340.667v172h172v-172h-172zM170 84.667v172h172v-172h-172zM426 84.667v172h172v-172h-172zM170 596.667v172h172v-172h-172z" />
|
||||||
|
<glyph unicode="" glyph-name="fullscreen" data-tags="fullscreen" d="M598 724.667h212v-212h-84v128h-128v84zM726 212.667v128h84v-212h-212v84h128zM214 512.667v212h212v-84h-128v-128h-84zM298 340.667v-128h128v-84h-212v212h84z" />
|
||||||
|
<glyph unicode="" glyph-name="fullscreen_exit" data-tags="fullscreen_exit" d="M682 596.667h128v-84h-212v212h84v-128zM598 128.667v212h212v-84h-128v-128h-84zM342 596.667v128h84v-212h-212v84h128zM214 256.667v84h212v-212h-84v128h-128z" />
|
||||||
|
<glyph unicode="" glyph-name="zoom_in" data-tags="zoom_in" d="M512 512.667h-86v-86h-42v86h-86v42h86v86h42v-86h86v-42zM406 340.667q80 0 136 56t56 136-56 136-136 56-136-56-56-136 56-136 136-56zM662 340.667l212-212-64-64-212 212v34l-12 12q-76-66-180-66-116 0-197 80t-81 196 81 197 197 81 196-81 80-197q0-104-66-180l12-12h34z" />
|
||||||
|
<glyph unicode="" glyph-name="zoom_out" data-tags="zoom_out" d="M298 554.667h214v-42h-214v42zM406 340.667q80 0 136 56t56 136-56 136-136 56-136-56-56-136 56-136 136-56zM662 340.667l212-212-64-64-212 212v34l-12 12q-76-66-180-66-116 0-197 80t-81 196 81 197 197 81 196-81 80-197q0-104-66-180l12-12h34z" />
|
||||||
|
<glyph unicode="" glyph-name="share" data-tags="share" d="M768 252.667c68 0 124-56 124-124s-56-126-124-126-124 58-124 126c0 10 0 20 2 28l-302 176c-24-22-54-34-88-34-70 0-128 58-128 128s58 128 128 128c34 0 64-12 88-34l300 174c-2 10-4 20-4 30 0 70 58 128 128 128s128-58 128-128-58-128-128-128c-34 0-64 14-88 36l-300-176c2-10 4-20 4-30s-2-20-4-30l304-176c22 20 52 32 84 32z" />
|
||||||
|
<glyph unicode="" glyph-name="rotate_left" data-tags="rotate_left" d="M554 764.667q126-16 213-112t87-226-87-226-213-112v86q92 16 153 87t61 165-61 165-153 87v-166l-194 190 194 194v-132zM302 156.667l62 62q46-34 106-44v-86q-96 12-168 68zM260 384.667q10-58 42-106l-60-60q-56 74-68 166h86zM304 574.667q-36-52-44-106h-86q12 90 70 166z" />
|
||||||
|
<glyph unicode="" glyph-name="rotate_right" data-tags="rotate_right" d="M720 278.667q34 46 44 106h86q-12-92-68-166zM554 174.667q60 10 106 44l62-62q-72-56-168-68v86zM850 468.667h-86q-10 60-44 106l62 60q58-72 68-166zM664 702.667l-194-190v166q-92-16-153-87t-61-165 61-165 153-87v-86q-126 16-213 112t-87 226 87 226 213 112v132z" />
|
||||||
|
<glyph unicode="" glyph-name="swap_horiz" data-tags="swap_horiz" d="M896 554.667l-170-170v128h-300v84h300v128zM298 468.667v-128h300v-84h-300v-128l-170 170z" />
|
||||||
|
<glyph unicode="" glyph-name="swap_vert" data-tags="swap_vert" d="M384 810.667l170-170h-128v-300h-84v300h-128zM682 212.667h128l-170-170-170 170h128v300h84v-300z" />
|
||||||
|
<glyph unicode="" glyph-name="facebook-with-circle" data-tags="facebook-with-circle" d="M512 952.32c-271.462 0-491.52-220.058-491.52-491.52s220.058-491.52 491.52-491.52 491.52 220.058 491.52 491.52-220.058 491.52-491.52 491.52zM628.429 612.659h-73.882c-8.755 0-18.483-11.52-18.483-26.829v-53.35h92.416l-13.978-76.083h-78.438v-228.403h-87.194v228.403h-79.104v76.083h79.104v44.749c0 64.205 44.544 116.378 105.677 116.378h73.882v-80.947z" />
|
||||||
|
<glyph unicode="" glyph-name="google-with-circle" data-tags="google+-with-circle" d="M512 952.32c-271.462 0-491.52-220.058-491.52-491.52s220.058-491.52 491.52-491.52 491.52 220.058 491.52 491.52-220.058 491.52-491.52 491.52zM483.686 249.805c-30.874-15.002-64.102-16.589-76.954-16.589-2.458 0-3.84 0-3.84 0s-1.178 0-2.765 0c-20.070 0-119.962 4.608-119.962 95.59 0 89.395 108.8 96.41 142.131 96.41h0.87c-19.251 25.702-15.258 51.61-15.258 51.61-1.69-0.102-4.147-0.205-7.168-0.205-12.544 0-36.762 1.997-57.549 15.411-25.498 16.384-38.4 44.288-38.4 82.893 0 109.107 119.142 113.51 120.32 113.613h118.989v-2.611c0-13.312-23.91-15.923-40.192-18.125-5.53-0.819-16.64-1.894-19.763-3.482 30.157-16.128 35.021-41.421 35.021-79.104 0-42.906-16.794-65.587-34.611-81.51-11.059-9.882-19.712-17.613-19.712-28.006 0-10.189 11.878-20.582 25.702-32.717 22.579-19.917 53.555-47.002 53.555-92.723 0-47.258-20.326-81.050-60.416-100.454zM742.4 460.8h-76.8v-76.8h-51.2v76.8h-76.8v51.2h76.8v76.8h51.2v-76.8h76.8v-51.2zM421.018 401.92c-2.662 0-5.325-0.102-8.038-0.307-22.733-1.69-43.725-10.189-58.88-24.013-15.053-13.619-22.733-30.822-21.658-48.179 2.304-36.403 41.37-57.702 88.832-54.323 46.694 3.379 77.824 30.31 75.571 66.714-2.15 34.202-31.898 60.109-75.827 60.109zM465.766 599.808c-12.39 43.52-32.358 56.422-63.386 56.422-3.328 0-6.707-0.512-9.933-1.382-13.466-3.84-24.166-15.053-30.106-31.744-6.093-16.896-6.451-34.509-1.229-54.579 9.472-35.891 34.97-61.901 60.672-61.901 3.379 0 6.758 0.41 9.933 1.382 28.109 7.885 45.722 50.79 34.048 91.802z" />
|
||||||
|
<glyph unicode="" glyph-name="pinterest-with-circle" data-tags="pinterest-with-circle" d="M512 952.32c-271.462 0-491.52-220.058-491.52-491.52s220.058-491.52 491.52-491.52 491.52 220.058 491.52 491.52-220.058 491.52-491.52 491.52zM545.638 344.32c-31.539 2.406-44.749 18.022-69.427 32.973-13.568-71.219-30.157-139.52-79.309-175.206-15.206 107.725 22.221 188.518 39.629 274.381-29.645 49.92 3.533 150.323 66.099 125.645 76.954-30.515-66.662-185.6 29.747-205.005 100.659-20.173 141.773 174.694 79.36 237.978-90.214 91.494-262.502 2.099-241.306-128.87 5.12-32 38.246-41.728 13.21-85.914-57.702 12.8-74.957 58.317-72.704 118.989 3.533 99.328 89.242 168.909 175.155 178.483 108.698 12.083 210.688-39.885 224.819-142.182 15.821-115.405-49.101-240.282-165.274-231.27z" />
|
||||||
|
<glyph unicode="" glyph-name="twitter-with-circle" data-tags="twitter-with-circle" d="M512 952.32c-271.462 0-491.52-220.058-491.52-491.52s220.058-491.52 491.52-491.52 491.52 220.058 491.52 491.52-220.058 491.52-491.52 491.52zM711.936 549.683c0.205-4.198 0.256-8.397 0.256-12.493 0-128-97.331-275.507-275.405-275.507-54.682 0-105.574 15.974-148.378 43.52 7.526-0.922 15.258-1.28 23.091-1.28 45.363 0 87.091 15.411 120.218 41.421-42.342 0.819-78.080 28.774-90.419 67.174 5.888-1.075 11.93-1.69 18.176-1.69 8.806 0 17.408 1.178 25.498 3.379-44.288 8.909-77.67 48.026-77.67 94.925v1.178c13.056-7.219 28.006-11.622 43.878-12.134-26.010 17.408-43.059 47.002-43.059 80.64 0 17.715 4.762 34.406 13.107 48.691 47.77-58.573 119.040-97.075 199.526-101.222-1.69 7.117-2.509 14.49-2.509 22.118 0 53.402 43.315 96.819 96.819 96.819 27.802 0 52.992-11.776 70.656-30.618 22.067 4.403 42.752 12.39 61.44 23.501-7.219-22.579-22.528-41.574-42.547-53.606 19.61 2.406 38.246 7.578 55.603 15.309-12.954-19.405-29.389-36.506-48.282-50.125z" />
|
||||||
|
<glyph unicode="" glyph-name="message-circle" data-tags="message-circle" d="M938.667 448.128v21.205c0 0.725-0.043 1.621-0.085 2.475-5.803 99.755-47.488 190.336-112.725 258.176-68.352 71.125-162.731 117.419-268.843 123.264-0.683 0.043-1.536 0.085-2.347 0.085h-20.864c-59.947 0.683-122.965-13.227-181.931-43.008-52.181-26.496-97.749-63.488-133.931-108.16-56.405-69.717-89.899-158.080-89.941-253.696-0.597-54.4 10.795-111.36 35.157-165.419l-75.605-226.859c-2.816-8.363-3.072-17.835 0-26.965 7.467-22.357 31.616-34.432 53.973-26.965l226.731 75.563c49.493-22.485 105.984-35.243 165.376-35.115 58.539 0.384 115.797 13.141 168.149 36.949 81.579 37.163 151.040 101.248 193.749 186.667 27.477 53.291 43.307 115.84 43.136 181.803zM853.333 447.872c0.128-52.267-12.459-101.333-33.664-142.464-34.176-68.352-88.832-118.827-153.259-148.139-41.387-18.859-86.827-28.971-133.376-29.269-52.096-0.128-101.163 12.459-142.293 33.664-10.624 5.504-22.528 6.059-33.067 2.56l-162.261-54.101 54.101 162.261c3.755 11.221 2.56 22.912-2.389 32.725-23.552 46.677-34.304 96.171-33.792 142.421 0.043 76.331 26.411 145.92 70.955 200.917 28.629 35.371 64.768 64.725 106.24 85.76 46.592 23.552 96.085 34.304 142.336 33.792h19.456c83.712-4.565 158.037-41.003 212.011-97.152 51.285-53.376 84.139-124.416 89.003-202.795z" />
|
||||||
|
<glyph unicode="" glyph-name="maximize-2" data-tags="maximize-2" d="M793.003 768l-225.835-225.835c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0l225.835 225.835v-153.003c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v256c0 5.803-1.152 11.307-3.243 16.341s-5.163 9.728-9.216 13.781c-0.043 0.043-0.043 0.043-0.085 0.085-3.925 3.925-8.619 7.083-13.781 9.216-5.035 2.091-10.539 3.243-16.341 3.243h-256c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667zM230.997 85.334l225.835 225.835c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-225.835-225.835v153.003c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667v-256c0-23.552 19.115-42.667 42.667-42.667h256c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667z" />
|
||||||
|
<glyph unicode="" glyph-name="minimize-2" data-tags="minimize-2" d="M700.331 554.667l225.835 225.835c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-225.835-225.835v153.003c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667v-256c0-5.803 1.152-11.307 3.243-16.341s5.163-9.728 9.216-13.781c0.043-0.043 0.043-0.043 0.085-0.085 3.925-3.925 8.619-7.083 13.781-9.216 5.035-2.091 10.539-3.243 16.341-3.243h256c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667zM158.165 12.502l225.835 225.835v-153.003c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v256c0 5.803-1.152 11.307-3.243 16.341s-5.163 9.728-9.216 13.781c-0.043 0.043-0.043 0.043-0.085 0.085-4.096 4.053-8.789 7.125-13.781 9.216-5.035 2.091-10.539 3.243-16.341 3.243h-256c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667h153.003l-225.835-225.835c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0z" />
|
||||||
|
</font></defs></svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,23 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
## v1.2.1
|
||||||
|
|
||||||
|
### 问题修复
|
||||||
|
|
||||||
|
- 🐛 **修复升级后整站没有样式**:Astro 会给 CSS/JS 加内容哈希,升级后旧文件被删除,被缓存的 HTML 仍请求旧文件名,
|
||||||
|
Halo 直接抛 `FileNotFoundException` 返回 500,样式全部丢失。主题现在会把各版本的哈希资源一并保留
|
||||||
|
(`compat/assets` 在构建时合并进 `templates/assets`,已覆盖 v1.1.7 起的所有版本)
|
||||||
|
- 🐛 **避免页面出现 500**:新增构建期静态资源引用校验,模板引用的任一缺失资源都会让构建失败
|
||||||
|
- 🐛 **修复鼠标点击效果开启后没有任何作用**:旧实现用 `th:if` 判断开关、且直接读取配置值,
|
||||||
|
一旦配置项未随版本写入(或被 Thymeleaf 整体移除属性)就读取为空,脚本直接跳过;
|
||||||
|
同时修复了多种特效在插入节点后随即改写 `transform` / `opacity`,导致浏览器把初态与终态合并、
|
||||||
|
transition 完全不触发的问题(表现为点了没反应)
|
||||||
|
- 🐛 次要修复:点击特效节点统一挂载到 `body` 下的独立图层(`z-index: 12050`),不再被导航栏、页脚遮挡
|
||||||
|
|
||||||
|
### 其他
|
||||||
|
|
||||||
|
- 🧰 新增构建脚本:`pnpm snapshot`(历史资源快照)、`pnpm package`(打安装包)、`pnpm smoke`(点击特效冒烟测试)
|
||||||
|
|
||||||
## v1.2.0
|
## v1.2.0
|
||||||
|
|
||||||
### 主题配色系统
|
### 主题配色系统
|
||||||
|
|||||||
+25
-1
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
检查以下几点:
|
检查以下几点:
|
||||||
|
|
||||||
1. 确认 Halo 版本 >= 2.24.0
|
1. 确认 Halo 版本 >= 2.26.0
|
||||||
2. 确认主题已启用
|
2. 确认主题已启用
|
||||||
3. 清除浏览器缓存后重试(Ctrl+Shift+R 强制刷新)
|
3. 清除浏览器缓存后重试(Ctrl+Shift+R 强制刷新)
|
||||||
4. 检查 Halo 后台日志是否有报错
|
4. 检查 Halo 后台日志是否有报错
|
||||||
@@ -26,8 +26,32 @@ docker cp templates/ halo:/root/.halo2/themes/warm-island/
|
|||||||
docker restart halo
|
docker restart halo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 升级主题后整站都没有样式?
|
||||||
|
|
||||||
|
主题的 CSS / JS 文件名带内容哈希,每次构建都会变化。如果 Halo 页面缓存或前置反向代理
|
||||||
|
(OpenResty、CDN 等)里还留着上一版本的 HTML,它请求的仍是已被删除的旧文件名,
|
||||||
|
Halo 找不到文件时会返回 **500**(日志里能看到 `FileNotFoundException: .../templates/assets/Layout.xxxx.css`),
|
||||||
|
于是样式全部失效。
|
||||||
|
|
||||||
|
依次处理:
|
||||||
|
|
||||||
|
1. Halo 后台执行一次「清理缓存」(设置 → 缓存,或直接重启 Halo / 容器)
|
||||||
|
2. 刷新 CDN、反向代理缓存
|
||||||
|
3. 浏览器强制刷新(Ctrl+Shift+R)
|
||||||
|
|
||||||
|
v1.2.1 起安装包会保留各版本的哈希资源(v1.1.7 之后全部覆盖),
|
||||||
|
即使缓存里的旧 HTML 被再次请求也不会 500,但最终仍建议清一次缓存以使用最新样式。
|
||||||
|
|
||||||
## 功能相关
|
## 功能相关
|
||||||
|
|
||||||
|
### 鼠标点击效果 / 拖尾效果开启了却没反应?
|
||||||
|
|
||||||
|
1. 确认「总体样式 → 鼠标点击效果」开关已开启并点击了保存
|
||||||
|
2. 确认「点击效果类型」没有选择「关闭」(选了关闭时即使开启开关也不会有任何特效)
|
||||||
|
3. 清除浏览器缓存后重试;老版本的浏览器请更新到较新的 Chrome / Edge / Firefox / Safari
|
||||||
|
|
||||||
|
若仍无效,打开浏览器控制台(F12)检查是否有 JS 报错 —— 第三方脚本报错会中断后续脚本执行。
|
||||||
|
|
||||||
### 搜索按钮不显示?
|
### 搜索按钮不显示?
|
||||||
|
|
||||||
搜索功能依赖 Halo 搜索插件:
|
搜索功能依赖 Halo 搜索插件:
|
||||||
|
|||||||
+5
-2
@@ -1,12 +1,15 @@
|
|||||||
{
|
{
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.12.0"
|
"node": ">=22.12.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon --config nodemon.json --exec 'astro build'",
|
"dev": "nodemon --config nodemon.json --exec 'astro build'",
|
||||||
"build": "astro build && node -e \"const fs=require('fs');const src='templates/logo.png';const dst='templates/assets/logo.png';if(fs.existsSync(src)){fs.copyFileSync(src,dst)}\"",
|
"build": "astro build && node scripts/postbuild.mjs",
|
||||||
|
"snapshot": "node scripts/snapshot-assets.mjs",
|
||||||
|
"package": "node scripts/package.mjs",
|
||||||
|
"smoke": "node scripts/smoke-clickfx.mjs",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* 打包主题安装包:dist/warm-island-<version>.zip
|
||||||
|
*
|
||||||
|
* 安装包结构与 Halo 主题目录一致(根目录直接放 theme.yaml / settings.yaml /
|
||||||
|
* templates),与历史版本安装包保持相同布局,可直接上传安装。
|
||||||
|
*
|
||||||
|
* 仅依赖 Node 内置模块,无需额外依赖。
|
||||||
|
*/
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import zlib from "node:zlib";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
|
||||||
|
/** 读取 theme.yaml 中的版本号(避免手抄版本号不一致) */
|
||||||
|
function readThemeVersion() {
|
||||||
|
const yaml = fs.readFileSync(path.join(root, "theme.yaml"), "utf8");
|
||||||
|
const match = yaml.match(/^\s*version:\s*["']?([^"'\n]+)["']?\s*$/m);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error("theme.yaml 中未找到 version 字段");
|
||||||
|
}
|
||||||
|
return match[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- CRC32 ---------------- */
|
||||||
|
const CRC_TABLE = (() => {
|
||||||
|
const table = new Int32Array(256);
|
||||||
|
for (let i = 0; i < 256; i++) {
|
||||||
|
let c = i;
|
||||||
|
for (let k = 0; k < 8; k++) {
|
||||||
|
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||||
|
}
|
||||||
|
table[i] = c;
|
||||||
|
}
|
||||||
|
return table;
|
||||||
|
})();
|
||||||
|
|
||||||
|
function crc32(buf) {
|
||||||
|
let c = 0xffffffff;
|
||||||
|
for (let i = 0; i < buf.length; i++) {
|
||||||
|
c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
||||||
|
}
|
||||||
|
return (c ^ 0xffffffff) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- ZIP 写入 ---------------- */
|
||||||
|
function dosDateTime(date = new Date()) {
|
||||||
|
const time =
|
||||||
|
((date.getHours() & 0x1f) << 11) |
|
||||||
|
((date.getMinutes() & 0x3f) << 5) |
|
||||||
|
((date.getSeconds() / 2) & 0x1f);
|
||||||
|
const day =
|
||||||
|
(((date.getFullYear() - 1980) & 0x7f) << 9) |
|
||||||
|
(((date.getMonth() + 1) & 0xf) << 5) |
|
||||||
|
(date.getDate() & 0x1f);
|
||||||
|
return { time, day };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildZip(entries, outFile) {
|
||||||
|
const chunks = [];
|
||||||
|
const central = [];
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const nameBuf = Buffer.from(entry.name, "utf8");
|
||||||
|
const raw = entry.data;
|
||||||
|
const crc = crc32(raw);
|
||||||
|
const compressed = zlib.deflateRawSync(raw, { level: 9 });
|
||||||
|
const useDeflate = compressed.length < raw.length;
|
||||||
|
const body = useDeflate ? compressed : raw;
|
||||||
|
const method = useDeflate ? 8 : 0;
|
||||||
|
const { time, day } = dosDateTime(entry.date);
|
||||||
|
|
||||||
|
const local = Buffer.alloc(30);
|
||||||
|
local.writeUInt32LE(0x04034b50, 0);
|
||||||
|
local.writeUInt16LE(20, 4); // version needed
|
||||||
|
local.writeUInt16LE(0x0800, 6); // UTF-8 名称
|
||||||
|
local.writeUInt16LE(method, 8);
|
||||||
|
local.writeUInt16LE(time, 10);
|
||||||
|
local.writeUInt16LE(day, 12);
|
||||||
|
local.writeUInt32LE(crc, 14);
|
||||||
|
local.writeUInt32LE(body.length, 18);
|
||||||
|
local.writeUInt32LE(raw.length, 22);
|
||||||
|
local.writeUInt16LE(nameBuf.length, 26);
|
||||||
|
local.writeUInt16LE(0, 28);
|
||||||
|
|
||||||
|
chunks.push(local, nameBuf, body);
|
||||||
|
|
||||||
|
const cd = Buffer.alloc(46);
|
||||||
|
cd.writeUInt32LE(0x02014b50, 0);
|
||||||
|
cd.writeUInt16LE(20, 4); // version made by
|
||||||
|
cd.writeUInt16LE(20, 6); // version needed
|
||||||
|
cd.writeUInt16LE(0x0800, 8);
|
||||||
|
cd.writeUInt16LE(method, 10);
|
||||||
|
cd.writeUInt16LE(time, 12);
|
||||||
|
cd.writeUInt16LE(day, 14);
|
||||||
|
cd.writeUInt32LE(crc, 16);
|
||||||
|
cd.writeUInt32LE(body.length, 20);
|
||||||
|
cd.writeUInt32LE(raw.length, 24);
|
||||||
|
cd.writeUInt16LE(nameBuf.length, 28);
|
||||||
|
cd.writeUInt16LE(0, 30); // extra
|
||||||
|
cd.writeUInt16LE(0, 32); // comment
|
||||||
|
cd.writeUInt16LE(0, 34); // disk start
|
||||||
|
cd.writeUInt16LE(0, 36); // internal attrs
|
||||||
|
cd.writeUInt32LE(0, 38); // external attrs
|
||||||
|
cd.writeUInt32LE(offset, 42);
|
||||||
|
central.push(Buffer.concat([cd, nameBuf]));
|
||||||
|
|
||||||
|
offset += local.length + nameBuf.length + body.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const centralBuf = Buffer.concat(central);
|
||||||
|
const eocd = Buffer.alloc(22);
|
||||||
|
eocd.writeUInt32LE(0x06054b50, 0);
|
||||||
|
eocd.writeUInt16LE(0, 4);
|
||||||
|
eocd.writeUInt16LE(0, 6);
|
||||||
|
eocd.writeUInt16LE(entries.length, 8);
|
||||||
|
eocd.writeUInt16LE(entries.length, 10);
|
||||||
|
eocd.writeUInt32LE(centralBuf.length, 12);
|
||||||
|
eocd.writeUInt32LE(offset, 16);
|
||||||
|
eocd.writeUInt16LE(0, 20);
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
||||||
|
fs.writeFileSync(outFile, Buffer.concat([...chunks, centralBuf, eocd]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- 收集文件 ---------------- */
|
||||||
|
const INCLUDE_ROOT = [
|
||||||
|
"theme.yaml",
|
||||||
|
"settings.yaml",
|
||||||
|
"LICENSE",
|
||||||
|
"README.md",
|
||||||
|
"pnpm-lock.yaml",
|
||||||
|
"pnpm-workspace.yaml",
|
||||||
|
];
|
||||||
|
const INCLUDE_DIRS = ["templates"];
|
||||||
|
const EXCLUDE_DIRS = new Set([".git", "node_modules", "src", "dist", "compat", ".astro"]);
|
||||||
|
|
||||||
|
const entries = [];
|
||||||
|
|
||||||
|
for (const name of INCLUDE_ROOT) {
|
||||||
|
const full = path.join(root, name);
|
||||||
|
if (fs.existsSync(full)) {
|
||||||
|
entries.push({ name, data: fs.readFileSync(full), date: fs.statSync(full).mtime });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function walk(dir, prefix) {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const abs = path.join(dir, entry.name);
|
||||||
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (EXCLUDE_DIRS.has(entry.name)) continue;
|
||||||
|
walk(abs, rel);
|
||||||
|
} else {
|
||||||
|
entries.push({ name: rel, data: fs.readFileSync(abs), date: fs.statSync(abs).mtime });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dir of INCLUDE_DIRS) {
|
||||||
|
const full = path.join(root, dir);
|
||||||
|
if (fs.existsSync(full)) walk(full, dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = readThemeVersion();
|
||||||
|
const outFile = path.join(root, "dist", `warm-island-${version}.zip`);
|
||||||
|
buildZip(entries, outFile);
|
||||||
|
|
||||||
|
const sizeMB = (fs.statSync(outFile).size / 1024 / 1024).toFixed(2);
|
||||||
|
console.log(`[package] dist/warm-island-${version}.zip(${entries.length} 个文件,${sizeMB}MB)`);
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* 构建后置处理:
|
||||||
|
* 1. 复制 logo.png 到 assets(保留历史行为)
|
||||||
|
* 2. 合并 compat/assets 下的历史版本静态资源,避免站点/代理缓存中的旧 HTML
|
||||||
|
* 仍然引用上一版本的哈希文件名时出现 500(FileNotFoundException)
|
||||||
|
* 3. 校验所有 templates/*.html 引用的静态资源真实存在,缺失即构建失败
|
||||||
|
*/
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const assetsDir = path.join(root, "templates", "assets");
|
||||||
|
const compatDir = path.join(root, "compat", "assets");
|
||||||
|
const templatesDir = path.join(root, "templates");
|
||||||
|
|
||||||
|
fs.mkdirSync(assetsDir, { recursive: true });
|
||||||
|
|
||||||
|
// 1. logo
|
||||||
|
const logoSrc = path.join(templatesDir, "logo.png");
|
||||||
|
if (fs.existsSync(logoSrc)) {
|
||||||
|
fs.copyFileSync(logoSrc, path.join(assetsDir, "logo.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 历史资源合并(已存在的同名文件以当前构建为准,不覆盖)
|
||||||
|
if (fs.existsSync(compatDir)) {
|
||||||
|
let copied = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
for (const entry of fs.readdirSync(compatDir, { withFileTypes: true })) {
|
||||||
|
if (!entry.isFile()) continue;
|
||||||
|
const src = path.join(compatDir, entry.name);
|
||||||
|
const dst = path.join(assetsDir, entry.name);
|
||||||
|
if (fs.existsSync(dst)) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fs.copyFileSync(src, dst);
|
||||||
|
copied++;
|
||||||
|
}
|
||||||
|
console.log(`[compat] 历史静态资源:复制 ${copied} 个,当前构建已覆盖 ${skipped} 个`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 资源引用校验
|
||||||
|
const missing = [];
|
||||||
|
const checked = new Set();
|
||||||
|
/** 主题静态资源前缀,必须与 astro.config.mjs 的 base 保持一致 */
|
||||||
|
const prefixes = ["/themes/warm-island/assets/"];
|
||||||
|
|
||||||
|
/** Thymeleaf 的 @{...} 链接表达式同样需要校验 */
|
||||||
|
const REFERENCE_RE =
|
||||||
|
/(?:href|src)=["']([^"']*?\/(?:themes\/[^/]+)\/assets\/[^"']+?)["']/g;
|
||||||
|
|
||||||
|
for (const file of fs.readdirSync(templatesDir)) {
|
||||||
|
if (!file.endsWith(".html")) continue;
|
||||||
|
const html = fs.readFileSync(path.join(templatesDir, file), "utf8");
|
||||||
|
REFERENCE_RE.lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
while ((match = REFERENCE_RE.exec(html)) !== null) {
|
||||||
|
let ref = match[1];
|
||||||
|
if (ref.includes("@{")) continue; // 由 Halo 运行时解析,跳过
|
||||||
|
for (const prefix of prefixes) {
|
||||||
|
if (!ref.startsWith(prefix)) continue;
|
||||||
|
const rel = ref.slice(prefix.length).split("#")[0].split("?")[0];
|
||||||
|
const key = `${file}:${rel}`;
|
||||||
|
if (checked.has(key)) continue;
|
||||||
|
checked.add(key);
|
||||||
|
if (!fs.existsSync(path.join(assetsDir, rel))) {
|
||||||
|
missing.push(`${file} -> ${ref}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
console.error(`[verify] 发现 ${missing.length} 处静态资源缺失:`);
|
||||||
|
for (const item of missing) console.error(` - ${item}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[verify] 静态资源引用校验通过(${checked.size} 条引用)`);
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* 点击特效脚本冒烟测试(Node 内置模块,无第三方依赖)。
|
||||||
|
*
|
||||||
|
* 直接从构建产物 templates/index.html 中取出特效脚本,放进桩 DOM 里执行,
|
||||||
|
* 校验:启用/禁用判定、拖尾与点击分支、节点生成与定位。
|
||||||
|
*
|
||||||
|
* 这类 bug 的共同点是"不报错但没效果",靠读代码很难发现,所以固化成测试。
|
||||||
|
* 用法:npm run build && npm run smoke
|
||||||
|
*/
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import vm from "node:vm";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const template = path.join(root, "templates", "index.html");
|
||||||
|
|
||||||
|
if (!fs.existsSync(template)) {
|
||||||
|
console.error("[smoke] 未找到 templates/index.html,请先执行 npm run build");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = fs.readFileSync(template, "utf8");
|
||||||
|
const anchor = html.indexOf('id="wi-click-effect"');
|
||||||
|
if (anchor === -1) {
|
||||||
|
console.error("[smoke] 模板中未找到 #wi-click-effect 节点");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const scriptOpen = html.indexOf("<script", anchor);
|
||||||
|
const scriptStart = html.indexOf(">", scriptOpen) + 1;
|
||||||
|
const scriptEnd = html.indexOf("</script>", scriptStart);
|
||||||
|
const code = html.slice(scriptStart, scriptEnd);
|
||||||
|
|
||||||
|
const failures = [];
|
||||||
|
|
||||||
|
function makeEl(tag) {
|
||||||
|
return {
|
||||||
|
tagName: tag,
|
||||||
|
className: "",
|
||||||
|
innerHTML: "",
|
||||||
|
textContent: "",
|
||||||
|
children: [],
|
||||||
|
parentNode: null,
|
||||||
|
style: {},
|
||||||
|
_attrs: {},
|
||||||
|
get offsetWidth() {
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
getAttribute(name) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(this._attrs, name) ? this._attrs[name] : null;
|
||||||
|
},
|
||||||
|
appendChild(child) {
|
||||||
|
if (child.parentNode) return child;
|
||||||
|
child.parentNode = this;
|
||||||
|
this.children.push(child);
|
||||||
|
return child;
|
||||||
|
},
|
||||||
|
removeChild(child) {
|
||||||
|
const i = this.children.indexOf(child);
|
||||||
|
if (i >= 0) {
|
||||||
|
this.children.splice(i, 1);
|
||||||
|
child.parentNode = null;
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(attrs, expectEnabled, label) {
|
||||||
|
const cfg = makeEl("div");
|
||||||
|
Object.assign(cfg._attrs, attrs);
|
||||||
|
const body = makeEl("body");
|
||||||
|
const listeners = {};
|
||||||
|
const sandbox = {
|
||||||
|
document: {
|
||||||
|
getElementById: (id) => (id === "wi-click-effect" ? cfg : null),
|
||||||
|
createElement: makeEl,
|
||||||
|
body,
|
||||||
|
addEventListener(type, fn) {
|
||||||
|
(listeners[type] = listeners[type] || []).push(fn);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
window: {},
|
||||||
|
requestAnimationFrame: (fn) => fn(),
|
||||||
|
setTimeout: () => 0,
|
||||||
|
};
|
||||||
|
vm.createContext(sandbox);
|
||||||
|
vm.runInContext(code, sandbox);
|
||||||
|
|
||||||
|
const hasListener = Object.keys(listeners).length > 0;
|
||||||
|
if (hasListener !== expectEnabled) {
|
||||||
|
failures.push(`${label}:期望 ${expectEnabled ? "注册监听" : "不注册监听"},实际相反`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!expectEnabled) {
|
||||||
|
console.log(` ✔ ${label} -> 已跳过`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const layer = body.children.find((c) => c.className === "wi-click-fx-layer");
|
||||||
|
if (!layer) {
|
||||||
|
failures.push(`${label}:未创建 .wi-click-fx-layer`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (layer.children.length !== 0) {
|
||||||
|
failures.push(`${label}:初始化时特效层不应有子节点`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = attrs["data-effect"] || "(缺失,走兜底 values)";
|
||||||
|
if (listeners.click) {
|
||||||
|
listeners.click[0]({ clientX: 120, clientY: 240 });
|
||||||
|
} else if (listeners.mousemove) {
|
||||||
|
listeners.mousemove[0]({ clientX: 120, clientY: 240 });
|
||||||
|
} else {
|
||||||
|
failures.push(`${label}:未注册 click 或 mousemove 监听`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spawned = layer.children;
|
||||||
|
if (spawned.length === 0) {
|
||||||
|
failures.push(`${label}:effect=${type} 触发后未生成任何特效节点`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const first = spawned[0];
|
||||||
|
if (!String(first.className).startsWith("wi-click-fx ")) {
|
||||||
|
failures.push(`${label}:特效节点缺少 wi-click-fx 基类(${first.className})`);
|
||||||
|
}
|
||||||
|
if (first.style.left !== "120px" || first.style.top !== "240px") {
|
||||||
|
failures.push(`${label}:定位不正确(${first.style.left}, ${first.style.top})`);
|
||||||
|
}
|
||||||
|
console.log(` ✔ ${label} effect=${type} -> ${spawned.length} 个节点,className=${first.className}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("禁用场景:");
|
||||||
|
run({ "data-enabled": "false", "data-effect": "values" }, false, "显式关闭");
|
||||||
|
run({ "data-effect": "values" }, false, "配置缺失");
|
||||||
|
run({ "data-enabled": "true", "data-effect": "off" }, false, "类型选了「关闭」");
|
||||||
|
|
||||||
|
console.log("点击场景:");
|
||||||
|
for (const effect of ["values", "hearts", "fireworks", "ripples", "particles", "stars"]) {
|
||||||
|
run({ "data-enabled": "true", "data-effect": effect }, true, effect);
|
||||||
|
}
|
||||||
|
console.log("兜底与拖尾:");
|
||||||
|
run({ "data-enabled": "true" }, true, "type 缺省");
|
||||||
|
run({ "data-enabled": "true", "data-effect": "star-trail" }, true, "star-trail");
|
||||||
|
run({ "data-enabled": "true", "data-effect": "particle-trail" }, true, "particle-trail");
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
console.error("\n[smoke] 失败:");
|
||||||
|
for (const f of failures) console.error(" - " + f);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log("\n[smoke] 点击特效全部通过");
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* 把当前构建产出的哈希静态资源快照进 compat/assets。
|
||||||
|
*
|
||||||
|
* 为什么要这么做:
|
||||||
|
* Astro 给 CSS/JS 加内容哈希,主题升级后旧文件会被删除。若站点缓存或反向代理
|
||||||
|
* (Halo 页面缓存 / Nginx / CDN)里还留着上一版本的 HTML,它仍然会请求旧哈希
|
||||||
|
* 文件,Halo 找不到就直接抛 FileNotFoundException 返回 500,表现为"整站没样式"。
|
||||||
|
* 把每个版本的哈希文件都留一份,旧 HTML 就永远不会请求失败。
|
||||||
|
*
|
||||||
|
* 用法:npm run build && npm run snapshot(随后把 compat/assets 的新增文件提交进仓库)
|
||||||
|
*/
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const assetsDir = path.join(root, "templates", "assets");
|
||||||
|
const compatDir = path.join(root, "compat", "assets");
|
||||||
|
|
||||||
|
if (!fs.existsSync(assetsDir)) {
|
||||||
|
console.error("[snapshot] 未找到 templates/assets,请先执行 npm run build");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(compatDir, { recursive: true });
|
||||||
|
|
||||||
|
/** 名称固定、体积大的资源不需要进兼容层 */
|
||||||
|
const SKIP_EXACT = new Set(["logo.png"]);
|
||||||
|
const SKIP_DIRS = new Set(["fonts"]);
|
||||||
|
|
||||||
|
let added = 0;
|
||||||
|
let updated = 0;
|
||||||
|
let unchanged = 0;
|
||||||
|
|
||||||
|
for (const entry of fs.readdirSync(assetsDir, { withFileTypes: true })) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (SKIP_DIRS.has(entry.name)) continue;
|
||||||
|
// 哈希资源都在根目录,子目录(如 fonts)不参与快照
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (SKIP_EXACT.has(entry.name)) continue;
|
||||||
|
|
||||||
|
const src = path.join(assetsDir, entry.name);
|
||||||
|
const dst = path.join(compatDir, entry.name);
|
||||||
|
const stat = fs.statSync(src);
|
||||||
|
if (!fs.existsSync(dst)) {
|
||||||
|
fs.copyFileSync(src, dst);
|
||||||
|
added++;
|
||||||
|
} else {
|
||||||
|
const dstBuf = fs.readFileSync(dst);
|
||||||
|
const srcBuf = fs.readFileSync(src);
|
||||||
|
if (dstBuf.length !== stat.size || !dstBuf.equals(srcBuf)) {
|
||||||
|
fs.copyFileSync(src, dst);
|
||||||
|
updated++;
|
||||||
|
} else {
|
||||||
|
unchanged++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[snapshot] 新增 ${added} / 更新 ${updated} / 无变化 ${unchanged} 个历史资源(compat/assets)`
|
||||||
|
);
|
||||||
+187
-101
@@ -1,14 +1,35 @@
|
|||||||
---
|
---
|
||||||
|
/*
|
||||||
|
* 点击特效组件。
|
||||||
|
*
|
||||||
|
* 设计要点:容器节点始终输出,由脚本读取 data-enabled / data-effect 决定行为。
|
||||||
|
* 旧写法用 th:if 依赖配置值严格等于布尔 true,一旦配置未落库、值不是布尔,
|
||||||
|
* 或 Thymeleaf 求值把属性移除,节点就不会渲染 —— 特效静默失效且无任何报错。
|
||||||
|
*
|
||||||
|
* 特效元素统一挂到 body 下的 .wi-click-fx-layer:
|
||||||
|
* .wi-main 是独立的层叠上下文(z-index:1),留在里面会被导航栏/页脚遮住。
|
||||||
|
*
|
||||||
|
* spawn() 里必须强制回流,否则新节点的初态与终态在同一帧被合并,
|
||||||
|
* transition 不会触发,表现同样是"点了没反应"。
|
||||||
|
*/
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="wi-click-effect" th:if="${theme.config?.style?.click_effect_enabled == true}" th:attr="data-effect=${theme.config?.style?.click_effect_type}">
|
<div
|
||||||
|
id="wi-click-effect"
|
||||||
|
hidden
|
||||||
|
th:attr="data-effect=${theme.config?.style?.click_effect_type ?: 'values'},data-enabled=${theme.config?.style?.click_effect_enabled}"
|
||||||
|
></div>
|
||||||
|
|
||||||
<script is:inline th:inline="none">
|
<script is:inline th:inline="none">
|
||||||
(function () {
|
(function () {
|
||||||
var container = document.getElementById("wi-click-effect");
|
var cfg = document.getElementById("wi-click-effect");
|
||||||
if (!container) return;
|
if (!cfg) return;
|
||||||
var effect = container.getAttribute("data-effect") || "off";
|
|
||||||
if (effect === "off" || !effect) return;
|
if ((cfg.getAttribute("data-enabled") || "").trim() !== "true") return;
|
||||||
|
|
||||||
|
var effect = (cfg.getAttribute("data-effect") || "").trim();
|
||||||
|
if (!effect) effect = "values";
|
||||||
|
if (effect === "off") return;
|
||||||
|
|
||||||
/* 社会主义核心价值观 */
|
/* 社会主义核心价值观 */
|
||||||
var CORE_VALUES = [
|
var CORE_VALUES = [
|
||||||
@@ -23,23 +44,47 @@
|
|||||||
"#51cf66", "#94d82d", "#fcc419", "#ff922b"
|
"#51cf66", "#94d82d", "#fcc419", "#ff922b"
|
||||||
];
|
];
|
||||||
|
|
||||||
/* 创建单个 DOM 元素并加入舞台 */
|
function pick(list) {
|
||||||
|
return list[Math.floor(Math.random() * list.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 特效层统一挂在 body 下:
|
||||||
|
* .wi-main 自身是层叠上下文(z-index:1),留在里面会被导航栏/页脚遮住;
|
||||||
|
* 同时该层 position:fixed 且铺满视口,内部 fixed 子元素的
|
||||||
|
* left/top 仍等价于 clientX/clientY。
|
||||||
|
*/
|
||||||
|
var layer = document.createElement("div");
|
||||||
|
layer.className = "wi-click-fx-layer";
|
||||||
|
document.body.appendChild(layer);
|
||||||
|
|
||||||
|
/* 创建单个 DOM 元素并加入特效层,返回后即可设置终态样式 */
|
||||||
function spawn(el, x, y) {
|
function spawn(el, x, y) {
|
||||||
el.style.left = x + "px";
|
el.style.left = x + "px";
|
||||||
el.style.top = y + "px";
|
el.style.top = y + "px";
|
||||||
container.appendChild(el);
|
layer.appendChild(el);
|
||||||
|
/*
|
||||||
|
* 强制回流:新插入的元素若在同一帧内被改写 transform/opacity,
|
||||||
|
* 浏览器会把初态与终态合并,transition 不会触发 —— 表现为"没有任何效果"。
|
||||||
|
*/
|
||||||
|
void el.offsetWidth;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLater(el, delay) {
|
||||||
|
setTimeout(function () {
|
||||||
|
if (el.parentNode) el.parentNode.removeChild(el);
|
||||||
|
}, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 1. 社会主义核心价值观 ========== */
|
/* ========== 1. 社会主义核心价值观 ========== */
|
||||||
function spawnValueText(x, y) {
|
function spawnValueText(x, y) {
|
||||||
var el = document.createElement("span");
|
var el = document.createElement("span");
|
||||||
el.className = "wi-click-fx wi-click-fx--text";
|
el.className = "wi-click-fx wi-click-fx--text";
|
||||||
el.textContent = CORE_VALUES[Math.floor(Math.random() * CORE_VALUES.length)];
|
el.textContent = pick(CORE_VALUES);
|
||||||
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
el.style.color = pick(RANDOM_COLORS);
|
||||||
spawn(el, x, y);
|
spawn(el, x, y);
|
||||||
setTimeout(function () {
|
removeLater(el, 1300);
|
||||||
if (el.parentNode) el.parentNode.removeChild(el);
|
|
||||||
}, 1200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 2. 随机彩色爱心 ========== */
|
/* ========== 2. 随机彩色爱心 ========== */
|
||||||
@@ -47,89 +92,61 @@
|
|||||||
var el = document.createElement("span");
|
var el = document.createElement("span");
|
||||||
el.className = "wi-click-fx wi-click-fx--heart";
|
el.className = "wi-click-fx wi-click-fx--heart";
|
||||||
el.innerHTML = "❤";
|
el.innerHTML = "❤";
|
||||||
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
el.style.color = pick(RANDOM_COLORS);
|
||||||
var dx = (Math.random() - 0.5) * 80;
|
|
||||||
spawn(el, x, y);
|
spawn(el, x, y);
|
||||||
requestAnimationFrame(function () {
|
el.style.transform = "translate(calc(-50% + " + ((Math.random() - 0.5) * 80).toFixed(1) + "px), -90px) scale(0.3)";
|
||||||
el.style.transform = "translate(" + dx + "px, -80px) scale(0.2)";
|
|
||||||
el.style.opacity = "0";
|
el.style.opacity = "0";
|
||||||
});
|
removeLater(el, 1100);
|
||||||
setTimeout(function () {
|
|
||||||
if (el.parentNode) el.parentNode.removeChild(el);
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 3. 烟花 ========== */
|
/* ========== 3. 烟花 ========== */
|
||||||
function spawnFirework(x, y) {
|
function spawnFirework(x, y) {
|
||||||
var count = 16;
|
var count = 18;
|
||||||
var color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
var color = pick(RANDOM_COLORS);
|
||||||
for (var i = 0; i < count; i++) {
|
for (var i = 0; i < count; i++) {
|
||||||
var el = document.createElement("span");
|
var el = document.createElement("span");
|
||||||
el.className = "wi-click-fx wi-click-fx--spark";
|
el.className = "wi-click-fx wi-click-fx--spark";
|
||||||
el.style.background = color;
|
el.style.background = color;
|
||||||
el.style.boxShadow = "0 0 6px " + color;
|
el.style.boxShadow = "0 0 6px " + color;
|
||||||
var angle = (Math.PI * 2 * i) / count;
|
var angle = (Math.PI * 2 * i) / count;
|
||||||
var dist = 40 + Math.random() * 30;
|
var dist = 40 + Math.random() * 34;
|
||||||
var dx = Math.cos(angle) * dist;
|
|
||||||
var dy = Math.sin(angle) * dist;
|
|
||||||
spawn(el, x, y);
|
spawn(el, x, y);
|
||||||
requestAnimationFrame(function (e, tx, ty) {
|
el.style.transform = "translate(calc(-50% + " + (Math.cos(angle) * dist).toFixed(1) + "px), calc(-50% + " + (Math.sin(angle) * dist).toFixed(1) + "px)) scale(0)";
|
||||||
return function () {
|
el.style.opacity = "0";
|
||||||
e.style.transform = "translate(" + tx + "px, " + ty + "px) scale(0)";
|
removeLater(el, 900);
|
||||||
e.style.opacity = "0";
|
|
||||||
};
|
|
||||||
}(el, dx, dy));
|
|
||||||
(function (e) {
|
|
||||||
setTimeout(function () {
|
|
||||||
if (e.parentNode) e.parentNode.removeChild(e);
|
|
||||||
}, 800);
|
|
||||||
})(el);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 4. 烟花波纹 ========== */
|
/* ========== 4. 烟花波纹 ========== */
|
||||||
function spawnRipple(x, y) {
|
function spawnRipple(x, y) {
|
||||||
var color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
var color = pick(RANDOM_COLORS);
|
||||||
for (var r = 0; r < 2; r++) {
|
for (var r = 0; r < 2; r++) {
|
||||||
var el = document.createElement("span");
|
var el = document.createElement("span");
|
||||||
el.className = "wi-click-fx wi-click-fx--ripple";
|
el.className = "wi-click-fx wi-click-fx--ripple";
|
||||||
el.style.borderColor = color;
|
el.style.borderColor = color;
|
||||||
spawn(el, x, y);
|
spawn(el, x, y);
|
||||||
(function (e, delay) {
|
(function (el, delay) {
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
requestAnimationFrame(function () {
|
el.style.transform = "translate(-50%, -50%) scale(4)";
|
||||||
e.style.transform = "translate(-50%, -50%) scale(4)";
|
el.style.opacity = "0";
|
||||||
e.style.opacity = "0";
|
removeLater(el, 700);
|
||||||
});
|
|
||||||
}, delay);
|
}, delay);
|
||||||
setTimeout(function () {
|
})(el, r * 160);
|
||||||
if (e.parentNode) e.parentNode.removeChild(e);
|
removeLater(el, 160 + 900);
|
||||||
}, delay + 700);
|
|
||||||
})(el, r * 150);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 5. 粒子 ========== */
|
/* ========== 5. 粒子 ========== */
|
||||||
function spawnParticles(x, y, n) {
|
function spawnParticles(x, y, n) {
|
||||||
n = n || 8;
|
n = n || 10;
|
||||||
for (var i = 0; i < n; i++) {
|
for (var i = 0; i < n; i++) {
|
||||||
var el = document.createElement("span");
|
var el = document.createElement("span");
|
||||||
el.className = "wi-click-fx wi-click-fx--particle";
|
el.className = "wi-click-fx wi-click-fx--particle";
|
||||||
el.style.background = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
el.style.background = pick(RANDOM_COLORS);
|
||||||
var dx = (Math.random() - 0.5) * 60;
|
|
||||||
var dy = (Math.random() - 0.5) * 60 - 20;
|
|
||||||
spawn(el, x, y);
|
spawn(el, x, y);
|
||||||
requestAnimationFrame(function (e, tx, ty) {
|
el.style.transform = "translate(calc(-50% + " + ((Math.random() - 0.5) * 60).toFixed(1) + "px), calc(-50% + " + ((Math.random() - 0.5) * 60 - 20).toFixed(1) + "px)) scale(0)";
|
||||||
return function () {
|
el.style.opacity = "0";
|
||||||
e.style.transform = "translate(" + tx + "px, " + ty + "px) scale(0)";
|
removeLater(el, 800);
|
||||||
e.style.opacity = "0";
|
|
||||||
};
|
|
||||||
}(el, dx, dy));
|
|
||||||
(function (e) {
|
|
||||||
setTimeout(function () {
|
|
||||||
if (e.parentNode) e.parentNode.removeChild(e);
|
|
||||||
}, 700);
|
|
||||||
})(el);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,17 +155,11 @@
|
|||||||
var el = document.createElement("span");
|
var el = document.createElement("span");
|
||||||
el.className = "wi-click-fx wi-click-fx--star";
|
el.className = "wi-click-fx wi-click-fx--star";
|
||||||
el.innerHTML = "★";
|
el.innerHTML = "★";
|
||||||
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
el.style.color = pick(RANDOM_COLORS);
|
||||||
var dx = (Math.random() - 0.5) * 50;
|
|
||||||
var dy = (Math.random() - 0.5) * 50 - 30;
|
|
||||||
spawn(el, x, y);
|
spawn(el, x, y);
|
||||||
requestAnimationFrame(function () {
|
el.style.transform = "translate(calc(-50% + " + ((Math.random() - 0.5) * 50).toFixed(1) + "px), calc(-50% + " + ((Math.random() - 0.5) * 50 - 30).toFixed(1) + "px)) scale(1.6)";
|
||||||
el.style.transform = "translate(" + dx + "px, " + dy + "px) scale(1.6)";
|
|
||||||
el.style.opacity = "0";
|
el.style.opacity = "0";
|
||||||
});
|
removeLater(el, 900);
|
||||||
setTimeout(function () {
|
|
||||||
if (el.parentNode) el.parentNode.removeChild(el);
|
|
||||||
}, 800);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 点击分发 */
|
/* 点击分发 */
|
||||||
@@ -162,26 +173,28 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* 拖尾效果 */
|
/* 拖尾效果 */
|
||||||
var trailHandlers = {
|
function spawnTrailStar(x, y) {
|
||||||
"star-trail": function (x, y) {
|
|
||||||
var el = document.createElement("span");
|
var el = document.createElement("span");
|
||||||
el.className = "wi-click-fx wi-click-fx--trail-star";
|
el.className = "wi-click-fx wi-click-fx--trail-star";
|
||||||
el.innerHTML = "★";
|
el.innerHTML = "★";
|
||||||
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
el.style.color = pick(RANDOM_COLORS);
|
||||||
spawn(el, x, y);
|
spawn(el, x, y);
|
||||||
setTimeout(function () {
|
el.style.opacity = "0";
|
||||||
if (el.parentNode) el.parentNode.removeChild(el);
|
removeLater(el, 700);
|
||||||
}, 600);
|
}
|
||||||
},
|
|
||||||
"particle-trail": function (x, y) {
|
function spawnTrailParticle(x, y) {
|
||||||
var el = document.createElement("span");
|
var el = document.createElement("span");
|
||||||
el.className = "wi-click-fx wi-click-fx--trail-particle";
|
el.className = "wi-click-fx wi-click-fx--trail-particle";
|
||||||
el.style.background = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
|
el.style.background = pick(RANDOM_COLORS);
|
||||||
spawn(el, x, y);
|
spawn(el, x, y);
|
||||||
setTimeout(function () {
|
el.style.opacity = "0";
|
||||||
if (el.parentNode) el.parentNode.removeChild(el);
|
removeLater(el, 600);
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var trailHandlers = {
|
||||||
|
"star-trail": spawnTrailStar,
|
||||||
|
"particle-trail": spawnTrailParticle
|
||||||
};
|
};
|
||||||
|
|
||||||
var isTrail = effect.indexOf("trail") !== -1;
|
var isTrail = effect.indexOf("trail") !== -1;
|
||||||
@@ -189,55 +202,128 @@
|
|||||||
if (isTrail) {
|
if (isTrail) {
|
||||||
var handler = trailHandlers[effect];
|
var handler = trailHandlers[effect];
|
||||||
if (!handler) return;
|
if (!handler) return;
|
||||||
var lastX = 0, lastY = 0;
|
var lastX = 0;
|
||||||
var throttle = false;
|
var lastY = 0;
|
||||||
document.addEventListener("mousemove", function (e) {
|
var throttling = false;
|
||||||
|
document.addEventListener(
|
||||||
|
"mousemove",
|
||||||
|
function (e) {
|
||||||
lastX = e.clientX;
|
lastX = e.clientX;
|
||||||
lastY = e.clientY;
|
lastY = e.clientY;
|
||||||
if (throttle) return;
|
if (throttling) return;
|
||||||
throttle = true;
|
throttling = true;
|
||||||
requestAnimationFrame(function () {
|
requestAnimationFrame(function () {
|
||||||
throttle = false;
|
throttling = false;
|
||||||
handler(lastX, lastY);
|
handler(lastX, lastY);
|
||||||
});
|
});
|
||||||
}, { passive: true });
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
var handler = clickHandlers[effect];
|
var handler = clickHandlers[effect];
|
||||||
if (!handler) return;
|
if (!handler) return;
|
||||||
document.addEventListener("click", function (e) {
|
document.addEventListener(
|
||||||
|
"click",
|
||||||
|
function (e) {
|
||||||
handler(e.clientX, e.clientY);
|
handler(e.clientX, e.clientY);
|
||||||
}, { passive: true });
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style is:global>
|
<style is:global>
|
||||||
.wi-click-fx{position:fixed;pointer-events:none;will-change:transform,opacity
|
/* 特效层:铺满视口的固定层,统一承载所有点击/拖尾元素 */
|
||||||
|
.wi-click-fx-layer {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 12050;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wi-click-fx--text{font-size:18px;font-weight:700;transform:translate(-50%,-50%);opacity:0;animation:wi-fx-text 1.2s ease-out forwards
|
.wi-click-fx {
|
||||||
|
position: fixed;
|
||||||
|
pointer-events: none;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wi-click-fx--heart{font-size:20px;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform 1s cubic-bezier(.16,1,.3,1),opacity 1s ease-out
|
.wi-click-fx--text {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
animation: wi-fx-text 1.2s ease-out forwards;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wi-click-fx--spark{width:6px;height:6px;border-radius:50%;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform .8s cubic-bezier(.16,1,.3,1),opacity .8s ease-out
|
.wi-click-fx--heart {
|
||||||
|
font-size: 20px;
|
||||||
|
opacity: 1;
|
||||||
|
transition: transform 1s cubic-bezier(0.16, 1, 0.3, 1), opacity 1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wi-click-fx--ripple{width:30px;height:30px;border:2px solid;border-radius:50%;transform:translate(-50%,-50%) scale(.5);opacity:1;transition:transform .7s ease-out,opacity .7s ease-out
|
.wi-click-fx--spark {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 1;
|
||||||
|
transition: transform 0.8s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.8s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wi-click-fx--particle{width:8px;height:8px;border-radius:50%;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform .7s cubic-bezier(.16,1,.3,1),opacity .7s ease-out
|
.wi-click-fx--ripple {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%) scale(0.5);
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: transform 0.7s ease-out, opacity 0.7s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wi-click-fx--star{font-size:16px;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform .8s cubic-bezier(.16,1,.3,1),opacity .8s ease-out
|
.wi-click-fx--particle {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 1;
|
||||||
|
transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.7s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wi-click-fx--trail-star{font-size:12px;transform:translate(-50%,-50%) scale(1);opacity:.8;transition:opacity .6s ease-out,transform .6s ease-out
|
.wi-click-fx--star {
|
||||||
|
font-size: 16px;
|
||||||
|
opacity: 1;
|
||||||
|
transition: transform 0.8s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.8s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wi-click-fx--trail-particle{width:6px;height:6px;border-radius:50%;transform:translate(-50%,-50%) scale(1);opacity:.8;transition:opacity .5s ease-out,transform .5s ease-out
|
.wi-click-fx--trail-star {
|
||||||
|
font-size: 12px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes wi-fx-text{0%{opacity:0;transform:translate(-50%,-50%) scale(.5)}20%{opacity:1;transform:translate(-50%,-80%) scale(1.1)}to{opacity:0;transform:translate(-50%,-120%) scale(.9)}}
|
.wi-click-fx--trail-particle {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 0.5s ease-out, transform 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes wi-fx-text {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%) scale(0.5);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -80%) scale(1.1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -120%) scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ spec:
|
|||||||
description: 微信读书书架(依赖微信读书插件)
|
description: 微信读书书架(依赖微信读书插件)
|
||||||
screenshot:
|
screenshot:
|
||||||
file: weread.html
|
file: weread.html
|
||||||
version: "1.2.0"
|
version: "1.2.1"
|
||||||
requires: ">=2.26.0"
|
requires: ">=2.26.0"
|
||||||
license:
|
license:
|
||||||
- name: "GPL-3.0"
|
- name: "GPL-3.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user