优化移动端界面,新增退单管理功能,添加相关API接口,更新权限设置,调整布局以支持响应式设计,提升用户体验。

This commit is contained in:
zyh
2025-08-27 21:20:05 +08:00
parent 4060a1c6e9
commit 7f4c2ca831
15 changed files with 3595 additions and 198 deletions

View File

@@ -1,18 +1,26 @@
<template>
<div class="admin-layout">
<aside class="sider">
<!-- 移动端遮罩层 -->
<div
v-if="isMobile && !collapsed"
class="mobile-overlay"
@click="collapsed = true"
></div>
<aside :class="['sider', { 'sider-mobile': isMobile, 'sider-collapsed': isMobile && collapsed }]">
<div class="brand">
<img class="logo" src="https://vuejs.org/images/logo.png" alt="logo" />
<span class="name">管理后台</span>
<span v-show="!collapsed || !isMobile" class="name">管理后台</span>
</div>
<el-menu
class="menu"
router
:default-active="$route.name"
:collapse="collapsed"
:collapse="collapsed && !isMobile"
background-color="#001529"
text-color="#bfcbd9"
active-text-color="#fff"
@select="onMenuSelect"
>
<el-menu-item v-if="canAccessUsers" index="Users" :route="{ name: 'Users' }">
@@ -26,6 +34,11 @@
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M10.59 13.41c.41.39.41 1.03 0 1.42c-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0a5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24a2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0-4.24zm2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0a5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24a2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24a.973.973 0 0 1 0-1.42z"/></svg></i>
<span>链接管理</span>
</el-menu-item>
<el-menu-item v-if="canAccessRefund" index="Refund" :route="{ name: 'Refund' }">
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg></i>
<span>退单管理</span>
</el-menu-item>
<el-menu-item v-if="canAccessAnnouncements" index="Announcements" :route="{ name: 'Announcements' }">
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></i>
<span>公告管理</span>
@@ -40,13 +53,17 @@
</aside>
<section class="main">
<header class="header">
<el-button text @click="collapsed = !collapsed" class="collapse-btn">
<el-button text @click="toggleSidebar" class="collapse-btn">
<svg width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M3 6h18v2H3V6m0 5h12v2H3v-2m0 5h18v2H3v-2Z"/></svg>
</el-button>
<div class="spacer" />
<!-- 移动端显示当前页面标题 -->
<span v-if="isMobile" class="mobile-page-title">{{ currentPageTitle }}</span>
<div v-if="!isMobile" class="spacer" />
<el-dropdown>
<span class="el-dropdown-link">
{{ currentUser?.username || '用户' }}<i class="el-icon el-icon--right"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M7 10l5 5 5-5z"/></svg></i>
<span v-if="!isMobile">{{ currentUser?.username || '用户' }}</span>
<i class="el-icon el-icon--right"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M7 10l5 5 5-5z"/></svg></i>
</span>
<template #dropdown>
<el-dropdown-menu>
@@ -65,23 +82,61 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { clearTokens } from '@/utils/auth'
import { useRouter } from 'vue-router'
import { useRouter, useRoute } from 'vue-router'
import { canAccessRoute, getCurrentUser } from '@/utils/permission'
const collapsed = ref(false)
const isMobile = ref(false)
const router = useRouter()
const route = useRoute()
// 检测移动端
const checkMobile = () => {
isMobile.value = window.innerWidth <= 768
// 移动端默认收起侧边栏
if (isMobile.value) {
collapsed.value = true
}
}
// 页面标题映射
const pageTitleMap = {
'Users': '用户管理',
'Links': '链接管理',
'Refund': '退单管理',
'Announcements': '公告管理',
'Settings': '系统设置'
}
// 获取当前用户信息
const currentUser = computed(() => getCurrentUser())
// 获取当前页面标题
const currentPageTitle = computed(() => {
return pageTitleMap[route.name] || '管理后台'
})
// 权限检查
const canAccessUsers = computed(() => canAccessRoute('Users'))
const canAccessLinks = computed(() => canAccessRoute('Links'))
const canAccessRefund = computed(() => canAccessRoute('Refund'))
const canAccessAnnouncements = computed(() => canAccessRoute('Announcements'))
const canAccessSettings = computed(() => canAccessRoute('Settings'))
// 切换侧边栏
const toggleSidebar = () => {
collapsed.value = !collapsed.value
}
// 菜单选择事件(移动端选择后自动收起)
const onMenuSelect = () => {
if (isMobile.value) {
collapsed.value = true
}
}
function onProfile() {
// 可跳转到个人中心占位页
}
@@ -90,20 +145,61 @@ function onLogout() {
clearTokens()
router.replace({ name: 'Login' })
}
// 监听窗口大小变化
onMounted(() => {
checkMobile()
window.addEventListener('resize', checkMobile)
})
onUnmounted(() => {
window.removeEventListener('resize', checkMobile)
})
</script>
<style scoped>
.admin-layout {
display: flex;
height: 100vh;
position: relative;
}
/* 移动端遮罩层 */
.mobile-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: block;
}
.sider {
width: 220px;
background: #001529;
color: #bfcbd9;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
z-index: 1000;
}
/* 移动端侧边栏样式 */
.sider-mobile {
position: fixed;
top: 0;
left: 0;
height: 100vh;
transform: translateX(0);
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
}
.sider-mobile.sider-collapsed {
transform: translateX(-100%);
}
.brand {
height: 56px;
display: flex;
@@ -111,10 +207,34 @@ function onLogout() {
gap: 10px;
padding: 0 16px;
color: #fff;
border-bottom: 1px solid #334050;
}
.logo { width: 24px; height: 24px; }
.menu { border-right: none; flex: 1; }
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.logo {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.name {
font-size: 16px;
font-weight: 600;
}
.menu {
border-right: none;
flex: 1;
overflow-y: auto;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
transition: all 0.3s ease;
}
.header {
height: 56px;
display: flex;
@@ -122,9 +242,110 @@ function onLogout() {
padding: 0 16px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
position: relative;
z-index: 100;
}
.collapse-btn {
margin-right: 8px;
min-width: 32px;
height: 32px;
border-radius: 4px;
}
.collapse-btn:hover {
background-color: #f5f5f5;
}
.spacer {
flex: 1;
}
.mobile-page-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-dropdown-link {
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
color: #606266;
font-size: 14px;
}
.el-dropdown-link:hover {
color: #409eff;
}
.content {
padding: 16px;
overflow: auto;
background: #f5f7fa;
height: calc(100vh - 56px);
flex: 1;
}
/* 移动端响应式布局 */
@media (max-width: 768px) {
.main {
width: 100%;
}
.header {
padding: 0 12px;
}
.content {
padding: 12px;
}
.collapse-btn {
margin-right: 12px;
}
.mobile-page-title {
font-size: 15px;
}
.el-dropdown-link {
font-size: 16px;
min-height: var(--mobile-touch-target);
padding: 8px;
}
}
@media (max-width: 480px) {
.header {
padding: 0 8px;
}
.content {
padding: 8px;
}
.brand {
padding: 0 12px;
}
.name {
font-size: 15px;
}
}
/* 桌面端样式 */
@media (min-width: 769px) {
.sider-mobile {
position: relative;
transform: none !important;
box-shadow: none;
}
.mobile-overlay {
display: none;
}
}
.collapse-btn { margin-right: 8px; }
.spacer { flex: 1; }
.content { padding: 16px; overflow: auto; background: #f5f7fa; height: calc(100vh - 56px); }
</style>