Files
sionrui/frontend/app/web-gold/src/components/SidebarNav.vue
sion123 7b32191987 feat: enhance sidebar quota display with progress bar and improve upload modal UX
- Replace percentage-based quota with point-based display in sidebar
- Add visual progress bar for remaining quota with gradient styling
- Implement upload progress tracking in material upload modal
- Add loading indicators and progress information during file uploads
- Prevent modal interaction while uploading by disabling close controls
- Show current upload status including file index and completion percentage
2026-03-03 22:15:06 +08:00

225 lines
5.1 KiB
Vue

<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { navConfig, navIcons } from '@/router'
const route = useRoute()
const userStore = useUserStore()
function filterVisibleGroups(config, isLoggedIn) {
return config
.filter(group => !group.requiresAuth || isLoggedIn)
.map(group => ({
...group,
items: group.items.filter(item => !item.requiresAuth || isLoggedIn)
}))
.filter(group => group.items.length > 0)
}
const visibleNavConfig = computed(() => {
return filterVisibleGroups(navConfig, userStore.isLoggedIn)
})
// 脱敏手机号
const maskedMobile = computed(() => {
const mobile = userStore.mobile
if (!mobile) return '未设置'
return mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
})
// 剩余额度百分比
const remainingPercent = computed(() => {
const total = userStore.totalStorage
const remaining = userStore.remainingStorage
if (total === 0) return 0
return Math.min(100, Math.round((remaining / total) * 100))
})
</script>
<template>
<aside class="sidebar">
<nav class="sidebar__nav">
<div v-for="group in visibleNavConfig" :key="group.group" class="nav-group">
<div class="nav-group__title">{{ group.group }}</div>
<router-link
v-for="item in group.items"
:key="item.name"
:to="{ name: item.name, ...(item.params && { params: item.params }) }"
class="nav-item"
:class="{ 'is-active': route.name === item.name }"
custom
v-slot="{ navigate }"
>
<button class="nav-item" @click="navigate">
<span class="nav-item__icon" v-html="navIcons[item.icon]"></span>
<span class="nav-item__label">{{ item.name }}</span>
</button>
</router-link>
</div>
</nav>
<!-- 底部用户信息卡片 -->
<router-link
v-if="userStore.isLoggedIn"
to="/user/profile"
class="sidebar__footer"
>
<div class="user-card">
<div class="user-card__mobile">{{ maskedMobile }}</div>
<div class="user-card__quota">
<span>剩余额度 {{ userStore.remainingPoints }} </span>
<div class="quota-progress">
<div class="quota-progress__bar" :style="{ width: remainingPercent + '%' }"></div>
</div>
</div>
</div>
</router-link>
</aside>
</template>
<style scoped>
.sidebar {
position: sticky;
top: 70px;
height: calc(100vh - 70px);
width: 220px;
border-right: 1px solid var(--color-border);
background: var(--color-surface);
display: flex;
flex-direction: column;
}
.sidebar__nav {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
padding: 12px;
gap: 6px;
}
.nav-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.nav-group__title {
height: 30px;
display: flex;
align-items: center;
padding: 0 8px;
font-size: var(--font-small-size);
color: var(--color-text-secondary);
letter-spacing: .06em;
}
.nav-item {
height: 40px;
border-radius: 12px;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
color: var(--color-gray-600);
background: transparent;
border: 1px solid transparent;
cursor: pointer;
transition: background .2s ease, color .2s ease, box-shadow .2s ease, transform .12s ease, border-color .2s ease;
width: 100%;
text-align: left;
font-size: 14px;
font-weight: 400;
}
.nav-item:hover {
background: var(--color-gray-50);
color: var(--color-gray-700);
}
.nav-item.is-active {
background: var(--color-primary-50);
color: var(--color-primary-700);
border-color: transparent;
}
.nav-item.is-active:hover {
background: var(--color-primary-100);
color: var(--color-primary-700);
}
.nav-item__icon {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.nav-item__label {
font-size: 14px;
}
/* 底部用户信息卡片 */
.sidebar__footer {
flex-shrink: 0;
padding: 12px;
border-top: 1px solid var(--color-border);
text-decoration: none;
}
.user-card {
padding: 12px;
border-radius: 12px;
background: var(--color-gray-50);
cursor: pointer;
transition: background .2s ease, transform .12s ease;
}
.user-card:hover {
background: var(--color-gray-100);
transform: translateY(-1px);
}
.user-card__mobile {
font-size: 14px;
font-weight: 500;
color: var(--color-gray-700);
margin-bottom: 6px;
}
.user-card__quota {
font-size: 12px;
color: var(--color-text-secondary);
}
.quota-progress {
margin-top: 6px;
height: 6px;
background: var(--color-gray-100);
border-radius: 3px;
overflow: hidden;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.06);
}
.quota-progress__bar {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #60a5fa);
border-radius: 3px;
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
box-shadow: 0 1px 3px rgba(59, 130, 246, 0.4);
}
.quota-progress__bar::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.3), transparent);
border-radius: 3px 3px 0 0;
}
</style>