From aeb226a0e6fb883bd731a88396cf59ea923d46b6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com>
Date: Sat, 5 Sep 2026 15:21:56 +0800
Subject: [PATCH] =?UTF-8?q?task-58(=E8=B4=A6=E5=8F=B7/=E6=9D=83=E9=99=90?=
=?UTF-8?q?=E9=A1=B5=E9=9D=A2):=20=E5=AE=9E=E7=8E=B0=E5=90=8C=E7=BA=A7?=
=?UTF-8?q?=E8=8F=9C=E5=8D=95=E6=8E=92=E5=BA=8F=E4=BA=A4=E4=BA=92?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
menu-manage-model.ts 增 siblingOrderAfterMove 同级内上下移(越界/缺失原序);menu-manage-api.ts
增 reorderMenus POST /api/admin/column/reorder({ordered_ids});MenusPage 每行上移/下移按钮
按同级组计算新顺序并提交后刷新。8 用例全过,427 单测 + build 绿。
---
.../src/pages/account/MenusPage.vue | 23 +++++++-
.../src/pages/account/menu-manage-api.ts | 7 +++
.../src/pages/account/menu-manage-model.ts | 13 +++++
admin-frontend-vue/tests/task-58.test.ts | 56 +++++++++++++++++++
4 files changed, 98 insertions(+), 1 deletion(-)
create mode 100644 admin-frontend-vue/tests/task-58.test.ts
diff --git a/admin-frontend-vue/src/pages/account/MenusPage.vue b/admin-frontend-vue/src/pages/account/MenusPage.vue
index b20ef077..8a24be10 100644
--- a/admin-frontend-vue/src/pages/account/MenusPage.vue
+++ b/admin-frontend-vue/src/pages/account/MenusPage.vue
@@ -1,7 +1,7 @@
@@ -113,6 +128,12 @@ onMounted(loadMenus)
+
+
+ 上移
+ 下移
+
+
编辑
diff --git a/admin-frontend-vue/src/pages/account/menu-manage-api.ts b/admin-frontend-vue/src/pages/account/menu-manage-api.ts
index a9be7add..abb47355 100644
--- a/admin-frontend-vue/src/pages/account/menu-manage-api.ts
+++ b/admin-frontend-vue/src/pages/account/menu-manage-api.ts
@@ -13,6 +13,7 @@ import {
} from './menu-manage-model'
export const MENUS_ENDPOINT = '/api/admin/permission-menus'
+export const REORDER_ENDPOINT = '/api/admin/column/reorder'
/** 拉取扁平可编辑菜单节点。 */
export async function fetchMenuManageNodes(): Promise {
@@ -44,3 +45,9 @@ export async function deleteMenu(id: number): Promise {
const { data } = await http.delete(`${MENUS_ENDPOINT}/${id}`)
unwrap(data)
}
+
+/** 同级菜单排序:POST /api/admin/column/reorder 提交新的同级 id 顺序(含子树节点时仅同级生效)。 */
+export async function reorderMenus(orderedIds: number[]): Promise {
+ const { data } = await http.post(REORDER_ENDPOINT, { ordered_ids: orderedIds })
+ unwrap(data)
+}
diff --git a/admin-frontend-vue/src/pages/account/menu-manage-model.ts b/admin-frontend-vue/src/pages/account/menu-manage-model.ts
index 6324c90e..fbe5bd17 100644
--- a/admin-frontend-vue/src/pages/account/menu-manage-model.ts
+++ b/admin-frontend-vue/src/pages/account/menu-manage-model.ts
@@ -177,6 +177,19 @@ export function menuDeleteReason(node: MenuManageNode | null | undefined): strin
return undefined
}
+/** 同级拖拽/上下移:把 targetId 移动 dir 位(±1),返回新的同级 id 顺序;越界/缺失则原序。 */
+export function siblingOrderAfterMove(group: number[], targetId: number, dir: number): number[] {
+ if (!Array.isArray(group) || group.length === 0) return []
+ const next = [...group]
+ const idx = next.indexOf(targetId)
+ if (idx < 0) return next
+ const to = idx + (dir === -1 || dir === 1 ? dir : 0)
+ if (to < 0 || to >= next.length) return next
+ const [moved] = next.splice(idx, 1)
+ next.splice(to, 0, moved)
+ return next
+}
+
/** 新建/编辑表单空值工厂:默认菜单类型为 admin、排序 0、根父节点。 */
export function emptyMenuManageForm(): MenuManageForm {
return { name: '', columnKey: '', parentId: null, menuType: ADMIN_MENU_TYPE, routePath: '', sortOrder: 0 }
diff --git a/admin-frontend-vue/tests/task-58.test.ts b/admin-frontend-vue/tests/task-58.test.ts
new file mode 100644
index 00000000..5e3add8c
--- /dev/null
+++ b/admin-frontend-vue/tests/task-58.test.ts
@@ -0,0 +1,56 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readSource } from './helpers.ts'
+import { siblingOrderAfterMove } from '../src/pages/account/menu-manage-model.ts'
+
+test('test_task_058_menu_sort_normal_primary_path', () => {
+ // 正常主路径:同级内下移一位,返回新顺序 id 列表。
+ assert.deepEqual(siblingOrderAfterMove([1, 2, 3, 4], 2, 1), [1, 3, 2, 4])
+})
+
+test('test_task_058_menu_sort_normal_variant_input', () => {
+ // 正常变体:上移一位。
+ assert.deepEqual(siblingOrderAfterMove([1, 2, 3, 4], 3, -1), [1, 3, 2, 4])
+})
+
+test('test_task_058_menu_sort_normal_repeated_operation_is_idempotent', () => {
+ // 正常重复:纯函数不改输入;结果稳定。
+ const group = [1, 2, 3]
+ assert.deepEqual(siblingOrderAfterMove(group, 1, 1), siblingOrderAfterMove(group, 1, 1))
+ assert.deepEqual(group, [1, 2, 3])
+})
+
+test('test_task_058_menu_sort_boundary_empty_input', () => {
+ // 边界空值:空组返回空,不崩溃。
+ assert.deepEqual(siblingOrderAfterMove([], 1, 1), [])
+})
+
+test('test_task_058_menu_sort_boundary_single_item', () => {
+ // 边界单元素:单元素不可移。
+ assert.deepEqual(siblingOrderAfterMove([1], 1, 1), [1])
+ assert.deepEqual(siblingOrderAfterMove([1], 1, -1), [1])
+})
+
+test('test_task_058_menu_sort_boundary_limit_or_missing_field', () => {
+ // 边界上限:已在首/尾则顺序不变。
+ assert.deepEqual(siblingOrderAfterMove([1, 2, 3], 1, -1), [1, 2, 3])
+ assert.deepEqual(siblingOrderAfterMove([1, 2, 3], 3, 1), [1, 2, 3])
+ assert.deepEqual(siblingOrderAfterMove([1, 2, 3], 9, 1), [1, 2, 3])
+})
+
+test('test_task_058_menu_sort_invalid_input_rejected', () => {
+ // 异常输入:非法方向按无操作处理;移动后集合元素不变。
+ const moved = siblingOrderAfterMove([2, 1, 3], 1, 0 as never)
+ assert.deepEqual(moved, [2, 1, 3])
+})
+
+test('test_task_058_menu_sort_dependency_failure_returns_actionable_message', () => {
+ // 依赖失败/排序走 adapter:POST /column/reorder 提交 ordered_ids,页面不内联请求。
+ const api = readSource('src/pages/account/menu-manage-api.ts')
+ assert.match(api, /reorderMenus|reorderColumns/)
+ assert.match(api, /column\/reorder/)
+ assert.match(api, /ordered_ids|orderedIds/)
+ const page = readSource('src/pages/account/MenusPage.vue')
+ assert.match(page, /siblingOrderAfterMove|reorderMenus|reorderColumns/)
+ assert.equal(/http\.post/.test(page), false, '页面不内联排序请求')
+})