bingbingyihao-focus-ui

Focus UI:聚焦 Dialog / Confirm / Message / Select / DatePicker 五个高频交互组件的 Vue 3 组件库,内置黑白灰极简、暖色、冷色三套主题
181
Install
npm install bingbingyihao-focus-ui
Latest Version:0.1.0
Vue:3
License:Apache-2.0
Last Updated:Sep 29, 2026
Links: Gitee  ·  npm
Maintainer: bingbingyihao

Focus UI

聚焦 5 个高频交互组件的 Vue 3 组件库,内置 黑白灰极简 / 暖色系 / 冷色系 三套主题。

不做大而全的组件库。绝大多数业务页面反复用到的其实只有这几个: Dialog(对话框)、Confirm(确认框)、Message(消息提示)、Select(下拉)、DatePicker(日期选择) —— 而它们恰恰是原生控件最不好用、各平台差异最大、最影响"整体质感"的五个。 所以这里只把它们做透:每个都能独立拿走用,且共享同一套设计令牌。

┌─ 组件 ────────────────────────────────────────────────────────────┐
│ FxSelect        单选 / 多选 / 搜索 / 分组 / 色点 / 清除 / 键盘       │
│ FxDatePicker    单选 / 区间 / 日-月-年三级视图 / 快捷预设 / 格式化    │
│ FxDialog        遮罩 / Esc / 焦点陷阱 / 锁滚动 / 命令式 openDialog   │
│ FxConfirm       命令式 confirm() → Promise<boolean>,支持异步校验    │
│ message         命令式 message.success / error / warning / loading │
│ FxButton        统一按钮(弹窗 / 提示内部与业务页共用)               │
└─ 主题 ────────────────────────────────────────────────────────────┘
   data-theme="mono" | "warm" | "cool"   →  只换令牌,组件代码零改动

快速开始

npm install
npm run dev        # http://127.0.0.1:5175/  演示页(顶栏可切三套主题)

在别的工程里使用

npm install bingbingyihao-focus-ui

装进来的只有组件本体与三份样式(src/ui/ + src/styles/),演示页与测试脚本不在包内。

// main.js —— 样式顺序不能反:基线里会用主题令牌,必须在其之后解析
import 'bingbingyihao-focus-ui/styles/tokens.css' // 结构令牌:字体 / 间距 / 动效 / 层级
import 'bingbingyihao-focus-ui/styles/themes.css' // 三套主题令牌
import 'bingbingyihao-focus-ui/styles/base.css' // reset + 原生控件归一化

import FocusUI from 'bingbingyihao-focus-ui'
createApp(App).use(FocusUI) // 方式一:全量注册(FxSelect / FxDialog ...)

// 方式二:按需引入(推荐)
import { FxSelect, confirm, message, setTheme } from 'bingbingyihao-focus-ui'

本包以源码形式发布(直接暴露 .vue 文件),因此需要打包器能编译 node_modules 里的 单文件组件 —— Vite / webpack + vue-loader / Nuxt 都可以。包只依赖 Vue 3.4+, 且 vue 声明在 peerDependencies,不会把第二份 Vue 装进你的工程。

主题令牌挂在 :root 上,所以必须引入 themes.css 才能拿到颜色; 不想全量引 base.css 也行,但它负责把原生 select / input / textarea 归一化, 少了它页面里会出现"自绘组件很精致、旁边原生控件很土"的割裂感。

三套主题

import { setTheme } from 'bingbingyihao-focus-ui'

setTheme('warm') // mono | warm | cool
// 等价于:
document.documentElement.dataset.theme = 'warm'

三套主题的差异不只是色相 —— 圆角、阴影、边框强度、字重都在变。 只换颜色的话,三套主题看起来会像同一个东西换了皮:

