diff --git a/frontend/src/pages/route-groups/__tests__/index.spec.ts b/frontend/src/pages/route-groups/__tests__/index.spec.ts
index 4183d84..e837a3d 100644
--- a/frontend/src/pages/route-groups/__tests__/index.spec.ts
+++ b/frontend/src/pages/route-groups/__tests__/index.spec.ts
@@ -157,6 +157,22 @@ describe('useRouteGroupStore', () => {
expect(api.get).toHaveBeenCalledWith('/api/v1/routes', undefined)
})
})
+
+ describe('syncRoutes', () => {
+ it('calls sync API and refreshes groups + routes', async () => {
+ const store = useRouteGroupStore()
+ vi.mocked(api.post).mockResolvedValueOnce(undefined)
+ vi.mocked(api.get)
+ .mockResolvedValueOnce(mockGroups)
+ .mockResolvedValueOnce(mockRoutes)
+
+ await store.syncRoutes()
+
+ expect(api.post).toHaveBeenCalledWith('/api/v1/routes/sync')
+ expect(api.get).toHaveBeenCalledWith('/api/v1/route-groups')
+ expect(api.get).toHaveBeenCalledWith('/api/v1/routes', undefined)
+ })
+ })
})
// ─── Page 组件测试 ───
@@ -177,6 +193,7 @@ describe('RouteGroupsPage', () => {
EditOutlined: { template: '' },
DeleteOutlined: { template: '' },
SaveOutlined: { template: '' },
+ SyncOutlined: { template: '' },
},
},
})
@@ -242,6 +259,23 @@ describe('RouteGroupsPage', () => {
expect(wrapper.text()).toContain('未分组')
})
+ it('renders 同步路由 button and calls syncRoutes on click', async () => {
+ await mountPage()
+
+ const syncBtn = wrapper.findAll('.ant-btn').find((b) => b.text().includes('同步路由'))
+ expect(syncBtn).toBeDefined()
+
+ vi.mocked(api.post).mockResolvedValueOnce(undefined)
+ vi.mocked(api.get)
+ .mockResolvedValueOnce(mockGroups)
+ .mockResolvedValueOnce(mockRoutes)
+
+ await syncBtn!.trigger('click')
+ await flushPromises()
+
+ expect(api.post).toHaveBeenCalledWith('/api/v1/routes/sync')
+ })
+
it('calls batchAssignRoutes on save button click', async () => {
await mountPage()
diff --git a/frontend/src/pages/route-groups/index.vue b/frontend/src/pages/route-groups/index.vue
index 56e6b76..e38a24a 100644
--- a/frontend/src/pages/route-groups/index.vue
+++ b/frontend/src/pages/route-groups/index.vue
@@ -6,6 +6,7 @@ import {
EditOutlined,
DeleteOutlined,
SaveOutlined,
+ SyncOutlined,
} from '@ant-design/icons-vue'
const groupStore = useRouteGroupStore()
@@ -55,6 +56,7 @@ const localCheckedKeys = ref<{ checked: (string | number)[], halfChecked: (strin
halfChecked: [],
})
const assignSaving = ref(false)
+const syncing = ref(false)
// 当前选中组的名称
const selectedGroupName = computed(() => {
@@ -148,6 +150,19 @@ async function handleDelete(group: RouteGroupRecord) {
}
}
+async function handleSyncRoutes() {
+ syncing.value = true
+ try {
+ await groupStore.syncRoutes()
+ message.success('路由同步成功')
+ } catch (err: unknown) {
+ const msg = err instanceof Error ? err.message : '同步失败'
+ message.error(msg)
+ } finally {
+ syncing.value = false
+ }
+}
+
async function handleGroupChange(routeId: number, groupId: number | null) {
try {
await groupStore.assignRouteToGroup(routeId, groupId)
@@ -171,9 +186,14 @@ onMounted(async () => {
-
- 新建
-
+
+
+ 同步路由
+
+
+ 新建
+
+
diff --git a/frontend/src/stores/route-group.ts b/frontend/src/stores/route-group.ts
index a055694..9985138 100644
--- a/frontend/src/stores/route-group.ts
+++ b/frontend/src/stores/route-group.ts
@@ -86,6 +86,11 @@ export const useRouteGroupStore = defineStore('route-group', () => {
await Promise.all([fetchGroups(), fetchRoutes()])
}
+ async function syncRoutes() {
+ await api.post('/api/v1/routes/sync')
+ await Promise.all([fetchGroups(), fetchRoutes()])
+ }
+
// 按首段资源名分组,避免同名叶节点与目录共存导致 UI 混乱
const routeTree = computed(() => {
const root: RouteTreeNode[] = []
@@ -167,6 +172,7 @@ export const useRouteGroupStore = defineStore('route-group', () => {
fetchRoutes,
assignRouteToGroup,
batchAssignRoutes,
+ syncRoutes,
routeTree,
checkedRouteKeys,
}