feat: 新增文档预览组件及相关页面
- 新增 DocumentPreview 统一文档预览组件,支持 PDF/Word/Excel/PPT 格式 - 新增文档预览页面和示例页面,支持大文件在线预览 - 新增 OfficeViewer 和 PdfPreview 组件用于 H5 环境 - 新增文档预览工具函数,支持文件类型检测和大小获取 - 配置 ESLint 以支持 Vue 项目,修复相关依赖问题 - 更新 IconFont 组件,添加文档预览所需图标 - 在应用配置中注册新增页面路由
Showing
17 changed files
with
1519 additions
and
2 deletions
| ... | @@ -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 | } | ... | ... |
.eslintrc.cjs
0 → 100644
| 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', | ... | ... |
src/components/DocumentPreview/README.md
0 → 100644
| 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 |
src/components/DocumentPreview/index.vue
0 → 100644
| 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> |
src/components/DocumentPreview/utils.js
0 → 100644
| 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, | ... | ... |
src/components/OfficeViewer.vue
0 → 100644
| 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> |
src/components/PdfPreview.vue
0 → 100644
| 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> |
src/pages/document-demo/index.config.js
0 → 100644
src/pages/document-demo/index.vue
0 → 100644
| 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> |
src/pages/document-preview/index.config.js
0 → 100644
src/pages/document-preview/index.vue
0 → 100644
| 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> |
-
Please register or login to post a comment