模式
单个组件之外的高频组合用法。
命令式对话框
声明式 XDialog 适合模板里固定存在的弹层;「先斩后奏」的中断确认用命令式 confirm / alert / prompt——调用即出现、决策后自毁,不需要在模板里预留任何标签。
注入
useDialog() 依赖宿主先 provide createDialogApi(app)(动态挂载要继承宿主 app 的 provides,组件里的 useUiLocale() 才能拿到宿主替换过的文案):
// 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:
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 组合脱离表单后仍能在失焦或输入时完成校验:
<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 透传
业务侧把库内组件包一层加业务语义(ContextMenu 包 XNavMenu、UserPicker 包 XSelect)时,
不要重抄一遍内部组件的 props。三种姿势按「包了几个内部组件」选:
通用能力片段:每层各拿一份
size 这类横切能力抽成了 ControlSizeProps,谁需要谁 extends 一份,不靠转发:
import type { ControlSizeProps } from '@xwink/ui'
interface UserPickerProps extends ControlSizeProps {
roleId?: string
}
只包一个内部组件:交叉类型 + filterProps
把内部组件的 props 类型交叉进自己的声明,转发时用 filterProps 分流出它认识的那些:
<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:
<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> 逐个透传,没有批量转发的写法。