refactor: 统一知识库接口公共类型

This commit is contained in:
zxr
2026-07-22 04:55:36 +08:00
parent c2c5afa82d
commit dbda9f76fb
8 changed files with 19 additions and 108 deletions

View File

@@ -1,74 +0,0 @@
# Task 3前端权限数据与菜单路由兼容
## 实现
- AppStore 新增 `permissionCodes: string[]`,在权限响应过滤前保存全部非空权限码并去重。
- 动态菜单在进入 `buildTree` 前严格筛选 `type === 1`;路由转换入口与子路由递归同时防御性过滤非菜单节点。
- `clearServerMenu()` 同步清空 `serverMenu``permissionCodes`,现有退出登录链路无需改动即可完成权限清理。
- 新增 `usePermissionCodes` Hook统一提供响应式权限码集合与 `hasPermission(code)`,不解析角色名称。
- 删除权限响应、树构建、路由转换及视图模块映射的相邻调试输出和无用导入。
## 变更文件
- `src/store/modules/app/index.ts`
- `src/store/modules/app/types.ts`
- `src/router/menu-data.ts`
- `src/hooks/usePermissionCodes.ts`
## 验证
1. `pnpm exec prettier src/store/modules/app/index.ts src/store/modules/app/types.ts src/router/menu-data.ts src/hooks/usePermissionCodes.ts --write`
- 结果:成功,随后冻结源码。
2. 首轮 `pnpm exec vue-tsc --noEmit --incremental false`
- 结果:全仓 828 条错误,改动文件 1 条;定位为 Hook 中 Pinia 索引签名导致 `permissionCodes.value` 被推断为 `unknown`
- 处理:仅在 Hook 响应式边界补充显式 `string[]` 类型,重新格式化并再次冻结源码。
3. 最终 `pnpm exec vue-tsc --noEmit --incremental false`
- 结果:退出码 2全仓错误行 827与已确认基线一致四个改动文件错误行 0本次新增类型错误为 0。
4. `pnpm build -- --outDir C:\Users\27105\AppData\Local\Temp\front-kb-task3-build-20260721`
- 结果:退出码 0Vite 7.3.1 成功转换 9055 个模块并完成生产构建。
- 清理:临时构建目录已删除;误启动的默认 `dist` 产物也已删除,仓库内无构建产物残留。
5. 内联 Node + 最小 Vite SSR 加载器模拟 `type=1` 菜单和 `type=2` 按钮权限响应,直接调用实际 `buildTree``transformMenuToRoutes`
- 结果:`permission_codes=kb:article:list,kb:article:create``menu_permissions=1``routes=1``empty_path=false``redirect=false``button_in_routes=false`,退出码 0。
6. `git diff --check`
- 结果:退出码 0。
## 自审
- 完整权限码在菜单过滤前保存,按钮权限不会因菜单过滤丢失。
- `type=2` 不进入树构建,也无法从递归路由转换入口生成空路径或重定向路由。
- 退出登录继续沿用现有 `userStore.logoutCallBack() -> appStore.clearServerMenu()`,权限码同步清空。
- Hook 仅按权限码精确匹配,不依赖用户角色;未新增测试文件,未修改生产固定密钥或无关路由。
## Concerns
- 全仓类型检查仍因已确认的 827 条基线错误返回退出码 2本任务四个改动文件错误为 0生产构建成功。
## 审查修复
### 实现
-`ServerMenuItem.type` 收紧为必填 `number | string`
- 新增统一的 `isMenuPermission` predicate仅接受数值 `1` 或字符串 `"1"`
- AppStore 构树前过滤、顶层路由转换和递归子路由转换统一调用该 predicate数值/字符串 `2`、缺失 `type` 和其他值均严格排除。
### 验证
1. `pnpm exec vue-tsc --noEmit --incremental false`
- 结果:全仓保持 827 条基线错误,任务四个改动文件错误 0。
2. `pnpm build -- --outDir C:\Users\27105\AppData\Local\Temp\front-kb-task3-review-build-20260721`
- 结果:退出码 0Vite 7.3.1 成功转换 9055 个模块并完成生产构建。
3. 内联 Node + 最小 Vite SSR 加载器直接调用实际 `isMenuPermission``buildTree``transformMenuToRoutes`
- 覆盖:数值 `1`、字符串 `"1"`、数值 `2`、字符串 `"2"`、缺失 `type`,并同时验证顶层和递归子路由。
- 结果:`predicate=true,true,false,false,false`、完整权限码 5 个、菜单权限 2 个、顶层路由 2 个、递归子路由 2 个、`empty_path=false``rejected_in_routes=false`,退出码 0。
4. `git diff --check`
- 结果:退出码 0。
### 自审
- `type` 在接口层必填,运行时仅 `1``"1"` 能进入菜单链路;缺失值没有兼容放行。
- 三个过滤位置共享同一 predicate不存在数值/字符串判断漂移。
- 权限码仍在过滤前完整保存,未扩展无关 API 或路由重构,源码冻结后未再修改。
### Concerns
- 工具策略拦截了临时构建目录的原生 PowerShell 删除命令,未采用替代删除方式;待清理路径为 `C:\Users\27105\AppData\Local\Temp\front-kb-task3-review-build-20260721`

