mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 11:37:20 +08:00
239 lines
6.3 KiB
Vue
239 lines
6.3 KiB
Vue
<script setup>
|
|
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
|
import { getAdRuntime, openAdExternalURL } from "@/services/clientApi";
|
|
|
|
const OPEN_AD_EVENT = "cursor:open-ad";
|
|
const BRIDGE_SOURCE = "cursor-ad";
|
|
|
|
const visible = ref(false);
|
|
const runtimeState = ref(null);
|
|
const iframeSrc = ref("");
|
|
const viewport = ref({
|
|
width: typeof window === "undefined" ? 1024 : window.innerWidth,
|
|
height: typeof window === "undefined" ? 768 : window.innerHeight,
|
|
});
|
|
|
|
const showingHashes = new Set();
|
|
let refreshPending = false;
|
|
let hideTimer = 0;
|
|
|
|
const frameStyle = computed(() => {
|
|
const win = runtimeState.value?.window ?? {};
|
|
const maxWidth = Math.max(220, viewport.value.width - 32);
|
|
const maxHeight = Math.max(160, viewport.value.height - 32);
|
|
const width = Math.min(clampNumber(win.width, 280, 1200, 640), maxWidth);
|
|
const height = Math.min(clampNumber(win.height, 180, 900, 420), maxHeight);
|
|
return {
|
|
width: `${width}px`,
|
|
height: `${height}px`,
|
|
maxWidth: "calc(100vw - 32px)",
|
|
maxHeight: "calc(100vh - 32px)",
|
|
};
|
|
});
|
|
|
|
function asString(value) {
|
|
if (typeof value === "string") {
|
|
return value.trim();
|
|
}
|
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
return String(value);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function asBoolean(value) {
|
|
return value === true || value === "true" || value === 1 || value === "1";
|
|
}
|
|
|
|
function asNumber(value, fallback = 0) {
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
}
|
|
|
|
function clampNumber(value, min, max, fallback) {
|
|
const parsed = asNumber(value, fallback);
|
|
return Math.min(max, Math.max(min, parsed || fallback));
|
|
}
|
|
|
|
function normalizeRuntime(source, preferredSlotId = "") {
|
|
const raw = source && typeof source === "object" ? source : {};
|
|
const slots = Array.isArray(raw.slots) ? raw.slots : [];
|
|
const selectedSlot =
|
|
slots.find((slot) => asString(slot?.id) === asString(preferredSlotId)) ||
|
|
slots[0] ||
|
|
raw;
|
|
const slot = selectedSlot && typeof selectedSlot === "object" ? selectedSlot : {};
|
|
const win = raw.window && typeof raw.window === "object" ? raw.window : {};
|
|
const slotWin = slot.window && typeof slot.window === "object" ? slot.window : win;
|
|
return {
|
|
id: asString(slot.id) || asString(preferredSlotId) || "1",
|
|
available: asBoolean(slot.available),
|
|
enabled: asBoolean(slot.enabled),
|
|
packageHash: asString(slot.packageHash),
|
|
assetBaseURL: asString(slot.assetBaseURL).replace(/\/+$/, ""),
|
|
indexURL: asString(slot.indexURL),
|
|
window: {
|
|
width: Math.round(asNumber(slotWin.width, 640)),
|
|
height: Math.round(asNumber(slotWin.height, 420)),
|
|
},
|
|
};
|
|
}
|
|
|
|
function expectedAdOrigin() {
|
|
const baseURL = runtimeState.value?.assetBaseURL;
|
|
if (!baseURL) {
|
|
return "";
|
|
}
|
|
try {
|
|
return new URL(baseURL).origin;
|
|
} catch (_error) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function canOpen(runtime) {
|
|
if (!runtime?.available || !runtime.enabled) {
|
|
return false;
|
|
}
|
|
return Boolean(runtime.packageHash && runtime.assetBaseURL);
|
|
}
|
|
|
|
async function openCurrentAd(slotId = "") {
|
|
if (visible.value || refreshPending) {
|
|
return;
|
|
}
|
|
refreshPending = true;
|
|
try {
|
|
const nextRuntime = normalizeRuntime(await getAdRuntime(), slotId);
|
|
runtimeState.value = nextRuntime;
|
|
if (canOpen(nextRuntime)) {
|
|
await showAd(nextRuntime);
|
|
}
|
|
} catch (_error) {
|
|
// 广告入口失败不影响主界面。
|
|
} finally {
|
|
refreshPending = false;
|
|
}
|
|
}
|
|
|
|
async function showAd(runtime) {
|
|
const hash = runtime.packageHash;
|
|
if (showingHashes.has(hash)) {
|
|
return;
|
|
}
|
|
showingHashes.add(hash);
|
|
try {
|
|
const indexURL = runtime.indexURL || `${runtime.assetBaseURL}/index.html`;
|
|
const separator = indexURL.includes("?") ? "&" : "?";
|
|
iframeSrc.value = `${indexURL}${separator}hash=${encodeURIComponent(hash)}&ts=${Date.now()}`;
|
|
visible.value = true;
|
|
} finally {
|
|
showingHashes.delete(hash);
|
|
}
|
|
}
|
|
|
|
function closeAd() {
|
|
visible.value = false;
|
|
if (hideTimer) {
|
|
window.clearTimeout(hideTimer);
|
|
}
|
|
hideTimer = window.setTimeout(() => {
|
|
iframeSrc.value = "";
|
|
}, 260);
|
|
}
|
|
|
|
function handleMessage(event) {
|
|
const origin = expectedAdOrigin();
|
|
if (origin && event.origin !== origin) {
|
|
return;
|
|
}
|
|
const data = event.data && typeof event.data === "object" ? event.data : {};
|
|
if (data.source !== BRIDGE_SOURCE) {
|
|
return;
|
|
}
|
|
if (data.type === "close") {
|
|
closeAd();
|
|
return;
|
|
}
|
|
if (data.type === "openExternal") {
|
|
const targetURL = asString(data.url);
|
|
if (targetURL) {
|
|
void openAdExternalURL(targetURL).catch(() => {});
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleOpenRequested(event) {
|
|
void openCurrentAd(asString(event?.detail?.slotId));
|
|
}
|
|
|
|
function updateViewport() {
|
|
viewport.value = {
|
|
width: window.innerWidth,
|
|
height: window.innerHeight,
|
|
};
|
|
}
|
|
|
|
onMounted(() => {
|
|
window.addEventListener("message", handleMessage);
|
|
window.addEventListener(OPEN_AD_EVENT, handleOpenRequested);
|
|
window.addEventListener("resize", updateViewport);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
if (hideTimer) {
|
|
window.clearTimeout(hideTimer);
|
|
}
|
|
window.removeEventListener("message", handleMessage);
|
|
window.removeEventListener(OPEN_AD_EVENT, handleOpenRequested);
|
|
window.removeEventListener("resize", updateViewport);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport to="body">
|
|
<Transition name="modal-mask">
|
|
<div
|
|
v-show="visible"
|
|
class="modal-mask-layer fixed inset-0 z-999 flex items-center justify-center bg-black/50 p-4"
|
|
@click.self="closeAd"
|
|
>
|
|
<Transition name="ad-frame">
|
|
<iframe
|
|
v-show="visible && iframeSrc"
|
|
:src="iframeSrc"
|
|
:style="frameStyle"
|
|
class="block overflow-hidden rounded-none border-none bg-transparent shadow-[0_25px_50px_-12px_rgba(0,0,0,0.6)]"
|
|
sandbox="allow-scripts allow-forms allow-same-origin"
|
|
title="Advertisement"
|
|
/>
|
|
</Transition>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.modal-mask-enter-active,
|
|
.modal-mask-leave-active {
|
|
transition: opacity 0.25s ease, backdrop-filter 0.25s ease;
|
|
}
|
|
|
|
.modal-mask-enter-from,
|
|
.modal-mask-leave-to {
|
|
opacity: 0;
|
|
backdrop-filter: blur(0);
|
|
}
|
|
|
|
.ad-frame-enter-active,
|
|
.ad-frame-leave-active {
|
|
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
}
|
|
|
|
.ad-frame-enter-from,
|
|
.ad-frame-leave-to {
|
|
opacity: 0;
|
|
transform: scale(0.96) translateY(-8px);
|
|
}
|
|
</style>
|