@x-withu/page-withu 0.1.2 → 1.0.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
@@ -41,18 +41,18 @@
41
41
  # 安装 page-withu
42
42
  npm install -g @x-withu/page-withu
43
43
 
44
- # 创建项目(会依次询问站点标题、作者名、标签页标题、favicon、GitHub 链接和邮箱)
45
- page-withu create my-homepage
46
-
47
- # 也可以通过参数直接指定
48
- page-withu create my-homepage --title "My Site" --tab-title "My Blog" --favicon /favicon.svg
44
+ # 创建项目,默认配置会写入 config.js,后续可自行编辑
45
+ page-withu new my-homepage
49
46
 
50
47
  # 安装依赖
51
48
  cd my-homepage
52
49
  npm install
53
50
 
54
- # 预览网站
55
- npm run dev
51
+ # 本地预览
52
+ page-withu serve
53
+
54
+ # 构建分发产物
55
+ page-withu build
56
56
  ```
57
57
 
58
58
  下一步?[配置网站](./docs/setup.md)、[部署网站](./docs/deploy.md)、[开发网站](./docs/develop.md)。
package/bin/page-withu.js CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { createHash } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
3
  import { Command } from 'commander';
4
- import inquirer from 'inquirer';
5
4
  import fs from 'fs-extra';
6
5
  import path from 'node:path';
7
6
  import { fileURLToPath } from 'node:url';
@@ -13,117 +12,8 @@ const pkg = await fs.readJson(new URL('../package.json', import.meta.url));
13
12
  const __filename = fileURLToPath(import.meta.url);
14
13
  const __dirname = path.dirname(__filename);
15
14
  const templateDir = path.resolve(__dirname, '../template');
16
- const manifestName = '.page-withu.json';
17
- const managedRoots = ['src'];
18
- const managedFiles = ['index.html', 'vite.config.js'];
19
- const generatedGitignore = `node_modules/
20
- dist/
21
-
22
- .env
23
- .env.*
24
- !.env.example
25
- *.local
26
-
27
- .DS_Store
28
- .idea/
29
- `;
30
-
31
15
  const program = new Command();
32
16
 
33
- function hashContent(content) {
34
- return createHash('sha256').update(content).digest('hex');
35
- }
36
-
37
- async function hashFile(filePath) {
38
- return hashContent(await fs.readFile(filePath));
39
- }
40
-
41
- function toPosixPath(value) {
42
- return value.split(path.sep).join('/');
43
- }
44
-
45
- async function listFiles(dir) {
46
- const entries = await fs.readdir(dir, { withFileTypes: true });
47
- const files = [];
48
-
49
- for (const entry of entries) {
50
- const entryPath = path.join(dir, entry.name);
51
- if (entry.isDirectory()) {
52
- files.push(...await listFiles(entryPath));
53
- } else if (entry.isFile()) {
54
- files.push(entryPath);
55
- }
56
- }
57
-
58
- return files;
59
- }
60
-
61
- async function listManagedTemplateFiles() {
62
- const files = [];
63
-
64
- for (const file of managedFiles) {
65
- if (await fs.pathExists(path.join(templateDir, file))) files.push(file);
66
- }
67
-
68
- for (const root of managedRoots) {
69
- const rootPath = path.join(templateDir, root);
70
- if (!await fs.pathExists(rootPath)) continue;
71
- const rootFiles = await listFiles(rootPath);
72
- files.push(...rootFiles.map((file) => toPosixPath(path.relative(templateDir, file))));
73
- }
74
-
75
- return files.sort((a, b) => a.localeCompare(b));
76
- }
77
-
78
- async function createManifest(projectDir, previousManifest = null) {
79
- const files = {};
80
-
81
- for (const file of await listManagedTemplateFiles()) {
82
- const projectFile = path.join(projectDir, file);
83
- const templateFile = path.join(templateDir, file);
84
- if (!await fs.pathExists(projectFile)) continue;
85
-
86
- const projectHash = await hashFile(projectFile);
87
- const templateHash = await hashFile(templateFile);
88
- if (projectHash === templateHash) {
89
- files[file] = templateHash;
90
- } else if (previousManifest?.files?.[file]) {
91
- files[file] = previousManifest.files[file];
92
- }
93
- }
94
-
95
- return {
96
- templateVersion: pkg.version,
97
- updatedAt: new Date().toISOString(),
98
- files,
99
- };
100
- }
101
-
102
- async function writeManifest(projectDir, dryRun, previousManifest = null) {
103
- const manifest = await createManifest(projectDir, previousManifest);
104
- if (!dryRun) {
105
- await fs.writeJson(path.join(projectDir, manifestName), manifest, { spaces: 2 });
106
- }
107
- return manifest;
108
- }
109
-
110
- function renderConfig({ title, tabTitle, favicon, author, footerLinks }) {
111
- const links = footerLinks.map((link) => ` { label: ${JSON.stringify(link.label)}, url: ${JSON.stringify(link.url)} }`);
112
- return `export default {
113
- title: ${JSON.stringify(title)},
114
- tabTitle: ${JSON.stringify(tabTitle)},
115
- favicon: ${JSON.stringify(favicon)},
116
- author: ${JSON.stringify(author)},
117
- year: new Date().getFullYear(),
118
- footerLinks: [
119
- ${links.join(',\n')}
120
- ],
121
- pagination: {
122
- pageSize: 5
123
- }
124
- }`;
125
- }
126
-
127
17
  async function detectProject(projectDir) {
128
18
  const required = ['package.json', 'config.js', 'content', 'src', 'vite.config.js'];
129
19
  const missing = [];
@@ -135,277 +25,91 @@ async function detectProject(projectDir) {
135
25
  return missing;
136
26
  }
137
27
 
138
- async function loadManifest(projectDir) {
139
- const manifestPath = path.join(projectDir, manifestName);
140
- if (!await fs.pathExists(manifestPath)) return null;
141
- return fs.readJson(manifestPath);
28
+ function shouldCopyTemplateItem(source) {
29
+ return path.relative(templateDir, source) !== '.gitignore';
142
30
  }
143
31
 
144
- async function ensureGeneratedGitignore(projectDir, dryRun) {
145
- const gitignorePath = path.join(projectDir, '.gitignore');
146
- const npmignorePath = path.join(projectDir, '.npmignore');
147
- const hasGitignore = await fs.pathExists(gitignorePath);
148
- const hasNpmignore = await fs.pathExists(npmignorePath);
149
-
150
- if (dryRun) return !hasGitignore || hasNpmignore;
151
- if (hasNpmignore) await fs.remove(npmignorePath);
152
- if (!hasGitignore) await fs.writeFile(gitignorePath, generatedGitignore);
153
-
154
- return !hasGitignore || hasNpmignore;
32
+ function normalizePackageName(name) {
33
+ return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/^-+|-+$/g, '') || 'page-withu-site';
155
34
  }
156
35
 
157
- async function shouldOverwriteConflict(file, options) {
158
- if (options.force) return true;
159
- if (options.yes || options.dryRun) return false;
160
-
161
- const answer = await inquirer.prompt([{
162
- type: 'confirm',
163
- name: 'overwrite',
164
- message: `${file} has local changes. Overwrite it?`,
165
- default: false,
166
- }]);
167
-
168
- return answer.overwrite;
169
- }
36
+ async function createProject(projectName = 'my-homepage') {
37
+ const targetDir = projectName.trim() || 'my-homepage';
38
+ const fullPath = path.resolve(process.cwd(), targetDir);
170
39
 
171
- async function planTemplateUpdates(projectDir, options) {
172
- const manifest = await loadManifest(projectDir);
173
- const files = await listManagedTemplateFiles();
174
- const result = {
175
- added: [],
176
- updated: [],
177
- skipped: [],
178
- unchanged: [],
179
- };
180
-
181
- for (const file of files) {
182
- const sourceFile = path.join(templateDir, file);
183
- const targetFile = path.join(projectDir, file);
184
- const sourceHash = await hashFile(sourceFile);
185
- const exists = await fs.pathExists(targetFile);
186
-
187
- if (!exists) {
188
- result.added.push(file);
189
- if (!options.dryRun) await fs.copy(sourceFile, targetFile);
190
- continue;
191
- }
192
-
193
- const currentHash = await hashFile(targetFile);
194
- if (currentHash === sourceHash) {
195
- result.unchanged.push(file);
196
- continue;
197
- }
40
+ if (await fs.pathExists(fullPath)) {
41
+ console.error(chalk.red(`\nError: Directory ${targetDir} already exists.`));
42
+ process.exit(1);
43
+ }
198
44
 
199
- const previousHash = manifest?.files?.[file];
200
- const safeToUpdate = previousHash && currentHash === previousHash;
201
- const overwrite = safeToUpdate || await shouldOverwriteConflict(file, options);
45
+ const spinner = ora('Creating project...').start();
202
46
 
203
- if (overwrite) {
204
- result.updated.push(file);
205
- if (!options.dryRun) await fs.copy(sourceFile, targetFile);
206
- } else {
207
- result.skipped.push(file);
208
- }
209
- }
47
+ try {
48
+ await fs.copy(templateDir, fullPath, { filter: shouldCopyTemplateItem });
210
49
 
211
- return result;
212
- }
50
+ const pkgPath = path.join(fullPath, 'package.json');
51
+ const projectPkg = await fs.readJson(pkgPath);
52
+ projectPkg.name = normalizePackageName(path.basename(targetDir));
53
+ await fs.writeJson(pkgPath, projectPkg, { spaces: 2 });
213
54
 
214
- function mergeSection(projectPkg, templatePkg, key) {
215
- const next = { ...(projectPkg[key] || {}) };
216
- let changed = false;
55
+ await fs.ensureDir(path.join(fullPath, 'content/blog'));
217
56
 
218
- for (const [name, value] of Object.entries(templatePkg[key] || {})) {
219
- if (next[name] !== value) {
220
- next[name] = value;
221
- changed = true;
222
- }
57
+ spinner.succeed(chalk.green(`Successfully created PageWithU project in ${targetDir}!`));
58
+ console.log('\nNext steps:');
59
+ console.log(chalk.cyan(` cd ${targetDir}`));
60
+ console.log(chalk.cyan(' npm install'));
61
+ console.log(chalk.cyan(' page-withu serve'));
62
+ console.log(chalk.cyan(' page-withu build\n'));
63
+ } catch (err) {
64
+ spinner.fail(chalk.red('Failed to create project.'));
65
+ console.error(err);
66
+ process.exit(1);
223
67
  }
224
-
225
- if (changed) projectPkg[key] = next;
226
- return changed;
227
68
  }
228
69
 
229
- async function mergePackage(projectDir, dryRun) {
230
- const projectPkgPath = path.join(projectDir, 'package.json');
231
- const templatePkg = await fs.readJson(path.join(templateDir, 'package.json'));
232
- const projectPkg = await fs.readJson(projectPkgPath);
233
- const before = JSON.stringify(projectPkg);
234
-
235
- mergeSection(projectPkg, templatePkg, 'scripts');
236
- mergeSection(projectPkg, templatePkg, 'dependencies');
237
- mergeSection(projectPkg, templatePkg, 'devDependencies');
238
-
239
- const changed = JSON.stringify(projectPkg) !== before;
240
- if (changed && !dryRun) {
241
- await fs.writeJson(projectPkgPath, projectPkg, { spaces: 2 });
70
+ async function runProjectScript(script, args) {
71
+ const missing = await detectProject(process.cwd());
72
+ if (missing.length) {
73
+ console.error(chalk.red('Current directory does not look like a PageWithU project.'));
74
+ console.error(chalk.yellow(`Missing: ${missing.join(', ')}`));
75
+ process.exit(1);
242
76
  }
243
77
 
244
- return changed;
245
- }
246
-
247
- function printUpdateSummary(summary, dryRun) {
248
- const label = dryRun ? 'Would update' : 'Updated';
78
+ const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
79
+ const child = spawn(npmCommand, ['run', script, '--', ...args], {
80
+ cwd: process.cwd(),
81
+ stdio: 'inherit',
82
+ });
249
83
 
250
- if (summary.added.length) console.log(chalk.green(`${label} ${summary.added.length} missing file(s).`));
251
- if (summary.updated.length) console.log(chalk.green(`${label} ${summary.updated.length} template file(s).`));
252
- if (summary.packageChanged) console.log(chalk.green(`${label} package.json dependencies and scripts.`));
253
- if (summary.gitignoreChanged) console.log(chalk.green(`${label} .gitignore.`));
254
- if (summary.skipped.length) {
255
- console.log(chalk.yellow(`Skipped ${summary.skipped.length} file(s) with local changes:`));
256
- for (const file of summary.skipped) console.log(chalk.yellow(` - ${file}`));
257
- }
258
- if (!summary.added.length && !summary.updated.length && !summary.packageChanged && !summary.gitignoreChanged && !summary.skipped.length) {
259
- console.log(chalk.green('Page With U project is already up to date.'));
260
- }
261
- if (!dryRun && (summary.added.length || summary.updated.length || summary.packageChanged || summary.gitignoreChanged)) {
262
- console.log(chalk.cyan('\nNext steps:'));
263
- console.log(chalk.cyan(' npm install'));
264
- console.log(chalk.cyan(' npm run build'));
265
- }
84
+ child.on('exit', (code, signal) => {
85
+ if (signal) {
86
+ process.kill(process.pid, signal);
87
+ return;
88
+ }
89
+ process.exit(code ?? 1);
90
+ });
266
91
  }
267
92
 
268
93
  program
269
94
  .name('page-withu')
270
- .description('CLI to scaffold a lightweight, elegant personal homepage')
95
+ .description('CLI to scaffold, preview, and build a lightweight personal homepage')
271
96
  .version(pkg.version);
272
97
 
273
98
  program
274
- .command('create [project-name]')
275
- .description('create a new PageWithU project')
276
- .option('-t, --title <title>', 'site title')
277
- .option('--tab-title <title>', 'browser tab title')
278
- .option('--favicon <path-or-url>', 'browser tab favicon path or URL')
279
- .option('-a, --author <author>', 'author name')
280
- .option('-g, --github <github>', 'github url')
281
- .option('-e, --email <email>', 'email address')
282
- .action(async (projectName, options) => {
283
- let targetDir = projectName;
284
-
285
- if (!targetDir) {
286
- const answers = await inquirer.prompt([
287
- {
288
- type: 'input',
289
- name: 'projectName',
290
- message: 'What is your project named?',
291
- default: 'my-homepage',
292
- },
293
- ]);
294
- targetDir = answers.projectName;
295
- }
296
-
297
- const fullPath = path.resolve(process.cwd(), targetDir);
298
-
299
- if (fs.existsSync(fullPath)) {
300
- console.error(chalk.red(`\nError: Directory ${targetDir} already exists.`));
301
- process.exit(1);
302
- }
303
-
304
- const title = options.title || (await inquirer.prompt([{
305
- type: 'input',
306
- name: 'title',
307
- message: 'Site Title:',
308
- default: 'PageWithU',
309
- }])).title;
310
-
311
- const author = options.author || (await inquirer.prompt([{
312
- type: 'input',
313
- name: 'author',
314
- message: 'Author Name:',
315
- default: 'Your Name',
316
- }])).author;
317
-
318
- const tabTitle = options.tabTitle || (await inquirer.prompt([{
319
- type: 'input',
320
- name: 'tabTitle',
321
- message: 'Browser Tab Title:',
322
- default: title,
323
- }])).tabTitle;
324
-
325
- const favicon = options.favicon || (await inquirer.prompt([{
326
- type: 'input',
327
- name: 'favicon',
328
- message: 'Favicon path or URL:',
329
- default: '/src/assets/bulb.svg',
330
- }])).favicon;
331
-
332
- const github = options.github !== undefined ? options.github : (await inquirer.prompt([{
333
- type: 'input',
334
- name: 'github',
335
- message: 'GitHub URL (optional):',
336
- }])).github;
337
-
338
- const email = options.email !== undefined ? options.email : (await inquirer.prompt([{
339
- type: 'input',
340
- name: 'email',
341
- message: 'Email (optional):',
342
- }])).email;
343
-
344
- const spinner = ora('Creating project...').start();
345
-
346
- try {
347
- await fs.copy(templateDir, fullPath);
348
-
349
- const footerLinks = [];
350
- if (github) footerLinks.push({ label: 'GitHub', url: github });
351
- if (email) footerLinks.push({ label: 'Email', url: `mailto:${email}` });
352
-
353
- await fs.writeFile(path.join(fullPath, 'config.js'), renderConfig({ title, tabTitle, favicon, author, footerLinks }));
354
-
355
- const pkgPath = path.join(fullPath, 'package.json');
356
- const projectPkg = await fs.readJson(pkgPath);
357
- projectPkg.name = targetDir.toLowerCase().replace(/[^a-z0-9-]/g, '-');
358
- await fs.writeJson(pkgPath, projectPkg, { spaces: 2 });
359
-
360
- await fs.ensureDir(path.join(fullPath, 'content/blog'));
361
- await ensureGeneratedGitignore(fullPath, false);
362
- await writeManifest(fullPath, false);
363
-
364
- spinner.succeed(chalk.green(`Successfully created PageWithU project in ${targetDir}!`));
365
- console.log('\nNext steps:');
366
- console.log(chalk.cyan(` cd ${targetDir}`));
367
- console.log(chalk.cyan(' npm install'));
368
- console.log(chalk.cyan(' npm run dev\n'));
369
-
370
- } catch (err) {
371
- spinner.fail(chalk.red('Failed to create project.'));
372
- console.error(err);
373
- process.exit(1);
374
- }
375
- });
99
+ .command('new [project-name]')
100
+ .description('create a new PageWithU project with the default template config')
101
+ .action(createProject);
376
102
 
377
103
  program
378
- .command('update')
379
- .description('update an existing PageWithU project with the latest template')
380
- .option('--dry-run', 'show what would change without writing files')
381
- .option('--yes', 'skip prompts and apply safe updates')
382
- .option('--force', 'overwrite changed template-managed files')
383
- .action(async (options) => {
384
- const projectDir = process.cwd();
385
- const spinner = ora(options.dryRun ? 'Checking project update...' : 'Updating project...').start();
386
-
387
- try {
388
- const missing = await detectProject(projectDir);
389
- if (missing.length) {
390
- spinner.fail(chalk.red('Current directory does not look like a PageWithU project.'));
391
- console.error(chalk.yellow(`Missing: ${missing.join(', ')}`));
392
- process.exit(1);
393
- }
104
+ .command('serve [args...]')
105
+ .description('start local preview for the current PageWithU project')
106
+ .allowUnknownOption(true)
107
+ .action((args = []) => runProjectScript('dev', args));
394
108
 
395
- spinner.stop();
396
- const previousManifest = await loadManifest(projectDir);
397
- const updates = await planTemplateUpdates(projectDir, options);
398
- const packageChanged = await mergePackage(projectDir, options.dryRun);
399
- const gitignoreChanged = await ensureGeneratedGitignore(projectDir, options.dryRun);
400
-
401
- if (!options.dryRun) await writeManifest(projectDir, false, previousManifest);
402
-
403
- printUpdateSummary({ ...updates, packageChanged, gitignoreChanged }, options.dryRun);
404
- } catch (err) {
405
- spinner.fail(chalk.red('Failed to update project.'));
406
- console.error(err);
407
- process.exit(1);
408
- }
409
- });
109
+ program
110
+ .command('build [args...]')
111
+ .description('build dist assets for the current PageWithU project')
112
+ .allowUnknownOption(true)
113
+ .action((args = []) => runProjectScript('build', args));
410
114
 
411
115
  program.parse();
package/docs/develop.md CHANGED
@@ -46,18 +46,18 @@ bin/page-withu.js
46
46
 
47
47
  当前 CLI 主要负责:
48
48
 
49
- 1. 读取命令行参数或交互式输入。
50
- 2. 复制 `template/` 到目标目录。
51
- 3. 根据用户输入生成目标项目的 `config.js`。
52
- 4. 修改目标项目 `package.json` 的 `name` 字段。
53
- 5. 输出下一步命令提示。
49
+ 1. `new`:复制 `template/` 到目标目录,保留默认配置。
50
+ 2. `serve`:在当前主页项目中启动本地预览。
51
+ 3. `build`:在当前主页项目中构建 `dist` 产物。
54
52
 
55
53
  本地调试 CLI:
56
54
 
57
55
  ```bash
