调整用户页样式, 添加过渡动画

This commit is contained in:
2025-04-01 03:10:19 +08:00
parent 574a177f8e
commit 4c188826ac
7 changed files with 676 additions and 385 deletions

View File

@@ -50,12 +50,9 @@ const layout = computed(() => {
watchEffect(() => { watchEffect(() => {
if (isDarkMode.value) { if (isDarkMode.value) {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
console.log('Added dark class to HTML'); // For debugging
} else { } else {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
console.log('Removed dark class from HTML'); // For debugging
} }
// If you dynamically apply Naive UI theme to body or provider, do it here too
}); });
const themeOverrides = { const themeOverrides = {

View File

@@ -1,26 +1,26 @@
<!-- eslint-disable vue/component-name-in-template-casing -->
<script setup lang="ts"> <script setup lang="ts">
import { NavigateToNewTab, isDarkMode } from '@/Utils' import { NavigateToNewTab, isDarkMode } from '@/Utils';
import { useAccount } from '@/api/account' import { useAccount } from '@/api/account';
import { FunctionTypes, ThemeType, UserInfo } from '@/api/api-models' import { FunctionTypes, ThemeType, UserInfo } from '@/api/api-models';
import { useUser } from '@/api/user' import { useUser } from '@/api/user';
import RegisterAndLogin from '@/components/RegisterAndLogin.vue' import RegisterAndLogin from '@/components/RegisterAndLogin.vue';
import { AVATAR_URL, FETCH_API } from '@/data/constants' import { FETCH_API } from '@/data/constants'; // 移除了未使用的 AVATAR_URL
import { useAuthStore } from '@/store/useAuthStore' import { useAuthStore } from '@/store/useAuthStore';
import { import {
BookCoins20Filled, BookCoins20Filled,
CalendarClock24Filled, CalendarClock24Filled,
Person48Filled, Person48Filled,
VideoAdd20Filled, VideoAdd20Filled,
WindowWrench20Filled, WindowWrench20Filled,
} from '@vicons/fluent' } from '@vicons/fluent';
import { Chatbox, Home, Moon, MusicalNote, Sunny } from '@vicons/ionicons5' import { Chatbox, Home, Moon, MusicalNote, Sunny } from '@vicons/ionicons5';
import { useElementSize, useStorage } from '@vueuse/core' import { useElementSize, useStorage } from '@vueuse/core';
import { import {
MenuOption, MenuOption,
NAvatar, NAvatar,
NBackTop, NBackTop,
NButton, NButton,
NDivider,
NEllipsis, NEllipsis,
NIcon, NIcon,
NLayout, NLayout,
@@ -36,195 +36,237 @@ import {
NSwitch, NSwitch,
NText, NText,
useMessage, useMessage,
} from 'naive-ui' // NSpin 已默认导入,如果单独使用需确保导入
import { computed, h, onMounted, ref } from 'vue' } from 'naive-ui';
import { RouterLink, useRoute } from 'vue-router' import { computed, h, onMounted, ref, watch, defineAsyncComponent } from 'vue'; // 引入 watch
import { RouterLink, useRoute, useRouter } from 'vue-router'; // 引入 useRouter
const route = useRoute() // --- 响应式状态和常量 ---
const id = computed(() => { const route = useRoute();
return route.params.id const router = useRouter(); // 获取 router 实例
}) const message = useMessage();
const themeType = useStorage('Settings.Theme', ThemeType.Auto) const accountInfo = useAccount(); // 获取当前登录账户信息
const useAuth = useAuthStore(); // 获取认证状态 Store
const userInfo = ref<UserInfo>() // 路由参数
const biliUserInfo = ref() const id = computed(() => route.params.id);
const accountInfo = useAccount()
const useAuth = useAuthStore()
const message = useMessage()
const notfount = ref(false) // 主题设置
const themeType = useStorage('Settings.Theme', ThemeType.Auto);
const registerAndLoginModalVisiable = ref(false) // 用户和页面状态
const sider = ref() const userInfo = ref<UserInfo | null>(null); // 用户信息,初始化为 null
const { width } = useElementSize(sider) const biliUserInfo = ref<any>(null); // B站用户信息
const windowWidth = window.innerWidth const isLoading = ref(true); // 是否正在加载数据
const notFound = ref(false); // 是否未找到用户
// UI 控制状态
const registerAndLoginModalVisiable = ref(false); // 注册/登录弹窗可见性
const sider = ref(); // 侧边栏 DOM 引用
const { width: siderWidth } = useElementSize(sider); // 侧边栏宽度
const windowWidth = window.innerWidth; // 窗口宽度,用于响应式显示
// 侧边栏菜单项
const menuOptions = ref<MenuOption[]>([]); // 初始化为空数组
// --- 方法 ---
/** 渲染图标的辅助函数 */
function renderIcon(icon: unknown) { function renderIcon(icon: unknown) {
return () => h(NIcon, null, { default: () => h(icon as any) }) return () => h(NIcon, null, { default: () => h(icon as any) });
}
const menuOptions = ref<MenuOption[]>()
async function RequestBiliUserData() {
await fetch(FETCH_API + `https://workers.vrp.moe/api/bilibili/user-info/${userInfo.value?.biliId}`).then(
async (respone) => {
const data = await respone.json()
if (data.code == 0) {
biliUserInfo.value = data.card
} else {
throw new Error('Bili User API Error: ' + data.message)
}
},
)
}
function gotoAuthPage() {
if (!accountInfo.value?.biliUserAuthInfo) {
message.error('你尚未进行 Bilibili 认证, 请前往面板进行认证和绑定')
return
}
/*useAuthStore()
.setCurrentAuth(accountInfo.value?.biliUserAuthInfo.token)
.then(() => {
NavigateToNewTab('/bili-user')
})*/
NavigateToNewTab('/bili-user')
}
onMounted(async () => {
userInfo.value = await useUser(id.value?.toString())
if (!userInfo.value) {
notfount.value = true
} }
/** 根据 userInfo 更新侧边栏菜单 */
function updateMenuOptions() {
// 如果没有用户信息,清空菜单
if (!userInfo.value) {
menuOptions.value = [];
return;
}
// 基于 userInfo.extra.enableFunctions 构建菜单项
menuOptions.value = [ menuOptions.value = [
{ {
label: () => label: () => h(RouterLink, { to: { name: 'user-index' } }, { default: () => '主页' }),
h( key: 'user-index', icon: renderIcon(Home),
RouterLink, // 主页通常都显示
{ show: true
to: {
name: 'user-index',
},
},
{ default: () => '主页' },
),
key: 'user-index',
icon: renderIcon(Home),
}, },
{ {
label: () => label: () => h(RouterLink, { to: { name: 'user-songList' } }, { default: () => '歌单' }),
h( key: 'user-songList', icon: renderIcon(MusicalNote),
RouterLink, // 根据用户配置判断是否显示
{ show: userInfo.value?.extra?.enableFunctions.includes(FunctionTypes.SongList)
to: {
name: 'user-songList',
},
},
{ default: () => '歌单' },
),
show: (userInfo.value?.extra?.enableFunctions.indexOf(FunctionTypes.SongList) ?? -1) > -1,
key: 'user-songList',
icon: renderIcon(MusicalNote),
}, },
{ {
label: () => label: () => h(RouterLink, { to: { name: 'user-schedule' } }, { default: () => '日程' }),
h( key: 'user-schedule', icon: renderIcon(CalendarClock24Filled),
RouterLink, show: userInfo.value?.extra?.enableFunctions.includes(FunctionTypes.Schedule)
{
to: {
name: 'user-schedule',
},
},
{ default: () => '日程' },
),
show: (userInfo.value?.extra?.enableFunctions.indexOf(FunctionTypes.Schedule) ?? -1) > -1,
key: 'user-schedule',
icon: renderIcon(CalendarClock24Filled),
}, },
{ {
label: () => label: () => h(RouterLink, { to: { name: 'user-questionBox' } }, { default: () => '棉花糖 (提问箱)' }),
h( key: 'user-questionBox', icon: renderIcon(Chatbox),
RouterLink, show: userInfo.value?.extra?.enableFunctions.includes(FunctionTypes.QuestionBox)
{
to: {
name: 'user-questionBox',
},
},
{ default: () => '棉花糖 (提问箱' },
),
show: (userInfo.value?.extra?.enableFunctions.indexOf(FunctionTypes.QuestionBox) ?? -1) > -1,
key: 'user-questionBox',
icon: renderIcon(Chatbox),
}, },
{ {
label: () => label: () => h(RouterLink, { to: { name: 'user-video-collect' } }, { default: () => '视频征集' }),
h( key: 'user-video-collect', icon: renderIcon(VideoAdd20Filled),
RouterLink, show: userInfo.value?.extra?.enableFunctions.includes(FunctionTypes.VideoCollect)
{
to: {
name: 'user-video-collect',
},
},
{ default: () => '视频征集' },
),
show: (userInfo.value?.extra?.enableFunctions.indexOf(FunctionTypes.VideoCollect) ?? -1) > -1,
key: 'user-video-collect',
icon: renderIcon(VideoAdd20Filled),
}, },
{ {
label: () => label: () => h(RouterLink, { to: { name: 'user-goods' } }, { default: () => '积分' }),
h( key: 'user-goods', icon: renderIcon(BookCoins20Filled),
RouterLink, show: userInfo.value?.extra?.enableFunctions.includes(FunctionTypes.Point)
{
to: {
name: 'user-goods',
}, },
].filter(option => option.show !== false) as MenuOption[]; // 过滤掉 show 为 false 的菜单项
}
/** 获取 Bilibili 用户信息 */
async function RequestBiliUserData() {
// 确保 userInfo 和 biliId 存在
if (!userInfo.value?.biliId) return;
try {
const response = await fetch(FETCH_API + `https://workers.vrp.moe/api/bilibili/user-info/${userInfo.value.biliId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.code === 0) {
biliUserInfo.value = data.card; // 存储获取到的 B 站信息
} else {
console.error('Bili User API Error:', data.message);
// message.warning('获取B站信息失败: ' + data.message) // 可选: 轻微提示用户
}
} catch (error) {
console.error('Failed to fetch Bili user data:', error);
// message.error('获取B站信息时网络错误') // 可选: 提示用户网络问题
}
}
/** 获取 Vtsuru 用户信息和相关数据 */
async function fetchUserData(userId: string | string[] | undefined) {
// 验证 userId 的有效性
if (!userId || Array.isArray(userId)) {
notFound.value = true; // 标记为未找到
isLoading.value = false; // 加载结束
userInfo.value = null; // 清空用户信息
menuOptions.value = []; // 清空菜单
console.error("无效的用户 ID:", userId);
return;
}
// 重置状态,准备加载新数据
isLoading.value = true;
notFound.value = false;
userInfo.value = null;
menuOptions.value = [];
biliUserInfo.value = null;
try {
// 调用 API 获取用户信息
const fetchedUserInfo = await useUser(userId as string); // 强制转换为 string
if (!fetchedUserInfo) {
// 如果 API 返回 null 或 undefined则视为未找到
notFound.value = true;
userInfo.value = null;
} else {
// 成功获取用户信息
userInfo.value = fetchedUserInfo;
// 基于新的用户信息更新菜单
updateMenuOptions();
// 异步获取 B 站信息(不阻塞主流程)
await RequestBiliUserData();
}
} catch (error) {
console.error("获取用户信息时出错:", error);
message.error("加载用户信息时发生错误");
notFound.value = true; // 标记为未找到状态
userInfo.value = null;
} finally {
// 无论成功或失败,加载状态都结束
isLoading.value = false;
}
}
/** 跳转到 Bilibili 认证用户中心 */
function gotoAuthPage() {
if (!accountInfo.value?.biliUserAuthInfo) {
message.error('你尚未进行 Bilibili 认证, 请前往面板进行认证和绑定');
return;
}
NavigateToNewTab('/bili-user'); // 在新标签页打开
}
// --- Watcher ---
// 监听路由参数 id 的变化
watch(
() => route.params.id,
(newId, oldId) => {
// 只有当 newId 有效且与 oldId 不同时才重新加载数据
if (newId && newId !== oldId) {
fetchUserData(newId);
} else if (!newId) {
// 如果 id 从路由中移除,处理相应的状态
notFound.value = true;
isLoading.value = false;
userInfo.value = null;
menuOptions.value = [];
}
}, },
{ default: () => '积分' }, { immediate: true } // 关键: 组件挂载时立即执行一次 watcher触发初始数据加载
), );
show: (userInfo.value?.extra?.enableFunctions.indexOf(FunctionTypes.Point) ?? -1) > -1,
key: 'user-goods', // --- 组件模板 ---
icon: renderIcon(BookCoins20Filled),
},
]
await RequestBiliUserData()
})
</script> </script>
<template> <template>
<!-- 情况 1: 加载完毕 URL 中没有提供用户 ID -->
<NLayoutContent <NLayoutContent
v-if="!id" v-if="!id && !isLoading"
style="height: 100vh" class="center-container"
> >
<NResult <NResult
status="error" status="error"
title="输入的uId无效" title="未提供用户ID"
description="再检查检查" description="请检查访问的URL地址"
/> />
</NLayoutContent> </NLayoutContent>
<!-- 情况 2: 加载完毕但未找到指定 ID 的用户 -->
<NLayoutContent <NLayoutContent
v-else-if="notfount" v-else-if="notFound && !isLoading"
style="height: 100vh" class="center-container"
> >
<NResult <NResult
status="error" status="error"
title="未找到指定 uId 的用户" title="用户不存在"
description="或者是没有进行认证" description="无法找到指定ID的用户或者该用户未完成认证"
/> />
</NLayoutContent> </NLayoutContent>
<!-- 情况 3: 存在 ID (正在加载 加载成功且找到用户) -->
<NLayout <NLayout
v-else v-else
style="height: 100vh" style="height: 100vh"
> >
<NLayoutHeader style="height: 50px; padding: 5px 15px 5px 15px"> <!-- 顶部导航栏 -->
<NLayoutHeader class="layout-header">
<NPageHeader <NPageHeader
:subtitle="($route.meta.title as string) ?? ''" :subtitle="isLoading ? '加载中...' : ($route.meta.title as string) ?? ''"
style="margin-top: 6px" style="width: 100%"
> >
<!-- 右侧额外操作区域 -->
<template #extra> <template #extra>
<NSpace align="center"> <NSpace align="center">
<!-- 主题切换开关 -->
<NSwitch <NSwitch
:default-value="!isDarkMode" :value="themeType === ThemeType.Light"
@update:value=" :disabled="isLoading"
(value: string & number & boolean) => (themeType = value ? ThemeType.Light : ThemeType.Dark) title="切换亮/暗色主题"
" @update:value="(value) => (themeType = value ? ThemeType.Light : ThemeType.Dark)"
> >
<template #checked> <template #checked>
<NIcon :component="Sunny" /> <NIcon :component="Sunny" />
@@ -233,11 +275,12 @@ onMounted(async () => {
<NIcon :component="Moon" /> <NIcon :component="Moon" />
</template> </template>
</NSwitch> </NSwitch>
<template v-if="accountInfo.id"> <!-- 已登录用户操作 -->
<template v-if="accountInfo?.id">
<NSpace> <NSpace>
<!-- B站认证中心按钮 (如果已认证) -->
<NButton <NButton
v-if="useAuth.isAuthed || accountInfo.biliUserAuthInfo" v-if="useAuth.isAuthed || accountInfo.biliUserAuthInfo"
style="right: 0px; position: relative"
type="primary" type="primary"
tag="a" tag="a"
href="/bili-user" href="/bili-user"
@@ -250,8 +293,8 @@ onMounted(async () => {
</template> </template>
<span v-if="windowWidth >= 768"> 认证用户中心 </span> <span v-if="windowWidth >= 768"> 认证用户中心 </span>
</NButton> </NButton>
<!-- 主播后台按钮 -->
<NButton <NButton
style="right: 0px; position: relative"
type="primary" type="primary"
size="small" size="small"
@click="$router.push({ name: 'manage-index' })" @click="$router.push({ name: 'manage-index' })"
@@ -263,9 +306,9 @@ onMounted(async () => {
</NButton> </NButton>
</NSpace> </NSpace>
</template> </template>
<!-- 未登录用户操作 -->
<template v-else> <template v-else>
<NButton <NButton
style="right: 0px; position: relative"
type="primary" type="primary"
@click="registerAndLoginModalVisiable = true" @click="registerAndLoginModalVisiable = true"
> >
@@ -274,37 +317,37 @@ onMounted(async () => {
</template> </template>
</NSpace> </NSpace>
</template> </template>
<!-- 页面标题 (网站 Logo) -->
<template #title> <template #title>
<NButton <span>
text
tag="a"
@click="$router.push({ name: 'index' })"
>
<NText <NText
strong strong
style="font-size: 1.5rem; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)" class="site-title"
> >
VTSURU VTSURU
</NText> </NText>
</NButton> </span>
</template> </template>
</NPageHeader> </NPageHeader>
</NLayoutHeader> </NLayoutHeader>
<!-- 主体布局 (包含侧边栏和内容区) -->
<NLayout <NLayout
has-sider has-sider
style="height: calc(100vh - --vtsuru-header-height)" class="main-layout-body"
> >
<!-- 左侧边栏 -->
<NLayoutSider <NLayoutSider
ref="sider" ref="sider"
show-trigger show-trigger
default-collapsed
collapse-mode="width" collapse-mode="width"
:collapsed-width="64" :collapsed-width="64"
:width="180" :width="180"
:native-scrollbar="false" :native-scrollbar="false"
style="height: calc(100vh - --vtsuru-header-height)" :default-collapsed="windowWidth < 768"
style="height: 100%"
> >
<Transition> <!-- 用户头像和昵称 (加载完成后显示) -->
<div <div
v-if="userInfo?.streamerInfo" v-if="userInfo?.streamerInfo"
style="margin-top: 8px" style="margin-top: 8px"
@@ -315,16 +358,16 @@ onMounted(async () => {
align="center" align="center"
> >
<NAvatar <NAvatar
class="sider-avatar"
:src="userInfo.streamerInfo.faceUrl" :src="userInfo.streamerInfo.faceUrl"
:img-props="{ referrerpolicy: 'no-referrer' }" :img-props="{ referrerpolicy: 'no-referrer' }"
round round
bordered bordered
:style="{ title="前往用户B站主页"
boxShadow: isDarkMode ? 'rgb(195 192 192 / 35%) 0px 0px 8px' : '0 2px 3px rgba(0, 0, 0, 0.1)', @click="NavigateToNewTab(`https://space.bilibili.com/${userInfo.biliId}`)"
}"
/> />
<NEllipsis <NEllipsis
v-if="width > 100" v-if="siderWidth > 100"
style="max-width: 100%" style="max-width: 100%"
> >
<NText strong> <NText strong>
@@ -333,87 +376,266 @@ onMounted(async () => {
</NEllipsis> </NEllipsis>
</NSpace> </NSpace>
</div> </div>
</Transition> <!-- 侧边栏加载状态 -->
<div
v-else-if="isLoading"
class="sider-loading"
>
<NSpin size="small" />
</div>
<NDivider style="margin: 0; margin-top: 5px;" />
<!-- 导航菜单 -->
<NMenu <NMenu
:default-value="$route.name?.toString()" :value="route.name?.toString()"
:collapsed-width="64" :collapsed-width="64"
:collapsed-icon-size="22" :collapsed-icon-size="22"
:options="menuOptions" :options="menuOptions"
:disabled="isLoading"
class="sider-menu"
/> />
<!-- 侧边栏底部链接 -->
<div class="sider-footer">
<!-- 仅在侧边栏展开时显示 -->
<NSpace <NSpace
v-if="width > 150" v-if="siderWidth > 150"
justify="center" justify="center"
align="center" align="center"
vertical vertical
size="small"
style="width: 100%;"
> >
<NText depth="3"> <NText
有更多功能建议请 depth="3"
<NButton class="footer-text"
>
有有更多功能建议请 <NButton
text text
type="info" type="info"
tag="a" tag="a"
href="/feedback" href="/feedback"
target="_blank" target="_blank"
size="tiny"
> >
反馈 反馈
</NButton> </NButton>
</NText> </NText>
<NText depth="3"> <NDivider style="margin: 0; width: 100%" />
<NText
depth="3"
class="footer-text"
>
<NButton <NButton
text text
type="info" type="info"
tag="a" tag="a"
href="/about" href="/about"
target="_blank" target="_blank"
size="tiny"
> >
关于本站 关于本站
</NButton> </NButton>
</NText> </NText>
</NSpace> </NSpace>
</div>
</NLayoutSider> </NLayoutSider>
<NLayout style="height: 100%">
<!-- 右侧内容区域布局容器 -->
<NLayout class="content-layout-container">
<!-- 全局加载动画 (覆盖内容区) -->
<div <div
v-if="isLoading"
class="loading-container"
>
<NSpin size="large" />
</div>
<!-- 实际内容区域 (加载完成且找到用户时显示) -->
<div
v-else-if="userInfo && !notFound"
class="viewer-page-content" class="viewer-page-content"
:style="`box-shadow:${isDarkMode ? 'rgb(28 28 28 / 9%) 5px 5px 6px inset, rgba(139, 139, 139, 0.09) -5px -5px 6px inset' : 'inset 5px 5px 6px #8b8b8b17, inset -5px -5px 6px #8b8b8b17;'}`" :style="`box-shadow:${isDarkMode ? 'rgb(28 28 28 / 9%) 5px 5px 6px inset, rgba(139, 139, 139, 0.09) -5px -5px 6px inset' : 'inset 5px 5px 6px #8b8b8b17, inset -5px -5px 6px #8b8b8b17;'}`"
> >
<RouterView <!-- 路由视图和动画 -->
v-if="userInfo" <RouterView v-slot="{ Component }">
v-slot="{ Component }" <Transition
name="fade-slide"
mode="out-in"
:appear="true"
> >
<KeepAlive> <KeepAlive>
<component <component
:is="Component" :is="Component"
:key="route.fullPath"
:bili-info="biliUserInfo" :bili-info="biliUserInfo"
:user-info="userInfo" :user-info="userInfo"
/> />
</KeepAlive> </KeepAlive>
</Transition>
</RouterView> </RouterView>
<template v-else> <NBackTop
<NSpin show /> :right="40"
</template> :bottom="40"
<NBackTop /> :listen-to="'.viewer-page-content'"
/>
</div> </div>
<!-- 如果 !isLoading && notFound, 会显示顶部的 NResult这里不需要 else -->
</NLayout> </NLayout>
</NLayout> </NLayout>
</NLayout> </NLayout>
<!-- 注册/登录弹窗 -->
<NModal <NModal
v-model:show="registerAndLoginModalVisiable" v-model:show="registerAndLoginModalVisiable"
preset="card"
style="width: 500px; max-width: 90vw" style="width: 500px; max-width: 90vw"
title="注册 / 登录"
:auto-focus="false"
:mask-closable="false"
> >
<div> <!-- 异步加载注册登录组件优化初始加载性能 -->
<RegisterAndLogin /> <RegisterAndLogin @close="registerAndLoginModalVisiable = false" />
</div>
</NModal> </NModal>
</template> </template>
<style lang="stylus" scoped> <style lang="stylus" scoped>
// --- CSS 变量定义 ---
:root {
--vtsuru-header-height: 50px; // 顶部导航栏高度
--vtsuru-content-padding: 20px; // 内容区域内边距
}
// --- 布局样式 ---
.center-container {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.layout-header {
height: var(--vtsuru-header-height);
padding: 0 15px; // 左右内边距
display: flex;
align-items: center;
border-bottom: 1px solid var(--n-border-color); // 底部边框
flex-shrink: 0; // 防止头部被压缩
}
.site-title {
font-size: 1.5rem;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.main-layout-body {
height: calc(100vh - var(--vtsuru-header-height)); // 填充剩余高度
}
.sider-avatar {
box-shadow: var(--n-avatar-box-shadow, 0 2px 3px rgba(0, 0, 0, 0.1)); // 使用 Naive UI 变量或默认值
cursor: pointer;
transition: transform 0.2s ease; // 添加悬浮效果
&:hover {
transform: scale(1.1);
}
}
.sider-username {
max-width: 90%;
margin: 8px auto 0;
font-size: 14px; // 调整字体大小
}
.sider-loading {
display: flex;
justify-content: center;
align-items: center; // 垂直居中
padding: 30px 0; // 增加上下间距
height: 98px; // 大致等于头像+昵称的高度,防止跳动
}
.sider-menu {
margin-top: 10px;
width: 100%; // 确保菜单宽度正确
}
.sider-footer {
position: absolute;
bottom: 20px;
width: 100%;
text-align: center;
padding: 0 5px; // 左右留白,防止文字贴边
box-sizing: border-box;
}
.footer-text {
font-size: 12px;
}
// --- 内容区域样式 ---
.content-layout-container {
height: 100%;
min-height: 100%; // 保证最小高度,防止塌陷
overflow: hidden; // 关键: 隐藏此容器自身的滚动条,剪切内部溢出内容
position: relative; // 关键: 作为内部绝对定位元素(过渡中的组件)的定位基准
}
.loading-container {
// ... (保持不变) ...
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
background-color: var(--n-body-color);
position: absolute; // 相对于 content-layout-container 定位
top: 0;
left: 0;
z-index: 5;
}
.viewer-page-content { .viewer-page-content {
height: 100%; height: 100%;
border-radius: 18px; min-height: 100%; // 同样保证最小高度
border-radius: 8px;
padding: var(--vtsuru-content-padding); padding: var(--vtsuru-content-padding);
height: calc(100vh - var(--vtsuru-header-height));
margin-right: 10px;
box-sizing: border-box; box-sizing: border-box;
overflow-y: auto; overflow-y: auto; // 允许内容 Y 轴滚动
overflow-x: hidden; // 禁止内容 X 轴滚动 (可选,但通常推荐)
position: relative; // 为内部非绝对定位的内容提供上下文,例如 NBackTop
background-color: var(--n-card-color);
box-shadow: var(--content-shadow);
}
// --- 路由过渡动画 ---
.fade-slide-enter-active,
.fade-slide-leave-active {
transition: opacity 0.25s ease, transform 0.25s ease;
// 关键: 相对于 content-layout-container 定位
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%; // 让过渡元素也撑满容器高度
// 关键: 保持内边距和盒模型一致
padding: var(--vtsuru-content-padding);
box-sizing: border-box;
// 关键: 背景色防止透视
background-color: var(--n-card-color); // 使用内容区的背景色
z-index: 1;
}
.fade-slide-enter-from {
opacity: 0;
transform: translateX(15px);
}
.fade-slide-leave-to {
opacity: 0;
transform: translateX(-15px);
}
// --- 返回顶部按钮 ---
.n-back-top {
z-index: 10; // 确保在最上层
} }
</style> </style>

View File

@@ -30,7 +30,6 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import VueTurnstile from 'vue-turnstile' import VueTurnstile from 'vue-turnstile'
const { biliInfo, userInfo } = defineProps<{ const { biliInfo, userInfo } = defineProps<{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
biliInfo: any | undefined biliInfo: any | undefined
userInfo: UserInfo | undefined userInfo: UserInfo | undefined
}>() }>()
@@ -172,40 +171,50 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<div style="max-width: 700px; margin: 0 auto" title="提问"> <div
style="max-width: 700px; margin: 0 auto"
title="提问"
>
<NCard embedded> <NCard embedded>
<NSpace vertical> <NSpace vertical>
<NCard v-if="tags.length > 0" title="投稿话题 (可选)" size="small"> <NCard
v-if="tags.length > 0"
title="投稿话题 (可选)"
size="small"
>
<NSpace> <NSpace>
<NTag <NTag
v-for="tag in tags" v-for="tag in tags"
:key="tag" :key="tag"
@click="onSelectTag(tag)"
style="cursor: pointer" style="cursor: pointer"
:bordered="false" :bordered="false"
:type="selectedTag == tag ? 'primary' : 'default'" :type="selectedTag == tag ? 'primary' : 'default'"
@click="onSelectTag(tag)"
> >
{{ tag }} {{ tag }}
</NTag> </NTag>
</NSpace> </NSpace>
</NCard> </NCard>
<NSpace align="center" justify="center"> <NSpace
align="center"
justify="center"
>
<NInput <NInput
v-model:value="questionMessage"
:disabled="isSelf" :disabled="isSelf"
show-count show-count
maxlength="5000" maxlength="5000"
type="textarea" type="textarea"
:count-graphemes="countGraphemes" :count-graphemes="countGraphemes"
v-model:value="questionMessage"
style="width: 300px" style="width: 300px"
/> />
<NUpload <NUpload
v-model:file-list="fileList"
:max="1" :max="1"
accept=".png,.jpg,.jpeg,.gif,.svg,.webp,.ico" accept=".png,.jpg,.jpeg,.gif,.svg,.webp,.ico"
list-type="image-card" list-type="image-card"
:disabled="!accountInfo.id || isSelf" :disabled="!accountInfo.id || isSelf"
:default-upload="false" :default-upload="false"
v-model:file-list="fileList"
@update:file-list="OnFileListChange" @update:file-list="OnFileListChange"
> >
+ 上传图片 + 上传图片
@@ -213,14 +222,31 @@ onUnmounted(() => {
</NSpace> </NSpace>
<NDivider style="margin: 10px 0 10px 0" /> <NDivider style="margin: 10px 0 10px 0" />
<NSpace align="center"> <NSpace align="center">
<NAlert v-if="!accountInfo.id && !isSelf" type="warning"> 只有注册用户才能够上传图片 </NAlert> <NAlert
v-if="!accountInfo.id && !isSelf"
type="warning"
>
只有注册用户才能够上传图片
</NAlert>
</NSpace> </NSpace>
<NSpace v-if="accountInfo.id" vertical> <NSpace
<NCheckbox :disabled="isSelf" v-model:checked="isAnonymous" label="匿名提问" /> v-if="accountInfo.id"
vertical
>
<NCheckbox
v-model:checked="isAnonymous"
:disabled="isSelf"
label="匿名提问"
/>
<NDivider style="margin: 10px 0 10px 0" /> <NDivider style="margin: 10px 0 10px 0" />
</NSpace> </NSpace>
<NSpace justify="center"> <NSpace justify="center">
<NButton :disabled="isSelf" type="primary" :loading="isSending || !token" @click="SendQuestion"> <NButton
:disabled="isSelf"
type="primary"
:loading="isSending || !token"
@click="SendQuestion"
>
发送 发送
</NButton> </NButton>
<NButton <NButton
@@ -233,24 +259,46 @@ onUnmounted(() => {
</NSpace> </NSpace>
<VueTurnstile <VueTurnstile
ref="turnstile" ref="turnstile"
:site-key="TURNSTILE_KEY"
v-model="token" v-model="token"
:site-key="TURNSTILE_KEY"
theme="auto" theme="auto"
style="text-align: center" style="text-align: center"
/> />
<NAlert v-if="isSelf" type="warning"> 不能给自己提问 </NAlert> <NAlert
v-if="isSelf"
type="warning"
>
不能给自己提问
</NAlert>
</NSpace> </NSpace>
</NCard> </NCard>
<NDivider> 公开回复 </NDivider> <NDivider> 公开回复 </NDivider>
<NList v-if="publicQuestions.length > 0"> <NList v-if="publicQuestions.length > 0">
<NListItem v-for="item in publicQuestions" :key="item.id"> <NListItem
<NCard :embedded="!item.isReaded" hoverable size="small"> v-for="item in publicQuestions"
:key="item.id"
>
<NCard
:embedded="!item.isReaded"
hoverable
size="small"
>
<template #header> <template #header>
<NSpace :size="0" align="center"> <NSpace
<NText depth="3" style="font-size: small"> :size="0"
align="center"
>
<NText
depth="3"
style="font-size: small"
>
<NTooltip> <NTooltip>
<template #trigger> <template #trigger>
<NTime :time="item.sendAt" :to="Date.now()" type="relative" /> <NTime
:time="item.sendAt"
:to="Date.now()"
type="relative"
/>
</template> </template>
<NTime /> <NTime />
</NTooltip> </NTooltip>
@@ -259,12 +307,29 @@ onUnmounted(() => {
</template> </template>
<NCard style="text-align: center"> <NCard style="text-align: center">
{{ item.question.message }} {{ item.question.message }}
<br /> <br>
<NImage v-if="item.question.image" :src="item.question.image" height="100" lazy /> <NImage
v-if="item.question.image"
:src="item.question.image"
height="100"
lazy
/>
</NCard> </NCard>
<template v-if="item.answer" #footer> <template
<NSpace align="center" :size="6" :wrap="false"> v-if="item.answer"
<NAvatar :src="AVATAR_URL + userInfo?.biliId + '?size=64'" circle :size="45" :img-props="{ referrerpolicy: 'no-referrer' }" /> #footer
>
<NSpace
align="center"
:size="6"
:wrap="false"
>
<NAvatar
:src="AVATAR_URL + userInfo?.biliId + '?size=64'"
circle
:size="45"
:img-props="{ referrerpolicy: 'no-referrer' }"
/>
<NDivider vertical /> <NDivider vertical />
<NText style="font-size: 16px"> <NText style="font-size: 16px">
{{ item.answer?.message }} {{ item.answer?.message }}

View File

@@ -1,4 +1,5 @@
<template> <template>
<div>
<NSpin <NSpin
v-if="isLoading" v-if="isLoading"
show show
@@ -37,6 +38,7 @@
:config="selectedTemplateConfig" :config="selectedTemplateConfig"
/> />
</NModal> </NModal>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -135,7 +137,8 @@ import { computed, onMounted, ref, watch } from 'vue';
await DownloadConfig(selectedTemplate.value!.settingName, props.userInfo?.id) await DownloadConfig(selectedTemplate.value!.settingName, props.userInfo?.id)
.then((data) => { .then((data) => {
if (data.msg) { if (data.msg) {
message.error('加载失败: ' + data.msg); //message.error('加载失败: ' + data.msg);
console.log('当前模板没有配置, 使用默认配置');
} else { } else {
currentConfig.value = data.data; currentConfig.value = data.data;
} }

View File

@@ -1,9 +1,11 @@
<template> <template>
<div>
<component <component
:is="componentType" :is="componentType"
:user-info="userInfo" :user-info="userInfo"
:bili-info="biliInfo" :bili-info="biliInfo"
/> />
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@@ -39,6 +39,7 @@ async function get() {
</script> </script>
<template> <template>
<div>
<NSpin :show="isLoading"> <NSpin :show="isLoading">
<NFlex justify="center"> <NFlex justify="center">
<NEmpty <NEmpty
@@ -60,4 +61,5 @@ async function get() {
</NList> </NList>
</NFlex> </NFlex>
</NSpin> </NSpin>
</div>
</template> </template>

View File

@@ -472,10 +472,10 @@ export const Config = defineTemplateConfig([
<!-- Social Links (Visible on Hover) --> <!-- Social Links (Visible on Hover) -->
<div class="social-links"> <div class="social-links">
<p class="social-links-title"> <p class="social-links-title">
关于 关于
</p> </p>
<p class="social-links-subtitle"> <p class="social-links-subtitle">
{{ props.config?.longDescription }} {{ props.config?.longDescription ?? '暂时没有填写介绍' }}
</p> </p>
<div class="social-icons-bar"> <div class="social-icons-bar">
<!-- Add actual icons here --> <!-- Add actual icons here -->
@@ -918,7 +918,7 @@ html.dark .filter-input::placeholder {
top: 0; left: 0; right: 0; bottom: 0; top: 0; left: 0; right: 0; bottom: 0;
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
background-color: rgba(0, 0, 0, 0.1); /* Optional overlay */ background-color: rgba(80, 80, 80, 0.1); /* Optional overlay */
border-radius: inherit; /* Inherit rounding */ border-radius: inherit; /* Inherit rounding */
z-index: 1; /* Below content */ z-index: 1; /* Below content */
pointer-events: none; pointer-events: none;