1323 lines
32 KiB
Markdown
1323 lines
32 KiB
Markdown
# 前后端智能协作方案实现计划
|
||
|
||
> **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)
|