feat: 员工自助注册+审核流程
- 新增 registration_packages 表:管理员配置注册套餐(名称/角色) - AuthController 新增 register() 和 registrationPackages() 公开接口 - UserController 新增 approve() / reject() 审核接口 - StoreController 新增 publicList() 公开门店列表 - 前端 /register 注册页:选门店→选岗位套餐→填信息→提交 - 前端 system/registration-packages:套餐 CRUD - 用户管理页:待审核状态展示 + 通过/拒绝快捷操作 - 登录页底部加「申请注册」跳转链接 - 路由白名单加 /register
This commit is contained in:
@@ -1,198 +1,150 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
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
|
||||
}
|
||||
collapsed: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const activeMenu = computed(() => route.path)
|
||||
|
||||
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')
|
||||
|
||||
function resolveIcon(iconName) {
|
||||
return iconName || 'Menu'
|
||||
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="sidebar-wrapper" :class="{ collapsed }">
|
||||
<div class="flex flex-col h-full bg-white border-r border-gray-100 overflow-hidden">
|
||||
<!-- Logo -->
|
||||
<div class="sidebar-logo">
|
||||
<el-icon :size="28" color="#E8A87C"><House /></el-icon>
|
||||
<span v-if="!collapsed" class="logo-text">宫中有喜</span>
|
||||
<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>
|
||||
|
||||
<!-- Menu -->
|
||||
<el-scrollbar class="sidebar-scrollbar">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="collapsed"
|
||||
:collapse-transition="false"
|
||||
router
|
||||
class="sidebar-menu"
|
||||
background-color="#2D1B0E"
|
||||
text-color="#C8B09A"
|
||||
active-text-color="#E8A87C"
|
||||
<!-- 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')"
|
||||
>
|
||||
<!-- Always show Dashboard first -->
|
||||
<el-menu-item index="/dashboard">
|
||||
<el-icon><Odometer /></el-icon>
|
||||
<template #title>工作台</template>
|
||||
</el-menu-item>
|
||||
<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 menu items from server -->
|
||||
<template v-for="menu in menus" :key="menu.id || menu.path">
|
||||
<!-- Has children → sub-menu -->
|
||||
<el-sub-menu
|
||||
v-if="menu.children && menu.children.length > 0 && menu.visible !== 0"
|
||||
:index="menu.path || `menu-${menu.id}`"
|
||||
<!-- 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)"
|
||||
>
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<component :is="resolveIcon(menu.icon)" />
|
||||
</el-icon>
|
||||
<span>{{ menu.name }}</span>
|
||||
</template>
|
||||
<template v-for="child in menu.children" :key="child.id || child.path">
|
||||
<el-sub-menu
|
||||
v-if="child.children && child.children.length > 0 && child.visible !== 0"
|
||||
:index="child.path"
|
||||
>
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<component :is="resolveIcon(child.icon)" />
|
||||
</el-icon>
|
||||
<span>{{ child.name }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="grandchild in child.children"
|
||||
:key="grandchild.id || grandchild.path"
|
||||
:index="grandchild.path"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="resolveIcon(grandchild.icon)" />
|
||||
</el-icon>
|
||||
<template #title>{{ grandchild.name }}</template>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item
|
||||
v-else-if="child.visible !== 0"
|
||||
:index="child.path"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="resolveIcon(child.icon)" />
|
||||
</el-icon>
|
||||
<template #title>{{ child.name }}</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- No children → single item -->
|
||||
<el-menu-item
|
||||
v-else-if="menu.visible !== 0"
|
||||
:index="menu.path"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="resolveIcon(menu.icon)" />
|
||||
</el-icon>
|
||||
<template #title>{{ menu.name }}</template>
|
||||
</el-menu-item>
|
||||
<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>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sidebar-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--sidebar-width);
|
||||
height: 100%;
|
||||
background-color: #2D1B0E;
|
||||
transition: width 0.25s ease;
|
||||
overflow: hidden;
|
||||
|
||||
&.collapsed {
|
||||
width: var(--sidebar-collapsed-width);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
height: var(--header-height);
|
||||
padding: 0 16px;
|
||||
background-color: #1E1108;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.logo-text {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #E8A87C;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-scrollbar {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border-right: none;
|
||||
height: 100%;
|
||||
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
background-color: #4A2E1A !important;
|
||||
color: #E8A87C !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-menu-item.is-active) {
|
||||
background-color: #4A2E1A !important;
|
||||
color: #E8A87C !important;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: #E8A87C;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu .el-menu) {
|
||||
background-color: #241509 !important;
|
||||
}
|
||||
|
||||
:deep(.el-menu--collapse) {
|
||||
.el-sub-menu__title span,
|
||||
.el-menu-item span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user