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 CHANGED
@@ -1,88 +1,80 @@
1
1
  # xhs-minitool-creator
2
2
 
3
- 小红书「小工具」脚手架:一键创建合规项目、构建、打包、静态校验,内置官方打包规范 Skill。
3
+ 小红书「小工具」项目脚手架:创建、构建、打包、合规校验,一条命令完成。
4
4
 
5
- - **零系统依赖**:ZIP 用纯 Node 实现,校验也是纯 Node —— 不再依赖系统 `zip` / `zipinfo` / Python,Windows / macOS / Linux 均可使用
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 dev # 本地预览
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
- `my-tool-xhs-tool.zip` 上传至小红书创作服务平台即可。
16
+ 交互式创建(自定义标题 / 描述):
26
17
 
27
- ## CLI 命令
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
- ### create 选项
22
+ ## 命令
39
23
 
40
- | 选项 | 说明 |
24
+ | 命令 | 作用 |
41
25
  | --- | --- |
42
- | `-y, --yes` | 跳过交互,全部使用默认值 |
43
- | `--title <标题>` | 页面标题(默认同项目名) |
44
- | `--desc <描述>` | 项目描述 |
45
- | `--icon <字符>` | 首页图标字符(默认「工」) |
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
- ```js
51
- import { defineMinitoolConfig } from 'xhs-minitool-creator/vite';
52
- import { pack } from 'xhs-minitool-creator';
53
- import { validateDir } from 'xhs-minitool-creator/validate';
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
- 参考实现(`create-xhs-minitool`)存在几个长期风险:
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
- 1. 依赖系统 `zip` / `zipinfo` 二进制,Windows 不可用
61
- 2. 校验依赖 Python,且规则停留在旧版规范
62
- 3. Vite 预设未设置 `build.target`,ES2020 语法(`??`)会泄漏进产物,在低端 WebView 上白屏且无报错
63
- 4. 内置的官方规范快照会随官方演进而过时
53
+ `npm run validate` 内置上述规则的静态扫描;静态检查无法覆盖运行时行为,交付前务必真机预览。
64
54
 
65
- 本脚手架的对应方案:
55
+ ## 更新官方规范
66
56
 
67
- 1. ZIP 写入用 Node `zlib.deflateRawSync` 自实现,交叉验证过 macOS `unzip` 与 Python `zipfile`
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
- ## 官方规范 Skill 的更新
59
+ ```bash
60
+ npx xhs-minitool-creator skill update --url <官方 .skill 包地址>
61
+ ```
73
62
 
74
- 官方 CDN **没有稳定的 latest 端点**(URL 内含不可预测的时间戳),因此无法自动发现新版本。做法:
63
+ ## 编程式使用
64
+
65
+ ```js
66
+ import { defineMinitoolConfig } from 'xhs-minitool-creator/vite';
67
+ import { pack, validateDir } from 'xhs-minitool-creator';
68
+ ```
75
69
 
76
- 1. 从官方文档取得最新 `.skill` 地址
77
- 2. 执行 `xhs-minitool-creator skill update --url <地址>`
70
+ ## 打包器说明
78
71
 
79
- 校验依据随 Skill 更新而变化时,请同时关注本包的新版本发布。
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-tool
44
- --zipName <文件名> 输出 zip 名(默认 <包名>-xhs-tool.zip)
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 === '--icon' || a === '--theme' ||
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
- let input = name;
164
- while (!input || !validName(input)) {
165
- input = await prompt(
166
- '项目名(小写字母/数字/连字符,如 lucky-draw):',
167
- name || '',
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(`页面标题(默认「${name}」):`, name);
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, icon, theme };
181
- console.log(`\n将创建:${name}(标题「${title}」,主题色 ${theme})`);
182
- const ok = await prompt('确认?(Y/n) :', 'Y');
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-tool.zip)');
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-tool',
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-tool');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xhs-minitool-creator",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "小红书小工具脚手架:创建项目 / Vite 构建预设 / 打包 / 静态校验,内置官方打包规范 Skill",
5
5
  "type": "module",
6
6
  "bin": {
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 { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
14
- import { dirname, extname, join, relative, resolve } from 'node:path';
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, publicDir?: string,
115
- * skipValidate?: boolean, strict?: boolean
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-tool');
121
- const zipName = options.zipName || `${readPackageName(cwd)}-xhs-tool.zip`;
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 = createZip(entries);
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);
@@ -14,7 +14,7 @@ export function defineMinitoolConfig(overrides = {}) {
14
14
  return {
15
15
  base: './',
16
16
  build: {
17
- outDir: 'xhs-tool',
17
+ outDir: 'xhs-minitool',
18
18
  emptyOutDir: true,
19
19
  assetsInlineLimit: 0,
20
20
  cssCodeSplit: false,
@@ -1,6 +1,6 @@
1
1
  node_modules/
2
2
  dist/
3
- xhs-tool/
3
+ xhs-minitool/
4
4
  *.zip
5
5
  .DS_Store
6
6
  *.log
@@ -16,7 +16,7 @@
16
16
  -->
17
17
  <section class="page is-active" data-page="home">
18
18
  <header class="hero">
19
- <div class="hero-icon">{{ICON}}</div>
19
+ <div class="hero-icon">😊</div>
20
20
  <h1 class="hero-title">{{TITLE}}</h1>
21
21
  <p class="hero-desc">{{DESC}}</p>
22
22
  </header>
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "devDependencies": {
16
16
  "vite": "^6.3.5",
17
- "xhs-minitool-creator": "^1.0.0"
17
+ "xhs-minitool-creator": "^1.2.0"
18
18
  },
19
19
  "engines": {
20
20
  "node": ">=18.17.0"
@@ -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 绑定(容器禁止 onclick 等行内事件)
8
- * 2. 本地存储用 safeStorage 包装(个别环境 localStorage 会抛错)
9
- * 3. 多视图在单页内切换(容器不支持打开新页面)
6
+ * 1. 事件用 addEventListener 绑定(容器 CSP 禁止 onclick 等行内事件)
7
+ * 2. 多视图在单页内切换(容器不支持打开新页面)
8
+ * 3. 提示用页内 toast 元素实现(容器不支持系统弹窗)
10
9
  */
11
10
 
12
- const THEME_KEY = 'demo_theme';
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(Number(safeStorage.get(COUNT_KEY) || 0));
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
- safeStorage.set('demo_name', name);
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}!这是第 ${next} 次使用。`;
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
- safeStorage.remove(COUNT_KEY);
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 = safeStorage.get(THEME_KEY) || (dark ? 'dark' : 'light');
63
+ document.body.dataset.theme = dark ? 'dark' : 'light';
78
64
  }
79
65
 
80
66
  // 经典脚本环境下 DOM 已就绪(脚本在 body 末尾),直接初始化即可
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  :root {
9
- --accent: #ff2442; /* 由脚手架按主题色注入 */
9
+ --accent: #ff2442; /* 主题色,改成你喜欢的颜色即可 */
10
10
  --bg: #f6f6f8;
11
11
  --card: #ffffff;
12
12
  --text: #1a1a1e;
@@ -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;