基本功能已初步完善
This commit is contained in:
@@ -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">→</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">→</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>`;
|
||||
|
||||
Reference in New Issue
Block a user