feat: 员工自助注册+审核流程

- 新增 registration_packages 表:管理员配置注册套餐(名称/角色)
- AuthController 新增 register() 和 registrationPackages() 公开接口
- UserController 新增 approve() / reject() 审核接口
- StoreController 新增 publicList() 公开门店列表
- 前端 /register 注册页:选门店→选岗位套餐→填信息→提交
- 前端 system/registration-packages:套餐 CRUD
- 用户管理页:待审核状态展示 + 通过/拒绝快捷操作
- 登录页底部加「申请注册」跳转链接
- 路由白名单加 /register
This commit is contained in:
li
2026-03-14 17:39:50 +08:00
parent 4a68855a88
commit ee2b1757d0
105 changed files with 7859 additions and 2469 deletions
+125 -173
View File
@@ -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>