|
|
|
|
@@ -1,798 +0,0 @@
|
|
|
|
|
# Sakai Vue - Admin Dashboard Template
|
|
|
|
|
|
|
|
|
|
Sakai Vue is a modern, production-ready admin dashboard template built with Vue 3, Vite, and PrimeVue. It provides a comprehensive set of UI components, pre-built layouts, and data services for rapidly developing enterprise-grade web applications. The template follows Vue 3 Composition API patterns with reactive state management and includes extensive component showcases demonstrating best practices.
|
|
|
|
|
|
|
|
|
|
The application features a modular architecture with five mock data services (Product, Customer, Country, Node, and Photo), a flexible theming system with three presets and 17 color schemes, and responsive layouts optimized for both desktop and mobile devices. It includes a complete CRUD implementation, dashboard with analytics widgets, 15+ UI component demo pages, authentication pages, and a public landing page, making it an ideal starting point for building admin panels, dashboards, and data-driven applications.
|
|
|
|
|
|
|
|
|
|
## Application Initialization
|
|
|
|
|
|
|
|
|
|
Vue 3 application setup with PrimeVue integration and global services
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import { createApp } from 'vue';
|
|
|
|
|
import App from './App.vue';
|
|
|
|
|
import router from './router';
|
|
|
|
|
|
|
|
|
|
import Aura from '@primeuix/themes/aura';
|
|
|
|
|
import PrimeVue from 'primevue/config';
|
|
|
|
|
import ConfirmationService from 'primevue/confirmationservice';
|
|
|
|
|
import ToastService from 'primevue/toastservice';
|
|
|
|
|
|
|
|
|
|
import '@/assets/styles.scss';
|
|
|
|
|
|
|
|
|
|
const app = createApp(App);
|
|
|
|
|
|
|
|
|
|
app.use(router);
|
|
|
|
|
app.use(PrimeVue, {
|
|
|
|
|
theme: {
|
|
|
|
|
preset: Aura,
|
|
|
|
|
options: {
|
|
|
|
|
darkModeSelector: '.app-dark'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
app.use(ToastService);
|
|
|
|
|
app.use(ConfirmationService);
|
|
|
|
|
|
|
|
|
|
app.mount('#app');
|
|
|
|
|
|
|
|
|
|
// Expected result: Application mounts to #app with PrimeVue Aura theme,
|
|
|
|
|
// Toast and Confirmation services available globally
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Product Service API
|
|
|
|
|
|
|
|
|
|
Mock data service providing product inventory with 30 sample products across four categories
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import { ProductService } from '@/service/ProductService';
|
|
|
|
|
|
|
|
|
|
// Fetch all products (30 items)
|
|
|
|
|
ProductService.getProducts()
|
|
|
|
|
.then(products => {
|
|
|
|
|
console.log(products);
|
|
|
|
|
// Returns array of products with structure:
|
|
|
|
|
// {
|
|
|
|
|
// id: '1000',
|
|
|
|
|
// code: 'f230fh0g3',
|
|
|
|
|
// name: 'Bamboo Watch',
|
|
|
|
|
// description: 'Product Description',
|
|
|
|
|
// image: 'bamboo-watch.jpg',
|
|
|
|
|
// price: 65,
|
|
|
|
|
// category: 'Accessories',
|
|
|
|
|
// quantity: 24,
|
|
|
|
|
// inventoryStatus: 'INSTOCK',
|
|
|
|
|
// rating: 5
|
|
|
|
|
// }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Fetch products with order history
|
|
|
|
|
ProductService.getProductsWithOrders()
|
|
|
|
|
.then(products => {
|
|
|
|
|
products.forEach(product => {
|
|
|
|
|
console.log(`${product.name} has ${product.orders.length} orders`);
|
|
|
|
|
// Each order contains:
|
|
|
|
|
// { id, productCode, date, amount, quantity, customer, status }
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Fetch limited product sets
|
|
|
|
|
ProductService.getProductsMini() // First 5 products
|
|
|
|
|
.then(products => console.log(products.length)); // Output: 5
|
|
|
|
|
|
|
|
|
|
ProductService.getProductsSmall() // First 10 products
|
|
|
|
|
.then(products => console.log(products.length)); // Output: 10
|
|
|
|
|
|
|
|
|
|
// Available inventory statuses: 'INSTOCK', 'LOWSTOCK', 'OUTOFSTOCK'
|
|
|
|
|
// Available categories: 'Accessories', 'Fitness', 'Clothing', 'Electronics'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Layout State Management Composable
|
|
|
|
|
|
|
|
|
|
Reactive layout configuration for theme switching, menu management, and dark mode
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import { useLayout } from '@/layout/composables/layout';
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
layoutConfig,
|
|
|
|
|
layoutState,
|
|
|
|
|
toggleMenu,
|
|
|
|
|
isSidebarActive,
|
|
|
|
|
isDarkTheme,
|
|
|
|
|
getPrimary,
|
|
|
|
|
getSurface,
|
|
|
|
|
setActiveMenuItem,
|
|
|
|
|
toggleDarkMode
|
|
|
|
|
} = useLayout();
|
|
|
|
|
|
|
|
|
|
// Access current configuration
|
|
|
|
|
console.log(layoutConfig.preset); // 'Aura'
|
|
|
|
|
console.log(layoutConfig.primary); // 'emerald'
|
|
|
|
|
console.log(layoutConfig.menuMode); // 'static' or 'overlay'
|
|
|
|
|
console.log(layoutConfig.darkTheme); // false
|
|
|
|
|
|
|
|
|
|
// Toggle dark mode with view transitions
|
|
|
|
|
toggleDarkMode();
|
|
|
|
|
// Result: Adds/removes 'app-dark' class from document element
|
|
|
|
|
// Uses View Transition API if supported for smooth animation
|
|
|
|
|
|
|
|
|
|
// Toggle menu (responsive behavior)
|
|
|
|
|
toggleMenu();
|
|
|
|
|
// Desktop (>991px): Toggles staticMenuDesktopInactive
|
|
|
|
|
// Mobile: Toggles staticMenuMobileActive
|
|
|
|
|
// Overlay mode: Toggles overlayMenuActive
|
|
|
|
|
|
|
|
|
|
// Check sidebar state
|
|
|
|
|
if (isSidebarActive.value) {
|
|
|
|
|
console.log('Sidebar is currently visible');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set active menu item
|
|
|
|
|
setActiveMenuItem({ value: 'dashboard' });
|
|
|
|
|
// Result: layoutState.activeMenuItem = 'dashboard'
|
|
|
|
|
|
|
|
|
|
// Access computed values
|
|
|
|
|
console.log(isDarkTheme.value); // true/false
|
|
|
|
|
console.log(getPrimary.value); // 'emerald'
|
|
|
|
|
console.log(getSurface.value); // null or surface palette name
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Router Configuration
|
|
|
|
|
|
|
|
|
|
Vue Router setup with lazy-loaded components and nested routes
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import { createRouter, createWebHistory } from 'vue-router';
|
|
|
|
|
import AppLayout from '@/layout/AppLayout.vue';
|
|
|
|
|
|
|
|
|
|
const router = createRouter({
|
|
|
|
|
history: createWebHistory(),
|
|
|
|
|
routes: [
|
|
|
|
|
{
|
|
|
|
|
path: '/',
|
|
|
|
|
component: AppLayout,
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
path: '/',
|
|
|
|
|
name: 'dashboard',
|
|
|
|
|
component: () => import('@/views/Dashboard.vue')
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: '/pages/crud',
|
|
|
|
|
name: 'crud',
|
|
|
|
|
component: () => import('@/views/pages/Crud.vue')
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: '/uikit/table',
|
|
|
|
|
name: 'table',
|
|
|
|
|
component: () => import('@/views/uikit/TableDoc.vue')
|
|
|
|
|
}
|
|
|
|
|
// ... 15+ UI kit routes
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: '/landing',
|
|
|
|
|
name: 'landing',
|
|
|
|
|
component: () => import('@/views/pages/Landing.vue')
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: '/auth/login',
|
|
|
|
|
name: 'login',
|
|
|
|
|
component: () => import('@/views/pages/auth/Login.vue')
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Navigate programmatically
|
|
|
|
|
import { useRouter } from 'vue-router';
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
router.push({ name: 'dashboard' });
|
|
|
|
|
router.push('/pages/crud');
|
|
|
|
|
router.push({ name: 'table' });
|
|
|
|
|
|
|
|
|
|
// All routes use lazy loading for optimal code splitting
|
|
|
|
|
// Routes under AppLayout include sidebar and topbar navigation
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## CRUD Operations Implementation
|
|
|
|
|
|
|
|
|
|
Complete product management with create, read, update, delete operations using PrimeVue DataTable
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ProductService } from '@/service/ProductService';
|
|
|
|
|
import { FilterMatchMode } from '@primevue/core/api';
|
|
|
|
|
import { useToast } from 'primevue/usetoast';
|
|
|
|
|
import { onMounted, ref } from 'vue';
|
|
|
|
|
|
|
|
|
|
const toast = useToast();
|
|
|
|
|
const products = ref([]);
|
|
|
|
|
const product = ref({});
|
|
|
|
|
const productDialog = ref(false);
|
|
|
|
|
const selectedProducts = ref();
|
|
|
|
|
const filters = ref({
|
|
|
|
|
global: { value: null, matchMode: FilterMatchMode.CONTAINS }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Load products on component mount
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
ProductService.getProducts().then((data) => {
|
|
|
|
|
products.value = data;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Create new product
|
|
|
|
|
function openNew() {
|
|
|
|
|
product.value = {};
|
|
|
|
|
productDialog.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save product (create or update)
|
|
|
|
|
function saveProduct() {
|
|
|
|
|
if (product.value.name?.trim()) {
|
|
|
|
|
if (product.value.id) {
|
|
|
|
|
// Update existing product
|
|
|
|
|
const index = products.value.findIndex(p => p.id === product.value.id);
|
|
|
|
|
products.value[index] = product.value;
|
|
|
|
|
toast.add({
|
|
|
|
|
severity: 'success',
|
|
|
|
|
summary: 'Successful',
|
|
|
|
|
detail: 'Product Updated',
|
|
|
|
|
life: 3000
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// Create new product
|
|
|
|
|
product.value.id = createId();
|
|
|
|
|
product.value.code = createId();
|
|
|
|
|
product.value.inventoryStatus = 'INSTOCK';
|
|
|
|
|
products.value.push(product.value);
|
|
|
|
|
toast.add({
|
|
|
|
|
severity: 'success',
|
|
|
|
|
summary: 'Successful',
|
|
|
|
|
detail: 'Product Created',
|
|
|
|
|
life: 3000
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
productDialog.value = false;
|
|
|
|
|
product.value = {};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update existing product
|
|
|
|
|
function editProduct(prod) {
|
|
|
|
|
product.value = { ...prod };
|
|
|
|
|
productDialog.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete single product
|
|
|
|
|
function deleteProduct() {
|
|
|
|
|
products.value = products.value.filter((val) => val.id !== product.value.id);
|
|
|
|
|
toast.add({
|
|
|
|
|
severity: 'success',
|
|
|
|
|
summary: 'Successful',
|
|
|
|
|
detail: 'Product Deleted',
|
|
|
|
|
life: 3000
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete multiple products
|
|
|
|
|
function deleteSelectedProducts() {
|
|
|
|
|
products.value = products.value.filter((val) => !selectedProducts.value.includes(val));
|
|
|
|
|
selectedProducts.value = null;
|
|
|
|
|
toast.add({
|
|
|
|
|
severity: 'success',
|
|
|
|
|
summary: 'Successful',
|
|
|
|
|
detail: 'Products Deleted',
|
|
|
|
|
life: 3000
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate unique ID
|
|
|
|
|
function createId() {
|
|
|
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
|
|
|
return Array.from({ length: 5 }, () =>
|
|
|
|
|
chars.charAt(Math.floor(Math.random() * chars.length))
|
|
|
|
|
).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Expected workflow:
|
|
|
|
|
// 1. Component loads products from service
|
|
|
|
|
// 2. User clicks "New" -> openNew() shows dialog
|
|
|
|
|
// 3. User fills form and saves -> saveProduct() creates product, shows toast
|
|
|
|
|
// 4. User clicks edit icon -> editProduct() pre-fills dialog with product data
|
|
|
|
|
// 5. User updates and saves -> saveProduct() updates product, shows toast
|
|
|
|
|
// 6. User clicks delete icon -> deleteProduct() removes product, shows toast
|
|
|
|
|
// 7. User selects multiple products and clicks delete -> deleteSelectedProducts()
|
|
|
|
|
</script>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Dashboard Component Structure
|
|
|
|
|
|
|
|
|
|
Dashboard page composing five analytics widgets in responsive grid layout
|
|
|
|
|
|
|
|
|
|
```vue
|
|
|
|
|
<script setup>
|
|
|
|
|
import BestSellingWidget from '@/components/dashboard/BestSellingWidget.vue';
|
|
|
|
|
import NotificationsWidget from '@/components/dashboard/NotificationsWidget.vue';
|
|
|
|
|
import RecentSalesWidget from '@/components/dashboard/RecentSalesWidget.vue';
|
|
|
|
|
import RevenueStreamWidget from '@/components/dashboard/RevenueStreamWidget.vue';
|
|
|
|
|
import StatsWidget from '@/components/dashboard/StatsWidget.vue';
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="grid grid-cols-12 gap-8">
|
|
|
|
|
<!-- Stats cards spanning full width -->
|
|
|
|
|
<StatsWidget />
|
|
|
|
|
|
|
|
|
|
<!-- Two-column layout on extra large screens -->
|
|
|
|
|
<div class="col-span-12 xl:col-span-6">
|
|
|
|
|
<RecentSalesWidget />
|
|
|
|
|
<BestSellingWidget />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-span-12 xl:col-span-6">
|
|
|
|
|
<RevenueStreamWidget />
|
|
|
|
|
<NotificationsWidget />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
Responsive behavior:
|
|
|
|
|
- Mobile/Tablet: Single column, widgets stack vertically
|
|
|
|
|
- Desktop (xl): Two-column layout with widgets side by side
|
|
|
|
|
- StatsWidget shows 4 KPI cards: Orders, Revenue, Customers, Comments
|
|
|
|
|
- RecentSalesWidget shows table of recent sales transactions
|
|
|
|
|
- BestSellingWidget displays chart of best-selling products
|
|
|
|
|
- RevenueStreamWidget visualizes revenue breakdown
|
|
|
|
|
- NotificationsWidget lists recent system notifications
|
|
|
|
|
-->
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Customer Service API
|
|
|
|
|
|
|
|
|
|
Mock data service providing customer records with representative and activity data
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import { CustomerService } from '@/service/CustomerService';
|
|
|
|
|
|
|
|
|
|
CustomerService.getCustomers()
|
|
|
|
|
.then(customers => {
|
|
|
|
|
customers.forEach(customer => {
|
|
|
|
|
console.log({
|
|
|
|
|
id: customer.id,
|
|
|
|
|
name: customer.name,
|
|
|
|
|
country: customer.country, // { name: 'Algeria', code: 'dz' }
|
|
|
|
|
company: customer.company,
|
|
|
|
|
date: customer.date, // ISO date string
|
|
|
|
|
status: customer.status, // 'unqualified', 'negotiation', 'qualified', 'new'
|
|
|
|
|
verified: customer.verified, // boolean
|
|
|
|
|
activity: customer.activity, // 0-100
|
|
|
|
|
representative: {
|
|
|
|
|
name: customer.representative.name,
|
|
|
|
|
image: customer.representative.image
|
|
|
|
|
},
|
|
|
|
|
balance: customer.balance // number
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Usage in DataTable with filtering
|
|
|
|
|
<DataTable
|
|
|
|
|
:value="customers"
|
|
|
|
|
:filters="filters"
|
|
|
|
|
filterDisplay="row"
|
|
|
|
|
paginator
|
|
|
|
|
:rows="10"
|
|
|
|
|
>
|
|
|
|
|
<Column field="name" header="Name" sortable />
|
|
|
|
|
<Column field="country.name" header="Country" sortable />
|
|
|
|
|
<Column field="company" header="Company" sortable />
|
|
|
|
|
<Column field="status" header="Status">
|
|
|
|
|
<template #body="{ data }">
|
|
|
|
|
<Tag
|
|
|
|
|
:value="data.status"
|
|
|
|
|
:severity="getStatusSeverity(data.status)"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</Column>
|
|
|
|
|
<Column field="verified" header="Verified" dataType="boolean" />
|
|
|
|
|
<Column field="balance" header="Balance" sortable>
|
|
|
|
|
<template #body="{ data }">
|
|
|
|
|
{{ formatCurrency(data.balance) }}
|
|
|
|
|
</template>
|
|
|
|
|
</Column>
|
|
|
|
|
</DataTable>
|
|
|
|
|
|
|
|
|
|
// Expected data structure: Array of customer objects with nested country
|
|
|
|
|
// and representative objects, suitable for DataTable display with badges
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Country Service API
|
|
|
|
|
|
|
|
|
|
Mock data service providing 249 world countries with ISO codes
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import { CountryService } from '@/service/CountryService';
|
|
|
|
|
|
|
|
|
|
CountryService.getCountries()
|
|
|
|
|
.then(countries => {
|
|
|
|
|
console.log(countries.length); // Output: 249
|
|
|
|
|
|
|
|
|
|
// Sample country structure
|
|
|
|
|
countries.forEach(country => {
|
|
|
|
|
console.log(country);
|
|
|
|
|
// { name: 'Afghanistan', code: 'af' }
|
|
|
|
|
// { name: 'United States', code: 'us' }
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Usage in Dropdown component
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, onMounted } from 'vue';
|
|
|
|
|
import { CountryService } from '@/service/CountryService';
|
|
|
|
|
|
|
|
|
|
const countries = ref([]);
|
|
|
|
|
const selectedCountry = ref(null);
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
CountryService.getCountries().then(data => {
|
|
|
|
|
countries.value = data;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<Dropdown
|
|
|
|
|
v-model="selectedCountry"
|
|
|
|
|
:options="countries"
|
|
|
|
|
optionLabel="name"
|
|
|
|
|
placeholder="Select a Country"
|
|
|
|
|
filter
|
|
|
|
|
class="w-full"
|
|
|
|
|
>
|
|
|
|
|
<template #value="slotProps">
|
|
|
|
|
<div v-if="slotProps.value">
|
|
|
|
|
{{ slotProps.value.name }}
|
|
|
|
|
</div>
|
|
|
|
|
<span v-else>
|
|
|
|
|
{{ slotProps.placeholder }}
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
<template #option="slotProps">
|
|
|
|
|
<div>{{ slotProps.option.name }}</div>
|
|
|
|
|
</template>
|
|
|
|
|
</Dropdown>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
// Expected result: Dropdown with 249 countries, filterable search,
|
|
|
|
|
// returns selected country object with name and code properties
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Node Service API
|
|
|
|
|
|
|
|
|
|
Mock data service providing hierarchical tree structures for file browsers and navigation
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import { NodeService } from '@/service/NodeService';
|
|
|
|
|
|
|
|
|
|
// Tree structure for Tree component
|
|
|
|
|
NodeService.getTreeNodes()
|
|
|
|
|
.then(nodes => {
|
|
|
|
|
console.log(nodes);
|
|
|
|
|
// Returns 3 root nodes: Documents, Events, Movies
|
|
|
|
|
// Each with nested children representing folders/files
|
|
|
|
|
// Structure: { key, label, data, icon, children[] }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Tree table structure with detailed data
|
|
|
|
|
NodeService.getTreeTableNodes()
|
|
|
|
|
.then(nodes => {
|
|
|
|
|
nodes.forEach(node => {
|
|
|
|
|
console.log({
|
|
|
|
|
key: node.key, // Unique identifier
|
|
|
|
|
data: {
|
|
|
|
|
name: node.data.name,
|
|
|
|
|
size: node.data.size,
|
|
|
|
|
type: node.data.type
|
|
|
|
|
},
|
|
|
|
|
children: node.children // Nested nodes
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Usage in TreeTable component
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, onMounted } from 'vue';
|
|
|
|
|
import { NodeService } from '@/service/NodeService';
|
|
|
|
|
|
|
|
|
|
const nodes = ref([]);
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
NodeService.getTreeTableNodes().then(data => {
|
|
|
|
|
nodes.value = data;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<TreeTable :value="nodes" :expandedKeys="expandedKeys">
|
|
|
|
|
<Column field="name" header="Name" expander />
|
|
|
|
|
<Column field="size" header="Size" />
|
|
|
|
|
<Column field="type" header="Type" />
|
|
|
|
|
</TreeTable>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
// Expected result: 9 root nodes (Applications, Cloud, Desktop, Documents,
|
|
|
|
|
// Downloads, Main, Movies, Photos, Work) with nested children
|
|
|
|
|
// Perfect for file manager UIs and hierarchical data display
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Photo Service API
|
|
|
|
|
|
|
|
|
|
Mock data service providing gallery images hosted on CDN
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import { PhotoService } from '@/service/PhotoService';
|
|
|
|
|
|
|
|
|
|
PhotoService.getImages()
|
|
|
|
|
.then(images => {
|
|
|
|
|
console.log(images.length); // Output: 15
|
|
|
|
|
|
|
|
|
|
images.forEach(image => {
|
|
|
|
|
console.log({
|
|
|
|
|
itemImageSrc: image.itemImageSrc,
|
|
|
|
|
thumbnailImageSrc: image.thumbnailImageSrc,
|
|
|
|
|
alt: image.alt,
|
|
|
|
|
title: image.title
|
|
|
|
|
});
|
|
|
|
|
// Example output:
|
|
|
|
|
// {
|
|
|
|
|
// itemImageSrc: 'https://primefaces.org/cdn/primevue/images/galleria/galleria1.jpg',
|
|
|
|
|
// thumbnailImageSrc: 'https://primefaces.org/cdn/primevue/images/galleria/galleria1s.jpg',
|
|
|
|
|
// alt: 'Description for Image 1',
|
|
|
|
|
// title: 'Title 1'
|
|
|
|
|
// }
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Usage in Galleria component
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, onMounted } from 'vue';
|
|
|
|
|
import { PhotoService } from '@/service/PhotoService';
|
|
|
|
|
|
|
|
|
|
const images = ref([]);
|
|
|
|
|
const activeIndex = ref(0);
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
PhotoService.getImages().then(data => {
|
|
|
|
|
images.value = data;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<Galleria
|
|
|
|
|
:value="images"
|
|
|
|
|
v-model:activeIndex="activeIndex"
|
|
|
|
|
:numVisible="5"
|
|
|
|
|
containerStyle="max-width: 640px"
|
|
|
|
|
:showThumbnails="true"
|
|
|
|
|
:showItemNavigators="true"
|
|
|
|
|
>
|
|
|
|
|
<template #item="slotProps">
|
|
|
|
|
<img
|
|
|
|
|
:src="slotProps.item.itemImageSrc"
|
|
|
|
|
:alt="slotProps.item.alt"
|
|
|
|
|
style="width: 100%; display: block"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
<template #thumbnail="slotProps">
|
|
|
|
|
<img
|
|
|
|
|
:src="slotProps.item.thumbnailImageSrc"
|
|
|
|
|
:alt="slotProps.item.alt"
|
|
|
|
|
style="display: block"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</Galleria>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
// Expected result: Image gallery with 15 photos, thumbnail navigation,
|
|
|
|
|
// full-size image display, navigation arrows, responsive layout
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Theme Configuration and Dynamic Switching
|
|
|
|
|
|
|
|
|
|
Real-time theme customization with preset, color, and surface palette options
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import { useLayout } from '@/layout/composables/layout';
|
|
|
|
|
|
|
|
|
|
const { layoutConfig } = useLayout();
|
|
|
|
|
|
|
|
|
|
// Available theme presets
|
|
|
|
|
const presets = ['Aura', 'Lara', 'Nora'];
|
|
|
|
|
|
|
|
|
|
// Available primary colors
|
|
|
|
|
const primaryColors = [
|
|
|
|
|
'noir', 'emerald', 'green', 'lime', 'orange', 'amber',
|
|
|
|
|
'yellow', 'teal', 'cyan', 'sky', 'blue', 'indigo',
|
|
|
|
|
'violet', 'purple', 'fuchsia', 'pink', 'rose'
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Available surface palettes
|
|
|
|
|
const surfacePalettes = [
|
|
|
|
|
'slate', 'gray', 'zinc', 'neutral',
|
|
|
|
|
'stone', 'soho', 'viva', 'ocean'
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Change theme preset
|
|
|
|
|
import { updatePreset } from '@primeuix/themes';
|
|
|
|
|
layoutConfig.preset = 'Lara';
|
|
|
|
|
updatePreset({
|
|
|
|
|
preset: 'Lara',
|
|
|
|
|
primary: layoutConfig.primary,
|
|
|
|
|
surface: layoutConfig.surface
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Change primary color
|
|
|
|
|
layoutConfig.primary = 'blue';
|
|
|
|
|
updatePreset({
|
|
|
|
|
preset: layoutConfig.preset,
|
|
|
|
|
primary: 'blue',
|
|
|
|
|
surface: layoutConfig.surface
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Change surface palette
|
|
|
|
|
import { updateSurfacePalette } from '@primeuix/themes';
|
|
|
|
|
layoutConfig.surface = 'slate';
|
|
|
|
|
updateSurfacePalette('slate');
|
|
|
|
|
|
|
|
|
|
// Toggle dark mode
|
|
|
|
|
import { toggleDarkMode } from '@/layout/composables/layout';
|
|
|
|
|
toggleDarkMode();
|
|
|
|
|
// Result: Adds/removes 'app-dark' class with smooth View Transition API animation
|
|
|
|
|
|
|
|
|
|
// Complete theme change example
|
|
|
|
|
function applyTheme(preset, primary, surface) {
|
|
|
|
|
layoutConfig.preset = preset;
|
|
|
|
|
layoutConfig.primary = primary;
|
|
|
|
|
layoutConfig.surface = surface;
|
|
|
|
|
|
|
|
|
|
updatePreset({ preset, primary, surface });
|
|
|
|
|
if (surface) {
|
|
|
|
|
updateSurfacePalette(surface);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
applyTheme('Nora', 'purple', 'ocean');
|
|
|
|
|
// Result: Theme instantly updates across entire application
|
|
|
|
|
// All PrimeVue components reflect new colors and styling
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## PrimeVue DataTable with Filtering and Pagination
|
|
|
|
|
|
|
|
|
|
Advanced data table with global search, column filtering, sorting, and pagination
|
|
|
|
|
|
|
|
|
|
```vue
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, onMounted } from 'vue';
|
|
|
|
|
import { FilterMatchMode } from '@primevue/core/api';
|
|
|
|
|
import { ProductService } from '@/service/ProductService';
|
|
|
|
|
|
|
|
|
|
const products = ref([]);
|
|
|
|
|
const filters = ref({
|
|
|
|
|
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
|
|
|
|
name: { value: null, matchMode: FilterMatchMode.STARTS_WITH },
|
|
|
|
|
'category': { value: null, matchMode: FilterMatchMode.IN }
|
|
|
|
|
});
|
|
|
|
|
const categories = ref(['Accessories', 'Fitness', 'Clothing', 'Electronics']);
|
|
|
|
|
const selectedProducts = ref([]);
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
ProductService.getProducts().then(data => {
|
|
|
|
|
products.value = data;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function formatCurrency(value) {
|
|
|
|
|
return value.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getStatusSeverity(status) {
|
|
|
|
|
switch (status) {
|
|
|
|
|
case 'INSTOCK': return 'success';
|
|
|
|
|
case 'LOWSTOCK': return 'warn';
|
|
|
|
|
case 'OUTOFSTOCK': return 'danger';
|
|
|
|
|
default: return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<DataTable
|
|
|
|
|
v-model:selection="selectedProducts"
|
|
|
|
|
v-model:filters="filters"
|
|
|
|
|
:value="products"
|
|
|
|
|
paginator
|
|
|
|
|
:rows="10"
|
|
|
|
|
:rowsPerPageOptions="[5, 10, 20, 50]"
|
|
|
|
|
filterDisplay="row"
|
|
|
|
|
:globalFilterFields="['name', 'category', 'description']"
|
|
|
|
|
sortMode="multiple"
|
|
|
|
|
>
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="flex justify-between">
|
|
|
|
|
<span class="text-xl">Products</span>
|
|
|
|
|
<InputText
|
|
|
|
|
v-model="filters.global.value"
|
|
|
|
|
placeholder="Global Search"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<Column selectionMode="multiple" headerStyle="width: 3rem" />
|
|
|
|
|
|
|
|
|
|
<Column field="name" header="Name" sortable>
|
|
|
|
|
<template #filter="{ filterModel, filterCallback }">
|
|
|
|
|
<InputText
|
|
|
|
|
v-model="filterModel.value"
|
|
|
|
|
@input="filterCallback()"
|
|
|
|
|
placeholder="Search by name"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</Column>
|
|
|
|
|
|
|
|
|
|
<Column field="category" header="Category" sortable>
|
|
|
|
|
<template #filter="{ filterModel, filterCallback }">
|
|
|
|
|
<MultiSelect
|
|
|
|
|
v-model="filterModel.value"
|
|
|
|
|
@change="filterCallback()"
|
|
|
|
|
:options="categories"
|
|
|
|
|
placeholder="Any"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</Column>
|
|
|
|
|
|
|
|
|
|
<Column field="price" header="Price" sortable>
|
|
|
|
|
<template #body="{ data }">
|
|
|
|
|
{{ formatCurrency(data.price) }}
|
|
|
|
|
</template>
|
|
|
|
|
</Column>
|
|
|
|
|
|
|
|
|
|
<Column field="inventoryStatus" header="Status" sortable>
|
|
|
|
|
<template #body="{ data }">
|
|
|
|
|
<Tag
|
|
|
|
|
:value="data.inventoryStatus"
|
|
|
|
|
:severity="getStatusSeverity(data.inventoryStatus)"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</Column>
|
|
|
|
|
|
|
|
|
|
<Column header="Actions">
|
|
|
|
|
<template #body="{ data }">
|
|
|
|
|
<Button icon="pi pi-pencil" class="mr-2" @click="editProduct(data)" />
|
|
|
|
|
<Button icon="pi pi-trash" severity="danger" @click="deleteProduct(data)" />
|
|
|
|
|
</template>
|
|
|
|
|
</Column>
|
|
|
|
|
</DataTable>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
Features demonstrated:
|
|
|
|
|
- Global search across multiple fields
|
|
|
|
|
- Column-specific filters (text input, multiselect)
|
|
|
|
|
- Multi-column sorting
|
|
|
|
|
- Row selection with checkboxes
|
|
|
|
|
- Pagination with configurable rows per page
|
|
|
|
|
- Custom cell rendering (currency, badges, action buttons)
|
|
|
|
|
- Responsive layout
|
|
|
|
|
-->
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Summary
|
|
|
|
|
|
|
|
|
|
Sakai Vue serves as a comprehensive foundation for building modern admin dashboards and data-driven web applications with Vue 3. The template demonstrates enterprise-grade patterns including service-based architecture for data management, reactive state with composables, and extensive PrimeVue component integration. The five mock data services (Product, Customer, Country, Node, Photo) provide realistic sample data with proper structure for immediate prototyping, while the complete CRUD implementation showcases best practices for data manipulation with user feedback through toasts and confirmations.
|
|
|
|
|
|
|
|
|
|
The template's standout features include its flexible theming system supporting three presets (Aura, Lara, Nora) with 17 primary colors and 8 surface palettes, all switchable in real-time without page reloads. The responsive layout system adapts seamlessly from mobile to desktop with static and overlay menu modes, dark mode support with smooth view transitions, and a component library of 15+ UI showcase pages demonstrating every major PrimeVue component. Whether building internal tools, customer portals, or SaaS applications, Sakai Vue provides production-ready code, professional design, and extensive examples that accelerate development from concept to deployment.
|