按显卡世代和创世编号结算回本,机房批量接入需审核网段并用硬件指纹防克隆。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -193,7 +193,36 @@ input:focus { border-color: var(--gold); }
|
||||
.pill.online, .pill.completed { color: var(--online); }
|
||||
.pill.offline, .pill.rejected, .pill.failed { color: var(--offline); }
|
||||
.pill.pending { color: var(--gold); }
|
||||
.genesis-tag {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
margin: 12px 0 0;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(224, 192, 122, 0.55);
|
||||
border-radius: 999px;
|
||||
color: #e0c07a;
|
||||
background: #08090d;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.network-line { margin: 18px 0 0; color: #e0c07a; font-size: 14px; letter-spacing: 0.04em; }
|
||||
.whitelist-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 12px; }
|
||||
.toggle { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-size: 12px; letter-spacing: 0.06em; cursor: pointer; }
|
||||
.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
|
||||
.toggle span { position: relative; width: 42px; height: 24px; border-radius: 999px; border: 1px solid rgba(224, 192, 122, 0.4); background: #08090d; }
|
||||
.toggle span::after { content: ""; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #8a8478; transition: transform 0.15s ease, background 0.15s ease; }
|
||||
.toggle input:checked + span { border-color: #e0c07a; }
|
||||
.toggle input:checked + span::after { transform: translateX(18px); background: #e0c07a; }
|
||||
.toggle input:focus-visible + span { outline: 1px solid #e0c07a; }
|
||||
.whitelist-panel { margin-top: 8px; }
|
||||
.ip-form { display: grid; grid-template-columns: 1.2fr 1fr auto; gap: 12px; align-items: end; }
|
||||
.ip-form button { height: 42px; }
|
||||
@media (max-width: 720px) { .ip-form { grid-template-columns: 1fr; } }
|
||||
.forms { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; align-items: start; }
|
||||
.span-2 { grid-column: 1 / -1; }
|
||||
textarea, select { width: 100%; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--line); background: rgba(8, 9, 13, 0.55); color: inherit; font: inherit; }
|
||||
textarea:focus, select:focus { border-color: var(--gold); outline: none; }
|
||||
textarea { min-height: 96px; resize: vertical; }
|
||||
.panel { display: grid; gap: 12px; padding: 22px; }
|
||||
.panel h2 { font-family: var(--serif); font-size: 28px; font-weight: 560; }
|
||||
.table-wrap { overflow: auto; margin-top: 8px; }
|
||||
|
||||
@@ -10,23 +10,30 @@ const nodes = ref([]);
|
||||
const withdrawals = ref([]);
|
||||
const orders = ref([]);
|
||||
const hashes = ref({});
|
||||
const whitelist = ref([]);
|
||||
const applications = ref([]);
|
||||
const ipForm = ref({ ip_address: "", note: "" });
|
||||
const error = ref("");
|
||||
const notice = ref("");
|
||||
let timer = 0;
|
||||
|
||||
async function load() {
|
||||
const [nextSummary, nextUsers, nextNodes, nextCash, nextOrders] = await Promise.all([
|
||||
const [nextSummary, nextUsers, nextNodes, nextCash, nextOrders, nextIps, nextApps] = await Promise.all([
|
||||
api("/admin/summary"),
|
||||
api("/admin/users"),
|
||||
api("/admin/nodes"),
|
||||
api("/admin/withdrawals"),
|
||||
api("/admin/orders"),
|
||||
api("/admin/ip-whitelist"),
|
||||
api("/admin/datacenter-applications"),
|
||||
]);
|
||||
summary.value = nextSummary;
|
||||
users.value = nextUsers;
|
||||
nodes.value = nextNodes;
|
||||
withdrawals.value = nextCash;
|
||||
orders.value = nextOrders;
|
||||
whitelist.value = nextIps;
|
||||
applications.value = nextApps;
|
||||
}
|
||||
|
||||
async function approveOrder(row) {
|
||||
@@ -34,7 +41,7 @@ async function approveOrder(row) {
|
||||
notice.value = "";
|
||||
try {
|
||||
const result = await api(`/admin/orders/${row.id}/approve`, { method: "POST" });
|
||||
notice.value = `订单 #${row.id} 已开通,节点 ${result.sn}`;
|
||||
notice.value = `订单 #${row.id} 已开通,节点 ${result.display_node_id || result.sn}`;
|
||||
await load();
|
||||
} catch (err) {
|
||||
error.value = err.message;
|
||||
@@ -76,6 +83,82 @@ async function confirm(row) {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleWhitelist(node) {
|
||||
error.value = "";
|
||||
notice.value = "";
|
||||
const next = !node.is_whitelist;
|
||||
try {
|
||||
await api(`/admin/nodes/${encodeURIComponent(node.sn)}/whitelist`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ is_whitelist: next }),
|
||||
});
|
||||
notice.value = next
|
||||
? `节点 ${node.display_node_id || node.sn} 已加入白名单`
|
||||
: `节点 ${node.display_node_id || node.sn} 已取消白名单`;
|
||||
await load();
|
||||
} catch (err) {
|
||||
error.value = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function addIp() {
|
||||
error.value = "";
|
||||
notice.value = "";
|
||||
try {
|
||||
await api("/admin/ip-whitelist", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
ip_address: ipForm.value.ip_address.trim(),
|
||||
note: ipForm.value.note.trim(),
|
||||
}),
|
||||
});
|
||||
ipForm.value = { ip_address: "", note: "" };
|
||||
notice.value = "白名单 IP 已添加";
|
||||
await load();
|
||||
} catch (err) {
|
||||
error.value = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function approveDatacenter(row) {
|
||||
error.value = "";
|
||||
notice.value = "";
|
||||
try {
|
||||
await api(`/admin/datacenter-applications/${row.id}/approve`, { method: "POST" });
|
||||
notice.value = `机房申请 #${row.id} 已通过,网段 ${row.subnet_ip} 已加入白名单`;
|
||||
await load();
|
||||
} catch (err) {
|
||||
error.value = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectDatacenter(row) {
|
||||
error.value = "";
|
||||
notice.value = "";
|
||||
try {
|
||||
await api(`/admin/datacenter-applications/${row.id}/reject`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ note: "未达到机房门槛" }),
|
||||
});
|
||||
notice.value = `机房申请 #${row.id} 已拒绝`;
|
||||
await load();
|
||||
} catch (err) {
|
||||
error.value = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeIp(row) {
|
||||
error.value = "";
|
||||
notice.value = "";
|
||||
try {
|
||||
await api(`/admin/ip-whitelist/${row.id}`, { method: "DELETE" });
|
||||
notice.value = `${row.ip_address} 已移出白名单`;
|
||||
await load();
|
||||
} catch (err) {
|
||||
error.value = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function reject(row) {
|
||||
error.value = "";
|
||||
notice.value = "";
|
||||
@@ -118,7 +201,7 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
<em>已注册钱包</em>
|
||||
</article>
|
||||
<article class="metric">
|
||||
<span>节点</span>
|
||||
<span>全网节点</span>
|
||||
<strong>{{ summary.nodes }}</strong>
|
||||
<em>在线 {{ summary.online }} · 离线 {{ summary.offline }}</em>
|
||||
</article>
|
||||
@@ -147,7 +230,7 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
<td>{{ money(row.amount) }} {{ row.asset_symbol }}</td>
|
||||
<td class="mono">{{ row.tx_id }}</td>
|
||||
<td><span class="pill" :class="row.status">{{ statusText[row.status] }}</span></td>
|
||||
<td>{{ row.sn || "—" }}</td>
|
||||
<td>{{ row.display_node_id || row.sn || "—" }}</td>
|
||||
<td class="actions" v-if="row.status === 'pending'">
|
||||
<button class="gold" type="button" @click="approveOrder(row)">通过</button>
|
||||
<button class="texty" type="button" @click="rejectOrder(row)">拒绝</button>
|
||||
@@ -163,19 +246,84 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
<section class="board">
|
||||
<article v-for="node in nodes" :key="node.sn" class="plate">
|
||||
<header>
|
||||
<strong>{{ node.sn }}</strong>
|
||||
<strong>{{ node.display_node_id || node.sn }}</strong>
|
||||
<span class="pill" :class="node.status"><i />{{ statusText[node.status] }}</span>
|
||||
</header>
|
||||
<div class="whitelist-row">
|
||||
<span v-if="node.is_whitelist" class="pill pending">白名单</span>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" :checked="node.is_whitelist" @change="toggleWhitelist(node)" />
|
||||
<span></span>
|
||||
白名单
|
||||
</label>
|
||||
</div>
|
||||
<p v-if="node.genesis" class="genesis-tag">[Genesis Special Node - 30 Days Recovery]</p>
|
||||
<p v-if="node.genesis" class="sub">创世特权节点 - 30天回本</p>
|
||||
<p class="wallet">{{ node.wallet_address }}</p>
|
||||
<p class="sub">{{ node.sn }}</p>
|
||||
<p class="rate"><b :style="{ width: pct(node.online_rate) }" />{{ pct(node.online_rate) }} 今日在线率</p>
|
||||
<dl>
|
||||
<div><dt>今日积分</dt><dd>{{ money(node.today_points) }}</dd></div>
|
||||
<div><dt>心跳</dt><dd>{{ when(node.last_heartbeat_at) }}</dd></div>
|
||||
<div><dt>绑定 IP</dt><dd>{{ node.client_ip || "—" }}</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
<p v-if="!nodes.length" class="empty">还没有节点。</p>
|
||||
</section>
|
||||
|
||||
<h2 class="section-title">IP 白名单设置</h2>
|
||||
<section class="panel whitelist-panel">
|
||||
<p class="sub">可以填写单 IP,或整个机房网段,例如 203.0.113.0/24。网段内的地址都可以挂载多台设备。</p>
|
||||
<form class="ip-form" @submit.prevent="addIp">
|
||||
<label>豁免 IP / 网段<input v-model="ipForm.ip_address" required placeholder="203.0.113.0/24" /></label>
|
||||
<label>备注<input v-model="ipForm.note" placeholder="自营机房出口" /></label>
|
||||
<button class="gold" type="submit">添加</button>
|
||||
</form>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>IP</th><th>备注</th><th>添加时间</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in whitelist" :key="row.id">
|
||||
<td class="mono">{{ row.ip_address }}</td>
|
||||
<td>{{ row.note || "—" }}</td>
|
||||
<td>{{ when(row.created_at) }}</td>
|
||||
<td class="actions"><button class="texty" type="button" @click="removeIp(row)">删除</button></td>
|
||||
</tr>
|
||||
<tr v-if="!whitelist.length"><td colspan="4" class="empty">还没有豁免 IP。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h2 class="section-title">机房大客户申请</h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>单号</th><th>钱包</th><th>联系方式</th><th>节点数</th><th>网段</th><th>显卡</th><th>证明</th><th>状态</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in applications" :key="row.id">
|
||||
<td>#{{ row.id }}</td>
|
||||
<td class="mono">{{ row.wallet_address }}</td>
|
||||
<td>{{ row.contact }}</td>
|
||||
<td>{{ row.node_count }}</td>
|
||||
<td class="mono">{{ row.subnet_ip }}</td>
|
||||
<td>{{ row.gpu_generation }} 系</td>
|
||||
<td>{{ row.hardware_proof }}</td>
|
||||
<td><span class="pill" :class="row.status">{{ statusText[row.status] }}</span></td>
|
||||
<td class="actions" v-if="row.status === 'pending'">
|
||||
<button class="gold" type="button" @click="approveDatacenter(row)">通过</button>
|
||||
<button class="texty" type="button" @click="rejectDatacenter(row)">拒绝</button>
|
||||
</td>
|
||||
<td v-else></td>
|
||||
</tr>
|
||||
<tr v-if="!applications.length"><td colspan="9" class="empty">还没有机房申请。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">提现审核</h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
|
||||
@@ -9,6 +9,13 @@ const error = ref("");
|
||||
const notice = ref("");
|
||||
const txId = ref("");
|
||||
const cashForm = ref({ amount: "", asset_symbol: "POINT" });
|
||||
const datacenterForm = ref({
|
||||
contact: "",
|
||||
node_count: "5",
|
||||
subnet_ip: "",
|
||||
hardware_proof: "",
|
||||
gpu_generation: "40",
|
||||
});
|
||||
let timer = 0;
|
||||
|
||||
async function load() {
|
||||
@@ -27,6 +34,17 @@ async function run(work, done) {
|
||||
}
|
||||
}
|
||||
|
||||
function gpuText(node) {
|
||||
if (!node.gpu_model) return "未上报";
|
||||
return node.vram_gb ? `${node.gpu_model} · ${node.vram_gb} GB` : node.gpu_model;
|
||||
}
|
||||
|
||||
function generationText(node) {
|
||||
if (node.gpu_generation === "40") return "40系及以上";
|
||||
if (node.gpu_generation === "30") return "30系";
|
||||
return "10系及以下";
|
||||
}
|
||||
|
||||
function placeOrder() {
|
||||
return run(async () => {
|
||||
await api("/me/orders", { method: "POST", body: JSON.stringify({ tx_id: txId.value.trim() }) });
|
||||
@@ -48,6 +66,23 @@ function downloadClient(node) {
|
||||
return run(() => download(`/me/nodes/${encodeURIComponent(node.sn)}/client`, `hypernode-${node.sn}.py`), "客户端已开始下载");
|
||||
}
|
||||
|
||||
function applyDatacenter() {
|
||||
return run(async () => {
|
||||
await api("/me/datacenter-applications", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
contact: datacenterForm.value.contact.trim(),
|
||||
node_count: Number(datacenterForm.value.node_count),
|
||||
subnet_ip: datacenterForm.value.subnet_ip.trim(),
|
||||
hardware_proof: datacenterForm.value.hardware_proof.trim(),
|
||||
gpu_generation: datacenterForm.value.gpu_generation,
|
||||
}),
|
||||
});
|
||||
datacenterForm.value.subnet_ip = "";
|
||||
datacenterForm.value.hardware_proof = "";
|
||||
}, "机房申请已提交,等待审核");
|
||||
}
|
||||
|
||||
function requestCash() {
|
||||
return run(async () => {
|
||||
await api("/me/withdrawals", {
|
||||
@@ -93,9 +128,9 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
<em>按当前在线实时估算</em>
|
||||
</article>
|
||||
<article class="metric">
|
||||
<span>节点</span>
|
||||
<strong>{{ board.nodes.length }}</strong>
|
||||
<em>{{ board.nodes.filter((node) => node.status === "online").length }} 台在线</em>
|
||||
<span>全网节点</span>
|
||||
<strong>{{ board.network_nodes }}</strong>
|
||||
<em>在线 {{ board.network_online }}</em>
|
||||
</article>
|
||||
<p class="fine">* 算力积分兑换汇率:1 积分 = 0.8 ~ 1.0 USDT(根据全网算力动态浮动,当前基准约 0.95 USDT)</p>
|
||||
</section>
|
||||
@@ -108,7 +143,7 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
</header>
|
||||
<p class="wallet">{{ money(order.amount) }} {{ order.asset_symbol }} · {{ order.tx_id }}</p>
|
||||
<p v-if="order.status === 'pending'" class="sub">审核通过前不能复制 Token,也不能下载客户端。</p>
|
||||
<p v-else-if="order.sn" class="sub">节点 {{ order.sn }}</p>
|
||||
<p v-else-if="order.display_node_id" class="sub">节点 {{ order.display_node_id }}</p>
|
||||
<div v-if="order.status !== 'approved'" class="actions">
|
||||
<button class="gold" type="button" disabled>复制 Token</button>
|
||||
<button class="texty" type="button" disabled>下载客户端</button>
|
||||
@@ -116,14 +151,21 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
</article>
|
||||
<article v-for="node in board?.nodes || []" :key="node.sn" class="plate">
|
||||
<header>
|
||||
<strong>{{ node.sn }}</strong>
|
||||
<strong>{{ node.display_node_id || node.sn }}</strong>
|
||||
<span class="pill" :class="node.activated ? node.status : 'pending'"><i />{{ node.activated ? statusText[node.status] : "待审核" }}</span>
|
||||
</header>
|
||||
<p v-if="node.genesis" class="genesis-tag">[Genesis Special Node - 30 Days Recovery]</p>
|
||||
<p v-if="node.genesis" class="sub">创世特权节点 - 30天回本</p>
|
||||
<p class="rate"><b :style="{ width: pct(node.online_rate) }" />{{ pct(node.online_rate) }} 今日在线</p>
|
||||
<dl>
|
||||
<div><dt>购买金额</dt><dd>{{ money(node.purchase_amount) }}</dd></div>
|
||||
<div><dt>今日积分</dt><dd>{{ money(node.today_points) }}</dd></div>
|
||||
<div><dt>最近心跳</dt><dd>{{ when(node.last_heartbeat_at) }}</dd></div>
|
||||
<div><dt>显卡</dt><dd>{{ gpuText(node) }}</dd></div>
|
||||
<div><dt>硬件权重</dt><dd>{{ node.hardware_weight }}</dd></div>
|
||||
<div><dt>显卡世代</dt><dd>{{ generationText(node) }}</dd></div>
|
||||
<div><dt>回本周期</dt><dd>{{ node.recovery_days }} 天</dd></div>
|
||||
<div><dt>待返还质押</dt><dd>{{ money(node.recoverable_stake_amount || 0) }}</dd></div>
|
||||
</dl>
|
||||
<label>挂机 Token<input class="token-box" :value="node.activated ? node.node_token : '审核通过后发放'" readonly /></label>
|
||||
<div class="actions">
|
||||
@@ -154,6 +196,27 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
<form class="panel span-2" @submit.prevent="applyDatacenter">
|
||||
<h2>机房白名单申请</h2>
|
||||
<p class="sub">同一公网出口批量接入,需至少 5 台且全部为 30 系或 40 系显卡。审核通过后,该网段内硬件指纹不同的机器可以一起挂载。</p>
|
||||
<label>联系方式<input v-model="datacenterForm.contact" required minlength="4" placeholder="Telegram / 邮箱 / 手机" /></label>
|
||||
<label>预估节点数<input v-model="datacenterForm.node_count" required type="number" min="5" placeholder="5" /></label>
|
||||
<label>出口网段<input v-model="datacenterForm.subnet_ip" required placeholder="203.0.113.0/24" /></label>
|
||||
<label>显卡世代
|
||||
<select v-model="datacenterForm.gpu_generation">
|
||||
<option value="40">40 系及以上</option>
|
||||
<option value="30">30 系</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>硬件配置证明<textarea v-model="datacenterForm.hardware_proof" required minlength="4" placeholder="例如 8 台 RTX 4090,附采购或机房照片说明" /></label>
|
||||
<button class="gold" type="submit">提交机房申请</button>
|
||||
<ul class="mini">
|
||||
<li v-for="row in board?.datacenter_applications || []" :key="row.id">
|
||||
<span>#{{ row.id }} · {{ row.subnet_ip }} · {{ row.node_count }} 台</span>
|
||||
<span class="pill" :class="row.status">{{ statusText[row.status] }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</section>
|
||||
</Shell>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { api, saveSession } from "../api";
|
||||
|
||||
@@ -15,6 +15,18 @@ const form = ref({
|
||||
confirm: "",
|
||||
});
|
||||
const termsOpen = ref(false);
|
||||
const networkNodes = ref(null);
|
||||
const networkOnline = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await api("/network");
|
||||
networkNodes.value = data.nodes;
|
||||
networkOnline.value = data.online;
|
||||
} catch {
|
||||
networkNodes.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function submit() {
|
||||
error.value = "";
|
||||
@@ -68,6 +80,7 @@ function register() {
|
||||
<p class="eyebrow">HyperNode</p>
|
||||
<h1>连接全球 DePIN 分布式算力网络</h1>
|
||||
<p>基于去中心化节点的推理验证与任务调度平台。连接您的算力节点,实时监控质押释放与算力收益。</p>
|
||||
<p v-if="networkNodes !== null" class="network-line">全网节点 {{ networkNodes }} · 在线 {{ networkOnline }}</p>
|
||||
</section>
|
||||
<form class="gate-card" @submit.prevent="submit">
|
||||
<div class="switch">
|
||||
|
||||
Reference in New Issue
Block a user