feat(auth): 后台登录、角色权限与会话访问策略
管理员与客服分角色登录,权限由后端判定;前端补齐登录、人员与组织管理页面。
This commit is contained in:
1 parent
9eb8613c05
commit
556b650c7a
123 files changed
+25293
No files matched your search
@@ -0,0 +1,394 @@
|
||||
<!--
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup lang="ts" name="MoreSheet">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useNavItems, navIconSvg, formatBadgeCount } from './navItems'
|
||||
import InstallPrompt from '@/components/pwa/InstallPrompt.vue'
|
||||
import type { ThemeMode } from '@/composables/useTheme'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
isOnline?: boolean
|
||||
statusUpdating?: boolean
|
||||
themeMode?: ThemeMode
|
||||
unreadCount?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
(e: 'toggle-status'): void
|
||||
(e: 'toggle-theme'): void
|
||||
(e: 'notifications'): void
|
||||
(e: 'logout'): void
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const { moreNavGroups } = useNavItems()
|
||||
|
||||
const themeLabel = computed(() =>
|
||||
props.themeMode === 'dark' ? '深色' : props.themeMode === 'light' ? '浅色' : '跟随系统'
|
||||
)
|
||||
|
||||
const themeIconName = computed(() =>
|
||||
props.themeMode === 'dark' ? 'moon' : props.themeMode === 'light' ? 'sun' : 'monitor'
|
||||
)
|
||||
|
||||
const badgeText = computed(() => formatBadgeCount(props.unreadCount))
|
||||
|
||||
const navigate = (to?: string) => {
|
||||
if (!to) return
|
||||
emit('close')
|
||||
router.push(to)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="more-sheet">
|
||||
<div v-if="open" class="more-sheet-root">
|
||||
<div class="more-scrim" @click="emit('close')"></div>
|
||||
<div class="more-sheet" role="dialog" aria-label="更多">
|
||||
<div class="drag-handle" aria-hidden="true"></div>
|
||||
|
||||
<!-- Availability -->
|
||||
<button
|
||||
type="button"
|
||||
class="availability-card"
|
||||
:class="{ offline: !isOnline }"
|
||||
:disabled="statusUpdating"
|
||||
@click="emit('toggle-status')"
|
||||
>
|
||||
<span class="availability-dot" :class="{ online: isOnline }"></span>
|
||||
<span class="availability-text">
|
||||
<span class="availability-title">{{ isOnline ? "你当前在线" : "你当前离线" }}</span>
|
||||
<span class="availability-sub">{{ isOnline ? '接收新会话' : '暂停接收新会话' }}</span>
|
||||
</span>
|
||||
<span class="toggle-track" :class="{ on: isOnline }">
|
||||
<span class="toggle-knob"></span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Theme -->
|
||||
<button type="button" class="sheet-row standalone" @click="emit('toggle-theme')">
|
||||
<span class="row-icon theme-icon" aria-hidden="true" v-html="navIconSvg(themeIconName, 20)"></span>
|
||||
<span class="row-label">主题</span>
|
||||
<span class="row-value">{{ themeLabel }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Overflow nav links, grouped exactly as the desktop sidebar -->
|
||||
<template v-for="(group, index) in moreNavGroups" :key="group.section || index">
|
||||
<div v-if="group.section" class="sheet-section nav-section-heading">{{ group.section }}</div>
|
||||
<div class="sheet-card">
|
||||
<button
|
||||
v-for="item in group.items"
|
||||
:key="item.to"
|
||||
type="button"
|
||||
class="sheet-row"
|
||||
@click="navigate(item.to)"
|
||||
>
|
||||
<span class="row-icon" v-html="navIconSvg(item.icon, 20)"></span>
|
||||
<span class="row-label">{{ item.label }}</span>
|
||||
<span class="row-chevron" aria-hidden="true" v-html="navIconSvg('chevronRight', 18)"></span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Notifications -->
|
||||
<button type="button" class="sheet-row standalone" @click="emit('notifications')">
|
||||
<span class="row-icon" v-html="navIconSvg('bell', 20)"></span>
|
||||
<span class="row-label">通知</span>
|
||||
<span v-if="unreadCount" class="row-badge">{{ badgeText }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Install as app (hidden once installed) -->
|
||||
<div class="install-slot">
|
||||
<InstallPrompt />
|
||||
</div>
|
||||
|
||||
<!-- Logout -->
|
||||
<button type="button" class="logout-row" @click="emit('logout')">
|
||||
<span class="row-icon" v-html="navIconSvg('logout', 19)"></span>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.more-sheet-root {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: var(--z-sheet);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.more-scrim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--scrim);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.more-sheet {
|
||||
position: relative;
|
||||
background: var(--bg2);
|
||||
border-top: 1px solid var(--o07);
|
||||
border-top-left-radius: 24px;
|
||||
border-top-right-radius: 24px;
|
||||
padding: 12px 18px calc(var(--space-lg) + var(--safe-bottom));
|
||||
box-shadow: var(--shadow-lg);
|
||||
max-height: calc(100dvh - 60px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
width: 38px;
|
||||
height: 5px;
|
||||
border-radius: 3px;
|
||||
background: var(--o10);
|
||||
margin: 0 auto 18px;
|
||||
}
|
||||
|
||||
.availability-card {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 16px;
|
||||
background: var(--success-color-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--success-color) 35%, transparent);
|
||||
margin-bottom: 14px;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-sans);
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.availability-card.offline {
|
||||
background: var(--o04);
|
||||
border-color: var(--o10);
|
||||
}
|
||||
|
||||
.availability-card:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.availability-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.availability-dot.online {
|
||||
background: var(--success-color);
|
||||
box-shadow: 0 0 0 4px var(--success-color-soft);
|
||||
}
|
||||
|
||||
.availability-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.availability-title {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.availability-sub {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.toggle-track {
|
||||
width: 48px;
|
||||
height: 28px;
|
||||
border-radius: 16px;
|
||||
background: var(--toggle-track-off);
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.toggle-track.on {
|
||||
background: var(--success-color);
|
||||
}
|
||||
|
||||
.toggle-knob {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: var(--toggle-knob);
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.toggle-track.on .toggle-knob {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
/* Typography comes from the shared .nav-section-heading (components.css) */
|
||||
.sheet-section {
|
||||
padding: 0 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sheet-card {
|
||||
background: var(--o04);
|
||||
border: 1px solid var(--o07);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.sheet-card .sheet-row + .sheet-row {
|
||||
border-top: 1px solid var(--o06);
|
||||
}
|
||||
|
||||
.sheet-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 13px;
|
||||
padding: 14px 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 15px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background-color var(--transition-fast);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.sheet-row:hover {
|
||||
background: var(--o05);
|
||||
}
|
||||
|
||||
.sheet-row.standalone {
|
||||
background: var(--o04);
|
||||
border: 1px solid var(--o07);
|
||||
border-radius: 14px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.row-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
color: var(--c-purple);
|
||||
}
|
||||
|
||||
.row-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.row-value {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.row-chevron {
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.row-badge {
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
padding: 0 7px;
|
||||
border-radius: 11px;
|
||||
background: var(--c-coral);
|
||||
color: var(--on-light);
|
||||
font-size: 12px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.install-slot {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.install-slot:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logout-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 9px;
|
||||
padding: 13px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--c-coral);
|
||||
font-family: var(--font-sans);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logout-row .row-icon {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Slide-up transition */
|
||||
.more-sheet-enter-active,
|
||||
.more-sheet-leave-active {
|
||||
transition: opacity var(--transition-normal);
|
||||
}
|
||||
|
||||
.more-sheet-enter-active .more-sheet,
|
||||
.more-sheet-leave-active .more-sheet {
|
||||
transition: transform var(--transition-normal);
|
||||
}
|
||||
|
||||
.more-sheet-enter-from,
|
||||
.more-sheet-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.more-sheet-enter-from .more-sheet,
|
||||
.more-sheet-leave-to .more-sheet {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user