Skip to content

Vue Version โ€‹

HaloLight Vue version is built on Vue 3.5 + Vite 7, using Composition API + TypeScript.

Live Preview: https://halolight-vue.h7ml.cn/

GitHub: https://github.com/halolight/halolight-vue

Features โ€‹

  • ๐Ÿ—๏ธ Composition API - Vue 3.5 Composition API for flexible logic reuse
  • โšก Vite 7 + Rolldown - Lightning-fast HMR, Rust-powered build tool
  • ๐ŸŽจ Theme System - 11 skins, light/dark mode, View Transitions
  • ๐Ÿ” Authentication - Complete login/register/password recovery flow
  • ๐Ÿ“Š Dashboard - Data visualization and business management
  • ๐Ÿ›ก๏ธ Permission Control - Fine-grained RBAC permission management
  • ๐Ÿ“‘ Multi-Tab - Tab bar management
  • โŒ˜ Command Palette - Keyboard shortcuts navigation

Tech Stack โ€‹

TechnologyVersionDescription
Vue3.5.xProgressive framework
Vite7.x (Rolldown)Build tool
TypeScript5.xType safety
Vue Router4.xRouting
Pinia2.xState management
TanStack Query5.xServer state
VeeValidate4.xForm validation
Zod3.xData validation
Tailwind CSS4.xAtomic CSS
shadcn-vuelatestUI component library
grid-layout-plus1.xDrag-and-drop layout
ECharts5.xChart visualization
Mock.js1.xData mocking

Core Features โ€‹

  • Configurable Dashboard - 9 widgets, drag-and-drop layout, responsive adaptation
  • Multi-Tab Navigation - Browser-style tabs, context menu, state caching
  • Permission System - RBAC permission control, route guards, permission components
  • Theme System - 11 skins, light/dark mode, View Transitions
  • Multi-Account Switching - Quick account switching, remember login state
  • Command Palette - Keyboard shortcuts (โŒ˜K), global search
  • Real-time Notifications - WebSocket push, notification center

Directory Structure โ€‹

halolight-vue/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ views/               # Page views
โ”‚   โ”‚   โ”œโ”€โ”€ (auth)/         # Auth pages
โ”‚   โ”‚   โ””โ”€โ”€ (dashboard)/    # Dashboard pages
โ”‚   โ”œโ”€โ”€ components/         # Components
โ”‚   โ”‚   โ”œโ”€โ”€ ui/             # Base UI components
โ”‚   โ”‚   โ”œโ”€โ”€ layout/         # Layout components
โ”‚   โ”‚   โ””โ”€โ”€ dashboard/      # Dashboard components
โ”‚   โ”œโ”€โ”€ composables/        # Composable functions
โ”‚   โ”œโ”€โ”€ stores/             # Pinia state management
โ”‚   โ”œโ”€โ”€ lib/                # Utility library
โ”‚   โ”œโ”€โ”€ mocks/              # Mock data
โ”‚   โ””โ”€โ”€ types/              # Type definitions
โ”œโ”€โ”€ public/                 # Static assets
โ”œโ”€โ”€ vite.config.ts
โ””โ”€โ”€ package.json

Quick Start โ€‹

Environment Requirements โ€‹

  • Node.js >= 18.0.0
  • pnpm >= 9.x

Installation โ€‹

bash
git clone https://github.com/halolight/halolight-vue.git
cd halolight-vue
pnpm install

Environment Variables โ€‹

bash
cp .env.example .env.local
env
# .env.local
VITE_API_URL=/api
VITE_USE_MOCK=true
VITE_DEMO_EMAIL=admin@halolight.h7ml.cn
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

Start Development โ€‹

bash
pnpm dev

Visit http://localhost:5173

Build for Production โ€‹

bash
pnpm build
pnpm preview

Demo Account โ€‹

RoleEmailPassword
Adminadmin@halolight.h7ml.cn123456
Useruser@halolight.h7ml.cn123456

Core Functionality โ€‹

State Management (Pinia) โ€‹

ts
// stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useAuthStore = defineStore('auth', () => {
  // State
  const user = ref<User | null>(null)
  const token = ref<string | null>(null)

  // Getters
  const isAuthenticated = computed(() => !!token.value && !!user.value)
  const permissions = computed(() => user.value?.permissions || [])

  // Actions
  async function login(credentials: LoginCredentials) {
    const response = await authService.login(credentials)
    user.value = response.user
    token.value = response.token
  }

  function logout() {
    user.value = null
    token.value = null
  }

  function hasPermission(permission: string): boolean {
    return permissions.value.some(p =>
      p === '*' || p === permission ||
      (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
    )
  }

  return {
    user,
    token,
    isAuthenticated,
    permissions,
    login,
    logout,
    hasPermission,
  }
}, {
  persist: {
    paths: ['token', 'user'],
  },
})

