基本功能已初步完善
Some checks failed
build / build (api, amd64, linux) (push) Has been cancelled
build / build (api, arm64, linux) (push) Has been cancelled
build / build (api.exe, amd64, windows) (push) Has been cancelled

This commit is contained in:
CN-JS-HuiBai
2026-04-17 20:41:47 +08:00
parent 25fd919477
commit b3435e5ef8
34 changed files with 3495 additions and 429 deletions

View File

@@ -11,7 +11,7 @@
const state = {
token: readToken(),
user: null,
route: normalizeRoute(readRoute()),
route: "overview",
busy: false,
message: "",
messageType: "info",
@@ -38,6 +38,7 @@
tickets: { list: [], pagination: null },
realname: { list: [], pagination: null },
devices: { list: [], pagination: null },
ipv6: { list: [], pagination: null },
expandedNodes: new Set()
};
@@ -54,9 +55,12 @@
"ticket-manage": { title: "工单中心", description: "查看用户工单和处理状态。" },
realname: { title: "实名认证", description: "审核实名记录和同步状态。" },
"user-online-devices": { title: "在线设备", description: "查看用户在线 IP 和设备分布。" },
"user-ipv6-subscription": { title: "IPv6 子账号", description: "管理 IPv6 阴影账号与密码同步。" },
"system-config": { title: "系统设置", description: "编辑站点、订阅和安全参数。" }
};
state.route = normalizeRoute(readRoute());
boot();
async function boot() {
@@ -146,7 +150,7 @@
state.plans = toArray(unwrap(plans));
state.groups = toArray(unwrap(groups));
} else if (state.route === "order-manage") {
const payload = unwrap(await request(`${cfg.api.orders}?page=${page}&per_page=20`));
const payload = await request(`${cfg.api.orders}?page=${page}&per_page=20`);
state.orders = normalizeListPayload(payload);
} else if (state.route === "coupon-manage") {
state.coupons = toArray(unwrap(await request(cfg.api.coupons)));
@@ -156,18 +160,21 @@
request(cfg.api.plans),
request(cfg.api.serverGroups)
]);
state.users = normalizeListPayload(unwrap(users));
state.users = normalizeListPayload(users);
state.plans = toArray(unwrap(plans));
state.groups = toArray(unwrap(groups));
} else if (state.route === "ticket-manage") {
const payload = unwrap(await request(`${cfg.api.tickets}?page=${page}&per_page=20`));
const payload = await request(`${cfg.api.tickets}?page=${page}&per_page=20`);
state.tickets = normalizeListPayload(payload);
} else if (state.route === "realname") {
const payload = unwrap(await request(`${cfg.api.realnameBase}/records?page=${page}&per_page=20`));
const payload = await request(`${cfg.api.realnameBase}/records?page=${page}&per_page=20`);
state.realname = normalizeListPayload(payload, "data");
} else if (state.route === "user-online-devices") {
const payload = unwrap(await request(`${cfg.api.onlineDevices}?page=${page}&per_page=20`));
const payload = await request(`${cfg.api.onlineDevices}?page=${page}&per_page=20`);
state.devices = normalizeListPayload(payload);
} else if (state.route === "user-ipv6-subscription") {
const payload = await request(`${cfg.api.ipv6Base}/users?page=${page}&per_page=20`);
state.ipv6 = normalizeListPayload(payload);
} else if (state.route === "system-config") {
state.config = unwrap(await request(cfg.api.adminConfig));
}
@@ -426,6 +433,21 @@
if (action === "sync-all") {
await adminPost(`${cfg.api.realnameBase}/sync-all`, {});
await hydrateRoute();
return;
}
if (action === "ipv6-enable") {
const userId = actionEl.getAttribute("data-user-id");
await adminPost(`${cfg.api.ipv6Base}/enable/${userId}`, {});
await hydrateRoute();
return;
}
if (action === "ipv6-sync-password") {
const userId = actionEl.getAttribute("data-user-id");
await adminPost(`${cfg.api.ipv6Base}/sync-password/${userId}`, {});
await hydrateRoute();
return;
}
}
@@ -441,7 +463,7 @@
if (action === "login") {
try {
setBusy(true);
const payload = unwrap(await request("/api/v1/passport/auth/login", {
const payload = unwrap(await request("/api/v2/passport/auth/login", {
method: "POST",
auth: false,
body: serializeForm(form)
@@ -571,6 +593,7 @@
navItem("user-manage", "用户管理", "账号、订阅、流量"),
navItem("realname", "实名认证", "实名审核"),
navItem("user-online-devices", "在线设备", "在线 IP 与会话"),
navItem("user-ipv6-subscription", "IPv6 子账号", "开通与密码同步"),
navItem("ticket-manage", "工单中心", "用户支持")
]),
renderSidebarGroup("system", "系统", [
@@ -643,6 +666,7 @@
if (state.route === "ticket-manage") return renderTicketManage();
if (state.route === "realname") return renderRealnameManage();
if (state.route === "user-online-devices") return renderOnlineDevices();
if (state.route === "user-ipv6-subscription") return renderIPv6Manage();
if (state.route === "system-config") return renderSystemConfig();
return renderOverview();
}
@@ -719,10 +743,13 @@
const toggle = hasChildren
? `<button class="node-toggle" data-action="node-expand" data-id="${node.id}">${expanded ? "" : "+"}</button>`
: '<span class="node-toggle node-toggle-placeholder"></span>';
const relationCell = isChild && node.parent_id
? `<span class="relation-chip"><code>${escapeHtml(node.id)}</code><span class="relation-arrow">&rarr;</span><code>${escapeHtml(node.parent_id)}</code></span>`
: `<code>${node.id}</code>`;
return {
className: isChild ? "node-child" : "",
cells: [
`<code>${node.id}</code>`,
relationCell,
[
'<div class="node-name-block">',
toggle,
@@ -843,10 +870,13 @@
function renderUserManage() {
const rows = state.users.list.map((user) => [
`<code>${user.id}</code>`,
user.parent_id ? renderRelationChip(user.parent_id, user.id) : `<code>${user.id}</code>`,
[
`<strong>${escapeHtml(user.email || "-")}</strong>`,
user.online_ip ? `<div class="subtle-text">在线 IP: ${escapeHtml(user.online_ip)}</div>` : ""
user.parent_id ? `<div class="subtle-text">${renderRelationChip(user.parent_id, user.id)}</div>` : "",
user.online_ip ? `<div class="subtle-text">在线 IP: ${escapeHtml(user.online_ip)}</div>` : "",
`<div class="subtle-text">实名: ${escapeHtml(user.realname_label || user.realname_status || "-")}</div>`,
user.ipv6_enabled ? `<div class="subtle-text">IPv6: ${renderRelationChip(user.id, user.ipv6_shadow_id)}</div>` : ""
].join(""),
renderStatus(user.banned ? "banned" : "active"),
escapeHtml(`${formatTraffic((user.u || 0) + (user.d || 0))} / ${formatTraffic(user.transfer_enable)}`),
@@ -922,6 +952,28 @@
].join("");
}
function renderIPv6Manage() {
const rows = state.ipv6.list.map((item) => [
renderRelationChip(item.id, item.shadow_user_id || "-"),
[
`<strong>${escapeHtml(item.email || "-")}</strong>`,
`<div class="subtle-text">${escapeHtml(item.ipv6_email || "-")}</div>`
].join(""),
escapeHtml(item.plan_name || "-"),
renderStatus(item.status_label || item.status || "-"),
escapeHtml(formatDate(item.updated_at)),
renderActionRow([
buttonAction("开通/同步", "ipv6-enable", null, `data-user-id="${item.id}"`),
buttonAction("同步密码", "ipv6-sync-password", null, `data-user-id="${item.id}"`)
])
]);
return [
wrapTable(["主从关系", "账号", "套餐", "状态", "更新时间", "操作"], rows),
renderPagination(state.ipv6.pagination)
].join("");
}
function renderSystemConfig() {
const sections = ["site", "subscribe", "server", "safe", "invite", "frontend"];
const activeTab = sections.includes(state.configTab) ? state.configTab : "site";
@@ -1163,6 +1215,10 @@
return `<div class="row-actions">${items.join("")}</div>`;
}
function renderRelationChip(fromId, toId) {
return `<span class="relation-chip"><code>${escapeHtml(fromId)}</code><span class="relation-arrow">&rarr;</span><code>${escapeHtml(toId)}</code></span>`;
}
function hiddenField(name, value) {
return `<input type="hidden" name="${escapeHtml(name)}" value="${escapeHtml(String(value))}" />`;
}
@@ -1432,9 +1488,9 @@
const text = String(status || "-");
const normalized = text.toLowerCase();
let type = "danger";
if (/(ok|active|visible|approved|paid|answered|online)/.test(normalized)) {
if (/(ok|active|visible|approved|paid|answered|online|enabled)/.test(normalized)) {
type = "ok";
} else if (/(pending|warn|unverified)/.test(normalized)) {
} else if (/(pending|warn|unverified|eligible|ready)/.test(normalized)) {
type = "warn";
}
return `<span class="status-pill status-${type}">${escapeHtml(text)}</span>`;