refactor(frontend): Rework home layout and user interactions
This commit is contained in:
@@ -1,50 +1,203 @@
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<div class="settings-shell">
|
||||
<section class="settings-hero">
|
||||
<h2>账号设置</h2>
|
||||
<p>管理个人资料、隐私选项、通知偏好和应用信息。</p>
|
||||
</section>
|
||||
<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="settings-column">
|
||||
<van-cell-group inset>
|
||||
<van-cell title="账号与安全" is-link to="/settings/profile" />
|
||||
<van-cell title="隐私设置" is-link to="/settings/privacy" />
|
||||
<van-cell title="通知设置" is-link to="/settings/notification" />
|
||||
</van-cell-group>
|
||||
<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="settings-column">
|
||||
<van-cell-group inset>
|
||||
<van-cell title="清除缓存" :value="cacheSize" @click="clearCache" />
|
||||
<van-cell title="关于我们" is-link to="/about" />
|
||||
<van-cell title="版本号" value="1.0.0" />
|
||||
</van-cell-group>
|
||||
<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="logout-btn">
|
||||
<van-button type="danger" block @click="handleLogout">退出登录</van-button>
|
||||
<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 { ref } from 'vue'
|
||||
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 = () => {
|
||||
showToast('缓存已清除')
|
||||
cacheSize.value = '0KB'
|
||||
sessionStorage.clear()
|
||||
cacheSize.value = calcStorageSize()
|
||||
showToast('会话缓存已清除')
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
@@ -57,60 +210,159 @@ const handleLogout = async () => {
|
||||
showSuccessToast('已退出登录')
|
||||
router.push('/')
|
||||
} catch {
|
||||
// 取消
|
||||
// cancel
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(refreshStats)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
min-height: 100vh;
|
||||
background: #f6f7fb;
|
||||
padding-bottom: 60px;
|
||||
background: #f4f5f7;
|
||||
padding-bottom: 36px;
|
||||
}
|
||||
|
||||
.settings-shell {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 12px 28px;
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.settings-hero {
|
||||
padding: 24px 26px;
|
||||
border-radius: 28px;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: #fff;
|
||||
margin-bottom: 18px;
|
||||
.page-head,
|
||||
.panel-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 28px;
|
||||
.page-head {
|
||||
padding: 16px 18px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.head-main {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
|
||||
strong {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
span {
|
||||
color: #9499a0;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.settings-column :deep(.van-cell-group) {
|
||||
margin: 0;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.08);
|
||||
.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: 768px) {
|
||||
@media (max-width: 1100px) {
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user