@owocow/saber-ui
v0.1.31
Published
Saber application foundation for Vue 3 projects built on Nuxt UI
Readme
@owocow/saber-ui
Saber application foundation for Vue 3 projects, built on top of @nuxt/ui.
Documentation
The VitePress documentation site lives in docs/ and runs directly against the local src/ code:
pnpm docs:dev
pnpm docs:build
pnpm docs:preview近期新增能力和升级注意事项见近期更新与升级。
图片相关文档:SaberImageViewer 图片查看器、SaberUploadImage 图片上传。
Install
npm install @owocow/saber-ui @nuxt/uiRequires:
vue^3,@nuxt/ui^4
Consumption contract
重要:saber-ui 的组件在宿主编译时需要 nuxt/ui 的
#imports/#build/*虚拟模块, 任何使用 saber-ui 的宿主都必须运行@nuxt/ui/vite(Vite 项目下让 nuxt/ui 脱离 Nuxt 运行的标准插件)。 不满足该条件时,组件内的<UBadge>/<UIcon>与主题配置将无法解析。
// vite.config.ts
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
theme: {
colors: ['primary', 'secondary', 'success', 'info', 'warning', 'error'],
},
// ... 主题配置会同样作用到 saber-ui 内部的 nuxt/ui 组件
}),
],
})关于
@owocow/saber-ui/vite:它只是@nuxt/ui/vite的转发导出(re-export), saber-ui 不自带 Vite 插件。直接使用@nuxt/ui/vite即可,不必引入这个入口。
Usage
npm install @owocow/saber-ui @nuxt/ui// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import SaberUI from '@owocow/saber-ui'
const app = createApp(App)
app.use(SaberUI)
app.mount('#app')<!-- Anywhere in your app -->
<template>
<BadgeStatus
:value="row.status"
:config="['闲置:0:green', '已预订:1:blue', '使用中:2:orange']"
/>
</template>On-demand import
组件始终需要宿主提供 @nuxt/ui/vite,此处仅指按需引入组件而不是注册全部:
<script setup>
import { BadgeStatus } from '@owocow/saber-ui'
</script>
<template>
<BadgeStatus
:value="row.status"
:config="['闲置:0:green', '已预订:1:blue', '使用中:2:orange']"
/>
</template>Image viewer
SaberImageViewer 用于查看已有图片,支持底部切图、25%–300% 缩放和每次 90° 旋转。执行 app.use(SaberUI) 后可全局使用,也可按需导入:
<script setup lang="ts">
import SaberImageViewer from '@owocow/saber-ui/components/SaberImageViewer'
const images = [
{ src: '/license.jpg', label: '营业执照' },
{ src: '/identity.jpg', label: '法人身份证' },
]
</script>
<template>
<SaberImageViewer v-slot="{ open }" :images="images">
<button type="button" @click="open(0)">查看证照</button>
</SaberImageViewer>
</template>支持 :masked="true" 图片脱敏,默认关闭。开启后,每张图片独立覆盖 12px 背景模糊遮罩,并禁用点击、键盘和程序打开预览;已打开的预览立即关闭,取消脱敏后需重新打开。
Ref 方法、Model 控制、API 和迁移方式见 SaberImageViewer 文档。缩略图铺满容器、圆角和遮罩范围见缩略图布局与遮罩范围。查看已有图片无需上传数据源。
Forms
app.use(SaberUI) 会全局注册 SaberFilterBar、SaberFormModal、SaberModal 和基础 Saber 字段,并为动态字段提供 app 级 registry。
<script setup lang="ts">
import { ref } from 'vue'
import type { FilterField } from '@owocow/saber-ui/forms'
const params = ref({ keyword: '', status: undefined })
const fields: FilterField[] = [
{
key: 'keyword',
which: 'ui-input',
props: { placeholder: '搜索名称' },
},
{
key: 'status',
which: 'ui-select',
props: { items: [{ label: '启用', value: 'enabled' }] },
searchOn: 'update:model-value',
},
]
</script>
<template>
<SaberFilterBar v-model="params" :fields="fields" @search="loadData" @reset="resetParams" />
</template>基础 registry 包含 saber-input、saber-select、saber-date-picker、saber-month-picker、saber-date-time-picker、saber-time-picker 等 Saber 字段,以及 ui-input、ui-select、ui-textarea 等 Nuxt UI 原生字段映射。日期时间字段族也会被全局注册,统一支持字符串值和边界控制。
文本输入默认在 change 时触发搜索;Select、Radio 等值变更后立即搜索的字段应设置 searchOn: 'update:model-value'。复杂业务字段可使用 searchOn: 'manual'。
项目自定义字段在安装时提供:
import SaberUI from '@owocow/saber-ui'
import SupplierPicker from './SupplierPicker.vue'
app.use(SaberUI, {
forms: {
components: {
'supplier-picker': SupplierPicker,
},
},
})字典字段由宿主提供数据源,saber-ui 不依赖具体 store 或接口:
const dictStore = useDictStore()
app.use(SaberUI, {
forms: {
dictDataSource: {
async loadOptions(dictType) {
await dictStore.loadDict(dictType)
return dictStore.getOptions(dictType)
},
},
},
})配置后可使用 saber-dict-radio、saber-dict-select、saber-dict-checkbox,或直接使用全局组件 SaberDictRadio、SaberDictSelect、SaberDictCheckbox。SaberDictSelect 默认基于 SaberPopSelect;加上 searchable 后使用 USelectMenu,在弹层内按标签本地过滤。
需要直接在外层输入框搜索静态选项或业务接口时,使用 SaberSearchSelectV1:
<script setup lang="ts">
import { ref } from 'vue'
import { SaberSearchSelectV1 } from '@owocow/saber-ui/forms'
const brandId = ref<string | null>(null)
const brands = [{ label: 'Apple', value: 'apple' }, { label: '华为', value: 'huawei' }]
</script>
<template>
<SaberSearchSelectV1 v-model="brandId" :options="brands" placeholder="搜索品牌" />
</template>静态数据支持 options / items;远程模式传入 api 与 search-key,兼容 { data: { rows } } 和 { data: [] }。支持多选、initialOptions 回显、params 级联和 change(value, row)。右侧同一位置按 loading → 清除 → 下拉箭头互斥显示。组件默认注册为 saber-search-select-v1,完整 API 与示例见 SaberSearchSelectV1。
树形数据使用 SaberCascader:默认按钮选择,searchable 开启外层输入搜索;节点 disabled: true 禁用整棵子树,optionsDisabled 可接管禁用判定。SaberDistrictSelect 则通过 maxLevel="province" / "city" / "district" 控制选择深度,提交值的 districtCode 始终表示最终选中节点的编码。
行政区划选择和地址解析共用 app 级地区服务。使用内置高德数据源时只需配置 Web 服务 Key:
app.use(SaberUI, {
forms: {
district: {
amapKey: import.meta.env.VITE_APP_AMAP_KEY,
},
},
})配置后,动态表单可直接使用 saber-district-select,无需页面引入组件、加载数据或传递 districts:
const fields = [{
key: 'districtValue',
label: '所在地区',
which: 'saber-district-select',
}]地区数据仅在 SaberDistrictSelect 首次渲染或地址首次解析时加载;内置高德数据源默认使用 7 天 localStorage 缓存。也可以通过 forms.district.dataSource 替换成其他数据源。
自定义数据源、DistrictService 和区划类型可从 @owocow/saber-ui/district 独立导入;该入口不加载 UI 组件。
图片上传与图片/文档混合上传组件通过独立入口按需使用,上传、删除和响应字段由宿主适配:
import type { UploadImageDataSource } from '@owocow/saber-ui/forms'
const uploadImageDataSource: UploadImageDataSource<{ ossId: string; url: string }> = {
upload: (file, { onProgress }) => uploadFile(file, onProgress),
remove: value => deleteFile(value.ossId),
getKey: value => value.ossId,
getUrl: value => value.url,
}
app.use(SaberUI, {
forms: { uploadImageDataSource },
})<script setup lang="ts">
import SaberUploadImage from '@owocow/saber-ui/forms/upload-image'
</script>
<template>
<SaberUploadImage v-model="avatar" />
</template>SaberUploadImage 与 SaberUploadFile 都不进入默认 registry;动态表单需要时显式注册 saber-upload-image / saber-upload-file。混合上传入口为 @owocow/saber-ui/forms/upload-file,两个组件共用 forms.uploadImageDataSource。
remove(value, { origin: 'uploaded' }) 仅为当前实例中新上传的文件调用;初始或父组件写入的文件移除时只更新 Model,clear() 也不调用远程删除。SaberUploadImage 的大图预览复用 SaberImageViewer,支持底部切图、缩放和旋转;混合上传中的文档点击后在新窗口打开原文件。
图片上传框和缩略图默认统一为 96×96px,通过 thumbnail-size="size-32" 可同时调整两者尺寸。表单自动传入 size="lg" 等通用大小不会影响缩略图,旧的 size="size-32" 写法继续兼容。示例与完整 API 见 SaberUploadImage 文档。
Utilities
通用应用能力通过独立子路径提供,不会从 @owocow/saber-ui 根入口自动加载:
import { setupHttp, http } from '@owocow/saber-ui/utils/http'
import { setupStorage, storage } from '@owocow/saber-ui/utils/storage'
import { setupPermission, canAccess } from '@owocow/saber-ui/utils/permission'
import { getToast, setToast } from '@owocow/saber-ui/utils/toast'初始化 Permission 后,可以使用全局 SaberAuth 组件控制模板片段,并提供无权限回退内容:
<SaberAuth :permissions="['customer:info:add']">
<UButton>新增客户</UButton>
<template #fallback>
<span>无操作权限</span>
</template>
</SaberAuth>HTTP 对带数值 code 的业务响应,只有 200 正常 resolve,其他业务码会提示并抛出 HttpBusinessError;仅 401 调用 onAuthError。useTable 会消化请求阶段的业务错误并保留现有列表;useDelete 对取消、重复调用或删除业务错误返回 false,确认弹窗和接口执行期间均有重复调用守卫。升级时应检查依赖“错误响应仍 resolve”的调用代码,详见 HTTP和确认与删除。
Vue 组件内推荐使用 composable;它会自动等待安装时配置的共享地区服务,并复用地址解析索引:
import { useAddressParse } from '@owocow/saber-ui/composables/use-address-parse'
const { parseAddress } = useAddressParse()
const results = await parseAddress('张三 浙江省杭州市西湖区文三路90号 13800138000', true)底层地址解析器仍是纯函数,适用于 Node 脚本、单元测试或自定义数据:
import { createAddressParser } from '@owocow/saber-ui/utils/addressParse'
const parser = createAddressParser(customDistricts)
const results = parser.parse('张三 浙江省杭州市西湖区文三路90号 13800138000', true)行政区划节点至少需要包含 name、adcode 和可选的 districts。传入数据应以省级节点作为数组第一层;底层解析器不会请求网络,也不会依赖 Vue。
可用入口包括 http、storage、rsa、crypto、permission、toast、format、filter、tree、klona、nanoid 和 addressParse。
公共后端协议和基础类型通过独立入口导入:
import type { ApiResponse, AuthUserInfo, Id, ListResponse, PageParams } from '@owocow/saber-ui/types'该入口只包含基础、API 和认证协议类型,不包含具体 System 或业务 DTO。
http、storage、rsa、crypto 与 toast 共享模块级初始化状态,迁移或初始化时应始终从 @owocow/saber-ui/utils/* 引用,不能与旧的 @owocow/saber-utils/* 混用。
版本对齐
saber-ui 通过 @nuxt/ui 的裸导入引用宿主的 nuxt/ui 副本,请保持 @nuxt/ui
主版本(^4)一致;宿主如遇解析到多份副本,可在 Vite 配置中加
resolve: { dedupe: ['@nuxt/ui', '@nuxt/icon'] } 强制解析到宿主副本。
Components
BadgeStatus
Status badge driven by label:value:color config. Renders <UBadge> from @nuxt/ui.
Colors use inline styles (hex → rgba) to avoid Tailwind v4 JIT dynamic-class issues.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string \| number | (required) | Current value, matched against config's value segment |
| config | string[] | (required) | 'label:value:color' array, color = Tailwind color name (500/600 shades) |
| bgOpacity | number | 20 | Background opacity (0–100) |
| textColorWeight | number | 500 | Text color Tailwind shade |
License
MIT
