From eb526fef5d0c4ee163515dc8682ec6496d88a360 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:23:38 +0800 Subject: [PATCH] =?UTF-8?q?task-59(=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=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=9D=83=E9=99=90=E5=88=86=E7=BB=84=E5=88=97=E8=A1=A8=E5=92=8C?= =?UTF-8?q?=E6=91=98=E8=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 shop-group-model.ts 解析 Java ShopManageGroupItemVo(camel)并汇总(groupCount/memberTotal); shop-group-api.ts GET /api/admin/shop-manages/groups;GroupsPage 去内联 http,改走 adapter 展示分组列表 + 顶部摘要统计卡。8 用例全过,435 单测 + build 绿。 --- .../src/pages/account/GroupsPage.vue | 78 ++++++++++++++++++ .../src/pages/account/shop-group-api.ts | 10 +++ .../src/pages/account/shop-group-model.ts | 57 +++++++++++++ admin-frontend-vue/tests/task-59.test.ts | 81 +++++++++++++++++++ 4 files changed, 226 insertions(+) create mode 100644 admin-frontend-vue/src/pages/account/GroupsPage.vue create mode 100644 admin-frontend-vue/src/pages/account/shop-group-api.ts create mode 100644 admin-frontend-vue/src/pages/account/shop-group-model.ts create mode 100644 admin-frontend-vue/tests/task-59.test.ts diff --git a/admin-frontend-vue/src/pages/account/GroupsPage.vue b/admin-frontend-vue/src/pages/account/GroupsPage.vue new file mode 100644 index 00000000..f59b9753 --- /dev/null +++ b/admin-frontend-vue/src/pages/account/GroupsPage.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/admin-frontend-vue/src/pages/account/shop-group-api.ts b/admin-frontend-vue/src/pages/account/shop-group-api.ts new file mode 100644 index 00000000..6fd5d680 --- /dev/null +++ b/admin-frontend-vue/src/pages/account/shop-group-api.ts @@ -0,0 +1,10 @@ +/** 数据权限分组加载适配(任务 59):GET /api/admin/shop-manages/groups → 分组列表。 */ +import { http } from '@/api/http' +import { parseShopGroupList, type ShopGroupItem } from './shop-group-model' + +export const SHOP_GROUPS_ENDPOINT = '/api/admin/shop-manages/groups' + +export async function fetchShopGroups(): Promise { + const { data } = await http.get(SHOP_GROUPS_ENDPOINT) + return parseShopGroupList(data) +} diff --git a/admin-frontend-vue/src/pages/account/shop-group-model.ts b/admin-frontend-vue/src/pages/account/shop-group-model.ts new file mode 100644 index 00000000..17a7d1aa --- /dev/null +++ b/admin-frontend-vue/src/pages/account/shop-group-model.ts @@ -0,0 +1,57 @@ +/** 数据权限分组列表模型(任务 59):解析 Java ShopManageGroupItemVo + 摘要统计,纯逻辑。 */ +import { unwrap } from '../../api/envelope.ts' + +export interface ShopGroupItem { + id: number + name: string + leaderUsername: string + memberCount: number + memberUsernames: string[] + createdAt?: string +} + +function text(value: unknown): string { + return typeof value === 'string' ? value.trim() : '' +} + +function numberOrNull(value: unknown): number | null { + return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null +} + +/** 解析单条分组项;缺 id 视为无效。 */ +export function parseShopGroupItem(raw: unknown): ShopGroupItem | null { + if (!raw || typeof raw !== 'object') return null + const record = raw as Record + const id = numberOrNull(record.id) + if (id === null) return null + const names = Array.isArray(record.memberUsernames) + ? record.memberUsernames.map((v) => text(v)).filter(Boolean) + : [] + const count = numberOrNull(record.memberCount) + const item: ShopGroupItem = { + id, + name: text(record.groupName ?? record.name) || `分组${id}`, + leaderUsername: text(record.leaderUsername ?? record.leader_name), + memberCount: count === null ? names.length : count, + memberUsernames: names, + } + const createdAt = text(record.createdAt ?? record.created_at) + if (createdAt) item.createdAt = createdAt + return item +} + +/** 归一化分组列表响应(信封或裸数组)。 */ +export function parseShopGroupList(payload: unknown): ShopGroupItem[] { + const data = unwrap(payload) + if (!Array.isArray(data)) return [] + return data + .map((raw) => parseShopGroupItem(raw)) + .filter((item): item is ShopGroupItem => item !== null) +} + +/** 分组摘要:分组总数 + 组员总数。 */ +export function shopGroupSummary(groups: ShopGroupItem[]): { groupCount: number; memberTotal: number } { + let memberTotal = 0 + for (const group of groups) memberTotal += group.memberCount || 0 + return { groupCount: groups.length, memberTotal } +} diff --git a/admin-frontend-vue/tests/task-59.test.ts b/admin-frontend-vue/tests/task-59.test.ts new file mode 100644 index 00000000..2c0008b5 --- /dev/null +++ b/admin-frontend-vue/tests/task-59.test.ts @@ -0,0 +1,81 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { parseShopGroupList, shopGroupSummary } from '../src/pages/account/shop-group-model.ts' + +test('test_task_059_data_group_list_normal_primary_path', () => { + // 正常主路径:解析 Java ShopManageGroupItemVo(camel) 为展示项。 + const groups = parseShopGroupList({ + success: true, + data: [ + { + id: 1, + groupName: '华东组', + leaderUsername: '张伟恒', + memberUserIds: [2, 3], + memberUsernames: ['李四', '王五'], + memberCount: 2, + createdAt: '2026-01-01T00:00:00', + }, + ], + }) + assert.equal(groups.length, 1) + assert.equal(groups[0].name, '华东组') + assert.equal(groups[0].leaderUsername, '张伟恒') + assert.equal(groups[0].memberCount, 2) + assert.deepEqual(groups[0].memberUsernames, ['李四', '王五']) +}) + +test('test_task_059_data_group_list_normal_variant_input', () => { + // 正常变体:缺组长/组员给空默认;memberCount 缺失按列表长度回退。 + const groups = parseShopGroupList([{ id: 2, groupName: '组B', memberUsernames: ['a', 'b'] }]) + assert.equal(groups[0].leaderUsername, '') + assert.equal(groups[0].memberCount, 2) +}) + +test('test_task_059_data_group_list_normal_repeated_operation_is_idempotent', () => { + // 正常重复:解析不改输入、结果稳定。 + const payload = [{ id: 3, groupName: 'G', memberCount: 1 }] + assert.deepEqual(parseShopGroupList(payload), parseShopGroupList(payload)) + assert.equal(payload[0].memberCount, 1) +}) + +test('test_task_059_data_group_list_boundary_empty_input', () => { + // 边界空值:空负载/空数组回空列表;空摘要计数为 0。 + assert.deepEqual(parseShopGroupList({}), []) + assert.deepEqual(shopGroupSummary([]), { groupCount: 0, memberTotal: 0 }) +}) + +test('test_task_059_data_group_list_boundary_single_item', () => { + // 边界单元素:单分组摘要统计正确。 + const groups = parseShopGroupList([{ id: 4, groupName: 'A', memberCount: 3 }]) + assert.deepEqual(shopGroupSummary(groups), { groupCount: 1, memberTotal: 3 }) +}) + +test('test_task_059_data_group_list_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:多分组汇总组员总数;memberCount 缺失按 0。 + const groups = parseShopGroupList([ + { id: 1, groupName: 'A', memberCount: 2 }, + { id: 2, groupName: 'B', memberCount: 5 }, + ]) + assert.deepEqual(shopGroupSummary(groups), { groupCount: 2, memberTotal: 7 }) +}) + +test('test_task_059_data_group_list_invalid_input_rejected', () => { + // 异常输入:success=false 抛后端 message;非数组负载回空。 + assert.throws(() => parseShopGroupList({ success: false, message: '无权限查看分组' }), /无权限查看分组/) + assert.deepEqual(parseShopGroupList('garbage'), []) +}) + +test('test_task_059_data_group_list_dependency_failure_returns_actionable_message', () => { + // 依赖失败/加载走 adapter:GET shop-manages/groups,页面用 fetch + 摘要,不内联 http。 + const mod = readSource('src/pages/account/shop-group-model.ts') + assert.equal(/axios|http\./.test(mod), false, '分组模型保持纯逻辑') + const api = readSource('src/pages/account/shop-group-api.ts') + assert.match(api, /shop-manages\/groups/) + assert.match(api, /parseShopGroupList/) + const page = readSource('src/pages/account/GroupsPage.vue') + assert.match(page, /shop-group-api|fetchShopGroups/) + assert.match(page, /shopGroupSummary|summary/) + assert.equal(/http\.(get|post)|unwrap\(/.test(page), false, '页面不内联请求') +})