@gindow/vue
@gindow/vue
Vue 3 + TypeScript 通用基础包:工具类、Hooks、类型、i18n 底座、级联层样式。 是
@gindow/element-go(桌面端)与@gindow/vant-go(移动端)的公共依赖,也可单独使用。
当前版本:1.0.8 · 无 UI 框架依赖(不依赖 element-plus / vant)
1. 速查表
import Vue, {
// 组件
VueIcon,
// Hooks
useBreak, useCaptcha, useIcon, useNanoid,
// 工具类(静态类,直接调用)
Formatter, Validate, DateTime, Platform,
// i18n
setLocale, getLocale, useLocale,
} from '@gindow/vue'
import type {
IModel, IAsset, IField, IFilter, IParams,
IPagination, IResult, IUploadUserFile, IMenu,
} from '@gindow/vue'
import '@gindow/vue/style.css' // 级联层顺序 + Tailwind + 工具类
import { VueResolver } from '@gindow/vue/resolver' // unplugin-vue-components 解析器
⚠️
provider不从根路径导出(element-go / vant-go 才导出它)。本包只能通过app.use(Vue, { locale, messages })初始化语言。
子路径导出(package.json exports)
| 子路径 | 内容 |
|---|---|
@gindow/vue |
主入口(含 import './style.css' 副作用) |
@gindow/vue/resolver |
VueResolver |
@gindow/vue/style.css |
样式(级联层声明 + Tailwind + 工具类) |
@gindow/vue/src/* |
源码直取,用于未在主入口导出的东西(如图表组件) |
@gindow/vue/package.json |
— |
2. 安装
pnpm add @gindow/vue vue
vue ^3.5 是唯一必装 peer。其余 peer 全部 optional:
| peer 包 | 用途 | npm 包模式是否必装 |
|---|---|---|
@iconify/vue ^5 |
VueIcon / useIcon |
是(未打进产物,external) |
dayjs ^1.11 |
DateTime |
否(已内联进 dist) |
nanoid ^5 |
useNanoid |
否(已内联进 dist) |
@visactor/vchart ^2 |
图表组件(@gindow/vue/src/components/chart) |
用到才装 |
@vueuse/core ^12/13/14 |
图表暗色跟随(useDark) |
用到图表才装 |
lodash ^4.17 |
图表 spec 合并(vLine / vPie) |
用到图表才装 |
为什么 dayjs / nanoid 不用装:本包构建时只把
vue、@iconify/vue列为 external,dayjs与nanoid已被打进dist/vue.mjs。 但源码包模式(走src)例外——那时源码直接import dayjs from 'dayjs',必须自行安装。
3. 注册
// main.ts
import { createApp } from 'vue'
import Vue from '@gindow/vue'
import '@gindow/vue/style.css'
import App from './App.vue'
createApp(App)
.use(Vue, { locale: 'zh-CN' }) // options 可省
.mount('#app')
app.use(Vue, options) 做两件事:初始化 locale(provider(options))、全局注册 VueIcon。
options 类型:{ locale?: string; messages?: Record<string, any> }
自动导入(可选)
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { VueResolver } from '@gindow/vue/resolver'
export default defineConfig({
plugins: [Components({ resolvers: [VueResolver()] })],
})
VueResolver(options?):
| 选项 | 类型 | 默认 | 说明 |
|---|---|---|---|
prefix |
string |
'Vue' |
只解析以此开头的组件名 |
importStyle |
boolean |
true |
附带 @gindow/vue/style.css 副作用导入(unplugin 会去重) |
本包只有
VueIcon一个组件,解析器价值有限;多数项目直接app.use(Vue)即可。
4. 组件
VueIcon
基于 Iconify 的图标组件,额外支持按文件类型/MIME 自动选图标。
| Prop | 类型 | 默认 | 说明 |
|---|---|---|---|
icon |
string |
'' |
图标名。含 : 时视为完整名(mdi:home),否则拼 vendor |
type |
string |
'' |
文件扩展名 / MIME / 业务类型;仅在 icon 为空时生效 |
vendor |
string |
'icon-park-outline' |
图标集前缀 |
size |
number |
16 |
宽高(px) |
strokeWidth |
number |
3 |
线宽(通过 v-bind 注入 g/path 的 stroke-width) |
color |
string |
— | 颜色 |
无 emits、无 slots。icon 与 type 都为空时不渲染任何元素。
type 解析优先级:icon → type === 'folder' → MIME 子类型表 → MIME 主类型表 → 扩展名表 → 兜底 file-text。
<VueIcon icon="home" :size="20" color="#093" />
<VueIcon type="application/pdf" /> <!-- → file-pdf -->
<VueIcon type="xlsx" /> <!-- → file-excel -->
<VueIcon type="image/png" /> <!-- → pic-one -->
<VueIcon icon="mdi:rocket" /> <!-- 含 : 时忽略 vendor -->
内置映射覆盖:图片(png/jpg/gif/webp/bmp/svg)、视频(mp4/avi/mov/wmv/flv/mkv/webm)、音频(mp3/wav/wma/aac/ogg/flac)、pdf、Word、Excel、PPT、文本、代码(json/xml/html/css/js/ts/py/php/cpp/java/go/rs…)、压缩包(zip/rar/7z/tar/gz/bz2),以及业务类型 image/video/audio/text/application/font/pdf/doc/sheet/ppt/md/canvas/album/yearbook。
图表组件(未在主入口导出)
只能通过源码子路径引入,且需自行安装 @visactor/vchart、@vueuse/core、lodash:
import { vBar, vLine, vPie, vFunnel, vNum, useChart } from '@gindow/vue/src/components/chart'
⚠️ 走
src需要宿主 Vite 能编译.vue,且node_modules不被 AutoImport 排除。详见 §9。
| 组件 | 说明 | Props |
|---|---|---|
vBar |
柱状图 | title(必填)、props(合并进 spec,支持 xField/yField/autoRotate)、values |
vLine |
折线图(默认堆叠) | title、props、series、values、spec |
vPie |
饼图(默认环形 innerRadius:0.4) |
title(必填)、props、values |
vFunnel |
漏斗图 | title(必填)、props、values |
vNum |
数字指标卡(非图表,纯 DOM) | icon、color(默认 #093)、title(默认 'Total')、amount(默认 0)、growth |
vLine/vPie触发@elementClick="(datum) => {}";vBar/vFunnel/vNum无此事件。- 图表根元素为
<div class="h-full min-h-60">,高度由父容器决定(vNum是min-h-40)。 vNum.growth为null时不显示涨跌行;为字符串时取其中第一个数字判断正负(正数绿色带+,否则红色)。
useChart() 返回:
| 方法 | 签名 | 说明 |
|---|---|---|
draw |
(spec: ISpec, dom?: HTMLDivElement) => void |
创建并渲染;自动跟随 useDark() 切换 light/dark 主题 |
update |
(spec: ISpec) => Promise<void> |
更新 spec 并重渲染 |
onElementClick |
(cb: (datum) => void) => void |
监听 mark 级点击 |
组件卸载时自动 chart.release()。
5. Hooks
useBreak()
响应式断点,阈值固定 768px(min-width: 768px 视为桌面)。
const { isMobile, isDesktop } = useBreak() // 两个 ComputedRef<boolean>
必须在 setup 内调用(内部用了 onMounted / onBeforeUnmount)。SSR 安全(typeof window 守卫,默认 true)。
useCaptcha(fetch)
验证码发送 + 60 秒倒计时。
const { waiting, send } = useCaptcha((data) => api.sendOTP(data))
await send({ phone: '13800138000' }) // 成功后 waiting 从 60 递减到 0
| 返回 | 类型 | 说明 |
|---|---|---|
waiting |
Ref<number> |
剩余秒数,0 表示可再次发送 |
send |
(para?: object) => Promise<unknown> |
调用传入的 fetch(para);成功后才启动倒计时;失败 reject 原始错误 |
send内部有loading互斥:请求进行中重复调用会静默返回一个永不 settle 的 Promise,不要await后依赖它继续。
useIcon()
const { i } = useIcon()
i('home', { size: 20, color: '#093' }) // → VNode(VueIcon) | undefined
i(icon, para?) 在 icon 为空时返回 undefined。用于给只接受 VNode 的 API 传图标(如 el-button :icon)。
useNanoid()
const { nanoid, numeric } = useNanoid()
nanoid() // 'V1StGXR8_Z5jdHi6B-myT'(21 位,安全随机)
numeric(6) // '834192'(纯数字,非安全随机,默认长度 10)
6. 工具类
四个都是静态类,无需实例化。
Formatter — 格式化 / 脱敏
Formatter.set({ locale: 'zh-CN', currency: 'CNY', decimals: 2, cents: true })
| 配置项 | 类型 | 默认 | 说明 |
|---|---|---|---|
locale |
string |
跟随环境 | 传给 Intl.NumberFormat |
currency |
string |
'CNY' |
默认币种 |
decimals |
number |
2 |
小数位(同时作为最小/最大位数) |
cents |
boolean |
true |
入参单位是否为「分」。默认 true → price 会除以 100;存「元」的系统必须设 false |
⚠️
Formatter.set()是整体替换config,不是合并。每次调用要传完整配置。
| 方法 | 示例 | 输出 |
|---|---|---|
id(str) |
Formatter.id('a1b2c3d4e5f6g7h8') |
'C3D4E5F6G7H8'(取末 12 位转大写) |
date(str) |
Formatter.date('2026-08-14T10:30:00Z') |
'2026-08-14 10:30:00'(纯字符串截取,不解析时区) |
phone(str, naked?) |
Formatter.phone('13800138000') |
'138****8000';naked=true 时原样返回 |
idcard(str) |
Formatter.idcard('110101199001011234') |
'110101****1234' |
email(str) |
Formatter.email('zhangsan@qq.com') |
'z****n@qq.com'(用户名 ≤2 位时不脱敏) |
bankcard(str) |
Formatter.bankcard('6222021234567890') |
'6222****7890' |
url(str) |
Formatter.url('https://a.com//b//c') |
'https://a.com/b/c'(压缩重复斜杠,保留协议) |
price(v, currency?) |
Formatter.price(12345) |
'¥123.45'(cents:true 时除以 100) |
currency(v, currency?) |
— | price 的别名 |
Validate — 校验
Validate.set('CN') // 影响 phone 规则;默认 '' 走国际规则
Validate.phone('13800138000') // true
| 成员 | 说明 |
|---|---|
set(country) |
设置国家码(大小写不敏感) |
phone(str) |
country === 'CN' → 1[3-9]\d{9};否则国际号 +?[1-9]\d{7,14} 或 \d{6,11} |
email(str) |
常规邮箱 |
idcard(str) |
15 / 18 位(末位可为 X) |
cname(str) |
中文姓名 2–16 字(含 ·) |
phone_pattern / email_pattern / idcard_pattern / cname_pattern / password_pattern |
直接取正则 |
⚠️ 有
password_pattern(8–20 位,且不能是纯字母/纯数字/纯符号),但没有Validate.password()方法,需自行Validate.password_pattern.test(str)。
DateTime — 日期格式化(dayjs 封装)
所有方法签名 (date?: dayjs.ConfigType, symbol = '/'),symbol 替换日期分隔符。
| 方法 | 输出格式 | 示例(symbol='-') |
|---|---|---|
DateTime.s(d) |
YYYY/MM/DD HH:mm:ss |
2026-08-14 10:30:00 |
DateTime.m(d) |
YYYY/MM/DD HH:mm |
2026-08-14 10:30 |
DateTime.h(d) |
YYYY/MM/DD HH |
2026-08-14 10 |
DateTime.d(d) / DateTime.date(d) |
YYYY/MM/DD |
2026-08-14 |
DateTime.M(d) |
YYYY/MM |
2026-08 |
DateTime.y(d) |
YYYY |
2026(无 symbol 参数) |
DateTime.time(d) |
HH:mm |
10:30(无 symbol 参数) |
DateTime.format(d, fmt?) |
自定义,默认 YYYY/MM/DD HH:mm:ss |
— |
DateTime.dayjs |
原始 dayjs 实例 | 需要插件/额外能力时用 |
Platform — 环境判断
全部是 getter(Platform.isIOS,不要加括号)。依赖 window,不可在 SSR 顶层调用。
| Getter | 判断依据 |
|---|---|
userAgent |
小写 UA |
isWeb |
matchMedia('(min-width: 992px)') —— 注意这是宽度判断,与 useBreak 的 768px 阈值不同 |
isMobile |
UA 含 phone/iPhone/Android/Mobile/BlackBerry… |
isIOS |
UA 含 iphone/ipad/ipod/ios |
isFlutter |
UA 含 flutter |
isFlutterIOS |
isFlutter && isIOS |
isWechat |
UA 含 MicroMessenger |
isWxwork |
MicroMessenger + wxwork |
isMiniprogram |
MicroMessenger + miniprogram |
7. 类型定义
interface IModel { // 所有业务模型的基类,带索引签名
id?: string | any
created_at?: string
updated_at?: string
deleted_at?: string
[property: string]: any
}
interface IAsset extends IModel { // 资源(图片/视频/文件)
id: string; type: string; title: string
url: string // 原图
shrink: string // 缩略图
}
interface IField extends IModel { // 表单字段描述
name: string
type: string
readonly?: boolean
multiple?: boolean
required?: boolean
options?: { label: string; value: string }[] | any[]
}
type IFilter = Omit<IField, 'name'> & { label?: string; rules?: any[] }
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 IUploadUserFile {
id?: string; 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[]
}
element-go有一份字段更多的同名类型(IField.hidden/advanced/component、IUploadUserFile.uid/name/status/raw等)。 在 element-go 工程里请从@gindow/element-go导入,不要从本包导入。
8. 国际化
Vue3 provide/inject 实现,不依赖 vue-i18n。
import { setLocale, getLocale, useLocale } from '@gindow/vue'
setLocale('en')
getLocale() // 'en'
const { locale, t } = useLocale()
t('empty.noData') // 'No data'
t('some.key', { name: 'Tom' }) // 支持 {name} 占位符插值
初始语言探测顺序:localStorage.getItem('locale') → navigator.language 完全匹配 → 语言族匹配(en-US → en)→ 兜底 'en'。可用码:['en', 'zh-CN']。
内置文案(zh-CN / en 两份):
cancel · confirm · empty.noData · dialog.tip · dialog.confirm
扩展/覆盖语言包:
app.use(Vue, {
locale: 'zh-CN',
messages: { 'zh-CN': { ...myMessages } },
})
⚠️ 本包的
t()没有兜底:messages[locale]被整体替换后,找不到的 key 会原样返回路径字符串。 传messages时必须把内置的 5 个 key 一起带上,或改用 element-go / vant-go —— 那两个包有永不被覆盖的base底座(见各自 README)。
9. 样式与级联层
@gindow/vue/style.css 做三件事:
- 声明级联层规范顺序(整个体系只声明这一次):
每层放一条@layer theme, base, vendor, element-plus, components, utilities;.gindow-layer-anchor占位规则,防止构建期压缩把「空层」从顺序声明里剪掉导致层序错乱。 @import "tailwindcss",并@source扫描本包 +../../vant-go/src+../../element-go/src(monorepo 相对路径,npm 安装场景下这两条不生效)。- 提供通用工具类。
| 工具类 | 作用 |
|---|---|
.flex-center |
flex + 居中(主轴/交叉轴) |
.flex-center-end |
flex + 交叉轴居中 + 主轴靠右 |
.flex-center-between |
flex + 交叉轴居中 + space-between |
.flex-center-items |
flex + 仅交叉轴居中 |
.bg-light / .text-light |
#ccc(.dark 下自动变 #333) |
.mob |
≥768px 时隐藏(仅手机端显示) |
.web |
≤768px 时隐藏(仅电脑端显示) |
为什么必须分层:CSS 规范里无层样式永远赢过有层样式,与源码顺序、特异性无关。 Tailwind v4 把工具类放进
@layer utilities,而第三方组件库默认无层——不处理的话<van-button class="mt-6">的mt-6会被静默吃掉,只能靠满屏!去救。
10. 两种消费模式
exports 的 @source 条件指向 src,默认条件指向 dist。
npm 包模式(默认,推荐)
直接安装使用,吃 dist,组件已编译完毕。不要在 vite.config.ts 里开 resolve.conditions: ['@source']。
源码包模式(submodule / monorepo)
// vite.config.ts
export default defineConfig({
resolve: {
conditions: ['@source'],
alias: { '@gindow/vue': resolve(__dirname, '../packages/vue/src') },
},
})
此时源码由宿主 app 编译,因此宿主必须满足:
unplugin-auto-import覆盖到这些文件(本包源码依赖自动导入注入computed/ref等);- 自行安装
dayjs、nanoid(npm 模式下它们已内联,源码模式下需要真实依赖)。
反过来,npm 包模式下开
@source会直接报computed is not defined—— AutoImport 默认不处理node_modules。
11. 开发
pnpm install
pnpm build # vue-tsc --noEmit 类型检查 + vite 构建(dist/ 双产物 + style.css)
产物:dist/vue.mjs、dist/vue.cjs、dist/vue.d.ts、dist/resolver.*、dist/style.css。
external 仅 vue 与 @iconify/vue,其余依赖内联。
License
MIT © joming
Version History
| Version | Released | License |
|---|---|---|
| 1.0.8 | MIT | |
| 1.0.7 | MIT | |
| 1.0.6 | MIT | |
| 1.0.5 | MIT | |
| 1.0.4 | MIT | |
| 1.0.3 | MIT | |
| 1.0.2 | MIT | |
| 1.0.1 | MIT | |
| 1.0.0 | MIT |