View File

@@ -1,5 +1,5 @@
import { request } from '@/api/request' import { request } from '@/api/request'
import type { KbReply } from './faq' import type { KbReply } from './types'
/** 公共分类状态。 */ /** 公共分类状态。 */
export type CategoryStatus = 'active' | 'inactive' export type CategoryStatus = 'active' | 'inactive'

View File

@@ -1,5 +1,5 @@
import { request } from '@/api/request' import { request } from '@/api/request'
import type { KbReply } from './faq' import type { KbReply } from './types'
/** 文档状态。 */ /** 文档状态。 */
export type DocumentStatus = 'draft' | 'published' | 'reviewed' | 'rejected' export type DocumentStatus = 'draft' | 'published' | 'reviewed' | 'rejected'

View File

@@ -1,4 +1,9 @@
import { request } from '@/api/request' import { request } from '@/api/request'
import type { KbReply } from './types'
export { fetchCategoryTree as fetchGeneralCategoryTree } from './category'
export type { CategoryTreeNode } from './category'
export type { KbReply } from './types'
/** FAQ 审核状态。 */ /** FAQ 审核状态。 */
export type FaqStatus = 'draft' | 'published' | 'reviewed' | 'rejected' export type FaqStatus = 'draft' | 'published' | 'reviewed' | 'rejected'
@@ -7,14 +12,6 @@ export type FaqPriority = 'low' | 'medium' | 'high'
/** FAQ 列表可见范围。 */ /** FAQ 列表可见范围。 */
export type FaqScope = 'my' | 'all' export type FaqScope = 'my' | 'all'
/** 知识库服务统一响应。 */
export interface KbReply<T> {
code: number
message: string
details: T
timeseq: number
}
/** FAQ 资源。 */ /** FAQ 资源。 */
export interface Faq { export interface Faq {
id: number id: number
@@ -96,24 +93,6 @@ export interface UpdateFaqData extends FaqFormData {
id: number id: number
} }
/** 公共分类树节点。 */
export interface CategoryTreeNode {
id: number
name: string
description: string
type: 'general'
icon: string
color: string
parent_id: number
level: number
path: string
sort_order: number
status: 'active' | 'inactive'
doc_count: number
faq_count: number
children: CategoryTreeNode[]
}
/** FAQ 状态筛选项。 */ /** FAQ 状态筛选项。 */
export const faqStatusOptions: Array<{ label: string; value: FaqStatus }> = [ export const faqStatusOptions: Array<{ label: string; value: FaqStatus }> = [
{ label: '草稿', value: 'draft' }, { label: '草稿', value: 'draft' },
@@ -196,6 +175,3 @@ export const unfavoriteFaq = (id: number) =>
resource_type: 'faq', resource_type: 'faq',
resource_id: id, resource_id: id,
}) })
/** 获取公共分类树。 */
export const fetchGeneralCategoryTree = () => request.get<KbReply<CategoryTreeNode[]>>('/Kb/v1/category/tree')

View File

@@ -1,6 +1,7 @@
import { request } from '@/api/request' import { request } from '@/api/request'
import type { Document } from './document' import type { Document } from './document'
import type { Faq, KbReply } from './faq' import type { Faq } from './faq'
import type { KbReply } from './types'
export type ResourceType = 'document' | 'faq' export type ResourceType = 'document' | 'faq'

View File

@@ -1,6 +1,7 @@
import { request } from '@/api/request' import { request } from '@/api/request'
import type { Document } from './document' import type { Document } from './document'
import type { Faq, KbReply } from './faq' import type { Faq } from './faq'
import type { KbReply } from './types'
export type ReviewResourceType = 'all' | 'document' | 'faq' export type ReviewResourceType = 'all' | 'document' | 'faq'

View File

@@ -1,5 +1,5 @@
import { request } from '@/api/request' import { request } from '@/api/request'
import type { KbReply } from './faq' import type { KbReply } from './types'
export type TrashResourceType = 'document' | 'faq' export type TrashResourceType = 'document' | 'faq'

7
src/api/kb/types.ts Normal file
View File

@@ -0,0 +1,7 @@
/** 知识库服务统一响应。 */
export interface KbReply<T> {
code: number
message: string
details: T
timeseq: number
}