mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-10 11:36:36 +08:00
fix(frontend): refine node renewal UI and filters (#299)
* style: restore Prettier formatting in renewal.ts * fix(frontend): refine node renewal UI and filters Entire-Checkpoint: 88c8ae47fb7d * refactor(nodes): remove unused tags field from node model * refactor(node): improve card layout and reorder elements Entire-Checkpoint: fb89f3ebef5c
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
# Session Context
|
||||||
|
|
||||||
|
Session ID: ses_32f7e3b05ffeK8IxNvZAZISGWO
|
||||||
|
Commit Message: 编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||||
|
|
||||||
|
## Prompts
|
||||||
|
|
||||||
|
### Prompt 1
|
||||||
|
|
||||||
|
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||||
|
|
||||||
|
### Prompt 2
|
||||||
|
|
||||||
|
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||||
|
|
||||||
|
### Prompt 3
|
||||||
|
|
||||||
|
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||||
|
|
||||||
|
### Prompt 4
|
||||||
|
|
||||||
|
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||||
|
|
||||||
|
### Prompt 5
|
||||||
|
|
||||||
|
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
已成功构建并推送 `ghcr.io/sagit-chu/vite-frontend:beta` (linux/amd64)
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
|||||||
|
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
编译docker 镜像 ghcr.io/sagit-chu/vite-frontend:beta linux/amd64架构 推送
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
已成功构建并推送 `ghcr.io/sagit-chu/vite-frontend:beta` (linux/amd64)
|
||||||
@@ -7,13 +7,13 @@
|
|||||||
"title": "规则页精简模式显示规则数量",
|
"title": "规则页精简模式显示规则数量",
|
||||||
"version": "1.2.21",
|
"version": "1.2.21",
|
||||||
"summary": {
|
"summary": {
|
||||||
"additions": 67,
|
"additions": 4529,
|
||||||
"deletions": 57,
|
"deletions": 65,
|
||||||
"files": 1
|
"files": 11
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"created": 1773020783005,
|
"created": 1773020783005,
|
||||||
"updated": 1773020978012
|
"updated": 1773022814998
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"messages": [
|
"messages": [
|
||||||
@@ -3536,6 +3536,505 @@
|
|||||||
"messageID": "msg_cd0491b35001Gyu7B4fNEHivH2"
|
"messageID": "msg_cd0491b35001Gyu7B4fNEHivH2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"role": "user",
|
||||||
|
"time": {
|
||||||
|
"created": 1773022792451
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"diffs": []
|
||||||
|
},
|
||||||
|
"agent": "build",
|
||||||
|
"model": {
|
||||||
|
"providerID": "api",
|
||||||
|
"modelID": "gemini-3.1-pro-high"
|
||||||
|
},
|
||||||
|
"id": "msg_cd064ca87001mKUQE2EyR8PezH",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8"
|
||||||
|
},
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "这个没改对,页面没变化,请检查",
|
||||||
|
"id": "prt_cd064ca87002we266x5BvrANpB",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd064ca87001mKUQE2EyR8PezH"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"role": "assistant",
|
||||||
|
"time": {
|
||||||
|
"created": 1773022792467,
|
||||||
|
"completed": 1773022801627
|
||||||
|
},
|
||||||
|
"parentID": "msg_cd064ca87001mKUQE2EyR8PezH",
|
||||||
|
"modelID": "gemini-3.1-pro-high",
|
||||||
|
"providerID": "api",
|
||||||
|
"mode": "build",
|
||||||
|
"agent": "build",
|
||||||
|
"path": {
|
||||||
|
"cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon",
|
||||||
|
"root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon"
|
||||||
|
},
|
||||||
|
"cost": 0,
|
||||||
|
"tokens": {
|
||||||
|
"total": 58585,
|
||||||
|
"input": 58084,
|
||||||
|
"output": 59,
|
||||||
|
"reasoning": 442,
|
||||||
|
"cache": {
|
||||||
|
"read": 0,
|
||||||
|
"write": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"finish": "tool-calls",
|
||||||
|
"id": "msg_cd064cb13001TWoWh6nBESUPZl",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8"
|
||||||
|
},
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"type": "step-start",
|
||||||
|
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||||
|
"id": "prt_cd064ee23001PcfPcy835V87tU",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd064cb13001TWoWh6nBESUPZl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tool",
|
||||||
|
"callID": "bash-1773022801288808061-252",
|
||||||
|
"tool": "bash",
|
||||||
|
"state": {
|
||||||
|
"status": "completed",
|
||||||
|
"input": {
|
||||||
|
"command": "git show HEAD:vite-frontend/src/pages/forward.tsx | grep -B 5 -A 20 \"根据显示模式渲染不同内容\"",
|
||||||
|
"description": "Check original code structure for rendering modes"
|
||||||
|
},
|
||||||
|
"output": " value={batchProgress.percent}\n />\n </div>\n )}\n\n {/* 根据显示模式渲染不同内容 */}\n {compactMode ? (\n viewMode === \"grouped\" ? (\n sortedForwards.length > 0 ? (\n <>\n <div className=\"flex items-center justify-between px-1 mb-3\">\n <span className=\"text-sm font-semibold text-foreground\">\n 全部规则\n </span>\n <span className=\"text-xs text-default-600\">\n {sortedForwards.length} 条规则\n </span>\n </div>\n <div className=\"overflow-hidden rounded-xl border border-divider bg-content1 shadow-md\">\n <DndContext\n collisionDetection={closestCenter}\n sensors={sensors}\n onDragEnd={handleDragEnd}\n >\n <SortableContext\n items={sortableForwardIds}\n",
|
||||||
|
"title": "Check original code structure for rendering modes",
|
||||||
|
"metadata": {
|
||||||
|
"output": " value={batchProgress.percent}\n />\n </div>\n )}\n\n {/* 根据显示模式渲染不同内容 */}\n {compactMode ? (\n viewMode === \"grouped\" ? (\n sortedForwards.length > 0 ? (\n <>\n <div className=\"flex items-center justify-between px-1 mb-3\">\n <span className=\"text-sm font-semibold text-foreground\">\n 全部规则\n </span>\n <span className=\"text-xs text-default-600\">\n {sortedForwards.length} 条规则\n </span>\n </div>\n <div className=\"overflow-hidden rounded-xl border border-divider bg-content1 shadow-md\">\n <DndContext\n collisionDetection={closestCenter}\n sensors={sensors}\n onDragEnd={handleDragEnd}\n >\n <SortableContext\n items={sortableForwardIds}\n",
|
||||||
|
"exit": 0,
|
||||||
|
"description": "Check original code structure for rendering modes",
|
||||||
|
"truncated": false
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"start": 1773022801444,
|
||||||
|
"end": 1773022801445
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "prt_cd064ee24001VvqiBXMcI8KgPH",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd064cb13001TWoWh6nBESUPZl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "step-finish",
|
||||||
|
"reason": "tool-calls",
|
||||||
|
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||||
|
"cost": 0,
|
||||||
|
"tokens": {
|
||||||
|
"total": 58585,
|
||||||
|
"input": 58084,
|
||||||
|
"output": 59,
|
||||||
|
"reasoning": 442,
|
||||||
|
"cache": {
|
||||||
|
"read": 0,
|
||||||
|
"write": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "prt_cd064ee250016oJVS2eLQzwT01",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd064cb13001TWoWh6nBESUPZl"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"role": "assistant",
|
||||||
|
"time": {
|
||||||
|
"created": 1773022801632,
|
||||||
|
"completed": 1773022806653
|
||||||
|
},
|
||||||
|
"parentID": "msg_cd064ca87001mKUQE2EyR8PezH",
|
||||||
|
"modelID": "gemini-3.1-pro-high",
|
||||||
|
"providerID": "api",
|
||||||
|
"mode": "build",
|
||||||
|
"agent": "build",
|
||||||
|
"path": {
|
||||||
|
"cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon",
|
||||||
|
"root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon"
|
||||||
|
},
|
||||||
|
"cost": 0,
|
||||||
|
"tokens": {
|
||||||
|
"total": 58423,
|
||||||
|
"input": 1479,
|
||||||
|
"output": 44,
|
||||||
|
"reasoning": 0,
|
||||||
|
"cache": {
|
||||||
|
"read": 56900,
|
||||||
|
"write": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"finish": "tool-calls",
|
||||||
|
"id": "msg_cd064eedf001lx0GW03y2zsL2c",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8"
|
||||||
|
},
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"type": "step-start",
|
||||||
|
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||||
|
"id": "prt_cd06501d0001fbtq5FOqwf6wp7",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd064eedf001lx0GW03y2zsL2c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tool",
|
||||||
|
"callID": "bash-1773022806321799405-253",
|
||||||
|
"tool": "bash",
|
||||||
|
"state": {
|
||||||
|
"status": "completed",
|
||||||
|
"input": {
|
||||||
|
"description": "Check exactly what I changed in my last commit",
|
||||||
|
"command": "git diff HEAD~1 HEAD vite-frontend/src/pages/forward.tsx"
|
||||||
|
},
|
||||||
|
"output": "",
|
||||||
|
"title": "Check exactly what I changed in my last commit",
|
||||||
|
"metadata": {
|
||||||
|
"output": "",
|
||||||
|
"exit": 0,
|
||||||
|
"description": "Check exactly what I changed in my last commit",
|
||||||
|
"truncated": false
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"start": 1773022806480,
|
||||||
|
"end": 1773022806481
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "prt_cd06501d0002aJQPoXzQjQ6QxP",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd064eedf001lx0GW03y2zsL2c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "step-finish",
|
||||||
|
"reason": "tool-calls",
|
||||||
|
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||||
|
"cost": 0,
|
||||||
|
"tokens": {
|
||||||
|
"total": 58423,
|
||||||
|
"input": 1479,
|
||||||
|
"output": 44,
|
||||||
|
"reasoning": 0,
|
||||||
|
"cache": {
|
||||||
|
"read": 56900,
|
||||||
|
"write": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "prt_cd06501d1001UHeBODXWcVRT3P",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd064eedf001lx0GW03y2zsL2c"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"role": "assistant",
|
||||||
|
"time": {
|
||||||
|
"created": 1773022806657,
|
||||||
|
"completed": 1773022810679
|
||||||
|
},
|
||||||
|
"parentID": "msg_cd064ca87001mKUQE2EyR8PezH",
|
||||||
|
"modelID": "gemini-3.1-pro-high",
|
||||||
|
"providerID": "api",
|
||||||
|
"mode": "build",
|
||||||
|
"agent": "build",
|
||||||
|
"path": {
|
||||||
|
"cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon",
|
||||||
|
"root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon"
|
||||||
|
},
|
||||||
|
"cost": 0,
|
||||||
|
"tokens": {
|
||||||
|
"total": 58460,
|
||||||
|
"input": 1540,
|
||||||
|
"output": 25,
|
||||||
|
"reasoning": 0,
|
||||||
|
"cache": {
|
||||||
|
"read": 56895,
|
||||||
|
"write": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"finish": "tool-calls",
|
||||||
|
"id": "msg_cd065028100100U8RbhbWKxrf3",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8"
|
||||||
|
},
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"type": "step-start",
|
||||||
|
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||||
|
"id": "prt_cd06511890014PkWgUY9CAmTPd",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd065028100100U8RbhbWKxrf3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tool",
|
||||||
|
"callID": "bash-1773022810346559153-254",
|
||||||
|
"tool": "bash",
|
||||||
|
"state": {
|
||||||
|
"status": "completed",
|
||||||
|
"input": {
|
||||||
|
"description": "Check latest commit",
|
||||||
|
"command": "git log -1"
|
||||||
|
},
|
||||||
|
"output": "commit 78af992b6fde82eb3af4d09daa02a43866210636\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:59:59 2026 +0800\n\n fix: restore renewal.ts from main to resolve build errors\n",
|
||||||
|
"title": "Check latest commit",
|
||||||
|
"metadata": {
|
||||||
|
"output": "commit 78af992b6fde82eb3af4d09daa02a43866210636\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:59:59 2026 +0800\n\n fix: restore renewal.ts from main to resolve build errors\n",
|
||||||
|
"exit": 0,
|
||||||
|
"description": "Check latest commit",
|
||||||
|
"truncated": false
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"start": 1773022810506,
|
||||||
|
"end": 1773022810506
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "prt_cd065118a0014hx1B9opNcSfJl",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd065028100100U8RbhbWKxrf3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "step-finish",
|
||||||
|
"reason": "tool-calls",
|
||||||
|
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||||
|
"cost": 0,
|
||||||
|
"tokens": {
|
||||||
|
"total": 58460,
|
||||||
|
"input": 1540,
|
||||||
|
"output": 25,
|
||||||
|
"reasoning": 0,
|
||||||
|
"cache": {
|
||||||
|
"read": 56895,
|
||||||
|
"write": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "prt_cd065118b001SISeN0O2IRDH5C",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd065028100100U8RbhbWKxrf3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"role": "assistant",
|
||||||
|
"time": {
|
||||||
|
"created": 1773022810683,
|
||||||
|
"completed": 1773022814810
|
||||||
|
},
|
||||||
|
"parentID": "msg_cd064ca87001mKUQE2EyR8PezH",
|
||||||
|
"modelID": "gemini-3.1-pro-high",
|
||||||
|
"providerID": "api",
|
||||||
|
"mode": "build",
|
||||||
|
"agent": "build",
|
||||||
|
"path": {
|
||||||
|
"cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon",
|
||||||
|
"root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon"
|
||||||
|
},
|
||||||
|
"cost": 0,
|
||||||
|
"tokens": {
|
||||||
|
"total": 58595,
|
||||||
|
"input": 1681,
|
||||||
|
"output": 24,
|
||||||
|
"reasoning": 0,
|
||||||
|
"cache": {
|
||||||
|
"read": 56890,
|
||||||
|
"write": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"finish": "tool-calls",
|
||||||
|
"id": "msg_cd065123a001uXPsKZwVH7RDJb",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8"
|
||||||
|
},
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"type": "step-start",
|
||||||
|
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||||
|
"id": "prt_cd06521a8001yiaSoVddfbN1tj",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd065123a001uXPsKZwVH7RDJb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tool",
|
||||||
|
"callID": "bash-1773022814469937121-255",
|
||||||
|
"tool": "bash",
|
||||||
|
"state": {
|
||||||
|
"status": "completed",
|
||||||
|
"input": {
|
||||||
|
"description": "Check history",
|
||||||
|
"command": "git log -3"
|
||||||
|
},
|
||||||
|
"output": "commit 78af992b6fde82eb3af4d09daa02a43866210636\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:59:59 2026 +0800\n\n fix: restore renewal.ts from main to resolve build errors\n\ncommit f790becda8ead611b847637daafad6e7d40d8115\nMerge: e7548e7 413081f\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:58:02 2026 +0800\n\n merge: resolve conflicts with main branch\n\ncommit e7548e7b30ecd1c73e4ef98f5d36e6b6394c209a\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:51:28 2026 +0800\n\n style(frontend): format code and improve node card layout\n \n Entire-Checkpoint: 08b760b76538\n",
|
||||||
|
"title": "Check history",
|
||||||
|
"metadata": {
|
||||||
|
"output": "commit 78af992b6fde82eb3af4d09daa02a43866210636\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:59:59 2026 +0800\n\n fix: restore renewal.ts from main to resolve build errors\n\ncommit f790becda8ead611b847637daafad6e7d40d8115\nMerge: e7548e7 413081f\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:58:02 2026 +0800\n\n merge: resolve conflicts with main branch\n\ncommit e7548e7b30ecd1c73e4ef98f5d36e6b6394c209a\nAuthor: sagitchu <sagitchu@gmail.com>\nDate: Mon Mar 9 09:51:28 2026 +0800\n\n style(frontend): format code and improve node card layout\n \n Entire-Checkpoint: 08b760b76538\n",
|
||||||
|
"exit": 0,
|
||||||
|
"description": "Check history",
|
||||||
|
"truncated": false
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"start": 1773022814633,
|
||||||
|
"end": 1773022814633
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "prt_cd06521a8002I2rlXCrLkmKM1d",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd065123a001uXPsKZwVH7RDJb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "step-finish",
|
||||||
|
"reason": "tool-calls",
|
||||||
|
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||||
|
"cost": 0,
|
||||||
|
"tokens": {
|
||||||
|
"total": 58595,
|
||||||
|
"input": 1681,
|
||||||
|
"output": 24,
|
||||||
|
"reasoning": 0,
|
||||||
|
"cache": {
|
||||||
|
"read": 56890,
|
||||||
|
"write": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "prt_cd06521a9001CzTlcvvzi6vd8Y",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd065123a001uXPsKZwVH7RDJb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"role": "assistant",
|
||||||
|
"time": {
|
||||||
|
"created": 1773022814814,
|
||||||
|
"completed": 1773022818942
|
||||||
|
},
|
||||||
|
"parentID": "msg_cd064ca87001mKUQE2EyR8PezH",
|
||||||
|
"modelID": "gemini-3.1-pro-high",
|
||||||
|
"providerID": "api",
|
||||||
|
"mode": "build",
|
||||||
|
"agent": "build",
|
||||||
|
"path": {
|
||||||
|
"cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon",
|
||||||
|
"root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon"
|
||||||
|
},
|
||||||
|
"cost": 0,
|
||||||
|
"tokens": {
|
||||||
|
"total": 58974,
|
||||||
|
"input": 2054,
|
||||||
|
"output": 33,
|
||||||
|
"reasoning": 0,
|
||||||
|
"cache": {
|
||||||
|
"read": 56887,
|
||||||
|
"write": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"finish": "tool-calls",
|
||||||
|
"id": "msg_cd065225e0010wlkl67le6h46g",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8"
|
||||||
|
},
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"type": "step-start",
|
||||||
|
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||||
|
"id": "prt_cd06531d8001vtXeqUZ9z4fx14",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd065225e0010wlkl67le6h46g"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tool",
|
||||||
|
"callID": "bash-1773022818624349202-256",
|
||||||
|
"tool": "bash",
|
||||||
|
"state": {
|
||||||
|
"status": "completed",
|
||||||
|
"input": {
|
||||||
|
"command": "git log --oneline | head -n 10",
|
||||||
|
"description": "Check recent commits"
|
||||||
|
},
|
||||||
|
"output": "78af992 fix: restore renewal.ts from main to resolve build errors\nf790bec merge: resolve conflicts with main branch\ne7548e7 style(frontend): format code and improve node card layout\n50e4107 fix(frontend): show rule count in compact view mode for forward pages\n413081f feat(nodes): add renewal cycle and auto-advance scheduling (#296)\nbfac640 feat(nodes): add renewal cycle and auto-advance scheduling\n508a37a feat(nodes): add node metadata and expiry reminders (#293)\nd606550 feat(forward): display tunnel traffic ratio on forward page\n31ef861 feat(nodes): add node metadata and expiry reminders (#246)\nf1bdb2e feat(frontend): implement AJAX no-refresh UX improvements (#276)\n",
|
||||||
|
"title": "Check recent commits",
|
||||||
|
"metadata": {
|
||||||
|
"output": "78af992 fix: restore renewal.ts from main to resolve build errors\nf790bec merge: resolve conflicts with main branch\ne7548e7 style(frontend): format code and improve node card layout\n50e4107 fix(frontend): show rule count in compact view mode for forward pages\n413081f feat(nodes): add renewal cycle and auto-advance scheduling (#296)\nbfac640 feat(nodes): add renewal cycle and auto-advance scheduling\n508a37a feat(nodes): add node metadata and expiry reminders (#293)\nd606550 feat(forward): display tunnel traffic ratio on forward page\n31ef861 feat(nodes): add node metadata and expiry reminders (#246)\nf1bdb2e feat(frontend): implement AJAX no-refresh UX improvements (#276)\n",
|
||||||
|
"exit": 0,
|
||||||
|
"description": "Check recent commits",
|
||||||
|
"truncated": false
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"start": 1773022818776,
|
||||||
|
"end": 1773022818776
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "prt_cd06531d8002HjsfEaDHfZzBM7",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd065225e0010wlkl67le6h46g"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "step-finish",
|
||||||
|
"reason": "tool-calls",
|
||||||
|
"snapshot": "97ed982edddf0655a654981a72da08c33b9e467d",
|
||||||
|
"cost": 0,
|
||||||
|
"tokens": {
|
||||||
|
"total": 58974,
|
||||||
|
"input": 2054,
|
||||||
|
"output": 33,
|
||||||
|
"reasoning": 0,
|
||||||
|
"cache": {
|
||||||
|
"read": 56887,
|
||||||
|
"write": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "prt_cd06531d9001eYxQ5nQBoU1rsk",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8",
|
||||||
|
"messageID": "msg_cd065225e0010wlkl67le6h46g"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"role": "assistant",
|
||||||
|
"time": {
|
||||||
|
"created": 1773022818946,
|
||||||
|
"completed": 1773022824318
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"name": "MessageAbortedError",
|
||||||
|
"data": {
|
||||||
|
"message": "The operation was aborted."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parentID": "msg_cd064ca87001mKUQE2EyR8PezH",
|
||||||
|
"modelID": "gemini-3.1-pro-high",
|
||||||
|
"providerID": "api",
|
||||||
|
"mode": "build",
|
||||||
|
"agent": "build",
|
||||||
|
"path": {
|
||||||
|
"cwd": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon",
|
||||||
|
"root": "/Users/sagit/.local/share/opencode/worktree/13650be91caa3a9e08a2e63127fef62757658384/quiet-canyon"
|
||||||
|
},
|
||||||
|
"cost": 0,
|
||||||
|
"tokens": {
|
||||||
|
"input": 0,
|
||||||
|
"output": 0,
|
||||||
|
"reasoning": 0,
|
||||||
|
"cache": {
|
||||||
|
"read": 0,
|
||||||
|
"write": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "msg_cd0653282001hDYLdWTwLVss9i",
|
||||||
|
"sessionID": "ses_32fb9de62ffeXG3aOGlPDusGE8"
|
||||||
|
},
|
||||||
|
"parts": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
规则标签页下,精简模式,和非精简模式一样要显示规则数量
|
这个没改对,页面没变化,请检查
|
||||||
@@ -262,7 +262,6 @@ func (h *Handler) nodeCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
nullableText(asString(req["interfaceName"])),
|
nullableText(asString(req["interfaceName"])),
|
||||||
nullableText(""),
|
nullableText(""),
|
||||||
nullableText(strings.TrimSpace(asString(req["remark"]))),
|
nullableText(strings.TrimSpace(asString(req["remark"]))),
|
||||||
nullableText(strings.TrimSpace(asString(req["tags"]))),
|
|
||||||
nullableUnixMilli(asInt64(req["expiryTime"], 0)),
|
nullableUnixMilli(asInt64(req["expiryTime"], 0)),
|
||||||
nullableText(normalizeNodeRenewalCycle(asString(req["renewalCycle"]))),
|
nullableText(normalizeNodeRenewalCycle(asString(req["renewalCycle"]))),
|
||||||
asInt(req["http"], 0),
|
asInt(req["http"], 0),
|
||||||
@@ -331,7 +330,6 @@ func (h *Handler) nodeUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
nullableText(asString(req["interfaceName"])),
|
nullableText(asString(req["interfaceName"])),
|
||||||
nullableText(asString(req["extraIPs"])),
|
nullableText(asString(req["extraIPs"])),
|
||||||
nullableText(strings.TrimSpace(asString(req["remark"]))),
|
nullableText(strings.TrimSpace(asString(req["remark"]))),
|
||||||
nullableText(strings.TrimSpace(asString(req["tags"]))),
|
|
||||||
nullableUnixMilli(asInt64(req["expiryTime"], 0)),
|
nullableUnixMilli(asInt64(req["expiryTime"], 0)),
|
||||||
nullableText(normalizeNodeRenewalCycle(asString(req["renewalCycle"]))),
|
nullableText(normalizeNodeRenewalCycle(asString(req["renewalCycle"]))),
|
||||||
newHTTP,
|
newHTTP,
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ type Node struct {
|
|||||||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
Name string `gorm:"type:varchar(100);not null"`
|
Name string `gorm:"type:varchar(100);not null"`
|
||||||
Remark sql.NullString `gorm:"column:remark;type:text"`
|
Remark sql.NullString `gorm:"column:remark;type:text"`
|
||||||
Tags sql.NullString `gorm:"column:tags;type:text"`
|
|
||||||
ExpiryTime sql.NullInt64 `gorm:"column:expiry_time"`
|
ExpiryTime sql.NullInt64 `gorm:"column:expiry_time"`
|
||||||
RenewalCycle sql.NullString `gorm:"column:renewal_cycle;type:varchar(20)"`
|
RenewalCycle sql.NullString `gorm:"column:renewal_cycle;type:varchar(20)"`
|
||||||
Secret string `gorm:"type:varchar(100);not null"`
|
Secret string `gorm:"type:varchar(100);not null"`
|
||||||
@@ -341,7 +340,6 @@ type NodeBackup struct {
|
|||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Remark string `json:"remark,omitempty"`
|
Remark string `json:"remark,omitempty"`
|
||||||
Tags string `json:"tags,omitempty"`
|
|
||||||
ExpiryTime int64 `json:"expiryTime,omitempty"`
|
ExpiryTime int64 `json:"expiryTime,omitempty"`
|
||||||
RenewalCycle string `json:"renewalCycle,omitempty"`
|
RenewalCycle string `json:"renewalCycle,omitempty"`
|
||||||
Secret string `json:"secret"`
|
Secret string `json:"secret"`
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ func prepareSQLiteLegacyColumns(db *gorm.DB) error {
|
|||||||
m := db.Migrator()
|
m := db.Migrator()
|
||||||
|
|
||||||
if m.HasTable(&model.Node{}) {
|
if m.HasTable(&model.Node{}) {
|
||||||
for _, field := range []string{"ServerIPV4", "ServerIPV6", "ExtraIPs", "TCPListenAddr", "UDPListenAddr", "Inx", "IsRemote", "RemoteURL", "RemoteToken", "RemoteConfig", "Remark", "Tags", "ExpiryTime", "RenewalCycle"} {
|
for _, field := range []string{"ServerIPV4", "ServerIPV6", "ExtraIPs", "TCPListenAddr", "UDPListenAddr", "Inx", "IsRemote", "RemoteURL", "RemoteToken", "RemoteConfig", "Remark", "ExpiryTime", "RenewalCycle"} {
|
||||||
if m.HasColumn(&model.Node{}, field) {
|
if m.HasColumn(&model.Node{}, field) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -635,7 +635,6 @@ func (r *Repository) ListNodes() ([]map[string]interface{}, error) {
|
|||||||
items = append(items, map[string]interface{}{
|
items = append(items, map[string]interface{}{
|
||||||
"id": n.ID, "inx": n.Inx, "name": n.Name,
|
"id": n.ID, "inx": n.Inx, "name": n.Name,
|
||||||
"remark": nullableString(n.Remark),
|
"remark": nullableString(n.Remark),
|
||||||
"tags": nullableString(n.Tags),
|
|
||||||
"expiryTime": nullableInt64(n.ExpiryTime),
|
"expiryTime": nullableInt64(n.ExpiryTime),
|
||||||
"renewalCycle": nullableString(n.RenewalCycle),
|
"renewalCycle": nullableString(n.RenewalCycle),
|
||||||
"ip": n.ServerIP, "serverIp": n.ServerIP,
|
"ip": n.ServerIP, "serverIp": n.ServerIP,
|
||||||
@@ -1820,7 +1819,7 @@ func (r *Repository) exportNodes() ([]model.NodeBackup, error) {
|
|||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
b := model.NodeBackup{
|
b := model.NodeBackup{
|
||||||
ID: n.ID, Name: n.Name, Secret: n.Secret, ServerIP: n.ServerIP,
|
ID: n.ID, Name: n.Name, Secret: n.Secret, ServerIP: n.ServerIP,
|
||||||
Remark: n.Remark.String, Tags: n.Tags.String, RenewalCycle: n.RenewalCycle.String,
|
Remark: n.Remark.String, RenewalCycle: n.RenewalCycle.String,
|
||||||
Port: n.Port, HTTP: n.HTTP, TLS: n.TLS, Socks: n.Socks,
|
Port: n.Port, HTTP: n.HTTP, TLS: n.TLS, Socks: n.Socks,
|
||||||
CreatedTime: n.CreatedTime, Status: n.Status,
|
CreatedTime: n.CreatedTime, Status: n.Status,
|
||||||
TCPListenAddr: n.TCPListenAddr, UDPListenAddr: n.UDPListenAddr,
|
TCPListenAddr: n.TCPListenAddr, UDPListenAddr: n.UDPListenAddr,
|
||||||
@@ -2179,7 +2178,6 @@ func importNodes(tx *gorm.DB, nodes []model.NodeBackup, now int64) (int, error)
|
|||||||
ID: n.ID,
|
ID: n.ID,
|
||||||
Name: n.Name,
|
Name: n.Name,
|
||||||
Remark: sql.NullString{String: n.Remark, Valid: n.Remark != ""},
|
Remark: sql.NullString{String: n.Remark, Valid: n.Remark != ""},
|
||||||
Tags: sql.NullString{String: n.Tags, Valid: n.Tags != ""},
|
|
||||||
ExpiryTime: sql.NullInt64{Int64: n.ExpiryTime, Valid: n.ExpiryTime > 0},
|
ExpiryTime: sql.NullInt64{Int64: n.ExpiryTime, Valid: n.ExpiryTime > 0},
|
||||||
RenewalCycle: sql.NullString{String: n.RenewalCycle, Valid: n.RenewalCycle != ""},
|
RenewalCycle: sql.NullString{String: n.RenewalCycle, Valid: n.RenewalCycle != ""},
|
||||||
Secret: n.Secret,
|
Secret: n.Secret,
|
||||||
@@ -2206,7 +2204,7 @@ func importNodes(tx *gorm.DB, nodes []model.NodeBackup, now int64) (int, error)
|
|||||||
err := tx.Clauses(clause.OnConflict{
|
err := tx.Clauses(clause.OnConflict{
|
||||||
Columns: []clause.Column{{Name: "id"}},
|
Columns: []clause.Column{{Name: "id"}},
|
||||||
DoUpdates: clause.AssignmentColumns([]string{
|
DoUpdates: clause.AssignmentColumns([]string{
|
||||||
"name", "remark", "tags", "expiry_time", "renewal_cycle", "secret", "server_ip", "server_ip_v4", "server_ip_v6", "port", "interface_name", "version",
|
"name", "remark", "expiry_time", "renewal_cycle", "secret", "server_ip", "server_ip_v4", "server_ip_v6", "port", "interface_name", "version",
|
||||||
"http", "tls", "socks", "updated_time", "status", "tcp_listen_addr", "udp_listen_addr",
|
"http", "tls", "socks", "updated_time", "status", "tcp_listen_addr", "udp_listen_addr",
|
||||||
"inx", "is_remote", "remote_url", "remote_token", "remote_config",
|
"inx", "is_remote", "remote_url", "remote_token", "remote_config",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func TestPrepareSQLiteLegacyColumnsAddsNodeMetadataColumns(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := db.Migrator()
|
m := db.Migrator()
|
||||||
for _, field := range []string{"Remark", "Tags", "ExpiryTime", "RenewalCycle"} {
|
for _, field := range []string{"Remark", "ExpiryTime", "RenewalCycle"} {
|
||||||
if !m.HasColumn(&model.Node{}, field) {
|
if !m.HasColumn(&model.Node{}, field) {
|
||||||
t.Fatalf("expected node.%s column to exist", field)
|
t.Fatalf("expected node.%s column to exist", field)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,14 +197,13 @@ func (r *Repository) GetUserDefaultsForTunnel(userID int64) (flow int64, num int
|
|||||||
return user.Flow, user.Num, user.ExpTime, user.FlowResetTime, nil
|
return user.Flow, user.Num, user.ExpTime, user.FlowResetTime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) CreateNode(name, secret, serverIP string, serverIPV4, serverIPV6, port, interfaceName, version, remark, tags, expiryTime, renewalCycle interface{}, httpFlag, tlsFlag, socksFlag int, now int64, status int, tcpAddr, udpAddr string, inx, isRemote int, remoteURL, remoteToken, remoteConfig, extraIPs interface{}) error {
|
func (r *Repository) CreateNode(name, secret, serverIP string, serverIPV4, serverIPV6, port, interfaceName, version, remark, expiryTime, renewalCycle interface{}, httpFlag, tlsFlag, socksFlag int, now int64, status int, tcpAddr, udpAddr string, inx, isRemote int, remoteURL, remoteToken, remoteConfig, extraIPs interface{}) error {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return errors.New("repository not initialized")
|
return errors.New("repository not initialized")
|
||||||
}
|
}
|
||||||
node := model.Node{
|
node := model.Node{
|
||||||
Name: name,
|
Name: name,
|
||||||
Remark: nullStringFromInterface(remark),
|
Remark: nullStringFromInterface(remark),
|
||||||
Tags: nullStringFromInterface(tags),
|
|
||||||
ExpiryTime: nullInt64FromInterface(expiryTime),
|
ExpiryTime: nullInt64FromInterface(expiryTime),
|
||||||
RenewalCycle: nullStringFromInterface(renewalCycle),
|
RenewalCycle: nullStringFromInterface(renewalCycle),
|
||||||
Secret: secret,
|
Secret: secret,
|
||||||
@@ -244,7 +243,7 @@ func (r *Repository) GetNodeStatusFields(nodeID int64) (status, httpFlag, tlsFla
|
|||||||
return node.Status, node.HTTP, node.TLS, node.Socks, nil
|
return node.Status, node.HTTP, node.TLS, node.Socks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) UpdateNode(id int64, name, serverIP string, serverIPV4, serverIPV6, port, interfaceName, extraIPs, remark, tags, expiryTime, renewalCycle interface{}, httpFlag, tlsFlag, socksFlag int, tcpAddr, udpAddr string, now int64) error {
|
func (r *Repository) UpdateNode(id int64, name, serverIP string, serverIPV4, serverIPV6, port, interfaceName, extraIPs, remark, expiryTime, renewalCycle interface{}, httpFlag, tlsFlag, socksFlag int, tcpAddr, udpAddr string, now int64) error {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return errors.New("repository not initialized")
|
return errors.New("repository not initialized")
|
||||||
}
|
}
|
||||||
@@ -253,7 +252,6 @@ func (r *Repository) UpdateNode(id int64, name, serverIP string, serverIPV4, ser
|
|||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
"name": name,
|
"name": name,
|
||||||
"remark": nullStringFromInterface(remark),
|
"remark": nullStringFromInterface(remark),
|
||||||
"tags": nullStringFromInterface(tags),
|
|
||||||
"expiry_time": nullInt64FromInterface(expiryTime),
|
"expiry_time": nullInt64FromInterface(expiryTime),
|
||||||
"renewal_cycle": nullStringFromInterface(renewalCycle),
|
"renewal_cycle": nullStringFromInterface(renewalCycle),
|
||||||
"server_ip": serverIP,
|
"server_ip": serverIP,
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Node Full-Stack Tags Removal Plan
|
||||||
|
|
||||||
|
- [x] Remove node tags usage from frontend node management and dashboard views
|
||||||
|
- [x] Remove node tags fields from backend models, handlers, repository, and backup logic
|
||||||
|
- [x] Verify frontend build and backend tests pass after the removal
|
||||||
@@ -4,7 +4,6 @@ export interface NodeApiItem {
|
|||||||
status: number;
|
status: number;
|
||||||
inx?: number;
|
inx?: number;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
tags?: string;
|
|
||||||
expiryTime?: number;
|
expiryTime?: number;
|
||||||
renewalCycle?: "month" | "quarter" | "year" | "";
|
renewalCycle?: "month" | "quarter" | "year" | "";
|
||||||
syncError?: string;
|
syncError?: string;
|
||||||
@@ -220,7 +219,6 @@ export interface NodeMutationPayload {
|
|||||||
status?: number;
|
status?: number;
|
||||||
inx?: number;
|
inx?: number;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
tags?: string;
|
|
||||||
expiryTime?: number;
|
expiryTime?: number;
|
||||||
renewalCycle?: "month" | "quarter" | "year" | "";
|
renewalCycle?: "month" | "quarter" | "year" | "";
|
||||||
serverIp?: string;
|
serverIp?: string;
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export default function DashboardPage() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const diffDays = Math.ceil(
|
const diffDays = Math.ceil(
|
||||||
(nextDueTime - Date.now()) / (1000 * 60 * 60 * 24),
|
(nextDueTime - Date.now()) / (1000 * 60 * 60 * 24),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -122,16 +122,6 @@ const diffDays = Math.ceil(
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseNodeTags = (tags?: string) => {
|
|
||||||
if (!tags) return [];
|
|
||||||
|
|
||||||
return tags
|
|
||||||
.split(",")
|
|
||||||
.map((tag) => tag.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
.slice(0, 3);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderNodeExpiryCard = (node: DashboardNodeExpiryItem) => {
|
const renderNodeExpiryCard = (node: DashboardNodeExpiryItem) => {
|
||||||
const renewalSnapshot = getNodeRenewalSnapshot(
|
const renewalSnapshot = getNodeRenewalSnapshot(
|
||||||
node.expiryTime,
|
node.expiryTime,
|
||||||
@@ -141,7 +131,6 @@ const diffDays = Math.ceil(
|
|||||||
renewalSnapshot.nextDueTime,
|
renewalSnapshot.nextDueTime,
|
||||||
renewalSnapshot.state,
|
renewalSnapshot.state,
|
||||||
);
|
);
|
||||||
const tags = parseNodeTags(node.tags);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -177,19 +166,6 @@ const diffDays = Math.ceil(
|
|||||||
{node.remark.trim()}
|
{node.remark.trim()}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tags.length > 0 && (
|
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
|
||||||
{tags.map((tag) => (
|
|
||||||
<span
|
|
||||||
key={`${node.id}-${tag}`}
|
|
||||||
className="rounded-full bg-white/80 px-2 py-1 text-[11px] text-default-600 ring-1 ring-amber-200/80 dark:bg-white/5 dark:text-default-300 dark:ring-amber-500/20"
|
|
||||||
>
|
|
||||||
#{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ export interface DashboardNodeExpiryItem {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
tags?: string;
|
|
||||||
expiryTime?: number;
|
expiryTime?: number;
|
||||||
renewalCycle?: "month" | "quarter" | "year" | "";
|
renewalCycle?: "month" | "quarter" | "year" | "";
|
||||||
}
|
}
|
||||||
@@ -222,7 +221,6 @@ const normalizeNodeExpiryReminders = (items: NodeApiItem[]) => {
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name || "",
|
name: item.name || "",
|
||||||
remark: typeof item.remark === "string" ? item.remark : "",
|
remark: typeof item.remark === "string" ? item.remark : "",
|
||||||
tags: typeof item.tags === "string" ? item.tags : "",
|
|
||||||
renewalCycle: normalizeDashboardRenewalCycle(item.renewalCycle),
|
renewalCycle: normalizeDashboardRenewalCycle(item.renewalCycle),
|
||||||
expiryTime:
|
expiryTime:
|
||||||
typeof item.expiryTime === "number" && item.expiryTime > 0
|
typeof item.expiryTime === "number" && item.expiryTime > 0
|
||||||
|
|||||||
+208
-196
@@ -60,9 +60,9 @@ import {
|
|||||||
} from "@/pages/node/display";
|
} from "@/pages/node/display";
|
||||||
import { tryCopyInstallCommand } from "@/pages/node/install-command";
|
import { tryCopyInstallCommand } from "@/pages/node/install-command";
|
||||||
import {
|
import {
|
||||||
|
getNodeRenewalSnapshot,
|
||||||
formatNodeRenewalTime,
|
formatNodeRenewalTime,
|
||||||
getNodeRenewalCycleLabel,
|
getNodeRenewalCycleLabel,
|
||||||
getNodeRenewalSnapshot,
|
|
||||||
type NodeRenewalCycle,
|
type NodeRenewalCycle,
|
||||||
} from "@/pages/node/renewal";
|
} from "@/pages/node/renewal";
|
||||||
import { buildNodeSystemInfo } from "@/pages/node/system-info";
|
import { buildNodeSystemInfo } from "@/pages/node/system-info";
|
||||||
@@ -78,7 +78,6 @@ interface Node {
|
|||||||
inx?: number;
|
inx?: number;
|
||||||
name: string;
|
name: string;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
tags?: string;
|
|
||||||
expiryTime?: number;
|
expiryTime?: number;
|
||||||
renewalCycle?: NodeRenewalCycle;
|
renewalCycle?: NodeRenewalCycle;
|
||||||
ip: string;
|
ip: string;
|
||||||
@@ -116,7 +115,6 @@ interface NodeForm {
|
|||||||
id: number | null;
|
id: number | null;
|
||||||
name: string;
|
name: string;
|
||||||
remark: string;
|
remark: string;
|
||||||
tags: string;
|
|
||||||
expiryTime: number;
|
expiryTime: number;
|
||||||
renewalCycle: NodeRenewalCycle;
|
renewalCycle: NodeRenewalCycle;
|
||||||
serverHost: string;
|
serverHost: string;
|
||||||
@@ -200,15 +198,6 @@ const getNodeExpiryMeta = (timestamp?: number, cycle?: NodeRenewalCycle) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeNodeTags = (tags?: string): string[] => {
|
|
||||||
if (!tags) return [];
|
|
||||||
|
|
||||||
return tags
|
|
||||||
.split(",")
|
|
||||||
.map((tag) => tag.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mergeNodeRealtimeState = (
|
const mergeNodeRealtimeState = (
|
||||||
incomingNode: Node,
|
incomingNode: Node,
|
||||||
existingNode?: Node,
|
existingNode?: Node,
|
||||||
@@ -274,9 +263,10 @@ export default function NodePage() {
|
|||||||
"node-search-keyword",
|
"node-search-keyword",
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
const [nodeFilterMode, setNodeFilterMode] =
|
const [nodeFilterMode, setNodeFilterMode, resetNodeFilterMode] =
|
||||||
useLocalStorageState<NodeFilterMode>("node-expiry-filter-mode", "all");
|
useLocalStorageState<NodeFilterMode>("node-expiry-filter-mode", "all");
|
||||||
const [isSearchVisible, setIsSearchVisible] = useState(false);
|
const [isSearchVisible, setIsSearchVisible] = useState(false);
|
||||||
|
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
|
||||||
const [dialogVisible, setDialogVisible] = useState(false);
|
const [dialogVisible, setDialogVisible] = useState(false);
|
||||||
const [dialogTitle, setDialogTitle] = useState("");
|
const [dialogTitle, setDialogTitle] = useState("");
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
@@ -290,7 +280,6 @@ export default function NodePage() {
|
|||||||
id: null,
|
id: null,
|
||||||
name: "",
|
name: "",
|
||||||
remark: "",
|
remark: "",
|
||||||
tags: "",
|
|
||||||
expiryTime: 0,
|
expiryTime: 0,
|
||||||
renewalCycle: "",
|
renewalCycle: "",
|
||||||
serverHost: "",
|
serverHost: "",
|
||||||
@@ -675,7 +664,7 @@ export default function NodePage() {
|
|||||||
newErrors.name = "节点名称长度不能超过50位";
|
newErrors.name = "节点名称长度不能超过50位";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(form.renewalCycle && !form.expiryTime) ||
|
(form.renewalCycle && !form.expiryTime) ||
|
||||||
(!form.renewalCycle && form.expiryTime)
|
(!form.renewalCycle && form.expiryTime)
|
||||||
) {
|
) {
|
||||||
@@ -742,7 +731,6 @@ if (
|
|||||||
id: node.id,
|
id: node.id,
|
||||||
name: node.name,
|
name: node.name,
|
||||||
remark: node.remark || "",
|
remark: node.remark || "",
|
||||||
tags: node.tags || "",
|
|
||||||
expiryTime: node.expiryTime || 0,
|
expiryTime: node.expiryTime || 0,
|
||||||
renewalCycle: node.renewalCycle || "",
|
renewalCycle: node.renewalCycle || "",
|
||||||
serverHost: normalizedHost,
|
serverHost: normalizedHost,
|
||||||
@@ -981,7 +969,6 @@ if (
|
|||||||
const data = {
|
const data = {
|
||||||
...rest,
|
...rest,
|
||||||
remark: form.remark.trim(),
|
remark: form.remark.trim(),
|
||||||
tags: form.tags.trim(),
|
|
||||||
expiryTime: form.expiryTime,
|
expiryTime: form.expiryTime,
|
||||||
renewalCycle: form.renewalCycle,
|
renewalCycle: form.renewalCycle,
|
||||||
extraIPs: form.extraIPs,
|
extraIPs: form.extraIPs,
|
||||||
@@ -1006,7 +993,6 @@ if (
|
|||||||
...n,
|
...n,
|
||||||
name: form.name,
|
name: form.name,
|
||||||
remark: form.remark.trim(),
|
remark: form.remark.trim(),
|
||||||
tags: form.tags.trim(),
|
|
||||||
expiryTime: form.expiryTime,
|
expiryTime: form.expiryTime,
|
||||||
renewalCycle: form.renewalCycle,
|
renewalCycle: form.renewalCycle,
|
||||||
serverIp:
|
serverIp:
|
||||||
@@ -1046,7 +1032,6 @@ if (
|
|||||||
id: null,
|
id: null,
|
||||||
name: "",
|
name: "",
|
||||||
remark: "",
|
remark: "",
|
||||||
tags: "",
|
|
||||||
expiryTime: 0,
|
expiryTime: 0,
|
||||||
renewalCycle: "",
|
renewalCycle: "",
|
||||||
serverHost: "",
|
serverHost: "",
|
||||||
@@ -1207,14 +1192,13 @@ if (
|
|||||||
if (searchKeyword.trim()) {
|
if (searchKeyword.trim()) {
|
||||||
const lowerKeyword = searchKeyword.toLowerCase();
|
const lowerKeyword = searchKeyword.toLowerCase();
|
||||||
|
|
||||||
filteredNodes = filteredNodes.filter(
|
filteredNodes = filteredNodes.filter(
|
||||||
(n) =>
|
(n) =>
|
||||||
(n.name && n.name.toLowerCase().includes(lowerKeyword)) ||
|
(n.name && n.name.toLowerCase().includes(lowerKeyword)) ||
|
||||||
(n.remark && n.remark.toLowerCase().includes(lowerKeyword)) ||
|
(n.remark && n.remark.toLowerCase().includes(lowerKeyword)) ||
|
||||||
(n.tags && n.tags.toLowerCase().includes(lowerKeyword)) ||
|
(n.serverIp && n.serverIp.toLowerCase().includes(lowerKeyword)) ||
|
||||||
(n.serverIp && n.serverIp.toLowerCase().includes(lowerKeyword)) ||
|
(n.serverIpV4 && n.serverIpV4.toLowerCase().includes(lowerKeyword)) ||
|
||||||
(n.serverIpV4 && n.serverIpV4.toLowerCase().includes(lowerKeyword)) ||
|
(n.serverIpV6 && n.serverIpV6.toLowerCase().includes(lowerKeyword)),
|
||||||
(n.serverIpV6 && n.serverIpV6.toLowerCase().includes(lowerKeyword)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1288,7 +1272,7 @@ if (
|
|||||||
return (
|
return (
|
||||||
<AnimatedPage className="px-3 lg:px-6 py-8">
|
<AnimatedPage className="px-3 lg:px-6 py-8">
|
||||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between mb-6 gap-3">
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between mb-6 gap-3">
|
||||||
<div className="flex flex-1 flex-col gap-3 lg:flex-row lg:items-center">
|
<div className="flex-1 max-w-sm flex items-center gap-2">
|
||||||
<SearchBar
|
<SearchBar
|
||||||
isVisible={isSearchVisible}
|
isVisible={isSearchVisible}
|
||||||
placeholder="搜索名称、IP、备注或标签"
|
placeholder="搜索名称、IP、备注或标签"
|
||||||
@@ -1297,42 +1281,14 @@ if (
|
|||||||
onClose={() => setIsSearchVisible(false)}
|
onClose={() => setIsSearchVisible(false)}
|
||||||
onOpen={() => setIsSearchVisible(true)}
|
onOpen={() => setIsSearchVisible(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
|
||||||
className="w-full max-w-sm lg:w-56"
|
|
||||||
label="到期筛选"
|
|
||||||
selectedKeys={[nodeFilterMode]}
|
|
||||||
size="sm"
|
|
||||||
variant="bordered"
|
|
||||||
onSelectionChange={(keys) => {
|
|
||||||
const selected = Array.from(keys)[0] as
|
|
||||||
| NodeFilterMode
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
setNodeFilterMode(selected || "all");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectItem key="all" textValue="全部节点">
|
|
||||||
全部节点
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem key="expiringSoon" textValue="7天内到期">
|
|
||||||
7天内续费 ({nodeExpiryStats.expiringSoon})
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem key="expired" textValue="已过期">
|
|
||||||
已逾期 ({nodeExpiryStats.expired})
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem key="withExpiry" textValue="已设置到期时间">
|
|
||||||
已启用续费提醒 ({nodeExpiryStats.withExpiry})
|
|
||||||
</SelectItem>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-h-9 min-w-0 max-w-full overflow-x-auto touch-pan-x">
|
<div className="min-h-9 min-w-0 max-w-full overflow-x-auto touch-pan-x">
|
||||||
<div className="flex min-h-9 w-max min-w-full items-center justify-end gap-2 whitespace-nowrap [&>*]:shrink-0">
|
<div className="flex min-h-9 w-max min-w-full items-center justify-end gap-2 whitespace-nowrap sm:gap-3 [&>*]:shrink-0">
|
||||||
{selectMode ? (
|
{selectMode ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-sm text-default-600 shrink-0">
|
<span className="text-sm text-default-600 shrink-0">
|
||||||
已选 {selectedIds.size} 项
|
已选择 {selectedIds.size} 项
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -1380,6 +1336,39 @@ if (
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{/* 筛选按钮 */}
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
aria-label="筛选条件"
|
||||||
|
className={
|
||||||
|
nodeFilterMode !== "all"
|
||||||
|
? "bg-primary/20 text-primary relative"
|
||||||
|
: "text-default-600 relative"
|
||||||
|
}
|
||||||
|
color={nodeFilterMode !== "all" ? "primary" : "default"}
|
||||||
|
size="sm"
|
||||||
|
title="筛选条件"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => setIsFilterModalOpen(true)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{nodeFilterMode !== "all" && (
|
||||||
|
<span className="absolute top-1.5 right-1.5 flex h-1.5 w-1.5 rounded-full bg-primary" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-300 dark:hover:bg-sky-900/45"
|
className="bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-300 dark:hover:bg-sky-900/45"
|
||||||
color="default"
|
color="default"
|
||||||
@@ -1447,22 +1436,11 @@ if (
|
|||||||
key={node.id}
|
key={node.id}
|
||||||
className={`group shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 overflow-hidden h-full flex flex-col ${expiryMeta.accentClassName}`}
|
className={`group shadow-sm border border-divider hover:shadow-md transition-shadow duration-200 overflow-hidden h-full flex flex-col ${expiryMeta.accentClassName}`}
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-2 md:pb-2">
|
<CardHeader className="pb-3 md:pb-3">
|
||||||
<div className="flex justify-between items-start w-full">
|
<div className="flex justify-between items-start w-full gap-3">
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
<div className="flex items-start gap-2 flex-1 min-w-0">
|
||||||
{selectMode && (
|
|
||||||
<Checkbox
|
|
||||||
isSelected={selectedIds.has(node.id)}
|
|
||||||
onValueChange={() => toggleSelect(node.id)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<h3 className="font-semibold text-foreground truncate text-sm">
|
|
||||||
{node.name}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5 ml-2">
|
|
||||||
<div
|
<div
|
||||||
className="cursor-grab active:cursor-grabbing p-2 text-default-400 hover:text-default-600 transition-colors touch-manipulation opacity-100 sm:opacity-0 sm:group-hover:opacity-100"
|
className="cursor-grab active:cursor-grabbing p-2 -ml-2 -mt-1 text-default-400 hover:text-default-600 transition-colors touch-manipulation opacity-100 sm:opacity-0 sm:group-hover:opacity-100 flex-shrink-0"
|
||||||
{...listeners}
|
{...listeners}
|
||||||
style={{ touchAction: "none" }}
|
style={{ touchAction: "none" }}
|
||||||
title="拖拽排序"
|
title="拖拽排序"
|
||||||
@@ -1476,9 +1454,21 @@ if (
|
|||||||
<path d="M7 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 2zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 14zm6-8a2 2 0 1 1-.001-4.001A2 2 0 0 1 13 6zm0 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 14z" />
|
<path d="M7 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 2zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 14zm6-8a2 2 0 1 1-.001-4.001A2 2 0 0 1 13 6zm0 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 14z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
{selectMode && (
|
||||||
|
<Checkbox
|
||||||
|
className="mt-0.5"
|
||||||
|
isSelected={selectedIds.has(node.id)}
|
||||||
|
onValueChange={() => toggleSelect(node.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<h3 className="pt-0.5 font-semibold text-foreground truncate text-sm leading-5">
|
||||||
|
{node.name}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="ml-2 flex max-w-[58%] flex-wrap items-center justify-end gap-1.5 self-start">
|
||||||
{isRemoteNode && (
|
{isRemoteNode && (
|
||||||
<Chip
|
<Chip
|
||||||
className="text-xs"
|
className="text-[10px] h-5 px-1 flex-shrink-0"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
@@ -1494,7 +1484,7 @@ if (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
className="text-xs"
|
className="text-[10px] h-5 px-1"
|
||||||
color={connectionStatusMeta.color}
|
color={connectionStatusMeta.color}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
@@ -1503,6 +1493,19 @@ if (
|
|||||||
</Chip>
|
</Chip>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
{node.expiryTime &&
|
||||||
|
node.expiryTime > 0 &&
|
||||||
|
node.renewalCycle && (
|
||||||
|
<Chip
|
||||||
|
className="text-[10px] h-5 px-1 flex-shrink-0"
|
||||||
|
color={expiryMeta.tone}
|
||||||
|
size="sm"
|
||||||
|
title={`${formatNodeRenewalTime(expiryMeta.nextDueTime)} (${getNodeRenewalCycleLabel(node.renewalCycle)})`}
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
{expiryMeta.label}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -1513,61 +1516,11 @@ if (
|
|||||||
{getRemoteSyncErrorMessage(node.syncError)}
|
{getRemoteSyncErrorMessage(node.syncError)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{expiryMeta.isHighlighted && (
|
|
||||||
<div
|
|
||||||
className={`mb-3 flex items-center justify-between gap-3 rounded-md px-2 py-1.5 text-xs font-medium ${expiryMeta.bannerClassName}`}
|
|
||||||
>
|
|
||||||
<span>节点到期提醒</span>
|
|
||||||
<span>{expiryMeta.label}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* 基础信息 */}
|
{/* 基础信息 */}
|
||||||
<div className="space-y-2 mb-4">
|
<div className="space-y-2 mb-4">
|
||||||
{(node.remark?.trim() || node.tags?.trim()) && (
|
{node.expiryTime &&
|
||||||
<div className="rounded-lg border border-divider/80 bg-default-50/80 px-3 py-2">
|
|
||||||
{node.remark?.trim() && (
|
|
||||||
<div
|
|
||||||
className="text-xs leading-5 text-default-700 line-clamp-1 break-all"
|
|
||||||
title={node.remark.trim()}
|
|
||||||
>
|
|
||||||
{node.remark.trim()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{normalizeNodeTags(node.tags).length > 0 && (
|
|
||||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
||||||
{normalizeNodeTags(node.tags).map((tag) => (
|
|
||||||
<Chip
|
|
||||||
key={`${node.id}-${tag}`}
|
|
||||||
className="text-[11px]"
|
|
||||||
color="secondary"
|
|
||||||
size="sm"
|
|
||||||
variant="flat"
|
|
||||||
>
|
|
||||||
#{tag}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{node.expiryTime &&
|
|
||||||
node.expiryTime > 0 &&
|
node.expiryTime > 0 &&
|
||||||
node.renewalCycle && (
|
node.renewalCycle && <div className="hidden" />}
|
||||||
<div className="flex justify-between items-center text-sm">
|
|
||||||
<span className="text-default-600">
|
|
||||||
下次续费
|
|
||||||
</span>
|
|
||||||
<Chip
|
|
||||||
className="text-[11px]"
|
|
||||||
color={expiryMeta.tone}
|
|
||||||
size="sm"
|
|
||||||
title={`${formatNodeRenewalTime(expiryMeta.nextDueTime)} (${getNodeRenewalCycleLabel(node.renewalCycle)})`}
|
|
||||||
variant="flat"
|
|
||||||
>
|
|
||||||
{expiryMeta.label}
|
|
||||||
</Chip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex justify-between items-center text-sm min-w-0">
|
<div className="flex justify-between items-center text-sm min-w-0">
|
||||||
<span className="text-default-600 flex-shrink-0">
|
<span className="text-default-600 flex-shrink-0">
|
||||||
IP
|
IP
|
||||||
@@ -1764,73 +1717,83 @@ if (
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
<div className="mt-auto space-y-3">
|
||||||
<div className="space-y-1.5 mt-auto">
|
{node.remark?.trim() && (
|
||||||
{!isRemoteNode && (
|
<div className="rounded-md border border-divider/80 bg-default-50/80 px-2.5 py-2.5 text-xs leading-5 text-default-700 break-all">
|
||||||
<div className="grid grid-cols-3 gap-1.5">
|
<div title={node.remark.trim()}>
|
||||||
<Button
|
{node.remark.trim()}
|
||||||
className="min-h-8"
|
</div>
|
||||||
color="success"
|
|
||||||
isLoading={node.copyLoading}
|
|
||||||
size="sm"
|
|
||||||
variant="flat"
|
|
||||||
onPress={() => openInstallSelector(node)}
|
|
||||||
>
|
|
||||||
安装
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="min-h-8"
|
|
||||||
color="warning"
|
|
||||||
isDisabled={
|
|
||||||
node.connectionStatus !== "online"
|
|
||||||
}
|
|
||||||
isLoading={node.upgradeLoading}
|
|
||||||
size="sm"
|
|
||||||
variant="flat"
|
|
||||||
onPress={() =>
|
|
||||||
openUpgradeModal("single", node.id)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
升级
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="min-h-8"
|
|
||||||
color="secondary"
|
|
||||||
isDisabled={
|
|
||||||
node.connectionStatus !== "online"
|
|
||||||
}
|
|
||||||
isLoading={node.rollbackLoading}
|
|
||||||
size="sm"
|
|
||||||
variant="flat"
|
|
||||||
onPress={() => handleRollbackNode(node)}
|
|
||||||
>
|
|
||||||
回退
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
|
||||||
className={`grid gap-1.5 ${isRemoteNode ? "grid-cols-1" : "grid-cols-2"}`}
|
{/* 操作按钮 */}
|
||||||
>
|
<div className="space-y-1.5">
|
||||||
{!isRemoteNode && (
|
{!isRemoteNode && (
|
||||||
|
<div className="grid grid-cols-3 gap-1.5">
|
||||||
|
<Button
|
||||||
|
className="min-h-8"
|
||||||
|
color="success"
|
||||||
|
isLoading={node.copyLoading}
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => openInstallSelector(node)}
|
||||||
|
>
|
||||||
|
安装
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="min-h-8"
|
||||||
|
color="warning"
|
||||||
|
isDisabled={
|
||||||
|
node.connectionStatus !== "online"
|
||||||
|
}
|
||||||
|
isLoading={node.upgradeLoading}
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() =>
|
||||||
|
openUpgradeModal("single", node.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
升级
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="min-h-8"
|
||||||
|
color="secondary"
|
||||||
|
isDisabled={
|
||||||
|
node.connectionStatus !== "online"
|
||||||
|
}
|
||||||
|
isLoading={node.rollbackLoading}
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => handleRollbackNode(node)}
|
||||||
|
>
|
||||||
|
回退
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`grid gap-1.5 ${isRemoteNode ? "grid-cols-1" : "grid-cols-2"}`}
|
||||||
|
>
|
||||||
|
{!isRemoteNode && (
|
||||||
|
<Button
|
||||||
|
className="min-h-8"
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
|
onPress={() => handleEdit(node)}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
className="min-h-8"
|
className="min-h-8"
|
||||||
color="primary"
|
color="danger"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
onPress={() => handleEdit(node)}
|
onPress={() => handleDelete(node)}
|
||||||
>
|
>
|
||||||
编辑
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
<Button
|
|
||||||
className="min-h-8"
|
|
||||||
color="danger"
|
|
||||||
size="sm"
|
|
||||||
variant="flat"
|
|
||||||
onPress={() => handleDelete(node)}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
@@ -1883,17 +1846,6 @@ if (
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Input
|
|
||||||
description="多个标签用逗号分隔,可用于搜索过滤"
|
|
||||||
label="标签"
|
|
||||||
placeholder="例如: 年付,日本中转,测试机"
|
|
||||||
value={form.tags}
|
|
||||||
variant="bordered"
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm((prev) => ({ ...prev, tags: e.target.value }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label="续费周期"
|
label="续费周期"
|
||||||
placeholder="选择续费周期"
|
placeholder="选择续费周期"
|
||||||
@@ -2559,6 +2511,66 @@ if (
|
|||||||
)}
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={isFilterModalOpen}
|
||||||
|
placement="center"
|
||||||
|
size="md"
|
||||||
|
onOpenChange={setIsFilterModalOpen}
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
{(onClose) => (
|
||||||
|
<>
|
||||||
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
|
筛选条件
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<div className="flex flex-col gap-4 py-2">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p className="text-sm font-medium">按到期状态筛选</p>
|
||||||
|
<Select
|
||||||
|
aria-label="按到期状态筛选"
|
||||||
|
className="w-full"
|
||||||
|
selectedKeys={[nodeFilterMode]}
|
||||||
|
variant="bordered"
|
||||||
|
onSelectionChange={(keys) => {
|
||||||
|
const selected = Array.from(keys)[0] as
|
||||||
|
| NodeFilterMode
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
setNodeFilterMode(selected || "all");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectItem key="all">全部节点</SelectItem>
|
||||||
|
<SelectItem key="expiringSoon">
|
||||||
|
7天内续费 ({nodeExpiryStats.expiringSoon})
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem key="expired">
|
||||||
|
已逾期 ({nodeExpiryStats.expired})
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem key="withExpiry">
|
||||||
|
已启用续费提醒 ({nodeExpiryStats.withExpiry})
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
color="default"
|
||||||
|
variant="flat"
|
||||||
|
onPress={resetNodeFilterMode}
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" onPress={onClose}>
|
||||||
|
完成
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user