studynote/src/components/editor/WelcomeView.tsx
MikiVL aa1430d4bf feat(M4): 细节打磨——可拖拽/缩放 AI 浮窗、弹窗动画、全局快捷键、翻译功能
- AI 助手改为可自由拖拽移动的浮窗(position: fixed,默认右下角)
- 支持 8 方向拖拽缩放(N/S/E/W + 四角),尺寸 280–720 × 320–900px
- 弹窗(ModelSettingsModal、AiResultModal)加入 framer-motion 缩放动画
- App.tsx 注册全局快捷键:Cmd+\ 专注模式、Cmd+Shift+J AI 面板、Cmd+N 新建笔记
- 浮动工具栏新增"翻译成英文"按钮(Languages 图标),流式预览后可替换
- 服务端 buildMessages 增加 translate 分支
- AI 面板 textarea 随输入自动扩展高度(最多 5 行)
- 欢迎页功能一览补充 AI 续写/润色/摘要/问答四卡片及三条快捷键

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 00:28:01 +08:00

256 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react'
import {
FileText, Folder, Slash, MousePointer, Save,
Search, Moon, Plus, Hash, Star, Image, Code2,
BookOpen, Maximize2, Zap, Wand2, Bot,
} from 'lucide-react'
import { useAppStore } from '../../stores/appStore'
const FEATURES = [
{
icon: <FileText size={18} />,
title: '富文本编辑',
desc: '标题、列表、引用、代码块、表格、任务清单,应有尽有',
},
{
icon: <Code2 size={18} />,
title: '语法高亮',
desc: '代码块自动识别语言并高亮,支持 JS、TS、Python、Go 等数十种语言',
},
{
icon: <Image size={18} />,
title: '图片插入',
desc: '斜杠命令输入 URL或直接拖拽 / 粘贴截图到编辑器',
},
{
icon: <Slash size={18} />,
title: '斜杠命令',
desc: '输入 / 唤出命令菜单,用键盘快速插入任意内容块',
},
{
icon: <MousePointer size={18} />,
title: '浮动工具栏',
desc: '选中文字后浮现格式工具栏,点击即可加粗、高亮、转标题',
},
{
icon: <Save size={18} />,
title: '自动保存',
desc: '停止输入 1 秒后自动写入本地 IndexedDB无需手动保存',
},
{
icon: <BookOpen size={18} />,
title: '字数 & 阅读时长',
desc: '底部实时显示字数,并估算当前笔记的阅读时长',
},
{
icon: <Maximize2 size={18} />,
title: '专注模式',
desc: '点击标题栏图标收起侧边栏,全屏沉浸写作,按 Esc 退出',
},
{
icon: <Folder size={18} />,
title: '文件夹管理',
desc: '创建多级文件夹,右键可重命名或删除,笔记可分类归档',
},
{
icon: <Star size={18} />,
title: '收藏',
desc: '点击标题旁星标或笔记列表 hover 按钮,快速收藏重要笔记',
},
{
icon: <Search size={18} />,
title: '全文搜索',
desc: '侧边栏搜索框实时过滤笔记标题和标签',
},
{
icon: <Hash size={18} />,
title: '标签系统',
desc: '为笔记添加多个标签,跨文件夹快速定位相关内容',
},
{
icon: <Moon size={18} />,
title: '深色模式',
desc: '点击左上角图标一键切换亮色 / 暗色主题',
},
{
icon: <Zap size={18} />,
title: 'AI 续写',
desc: '点击标题旁 ⚡ 按钮AI 基于当前上下文流式续写下一段',
},
{
icon: <Wand2 size={18} />,
title: 'AI 润色',
desc: '选中文字 → 浮动工具栏 → 润色,流式预览后一键替换原文',
},
{
icon: <FileText size={18} />,
title: 'AI 摘要',
desc: '选中段落生成片段摘要,或在 AI 面板一键生成全文摘要',
},
{
icon: <Bot size={18} />,
title: 'AI 问答',
desc: '点击 ✨ 打开 AI 助手面板,针对当前笔记内容自由提问',
},
]
const SHORTCUTS: { mac: string[]; win: string[]; desc: string }[] = [
{ mac: ['⌘', 'B'], win: ['Ctrl', 'B'], desc: '粗体' },
{ mac: ['⌘', 'I'], win: ['Ctrl', 'I'], desc: '斜体' },
{ mac: ['⌘', 'U'], win: ['Ctrl', 'U'], desc: '下划线' },
{ mac: ['⌘', 'E'], win: ['Ctrl', 'E'], desc: '行内代码' },
{ mac: ['⌘', 'Z'], win: ['Ctrl', 'Z'], desc: '撤销' },
{ mac: ['⌘', '⇧', 'Z'], win: ['Ctrl', 'Y'], desc: '重做' },
{ mac: ['/'], win: ['/'], desc: '命令菜单' },
{ mac: ['Esc'], win: ['Esc'], desc: '退出专注模式' },
{ mac: ['⌘', 'N'], win: ['Ctrl', 'N'], desc: '新建笔记' },
{ mac: ['⌘', '\\'], win: ['Ctrl', '\\'], desc: '切换专注模式' },
{ mac: ['⌘', '⇧', 'J'], win: ['Ctrl', '⇧', 'J'], desc: '打开 AI 助手' },
]
function detectOS(): 'mac' | 'win' {
return /mac/i.test(navigator.platform) ? 'mac' : 'win'
}
export function WelcomeView() {
const { createNote } = useAppStore()
const [os, setOs] = useState<'mac' | 'win'>(detectOS)
return (
<div
className="flex-1 overflow-y-auto h-full"
style={{ background: 'var(--bg)' }}
>
<div className="max-w-2xl mx-auto px-12 pt-16 pb-20">
{/* Hero */}
<div className="mb-12">
<div
className="inline-flex items-center justify-center w-14 h-14 rounded-2xl mb-6 text-2xl"
style={{ background: 'var(--accent-subtle)', color: 'var(--accent)' }}
>
</div>
<h1
className="text-4xl font-bold mb-3"
style={{ color: 'var(--text)', letterSpacing: '-0.03em', lineHeight: 1.15 }}
>
使
</h1>
<p className="text-base" style={{ color: 'var(--text-muted)', lineHeight: 1.7 }}>
Vite + React + TipTap
<br />
</p>
<button
onClick={() => createNote(null)}
className="inline-flex items-center gap-2 mt-6 px-4 py-2 rounded-lg text-sm font-medium transition-colors"
style={{
background: 'var(--accent)',
color: '#fff',
}}
onMouseEnter={e => (e.currentTarget.style.background = 'var(--accent-hover)')}
onMouseLeave={e => (e.currentTarget.style.background = 'var(--accent)')}
>
<Plus size={15} />
</button>
</div>
{/* Divider */}
<div style={{ borderTop: '1px solid var(--border)', marginBottom: '2.5rem' }} />
{/* Features */}
<h2
className="text-xs font-semibold uppercase tracking-widest mb-5"
style={{ color: 'var(--text-faint)' }}
>
</h2>
<div className="grid grid-cols-2 gap-3 mb-12">
{FEATURES.map(f => (
<div
key={f.title}
className="rounded-xl p-4"
style={{ background: 'var(--bg-subtle)', border: '1px solid var(--border)' }}
>
<div
className="inline-flex items-center justify-center w-8 h-8 rounded-lg mb-3"
style={{ background: 'var(--accent-subtle)', color: 'var(--accent)' }}
>
{f.icon}
</div>
<div className="text-sm font-semibold mb-1" style={{ color: 'var(--text)' }}>{f.title}</div>
<div className="text-xs leading-relaxed" style={{ color: 'var(--text-muted)' }}>{f.desc}</div>
</div>
))}
</div>
{/* Shortcuts */}
<div className="flex items-center justify-between mb-4">
<h2
className="text-xs font-semibold uppercase tracking-widest"
style={{ color: 'var(--text-faint)' }}
>
</h2>
<div
className="flex items-center rounded-lg p-0.5 text-xs"
style={{ background: 'var(--bg-muted)', border: '1px solid var(--border)' }}
>
{(['mac', 'win'] as const).map(opt => (
<button
key={opt}
onClick={() => setOs(opt)}
className="px-2.5 py-1 rounded-md font-medium transition-colors"
style={{
background: os === opt ? 'var(--bg)' : 'transparent',
color: os === opt ? 'var(--text)' : 'var(--text-faint)',
boxShadow: os === opt ? '0 1px 3px rgba(0,0,0,0.08)' : 'none',
}}
>
{opt === 'mac' ? 'macOS' : 'Windows'}
</button>
))}
</div>
</div>
<div
className="rounded-xl overflow-hidden"
style={{ border: '1px solid var(--border)' }}
>
{SHORTCUTS.map((s, i) => (
<div
key={s.desc}
className="flex items-center justify-between px-5 py-3"
style={{
borderTop: i > 0 ? '1px solid var(--border)' : 'none',
background: 'var(--bg-subtle)',
}}
>
<span className="text-sm" style={{ color: 'var(--text-muted)' }}>{s.desc}</span>
<div className="flex items-center gap-1">
{s[os].map(k => (
<kbd
key={k}
className="inline-flex items-center justify-center px-2 py-0.5 rounded text-xs font-mono"
style={{
background: 'var(--bg)',
border: '1px solid var(--border)',
color: 'var(--text)',
minWidth: 28,
boxShadow: '0 1px 0 var(--border)',
}}
>
{k}
</kbd>
))}
</div>
</div>
))}
</div>
</div>
</div>
)
}