371 lines
8.6 KiB
Vue
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>
|