黑白灰极简 mono 暖色系 warm 冷色系 cool(默认)
主色 #18181b 近黑 #c2410c 橙 #2563eb 蓝
页面底色 #f5f5f6 #fdf8f2 暖白 #f4f7fc 冷白
圆角 md 6px 小硬 12px 大柔 10px 中
阴影 几乎不投影,靠"线"分层 柔和暖阴影 清晰冷阴影
边框 对比更强(#b9b9c0) 压得很淡 中性
强调字重 700 600 600
危险色 仍保留红(语义不能丢) 砖红 正红

两个配套能力:

  • 可以局部生效。令牌挂在 [data-theme] 选择器上,所以任意元素都能套一套风格 —— 演示页的「主题与令牌」区块就是并排渲染了三套风格,不用来回切换。 ⚠️ 唯一的例外是 Teleport 到 body 的浮层(下拉面板 / 日历 / 弹窗),它们只跟随 html 上的主题。
  • 令牌表读的是真实值。演示页的令牌对照表是用游离探针元素 getComputedStyle 读出来的, 改了 themes.css 表格会跟着变,不存在"文档写了但代码没改"的偏差。

令牌清单

组件样式只引用 --fx-*,因此换主题不需要动任何组件代码。

分类 令牌
表面层级 --fx-color-bg → --fx-color-surface → -2 → -3
描边 --fx-color-border、--fx-color-border-strong
文本 --fx-color-text、-2、-3
强调色 --fx-color-primary(-hover/-active/-weak/-contrast)、danger、success、warning、info
遮罩 --fx-color-mask
形状 --fx-radius-xs/sm/md/lg/full、--fx-shadow-1/2/3、--fx-ring
质感 --fx-border-width、--fx-font-weight-strong
结构 --fx-font-*、--fx-space-*、--fx-control-*、--fx-dur-*、--fx-ease、--fx-z-*

三套主题的令牌集合由 npm run test:theme 强制保持一致:少写一个令牌就会回退到上一套主题的值, 表现为"切了主题但某个角落没变",这类问题在页面上极难靠肉眼发现。

组件

FxSelect 下拉选择

替代原生 <select>(展开面板由浏览器渲染,样式完全不可控,各平台外观差异最大)。

<FxSelect v-model="value" :options="options" clearable placeholder="请选择" />

<FxSelect v-model="list" multiple searchable :options="grouped" :max-tag-count="3" block />

<script setup>
const options = [
  { value: 'a', label: '选项 A' },
  { value: 'b', label: '选项 B', hint: '推荐', color: '#2563eb' },
  { value: 'c', label: '禁用项', disabled: true },
]
// 分组写法可与扁平写法混用
const grouped = [{ label: '流程控制', options: [{ value: 'x', label: '条件分支' }] }]
</script>
属性 / 事件 类型 说明
v-model any | any[] 当前值,多选时为数组
options Array 支持扁平与分组两种写法混用
multiple boolean 多选:选中后不关闭面板,标签超出 maxTagCount 折叠为 +N
searchable boolean 面板内搜索框;分组标题会跟着过滤结果一起消失
clearable / disabled / loading boolean 清除按钮 / 禁用 / 载入中
size 'sm' | 'md' | 'lg' 尺寸
block boolean 占满父容器宽度
max-tag-count number 多选时最多展示几个标签(默认 2)
empty-text string 无匹配项时的文案
align 'start' | 'end' 浮层对齐方式
@change / @clear (value) => void / () => void 值变化 / 点击清空

键盘:↑ ↓ Home End Enter Esc Tab;多选时 Backspace 删掉最后一个标签。

FxDatePicker 日期选择

替代原生 input[type=date](无法主题化,且完全不支持区间)。

<!-- 默认产出 Date 对象,方便参与计算 -->
<FxDatePicker v-model="day" clearable />

<!-- 直接产出字符串,方便提交给接口 -->
<FxDatePicker v-model="day" value-format="YYYY-MM-DD" />

<!-- 区间 + 快捷预设 -->
<FxDatePicker v-model="range" type="range" :shortcuts="shortcuts" :max="new Date()" />
属性 / 事件 类型 说明
v-model Date | [Date, Date] | string | number 区间模式为数组
type 'date' | 'range' 单选 / 区间
value-format 'date' | 'timestamp' | pattern 值形态:Date 对象(默认)/ 毫秒时间戳 / 按模式串格式化
format string 展示格式,缺省取全局配置(YYYY-MM-DD)
shortcuts Array [{ label, value: Date | [Date,Date] | () => ... }]
min / max Date | string | number 可选范围,超出部分置灰
disabled-date (date) => boolean 自定义不可选规则
week-starts-on number 一周从周几开始,1 = 周一(默认)
range-separator string 区间连接符,默认 " ~ "
clearable / disabled / size / block — 同 FxSelect
@change / @clear (value) => void / () => void 值变化 / 点击清空

面板支持 日 → 月 → 年 三级视图(点标题逐级上升):选去年某天不用点十几次箭头。 区间模式的中间态(已点第一下、还没点第二下)只在面板内部存在,不会向外抛出 [Date, null] 这种半成品。

FxDialog 对话框

<FxDialog v-model:open="open" title="编辑资料" :width="460">
  <FxSelect v-model="role" :options="roles" block />
  <template #footer>
    <FxButton @click="open = false">取消</FxButton>
    <FxButton variant="primary" :loading="saving" @click="save">保存</FxButton>
  </template>
</FxDialog>
属性 / 事件 说明
v-model:open 受控开合;不传 open 即为非受控,组件自己管状态
title 标题;也可用 #header 插槽完全自定义
width 数字按 px,也可传任意 CSS 宽度;默认取全局配置(420)
placement center 居中(默认)/ top 顶部对齐(长表单更合适)
close-on-mask / close-on-esc 默认均为 true
show-close 是否显示右上角关闭按钮
lock-scroll 打开时锁 body 滚动(含滚动条宽度补偿),默认 true
#default / #header / #footer 主体 / 头部 / 底部
@open @opened @close @closed 开合前后事件;closed 在关闭动画结束后触发
defineExpose close()

模态该有的默认行为都做了:遮罩、Esc、点击遮罩关闭、焦点陷阱(Tab 不会跑到弹窗外的元素上)、 关闭后焦点归还触发按钮、锁滚动、打开即聚焦第一个可操作元素。

命令式:

const dialog = openDialog({
  title: '说明',
  content: '字符串 / VNode / 渲染函数都支持',
  footer: () => h(FxButton, { onClick: () => dialog.close() }, () => '知道了'),
})

FxConfirm 确认框

import { confirm, message } from 'bingbingyihao-focus-ui'

if (await confirm({ title: '确认删除?', danger: true, confirmText: '删除' })) {
  await api.remove(id)
  message.success('已删除')
}

// 需要异步校验时:返回 false 就不关闭,期间确定按钮 loading
await confirm({
  title: '提交审批?',
  onConfirm: async () => await checkSomething(),
})
参数 / 属性 说明
confirm(options) 返回 Promise<boolean>:确定 → true;取消 / Esc / 遮罩 → false
options.onConfirm 返回 false 或 Promise<false> 表示"校验没过",弹窗保持打开
danger / icon 危险操作(按钮与图标变红)/ question | warning | danger | success
message / description 主文案 / 次要说明
confirm-text / cancel-text 按钮文案
v-model:open 组件式用法;@confirm / @cancel 事件

语义约定:取消一定关闭;确定在非受控用法下自动关闭,受控 / 命令式时交给调用方 (因为命令式需要等异步校验结束才关,乐观关闭会让 loading 来不及显示)。

message 消息提示

import { message } from 'bingbingyihao-focus-ui'

message.success('保存成功')
message.error('请求失败,请稍后重试', { title: '出错了' })
message.warning('这条不会自动消失', { duration: 0 })

const handle = message.loading('正在上传…')
await upload()
handle.close()
API / 选项 说明
message(content, options) 基础调用(等价 message.info),返回 { id, close }
.success .error .warning .info 四种语义类型
.loading 载入中,默认不自动关闭,用返回句柄手动 close()
options.title 标题行
options.duration 自动关闭时间(ms),0 常驻,默认 3000
options.closable / options.max 关闭按钮 / 同屏最大条数(默认 5,超出移除最早的)
message.close(id) / .closeAll() 关闭某条 / 全部

细节:鼠标悬停会暂停倒计时且不重置 —— 划过时不会让刚出现的长文案立刻消失。

FxButton 按钮

属性 说明
variant primary | default | ghost | text | danger
size sm | md | lg
loading / disabled 载入态(带 spinner)/ 禁用
block 占满父容器宽度

全局配置

import { setConfig } from 'bingbingyihao-focus-ui'

setConfig({
  dialog: { width: 480 },
  message: { duration: 2000, max: 3, placement: 'top-center' }, // top-right(默认) | top-center | bottom-right | bottom-center
  datePicker: { weekStartsOn: 0, format: 'YYYY/MM/DD' },
})

优先级:组件 props > 全局配置 > 代码内兜底常量。

目录结构

ui-framework/
├─ index.html / vite.config.js
├─ eslint.config.js        # ESLint 扁平配置(vue3-recommended + 与 Prettier 分工)
├─ .prettierrc.cjs / .editorconfig / .gitattributes
├─ scripts/
│  ├─ unit-test.mjs        # 纯 Node 单测:日期工具 + 选项工具(67 项断言)
│  ├─ theme-test.mjs       # 解析 themes.css:令牌一致性 + 对比度 + 三套必须真的不同(36 项)
│  ├─ smoke.mjs            # 真实浏览器端到端冒烟(78 项断言,含 pageerror / console.error 检查)
│  ├─ format.mjs           # ★ 一键 lint + format(跨平台,Mac / Linux / CI 用这个)
│  └─ format.ps1           # ★ 同上,Windows PowerShell 版(纯 ASCII,见文件内注释)
└─ src/
   ├─ main.js / App.vue    # 演示壳:顶栏主题切换 + 侧栏锚点 + 内容区
   ├─ styles/
   │  ├─ tokens.css        # 结构令牌(字体 / 间距 / 控件高度 / 动效 / 层级)
   │  ├─ themes.css        # ★ 三套主题令牌(改这里就能加第 4 套)
   │  └─ base.css          # reset + 原生控件归一化 + 表单触发器基类 .fx-field
   ├─ ui/
   │  ├─ index.js          # 统一出口 + Vue 插件 + setTheme / dateUtils / selectUtils
   │  ├─ config.js         # 全局配置
   │  ├─ composables/
   │  │  ├─ useZIndex.js        # 浮层层级:全局单调递增
   │  │  ├─ useScrollLock.js    # 滚动锁:计数式,支持多个弹窗叠加
   │  │  ├─ usePopupPosition.js # 浮层定位:fixed + 空间不足自动翻转 + 先定位后显示
   │  │  ├─ useFocusTrap.js     # 焦点陷阱 + 焦点归还
   │  │  └─ useClickOutside.js  # 点击外部关闭(mousedown 捕获阶段)
   │  └─ components/
   │     ├─ base/FxButton.vue
   │     ├─ select/{FxSelect.vue, options.js}          # options.js 为纯函数(可单测)
   │     ├─ date-picker/{FxDatePicker.vue, CalendarPanel.vue, dateUtils.js}
   │     ├─ dialog/{FxDialog.vue, imperative.js}       # imperative.js = openDialog + confirm
   │     └─ message/{message.js, MessageHost.vue, MessageItem.vue}
   └─ views/               # 演示页(与组件库本体无关,复用时可整目录删掉)
      ├─ DemoView.vue / demo.css
      └─ sections/{Theme,Select,DatePicker,Dialog,Message}Section.vue

质量保障

npm run fix           # 一键:eslint --fix + prettier --write
npm run verify        # 一键:lint + 格式 + 单测/主题测试,全绿才退出 0
npm run lint          # ESLint 修复(vue3-recommended)
npm run lint:check    # ESLint 只检查不修改
npm run format        # Prettier(无分号 / 单引号 / 100 列 / ES5 尾逗号)
npm run format:check  # Prettier 只检查不修改
npm run test          # = test:unit + test:theme,纯 Node,不需要浏览器
npm run build         # 生产构建
npm run dev           # 另开终端
npm run smoke         # 真实浏览器端到端(复用本机 Edge / Chrome)

提交前跑一条就够:

npm run verify                                                     # 任意平台
powershell -ExecutionPolicy Bypass -File scripts/format.ps1 check   # Windows

ESLint 与 Prettier 的分工:格式一律归 Prettier,ESLint 只管"会不会出错" (未使用变量、重复的 prop 键、改 props、组件名多词……)。两者冲突的规则由 @vue/eslint-config-prettier/skip-formatting 关闭,它必须放在配置数组最后, 否则会被后面的 recommended 覆盖回去。

为什么要 .gitattributes:本机 git 若开了 core.autocrlf=true,工作区文件会变成 CRLF,而 .prettierrc.cjs 是 endOfLine: 'lf' —— format:check 会对每一个文件报错, format 再改回 LF,下次检出又变 CRLF,来回抖。行尾不该由"谁的机器怎么配的 git"决定, 统一写进仓库:* text=auto eol=lf。

  • test:unit:日期计算是这类组件最容易出错的地方 —— 月份网格少一天、1/31 + 1 月 溢出到 3 月、 2025-02-30 被静默滚成 3 月 2 日…… 这些在界面上只表现为"某个日期莫名点不了",靠肉眼几乎发现不了。
  • test:theme:直接解析 themes.css 文本。三套主题的令牌集合必须完全一致;正文 / 次级文字 / 主色上的文字需满足对比度下限(4.5:1、4.5:1、3:1);主色与圆角必须两两不同。
  • smoke:真浏览器里跑完整链路,并断言全程无 pageerror 与 console.error。

发布(维护者)

包以源码形式发布,因此没有构建步骤,发布前跑一遍校验即可:

npm config get registry     # 必须是 https://registry.npmjs.org/,本地若用镜像要先切回来
npm login                   # 需要 2FA 或 granular access token
npm run verify              # lint + 格式 + 单测 / 主题测试(prepublishOnly 也会自动跑)
npm pack --dry-run          # 逐条确认打包清单:只应有 src/ui、src/styles、README、LICENSE
npm publish                 # 包名非 scoped,且 publishConfig.access 已设为 public
git tag v0.1.0 && git push origin v0.1.0

两个注意点:

  • files 字段是白名单,src/views、scripts、index.html 都不会进包; 若新增「使用者需要但不属于 src/ui / src/styles」的文件,记得同步加进 files。
  • 版本号一旦发布成功就不可复用,改动后必须 npm version patch | minor | major 再发。

设计取舍

为什么只用 CSS 变量做主题,不上 CSS-in-JS。 三套主题 = 三份令牌,加一套主题就是复制一段 :root 区块;而组件代码一行不用改,也不需要额外的运行时依赖。

为什么自绘 Select / DatePicker 而不是包一层原生控件。 原生控件的展开面板不在我们的样式 作用域内(下拉、日历、window.confirm 都是),包一层的结果永远是"框好看、面板丑"。 既然要统一风格,就得自己画。

为什么不引 dayjs / date-fns。 这个组件只需要「取当月网格 + 格式化 + 加减 + 比较」四件事, 为它引入一个日期库,等于把库的体积与升级成本绑在一个无关紧要的依赖上。 dateUtils.js 是纯函数,反而更好测。

为什么浮层层级全局递增,而不是按组件类型分层。 「后打开的必须盖住先打开的」是唯一成立的规则。 按类型分层(比如 dialog 永远高于 select)会导致「从下拉面板里点出一个对话框, 结果对话框被自己的下拉压住」。

为什么 Esc 监听挂在 document 的冒泡阶段。 挂在弹窗元素上时,焦点一旦跑到弹窗外的元素, Esc 就会"失灵";用捕获阶段则会出现"一次 Esc 把下拉和弹窗一起关掉"。 冒泡 + 检查 event.defaultPrevented,才能做到「先关内层、再关外层」。

代码风格参考 workflow-orchestrator: 无分号 / 单引号 / 100 列 / ES5 尾逗号 + .editorconfig,注释一律写"为什么这么设计"而不是复述代码。

后续可扩展

  • Tooltip / Popover:usePopupPosition 已经是通用的,补一层触发逻辑即可;
  • 表格与分页:目前刻意不做,等有真实需求再进;
  • 暗色模式:令牌已按「表面层级」分层,补一份 [data-theme='dark'] 即可;
  • 表单校验联动:FxConfirm 的 onConfirm 返回 false 保持打开的机制可以直接复用。

Related Packages

vue-rolekit

Componentes Vue 3 con estilo propio y modo claro y oscuro: formularios, datos, n...

0 0
@apform-ui/core

Apform UI:Element Plus 底座包装在本库栈下;el-* 算可用能力,视觉以本库 Design T...

8,379 0
@qingmingmu/theme

青冥组件系统 —— CSS 变量主题系统

616
@dskripchenko/ui

Vue 3 component, token, and theme library on CSS custom properties

6,792 0
@benjaminor-dev/quasar-app-extension-dialog-message

Dialog message for Quasar — alerts and confirm via useDialogMessage()

3,606

Version History

Version Released Vue License
0.1.0 ^3.4.0 Apache-2.0