Files
rui-docs/superpowers/plans/2026-06-06-api-collaboration-plan.md
T

1323 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 前后端智能协作方案实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development (recommended) or superpowers-executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 建立前后端智能协作机制,通过 OpenAPI 自动生成 TypeScript 类型,实现类型安全的表单开发,减少前后端字段不一致问题。
**Architecture:** 保留现有 BaseService + Service 层架构,引入 openapi-typescript 生成类型,构建 useApiForm() 组合式函数和 ApiFormDialog 组件实现配置化表单,支持聚合启动器多模块架构。
**Tech Stack:** Vue 3 + TypeScript + Vite + openapi-typescript + Element Plus
---
## 文件结构
### 新增文件
| 文件 | 职责 |
|------|------|
| `scripts/parse-api-config.ts` | 解析 API设计规范.md,提取 api-docs 地址 |
| `scripts/generate-api.ts` | 调用 openapi-typescript 生成类型定义 |
| `scripts/check-api-changes.ts` | Git Hook 脚本,提交前检查 API 变更 |
| `src/composables/useApiForm.ts` | 类型驱动的表单管理组合式函数 |
| `src/components/ApiFormDialog.vue` | 配置化表单对话框组件 |
| `src/types/api.d.ts` | 统一类型导出(自动生成) |
| `src/types/api-modules.json` | 模块映射配置(自动生成) |
### 修改文件
| 文件 | 修改内容 |
|------|---------|
| `package.json` | 添加 api:generate、api:check、api:watch 脚本 |
| `src/service/BaseService.ts` | 增强类型安全,完善泛型使用 |
| `.env` / `.env.development` | 添加 API_GENERATE_URL 配置 |
---
## Task 1: 安装依赖
**Files:**
- Modify: `admin-ui/package.json`
- [ ] **Step 1: 安装类型生成依赖**
```bash
cd /Users/zhangsheng/rui/rui-frontend/admin-ui
pnpm add -D openapi-typescript tsx nodemon cross-env
```
- [ ] **Step 2: 验证安装**
```bash
pnpm list openapi-typescript tsx nodemon cross-env
```
Expected: 显示已安装版本
- [ ] **Step 3: Commit**
```bash
git add package.json pnpm-lock.yaml
git commit -m "chore: 安装 openapi-typescript 类型生成依赖"
```
---
## Task 2: 创建 API 配置解析脚本
**Files:**
- Create: `admin-ui/scripts/parse-api-config.ts`
- [ ] **Step 1: 创建解析脚本**
```typescript
import fs from 'fs'
import path from 'path'
/**
* API 模块配置
*/
export interface ApiModuleConfig {
/** 模块名称 */
name: string
/** OpenAPI JSON URL */
url: string
/** 类型输出路径 */
output: string
/** API 路径前缀 */
prefix: string
/** 模块描述 */
description: string
/** 所属聚合器(如果有) */
aggregator?: string
}
/**
* 聚合器配置
*/
export interface AggregatorConfig {
/** 聚合器名称 */
name: string
/** 基础 URL */
baseUrl: string
/** 包含的模块 */
modules: Array<{
name: string
prefix: string
description: string
}>
}
/**
* 从 API设计规范.md 解析配置
*
* 聚合启动器结构:
* http://localhost:9399/v3/api-docs # 聚合启动器(包含 /user, /system
* http://localhost:9601/v3/api-docs # 收银服务(独立)
*
* 通过 group 参数获取子模块:
* http://localhost:9399/v3/api-docs?group=用户服务
* http://localhost:9399/v3/api-docs?group=系统服务
*/
export function parseApiConfigFromMarkdown(filePath: string): {
aggregators: AggregatorConfig[]
standalone: ApiModuleConfig[]
} {
const content = fs.readFileSync(filePath, 'utf-8')
// 查找 OpenAPI JSON 接口 部分
const openapiSection = content.match(/####\s+OpenAPI\s+JSON\s+接口\s*\n([\s\S]*?)(?=\n#{1,4}\s|\n---\s*$|$)/)
if (!openapiSection) {
throw new Error('未在 API设计规范.md 中找到 OpenAPI JSON 接口部分')
}
const lines = openapiSection[1].trim().split('\n')
const aggregators: Map<string, AggregatorConfig> = new Map()
const standalone: ApiModuleConfig[] = []
for (const line of lines) {
const match = line.match(/(http:\/\/[^\s]+)\s+#\s+(.+)/)
if (!match) continue
const url = match[1]
const description = match[2].trim()
// 解析 URL 结构
const urlInfo = parseUrl(url, description)
if (urlInfo.isAggregator) {
// 聚合启动器 - 包含多个模块
const aggregator: AggregatorConfig = {
name: urlInfo.name,
baseUrl: url,
modules: urlInfo.modules.map(m => ({
name: m.name,
prefix: m.prefix,
description: m.description
}))
}
aggregators.set(urlInfo.name, aggregator)
} else {
// 独立服务
standalone.push({
name: urlInfo.name,
url,
output: `src/types/${urlInfo.name}-api.d.ts`,
prefix: urlInfo.prefix,
description
})
}
}
return {
aggregators: Array.from(aggregators.values()),
standalone
}
}
/**
* 解析 URL 信息
*/
function parseUrl(url: string, description: string): {
name: string
isAggregator: boolean
prefix: string
modules: Array<{ name: string; prefix: string; description: string }>
} {
const portMatch = url.match(/:(\d+)/)
const port = portMatch ? portMatch[1] : ''
// 聚合启动器识别规则
const aggregatorPorts = ['9399']
const isAggregator = aggregatorPorts.includes(port)
// 端口映射
const portMapping: Record<string, {
name: string
prefix: string
isAggregator: boolean
modules?: Array<{ name: string; prefix: string; description: string }>
}> = {
'9300': { name: 'gateway', prefix: '', isAggregator: false },
'9301': { name: 'auth', prefix: '/auth', isAggregator: false },
'9399': {
name: 'aggregator',
prefix: '',
isAggregator: true,
modules: [
{ name: 'user', prefix: '/user', description: '用户服务 API' },
{ name: 'system', prefix: '/system', description: '系统服务 API' }
]
},
'9601': { name: 'cashier', prefix: '/cashier', isAggregator: false }
}
const mapping = portMapping[port]
if (!mapping) {
console.warn(`⚠️ 未知端口 ${port},作为独立服务处理`)
return {
name: `service-${port}`,
isAggregator: false,
prefix: '',
modules: []
}
}
return {
name: mapping.name,
isAggregator: mapping.isAggregator,
prefix: mapping.prefix,
modules: mapping.modules || []
}
}
/**
* 生成完整的模块配置列表(展开聚合器)
*/
export function expandModules(config: {
aggregators: AggregatorConfig[]
standalone: ApiModuleConfig[]
}): ApiModuleConfig[] {
const modules: ApiModuleConfig[] = [...config.standalone]
// 展开聚合器中的模块
for (const aggregator of config.aggregators) {
for (const module of aggregator.modules) {
modules.push({
name: module.name,
url: `${aggregator.baseUrl}?group=${encodeURIComponent(module.description)}`,
output: `src/types/${module.name}-api.d.ts`,
prefix: module.prefix,
description: module.description,
aggregator: aggregator.name
})
}
}
return modules
}
/**
* 默认配置(当无法解析规范文件时使用)
*/
export const defaultApiConfig = {
aggregators: [
{
name: 'aggregator',
baseUrl: 'http://localhost:9399/v3/api-docs',
modules: [
{ name: 'user', prefix: '/user', description: '用户服务 API' },
{ name: 'system', prefix: '/system', description: '系统服务 API' }
]
}
],
standalone: [
{
name: 'cashier',
url: 'http://localhost:9601/v3/api-docs',
output: 'src/types/cashier-api.d.ts',
prefix: '/cashier',
description: '收银服务 API 文档'
}
]
}
/**
* 获取 API 模块配置
*/
export function getApiModules(specPath?: string): ApiModuleConfig[] {
const mdPath = specPath || path.resolve(process.cwd(), '../docs/standards/API设计规范.md')
try {
if (fs.existsSync(mdPath)) {
console.log('📖 从 API设计规范.md 读取配置...')
const config = parseApiConfigFromMarkdown(mdPath)
const modules = expandModules(config)
if (modules.length > 0) {
console.log(`✅ 成功解析 ${modules.length} 个模块:`)
// 按聚合器分组显示
const aggregatorModules = modules.filter(m => m.aggregator)
const standaloneModules = modules.filter(m => !m.aggregator)
if (aggregatorModules.length > 0) {
console.log(' 📦 聚合启动器:')
aggregatorModules.forEach(m =>
console.log(` - ${m.name}: ${m.url}`)
)
}
if (standaloneModules.length > 0) {
console.log(' 🔗 独立服务:')
standaloneModules.forEach(m =>
console.log(` - ${m.name}: ${m.url}`)
)
}
return modules
}
}
} catch (error) {
console.warn('⚠️ 解析 API设计规范.md 失败:', error)
}
console.log('📋 使用默认配置')
return expandModules(defaultApiConfig)
}
```
- [ ] **Step 2: 验证解析脚本**
```bash
cd /Users/zhangsheng/rui/rui-frontend/admin-ui
npx tsx scripts/parse-api-config.ts
```
Expected: 显示解析的模块列表
- [ ] **Step 3: Commit**
```bash
git add scripts/parse-api-config.ts
git commit -m "feat: 添加 API 配置解析脚本"
```
---
## Task 3: 创建类型生成脚本
**Files:**
- Create: `admin-ui/scripts/generate-api.ts`
- [ ] **Step 1: 创建生成脚本**
```typescript
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
import { getApiModules, type ApiModuleConfig } from './parse-api-config'
/**
* 生成单个模块的类型定义
*/
async function generateModuleTypes(module: ApiModuleConfig): Promise<boolean> {
console.log(`\n🔄 生成 ${module.name} 模块类型...`)
if (module.aggregator) {
console.log(` 📦 来自聚合器: ${module.aggregator}`)
}
try {
// 确保输出目录存在
const outputDir = path.dirname(module.output)
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
}
// 使用 openapi-typescript 生成类型
execSync(
`npx openapi-typescript "${module.url}" -o ${module.output}`,
{ stdio: 'inherit' }
)
// 添加模块元数据注释
const content = fs.readFileSync(module.output, 'utf-8')
const header = `/**
* ${module.description}
*
* @module ${module.name}
* @prefix ${module.prefix || '/'}
* @source ${module.url}
* ${module.aggregator ? `@aggregator ${module.aggregator}` : '@standalone'}
* @generated ${new Date().toISOString()}
*
* ⚠️ 此文件由脚本自动生成,请勿手动修改
* 如需更新,请运行: pnpm api:generate
*/
`
fs.writeFileSync(module.output, header + content)
console.log(`${module.name} 生成成功 -> ${module.output}`)
return true
} catch (error) {
console.error(`${module.name} 生成失败:`, error)
return false
}
}
/**
* 生成统一导出文件
*/
function generateIndex(modules: ApiModuleConfig[]) {
const indexPath = 'src/types/api.d.ts'
const imports = modules.map(m =>
`// ${m.description}\nexport type * from './${m.name}-api'`
).join('\n\n')
const content = `/**
* API 类型统一导出
*
* ⚠️ 此文件由脚本自动生成,请勿手动修改
*/
${imports}
`
fs.writeFileSync(indexPath, content)
console.log(`\n📦 统一导出文件已生成: ${indexPath}`)
}
/**
* 生成聚合器映射文件
*/
function generateAggregatorMap(modules: ApiModuleConfig[]) {
const mapPath = 'src/types/aggregator-modules.json'
const aggregators = modules
.filter(m => m.aggregator)
.reduce((acc, m) => {
if (!acc[m.aggregator!]) {
acc[m.aggregator!] = []
}
acc[m.aggregator!].push({
name: m.name,
prefix: m.prefix,
description: m.description
})
return acc
}, {} as Record<string, any[]>)
fs.writeFileSync(mapPath, JSON.stringify(aggregators, null, 2))
console.log(`🗺️ 聚合器映射文件已生成: ${mapPath}`)
}
/**
* 主函数
*/
async function main() {
console.log('🚀 开始生成 API 类型定义...\n')
// 从 API设计规范.md 读取配置
const modules = getApiModules()
if (modules.length === 0) {
console.error('❌ 未找到任何 API 模块配置')
process.exit(1)
}
// 并行生成所有模块
const results = await Promise.all(
modules.map(m => generateModuleTypes(m))
)
const successCount = results.filter(Boolean).length
if (successCount === 0) {
console.error('\n❌ 所有模块生成失败')
process.exit(1)
}
// 生成辅助文件
generateIndex(modules)
generateAggregatorMap(modules)
console.log(`\n✨ 完成! ${successCount}/${modules.length} 个模块生成成功`)
if (successCount < modules.length) {
console.warn('⚠️ 部分模块生成失败,请检查服务是否启动')
}
}
// 执行
main().catch(error => {
console.error('💥 生成过程出错:', error)
process.exit(1)
})
```
- [ ] **Step 2: 配置 package.json 脚本**
修改 `admin-ui/package.json`
```json
{
"scripts": {
"dev": "vite --port 3000",
"build": "vite build",
"api:generate": "tsx scripts/generate-api.ts",
"api:check": "tsx scripts/check-api-changes.ts",
"api:watch": "nodemon --watch ../docs/standards/API设计规范.md --exec 'pnpm api:generate'"
}
}
```
- [ ] **Step 3: 测试生成**
```bash
pnpm api:generate
```
Expected: 生成 src/types/user-api.d.ts, src/types/system-api.d.ts, src/types/cashier-api.d.ts
- [ ] **Step 4: Commit**
```bash
git add scripts/generate-api.ts package.json
git commit -m "feat: 添加 API 类型生成脚本"
```
---
## Task 4: 创建 API 变更检查脚本
**Files:**
- Create: `admin-ui/scripts/check-api-changes.ts`
- [ ] **Step 1: 创建检查脚本**
```typescript
import { execSync } from 'child_process'
import fs from 'fs'
function checkApiChanges() {
console.log('🔍 检查 API 变更...')
// 获取当前分支的 API 类型文件 hash
const currentHash = execSync('git hash-object src/types/*.d.ts 2>/dev/null || echo ""').toString()
if (!currentHash.trim()) {
console.log('⚠️ 未找到现有类型文件,跳过检查')
return
}
// 尝试重新生成 API 类型
try {
execSync('pnpm api:generate', { stdio: 'pipe' })
// 比较生成后的 hash
const newHash = execSync('git hash-object src/types/*.d.ts').toString()
if (currentHash !== newHash) {
console.error('❌ API 类型已变更!')
console.error('')
console.error('请运行以下命令更新类型:')
console.error(' pnpm api:generate')
console.error('')
console.error('然后提交变更:')
console.error(' git add src/types/')
console.error(' git commit -m "chore: 更新 API 类型"')
console.error('')
process.exit(1)
}
console.log('✅ API 类型已是最新')
} catch (error) {
console.warn('⚠️ 无法检查 API 变更:', error)
// 不阻断提交,仅警告
}
}
checkApiChanges()
```
- [ ] **Step 2: 配置 Git Hook(可选)**
在项目根目录创建 `.husky/pre-commit`
```bash
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd admin-ui
pnpm api:check
```
- [ ] **Step 3: Commit**
```bash
git add scripts/check-api-changes.ts
git commit -m "feat: 添加 API 变更检查脚本"
```
---
## Task 5: 创建 useApiForm 组合式函数
**Files:**
- Create: `admin-ui/src/composables/useApiForm.ts`
- [ ] **Step 1: 创建组合式函数**
```typescript
import { ref, computed } from 'vue'
import type { Ref, ComputedRef } from 'vue'
/**
* 字段类型
*/
export type FieldType =
| 'input'
| 'textarea'
| 'select'
| 'radio'
| 'checkbox'
| 'number'
| 'tree-select'
| 'date'
| 'datetime'
/**
* 选项配置
*/
export interface Option {
label: string
value: any
}
/**
* 字段配置接口
*/
export interface FieldConfig<T = any> {
/** 字段名 */
key: keyof T
/** 字段标签 */
label: string
/** 字段类型 */
type: FieldType
/** 是否必填 */
required?: boolean
/** 验证规则 */
rules?: any[]
/** 选项(用于 select/radio/checkbox */
options?: Option[]
/** 是否禁用 */
disabled?: boolean | ((form: T) => boolean)
/** 占位符 */
placeholder?: string
/** 额外属性 */
props?: Record<string, any>
}
/**
* 表单配置接口
*/
export interface FormConfig<T extends Record<string, any>> {
/** 初始数据 */
initial?: Partial<T>
/** 字段配置列表 */
fields: FieldConfig<T>[]
/** 提交回调 */
onSubmit: (data: T) => Promise<void>
/** 提交前验证 */
beforeSubmit?: (data: T) => boolean | string
}
/**
* 类型驱动的表单组合式函数
*
* @example
* const { formRef, form, rules, loading, handleSubmit, resetForm } = useApiForm<UserDTO>({
* fields: [
* { key: 'username', label: '用户名', type: 'input', required: true },
* { key: 'status', label: '状态', type: 'radio', options: [...] }
* ],
* onSubmit: async (data) => {
* await userService.add(data)
* }
* })
*/
export function useApiForm<T extends Record<string, any>>(config: FormConfig<T>) {
const formRef = ref()
const loading = ref(false)
// 生成响应式表单数据
const form = ref<Partial<T>>({ ...config.initial } as Partial<T>)
// 自动生成验证规则
const rules = computed(() => {
const result: Record<string, any[]> = {}
for (const field of config.fields) {
if (field.required) {
result[field.key as string] = [
{ required: true, message: `请输入${field.label}`, trigger: 'blur' },
...(field.rules || [])
]
} else if (field.rules) {
result[field.key as string] = field.rules
}
}
return result
})
// 字段配置列表(响应式)
const fields = computed(() => config.fields)
/**
* 重置表单
*/
function resetForm(initial?: Partial<T>) {
form.value = { ...(initial || config.initial) } as Partial<T>
formRef.value?.resetFields()
}
/**
* 提交表单
*/
async function handleSubmit(): Promise<boolean> {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return false
// 自定义验证
if (config.beforeSubmit) {
const result = config.beforeSubmit(form.value as T)
if (result === false) return false
if (typeof result === 'string') {
console.error(result)
return false
}
}
loading.value = true
try {
await config.onSubmit(form.value as T)
return true
} catch (error) {
return false
} finally {
loading.value = false
}
}
return {
formRef,
form,
rules,
fields,
loading,
handleSubmit,
resetForm
}
}
export default useApiForm
```
- [ ] **Step 2: Commit**
```bash
git add src/composables/useApiForm.ts
git commit -m "feat: 添加 useApiForm 类型驱动表单组合式函数"
```
---
## Task 6: 创建 ApiFormDialog 组件
**Files:**
- Create: `admin-ui/src/components/ApiFormDialog.vue`
- [ ] **Step 1: 创建组件**
```vue
<script setup lang="ts" generic="T extends Record<string, any>">
import { computed } from 'vue'
import type { FieldConfig, FieldType } from '@/composables/useApiForm'
const props = defineProps<{
visible: boolean
title: string
width?: string | number
labelWidth?: string | number
fields: FieldConfig<T>[]
form: Partial<T>
rules: Record<string, any[]
loading?: boolean
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'update:form', value: Partial<T>): void
(e: 'submit'): void
}>()
const dialogVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
})
const formData = computed({
get: () => props.form,
set: (val) => emit('update:form', val)
})
function getFieldDisabled(field: FieldConfig<T>) {
if (typeof field.disabled === 'function') {
return field.disabled(formData.value as T)
}
return field.disabled
}
function handleClose() {
emit('update:visible', false)
}
</script>
<template>
<el-dialog
v-model="dialogVisible"
:title="title"
:width="width || '600px'"
:close-on-click-modal="false"
@close="handleClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
:label-width="labelWidth || '90px'"
>
<template v-for="field in fields" :key="field.key as string">
<el-form-item
:label="field.label"
:prop="field.key as string"
:required="field.required"
>
<!-- Input -->
<el-input
v-if="field.type === 'input'"
v-model="formData[field.key]"
:placeholder="field.placeholder || `请输入${field.label}`"
:disabled="getFieldDisabled(field)"
v-bind="field.props"
/>
<!-- Textarea -->
<el-input
v-else-if="field.type === 'textarea'"
v-model="formData[field.key]"
type="textarea"
:rows="field.props?.rows || 3"
:placeholder="field.placeholder || `请输入${field.label}`"
:disabled="getFieldDisabled(field)"
v-bind="field.props"
/>
<!-- Select -->
<el-select
v-else-if="field.type === 'select'"
v-model="formData[field.key]"
:placeholder="field.placeholder || `请选择${field.label}`"
:disabled="getFieldDisabled(field)"
style="width: 100%"
v-bind="field.props"
>
<el-option
v-for="opt in field.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<!-- Radio -->
<el-radio-group
v-else-if="field.type === 'radio'"
v-model="formData[field.key]"
:disabled="getFieldDisabled(field)"
>
<el-radio
v-for="opt in field.options"
:key="opt.value"
:label="opt.value"
>
{{ opt.label }}
</el-radio>
</el-radio-group>
<!-- Checkbox -->
<el-checkbox-group
v-else-if="field.type === 'checkbox'"
v-model="formData[field.key]"
:disabled="getFieldDisabled(field)"
>
<el-checkbox
v-for="opt in field.options"
:key="opt.value"
:label="opt.value"
>
{{ opt.label }}
</el-checkbox>
</el-checkbox-group>
<!-- Number -->
<el-input-number
v-else-if="field.type === 'number'"
v-model="formData[field.key]"
:min="field.props?.min"
:max="field.props?.max"
style="width: 100%"
v-bind="field.props"
/>
<!-- Tree Select -->
<el-tree-select
v-else-if="field.type === 'tree-select'"
v-model="formData[field.key]"
:data="field.props?.data || []"
:placeholder="field.placeholder || `请选择${field.label}`"
v-bind="field.props"
/>
<!-- Date / DateTime -->
<el-date-picker
v-else-if="field.type === 'date' || field.type === 'datetime'"
v-model="formData[field.key]"
:type="field.type"
:placeholder="field.placeholder || `请选择${field.label}`"
style="width: 100%"
v-bind="field.props"
/>
</el-form-item>
</template>
<!-- 自定义插槽 -->
<slot name="custom-fields" :form="formData" />
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="loading" @click="$emit('submit')">
保存
</el-button>
</template>
</el-dialog>
</template>
```
- [ ] **Step 2: Commit**
```bash
git add src/components/ApiFormDialog.vue
git commit -m "feat: 添加 ApiFormDialog 配置化表单组件"
```
---
## Task 7: 增强 BaseService 类型安全
**Files:**
- Modify: `admin-ui/src/service/BaseService.ts`
- [ ] **Step 1: 增强类型定义**
在现有 BaseService 基础上,添加更严格的类型约束:
```typescript
// 在 BaseService.ts 中添加
/**
* 类型安全的 Service 工厂函数
* 用于新模块,完全类型安全
*/
export function createTypedService<T extends { id?: number | string }>(basePath: string) {
return {
async getById(id: number | string): Promise<T> {
const res: any = await request({
url: `${basePath}/${id}`,
method: 'get',
})
return res.data
},
async add(data: Omit<T, 'id'>): Promise<T> {
const res: any = await request({
url: basePath,
method: 'post',
data,
})
return res.data
},
async update(data: T): Promise<boolean> {
const res: any = await request({
url: basePath,
method: 'put',
data,
})
return res.data === true
},
async remove(id: number | string): Promise<boolean> {
const res: any = await request({
url: `${basePath}/${id}`,
method: 'delete',
})
return res.data === true
}
}
}
```
- [ ] **Step 2: Commit**
```bash
git add src/service/BaseService.ts
git commit -m "feat: 增强 BaseService 类型安全"
```
---
## Task 8: 试点迁移 - UserFormDialog
**Files:**
- Modify: `admin-ui/src/views/user/info/UserFormDialog.vue`
- [ ] **Step 1: 重写 UserFormDialog**
```vue
<script setup lang="ts">
import { watch } from 'vue'
import { ElMessage } from 'element-plus'
import { useApiForm } from '@/composables/useApiForm'
import { userService } from '@/service/user/userService'
import { userDeptService } from '@/service/user/userDeptService'
import { userPostService } from '@/service/user/userPostService'
import { deptService } from '@/service/system/deptService'
import { postService } from '@/service/system/postService'
import type { components } from '@/types/user-api'
type UserDTO = components['schemas']['UserDTO']
const props = defineProps<{
visible: boolean
row?: any
}>()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
(e: 'success'): void
}>()
// 加载基础数据
const deptTree = ref<any[]>([])
const postList = ref<any[]>([])
async function loadDeptTree() {
deptTree.value = await deptService.tree()
}
async function loadPostList() {
postList.value = await postService.list({ status: 1 })
}
// 使用类型驱动的表单
const { formRef, form, rules, fields, loading, handleSubmit, resetForm } = useApiForm<UserDTO>({
initial: {
userType: 1,
status: 1,
deptIds: [],
postIds: []
} as any,
fields: [
{ key: 'username', label: '用户名', type: 'input', required: true },
{ key: 'password', label: '密码', type: 'input', required: true, props: { type: 'password', 'show-password': true } },
{ key: 'userType', label: '用户类型', type: 'select', options: [
{ label: '普通用户', value: 1 },
{ label: '管理员', value: 2 },
{ label: '系统用户', value: 3 }
]},
{ key: 'status', label: '状态', type: 'radio', options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 }
]}
],
onSubmit: async (data) => {
const isEdit = !!form.value.id
let userId = form.value.id
if (isEdit) {
await userService.update(data as any)
} else {
const user = await userService.add(data as any)
userId = user?.id
}
if (userId) {
await userDeptService.assignDepts(userId, (data as any).deptIds)
await userPostService.assignPosts(userId, (data as any).postIds)
}
ElMessage.success(isEdit ? '修改成功' : '新增成功')
emit('success')
emit('update:visible', false)
}
})
// 监听编辑数据
watch(() => props.visible, async (val) => {
if (val) {
await Promise.all([loadDeptTree(), loadPostList()])
if (props.row) {
resetForm({
...props.row,
password: ''
})
} else {
resetForm()
}
}
})
</script>
<template>
<ApiFormDialog
v-model:visible="props.visible"
v-model:form="form"
:title="form.id ? '编辑用户' : '新增用户'"
:fields="fields"
:rules="rules"
:loading="loading"
@submit="handleSubmit"
>
<template #custom-fields="{ form }">
<el-form-item label="所属部门">
<el-tree-select
v-model="form.deptIds"
:data="deptTree"
multiple
show-checkbox
check-strictly
node-key="id"
:props="{ label: 'deptName', children: 'children' }"
placeholder="请选择部门"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="岗位">
<el-select v-model="form.postIds" multiple placeholder="请选择岗位" style="width: 100%">
<el-option
v-for="post in postList"
:key="post.id"
:label="post.postName"
:value="post.id"
/>
</el-select>
</el-form-item>
</template>
</ApiFormDialog>
</template>
```
- [ ] **Step 2: 测试表单功能**
1. 打开用户管理页面
2. 点击"新增用户"
3. 验证表单字段是否正确显示
4. 提交表单,验证数据是否正确
- [ ] **Step 3: Commit**
```bash
git add src/views/user/info/UserFormDialog.vue
git commit -m "refactor: 使用类型驱动表单重写 UserFormDialog"
```
---
## Task 9: 文档和配置
**Files:**
- Create: `admin-ui/docs/api-collaboration.md`
- [ ] **Step 1: 创建使用文档**
```markdown
# 前后端智能协作使用指南
## 快速开始
### 1. 生成 API 类型
```bash
# 生成所有模块类型
pnpm api:generate
# 监视 API 规范变更(开发时)
pnpm api:watch
```
### 2. 在 Service 中使用类型
```typescript
import type { components } from '@/types/user-api'
type UserDTO = components['schemas']['UserDTO']
class UserService extends BaseService<UserDTO> {
constructor() {
super('/user/admin/user')
}
}
```
### 3. 创建类型驱动表单
```typescript
import { useApiForm } from '@/composables/useApiForm'
const { form, fields, rules, handleSubmit } = useApiForm<UserDTO>({
fields: [
{ key: 'username', label: '用户名', type: 'input', required: true },
{ key: 'status', label: '状态', type: 'radio', options: [...] }
],
onSubmit: async (data) => {
await userService.add(data)
}
})
```
## 自定义扩展
### 添加自定义字段
```vue
<ApiFormDialog
v-model:visible="visible"
v-model:form="form"
:fields="fields"
:rules="rules"
@submit="handleSubmit"
>
<template #custom-fields="{ form }">
<!-- 自定义字段 -->
<el-form-item label="自定义部门">
<el-tree-select v-model="customDeptIds" :data="deptTree" />
</el-form-item>
</template>
</ApiFormDialog>
```
## 添加新模块
1.`API设计规范.md` 中添加 api-docs 地址
2. 运行 `pnpm api:generate`
3. 类型文件自动生成
## 故障排除
### 类型生成失败
- 检查后端服务是否启动
- 检查 `API设计规范.md` 中的 URL 是否正确
### 类型不匹配
- 运行 `pnpm api:generate` 重新生成
- 检查后端 API 是否变更
```
- [ ] **Step 2: Commit**
```bash
git add docs/api-collaboration.md
git commit -m "docs: 添加前后端智能协作使用指南"
```
---
## 实施检查清单
- [ ] Task 1: 安装依赖
- [ ] Task 2: 创建 API 配置解析脚本
- [ ] Task 3: 创建类型生成脚本
- [ ] Task 4: 创建 API 变更检查脚本
- [ ] Task 5: 创建 useApiForm 组合式函数
- [ ] Task 6: 创建 ApiFormDialog 组件
- [ ] Task 7: 增强 BaseService 类型安全
- [ ] Task 8: 试点迁移 UserFormDialog
- [ ] Task 9: 文档和配置
---
## 成功标准
1.`pnpm api:generate` 成功生成所有模块类型
2. ✅ UserFormDialog 使用新方案正常运行
3. ✅ 表单字段与后端 API 类型一致
4. ✅ 提交时自动检测 API 变更
5. ✅ 团队可以使用新方案开发新功能
---
> **下一步**: 执行计划(使用 superpowers-subagent-driven-development 或 superpowers-executing-plans