web-view-antd/src/views/infra/bookmark/index.vue
2025-03-22 14:23:12 +08:00

345 lines
8.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useEventListener } from '@vueuse/core'
import axios from 'axios'
import { IFrame } from '@/components/IFrame'
import { isInIframe } from '@/utils/iframe'
import { useMessage } from '@/hooks/web/useMessage'
import { uploadOneFile } from '@/api/base/upload'
import { initFilePreviewUrl } from '@/utils/file/preview'
import { getConfigKey } from '@/api/infra/config'
import type { FileDO } from '@/types/axios'
import { useGlobSetting } from '@/hooks/setting'
import { getAccessToken } from '@/utils/auth'
import { extractMethodName } from '@/views/infra/bookmark/logic'
defineOptions({ name: 'BookmarkReplace' })
const isEmbed = isInIframe()
const globSetting = useGlobSetting()
const previewUrl = ref<string | undefined>(undefined)
const iframeRef = ref<InstanceType<typeof IFrame> | null>(null)
const { createMessage } = useMessage()
const wopi_client = ref(undefined)
const wopi_server = ref(undefined)
const editStatus = ref<{ Modified: boolean }>({ Modified: false })
const currentEditFile = ref<FileDO | undefined>(undefined)
const isPostMessageRead = ref<boolean>(false)
onMounted(async () => {
wopi_client.value = await getConfigKey('wopi_client_addr')
wopi_server.value = await getConfigKey('wopi_server_ip_addr')
/**
* 接收消息
*/
useEventListener(window, 'message', async (e: MessageEvent) => {
logEvent(e)
let data: any = null
if (typeof e.data === 'string')
data = JSON.parse(e.data)
if (typeof e.data === 'object')
data = e.data
if (!data)
return
const ActionId = data.ActionId
const MessageId = data.MessageId
// 第三方调用发送的Event
switch (ActionId) {
case 'OpenFile':
await handleFileBinary(data)
break
case 'QueryAllWithJSON':
proxyQueryBookmark()
break
case 'ReplaceWithJSON':
proxyReplaceWithJSON(data)
break
case 'SaveFile':
handleSaveFile()
break
}
// WOPI Client发送消息
switch (MessageId) {
// 更新文档是否被编辑
case 'Doc_ModifiedStatus':
handleUpdateModifiedStatus(data)
break
// 用户执行了保存操作
case 'UI_Save':
handleUserSaveOp()
break
case 'CallPythonScript-Result':
handlePythonScriptCallBack(data)
break
case 'Action_Save_Resp':
handleUserSaveOp()
break
}
})
})
/**
* 更新是否编辑状态
* 保存后会更新是否编辑为否,正常编辑后会更新是否编辑为是
* 用于区分是否要向第三方发送保存文件的回调
* @param data
*/
function handleUpdateModifiedStatus(data: any) {
editStatus.value.Modified = data.Values.Modified
}
let pollingInterval: NodeJS.Timeout | null = null
let lastCallTime: number | null = null
async function handleUserSaveOp() {
// 情况 1: 如果当前状态未修改,直接发送消息
if (!editStatus.value.Modified) {
// 下载文件并发送
downloadAndSendCurrentEditFile()
return
}
// 情况 2: 已处于修改状态,开始/重置等待流程
// 更新最后一次调用时间戳
lastCallTime = Date.now()
// 清除已有定时器(实现调用重置)
if (pollingInterval) {
clearInterval(pollingInterval)
pollingInterval = null
}
// 启动新的轮询检测
pollingInterval = setInterval(() => {
// 子情况 2a: 状态已恢复未修改
if (!editStatus.value.Modified) {
downloadAndSendCurrentEditFile()
cleanup()
return
}
// 子情况 2b: 检测超时10秒逻辑
const currentTime = Date.now()
if (lastCallTime && currentTime - lastCallTime >= 10000) {
createMessage.error('保存操作执行超时!')
cleanup()
}
}, 500) // 每 500ms 检测一次
}
// 清理函数
function cleanup() {
if (pollingInterval) {
clearInterval(pollingInterval)
pollingInterval = null
}
lastCallTime = null
}
/**
* 下载当前编辑的文件,发送给调用者
*/
async function downloadAndSendCurrentEditFile() {
if (!currentEditFile.value)
return
try {
// 1. 发送请求(关键配置 responseType: 'arraybuffer'
const response = await axios.get(`${globSetting.apiUrl}/infra/file/download/${currentEditFile.value.id}`, {
responseType: 'arraybuffer', // [!code focus]
headers: {
Authorization: `Bearer ${getAccessToken()}`,
},
})
// 2. 验证响应状态
if (response.status !== 200)
createMessage.error(`请求失败,状态码:${response.status}`)
// 3. 获取 ArrayBuffer 数据
const arrayBuffer: ArrayBuffer = response.data
sendMessageToCaller({
ActionId: 'SaveFile',
Payload: {
name: currentEditFile.value.name,
buffer: arrayBuffer,
},
}, true)
}
catch (error) {
console.error('文件下载失败:', error)
createMessage.error('文件下载失败,请查看控制台日志')
throw error
}
}
/**
* 第三方上传文件
* @param data
*/
async function handleFileBinary(data: any) {
const { name, type, buffer } = data.Payload
const blob = new Blob([buffer], { type })
const uploadResult = await uploadOneFile({
filename: name,
file: new File([blob], name, { type }),
})
currentEditFile.value = uploadResult.data.data as FileDO
previewUrl.value = await initFilePreviewUrl(currentEditFile.value.id, wopi_client.value, wopi_server.value)
}
/**
* 第三方调用,查询所有书签
*/
function proxyQueryBookmark() {
sendMessageToWopiClient({
MessageId: 'CallPythonScript',
SendTime: Date.now(),
ScriptFile: 'BookmarkOP.py',
Function: 'QueryAllWithJSON',
})
}
/**
* 第三方调用,书签替换
* @param data
*/
function proxyReplaceWithJSON(data: any) {
sendMessageToWopiClient({
MessageId: 'CallPythonScript',
SendTime: Date.now(),
ScriptFile: 'BookmarkOP.py',
Function: 'ReplaceWithJSON',
Values: {
params: {
type: 'string',
value: JSON.stringify(data.Payload),
},
},
})
}
/**
* 发送消息给WopiClient
* 用于执行书签相关操作
*/
function sendMessageToWopiClient(data: any) {
try {
if (!isPostMessageRead.value) {
console.log('Host准备好')
iframeRef.value?.sendMessageToIframe(JSON.stringify({
MessageId: 'Host_PostmessageReady',
SendTime: Date.now(),
}))
isPostMessageRead.value = true
}
iframeRef.value?.sendMessageToIframe(JSON.stringify(data))
console.log('已向WOPI_Client发送消息')
console.log(JSON.stringify(data))
}
catch (e) {
console.error(e)
createMessage.error(`消息发送失败,查看控制台日志!`)
}
}
/**
* 发送消息给第三方调用者
* 用于转发部分WopiClient回调
*/
function sendMessageToCaller(data: any, ignore: boolean = false) {
try {
const targetWindow = window.parent
if (ignore) {
targetWindow.postMessage(data, '*')
return
}
if (typeof data === 'string')
targetWindow.postMessage(data, '*')
if (typeof data === 'object')
targetWindow.postMessage(JSON.stringify(data), '*')
}
catch (e) {
console.error(e)
createMessage.error(`消息发送失败,查看控制台日志!`)
}
}
/**
* Python脚本执行回调
* @param data
*/
function handlePythonScriptCallBack(data: any) {
const Values = data.Values
const success = Values.success as boolean
const commandName = Values.commandName
if (success) {
const jsonData = JSON.parse(Values.result.value)
sendMessageToCaller({
ActionId: extractMethodName(commandName),
Success: success,
Payload: jsonData,
Message: null,
})
}
else {
console.error(data)
createMessage.error(`执行Python脚本失败: ${data.Values.result.value}`)
sendMessageToCaller({
ActionId: extractMethodName(commandName),
Success: success,
Payload: null,
Message: data.Values.result.value,
})
}
}
function handleSaveFile() {
sendMessageToWopiClient({
MessageId: 'Action_Save',
Values: {
DontTerminateEdit: true,
DontSaveIfUnmodified: false,
Notify: true,
ExtendedData: '',
},
})
}
function logEvent(e: MessageEvent) {
console.log('=============receive message start=======')
console.log(e.data)
console.log(e.origin)
}
</script>
<template>
<div>
<div class="flex flex-col" :class="{ 'p-2 pt-4': !isEmbed }">
<div class="w-full flex flex-row" :class="{ 'h-[calc(100vh)]': isEmbed, 'h-[calc(100vh-105px)]': !isEmbed }">
<div class="w-full flex flex-row bg-white dark:bg-black">
<i-frame v-if="previewUrl" ref="iframeRef" class="w-full bg-white dark:bg-black" :src="previewUrl" :height="isEmbed ? 'calc(100vh)' : 'calc(100vh) - 105px'" />
</div>
</div>
</div>
</div>
</template>