hookehuyr

feat(list): 新增资料列表操作组件并统一页面样式

- 新增 ListItemActions 组件,支持查看、收藏、删除操作
- 修复资料列表页布局问题(图标和文字水平排列)
- 统一首页和资料列表页的资料项样式和交互
- 更新收藏页、知识库页、计划页使用新组件
- 添加完整的组件文档和类型定义

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
...@@ -10,14 +10,10 @@ declare module 'vue' { ...@@ -10,14 +10,10 @@ declare module 'vue' {
10 DocumentPreview: typeof import('./src/components/DocumentPreview/index.vue')['default'] 10 DocumentPreview: typeof import('./src/components/DocumentPreview/index.vue')['default']
11 IconFont: typeof import('./src/components/IconFont.vue')['default'] 11 IconFont: typeof import('./src/components/IconFont.vue')['default']
12 IndexNav: typeof import('./src/components/indexNav.vue')['default'] 12 IndexNav: typeof import('./src/components/indexNav.vue')['default']
13 + ListItemActions: typeof import('./src/components/ListItemActions/index.vue')['default']
13 NavHeader: typeof import('./src/components/NavHeader.vue')['default'] 14 NavHeader: typeof import('./src/components/NavHeader.vue')['default']
14 NutAvatar: typeof import('@nutui/nutui-taro')['Avatar'] 15 NutAvatar: typeof import('@nutui/nutui-taro')['Avatar']
15 NutButton: typeof import('@nutui/nutui-taro')['Button'] 16 NutButton: typeof import('@nutui/nutui-taro')['Button']
16 - NutCheckbox: typeof import('@nutui/nutui-taro')['Checkbox']
17 - NutCheckboxGroup: typeof import('@nutui/nutui-taro')['CheckboxGroup']
18 - NutForm: typeof import('@nutui/nutui-taro')['Form']
19 - NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
20 - NutIcon: typeof import('@nutui/nutui-taro')['Icon']
21 NutInput: typeof import('@nutui/nutui-taro')['Input'] 17 NutInput: typeof import('@nutui/nutui-taro')['Input']
22 NutPicker: typeof import('@nutui/nutui-taro')['Picker'] 18 NutPicker: typeof import('@nutui/nutui-taro')['Picker']
23 NutPopup: typeof import('@nutui/nutui-taro')['Popup'] 19 NutPopup: typeof import('@nutui/nutui-taro')['Popup']
...@@ -26,7 +22,6 @@ declare module 'vue' { ...@@ -26,7 +22,6 @@ declare module 'vue' {
26 NutSearchbar: typeof import('@nutui/nutui-taro')['Searchbar'] 22 NutSearchbar: typeof import('@nutui/nutui-taro')['Searchbar']
27 NutTabPane: typeof import('@nutui/nutui-taro')['TabPane'] 23 NutTabPane: typeof import('@nutui/nutui-taro')['TabPane']
28 NutTabs: typeof import('@nutui/nutui-taro')['Tabs'] 24 NutTabs: typeof import('@nutui/nutui-taro')['Tabs']
29 - NutTextarea: typeof import('@nutui/nutui-taro')['Textarea']
30 NutUploader: typeof import('@nutui/nutui-taro')['Uploader'] 25 NutUploader: typeof import('@nutui/nutui-taro')['Uploader']
31 OfficeViewer: typeof import('./src/components/OfficeViewer.vue')['default'] 26 OfficeViewer: typeof import('./src/components/OfficeViewer.vue')['default']
32 PdfPreview: typeof import('./src/components/PdfPreview.vue')['default'] 27 PdfPreview: typeof import('./src/components/PdfPreview.vue')['default']
......
1 +<!--
2 + 列表项操作按钮组件
3 +
4 + @description 统一的列表项操作按钮组件,支持查看、收藏、删除三种操作
5 + @example
6 + <ListItemActions
7 + :viewable="true"
8 + :collectable="true"
9 + :deletable="true"
10 + :collected="item.collected"
11 + @view="onView(item)"
12 + @collect="onCollect(item)"
13 + @delete="onDelete(item)"
14 + />
15 +-->
16 +<template>
17 + <view class="flex justify-end gap-[24rpx]">
18 + <!-- 查看按钮 -->
19 + <view v-if="viewable" class="flex items-center text-blue-600" @tap="handleView">
20 + <IconFont name="eye" size="14" class="mr-[8rpx]" />
21 + <text class="text-[24rpx]">查看</text>
22 + </view>
23 +
24 + <!-- 收藏按钮 -->
25 + <view v-if="collectable" class="flex items-center" :class="isCollected ? 'text-red-500' : 'text-gray-400'" @tap="handleCollect">
26 + <IconFont :name="isCollected ? 'heart-fill' : 'heart'" size="14" class="mr-[8rpx]" />
27 + <text class="text-[24rpx]">{{ isCollected ? '已收藏' : '收藏' }}</text>
28 + </view>
29 +
30 + <!-- 删除按钮 -->
31 + <view v-if="deletable" class="flex items-center text-red-500" @tap="handleDelete">
32 + <IconFont name="del" size="14" class="mr-[8rpx]" />
33 + <text class="text-[24rpx]">删除</text>
34 + </view>
35 + </view>
36 +</template>
37 +
38 +<script setup>
39 +import { computed } from 'vue'
40 +import IconFont from '@/components/IconFont.vue'
41 +
42 +/**
43 + * 组件属性
44 + */
45 +const props = defineProps({
46 + /**
47 + * 是否显示查看按钮
48 + * @type {boolean}
49 + * @default true
50 + */
51 + viewable: {
52 + type: Boolean,
53 + default: true
54 + },
55 + /**
56 + * 是否显示收藏按钮
57 + * @type {boolean}
58 + * @default false
59 + */
60 + collectable: {
61 + type: Boolean,
62 + default: false
63 + },
64 + /**
65 + * 是否显示删除按钮
66 + * @type {boolean}
67 + * @default false
68 + */
69 + deletable: {
70 + type: Boolean,
71 + default: false
72 + },
73 + /**
74 + * 是否已收藏
75 + * @type {boolean}
76 + * @default false
77 + */
78 + collected: {
79 + type: Boolean,
80 + default: false
81 + }
82 +})
83 +
84 +/**
85 + * 组件事件
86 + */
87 +const emit = defineEmits({
88 + /**
89 + * 点击查看按钮时触发
90 + */
91 + view: null,
92 + /**
93 + * 点击收藏按钮时触发
94 + */
95 + collect: null,
96 + /**
97 + * 点击删除按钮时触发
98 + */
99 + delete: null
100 +})
101 +
102 +const isCollected = computed(() => props.collected)
103 +
104 +/**
105 + * 处理查看点击
106 + */
107 +const handleView = () => {
108 + emit('view')
109 +}
110 +
111 +/**
112 + * 处理收藏点击
113 + */
114 +const handleCollect = () => {
115 + emit('collect')
116 +}
117 +
118 +/**
119 + * 处理删除点击
120 + */
121 +const handleDelete = () => {
122 + emit('delete')
123 +}
124 +</script>
125 +
126 +<style lang="less" scoped>
127 +</style>
...@@ -43,16 +43,12 @@ ...@@ -43,16 +43,12 @@
43 <view class="h-[1rpx] bg-gray-100 mb-[20rpx]"></view> 43 <view class="h-[1rpx] bg-gray-100 mb-[20rpx]"></view>
44 44
45 <!-- Actions --> 45 <!-- Actions -->
46 - <view class="flex justify-end gap-[24rpx]"> 46 + <ListItemActions
47 - <view class="flex items-center text-blue-600" @tap="viewFile({...item, fileName: item.title})"> 47 + :viewable="true"
48 - <IconFont name="eye" size="14" class="mr-[8rpx]" /> 48 + :deletable="true"
49 - <text class="text-[24rpx]">查看</text> 49 + @view="viewFile({...item, fileName: item.title})"
50 - </view> 50 + @delete="onDelete(item)"
51 - <view class="flex items-center text-red-500" @tap="onDelete(item)"> 51 + />
52 - <IconFont name="del" size="14" class="mr-[8rpx]" />
53 - <text class="text-[24rpx]">删除</text>
54 - </view>
55 - </view>
56 </view> 52 </view>
57 53
58 <!-- Empty State --> 54 <!-- Empty State -->
...@@ -77,6 +73,7 @@ import { getDocumentIcon } from '@/utils/documentIcons' ...@@ -77,6 +73,7 @@ import { getDocumentIcon } from '@/utils/documentIcons'
77 import IconFont from '@/components/IconFont.vue' 73 import IconFont from '@/components/IconFont.vue'
78 import TabBar from '@/components/TabBar.vue' 74 import TabBar from '@/components/TabBar.vue'
79 import NavHeader from '@/components/NavHeader.vue' 75 import NavHeader from '@/components/NavHeader.vue'
76 +import ListItemActions from '@/components/ListItemActions/index.vue'
80 77
81 const go = useGo() 78 const go = useGo()
82 const { viewFile } = useFileOperation() 79 const { viewFile } = useFileOperation()
......
...@@ -126,61 +126,40 @@ ...@@ -126,61 +126,40 @@
126 </view> 126 </view>
127 127
128 <!-- Material List --> 128 <!-- Material List -->
129 - <view class="flex flex-col gap-[32rpx]"> 129 + <view class="flex flex-col gap-[24rpx]">
130 - <!-- Item 1 --> 130 + <!-- Material Items -->
131 - <view class="flex gap-[24rpx]" @tap="handleMaterialClick(hotMaterials[0])"> 131 + <view v-for="(item, index) in hotMaterials" :key="index"
132 - <view class="w-[80rpx] h-[88rpx] flex-shrink-0 flex items-center justify-center bg-blue-50 rounded-[12rpx]"> 132 + class="flex flex-row bg-white rounded-[24rpx] p-[24rpx] border border-gray-50">
133 - <image :src="getDocumentIcon(hotMaterials[0].fileName)" class="w-[48rpx] h-[48rpx]" mode="aspectFit" /> 133 +
134 - </view> 134 + <!-- 左侧图标 -->
135 - <view class="flex-1 flex flex-col justify-between py-[4rpx]"> 135 + <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">
136 - <text class="text-gray-800 text-[28rpx] leading-[40rpx] line-clamp-2 mb-1">{{ hotMaterials[0].title }}</text> 136 + <image :src="getDocumentIcon(item.fileName)" class="w-[48rpx] h-[48rpx]" mode="aspectFit" />
137 - <view class="flex items-center gap-2 mb-1">
138 - <view class="bg-blue-50 rounded px-2 py-0.5">
139 - <text class="text-blue-600 text-[22rpx]">{{ getDocumentLabel(hotMaterials[0].fileName) }}</text>
140 - </view>
141 - </view>
142 - <view class="flex justify-between items-end">
143 - <text class="text-gray-400 text-[24rpx]">{{ hotMaterials[0].learners }}</text>
144 - <text class="text-blue-600 text-[26rpx]">{{ hotMaterials[0].progress }}</text>
145 - </view>
146 - </view>
147 - </view>
148 - <view class="h-[2rpx] bg-gray-100"></view>
149 - <!-- Item 2 -->
150 - <view class="flex gap-[24rpx]" @tap="handleMaterialClick(hotMaterials[1])">
151 - <view class="w-[80rpx] h-[88rpx] flex-shrink-0 flex items-center justify-center bg-blue-50 rounded-[12rpx]">
152 - <image :src="getDocumentIcon(hotMaterials[1].fileName)" class="w-[48rpx] h-[48rpx]" mode="aspectFit" />
153 - </view>
154 - <view class="flex-1 flex flex-col justify-between py-[4rpx]">
155 - <text class="text-gray-800 text-[28rpx] leading-[40rpx] line-clamp-2 mb-1">{{ hotMaterials[1].title }}</text>
156 - <view class="flex items-center gap-2 mb-1">
157 - <view class="bg-blue-50 rounded px-2 py-0.5">
158 - <text class="text-blue-600 text-[22rpx]">{{ getDocumentLabel(hotMaterials[1].fileName) }}</text>
159 - </view>
160 - </view>
161 - <view class="flex justify-between items-end">
162 - <text class="text-gray-400 text-[24rpx]">{{ hotMaterials[1].learners }}</text>
163 - <text class="text-blue-600 text-[26rpx]">{{ hotMaterials[1].progress }}</text>
164 - </view>
165 - </view>
166 - </view>
167 - <view class="h-[2rpx] bg-gray-100"></view>
168 - <!-- Item 3 -->
169 - <view class="flex gap-[24rpx]" @tap="handleMaterialClick(hotMaterials[2])">
170 - <view class="w-[80rpx] h-[88rpx] flex-shrink-0 flex items-center justify-center bg-blue-50 rounded-[12rpx]">
171 - <image :src="getDocumentIcon(hotMaterials[2].fileName)" class="w-[48rpx] h-[48rpx]" mode="aspectFit" />
172 - </view>
173 - <view class="flex-1 flex flex-col justify-between py-[4rpx]">
174 - <text class="text-gray-800 text-[28rpx] leading-[40rpx] line-clamp-2 mb-1">{{ hotMaterials[2].title }}</text>
175 - <view class="flex items-center gap-2 mb-1">
176 - <view class="bg-blue-50 rounded px-2 py-0.5">
177 - <text class="text-blue-600 text-[22rpx]">{{ getDocumentLabel(hotMaterials[2].fileName) }}</text>
178 </view> 137 </view>
138 +
139 + <!-- 内容区域 -->
140 + <view class="flex-1 min-w-0">
141 + <text class="text-[#1F2937] text-[30rpx] font-bold leading-[1.4] line-clamp-2 mb-[8rpx]">
142 + {{ item.title }}
143 + </text>
144 + <view class="flex items-center gap-[12rpx] mb-[16rpx]">
145 + <view class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-gray-100 text-gray-500 text-[20rpx] font-medium rounded-[8rpx]">
146 + <text>{{ getDocumentLabel(item.fileName) }}</text>
179 </view> 147 </view>
180 - <view class="flex justify-between items-end"> 148 + <text class="text-[#9CA3AF] text-[22rpx]">{{ item.learners }}</text>
181 - <text class="text-gray-400 text-[24rpx]">{{ hotMaterials[2].learners }}</text>
182 - <text class="text-blue-600 text-[26rpx]">{{ hotMaterials[2].progress }}</text>
183 </view> 149 </view>
150 +
151 + <!-- 分割线 -->
152 + <view class="h-[1rpx] bg-gray-100 my-[20rpx]"></view>
153 +
154 + <!-- 操作按钮 -->
155 + <ListItemActions
156 + :viewable="true"
157 + :collectable="true"
158 + :deletable="false"
159 + :collected="item.collected"
160 + @view="onViewMaterial(item)"
161 + @collect="toggleMaterialCollect(item)"
162 + />
184 </view> 163 </view>
185 </view> 164 </view>
186 </view> 165 </view>
...@@ -217,6 +196,7 @@ import IconFont from '@/components/IconFont.vue'; ...@@ -217,6 +196,7 @@ import IconFont from '@/components/IconFont.vue';
217 import PlanPopup from '@/components/PlanPopup/index.vue'; 196 import PlanPopup from '@/components/PlanPopup/index.vue';
218 import SchemeA from '@/components/PlanSchemes/SchemeA.vue'; 197 import SchemeA from '@/components/PlanSchemes/SchemeA.vue';
219 import SchemeB from '@/components/PlanSchemes/SchemeB.vue'; 198 import SchemeB from '@/components/PlanSchemes/SchemeB.vue';
199 +import ListItemActions from '@/components/ListItemActions/index.vue';
220 200
221 // Plan Popup State 201 // Plan Popup State
222 const showPlanPopup = ref(false); 202 const showPlanPopup = ref(false);
...@@ -256,6 +236,7 @@ const hotMaterials = ref([ ...@@ -256,6 +236,7 @@ const hotMaterials = ref([
256 title: '2024年保险市场趋势分析报告', 236 title: '2024年保险市场趋势分析报告',
257 learners: '256人学习', 237 learners: '256人学习',
258 progress: '78%', 238 progress: '78%',
239 + collected: false,
259 // PDF 文件 240 // PDF 文件
260 fileName: '2024年保险市场趋势分析报告.pdf', 241 fileName: '2024年保险市场趋势分析报告.pdf',
261 downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf' 242 downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
...@@ -264,6 +245,7 @@ const hotMaterials = ref([ ...@@ -264,6 +245,7 @@ const hotMaterials = ref([
264 title: '高净值客户产品配置方案模板', 245 title: '高净值客户产品配置方案模板',
265 learners: '189人学习', 246 learners: '189人学习',
266 progress: '65%', 247 progress: '65%',
248 + collected: true,
267 // Word 文件 249 // Word 文件
268 fileName: '高净值客户产品配置方案模板.docx', 250 fileName: '高净值客户产品配置方案模板.docx',
269 downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf' 251 downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
...@@ -272,6 +254,7 @@ const hotMaterials = ref([ ...@@ -272,6 +254,7 @@ const hotMaterials = ref([
272 title: '产品收益率测算表(2024版)', 254 title: '产品收益率测算表(2024版)',
273 learners: '142人学习', 255 learners: '142人学习',
274 progress: '52%', 256 progress: '52%',
257 + collected: false,
275 // Excel 文件 258 // Excel 文件
276 fileName: '产品收益率测算表.xlsx', 259 fileName: '产品收益率测算表.xlsx',
277 downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf' 260 downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
...@@ -286,13 +269,28 @@ const go = useGo(); ...@@ -286,13 +269,28 @@ const go = useGo();
286 * 269 *
287 * @description 配置为文件类型列表,点击时打开文件预览 270 * @description 配置为文件类型列表,点击时打开文件预览
288 */ 271 */
289 -const { handleClick: handleMaterialClick } = useListItemClick({ 272 +const { handleClick: onViewMaterial } = useListItemClick({
290 listType: ListType.FILE, 273 listType: ListType.FILE,
291 onAfterClick: (item) => { 274 onAfterClick: (item) => {
292 console.log('用户打开了资料:', item.title); 275 console.log('用户打开了资料:', item.title);
293 } 276 }
294 }); 277 });
295 278
279 +/**
280 + * 切换资料收藏状态
281 + *
282 + * @description 切换热门资料的收藏状态
283 + * @param {Object} item - 资料项
284 + */
285 +const toggleMaterialCollect = (item) => {
286 + item.collected = !item.collected;
287 + Taro.showToast({
288 + title: item.collected ? '已收藏' : '已取消收藏',
289 + icon: 'success',
290 + duration: 1000
291 + });
292 +};
293 +
296 // Handle grid navigation click 294 // Handle grid navigation click
297 const handleGridNav = (item) => { 295 const handleGridNav = (item) => {
298 if (!item.route) { 296 if (!item.route) {
......
...@@ -45,7 +45,8 @@ ...@@ -45,7 +45,8 @@
45 </div> 45 </div>
46 46
47 <!-- Desc --> 47 <!-- Desc -->
48 - <div class="mt-auto self-start bg-[#F3F4F6] text-[#6B7280] text-[22rpx] px-[12rpx] py-[4rpx] rounded-full"> 48 + <div class="mt-auto self-start text-[22rpx] px-[12rpx] py-[4rpx] rounded-full"
49 + :class="[getDescColor(item.id).bg, getDescColor(item.id).text]">
49 {{ item.desc }} 50 {{ item.desc }}
50 </div> 51 </div>
51 </div> 52 </div>
...@@ -68,6 +69,34 @@ const activeTab = ref(0) ...@@ -68,6 +69,34 @@ const activeTab = ref(0)
68 const tabs = ['全部产品', '人寿保险', '医疗保险', '意外保险'] 69 const tabs = ['全部产品', '人寿保险', '医疗保险', '意外保险']
69 70
70 /** 71 /**
72 + * Desc 颜色调色板
73 + *
74 + * @description 为不同描述提供柔和的背景色和对应的文字颜色
75 + */
76 +const descColorPalette = [
77 + { bg: 'bg-blue-50', text: 'text-blue-600' }, // 蓝色
78 + { bg: 'bg-green-50', text: 'text-green-600' }, // 绿色
79 + { bg: 'bg-purple-50', text: 'text-purple-600' }, // 紫色
80 + { bg: 'bg-orange-50', text: 'text-orange-600' }, // 橙色
81 + { bg: 'bg-pink-50', text: 'text-pink-600' }, // 粉色
82 + { bg: 'bg-teal-50', text: 'text-teal-600' }, // 青色
83 + { bg: 'bg-indigo-50', text: 'text-indigo-600' }, // 靛蓝色
84 + { bg: 'bg-red-50', text: 'text-red-600' }, // 红色
85 +]
86 +
87 +/**
88 + * 获取 desc 的颜色样式
89 + *
90 + * @description 根据 ID 获取固定的背景色和文字颜色
91 + * @param {number} id - 产品 ID
92 + * @returns {Object} 包含 bg 和 text 属性的颜色对象
93 + */
94 +const getDescColor = (id) => {
95 + const index = id % descColorPalette.length
96 + return descColorPalette[index]
97 +}
98 +
99 +/**
71 * 生成产品数据 100 * 生成产品数据
72 * 101 *
73 * @description 生成模拟产品列表数据,每个产品包含唯一 id 102 * @description 生成模拟产品列表数据,每个产品包含唯一 id
......
1 <!-- 1 <!--
2 - * @Date: 2026-01-30 2 + * @Date: 2026-01-31
3 * @Description: 资料列表页 3 * @Description: 资料列表页
4 --> 4 -->
5 <template> 5 <template>
...@@ -21,12 +21,12 @@ ...@@ -21,12 +21,12 @@
21 <div class="px-[32rpx] mt-[32rpx]"> 21 <div class="px-[32rpx] mt-[32rpx]">
22 <div class="flex flex-col gap-[24rpx]"> 22 <div class="flex flex-col gap-[24rpx]">
23 <div v-for="(item, index) in list" :key="index" 23 <div v-for="(item, index) in list" :key="index"
24 - class="material-item bg-white rounded-[24rpx] p-[24rpx] shadow-sm flex items-start transition-all duration-200 border border-gray-50" 24 + class="material-item bg-white rounded-[24rpx] p-[24rpx] shadow-sm transition-all duration-200 border border-gray-50 flex flex-row"
25 :style="{ animationDelay: `${index * 50}ms` }"> 25 :style="{ animationDelay: `${index * 50}ms` }">
26 26
27 <!-- 左侧图标 --> 27 <!-- 左侧图标 -->
28 <div 28 <div
29 - 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"> 29 + 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">
30 <image 30 <image
31 :src="getDocumentIcon(item.fileName)" 31 :src="getDocumentIcon(item.fileName)"
32 class="w-[48rpx] h-[48rpx]" 32 class="w-[48rpx] h-[48rpx]"
...@@ -36,30 +36,15 @@ ...@@ -36,30 +36,15 @@
36 36
37 <!-- 内容区域 --> 37 <!-- 内容区域 -->
38 <div class="flex-1 min-w-0"> 38 <div class="flex-1 min-w-0">
39 - <div class="flex justify-between items-start gap-[16rpx]">
40 - <div class="flex-1 pr-[8rpx]">
41 <h3 class="text-[#1F2937] text-[30rpx] font-bold leading-[1.4] line-clamp-2 mb-[8rpx]"> 39 <h3 class="text-[#1F2937] text-[30rpx] font-bold leading-[1.4] line-clamp-2 mb-[8rpx]">
42 {{ item.title }} 40 {{ item.title }}
43 </h3> 41 </h3>
44 <p class="text-[#6B7280] text-[24rpx] leading-[1.4] line-clamp-1 mb-[16rpx]"> 42 <p class="text-[#6B7280] text-[24rpx] leading-[1.4] line-clamp-1 mb-[16rpx]">
45 {{ item.desc }} 43 {{ item.desc }}
46 </p> 44 </p>
47 - </div>
48 -
49 - <!-- 收藏图标 -->
50 - <div
51 - class="heart-btn w-[64rpx] h-[64rpx] -mt-[12rpx] -mr-[12rpx] flex items-center justify-center rounded-full active:bg-gray-50 transition-colors"
52 - @click.stop="toggleCollect(item)">
53 - <div :class="{ 'is-collected': item.collected }" class="transform transition-transform duration-300">
54 - <IconFont :name="item.collected ? 'heart-fill' : 'heart'" size="24"
55 - :color="item.collected ? '#EF4444' : '#D1D5DB'" />
56 - </div>
57 - </div>
58 - </div>
59 45
60 - <!-- 底部信息:文件类型、大小和查看按钮 --> 46 + <!-- 文件信息 -->
61 - <div class="flex items-center justify-between"> 47 + <div class="flex items-center gap-[12rpx] mb-[20rpx]">
62 - <div class="flex items-center gap-[12rpx]">
63 <span 48 <span
64 class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-gray-100 text-gray-500 text-[20rpx] font-medium rounded-[8rpx]"> 49 class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-gray-100 text-gray-500 text-[20rpx] font-medium rounded-[8rpx]">
65 {{ getDocumentLabel(item.fileName) }} 50 {{ getDocumentLabel(item.fileName) }}
...@@ -69,14 +54,18 @@ ...@@ -69,14 +54,18 @@
69 </span> 54 </span>
70 </div> 55 </div>
71 56
72 - <!-- 查看按钮 --> 57 + <!-- 分割线 -->
73 - <view 58 + <view class="h-[1rpx] bg-gray-100 my-[20rpx]"></view>
74 - class="flex items-center justify-center px-[20rpx] py-[10rpx] bg-blue-50 rounded-full active:bg-blue-100 transition-colors" 59 +
75 - @tap.stop="onView(item)"> 60 + <!-- 操作按钮 -->
76 - <IconFont name="eye" size="14" color="#2563EB" class="mr-[6rpx]" /> 61 + <ListItemActions
77 - <text class="text-[24rpx] text-blue-600 font-medium leading-none ml-[8rpx]">查看</text> 62 + :viewable="true"
78 - </view> 63 + :collectable="true"
79 - </div> 64 + :collected="item.collected"
65 + @view="onView(item)"
66 + @collect="toggleCollect(item)"
67 + @delete="onDelete(item)"
68 + />
80 </div> 69 </div>
81 </div> 70 </div>
82 </div> 71 </div>
...@@ -92,8 +81,10 @@ import { ref } from 'vue' ...@@ -92,8 +81,10 @@ import { ref } from 'vue'
92 import NavHeader from '@/components/NavHeader.vue' 81 import NavHeader from '@/components/NavHeader.vue'
93 import TabBar from '@/components/TabBar.vue' 82 import TabBar from '@/components/TabBar.vue'
94 import IconFont from '@/components/IconFont.vue' 83 import IconFont from '@/components/IconFont.vue'
84 +import ListItemActions from '@/components/ListItemActions/index.vue'
95 import { useListItemClick, ListType } from '@/composables/useListItemClick' 85 import { useListItemClick, ListType } from '@/composables/useListItemClick'
96 import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons' 86 import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'
87 +import Taro from '@tarojs/taro'
97 88
98 const searchValue = ref('') 89 const searchValue = ref('')
99 90
...@@ -111,7 +102,7 @@ const list = ref([ ...@@ -111,7 +102,7 @@ const list = ref([
111 iconColor: '#EF4444', 102 iconColor: '#EF4444',
112 collected: true, 103 collected: true,
113 fileName: '2024年保险代理人考试大纲.pdf', 104 fileName: '2024年保险代理人考试大纲.pdf',
114 - downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf' 105 + downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
115 }, 106 },
116 { 107 {
117 title: '历年真题汇总及解析.pdf', 108 title: '历年真题汇总及解析.pdf',
...@@ -121,7 +112,7 @@ const list = ref([ ...@@ -121,7 +112,7 @@ const list = ref([
121 iconColor: '#EF4444', 112 iconColor: '#EF4444',
122 collected: false, 113 collected: false,
123 fileName: '历年真题汇总及解析.pdf', 114 fileName: '历年真题汇总及解析.pdf',
124 - downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf' 115 + downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
125 }, 116 },
126 { 117 {
127 title: '考试技巧与经验分享.pdf', 118 title: '考试技巧与经验分享.pdf',
...@@ -131,7 +122,7 @@ const list = ref([ ...@@ -131,7 +122,7 @@ const list = ref([
131 iconColor: '#EF4444', 122 iconColor: '#EF4444',
132 collected: false, 123 collected: false,
133 fileName: '考试技巧与经验分享.pdf', 124 fileName: '考试技巧与经验分享.pdf',
134 - downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf' 125 + downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
135 }, 126 },
136 { 127 {
137 title: '保险基础知识速记手册.pdf', 128 title: '保险基础知识速记手册.pdf',
...@@ -141,7 +132,7 @@ const list = ref([ ...@@ -141,7 +132,7 @@ const list = ref([
141 iconColor: '#EF4444', 132 iconColor: '#EF4444',
142 collected: false, 133 collected: false,
143 fileName: '保险基础知识速记手册.pdf', 134 fileName: '保险基础知识速记手册.pdf',
144 - downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf' 135 + downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
145 }, 136 },
146 { 137 {
147 title: '模拟试卷10套及答案.pdf', 138 title: '模拟试卷10套及答案.pdf',
...@@ -151,7 +142,7 @@ const list = ref([ ...@@ -151,7 +142,7 @@ const list = ref([
151 iconColor: '#EF4444', 142 iconColor: '#EF4444',
152 collected: true, 143 collected: true,
153 fileName: '模拟试卷10套及答案.pdf', 144 fileName: '模拟试卷10套及答案.pdf',
154 - downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf' 145 + downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
155 }, 146 },
156 { 147 {
157 title: '法律法规重点条款解读.pdf', 148 title: '法律法规重点条款解读.pdf',
...@@ -161,7 +152,7 @@ const list = ref([ ...@@ -161,7 +152,7 @@ const list = ref([
161 iconColor: '#EF4444', 152 iconColor: '#EF4444',
162 collected: false, 153 collected: false,
163 fileName: '法律法规重点条款解读.pdf', 154 fileName: '法律法规重点条款解读.pdf',
164 - downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf' 155 + downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
165 }, 156 },
166 { 157 {
167 title: '考试常见易错题分析.pdf', 158 title: '考试常见易错题分析.pdf',
...@@ -171,7 +162,7 @@ const list = ref([ ...@@ -171,7 +162,7 @@ const list = ref([
171 iconColor: '#EF4444', 162 iconColor: '#EF4444',
172 collected: false, 163 collected: false,
173 fileName: '考试常见易错题分析.pdf', 164 fileName: '考试常见易错题分析.pdf',
174 - downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf' 165 + downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
175 }, 166 },
176 { 167 {
177 title: '案例分析题库及解答.pdf', 168 title: '案例分析题库及解答.pdf',
...@@ -181,7 +172,7 @@ const list = ref([ ...@@ -181,7 +172,7 @@ const list = ref([
181 iconColor: '#EF4444', 172 iconColor: '#EF4444',
182 collected: false, 173 collected: false,
183 fileName: '案例分析题库及解答.pdf', 174 fileName: '案例分析题库及解答.pdf',
184 - downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf' 175 + downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
185 }, 176 },
186 { 177 {
187 title: '考前冲刺复习资料.pdf', 178 title: '考前冲刺复习资料.pdf',
...@@ -191,7 +182,7 @@ const list = ref([ ...@@ -191,7 +182,7 @@ const list = ref([
191 iconColor: '#EF4444', 182 iconColor: '#EF4444',
192 collected: false, 183 collected: false,
193 fileName: '考前冲刺复习资料.pdf', 184 fileName: '考前冲刺复习资料.pdf',
194 - downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf' 185 + downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
195 }, 186 },
196 { 187 {
197 title: '考场注意事项及答题技巧.pdf', 188 title: '考场注意事项及答题技巧.pdf',
...@@ -201,7 +192,7 @@ const list = ref([ ...@@ -201,7 +192,7 @@ const list = ref([
201 iconColor: '#EF4444', 192 iconColor: '#EF4444',
202 collected: false, 193 collected: false,
203 fileName: '考场注意事项及答题技巧.pdf', 194 fileName: '考场注意事项及答题技巧.pdf',
204 - downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf' 195 + downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf'
205 } 196 }
206 ]) 197 ])
207 198
...@@ -215,6 +206,18 @@ const onSearch = () => { ...@@ -215,6 +206,18 @@ const onSearch = () => {
215 } 206 }
216 207
217 /** 208 /**
209 + * 使用文件列表点击处理器
210 + *
211 + * @description 配置为文件类型列表,点击时打开文件预览
212 + */
213 +const { handleClick: onView } = useListItemClick({
214 + listType: ListType.FILE,
215 + onAfterClick: (item) => {
216 + console.log('用户打开了资料:', item.title)
217 + }
218 +})
219 +
220 +/**
218 * 切换收藏状态 221 * 切换收藏状态
219 * 222 *
220 * @description 切换资料的收藏状态 223 * @description 切换资料的收藏状态
...@@ -222,19 +225,31 @@ const onSearch = () => { ...@@ -222,19 +225,31 @@ const onSearch = () => {
222 */ 225 */
223 const toggleCollect = (item) => { 226 const toggleCollect = (item) => {
224 item.collected = !item.collected 227 item.collected = !item.collected
228 + Taro.showToast({
229 + title: item.collected ? '已收藏' : '已取消收藏',
230 + icon: 'success',
231 + duration: 1000
232 + })
225 } 233 }
226 234
227 /** 235 /**
228 - * 使用文件列表点击处理器 236 + * 删除资料
229 - *
230 - * @description 配置为文件类型列表,点击时打开文件预览
231 */ 237 */
232 -const { handleClick: onView } = useListItemClick({ 238 +const onDelete = (item) => {
233 - listType: ListType.FILE, 239 + Taro.showModal({
234 - onAfterClick: (item) => { 240 + title: '提示',
235 - console.log('用户打开了资料:', item.title) 241 + content: '确定要删除该资料吗?',
242 + success: (res) => {
243 + if (res.confirm) {
244 + const index = list.value.findIndex(i => i.title === item.title)
245 + if (index !== -1) {
246 + list.value.splice(index, 1)
247 + Taro.showToast({ title: '已删除', icon: 'success' })
236 } 248 }
237 -}) 249 + }
250 + }
251 + })
252 +}
238 </script> 253 </script>
239 254
240 <style lang="less" scoped> 255 <style lang="less" scoped>
...@@ -253,31 +268,4 @@ const { handleClick: onView } = useListItemClick({ ...@@ -253,31 +268,4 @@ const { handleClick: onView } = useListItemClick({
253 .material-item { 268 .material-item {
254 animation: slideIn 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) backwards; 269 animation: slideIn 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
255 } 270 }
256 -
257 -/* 收藏成功的动画 */
258 -.is-collected {
259 - animation: heartBeat 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
260 -}
261 -
262 -@keyframes heartBeat {
263 - 0% {
264 - transform: scale(1);
265 - }
266 -
267 - 14% {
268 - transform: scale(1.3);
269 - }
270 -
271 - 28% {
272 - transform: scale(1);
273 - }
274 -
275 - 42% {
276 - transform: scale(1.3);
277 - }
278 -
279 - 70% {
280 - transform: scale(1);
281 - }
282 -}
283 </style> 271 </style>
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
23 </view> 23 </view>
24 24
25 <!-- Arrow --> 25 <!-- Arrow -->
26 - <IconFont name="rectRight" size="20" color="#9CA3AF" /> 26 + <IconFont name="rect-right" size="20" color="#9CA3AF" />
27 </view> 27 </view>
28 28
29 <!-- Menu List --> 29 <!-- Menu List -->
......
...@@ -53,16 +53,12 @@ ...@@ -53,16 +53,12 @@
53 <view class="h-[1rpx] bg-gray-100 my-[20rpx]"></view> 53 <view class="h-[1rpx] bg-gray-100 my-[20rpx]"></view>
54 54
55 <!-- Actions --> 55 <!-- Actions -->
56 - <view class="flex justify-end gap-[24rpx]"> 56 + <ListItemActions
57 - <view class="flex items-center text-blue-600" @tap="onView(item)"> 57 + :viewable="true"
58 - <IconFont name="eye" size="14" class="mr-[8rpx]" /> 58 + :deletable="true"
59 - <text class="text-[24rpx]">查看</text> 59 + @view="onView(item)"
60 - </view> 60 + @delete="onDelete(item)"
61 - <view class="flex items-center text-red-500" @tap="onDelete(item)"> 61 + />
62 - <IconFont name="del" size="14" class="mr-[8rpx]" />
63 - <text class="text-[24rpx]">删除</text>
64 - </view>
65 - </view>
66 </view> 62 </view>
67 63
68 <!-- Empty State --> 64 <!-- Empty State -->
...@@ -85,6 +81,7 @@ import { useFileOperation } from '@/composables/useFileOperation' ...@@ -85,6 +81,7 @@ import { useFileOperation } from '@/composables/useFileOperation'
85 import IconFont from '@/components/IconFont.vue' 81 import IconFont from '@/components/IconFont.vue'
86 import TabBar from '@/components/TabBar.vue' 82 import TabBar from '@/components/TabBar.vue'
87 import NavHeader from '@/components/NavHeader.vue' 83 import NavHeader from '@/components/NavHeader.vue'
84 +import ListItemActions from '@/components/ListItemActions/index.vue'
88 import Taro from '@tarojs/taro' 85 import Taro from '@tarojs/taro'
89 86
90 const go = useGo() 87 const go = useGo()
......