@gindow/vue
v1.0.8
Published
Vue3 通用基础包,包含工具函数、Hooks、类型定义、国际化、基础样式
Readme
@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 vuevue ^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) | undefinedi(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('[email protected]') | 'z****[email protected]'(用户名 ≤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
