feat(auth): 后台登录、角色权限与会话访问策略
管理员与客服分角色登录,权限由后端判定;前端补齐登录、人员与组织管理页面。
This commit is contained in:
1 parent
9eb8613c05
commit
556b650c7a
123 files changed
+25293
No files matched your search
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { permissionChecks } from '@/utils/permissions'
|
||||
// Visibility comes from the same map the router enforces, so a link can never
|
||||
// appear for a page the guard will refuse (People used to do exactly that).
|
||||
import { canAccessPath } from '@/router/routePermissions'
|
||||
import { useEnterpriseFeatures } from '@/composables/useEnterpriseFeatures'
|
||||
|
||||
// Re-exported so nav consumers keep a single import site
|
||||
export { NAV_ICONS, navIconSvg } from './navIcons'
|
||||
|
||||
export interface NavItem {
|
||||
to?: string
|
||||
icon?: string
|
||||
label?: string
|
||||
section?: string
|
||||
show?: boolean
|
||||
}
|
||||
|
||||
export interface NavGroup {
|
||||
section: string
|
||||
items: NavItem[]
|
||||
}
|
||||
|
||||
// Shared unread-badge cap (bottom nav, More sheet, header bell)
|
||||
export const formatBadgeCount = (count?: number) =>
|
||||
count && count > 99 ? '99+' : String(count || '')
|
||||
|
||||
// Bottom-nav primary slots, in display order (remaining links go to the More sheet)
|
||||
export const PRIMARY_NAV_PATHS = ['/conversations', '/people', '/ai-agents', '/analytics']
|
||||
|
||||
export function useNavItems() {
|
||||
const { hasEnterpriseModule } = useEnterpriseFeatures()
|
||||
|
||||
// Section membership is explicit rather than inferred from array position:
|
||||
// a header can be permission-hidden while one of its items is not (User
|
||||
// Settings is always visible), which silently orphaned items into the
|
||||
// preceding section.
|
||||
const navGroups = computed<NavGroup[]>(() =>
|
||||
[
|
||||
{
|
||||
section: '主菜单',
|
||||
items: [
|
||||
{
|
||||
to: '/ai-agents',
|
||||
icon: 'agents',
|
||||
label: 'AI 客服',
|
||||
show: canAccessPath('/ai-agents'),
|
||||
},
|
||||
{
|
||||
to: '/human-agents',
|
||||
icon: 'humans',
|
||||
label: '人工客服',
|
||||
show: canAccessPath('/human-agents'),
|
||||
},
|
||||
{
|
||||
to: '/conversations',
|
||||
icon: 'inbox',
|
||||
label: '会话',
|
||||
show: canAccessPath('/conversations'),
|
||||
},
|
||||
{
|
||||
to: '/tickets',
|
||||
icon: 'tickets',
|
||||
label: '工单',
|
||||
show: canAccessPath('/tickets'),
|
||||
},
|
||||
{
|
||||
to: '/people',
|
||||
icon: 'people',
|
||||
label: '联系人',
|
||||
show: canAccessPath('/people'),
|
||||
},
|
||||
{
|
||||
to: '/knowledge',
|
||||
icon: 'knowledge',
|
||||
label: '知识库',
|
||||
show: canAccessPath('/knowledge'),
|
||||
},
|
||||
{
|
||||
to: '/faq',
|
||||
icon: 'faq',
|
||||
label: '帮助中心',
|
||||
show: canAccessPath('/faq'),
|
||||
},
|
||||
{
|
||||
to: '/analytics',
|
||||
icon: 'analytics',
|
||||
label: '数据分析',
|
||||
show: canAccessPath('/analytics'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: '设置',
|
||||
items: [
|
||||
{
|
||||
to: '/settings/organization',
|
||||
icon: 'org',
|
||||
label: '组织',
|
||||
show: canAccessPath('/settings/organization'),
|
||||
},
|
||||
{
|
||||
to: '/settings/subscription',
|
||||
icon: 'subscription',
|
||||
label: '订阅',
|
||||
show: hasEnterpriseModule && permissionChecks.canViewSubscription(),
|
||||
},
|
||||
{
|
||||
to: '/settings/ticketing',
|
||||
icon: 'ticketing',
|
||||
label: '工单设置',
|
||||
show: canAccessPath('/settings/ticketing'),
|
||||
},
|
||||
{
|
||||
to: '/settings/integrations',
|
||||
icon: 'integrations',
|
||||
label: '集成',
|
||||
show: canAccessPath('/settings/integrations'),
|
||||
},
|
||||
{
|
||||
to: '/settings/app-integrations',
|
||||
icon: 'integrations',
|
||||
label: '应用集成',
|
||||
show: hasEnterpriseModule && canAccessPath('/settings/app-integrations'),
|
||||
},
|
||||
{
|
||||
to: '/settings/widget-apps',
|
||||
icon: 'widgets',
|
||||
label: '网站组件',
|
||||
show: canAccessPath('/settings/widget-apps'),
|
||||
},
|
||||
{
|
||||
to: '/settings/ai-config',
|
||||
icon: 'aiconfig',
|
||||
label: 'AI 配置',
|
||||
show: canAccessPath('/settings/ai-config'),
|
||||
},
|
||||
{
|
||||
to: '/settings/user',
|
||||
icon: 'usersettings',
|
||||
label: '个人设置',
|
||||
show: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
.map((group) => ({ ...group, items: group.items.filter((item) => item.show !== false) }))
|
||||
// A section is visible when it has something to show — no separate
|
||||
// permission flag to drift from its items
|
||||
.filter((group) => group.items.length > 0),
|
||||
)
|
||||
|
||||
// Flat list (heading followed by its links) for the desktop sidebar
|
||||
const navItems = computed<NavItem[]>(() =>
|
||||
navGroups.value.flatMap((group) => [{ section: group.section }, ...group.items]),
|
||||
)
|
||||
|
||||
// Bottom-nav slots in design order
|
||||
const primaryNavItems = computed<NavItem[]>(() =>
|
||||
PRIMARY_NAV_PATHS.map((path) => navItems.value.find((item) => item.to === path)).filter(
|
||||
(item): item is NavItem => !!item,
|
||||
),
|
||||
)
|
||||
|
||||
// Everything the bottom nav doesn't carry, still grouped like the sidebar
|
||||
const moreNavGroups = computed<NavGroup[]>(() =>
|
||||
navGroups.value
|
||||
.map((group) => ({
|
||||
...group,
|
||||
items: group.items.filter((item) => item.to && !PRIMARY_NAV_PATHS.includes(item.to)),
|
||||
}))
|
||||
.filter((group) => group.items.length > 0),
|
||||
)
|
||||
|
||||
// Derived from the groups so there is one definition of "overflow link"
|
||||
const moreNavItems = computed<NavItem[]>(() =>
|
||||
moreNavGroups.value.flatMap((group) => group.items),
|
||||
)
|
||||
|
||||
return { navGroups, navItems, primaryNavItems, moreNavItems, moreNavGroups }
|
||||
}
|
||||
Reference in new issue
Block a user