hookehuyr

refactor(permission): 统一权限检查并移除重复代码

- 移除 material-list 页面中重复的 usePermission 导入和调用
- 权限检查完全由 ListItemActions 组件内部处理
- 简化 onView 函数直接调用 handleFileClick
- 添加 ListItemActions 组件 README 文档

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## [2026-02-13] - 统一权限检查与移除重复代码
### 新增
- ListItemActions 组件集成权限检查逻辑
### 优化
- 移除 material-list 页面中重复的 usePermission 调用
- 权限检查完全由 ListItemActions 组件内部处理
- 添加 ListItemActions 组件 README 文档
---
**详细信息**
- **影响文件**: src/pages/material-list/index.vue, src/components/list/ListItemActions/index.vue, src/components/list/ListItemActions/README.md
- **技术栈**: Vue 3, Taro 4
- **测试状态**: 已通过
- **备注**: 组件自包含业务逻辑模式,父组件无需重复权限检查
---
## [2026-02-13] - 资料查看权限与搜索页测试对齐
### 新增
- 统一动作级权限映射,支持页面查看权限扩展
- 资料查看入口增加登录权限校验与回跳路径记录
### 优化
- 搜索页测试对齐当前实现并补充接口 Mock
---
**详细信息**
- **影响文件**: 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
- **技术栈**: Vue 3, Taro 4, Pinia, Vitest
- **测试状态**: 已通过(pnpm test)
- **备注**: lint 存在历史 warning 未处理
---
## [2026-02-13] - 我的页面消息红点显示
### 新增
......
# ListItemActions 组件使用指南
## 📋 组件概述
**组件名称**: `ListItemActions`
**文件路径**: `src/components/list/ListItemActions/index.vue`
**用途**: 列表项的操作按钮组件,支持查看、收藏、删除三种操作
---
## ✨ 功能特性
### 1. 按钮类型
| 按钮 | 说明 | 依赖属性 |
|------|------|----------|
| **查看** | 查看文件/产品详情 | `viewable` |
| **收藏** | 切换收藏状态 | `collectable` |
| **删除** | 删除列表项 | `deletable` |
### 2. 内置功能
-**权限检查**: 自动检查登录权限(通过 `usePermission`
-**埋点上报**: 权限通过后自动发送查看埋点(通过 `useEventTracking`
-**状态管理**: 收藏按钮根据 `collected` 状态切换样式
---
## 🎯 使用方法
### 基础用法
```vue
<template>
<ListItemActions
:viewable="true"
:collectable="true"
:deletable="false"
:collected="item.collected"
:item-id="item.id"
@view="handleView"
@collect="handleCollect"
@delete="handleDelete"
/>
</template>
<script setup>
const handleView = () => {
// 权限检查通过后自动触发
// 不需要手动检查登录状态
console.log('查看文件')
}
const handleCollect = () => {
// 收藏状态由组件内部管理
// 父组件可以监听 collectChanged 事件
console.log('切换收藏')
}
const handleDelete = () => {
// 触发删除操作
console.log('删除项目')
}
</script>
```
---
## 📝 Props 说明
| 属性 | 类型 | 默认值 | 说明 |
|------|------|---------|------|
| `viewable` | Boolean | `true` | 是否显示查看按钮 |
| `collectable` | Boolean | `false` | 是否显示收藏按钮 |
| `deletable` | Boolean | `false` | 是否显示删除按钮 |
| `collected` | Boolean | `false` | 是否已收藏(影响收藏按钮样式) |
| `itemId` | String | `''` | 关联对象 ID,用于埋点上报 |
### 使用示例
```vue
<!-- 只显示查看和删除 -->
<ListItemActions
:viewable="true"
:deletable="true"
:item-id="doc123"
@view="onView"
@delete="onDelete"
/>
<!-- 三个按钮都显示 -->
<ListItemActions
:viewable="true"
:collectable="true"
:deletable="true"
:collected="true"
:item-id="product456"
@view="onView"
@collect="onCollect"
@delete="onDelete"
/>
```
---
## 📤 事件说明
### @view
**触发时机**: 权限检查通过后
**注意**: 如果用户未登录,会先显示登录弹框,登录成功后才会触发此事件
```vue
<script setup>
const handleView = (itemData) => {
console.log('执行查看逻辑:', itemData)
// 这里调用实际的文件打开逻辑
}
</script>
<template>
<ListItemActions @view="handleView" />
</template>
```
### @collect
**触发时机**: 点击收藏按钮时立即触发
```vue
<script setup>
const handleCollect = () => {
// 切换收藏状态
// 实际的收藏状态由 collected 属性控制
}
</script>
<template>
<ListItemActions @collect="handleCollect" />
</template>
```
### @delete
**触发时机**: 点击删除按钮时立即触发
```vue
<script setup>
const handleDelete = () => {
// 执行删除操作
// 调用删除 API 等
}
</script>
<template>
<ListItemActions @delete="handleDelete" />
</template>
```
---
## 🔐 权限和埋点流程
### 查看按钮完整流程
```
用户点击"查看"按钮
组件内部: requireAction('view_material', callback)
┌─ 未登录 ──────────────────────────────┐
│ │
│ 1. 检查登录状态 │
│ 2. 显示登录弹框 │
│ 3. ❌ 不发送埋点 │
│ 4. ❌ 不触发 @view 事件 │
│ │
└─────────────────────────────────────────┘
┌─ 已登录 ──────────────────────────────┐
│ │
│ 1. 权限检查通过 │
│ 2. ✅ 发送埋点 (trackFileRead) │
│ 3. ✅ 触发 @view 事件 │
│ │
└─────────────────────────────────────────┘
父组件收到 @view 事件
执行实际业务逻辑(打开文件等)
```
### 权限配置
权限配置位于 `src/config/permissions.js`
```javascript
export const ACTION_PERMISSIONS = {
view_material: {
permission_type: PermissionType.LOGIN,
options: {
content: '请先登录后查看资料',
confirmText: '去登录'
}
}
}
}
```
### 埋点配置
埋点配置位于 `src/composables/useEventTracking.js`
- **事件类型**: `READ_FILE`
- **上报时机**: 权限检查通过后立即上报
- **上报数据**: `{ type: 'READ_FILE', object_id: itemId }`
---
## 🎨 样式说明
### TailwindCSS 类名
| 类名 | 作用 |
|------|------|
| `flex justify-end gap-[24rpx]` | 右对齐,24rpx 间距 |
| `flex items-center` | 垂直居中 |
| `text-blue-600` | 蓝色文字(查看按钮) |
| `text-red-500` | 红色文字(删除按钮) |
| `text-red-500` / `text-gray-400` | 收藏按钮动态颜色 |
| `text-[24rpx]` | 字体大小 |
### 自定义样式
如果需要覆盖默认样式,可以在父组件中使用:
```vue
<template>
<ListItemActions
class="custom-actions"
@view="handleView"
/>
</template>
<style scoped>
.custom-actions :deep(.text-blue-600) {
color: #custom-color;
}
</style>
```
---
## ⚠️ 注意事项
### 1. 权限检查是自动的
不需要手动检查登录状态,组件内部已处理:
```vue
<!-- ❌ 错误:手动检查权限 -->
<script setup>
import { usePermission } from '@/composables/usePermission'
const { isLoggedIn } = usePermission()
const handleView = () => {
if (!isLoggedIn()) {
showToast('请先登录')
return
}
// 继续逻辑
}
</script>
<!-- ✅ 正确:让组件处理 -->
<script setup>
const handleView = () => {
// 直接执行,权限检查在组件内部
}
</script>
```
### 2. 埋点需要 itemId
如果不上报埋点,确保传递了 `item-id` 属性:
```vue
<!-- ❌ 缺少 itemId,埋点不会上报 -->
<ListItemActions @view="handleView" />
<!-- ✅ 正确:提供 itemId -->
<ListItemActions :item-id="'123456'" @view="handleView" />
```
### 3. 父组件的 @view 事件参数
`@view` 事件目前不传递参数,如果需要文件信息,请:
1. 在父组件中维护数据对象引用
2. 或者扩展组件支持传递完整 item 数据
```vue
<!-- 父组件示例 -->
<script setup>
const currentItem = ref(null)
const handleView = () => {
// currentItem 已在组件内部设置
console.log('查看:', currentItem.value)
}
</script>
<template>
<ListItemActions @view="handleView" />
</template>
```
---
## 🔧 扩展组件
### 添加新的按钮类型
如果需要添加新的操作按钮(如"分享"、"下载"等):
1.`props` 中添加新的控制属性
2. 在模板中添加按钮 UI
3. 添加对应的 emit 和 handler
```javascript
// 示例:添加分享按钮
const props = defineProps({
// ... 现有属性
shareable: {
type: Boolean,
default: false
}
})
const emit = defineEmits({
// ... 现有事件
share: null
})
const handleShare = () => {
emit('share')
}
```
---
## 📚 相关文件
- **权限配置**: `src/config/permissions.js`
- **权限 Composable**: `src/composables/usePermission.js`
- **埋点 Composable**: `src/composables/useEventTracking.js`
- **收藏操作**: `src/composables/useCollectOperation.js`
......@@ -40,6 +40,7 @@
import { computed } from 'vue'
import IconFont from '@/components/icons/IconFont.vue'
import { useEventTracking } from '@/composables/useEventTracking'
import { usePermission } from '@/composables/usePermission'
/**
* 组件属性
......@@ -114,18 +115,23 @@ const isCollected = computed(() => props.collected)
// 初始化埋点功能
const { trackFileRead } = useEventTracking()
// 初始化权限检查
const { requireAction } = usePermission()
/**
* 处理查看点击
*
* @description 触发查看事件,并自动发送埋点数据
* @description 先检查权限,通过后发送埋点并通知父组件
*/
const handleView = () => {
// 如果提供了 itemId,自动发送埋点
requireAction('view_material', () => {
// 权限检查通过后,发送埋点
if (props.itemId) {
trackFileRead(props.itemId)
}
// 通知父组件执行实际业务逻辑
emit('view')
})
}
/**
......
/**
* 通用权限检查 Composable
*
* @description 提供统一的权限检查逻辑,支持登录、VIP 等多种权限类型
* @module composables/usePermission
* @author Claude Code
* @created 2026-02-13
*
* @example
* // 基础使用 - 检查登录权限
* const { requireLogin } = usePermission()
*
* requireLogin(() => {
* // 已登录时执行的操作
* openDocument()
* })
*
* @example
* // 高级使用 - 自定义提示文案
* const { checkPermission } = usePermission()
*
* checkPermission(PermissionType.LOGIN, callback, {
* content: '请先登录后查看完整内容',
* confirmText: '立即登录'
* })
*/
import { useUserStore } from '@/stores/user'
import Taro from '@tarojs/taro'
import { routerStore } from '@/stores/router'
import { PermissionType, getPermissionConfig, getActionPermissionConfig } from '@/config/permissions'
/**
* 通用权限检查 Hook
*
* @description 提供权限检查、权限弹窗等功能
* @returns {Object} 权限检查方法集合
*/
export function usePermission() {
const userStore = useUserStore()
/**
* 检查登录状态
* @description 判断用户是否已登录
* @returns {boolean} 是否已登录
*
* @example
* const { isLoggedIn } = usePermission()
* if (isLoggedIn()) {
* console.log('用户已登录')
* }
*/
const isLoggedIn = () => {
return userStore.isLoggedIn
}
const getCurrentPageUrl = () => {
const pages = Taro.getCurrentPages()
const currentPage = pages[pages.length - 1]
if (!currentPage || !currentPage.route) {
return ''
}
const options = currentPage.options || {}
const query = Object.keys(options)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(options[key])}`)
.join('&')
return `/${currentPage.route}${query ? `?${query}` : ''}`
}
/**
* 通用权限检查
* @description 检查指定权限,无权限时弹窗提示,有权限时执行回调
* @param {string} permissionType - 权限类型(使用 PermissionType 枚举)
* @param {Function} callback - 有权限时执行的回调函数
* @param {Object} customOptions - 自定义配置选项(会覆盖默认配置)
* @returns {boolean} 是否有权限(true=有权限,false=无权限)
*
* @example
* // 使用默认配置
* checkPermission(PermissionType.LOGIN, () => {
* console.log('已登录,执行操作')
* })
*
* @example
* // 自定义提示文案
* checkPermission(PermissionType.LOGIN, callback, {
* content: '请先登录后查看完整内容',
* confirmText: '立即登录'
* })
*/
const checkPermission = (permissionType, callback, customOptions = {}) => {
console.log(`[usePermission] 检查权限: ${permissionType}`)
// 根据权限类型执行不同的检查逻辑
switch (permissionType) {
case PermissionType.LOGIN:
return checkLoginPermission(callback, customOptions)
case PermissionType.VIP:
return checkVipPermission(callback, customOptions)
case PermissionType.VERIFIED:
return checkVerifiedPermission(callback, customOptions)
default:
console.warn(`[usePermission] 未知的权限类型: ${permissionType}`)
return false
}
}
/**
* 检查登录权限
* @description 判断用户是否登录,未登录时弹窗引导
* @param {Function} callback - 已登录时执行的回调
* @param {Object} customOptions - 自定义配置选项
* @returns {boolean} 是否已登录
* @private
*/
const checkLoginPermission = (callback, customOptions = {}) => {
if (isLoggedIn()) {
// 已登录,直接执行回调
console.log('[usePermission] 用户已登录,执行回调')
callback?.()
return true
}
// 未登录,弹窗提示
console.log('[usePermission] 用户未登录,显示登录提示')
showLoginModal(callback, customOptions)
return false
}
/**
* 检查 VIP 权限(预留)
* @description 检查用户是否为 VIP,非 VIP 时弹窗引导
* @param {Function} callback - 有权限时执行的回调
* @param {Object} customOptions - 自定义配置选项
* @returns {boolean} 是否有权限
* @private
*/
const checkVipPermission = (callback, customOptions = {}) => {
// 预留:检查 VIP 状态
// const isVip = userStore.userInfo?.is_vip
const isVip = true // 暂时返回 true
if (isVip) {
callback?.()
return true
}
showPermissionModal(PermissionType.VIP, callback, customOptions)
return false
}
/**
* 检查实名认证权限(预留)
* @description 检查用户是否实名认证,未认证时弹窗引导
* @param {Function} callback - 有权限时执行的回调
* @param {Object} customOptions - 自定义配置选项
* @returns {boolean} 是否有权限
* @private
*/
const checkVerifiedPermission = (callback, customOptions = {}) => {
// 预留:检查实名认证状态
// const isVerified = userStore.userInfo?.is_verified
const isVerified = true // 暂时返回 true
if (isVerified) {
callback?.()
return true
}
showPermissionModal(PermissionType.VERIFIED, callback, customOptions)
return false
}
/**
* 显示登录弹窗
* @description 显示登录引导弹窗,点击确定跳转登录页
* @param {Function} callback - 用户登录成功后希望执行的回调(用于登录后返回)
* @param {Object} customOptions - 自定义配置选项
* @private
*/
const showLoginModal = (callback, customOptions = {}) => {
const config = getPermissionConfig(PermissionType.LOGIN, customOptions)
Taro.showModal({
title: config.title,
content: config.content,
confirmText: config.confirmText,
cancelText: config.cancelText,
success: (res) => {
if (res.confirm) {
// 用户点击"去登录"
console.log('[usePermission] 用户选择去登录')
const store = routerStore()
const currentUrl = getCurrentPageUrl()
if (currentUrl) {
store.add(currentUrl)
}
goToLoginPage()
} else {
// 用户点击"暂不登录"
console.log('[usePermission] 用户取消登录')
}
}
})
}
/**
* 显示权限提示弹窗(通用)
* @description 显示权限不足的提示弹窗
* @param {string} permissionType - 权限类型
* @param {Function} callback - 有权限时执行的回调
* @param {Object} customOptions - 自定义配置选项
* @private
*/
const showPermissionModal = (permissionType, callback, customOptions = {}) => {
const config = getPermissionConfig(permissionType, customOptions)
Taro.showModal({
title: config.title,
content: config.content,
confirmText: config.confirmText,
cancelText: config.cancelText,
success: (res) => {
if (res.confirm) {
console.log(`[usePermission] 用户确认权限提示: ${permissionType}`)
// 根据权限类型执行不同操作
handlePermissionAction(permissionType)
}
}
})
}
/**
* 处理权限确认后的操作
* @description 根据权限类型跳转到对应页面
* @param {string} permissionType - 权限类型
* @private
*/
const handlePermissionAction = (permissionType) => {
switch (permissionType) {
case PermissionType.LOGIN:
goToLoginPage()
break
case PermissionType.VIP:
// 跳转到 VIP 开通页面
console.log('[usePermission] 跳转到 VIP 开通页面')
break
case PermissionType.VERIFIED:
// 跳转到实名认证页面
console.log('[usePermission] 跳转到实名认证页面')
break
}
}
/**
* 跳转到登录页
* @description 跳转到登录页面
* @private
*/
const goToLoginPage = () => {
Taro.navigateTo({
url: '/pages/login/index'
})
}
/**
* 便捷方法:要求登录权限
* @description 专门用于检查登录权限的便捷方法
* @param {Function} callback - 已登录时执行的回调
* @param {Object} customOptions - 自定义配置选项
* @returns {boolean} 是否已登录
*
* @example
* const { requireLogin } = usePermission()
*
* // 查看资料需要登录
* onView(item) {
* requireLogin(() => {
* openDocument(item.url)
* })
* }
*/
const requireLogin = (callback, customOptions = {}) => {
return checkPermission(PermissionType.LOGIN, callback, customOptions)
}
const requireAction = (action, callback, customOptions = {}) => {
const actionConfig = getActionPermissionConfig(action, customOptions)
return checkPermission(actionConfig.permission_type, callback, actionConfig.options)
}
/**
* 便捷方法:静默检查权限(不弹窗)
* @description 只检查权限状态,不弹窗提示
* @param {string} permissionType - 权限类型
* @returns {boolean} 是否有权限
*
* @example
* const { hasPermission } = usePermission()
*
* if (hasPermission(PermissionType.LOGIN)) {
* console.log('用户已登录')
* }
*/
const hasPermission = (permissionType) => {
switch (permissionType) {
case PermissionType.LOGIN:
return isLoggedIn()
case PermissionType.VIP:
return true // 暂时返回 true
case PermissionType.VERIFIED:
return true // 暂时返回 true
default:
return false
}
}
// ========== 返回 ==========
return {
/** 核心方法:通用权限检查 */
checkPermission,
/** 便捷方法:检查登录权限(最常用) */
requireLogin,
requireAction,
/** 工具方法:静默检查是否有权限(不弹窗) */
hasPermission,
/** 工具方法:获取当前登录状态 */
isLoggedIn
}
}
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { usePermission } from './usePermission'
import { getActionPermissionConfig } from '@/config/permissions'
const show_modal_mock = vi.fn()
const navigate_to_mock = vi.fn()
const get_current_pages_mock = vi.fn()
const add_mock = vi.fn()
let user_state = { isLoggedIn: false }
vi.mock('@tarojs/taro', () => ({
default: {
showModal: (...args) => show_modal_mock(...args),
navigateTo: (...args) => navigate_to_mock(...args),
getCurrentPages: () => get_current_pages_mock()
}
}))
vi.mock('@/stores/user', () => ({
useUserStore: () => user_state
}))
vi.mock('@/stores/router', () => ({
routerStore: () => ({
add: add_mock
})
}))
describe('usePermission', () => {
beforeEach(() => {
show_modal_mock.mockReset()
navigate_to_mock.mockReset()
get_current_pages_mock.mockReset()
add_mock.mockReset()
user_state = { isLoggedIn: false }
})
it('requireAction 登录状态下执行回调', () => {
user_state.isLoggedIn = true
const callback = vi.fn()
const { requireAction } = usePermission()
const result = requireAction('view_material', callback)
expect(result).toBe(true)
expect(callback).toHaveBeenCalledTimes(1)
expect(show_modal_mock).not.toHaveBeenCalled()
})
it('requireAction 未登录时保存回跳并跳转登录', () => {
get_current_pages_mock.mockReturnValue([
{ route: 'pages/material-list/index', options: { id: '1', title: '资料' } }
])
show_modal_mock.mockImplementation((options) => {
options.success({ confirm: true })
})
const { requireAction } = usePermission()
const result = requireAction('view_material', () => {})
expect(result).toBe(false)
expect(show_modal_mock).toHaveBeenCalledTimes(1)
expect(add_mock).toHaveBeenCalledWith('/pages/material-list/index?id=1&title=%E8%B5%84%E6%96%99')
expect(navigate_to_mock).toHaveBeenCalledWith({ url: '/pages/login/index' })
})
})
describe('getActionPermissionConfig', () => {
it('未知 action 返回默认登录配置', () => {
const result = getActionPermissionConfig('unknown_action', { content: 'x' })
expect(result.permission_type).toBe('login')
expect(result.options.content).toBe('x')
})
})
/**
* 计划书权限检查 Composable
* 计划书权限检查 Composable(重构版)
*
* @description 统一处理制作计划书的登录权限检查
* @description 统一处理制作计划书的登录权限检查,内部调用通用 usePermission
* @module composables/usePlanPermission
* @author Claude Code
* @created 2026-02-12
* @updated 2026-02-13 - 重构为使用通用 usePermission
*/
import { useUserStore } from '@/stores/user'
import Taro from '@tarojs/taro'
import { usePermission } from '@/composables/usePermission'
/**
* 计划书权限检查 Hook
......@@ -26,48 +26,37 @@ import Taro from '@tarojs/taro'
* })
*/
export function usePlanPermission() {
const userStore = useUserStore()
// 获取通用权限检查方法
const { requireLogin } = usePermission()
/**
* 检查计划书权限
*
* @description 判断用户是否登录,未登录时提示并引导登录,已登录时执行回调
* @param {Function} callback - 已登录时执行的回调函数
* @param {Object} customOptions - 自定义配置选项(可选)
* @returns {boolean} 是否有权限(true=已登录,false=未登录)
*
* @example
* // 使用默认配置
* const hasPermission = checkPlanPermission(() => {
* console.log('用户已登录,可以制作计划书')
* })
*
* @example
* // 自定义提示文案
* const hasPermission = checkPlanPermission(() => {
* openPlanPopup(productId)
* }, {
* content: '请先登录后制作专属计划书',
* confirmText: '立即登录'
* })
*/
const checkPlanPermission = (callback) => {
console.log('[usePlanPermission] 检查权限,当前登录状态:', userStore.isLoggedIn)
// 检查登录状态
if (!userStore.isLoggedIn) {
console.log('[usePlanPermission] 用户未登录,显示登录提示')
// 未登录,显示提示框
Taro.showModal({
title: '提示',
content: '请先登录后再制作计划书',
confirmText: '去登录',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 用户点击"去登录",跳转到登录页
console.log('[usePlanPermission] 用户点击去登录')
Taro.navigateTo({
url: '/pages/login/index'
})
}
}
})
return false
}
const checkPlanPermission = (callback, customOptions = {}) => {
console.log('[usePlanPermission] 检查计划书权限')
console.log('[usePlanPermission] 用户已登录,执行回调')
// 已登录,执行回调
callback?.()
return true
// 调用通用权限检查(登录权限)
return requireLogin(callback, customOptions)
}
return {
......
/**
* 权限配置中心
*
* @description 统一管理应用中的权限类型和提示文案
* @module config/permissions
* @author Claude Code
* @created 2026-02-13
*/
/**
* 权限类型枚举
* @description 定义应用中的各种权限类型
* @enum {string}
*
* @example
* import { PermissionType } from '@/config/permissions'
*
* if (needLogin) {
* checkPermission(PermissionType.LOGIN, callback)
* }
*/
export const PermissionType = {
/** 登录权限 */
LOGIN: 'login',
/** VIP 权限(预留) */
VIP: 'vip',
/** 实名认证权限(预留) */
VERIFIED: 'verified'
}
/**
* 权限提示文案配置
* @description 为每种权限类型配置对应的提示文案
*
* @example
* import { PermissionMessages } from '@/config/permissions'
*
* const message = PermissionMessages[PermissionType.LOGIN]
* console.log(message.content) // "登录后即可查看完整内容"
*/
export const PermissionMessages = {
/** 登录权限提示 */
[PermissionType.LOGIN]: {
/** 弹窗标题 */
title: '温馨提示',
/** 弹窗内容 */
content: '登录后即可查看完整内容',
/** 确认按钮文案 */
confirmText: '去登录',
/** 取消按钮文案 */
cancelText: '暂不登录'
},
/** VIP 权限提示(预留) */
[PermissionType.VIP]: {
title: 'VIP 专享',
content: '此功能仅对 VIP 用户开放',
confirmText: '开通 VIP',
cancelText: '取消'
},
/** 实名认证权限提示(预留) */
[PermissionType.VERIFIED]: {
title: '需要实名认证',
content: '请先完成实名认证',
confirmText: '去认证',
cancelText: '取消'
}
}
export const ActionPermissionMap = {
view_material: {
permission_type: PermissionType.LOGIN,
options: {}
}
}
/**
* 获取权限提示配置
* @description 根据权限类型获取对应的提示配置,支持自定义覆盖
* @param {string} permissionType - 权限类型
* @param {Object} customOptions - 自定义配置选项
* @returns {Object} 合并后的权限配置
*
* @example
* const config = getPermissionConfig(PermissionType.LOGIN, {
* content: '请先登录后查看资料详情'
* })
*/
export function getPermissionConfig(permissionType, customOptions = {}) {
const defaultConfig = PermissionMessages[permissionType]
if (!defaultConfig) {
console.warn(`[Permission] 未找到权限类型配置: ${permissionType}`)
return {
title: '提示',
content: '您没有相应的权限',
confirmText: '确定',
cancelText: '取消'
}
}
return {
...defaultConfig,
...customOptions
}
}
export function getActionPermissionConfig(action, customOptions = {}) {
const actionConfig = ActionPermissionMap[action] || {
permission_type: PermissionType.LOGIN,
options: {}
}
return {
permission_type: actionConfig.permission_type,
options: {
...actionConfig.options,
...customOptions
}
}
}
......@@ -676,7 +676,7 @@ const onClear = async () => {
* - 视频文件:自动跳转到视频播放页面
* - 其他文件:自动下载并使用 Taro.openDocument 打开
*/
const { handleClick: onView } = useListItemClick({
const { handleClick: handleFileClick } = useListItemClick({
listType: ListType.FILE,
onAfterClick: (item) => {
console.log('[Material List] 用户打开了资料:', item.title)
......@@ -684,6 +684,17 @@ const { handleClick: onView } = useListItemClick({
})
/**
* 查看资料
* @description 点击查看按钮时,直接触发文件预览
* @param {Object} item - 资料对象
*
* @note 权限检查已在 ListItemActions 组件内部统一处理
*/
const onView = (item) => {
handleFileClick(item)
}
/**
* 切换收藏状态
* @description 使用 useCollectOperation composable 处理收藏操作
*/
......
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath } from 'url'
import { dirname, resolve } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const ignoreCssPlugin = () => ({
name: 'ignore-css',
enforce: 'pre',
resolveId(id) {
if (id.endsWith('.css')) {
return id
}
return null
},
load(id) {
if (id.endsWith('.css')) {
return ''
}
return null
}
})
export default defineConfig({
plugins: [ignoreCssPlugin(), vue()],
test: {
environment: 'happy-dom',
css: true,
deps: {
inline: ['@nutui/icons-vue-taro']
}
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@nutui/icons-vue-taro': resolve(__dirname, 'node_modules/@nutui/icons-vue-taro/dist/es/index.es.js')
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
}
})