@complex-suite/data 快速使用
@complex-suite/data 是 complex-suite 的核心数据层。它不渲染 UI,只负责描述业务页面的数据结构、搜索条件、字典字段、分页、选择、编辑、详情、异步加载和业务操作。
依赖链:utils → plugin → data → component → component-antd。业务页面通过 data 创建无头数据模型,再交给 component-antd 的 ComplexQuickList / ComplexQuickCascade 渲染。
安装
pnpm add @complex-suite/data
核心导出
import {
ComplexList, ComplexInfo, SelectData, TrackData,
Data, SimpleData, DefaultData, BaseData, ComplexData,
ModuleData, DictionaryData, SearchData, PaginationData,
ChoiceData, SortData, UpdateData, StatusData, DependData, PromiseData, ResetData,
DictionaryValue, ArrayValue, SelectValue, CascaderValue, FormValue,
StorageValue, ForceValue, InterfaceValue, AttrsValue, TipValue, FileValue,
GridParse, LayoutParse,
DefaultList, DefaultInfo, DefaultEdit, InputEdit, InputNumberEdit,
TextAreaEdit, SelectEdit, SwitchEdit, DateEdit, DateRangeEdit, SimpleDateEdit,
FileEdit, ButtonEdit, ButtonGroupEdit, ContentEdit, CustomEdit, CustomLoadEdit,
FormEdit, ListEdit, ObserveList, DefaultMod, DefaultSimpleEdit, DefaultLoadEdit, DefaultSelectEdit,
dataConfig, dataLocale
} from '@complex-suite/data'
数据类继承链
Data (utils._Data)
└─ SimpleData # $extra 额外数据容器
└─ DefaultData # $prop / $life 生命周期 / $storage
└─ BaseData # $status / $promise / $depend / $module / loadData / reset / destroy
└─ ComplexData # 业务回调(getData/buildData/changeData/...) + 模块代理方法
├─ ComplexList # $list 列表页主模型
└─ ComplexInfo # $info 详情页模型
每一层通过 _triggerCreateLife('ClassName', false/true, initOption) 触发 beforeCreate/created 生命周期。
模块系统
ComplexList 通过 module 挂载功能模块:
dictionary | DictionaryData | 字段字典管理 | ✅ |
search | SearchData | 检索表单与菜单 | - |
pagination | PaginationData | 分页参数与总数 | - |
choice | ChoiceData | 多选状态 | - |
sort | SortData | 排序参数 | - |
update | UpdateData | 定时刷新 | - |
depend | DependData | 依赖加载 | - |
status | StatusData | 状态机 | 内置 |
promise | PromiseData | Promise 缓存 | 内置 |
快速开始:标准列表页
下面示例提炼自真实业务页面 src/pages/index/views/list/base/index.vue。
<template>
<div class="local-page">
<ComplexQuickList
:list-data="mainData"
:components="['spin', 'search', 'table', 'edit', 'info']"
:simple-table="false"
:components-props="componentsProps"
@search="onSearch"
@table="onTable"
/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { ButtonEdit, ComplexList, SelectEdit, SelectValue } from '@complex-suite/data'
import { ComplexQuickList, type componentsProps, type tablePayload } from '@complex-suite/component-antd'
import listApi from '@/api/listApi'
export default defineComponent({
name: 'RecordList',
components: { ComplexQuickList },
setup() {
// 1) 静态选项数据源
const select = new SelectValue({
list: [
{ value: 0, label: '选项1' },
{ value: 1, label: '选项2' },
{ value: 2, label: '选项3' }
]
})
// 2) 创建列表数据模型
const mainData = new ComplexList({
prop: 'mainData',
module: {
// 搜索区:菜单 + 搜索字段
search: {
menu: {
list: [
'build', // 内置:新增按钮
'delete', // 内置:批量删除按钮
new ButtonEdit({ // 自定义按钮
prop: 'choice2',
type: 'button',
option: {
type: 'default',
name: () => '已选择2条时可用',
disabled(payload: any) {
return payload.choice !== 2
},
click() {
return new Promise((resolve) => setTimeout(() => resolve({}), 2000))
}
}
}),
new ButtonEdit({ // 文件上传按钮
prop: '$import',
type: 'button',
option: {
type: 'default',
name: () => '导入',
upload: () => new Promise((resolve) => setTimeout(() => resolve({}), 3000))
}
})
]
},
list: [
{
prop: 'input',
name: '输入框',
mod: {
search: { $format: 'edit', type: 'input', collapse: 2, option: { size: 20 } }
}
},
{
prop: 'search',
name: '检索框',
mod: {
search: {
$format: 'edit',
type: 'select',
cascader: undefined, // 显式声明无级联
width: 100,
reload: true, // 值变化时重新加载选项
search: {},
// 远程加载选项
getData(this: SelectEdit) {
return new Promise((resolve) => {
this.$select.setList([
{ value: this.$search?.value, label: this.$search?.value || '' },
{ value: 1, label: '1' },
{ value: 2, label: '2' }
])
resolve({})
})
}
}
}
}
]
},
// 字典:字段定义(同时服务 list/info/edit/build/change)
dictionary: {
propData: { id: 'id' },
list: [
{ prop: 'menu', name: '操作', originFrom: 'local', mod: { list: { width: 140 } } },
{ prop: '$index', name: 'No', originFrom: 'local', mod: { list: { width: 60 } } },
{ prop: 'id', name: 'ID', mod: { list: { width: 80 }, info: {} } },
{
prop: 'input',
name: '输入框',
mod: {
list: { width: 100 },
info: {},
edit: { type: 'input', required: true, option: { size: 20 } },
build: { $redirect: 'edit' },
change: { $redirect: 'edit' }
}
},
{
prop: 'switch',
name: '开关',
assign(value) { return value === 1 },
collect(value) { return value ? 1 : 0 },
mod: {
list: { width: 70 },
edit: { type: 'switch', required: true },
build: { $redirect: 'edit' },
change: { $redirect: 'edit' }
}
},
{
prop: 'select',
name: '选择器',
showProp: { default: 'value', list: 'label' },
assign(value) { return select.getItem(value) },
mod: {
list: { width: 100 },
edit: {
type: 'select',
cascader: undefined,
required: true,
select: select,
pagination: {},
getData(this: any) {
return new Promise((resolve) => {
setTimeout(() => {
this.$select.setList(select.getList())
this.$pagination.setCount(100)
resolve({})
}, 200)
})
}
},
build: { $redirect: 'edit' },
change: { $redirect: 'edit' }
}
},
{
prop: 'timeRange',
name: '时间范围',
assign(value) { return (value as string).split(',') },
collect(value) { return (value as string[]).join(',') },
mod: {
list: { width: 300 },
edit: {
type: 'dateRange',
required: true,
option: {
time: {},
rangeLimit: { value: 5 * 24 * 60 * 60 },
disabledDate: {
start: { value: 'today', eq: true },
end: { value: 'tomorrow', eq: true }
}
}
},
build: { $redirect: 'edit' },
change: { $redirect: 'edit' }
}
}
]
},
pagination: true
},
// 业务回调:均要求返回 Promise,this 绑定到当前实例
getData(this: ComplexList) {
return new Promise((resolve, reject) => {
const postData = {
...this.getSearch(),
page: this.getPage(),
size: this.getPageSize()
} as any
listApi.baseList.require(postData).then(res => {
this.formatList(res.data.data.list, res.data.data.num)
resolve(res)
}).catch(reject)
})
},
buildData(this: ComplexList, targetData) {
return new Promise((resolve, reject) => {
listApi.baseBuild.require(targetData).then(res => {
this.reloadData(true)
resolve(res)
}).catch(reject)
})
},
changeData(this: ComplexList, targetData, originData) {
return new Promise((resolve, reject) => {
targetData.id = originData.id
listApi.baseChange.require(targetData).then(res => {
this.reloadData(true)
resolve(res)
}).catch(reject)
})
},
deleteData(this: ComplexList, targetData) {
return new Promise((resolve) => {
const index = this.$list.indexOf(targetData)
if (index > -1) this.$list.splice(index, 1)
resolve({})
})
},
multipleDeleteData(this: ComplexList, choiceList) {
return new Promise((resolve) => {
choiceList.forEach(item => {
const index = this.$list.indexOf(item)
if (index > -1) this.$list.splice(index, 1)
})
resolve({})
})
}
})
const onSearch = function(_prop: string) {}
const onTable = function(_prop: string, _payload: tablePayload) {}
return {
mainData: mainData,
onSearch: onSearch,
onTable: onTable,
componentsProps: {
table: {
menu: {
menu: [
{ prop: '$change', name: () => '编辑', color: 'link' },
{ prop: '$delete', name: () => '删除', color: 'danger' },
{ prop: '$info', name: () => '详情', color: 'link' }
]
}
},
edit: {}
} as componentsProps
}
},
mounted() {
this.mainData.loadData(true)
}
})
</script>
字典字段配置
module.dictionary.list 是核心协议。一个字段可同时服务多个场景:
{
prop: 'name',
name: '名称',
originFrom: 'local',
showProp: {
default: 'value',
list: 'label'
},
assign(value) { ... },
collect(value) { ... },
dictionary: { list: [...] },
mod: {
list: { width: 100 },
info: {},
edit: { type: 'input', required: true },
build: { $redirect: 'edit' },
change: { $redirect: 'edit' },
search: { $format: 'edit', type: 'input' }
}
}
编辑类型对照
input | InputEdit | 文本输入 |
textArea | TextAreaEdit | 多行文本 |
inputNumber | InputNumberEdit | 数字输入 |
select | SelectEdit / DefaultSelectEdit | 选择器 |
switch | SwitchEdit | 开关 |
date | DateEdit / SimpleDateEdit | 日期 |
dateRange | DateRangeEdit | 日期范围 |
file | FileEdit | 文件上传 |
button | ButtonEdit | 按钮(搜索区/自定义操作) |
form | FormEdit | 内嵌表单 |
list | ListEdit | 内嵌列表 |
content | ContentEdit | 纯展示 |
custom | CustomEdit / CustomLoadEdit | 自定义渲染 |
搜索区配置
search: {
menu: {
list: [
'build',
'delete',
new ButtonEdit({ ... })
]
},
list: [
{
prop: 'keyword',
name: '关键字',
mod: {
search: { $format: 'edit', type: 'input', collapse: 2 }
}
}
]
}
ComplexQuickList 内置菜单 prop:
$search | 调用 listData.setSearch() |
$reset | 调用 listData.resetSearch() |
$refresh | 调用 listData.reloadData({ data: true, sync: true }) |
$build | 打开新增弹窗 |
$delete | 触发批量删除(基于 choice) |
$info | 打开详情弹窗 |
$export | 调用 listData.triggerMethod('exportData') |
$import / $image | 文件上传按钮(通过 upload 回调) |
ButtonEdit 区分两种语义:
click():普通点击回调
upload(file: File):文件上传专用回调
数据加载与业务回调
所有业务方法都要求返回 Promise,this 绑定到当前实例:
interface ComplexDataInitOption {
module: ModuleDataInitOption
getData?: (this: ComplexList) => Promise<any>
buildData?: (this: ComplexList, targetData) => Promise<any>
changeData?: (this: ComplexList, targetData, originData) => Promise<any>
editData?: (this: ComplexList, targetData, originData) => Promise<any>
deleteData?: (this: ComplexList, targetData) => Promise<any>
multipleDeleteData?: (this: ComplexList, choiceList) => Promise<any>
refreshData?: (this: ComplexList, targetData) => Promise<any>
updateData?: (this: ComplexList) => Promise<any>
exportData?: (this: ComplexList) => Promise<any>
importData?: (this: ComplexList, file: File) => Promise<any>
}
回调内可直接访问 this.$list、this.reloadData()、this.getSearch() 等。修改类方法(buildData/changeData/deleteData)成功后会自动触发 dataChange 生命周期。
loadData 的 force 参数
listData.loadData(true)
listData.loadData(false)
listData.loadData({ data: true, ing: true })
listData.loadData({
data: true,
ing: true,
promise: true,
sync: true,
trigger: { from: 'search', action: 'set' },
module: { pagination: true }
})
常用方法
loadData(force?, ...args) | 加载数据 |
reloadData(force?, ...args) | 重新加载,触发 beforeReload/reloaded/reloadFail |
formatList(list, total) | 写入 $list,同步分页总数 |
getSearch() | 获取搜索区当前值 |
getPage() / getPageSize() | 获取当前分页参数 |
setPage(page) / setPageSize(size) | 修改分页参数 |
setSearch(action?) | 校验搜索区并触发 reload |
resetSearch(option?) | 重置搜索条件并触发 setSearch('reset') |
triggerMethod(method, args, option) | 通过状态机触发业务方法 |
setSort(prop, order) | 设置排序 |
setChoice(data) / resetChoice() | 多选操作 |
字段转换:assign 与 collect
接口值与表单值往往存在差异,字典字段提供两个转换点:
{
prop: 'switch',
assign(value) { return value === 1 },
collect(value) { return value ? 1 : 0 }
}
assign:在 getData 成功后、数据进入 $list/$info/表单前调用
collect:在 buildData/changeData 提交前调用
嵌套字典(FormEdit / ListEdit)
字段可嵌套完整字典,用于内嵌表单/列表:
{
prop: 'form',
name: '内嵌表单',
dictionary: {
list: [
{
prop: 'name',
name: '名称',
mod: {
edit: { type: 'input', required: true },
build: { $redirect: 'edit' },
change: { $redirect: 'edit' }
}
}
]
},
mod: {
list: { width: 300 },
info: { $redirect: 'edit' },
edit: {
grid: {
line: 1,
custom(data, position, _gridParse) {
if (position === 'label') return { span: 0 }
if (position === 'content') return { span: 24 }
return data
}
},
type: 'form',
required: true,
option: {}
},
build: { $redirect: 'edit' },
change: { $redirect: 'edit' }
}
}
生命周期
listData.onLife('loaded', {
id: 'myHandler',
handler: (lifeValue, res) => {
console.log('加载完成', res)
}
})
listData.offLife('loaded', 'myHandler')
listData.onLoaded({
handler: (lifeValue) => { }
})
常用生命周期:
beforeCreate / created | 实例构造前后 |
beforeLoad | loadData 开始 |
loaded | 加载成功 |
loadFail | 加载失败 |
beforeReload / reloaded / reloadFail | reloadData 各阶段 |
beforeSearch | setSearch 校验前 |
searched / searchFail | 检索成功/失败 |
beforeUpdate / updated / updateFail | 更新数据各阶段 |
beforeReset / reseted | reset 前后 |
beforeDestroy / destroyed | destroy 前后 |
dataChange | 业务方法成功后(buildData/changeData/deleteData/...) |
parentChange | $setParent 时 |
远程选择器
选择器可使用静态 SelectValue,也可在 SelectEdit.getData 中远程加载:
{
prop: 'search',
mod: {
search: {
$format: 'edit',
type: 'select',
cascader: undefined,
reload: true,
search: { limit: 3, limitContent: '限制3个' },
getData(this: SelectEdit) {
return new Promise((resolve) => {
this.$select.setList([
{ value: this.$search?.value, label: this.$search?.value || '' },
{ value: 1, label: '1' }
])
resolve({})
})
}
}
}
}
远程接口有分页时,在 getData 中同步调用 this.$pagination.setCount(total)。
级联列表
主列表通过表格菜单暴露 $sublist,点击打开子列表弹窗,子列表用自己的 ComplexList 加载:
const parentData = new ComplexList({
prop: 'parentData',
module: { dictionary: { list: [...] }, pagination: true },
getData(this: ComplexList) {
return new Promise((resolve, reject) => {
listApi.parentList.require({
...this.getSearch(),
page: this.getPage(),
size: this.getPageSize()
}).then(res => {
this.formatList(res.data.data.list, res.data.data.num)
resolve(res)
}).catch(reject)
})
}
})
const childData = new ComplexList({
prop: 'childData',
module: { dictionary: { list: [...] }, pagination: true },
getData(this: ComplexList) {
const parentId = this.getExtra('parentId')
return new Promise((resolve, reject) => {
listApi.childList.require({
parentId,
page: this.getPage(),
size: this.getPageSize()
}).then(res => {
this.formatList(res.data.data.list, res.data.data.num)
resolve(res)
}).catch(reject)
})
}
})
const showSubList = function(targetData: any) {
childData.setExtra('parentId', targetData.id)
childData.reloadData(true)
}
$extra 是不参与字典/搜索/分页的额外数据容器,通过 setExtra / getExtra / clearExtra 操作。
TypeScript 泛型
interface UserItem { id: number; name: string }
interface UserVO { id: number; name: string; deptName: string }
const listData = new ComplexList<UserItem, UserVO>({
prop: 'userList',
module: { dictionary: { list: [] }, pagination: true },
getData(this: ComplexList<UserItem, UserVO>) {
return fetch('/api/users').then(r => r.json()).then(res => {
this.formatList(res.list, res.total)
return res
})
}
})
listData.$list
const infoData = new ComplexInfo<UserVO>({
prop: 'userInfo',
module: { dictionary: { list: [...] } },
getData(this: ComplexInfo<UserVO>) {
return fetch(`/api/users/${this.$extra.id}`).then(r => r.json()).then(res => {
this.formatInfo(res.data)
return res
})
}
})
infoData.$info
SelectValue 与级联
import { SelectValue, CascaderValue } from '@complex-suite/data'
const select = new SelectValue({
list: [{ value: 1, label: 'A' }, { value: 2, label: 'B' }],
equal: (a, b) => a.value === b.value,
hidden: true
})
const cascader = new CascaderValue({
list: [
{
value: 'zhejiang', label: '浙江',
children: [
{ value: 'hangzhou', label: '杭州' },
{ value: 'ningbo', label: '宁波' }
]
}
],
cascader: 'children'
})
重置与销毁
listData.reset()
listData.reset({ status: false, pagination: true })
listData.destroy()
listData.destroy({ life: true, depend: true })
resetOption 通过 parseResetOption(option, prop) 解析,支持 true / false / undefined / { [prop]: boolean }。
全局配置与国际化
import { dataConfig, dataLocale, type DataMessages } from '@complex-suite/data'
const primary = dataConfig.style.color.primary
dataLocale.use('en')
dataLocale.t('baseData.methodNotExist', { method: 'foo' })
const customMessages: Partial<DataMessages> = {
zh: { baseData: { methodNotExist: '方法 {method} 不存在' } }
}
dataLocale.inject(customMessages)
推荐实践
- 列表页优先用
ComplexList 作为唯一业务状态入口,UI 组件只消费它
- 字段配置集中在
module.dictionary.list,通过 list/info/edit/build/change 复用同一字段语义
- 所有业务回调返回
Promise,包括 getData/buildData/changeData/deleteData/自定义 click/上传 upload
- 接口值进入表单前用
assign,提交前用 collect,避免转换逻辑散落在 UI 组件
- 新增和修改表单大多可通过
$redirect: 'edit' 复用配置
- 远程选择器在
getData 中同步调用 this.$pagination.setCount(total)
- 用
onLife/offLife 监听关键生命周期,避免 setTimeout 等待异步
force 参数优先用对象形式({ data: true, ing: true }),可读性优于布尔简写
data 只负责无头数据协议,渲染细节交给 component-antd 的 ComplexQuickList/TableView/EditArea/InfoArea
依赖
@complex-suite/utils:核心工具函数和基类(Life、Watcher、setProp 等)
@complex-suite/plugin:布局和通知系统