first commit

This commit is contained in:
2026-08-11 16:17:49 +08:00
commit 21d086aa23
170 changed files with 13204 additions and 0 deletions

View File

@@ -0,0 +1,283 @@
export const meta = {
name: 'build-chanlun-views',
description: '并行构建缠论阅读站的 5 个页面视图并评审',
phases: [
{ title: 'Build', detail: '5 个页面视图并行构建' },
{ title: 'Review', detail: '设计一致性与暗色/响应式评审' },
],
}
// ============================================================
// 共享约定契约:所有构建 agent 必须遵守
// ============================================================
const CONV = `
你在为一个名为「缠论研习社」的 Vue 3 + Vite + Tailwind v4 + TypeScript 阅读站构建页面。
站点用途:展示缠中说禅《教你炒股票》原文与杂文的沉浸式阅读。设计风格=「现代融合」:玄鉴克制 UI + 衬线正文阅读优化,亮/暗双主题。
## 硬性规范
- 仅写你被分配的那一个 .vue 文件,用 Write 工具写到指定绝对路径。不要创建或修改其它文件。
- 不要运行 pnpm install / build / typecheck / dev 等命令(共享目录会冲突)。只写文件。
- 用 <script setup lang="ts">。类型导入必须用 import type。不要留未使用的变量/导入(会被 vue-tsc 拒绝)。
- UI 文案禁止 emoji。中文标点正确。语义化 HTML用 <section><article><h1> 等)。
- 颜色/间距/圆角全部用下方语义 Token禁止写死任意 hex/rgb 颜色值。
## 设计 TokenTailwind 工具类,已全局定义)
背景bg-app(应用底色) bg-background(卡片) bg-surface(次级面板) bg-surface-2(更浅悬浮) bg-hover
文本text-foreground(主) text-muted(次) text-subtle(辅) text-faint(占位/禁用)
品牌/语义text-primary bg-primary bg-primary-soft(浅蓝底) text-success/warning/danger及 bg-success/10 等 /10 透明
边框border-line(默认弱) border-line-strong(分割) border-line-card(卡片描边)
圆角rounded-sm(8) rounded-md(12) rounded-lg(16) rounded-xl(20) rounded-2xl(24) rounded-full
阴影shadow-xs shadow-sm shadow-soft shadow-md shadow-lg shadow-xl
字体font-sans(默认) font-serif(衬线,正文/标题点缀) font-mono
过渡缓动transition 配合 duration-150/200/300
留白要克制、克制、克制——玄鉴美学核心是「安静、轻、可信」:大量留白、弱边框、柔和阴影。
## 中文图标lucide-vue-next按名导入例如
import { BookOpen, Clock, ArrowRight, Search, ChevronRight } from 'lucide-vue-next'
用 <BookOpen class="size-5" /> 渲染size-4/5/[18px])。
## 数据访问useArticles()@/composables/useArticles
返回:{ chanlun, essaysList, latest, featured, all, totalCount, find, neighbors, search }
- chanlun / essaysList / latest / featured 都是 computed用 .value 在 script 内,模板内直接用)。
- find(category, slug) → Article | undefined
- neighbors(article) → { prev?: ArticleMeta, next?: ArticleMeta }
- search(query) → ArticleMeta[]
调用方式const { chanlun, latest } = useArticles()
## 类型(@/types/article
ArticleMeta: { id, slug, category:'chanlun'|'essay'|'notes', title, subtitle?, lesson?, date(ISO字符串), tags:string[], excerpt, readingTime(分钟) }
Article 继承 ArticleMeta 并含 content(markdown字符串)。
## 路由(@/router
首页 '/'(name:'home')、缠论列表 '/chanlun'(name:'chanlun')、杂谈 '/articles'(name:'articles')、关于 '/about'(name:'about')。
阅读页name 'reader',路径 '/read/:category/:slug',用 <RouterLink :to="{ name:'reader', params:{ category, slug } }">。
用 useRouter()/useRoute() 获取路由。RouterLink 从 'vue-router' 导入。
## 已有组件(按需导入使用,不要重造)
- @/components/ui/BaseButton.vue: props variant('primary'|'outline'|'surface'|'ghost'|'danger'), size('sm'|'md'|'lg'), block(bool), disabled(bool);默认 slot
- @/components/ui/Badge.vue: props tone('primary'|'neutral'|'success'|'warning'|'danger');默认 slot
- @/components/ui/Card.vue: props hover(bool), padded(bool,默认true);默认 slot
- @/components/ui/SearchInput.vue: v-model(modelValue) + placeholder
- @/components/ArticleCard.vue: props article(ArticleMeta), showExcerpt(bool,默认true)——已是 RouterLink 包裹的卡片,直接循环渲染
- @/components/TableOfContents.vue: props items(TocItem[])TocItem={id,text,level:2|3}
## 工具
- @/utils/cn: cn(...classes), formatDate(iso)->'YYYY年M月D日', formatDateShort(iso)->'MM-DD'
- @/composables/useMarkdown: renderMarkdown(mdString)->htmlString, extractToc(mdString)->TocItem[]
- @/stores/reading: useReadingStore() → { fontSize, fontFamily, showToc, focusMode, toggleToc()... }
- @/stores/theme: useThemeStore()
## 布局说明(重要)
- 首页/缠论列表/杂谈/关于 这 4 个视图,运行在 DefaultLayout 内——该布局已提供 AppHeader(顶栏)+AppSidebar(侧栏)+AppFooter。所以你的视图只写主体内容不要再加顶栏/侧栏/页脚。
- 阅读视图运行在 ReaderLayout 内——该布局已提供极简顶栏(返回/主题/阅读设置/目录开关)+ReadingProgress。所以阅读视图只写[目录 | 正文] 网格 + 文章头部 + 正文 + 上下篇导航。
`
const BUILD_SCHEMA = {
type: 'object',
additionalProperties: false,
properties: {
file: { type: 'string', description: '写入的文件绝对路径' },
summary: { type: 'string', description: '一句话说明你实现了什么' },
components_used: { type: 'array', items: { type: 'string' } },
notes: { type: 'string', description: '任何需要主流程注意的点' },
},
required: ['file', 'summary'],
}
// ============================================================
// 各页面规格
// ============================================================
const HOME_SPEC = `${CONV}
## 你的任务:构建首页 HomeView
写入文件D:/code/chan/src/views/HomeView.vue
首页是站点的门面,要精致、有呼吸感。包含以下区块(用 <section> 组织):
1. Hero 区(顶部,克制而有气质):
- 大标题「缠论研习社」(font-serif, text-4xl~5xl, font-semibold)。
- 副标题:一句简介,如「缠中说禅《教你炒股票》原文与杂文 · 沉浸式系统性研习」。
- 两个按钮BaseButton variant=primary「开始阅读」→ 跳转 featured 文章(用 useArticles().featured 的 category/slug)BaseButton variant=outline「了解缠论」→ /about。
- 可加极淡的装饰(如右上角半透明走势曲线 SVGopacity 很低,不抢戏)。
- Hero 下方可放一行小统计:文章总数(totalCount)、分类数等text-subtle。
2. 缠论体系概览4 个概念卡片):分型 / 笔与线段 / 走势中枢 / 背驰与买卖点。每个卡片lucide 图标(在 bg-primary-soft 圆角方块里)+ 标题 + 一句话。整组用 gridsm:grid-cols-2 lg:grid-cols-4。卡片用 Card 或自定义 div(bg-background border rounded-lg p-5)。点击可跳转第一课 /read/chanlun/ke-02-fenxing 之类(用 featured 或第一篇 chanlun
3. 分类入口(用 @/data/categories 的 categories 数组3 个缠论108课 / 杂谈随笔 / 我的理解。每个卡片显示 label、description、文章数(chanlun.length / essaysList.length / 0)。点击跳转对应路由。notes 分类显示「即将上线」徽标且不可点(或点向 /about)。
分类数据导入import { categories } from '@/data/categories'
4. 最新文章:取 useArticles().latestcomputed最多6篇用 ArticleCard 渲染grid sm:grid-cols-2。标题旁有「查看全部 →」链接到 /chanlun。
5. 页尾引言条一句大字衬线引言「走势无须预测只需应对。」居中bg-surface 或带 primary 左边框的引用样式。
区块之间用 py-12 / py-16 等充足纵向留白分隔。整体 max-w 容器用 mx-auto max-w-[1200px] px-4。
`
const CHANLUN_SPEC = `${CONV}
## 你的任务:构建缠论列表页 ChanlunListView
写入文件D:/code/chan/src/views/ChanlunListView.vue
这是「缠论 108 课」系列的文章列表页。
1. 页头:标题「缠论 108 课」(font-serif, text-3xl, font-semibold) + 副标题描述(取 categories 里 key='chanlun' 的 description+ 右侧或下方显示文章数「共 N 课」。
2. 搜索栏:用 SearchInputv-model 一个 query refplaceholder「搜索课程…」。过滤逻辑const filtered = computed(() => query.value ? search(query.value) : chanlun.value)。注意 search 返回 ArticleMeta[]。
3. 列表:用 ArticleCard 渲染 filtered。布局用 flex flex-col gap-3单列堆叠卡片适合课程列表的顺序阅读感
4. 空状态filtered 为空时显示一个居中提示(图标 + 「未找到相关课程」+ 清除按钮)。
5. 顶部可加一个简短的「学习路径」提示卡(可选):用 bg-primary-soft 或 bg-surface说明建议按课次顺序学习。
用 useArticles() 取 chanlun 与 search。用 @/data/categories 取描述。注意:模板里直接用 chanluncomputed 在模板自动解包script 里用 chanlun.value。
`
const READER_SPEC = `${CONV}
## 你的任务:构建阅读视图 ArticleReaderView最核心页面
写入文件D:/code/chan/src/views/ArticleReaderView.vue
这是沉浸式阅读页。运行在 ReaderLayout 内(已提供顶栏+进度条,不要再加)。
数据获取:
- const route = useRoute(); const { find, neighbors } = useArticles()
- const article = computed(() => find(route.params.category as ArticleCategory, route.params.slug as string))
- 文章切换时要滚回顶部watch(() => route.params.slug, () => window.scrollTo({top:0})) (或 watch article
找不到文章时:渲染一个居中的「未找到文章」状态 + 返回首页/列表的 RouterLink。
找到文章时,布局(网格):
<div class="mx-auto grid max-w-[1180px] gap-8 px-4 py-10 sm:px-6 lg:grid-cols-[220px_minmax(0,1fr)] lg:py-14">
<!-- 目录列:桌面端且 reading.showToc 为真时显示 -->
<aside class="hidden lg:block"> <div class="sticky top-20"> <TableOfContents :items="toc" v-if="toc.length"/> </div> </aside>
<!-- 正文列 -->
<article class="min-w-0 mx-auto w-full max-w-[760px]"> ... </article>
</div>
注意:目录列的显隐 = reading.showToclg 才显示)。当 reading.showToc 为 false 时,整个 aside 用 hidden为 true 时 hidden lg:block。可用 :class="reading.showToc ? 'hidden lg:block' : 'hidden'"。
文章头部(在 <article> 内,正文之前):
- 分类 Badgechanlun→「第 N 课」primaryessay→「杂谈」neutral
- 标题article.titlefont-serif text-3xl sm:text-4xl font-semiboldleading-tight。
- 副标题 article.subtitle若有text-muted。
- meta 行formatDate(article.date) · 阅读时间 article.readingTime 分钟 · 标签(#tag。用 text-subtle text-smlucide Clock 图标。
- 一条分割线 border-t border-line mt-6。
正文渲染(关键):
- const html = computed(() => renderMarkdown(article.value.content))
- const toc = computed(() => extractToc(article.value.content))
- 容器:<div class="prose-chan mt-8" :data-font="reading.fontFamily" :data-size="reading.fontSize" v-html="html"></div>
prose-chan 的样式与 data-font/data-size 已在 style.css 定义,会驱动衬线/字号)
底部(正文之后):
- 标签云(如有)。
- 分割线。
- 上一篇/下一篇导航neighbors(article.value) → { prev, next }。两个卡片横向排列grid sm:grid-cols-2 gap-4每张显示「上一篇/下一篇」小标 + 标题,用 RouterLink 指向对应 reader 路由。缺失的一侧显示占位「已是第一篇/最后一篇」(text-subtle)。
- 一个返回列表的链接(返回 /chanlun 或 /articles依据 article.category
务必:导入 ArticleCategory 类型用 import type。watch、computed 从 vue 导入。useRoute 从 vue-router 导入。
`
const ESSAYS_SPEC = `${CONV}
## 你的任务:构建杂谈列表页 OtherArticlesView
写入文件D:/code/chan/src/views/OtherArticlesView.vue
这是「杂谈随笔」的文章列表页(结构可参考缠论列表页,但气质更"随笔")。
1. 页头:标题「杂谈随笔」(font-serif text-3xl font-semibold) + 描述categories 里 key='essay' 的 description+ 文章数「共 N 篇」。
2. 搜索栏SearchInput v-model query过滤 essaysList。
3. 列表:用 ArticleCard 渲染。可用 flex flex-col gap-3 单列,或 grid sm:grid-cols-2 双列(杂谈更适合双列卡片网格)。自行选择更美观的。
4. 空状态:无结果提示。
5. 顶部可加一段引言卡片可选关于缠论作者或市场哲学的短引bg-surface + primary 左边框引用样式。
用 useArticles() 取 essaysList 与 search。模板内直接用 essaysListscript 内用 essaysList.value。
`
const ABOUT_SPEC = `${CONV}
## 你的任务:构建关于页 AboutView
写入文件D:/code/chan/src/views/AboutView.vue
关于本站页面。运行在 DefaultLayout 内(已有顶栏侧栏页脚)。
用居中窄栏max-w-[760px] mx-auto+ 衬线正文风格组织,分几个 <section>
1. 页头:标题「关于缠论研习社」(font-serif text-3xl font-semibold) + 一句定位。
2. 「什么是缠论」:用 2-3 段说明缠论是缠中说禅创立的市场分析体系,核心是形态学(分型/笔/线段/中枢/走势类型)+动力学(背驰/买卖点),强调"无须预测、只需应对"。可用 .prose-chan 排版或自定义段落样式。
3. 「关于缠中说禅」简述作者匿名网络ID2006-2008博客连载《教你炒股票》108课。提醒可点击「杂谈随笔」看相关文章——放一个 RouterLink。
4. 「关于本站」:说明本站用途(系统研习缠论原文与杂文)、内容来源(抓取整理,仅供学习)、后续计划(结合当下股市的个人理解与实战笔记,即「我的理解」分区即将上线)。
5. 免责声明:用一个小卡片(bg-surface rounded-lg p-4 text-sm text-muted) 说明「本站内容仅供学习研究,不构成任何投资建议;市场有风险,入市需谨慎。」
可以适当用 lucide 图标点缀小标题。排版要克制、有书卷气。不要 emoji。
`
// ============================================================
// 评审规格
// ============================================================
const REVIEW_SPEC = `${CONV}
## 你的任务:评审刚构建的 5 个页面视图
用 Read 工具逐一阅读这 5 个文件:
- D:/code/chan/src/views/HomeView.vue
- D:/code/chan/src/views/ChanlunListView.vue
- D:/code/chan/src/views/ArticleReaderView.vue
- D:/code/chan/src/views/OtherArticlesView.vue
- D:/code/chan/src/views/AboutView.vue
逐一检查并报告问题(不要修改文件,只报告):
1. TypeScript 正确性:是否有未使用的导入/变量vue-tsc noUnusedLocals 会报错、import type 是否用于纯类型导入、props 类型是否正确、computed/watch 用法是否正确。
2. 组件 API 用法BaseButton/Badge/Card/SearchInput/ArticleCard/TableOfContents 的 props 是否传对(尤其 ArticleCard 的 article 是 ArticleMetaTableOfContents 的 items 是 TocItem[])。
3. 设计一致性:是否只用了语义 Token无写死颜色、留白是否克制、圆角/阴影是否合规、是否符合玄鉴"安静克制"美学。
4. 暗色模式:所有颜色是否都用语义 Token能自动适配暗色有无硬编码深浅色。
5. 响应式:移动端(<lg)与桌面端布局是否合理grid 断点、hidden/sm:block 等)。
6. 数据访问useArticles 返回的 computed 在 script 中是否用 .value、模板中是否正确解包。
7. 路由RouterLink 的 :to 是否正确reader 用 {name,params})。
8. 可访问性:按钮有无 aria-label、语义化标签、标题层级。
按严重度 high/medium/low 报告,每条给出 file + issue + 具体修复建议(可粘贴代码)。最后给 overall 总评。
`
const REVIEW_SCHEMA = {
type: 'object',
additionalProperties: false,
properties: {
findings: {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
properties: {
file: { type: 'string' },
severity: { type: 'string', enum: ['high', 'medium', 'low'] },
issue: { type: 'string' },
suggestion: { type: 'string' },
},
required: ['file', 'severity', 'issue', 'suggestion'],
},
},
overall: { type: 'string' },
},
required: ['findings', 'overall'],
}
// ============================================================
// 执行
// ============================================================
phase('Build')
const builds = await parallel([
() => agent(HOME_SPEC, { label: 'build:home', phase: 'Build', schema: BUILD_SCHEMA }),
() => agent(CHANLUN_SPEC, { label: 'build:chanlun', phase: 'Build', schema: BUILD_SCHEMA }),
() => agent(READER_SPEC, { label: 'build:reader', phase: 'Build', schema: BUILD_SCHEMA }),
() => agent(ESSAYS_SPEC, { label: 'build:essays', phase: 'Build', schema: BUILD_SCHEMA }),
() => agent(ABOUT_SPEC, { label: 'build:about', phase: 'Build', schema: BUILD_SCHEMA }),
])
const ok = builds.filter(Boolean)
log(`已构建 ${ok.length}/5 个视图:${ok.map((b) => b?.file?.split('/').pop()).join(', ')}`)
phase('Review')
const review = await agent(REVIEW_SPEC, { label: 'review:views', phase: 'Review', schema: REVIEW_SCHEMA })
return {
built: ok.map((b) => ({ file: b.file, summary: b.summary })),
review,
}

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

