mcpField
S
shadcn-vue-inertia

shadcn-vue-inertia

github.com/inertia-rails开发70115 次安装37 周前更新
代码仓库

关于 shadcn-vue-inertia

面向使用 Inertia.js 和 Vue 3 的 Rails 应用,说明如何采用 shadcn-vue 模式。内容涵盖框架差异、组件用法和常见问题,不适用于 Nuxt。

主要功能

  1. 01Inertia.js 表单与导航模式
  2. 02shadcn-vue 组件用法指导
  3. 03暗色模式与 Toast 设置提示
  4. 04Vue 响应性和对话框注意事项

适用场景

  1. 01将 Nuxt 风格示例改用于 Rails
  2. 02使用 Inertia 错误处理构建表单
  3. 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 + zodInertia <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 — 上述示例已足够。

评论

支持 Markdown