Files
HAOVP/frontend/video-frontend-web/src/views/user/settings.vue
T

371 lines
8.6 KiB
Vue

<template>
<div class="settings-page">
<div class="page-shell settings-shell">
<div class="page-head">
<div class="head-main">
<strong>账号设置</strong>
<span>{{ accountInfo.username }}</span>
</div>
</div>
<div class="settings-grid">
<div class="panel-card profile-card">
<div class="profile-top">
<div class="profile-copy">
<strong>{{ accountInfo.nickname }}</strong>
<span>{{ accountInfo.region }}</span>
</div>
<div class="profile-tags">
<span>Lv.{{ accountInfo.level }}</span>
<span>{{ accountInfo.vipLabel }}</span>
</div>
</div>
<div class="status-list">
<div class="status-item">
<span>公开范围</span>
<strong>{{ privacyLabel }}</strong>
</div>
<div class="status-item">
<span>通知偏好</span>
<strong>{{ notificationSummary }}</strong>
</div>
<div class="status-item">
<span>未读消息</span>
<strong>{{ unreadSummary }}</strong>
</div>
<div class="status-item">
<span>资料完整度</span>
<strong>{{ profileCompleteness }}</strong>
</div>
</div>
</div>
<div class="panel-card menu-card">
<button
class="menu-item"
@click="router.push('/settings/profile')"
>
<span>编辑资料</span>
<small>头像、昵称、签名</small>
</button>
<button
class="menu-item"
@click="router.push('/settings/privacy')"
>
<span>隐私设置</span>
<small>{{ privacyLabel }}</small>
</button>
<button
class="menu-item"
@click="router.push('/settings/notification')"
>
<span>通知设置</span>
<small>{{ notificationSummary }}</small>
</button>
<button
class="menu-item"
@click="router.push('/about')"
>
<span>关于项目</span>
<small>查看项目信息</small>
</button>
</div>
<div class="panel-card runtime-card">
<div class="status-list">
<div class="status-item">
<span>最近登录</span>
<strong>{{ accountInfo.lastLoginTime }}</strong>
</div>
<div class="status-item">
<span>登录 IP</span>
<strong>{{ accountInfo.lastLoginIp }}</strong>
</div>
<div class="status-item">
<span>缓存占用</span>
<strong>{{ cacheSize }}</strong>
</div>
<div class="status-item">
<span>API 地址</span>
<strong>{{ apiTarget }}</strong>
</div>
</div>
<div class="runtime-actions">
<button
class="text-link"
@click="refreshStats"
>
刷新状态
</button>
<button
class="text-link"
@click="clearCache"
>
清除缓存
</button>
</div>
</div>
</div>
<van-button
type="danger"
block
class="logout-btn"
@click="handleLogout"
>
退出登录
</van-button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { showConfirmDialog, showToast, showSuccessToast } from 'vant'
import { useUserStore } from '@/stores/user.ts'
import { useMessageStore } from '@/stores/message'
import { userApi } from '@/api/user.ts'
const router = useRouter()
const userStore = useUserStore()
const messageStore = useMessageStore()
const cacheSize = ref('0KB')
const settings = ref({
privacyLevel: 1,
commentNotify: 1,
likeNotify: 1,
followNotify: 1,
systemNotify: 1
})
const apiTarget = import.meta.env.VITE_PROXY_TARGET || window.location.origin
const accountInfo = computed(() => ({
username: userStore.authUser?.username || '-',
nickname: userStore.userInfo?.nickname || '-',
region: userStore.userInfo?.region || '未设置地区',
level: userStore.userInfo?.level || 1,
vipLabel: userStore.userInfo?.vipLevel ? `VIP ${userStore.userInfo.vipLevel}` : '普通用户',
lastLoginTime: userStore.authUser?.lastLoginTime || '未记录',
lastLoginIp: userStore.authUser?.lastLoginIp || '未记录'
}))
const privacyLabel = computed(() => ({ 1: '公开', 2: '仅关注可见', 3: '仅自己可见' }[settings.value.privacyLevel] || '未设置'))
const notificationSummary = computed(() => {
const enabled: string[] = []
if (settings.value.commentNotify) enabled.push('评论')
if (settings.value.likeNotify) enabled.push('点赞')
if (settings.value.followNotify) enabled.push('关注')
if (settings.value.systemNotify) enabled.push('系统')
return enabled.length ? enabled.join(' / ') : '全部关闭'
})
const unreadSummary = computed(() => `私信 ${messageStore.unreadMessageCount} · 通知 ${messageStore.unreadNotificationCount}`)
const profileCompleteness = computed(() => {
const fields = [
!!userStore.userInfo?.nickname,
!!userStore.userInfo?.avatar,
!!userStore.userInfo?.signature,
!!userStore.userInfo?.region,
!!userStore.userInfo?.birthday
]
return `${Math.round((fields.filter(Boolean).length / fields.length) * 100)}%`
})
const calcStorageSize = () => {
const total = [localStorage, sessionStorage].reduce((sum, storage) => {
return sum + Object.keys(storage).reduce((inner, key) => inner + key.length + (storage.getItem(key)?.length || 0), 0)
}, 0)
if (total < 1024) return `${total} B`
if (total < 1024 * 1024) return `${(total / 1024).toFixed(1)} KB`
return `${(total / (1024 * 1024)).toFixed(1)} MB`
}
const refreshStats = async () => {
try {
if (!userStore.authUser) await userStore.fetchAuthUser()
await userStore.fetchUserInfo()
settings.value = await userApi.getSettings()
await messageStore.fetchAllUnreadCount()
cacheSize.value = calcStorageSize()
} catch {
// keep current data
}
}
const clearCache = () => {
sessionStorage.clear()
cacheSize.value = calcStorageSize()
showToast('会话缓存已清除')
}
const handleLogout = async () => {
try {
await showConfirmDialog({
title: '提示',
message: '确定要退出登录吗?'
})
await userStore.logout()
showSuccessToast('已退出登录')
router.push('/')
} catch {
// cancel
}
}
onMounted(refreshStats)
</script>
<style scoped lang="scss">
.settings-page {
min-height: 100vh;
background: #f4f5f7;
padding-bottom: 36px;
}
.settings-shell {
padding-top: 18px;
}
.page-head,
.panel-card {
background: #fff;
border-radius: 10px;
box-shadow: var(--shadow-sm);
}
.page-head {
padding: 16px 18px;
margin-bottom: 16px;
}
.head-main {
display: grid;
gap: 4px;
strong {
font-size: 24px;
}
span {
color: #9499a0;
font-size: 13px;
}
}
.settings-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.panel-card {
padding: 18px;
}
.profile-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.profile-copy {
display: grid;
gap: 4px;
strong {
font-size: 20px;
}
span {
color: #61666d;
font-size: 13px;
}
}
.profile-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
span {
padding: 4px 8px;
border-radius: 999px;
background: #f6f7f8;
font-size: 12px;
color: #61666d;
}
}
.status-list {
display: grid;
gap: 10px;
}
.status-item {
display: grid;
gap: 6px;
padding: 12px 14px;
border-radius: 8px;
background: #f6f7f8;
span {
font-size: 12px;
color: #9499a0;
}
strong {
color: #18191c;
word-break: break-all;
}
}
.menu-card {
display: grid;
gap: 10px;
}
.menu-item {
padding: 14px;
border-radius: 8px;
background: #f6f7f8;
text-align: left;
display: grid;
gap: 4px;
span {
font-size: 15px;
color: #18191c;
}
small {
color: #9499a0;
font-size: 12px;
}
}
.runtime-actions {
margin-top: 14px;
display: flex;
gap: 16px;
}
.text-link {
color: var(--link-color);
font-size: 13px;
}
.logout-btn {
margin-top: 18px;
}
@media (max-width: 1100px) {
.settings-grid {
grid-template-columns: 1fr;
}
}
</style>