refactor(permission): 统一权限检查并移除重复代码
- 移除 material-list 页面中重复的 usePermission 导入和调用 - 权限检查完全由 ListItemActions 组件内部处理 - 简化 onView 函数直接调用 handleFileClick - 添加 ListItemActions 组件 README 文档 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Showing
9 changed files
with
1017 additions
and
40 deletions
| 1 | +## [2026-02-13] - 统一权限检查与移除重复代码 | ||
| 2 | + | ||
| 3 | +### 新增 | ||
| 4 | +- ListItemActions 组件集成权限检查逻辑 | ||
| 5 | + | ||
| 6 | +### 优化 | ||
| 7 | +- 移除 material-list 页面中重复的 usePermission 调用 | ||
| 8 | +- 权限检查完全由 ListItemActions 组件内部处理 | ||
| 9 | +- 添加 ListItemActions 组件 README 文档 | ||
| 10 | + | ||
| 11 | +--- | ||
| 12 | + | ||
| 13 | +**详细信息**: | ||
| 14 | +- **影响文件**: src/pages/material-list/index.vue, src/components/list/ListItemActions/index.vue, src/components/list/ListItemActions/README.md | ||
| 15 | +- **技术栈**: Vue 3, Taro 4 | ||
| 16 | +- **测试状态**: 已通过 | ||
| 17 | +- **备注**: 组件自包含业务逻辑模式,父组件无需重复权限检查 | ||
| 18 | + | ||
| 19 | +--- | ||
| 20 | + | ||
| 21 | +## [2026-02-13] - 资料查看权限与搜索页测试对齐 | ||
| 22 | + | ||
| 23 | +### 新增 | ||
| 24 | +- 统一动作级权限映射,支持页面查看权限扩展 | ||
| 25 | +- 资料查看入口增加登录权限校验与回跳路径记录 | ||
| 26 | + | ||
| 27 | +### 优化 | ||
| 28 | +- 搜索页测试对齐当前实现并补充接口 Mock | ||
| 29 | + | ||
| 30 | +--- | ||
| 31 | + | ||
| 32 | +**详细信息**: | ||
| 33 | +- **影响文件**: src/composables/usePermission.js, src/config/permissions.js, src/components/cards/MaterialCard.vue, src/pages/material-list/index.vue, src/pages/search/index.test.js, vitest.config.js, package.json | ||
| 34 | +- **技术栈**: Vue 3, Taro 4, Pinia, Vitest | ||
| 35 | +- **测试状态**: 已通过(pnpm test) | ||
| 36 | +- **备注**: lint 存在历史 warning 未处理 | ||
| 37 | + | ||
| 38 | +--- | ||
| 39 | + | ||
| 1 | ## [2026-02-13] - 我的页面消息红点显示 | 40 | ## [2026-02-13] - 我的页面消息红点显示 |
| 2 | 41 | ||
| 3 | ### 新增 | 42 | ### 新增 | ... | ... |
| 1 | +# ListItemActions 组件使用指南 | ||
| 2 | + | ||
| 3 | +## 📋 组件概述 | ||
| 4 | + | ||
| 5 | +**组件名称**: `ListItemActions` | ||
| 6 | +**文件路径**: `src/components/list/ListItemActions/index.vue` | ||
| 7 | +**用途**: 列表项的操作按钮组件,支持查看、收藏、删除三种操作 | ||
| 8 | + | ||
| 9 | +--- | ||
| 10 | + | ||
| 11 | +## ✨ 功能特性 | ||
| 12 | + | ||
| 13 | +### 1. 按钮类型 | ||
| 14 | + | ||
| 15 | +| 按钮 | 说明 | 依赖属性 | | ||
| 16 | +|------|------|----------| | ||
| 17 | +| **查看** | 查看文件/产品详情 | `viewable` | | ||
| 18 | +| **收藏** | 切换收藏状态 | `collectable` | | ||
| 19 | +| **删除** | 删除列表项 | `deletable` | | ||
| 20 | + | ||
| 21 | +### 2. 内置功能 | ||
| 22 | + | ||
| 23 | +- ✅ **权限检查**: 自动检查登录权限(通过 `usePermission`) | ||
| 24 | +- ✅ **埋点上报**: 权限通过后自动发送查看埋点(通过 `useEventTracking`) | ||
| 25 | +- ✅ **状态管理**: 收藏按钮根据 `collected` 状态切换样式 | ||
| 26 | + | ||
| 27 | +--- | ||
| 28 | + | ||
| 29 | +## 🎯 使用方法 | ||
| 30 | + | ||
| 31 | +### 基础用法 | ||
| 32 | + | ||
| 33 | +```vue | ||
| 34 | +<template> | ||
| 35 | + <ListItemActions | ||
| 36 | + :viewable="true" | ||
| 37 | + :collectable="true" | ||
| 38 | + :deletable="false" | ||
| 39 | + :collected="item.collected" | ||
| 40 | + :item-id="item.id" | ||
| 41 | + @view="handleView" | ||
| 42 | + @collect="handleCollect" | ||
| 43 | + @delete="handleDelete" | ||
| 44 | + /> | ||
| 45 | +</template> | ||
| 46 | + | ||
| 47 | +<script setup> | ||
| 48 | +const handleView = () => { | ||
| 49 | + // 权限检查通过后自动触发 | ||
| 50 | + // 不需要手动检查登录状态 | ||
| 51 | + console.log('查看文件') | ||
| 52 | +} | ||
| 53 | + | ||
| 54 | +const handleCollect = () => { | ||
| 55 | + // 收藏状态由组件内部管理 | ||
| 56 | + // 父组件可以监听 collectChanged 事件 | ||
| 57 | + console.log('切换收藏') | ||
| 58 | +} | ||
| 59 | + | ||
| 60 | +const handleDelete = () => { | ||
| 61 | + // 触发删除操作 | ||
| 62 | + console.log('删除项目') | ||
| 63 | +} | ||
| 64 | +</script> | ||
| 65 | +``` | ||
| 66 | + | ||
| 67 | +--- | ||
| 68 | + | ||
| 69 | +## 📝 Props 说明 | ||
| 70 | + | ||
| 71 | +| 属性 | 类型 | 默认值 | 说明 | | ||
| 72 | +|------|------|---------|------| | ||
| 73 | +| `viewable` | Boolean | `true` | 是否显示查看按钮 | | ||
| 74 | +| `collectable` | Boolean | `false` | 是否显示收藏按钮 | | ||
| 75 | +| `deletable` | Boolean | `false` | 是否显示删除按钮 | | ||
| 76 | +| `collected` | Boolean | `false` | 是否已收藏(影响收藏按钮样式) | | ||
| 77 | +| `itemId` | String | `''` | 关联对象 ID,用于埋点上报 | | ||
| 78 | + | ||
| 79 | +### 使用示例 | ||
| 80 | + | ||
| 81 | +```vue | ||
| 82 | +<!-- 只显示查看和删除 --> | ||
| 83 | +<ListItemActions | ||
| 84 | + :viewable="true" | ||
| 85 | + :deletable="true" | ||
| 86 | + :item-id="doc123" | ||
| 87 | + @view="onView" | ||
| 88 | + @delete="onDelete" | ||
| 89 | +/> | ||
| 90 | + | ||
| 91 | +<!-- 三个按钮都显示 --> | ||
| 92 | +<ListItemActions | ||
| 93 | + :viewable="true" | ||
| 94 | + :collectable="true" | ||
| 95 | + :deletable="true" | ||
| 96 | + :collected="true" | ||
| 97 | + :item-id="product456" | ||
| 98 | + @view="onView" | ||
| 99 | + @collect="onCollect" | ||
| 100 | + @delete="onDelete" | ||
| 101 | +/> | ||
| 102 | +``` | ||
| 103 | + | ||
| 104 | +--- | ||
| 105 | + | ||
| 106 | +## 📤 事件说明 | ||
| 107 | + | ||
| 108 | +### @view | ||
| 109 | + | ||
| 110 | +**触发时机**: 权限检查通过后 | ||
| 111 | + | ||
| 112 | +**注意**: 如果用户未登录,会先显示登录弹框,登录成功后才会触发此事件 | ||
| 113 | + | ||
| 114 | +```vue | ||
| 115 | +<script setup> | ||
| 116 | +const handleView = (itemData) => { | ||
| 117 | + console.log('执行查看逻辑:', itemData) | ||
| 118 | + // 这里调用实际的文件打开逻辑 | ||
| 119 | +} | ||
| 120 | +</script> | ||
| 121 | + | ||
| 122 | +<template> | ||
| 123 | + <ListItemActions @view="handleView" /> | ||
| 124 | +</template> | ||
| 125 | +``` | ||
| 126 | + | ||
| 127 | +### @collect | ||
| 128 | + | ||
| 129 | +**触发时机**: 点击收藏按钮时立即触发 | ||
| 130 | + | ||
| 131 | +```vue | ||
| 132 | +<script setup> | ||
| 133 | +const handleCollect = () => { | ||
| 134 | + // 切换收藏状态 | ||
| 135 | + // 实际的收藏状态由 collected 属性控制 | ||
| 136 | +} | ||
| 137 | +</script> | ||
| 138 | + | ||
| 139 | +<template> | ||
| 140 | + <ListItemActions @collect="handleCollect" /> | ||
| 141 | +</template> | ||
| 142 | +``` | ||
| 143 | + | ||
| 144 | +### @delete | ||
| 145 | + | ||
| 146 | +**触发时机**: 点击删除按钮时立即触发 | ||
| 147 | + | ||
| 148 | +```vue | ||
| 149 | +<script setup> | ||
| 150 | +const handleDelete = () => { | ||
| 151 | + // 执行删除操作 | ||
| 152 | + // 调用删除 API 等 | ||
| 153 | +} | ||
| 154 | +</script> | ||
| 155 | + | ||
| 156 | +<template> | ||
| 157 | + <ListItemActions @delete="handleDelete" /> | ||
| 158 | +</template> | ||
| 159 | +``` | ||
| 160 | + | ||
| 161 | +--- | ||
| 162 | + | ||
| 163 | +## 🔐 权限和埋点流程 | ||
| 164 | + | ||
| 165 | +### 查看按钮完整流程 | ||
| 166 | + | ||
| 167 | +``` | ||
| 168 | +用户点击"查看"按钮 | ||
| 169 | + ↓ | ||
| 170 | +组件内部: requireAction('view_material', callback) | ||
| 171 | + ↓ | ||
| 172 | +┌─ 未登录 ──────────────────────────────┐ | ||
| 173 | +│ │ | ||
| 174 | +│ 1. 检查登录状态 │ | ||
| 175 | +│ 2. 显示登录弹框 │ | ||
| 176 | +│ 3. ❌ 不发送埋点 │ | ||
| 177 | +│ 4. ❌ 不触发 @view 事件 │ | ||
| 178 | +│ │ | ||
| 179 | +└─────────────────────────────────────────┘ | ||
| 180 | + ↓ | ||
| 181 | +┌─ 已登录 ──────────────────────────────┐ | ||
| 182 | +│ │ | ||
| 183 | +│ 1. 权限检查通过 │ | ||
| 184 | +│ 2. ✅ 发送埋点 (trackFileRead) │ | ||
| 185 | +│ 3. ✅ 触发 @view 事件 │ | ||
| 186 | +│ │ | ||
| 187 | +└─────────────────────────────────────────┘ | ||
| 188 | + ↓ | ||
| 189 | +父组件收到 @view 事件 | ||
| 190 | + ↓ | ||
| 191 | +执行实际业务逻辑(打开文件等) | ||
| 192 | +``` | ||
| 193 | + | ||
| 194 | +### 权限配置 | ||
| 195 | + | ||
| 196 | +权限配置位于 `src/config/permissions.js`: | ||
| 197 | + | ||
| 198 | +```javascript | ||
| 199 | +export const ACTION_PERMISSIONS = { | ||
| 200 | + view_material: { | ||
| 201 | + permission_type: PermissionType.LOGIN, | ||
| 202 | + options: { | ||
| 203 | + content: '请先登录后查看资料', | ||
| 204 | + confirmText: '去登录' | ||
| 205 | + } | ||
| 206 | + } | ||
| 207 | + } | ||
| 208 | +} | ||
| 209 | +``` | ||
| 210 | + | ||
| 211 | +### 埋点配置 | ||
| 212 | + | ||
| 213 | +埋点配置位于 `src/composables/useEventTracking.js`: | ||
| 214 | + | ||
| 215 | +- **事件类型**: `READ_FILE` | ||
| 216 | +- **上报时机**: 权限检查通过后立即上报 | ||
| 217 | +- **上报数据**: `{ type: 'READ_FILE', object_id: itemId }` | ||
| 218 | + | ||
| 219 | +--- | ||
| 220 | + | ||
| 221 | +## 🎨 样式说明 | ||
| 222 | + | ||
| 223 | +### TailwindCSS 类名 | ||
| 224 | + | ||
| 225 | +| 类名 | 作用 | | ||
| 226 | +|------|------| | ||
| 227 | +| `flex justify-end gap-[24rpx]` | 右对齐,24rpx 间距 | | ||
| 228 | +| `flex items-center` | 垂直居中 | | ||
| 229 | +| `text-blue-600` | 蓝色文字(查看按钮) | | ||
| 230 | +| `text-red-500` | 红色文字(删除按钮) | | ||
| 231 | +| `text-red-500` / `text-gray-400` | 收藏按钮动态颜色 | | ||
| 232 | +| `text-[24rpx]` | 字体大小 | | ||
| 233 | + | ||
| 234 | +### 自定义样式 | ||
| 235 | + | ||
| 236 | +如果需要覆盖默认样式,可以在父组件中使用: | ||
| 237 | + | ||
| 238 | +```vue | ||
| 239 | +<template> | ||
| 240 | + <ListItemActions | ||
| 241 | + class="custom-actions" | ||
| 242 | + @view="handleView" | ||
| 243 | + /> | ||
| 244 | +</template> | ||
| 245 | + | ||
| 246 | +<style scoped> | ||
| 247 | +.custom-actions :deep(.text-blue-600) { | ||
| 248 | + color: #custom-color; | ||
| 249 | +} | ||
| 250 | +</style> | ||
| 251 | +``` | ||
| 252 | + | ||
| 253 | +--- | ||
| 254 | + | ||
| 255 | +## ⚠️ 注意事项 | ||
| 256 | + | ||
| 257 | +### 1. 权限检查是自动的 | ||
| 258 | + | ||
| 259 | +不需要手动检查登录状态,组件内部已处理: | ||
| 260 | + | ||
| 261 | +```vue | ||
| 262 | +<!-- ❌ 错误:手动检查权限 --> | ||
| 263 | +<script setup> | ||
| 264 | +import { usePermission } from '@/composables/usePermission' | ||
| 265 | + | ||
| 266 | +const { isLoggedIn } = usePermission() | ||
| 267 | + | ||
| 268 | +const handleView = () => { | ||
| 269 | + if (!isLoggedIn()) { | ||
| 270 | + showToast('请先登录') | ||
| 271 | + return | ||
| 272 | + } | ||
| 273 | + // 继续逻辑 | ||
| 274 | +} | ||
| 275 | +</script> | ||
| 276 | + | ||
| 277 | +<!-- ✅ 正确:让组件处理 --> | ||
| 278 | +<script setup> | ||
| 279 | +const handleView = () => { | ||
| 280 | + // 直接执行,权限检查在组件内部 | ||
| 281 | +} | ||
| 282 | +</script> | ||
| 283 | +``` | ||
| 284 | + | ||
| 285 | +### 2. 埋点需要 itemId | ||
| 286 | + | ||
| 287 | +如果不上报埋点,确保传递了 `item-id` 属性: | ||
| 288 | + | ||
| 289 | +```vue | ||
| 290 | +<!-- ❌ 缺少 itemId,埋点不会上报 --> | ||
| 291 | +<ListItemActions @view="handleView" /> | ||
| 292 | + | ||
| 293 | +<!-- ✅ 正确:提供 itemId --> | ||
| 294 | +<ListItemActions :item-id="'123456'" @view="handleView" /> | ||
| 295 | +``` | ||
| 296 | + | ||
| 297 | +### 3. 父组件的 @view 事件参数 | ||
| 298 | + | ||
| 299 | +`@view` 事件目前不传递参数,如果需要文件信息,请: | ||
| 300 | + | ||
| 301 | +1. 在父组件中维护数据对象引用 | ||
| 302 | +2. 或者扩展组件支持传递完整 item 数据 | ||
| 303 | + | ||
| 304 | +```vue | ||
| 305 | +<!-- 父组件示例 --> | ||
| 306 | +<script setup> | ||
| 307 | +const currentItem = ref(null) | ||
| 308 | + | ||
| 309 | +const handleView = () => { | ||
| 310 | + // currentItem 已在组件内部设置 | ||
| 311 | + console.log('查看:', currentItem.value) | ||
| 312 | +} | ||
| 313 | +</script> | ||
| 314 | + | ||
| 315 | +<template> | ||
| 316 | + <ListItemActions @view="handleView" /> | ||
| 317 | +</template> | ||
| 318 | +``` | ||
| 319 | + | ||
| 320 | +--- | ||
| 321 | + | ||
| 322 | +## 🔧 扩展组件 | ||
| 323 | + | ||
| 324 | +### 添加新的按钮类型 | ||
| 325 | + | ||
| 326 | +如果需要添加新的操作按钮(如"分享"、"下载"等): | ||
| 327 | + | ||
| 328 | +1. 在 `props` 中添加新的控制属性 | ||
| 329 | +2. 在模板中添加按钮 UI | ||
| 330 | +3. 添加对应的 emit 和 handler | ||
| 331 | + | ||
| 332 | +```javascript | ||
| 333 | +// 示例:添加分享按钮 | ||
| 334 | +const props = defineProps({ | ||
| 335 | + // ... 现有属性 | ||
| 336 | + shareable: { | ||
| 337 | + type: Boolean, | ||
| 338 | + default: false | ||
| 339 | + } | ||
| 340 | +}) | ||
| 341 | + | ||
| 342 | +const emit = defineEmits({ | ||
| 343 | + // ... 现有事件 | ||
| 344 | + share: null | ||
| 345 | +}) | ||
| 346 | + | ||
| 347 | +const handleShare = () => { | ||
| 348 | + emit('share') | ||
| 349 | +} | ||
| 350 | +``` | ||
| 351 | + | ||
| 352 | +--- | ||
| 353 | + | ||
| 354 | +## 📚 相关文件 | ||
| 355 | + | ||
| 356 | +- **权限配置**: `src/config/permissions.js` | ||
| 357 | +- **权限 Composable**: `src/composables/usePermission.js` | ||
| 358 | +- **埋点 Composable**: `src/composables/useEventTracking.js` | ||
| 359 | +- **收藏操作**: `src/composables/useCollectOperation.js` |
| ... | @@ -40,6 +40,7 @@ | ... | @@ -40,6 +40,7 @@ |
| 40 | import { computed } from 'vue' | 40 | import { computed } from 'vue' |
| 41 | import IconFont from '@/components/icons/IconFont.vue' | 41 | import IconFont from '@/components/icons/IconFont.vue' |
| 42 | import { useEventTracking } from '@/composables/useEventTracking' | 42 | import { useEventTracking } from '@/composables/useEventTracking' |
| 43 | +import { usePermission } from '@/composables/usePermission' | ||
| 43 | 44 | ||
| 44 | /** | 45 | /** |
| 45 | * 组件属性 | 46 | * 组件属性 |
| ... | @@ -114,18 +115,23 @@ const isCollected = computed(() => props.collected) | ... | @@ -114,18 +115,23 @@ const isCollected = computed(() => props.collected) |
| 114 | // 初始化埋点功能 | 115 | // 初始化埋点功能 |
| 115 | const { trackFileRead } = useEventTracking() | 116 | const { trackFileRead } = useEventTracking() |
| 116 | 117 | ||
| 118 | +// 初始化权限检查 | ||
| 119 | +const { requireAction } = usePermission() | ||
| 120 | + | ||
| 117 | /** | 121 | /** |
| 118 | * 处理查看点击 | 122 | * 处理查看点击 |
| 119 | * | 123 | * |
| 120 | - * @description 触发查看事件,并自动发送埋点数据 | 124 | + * @description 先检查权限,通过后发送埋点并通知父组件 |
| 121 | */ | 125 | */ |
| 122 | const handleView = () => { | 126 | const handleView = () => { |
| 123 | - // 如果提供了 itemId,自动发送埋点 | 127 | + requireAction('view_material', () => { |
| 124 | - if (props.itemId) { | 128 | + // 权限检查通过后,发送埋点 |
| 125 | - trackFileRead(props.itemId) | 129 | + if (props.itemId) { |
| 126 | - } | 130 | + trackFileRead(props.itemId) |
| 127 | - | 131 | + } |
| 128 | - emit('view') | 132 | + // 通知父组件执行实际业务逻辑 |
| 133 | + emit('view') | ||
| 134 | + }) | ||
| 129 | } | 135 | } |
| 130 | 136 | ||
| 131 | /** | 137 | /** | ... | ... |
src/composables/usePermission.js
0 → 100644
| 1 | +/** | ||
| 2 | + * 通用权限检查 Composable | ||
| 3 | + * | ||
| 4 | + * @description 提供统一的权限检查逻辑,支持登录、VIP 等多种权限类型 | ||
| 5 | + * @module composables/usePermission | ||
| 6 | + * @author Claude Code | ||
| 7 | + * @created 2026-02-13 | ||
| 8 | + * | ||
| 9 | + * @example | ||
| 10 | + * // 基础使用 - 检查登录权限 | ||
| 11 | + * const { requireLogin } = usePermission() | ||
| 12 | + * | ||
| 13 | + * requireLogin(() => { | ||
| 14 | + * // 已登录时执行的操作 | ||
| 15 | + * openDocument() | ||
| 16 | + * }) | ||
| 17 | + * | ||
| 18 | + * @example | ||
| 19 | + * // 高级使用 - 自定义提示文案 | ||
| 20 | + * const { checkPermission } = usePermission() | ||
| 21 | + * | ||
| 22 | + * checkPermission(PermissionType.LOGIN, callback, { | ||
| 23 | + * content: '请先登录后查看完整内容', | ||
| 24 | + * confirmText: '立即登录' | ||
| 25 | + * }) | ||
| 26 | + */ | ||
| 27 | + | ||
| 28 | +import { useUserStore } from '@/stores/user' | ||
| 29 | +import Taro from '@tarojs/taro' | ||
| 30 | +import { routerStore } from '@/stores/router' | ||
| 31 | +import { PermissionType, getPermissionConfig, getActionPermissionConfig } from '@/config/permissions' | ||
| 32 | + | ||
| 33 | +/** | ||
| 34 | + * 通用权限检查 Hook | ||
| 35 | + * | ||
| 36 | + * @description 提供权限检查、权限弹窗等功能 | ||
| 37 | + * @returns {Object} 权限检查方法集合 | ||
| 38 | + */ | ||
| 39 | +export function usePermission() { | ||
| 40 | + const userStore = useUserStore() | ||
| 41 | + | ||
| 42 | + /** | ||
| 43 | + * 检查登录状态 | ||
| 44 | + * @description 判断用户是否已登录 | ||
| 45 | + * @returns {boolean} 是否已登录 | ||
| 46 | + * | ||
| 47 | + * @example | ||
| 48 | + * const { isLoggedIn } = usePermission() | ||
| 49 | + * if (isLoggedIn()) { | ||
| 50 | + * console.log('用户已登录') | ||
| 51 | + * } | ||
| 52 | + */ | ||
| 53 | + const isLoggedIn = () => { | ||
| 54 | + return userStore.isLoggedIn | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + const getCurrentPageUrl = () => { | ||
| 58 | + const pages = Taro.getCurrentPages() | ||
| 59 | + const currentPage = pages[pages.length - 1] | ||
| 60 | + if (!currentPage || !currentPage.route) { | ||
| 61 | + return '' | ||
| 62 | + } | ||
| 63 | + const options = currentPage.options || {} | ||
| 64 | + const query = Object.keys(options) | ||
| 65 | + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(options[key])}`) | ||
| 66 | + .join('&') | ||
| 67 | + return `/${currentPage.route}${query ? `?${query}` : ''}` | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + /** | ||
| 71 | + * 通用权限检查 | ||
| 72 | + * @description 检查指定权限,无权限时弹窗提示,有权限时执行回调 | ||
| 73 | + * @param {string} permissionType - 权限类型(使用 PermissionType 枚举) | ||
| 74 | + * @param {Function} callback - 有权限时执行的回调函数 | ||
| 75 | + * @param {Object} customOptions - 自定义配置选项(会覆盖默认配置) | ||
| 76 | + * @returns {boolean} 是否有权限(true=有权限,false=无权限) | ||
| 77 | + * | ||
| 78 | + * @example | ||
| 79 | + * // 使用默认配置 | ||
| 80 | + * checkPermission(PermissionType.LOGIN, () => { | ||
| 81 | + * console.log('已登录,执行操作') | ||
| 82 | + * }) | ||
| 83 | + * | ||
| 84 | + * @example | ||
| 85 | + * // 自定义提示文案 | ||
| 86 | + * checkPermission(PermissionType.LOGIN, callback, { | ||
| 87 | + * content: '请先登录后查看完整内容', | ||
| 88 | + * confirmText: '立即登录' | ||
| 89 | + * }) | ||
| 90 | + */ | ||
| 91 | + const checkPermission = (permissionType, callback, customOptions = {}) => { | ||
| 92 | + console.log(`[usePermission] 检查权限: ${permissionType}`) | ||
| 93 | + | ||
| 94 | + // 根据权限类型执行不同的检查逻辑 | ||
| 95 | + switch (permissionType) { | ||
| 96 | + case PermissionType.LOGIN: | ||
| 97 | + return checkLoginPermission(callback, customOptions) | ||
| 98 | + | ||
| 99 | + case PermissionType.VIP: | ||
| 100 | + return checkVipPermission(callback, customOptions) | ||
| 101 | + | ||
| 102 | + case PermissionType.VERIFIED: | ||
| 103 | + return checkVerifiedPermission(callback, customOptions) | ||
| 104 | + | ||
| 105 | + default: | ||
| 106 | + console.warn(`[usePermission] 未知的权限类型: ${permissionType}`) | ||
| 107 | + return false | ||
| 108 | + } | ||
| 109 | + } | ||
| 110 | + | ||
| 111 | + /** | ||
| 112 | + * 检查登录权限 | ||
| 113 | + * @description 判断用户是否登录,未登录时弹窗引导 | ||
| 114 | + * @param {Function} callback - 已登录时执行的回调 | ||
| 115 | + * @param {Object} customOptions - 自定义配置选项 | ||
| 116 | + * @returns {boolean} 是否已登录 | ||
| 117 | + * @private | ||
| 118 | + */ | ||
| 119 | + const checkLoginPermission = (callback, customOptions = {}) => { | ||
| 120 | + if (isLoggedIn()) { | ||
| 121 | + // 已登录,直接执行回调 | ||
| 122 | + console.log('[usePermission] 用户已登录,执行回调') | ||
| 123 | + callback?.() | ||
| 124 | + return true | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + // 未登录,弹窗提示 | ||
| 128 | + console.log('[usePermission] 用户未登录,显示登录提示') | ||
| 129 | + showLoginModal(callback, customOptions) | ||
| 130 | + return false | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + /** | ||
| 134 | + * 检查 VIP 权限(预留) | ||
| 135 | + * @description 检查用户是否为 VIP,非 VIP 时弹窗引导 | ||
| 136 | + * @param {Function} callback - 有权限时执行的回调 | ||
| 137 | + * @param {Object} customOptions - 自定义配置选项 | ||
| 138 | + * @returns {boolean} 是否有权限 | ||
| 139 | + * @private | ||
| 140 | + */ | ||
| 141 | + const checkVipPermission = (callback, customOptions = {}) => { | ||
| 142 | + // 预留:检查 VIP 状态 | ||
| 143 | + // const isVip = userStore.userInfo?.is_vip | ||
| 144 | + const isVip = true // 暂时返回 true | ||
| 145 | + | ||
| 146 | + if (isVip) { | ||
| 147 | + callback?.() | ||
| 148 | + return true | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + showPermissionModal(PermissionType.VIP, callback, customOptions) | ||
| 152 | + return false | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + /** | ||
| 156 | + * 检查实名认证权限(预留) | ||
| 157 | + * @description 检查用户是否实名认证,未认证时弹窗引导 | ||
| 158 | + * @param {Function} callback - 有权限时执行的回调 | ||
| 159 | + * @param {Object} customOptions - 自定义配置选项 | ||
| 160 | + * @returns {boolean} 是否有权限 | ||
| 161 | + * @private | ||
| 162 | + */ | ||
| 163 | + const checkVerifiedPermission = (callback, customOptions = {}) => { | ||
| 164 | + // 预留:检查实名认证状态 | ||
| 165 | + // const isVerified = userStore.userInfo?.is_verified | ||
| 166 | + const isVerified = true // 暂时返回 true | ||
| 167 | + | ||
| 168 | + if (isVerified) { | ||
| 169 | + callback?.() | ||
| 170 | + return true | ||
| 171 | + } | ||
| 172 | + | ||
| 173 | + showPermissionModal(PermissionType.VERIFIED, callback, customOptions) | ||
| 174 | + return false | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + /** | ||
| 178 | + * 显示登录弹窗 | ||
| 179 | + * @description 显示登录引导弹窗,点击确定跳转登录页 | ||
| 180 | + * @param {Function} callback - 用户登录成功后希望执行的回调(用于登录后返回) | ||
| 181 | + * @param {Object} customOptions - 自定义配置选项 | ||
| 182 | + * @private | ||
| 183 | + */ | ||
| 184 | + const showLoginModal = (callback, customOptions = {}) => { | ||
| 185 | + const config = getPermissionConfig(PermissionType.LOGIN, customOptions) | ||
| 186 | + | ||
| 187 | + Taro.showModal({ | ||
| 188 | + title: config.title, | ||
| 189 | + content: config.content, | ||
| 190 | + confirmText: config.confirmText, | ||
| 191 | + cancelText: config.cancelText, | ||
| 192 | + success: (res) => { | ||
| 193 | + if (res.confirm) { | ||
| 194 | + // 用户点击"去登录" | ||
| 195 | + console.log('[usePermission] 用户选择去登录') | ||
| 196 | + const store = routerStore() | ||
| 197 | + const currentUrl = getCurrentPageUrl() | ||
| 198 | + if (currentUrl) { | ||
| 199 | + store.add(currentUrl) | ||
| 200 | + } | ||
| 201 | + goToLoginPage() | ||
| 202 | + } else { | ||
| 203 | + // 用户点击"暂不登录" | ||
| 204 | + console.log('[usePermission] 用户取消登录') | ||
| 205 | + } | ||
| 206 | + } | ||
| 207 | + }) | ||
| 208 | + } | ||
| 209 | + | ||
| 210 | + /** | ||
| 211 | + * 显示权限提示弹窗(通用) | ||
| 212 | + * @description 显示权限不足的提示弹窗 | ||
| 213 | + * @param {string} permissionType - 权限类型 | ||
| 214 | + * @param {Function} callback - 有权限时执行的回调 | ||
| 215 | + * @param {Object} customOptions - 自定义配置选项 | ||
| 216 | + * @private | ||
| 217 | + */ | ||
| 218 | + const showPermissionModal = (permissionType, callback, customOptions = {}) => { | ||
| 219 | + const config = getPermissionConfig(permissionType, customOptions) | ||
| 220 | + | ||
| 221 | + Taro.showModal({ | ||
| 222 | + title: config.title, | ||
| 223 | + content: config.content, | ||
| 224 | + confirmText: config.confirmText, | ||
| 225 | + cancelText: config.cancelText, | ||
| 226 | + success: (res) => { | ||
| 227 | + if (res.confirm) { | ||
| 228 | + console.log(`[usePermission] 用户确认权限提示: ${permissionType}`) | ||
| 229 | + // 根据权限类型执行不同操作 | ||
| 230 | + handlePermissionAction(permissionType) | ||
| 231 | + } | ||
| 232 | + } | ||
| 233 | + }) | ||
| 234 | + } | ||
| 235 | + | ||
| 236 | + /** | ||
| 237 | + * 处理权限确认后的操作 | ||
| 238 | + * @description 根据权限类型跳转到对应页面 | ||
| 239 | + * @param {string} permissionType - 权限类型 | ||
| 240 | + * @private | ||
| 241 | + */ | ||
| 242 | + const handlePermissionAction = (permissionType) => { | ||
| 243 | + switch (permissionType) { | ||
| 244 | + case PermissionType.LOGIN: | ||
| 245 | + goToLoginPage() | ||
| 246 | + break | ||
| 247 | + | ||
| 248 | + case PermissionType.VIP: | ||
| 249 | + // 跳转到 VIP 开通页面 | ||
| 250 | + console.log('[usePermission] 跳转到 VIP 开通页面') | ||
| 251 | + break | ||
| 252 | + | ||
| 253 | + case PermissionType.VERIFIED: | ||
| 254 | + // 跳转到实名认证页面 | ||
| 255 | + console.log('[usePermission] 跳转到实名认证页面') | ||
| 256 | + break | ||
| 257 | + } | ||
| 258 | + } | ||
| 259 | + | ||
| 260 | + /** | ||
| 261 | + * 跳转到登录页 | ||
| 262 | + * @description 跳转到登录页面 | ||
| 263 | + * @private | ||
| 264 | + */ | ||
| 265 | + const goToLoginPage = () => { | ||
| 266 | + Taro.navigateTo({ | ||
| 267 | + url: '/pages/login/index' | ||
| 268 | + }) | ||
| 269 | + } | ||
| 270 | + | ||
| 271 | + /** | ||
| 272 | + * 便捷方法:要求登录权限 | ||
| 273 | + * @description 专门用于检查登录权限的便捷方法 | ||
| 274 | + * @param {Function} callback - 已登录时执行的回调 | ||
| 275 | + * @param {Object} customOptions - 自定义配置选项 | ||
| 276 | + * @returns {boolean} 是否已登录 | ||
| 277 | + * | ||
| 278 | + * @example | ||
| 279 | + * const { requireLogin } = usePermission() | ||
| 280 | + * | ||
| 281 | + * // 查看资料需要登录 | ||
| 282 | + * onView(item) { | ||
| 283 | + * requireLogin(() => { | ||
| 284 | + * openDocument(item.url) | ||
| 285 | + * }) | ||
| 286 | + * } | ||
| 287 | + */ | ||
| 288 | + const requireLogin = (callback, customOptions = {}) => { | ||
| 289 | + return checkPermission(PermissionType.LOGIN, callback, customOptions) | ||
| 290 | + } | ||
| 291 | + | ||
| 292 | + const requireAction = (action, callback, customOptions = {}) => { | ||
| 293 | + const actionConfig = getActionPermissionConfig(action, customOptions) | ||
| 294 | + return checkPermission(actionConfig.permission_type, callback, actionConfig.options) | ||
| 295 | + } | ||
| 296 | + | ||
| 297 | + /** | ||
| 298 | + * 便捷方法:静默检查权限(不弹窗) | ||
| 299 | + * @description 只检查权限状态,不弹窗提示 | ||
| 300 | + * @param {string} permissionType - 权限类型 | ||
| 301 | + * @returns {boolean} 是否有权限 | ||
| 302 | + * | ||
| 303 | + * @example | ||
| 304 | + * const { hasPermission } = usePermission() | ||
| 305 | + * | ||
| 306 | + * if (hasPermission(PermissionType.LOGIN)) { | ||
| 307 | + * console.log('用户已登录') | ||
| 308 | + * } | ||
| 309 | + */ | ||
| 310 | + const hasPermission = (permissionType) => { | ||
| 311 | + switch (permissionType) { | ||
| 312 | + case PermissionType.LOGIN: | ||
| 313 | + return isLoggedIn() | ||
| 314 | + case PermissionType.VIP: | ||
| 315 | + return true // 暂时返回 true | ||
| 316 | + case PermissionType.VERIFIED: | ||
| 317 | + return true // 暂时返回 true | ||
| 318 | + default: | ||
| 319 | + return false | ||
| 320 | + } | ||
| 321 | + } | ||
| 322 | + | ||
| 323 | + // ========== 返回 ========== | ||
| 324 | + return { | ||
| 325 | + /** 核心方法:通用权限检查 */ | ||
| 326 | + checkPermission, | ||
| 327 | + | ||
| 328 | + /** 便捷方法:检查登录权限(最常用) */ | ||
| 329 | + requireLogin, | ||
| 330 | + | ||
| 331 | + requireAction, | ||
| 332 | + | ||
| 333 | + /** 工具方法:静默检查是否有权限(不弹窗) */ | ||
| 334 | + hasPermission, | ||
| 335 | + | ||
| 336 | + /** 工具方法:获取当前登录状态 */ | ||
| 337 | + isLoggedIn | ||
| 338 | + } | ||
| 339 | +} |
src/composables/usePermission.test.js
0 → 100644
| 1 | +import { describe, it, expect, vi, beforeEach } from 'vitest' | ||
| 2 | +import { usePermission } from './usePermission' | ||
| 3 | +import { getActionPermissionConfig } from '@/config/permissions' | ||
| 4 | + | ||
| 5 | +const show_modal_mock = vi.fn() | ||
| 6 | +const navigate_to_mock = vi.fn() | ||
| 7 | +const get_current_pages_mock = vi.fn() | ||
| 8 | +const add_mock = vi.fn() | ||
| 9 | +let user_state = { isLoggedIn: false } | ||
| 10 | + | ||
| 11 | +vi.mock('@tarojs/taro', () => ({ | ||
| 12 | + default: { | ||
| 13 | + showModal: (...args) => show_modal_mock(...args), | ||
| 14 | + navigateTo: (...args) => navigate_to_mock(...args), | ||
| 15 | + getCurrentPages: () => get_current_pages_mock() | ||
| 16 | + } | ||
| 17 | +})) | ||
| 18 | + | ||
| 19 | +vi.mock('@/stores/user', () => ({ | ||
| 20 | + useUserStore: () => user_state | ||
| 21 | +})) | ||
| 22 | + | ||
| 23 | +vi.mock('@/stores/router', () => ({ | ||
| 24 | + routerStore: () => ({ | ||
| 25 | + add: add_mock | ||
| 26 | + }) | ||
| 27 | +})) | ||
| 28 | + | ||
| 29 | +describe('usePermission', () => { | ||
| 30 | + beforeEach(() => { | ||
| 31 | + show_modal_mock.mockReset() | ||
| 32 | + navigate_to_mock.mockReset() | ||
| 33 | + get_current_pages_mock.mockReset() | ||
| 34 | + add_mock.mockReset() | ||
| 35 | + user_state = { isLoggedIn: false } | ||
| 36 | + }) | ||
| 37 | + | ||
| 38 | + it('requireAction 登录状态下执行回调', () => { | ||
| 39 | + user_state.isLoggedIn = true | ||
| 40 | + const callback = vi.fn() | ||
| 41 | + const { requireAction } = usePermission() | ||
| 42 | + const result = requireAction('view_material', callback) | ||
| 43 | + expect(result).toBe(true) | ||
| 44 | + expect(callback).toHaveBeenCalledTimes(1) | ||
| 45 | + expect(show_modal_mock).not.toHaveBeenCalled() | ||
| 46 | + }) | ||
| 47 | + | ||
| 48 | + it('requireAction 未登录时保存回跳并跳转登录', () => { | ||
| 49 | + get_current_pages_mock.mockReturnValue([ | ||
| 50 | + { route: 'pages/material-list/index', options: { id: '1', title: '资料' } } | ||
| 51 | + ]) | ||
| 52 | + show_modal_mock.mockImplementation((options) => { | ||
| 53 | + options.success({ confirm: true }) | ||
| 54 | + }) | ||
| 55 | + const { requireAction } = usePermission() | ||
| 56 | + const result = requireAction('view_material', () => {}) | ||
| 57 | + expect(result).toBe(false) | ||
| 58 | + expect(show_modal_mock).toHaveBeenCalledTimes(1) | ||
| 59 | + expect(add_mock).toHaveBeenCalledWith('/pages/material-list/index?id=1&title=%E8%B5%84%E6%96%99') | ||
| 60 | + expect(navigate_to_mock).toHaveBeenCalledWith({ url: '/pages/login/index' }) | ||
| 61 | + }) | ||
| 62 | +}) | ||
| 63 | + | ||
| 64 | +describe('getActionPermissionConfig', () => { | ||
| 65 | + it('未知 action 返回默认登录配置', () => { | ||
| 66 | + const result = getActionPermissionConfig('unknown_action', { content: 'x' }) | ||
| 67 | + expect(result.permission_type).toBe('login') | ||
| 68 | + expect(result.options.content).toBe('x') | ||
| 69 | + }) | ||
| 70 | +}) |
| 1 | /** | 1 | /** |
| 2 | - * 计划书权限检查 Composable | 2 | + * 计划书权限检查 Composable(重构版) |
| 3 | * | 3 | * |
| 4 | - * @description 统一处理制作计划书的登录权限检查 | 4 | + * @description 统一处理制作计划书的登录权限检查,内部调用通用 usePermission |
| 5 | * @module composables/usePlanPermission | 5 | * @module composables/usePlanPermission |
| 6 | * @author Claude Code | 6 | * @author Claude Code |
| 7 | * @created 2026-02-12 | 7 | * @created 2026-02-12 |
| 8 | + * @updated 2026-02-13 - 重构为使用通用 usePermission | ||
| 8 | */ | 9 | */ |
| 9 | 10 | ||
| 10 | -import { useUserStore } from '@/stores/user' | 11 | +import { usePermission } from '@/composables/usePermission' |
| 11 | -import Taro from '@tarojs/taro' | ||
| 12 | 12 | ||
| 13 | /** | 13 | /** |
| 14 | * 计划书权限检查 Hook | 14 | * 计划书权限检查 Hook |
| ... | @@ -26,48 +26,37 @@ import Taro from '@tarojs/taro' | ... | @@ -26,48 +26,37 @@ import Taro from '@tarojs/taro' |
| 26 | * }) | 26 | * }) |
| 27 | */ | 27 | */ |
| 28 | export function usePlanPermission() { | 28 | export function usePlanPermission() { |
| 29 | - const userStore = useUserStore() | 29 | + // 获取通用权限检查方法 |
| 30 | + const { requireLogin } = usePermission() | ||
| 30 | 31 | ||
| 31 | /** | 32 | /** |
| 32 | * 检查计划书权限 | 33 | * 检查计划书权限 |
| 33 | * | 34 | * |
| 34 | * @description 判断用户是否登录,未登录时提示并引导登录,已登录时执行回调 | 35 | * @description 判断用户是否登录,未登录时提示并引导登录,已登录时执行回调 |
| 35 | * @param {Function} callback - 已登录时执行的回调函数 | 36 | * @param {Function} callback - 已登录时执行的回调函数 |
| 37 | + * @param {Object} customOptions - 自定义配置选项(可选) | ||
| 36 | * @returns {boolean} 是否有权限(true=已登录,false=未登录) | 38 | * @returns {boolean} 是否有权限(true=已登录,false=未登录) |
| 37 | * | 39 | * |
| 38 | * @example | 40 | * @example |
| 41 | + * // 使用默认配置 | ||
| 39 | * const hasPermission = checkPlanPermission(() => { | 42 | * const hasPermission = checkPlanPermission(() => { |
| 40 | * console.log('用户已登录,可以制作计划书') | 43 | * console.log('用户已登录,可以制作计划书') |
| 41 | * }) | 44 | * }) |
| 45 | + * | ||
| 46 | + * @example | ||
| 47 | + * // 自定义提示文案 | ||
| 48 | + * const hasPermission = checkPlanPermission(() => { | ||
| 49 | + * openPlanPopup(productId) | ||
| 50 | + * }, { | ||
| 51 | + * content: '请先登录后制作专属计划书', | ||
| 52 | + * confirmText: '立即登录' | ||
| 53 | + * }) | ||
| 42 | */ | 54 | */ |
| 43 | - const checkPlanPermission = (callback) => { | 55 | + const checkPlanPermission = (callback, customOptions = {}) => { |
| 44 | - console.log('[usePlanPermission] 检查权限,当前登录状态:', userStore.isLoggedIn) | 56 | + console.log('[usePlanPermission] 检查计划书权限') |
| 45 | - // 检查登录状态 | ||
| 46 | - if (!userStore.isLoggedIn) { | ||
| 47 | - console.log('[usePlanPermission] 用户未登录,显示登录提示') | ||
| 48 | - // 未登录,显示提示框 | ||
| 49 | - Taro.showModal({ | ||
| 50 | - title: '提示', | ||
| 51 | - content: '请先登录后再制作计划书', | ||
| 52 | - confirmText: '去登录', | ||
| 53 | - cancelText: '取消', | ||
| 54 | - success: (res) => { | ||
| 55 | - if (res.confirm) { | ||
| 56 | - // 用户点击"去登录",跳转到登录页 | ||
| 57 | - console.log('[usePlanPermission] 用户点击去登录') | ||
| 58 | - Taro.navigateTo({ | ||
| 59 | - url: '/pages/login/index' | ||
| 60 | - }) | ||
| 61 | - } | ||
| 62 | - } | ||
| 63 | - }) | ||
| 64 | - return false | ||
| 65 | - } | ||
| 66 | 57 | ||
| 67 | - console.log('[usePlanPermission] 用户已登录,执行回调') | 58 | + // 调用通用权限检查(登录权限) |
| 68 | - // 已登录,执行回调 | 59 | + return requireLogin(callback, customOptions) |
| 69 | - callback?.() | ||
| 70 | - return true | ||
| 71 | } | 60 | } |
| 72 | 61 | ||
| 73 | return { | 62 | return { | ... | ... |
src/config/permissions.js
0 → 100644
| 1 | +/** | ||
| 2 | + * 权限配置中心 | ||
| 3 | + * | ||
| 4 | + * @description 统一管理应用中的权限类型和提示文案 | ||
| 5 | + * @module config/permissions | ||
| 6 | + * @author Claude Code | ||
| 7 | + * @created 2026-02-13 | ||
| 8 | + */ | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 权限类型枚举 | ||
| 12 | + * @description 定义应用中的各种权限类型 | ||
| 13 | + * @enum {string} | ||
| 14 | + * | ||
| 15 | + * @example | ||
| 16 | + * import { PermissionType } from '@/config/permissions' | ||
| 17 | + * | ||
| 18 | + * if (needLogin) { | ||
| 19 | + * checkPermission(PermissionType.LOGIN, callback) | ||
| 20 | + * } | ||
| 21 | + */ | ||
| 22 | +export const PermissionType = { | ||
| 23 | + /** 登录权限 */ | ||
| 24 | + LOGIN: 'login', | ||
| 25 | + /** VIP 权限(预留) */ | ||
| 26 | + VIP: 'vip', | ||
| 27 | + /** 实名认证权限(预留) */ | ||
| 28 | + VERIFIED: 'verified' | ||
| 29 | +} | ||
| 30 | + | ||
| 31 | +/** | ||
| 32 | + * 权限提示文案配置 | ||
| 33 | + * @description 为每种权限类型配置对应的提示文案 | ||
| 34 | + * | ||
| 35 | + * @example | ||
| 36 | + * import { PermissionMessages } from '@/config/permissions' | ||
| 37 | + * | ||
| 38 | + * const message = PermissionMessages[PermissionType.LOGIN] | ||
| 39 | + * console.log(message.content) // "登录后即可查看完整内容" | ||
| 40 | + */ | ||
| 41 | +export const PermissionMessages = { | ||
| 42 | + /** 登录权限提示 */ | ||
| 43 | + [PermissionType.LOGIN]: { | ||
| 44 | + /** 弹窗标题 */ | ||
| 45 | + title: '温馨提示', | ||
| 46 | + /** 弹窗内容 */ | ||
| 47 | + content: '登录后即可查看完整内容', | ||
| 48 | + /** 确认按钮文案 */ | ||
| 49 | + confirmText: '去登录', | ||
| 50 | + /** 取消按钮文案 */ | ||
| 51 | + cancelText: '暂不登录' | ||
| 52 | + }, | ||
| 53 | + | ||
| 54 | + /** VIP 权限提示(预留) */ | ||
| 55 | + [PermissionType.VIP]: { | ||
| 56 | + title: 'VIP 专享', | ||
| 57 | + content: '此功能仅对 VIP 用户开放', | ||
| 58 | + confirmText: '开通 VIP', | ||
| 59 | + cancelText: '取消' | ||
| 60 | + }, | ||
| 61 | + | ||
| 62 | + /** 实名认证权限提示(预留) */ | ||
| 63 | + [PermissionType.VERIFIED]: { | ||
| 64 | + title: '需要实名认证', | ||
| 65 | + content: '请先完成实名认证', | ||
| 66 | + confirmText: '去认证', | ||
| 67 | + cancelText: '取消' | ||
| 68 | + } | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +export const ActionPermissionMap = { | ||
| 72 | + view_material: { | ||
| 73 | + permission_type: PermissionType.LOGIN, | ||
| 74 | + options: {} | ||
| 75 | + } | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +/** | ||
| 79 | + * 获取权限提示配置 | ||
| 80 | + * @description 根据权限类型获取对应的提示配置,支持自定义覆盖 | ||
| 81 | + * @param {string} permissionType - 权限类型 | ||
| 82 | + * @param {Object} customOptions - 自定义配置选项 | ||
| 83 | + * @returns {Object} 合并后的权限配置 | ||
| 84 | + * | ||
| 85 | + * @example | ||
| 86 | + * const config = getPermissionConfig(PermissionType.LOGIN, { | ||
| 87 | + * content: '请先登录后查看资料详情' | ||
| 88 | + * }) | ||
| 89 | + */ | ||
| 90 | +export function getPermissionConfig(permissionType, customOptions = {}) { | ||
| 91 | + const defaultConfig = PermissionMessages[permissionType] | ||
| 92 | + | ||
| 93 | + if (!defaultConfig) { | ||
| 94 | + console.warn(`[Permission] 未找到权限类型配置: ${permissionType}`) | ||
| 95 | + return { | ||
| 96 | + title: '提示', | ||
| 97 | + content: '您没有相应的权限', | ||
| 98 | + confirmText: '确定', | ||
| 99 | + cancelText: '取消' | ||
| 100 | + } | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + return { | ||
| 104 | + ...defaultConfig, | ||
| 105 | + ...customOptions | ||
| 106 | + } | ||
| 107 | +} | ||
| 108 | + | ||
| 109 | +export function getActionPermissionConfig(action, customOptions = {}) { | ||
| 110 | + const actionConfig = ActionPermissionMap[action] || { | ||
| 111 | + permission_type: PermissionType.LOGIN, | ||
| 112 | + options: {} | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + return { | ||
| 116 | + permission_type: actionConfig.permission_type, | ||
| 117 | + options: { | ||
| 118 | + ...actionConfig.options, | ||
| 119 | + ...customOptions | ||
| 120 | + } | ||
| 121 | + } | ||
| 122 | +} |
| ... | @@ -676,7 +676,7 @@ const onClear = async () => { | ... | @@ -676,7 +676,7 @@ const onClear = async () => { |
| 676 | * - 视频文件:自动跳转到视频播放页面 | 676 | * - 视频文件:自动跳转到视频播放页面 |
| 677 | * - 其他文件:自动下载并使用 Taro.openDocument 打开 | 677 | * - 其他文件:自动下载并使用 Taro.openDocument 打开 |
| 678 | */ | 678 | */ |
| 679 | -const { handleClick: onView } = useListItemClick({ | 679 | +const { handleClick: handleFileClick } = useListItemClick({ |
| 680 | listType: ListType.FILE, | 680 | listType: ListType.FILE, |
| 681 | onAfterClick: (item) => { | 681 | onAfterClick: (item) => { |
| 682 | console.log('[Material List] 用户打开了资料:', item.title) | 682 | console.log('[Material List] 用户打开了资料:', item.title) |
| ... | @@ -684,6 +684,17 @@ const { handleClick: onView } = useListItemClick({ | ... | @@ -684,6 +684,17 @@ const { handleClick: onView } = useListItemClick({ |
| 684 | }) | 684 | }) |
| 685 | 685 | ||
| 686 | /** | 686 | /** |
| 687 | + * 查看资料 | ||
| 688 | + * @description 点击查看按钮时,直接触发文件预览 | ||
| 689 | + * @param {Object} item - 资料对象 | ||
| 690 | + * | ||
| 691 | + * @note 权限检查已在 ListItemActions 组件内部统一处理 | ||
| 692 | + */ | ||
| 693 | +const onView = (item) => { | ||
| 694 | + handleFileClick(item) | ||
| 695 | +} | ||
| 696 | + | ||
| 697 | +/** | ||
| 687 | * 切换收藏状态 | 698 | * 切换收藏状态 |
| 688 | * @description 使用 useCollectOperation composable 处理收藏操作 | 699 | * @description 使用 useCollectOperation composable 处理收藏操作 |
| 689 | */ | 700 | */ | ... | ... |
vitest.config.js
0 → 100644
| 1 | +import { defineConfig } from 'vitest/config' | ||
| 2 | +import vue from '@vitejs/plugin-vue' | ||
| 3 | +import { fileURLToPath } from 'url' | ||
| 4 | +import { dirname, resolve } from 'path' | ||
| 5 | + | ||
| 6 | +const __filename = fileURLToPath(import.meta.url) | ||
| 7 | +const __dirname = dirname(__filename) | ||
| 8 | + | ||
| 9 | +const ignoreCssPlugin = () => ({ | ||
| 10 | + name: 'ignore-css', | ||
| 11 | + enforce: 'pre', | ||
| 12 | + resolveId(id) { | ||
| 13 | + if (id.endsWith('.css')) { | ||
| 14 | + return id | ||
| 15 | + } | ||
| 16 | + return null | ||
| 17 | + }, | ||
| 18 | + load(id) { | ||
| 19 | + if (id.endsWith('.css')) { | ||
| 20 | + return '' | ||
| 21 | + } | ||
| 22 | + return null | ||
| 23 | + } | ||
| 24 | +}) | ||
| 25 | + | ||
| 26 | +export default defineConfig({ | ||
| 27 | + plugins: [ignoreCssPlugin(), vue()], | ||
| 28 | + test: { | ||
| 29 | + environment: 'happy-dom', | ||
| 30 | + css: true, | ||
| 31 | + deps: { | ||
| 32 | + inline: ['@nutui/icons-vue-taro'] | ||
| 33 | + } | ||
| 34 | + }, | ||
| 35 | + resolve: { | ||
| 36 | + alias: { | ||
| 37 | + '@': resolve(__dirname, 'src'), | ||
| 38 | + '@nutui/icons-vue-taro': resolve(__dirname, 'node_modules/@nutui/icons-vue-taro/dist/es/index.es.js') | ||
| 39 | + }, | ||
| 40 | + extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'] | ||
| 41 | + } | ||
| 42 | +}) |
-
Please register or login to post a comment