@xwink/ui

模式

单个组件之外的高频组合用法。

命令式对话框

声明式 XDialog 适合模板里固定存在的弹层;「先斩后奏」的中断确认用命令式 confirm / alert / prompt——调用即出现、决策后自毁,不需要在模板里预留任何标签。

注入

useDialog() 依赖宿主先 provide createDialogApi(app)(动态挂载要继承宿主 app 的 provides,组件里的 useUiLocale() 才能拿到宿主替换过的文案):

ts
// app/plugins/dialog.ts
import { createDialogApi, dialogInjectionKey } from '@xwink/ui'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.provide(dialogInjectionKey, createDialogApi(nuxtApp.vueApp))
})

用法

返回值按全仓约定取 undefined 而非 null——prompt 取消返回 undefined

ts
const dialog = useDialog()

// 确认框:用户点「确定」才继续
const ok = await dialog.confirm({
  title: '删除这个 Agent?',
  message: '删除后不可恢复,确定继续吗?',
  variant: 'danger', // 危险操作确认按钮变红
})
if (ok) {
  // 执行删除
}

// 提示框:无返回值
await dialog.alert('保存成功')

// 输入框:取消返回 undefined
const name = await dialog.prompt({
  title: '重命名',
  label: '新名称',
  defaultValue: '未命名 Agent',
})
if (name !== undefined) {
  // 使用 name
}

三者在 Reka 的 AlertDialog 之上:点遮罩不关闭(「必须做出选择」的中断语义与原生 window.confirm 一致),SSR 下无宿主挂载点,直接按「未确认」返回。

表单

XField 独立持有字段规则、状态和提示,XForm 在提交时只下派校验任务并收集结果。控件保持纯粹,因此同一个 XField + XInput 组合脱离表单后仍能在失焦或输入时完成校验:

vue
<script setup lang="ts">
import { ref } from 'vue'
import { XButton, XField, XForm, XInput, XSelect, type FieldRules } from '@xwink/ui'

const form = ref({ name: '', scene: '' })
const nameRules: FieldRules = {
  required: true,
  whitespace: true,
  trigger: ['blur', 'change'],
}
const sceneRules: FieldRules = { required: true, trigger: 'change' }

function submit() {
  // 所有字段通过校验后提交…
}
</script>

<template>
  <XForm class="flex w-full max-w-sm flex-col gap-3" @submit="submit">
    <XField name="name" label="名称" :rules="nameRules">
      <XInput v-model="form.name" placeholder="Agent 名称" />
    </XField>
    <XField name="scene" label="场景" :rules="sceneRules">
      <XSelect
        v-model="form.scene"
        :options="[{ value: 'chat' }, { value: 'admin', label: '管理端助手' }]"
        placeholder="选择场景"
      />
    </XField>
    <XButton type="submit" variant="primary">提交</XButton>
  </XForm>
</template>

二次封装的 props 透传

业务侧把库内组件包一层加业务语义(ContextMenuXNavMenuUserPickerXSelect)时, 不要重抄一遍内部组件的 props。三种姿势按「包了几个内部组件」选:

通用能力片段:每层各拿一份

size 这类横切能力抽成了 ControlSizeProps,谁需要谁 extends 一份,不靠转发:

ts
import type { ControlSizeProps } from '@xwink/ui'

interface UserPickerProps extends ControlSizeProps {
  roleId?: string
}

只包一个内部组件:交叉类型 + filterProps

把内部组件的 props 类型交叉进自己的声明,转发时用 filterProps 分流出它认识的那些:

vue
<script setup lang="ts">
import { XSelect, filterProps, type SelectProps } from '@xwink/ui'

const props = defineProps<{ roleId?: string } & SelectProps>()
const model = defineModel<string>()
</script>

<template>
  <XSelect v-model="model" v-bind="filterProps(props, XSelect)" :options="users" />
</template>

filterProps 的键清单直接从组件身上读(defineProps<XxxProps>() 在编译期就展开成了运行时 props 声明),接口加一个 prop 这里自动跟上——不需要 Vuetify makeXxxProps 那样再手工维护一份 key 列表。直接 v-bind="props" 则会把 roleId 这类业务 prop 漏成 DOM 属性。

写在 v-bind 之后的显式绑定优先,所以要「锁死某个 prop 不给宿主改」就写在后面。

包两个以上内部组件:分组 props

扁平展开时两个内部组件的同名 prop(都有 size、都有 disabled)会撞在一起,filterProps 分不出该给谁。这种情况各收一份分组 props:

vue
<script setup lang="ts">
import { XNavMenu, XCard, type NavMenuProps, type CardProps } from '@xwink/ui'

defineProps<{
  title: string
  menuProps?: NavMenuProps
  cardProps?: CardProps
}>()
</script>

<template>
  <XCard v-bind="cardProps">
    <XNavMenu v-bind="menuProps" />
  </XCard>
</template>

每个导出组件都有同名 XxxProps 类型可用(ButtonProps / TableProps / DialogProps …), 全部从 @xwink/ui 主入口拿。

事件与插槽

事件不需要管:组件根节点的 fallthrough attrs 会把 @click 这类监听器带下去。多根或需要指定 落点时用 v-bind="$attrs"。插槽用 <slot> 逐个透传,没有批量转发的写法。