chore: 界面改进和问题修复

This commit is contained in:
2026-02-26 23:43:03 +08:00
parent 756e3fee71
commit 8506dab51e
9 changed files with 699 additions and 132 deletions

View File

@@ -1,78 +1,7 @@
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="contact?.customHtml" v-html="customHeaderHtml" />
<div class="app-shell" :style="backgroundStyle">
<div class="background-overlay" :style="overlayStyle" />
<button
class="background-toggle"
:title="hideComponents ? '显示内容' : '隐藏内容'"
:class="{ active: hideComponents }"
@click="hideComponents = !hideComponents"
>
<span class="toggle-icon">{{ hideComponents ? "👁️" : "🙈" }}</span>
<span class="toggle-label">{{ hideComponents ? "显示" : "隐藏" }}</span>
</button>
<div class="content-stack">
<Transition name="fade-down">
<main v-if="!hideComponents" key="content" class="app-body">
<NuxtPage />
</main>
</Transition>
<Transition name="fade-up">
<PageSwitcher v-if="!hideComponents" key="switcher" />
</Transition>
<Transition name="fade-down">
<FooterSection v-if="!hideComponents" key="footer" :contact="contact" />
</Transition>
</div>
<MusicPlayer />
</div>
<!-- <UApp> -->
<NuxtLayout>
<NuxtPage class="w-full" />
</NuxtLayout>
<!-- </UApp> -->
</template>
<script setup>
import { onMounted, computed, ref } from "vue";
import PageSwitcher from "~/components/PageSwitcher.vue";
import FooterSection from "~/components/FooterSection.vue";
import MusicPlayer from "~/components/MusicPlayer.vue";
import siteConfig from "~/config/siteConfig";
const customHeaderHtml = siteConfig.header.customHtml;
const contact = siteConfig.footer;
const bg = siteConfig.appearance.background;
const isMobile = ref(false);
const hideComponents = ref(false);
const checkIfMobile = () => {
isMobile.value = typeof window !== "undefined" && window.innerWidth <= 768;
};
onMounted(() => {
checkIfMobile();
window.addEventListener("resize", checkIfMobile);
});
const getBackgroundImage = () => {
if (!bg.enable) return undefined;
const image = isMobile.value && bg.mobileImage ? bg.mobileImage : bg.image;
if (!image) return undefined;
return image.startsWith("http") ? image : `/${image}`;
};
const backgroundStyle = computed(() => ({ backgroundColor: "#0f1629" }));
const overlayStyle = computed(() => {
const imageUrl = getBackgroundImage();
if (!bg.enable || !imageUrl) return {};
return {
backgroundImage: `linear-gradient(${bg.overlay}, ${bg.overlay}), url('${imageUrl}')`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundAttachment: "fixed",
filter: bg.blur ? `blur(${bg.blur}px)` : undefined,
};
});
</script>
<!-- <style>
@import "/css/netease-mini-player-v2.css";
</style> -->

View File

@@ -3,14 +3,14 @@
<h2 class="m-0 mb-1">个人简介</h2>
<p class="text-text-muted text-sm m-0 mb-3 block">关于我 · About Me</p>
<div class="flex flex-wrap justify-center gap-3.5">
<div class="flex flex-wrap justify-center gap-2 sm:gap-3.5">
<article
v-if="age"
class="flex-1 min-w-35 flex items-center justify-between gap-2 bg-linear-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2.5 px-3.5 shadow-md-dark transition-all duration-300 hover:-translate-y-1 hover:border-primary/40 hover:shadow-lg-dark hover:bg-linear-to-br hover:from-primary/6"
class="flex-1 min-w-28 sm:min-w-35 flex items-center justify-between gap-2 bg-linear-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2 sm:p-2.5 sm:px-3.5 px-2.5 shadow-md-dark transition-all duration-300 hover:-translate-y-1 hover:border-primary/40 hover:shadow-lg-dark hover:bg-linear-to-br hover:from-primary/6"
>
<div class="flex items-center gap-2">
<span class="text-xl leading-none">🎂</span>
<h3 class="m-0 text-sm font-semibold text-white/90">年龄</h3>
<span class="text-lg sm:text-xl leading-none">🎂</span>
<h3 class="m-0 text-xs sm:text-sm font-semibold text-white/90">年龄</h3>
</div>
<p
class="text-text-muted text-xs m-0 text-right whitespace-nowrap font-medium text-white/60"
@@ -21,11 +21,11 @@
<article
v-if="profile?.gender"
class="flex-1 min-w-35 flex items-center justify-between gap-2 bg-linear-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2.5 px-3.5 shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark"
class="flex-1 min-w-28 sm:min-w-35 flex items-center justify-between gap-2 bg-linear-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2 sm:p-2.5 sm:px-3.5 px-2.5 shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark"
>
<div class="flex items-center gap-2">
<span class="text-xl leading-none"></span>
<h3 class="m-0 text-sm font-semibold text-white/90">性别</h3>
<span class="text-lg sm:text-xl leading-none"></span>
<h3 class="m-0 text-xs sm:text-sm font-semibold text-white/90">性别</h3>
</div>
<p
class="text-text-muted text-xs m-0 text-right whitespace-nowrap font-medium text-white/60"
@@ -36,11 +36,11 @@
<article
v-if="profile?.pronouns"
class="flex-1 min-w-35 flex items-center justify-between gap-2 bg-linear-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2.5 px-3.5 shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark"
class="flex-1 min-w-28 sm:min-w-35 flex items-center justify-between gap-2 bg-linear-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2 sm:p-2.5 sm:px-3.5 px-2.5 shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark"
>
<div class="flex items-center gap-2">
<span class="text-xl leading-none">🗣</span>
<h3 class="m-0 text-sm font-semibold text-white/90">代词</h3>
<span class="text-lg sm:text-xl leading-none">🗣</span>
<h3 class="m-0 text-xs sm:text-sm font-semibold text-white/90">代词</h3>
</div>
<p
class="text-text-muted text-xs m-0 text-right whitespace-nowrap font-medium text-white/60"
@@ -51,11 +51,11 @@
<article
v-if="profile?.location"
class="flex-1 min-w-35 flex items-center justify-between gap-2 bg-linear-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2.5 px-3.5 shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark"
class="flex-1 min-w-28 sm:min-w-35 flex items-center justify-between gap-2 bg-linear-to-br from-white/5 to-white/2 border border-white/10 rounded-2xl p-2 sm:p-2.5 sm:px-3.5 px-2.5 shadow-md-dark transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg-dark"
>
<div class="flex items-center gap-2">
<span class="text-xl leading-none">📍</span>
<h3 class="m-0 text-sm font-semibold text-white/90">地区</h3>
<span class="text-lg sm:text-xl leading-none">📍</span>
<h3 class="m-0 text-xs sm:text-sm font-semibold text-white/90">地区</h3>
</div>
<p
class="text-text-muted text-xs m-0 text-right whitespace-nowrap font-medium text-white/60"