Data Fetching (TanStack Query) โ€‹

ts
// composables/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
import { userService } from '@/services/users'

export function useUsers(params?: Ref<UserQueryParams>) {
  return useQuery({
    queryKey: ['users', params],
    queryFn: () => userService.getList(unref(params)),
  })
}

export function useCreateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: userService.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })
    },
  })
}

Permission Control โ€‹

ts
// composables/usePermission.ts
import { useAuthStore } from '@/stores/auth'

export function usePermission() {
  const authStore = useAuthStore()

  function hasPermission(permission: string): boolean {
    return authStore.hasPermission(permission)
  }

  function hasAnyPermission(permissions: string[]): boolean {
    return permissions.some(p => hasPermission(p))
  }

  function hasAllPermissions(permissions: string[]): boolean {
    return permissions.every(p => hasPermission(p))
  }

  return {
    hasPermission,
    hasAnyPermission,
    hasAllPermissions,
  }
}
ts
// directives/permission.ts
import { useAuthStore } from '@/stores/auth'

export const vPermission = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const authStore = useAuthStore()
    if (!authStore.hasPermission(binding.value)) {
      el.parentNode?.removeChild(el)
    }
  },
}

// Register directive
app.directive('permission', vPermission)
vue
<!-- Use permission directive -->
<button v-permission="'users:delete'">Delete</button>

<!-- Use permission component -->
<PermissionGuard permission="users:delete">
  <DeleteButton />
  <template #fallback>
    <span>No permission</span>
  </template>
</PermissionGuard>

Draggable Dashboard โ€‹

vue
<!-- components/dashboard/DashboardGrid.vue -->
<script setup lang="ts">
import { GridLayout, GridItem } from 'grid-layout-plus'
import { useDashboardStore } from '@/stores/dashboard'

const dashboardStore = useDashboardStore()
const { layout, isEditing } = storeToRefs(dashboardStore)
</script>

<template>
  <GridLayout
    v-model:layout="layout"
    :col-num="12"
    :row-height="80"
    :is-draggable="isEditing"
    :is-resizable="isEditing"
    :margin="[16, 16]"
  >
    <GridItem
      v-for="item in layout"
      :key="item.i"
      :x="item.x"
      :y="item.y"
      :w="item.w"
      :h="item.h"
    >
      <WidgetWrapper :widget="getWidget(item.i)" />
    </GridItem>
  </GridLayout>
</template>

Theme System โ€‹

Skin Presets โ€‹

Supports 11 preset skins, switchable via quick settings panel:

SkinPrimary ColorCSS Variable
DefaultPurple--primary: 51.1% 0.262 276.97
BlueBlue--primary: 54.8% 0.243 264.05
EmeraldEmerald--primary: 64.6% 0.178 142.49
OrangeOrange--primary: 69.7% 0.196 49.27
RoseRose--primary: 63.4% 0.243 357.61
AmberAmber--primary: 79.1% 0.177 77.54
CyanCyan--primary: 74.4% 0.167 197.13
VioletViolet--primary: 57.2% 0.267 285.75
LimeLime--primary: 78.8% 0.184 127.38
PinkPink--primary: 70.9% 0.254 347.58
TealTeal--primary: 67.8% 0.157 181.02

CSS Variables (OKLch) โ€‹

css
/* Example variable definitions */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 100% 0 0;
  --secondary: 97.3% 0.006 285.75;
  --secondary-foreground: 17.9% 0.018 285.75;
  --muted: 97.3% 0.006 285.75;
  --muted-foreground: 49.5% 0.023 285.75;
  --accent: 97.3% 0.006 285.75;
  --accent-foreground: 17.9% 0.018 285.75;
  --destructive: 59.9% 0.24 29.23;
  --destructive-foreground: 98.3% 0.002 285.75;
  --border: 91.9% 0.010 285.75;
  --input: 91.9% 0.010 285.75;
  --ring: 51.1% 0.262 276.97;
  --radius: 0.5rem;
}

Theme Toggle โ€‹

ts
// composables/useTheme.ts
import { ref, computed, watch } from 'vue'

