Files
yuezi-saas/frontend/src/layout/components/Sidebar.vue
T
li ee2b1757d0 feat: 员工自助注册+审核流程
- 新增 registration_packages 表:管理员配置注册套餐(名称/角色)
- AuthController 新增 register() 和 registrationPackages() 公开接口
- UserController 新增 approve() / reject() 审核接口
- StoreController 新增 publicList() 公开门店列表
- 前端 /register 注册页:选门店→选岗位套餐→填信息→提交
- 前端 system/registration-packages:套餐 CRUD
- 用户管理页:待审核状态展示 + 通过/拒绝快捷操作
- 登录页底部加「申请注册」跳转链接
- 路由白名单加 /register
2026-03-14 17:39:50 +08:00

151 lines
6.3 KiB
Vue

<script setup>
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user.js'
import { Icon } from '@iconify/vue'
const props = defineProps({
collapsed: { type: Boolean, default: false }
})
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const activePath = computed(() => route.path)
const menus = computed(() => userStore.menus)
const username = computed(() => userStore.userInfo?.name || userStore.userInfo?.username || '用户')
const storeName = computed(() => userStore.userInfo?.store?.name || '')
const roleLabel = computed(() => userStore.userInfo?.is_super ? 'SUPER ADMIN' : 'STORE ADMIN')
const iconMap = {
Odometer: 'solar:widget-add-bold-duotone',
House: 'solar:home-bold-duotone',
User: 'solar:users-group-rounded-bold-duotone',
UserFilled: 'solar:users-group-rounded-bold-duotone',
ShoppingCart: 'solar:cart-large-bold-duotone',
Goods: 'solar:cart-large-bold-duotone',
OfficeBuilding: 'solar:bed-bold-duotone',
HeartPulse: 'solar:heart-pulse-bold-duotone',
Food: 'solar:cup-bold',
Trophy: 'solar:health-bold',
Avatar: 'solar:user-speak-bold-duotone',
Box: 'solar:box-bold-duotone',
Money: 'solar:money-bag-bold-duotone',
Wallet: 'solar:money-bag-bold-duotone',
Suitcase: 'solar:users-group-two-rounded-bold-duotone',
Connection: 'solar:streets-navigation-bold-duotone',
DataLine: 'solar:chart-2-bold-duotone',
Reading: 'solar:book-bookmark-bold-duotone',
Setting: 'solar:settings-bold-duotone',
Tools: 'solar:settings-bold-duotone',
Menu: 'solar:list-bold-duotone',
}
function resolveIcon(name) {
return iconMap[name] || 'solar:circle-bold-duotone'
}
function isActive(menu) {
if (menu.path && activePath.value.startsWith(menu.path)) return true
if (menu.children) return menu.children.some(c => isActive(c))
return false
}
function navigate(path) {
if (path) router.push(path)
}
async function handleLogout() {
await userStore.logout()
router.push('/login')
}
</script>
<template>
<div class="flex flex-col h-full bg-white border-r border-gray-100 overflow-hidden">
<!-- Logo -->
<div class="flex items-center gap-3 px-5 py-4 border-b border-gray-50 flex-shrink-0" style="height:64px">
<div class="w-9 h-9 bg-rose-500 rounded-xl flex items-center justify-center flex-shrink-0 shadow-sm">
<Icon icon="solar:heart-bold" class="text-white text-lg" />
</div>
<div v-if="!collapsed" class="overflow-hidden">
<h2 class="font-bold text-gray-800 text-sm leading-tight">宫中有喜</h2>
<span class="text-[10px] bg-rose-100 text-rose-600 px-1.5 py-0.5 rounded font-bold tracking-wider">
{{ roleLabel }}
</span>
</div>
</div>
<!-- Nav -->
<nav class="flex-1 overflow-y-auto no-scrollbar py-3 px-3 space-y-0.5">
<!-- Dashboard -->
<button
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-150 text-left"
:class="activePath === '/dashboard' ? 'bg-rose-50 text-rose-500' : 'text-gray-500 hover:bg-gray-50 hover:text-gray-700'"
@click="navigate('/dashboard')"
>
<Icon icon="solar:widget-add-bold-duotone" class="text-xl flex-shrink-0" />
<span v-if="!collapsed" class="text-sm font-medium truncate">工作台</span>
<div v-if="!collapsed && activePath === '/dashboard'" class="ml-auto w-1.5 h-1.5 rounded-full bg-rose-500 flex-shrink-0"></div>
</button>
<!-- Dynamic menus -->
<template v-for="menu in menus" :key="menu.id || menu.path">
<!-- Group with children -->
<template v-if="menu.children && menu.children.length > 0 && menu.visible !== 0">
<div v-if="!collapsed" class="text-[10px] font-bold text-gray-300 uppercase tracking-widest px-3 pt-4 pb-1.5">
{{ menu.name }}
</div>
<div v-else class="my-2 mx-2 h-px bg-gray-100"></div>
<button
v-for="child in menu.children.filter(c => c.visible !== 0)"
:key="child.id || child.path"
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-150 text-left"
:class="isActive(child) ? 'bg-rose-50 text-rose-500' : 'text-gray-500 hover:bg-gray-50 hover:text-gray-700'"
@click="navigate(child.path)"
>
<Icon :icon="resolveIcon(child.icon)" class="text-xl flex-shrink-0" />
<span v-if="!collapsed" class="text-sm font-medium truncate">{{ child.name }}</span>
<div v-if="!collapsed && isActive(child)" class="ml-auto w-1.5 h-1.5 rounded-full bg-rose-500 flex-shrink-0"></div>
</button>
</template>
<!-- Single item -->
<button
v-else-if="menu.visible !== 0"
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-150 text-left"
:class="isActive(menu) ? 'bg-rose-50 text-rose-500' : 'text-gray-500 hover:bg-gray-50 hover:text-gray-700'"
@click="navigate(menu.path)"
>
<Icon :icon="resolveIcon(menu.icon)" class="text-xl flex-shrink-0" />
<span v-if="!collapsed" class="text-sm font-medium truncate">{{ menu.name }}</span>
<div v-if="!collapsed && isActive(menu)" class="ml-auto w-1.5 h-1.5 rounded-full bg-rose-500 flex-shrink-0"></div>
</button>
</template>
</nav>
<!-- User card -->
<div class="flex-shrink-0 p-3 border-t border-gray-50">
<div class="flex items-center gap-3 p-3 bg-gray-50 rounded-2xl" :class="collapsed ? 'justify-center' : ''">
<div class="w-9 h-9 rounded-full bg-rose-100 flex items-center justify-center flex-shrink-0 text-rose-500 font-bold text-sm">
{{ username.charAt(0) }}
</div>
<div v-if="!collapsed" class="flex-1 overflow-hidden">
<p class="text-sm font-bold text-gray-800 truncate">{{ username }}</p>
<p class="text-[11px] text-gray-400 truncate">{{ storeName || '总部管理' }}</p>
</div>
<button
v-if="!collapsed"
class="flex-shrink-0 p-1.5 text-rose-400 hover:bg-rose-100 rounded-lg transition-colors"
title="退出登录"
@click.stop="handleLogout"
>
<Icon icon="solar:logout-bold-duotone" class="text-lg" />
</button>
</div>
</div>
</div>
</template>