94
CLAUDE.md Normal file
View File

@@ -0,0 +1,94 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
项目:缠论研习社——缠中说禅《教你炒股票》原文与杂文的沉浸式阅读站。
## 命令
```bash
pnpm install # 安装依赖(需 Node 20+ 与 pnpm
pnpm dev # 开发服务器 → http://localhost:5173vite open 自动开浏览器)
pnpm build # vue-tsc -b 类型检查 + vite 生产构建
pnpm build:only # 跳过类型检查,仅 vite build
pnpm typecheck # 仅 vue-tsc -b 类型检查(改完 TS/Vue 先跑这个)
pnpm preview # 预览生产构建
```
本机 pnpm v11 用 `pnpm-workspace.yaml``allowBuilds`(不是 `onlyBuiltDependencies`)放行 esbuild / vue-demi 的构建脚本;新增带构建脚本的 devDep 时需在此手动放行,否则安装会被拦。
## 样式约定(重要)
### 每个用 Tailwind 写的元素必须带一个「标记类名」
用 Tailwind 工具类写样式时,**必须给每个有独立含义的元素加一个语义化的标记 class并放在 class 列表最前面**。这样用户在对话里能精确指代「改哪个 div」。
```vue
<!-- 不要纯工具类堆叠用户无法指代 -->
<div class="flex items-center gap-2 rounded-md bg-surface px-3 py-2">
<!-- 标记类名打头组件名__部位kebab-case -->
<div class="article-card__lesson flex items-center gap-2 rounded-md bg-surface px-3 py-2">
```
命名规则:`<组件或区块名>__<部位>`,如 `home-hero__title``reader-meta``article-card__tags`。一个 `.vue` 文件内的标记类名以该组件的功能名为前缀,保证跨组件不撞。
> 现存组件(如 `ArticleCard.vue`)尚未遵循此约定;**新建或重写元素时一律补上**,改动既有元素时顺手补。
### 颜色必须走设计 Token禁止裸色值
所有颜色用 `src/style.css``@theme inline` 定义的语义类,**不要写 `bg-white` / `text-gray-500` / `#fff` 这类**
- 背景:`bg-app`(应用底)/ `bg-background`(卡片)/ `bg-surface` / `bg-surface-2` / `bg-hover`
- 文本:`text-foreground` / `text-muted` / `text-subtle` / `text-faint`
- 品牌/语义:`text-primary` / `bg-primary-soft` / `text-success` / `text-warning` / `text-danger`
- 边框:`border-line` / `border-line-strong` / `border-line-card`
- 圆角用 `rounded-xs/sm/md/lg/xl/2xl`4/8/12/16/20/24阴影用 `shadow-xs/sm/soft/md/lg/xl`
亮/暗主题切换靠 `<html class="dark">`,所有语义色自动适配——**只要用 Token就不用单独写 dark: 变体**。
### class 合并用 `cn()`
`src/utils/cn.ts``cn()` = clsx条件+ tailwind-merge冲突后写覆盖前写`px-2 px-4 → px-4`)。有条件 class 或动态拼接时用它,别手写字符串拼接。
## 架构(需要跨文件理解的)
**技术栈**Vue 3 `<script setup>` + Vite 6 + Tailwind v4CSS-first `@theme`+ TS strict + Pinia + Vue Router 4 + markdown-it + lucide-vue-next。`@` 别名 → `src/`
### 数据层是完全解耦的(核心设计)
页面不直接读数据文件,而是通过 `src/composables/useArticles.ts`:它把 `chanlunLessons``data/chanlun.ts`)和 `essays``data/essays.ts`)合并,提供 `chanlun / essaysList / latest / featured / find() / neighbors() / search()`
- 接入真实抓取数据时:只改数据源(替换 `data/*.ts`)或把 `useArticles.ts` 内的静态数组改成异步请求,**页面和组件无需改动**。
- 文章正文是 **markdown 字符串**`Article.content`,见 `types/article.ts`)。抓取的 HTML 入库时转 markdown或扩展 `useMarkdown.ts` 直接渲染 HTML。
### 文章正文排版 = `.prose-chan`(不要手写)
正文区一律挂 `prose-chan` 类(定义在 `style.css``@layer components`),它已处理 h2/h3/p/blockquote/code/table/列表等全部排版。字号/字体不写死,而是通过 `data-size``sm|base|lg|xl`)和 `data-font``serif|sans`)属性驱动,值来自 reading store。
### 两个 Pinia store 驱动全局体验
- `stores/theme.ts``light | dark | system`,持久化到 `localStorage['chan-theme']`,靠给 `<html>``.dark` 生效。
- `stores/reading.ts`:字号 / 字体 / 专注模式,分别持久化。
- **防闪烁FOUC**`index.html` 里有段内联脚本在 Vue 挂载前同步读 `chan-theme``.dark`。改主题存储 key 时,`index.html` 脚本和 `stores/theme.ts` 必须**同步改**,否则首屏会闪。
### 路由与布局
5 个 view两种 layout
- `DefaultLayout``/`):顶栏 + 侧栏 + 内容 + 页脚,包 home / chanlun / articles / about。
- `ReaderLayout``/read/:category/:slug`):沉浸阅读,含阅读进度、阅读设置、上下篇导航。
- markdown 的 h2/h3 会被 `useMarkdown.ts` 注入 `id`slug 来自 `utils/slug.ts`),便于锚点跳转。
### 现存组件缺标记类名
`components/ui/`BaseButton/Badge/Card/SearchInput`components/layout/`AppHeader/AppSidebar/AppFooter`ArticleCard` 等目前是纯工具类。改动它们时按上面的「标记类名」约定补齐。
## 其他约定
- TS strict 且开了 `noUnusedLocals` / `noUnusedParameters`**未用的 import / 变量会直接让 `pnpm build` 失败**,删代码时记得清干净。
- `<script setup lang="ts">` + Composition API。
- 图标:默认用 `lucide-vue-next`Vue 组件,按需 tree-shake。另装了 **Remix Icon**webfont补 lucide 缺的图标,品牌 Logo 即用 `ri-line-chart-line`
- CSS 已在 `main.ts` 全局引入 `remixicon/fonts/remixicon.css`,直接用 `<i class="ri-xxx-line" aria-hidden="true"></i>`
- webfont 字号靠 `text-*`font-size 驱动,不用 `size-*`);颜色继承 `currentColor`,所以放在 `text-white` 的容器里就是白色。
- 图标名查 https://remixicon.comv4.9,无 candlestick走势/图表类可用 `ri-line-chart-line` / `ri-bar-chart-line` / `ri-stock-line` / `ri-funds-line`)。
- `verbatimModuleSyntax: true`:类型 import 必须写 `import type { ... }`

