All checks were successful
Build and Deploy Front / deploy (push) Successful in 9m42s
Co-authored-by: Cursor <cursoragent@cursor.com>
557 lines
16 KiB
Vue
557 lines
16 KiB
Vue
<template>
|
||
<div
|
||
ref="glassSceneEl"
|
||
class="official-site lg-scene"
|
||
:class="{
|
||
'is-routes-open': panel === 'routes',
|
||
'is-services-open': panel === 'services',
|
||
'is-topic-open': panel === 'pricing' || panel === 'about' || panel === 'contact',
|
||
'is-glass-ready': glass.ready.value,
|
||
'is-glass-fallback': glass.fallback.value,
|
||
}"
|
||
@click.capture="onOfficialLoginCapture"
|
||
>
|
||
<!--
|
||
lg-content 会被 liquid-glass 在 resize 时改写 left/right/宽高;
|
||
波浪放在固定视口 __paint 层,避免缩窗后右侧留白。
|
||
-->
|
||
<div
|
||
ref="glassContentEl"
|
||
class="ai-shell-bg lg-content"
|
||
data-layout="overlay"
|
||
aria-hidden="true"
|
||
>
|
||
<div class="ai-shell-bg__paint">
|
||
<GradientWaves
|
||
class="ai-shell-bg__waves"
|
||
horizon-color="#023e8a"
|
||
wave-color="#0077b6"
|
||
crest-color="#caf0f8"
|
||
:speed="0.34"
|
||
:amplitude="3.3"
|
||
:wave-scale="0.72"
|
||
:wave-ratio="0.82"
|
||
:swell="42"
|
||
:turbulence="26"
|
||
:tilt="1.08"
|
||
:zoom="0.92"
|
||
:height="4.8"
|
||
:fog-depth="22"
|
||
detail="low"
|
||
:brightness="1.08"
|
||
:opacity="1.0"
|
||
:mouse-interaction="true"
|
||
:parallax-strength="0.4"
|
||
:grain="false"
|
||
:fps="30"
|
||
:render-scale="0.72"
|
||
:max-dpr="1.25"
|
||
/>
|
||
<div class="ai-shell-bg__veil" aria-hidden="true"></div>
|
||
</div>
|
||
</div>
|
||
<SiteHeader
|
||
:nav="nav"
|
||
:is-zh="isZh"
|
||
:login-url="loginUrl"
|
||
@home="onHome"
|
||
@nav="onNav"
|
||
@toggle-lang="toggleLang"
|
||
@open-nav="openMobileNav"
|
||
@close-nav="closeMobileNav"
|
||
@warm-login="warmupOfficialLogin"
|
||
/>
|
||
<div id="official-site-mount">
|
||
<SiteAdvisorShell
|
||
v-model="draft"
|
||
:is-zh="isZh"
|
||
:chatting="chatting"
|
||
:history-open="historyOpen"
|
||
:busy="busy"
|
||
:login-url="loginUrl"
|
||
:show-new-chat-tip="showNewChatTip"
|
||
:messages="messages"
|
||
:typing="typing"
|
||
:threads="threads"
|
||
:active-thread-id="activeThreadId"
|
||
:routes-pending="routesPending"
|
||
@submit="onSubmit"
|
||
@intent="onIntent"
|
||
@human="onNav('contact')"
|
||
@wechat="openWecom"
|
||
@prefill="draft = $event"
|
||
@new-chat="onHome"
|
||
@toggle-history="historyOpen = !historyOpen"
|
||
@close-history="historyOpen = false"
|
||
@warm-login="warmupOfficialLogin"
|
||
@open-routes="openRoutes"
|
||
@open-panel="openPanel"
|
||
@switch-thread="onSwitchThread"
|
||
@choice="onChoice"
|
||
/>
|
||
</div>
|
||
<Transition name="site-fade">
|
||
<button
|
||
v-if="panel"
|
||
type="button"
|
||
class="site-panel-backdrop"
|
||
aria-hidden="true"
|
||
tabindex="-1"
|
||
@click="closePanel"
|
||
></button>
|
||
</Transition>
|
||
<Transition name="site-sheet">
|
||
<SiteRoutesPanel
|
||
v-if="panel === 'routes'"
|
||
key="routes"
|
||
:is-zh="isZh"
|
||
:dest-id="routesDest"
|
||
@close="closePanel"
|
||
@ask="askAdvisor"
|
||
/>
|
||
<SiteServicesPanel
|
||
v-else-if="panel === 'services'"
|
||
key="services"
|
||
:is-zh="isZh"
|
||
@close="closePanel"
|
||
@ask="askAdvisor"
|
||
/>
|
||
<SiteTopicPanel
|
||
v-else-if="panel === 'pricing' || panel === 'about' || panel === 'contact'"
|
||
:key="panel"
|
||
:topic="panel"
|
||
:is-zh="isZh"
|
||
:wecom-url="WECOM_KF_URL"
|
||
@close="closePanel"
|
||
@ask="askAdvisor"
|
||
@wechat="onTopicWechat"
|
||
@open-panel="openPanel"
|
||
/>
|
||
</Transition>
|
||
<SiteWecomModal :open="wecomOpen" :is-zh="isZh" :url="WECOM_KF_URL" @close="wecomOpen = false" />
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import SiteHeader from './SiteHeader.vue';
|
||
import SiteAdvisorShell from './SiteAdvisorShell.vue';
|
||
import SiteRoutesPanel from './SiteRoutesPanel.vue';
|
||
import SiteServicesPanel from './SiteServicesPanel.vue';
|
||
import SiteTopicPanel from './SiteTopicPanel.vue';
|
||
import SiteWecomModal from './SiteWecomModal.vue';
|
||
import GradientWaves from './GradientWaves.vue';
|
||
import {
|
||
goOfficialLoginFromHref,
|
||
officialLoginUrl,
|
||
warmupOfficialLogin,
|
||
} from './lib/site-urls';
|
||
import { shouldOpenWecomDirect, WECOM_KF_URL } from './lib/site-wecom';
|
||
import { provideSiteLiquidGlass } from './lib/site-liquid-glass';
|
||
import { useSiteAdvisor } from './useSiteAdvisor';
|
||
import { trackSiteEvent } from '@/utils/ga';
|
||
import './styles/vendor.css';
|
||
import './styles/icons.css';
|
||
import './styles/main.css';
|
||
import './styles/ai-advisor.css';
|
||
|
||
const glass = provideSiteLiquidGlass();
|
||
const glassSceneEl = ref<HTMLElement | null>(null);
|
||
const glassContentEl = ref<HTMLElement | null>(null);
|
||
|
||
type PanelId = 'routes' | 'services' | 'pricing' | 'about' | 'contact' | null;
|
||
const NAV_PANELS: PanelId[] = ['routes', 'services', 'pricing', 'about', 'contact'];
|
||
|
||
const TITLE = 'IDE - 跨境物流,全球即递';
|
||
const DESCRIPTION =
|
||
'IDE 面向中小卖家的跨境物流。全球数字化一站式跨境物流服务商。注册登记后即可询价,一口价不砍,专属顾问跟到签收。';
|
||
|
||
const router = useRouter();
|
||
const lang = ref<'zh' | 'en'>('zh');
|
||
const nav = ref('home');
|
||
const panel = ref<PanelId>(null);
|
||
const routesDest = ref('all');
|
||
const draft = ref('');
|
||
const historyOpen = ref(false);
|
||
const wecomOpen = ref(false);
|
||
const isZh = computed(() => lang.value === 'zh');
|
||
const loginUrl = computed(() => officialLoginUrl());
|
||
|
||
/** 顾问卡片等其它「去登录」链接统一走 SPA 翻页 */
|
||
function onOfficialLoginCapture(e: MouseEvent) {
|
||
const target = e.target as Element | null;
|
||
const anchor = target?.closest?.('a[href]') as HTMLAnchorElement | null;
|
||
if (!anchor) return;
|
||
const href = anchor.getAttribute('href') || '';
|
||
if (!href.includes('from=official-site')) return;
|
||
trackSiteEvent('home_login_click', {
|
||
link_role: anchor.classList.contains('site-nav-login')
|
||
? 'nav_login'
|
||
: anchor.classList.contains('ai-welcome-cta-link')
|
||
? 'hero_cta'
|
||
: 'other',
|
||
});
|
||
if (anchor.classList.contains('site-nav-login')) return;
|
||
goOfficialLoginFromHref(router, href, e);
|
||
}
|
||
|
||
function focusComposer() {
|
||
const input = document.querySelector('.ai-composer-input') as HTMLTextAreaElement | null;
|
||
input?.focus();
|
||
}
|
||
|
||
const advisor = useSiteAdvisor({
|
||
lang,
|
||
openServices: () => openPanel('services'),
|
||
focusComposer,
|
||
});
|
||
|
||
const {
|
||
messages,
|
||
threads,
|
||
activeThreadId,
|
||
busy,
|
||
typing,
|
||
chatting,
|
||
showNewChatTip,
|
||
routesPending,
|
||
} = advisor;
|
||
|
||
let previousTitle = '';
|
||
|
||
function readLang(): 'zh' | 'en' {
|
||
try {
|
||
const saved = localStorage.getItem('ide-language');
|
||
if (saved === 'zh' || saved === 'en') return saved;
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return 'zh';
|
||
}
|
||
|
||
function ensureMeta(name: string, content: string) {
|
||
let node = document.querySelector(`meta[name="${name}"]`);
|
||
if (!node) {
|
||
node = document.createElement('meta');
|
||
node.setAttribute('name', name);
|
||
document.head.appendChild(node);
|
||
}
|
||
node.setAttribute('content', content);
|
||
}
|
||
|
||
function ensureOg(property: string, content: string) {
|
||
let node = document.querySelector(`meta[property="${property}"]`);
|
||
if (!node) {
|
||
node = document.createElement('meta');
|
||
node.setAttribute('property', property);
|
||
document.head.appendChild(node);
|
||
}
|
||
node.setAttribute('content', content);
|
||
}
|
||
|
||
function applyMeta() {
|
||
document.title = TITLE;
|
||
ensureMeta('description', DESCRIPTION);
|
||
ensureMeta(
|
||
'keywords',
|
||
'IDE, 跨境物流, 中小卖家, 9610, 疑难单, 出口退税, 欧美专线, 东南亚专线',
|
||
);
|
||
const origin = window.location.origin;
|
||
ensureOg('og:title', TITLE);
|
||
ensureOg('og:description', DESCRIPTION);
|
||
ensureOg('og:image', `${origin}/site/img/og-cover.png`);
|
||
ensureOg('og:url', `${origin}/`);
|
||
ensureOg('og:type', 'website');
|
||
}
|
||
|
||
function onMobileNavBackdrop(e: MouseEvent) {
|
||
const navEl = document.getElementById('navbar');
|
||
if (!navEl || e.target !== navEl) return;
|
||
closeMobileNav();
|
||
}
|
||
|
||
function openMobileNav() {
|
||
document.body.classList.add('mobile-nav-active');
|
||
document.querySelectorAll('.mobile-nav-show').forEach((n) => n.classList.add('d-none'));
|
||
document.querySelectorAll('.mobile-nav-hide').forEach((n) => n.classList.remove('d-none'));
|
||
document.getElementById('navbar')?.addEventListener('click', onMobileNavBackdrop);
|
||
}
|
||
|
||
function closeMobileNav() {
|
||
document.body.classList.remove('mobile-nav-active');
|
||
document.querySelectorAll('.mobile-nav-show').forEach((n) => n.classList.remove('d-none'));
|
||
document.querySelectorAll('.mobile-nav-hide').forEach((n) => n.classList.add('d-none'));
|
||
document.getElementById('navbar')?.removeEventListener('click', onMobileNavBackdrop);
|
||
}
|
||
|
||
function syncHash(id: string) {
|
||
const next = id && id !== 'home' ? `#${id}` : location.pathname + location.search;
|
||
if (id === 'home') {
|
||
if (location.hash) history.replaceState(null, '', location.pathname + location.search);
|
||
return;
|
||
}
|
||
if (location.hash !== next) history.replaceState(null, '', next);
|
||
}
|
||
|
||
function openPanel(id: Exclude<PanelId, null>) {
|
||
closeMobileNav();
|
||
historyOpen.value = false;
|
||
if (id !== 'routes') routesDest.value = 'all';
|
||
panel.value = id;
|
||
nav.value = id;
|
||
syncHash(id);
|
||
trackSiteEvent('home_panel_open', { panel_id: id });
|
||
}
|
||
|
||
function openRoutes(dest?: string) {
|
||
const destId = dest || 'all';
|
||
advisor.clearRoutesPending();
|
||
routesDest.value = destId;
|
||
trackSiteEvent('home_dest_click', { dest_id: destId });
|
||
openPanel('routes');
|
||
}
|
||
|
||
function onIntent(intentId: string) {
|
||
trackSiteEvent('home_intent_click', { intent_id: intentId });
|
||
advisor.askIntent(intentId);
|
||
}
|
||
|
||
function closePanel(id?: string) {
|
||
if (id && panel.value && panel.value !== id) return;
|
||
if (!panel.value) return;
|
||
panel.value = null;
|
||
nav.value = 'home';
|
||
syncHash('home');
|
||
}
|
||
|
||
function onHome() {
|
||
closeMobileNav();
|
||
closePanel();
|
||
historyOpen.value = false;
|
||
nav.value = 'home';
|
||
syncHash('home');
|
||
trackSiteEvent('home_nav', { nav_id: 'home' });
|
||
advisor.startNewChat();
|
||
}
|
||
|
||
function onNav(id: string) {
|
||
closeMobileNav();
|
||
if (id === 'home') {
|
||
onHome();
|
||
return;
|
||
}
|
||
trackSiteEvent('home_nav', { nav_id: id });
|
||
if (NAV_PANELS.includes(id as PanelId)) {
|
||
openPanel(id as Exclude<PanelId, null>);
|
||
return;
|
||
}
|
||
closePanel();
|
||
nav.value = id;
|
||
syncHash(id);
|
||
}
|
||
|
||
function askAdvisor(payload: { text: string; intentId: string }) {
|
||
const fromRoutes = panel.value === 'routes';
|
||
closePanel();
|
||
draft.value = '';
|
||
trackSiteEvent('home_ask', {
|
||
intent_id: payload.intentId,
|
||
from_routes: fromRoutes,
|
||
});
|
||
advisor.ask(payload.text, { intentId: payload.intentId, fromRoutes });
|
||
}
|
||
|
||
function onSubmit() {
|
||
const text = draft.value.trim();
|
||
if (!text) return;
|
||
draft.value = '';
|
||
trackSiteEvent('home_ask', { intent_id: 'free_text' });
|
||
advisor.ask(text);
|
||
}
|
||
|
||
function onChoice(payload: { id: string; label: string; flowId: string }) {
|
||
draft.value = '';
|
||
trackSiteEvent('home_choice', {
|
||
choice_id: payload.id,
|
||
flow_id: payload.flowId,
|
||
});
|
||
advisor.ask(payload.label, { kind: 'choice', flowId: payload.flowId, choice: payload.id });
|
||
}
|
||
|
||
function onSwitchThread(id: string) {
|
||
advisor.switchThread(id);
|
||
historyOpen.value = false;
|
||
}
|
||
|
||
function openWecom() {
|
||
trackSiteEvent('home_wecom_click', { entry: 'advisor' });
|
||
if (shouldOpenWecomDirect()) {
|
||
window.location.href = WECOM_KF_URL;
|
||
return;
|
||
}
|
||
wecomOpen.value = true;
|
||
}
|
||
|
||
function onTopicWechat() {
|
||
trackSiteEvent('home_wecom_click', { entry: 'topic' });
|
||
if (shouldOpenWecomDirect()) {
|
||
window.location.href = WECOM_KF_URL;
|
||
return;
|
||
}
|
||
wecomOpen.value = true;
|
||
}
|
||
|
||
function toggleLang() {
|
||
const next = lang.value === 'zh' ? 'en' : 'zh';
|
||
lang.value = next;
|
||
localStorage.setItem('ide-language', next);
|
||
trackSiteEvent('home_lang_toggle', { lang: next });
|
||
}
|
||
|
||
function onKeydown(e: KeyboardEvent) {
|
||
if (e.key === 'Escape') {
|
||
if (document.body.classList.contains('mobile-nav-active')) {
|
||
closeMobileNav();
|
||
return;
|
||
}
|
||
closePanel();
|
||
historyOpen.value = false;
|
||
wecomOpen.value = false;
|
||
}
|
||
}
|
||
|
||
function syncHeaderOffset() {
|
||
const header = document.getElementById('header');
|
||
const height = header ? Math.round(header.getBoundingClientRect().height) : 72;
|
||
document.documentElement.style.setProperty('--ai-header-h', `${height}px`);
|
||
}
|
||
|
||
function syncVisualViewport() {
|
||
const vv = window.visualViewport;
|
||
const height = Math.round(vv?.height ?? window.innerHeight);
|
||
const offsetTop = Math.round(vv?.offsetTop ?? 0);
|
||
document.documentElement.style.setProperty('--ai-vv-height', `${height}px`);
|
||
document.documentElement.style.setProperty('--ai-vv-offset-top', `${offsetTop}px`);
|
||
}
|
||
|
||
watch(panel, (id) => {
|
||
document.body.classList.toggle('site-panel-routes', id === 'routes');
|
||
document.body.classList.toggle('site-panel-services', id === 'services');
|
||
});
|
||
|
||
onMounted(() => {
|
||
previousTitle = document.title;
|
||
lang.value = readLang();
|
||
applyMeta();
|
||
document.documentElement.classList.add('command-home');
|
||
document.body.classList.add('command-home');
|
||
window.addEventListener('keydown', onKeydown);
|
||
window.addEventListener('resize', syncHeaderOffset);
|
||
window.addEventListener('resize', syncVisualViewport);
|
||
window.visualViewport?.addEventListener('resize', syncVisualViewport);
|
||
window.visualViewport?.addEventListener('scroll', syncVisualViewport);
|
||
syncHeaderOffset();
|
||
syncVisualViewport();
|
||
const hash = (window.location.hash || '').replace('#', '');
|
||
if (hash === '9610' || hash === 'services') openPanel('services');
|
||
else if (NAV_PANELS.includes(hash as PanelId)) openPanel(hash as Exclude<PanelId, null>);
|
||
else if (hash) nav.value = hash;
|
||
void nextTick(() => {
|
||
if (glassSceneEl.value && glassContentEl.value) {
|
||
glass.init(glassSceneEl.value, glassContentEl.value);
|
||
}
|
||
});
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
glass.dispose();
|
||
window.removeEventListener('keydown', onKeydown);
|
||
window.removeEventListener('resize', syncHeaderOffset);
|
||
window.removeEventListener('resize', syncVisualViewport);
|
||
window.visualViewport?.removeEventListener('resize', syncVisualViewport);
|
||
window.visualViewport?.removeEventListener('scroll', syncVisualViewport);
|
||
document.body.classList.remove('site-panel-routes', 'site-panel-services');
|
||
document.documentElement.classList.remove('command-home', 'app-boot-site');
|
||
closeMobileNav();
|
||
if (previousTitle) document.title = previousTitle;
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.official-site {
|
||
position: relative;
|
||
min-height: 100vh;
|
||
isolation: isolate;
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
body.command-home #app {
|
||
background: transparent;
|
||
min-height: 100%;
|
||
}
|
||
|
||
.site-panel-backdrop {
|
||
display: block;
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 995;
|
||
border: none;
|
||
background: color-mix(in srgb, var(--ai-bg, #f0f3f4) 55%, rgba(18, 24, 32, 0.18));
|
||
backdrop-filter: blur(6px) saturate(140%);
|
||
-webkit-backdrop-filter: blur(6px) saturate(140%);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.site-fade-enter-active,
|
||
.site-fade-leave-active {
|
||
transition: opacity 0.22s ease;
|
||
}
|
||
|
||
.site-fade-enter-from,
|
||
.site-fade-leave-to {
|
||
opacity: 0;
|
||
}
|
||
|
||
.site-sheet-enter-active,
|
||
.site-sheet-leave-active {
|
||
transition:
|
||
opacity 0.24s cubic-bezier(0.22, 1, 0.36, 1),
|
||
transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
||
}
|
||
|
||
.site-sheet-leave-active {
|
||
pointer-events: none;
|
||
}
|
||
|
||
.site-sheet-enter-from {
|
||
opacity: 0;
|
||
transform: translateX(-50%) translateY(-8px);
|
||
}
|
||
|
||
.site-sheet-leave-to {
|
||
opacity: 0;
|
||
transform: translateX(-50%) translateY(-4px);
|
||
}
|
||
|
||
@media (max-width: 780px) {
|
||
.site-sheet-enter-from {
|
||
transform: translateY(12px);
|
||
}
|
||
|
||
.site-sheet-leave-to {
|
||
transform: translateY(8px);
|
||
}
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.site-fade-enter-active,
|
||
.site-fade-leave-active,
|
||
.site-sheet-enter-active,
|
||
.site-sheet-leave-active {
|
||
transition-duration: 0.01ms;
|
||
}
|
||
}
|
||
</style>
|