32 KiB
前后端智能协作方案实现计划
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: 测试表单功能
- 打开用户管理页面
- 点击"新增用户"
- 验证表单字段是否正确显示
- 提交表单,验证数据是否正确
- 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>
添加新模块
- 在
API设计规范.md中添加 api-docs 地址 - 运行
pnpm api:generate - 类型文件自动生成
故障排除
类型生成失败
- 检查后端服务是否启动
- 检查
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: 文档和配置
成功标准
- ✅
pnpm api:generate成功生成所有模块类型 - ✅ UserFormDialog 使用新方案正常运行
- ✅ 表单字段与后端 API 类型一致
- ✅ 提交时自动检测 API 变更
- ✅ 团队可以使用新方案开发新功能
下一步: 执行计划(使用 superpowers-subagent-driven-development 或 superpowers-executing-plans)