100
README.md Normal file
View File

@@ -0,0 +1,100 @@
# 缠论研习社
> 缠中说禅《教你炒股票》原文与杂文的沉浸式阅读站 · Vue 3 + Tailwind v4
专注缠论的系统性研习:从分型、笔、线段,到走势中枢、背驰、买卖点。亮/暗双主题、衬线正文阅读优化、目录导航、字号字体可调。
## 技术栈
| 维度 | 选型 |
|------|------|
| 框架 | Vue 3`<script setup>` + Composition API |
| 构建 | Vite 6 |
| 样式 | Tailwind CSS v4CSS-first `@theme` |
| 语言 | TypeScriptstrict |
| 路由 | Vue Router 4 |
| 状态 | Pinia主题、阅读偏好 |
| 图标 | lucide-vue-next |
| 正文 | markdown-it |
## 快速开始
```bash
pnpm install # 安装依赖
pnpm dev # 启动开发服务器 → http://localhost:5173
pnpm build # 类型检查 + 生产构建
pnpm preview # 预览生产构建
pnpm typecheck # 仅类型检查
```
> 需要 Node 20+ 与 pnpm。首次安装若提示构建脚本已通过 `pnpm-workspace.yaml` 自动放行 esbuild / vue-demi。
## 目录结构
```
src/
├── components/
│ ├── ui/ # 基础组件BaseButton / Badge / Card / SearchInput
│ ├── layout/ # AppHeader / AppSidebar / AppFooter
│ ├── ArticleCard.vue # 文章列表卡片
│ ├── TableOfContents.vue
│ ├── ThemeToggle.vue / ReaderSettings.vue / ReadingProgress.vue
├── composables/
│ ├── useArticles.ts # 文章数据访问层(列表/查找/相邻/搜索)
│ └── useMarkdown.ts # markdown 渲染 + 目录提取
├── data/
│ ├── chanlun.ts # 缠论 108 课(示例数据)
│ ├── essays.ts # 杂谈随笔(示例数据)
│ └── categories.ts # 分类信息
├── layouts/
│ ├── DefaultLayout.vue # 顶栏 + 侧栏 + 内容 + 页脚
│ └── ReaderLayout.vue # 沉浸阅读布局
├── views/ # 5 个页面
├── stores/ # theme / readingPinia
├── types/article.ts # 类型定义
├── utils/ # cn / slug
└── style.css # 设计系统:玄鉴 Token + 亮暗主题 + 正文排版
```
## 接入你抓取的真实数据
数据层是完全解耦的。当前 `src/data/chanlun.ts``src/data/essays.ts` 是示例数据,**替换它们即可接入真实抓取内容**。
每篇文章的结构(见 `src/types/article.ts`
```ts
interface Article {
id: string // 唯一 id
slug: string // URL 标识
category: 'chanlun' | 'essay' | 'notes'
title: string
subtitle?: string
lesson?: number // 仅缠论系列:第几课
date: string // ISO 日期,如 '2006-06-07'
tags: string[]
excerpt: string // 摘要
readingTime: number // 预计阅读分钟
content: string // markdown 正文
}
```
正文为 **markdown 字符串**——抓取的 HTML 可在入库时转换为 markdown`turndown`),或扩展 `useMarkdown.ts` 支持直接渲染 HTML。后续若接入后端 API只需把 `useArticles.ts` 的静态数据源改为异步请求即可,页面无需改动。
## 设计系统
采用「玄鉴」设计语言(克制、安静、可信),映射到 Tailwind v4 语义 Token
- 颜色:`bg-app / bg-background / bg-surface``text-foreground / muted / subtle / faint``text-primary(#4d95ff) / bg-primary-soft``text-success / warning / danger``border-line / line-strong / line-card`
- 亮/暗双主题:通过 `<html class="dark">` 切换,所有颜色自动适配
- 正文:`.prose-chan` 衬线排版,字号/字体由阅读设置驱动(小/中/大/特大 · 衬线/黑体)
## 后续规划
- 「我的理解」分区:结合当下股市的缠论实战笔记
- 全文搜索、书签与阅读进度记录
- 走势图可视化(分型/笔/中枢标注)
- 后端 API 接入与内容管理
---
内容仅供学习研究,不构成任何投资建议。市场有风险,入市需谨慎。

