关于 shadcn-vue-inertia
面向使用 Inertia.js 和 Vue 3 的 Rails 应用,说明如何采用 shadcn-vue 模式。内容涵盖框架差异、组件用法和常见问题,不适用于 Nuxt。
主要功能
- 01Inertia.js 表单与导航模式
- 02shadcn-vue 组件用法指导
- 03暗色模式与 Toast 设置提示
- 04Vue 响应性和对话框注意事项
适用场景
- 01将 Nuxt 风格示例改用于 Rails
- 02使用 Inertia 错误处理构建表单
- 03排查过期属性或对话框行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ inertia-rails/ skills --skill shadcn-vue-inertia安装后在 Claude Code 中输入/shadcn-vue-inertia确认已加载。
附带文件references/components.mdreferences/flash-toast.md
详细介绍
用于 Inertia Rails 的 shadcn-vue
适配 Inertia.js + Rails + Vue 3 的 shadcn-vue 模式。不是 Nuxt。
使用 shadcn-vue 示例前,请先确认:
- 是否使用了 Nuxt 专属 API?(
useRouter、useFetch、<NuxtLink>)→ 替换为 Inertia 的router、服务端 props、<Link> - 是否使用了
vee-validate+zod? → 替换为 Inertia 的<Form>+name属性。Inertia 会处理 CSRF、错误、重定向和处理中状态。
与 Nuxt 默认设置的主要区别
| shadcn-vue 默认设置(Nuxt) | Inertia 等效方式 |
|---|---|
useFetch / useAsyncData | 通过控制器传递服务端渲染的 props |
useRouter()(Nuxt) | 来自 @inertiajs/vue3 的 router |
<NuxtLink> | 来自 @inertiajs/vue3 的 <Link> |
vee-validate + zod | Inertia <Form> 组件 |
FormField、FormItem、FormMessage | 普通的 <Input name="..."> + errors.field |
useHead()(Nuxt) | 来自 @inertiajs/vue3 的 <Head> |
切勿使用 shadcn-vue 的 FormField、FormItem、FormLabel、FormMessage 组件 —
它们内部依赖 vee-validate 的表单上下文,没有该上下文就会崩溃。
在 Inertia <Form> 中使用带有 name 属性的普通 shadcn-vue Input/Label/Select,
并通过作用域插槽的 errors 对象显示错误。
设置
运行 npx shadcn-vue@latest init。如果 tsconfig.json 中没有 @/ 解析别名,请添加。
不要在 vite.config.ts 中添加 @/ 解析别名 — vite-plugin-ruby 已提供这些别名。
Inertia <Form> 中的 shadcn-vue 输入组件
在 Inertia <Form> 中使用带有 name 属性的普通 shadcn-vue Input/Label/Button。
完整的 <Form> API 请参阅 inertia-rails-forms 技能(以及 references/vue.md)。
关键模式: 将 shadcn-vue 的 FormField/FormItem/FormMessage 替换为普通
组件 + 手动显示错误:
<script setup lang="ts">
import { Form } from '@inertiajs/vue3'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
</script>
<template>
<Form method="post" action="/users">
<template #default="{ errors, processing }">
<div class="space-y-4">
<div>
<Label for="name">Name</Label>
<Input id="name" name="name" />
<p v-if="errors.name" class="text-sm text-destructive">{{ errors.name }}</p>
</div>
<div>
<Label for="email">Email</Label>
<Input id="email" name="email" type="email" />
<p v-if="errors.email" class="text-sm text-destructive">{{ errors.email }}</p>
</div>
<Button type="submit" :disabled="processing">
{{ processing ? 'Creating...' : 'Create User' }}
</Button>
</div>
</template>
</Form>
</template>
Inertia <Form> 集成要求 <Select> 提供 name 属性:
<template>
<Select name="role" default-value="member">
<SelectTrigger><SelectValue placeholder="Select role" /></SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="member">Member</SelectItem>
</SelectContent>
</Select>
</template>
使用 Inertia 导航的对话框
<script setup lang="ts">
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { router } from '@inertiajs/vue3'
defineProps<{ open: boolean; user: User }>()
</script>
<template>
<Dialog
:open="open"
@update:open="(isOpen) => { if (!isOpen) router.replaceProp('show_dialog', false) }"
>
<DialogContent>
<DialogHeader>
<DialogTitle>{{ user.name }}</DialogTitle>
</DialogHeader>
</DialogContent>
</Dialog>
</template>
服务端排序表格
<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { router } from '@inertiajs/vue3'
defineProps<{ users: User[]; sort: string }>()
const handleSort = (column: string) => {
router.get('/users', { sort: column }, { preserveState: true })
}
</script>
<template>
<Table>
<TableHeader>
<TableRow>
<TableHead class="cursor-pointer" @click="handleSort('name')">
Name {{ sort === 'name' ? '↑' : '' }}
</TableHead>
<TableHead>Email</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="user in users" :key="user.id">
<TableCell>{{ user.name }}</TableCell>
<TableCell>{{ user.email }}</TableCell>
</TableRow>
</TableBody>
</Table>
</template>
行链接请使用 <Link>(而不是 <a>),以保留 SPA 导航。
使用 Flash 消息的 Toast
Flash 配置(flash_keys)位于 inertia-rails-controllers 中。Flash 访问(usePage().flash)位于 inertia-rails-pages 中。本节仅介绍 Toast UI 的接线方式。
必须 — 使用 Sonner 实现基于 Flash 的 Toast 时,请阅读整个文件:
references/flash-toast.md(约 80 行)— 完整的 useFlash
组合式函数和 Sonner Toast 提供程序。如果只是读取 Flash 值而不使用 Toast UI,则不要加载此文件。
暗色模式(不使用 Nuxt color-mode)
npx shadcn-vue@latest init 会为浅色/暗色模式生成 CSS 变量,并在 CSS 中生成
@custom-variant dark (&:is(.dark *));(Tailwind v4)。变量本身无需额外
设置。
关键 — 防止错误主题闪烁(FOUC): 在 <head> 中添加内联脚本
(在 Vue 水合之前):
<%# app/views/layouts/application.html.erb — in <head>, before any stylesheets %>
<script>
document.documentElement.classList.toggle(
"dark",
localStorage.appearance === "dark" ||
(!("appearance" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches),
);
</script>
请使用 useAppearance 组合式函数(支持浅色/暗色/系统模式、localStorage 持久化、
matchMedia 监听器),而不是 Nuxt color-mode。通过 <html> 上的 .dark 类切换 —
无需提供程序。
Vue 特有的注意事项
v-model 不适用于 Inertia <Form> — <Form> 会在提交时从输入框的
name 属性读取值,而不是从 Vue 的响应式系统读取。使用
v-model 会创建第二个数据源,而 <Form> 会忽略它:
<Form method="post" action="/users">
<Input v-model="name" />
</Form>
<Form method="post" action="/users">
<Input name="name" />
</Form>
仅在使用 useForm 时使用 v-model(此时需要显式管理 form.name)。
usePage() 返回响应式对象 — 派生值请使用 computed():
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'
const page = usePage()
// BAD — not reactive, won't update when page changes:
// const user = page.props.auth.user
// GOOD — reactive, updates on navigation:
const user = computed(() => page.props.auth.user)
</script>
如果不使用 computed(),解构出的值会停留在初始状态,且在 Inertia 导航后
不会更新。
对话框使用 @update:open,而不是 @close — shadcn-vue Dialog 会触发
update:open,而不是 close。使用 @close 不会报错,但也不会执行任何操作:
<Dialog @close="handleClose">
<Dialog :open="open" @update:open="(isOpen) => { if (!isOpen) handleClose() }">
故障排除
| 症状 | 原因 | 解决方法 |
|---|---|---|
FormField/FormMessage 崩溃 | 使用了依赖 vee-validate 的 shadcn-vue 表单组件 | 替换为普通的 Input/Label,并显示 errors.field |
Select 的值未提交 | 缺少 name 属性 | 为 <Select> 添加 name="field" |
| 对话框意外关闭 | 缺少或错误的 @update:open 处理程序 | 使用 @update:open="(open) => { if (!open) closeHandler() }" |
| 短暂显示错误主题(FOUC) | <head> 中缺少内联 <script> | 在样式表之前添加深色模式脚本 |
v-model 的值未提交 | <Form> 读取的是 name 属性,而不是 Vue 响应式状态 | 使用 name 属性;仅在 useForm 中使用 v-model |
| 导航后共享属性过时 | 解构 usePage() 时没有使用 computed() | 使用 computed(() => ...) 包装派生值 |
相关技能
- 表单组件 →
inertia-rails-forms+references/vue.md(<Form>作用域插槽、useForm) - Flash 配置 →
inertia-rails-controllers(flash_keys 初始化器) - Flash 访问 →
inertia-rails-pages+references/vue.md(usePage().flash) - 由 URL 驱动的对话框 →
inertia-rails-pages+references/vue.md(router.get 模式)
参考资料
构建上述内容未涵盖的 shadcn-vue 组件(Accordion、Sheet、Tabs、DropdownMenu、
采用 Inertia 模式的 AlertDialog)时,加载 references/components.md(约 200 行)。
基本的 Form、Select、Dialog 或 Table 用法无需加载 components.md —
上述示例已足够。