بناء تطبيق ويب متكامل باستخدام Nuxt 4 و Vue 3

يرشدك هذا الدليل خطوة بخطوة لبناء تطبيق ويب متكامل باستخدام Nuxt 4 و Vue 3. ستقوم ببناء تطبيق لإدارة المهام (TaskFlow) مع المصادقة ومسارات API وقاعدة بيانات PostgreSQL عبر Prisma. في النهاية سيكون لديك تطبيق كامل جاهز للإنتاج.
أهداف التعلم
في نهاية هذا الدليل ستكون قادراً على:
- إنشاء وتكوين مشروع Nuxt 4 مع TypeScript
- إتقان نظام التوجيه القائم على الملفات في Nuxt
- بناء مكونات Vue 3 تفاعلية باستخدام Composition API
- إنشاء مسارات API على الخادم مع Nitro
- دمج Prisma ORM لإدارة قاعدة البيانات
- تنفيذ مصادقة بسيطة قائمة على الجلسات
- نشر تطبيقك في بيئة الإنتاج
المتطلبات الأساسية
قبل البدء تأكد من توفر:
- Node.js 20+ مثبت على جهازك
- pnpm (مدير الحزم الموصى به لـ Nuxt)
- PostgreSQL مثبت محلياً أو خدمة سحابية (Neon أو Supabase)
- معرفة أساسية بـ JavaScript/TypeScript
- إلمام بأساسيات Vue.js (المكونات والتفاعلية)
- محرر أكواد (يُوصى بـ VS Code مع إضافة Volar)
ما ستبنيه
تطبيق TaskFlow — مدير مهام متكامل يتضمن:
- تسجيل ودخول المستخدمين
- إنشاء وتعديل وحذف المهام
- التصفية حسب الحالة (للتنفيذ، قيد التنفيذ، مكتمل)
- واجهة متجاوبة مع نظام تصميم Nuxt UI
- واجهة برمجة REST آمنة على جانب الخادم
الخطوة 1: تهيئة مشروع Nuxt 4
ابدأ بإنشاء مشروع Nuxt 4 جديد:
pnpm dlx nuxi@latest init taskflow-app
cd taskflow-appعندما يطلب منك CLI الخيارات اختر:
- مدير الحزم: pnpm
- تهيئة git: نعم
ثم ثبّت التبعيات وشغّل خادم التطوير:
pnpm install
pnpm devتطبيقك متاح على http://localhost:3000.
هيكل المشروع
إليك الهيكل الأساسي لمشروع Nuxt 4:
taskflow-app/
├── app/
│ ├── components/ # مكونات Vue قابلة لإعادة الاستخدام
│ ├── composables/ # منطق قابل لإعادة الاستخدام (hooks)
│ ├── layouts/ # تخطيطات الصفحات
│ ├── pages/ # الصفحات (توجيه تلقائي)
│ └── app.vue # المكون الجذر
├── server/
│ ├── api/ # مسارات API
│ ├── middleware/ # وسيط الخادم
│ └── utils/ # أدوات الخادم
├── prisma/
│ └── schema.prisma # مخطط قاعدة البيانات
├── nuxt.config.ts # تكوين Nuxt
├── package.json
└── tsconfig.json
يتبنى Nuxt 4 هيكل مجلدات جديد مع مجلد app/ الذي يحتوي على كل الكود العميل. هذا الفصل الواضح بين العميل (app/) والخادم (server/) يحسّن تنظيم الكود.
الخطوة 2: تكوين Nuxt 4
حدّث ملف nuxt.config.ts مع الوحدات المطلوبة:
// nuxt.config.ts
export default defineNuxtConfig({
future: {
compatibilityVersion: 4,
},
devtools: { enabled: true },
modules: [
'@nuxt/ui',
'@nuxt/fonts',
],
runtimeConfig: {
sessionSecret: process.env.SESSION_SECRET || 'dev-secret-change-in-production',
databaseUrl: process.env.DATABASE_URL,
public: {
appName: 'TaskFlow',
},
},
compatibilityDate: '2026-03-01',
})ثبّت وحدات Nuxt UI و Fonts:
pnpm add @nuxt/ui @nuxt/fontsأنشئ ملف .env في جذر المشروع:
DATABASE_URL="postgresql://user:password@localhost:5432/taskflow"
SESSION_SECRET="سر-آمن-جداً-غيّره-في-الإنتاج"الخطوة 3: إعداد Prisma وقاعدة البيانات
ثبّت Prisma وقم بتهيئته:
pnpm add -D prisma
pnpm add @prisma/client
pnpm dlx prisma initحدّد مخطط قاعدة البيانات في prisma/schema.prisma:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String
password String
tasks Task[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Task {
id String @id @default(cuid())
title String
description String?
status TaskStatus @default(TODO)
priority Priority @default(MEDIUM)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum TaskStatus {
TODO
IN_PROGRESS
DONE
}
enum Priority {
LOW
MEDIUM
HIGH
}طبّق عمليات الترحيل:
pnpm dlx prisma migrate dev --name initأنشئ أداة مساعدة على الخادم للوصول إلى عميل Prisma:
// server/utils/prisma.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default prismaالخطوة 4: إنشاء مسارات API
يستخدم Nuxt محرك Nitro لمسارات الخادم. أنشئ نقاط نهاية API الخاصة بك.
مسار التسجيل
// server/api/auth/register.post.ts
import bcrypt from 'bcryptjs'
import prisma from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
if (!body.email || !body.password || !body.name) {
throw createError({
statusCode: 400,
statusMessage: 'البريد الإلكتروني والاسم وكلمة المرور مطلوبة',
})
}
const existingUser = await prisma.user.findUnique({
where: { email: body.email },
})
if (existingUser) {
throw createError({
statusCode: 409,
statusMessage: 'يوجد حساب بهذا البريد الإلكتروني بالفعل',
})
}
const hashedPassword = await bcrypt.hash(body.password, 12)
const user = await prisma.user.create({
data: {
email: body.email,
name: body.name,
password: hashedPassword,
},
select: {
id: true,
email: true,
name: true,
},
})
const session = await useSession(event, {
password: useRuntimeConfig().sessionSecret,
})
await session.update({ userId: user.id })
return user
})مسار تسجيل الدخول
// server/api/auth/login.post.ts
import bcrypt from 'bcryptjs'
import prisma from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
if (!body.email || !body.password) {
throw createError({
statusCode: 400,
statusMessage: 'البريد الإلكتروني وكلمة المرور مطلوبان',
})
}
const user = await prisma.user.findUnique({
where: { email: body.email },
})
if (!user || !(await bcrypt.compare(body.password, user.password))) {
throw createError({
statusCode: 401,
statusMessage: 'بيانات الاعتماد غير صالحة',
})
}
const session = await useSession(event, {
password: useRuntimeConfig().sessionSecret,
})
await session.update({ userId: user.id })
return {
id: user.id,
email: user.email,
name: user.name,
}
})وسيط المصادقة على الخادم
// server/middleware/auth.ts
import prisma from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const protectedRoutes = ['/api/tasks']
const isProtected = protectedRoutes.some((route) =>
event.path?.startsWith(route)
)
if (!isProtected) return
const session = await useSession(event, {
password: useRuntimeConfig().sessionSecret,
})
if (!session.data?.userId) {
throw createError({
statusCode: 401,
statusMessage: 'غير مصادق',
})
}
const user = await prisma.user.findUnique({
where: { id: session.data.userId as string },
select: { id: true, email: true, name: true },
})
if (!user) {
throw createError({
statusCode: 401,
statusMessage: 'المستخدم غير موجود',
})
}
event.context.user = user
})عمليات CRUD للمهام
// server/api/tasks/index.get.ts
import prisma from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = event.context.user
const query = getQuery(event)
const where: any = { userId: user.id }
if (query.status) {
where.status = query.status
}
const tasks = await prisma.task.findMany({
where,
orderBy: { createdAt: 'desc' },
})
return tasks
})// server/api/tasks/index.post.ts
import prisma from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = event.context.user
const body = await readBody(event)
if (!body.title) {
throw createError({
statusCode: 400,
statusMessage: 'العنوان مطلوب',
})
}
const task = await prisma.task.create({
data: {
title: body.title,
description: body.description || null,
priority: body.priority || 'MEDIUM',
userId: user.id,
},
})
return task
})// server/api/tasks/[id].patch.ts
import prisma from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = event.context.user
const id = getRouterParam(event, 'id')
const body = await readBody(event)
const task = await prisma.task.findFirst({
where: { id, userId: user.id },
})
if (!task) {
throw createError({
statusCode: 404,
statusMessage: 'المهمة غير موجودة',
})
}
const updated = await prisma.task.update({
where: { id },
data: {
title: body.title ?? task.title,
description: body.description ?? task.description,
status: body.status ?? task.status,
priority: body.priority ?? task.priority,
},
})
return updated
})// server/api/tasks/[id].delete.ts
import prisma from '~/server/utils/prisma'
export default defineEventHandler(async (event) => {
const user = event.context.user
const id = getRouterParam(event, 'id')
const task = await prisma.task.findFirst({
where: { id, userId: user.id },
})
if (!task) {
throw createError({
statusCode: 404,
statusMessage: 'المهمة غير موجودة',
})
}
await prisma.task.delete({ where: { id } })
return { success: true }
})ثبّت bcryptjs لتشفير كلمات المرور:
pnpm add bcryptjs
pnpm add -D @types/bcryptjsالخطوة 5: إنشاء composable المصادقة
أنشئ composable لإدارة حالة المصادقة على جانب العميل:
// app/composables/useAuth.ts
interface User {
id: string
email: string
name: string
}
export function useAuth() {
const user = useState<User | null>('auth-user', () => null)
const isAuthenticated = computed(() => !!user.value)
async function login(email: string, password: string) {
const data = await $fetch<User>('/api/auth/login', {
method: 'POST',
body: { email, password },
})
user.value = data
return data
}
async function register(name: string, email: string, password: string) {
const data = await $fetch<User>('/api/auth/register', {
method: 'POST',
body: { name, email, password },
})
user.value = data
return data
}
async function logout() {
await $fetch('/api/auth/logout', { method: 'POST' })
user.value = null
navigateTo('/login')
}
async function fetchUser() {
try {
const data = await $fetch<User>('/api/auth/me')
user.value = data
} catch {
user.value = null
}
}
return {
user,
isAuthenticated,
login,
register,
logout,
fetchUser,
}
}أضف مسارات الجلسة المتبقية:
// server/api/auth/me.get.ts
export default defineEventHandler(async (event) => {
const user = event.context.user
if (!user) {
throw createError({ statusCode: 401, statusMessage: 'غير مصادق' })
}
return user
})// server/api/auth/logout.post.ts
export default defineEventHandler(async (event) => {
const session = await useSession(event, {
password: useRuntimeConfig().sessionSecret,
})
await session.clear()
return { success: true }
})الخطوة 6: إنشاء التخطيط الرئيسي
حدّد تخطيطاً مع شريط تنقل:
<!-- app/layouts/default.vue -->
<script setup lang="ts">
const { user, isAuthenticated, logout } = useAuth()
const config = useRuntimeConfig()
</script>
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
<header class="bg-white dark:bg-gray-800 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<NuxtLink to="/" class="text-xl font-bold text-primary">
{{ config.public.appName }}
</NuxtLink>
<nav class="flex items-center gap-4">
<template v-if="isAuthenticated">
<span class="text-sm text-gray-600 dark:text-gray-300">
{{ user?.name }}
</span>
<UButton
variant="ghost"
color="red"
@click="logout"
>
تسجيل الخروج
</UButton>
</template>
<template v-else>
<UButton to="/login" variant="ghost">تسجيل الدخول</UButton>
<UButton to="/register" color="primary">إنشاء حساب</UButton>
</template>
</nav>
</div>
</div>
</header>
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<slot />
</main>
</div>
</template>الخطوة 7: بناء الصفحات
الصفحة الرئيسية
<!-- app/pages/index.vue -->
<script setup lang="ts">
const { isAuthenticated } = useAuth()
</script>
<template>
<div class="text-center py-20">
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4">
أدر مهامك بكفاءة
</h1>
<p class="text-lg text-gray-600 dark:text-gray-300 mb-8 max-w-2xl mx-auto">
TaskFlow هو مدير مهام بسيط وقوي.
نظّم وحدد الأولويات وتابع تقدم مشاريعك.
</p>
<div class="flex gap-4 justify-center">
<UButton
v-if="!isAuthenticated"
to="/register"
size="lg"
color="primary"
>
ابدأ مجاناً
</UButton>
<UButton
v-if="isAuthenticated"
to="/dashboard"
size="lg"
color="primary"
>
الذهاب إلى لوحة التحكم
</UButton>
</div>
</div>
</template>صفحة تسجيل الدخول
<!-- app/pages/login.vue -->
<script setup lang="ts">
definePageMeta({ layout: 'default' })
const { login } = useAuth()
const error = ref('')
const loading = ref(false)
const form = reactive({
email: '',
password: '',
})
async function handleSubmit() {
error.value = ''
loading.value = true
try {
await login(form.email, form.password)
navigateTo('/dashboard')
} catch (e: any) {
error.value = e.data?.statusMessage || 'خطأ في تسجيل الدخول'
} finally {
loading.value = false
}
}
</script>
<template>
<div class="max-w-md mx-auto mt-16">
<UCard>
<template #header>
<h2 class="text-2xl font-bold text-center">تسجيل الدخول</h2>
</template>
<form @submit.prevent="handleSubmit" class="space-y-4">
<UAlert
v-if="error"
color="red"
:title="error"
variant="subtle"
/>
<UFormGroup label="البريد الإلكتروني">
<UInput
v-model="form.email"
type="email"
placeholder="you@email.com"
required
/>
</UFormGroup>
<UFormGroup label="كلمة المرور">
<UInput
v-model="form.password"
type="password"
placeholder="كلمة المرور"
required
/>
</UFormGroup>
<UButton
type="submit"
block
:loading="loading"
>
تسجيل الدخول
</UButton>
</form>
<template #footer>
<p class="text-center text-sm text-gray-500">
ليس لديك حساب؟
<NuxtLink to="/register" class="text-primary font-medium">
سجّل الآن
</NuxtLink>
</p>
</template>
</UCard>
</div>
</template>صفحة التسجيل
<!-- app/pages/register.vue -->
<script setup lang="ts">
definePageMeta({ layout: 'default' })
const { register } = useAuth()
const error = ref('')
const loading = ref(false)
const form = reactive({
name: '',
email: '',
password: '',
confirmPassword: '',
})
async function handleSubmit() {
if (form.password !== form.confirmPassword) {
error.value = 'كلمتا المرور غير متطابقتين'
return
}
error.value = ''
loading.value = true
try {
await register(form.name, form.email, form.password)
navigateTo('/dashboard')
} catch (e: any) {
error.value = e.data?.statusMessage || 'خطأ في التسجيل'
} finally {
loading.value = false
}
}
</script>
<template>
<div class="max-w-md mx-auto mt-16">
<UCard>
<template #header>
<h2 class="text-2xl font-bold text-center">إنشاء حساب</h2>
</template>
<form @submit.prevent="handleSubmit" class="space-y-4">
<UAlert
v-if="error"
color="red"
:title="error"
variant="subtle"
/>
<UFormGroup label="الاسم">
<UInput
v-model="form.name"
placeholder="اسمك"
required
/>
</UFormGroup>
<UFormGroup label="البريد الإلكتروني">
<UInput
v-model="form.email"
type="email"
placeholder="you@email.com"
required
/>
</UFormGroup>
<UFormGroup label="كلمة المرور">
<UInput
v-model="form.password"
type="password"
placeholder="8 أحرف على الأقل"
required
minlength="8"
/>
</UFormGroup>
<UFormGroup label="تأكيد كلمة المرور">
<UInput
v-model="form.confirmPassword"
type="password"
placeholder="أعد كتابة كلمة المرور"
required
/>
</UFormGroup>
<UButton
type="submit"
block
:loading="loading"
>
إنشاء الحساب
</UButton>
</form>
<template #footer>
<p class="text-center text-sm text-gray-500">
لديك حساب بالفعل؟
<NuxtLink to="/login" class="text-primary font-medium">
سجّل دخولك
</NuxtLink>
</p>
</template>
</UCard>
</div>
</template>صفحة لوحة التحكم
<!-- app/pages/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
middleware: 'auth',
})
interface Task {
id: string
title: string
description: string | null
status: 'TODO' | 'IN_PROGRESS' | 'DONE'
priority: 'LOW' | 'MEDIUM' | 'HIGH'
createdAt: string
}
const activeFilter = ref<string | null>(null)
const { data: tasks, refresh } = await useFetch<Task[]>('/api/tasks', {
query: computed(() => ({
status: activeFilter.value || undefined,
})),
})
const showCreateModal = ref(false)
const newTask = reactive({
title: '',
description: '',
priority: 'MEDIUM' as 'LOW' | 'MEDIUM' | 'HIGH',
})
async function createTask() {
await $fetch('/api/tasks', {
method: 'POST',
body: {
title: newTask.title,
description: newTask.description || null,
priority: newTask.priority,
},
})
newTask.title = ''
newTask.description = ''
newTask.priority = 'MEDIUM'
showCreateModal.value = false
refresh()
}
async function updateTaskStatus(taskId: string, status: string) {
await $fetch(`/api/tasks/${taskId}`, {
method: 'PATCH',
body: { status },
})
refresh()
}
async function deleteTask(taskId: string) {
await $fetch(`/api/tasks/${taskId}`, {
method: 'DELETE',
})
refresh()
}
const filters = [
{ label: 'الكل', value: null },
{ label: 'للتنفيذ', value: 'TODO' },
{ label: 'قيد التنفيذ', value: 'IN_PROGRESS' },
{ label: 'مكتمل', value: 'DONE' },
]
const priorityColors = {
LOW: 'green',
MEDIUM: 'yellow',
HIGH: 'red',
} as const
const statusLabels = {
TODO: 'للتنفيذ',
IN_PROGRESS: 'قيد التنفيذ',
DONE: 'مكتمل',
} as const
</script>
<template>
<div>
<div class="flex justify-between items-center mb-8">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
مهامي
</h1>
<UButton
color="primary"
icon="i-heroicons-plus"
@click="showCreateModal = true"
>
مهمة جديدة
</UButton>
</div>
<!-- الفلاتر -->
<div class="flex gap-2 mb-6">
<UButton
v-for="filter in filters"
:key="filter.label"
:variant="activeFilter === filter.value ? 'solid' : 'ghost'"
size="sm"
@click="activeFilter = filter.value"
>
{{ filter.label }}
</UButton>
</div>
<!-- قائمة المهام -->
<div class="space-y-3">
<UCard
v-for="task in tasks"
:key="task.id"
>
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<h3
class="font-medium"
:class="task.status === 'DONE' ? 'line-through text-gray-400' : ''"
>
{{ task.title }}
</h3>
<UBadge :color="priorityColors[task.priority]" size="xs">
{{ task.priority }}
</UBadge>
</div>
<p v-if="task.description" class="text-sm text-gray-500">
{{ task.description }}
</p>
</div>
<div class="flex items-center gap-2">
<USelect
:model-value="task.status"
:options="[
{ label: 'للتنفيذ', value: 'TODO' },
{ label: 'قيد التنفيذ', value: 'IN_PROGRESS' },
{ label: 'مكتمل', value: 'DONE' },
]"
size="sm"
@update:model-value="updateTaskStatus(task.id, $event)"
/>
<UButton
icon="i-heroicons-trash"
color="red"
variant="ghost"
size="sm"
@click="deleteTask(task.id)"
/>
</div>
</div>
</UCard>
<div
v-if="!tasks?.length"
class="text-center py-12 text-gray-500"
>
<p class="text-lg mb-2">لا توجد مهام</p>
<p class="text-sm">انقر على "مهمة جديدة" للبدء</p>
</div>
</div>
<!-- نافذة الإنشاء -->
<UModal v-model="showCreateModal">
<UCard>
<template #header>
<h3 class="text-lg font-medium">مهمة جديدة</h3>
</template>
<form @submit.prevent="createTask" class="space-y-4">
<UFormGroup label="العنوان" required>
<UInput
v-model="newTask.title"
placeholder="عنوان المهمة"
required
/>
</UFormGroup>
<UFormGroup label="الوصف">
<UTextarea
v-model="newTask.description"
placeholder="وصف اختياري"
/>
</UFormGroup>
<UFormGroup label="الأولوية">
<USelect
v-model="newTask.priority"
:options="[
{ label: 'منخفضة', value: 'LOW' },
{ label: 'متوسطة', value: 'MEDIUM' },
{ label: 'عالية', value: 'HIGH' },
]"
/>
</UFormGroup>
<div class="flex justify-end gap-2">
<UButton
variant="ghost"
@click="showCreateModal = false"
>
إلغاء
</UButton>
<UButton type="submit" color="primary">
إنشاء
</UButton>
</div>
</form>
</UCard>
</UModal>
</div>
</template>الخطوة 8: وسيط المصادقة على العميل
أنشئ وسيط تنقل لحماية الصفحات:
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware(async (to) => {
const { isAuthenticated, fetchUser } = useAuth()
await fetchUser()
if (!isAuthenticated.value) {
return navigateTo('/login')
}
})الخطوة 9: إضافة التحقق مع Zod
ثبّت Zod للتحقق من البيانات على جانب الخادم:
pnpm add zodأنشئ أداة تحقق مساعدة:
// server/utils/validate.ts
import { z } from 'zod'
export const createTaskSchema = z.object({
title: z.string().min(1, 'العنوان مطلوب').max(200),
description: z.string().max(1000).optional(),
priority: z.enum(['LOW', 'MEDIUM', 'HIGH']).default('MEDIUM'),
})
export const updateTaskSchema = z.object({
title: z.string().min(1).max(200).optional(),
description: z.string().max(1000).nullable().optional(),
status: z.enum(['TODO', 'IN_PROGRESS', 'DONE']).optional(),
priority: z.enum(['LOW', 'MEDIUM', 'HIGH']).optional(),
})
export const loginSchema = z.object({
email: z.string().email('بريد إلكتروني غير صالح'),
password: z.string().min(1, 'كلمة المرور مطلوبة'),
})
export const registerSchema = z.object({
name: z.string().min(2, 'يجب أن يكون الاسم حرفين على الأقل'),
email: z.string().email('بريد إلكتروني غير صالح'),
password: z.string().min(8, 'يجب أن تكون كلمة المرور 8 أحرف على الأقل'),
})ثم حدّث مسار إنشاء المهام لاستخدام التحقق:
// server/api/tasks/index.post.ts (محدّث)
import prisma from '~/server/utils/prisma'
import { createTaskSchema } from '~/server/utils/validate'
export default defineEventHandler(async (event) => {
const user = event.context.user
const body = await readBody(event)
const result = createTaskSchema.safeParse(body)
if (!result.success) {
throw createError({
statusCode: 400,
statusMessage: result.error.issues[0].message,
})
}
const task = await prisma.task.create({
data: {
...result.data,
description: result.data.description || null,
userId: user.id,
},
})
return task
})الخطوة 10: اختبار تطبيقك
شغّل خادم التطوير:
pnpm devاختبر التدفق الكامل:
- انتقل إلى
http://localhost:3000— تظهر الصفحة الرئيسية - سجّل بالنقر على "إنشاء حساب" وملء النموذج
- أنشئ مهمة من لوحة التحكم
- غيّر حالة مهمة باستخدام القائمة المنسدلة
- صفّي المهام حسب الحالة
- احذف مهمة بزر سلة المهملات
تأكد من أن PostgreSQL يعمل وأن DATABASE_URL صحيح قبل تشغيل التطبيق. إذا كنت تستخدم خدمة سحابية مثل Neon فتحقق من أن عنوان IP لجهازك مسموح به.
الخطوة 11: التحضير للإنتاج
تكوين البناء
حدّث nuxt.config.ts للإنتاج:
// nuxt.config.ts (إضافات الإنتاج)
export default defineNuxtConfig({
// ... التكوين الحالي
nitro: {
preset: 'node-server',
compressPublicAssets: true,
},
app: {
head: {
title: 'TaskFlow - مدير المهام',
meta: [
{ name: 'description', content: 'تطبيق إدارة مهام حديث وفعال' },
],
},
},
})البناء والتشغيل
# بناء الإنتاج
pnpm build
# التشغيل في الإنتاج
node .output/server/index.mjsالنشر مع Docker
أنشئ ملف Dockerfile:
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm dlx prisma generate
RUN pnpm build
FROM base AS production
WORKDIR /app
COPY --from=build /app/.output .output
COPY --from=build /app/node_modules/.prisma node_modules/.prisma
COPY --from=build /app/prisma prisma
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]# البناء والتشغيل
docker build -t taskflow .
docker run -p 3000:3000 --env-file .env taskflowاستكشاف الأخطاء وإصلاحها
خطأ "Cannot find module @prisma/client"
تأكد من تنفيذ pnpm dlx prisma generate بعد تثبيت التبعيات.
الجلسات لا تستمر
تحقق من أن SESSION_SECRET معرّف في ملف .env. في الإنتاج استخدم سراً طويلاً وعشوائياً.
خطأ في الاتصال بقاعدة البيانات
تحقق من DATABASE_URL وتأكد من إمكانية الوصول إلى PostgreSQL. للخدمات السحابية تحقق من قواعد جدار الحماية.
مكونات Nuxt UI لا تظهر
تأكد من أن @nuxt/ui موجود في قائمة modules في nuxt.config.ts وأن التبعيات مثبتة.
الخطوات التالية
الآن بعد أن أصبح تطبيقك يعمل يمكنك:
- إضافة فئات لتنظيم المهام حسب المشروع
- تنفيذ السحب والإفلات مع لوحة كانبان (vue-draggable)
- إضافة إشعارات بالبريد الإلكتروني للمهام المتأخرة
- دمج OAuth مع Google أو GitHub عبر nuxt-auth-utils
- إضافة اختبارات مع Vitest و Testing Library
- إعداد SSR انتقائي لتحسين الأداء
الخلاصة
لقد بنيت تطبيق ويب متكامل وشامل باستخدام Nuxt 4 و Vue 3. غطى هذا الدليل:
- هيكل مشروع Nuxt 4 الجديد مع مجلد
app/ - نظام التوجيه التلقائي القائم على الملفات
- مسارات API مع محرك Nitro
- إدارة قاعدة البيانات مع Prisma ORM
- واجهة Composition API في Vue 3 للمكونات التفاعلية
- التحقق من البيانات مع Zod
- النشر مع Docker
يوفر Nuxt 4 تجربة تطوير متكاملة رائعة بفضل تكامله السلس مع جانب الخادم ونظامه الغني بالوحدات. سواء كنت تبني منتجاً أولياً أو تطبيقاً مؤسسياً فإن Nuxt 4 خيار قوي لمشاريع Vue.js الخاصة بك.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء تطبيق full-stack مع TanStack Start: إطار عمل React من الجيل القادم
تعلّم كيفية بناء تطبيق full-stack كامل مع TanStack Start، إطار عمل React الوصفي المدعوم بـ TanStack Router وVite. يغطي هذا الدليل التوجيه المبني على الملفات، ودوال الخادم، والبرمجيات الوسيطة، والمصادقة، والنشر.

بناء تطبيق متكامل باستخدام PocketBase و Next.js في 2026
تعلّم كيفية بناء تطبيق متكامل باستخدام PocketBase كخادم خلفي و Next.js كواجهة أمامية. يغطي هذا الدليل المصادقة وعمليات CRUD في الوقت الفعلي والنشر على الخادم.

React Router v7: بناء تطبيق full-stack مع وضع الإطار (Framework Mode)
تعلم كيفية بناء تطبيق full-stack كامل باستخدام React Router v7 في وضع الإطار. يغطي هذا الدرس SSR والـ loaders والـ actions والتحقق من النماذج ومعالجة الأخطاء.