188 lines
6.7 KiB
Markdown
188 lines
6.7 KiB
Markdown
# 平台化架构设计
|
||
|
||
## 目标
|
||
|
||
将 Painting / Video 两套硬编码分支重构为统一的平台描述符架构,使加入新平台只需新建一个文件夹并实现标准接口,零改动 dialogBox。
|
||
|
||
## 背景
|
||
|
||
当前 `dialogBox/index.vue`(792 行)通过 `v-if="type === 'Painting'"` / `v-if="type === 'Video'"` 承载两套完全不同的逻辑:
|
||
|
||
- **模型列表**:Painting 走后端 API,Video 走静态 JSON
|
||
- **参数 schema**:Painting 走本地 JS 文件,Video 走远程 workflow JSON
|
||
- **UI 控件**:Painting 有 proportion/dimension/quality/quantity,Video 有 pattern/proportion/time
|
||
- **任务 body**:Painting 扁平 modelParams,Video `{ workflowId, nodeInfoList }`
|
||
|
||
后续模型参数后端化后,所有平台将统一为"API 获取模型列表 + API 获取参数 schema"模式。
|
||
|
||
## 核心设计:平台描述符模式
|
||
|
||
每个平台封装为一个文件夹,导出 `definePlatform()` 工厂函数,返回标准接口对象。dialogBox 退化为纯渲染引擎。
|
||
|
||
### 平台接口
|
||
|
||
```js
|
||
// src/platforms/<name>/index.js
|
||
export function definePlatform() {
|
||
// 响应式状态(各平台自定义)
|
||
const model = ref(defaultValue)
|
||
const modelType = ref(defaultValue)
|
||
const state = reactive({ ... })
|
||
|
||
// 模型选择器组件
|
||
const ModelSelector = markRaw(Component)
|
||
|
||
// 参数控件列表(有序,决定渲染顺序)
|
||
const controls = [
|
||
{
|
||
name: 'proportion',
|
||
component: markRaw(ProportionComponent),
|
||
show: (config) => config?.params?.some(p => p.ui === 'proportion'),
|
||
props: (config) => ({ /* 额外 props */ }),
|
||
},
|
||
// ...
|
||
]
|
||
|
||
// 图片上传器(可选)
|
||
const ImageUploader = markRaw(Component) | null
|
||
|
||
return {
|
||
id: 'painting', // 平台标识
|
||
label: 'AI绘画2026', // 显示标题
|
||
ModelSelector, // 模型选择器组件
|
||
controls, // 有序控件列表
|
||
ImageUploader, // 图片上传器组件(可选)
|
||
state, // 平台自定义响应式状态
|
||
model, // 当前模型 ref
|
||
modelType, // 当前模型类型 ref
|
||
|
||
async loadModels() { }, // 获取模型列表
|
||
async loadConfig(modelName) { }, // 获取模型参数配置
|
||
buildTaskBody(state) { }, // 构造请求 body
|
||
getDefaultModel() { }, // 默认模型名称
|
||
isImageRequired(state) { }, // 是否必须上传图片
|
||
}
|
||
}
|
||
```
|
||
|
||
### 控件绑定约定
|
||
|
||
dialogBox 渲染控件时自动处理 `name` 与 `state` 的 v-model 绑定:
|
||
|
||
- `modelValue` → `state[name]`
|
||
- `onUpdate:modelValue` → `state[name] = v`
|
||
|
||
控件 descriptor 仅需定义 `show` 条件(基于 modelConfig 上下文)和额外 `props`。
|
||
|
||
### 平台注册表
|
||
|
||
```js
|
||
// src/platforms/registry.js
|
||
import { definePaintingPlatform } from './painting/index.js'
|
||
import { defineVideoPlatform } from './video/index.js'
|
||
|
||
const registry = { Painting, Video }
|
||
|
||
export function createPlatform(type) {
|
||
const factory = registry[type]
|
||
if (!factory) throw new Error(`未找到平台: ${type}`)
|
||
return factory()
|
||
}
|
||
```
|
||
|
||
### dialogBox 角色变化
|
||
|
||
dialogBox 接收 `type` prop → 调用 `createPlatform(type)` → 获得 descriptor → 据 descriptor 渲染一切:
|
||
|
||
1. 渲染 `<component :is="platform.ModelSelector">`
|
||
2. 遍历 `platform.controls`,`show` 返回 true 的渲染 `<component :is>`
|
||
3. `handleStart()` 委托给 `platform.buildTaskBody(state)` → 调用 `taskPolling.generate(body)`
|
||
4. `watch(model)` 委托给 `platform.loadConfig(name)`
|
||
|
||
## 数据流
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
subgraph dialogBox["dialogBox 编排层"]
|
||
A["platform.loadModels()"] --> B[模型选择器渲染]
|
||
B --> C["watch(model) → platform.loadConfig(name)"]
|
||
C --> D[计算 visibleControls]
|
||
D --> E["v-for 渲染 controls(自动 v-model)"]
|
||
E --> F["handleStart → platform.buildTaskBody()"]
|
||
F --> G["taskPolling.generate(body)"]
|
||
end
|
||
|
||
subgraph platform["platform 包"]
|
||
H["loadModels() → API"]
|
||
I["loadConfig(name) → API"]
|
||
J["buildTaskBody() → 扁平 body"]
|
||
end
|
||
|
||
G --> K[POST /suanli/v1/tasks]
|
||
K --> L[轮询 → displayStore 更新虚拟滚动列表]
|
||
```
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
src/
|
||
├── platforms/ # 平台包(新增)
|
||
│ ├── painting/
|
||
│ │ ├── index.js # definePlatform()
|
||
│ │ ├── modelSelector.vue
|
||
│ │ ├── imageUploader.vue
|
||
│ │ └── controls/
|
||
│ │ ├── proportion.vue
|
||
│ │ ├── dimension.vue
|
||
│ │ ├── quality.vue
|
||
│ │ └── quantity.vue
|
||
│ ├── video/
|
||
│ │ ├── index.js
|
||
│ │ ├── modelSelector.vue
|
||
│ │ ├── imageUploader.vue
|
||
│ │ └── controls/
|
||
│ │ ├── pattern.vue
|
||
│ │ ├── proportion.vue
|
||
│ │ └── time.vue
|
||
│ └── registry.js
|
||
│
|
||
├── components/
|
||
│ ├── dialogBox/index.vue # 精简后 ~200 行
|
||
│ ├── Popover/ # 共享基础组件(不变)
|
||
│ ├── Select/ # 共享基础组件(不变)
|
||
│ ├── Img/ # 共享基础组件(不变)
|
||
│ └── virtual-scroller/ # 共享基础组件(不变)
|
||
│
|
||
├── apis/ # API 层(不变)
|
||
├── utils/
|
||
│ ├── taskPolling.js # 任务轮询(不变)
|
||
│ ├── request.js # Axios(不变)
|
||
│ └── modelApi.js # 平台 API 封装
|
||
│
|
||
├── config/
|
||
│ ├── models/ # 逐步废弃
|
||
│ ├── runninghub/ # 逐步废弃
|
||
│ └── plugins.js # 不变
|
||
```
|
||
|
||
### 迁移路径
|
||
|
||
| 步骤 | 内容 | 影响范围 |
|
||
|------|------|----------|
|
||
| 1 | 新建 `src/platforms/` + `registry.js`,不删旧代码 | 纯新增 |
|
||
| 2 | Painting 迁入 descriptor,dialogBox 切换读取路径 | dialogBox 精简 |
|
||
| 3 | Video 迁入 descriptor | dialogBox 继续精简 |
|
||
| 4 | 删除旧代码:`config/models/`、`config/runninghub/`、`modelConfig.js` | 清理 |
|
||
| 5 | 后端化:各平台 `loadConfig()` 改为调 API | 仅改 descriptor 内部 |
|
||
|
||
每步独立提交,方便回滚。
|
||
|
||
## 不变更的部分
|
||
|
||
- `taskPolling.js`:任务创建和轮询逻辑通用,不变
|
||
- `displayStore`:虚拟滚动列表状态通用,不变
|
||
- `Popover`、`Select`、`Img`、`virtual-scroller`:共享 UI 组件
|
||
- `home/index.vue`:仍然传 `type` prop,不变
|
||
- `apis/`、`request.js`:HTTP 层不变
|
||
- `config/plugins.js`、router、stores:不变
|