af-mobile-client-vue3 1.6.48 → 1.6.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +1 -1
- package/package.json +1 -1
- package/src/components/data/FilePreview/components/KkPreview.vue +79 -0
- package/src/components/data/FilePreview/components/MediaPreview.vue +64 -0
- package/src/components/data/FilePreview/components/OfficePreview.vue +65 -0
- package/src/components/data/FilePreview/components/TextPreview.vue +27 -0
- package/src/components/data/FilePreview/index.vue +98 -202
- package/src/components/data/OtherCharge/OtherChargeGroupModal.vue +542 -542
- package/src/components/data/step/person-select.vue +168 -168
- package/src/stores/modules/step.ts +88 -88
- package/src/views/component/FilePreviewView/index.vue +5 -3
- package/src/views/component/StepView/index.vue +1 -1
package/.env
CHANGED
|
@@ -5,7 +5,7 @@ VITE_APP_OUT_DIR=dist_af-library-mobile-web
|
|
|
5
5
|
VITE_APP_VCONSOLE=false
|
|
6
6
|
# 自定义参数
|
|
7
7
|
VITE_APP_WEB_CONFIG_KEY=admin.webconfig
|
|
8
|
-
VITE_APP_SYSTEM_NAME=af-
|
|
8
|
+
VITE_APP_SYSTEM_NAME=af-system
|
|
9
9
|
# 最低兼容性 V4(最新产品)V3(V3产品) OA(公司OA)
|
|
10
10
|
VITE_APP_COMPATIBLE=V4
|
|
11
11
|
VITE_RSA_PRIVATE_KEY=MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIRrmftLDHCQqREEJ132Onu+W3vmFbdF7QD751SrcDDGDTfzuz1zBuElvkHhuDBb7KZkXrCIe+MhvX2IvxcLObl3faX+evYlnfj2HRbF0hIpQLuIq22tL06ZcV5w7wqLxUZRpFElIFm8gZTkUvfKXVuHw89e4daDVhU5hK3GHNGTAgMBAAECgYABiINrFaE1E8pkBYx1JJA5yuhL73aUktfd2TeCU00vFg6kyrWCI85Sa2RKu/6CJNZWeOFgdubEUv7a22tRrNIZb3yUMaqtTwSso78mspIOJqjWXTkTH9WPElfTcdpdIse/lgZtPz6egxkuhadSvwrM9Y6NgusiW/5+x95Ct08iOQJBAN5aK+7uISURvGQj2EaRtgGEd8+d4oHl+BYvvTeG3qSgUikHQW3j0sp4gXPw2kxw6sjVgLFOc4FB6LGqwzOTzokCQQCYdYG8ty3Uo/ebUlNzeJFxHXjy/KvBSytAUzAXkRu3nZrkEaPQsi3dgOkZgk+F1fMDzfQ4EbDIU6xvqOoZXHg7AkATCW9XfoXR8anKfRMoP5Nwn9HOMbtR2cmaxK2TknV/bMZ8AsYETYwfj5+tuIJIJybC2RyykX/sIiN1CqS5xr7ZAkArj19rMRdaKyMi8MnBM1Cy9g3Jt2HHj5ejAGG8SgyWUOShh1y70z0BjcSMMkxQXAncK2s83ekZw7aADM4eQupjAkARRgTwwMOnn3IoKmQusKhZk0uxilZ4Zc2LH6Z4GiWnvteM0W8Zw4Z1lJUcjgQq3dGqL2RdmzeQZ+HgPIOXrZVK
|
package/package.json
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* kkFileView 预览子组件
|
|
4
|
+
* 通过 iframe 加载 kkFileView 服务端预览页面
|
|
5
|
+
* 适用于 office 渲染器无法处理的格式(如 .doc / .xls 旧格式)或 office 渲染失败的降级场景
|
|
6
|
+
*/
|
|
7
|
+
import { getConfigByNameAsync } from '@af-mobile-client-vue3/services/api/common'
|
|
8
|
+
import { Base64 } from 'js-base64'
|
|
9
|
+
import { Loading as VanLoading } from 'vant'
|
|
10
|
+
import { onMounted, ref } from 'vue'
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
/** 文件原始路径(相对路径或完整 URL) */
|
|
14
|
+
filePath: string
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const iframeUrl = ref('')
|
|
18
|
+
const iframeLoading = ref(true)
|
|
19
|
+
|
|
20
|
+
function isHttp(path: string) {
|
|
21
|
+
return /^https?:\/\//i.test(path)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 读取 kkFileView 服务配置,拼接预览 URL */
|
|
25
|
+
async function buildPreviewUrl() {
|
|
26
|
+
try {
|
|
27
|
+
const res: any = await getConfigByNameAsync('previewDocServiceConfig')
|
|
28
|
+
if (!res?.previewDocService || !res?.fileServer)
|
|
29
|
+
throw new Error('previewDocServiceConfig 配置缺失')
|
|
30
|
+
|
|
31
|
+
const sourceUrl = isHttp(props.filePath)
|
|
32
|
+
? props.filePath
|
|
33
|
+
: `${res.fileServer}${props.filePath}`
|
|
34
|
+
|
|
35
|
+
// kkFileView 要求 url 参数为 Base64 编码后再 encodeURIComponent
|
|
36
|
+
iframeUrl.value = `${res.previewDocService}${encodeURIComponent(Base64.encode(sourceUrl))}`
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
console.warn('[KkPreview] 配置加载失败,降级直接加载原始地址', e)
|
|
40
|
+
// 服务未配置时,直接把原始地址塞给 iframe
|
|
41
|
+
iframeUrl.value = isHttp(props.filePath) ? props.filePath : ''
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onMounted(buildPreviewUrl)
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<div class="preview-doc-container">
|
|
50
|
+
<VanLoading v-if="iframeLoading" class="kk-loading" type="spinner" vertical>
|
|
51
|
+
加载中...
|
|
52
|
+
</VanLoading>
|
|
53
|
+
<iframe
|
|
54
|
+
v-if="iframeUrl"
|
|
55
|
+
v-show="!iframeLoading"
|
|
56
|
+
:src="iframeUrl"
|
|
57
|
+
width="100%"
|
|
58
|
+
height="100%"
|
|
59
|
+
frameborder="0"
|
|
60
|
+
@load="iframeLoading = false"
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<style scoped>
|
|
66
|
+
.preview-doc-container {
|
|
67
|
+
height: 100%;
|
|
68
|
+
position: relative;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.kk-loading {
|
|
72
|
+
position: absolute;
|
|
73
|
+
inset: 0;
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
justify-content: center;
|
|
77
|
+
z-index: 1;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 媒体预览子组件
|
|
4
|
+
* 处理 image / video / audio 三种类型,直接用原生 HTML 标签渲染
|
|
5
|
+
*/
|
|
6
|
+
import type { PreviewFileKind } from '@af-mobile-client-vue3/utils/fileType'
|
|
7
|
+
|
|
8
|
+
defineProps<{
|
|
9
|
+
kind: PreviewFileKind
|
|
10
|
+
src: string
|
|
11
|
+
}>()
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div v-if="kind === 'image'" class="media-wrapper">
|
|
16
|
+
<img :src="src" alt="预览图片" class="media-image">
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div v-else-if="kind === 'video'" class="media-wrapper media-dark">
|
|
20
|
+
<video class="media-video" controls :src="src" />
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div v-else-if="kind === 'audio'" class="media-audio-wrapper">
|
|
24
|
+
<audio class="media-audio" controls :src="src" />
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<style scoped>
|
|
29
|
+
.media-wrapper {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
flex: 1;
|
|
34
|
+
min-height: 200px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.media-dark {
|
|
38
|
+
background: #000;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.media-image {
|
|
42
|
+
max-width: 100%;
|
|
43
|
+
max-height: 100%;
|
|
44
|
+
object-fit: contain;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.media-video {
|
|
48
|
+
width: 100%;
|
|
49
|
+
max-height: 100%;
|
|
50
|
+
border: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.media-audio-wrapper {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
padding: 32px 16px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.media-audio {
|
|
61
|
+
width: 100%;
|
|
62
|
+
max-width: 480px;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Office 文档预览子组件
|
|
4
|
+
* 使用 @vue-office 系列库渲染 pdf / docx / xlsx / xls / doc
|
|
5
|
+
* 渲染失败时 emit 'error',由父组件决定是否降级到 kk 预览
|
|
6
|
+
*/
|
|
7
|
+
import type { PreviewFileKind } from '@af-mobile-client-vue3/utils/fileType'
|
|
8
|
+
import { defineAsyncComponent } from 'vue'
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
kind: PreviewFileKind
|
|
12
|
+
src: string | ArrayBuffer
|
|
13
|
+
}>()
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits<{
|
|
16
|
+
(e: 'error'): void
|
|
17
|
+
}>()
|
|
18
|
+
|
|
19
|
+
// 按需异步加载,避免影响首屏
|
|
20
|
+
const VueOfficePdf = defineAsyncComponent(() =>
|
|
21
|
+
import('@vue-office/pdf/lib/v3/vue-office-pdf.mjs').then(m => m.default),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const VueOfficeDocx = defineAsyncComponent(async () => {
|
|
25
|
+
await import('@vue-office/docx/lib/v3/index.css')
|
|
26
|
+
return import('@vue-office/docx/lib/v3/vue-office-docx.mjs').then(m => m.default)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const VueOfficeExcel = defineAsyncComponent(async () => {
|
|
30
|
+
await import('@vue-office/excel/lib/v3/index.css')
|
|
31
|
+
return import('@vue-office/excel/lib/v3/vue-office-excel.mjs').then(m => m.default)
|
|
32
|
+
})
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<VueOfficePdf
|
|
37
|
+
v-if="kind === 'pdf'"
|
|
38
|
+
:src="src"
|
|
39
|
+
class="office-viewer"
|
|
40
|
+
@error="emit('error')"
|
|
41
|
+
/>
|
|
42
|
+
<!-- docx / doc 共用 docx 渲染器;.doc 若解析失败会触发 error 降级到 kk -->
|
|
43
|
+
<VueOfficeDocx
|
|
44
|
+
v-else-if="kind === 'docx' || kind === 'doc'"
|
|
45
|
+
:src="src"
|
|
46
|
+
class="office-viewer"
|
|
47
|
+
@error="emit('error')"
|
|
48
|
+
/>
|
|
49
|
+
<!-- xlsx / xls 共用 excel 渲染器 -->
|
|
50
|
+
<VueOfficeExcel
|
|
51
|
+
v-else-if="kind === 'xlsx' || kind === 'xls'"
|
|
52
|
+
:src="src"
|
|
53
|
+
class="office-viewer"
|
|
54
|
+
@error="emit('error')"
|
|
55
|
+
/>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<style scoped>
|
|
59
|
+
.office-viewer {
|
|
60
|
+
width: 100%;
|
|
61
|
+
flex: 1;
|
|
62
|
+
min-height: 200px;
|
|
63
|
+
overflow: auto;
|
|
64
|
+
}
|
|
65
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 纯文本预览子组件
|
|
4
|
+
* 直接渲染文本内容,保留空白和换行
|
|
5
|
+
*/
|
|
6
|
+
defineProps<{
|
|
7
|
+
content: string
|
|
8
|
+
}>()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<pre class="text-preview">{{ content }}</pre>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<style scoped>
|
|
16
|
+
.text-preview {
|
|
17
|
+
margin: 0;
|
|
18
|
+
padding: 12px;
|
|
19
|
+
white-space: pre-wrap;
|
|
20
|
+
word-break: break-word;
|
|
21
|
+
font-size: 13px;
|
|
22
|
+
line-height: 1.5;
|
|
23
|
+
color: #333;
|
|
24
|
+
overflow: auto;
|
|
25
|
+
flex: 1;
|
|
26
|
+
}
|
|
27
|
+
</style>
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 文件预览组件(主入口)
|
|
4
|
+
*
|
|
5
|
+
* 渲染策略:
|
|
6
|
+
* 1. image / video / audio → MediaPreview(原生标签)
|
|
7
|
+
* 2. text → TextPreview(<pre>)
|
|
8
|
+
* 3. pdf / docx / doc / xlsx / xls → OfficePreview(@vue-office)
|
|
9
|
+
* └─ @vue-office 渲染失败 → 自动降级到 KkPreview(kkFileView iframe)
|
|
10
|
+
* 4. doc 旧格式 / unsupported → 直接走 KkPreview
|
|
11
|
+
*/
|
|
2
12
|
import type { PreviewFileKind } from '@af-mobile-client-vue3/utils/fileType'
|
|
3
13
|
import { downloadAttachment } from '@af-mobile-client-vue3/utils/fileDownload'
|
|
4
14
|
import { getPreviewFileKind } from '@af-mobile-client-vue3/utils/fileType'
|
|
@@ -9,96 +19,78 @@ import {
|
|
|
9
19
|
resolveResourceUrl,
|
|
10
20
|
} from '@af-mobile-client-vue3/utils/resourceUrl'
|
|
11
21
|
import { Button as VanButton, Empty as VanEmpty, Loading as VanLoading } from 'vant'
|
|
12
|
-
import {
|
|
22
|
+
import { ref, shallowRef } from 'vue'
|
|
23
|
+
import KkPreview from './components/KkPreview.vue'
|
|
24
|
+
import MediaPreview from './components/MediaPreview.vue'
|
|
25
|
+
import OfficePreview from './components/OfficePreview.vue'
|
|
26
|
+
import TextPreview from './components/TextPreview.vue'
|
|
13
27
|
|
|
14
28
|
export interface FilePreviewInitOptions {
|
|
15
29
|
/** 后端 f_downloadpath 或完整 URL */
|
|
16
30
|
path: string
|
|
17
|
-
/**
|
|
31
|
+
/** 下载时使用的文件名(可选,默认从路径提取) */
|
|
18
32
|
name?: string
|
|
19
33
|
}
|
|
20
34
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return mod.default
|
|
24
|
-
})
|
|
35
|
+
/** office 类型集合,这些类型优先走 @vue-office,失败降级 kk */
|
|
36
|
+
const OFFICE_KINDS: PreviewFileKind[] = ['pdf', 'docx', 'xlsx']
|
|
25
37
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
/**
|
|
39
|
+
* 直接走 kk 预览的格式,不尝试 @vue-office:
|
|
40
|
+
* - doc 旧二进制格式,@vue-office 无法解析
|
|
41
|
+
* - xls 旧二进制格式,@vue-office 解析效果差
|
|
42
|
+
* - unsupported 未知格式,交给 kkFileView 服务端判断
|
|
43
|
+
*/
|
|
44
|
+
const KK_ONLY_KINDS: PreviewFileKind[] = ['doc', 'xls', 'unsupported']
|
|
31
45
|
|
|
32
|
-
const
|
|
33
|
-
await import('@vue-office/excel/lib/v3/index.css')
|
|
34
|
-
const mod = await import('@vue-office/excel/lib/v3/vue-office-excel.mjs')
|
|
35
|
-
return mod.default
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
const previewKind = ref<PreviewFileKind>('unsupported')
|
|
46
|
+
const kind = ref<PreviewFileKind>('unsupported')
|
|
39
47
|
const fileSource = shallowRef<string | ArrayBuffer>('')
|
|
40
48
|
const textContent = ref('')
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const openUrl = ref('')
|
|
49
|
+
const mediaSrc = ref('')
|
|
50
|
+
const rawUrl = ref('') // 文件原始 URL(kk 预览和下载共用)
|
|
51
|
+
const rawPath = ref('') // 文件相对路径(kk 预览用)
|
|
45
52
|
const downloadName = ref('')
|
|
46
53
|
const loading = ref(false)
|
|
47
54
|
const errorMsg = ref('')
|
|
55
|
+
/** true = office 渲染失败,已降级到 kk 预览 */
|
|
56
|
+
const officeFailed = ref(false)
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
/** office 渲染失败回调,切换到 kk 预览 */
|
|
59
|
+
function onOfficeFailed() {
|
|
60
|
+
officeFailed.value = true
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
async function init(options: FilePreviewInitOptions) {
|
|
54
64
|
const path = getPreviewPath(options.path)
|
|
55
65
|
if (!path) {
|
|
56
66
|
errorMsg.value = '文件路径为空'
|
|
57
|
-
previewKind.value = 'unsupported'
|
|
58
67
|
return
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
|
|
70
|
+
// 重置所有状态
|
|
71
|
+
kind.value = getPreviewFileKind(path)
|
|
72
|
+
rawUrl.value = resolveResourceUrl(path)
|
|
73
|
+
rawPath.value = path
|
|
62
74
|
downloadName.value = options.name || path.split('/').pop()?.split('?')[0] || '附件'
|
|
63
|
-
previewKind.value = getPreviewFileKind(path)
|
|
64
75
|
loading.value = true
|
|
65
76
|
errorMsg.value = ''
|
|
66
|
-
|
|
77
|
+
officeFailed.value = false
|
|
67
78
|
fileSource.value = ''
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
audioSrc.value = ''
|
|
79
|
+
textContent.value = ''
|
|
80
|
+
mediaSrc.value = ''
|
|
71
81
|
|
|
72
82
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
case 'audio':
|
|
83
|
-
audioSrc.value = openUrl.value
|
|
84
|
-
break
|
|
85
|
-
|
|
86
|
-
case 'text':
|
|
87
|
-
textContent.value = await fetchResourceText(openUrl.value)
|
|
88
|
-
break
|
|
89
|
-
|
|
90
|
-
case 'pdf':
|
|
91
|
-
case 'docx':
|
|
92
|
-
case 'doc':
|
|
93
|
-
case 'xlsx':
|
|
94
|
-
case 'xls':
|
|
95
|
-
fileSource.value = await fetchResourceArrayBuffer(openUrl.value)
|
|
96
|
-
break
|
|
97
|
-
|
|
98
|
-
case 'unsupported':
|
|
99
|
-
// 未知格式,直接用 iframe 尝试打开,无需预加载内容
|
|
100
|
-
break
|
|
83
|
+
if (kind.value === 'image' || kind.value === 'video' || kind.value === 'audio') {
|
|
84
|
+
mediaSrc.value = rawUrl.value
|
|
85
|
+
}
|
|
86
|
+
else if (kind.value === 'text') {
|
|
87
|
+
textContent.value = await fetchResourceText(rawUrl.value)
|
|
88
|
+
}
|
|
89
|
+
else if (OFFICE_KINDS.includes(kind.value)) {
|
|
90
|
+
// 预加载文件二进制,交给 @vue-office 渲染
|
|
91
|
+
fileSource.value = await fetchResourceArrayBuffer(rawUrl.value)
|
|
101
92
|
}
|
|
93
|
+
// doc / unsupported 直接走 KkPreview,无需预加载
|
|
102
94
|
}
|
|
103
95
|
catch (e: any) {
|
|
104
96
|
errorMsg.value = e?.message || '文件加载失败'
|
|
@@ -109,85 +101,67 @@ async function init(options: FilePreviewInitOptions) {
|
|
|
109
101
|
}
|
|
110
102
|
|
|
111
103
|
function handleDownload() {
|
|
112
|
-
|
|
113
|
-
return
|
|
114
|
-
downloadAttachment({ url: openUrl.value, name: downloadName.value })
|
|
104
|
+
downloadAttachment({ url: rawUrl.value, name: downloadName.value })
|
|
115
105
|
}
|
|
116
106
|
|
|
117
|
-
defineExpose({
|
|
118
|
-
init,
|
|
119
|
-
download: handleDownload,
|
|
120
|
-
})
|
|
107
|
+
defineExpose({ init, download: handleDownload })
|
|
121
108
|
</script>
|
|
122
109
|
|
|
123
110
|
<template>
|
|
124
111
|
<div class="file-preview-root">
|
|
125
|
-
|
|
112
|
+
<!-- 悬浮下载按钮,定位在预览区右上角 -->
|
|
113
|
+
<div v-if="rawUrl && !loading" class="preview-download-btn">
|
|
126
114
|
<VanButton size="small" type="primary" plain icon="down" @click="handleDownload">
|
|
127
115
|
下载
|
|
128
116
|
</VanButton>
|
|
129
117
|
</div>
|
|
130
118
|
|
|
119
|
+
<!-- 加载中 -->
|
|
131
120
|
<VanLoading v-if="loading" class="preview-loading" type="spinner" vertical>
|
|
132
121
|
加载中...
|
|
133
122
|
</VanLoading>
|
|
134
123
|
|
|
124
|
+
<!-- 加载/解析出错 -->
|
|
135
125
|
<VanEmpty v-else-if="errorMsg" :description="errorMsg">
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
</VanButton>
|
|
140
|
-
</template>
|
|
126
|
+
<VanButton v-if="rawUrl" size="small" type="primary" @click="handleDownload">
|
|
127
|
+
下载
|
|
128
|
+
</VanButton>
|
|
141
129
|
</VanEmpty>
|
|
142
130
|
|
|
143
131
|
<template v-else>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
132
|
+
<!-- 媒体:图片 / 视频 / 音频 -->
|
|
133
|
+
<MediaPreview
|
|
134
|
+
v-if="kind === 'image' || kind === 'video' || kind === 'audio'"
|
|
135
|
+
:kind="kind"
|
|
136
|
+
:src="mediaSrc"
|
|
149
137
|
/>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
@error="onRenderError"
|
|
138
|
+
|
|
139
|
+
<!-- 纯文本 -->
|
|
140
|
+
<TextPreview
|
|
141
|
+
v-else-if="kind === 'text'"
|
|
142
|
+
:content="textContent"
|
|
156
143
|
/>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
144
|
+
|
|
145
|
+
<!--
|
|
146
|
+
Office 文档:先走 @vue-office
|
|
147
|
+
失败(officeFailed)或 doc/unsupported 直接走 KkPreview
|
|
148
|
+
-->
|
|
149
|
+
<OfficePreview
|
|
150
|
+
v-else-if="OFFICE_KINDS.includes(kind) && fileSource && !officeFailed"
|
|
151
|
+
:kind="kind"
|
|
160
152
|
:src="fileSource"
|
|
161
|
-
|
|
162
|
-
@error="onRenderError"
|
|
153
|
+
@error="onOfficeFailed"
|
|
163
154
|
/>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
</div>
|
|
173
|
-
<pre v-else-if="previewKind === 'text'" class="preview-text">{{ textContent }}</pre>
|
|
174
|
-
<!-- 未知格式:用 iframe 尝试直接打开,系统/浏览器自行决定如何处理 -->
|
|
175
|
-
<div v-else-if="previewKind === 'unsupported'" class="preview-iframe-wrapper">
|
|
176
|
-
<iframe
|
|
177
|
-
:src="openUrl"
|
|
178
|
-
class="preview-iframe"
|
|
179
|
-
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
|
180
|
-
@error="onRenderError"
|
|
181
|
-
/>
|
|
182
|
-
<div class="preview-iframe-fallback">
|
|
183
|
-
<span>若文件无法显示,请</span>
|
|
184
|
-
<VanButton size="small" type="primary" plain @click="handleDownload">
|
|
185
|
-
下载查看
|
|
186
|
-
</VanButton>
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
155
|
+
|
|
156
|
+
<!-- kk 预览:office 降级 或 doc/unsupported 直接走 -->
|
|
157
|
+
<KkPreview
|
|
158
|
+
v-else-if="officeFailed || KK_ONLY_KINDS.includes(kind)"
|
|
159
|
+
:file-path="rawPath"
|
|
160
|
+
/>
|
|
161
|
+
|
|
162
|
+
<!-- 兜底:路径异常 -->
|
|
189
163
|
<VanEmpty v-else description="文件路径异常">
|
|
190
|
-
<VanButton v-if="
|
|
164
|
+
<VanButton v-if="rawUrl" size="small" type="primary" @click="handleDownload">
|
|
191
165
|
下载
|
|
192
166
|
</VanButton>
|
|
193
167
|
</VanEmpty>
|
|
@@ -200,103 +174,25 @@ defineExpose({
|
|
|
200
174
|
width: 100%;
|
|
201
175
|
height: 100%;
|
|
202
176
|
min-height: 200px;
|
|
203
|
-
position: relative;
|
|
204
|
-
overflow: auto;
|
|
205
|
-
background: #fff;
|
|
206
177
|
display: flex;
|
|
207
178
|
flex-direction: column;
|
|
179
|
+
background: #fff;
|
|
180
|
+
overflow: hidden;
|
|
181
|
+
/* 作为悬浮按钮的定位基准 */
|
|
182
|
+
position: relative;
|
|
208
183
|
}
|
|
209
184
|
|
|
210
|
-
.preview-
|
|
211
|
-
display: flex;
|
|
212
|
-
justify-content: flex-end;
|
|
213
|
-
padding: 8px 12px 0;
|
|
214
|
-
flex-shrink: 0;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
.preview-loading {
|
|
185
|
+
.preview-download-btn {
|
|
218
186
|
position: absolute;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
justify-content: center;
|
|
223
|
-
z-index: 1;
|
|
187
|
+
top: 10px;
|
|
188
|
+
right: 12px;
|
|
189
|
+
z-index: 10;
|
|
224
190
|
}
|
|
225
191
|
|
|
226
|
-
.
|
|
227
|
-
width: 100%;
|
|
228
|
-
min-height: 200px;
|
|
229
|
-
height: 100%;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
.preview-image-wrapper,
|
|
233
|
-
.preview-video-wrapper {
|
|
234
|
-
display: flex;
|
|
235
|
-
align-items: center;
|
|
236
|
-
justify-content: center;
|
|
237
|
-
min-height: 200px;
|
|
238
|
-
height: 100%;
|
|
239
|
-
background: #000;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
.preview-image {
|
|
243
|
-
max-width: 100%;
|
|
244
|
-
max-height: 100%;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
.preview-video {
|
|
248
|
-
width: 100%;
|
|
249
|
-
max-height: 100%;
|
|
250
|
-
border: none;
|
|
251
|
-
background: #000;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
.preview-text {
|
|
255
|
-
margin: 0;
|
|
256
|
-
padding: 12px;
|
|
257
|
-
white-space: pre-wrap;
|
|
258
|
-
word-break: break-word;
|
|
259
|
-
font-size: 13px;
|
|
260
|
-
line-height: 1.5;
|
|
261
|
-
color: #333;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
.preview-audio-wrapper {
|
|
265
|
-
display: flex;
|
|
266
|
-
align-items: center;
|
|
267
|
-
justify-content: center;
|
|
268
|
-
padding: 32px 16px;
|
|
269
|
-
min-height: 120px;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
.preview-audio {
|
|
273
|
-
width: 100%;
|
|
274
|
-
max-width: 480px;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
.preview-iframe-wrapper {
|
|
278
|
-
display: flex;
|
|
279
|
-
flex-direction: column;
|
|
280
|
-
width: 100%;
|
|
281
|
-
height: 100%;
|
|
282
|
-
flex: 1;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
.preview-iframe {
|
|
286
|
-
width: 100%;
|
|
192
|
+
.preview-loading {
|
|
287
193
|
flex: 1;
|
|
288
|
-
min-height: 200px;
|
|
289
|
-
border: none;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
.preview-iframe-fallback {
|
|
293
194
|
display: flex;
|
|
294
195
|
align-items: center;
|
|
295
|
-
|
|
296
|
-
padding: 8px 12px;
|
|
297
|
-
font-size: 12px;
|
|
298
|
-
color: #999;
|
|
299
|
-
flex-shrink: 0;
|
|
300
|
-
border-top: 1px solid #f0f0f0;
|
|
196
|
+
justify-content: center;
|
|
301
197
|
}
|
|
302
198
|
</style>
|