View File

@@ -1,22 +1,22 @@
<template>
<section
class="card main-section grid grid-cols-[120px_1fr] gap-4 items-center hover:shadow-lg-dark group"
class="card main-section grid grid-cols-1 sm:grid-cols-[120px_1fr] gap-3 sm:gap-4 items-start sm:items-center hover:shadow-lg-dark group text-center sm:text-left"
>
<div class="relative">
<div class="relative justify-self-center sm:justify-self-start">
<div
class="absolute inset-0 rounded-full bg-linear-to-br from-primary/30 to-accent/20 blur-xl group-hover:blur-2xl transition-all duration-300 opacity-0 group-hover:opacity-100"
/>
<NuxtImg
class="relative w-30 h-30 rounded-full object-cover border-2 border-primary/40 shadow-md-dark bg-white transition-transform duration-300 group-hover:scale-105"
class="relative w-24 h-24 sm:w-30 sm:h-30 rounded-full object-cover border-2 border-primary/40 shadow-md-dark bg-white transition-transform duration-300 group-hover:scale-105"
:src="profile.avatar"
alt="avatar"
loading="eager"
/>
</div>
<div class="overflow-hidden">
<h1 class="text-2xl font-bold">{{ profile.name }}</h1>
<h1 class="text-2xl font-bold leading-tight">{{ profile.name }}</h1>
<p class="text-text-muted text-sm mt-1">{{ profile.title }}</p>
<p class="mt-2 line-clamp-2">{{ profile.bio }}</p>
<p class="mt-2 line-clamp-3 sm:line-clamp-2">{{ profile.bio }}</p>
</div>
</section>
</template>

View File

