Basic Usage Examples
Learn how to implement God Panel components with practical examples and real-world use cases.
Getting Started Example
Complete Dashboard Page
vue
<!-- pages/dashboard/index.vue -->
<template>
<div class="dashboard-page">
<!-- Header -->
<div class="dashboard-header">
<div class="header-content">
<h1>{{ $t('dashboard.title') }}</h1>
<p>{{ $t('dashboard.subtitle') }}</p>
</div>
<div class="header-actions">
<Button @click="refreshData" variant="secondary">
<Icon name="mdi-refresh" />
Refresh
</Button>
<Button @click="exportData" variant="primary">
<Icon name="mdi-download" />
Export
</Button>
</div>
</div>
<!-- Stats Cards -->
<div class="stats-grid">
<Card v-for="stat in stats" :key="stat.id" class="stat-card">
<div class="stat-header">
<Icon :name="stat.icon" class="stat-icon" />
<h3>{{ stat.title }}</h3>
</div>
<div class="stat-value">{{ formatNumber(stat.value) }}</div>
<div class="stat-change" :class="{ 'positive': stat.change > 0, 'negative': stat.change < 0 }">
<Icon :name="stat.change > 0 ? 'mdi-trending-up' : 'mdi-trending-down'" />
{{ Math.abs(stat.change) }}%
</div>
</Card>
</div>
<!-- Charts Section -->
<div class="charts-grid">
<Card class="chart-card">
<div class="card-header">
<h3>Revenue Overview</h3>
</div>
<div class="card-body">
<LineChart :data="revenueData" />
</div>
</Card>
<Card class="chart-card">
<div class="card-header">
<h3>User Growth</h3>
</div>
<div class="card-body">
<BarChart :data="userData" />
</div>
</Card>
</div>
<!-- Recent Activity -->
<Card class="activity-card">
<div class="card-header">
<h3>Recent Activity</h3>
<Button variant="ghost" size="sm">
View All
<Icon name="mdi-arrow-right" />
</Button>
</div>
<div class="card-body">
<div v-if="loading" class="loading-state">
<Icon name="mdi-loading" class="loading-icon" />
Loading activities...
</div>
<div v-else-if="activities.length === 0" class="empty-state">
<Icon name="mdi-information" />
No recent activities
</div>
<div v-else class="activity-list">
<div v-for="activity in activities" :key="activity.id" class="activity-item">
<div class="activity-icon">
<Icon :name="activity.icon" />
</div>
<div class="activity-content">
<div class="activity-title">{{ activity.title }}</div>
<div class="activity-description">{{ activity.description }}</div>
<div class="activity-time">{{ formatRelativeTime(activity.createdAt) }}</div>
</div>
</div>
</div>
</div>
</Card>
</div>
</template>
<script setup>
// Composables
const { formatNumber, formatRelativeTime } = useLocale()
const { t } = useI18n()
// Reactive data
const stats = ref([
{
id: 'users',
title: 'Total Users',
value: 12543,
change: 12.5,
icon: 'mdi-account-group'
},
{
id: 'orders',
title: 'Total Orders',
value: 892,
change: -3.2,
icon: 'mdi-cart'
},
{
id: 'revenue',
title: 'Revenue',
value: 45678,
change: 8.7,
icon: 'mdi-currency-usd'
},
{
id: 'conversion',
title: 'Conversion Rate',
value: 3.24,
change: 15.3,
icon: 'mdi-trending-up'
}
])
const activities = ref([
{
id: '1',
title: 'New user registered',
description: 'john.doe@example.com joined the platform',
icon: 'mdi-account-plus',
createdAt: new Date(Date.now() - 1000 * 60 * 5) // 5 minutes ago
},
{
id: '2',
title: 'Order completed',
description: 'Order #1234 has been completed successfully',
icon: 'mdi-check-circle',
createdAt: new Date(Date.now() - 1000 * 60 * 15) // 15 minutes ago
},
{
id: '3',
title: 'Payment received',
description: '$299.99 payment received from customer',
icon: 'mdi-credit-card',
createdAt: new Date(Date.now() - 1000 * 60 * 30) // 30 minutes ago
}
])
const loading = ref(false)
// Chart data
const revenueData = ref({
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Revenue',
data: [12000, 19000, 15000, 25000, 22000, 30000],
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4
}]
})
const userData = ref({
labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4'],
datasets: [{
label: 'New Users',
data: [120, 150, 180, 200],
backgroundColor: 'rgba(16, 185, 129, 0.8)',
borderColor: 'rgb(16, 185, 129)',
borderWidth: 1
}]
})
// Methods
const refreshData = async () => {
loading.value = true
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000))
// Update data (in real app, fetch from API)
stats.value = stats.value.map(stat => ({
...stat,
value: stat.value + Math.floor(Math.random() * 100)
}))
activities.value.unshift({
id: Date.now().toString(),
title: 'Data refreshed',
description: 'Dashboard data has been updated',
icon: 'mdi-refresh',
createdAt: new Date()
})
activities.value = activities.value.slice(0, 5)
} finally {
loading.value = false
}
}
const exportData = async () => {
try {
// Simulate export
const data = {
stats: stats.value,
activities: activities.value,
exportedAt: new Date().toISOString()
}
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: 'application/json'
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `dashboard-export-${new Date().toISOString().split('T')[0]}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
} catch (error) {
console.error('Export failed:', error)
}
}
</script>
<style scoped>
.dashboard-page {
padding: var(--space-6);
max-width: 1200px;
margin: 0 auto;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--space-8);
gap: var(--space-4);
}
.header-content h1 {
font-size: var(--text-3xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin-bottom: var(--space-2);
}
.header-content p {
color: var(--text-secondary);
font-size: var(--text-lg);
}
.header-actions {
display: flex;
gap: var(--space-3);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--space-6);
margin-bottom: var(--space-8);
}
.stat-card {
padding: var(--space-6);
text-align: center;
}
.stat-header {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-3);
margin-bottom: var(--space-4);
}
.stat-header h3 {
font-size: var(--text-lg);
font-weight: var(--font-medium);
color: var(--text-secondary);
margin: 0;
}
.stat-icon {
width: 1.5rem;
height: 1.5rem;
color: var(--color-primary);
}
.stat-value {
font-size: var(--text-3xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin-bottom: var(--space-2);
}
.stat-change {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-1);
font-size: var(--text-sm);
font-weight: var(--font-medium);
}
.stat-change.positive {
color: var(--color-success);
}
.stat-change.negative {
color: var(--color-error);
}
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: var(--space-6);
margin-bottom: var(--space-8);
}
.chart-card {
min-height: 300px;
}
.activity-card {
min-height: 400px;
}
.loading-state,
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-3);
padding: var(--space-8);
color: var(--text-tertiary);
}
.loading-icon {
width: 2rem;
height: 2rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.activity-list {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.activity-item {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-3);
border-radius: var(--radius-md);
background-color: var(--bg-secondary);
transition: background-color 0.2s ease;
}
.activity-item:hover {
background-color: var(--bg-tertiary);
}
.activity-icon {
width: 2rem;
height: 2rem;
background-color: var(--color-primary);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: white;
flex-shrink: 0;
}
.activity-content {
flex: 1;
}
.activity-title {
font-weight: var(--font-medium);
color: var(--text-primary);
margin-bottom: var(--space-1);
}
.activity-description {
font-size: var(--text-sm);
color: var(--text-secondary);
margin-bottom: var(--space-1);
}
.activity-time {
font-size: var(--text-xs);
color: var(--text-tertiary);
}
@media (max-width: 768px) {
.dashboard-page {
padding: var(--space-4);
}
.dashboard-header {
flex-direction: column;
align-items: stretch;
}
.header-actions {
justify-content: stretch;
}
.stats-grid {
grid-template-columns: 1fr;
}
.charts-grid {
grid-template-columns: 1fr;
}
}
</style>User Management Example
User List with Actions
vue
<!-- pages/users/index.vue -->
<template>
<div class="users-page">
<!-- Page Header -->
<div class="page-header">
<div class="header-content">
<h1>{{ $t('users.title') }}</h1>
<p>{{ $t('users.subtitle') }}</p>
</div>
<div class="header-actions">
<Button @click="showCreateModal = true" variant="primary">
<Icon name="mdi-account-plus" />
Add User
</Button>
</div>
</div>
<!-- Filters and Search -->
<Card class="filters-card">
<div class="filters-content">
<div class="search-box">
<Input
v-model="searchQuery"
:placeholder="$t('users.search')"
icon="mdi-magnify"
/>
</div>
<div class="filter-selects">
<Select v-model="roleFilter" :options="roleOptions" placeholder="All Roles" />
<Select v-model="statusFilter" :options="statusOptions" placeholder="All Status" />
</div>
<div class="filter-actions">
<Button @click="clearFilters" variant="ghost">Clear</Button>
<Button @click="applyFilters" variant="secondary">Apply</Button>
</div>
</div>
</Card>
<!-- Users Table -->
<Card class="table-card">
<div class="table-header">
<div class="table-info">
<span class="table-count">
{{ $t('users.showing', { count: filteredUsers.length, total: totalUsers }) }}
</span>
</div>
<div class="table-actions">
<Button @click="exportUsers" variant="ghost" size="sm">
<Icon name="mdi-download" />
Export
</Button>
</div>
</div>
<DataTable
:columns="tableColumns"
:data="filteredUsers"
:loading="loading"
:empty-message="$t('users.noUsers')"
@sort="handleSort"
@filter="handleFilter"
>
<!-- Custom cell templates -->
<template #user="{ item }">
<div class="user-cell">
<div class="user-avatar">
<img :src="item.avatar" :alt="item.name" />
</div>
<div class="user-info">
<div class="user-name">{{ item.name }}</div>
<div class="user-email">{{ item.email }}</div>
</div>
</div>
</template>
<template #role="{ item }">
<span class="role-badge" :class="`role-${item.role}`">
{{ $t(`roles.${item.role}`) }}
</span>
</template>
<template #status="{ item }">
<span class="status-badge" :class="`status-${item.status}`">
<Icon :name="item.status === 'active' ? 'mdi-check-circle' : 'mdi-minus-circle'" />
{{ $t(`status.${item.status}`) }}
</span>
</template>
<template #actions="{ item }">
<div class="action-buttons">
<Button @click="viewUser(item)" variant="ghost" size="sm">
<Icon name="mdi-eye" />
</Button>
<Button @click="editUser(item)" variant="ghost" size="sm">
<Icon name="mdi-pencil" />
</Button>
<Button @click="deleteUser(item)" variant="ghost" size="sm" class="danger">
<Icon name="mdi-delete" />
</Button>
</div>
</template>
</DataTable>
<!-- Pagination -->
<div class="table-footer">
<Pagination
v-model:current="currentPage"
v-model:page-size="pageSize"
:total="totalUsers"
:show-size-changer="true"
:page-size-options="[10, 20, 50, 100]"
@change="handlePageChange"
/>
</div>
</Card>
<!-- Create User Modal -->
<Modal v-model="showCreateModal" :title="$t('users.createTitle')">
<form @submit.prevent="createUser" class="user-form">
<div class="form-row">
<Input
v-model="newUser.firstName"
:label="$t('users.firstName')"
required
/>
<Input
v-model="newUser.lastName"
:label="$t('users.lastName')"
required
/>
</div>
<Input
v-model="newUser.email"
type="email"
:label="$t('users.email')"
required
/>
<Select
v-model="newUser.role"
:label="$t('users.role')"
:options="roleOptions"
required
/>
<div class="form-actions">
<Button type="button" @click="showCreateModal = false" variant="secondary">
{{ $t('common.cancel') }}
</Button>
<Button type="submit" variant="primary" :loading="creating">
{{ $t('users.create') }}
</Button>
</div>
</form>
</Modal>
</div>
</template>
<script setup>
// Composables
const { t } = useI18n()
const { formatDate } = useLocale()
// Reactive data
const users = ref([])
const filteredUsers = ref([])
const loading = ref(false)
const creating = ref(false)
const searchQuery = ref('')
const roleFilter = ref('')
const statusFilter = ref('')
const currentPage = ref(1)
const pageSize = ref(20)
const totalUsers = ref(0)
const showCreateModal = ref(false)
const newUser = ref({
firstName: '',
lastName: '',
email: '',
role: 'user'
})
// Options
const roleOptions = [
{ label: t('roles.admin'), value: 'admin' },
{ label: t('roles.user'), value: 'user' },
{ label: t('roles.moderator'), value: 'moderator' }
]
const statusOptions = [
{ label: t('status.active'), value: 'active' },
{ label: t('status.inactive'), value: 'inactive' },
{ label: t('status.pending'), value: 'pending' }
]
// Table columns
const tableColumns = [
{
key: 'user',
title: t('users.name'),
sortable: true,
width: '250px'
},
{
key: 'role',
title: t('users.role'),
sortable: true,
width: '120px'
},
{
key: 'status',
title: t('users.status'),
sortable: true,
width: '120px'
},
{
key: 'createdAt',
title: t('users.createdAt'),
sortable: true,
width: '150px',
formatter: (value: string) => formatDate(new Date(value))
},
{
key: 'actions',
title: t('common.actions'),
width: '150px'
}
]
// Methods
const fetchUsers = async () => {
loading.value = true
try {
const response = await $fetch('/api/users', {
params: {
page: currentPage.value,
limit: pageSize.value,
search: searchQuery.value,
role: roleFilter.value,
status: statusFilter.value
}
})
users.value = response.data
filteredUsers.value = response.data
totalUsers.value = response.total
} catch (error) {
console.error('Failed to fetch users:', error)
} finally {
loading.value = false
}
}
const applyFilters = () => {
filteredUsers.value = users.value.filter(user => {
const matchesSearch = !searchQuery.value ||
user.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchesRole = !roleFilter.value || user.role === roleFilter.value
const matchesStatus = !statusFilter.value || user.status === statusFilter.value
return matchesSearch && matchesRole && matchesStatus
})
currentPage.value = 1
}
const clearFilters = () => {
searchQuery.value = ''
roleFilter.value = ''
statusFilter.value = ''
applyFilters()
}
const handleSort = (column: string, direction: 'asc' | 'desc') => {
filteredUsers.value.sort((a, b) => {
const aValue = a[column]
const bValue = b[column]
if (direction === 'asc') {
return aValue > bValue ? 1 : -1
} else {
return aValue < bValue ? 1 : -1
}
})
}
const handlePageChange = (page: number, size: number) => {
currentPage.value = page
pageSize.value = size
fetchUsers()
}
const createUser = async () => {
creating.value = true
try {
await $fetch('/api/users', {
method: 'POST',
body: {
name: `${newUser.value.firstName} ${newUser.value.lastName}`,
email: newUser.value.email,
role: newUser.value.role
}
})
showCreateModal.value = false
newUser.value = { firstName: '', lastName: '', email: '', role: 'user' }
await fetchUsers()
} catch (error) {
console.error('Failed to create user:', error)
} finally {
creating.value = false
}
}
const viewUser = (user: any) => {
navigateTo(`/users/${user.id}`)
}
const editUser = (user: any) => {
navigateTo(`/users/${user.id}/edit`)
}
const deleteUser = async (user: any) => {
if (confirm(t('users.confirmDelete', { name: user.name }))) {
try {
await $fetch(`/api/users/${user.id}`, { method: 'DELETE' })
await fetchUsers()
} catch (error) {
console.error('Failed to delete user:', error)
}
}
}
const exportUsers = async () => {
try {
const data = await $fetch('/api/users/export')
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `users-export-${new Date().toISOString().split('T')[0]}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
} catch (error) {
console.error('Export failed:', error)
}
}
// Lifecycle
onMounted(() => {
fetchUsers()
})
</script>
<style scoped>
.users-page {
padding: var(--space-6);
max-width: 1400px;
margin: 0 auto;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--space-6);
gap: var(--space-4);
}
.header-content h1 {
font-size: var(--text-3xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin-bottom: var(--space-2);
}
.header-content p {
color: var(--text-secondary);
font-size: var(--text-lg);
}
.filters-card {
margin-bottom: var(--space-6);
}
.filters-content {
display: flex;
gap: var(--space-4);
align-items: flex-end;
flex-wrap: wrap;
}
.search-box {
flex: 1;
min-width: 200px;
}
.filter-selects {
display: flex;
gap: var(--space-3);
}
.filter-actions {
display: flex;
gap: var(--space-2);
}
.table-card {
margin-bottom: var(--space-6);
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-4) var(--space-6);
border-bottom: 1px solid var(--border-primary);
}
.table-info {
color: var(--text-secondary);
font-size: var(--text-sm);
}
.table-count {
font-weight: var(--font-medium);
}
.table-footer {
padding: var(--space-4) var(--space-6);
border-top: 1px solid var(--border-primary);
}
.user-cell {
display: flex;
align-items: center;
gap: var(--space-3);
}
.user-avatar {
width: 2.5rem;
height: 2.5rem;
border-radius: var(--radius-full);
overflow: hidden;
background-color: var(--bg-secondary);
display: flex;
align-items: center;
justify-content: center;
}
.user-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-info {
flex: 1;
}
.user-name {
font-weight: var(--font-medium);
color: var(--text-primary);
}
.user-email {
font-size: var(--text-sm);
color: var(--text-secondary);
}
.role-badge,
.status-badge {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-full);
font-size: var(--text-xs);
font-weight: var(--font-medium);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.role-badge.role-admin {
background-color: rgba(239, 68, 68, 0.1);
color: var(--color-error);
}
.role-badge.role-user {
background-color: rgba(59, 130, 246, 0.1);
color: var(--color-primary);
}
.role-badge.role-moderator {
background-color: rgba(245, 158, 11, 0.1);
color: var(--color-warning);
}
.status-badge.status-active {
background-color: rgba(16, 185, 129, 0.1);
color: var(--color-success);
}
.status-badge.status-inactive {
background-color: rgba(107, 114, 128, 0.1);
color: var(--color-neutral-500);
}
.status-badge.status-pending {
background-color: rgba(245, 158, 11, 0.1);
color: var(--color-warning);
}
.action-buttons {
display: flex;
gap: var(--space-1);
}
.action-buttons .danger {
color: var(--color-error);
}
.user-form {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-4);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-3);
padding-top: var(--space-4);
border-top: 1px solid var(--border-primary);
}
@media (max-width: 768px) {
.users-page {
padding: var(--space-4);
}
.page-header {
flex-direction: column;
align-items: stretch;
}
.filters-content {
flex-direction: column;
align-items: stretch;
}
.filter-selects {
flex-direction: column;
}
.form-row {
grid-template-columns: 1fr;
}
}
</style>Form Handling Example
Advanced Form with Validation
vue
<!-- components/forms/UserForm.vue -->
<template>
<form @submit.prevent="handleSubmit" class="user-form">
<!-- Basic Information -->
<Card class="form-section">
<div class="card-header">
<h3>Basic Information</h3>
</div>
<div class="card-body">
<div class="form-grid">
<Input
v-model="form.firstName"
:label="$t('users.firstName')"
:error="errors.firstName"
required
:disabled="loading"
/>
<Input
v-model="form.lastName"
:label="$t('users.lastName')"
:error="errors.lastName"
required
:disabled="loading"
/>
</div>
<Input
v-model="form.email"
type="email"
:label="$t('users.email')"
:error="errors.email"
required
:disabled="loading"
/>
<Input
v-model="form.phone"
type="tel"
:label="$t('users.phone')"
:error="errors.phone"
:disabled="loading"
/>
</div>
</Card>
<!-- Account Settings -->
<Card class="form-section">
<div class="card-header">
<h3>Account Settings</h3>
</div>
<div class="card-body">
<Select
v-model="form.role"
:label="$t('users.role')"
:options="roleOptions"
:error="errors.role"
required
:disabled="loading"
/>
<Select
v-model="form.status"
:label="$t('users.status')"
:options="statusOptions"
:error="errors.status"
required
:disabled="loading"
/>
<div class="form-group">
<label class="form-label">Permissions</label>
<div class="permissions-grid">
<Checkbox
v-for="permission in permissions"
:key="permission.id"
v-model="form.permissions"
:value="permission.id"
:label="permission.name"
:description="permission.description"
:disabled="loading"
/>
</div>
</div>
</div>
</Card>
<!-- Security Settings -->
<Card class="form-section" v-if="!isEdit">
<div class="card-header">
<h3>Security</h3>
</div>
<div class="card-body">
<Input
v-model="form.password"
type="password"
:label="$t('auth.password')"
:error="errors.password"
required
:disabled="loading"
:hint="isEdit ? $t('users.leaveBlankToKeepCurrent') : ''"
/>
<Input
v-model="form.confirmPassword"
type="password"
:label="$t('auth.confirmPassword')"
:error="errors.confirmPassword"
required
:disabled="loading"
/>
</div>
</Card>
<!-- Form Actions -->
<div class="form-actions">
<Button
type="button"
variant="secondary"
@click="$emit('cancel')"
:disabled="loading"
>
{{ $t('common.cancel') }}
</Button>
<Button
type="submit"
variant="primary"
:loading="loading"
>
{{ isEdit ? $t('users.update') : $t('users.create') }}
</Button>
</div>
<!-- Error Summary -->
<div v-if="Object.keys(errors).length > 0" class="error-summary">
<h4>{{ $t('validation.errorsFound') }}</h4>
<ul>
<li v-for="(error, field) in errors" :key="field">
{{ $t(`validation.${field}`) }}: {{ error }}
</li>
</ul>
</div>
</form>
</template>
<script setup>
interface UserFormData {
firstName: string
lastName: string
email: string
phone?: string
role: string
status: string
permissions: string[]
password?: string
confirmPassword?: string
}
interface Props {
modelValue?: Partial<UserFormData>
isEdit?: boolean
loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isEdit: false,
loading: false
})
const emit = defineEmits<{
submit: [data: UserFormData]
cancel: []
'update:modelValue': [data: Partial<UserFormData>]
}>()
// Form data
const form = ref<UserFormData>({
firstName: props.modelValue?.firstName || '',
lastName: props.modelValue?.lastName || '',
email: props.modelValue?.email || '',
phone: props.modelValue?.phone || '',
role: props.modelValue?.role || 'user',
status: props.modelValue?.status || 'active',
permissions: props.modelValue?.permissions || [],
password: '',
confirmPassword: ''
})
// Validation errors
const errors = ref<Record<string, string>>({})
// Options
const roleOptions = [
{ label: 'Administrator', value: 'admin' },
{ label: 'User', value: 'user' },
{ label: 'Moderator', value: 'moderator' }
]
const statusOptions = [
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
{ label: 'Pending', value: 'pending' }
]
const permissions = [
{ id: 'read_users', name: 'Read Users', description: 'Can view user profiles' },
{ id: 'write_users', name: 'Write Users', description: 'Can create and edit users' },
{ id: 'delete_users', name: 'Delete Users', description: 'Can delete user accounts' },
{ id: 'manage_settings', name: 'Manage Settings', description: 'Can modify system settings' }
]
// Validation rules
const validationRules = {
firstName: { required: true, minLength: 2 },
lastName: { required: true, minLength: 2 },
email: { required: true, email: true },
phone: { phone: true },
role: { required: true },
status: { required: true },
password: {
required: !props.isEdit,
minLength: 8,
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/
},
confirmPassword: {
required: !props.isEdit,
match: 'password'
}
}
// Validation function
const validateField = (field: string, value: any): string => {
const rules = validationRules[field as keyof typeof validationRules]
if (!rules) return ''
if (rules.required && (!value || value.toString().trim() === '')) {
return 'This field is required'
}
if (rules.minLength && value && value.length < rules.minLength) {
return `Must be at least ${rules.minLength} characters`
}
if (rules.email && value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(value)) {
return 'Please enter a valid email address'
}
}
if (rules.phone && value) {
const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/
if (!phoneRegex.test(value.replace(/\s/g, ''))) {
return 'Please enter a valid phone number'
}
}
if (rules.pattern && value) {
if (!rules.pattern.test(value)) {
return 'Password must contain uppercase, lowercase, and number'
}
}
if (rules.match && value) {
const matchField = form.value[rules.match as keyof UserFormData]
if (value !== matchField) {
return 'Passwords do not match'
}
}
return ''
}
// Validate entire form
const validateForm = (): boolean => {
errors.value = {}
Object.keys(validationRules).forEach(field => {
const error = validateField(field, form.value[field as keyof UserFormData])
if (error) {
errors.value[field] = error
}
})
return Object.keys(errors.value).length === 0
}
// Submit handler
const handleSubmit = () => {
if (!validateForm()) {
return
}
// Prepare data for submission
const submitData = { ...form.value }
// Remove confirmPassword from submission
delete submitData.confirmPassword
// Only include password if it's not empty (for edit mode)
if (!submitData.password) {
delete submitData.password
}
emit('submit', submitData)
}
// Watch for external changes
watch(() => props.modelValue, (newValue) => {
if (newValue) {
Object.assign(form.value, newValue)
}
}, { deep: true })
// Emit form changes
watch(form.value, (newValue) => {
emit('update:modelValue', newValue)
}, { deep: true })
</script>
<style scoped>
.user-form {
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.form-section {
margin-bottom: var(--space-6);
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-4);
margin-bottom: var(--space-4);
}
.permissions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--space-3);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-3);
padding: var(--space-6);
border-top: 1px solid var(--border-primary);
background-color: var(--bg-secondary);
}
.error-summary {
background-color: rgba(239, 68, 68, 0.1);
border: 1px solid var(--color-error);
border-radius: var(--radius-md);
padding: var(--space-4);
margin-top: var(--space-4);
}
.error-summary h4 {
color: var(--color-error);
margin-bottom: var(--space-2);
font-size: var(--text-sm);
font-weight: var(--font-semibold);
}
.error-summary ul {
list-style: none;
padding: 0;
margin: 0;
}
.error-summary li {
color: var(--color-error);
font-size: var(--text-sm);
padding: var(--space-1) 0;
}
@media (max-width: 768px) {
.form-grid {
grid-template-columns: 1fr;
}
.permissions-grid {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column-reverse;
}
}
</style>Next Steps
These examples demonstrate the core functionality of God Panel components. For more advanced examples:
- Advanced Usage Examples - Complex implementations
- Code Examples - Reusable code snippets
- Component Library Documentation - Complete component reference
Troubleshooting
Common Issues
Component not rendering:
typescript
// Check component registration
console.log('Available components:', getCurrentInstance()?.appContext.components)
// Check props
console.log('Component props:', props)Styling conflicts:
css
/* Use specific selectors */
.my-component .btn-primary {
/* Your custom styles */
}Performance issues:
typescript
// Use computed for expensive operations
const processedData = computed(() => {
return data.value.map(item => ({
...item,
formattedValue: formatCurrency(item.value)
}))
})Next: Check out the Advanced Usage Examples for more complex implementations!