58
56
  node bin/page-withu.js --help
59
- node bin/page-withu.js create my-homepage
60
- node bin/page-withu.js update --dry-run
57
+ node bin/page-withu.js new my-homepage
58
+ cd my-homepage
59
+ node /path/to/page-withu/bin/page-withu.js serve
60
+ node /path/to/page-withu/bin/page-withu.js build
61
61
  ```
62
62
 
63
63
  如果想全局挂载本地版本:
@@ -65,14 +65,12 @@ node bin/page-withu.js update --dry-run
65
65
  ```bash
66
66
  npm link
67
67
  page-withu --help
68
- page-withu create my-homepage
68
+ page-withu new my-homepage
69
69
  ```
70
70
 
71
- 新增 CLI 参数时,通常需要同步修改:
71
+ 调整 CLI 行为时,通常需要同步修改:
72
72
 
73
- - `bin/page-withu.js` 中的 commander option
74
- - 交互式 prompt 的默认值。
75
- - 生成 `config.js` 的逻辑。
73
+ - `bin/page-withu.js` 中的 commander command
76
74
  - 必要时修改 `template/config.js` 的默认字段。
77
75
  - 必要时修改模板中读取配置的 Vue 组件。
78
76
  - 必要时更新 `README.md` 和 `docs/setup.md`。
@@ -82,13 +80,13 @@ page-withu create my-homepage
82
80
  ```bash
