From 6918542b86be227061d1729608226f98f8e913cd Mon Sep 17 00:00:00 2001 From: Mohan Date: Mon, 10 Aug 2026 08:57:05 +0800 Subject: [PATCH] chore(api-gen): add phase2 deps and use empty basePath --- package-lock.json | 46 ++++++- package.json | 4 + prompt-agent2-frontend-v0.4.md | 228 +++++++++++++++++++++++++++++++++ src/api/client.ts | 20 +-- 4 files changed, 286 insertions(+), 12 deletions(-) create mode 100644 prompt-agent2-frontend-v0.4.md diff --git a/package-lock.json b/package-lock.json index 2f2596c..3c73eca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,16 @@ "dependencies": { "axios": "^1.7.9", "dompurify": "^3.2.4", + "highlight.js": "^11.11.1", + "marked": "^18.0.9", "pinia": "^2.3.0", "vue": "^3.5.13", "vue-router": "^4.5.0" }, "devDependencies": { "@openapitools/openapi-generator-cli": "^2.0.0", + "@types/dompurify": "^3.0.5", + "@types/marked": "^5.0.2", "@types/node": "^22.10.2", "@vitejs/plugin-vue": "^5.2.1", "playwright": "^1.62.1", @@ -1128,6 +1132,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", @@ -1135,6 +1149,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/marked": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/@types/marked/-/marked-5.0.2.tgz", + "integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mute-stream": { "version": "0.0.4", "resolved": "https://registry.npmmirror.com/@types/mute-stream/-/mute-stream-0.0.4.tgz", @@ -1159,8 +1180,8 @@ "version": "2.0.7", "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", @@ -2381,6 +2402,15 @@ "he": "bin/he" } }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/http-proxy-agent": { "version": "9.1.0", "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz", @@ -2522,6 +2552,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "18.0.9", + "resolved": "https://registry.npmmirror.com/marked/-/marked-18.0.9.tgz", + "integrity": "sha512-/Sa4qiiHZxf0/FQdBBowr9q4r10krCwMvpK48FUBdXdUXScDxiQGR9zCPrFgRVR5LU3iySOiIjy09ZQvADir1w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 4011c47..0b9eeef 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,16 @@ "dependencies": { "axios": "^1.7.9", "dompurify": "^3.2.4", + "highlight.js": "^11.11.1", + "marked": "^18.0.9", "pinia": "^2.3.0", "vue": "^3.5.13", "vue-router": "^4.5.0" }, "devDependencies": { "@openapitools/openapi-generator-cli": "^2.0.0", + "@types/dompurify": "^3.0.5", + "@types/marked": "^5.0.2", "@types/node": "^22.10.2", "@vitejs/plugin-vue": "^5.2.1", "playwright": "^1.62.1", diff --git a/prompt-agent2-frontend-v0.4.md b/prompt-agent2-frontend-v0.4.md new file mode 100644 index 0000000..cfd72e9 --- /dev/null +++ b/prompt-agent2-frontend-v0.4.md @@ -0,0 +1,228 @@ +# Agent2 Prompt: Mach-CMS Frontend Phase 2 Implementation + +## 你的角色 +你是前端开发 Agent,负责实现 Mach-CMS Phase 2 功能。你不是设计师,只按契约和规范实现功能。 + +## 契约文件(唯一真理来源) +后端仓库根目录下的 `openapi.yaml` 是你唯一的 API 规范。 + +## 技术栈 +- Vue 3.4+ +- TypeScript 5.4+(严格模式) +- Vite 5.2+ +- Vue Router 4.3+ +- Pinia 2.1+ +- Axios 1.7+ +- OpenAPI Generator 7.6.0 +- marked / markdown-it(Markdown 渲染) +- DOMPurify(XSS 防护) +- highlight.js 或 prismjs(代码高亮) + +## 项目状态 +Phase 1 已完成: +- OpenAPI 代码生成配置 +- Axios 客户端封装(含 401 自动刷新) +- Pinia auth store +- Vue Router + 路由守卫 +- 首页文章列表、文章详情、搜索页 +- 登录页、管理后台布局、文章列表/编辑器、标签管理 + +## 重要修正:basePath + +**`client.ts` 中所有 API 实例化时,basePath 必须传空字符串 `''`**: + +```typescript +export const authApi = new AuthApi(config, '', axiosInstance) +export const articlesApi = new ArticlesApi(config, '', axiosInstance) +export const articleAdminApi = new ArticleAdminApi(config, '', axiosInstance) +export const tagsApi = new TagsApi(config, '', axiosInstance) +export const mediaApi = new MediaApi(config, '', axiosInstance) +export const searchApi = new SearchApi(config, '', axiosInstance) +// Phase 2 新增 +export const commentsApi = new CommentsApi(config, '', axiosInstance) +export const commentAdminApi = new CommentAdminApi(config, '', axiosInstance) +export const userAdminApi = new UserAdminApi(config, '', axiosInstance) +``` + +**原因**:Vite 代理已配置 `/api` → `localhost:8080`,生成代码的 `BASE_PATH` 也是 `/api`,若不覆盖会导致 `/api/api/xxx`。 + +**纪律**:每次 `npm run api:generate` 后,检查 `client.ts` 中 basePath 是否为 `''`。 + +## 你的任务(按顺序执行,逐项验收) + +### Task 1: 重新生成 API 代码 + +后端更新 `openapi.yaml` 至 0.2.0 后,执行: +```bash +npm run api:generate +``` + +确认生成: +- `CommentResponse`, `CommentSubmitRequest` +- `UserInfo`, `UpdateRoleRequest` +- `CommentAdminApi`, `UserAdminApi` +- `StoredFile` 增加 `thumbnailUrl` + +### Task 2: 注册页面 + +文件:`src/views/RegisterView.vue` +- 路由 `/register` +- 表单:用户名(3-50 字符)、密码(6-100 字符)、确认密码、邮箱(可选) +- 前端校验:密码与确认密码一致 +- 调用 `authApi.register({ username, password, email })` +- 注册成功后自动调用 `authStore.login()` 并跳转 `/admin` +- 已登录用户访问 `/register` 自动跳转 `/admin` + +路由配置更新:`router/index.ts` 添加 `/register` 路由 + +### Task 3: 评论组件 + +文件:`src/components/CommentSection.vue` +- Props:`slug: string` +- 功能: + - 加载评论:`GET /api/articles/{slug}/comments` + - 展示评论列表(作者名、内容、时间) + - 评论提交表单:昵称(必填)、邮箱(可选)、内容(必填) + - 提交后提示"评论已提交,等待审核" +- 样式:简单即可,每条评论有分隔线 + +文件:`src/views/ArticleDetailView.vue` +- 底部引入 `` + +### Task 4: Markdown 渲染 + XSS 防护 + +```bash +npm install marked dompurify highlight.js +npm install -D @types/dompurify @types/marked +``` + +文件:`src/utils/markdown.ts` +```typescript +import { marked } from 'marked' +import DOMPurify from 'dompurify' +import hljs from 'highlight.js' +import 'highlight.js/styles/github-dark.css' + +marked.setOptions({ + highlight: (code, lang) => { + if (lang && hljs.getLanguage(lang)) { + return hljs.highlight(code, { language: lang }).value + } + return hljs.highlightAuto(code).value + } +}) + +export function renderMarkdown(content: string): string { + const rawHtml = marked.parse(content) as string + return DOMPurify.sanitize(rawHtml) +} +``` + +文件:`src/views/ArticleDetailView.vue` +- 文章内容用 `v-html="renderMarkdown(article.content)"` +- 引入 `highlight.js` 样式 + +### Task 5: 前台标签云 + +文件:`src/components/TagCloud.vue` +- 调用 `tagsApi.listTags()` +- 展示所有标签,标签大小按 `articleCount` 比例(简单实现:用不同 font-size class) +- 点击标签跳转首页并过滤该标签文章(可选,先实现展示即可) + +文件:`src/views/HomeView.vue` +- 侧边栏或底部引入 `` + +### Task 6: 管理后台增强 + +#### 6.1 评论审核页 +文件:`src/views/admin/CommentReviewView.vue` +- 路由 `/admin/comments` +- 调用 `commentAdminApi.listPendingComments()` +- 表格:作者、邮箱、内容、文章标题、提交时间 +- 操作:通过、拒绝、删除 +- 通过后评论在前台显示 + +#### 6.2 用户管理页 +文件:`src/views/admin/UserListView.vue` +- 路由 `/admin/users` +- 调用 `userAdminApi.listUsers()` +- 表格:用户名、邮箱、角色、状态、注册时间 +- 操作:修改角色(下拉选择 ADMIN/EDITOR/VISITOR)、启用/禁用 +- **只有 ADMIN 可见此菜单项**(路由守卫或菜单条件渲染) + +#### 6.3 菜单更新 +文件:`src/views/admin/AdminLayout.vue` +- 侧边栏新增:评论审核、用户管理 +- 根据当前用户角色条件显示(先实现显示,权限由后端控制) + +### Task 7: 图片上传集成到编辑器 + +文件:`src/views/admin/ArticleEditorView.vue` +- 封面图:点击上传按钮 → `mediaApi.upload()` → 返回 url 填入表单 +- 内容区粘贴图片:监听 `paste` 事件,提取图片文件 → 上传 → 插入 Markdown 图片语法 `![alt](url)` + +## 代码规范(违反 = 拒收) + +| # | 规则 | +|---|------| +| 1 | **禁止手写 API 类型**,必须从 openapi.yaml 生成 | +| 2 | **禁止直接 import axios**,统一用 `src/api/client.ts` | +| 3 | 组件必须用 `