hookehuyr

feat(material): 完成本周热门资料功能并优化资料列表显示

新增功能:
- 创建本周热门资料详情页面,复用资料列表样式
- 学习人数和热度标签统一显示样式
- 添加文件大小显示

样式优化:
- 统一 tag 样式(padding、圆角、字体大小)
- 加强卡片阴影效果(shadow-sm → shadow-md)
- 添加多行文本省略样式支持(index、week-hot-material、knowledge-base)

Bug 修复:
- 修复收藏状态判断逻辑,支持布尔值 true
- 移除学习人数标签中的火焰图标(避免重复)

页面路由:
- 注册 week-hot-material 页面路由
- 首页"查看更多"链接指向新页面

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
...@@ -17,6 +17,7 @@ const pages = [ ...@@ -17,6 +17,7 @@ const pages = [
17 'pages/product-detail/index', 17 'pages/product-detail/index',
18 'pages/category-list/index', 18 'pages/category-list/index',
19 'pages/material-list/index', 19 'pages/material-list/index',
20 + 'pages/week-hot-material/index',
20 'pages/signing/index', 21 'pages/signing/index',
21 'pages/mine/index', 22 'pages/mine/index',
22 'pages/plan/index', 23 'pages/plan/index',
......
...@@ -93,10 +93,10 @@ ...@@ -93,10 +93,10 @@
93 </view> 93 </view>
94 94
95 <!-- Hot Materials --> 95 <!-- Hot Materials -->
96 - <view v-if="hotMaterials.length > 0" class="bg-white rounded-[32rpx] shadow-sm p-[32rpx] mb-[48rpx]"> 96 + <view v-if="hotMaterials.length > 0" class="bg-white rounded-[32rpx] shadow-md p-[32rpx] mb-[48rpx]">
97 <view class="flex justify-between items-center mb-[24rpx]"> 97 <view class="flex justify-between items-center mb-[24rpx]">
98 <text class="text-gray-900 text-[32rpx] font-bold">本周热门资料</text> 98 <text class="text-gray-900 text-[32rpx] font-bold">本周热门资料</text>
99 - <view class="flex items-center text-blue-600" @tap="go('/pages/material-list/index')"> 99 + <view class="flex items-center text-blue-600" @tap="go('/pages/week-hot-material/index')">
100 <text class="text-[26rpx] mr-[4rpx]">查看更多</text> 100 <text class="text-[26rpx] mr-[4rpx]">查看更多</text>
101 <IconFont name="rectRight" size="12" /> 101 <IconFont name="rectRight" size="12" />
102 </view> 102 </view>
...@@ -106,7 +106,7 @@ ...@@ -106,7 +106,7 @@
106 <view class="flex flex-col gap-[24rpx]"> 106 <view class="flex flex-col gap-[24rpx]">
107 <!-- Material Items --> 107 <!-- Material Items -->
108 <view v-for="(item, index) in hotMaterials" :key="item.id || index" 108 <view v-for="(item, index) in hotMaterials" :key="item.id || index"
109 - class="flex flex-row bg-white rounded-[24rpx] p-[24rpx] border border-gray-50"> 109 + class="flex flex-row bg-white rounded-[24rpx] p-[24rpx] shadow-md border border-gray-50">
110 110
111 <!-- 左侧图标 --> 111 <!-- 左侧图标 -->
112 <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"> 112 <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">
...@@ -115,17 +115,26 @@ ...@@ -115,17 +115,26 @@
115 115
116 <!-- 内容区域 --> 116 <!-- 内容区域 -->
117 <view class="flex-1 min-w-0"> 117 <view class="flex-1 min-w-0">
118 - <text class="text-[#1F2937] text-[30rpx] font-bold leading-[1.4] line-clamp-2 mb-[8rpx]"> 118 + <view class="text-[#1F2937] text-[30rpx] font-bold leading-[1.4] mb-[8rpx] line-clamp-2">
119 {{ item.title }} 119 {{ item.title }}
120 - </text> 120 + </view>
121 + <!-- 学习人数信息 -->
122 + <view v-if="item.learners" class="flex items-center gap-[12rpx] mb-[16rpx]">
123 + <view class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-orange-50 text-orange-600 text-[20rpx] font-medium rounded-[8rpx]">
124 + <text>{{ item.learners }}</text>
125 + </view>
126 + <!-- 学习人数比例 -->
127 + <view v-if="item.readPeoplePercent !== undefined && item.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]">
128 + <text>{{ item.readPeoplePercent }}%热度</text>
129 + </view>
130 + </view>
131 + <!-- 文档类型和文件大小 -->
121 <view class="flex items-center gap-[12rpx] mb-[16rpx]"> 132 <view class="flex items-center gap-[12rpx] mb-[16rpx]">
122 <view class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-gray-100 text-gray-500 text-[20rpx] font-medium rounded-[8rpx]"> 133 <view class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-gray-100 text-gray-500 text-[20rpx] font-medium rounded-[8rpx]">
123 <text>{{ getDocumentLabel(item.fileName) }}</text> 134 <text>{{ getDocumentLabel(item.fileName) }}</text>
124 </view> 135 </view>
125 - <text class="text-[#9CA3AF] text-[22rpx]">{{ item.learners }}</text> 136 + <view v-if="item.fileSize" class="text-[#9CA3AF] text-[22rpx]">
126 - <!-- 学习人数比例 --> 137 + {{ item.fileSize }}
127 - <view v-if="item.readPeoplePercent !== undefined && item.readPeoplePercent !== null" class="inline-flex items-center px-[8rpx] py-[2rpx] bg-green-50 text-green-600 text-[20rpx] font-medium rounded-[6rpx]">
128 - <text>{{ item.readPeoplePercent }}%热度</text>
129 </view> 138 </view>
130 </view> 139 </view>
131 140
...@@ -388,3 +397,15 @@ useShareAppMessage(() => { ...@@ -388,3 +397,15 @@ useShareAppMessage(() => {
388 }; 397 };
389 }); 398 });
390 </script> 399 </script>
400 +
401 +<style lang="less">
402 +/* 多行文本省略 */
403 +.line-clamp-2 {
404 + display: -webkit-box;
405 + -webkit-box-orient: vertical;
406 + -webkit-line-clamp: 2;
407 + line-clamp: 2;
408 + overflow: hidden;
409 + word-break: break-all;
410 +}
411 +</style>
......
...@@ -411,4 +411,14 @@ useReachBottom(() => { ...@@ -411,4 +411,14 @@ useReachBottom(() => {
411 :deep(.nut-tabs__content) { 411 :deep(.nut-tabs__content) {
412 display: none; 412 display: none;
413 } 413 }
414 +
415 +/* 多行文本省略 */
416 +.line-clamp-2 {
417 + display: -webkit-box;
418 + -webkit-box-orient: vertical;
419 + -webkit-line-clamp: 2;
420 + line-clamp: 2;
421 + overflow: hidden;
422 + word-break: break-all;
423 +}
414 </style> 424 </style>
......
1 +/*
2 + * @Date: 2026-02-05
3 + * @Description: 本周热门资料页面配置
4 + */
5 +export default {
6 + navigationBarTitleText: '本周热门资料',
7 + enablePullDownRefresh: true,
8 + backgroundColor: '#F9FAFB',
9 + navigationStyle: 'custom'
10 +}
1 +<!--
2 + * @Date: 2026-02-05
3 + * @Description: 本周热门资料页 - 简化版资料列表(无搜索和Tab)
4 +-->
5 +<template>
6 + <view class="h-screen bg-[#F9FAFB] flex flex-col py-[32rpx]">
7 + <NavHeader title="本周热门资料" />
8 +
9 + <!-- 列表容器 -->
10 + <view
11 + v-if="listVisible"
12 + :key="listRenderKey"
13 + class="flex-1 min-h-0 overflow-y-auto px-[32rpx] pb-[calc(160rpx+env(safe-area-inset-bottom))] box-border"
14 + >
15 + <!-- 加载状态 -->
16 + <view v-if="loading && currentList.length === 0" class="flex items-center justify-center py-[60rpx]">
17 + <view class="loading-spinner"></view>
18 + <text class="ml-[16rpx] text-[#9CA3AF] text-[28rpx]">加载中...</text>
19 + </view>
20 +
21 + <view v-else class="flex flex-col gap-[24rpx]">
22 + <view v-for="(item, index) in currentList" :key="item.meta_id"
23 + class="material-item bg-white rounded-[24rpx] p-[24rpx] shadow-md transition-all duration-200 border border-gray-50 flex flex-row"
24 + :style="{ animationDelay: `${index * 50}ms` }">
25 +
26 + <view
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">
28 + <image
29 + :src="getDocumentIcon(item.extension ? `file.${item.extension}` : item.name)"
30 + class="w-[48rpx] h-[48rpx]"
31 + mode="aspectFit"
32 + />
33 + </view>
34 +
35 + <view class="flex-1 min-w-0">
36 + <view class="text-[#1F2937] text-[30rpx] font-bold leading-[1.4] mb-[8rpx] line-clamp-2">
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 +
74 + <!-- 空状态 -->
75 + <view v-if="currentList.length === 0 && !loading && !loadingMore">
76 + <nut-empty description="暂无热门资料" image="empty" />
77 + </view>
78 +
79 + <!-- 加载更多提示 -->
80 + <view v-if="currentList.length > 0" class="flex items-center justify-center py-[40rpx]">
81 + <view v-if="loadingMore" class="flex items-center">
82 + <view class="loading-spinner-small"></view>
83 + <text class="ml-[12rpx] text-[#9CA3AF] text-[24rpx]">加载中...</text>
84 + </view>
85 + <view v-else-if="!hasMore" class="text-[#9CA3AF] text-[24rpx]">
86 + 没有更多了
87 + </view>
88 + </view>
89 + </view>
90 + </view>
91 + </view>
92 +</template>
93 +
94 +<script setup>
95 +import { ref } from 'vue'
96 +import { useLoad, useReachBottom } from '@tarojs/taro'
97 +import NavHeader from '@/components/NavHeader.vue'
98 +import ListItemActions from '@/components/ListItemActions/index.vue'
99 +import { useListItemClick, ListType } from '@/composables/useListItemClick'
100 +import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'
101 +import { weekHotAPI } from '@/api/file'
102 +import { useCollectOperation } from '@/composables/useCollectOperation'
103 +import Taro from '@tarojs/taro'
104 +
105 +const listVisible = ref(true)
106 +const listRenderKey = ref(0)
107 +const loading = ref(false)
108 +const loadingMore = ref(false) // 加载更多状态
109 +const hasMore = ref(true) // 是否还有更多数据
110 +const currentList = ref([])
111 +const currentPage = ref(0) // 当前页码(从0开始)
112 +const pageSize = 20 // 每页数量
113 +
114 +/**
115 + * 转换文档数据格式
116 + * @description 将 API 返回的文档数据转换为组件需要的格式
117 + * @param {Object} doc - API 返回的文档对象
118 + * @returns {Object} 转换后的文档对象
119 + */
120 +const transformDocItem = (doc) => {
121 + // 处理文件名为空的情况
122 + const fileName = doc.name || '未命名文件'
123 + // 如果没有扩展名,从文件名中提取(如果有)
124 + const extension = doc.extension || fileName.split('.').pop()?.toLowerCase() || ''
125 +
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 + }
136 +}
137 +
138 +/**
139 + * 获取本周热门资料列表
140 + * @param {Object} params - 请求参数
141 + * @param {number} params.page - 页码(从0开始)
142 + * @param {number} params.limit - 每页数量
143 + * @param {boolean} params.isLoadMore - 是否为加载更多
144 + */
145 +const fetchWeekHotList = async (params = {}, isLoadMore = false) => {
146 + try {
147 + // 如果是加载更多,使用 loadingMore 状态,否则使用 loading 状态
148 + if (isLoadMore) {
149 + loadingMore.value = true
150 + } else {
151 + loading.value = true
152 + }
153 +
154 + console.log('[Week Hot] 请求参数:', params)
155 +
156 + // 调用接口
157 + const res = await weekHotAPI(params)
158 +
159 + if (res.code === 1 && res.data) {
160 + console.log('[Week Hot] 数据:', res.data)
161 +
162 + // 处理列表数据
163 + if (res.data.list?.length) {
164 + const listData = res.data.list.map(transformDocItem)
165 +
166 + if (isLoadMore) {
167 + // 加载更多:追加数据
168 + currentList.value = [...currentList.value, ...listData]
169 + } else {
170 + // 首次加载或刷新:替换数据
171 + currentList.value = listData
172 + }
173 +
174 + // 判断是否还有更多数据
175 + // 如果返回的数据量少于请求的量,说明没有更多了
176 + hasMore.value = listData.length >= params.limit
177 + } else {
178 + // 没有数据了
179 + if (isLoadMore) {
180 + hasMore.value = false
181 + } else {
182 + currentList.value = []
183 + }
184 + }
185 + } else {
186 + Taro.showToast({
187 + title: res.msg || '获取热门资料失败',
188 + icon: 'none',
189 + duration: 2000
190 + })
191 + }
192 + } catch (error) {
193 + console.error('[Week Hot] 获取热门资料失败:', error)
194 + Taro.showToast({
195 + title: '加载失败',
196 + icon: 'error',
197 + duration: 2000
198 + })
199 + } finally {
200 + if (isLoadMore) {
201 + loadingMore.value = false
202 + } else {
203 + loading.value = false
204 + }
205 + }
206 +}
207 +
208 +/**
209 + * 页面加载时获取数据
210 + */
211 +useLoad(async (options) => {
212 + console.log('[Week Hot] 页面参数:', options)
213 +
214 + // 重置分页状态
215 + currentPage.value = 0
216 + hasMore.value = true
217 +
218 + // 获取本周热门资料列表
219 + await fetchWeekHotList({ page: 0, limit: pageSize })
220 +})
221 +
222 +/**
223 + * 触底加载更多
224 + * @description 使用防抖避免频繁触发
225 + */
226 +let loadMoreTimer = null
227 +useReachBottom(() => {
228 + // 如果正在加载或没有更多数据,不执行
229 + if (loadingMore.value || !hasMore.value) {
230 + return
231 + }
232 +
233 + // 防抖:300ms 内只触发一次
234 + if (loadMoreTimer) {
235 + clearTimeout(loadMoreTimer)
236 + }
237 +
238 + loadMoreTimer = setTimeout(async () => {
239 + console.log('[Week Hot] 触底加载更多')
240 +
241 + // 页码 +1
242 + currentPage.value += 1
243 +
244 + // 加载下一页数据
245 + await fetchWeekHotList(
246 + { page: currentPage.value, limit: pageSize },
247 + true // 标记为加载更多
248 + )
249 + }, 300)
250 +})
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 + Taro.showToast({
279 + title: '点击图片预览,长按可保存到相册',
280 + icon: 'none',
281 + duration: 2000
282 + })
283 +
284 + // 短暂延迟后打开预览(让用户看到提示)
285 + await new Promise(resolve => setTimeout(resolve, 300))
286 +
287 + await Taro.previewImage({
288 + current: item.downloadUrl,
289 + urls: urls
290 + })
291 +
292 + // 预览成功,阻止默认的文件打开行为
293 + return false
294 + } catch (err) {
295 + console.error('[Week Hot] 图片预览失败:', err)
296 + Taro.showToast({
297 + title: '图片预览失败',
298 + icon: 'none',
299 + duration: 2000
300 + })
301 + // 预览失败,返回 true 继续默认行为
302 + return true
303 + }
304 + }
305 +
306 + // 非图片文件:继续默认的文件打开流程
307 + console.log('[Week Hot] 非图片文件,使用默认打开方式')
308 + return true
309 + },
310 + onAfterClick: (item) => {
311 + console.log('用户打开了资料:', item.name)
312 + }
313 +})
314 +
315 +/**
316 + * 切换收藏状态
317 + * @description 使用 useCollectOperation composable 处理收藏操作
318 + */
319 +const { toggleCollect } = useCollectOperation()
320 +</script>
321 +
322 +<style lang="less">
323 +/* 列表项进入动画 */
324 +@keyframes slideIn {
325 + from {
326 + opacity: 0;
327 + transform: translateY(20rpx);
328 + }
329 +
330 + to {
331 + opacity: 1;
332 + transform: translateY(0);
333 + }
334 +}
335 +
336 +.material-item {
337 + animation: slideIn 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
338 +}
339 +
340 +/* 加载动画 */
341 +@keyframes spin {
342 + 0% {
343 + transform: rotate(0deg);
344 + }
345 + 100% {
346 + transform: rotate(360deg);
347 + }
348 +}
349 +
350 +.loading-spinner {
351 + width: 40rpx;
352 + height: 40rpx;
353 + border: 4rpx solid #E5E7EB;
354 + border-top-color: #4CAF50;
355 + border-radius: 50%;
356 + animation: spin 0.8s linear infinite;
357 +}
358 +
359 +.loading-spinner-small {
360 + width: 32rpx;
361 + height: 32rpx;
362 + border: 3rpx solid #E5E7EB;
363 + border-top-color: #4CAF50;
364 + border-radius: 50%;
365 + animation: spin 0.8s linear infinite;
366 +}
367 +
368 +/* 多行文本省略 */
369 +.line-clamp-2 {
370 + display: -webkit-box;
371 + -webkit-box-orient: vertical;
372 + -webkit-line-clamp: 2;
373 + line-clamp: 2;
374 + overflow: hidden;
375 + word-break: break-all;
376 +}
377 +</style>