refactor(ui): 提取可复用卡片组件并重构页面
- 新增 MaterialCard 和 ProductCard 组件,减少代码重复 - 重构首页、搜索页、本周热门资料页使用新组件 - 搜索页面集成真实API,支持分页加载和实时查询 - 移除测试数据,全部对接后端接口 影响文件: - src/components/MaterialCard.vue (新增) - src/components/ProductCard.vue (新增) - src/pages/index/index.vue - src/pages/search/index.vue - src/pages/week-hot-material/index.vue - components.d.ts - docs/CHANGELOG.md Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing
7 changed files
with
450 additions
and
141 deletions
| ... | @@ -18,6 +18,7 @@ declare module 'vue' { | ... | @@ -18,6 +18,7 @@ declare module 'vue' { |
| 18 | IndexNav: typeof import('./src/components/indexNav.vue')['default'] | 18 | IndexNav: typeof import('./src/components/indexNav.vue')['default'] |
| 19 | LifeInsuranceTemplate: typeof import('./src/components/PlanTemplates/LifeInsuranceTemplate.vue')['default'] | 19 | LifeInsuranceTemplate: typeof import('./src/components/PlanTemplates/LifeInsuranceTemplate.vue')['default'] |
| 20 | ListItemActions: typeof import('./src/components/ListItemActions/index.vue')['default'] | 20 | ListItemActions: typeof import('./src/components/ListItemActions/index.vue')['default'] |
| 21 | + MaterialCard: typeof import('./src/components/MaterialCard.vue')['default'] | ||
| 21 | NavHeader: typeof import('./src/components/NavHeader.vue')['default'] | 22 | NavHeader: typeof import('./src/components/NavHeader.vue')['default'] |
| 22 | NutAvatar: typeof import('@nutui/nutui-taro')['Avatar'] | 23 | NutAvatar: typeof import('@nutui/nutui-taro')['Avatar'] |
| 23 | NutButton: typeof import('@nutui/nutui-taro')['Button'] | 24 | NutButton: typeof import('@nutui/nutui-taro')['Button'] |
| ... | @@ -36,6 +37,7 @@ declare module 'vue' { | ... | @@ -36,6 +37,7 @@ declare module 'vue' { |
| 36 | PlanFormContainer: typeof import('./src/components/PlanFormContainer.vue')['default'] | 37 | PlanFormContainer: typeof import('./src/components/PlanFormContainer.vue')['default'] |
| 37 | PlanPopup: typeof import('./src/components/PlanSchemes/PlanPopup.vue')['default'] | 38 | PlanPopup: typeof import('./src/components/PlanSchemes/PlanPopup.vue')['default'] |
| 38 | PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] | 39 | PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] |
| 40 | + ProductCard: typeof import('./src/components/ProductCard.vue')['default'] | ||
| 39 | QrCode: typeof import('./src/components/qrCode.vue')['default'] | 41 | QrCode: typeof import('./src/components/qrCode.vue')['default'] |
| 40 | QrCodeSearch: typeof import('./src/components/qrCodeSearch.vue')['default'] | 42 | QrCodeSearch: typeof import('./src/components/qrCodeSearch.vue')['default'] |
| 41 | RadioGroup: typeof import('./src/components/PlanFields/RadioGroup.vue')['default'] | 43 | RadioGroup: typeof import('./src/components/PlanFields/RadioGroup.vue')['default'] | ... | ... |
| ... | @@ -5,6 +5,30 @@ | ... | @@ -5,6 +5,30 @@ |
| 5 | 5 | ||
| 6 | --- | 6 | --- |
| 7 | 7 | ||
| 8 | +## [2026-02-06] - 搜索页面联调完成 | ||
| 9 | + | ||
| 10 | +### 新增 | ||
| 11 | +- 集成搜索API,实现产品和资料的实时搜索功能 | ||
| 12 | +- 添加下拉刷新功能,支持刷新当前选中tab的数据 | ||
| 13 | +- 实现首次搜索自动选择tab逻辑(根据返回数据判断) | ||
| 14 | + | ||
| 15 | +### 优化 | ||
| 16 | +- 移除"全部"tab,只保留"产品"和"资料"两个tab | ||
| 17 | +- 点击tab时实时查询对应类型的数据,不再使用本地缓存 | ||
| 18 | +- 优化搜索结果展示,适配后端返回的数据结构 | ||
| 19 | + | ||
| 20 | +### 技术改进 | ||
| 21 | +- 使用 `searchAPI` 调用后端搜索接口 | ||
| 22 | +- 首次搜索不传 `type` 参数,根据返回的 `products.total` 和 `files.total` 自动选择有数据的tab | ||
| 23 | +- 切换tab时传递 `type` 参数('product' | 'file')进行精确查询 | ||
| 24 | +- 下拉刷新时使用当前选中的tab的type进行查询 | ||
| 25 | + | ||
| 26 | +### 修复 | ||
| 27 | +- 修复产品卡片显示逻辑,正确显示 `product_name`、`cover_image`、`tags` | ||
| 28 | +- 修复资料卡片显示逻辑,正确显示 `name`、`value`、`extension` | ||
| 29 | + | ||
| 30 | +--- | ||
| 31 | + | ||
| 8 | ## [2026-02-06] - 修复计划书表单重置不稳定问题 | 32 | ## [2026-02-06] - 修复计划书表单重置不稳定问题 |
| 9 | 33 | ||
| 10 | ### 修复 | 34 | ### 修复 | ... | ... |
src/components/MaterialCard.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="flex flex-row bg-white rounded-[24rpx] p-[24rpx] shadow-md border border-gray-50 material-card"> | ||
| 3 | + <!-- 左侧图标 --> | ||
| 4 | + <view class="w-[88rpx] h-[88rpx] mr-[24rpx] flex-shrink-0 flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100 rounded-[20rpx] shadow-inner self-start"> | ||
| 5 | + <image :src="iconUrl" class="w-[48rpx] h-[48rpx]" mode="aspectFit" /> | ||
| 6 | + </view> | ||
| 7 | + | ||
| 8 | + <!-- 内容区域 --> | ||
| 9 | + <view class="flex-1 min-w-0"> | ||
| 10 | + <!-- 标题 --> | ||
| 11 | + <view class="text-[#1F2937] text-[30rpx] font-bold leading-[1.4] mb-[8rpx] line-clamp-2"> | ||
| 12 | + {{ title }} | ||
| 13 | + </view> | ||
| 14 | + | ||
| 15 | + <!-- 学习人数信息 --> | ||
| 16 | + <view v-if="learners" class="flex items-center gap-[12rpx] mb-[16rpx]"> | ||
| 17 | + <view class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-orange-50 text-orange-600 text-[20rpx] font-medium rounded-[8rpx]"> | ||
| 18 | + <text>{{ learners }}</text> | ||
| 19 | + </view> | ||
| 20 | + <!-- 学习人数比例 --> | ||
| 21 | + <view v-if="readPeoplePercent !== undefined && readPeoplePercent !== null" class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-green-50 text-green-600 text-[20rpx] font-medium rounded-[8rpx]"> | ||
| 22 | + <text>{{ readPeoplePercent }}%热度</text> | ||
| 23 | + </view> | ||
| 24 | + </view> | ||
| 25 | + | ||
| 26 | + <!-- 文档类型和文件大小 --> | ||
| 27 | + <view class="flex items-center gap-[12rpx] mb-[16rpx]"> | ||
| 28 | + <view class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-gray-100 text-gray-500 text-[20rpx] font-medium rounded-[8rpx]"> | ||
| 29 | + <text>{{ docTypeLabel }}</text> | ||
| 30 | + </view> | ||
| 31 | + <view v-if="fileSize" class="text-[#9CA3AF] text-[22rpx]"> | ||
| 32 | + {{ fileSize }} | ||
| 33 | + </view> | ||
| 34 | + </view> | ||
| 35 | + | ||
| 36 | + <!-- 分割线 --> | ||
| 37 | + <view class="h-[1rpx] bg-gray-100 my-[20rpx]"></view> | ||
| 38 | + | ||
| 39 | + <!-- 操作按钮 --> | ||
| 40 | + <ListItemActions | ||
| 41 | + :viewable="true" | ||
| 42 | + :collectable="true" | ||
| 43 | + :deletable="false" | ||
| 44 | + :collected="collected" | ||
| 45 | + :item-id="String(id)" | ||
| 46 | + @view="handleView" | ||
| 47 | + @collect="handleCollect" | ||
| 48 | + /> | ||
| 49 | + </view> | ||
| 50 | + </view> | ||
| 51 | +</template> | ||
| 52 | + | ||
| 53 | +<script setup> | ||
| 54 | +/** | ||
| 55 | + * 资料卡片组件 | ||
| 56 | + * | ||
| 57 | + * @description 热门资料列表项卡片,展示资料图标、标题、学习人数和操作按钮 | ||
| 58 | + * @component MaterialCard | ||
| 59 | + */ | ||
| 60 | + | ||
| 61 | +import { defineProps, defineEmits } from 'vue'; | ||
| 62 | +import Taro from '@tarojs/taro'; | ||
| 63 | +import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'; | ||
| 64 | +import ListItemActions from '@/components/ListItemActions/index.vue'; | ||
| 65 | +import { useCollectOperation } from '@/composables/useCollectOperation'; | ||
| 66 | +import { useListItemClick, ListType } from '@/composables/useListItemClick'; | ||
| 67 | + | ||
| 68 | +/** | ||
| 69 | + * 组件属性 | ||
| 70 | + */ | ||
| 71 | +const props = defineProps({ | ||
| 72 | + /** 资料 ID */ | ||
| 73 | + id: { | ||
| 74 | + type: [Number, String], | ||
| 75 | + required: true | ||
| 76 | + }, | ||
| 77 | + /** 资料标题 */ | ||
| 78 | + title: { | ||
| 79 | + type: String, | ||
| 80 | + required: true | ||
| 81 | + }, | ||
| 82 | + /** 文件名(用于获取图标和标签) */ | ||
| 83 | + fileName: { | ||
| 84 | + type: String, | ||
| 85 | + default: '' | ||
| 86 | + }, | ||
| 87 | + /** 文件大小 */ | ||
| 88 | + fileSize: { | ||
| 89 | + type: String, | ||
| 90 | + default: '' | ||
| 91 | + }, | ||
| 92 | + /** 学习人数文本 */ | ||
| 93 | + learners: { | ||
| 94 | + type: String, | ||
| 95 | + default: '' | ||
| 96 | + }, | ||
| 97 | + /** 学习人数百分比(热度) */ | ||
| 98 | + readPeoplePercent: { | ||
| 99 | + type: Number, | ||
| 100 | + default: null | ||
| 101 | + }, | ||
| 102 | + /** 是否已收藏 */ | ||
| 103 | + collected: { | ||
| 104 | + type: Boolean, | ||
| 105 | + default: false | ||
| 106 | + }, | ||
| 107 | + /** 文件扩展名 */ | ||
| 108 | + extension: { | ||
| 109 | + type: String, | ||
| 110 | + default: '' | ||
| 111 | + }, | ||
| 112 | + /** 下载URL */ | ||
| 113 | + downloadUrl: { | ||
| 114 | + type: String, | ||
| 115 | + default: '' | ||
| 116 | + } | ||
| 117 | +}); | ||
| 118 | + | ||
| 119 | +/** | ||
| 120 | + * 组件事件(简化版) | ||
| 121 | + */ | ||
| 122 | +const emit = defineEmits({ | ||
| 123 | + /** 查看完成 */ | ||
| 124 | + viewed: (item) => true, | ||
| 125 | + /** 收藏状态改变 */ | ||
| 126 | + collectChanged: (item) => true | ||
| 127 | +}); | ||
| 128 | + | ||
| 129 | +/** | ||
| 130 | + * 获取文档图标 URL | ||
| 131 | + * | ||
| 132 | + * @description 根据文件名获取对应的文档类型图标 | ||
| 133 | + * @returns {string} 图标 URL | ||
| 134 | + */ | ||
| 135 | +const iconUrl = props.fileName ? getDocumentIcon(props.fileName) : ''; | ||
| 136 | + | ||
| 137 | +/** | ||
| 138 | + * 获取文档类型标签 | ||
| 139 | + * | ||
| 140 | + * @description 根据文件名获取文档类型标签文本 | ||
| 141 | + * @returns {string} 文档类型标签 | ||
| 142 | + */ | ||
| 143 | +const docTypeLabel = props.fileName ? getDocumentLabel(props.fileName) : ''; | ||
| 144 | + | ||
| 145 | +/** | ||
| 146 | + * 使用收藏操作 composable | ||
| 147 | + */ | ||
| 148 | +const { toggleCollect } = useCollectOperation(); | ||
| 149 | + | ||
| 150 | +/** | ||
| 151 | + * 使用文件列表点击处理器(内部实现) | ||
| 152 | + */ | ||
| 153 | +const { handleClick } = useListItemClick({ | ||
| 154 | + listType: ListType.FILE, | ||
| 155 | + onBeforeClick: async (item) => { | ||
| 156 | + /** | ||
| 157 | + * 检查文件类型并使用对应的预览方式 | ||
| 158 | + * - 图片文件:使用 Taro.previewImage 预览 | ||
| 159 | + * - 其他文件:继续默认的文件打开流程 | ||
| 160 | + */ | ||
| 161 | + const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'] | ||
| 162 | + const extension = item.extension?.toLowerCase() || '' | ||
| 163 | + | ||
| 164 | + console.log('[MaterialCard] 文件类型:', extension, '文件名:', item.title) | ||
| 165 | + | ||
| 166 | + if (imageExtensions.includes(extension)) { | ||
| 167 | + // 图片文件:使用 Taro 预览 | ||
| 168 | + console.log('[MaterialCard] 检测到图片文件,使用图片预览') | ||
| 169 | + | ||
| 170 | + // 构建图片列表(当前图片) | ||
| 171 | + const urls = [item.downloadUrl] | ||
| 172 | + | ||
| 173 | + try { | ||
| 174 | + // 短暂延迟后打开预览(让用户看到提示) | ||
| 175 | + await new Promise(resolve => setTimeout(resolve, 300)) | ||
| 176 | + | ||
| 177 | + await Taro.previewImage({ | ||
| 178 | + current: item.downloadUrl, | ||
| 179 | + urls: urls | ||
| 180 | + }) | ||
| 181 | + | ||
| 182 | + // 预览成功,阻止默认的文件打开行为 | ||
| 183 | + return false | ||
| 184 | + } catch (err) { | ||
| 185 | + console.error('[MaterialCard] 图片预览失败:', err) | ||
| 186 | + Taro.showToast({ | ||
| 187 | + title: '图片预览失败', | ||
| 188 | + icon: 'none', | ||
| 189 | + duration: 2000 | ||
| 190 | + }) | ||
| 191 | + // 预览失败,返回 true 继续默认行为 | ||
| 192 | + return true | ||
| 193 | + } | ||
| 194 | + } | ||
| 195 | + | ||
| 196 | + // 非图片文件:继续默认的文件打开流程 | ||
| 197 | + console.log('[MaterialCard] 非图片文件,使用默认打开方式') | ||
| 198 | + return true | ||
| 199 | + }, | ||
| 200 | + onAfterClick: (item) => { | ||
| 201 | + console.log('[MaterialCard] 用户打开了资料:', item.title) | ||
| 202 | + // 通知父组件查看完成 | ||
| 203 | + emit('viewed', item) | ||
| 204 | + } | ||
| 205 | +}) | ||
| 206 | + | ||
| 207 | +/** | ||
| 208 | + * 处理查看点击 | ||
| 209 | + * | ||
| 210 | + * @description 内部处理查看逻辑,调用 useListItemClick | ||
| 211 | + */ | ||
| 212 | +const handleView = () => { | ||
| 213 | + handleClick({ | ||
| 214 | + id: props.id, | ||
| 215 | + title: props.title, | ||
| 216 | + fileName: props.fileName, | ||
| 217 | + fileSize: props.fileSize, | ||
| 218 | + learners: props.learners, | ||
| 219 | + readPeoplePercent: props.readPeoplePercent, | ||
| 220 | + collected: props.collected, | ||
| 221 | + extension: props.extension, | ||
| 222 | + downloadUrl: props.downloadUrl | ||
| 223 | + }) | ||
| 224 | +}; | ||
| 225 | + | ||
| 226 | +/** | ||
| 227 | + * 处理收藏点击 | ||
| 228 | + * | ||
| 229 | + * @description 内部处理收藏逻辑,调用 useCollectOperation 并通知父组件 | ||
| 230 | + */ | ||
| 231 | +const handleCollect = () => { | ||
| 232 | + // 调用收藏操作 | ||
| 233 | + toggleCollect({ | ||
| 234 | + id: props.id, | ||
| 235 | + title: props.title, | ||
| 236 | + fileName: props.fileName, | ||
| 237 | + fileSize: props.fileSize, | ||
| 238 | + learners: props.learners, | ||
| 239 | + readPeoplePercent: props.readPeoplePercent, | ||
| 240 | + collected: props.collected, | ||
| 241 | + extension: props.extension, | ||
| 242 | + downloadUrl: props.downloadUrl | ||
| 243 | + }) | ||
| 244 | + | ||
| 245 | + // 通知父组件收藏状态改变 | ||
| 246 | + emit('collectChanged', { | ||
| 247 | + id: props.id, | ||
| 248 | + title: props.title, | ||
| 249 | + fileName: props.fileName, | ||
| 250 | + fileSize: props.fileSize, | ||
| 251 | + learners: props.learners, | ||
| 252 | + readPeoplePercent: props.readPeoplePercent, | ||
| 253 | + collected: !props.collected, // 新状态(取反) | ||
| 254 | + extension: props.extension, | ||
| 255 | + downloadUrl: props.downloadUrl | ||
| 256 | + }) | ||
| 257 | +}; | ||
| 258 | +</script> | ||
| 259 | + | ||
| 260 | +<style lang="less" scoped> | ||
| 261 | +.material-card { | ||
| 262 | + // 多行文本省略 | ||
| 263 | + .line-clamp-2 { | ||
| 264 | + display: -webkit-box; | ||
| 265 | + -webkit-box-orient: vertical; | ||
| 266 | + -webkit-line-clamp: 2; | ||
| 267 | + line-clamp: 2; | ||
| 268 | + overflow: hidden; | ||
| 269 | + word-break: break-all; | ||
| 270 | + } | ||
| 271 | +} | ||
| 272 | +</style> |
src/components/ProductCard.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="bg-gray-50 rounded-[24rpx] p-[28rpx] product-card"> | ||
| 3 | + <!-- 产品名称 --> | ||
| 4 | + <text class="block text-gray-800 text-[28rpx] font-medium mb-[20rpx]">{{ productName }}</text> | ||
| 5 | + | ||
| 6 | + <!-- 产品标签 --> | ||
| 7 | + <view v-if="tags && tags.length" class="flex flex-wrap gap-[12rpx] mb-[24rpx]"> | ||
| 8 | + <view | ||
| 9 | + v-for="tag in tags" | ||
| 10 | + :key="tag.id" | ||
| 11 | + class="rounded-[8rpx] px-[16rpx] py-[6rpx]" | ||
| 12 | + :style="{ | ||
| 13 | + backgroundColor: tag.bg_color, | ||
| 14 | + color: tag.text_color | ||
| 15 | + }" | ||
| 16 | + > | ||
| 17 | + <text class="text-[22rpx]">{{ tag.name }}</text> | ||
| 18 | + </view> | ||
| 19 | + </view> | ||
| 20 | + | ||
| 21 | + <!-- 操作按钮 --> | ||
| 22 | + <view class="flex justify-between gap-[24rpx]"> | ||
| 23 | + <nut-button | ||
| 24 | + plain | ||
| 25 | + color="#2563EB" | ||
| 26 | + class="flex-1 !h-[64rpx] !rounded-[16rpx] !text-[26rpx] !m-0 !border-blue-600" | ||
| 27 | + @tap="handleDetail" | ||
| 28 | + > | ||
| 29 | + 产品详情 | ||
| 30 | + </nut-button> | ||
| 31 | + <nut-button | ||
| 32 | + color="#2563EB" | ||
| 33 | + class="flex-1 !h-[64rpx] !rounded-[16rpx] !text-[26rpx] !m-0" | ||
| 34 | + @tap="handlePlan" | ||
| 35 | + > | ||
| 36 | + 计划书 | ||
| 37 | + </nut-button> | ||
| 38 | + </view> | ||
| 39 | + </view> | ||
| 40 | +</template> | ||
| 41 | + | ||
| 42 | +<script setup> | ||
| 43 | +/** | ||
| 44 | + * 产品卡片组件 | ||
| 45 | + * | ||
| 46 | + * @description 热卖产品列表项卡片,展示产品名称、标签和操作按钮 | ||
| 47 | + * @component ProductCard | ||
| 48 | + */ | ||
| 49 | + | ||
| 50 | +import { defineProps, defineEmits } from 'vue'; | ||
| 51 | + | ||
| 52 | +/** | ||
| 53 | + * 组件属性 | ||
| 54 | + */ | ||
| 55 | +const props = defineProps({ | ||
| 56 | + /** 产品 ID */ | ||
| 57 | + productId: { | ||
| 58 | + type: Number, | ||
| 59 | + required: true | ||
| 60 | + }, | ||
| 61 | + /** 产品名称 */ | ||
| 62 | + productName: { | ||
| 63 | + type: String, | ||
| 64 | + required: true | ||
| 65 | + }, | ||
| 66 | + /** 产品标签数组 */ | ||
| 67 | + tags: { | ||
| 68 | + type: Array, | ||
| 69 | + default: () => [] | ||
| 70 | + } | ||
| 71 | +}); | ||
| 72 | + | ||
| 73 | +/** | ||
| 74 | + * 组件事件 | ||
| 75 | + */ | ||
| 76 | +const emit = defineEmits({ | ||
| 77 | + /** 点击产品详情按钮 */ | ||
| 78 | + detail: (productId) => typeof productId === 'number', | ||
| 79 | + /** 点击计划书按钮 */ | ||
| 80 | + plan: (productId) => typeof productId === 'number' | ||
| 81 | +}); | ||
| 82 | + | ||
| 83 | +/** | ||
| 84 | + * 处理产品详情点击 | ||
| 85 | + * | ||
| 86 | + * @description 触发 detail 事件,传递产品 ID | ||
| 87 | + */ | ||
| 88 | +const handleDetail = () => { | ||
| 89 | + emit('detail', props.productId); | ||
| 90 | +}; | ||
| 91 | + | ||
| 92 | +/** | ||
| 93 | + * 处理计划书点击 | ||
| 94 | + * | ||
| 95 | + * @description 触发 plan 事件,传递产品 ID | ||
| 96 | + */ | ||
| 97 | +const handlePlan = () => { | ||
| 98 | + emit('plan', props.productId); | ||
| 99 | +}; | ||
| 100 | +</script> | ||
| 101 | + | ||
| 102 | +<style lang="less" scoped> | ||
| 103 | +.product-card { | ||
| 104 | + // 可以添加卡片特定的样式 | ||
| 105 | +} | ||
| 106 | +</style> |
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
| 1 | <!-- | 1 | <!-- |
| 2 | - * @Date: 2026-02-05 | 2 | + * @Date: 2026-02-06 |
| 3 | - * @Description: 本周热门资料页 - 简化版资料列表(无搜索和Tab) | 3 | + * @Description: 本周热门资料页 - 使用 MaterialCard 组件 |
| 4 | --> | 4 | --> |
| 5 | <template> | 5 | <template> |
| 6 | <view class="h-screen bg-[#F9FAFB] flex flex-col py-[32rpx]"> | 6 | <view class="h-screen bg-[#F9FAFB] flex flex-col py-[32rpx]"> |
| ... | @@ -19,57 +19,21 @@ | ... | @@ -19,57 +19,21 @@ |
| 19 | </view> | 19 | </view> |
| 20 | 20 | ||
| 21 | <view v-else class="flex flex-col gap-[24rpx]"> | 21 | <view v-else class="flex flex-col gap-[24rpx]"> |
| 22 | - <view v-for="(item, index) in currentList" :key="item.meta_id" | 22 | + <MaterialCard |
| 23 | - class="material-item bg-white rounded-[24rpx] p-[24rpx] shadow-md transition-all duration-200 border border-gray-50 flex flex-row" | 23 | + v-for="(item, index) in currentList" |
| 24 | - :style="{ animationDelay: `${index * 50}ms` }"> | 24 | + :key="item.meta_id" |
| 25 | - | 25 | + :id="item.meta_id" |
| 26 | - <view | 26 | + :title="item.name" |
| 27 | - class="w-[88rpx] h-[88rpx] mr-[24rpx] flex-shrink-0 flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100 rounded-[20rpx] shadow-inner self-start"> | 27 | + :file-name="item.name" |
| 28 | - <image | 28 | + :file-size="item.size" |
| 29 | - :src="getDocumentIcon(item.extension ? `file.${item.extension}` : item.name)" | 29 | + :learners="item.read_people_count ? `${item.read_people_count}人学习` : ''" |
| 30 | - class="w-[48rpx] h-[48rpx]" | 30 | + :read-people-percent="item.read_people_percent" |
| 31 | - mode="aspectFit" | 31 | + :collected="item.collected" |
| 32 | - /> | 32 | + :extension="item.extension" |
| 33 | - </view> | 33 | + :download-url="item.downloadUrl" |
| 34 | - | 34 | + :style="{ animationDelay: `${index * 50}ms` }" |
| 35 | - <view class="flex-1 min-w-0"> | 35 | + @collect-changed="handleCollectChanged(item, $event)" |
| 36 | - <view class="text-[#1F2937] text-[30rpx] font-bold leading-[1.4] mb-[8rpx] line-clamp-2"> | 36 | + /> |
| 37 | - {{ item.name }} | ||
| 38 | - </view> | ||
| 39 | - | ||
| 40 | - <!-- 学习人数信息(本周热门特有) --> | ||
| 41 | - <view v-if="item.read_people_count !== undefined" class="flex items-center gap-[12rpx] mb-[16rpx]"> | ||
| 42 | - <view class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-orange-50 text-orange-600 text-[20rpx] font-medium rounded-[8rpx]"> | ||
| 43 | - <text>{{ item.read_people_count }}人学习</text> | ||
| 44 | - </view> | ||
| 45 | - <!-- 热度百分比 --> | ||
| 46 | - <view v-if="item.read_people_percent !== undefined" class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-green-50 text-green-600 text-[20rpx] font-medium rounded-[8rpx]"> | ||
| 47 | - <text>{{ item.read_people_percent }}%热度</text> | ||
| 48 | - </view> | ||
| 49 | - </view> | ||
| 50 | - | ||
| 51 | - <view class="flex items-center gap-[12rpx] mb-[16rpx]"> | ||
| 52 | - <view | ||
| 53 | - class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-gray-100 text-gray-500 text-[20rpx] font-medium rounded-[8rpx]"> | ||
| 54 | - {{ getDocumentLabel(item.extension ? `file.${item.extension}` : item.name) }} | ||
| 55 | - </view> | ||
| 56 | - <view class="text-[#9CA3AF] text-[22rpx]"> | ||
| 57 | - {{ item.size }} | ||
| 58 | - </view> | ||
| 59 | - </view> | ||
| 60 | - | ||
| 61 | - <view class="h-[1rpx] bg-gray-100 my-[20rpx]"></view> | ||
| 62 | - | ||
| 63 | - <ListItemActions | ||
| 64 | - :viewable="true" | ||
| 65 | - :collectable="true" | ||
| 66 | - :collected="item.collected" | ||
| 67 | - :item-id="String(item.meta_id)" | ||
| 68 | - @view="onView(item)" | ||
| 69 | - @collect="toggleCollect(item)" | ||
| 70 | - /> | ||
| 71 | - </view> | ||
| 72 | - </view> | ||
| 73 | 37 | ||
| 74 | <!-- 空状态 --> | 38 | <!-- 空状态 --> |
| 75 | <view v-if="currentList.length === 0 && !loading && !loadingMore"> | 39 | <view v-if="currentList.length === 0 && !loading && !loadingMore"> |
| ... | @@ -93,14 +57,10 @@ | ... | @@ -93,14 +57,10 @@ |
| 93 | 57 | ||
| 94 | <script setup> | 58 | <script setup> |
| 95 | import { ref } from 'vue' | 59 | import { ref } from 'vue' |
| 96 | -import { useLoad, useReachBottom } from '@tarojs/taro' | 60 | +import Taro, { useLoad, useReachBottom } from '@tarojs/taro' |
| 97 | import NavHeader from '@/components/NavHeader.vue' | 61 | import NavHeader from '@/components/NavHeader.vue' |
| 98 | -import ListItemActions from '@/components/ListItemActions/index.vue' | 62 | +import MaterialCard from '@/components/MaterialCard.vue' |
| 99 | -import { useListItemClick, ListType } from '@/composables/useListItemClick' | ||
| 100 | -import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons' | ||
| 101 | import { weekHotAPI } from '@/api/file' | 63 | import { weekHotAPI } from '@/api/file' |
| 102 | -import { useCollectOperation } from '@/composables/useCollectOperation' | ||
| 103 | -import Taro from '@tarojs/taro' | ||
| 104 | 64 | ||
| 105 | const listVisible = ref(true) | 65 | const listVisible = ref(true) |
| 106 | const listRenderKey = ref(0) | 66 | const listRenderKey = ref(0) |
| ... | @@ -112,26 +72,18 @@ const currentPage = ref(0) // 当前页码(从0开始) | ... | @@ -112,26 +72,18 @@ const currentPage = ref(0) // 当前页码(从0开始) |
| 112 | const pageSize = 20 // 每页数量 | 72 | const pageSize = 20 // 每页数量 |
| 113 | 73 | ||
| 114 | /** | 74 | /** |
| 115 | - * 转换文档数据格式 | 75 | + * 处理收藏状态改变 |
| 116 | - * @description 将 API 返回的文档数据转换为组件需要的格式 | 76 | + * |
| 117 | - * @param {Object} doc - API 返回的文档对象 | 77 | + * @description 当用户点击收藏按钮时,更新本地状态 |
| 118 | - * @returns {Object} 转换后的文档对象 | 78 | + * @param {Object} item - 资料对象 |
| 79 | + * @param {Object} newStatus - 新的状态 | ||
| 119 | */ | 80 | */ |
| 120 | -const transformDocItem = (doc) => { | 81 | +const handleCollectChanged = (item, newStatus) => { |
| 121 | - // 处理文件名为空的情况 | 82 | + console.log('[Week Hot] 收藏状态改变:', item.name, newStatus.collected) |
| 122 | - const fileName = doc.name || '未命名文件' | 83 | + // 找到对应的项并更新状态 |
| 123 | - // 如果没有扩展名,从文件名中提取(如果有) | 84 | + const material = currentList.value.find(m => m.meta_id === item.meta_id) |
| 124 | - const extension = doc.extension || fileName.split('.').pop()?.toLowerCase() || '' | 85 | + if (material) { |
| 125 | - | 86 | + material.collected = newStatus.collected |
| 126 | - return { | ||
| 127 | - meta_id: doc.meta_id, | ||
| 128 | - name: fileName, | ||
| 129 | - size: doc.size || '', | ||
| 130 | - downloadUrl: doc.src, | ||
| 131 | - extension: extension, | ||
| 132 | - collected: doc.is_favorite === '1' || doc.is_favorite === 1 || doc.is_favorite === true, | ||
| 133 | - read_people_count: doc.read_people_count, | ||
| 134 | - read_people_percent: doc.read_people_percent | ||
| 135 | } | 87 | } |
| 136 | } | 88 | } |
| 137 | 89 | ||
| ... | @@ -161,7 +113,22 @@ const fetchWeekHotList = async (params = {}, isLoadMore = false) => { | ... | @@ -161,7 +113,22 @@ const fetchWeekHotList = async (params = {}, isLoadMore = false) => { |
| 161 | 113 | ||
| 162 | // 处理列表数据 | 114 | // 处理列表数据 |
| 163 | if (res.data.list?.length) { | 115 | if (res.data.list?.length) { |
| 164 | - const listData = res.data.list.map(transformDocItem) | 116 | + // 直接映射为 MaterialCard 需要的格式 |
| 117 | + const listData = res.data.list.map(item => { | ||
| 118 | + const fileName = item.name || '未命名文件' | ||
| 119 | + const extension = item.extension || fileName.split('.').pop()?.toLowerCase() || '' | ||
| 120 | + | ||
| 121 | + return { | ||
| 122 | + meta_id: item.meta_id, | ||
| 123 | + name: fileName, | ||
| 124 | + size: item.size || '', | ||
| 125 | + downloadUrl: item.src, | ||
| 126 | + extension: extension, | ||
| 127 | + collected: item.is_favorite === '1' || item.is_favorite === 1 || item.is_favorite === true, | ||
| 128 | + read_people_count: item.read_people_count, | ||
| 129 | + read_people_percent: item.read_people_percent | ||
| 130 | + } | ||
| 131 | + }) | ||
| 165 | 132 | ||
| 166 | if (isLoadMore) { | 133 | if (isLoadMore) { |
| 167 | // 加载更多:追加数据 | 134 | // 加载更多:追加数据 |
| ... | @@ -248,68 +215,6 @@ useReachBottom(() => { | ... | @@ -248,68 +215,6 @@ useReachBottom(() => { |
| 248 | ) | 215 | ) |
| 249 | }, 300) | 216 | }, 300) |
| 250 | }) | 217 | }) |
| 251 | - | ||
| 252 | -/** | ||
| 253 | - * 使用文件列表点击处理器 | ||
| 254 | - * @description 添加图片预览功能,点击图片文件时使用 Taro.previewImage | ||
| 255 | - */ | ||
| 256 | -const { handleClick: onView } = useListItemClick({ | ||
| 257 | - listType: ListType.FILE, | ||
| 258 | - onBeforeClick: async (item) => { | ||
| 259 | - /** | ||
| 260 | - * 检查文件类型并使用对应的预览方式 | ||
| 261 | - * - 图片文件:使用 Taro.previewImage 预览 | ||
| 262 | - * - 其他文件:继续默认的文件打开流程 | ||
| 263 | - */ | ||
| 264 | - const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'] | ||
| 265 | - const extension = item.extension?.toLowerCase() || '' | ||
| 266 | - | ||
| 267 | - console.log('[Week Hot] 文件类型:', extension, '文件名:', item.name) | ||
| 268 | - | ||
| 269 | - if (imageExtensions.includes(extension)) { | ||
| 270 | - // 图片文件:使用 Taro 预览 | ||
| 271 | - console.log('[Week Hot] 检测到图片文件,使用图片预览') | ||
| 272 | - | ||
| 273 | - // 构建图片列表(当前图片) | ||
| 274 | - const urls = [item.downloadUrl] | ||
| 275 | - | ||
| 276 | - try { | ||
| 277 | - // 短暂延迟后打开预览(让用户看到提示) | ||
| 278 | - await new Promise(resolve => setTimeout(resolve, 300)) | ||
| 279 | - | ||
| 280 | - await Taro.previewImage({ | ||
| 281 | - current: item.downloadUrl, | ||
| 282 | - urls: urls | ||
| 283 | - }) | ||
| 284 | - | ||
| 285 | - // 预览成功,阻止默认的文件打开行为 | ||
| 286 | - return false | ||
| 287 | - } catch (err) { | ||
| 288 | - console.error('[Week Hot] 图片预览失败:', err) | ||
| 289 | - Taro.showToast({ | ||
| 290 | - title: '图片预览失败', | ||
| 291 | - icon: 'none', | ||
| 292 | - duration: 2000 | ||
| 293 | - }) | ||
| 294 | - // 预览失败,返回 true 继续默认行为 | ||
| 295 | - return true | ||
| 296 | - } | ||
| 297 | - } | ||
| 298 | - | ||
| 299 | - // 非图片文件:继续默认的文件打开流程 | ||
| 300 | - console.log('[Week Hot] 非图片文件,使用默认打开方式') | ||
| 301 | - return true | ||
| 302 | - }, | ||
| 303 | - onAfterClick: (item) => { | ||
| 304 | - console.log('用户打开了资料:', item.name) | ||
| 305 | - } | ||
| 306 | -}) | ||
| 307 | - | ||
| 308 | -/** | ||
| 309 | - * 切换收藏状态 | ||
| 310 | - * @description 使用 useCollectOperation composable 处理收藏操作 | ||
| 311 | - */ | ||
| 312 | -const { toggleCollect } = useCollectOperation() | ||
| 313 | </script> | 218 | </script> |
| 314 | 219 | ||
| 315 | <style lang="less"> | 220 | <style lang="less"> | ... | ... |
-
Please register or login to post a comment