xhs-minitool-creator 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -102
- package/bin/create.mjs +16 -27
- package/package.json +1 -1
- package/src/pack.mjs +4 -17
- package/template/index.html +1 -1
- package/template/package.json +1 -1
- package/template/src/main.js +10 -24
- package/template/src/styles/app.css +1 -1
- package/template/public/icons/icon-192.svg +0 -4
- package/template/public/icons/icon-512.svg +0 -4
- package/template/src/lib/storage.js +0 -54
package/README.md
CHANGED
|
@@ -1,143 +1,80 @@
|
|
|
1
1
|
# xhs-minitool-creator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
小红书「小工具」项目脚手架:创建、构建、打包、合规校验,一条命令完成。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
你不需要先啃小红书小工具的平台规范,脚手架已把平台约束内置到构建配置与校验规则中。用它产出的 zip 直接上传创作服务平台即可。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## 快速上手(3 步)
|
|
10
|
-
|
|
11
|
-
> 前提:电脑上装好 [Node.js](https://nodejs.org)(18.17 或更新版本)。装好后在终端输入 `node -v` 能显示版本号即可。
|
|
12
|
-
|
|
13
|
-
### 第 1 步:创建项目
|
|
14
|
-
|
|
15
|
-
在终端里输入(把 `my-tool` 换成你喜欢的名字,只能用小写字母、数字和短横线):
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npx xhs-minitool-creator my-tool
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
输入后它会问你几个简单的问题(页面标题、描述等),**不想回答就一路按回车**,全部用默认值即可。
|
|
22
|
-
|
|
23
|
-
### 第 2 步:进入项目并安装依赖
|
|
7
|
+
## 快速开始
|
|
24
8
|
|
|
25
9
|
```bash
|
|
10
|
+
npx xhs-minitool-creator my-tool -y
|
|
26
11
|
cd my-tool
|
|
27
12
|
npm install
|
|
13
|
+
npm run build # 产出 my-tool-xhs-minitool.zip,上传创作服务平台即可
|
|
28
14
|
```
|
|
29
15
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
### 第 3 步:打包
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npm run build
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
完成后,项目目录下会多出一个 `my-tool-xhs-minitool.zip`。**把这个 zip 上传到小红书创作服务平台,你的小工具就上线了。**
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
## 日常使用
|
|
16
|
+
交互式创建(自定义标题 / 描述):
|
|
43
17
|
|
|
44
18
|
```bash
|
|
45
|
-
|
|
46
|
-
npm run build # 打包:生成可上传的 zip
|
|
47
|
-
npm run validate # 体检:检查有没有违反平台规则的地方
|
|
19
|
+
npx xhs-minitool-creator my-tool
|
|
48
20
|
```
|
|
49
21
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
## 常见问题
|
|
22
|
+
## 命令
|
|
55
23
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
会用就行。它生成的项目自带一个能跑通的示例页面(输入名字、显示计数),你可以直接照葫芦画瓢改文字、改颜色。想做得更复杂,可以基于示例继续加代码,或让 AI 按项目里自带的开发规范(`.agents/` 目录)帮你改。
|
|
59
|
-
|
|
60
|
-
**打包时报错了怎么办?**
|
|
61
|
-
|
|
62
|
-
执行 `npm run validate`,它会告诉你哪里不合规、怎么改。大多数问题(比如引用了网络图片)它都能直接指出。
|
|
63
|
-
|
|
64
|
-
**zip 最大能多大?**
|
|
65
|
-
|
|
66
|
-
平台硬性上限 10 MB,建议控制在 2 MB 以内。图片尽量压缩后再放进去。打包时工具会自动汇报体积。
|
|
67
|
-
|
|
68
|
-
**为什么不能直接引用网络图片?**
|
|
69
|
-
|
|
70
|
-
小红书小工具在离线环境运行,网络图片、网络请求都会被禁止。把图片下载下来放进 `public/` 目录即可。
|
|
71
|
-
|
|
72
|
-
**这个工具和小红书官方是什么关系?**
|
|
73
|
-
|
|
74
|
-
没有隶属关系。它只是把小红书的官方开发规范内置进来帮你自动检查,规范原文就在项目 `.agents/skills/` 目录里,会随官方更新。
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
## 命令速查
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
npx xhs-minitool-creator <名字> 创建项目(默认)
|
|
82
|
-
npx xhs-minitool-creator <名字> --yes 跳过提问,全部用默认值
|
|
83
|
-
npx xhs-minitool-creator <名字> --title "我的工具" 指定页面标题
|
|
84
|
-
npx xhs-minitool-creator <名字> --theme "#ff2442" 指定主题色
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
| 选项 | 说明 |
|
|
24
|
+
| 命令 | 作用 |
|
|
88
25
|
| --- | --- |
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
| `--theme` | 主题色(默认 `#ff2442`) |
|
|
26
|
+
| `npm run dev` | 本地预览(Vite dev server) |
|
|
27
|
+
| `npm run build` | 构建 + 打包,输出 `<name>-xhs-minitool.zip` |
|
|
28
|
+
| `npm run validate` | 静态合规校验(输出 ERROR / WARNING 汇总) |
|
|
29
|
+
| `npm run skill` | 查看项目内置官方规范版本 |
|
|
94
30
|
|
|
95
|
-
|
|
31
|
+
CLI 子命令 `create / pack / validate / skill`,详见 `npx xhs-minitool-creator --help`。
|
|
96
32
|
|
|
97
|
-
|
|
33
|
+
create 选项:`--yes`(跳过交互)、`--title`、`--desc`。
|
|
98
34
|
|
|
99
|
-
|
|
35
|
+
## 生成的项目
|
|
100
36
|
|
|
101
|
-
|
|
37
|
+
- Vite 项目,构建预设已内置平台约束,输出目录 `xhs-minitool/`
|
|
38
|
+
- 示例页面:单页多视图切换、适配平台原生导航栏与安全区
|
|
39
|
+
- 官方打包规范 `minitool-zip-builder` 快照(`.agents/skills/`),可直接供 AI 参考辅助开发
|
|
102
40
|
|
|
103
|
-
|
|
104
|
-
- 已配置好的构建工具(Vite),**默认输出到 `xhs-minitool/` 目录**
|
|
105
|
-
- 打包与检查命令(纯 Node 实现,Windows / macOS / Linux 都能用)
|
|
106
|
-
- 官方开发规范 `minitool-zip-builder`(放在 `.agents/skills/`,给 AI 或人参考)
|
|
41
|
+
## 平台约束速览
|
|
107
42
|
|
|
108
|
-
|
|
43
|
+
- **完全离线**:禁 `fetch` / `XMLHttpRequest` / `WebSocket`;外部 `http(s)://` 资源(图片、字体、CDN 脚本)必须下载进包改相对路径
|
|
44
|
+
- **CSP 限制**:禁内联 `<script>` 与行内事件(`onclick=` 等),脚本必须外置 + `addEventListener`
|
|
45
|
+
- **经典脚本**:产物不允许 `type="module"` / `import`(离线环境下模块解析不可靠)——脚手架打包时自动转换,勿自行改回
|
|
46
|
+
- **单页应用**:容器不打开新页面,多视图在单页内用 JS 切换 DOM
|
|
47
|
+
- **WebView 基线低**:Chrome 61 / ES2017。构建预设已设 `build.target: ['es2017', 'chrome61']`,若自己覆盖 Vite 配置别把它弄丢(否则 `??` 等 ES2020 语法会泄漏,真机白屏无报错)
|
|
48
|
+
- **体积**:zip 硬上限 10 MiB,建议 ≤ 2 MiB;单条 Base64 ≤ 1 MiB
|
|
49
|
+
- **禁用 API**:`window.open`、Worker、剪贴板、地理定位、设备传感器、WebAssembly 等(`validate` 会扫描)
|
|
50
|
+
- **安全区**:`viewport-fit=cover` + `env(safe-area-inset-*)`,且顶部需为平台原生导航栏让位
|
|
51
|
+
- **原生能力**:官方提供 4 个 JSBridge API(发笔记、存相册等,见 `.agents/skills/` 内 `jsbridge-api.md`)
|
|
109
52
|
|
|
110
|
-
|
|
53
|
+
`npm run validate` 内置上述规则的静态扫描;静态检查无法覆盖运行时行为,交付前务必真机预览。
|
|
111
54
|
|
|
112
|
-
|
|
113
|
-
2. 在项目目录执行:
|
|
55
|
+
## 更新官方规范
|
|
114
56
|
|
|
115
|
-
|
|
116
|
-
npx xhs-minitool-creator skill update --url <地址>
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
### 检查命令的更多用法
|
|
57
|
+
官方规范经小红书 CDN 分发,**无稳定 latest 端点**(URL 含时间戳),需手动更新:
|
|
120
58
|
|
|
121
59
|
```bash
|
|
122
|
-
npx xhs-minitool-creator
|
|
123
|
-
npx xhs-minitool-creator validate ./xxx-xhs-minitool.zip # 检查已打包的 zip
|
|
124
|
-
npx xhs-minitool-creator pack --outDir <目录> # 指定产物目录再打包
|
|
60
|
+
npx xhs-minitool-creator skill update --url <官方 .skill 包地址>
|
|
125
61
|
```
|
|
126
62
|
|
|
127
|
-
|
|
63
|
+
## 编程式使用
|
|
128
64
|
|
|
129
65
|
```js
|
|
130
66
|
import { defineMinitoolConfig } from 'xhs-minitool-creator/vite';
|
|
131
67
|
import { pack, validateDir } from 'xhs-minitool-creator';
|
|
132
68
|
```
|
|
133
69
|
|
|
134
|
-
|
|
70
|
+
## 打包器说明
|
|
71
|
+
|
|
72
|
+
打包优先使用系统 `zip` 命令(macOS 自带),未安装则回退内置纯 Node 实现,两条路径产物一致;校验与 zip 读写零系统依赖(Windows / Linux 均可用)。
|
|
135
73
|
|
|
136
74
|
## 说明
|
|
137
75
|
|
|
138
|
-
-
|
|
139
|
-
-
|
|
140
|
-
- 平台规则可能随时调整,关键决策以小红书创作服务平台实时规则为准
|
|
76
|
+
- 本工具与小红书官方无隶属关系,内置规范内容版权归小红书
|
|
77
|
+
- 平台规则可能调整,关键决策以创作服务平台实时规则为准
|
|
141
78
|
|
|
142
79
|
## License
|
|
143
80
|
|
package/bin/create.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 零运行时依赖,仅使用 Node 内置模块。
|
|
12
12
|
*/
|
|
13
13
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
14
|
-
import { join, resolve, relative, dirname } from 'node:path';
|
|
14
|
+
import { join, resolve, relative, dirname, basename } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { createInterface } from 'node:readline/promises';
|
|
17
17
|
import { stdin, stdout } from 'node:process';
|
|
@@ -36,8 +36,6 @@ create 选项:
|
|
|
36
36
|
-y, --yes 跳过交互,全部使用默认值
|
|
37
37
|
--title <标题> 页面标题(默认同项目名)
|
|
38
38
|
--desc <描述> 项目描述
|
|
39
|
-
--icon <字符> 首页图标字符(默认「工」)
|
|
40
|
-
--theme <颜色> 主题色(默认 #ff2442)
|
|
41
39
|
|
|
42
40
|
pack 选项:
|
|
43
41
|
--outDir <目录> 构建产物目录(默认 xhs-minitool)
|
|
@@ -63,8 +61,7 @@ function parseArgs(argv) {
|
|
|
63
61
|
if (a === '--yes' || a === '-y') args.flags.yes = true;
|
|
64
62
|
else if (a === '--strict') args.flags.strict = true;
|
|
65
63
|
else if (a === '--no-strict' || a === '--strict=false') args.flags.strict = false;
|
|
66
|
-
else if (a === '--title' || a === '--desc' || a === '--
|
|
67
|
-
a === '--outDir' || a === '--zipName' || a === '--url') {
|
|
64
|
+
else if (a === '--title' || a === '--desc' || a === '--outDir' || a === '--zipName' || a === '--url') {
|
|
68
65
|
const v = argv[i + 1];
|
|
69
66
|
if (!v) fail(`选项 ${a} 缺少参数`);
|
|
70
67
|
args.flags[a.slice(2)] = v;
|
|
@@ -119,6 +116,9 @@ function copyTemplate(targetDir, vars) {
|
|
|
119
116
|
|
|
120
117
|
let count = 0;
|
|
121
118
|
for (const file of walk(srcDir)) {
|
|
119
|
+
// macOS 目录垃圾文件不入模板
|
|
120
|
+
if (basename(file) === '.DS_Store') continue;
|
|
121
|
+
|
|
122
122
|
let rel = relative(srcDir, file).split('\\').join('/');
|
|
123
123
|
// npm 发布时 .gitignore 会被吞掉,模板内用 _gitignore 占位,落盘时改回
|
|
124
124
|
if (rel === '_gitignore') rel = '.gitignore';
|
|
@@ -137,10 +137,7 @@ function copyTemplate(targetDir, vars) {
|
|
|
137
137
|
text = text
|
|
138
138
|
.replace(/\{\{NAME\}\}/g, vars.title)
|
|
139
139
|
.replace(/\{\{TITLE\}\}/g, vars.title)
|
|
140
|
-
.replace(/\{\{DESC\}\}/g, vars.desc)
|
|
141
|
-
.replace(/\{\{ICON\}\}/g, vars.icon);
|
|
142
|
-
} else if (rel.endsWith('.css') && vars.theme && vars.theme !== '#ff2442') {
|
|
143
|
-
text = text.replace(/--accent:\s*#[0-9a-fA-F]{3,8};/, `--accent: ${vars.theme};`);
|
|
140
|
+
.replace(/\{\{DESC\}\}/g, vars.desc);
|
|
144
141
|
}
|
|
145
142
|
content = Buffer.from(text, 'utf8');
|
|
146
143
|
}
|
|
@@ -160,26 +157,20 @@ async function createProject(args, flags) {
|
|
|
160
157
|
if (!flags.yes && tty) {
|
|
161
158
|
console.log('\n创建小红书小工具项目\n');
|
|
162
159
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
).then((v) => normalizeName(v));
|
|
169
|
-
if (!input) {
|
|
170
|
-
console.log(' 名称无效,请重新输入');
|
|
160
|
+
while (!name || !validName(name)) {
|
|
161
|
+
const answer = await prompt('项目名(必填,小写字母/数字/连字符,如 lucky-draw):', '');
|
|
162
|
+
name = normalizeName(answer);
|
|
163
|
+
if (!name || !validName(name)) {
|
|
164
|
+
console.log(' 名称无效:需以小写字母开头,仅含小写字母/数字/连字符');
|
|
171
165
|
}
|
|
172
166
|
}
|
|
173
|
-
name = input;
|
|
174
167
|
|
|
175
|
-
const title = await prompt(
|
|
176
|
-
const desc = await prompt('
|
|
177
|
-
const icon = await prompt('首页图标字符(默认「工」):', '工');
|
|
178
|
-
const theme = await prompt('主题色(默认 #ff2442):', '#ff2442');
|
|
168
|
+
const title = await prompt(`页面标题(回车跳过,默认「${name}」):`, name);
|
|
169
|
+
const desc = await prompt('项目描述(回车跳过):', '一个小红书小工具');
|
|
179
170
|
|
|
180
|
-
const finalVars = { name, title, desc
|
|
181
|
-
console.log(`\n
|
|
182
|
-
const ok = await prompt('
|
|
171
|
+
const finalVars = { name, title, desc };
|
|
172
|
+
console.log(`\n将创建:./${name}(标题「${title}」)`);
|
|
173
|
+
const ok = await prompt('确认创建?(Y/n,回车确认):', 'Y');
|
|
183
174
|
if (ok && !/^y/i.test(ok)) fail('已取消');
|
|
184
175
|
await doCreate(name, finalVars);
|
|
185
176
|
} else {
|
|
@@ -193,8 +184,6 @@ async function createProject(args, flags) {
|
|
|
193
184
|
name,
|
|
194
185
|
title: flags.title || name,
|
|
195
186
|
desc: flags.desc || '一个小红书小工具',
|
|
196
|
-
icon: flags.icon || '工',
|
|
197
|
-
theme: flags.theme || '#ff2442',
|
|
198
187
|
};
|
|
199
188
|
await doCreate(name, finalVars);
|
|
200
189
|
}
|
package/package.json
CHANGED
package/src/pack.mjs
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* 关键改进:zip 用纯 Node 实现(src/zip.mjs),
|
|
11
11
|
* 不再依赖系统的 zip / zipinfo 二进制,Windows 亦可用。
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { extname, join, relative, resolve } from 'node:path';
|
|
15
15
|
import { spawnSync } from 'node:child_process';
|
|
16
16
|
import { createZip, listZipEntries } from './zip.mjs';
|
|
17
17
|
import { ALLOWED_EXT, validateDir, validateZip, formatIssues } from './validate.mjs';
|
|
@@ -89,17 +89,6 @@ export function patchIndexHtml(outDir) {
|
|
|
89
89
|
return scripts.length;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function copyIcons(outDir, publicDir) {
|
|
93
|
-
for (const icon of ['icon-192.svg', 'icon-512.svg']) {
|
|
94
|
-
const dest = join(outDir, 'icons', icon);
|
|
95
|
-
const src = join(publicDir, 'icons', icon);
|
|
96
|
-
if (!existsSync(dest) && existsSync(src)) {
|
|
97
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
98
|
-
copyFileSync(src, dest);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
92
|
function readPackageName(cwd) {
|
|
104
93
|
const pkgPath = join(cwd, 'package.json');
|
|
105
94
|
if (!existsSync(pkgPath)) return 'minitool';
|
|
@@ -143,8 +132,8 @@ function makeZip(outDir, zipPath, entries) {
|
|
|
143
132
|
|
|
144
133
|
/**
|
|
145
134
|
* @param {{
|
|
146
|
-
* cwd?: string, outDir?: string, zipName?: string,
|
|
147
|
-
*
|
|
135
|
+
* cwd?: string, outDir?: string, zipName?: string,
|
|
136
|
+
* strict?: boolean
|
|
148
137
|
* }} [options]
|
|
149
138
|
*/
|
|
150
139
|
export function pack(options = {}) {
|
|
@@ -152,14 +141,12 @@ export function pack(options = {}) {
|
|
|
152
141
|
const outDir = resolve(cwd, options.outDir || 'xhs-minitool');
|
|
153
142
|
const zipName = options.zipName || `${readPackageName(cwd)}-xhs-minitool.zip`;
|
|
154
143
|
const zipPath = resolve(cwd, zipName);
|
|
155
|
-
const publicDir = resolve(cwd, options.publicDir || 'public');
|
|
156
144
|
|
|
157
145
|
if (!existsSync(outDir)) {
|
|
158
146
|
throw new Error(`缺少输出目录 ${relative(cwd, outDir)},请先执行构建`);
|
|
159
147
|
}
|
|
160
148
|
|
|
161
149
|
console.log('\n▸ 整理产物');
|
|
162
|
-
copyIcons(outDir, publicDir);
|
|
163
150
|
removeUnsupported(outDir);
|
|
164
151
|
pruneEmptyDirs(outDir);
|
|
165
152
|
const scriptCount = patchIndexHtml(outDir);
|
package/template/index.html
CHANGED
package/template/package.json
CHANGED
package/template/src/main.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import './styles/app.css';
|
|
2
|
-
import { safeStorage } from './lib/storage.js';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* 示例:一个最小可用的小工具页面。
|
|
6
5
|
* 演示三件事:
|
|
7
|
-
* 1. 事件用 addEventListener
|
|
8
|
-
* 2.
|
|
9
|
-
* 3.
|
|
6
|
+
* 1. 事件用 addEventListener 绑定(容器 CSP 禁止 onclick 等行内事件)
|
|
7
|
+
* 2. 多视图在单页内切换(容器不支持打开新页面)
|
|
8
|
+
* 3. 提示用页内 toast 元素实现(容器不支持系统弹窗)
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
const COUNT_KEY = 'demo_count';
|
|
11
|
+
let count = 0;
|
|
14
12
|
|
|
15
|
-
/** 朴素但够用的 toast */
|
|
16
13
|
function toast(message) {
|
|
17
14
|
const el = document.getElementById('toast');
|
|
18
15
|
if (!el) return;
|
|
@@ -28,7 +25,7 @@ function toast(message) {
|
|
|
28
25
|
|
|
29
26
|
function renderCount() {
|
|
30
27
|
const el = document.getElementById('count');
|
|
31
|
-
if (el) el.textContent = String(
|
|
28
|
+
if (el) el.textContent = String(count);
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
function init() {
|
|
@@ -37,12 +34,6 @@ function init() {
|
|
|
37
34
|
const resetBtn = document.getElementById('resetBtn');
|
|
38
35
|
const greet = document.getElementById('greet');
|
|
39
36
|
|
|
40
|
-
// 恢复上次输入
|
|
41
|
-
const saved = safeStorage.get('demo_name');
|
|
42
|
-
if (saved && input) input.value = saved;
|
|
43
|
-
|
|
44
|
-
renderCount();
|
|
45
|
-
|
|
46
37
|
goBtn?.addEventListener('click', () => {
|
|
47
38
|
const name = (input?.value || '').trim();
|
|
48
39
|
if (!name) {
|
|
@@ -50,22 +41,17 @@ function init() {
|
|
|
50
41
|
input?.focus();
|
|
51
42
|
return;
|
|
52
43
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const next = Number(safeStorage.get(COUNT_KEY) || 0) + 1;
|
|
56
|
-
safeStorage.set(COUNT_KEY, String(next));
|
|
44
|
+
count += 1;
|
|
57
45
|
renderCount();
|
|
58
|
-
|
|
59
46
|
if (greet) {
|
|
60
|
-
greet.textContent = `你好,${name}!这是第 ${
|
|
47
|
+
greet.textContent = `你好,${name}!这是第 ${count} 次使用。`;
|
|
61
48
|
greet.hidden = false;
|
|
62
49
|
}
|
|
63
|
-
toast('
|
|
50
|
+
toast('已更新');
|
|
64
51
|
});
|
|
65
52
|
|
|
66
53
|
resetBtn?.addEventListener('click', () => {
|
|
67
|
-
|
|
68
|
-
safeStorage.remove('demo_name');
|
|
54
|
+
count = 0;
|
|
69
55
|
if (input) input.value = '';
|
|
70
56
|
if (greet) greet.hidden = true;
|
|
71
57
|
renderCount();
|
|
@@ -74,7 +60,7 @@ function init() {
|
|
|
74
60
|
|
|
75
61
|
// 主题跟随系统(仅作演示,可删)
|
|
76
62
|
const dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
77
|
-
document.body.dataset.theme =
|
|
63
|
+
document.body.dataset.theme = dark ? 'dark' : 'light';
|
|
78
64
|
}
|
|
79
65
|
|
|
80
66
|
// 经典脚本环境下 DOM 已就绪(脚本在 body 末尾),直接初始化即可
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
|
|
2
|
-
<rect width="192" height="192" rx="42" fill="#FF2442"/>
|
|
3
|
-
<text x="96" y="118" font-size="72" text-anchor="middle" fill="#fff" font-family="-apple-system,sans-serif" font-weight="700">工</text>
|
|
4
|
-
</svg>
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
|
2
|
-
<rect width="192" height="192" rx="112" fill="#FF2442"/>
|
|
3
|
-
<text x="256" y="310" font-size="192" text-anchor="middle" fill="#fff" font-family="-apple-system,sans-serif" font-weight="700">工</text>
|
|
4
|
-
</svg>
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* localStorage 安全包装
|
|
3
|
-
*
|
|
4
|
-
* 容器内 localStorage 通常可用,但个别环境(隐私模式、存储被禁用)会直接抛异常。
|
|
5
|
-
* 未捕获会导致整个脚本中断、页面白屏。这里统一 try/catch,
|
|
6
|
-
* 失败时降级到内存 Map,保证功能不崩(数据在本次会话内有效)。
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const memory = new Map();
|
|
10
|
-
|
|
11
|
-
function canUse() {
|
|
12
|
-
try {
|
|
13
|
-
const k = '__xmc_probe__';
|
|
14
|
-
window.localStorage.setItem(k, '1');
|
|
15
|
-
window.localStorage.removeItem(k);
|
|
16
|
-
return true;
|
|
17
|
-
} catch {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const usable = canUse();
|
|
23
|
-
|
|
24
|
-
export const safeStorage = {
|
|
25
|
-
/** @returns {string|null} */
|
|
26
|
-
get(key) {
|
|
27
|
-
try {
|
|
28
|
-
if (usable) return window.localStorage.getItem(key);
|
|
29
|
-
} catch { /* 降级 */ }
|
|
30
|
-
return memory.has(key) ? memory.get(key) : null;
|
|
31
|
-
},
|
|
32
|
-
set(key, value) {
|
|
33
|
-
try {
|
|
34
|
-
if (usable) {
|
|
35
|
-
window.localStorage.setItem(key, value);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
} catch { /* 降级 */ }
|
|
39
|
-
memory.set(key, value);
|
|
40
|
-
},
|
|
41
|
-
remove(key) {
|
|
42
|
-
try {
|
|
43
|
-
if (usable) {
|
|
44
|
-
window.localStorage.removeItem(key);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
} catch { /* 降级 */ }
|
|
48
|
-
memory.delete(key);
|
|
49
|
-
},
|
|
50
|
-
/** localStorage 是否真的可用(false 表示已降级到内存) */
|
|
51
|
-
persistent: usable,
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export default safeStorage;
|