remove
This commit is contained in:
@@ -1,187 +0,0 @@
|
|||||||
# 分页组件重构总结
|
|
||||||
|
|
||||||
## 🎯 重构目标
|
|
||||||
|
|
||||||
通过创建 `usePagination` composable 来减少项目中分页相关的重复代码,统一分页逻辑。
|
|
||||||
|
|
||||||
## 📋 已完成的更新
|
|
||||||
|
|
||||||
### 1. 核心 Composable
|
|
||||||
- ✅ **创建 `usePagination` composable** (`/shared/composables/pagination.ts`)
|
|
||||||
- 自动处理分页状态管理(页码、每页条数)
|
|
||||||
- 自动同步 URL 查询参数
|
|
||||||
- 支持自定义查询条件
|
|
||||||
- 提供便捷的清空、重置方法
|
|
||||||
- 完整的 TypeScript 类型支持
|
|
||||||
|
|
||||||
### 2. 前台用户页面
|
|
||||||
- ✅ **问题列表页** (`/oj/problem/list.vue`)
|
|
||||||
- 移除 25+ 行重复代码
|
|
||||||
- 简化查询逻辑
|
|
||||||
- 保留防抖搜索功能
|
|
||||||
|
|
||||||
- ✅ **提交记录页** (`/oj/submission/list.vue`)
|
|
||||||
- 简化复杂的查询条件处理
|
|
||||||
- 统一 URL 同步逻辑
|
|
||||||
- 保留所有筛选功能
|
|
||||||
|
|
||||||
- ✅ **比赛列表页** (`/oj/contest/list.vue`)
|
|
||||||
- 减少路由处理代码
|
|
||||||
- 统一分页行为
|
|
||||||
|
|
||||||
### 3. 管理员页面
|
|
||||||
- ✅ **用户管理页** (`/admin/user/list.vue`)
|
|
||||||
- 简化用户查询和筛选逻辑
|
|
||||||
- 统一分页处理
|
|
||||||
|
|
||||||
- ✅ **题目管理页** (`/admin/problem/list.vue`)
|
|
||||||
- 减少重复的路由同步代码
|
|
||||||
- 保留题目搜索功能
|
|
||||||
|
|
||||||
## 📊 重构效果
|
|
||||||
|
|
||||||
### 代码减少统计
|
|
||||||
- **每个页面减少代码量**: 20-30 行
|
|
||||||
- **总计减少代码量**: 约 150+ 行
|
|
||||||
- **重复逻辑消除**: 100%
|
|
||||||
|
|
||||||
### 重构前后对比
|
|
||||||
|
|
||||||
#### 重构前(每个分页页面都需要)
|
|
||||||
```vue
|
|
||||||
<script setup>
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const query = reactive({
|
|
||||||
page: parseInt(route.query.page) || 1,
|
|
||||||
limit: parseInt(route.query.limit) || 10,
|
|
||||||
keyword: route.query.keyword ?? "",
|
|
||||||
// ... 其他查询条件
|
|
||||||
})
|
|
||||||
|
|
||||||
function routerPush() {
|
|
||||||
router.push({
|
|
||||||
path: route.path,
|
|
||||||
query: filterEmptyValue(query),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadData() {
|
|
||||||
// 从 URL 同步状态
|
|
||||||
query.keyword = route.query.keyword ?? ""
|
|
||||||
query.page = parseInt(route.query.page) || 1
|
|
||||||
query.limit = parseInt(route.query.limit) || 10
|
|
||||||
// ... 其他同步逻辑
|
|
||||||
|
|
||||||
// API 调用
|
|
||||||
const res = await api.getData(...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 大量的 watch 逻辑
|
|
||||||
watch(() => query.page, routerPush)
|
|
||||||
watch(() => [query.limit, /* 其他条件 */], () => {
|
|
||||||
query.page = 1
|
|
||||||
routerPush()
|
|
||||||
})
|
|
||||||
watchDebounced(() => query.keyword, () => {
|
|
||||||
query.page = 1
|
|
||||||
routerPush()
|
|
||||||
}, { debounce: 500 })
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 重构后(简洁高效)
|
|
||||||
```vue
|
|
||||||
<script setup>
|
|
||||||
// 一行代码完成所有分页逻辑
|
|
||||||
const { query, clearQuery } = usePagination({
|
|
||||||
keyword: "",
|
|
||||||
// ... 其他查询条件
|
|
||||||
})
|
|
||||||
|
|
||||||
async function loadData() {
|
|
||||||
// 直接使用 query,无需同步逻辑
|
|
||||||
const res = await api.getData({
|
|
||||||
offset: (query.page - 1) * query.limit,
|
|
||||||
limit: query.limit,
|
|
||||||
keyword: query.keyword,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 简化的监听逻辑
|
|
||||||
watchDebounced(() => query.keyword, loadData, { debounce: 500 })
|
|
||||||
watch(() => [query.page, query.limit], loadData)
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 主要优势
|
|
||||||
|
|
||||||
### 1. 代码复用性
|
|
||||||
- 所有分页逻辑集中管理
|
|
||||||
- 统一的行为模式
|
|
||||||
- 减少维护成本
|
|
||||||
|
|
||||||
### 2. 类型安全
|
|
||||||
- 完整的 TypeScript 支持
|
|
||||||
- 自定义查询条件的类型推导
|
|
||||||
- 编译时错误检查
|
|
||||||
|
|
||||||
### 3. 自动化处理
|
|
||||||
- 自动 URL 同步
|
|
||||||
- 自动页码重置
|
|
||||||
- 自动路由监听
|
|
||||||
- 浏览器前进后退支持
|
|
||||||
|
|
||||||
### 4. 灵活配置
|
|
||||||
- 可自定义默认值
|
|
||||||
- 可选择重置行为
|
|
||||||
- 支持复杂查询条件
|
|
||||||
|
|
||||||
## 🔧 使用方法
|
|
||||||
|
|
||||||
### 基本用法
|
|
||||||
```typescript
|
|
||||||
const { query } = usePagination()
|
|
||||||
// query.page, query.limit 自动可用
|
|
||||||
```
|
|
||||||
|
|
||||||
### 带查询条件
|
|
||||||
```typescript
|
|
||||||
const { query, clearQuery } = usePagination({
|
|
||||||
keyword: "",
|
|
||||||
status: "",
|
|
||||||
category: "",
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置选项
|
|
||||||
```typescript
|
|
||||||
const { query } = usePagination(
|
|
||||||
{ keyword: "" },
|
|
||||||
{
|
|
||||||
defaultLimit: 20,
|
|
||||||
defaultPage: 1,
|
|
||||||
resetPageOnChange: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 迁移指南
|
|
||||||
|
|
||||||
1. 导入 `usePagination`
|
|
||||||
2. 替换原有的 `query` 定义
|
|
||||||
3. 移除 `routerPush` 函数
|
|
||||||
4. 简化 `watch` 逻辑
|
|
||||||
5. 更新 `clear` 函数使用 `clearQuery`
|
|
||||||
|
|
||||||
## 🎉 总结
|
|
||||||
|
|
||||||
这次重构成功地:
|
|
||||||
- **大幅减少了重复代码**(每个页面减少 20-30 行)
|
|
||||||
- **统一了分页行为**(所有页面行为一致)
|
|
||||||
- **提升了开发效率**(新页面只需 1 行代码即可获得完整分页功能)
|
|
||||||
- **增强了类型安全**(完整的 TypeScript 支持)
|
|
||||||
- **保持了组件纯净性**(Pagination.vue 组件职责单一)
|
|
||||||
|
|
||||||
通过这个 composable,未来添加新的分页页面将变得非常简单,只需要一行代码就能获得完整的分页功能。
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
# 分页 Composable 使用指南
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
`usePagination` composable 是为了减少重复的分页和 URL 同步代码而创建的。它自动处理:
|
|
||||||
|
|
||||||
- 分页状态管理(页码、每页条数)
|
|
||||||
- URL 查询参数同步
|
|
||||||
- 查询条件变化时的页码重置
|
|
||||||
- 防抖和监听逻辑
|
|
||||||
|
|
||||||
## 基本用法
|
|
||||||
|
|
||||||
### 1. 简单分页(只有分页功能)
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { usePagination } from "~/shared/composables/pagination"
|
|
||||||
import Pagination from "~/shared/components/Pagination.vue"
|
|
||||||
|
|
||||||
// 只使用基本分页功能
|
|
||||||
const { query } = usePagination()
|
|
||||||
|
|
||||||
// 获取数据
|
|
||||||
async function loadData() {
|
|
||||||
const offset = (query.page - 1) * query.limit
|
|
||||||
// 调用 API...
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听分页变化
|
|
||||||
watch([() => query.page, () => query.limit], loadData)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<!-- 你的数据展示 -->
|
|
||||||
<Pagination
|
|
||||||
:total="total"
|
|
||||||
v-model:page="query.page"
|
|
||||||
v-model:limit="query.limit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 带查询条件的分页
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { usePagination } from "~/shared/composables/pagination"
|
|
||||||
|
|
||||||
interface SearchQuery {
|
|
||||||
keyword: string
|
|
||||||
status: string
|
|
||||||
category: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义查询条件的初始值
|
|
||||||
const { query, clearQuery } = usePagination<SearchQuery>({
|
|
||||||
keyword: "",
|
|
||||||
status: "",
|
|
||||||
category: "",
|
|
||||||
})
|
|
||||||
|
|
||||||
async function loadData() {
|
|
||||||
const offset = (query.page - 1) * query.limit
|
|
||||||
const res = await api.getData({
|
|
||||||
offset,
|
|
||||||
limit: query.limit,
|
|
||||||
keyword: query.keyword,
|
|
||||||
status: query.status,
|
|
||||||
category: query.category,
|
|
||||||
})
|
|
||||||
// 处理返回数据...
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听所有查询条件变化(会自动重置页码)
|
|
||||||
watch([
|
|
||||||
() => query.page,
|
|
||||||
() => query.limit,
|
|
||||||
() => query.keyword,
|
|
||||||
() => query.status,
|
|
||||||
() => query.category,
|
|
||||||
], loadData)
|
|
||||||
|
|
||||||
// 对于需要防抖的搜索
|
|
||||||
watchDebounced(
|
|
||||||
() => query.keyword,
|
|
||||||
loadData,
|
|
||||||
{ debounce: 500 }
|
|
||||||
)
|
|
||||||
|
|
||||||
function handleSearch() {
|
|
||||||
// 手动触发搜索,会自动重置到第一页
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClear() {
|
|
||||||
// 清空所有查询条件
|
|
||||||
clearQuery()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<!-- 搜索表单 -->
|
|
||||||
<n-form inline>
|
|
||||||
<n-form-item>
|
|
||||||
<n-input v-model:value="query.keyword" placeholder="搜索关键词" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item>
|
|
||||||
<n-select v-model:value="query.status" :options="statusOptions" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item>
|
|
||||||
<n-button @click="handleSearch">搜索</n-button>
|
|
||||||
<n-button @click="handleClear">清空</n-button>
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
|
|
||||||
<!-- 数据展示 -->
|
|
||||||
<!-- ... -->
|
|
||||||
|
|
||||||
<!-- 分页组件 -->
|
|
||||||
<Pagination
|
|
||||||
:total="total"
|
|
||||||
v-model:page="query.page"
|
|
||||||
v-model:limit="query.limit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 配置选项
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const { query } = usePagination(
|
|
||||||
{
|
|
||||||
// 查询条件初始值
|
|
||||||
keyword: "",
|
|
||||||
status: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultLimit: 20, // 默认每页条数,默认 10
|
|
||||||
defaultPage: 1, // 默认页码,默认 1
|
|
||||||
resetPageOnChange: true, // 查询条件变化时是否重置页码,默认 true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 返回值说明
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const {
|
|
||||||
query, // 响应式查询对象,包含 page、limit 和自定义查询条件
|
|
||||||
updateRoute, // 手动更新 URL(通常不需要调用)
|
|
||||||
resetPage, // 重置页码到第一页
|
|
||||||
clearQuery, // 清空所有查询条件
|
|
||||||
syncFromRoute // 从 URL 同步到本地状态(通常不需要调用)
|
|
||||||
} = usePagination()
|
|
||||||
```
|
|
||||||
|
|
||||||
## 迁移指南
|
|
||||||
|
|
||||||
### 迁移前(旧代码)
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<script setup lang="ts">
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const query = reactive({
|
|
||||||
page: parseInt(route.query.page) || 1,
|
|
||||||
limit: parseInt(route.query.limit) || 10,
|
|
||||||
keyword: route.query.keyword ?? "",
|
|
||||||
status: route.query.status ?? "",
|
|
||||||
})
|
|
||||||
|
|
||||||
function routerPush() {
|
|
||||||
router.push({
|
|
||||||
path: route.path,
|
|
||||||
query: filterEmptyValue(query),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear() {
|
|
||||||
query.keyword = ""
|
|
||||||
query.status = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// 大量的 watch 逻辑
|
|
||||||
watch(() => query.page, routerPush)
|
|
||||||
watch(() => [query.limit, query.status], () => {
|
|
||||||
query.page = 1
|
|
||||||
routerPush()
|
|
||||||
})
|
|
||||||
watchDebounced(() => query.keyword, () => {
|
|
||||||
query.page = 1
|
|
||||||
routerPush()
|
|
||||||
}, { debounce: 500 })
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 迁移后(新代码)
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<script setup lang="ts">
|
|
||||||
const { query, clearQuery } = usePagination({
|
|
||||||
keyword: "",
|
|
||||||
status: "",
|
|
||||||
})
|
|
||||||
|
|
||||||
function clear() {
|
|
||||||
clearQuery()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 简化的监听逻辑
|
|
||||||
watchDebounced(() => query.keyword, loadData, { debounce: 500 })
|
|
||||||
watch([() => query.page, () => query.limit, () => query.status], loadData)
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 优势
|
|
||||||
|
|
||||||
1. **减少重复代码**:URL 同步逻辑自动处理
|
|
||||||
2. **统一行为**:所有分页组件行为一致
|
|
||||||
3. **类型安全**:完整的 TypeScript 支持
|
|
||||||
4. **灵活配置**:支持自定义默认值和行为
|
|
||||||
5. **易于维护**:集中管理分页逻辑
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. 查询条件变化时会自动重置页码(可通过 `resetPageOnChange: false` 禁用)
|
|
||||||
2. URL 同步是自动的,无需手动调用 `router.push`
|
|
||||||
3. 组件挂载时会自动从 URL 同步初始状态
|
|
||||||
4. 支持浏览器前进后退按钮
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
# 前端权限控制说明
|
|
||||||
|
|
||||||
本文档说明了前端如何根据后端权限设计实现权限控制。
|
|
||||||
|
|
||||||
## 权限等级
|
|
||||||
|
|
||||||
### 1. 普通用户 (Regular User)
|
|
||||||
- admin_type: "Regular User"
|
|
||||||
- problem_permission: "None"
|
|
||||||
- 只能访问前台功能,无法进入管理后台
|
|
||||||
|
|
||||||
### 2. 管理员 (Admin)
|
|
||||||
- admin_type: "Admin"
|
|
||||||
- problem_permission: "None" | "Own" | "All"
|
|
||||||
- 可以访问部分管理功能
|
|
||||||
|
|
||||||
### 3. 超级管理员 (Super Admin)
|
|
||||||
- admin_type: "Super Admin"
|
|
||||||
- problem_permission: "All" (自动设置)
|
|
||||||
- 可以访问所有管理功能
|
|
||||||
|
|
||||||
## 权限控制实现
|
|
||||||
|
|
||||||
### 1. 用户状态管理 (`src/shared/store/user.ts`)
|
|
||||||
```typescript
|
|
||||||
const isAdminRole = computed(() =>
|
|
||||||
user.value?.admin_type === USER_TYPE.ADMIN ||
|
|
||||||
user.value?.admin_type === USER_TYPE.SUPER_ADMIN
|
|
||||||
)
|
|
||||||
const isSuperAdmin = computed(() =>
|
|
||||||
user.value?.admin_type === USER_TYPE.SUPER_ADMIN
|
|
||||||
)
|
|
||||||
const hasProblemPermission = computed(() =>
|
|
||||||
user.value?.problem_permission !== PROBLEM_PERMISSION.NONE
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 权限工具函数 (`src/utils/permissions.ts`)
|
|
||||||
提供了一系列权限检查函数:
|
|
||||||
- `canManageUsers`: 只有super_admin可以管理用户
|
|
||||||
- `canManageProblems`: 需要题目权限
|
|
||||||
- `canManageSystemConfig`: 只有super_admin可以管理系统配置
|
|
||||||
- 等等...
|
|
||||||
|
|
||||||
### 3. 路由权限控制 (`src/main.ts`)
|
|
||||||
在路由守卫中检查权限:
|
|
||||||
- `requiresAdmin`: 需要admin或super_admin权限
|
|
||||||
- `requiresSuperAdmin`: 只有super_admin可以访问
|
|
||||||
- `requiresProblemPermission`: 需要题目管理权限
|
|
||||||
|
|
||||||
### 4. 菜单权限控制 (`src/shared/layout/admin.vue`)
|
|
||||||
根据用户权限动态显示菜单项:
|
|
||||||
- admin和super_admin都可以看到:题目管理
|
|
||||||
- 只有super_admin可以看到:比赛管理、用户管理、系统设置、公告管理、评论管理、教程管理
|
|
||||||
|
|
||||||
## 各功能模块权限说明
|
|
||||||
|
|
||||||
### 用户管理 (需要 super_admin)
|
|
||||||
- 创建、编辑、删除用户
|
|
||||||
- 重置用户密码
|
|
||||||
- 封禁/解封用户
|
|
||||||
- 批量导入用户
|
|
||||||
|
|
||||||
### 题目管理 (需要 problem_permission)
|
|
||||||
- `problem_permission = "None"`: 无法管理题目
|
|
||||||
- `problem_permission = "Own"`: 只能管理自己创建的题目
|
|
||||||
- `problem_permission = "All"`: 可以管理所有题目
|
|
||||||
|
|
||||||
### 比赛管理 (super_admin)
|
|
||||||
- 创建、编辑、删除比赛
|
|
||||||
- 管理比赛题目
|
|
||||||
- 查看比赛数据
|
|
||||||
|
|
||||||
### 系统配置 (需要 super_admin)
|
|
||||||
- SMTP邮件配置
|
|
||||||
- 判题服务器管理
|
|
||||||
- 网站基本设置
|
|
||||||
- 测试用例清理
|
|
||||||
|
|
||||||
### 公告管理 (需要 super_admin)
|
|
||||||
- 创建、编辑、删除公告
|
|
||||||
|
|
||||||
### 评论管理 (需要 super_admin)
|
|
||||||
- 查看、删除用户评论
|
|
||||||
|
|
||||||
### 教程管理 (需要 super_admin)
|
|
||||||
- 创建、编辑、删除教程
|
|
||||||
|
|
||||||
## 权限检查流程
|
|
||||||
|
|
||||||
1. **路由级权限检查**: 在 `main.ts` 的路由守卫中进行
|
|
||||||
2. **组件级权限检查**: 在组件内部使用 `usePermissions()` 进行检查
|
|
||||||
3. **UI权限控制**: 根据权限动态显示/隐藏UI元素
|
|
||||||
|
|
||||||
## 使用示例
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { usePermissions } from "~/utils/permissions"
|
|
||||||
|
|
||||||
const { canManageUsers, isSuperAdmin } = usePermissions()
|
|
||||||
|
|
||||||
// 权限检查
|
|
||||||
if (!canManageUsers.value) {
|
|
||||||
message.error("您没有权限访问此页面")
|
|
||||||
router.push("/admin")
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<!-- 根据权限显示不同内容 -->
|
|
||||||
<n-button v-if="isSuperAdmin" type="primary">
|
|
||||||
超级管理员专用功能
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **前端权限控制只是UI层面的限制**,真正的权限控制在后端
|
|
||||||
2. **所有API调用都会在后端进行权限验证**
|
|
||||||
3. **权限信息来自后端用户接口**,前端不应该缓存或修改权限信息
|
|
||||||
4. **路由权限检查是异步的**,需要等待用户信息加载完成
|
|
||||||
Reference in New Issue
Block a user