Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added ui/src/assets/application/embed-floating.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/src/assets/application/embed-fullscreen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/src/assets/application/embed-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,9 @@ View 负责路由、资源与模式、图加载、保存/发布/调试编排
`api-key/`,入口和对应弹窗同目录维护。API Key 弹窗通过 `resourceApi.apiKey` 传入当前范围的完整 API 与智能体 ID,
复用 `business/api-key-list/`,Base URL 使用 Chat API 部署前缀加智能体 ID;
Workspace 与 System 资源管理共用智能体弹窗,分别使用各自接口,不回退或使用头像菜单的全局系统 API Key。
嵌入第三方弹窗接收概览已有的 Web 链接与访问凭证,不额外请求接口;按全屏、浮窗、移动端单选切换并复制代码,
关闭后恢复全屏模式。iframe 沿用 Web 链接及工作流参数,移动端增加 `mode=mobile`;浮窗使用 Chat API 前缀下的
`/embed`,携带协议、主机、访问凭证及工作流参数,生成 HTML 时转义属性值。无访问凭证时禁用入口。
其他访问配置及弹窗业务尚待接入,不模拟保存。
三类场景统一调用 `getMonitoring(workspaceId, range, applicationId)`;全部智能体使用空 ID,
两类首页不提交智能体筛选,概览传当前智能体 ID。页面直接传入完整 API,不包装接口。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ onMounted(() => {
</div>
<div class="flex-wrap">
<ButtonChat />
<ButtonEmbedThirdParty />
<ButtonEmbedThirdParty :share-url="shareUrl" :access-token="accessToken" />
<ButtonAccessRestriction />
<ButtonDisplaySetting />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ import EmbedThirdPartyDialog from './EmbedThirdPartyDialog.vue'

defineOptions({ name: 'ButtonEmbedThirdParty' })

const props = defineProps<{ shareUrl: string; accessToken: string }>()
const dialogRef = useTemplateRef<InstanceType<typeof EmbedThirdPartyDialog>>('dialogRef')

function handleOpenEmbedThirdParty() {
if (!props.shareUrl || !props.accessToken) return
dialogRef.value?.open()
}
</script>

<template>
<!-- 嵌入第三方 -->
<el-button plain @click="handleOpenEmbedThirdParty">
<el-button plain :disabled="!props.shareUrl || !props.accessToken" @click="handleOpenEmbedThirdParty">
<MkIcon name="icon_keyboard_outlined1" />
<span>嵌入第三方</span>
</el-button>
<EmbedThirdPartyDialog ref="dialogRef" />
<EmbedThirdPartyDialog ref="dialogRef" :share-url="props.shareUrl" :access-token="props.accessToken" />
</template>
Original file line number Diff line number Diff line change
@@ -1,9 +1,51 @@
<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { escape } from 'lodash'
import { CHAT_API_BASE_PATH } from '@/api/constants'
import { copyText } from '@/utils/clipboard'
import fullscreenImage from '@/assets/application/embed-fullscreen.png'
import floatingImage from '@/assets/application/embed-floating.png'
import mobileImage from '@/assets/application/embed-mobile.png'

defineOptions({ name: 'EmbedThirdPartyDialog' })

const props = defineProps<{ shareUrl: string; accessToken: string }>()
const visible = ref(false)
const selectedMode = ref<'fullscreen' | 'floating' | 'mobile'>('fullscreen')
const embedModes = [
{ value: 'fullscreen', label: '全屏模式', image: fullscreenImage },
{ value: 'floating', label: '浮窗模式', image: floatingImage },
{ value: 'mobile', label: '移动端模式', image: mobileImage },
] as const

const embedCode = computed(() => {
if (!props.shareUrl || !props.accessToken) return ''
const chatUrl = new URL(props.shareUrl)

if (selectedMode.value === 'floating') {
const embedUrl = new URL(`${CHAT_API_BASE_PATH}/embed`, window.location.origin)
embedUrl.search = new URLSearchParams({
protocol: window.location.protocol.replace(':', ''),
host: window.location.host,
token: props.accessToken,
}).toString()
// 复用访问链接的工作流参数,由 URLSearchParams 编码,不再手动编码。
chatUrl.searchParams.forEach((value, key) => embedUrl.searchParams.append(key, value))
return `<script
async
defer
src="${escape(embedUrl.href)}">
<\/script>`
}

if (selectedMode.value === 'mobile') chatUrl.searchParams.set('mode', 'mobile')
return `<iframe
src="${escape(chatUrl.href)}"
style="width: 100%; height: 100%;"
frameborder="0"
allow="microphone">
</iframe>`
})

function open() {
visible.value = true
Expand All @@ -13,8 +55,41 @@ defineExpose({ open })
</script>

<template>
<MkDialog v-model="visible" title="嵌入第三方">
<!-- TODO:按确认后的设计接入嵌入第三方业务。 -->
<p class="text-N600">嵌入第三方配置待接入。</p>
<MkDialog v-model="visible" title="嵌入第三方" :width="840" @closed="selectedMode = 'fullscreen'">
<div class="space-y-4">
<div class="grid grid-cols-3 gap-4">
<template v-for="mode in embedModes" :key="mode.value">
<!-- 切换嵌入模式 -->
<el-card
shadow="never"
class="cursor-pointer text-center"
:class="{ 'border-primary! bg-primary/10!': selectedMode === mode.value }"
role="button"
tabindex="0"
:aria-pressed="selectedMode === mode.value"
@click="selectedMode = mode.value"
@keydown.enter.prevent="selectedMode = mode.value"
@keydown.space.prevent="selectedMode = mode.value"
>
<div class="space-y-2">
<div>{{ mode.label }}</div>
<img :src="mode.image" alt="" class="mx-auto h-28 object-contain" />
</div>
</el-card>
</template>
</div>
<div class="mk-gray-card-lg space-y-2">
<div class="flex-between">
<h6>复制以下代码进行嵌入</h6>
<!-- 复制当前模式的嵌入代码 -->
<MkTooltip content="复制">
<el-button text :disabled="!embedCode" @click="copyText(embedCode)">
<MkIcon name="icon_copy_outlined" />
</el-button>
</MkTooltip>
</div>
<div class="whitespace-pre-wrap break-all">{{ embedCode }}</div>
</div>
</div>
</MkDialog>
</template>
Loading