feat: node management enhancements and UX improvements (#297)

* feat(nodes): add renewal cycle and auto-advance scheduling

- Add renewal_cycle field to nodes for tracking paid vs free cycles
- Implement auto-advance scheduling when renewal is processed
- Add migration test for renewal_cycle column
- Update dashboard to show renewal cycle count
- Add renewal status display on node detail page

* fix(frontend): show rule count in compact view mode for forward pages

* style(frontend): format code and improve node card layout

Entire-Checkpoint: 08b760b76538

* fix: restore renewal.ts from main to resolve build errors
This commit is contained in:
sagit
2026-03-09 10:01:55 +08:00
committed by GitHub
parent 413081f72a
commit de9ab51def
50 changed files with 26959 additions and 371 deletions
+28 -20
View File
@@ -265,27 +265,30 @@ export default function TunnelPage() {
);
}, []);
const refreshTunnelList = useCallback(async (withLoading = true) => {
if (withLoading) {
setLoading(true);
}
try {
const tunnelsRes = await getTunnelList();
if (tunnelsRes.code === 0) {
applyTunnelList(mapTunnelApiItems(tunnelsRes.data || []));
} else {
toast.error(tunnelsRes.msg || "获取隧道列表失败");
}
} catch {
toast.error("获取隧道列表失败");
} finally {
const refreshTunnelList = useCallback(
async (withLoading = true) => {
if (withLoading) {
setLoading(false);
setLoading(true);
}
}
}, [applyTunnelList]);
try {
const tunnelsRes = await getTunnelList();
if (tunnelsRes.code === 0) {
applyTunnelList(mapTunnelApiItems(tunnelsRes.data || []));
} else {
toast.error(tunnelsRes.msg || "获取隧道列表失败");
}
} catch {
toast.error("获取隧道列表失败");
} finally {
if (withLoading) {
setLoading(false);
}
}
},
[applyTunnelList],
);
const refreshNodes = useCallback(async () => {
try {
@@ -381,12 +384,14 @@ export default function TunnelPage() {
const next = prev.filter((id) => id !== tunnelToDelete.id);
saveOrder(TUNNEL_ORDER_KEY, next);
return next;
});
setSelectedIds((prev) => {
const next = new Set(prev);
next.delete(tunnelToDelete.id);
return next;
});
} else {
@@ -858,11 +863,14 @@ export default function TunnelPage() {
label: `删除完成:成功 ${result.successCount} 项`,
percent: 100,
});
setTunnels((prev) => prev.filter((tunnel) => !selectedIds.has(tunnel.id)));
setTunnels((prev) =>
prev.filter((tunnel) => !selectedIds.has(tunnel.id)),
);
setTunnelOrder((prev) => {
const next = prev.filter((id) => !selectedIds.has(id));
saveOrder(TUNNEL_ORDER_KEY, next);
return next;
});
} else {