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

32 KiB
Raw Permalink Blame History

前后端智能协作方案实现计划

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: 安装类型生成依赖

cd /Users/zhangsheng/rui/rui-frontend/admin-ui
pnpm add -D openapi-typescript tsx nodemon cross-env
  • Step 2: 验证安装
pnpm list openapi-typescript tsx nodemon cross-env

Expected: 显示已安装版本

  • Step 3: Commit
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: 创建解析脚本

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: 验证解析脚本
cd /Users/zhangsheng/rui/rui-frontend/admin-ui
npx tsx scripts/parse-api-config.ts

Expected: 显示解析的模块列表

  • Step 3: Commit
git add scripts/parse-api-config.ts
git commit -m "feat: 添加 API 配置解析脚本"

Task 3: 创建类型生成脚本

Files:

  • Create: admin-ui/scripts/generate-api.ts

  • Step 1: 创建生成脚本

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

{
  "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: 测试生成
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
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: 创建检查脚本

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

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

cd admin-ui
pnpm api:check
  • Step 3: Commit
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: 创建组合式函数

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
git add src/composables/useApiForm.ts
git commit -m "feat: 添加 useApiForm 类型驱动表单组合式函数"

Task 6: 创建 ApiFormDialog 组件

Files:

  • Create: admin-ui/src/components/ApiFormDialog.vue

  • Step 1: 创建组件

<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
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 基础上,添加更严格的类型约束:

// 在 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
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

<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
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: 创建使用文档

# 前后端智能协作使用指南

## 快速开始

### 1. 生成 API 类型

```bash
# 生成所有模块类型
pnpm api:generate

# 监视 API 规范变更(开发时)
pnpm api:watch

2. 在 Service 中使用类型

import type { components } from '@/types/user-api'

type UserDTO = components['schemas']['UserDTO']

class UserService extends BaseService<UserDTO> {
  constructor() {
    super('/user/admin/user')
  }
}

3. 创建类型驱动表单

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)
  }
})

自定义扩展

添加自定义字段

<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