feat: drag account groups and separate grouped card rows
This commit is contained in:
1 parent
59837cf09c
commit
9a133e82e9
13 files changed
+360
-94
No files matched your search
@@ -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);
|
||||
});
|
||||
+78
-83
@@ -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) => (
|
||||
<button
|
||||
className="position-card"
|
||||
key={p.id}
|
||||
onClick={() =>
|
||||
page === 'account' && quickMode && !p.archived
|
||||
? setModal({ kind: 'revision', p })
|
||||
: setSelected(p.id)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<span className="position-icon">
|
||||
{p.iconId ? (
|
||||
<img src={iconUrl(p.iconId)} alt={tr('账户图标')} />
|
||||
) : p.kind === 'asset' ? (
|
||||
<Landmark />
|
||||
) : p.kind === 'debt' ? (
|
||||
<HandCoins />
|
||||
) : (
|
||||
<Wallet />
|
||||
)}
|
||||
</span>
|
||||
<ArrowUpRight size={18} />
|
||||
</div>
|
||||
<h3>{p.name}</h3>
|
||||
{p.groupName && <span className="badge">{p.groupName}</span>}
|
||||
<p>
|
||||
{categoryLabel(p)} ·{' '}
|
||||
{p.kind === 'debt'
|
||||
? tr(p.side === 'asset' ? '借款(待收)' : '欠款(待还)')
|
||||
: p.side === 'asset'
|
||||
? tr('资产')
|
||||
: tr(
|
||||
p.kind === 'account' && p.amount.startsWith('-')
|
||||
? '溢缴存款'
|
||||
: '负债',
|
||||
)}
|
||||
{p.archived ? tr(' · 已归档') : ''}
|
||||
</p>
|
||||
<strong
|
||||
className={
|
||||
positionAmount(p).startsWith('-') ? 'danger-text' : undefined
|
||||
.map((p, index, rows) => (
|
||||
<Fragment key={p.id}>
|
||||
{page === 'account' &&
|
||||
accountGroup === null &&
|
||||
(index === 0 ||
|
||||
(rows[index - 1].groupName || '') !== (p.groupName || '')) && (
|
||||
<h3 className="account-group-title">{p.groupName || tr('未分组')}</h3>
|
||||
)}
|
||||
<button
|
||||
className="position-card"
|
||||
key={p.id}
|
||||
onClick={() =>
|
||||
page === 'account' && quickMode && !p.archived
|
||||
? setModal({ kind: 'revision', p })
|
||||
: setSelected(p.id)
|
||||
}
|
||||
>
|
||||
{money(positionAmount(p), p.currency)}
|
||||
</strong>
|
||||
<small>
|
||||
{tr('折合')}{' '}
|
||||
{p.converted === null
|
||||
? tr('缺少汇率')
|
||||
: money(positionAmount(p, p.converted || '0'), user.baseCurrency)}
|
||||
</small>
|
||||
</button>
|
||||
<div>
|
||||
<span className="position-icon">
|
||||
{p.iconId ? (
|
||||
<img src={iconUrl(p.iconId)} alt={tr('账户图标')} />
|
||||
) : p.kind === 'asset' ? (
|
||||
<Landmark />
|
||||
) : p.kind === 'debt' ? (
|
||||
<HandCoins />
|
||||
) : (
|
||||
<Wallet />
|
||||
)}
|
||||
</span>
|
||||
<ArrowUpRight size={18} />
|
||||
</div>
|
||||
<h3>{p.name}</h3>
|
||||
{p.groupName && <span className="badge">{p.groupName}</span>}
|
||||
<p>
|
||||
{categoryLabel(p)} ·{' '}
|
||||
{p.kind === 'debt'
|
||||
? tr(p.side === 'asset' ? '借款(待收)' : '欠款(待还)')
|
||||
: p.side === 'asset'
|
||||
? tr('资产')
|
||||
: tr(
|
||||
p.kind === 'account' && p.amount.startsWith('-')
|
||||
? '溢缴存款'
|
||||
: '负债',
|
||||
)}
|
||||
{p.archived ? tr(' · 已归档') : ''}
|
||||
</p>
|
||||
<strong
|
||||
className={
|
||||
positionAmount(p).startsWith('-') ? 'danger-text' : undefined
|
||||
}
|
||||
>
|
||||
{money(positionAmount(p), p.currency)}
|
||||
</strong>
|
||||
<small>
|
||||
{tr('折合')}{' '}
|
||||
{p.converted === null
|
||||
? tr('缺少汇率')
|
||||
: money(positionAmount(p, p.converted || '0'), user.baseCurrency)}
|
||||
</small>
|
||||
</button>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -2072,37 +2089,15 @@ export default function App() {
|
||||
)}
|
||||
{modal?.kind === 'group-order' && modal.groups && (
|
||||
<Modal title={tr('分组排序')} close={() => !busy && setModal(null)}>
|
||||
<p className="muted">{tr('调整账户分组顺序,账户页和选择弹窗会同步使用。')}</p>
|
||||
<ol className="group-order-list">
|
||||
{modal.groups.map((g, index) => (
|
||||
<li key={g}>
|
||||
<span>{g || tr('未分组')}</span>
|
||||
<div>
|
||||
{[-1, 1].map((offset) => (
|
||||
<button
|
||||
key={offset}
|
||||
type="button"
|
||||
className="secondary"
|
||||
disabled={
|
||||
busy || index + offset < 0 || index + offset >= modal.groups!.length
|
||||
}
|
||||
aria-label={(g || tr('未分组')) + ' ' + tr(offset < 0 ? '上移' : '下移')}
|
||||
onClick={() =>
|
||||
setModal((current) => {
|
||||
if (!current?.groups) return current;
|
||||
const next = [...current.groups];
|
||||
[next[index], next[index + offset]] = [next[index + offset], next[index]];
|
||||
return { ...current, groups: next };
|
||||
})
|
||||
}
|
||||
>
|
||||
{tr(offset < 0 ? '上移' : '下移')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<GroupOrderList
|
||||
groups={modal.groups}
|
||||
busy={busy}
|
||||
onChange={(groups) =>
|
||||
setModal((current) =>
|
||||
current?.kind === 'group-order' ? { ...current, groups } : current,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="modal-actions">
|
||||
<button className="secondary" disabled={busy} onClick={() => setModal(null)}>
|
||||
{tr('取消')}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { useEffect, useId, useRef, useState, type PointerEvent } from 'react';
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import { t as tr } from './i18n';
|
||||
import { moveGroup } from './group-order';
|
||||
|
||||
export function GroupOrderList({
|
||||
groups,
|
||||
onChange,
|
||||
busy,
|
||||
}: {
|
||||
groups: string[];
|
||||
onChange: (groups: string[]) => void;
|
||||
busy: boolean;
|
||||
}) {
|
||||
const list = useRef<HTMLOListElement>(null),
|
||||
help = useId();
|
||||
const current = useRef({ groups, onChange });
|
||||
current.current = { groups, onChange };
|
||||
const drag = useRef<{ group: string; pointer: number; initial: string[]; y: number } | null>(
|
||||
null,
|
||||
);
|
||||
const frame = useRef<number | null>(null);
|
||||
const [active, setActive] = useState<string | null>(null),
|
||||
[announcement, setAnnouncement] = useState('');
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (frame.current !== null) cancelAnimationFrame(frame.current);
|
||||
},
|
||||
[],
|
||||
);
|
||||
function move(from: number, to: number) {
|
||||
const next = moveGroup(current.current.groups, from, to);
|
||||
if (next === current.current.groups) return;
|
||||
current.current.groups = next;
|
||||
current.current.onChange(next);
|
||||
setAnnouncement((next[to] || tr('未分组')) + ' · ' + (to + 1) + ' / ' + next.length);
|
||||
}
|
||||
function track() {
|
||||
const value = drag.current,
|
||||
container = list.current;
|
||||
if (!value || !container) return;
|
||||
const bounds = container.getBoundingClientRect();
|
||||
if (value.y < bounds.top + 30) container.scrollTop -= 10;
|
||||
else if (value.y > bounds.bottom - 30) container.scrollTop += 10;
|
||||
const rows = Array.from(container.children);
|
||||
let nearest = 0,
|
||||
distance = Infinity;
|
||||
rows.forEach((row, index) => {
|
||||
const box = row.getBoundingClientRect(),
|
||||
gap = Math.abs(value.y - (box.top + box.height / 2));
|
||||
if (gap < distance) {
|
||||
nearest = index;
|
||||
distance = gap;
|
||||
}
|
||||
});
|
||||
move(current.current.groups.indexOf(value.group), nearest);
|
||||
frame.current = requestAnimationFrame(track);
|
||||
}
|
||||
function finish(cancel = false) {
|
||||
const value = drag.current;
|
||||
if (!value) return;
|
||||
drag.current = null;
|
||||
if (frame.current !== null) cancelAnimationFrame(frame.current);
|
||||
frame.current = null;
|
||||
if (cancel) {
|
||||
current.current.groups = value.initial;
|
||||
current.current.onChange(value.initial);
|
||||
setAnnouncement(tr('已取消拖拽'));
|
||||
}
|
||||
setActive(null);
|
||||
if (list.current?.hasPointerCapture(value.pointer))
|
||||
list.current.releasePointerCapture(value.pointer);
|
||||
}
|
||||
function start(e: PointerEvent<HTMLButtonElement>, group: string) {
|
||||
if (busy || !e.isPrimary || e.button !== 0 || drag.current || groups.length < 2) return;
|
||||
e.preventDefault();
|
||||
e.currentTarget.focus();
|
||||
drag.current = { group, pointer: e.pointerId, initial: [...groups], y: e.clientY };
|
||||
list.current?.setPointerCapture(e.pointerId);
|
||||
setActive(group);
|
||||
frame.current = requestAnimationFrame(track);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<p id={help} className="muted group-order-help">
|
||||
{tr('拖动手柄调整顺序,也可聚焦手柄后使用方向键。保存后同步到账户页和选择弹窗。')}
|
||||
</p>
|
||||
<ol
|
||||
ref={list}
|
||||
className={'group-order-list' + (active !== null ? ' is-dragging' : '')}
|
||||
aria-label={tr('账户分组顺序')}
|
||||
onPointerMove={(e) => {
|
||||
if (drag.current?.pointer === e.pointerId) drag.current.y = e.clientY;
|
||||
}}
|
||||
onPointerUp={(e) => {
|
||||
if (drag.current?.pointer === e.pointerId) finish();
|
||||
}}
|
||||
onPointerCancel={(e) => {
|
||||
if (drag.current?.pointer === e.pointerId) finish(true);
|
||||
}}
|
||||
onLostPointerCapture={(e) => {
|
||||
if (drag.current?.pointer === e.pointerId) finish(true);
|
||||
}}
|
||||
>
|
||||
{groups.map((group, index) => (
|
||||
<li key={group} className={active === group ? 'drag-active' : undefined}>
|
||||
<button
|
||||
type="button"
|
||||
className="group-drag-handle"
|
||||
aria-label={tr('拖动分组') + ' ' + (group || tr('未分组'))}
|
||||
aria-describedby={help}
|
||||
disabled={busy || groups.length < 2}
|
||||
onPointerDown={(e) => start(e, group)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape' && drag.current) {
|
||||
e.preventDefault();
|
||||
finish(true);
|
||||
return;
|
||||
}
|
||||
if (drag.current || busy) return;
|
||||
const targets: Record<string, number> = {
|
||||
ArrowUp: index - 1,
|
||||
ArrowDown: index + 1,
|
||||
Home: 0,
|
||||
End: groups.length - 1,
|
||||
};
|
||||
if (e.key in targets) {
|
||||
e.preventDefault();
|
||||
move(index, targets[e.key]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<GripVertical size={20} aria-hidden="true" />
|
||||
</button>
|
||||
<span className="group-order-name">{group || tr('未分组')}</span>
|
||||
<span className="group-order-index" aria-hidden="true">
|
||||
{index + 1}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<span className="sr-only" role="status" aria-live="polite">
|
||||
{announcement}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -473,5 +473,9 @@
|
||||
"下移": "下移",
|
||||
"分组顺序已保存": "分組順序已儲存",
|
||||
"分组不可重复": "分組不可重複",
|
||||
"调整账户分组顺序,账户页和选择弹窗会同步使用。": "調整帳戶分組順序,帳戶頁和選擇彈窗會同步使用。"
|
||||
"调整账户分组顺序,账户页和选择弹窗会同步使用。": "調整帳戶分組順序,帳戶頁和選擇彈窗會同步使用。",
|
||||
"拖动手柄调整顺序,也可聚焦手柄后使用方向键。保存后同步到账户页和选择弹窗。": "拖動手柄調整順序,也可聚焦手柄後使用方向鍵。儲存後同步到帳戶頁和選擇彈窗。",
|
||||
"账户分组顺序": "帳戶分組順序",
|
||||
"拖动分组": "拖動分組",
|
||||
"已取消拖拽": "已取消拖曳"
|
||||
}
|
||||
+69
-9
@@ -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;
|
||||
}
|
||||
@@ -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)。
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -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。
|
||||
|
||||

|
||||
|
||||

|
||||
@@ -1,5 +1,7 @@
|
||||
# 2026-10-03 账户分组排序
|
||||
|
||||
当前排序界面已改为拖拽;最新操作及全部账户分行行为见 [后续更新](update-group-drag-2026-10-03.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)。
|
||||
Reference in new issue
Block a user