hookehuyr

feat: 新增文档预览组件及相关页面

- 新增 DocumentPreview 统一文档预览组件,支持 PDF/Word/Excel/PPT 格式
- 新增文档预览页面和示例页面,支持大文件在线预览
- 新增 OfficeViewer 和 PdfPreview 组件用于 H5 环境
- 新增文档预览工具函数,支持文件类型检测和大小获取
- 配置 ESLint 以支持 Vue 项目,修复相关依赖问题
- 更新 IconFont 组件,添加文档预览所需图标
- 在应用配置中注册新增页面路由
...@@ -50,7 +50,11 @@ ...@@ -50,7 +50,11 @@
50 "Bash(git checkout:*)", 50 "Bash(git checkout:*)",
51 "Bash(python3:*)", 51 "Bash(python3:*)",
52 "Bash(./test-mcp-connection.sh:*)", 52 "Bash(./test-mcp-connection.sh:*)",
53 - "Bash(timeout:*)" 53 + "Bash(timeout:*)",
54 + "Bash(pnpm lint:*)",
55 + "Bash(pnpm dev:weapp:*)",
56 + "Bash(tee:*)",
57 + "Bash(node:*)"
54 ] 58 ]
55 } 59 }
56 } 60 }
......
1 +module.exports = {
2 + root: true,
3 + env: {
4 + node: true,
5 + es2021: true
6 + },
7 + globals: {
8 + definePageConfig: 'readonly',
9 + getCurrentPages: 'readonly',
10 + ENABLE_AUTH_MODE: 'readonly',
11 + wx: 'readonly'
12 + },
13 + extends: ['taro'],
14 + rules: {
15 + 'react-hooks/rules-of-hooks': 'off',
16 + 'react-hooks/exhaustive-deps': 'off',
17 + 'vue/multi-word-component-names': 'off',
18 + 'import/first': 'off',
19 + 'import/newline-after-import': 'off',
20 + 'import/no-duplicates': 'off',
21 + 'import/no-mutable-exports': 'off',
22 + 'no-unused-vars': 'warn'
23 + },
24 + overrides: [
25 + {
26 + files: ['**/*.vue'],
27 + extends: ['taro/vue3'],
28 + parser: 'vue-eslint-parser',
29 + parserOptions: {
30 + parser: '@babel/eslint-parser',
31 + requireConfigFile: false,
32 + ecmaVersion: 2021,
33 + sourceType: 'module'
34 + },
35 + rules: {
36 + 'vue/multi-word-component-names': 'off'
37 + }
38 + },
39 + {
40 + files: ['**/*.js'],
41 + parser: '@babel/eslint-parser',
42 + parserOptions: {
43 + requireConfigFile: false,
44 + ecmaVersion: 2021,
45 + sourceType: 'module'
46 + }
47 + }
48 + ]
49 +}
...@@ -7,14 +7,19 @@ export {} ...@@ -7,14 +7,19 @@ export {}
7 7
8 declare module 'vue' { 8 declare module 'vue' {
9 export interface GlobalComponents { 9 export interface GlobalComponents {
10 + DocumentPreview: typeof import('./src/components/DocumentPreview/index.vue')['default']
10 IconFont: typeof import('./src/components/IconFont.vue')['default'] 11 IconFont: typeof import('./src/components/IconFont.vue')['default']
11 IndexNav: typeof import('./src/components/indexNav.vue')['default'] 12 IndexNav: typeof import('./src/components/indexNav.vue')['default']
12 NavHeader: typeof import('./src/components/NavHeader.vue')['default'] 13 NavHeader: typeof import('./src/components/NavHeader.vue')['default']
13 NutAvatar: typeof import('@nutui/nutui-taro')['Avatar'] 14 NutAvatar: typeof import('@nutui/nutui-taro')['Avatar']
14 NutButton: typeof import('@nutui/nutui-taro')['Button'] 15 NutButton: typeof import('@nutui/nutui-taro')['Button']
15 NutSearchbar: typeof import('@nutui/nutui-taro')['Searchbar'] 16 NutSearchbar: typeof import('@nutui/nutui-taro')['Searchbar']
17 + NutTabPane: typeof import('@nutui/nutui-taro')['TabPane']
18 + NutTabs: typeof import('@nutui/nutui-taro')['Tabs']
16 NutTextarea: typeof import('@nutui/nutui-taro')['Textarea'] 19 NutTextarea: typeof import('@nutui/nutui-taro')['Textarea']
17 NutUploader: typeof import('@nutui/nutui-taro')['Uploader'] 20 NutUploader: typeof import('@nutui/nutui-taro')['Uploader']
21 + OfficeViewer: typeof import('./src/components/OfficeViewer.vue')['default']
22 + PdfPreview: typeof import('./src/components/PdfPreview.vue')['default']
18 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] 23 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
19 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] 24 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
20 QrCode: typeof import('./src/components/qrCode.vue')['default'] 25 QrCode: typeof import('./src/components/qrCode.vue')['default']
......
...@@ -115,3 +115,14 @@ All notable changes to this project will be documented in this file. ...@@ -115,3 +115,14 @@ All notable changes to this project will be documented in this file.
115 - 移除 `src/api/index.js` 中的离线专用接口定义 115 - 移除 `src/api/index.js` 中的离线专用接口定义
116 - 更新配置文件,移除 `ENABLE_OFFLINE_MODE` 开关 116 - 更新配置文件,移除 `ENABLE_OFFLINE_MODE` 开关
117 - 修复构建告警:移除首页残留的 `ENABLE_OFFLINE_MODE``@/utils/uiText` 引用 117 - 修复构建告警:移除首页残留的 `ENABLE_OFFLINE_MODE``@/utils/uiText` 引用
118 +
119 +### Fixed
120 +- 修复 ESLint 无法解析 Vue SFC 导致 lint 全量报错:补齐 ESLint 配置与 Vue 解析依赖
121 +- 修复 eslint-config-taro 在 Vue 项目中触发 React Hooks 规则导致误报的问题
122 +
123 +### Changed
124 +- 优化 DocumentPreview 小程序端预览策略:无法获取文件大小时默认走在线预览
125 +- 将 DocumentPreview 小程序端样式单位统一为 rpx
126 +
127 +### Added
128 +- 补全文档预览示例页的 Excel / PPT 在线示例链接
......
...@@ -41,6 +41,7 @@ ...@@ -41,6 +41,7 @@
41 "license": "MIT", 41 "license": "MIT",
42 "dependencies": { 42 "dependencies": {
43 "@babel/runtime": "^7.7.7", 43 "@babel/runtime": "^7.7.7",
44 + "@nutui/icons-vue": "^0.1.1",
44 "@nutui/icons-vue-taro": "^0.0.9", 45 "@nutui/icons-vue-taro": "^0.0.9",
45 "@nutui/nutui-taro": "^4.3.13", 46 "@nutui/nutui-taro": "^4.3.13",
46 "@tarojs/components": "4.1.9", 47 "@tarojs/components": "4.1.9",
...@@ -80,6 +81,9 @@ ...@@ -80,6 +81,9 @@
80 "css-loader": "3.4.2", 81 "css-loader": "3.4.2",
81 "eslint": "^8.12.0", 82 "eslint": "^8.12.0",
82 "eslint-config-taro": "4.1.9", 83 "eslint-config-taro": "4.1.9",
84 + "eslint-plugin-react": "^7.33.2",
85 + "eslint-plugin-react-hooks": "^4.4.0",
86 + "eslint-plugin-vue": "^8.0.0",
83 "js-yaml": "^4.1.1", 87 "js-yaml": "^4.1.1",
84 "less": "^4.2.0", 88 "less": "^4.2.0",
85 "postcss": "^8.5.6", 89 "postcss": "^8.5.6",
...@@ -88,6 +92,7 @@ ...@@ -88,6 +92,7 @@
88 "tailwindcss": "^3.4.0", 92 "tailwindcss": "^3.4.0",
89 "unplugin-vue-components": "^0.26.0", 93 "unplugin-vue-components": "^0.26.0",
90 "vue-loader": "^17.0.0", 94 "vue-loader": "^17.0.0",
95 + "vue-eslint-parser": "^9.0.0",
91 "weapp-tailwindcss": "^4.1.10", 96 "weapp-tailwindcss": "^4.1.10",
92 "webpack": "5.91.0" 97 "webpack": "5.91.0"
93 }, 98 },
......
This diff is collapsed. Click to expand it.
1 /* 1 /*
2 * @Date: 2025-06-28 10:33:00 2 * @Date: 2025-06-28 10:33:00
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2026-01-29 22:47:02 4 + * @LastEditTime: 2026-01-30 10:07:28
5 * @FilePath: /manulife-weapp/src/app.config.js 5 * @FilePath: /manulife-weapp/src/app.config.js
6 * @Description: 小程序配置文件 6 * @Description: 小程序配置文件
7 */ 7 */
...@@ -9,6 +9,8 @@ const pages = [ ...@@ -9,6 +9,8 @@ const pages = [
9 'pages/index/index', 9 'pages/index/index',
10 'pages/search/index', 10 'pages/search/index',
11 'pages/webview/index', 11 'pages/webview/index',
12 + 'pages/document-preview/index',
13 + 'pages/document-demo/index',
12 'pages/auth/index', 14 'pages/auth/index',
13 'pages/onboarding/index', 15 'pages/onboarding/index',
14 'pages/family-office/index', 16 'pages/family-office/index',
......
1 +# DocumentPreview 组件使用文档
2 +
3 +## 📖 概述
4 +
5 +`DocumentPreview` 是一个统一的文档预览组件,支持 **PDF、Word、Excel、PPT** 等多种格式。
6 +
7 +### 核心特性
8 +
9 +**多环境支持**:H5 + 微信小程序
10 +**智能预览**:根据文件大小自动选择最佳预览方式
11 +**全格式支持**:PDF、Word、Excel、PPT
12 +**优雅降级**:大文件自动使用在线预览
13 +
14 +---
15 +
16 +## 🚀 快速开始
17 +
18 +### 1. 基本使用
19 +
20 +```vue
21 +<template>
22 + <view class="page">
23 + <DocumentPreview
24 + :src="documentUrl"
25 + :fileType="fileType"
26 + :fileName="fileName"
27 + @rendered="handleRendered"
28 + @error="handleError"
29 + />
30 + </view>
31 +</template>
32 +
33 +<script setup>
34 +import { ref } from 'vue'
35 +import DocumentPreview from '@/components/DocumentPreview/index.vue'
36 +
37 +const documentUrl = ref('https://example.com/document.pdf')
38 +const fileType = ref('pdf') // pdf, doc, docx, xls, xlsx, ppt, pptx
39 +const fileName = ref('重要文档.pdf')
40 +
41 +const handleRendered = () => {
42 + console.log('文档渲染完成')
43 +}
44 +
45 +const handleError = (err) => {
46 + console.error('文档渲染失败:', err)
47 +}
48 +</script>
49 +```
50 +
51 +---
52 +
53 +## 📋 Props
54 +
55 +| 属性 | 类型 | 默认值 | 必填 | 说明 |
56 +|------|------|--------|------|------|
57 +| `src` | `String` | - | ✅ | 文档 URL(必须 HTTPS) |
58 +| `fileType` | `String` | `''` | ❌ | 文档类型(不填则自动检测) |
59 +| `fileName` | `String` | `''` | ❌ | 文件名(用于显示) |
60 +| `show` | `Boolean` | `false` | ❌ | 是否显示(H5 PDF 预览用) |
61 +
62 +### fileType 支持的值
63 +
64 +- `'pdf'` - PDF 文档
65 +- `'doc'` / `'docx'` - Word 文档
66 +- `'xls'` / `'xlsx'` - Excel 表格
67 +- `'ppt'` / `'pptx'` - PowerPoint 演示文稿
68 +
69 +---
70 +
71 +## 🎯 Events
72 +
73 +| 事件名 | 参数 | 说明 |
74 +|--------|------|------|
75 +| `rendered` | - | 文档渲染完成(H5) |
76 +| `error` | `Error` | 文档渲染失败 |
77 +| `update:show` | `Boolean` | 更新显示状态(H5 PDF) |
78 +
79 +---
80 +
81 +## 🔍 预览策略
82 +
83 +### 小程序环境
84 +
85 +| 文件大小 | 预览方式 | 体验 |
86 +|---------|---------|------|
87 +| **< 10MB** | 微信原生 API (`wx.openDocument`) | ⭐⭐⭐ 跳转新页面 |
88 +| **≥ 10MB** | web-view + 腾讯文档预览 | ⭐⭐⭐⭐⭐ 在线预览 |
89 +
90 +### H5 环境
91 +
92 +- **PDF**:使用 `PdfPreview` 组件(内嵌预览)
93 +- **Office 文档**:使用 `OfficeViewer` 组件(内嵌预览)
94 +
95 +---
96 +
97 +## 💡 使用场景
98 +
99 +### 场景 1:预览小文件 PDF
100 +
101 +```vue
102 +<DocumentPreview
103 + src="https://example.com/small.pdf"
104 + fileType="pdf"
105 + fileName="产品手册.pdf"
106 +/>
107 +```
108 +
109 +**小程序**:使用微信原生 API(快速)✨
110 +**H5**:内嵌预览(优雅)✨
111 +
112 +---
113 +
114 +### 场景 2:预览大文件 Word
115 +
116 +```vue
117 +<DocumentPreview
118 + src="https://example.com/large-document.docx"
119 + fileType="docx"
120 + fileName="大型技术文档.docx"
121 +/>
122 +```
123 +
124 +**小程序**:自动跳转到腾讯文档在线预览(支持大文件)🚀
125 +**H5**:内嵌预览
126 +
127 +---
128 +
129 +### 场景 3:自动检测文件类型
130 +
131 +```vue
132 +<DocumentPreview
133 + src="https://example.com/document.xlsx"
134 + fileName="销售数据表"
135 +/>
136 +```
137 +
138 +不指定 `fileType`,组件会自动从 URL 中提取文件类型。
139 +
140 +---
141 +
142 +## ⚙️ 高级配置
143 +
144 +### 域名白名单配置
145 +
146 +**小程序环境必须配置业务域名白名单**
147 +
148 +1. 登录[微信公众平台](https://mp.weixin.qq.com/)
149 +2. 进入「开发」→「开发管理」→「开发设置」
150 +3. 找到「业务域名」
151 +4. 添加以下域名:
152 + - `view.officeapps.live.com`(腾讯文档预览)
153 + - 您自己的文档服务器域名(如 `cdn.example.com`
154 +
155 +### HTTPS 要求
156 +
157 +**文档 URL 必须使用 HTTPS 协议**
158 +
159 +```javascript
160 +// ✅ 正确
161 +const url = 'https://cdn.example.com/document.pdf'
162 +
163 +// ❌ 错误(HTTP 不支持)
164 +const url = 'http://cdn.example.com/document.pdf'
165 +```
166 +
167 +---
168 +
169 +## 🐛 故障排查
170 +
171 +### 问题 1:小程序提示"无法打开文档"
172 +
173 +**原因**:域名未配置白名单
174 +
175 +**解决**
176 +1. 在微信公众平台配置业务域名
177 +2. 确保文档 URL 使用 HTTPS
178 +3. 清除小程序缓存重新打开
179 +
180 +---
181 +
182 +### 问题 2:大文件预览失败
183 +
184 +**原因**:文件超过 10MB,但未正确跳转到 web-view
185 +
186 +**解决**
187 +1. 检查 `pages/document-preview/index` 是否在 `app.config.js` 中注册
188 +2. 检查 web-view 域名是否在白名单中
189 +3. 查看控制台错误日志
190 +
191 +---
192 +
193 +### 问题 3:文件类型检测失败
194 +
195 +**原因**:URL 中没有文件扩展名
196 +
197 +**解决**:手动指定 `fileType` 属性
198 +
199 +```vue
200 +<DocumentPreview
201 + src="https://api.example.com/download?id=123"
202 + fileType="pdf"
203 +/>
204 +```
205 +
206 +---
207 +
208 +## 📦 依赖
209 +
210 +### 小程序环境
211 +
212 +- Taro 4.x(内置)
213 +- web-view 组件(内置)
214 +- 无需额外依赖
215 +
216 +### H5 环境
217 +
218 +- 使用 iframe 预览(PDF 直开、Office 走腾讯文档预览)
219 +- 无需额外依赖
220 +
221 +---
222 +
223 +## 🎨 样式自定义
224 +
225 +组件使用了 Scoped 样式,如需自定义,可以使用深度选择器:
226 +
227 +```vue
228 +<style lang="less" scoped>
229 +// 自定义加载容器
230 +:deep(.loading-container) {
231 + background: #f0f0f0;
232 +}
233 +
234 +// 自定义错误提示
235 +:deep(.error-container) {
236 + background: #fff0f0;
237 +}
238 +</style>
239 +```
240 +
241 +---
242 +
243 +## 📝 更新日志
244 +
245 +### v1.0.0 (2025-01-30)
246 +
247 +✨ 新功能:
248 +- 支持多种文档格式预览
249 +- 智能选择预览方式
250 +- H5 + 小程序环境适配
251 +
252 +🐛 Bug 修复:
253 +- 修复 PdfPreview 组件 watch 未导入问题
254 +
255 +---
256 +
257 +## 🤝 贡献
258 +
259 +欢迎提交 Issue 和 Pull Request!
260 +
261 +---
262 +
263 +## 📄 许可
264 +
265 +MIT
1 +<!--
2 + * @Description: 统一文档预览组件
3 + * @Date: 2025-01-30
4 + * @Features:
5 + * - H5 环境:使用 OfficeViewer + PdfPreview 组件
6 + * - 小程序环境:根据文件大小自动选择预览方式
7 + * - 小于 10MB:微信原生 API (wx.openDocument)
8 + * - 大于等于 10MB:web-view + 腾讯文档预览
9 +-->
10 +<template>
11 + <view class="document-preview">
12 + <!-- #ifdef H5 -->
13 + <!-- H5 环境:使用现有组件 -->
14 + <OfficeViewer
15 + v-if="isOfficeDocument && src"
16 + :src="src"
17 + :fileType="finalFileType"
18 + @rendered="handleRendered"
19 + @error="handleError"
20 + />
21 +
22 + <PdfPreview
23 + v-if="isPdfDocument && src"
24 + :url="src"
25 + :show="show"
26 + @update:show="handleUpdateShow"
27 + @onLoad="handlePdfLoad"
28 + />
29 + <!-- #endif -->
30 +
31 + <!-- #ifdef WEAPP -->
32 + <!-- 小程序环境:使用微信原生 API 或 web-view -->
33 + <view class="preview-container">
34 + <!-- 加载状态 -->
35 + <view v-if="loading" class="loading-container">
36 + <IconFont name="Loading" size="24" class="animate-spin text-blue-600" />
37 + <text class="loading-text">{{ loadingText }}</text>
38 + </view>
39 +
40 + <!-- 错误状态 -->
41 + <view v-else-if="error" class="error-container">
42 + <IconFont name="Issue" size="48" color="#ff6b6b" />
43 + <text class="error-text">{{ error }}</text>
44 + <nut-button type="primary" size="small" @click="retry">
45 + 重试
46 + </nut-button>
47 + </view>
48 +
49 + <!-- 预览按钮 -->
50 + <view v-else class="action-container">
51 + <view class="file-info">
52 + <IconFont :name="fileIcon" size="64" class="text-blue-600" />
53 + <text class="file-name">{{ fileName || '未知文件' }}</text>
54 + <text class="file-size">{{ formatFileSize(fileSize) }}</text>
55 + </view>
56 +
57 + <nut-button
58 + type="primary"
59 + block
60 + @click="openDocument"
61 + :loading="loading"
62 + >
63 + {{ previewButtonText }}
64 + </nut-button>
65 +
66 + <text v-if="needWebView" class="hint-text">
67 + 大文件将使用在线预览
68 + </text>
69 + </view>
70 + </view>
71 + <!-- #endif -->
72 + </view>
73 +</template>
74 +
75 +<script setup>
76 +import { ref, computed, watch } from 'vue'
77 +import { getFileSize, detectFileType, formatFileSize } from './utils'
78 +import IconFont from '@/components/IconFont.vue'
79 +
80 +// #ifdef H5
81 +import OfficeViewer from '../OfficeViewer.vue'
82 +import PdfPreview from '../PdfPreview.vue'
83 +// #endif
84 +
85 +// #ifdef WEAPP
86 +import Taro from '@tarojs/taro'
87 +// #endif
88 +
89 +// Props 定义
90 +const props = defineProps({
91 + // 文档 URL
92 + src: {
93 + type: String,
94 + required: true
95 + },
96 + // 文件类型(可选,自动检测)
97 + fileType: {
98 + type: String,
99 + default: ''
100 + },
101 + // 是否显示(H5 PDF 预览用)
102 + show: {
103 + type: Boolean,
104 + default: false
105 + },
106 + // 文件名(用于显示)
107 + fileName: {
108 + type: String,
109 + default: ''
110 + }
111 +})
112 +
113 +// Emits 定义
114 +const emit = defineEmits(['rendered', 'error', 'update:show'])
115 +
116 +const finalFileType = computed(() => {
117 + const detectedType = detectFileType(props.src)
118 + return (props.fileType || detectedType || '').toLowerCase()
119 +})
120 +
121 +const isOfficeDocument = computed(() => {
122 + return ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(finalFileType.value)
123 +})
124 +
125 +const isPdfDocument = computed(() => {
126 + return finalFileType.value === 'pdf'
127 +})
128 +
129 +// #ifdef WEAPP
130 +// 响应式数据
131 +const loading = ref(false)
132 +const loadingText = ref('准备中...')
133 +const error = ref('')
134 +const fileSize = ref(0)
135 +
136 +// 计算属性
137 +const needWebView = computed(() => fileSize.value === 0 || fileSize.value >= 10 * 1024 * 1024)
138 +
139 +const fileIcon = computed(() => {
140 + const type = finalFileType.value.toLowerCase()
141 + const iconMap = {
142 + pdf: 'Order',
143 + doc: 'Edit',
144 + docx: 'Edit',
145 + xls: 'Category',
146 + xlsx: 'Category',
147 + ppt: 'PlayCircleFill',
148 + pptx: 'PlayCircleFill'
149 + }
150 + return iconMap[type] || 'Link'
151 +})
152 +
153 +const previewButtonText = computed(() => {
154 + return needWebView.value ? '在线预览文档' : '打开文档'
155 +})
156 +
157 +/**
158 + * 初始化文件信息
159 + */
160 +const initFileInfo = async () => {
161 + if (!props.src) return
162 +
163 + loading.value = true
164 + loadingText.value = '检测文件...'
165 + error.value = ''
166 +
167 + try {
168 + // 获取文件大小
169 + loadingText.value = '获取文件信息...'
170 + const size = await getFileSize(props.src)
171 + fileSize.value = size
172 +
173 + console.log('文件信息:', {
174 + url: props.src,
175 + type: finalFileType.value,
176 + size: size,
177 + needWebView: needWebView.value
178 + })
179 + } catch (err) {
180 + console.error('获取文件信息失败:', err)
181 + error.value = '无法获取文件信息,请检查网络连接'
182 + } finally {
183 + loading.value = false
184 + }
185 +}
186 +
187 +/**
188 + * 打开文档
189 + */
190 +const openDocument = async () => {
191 + loading.value = true
192 + loadingText.value = needWebView.value ? '跳转到在线预览...' : '下载中...'
193 + error.value = ''
194 +
195 + try {
196 + if (needWebView.value) {
197 + // 大文件:使用 web-view 在线预览
198 + await openWithWebView()
199 + } else {
200 + // 小文件:使用微信原生 API
201 + await openWithNativeAPI()
202 + }
203 + } catch (err) {
204 + console.error('打开文档失败:', err)
205 + error.value = err.message || '文档打开失败,请重试'
206 + } finally {
207 + loading.value = false
208 + }
209 +}
210 +
211 +/**
212 + * 使用微信原生 API 打开文档
213 + */
214 +const openWithNativeAPI = async () => {
215 + try {
216 + // 下载文件
217 + const downloadRes = await Taro.downloadFile({
218 + url: props.src,
219 + timeout: 30000 // 30秒超时
220 + })
221 +
222 + if (downloadRes.statusCode !== 200) {
223 + throw new Error('文件下载失败')
224 + }
225 +
226 + // 打开文档
227 + await Taro.openDocument({
228 + filePath: downloadRes.tempFilePath,
229 + fileType: finalFileType.value
230 + })
231 +
232 + console.log('文档打开成功')
233 + } catch (err) {
234 + console.error('微信原生 API 打开文档失败:', err)
235 + throw new Error('文档打开失败: ' + (err.errMsg || err.message))
236 + }
237 +}
238 +
239 +/**
240 + * 使用 web-view 在线预览
241 + */
242 +const openWithWebView = async () => {
243 + try {
244 + // 跳转到 web-view 容器页面
245 + const previewUrl = encodeURIComponent(props.src)
246 + const fileType = encodeURIComponent(finalFileType.value)
247 +
248 + await Taro.navigateTo({
249 + url: `/pages/document-preview/index?url=${previewUrl}&type=${fileType}`
250 + })
251 +
252 + console.log('跳转到在线预览页面')
253 + } catch (err) {
254 + console.error('跳转失败:', err)
255 + throw new Error('打开预览页面失败')
256 + }
257 +}
258 +
259 +/**
260 + * 重试
261 + */
262 +const retry = () => {
263 + initFileInfo()
264 +}
265 +
266 +// 监听 src 变化
267 +watch(() => props.src, (newSrc) => {
268 + if (newSrc) {
269 + initFileInfo()
270 + }
271 +}, { immediate: true })
272 +// #endif
273 +
274 +// #ifdef H5
275 +const handleRendered = () => {
276 + console.log('H5: 文档渲染完成')
277 + emit('rendered')
278 +}
279 +
280 +const handleError = (err) => {
281 + console.error('H5: 文档渲染失败', err)
282 + emit('error', err)
283 +}
284 +
285 +const handleUpdateShow = (value) => {
286 + emit('update:show', value)
287 +}
288 +
289 +const handlePdfLoad = () => {
290 + console.log('H5: PDF 加载完成')
291 +}
292 +// #endif
293 +</script>
294 +
295 +<style lang="less" scoped>
296 +.document-preview {
297 + width: 100%;
298 + height: 100%;
299 + background: #f5f5f5;
300 +}
301 +
302 +// #ifdef WEAPP
303 +.preview-container {
304 + display: flex;
305 + flex-direction: column;
306 + align-items: center;
307 + justify-content: center;
308 + min-height: 800rpx;
309 + padding: 60rpx;
310 + background: #fff;
311 + border-radius: 32rpx;
312 +}
313 +
314 +.loading-container {
315 + display: flex;
316 + flex-direction: column;
317 + align-items: center;
318 + gap: 40rpx;
319 +
320 + .loading-text {
321 + font-size: 56rpx;
322 + color: #999;
323 + }
324 +}
325 +
326 +.error-container {
327 + display: flex;
328 + flex-direction: column;
329 + align-items: center;
330 + gap: 40rpx;
331 + padding: 80rpx 40rpx;
332 +
333 + .error-text {
334 + font-size: 56rpx;
335 + color: #666;
336 + text-align: center;
337 + line-height: 1.6;
338 + }
339 +}
340 +
341 +.action-container {
342 + display: flex;
343 + flex-direction: column;
344 + align-items: center;
345 + gap: 60rpx;
346 + width: 100%;
347 + max-width: 1000rpx;
348 +
349 + .file-info {
350 + display: flex;
351 + flex-direction: column;
352 + align-items: center;
353 + gap: 40rpx;
354 + padding: 80rpx;
355 + background: #f8f9fa;
356 + border-radius: 32rpx;
357 + width: 100%;
358 +
359 + .file-name {
360 + font-size: 64rpx;
361 + font-weight: 500;
362 + color: #333;
363 + text-align: center;
364 + word-break: break-all;
365 + }
366 +
367 + .file-size {
368 + font-size: 48rpx;
369 + color: #999;
370 + }
371 + }
372 +
373 + .hint-text {
374 + font-size: 48rpx;
375 + color: #ff9800;
376 + text-align: center;
377 + }
378 +}
379 +// #endif
380 +</style>
1 +/**
2 + * @Description: 文档预览工具函数
3 + * @Date: 2025-01-30
4 + */
5 +
6 +// #ifdef WEAPP
7 +import Taro from '@tarojs/taro'
8 +// #endif
9 +
10 +/**
11 + * 从 URL 中检测文件类型
12 + * @param {string} url - 文档 URL
13 + * @returns {string} 文件类型(小写)
14 + */
15 +export function detectFileType(url) {
16 + if (!url) return ''
17 +
18 + // 从 URL 中提取扩展名
19 + const match = url.match(/\.([a-z0-9]+)(?:\?|#|$)/i)
20 +
21 + if (match && match[1]) {
22 + const ext = match[1].toLowerCase()
23 +
24 + // 映射常见扩展名到统一类型
25 + const typeMap = {
26 + pdf: 'pdf',
27 + doc: 'doc',
28 + docx: 'docx',
29 + xls: 'xls',
30 + xlsx: 'xlsx',
31 + ppt: 'ppt',
32 + pptx: 'pptx'
33 + }
34 +
35 + return typeMap[ext] || ext
36 + }
37 +
38 + // 如果无法从 URL 判断,尝试从 Content-Type 头(需要后端支持)
39 + return ''
40 +}
41 +
42 +/**
43 + * 获取文件大小(通过 HEAD 请求)
44 + * @param {string} url - 文档 URL
45 + * @returns {Promise<number>} 文件大小(字节)
46 + */
47 +export async function getFileSize(url) {
48 + return new Promise((resolve) => {
49 + // #ifdef H5
50 + // H5 环境:使用 fetch HEAD 请求
51 + fetch(url, { method: 'HEAD' })
52 + .then(response => {
53 + const contentLength = response.headers.get('Content-Length')
54 + if (contentLength) {
55 + resolve(parseInt(contentLength, 10))
56 + } else {
57 + // 无法获取大小,返回 0(将使用 web-view)
58 + resolve(0)
59 + }
60 + })
61 + .catch(err => {
62 + console.error('获取文件大小失败:', err)
63 + // 失败时返回 0,将使用 web-view
64 + resolve(0)
65 + })
66 + // #endif
67 +
68 + // #ifdef WEAPP
69 + // 小程序环境:使用 Taro.request HEAD 请求
70 + Taro.request({
71 + url: url,
72 + method: 'HEAD',
73 + success: (res) => {
74 + const contentLength = res.header['Content-Length'] || res.header['content-length']
75 + if (contentLength) {
76 + resolve(parseInt(contentLength, 10))
77 + } else {
78 + // 无法获取大小,返回 0(将使用 web-view)
79 + resolve(0)
80 + }
81 + },
82 + fail: (err) => {
83 + console.error('获取文件大小失败:', err)
84 + // 失败时返回 0,将使用 web-view
85 + resolve(0)
86 + }
87 + })
88 + // #endif
89 + })
90 +}
91 +
92 +/**
93 + * 格式化文件大小显示
94 + * @param {number} bytes - 文件大小(字节)
95 + * @returns {string} 格式化后的字符串
96 + */
97 +export function formatFileSize(bytes) {
98 + if (!bytes || bytes === 0) return '未知大小'
99 +
100 + const units = ['B', 'KB', 'MB', 'GB']
101 + let size = bytes
102 + let unitIndex = 0
103 +
104 + while (size >= 1024 && unitIndex < units.length - 1) {
105 + size /= 1024
106 + unitIndex++
107 + }
108 +
109 + // 保留两位小数
110 + const formatted = size.toFixed(2).replace(/\.00$/, '')
111 +
112 + return `${formatted} ${units[unitIndex]}`
113 +}
114 +
115 +/**
116 + * 判断是否为支持的文档类型
117 + * @param {string} fileType - 文件类型
118 + * @returns {boolean}
119 + */
120 +export function isSupportedDocumentType(fileType) {
121 + const supportedTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']
122 + return supportedTypes.includes(fileType?.toLowerCase())
123 +}
124 +
125 +/**
126 + * 获取文件图标名称
127 + * @param {string} fileType - 文件类型
128 + * @returns {string} 图标名称
129 + */
130 +export function getFileIconName(fileType) {
131 + const iconMap = {
132 + pdf: 'pdf',
133 + doc: 'word',
134 + docx: 'word',
135 + xls: 'excel',
136 + xlsx: 'excel',
137 + ppt: 'ppt',
138 + pptx: 'ppt'
139 + }
140 + return iconMap[fileType?.toLowerCase()] || 'file'
141 +}
142 +
143 +/**
144 + * 生成腾讯文档预览 URL
145 + * @param {string} url - 原始文档 URL
146 + * @returns {string} 腾讯文档预览 URL
147 + */
148 +export function getTencentPreviewUrl(url) {
149 + const encodedUrl = encodeURIComponent(url)
150 + return `https://view.officeapps.live.com/op/view.aspx?src=${encodedUrl}`
151 +}
152 +
153 +/**
154 + * 生成微软在线预览 URL
155 + * @param {string} url - 原始文档 URL
156 + * @returns {string} 微软预览 URL
157 + */
158 +export function getMicrosoftPreviewUrl(url) {
159 + const encodedUrl = encodeURIComponent(url)
160 + return `https://view.officeapps.live.com/op/embed.aspx?src=${encodedUrl}`
161 +}
...@@ -17,12 +17,19 @@ import { ...@@ -17,12 +17,19 @@ import {
17 Check, 17 Check,
18 Checklist, 18 Checklist,
19 Clock, 19 Clock,
20 + Download,
20 Edit, 21 Edit,
21 Find, 22 Find,
22 Home, 23 Home,
24 + Issue,
25 + Link,
26 + Loading,
27 + Location,
23 My, 28 My,
24 Order, 29 Order,
30 + People,
25 PlayCircleFill, 31 PlayCircleFill,
32 + Refresh,
26 RectRight, 33 RectRight,
27 RectLeft, 34 RectLeft,
28 Search, 35 Search,
...@@ -58,12 +65,19 @@ const icons = { ...@@ -58,12 +65,19 @@ const icons = {
58 Check, 65 Check,
59 Checklist, 66 Checklist,
60 Clock, 67 Clock,
68 + Download,
61 Edit, 69 Edit,
62 Find, 70 Find,
63 Home, 71 Home,
72 + Issue,
73 + Link,
74 + Loading,
75 + Location,
64 My, 76 My,
65 Order, 77 Order,
78 + People,
66 PlayCircleFill, 79 PlayCircleFill,
80 + Refresh,
67 RectRight, 81 RectRight,
68 RectLeft, 82 RectLeft,
69 Search, 83 Search,
......
1 +<template>
2 + <view class="office-viewer">
3 + <view v-if="error" class="error-container">
4 + <IconFont name="Issue" size="24" color="#ff6b6b" />
5 + <text class="error-text">{{ error }}</text>
6 + <nut-button type="primary" size="small" class="retry-btn" @click="retry">重试</nut-button>
7 + </view>
8 +
9 + <view v-else class="document-container">
10 + <!-- #ifdef H5 -->
11 + <iframe
12 + v-if="preview_url"
13 + :src="preview_url"
14 + frameborder="0"
15 + class="preview-iframe"
16 + :style="{ height: container_height }"
17 + @load="on_loaded"
18 + />
19 + <view v-else class="unsupported-container">
20 + <IconFont name="Issue" size="24" color="#ff6b6b" />
21 + <text class="unsupported-text">不支持的文件类型: {{ normalized_type || '未知' }}</text>
22 + </view>
23 + <!-- #endif -->
24 +
25 + <!-- #ifdef WEAPP -->
26 + <view class="unsupported-container">
27 + <IconFont name="Issue" size="24" color="#ff6b6b" />
28 + <text class="unsupported-text">小程序不支持内嵌 Office 预览</text>
29 + </view>
30 + <!-- #endif -->
31 + </view>
32 +
33 + <view v-if="loading" class="loading-overlay">
34 + <IconFont name="Loading" size="24" class="animate-spin text-blue-600" />
35 + <text class="loading-text">加载中...</text>
36 + </view>
37 + </view>
38 +</template>
39 +
40 +<script setup>
41 +import { ref, computed, watch } from 'vue'
42 +import IconFont from '@/components/IconFont.vue'
43 +import { getTencentPreviewUrl } from '@/components/DocumentPreview/utils'
44 +
45 +const props = defineProps({
46 + src: {
47 + type: [String, ArrayBuffer],
48 + required: true
49 + },
50 + fileType: {
51 + type: String,
52 + default: ''
53 + },
54 + height: {
55 + type: String,
56 + default: '70vh'
57 + }
58 +})
59 +
60 +const emit = defineEmits(['rendered', 'error', 'retry'])
61 +
62 +const loading = ref(false)
63 +const error = ref('')
64 +
65 +const container_height = computed(() => props.height)
66 +
67 +const normalized_type = computed(() => {
68 + const raw_type = (props.fileType || '').toLowerCase()
69 + if (raw_type === 'doc' || raw_type === 'docx') return 'docx'
70 + if (raw_type === 'xls' || raw_type === 'xlsx') return 'xlsx'
71 + if (raw_type === 'ppt' || raw_type === 'pptx') return 'pptx'
72 + if (raw_type === 'pdf') return 'pdf'
73 + return raw_type
74 +})
75 +
76 +const preview_url = computed(() => {
77 + if (!props.src || typeof props.src !== 'string') return ''
78 +
79 + if (normalized_type.value === 'pdf') return props.src
80 + if (['docx', 'xlsx', 'pptx'].includes(normalized_type.value)) {
81 + return getTencentPreviewUrl(props.src)
82 + }
83 +
84 + return ''
85 +})
86 +
87 +const on_loaded = () => {
88 + loading.value = false
89 + emit('rendered')
90 +}
91 +
92 +const retry = () => {
93 + loading.value = true
94 + error.value = ''
95 + emit('retry')
96 +}
97 +
98 +watch(() => [props.src, props.fileType], () => {
99 + error.value = ''
100 +
101 + if (!props.src) {
102 + loading.value = false
103 + error.value = '文档地址不能为空'
104 + emit('error', new Error(error.value))
105 + return
106 + }
107 +
108 + loading.value = true
109 +
110 + if (!preview_url.value) {
111 + loading.value = false
112 + error.value = '文档类型不支持或地址格式不正确'
113 + emit('error', new Error(error.value))
114 + }
115 +}, { immediate: true })
116 +</script>
117 +
118 +<style lang="less" scoped>
119 +.office-viewer {
120 + position: relative;
121 + width: 100%;
122 + height: 100%;
123 + background: #fff;
124 + border-radius: 8px;
125 + overflow: hidden;
126 +
127 + .error-container,
128 + .unsupported-container {
129 + display: flex;
130 + flex-direction: column;
131 + align-items: center;
132 + justify-content: center;
133 + height: 400rpx;
134 + gap: 24rpx;
135 + padding: 40rpx;
136 +
137 + .error-text,
138 + .unsupported-text {
139 + font-size: 28rpx;
140 + color: #666;
141 + text-align: center;
142 + line-height: 1.5;
143 + }
144 +
145 + .retry-btn {
146 + margin-top: 16rpx;
147 + }
148 + }
149 +
150 + .document-container {
151 + width: 100%;
152 + height: 100%;
153 + }
154 +
155 + .preview-iframe {
156 + width: 100%;
157 + border: none;
158 + }
159 +
160 + .loading-overlay {
161 + position: absolute;
162 + top: 50%;
163 + left: 50%;
164 + transform: translate(-50%, -50%);
165 + z-index: 10;
166 + background: rgba(255, 255, 255, 0.9);
167 + border-radius: 16rpx;
168 + padding: 24rpx 32rpx;
169 + display: flex;
170 + align-items: center;
171 + gap: 16rpx;
172 +
173 + .loading-text {
174 + font-size: 28rpx;
175 + color: #333;
176 + }
177 + }
178 +}
179 +</style>
1 +<!--
2 + * @Date: 2024-01-17
3 + * @Description: PDF预览组件
4 +-->
5 +<template>
6 + <view v-if="show" class="pdf-preview">
7 + <view class="mask" @tap="close"></view>
8 + <view class="panel">
9 + <view class="header">
10 + <text class="title">{{ title || 'PDF 预览' }}</text>
11 + <view class="close" @tap="close">
12 + <IconFont name="Del" size="16" color="#666" />
13 + </view>
14 + </view>
15 +
16 + <view class="body">
17 + <!-- #ifdef H5 -->
18 + <iframe
19 + v-if="url"
20 + class="pdf-iframe"
21 + :src="url"
22 + frameborder="0"
23 + @load="on_loaded"
24 + />
25 + <view v-else class="empty">
26 + <text class="empty-text">PDF 地址不能为空</text>
27 + </view>
28 + <!-- #endif -->
29 +
30 + <!-- #ifdef WEAPP -->
31 + <view class="empty">
32 + <text class="empty-text">小程序不支持内嵌 PDF 预览</text>
33 + </view>
34 + <!-- #endif -->
35 + </view>
36 +
37 + <view v-if="loading" class="loading">
38 + <IconFont name="Loading" size="24" class="animate-spin text-blue-600" />
39 + <text class="loading-text">加载中...</text>
40 + </view>
41 + </view>
42 + </view>
43 +</template>
44 +
45 +<script setup>
46 +import { ref, watch } from 'vue'
47 +import IconFont from '@/components/IconFont.vue'
48 +
49 +const props = defineProps({
50 + show: {
51 + type: Boolean,
52 + default: false
53 + },
54 + url: {
55 + type: String,
56 + default: ''
57 + },
58 + title: {
59 + type: String,
60 + default: ''
61 + }
62 +})
63 +
64 +const emit = defineEmits(['update:show', 'onLoad'])
65 +
66 +const loading = ref(false)
67 +
68 +const close = () => {
69 + emit('update:show', false)
70 +}
71 +
72 +const on_loaded = () => {
73 + loading.value = false
74 + emit('onLoad', false)
75 +}
76 +
77 +watch(() => props.show, (new_show) => {
78 + if (new_show) {
79 + loading.value = true
80 + } else {
81 + loading.value = false
82 + }
83 +}, { immediate: true })
84 +</script>
85 +
86 +<style lang="less" scoped>
87 +.pdf-preview {
88 + position: fixed;
89 + inset: 0;
90 + z-index: 999;
91 +
92 + .mask {
93 + position: absolute;
94 + inset: 0;
95 + background: rgba(0, 0, 0, 0.5);
96 + }
97 +
98 + .panel {
99 + position: absolute;
100 + top: 0;
101 + right: 0;
102 + width: 100%;
103 + height: 100%;
104 + background: #fff;
105 + display: flex;
106 + flex-direction: column;
107 + }
108 +
109 + .header {
110 + height: 96rpx;
111 + padding: 0 24rpx;
112 + display: flex;
113 + align-items: center;
114 + justify-content: space-between;
115 + border-bottom: 2rpx solid #f1f5f9;
116 +
117 + .title {
118 + font-size: 30rpx;
119 + font-weight: 600;
120 + color: #111827;
121 + }
122 +
123 + .close {
124 + width: 64rpx;
125 + height: 64rpx;
126 + display: flex;
127 + align-items: center;
128 + justify-content: center;
129 + border-radius: 9999rpx;
130 + background: #f3f4f6;
131 + }
132 + }
133 +
134 + .body {
135 + flex: 1;
136 + position: relative;
137 + }
138 +
139 + .pdf-iframe {
140 + width: 100%;
141 + height: 100%;
142 + border: none;
143 + }
144 +
145 + .empty {
146 + width: 100%;
147 + height: 100%;
148 + display: flex;
149 + align-items: center;
150 + justify-content: center;
151 + background: #f9fafb;
152 +
153 + .empty-text {
154 + font-size: 28rpx;
155 + color: #6b7280;
156 + }
157 + }
158 +
159 + .loading {
160 + position: absolute;
161 + top: 50%;
162 + left: 50%;
163 + transform: translate(-50%, -50%);
164 + background: rgba(255, 255, 255, 0.9);
165 + border-radius: 16rpx;
166 + padding: 24rpx 32rpx;
167 + display: flex;
168 + align-items: center;
169 + gap: 16rpx;
170 +
171 + .loading-text {
172 + font-size: 28rpx;
173 + color: #333;
174 + }
175 + }
176 +}
177 +</style>
1 +/**
2 + * @Description: 文档预览示例页面配置
3 + */
4 +export default {
5 + navigationBarTitleText: '文档预览示例',
6 + navigationBarBackgroundColor: '#4caf50',
7 + navigationBarTextStyle: 'white',
8 + backgroundColor: '#f5f5f5',
9 + enablePullDownRefresh: false
10 +}
1 +<!--
2 + * @Description: 文档预览示例页面
3 + * @Date: 2025-01-30
4 + * @Usage: 展示 DocumentPreview 组件的各种使用场景
5 +-->
6 +<template>
7 + <view class="document-demo-page">
8 + <nut-tabs v-model="activeTab">
9 + <nut-tab-pane title="PDF 预览" pane-key="pdf">
10 + <DocumentPreview
11 + src="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
12 + fileType="pdf"
13 + fileName="示例 PDF 文档.pdf"
14 + @rendered="handleRendered"
15 + @error="handleError"
16 + />
17 + </nut-tab-pane>
18 +
19 + <nut-tab-pane title="Word 预览" pane-key="word">
20 + <DocumentPreview
21 + src="https://calibre-ebook.com/downloads/demos/demo.docx"
22 + fileType="docx"
23 + fileName="示例 Word 文档.docx"
24 + @rendered="handleRendered"
25 + @error="handleError"
26 + />
27 + </nut-tab-pane>
28 +
29 + <nut-tab-pane title="Excel 预览" pane-key="excel">
30 + <DocumentPreview
31 + src="https://filesamples.com/samples/document/xlsx/sample1.xlsx"
32 + fileType="xlsx"
33 + fileName="示例 Excel 文档.xlsx"
34 + @rendered="handleRendered"
35 + @error="handleError"
36 + />
37 + </nut-tab-pane>
38 +
39 + <nut-tab-pane title="PPT 预览" pane-key="ppt">
40 + <DocumentPreview
41 + src="https://filesamples.com/samples/document/ppt/sample1.ppt"
42 + fileType="ppt"
43 + fileName="示例 PPT 文档.ppt"
44 + @rendered="handleRendered"
45 + @error="handleError"
46 + />
47 + </nut-tab-pane>
48 + </nut-tabs>
49 + </view>
50 +</template>
51 +
52 +<script setup>
53 +import { ref } from 'vue'
54 +import DocumentPreview from '@/components/DocumentPreview/index.vue'
55 +
56 +// #ifdef WEAPP
57 +import Taro from '@tarojs/taro'
58 +// #endif
59 +
60 +const activeTab = ref('pdf')
61 +
62 +const handleRendered = () => {
63 + console.log('文档渲染完成')
64 + // #ifdef WEAPP
65 + Taro.showToast({
66 + title: '文档加载完成',
67 + icon: 'success'
68 + })
69 + // #endif
70 +}
71 +
72 +const handleError = (err) => {
73 + console.error('文档渲染失败:', err)
74 + // #ifdef WEAPP
75 + Taro.showToast({
76 + title: '文档加载失败',
77 + icon: 'error'
78 + })
79 + // #endif
80 +}
81 +</script>
82 +
83 +<style lang="less" scoped>
84 +.document-demo-page {
85 + min-height: 100vh;
86 + background: #f5f5f5;
87 +}
88 +
89 +.placeholder {
90 + display: flex;
91 + flex-direction: column;
92 + align-items: center;
93 + justify-content: center;
94 + min-height: 800rpx;
95 + padding: 40rpx;
96 + gap: 20rpx;
97 +
98 + .placeholder-text {
99 + font-size: 32rpx;
100 + font-weight: 500;
101 + color: #333;
102 + }
103 +
104 + .placeholder-hint {
105 + font-size: 28rpx;
106 + color: #999;
107 + }
108 +}
109 +</style>
1 +/**
2 + * @Description: 文档预览页面配置
3 + */
4 +export default {
5 + navigationBarTitleText: '文档预览',
6 + navigationBarBackgroundColor: '#4caf50',
7 + navigationBarTextStyle: 'white',
8 + backgroundColor: '#ffffff',
9 + enablePullDownRefresh: false
10 +}
1 +<!--
2 + * @Description: 文档在线预览页面(web-view 容器)
3 + * @Date: 2025-01-30
4 + * @Usage: 用于大文件(>= 10MB)的在线预览
5 +-->
6 +<template>
7 + <view class="document-preview-page">
8 + <!-- #ifdef WEAPP -->
9 + <web-view :src="previewUrl" @message="handleMessage" @load="handleLoad" @error="handleError" />
10 + <!-- #endif -->
11 +
12 + <!-- #ifdef H5 -->
13 + <iframe :src="previewUrl" frameborder="0" class="preview-iframe" />
14 + <!-- #endif -->
15 + </view>
16 +</template>
17 +
18 +<script setup>
19 +import { computed, ref } from 'vue'
20 +import { useLoad, useReady } from '@tarojs/taro'
21 +import Taro from '@tarojs/taro'
22 +import { getTencentPreviewUrl } from '@/components/DocumentPreview/utils'
23 +
24 +// 响应式数据
25 +const url = ref('')
26 +const fileType = ref('')
27 +const loading = ref(true)
28 +
29 +// 计算属性
30 +const previewUrl = computed(() => {
31 + if (!url.value) return ''
32 +
33 + const decodedUrl = decodeURIComponent(url.value)
34 +
35 + // 根据文件类型选择预览方式
36 + if (fileType.value === 'pdf') {
37 + // PDF 可以直接显示(需要支持跨域)
38 + return decodedUrl
39 + } else {
40 + // Office 文档使用腾讯文档预览
41 + return getTencentPreviewUrl(decodedUrl)
42 + }
43 +})
44 +
45 +// 页面加载
46 +useLoad((options) => {
47 + console.log('文档预览页面参数:', options)
48 +
49 + if (options.url) {
50 + url.value = options.url
51 + }
52 +
53 + if (options.type) {
54 + fileType.value = decodeURIComponent(options.type)
55 + }
56 +
57 + // 设置导航栏标题
58 + const titleMap = {
59 + pdf: 'PDF 预览',
60 + doc: 'Word 预览',
61 + docx: 'Word 预览',
62 + xls: 'Excel 预览',
63 + xlsx: 'Excel 预览',
64 + ppt: 'PPT 预览',
65 + pptx: 'PPT 预览'
66 + }
67 +
68 + const title = titleMap[fileType.value] || '文档预览'
69 +
70 + // #ifdef WEAPP
71 + Taro.setNavigationBarTitle({ title })
72 + // #endif
73 +})
74 +
75 +useReady(() => {
76 + console.log('文档预览页面 ready')
77 +})
78 +
79 +/**
80 + * web-view 加载完成
81 + */
82 +const handleLoad = () => {
83 + console.log('web-view 加载完成')
84 + loading.value = false
85 +
86 + // #ifdef WEAPP
87 + Taro.hideLoading()
88 + // #endif
89 +}
90 +
91 +/**
92 + * web-view 错误
93 + */
94 +const handleError = (e) => {
95 + console.error('web-view 加载失败:', e)
96 + loading.value = false
97 +
98 + // #ifdef WEAPP
99 + Taro.hideLoading()
100 + Taro.showToast({
101 + title: '预览加载失败',
102 + icon: 'none'
103 + })
104 + // #endif
105 +}
106 +
107 +/**
108 + * 接收 web-view 消息
109 + */
110 +const handleMessage = (e) => {
111 + console.log('收到 web-view 消息:', e.detail.data)
112 +}
113 +</script>
114 +
115 +<style lang="less" scoped>
116 +.document-preview-page {
117 + width: 100%;
118 + height: 100vh;
119 + background: #fff;
120 +}
121 +
122 +// #ifdef WEAPP
123 +web-view {
124 + width: 100%;
125 + height: 100%;
126 +}
127 +// #endif
128 +
129 +// #ifdef H5
130 +.preview-iframe {
131 + width: 100%;
132 + height: 100vh;
133 + border: none;
134 +}
135 +// #endif
136 +</style>