export function useTheme() {
  const theme = ref<'light' | 'dark' | 'system'>('system')
  const skin = ref<SkinPreset>('default')

  const actualTheme = computed(() => {
    if (theme.value === 'system') {
      return window.matchMedia('(prefers-color-scheme: dark)').matches
        ? 'dark'
        : 'light'
    }
    return theme.value
  })

  async function toggleTheme(event?: MouseEvent) {
    const newTheme = actualTheme.value === 'dark' ? 'light' : 'dark'

    // View Transitions API
    if (!document.startViewTransition) {
      theme.value = newTheme
      return
    }

    await document.startViewTransition(() => {
      theme.value = newTheme
    }).ready

    // Circular reveal animation
    if (event) {
      const { clientX, clientY } = event
      const radius = Math.hypot(
        Math.max(clientX, window.innerWidth - clientX),
        Math.max(clientY, window.innerHeight - clientY)
      )

      document.documentElement.animate(
        {
          clipPath: [
            `circle(0px at ${clientX}px ${clientY}px)`,
            `circle(${radius}px at ${clientX}px ${clientY}px)`,
          ],
        },
        {
          duration: 500,
          easing: 'ease-in-out',
          pseudoElement: '::view-transition-new(root)',
        }
      )
    }
  }

  watch([theme, skin], () => {
    document.documentElement.classList.remove('light', 'dark')
    document.documentElement.classList.add(actualTheme.value)
    document.documentElement.setAttribute('data-skin', skin.value)
  }, { immediate: true })

  return { theme, skin, actualTheme, toggleTheme }
}

Page Routes โ€‹

PathPagePermission
/Redirect to /dashboard-
/loginLoginPublic
/registerRegisterPublic
/forgot-passwordForgot passwordPublic
/reset-passwordReset passwordPublic
/dashboardDashboarddashboard:view
/usersUser managementusers:view
/analyticsAnalyticsanalytics:view
/calendarCalendarcalendar:view
/documentsDocumentsdocuments:view
/filesFile storagefiles:view
/messagesMessagesmessages:view
/notificationsNotificationsnotifications:view
/settingsSystem settingssettings:view
/profileUser profilesettings:view

Environment Variables โ€‹

Configuration Example โ€‹

env
# .env.local
VITE_API_URL=/api
VITE_USE_MOCK=true
VITE_DEMO_EMAIL=admin@halolight.h7ml.cn
VITE_DEMO_PASSWORD=123456
VITE_SHOW_DEMO_HINT=false
VITE_APP_TITLE=Admin Pro
VITE_BRAND_NAME=Halolight

Variable Description โ€‹

Variable NameDescriptionDefault Value
VITE_API_URLAPI base path/api
VITE_USE_MOCKWhether to use Mock datatrue
VITE_DEMO_EMAILDemo account emailadmin@halolight.h7ml.cn
VITE_DEMO_PASSWORDDemo account password123456
VITE_SHOW_DEMO_HINTWhether to show demo hintfalse
VITE_APP_TITLEApplication titleAdmin Pro
VITE_BRAND_NAMEBrand nameHalolight

Usage โ€‹

ts
// Use in code
const apiUrl = import.meta.env.VITE_API_URL
const useMock = import.meta.env.VITE_USE_MOCK === 'true'
const appTitle = import.meta.env.VITE_APP_TITLE

Common Commands โ€‹

bash
pnpm dev            # Start development server
pnpm build          # Production build
pnpm preview        # Preview production build
pnpm lint           # Code linting
pnpm lint:fix       # Auto-fix
pnpm type-check     # Type checking
pnpm test           # Run tests
pnpm test:coverage  # Test coverage

Testing โ€‹

bash
pnpm test           # Run tests (watch mode)
pnpm test:run       # Run once
pnpm test:coverage  # Coverage report
pnpm test:ui        # Vitest UI

Test Example โ€‹

ts
// tests/components/Button.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '@/components/ui/Button.vue'

describe('Button', () => {
  it('renders properly', () => {
    const wrapper = mount(Button, {
      props: { variant: 'default' },
      slots: { default: 'Click me' }
    })
    expect(wrapper.text()).toContain('Click me')
  })

  it('emits click event', async () => {
    const wrapper = mount(Button)
    await wrapper.trigger('click')
    expect(wrapper.emitted()).toHaveProperty('click')
  })
})

Configuration โ€‹

Vite Configuration โ€‹

ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 5173,
    open: true,
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-vendor': ['@tanstack/vue-query'],
        },
      },
    },
  },
})

Deployment โ€‹

