style: align public motion with original Home-Vue

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