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 },
......
...@@ -11,6 +11,9 @@ importers: ...@@ -11,6 +11,9 @@ importers:
11 '@babel/runtime': 11 '@babel/runtime':
12 specifier: ^7.7.7 12 specifier: ^7.7.7
13 version: 7.28.6 13 version: 7.28.6
14 + '@nutui/icons-vue':
15 + specifier: ^0.1.1
16 + version: 0.1.1
14 '@nutui/icons-vue-taro': 17 '@nutui/icons-vue-taro':
15 specifier: ^0.0.9 18 specifier: ^0.0.9
16 version: 0.0.9 19 version: 0.0.9
...@@ -122,7 +125,16 @@ importers: ...@@ -122,7 +125,16 @@ importers:
122 version: 8.57.1 125 version: 8.57.1
123 eslint-config-taro: 126 eslint-config-taro:
124 specifier: 4.1.9 127 specifier: 4.1.9
125 - version: 4.1.9(@babel/core@7.28.6)(eslint@8.57.1)(typescript@5.9.3) 128 + version: 4.1.9(@babel/core@7.28.6)(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint-plugin-vue@8.7.1(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3)
129 + eslint-plugin-react:
130 + specifier: ^7.33.2
131 + version: 7.37.5(eslint@8.57.1)
132 + eslint-plugin-react-hooks:
133 + specifier: ^4.4.0
134 + version: 4.6.2(eslint@8.57.1)
135 + eslint-plugin-vue:
136 + specifier: ^8.0.0
137 + version: 8.7.1(eslint@8.57.1)
126 js-yaml: 138 js-yaml:
127 specifier: ^4.1.1 139 specifier: ^4.1.1
128 version: 4.1.1 140 version: 4.1.1
...@@ -144,6 +156,9 @@ importers: ...@@ -144,6 +156,9 @@ importers:
144 unplugin-vue-components: 156 unplugin-vue-components:
145 specifier: ^0.26.0 157 specifier: ^0.26.0
146 version: 0.26.0(@babel/parser@7.28.6)(rollup@3.29.5)(vue@3.5.27(typescript@5.9.3)) 158 version: 0.26.0(@babel/parser@7.28.6)(rollup@3.29.5)(vue@3.5.27(typescript@5.9.3))
159 + vue-eslint-parser:
160 + specifier: ^9.0.0
161 + version: 9.4.3(eslint@8.57.1)
147 vue-loader: 162 vue-loader:
148 specifier: ^17.0.0 163 specifier: ^17.0.0
149 version: 17.4.2(@vue/compiler-sfc@3.5.27)(vue@3.5.27(typescript@5.9.3))(webpack@5.91.0(@swc/core@1.3.96)) 164 version: 17.4.2(@vue/compiler-sfc@3.5.27)(vue@3.5.27(typescript@5.9.3))(webpack@5.91.0(@swc/core@1.3.96))
...@@ -1443,6 +1458,9 @@ packages: ...@@ -1443,6 +1458,9 @@ packages:
1443 '@nutui/icons-vue-taro@0.0.9': 1458 '@nutui/icons-vue-taro@0.0.9':
1444 resolution: {integrity: sha512-10VYAtFC+o1X0anGs+y2PgF1NWMeLFz2JVMRw4BWLg6wbtVbYy9wukLxyGhZC6Yf6t39DcwaGVda8paV7K6/Ew==} 1459 resolution: {integrity: sha512-10VYAtFC+o1X0anGs+y2PgF1NWMeLFz2JVMRw4BWLg6wbtVbYy9wukLxyGhZC6Yf6t39DcwaGVda8paV7K6/Ew==}
1445 1460
1461 + '@nutui/icons-vue@0.1.1':
1462 + resolution: {integrity: sha512-ekn6R9GNHWNUcV4pxdhQzrI1g1VhZ5zknklXoUJrCzt0RkRRLU0bUXX3ouWT0sl0U3MNJClnXAzZe7iPAvgPtw==}
1463 +
1446 '@nutui/nutui-taro@4.3.14': 1464 '@nutui/nutui-taro@4.3.14':
1447 resolution: {integrity: sha512-MeAipTH2i35H1IiZOMDy2yTuIbhl5qk9x4XYnVh0wgjb+zc3oTaJ9mIds3IleryFI0wxYZJsN4xCHIYSEqLutA==} 1465 resolution: {integrity: sha512-MeAipTH2i35H1IiZOMDy2yTuIbhl5qk9x4XYnVh0wgjb+zc3oTaJ9mIds3IleryFI0wxYZJsN4xCHIYSEqLutA==}
1448 peerDependencies: 1466 peerDependencies:
...@@ -2446,6 +2464,10 @@ packages: ...@@ -2446,6 +2464,10 @@ packages:
2446 resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 2464 resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
2447 engines: {node: '>=8'} 2465 engines: {node: '>=8'}
2448 2466
2467 + array.prototype.findlast@1.2.5:
2468 + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==}
2469 + engines: {node: '>= 0.4'}
2470 +
2449 array.prototype.findlastindex@1.2.6: 2471 array.prototype.findlastindex@1.2.6:
2450 resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} 2472 resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==}
2451 engines: {node: '>= 0.4'} 2473 engines: {node: '>= 0.4'}
...@@ -2458,6 +2480,10 @@ packages: ...@@ -2458,6 +2480,10 @@ packages:
2458 resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} 2480 resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==}
2459 engines: {node: '>= 0.4'} 2481 engines: {node: '>= 0.4'}
2460 2482
2483 + array.prototype.tosorted@1.1.4:
2484 + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==}
2485 + engines: {node: '>= 0.4'}
2486 +
2461 arraybuffer.prototype.slice@1.0.4: 2487 arraybuffer.prototype.slice@1.0.4:
2462 resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} 2488 resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
2463 engines: {node: '>= 0.4'} 2489 engines: {node: '>= 0.4'}
...@@ -3352,6 +3378,10 @@ packages: ...@@ -3352,6 +3378,10 @@ packages:
3352 resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 3378 resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
3353 engines: {node: '>= 0.4'} 3379 engines: {node: '>= 0.4'}
3354 3380
3381 + es-iterator-helpers@1.2.2:
3382 + resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==}
3383 + engines: {node: '>= 0.4'}
3384 +
3355 es-module-lexer@0.10.5: 3385 es-module-lexer@0.10.5:
3356 resolution: {integrity: sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==} 3386 resolution: {integrity: sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==}
3357 3387
...@@ -3454,6 +3484,24 @@ packages: ...@@ -3454,6 +3484,24 @@ packages:
3454 '@typescript-eslint/parser': 3484 '@typescript-eslint/parser':
3455 optional: true 3485 optional: true
3456 3486
3487 + eslint-plugin-react-hooks@4.6.2:
3488 + resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==}
3489 + engines: {node: '>=10'}
3490 + peerDependencies:
3491 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
3492 +
3493 + eslint-plugin-react@7.37.5:
3494 + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
3495 + engines: {node: '>=4'}
3496 + peerDependencies:
3497 + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
3498 +
3499 + eslint-plugin-vue@8.7.1:
3500 + resolution: {integrity: sha512-28sbtm4l4cOzoO1LtzQPxfxhQABararUb1JtqusQqObJpWX2e/gmVyeYVfepizPFne0Q5cILkYGiBoV36L12Wg==}
3501 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
3502 + peerDependencies:
3503 + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
3504 +
3457 eslint-scope@5.1.1: 3505 eslint-scope@5.1.1:
3458 resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} 3506 resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
3459 engines: {node: '>=8.0.0'} 3507 engines: {node: '>=8.0.0'}
...@@ -3462,6 +3510,12 @@ packages: ...@@ -3462,6 +3510,12 @@ packages:
3462 resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} 3510 resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
3463 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 3511 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
3464 3512
3513 + eslint-utils@3.0.0:
3514 + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
3515 + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
3516 + peerDependencies:
3517 + eslint: '>=5'
3518 +
3465 eslint-visitor-keys@2.1.0: 3519 eslint-visitor-keys@2.1.0:
3466 resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} 3520 resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==}
3467 engines: {node: '>=10'} 3521 engines: {node: '>=10'}
...@@ -4280,6 +4334,10 @@ packages: ...@@ -4280,6 +4334,10 @@ packages:
4280 resolution: {integrity: sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==} 4334 resolution: {integrity: sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==}
4281 engines: {node: '>= 4'} 4335 engines: {node: '>= 4'}
4282 4336
4337 + iterator.prototype@1.1.5:
4338 + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
4339 + engines: {node: '>= 0.4'}
4340 +
4283 j-component@1.4.9: 4341 j-component@1.4.9:
4284 resolution: {integrity: sha512-7TaTylECTW4sRaDLaj463sTj9BK6/3rSD67um47ypLPwtZW3wPwynCQ9sdnEJmTIw9Jfy2ZLKWiSDRdaINv50w==} 4342 resolution: {integrity: sha512-7TaTylECTW4sRaDLaj463sTj9BK6/3rSD67um47ypLPwtZW3wPwynCQ9sdnEJmTIw9Jfy2ZLKWiSDRdaINv50w==}
4285 4343
...@@ -4373,6 +4431,10 @@ packages: ...@@ -4373,6 +4431,10 @@ packages:
4373 jsonp-retry@1.0.3: 4431 jsonp-retry@1.0.3:
4374 resolution: {integrity: sha512-/jmE9+shtKP+oIt2AWO9Wx+C27NTGpLCEw4QHOqpoV2X6ta374HE9C+EEdgu8r3iLKgFMx7u5j0mCwxWN8UdlA==} 4432 resolution: {integrity: sha512-/jmE9+shtKP+oIt2AWO9Wx+C27NTGpLCEw4QHOqpoV2X6ta374HE9C+EEdgu8r3iLKgFMx7u5j0mCwxWN8UdlA==}
4375 4433
4434 + jsx-ast-utils@3.3.5:
4435 + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
4436 + engines: {node: '>=4.0'}
4437 +
4376 keyv@3.0.0: 4438 keyv@3.0.0:
4377 resolution: {integrity: sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==} 4439 resolution: {integrity: sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==}
4378 4440
...@@ -4642,6 +4704,10 @@ packages: ...@@ -4642,6 +4704,10 @@ packages:
4642 resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} 4704 resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
4643 engines: {node: '>= 0.6.0'} 4705 engines: {node: '>= 0.6.0'}
4644 4706
4707 + loose-envify@1.4.0:
4708 + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
4709 + hasBin: true
4710 +
4645 lower-case@1.1.4: 4711 lower-case@1.1.4:
4646 resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} 4712 resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==}
4647 4713
...@@ -4925,6 +4991,10 @@ packages: ...@@ -4925,6 +4991,10 @@ packages:
4925 resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} 4991 resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==}
4926 engines: {node: '>= 0.4'} 4992 engines: {node: '>= 0.4'}
4927 4993
4994 + object.entries@1.1.9:
4995 + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==}
4996 + engines: {node: '>= 0.4'}
4997 +
4928 object.fromentries@2.0.8: 4998 object.fromentries@2.0.8:
4929 resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} 4999 resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==}
4930 engines: {node: '>= 0.4'} 5000 engines: {node: '>= 0.4'}
...@@ -5687,6 +5757,9 @@ packages: ...@@ -5687,6 +5757,9 @@ packages:
5687 promise-polyfill@7.1.2: 5757 promise-polyfill@7.1.2:
5688 resolution: {integrity: sha512-FuEc12/eKqqoRYIGBrUptCBRhobL19PS2U31vMNTfyck1FxPyMfgsXyW4Mav85y/ZN1hop3hOwRlUDok23oYfQ==} 5758 resolution: {integrity: sha512-FuEc12/eKqqoRYIGBrUptCBRhobL19PS2U31vMNTfyck1FxPyMfgsXyW4Mav85y/ZN1hop3hOwRlUDok23oYfQ==}
5689 5759
5760 + prop-types@15.8.1:
5761 + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
5762 +
5690 property-expr@2.0.6: 5763 property-expr@2.0.6:
5691 resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} 5764 resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==}
5692 5765
...@@ -5757,6 +5830,9 @@ packages: ...@@ -5757,6 +5830,9 @@ packages:
5757 resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} 5830 resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
5758 hasBin: true 5831 hasBin: true
5759 5832
5833 + react-is@16.13.1:
5834 + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
5835 +
5760 react-is@17.0.2: 5836 react-is@17.0.2:
5761 resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} 5837 resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
5762 5838
...@@ -5866,6 +5942,10 @@ packages: ...@@ -5866,6 +5942,10 @@ packages:
5866 engines: {node: '>= 0.4'} 5942 engines: {node: '>= 0.4'}
5867 hasBin: true 5943 hasBin: true
5868 5944
5945 + resolve@2.0.0-next.5:
5946 + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
5947 + hasBin: true
5948 +
5869 responselike@1.0.2: 5949 responselike@1.0.2:
5870 resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} 5950 resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==}
5871 5951
...@@ -6192,6 +6272,13 @@ packages: ...@@ -6192,6 +6272,13 @@ packages:
6192 string.fromcodepoint@0.2.1: 6272 string.fromcodepoint@0.2.1:
6193 resolution: {integrity: sha512-n69H31OnxSGSZyZbgBlvYIXlrMhJQ0dQAX1js1QDhpaUH6zmU3QYlj07bCwCNlPOu3oRXIubGPl2gDGnHsiCqg==} 6273 resolution: {integrity: sha512-n69H31OnxSGSZyZbgBlvYIXlrMhJQ0dQAX1js1QDhpaUH6zmU3QYlj07bCwCNlPOu3oRXIubGPl2gDGnHsiCqg==}
6194 6274
6275 + string.prototype.matchall@4.0.12:
6276 + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==}
6277 + engines: {node: '>= 0.4'}
6278 +
6279 + string.prototype.repeat@1.0.0:
6280 + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==}
6281 +
6195 string.prototype.trim@1.2.10: 6282 string.prototype.trim@1.2.10:
6196 resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} 6283 resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
6197 engines: {node: '>= 0.4'} 6284 engines: {node: '>= 0.4'}
...@@ -6630,6 +6717,18 @@ packages: ...@@ -6630,6 +6717,18 @@ packages:
6630 engines: {node: '>=6.0'} 6717 engines: {node: '>=6.0'}
6631 hasBin: true 6718 hasBin: true
6632 6719
6720 + vue-eslint-parser@8.3.0:
6721 + resolution: {integrity: sha512-dzHGG3+sYwSf6zFBa0Gi9ZDshD7+ad14DGOdTLjruRVgZXe2J+DcZ9iUhyR48z5g1PqRa20yt3Njna/veLJL/g==}
6722 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
6723 + peerDependencies:
6724 + eslint: '>=6.0.0'
6725 +
6726 + vue-eslint-parser@9.4.3:
6727 + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==}
6728 + engines: {node: ^14.17.0 || >=16.0.0}
6729 + peerDependencies:
6730 + eslint: '>=6.0.0'
6731 +
6633 vue-loader@17.4.2: 6732 vue-loader@17.4.2:
6634 resolution: {integrity: sha512-yTKOA4R/VN4jqjw4y5HrynFL8AK0Z3/Jt7eOJXEitsm0GMRHDBjCfCiuTiLP7OESvsZYo2pATCWhDqxC5ZrM6w==} 6733 resolution: {integrity: sha512-yTKOA4R/VN4jqjw4y5HrynFL8AK0Z3/Jt7eOJXEitsm0GMRHDBjCfCiuTiLP7OESvsZYo2pATCWhDqxC5ZrM6w==}
6635 peerDependencies: 6734 peerDependencies:
...@@ -8220,6 +8319,8 @@ snapshots: ...@@ -8220,6 +8319,8 @@ snapshots:
8220 8319
8221 '@nutui/icons-vue-taro@0.0.9': {} 8320 '@nutui/icons-vue-taro@0.0.9': {}
8222 8321
8322 + '@nutui/icons-vue@0.1.1': {}
8323 +
8223 '@nutui/nutui-taro@4.3.14(unplugin-vue-components@0.26.0(@babel/parser@7.28.6)(rollup@3.29.5)(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))': 8324 '@nutui/nutui-taro@4.3.14(unplugin-vue-components@0.26.0(@babel/parser@7.28.6)(rollup@3.29.5)(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))':
8224 dependencies: 8325 dependencies:
8225 '@nutui/icons-vue-taro': 0.0.9 8326 '@nutui/icons-vue-taro': 0.0.9
...@@ -9432,6 +9533,15 @@ snapshots: ...@@ -9432,6 +9533,15 @@ snapshots:
9432 9533
9433 array-union@2.1.0: {} 9534 array-union@2.1.0: {}
9434 9535
9536 + array.prototype.findlast@1.2.5:
9537 + dependencies:
9538 + call-bind: 1.0.8
9539 + define-properties: 1.2.1
9540 + es-abstract: 1.24.1
9541 + es-errors: 1.3.0
9542 + es-object-atoms: 1.1.1
9543 + es-shim-unscopables: 1.1.0
9544 +
9435 array.prototype.findlastindex@1.2.6: 9545 array.prototype.findlastindex@1.2.6:
9436 dependencies: 9546 dependencies:
9437 call-bind: 1.0.8 9547 call-bind: 1.0.8
...@@ -9456,6 +9566,14 @@ snapshots: ...@@ -9456,6 +9566,14 @@ snapshots:
9456 es-abstract: 1.24.1 9566 es-abstract: 1.24.1
9457 es-shim-unscopables: 1.1.0 9567 es-shim-unscopables: 1.1.0
9458 9568
9569 + array.prototype.tosorted@1.1.4:
9570 + dependencies:
9571 + call-bind: 1.0.8
9572 + define-properties: 1.2.1
9573 + es-abstract: 1.24.1
9574 + es-errors: 1.3.0
9575 + es-shim-unscopables: 1.1.0
9576 +
9459 arraybuffer.prototype.slice@1.0.4: 9577 arraybuffer.prototype.slice@1.0.4:
9460 dependencies: 9578 dependencies:
9461 array-buffer-byte-length: 1.0.2 9579 array-buffer-byte-length: 1.0.2
...@@ -10513,6 +10631,25 @@ snapshots: ...@@ -10513,6 +10631,25 @@ snapshots:
10513 10631
10514 es-errors@1.3.0: {} 10632 es-errors@1.3.0: {}
10515 10633
10634 + es-iterator-helpers@1.2.2:
10635 + dependencies:
10636 + call-bind: 1.0.8
10637 + call-bound: 1.0.4
10638 + define-properties: 1.2.1
10639 + es-abstract: 1.24.1
10640 + es-errors: 1.3.0
10641 + es-set-tostringtag: 2.1.0
10642 + function-bind: 1.1.2
10643 + get-intrinsic: 1.3.0
10644 + globalthis: 1.0.4
10645 + gopd: 1.2.0
10646 + has-property-descriptors: 1.0.2
10647 + has-proto: 1.2.0
10648 + has-symbols: 1.1.0
10649 + internal-slot: 1.1.0
10650 + iterator.prototype: 1.1.5
10651 + safe-array-concat: 1.1.3
10652 +
10516 es-module-lexer@0.10.5: {} 10653 es-module-lexer@0.10.5: {}
10517 10654
10518 es-module-lexer@1.7.0: {} 10655 es-module-lexer@1.7.0: {}
...@@ -10609,13 +10746,17 @@ snapshots: ...@@ -10609,13 +10746,17 @@ snapshots:
10609 10746
10610 escape-string-regexp@4.0.0: {} 10747 escape-string-regexp@4.0.0: {}
10611 10748
10612 - eslint-config-taro@4.1.9(@babel/core@7.28.6)(eslint@8.57.1)(typescript@5.9.3): 10749 + eslint-config-taro@4.1.9(@babel/core@7.28.6)(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint-plugin-vue@8.7.1(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3):
10613 dependencies: 10750 dependencies:
10614 '@babel/eslint-parser': 7.28.6(@babel/core@7.28.6)(eslint@8.57.1) 10751 '@babel/eslint-parser': 7.28.6(@babel/core@7.28.6)(eslint@8.57.1)
10615 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) 10752 '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
10616 '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) 10753 '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3)
10617 eslint: 8.57.1 10754 eslint: 8.57.1
10618 eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1) 10755 eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)
10756 + optionalDependencies:
10757 + eslint-plugin-react: 7.37.5(eslint@8.57.1)
10758 + eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1)
10759 + eslint-plugin-vue: 8.7.1(eslint@8.57.1)
10619 transitivePeerDependencies: 10760 transitivePeerDependencies:
10620 - '@babel/core' 10761 - '@babel/core'
10621 - eslint-import-resolver-typescript 10762 - eslint-import-resolver-typescript
...@@ -10670,6 +10811,44 @@ snapshots: ...@@ -10670,6 +10811,44 @@ snapshots:
10670 - eslint-import-resolver-webpack 10811 - eslint-import-resolver-webpack
10671 - supports-color 10812 - supports-color
10672 10813
10814 + eslint-plugin-react-hooks@4.6.2(eslint@8.57.1):
10815 + dependencies:
10816 + eslint: 8.57.1
10817 +
10818 + eslint-plugin-react@7.37.5(eslint@8.57.1):
10819 + dependencies:
10820 + array-includes: 3.1.9
10821 + array.prototype.findlast: 1.2.5
10822 + array.prototype.flatmap: 1.3.3
10823 + array.prototype.tosorted: 1.1.4
10824 + doctrine: 2.1.0
10825 + es-iterator-helpers: 1.2.2
10826 + eslint: 8.57.1
10827 + estraverse: 5.3.0
10828 + hasown: 2.0.2
10829 + jsx-ast-utils: 3.3.5
10830 + minimatch: 3.1.2
10831 + object.entries: 1.1.9
10832 + object.fromentries: 2.0.8
10833 + object.values: 1.2.1
10834 + prop-types: 15.8.1
10835 + resolve: 2.0.0-next.5
10836 + semver: 6.3.1
10837 + string.prototype.matchall: 4.0.12
10838 + string.prototype.repeat: 1.0.0
10839 +
10840 + eslint-plugin-vue@8.7.1(eslint@8.57.1):
10841 + dependencies:
10842 + eslint: 8.57.1
10843 + eslint-utils: 3.0.0(eslint@8.57.1)
10844 + natural-compare: 1.4.0
10845 + nth-check: 2.1.1
10846 + postcss-selector-parser: 6.1.2
10847 + semver: 7.7.3
10848 + vue-eslint-parser: 8.3.0(eslint@8.57.1)
10849 + transitivePeerDependencies:
10850 + - supports-color
10851 +
10673 eslint-scope@5.1.1: 10852 eslint-scope@5.1.1:
10674 dependencies: 10853 dependencies:
10675 esrecurse: 4.3.0 10854 esrecurse: 4.3.0
...@@ -10680,6 +10859,11 @@ snapshots: ...@@ -10680,6 +10859,11 @@ snapshots:
10680 esrecurse: 4.3.0 10859 esrecurse: 4.3.0
10681 estraverse: 5.3.0 10860 estraverse: 5.3.0
10682 10861
10862 + eslint-utils@3.0.0(eslint@8.57.1):
10863 + dependencies:
10864 + eslint: 8.57.1
10865 + eslint-visitor-keys: 2.1.0
10866 +
10683 eslint-visitor-keys@2.1.0: {} 10867 eslint-visitor-keys@2.1.0: {}
10684 10868
10685 eslint-visitor-keys@3.4.3: {} 10869 eslint-visitor-keys@3.4.3: {}
...@@ -11648,6 +11832,15 @@ snapshots: ...@@ -11648,6 +11832,15 @@ snapshots:
11648 has-to-string-tag-x: 1.4.1 11832 has-to-string-tag-x: 1.4.1
11649 is-object: 1.0.2 11833 is-object: 1.0.2
11650 11834
11835 + iterator.prototype@1.1.5:
11836 + dependencies:
11837 + define-data-property: 1.1.4
11838 + es-object-atoms: 1.1.1
11839 + get-intrinsic: 1.3.0
11840 + get-proto: 1.0.1
11841 + has-symbols: 1.1.0
11842 + set-function-name: 2.0.2
11843 +
11651 j-component@1.4.9: 11844 j-component@1.4.9:
11652 dependencies: 11845 dependencies:
11653 expr-parser: 1.0.0 11846 expr-parser: 1.0.0
...@@ -11770,6 +11963,13 @@ snapshots: ...@@ -11770,6 +11963,13 @@ snapshots:
11770 dependencies: 11963 dependencies:
11771 object-assign: 4.1.1 11964 object-assign: 4.1.1
11772 11965
11966 + jsx-ast-utils@3.3.5:
11967 + dependencies:
11968 + array-includes: 3.1.9
11969 + array.prototype.flat: 1.3.3
11970 + object.assign: 4.1.7
11971 + object.values: 1.2.1
11972 +
11773 keyv@3.0.0: 11973 keyv@3.0.0:
11774 dependencies: 11974 dependencies:
11775 json-buffer: 3.0.0 11975 json-buffer: 3.0.0
...@@ -11997,6 +12197,10 @@ snapshots: ...@@ -11997,6 +12197,10 @@ snapshots:
11997 12197
11998 loglevel@1.9.2: {} 12198 loglevel@1.9.2: {}
11999 12199
12200 + loose-envify@1.4.0:
12201 + dependencies:
12202 + js-tokens: 4.0.0
12203 +
12000 lower-case@1.1.4: {} 12204 lower-case@1.1.4: {}
12001 12205
12002 lower-case@2.0.2: 12206 lower-case@2.0.2:
...@@ -12246,6 +12450,13 @@ snapshots: ...@@ -12246,6 +12450,13 @@ snapshots:
12246 has-symbols: 1.1.0 12450 has-symbols: 1.1.0
12247 object-keys: 1.1.1 12451 object-keys: 1.1.1
12248 12452
12453 + object.entries@1.1.9:
12454 + dependencies:
12455 + call-bind: 1.0.8
12456 + call-bound: 1.0.4
12457 + define-properties: 1.2.1
12458 + es-object-atoms: 1.1.1
12459 +
12249 object.fromentries@2.0.8: 12460 object.fromentries@2.0.8:
12250 dependencies: 12461 dependencies:
12251 call-bind: 1.0.8 12462 call-bind: 1.0.8
...@@ -13031,6 +13242,12 @@ snapshots: ...@@ -13031,6 +13242,12 @@ snapshots:
13031 13242
13032 promise-polyfill@7.1.2: {} 13243 promise-polyfill@7.1.2: {}
13033 13244
13245 + prop-types@15.8.1:
13246 + dependencies:
13247 + loose-envify: 1.4.0
13248 + object-assign: 4.1.1
13249 + react-is: 16.13.1
13250 +
13034 property-expr@2.0.6: {} 13251 property-expr@2.0.6: {}
13035 13252
13036 proto-list@1.2.4: {} 13253 proto-list@1.2.4: {}
...@@ -13109,6 +13326,8 @@ snapshots: ...@@ -13109,6 +13326,8 @@ snapshots:
13109 minimist: 1.2.8 13326 minimist: 1.2.8
13110 strip-json-comments: 2.0.1 13327 strip-json-comments: 2.0.1
13111 13328
13329 + react-is@16.13.1: {}
13330 +
13112 react-is@17.0.2: {} 13331 react-is@17.0.2: {}
13113 13332
13114 react@19.2.4: {} 13333 react@19.2.4: {}
...@@ -13232,6 +13451,12 @@ snapshots: ...@@ -13232,6 +13451,12 @@ snapshots:
13232 path-parse: 1.0.7 13451 path-parse: 1.0.7
13233 supports-preserve-symlinks-flag: 1.0.0 13452 supports-preserve-symlinks-flag: 1.0.0
13234 13453
13454 + resolve@2.0.0-next.5:
13455 + dependencies:
13456 + is-core-module: 2.16.1
13457 + path-parse: 1.0.7
13458 + supports-preserve-symlinks-flag: 1.0.0
13459 +
13235 responselike@1.0.2: 13460 responselike@1.0.2:
13236 dependencies: 13461 dependencies:
13237 lowercase-keys: 1.0.1 13462 lowercase-keys: 1.0.1
...@@ -13601,6 +13826,27 @@ snapshots: ...@@ -13601,6 +13826,27 @@ snapshots:
13601 13826
13602 string.fromcodepoint@0.2.1: {} 13827 string.fromcodepoint@0.2.1: {}
13603 13828
13829 + string.prototype.matchall@4.0.12:
13830 + dependencies:
13831 + call-bind: 1.0.8
13832 + call-bound: 1.0.4
13833 + define-properties: 1.2.1
13834 + es-abstract: 1.24.1
13835 + es-errors: 1.3.0
13836 + es-object-atoms: 1.1.1
13837 + get-intrinsic: 1.3.0
13838 + gopd: 1.2.0
13839 + has-symbols: 1.1.0
13840 + internal-slot: 1.1.0
13841 + regexp.prototype.flags: 1.5.4
13842 + set-function-name: 2.0.2
13843 + side-channel: 1.1.0
13844 +
13845 + string.prototype.repeat@1.0.0:
13846 + dependencies:
13847 + define-properties: 1.2.1
13848 + es-abstract: 1.24.1
13849 +
13604 string.prototype.trim@1.2.10: 13850 string.prototype.trim@1.2.10:
13605 dependencies: 13851 dependencies:
13606 call-bind: 1.0.8 13852 call-bind: 1.0.8
...@@ -14074,6 +14320,32 @@ snapshots: ...@@ -14074,6 +14320,32 @@ snapshots:
14074 acorn: 8.15.0 14320 acorn: 8.15.0
14075 acorn-walk: 8.3.4 14321 acorn-walk: 8.3.4
14076 14322
14323 + vue-eslint-parser@8.3.0(eslint@8.57.1):
14324 + dependencies:
14325 + debug: 4.4.3
14326 + eslint: 8.57.1
14327 + eslint-scope: 7.2.2
14328 + eslint-visitor-keys: 3.4.3
14329 + espree: 9.6.1
14330 + esquery: 1.7.0
14331 + lodash: 4.17.23
14332 + semver: 7.7.3
14333 + transitivePeerDependencies:
14334 + - supports-color
14335 +
14336 + vue-eslint-parser@9.4.3(eslint@8.57.1):
14337 + dependencies:
14338 + debug: 4.4.3
14339 + eslint: 8.57.1
14340 + eslint-scope: 7.2.2
14341 + eslint-visitor-keys: 3.4.3
14342 + espree: 9.6.1
14343 + esquery: 1.7.0
14344 + lodash: 4.17.23
14345 + semver: 7.7.3
14346 + transitivePeerDependencies:
14347 + - supports-color
14348 +
14077 vue-loader@17.4.2(@vue/compiler-sfc@3.5.27)(vue@3.5.27(typescript@5.9.3))(webpack@5.91.0(@swc/core@1.3.96)): 14349 vue-loader@17.4.2(@vue/compiler-sfc@3.5.27)(vue@3.5.27(typescript@5.9.3))(webpack@5.91.0(@swc/core@1.3.96)):
14078 dependencies: 14350 dependencies:
14079 chalk: 4.1.2 14351 chalk: 4.1.2
......
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>