83
81
  mkdir -p /tmp/page-withu-test
84
82
  cd /tmp/page-withu-test
85
- node /path/to/page-withu/bin/page-withu.js create demo-site --title "Demo" --author "Tester"
83
+ node /path/to/page-withu/bin/page-withu.js new demo-site
86
84
  cd demo-site
87
85
  npm install
88
- npm run build
86
+ node /path/to/page-withu/bin/page-withu.js build
89
87
  ```
90
88
 
91
- 如果 `npm run build` 成功,说明模板可以被正常生成和构建。
89
+ 如果构建成功,说明模板可以被正常生成和构建。
92
90
 
93
91
  ## 开发 Template
94
92
 
@@ -202,11 +200,11 @@ npm publish --access public
202
200
 
203
201
  ```bash
204
202
  npm install -g @x-withu/page-withu
205
- page-withu create my-homepage
203
+ page-withu new my-homepage
206
204
  ```
207
205
 
208
206
  或在线构建无需本地安装:
209
207
 
210
208
  ```bash
211
- npx @x-withu/page-withu create my-homepage
209
+ npx @x-withu/page-withu new my-homepage
212
210
  ```
package/docs/setup.md CHANGED
@@ -83,36 +83,21 @@ index_img: https://example.com/cover.jpg
83
83
  - `tags`:文章标签。
84
84
  - `index_img`:博客列表封面图,可选;不填会保留占位布局。
85
85
 
86
- ## 更新项目
86
+ ## CLI 命令
87
87
 
88
- 如果你已经用 `page-withu create` 创建过项目,后续升级 CLI 后可以在项目目录中更新模板能力:
88
+ Page With U 只提供三个常用命令:
89
89
 
90
90
  ```bash
