8b55b9cf55
Co-authored-by: Cursor <cursoragent@cursor.com>
171 lines
5.4 KiB
Vue
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>
|