refactor route group
This commit is contained in:
@@ -0,0 +1,269 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
|
import { mount, flushPromises, VueWrapper } from '@vue/test-utils'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
import { nextTick } from 'vue'
|
||||||
|
import { useRouteGroupStore } from '@/stores/route-group'
|
||||||
|
import type { RouteRecord } from '@/stores/route-group'
|
||||||
|
import RouteGroupsPage from '../index.vue'
|
||||||
|
|
||||||
|
vi.mock('@/utils/request', () => ({
|
||||||
|
api: {
|
||||||
|
get: vi.fn(),
|
||||||
|
post: vi.fn(),
|
||||||
|
put: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { api } = await import('@/utils/request')
|
||||||
|
|
||||||
|
// Ant Design Vue jsdom 兼容
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
value: vi.fn().mockImplementation((query: string) => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: vi.fn(),
|
||||||
|
removeListener: vi.fn(),
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockGroups = [
|
||||||
|
{ id: 1, name: 'user-mgmt', label: '用户管理', description: '', sort_order: 1, route_count: 2 },
|
||||||
|
{ id: 2, name: 'order-mgmt', label: '订单管理', description: '', sort_order: 2, route_count: 1 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockRoutes: RouteRecord[] = [
|
||||||
|
{ id: 1, method: 'GET', path: '/api/v1/users', description: '用户列表', group_id: 1, group_name: '用户管理' },
|
||||||
|
{ id: 2, method: 'POST', path: '/api/v1/users', description: '创建用户', group_id: 1, group_name: '用户管理' },
|
||||||
|
{ id: 3, method: 'GET', path: '/api/v1/orders', description: '订单列表', group_id: 2, group_name: '订单管理' },
|
||||||
|
{ id: 4, method: 'GET', path: '/api/v1/products', description: '产品列表', group_id: null, group_name: null },
|
||||||
|
{ id: 5, method: 'GET', path: '/api/v1/users/{id}', description: '用户详情', group_id: 1, group_name: '用户管理' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// ─── Store 测试 ───
|
||||||
|
|
||||||
|
describe('useRouteGroupStore', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('routeTree', () => {
|
||||||
|
it('groups all routes by first path segment as directories', () => {
|
||||||
|
const store = useRouteGroupStore()
|
||||||
|
store.routes = [...mockRoutes]
|
||||||
|
|
||||||
|
const tree = store.routeTree
|
||||||
|
const topKeys = tree.map((n) => n.key)
|
||||||
|
// 按资源名分组为目录节点
|
||||||
|
expect(topKeys).toContain('/users')
|
||||||
|
expect(topKeys).toContain('/orders')
|
||||||
|
expect(topKeys).toContain('/products')
|
||||||
|
expect(tree).toHaveLength(3)
|
||||||
|
// 所有顶层节点都是目录
|
||||||
|
expect(tree.every((n) => !n.isLeaf)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('places same-path different-method routes inside same directory', () => {
|
||||||
|
const store = useRouteGroupStore()
|
||||||
|
store.routes = [...mockRoutes]
|
||||||
|
|
||||||
|
const usersDir = store.routeTree.find((n) => n.key === '/users')!
|
||||||
|
const userLeaves = usersDir.children!.filter((n) => n.isLeaf && n.title === 'users')
|
||||||
|
expect(userLeaves).toHaveLength(2)
|
||||||
|
expect(userLeaves.map((n) => n.method)).toEqual(expect.arrayContaining(['GET', 'POST']))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('nests multi-segment routes under same directory', () => {
|
||||||
|
const store = useRouteGroupStore()
|
||||||
|
store.routes = [...mockRoutes]
|
||||||
|
|
||||||
|
const usersDir = store.routeTree.find((n) => n.key === '/users')!
|
||||||
|
// users 目录下:GET users, POST users, GET {id}
|
||||||
|
expect(usersDir.children).toHaveLength(3)
|
||||||
|
const idLeaf = usersDir.children!.find((n) => n.key === 'route:5')
|
||||||
|
expect(idLeaf).toBeDefined()
|
||||||
|
expect(idLeaf!.title).toBe('{id}')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates leaf nodes with route metadata', () => {
|
||||||
|
const store = useRouteGroupStore()
|
||||||
|
store.routes = [...mockRoutes]
|
||||||
|
|
||||||
|
const productsDir = store.routeTree.find((n) => n.key === '/products')!
|
||||||
|
const productsLeaf = productsDir.children!.find((n) => n.key === 'route:4')
|
||||||
|
expect(productsLeaf).toBeDefined()
|
||||||
|
expect(productsLeaf!.isLeaf).toBe(true)
|
||||||
|
expect(productsLeaf!.routeId).toBe(4)
|
||||||
|
expect(productsLeaf!.method).toBe('GET')
|
||||||
|
expect(productsLeaf!.fullPath).toBe('/api/v1/products')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns empty array when no routes', () => {
|
||||||
|
const store = useRouteGroupStore()
|
||||||
|
expect(store.routeTree).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('checkedRouteKeys', () => {
|
||||||
|
it('returns keys for routes matching selected group', () => {
|
||||||
|
const store = useRouteGroupStore()
|
||||||
|
store.routes = [...mockRoutes]
|
||||||
|
store.selectedGroupId = 1
|
||||||
|
|
||||||
|
expect(store.checkedRouteKeys).toEqual(
|
||||||
|
expect.arrayContaining(['route:1', 'route:2', 'route:5']),
|
||||||
|
)
|
||||||
|
expect(store.checkedRouteKeys).toHaveLength(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns empty array when no group selected', () => {
|
||||||
|
const store = useRouteGroupStore()
|
||||||
|
store.routes = [...mockRoutes]
|
||||||
|
store.selectedGroupId = null
|
||||||
|
|
||||||
|
expect(store.checkedRouteKeys).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns empty array for ungrouped selection', () => {
|
||||||
|
const store = useRouteGroupStore()
|
||||||
|
store.routes = [...mockRoutes]
|
||||||
|
store.selectedGroupId = 'ungrouped'
|
||||||
|
|
||||||
|
expect(store.checkedRouteKeys).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('batchAssignRoutes', () => {
|
||||||
|
it('calls API and refreshes data', async () => {
|
||||||
|
const store = useRouteGroupStore()
|
||||||
|
vi.mocked(api.put).mockResolvedValueOnce(undefined)
|
||||||
|
vi.mocked(api.get)
|
||||||
|
.mockResolvedValueOnce(mockGroups)
|
||||||
|
.mockResolvedValueOnce(mockRoutes)
|
||||||
|
|
||||||
|
await store.batchAssignRoutes(1, [1, 2, 3])
|
||||||
|
|
||||||
|
expect(api.put).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/route-groups/1/routes',
|
||||||
|
{ route_ids: [1, 2, 3] },
|
||||||
|
)
|
||||||
|
expect(api.get).toHaveBeenCalledWith('/api/v1/route-groups')
|
||||||
|
expect(api.get).toHaveBeenCalledWith('/api/v1/routes', undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── Page 组件测试 ───
|
||||||
|
|
||||||
|
describe('RouteGroupsPage', () => {
|
||||||
|
let wrapper: VueWrapper
|
||||||
|
|
||||||
|
async function mountPage() {
|
||||||
|
vi.mocked(api.get)
|
||||||
|
.mockResolvedValueOnce(mockGroups)
|
||||||
|
.mockResolvedValueOnce(mockRoutes)
|
||||||
|
|
||||||
|
wrapper = mount(RouteGroupsPage, {
|
||||||
|
attachTo: document.body,
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
PlusOutlined: { template: '<span class="icon-plus" />' },
|
||||||
|
EditOutlined: { template: '<span class="icon-edit" />' },
|
||||||
|
DeleteOutlined: { template: '<span class="icon-delete" />' },
|
||||||
|
SaveOutlined: { template: '<span class="icon-save" />' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
await nextTick()
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper?.unmount()
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows empty state when no group is selected', async () => {
|
||||||
|
await mountPage()
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('请选择路由组')
|
||||||
|
expect(wrapper.find('.ant-empty').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows ungrouped routes table when ungrouped is selected', async () => {
|
||||||
|
await mountPage()
|
||||||
|
|
||||||
|
const store = useRouteGroupStore()
|
||||||
|
store.selectedGroupId = 'ungrouped'
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('未分组路由')
|
||||||
|
expect(wrapper.find('.ant-table').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows route tree when a group is selected', async () => {
|
||||||
|
await mountPage()
|
||||||
|
|
||||||
|
const store = useRouteGroupStore()
|
||||||
|
store.selectedGroupId = 1
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('用户管理')
|
||||||
|
expect(wrapper.find('.ant-tree').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows save button when a group is selected', async () => {
|
||||||
|
await mountPage()
|
||||||
|
|
||||||
|
const store = useRouteGroupStore()
|
||||||
|
store.selectedGroupId = 1
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('保存分配')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders group list in left panel', async () => {
|
||||||
|
await mountPage()
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('用户管理')
|
||||||
|
expect(wrapper.text()).toContain('订单管理')
|
||||||
|
expect(wrapper.text()).toContain('未分组')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls batchAssignRoutes on save button click', async () => {
|
||||||
|
await mountPage()
|
||||||
|
|
||||||
|
const store = useRouteGroupStore()
|
||||||
|
store.selectedGroupId = 1
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// 模拟 batchAssignRoutes 成功
|
||||||
|
vi.mocked(api.put).mockResolvedValueOnce(undefined)
|
||||||
|
vi.mocked(api.get)
|
||||||
|
.mockResolvedValueOnce(mockGroups)
|
||||||
|
.mockResolvedValueOnce(mockRoutes)
|
||||||
|
|
||||||
|
// 点击保存按钮
|
||||||
|
const saveBtn = wrapper.findAll('.ant-btn').find((b) => b.text().includes('保存分配'))
|
||||||
|
expect(saveBtn).toBeDefined()
|
||||||
|
await saveBtn!.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(api.put).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/route-groups/1/routes',
|
||||||
|
expect.objectContaining({ route_ids: expect.any(Array) }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouteGroupStore } from '@/stores/route-group'
|
import { useRouteGroupStore } from '@/stores/route-group'
|
||||||
import type { RouteGroupRecord } from '@/stores/route-group'
|
import type { RouteGroupRecord, RouteTreeNode } from '@/stores/route-group'
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
|
SaveOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
|
|
||||||
const groupStore = useRouteGroupStore()
|
const groupStore = useRouteGroupStore()
|
||||||
@@ -43,14 +44,53 @@ const groupOptions = computed(() => [
|
|||||||
...groupStore.groups.map((g) => ({ value: g.id, label: g.label || g.name })),
|
...groupStore.groups.map((g) => ({ value: g.id, label: g.label || g.name })),
|
||||||
])
|
])
|
||||||
|
|
||||||
// 筛选路由
|
// 筛选未分组路由
|
||||||
const filteredRoutes = computed(() => {
|
const ungroupedRoutes = computed(() =>
|
||||||
if (groupStore.selectedGroupId === null) return groupStore.routes
|
groupStore.routes.filter((r) => r.group_id === null),
|
||||||
if (groupStore.selectedGroupId === 'ungrouped') {
|
)
|
||||||
return groupStore.routes.filter((r) => r.group_id === null)
|
|
||||||
}
|
// 树形选择器本地勾选状态
|
||||||
return groupStore.routes.filter((r) => r.group_id === groupStore.selectedGroupId)
|
const localCheckedKeys = ref<{ checked: (string | number)[], halfChecked: (string | number)[] }>({
|
||||||
|
checked: [],
|
||||||
|
halfChecked: [],
|
||||||
})
|
})
|
||||||
|
const assignSaving = ref(false)
|
||||||
|
|
||||||
|
// 当前选中组的名称
|
||||||
|
const selectedGroupName = computed(() => {
|
||||||
|
if (typeof groupStore.selectedGroupId !== 'number') return ''
|
||||||
|
const g = groupStore.groups.find((g) => g.id === groupStore.selectedGroupId)
|
||||||
|
return g ? (g.label || g.name) : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切换路由组时重置 checkedKeys
|
||||||
|
watch(
|
||||||
|
() => groupStore.selectedGroupId,
|
||||||
|
() => {
|
||||||
|
localCheckedKeys.value = {
|
||||||
|
checked: [...groupStore.checkedRouteKeys],
|
||||||
|
halfChecked: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 保存路由分配
|
||||||
|
async function handleSaveAssignment() {
|
||||||
|
if (typeof groupStore.selectedGroupId !== 'number') return
|
||||||
|
assignSaving.value = true
|
||||||
|
try {
|
||||||
|
const routeIds = localCheckedKeys.value.checked
|
||||||
|
.filter((k) => String(k).startsWith('route:'))
|
||||||
|
.map((k) => Number(String(k).replace('route:', '')))
|
||||||
|
await groupStore.batchAssignRoutes(groupStore.selectedGroupId, routeIds)
|
||||||
|
message.success('路由分配保存成功')
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg = err instanceof Error ? err.message : '保存失败'
|
||||||
|
message.error(msg)
|
||||||
|
} finally {
|
||||||
|
assignSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selectGroup(id: number | null | 'ungrouped') {
|
function selectGroup(id: number | null | 'ungrouped') {
|
||||||
groupStore.selectedGroupId = id
|
groupStore.selectedGroupId = id
|
||||||
@@ -177,12 +217,18 @@ onMounted(async () => {
|
|||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
<!-- 右侧:路由列表 -->
|
<!-- 右侧:条件渲染 -->
|
||||||
<a-col :span="16">
|
<a-col :span="16">
|
||||||
<a-card title="路由列表">
|
<!-- 状态1: 未选中路由组 -->
|
||||||
|
<a-card v-if="groupStore.selectedGroupId === null" title="路由列表">
|
||||||
|
<a-empty description="请选择路由组" />
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 状态2: 未分组路由表格 -->
|
||||||
|
<a-card v-else-if="groupStore.selectedGroupId === 'ungrouped'" title="未分组路由">
|
||||||
<a-table
|
<a-table
|
||||||
:columns="routeColumns"
|
:columns="routeColumns"
|
||||||
:data-source="filteredRoutes"
|
:data-source="ungroupedRoutes"
|
||||||
:loading="groupStore.routesLoading"
|
:loading="groupStore.routesLoading"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
@@ -207,6 +253,41 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 状态3: 路由组树形选择器 -->
|
||||||
|
<a-card v-else :title="selectedGroupName">
|
||||||
|
<template #extra>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:loading="assignSaving"
|
||||||
|
@click="handleSaveAssignment"
|
||||||
|
>
|
||||||
|
<SaveOutlined /> 保存分配
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-tree
|
||||||
|
v-model:checkedKeys="localCheckedKeys"
|
||||||
|
checkable
|
||||||
|
:check-strictly="true"
|
||||||
|
:tree-data="groupStore.routeTree"
|
||||||
|
:selectable="false"
|
||||||
|
default-expand-all
|
||||||
|
>
|
||||||
|
<template #title="node">
|
||||||
|
<template v-if="(node as RouteTreeNode).isLeaf">
|
||||||
|
<a-tag :color="methodColorMap[(node as RouteTreeNode).method!] || 'default'" class="mr-1">
|
||||||
|
{{ (node as RouteTreeNode).method }}
|
||||||
|
</a-tag>
|
||||||
|
<span class="text-gray-600">{{ (node as RouteTreeNode).title }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="font-medium">{{ (node as RouteTreeNode).title }}</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-tree>
|
||||||
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ export interface RouteRecord {
|
|||||||
group_name: string | null
|
group_name: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RouteTreeNode {
|
||||||
|
key: string
|
||||||
|
title: string
|
||||||
|
children?: RouteTreeNode[]
|
||||||
|
isLeaf?: boolean
|
||||||
|
routeId?: number
|
||||||
|
method?: string
|
||||||
|
fullPath?: string
|
||||||
|
}
|
||||||
|
|
||||||
export const useRouteGroupStore = defineStore('route-group', () => {
|
export const useRouteGroupStore = defineStore('route-group', () => {
|
||||||
const groups = ref<RouteGroupRecord[]>([])
|
const groups = ref<RouteGroupRecord[]>([])
|
||||||
const routes = ref<RouteRecord[]>([])
|
const routes = ref<RouteRecord[]>([])
|
||||||
@@ -70,6 +80,80 @@ export const useRouteGroupStore = defineStore('route-group', () => {
|
|||||||
await api.put(`/api/v1/routes/${routeId}/group`, { group_id: groupId })
|
await api.put(`/api/v1/routes/${routeId}/group`, { group_id: groupId })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function batchAssignRoutes(groupId: number, routeIds: number[]) {
|
||||||
|
await api.put(`/api/v1/route-groups/${groupId}/routes`, { route_ids: routeIds })
|
||||||
|
// 刻意获取全量路由(无过滤参数),因为 routeTree 需要完整数据构建树
|
||||||
|
await Promise.all([fetchGroups(), fetchRoutes()])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按首段资源名分组,避免同名叶节点与目录共存导致 UI 混乱
|
||||||
|
const routeTree = computed(() => {
|
||||||
|
const root: RouteTreeNode[] = []
|
||||||
|
const nodeMap = new Map<string, RouteTreeNode>()
|
||||||
|
|
||||||
|
function getOrCreateDir(parent: RouteTreeNode[], pathKey: string, title: string) {
|
||||||
|
let node = nodeMap.get(pathKey)
|
||||||
|
if (!node) {
|
||||||
|
node = { key: pathKey, title, children: [] }
|
||||||
|
nodeMap.set(pathKey, node)
|
||||||
|
parent.push(node)
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const route of routes.value) {
|
||||||
|
const relative = route.path.replace(/^\/api\/v1\//, '')
|
||||||
|
const segments = relative.split('/').filter(Boolean)
|
||||||
|
if (segments.length === 0) continue
|
||||||
|
|
||||||
|
const firstSeg = segments[0]!
|
||||||
|
const dirNode = getOrCreateDir(root, '/' + firstSeg, firstSeg)
|
||||||
|
|
||||||
|
if (segments.length === 1) {
|
||||||
|
dirNode.children!.push({
|
||||||
|
key: `route:${route.id}`,
|
||||||
|
title: firstSeg,
|
||||||
|
isLeaf: true,
|
||||||
|
routeId: route.id,
|
||||||
|
method: route.method,
|
||||||
|
fullPath: route.path,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let parentChildren = dirNode.children!
|
||||||
|
let pathSoFar = '/' + firstSeg
|
||||||
|
|
||||||
|
for (let i = 1; i < segments.length; i++) {
|
||||||
|
const seg = segments[i]!
|
||||||
|
pathSoFar += '/' + seg
|
||||||
|
const isLast = i === segments.length - 1
|
||||||
|
|
||||||
|
if (isLast) {
|
||||||
|
parentChildren.push({
|
||||||
|
key: `route:${route.id}`,
|
||||||
|
title: seg,
|
||||||
|
isLeaf: true,
|
||||||
|
routeId: route.id,
|
||||||
|
method: route.method,
|
||||||
|
fullPath: route.path,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const node = getOrCreateDir(parentChildren, pathSoFar, seg)
|
||||||
|
parentChildren = node.children!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return root
|
||||||
|
})
|
||||||
|
|
||||||
|
const checkedRouteKeys = computed(() => {
|
||||||
|
if (typeof selectedGroupId.value !== 'number') return []
|
||||||
|
return routes.value
|
||||||
|
.filter((r) => r.group_id === selectedGroupId.value)
|
||||||
|
.map((r) => `route:${r.id}`)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groups,
|
groups,
|
||||||
routes,
|
routes,
|
||||||
@@ -82,5 +166,8 @@ export const useRouteGroupStore = defineStore('route-group', () => {
|
|||||||
deleteGroup,
|
deleteGroup,
|
||||||
fetchRoutes,
|
fetchRoutes,
|
||||||
assignRouteToGroup,
|
assignRouteToGroup,
|
||||||
|
batchAssignRoutes,
|
||||||
|
routeTree,
|
||||||
|
checkedRouteKeys,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user