1599 lines
38 KiB
Markdown
1599 lines
38 KiB
Markdown
|
|
# 管理后台架构文档
|
|||
|
|
|
|||
|
|
## 版本历史
|
|||
|
|
| 版本 | 日期 | 作者 | 变更说明 |
|
|||
|
|
|------|------|------|----------|
|
|||
|
|
| 1.0 | 2024-01-20 | 前端团队 | 初始版本 |
|
|||
|
|
|
|||
|
|
## 1. 管理后台架构概述
|
|||
|
|
|
|||
|
|
### 1.1 项目背景
|
|||
|
|
管理后台是养殖管理平台的Web端管理系统,主要面向系统管理员、运营人员和客服人员,提供用户管理、数据统计、系统配置等管理功能。
|
|||
|
|
|
|||
|
|
### 1.2 架构目标
|
|||
|
|
- **易用性**:直观的操作界面和良好的用户体验
|
|||
|
|
- **功能完整**:覆盖所有业务管理需求
|
|||
|
|
- **性能优化**:快速的页面加载和响应
|
|||
|
|
- **可维护性**:清晰的代码结构和组件化开发
|
|||
|
|
- **扩展性**:支持功能模块的快速扩展
|
|||
|
|
- **安全性**:完善的权限控制和安全防护
|
|||
|
|
|
|||
|
|
### 1.3 技术栈
|
|||
|
|
- **前端框架**:Vue.js 3.x + Composition API
|
|||
|
|
- **开发语言**:TypeScript 5.x
|
|||
|
|
- **UI框架**:Element Plus 2.x
|
|||
|
|
- **状态管理**:Pinia 2.x
|
|||
|
|
- **路由管理**:Vue Router 4.x
|
|||
|
|
- **构建工具**:Vite 4.x
|
|||
|
|
- **HTTP客户端**:Axios
|
|||
|
|
- **图表库**:ECharts 5.x
|
|||
|
|
- **代码规范**:ESLint + Prettier
|
|||
|
|
- **CSS预处理**:Sass/SCSS
|
|||
|
|
|
|||
|
|
## 2. 系统架构设计
|
|||
|
|
|
|||
|
|
### 2.1 整体架构
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 表现层 (View) │
|
|||
|
|
│ Pages + Components │
|
|||
|
|
├─────────────────────────────────────────────────────────────┤
|
|||
|
|
│ 状态层 (State) │
|
|||
|
|
│ Pinia Stores │
|
|||
|
|
├─────────────────────────────────────────────────────────────┤
|
|||
|
|
│ 服务层 (Service) │
|
|||
|
|
│ API + Utils + Plugins │
|
|||
|
|
├─────────────────────────────────────────────────────────────┤
|
|||
|
|
│ 数据层 (Data) │
|
|||
|
|
│ HTTP + WebSocket + Storage │
|
|||
|
|
└─────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.2 目录结构
|
|||
|
|
```
|
|||
|
|
admin-system/
|
|||
|
|
├── public/ # 静态资源
|
|||
|
|
│ ├── favicon.ico
|
|||
|
|
│ └── index.html
|
|||
|
|
├── src/ # 源代码
|
|||
|
|
│ ├── api/ # API接口
|
|||
|
|
│ │ ├── auth.ts # 认证接口
|
|||
|
|
│ │ ├── user.ts # 用户接口
|
|||
|
|
│ │ ├── farm.ts # 养殖接口
|
|||
|
|
│ │ └── trade.ts # 交易接口
|
|||
|
|
│ ├── assets/ # 静态资源
|
|||
|
|
│ │ ├── images/ # 图片资源
|
|||
|
|
│ │ ├── icons/ # 图标资源
|
|||
|
|
│ │ └── styles/ # 样式文件
|
|||
|
|
│ ├── components/ # 组件
|
|||
|
|
│ │ ├── common/ # 通用组件
|
|||
|
|
│ │ ├── business/ # 业务组件
|
|||
|
|
│ │ └── layout/ # 布局组件
|
|||
|
|
│ ├── composables/ # 组合式函数
|
|||
|
|
│ │ ├── useAuth.ts # 认证逻辑
|
|||
|
|
│ │ ├── useTable.ts # 表格逻辑
|
|||
|
|
│ │ └── useChart.ts # 图表逻辑
|
|||
|
|
│ ├── directives/ # 自定义指令
|
|||
|
|
│ │ ├── permission.ts # 权限指令
|
|||
|
|
│ │ └── loading.ts # 加载指令
|
|||
|
|
│ ├── layouts/ # 布局
|
|||
|
|
│ │ ├── DefaultLayout.vue # 默认布局
|
|||
|
|
│ │ └── AuthLayout.vue # 认证布局
|
|||
|
|
│ ├── pages/ # 页面
|
|||
|
|
│ │ ├── dashboard/ # 仪表板
|
|||
|
|
│ │ ├── user/ # 用户管理
|
|||
|
|
│ │ ├── farm/ # 养殖管理
|
|||
|
|
│ │ ├── trade/ # 交易管理
|
|||
|
|
│ │ └── system/ # 系统管理
|
|||
|
|
│ ├── plugins/ # 插件
|
|||
|
|
│ │ ├── element-plus.ts # Element Plus
|
|||
|
|
│ │ └── echarts.ts # ECharts
|
|||
|
|
│ ├── router/ # 路由
|
|||
|
|
│ │ ├── index.ts # 路由配置
|
|||
|
|
│ │ └── guards.ts # 路由守卫
|
|||
|
|
│ ├── stores/ # 状态管理
|
|||
|
|
│ │ ├── auth.ts # 认证状态
|
|||
|
|
│ │ ├── user.ts # 用户状态
|
|||
|
|
│ │ └── app.ts # 应用状态
|
|||
|
|
│ ├── types/ # 类型定义
|
|||
|
|
│ │ ├── api.ts # API类型
|
|||
|
|
│ │ ├── user.ts # 用户类型
|
|||
|
|
│ │ └── common.ts # 通用类型
|
|||
|
|
│ ├── utils/ # 工具函数
|
|||
|
|
│ │ ├── request.ts # 请求封装
|
|||
|
|
│ │ ├── auth.ts # 认证工具
|
|||
|
|
│ │ ├── validator.ts # 验证工具
|
|||
|
|
│ │ └── formatter.ts # 格式化工具
|
|||
|
|
│ ├── App.vue # 根组件
|
|||
|
|
│ └── main.ts # 应用入口
|
|||
|
|
├── .env # 环境变量
|
|||
|
|
├── .env.development # 开发环境变量
|
|||
|
|
├── .env.production # 生产环境变量
|
|||
|
|
├── vite.config.ts # Vite配置
|
|||
|
|
├── tsconfig.json # TypeScript配置
|
|||
|
|
├── package.json # 项目配置
|
|||
|
|
└── README.md # 项目说明
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 3. 核心模块设计
|
|||
|
|
|
|||
|
|
### 3.1 认证授权模块
|
|||
|
|
**功能**: 用户登录、权限验证、角色管理
|
|||
|
|
|
|||
|
|
**核心组件**:
|
|||
|
|
- **登录页面**: 用户名密码登录
|
|||
|
|
- **权限控制**: 基于RBAC的权限控制
|
|||
|
|
- **角色管理**: 角色创建、编辑、权限分配
|
|||
|
|
|
|||
|
|
**状态管理**:
|
|||
|
|
```typescript
|
|||
|
|
// 认证状态管理
|
|||
|
|
export const useAuthStore = defineStore('auth', () => {
|
|||
|
|
const token = ref<string>('')
|
|||
|
|
const userInfo = ref<UserInfo | null>(null)
|
|||
|
|
const permissions = ref<string[]>([])
|
|||
|
|
|
|||
|
|
// 登录
|
|||
|
|
const login = async (credentials: LoginCredentials) => {
|
|||
|
|
const response = await authApi.login(credentials)
|
|||
|
|
if (response.success) {
|
|||
|
|
token.value = response.data.token
|
|||
|
|
userInfo.value = response.data.user
|
|||
|
|
permissions.value = response.data.permissions
|
|||
|
|
|
|||
|
|
// 保存到本地存储
|
|||
|
|
localStorage.setItem('token', token.value)
|
|||
|
|
localStorage.setItem('userInfo', JSON.stringify(userInfo.value))
|
|||
|
|
}
|
|||
|
|
return response
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 登出
|
|||
|
|
const logout = async () => {
|
|||
|
|
await authApi.logout()
|
|||
|
|
token.value = ''
|
|||
|
|
userInfo.value = null
|
|||
|
|
permissions.value = []
|
|||
|
|
|
|||
|
|
// 清除本地存储
|
|||
|
|
localStorage.removeItem('token')
|
|||
|
|
localStorage.removeItem('userInfo')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查权限
|
|||
|
|
const hasPermission = (permission: string): boolean => {
|
|||
|
|
return permissions.value.includes(permission)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
token,
|
|||
|
|
userInfo,
|
|||
|
|
permissions,
|
|||
|
|
login,
|
|||
|
|
logout,
|
|||
|
|
hasPermission
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 用户管理模块
|
|||
|
|
**功能**: 用户列表、用户详情、用户操作
|
|||
|
|
|
|||
|
|
**核心页面**:
|
|||
|
|
- **用户列表**: 分页展示用户信息,支持搜索和筛选
|
|||
|
|
- **用户详情**: 查看和编辑用户详细信息
|
|||
|
|
- **用户操作**: 启用/禁用用户、重置密码等
|
|||
|
|
|
|||
|
|
**表格组件**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<div class="user-management">
|
|||
|
|
<!-- 搜索栏 -->
|
|||
|
|
<el-form :model="searchForm" inline>
|
|||
|
|
<el-form-item label="用户名">
|
|||
|
|
<el-input v-model="searchForm.username" placeholder="请输入用户名" />
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item label="状态">
|
|||
|
|
<el-select v-model="searchForm.status" placeholder="请选择状态">
|
|||
|
|
<el-option label="全部" value="" />
|
|||
|
|
<el-option label="正常" value="1" />
|
|||
|
|
<el-option label="禁用" value="0" />
|
|||
|
|
</el-select>
|
|||
|
|
</el-form-item>
|
|||
|
|
<el-form-item>
|
|||
|
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
|||
|
|
<el-button @click="handleReset">重置</el-button>
|
|||
|
|
</el-form-item>
|
|||
|
|
</el-form>
|
|||
|
|
|
|||
|
|
<!-- 数据表格 -->
|
|||
|
|
<el-table :data="tableData" v-loading="loading">
|
|||
|
|
<el-table-column prop="id" label="ID" width="80" />
|
|||
|
|
<el-table-column prop="username" label="用户名" />
|
|||
|
|
<el-table-column prop="phone" label="手机号" />
|
|||
|
|
<el-table-column prop="email" label="邮箱" />
|
|||
|
|
<el-table-column prop="status" label="状态">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
|||
|
|
{{ row.status === 1 ? '正常' : '禁用' }}
|
|||
|
|
</el-tag>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column prop="createdAt" label="创建时间" />
|
|||
|
|
<el-table-column label="操作" width="200">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
<el-button size="small" @click="handleView(row)">查看</el-button>
|
|||
|
|
<el-button size="small" type="primary" @click="handleEdit(row)">编辑</el-button>
|
|||
|
|
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
</el-table>
|
|||
|
|
|
|||
|
|
<!-- 分页 -->
|
|||
|
|
<el-pagination
|
|||
|
|
v-model:current-page="pagination.page"
|
|||
|
|
v-model:page-size="pagination.limit"
|
|||
|
|
:total="pagination.total"
|
|||
|
|
:page-sizes="[10, 20, 50, 100]"
|
|||
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|||
|
|
@size-change="handleSizeChange"
|
|||
|
|
@current-change="handleCurrentChange"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, reactive, onMounted } from 'vue'
|
|||
|
|
import { useUserStore } from '@/stores/user'
|
|||
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|||
|
|
|
|||
|
|
const userStore = useUserStore()
|
|||
|
|
|
|||
|
|
const loading = ref(false)
|
|||
|
|
const tableData = ref<User[]>([])
|
|||
|
|
|
|||
|
|
const searchForm = reactive({
|
|||
|
|
username: '',
|
|||
|
|
status: ''
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const pagination = reactive({
|
|||
|
|
page: 1,
|
|||
|
|
limit: 20,
|
|||
|
|
total: 0
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 加载用户列表
|
|||
|
|
const loadUsers = async () => {
|
|||
|
|
loading.value = true
|
|||
|
|
try {
|
|||
|
|
const response = await userStore.getUsers({
|
|||
|
|
...searchForm,
|
|||
|
|
page: pagination.page,
|
|||
|
|
limit: pagination.limit
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if (response.success) {
|
|||
|
|
tableData.value = response.data.items
|
|||
|
|
pagination.total = response.data.total
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
ElMessage.error('加载用户列表失败')
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 搜索
|
|||
|
|
const handleSearch = () => {
|
|||
|
|
pagination.page = 1
|
|||
|
|
loadUsers()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 重置
|
|||
|
|
const handleReset = () => {
|
|||
|
|
Object.assign(searchForm, {
|
|||
|
|
username: '',
|
|||
|
|
status: ''
|
|||
|
|
})
|
|||
|
|
handleSearch()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查看用户
|
|||
|
|
const handleView = (user: User) => {
|
|||
|
|
// 跳转到用户详情页
|
|||
|
|
router.push(`/user/detail/${user.id}`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 编辑用户
|
|||
|
|
const handleEdit = (user: User) => {
|
|||
|
|
// 跳转到用户编辑页
|
|||
|
|
router.push(`/user/edit/${user.id}`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 删除用户
|
|||
|
|
const handleDelete = async (user: User) => {
|
|||
|
|
try {
|
|||
|
|
await ElMessageBox.confirm('确定要删除该用户吗?', '提示', {
|
|||
|
|
confirmButtonText: '确定',
|
|||
|
|
cancelButtonText: '取消',
|
|||
|
|
type: 'warning'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const response = await userStore.deleteUser(user.id)
|
|||
|
|
if (response.success) {
|
|||
|
|
ElMessage.success('删除成功')
|
|||
|
|
loadUsers()
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
// 用户取消删除
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
loadUsers()
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.3 数据统计模块
|
|||
|
|
**功能**: 数据可视化、报表生成、趋势分析
|
|||
|
|
|
|||
|
|
**核心组件**:
|
|||
|
|
- **仪表板**: 关键指标展示
|
|||
|
|
- **图表组件**: 各类数据图表
|
|||
|
|
- **报表页面**: 详细数据报表
|
|||
|
|
|
|||
|
|
**图表组件**:
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<div class="chart-container">
|
|||
|
|
<div ref="chartRef" :style="{ width: '100%', height: height + 'px' }"></div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
|||
|
|
import * as echarts from 'echarts'
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
data: any[]
|
|||
|
|
type: 'line' | 'bar' | 'pie'
|
|||
|
|
height?: number
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const props = withDefaults(defineProps<Props>(), {
|
|||
|
|
height: 400
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const chartRef = ref<HTMLDivElement>()
|
|||
|
|
let chartInstance: echarts.ECharts | null = null
|
|||
|
|
|
|||
|
|
// 初始化图表
|
|||
|
|
const initChart = () => {
|
|||
|
|
if (!chartRef.value) return
|
|||
|
|
|
|||
|
|
chartInstance = echarts.init(chartRef.value)
|
|||
|
|
updateChart()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新图表
|
|||
|
|
const updateChart = () => {
|
|||
|
|
if (!chartInstance) return
|
|||
|
|
|
|||
|
|
const option = getChartOption()
|
|||
|
|
chartInstance.setOption(option)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取图表配置
|
|||
|
|
const getChartOption = () => {
|
|||
|
|
switch (props.type) {
|
|||
|
|
case 'line':
|
|||
|
|
return getLineChartOption()
|
|||
|
|
case 'bar':
|
|||
|
|
return getBarChartOption()
|
|||
|
|
case 'pie':
|
|||
|
|
return getPieChartOption()
|
|||
|
|
default:
|
|||
|
|
return {}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 折线图配置
|
|||
|
|
const getLineChartOption = () => {
|
|||
|
|
return {
|
|||
|
|
title: {
|
|||
|
|
text: '趋势图'
|
|||
|
|
},
|
|||
|
|
tooltip: {
|
|||
|
|
trigger: 'axis'
|
|||
|
|
},
|
|||
|
|
xAxis: {
|
|||
|
|
type: 'category',
|
|||
|
|
data: props.data.map(item => item.name)
|
|||
|
|
},
|
|||
|
|
yAxis: {
|
|||
|
|
type: 'value'
|
|||
|
|
},
|
|||
|
|
series: [{
|
|||
|
|
data: props.data.map(item => item.value),
|
|||
|
|
type: 'line',
|
|||
|
|
smooth: true
|
|||
|
|
}]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 柱状图配置
|
|||
|
|
const getBarChartOption = () => {
|
|||
|
|
return {
|
|||
|
|
title: {
|
|||
|
|
text: '柱状图'
|
|||
|
|
},
|
|||
|
|
tooltip: {
|
|||
|
|
trigger: 'axis'
|
|||
|
|
},
|
|||
|
|
xAxis: {
|
|||
|
|
type: 'category',
|
|||
|
|
data: props.data.map(item => item.name)
|
|||
|
|
},
|
|||
|
|
yAxis: {
|
|||
|
|
type: 'value'
|
|||
|
|
},
|
|||
|
|
series: [{
|
|||
|
|
data: props.data.map(item => item.value),
|
|||
|
|
type: 'bar'
|
|||
|
|
}]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 饼图配置
|
|||
|
|
const getPieChartOption = () => {
|
|||
|
|
return {
|
|||
|
|
title: {
|
|||
|
|
text: '饼图'
|
|||
|
|
},
|
|||
|
|
tooltip: {
|
|||
|
|
trigger: 'item'
|
|||
|
|
},
|
|||
|
|
series: [{
|
|||
|
|
type: 'pie',
|
|||
|
|
radius: '50%',
|
|||
|
|
data: props.data,
|
|||
|
|
emphasis: {
|
|||
|
|
itemStyle: {
|
|||
|
|
shadowBlur: 10,
|
|||
|
|
shadowOffsetX: 0,
|
|||
|
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 监听数据变化
|
|||
|
|
watch(() => props.data, () => {
|
|||
|
|
updateChart()
|
|||
|
|
}, { deep: true })
|
|||
|
|
|
|||
|
|
// 窗口大小变化时重新调整图表
|
|||
|
|
const handleResize = () => {
|
|||
|
|
if (chartInstance) {
|
|||
|
|
chartInstance.resize()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
initChart()
|
|||
|
|
window.addEventListener('resize', handleResize)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onUnmounted(() => {
|
|||
|
|
if (chartInstance) {
|
|||
|
|
chartInstance.dispose()
|
|||
|
|
}
|
|||
|
|
window.removeEventListener('resize', handleResize)
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.4 系统配置模块
|
|||
|
|
**功能**: 系统参数配置、菜单管理、字典管理
|
|||
|
|
|
|||
|
|
**核心页面**:
|
|||
|
|
- **系统参数**: 系统基础配置
|
|||
|
|
- **菜单管理**: 动态菜单配置
|
|||
|
|
- **字典管理**: 数据字典维护
|
|||
|
|
|
|||
|
|
## 4. 路由设计
|
|||
|
|
|
|||
|
|
### 4.1 路由配置
|
|||
|
|
```typescript
|
|||
|
|
// 路由配置
|
|||
|
|
import { createRouter, createWebHistory } from 'vue-router'
|
|||
|
|
import type { RouteRecordRaw } from 'vue-router'
|
|||
|
|
|
|||
|
|
const routes: RouteRecordRaw[] = [
|
|||
|
|
{
|
|||
|
|
path: '/login',
|
|||
|
|
name: 'Login',
|
|||
|
|
component: () => import('@/pages/auth/Login.vue'),
|
|||
|
|
meta: {
|
|||
|
|
title: '登录',
|
|||
|
|
requiresAuth: false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
path: '/',
|
|||
|
|
component: () => import('@/layouts/DefaultLayout.vue'),
|
|||
|
|
redirect: '/dashboard',
|
|||
|
|
children: [
|
|||
|
|
{
|
|||
|
|
path: 'dashboard',
|
|||
|
|
name: 'Dashboard',
|
|||
|
|
component: () => import('@/pages/dashboard/Index.vue'),
|
|||
|
|
meta: {
|
|||
|
|
title: '仪表板',
|
|||
|
|
icon: 'dashboard'
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
path: 'user',
|
|||
|
|
name: 'UserManagement',
|
|||
|
|
redirect: '/user/list',
|
|||
|
|
meta: {
|
|||
|
|
title: '用户管理',
|
|||
|
|
icon: 'user'
|
|||
|
|
},
|
|||
|
|
children: [
|
|||
|
|
{
|
|||
|
|
path: 'list',
|
|||
|
|
name: 'UserList',
|
|||
|
|
component: () => import('@/pages/user/List.vue'),
|
|||
|
|
meta: {
|
|||
|
|
title: '用户列表'
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
path: 'detail/:id',
|
|||
|
|
name: 'UserDetail',
|
|||
|
|
component: () => import('@/pages/user/Detail.vue'),
|
|||
|
|
meta: {
|
|||
|
|
title: '用户详情',
|
|||
|
|
hidden: true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const router = createRouter({
|
|||
|
|
history: createWebHistory(),
|
|||
|
|
routes
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export default router
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 路由守卫
|
|||
|
|
```typescript
|
|||
|
|
// 路由守卫
|
|||
|
|
import { useAuthStore } from '@/stores/auth'
|
|||
|
|
|
|||
|
|
router.beforeEach(async (to, from, next) => {
|
|||
|
|
const authStore = useAuthStore()
|
|||
|
|
|
|||
|
|
// 设置页面标题
|
|||
|
|
if (to.meta.title) {
|
|||
|
|
document.title = `${to.meta.title} - 养殖管理平台`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否需要认证
|
|||
|
|
if (to.meta.requiresAuth !== false) {
|
|||
|
|
if (!authStore.token) {
|
|||
|
|
next('/login')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查权限
|
|||
|
|
if (to.meta.permission && !authStore.hasPermission(to.meta.permission)) {
|
|||
|
|
ElMessage.error('权限不足')
|
|||
|
|
next('/403')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
next()
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 5. 状态管理
|
|||
|
|
|
|||
|
|
### 5.1 应用状态
|
|||
|
|
```typescript
|
|||
|
|
// 应用状态管理
|
|||
|
|
export const useAppStore = defineStore('app', () => {
|
|||
|
|
const sidebar = ref({
|
|||
|
|
opened: true,
|
|||
|
|
withoutAnimation: false
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const device = ref('desktop')
|
|||
|
|
const size = ref('default')
|
|||
|
|
|
|||
|
|
// 切换侧边栏
|
|||
|
|
const toggleSidebar = () => {
|
|||
|
|
sidebar.value.opened = !sidebar.value.opened
|
|||
|
|
sidebar.value.withoutAnimation = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 关闭侧边栏
|
|||
|
|
const closeSidebar = (withoutAnimation: boolean) => {
|
|||
|
|
sidebar.value.opened = false
|
|||
|
|
sidebar.value.withoutAnimation = withoutAnimation
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置设备类型
|
|||
|
|
const setDevice = (deviceType: string) => {
|
|||
|
|
device.value = deviceType
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置组件大小
|
|||
|
|
const setSize = (sizeType: string) => {
|
|||
|
|
size.value = sizeType
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
sidebar,
|
|||
|
|
device,
|
|||
|
|
size,
|
|||
|
|
toggleSidebar,
|
|||
|
|
closeSidebar,
|
|||
|
|
setDevice,
|
|||
|
|
setSize
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 用户状态
|
|||
|
|
```typescript
|
|||
|
|
// 用户状态管理
|
|||
|
|
export const useUserStore = defineStore('user', () => {
|
|||
|
|
const users = ref<User[]>([])
|
|||
|
|
const currentUser = ref<User | null>(null)
|
|||
|
|
|
|||
|
|
// 获取用户列表
|
|||
|
|
const getUsers = async (params: UserListParams) => {
|
|||
|
|
const response = await userApi.getUsers(params)
|
|||
|
|
if (response.success) {
|
|||
|
|
users.value = response.data.items
|
|||
|
|
}
|
|||
|
|
return response
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取用户详情
|
|||
|
|
const getUserDetail = async (id: string) => {
|
|||
|
|
const response = await userApi.getUserDetail(id)
|
|||
|
|
if (response.success) {
|
|||
|
|
currentUser.value = response.data
|
|||
|
|
}
|
|||
|
|
return response
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建用户
|
|||
|
|
const createUser = async (userData: CreateUserData) => {
|
|||
|
|
const response = await userApi.createUser(userData)
|
|||
|
|
if (response.success) {
|
|||
|
|
users.value.push(response.data)
|
|||
|
|
}
|
|||
|
|
return response
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新用户
|
|||
|
|
const updateUser = async (id: string, userData: UpdateUserData) => {
|
|||
|
|
const response = await userApi.updateUser(id, userData)
|
|||
|
|
if (response.success) {
|
|||
|
|
const index = users.value.findIndex(user => user.id === id)
|
|||
|
|
if (index !== -1) {
|
|||
|
|
users.value[index] = { ...users.value[index], ...response.data }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return response
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 删除用户
|
|||
|
|
const deleteUser = async (id: string) => {
|
|||
|
|
const response = await userApi.deleteUser(id)
|
|||
|
|
if (response.success) {
|
|||
|
|
const index = users.value.findIndex(user => user.id === id)
|
|||
|
|
if (index !== -1) {
|
|||
|
|
users.value.splice(index, 1)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return response
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
users,
|
|||
|
|
currentUser,
|
|||
|
|
getUsers,
|
|||
|
|
getUserDetail,
|
|||
|
|
createUser,
|
|||
|
|
updateUser,
|
|||
|
|
deleteUser
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 6. 网络层设计
|
|||
|
|
|
|||
|
|
### 6.1 HTTP请求封装
|
|||
|
|
```typescript
|
|||
|
|
// HTTP请求封装
|
|||
|
|
import axios from 'axios'
|
|||
|
|
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
|
|||
|
|
import { ElMessage } from 'element-plus'
|
|||
|
|
import { useAuthStore } from '@/stores/auth'
|
|||
|
|
|
|||
|
|
// 创建axios实例
|
|||
|
|
const service = axios.create({
|
|||
|
|
baseURL: import.meta.env.VITE_API_BASE_URL,
|
|||
|
|
timeout: 10000
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 请求拦截器
|
|||
|
|
service.interceptors.request.use(
|
|||
|
|
(config: AxiosRequestConfig) => {
|
|||
|
|
const authStore = useAuthStore()
|
|||
|
|
|
|||
|
|
// 添加认证token
|
|||
|
|
if (authStore.token) {
|
|||
|
|
config.headers = {
|
|||
|
|
...config.headers,
|
|||
|
|
Authorization: `Bearer ${authStore.token}`
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return config
|
|||
|
|
},
|
|||
|
|
(error) => {
|
|||
|
|
return Promise.reject(error)
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// 响应拦截器
|
|||
|
|
service.interceptors.response.use(
|
|||
|
|
(response: AxiosResponse) => {
|
|||
|
|
const { data } = response
|
|||
|
|
|
|||
|
|
// 统一处理响应
|
|||
|
|
if (data.success) {
|
|||
|
|
return data
|
|||
|
|
} else {
|
|||
|
|
ElMessage.error(data.message || '请求失败')
|
|||
|
|
return Promise.reject(data)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
(error) => {
|
|||
|
|
const { response } = error
|
|||
|
|
|
|||
|
|
if (response) {
|
|||
|
|
switch (response.status) {
|
|||
|
|
case 401:
|
|||
|
|
ElMessage.error('登录已过期,请重新登录')
|
|||
|
|
const authStore = useAuthStore()
|
|||
|
|
authStore.logout()
|
|||
|
|
router.push('/login')
|
|||
|
|
break
|
|||
|
|
case 403:
|
|||
|
|
ElMessage.error('权限不足')
|
|||
|
|
break
|
|||
|
|
case 404:
|
|||
|
|
ElMessage.error('请求的资源不存在')
|
|||
|
|
break
|
|||
|
|
case 500:
|
|||
|
|
ElMessage.error('服务器内部错误')
|
|||
|
|
break
|
|||
|
|
default:
|
|||
|
|
ElMessage.error('网络错误')
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
ElMessage.error('网络连接失败')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return Promise.reject(error)
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
export default service
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 API接口定义
|
|||
|
|
```typescript
|
|||
|
|
// API接口定义
|
|||
|
|
import request from '@/utils/request'
|
|||
|
|
import type {
|
|||
|
|
LoginCredentials,
|
|||
|
|
LoginResponse,
|
|||
|
|
UserListParams,
|
|||
|
|
UserListResponse,
|
|||
|
|
CreateUserData,
|
|||
|
|
UpdateUserData
|
|||
|
|
} from '@/types/api'
|
|||
|
|
|
|||
|
|
// 认证相关接口
|
|||
|
|
export const authApi = {
|
|||
|
|
// 登录
|
|||
|
|
login: (data: LoginCredentials): Promise<LoginResponse> => {
|
|||
|
|
return request.post('/auth/login', data)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 登出
|
|||
|
|
logout: (): Promise<void> => {
|
|||
|
|
return request.post('/auth/logout')
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 获取用户信息
|
|||
|
|
getUserInfo: (): Promise<UserInfoResponse> => {
|
|||
|
|
return request.get('/auth/user-info')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 用户相关接口
|
|||
|
|
export const userApi = {
|
|||
|
|
// 获取用户列表
|
|||
|
|
getUsers: (params: UserListParams): Promise<UserListResponse> => {
|
|||
|
|
return request.get('/users', { params })
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 获取用户详情
|
|||
|
|
getUserDetail: (id: string): Promise<UserDetailResponse> => {
|
|||
|
|
return request.get(`/users/${id}`)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 创建用户
|
|||
|
|
createUser: (data: CreateUserData): Promise<UserResponse> => {
|
|||
|
|
return request.post('/users', data)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 更新用户
|
|||
|
|
updateUser: (id: string, data: UpdateUserData): Promise<UserResponse> => {
|
|||
|
|
return request.put(`/users/${id}`, data)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 删除用户
|
|||
|
|
deleteUser: (id: string): Promise<void> => {
|
|||
|
|
return request.delete(`/users/${id}`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 7. 组件化设计
|
|||
|
|
|
|||
|
|
### 7.1 通用组件
|
|||
|
|
```vue
|
|||
|
|
<!-- 表格组件 -->
|
|||
|
|
<template>
|
|||
|
|
<div class="data-table">
|
|||
|
|
<el-table
|
|||
|
|
:data="data"
|
|||
|
|
:loading="loading"
|
|||
|
|
v-bind="$attrs"
|
|||
|
|
@selection-change="handleSelectionChange"
|
|||
|
|
>
|
|||
|
|
<slot />
|
|||
|
|
</el-table>
|
|||
|
|
|
|||
|
|
<el-pagination
|
|||
|
|
v-if="showPagination"
|
|||
|
|
v-model:current-page="currentPage"
|
|||
|
|
v-model:page-size="pageSize"
|
|||
|
|
:total="total"
|
|||
|
|
:page-sizes="pageSizes"
|
|||
|
|
:layout="paginationLayout"
|
|||
|
|
@size-change="handleSizeChange"
|
|||
|
|
@current-change="handleCurrentChange"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
interface Props {
|
|||
|
|
data: any[]
|
|||
|
|
loading?: boolean
|
|||
|
|
showPagination?: boolean
|
|||
|
|
total?: number
|
|||
|
|
pageSizes?: number[]
|
|||
|
|
paginationLayout?: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const props = withDefaults(defineProps<Props>(), {
|
|||
|
|
loading: false,
|
|||
|
|
showPagination: true,
|
|||
|
|
total: 0,
|
|||
|
|
pageSizes: () => [10, 20, 50, 100],
|
|||
|
|
paginationLayout: 'total, sizes, prev, pager, next, jumper'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
selectionChange: [selection: any[]]
|
|||
|
|
sizeChange: [size: number]
|
|||
|
|
currentChange: [current: number]
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
const currentPage = ref(1)
|
|||
|
|
const pageSize = ref(20)
|
|||
|
|
|
|||
|
|
const handleSelectionChange = (selection: any[]) => {
|
|||
|
|
emit('selectionChange', selection)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleSizeChange = (size: number) => {
|
|||
|
|
pageSize.value = size
|
|||
|
|
emit('sizeChange', size)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleCurrentChange = (current: number) => {
|
|||
|
|
currentPage.value = current
|
|||
|
|
emit('currentChange', current)
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7.2 业务组件
|
|||
|
|
```vue
|
|||
|
|
<!-- 用户选择器组件 -->
|
|||
|
|
<template>
|
|||
|
|
<el-select
|
|||
|
|
v-model="selectedValue"
|
|||
|
|
:placeholder="placeholder"
|
|||
|
|
:multiple="multiple"
|
|||
|
|
:filterable="filterable"
|
|||
|
|
:remote="remote"
|
|||
|
|
:remote-method="remoteMethod"
|
|||
|
|
:loading="loading"
|
|||
|
|
@change="handleChange"
|
|||
|
|
>
|
|||
|
|
<el-option
|
|||
|
|
v-for="user in users"
|
|||
|
|
:key="user.id"
|
|||
|
|
:label="user.username"
|
|||
|
|
:value="user.id"
|
|||
|
|
>
|
|||
|
|
<span>{{ user.username }}</span>
|
|||
|
|
<span style="float: right; color: #8492a6; font-size: 13px">
|
|||
|
|
{{ user.phone }}
|
|||
|
|
</span>
|
|||
|
|
</el-option>
|
|||
|
|
</el-select>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, onMounted, watch } from 'vue'
|
|||
|
|
import { userApi } from '@/api/user'
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
modelValue: string | string[]
|
|||
|
|
placeholder?: string
|
|||
|
|
multiple?: boolean
|
|||
|
|
filterable?: boolean
|
|||
|
|
remote?: boolean
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const props = withDefaults(defineProps<Props>(), {
|
|||
|
|
placeholder: '请选择用户',
|
|||
|
|
multiple: false,
|
|||
|
|
filterable: true,
|
|||
|
|
remote: true
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
'update:modelValue': [value: string | string[]]
|
|||
|
|
change: [value: string | string[]]
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
const selectedValue = ref(props.modelValue)
|
|||
|
|
const users = ref<User[]>([])
|
|||
|
|
const loading = ref(false)
|
|||
|
|
|
|||
|
|
// 远程搜索用户
|
|||
|
|
const remoteMethod = async (query: string) => {
|
|||
|
|
if (!query) {
|
|||
|
|
users.value = []
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
loading.value = true
|
|||
|
|
try {
|
|||
|
|
const response = await userApi.searchUsers({ keyword: query })
|
|||
|
|
if (response.success) {
|
|||
|
|
users.value = response.data
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('搜索用户失败:', error)
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理选择变化
|
|||
|
|
const handleChange = (value: string | string[]) => {
|
|||
|
|
emit('update:modelValue', value)
|
|||
|
|
emit('change', value)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 监听外部值变化
|
|||
|
|
watch(() => props.modelValue, (newValue) => {
|
|||
|
|
selectedValue.value = newValue
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
if (!props.remote) {
|
|||
|
|
// 非远程模式,加载所有用户
|
|||
|
|
remoteMethod('')
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 8. 性能优化
|
|||
|
|
|
|||
|
|
### 8.1 代码分割
|
|||
|
|
```typescript
|
|||
|
|
// 路由懒加载
|
|||
|
|
const routes = [
|
|||
|
|
{
|
|||
|
|
path: '/user',
|
|||
|
|
component: () => import('@/pages/user/Index.vue')
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
path: '/farm',
|
|||
|
|
component: () => import('@/pages/farm/Index.vue')
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
// 组件懒加载
|
|||
|
|
const LazyComponent = defineAsyncComponent(() => import('@/components/HeavyComponent.vue'))
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.2 虚拟滚动
|
|||
|
|
```vue
|
|||
|
|
<!-- 虚拟列表组件 -->
|
|||
|
|
<template>
|
|||
|
|
<div class="virtual-list" :style="{ height: height + 'px' }">
|
|||
|
|
<div class="virtual-list-phantom" :style="{ height: totalHeight + 'px' }"></div>
|
|||
|
|
<div class="virtual-list-content" :style="{ transform: `translateY(${offset}px)` }">
|
|||
|
|
<div
|
|||
|
|
v-for="item in visibleItems"
|
|||
|
|
:key="item.id"
|
|||
|
|
class="virtual-list-item"
|
|||
|
|
:style="{ height: itemHeight + 'px' }"
|
|||
|
|
>
|
|||
|
|
<slot :item="item" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
interface Props {
|
|||
|
|
items: any[]
|
|||
|
|
itemHeight: number
|
|||
|
|
height: number
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const props = defineProps<Props>()
|
|||
|
|
|
|||
|
|
const scrollTop = ref(0)
|
|||
|
|
const visibleCount = computed(() => Math.ceil(props.height / props.itemHeight))
|
|||
|
|
const totalHeight = computed(() => props.items.length * props.itemHeight)
|
|||
|
|
const startIndex = computed(() => Math.floor(scrollTop.value / props.itemHeight))
|
|||
|
|
const endIndex = computed(() => Math.min(startIndex.value + visibleCount.value, props.items.length))
|
|||
|
|
const visibleItems = computed(() => props.items.slice(startIndex.value, endIndex.value))
|
|||
|
|
const offset = computed(() => startIndex.value * props.itemHeight)
|
|||
|
|
|
|||
|
|
const handleScroll = (e: Event) => {
|
|||
|
|
scrollTop.value = (e.target as HTMLElement).scrollTop
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.3 缓存策略
|
|||
|
|
```typescript
|
|||
|
|
// 请求缓存
|
|||
|
|
class RequestCache {
|
|||
|
|
private cache = new Map<string, { data: any; timestamp: number }>()
|
|||
|
|
private ttl = 5 * 60 * 1000 // 5分钟
|
|||
|
|
|
|||
|
|
get(key: string): any {
|
|||
|
|
const item = this.cache.get(key)
|
|||
|
|
if (!item) return null
|
|||
|
|
|
|||
|
|
if (Date.now() - item.timestamp > this.ttl) {
|
|||
|
|
this.cache.delete(key)
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return item.data
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
set(key: string, data: any): void {
|
|||
|
|
this.cache.set(key, {
|
|||
|
|
data,
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
clear(): void {
|
|||
|
|
this.cache.clear()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用缓存的API请求
|
|||
|
|
const requestWithCache = async (url: string, options?: any) => {
|
|||
|
|
const cacheKey = `${url}${JSON.stringify(options)}`
|
|||
|
|
|
|||
|
|
// 尝试从缓存获取
|
|||
|
|
const cachedData = requestCache.get(cacheKey)
|
|||
|
|
if (cachedData) {
|
|||
|
|
return cachedData
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发起请求
|
|||
|
|
const response = await request(url, options)
|
|||
|
|
|
|||
|
|
// 缓存响应数据
|
|||
|
|
requestCache.set(cacheKey, response)
|
|||
|
|
|
|||
|
|
return response
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 9. 安全设计
|
|||
|
|
|
|||
|
|
### 9.1 XSS防护
|
|||
|
|
```typescript
|
|||
|
|
// XSS防护工具
|
|||
|
|
export const xssFilter = {
|
|||
|
|
// 转义HTML特殊字符
|
|||
|
|
escapeHtml: (str: string): string => {
|
|||
|
|
const map: Record<string, string> = {
|
|||
|
|
'&': '&',
|
|||
|
|
'<': '<',
|
|||
|
|
'>': '>',
|
|||
|
|
'"': '"',
|
|||
|
|
"'": '''
|
|||
|
|
}
|
|||
|
|
return str.replace(/[&<>"']/g, (match) => map[match])
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 过滤危险标签
|
|||
|
|
filterTags: (str: string): string => {
|
|||
|
|
return str.replace(/<script[^>]*>.*?<\/script>/gi, '')
|
|||
|
|
.replace(/<iframe[^>]*>.*?<\/iframe>/gi, '')
|
|||
|
|
.replace(/javascript:/gi, '')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 在组件中使用
|
|||
|
|
const safeContent = computed(() => {
|
|||
|
|
return xssFilter.escapeHtml(props.content)
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 9.2 CSRF防护
|
|||
|
|
```typescript
|
|||
|
|
// CSRF Token处理
|
|||
|
|
const csrfToken = ref('')
|
|||
|
|
|
|||
|
|
// 获取CSRF Token
|
|||
|
|
const getCsrfToken = async () => {
|
|||
|
|
const response = await request.get('/csrf-token')
|
|||
|
|
csrfToken.value = response.data.token
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 在请求中添加CSRF Token
|
|||
|
|
service.interceptors.request.use((config) => {
|
|||
|
|
if (['post', 'put', 'delete'].includes(config.method?.toLowerCase() || '')) {
|
|||
|
|
config.headers['X-CSRF-Token'] = csrfToken.value
|
|||
|
|
}
|
|||
|
|
return config
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 9.3 权限控制
|
|||
|
|
```typescript
|
|||
|
|
// 权限指令
|
|||
|
|
const permissionDirective = {
|
|||
|
|
mounted(el: HTMLElement, binding: any) {
|
|||
|
|
const { value } = binding
|
|||
|
|
const authStore = useAuthStore()
|
|||
|
|
|
|||
|
|
if (!authStore.hasPermission(value)) {
|
|||
|
|
el.style.display = 'none'
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
updated(el: HTMLElement, binding: any) {
|
|||
|
|
const { value } = binding
|
|||
|
|
const authStore = useAuthStore()
|
|||
|
|
|
|||
|
|
if (!authStore.hasPermission(value)) {
|
|||
|
|
el.style.display = 'none'
|
|||
|
|
} else {
|
|||
|
|
el.style.display = ''
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 注册指令
|
|||
|
|
app.directive('permission', permissionDirective)
|
|||
|
|
|
|||
|
|
// 在模板中使用
|
|||
|
|
// <el-button v-permission="'user:delete'">删除</el-button>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 10. 测试策略
|
|||
|
|
|
|||
|
|
### 10.1 单元测试
|
|||
|
|
```typescript
|
|||
|
|
// 组件测试
|
|||
|
|
import { mount } from '@vue/test-utils'
|
|||
|
|
import { describe, it, expect } from 'vitest'
|
|||
|
|
import UserList from '@/pages/user/List.vue'
|
|||
|
|
|
|||
|
|
describe('UserList', () => {
|
|||
|
|
it('renders user list correctly', () => {
|
|||
|
|
const wrapper = mount(UserList, {
|
|||
|
|
props: {
|
|||
|
|
users: [
|
|||
|
|
{ id: 1, username: 'test1', email: 'test1@example.com' },
|
|||
|
|
{ id: 2, username: 'test2', email: 'test2@example.com' }
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
expect(wrapper.find('.user-list').exists()).toBe(true)
|
|||
|
|
expect(wrapper.findAll('.user-item')).toHaveLength(2)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('emits edit event when edit button clicked', async () => {
|
|||
|
|
const wrapper = mount(UserList)
|
|||
|
|
await wrapper.find('.edit-btn').trigger('click')
|
|||
|
|
|
|||
|
|
expect(wrapper.emitted('edit')).toBeTruthy()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 10.2 集成测试
|
|||
|
|
```typescript
|
|||
|
|
// API测试
|
|||
|
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|||
|
|
import { userApi } from '@/api/user'
|
|||
|
|
|
|||
|
|
describe('User API', () => {
|
|||
|
|
beforeEach(() => {
|
|||
|
|
// 设置测试环境
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('should get user list', async () => {
|
|||
|
|
const response = await userApi.getUsers({ page: 1, limit: 10 })
|
|||
|
|
|
|||
|
|
expect(response.success).toBe(true)
|
|||
|
|
expect(response.data.items).toBeInstanceOf(Array)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('should create user', async () => {
|
|||
|
|
const userData = {
|
|||
|
|
username: 'testuser',
|
|||
|
|
email: 'test@example.com',
|
|||
|
|
password: 'password123'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const response = await userApi.createUser(userData)
|
|||
|
|
|
|||
|
|
expect(response.success).toBe(true)
|
|||
|
|
expect(response.data.username).toBe(userData.username)
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 11. 构建与部署
|
|||
|
|
|
|||
|
|
### 11.1 构建配置
|
|||
|
|
```typescript
|
|||
|
|
// vite.config.ts
|
|||
|
|
import { defineConfig } from 'vite'
|
|||
|
|
import vue from '@vitejs/plugin-vue'
|
|||
|
|
import { resolve } from 'path'
|
|||
|
|
|
|||
|
|
export default defineConfig({
|
|||
|
|
plugins: [vue()],
|
|||
|
|
resolve: {
|
|||
|
|
alias: {
|
|||
|
|
'@': resolve(__dirname, 'src')
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
build: {
|
|||
|
|
target: 'es2015',
|
|||
|
|
outDir: 'dist',
|
|||
|
|
assetsDir: 'assets',
|
|||
|
|
sourcemap: false,
|
|||
|
|
rollupOptions: {
|
|||
|
|
output: {
|
|||
|
|
chunkFileNames: 'js/[name]-[hash].js',
|
|||
|
|
entryFileNames: 'js/[name]-[hash].js',
|
|||
|
|
assetFileNames: '[ext]/[name]-[hash].[ext]'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
server: {
|
|||
|
|
port: 3000,
|
|||
|
|
proxy: {
|
|||
|
|
'/api': {
|
|||
|
|
target: 'http://localhost:8080',
|
|||
|
|
changeOrigin: true,
|
|||
|
|
rewrite: (path) => path.replace(/^\/api/, '')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 11.2 Docker部署
|
|||
|
|
```dockerfile
|
|||
|
|
# Dockerfile
|
|||
|
|
FROM node:18-alpine as builder
|
|||
|
|
|
|||
|
|
WORKDIR /app
|
|||
|
|
COPY package*.json ./
|
|||
|
|
RUN npm ci
|
|||
|
|
|
|||
|
|
COPY . .
|
|||
|
|
RUN npm run build
|
|||
|
|
|
|||
|
|
FROM nginx:alpine
|
|||
|
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
|||
|
|
COPY nginx.conf /etc/nginx/nginx.conf
|
|||
|
|
|
|||
|
|
EXPOSE 80
|
|||
|
|
CMD ["nginx", "-g", "daemon off;"]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 11.3 CI/CD配置
|
|||
|
|
```yaml
|
|||
|
|
# .gitlab-ci.yml
|
|||
|
|
stages:
|
|||
|
|
- test
|
|||
|
|
- build
|
|||
|
|
- deploy
|
|||
|
|
|
|||
|
|
test:
|
|||
|
|
stage: test
|
|||
|
|
script:
|
|||
|
|
- npm ci
|
|||
|
|
- npm run test
|
|||
|
|
- npm run lint
|
|||
|
|
|
|||
|
|
build:
|
|||
|
|
stage: build
|
|||
|
|
script:
|
|||
|
|
- npm ci
|
|||
|
|
- npm run build
|
|||
|
|
artifacts:
|
|||
|
|
paths:
|
|||
|
|
- dist/
|
|||
|
|
|
|||
|
|
deploy:
|
|||
|
|
stage: deploy
|
|||
|
|
script:
|
|||
|
|
- docker build -t admin-system .
|
|||
|
|
- docker push $CI_REGISTRY_IMAGE
|
|||
|
|
- kubectl apply -f k8s/
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 12. 监控与运维
|
|||
|
|
|
|||
|
|
### 12.1 性能监控
|
|||
|
|
```typescript
|
|||
|
|
// 性能监控
|
|||
|
|
class PerformanceMonitor {
|
|||
|
|
// 监控页面加载时间
|
|||
|
|
monitorPageLoad() {
|
|||
|
|
window.addEventListener('load', () => {
|
|||
|
|
const timing = performance.timing
|
|||
|
|
const loadTime = timing.loadEventEnd - timing.navigationStart
|
|||
|
|
|
|||
|
|
this.reportMetric('page_load_time', loadTime)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 监控API响应时间
|
|||
|
|
monitorApiResponse(url: string, startTime: number, endTime: number) {
|
|||
|
|
const responseTime = endTime - startTime
|
|||
|
|
this.reportMetric('api_response_time', responseTime, { url })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 上报指标
|
|||
|
|
reportMetric(name: string, value: number, tags?: Record<string, any>) {
|
|||
|
|
// 发送到监控系统
|
|||
|
|
fetch('/api/metrics', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json'
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
name,
|
|||
|
|
value,
|
|||
|
|
tags,
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 12.2 错误监控
|
|||
|
|
```typescript
|
|||
|
|
// 错误监控
|
|||
|
|
class ErrorMonitor {
|
|||
|
|
constructor() {
|
|||
|
|
this.setupGlobalErrorHandler()
|
|||
|
|
this.setupUnhandledRejectionHandler()
|
|||
|
|
this.setupVueErrorHandler()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 全局错误处理
|
|||
|
|
setupGlobalErrorHandler() {
|
|||
|
|
window.addEventListener('error', (event) => {
|
|||
|
|
this.reportError({
|
|||
|
|
type: 'javascript',
|
|||
|
|
message: event.message,
|
|||
|
|
filename: event.filename,
|
|||
|
|
lineno: event.lineno,
|
|||
|
|
colno: event.colno,
|
|||
|
|
stack: event.error?.stack
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Promise错误处理
|
|||
|
|
setupUnhandledRejectionHandler() {
|
|||
|
|
window.addEventListener('unhandledrejection', (event) => {
|
|||
|
|
this.reportError({
|
|||
|
|
type: 'promise',
|
|||
|
|
message: event.reason?.message || 'Unhandled Promise Rejection',
|
|||
|
|
stack: event.reason?.stack
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Vue错误处理
|
|||
|
|
setupVueErrorHandler() {
|
|||
|
|
app.config.errorHandler = (err, vm, info) => {
|
|||
|
|
this.reportError({
|
|||
|
|
type: 'vue',
|
|||
|
|
message: err.message,
|
|||
|
|
stack: err.stack,
|
|||
|
|
componentName: vm?.$options.name,
|
|||
|
|
errorInfo: info
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 上报错误
|
|||
|
|
reportError(error: any) {
|
|||
|
|
fetch('/api/errors', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json'
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
...error,
|
|||
|
|
url: window.location.href,
|
|||
|
|
userAgent: navigator.userAgent,
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 13. 扩展性设计
|
|||
|
|
|
|||
|
|
### 13.1 插件系统
|
|||
|
|
```typescript
|
|||
|
|
// 插件系统
|
|||
|
|
interface Plugin {
|
|||
|
|
name: string
|
|||
|
|
version: string
|
|||
|
|
install: (app: App) => void
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class PluginManager {
|
|||
|
|
private plugins: Map<string, Plugin> = new Map()
|
|||
|
|
|
|||
|
|
register(plugin: Plugin) {
|
|||
|
|
this.plugins.set(plugin.name, plugin)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
install(app: App, pluginName: string) {
|
|||
|
|
const plugin = this.plugins.get(pluginName)
|
|||
|
|
if (plugin) {
|
|||
|
|
plugin.install(app)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
installAll(app: App) {
|
|||
|
|
this.plugins.forEach(plugin => {
|
|||
|
|
plugin.install(app)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用插件
|
|||
|
|
const pluginManager = new PluginManager()
|
|||
|
|
|
|||
|
|
pluginManager.register({
|
|||
|
|
name: 'chart',
|
|||
|
|
version: '1.0.0',
|
|||
|
|
install: (app) => {
|
|||
|
|
app.component('Chart', ChartComponent)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 13.2 主题系统
|
|||
|
|
```typescript
|
|||
|
|
// 主题系统
|
|||
|
|
interface Theme {
|
|||
|
|
name: string
|
|||
|
|
colors: Record<string, string>
|
|||
|
|
fonts: Record<string, string>
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class ThemeManager {
|
|||
|
|
private themes: Map<string, Theme> = new Map()
|
|||
|
|
private currentTheme = ref<string>('default')
|
|||
|
|
|
|||
|
|
register(theme: Theme) {
|
|||
|
|
this.themes.set(theme.name, theme)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setTheme(themeName: string) {
|
|||
|
|
const theme = this.themes.get(themeName)
|
|||
|
|
if (theme) {
|
|||
|
|
this.currentTheme.value = themeName
|
|||
|
|
this.applyTheme(theme)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private applyTheme(theme: Theme) {
|
|||
|
|
const root = document.documentElement
|
|||
|
|
|
|||
|
|
Object.entries(theme.colors).forEach(([key, value]) => {
|
|||
|
|
root.style.setProperty(`--color-${key}`, value)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
Object.entries(theme.fonts).forEach(([key, value]) => {
|
|||
|
|
root.style.setProperty(`--font-${key}`, value)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 14. 未来规划
|
|||
|
|
|
|||
|
|
### 14.1 技术升级
|
|||
|
|
- **Vue 3.4+**: 升级到最新版本Vue.js
|
|||
|
|
- **Vite 5.x**: 升级构建工具
|
|||
|
|
- **TypeScript 5.x**: 使用最新TypeScript特性
|
|||
|
|
- **微前端**: 考虑微前端架构
|
|||
|
|
|
|||
|
|
### 14.2 功能扩展
|
|||
|
|
- **国际化**: 支持多语言
|
|||
|
|
- **PWA**: 渐进式Web应用
|
|||
|
|
- **离线功能**: 支持离线操作
|
|||
|
|
- **实时通信**: WebSocket实时更新
|