front/src/views/site/index.vue
zzh 336f8657ee
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m56s
feat: 官网海洋蓝波浪首页与指南分受众,并同步业务页优化
官网换 GradientWaves/TextType 并做性能减负;模块指南拆开发/运营;含营销指南、轨迹单与 H5 价格等改动。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 18:07:41 +08:00

406 lines
11 KiB
Vue

<template>
<div
class="official-site"
:class="{
'is-routes-open': panel === 'routes',
'is-services-open': panel === 'services',
'is-topic-open': panel === 'pricing' || panel === 'about' || panel === 'contact',
}"
>
<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="advisor.askIntent"
@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" mode="out-in">
<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, onBeforeUnmount, onMounted, ref, watch } from 'vue';
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 { officialLoginUrl, warmupOfficialLogin } from './lib/site-urls';
import { shouldOpenWecomDirect, WECOM_KF_URL } from './lib/site-wecom';
import { useSiteAdvisor } from './useSiteAdvisor';
import './styles/vendor.css';
import './styles/icons.css';
import './styles/main.css';
import './styles/ai-advisor.css';
type PanelId = 'routes' | 'services' | 'pricing' | 'about' | 'contact' | null;
const NAV_PANELS: PanelId[] = ['routes', 'services', 'pricing', 'about', 'contact'];
const TITLE = 'IDE - 跨境物流,全球即递';
const DESCRIPTION =
'IDE 面向中小卖家的跨境物流。全球数字化一站式跨境物流服务商。注册登记后即可询价,一口价不砍,专属顾问跟到签收。';
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());
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 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'));
}
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'));
}
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);
}
function openRoutes(dest?: string) {
advisor.clearRoutesPending();
routesDest.value = dest || 'all';
openPanel('routes');
}
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');
advisor.startNewChat();
}
function onNav(id: string) {
closeMobileNav();
if (id === 'home') {
onHome();
return;
}
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 = '';
advisor.ask(payload.text, { intentId: payload.intentId, fromRoutes });
}
function onSubmit() {
const text = draft.value.trim();
if (!text) return;
draft.value = '';
advisor.ask(text);
}
function onChoice(payload: { id: string; label: string; flowId: string }) {
draft.value = '';
advisor.ask(payload.label, { kind: 'choice', flowId: payload.flowId, choice: payload.id });
}
function onSwitchThread(id: string) {
advisor.switchThread(id);
historyOpen.value = false;
}
function openWecom() {
if (shouldOpenWecomDirect()) {
window.location.href = WECOM_KF_URL;
return;
}
wecomOpen.value = true;
}
function onTopicWechat() {
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);
}
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
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`);
}
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);
syncHeaderOffset();
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;
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKeydown);
window.removeEventListener('resize', syncHeaderOffset);
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 {
min-height: 100vh;
}
</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 cubic-bezier(0.22, 1, 0.36, 1);
}
.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.24s cubic-bezier(0.22, 1, 0.36, 1);
}
.site-sheet-enter-from,
.site-sheet-leave-to {
opacity: 0;
transform: translateX(-50%) translateY(-12px) scale(0.985);
}
@media (max-width: 780px) {
.site-sheet-enter-from,
.site-sheet-leave-to {
transform: translateY(18px) scale(1);
}
}
</style>