xhs-minitool-creator 1.0.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 +48 -56
- package/bin/create.mjs +21 -32
- package/package.json +1 -1
- package/src/pack.mjs +39 -21
- package/src/vite-preset.mjs +1 -1
- package/template/_gitignore +1 -1
- 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,88 +1,80 @@
|
|
|
1
1
|
# xhs-minitool-creator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
小红书「小工具」项目脚手架:创建、构建、打包、合规校验,一条命令完成。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- **默认即合规**:Vite 预设内置 `build.target: ['es2017', 'chrome61']`,从源头避免 ES2020 语法泄漏导致的真机白屏
|
|
7
|
-
- **以官方规范为准**:内置官方打包规范 Skill(`minitool-zip-builder`)v1.6.0,校验规则与之一致
|
|
5
|
+
你不需要先啃小红书小工具的平台规范,脚手架已把平台约束内置到构建配置与校验规则中。用它产出的 zip 直接上传创作服务平台即可。
|
|
8
6
|
|
|
9
7
|
## 快速开始
|
|
10
8
|
|
|
11
9
|
```bash
|
|
12
|
-
|
|
13
|
-
npx xhs-minitool-creator my-tool
|
|
14
|
-
|
|
15
|
-
# 非交互式
|
|
16
|
-
npx xhs-minitool-creator my-tool --yes --title "我的工具" --theme "#ff2442"
|
|
17
|
-
|
|
10
|
+
npx xhs-minitool-creator my-tool -y
|
|
18
11
|
cd my-tool
|
|
19
12
|
npm install
|
|
20
|
-
npm run
|
|
21
|
-
npm run build # 构建 + 打包,输出 my-tool-xhs-tool.zip
|
|
22
|
-
npm run validate # 静态校验
|
|
13
|
+
npm run build # 产出 my-tool-xhs-minitool.zip,上传创作服务平台即可
|
|
23
14
|
```
|
|
24
15
|
|
|
25
|
-
|
|
16
|
+
交互式创建(自定义标题 / 描述):
|
|
26
17
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
xhs-minitool-creator <name> 创建新项目(默认子命令)
|
|
31
|
-
xhs-minitool-creator create <name> [选项] 同上
|
|
32
|
-
xhs-minitool-creator pack [选项] 整理产物并打包
|
|
33
|
-
xhs-minitool-creator validate <目录|zip> 静态校验
|
|
34
|
-
xhs-minitool-creator skill 查看内置官方 Skill 版本
|
|
35
|
-
xhs-minitool-creator skill update --url <地址> 手动更新官方 Skill
|
|
18
|
+
```bash
|
|
19
|
+
npx xhs-minitool-creator my-tool
|
|
36
20
|
```
|
|
37
21
|
|
|
38
|
-
|
|
22
|
+
## 命令
|
|
39
23
|
|
|
40
|
-
|
|
|
24
|
+
| 命令 | 作用 |
|
|
41
25
|
| --- | --- |
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
| `--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` | 查看项目内置官方规范版本 |
|
|
47
30
|
|
|
48
|
-
|
|
31
|
+
CLI 子命令 `create / pack / validate / skill`,详见 `npx xhs-minitool-creator --help`。
|
|
49
32
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
33
|
+
create 选项:`--yes`(跳过交互)、`--title`、`--desc`。
|
|
34
|
+
|
|
35
|
+
## 生成的项目
|
|
36
|
+
|
|
37
|
+
- Vite 项目,构建预设已内置平台约束,输出目录 `xhs-minitool/`
|
|
38
|
+
- 示例页面:单页多视图切换、适配平台原生导航栏与安全区
|
|
39
|
+
- 官方打包规范 `minitool-zip-builder` 快照(`.agents/skills/`),可直接供 AI 参考辅助开发
|
|
55
40
|
|
|
56
|
-
##
|
|
41
|
+
## 平台约束速览
|
|
57
42
|
|
|
58
|
-
|
|
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`)
|
|
59
52
|
|
|
60
|
-
|
|
61
|
-
2. 校验依赖 Python,且规则停留在旧版规范
|
|
62
|
-
3. Vite 预设未设置 `build.target`,ES2020 语法(`??`)会泄漏进产物,在低端 WebView 上白屏且无报错
|
|
63
|
-
4. 内置的官方规范快照会随官方演进而过时
|
|
53
|
+
`npm run validate` 内置上述规则的静态扫描;静态检查无法覆盖运行时行为,交付前务必真机预览。
|
|
64
54
|
|
|
65
|
-
|
|
55
|
+
## 更新官方规范
|
|
66
56
|
|
|
67
|
-
|
|
68
|
-
2. 校验为纯 Node 静态扫描,规则对齐官方 Skill v1.6.0(含 10 MiB 硬上限、经典脚本约束、ES2017 基线)
|
|
69
|
-
3. 预设显式声明 `build.target: ['es2017', 'chrome61']`
|
|
70
|
-
4. 提供 `skill` 子命令查看版本并手动更新
|
|
57
|
+
官方规范经小红书 CDN 分发,**无稳定 latest 端点**(URL 含时间戳),需手动更新:
|
|
71
58
|
|
|
72
|
-
|
|
59
|
+
```bash
|
|
60
|
+
npx xhs-minitool-creator skill update --url <官方 .skill 包地址>
|
|
61
|
+
```
|
|
73
62
|
|
|
74
|
-
|
|
63
|
+
## 编程式使用
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
import { defineMinitoolConfig } from 'xhs-minitool-creator/vite';
|
|
67
|
+
import { pack, validateDir } from 'xhs-minitool-creator';
|
|
68
|
+
```
|
|
75
69
|
|
|
76
|
-
|
|
77
|
-
2. 执行 `xhs-minitool-creator skill update --url <地址>`
|
|
70
|
+
## 打包器说明
|
|
78
71
|
|
|
79
|
-
|
|
72
|
+
打包优先使用系统 `zip` 命令(macOS 自带),未安装则回退内置纯 Node 实现,两条路径产物一致;校验与 zip 读写零系统依赖(Windows / Linux 均可用)。
|
|
80
73
|
|
|
81
|
-
##
|
|
74
|
+
## 说明
|
|
82
75
|
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
- 本脚手架与小红书官方无隶属关系,`minitool-zip-builder` Skill 版权归小红书所有
|
|
76
|
+
- 本工具与小红书官方无隶属关系,内置规范内容版权归小红书
|
|
77
|
+
- 平台规则可能调整,关键决策以创作服务平台实时规则为准
|
|
86
78
|
|
|
87
79
|
## License
|
|
88
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,12 +36,10 @@ create 选项:
|
|
|
36
36
|
-y, --yes 跳过交互,全部使用默认值
|
|
37
37
|
--title <标题> 页面标题(默认同项目名)
|
|
38
38
|
--desc <描述> 项目描述
|
|
39
|
-
--icon <字符> 首页图标字符(默认「工」)
|
|
40
|
-
--theme <颜色> 主题色(默认 #ff2442)
|
|
41
39
|
|
|
42
40
|
pack 选项:
|
|
43
|
-
--outDir <目录> 构建产物目录(默认 xhs-
|
|
44
|
-
--zipName <文件名> 输出 zip 名(默认 <包名>-xhs-
|
|
41
|
+
--outDir <目录> 构建产物目录(默认 xhs-minitool)
|
|
42
|
+
--zipName <文件名> 输出 zip 名(默认 <包名>-xhs-minitool.zip)
|
|
45
43
|
--strict 警告也视为失败
|
|
46
44
|
|
|
47
45
|
其他:
|
|
@@ -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
|
}
|
|
@@ -215,7 +204,7 @@ async function doCreate(name, vars) {
|
|
|
215
204
|
console.log(` cd ${name}`);
|
|
216
205
|
console.log(' npm install');
|
|
217
206
|
console.log(' npm run dev # 本地预览');
|
|
218
|
-
console.log(' npm run build # 构建 + 打包(输出 <项目名>-xhs-
|
|
207
|
+
console.log(' npm run build # 构建 + 打包(输出 <项目名>-xhs-minitool.zip)');
|
|
219
208
|
console.log(' npm run validate # 静态校验');
|
|
220
209
|
console.log('\n上传入口:小红书创作服务平台(以平台实时规则为准)');
|
|
221
210
|
}
|
|
@@ -224,7 +213,7 @@ async function doCreate(name, vars) {
|
|
|
224
213
|
|
|
225
214
|
function cmdPack(flags) {
|
|
226
215
|
const code = pack({
|
|
227
|
-
outDir: flags.outDir || 'xhs-
|
|
216
|
+
outDir: flags.outDir || 'xhs-minitool',
|
|
228
217
|
zipName: flags.zipName,
|
|
229
218
|
strict: flags.strict !== undefined ? flags.strict : true,
|
|
230
219
|
});
|
|
@@ -234,7 +223,7 @@ function cmdPack(flags) {
|
|
|
234
223
|
function cmdValidate(args, flags) {
|
|
235
224
|
const target = args._[0];
|
|
236
225
|
if (!target) {
|
|
237
|
-
fail('请指定要校验的目录或 zip,例如:xhs-minitool-creator validate ./xhs-
|
|
226
|
+
fail('请指定要校验的目录或 zip,例如:xhs-minitool-creator validate ./xhs-minitool');
|
|
238
227
|
}
|
|
239
228
|
const strict = flags.strict !== undefined ? flags.strict : true;
|
|
240
229
|
|
package/package.json
CHANGED
package/src/pack.mjs
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
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
|
+
import { spawnSync } from 'node:child_process';
|
|
15
16
|
import { createZip, listZipEntries } from './zip.mjs';
|
|
16
17
|
import { ALLOWED_EXT, validateDir, validateZip, formatIssues } from './validate.mjs';
|
|
17
18
|
|
|
@@ -88,17 +89,6 @@ export function patchIndexHtml(outDir) {
|
|
|
88
89
|
return scripts.length;
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
function copyIcons(outDir, publicDir) {
|
|
92
|
-
for (const icon of ['icon-192.svg', 'icon-512.svg']) {
|
|
93
|
-
const dest = join(outDir, 'icons', icon);
|
|
94
|
-
const src = join(publicDir, 'icons', icon);
|
|
95
|
-
if (!existsSync(dest) && existsSync(src)) {
|
|
96
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
97
|
-
copyFileSync(src, dest);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
92
|
function readPackageName(cwd) {
|
|
103
93
|
const pkgPath = join(cwd, 'package.json');
|
|
104
94
|
if (!existsSync(pkgPath)) return 'minitool';
|
|
@@ -109,25 +99,54 @@ function readPackageName(cwd) {
|
|
|
109
99
|
}
|
|
110
100
|
}
|
|
111
101
|
|
|
102
|
+
/** 系统 zip 命令是否可用(macOS 自带;多数 Linux 需自行安装;Windows 无) */
|
|
103
|
+
function systemZipAvailable() {
|
|
104
|
+
const probe = spawnSync('zip', ['-v'], { stdio: 'ignore' });
|
|
105
|
+
return !probe.error && probe.status === 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 打 zip:优先使用系统 zip 命令(行为与平台工具一致),
|
|
110
|
+
* 不可用时回退到纯 Node 实现(零依赖,跨平台兜底)。
|
|
111
|
+
* 两种路径产物都会被自实现读取器校验结构。
|
|
112
|
+
*/
|
|
113
|
+
function makeZip(outDir, zipPath, entries) {
|
|
114
|
+
if (systemZipAvailable()) {
|
|
115
|
+
if (existsSync(zipPath)) rmSync(zipPath, { force: true });
|
|
116
|
+
const result = spawnSync('zip', ['-r', zipPath, '.', '-x', '*.DS_Store'], {
|
|
117
|
+
cwd: outDir,
|
|
118
|
+
encoding: 'utf8',
|
|
119
|
+
});
|
|
120
|
+
if (result.status !== 0) {
|
|
121
|
+
throw new Error(`系统 zip 打包失败:${result.stderr || result.stdout || 'unknown'}`);
|
|
122
|
+
}
|
|
123
|
+
console.log(' · 使用系统 zip 命令打包');
|
|
124
|
+
return readFileSync(zipPath);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log(' · 未检测到系统 zip,使用内置打包器');
|
|
128
|
+
const zipBuf = createZip(entries);
|
|
129
|
+
writeFileSync(zipPath, zipBuf);
|
|
130
|
+
return zipBuf;
|
|
131
|
+
}
|
|
132
|
+
|
|
112
133
|
/**
|
|
113
134
|
* @param {{
|
|
114
|
-
* cwd?: string, outDir?: string, zipName?: string,
|
|
115
|
-
*
|
|
135
|
+
* cwd?: string, outDir?: string, zipName?: string,
|
|
136
|
+
* strict?: boolean
|
|
116
137
|
* }} [options]
|
|
117
138
|
*/
|
|
118
139
|
export function pack(options = {}) {
|
|
119
140
|
const cwd = resolve(options.cwd || process.cwd());
|
|
120
|
-
const outDir = resolve(cwd, options.outDir || 'xhs-
|
|
121
|
-
const zipName = options.zipName || `${readPackageName(cwd)}-xhs-
|
|
141
|
+
const outDir = resolve(cwd, options.outDir || 'xhs-minitool');
|
|
142
|
+
const zipName = options.zipName || `${readPackageName(cwd)}-xhs-minitool.zip`;
|
|
122
143
|
const zipPath = resolve(cwd, zipName);
|
|
123
|
-
const publicDir = resolve(cwd, options.publicDir || 'public');
|
|
124
144
|
|
|
125
145
|
if (!existsSync(outDir)) {
|
|
126
146
|
throw new Error(`缺少输出目录 ${relative(cwd, outDir)},请先执行构建`);
|
|
127
147
|
}
|
|
128
148
|
|
|
129
149
|
console.log('\n▸ 整理产物');
|
|
130
|
-
copyIcons(outDir, publicDir);
|
|
131
150
|
removeUnsupported(outDir);
|
|
132
151
|
pruneEmptyDirs(outDir);
|
|
133
152
|
const scriptCount = patchIndexHtml(outDir);
|
|
@@ -144,8 +163,7 @@ export function pack(options = {}) {
|
|
|
144
163
|
throw new Error('产物根目录缺少 index.html');
|
|
145
164
|
}
|
|
146
165
|
|
|
147
|
-
const zipBuf =
|
|
148
|
-
writeFileSync(zipPath, zipBuf);
|
|
166
|
+
const zipBuf = makeZip(outDir, zipPath, entries);
|
|
149
167
|
|
|
150
168
|
// 自检:确认 zip 结构正确(根含 index.html、无禁止文件)
|
|
151
169
|
const listed = listZipEntries(zipBuf).map((e) => e.name);
|
package/src/vite-preset.mjs
CHANGED
package/template/_gitignore
CHANGED
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;
|