ui-stack-vue
Building interfaces in Vue 3 and Nuxt: project layout, script setup with typed defineProps, defineEmits and defineModel, attribute fallthrough, slots and scoped slots, template refs, ref, reactive and computed, watch versus watchEffect, composables and VueUse, provide and inject, Pinia, keyed v-for, transitions, Nuxt 4 (app directory, pages, layouts, useFetch and useAsyncData keys, $fetch in handlers, server routes, runtime config, useSeoMeta, NuxtImg, middleware), VeeValidate forms, Reka UI and Nuxt UI, testing, vue-tsc, and the traps. Read when the project uses Vue or Nuxt.
- License 许可
- Apache-2.0
- 所属仓库
- enowdev/enowxcli
- 最近更新
- 2026-10-06
- Stars
- ★ 67
- Forks
- 21
01 前言 Front Matter
- name
- ui-stack-vue
- description
- Building interfaces in Vue 3 and Nuxt: project layout, script setup with typed defineProps, defineEmits and defineModel, attribute fallthrough, slots and scoped slots, template refs, ref, reactive and computed, watch versus watchEffect, composables and VueUse, provide and inject, Pinia, keyed v-for, transitions, Nuxt 4 (app directory, pages, layouts, useFetch and useAsyncData keys, $fetch in handlers, server routes, runtime config, useSeoMeta, NuxtImg, middleware), VeeValidate forms, Reka UI and Nuxt UI, testing, vue-tsc, and the traps. Read when the project uses Vue or Nuxt.
02 资源文件 1
- 参考文档references/api_reference.md13.0 KB
03 SKILL.md 正文
Vue 3 and Nuxt
Generated Vue mixes the Options API into <script setup>, destructures a
reactive object and loses its reactivity, watches one ref to set another,
mutates props, fetches page data with $fetch so a Nuxt page requests it
twice, and leaves the starter's green look in place. This is Vue 3.5 with
Nuxt 4 (4.5 in 2026) as written now: typed <script setup>, composables,
keyed data fetching, and the Nuxt built-ins.
1. Project layout
Vite (npm create vue@latest) Nuxt 4
src/main.ts src/App.vue app/app.vue app/error.vue
src/router/ src/stores/ app/pages/orders/index.vue, [id].vue
src/components/ui/ app/layouts/default.vue, dashboard.vue
src/components/orders/ app/components/ app/composables/
src/composables/ src/views/ app/middleware/ app/assets/css/main.css
server/api/orders.get.ts server only
shared/ types both sides use
- Nuxt auto-imports components, composables and Vue's APIs; a Vite project imports them explicitly.
- Delete the starter: create-vue's
HelloWorld.vue,TheWelcome.vueand itsbase.cssandmain.css(the green links), or<NuxtWelcome />.
2. Components
- Single-file components with
<script setup lang="ts">, named for what they are (multi-word:OrderTable.vue), one per file. - Props with
defineProps<{ ... }>(), destructured with defaults (3.5); events with typeddefineEmits;v-modelon components throughdefineModel. Slots for content, not a prop per variation.
<!-- components/ui/TextField.vue -->
<script setup lang="ts">
import { useId } from "vue";
const model = defineModel<string>({ required: true });
const { label, error } = defineProps<{ label: string; error?: string }>();
const id = useId(); // stable between server and client (3.5)
</script>
<template>
<div class="field">
<label :for="id">{{ label }}</label>
<input :id="id" v-model="model" :aria-invalid="!!error"
:aria-describedby="error ? `${id}-error` : undefined" />
<p v-if="error" :id="`${id}-error`">{{ error }}</p>
</div>
</template>
- Attributes a component does not declare (
class,type,aria-*, listeners) fall through to its root element, so a Button stays a button. When the root is a wrapper,defineOptions({ inheritAttrs: false })andv-bind="$attrs"on the native element. - Scoped slots let the caller render each item;
defineSlotstypes them andgenerictypes the item:
<script setup lang="ts" generic="T extends { id: string }">
defineProps<{ items: T[] }>();
defineSlots<{ row(props: { item: T }): any; empty(): any }>();
</script>
<template>
<ul v-if="items.length">
<li v-for="item in items" :key="item.id"><slot name="row" :item="item" /></li>
</ul>
<slot v-else name="empty" />
</template>
- Template refs with
useTemplateRef<HTMLInputElement>("search")andref="search"(3.5); DOM work inonMounted. - Styles scoped, or the project's Tailwind; tokens as CSS variables in one place.
3. Reactivity
refby default, for primitives and objects;computedfor anything derived;reactiveonly for an object never reassigned or destructured (destructuring copies;toRefsif you must).- Derive with
computedinstead of watching one value to set another:
// not this
const fullName = ref("");
watch([first, last], () => { fullName.value = `${first.value} ${last.value}`; });
// this
const fullName = computed(() => `${first.value} ${last.value}`);
watchfor effects outside the component (a request,localStorage, a chart) on named sources;watchEffectwhen the effect should track whatever it reads.onWatcherCleanup(3.5) aborts the previous run.- Destructured props are reactive in the template and in
computed, but awatchneeds a getter:watch(() => id, load). shallowReffor large data replaced whole (an API response, a chart's dataset)..valuein script, never in the template.
4. Shared logic and state
- Logic several components share goes in a composable (
useX), not a mixin; it accepts refs or getters (MaybeRefOrGetter) and reads them withtoValue. VueUse already has most browser ones (useMediaQuery,useLocalStorage,onClickOutside,useEventListener): use them before writing one. provideandinjectwith a typedInjectionKeyfor compound components (tabs and their panels, a form and its fields), not for app state.- Pinia for shared client state, as setup stores;
storeToRefsto destructure state and getters (actions destructure directly):
export const useCartStore = defineStore("cart", () => {
const items = ref<CartItem[]>([]);
const count = computed(() => items.value.reduce((n, item) => n + item.quantity, 0));
function add(item: CartItem) { items.value.push(item); }
return { items, count, add };
});
// in a component: const cart = useCartStore(); const { count } = storeToRefs(cart);
- Server data is not store state:
useFetchin Nuxt; TanStack Query for Vue or Pinia Colada in a Vite app (frontend-data).
5. Templates
v-forwith a stable:keyfrom the data; neverv-ifandv-foron the same element (a<template v-for>around, or a filteredcomputed).- Loading, error and empty as three branches in the template.
<Transition>and<TransitionGroup>for purposeful motion, turned off underprefers-reduced-motion(motion-stacks).<Teleport to="body">for an overlay when no library renders it.v-htmlonly with sanitised content.
6. Nuxt
NuxtLinkfor internal links,NuxtImgorNuxtPicturewhen@nuxt/imageis installed (width,height,sizes="100vw md:50vw"),@nuxt/fontsto self-host the faces the CSS names,useSeoMetaoruseHeadfor a title and description per page, layouts inapp/layouts/chosen withdefinePageMeta({ layout: "dashboard" }).- Page data with
useFetchoruseAsyncData: fetched on the server and handed to the client without a second request.
<script setup lang="ts">
const route = useRoute();
const status = computed(() => String(route.query.status ?? "open"));
const { data: orders, status: load, error, refresh } = await useFetch("/api/orders", {
query: { status }, // refetches when it changes
});
useSeoMeta({ title: "Orders", description: "[What this page lists]" });
</script>
<template>
<OrdersSkeleton v-if="load === 'pending'" />
<LoadError v-else-if="error" what="orders" @retry="refresh" />
<NoOrders v-else-if="!orders?.length" />
<OrderTable v-else :orders="orders" />
</template>
useAsyncDatatakes a key that names its data (order-42); in Nuxt 4 calls with one key share one state, so different data needs different keys.lazy: true(oruseLazyFetch) lets navigation finish first.$fetchin event handlers and actions (a click, a submit), never at the top of<script setup>for page data, where it runs on the server and again on the client;useFetchnever inside a handler.- Server routes in
server/api/(orders.get.ts,orders.post.ts) withdefineEventHandler, validating withgetValidatedQueryandreadValidatedBody(backend-api). runtimeConfig: top-level keys are server-only (NUXT_API_SECRET),publicones ship to the browser (NUXT_PUBLIC_SITE_URL); read withuseRuntimeConfig().- Route middleware in
app/middleware/redirects withnavigateTo("/login"); the server route still checks the user on every request. useState("key", () => ...)for SSR-safe shared state;<ClientOnly>oronMountedfor browser-only parts;app/error.vueandcreateError({ statusCode: 404, statusMessage: "Order not found" })for failures.
7. Forms and accessible components
- VeeValidate with a zod schema through
toTypedSchemafrom@vee-validate/zod(frontend-forms):
const { defineField, handleSubmit, errors, isSubmitting } = useForm({
validationSchema: toTypedSchema(z.object({ email: z.string().email("Enter an email like name@example.com") })),
});
const [email, emailAttrs] = defineField("email");
const onSubmit = handleSubmit((values) => $fetch("/api/invites", { method: "POST", body: values }));
In the template: <form @submit="onSubmit">, the input with
v-model="email" v-bind="emailAttrs", errors.email under the field
with aria-describedby, and the button :disabled="isSubmitting".
- With Nuxt UI,
UFormtakes the schema and the state directly andUFormFieldwires the label, hint and error. - Accessible widgets from Reka UI (headless, formerly Radix Vue),
shadcn-vue (Reka UI with Tailwind), Nuxt UI v4 (Reka UI and Tailwind v4)
or Headless UI for Vue; PrimeVue for data-heavy screens. Themed from the
tokens: Nuxt UI's colours in
app.config.ts(ui.colors, green and slate by default) and a@theme staticscale for the brand colour.
8. Testing
Vitest with @vue/test-utils (mount, then
await wrapper.get("button").trigger("click") and wrapper.emitted()) or
@testing-library/vue (queries by role); @nuxt/test-utils and
mountSuspended for components that use Nuxt composables; Playwright for
flows (frontend-testing).
9. Traps
- Destructuring
reactive()or a store loses reactivity:toRefs,storeToRefs. Reassigning areactiveobject breaks every reference to it: useref. - Watchers instead of computed; mutating props, including an object prop's
fields (emit, or
defineModel). v-ifwithv-foron one element; index keys on lists that change.$fetchfor page data in setup,useFetchin a handler, twouseAsyncDatacalls sharing a key for different data.window,documentorlocalStorageread during setup, which runs on the server:onMounted,import.meta.client, or<ClientOnly>.- Class instances or functions in
useStateor Pinia state: the SSR payload cannot carry them. - A date or random value rendered on the server and again on the client:
format with a fixed locale and time zone, or put
data-allow-mismatch(3.5) on that one element.
Check it
- Types:
npm run type-check(create-vue runsvue-tsc --build), ornpx nuxt typecheck; thennpm run build,npm run lintandnpx vitest run. - Look at the result with the
previewtool: with Vite,urlhttp://localhost:5173/andstartnpm run dev -- --port 5173 --strictPort; with Nuxt,urlhttp://localhost:3000/andstartnpm run dev -- --port 3000. - Hydration mismatches in the console the
previewreport lists are bugs.
Avoid
Options API and <script setup> mixed in one component; destructured
reactive state; watch to derive a value; mutated props; v-if with
v-for; $fetch for page data at setup; shared useAsyncData keys; a
store for server data; browser APIs in setup; hand-rolled dialogs and
menus; Nuxt UI left on its default green and slate; the starter's
components and colours left in place.
04 同仓库更多 Skills
demo-skill
Project-scope demo skill for tests.
writing
Writing words people read: the specific thing instead of claims, words to drop, sentences, interface copy (buttons, labels, errors, empty states, confirmations), page copy, notifications and emails, alt text and link text, numbers and dates, words that translate well, documentation and voice. Read before writing a page's copy, an interface's text or a document.
ui
Designing or restyling an interface: direction, layout, type, colour, icons, components, states, responsive behaviour. Read before building a page, a screen or a set of components.
ui-themes
Light and dark themes, and colour that holds in both: tokens with a value per theme, and what each kind of content needs to stay visible (icons, SVG drawings and line art, logos, photos, screenshots, charts, code, maps, borders, shadows, focus, form controls), switching without a flash, and checking both. Read before building a second theme or a theme toggle, and before any drawing, logo or chart on a page that has two themes.
ui-structure
Structure before styling: the blueprint written before any interface code (the screen's job, the regions its kind of page needs, their order and why, the layout skeleton in numbers, the components and their states), the anatomy and order of a homepage, a dashboard, a list, a detail page, settings, a form, sign-in, pricing, docs, an article and a checkout, and how to turn the blueprint into code (tokens, layout primitives, components, sections, page). Read first, before building or rebuilding any page or screen.
ui-stack-tailwind
Building with Tailwind CSS v4 (and v3) without the Tailwind-default look: setup per framework (Vite, Next.js, Astro, Laravel, monorepos), @theme tokens in OKLCH named by role, dark mode with @custom-variant, the layout system as tokens (one container, section spacing, a type scale, stretched rows) and how to fix each preview layout line with classes, state, aria, data, group, peer, has, motion and container-query variants, component recipes (button, field, card, navigation, table, dialog) with every state, motion with --animate-* and starting:, cn with tailwind-merge, cva, class order, @utility, arbitrary values, typography, v3 to v4 migration, and the generated look. Read when the project uses Tailwind.