@@ -1,9 +1,9 @@
<template>
<section class="card flex flex-col gap-2.5">
<section class="card relative main-section flex flex-col gap-2.5">
<h2 class="m-0 mb-1 text-lg font-semibold">社交链接</h2>
<p class="text-text-muted text-sm m-0 mb-3 block">社交账号 · Links</p>
<div class="relative">
<button
<!-- <button
v-show="canScrollLeft"
aria-label="向左滚动"
class="scroll-arrow left flex items-center justify-center"
@@ -25,26 +25,26 @@
stroke-linejoin="round"
/>
</svg>
</button>
</button> -->
<div
ref="container"
class="flex flex-nowrap gap-2.5 overflow-x-auto social-links-scroll py-1"
>
<div ref="container" class="relative flex flex-wrap gap-2.5">
<template v-for="link in links" :key="link.url">
<NuxtLink
:to="link.url"
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-white/10 backdrop-blur-sm border border-white/10 text-text-primary text-sm font-medium transition-all duration-200 hover:bg-primary/20 hover:border-primary/40 hover:text-primary hover:-translate-y-1"
>
<span v-if="iconFor(link)" class="inline-flex items-center justify-center w-5 h-5">
<Icon v-if="iconFor(link).name" :name="iconFor(link).name" width="20" height="20" />
<span
v-if="iconFor(link)"
class="inline-flex items-center justify-center w-5 h-5 shrink-0"
>
<Icon :name="iconFor(link).name" width="20" height="20" />
</span>
<span>{{ link.name }}</span>
<span class="whitespace-nowrap">{{ link.name }}</span>
</NuxtLink>
</template>
</div>
<button
<!-- <button
v-show="canScrollRight"
aria-label="向右滚动"
class="scroll-arrow right flex items-center justify-center"
@@ -66,7 +66,7 @@
stroke-linejoin="round"
/>
</svg>
</button>
</button> -->
</div>
</section>
</template>
@@ -82,8 +82,7 @@ defineProps({
});
const container = ref(null);
const canScrollLeft = ref(false);
const canScrollRight = ref(false);
const showOnlyIcons = ref(false);
const iconMap = {
bilibili: "simple-icons:bilibili",
@@ -118,24 +117,17 @@ const iconFor = (link) => {
function updateScrollButtons() {
const el = container.value;
if (!el) return;
canScrollLeft.value = el.scrollLeft > 0;
canScrollRight.value = el.scrollWidth - el.clientWidth - el.scrollLeft > 1;
// 检测是否需要换行(内容高度大于一行)
const hasOverflow = el.scrollHeight > el.clientHeight + 5;
showOnlyIcons.value = hasOverflow;
}
function scrollByAmount(amount) {
const el = container.value;
if (!el) return;
el.scrollBy({ left: amount, behavior: "smooth" });
setTimeout(updateScrollButtons, 300);
}
function scrollLeft() {
scrollByAmount(-200);
}
function scrollRight() {
scrollByAmount(200);
}
// function scrollByAmount(amount) {
// const el = container.value;
// if (!el) return;
// el.scrollBy({ left: amount, behavior: "smooth" });
// setTimeout(updateScrollButtons, 300);
// }
onMounted(() => {
updateScrollButtons();

80
app/layouts/default.vue Normal file
View File

@@ -0,0 +1,80 @@
<template>
<div class="relative min-h-screen flex flex-col">
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="contact?.customHtml" v-html="customHeaderHtml" />
<div class="app-shell" :style="backgroundStyle">
<div class="background-overlay" :style="overlayStyle" />
<button
class="background-toggle"
:title="hideComponents ? '显示内容' : '隐藏内容'"
:class="{ active: hideComponents }"
@click="hideComponents = !hideComponents"
>
<span class="toggle-icon">{{ hideComponents ? "👁️" : "🙈" }}</span>
<span class="toggle-label">{{ hideComponents ? "显示" : "隐藏" }}</span>
</button>
<div class="content-stack">
<Transition name="fade-down">
<main v-if="!hideComponents" key="content" class="app-body">
<NuxtPage />
</main>
</Transition>
<Transition name="fade-up">
<PageSwitcher v-if="!hideComponents" key="switcher" />
</Transition>
<Transition name="fade-down">
<FooterSection v-if="!hideComponents" key="footer" :contact="contact" />
</Transition>
</div>
<MusicPlayer />
</div>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import PageSwitcher from "~/components/PageSwitcher.vue";
import FooterSection from "~/components/FooterSection.vue";
import MusicPlayer from "~/components/MusicPlayer.vue";
import siteConfig from "~/config/siteConfig";
const customHeaderHtml = siteConfig.header.customHtml;
const contact = siteConfig.footer;
const bg = siteConfig.appearance.background;
const isMobile = ref(false);
const hideComponents = ref(false);
const checkIfMobile = () => {
isMobile.value = typeof window !== "undefined" && window.innerWidth <= 768;
};
onMounted(() => {
checkIfMobile();
window.addEventListener("resize", checkIfMobile);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", checkIfMobile);
});
const getBackgroundImage = () => {
if (!bg.enable) return undefined;
const image = isMobile.value && bg.mobileImage ? bg.mobileImage : bg.image;
if (!image) return undefined;
return image.startsWith("http") ? image : `/${image}`;
};
const backgroundStyle = computed(() => ({ backgroundColor: "#0f1629" }));
const overlayStyle = computed(() => {
const imageUrl = getBackgroundImage();
if (!bg.enable || !imageUrl) return {};
return {
backgroundImage: `linear-gradient(${bg.overlay}, ${bg.overlay}), url('${imageUrl}')`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundAttachment: "fixed",
filter: bg.blur ? `blur(${bg.blur}px)` : undefined,
};
});
</script>

View File

@@ -161,15 +161,18 @@
}
.page {
/* width: 100%; */
max-width: var(--site-content-max-width);
margin: 0 auto;
padding: 2rem 1rem 3rem;
display: flex;
flex-direction: column;
gap: 1rem;
box-sizing: border-box;
}
.main-section {
width: 100%;
max-width: var(--main-max-width);
/* min-width: var(--main-min-width); */
}
@@ -258,6 +261,7 @@
.netease-mini-player[data-position="bottom-right"] .playlist-container {
position: fixed !important;
bottom: calc(20px + 80px) !important;
width: 45% !important;
}
.muted {
@@ -334,6 +338,15 @@
}
@media (max-width: 640px) {
.page {
padding: 1.25rem 0.75rem 2rem;
gap: 0.75rem;
}
/* .card {
padding: 1rem 1rem;
} */
.background-toggle {
right: 0.75rem;
bottom: 0.75rem;