用DevUI + Vite + Pinia快速搭一个带主题切换的仪表盘(附完整代码)

发布时间:2026/5/26 18:47:00

用DevUI + Vite + Pinia快速搭一个带主题切换的仪表盘(附完整代码) 构建现代化仪表盘DevUI Vite Pinia全栈实践指南仪表盘作为数据可视化的核心载体已成为企业级应用的标准配置。本文将带您从零开始使用DevUI组件库、Vite构建工具和Pinia状态管理打造一个支持主题切换、具备完整交互功能的生产级仪表盘应用。不同于基础教程我们将重点解决三个核心问题如何实现动态主题系统如何优雅管理复杂应用状态如何组织可维护的项目结构1. 环境搭建与项目初始化现代前端开发已进入秒级启动时代Vite的出现彻底改变了项目初始化体验。我们先从工程化配置开始# 创建Vue3项目选择TypeScript模板 npm create vitelatest devui-dashboard -- --template vue-ts # 进入项目目录并安装核心依赖 cd devui-dashboard npm install vue-devui devui-design/icons devui-theme pinia vueuse/core echarts项目结构设计遵循功能模块化原则/src ├── assets # 静态资源 ├── components # 公共组件 │ ├── charts # 图表封装 │ └── layout # 布局组件 ├── composables # 组合式函数 ├── stores # Pinia状态管理 ├── styles # 全局样式 ├── views # 页面视图 │ ├── dashboard # 仪表盘主界面 │ └── settings # 系统设置 └── main.ts # 应用入口在main.ts中完成基础集成import { createApp } from vue import { createPinia } from pinia import App from ./App.vue import DevUI from vue-devui import vue-devui/style.css import devui-design/icons/icomoon/devui-icon.css import { ThemeServiceInit, infinityTheme } from devui-theme // 初始化主题系统 ThemeServiceInit({ infinityTheme }, infinityTheme) const app createApp(App) app.use(createPinia()) app.use(DevUI) app.mount(#app)2. 主题系统深度集成DevUI的主题系统基于CSS变量设计我们可以通过Pinia实现全局主题状态管理。首先创建主题存储// stores/theme.ts import { defineStore } from pinia import { useDark, useToggle } from vueuse/core export const useThemeStore defineStore(theme, { state: () ({ isDark: useDark(), themeColor: #5e7ce0 // 默认主题色 }), actions: { toggleDark() { this.isDark useToggle(this.isDark)() }, setThemeColor(color: string) { this.themeColor color document.documentElement.style.setProperty(--devui-brand, color) } } })在布局组件中实现主题切换UI!-- components/layout/NavBar.vue -- script setup import { useThemeStore } from /stores/theme const theme useThemeStore() /script template d-button-group d-button clicktheme.toggleDark template #icon d-icon :nametheme.isDark ? sun : moon / /template {{ theme.isDark ? 浅色模式 : 深色模式 }} /d-button d-color-picker v-modeltheme.themeColor changetheme.setThemeColor / /d-button-group /template主题样式需要全局适配在styles/theme.css中定义变量映射:root { --bg-color: #f5f5f5; --text-color: #333; --card-bg: #fff; } .dark { --bg-color: #1a1a1a; --text-color: #f0f0f0; --card-bg: #242424; } body { background-color: var(--bg-color); color: var(--text-color); transition: all 0.3s ease; }3. 仪表盘核心功能实现3.1 数据可视化集成使用ECharts封装可复用的图表组件!-- components/charts/LineChart.vue -- script setup langts import { ref, onMounted, watch } from vue import * as echarts from echarts import { useResizeObserver } from vueuse/core const props defineProps{ data: Array{ date: string; value: number } }() const chartEl refHTMLElement() let chart: echarts.ECharts | null null const initChart () { if (!chartEl.value) return chart echarts.init(chartEl.value) updateChart() } const updateChart () { chart?.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: props.data.map(item item.date) }, yAxis: { type: value }, series: [{ data: props.data.map(item item.value), type: line, smooth: true }] }) } onMounted(initChart) watch(() props.data, updateChart) useResizeObserver(chartEl, () chart?.resize()) /script template div refchartEl classh-80 w-full / /template3.2 复杂表单交互设计仪表盘常包含数据过滤表单使用DevUI表单组件构建!-- components/DataFilter.vue -- script setup langts import { reactive } from vue const emit defineEmits([filter]) const form reactive({ dateRange: [new Date(), new Date()], category: , status: }) const categories [ { label: 全部, value: }, { label: 电子产品, value: electronics }, { label: 家居用品, value: home } ] const handleSubmit () { emit(filter, { ...form, startDate: form.dateRange[0].toISOString(), endDate: form.dateRange[1].toISOString() }) } /script template d-form submit.preventhandleSubmit d-row :gutter16 d-col :span8 d-form-item label日期范围 d-date-picker v-modelform.dateRange typedaterange clearable / /d-form-item /d-col d-col :span6 d-form-item label商品类别 d-select v-modelform.category :optionscategories clearable / /d-form-item /d-col d-col :span6 d-form-item label订单状态 d-radio-group v-modelform.status d-radio value全部/d-radio d-radio valuepaid已支付/d-radio d-radio valuepending待处理/d-radio /d-radio-group /d-form-item /d-col d-col :span4 classflex items-end d-button typesubmit bsStyleprimary筛选/d-button /d-col /d-row /d-form /template4. 状态管理与性能优化4.1 Pinia模块化设计将仪表盘数据状态分离到独立store// stores/dashboard.ts import { defineStore } from pinia import { ref, computed } from vue import { fetchDashboardData } from /api export const useDashboardStore defineStore(dashboard, () { const rawData refany(null) const loading ref(false) const error ref(null) const metrics computed(() ({ totalUsers: rawData.value?.userCount || 0, revenue: rawData.value?.revenue || 0, conversion: rawData.value?.conversionRate || 0 })) const fetchData async (params {}) { loading.value true try { rawData.value await fetchDashboardData(params) } catch (err) { error.value err } finally { loading.value false } } return { rawData, loading, error, metrics, fetchData } })4.2 请求防抖与缓存使用组合式函数封装优化策略// composables/useOptimizedFetch.ts import { debounce } from lodash-es import { ref, onMounted } from vue export function useOptimizedFetch(fetchFn: Function, initialData: any) { const data ref(initialData) const isLoading ref(false) const execute debounce(async (...args: any[]) { isLoading.value true try { data.value await fetchFn(...args) } finally { isLoading.value false } }, 300) onMounted(() execute()) return { data, isLoading, execute } }在组件中使用script setup langts import { useDashboardStore } from /stores/dashboard import { useOptimizedFetch } from /composables/useOptimizedFetch const dashboard useDashboardStore() const { data: chartData, execute: refreshChart } useOptimizedFetch( dashboard.fetchChartData, [] ) /script5. 生产环境优化策略5.1 组件自动导入配置通过unplugin-vue-components实现DevUI组件按需导入// vite.config.ts import Components from unplugin-vue-components/vite import { DevUiResolver } from unplugin-vue-components/resolvers export default defineConfig({ plugins: [ Components({ resolvers: [DevUiResolver()], dts: true // 生成类型声明文件 }) ] })5.2 构建输出分析添加打包分析工具npm install rollup-plugin-visualizer -D配置viteimport { visualizer } from rollup-plugin-visualizer export default defineConfig({ plugins: [ visualizer({ open: true, gzipSize: true }) ] })5.3 性能监控集成使用web-vitals进行核心指标监控// src/utils/perfMonitor.ts import { getCLS, getFID, getLCP } from web-vitals const monitor () { if (import.meta.env.PROD) { getCLS(console.log) getFID(console.log) getLCP(console.log) } } export default monitor在main.ts中初始化import monitor from /utils/perfMonitor monitor()6. 项目部署与持续集成6.1 Docker容器化部署创建Dockerfile# 使用官方Node镜像作为构建环境 FROM node:16-alpine as builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # 使用Nginx作为生产服务器 FROM nginx:alpine COPY --frombuilder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD [nginx, -g, daemon off;]配套nginx.conf配置server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } gzip on; gzip_types text/plain text/css application/json application/javascript text/xml; }6.2 CI/CD流水线示例.github/workflows/deploy.yml配置name: Deploy Dashboard on: push: branches: [main] jobs: build-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkoutv2 - name: Setup Node uses: actions/setup-nodev2 with: node-version: 16 - name: Install dependencies run: npm ci - name: Build project run: npm run build - name: Deploy to Server uses: appleboy/ssh-actionmaster with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USER }} key: ${{ secrets.SSH_KEY }} script: | cd /var/www/dashboard git pull origin main docker-compose down docker-compose up -d --build7. 错误处理与监控7.1 全局错误边界创建错误边界组件!-- components/ErrorBoundary.vue -- script setup langts import { ref, onErrorCaptured } from vue const error refany(null) onErrorCaptured((err) { error.value err // 上报错误到监控系统 reportError(err) return false }) const reset () { error.value null } /script template slot v-if!error / d-alert v-else typeerror :closablefalse h3组件渲染错误/h3 pre{{ error.message }}/pre d-button clickreset重试/d-button /d-alert /template7.2 API错误统一处理在axios拦截器中实现// utils/http.ts import axios from axios const http axios.create({ baseURL: import.meta.env.VITE_API_URL }) http.interceptors.response.use( response response, error { const err { code: error.response?.status || 500, message: error.response?.data?.message || error.message } // 根据状态码执行不同策略 switch (err.code) { case 401: router.push(/login) break case 403: showForbiddenMessage() break case 500: triggerErrorMonitoring(err) break } return Promise.reject(err) } )8. 国际化与可访问性8.1 多语言支持集成使用vue-i18n实现// src/i18n.ts import { createI18n } from vue-i18n import en from ./locales/en.json import zh from ./locales/zh.json const i18n createI18n({ legacy: false, locale: navigator.language.split(-)[0] || en, fallbackLocale: en, messages: { en, zh } }) export default i18n在DevUI组件中使用template d-button {{ $t(buttons.submit) }} /d-button /template script setup import { useI18n } from vue-i18n const { t } useI18n() /script8.2 可访问性增强为DevUI组件添加ARIA属性template d-button :aria-labelt(buttons.submit) aria-livepolite {{ t(buttons.submit) }} /d-button /template键盘导航支持// 为自定义组件添加键盘事件 const handleKeyDown (e: KeyboardEvent) { if (e.key Enter) { submitForm() } }9. 测试策略与实施9.1 单元测试配置使用Vitest测试工具// tests/themeStore.spec.ts import { setActivePinia, createPinia } from pinia import { useThemeStore } from /stores/theme describe(Theme Store, () { beforeEach(() { setActivePinia(createPinia()) }) it(should toggle dark mode, () { const theme useThemeStore() expect(theme.isDark).toBe(false) theme.toggleDark() expect(theme.isDark).toBe(true) }) })9.2 组件测试示例测试主题切换按钮// tests/components/NavBar.spec.ts import { mount } from vue/test-utils import NavBar from /components/layout/NavBar.vue test(emits toggle event when button clicked, async () { const wrapper mount(NavBar) await wrapper.find(button).trigger(click) expect(wrapper.emitted()).toHaveProperty(toggle) })9.3 E2E测试配置使用Cypress进行端到端测试// cypress/e2e/dashboard.cy.ts describe(Dashboard, () { it(successfully loads, () { cy.visit(/) cy.contains(h1, 数据概览).should(be.visible) }) it(changes theme color, () { cy.get(.color-picker).click() cy.get(.color-option).first().click() cy.get(body).should(have.css, background-color, rgb(245, 245, 245)) }) })10. 进阶功能扩展10.1 微前端集成方案使用qiankun实现模块化加载// src/micro-apps.ts import { registerMicroApps, start } from qiankun registerMicroApps([ { name: report-module, entry: //localhost:7101, container: #subapp-container, activeRule: /report } ]) start()10.2 Web Workers优化计算将复杂计算移入Worker// src/workers/dataProcessor.ts self.onmessage (e) { const result heavyCalculation(e.data) self.postMessage(result) } function heavyCalculation(data: any) { // 复杂数据处理逻辑 return processedData }在组件中使用const worker new Worker(/workers/dataProcessor.js, { type: module }) worker.onmessage (e) { chartData.value e.data } const processData (rawData: any) { worker.postMessage(rawData) }10.3 实时数据推送使用WebSocket实现实时更新// src/utils/socket.ts import { ref } from vue const socket refWebSocket | null(null) const data refany(null) export function useSocket() { const connect (url: string) { socket.value new WebSocket(url) socket.value.onmessage (e) { data.value JSON.parse(e.data) } } return { data, connect } }在仪表盘组件中集成const { data: realtimeData } useSocket() onMounted(() { connect(wss://api.example.com/realtime) })

相关新闻