Deploy with Vercel

Docker โ€‹

bash
docker build -t halolight-vue .
docker run -p 3000:3000 halolight-vue

Other Platforms โ€‹

CI/CD โ€‹

The project is configured with a complete GitHub Actions CI workflow:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Advanced Features โ€‹

ECharts Integration โ€‹

vue
<script setup lang="ts">
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart, BarChart, PieChart } from 'echarts/charts'
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import { useTheme } from '@/composables/useTheme'

use([CanvasRenderer, LineChart, BarChart, PieChart, GridComponent, TooltipComponent, LegendComponent])

const { actualTheme } = useTheme()

const option = computed(() => ({
  backgroundColor: 'transparent',
  textStyle: {
    color: actualTheme.value === 'dark' ? '#e5e5e5' : '#333',
  },
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
    type: 'value'
  },
  series: [{
    data: [820, 932, 901, 934, 1290, 1330, 1320],
    type: 'line'
  }]
}))
</script>

<template>
  <VChart :option="option" autoresize />
</template>

Route Guards โ€‹

ts
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const router = createRouter({
  history: createWebHistory(),
  routes: [...routes]
})

router.beforeEach((to, from, next) => {
  const authStore = useAuthStore()

  // Pages requiring authentication
  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next({ name: 'login', query: { redirect: to.fullPath } })
    return
  }

  // Permission check
  if (to.meta.permission && !authStore.hasPermission(to.meta.permission)) {
    next({ name: '403' })
    return
  }

  next()
})

export default router

Performance Optimization โ€‹

Image Optimization โ€‹

vue
<script setup lang="ts">
const imageSrc = computed(() => {
  const { width } = useWindowSize()
  if (width.value < 768) return '/images/mobile.webp'
  if (width.value < 1024) return '/images/tablet.webp'
  return '/images/desktop.webp'
})
</script>

<template>
  <img
    :src="imageSrc"
    loading="lazy"
    decoding="async"
    alt="Responsive image"
  >
</template>

Lazy Load Components โ€‹

ts
// router/routes.ts
const routes = [
  {
    path: '/dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/users',
    component: () => import('@/views/Users.vue'),
    meta: { requiresAuth: true, permission: 'users:view' }
  },
]

Preloading โ€‹

vue
<script setup lang="ts">
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'

const router = useRouter()

onMounted(() => {
  // Preload commonly used routes
  router.resolve({ name: 'users' })
  router.resolve({ name: 'settings' })
})
</script>

FAQ โ€‹

Q: How to switch themes? โ€‹

A: Use the useTheme composable:

vue
<script setup lang="ts">
import { useTheme } from '@/composables/useTheme'

const { theme, toggleTheme, skin } = useTheme()

// Toggle light/dark theme
function handleToggle(event: MouseEvent) {
  toggleTheme(event)
}

// Change skin
function changeSkin(newSkin: SkinPreset) {
  skin.value = newSkin
}
</script>

<template>
  <button @click="handleToggle">Toggle Theme</button>
  <select v-model="skin">
    <option value="default">Default</option>
    <option value="blue">Blue</option>
    <option value="emerald">Emerald</option>
  </select>
</template>

Q: How to add new permissions? โ€‹

A: Add permission strings in the authentication response:

ts
// types/auth.ts
interface User {
  id: string
  name: string
  email: string
  permissions: string[] // ['users:*', 'posts:view', 'posts:create']
}

// Using wildcards
// 'users:*' - All permissions for user module
// '*' - All permissions
// 'users:view' - Specific permission

Q: How to customize dashboard layout? โ€‹

A: Manage layout through Dashboard Store:

ts
// stores/dashboard.ts
import { defineStore } from 'pinia'

export const useDashboardStore = defineStore('dashboard', () => {
  const layout = ref([
    { i: 'widget-1', x: 0, y: 0, w: 6, h: 4 },
    { i: 'widget-2', x: 6, y: 0, w: 6, h: 4 },
  ])

  function saveLayout(newLayout: Layout[]) {
    layout.value = newLayout
    // Save to server
  }

  return { layout, saveLayout }
})

Comparison with Other Versions โ€‹

FeatureVueNext.jsAngular
SSR/SSGโŒ (Requires Nuxt)โœ…โœ… (Requires Angular Universal)
State ManagementPiniaZustandRxJS/Signals
RoutingVue RouterApp RouterAngular Router
Build ToolViteNext.jsAngular CLI
Learning CurveMediumMediumHigh
EcosystemRichRichEnterprise