Files
erp/docs/superpowers/plans/2026-04-13-wasm-plugin-system-plan.md
iven ff352a4c24 feat(plugin): 集成 WASM 插件系统到主服务并修复链路问题
- 新增 erp-plugin crate:插件管理、WASM 运行时、动态表、数据 CRUD
- 新增前端插件管理页面(PluginAdmin/PluginCRUDPage)和 API 层
- 新增插件数据迁移(plugins/plugin_entities/plugin_event_subscriptions)
- 新增权限补充迁移(为已有租户补充 plugin.admin/plugin.list 权限)
- 修复 PluginAdmin 页面 InstallOutlined 图标不存在的崩溃问题
- 修复 settings 唯一索引迁移顺序错误(先去重再建索引)
- 更新 wiki 和 CLAUDE.md 反映插件系统集成状态
- 新增 dev.ps1 一键启动脚本
2026-04-15 23:32:02 +08:00

703 lines
21 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.

# WASM 插件系统实施计划
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为 ERP 平台引入 WASM 运行时插件系统,使行业模块可动态安装/启用/停用。
**Architecture:** 基础模块auth/config/workflow/message保持 Rust 编译时,新增 `erp-plugin-runtime` crate 封装 Wasmtime 运行时。插件通过宿主代理 API 访问数据库和事件总线,前端使用配置驱动 UI 渲染引擎自动生成 CRUD 页面。
**Tech Stack:** Rust + Wasmtime 27+ / WIT (wit-bindgen 0.24+) / SeaORM / Axum 0.8 / React 19 + Ant Design 6 + Zustand 5
**Spec:** `docs/superpowers/specs/2026-04-13-wasm-plugin-system-design.md`
---
## File Structure
### 新建文件
```
crates/erp-plugin-runtime/
├── Cargo.toml
├── wit/
│ └── plugin.wit # WIT 接口定义
└── src/
├── lib.rs # crate 入口
├── manifest.rs # plugin.toml 解析
├── engine.rs # Wasmtime 引擎封装
├── host_api.rs # 宿主 APIdb/event/config/log
├── loader.rs # 插件加载器
├── schema.rs # 动态建表逻辑
├── error.rs # 插件错误类型
└── wasm_module.rs # ErpModule trait 的 WASM 适配器
crates/erp-server/migration/src/
└── m20260413_000032_create_plugins_table.rs # plugins + plugin_schema_versions 表
crates/erp-server/src/
└── handlers/
└── plugin.rs # 插件管理 + 数据 CRUD handler
apps/web/src/
├── api/
│ └── plugins.ts # 插件 API service
├── stores/
│ └── plugin.ts # PluginStore (Zustand)
├── pages/
│ ├── PluginAdmin.tsx # 插件管理页面
│ └── PluginCRUDPage.tsx # 通用 CRUD 渲染引擎
└── components/
└── DynamicMenu.tsx # 动态菜单组件
```
### 修改文件
```
Cargo.toml # 添加 erp-plugin-runtime workspace member
crates/erp-core/src/module.rs # 升级 ErpModule trait v2
crates/erp-core/src/events.rs # 添加 subscribe_filtered
crates/erp-core/src/lib.rs # 导出新类型
crates/erp-auth/src/module.rs # 迁移到 v2 trait
crates/erp-config/src/module.rs # 迁移到 v2 trait
crates/erp-workflow/src/module.rs # 迁移到 v2 trait
crates/erp-message/src/module.rs # 迁移到 v2 trait
crates/erp-server/src/main.rs # 使用新注册系统 + 加载 WASM 插件
crates/erp-server/src/state.rs # 添加 PluginState
crates/erp-server/migration/src/lib.rs # 注册新迁移
apps/web/src/App.tsx # 添加动态路由 + PluginAdmin 路由
apps/web/src/layouts/MainLayout.tsx # 使用 DynamicMenu
```
---
## Chunk 1: ErpModule Trait v2 迁移 + EventBus 扩展
### Task 1: 升级 ErpModule trait
**Files:**
- Modify: `crates/erp-core/src/module.rs`
- Modify: `crates/erp-core/src/lib.rs`
- [ ] **Step 1: 升级 ErpModule trait — 添加新方法(全部有默认实现)**
`crates/erp-core/src/module.rs` 中,保留所有现有方法签名不变,追加新方法:
```rust
use std::collections::HashMap;
// 新增类型
pub enum ModuleType {
Native,
Wasm,
}
pub struct ModuleHealth {
pub status: String,
pub details: Option<String>,
}
pub struct ModuleContext {
pub db: sea_orm::DatabaseConnection,
pub event_bus: crate::events::EventBus,
pub config: Arc<serde_json::Value>,
}
// 在 ErpModule trait 中追加(不改现有方法):
fn id(&self) -> &str { self.name() } // 默认等于 name
fn module_type(&self) -> ModuleType { ModuleType::Native }
async fn on_startup(&self, _ctx: &ModuleContext) -> crate::error::AppResult<()> { Ok(()) }
async fn on_shutdown(&self) -> crate::error::AppResult<()> { Ok(()) }
async fn health_check(&self) -> crate::error::AppResult<ModuleHealth> {
Ok(ModuleHealth { status: "ok".into(), details: None })
}
fn public_routes(&self) -> Option<axum::Router> { None } // 需要 axum 依赖
fn protected_routes(&self) -> Option<axum::Router> { None }
fn migrations(&self) -> Vec<Box<dyn sea_orm_migration::MigrationTrait>> { vec![] }
fn config_schema(&self) -> Option<serde_json::Value> { None }
```
> **注意:** `on_tenant_created/deleted` 的签名暂不改动(加 ctx 参数是破坏性变更),在 Task 2 中单独处理。
- [ ] **Step 2: 升级 ModuleRegistry — 添加索引 + 拓扑排序 + build_routes**
在同一个文件中扩展 `ModuleRegistry`
```rust
impl ModuleRegistry {
pub fn get_module(&self, id: &str) -> Option<&Arc<dyn ErpModule>> { ... }
pub fn build_routes(&self) -> (axum::Router, axum::Router) {
// 遍历 modules收集 public_routes + protected_routes
}
fn topological_sort(&self) -> crate::error::AppResult<Vec<Arc<dyn ErpModule>>> {
// 基于 dependencies() 的 Kahn 算法拓扑排序
}
pub async fn startup_all(&self, ctx: &ModuleContext) -> crate::error::AppResult<()> {
// 按拓扑顺序调用 on_startup
}
pub async fn health_check_all(&self) -> HashMap<String, ModuleHealth> { ... }
}
```
- [ ] **Step 3: 更新 lib.rs 导出**
`crates/erp-core/src/lib.rs` 追加:
```rust
pub use module::{ModuleType, ModuleHealth, ModuleContext};
```
- [ ] **Step 4: 更新 erp-core Cargo.toml 添加 axum 依赖**
`crates/erp-core/Cargo.toml``[dependencies]` 添加:
```toml
axum = { workspace = true }
sea-orm-migration = { workspace = true }
```
- [ ] **Step 5: 运行 `cargo check --workspace` 确保现有模块编译通过(所有新方法有默认实现)**
- [ ] **Step 6: 迁移四个现有模块的 routes**
`erp-auth/src/module.rs``erp-config/src/module.rs``erp-workflow/src/module.rs``erp-message/src/module.rs`
-`pub fn public_routes<S>()` 关联函数改为 `fn public_routes(&self) -> Option<Router>` trait 方法
- 同样处理 `protected_routes`
- 添加 `fn id()` 返回与 `name()` 相同值
每个模块的改动模式:
```rust
// 之前: pub fn public_routes<S>() -> Router<S> where ... { Router::new().route(...) }
// 之后:
fn public_routes(&self) -> Option<axum::Router> {
Some(axum::Router::new().route("/auth/login", axum::routing::post(auth_handler::login)).route("/auth/refresh", axum::routing::post(auth_handler::refresh)))
}
fn protected_routes(&self) -> Option<axum::Router> { Some(...) }
fn id(&self) -> &str { "auth" } // 与 name() 相同
```
- [ ] **Step 7: 更新 main.rs 使用 build_routes**
`crates/erp-server/src/main.rs`
```rust
// 替换手动 merge 为:
let (public_mod, protected_mod) = registry.build_routes();
let public_routes = Router::new()
.merge(handlers::health::health_check_router())
.merge(public_mod) // 替代 erp_auth::AuthModule::public_routes()
.route("/docs/openapi.json", ...)
...;
let protected_routes = protected_mod // 替代手动 merge 四个模块
.merge(handlers::audit_log::audit_log_router())
...;
```
- [ ] **Step 8: 运行 `cargo check --workspace` 确认全 workspace 编译通过**
- [ ] **Step 9: 运行 `cargo test --workspace` 确认测试通过**
- [ ] **Step 10: Commit**
```
feat(core): upgrade ErpModule trait v2 with lifecycle hooks, route methods, and auto-collection
```
---
### Task 2: EventBus subscribe_filtered 扩展
**Files:**
- Modify: `crates/erp-core/src/events.rs`
- [ ] **Step 1: 添加类型化订阅支持**
`events.rs` 中扩展 `EventBus`
```rust
use std::sync::RwLock;
pub type EventHandler = Box<dyn Fn(DomainEvent) + Send + Sync>;
pub type SubscriptionId = Uuid;
pub struct EventBus {
sender: broadcast::Sender<DomainEvent>,
handlers: Arc<RwLock<HashMap<String, Vec<(SubscriptionId, EventHandler)>>>>,
}
impl EventBus {
pub fn subscribe_filtered(
&self,
event_type: &str,
handler: EventHandler,
) -> SubscriptionId {
let id = Uuid::now_v7();
let mut handlers = self.handlers.write().unwrap();
handlers.entry(event_type.to_string())
.or_default()
.push((id, handler));
id
}
pub fn unsubscribe(&self, id: SubscriptionId) {
let mut handlers = self.handlers.write().unwrap();
for (_, list) in handlers.iter_mut() {
list.retain(|(sid, _)| *sid != id);
}
}
}
```
修改 `broadcast()` 方法,在广播时同时分发给 `handlers` 中匹配的处理器。
- [ ] **Step 2: 运行 `cargo test --workspace`**
- [ ] **Step 3: Commit**
```
feat(core): add typed event subscription to EventBus
```
---
## Chunk 2: 数据库迁移 + erp-plugin-runtime Crate
### Task 3: 插件数据库表迁移
**Files:**
- Create: `crates/erp-server/migration/src/m20260413_000032_create_plugins_table.rs`
- Modify: `crates/erp-server/migration/src/lib.rs`
- [ ] **Step 1: 编写迁移文件**
创建 `plugins``plugin_schema_versions``plugin_event_subscriptions` 三张表DDL 参见 spec §7.1)。
- [ ] **Step 2: 注册到 lib.rs 的迁移列表**
- [ ] **Step 3: 运行 `cargo run -p erp-server` 验证迁移执行**
- [ ] **Step 4: Commit**
```
feat(db): add plugins, plugin_schema_versions, and plugin_event_subscriptions tables
```
---
### Task 4: 创建 erp-plugin-runtime crate 骨架
**Files:**
- Create: `crates/erp-plugin-runtime/Cargo.toml`
- Create: `crates/erp-plugin-runtime/src/lib.rs`
- Create: `crates/erp-plugin-runtime/src/error.rs`
- Create: `crates/erp-plugin-runtime/src/manifest.rs`
- Modify: `Cargo.toml` (workspace members)
- [ ] **Step 1: 创建 Cargo.toml**
```toml
[package]
name = "erp-plugin-runtime"
version.workspace = true
edition.workspace = true
[dependencies]
erp-core = { workspace = true }
wasmtime = "27"
wasmtime-wasi = "27"
wit-bindgen = "0.24"
serde = { workspace = true }
serde_json = { workspace = true }
toml = "0.8"
uuid = { workspace = true }
chrono = { workspace = true }
sea-orm = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true }
thiserror = { workspace = true }
axum = { workspace = true }
async-trait = { workspace = true }
```
- [ ] **Step 2: 更新根 Cargo.toml workspace members + dependencies**
- [ ] **Step 3: 实现 manifest.rs — PluginManifest 类型 + 解析**
定义 `PluginManifest``PluginInfo``PermissionSet``EntityDef``FieldDef``PageDef` 等结构体,实现 `fn parse(toml_str: &str) -> Result<PluginManifest>`
- [ ] **Step 4: 实现 error.rs**
```rust
#[derive(Debug, thiserror::Error)]
pub enum PluginError {
#[error("Manifest 解析失败: {0}")]
ManifestParse(String),
#[error("WASM 加载失败: {0}")]
WasmLoad(String),
#[error("Host API 错误: {0}")]
HostApi(String),
#[error("插件未找到: {0}")]
NotFound(String),
#[error("依赖未满足: {0}")]
DependencyUnmet(String),
}
```
- [ ] **Step 5: 实现 lib.rs — crate 入口 + re-exports**
- [ ] **Step 6: 运行 `cargo check --workspace`**
- [ ] **Step 7: Commit**
```
feat(plugin): create erp-plugin-runtime crate with manifest parsing
```
---
### Task 5: WIT 接口定义
**Files:**
- Create: `crates/erp-plugin-runtime/wit/plugin.wit`
- [ ] **Step 1: 编写 WIT 文件**
参见 spec 附录 D.1 的完整 `plugin.wit` 内容host interface + plugin interface + plugin-world
- [ ] **Step 2: 验证 WIT 语法**
```bash
cargo install wit-bindgen-cli
wit-bindgen rust ./crates/erp-plugin-runtime/wit/plugin.wit --out-dir /tmp/test-bindgen
```
- [ ] **Step 3: Commit**
```
feat(plugin): define WIT interface for host-plugin contract
```
---
## Chunk 3: Host API + 插件加载器
### Task 6: 实现 Host API 层
**Files:**
- Create: `crates/erp-plugin-runtime/src/host_api.rs`
- [ ] **Step 1: 实现 PluginHostState 结构体**
持有 `db``tenant_id``plugin_id``event_bus` 等上下文。实现 `db_insert``db_query``db_update``db_delete``db_aggregate` 方法,每个方法都:
1. 自动注入 `tenant_id` 过滤
2. 自动注入标准字段id, created_at 等)
3. 参数化 SQL 防注入
4. 自动审计日志
- [ ] **Step 2: 注册为 Wasmtime host functions**
使用 `wasmtime::Linker::func_wrap` 将 host_api 方法注册到 WASM 实例。
- [ ] **Step 3: 编写单元测试**
使用 mock 数据库测试 db_insert 自动注入 tenant_id、db_query 自动过滤。
- [ ] **Step 4: Commit**
```
feat(plugin): implement host API layer with tenant isolation
```
---
### Task 7: 实现插件加载器 + 动态建表
**Files:**
- Create: `crates/erp-plugin-runtime/src/engine.rs`
- Create: `crates/erp-plugin-runtime/src/loader.rs`
- Create: `crates/erp-plugin-runtime/src/schema.rs`
- Create: `crates/erp-plugin-runtime/src/wasm_module.rs`
- [ ] **Step 1: engine.rs — Wasmtime Engine 封装**
单例 Engine + Store 工厂方法配置内存限制64MB 默认、fuel 消耗限制。
- [ ] **Step 2: schema.rs — 从 manifest 动态建表**
`create_entity_table(db, entity_def)` 函数:生成 `CREATE TABLE IF NOT EXISTS plugin_{name} (...)` SQL包含所有标准字段 + tenant_id 索引。
- [ ] **Step 3: loader.rs — 从数据库加载 + 实例化**
`load_plugins(db, engine, event_bus) -> Vec<LoadedPlugin>`:查询 `plugins` 表中 status=enabled 的记录,实例化 WASM调用 init(),注册事件处理器。
- [ ] **Step 4: wasm_module.rs — WasmModule实现 ErpModule trait**
包装 WASM 实例,实现 ErpModule trait 的各方法(调用 WASM 导出函数)。
- [ ] **Step 5: 集成测试**
测试完整的 load → init → db_insert → db_query 流程(使用真实 PostgreSQL
- [ ] **Step 6: Commit**
```
feat(plugin): implement plugin loader with dynamic schema creation
```
---
### Task 8: 插件管理 API + 数据 CRUD API
**Files:**
- Create: `crates/erp-server/src/handlers/plugin.rs`
- Modify: `crates/erp-server/src/main.rs`
- Modify: `crates/erp-server/src/state.rs`
- [ ] **Step 1: 实现 plugin handler**
上传(解析 plugin.toml + 存储 wasm_binary、列表、详情、启用建表+写状态)、停用、卸载(软删除)。
- [ ] **Step 2: 实现插件数据 CRUD**
`GET/POST/PUT/DELETE /api/v1/plugins/{plugin_id}/{entity}` — 动态路由,从 manifest 查找 entity调用 host_api 执行操作。
- [ ] **Step 3: 注册路由到 main.rs**
- [ ] **Step 4: 添加 PluginState 到 state.rs**
```rust
impl FromRef<AppState> for erp_plugin_runtime::PluginState { ... }
```
- [ ] **Step 5: 运行 `cargo test --workspace`**
- [ ] **Step 6: Commit**
```
feat(server): add plugin management and dynamic CRUD API endpoints
```
---
## Chunk 4: 前端配置驱动 UI
### Task 9: PluginStore + API Service
**Files:**
- Create: `apps/web/src/api/plugins.ts`
- Create: `apps/web/src/stores/plugin.ts`
- [ ] **Step 1: plugins.ts API service**
接口类型定义 + API 函数:`listPlugins``getPlugin``uploadPlugin``enablePlugin``disablePlugin``uninstallPlugin``getPluginConfig``updatePluginConfig``getPluginData``createPluginData``updatePluginData``deletePluginData`
- [ ] **Step 2: plugin.ts PluginStore**
```typescript
interface PluginStore {
plugins: PluginInfo[];
loading: boolean;
fetchPlugins(): Promise<void>;
getPageConfigs(): PluginPageConfig[];
}
```
启动时调用 `fetchPlugins()` 加载已启用插件列表及页面配置。
- [ ] **Step 3: Commit**
```
feat(web): add plugin API service and PluginStore
```
---
### Task 10: PluginCRUDPage 通用渲染引擎
**Files:**
- Create: `apps/web/src/pages/PluginCRUDPage.tsx`
- [ ] **Step 1: 实现 PluginCRUDPage 组件**
接收 `PluginPageConfig` 作为 props渲染
- **SearchBar**: 从 `filters` 配置生成 Ant Design Form.Item 搜索条件
- **DataTable**: 从 `columns` 配置生成 Ant Design Table 列
- **FormDialog**: 从 `form` 配置或自动推导的 `schema.entities` 字段生成新建/编辑 Modal 表单
- **ActionBar**: 从 `actions` 配置生成操作按钮
API 调用统一走 `/api/v1/plugins/{plugin_id}/{entity}` 路径。
- [ ] **Step 2: Commit**
```
feat(web): implement PluginCRUDPage config-driven rendering engine
```
---
### Task 11: 动态路由 + 动态菜单
**Files:**
- Create: `apps/web/src/components/DynamicMenu.tsx`
- Modify: `apps/web/src/App.tsx`
- Modify: `apps/web/src/layouts/MainLayout.tsx`
- [ ] **Step 1: DynamicMenu 组件**
`usePluginStore` 读取 `getPageConfigs()`,按 `menu_group` 分组生成 Ant Design Menu.Item追加到侧边栏。
- [ ] **Step 2: App.tsx 添加动态路由**
在 private routes 中,遍历 PluginStore 的 pageConfigs为每个 CRUD 页面生成:
```tsx
<Route path={page.path} element={<PluginCRUDPage config={page} />} />
```
同时添加 `/plugin-admin` 路由指向 `PluginAdmin` 页面。
- [ ] **Step 3: MainLayout.tsx 集成 DynamicMenu**
替换硬编码的 `bizMenuItems`,追加插件动态菜单。
- [ ] **Step 4: 运行 `pnpm dev` 验证前端编译通过**
- [ ] **Step 5: Commit**
```
feat(web): add dynamic routing and menu generation from plugin configs
```
---
### Task 12: 插件管理页面
**Files:**
- Create: `apps/web/src/pages/PluginAdmin.tsx`
- [ ] **Step 1: 实现 PluginAdmin 页面**
包含插件列表Table、上传按钮Upload、启用/停用/卸载操作、配置编辑 Modal。使用 Ant Design 组件。
- [ ] **Step 2: Commit**
```
feat(web): add plugin admin page with upload/enable/disable/configure
```
---
## Chunk 5: 第一个行业插件(进销存)
### Task 13: 创建 erp-plugin-inventory
**Files:**
- Create: `crates/plugins/inventory/Cargo.toml`
- Create: `crates/plugins/inventory/plugin.toml`
- Create: `crates/plugins/inventory/src/lib.rs`
- [ ] **Step 1: 创建插件项目**
`Cargo.toml` crate-type = ["cdylib"],依赖 wit-bindgen + serde + serde_json。
- [ ] **Step 2: 编写 plugin.toml**
完整清单spec §4 的进销存示例inventory_item、purchase_order 两个 entity3 个 CRUD 页面 + 1 个 custom 页面。
- [ ] **Step 3: 实现 lib.rs**
使用 wit-bindgen 生成的绑定,实现 `init()``on_tenant_created()``handle_event()`
- [ ] **Step 4: 编译为 WASM**
```bash
rustup target add wasm32-unknown-unknown
cargo build -p erp-plugin-inventory --target wasm32-unknown-unknown --release
```
- [ ] **Step 5: Commit**
```
feat(inventory): create erp-plugin-inventory as first industry plugin
```
---
### Task 14: 端到端集成测试
- [ ] **Step 1: 启动后端服务**
```bash
cd docker && docker compose up -d
cd crates/erp-server && cargo run
```
- [ ] **Step 2: 通过 API 上传进销存插件**
```bash
# 打包
cp target/wasm32-unknown-unknown/release/erp_plugin_inventory.wasm /tmp/
# 上传
curl -X POST http://localhost:3000/api/v1/admin/plugins/upload \
-F "wasm=@/tmp/erp_plugin_inventory.wasm" \
-F "manifest=@crates/plugins/inventory/plugin.toml"
```
- [ ] **Step 3: 启用插件 + 验证建表**
```bash
curl -X POST http://localhost:3000/api/v1/admin/plugins/erp-inventory/enable
docker exec erp-postgres psql -U erp -c "\dt plugin_*"
```
- [ ] **Step 4: 测试 CRUD API**
```bash
curl -X POST http://localhost:3000/api/v1/plugins/erp-inventory/inventory_item \
-H "Authorization: Bearer $TOKEN" \
-d '{"sku":"ITEM001","name":"测试商品","quantity":100}'
curl http://localhost:3000/api/v1/plugins/erp-inventory/inventory_item \
-H "Authorization: Bearer $TOKEN"
```
- [ ] **Step 5: 前端验证**
启动 `pnpm dev`,验证:
- 侧边栏出现"进销存"菜单组 + 子菜单
- 点击"商品管理"显示 PluginCRUDPage
- 可以新建/编辑/删除/搜索商品
- [ ] **Step 6: 测试停用 + 卸载**
```bash
curl -X POST http://localhost:3000/api/v1/admin/plugins/erp-inventory/disable
curl -X DELETE http://localhost:3000/api/v1/admin/plugins/erp-inventory
# 验证数据表仍在
docker exec erp-postgres psql -U erp -c "\dt plugin_*"
```
- [ ] **Step 7: Commit**
```
test(inventory): end-to-end integration test for plugin lifecycle
```
---
## 执行顺序
```
Chunk 1 (Tasks 1-2) ← 先做,所有后续依赖 trait v2 和 EventBus 扩展
Chunk 2 (Tasks 3-5) ← 数据库表 + crate 骨架 + WIT
Chunk 3 (Tasks 6-8) ← 核心运行时 + API后端完成
Chunk 4 (Tasks 9-12) ← 前端(可与 Chunk 5 并行)
Chunk 5 (Tasks 13-14) ← 第一个插件 + E2E 验证
```
## 关键风险
| 风险 | 缓解 |
|------|------|
| Wasmtime 版本与 WIT 不兼容 | 锁定 wasmtime = "27"CI 验证 |
| axum Router 在 erp-core 中引入重依赖 | 考虑将 trait routes 方法改为返回路由描述结构体,在 erp-server 层构建 Router |
| 动态建表安全性 | 仅允许白名单列类型,禁止 DDL 注入 |
| 前端 PluginCRUDPage 覆盖不足 | 先支持 text/number/date/select/currencycustom 页面后续迭代 |