- 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
225 lines
5.1 KiB
Vue
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>
|