Files
HyperNode-AI/frontend/src/views/AdminOverview.vue
T

171 lines
5.4 KiB
Vue

<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import Shell from "../components/Shell.vue";
import { api } from "../api";
import { money, pct, statusText, when } from "../format";
const summary = ref(null);
const users = ref([]);
const nodes = ref([]);
const withdrawals = ref([]);
const hashes = ref({});
const error = ref("");
const notice = ref("");
let timer = 0;
async function load() {
const [nextSummary, nextUsers, nextNodes, nextCash] = await Promise.all([
api("/admin/summary"),
api("/admin/users"),
api("/admin/nodes"),
api("/admin/withdrawals"),
]);
summary.value = nextSummary;
users.value = nextUsers;
nodes.value = nextNodes;
withdrawals.value = nextCash;
}
async function confirm(row) {
const txHash = (hashes.value[row.id] || "").trim();
if (!txHash) {
error.value = "请先填写链上哈希";
return;
}
error.value = "";
notice.value = "";
try {
await api(`/admin/withdrawals/${row.id}/confirm`, {
method: "POST",
body: JSON.stringify({ tx_hash: txHash }),
});
notice.value = `提现 #${row.id} 已打款`;
await load();
} catch (err) {
error.value = err.message;
}
}
async function reject(row) {
error.value = "";
notice.value = "";
try {
await api(`/admin/withdrawals/${row.id}/reject`, {
method: "POST",
body: JSON.stringify({ note: "管理后台拒绝" }),
});
notice.value = `提现 #${row.id} 已拒绝`;
await load();
} catch (err) {
error.value = err.message;
}
}
onMounted(async () => {
try {
await load();
} catch (err) {
error.value = err.message;
}
timer = window.setInterval(() => {
load().catch((err) => {
error.value = err.message;
});
}, 10000);
});
onUnmounted(() => window.clearInterval(timer));
</script>
<template>
<Shell eyebrow="管理后台" title="全网概览">
<p v-if="error" class="form-error banner">{{ error }}</p>
<p v-if="notice" class="banner ok">{{ notice }}</p>
<section v-if="summary" class="metrics">
<article class="metric">
<span>用户</span>
<strong>{{ summary.users }}</strong>
<em>已注册钱包</em>
</article>
<article class="metric">
<span>节点</span>
<strong>{{ summary.nodes }}</strong>
<em>在线 {{ summary.online }} · 离线 {{ summary.offline }}</em>
</article>
<article class="metric">
<span>总产出</span>
<strong>{{ money(summary.settled_points) }}</strong>
<em>授权 {{ money(summary.authorized_amount) }}</em>
</article>
<article class="metric">
<span>待审核</span>
<strong>{{ money(summary.pending_withdrawals) }}</strong>
<em>占用中的提现</em>
</article>
</section>
<h2 class="section-title">节点监控</h2>
<section class="board">
<article v-for="node in nodes" :key="node.sn" class="plate">
<header>
<strong>{{ node.sn }}</strong>
<span class="pill" :class="node.status"><i />{{ statusText[node.status] }}</span>
</header>
<p class="wallet">{{ node.wallet_address }}</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>
</dl>
</article>
<p v-if="!nodes.length" class="empty">还没有节点。</p>
</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></tr>
</thead>
<tbody>
<tr v-for="row in withdrawals" :key="row.id">
<td>#{{ row.id }}</td>
<td class="mono">{{ row.wallet_address }}</td>
<td>{{ money(row.amount) }} {{ row.asset_symbol }}</td>
<td><span class="pill" :class="row.status">{{ statusText[row.status] }}</span></td>
<td>
<input v-if="row.status === 'pending'" v-model="hashes[row.id]" placeholder="交易哈希" />
<span v-else class="mono">{{ row.tx_hash || "—" }}</span>
</td>
<td class="actions" v-if="row.status === 'pending'">
<button class="gold" type="button" @click="confirm(row)">打款</button>
<button class="texty" type="button" @click="reject(row)">拒绝</button>
</td>
<td v-else></td>
</tr>
<tr v-if="!withdrawals.length"><td colspan="6" class="empty">还没有提现申请。</td></tr>
</tbody>
</table>
</div>
<h2 class="section-title">用户</h2>
<div class="table-wrap">
<table>
<thead>
<tr><th>钱包</th><th>用户名</th><th>角色</th><th>节点</th><th>可用积分</th><th>注册</th></tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td class="mono">{{ user.wallet_address }}</td>
<td>{{ user.username || "—" }}</td>
<td>{{ statusText[user.role] }}</td>
<td>{{ user.node_count }}</td>
<td>{{ money(user.available_points) }}</td>
<td>{{ when(user.created_at) }}</td>
</tr>
</tbody>
</table>
</div>
</Shell>
</template>