91
- npm install -g @x-withu/page-withu
92
- cd my-homepage
93
- page-withu update --dry-run
94
- page-withu update
91
+ page-withu new my-homepage
92
+ page-withu serve
93
+ page-withu build
95
94
  ```
96
95
 
97
- `page-withu update` 会更新模板管理的文件,例如 `src/`、`index.html` 和 `vite.config.js`,并合并 `package.json` 中模板需要的脚本和依赖。
96
+ - `page-withu new my-homepage`:新建主页项目,所有站点配置都保持模板默认值。
97
+ - `page-withu serve`:在当前项目目录启动本地预览,对应模板项目的 `npm run dev`。
98
+ - `page-withu build`:在当前项目目录构建 `dist` 产物,对应模板项目的 `npm run build`。
98
99
 
99
- 为了避免覆盖你的内容,以下文件默认不会被更新:
100
-
101
- - `config.js`
102
- - `content/`
103
-
104
- 如果你修改过模板源码,更新时会跳过有冲突的文件并在终端列出来。确认要覆盖这些模板源码时,可以使用:
105
-
106
- ```bash
107
- page-withu update --force
108
- ```
109
-
110
- 更新完成后建议执行:
111
-
112
- ```bash
113
- npm install
114
- npm run build
115
- ```
100
+ 创建完成后,直接编辑 `config.js`、`content/index.md`、`content/domains.md` 和 `content/blog/` 即可定制网站。
116
101
 
117
102
  ## Markdown
118
103
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x-withu/page-withu",
3
- "version": "0.1.2",
3
+ "version": "1.0.0",
4
4
  "description": "A lightweight, elegant personal homepage generator.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,7 +37,6 @@
37
37
  "chalk": "^5.3.0",
38
38
  "commander": "^12.1.0",
39
39
  "fs-extra": "^11.2.0",
40
- "inquirer": "^9.3.2",
41
40
  "ora": "^8.0.1"
42
41
  }
43
42
  }
@@ -24,10 +24,19 @@
24
24
  </nav>
25
25
  </div>
26
26
 
27
- <aside v-if="toc.length" class="blog-toc" aria-label="Table of contents">
28
- <p>Contents</p>
27
+ <button v-if="toc.length" class="blog-toc-toggle" type="button" :aria-expanded="tocOpen"
28
+ aria-controls="blog-toc" @click="tocOpen = true">
29
+ 目录
30
+ </button>
31
+ <div v-if="toc.length && tocOpen" class="blog-toc-backdrop" @click="tocOpen = false"></div>
32
+ <aside v-if="toc.length" id="blog-toc" :class="['blog-toc', { open: tocOpen }]" aria-label="Table of contents">
33
+ <div class="blog-toc-header">
34
+ <p>目录</p>
35
+ <button class="blog-toc-close" type="button" aria-label="Close table of contents"
36
+ @click="tocOpen = false">×</button>
37
+ </div>
29
38
  <a v-for="item in toc" :key="item.id" :href="`#${item.id}`"
