修复丢失的前端文件
Some checks failed
build / build (api, amd64, linux) (push) Failing after -52s
build / build (api, arm64, linux) (push) Failing after -52s
build / build (api.exe, amd64, windows) (push) Failing after -52s

This commit is contained in:
CN-JS-HuiBai
2026-04-18 22:03:26 +08:00
parent 8cca428d89
commit 609ab002b3
60 changed files with 338638 additions and 0 deletions

View File

@@ -0,0 +1,526 @@
import React, { useEffect, useState } from "../../../recovery-preview/node_modules/react/index.js";
import {
compactText,
datetimeLocalToUnix,
formatBytes,
formatCurrencyFen,
formatUnixSeconds,
requestJson,
unixToDatetimeLocal,
} from "../../runtime/client.js";
const initialEditForm = {
id: "",
email: "",
password: "",
balance: "",
commission_balance: "",
commission_type: "",
commission_rate: "",
group_id: "",
plan_id: "",
speed_limit: "",
device_limit: "",
expired_at: "",
remarks: "",
};
const initialMailForm = {
user_id: "",
subject: "",
content: "",
};
function UserManagePage() {
const [loading, setLoading] = useState(true);
const [refreshTick, setRefreshTick] = useState(0);
const [searchInput, setSearchInput] = useState("");
const [keyword, setKeyword] = useState("");
const [rows, setRows] = useState([]);
const [pagination, setPagination] = useState({ current: 1, last_page: 1, per_page: 20, total: 0 });
const [plans, setPlans] = useState([]);
const [groups, setGroups] = useState([]);
const [modal, setModal] = useState(null);
const [editForm, setEditForm] = useState(initialEditForm);
const [mailForm, setMailForm] = useState(initialMailForm);
const [notice, setNotice] = useState("");
const [error, setError] = useState("");
useEffect(() => {
let active = true;
(async () => {
try {
const [planResult, groupResult] = await Promise.all([
requestJson("/plan/fetch"),
requestJson("/server/group/fetch"),
]);
if (!active) {
return;
}
setPlans(Array.isArray(planResult?.data) ? planResult.data : planResult?.list || planResult || []);
setGroups(Array.isArray(groupResult?.data) ? groupResult.data : groupResult?.list || groupResult || []);
} catch (err) {
if (active) {
setNotice(err.message || "Reference data failed to load");
}
}
})();
return () => {
active = false;
};
}, []);
useEffect(() => {
let active = true;
(async () => {
setLoading(true);
setError("");
try {
const query = new URLSearchParams({
page: String(pagination.current || 1),
per_page: String(pagination.per_page || 20),
});
if (keyword) {
query.set("keyword", keyword);
}
const payload = await requestJson(`/user/fetch?${query.toString()}`);
if (!active) {
return;
}
setRows(Array.isArray(payload?.list) ? payload.list : []);
setPagination(payload?.pagination || pagination);
} catch (err) {
if (active) {
setError(err.message || "Failed to load users");
setRows([]);
}
} finally {
if (active) {
setLoading(false);
}
}
})();
return () => {
active = false;
};
}, [keyword, pagination.current, pagination.per_page, refreshTick]);
const openEditModal = (user) => {
setEditForm({
id: user.id,
email: user.email || "",
password: "",
balance: user.balance ?? "",
commission_balance: user.commission_balance ?? "",
commission_type: user.commission_type ?? "",
commission_rate: user.commission_rate ?? "",
group_id: user.group_id ?? "",
plan_id: user.plan_id ?? "",
speed_limit: user.speed_limit ?? "",
device_limit: user.device_limit ?? "",
expired_at: unixToDatetimeLocal(user.expired_at),
remarks: user.remarks || "",
});
setModal({ type: "edit", user });
};
const openMailModal = (user) => {
setMailForm({
user_id: user.id,
subject: `Hello ${user.email}`,
content: "",
});
setModal({ type: "mail", user });
};
const submitSearch = (event) => {
event.preventDefault();
setPagination((current) => ({ ...current, current: 1 }));
setKeyword(compactText(searchInput));
};
const reload = () => setRefreshTick((value) => value + 1);
const saveUser = async (event) => {
event.preventDefault();
setNotice("");
setError("");
try {
const payload = {
id: Number(editForm.id),
email: compactText(editForm.email),
password: compactText(editForm.password) || undefined,
balance: toNullableNumber(editForm.balance),
commission_balance: toNullableNumber(editForm.commission_balance),
commission_type: toNullableNumber(editForm.commission_type),
commission_rate: toNullableNumber(editForm.commission_rate),
group_id: toNullableNumber(editForm.group_id),
plan_id: toNullableNumber(editForm.plan_id),
speed_limit: toNullableNumber(editForm.speed_limit),
device_limit: toNullableNumber(editForm.device_limit),
expired_at: datetimeLocalToUnix(editForm.expired_at),
remarks: compactText(editForm.remarks),
};
pruneUndefined(payload);
await requestJson("/user/update", { method: "POST", body: payload });
setModal(null);
reload();
} catch (err) {
setError(err.message || "Failed to save user");
}
};
const resetSecret = async (user) => {
if (!window.confirm(`Reset secret for ${user.email}?`)) {
return;
}
try {
const payload = await requestJson("/user/resetSecret", {
method: "POST",
body: { id: user.id },
});
setNotice(typeof payload?.data === "string" ? `Secret reset: ${payload.data}` : "Secret reset");
reload();
} catch (err) {
setError(err.message || "Failed to reset secret");
}
};
const resetTraffic = async (user) => {
if (!window.confirm(`Reset traffic for ${user.email}?`)) {
return;
}
try {
await requestJson("/user/resetTraffic", {
method: "POST",
body: { id: user.id },
});
setNotice("Traffic reset complete");
reload();
} catch (err) {
setError(err.message || "Failed to reset traffic");
}
};
const toggleBan = async (user) => {
const banned = !Boolean(user.banned);
if (!window.confirm(`${banned ? "Ban" : "Unban"} ${user.email}?`)) {
return;
}
try {
await requestJson("/user/ban", {
method: "POST",
body: { id: user.id, banned },
});
reload();
} catch (err) {
setError(err.message || "Failed to update status");
}
};
const deleteUser = async (user) => {
if (!window.confirm(`Delete ${user.email}?`)) {
return;
}
try {
await requestJson("/user/drop", {
method: "POST",
body: { id: user.id },
});
reload();
} catch (err) {
setError(err.message || "Failed to delete user");
}
};
const saveMail = async (event) => {
event.preventDefault();
try {
await requestJson("/user/sendMail", {
method: "POST",
body: {
user_id: Number(mailForm.user_id),
subject: compactText(mailForm.subject),
content: compactText(mailForm.content),
},
});
setModal(null);
setNotice("Mail submitted");
} catch (err) {
setError(err.message || "Failed to send mail");
}
};
return (
<div className="recovery-live-page">
<header className="recovery-live-hero">
<div>
<p className="eyebrow">Recovered React Page</p>
<h2>User Management</h2>
<p>Search, edit, ban, reset traffic, and reset secrets against the live admin API.</p>
</div>
<div className="hero-stats">
<div><strong>{pagination.total}</strong><span>users</span></div>
<div><strong>{pagination.current}</strong><span>page</span></div>
<div><strong>{plans.length}</strong><span>plans</span></div>
</div>
</header>
<section className="recovery-toolbar">
<form className="recovery-search" onSubmit={submitSearch}>
<input
value={searchInput}
onChange={(event) => setSearchInput(event.target.value)}
placeholder="Search by email or ID"
/>
<button type="submit">Search</button>
</form>
<button
type="button"
className="secondary"
onClick={() => {
setSearchInput("");
setKeyword("");
setPagination((current) => ({ ...current, current: 1 }));
}}
>
Clear
</button>
</section>
{notice ? <div className="toast toast-info">{notice}</div> : null}
{error ? <div className="toast toast-error">{error}</div> : null}
<section className="recovery-table-card">
<div className="table-scroll">
<table className="recovery-table">
<thead>
<tr>
<th>ID</th>
<th>User</th>
<th>Plan / Group</th>
<th>Traffic</th>
<th>Balance</th>
<th>Status</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr><td colSpan={8} className="empty-cell">Loading...</td></tr>
) : rows.length === 0 ? (
<tr><td colSpan={8} className="empty-cell">No users found</td></tr>
) : rows.map((user) => (
<tr key={user.id}>
<td>{user.id}</td>
<td>
<div className="cell-stack">
<strong>{user.email}</strong>
<span className="muted">UID: {user.id}</span>
{user.online_ip ? <span className="chip chip-soft">{user.online_ip}</span> : null}
</div>
</td>
<td>
<div className="cell-stack">
<span>{user.plan_name || `Plan ${user.plan_id || "-"}`}</span>
<span className="muted">{user.group_name || `Group ${user.group_id || "-"}`}</span>
</div>
</td>
<td>
<div className="cell-stack">
<span>{formatBytes(user.transfer_enable)}</span>
<span className="muted">Used {formatBytes(Number(user.u || 0) + Number(user.d || 0))}</span>
</div>
</td>
<td>
<div className="cell-stack">
<span>{formatCurrencyFen(user.balance)}</span>
<span className="muted">Comm {formatCurrencyFen(user.commission_balance)}</span>
</div>
</td>
<td>
<span className={`chip ${user.banned ? "chip-danger" : "chip-ok"}`}>
{user.banned ? "Banned" : "Active"}
</span>
</td>
<td>{formatUnixSeconds(user.updated_at || user.last_login_at || user.created_at)}</td>
<td>
<div className="action-row">
<button type="button" onClick={() => openEditModal(user)}>Edit</button>
<button type="button" onClick={() => openMailModal(user)}>Mail</button>
<button type="button" onClick={() => resetSecret(user)}>Secret</button>
<button type="button" onClick={() => resetTraffic(user)}>Traffic</button>
<button type="button" onClick={() => toggleBan(user)}>
{user.banned ? "Unban" : "Ban"}
</button>
<button type="button" className="danger" onClick={() => deleteUser(user)}>Delete</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="pagination-bar">
<button
type="button"
disabled={pagination.current <= 1}
onClick={() => setPagination((current) => ({ ...current, current: Math.max(1, current.current - 1) }))}
>
Prev
</button>
<span>
Page {pagination.current} / {pagination.last_page}
</span>
<button
type="button"
disabled={pagination.current >= pagination.last_page}
onClick={() => setPagination((current) => ({ ...current, current: Math.min(current.last_page, current.current + 1) }))}
>
Next
</button>
</div>
</section>
{modal?.type === "edit" ? (
<Modal title={`Edit user ${editForm.email}`} onClose={() => setModal(null)}>
<form className="modal-grid" onSubmit={saveUser}>
<label>
<span>ID</span>
<input value={editForm.id} readOnly />
</label>
<label>
<span>Email</span>
<input value={editForm.email} onChange={(event) => setEditForm((current) => ({ ...current, email: event.target.value }))} />
</label>
<label>
<span>Password</span>
<input
type="password"
value={editForm.password}
onChange={(event) => setEditForm((current) => ({ ...current, password: event.target.value }))}
placeholder="leave blank to keep"
/>
</label>
<label>
<span>Balance</span>
<input type="number" value={editForm.balance} onChange={(event) => setEditForm((current) => ({ ...current, balance: event.target.value }))} />
</label>
<label>
<span>Commission Balance</span>
<input type="number" value={editForm.commission_balance} onChange={(event) => setEditForm((current) => ({ ...current, commission_balance: event.target.value }))} />
</label>
<label>
<span>Commission Type</span>
<input type="number" value={editForm.commission_type} onChange={(event) => setEditForm((current) => ({ ...current, commission_type: event.target.value }))} />
</label>
<label>
<span>Commission Rate</span>
<input type="number" value={editForm.commission_rate} onChange={(event) => setEditForm((current) => ({ ...current, commission_rate: event.target.value }))} />
</label>
<label>
<span>Group</span>
<select value={editForm.group_id} onChange={(event) => setEditForm((current) => ({ ...current, group_id: event.target.value }))}>
<option value="">-</option>
{groups.map((group) => (
<option key={group.id} value={group.id}>{group.name || `Group ${group.id}`}</option>
))}
</select>
</label>
<label>
<span>Plan</span>
<select value={editForm.plan_id} onChange={(event) => setEditForm((current) => ({ ...current, plan_id: event.target.value }))}>
<option value="">-</option>
{plans.map((plan) => (
<option key={plan.id} value={plan.id}>{plan.name || `Plan ${plan.id}`}</option>
))}
</select>
</label>
<label>
<span>Speed Limit</span>
<input type="number" value={editForm.speed_limit} onChange={(event) => setEditForm((current) => ({ ...current, speed_limit: event.target.value }))} />
</label>
<label>
<span>Device Limit</span>
<input type="number" value={editForm.device_limit} onChange={(event) => setEditForm((current) => ({ ...current, device_limit: event.target.value }))} />
</label>
<label>
<span>Expired At</span>
<input
type="datetime-local"
value={editForm.expired_at}
onChange={(event) => setEditForm((current) => ({ ...current, expired_at: event.target.value }))}
/>
</label>
<label className="span-2">
<span>Remarks</span>
<textarea value={editForm.remarks} onChange={(event) => setEditForm((current) => ({ ...current, remarks: event.target.value }))} />
</label>
<div className="modal-actions span-2">
<button type="button" className="secondary" onClick={() => setModal(null)}>Cancel</button>
<button type="submit">Save</button>
</div>
</form>
</Modal>
) : null}
{modal?.type === "mail" ? (
<Modal title={`Send mail to ${mailForm.user_id}`} onClose={() => setModal(null)}>
<form className="modal-grid" onSubmit={saveMail}>
<label>
<span>User ID</span>
<input value={mailForm.user_id} readOnly />
</label>
<label className="span-2">
<span>Subject</span>
<input value={mailForm.subject} onChange={(event) => setMailForm((current) => ({ ...current, subject: event.target.value }))} />
</label>
<label className="span-2">
<span>Content</span>
<textarea rows={8} value={mailForm.content} onChange={(event) => setMailForm((current) => ({ ...current, content: event.target.value }))} />
</label>
<div className="modal-actions span-2">
<button type="button" className="secondary" onClick={() => setModal(null)}>Cancel</button>
<button type="submit">Send</button>
</div>
</form>
</Modal>
) : null}
</div>
);
}
function Modal({ title, children, onClose }) {
return (
<div className="modal-backdrop" role="presentation" onClick={onClose}>
<div className="modal-panel" role="dialog" aria-modal="true" onClick={(event) => event.stopPropagation()}>
<header className="modal-header">
<h3>{title}</h3>
<button type="button" className="secondary" onClick={onClose}>Close</button>
</header>
{children}
</div>
</div>
);
}
function pruneUndefined(target) {
Object.keys(target).forEach((key) => {
if (target[key] === undefined || target[key] === null || target[key] === "") {
delete target[key];
}
});
}
function toNullableNumber(value) {
const trimmed = compactText(value);
if (!trimmed) {
return undefined;
}
const number = Number(trimmed);
return Number.isNaN(number) ? undefined : number;
}
export default UserManagePage;