284 lines
17 KiB
JavaScript
284 lines
17 KiB
JavaScript
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 颜色值。
|
||
|
||
## 设计 Token(Tailwind 工具类,已全局定义)
|
||
背景: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。
|
||
- 可加极淡的装饰(如右上角半透明走势曲线 SVG,opacity 很低,不抢戏)。
|
||
- Hero 下方可放一行小统计:文章总数(totalCount)、分类数等,text-subtle。
|
||
|
||
2. 缠论体系概览(4 个概念卡片):分型 / 笔与线段 / 走势中枢 / 背驰与买卖点。每个卡片:lucide 图标(在 bg-primary-soft 圆角方块里)+ 标题 + 一句话。整组用 grid(sm: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().latest(computed,最多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. 搜索栏:用 SearchInput(v-model 一个 query ref),placeholder「搜索课程…」。过滤逻辑: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 取描述。注意:模板里直接用 chanlun(computed 在模板自动解包);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.showToc(lg 才显示)。当 reading.showToc 为 false 时,整个 aside 用 hidden;为 true 时 hidden lg:block。可用 :class="reading.showToc ? 'hidden lg:block' : 'hidden'"。
|
||
|
||
文章头部(在 <article> 内,正文之前):
|
||
- 分类 Badge(chanlun→「第 N 课」primary;essay→「杂谈」neutral)。
|
||
- 标题:article.title,font-serif text-3xl sm:text-4xl font-semibold,leading-tight。
|
||
- 副标题 article.subtitle(若有):text-muted。
|
||
- meta 行:formatDate(article.date) · 阅读时间 article.readingTime 分钟 · 标签(#tag)。用 text-subtle text-sm,lucide 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。模板内直接用 essaysList;script 内用 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. 「关于缠中说禅」:简述作者(匿名网络ID,2006-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 是 ArticleMeta;TableOfContents 的 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,
|
||
}
|