2025-03-05 17:13:23 +08:00
|
|
|
|
<script setup lang="ts">
|
2025-03-21 17:06:09 +08:00
|
|
|
|
import { onMounted, ref } from 'vue'
|
2025-03-21 11:51:27 +08:00
|
|
|
|
import { useEventListener } from '@vueuse/core'
|
2025-03-21 18:52:41 +08:00
|
|
|
|
import axios from 'axios'
|
2025-03-05 17:13:23 +08:00
|
|
|
|
import { IFrame } from '@/components/IFrame'
|
2025-03-21 17:06:09 +08:00
|
|
|
|
|
|
|
|
|
import { isInIframe } from '@/utils/iframe'
|
|
|
|
|
|
2025-03-21 18:52:41 +08:00
|
|
|
|
import { useMessage } from '@/hooks/web/useMessage'
|
2025-03-21 17:06:09 +08:00
|
|
|
|
import { uploadOneFile } from '@/api/base/upload'
|
2025-03-05 17:13:23 +08:00
|
|
|
|
import { initFilePreviewUrl } from '@/utils/file/preview'
|
|
|
|
|
|
2025-03-21 17:06:09 +08:00
|
|
|
|
import { getConfigKey } from '@/api/infra/config'
|
2025-03-21 18:52:41 +08:00
|
|
|
|
import type { FileDO } from '@/types/axios'
|
|
|
|
|
import { useGlobSetting } from '@/hooks/setting'
|
|
|
|
|
import { getAccessToken } from '@/utils/auth'
|
2025-03-22 11:49:12 +08:00
|
|
|
|
import { extractMethodName } from '@/views/infra/bookmark/logic'
|
2025-03-21 17:06:09 +08:00
|
|
|
|
|
2025-03-05 17:13:23 +08:00
|
|
|
|
defineOptions({ name: 'BookmarkReplace' })
|
2025-03-21 17:06:09 +08:00
|
|
|
|
const isEmbed = isInIframe()
|
2025-03-21 18:52:41 +08:00
|
|
|
|
const globSetting = useGlobSetting()
|
2025-03-05 17:13:23 +08:00
|
|
|
|
|
|
|
|
|
const previewUrl = ref<string | undefined>(undefined)
|
2025-03-22 11:49:12 +08:00
|
|
|
|
const iframeRef = ref<InstanceType<typeof IFrame> | null>(null)
|
2025-03-05 17:13:23 +08:00
|
|
|
|
|
2025-03-21 18:52:41 +08:00
|
|
|
|
const { createMessage } = useMessage()
|
2025-03-05 17:13:23 +08:00
|
|
|
|
|
2025-03-21 17:06:09 +08:00
|
|
|
|
const wopi_client = ref(undefined)
|
|
|
|
|
const wopi_server = ref(undefined)
|
2025-03-05 17:13:23 +08:00
|
|
|
|
|
2025-03-21 18:52:41 +08:00
|
|
|
|
const editStatus = ref<{ Modified: boolean }>({ Modified: false })
|
|
|
|
|
const currentEditFile = ref<FileDO | undefined>(undefined)
|
2025-03-22 11:49:12 +08:00
|
|
|
|
|
|
|
|
|
const isPostMessageRead = ref<boolean>(false)
|
2025-03-21 17:06:09 +08:00
|
|
|
|
onMounted(async () => {
|
|
|
|
|
wopi_client.value = await getConfigKey('wopi_client_addr')
|
|
|
|
|
wopi_server.value = await getConfigKey('wopi_server_ip_addr')
|
2025-03-05 17:13:23 +08:00
|
|
|
|
|
2025-03-21 18:52:41 +08:00
|
|
|
|
/**
|
|
|
|
|
* 接收消息
|
|
|
|
|
*/
|
2025-03-21 17:06:09 +08:00
|
|
|
|
useEventListener(window, 'message', async (e: MessageEvent) => {
|
|
|
|
|
logEvent(e)
|
2025-03-21 18:52:41 +08:00
|
|
|
|
|
|
|
|
|
let data: any = null
|
|
|
|
|
|
|
|
|
|
if (typeof e.data === 'string')
|
|
|
|
|
data = JSON.parse(e.data)
|
|
|
|
|
|
|
|
|
|
if (typeof e.data === 'object')
|
|
|
|
|
data = e.data
|
|
|
|
|
|
2025-03-22 11:49:12 +08:00
|
|
|
|
if (!data)
|
|
|
|
|
return
|
|
|
|
|
|
2025-03-21 18:52:41 +08:00
|
|
|
|
const ActionId = data.ActionId
|
|
|
|
|
const MessageId = data.MessageId
|
2025-03-05 18:12:55 +08:00
|
|
|
|
|
2025-03-21 17:06:09 +08:00
|
|
|
|
// 第三方调用发送的Event
|
|
|
|
|
switch (ActionId) {
|
2025-03-22 11:49:12 +08:00
|
|
|
|
case 'OpenFile':
|
|
|
|
|
await handleFileBinary(data)
|
|
|
|
|
break
|
|
|
|
|
case 'QueryAllWithJSON':
|
|
|
|
|
proxyQueryBookmark()
|
|
|
|
|
break
|
|
|
|
|
case 'ReplaceWithJSON':
|
|
|
|
|
proxyReplaceWithJSON(data)
|
2025-03-21 17:06:09 +08:00
|
|
|
|
break
|
|
|
|
|
}
|
2025-03-21 18:52:41 +08:00
|
|
|
|
// WOPI Client发送消息
|
|
|
|
|
switch (MessageId) {
|
|
|
|
|
// 更新文档是否被编辑
|
|
|
|
|
case 'Doc_ModifiedStatus':
|
2025-03-22 11:49:12 +08:00
|
|
|
|
handleUpdateModifiedStatus(data)
|
2025-03-21 18:52:41 +08:00
|
|
|
|
break
|
|
|
|
|
// 用户执行了保存报错
|
|
|
|
|
case 'UI_Save':
|
|
|
|
|
handleUserSaveOp()
|
|
|
|
|
break
|
2025-03-22 11:49:12 +08:00
|
|
|
|
case 'CallPythonScript-Result':
|
|
|
|
|
handlePythonScriptCallBack(data)
|
|
|
|
|
break
|
2025-03-21 18:52:41 +08:00
|
|
|
|
}
|
2025-03-21 17:06:09 +08:00
|
|
|
|
})
|
2025-03-05 18:12:55 +08:00
|
|
|
|
})
|
|
|
|
|
|
2025-03-21 18:52:41 +08:00
|
|
|
|
/**
|
|
|
|
|
* 更新是否编辑状态
|
|
|
|
|
* 保存后会更新是否编辑为否,正常编辑后会更新是否编辑为是
|
|
|
|
|
* 用于区分是否要向第三方发送保存文件的回调
|
2025-03-22 11:49:12 +08:00
|
|
|
|
* @param data
|
2025-03-21 18:52:41 +08:00
|
|
|
|
*/
|
2025-03-22 11:49:12 +08:00
|
|
|
|
function handleUpdateModifiedStatus(data: any) {
|
|
|
|
|
editStatus.value.Modified = data.Values.Modified
|
2025-03-21 18:52:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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: 'SAVE_FILE',
|
|
|
|
|
Payload: {
|
|
|
|
|
name: currentEditFile.value.name,
|
|
|
|
|
buffer: arrayBuffer,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
catch (error) {
|
|
|
|
|
console.error('文件下载失败:', error)
|
|
|
|
|
createMessage.error('文件下载失败,请查看控制台日志')
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 第三方上传文件
|
2025-03-22 11:49:12 +08:00
|
|
|
|
* @param data
|
2025-03-21 18:52:41 +08:00
|
|
|
|
*/
|
2025-03-22 11:49:12 +08:00
|
|
|
|
async function handleFileBinary(data: any) {
|
|
|
|
|
const { name, type, buffer } = data.Payload
|
2025-03-21 17:06:09 +08:00
|
|
|
|
const blob = new Blob([buffer], { type })
|
|
|
|
|
const uploadResult = await uploadOneFile({
|
|
|
|
|
filename: name,
|
|
|
|
|
file: new File([blob], name, { type }),
|
|
|
|
|
})
|
2025-03-21 18:52:41 +08:00
|
|
|
|
currentEditFile.value = uploadResult.data.data as FileDO
|
|
|
|
|
previewUrl.value = await initFilePreviewUrl(currentEditFile.value.id, wopi_client.value, wopi_server.value)
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-22 11:49:12 +08:00
|
|
|
|
/**
|
|
|
|
|
* 第三方调用,查询所有书签
|
|
|
|
|
*/
|
|
|
|
|
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),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-21 18:52:41 +08:00
|
|
|
|
/**
|
|
|
|
|
* 发送消息给WopiClient
|
|
|
|
|
* 用于执行书签相关操作
|
|
|
|
|
*/
|
2025-03-22 11:49:12 +08:00
|
|
|
|
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(`消息发送失败,查看控制台日志!`)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-21 18:52:41 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 发送消息给第三方调用者
|
|
|
|
|
* 用于转发部分WopiClient回调
|
|
|
|
|
*/
|
|
|
|
|
function sendMessageToCaller(data: any) {
|
|
|
|
|
try {
|
|
|
|
|
const targetWindow = window.parent
|
2025-03-22 11:49:12 +08:00
|
|
|
|
if (typeof data === 'string')
|
|
|
|
|
targetWindow.postMessage(data, '*')
|
|
|
|
|
|
|
|
|
|
if (typeof data === 'object')
|
|
|
|
|
targetWindow.postMessage(JSON.stringify(data), '*')
|
2025-03-21 18:52:41 +08:00
|
|
|
|
}
|
|
|
|
|
catch (e) {
|
|
|
|
|
console.error(e)
|
|
|
|
|
createMessage.error(`消息发送失败,查看控制台日志!`)
|
|
|
|
|
}
|
2025-03-21 17:06:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-03-22 11:49:12 +08:00
|
|
|
|
/**
|
|
|
|
|
* 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),
|
2025-03-22 14:08:02 +08:00
|
|
|
|
Success: success,
|
2025-03-22 11:49:12 +08:00
|
|
|
|
Payload: jsonData,
|
2025-03-22 14:08:02 +08:00
|
|
|
|
Message: null,
|
2025-03-22 11:49:12 +08:00
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
console.error(data)
|
|
|
|
|
createMessage.error(`执行Python脚本失败: ${data.Values.result.value}`)
|
2025-03-22 14:08:02 +08:00
|
|
|
|
|
|
|
|
|
sendMessageToCaller({
|
|
|
|
|
ActionId: extractMethodName(commandName),
|
|
|
|
|
Success: success,
|
|
|
|
|
Payload: null,
|
|
|
|
|
Message: data.Values.result.value,
|
|
|
|
|
})
|
2025-03-22 11:49:12 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-21 17:06:09 +08:00
|
|
|
|
function logEvent(e: MessageEvent) {
|
|
|
|
|
console.log('=============receive message start=======')
|
|
|
|
|
console.log(e.data)
|
|
|
|
|
console.log(e.origin)
|
|
|
|
|
}
|
2025-03-05 17:13:23 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div>
|
2025-03-21 17:06:09 +08:00
|
|
|
|
<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">
|
2025-03-22 11:49:12 +08:00
|
|
|
|
<i-frame v-if="previewUrl" ref="iframeRef" class="w-full bg-white dark:bg-black" :src="previewUrl" :height="isEmbed ? 'calc(100vh)' : 'calc(100vh) - 105px'" />
|
2025-03-05 17:13:23 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|