style: align public motion with original Home-Vue
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<router-link to="/" class="sidebar-action" title="查看主页" aria-label="查看主页"><i class="fas fa-arrow-up-right-from-square"></i></router-link>
|
||||
<button type="button" class="sidebar-action" :title="themeLabel" :aria-label="themeLabel" @click="toggleTheme($event)"><i :class="themeIcon"></i></button>
|
||||
<button type="button" class="sidebar-action" :title="themeLabel" :aria-label="themeLabel" @click="toggleTheme"><i :class="themeIcon"></i></button>
|
||||
<button type="button" class="sidebar-action" title="修改密码" aria-label="修改密码" @click="passwordModalOpen = true"><i class="fas fa-key"></i></button>
|
||||
<button type="button" class="sidebar-action sidebar-action--danger" title="退出登录" aria-label="退出登录" @click="requestLogout"><i class="fas fa-right-from-bracket"></i></button>
|
||||
</div>
|
||||
|
||||
+32
-61
@@ -6,33 +6,20 @@
|
||||
type="button"
|
||||
aria-label="查看关于信息"
|
||||
@click="showAbout = true"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 18, scale: 0.72 }"
|
||||
:enter="{ opacity: 1, y: 0, scale: 1, transition: { duration: prefersReducedMotion ? 0 : 360, delay: prefersReducedMotion ? 0 : 30 } }"
|
||||
v-motion-pop
|
||||
>
|
||||
<img v-if="profileImage && !profileImageFailed" :src="profileImage" :alt="`${userName} 的头像`" @error="profileImageFailed = true" />
|
||||
<span v-else class="profile-fallback"><i class="fas fa-user" aria-hidden="true"></i></span>
|
||||
<span class="status-ball"><span>在线</span></span>
|
||||
<span class="status-ball"><span>在线中</span></span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="user-name"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, x: 28 }"
|
||||
:enter="{ opacity: 1, x: 0, transition: { duration: prefersReducedMotion ? 0 : 360, delay: prefersReducedMotion ? 0 : 90 } }"
|
||||
>
|
||||
<div class="user-name" v-motion-slide-left>
|
||||
<h1 id="home-title">Hi,</h1>
|
||||
<h1>I'm <span class="name-style">{{ userName }}</span></h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div
|
||||
class="description"
|
||||
aria-live="polite"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 12 }"
|
||||
:enter="{ opacity: 1, y: 0, transition: { duration: prefersReducedMotion ? 0 : 300, delay: prefersReducedMotion ? 0 : 150 } }"
|
||||
>
|
||||
<div class="description" aria-live="polite">
|
||||
<p v-if="loading">正在加载主页...</p>
|
||||
<p v-else ref="descriptionElement"></p>
|
||||
</div>
|
||||
@@ -45,9 +32,7 @@
|
||||
<nav
|
||||
class="contact-section"
|
||||
aria-label="联系方式"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 14, scale: 0.96 }"
|
||||
:enter="{ opacity: 1, y: 0, scale: 1, transition: { duration: prefersReducedMotion ? 0 : 300, delay: prefersReducedMotion ? 0 : 210 } }"
|
||||
v-motion-pop
|
||||
>
|
||||
<template v-for="contact in contacts" :key="contact.id || contact.type">
|
||||
<a
|
||||
@@ -74,7 +59,7 @@
|
||||
<span class="tooltip">{{ contact.type }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<button type="button" class="contact-item" :aria-label="themeLabel" @click="toggleTheme($event)">
|
||||
<button type="button" class="contact-item" :aria-label="themeLabel" @click="toggleTheme">
|
||||
<i :class="themeIcon" aria-hidden="true"></i>
|
||||
<span class="tooltip">{{ isDarkMode ? '浅色' : '深色' }}</span>
|
||||
</button>
|
||||
@@ -105,7 +90,6 @@ import Typed from 'typed.js'
|
||||
import api, { getContacts, getSiteConfig } from '../api'
|
||||
import fallbackContacts from '../config/links.json'
|
||||
import { useTheme } from '../composables/useTheme'
|
||||
import { useMotionPreference } from '../composables/useMotionPreference'
|
||||
import AboutPage from './AboutPage.vue'
|
||||
import VisitTimer from './VisitTimer.vue'
|
||||
import Website from './Website.vue'
|
||||
@@ -131,7 +115,6 @@ const showAbout = ref(false)
|
||||
const showQR = ref(false)
|
||||
const qrCodeSrc = ref('')
|
||||
const { isDarkMode, themeIcon, themeLabel, toggleTheme } = useTheme()
|
||||
const { prefersReducedMotion } = useMotionPreference()
|
||||
let typedInstance = null
|
||||
let configChannel = null
|
||||
|
||||
@@ -141,9 +124,8 @@ const initializeTyped = async () => {
|
||||
if (!descriptionElement.value) return
|
||||
typedInstance = new Typed(descriptionElement.value, {
|
||||
strings: descriptions.value,
|
||||
typeSpeed: 90,
|
||||
backSpeed: 55,
|
||||
backDelay: 1400,
|
||||
typeSpeed: 120,
|
||||
backSpeed: 80,
|
||||
showCursor: true,
|
||||
cursorChar: '|',
|
||||
loop: true,
|
||||
@@ -251,12 +233,8 @@ onUnmounted(() => {
|
||||
background: var(--surface-muted);
|
||||
box-shadow: 0 2px 8px var(--shadow-color);
|
||||
cursor: pointer;
|
||||
transition: border-color var(--motion-base) var(--motion-ease-standard), box-shadow var(--motion-base) var(--motion-ease), transform var(--motion-fast) var(--motion-ease);
|
||||
}
|
||||
|
||||
.profile-button:hover { border-color: var(--hover-link-color); box-shadow: 0 8px 24px var(--shadow-color); transform: translateY(-3px); }
|
||||
.profile-button:active { transform: translateY(-1px) scale(0.98); }
|
||||
|
||||
.profile-button img,
|
||||
.profile-fallback {
|
||||
width: 150px;
|
||||
@@ -264,30 +242,28 @@ onUnmounted(() => {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.profile-button img { object-fit: cover; animation: profile-image-in var(--motion-emphasis) var(--motion-ease) both; transition: transform var(--motion-emphasis) var(--motion-ease); }
|
||||
.profile-button:hover img { transform: scale(1.025); }
|
||||
.profile-button img { object-fit: cover; }
|
||||
.profile-fallback { display: grid; place-items: center; font-size: 52px; background: var(--surface-color); }
|
||||
|
||||
.status-ball {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
right: 15px;
|
||||
bottom: 5px;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--background-color);
|
||||
border-radius: 20px;
|
||||
color: white;
|
||||
background: #248f3d;
|
||||
transition: width 0.2s ease;
|
||||
background: #00c800;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.status-ball span { opacity: 0; font-size: 12px; white-space: nowrap; transition: opacity var(--motion-fast) var(--motion-ease-standard); }
|
||||
.profile-button:hover .status-ball { width: 56px; }
|
||||
.profile-button:hover .status-ball span { opacity: 1; }
|
||||
.status-ball span { color: #00c800; opacity: 0; font-size: 12px; white-space: nowrap; transition: opacity 0.3s ease, color 0.1s ease; }
|
||||
.profile-button:hover .status-ball { width: 4.5em; }
|
||||
.profile-button:hover .status-ball span { color: #eee; opacity: 1; }
|
||||
|
||||
.user-name { display: flex; flex-direction: column; align-items: flex-start; font-size: 1.3em; }
|
||||
.user-name h1 { margin: 0; letter-spacing: 0; }
|
||||
@@ -302,7 +278,7 @@ onUnmounted(() => {
|
||||
background: rgba(var(--hover-link-color-rgb), 0.68);
|
||||
content: '';
|
||||
transform: translateX(-50%);
|
||||
transition: height 0.2s ease;
|
||||
transition: height 0.3s ease-in-out;
|
||||
z-index: -1;
|
||||
}
|
||||
.name-style:hover::before { height: 60%; }
|
||||
@@ -326,14 +302,14 @@ onUnmounted(() => {
|
||||
.contact-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 18px;
|
||||
gap: 20px;
|
||||
min-height: 42px;
|
||||
padding: 8px 12px;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--border-radius);
|
||||
transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.contact-section:hover { border-color: var(--border-color); background: var(--surface-muted); box-shadow: 0 2px 8px var(--shadow-color); }
|
||||
.contact-section:hover { border-color: var(--border-color); background: rgba(var(--background-color-rgb), 0.2); box-shadow: 0 2px 8px var(--shadow-color); backdrop-filter: blur(10px); }
|
||||
.contact-item {
|
||||
position: relative;
|
||||
width: 28px;
|
||||
@@ -346,10 +322,9 @@ onUnmounted(() => {
|
||||
background: transparent;
|
||||
font-size: var(--icon-size);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease, transform 0.2s ease;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.contact-item:hover { color: var(--hover-color, var(--hover-link-color)); transform: translateY(-5px) rotate(8deg); }
|
||||
.contact-item:active { transform: translateY(-2px) rotate(3deg) scale(0.94); }
|
||||
.contact-item:hover { color: var(--hover-color, var(--hover-link-color)); transform: translateY(-5px) rotate(10deg); }
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@@ -363,28 +338,24 @@ onUnmounted(() => {
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, 4px);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
transform: translate(-50%, 10px);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
.contact-item:hover .tooltip,
|
||||
.contact-item:focus-visible .tooltip { opacity: 1; transform: translate(-50%, 0); }
|
||||
|
||||
.overlay { position: fixed; inset: 0; display: grid; place-items: center; padding: 20px; background: rgba(0, 0, 0, 0.62); z-index: 1000; }
|
||||
.modal-content { max-width: 100%; }
|
||||
.qr-image { width: min(320px, 82vw); aspect-ratio: 1; object-fit: contain; padding: 18px; border-radius: 8px; background: white; box-shadow: 0 12px 34px rgba(0, 0, 0, 0.28); transition: transform var(--motion-emphasis) var(--motion-ease), box-shadow var(--motion-emphasis) var(--motion-ease); }
|
||||
.qr-image:hover { transform: translateY(-4px) scale(1.025); box-shadow: 0 18px 42px rgba(0, 0, 0, 0.3); }
|
||||
.qr-image { width: min(300px, 82vw); aspect-ratio: 1; object-fit: contain; padding: 20px; border-radius: var(--border-radius); background: white; box-shadow: 0 4px 8px var(--shadow-color); transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.4s ease; }
|
||||
.qr-image:hover { transform: translateY(-5px) scale(1.03); box-shadow: 0 15px 30px -10px var(--shadow-color); }
|
||||
.fade-enter-active,
|
||||
.fade-leave-active { transition: opacity var(--motion-base) var(--motion-ease); }
|
||||
.fade-leave-active { transition: opacity 0.3s ease-out; }
|
||||
.fade-enter-active .modal-content,
|
||||
.fade-leave-active .modal-content { transition: opacity var(--motion-base) var(--motion-ease), transform var(--motion-emphasis) var(--motion-ease); }
|
||||
.fade-leave-active .modal-content { transition: opacity 0.3s ease-out, transform 0.3s ease-out; }
|
||||
.fade-enter-from,
|
||||
.fade-leave-to { opacity: 0; }
|
||||
.fade-enter-from .modal-content,
|
||||
.fade-leave-to .modal-content { opacity: 0; transform: translateY(24px) scale(0.9); }
|
||||
|
||||
@keyframes profile-image-in {
|
||||
from { opacity: 0; transform: scale(0.9); }
|
||||
}
|
||||
.fade-leave-to .modal-content { opacity: 0; transform: translateY(30px) scale(0.8); }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.home-view { justify-content: flex-start; gap: 17px; padding-top: 28px; }
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<router-link to="/" class="icon-button" title="返回主页" aria-label="返回主页">
|
||||
<i class="fas fa-house" aria-hidden="true"></i>
|
||||
</router-link>
|
||||
<button type="button" class="icon-button" :title="themeLabel" :aria-label="themeLabel" @click="toggleTheme($event)">
|
||||
<button type="button" class="icon-button" :title="themeLabel" :aria-label="themeLabel" @click="toggleTheme">
|
||||
<i :class="themeIcon" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="visit-timer"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 50, x: '-50%', scale: 0.5 }"
|
||||
:enter="{ opacity: 1, y: 0, x: '-50%', scale: 1, transition: { duration: prefersReducedMotion ? 0 : 360, delay: prefersReducedMotion ? 0 : 300 } }"
|
||||
:enter="{ opacity: 1, y: 0, x: '-50%', scale: 1, transition: { duration: prefersReducedMotion ? 0 : 300 } }"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@click="toggleCalendar">
|
||||
@@ -138,7 +138,7 @@ const toggleCalendar = () => {
|
||||
bottom: 50px;
|
||||
padding: 8px 15px;
|
||||
border-radius: 20px;
|
||||
transition: transform var(--motion-base) var(--motion-ease), box-shadow var(--motion-base) var(--motion-ease), border-color var(--motion-fast) var(--motion-ease-standard);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
|
||||
+12
-16
@@ -8,13 +8,12 @@
|
||||
<div v-for="(siteChunk, index) in chunkedSites" :key="index" class="swiper-slide">
|
||||
<div class="site-grid">
|
||||
<a
|
||||
v-for="(site, siteIndex) in siteChunk"
|
||||
v-for="site in siteChunk"
|
||||
:key="site.id || site.url"
|
||||
:href="site.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="site-box"
|
||||
:style="{ '--site-index': siteIndex }"
|
||||
>
|
||||
<i :class="site.icon" aria-hidden="true"></i>
|
||||
<span>{{ site.name }}</span>
|
||||
@@ -75,35 +74,32 @@ onUnmounted(() => swiperInstance?.destroy(true, true))
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.sites { width: min(100%, 700px); min-height: 185px; margin: 2px 0 0; }
|
||||
.sites-swiper { overflow: hidden; padding: 10px 10px 28px; }
|
||||
.site-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
|
||||
.sites { width: min(100%, 700px); min-height: 185px; margin: 30px 0 20px; }
|
||||
.sites-swiper { overflow: hidden; padding: 10px; }
|
||||
.site-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 15px; }
|
||||
.site-box,
|
||||
.site-skeleton {
|
||||
min-width: 0;
|
||||
min-height: 72px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-muted);
|
||||
border-radius: var(--border-radius);
|
||||
background: rgba(var(--background-color-rgb), 0.2);
|
||||
}
|
||||
.site-box { display: flex; align-items: center; justify-content: center; gap: 9px; padding: 18px; font-weight: 600; animation: site-card-in var(--motion-emphasis) var(--motion-ease) both; animation-delay: calc(var(--site-index) * 50ms); transition: transform var(--motion-base) var(--motion-ease), box-shadow var(--motion-base) var(--motion-ease), border-color var(--motion-fast) var(--motion-ease-standard); }
|
||||
.site-box:hover { transform: translateY(-4px); border-color: var(--hover-link-color); box-shadow: 0 7px 18px var(--shadow-color); }
|
||||
.site-box:active { transform: translateY(-1px) scale(0.98); }
|
||||
.site-box { display: flex; align-items: center; justify-content: center; gap: 9px; padding: 30px; font-weight: 600; backdrop-filter: blur(10px); transition: all 0.3s ease; }
|
||||
.site-box:hover { transform: translateY(-3px); box-shadow: 0 1px 8px var(--shadow-color); }
|
||||
.site-box i { flex: 0 0 auto; font-size: var(--icon-size); }
|
||||
.site-box span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.site-grid--loading { padding: 10px; }
|
||||
.site-skeleton { animation: pulse 1.2s ease-in-out infinite alternate; }
|
||||
@keyframes pulse { to { opacity: 0.45; } }
|
||||
@keyframes site-card-in { from { opacity: 0; transform: translateY(14px) scale(0.97); } }
|
||||
:deep(.swiper-pagination-bullet) { background: var(--text-muted); }
|
||||
:deep(.swiper-pagination-bullet) { transition: width var(--motion-base) var(--motion-ease), transform var(--motion-fast) var(--motion-ease); }
|
||||
:deep(.swiper-pagination-bullet-active) { width: 20px; border-radius: 5px; background: var(--hover-link-color); transform: scaleY(1.08); }
|
||||
:deep(.swiper-pagination-bullet) { background: var(--text-muted); transition: width 0.3s ease; }
|
||||
:deep(.swiper-pagination-bullet-active) { width: 20px; border-radius: 5px; background: #8c8c8c94; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.sites { min-height: 160px; }
|
||||
.site-grid { gap: 9px; }
|
||||
.site-grid { gap: 15px; }
|
||||
.site-box,
|
||||
.site-skeleton { min-height: 60px; }
|
||||
.site-box { flex-direction: column; gap: 5px; padding: 10px 6px; font-size: 14px; }
|
||||
.site-box { flex-direction: column; gap: 5px; padding: 15px 6px; border-radius: 8px; font-size: 14px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,22 +22,9 @@ export const initializeTheme = () => {
|
||||
export const useTheme = () => {
|
||||
initializeTheme()
|
||||
|
||||
const toggleTheme = (event) => {
|
||||
const updateTheme = () => {
|
||||
applyTheme(!isDarkMode.value)
|
||||
localStorage.setItem('darkMode', String(isDarkMode.value))
|
||||
}
|
||||
const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
||||
if (!document.startViewTransition || reducedMotion) {
|
||||
updateTheme()
|
||||
return
|
||||
}
|
||||
|
||||
const originX = Number.isFinite(event?.clientX) && event.clientX > 0 ? event.clientX : window.innerWidth / 2
|
||||
const originY = Number.isFinite(event?.clientY) && event.clientY > 0 ? event.clientY : window.innerHeight / 2
|
||||
document.documentElement.style.setProperty('--theme-origin-x', `${originX}px`)
|
||||
document.documentElement.style.setProperty('--theme-origin-y', `${originY}px`)
|
||||
document.startViewTransition(updateTheme)
|
||||
const toggleTheme = () => {
|
||||
applyTheme(!isDarkMode.value)
|
||||
localStorage.setItem('darkMode', String(isDarkMode.value))
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+5
-53
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user