hookehuyr

feat(docs): 实现文档图标系统并优化首页展示

## 主要更新

### 新增
- 创建文档图标工具函数 (src/utils/documentIcons.js)
  - 根据文件扩展名自动匹配对应图标 (PDF/Word/Excel/PPT/图片/视频等)
  - 提供 getDocumentIcon() 和 getDocumentLabel() 工具函数
  - 支持判断文件类型 (isPDF/isImage/isVideo)
- 添加文档图标资源 (src/assets/images/icon/doc/)
  - 10 个 SVG 图标文件,覆盖常见文档类型
- 新增文档图标更新说明文档 (docs/DOCUMENT_ICONS_UPDATE.md)
- 移动计划书开发指南到组件目录 (src/components/PlanSchemes/)

### 优化
- 更新首页"本周热门资料"展示 (src/pages/index/index.vue)
  - 使用真实文档图标替换通用图标
  - 添加文件类型标签,位于标题下方
  - Mock 不同类型文件数据 (PDF/Word/Excel)
- 更新资料列表页 (src/pages/material-list/index.vue)
  - 集成文档图标系统
- 更新产品详情页 (src/pages/product-detail/index.vue)
  - 集成文档图标系统

### 技术细节
- 使用 ES6 import 正确导入 SVG 资源
- 解决 Taro 小程序静态资源加载问题
- 统一文档类型识别逻辑

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
## [2026-01-31] - 优化首页热门资料展示
### 优化
- 优化首页"本周热门资料"栏目 (`src/pages/index/index.vue`)
- 使用真实的文档图标系统,根据文件后缀自动显示对应图标
- 添加文件类型标签,显示在标题下方(蓝色背景突出显示)
- 更新 Mock 数据,包含不同类型文件(PDF、Word、Excel)
- 集成 `getDocumentIcon``getDocumentLabel` 工具函数
---
**详细信息**
- **影响文件**: src/pages/index/index.vue
- **技术栈**: Vue 3, Taro, TailwindCSS
- **测试状态**: 已通过
- **备注**:
- 将 IconFont 图标替换为 image 标签,使用真实文档图标
- 文件类型标签更明显,提升用户体验
- 支持多种文件类型:PDF、Word、Excel、PPT、图片、视频等
---
## [2026-01-31] - 修复文档图标加载问题
### 修复
- 修复文档图标加载失败(500 错误)的问题
- 原因:使用字符串路径引用静态资源,Taro 构建工具无法正确处理
- 解决:使用 ES6 `import` 导入所有 SVG 图标资源
- 影响:`src/utils/documentIcons.js` 工具函数
---
**详细信息**
- **影响文件**: src/utils/documentIcons.js
- **技术栈**: Vue 3, Taro, ES6 Modules
- **测试状态**: 已通过
- **备注**:
- 将字符串路径(如 `/assets/images/icon/doc/doc.svg`)改为导入变量(如 `docIcon`
- 确保所有 10 个 SVG 图标都正确导入和映射
- 在资料列表页和产品详情页中正确显示文件类型图标
---
## [2026-01-31] - 统一方案A样式实现模式
### 优化
......
# 资料项图标更新总结
> **更新日期**: 2026-01-31
> **更新内容**: 根据文件后缀使用真实的文档图标 SVG
## 更新内容
### 1. 创建文档图标工具函数
**文件**: `src/utils/documentIcons.js`
提供了以下函数:
#### `getDocumentIcon(fileName)`
根据文件名返回对应的文档图标路径
```javascript
import { getDocumentIcon } from '@/utils/documentIcons'
getDocumentIcon('报告.pdf') // '/assets/images/icon/doc/doc.svg'
getDocumentIcon('数据.xlsx') // '/assets/images/icon/doc/xls.svg'
getDocumentIcon('图片.png') // '/assets/images/icon/doc/png.svg'
getDocumentIcon('视频.mp4') // '/assets/images/icon/doc/mp4.svg'
```
#### `getDocumentLabel(fileName)`
根据文件名返回文件类型标签
```javascript
import { getDocumentLabel } from '@/utils/documentIcons'
getDocumentLabel('报告.pdf') // 'PDF'
getDocumentLabel('数据.xlsx') // 'Excel'
getDocumentLabel('图片.png') // 'PNG'
```
#### 其他辅助函数
- `isPDF(fileName)` - 判断是否为 PDF 文件
- `isImage(fileName)` - 判断是否为图片文件
- `isVideo(fileName)` - 判断是否为视频文件
### 2. 支持的文件类型
| 文件类型 | 扩展名 | 图标文件 | 标签 |
|---------|--------|---------|------|
| **通用文档** | .pdf | doc.svg | PDF |
| **Word** | .doc, .docx | word.svg | Word |
| **Excel** | .xls, .xlsx | xls.svg | Excel |
| **PPT** | .ppt, .pptx | ppt.svg | PPT |
| **图片** | .jpg, .jpeg, .png, .gif, .webp | jpeg.svg, png.svg | JPG, PNG, GIF, WebP |
| **视频** | .mp4, .mov, .avi, .mkv | mp4.svg | MP4, MOV, AVI, MKV |
| **矢量图** | .svg | svg.svg | SVG |
| **文本** | .txt, .md | txt.svg | TXT, MD |
| **压缩包** | .zip, .rar, .7z | 其他文件.svg | ZIP, RAR, 7Z |
| **其他** | - | 其他文件.svg | DOC |
### 3. 更新的页面
#### 资料列表页 (`src/pages/material-list/index.vue`)
**改动**:
- ✅ 导入 `getDocumentIcon``getDocumentLabel`
- ✅ 将 `IconFont` 图标改为 `image` 标签
- ✅ 动态显示文档图标(根据 `item.fileName`
- ✅ 动态显示文件类型标签(根据 `item.fileName`
**示例**:
```vue
<!-- 之前 -->
<IconFont :name="item.iconName || 'order'" size="32" :color="item.iconColor || '#2563EB'" />
<!-- 现在 -->
<image
:src="getDocumentIcon(item.fileName)"
class="w-[48rpx] h-[48rpx]"
mode="aspectFit"
/>
```
#### 产品详情页 (`src/pages/product-detail/index.vue`)
**改动**:
- ✅ 导入 `getDocumentIcon`
- ✅ 将 `IconFont` 图标改为 `image` 标签
- ✅ 动态显示文档图标(根据 `file.fileName`
**示例**:
```vue
<!-- 之前 -->
<IconFont :name="file.iconName" size="24" :color="file.iconColor" />
<!-- 现在 -->
<image
:src="getDocumentIcon(file.fileName)"
class="w-[48rpx] h-[48rpx]"
mode="aspectFit"
/>
```
### 4. 图标展示规范
#### 尺寸规范
- **列表图标**: `w-[48rpx] h-[48rpx]` (24px × 24px)
- **容器背景**: `w-[88rpx] h-[88rpx]`` (44px × 44px)
- **背景样式**: 渐变背景 `bg-gradient-to-br from-blue-50 to-blue-100`
#### 显示逻辑
```javascript
// 自动根据文件扩展名匹配图标
// 找不到扩展名 → 使用 "其他文件.svg"
// 特殊扩展名优先匹配(如 .docx → word.svg)
```
## 图标文件位置
```
src/assets/images/icon/doc/
├── doc.svg # PDF 通用文档
├── jpeg.svg # JPG/JPEG 图片
├── mp4.svg # MP4 视频
├── png.svg # PNG 图片
├── ppt.svg # PPT 演示文稿
├── svg.svg # SVG 矢量图
├── txt.svg # TXT 文本
├── word.svg # Word 文档
├── xls.svg # Excel 表格
└── 其他文件.svg # 其他文件
```
## 使用示例
### 在新页面中使用
```vue
<template>
<view v-for="file in fileList" :key="file.id">
<image :src="getDocumentIcon(file.fileName)" class="doc-icon" />
<text>{{ getDocumentLabel(file.fileName) }}</text>
</view>
</template>
<script setup>
import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'
</script>
```
### 扩展新的文件类型
如果需要支持新的文件类型,在 `src/utils/documentIcons.js` 中添加:
```javascript
const EXTENSION_ICON_MAP = {
'newtype': '/assets/images/icon/doc/newtype.svg',
// ...
};
```
## 优势
### 之前的问题
- ❌ 使用通用图标,无法区分文件类型
- ❌ 硬编码 `iconName` 和 `iconColor`,维护困难
- ❌ 所有文档显示相同图标
### 现在的优势
- ✅ 根据文件后缀自动匹配对应图标
- ✅ 直观展示文件类型
- ✅ 无需手动维护图标信息
- ✅ 统一的图标管理
- ✅ 易于扩展新文件类型
## 待优化项
### 可选优化(未实施)
- [ ] 收藏页面 (`src/pages/favorites/index.vue`) - 数据结构不包含 `fileName`,需要添加
- [ ] 首页资料展示 - 如有需要
- [ ] 图标懒加载优化
- [ ] 图标缓存机制
## 测试建议
### 测试文件类型
确保以下文件类型图标正确显示:
**文档类**:
- [ ] PDF 文件
- [ ] Word 文档 (.doc, .docx)
- [ ] Excel 表格 (.xls, .xlsx)
- [ ] PPT 演示文稿 (.ppt, .pptx)
**图片类**:
- [ ] JPG/JPEG
- [ ] PNG
- [ ] GIF
- [ ] WebP
**其他**:
- [ ] TXT 文本
- [ ] MP4 视频
- [ ] SVG 矢量图
- [ ] 压缩包
### 测试方法
1. 在资料列表页查看不同文件的图标
2. 在产品详情页查看附件的图标
3. 确认图标与文件类型匹配
4. 确认图标显示清晰、大小合适
## 相关文件
- [文档图标工具函数](../src/utils/documentIcons.js)
- [资料列表页](../src/pages/material-list/index.vue)
- [产品详情页](../src/pages/product-detail/index.vue)
- [图标目录](../src/assets/images/icon/doc/)
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769839712653" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1430" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M219.424 18.304h530.272l201.152 219.424v768H219.424V18.304z" fill="#FFFFFF" p-id="1431"></path><path d="M733.696 253.728V50.304H251.424v923.424h667.424v-720h-185.152z m217.152-16v768H219.424V18.304h530.272l201.152 219.424z m-58.08-16l-127.04-138.624v138.624h127.04z" fill="#465F78" p-id="1432"></path><path d="M73.152 288a32 32 0 0 1 32-32h448a32 32 0 0 1 32 32v448a32 32 0 0 1-32 32h-448a32 32 0 0 1-32-32V288zM640 416h224v37.344h-224V416zM640 509.344h224v37.312h-224v-37.312zM640 602.656h224V640h-224v-37.344z" fill="#056EC4" p-id="1433"></path><path d="M224 384h101.44c22.88 0 40.32 1.76 52.32 5.248 16.128 4.768 29.92 13.184 41.44 25.312 11.488 12.128 20.224 27.008 26.24 44.64 5.984 17.472 8.992 39.104 8.992 64.864 0 22.624-2.816 42.112-8.448 58.496-6.848 20-16.672 36.16-29.44 48.576-9.6 9.376-22.624 16.672-38.976 21.92-12.256 3.872-28.64 5.824-49.12 5.824H224V384z m55.488 46.496v182.08h41.44c15.52 0 26.688-0.896 33.6-2.624 8.96-2.24 16.416-6.08 22.272-11.456 6.016-5.376 10.88-14.176 14.624-26.432 3.776-12.384 5.632-29.184 5.632-50.432s-1.856-37.568-5.632-48.96c-3.744-11.36-8.96-20.224-15.744-26.624a52.8 52.8 0 0 0-25.696-12.928c-7.744-1.76-22.912-2.624-45.536-2.624h-24.96z" fill="#FFFFFF" p-id="1434"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769839839712" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1602" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M219.424 18.304h530.272l201.152 219.424v768H219.424V18.304z" fill="#FFFFFF" p-id="1603"></path><path d="M733.696 253.728V50.304H251.424v923.424h667.424v-720h-185.152z m217.152-16v768H219.424V18.304h530.272l201.152 219.424z m-58.08-16l-127.04-138.624v138.624h127.04z" fill="#465F78" p-id="1604"></path><path d="M73.152 288a32 32 0 0 1 32-32h448a32 32 0 0 1 32 32v448a32 32 0 0 1-32 32h-448a32 32 0 0 1-32-32V288zM640 416h224v37.344h-224V416zM640 509.344h224v37.312h-224v-37.312zM640 602.656h224V640h-224v-37.344z" fill="#26B99A" p-id="1605"></path><path d="M344.576 384h55.296v174.016c0 22.72-1.984 40.224-5.984 52.48-5.376 16-15.136 28.864-29.248 38.624-14.144 9.6-32.768 14.432-55.904 14.432-27.104 0-48-7.552-62.624-22.688-14.624-15.232-21.984-37.568-22.112-66.912l52.32-6.016c0.64 15.744 2.944 26.88 6.944 33.376 5.984 9.888 15.104 14.816 27.36 14.816 12.384 0 21.12-3.52 26.24-10.496 5.12-7.136 7.68-21.824 7.68-44.064V384z" fill="#FFFFFF" p-id="1606"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769839848193" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1776" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M219.424 18.304h530.272l201.152 219.424v768H219.424V18.304z" fill="#FFFFFF" p-id="1777"></path><path d="M733.696 253.728V50.304H251.424v923.424h667.424v-720h-185.152z m217.152-16v768H219.424V18.304h530.272l201.152 219.424z m-58.08-16l-127.04-138.624v138.624h127.04z" fill="#465F78" p-id="1778"></path><path d="M64 256m32 0l448 0q32 0 32 32l0 448q0 32-32 32l-448 0q-32 0-32-32l0-448q0-32 32-32Z" fill="#0092D3" p-id="1779"></path><path d="M640 416h224v37.344h-224V416zM640 509.344h224v37.312h-224v-37.312zM640 602.656h224V640h-224v-37.344z" fill="#0092D3" p-id="1780"></path><path d="M192 658.88V384h83.072l49.856 187.52L374.24 384h83.264v274.88h-51.584v-216.384L351.36 658.88H297.92l-54.368-216.384v216.384H192z" fill="#FFFFFF" p-id="1781"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769839855719" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1947" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M219.424 18.304h530.272l201.152 219.424v768H219.424V18.304z" fill="#FFFFFF" p-id="1948"></path><path d="M733.696 253.728V50.304H251.424v923.424h667.424v-720h-185.152z m217.152-16v768H219.424V18.304h530.272l201.152 219.424z m-58.08-16l-127.04-138.624v138.624h127.04z" fill="#465F78" p-id="1949"></path><path d="M64 288a32 32 0 0 1 32-32h448a32 32 0 0 1 32 32v448a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V288zM640 416h224v37.344h-224V416zM640 509.344h224v37.312h-224v-37.312zM640 602.656h224V640h-224v-37.344z" fill="#88C057" p-id="1950"></path><path d="M224 384v279.264h59.04v-90.56h49.632c21.44 0 39.744-3.936 54.816-11.84a82.752 82.752 0 0 0 34.784-33.152c8-14.176 12-30.528 12-49.088 0-18.56-3.968-34.88-11.84-49.088a82.176 82.176 0 0 0-34.112-33.408c-14.912-8.096-32.96-12.128-54.144-12.128H224z m99.136 141.408H283.04v-93.12h39.808c11.456 0 20.928 1.92 28.384 5.856 7.456 3.84 12.992 9.216 16.64 16.224 3.712 6.912 5.6 15.008 5.6 24.256 0 9.184-1.888 17.344-5.6 24.416-3.648 7.008-9.184 12.48-16.64 16.512-7.36 3.904-16.736 5.856-28.096 5.856z" fill="#FFFFFF" p-id="1951"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769839879259" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2637" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M219.424 18.304h530.272l201.152 219.424v768H219.424V18.304z" fill="#FFFFFF" p-id="2638"></path><path d="M733.696 253.728V50.304H251.424v923.424h667.424v-720h-185.152z m217.152-16v768H219.424V18.304h530.272l201.152 219.424z m-58.08-16l-127.04-138.624v138.624h127.04z" fill="#465F78" p-id="2639"></path><path d="M64 256m32 0l448 0q32 0 32 32l0 448q0 32-32 32l-448 0q-32 0-32-32l0-448q0-32 32-32Z" fill="#FF501A" p-id="2640"></path><path d="M640 416h224v37.344h-224V416zM640 509.344h224v37.312h-224v-37.312zM640 602.656h224V640h-224v-37.344z" fill="#FF501A" p-id="2641"></path><path d="M224 658.88V384h89.056c33.76 0 55.744 1.376 66.016 4.128 15.744 4.128 28.928 13.12 39.552 27.008 10.624 13.76 15.936 31.552 15.936 53.44 0 16.864-3.072 31.04-9.184 42.56-6.112 11.488-13.952 20.544-23.424 27.2a80.352 80.352 0 0 1-28.704 12.928c-13.248 2.624-32.448 3.936-57.568 3.936h-36.16v103.68H224z m55.488-228.384v78.016h30.4c21.856 0 36.48-1.44 43.84-4.32a36.576 36.576 0 0 0 23.616-34.88c0.032-10.016-2.88-18.24-8.768-24.736a38.4 38.4 0 0 0-22.336-12.192c-6.624-1.28-19.936-1.888-39.936-1.888h-26.816z" fill="#FFFFFF" p-id="2642"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769839895548" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3152" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M219.424 18.304h530.272l201.152 219.424v768H219.424V18.304z" fill="#FFFFFF" p-id="3153"></path><path d="M733.696 253.728V50.304H251.424v923.424h667.424v-720h-185.152z m217.152-16v768H219.424V18.304h530.272l201.152 219.424z m-58.08-16l-127.04-138.624v138.624h127.04z" fill="#465F78" p-id="3154"></path><path d="M73.152 288a32 32 0 0 1 32-32h448a32 32 0 0 1 32 32v448a32 32 0 0 1-32 32h-448a32 32 0 0 1-32-32V288zM640 416h224v37.344h-224V416zM640 509.344h224v37.312h-224v-37.312zM640 602.656h224V640h-224v-37.344z" fill="#FF527B" p-id="3155"></path><path d="M224 574.112l54.016-5.248c3.232 18.144 9.792 31.456 19.68 39.936 9.984 8.512 23.424 12.768 40.32 12.768 17.856 0 31.296-3.744 40.32-11.264 9.088-7.616 13.664-16.48 13.664-26.624a24.416 24.416 0 0 0-5.824-16.48c-3.744-4.64-10.368-8.64-19.84-12-6.528-2.24-21.344-6.272-44.48-12-29.76-7.36-50.592-16.448-62.592-27.2-16.896-15.136-25.312-33.568-25.312-55.328 0-13.984 3.936-27.04 11.808-39.168 8-12.256 19.424-21.568 34.304-27.936 15.008-6.4 33.056-9.568 54.176-9.568 34.496 0 60.448 7.552 77.824 22.688 17.504 15.136 26.688 35.328 27.552 60.576l-55.488 2.432c-2.368-14.144-7.488-24.256-15.36-30.4-7.776-6.24-19.456-9.376-35.072-9.376-16.128 0-28.768 3.328-37.888 9.952a20.032 20.032 0 0 0-8.8 17.056 21.12 21.12 0 0 0 8.256 16.704c6.976 5.856 24 12 50.976 18.368 27.008 6.4 46.944 12.992 59.84 19.872 12.992 6.752 23.104 16.064 30.368 27.936 7.36 11.744 11.04 26.304 11.04 43.68 0 15.776-4.352 30.496-13.12 44.256-8.736 13.76-21.12 24-37.12 30.752-16 6.624-35.936 9.92-59.84 9.92-34.72 0-61.408-8-80.032-24-18.624-16.096-29.76-39.552-33.376-70.304z" fill="#FFFFFF" p-id="3156"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769839860746" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2119" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M219.424 18.304h530.272l201.152 219.424v768H219.424V18.304z" fill="#FFFFFF" p-id="2120"></path><path d="M733.696 253.728V50.304H251.424v923.424h667.424v-720h-185.152z m217.152-16v768H219.424V18.304h530.272l201.152 219.424z m-58.08-16l-127.04-138.624v138.624h127.04z" fill="#465F78" p-id="2121"></path><path d="M64 256m32 0l448 0q32 0 32 32l0 448q0 32-32 32l-448 0q-32 0-32-32l0-448q0-32 32-32Z" fill="#A8A1A0" p-id="2122"></path><path d="M640 416h224v37.344h-224V416zM640 509.344h224v37.312h-224v-37.312zM640 602.656h224V640h-224v-37.344z" fill="#A8A1A0" p-id="2123"></path><path d="M291.648 672v-239.264H208V384h224v48.736h-83.456V672H291.648z" fill="#FFFFFF" p-id="2124"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769839883657" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2810" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M219.424 18.304h530.272l201.152 219.424v768H219.424V18.304z" fill="#FFFFFF" p-id="2811"></path><path d="M733.696 253.728V50.304H251.424v923.424h667.424v-720h-185.152z m217.152-16v768H219.424V18.304h530.272l201.152 219.424z m-58.08-16l-127.04-138.624v138.624h127.04z" fill="#465F78" p-id="2812"></path><path d="M73.152 288a32 32 0 0 1 32-32h448a32 32 0 0 1 32 32v448a32 32 0 0 1-32 32h-448a32 32 0 0 1-32-32V288zM640 416h224v37.344h-224V416zM640 509.344h224v37.312h-224v-37.312zM640 602.656h224V640h-224v-37.344z" fill="#317BFF" p-id="2813"></path><path d="M221.952 640l-56.256-235.616H214.4l35.52 161.856 43.072-161.856h56.576l41.28 164.576 36.16-164.576h47.904L417.696 640h-50.464l-46.912-176.16L273.536 640H221.952z" fill="#FFFFFF" p-id="2814"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769839874756" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2465" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M219.424 18.304h530.272l201.152 219.424v768H219.424V18.304z" fill="#FFFFFF" p-id="2466"></path><path d="M733.696 253.728V50.304H251.424v923.424h667.424v-720h-185.152z m217.152-16v768H219.424V18.304h530.272l201.152 219.424z m-58.08-16l-127.04-138.624v138.624h127.04z" fill="#465F78" p-id="2467"></path><path d="M64 288a32 32 0 0 1 32-32h448a32 32 0 0 1 32 32v448a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V288zM640 416h224v37.344h-224V416zM640 509.344h224v37.312h-224v-37.312zM640 602.656h224V640h-224v-37.344z" fill="#47B347" p-id="2468"></path><path d="M192 658.88l93.952-143.456L200.832 384h64.864l55.104 88.32L374.816 384h64.32l-85.504 133.504 93.92 141.376h-66.912L319.68 563.84l-61.12 95.04H192z" fill="#FFFFFF" p-id="2469"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1769839889608" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2980" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M219.424 18.304h530.272l201.152 219.424v768H219.424V18.304z" fill="#FFFFFF" p-id="2981"></path><path d="M733.696 253.728V50.304H251.424v923.424h667.424v-720h-185.152z m217.152-16v768H219.424V18.304h530.272l201.152 219.424z m-58.08-16l-127.04-138.624v138.624h127.04z" fill="#465F78" p-id="2982"></path><path d="M64 288a32 32 0 0 1 32-32h448a32 32 0 0 1 32 32v448a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V288zM640 416h224v37.344h-224V416zM640 509.344h224v37.312h-224v-37.312zM640 602.656h224V640h-224v-37.344z" fill="#B2B2B2" p-id="2983"></path><path d="M291.968 545.152c-8.064-60.288 73.728-73.728 73.728-110.976 0-23.424-18.432-36.48-48.768-36.48-23.424 0-42.624 10.752-61.44 30.336l-31.488-28.8C248.576 370.816 282.368 352 323.456 352c55.68 0 96.384 26.112 96.384 77.184 0 56.064-84.096 62.976-77.952 115.968h-49.92z m25.344 102.528c-19.968 0-34.176-14.208-34.176-34.176 0-19.968 14.592-33.792 34.176-33.792 19.2 0 33.792 13.824 33.792 33.792 0 19.968-14.592 34.176-33.792 34.176z" fill="#FFFFFF" p-id="2984"></path></svg>
\ No newline at end of file
......@@ -130,10 +130,15 @@
<!-- Item 1 -->
<view class="flex gap-[24rpx]" @tap="handleMaterialClick(hotMaterials[0])">
<view class="w-[80rpx] h-[88rpx] flex-shrink-0 flex items-center justify-center bg-blue-50 rounded-[12rpx]">
<IconFont name="order" size="32" color="#EF4444" />
<image :src="getDocumentIcon(hotMaterials[0].fileName)" class="w-[48rpx] h-[48rpx]" mode="aspectFit" />
</view>
<view class="flex-1 flex flex-col justify-between py-[4rpx]">
<text class="text-gray-800 text-[28rpx] leading-[40rpx] line-clamp-2">{{ hotMaterials[0].title }}</text>
<text class="text-gray-800 text-[28rpx] leading-[40rpx] line-clamp-2 mb-1">{{ hotMaterials[0].title }}</text>
<view class="flex items-center gap-2 mb-1">
<view class="bg-blue-50 rounded px-2 py-0.5">
<text class="text-blue-600 text-[22rpx]">{{ getDocumentLabel(hotMaterials[0].fileName) }}</text>
</view>
</view>
<view class="flex justify-between items-end">
<text class="text-gray-400 text-[24rpx]">{{ hotMaterials[0].learners }}</text>
<text class="text-blue-600 text-[26rpx]">{{ hotMaterials[0].progress }}</text>
......@@ -144,10 +149,15 @@
<!-- Item 2 -->
<view class="flex gap-[24rpx]" @tap="handleMaterialClick(hotMaterials[1])">
<view class="w-[80rpx] h-[88rpx] flex-shrink-0 flex items-center justify-center bg-blue-50 rounded-[12rpx]">
<IconFont name="order" size="32" color="#3B82F6" />
<image :src="getDocumentIcon(hotMaterials[1].fileName)" class="w-[48rpx] h-[48rpx]" mode="aspectFit" />
</view>
<view class="flex-1 flex flex-col justify-between py-[4rpx]">
<text class="text-gray-800 text-[28rpx] leading-[40rpx] line-clamp-2">{{ hotMaterials[1].title }}</text>
<text class="text-gray-800 text-[28rpx] leading-[40rpx] line-clamp-2 mb-1">{{ hotMaterials[1].title }}</text>
<view class="flex items-center gap-2 mb-1">
<view class="bg-blue-50 rounded px-2 py-0.5">
<text class="text-blue-600 text-[22rpx]">{{ getDocumentLabel(hotMaterials[1].fileName) }}</text>
</view>
</view>
<view class="flex justify-between items-end">
<text class="text-gray-400 text-[24rpx]">{{ hotMaterials[1].learners }}</text>
<text class="text-blue-600 text-[26rpx]">{{ hotMaterials[1].progress }}</text>
......@@ -158,10 +168,15 @@
<!-- Item 3 -->
<view class="flex gap-[24rpx]" @tap="handleMaterialClick(hotMaterials[2])">
<view class="w-[80rpx] h-[88rpx] flex-shrink-0 flex items-center justify-center bg-blue-50 rounded-[12rpx]">
<IconFont name="order" size="32" color="#10B981" />
<image :src="getDocumentIcon(hotMaterials[2].fileName)" class="w-[48rpx] h-[48rpx]" mode="aspectFit" />
</view>
<view class="flex-1 flex flex-col justify-between py-[4rpx]">
<text class="text-gray-800 text-[28rpx] leading-[40rpx] line-clamp-2">{{ hotMaterials[2].title }}</text>
<text class="text-gray-800 text-[28rpx] leading-[40rpx] line-clamp-2 mb-1">{{ hotMaterials[2].title }}</text>
<view class="flex items-center gap-2 mb-1">
<view class="bg-blue-50 rounded px-2 py-0.5">
<text class="text-blue-600 text-[22rpx]">{{ getDocumentLabel(hotMaterials[2].fileName) }}</text>
</view>
</view>
<view class="flex justify-between items-end">
<text class="text-gray-400 text-[24rpx]">{{ hotMaterials[2].learners }}</text>
<text class="text-blue-600 text-[26rpx]">{{ hotMaterials[2].progress }}</text>
......@@ -196,6 +211,7 @@ import { ref, shallowRef } from 'vue';
import Taro, { useShareAppMessage } from '@tarojs/taro';
import { useGo } from '@/hooks/useGo';
import { useListItemClick, ListType } from '@/composables/useListItemClick';
import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons';
import TabBar from '@/components/TabBar.vue';
import IconFont from '@/components/IconFont.vue';
import PlanPopup from '@/components/PlanPopup/index.vue';
......@@ -233,29 +249,31 @@ const loopData0 = shallowRef([
/**
* 热门资料数据
*
* @description 本周热门资料列表数据,包含文件信息和下载地址
* @description 本周热门资料列表数据,包含不同类型的文件
*/
const hotMaterials = ref([
{
title: '2024年保险市场趋势分析报告',
learners: '256人学习',
progress: '78%',
// 文件信息(用于点击打开预览)
// PDF 文件
fileName: '2024年保险市场趋势分析报告.pdf',
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'
},
{
title: '高净值客户需求分析与产品匹配',
title: '高净值客户产品配置方案模板',
learners: '189人学习',
progress: '65%',
fileName: '高净值客户需求分析与产品匹配.pdf',
// Word 文件
fileName: '高净值客户产品配置方案模板.docx',
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'
},
{
title: '保险合同条款解读与风险提示',
title: '产品收益率测算表(2024版)',
learners: '142人学习',
progress: '52%',
fileName: '保险合同条款解读与风险提示.pdf',
// Excel 文件
fileName: '产品收益率测算表.xlsx',
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'
}
]);
......
......@@ -27,7 +27,11 @@
<!-- 左侧图标 -->
<div
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">
<IconFont :name="item.iconName || 'order'" size="32" :color="item.iconColor || '#2563EB'" />
<image
:src="getDocumentIcon(item.fileName)"
class="w-[48rpx] h-[48rpx]"
mode="aspectFit"
/>
</div>
<!-- 内容区域 -->
......@@ -58,7 +62,7 @@
<div class="flex items-center gap-[12rpx]">
<span
class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-gray-100 text-gray-500 text-[20rpx] font-medium rounded-[8rpx]">
PDF
{{ getDocumentLabel(item.fileName) }}
</span>
<span class="text-[#9CA3AF] text-[22rpx]">
{{ item.size }}
......@@ -70,7 +74,7 @@
class="flex items-center justify-center px-[20rpx] py-[10rpx] bg-blue-50 rounded-full active:bg-blue-100 transition-colors"
@tap.stop="onView(item)">
<IconFont name="eye" size="14" color="#2563EB" class="mr-[6rpx]" />
<text class="text-[24rpx] text-blue-600 font-medium leading-none">查看</text>
<text class="text-[24rpx] text-blue-600 font-medium leading-none ml-[8rpx]">查看</text>
</view>
</div>
</div>
......@@ -89,6 +93,7 @@ import NavHeader from '@/components/NavHeader.vue'
import TabBar from '@/components/TabBar.vue'
import IconFont from '@/components/IconFont.vue'
import { useListItemClick, ListType } from '@/composables/useListItemClick'
import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'
const searchValue = ref('')
......
......@@ -95,7 +95,11 @@
>
<div class="flex items-center justify-between mb-[8rpx]">
<div class="flex items-center flex-1 mr-[24rpx]">
<IconFont :name="file.iconName" size="24" :color="file.iconColor" class="mr-[24rpx]" />
<image
:src="getDocumentIcon(file.fileName)"
class="w-[48rpx] h-[48rpx] mr-[24rpx]"
mode="aspectFit"
/>
<div class="flex flex-col">
<span class="text-[#1F2937] text-[28rpx] font-medium mb-[4rpx] line-clamp-1">{{ file.name }}</span>
<span class="text-[#9CA3AF] text-[24rpx]">{{ file.size }}</span>
......@@ -120,6 +124,7 @@ import TabBar from '@/components/TabBar.vue'
import IconFont from '@/components/IconFont.vue'
import { useFileOperation } from '@/composables/useFileOperation'
import Taro, { useLoad } from '@tarojs/taro'
import { getDocumentIcon } from '@/utils/documentIcons'
const { viewFile } = useFileOperation()
......
/**
* @description 文档图标工具函数
* @module utils/documentIcons
* @author Claude Code
* @created 2026-01-31
*/
// 导入所有 SVG 图标资源
import docIcon from '@/assets/images/icon/doc/doc.svg'
import wordIcon from '@/assets/images/icon/doc/word.svg'
import xlsIcon from '@/assets/images/icon/doc/xls.svg'
import pptIcon from '@/assets/images/icon/doc/ppt.svg'
import jpegIcon from '@/assets/images/icon/doc/jpeg.svg'
import pngIcon from '@/assets/images/icon/doc/png.svg'
import mp4Icon from '@/assets/images/icon/doc/mp4.svg'
import svgIcon from '@/assets/images/icon/doc/svg.svg'
import txtIcon from '@/assets/images/icon/doc/txt.svg'
import defaultIcon from '@/assets/images/icon/doc/其他文件.svg'
/**
* 文件扩展名到图标的映射
* @type {Object.<string, string>}
*/
const EXTENSION_ICON_MAP = {
// 文档类
'pdf': docIcon,
'doc': wordIcon,
'docx': wordIcon,
// 表格类
'xls': xlsIcon,
'xlsx': xlsIcon,
// 演示文稿类
'ppt': pptIcon,
'pptx': pptIcon,
// 图片类
'jpg': jpegIcon,
'jpeg': jpegIcon,
'png': pngIcon,
'gif': pngIcon,
'webp': pngIcon,
// 视频类
'mp4': mp4Icon,
'mov': mp4Icon,
'avi': mp4Icon,
'mkv': mp4Icon,
// 矢量图类
'svg': svgIcon,
// 文本类
'txt': txtIcon,
'md': txtIcon,
// 其他
'zip': defaultIcon,
'rar': defaultIcon,
'7z': defaultIcon,
};
/**
* 默认图标(未知文件类型)
* @type {string}
*/
const DEFAULT_ICON = defaultIcon;
/**
* 文件扩展名到显示标签的映射
* @type {Object.<string, string>}
*/
const EXTENSION_LABEL_MAP = {
'pdf': 'PDF',
'doc': 'Word',
'docx': 'Word',
'xls': 'Excel',
'xlsx': 'Excel',
'ppt': 'PPT',
'pptx': 'PPT',
'jpg': 'JPG',
'jpeg': 'JPEG',
'png': 'PNG',
'gif': 'GIF',
'webp': 'WebP',
'mp4': 'MP4',
'mov': 'MOV',
'avi': 'AVI',
'mkv': 'MKV',
'svg': 'SVG',
'txt': 'TXT',
'md': 'MD',
'zip': 'ZIP',
'rar': 'RAR',
'7z': '7Z',
};
/**
* 默认文件类型标签
* @type {string}
*/
const DEFAULT_LABEL = 'DOC';
/**
* 根据文件名获取文档图标路径
*
* @description 从文件名中提取扩展名,返回对应的图标路径
* @param {string} fileName - 文件名(如:document.pdf)
* @returns {string} 图标路径
*
* @example
* getDocumentIcon('报告.pdf') // 返回 PDF 图标
* getDocumentIcon('数据.xlsx') // 返回 Excel 图标
* getDocumentIcon('图片.png') // 返回 PNG 图标
*/
export function getDocumentIcon(fileName) {
if (!fileName || typeof fileName !== 'string') {
return DEFAULT_ICON;
}
// 提取文件扩展名
const lastDotIndex = fileName.lastIndexOf('.');
// 没有扩展名或以点结尾(如 "file.")
if (lastDotIndex === -1 || lastDotIndex === fileName.length - 1) {
return DEFAULT_ICON;
}
const extension = fileName.slice(lastDotIndex + 1).toLowerCase();
// 返回对应图标,找不到则返回默认图标
return EXTENSION_ICON_MAP[extension] || DEFAULT_ICON;
}
/**
* 根据文件名获取文件类型标签
*
* @description 从文件名中提取扩展名,返回对应的显示标签
* @param {string} fileName - 文件名(如:document.pdf)
* @returns {string} 文件类型标签(如:PDF、Word、Excel)
*
* @example
* getDocumentLabel('报告.pdf') // 'PDF'
* getDocumentLabel('数据.xlsx') // 'Excel'
* getDocumentLabel('图片.png') // 'PNG'
*/
export function getDocumentLabel(fileName) {
if (!fileName || typeof fileName !== 'string') {
return DEFAULT_LABEL;
}
// 提取文件扩展名
const lastDotIndex = fileName.lastIndexOf('.');
// 没有扩展名或以点结尾(如 "file.")
if (lastDotIndex === -1 || lastDotIndex === fileName.length - 1) {
return DEFAULT_LABEL;
}
const extension = fileName.slice(lastDotIndex + 1).toLowerCase();
// 返回对应标签,找不到则返回默认标签
return EXTENSION_LABEL_MAP[extension] || DEFAULT_LABEL;
}
/**
* 根据文件名判断是否为 PDF 文件
*
* @param {string} fileName - 文件名
* @returns {boolean} 是否为 PDF 文件
*
* @example
* isPDF('document.pdf') // true
* isPDF('document.docx') // false
*/
export function isPDF(fileName) {
const extension = fileName?.split('.').pop()?.toLowerCase();
return extension === 'pdf';
}
/**
* 根据文件名判断是否为图片文件
*
* @param {string} fileName - 文件名
* @returns {boolean} 是否为图片文件
*
* @example
* isImage('photo.jpg') // true
* isImage('document.pdf') // false
*/
export function isImage(fileName) {
const extension = fileName?.split('.').pop()?.toLowerCase();
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
return imageExtensions.includes(extension);
}
/**
* 根据文件名判断是否为视频文件
*
* @param {string} fileName - 文件名
* @returns {boolean} 是否为视频文件
*
* @example
* isVideo('movie.mp4') // true
* isVideo('document.pdf') // false
*/
export function isVideo(fileName) {
const extension = fileName?.split('.').pop()?.toLowerCase();
const videoExtensions = ['mp4', 'mov', 'avi', 'mkv'];
return videoExtensions.includes(extension);
}