diff --git a/apps/api/test/update.test.ts b/apps/api/test/update.test.ts index 8ebd59b..77bb8ad 100644 --- a/apps/api/test/update.test.ts +++ b/apps/api/test/update.test.ts @@ -432,3 +432,14 @@ test('group order keeps visible subsets stable and distinguishes unnamed and nam assert.deepEqual(orderedGroups(['日常'], ['理财', '', '日常']), ['日常']); assert.deepEqual(mergeGroupOrder(['日常', '理财'], ['理财', '隐藏']), ['日常', '理财', '隐藏']); }); + +test('drag insertion moves groups without swapping neighbours, mutating input or crossing bounds', async () => { + const { moveGroup } = await import('../../web/src/group-order'); + const original = ['日常', '', '消费', '理财']; + assert.deepEqual(moveGroup(original, 3, 0), ['理财', '日常', '', '消费']); + assert.deepEqual(moveGroup(original, 0, 3), ['', '消费', '理财', '日常']); + assert.deepEqual(original, ['日常', '', '消费', '理财']); + assert.equal(moveGroup(original, 0, -1), original); + assert.equal(moveGroup(original, 3, 4), original); + assert.equal(moveGroup(original, 2, 2), original); +}); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index d74022a..2b3a8c6 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,5 +1,13 @@ import { t as tr, useLocale, LanguagePicker } from './i18n'; -import { useState, useEffect, useRef, createElement, type FormEvent, type ReactNode } from 'react'; +import { + useState, + useEffect, + useRef, + createElement, + Fragment, + type FormEvent, + type ReactNode, +} from 'react'; import { LayoutDashboard, CalendarDays, @@ -39,6 +47,7 @@ import { } from './api'; import './style.css'; import { GroupOrderContext } from './GroupOrderContext'; +import { GroupOrderList } from './GroupOrderList'; import { orderedGroups, mergeGroupOrder } from './group-order'; import { Calendar } from './Calendar'; import { SchedulePanel } from './SchedulePanel'; @@ -1357,59 +1366,67 @@ export default function App() { (groupRanks.get(b.groupName || '') || 0) : 0, ) - .map((p) => ( - +
+ + {p.iconId ? ( + {tr('账户图标')} + ) : p.kind === 'asset' ? ( + + ) : p.kind === 'debt' ? ( + + ) : ( + + )} + + +
+

{p.name}

+ {p.groupName && {p.groupName}} +

+ {categoryLabel(p)} ·{' '} + {p.kind === 'debt' + ? tr(p.side === 'asset' ? '借款(待收)' : '欠款(待还)') + : p.side === 'asset' + ? tr('资产') + : tr( + p.kind === 'account' && p.amount.startsWith('-') + ? '溢缴存款' + : '负债', + )} + {p.archived ? tr(' · 已归档') : ''} +

+ + {money(positionAmount(p), p.currency)} + + + {tr('折合')}{' '} + {p.converted === null + ? tr('缺少汇率') + : money(positionAmount(p, p.converted || '0'), user.baseCurrency)} + + + ))} ) : ( @@ -2072,37 +2089,15 @@ export default function App() { )} {modal?.kind === 'group-order' && modal.groups && ( !busy && setModal(null)}> -

{tr('调整账户分组顺序,账户页和选择弹窗会同步使用。')}

-
    - {modal.groups.map((g, index) => ( -
  1. - {g || tr('未分组')} -
    - {[-1, 1].map((offset) => ( - - ))} -
    -
  2. - ))} -
+ + setModal((current) => + current?.kind === 'group-order' ? { ...current, groups } : current, + ) + } + />
+ {group || tr('未分组')} + + + ))} + + + {announcement} + + + ); +} diff --git a/apps/web/src/group-order.ts b/apps/web/src/group-order.ts index 01542cd..fa4a6e1 100644 --- a/apps/web/src/group-order.ts +++ b/apps/web/src/group-order.ts @@ -6,3 +6,12 @@ export function orderedGroups(groups: string[], order: string[] = []) { export function mergeGroupOrder(visible: string[], saved: string[]) { return [...visible, ...saved.filter((g) => !visible.includes(g))]; } + +export function moveGroup(groups: string[], from: number, to: number) { + if (from === to || from < 0 || to < 0 || from >= groups.length || to >= groups.length) + return groups; + const next = [...groups], + [group] = next.splice(from, 1); + next.splice(to, 0, group); + return next; +} diff --git a/apps/web/src/locales/en.json b/apps/web/src/locales/en.json index ff9dc4a..27bba13 100644 --- a/apps/web/src/locales/en.json +++ b/apps/web/src/locales/en.json @@ -473,5 +473,9 @@ "下移": "Move down", "分组顺序已保存": "Group order saved", "分组不可重复": "Groups must be unique", - "调整账户分组顺序,账户页和选择弹窗会同步使用。": "Change the group order for both accounts and account selection." + "调整账户分组顺序,账户页和选择弹窗会同步使用。": "Change the group order for both accounts and account selection.", + "拖动手柄调整顺序,也可聚焦手柄后使用方向键。保存后同步到账户页和选择弹窗。": "Drag a handle to reorder, or focus it and use the arrow keys. Save to apply the order to accounts and account selection.", + "账户分组顺序": "Account group order", + "拖动分组": "Drag group", + "已取消拖拽": "Drag cancelled" } diff --git a/apps/web/src/locales/zh-Hant.json b/apps/web/src/locales/zh-Hant.json index 5e1e815..1d2daa6 100644 --- a/apps/web/src/locales/zh-Hant.json +++ b/apps/web/src/locales/zh-Hant.json @@ -473,5 +473,9 @@ "下移": "下移", "分组顺序已保存": "分組順序已儲存", "分组不可重复": "分組不可重複", - "调整账户分组顺序,账户页和选择弹窗会同步使用。": "調整帳戶分組順序,帳戶頁和選擇彈窗會同步使用。" + "调整账户分组顺序,账户页和选择弹窗会同步使用。": "調整帳戶分組順序,帳戶頁和選擇彈窗會同步使用。", + "拖动手柄调整顺序,也可聚焦手柄后使用方向键。保存后同步到账户页和选择弹窗。": "拖動手柄調整順序,也可聚焦手柄後使用方向鍵。儲存後同步到帳戶頁和選擇彈窗。", + "账户分组顺序": "帳戶分組順序", + "拖动分组": "拖動分組", + "已取消拖拽": "已取消拖曳" } diff --git a/apps/web/src/style.css b/apps/web/src/style.css index 9671961..9dff229 100644 --- a/apps/web/src/style.css +++ b/apps/web/src/style.css @@ -1497,24 +1497,84 @@ textarea, .group-order-list { list-style: none; - padding: 0; - max-height: 55vh; + padding: 4px; + margin: 16px -4px; + max-height: 50vh; overflow-y: auto; + overscroll-behavior: contain; } .group-order-list li { display: flex; align-items: center; - justify-content: space-between; gap: 12px; - padding: 10px 0; - border-bottom: 1px solid #e5e9e5; + margin: 8px 0; + padding: 10px 12px; + border: 1px solid #e1e8e3; + border-radius: 12px; + background: #f8faf8; } -.group-order-list li > span { +.group-order-list li.drag-active { + background: #e6f2eb; + border-color: #79a38a; + box-shadow: 0 3px 12px #234c3b18; +} +.group-order-name { + flex: 1; min-width: 0; overflow-wrap: anywhere; + font-weight: 500; } -.group-order-list li > div { - display: flex; - gap: 6px; +.group-order-index { + font-size: 12px; + color: #718279; + font-variant-numeric: tabular-nums; +} +.group-drag-handle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + padding: 0; flex-shrink: 0; + color: #718279; + border: 0; + background: transparent; + border-radius: 8px; + cursor: grab; + touch-action: none; +} +.group-drag-handle:hover { + background: #e8eee9; + color: #234c3b; +} +.group-drag-handle:focus-visible { + outline: 2px solid #39785b; + outline-offset: 2px; +} +.group-order-list.is-dragging, +.group-order-list.is-dragging .group-drag-handle { + cursor: grabbing; +} +.group-order-help { + line-height: 1.6; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.position-grid > .account-group-title { + grid-column: 1 / -1; + margin: 8px 0 0; + font-size: 14px; + color: #456352; } diff --git a/docs/acceptance.md b/docs/acceptance.md index 8face57..ec97099 100644 --- a/docs/acceptance.md +++ b/docs/acceptance.md @@ -78,3 +78,7 @@ ## 2026-10-03 图库补充 内置图库扩充为 36 项,新增“抖音月付”和“东方财富”。真实数据库与已认证接口验证名称搜索、共享范围、图片读取和透明 PNG 规范;已有财务数据保持不变。资源来源与验收见 [更新说明](update-icons-2026-10-03.md)。 + +## 2026-10-03 拖拽排序与分组分行 + +33 项单元测试、前端类型检查和生产构建通过;真实数据库分组排序专项检查通过。浏览器验证实际拖拽、取消、键盘调整、保存刷新和弹窗联动,确认全部账户页按分组顺序分行显示。无新增数据库字段。详见 [更新说明](update-group-drag-2026-10-03.md)。 diff --git a/docs/account-group-rows-preview.png b/docs/account-group-rows-preview.png new file mode 100644 index 0000000..c34e76d Binary files /dev/null and b/docs/account-group-rows-preview.png differ diff --git a/docs/group-drag-preview.png b/docs/group-drag-preview.png new file mode 100644 index 0000000..36fac4b Binary files /dev/null and b/docs/group-drag-preview.png differ diff --git a/docs/update-group-drag-2026-10-03.md b/docs/update-group-drag-2026-10-03.md new file mode 100644 index 0000000..44c082c --- /dev/null +++ b/docs/update-group-drag-2026-10-03.md @@ -0,0 +1,26 @@ +# 2026-10-03 分组拖拽与账户分行 + +## 使用方式 + +分组排序弹窗移除上下移动按钮,改为带拖拽手柄、分组名称和序号的列表。按住手柄拖到目标位置即可调整顺序,拖拽时高亮当前分组,靠近列表边缘自动滚动。点击保存后,账户页、分组筛选及账户选择弹窗同步使用新顺序;取消弹窗不会保存。 + +手柄支持鼠标、触屏和触控笔的 Pointer Events;44 像素操作区域与 touch-action 防止拖动手柄时页面误滚动。键盘可聚焦手柄后使用上下方向键调整、Home 置顶、End 置底;拖动过程中 Esc 或指针取消会恢复本次拖动前的顺序。辅助状态提示当前位置。 + +“全部账户”按保存顺序显示分组标题;标题横跨卡片网格,因此每个分组从新行开始。相同分组内仍使用响应式卡片排列;选择单一分组时只显示该组账户。未分组仍为独立分组。 + +复用现有 User.accountGroupOrder、settings 和备份协议,未增加数据库表或字段,不改变余额和财务记录,无需新增数据库迁移或依赖。 + +## 验证 + +- 前端类型检查和生产构建通过;33 项单元测试通过,新增重排测试覆盖跨多项插入、保持相邻顺序、不修改输入及边界处理。 +- 真实 MySQL 分组排序专项检查通过:顺序持久化、用户隔离、非法输入拒绝、ZIP 恢复及旧备份兼容。此次未重复执行其他 12 项后端专项,上轮完整 13 项回归记录保留。 +- 浏览器专用虚构账号:将“日常”从末尾实际拖到首位;取消后重新打开仍为原顺序;验证 Home、End 和方向键移动;再次拖拽并保存后刷新,账户页与选择弹窗保持“日常、未分组、消费、理财”顺序。 +- 浏览器读取各分组标题和卡片的实际位置,确认不同组的卡片纵向分离,未共享同一行。 +- 已检查桌面截图;触屏与触控笔使用统一事件实现,未在真实手机硬件上实测。 +- 验收临时账号和脚本清理,保留用户原有 Vite 配置改动;仅本地 Git 提交,不 push。 + +完成时间:2026-10-03 17:04。 + +![拖拽分组排序](group-drag-preview.png) + +![全部账户按分组分行](account-group-rows-preview.png) diff --git a/docs/update-group-order-2026-10-03.md b/docs/update-group-order-2026-10-03.md index 857b043..03a419a 100644 --- a/docs/update-group-order-2026-10-03.md +++ b/docs/update-group-order-2026-10-03.md @@ -1,5 +1,7 @@ # 2026-10-03 账户分组排序 +当前排序界面已改为拖拽;最新操作及全部账户分行行为见 [后续更新](update-group-drag-2026-10-03.md)。以下保留首轮验收记录。 + ## 使用方式 账户页点击“分组排序”,使用“上移/下移”调整分组后保存。顺序同步用于账户分组筛选、全部账户卡片、分组输入建议以及转账、借贷操作和定时计划中的账户选择弹窗。刷新网页或重新登录后仍保留;取消不会保存。只有一个分组时排序按钮禁用,首尾按钮防止越界。 diff --git a/update.md b/update.md index 69081c5..f002b91 100644 --- a/update.md +++ b/update.md @@ -35,6 +35,8 @@ - ~~负债(如信用卡)也可以显示为正数变成存款~~ — 已完成并验证:2026-10-03 12:05 - ~~分组可以排序~~ — 已完成并验证:2026-10-03 12:27 - ~~“抖音月付”“东方财富”添加图标~~ — 已完成并验证:2026-10-03 12:36 +- ~~优化分组排序UI,使用拖拽排序~~ — 已完成并验证:2026-10-03 17:04 +- ~~在全部账号时,也要按照分组顺序显示,并且不太组不要在同一行~~ — 已完成并验证:2026-10-03 17:04 验证与部署边界见 [更新说明](docs/update-2026-10-02.md)。 @@ -49,3 +51,5 @@ 分组排序验收完成:2026-10-03 12:27。32 项单元测试、13 项真实 MySQL 集成检查、类型检查与构建通过;14 个迁移已应用,9 张表与 89 个字段注释已核验。账户页、筛选与选择弹窗顺序同步并持久化,备份恢复包含分组顺序。详见 [更新说明](docs/update-group-order-2026-10-03.md)。 图库补充验收完成:2026-10-03 12:36。新增“抖音月付”和“东方财富”图标,36 项内置图库已初始化,两个图标的已认证搜索和图片读取通过。详见 [更新说明](docs/update-icons-2026-10-03.md)。 + +分组界面验收完成:2026-10-03 17:04。支持拖拽与键盘调整顺序,全部账户按分组顺序分行显示;33 项单元测试、前端类型检查、生产构建和真实数据库分组排序专项检查通过。详见 [更新说明](docs/update-group-drag-2026-10-03.md)。