feat: 添加清理未使用图片资源的脚本
添加 clean-unused-images.js 脚本用于自动扫描并删除未被引用的图片文件 更新 package.json 添加 clean 命令 更新 README.md 文档说明新功能
205
README.md
@@ -1,60 +1,173 @@
|
||||
# 🍥Fuwari
|
||||
# Fuwari
|
||||
|
||||
A static blog template built with [Astro](https://astro.build).
|
||||
一个基于 Astro 构建的现代化个人博客主题,专注于技术分享与实践。
|
||||
|
||||
[**🖥️ Live Demo (Vercel)**](https://fuwari.vercel.app) /
|
||||
[**📦 Old Hexo Version**](https://github.com/saicaca/hexo-theme-vivia) /
|
||||
## ✨ 特性
|
||||
|
||||
- 🚀 基于 Astro 4.0+ 构建,性能卓越
|
||||
- 📱 完全响应式设计,支持移动端
|
||||
- 🌙 支持深色/浅色主题切换
|
||||
- 📝 支持 Markdown 和 MDX 格式
|
||||
- 🔍 内置搜索功能
|
||||
- 📊 文章阅读时间统计
|
||||
- 🏷️ 标签和分类系统
|
||||
- 📈 SEO 优化
|
||||
- 🎨 可自定义配置
|
||||
- 💬 评论系统支持
|
||||
- 📡 RSS 订阅支持
|
||||
|
||||
> README version: `2024-09-10`
|
||||
## 🛠️ 技术栈
|
||||
|
||||

|
||||
- **框架**: Astro
|
||||
- **样式**: Tailwind CSS + Stylus
|
||||
- **交互**: Svelte
|
||||
- **构建工具**: Vite
|
||||
- **包管理**: pnpm
|
||||
- **代码规范**: Biome
|
||||
|
||||
## ✨ Features
|
||||
## 🚀 快速开始
|
||||
|
||||
- [x] Built with [Astro](https://astro.build) and [Tailwind CSS](https://tailwindcss.com)
|
||||
- [x] Smooth animations and page transitions
|
||||
- [x] Light / dark mode
|
||||
- [x] Customizable theme colors & banner
|
||||
- [x] Responsive design
|
||||
- [ ] Comments
|
||||
- [x] Search
|
||||
- [ ] TOC
|
||||
### 环境要求
|
||||
|
||||
## 🚀 How to Use
|
||||
- Node.js 18+
|
||||
- pnpm
|
||||
|
||||
1. [Generate a new repository](https://github.com/saicaca/fuwari/generate) from this template or fork this repository.
|
||||
2. To edit your blog locally, clone your repository, run `pnpm install` AND `pnpm add sharp` to install dependencies.
|
||||
- Install [pnpm](https://pnpm.io) `npm install -g pnpm` if you haven't.
|
||||
3. Edit the config file `src/config.ts` to customize your blog.
|
||||
4. Run `pnpm new-post <filename>` to create a new post and edit it in `src/content/posts/`.
|
||||
5. Deploy your blog to Vercel, Netlify, GitHub Pages, etc. following [the guides](https://docs.astro.build/en/guides/deploy/). You need to edit the site configuration in `astro.config.mjs` before deployment.
|
||||
### 安装依赖
|
||||
|
||||
## ⚙️ Frontmatter of Posts
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: My First Blog Post
|
||||
published: 2023-09-09
|
||||
description: This is the first post of my new Astro blog.
|
||||
image: ./cover.jpg
|
||||
tags: [Foo, Bar]
|
||||
category: Front-end
|
||||
draft: false
|
||||
lang: jp # Set only if the post's language differs from the site's language in `config.ts`
|
||||
---
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## 🧞 Commands
|
||||
### 开发模式
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
| Command | Action |
|
||||
|:------------------------------------|:-------------------------------------------------|
|
||||
| `pnpm install` AND `pnpm add sharp` | Installs dependencies |
|
||||
| `pnpm dev` | Starts local dev server at `localhost:4321` |
|
||||
| `pnpm build` | Build your production site to `./dist/` |
|
||||
| `pnpm preview` | Preview your build locally, before deploying |
|
||||
| `pnpm new-post <filename>` | Create a new post |
|
||||
| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `pnpm astro --help` | Get help using the Astro CLI |
|
||||
### 构建生产版本
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### 预览构建结果
|
||||
|
||||
```bash
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
## 📝 使用指南
|
||||
|
||||
### 创建新文章
|
||||
|
||||
使用内置脚本快速创建新文章:
|
||||
|
||||
```bash
|
||||
pnpm new-post helloword
|
||||
```
|
||||
|
||||
### 清理未使用的图片
|
||||
|
||||
清理 `src/content/assets` 目录下未被引用的图片文件:
|
||||
|
||||
```bash
|
||||
pnpm clean
|
||||
```
|
||||
|
||||
### 配置博客
|
||||
|
||||
编辑 `src/config.ts` 文件来自定义博客配置:
|
||||
|
||||
```typescript
|
||||
export const siteConfig: SiteConfig = {
|
||||
title: "Fuwari",
|
||||
subtitle: "技术分享与实践",
|
||||
lang: "zh_CN",
|
||||
themeColor: {
|
||||
hue: 250,
|
||||
fixed: false,
|
||||
},
|
||||
banner: {
|
||||
enable: false,
|
||||
src: "assets/images/demo-banner.png",
|
||||
position: "center",
|
||||
},
|
||||
favicon: [
|
||||
{
|
||||
src: "/favicon/icon.png",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 文章格式
|
||||
|
||||
文章使用 Markdown 格式,支持 frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: 文章标题
|
||||
published: 2024-01-01
|
||||
description: 文章描述
|
||||
image: ./cover.jpg
|
||||
tags: [标签1, 标签2]
|
||||
category: 分类
|
||||
draft: false
|
||||
---
|
||||
|
||||
# 文章内容
|
||||
|
||||
这里是文章正文...
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
├── public/ # 静态资源
|
||||
├── src/
|
||||
│ ├── components/ # 组件
|
||||
│ ├── content/ # 内容
|
||||
│ │ ├── posts/ # 博客文章
|
||||
│ │ └── assets/ # 资源文件
|
||||
│ ├── layouts/ # 布局
|
||||
│ ├── pages/ # 页面
|
||||
│ ├── styles/ # 样式
|
||||
│ └── config.ts # 配置文件
|
||||
├── scripts/ # 脚本工具
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## 🎨 自定义
|
||||
|
||||
### 主题颜色
|
||||
|
||||
在 `src/config.ts` 中修改 `themeColor` 配置:
|
||||
|
||||
```typescript
|
||||
themeColor: {
|
||||
hue: 250, // 主色调 (0-360)
|
||||
fixed: false, // 是否固定颜色
|
||||
}
|
||||
```
|
||||
|
||||
### 样式定制
|
||||
|
||||
- 全局样式:`src/styles/main.css`
|
||||
- Markdown 样式:`src/styles/markdown.css`
|
||||
- 变量定义:`src/styles/variables.styl`
|
||||
|
||||
## 📦 部署
|
||||
|
||||
构建后的静态文件位于 `dist/` 目录,可部署到任何静态托管平台。
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
[MIT License](LICENSE)
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢所有为这个项目做出贡献的开发者们!尤其感谢[上游仓库](https://github.com/saicaca/fuwari)
|
||||
@@ -10,6 +10,7 @@
|
||||
"astro": "astro",
|
||||
"type-check": "tsc --noEmit --isolatedDeclarations",
|
||||
"new-post": "node scripts/new-post.js",
|
||||
"clean": "node scripts/clean-unused-images.js",
|
||||
"format": "biome format --write ./src",
|
||||
"lint": "biome check --write ./src"
|
||||
},
|
||||
@@ -37,6 +38,7 @@
|
||||
"astro": "5.7.9",
|
||||
"astro-expressive-code": "^0.41.3",
|
||||
"astro-icon": "^1.1.5",
|
||||
"glob": "^11.0.3",
|
||||
"hastscript": "^9.0.1",
|
||||
"katex": "^0.16.22",
|
||||
"markdown-it": "^14.1.0",
|
||||
|
||||
62
pnpm-lock.yaml
generated
@@ -77,6 +77,9 @@ importers:
|
||||
astro-icon:
|
||||
specifier: ^1.1.5
|
||||
version: 1.1.5
|
||||
glob:
|
||||
specifier: ^11.0.3
|
||||
version: 11.0.3
|
||||
hastscript:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
@@ -1385,6 +1388,14 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@isaacs/balanced-match@4.0.1':
|
||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2908,6 +2919,11 @@ packages:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
hasBin: true
|
||||
|
||||
glob@11.0.3:
|
||||
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
|
||||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
@@ -3280,6 +3296,10 @@ packages:
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
jackspeak@4.1.1:
|
||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
jake@10.9.2:
|
||||
resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -3482,6 +3502,10 @@ packages:
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@11.2.1:
|
||||
resolution: {integrity: sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
@@ -3686,6 +3710,10 @@ packages:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
minimatch@10.0.3:
|
||||
resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
@@ -3973,6 +4001,10 @@ packages:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-scurry@2.0.0:
|
||||
resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
path-to-regexp@6.1.0:
|
||||
resolution: {integrity: sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==}
|
||||
|
||||
@@ -6745,6 +6777,12 @@ snapshots:
|
||||
'@img/sharp-win32-x64@0.34.1':
|
||||
optional: true
|
||||
|
||||
'@isaacs/balanced-match@4.0.1': {}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
dependencies:
|
||||
'@isaacs/balanced-match': 4.0.1
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
@@ -8523,6 +8561,15 @@ snapshots:
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
glob@11.0.3:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 4.1.1
|
||||
minimatch: 10.0.3
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 2.0.0
|
||||
|
||||
glob@7.2.3:
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
@@ -8979,6 +9026,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jackspeak@4.1.1:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
|
||||
jake@10.9.2:
|
||||
dependencies:
|
||||
async: 3.2.6
|
||||
@@ -9132,6 +9183,8 @@ snapshots:
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.2.1: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
@@ -9606,6 +9659,10 @@ snapshots:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
minimatch@10.0.3:
|
||||
dependencies:
|
||||
'@isaacs/brace-expansion': 5.0.0
|
||||
|
||||
minimatch@3.1.2:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.11
|
||||
@@ -9877,6 +9934,11 @@ snapshots:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
path-scurry@2.0.0:
|
||||
dependencies:
|
||||
lru-cache: 11.2.1
|
||||
minipass: 7.1.2
|
||||
|
||||
path-to-regexp@6.1.0: {}
|
||||
|
||||
path-to-regexp@6.3.0: {}
|
||||
|
||||
246
scripts/clean-unused-images.js
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { glob } from 'glob';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
/**
|
||||
* 清理未使用的图片资源脚本
|
||||
* 扫描 src/content/posts 下的所有 markdown 文件,
|
||||
* 查找 src/content/assets 中未被引用的图片并删除
|
||||
*/
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'src/content');
|
||||
const POSTS_DIR = path.join(CONTENT_DIR, 'posts');
|
||||
const ASSETS_DIR = path.join(CONTENT_DIR, 'assets');
|
||||
|
||||
// 支持的图片格式
|
||||
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.avif'];
|
||||
|
||||
/**
|
||||
* 获取所有 markdown 文件
|
||||
*/
|
||||
async function getAllMarkdownFiles() {
|
||||
try {
|
||||
const pattern = path.join(POSTS_DIR, '**/*.md').replace(/\\/g, '/');
|
||||
return await glob(pattern);
|
||||
} catch (error) {
|
||||
console.error('获取 markdown 文件失败:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有图片文件
|
||||
*/
|
||||
async function getAllImageFiles() {
|
||||
try {
|
||||
const extensions = IMAGE_EXTENSIONS.join(',');
|
||||
const pattern = path.join(ASSETS_DIR, `**/*{${extensions}}`).replace(/\\/g, '/');
|
||||
return await glob(pattern);
|
||||
} catch (error) {
|
||||
console.error('获取图片文件失败:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 markdown 内容中提取图片引用
|
||||
*/
|
||||
function extractImageReferences(content) {
|
||||
const references = new Set();
|
||||
|
||||
// 匹配 YAML frontmatter 中的 image 字段(支持带引号和不带引号的值)
|
||||
const yamlImageRegex = /^---[\s\S]*?image:\s*(?:['"]([^'"]+)['"]|([^\s\n]+))[\s\S]*?^---/m;
|
||||
let match = yamlImageRegex.exec(content);
|
||||
if (match) {
|
||||
// match[1] 是带引号的值,match[2] 是不带引号的值
|
||||
references.add(match[1] || match[2]);
|
||||
}
|
||||
|
||||
// 匹配 markdown 图片语法: 
|
||||
const markdownImageRegex = /!\[.*?\]\(([^)]+)\)/g;
|
||||
while ((match = markdownImageRegex.exec(content)) !== null) {
|
||||
references.add(match[1]);
|
||||
}
|
||||
|
||||
// 匹配 HTML img 标签: <img src="path">
|
||||
const htmlImageRegex = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi;
|
||||
while ((match = htmlImageRegex.exec(content)) !== null) {
|
||||
references.add(match[1]);
|
||||
}
|
||||
|
||||
// 匹配 Astro Image 组件引用
|
||||
const astroImageRegex = /import\s+.*?\s+from\s+["']([^"']+\.(jpg|jpeg|png|gif|webp|svg|avif))["']/gi;
|
||||
while ((match = astroImageRegex.exec(content)) !== null) {
|
||||
references.add(match[1]);
|
||||
}
|
||||
|
||||
return Array.from(references);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径,处理相对路径和绝对路径
|
||||
*/
|
||||
function normalizePath(imagePath, markdownFilePath) {
|
||||
// 跳过外部 URL
|
||||
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 跳过以 / 开头的绝对路径(通常指向 public 目录)
|
||||
if (imagePath.startsWith('/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理相对路径
|
||||
if (imagePath.startsWith('./') || imagePath.startsWith('../')) {
|
||||
const markdownDir = path.dirname(markdownFilePath);
|
||||
return path.resolve(markdownDir, imagePath);
|
||||
}
|
||||
|
||||
// 处理直接的文件名或相对路径
|
||||
const markdownDir = path.dirname(markdownFilePath);
|
||||
return path.resolve(markdownDir, imagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
*/
|
||||
async function cleanUnusedImages() {
|
||||
console.log('🔍 开始扫描未使用的图片资源...');
|
||||
|
||||
// 检查目录是否存在
|
||||
if (!fs.existsSync(POSTS_DIR)) {
|
||||
console.error(`❌ Posts 目录不存在: ${POSTS_DIR}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(ASSETS_DIR)) {
|
||||
console.log(`ℹ️ Assets 目录不存在: ${ASSETS_DIR}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取所有文件
|
||||
const markdownFiles = await getAllMarkdownFiles();
|
||||
const imageFiles = await getAllImageFiles();
|
||||
|
||||
console.log(`📄 找到 ${markdownFiles.length} 个 markdown 文件`);
|
||||
console.log(`🖼️ 找到 ${imageFiles.length} 个图片文件`);
|
||||
|
||||
if (imageFiles.length === 0) {
|
||||
console.log('✅ 没有找到图片文件,无需清理');
|
||||
return;
|
||||
}
|
||||
|
||||
// 收集所有被引用的图片
|
||||
const referencedImages = new Set();
|
||||
|
||||
for (const mdFile of markdownFiles) {
|
||||
try {
|
||||
const content = fs.readFileSync(mdFile, 'utf-8');
|
||||
const references = extractImageReferences(content);
|
||||
|
||||
for (const ref of references) {
|
||||
const normalizedPath = normalizePath(ref, mdFile);
|
||||
if (normalizedPath) {
|
||||
const resolvedPath = path.resolve(normalizedPath);
|
||||
referencedImages.add(resolvedPath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ 读取文件失败: ${mdFile} - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔗 找到 ${referencedImages.size} 个被引用的图片`);
|
||||
|
||||
// 找出未被引用的图片
|
||||
const unusedImages = [];
|
||||
|
||||
for (const imageFile of imageFiles) {
|
||||
const resolvedImagePath = path.resolve(imageFile);
|
||||
const isReferenced = referencedImages.has(resolvedImagePath);
|
||||
|
||||
if (!isReferenced) {
|
||||
unusedImages.push(imageFile);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🗑️ 找到 ${unusedImages.length} 个未使用的图片`);
|
||||
|
||||
if (unusedImages.length === 0) {
|
||||
console.log('✅ 所有图片都在使用中,无需清理');
|
||||
return;
|
||||
}
|
||||
|
||||
// 删除未使用的图片
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const unusedImage of unusedImages) {
|
||||
try {
|
||||
fs.unlinkSync(unusedImage);
|
||||
console.log(`🗑️ 已删除: ${path.relative(process.cwd(), unusedImage)}`);
|
||||
deletedCount++;
|
||||
} catch (error) {
|
||||
console.error(`❌ 删除失败: ${unusedImage} - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理空目录
|
||||
try {
|
||||
cleanEmptyDirectories(ASSETS_DIR);
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ 清理空目录时出错: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log(`\n✅ 清理完成!删除了 ${deletedCount} 个未使用的图片文件`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归清理空目录
|
||||
*/
|
||||
function cleanEmptyDirectories(dir) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
if (files.length === 0) {
|
||||
fs.rmdirSync(dir);
|
||||
console.log(`🗑️ 已删除空目录: ${path.relative(process.cwd(), dir)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
if (fs.statSync(filePath).isDirectory()) {
|
||||
cleanEmptyDirectories(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// 再次检查目录是否为空
|
||||
const remainingFiles = fs.readdirSync(dir);
|
||||
if (remainingFiles.length === 0) {
|
||||
fs.rmdirSync(dir);
|
||||
console.log(`🗑️ 已删除空目录: ${path.relative(process.cwd(), dir)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行脚本
|
||||
// 检查是否直接运行此脚本
|
||||
const scriptPath = fileURLToPath(import.meta.url);
|
||||
const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(scriptPath);
|
||||
|
||||
if (isMainModule) {
|
||||
cleanUnusedImages().catch(error => {
|
||||
console.error('❌ 脚本执行失败:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export { cleanUnusedImages };
|
||||
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 502 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 782 B |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 45 KiB |