7
env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>
export default component
}

32
index.html Normal file
View File

@@ -0,0 +1,32 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="缠论研习社 — 缠中说禅《教你炒股票》原文与杂文的沉浸式阅读站,专注走势、分型、笔、线段、中枢、背驰的系统性研习。" />
<!-- iOS 不把正文里的股票代码/年份误识别为 tel 链接 -->
<meta name="format-detection" content="telephone=no" />
<meta name="theme-color" content="#4d95ff" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0d0d0f" />
<title>缠论研习社</title>
<script>
// 防闪烁:在应用挂载前同步应用主题
(function () {
try {
var saved = localStorage.getItem('chan-theme')
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
var isDark = saved ? saved === 'dark' : prefersDark
if (isDark) document.documentElement.classList.add('dark')
// 同步地址栏/状态栏主题色(覆盖用户手动切换为暗色的场景)
var m = document.querySelector('meta[name="theme-color"]:not([media])')
if (m) m.setAttribute('content', isDark ? '#0d0d0f' : '#4d95ff')
} catch (e) {}
})()
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

34
package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "chanlun-reader",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "缠论研习社 — 缠中说禅《教你炒股票》原文与文章的沉浸式阅读站",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"build:only": "vite build",
"preview": "vite preview",
"typecheck": "vue-tsc -b"
},
"dependencies": {
"clsx": "^2.1.1",
"lucide-vue-next": "^0.468.0",
"markdown-it": "^14.1.0",
"pinia": "^2.3.0",
"remixicon": "^4.9.1",
"tailwind-merge": "^3.0.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@types/markdown-it": "^14.1.2",
"@types/node": "^22.13.0",
"@vitejs/plugin-vue": "^5.2.1",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.2",
"vite": "^6.0.0",
"vue-tsc": "^2.2.0"
}
}

1519
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
allowBuilds:
esbuild: true
vue-demi: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Some files were not shown because too many files have changed in this diff Show More