@complex-suite/data
v5.0.22
Published
a complex data
Readme
@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', // 数据来源:local / search / data
showProp: { // 不同场景下展示的字段
default: 'value',
list: 'label'
},
assign(value) { ... }, // 接口值 → 展示/表单值 转换
collect(value) { ... }, // 表单值 → 提交值 转换
dictionary: { list: [...] }, // 嵌套字典(type: 'form'/'list' 时使用)
mod: {
list: { width: 100 }, // 表格列配置
info: {}, // 详情字段配置
edit: { type: 'input', required: true },// 通用编辑配置
build: { $redirect: 'edit' }, // 新增表单:复用 edit
change: { $redirect: 'edit' }, // 修改表单:复用 edit
search: { $format: 'edit', type: 'input' } // 搜索字段
}
}编辑类型对照
| type | 字典类 | 适用场景 |
|---|---|---|
| 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:
| 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, // 强制触发 promise
sync: true, // 等待 promise 完成
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',
// 接口值(number)→ 展示/表单值(boolean)
assign(value) { return value === 1 },
// 表单值(boolean)→ 提交值(number)
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')
// onLoaded 是 loaded 的快捷监听,若当前已加载完成会立即触发
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 // 类型为 UserItem[]
// 详情页
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 // 类型为 Partial<UserVO>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
})
// 级联选择器(cascader 指定子字段名)
const cascader = new CascaderValue({
list: [
{
value: 'zhejiang', label: '浙江',
children: [
{ value: 'hangzhou', label: '杭州' },
{ value: 'ningbo', label: '宁波' }
]
}
],
cascader: 'children'
})重置与销毁
// 重置(不清除生命周期、storage)
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 // '#1677ff'
// 国际化
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:布局和通知系统
