2026-09-24 21:35:00 +08:00
|
|
|
|
<!--
|
|
|
|
|
|
Copyright 2024-2026 ChatterMate
|
|
|
|
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
|
|
limitations under the License.
|
|
|
|
|
|
-->
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-10-22 17:44:36 +08:00
|
|
|
|
import BrandMark from '@/components/common/BrandMark.vue'
|
|
|
|
|
|
import { computed, ref } from 'vue'
|
2026-09-24 21:35:00 +08:00
|
|
|
|
import { useRouter } from 'vue-router'
|
|
|
|
|
|
import { authService } from '@/services/auth'
|
|
|
|
|
|
import { resolveLandingRoute } from '@/router/landing'
|
|
|
|
|
|
import { useEnterpriseFeatures } from '@/composables/useEnterpriseFeatures'
|
|
|
|
|
|
import { useForgotPassword } from '@/composables/useForgotPassword'
|
|
|
|
|
|
import InstallPrompt from '@/components/pwa/InstallPrompt.vue'
|
2026-10-22 17:44:36 +08:00
|
|
|
|
import { useTheme } from '@/composables/useTheme'
|
|
|
|
|
|
import { navIconSvg } from '@/components/layout/navIcons'
|
2026-09-24 21:35:00 +08:00
|
|
|
|
import { apiPath, getApiUrl } from '@/config/api'
|
|
|
|
|
|
import type { AxiosError } from 'axios'
|
|
|
|
|
|
interface ErrorResponse {
|
|
|
|
|
|
detail: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const { loadModule } = useEnterpriseFeatures()
|
|
|
|
|
|
|
|
|
|
|
|
const trackLogin = async (method: string) => {
|
|
|
|
|
|
const mod = await loadModule('/src/modules/enterprise/utils/analytics.ts') as any
|
|
|
|
|
|
mod?.trackLogin?.(method)
|
|
|
|
|
|
}
|
2026-10-22 17:44:36 +08:00
|
|
|
|
const { mode: themeMode, toggle: toggleTheme } = useTheme()
|
|
|
|
|
|
const themeTitle = computed(() =>
|
|
|
|
|
|
themeMode.value === 'dark' ? '主题:深色 — 点击切换为浅色'
|
|
|
|
|
|
: themeMode.value === 'light' ? '主题:浅色 — 点击切换为跟随系统'
|
|
|
|
|
|
: '主题:跟随系统 — 点击切换为深色'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-09-24 21:35:00 +08:00
|
|
|
|
const email = ref('')
|
|
|
|
|
|
const password = ref('')
|
2026-10-22 17:44:36 +08:00
|
|
|
|
const showPassword = ref(false)
|
2026-09-24 21:35:00 +08:00
|
|
|
|
const error = ref('')
|
|
|
|
|
|
const isLoading = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// Check if enterprise module is available
|
|
|
|
|
|
const { hasEnterpriseModule } = useEnterpriseFeatures()
|
|
|
|
|
|
|
|
|
|
|
|
// Forgot password composable - only initialize if enterprise module is available
|
|
|
|
|
|
const showForgotPasswordModal = ref(false)
|
|
|
|
|
|
const forgotPassword = hasEnterpriseModule ? useForgotPassword() : null
|
|
|
|
|
|
|
|
|
|
|
|
// Destructure with fallbacks for when enterprise module is not available
|
|
|
|
|
|
const isForgotPasswordLoading = forgotPassword?.isLoading ?? ref(false)
|
|
|
|
|
|
const forgotPasswordError = forgotPassword?.error ?? ref('')
|
|
|
|
|
|
const forgotPasswordSuccess = forgotPassword?.success ?? ref('')
|
|
|
|
|
|
const forgotPasswordStep = forgotPassword?.currentStep ?? ref(1)
|
|
|
|
|
|
const forgotPasswordEmail = forgotPassword?.email ?? ref('')
|
|
|
|
|
|
const forgotPasswordOtp = forgotPassword?.otp ?? ref('')
|
|
|
|
|
|
const newPassword = forgotPassword?.newPassword ?? ref('')
|
|
|
|
|
|
const confirmPassword = forgotPassword?.confirmPassword ?? ref('')
|
|
|
|
|
|
const passwordValidation = forgotPassword?.passwordValidation ?? ref({
|
|
|
|
|
|
score: 0,
|
|
|
|
|
|
hasMinLength: false,
|
|
|
|
|
|
hasUpperCase: false,
|
|
|
|
|
|
hasLowerCase: false,
|
|
|
|
|
|
hasNumber: false,
|
|
|
|
|
|
hasSpecialChar: false
|
|
|
|
|
|
})
|
|
|
|
|
|
const requestPasswordReset = forgotPassword?.requestPasswordReset ?? (() => Promise.resolve(false))
|
|
|
|
|
|
const verifyAndResetPassword = forgotPassword?.verifyAndResetPassword ?? (() => Promise.resolve(false))
|
|
|
|
|
|
const resetForgotPasswordForm = forgotPassword?.resetForm ?? (() => {})
|
|
|
|
|
|
const goBackToEmailStep = forgotPassword?.goBackToEmailStep ?? (() => {})
|
|
|
|
|
|
|
|
|
|
|
|
// One ordered candidate list, shared with '/', the catch-all and the 403 page.
|
|
|
|
|
|
// It used to end at '/403', which is not somewhere to land — and the route was
|
|
|
|
|
|
// not even registered, so the user silently ended up on /ai-agents instead.
|
|
|
|
|
|
const getInitialRoute = () => resolveLandingRoute()
|
|
|
|
|
|
|
|
|
|
|
|
const handleLogin = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
isLoading.value = true
|
|
|
|
|
|
error.value = ''
|
|
|
|
|
|
|
|
|
|
|
|
await authService.login(email.value, password.value)
|
|
|
|
|
|
|
|
|
|
|
|
trackLogin('email')
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this is Shopify flow (new managed installation)
|
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search)
|
|
|
|
|
|
const isShopifyFlow = urlParams.get('shopify_flow') === '1'
|
|
|
|
|
|
const shopId = urlParams.get('shop_id')
|
|
|
|
|
|
const returnTo = urlParams.get('return_to')
|
|
|
|
|
|
|
|
|
|
|
|
if (isShopifyFlow && shopId) {
|
|
|
|
|
|
console.log('🔗 Shopify flow detected, linking organization')
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// If return_to is specified, redirect there
|
|
|
|
|
|
if (returnTo) {
|
|
|
|
|
|
console.log('📍 Redirecting to return_to:', returnTo)
|
|
|
|
|
|
window.location.href = returnTo
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (linkError: any) {
|
|
|
|
|
|
console.error('❌ Failed to link organization:', linkError)
|
|
|
|
|
|
error.value = linkError.response?.data?.detail || '关联组织失败'
|
|
|
|
|
|
isLoading.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this is an OLD embedded Shopify login (opened in popup) - for backward compatibility
|
|
|
|
|
|
const isEmbedded = router.currentRoute.value.query.embedded === 'true'
|
|
|
|
|
|
const legacyShopId = router.currentRoute.value.query.shop_id as string
|
|
|
|
|
|
|
|
|
|
|
|
if (isEmbedded && legacyShopId && window.opener) {
|
|
|
|
|
|
// This is a popup for Shopify embedded app (old flow)
|
|
|
|
|
|
console.log('Login successful in popup, notifying parent window')
|
|
|
|
|
|
|
|
|
|
|
|
// Show success state
|
|
|
|
|
|
isLoading.value = true
|
|
|
|
|
|
error.value = ''
|
|
|
|
|
|
|
|
|
|
|
|
// Notify the parent window of successful login
|
|
|
|
|
|
if (window.opener && !window.opener.closed) {
|
|
|
|
|
|
window.opener.postMessage(
|
|
|
|
|
|
{ type: 'login_success', shop_id: legacyShopId },
|
|
|
|
|
|
window.location.origin
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Close the popup after a brief delay to ensure message is sent
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
window.close()
|
|
|
|
|
|
}, 300)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// If opener is gone, just close the window
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
window.close()
|
|
|
|
|
|
}, 300)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check for Shopify flow (new simplified flow)
|
|
|
|
|
|
if (router.currentRoute.value.query.shopify_flow === '1') {
|
|
|
|
|
|
const shop = router.currentRoute.value.query.shop as string
|
|
|
|
|
|
const shopId = router.currentRoute.value.query.shop_id as string
|
|
|
|
|
|
const host = router.currentRoute.value.query.host as string
|
|
|
|
|
|
const returnTo = router.currentRoute.value.query.return_to as string
|
|
|
|
|
|
|
|
|
|
|
|
// Redirect to auth complete page
|
|
|
|
|
|
const query: any = { shop, shop_id: shopId, host }
|
|
|
|
|
|
if (returnTo) {
|
|
|
|
|
|
query.return_to = returnTo
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
router.push({
|
|
|
|
|
|
path: '/shopify/auth-complete',
|
|
|
|
|
|
query
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check if there's a Shopify redirect pending in localStorage (legacy flow)
|
|
|
|
|
|
const shopifyRedirectData = localStorage.getItem('shopifyRedirect')
|
|
|
|
|
|
if (shopifyRedirectData) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const redirectInfo = JSON.parse(shopifyRedirectData)
|
|
|
|
|
|
// Clear the stored redirect
|
|
|
|
|
|
localStorage.removeItem('shopifyRedirect')
|
|
|
|
|
|
|
|
|
|
|
|
// Build the full URL with all query parameters
|
|
|
|
|
|
const queryParams = new URLSearchParams()
|
|
|
|
|
|
for (const [key, value] of Object.entries(redirectInfo)) {
|
|
|
|
|
|
queryParams.append(key, value as string)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Redirect to the backend Shopify auth endpoint
|
|
|
|
|
|
window.location.href = apiPath(`/shopify/auth?${queryParams.toString()}`)
|
|
|
|
|
|
return // Don't do the normal navigation
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Failed to parse shopifyRedirect data:', e)
|
|
|
|
|
|
// Clear invalid data
|
|
|
|
|
|
localStorage.removeItem('shopifyRedirect')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check for redirect query parameter (internal frontend route)
|
|
|
|
|
|
const redirectPath = router.currentRoute.value.query.redirect as string
|
|
|
|
|
|
if (redirectPath) {
|
|
|
|
|
|
// If it's an internal route (starts with /), use router.push
|
|
|
|
|
|
if (redirectPath.startsWith('/')) {
|
|
|
|
|
|
router.push(redirectPath)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// Otherwise treat as API redirect (legacy behavior)
|
|
|
|
|
|
window.location.href = `${getApiUrl()}${redirectPath}`
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Determine initial route based on permissions
|
|
|
|
|
|
const initialRoute = getInitialRoute()
|
|
|
|
|
|
router.push(initialRoute)
|
|
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.log(err)
|
|
|
|
|
|
// authService.login rethrows the backend's detail as a plain Error, so
|
|
|
|
|
|
// reading only the axios response showed "Login failed" for everything.
|
|
|
|
|
|
const detail = (err as AxiosError<ErrorResponse>).response?.data?.detail
|
|
|
|
|
|
error.value = (typeof detail === 'string' && detail) || (err instanceof Error && err.message) || '登录失败'
|
|
|
|
|
|
console.error('Login error:', err)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
isLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const navigateToSignup = () => {
|
|
|
|
|
|
// Preserve embedded, shop_id, return_to, redirect query params if present
|
|
|
|
|
|
const isEmbedded = router.currentRoute.value.query.embedded
|
|
|
|
|
|
const shopId = router.currentRoute.value.query.shop_id
|
|
|
|
|
|
const returnTo = router.currentRoute.value.query.return_to
|
|
|
|
|
|
const shopifyFlow = router.currentRoute.value.query.shopify_flow
|
|
|
|
|
|
const redirect = router.currentRoute.value.query.redirect
|
|
|
|
|
|
|
|
|
|
|
|
const query: any = {}
|
|
|
|
|
|
|
|
|
|
|
|
if (isEmbedded) query.embedded = isEmbedded
|
|
|
|
|
|
if (shopId) query.shop_id = shopId
|
|
|
|
|
|
if (returnTo) query.return_to = returnTo
|
|
|
|
|
|
if (shopifyFlow) query.shopify_flow = shopifyFlow
|
|
|
|
|
|
if (redirect) query.redirect = redirect
|
|
|
|
|
|
|
|
|
|
|
|
if (Object.keys(query).length > 0) {
|
|
|
|
|
|
console.log('Navigating to signup with params:', query)
|
|
|
|
|
|
router.push({
|
|
|
|
|
|
path: '/signup',
|
|
|
|
|
|
query
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
router.push('/signup')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const openForgotPasswordModal = () => {
|
|
|
|
|
|
resetForgotPasswordForm()
|
|
|
|
|
|
showForgotPasswordModal.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const closeForgotPasswordModal = () => {
|
|
|
|
|
|
showForgotPasswordModal.value = false
|
|
|
|
|
|
resetForgotPasswordForm()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleRequestPasswordReset = async () => {
|
|
|
|
|
|
const success = await requestPasswordReset()
|
|
|
|
|
|
// Keep modal open to proceed to step 2
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleVerifyAndResetPassword = async () => {
|
|
|
|
|
|
const success = await verifyAndResetPassword()
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
// Wait a moment to show success message, then close modal
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
closeForgotPasswordModal()
|
|
|
|
|
|
// Optionally pre-fill the email in login form
|
|
|
|
|
|
email.value = forgotPasswordEmail.value
|
|
|
|
|
|
}, 2000)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<div class="login">
|
|
|
|
|
|
<!-- Form column -->
|
|
|
|
|
|
<section class="form-col">
|
|
|
|
|
|
<div class="form-top reveal" style="--i: 0">
|
|
|
|
|
|
<BrandMark :size="34" />
|
2026-10-22 19:38:57 +08:00
|
|
|
|
<span class="wordmark" style="font-size: 21px">Reply<i>Pier</i></span>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<button
|
|
|
|
|
|
class="icon-btn"
|
|
|
|
|
|
@click="toggleTheme"
|
|
|
|
|
|
:title="themeTitle"
|
|
|
|
|
|
:aria-label="themeTitle"
|
|
|
|
|
|
v-html="navIconSvg(themeMode === 'dark' ? 'moon' : themeMode === 'light' ? 'sun' : 'monitor', 18)"
|
|
|
|
|
|
></button>
|
2026-09-24 21:35:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<div class="form-wrap">
|
|
|
|
|
|
<div class="eyebrow reveal" style="--i: 1">Sign in · 登录</div>
|
|
|
|
|
|
<h1 class="title reveal" style="--i: 2">欢迎回来</h1>
|
|
|
|
|
|
<p class="lede reveal" style="--i: 3">登录工作台,继续今天的接待。</p>
|
|
|
|
|
|
|
|
|
|
|
|
<form class="login-form reveal" style="--i: 4" @submit.prevent="handleLogin" novalidate>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label for="email">邮箱</label>
|
|
|
|
|
|
<div class="input-wrap">
|
|
|
|
|
|
<span class="ic" v-html="navIconSvg('mail', 17)"></span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="email"
|
|
|
|
|
|
v-model="email"
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
type="email"
|
|
|
|
|
|
required
|
|
|
|
|
|
placeholder="you@company.com"
|
|
|
|
|
|
autocomplete="email"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-09-24 21:35:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<div class="field">
|
|
|
|
|
|
<div class="label-row">
|
|
|
|
|
|
<label for="password">密码</label>
|
|
|
|
|
|
<a
|
|
|
|
|
|
v-if="hasEnterpriseModule"
|
|
|
|
|
|
href="#"
|
|
|
|
|
|
@click.prevent="openForgotPasswordModal"
|
|
|
|
|
|
>忘记密码?</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="input-wrap">
|
|
|
|
|
|
<span class="ic" v-html="navIconSvg('lock', 17)"></span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="password"
|
|
|
|
|
|
v-model="password"
|
|
|
|
|
|
class="input"
|
|
|
|
|
|
:type="showPassword ? 'text' : 'password'"
|
|
|
|
|
|
required
|
|
|
|
|
|
placeholder="••••••••"
|
|
|
|
|
|
autocomplete="current-password"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="icon-btn sm peek"
|
|
|
|
|
|
:aria-label="showPassword ? '隐藏密码' : '显示密码'"
|
|
|
|
|
|
@click="showPassword = !showPassword"
|
|
|
|
|
|
v-html="navIconSvg('eye', 15)"
|
|
|
|
|
|
></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-09-24 21:35:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<div v-if="error" class="auth-error" role="alert">{{ error }}</div>
|
2026-09-24 21:35:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<button class="btn btn-primary btn-lg btn-block" type="submit" :disabled="isLoading">
|
|
|
|
|
|
<span v-if="isLoading">
|
|
|
|
|
|
{{ router.currentRoute.value.query.embedded === 'true' ? '连接中…' : '登录中…' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
登录工作台
|
|
|
|
|
|
<span class="icon-arrow" v-html="navIconSvg('arrowRight', 15)"></span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</form>
|
2026-09-24 21:35:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<p v-if="hasEnterpriseModule" class="form-note reveal" style="--i: 5">
|
2026-09-24 21:35:00 +08:00
|
|
|
|
还没有账号?
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<a href="#" class="inline-link" @click.prevent="navigateToSignup">注册</a>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p v-else class="form-note reveal" style="--i: 5">
|
|
|
|
|
|
还没有账号?请联系组织管理员,从「人工客服」页面邀请你加入团队。
|
2026-09-24 21:35:00 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<div class="install-hint-slot">
|
|
|
|
|
|
<InstallPrompt />
|
|
|
|
|
|
</div>
|
2026-09-24 21:35:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<div class="form-foot reveal" style="--i: 6">
|
2026-10-22 19:38:57 +08:00
|
|
|
|
<span>© 2026 ReplyPier</span>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<span class="mono">APACHE-2.0 · SELF-HOSTED</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
2026-09-24 21:35:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<!-- Brand stage — always night ink, even in the light theme -->
|
|
|
|
|
|
<section class="stage" data-theme="dark" aria-hidden="true">
|
|
|
|
|
|
<div class="stage-top">
|
|
|
|
|
|
<span><span class="pulse"></span>Open-source AI customer support</span>
|
|
|
|
|
|
<span>v0.1 · 本地部署</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="vertical">妥帖接待每一次对话</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="stage-body">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 class="headline reveal" style="--i: 2">让每一次对话,<br />都被<em>妥善</em>接待。</h2>
|
|
|
|
|
|
<p class="stage-lede reveal" style="--i: 3">
|
|
|
|
|
|
AI 依据你的知识库即时作答;需要时,连同完整上下文交给人工。接待、评价与洞察,尽在一处。
|
|
|
|
|
|
</p>
|
2026-09-24 21:35:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<div class="specimen">
|
|
|
|
|
|
<div class="spec">青禾测试套餐的退款期多长,校验码是什么?</div>
|
|
|
|
|
|
<div class="spec">
|
|
|
|
|
|
退款期限为购买后 <strong>17 天</strong>内,客服校验码为 <strong>QH-7319</strong>。
|
|
|
|
|
|
<div class="spec-meta">
|
|
|
|
|
|
<span class="who">AI 客服</span>
|
|
|
|
|
|
<span class="cite">
|
|
|
|
|
|
<span v-html="navIconSvg('book', 12)"></span>依据 · 演示规则
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="spec">
|
|
|
|
|
|
<span v-html="navIconSvg('transfer', 12)"></span>已转接人工 · 客服 A 接管
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="spec">
|
|
|
|
|
|
<span class="stars">
|
|
|
|
|
|
<span v-for="n in 5" :key="n" v-html="navIconSvg('star', 15, 'icon')"></span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div>人工客服回复及时,问题已解决。</div>
|
|
|
|
|
|
<div class="spec-meta"><span>访客评价 · 5.0</span></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-09-24 21:35:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<div class="stage-foot reveal" style="--i: 5">
|
|
|
|
|
|
<div class="fig"><b class="num">24<small>/7</small></b><span>全天候 AI 接待</span></div>
|
|
|
|
|
|
<div class="fig"><b class="num">7<small>家</small></b><span>主流模型自由切换</span></div>
|
|
|
|
|
|
<div class="fig"><b class="num">100<small>%</small></b><span>开源,可二次开发</span></div>
|
2026-09-24 21:35:00 +08:00
|
|
|
|
</div>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
</section>
|
2026-09-24 21:35:00 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- Forgot Password Modal - only show if enterprise module is available -->
|
|
|
|
|
|
<div v-if="hasEnterpriseModule && showForgotPasswordModal" class="modal-overlay">
|
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
|
<h2>{{ forgotPasswordStep === 1 ? '重置密码' : '验证并重置' }}</h2>
|
|
|
|
|
|
<button class="close-btn" @click="closeForgotPasswordModal" aria-label="关闭">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
|
<!-- Step 1: Request OTP -->
|
|
|
|
|
|
<div v-if="forgotPasswordStep === 1" class="forgot-password-step">
|
|
|
|
|
|
<p class="step-description">输入你的邮箱,我们会发送验证码以重置密码。</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="forgot-email">邮箱地址</label>
|
|
|
|
|
|
<div class="input-wrapper">
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="forgot-email"
|
|
|
|
|
|
v-model="forgotPasswordEmail"
|
|
|
|
|
|
type="email"
|
|
|
|
|
|
required
|
|
|
|
|
|
placeholder="输入邮箱"
|
|
|
|
|
|
:disabled="isForgotPasswordLoading"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="forgotPasswordError" class="error-message" role="alert">
|
|
|
|
|
|
{{ forgotPasswordError }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="forgotPasswordSuccess" class="success-message" role="status">
|
|
|
|
|
|
{{ forgotPasswordSuccess }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="modal-submit-btn"
|
|
|
|
|
|
@click="handleRequestPasswordReset"
|
|
|
|
|
|
:disabled="isForgotPasswordLoading || !forgotPasswordEmail"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ isForgotPasswordLoading ? '发送中…' : '发送验证码' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Step 2: Verify OTP and Reset Password -->
|
|
|
|
|
|
<div v-if="forgotPasswordStep === 2" class="forgot-password-step">
|
|
|
|
|
|
<p class="step-description">输入发送到你邮箱的验证码和新密码。</p>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="forgot-otp">验证码</label>
|
|
|
|
|
|
<div class="input-wrapper">
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="forgot-otp"
|
|
|
|
|
|
v-model="forgotPasswordOtp"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
required
|
|
|
|
|
|
placeholder="输入 6 位验证码"
|
|
|
|
|
|
maxlength="6"
|
|
|
|
|
|
:disabled="isForgotPasswordLoading"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="new-password">新密码</label>
|
|
|
|
|
|
<div class="input-wrapper">
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="new-password"
|
|
|
|
|
|
v-model="newPassword"
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
required
|
|
|
|
|
|
placeholder="输入新密码"
|
|
|
|
|
|
:disabled="isForgotPasswordLoading"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="password-requirements">
|
|
|
|
|
|
<p class="requirements-title">密码需包含:</p>
|
|
|
|
|
|
<ul>
|
|
|
|
|
|
<li :class="{ valid: passwordValidation.hasMinLength }">至少 8 个字符</li>
|
|
|
|
|
|
<li :class="{ valid: passwordValidation.hasUpperCase }">包含大写字母</li>
|
|
|
|
|
|
<li :class="{ valid: passwordValidation.hasLowerCase }">包含小写字母</li>
|
|
|
|
|
|
<li :class="{ valid: passwordValidation.hasNumber }">包含数字</li>
|
|
|
|
|
|
<li :class="{ valid: passwordValidation.hasSpecialChar }">包含特殊字符(!@#$%^&*)</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label for="confirm-password">确认密码</label>
|
|
|
|
|
|
<div class="input-wrapper">
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="confirm-password"
|
|
|
|
|
|
v-model="confirmPassword"
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
required
|
|
|
|
|
|
placeholder="再次输入新密码"
|
|
|
|
|
|
:disabled="isForgotPasswordLoading"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="forgotPasswordError" class="error-message" role="alert">
|
|
|
|
|
|
{{ forgotPasswordError }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="forgotPasswordSuccess" class="success-message" role="status">
|
|
|
|
|
|
{{ forgotPasswordSuccess }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="modal-actions">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="modal-back-btn"
|
|
|
|
|
|
@click="goBackToEmailStep"
|
|
|
|
|
|
:disabled="isForgotPasswordLoading"
|
|
|
|
|
|
>
|
|
|
|
|
|
返回
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="modal-submit-btn"
|
|
|
|
|
|
@click="handleVerifyAndResetPassword"
|
|
|
|
|
|
:disabled="isForgotPasswordLoading || !forgotPasswordOtp || !newPassword || !confirmPassword"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ isForgotPasswordLoading ? '重置中…' : '重置密码' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
/* 墨与瓷 sign-in: a quiet porcelain form beside a night-ink stage that tells
|
|
|
|
|
|
the product story with real conversation fragments. */
|
2026-09-24 21:35:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.login {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(440px, 40%) 1fr;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
min-height: 100dvh;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
background: var(--paper);
|
|
|
|
|
|
color: var(--ink);
|
|
|
|
|
|
font-family: var(--font-sans);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
/* ── Form column ───────────────────────────────────── */
|
|
|
|
|
|
.form-col {
|
2026-09-24 21:35:00 +08:00
|
|
|
|
display: flex;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
padding: 36px 56px 32px;
|
|
|
|
|
|
position: relative;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.form-top {
|
2026-09-24 21:35:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 12px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.form-top .icon-btn {
|
|
|
|
|
|
margin-left: auto;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.form-wrap {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 380px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
padding: 48px 0;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.form-wrap .title {
|
2026-09-24 21:35:00 +08:00
|
|
|
|
font-size: 40px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.form-wrap .lede {
|
|
|
|
|
|
margin-top: 10px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.login-form {
|
|
|
|
|
|
margin-top: 40px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 22px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.label-row {
|
2026-09-24 21:35:00 +08:00
|
|
|
|
display: flex;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
align-items: baseline;
|
|
|
|
|
|
justify-content: space-between;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.label-row a {
|
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
border-bottom: 1px solid var(--line-2);
|
|
|
|
|
|
padding-bottom: 1px;
|
|
|
|
|
|
transition: color var(--t-fast), border-color var(--t-fast);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.label-row a:hover {
|
|
|
|
|
|
color: var(--ink);
|
|
|
|
|
|
border-color: var(--ink);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.input-wrap {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.input-wrap .input {
|
2026-09-24 21:35:00 +08:00
|
|
|
|
width: 100%;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
padding-left: 44px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.input-wrap > .ic {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 15px;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
color: var(--faint);
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
}
|
2026-09-24 21:35:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.input-wrap .peek {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 6px;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
color: var(--muted);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.login-form .btn-primary .icon-arrow {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
transition: transform var(--t-fast);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.login-form .btn-primary:hover .icon-arrow {
|
|
|
|
|
|
transform: translateX(3px);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.auth-error {
|
|
|
|
|
|
padding: 10px 14px;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
border: 1px solid var(--vermilion-line);
|
|
|
|
|
|
background: var(--vermilion-soft);
|
|
|
|
|
|
color: var(--vermilion);
|
|
|
|
|
|
font-size: 13px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.form-note {
|
|
|
|
|
|
margin-top: 28px;
|
|
|
|
|
|
padding-top: 20px;
|
|
|
|
|
|
border-top: 1px solid var(--line);
|
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
line-height: 1.7;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.inline-link {
|
|
|
|
|
|
color: var(--ink);
|
|
|
|
|
|
border-bottom: 1px solid var(--line-2);
|
|
|
|
|
|
}
|
2026-09-24 21:35:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.inline-link:hover {
|
|
|
|
|
|
border-color: var(--ink);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.form-foot {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-foot .mono {
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Install hint (mobile only) */
|
|
|
|
|
|
.install-hint-slot {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
margin-top: var(--space-lg);
|
|
|
|
|
|
padding-bottom: var(--safe-bottom);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
/* ── Brand stage (always night ink) ────────────────── */
|
|
|
|
|
|
.stage {
|
2026-09-24 21:35:00 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow: hidden;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
background: var(--paper);
|
|
|
|
|
|
color: var(--ink);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
padding: 48px 64px 44px;
|
|
|
|
|
|
isolation: isolate;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.stage::before {
|
|
|
|
|
|
content: '';
|
2026-09-24 21:35:00 +08:00
|
|
|
|
position: absolute;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
inset: -20%;
|
|
|
|
|
|
z-index: -1;
|
|
|
|
|
|
background:
|
|
|
|
|
|
radial-gradient(40% 35% at 78% 22%, color-mix(in srgb, var(--vermilion) 22%, transparent), transparent 70%),
|
|
|
|
|
|
radial-gradient(45% 40% at 20% 85%, color-mix(in srgb, var(--celadon) 16%, transparent), transparent 70%),
|
|
|
|
|
|
radial-gradient(30% 30% at 60% 60%, color-mix(in srgb, var(--brass) 10%, transparent), transparent 70%);
|
|
|
|
|
|
filter: blur(10px);
|
|
|
|
|
|
animation: drift 22s ease-in-out infinite alternate;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
@keyframes drift {
|
|
|
|
|
|
to {
|
|
|
|
|
|
transform: translate3d(-3%, 2%, 0) scale(1.06);
|
|
|
|
|
|
}
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
/* Fine ruled lines, like letter paper */
|
|
|
|
|
|
.stage::after {
|
|
|
|
|
|
content: '';
|
2026-09-24 21:35:00 +08:00
|
|
|
|
position: absolute;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: -1;
|
|
|
|
|
|
background-image: linear-gradient(var(--line) 1px, transparent 1px);
|
|
|
|
|
|
background-size: 100% 44px;
|
|
|
|
|
|
mask-image: linear-gradient(180deg, transparent, #000 30%, #000 70%, transparent);
|
|
|
|
|
|
-webkit-mask-image: linear-gradient(180deg, transparent, #000 30%, #000 70%, transparent);
|
|
|
|
|
|
opacity: 0.5;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.stage-top {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 10.5px;
|
|
|
|
|
|
letter-spacing: 0.2em;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
color: var(--muted);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.stage-top span {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.stage-top .pulse {
|
|
|
|
|
|
color: var(--celadon);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.vertical {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 16px;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
writing-mode: vertical-rl;
|
|
|
|
|
|
font-family: var(--font-serif);
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
letter-spacing: 0.7em;
|
|
|
|
|
|
color: var(--faint);
|
|
|
|
|
|
display: flex;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
align-items: center;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 18px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.vertical::before,
|
|
|
|
|
|
.vertical::after {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
width: 1px;
|
|
|
|
|
|
height: 64px;
|
|
|
|
|
|
background: var(--line-2);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.stage-body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(0, 1fr) minmax(0, 344px);
|
|
|
|
|
|
gap: 40px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding-right: 56px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.headline {
|
|
|
|
|
|
font-family: var(--font-serif);
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: clamp(28px, 2.7vw, 40px);
|
|
|
|
|
|
line-height: 1.42;
|
|
|
|
|
|
letter-spacing: 0.05em;
|
|
|
|
|
|
text-wrap: balance;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.headline em {
|
2026-09-24 21:35:00 +08:00
|
|
|
|
font-style: normal;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
color: var(--vermilion);
|
|
|
|
|
|
text-emphasis: filled dot var(--vermilion);
|
|
|
|
|
|
text-emphasis-position: under right;
|
|
|
|
|
|
-webkit-text-emphasis: filled dot var(--vermilion);
|
|
|
|
|
|
-webkit-text-emphasis-position: under;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.stage-lede {
|
|
|
|
|
|
margin-top: 22px;
|
|
|
|
|
|
max-width: 26em;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
color: var(--muted);
|
2026-10-22 17:44:36 +08:00
|
|
|
|
font-size: 14.5px;
|
|
|
|
|
|
line-height: 1.95;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.specimen {
|
|
|
|
|
|
position: relative;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.spec {
|
|
|
|
|
|
background: color-mix(in srgb, var(--surface) 88%, transparent);
|
|
|
|
|
|
border: 1px solid var(--line-2);
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
padding: 14px 16px;
|
|
|
|
|
|
box-shadow: var(--shadow-lg);
|
|
|
|
|
|
backdrop-filter: blur(8px);
|
|
|
|
|
|
font-size: 13.5px;
|
|
|
|
|
|
line-height: 1.75;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
animation: rise 0.8s var(--ease) forwards, bob 7s ease-in-out infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.spec:nth-child(1) {
|
|
|
|
|
|
width: 72%;
|
|
|
|
|
|
animation-delay: 0.3s, 1.1s;
|
|
|
|
|
|
border-top-left-radius: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.spec:nth-child(2) {
|
|
|
|
|
|
width: 90%;
|
|
|
|
|
|
align-self: flex-end;
|
|
|
|
|
|
animation-delay: 0.7s, 1.5s;
|
|
|
|
|
|
border-top-right-radius: 5px;
|
|
|
|
|
|
background: color-mix(in srgb, var(--celadon) 10%, var(--surface));
|
|
|
|
|
|
border-color: var(--celadon-line);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.spec:nth-child(3) {
|
|
|
|
|
|
align-self: center;
|
|
|
|
|
|
animation-delay: 1.1s, 1.9s;
|
|
|
|
|
|
padding: 8px 14px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
color: var(--vermilion);
|
|
|
|
|
|
border-color: var(--vermilion-line);
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.spec:nth-child(4) {
|
|
|
|
|
|
width: 84%;
|
|
|
|
|
|
align-self: flex-end;
|
|
|
|
|
|
animation-delay: 1.5s, 2.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes rise {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(16px);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes bob {
|
|
|
|
|
|
50% {
|
|
|
|
|
|
translate: 0 -5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.spec-meta {
|
|
|
|
|
|
margin-top: 8px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 8px;
|
|
|
|
|
|
font-size: 11.5px;
|
|
|
|
|
|
color: var(--muted);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.spec-meta .who {
|
|
|
|
|
|
color: var(--celadon);
|
|
|
|
|
|
font-weight: 500;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.spec .cite {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 5px;
|
|
|
|
|
|
padding: 1px 8px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
border: 1px dashed var(--celadon-line);
|
|
|
|
|
|
color: var(--celadon);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.spec strong {
|
|
|
|
|
|
color: var(--ink);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.spec .stars {
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.spec .stars .icon {
|
|
|
|
|
|
width: 15px;
|
|
|
|
|
|
height: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stage-foot {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, auto);
|
|
|
|
|
|
justify-content: start;
|
|
|
|
|
|
border-top: 1px solid var(--line-2);
|
|
|
|
|
|
padding-top: 22px;
|
|
|
|
|
|
margin-right: 48px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.fig {
|
|
|
|
|
|
padding-right: 44px;
|
|
|
|
|
|
margin-right: 44px;
|
|
|
|
|
|
border-right: 1px solid var(--line);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.fig:last-child {
|
|
|
|
|
|
border-right: 0;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.fig b {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-family: var(--font-num);
|
|
|
|
|
|
font-weight: 350;
|
|
|
|
|
|
font-size: 30px;
|
|
|
|
|
|
line-height: 1.1;
|
|
|
|
|
|
letter-spacing: -0.01em;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.fig b small {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
margin-left: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.fig span {
|
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ── Forgot-password modal ─────────────────────────── */
|
2026-09-24 21:35:00 +08:00
|
|
|
|
.modal-overlay {
|
|
|
|
|
|
position: fixed;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: var(--z-modal);
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
background: var(--scrim);
|
|
|
|
|
|
backdrop-filter: blur(6px);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-content {
|
|
|
|
|
|
width: 100%;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
max-width: 460px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
max-height: 90vh;
|
|
|
|
|
|
overflow-y: auto;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
background: var(--surface);
|
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
border-radius: var(--radius-card-lg);
|
|
|
|
|
|
box-shadow: var(--shadow-lg);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
padding: 22px 24px 0;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-header h2 {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
font-family: var(--font-serif);
|
|
|
|
|
|
font-size: 20px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
font-weight: 600;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
letter-spacing: 0.02em;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.close-btn {
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
cursor: pointer;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.close-btn:hover {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
|
border-color: var(--line);
|
|
|
|
|
|
color: var(--ink);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-body {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
padding: 20px 24px 24px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.forgot-password-step {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 16px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-description {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
font-size: 13px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
color: var(--muted);
|
2026-10-22 17:44:36 +08:00
|
|
|
|
line-height: 1.7;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.input-wrapper input {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 44px;
|
|
|
|
|
|
padding: 0 14px;
|
|
|
|
|
|
border-radius: var(--radius-input);
|
|
|
|
|
|
border: 1px solid var(--line-2);
|
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
|
color: var(--text);
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
transition: border-color var(--t-fast), box-shadow var(--t-fast);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.input-wrapper input:focus {
|
|
|
|
|
|
border-color: var(--ink);
|
|
|
|
|
|
box-shadow: var(--ring);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.input-wrapper input:disabled {
|
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
|
color: var(--muted);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.error-message {
|
|
|
|
|
|
padding: 9px 12px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
border: 1px solid var(--vermilion-line);
|
|
|
|
|
|
background: var(--vermilion-soft);
|
|
|
|
|
|
color: var(--vermilion);
|
|
|
|
|
|
font-size: 12.5px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.success-message {
|
|
|
|
|
|
padding: 9px 12px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
border: 1px solid var(--celadon-line);
|
|
|
|
|
|
background: var(--celadon-soft);
|
|
|
|
|
|
color: var(--celadon);
|
|
|
|
|
|
font-size: 12.5px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.modal-submit-btn {
|
|
|
|
|
|
height: 42px;
|
|
|
|
|
|
padding: 0 18px;
|
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
|
border-radius: var(--radius-btn);
|
|
|
|
|
|
background: var(--primary-bg);
|
|
|
|
|
|
color: var(--primary-fg);
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background-color var(--t-fast), opacity var(--t-fast);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.modal-submit-btn:hover:not(:disabled) {
|
|
|
|
|
|
background: var(--primary-hover);
|
|
|
|
|
|
}
|
2026-09-24 21:35:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.modal-submit-btn:disabled {
|
|
|
|
|
|
opacity: 0.45;
|
|
|
|
|
|
cursor: not-allowed;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.modal-back-btn {
|
|
|
|
|
|
height: 42px;
|
|
|
|
|
|
padding: 0 16px;
|
|
|
|
|
|
border: 1px solid var(--line-2);
|
|
|
|
|
|
border-radius: var(--radius-btn);
|
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
|
color: var(--text2);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
font-size: 14px;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
cursor: pointer;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.modal-back-btn:hover:not(:disabled) {
|
|
|
|
|
|
border-color: var(--line-3);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.modal-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
justify-content: flex-end;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.password-requirements {
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
padding: 12px 14px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
|
border: 1px solid var(--line);
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.requirements-title {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
}
|
2026-09-24 21:35:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.password-requirements ul {
|
|
|
|
|
|
list-style: none;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
display: flex;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 3px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.password-requirements li {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
padding-left: 14px;
|
|
|
|
|
|
position: relative;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.password-requirements li::before {
|
|
|
|
|
|
content: '·';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 4px;
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.password-requirements li.valid {
|
|
|
|
|
|
color: var(--celadon);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.password-requirements li.valid::before {
|
|
|
|
|
|
content: '✓';
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ── Responsive ────────────────────────────────────── */
|
|
|
|
|
|
@media (max-width: 1180px) {
|
|
|
|
|
|
.stage-body {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
.specimen {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stage {
|
|
|
|
|
|
padding: 40px 36px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-24 21:35:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
|
.login {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stage {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-col {
|
|
|
|
|
|
padding: 28px 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.install-hint-slot {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
|
.spec {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
animation: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stage::before {
|
|
|
|
|
|
animation: none;
|
|
|
|
|
|
}
|
2026-09-24 21:35:00 +08:00
|
|
|
|
}
|
2026-10-22 17:44:36 +08:00
|
|
|
|
</style>
|