init: 初始化 VitePress 官网项目
This commit is contained in:
45
docs/.vitepress/theme/components/BookMockup.vue
Normal file
45
docs/.vitepress/theme/components/BookMockup.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
imageSrc?: string
|
||||
title: string
|
||||
color?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
color: 'bg-blue-600',
|
||||
className: '',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['relative group perspective-1000', props.className]">
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[120%] h-[120%] bg-gradient-to-tr from-orange-100/40 to-blue-100/40 blur-[60px] rounded-full pointer-events-none"></div>
|
||||
|
||||
<div class="relative z-10 transition-transform duration-500 hover:scale-105 hover:-translate-y-2">
|
||||
<img
|
||||
v-if="props.imageSrc"
|
||||
:src="props.imageSrc"
|
||||
:alt="props.title"
|
||||
class="w-full h-auto object-contain drop-shadow-2xl"
|
||||
style="filter: drop-shadow(0 20px 30px rgba(0,0,0,0.15));"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
:class="[
|
||||
'w-[220px] h-[300px] mx-auto rounded-r-xl rounded-l-sm shadow-2xl flex flex-col p-6 relative overflow-hidden',
|
||||
props.color
|
||||
]"
|
||||
>
|
||||
<div class="absolute left-0 top-0 bottom-0 w-[4px] bg-black/10"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-tr from-black/10 to-transparent pointer-events-none"></div>
|
||||
<div class="text-white/60 text-xs font-bold tracking-[0.2em] mb-auto uppercase">Gongxue</div>
|
||||
<div class="text-white font-serif text-3xl font-bold leading-tight">{{ props.title }}</div>
|
||||
<div class="mt-4 w-12 h-1 bg-white/50"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="absolute -bottom-6 left-1/2 -translate-x-1/2 w-[80%] h-4 bg-black/20 blur-xl rounded-[100%] opacity-0 group-hover:opacity-40 transition-opacity duration-500"></div>
|
||||
</div>
|
||||
</template>
|
||||
68
docs/.vitepress/theme/components/CountUpNumber.vue
Normal file
68
docs/.vitepress/theme/components/CountUpNumber.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
interface Props {
|
||||
end: number
|
||||
duration?: number
|
||||
suffix?: string
|
||||
decimals?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
duration: 2000,
|
||||
suffix: '',
|
||||
decimals: 0,
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
const refEl = ref<HTMLDivElement | null>(null)
|
||||
let hasAnimated = false
|
||||
let rafId: number | null = null
|
||||
let observer: IntersectionObserver | null = null
|
||||
|
||||
onMounted(() => {
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && !hasAnimated) {
|
||||
hasAnimated = true
|
||||
let startTime: number | null = null
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
if (!startTime) startTime = currentTime
|
||||
const progress = Math.min((currentTime - startTime) / props.duration, 1)
|
||||
const easeProgress = 1 - Math.pow(1 - progress, 4)
|
||||
count.value = props.end * easeProgress
|
||||
|
||||
if (progress < 1) {
|
||||
rafId = requestAnimationFrame(animate)
|
||||
} else {
|
||||
count.value = props.end
|
||||
}
|
||||
}
|
||||
|
||||
rafId = requestAnimationFrame(animate)
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 }
|
||||
)
|
||||
|
||||
if (refEl.value) {
|
||||
observer.observe(refEl.value)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (observer && refEl.value) observer.disconnect()
|
||||
if (rafId) cancelAnimationFrame(rafId)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="refEl"
|
||||
class="text-4xl md:text-5xl font-bold text-apple-dark mb-2 transition-transform group-hover:scale-110 duration-300 tabular-nums"
|
||||
>
|
||||
{{ count.toFixed(props.decimals) }}
|
||||
<span v-if="props.suffix" class="text-2xl ml-1 font-medium text-gray-400">{{ props.suffix }}</span>
|
||||
</div>
|
||||
</template>
|
||||
123
docs/.vitepress/theme/components/EnrollModal.vue
Normal file
123
docs/.vitepress/theme/components/EnrollModal.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onUnmounted } from 'vue'
|
||||
import { X } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
|
||||
const containerRef = ref<HTMLDivElement | null>(null)
|
||||
const modalRef = ref<HTMLDivElement | null>(null)
|
||||
|
||||
watch(() => props.isOpen, (open) => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
if (open) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
document.body.style.touchAction = 'none'
|
||||
|
||||
containerRef.value.innerHTML = `
|
||||
<div class="loading-indicator" style="text-align: center; padding: 2rem;">
|
||||
<div style="border: 4px solid #3b82f6; border-top: 4px solid transparent; border-radius: 50%; width: 48px; height: 48px; animation: spin 1s linear infinite; margin: 0 auto 1rem;"></div>
|
||||
<p style="color: #6b7280;">表单加载中...</p>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
`
|
||||
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.id = 'goldendata_form_xFYow5'
|
||||
iframe.src = 'https://baoming.gongxue100.com/f/xFYow5?background=white&banner=show&embedded=true&inner_redirect=false'
|
||||
iframe.width = '100%'
|
||||
iframe.height = '918'
|
||||
iframe.frameBorder = '0'
|
||||
iframe.style.border = 'none'
|
||||
|
||||
iframe.onload = () => {
|
||||
const loader = containerRef.value?.querySelector('.loading-indicator') as HTMLElement | null
|
||||
if (loader) loader.style.display = 'none'
|
||||
}
|
||||
|
||||
iframe.onerror = () => {
|
||||
const loader = containerRef.value?.querySelector('.loading-indicator') as HTMLElement | null
|
||||
if (loader) {
|
||||
loader.innerHTML = `
|
||||
<div style="text-align: center; padding: 2rem;">
|
||||
<div style="width: 48px; height: 48px; background: #fee2e2; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<p style="color: #6b7280; margin-bottom: 1rem;">表单加载失败</p>
|
||||
<button onclick="window.location.reload()" style="background: #3b82f6; color: white; border: none; padding: 0.5rem 1rem; border-radius: 0.5rem; cursor: pointer;">重新加载</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
containerRef.value.appendChild(iframe)
|
||||
} else {
|
||||
document.body.style.overflow = ''
|
||||
document.body.style.touchAction = ''
|
||||
if (containerRef.value) {
|
||||
containerRef.value.innerHTML = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.body.style.overflow = ''
|
||||
document.body.style.touchAction = ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isOpen" class="fixed inset-0 z-[9999] flex items-center justify-center p-4">
|
||||
<div
|
||||
class="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
@click="emit('close')"
|
||||
/>
|
||||
|
||||
<div
|
||||
ref="modalRef"
|
||||
class="relative bg-white rounded-3xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden animate-slide-up flex flex-col"
|
||||
>
|
||||
<button
|
||||
@click="emit('close')"
|
||||
class="absolute top-4 right-4 p-2 rounded-full hover:bg-gray-100 transition-colors z-10"
|
||||
>
|
||||
<X :size="24" class="text-gray-500" />
|
||||
</button>
|
||||
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div class="p-6 md:p-8">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="p-2 bg-orange-100 text-orange-600 rounded-xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
|
||||
<circle cx="12" cy="10" r="3"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-xl text-gray-900">报名上课</h3>
|
||||
<p class="text-xs text-gray-500">填写信息,获取专属学习规划</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="w-full min-h-[800px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
178
docs/.vitepress/theme/components/FloatingService.vue
Normal file
178
docs/.vitepress/theme/components/FloatingService.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRouter } from 'vitepress'
|
||||
import { MessageCircle, Phone, BookOpen, Users, X } from 'lucide-vue-next'
|
||||
|
||||
const router = useRouter()
|
||||
const isOpen = ref(false)
|
||||
const showChatModal = ref(false)
|
||||
const chatMessages = ref<{ text?: string; isQrcode?: boolean; isTyping?: boolean }[]>([])
|
||||
const isTypingComplete = ref(false)
|
||||
|
||||
const menuItems = [
|
||||
{ icon: MessageCircle, label: '在线咨询', sub: '实时答疑', href: '#consult' },
|
||||
{ icon: Phone, label: '课程报名', sub: '18322616529', href: 'tel:18322616529' },
|
||||
{ icon: BookOpen, label: '书籍购买', sub: '官方商城', href: '/books' },
|
||||
{ icon: Users, label: '商务合作', sub: '企业/机构', href: '/contact' },
|
||||
]
|
||||
|
||||
let timers: ReturnType<typeof setTimeout>[] = []
|
||||
|
||||
watch(showChatModal, (val) => {
|
||||
if (!val) {
|
||||
chatMessages.value = []
|
||||
isTypingComplete.value = false
|
||||
timers.forEach(clearTimeout)
|
||||
timers = []
|
||||
return
|
||||
}
|
||||
|
||||
timers.push(setTimeout(() => { chatMessages.value = [{ isTyping: true }] }, 500))
|
||||
timers.push(setTimeout(() => {
|
||||
chatMessages.value = [{
|
||||
text: '你好同学,正在接入人工客服,或者方便的话,您先留个手机号联系方式,或加我微信咱们再具体沟通?'
|
||||
}]
|
||||
}, 2500))
|
||||
timers.push(setTimeout(() => {
|
||||
chatMessages.value = [...chatMessages.value, { isTyping: true }]
|
||||
}, 3000))
|
||||
timers.push(setTimeout(() => {
|
||||
chatMessages.value = [...chatMessages.value.filter(m => !m.isTyping), { isQrcode: true }]
|
||||
isTypingComplete.value = true
|
||||
}, 4000))
|
||||
})
|
||||
|
||||
const handleMenuItemClick = (href: string) => {
|
||||
if (href === '#consult') {
|
||||
showChatModal.value = true
|
||||
isOpen.value = false
|
||||
} else if (href.startsWith('tel:')) {
|
||||
window.location.href = href
|
||||
} else {
|
||||
router.go(href)
|
||||
isOpen.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed bottom-6 right-6 z-50 flex flex-col items-end">
|
||||
<div
|
||||
:class="[
|
||||
'mb-4 flex flex-col gap-3 transition-all duration-300 origin-bottom-right',
|
||||
isOpen
|
||||
? 'opacity-100 scale-100 translate-y-0'
|
||||
: 'opacity-0 scale-90 translate-y-10 pointer-events-none'
|
||||
]"
|
||||
>
|
||||
<button
|
||||
v-for="(item, index) in menuItems"
|
||||
:key="index"
|
||||
@click="handleMenuItemClick(item.href)"
|
||||
class="flex items-center gap-3 bg-white/90 backdrop-blur-lg border border-white/20 p-3 pr-4 rounded-2xl shadow-lg cursor-pointer hover:bg-white hover:scale-105 transition-all group"
|
||||
>
|
||||
<div class="w-10 h-10 rounded-full bg-blue-50 text-apple-blue flex items-center justify-center group-hover:bg-apple-blue group-hover:text-white transition-colors flex-shrink-0">
|
||||
<component :is="item.icon" :size="20" />
|
||||
</div>
|
||||
<div class="text-left min-w-[80px]">
|
||||
<div class="text-sm font-bold text-gray-800">{{ item.label }}</div>
|
||||
<div class="text-xs text-gray-400">{{ item.sub }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="isOpen = !isOpen"
|
||||
:class="[
|
||||
'w-16 h-16 rounded-full shadow-2xl flex items-center justify-center transition-all duration-300 hover:scale-110 active:scale-95',
|
||||
isOpen
|
||||
? 'bg-gray-800 rotate-90'
|
||||
: 'bg-apple-blue hover:bg-apple-blue-hover'
|
||||
]"
|
||||
:aria-label="isOpen ? '关闭菜单' : '打开客服菜单'"
|
||||
>
|
||||
<X v-if="isOpen" class="text-white" :size="28" />
|
||||
<MessageCircle v-else class="text-white fill-current" :size="28" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="showChatModal"
|
||||
class="fixed inset-0 z-[9999] flex items-center justify-center p-4"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
@click="showChatModal = false"
|
||||
/>
|
||||
|
||||
<div class="relative bg-white rounded-3xl shadow-2xl w-full max-w-md max-h-[600px] flex flex-col animate-slide-up">
|
||||
<div class="flex items-center justify-between p-4 border-b bg-gradient-to-r from-apple-blue to-blue-600 rounded-t-3xl">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<MessageCircle :size="20" class="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-white">在线客服</h3>
|
||||
<p class="text-xs text-white/80">在线</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="showChatModal = false"
|
||||
class="p-2 rounded-full hover:bg-white/20 transition-colors"
|
||||
>
|
||||
<X :size="20" class="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50">
|
||||
<div v-for="(msg, index) in chatMessages" :key="index">
|
||||
<div v-if="msg.isTyping" class="flex items-start gap-2">
|
||||
<div class="w-8 h-8 bg-apple-blue rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<MessageCircle :size="16" class="text-white" />
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl rounded-tl-none px-4 py-3 shadow-sm">
|
||||
<div class="flex gap-1">
|
||||
<span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0ms;"></span>
|
||||
<span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 150ms;"></span>
|
||||
<span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 300ms;"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="msg.isQrcode" class="flex items-start gap-2">
|
||||
<div class="w-8 h-8 bg-apple-blue rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<MessageCircle :size="16" class="text-white" />
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl rounded-tl-none p-4 shadow-sm">
|
||||
<img
|
||||
src="https://duiwaifengxiang-1304851894.cos.ap-beijing.myqcloud.com/gwgongxue/code.jpg"
|
||||
alt="客服二维码"
|
||||
class="w-48 h-48 rounded-lg"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 text-center mt-2">扫码添加微信客服</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex items-start gap-2 animate-slide-up">
|
||||
<div class="w-8 h-8 bg-apple-blue rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<MessageCircle :size="16" class="text-white" />
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl rounded-tl-none px-4 py-3 shadow-sm max-w-[80%]">
|
||||
<p class="text-sm text-gray-800 leading-relaxed">{{ msg.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t bg-white rounded-b-3xl">
|
||||
<div class="bg-gray-50 rounded-xl p-3 text-center">
|
||||
<p class="text-xs text-gray-500">
|
||||
{{ isTypingComplete
|
||||
? '工作时间:周一至周日 9:00-21:00'
|
||||
: '客服正在输入中…' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
108
docs/.vitepress/theme/components/Footer.vue
Normal file
108
docs/.vitepress/theme/components/Footer.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
const currentYear = new Date().getFullYear()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="bg-apple-grey text-apple-text-gray py-16 border-t border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 mb-12">
|
||||
<div class="col-span-1 md:col-span-1">
|
||||
<h3 class="text-apple-dark font-bold text-lg mb-4">恭学教育集团</h3>
|
||||
<p class="text-xs leading-relaxed max-w-xs">
|
||||
致力于为天津专升本学子提供最优质的教育资源、最先进的刷题系统以及最权威的出版书籍。智慧教育,触手可及。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-apple-dark font-semibold text-sm mb-4">探索</h4>
|
||||
<ul class="space-y-2 text-xs">
|
||||
<li>
|
||||
<a href="/question-bank" class="hover:text-apple-blue hover:underline">
|
||||
刷题小程序
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/online-school" class="hover:text-apple-blue hover:underline">
|
||||
在线网校 APP
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/books" class="hover:text-apple-blue hover:underline">
|
||||
商城书籍
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-apple-dark font-semibold text-sm mb-4">服务</h4>
|
||||
<ul class="space-y-2 text-xs">
|
||||
<li>
|
||||
<a href="/knowledge/" class="hover:text-apple-blue hover:underline">
|
||||
专升本知识库
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact" class="hover:text-apple-blue hover:underline">
|
||||
企业合作
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span class="cursor-pointer hover:text-apple-dark">加入我们</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-apple-dark font-semibold text-sm mb-4">联系我们</h4>
|
||||
<ul class="space-y-2 text-xs">
|
||||
<li>天津市津南区咸水沽镇雅润路东侧金浩园17号楼底商3号</li>
|
||||
<li>Tel: 18322616529</li>
|
||||
<li>Email: gongxuejiaoyu@sina.cn</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-8 border-t border-gray-300">
|
||||
<div class="flex flex-col md:flex-row justify-center items-center gap-4 mb-4 text-xs">
|
||||
<a
|
||||
href="https://beian.miit.gov.cn/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-apple-text-gray hover:text-apple-blue flex items-center gap-1"
|
||||
>
|
||||
津ICP备19008758号-1
|
||||
</a>
|
||||
<a
|
||||
href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=12011202000308"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-apple-text-gray hover:text-apple-blue"
|
||||
>
|
||||
津公网安备 12011202000308号
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-xs text-apple-text-gray mb-4">
|
||||
违法和不良信息举报电话:
|
||||
<a href="tel:18322616529" class="text-apple-blue hover:underline ml-1">18322616529</a>
|
||||
举报邮箱:
|
||||
<a href="mailto:gongxuejiaoyu@sina.cn" class="text-apple-blue hover:underline ml-1">gongxuejiaoyu@sina.cn</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row justify-between items-center text-xs">
|
||||
<p>Copyright © {{ currentYear }} Gongxue Education Group. 保留所有权利。</p>
|
||||
<div class="flex space-x-4 mt-4 md:mt-0">
|
||||
<a href="/privacy-policy" class="hover:text-apple-blue hover:underline">
|
||||
隐私政策
|
||||
</a>
|
||||
<a href="/terms-of-service" class="hover:text-apple-blue hover:underline">
|
||||
使用条款
|
||||
</a>
|
||||
<span class="cursor-pointer hover:underline">网站地图</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
119
docs/.vitepress/theme/components/Header.vue
Normal file
119
docs/.vitepress/theme/components/Header.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vitepress'
|
||||
import { Menu, X } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
scrolled: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const route = useRoute()
|
||||
const mobileMenuOpen = ref(false)
|
||||
|
||||
interface NavItem {
|
||||
path: string
|
||||
label: string
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ path: '/', label: '首页' },
|
||||
{ path: '/question-bank', label: '刷题题库' },
|
||||
{ path: '/online-school', label: '在线网校' },
|
||||
{ path: '/books', label: '出版书籍' },
|
||||
{ path: '/knowledge/', label: '知识库' },
|
||||
]
|
||||
|
||||
const handleNavClick = (path: string) => {
|
||||
mobileMenuOpen.value = false
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const isActive = (path: string) => {
|
||||
if (path === '/') return route.path === '/'
|
||||
return route.path.startsWith(path)
|
||||
}
|
||||
|
||||
watch(() => route.path, () => {
|
||||
mobileMenuOpen.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
:class="[
|
||||
'fixed top-0 left-0 right-0 z-50 transition-all duration-300',
|
||||
scrolled || mobileMenuOpen
|
||||
? 'bg-white/80 backdrop-blur-xl shadow-sm'
|
||||
: 'bg-transparent'
|
||||
]"
|
||||
>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<a
|
||||
href="/"
|
||||
class="flex items-center cursor-pointer group"
|
||||
@click="handleNavClick('/')"
|
||||
>
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="恭学教育Logo"
|
||||
class="w-8 h-8 mr-2 transition-transform group-hover:scale-105"
|
||||
/>
|
||||
<span class="font-bold text-lg tracking-tight text-apple-blue">
|
||||
恭学教育
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav class="hidden md:flex space-x-8">
|
||||
<a
|
||||
v-for="item in navItems"
|
||||
:key="item.path"
|
||||
:href="item.path"
|
||||
:class="[
|
||||
'text-sm font-medium transition-colors duration-200',
|
||||
isActive(item.path)
|
||||
? 'text-apple-blue'
|
||||
: 'text-apple-text-gray hover:text-apple-dark'
|
||||
]"
|
||||
@click="handleNavClick(item.path)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="md:hidden">
|
||||
<button
|
||||
@click="mobileMenuOpen = !mobileMenuOpen"
|
||||
class="text-apple-dark p-2"
|
||||
:aria-label="mobileMenuOpen ? '关闭菜单' : '打开菜单'"
|
||||
>
|
||||
<X v-if="mobileMenuOpen" :size="24" />
|
||||
<Menu v-else :size="24" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="mobileMenuOpen"
|
||||
class="md:hidden absolute top-16 left-0 right-0 bg-white h-screen border-t border-gray-100 p-4 animate-fade-in"
|
||||
>
|
||||
<div class="flex flex-col space-y-6 mt-4">
|
||||
<a
|
||||
v-for="item in navItems"
|
||||
:key="item.path"
|
||||
:href="item.path"
|
||||
:class="[
|
||||
'text-2xl font-semibold text-left py-2 border-b border-gray-100 w-full',
|
||||
isActive(item.path) ? 'text-apple-blue' : 'text-apple-dark'
|
||||
]"
|
||||
@click="handleNavClick(item.path)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
41
docs/.vitepress/theme/components/PhoneMockup.vue
Normal file
41
docs/.vitepress/theme/components/PhoneMockup.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
imageSrc: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'relative mx-auto w-full max-w-[280px] z-10 transition-transform duration-300 hover:scale-[1.02]',
|
||||
props.className || ''
|
||||
]"
|
||||
style="aspect-ratio: 1290 / 2796;"
|
||||
>
|
||||
<div
|
||||
class="relative w-full h-full bg-black overflow-hidden"
|
||||
style="border-radius: 16% / 7.38%; box-shadow: 0 0 0 1px #444, 0 0 0 3px #d1d1d1, 0 0 0 4px #999, 0 20px 50px -10px rgba(0,0,0,0.3); padding: 3px;"
|
||||
>
|
||||
<div
|
||||
class="relative w-full h-full bg-[#111] overflow-hidden"
|
||||
style="border-radius: calc(16% - 1%) / calc(7.38% - 0.5%); box-shadow: inset 0 0 20px rgba(255,255,255,0.08);"
|
||||
>
|
||||
<div class="absolute top-[3.5%] left-1/2 -translate-x-1/2 w-[30%] h-[3.5%] bg-black rounded-[20px] z-50 pointer-events-none flex items-center justify-center">
|
||||
<div class="w-[40%] h-[40%] bg-white/10 rounded-full blur-[2px]"></div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
:src="props.imageSrc"
|
||||
alt="App Screen"
|
||||
class="w-full h-full object-cover object-top opacity-100 relative z-10"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<div class="absolute bottom-[2%] left-1/2 -translate-x-1/2 w-[35%] h-[0.5%] bg-white/80 rounded-full z-20 backdrop-blur-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
11
docs/.vitepress/theme/components/Skeleton.vue
Normal file
11
docs/.vitepress/theme/components/Skeleton.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['skeleton rounded-lg', props.className || '']"></div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user