Admin Application UI for Quasar 2.7 & Vue 3 & Vite
Features
- Web, iOS & Android support (Capacitor)
- Vue 3 Composition API & TypeScript
- Quasar 2.16 with Vite
- Pinia state management
- Vue I18n with lazy loading
- Firebase Cloud Messaging (FCM)
- Axios interceptors & request management
- Role-based authentication & authorization
- Route guards & permission system
- Vuelidate form validation
- Dark mode support
- Responsive design
Requirements
"node": "^28 || ^26 || ^24 || ^22 || ^20",
"yarn": ">= 1.21.1"
Setup Development Environment
# Install Quasar CLI globally
yarn global add @quasar/cli
# Install dependencies
yarn install
# Copy environment file
cp .env.example .env
# Start development server
yarn dev
# Build for production
yarn build
# Code quality
yarn lint # Check code issues
yarn lint:fix # Auto-fix issues
yarn format # Format code with PrettierMobile App Development
# iOS (requires Xcode)
yarn dev-ios # Development mode with Xcode IDE
yarn build-ios # Build iOS application
# Android (requires Android Studio)
yarn dev-android # Development mode with Android Studio
yarn build-android # Build Android applicationConfigure Realtime Notification (Web SPA) (Firebase FCM)
- Create Firebase Project at Firebase Console
- Go to Project Settings → General
- Add App → Web
- Configure
.envfile
FIREBASE_APIKEY=""
FIREBASE_DOMAIN=""
FIREBASE_PROJECTID=""
FIREBASE_STORAGEBUCKET=""
FIREBASE_SENDERID=""
FIREBASE_APPID=""
FIREBASE_MEASUREMENTID=""Build Configuration:
Framework=None
Build Command=yarn quasar build
Build output directory=dist/spa
Root directory path=adminEnvironment Variables:
API=https://dev.raiden.app
NODE_VERSION=20admin/
├── src/
│ ├── boot/ # App initialization & plugins
│ │ ├── app.ts # Main boot file (i18n, axios, stores)
│ │ └── helper/ # Route guards, interceptors, validation rules
│ ├── components/ # Reusable Vue components
│ │ ├── CustomDialog/
│ │ ├── Language/
│ │ ├── Layout/
│ │ ├── Localization/
│ │ ├── Notification/
│ │ └── SimpleTable/
│ ├── pages/ # Page components
│ │ ├── Admin/ # Admin panel pages
│ │ └── Auth/ # Authentication pages
│ ├── router/ # Route definitions
│ ├── stores/ # Pinia stores
│ │ ├── AppStore.ts # App state & helpers
│ │ └── AuthStore.ts # Authentication state
│ ├── helper/ # Utility functions
│ ├── i18n/ # Translation files
│ └── css/ # Global styles
├── .api/ # Auto-generated API client
├── public/ # Static assets
└── src-capacitor/ # Capacitor native app configuration
Add translations in src/i18n/[locale].ts:
// src/i18n/en-US.ts
export default {
Welcome: 'Welcome',
Login: 'Login',
Dashboard: 'Dashboard',
};Usage in templates:
<template>
<div>{{ $t('Welcome') }}</div>
</template>Usage with prefix (for nested translations):
<template>
<div>{{ $tt('auth', 'Login') }}</div>
</template>Programmatic usage:
import { i18n, tt } from 'boot/app';
// Standard translation
const text = i18n.global.t('Welcome');
// With prefix
const authText = tt('auth', 'Login');Automatic dark mode support. Use in custom components:
<template>
<div :class="{ dark: $q.dark.isActive }">Content</div>
</template>
<script setup>
import { useQuasar } from 'quasar';
const $q = useQuasar();
const isDark = $q.dark.isActive;
</script>Set page title and meta tags:
import { createMetaMixin } from 'quasar';
export default defineComponent({
name: 'LoginPage',
mixins: [
createMetaMixin(function () {
return {
title: this.$t('Login'),
meta: {
description: { name: 'description', content: 'Login to admin panel' },
},
};
}),
],
});Using AppStore methods:
// Success notification
this.$appStore.notifySuccess('Operation completed');
// Error notification
this.$appStore.notifyDanger('Something went wrong');
// Custom notification
import { notifyShow } from 'src/helper/NotifyHelper';
notifyShow('info', 'Custom message');Available in src/helper/AppHelper.ts:
import {
capitalize, // Capitalize first letter
ellipsis, // Truncate text
enumToOptions, // Convert enum to select options
formatJSON, // Format & parse nested JSON
mergeWithDefaults // Deep merge objects
} from 'src/helper/AppHelper';
// Example
const options = enumToOptions(UserType, 'user.types');Available in all components via this:
export default defineComponent({
name: 'MyComponent',
mounted() {
// API client (auto-generated from OpenAPI)
this.$api.admin.UserList();
// Raw Axios client
this.$client.get('/custom-endpoint');
// Check if any request is loading
this.$appStore.isBusy; // boolean
// Permission enum
this.$permission.AdminAccount.LIST;
// Authentication store
this.$authStore.user;
this.$authStore.hasPermission();
// Application store
this.$appStore.notifySuccess();
// Validation rules
this.$rules.required('Field is required');
this.$rules.email('Invalid email');
// Translation with prefix
this.$tt('auth', 'Login');
},
});Import and use in <script setup>:
<script setup lang="ts">
import { api, client, i18n, tt } from 'boot/app';
import { useAuthStore } from 'stores/AuthStore';
import { useAppStore } from 'stores/AppStore';
import { Permission } from '@api/enum/Permission';
const authStore = useAuthStore();
const appStore = useAppStore();
// API request
const loadData = async () => {
const response = await api.admin.UserList();
console.log(response.data);
};
// Check permission
const canEdit = authStore.hasPermission(Permission.AdminAccount.EDIT);
</script>Custom axios request config options:
interface CustomAxiosConfig {
showMessage?: boolean; // Show/hide response notifications (default: true)
uniqId?: string; // Unique request ID for tracking
skipInterceptor?: boolean; // Skip request/response interceptors
}
// Example usage
await this.$client.post('/api/data', payload, {
showMessage: false, // Don't show notification
uniqId: 'my-request',
});Using Vuelidate with built-in rules:
<script setup lang="ts">
import { useVuelidate } from '@vuelidate/core';
import validationRules from 'boot/helper/rules';
const form = reactive({
email: '',
password: '',
});
const rules = {
email: {
required: validationRules.required('Email is required'),
email: validationRules.email('Invalid email'),
},
password: {
required: validationRules.required('Password is required'),
minLength: validationRules.minLength(6, 'Min 6 characters'),
},
};
const v$ = useVuelidate(rules, form);
</script>Check user permissions:
<template>
<!-- Template permission check -->
<q-btn
icon="add"
v-if="$authStore.hasPermission($permission.AdminAccount.CREATE)"
>
Add Account
</q-btn>
</template>
<script setup lang="ts">
import { useAuthStore } from 'stores/AuthStore';
import { Permission } from '@api/enum/Permission';
const authStore = useAuthStore();
// Check single permission
const canCreate = authStore.hasPermission(Permission.AdminAccount.CREATE);
// Get current user
const user = authStore.user;
// Login programmatically
await authStore.loginUsername('user@example.com', 'password');
// Logout
await authStore.logout();
</script>Create route file in src/router/:
// src/router/admin.ts
import { UserType } from '@api/enum/UserType';
import { Permission } from '@api/enum/Permission';
import type { RouteRecordRaw } from 'vue-router';
export default [
{
path: '/',
name: 'admin',
component: () => import('pages/Admin/Layout.vue'),
meta: {
requireAuth: true, // Require authentication
userType: [UserType.ADMIN, UserType.SUPERADMIN], // Restrict by user type
breadcrumb: 'Dashboard', // Breadcrumb label (translated)
},
children: [
{
path: '/accounts',
name: 'accounts',
component: () => import('pages/Admin/Account/Accounts.vue'),
meta: {
breadcrumb: 'Accounts',
permission: [Permission.AdminAccount.LIST], // Require permission
},
},
],
},
] as RouteRecordRaw[];Register routes in src/router/index.ts:
import { route } from 'quasar/wrappers';
import { createRouter, createWebHistory } from 'vue-router';
import adminRoutes from './admin';
import authRoutes from './auth';
const routes = [
...adminRoutes,
...authRoutes,
{ path: '/:catchAll(.*)*', component: () => import('pages/404.vue') },
];
export default route(() => {
return createRouter({
history: createWebHistory(),
routes,
});
});Example: Seller Panel
- Create route file
src/router/seller.ts:
import { UserType } from '@api/enum/UserType';
import type { RouteRecordRaw } from 'vue-router';
export default [
{
path: '/seller',
name: 'seller',
component: () => import('pages/Seller/Layout.vue'),
meta: {
requireAuth: true,
userType: [UserType.SELLER],
breadcrumb: 'Seller Dashboard',
},
children: [
{
path: 'products',
component: () => import('pages/Seller/Products.vue'),
meta: { breadcrumb: 'Products' },
},
],
},
] as RouteRecordRaw[];- Import in
src/router/index.ts:
import sellerRoutes from './seller';
const routes = [
...adminRoutes,
...sellerRoutes,
...authRoutes,
];- Create layout
src/pages/Seller/Layout.vue:
<template>
<q-layout view="lHh LpR lFr">
<q-header elevated>
<q-toolbar>
<q-toolbar-title>
<q-breadcrumbs>
<q-breadcrumbs-el
v-for="(route, index) in breadcrumbs"
:key="index"
:label="$t(route.meta.breadcrumb)"
:to="route.path"
/>
</q-breadcrumbs>
</q-toolbar-title>
</q-toolbar>
</q-header>
<q-page-container>
<router-view v-slot="{ Component }">
<transition
appear
mode="out-in"
enter-active-class="animated fadeIn"
leave-active-class="animated fadeOut"
>
<component :is="Component" />
</transition>
</router-view>
</q-page-container>
</q-layout>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useAuthStore } from 'stores/AuthStore';
import { useAppStore } from 'stores/AppStore';
import { useMeta } from 'quasar';
const route = useRoute();
const authStore = useAuthStore();
const appStore = useAppStore();
const breadcrumbs = computed(() => {
return route.matched.filter((r) => r.meta?.breadcrumb);
});
useMeta(() => ({
titleTemplate: (title) => `${title} - ${appStore.title}`,
}));
onMounted(() => {
authStore.reloadUser();
});
</script>The project includes several ready-to-use components:
- CustomDialog - Custom dialog with advanced options
- SimpleTable - Data table with filtering, pagination, sorting
- SimpleEditor - Rich text editor
- SimpleViewer - File/content viewer
- Language Components - Language selector & input
- Localization Components - Country, city, currency, phone inputs
- Notification - Firebase push notification handler
Example usage:
<template>
<SimpleTable
:columns="columns"
:rows="rows"
:loading="loading"
@request="onRequest"
/>
</template>Never commit .env file. Always use .env.example as template.
Use strict typing:
// Define types for API responses
interface User {
id: number;
name: string;
email: string;
}
const users = ref<User[]>([]);- Use lazy loading for routes
- Implement virtual scrolling for long lists
- Use
v-memofor expensive renders - Optimize images (use WebP, lazy loading)
- Always validate user input
- Use permission checks for sensitive actions
- Sanitize data before rendering
- Keep dependencies updated
Build fails with Firebase error:
- Make sure Firebase env variables are set
- Run
yarn build(notquasar builddirectly)
Hot reload not working:
- Clear
.quasardirectory:rm -rf .quasar - Restart dev server
Mobile app not syncing:
- Run
npx cap syncinsrc-capacitor/ - Rebuild the app
MIT License - see LICENSE file for details.