|
|
@@ -1,6 +1,9 @@
|
|
|
<script setup>
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
|
import lottie from 'lottie-web'
|
|
|
+import { clearSession, loadSession, saveSession } from './services/auth'
|
|
|
+
|
|
|
+const AUTH_MOCK_ENABLED = import.meta.env.VITE_AUTH_MOCK !== 'false'
|
|
|
|
|
|
const activeQr = ref(null)
|
|
|
const previewRefs = ref({})
|
|
|
@@ -8,7 +11,10 @@ const lottieInstances = new Map()
|
|
|
const currentLocale = ref('en')
|
|
|
const languageMenuOpen = ref(false)
|
|
|
const languageMenuRef = ref(null)
|
|
|
+const profileMenuOpen = ref(false)
|
|
|
const loginModalOpen = ref(false)
|
|
|
+const logoutModalOpen = ref(false)
|
|
|
+const session = ref(loadSession())
|
|
|
|
|
|
const languageOptions = [
|
|
|
{ value: 'en', label: 'English' },
|
|
|
@@ -90,6 +96,7 @@ const scenes = Object.entries(sceneDataModules)
|
|
|
const heroScene = computed(() => scenes.find((scene) => scene.id === '自锁带点动') ?? null)
|
|
|
const interactiveScenes = computed(() => scenes.filter((scene) => scene.id === '家庭双控'))
|
|
|
const simulationScenes = computed(() => scenes.filter((scene) => scene.id === '工业自锁'))
|
|
|
+const isAuthenticated = computed(() => Boolean(session.value?.token))
|
|
|
|
|
|
function setPreviewRef(id, el) {
|
|
|
if (el) {
|
|
|
@@ -146,18 +153,79 @@ function setLocale(locale) {
|
|
|
|
|
|
function openLoginModal() {
|
|
|
loginModalOpen.value = true
|
|
|
+ logoutModalOpen.value = false
|
|
|
+ profileMenuOpen.value = false
|
|
|
}
|
|
|
|
|
|
function closeLoginModal() {
|
|
|
loginModalOpen.value = false
|
|
|
}
|
|
|
|
|
|
+function openLogoutModal() {
|
|
|
+ logoutModalOpen.value = true
|
|
|
+ profileMenuOpen.value = false
|
|
|
+}
|
|
|
+
|
|
|
+function closeLogoutModal() {
|
|
|
+ logoutModalOpen.value = false
|
|
|
+}
|
|
|
+
|
|
|
+function openProfileMenu() {
|
|
|
+ if (!isAuthenticated.value) return
|
|
|
+ profileMenuOpen.value = true
|
|
|
+}
|
|
|
+
|
|
|
+function closeProfileMenu() {
|
|
|
+ profileMenuOpen.value = false
|
|
|
+}
|
|
|
+
|
|
|
function handleDocumentClick(event) {
|
|
|
if (!languageMenuRef.value?.contains(event.target)) {
|
|
|
languageMenuOpen.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function applyMockLogin(provider) {
|
|
|
+ const nextSession = {
|
|
|
+ token: `mock-${provider}-token`,
|
|
|
+ userId: provider === 'google' ? '10001' : '10002',
|
|
|
+ isNewUser: false,
|
|
|
+ provider,
|
|
|
+ nickname: 'Andy',
|
|
|
+ avatarUrl: 'https://www.figma.com/api/mcp/asset/ad158d05-a000-4d0d-b32d-5e1881f01559',
|
|
|
+ email: '56998882589@gmail.com',
|
|
|
+ }
|
|
|
+ saveSession(nextSession)
|
|
|
+ session.value = nextSession
|
|
|
+ closeLoginModal()
|
|
|
+ openProfileMenu()
|
|
|
+}
|
|
|
+
|
|
|
+function handleGoogleClick() {
|
|
|
+ if (!AUTH_MOCK_ENABLED) return
|
|
|
+ applyMockLogin('google')
|
|
|
+}
|
|
|
+
|
|
|
+function handleAppleClick() {
|
|
|
+ if (!AUTH_MOCK_ENABLED) return
|
|
|
+ applyMockLogin('apple')
|
|
|
+}
|
|
|
+
|
|
|
+function handleProfileLogoutClick() {
|
|
|
+ closeProfileMenu()
|
|
|
+ openLogoutModal()
|
|
|
+}
|
|
|
+
|
|
|
+function confirmLogout() {
|
|
|
+ session.value = null
|
|
|
+ clearSession()
|
|
|
+ closeLogoutModal()
|
|
|
+}
|
|
|
+
|
|
|
+function useProfileLanguage() {
|
|
|
+ setLocale('en')
|
|
|
+}
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
mountAllAnimations()
|
|
|
document.addEventListener('click', handleDocumentClick)
|
|
|
@@ -241,6 +309,15 @@ const copy = {
|
|
|
modalSuffix: '. New users will be registered automatically.',
|
|
|
modalGoogle: 'Continue with Google',
|
|
|
modalApple: 'Continue with Apple',
|
|
|
+ logoutTitle: 'Are you sure you want to log out?',
|
|
|
+ logoutBody: 'Do you want to log out of Simubus as 3180349346@qq.com?',
|
|
|
+ logoutAction: 'Log Out Of The Account',
|
|
|
+ cancel: 'Cancel',
|
|
|
+ profileName: 'Andy',
|
|
|
+ profileEmail: '56998882589@gmail.com',
|
|
|
+ language: 'English',
|
|
|
+ footerTerms: 'Terms',
|
|
|
+ footerPrivacy: 'Privacy Policy',
|
|
|
},
|
|
|
zh: {
|
|
|
login: '登录',
|
|
|
@@ -263,16 +340,16 @@ const copy = {
|
|
|
temperatureController: '温度控制器',
|
|
|
automaticTransferSwitch: '双电源自动转换开关',
|
|
|
},
|
|
|
- benefitsTitle: '为什么选择Simubus',
|
|
|
+ benefitsTitle: '为什么选择 Simubus',
|
|
|
benefits: {
|
|
|
- secure: { title: '安全可靠', body: '你的项目会安全存储在云端,并定期备份。您的数据隐私是我们的首要任务。' },
|
|
|
+ secure: { title: '安全可靠', body: '你的项目会安全存储在云端,并定期备份。你的数据隐私是我们的首要任务。' },
|
|
|
professional: { title: '职业级', body: '行业标准的仿真引擎为教育和专业用途提供准确结果。' },
|
|
|
crossPlatform: { title: '跨平台', body: '在桌面、平板和移动设备上无缝运行。你的模拟会跟随你走到哪里。' },
|
|
|
cloud: { title: '基于云平台', body: '无需安装。随时随地,直接从浏览器访问强大的模拟工具。' },
|
|
|
},
|
|
|
footerSlogan: '你的虚拟电气实验室。',
|
|
|
quickLinksTitle: '快速链接',
|
|
|
- quickLinks: ['首页', '隐私政策', '使用条款', '订阅条款', '下载APP'],
|
|
|
+ quickLinks: ['首页', '隐私政策', '使用条款', '订阅条款', '下载 APP'],
|
|
|
contactTitle: '联系我们',
|
|
|
contactBody: '有问题或建议吗?我们很期待听到你的声音。',
|
|
|
copyright: 'Copyright © 2025 Simubus 保留所有权利。',
|
|
|
@@ -288,6 +365,15 @@ const copy = {
|
|
|
modalSuffix: '。新用户将自动注册。',
|
|
|
modalGoogle: '使用 Google 继续',
|
|
|
modalApple: '使用 Apple 继续',
|
|
|
+ logoutTitle: '确定要退出登录吗?',
|
|
|
+ logoutBody: '你要退出 3180349346@qq.com 的 Simubus 账号吗?',
|
|
|
+ logoutAction: '退出当前账号',
|
|
|
+ cancel: '取消',
|
|
|
+ profileName: 'Andy',
|
|
|
+ profileEmail: '56998882589@gmail.com',
|
|
|
+ language: '中文',
|
|
|
+ footerTerms: '使用条款',
|
|
|
+ footerPrivacy: '隐私政策',
|
|
|
},
|
|
|
}
|
|
|
|
|
|
@@ -301,39 +387,76 @@ const messages = computed(() => copy[currentLocale.value])
|
|
|
<img class="hero-map" :src="figma.map" alt="" data-node-id="2573:4526" />
|
|
|
</div>
|
|
|
|
|
|
- <header class="figma-header container">
|
|
|
- <div class="figma-logo" data-node-id="3059:7206">
|
|
|
- <img class="figma-logo-icon" :src="figma.logoIcon" alt="" />
|
|
|
- <img class="figma-logo-text" :src="figma.logoText" alt="Simubus" />
|
|
|
- </div>
|
|
|
+ <header class="figma-header">
|
|
|
+ <div class="container figma-header__inner">
|
|
|
+ <div class="figma-logo" data-node-id="3059:7206">
|
|
|
+ <img class="figma-logo-icon" :src="figma.logoIcon" alt="" />
|
|
|
+ <img class="figma-logo-text" :src="figma.logoText" alt="Simubus" />
|
|
|
+ </div>
|
|
|
|
|
|
- <div class="header-actions">
|
|
|
- <button class="login-btn" type="button" @click="openLoginModal">{{ messages.login }}</button>
|
|
|
-
|
|
|
- <div ref="languageMenuRef" class="language-menu">
|
|
|
- <button
|
|
|
- class="language-btn"
|
|
|
- type="button"
|
|
|
- aria-haspopup="true"
|
|
|
- :aria-expanded="languageMenuOpen ? 'true' : 'false'"
|
|
|
- @click.stop="toggleLanguageMenu"
|
|
|
- >
|
|
|
- <span>{{ messages.languageButton }}</span>
|
|
|
- <span class="language-caret"></span>
|
|
|
- </button>
|
|
|
-
|
|
|
- <div v-if="languageMenuOpen" class="language-dropdown">
|
|
|
- <button
|
|
|
- v-for="option in languageOptions"
|
|
|
- :key="option.value"
|
|
|
- class="language-option"
|
|
|
- :class="{ active: currentLocale === option.value }"
|
|
|
- type="button"
|
|
|
- @click="setLocale(option.value)"
|
|
|
- >
|
|
|
- {{ option.label }}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
+ <div class="header-actions">
|
|
|
+ <template v-if="isAuthenticated">
|
|
|
+ <div class="header-pro-badge">PRO</div>
|
|
|
+ <div class="header-profile-menu" @mouseenter="openProfileMenu" @mouseleave="closeProfileMenu">
|
|
|
+ <button class="header-avatar-button" type="button" @click="openProfileMenu">
|
|
|
+ <img class="header-avatar-image" :src="session?.avatarUrl || 'https://www.figma.com/api/mcp/asset/ad158d05-a000-4d0d-b32d-5e1881f01559'" alt="User avatar" />
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <div v-if="profileMenuOpen" class="header-profile-dropdown">
|
|
|
+ <div class="header-profile-dropdown__card">
|
|
|
+ <div class="login-modal__profile-avatar-wrap">
|
|
|
+ <img class="login-modal__profile-avatar" :src="session?.avatarUrl || 'https://www.figma.com/api/mcp/asset/ad158d05-a000-4d0d-b32d-5e1881f01559'" alt="User avatar" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <h2 class="login-modal__profile-name">{{ session?.nickname || messages.profileName }}</h2>
|
|
|
+ <p class="login-modal__profile-email">{{ session?.email || messages.profileEmail }}</p>
|
|
|
+
|
|
|
+ <button class="login-modal__ghost-button" type="button" @click="handleProfileLogoutClick">{{ messages.logoutAction }}</button>
|
|
|
+ <button class="login-modal__ghost-button login-modal__ghost-button--language" type="button" @click="useProfileLanguage">
|
|
|
+ <span>{{ messages.language }}</span>
|
|
|
+ <img class="login-modal__chevron" src="https://www.figma.com/api/mcp/asset/3bc941af-ef23-4f72-85ce-604343b147f1" alt="Open language menu" />
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <p class="login-modal__footer-links">
|
|
|
+ <a href="/">{{ messages.footerTerms }}</a>
|
|
|
+ <span>·</span>
|
|
|
+ <a href="/">{{ messages.footerPrivacy }}</a>
|
|
|
+ <span>·</span>
|
|
|
+ <a href="/">{{ messages.footerPrivacy }}</a>
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <button class="login-btn" type="button" @click="openLoginModal">{{ messages.login }}</button>
|
|
|
+
|
|
|
+ <div ref="languageMenuRef" class="language-menu">
|
|
|
+ <button
|
|
|
+ class="language-btn"
|
|
|
+ type="button"
|
|
|
+ aria-haspopup="true"
|
|
|
+ :aria-expanded="languageMenuOpen ? 'true' : 'false'"
|
|
|
+ @click.stop="toggleLanguageMenu"
|
|
|
+ >
|
|
|
+ <span>{{ messages.languageButton }}</span>
|
|
|
+ <span class="language-caret"></span>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <div v-if="languageMenuOpen" class="language-dropdown">
|
|
|
+ <button
|
|
|
+ v-for="option in languageOptions"
|
|
|
+ :key="option.value"
|
|
|
+ class="language-option"
|
|
|
+ :class="{ active: currentLocale === option.value }"
|
|
|
+ type="button"
|
|
|
+ @click="setLocale(option.value)"
|
|
|
+ >
|
|
|
+ {{ option.label }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
</div>
|
|
|
</div>
|
|
|
</header>
|
|
|
@@ -474,12 +597,12 @@ const messages = computed(() => copy[currentLocale.value])
|
|
|
|
|
|
<h2 class="login-modal__title">{{ messages.modalTitle }}</h2>
|
|
|
|
|
|
- <button class="login-modal__signin-option" type="button">
|
|
|
+ <button class="login-modal__signin-option" type="button" @click="handleGoogleClick">
|
|
|
<img class="login-modal__signin-icon" src="/auth-icons/google-icon.svg" alt="Google" />
|
|
|
<span>{{ messages.modalGoogle }}</span>
|
|
|
</button>
|
|
|
|
|
|
- <button class="login-modal__signin-option" type="button">
|
|
|
+ <button class="login-modal__signin-option" type="button" @click="handleAppleClick">
|
|
|
<img class="login-modal__signin-icon" src="/auth-icons/apple-icon.svg" alt="Apple" />
|
|
|
<span>{{ messages.modalApple }}</span>
|
|
|
</button>
|
|
|
@@ -493,6 +616,15 @@ const messages = computed(() => copy[currentLocale.value])
|
|
|
</p>
|
|
|
</section>
|
|
|
</div>
|
|
|
+
|
|
|
+ <div v-if="logoutModalOpen" class="login-modal-backdrop" @click="closeLogoutModal">
|
|
|
+ <section class="login-modal login-modal--logout" @click.stop>
|
|
|
+ <button class="login-modal__close" type="button" @click="closeLogoutModal">×</button>
|
|
|
+ <h2 class="login-modal__panel-title">{{ messages.logoutTitle }}</h2>
|
|
|
+ <p class="login-modal__panel-copy">{{ messages.logoutBody }}</p>
|
|
|
+ <button class="login-modal__ghost-button" type="button" @click="confirmLogout">{{ messages.logoutAction }}</button>
|
|
|
+ <button class="login-modal__ghost-button" type="button" @click="closeLogoutModal">{{ messages.cancel }}</button>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
-
|