@gindow/element-go
v1.0.13
Published
基于 Element Plus 的桌面端扩展组件库
Readme
@gindow/element-go
基于 Element Plus 的 Vue 3 + TypeScript 桌面端业务组件扩展库。 组件前缀
Ex,依赖@gindow/vue提供工具类与 i18n 底座。
当前版本:1.0.12 · 21 个组件
1. 速查表
import ElementGo, {
// 组件(20 个具名导出)
ExAssetPreview, ExButton, ExEmpty, ExForm, ExFormField, ExFormSearch, ExFormViewer,
ExIcon, ExInputPercentage, ExLayout, ExLoading, ExMenu, ExPage, ExPageHeader,
ExPagination, ExSelect, ExTable, ExTableColumn, ExUpload, ExUploadAsset,
// Hooks
useMessage, useNanoid, useIcon,
// 工具类(转自 @gindow/vue)
Formatter, DateTime, Validate,
// i18n
provider, setLocale, getLocale, useLocale,
} from '@gindow/element-go'
import type {
IModel, IField, IFilter, IParams, IPagination,
IResult, IAsset, IUploadUserFile, IMenu,
} from '@gindow/element-go'
import '@gindow/element-go/styles/index.css'
import { ElementGoResolver } from '@gindow/element-go/resolver'⚠️ 主入口带样式副作用(
src/index.ts里import './styles/index.css'),与 vant-go 相反。
⚠️ 主入口未导出、但确实存在的东西
| 名称 | 状态 | 正确用法 |
| --- | --- | --- |
| ExLink | 全局注册了(app.use 后模板可用),但没有具名导出 | 用 useMessage().link({ url }) 命令式调用;不要靠 ElementGoResolver 自动导入(会解析成不存在的具名导入而报错) |
| request / resource | 未从根导出 | import { request, resource } from '@gindow/element-go/src/utils/request' |
| get / download / $params | 未从根导出 | @gindow/element-go/src/utils/... |
| Platform | 未从根导出 | 从 @gindow/vue 导入 |
| useBreak / useUpload | 未从根导出 | @gindow/element-go/src/hooks/...,或 useBreak 从 @gindow/vue 导入 |
子路径导出
| 子路径 | 内容 |
| --- | --- |
| @gindow/element-go | 主入口(含样式副作用) |
| @gindow/element-go/resolver | ElementGoResolver |
| @gindow/element-go/locale | 单独的 i18n 入口,不拖入整个组件库 |
| @gindow/element-go/styles/index.css | 完整样式链(含 element-plus 本体 + 暗色变量,已分层) |
| @gindow/element-go/styles/arco.css · purple.css | 可选主题变量 |
| @gindow/element-go/src/* | 源码直取 |
2. 安装
# 必装
pnpm add @gindow/element-go @gindow/vue element-plus vueelement-plus、vue、@gindow/vue 必装;其余 peer 全部 optional,仅在使用对应组件时安装:
| peer 包 | 用到的组件 / 功能 |
| --- | --- |
| @iconify/vue | ExIcon / ExButton / 所有带图标的组件 |
| vue-router | ExPageHeader(back) / ExLayout(router-view) / ExMenu(router) / ExPage(menu 跳转) |
| dayjs | ExFormField(date/daterange) / ExFormSearch / DateTime |
| @vueuse/core | ExLayout(useDark/useToggle) / ExTable(watchOnce) / ExUploadAsset(useThrottleFn) / ExLink(useClipboard) |
| axios | ExUploadAsset / utils/request |
| rxjs | ExUploadAsset(多文件并发上传) |
| vue-request | ExTable(分页查询,onSelect 存在时才启用) |
| sortablejs | ExTable(拖拽排序) |
| lodash | ExTable(cloneDeep/get) |
| js-cookie | utils/request(token 存取) |
| heic2any | useUpload(HEIC → JPEG) |
| compressorjs | useUpload(图片压缩) |
| file-type-checker | useUpload(文件类型嗅探) |
提示:安装 element-plus 后
dayjs已作为其传递依赖存在,通常无需额外安装。qrcode.vue是本包的直接依赖(ExLink用),无需消费方安装。
3. 注册
A. 全量注册
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import ElementGo from '@gindow/element-go'
import '@gindow/element-go/styles/index.css'
import App from './App.vue'
createApp(App).use(ElementPlus).use(ElementGo, { locale: 'zh-CN' }).mount('#app')样式只需引一份:
styles/index.css已经把 element-plus(含暗色变量)打进@layer element-plus。 不要再单独import 'element-plus/dist/index.css'。
app.use(ElementGo, options):provider(options) 初始化语言 + 注册 21 个组件。
options:{ locale?: string; messages?: Record<string, any> }
B. 按需 + 自动导入(推荐)
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { ElementGoResolver } from '@gindow/element-go/resolver'
export default defineConfig({
plugins: [
vue(),
Components({ resolvers: [ElementPlusResolver(), ElementGoResolver()] }),
],
})ElementGoResolver(options?):
| 选项 | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| prefix | string | 'Ex' | 只解析以此开头的组件名 |
| importStyle | boolean | true | 附带 @gindow/element-go/styles/index.css 副作用(unplugin 去重) |
关掉自动样式注入时:
Components({ resolvers: [ElementPlusResolver({ importStyle: false }), ElementGoResolver({ importStyle: false })] })
// 然后在入口手动 import '@gindow/element-go/styles/index.css'⚠️ 解析器按前缀盲匹配,模板里写
<ExLink>会生成import { ExLink } from '@gindow/element-go'—— 该具名导出不存在。ExLink请走useMessage().link()。
4. 组件总览
| 分类 | 组件 |
| --- | --- |
| 基础 | ExIcon ExButton ExEmpty ExLoading |
| 布局 | ExPage ExPageHeader ExLayout ExMenu |
| 数据展示 | ExTable ExTableColumn ExPagination ExAssetPreview |
| 表单 | ExForm ExFormField ExFormSearch ExFormViewer ExInputPercentage ExSelect |
| 上传 | ExUpload ExUploadAsset |
| 命令式 | ExLink(经 useMessage().link()) |
5. 基础组件
ExIcon
包 el-icon + Iconify。
| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| icon | string | '' | 图标名;含 : 时视为完整名 |
| vendor | string | 'icon-park-outline'(由内部 vIcon 兜底) | 图标集前缀 |
| size | number | 16 | 尺寸 |
| color | string | — | 颜色 |
| strokeWidth | number | 3(由内部 vIcon 兜底) | 线宽 |
| loading | boolean | false | 加上 is-loading(旋转动画) |
| circle | boolean | false | 圆形描边容器(尺寸 = size + 16) |
icon 为空时不渲染。绑定了 @click 会自动加 cursor-pointer。
ExButton
扩展 el-button,集成图标 + tooltip。$attrs 全量透传(type / size / disabled / loading / @click 等)。
| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| icon | string | — | 图标名 |
| iconVendor | string | 'icon-park-outline' | 图标集 |
| iconSize | number | 16 | 图标尺寸(px) |
| iconColor | string | — | 图标颜色;未传且 on 为 true 时用主题色 |
| strokeWidth | number | 3 | 图标线宽 |
| link | boolean | true | 文本按钮样式 |
| circle | boolean | false | 圆形按钮;为 true 时强制 link=false |
| on | boolean | false | 激活态(加 .is-on,文字变主题色) |
| tooltip | string | — | 有值时外面包一层 el-tooltip |
| effect | string | 'dark' | tooltip 主题 |
| rawContent | boolean | false | tooltip 内容按 HTML 渲染 |
| showAfter | number | 500 | tooltip 延迟(ms) |
⚠️
link默认为true,与el-button默认实心不同。需要实心按钮:<ExButton :link="false" type="primary">提交</ExButton>
ExEmpty
| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| title | string | — | 主标题(不传则不渲染该行) |
| description | string | t('empty.noData') | 副标题 |
图标固定为 topic-discussion(72px,线宽 1)。
ExLoading
内联 loading 占位,不是遮罩。全屏遮罩请用 ElLoading.service() 或 v-loading。
| Prop | 类型 | 默认 |
| --- | --- | --- |
| size | number | 24 |
| padding | string | '20px' |
6. 布局组件
ExPage
页面容器 = 可选左侧菜单 + PageHeader + 内容区。
| Prop / Model | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| title | string | — | 标题(传给 ExPageHeader) |
| subTitle | string | — | 副标题 |
| icon | string | — | 标题前图标 |
| back | boolean | false | 显示返回按钮 |
| menu | IMenuItem[] | [] | 左侧次级菜单;非空时渲染 el-aside |
| flat | boolean | false | 扁平模式:去掉卡片底色/圆角/内边距 |
| v-model:active | string | — | 当前选中的菜单项 |
interface IMenuItem { label: string; name?: string; icon?: string; to?: string; onclick?: () => void }| Slot | 说明 |
| --- | --- |
| default | 内容区 |
| header | 完全替换头部(此时不渲染 ExPageHeader) |
| title | 自定义标题区 |
| extra | 头部右侧操作区 |
CSS 变量:--ex-page-padding(默认 20px)、--ex-page-border-radius(默认 0)。
ExPageHeader
| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| title | string | '' | 标题 |
| subTitle | string | '' | 副标题 |
| back | boolean | false | 返回按钮 |
| icon | string | — | 标题前图标 |
Slots:title(替换标题)、default(右侧操作区)。
back的实际行为:back为 true 且window.history.state.back存在时才显示返回按钮;点击调用useRouter().back()。所以直接打开的页面不会出现返回按钮。
ExLayout
后台整体框架:侧边栏 + 顶栏(暗黑切换 / 多语言)+ <router-view>。
| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| layout | string | 'lang,dark,account' | 顶栏右侧元素顺序,逗号分隔。可用值:lang、dark、div(竖分隔线)、account(当前已注释停用) |
| logo | string | 内置 icon.png | 侧边栏 logo |
| langs | { label, value }[] | [] | 语言选项;长度 > 1 时才显示切换入口(不在 layout 里也会自动追加) |
| hasPermission | boolean | true | 为 false 时主区显示「暂无权限」 |
| onClose | Function | — | 传入时顶栏右侧出现关闭图标,点击 emit close |
Emits:close。Slots:title(顶栏左侧)、default(顶栏右侧,在内置项之前)。
inheritAttrs: false —— 未声明的 attrs 全部透传给内部 aside:
| aside Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| menu | IMenu[] | [] | 侧边菜单数据 |
| title | string | — | logo 旁标题 |
| menuWidth | number | 160 | 菜单宽度(px) |
| uniqueOpened | boolean | false | 手风琴模式 |
| doubleColumn | boolean | false | 双列模式(一级图标列 + 二级分组列) |
| onLogoClick | () => void | — | 点击 logo 回调 |
路由 meta 会影响布局:
| route.meta | 效果 |
| --- | --- |
| wide: true | 桌面端隐藏侧边栏(宽屏内容页) |
| alive: true | <router-view> 外套 <keep-alive> |
侧边栏折叠状态持久化在 localStorage.collapse;移动端(<768px)折叠按钮改为打开抽屉。暗黑模式用 @vueuse/core 的 useDark()(写 html.dark + localStorage)。
ExMenu
| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| menu | IMenu[] | [] | 菜单数据,支持三级嵌套 |
| menuWidth | number | — | 用于计算文字区宽度(menuWidth - 93) |
| active | string | 自动推导 | 手动指定高亮项 |
| uniqueOpened | boolean | false | 手风琴 |
Slot default:菜单顶部插入内容(如 logo)。
行为:
hidden: true的项被过滤掉;key存在时用t(key)覆盖title(国际化菜单);isGroup: true+children→ 渲染el-menu-item-group(分组标题,不可点);children无isGroup→ 渲染el-sub-menu(可展开);- 未指定
active时,自动取能匹配route.path前缀且最长的path高亮。
7. 数据展示
ExTable
业务表格全家桶:搜索 + 列表 + 分页 + 新增/编辑弹窗 + 删除确认 + 拖拽排序。
inheritAttrs: false,未声明的 attrs 透传给 el-table(border、stripe、height、@selection-change 等)。
Props
| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| columns | IModel[] | [] | 列定义,每项即 ExTableColumn 的 props(另支持 customRender(row, col) => string,按 HTML 渲染) |
| actions | IModel[] | [] | 操作按钮,见下 |
| actionShow | number | 1 | 前 N 个操作直接展示,其余收进「更多」下拉 |
| filter | Record<string, IFilter> | — | 传入才渲染 ExFormSearch |
| search | boolean | true | 是否显示关键字搜索框 |
| placeholder | string | — | 搜索框 placeholder |
| pager | boolean | true | 是否显示分页 |
| sortable | boolean | false | 显示「排序」开关,开启后可拖拽行 |
| density | 'small' \| 'default' \| 'large' | — | 表格密度 |
| fields | object | {} | 编辑表单字段定义(传给 ExForm.fields) |
| rules | FormRules | {} | 表单校验规则 |
| model | object | {} | 新增时的表单初值 |
| formColumns | number | 1 | 表单列数 |
| formWidth | string | 自动 | 弹窗宽度;默认 max(formColumns * 400, 500)px |
| formTitle | string \| ((model, isNew) => string) | 自动 | 弹窗标题;默认新增→t('core.create'),编辑→t('core.edit') |
| labelPosition | 'top' \| 'left' \| 'right' | 'top' | 表单标签位置 |
| onSelect | (params) => Promise<IResult> | — | 列表查询接口;传了才启用内部分页请求 |
| onInsert | (model) => Promise<any> | — | 新增接口;传了才有「新增」能力 |
| onUpdate | (id, model) => Promise<any> | — | 更新接口;传了自动追加「编辑」操作 |
| onDelete | (id) => Promise<any> | — | 删除接口;传了自动追加「删除」操作(带确认框) |
v-model
| Model | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| v-model:data | IModel[] | [] | 表格数据(onSelect 模式下由内部写入) |
| v-model:loaded | boolean | false | 首次加载完成;为 false 时只显示 ExLoading |
| v-model:loading | boolean | false | 请求中(表格遮罩) |
| v-model:page | number | 1 | 当前页 |
| v-model:size | number | 15 | 每页条数 |
| v-model:total | number | 0 | 总条数 |
| v-model:params | IParams | { filter: {}, search: '' } | 查询参数 |
⚠️ 不传
onSelect时必须自己维护data/loaded/total,并监听@search自行取数;loaded保持false会永远停在 loading 态。
Emits
| 事件 | 载荷 | 触发 |
| --- | --- | --- |
| search | — | 仅在未传 onSelect 时,搜索/重置时触发 |
| sorted | IModel[] | 关闭排序开关且发生过拖拽时,抛出排序后的完整数据 |
Slots
| Slot | 作用域 | 说明 |
| --- | --- | --- |
| default | — | 追加自定义 ExTableColumn(在 columns 之前) |
| cell | { row, col } | 覆盖无 type 列的单元格渲染 |
| batch | — | 底部批量操作区(与「排序」开关同排) |
| formItem | — | 编辑表单顶部插入内容 |
| formField | { field, model } | 覆盖表单字段渲染 |
暴露方法(ref)
| 方法 | 说明 |
| --- | --- |
| create() | 打开「新增」弹窗(用 model 作初值) |
| reload() | 回到第 1 页重新查询 |
| refresh() | 保持当前页刷新 |
| clearSelection() | 清空多选 |
| toggleRowSelection(row, selected?, ignoreSelectable?) | 切换某行选中 |
actions 结构
{
type?: 'update' | 'delete', // 与内置操作合并(同 type 时你的配置覆盖默认)
label: string | ((row) => string),
icon?: string,
click: (row) => void,
disabled?: boolean | ((row) => boolean),
hidden?: boolean | ((row) => boolean),
divided?: boolean, // 「更多」下拉里的分隔线
}传了 onUpdate / onDelete 会自动补上「编辑」「删除」两个操作;用同名 type 可覆盖其 label/icon/click。
操作列宽度自动计算:前N个 * 70 + (有更多 ? 24 : 0) + 24。
示例
<template>
<ExPage title="用户列表">
<template #extra>
<ExButton :link="false" type="primary" icon="add" @click="table?.create()">新增</ExButton>
</template>
<ExTable ref="table" border
:columns :filter :fields :rules
:on-select="api.select" :on-insert="api.create"
:on-update="api.update" :on-delete="api.delete" />
</ExPage>
</template>
<script setup lang="ts">
import { ExTable } from '@gindow/element-go'
const table = useTemplateRef<InstanceType<typeof ExTable>>('table')
const columns = [
{ type: 'selection' },
{ type: 'id' },
{ prop: 'name', label: '姓名' },
{ type: 'phone' },
{ type: 'tag', prop: 'status', label: '状态', tagType: (row) => row.status === 'ok' ? 'success' : 'danger' },
{ type: 'datetime' },
]
const filter = {
status: { type: 'select', label: '状态', options: [{ label: '启用', value: 'ok' }] },
created_at: { type: 'daterange', label: '创建时间', advanced: true },
}
const fields = {
name: { type: 'text', label: '姓名', required: true },
status: { type: 'select', label: '状态', options: [...] },
}
</script>ExTableColumn
单列渲染器,内置 17 种列类型。
| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| type | IType | 'text' | 列类型,见下表 |
| prop | string | 按类型模板 | 取值字段(支持 a.b.c 路径) |
| label | string | 按类型模板 | 表头 |
| value | string \| number \| ((row, column) => any) | — | 直接指定值 / 计算函数,优先于 prop |
| defaultValue | string | — | 空值占位 |
| width / minWidth | string \| number | 按类型模板 | 宽度 |
| align | string | 按类型模板 | 对齐 |
| fixed | boolean \| 'left' \| 'right' | — | 固定列 |
| className | string | — | 单元格 class |
取值优先级:value(函数则调用)→ get(row, prop) → defaultValue。
| type | 渲染 | 默认 prop / label / width / align | 附加 Props |
| --- | --- | --- | --- |
| text | 纯文本(默认) | — | defaultValue |
| id | Formatter.id()(末 12 位大写) | id / — / 160 | — |
| selection | 多选列 | — / — / 30 / center | — |
| sort | 拖拽手柄图标 | — / — / 30 / center | — |
| state | 状态圆点(真=success 假=warning) | state / — / 40 / center | — |
| icon | 值即图标名 | — | iconSize(默认 16) |
| link | el-link | — | linkType(默认 'primary')、onClick(row) |
| phone | Formatter.phone() 脱敏 | phone / t('core.phoneNumber') | — |
| boolean | ✓ / ✗ 图标 | — | — |
| currency | Formatter.price() | amount / — / 140 / right | currency(不传则取 row.currency) |
| date | DateTime.date() | created_at / t('core.createdAt') / 140 / right | — |
| datetime | 上行日期 + 下行时间 | created_at / t('core.createdAt') / 140 / right | — |
| tags | 标签组(值为数组) | tags | tagKey、tagType、tagEffect、tagSize、tagRound |
| tag | 单标签 | — | 同上;tagType 可为 (row) => ITag |
| image | el-image 缩略图(48px,失败显示占位图标) | — / — / 60 / center | — |
| avatar | el-avatar(48px,失败用内置头像) | — / — / 60 / center | — |
| action | 操作列(由 ExTable 注入 show / more) | — | — |
Slots:default(作用域 { row },覆盖全部内置渲染)、front(作用域 { row },在内容前追加)。
action类型依赖ExTable通过provide('show'/'more')注入,不能脱离ExTable单独使用。
ExPagination
| Model | 类型 | 说明 |
| --- | --- | --- |
| v-model:page | number | 必填,当前页 |
| v-model:size | number | 必填,每页条数 |
Emits:currentChange(page)、sizeChange(size)。
layout 随断点自适应:桌面 total,sizes,prev,pager,next,移动端 total,prev,next。
可选条数 [15, 30, 60, 120, 240],当前 size 不在其中时自动插入并排序。
ExAssetPreview
el-image-viewer 封装,工具栏含缩放/旋转/下载。
| Prop / Model | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| v-model:show | boolean | false | 显示状态 |
| asset | IAsset | {} | 初始定位的资源(按 id 在 assets 中查找) |
| assets | IAsset[] | [] | 资源列表 |
展示 item.shrink ?? item.url;下载走 fetch(url).blob() → download(),失败 toast core.message.downloadFailed。
8. 表单组件
ExForm
动态表单,inline / dialog 两种模式自动切换。inheritAttrs: false,未声明 attrs 透传给 el-form。
| Prop / Model | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| v-model | IModel | {} | 表单数据 |
| v-model:show | boolean | false | 弹窗显隐;传了就切 dialog 模式 |
| fields | IField[] \| Record<string, IField> | {} | 字段定义;对象形式的 key 即 name |
| rules | FormRules | {} | 校验规则 |
| errors | Record<string, string[]> | {} | 后端字段错误,按 field.name 展示(数组用 ; 拼接) |
| title | string | 'Dialog' | 弹窗标题 |
| width | string | 自动 | 弹窗宽度;默认 (labelPosition==='top' ? 400 : 480) * formColumns |
| formColumns | number | 1 | 列数(span = 24 / formColumns) |
| labelWidth | string | 'auto' | 标签宽度 |
| labelPosition | 'top' \| 'left' \| 'right' | — | 标签位置 |
| loaded | boolean | true | 为 false 时隐藏所有操作按钮 |
| loading | boolean | false | 提交按钮 loading |
| disabled | boolean | false | 禁用提交按钮 |
| destroyOnClose | boolean | false | 取消时重置表单 |
| submitText / cancelText / resetText | string | 走 i18n | 按钮文案 |
Emits:submit(model)(校验通过后才触发)、reset、cancel。
Slots:default(表单顶部)、field(作用域 { model, field },仅 dialog 模式生效)。
暴露方法:submit()、reset()、clearValidate()、validate()。
模式判断:内部读
getCurrentInstance().vnode.props['onUpdate:show']。 只要绑了v-model:show或@update:show就是 dialog 模式,否则 inline 模式(底部是「保存 / 重置」,dialog 模式是「保存 / 取消」)。
必填提示:字段标了
required却没给rules时会自动补一条本地化必填规则 —— 选择型控件(select/cascader/radio/tree/date/daterange/switch/percentage,以及带component的自定义组件)用「请选择{label}」,其余用「请输入{label}」。
ExFormField
按 field.type 分发到具体控件。通常由 ExForm / ExFormSearch 内部调用,也可单独用。
| Prop / Model | 类型 | 说明 |
| --- | --- | --- |
| v-model | object | 整个 model 对象(内部写 model[field.name]),不是单个值 |
| field | IField | 必填,字段描述 |
Emits:change(value)(switch / number / percentage 不触发)。
| field.type | 控件 |
| --- | --- |
| text(默认) | el-input |
| password | el-input[type=password](autocomplete="new-password") |
| number | el-input-number |
| select | el-select(选项含 options 字段时渲染 el-option-group 分组) |
| cascader | el-cascader(collapse-tags + filterable) |
| radio | el-radio-group |
| tree | el-tree-select(filterable) |
| date / daterange | el-date-picker(格式与值格式均为 YYYY-MM-DD) |
| switch | el-switch |
| percentage | ExInputPercentage |
IField 字段:
| 字段 | 说明 |
| --- | --- |
| name | 必填,对应 model 的 key |
| type | 控件类型 |
| label | 标签(ExForm 用) |
| options | 选项数据 |
| required | 必填 |
| readonly / disabled | 二者任一为真都会禁用控件 |
| hidden | 在 ExForm 里隐藏该列,在 ExFormSearch 里过滤掉 |
| advanced | 仅 ExFormSearch:收进「高级筛选」折叠面板 |
| multiple | 多选(影响重置逻辑) |
| component | 自定义组件,优先于 type,接收 v-model + 其余字段作为 props |
| maxCollapseTags | cascader 折叠标签数,默认 2 |
| 其余任意字段 | 透传给底层控件(placeholder、clearable、min、max…) |
ExFormSearch
顶部查询表单(ExTable 传了 filter 时自动使用)。
| Prop / Model | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| v-model:params | IParams | { filter: {}, search: '' } | 查询参数 |
| filter | Record<string, IFilter> | {} | 过滤项定义,key 即字段名 |
| search | boolean | true | 关键字输入框;还需 params 里存在 search 键才会渲染 |
| submit | boolean | true | 显示「查询/重置」按钮 |
| placeholder | string | — | 关键字框 placeholder |
Emits:search(params)、reset。Slot default:表单最前插入内容。
高级筛选:filter 项标 advanced: true 收进折叠面板;折叠时用徽章提示生效中的高级条件数;初始就带值时自动展开。
重置会按类型清空(daterange → null,数组/multiple → [],其余 → '')。
CSS 变量:--ex-search-field-width(默认 180px)、--ex-search-daterange-width(默认 280px)。
ExFormViewer
只读详情,el-descriptions 封装,同样自动判断 dialog / inline 模式(依据 v-model:show)。
| Prop / Model | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| v-model:show | boolean | false | 传了就是 dialog 模式 |
| items | Item[] | [] | 详情项 |
| topic | string | '详情' | 弹窗标题(硬编码中文,未走 i18n) |
| title | string | — | descriptions 自身标题 |
| extra | string | — | 右上角附加文本 |
| column | number | 2 | 每行列数;移动端强制为 1 |
| border | boolean | true | 边框 |
| labelWidth | string | '120px' | 标签宽度 |
| width | string | '800px' | 弹窗宽度(移动端自动全屏) |
interface Item { label: string; value?: string; align?: string; span?: number; VNode?: VNode }Slot extra:右上角自定义区域。
⚠️
value用v-html渲染,不要放未转义的用户输入(XSS 风险)。需要复杂内容请用VNode字段。
ExInputPercentage
百分比输入:显示值 = 存储值 × 100。
| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| modelValue | number \| string | '' | 存储值(如 0.15) |
| symbol | string | '%' | 前缀符号,空字符串则不显示 |
| precision | number | 2 | 显示值的小数位;存储值保留 precision + 2 位 |
| controls | boolean | false | 显示步进按钮(步长 0.01) |
Emits:update:modelValue(value)、change(value) —— 都是换算回去的存储值。
存 0.15 → 显示 15;输入 15 → 存 0.15。
ExSelect
| Prop | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| options | any[] | [] | 本地选项数据 |
| props | { label?, value? } | {} | 字段映射,默认 label / value |
| params | object | {} | 远程加载入参 |
| onSelect | (params) => Promise<{ data }> | — | 远程数据源;传了会在 setup 期立即调用一次 |
Slot default:作用域 { row: { label, value } },自定义选项内容。
固定开启 filterable + clearable。
⚠️ 组件没有声明
v-model,值绑定靠el-select的 attrs 继承(v-model会以modelValue+onUpdate:modelValue落到根节点)。onSelect只在初始化时调用一次,params变化不会重新请求。
9. 上传组件
ExUpload
通用上传(HEIC 转码 / 图片压缩 / 图片预览)。$attrs 透传给 el-upload(list-type、accept、drag、action 等)。
| Prop / Model | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| v-model:fileList | IUploadUserFile[] | [] | 文件列表 |
| limit | number | 500 | 数量上限(超出 toast) |
| multiple | boolean | true | 多选;为 false 时新选文件会替换旧的 |
| disabled | boolean | false | 禁用(上传中也自动禁用) |
| showFileList | boolean | false | 显示文件列表 |
| showProgress | boolean | true | 右下角总进度圆环 |
| compressor | boolean | false | 图片压缩(质量 0.9) |
Emits:change(file, fileList)。Slots:default(作用域 { loading },替换触发区)、file(作用域 { file },自定义列表项)。
暴露:submit()(触发 el-upload 手动上传)。
预处理:>200MB 直接拒绝并 toast;HEIC 自动转 JPEG;jpeg/png/gif/bmp 在 compressor 为真时压缩。
ExUploadAsset
资源上传,支持 local / oss / obs 三种存储,rxjs 并发(上限 5)。
| Prop / Model | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| v-model:ids | string[] \| string | [] | 已上传资源 id;multiple=false 时是单个字符串 |
| v-model:fileList | IUploadUserFile[] | [] | 文件列表(传单对象会自动包成数组) |
| place | string | — | 必填,业务位置标识 |
| disk | 'local' \| 'oss' \| 'obs' | 'local' | 存储通道 |
| data | Record<string, any> | {} | 附加业务字段(OSS/OBS 下会加 x: 前缀) |
| compressor | boolean | false | 图片压缩;单文件 >20MB 时强制压缩 |
| multiple | boolean | true | 多选 |
| autoUpload | boolean | true | 选择后自动上传 |
| preview | boolean | true | 点击可预览 |
| border | boolean | false | 外框 |
| onSuccess | (assets: IAsset[]) => void | — | 全部上传完成回调 |
Slots:default(作用域 { loading })。暴露:submit()(autoUpload=false 时手动触发)。
$attrs 透传给内部 ExUpload → el-upload;带 list-type 时切换为图片卡片模式(含悬浮预览/删除与进度环)。
依赖的后端接口(硬编码):
| 用途 | 方法 | 路径 |
| --- | --- | --- |
| 服务器上传 | POST | /user/asset/upload |
| OSS 签名 | GET | /user/asset/upload/signature |
| OBS 签名 | GET | /user/asset/obsSignature |
请求走 utils/request,因此必须先 request.init({ baseURL }) 并设置 token,否则上传会失败。
文件 >200MB 直接拒绝。OSS/OBS 文件名规则:YYYYMMDDHHmmss + 4 位随机数字 + 原扩展名。
10. Hooks
useMessage()
const { message, success, error, warning, dialog, confirm, prompt, notify, link } = useMessage()| 方法 | 签名 | 底层 |
| --- | --- | --- |
| message | (msg, options?) | ElMessage |
| success / error / warning | (msg, options?) | ElMessage(对应 type) |
| dialog | (msg, title?, options?) | ElMessageBox.alert,标题默认 t('dialog.tip') |
| confirm | (msg, title?, options?) | ElMessageBox.confirm,标题默认 t('dialog.confirm'),返回 Promise |
| prompt | (msg, title?, options?) | ElMessageBox.prompt,内置非空校验 |
| notify | (msg, options?) | ElNotification |
| link | (config: { url, title?, width?, ...}) | 命令式挂载 ExLink:二维码 + 复制链接 + 新窗口访问,关闭即卸载 |
const { link } = useMessage()
link({ url: '/share/abc123' }) // 相对路径会自动补成绝对 URL需在 setup 上下文中调用(内部用 useLocale() 与 getCurrentInstance())。
useIcon()
const { i } = useIcon()
i('upload', { size: 18, color: '#093' }) // → VNode | undefined给只接受 VNode 的 element-plus API 传图标(如 el-button :icon、el-dropdown-item :icon)。
useNanoid()
const { nanoid, numeric } = useNanoid()
nanoid() // 21 位安全随机
numeric(6) // 纯数字,默认 10 位未从主入口导出的 hooks
| Hook | 引入方式 | 返回 |
| --- | --- | --- |
| useBreak | @gindow/vue | { isMobile, isDesktop }(断点 768px) |
| useUpload | @gindow/element-go/src/hooks/useUpload | { handler, getDimension, getFileType } |
useUpload:
handler(file, { compressor })→ HEIC 自动转 JPEG;jpeg/png/gif/bmp 且compressor为真时压缩(质量 0.9);其余原样返回getDimension(blob)→{ width, height }getFileType(file)→{ mimeType }(读文件头嗅探,不信任扩展名)
11. 工具(@gindow/element-go/src/utils/*)
request — HTTP 封装
import { request, resource } from '@gindow/element-go/src/utils/request'
request.init({ baseURL: 'https://api.example.com' })
request.setToken(token)| 成员 | 说明 |
| --- | --- |
| init({ baseURL?, publicKey? }) | 创建 axios 实例并装拦截器 |
| get/post/put/patch/delete(url, data?, auth?) | auth=true 时带 Authorization: Bearer <token> |
| blob(url, params?, auth?) | responseType: 'blob' |
| download(url, params?, filename?, auth?) | 下载并触发保存;未给 filename 时解析 content-disposition;Platform.isFlutter 时直接返回响应 |
| upload(url, formData) | multipart/form-data |
| sse(url, para?) | 返回 EventSource |
| setToken(token, { expires?, domain? }) / getToken() / delToken() | Cookie 存取(key 为 accessToken,domain 固定 . + hostname) |
| setHeader(k, v) / delHeader(k) | 全局请求头 |
响应拦截约定:code === 200 → 返回 response.data;code === 401 → 清 token 并 location.reload();其余 → Promise.reject(response.data)。
请求拦截自动带上 Accept-Language(取 localStorage.locale)。
resource 类 —— RESTful 资源:
const users = new resource('/user/users', true) // (url, auth)
users.select(params) // GET /user/users
users.create(data) // POST /user/users
users.find(id, params) // GET /user/users/:id
users.update(id, data) // PATCH /user/users/:id
users.delete(id, params) // DELETE /user/users/:id
users.get(params) // select + size=1,返回单条(data 取第一项)其它
| 函数 | 说明 |
| --- | --- |
| get(object, path, defaultValue?) | 安全深取('a.b[0].c'),零依赖版 lodash.get |
| download(blob, filename) | 触发浏览器下载 |
| $params(params) | 查询参数转化:把 search / searchFields 对象转成 key:value;key:value 字符串(request 内部自动调用) |
Formatter / DateTime / Validate / Platform 全部转自 @gindow/vue,API 见 该包 README §6。
其中 Formatter / DateTime / Validate 可从 @gindow/element-go 根路径导入,Platform 不行。
12. 类型定义
interface IModel {
id?: string | any
created_at?: string; updated_at?: string; deleted_at?: string
[property: string]: any
}
interface IField extends IModel {
name: string
type: string
readonly?: boolean
multiple?: boolean
required?: boolean
hidden?: boolean
advanced?: boolean // 高级搜索条件:收进 ExFormSearch 折叠面板
options?: { label: string; value: string }[] | any[]
component?: any // 自定义控件
}
type IFilter = Omit<IField, 'name'> & { label?: string; rules?: FormItemRule[] }
interface IParams extends IModel {
filter?: Object; search?: string; include?: string
page?: number; size?: number
}
interface IPagination {
current_page: number; per_page: number
count: number; total: number; total_pages: number
}
interface IResult {
code: number
message: string
data?: IModel | IModel[] | null
meta?: { pagination?: IPagination }
}
interface IAsset extends IModel {
id: string; type: string; title: string
url: string; shrink: string
}
interface IUploadUserFile {
id?: string; uid?: number
name: string
url?: string; size?: number; status?: string; response?: unknown
raw?: File & { uid?: number }
percentage?: number
asset?: IAsset
title?: string; width?: number; height?: number
type?: string; mimeType?: string
}
interface IMenu {
key?: string // i18n key,优先于 title
title: string
path: string
name?: string; icon?: string; depend?: string
hidden?: boolean; divider?: boolean; disabled?: boolean
isGroup?: boolean
children?: IMenu[]
}
IUploadUserFile有意不 extends element-plus 的UploadUserFile—— 后者字段可见性在 EP 小版本间不稳,会让下游vue-tsc报status/raw/name/url不存在。
@gindow/vue有一份字段更少的同名类型。element-go 工程请统一从@gindow/element-go导入。
13. 国际化
import { setLocale, getLocale, useLocale } from '@gindow/element-go'
// 或只引 locale 入口,不拖入组件库:
import { setLocale } from '@gindow/element-go/locale'
setLocale('en')
const { locale, t } = useLocale()
t('core.message.fileExceed', { limit: 5 }) // → "A maximum of 5 files can be uploaded"初始语言:localStorage.locale → navigator.language 完全匹配 → 语言族匹配 → 兜底 'en'。
内置文案(zh-CN / en):
core.back / save / cancel / reset / search / keywords / copy / advanced / upload /
confirm / phoneNumber / createdAt / noPermission / create / edit / delete /
sort / logout / profile / password
core.datetime.to / start / end / yesterday / today / thisWeek / thisMonth / thisYear
core.message.error / successful / downloadFailed / tooLarge / fileExceed /
confirm / delConfirm / copied / required / requiredSelect
empty.noData
exlink.scan / copy / visit
dialog.tip / confirm / prompt / required扩展语言包:
app.use(ElementGo, {
locale: 'en-KE',
messages: { 'en-KE': { ...业务文案 } },
})✅ 内置文案有永不被覆盖的
base底座:t()先查 app 的messages[locale], 查不到再按语言族(zh*→zh-CN,其余 →en)回落到包自带文案。 因此自定义 locale 码(en-KE/sw-KE)也能正常显示 Ex* 组件的内置文字,业务 app 只需写自己的业务 key。
provider(opts?) 已用 getCurrentInstance() 守卫,可在 setup 之外安全调用。
14. 样式与级联层
@gindow/element-go/styles/index.css 包含:
- 级联层顺序声明 + 层锚点:
@layer theme, base, vendor, element-plus, components, utilities; tailwindcss+element-plus/dist/index.css(→@layer element-plus)+ 暗色变量 +@gindow/vue/style.css- 通用工具类与 element-plus 变量微调(圆角归零等)
| 工具类 | 作用 |
| --- | --- |
| .flex-center / .flex-center-end / .flex-center-between / .flex-center-items | flex 布局快捷类 |
| .text-primary/success/warning/danger/info | element-plus 主题色文字 |
| .bg-color / .bg-color-page / .bg-color-overlay | element-plus 背景色 |
| .border-color[-light/-lighter/-extra-light] | element-plus 边框色 |
| .text-light | #ccc(.dark 下 #333) |
| .ex-form-wide | 表单项最大宽度从 500px 放宽到 800px |
为什么 element-plus 要夹在 base 与 components 之间:
- 在
base之后 → Tailwind preflight 的*{margin:0;padding:0;border:0}不会剥掉 EP 组件自带间距; - 在
components/utilities之前 → 页面上的<el-alert class="mt-4">这类工具类才能覆盖组件样式 (无层样式永远赢过有层样式,与源码顺序、特异性无关,所以 EP 必须进层)。
暗色模式:本包 CSS 带
html.dark下的 element-plus 变量,元件会自己变。 但 app 自己的设计令牌(页面底色 / 卡片 / 分隔线)要自己在html.dark下重定义一遍, 否则元件黑了、整页还是白的。
15. 两种消费模式
exports 的 @source 条件指向 src,默认条件指向 dist。
npm 包模式(默认,推荐)
不要开 resolve.conditions: ['@source'] —— 包的 src 依赖宿主的 AutoImport 注入,
而 AutoImport 默认不处理 node_modules,走 src 会直接 computed is not defined。
装成 npm 包就吃 dist,组件已编译完毕。
样式引一份即可:
import '@gindow/element-go/styles/index.css' // 不要再单独 import 'element-plus/dist/index.css'Components({ resolvers: [ElementPlusResolver({ importStyle: false }), ElementGoResolver({ importStyle: false })] })源码包模式(submodule / monorepo)
resolve: {
conditions: ['@source'],
alias: { '@gindow/element-go': resolve(__dirname, '../packages/element-go/src') },
}组件源码由宿主 app 编译,宿主的 unplugin-auto-import 必须能覆盖到它们。本包源码依赖这些自动导入:
vue、vue-router、vue-i18n全量@vueuse/core的useDark、watchDeep、watchOnce、useClipboard、useThrottleFn、useTogglesrc/hooks目录下的所有 hook
16. 开发
pnpm install
pnpm build # vue-tsc --noEmit 类型检查 + vite 构建产物入口:@gindow/element-go.{mjs,cjs}、resolver.*、locale.*、styles/index.css。
构建时 AutoImport / Components 的 importStyle 均为 false —— 库产物刻意不烘焙按组件样式,
避免无层 CSS 反压消费端的 @layer utilities。
License
MIT © joming
