Appearance
DSPlatform Vue3 前端架构详解
概述
DSPlatform 管理后台采用 Vue 3 + Element Plus + TypeScript + Pinia + Vite 技术栈开发,支持多角色、多平台的管理界面。项目采用模块化架构设计,包含管理员端、店铺端、商户端、消费者端等多个业务模块,提供完整的后台管理功能。
技术栈
核心技术
- Vue 3.4.29: 渐进式 JavaScript 框架
- Element Plus 2.9.8: Vue 3 UI 组件库
- TypeScript 5.4.0: JavaScript 的超集
- Pinia 2.1.7: Vue 3 状态管理库
- Vue Router 4.3.3: Vue 官方路由管理器
- Vite 5.3.1: 构建工具
- Axios 1.7.7: HTTP 客户端
依赖库
- @vueuse/core 11.0.3: Vue 组合式 API 工具库
- @wangeditor/editor-for-vue 5.1.12: 富文本编辑器
- @amap/amap-jsapi-loader 1.0.1: 高德地图 API
- vue-draggable-plus 0.6.0: 拖拽组件
- nprogress 0.2.0: 进度条
- lodash 4.17.21: 工具函数库
- tailwindcss 3.4.17: CSS 框架
项目结构
目录结构
vue-element-admin/
├── src/
│ ├── api/ # API 接口定义
│ │ ├── system/ # 系统相关接口
│ │ ├── tbl-store/ # 店铺相关接口
│ │ └── wechat/ # 微信相关接口
│ ├── assets/ # 静态资源
│ │ ├── icons/ # 图标资源
│ │ │ ├── local/ # 本地图标
│ │ │ └── svg/ # SVG 图标
│ │ └── login/ # 登录页背景图
│ ├── components/ # 公共组件
│ │ ├── attachment/ # 附件组件
│ │ ├── ds-area-picker/ # 地区选择器
│ │ ├── ds-lbs-picker/ # 位置选择器
│ │ ├── editor/ # 富文本编辑器
│ │ ├── icon/ # 图标组件
│ │ ├── icon-picker/ # 图标选择器
│ │ ├── popover-input/ # 弹出输入框
│ │ └── trade/ # 交易相关组件
│ ├── hooks/ # 组合式函数
│ │ ├── useCountdown.ts # 倒计时 Hook
│ │ ├── useEnum.ts # 枚举 Hook
│ │ └── usePagination.ts # 分页 Hook
│ ├── layout/ # 布局组件
│ │ ├── components/ # 布局子组件
│ │ │ ├── lay-header/ # 头部组件
│ │ │ ├── lay-main/ # 主内容区
│ │ │ ├── lay-menu/ # 菜单组件
│ │ │ └── lay-sidebar/ # 侧边栏组件
│ │ └── index.vue # 主布局
│ ├── pages-admin/ # 管理员端页面
│ │ ├── components/ # 管理员端组件
│ │ ├── main/ # 主要功能模块
│ │ │ ├── api/ # API 接口
│ │ │ └── views/ # 页面视图
│ │ └── platform/ # 平台相关页面
│ │ ├── food/ # 餐饮平台
│ │ ├── house/ # 家政平台
│ │ ├── kms/ # 知识管理平台
│ │ └── mall/ # 商城平台
│ ├── pages-consumer/ # 消费者端页面
│ ├── pages-merchant/ # 商户端页面
│ ├── pages-store/ # 店铺端页面
│ │ ├── components/ # 店铺端组件
│ │ ├── main/ # 主要功能模块
│ │ └── platform/ # 平台相关页面
│ ├── router/ # 路由配置
│ │ ├── index.ts # 路由入口
│ │ ├── modules/ # 路由模块
│ │ └── utils.ts # 路由工具函数
│ ├── stores/ # 状态管理
│ │ ├── index.ts # Store 入口
│ │ └── modules/ # Store 模块
│ │ ├── config.ts # 配置 Store
│ │ ├── editable.ts # 可编辑 Store
│ │ ├── enum.ts # 枚举 Store
│ │ ├── multiTags.ts # 多标签 Store
│ │ ├── system.ts # 系统 Store
│ │ ├── themeConfig.ts # 主题配置 Store
│ │ └── userInfo.ts # 用户信息 Store
│ ├── styles/ # 样式文件
│ │ ├── common.scss # 通用样式
│ │ ├── element-plus.scss # Element Plus 样式
│ │ ├── index.scss # 样式入口
│ │ ├── tailwind.css # Tailwind CSS
│ │ └── themes/ # 主题样式
│ ├── utils/ # 工具函数
│ │ ├── auth.ts # 认证工具
│ │ ├── request.ts # 请求工具
│ │ ├── storage.ts # 存储工具
│ │ └── util.ts # 通用工具
│ ├── App.vue # 应用入口
│ └── main.ts # 主入口文件
├── public/ # 公共资源
├── package.json # 项目配置
├── vite.config.ts # Vite 配置
├── tsconfig.json # TypeScript 配置
├── tailwind.config.js # Tailwind 配置
└── postcss.config.js # PostCSS 配置
架构设计
整体架构图
┌─────────────────────────────────────────────────────────────┐
│ 应用层 │
├─────────────┬─────────────┬─────────────┬─────────────────────┤
│ 管理员端 │ 店铺端 │ 商户端 │ 消费者端 │
│ Admin │ Store │ Merchant │ Consumer │
└─────────────┴─────────────┴─────────────┴─────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ 共享层 │
├─────────────┬─────────────┬─────────────┬─────────────────────┤
│ 组件库 │ 工具函数 │ 状态管理 │ 样式系统 │
│ Components │ Utils │ Stores │ Styles │
└─────────────┴─────────────┴─────────────┴─────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ API 层 │
├─────────────┬─────────────┬─────────────┬─────────────────────┤
│ 请求封装 │ 接口定义 │ 错误处理 │ 拦截器 │
│ Request │ API │ Error Handle│ Interceptors │
└─────────────┴─────────────┴─────────────┴─────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ 平台层 │
├─────────────┬─────────────┬─────────────┬─────────────────────┤
│ Web │ 移动端 │ 桌面端 │ 其他平台 │
│ Browser │ Mobile │ Desktop │ Other Platforms │
└─────────────┴─────────────┴─────────────┴─────────────────────┘
模块化设计
1. 管理员端模块 (pages-admin)
typescript
// 管理员端主要功能
interface AdminModule {
// 页面
pages: {
home: '首页'
user: '用户管理'
merchant: '商户管理'
store: '店铺管理'
goods: '商品管理'
order: '订单管理'
system: '系统管理'
setting: '系统设置'
editable: '可编辑页面'
distributor: '分销管理'
pointsGoods: '积分商品'
rider: '骑手管理'
technician: '师傅管理'
trade: '交易管理'
video: '短视频管理'
}
// API
api: {
admin: '管理员相关接口'
user: '用户相关接口'
merchant: '商户相关接口'
store: '店铺相关接口'
goods: '商品相关接口'
order: '订单相关接口'
system: '系统相关接口'
stat: '统计相关接口'
}
// 组件
components: {
merchant: '商户组件'
goods: '商品组件'
order: '订单组件'
user: '用户组件'
}
}
2. 店铺端模块 (pages-store)
typescript
// 店铺端主要功能
interface StoreModule {
pages: {
home: '店铺首页'
goods: '商品管理'
order: '订单管理'
marketing: '营销管理'
technician: '师傅管理'
setting: '店铺设置'
}
api: {
store: '店铺相关接口'
goods: '商品相关接口'
order: '订单相关接口'
marketing: '营销相关接口'
technician: '师傅相关接口'
}
components: {
goods: '商品组件'
order: '订单组件'
marketing: '营销组件'
technician: '师傅组件'
}
}
3. 商户端模块 (pages-merchant)
typescript
// 商户端主要功能
interface MerchantModule {
pages: {
home: '商户首页'
store: '店铺管理'
stat: '数据统计'
setting: '商户设置'
}
api: {
merchant: '商户相关接口'
store: '店铺相关接口'
stat: '统计相关接口'
}
}
4. 消费者端模块 (pages-consumer)
typescript
// 消费者端主要功能
interface ConsumerModule {
pages: {
home: '消费者首页'
profile: '个人中心'
order: '订单管理'
setting: '设置'
}
api: {
consumer: '消费者相关接口'
order: '订单相关接口'
}
}
核心功能实现
1. 请求封装
Request 类
typescript
// src/utils/request.ts
import axios from 'axios'
import type { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse, AxiosRequestConfig, AxiosError } from 'axios'
import { getToken, setToken } from './auth'
import { isUrl } from './util'
import { ElMessage } from 'element-plus'
import storage from './storage'
import useUserInfoStore from '@/stores/modules/userInfo'
import { refreshToken } from '@/api/login'
class Request {
private instance: AxiosInstance
private messageCache: Map<string, { timestamp: number }>
private isRefreshing: boolean = false
private refreshSubscribers: Array<(token: string) => void> = []
constructor() {
this.instance = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json'
}
})
this.messageCache = new Map()
this.setupInterceptors()
}
// 设置拦截器
private setupInterceptors() {
// 请求拦截器
this.instance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 添加访问令牌
const accessToken = getToken('access_token')
if (accessToken) {
config.headers['access-token'] = accessToken
}
// 添加店铺ID
const storeId = storage.get('manage_store_id')
if (storeId) {
config.headers['manage-store-id'] = storeId
}
return config
},
(error: AxiosError) => {
return Promise.reject(error)
}
)
// 响应拦截器
this.instance.interceptors.response.use(
(response: AxiosResponse) => {
const { data } = response
// 业务状态码检查
if (data.code === 401) {
// Token 过期,尝试刷新
return this.handleTokenRefresh(response.config)
}
if (data.code !== 10000) {
// 显示错误消息
if (response.config.showErrorMessage !== false) {
this.showMessage(data.message || '请求失败', 'error')
}
return Promise.reject(new Error(data.message || '请求失败'))
}
// 显示成功消息
if (response.config.showSuccessMessage && data.message) {
this.showMessage(data.message, 'success')
}
return data
},
(error: AxiosError) => {
this.handleError(error)
return Promise.reject(error)
}
)
}
// Token 刷新处理
private async handleTokenRefresh(originalConfig: InternalAxiosRequestConfig) {
if (this.isRefreshing) {
// 等待刷新完成
return new Promise((resolve) => {
this.refreshSubscribers.push((token: string) => {
originalConfig.headers['access-token'] = token
resolve(this.instance.request(originalConfig))
})
})
}
this.isRefreshing = true
try {
const refreshTokenValue = getToken('refresh_token')
const response = await refreshToken({ refresh_token: refreshTokenValue })
// 更新 token
setToken(response.data.access_token, 'access_token')
setToken(response.data.refresh_token, 'refresh_token')
// 通知等待的请求
this.refreshSubscribers.forEach(callback => callback(response.data.access_token))
this.refreshSubscribers = []
// 重新发送原请求
originalConfig.headers['access-token'] = response.data.access_token
return this.instance.request(originalConfig)
} catch (error) {
// 刷新失败,跳转登录
this.redirectToLogin()
throw error
} finally {
this.isRefreshing = false
}
}
// 显示消息
private showMessage(message: string, type: 'success' | 'error' | 'warning') {
const cacheKey = `${type}_${message}`
const now = Date.now()
// 防止重复显示相同消息
if (this.messageCache.has(cacheKey)) {
const lastTime = this.messageCache.get(cacheKey)!.timestamp
if (now - lastTime < 2000) return
}
this.messageCache.set(cacheKey, { timestamp: now })
ElMessage({
message,
type,
duration: 2000
})
}
// 错误处理
private handleError(error: AxiosError) {
if (error.response) {
const { status } = error.response
switch (status) {
case 401:
this.redirectToLogin()
break
case 403:
this.showMessage('没有权限访问', 'error')
break
case 404:
this.showMessage('请求的资源不存在', 'error')
break
case 500:
this.showMessage('服务器内部错误', 'error')
break
default:
this.showMessage('网络错误', 'error')
}
} else if (error.request) {
this.showMessage('网络连接失败', 'error')
} else {
this.showMessage('请求配置错误', 'error')
}
}
// 跳转到登录页
private redirectToLogin() {
const userStore = useUserInfoStore()
userStore.logout()
}
// 发送请求
async request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
return this.instance.request(config)
}
}
export default new Request()
2. 状态管理
Pinia Store 设计
typescript
// src/stores/modules/userInfo.ts
import { defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'
import { transformMenuToRoutes, removeDynamicRoutes, filterRoutesByPermissions } from '@/router/utils'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { getCurrentUserMenus, getCurrentUserInfo } from '@/api/menuRoutes'
import { loginNormal, logout } from '@/api/login'
import storage from '@/utils/storage'
import router from '@/router'
import useMultiTagsStore from '@/stores/modules/multiTags'
import { getSystemType } from '@/utils/util'
interface UserState {
access_token: string
refresh_token: string
userInfo: any
userMenus: any
menuRoutes: RouteRecordRaw[]
}
const useUserInfoStore = defineStore('userInfo', {
state: (): UserState => ({
access_token: getToken('access_token') || '',
refresh_token: getToken('refresh_token') || '',
userInfo: {},
userMenus: [],
menuRoutes: []
}),
getters: {
isLogin: (state) => !!state.access_token,
userRole: (state) => state.userInfo.role || 'user'
},
actions: {
// 登录
login(payload: { username: string; password: string }) {
const { username, password } = payload
return new Promise((resolve, reject) => {
loginNormal({
username: username.trim(),
password
})
.then(res => {
// 存储token和用户信息
setToken(res.data.access_token, 'access_token')
setToken(res.data.refresh_token, 'refresh_token')
if (res.data.userinfo.manage_store_list && res.data.userinfo.manage_store_list.length > 0) {
// 设置默认店铺id
storage.set('manage_store_id', res.data.userinfo.manage_store_list[0].id, 7 * 24 * 60 * 60)
}
this.access_token = res.data.access_token
this.refresh_token = res.data.refresh_token
this.userInfo = res.data.userinfo
resolve(res)
})
.catch(reject)
})
},
// 获取用户信息
async getUserInfo() {
try {
const res = await getCurrentUserInfo()
this.userInfo = res.data
return res.data
} catch (error) {
console.error('获取用户信息失败:', error)
throw error
}
},
// 获取用户菜单
async getUserMenus() {
try {
const res = await getCurrentUserMenus()
this.userMenus = res.data
// 转换菜单为路由
const routes = transformMenuToRoutes(res.data)
this.menuRoutes = routes
// 添加动态路由
addRoutesRecursively(router, routes)
return res.data
} catch (error) {
console.error('获取用户菜单失败:', error)
throw error
}
},
// 登出
async logout() {
try {
const refreshTokenValue = getToken('refresh_token')
const logoutParams = refreshTokenValue ? { refresh_token: refreshTokenValue } : undefined
await logout(logoutParams)
} catch (error) {
console.error('登出失败:', error)
} finally {
// 清理状态
this.access_token = ''
this.refresh_token = ''
this.userInfo = {}
this.userMenus = []
this.menuRoutes = []
// 清理存储
removeToken('access_token')
removeToken('refresh_token')
storage.remove('manage_store_id')
// 清理多标签
const multiTagsStore = useMultiTagsStore()
multiTagsStore.clearTags()
// 移除动态路由
removeDynamicRoutes(router)
// 跳转到登录页
const systemType = getSystemType()
const loginPath = systemType === 'admin' ? '/admin/login' : '/store/login'
router.push(loginPath)
}
}
}
})
export default useUserInfoStore
其他 Store 模块
typescript
// src/stores/modules/config.ts
import { defineStore } from 'pinia'
import { getSysConfigByKey, getConfigsByType } from '@/api/system/sysConfig'
import storage from '@/utils/storage'
interface ConfigState {
configCache: Record<string, string>
typeCache: Record<string, Record<string, string>>
loading: boolean
}
export const useConfigStore = defineStore('config', {
state: (): ConfigState => ({
configCache: {},
typeCache: {},
loading: false
}),
getters: {
getConfigValue: (state) => (key: string): string | null => {
return state.configCache[key] || null
},
getTypeConfigs: (state) => (type: string): Record<string, string> => {
return state.typeCache[type] || {}
}
},
actions: {
// 获取单个配置
async fetchConfig(key: string): Promise<string | null> {
if (this.configCache[key] !== undefined) {
return this.configCache[key]
}
this.loading = true
try {
const res = await getSysConfigByKey(key)
if (res.code === 10000 && res.data) {
const value = String(res.data)
this.configCache[key] = value
this.saveToStorage()
return value
}
return null
} catch (error) {
console.error(`获取配置[${key}]失败:`, error)
return null
} finally {
this.loading = false
}
},
// 保存到本地存储
saveToStorage() {
try {
storage.set('sys_config', this.configCache, 3600)
storage.set('sys_config_types', this.typeCache, 3600)
} catch (e) {
console.error('保存配置到本地存储失败', e)
}
}
}
})
// src/stores/modules/enum.ts
import { defineStore } from 'pinia'
import storage from '@/utils/storage'
import { getEnumData } from '@/api/enum'
interface EnumData {
[key: string]: Record<string | number, string>
}
const useEnumStore = defineStore('enum', {
state: () => ({
enumData: {} as EnumData,
loadingStates: {} as Record<string, boolean>,
isLoaded: false
}),
actions: {
// 获取枚举数据
async getEnum(type: string, forceRefresh = false): Promise<Record<string | number, string>> {
if (!this.isLoaded) {
this.loadFromStorage()
}
if (!forceRefresh && this.enumData[type]) {
return this.enumData[type]
}
if (this.loadingStates[type]) {
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
if (!this.loadingStates[type]) {
clearInterval(checkInterval)
resolve(this.enumData[type] || {})
}
}, 50)
})
}
this.loadingStates[type] = true
try {
const response = await getEnumData({ type: type })
if (response.code === 10000) {
const { data } = response
if (data && data[type]) {
if (Array.isArray(data[type])) {
const enumObj: Record<number, string> = {}
data[type].forEach((value: string, index: number) => {
enumObj[index] = value
})
this.enumData[type] = enumObj
} else {
this.enumData[type] = data[type] as Record<string | number, string>
}
this.saveEnumData()
}
}
return this.enumData[type] || {}
} catch (error) {
console.error(`加载枚举数据[${type}]失败:`, error)
return this.enumData[type] || {}
} finally {
this.loadingStates[type] = false
}
},
// 从本地存储加载
loadFromStorage(): void {
if (this.isLoaded) return
const storedData = storage.get('enumData')
if (storedData && typeof storedData === 'object') {
this.enumData = storedData
}
this.isLoaded = true
},
// 保存到本地存储
saveEnumData(): void {
storage.set('enumData', this.enumData)
}
}
})
export default useEnumStore
3. 路由配置
路由入口
typescript
// src/router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import useUserInfoStore from '@/stores/modules/userInfo'
import useMultiTagsStore from '@/stores/modules/multiTags'
import { findFirstAccessibleRoute, addRoutesRecursively } from './utils'
export const DEFAULT_LAYOUT = () => import('@/layout/index.vue')
// 导入消费者端路由
import { consumerRoutes } from './modules/consumer'
// 根据系统类型获取默认路由
import { getSystemType } from '@/utils/util'
const systemType = getSystemType()
// 根据系统类型获取默认路由
function getDefaultRoutes() {
if (systemType == 'admin') {
return [
{
path: '/:pathMatch(.*)*',
component: () => import('@/pages-admin/main/views/not-found/index.vue'),
meta: { title: '', show: false }
},
{
path: '/admin/login',
name: 'login',
component: () => import('@/pages-admin/main/views/login/index.vue'),
meta: { title: '登录', show: false }
}
]
} else if (systemType == 'store') {
return [
{
path: '/:pathMatch(.*)*',
component: () => import('@/pages-store/main/views/not-found/index.vue'),
meta: { title: '', show: false }
},
{
path: '/store/login',
name: 'login',
component: () => import('@/pages-store/main/views/login/index.vue'),
meta: { title: '登录', show: false }
}
]
} else if (systemType == 'merchant') {
return [
{
path: '/:pathMatch(.*)*',
component: () => import('@/pages-merchant/main/views/not-found/index.vue'),
meta: { title: '', show: false }
},
{
path: '/merchant/login',
name: 'login',
component: () => import('@/pages-merchant/main/views/login/index.vue'),
meta: { title: '登录', show: false }
}
]
} else {
return consumerRoutes
}
}
const router = createRouter({
history: createWebHistory(),
routes: getDefaultRoutes()
})
// 路由守卫
router.beforeEach(async (to, from, next) => {
NProgress.start()
const userStore = useUserInfoStore()
const multiTagsStore = useMultiTagsStore()
// 检查是否需要登录
if (to.meta.requiresAuth !== false) {
if (!userStore.isLogin) {
// 未登录,跳转到登录页
const systemType = getSystemType()
const loginPath = systemType === 'admin' ? '/admin/login' : '/store/login'
next(loginPath)
return
}
// 已登录,检查是否有用户信息
if (!userStore.userInfo.id) {
try {
await userStore.getUserInfo()
} catch (error) {
userStore.logout()
return
}
}
// 检查是否有菜单路由
if (userStore.menuRoutes.length === 0) {
try {
await userStore.getUserMenus()
} catch (error) {
userStore.logout()
return
}
}
}
// 添加标签
if (to.meta.title && to.meta.show !== false) {
multiTagsStore.addTag({
name: to.name as string,
title: to.meta.title as string,
path: to.path,
query: to.query,
params: to.params
})
}
next()
})
router.afterEach(() => {
NProgress.done()
})
export default router
4. 布局系统
主布局组件
vue
<!-- src/layout/index.vue -->
<template>
<div class="layout-container">
<el-container>
<!-- 头部 -->
<el-header class="layout-header">
<lay-header />
</el-header>
<el-container>
<!-- 侧边栏 -->
<el-aside class="layout-sidebar" :width="sidebarWidth">
<lay-sidebar />
</el-aside>
<!-- 主内容区 -->
<el-main class="layout-main">
<lay-main />
</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useThemeConfigStore } from '@/stores/modules/themeConfig'
import LayHeader from './components/lay-header/index.vue'
import LaySidebar from './components/lay-sidebar/index.vue'
import LayMain from './components/lay-main/index.vue'
const themeConfigStore = useThemeConfigStore()
const sidebarWidth = computed(() => {
return themeConfigStore.isCollapse ? '64px' : '200px'
})
</script>
<style lang="scss" scoped>
.layout-container {
height: 100vh;
.layout-header {
height: 60px;
padding: 0;
background-color: #fff;
border-bottom: 1px solid #e6e6e6;
}
.layout-sidebar {
background-color: #001529;
transition: width 0.3s;
}
.layout-main {
padding: 0;
background-color: #f0f2f5;
}
}
</style>
5. 组件设计
公共组件示例
vue
<!-- src/components/attachment/picker-image.vue -->
<template>
<div class="image-picker">
<el-upload
:action="uploadUrl"
:headers="uploadHeaders"
:data="uploadData"
:file-list="fileList"
:on-success="handleSuccess"
:on-error="handleError"
:before-upload="beforeUpload"
:on-remove="handleRemove"
list-type="picture-card"
:limit="limit"
:accept="accept"
>
<el-icon><Plus /></el-icon>
</el-upload>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getToken } from '@/utils/auth'
interface Props {
modelValue?: string[]
limit?: number
accept?: string
}
const props = withDefaults(defineProps<Props>(), {
limit: 9,
accept: 'image/*'
})
const emit = defineEmits<{
'update:modelValue': [value: string[]]
}>()
const fileList = ref<any[]>([])
const uploadUrl = computed(() => {
return import.meta.env.VITE_APP_BASE_URL + '/api/attachment/upload'
})
const uploadHeaders = computed(() => {
const token = getToken('access_token')
return {
'access-token': token || ''
}
})
const uploadData = computed(() => {
return {
type: 'image'
}
})
const handleSuccess = (response: any, file: any) => {
if (response.code === 10000) {
const currentValue = props.modelValue || []
const newValue = [...currentValue, response.data.url]
emit('update:modelValue', newValue)
ElMessage.success('上传成功')
} else {
ElMessage.error(response.message || '上传失败')
}
}
const handleError = (error: any) => {
console.error('上传失败:', error)
ElMessage.error('上传失败')
}
const beforeUpload = (file: File) => {
const isValidType = file.type.startsWith('image/')
if (!isValidType) {
ElMessage.error('只能上传图片文件')
return false
}
const isValidSize = file.size / 1024 / 1024 < 10
if (!isValidSize) {
ElMessage.error('图片大小不能超过 10MB')
return false
}
return true
}
const handleRemove = (file: any) => {
const currentValue = props.modelValue || []
const newValue = currentValue.filter(url => url !== file.response?.data?.url)
emit('update:modelValue', newValue)
}
</script>
<style lang="scss" scoped>
.image-picker {
:deep(.el-upload--picture-card) {
width: 100px;
height: 100px;
line-height: 100px;
}
}
</style>
6. 工具函数
通用工具函数
typescript
// src/utils/util.ts
import { cloneDeep } from 'lodash'
// 通过方法获取类型 方便后期使用多入口
export function getSystemType() {
return import.meta.env.VITE_SYSTEM_TYPE
}
/**
* 处理数字精度,避免浮点数计算误差
* @param num 需要处理的数字
* @param precision 精度位数,默认2位小数
*/
export function toFixed(num: number, precision: number = 2): number {
return Number(Number(num).toFixed(precision))
}
/**
* 判断是否是url
* @param str
* @returns
*/
export function isUrl(str: string): boolean {
return str.indexOf('http://') != -1 || str.indexOf('https://') != -1
}
// 获取服务器ICON地址
export function fetchRemoteIconUrl(path: string): string {
// 服务器ICON地址
const baseUrl = import.meta.env.VITE_APP_BASE_URL + '/static/'
// 参数校验
if (typeof path !== 'string' || path.trim() === '') {
throw new Error('图片路径不能为空')
}
// 确保 path 以斜杠开头
const normalizedPath = path.startsWith('/') ? path : `/${path}`
// 手动拼接完整的 URL
const imageUrl = `${baseUrl}${normalizedPath}`
// 返回完整的 URL
return imageUrl
}
/**
* 处理文件附件URL,根据存储类型补全完整路径
* @param path 文件路径
* @param storageType 存储类型
* @returns 完整的文件URL
*/
export function processAttachmentUrl(path: string, storageType: string = 'local'): string {
if (!path) return ''
// 如果是完整URL,直接返回
if (isUrl(path)) {
return path
}
// 根据存储类型处理
switch (storageType) {
case 'local':
return fetchRemoteIconUrl(path)
case 'oss':
// OSS存储,直接返回路径
return path
case 'qiniu':
// 七牛云存储,直接返回路径
return path
default:
return fetchRemoteIconUrl(path)
}
}
/**
* 格式化时间
* @param timestamp 时间戳
* @param format 格式
* @returns 格式化后的时间字符串
*/
export function formatTime(timestamp: number, format: string = 'YYYY-MM-DD HH:mm:ss'): string {
const date = new Date(timestamp * 1000)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
const second = String(date.getSeconds()).padStart(2, '0')
return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day)
.replace('HH', hour)
.replace('mm', minute)
.replace('ss', second)
}
/**
* 格式化价格
* @param price 价格
* @param decimals 小数位数
* @returns 格式化后的价格字符串
*/
export function formatPrice(price: number, decimals: number = 2): string {
return Number(price).toFixed(decimals)
}
/**
* 深度克隆对象
* @param obj 要克隆的对象
* @returns 克隆后的对象
*/
export function deepClone<T>(obj: T): T {
return cloneDeep(obj)
}
7. 样式系统
全局样式
scss
// src/styles/index.scss
@import './common.scss';
@import './element-plus.scss';
@import './tailwind.css';
// 全局样式重置
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
#app {
height: 100%;
}
// 通用类
.ds-container {
padding: 20px;
}
.ds-card {
background-color: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.ds-button {
background-color: #409eff;
color: white;
border-radius: 4px;
padding: 8px 16px;
border: none;
cursor: pointer;
&:hover {
background-color: #66b1ff;
}
&.disabled {
background-color: #c0c4cc;
color: #999;
cursor: not-allowed;
}
}
.ds-input {
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 8px 12px;
font-size: 14px;
&:focus {
border-color: #409eff;
outline: none;
}
}
Element Plus 样式
scss
// src/styles/element-plus.scss
// Element Plus 主题定制
:root {
--el-color-primary: #409eff;
--el-color-success: #67c23a;
--el-color-warning: #e6a23c;
--el-color-danger: #f56c6c;
--el-color-info: #909399;
}
// 自定义 Element Plus 组件样式
.el-table {
.el-table__header {
th {
background-color: #fafafa;
color: #606266;
font-weight: 500;
}
}
}
.el-form {
.el-form-item__label {
font-weight: 500;
}
}
.el-button {
&.el-button--primary {
background-color: var(--el-color-primary);
border-color: var(--el-color-primary);
}
}
主题样式
scss
// src/styles/themes/light.css
:root {
--bg-color: #ffffff;
--text-color: #303133;
--border-color: #dcdfe6;
--sidebar-bg: #001529;
--sidebar-text: #ffffff;
}
// src/styles/themes/dark.css
:root {
--bg-color: #1a1a1a;
--text-color: #e6e6e6;
--border-color: #4c4d4f;
--sidebar-bg: #001529;
--sidebar-text: #ffffff;
}
多端适配
1. 系统类型配置
环境变量配置
typescript
// 环境变量类型定义
declare namespace NodeJS {
interface ProcessEnv {
VITE_SYSTEM_TYPE: 'admin' | 'store' | 'merchant' | 'consumer'
VITE_APP_BASE_URL: string
VITE_APP_TITLE: string
}
}
系统类型判断
typescript
// src/utils/util.ts
export function getSystemType() {
return import.meta.env.VITE_SYSTEM_TYPE
}
export function isAdminSystem(): boolean {
return getSystemType() === 'admin'
}
export function isStoreSystem(): boolean {
return getSystemType() === 'store'
}
export function isMerchantSystem(): boolean {
return getSystemType() === 'merchant'
}
export function isConsumerSystem(): boolean {
return getSystemType() === 'consumer'
}
2. 响应式设计
屏幕适配
scss
// 响应式断点
$breakpoint-xs: 480px;
$breakpoint-sm: 768px;
$breakpoint-md: 1024px;
$breakpoint-lg: 1200px;
$breakpoint-xl: 1920px;
// 媒体查询
@media screen and (max-width: $breakpoint-sm) {
.layout-sidebar {
display: none;
}
.layout-main {
margin-left: 0;
}
}
@media screen and (min-width: $breakpoint-md) {
.layout-container {
.layout-sidebar {
display: block;
}
}
}
性能优化
1. 代码分割
路由懒加载
typescript
// 路由懒加载配置
export const DEFAULT_LAYOUT = () => import('@/layout/index.vue')
const routes = [
{
path: '/admin',
component: DEFAULT_LAYOUT,
children: [
{
path: 'user',
component: () => import('@/pages-admin/main/views/user/index.vue')
}
]
}
]
2. 组件优化
组件懒加载
vue
<template>
<div>
<component :is="dynamicComponent" v-if="showComponent" />
</div>
</template>
<script setup lang="ts">
import { ref, defineAsyncComponent } from 'vue'
const showComponent = ref(false)
const dynamicComponent = defineAsyncComponent(() =>
import('@/components/heavy-component.vue')
)
</script>
3. 缓存策略
数据缓存
typescript
// src/utils/cache.ts
class CacheManager {
private cache = new Map<string, { data: any; timestamp: number; ttl: number }>()
set(key: string, data: any, ttl: number = 300000) { // 默认5分钟
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl
})
}
get(key: string): any | null {
const item = this.cache.get(key)
if (!item) return null
if (Date.now() - item.timestamp > item.ttl) {
this.cache.delete(key)
return null
}
return item.data
}
clear() {
this.cache.clear()
}
}
export const cacheManager = new CacheManager()
开发规范
1. 代码规范
TypeScript 规范
typescript
// 接口定义
interface UserInfo {
id: number
username: string
nickname: string
avatar: string
mobile: string
email: string
status: number
create_at: number
update_at: number
}
// 类型别名
type ApiResponse<T> = {
code: number
message: string
data: T
}
// 枚举定义
enum OrderStatus {
PENDING = 0,
PAID = 1,
SHIPPED = 2,
DELIVERED = 3,
CANCELLED = 4
}
Vue 组件规范
vue
<template>
<!-- 模板内容 -->
</template>
<script setup lang="ts">
// 1. 导入
import { ref, computed, onMounted } from 'vue'
import type { PropType } from 'vue'
// 2. 接口定义
interface Props {
title: string
data: UserInfo[]
loading?: boolean
}
// 3. Props 定义
const props = withDefaults(defineProps<Props>(), {
loading: false
})
// 4. Emits 定义
const emit = defineEmits<{
change: [value: string]
submit: [data: UserInfo]
}>()
// 5. 响应式数据
const visible = ref(false)
const formData = ref<UserInfo>({})
// 6. 计算属性
const filteredData = computed(() => {
return props.data.filter(item => item.status === 1)
})
// 7. 方法
const handleSubmit = () => {
emit('submit', formData.value)
}
// 8. 生命周期
onMounted(() => {
// 初始化逻辑
})
</script>
<style lang="scss" scoped>
// 样式内容
</style>
2. 文件命名规范
目录结构
src/
├── components/ # 组件目录
│ ├── user-select/ # 组件名使用 kebab-case
│ │ ├── index.vue # 主组件文件
│ │ └── types.ts # 类型定义
│ └── goods-list/
├── pages/ # 页面目录
│ ├── admin/ # 模块名使用 kebab-case
│ │ ├── user/ # 页面名使用 kebab-case
│ │ │ └── index.vue
│ │ └── merchant/
├── api/ # API 目录
│ ├── user.ts # 文件名使用 camelCase
│ └── goods.ts
├── utils/ # 工具目录
│ ├── request.ts # 文件名使用 camelCase
│ └── auth.ts
└── stores/ # 状态管理目录
├── user.ts # 文件名使用 camelCase
└── config.ts
3. 提交规范
Git 提交信息
feat: 添加用户管理功能
fix: 修复订单状态显示问题
docs: 更新 API 文档
style: 调整按钮样式
refactor: 重构用户状态管理
test: 添加单元测试
chore: 更新依赖包
部署配置
1. 环境配置
环境变量
typescript
// src/env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
VITE_SYSTEM_TYPE: 'admin' | 'store' | 'merchant' | 'consumer'
VITE_APP_BASE_URL: string
VITE_APP_TITLE: string
}
}
配置文件
typescript
// src/config/index.ts
interface Config {
systemType: string
apiBaseUrl: string
appTitle: string
version: string
}
const config: Config = {
systemType: import.meta.env.VITE_SYSTEM_TYPE || 'admin',
apiBaseUrl: import.meta.env.VITE_APP_BASE_URL || 'http://localhost:8080',
appTitle: import.meta.env.VITE_APP_TITLE || 'DSPlatform',
version: '3.1.3'
}
export default config
2. 构建配置
Vite 配置
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
import sassImplementation from 'sass'
export default defineConfig({
plugins: [
vue(),
vueJsx(),
createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],
symbolId: '[name]'
})
],
server: {
port: 8080
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
css: {
preprocessorOptions: {
scss: {
implementation: sassImplementation,
api: 'modern-compiler',
sassOptions: {
warnLegacyJsApi: false
}
}
},
devSourcemap: true
},
build: {
target: 'es2015',
outDir: 'dist',
assetsDir: 'static',
sourcemap: false,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
})
相关链接
最后更新:2024-01-20
维护者:DSPlatform技术团队