Промты для Vue.js и Nuxt: компоненты, composables, роутинг

{
"title": "15 промтов для Vue.js и Nuxt: от компонентов до Pinia store — экспертный разбор",
"content": "## Введение\n\nVue.js и его фреймворк Nuxt — одни из самых популярных инструментов для создания современных веб-приложений. Однако даже опытные разработчики тратят значительное время на написание шаблонного кода, типовых компонентов, composables и настройку роутинга. В 2026 году, когда экосистема Vue 3 с Composition API и TypeScript стала стандартом, использование промтов (prompts) для генерации кода через AI-ассистентов — не просто тренд, а реальный способ ускорить разработку на 30–50%.\n\nВ этой статье я собрал 15 проверенных промтов, которые покрывают ключевые сценарии: от создания компонентов с пропсами и слотами до настройки Pinia store и динамического роутинга в Nuxt. Все примеры основаны на реальных кейсах из коммерческой разработки, с которыми я сталкивался в проектах для финтеха и e-commerce. Мы не будем обсуждать теоретические возможности AI — только конкретные задачи, промты и результаты.\n\nВажно: промты оптимизированы для работы с GPT-4o и Claude 3.5 Sonnet, но подойдут и для других моделей. Если вы используете локальные модели (например, Llama 3), адаптируйте уровень детализации.\n\n## Категория 1: Базовые промты\n\n### Промт 1: Создание компонента с типизированными пропсами\n\nЗадача: Сгенерировать компонент Vue 3 на Composition API с TypeScript, который принимает пропсы, испускает события и имеет слоты.\n\nПромт:\n\nНапиши компонент Vue 3 на Composition API с TypeScript. Компонент — карточка товара (ProductCard).\nТребования:\n- Пропсы: id (number), title (string), price (number), imageUrl (string, опционально), inStock (boolean, по умолчанию true).\n- События: @click, @add-to-cart (передаёт id и title).\n- Слоты: default (для дополнительного контента), badge (для бейджа, слот-пропс status: string).\n- Стили: scoped, минимальные — рамка, тень, отступы.\n- Используй defineProps, defineEmits, defineSlots.\n\n\nПример результата:\n```vue\n\n\n\n\n\n\n\n### Промт 2: Composables для работы с API\n\n**Задача:** Написать composable для получения данных с API с обработкой ошибок и загрузки.\n\n**Промт:**\n\nСоздай composable useApi для Vue 3 с TypeScript. Функция должна принимать URL (string) и опциональные опции (метод, заголовки, тело запроса). Возвращать: data (ref), error (ref), loading (ref), execute() для повторного запроса. Используй fetch. Добавь поддержку AbortController для отмены запроса.\n\n\n**Пример результата:**\ntypescript\nimport { ref, type Ref } from 'vue'\n\ninterface UseApiOptions {\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE'\n headers?: Record\n body?: unknown\n}\n\ninterface UseApiReturn {\n data: Ref\n error: Ref\n loading: Ref\n execute: () => Promise\n abort: () => void\n}\n\nexport function useApi(url: string, options: UseApiOptions = {}): UseApiReturn {\n const data = ref(null) as Ref\n const error = ref(null)\n const loading = ref(false)\n let controller: AbortController | null = null\n\n const execute = async () => {\n controller = new AbortController()\n loading.value = true\n error.value = null\n\n try {\n const response = await fetch(url, {\n method: options.method || 'GET',\n headers: { 'Content-Type': 'application/json', ...options.headers },\n body: options.body ? JSON.stringify(options.body) : undefined,\n signal: controller.signal\n })\n\n if (!response.ok) {\n throw new Error(HTTP error! status: ${response.status})\n }\n\n data.value = await response.json()\n } catch (err) {\n if (err instanceof Error) {\n if (err.name === 'AbortError') {\n return\n }\n error.value = err.message\n }\n } finally {\n loading.value = false\n }\n }\n\n const abort = () => {\n controller?.abort()\n }\n\n return { data, error, loading, execute, abort }\n}\n\n\n### Промт 3: Динамический роутинг в Nuxt\n\n**Задача:** Настроить динамический роутинг для блога на Nuxt 3.\n\n**Промт:**\n\nСоздай структуру файлов для блога на Nuxt 3 с динамическими маршрутами:\n- Страница списка постов: /blog\n- Страница отдельного поста: /blog/:slug\n- Используй файловую систему Nuxt (pages directory).\n- Для получения поста используй useFetch с серверным API (предположи, что API отдаёт JSON).\n- Добавь middleware для проверки существования slug.\n\n\n**Пример результата:**\nФайловая структура:\n\npages/\n blog/\n index.vue # /blog\n [slug].vue # /blog/:slug\n\n\n**pages/blog/index.vue:**\nvue\n\n\n\n\n\n**pages/blog/[slug].vue:**\nvue\n\n\n\n\n\n## Категория 2: Продвинутые промты\n\n### Промт 4: Pinia store с actions и getters\n\n**Задача:** Создать store для корзины товаров.\n\n**Промт:**\n\nНапиши Pinia store для корзины (useCartStore).\n- State: items (массив объектов {id, title, price, quantity}).\n- Getters: totalItems, totalPrice, isEmpty.\n- Actions: addItem(item), removeItem(id), updateQuantity(id, quantity), clearCart.\n- Используй defineStore с composition API.\n- Типизируй всё через TypeScript.\n\n\n**Пример результата:**\ntypescript\nimport { defineStore } from 'pinia'\nimport { ref, computed } from 'vue'\n\ninterface CartItem {\n id: number\n title: string\n price: number\n quantity: number\n}\n\nexport const useCartStore = defineStore('cart', () => {\n const items = ref([])\n\n const totalItems = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0))\n const totalPrice = computed(() => items.value.reduce((sum, item) => sum + item.price * item.quantity, 0))\n const isEmpty = computed(() => items.value.length === 0)\n\n function addItem(item: Omit) {\n const existing = items.value.find(i => i.id === item.id)\n if (existing) {\n existing.quantity++\n } else {\n items.value.push({ ...item, quantity: 1 })\n }\n }\n\n function removeItem(id: number) {\n items.value = items.value.filter(item => item.id !== id)\n }\n\n function updateQuantity(id: number, quantity: number) {\n const item = items.value.find(i => i.id === id)\n if (item && quantity > 0) {\n item.quantity = quantity\n }\n }\n\n function clearCart() {\n items.value = []\n }\n\n return { items, totalItems, totalPrice, isEmpty, addItem, removeItem, updateQuantity, clearCart }\n})\n\n\n### Промт 5: Компонент с v-model через composable\n\n**Задача:** Реализовать кастомный input с поддержкой v-model и валидацией.\n\n**Промт:**\n\nСоздай компонент CustomInput с v-model. Используй composable useModelWrapper для двустороннего связывания. Добавь валидацию: required, minLength, maxLength, pattern (регулярное выражение). Показывай ошибки под полем.\n\n\n**Пример результата (composable useModelWrapper):**\ntypescript\nimport { computed } from 'vue'\n\nexport function useModelWrapper(props: Record, emit: any, name = 'modelValue') {\n return computed({\n get: () => props[name],\n set: (value) => emit(update:${name}, value)\n })\n}\n\n\n**Компонент:**\nvue\n\n\n\n\n\n\n\n### Промт 6: Middleware для аутентификации в Nuxt\n\n**Задача:** Написать middleware, который проверяет авторизацию и перенаправляет на логин.\n\n**Промт:**\n\nСоздай middleware для Nuxt 3 под названием auth. Он должен:\n- Проверять наличие токена в cookie (useCookie).\n- Если токена нет — перенаправлять на /login.\n- Если есть — проверять его валидность через API (fetch /api/auth/verify).\n- Если токен невалидный — удалять cookie и перенаправлять на /login.\n- Исключить маршруты: /login, /register, /forgot-password.\n\n\n**Пример результата:**\ntypescript\n// middleware/auth.ts\nexport default defineNuxtRouteMiddleware(async (to) => {\n const publicRoutes = ['/login', '/register', '/forgot-password']\n \n if (publicRoutes.includes(to.path)) {\n return\n }\n\n const token = useCookie('auth_token')\n\n if (!token.value) {\n return navigateTo('/login')\n }\n\n try {\n const { valid } = await $fetch<{ valid: boolean }>('/api/auth/verify', {\n headers: { Authorization: Bearer ${token.value} }\n })\n\n if (!valid) {\n token.value = null\n return navigateTo('/login')\n }\n } catch {\n token.value = null\n return navigateTo('/login')\n }\n})\n\n\n## Категория 3: Экспертные промты\n\n### Промт 7: Оптимизация производительности с v-memo\n\n**Задача:** Использовать v-memo для мемоизации больших списков.\n\n**Промт:**\n\nНапиши компонент для отрисовки таблицы с 1000+ строк. Используй v-memo для оптимизации ререндера. Каждая строка — отдельный компонент TableRow с пропсами: id, name, price, quantity. При изменении цены только у одной строки, перерисовываться должна только она. Добавь пример с v-for и v-memo.\n\n\n**Пример результата:**\nvue\n\n\n\n\n\n**Компонент TableRow:**\nvue\n\n\n\n\n\n### Промт 8: Кастомный плагин для Nuxt\n\n**Задача:** Создать плагин для работы с WebSocket.\n\n**Промт:**\n\nНапиши плагин для Nuxt 3 под названием websocket.client.ts. Он должен:\n- Подключаться к WebSocket серверу при инициализации приложения.\n- Предоставлять композабл useWebSocket с методами: send(data), onMessage(callback), close().\n- Автоматически переподключаться при разрыве (до 5 попыток с задержкой 2 секунды).\n- Использовать provide/inject для глобального доступа.\n\n\n**Пример результата:**\ntypescript\n// plugins/websocket.client.ts\nexport default defineNuxtPlugin(() => {\n const webSocket = ref(null)\n const messageCallbacks = new Set<(data: any) => void>()\n let reconnectAttempts = 0\n const maxReconnectAttempts = 5\n const reconnectDelay = 2000\n\n function connect() {\n const config = useRuntimeConfig()\n const url = config.public.wsUrl as string\n\n webSocket.value = new WebSocket(url)\n\n webSocket.value.onopen = () => {\n reconnectAttempts = 0\n console.log('WebSocket connected')\n }\n\n webSocket.value.onmessage = (event) => {\n const data = JSON.parse(event.data)\n messageCallbacks.forEach(callback => callback(data))\n }\n\n webSocket.value.onclose = () => {\n if (reconnectAttempts < maxReconnectAttempts) {\n reconnectAttempts++\n setTimeout(connect, reconnectDelay)\n }\n }\n\n webSocket.value.onerror = (error) => {\n console.error('WebSocket error:', error)\n }\n }\n\n connect()\n\n const send = (data: any) => {\n if (webSocket.value?.readyState === WebSocket.OPEN) {\n webSocket.value.send(JSON.stringify(data))\n }\n }\n\n const onMessage = (callback: (data: any) => void) => {\n messageCallbacks.add(callback)\n return () => messageCallbacks.delete(callback)\n }\n\n const close = () => {\n webSocket.value?.close()\n messageCallbacks.clear()\n }\n\n return {\n provide: {\n webSocket: { send, onMessage, close }\n }\n }\n})\n\n\n### Промт 9: Композабл для работы с localStorage с реактивностью\n\n**Задача:** Создать composable, который синхронизирует реактивную переменную с localStorage.\n\n**Промт:**\n\nНапиши composable useLocalStorage для Vue 3. Принимает ключ (string) и значение по умолчанию. Возвращает ref, который автоматически сохраняется в localStorage при изменении. Обработай ошибки (например, когда localStorage недоступен в SSR). Типизируй через дженерики.\n\n\n**Пример результата:**\ntypescript\nimport { ref, watch, type Ref } from 'vue'\n\nexport function useLocalStorage(key: string, defaultValue: T): Ref {\n const storedValue = process.client ? localStorage.getItem(key) : null\n const data = ref(storedValue ? JSON.parse(storedValue) : defaultValue)\n\n watch(data, (newVal) => {\n try {\n if (process.client) {\n localStorage.setItem(key, JSON.stringify(newVal))\n }\n } catch (e) {\n console.error('Failed to save to localStorage', e)\n }\n }, { deep: true })\n\n return data\n}\n\n\n### Промт 10: Серверный API в Nuxt с Prisma\n\n**Задача:** Создать серверный эндпоинт для CRUD операций с Prisma.\n\n**Промт:**\n\nНапиши серверный API маршрут в Nuxt 3 для работы с пользователями:\n- GET /api/users — получить всех пользователей.\n- GET /api/users/:id — получить пользователя по ID.\n- POST /api/users — создать пользователя (body: {name, email}).\n- PUT /api/users/:id — обновить пользователя.\n- DELETE /api/users/:id — удалить пользователя.\n- Используй Prisma для работы с БД (модель User с полями id, name, email, createdAt).\n- Добавь валидацию через zod.\n\n\n**Пример результата (server/api/users/index.post.ts):**\ntypescript\nimport { z } from 'zod'\n\nconst userSchema = z.object({\n name: z.string().min(2),\n email: z.string().email()\n})\n\nexport default defineEventHandler(async (event) => {\n const body = await readBody(event)\n \n const parsed = userSchema.safeParse(body)\n if (!parsed.success) {\n throw createError({ statusCode: 400, statusMessage: 'Invalid data', data: parsed.error })\n }\n\n const prisma = getPrismaClient()\n const user = await prisma.user.create({ data: parsed.data })\n\n return user\n})\n\n\n### Промт 11: Анимации с помощью Vue Transitions\n\n**Задача:** Создать анимированный список с появлением/исчезновением элементов.\n\n**Промт:**\n\nНапиши компонент AnimatedList с использованием TransitionGroup. Анимация: элементы появляются слева, исчезают вправо, сдвигаются плавно. Использу CSS transitions. Добавь кнопки для добавления и удаления элементов.\n\n\n**Пример результата:**\nvue\n\n\n\n\n\n\n\n### Промт 12: Компонент с динамическими слот-пропсами\n\n**Задача:** Создать компонент DataTable с кастомизируемыми колонками через слоты.\n\n**Промт:**\n\nСоздай компонент DataTable, который принимает массив данных и массив колонок. Каждая колонка может быть кастомизирована через слот с именем column-{field}. Слот-пропс: row (текущая строка). Добавь сортировку по клику на заголовок.\n\n\n**Пример результата:**\nvue\n\n\n\n\n\n### Промт 13: Интеграция с внешним API через серверные маршруты\n\n**Задача:** Создать прокси для внешнего API с кэшированием.\n\n**Промт:**\n\nНапиши серверный маршрут в Nuxt 3, который проксирует запросы к внешнему API (например, погода) и кэширует ответы на 5 минут. Используй useStorage из unstorage для кэша. Добавь обработку ошибок.\n\n\n**Пример результата (server/api/weather.get.ts):**\ntypescript\nimport { defineEventHandler } from 'h3'\n\nexport default defineEventHandler(async (event) => {\n const query = getQuery(event)\n const city = query.city as string\n\n if (!city) {\n throw createError({ statusCode: 400, statusMessage: 'City is required' })\n }\n\n const cacheKey = weather:${city}\n const storage = useStorage()\n const cached = await storage.getItem(cacheKey)\n\n if (cached) {\n return cached\n }\n\n const apiKey = process.env.WEATHER_API_KEY\n const url = https://api.weatherapi.com/v1/current.json?key=${apiKey}&q=${city}\n\n try {\n const data = await $fetch(url)\n await storage.setItem(cacheKey, data, { ttl: 300 }) // 5 минут\n return data\n } catch (err) {\n throw createError({ statusCode: 502, statusMessage: 'External API error' })\n }\n})\n\n\n### Промт 14: Композиция composables\n\n**Задача:** Объединить несколько composables в один.\n\n**Промт:**\n\nСоздай composable useProductPage, который объединяет:\n- useApi для загрузки товара.\n- useCartStore для добавления в корзину.\n- useLocalStorage для сохранения избранного.\n- Возвращает: product, loading, error, addToCart, toggleFavorite, isFavorite.\n\n\n**Пример результата:**\ntypescript\nimport { computed } from 'vue'\nimport { useApi } from './useApi'\nimport { useCartStore } from '~/stores/cart'\nimport { useLocalStorage } from './useLocalStorage'\n\ninterface Product {\n id: number\n title: string\n price: number\n}\n\nexport function useProductPage(productId: number) {\n const { data: product, loading, error, execute } = useApi(/api/products/${productId})\n const cartStore = useCartStore()\n const favorites = useLocalStorage('favorites', [])\n\n const isFavorite = computed(() => favorites.value.includes(productId))\n\n function addToCart() {\n if (product.value) {\n cartStore.addItem({\n id: product.value.id,\n title: product.value.title,\n price: product.value.price\n })\n }\n }\n\n function toggleFavorite() {\n if (isFavorite.value) {\n favorites.value = favorites.value.filter(id => id !== productId)\n } else {\n favorites.value.push(productId)\n }\n }\n\n return {\n product,\n loading,\n error,\n addToCart,\n toggleFavorite,\n isFavorite,\n refresh: execute\n }\n}\n\n\n### Промт 15: SSR-безопасный компонент с клиентским рендерингом\n\n**Задача:** Написать компонент, который рендерится только на клиенте (например, для работы с canvas).\n\n**Промт:**\n\nСоздай компонент ClientOnlyChart, который использует Chart.js для отрисовки графика. Компонент должен рендериться только на клиенте (используй ClientOnly из Nuxt или dynamic import). Прими пропсы: labels, data, type (line/bar). Добавь обработку ошибок при загрузке библиотеки.\n\n\n**Пример результата:**\nvue\n\n\n