mirror of
https://github.com/tradecatlabs/vibe-coding-cn.git
synced 2026-08-20 06:18:05 +00:00
feat: Add CI workflows, issue templates, examples, and prompts
This commit is contained in:
@@ -1,209 +1,11 @@
|
||||
# 实战案例:个人博客系统
|
||||
# 实战案例:从零到一构建博客系统
|
||||
|
||||
> 难度:⭐⭐ 中等 | 预计时间:2-4 小时 | 技术栈:Next.js + MDX
|
||||
> 本案例将指导您如何使用 Vibe Coding 方法论,与 AI 协作从零开始构建一个功能完善的博客系统。
|
||||
|
||||
## 🎯 项目目标
|
||||
* **技术栈**: [待定,例如:Next.js, Tailwind CSS, Markdown, Vercel]
|
||||
* **难度**: 中级
|
||||
* **预计用时**: 8-12 小时
|
||||
|
||||
构建一个支持 Markdown 的个人博客系统,具备:
|
||||
- 文章列表和详情页
|
||||
- 标签分类
|
||||
- 代码高亮
|
||||
- 响应式设计
|
||||
|
||||
## 📋 开始前的准备
|
||||
|
||||
### 环境要求
|
||||
- Node.js 18+
|
||||
- 包管理器(npm/pnpm/yarn)
|
||||
|
||||
### 第一步:需求澄清
|
||||
|
||||
复制以下提示词给 AI:
|
||||
|
||||
```
|
||||
我想用 Vibe Coding 的方式开发一个个人博客系统。
|
||||
|
||||
技术要求:
|
||||
- 框架:Next.js 14 (App Router)
|
||||
- 内容:MDX 格式的 Markdown 文章
|
||||
- 样式:Tailwind CSS
|
||||
- 部署:Vercel
|
||||
|
||||
功能需求:
|
||||
1. 首页显示文章列表(标题、日期、摘要)
|
||||
2. 文章详情页(支持代码高亮)
|
||||
3. 标签页面(按标签筛选文章)
|
||||
4. 关于页面
|
||||
|
||||
请帮我:
|
||||
1. 确认技术栈是否合适
|
||||
2. 生成项目结构
|
||||
3. 一步步指导我完成开发
|
||||
|
||||
要求:每完成一步问我是否成功,再继续下一步。
|
||||
```
|
||||
|
||||
## 🏗️ 项目结构
|
||||
|
||||
```
|
||||
my-blog/
|
||||
├── app/
|
||||
│ ├── layout.tsx # 根布局
|
||||
│ ├── page.tsx # 首页
|
||||
│ ├── posts/
|
||||
│ │ └── [slug]/
|
||||
│ │ └── page.tsx # 文章详情
|
||||
│ ├── tags/
|
||||
│ │ └── [tag]/
|
||||
│ │ └── page.tsx # 标签页
|
||||
│ └── about/
|
||||
│ └── page.tsx # 关于页
|
||||
├── components/
|
||||
│ ├── Header.tsx
|
||||
│ ├── Footer.tsx
|
||||
│ ├── PostCard.tsx
|
||||
│ └── MDXContent.tsx
|
||||
├── content/
|
||||
│ └── posts/ # MDX 文章
|
||||
│ ├── hello-world.mdx
|
||||
│ └── vibe-coding.mdx
|
||||
├── lib/
|
||||
│ └── posts.ts # 文章处理逻辑
|
||||
└── tailwind.config.js
|
||||
```
|
||||
|
||||
## 🔧 核心代码片段
|
||||
|
||||
### 文章处理逻辑 (lib/posts.ts)
|
||||
|
||||
```typescript
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const postsDirectory = path.join(process.cwd(), 'content/posts')
|
||||
|
||||
export interface Post {
|
||||
slug: string
|
||||
title: string
|
||||
date: string
|
||||
tags: string[]
|
||||
excerpt: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export function getAllPosts(): Post[] {
|
||||
const fileNames = fs.readdirSync(postsDirectory)
|
||||
const posts = fileNames
|
||||
.filter(name => name.endsWith('.mdx'))
|
||||
.map(fileName => {
|
||||
const slug = fileName.replace(/\.mdx$/, '')
|
||||
const fullPath = path.join(postsDirectory, fileName)
|
||||
const fileContents = fs.readFileSync(fullPath, 'utf8')
|
||||
const { data, content } = matter(fileContents)
|
||||
|
||||
return {
|
||||
slug,
|
||||
title: data.title,
|
||||
date: data.date,
|
||||
tags: data.tags || [],
|
||||
excerpt: data.excerpt || content.slice(0, 150),
|
||||
content
|
||||
}
|
||||
})
|
||||
.sort((a, b) => (a.date < b.date ? 1 : -1))
|
||||
|
||||
return posts
|
||||
}
|
||||
|
||||
export function getPostBySlug(slug: string): Post | undefined {
|
||||
return getAllPosts().find(post => post.slug === slug)
|
||||
}
|
||||
```
|
||||
|
||||
### 文章卡片组件 (components/PostCard.tsx)
|
||||
|
||||
```tsx
|
||||
import Link from 'next/link'
|
||||
import { Post } from '@/lib/posts'
|
||||
|
||||
export function PostCard({ post }: { post: Post }) {
|
||||
return (
|
||||
<article className="border-b border-gray-200 py-6">
|
||||
<Link href={`/posts/${post.slug}`}>
|
||||
<h2 className="text-xl font-bold hover:text-blue-600">
|
||||
{post.title}
|
||||
</h2>
|
||||
</Link>
|
||||
<time className="text-sm text-gray-500">{post.date}</time>
|
||||
<p className="mt-2 text-gray-600">{post.excerpt}</p>
|
||||
<div className="mt-2 flex gap-2">
|
||||
{post.tags.map(tag => (
|
||||
<Link
|
||||
key={tag}
|
||||
href={`/tags/${tag}`}
|
||||
className="text-xs bg-gray-100 px-2 py-1 rounded hover:bg-gray-200"
|
||||
>
|
||||
#{tag}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 📝 示例文章格式
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: "Hello World"
|
||||
date: "2024-01-01"
|
||||
tags: ["入门", "博客"]
|
||||
excerpt: "这是我的第一篇博客文章"
|
||||
---
|
||||
|
||||
# Hello World
|
||||
|
||||
欢迎来到我的博客!
|
||||
|
||||
## 代码示例
|
||||
|
||||
```javascript
|
||||
console.log('Hello, Vibe Coding!')
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
这就是我的第一篇文章。
|
||||
```
|
||||
|
||||
## 🚀 部署步骤
|
||||
|
||||
1. 推送代码到 GitHub
|
||||
2. 在 Vercel 导入项目
|
||||
3. 自动部署完成
|
||||
|
||||
## ✅ 验收清单
|
||||
|
||||
- [ ] 首页正确显示文章列表
|
||||
- [ ] 点击文章可以进入详情页
|
||||
- [ ] 代码块有语法高亮
|
||||
- [ ] 标签页面可以筛选文章
|
||||
- [ ] 移动端显示正常
|
||||
- [ ] 部署到 Vercel 成功
|
||||
|
||||
## 🔗 相关资源
|
||||
|
||||
- [Next.js 文档](https://nextjs.org/docs)
|
||||
- [MDX 文档](https://mdxjs.com/)
|
||||
- [Tailwind CSS](https://tailwindcss.com/)
|
||||
|
||||
## 💡 进阶挑战
|
||||
|
||||
完成基础版本后,可以尝试:
|
||||
- 添加搜索功能
|
||||
- 添加评论系统(Giscus)
|
||||
- 添加 RSS 订阅
|
||||
- 添加暗色模式
|
||||
- 添加阅读时间估算
|
||||
*(内容待填充...)*
|
||||
@@ -1,298 +1,11 @@
|
||||
# 实战案例:命令行工具
|
||||
# 实战案例:开发一个高效的命令行工具 (CLI)
|
||||
|
||||
> 难度:⭐⭐ 中等 | 预计时间:1-2 小时 | 技术栈:Python + Click
|
||||
> 本案例将指导您如何与 AI 协作,构思、设计并实现一个实用的命令行工具,例如一个文件处理器或信息查询工具。
|
||||
|
||||
## 🎯 项目目标
|
||||
* **技术栈**: [待定,例如:Python (Typer/Click), Go (Cobra)]
|
||||
* **难度**: 初级到中级
|
||||
* **预计用时**: 4-6 小时
|
||||
|
||||
构建一个实用的命令行工具,实现:
|
||||
- 文件批量重命名
|
||||
- 支持多种命名规则
|
||||
- 预览和确认机制
|
||||
- 彩色输出
|
||||
---
|
||||
|
||||
## 📋 开始前的准备
|
||||
|
||||
### 环境要求
|
||||
- Python 3.8+
|
||||
- pip
|
||||
|
||||
### 第一步:需求澄清
|
||||
|
||||
复制以下提示词给 AI:
|
||||
|
||||
```
|
||||
我想用 Vibe Coding 的方式开发一个文件批量重命名的命令行工具。
|
||||
|
||||
技术要求:
|
||||
- 语言:Python 3.8+
|
||||
- CLI 框架:Click
|
||||
- 输出美化:Rich
|
||||
|
||||
功能需求:
|
||||
1. 支持多种重命名规则:
|
||||
- 添加前缀/后缀
|
||||
- 替换文本
|
||||
- 序号命名
|
||||
- 日期命名
|
||||
2. 预览模式(不实际执行)
|
||||
3. 递归处理子目录(可选)
|
||||
4. 支持文件类型过滤
|
||||
|
||||
请帮我:
|
||||
1. 确认技术栈是否合适
|
||||
2. 生成项目结构
|
||||
3. 一步步指导我完成开发
|
||||
|
||||
要求:每完成一步问我是否成功,再继续下一步。
|
||||
```
|
||||
|
||||
## 🏗️ 项目结构
|
||||
|
||||
```
|
||||
file-renamer/
|
||||
├── renamer/
|
||||
│ ├── __init__.py
|
||||
│ ├── cli.py # CLI 入口
|
||||
│ ├── core.py # 核心逻辑
|
||||
│ └── rules.py # 重命名规则
|
||||
├── tests/
|
||||
│ └── test_core.py
|
||||
├── pyproject.toml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 🔧 核心代码
|
||||
|
||||
### CLI 入口 (renamer/cli.py)
|
||||
|
||||
```python
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from .core import FileRenamer
|
||||
from .rules import PrefixRule, SuffixRule, ReplaceRule, SequenceRule
|
||||
|
||||
console = Console()
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version='1.0.0')
|
||||
def cli():
|
||||
"""文件批量重命名工具"""
|
||||
pass
|
||||
|
||||
@cli.command()
|
||||
@click.argument('directory', type=click.Path(exists=True))
|
||||
@click.option('--prefix', '-p', help='添加前缀')
|
||||
@click.option('--suffix', '-s', help='添加后缀')
|
||||
@click.option('--replace', '-r', nargs=2, help='替换文本 (旧文本 新文本)')
|
||||
@click.option('--sequence', '-n', is_flag=True, help='序号命名')
|
||||
@click.option('--ext', '-e', multiple=True, help='文件扩展名过滤')
|
||||
@click.option('--recursive', '-R', is_flag=True, help='递归处理子目录')
|
||||
@click.option('--dry-run', '-d', is_flag=True, help='预览模式')
|
||||
def rename(directory, prefix, suffix, replace, sequence, ext, recursive, dry_run):
|
||||
"""批量重命名文件"""
|
||||
renamer = FileRenamer(directory, recursive=recursive, extensions=ext)
|
||||
|
||||
# 添加规则
|
||||
if prefix:
|
||||
renamer.add_rule(PrefixRule(prefix))
|
||||
if suffix:
|
||||
renamer.add_rule(SuffixRule(suffix))
|
||||
if replace:
|
||||
renamer.add_rule(ReplaceRule(replace[0], replace[1]))
|
||||
if sequence:
|
||||
renamer.add_rule(SequenceRule())
|
||||
|
||||
if not renamer.rules:
|
||||
console.print("[red]错误:请至少指定一个重命名规则[/red]")
|
||||
return
|
||||
|
||||
# 获取预览
|
||||
changes = renamer.preview()
|
||||
|
||||
if not changes:
|
||||
console.print("[yellow]没有找到匹配的文件[/yellow]")
|
||||
return
|
||||
|
||||
# 显示预览表格
|
||||
table = Table(title="重命名预览")
|
||||
table.add_column("原文件名", style="cyan")
|
||||
table.add_column("新文件名", style="green")
|
||||
|
||||
for old, new in changes:
|
||||
table.add_row(old, new)
|
||||
|
||||
console.print(table)
|
||||
console.print(f"\n共 [bold]{len(changes)}[/bold] 个文件")
|
||||
|
||||
if dry_run:
|
||||
console.print("[yellow]预览模式,未执行实际操作[/yellow]")
|
||||
return
|
||||
|
||||
# 确认执行
|
||||
if click.confirm('确认执行重命名?'):
|
||||
renamer.execute()
|
||||
console.print("[green]✓ 重命名完成![/green]")
|
||||
else:
|
||||
console.print("[yellow]已取消[/yellow]")
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
||||
```
|
||||
|
||||
### 核心逻辑 (renamer/core.py)
|
||||
|
||||
```python
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
class FileRenamer:
|
||||
def __init__(self, directory: str, recursive: bool = False, extensions: tuple = ()):
|
||||
self.directory = Path(directory)
|
||||
self.recursive = recursive
|
||||
self.extensions = extensions
|
||||
self.rules = []
|
||||
|
||||
def add_rule(self, rule):
|
||||
self.rules.append(rule)
|
||||
|
||||
def get_files(self) -> List[Path]:
|
||||
pattern = '**/*' if self.recursive else '*'
|
||||
files = []
|
||||
for path in self.directory.glob(pattern):
|
||||
if path.is_file():
|
||||
if self.extensions:
|
||||
if path.suffix.lower() in [f'.{e.lower()}' for e in self.extensions]:
|
||||
files.append(path)
|
||||
else:
|
||||
files.append(path)
|
||||
return sorted(files)
|
||||
|
||||
def apply_rules(self, filename: str) -> str:
|
||||
result = filename
|
||||
for rule in self.rules:
|
||||
result = rule.apply(result)
|
||||
return result
|
||||
|
||||
def preview(self) -> List[Tuple[str, str]]:
|
||||
changes = []
|
||||
for file in self.get_files():
|
||||
old_name = file.stem
|
||||
new_name = self.apply_rules(old_name)
|
||||
if old_name != new_name:
|
||||
changes.append((file.name, new_name + file.suffix))
|
||||
return changes
|
||||
|
||||
def execute(self):
|
||||
for file in self.get_files():
|
||||
old_name = file.stem
|
||||
new_name = self.apply_rules(old_name)
|
||||
if old_name != new_name:
|
||||
new_path = file.parent / (new_name + file.suffix)
|
||||
file.rename(new_path)
|
||||
```
|
||||
|
||||
### 重命名规则 (renamer/rules.py)
|
||||
|
||||
```python
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class Rule(ABC):
|
||||
@abstractmethod
|
||||
def apply(self, filename: str) -> str:
|
||||
pass
|
||||
|
||||
class PrefixRule(Rule):
|
||||
def __init__(self, prefix: str):
|
||||
self.prefix = prefix
|
||||
|
||||
def apply(self, filename: str) -> str:
|
||||
return f"{self.prefix}{filename}"
|
||||
|
||||
class SuffixRule(Rule):
|
||||
def __init__(self, suffix: str):
|
||||
self.suffix = suffix
|
||||
|
||||
def apply(self, filename: str) -> str:
|
||||
return f"{filename}{self.suffix}"
|
||||
|
||||
class ReplaceRule(Rule):
|
||||
def __init__(self, old: str, new: str):
|
||||
self.old = old
|
||||
self.new = new
|
||||
|
||||
def apply(self, filename: str) -> str:
|
||||
return filename.replace(self.old, self.new)
|
||||
|
||||
class SequenceRule(Rule):
|
||||
def __init__(self, start: int = 1, padding: int = 3):
|
||||
self.counter = start
|
||||
self.padding = padding
|
||||
|
||||
def apply(self, filename: str) -> str:
|
||||
result = f"{str(self.counter).zfill(self.padding)}_{filename}"
|
||||
self.counter += 1
|
||||
return result
|
||||
```
|
||||
|
||||
## 📦 安装配置 (pyproject.toml)
|
||||
|
||||
```toml
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "file-renamer"
|
||||
version = "1.0.0"
|
||||
description = "文件批量重命名工具"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"click>=8.0",
|
||||
"rich>=13.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
renamer = "renamer.cli:cli"
|
||||
```
|
||||
|
||||
## 🚀 使用示例
|
||||
|
||||
```bash
|
||||
# 安装
|
||||
pip install -e .
|
||||
|
||||
# 添加前缀
|
||||
renamer rename ./photos --prefix "2024_"
|
||||
|
||||
# 替换文本
|
||||
renamer rename ./docs --replace "old" "new"
|
||||
|
||||
# 序号命名 + 过滤扩展名
|
||||
renamer rename ./images --sequence --ext jpg --ext png
|
||||
|
||||
# 预览模式
|
||||
renamer rename ./files --prefix "backup_" --dry-run
|
||||
|
||||
# 递归处理
|
||||
renamer rename ./project --suffix "_v2" --recursive
|
||||
```
|
||||
|
||||
## ✅ 验收清单
|
||||
|
||||
- [ ] 命令行帮助信息正确显示
|
||||
- [ ] 前缀/后缀功能正常
|
||||
- [ ] 替换功能正常
|
||||
- [ ] 序号命名功能正常
|
||||
- [ ] 预览模式不执行实际操作
|
||||
- [ ] 扩展名过滤正常
|
||||
- [ ] 递归处理正常
|
||||
|
||||
## 💡 进阶挑战
|
||||
|
||||
- 添加撤销功能(记录操作日志)
|
||||
- 支持正则表达式
|
||||
- 添加日期格式化规则
|
||||
- 支持配置文件
|
||||
- 添加交互式模式
|
||||
*(内容待填充...)*
|
||||
@@ -1,402 +1,11 @@
|
||||
# 实战案例:Chrome 扩展
|
||||
# 实战案例:制作一个浏览器扩展程序
|
||||
|
||||
> 难度:⭐⭐ 中等 | 预计时间:2-3 小时 | 技术栈:JavaScript + Chrome API
|
||||
> 本案例将指导您如何与 AI 协作,开发一个简单的 Chrome 浏览器扩展,例如页面截图、内容聚合或自定义脚本注入工具。
|
||||
|
||||
## 🎯 项目目标
|
||||
* **技术栈**: [JavaScript, HTML, CSS, Manifest V3]
|
||||
* **难度**: 中级
|
||||
* **预计用时**: 6-10 小时
|
||||
|
||||
构建一个实用的 Chrome 扩展 - **网页笔记助手**:
|
||||
- 选中文本快速保存
|
||||
- 自动记录来源 URL
|
||||
- 本地存储笔记
|
||||
- 导出功能
|
||||
---
|
||||
|
||||
## 📋 开始前的准备
|
||||
|
||||
### 环境要求
|
||||
- Chrome 浏览器
|
||||
- 代码编辑器
|
||||
|
||||
### 第一步:需求澄清
|
||||
|
||||
复制以下提示词给 AI:
|
||||
|
||||
```
|
||||
我想用 Vibe Coding 的方式开发一个 Chrome 扩展 - 网页笔记助手。
|
||||
|
||||
技术要求:
|
||||
- Manifest V3
|
||||
- 纯 JavaScript(不用框架)
|
||||
- Chrome Storage API
|
||||
|
||||
功能需求:
|
||||
1. 右键菜单:选中文本后右键保存
|
||||
2. 弹出窗口:显示所有笔记
|
||||
3. 自动记录:来源 URL、保存时间
|
||||
4. 导出功能:导出为 JSON/Markdown
|
||||
|
||||
请帮我:
|
||||
1. 确认技术方案
|
||||
2. 生成项目结构
|
||||
3. 一步步指导我完成开发
|
||||
|
||||
要求:每完成一步问我是否成功,再继续下一步。
|
||||
```
|
||||
|
||||
## 🏗️ 项目结构
|
||||
|
||||
```
|
||||
web-notes/
|
||||
├── manifest.json # 扩展配置
|
||||
├── background.js # 后台脚本
|
||||
├── popup/
|
||||
│ ├── popup.html # 弹出窗口
|
||||
│ ├── popup.css
|
||||
│ └── popup.js
|
||||
├── icons/
|
||||
│ ├── icon16.png
|
||||
│ ├── icon48.png
|
||||
│ └── icon128.png
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 🔧 核心代码
|
||||
|
||||
### 扩展配置 (manifest.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "网页笔记助手",
|
||||
"version": "1.0.0",
|
||||
"description": "快速保存网页内容到本地笔记",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"contextMenus",
|
||||
"activeTab"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "popup/popup.html",
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 后台脚本 (background.js)
|
||||
|
||||
```javascript
|
||||
// 创建右键菜单
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.contextMenus.create({
|
||||
id: 'saveNote',
|
||||
title: '保存到笔记',
|
||||
contexts: ['selection']
|
||||
});
|
||||
});
|
||||
|
||||
// 处理右键菜单点击
|
||||
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||
if (info.menuItemId === 'saveNote' && info.selectionText) {
|
||||
const note = {
|
||||
id: Date.now().toString(),
|
||||
text: info.selectionText,
|
||||
url: tab.url,
|
||||
title: tab.title,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 获取现有笔记
|
||||
const { notes = [] } = await chrome.storage.local.get('notes');
|
||||
|
||||
// 添加新笔记
|
||||
notes.unshift(note);
|
||||
|
||||
// 保存
|
||||
await chrome.storage.local.set({ notes });
|
||||
|
||||
// 显示通知(可选)
|
||||
console.log('笔记已保存:', note.text.slice(0, 50));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 弹出窗口 HTML (popup/popup.html)
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>📝 我的笔记</h1>
|
||||
<div class="actions">
|
||||
<button id="exportJson">导出 JSON</button>
|
||||
<button id="exportMd">导出 MD</button>
|
||||
<button id="clearAll">清空</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="search">
|
||||
<input type="text" id="searchInput" placeholder="搜索笔记...">
|
||||
</div>
|
||||
|
||||
<div id="notesList" class="notes-list">
|
||||
<!-- 笔记列表 -->
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<span id="noteCount">0 条笔记</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 弹出窗口样式 (popup/popup.css)
|
||||
|
||||
```css
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 400px;
|
||||
max-height: 500px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.actions button {
|
||||
padding: 4px 8px;
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.actions button:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.search input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.notes-list {
|
||||
max-height: 350px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.note-item {
|
||||
padding: 12px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.note-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.note-meta {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.note-meta a {
|
||||
color: #0066cc;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.note-delete {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.note-delete:hover {
|
||||
color: #ff4444;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 40px;
|
||||
}
|
||||
```
|
||||
|
||||
### 弹出窗口逻辑 (popup/popup.js)
|
||||
|
||||
```javascript
|
||||
let allNotes = [];
|
||||
|
||||
// 加载笔记
|
||||
async function loadNotes() {
|
||||
const { notes = [] } = await chrome.storage.local.get('notes');
|
||||
allNotes = notes;
|
||||
renderNotes(notes);
|
||||
}
|
||||
|
||||
// 渲染笔记列表
|
||||
function renderNotes(notes) {
|
||||
const container = document.getElementById('notesList');
|
||||
const countEl = document.getElementById('noteCount');
|
||||
|
||||
countEl.textContent = `${notes.length} 条笔记`;
|
||||
|
||||
if (notes.length === 0) {
|
||||
container.innerHTML = '<div class="empty">暂无笔记<br>选中网页文字后右键保存</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = notes.map(note => `
|
||||
<div class="note-item" data-id="${note.id}">
|
||||
<span class="note-delete" onclick="deleteNote('${note.id}')">✕</span>
|
||||
<div class="note-text">${escapeHtml(note.text)}</div>
|
||||
<div class="note-meta">
|
||||
<a href="${note.url}" target="_blank">${note.title || '未知页面'}</a>
|
||||
<br>
|
||||
${formatDate(note.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 删除笔记
|
||||
async function deleteNote(id) {
|
||||
allNotes = allNotes.filter(n => n.id !== id);
|
||||
await chrome.storage.local.set({ notes: allNotes });
|
||||
renderNotes(allNotes);
|
||||
}
|
||||
|
||||
// 搜索
|
||||
document.getElementById('searchInput').addEventListener('input', (e) => {
|
||||
const query = e.target.value.toLowerCase();
|
||||
const filtered = allNotes.filter(note =>
|
||||
note.text.toLowerCase().includes(query) ||
|
||||
(note.title && note.title.toLowerCase().includes(query))
|
||||
);
|
||||
renderNotes(filtered);
|
||||
});
|
||||
|
||||
// 导出 JSON
|
||||
document.getElementById('exportJson').addEventListener('click', () => {
|
||||
const blob = new Blob([JSON.stringify(allNotes, null, 2)], { type: 'application/json' });
|
||||
downloadBlob(blob, 'notes.json');
|
||||
});
|
||||
|
||||
// 导出 Markdown
|
||||
document.getElementById('exportMd').addEventListener('click', () => {
|
||||
const md = allNotes.map(note =>
|
||||
`## ${formatDate(note.createdAt)}\n\n${note.text}\n\n> 来源: [${note.title}](${note.url})\n`
|
||||
).join('\n---\n\n');
|
||||
const blob = new Blob([md], { type: 'text/markdown' });
|
||||
downloadBlob(blob, 'notes.md');
|
||||
});
|
||||
|
||||
// 清空
|
||||
document.getElementById('clearAll').addEventListener('click', async () => {
|
||||
if (confirm('确定清空所有笔记?')) {
|
||||
allNotes = [];
|
||||
await chrome.storage.local.set({ notes: [] });
|
||||
renderNotes([]);
|
||||
}
|
||||
});
|
||||
|
||||
// 工具函数
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatDate(isoString) {
|
||||
return new Date(isoString).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function downloadBlob(blob, filename) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// 初始化
|
||||
loadNotes();
|
||||
```
|
||||
|
||||
## 🚀 安装步骤
|
||||
|
||||
1. 打开 Chrome,访问 `chrome://extensions/`
|
||||
2. 开启「开发者模式」
|
||||
3. 点击「加载已解压的扩展程序」
|
||||
4. 选择项目文件夹
|
||||
|
||||
## ✅ 验收清单
|
||||
|
||||
- [ ] 扩展成功加载
|
||||
- [ ] 右键菜单显示「保存到笔记」
|
||||
- [ ] 选中文本可以保存
|
||||
- [ ] 弹出窗口显示笔记列表
|
||||
- [ ] 搜索功能正常
|
||||
- [ ] 删除功能正常
|
||||
- [ ] 导出 JSON 正常
|
||||
- [ ] 导出 Markdown 正常
|
||||
|
||||
## 💡 进阶挑战
|
||||
|
||||
- 添加标签分类
|
||||
- 支持云同步(Chrome Sync)
|
||||
- 添加快捷键
|
||||
- 支持图片保存
|
||||
- 添加笔记编辑功能
|
||||
*(内容待填充...)*
|
||||
Reference in New Issue
Block a user