Browse Source

chore(api-gen): add phase2 deps and use empty basePath

main
Mohan 1 month ago
parent
commit
6918542b86
  1. 46
      package-lock.json
  2. 4
      package.json
  3. 228
      prompt-agent2-frontend-v0.4.md
  4. 20
      src/api/client.ts

46
package-lock.json

@ -10,12 +10,16 @@
"dependencies": { "dependencies": {
"axios": "^1.7.9", "axios": "^1.7.9",
"dompurify": "^3.2.4", "dompurify": "^3.2.4",
"highlight.js": "^11.11.1",
"marked": "^18.0.9",
"pinia": "^2.3.0", "pinia": "^2.3.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@openapitools/openapi-generator-cli": "^2.0.0", "@openapitools/openapi-generator-cli": "^2.0.0",
"@types/dompurify": "^3.0.5",
"@types/marked": "^5.0.2",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"playwright": "^1.62.1", "playwright": "^1.62.1",
@ -1128,6 +1132,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/estree": {
"version": "1.0.9", "version": "1.0.9",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz",
@ -1135,6 +1149,13 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/mute-stream": {
"version": "0.0.4", "version": "0.0.4",
"resolved": "https://registry.npmmirror.com/@types/mute-stream/-/mute-stream-0.0.4.tgz", "resolved": "https://registry.npmmirror.com/@types/mute-stream/-/mute-stream-0.0.4.tgz",
@ -1159,8 +1180,8 @@
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
"devOptional": true,
"license": "MIT"
}, },
"node_modules/@types/wrap-ansi": { "node_modules/@types/wrap-ansi": {
"version": "3.0.0", "version": "3.0.0",
@ -2381,6 +2402,15 @@
"he": "bin/he" "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": { "node_modules/http-proxy-agent": {
"version": "9.1.0", "version": "9.1.0",
"resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz", "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" "@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": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",

4
package.json

@ -12,12 +12,16 @@
"dependencies": { "dependencies": {
"axios": "^1.7.9", "axios": "^1.7.9",
"dompurify": "^3.2.4", "dompurify": "^3.2.4",
"highlight.js": "^11.11.1",
"marked": "^18.0.9",
"pinia": "^2.3.0", "pinia": "^2.3.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@openapitools/openapi-generator-cli": "^2.0.0", "@openapitools/openapi-generator-cli": "^2.0.0",
"@types/dompurify": "^3.0.5",
"@types/marked": "^5.0.2",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"playwright": "^1.62.1", "playwright": "^1.62.1",

228
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`
- 底部引入 `<CommentSection :slug="slug" />`
### 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`
- 侧边栏或底部引入 `<TagCloud />`
### 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 | 组件必须用 `<script setup lang="ts">` |
| 4 | API 响应必须用生成代码中的类型,禁止 `any` |
| 5 | 路由跳转用 `useRouter()`,禁止 `window.location`(除 401 跳转) |
| 6 | 异步操作必须 `try/catch`,错误用 alert 或控制台输出 |
| 7 | 图片上传用 `FormData`,Content-Type 让浏览器自动设置 |
| 8 | 集合默认 `[]`,禁止 `undefined` 作为列表值 |
| 9 | 路由参数用 `useRoute()` 获取 |
## Git 规范(必须遵守)
- 直接在 `main` 分支开发,commit 后立即 `git push origin main`
- Conventional Commits:`type(scope): subject`
- **单 commit 单意图**,文件 ≤ 10,行数 ≤ 200
- **每次 commit 前 `npm run build` 通过(无 TS 错误)**
- 提交信息用英文、祈使句、首字母小写、≤ 50 字符
**Scope(前端)**:`page` `component` `store` `api` `router` `type` `asset` `config` `contract`
**示例**:
```bash
feat(page): add register view with form validation
feat(component): add CommentSection for article detail
feat(component): add TagCloud with article count sizing
feat(page): add comment review page in admin
feat(page): add user management page with role editing
chore(api-gen): regenerate types from openapi.yaml v0.2.0
fix(api): set basePath to empty string to avoid /api/api
```
## 与 CDD 结合的提交顺序
1. `chore(api-gen): regenerate types from openapi.yaml v0.2.0`(契约更新后)
2. `feat(page): add register view`
3. `feat(component): add CommentSection with submit form`
4. `feat(component): add markdown renderer with syntax highlight`
5. `feat(component): add TagCloud component`
6. `feat(page): add admin comment review page`
7. `feat(page): add admin user management page`
8. `feat(component): integrate image upload in article editor`
## 验收标准
- [ ] `npm run api:generate` 成功,生成 Comment/User 相关类型
- [ ] `npm run build` 无 TS 错误
- [ ] `/register` 页面可用,注册后自动登录跳转
- [ ] 文章详情页显示评论区,可提交评论,提交后提示审核中
- [ ] Markdown 渲染正确,代码块有高亮,XSS 被过滤
- [ ] 标签云显示正常,大小按文章数变化
- [ ] 后台评论审核页可列出待审核评论,通过/拒绝/删除生效
- [ ] 后台用户管理页可查看用户列表,修改角色
- [ ] 文章编辑器可上传封面图,粘贴图片到内容区
- [ ] 所有 commit 符合 Conventional Commits 规范
## CDD 纪律
- 只修改前端代码,禁止修改后端仓库
- 发现 openapi.yaml 与实际返回不符,停止开发,上报问题
- 每次后端接口变更后,必须重新生成 API 代码
- 禁止修改 `src/generated/api/` 目录下的任何文件

20
src/api/client.ts

@ -38,7 +38,7 @@ axiosInstance.interceptors.response.use(
const refresh = localStorage.getItem('refreshToken') const refresh = localStorage.getItem('refreshToken')
if (refresh) { if (refresh) {
try { try {
const authApi = new AuthApi(new Configuration(), '/api', axiosInstance)
const authApi = new AuthApi(new Configuration(), '', axiosInstance)
const resp = await authApi.apiAuthRefreshPost({ refreshToken: refresh }) const resp = await authApi.apiAuthRefreshPost({ refreshToken: refresh })
const data = resp.data.data! const data = resp.data.data!
localStorage.setItem('token', data.accessToken) localStorage.setItem('token', data.accessToken)
@ -60,12 +60,12 @@ axiosInstance.interceptors.response.use(
const config = new Configuration() const config = new Configuration()
export const articleApi = new ArticleApi(config, '/api', axiosInstance)
export const articleAdminApi = new ArticleAdminApi(config, '/api', axiosInstance)
export const authApi = new AuthApi(config, '/api', axiosInstance)
export const tagApi = new TagApi(config, '/api', axiosInstance)
export const tagAdminApi = new TagAdminApi(config, '/api', axiosInstance)
export const mediaApi = new MediaApi(config, '/api', axiosInstance)
export const searchApi = new SearchApi(config, '/api', axiosInstance)
export const publicApi = new PublicApi(config, '/api', axiosInstance)
export const userApi = new UserApi(config, '/api', axiosInstance)
export const articleApi = new ArticleApi(config, '', axiosInstance)
export const articleAdminApi = new ArticleAdminApi(config, '', axiosInstance)
export const authApi = new AuthApi(config, '', axiosInstance)
export const tagApi = new TagApi(config, '', axiosInstance)
export const tagAdminApi = new TagAdminApi(config, '', axiosInstance)
export const mediaApi = new MediaApi(config, '', axiosInstance)
export const searchApi = new SearchApi(config, '', axiosInstance)
export const publicApi = new PublicApi(config, '', axiosInstance)
export const userApi = new UserApi(config, '', axiosInstance)
Loading…
Cancel
Save