30
- :class="[`toc-level-${item.level}`, { active: activeHeading === item.id }]">
39
+ :class="[`toc-level-${item.level}`, { active: activeHeading === item.id }]" @click="tocOpen = false">
31
40
  {{ item.title }}
32
41
  </a>
33
42
  </aside>
@@ -58,6 +67,7 @@ defineEmits(['back', 'open-post'])
58
67
  const contentRef = ref(null)
59
68
  const zoomImage = ref(null)
60
69
  const activeHeading = ref('')
70
+ const tocOpen = ref(false)
61
71
  const basePath = import.meta.env.BASE_URL.replace(/\/$/, '')
62
72
  const blogHref = `${basePath}/blog.html` || '/blog.html'
63
73
  let mermaidModule
@@ -118,6 +128,7 @@ onMounted(() => {
118
128
  onUnmounted(() => window.removeEventListener('scroll', updateActiveHeading))
119
129
 
120
130
  watch(() => props.post.slug, () => {
131
+ tocOpen.value = false
121
132
  activeHeading.value = props.toc[0]?.id || ''
122
133
  nextTick(() => {
123
134
  updateActiveHeading()
@@ -992,6 +992,14 @@ footer p {
992
992
  margin-bottom: 8px;
993
993
  }
994
994
 
995
+ .blog-toc-toggle {
996
+ display: none;
997
+ }
998
+
999
+ .blog-toc-backdrop {
1000
+ display: none;
1001
+ }
1002
+
995
1003
  .blog-toc {
996
1004
  position: sticky;
997
1005
  top: 24px;
@@ -1005,6 +1013,17 @@ footer p {
1005
1013
  font-size: 0.84rem;
1006
1014
  }
1007
1015
 
1016
+ .blog-toc-header {
1017
+ display: flex;
1018
+ align-items: center;
1019
+ justify-content: space-between;
1020
+ gap: 12px;
1021
+ }
1022
+
1023
+ .blog-toc-close {
1024
+ display: none;
1025
+ }
1026
+
1008
1027
  .blog-toc p {
1009
1028
  margin: 0 0 4px;
1010
1029
  color: var(--color-heading);
@@ -1197,7 +1216,7 @@ footer p {
1197
1216
  }
1198
1217
 
1199
1218
  /* ===== Responsive: Mobile ===== */
1200
- @media (max-width: 480px) {
1219
+ @media (max-width: 960px) {
1201
1220
  body {
1202
1221
  padding: 0 16px;
1203
1222
  }
@@ -1258,10 +1277,80 @@ footer p {
1258
1277
  padding-bottom: 16px;
1259
1278
  }
1260
1279
 
1280
+ .blog-article-layout {
1281
+ grid-template-columns: 1fr;
1282
+ }
1283
+
1284
+ .blog-toc-toggle {
1285
+ position: fixed;
1286
+ right: 18px;
1287
+ bottom: 18px;
1288
+ z-index: 88;
1289
+ display: inline-flex;
1290
+ align-items: center;
1291
+ gap: 6px;
1292
+ padding: 9px 14px;
1293
+ border: 1px solid var(--color-card-border);
1294
+ border-radius: 999px;
1295
+ color: var(--color-heading);
1296
+ background: var(--color-bg);
1297
+ box-shadow: var(--shadow-card);
1298
+ font: inherit;
1299
+ font-size: 0.84rem;
1300
+ cursor: pointer;
1301
+ }
1302
+
1303
+ .blog-toc-backdrop {
1304
+ position: fixed;
1305
+ inset: 0;
1306
+ z-index: 89;
1307
+ display: block;
1308
+ background: rgba(15, 23, 42, 0.36);
1309
+ }
1310
+
1261
1311
  .blog-toc {
1262
- position: static;
1263
- order: -1;
1264
- padding: 12px 0 12px 14px;
1312
+ position: fixed;
1313
+ top: 50%;
1314
+ left: 50%;
1315
+ z-index: 90;
1316
+ width: min(calc(100vw - 48px), 320px);
1317
+ max-height: min(70vh, 480px);
1318
+ padding: 16px;
1319
+ border: 1px solid var(--color-card-border);
1320
+ border-radius: 16px;
1321
+ background: var(--color-card-bg);
1322
+ box-shadow: var(--shadow-card);
1323
+ transform: translate(-50%, -50%);
1324
+ display: none;
1325
+ }
1326
+
1327
+ .blog-toc.open {
1328
+ display: flex;
1329
+ }
1330
+
1331
+ .blog-toc-header {
1332
+ position: sticky;
1333
+ top: -16px;
1334
+ margin: -16px -16px 4px;
1335
+ padding: 16px;
1336
+ background: var(--color-card-bg);
1337
+ border-bottom: 1px solid var(--color-footer-border);
1338
+ }
1339
+
1340
+ .blog-toc-close {
1341
+ display: inline-flex;
1342
+ align-items: center;
1343
+ justify-content: center;
1344
+ width: 28px;
1345
+ height: 28px;
1346
+ border: 1px solid var(--color-card-border);
1347
+ border-radius: 999px;
1348
+ color: var(--color-subtext);
1349
+ background: var(--color-bg);
1350
+ font: inherit;
1351
+ font-size: 1rem;
1352
+ line-height: 1;
1353
+ cursor: pointer;
1265
1354
  }
1266
1355
 
1267
1356
  .blog-nav {