Files
xlxumu/docs/architecture/管理后台架构文档.md

38 KiB
Raw Blame History

管理后台架构文档

版本历史

版本 日期 作者 变更说明
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的权限控制
  • 角色管理: 角色创建、编辑、权限分配

状态管理:

// 认证状态管理
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 用户管理模块

功能: 用户列表、用户详情、用户操作

核心页面:

  • 用户列表: 分页展示用户信息,支持搜索和筛选
  • 用户详情: 查看和编辑用户详细信息
  • 用户操作: 启用/禁用用户、重置密码等

表格组件:

<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 数据统计模块

功能: 数据可视化、报表生成、趋势分析

核心组件:

  • 仪表板: 关键指标展示
  • 图表组件: 各类数据图表
  • 报表页面: 详细数据报表

图表组件:

<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 路由配置

// 路由配置
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 路由守卫

// 路由守卫
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 应用状态

// 应用状态管理
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 用户状态

// 用户状态管理
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请求封装

// 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接口定义

// 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 通用组件

<!-- 表格组件 -->
<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 业务组件

<!-- 用户选择器组件 -->
<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 代码分割

// 路由懒加载
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 虚拟滚动

<!-- 虚拟列表组件 -->
<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 缓存策略

// 请求缓存
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防护

// XSS防护工具
export const xssFilter = {
  // 转义HTML特殊字符
  escapeHtml: (str: string): string => {
    const map: Record<string, string> = {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#39;'
    }
    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防护

// 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 权限控制

// 权限指令
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 单元测试

// 组件测试
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 集成测试

// 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 构建配置

// 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
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配置

# .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 性能监控

// 性能监控
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 错误监控

// 错误监控
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 插件系统

// 插件系统
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 主题系统

// 主题系统
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实时更新