npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 个组件

version


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.tsimport './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 vue

element-plusvue@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' | 顶栏右侧元素顺序,逗号分隔。可用值:langdarkdiv(竖分隔线)、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/coreuseDark()(写 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(分组标题,不可点);
  • childrenisGroup → 渲染 el-sub-menu(可展开);
  • 未指定 active 时,自动取能匹配 route.path 前缀且最长path 高亮。

7. 数据展示

ExTable

业务表格全家桶:搜索 + 列表 + 分页 + 新增/编辑弹窗 + 删除确认 + 拖拽排序。

inheritAttrs: false未声明的 attrs 透传给 el-tableborderstripeheight@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 | tagKeytagTypetagEffecttagSizetagRound | | 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 | {} | 初始定位的资源(按 idassets 中查找) | | 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)校验通过后才触发)、resetcancel

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-cascadercollapse-tags + filterable) | | radio | el-radio-group | | tree | el-tree-selectfilterable) | | 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 | | 其余任意字段 | 透传给底层控件(placeholderclearableminmax…) |

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 收进折叠面板;折叠时用徽章提示生效中的高级条件数;初始就带值时自动展开。 重置会按类型清空(daterangenull,数组/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:右上角自定义区域。

⚠️ valuev-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-uploadlist-typeacceptdragaction 等)。

| 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 透传给内部 ExUploadel-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 :iconel-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-dispositionPlatform.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.datacode === 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-tscstatus/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.localenavigator.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 包含:

  1. 级联层顺序声明 + 层锚点:
    @layer theme, base, vendor, element-plus, components, utilities;
  2. tailwindcss + element-plus/dist/index.css(→ @layer element-plus)+ 暗色变量 + @gindow/vue/style.css
  3. 通用工具类与 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 要夹在 basecomponents 之间

  • 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 必须能覆盖到它们。本包源码依赖这些自动导入:

  • vuevue-routervue-i18n 全量
  • @vueuse/coreuseDarkwatchDeepwatchOnceuseClipboarduseThrottleFnuseToggle
  • src/hooks 目录下的所有 hook

16. 开发

pnpm install
pnpm build       # vue-tsc --noEmit 类型检查 + vite 构建

产物入口:@gindow/element-go.{mjs,cjs}resolver.*locale.*styles/index.css。 构建时 AutoImport / ComponentsimportStyle 均为 false —— 库产物刻意不烘焙按组件样式, 避免无层 CSS 反压消费端的 @layer utilities

License

MIT © joming