af-mobile-client-vue3 1.6.49 → 1.6.52
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/package.json +1 -1
- package/src/components/core/XMultiSelect/index.vue +54 -45
- package/src/components/core/XSelect/index.vue +18 -1
- package/src/components/data/FilePreview/FilePreviewDialog.vue +257 -0
- package/src/components/data/FilePreview/components/KkPreview.vue +9 -0
- package/src/components/data/FilePreview/components/OfficePreview.vue +24 -7
- package/src/components/data/FilePreview/index.vue +18 -61
- package/src/components/data/XCellList/index.vue +109 -93
- package/src/components/data/step/person-select.vue +186 -168
- package/src/router/routes.ts +6 -0
- package/src/utils/environment.ts +0 -1
- package/src/utils/fileDownload.ts +57 -27
- package/src/views/component/FilePreviewView/FilePreviewDialogView.vue +93 -0
- package/src/views/component/FilePreviewView/index.vue +60 -8
- package/src/views/component/index.vue +4 -0
package/package.json
CHANGED
|
@@ -149,52 +149,54 @@ const resultLabel = computed(() => {
|
|
|
149
149
|
:is-link="$attrs.disabled === undefined"
|
|
150
150
|
@click="showPopu($attrs.readonly)"
|
|
151
151
|
/>
|
|
152
|
-
<VanPopup v-model:show="show" position="bottom">
|
|
153
|
-
<div class="
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
<button type="button" class="van-picker__confirm" @click="onConfirm">
|
|
161
|
-
确认
|
|
162
|
-
</button>
|
|
163
|
-
</div>
|
|
164
|
-
<div class="x-multi-select-content">
|
|
165
|
-
<VanSearch
|
|
166
|
-
v-if="props.isSearch"
|
|
167
|
-
v-model="searchVal"
|
|
168
|
-
placeholder="搜索"
|
|
169
|
-
@update:model-value="search"
|
|
170
|
-
@cancel="search"
|
|
171
|
-
/>
|
|
172
|
-
<div v-if="isLoading" class="loading-container">
|
|
173
|
-
<div class="loading-text">
|
|
174
|
-
搜索中...
|
|
152
|
+
<VanPopup v-model:show="show" position="bottom" teleport="body" round>
|
|
153
|
+
<div class="x-multi-select-popup">
|
|
154
|
+
<div class="van-picker__toolbar">
|
|
155
|
+
<button type="button" class="van-picker__cancel" @click="cancel">
|
|
156
|
+
取消
|
|
157
|
+
</button>
|
|
158
|
+
<div class="van-ellipsis van-picker__title">
|
|
159
|
+
{{ $attrs.label }}
|
|
175
160
|
</div>
|
|
161
|
+
<button type="button" class="van-picker__confirm" @click="onConfirm">
|
|
162
|
+
确认
|
|
163
|
+
</button>
|
|
176
164
|
</div>
|
|
177
|
-
<div
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
165
|
+
<div class="x-multi-select-content">
|
|
166
|
+
<VanSearch
|
|
167
|
+
v-if="props.isSearch"
|
|
168
|
+
v-model="searchVal"
|
|
169
|
+
placeholder="搜索"
|
|
170
|
+
@update:model-value="search"
|
|
171
|
+
@cancel="search"
|
|
172
|
+
/>
|
|
173
|
+
<div v-if="isLoading" class="loading-container">
|
|
174
|
+
<div class="loading-text">
|
|
175
|
+
搜索中...
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div v-else class="x-multi-select-checkbox-group">
|
|
179
|
+
<VanCell title="全选">
|
|
180
|
+
<template #right-icon>
|
|
181
|
+
<VanCheckbox v-model="checkedAll" name="all" @click="toggleAll" />
|
|
182
|
+
</template>
|
|
183
|
+
</VanCell>
|
|
184
|
+
<VanCheckboxGroup ref="checkboxGroup" v-model="checkboxValue" @change="change">
|
|
185
|
+
<VanCellGroup>
|
|
186
|
+
<VanCell
|
|
187
|
+
v-for="(item, index) in columnsData"
|
|
188
|
+
:key="item[props.option.value]"
|
|
189
|
+
:title="item[props.option.text]"
|
|
190
|
+
clickable
|
|
191
|
+
@click="toggle(item, index)"
|
|
192
|
+
>
|
|
193
|
+
<template #right-icon>
|
|
194
|
+
<VanCheckbox ref="checkboxes" :name="item[props.option.value]" @click.stop />
|
|
195
|
+
</template>
|
|
196
|
+
</VanCell>
|
|
197
|
+
</VanCellGroup>
|
|
198
|
+
</VanCheckboxGroup>
|
|
199
|
+
</div>
|
|
198
200
|
</div>
|
|
199
201
|
</div>
|
|
200
202
|
</VanPopup>
|
|
@@ -213,12 +215,19 @@ const resultLabel = computed(() => {
|
|
|
213
215
|
left: -8px;
|
|
214
216
|
}
|
|
215
217
|
.x-multi-select-content {
|
|
216
|
-
max-height:
|
|
218
|
+
max-height: calc(70vh - 44px);
|
|
217
219
|
overflow-y: auto;
|
|
218
220
|
.x-multi-select-checkbox-group {
|
|
219
221
|
padding: 0 16px;
|
|
220
222
|
}
|
|
221
223
|
}
|
|
224
|
+
.x-multi-select-popup {
|
|
225
|
+
max-height: 70vh;
|
|
226
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
227
|
+
overflow: hidden;
|
|
228
|
+
display: flex;
|
|
229
|
+
flex-direction: column;
|
|
230
|
+
}
|
|
222
231
|
.loading-container {
|
|
223
232
|
display: flex;
|
|
224
233
|
justify-content: center;
|
|
@@ -197,7 +197,7 @@ onBeforeUnmount(() => {
|
|
|
197
197
|
:border="props.border"
|
|
198
198
|
@click="showPopu($attrs.readonly)"
|
|
199
199
|
/>
|
|
200
|
-
<VanPopup v-model:show="show" position="bottom" teleport="body">
|
|
200
|
+
<VanPopup v-model:show="show" position="bottom" teleport="body" round>
|
|
201
201
|
<div class="x-select-popup">
|
|
202
202
|
<!-- 搜索框 -->
|
|
203
203
|
<VanSearch
|
|
@@ -222,6 +222,23 @@ onBeforeUnmount(() => {
|
|
|
222
222
|
</template>
|
|
223
223
|
|
|
224
224
|
<style scoped>
|
|
225
|
+
.x-select-popup {
|
|
226
|
+
max-height: 70vh;
|
|
227
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
228
|
+
overflow: hidden;
|
|
229
|
+
display: flex;
|
|
230
|
+
flex-direction: column;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.x-select-popup :deep(.van-picker) {
|
|
234
|
+
flex: 1;
|
|
235
|
+
min-height: 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.x-select-popup :deep(.van-picker__columns) {
|
|
239
|
+
max-height: 40vh;
|
|
240
|
+
}
|
|
241
|
+
|
|
225
242
|
.loading-container {
|
|
226
243
|
display: flex;
|
|
227
244
|
justify-content: center;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { FilePreviewInitOptions } from './index.vue'
|
|
3
|
+
/**
|
|
4
|
+
* 文件预览弹窗组件
|
|
5
|
+
* 支持两种展示形式,通过 mode prop 控制:
|
|
6
|
+
* - drawer(默认):从底部滑出的抽屉,占屏幕 92% 高度
|
|
7
|
+
* - dialog:居中弹窗,固定宽高
|
|
8
|
+
*
|
|
9
|
+
* 底部操作栏(仅通过此组件打开时显示):
|
|
10
|
+
* - 下载
|
|
11
|
+
* - 用其他应用打开(仅 Flutter 环境,调用 openFileWithApp 方式二:传 url+name)
|
|
12
|
+
*
|
|
13
|
+
* 用法:
|
|
14
|
+
* <FilePreviewDialog ref="dialogRef" mode="dialog" @close="onClose" />
|
|
15
|
+
* dialogRef.value.open({ path: '/resource/...', name: '文件名.docx' })
|
|
16
|
+
*/
|
|
17
|
+
import { detectEnvironment } from '@af-mobile-client-vue3/utils/environment'
|
|
18
|
+
import { downloadAttachment, openFileWithApp } from '@af-mobile-client-vue3/utils/fileDownload'
|
|
19
|
+
import { Popup as VanPopup } from 'vant'
|
|
20
|
+
import { computed, ref } from 'vue'
|
|
21
|
+
import FilePreview from './index.vue'
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(defineProps<{
|
|
24
|
+
/** 展示形式:drawer 底部抽屉(默认)| dialog 居中弹窗 */
|
|
25
|
+
mode?: 'drawer' | 'dialog'
|
|
26
|
+
}>(), {
|
|
27
|
+
mode: 'drawer',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits<{
|
|
31
|
+
(e: 'close'): void
|
|
32
|
+
}>()
|
|
33
|
+
|
|
34
|
+
const visible = ref(false)
|
|
35
|
+
const fileName = ref('')
|
|
36
|
+
const fileUrl = ref('')
|
|
37
|
+
const previewRef = ref<InstanceType<typeof FilePreview> | null>(null)
|
|
38
|
+
const initOptions = ref<FilePreviewInitOptions | null>(null)
|
|
39
|
+
const loadingKey = ref<string | null>(null) // 当前正在执行的操作 key,null 表示空闲
|
|
40
|
+
|
|
41
|
+
const isApp = computed(() => detectEnvironment().isApp)
|
|
42
|
+
|
|
43
|
+
/** 操作按钮配置,后续新增按钮只需在此追加 */
|
|
44
|
+
interface ActionItem {
|
|
45
|
+
key: string
|
|
46
|
+
label: string
|
|
47
|
+
/** 仅在特定环境显示,不传则始终显示 */
|
|
48
|
+
show?: () => boolean
|
|
49
|
+
handler: () => Promise<void> | void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const actions = computed<ActionItem[]>(() => [
|
|
53
|
+
{
|
|
54
|
+
key: 'download',
|
|
55
|
+
label: '下载',
|
|
56
|
+
handler: handleDownload,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: 'openWithApp',
|
|
60
|
+
label: '用其他应用打开',
|
|
61
|
+
// 仅 Flutter App 环境显示
|
|
62
|
+
show: () => isApp.value,
|
|
63
|
+
handler: handleOpenWithApp,
|
|
64
|
+
},
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
/** 过滤出当前环境应显示的按钮 */
|
|
68
|
+
const visibleActions = computed(() =>
|
|
69
|
+
actions.value.filter(a => !a.show || a.show()),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
function open(options: FilePreviewInitOptions) {
|
|
73
|
+
fileName.value = options.name || options.path.split('/').pop()?.split('?')[0] || '文件预览'
|
|
74
|
+
fileUrl.value = options.path
|
|
75
|
+
initOptions.value = options
|
|
76
|
+
visible.value = true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function onOpened() {
|
|
80
|
+
if (initOptions.value)
|
|
81
|
+
previewRef.value?.init(initOptions.value)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleClose() {
|
|
85
|
+
visible.value = false
|
|
86
|
+
emit('close')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 下载文件 */
|
|
90
|
+
async function handleDownload() {
|
|
91
|
+
await downloadAttachment({ url: fileUrl.value, name: fileName.value })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 用其他应用打开(方式二:传 url+name,Flutter 端自动下载后调起系统分享面板)
|
|
96
|
+
*/
|
|
97
|
+
function handleOpenWithApp() {
|
|
98
|
+
openFileWithApp(fileUrl.value, fileName.value)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 执行操作按钮,防重复点击 */
|
|
102
|
+
async function runAction(action: ActionItem) {
|
|
103
|
+
if (loadingKey.value)
|
|
104
|
+
return
|
|
105
|
+
loadingKey.value = action.key
|
|
106
|
+
try {
|
|
107
|
+
await action.handler()
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
loadingKey.value = null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
defineExpose({ open })
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<template>
|
|
118
|
+
<VanPopup
|
|
119
|
+
v-model:show="visible"
|
|
120
|
+
:position="mode === 'drawer' ? 'bottom' : 'center'"
|
|
121
|
+
:style="mode === 'drawer'
|
|
122
|
+
? { height: '92vh' }
|
|
123
|
+
: { width: '90vw', height: '80vh', borderRadius: '12px' }"
|
|
124
|
+
:round="mode === 'drawer'"
|
|
125
|
+
@opened="onOpened"
|
|
126
|
+
>
|
|
127
|
+
<div class="dialog-root">
|
|
128
|
+
<!-- 顶部标题栏 -->
|
|
129
|
+
<div class="dialog-header" :class="{ 'dialog-header--center': mode === 'dialog' }">
|
|
130
|
+
<span class="dialog-title" :title="fileName">{{ fileName }}</span>
|
|
131
|
+
<button class="dialog-close" @click="handleClose">
|
|
132
|
+
✕
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- 预览区域 -->
|
|
137
|
+
<div class="dialog-body">
|
|
138
|
+
<FilePreview ref="previewRef" class="dialog-preview" />
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<!-- 底部操作栏 -->
|
|
142
|
+
<div class="dialog-actions">
|
|
143
|
+
<button
|
|
144
|
+
v-for="action in visibleActions"
|
|
145
|
+
:key="action.key"
|
|
146
|
+
class="action-btn"
|
|
147
|
+
:class="`action-btn--${action.key}`"
|
|
148
|
+
:disabled="!!loadingKey"
|
|
149
|
+
@pointerdown.stop="runAction(action)"
|
|
150
|
+
>
|
|
151
|
+
{{ loadingKey === action.key ? '处理中...' : action.label }}
|
|
152
|
+
</button>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</VanPopup>
|
|
156
|
+
</template>
|
|
157
|
+
|
|
158
|
+
<style scoped>
|
|
159
|
+
.dialog-root {
|
|
160
|
+
display: flex;
|
|
161
|
+
flex-direction: column;
|
|
162
|
+
height: 100%;
|
|
163
|
+
overflow: hidden;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.dialog-header {
|
|
167
|
+
display: flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
justify-content: space-between;
|
|
170
|
+
padding: 0 16px;
|
|
171
|
+
height: 48px;
|
|
172
|
+
background: #1677ff;
|
|
173
|
+
flex-shrink: 0;
|
|
174
|
+
border-radius: 16px 16px 0 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.dialog-header--center {
|
|
178
|
+
border-radius: 12px 12px 0 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.dialog-title {
|
|
182
|
+
flex: 1;
|
|
183
|
+
font-size: 16px;
|
|
184
|
+
font-weight: 500;
|
|
185
|
+
color: #fff;
|
|
186
|
+
white-space: nowrap;
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
text-overflow: ellipsis;
|
|
189
|
+
margin-right: 12px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.dialog-close {
|
|
193
|
+
width: 28px;
|
|
194
|
+
height: 28px;
|
|
195
|
+
display: flex;
|
|
196
|
+
align-items: center;
|
|
197
|
+
justify-content: center;
|
|
198
|
+
background: transparent;
|
|
199
|
+
border: none;
|
|
200
|
+
color: #fff;
|
|
201
|
+
font-size: 16px;
|
|
202
|
+
cursor: pointer;
|
|
203
|
+
padding: 0;
|
|
204
|
+
flex-shrink: 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.dialog-body {
|
|
208
|
+
flex: 1;
|
|
209
|
+
overflow: hidden;
|
|
210
|
+
display: flex;
|
|
211
|
+
flex-direction: column;
|
|
212
|
+
min-height: 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.dialog-preview {
|
|
216
|
+
flex: 1;
|
|
217
|
+
height: 100%;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* 底部操作栏:按钮数量自适应 */
|
|
221
|
+
.dialog-actions {
|
|
222
|
+
display: flex;
|
|
223
|
+
gap: 10px;
|
|
224
|
+
padding: 10px 16px;
|
|
225
|
+
padding-bottom: calc(10px + env(safe-area-inset-bottom));
|
|
226
|
+
border-top: 1px solid #f0f0f0;
|
|
227
|
+
flex-shrink: 0;
|
|
228
|
+
background: #fff;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.action-btn {
|
|
232
|
+
flex: 1;
|
|
233
|
+
height: 40px;
|
|
234
|
+
border: none;
|
|
235
|
+
border-radius: 8px;
|
|
236
|
+
font-size: 14px;
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
transition: opacity 0.15s;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.action-btn:disabled {
|
|
242
|
+
opacity: 0.5;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* 下载:主色蓝 */
|
|
246
|
+
.action-btn--download {
|
|
247
|
+
background: #1677ff;
|
|
248
|
+
color: #fff;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* 用其他应用打开:次级灰白 */
|
|
252
|
+
.action-btn--openWithApp {
|
|
253
|
+
background: #f5f5f5;
|
|
254
|
+
color: #333;
|
|
255
|
+
border: 1px solid #e0e0e0;
|
|
256
|
+
}
|
|
257
|
+
</style>
|
|
@@ -68,6 +68,15 @@ onMounted(buildPreviewUrl)
|
|
|
68
68
|
position: relative;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/* 透明遮罩:阻止 iframe 在点击悬浮按钮时抢夺事件 */
|
|
72
|
+
.preview-doc-container::after {
|
|
73
|
+
content: '';
|
|
74
|
+
position: absolute;
|
|
75
|
+
inset: 0;
|
|
76
|
+
z-index: 99;
|
|
77
|
+
pointer-events: none;
|
|
78
|
+
}
|
|
79
|
+
|
|
71
80
|
.kk-loading {
|
|
72
81
|
position: absolute;
|
|
73
82
|
inset: 0;
|
|
@@ -39,13 +39,14 @@ const VueOfficeExcel = defineAsyncComponent(async () => {
|
|
|
39
39
|
class="office-viewer"
|
|
40
40
|
@error="emit('error')"
|
|
41
41
|
/>
|
|
42
|
-
<!-- docx
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
<!-- docx 单独用缩放容器,整体缩小至 85% 以适配移动端屏幕 -->
|
|
43
|
+
<div v-else-if="kind === 'docx' || kind === 'doc'" class="docx-scale-wrapper">
|
|
44
|
+
<VueOfficeDocx
|
|
45
|
+
:src="src"
|
|
46
|
+
class="office-viewer docx-scaled"
|
|
47
|
+
@error="emit('error')"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
49
50
|
<!-- xlsx / xls 共用 excel 渲染器 -->
|
|
50
51
|
<VueOfficeExcel
|
|
51
52
|
v-else-if="kind === 'xlsx' || kind === 'xls'"
|
|
@@ -62,4 +63,20 @@ const VueOfficeExcel = defineAsyncComponent(async () => {
|
|
|
62
63
|
min-height: 200px;
|
|
63
64
|
overflow: auto;
|
|
64
65
|
}
|
|
66
|
+
|
|
67
|
+
/* docx 缩放容器:裁掉 scale 产生的右侧和底部留白 */
|
|
68
|
+
.docx-scale-wrapper {
|
|
69
|
+
flex: 1;
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
width: 100%;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* 整体缩放 85%,transform-origin 从左上角开始,保持左对齐 */
|
|
75
|
+
.docx-scaled {
|
|
76
|
+
transform: scale(0.7);
|
|
77
|
+
transform-origin: top left;
|
|
78
|
+
/* scale 不影响布局,需手动补偿宽度和高度 */
|
|
79
|
+
width: calc(100% / 0.7);
|
|
80
|
+
min-height: calc(100% / 0.7);
|
|
81
|
+
}
|
|
65
82
|
</style>
|
|
@@ -5,12 +5,10 @@
|
|
|
5
5
|
* 渲染策略:
|
|
6
6
|
* 1. image / video / audio → MediaPreview(原生标签)
|
|
7
7
|
* 2. text → TextPreview(<pre>)
|
|
8
|
-
* 3. pdf / docx /
|
|
9
|
-
*
|
|
10
|
-
* 4. doc 旧格式 / unsupported → 直接走 KkPreview
|
|
8
|
+
* 3. pdf / docx / xlsx → OfficePreview(@vue-office),失败自动降级到 KkPreview
|
|
9
|
+
* 4. doc / xls / unsupported → 直接走 KkPreview
|
|
11
10
|
*/
|
|
12
11
|
import type { PreviewFileKind } from '@af-mobile-client-vue3/utils/fileType'
|
|
13
|
-
import { downloadAttachment } from '@af-mobile-client-vue3/utils/fileDownload'
|
|
14
12
|
import { getPreviewFileKind } from '@af-mobile-client-vue3/utils/fileType'
|
|
15
13
|
import {
|
|
16
14
|
fetchResourceArrayBuffer,
|
|
@@ -18,7 +16,7 @@ import {
|
|
|
18
16
|
getPreviewPath,
|
|
19
17
|
resolveResourceUrl,
|
|
20
18
|
} from '@af-mobile-client-vue3/utils/resourceUrl'
|
|
21
|
-
import {
|
|
19
|
+
import { Empty as VanEmpty, Loading as VanLoading } from 'vant'
|
|
22
20
|
import { ref, shallowRef } from 'vue'
|
|
23
21
|
import KkPreview from './components/KkPreview.vue'
|
|
24
22
|
import MediaPreview from './components/MediaPreview.vue'
|
|
@@ -28,17 +26,16 @@ import TextPreview from './components/TextPreview.vue'
|
|
|
28
26
|
export interface FilePreviewInitOptions {
|
|
29
27
|
/** 后端 f_downloadpath 或完整 URL */
|
|
30
28
|
path: string
|
|
31
|
-
/**
|
|
29
|
+
/** 文件名(可选,默认从路径提取) */
|
|
32
30
|
name?: string
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
/**
|
|
33
|
+
/** 优先走 @vue-office,失败降级 kk */
|
|
36
34
|
const OFFICE_KINDS: PreviewFileKind[] = ['pdf', 'docx', 'xlsx']
|
|
37
35
|
|
|
38
36
|
/**
|
|
39
|
-
* 直接走 kk
|
|
40
|
-
* - doc 旧二进制格式,@vue-office 无法解析
|
|
41
|
-
* - xls 旧二进制格式,@vue-office 解析效果差
|
|
37
|
+
* 直接走 kk 预览的格式:
|
|
38
|
+
* - doc / xls 旧二进制格式,@vue-office 无法解析
|
|
42
39
|
* - unsupported 未知格式,交给 kkFileView 服务端判断
|
|
43
40
|
*/
|
|
44
41
|
const KK_ONLY_KINDS: PreviewFileKind[] = ['doc', 'xls', 'unsupported']
|
|
@@ -47,15 +44,12 @@ const kind = ref<PreviewFileKind>('unsupported')
|
|
|
47
44
|
const fileSource = shallowRef<string | ArrayBuffer>('')
|
|
48
45
|
const textContent = ref('')
|
|
49
46
|
const mediaSrc = ref('')
|
|
50
|
-
const rawUrl = ref('') // 文件原始 URL(kk 预览和下载共用)
|
|
51
47
|
const rawPath = ref('') // 文件相对路径(kk 预览用)
|
|
52
|
-
const downloadName = ref('')
|
|
53
48
|
const loading = ref(false)
|
|
54
49
|
const errorMsg = ref('')
|
|
55
50
|
/** true = office 渲染失败,已降级到 kk 预览 */
|
|
56
51
|
const officeFailed = ref(false)
|
|
57
52
|
|
|
58
|
-
/** office 渲染失败回调,切换到 kk 预览 */
|
|
59
53
|
function onOfficeFailed() {
|
|
60
54
|
officeFailed.value = true
|
|
61
55
|
}
|
|
@@ -67,11 +61,9 @@ async function init(options: FilePreviewInitOptions) {
|
|
|
67
61
|
return
|
|
68
62
|
}
|
|
69
63
|
|
|
70
|
-
|
|
64
|
+
const rawUrl = resolveResourceUrl(path)
|
|
71
65
|
kind.value = getPreviewFileKind(path)
|
|
72
|
-
rawUrl.value = resolveResourceUrl(path)
|
|
73
66
|
rawPath.value = path
|
|
74
|
-
downloadName.value = options.name || path.split('/').pop()?.split('?')[0] || '附件'
|
|
75
67
|
loading.value = true
|
|
76
68
|
errorMsg.value = ''
|
|
77
69
|
officeFailed.value = false
|
|
@@ -80,17 +72,16 @@ async function init(options: FilePreviewInitOptions) {
|
|
|
80
72
|
mediaSrc.value = ''
|
|
81
73
|
|
|
82
74
|
try {
|
|
83
|
-
if (
|
|
84
|
-
mediaSrc.value = rawUrl
|
|
75
|
+
if (['image', 'video', 'audio'].includes(kind.value)) {
|
|
76
|
+
mediaSrc.value = rawUrl
|
|
85
77
|
}
|
|
86
78
|
else if (kind.value === 'text') {
|
|
87
|
-
textContent.value = await fetchResourceText(rawUrl
|
|
79
|
+
textContent.value = await fetchResourceText(rawUrl)
|
|
88
80
|
}
|
|
89
81
|
else if (OFFICE_KINDS.includes(kind.value)) {
|
|
90
|
-
|
|
91
|
-
fileSource.value = await fetchResourceArrayBuffer(rawUrl.value)
|
|
82
|
+
fileSource.value = await fetchResourceArrayBuffer(rawUrl)
|
|
92
83
|
}
|
|
93
|
-
// doc / unsupported 直接走 KkPreview,无需预加载
|
|
84
|
+
// doc / xls / unsupported 直接走 KkPreview,无需预加载
|
|
94
85
|
}
|
|
95
86
|
catch (e: any) {
|
|
96
87
|
errorMsg.value = e?.message || '文件加载失败'
|
|
@@ -100,52 +91,32 @@ async function init(options: FilePreviewInitOptions) {
|
|
|
100
91
|
}
|
|
101
92
|
}
|
|
102
93
|
|
|
103
|
-
|
|
104
|
-
downloadAttachment({ url: rawUrl.value, name: downloadName.value })
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
defineExpose({ init, download: handleDownload })
|
|
94
|
+
defineExpose({ init })
|
|
108
95
|
</script>
|
|
109
96
|
|
|
110
97
|
<template>
|
|
111
98
|
<div class="file-preview-root">
|
|
112
|
-
<!-- 悬浮下载按钮,定位在预览区右上角 -->
|
|
113
|
-
<div v-if="rawUrl && !loading" class="preview-download-btn">
|
|
114
|
-
<VanButton size="small" type="primary" plain icon="down" @click="handleDownload">
|
|
115
|
-
下载
|
|
116
|
-
</VanButton>
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
99
|
<!-- 加载中 -->
|
|
120
100
|
<VanLoading v-if="loading" class="preview-loading" type="spinner" vertical>
|
|
121
101
|
加载中...
|
|
122
102
|
</VanLoading>
|
|
123
103
|
|
|
124
104
|
<!-- 加载/解析出错 -->
|
|
125
|
-
<VanEmpty v-else-if="errorMsg" :description="errorMsg"
|
|
126
|
-
<VanButton v-if="rawUrl" size="small" type="primary" @click="handleDownload">
|
|
127
|
-
下载
|
|
128
|
-
</VanButton>
|
|
129
|
-
</VanEmpty>
|
|
105
|
+
<VanEmpty v-else-if="errorMsg" :description="errorMsg" />
|
|
130
106
|
|
|
131
107
|
<template v-else>
|
|
132
|
-
<!-- 媒体:图片 / 视频 / 音频 -->
|
|
133
108
|
<MediaPreview
|
|
134
109
|
v-if="kind === 'image' || kind === 'video' || kind === 'audio'"
|
|
135
110
|
:kind="kind"
|
|
136
111
|
:src="mediaSrc"
|
|
137
112
|
/>
|
|
138
113
|
|
|
139
|
-
<!-- 纯文本 -->
|
|
140
114
|
<TextPreview
|
|
141
115
|
v-else-if="kind === 'text'"
|
|
142
116
|
:content="textContent"
|
|
143
117
|
/>
|
|
144
118
|
|
|
145
|
-
<!--
|
|
146
|
-
Office 文档:先走 @vue-office
|
|
147
|
-
失败(officeFailed)或 doc/unsupported 直接走 KkPreview
|
|
148
|
-
-->
|
|
119
|
+
<!-- office:先 @vue-office,失败降级 kk -->
|
|
149
120
|
<OfficePreview
|
|
150
121
|
v-else-if="OFFICE_KINDS.includes(kind) && fileSource && !officeFailed"
|
|
151
122
|
:kind="kind"
|
|
@@ -153,18 +124,13 @@ defineExpose({ init, download: handleDownload })
|
|
|
153
124
|
@error="onOfficeFailed"
|
|
154
125
|
/>
|
|
155
126
|
|
|
156
|
-
<!-- kk 预览:office 降级 或 doc/unsupported 直接走 -->
|
|
127
|
+
<!-- kk 预览:office 降级 或 doc/xls/unsupported 直接走 -->
|
|
157
128
|
<KkPreview
|
|
158
129
|
v-else-if="officeFailed || KK_ONLY_KINDS.includes(kind)"
|
|
159
130
|
:file-path="rawPath"
|
|
160
131
|
/>
|
|
161
132
|
|
|
162
|
-
|
|
163
|
-
<VanEmpty v-else description="文件路径异常">
|
|
164
|
-
<VanButton v-if="rawUrl" size="small" type="primary" @click="handleDownload">
|
|
165
|
-
下载
|
|
166
|
-
</VanButton>
|
|
167
|
-
</VanEmpty>
|
|
133
|
+
<VanEmpty v-else description="文件路径异常" />
|
|
168
134
|
</template>
|
|
169
135
|
</div>
|
|
170
136
|
</template>
|
|
@@ -178,15 +144,6 @@ defineExpose({ init, download: handleDownload })
|
|
|
178
144
|
flex-direction: column;
|
|
179
145
|
background: #fff;
|
|
180
146
|
overflow: hidden;
|
|
181
|
-
/* 作为悬浮按钮的定位基准 */
|
|
182
|
-
position: relative;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
.preview-download-btn {
|
|
186
|
-
position: absolute;
|
|
187
|
-
top: 10px;
|
|
188
|
-
right: 12px;
|
|
189
|
-
z-index: 10;
|
|
190
147
|
}
|
|
191
148
|
|
|
192
149
|
.preview-loading {
|