feat: 新增游客订阅链接与节点拖动排序

游客链接:可建多个带独立 token 的订阅,按节点名勾选白名单
(抗重新抓取),/surge|/clash|/ssr 复用同一路径回落匹配 guest
token,三个 generator 接收可选 whitelist 过滤生成。

节点排序:fetched_nodes 新增 sort_order,重抓时按名保留顺序,
选择/节点面板支持原生拖拽排序,输出按此顺序排列。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 23:43:28 +08:00
parent c3d8e1a961
commit fb7a30489e
14 changed files with 737 additions and 44 deletions

View File

@@ -6,6 +6,7 @@ export default function NodeSelector() {
const [selectedSub, setSelectedSub] = useState<number | null>(null);
const [nodeList, setNodeList] = useState<any[]>([]);
const [regexInput, setRegexInput] = useState('');
const [dragIndex, setDragIndex] = useState<number | null>(null);
useEffect(() => {
subsApi.list().then(data => {
@@ -52,6 +53,21 @@ export default function NodeSelector() {
}
};
const handleDrop = async (targetIndex: number) => {
if (dragIndex === null || dragIndex === targetIndex) { setDragIndex(null); return; }
const reordered = [...nodeList];
const [moved] = reordered.splice(dragIndex, 1);
reordered.splice(targetIndex, 0, moved);
setNodeList(reordered);
setDragIndex(null);
try {
await nodesApi.fetchedReorder(reordered.map(n => n.id));
} catch (err) {
console.error(err);
if (selectedSub) subsApi.nodes(selectedSub).then(setNodeList);
}
};
const enabledCount = nodeList.filter(n => n.enabled).length;
return (
@@ -129,9 +145,13 @@ export default function NodeSelector() {
)}
{/* Node list */}
<p style={{ fontSize: 11, color: 'var(--text-muted)', marginBottom: 8 }}>
</p>
<table>
<thead>
<tr>
<th style={{ width: 32 }}></th>
<th style={{ width: 50 }}></th>
<th></th>
<th style={{ width: 80 }}></th>
@@ -140,10 +160,25 @@ export default function NodeSelector() {
</tr>
</thead>
<tbody>
{nodeList.map(node => (
<tr key={node.id} style={{
opacity: node.enabled ? 1 : 0.5,
}}>
{nodeList.map((node, idx) => (
<tr
key={node.id}
onDragOver={e => e.preventDefault()}
onDrop={() => handleDrop(idx)}
style={{
opacity: node.enabled ? 1 : 0.5,
background: dragIndex === idx ? 'var(--bg-active)' : undefined,
}}
>
<td
draggable
onDragStart={() => setDragIndex(idx)}
onDragEnd={() => setDragIndex(null)}
style={{ cursor: 'grab', textAlign: 'center', color: 'var(--text-muted)' }}
title="拖动排序"
>
</td>
<td>
<input
type="checkbox"
@@ -189,7 +224,7 @@ export default function NodeSelector() {
))}
{nodeList.length === 0 && (
<tr>
<td colSpan={5} style={{ textAlign: 'center', color: 'var(--text-muted)', padding: 40 }}>
<td colSpan={6} style={{ textAlign: 'center', color: 'var(--text-muted)', padding: 40 }}>
{subs.length === 0
? '请先添加订阅源'
: '请先抓取订阅源节点'