基于 Vue 3 + TypeScript 的企业级中台系统实践
前言
本文介绍一个基于 Vue 3 + TypeScript 的企业级中台系统开发实践。该系统主要解决配送管理、订单处理、库存管理等核心业务场景,重点突破了传统 ERP 系统在性能、用户体验等方面的技术瓶颈。
系统架构
1. 目录结构
src/
├── api/ # API 接口定义
├── assets/ # 静态资源
├── components/ # 公共组件
├── composables/ # 组合式函数
├── directives/ # 自定义指令
├── hooks/ # 业务钩子
├── layouts/ # 布局组件
├── router/ # 路由配置
├── stores/ # 状态管理
├── styles/ # 全局样式
├── types/ # TypeScript 类型定义
├── utils/ # 工具函数
└── views/ # 页面组件
2. 技术选型
- Vue 3: 采用组合式 API,解决了业务逻辑复用和代码组织问题
- TypeScript: 在开发阶段通过静态类型检查发现潜在问题
- Pinia: 基于组合式 API 的状态管理方案,支持 TypeScript
- Element Plus: 企业级 UI 组件库,提供完整的类型定义
- VXE Table: 高性能表格组件,解决大数据渲染问题
核心功能实现
1. 权限控制系统
1.1 动态路由实现
// router/permission.ts
import { Router } from 'vue-router'
import { useUserStore } from '@/stores/user'
export function setupPermissionGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
// 检查用户是否已登录
if (!userStore.isLoggedIn && to.path !== '/login') {
next('/login')
return
}
// 动态添加路由
if (!userStore.hasLoadedPermissions) {
try {
// 获取用户权限
const permissions = await userStore.loadUserPermissions()
// 生成动态路由
const routes = generateRoutesFromPermissions(permissions)
// 添加路由
routes.forEach((route) => router.addRoute(route))
next({ ...to, replace: true })
return
} catch (error) {
next('/login')
return
}
}
next()
})
}
1.2 权限指令实现
// directives/permission.ts
import { DirectiveBinding } from 'vue'
import { useUserStore } from '@/stores/user'
export const permission = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding
const userStore = useUserStore()
if (value && !userStore.hasPermission(value)) {
el.parentNode?.removeChild(el)
}
}
}
// 使用示例
<el-button v-permission="'system:user:add'">添加用户</el-button>
2. 高级表单设计
2.1 动态表单验证
// composables/useFormValidation.ts
import { ref } from 'vue'
import type { FormInstance } from 'element-plus'
export function useFormValidation() {
const formRef = ref<FormInstance>()
// 自定义验证规则
const validateField = async (rule: any, value: any, callback: any) => {
if (!value) {
callback(new Error('必填项不能为空'))
return
}
try {
// 远程验证
const isValid = await validateWithServer(value)
if (!isValid) {
callback(new Error('验证失败'))
return
}
callback()
} catch (error) {
callback(new Error('验证出错'))
}
}
// 表单验证
const validateForm = async () => {
if (!formRef.value) return false
try {
await formRef.value.validate()
return true
} catch (error) {
return false
}
}
return {
formRef,
validateField,
validateForm,
}
}
2.2 表单联动控制
// composables/useFormControl.ts
import { ref, watch } from 'vue'
export function useFormControl() {
const formData = ref({
type: '',
subType: '',
details: [],
})
// 监听表单字段变化
watch(
() => formData.value.type,
async (newType) => {
if (newType) {
// 重置关联字段
formData.value.subType = ''
formData.value.details = []
// 加载关联数据
const subTypes = await loadSubTypes(newType)
// 更新选项
updateSubTypeOptions(subTypes)
}
}
)
return {
formData,
}
}
3. 数据处理优化
3.1 大数据渲染优化
// composables/useVirtualList.ts
import { ref, computed } from 'vue'
export function useVirtualList(list: any[], itemHeight: number) {
const containerHeight = ref(0)
const scrollTop = ref(0)
// 计算可视区域数据
const visibleData = computed(() => {
const start = Math.floor(scrollTop.value / itemHeight)
const visibleCount = Math.ceil(containerHeight.value / itemHeight)
return list.slice(start, start + visibleCount + 1)
})
// 计算总高度和偏移量
const wrapperStyle = computed(() => ({
height: `${list.length * itemHeight}px`,
transform: `translate3d(0, ${Math.floor(scrollTop.value / itemHeight) * itemHeight}px, 0)`,
}))
return {
visibleData,
wrapperStyle,
containerHeight,
scrollTop,
}
}
3.2 数据缓存策略
// utils/cache.ts
export class DataCache<T> {
private cache: Map<
string,
{
data: T
timestamp: number
expires: number
}
> = new Map()
constructor(private defaultExpires: number = 5 * 60 * 1000) {}
set(key: string, data: T, expires?: number) {
this.cache.set(key, {
data,
timestamp: Date.now(),
expires: expires ?? this.defaultExpires,
})
}
get(key: string): T | null {
const item = this.cache.get(key)
if (!item) return null
if (Date.now() - item.timestamp > item.expires) {
this.cache.delete(key)
return null
}
return item.data
}
}
// 使用示例
const dataCache = new DataCache()
async function fetchData(params: any) {
const cacheKey = JSON.stringify(params)
const cachedData = dataCache.get(cacheKey)
if (cachedData) return cachedData
const data = await api.getData(params)
dataCache.set(cacheKey, data)
return data
}
4. 性能优化实践
4.1 组件懒加载
// router/index.ts
const routes = [
{
path: '/delivery',
component: () => import('@/views/delivery/index.vue'),
children: [
{
path: 'wizard',
component: () => import('@/views/delivery/DeliveryWizard.vue'),
// 预加载
props: (route) => ({
preloadData: () => import('@/api/delivery').then((m) => m.preloadWizardData()),
}),
},
],
},
]
4.2 图片懒加载
// directives/lazyload.ts
export const lazyload = {
mounted(el: HTMLImageElement, binding: DirectiveBinding) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
el.src = binding.value
observer.unobserve(el)
}
})
},
{
rootMargin: '50px'
}
)
observer.observe(el)
}
}
// 使用示例
<img v-lazyload="imageUrl" alt="lazy image">
4.3 防抖与节流
// utils/performance.ts
export function debounce<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => void {
let timer: NodeJS.Timeout | null = null
return function (this: any, ...args: Parameters<T>) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
export function throttle<T extends (...args: any[]) => any>(fn: T, limit: number): (...args: Parameters<T>) => void {
let inThrottle = false
return function (this: any, ...args: Parameters<T>) {
if (!inThrottle) {
fn.apply(this, args)
inThrottle = true
setTimeout(() => {
inThrottle = false
}, limit)
}
}
}
// 使用示例
const handleScroll = throttle(() => {
// 处理滚动事件
}, 100)
5. 错误处理机制
5.1 全局错误处理
// utils/error.ts
import { App } from 'vue'
import { message } from '@/utils/message'
export function setupErrorHandle(app: App) {
app.config.errorHandler = (err, vm, info) => {
console.error('Vue Error:', err)
console.error('Error Info:', info)
// 错误上报
reportError({
error: err,
info,
location: window.location.href,
timestamp: new Date().toISOString(),
})
// 用户提示
message.error('系统错误,请稍后重试')
}
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled Promise Rejection:', event.reason)
// 错误上报
reportError({
error: event.reason,
type: 'unhandledrejection',
location: window.location.href,
timestamp: new Date().toISOString(),
})
event.preventDefault()
})
}
5.2 API 错误处理
// utils/http.ts
import axios from 'axios'
import { message } from '@/utils/message'
const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
})
// 响应拦截器
instance.interceptors.response.use(
(response) => {
const { code, data, msg } = response.data
// 处理业务错误
if (code !== 0) {
message.error(msg || '请求失败')
return Promise.reject(new Error(msg))
}
return data
},
(error) => {
// 处理网络错误
if (error.response) {
switch (error.response.status) {
case 401:
// 处理未授权
handleUnauthorized()
break
case 403:
// 处理权限不足
handleForbidden()
break
case 404:
// 处理资源不存在
handleNotFound()
break
default:
// 处理其他错误
message.error('服务器错误,请稍后重试')
}
} else if (error.request) {
// 处理请求超时
message.error('网络请求超时,请检查网络连接')
} else {
// 处理其他错误
message.error('请求失败,请稍后重试')
}
return Promise.reject(error)
}
)
// utils/http.ts
import axios from 'axios'
import { message } from '@/utils/message'
const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
})
// 响应拦截器
instance.interceptors.response.use(
(response) => {
const { code, data, msg } = response.data
// 处理业务错误
if (code !== 0) {
message.error(msg || '请求失败')
return Promise.reject(new Error(msg))
}
return data
},
(error) => {
// 处理网络错误
if (error.response) {
switch (error.response.status) {
case 401:
// 处理未授权
handleUnauthorized()
break
case 403:
// 处理权限不足
handleForbidden()
break
case 404:
// 处理资源不存在
handleNotFound()
break
default:
// 处理其他错误
message.error('服务器错误,请稍后重试')
}
} else if (error.request) {
// 处理请求超时
message.error('网络请求超时,请检查网络连接')
} else {
// 处理其他错误
message.error('请求失败,请稍后重试')
}
return Promise.reject(error)
}
)
项目难点解决
1. 签名验证机制
实现了基于时间戳的签名验证机制,确保请求的安全性:
// utils/sign.ts
function sortAndSerializeParams(params: Record<string, any>): string {
// 使用 Object.keys 进行字典排序
const sortedKeys = Object.keys(params).sort()
const parts: string[] = []
for (const key of sortedKeys) {
if (params[key] !== undefined) {
parts.push(`${key}=${serializeValue(params[key])}`)
}
}
return parts.join('&')
}
function serializeValue(value: any): string {
if (value === null || value === undefined) {
return ''
}
if (typeof value === 'object') {
if (Array.isArray(value)) {
// 处理数组
return value.map((item) => serializeValue(item)).join(',')
} else {
// 处理对象
return sortAndSerializeParams(value)
}
}
return value.toString()
}
function generateSign(params: Record<string, any>, timestamp: number): string {
// 添加时间戳
const paramsWithTimestamp = {
...params,
timestamp: timestamp.toString(),
}
// 排序并序列化参数
const sortedParams = sortAndSerializeParams(paramsWithTimestamp)
console.log('排序之后的签名参数:', sortedParams)
// 生成 MD5 签名
return md5(sortedParams)
}
2. 大数据量处理
在处理大量数据时采用以下策略:
// composables/useTableData.ts
import { ref, computed } from 'vue'
import type { TableColumn } from 'vxe-table'
export function useTableData(
options = {
pageSize: 20,
maxCachePages: 5,
}
) {
const currentPage = ref(1)
const tableData = ref<any[]>([])
const loading = ref(false)
const total = ref(0)
// 数据缓存
const dataCache = new Map<number, any[]>()
// 加载数据
const loadData = async (page: number) => {
// 检查缓存
if (dataCache.has(page)) {
tableData.value = dataCache.get(page)!
return
}
loading.value = true
try {
const { list, total: totalCount } = await fetchTableData({
pageNo: page,
pageSize: options.pageSize,
})
// 更新数据
tableData.value = list
total.value = totalCount
// 缓存数据
dataCache.set(page, list)
// 清理过期缓存
if (dataCache.size > options.maxCachePages) {
const oldestPage = Math.min(...dataCache.keys())
dataCache.delete(oldestPage)
}
} finally {
loading.value = false
}
}
// 处理页码变化
const handlePageChange = (page: number) => {
currentPage.value = page
loadData(page)
}
// 表格列配置
const columns = computed<TableColumn[]>(() => [
{ type: 'seq', width: 60, title: '序号' },
// ... 其他列配置
])
return {
currentPage,
tableData,
loading,
total,
columns,
handlePageChange,
}
}
3. 文件导出优化
// utils/export.ts
import { message } from '@/utils/message'
export async function handleExport(exportFn: () => Promise<Blob>, fileName: string) {
try {
message.loading('正在导出数据...')
// 调用导出接口
const blob = await exportFn()
// 创建下载链接
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = `${fileName}_${dayjs().format('YYYY-MM-DD')}.xlsx`
// 触发下载
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(link.href)
message.success('导出成功')
} catch (error) {
console.error('导出失败:', error)
message.error('导出失败,请重试')
}
}
// 使用示例
const handleExportData = () => {
handleExport(() => documentApi.exportDeliveryDocuments(exportParams), '配送单据')
}
项目亮点
1.完整的 TypeScript 支持
- 自定义类型声明
- 类型推导和检查
- 接口定义和复用
2.高性能的数据处理
- 虚拟滚动
- 数据缓存
- 懒加载
3.强大的组件系统
- 组件复用
- 插槽设计
- 组件通信
4.完善的工程化实践
- 代码规范
- 自动化测试
- CI/CD 流程
5.安全性考虑
- 请求签名
- XSS 防护
- CSRF 防护
1. VXE Table 高性能表格解决方案
1.1 基础配置与性能优化
// composables/useVxeTable.ts
import { ref, computed } from 'vue'
import type { VxeGridInstance, VxeGridProps } from 'vxe-table'
export function useVxeTable(
options = {
height: 'auto',
stripe: true,
border: true,
align: 'center',
columnConfig: {
resizable: true, // 可调整列宽
isCurrent: true, // 当前列高亮
isHover: true, // 鼠标悬停高亮
},
rowConfig: {
isCurrent: true, // 当前行高亮
isHover: true, // 鼠标悬停高亮
},
}
) {
const gridRef = ref<VxeGridInstance>()
// 表格配置
const gridOptions = computed<VxeGridProps>(() => ({
...options,
// 虚拟滚动配置
scrollX: {
enabled: true, // 启用横向虚拟滚动
gt: 60, // 大于 60 条时启用
},
scrollY: {
enabled: true, // 启用纵向虚拟滚动
gt: 100, // 大于 100 条时启用
scrollToTopOnChange: true, // 数据变化后滚动到顶部
},
// 缓存配置
cacheable: true, // 启用缓存
checkboxConfig: {
reserve: true, // 保留选中状态
},
}))
return {
gridRef,
gridOptions,
}
}
1.2 自定义列渲染
<template>
<vxe-grid ref="gridRef" v-bind="gridOptions">
<!-- 自定义列模板 -->
<template #operation="{ row }">
<vxe-button @click="handleEdit(row)">编辑</vxe-button>
<vxe-button @click="handleDelete(row)">删除</vxe-button>
</template>
<!-- 自定义编辑模板 -->
<template #edit_name="{ row }">
<vxe-input v-model="row.name" @change="handleNameChange(row)" />
</template>
<!-- 自定义格式化 -->
<template #format_date="{ row }">
{{ formatDate(row.date) }}
</template>
</vxe-grid>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useVxeTable } from '@/composables/useVxeTable'
const { gridRef, gridOptions } = useVxeTable()
// 列配置
const columns = [
{ type: 'checkbox', width: 60 },
{ type: 'seq', width: 60, title: '序号' },
{ field: 'name', title: '名称', editRender: { name: 'edit_name' } },
{ field: 'date', title: '日期', slots: { default: 'format_date' } },
{ title: '操作', slots: { default: 'operation' }, fixed: 'right', width: 150 },
]
</script>
1.3 高级功能实现
// 表格工具栏
const toolbarConfig = {
// 工具栏配置
buttons: [
{ code: 'insert', name: '新增' },
{ code: 'delete', name: '删除' },
{ code: 'export', name: '导出' },
],
// 刷新按钮
refresh: true,
// 自定义列显示隐藏
custom: true,
// 表格密度
zoom: true,
}
// 分页配置
const pagerConfig = {
// 分页大小
pageSize: 20,
// 可选分页大小
pageSizes: [20, 50, 100, 200],
// 总数
total: 0,
// 布局
layouts: ['PrevPage', 'JumpNumber', 'NextPage', 'FullJump', 'Sizes', 'Total'],
}
// 编辑配置
const editConfig = {
// 触发方式
trigger: 'click',
// 编辑模式
mode: 'cell',
// 显示状态图标
showStatus: true,
}
// 排序配置
const sortConfig = {
// 触发方式
trigger: 'cell',
// 远程排序
remote: true,
// 默认排序
defaultSort: {
field: 'date',
order: 'desc',
},
}
// 筛选配置
const filterConfig = {
// 远程筛选
remote: true,
// 显示条件
showCondition: true,
// 显示重置按钮
showResetButton: true,
}
1.4 表格事件处理
// 处理表格事件
const handleTableEvents = {
// 选择事件
checkboxChange({ records }) {
console.log('选中记录:', records)
},
// 排序事件
sortChange({ property, order }) {
loadTableData({
...queryParams,
sortField: property,
sortOrder: order,
})
},
// 筛选事件
filterChange({ filters }) {
loadTableData({
...queryParams,
...filters,
})
},
// 分页事件
pageChange({ currentPage, pageSize }) {
loadTableData({
...queryParams,
pageNo: currentPage,
pageSize,
})
},
}
// 表格数据加载
const loadTableData = async (params: any) => {
try {
gridRef.value?.setLoading(true)
const { list, total } = await fetchTableData(params)
tableData.value = list
pagerConfig.total = total
} finally {
gridRef.value?.setLoading(false)
}
}
1.5 表格导出功能
// 导出配置
const exportConfig = {
// 文件类型
type: 'xlsx',
// 文件名
filename: '导出数据',
// 导出模式
mode: 'current',
// 自定义数据
data: null,
// 自定义列
columns: null,
// 样式
style: {
head: {
font: { color: '#000000', bold: true },
alignment: { horizontal: 'center' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { rgb: 'f6f6f6' } },
},
},
}
// 处理导出
const handleExport = async () => {
const { options } = gridRef.value!
// 获取表格数据
const exportData = options.data
// 获取选中列
const exportColumns = options.columns.filter((column) => !column.type && column.visible)
try {
await gridRef.value?.exportData({
...exportConfig,
data: exportData,
columns: exportColumns,
filename: `${exportConfig.filename}_${dayjs().format('YYYY-MM-DD')}`,
})
message.success('导出成功')
} catch (error) {
message.error('导出失败')
}
}
1.6 表格打印功能
// 打印配置
const printConfig = {
// 打印样式
style: `
.vxe-table--print .vxe-table--body td {
color: #666;
border-color: #ddd;
}
`,
// 打印前处理
beforePrintMethod({ content }) {
return content
},
}
// 处理打印
const handlePrint = () => {
const { options } = gridRef.value!
gridRef.value?.print(printConfig)
}
这些功能的实际使用示例:
<template>
<vxe-grid
ref="gridRef"
v-bind="gridOptions"
:columns="columns"
:data="tableData"
:toolbar-config="toolbarConfig"
:pager-config="pagerConfig"
:edit-config="editConfig"
:sort-config="sortConfig"
:filter-config="filterConfig"
@checkbox-change="handleTableEvents.checkboxChange"
@sort-change="handleTableEvents.sortChange"
@filter-change="handleTableEvents.filterChange"
@page-change="handleTableEvents.pageChange">
<!-- 自定义列模板 -->
<template #toolbar_buttons>
<vxe-button @click="handleExport">导出</vxe-button>
<vxe-button @click="handlePrint">打印</vxe-button>
</template>
</vxe-grid>
</template>
通过以上配置和实现,我们的表格组件具备了以下特性:
1.性能优化:
- 虚拟滚动
- 数据缓存
- 选中状态保留
2.功能完善:
- 自定义列渲染
- 行/列编辑
- 排序/筛选
- 导出/打印
3.交互优化:
- 行/列高亮
- 列宽调整
- 列显示切换
- 表格密度调整
4.数据处理:
- 远程排序
- 远程筛选
- 分页加载
- 数据导出
这些功能的组合使得我们的表格组件能够满足各种复杂的业务场景需求,同时保持良好的性能和用户体验。
项目收获
- 深入理解了 Vue 3 的组合式 API 和 TypeScript 的类型系统
- 掌握了企业级前端项目的架构设计
- 积累了大量的性能优化经验
- 提升了代码质量和开发效率
- 学习了现代化的前端工程化实践
未来展望
1.性能优化
- 引入 Web Worker 处理复杂计算
- 使用 Service Worker 实现离线缓存
- 优化首屏加载速度
2.功能增强
- 引入微前端架构
- 支持多主题切换
- 增加数据可视化功能
3.工程化提升
- 完善单元测试覆盖率
- 引入 E2E 测试
- 优化构建流程
总结
通过这个项目,我们不仅实现了业务需求,还在技术层面有了很大的提升。项目中的很多实践和解决方案都具有普遍的参考价值,希望能对大家有所帮助。