优化ui
This commit is contained in:
+171
-156
@@ -1,4 +1,5 @@
|
||||
import { getRequestOrigin } from "@/lib/request-origin";
|
||||
import { formatLocalDateTime } from "@/lib/datetime";
|
||||
import {
|
||||
buildSubscriptionUrl,
|
||||
describeRemainingTraffic,
|
||||
@@ -30,7 +31,7 @@ function getUserPills(user: {
|
||||
? { label: "管理员", className: "status-pill status-pill--ink" }
|
||||
: null,
|
||||
user.expires_at
|
||||
? { label: user.expires_at, className: "status-pill status-pill--neutral" }
|
||||
? { label: formatLocalDateTime(user.expires_at), className: "status-pill status-pill--neutral" }
|
||||
: { label: "长期有效", className: "status-pill status-pill--warm" },
|
||||
].filter((pill): pill is { label: string; className: string } => Boolean(pill));
|
||||
}
|
||||
@@ -39,17 +40,14 @@ export default async function UsersPage() {
|
||||
const [users, origin] = await Promise.all([listUsers(), getRequestOrigin()]);
|
||||
|
||||
return (
|
||||
<main className="page-stack">
|
||||
<main className="page-stack management-page">
|
||||
<section className="hero-panel">
|
||||
<div className="hero-copy">
|
||||
<span className="eyebrow">USERS</span>
|
||||
<span className="eyebrow">用户</span>
|
||||
<h2>用户管理</h2>
|
||||
<p className="page-lede">
|
||||
管理用户名:密码、`auth_id`、到期时间、额度、订阅名和用户自助链接。
|
||||
</p>
|
||||
<div className="badge-row">
|
||||
<span className="status-pill status-pill--ok">自助登录</span>
|
||||
<span className="status-pill status-pill--neutral">订阅链接按用户独立生成</span>
|
||||
<span className="status-pill status-pill--ok">启用 {users.filter((user) => user.enabled).length}</span>
|
||||
<span className="status-pill status-pill--neutral">总数 {users.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hero-aside">
|
||||
@@ -64,157 +62,174 @@ export default async function UsersPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel-card">
|
||||
<div className="section-title">
|
||||
<h3>新建用户</h3>
|
||||
</div>
|
||||
<form action={createUserAction} className="form-grid">
|
||||
<label className="field">
|
||||
<span>用户名</span>
|
||||
<input name="username" required />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>密码</span>
|
||||
<input name="password" type="password" required />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>到期时间</span>
|
||||
<input name="expires_at" type="datetime-local" />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>总流量上限 GiB</span>
|
||||
<input name="traffic_limit_gib" type="number" min="0" step="0.1" />
|
||||
</label>
|
||||
<label className="field full-span">
|
||||
<span>备注</span>
|
||||
<textarea name="notes" rows={3} />
|
||||
</label>
|
||||
<label className="checkbox">
|
||||
<input name="enabled" type="checkbox" defaultChecked />
|
||||
<span>立即启用</span>
|
||||
</label>
|
||||
<button className="primary-button" type="submit">
|
||||
创建用户
|
||||
</button>
|
||||
</form>
|
||||
<section className="panel-card compact-create-panel">
|
||||
<details>
|
||||
<summary className="compact-create-summary">
|
||||
<div>
|
||||
<h3>新建用户</h3>
|
||||
</div>
|
||||
<span className="status-pill status-pill--warm">展开</span>
|
||||
</summary>
|
||||
<form action={createUserAction} className="dense-form-grid dense-form-grid--users">
|
||||
<label className="field">
|
||||
<span>用户名</span>
|
||||
<input name="username" required />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>密码</span>
|
||||
<input name="password" type="password" required />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>到期时间</span>
|
||||
<input name="expires_at" type="datetime-local" />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>总流量上限 GiB</span>
|
||||
<input name="traffic_limit_gib" type="number" min="0" step="0.1" />
|
||||
</label>
|
||||
<label className="field dense-field-wide">
|
||||
<span>备注</span>
|
||||
<textarea name="notes" rows={2} />
|
||||
</label>
|
||||
<label className="checkbox">
|
||||
<input name="enabled" type="checkbox" defaultChecked />
|
||||
<span>立即启用</span>
|
||||
</label>
|
||||
<button className="primary-button" type="submit">
|
||||
创建用户
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section className="stack-lg">
|
||||
{users.map((user) => (
|
||||
<article key={user.id} className="panel-card entity-card">
|
||||
<div className="section-title section-title--top">
|
||||
<div>
|
||||
<h3>{user.username}</h3>
|
||||
<p className="muted mono">auth_id: {user.auth_id}</p>
|
||||
</div>
|
||||
<div className="align-right stack-tight">
|
||||
<div className="badge-row badge-row--end">
|
||||
{getUserPills(user).map((pill) => (
|
||||
<span key={pill.label} className={pill.className}>
|
||||
{pill.label}
|
||||
</span>
|
||||
))}
|
||||
<section className="panel-card dense-panel">
|
||||
<div className="section-title">
|
||||
<h3>用户列表</h3>
|
||||
<span className="status-pill status-pill--neutral">{users.length} 个账户</span>
|
||||
</div>
|
||||
<div className="entity-table entity-table--users">
|
||||
<div className="entity-table__head user-row-grid">
|
||||
<span>用户</span>
|
||||
<span>状态</span>
|
||||
<span>用量</span>
|
||||
<span>剩余额度</span>
|
||||
<span>在线</span>
|
||||
<span>操作</span>
|
||||
</div>
|
||||
{users.map((user) => {
|
||||
const usedBytes = user.used_tx_bytes + user.used_rx_bytes;
|
||||
const subscriptionUrl = user.subscription_token
|
||||
? buildSubscriptionUrl(user.subscription_token, origin)
|
||||
: "暂不可用";
|
||||
|
||||
return (
|
||||
<details key={user.id} className="entity-table__item">
|
||||
<summary className="entity-table__summary user-row-grid">
|
||||
<span className="entity-main-cell">
|
||||
<strong>{user.username}</strong>
|
||||
<span className="muted mono break-all">{user.auth_id}</span>
|
||||
</span>
|
||||
<span className="badge-row entity-status-cell">
|
||||
{getUserPills(user).map((pill) => (
|
||||
<span key={pill.label} className={pill.className}>
|
||||
{pill.label}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
<span className="entity-number-cell">
|
||||
<strong>{formatBytes(usedBytes)}</strong>
|
||||
<span>TX {formatBytes(user.used_tx_bytes)} / RX {formatBytes(user.used_rx_bytes)}</span>
|
||||
</span>
|
||||
<span className="entity-number-cell">
|
||||
<strong>{describeRemainingTraffic(user)}</strong>
|
||||
<span>{user.traffic_limit_bytes ? "有额度" : "不限量"}</span>
|
||||
</span>
|
||||
<span className="entity-number-cell">
|
||||
<strong>{user.online_connections}</strong>
|
||||
<span>连接</span>
|
||||
</span>
|
||||
<span className="entity-expand-label">编辑</span>
|
||||
</summary>
|
||||
<div className="entity-table__details">
|
||||
<div className="dense-code-line">
|
||||
<span>订阅链接</span>
|
||||
<code>{subscriptionUrl}</code>
|
||||
</div>
|
||||
<form action={updateUserAction} className="dense-form-grid dense-form-grid--users">
|
||||
<input name="id" type="hidden" value={user.id} />
|
||||
<label className="field">
|
||||
<span>用户名</span>
|
||||
<input name="username" defaultValue={user.username} required />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>到期时间</span>
|
||||
<input
|
||||
name="expires_at"
|
||||
type="datetime-local"
|
||||
defaultValue={toDatetimeLocalValue(user.expires_at)}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>总流量上限 GiB</span>
|
||||
<input
|
||||
name="traffic_limit_gib"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
defaultValue={
|
||||
user.traffic_limit_bytes && !user.is_admin
|
||||
? (user.traffic_limit_bytes / 1024 / 1024 / 1024).toFixed(2)
|
||||
: ""
|
||||
}
|
||||
disabled={Boolean(user.is_admin)}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>订阅名称</span>
|
||||
<input
|
||||
name="subscription_label"
|
||||
defaultValue={user.subscription_label || user.username}
|
||||
/>
|
||||
</label>
|
||||
<label className="field dense-field-wide">
|
||||
<span>备注</span>
|
||||
<textarea name="notes" rows={2} defaultValue={user.notes} />
|
||||
</label>
|
||||
<label className="checkbox">
|
||||
<input
|
||||
name="enabled"
|
||||
type="checkbox"
|
||||
defaultChecked={Boolean(user.enabled)}
|
||||
/>
|
||||
<span>启用用户</span>
|
||||
</label>
|
||||
<button className="ghost-button" type="submit">
|
||||
保存修改
|
||||
</button>
|
||||
</form>
|
||||
<div className="entity-action-strip">
|
||||
<form action={resetUserPasswordAction} className="inline-form inline-form--compact">
|
||||
<input name="id" type="hidden" value={user.id} />
|
||||
<input name="password" type="password" placeholder="新密码" required />
|
||||
<button className="ghost-button" type="submit">
|
||||
重置密码
|
||||
</button>
|
||||
</form>
|
||||
{user.is_admin ? null : (
|
||||
<form action={deleteUserAction}>
|
||||
<input name="id" type="hidden" value={user.id} />
|
||||
<button className="danger-button" type="submit">
|
||||
删除用户
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="muted">
|
||||
累计 {formatBytes(user.used_tx_bytes + user.used_rx_bytes)} / 在线{" "}
|
||||
{user.online_connections}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="summary-grid">
|
||||
<div className="metric-inline">
|
||||
<span>总用量</span>
|
||||
<strong>{formatBytes(user.used_tx_bytes + user.used_rx_bytes)}</strong>
|
||||
</div>
|
||||
<div className="metric-inline">
|
||||
<span>剩余额度</span>
|
||||
<strong>{describeRemainingTraffic(user)}</strong>
|
||||
</div>
|
||||
<div className="metric-inline">
|
||||
<span>在线连接</span>
|
||||
<strong>{user.online_connections}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<form action={updateUserAction} className="form-grid compact">
|
||||
<input name="id" type="hidden" value={user.id} />
|
||||
<label className="field">
|
||||
<span>用户名</span>
|
||||
<input name="username" defaultValue={user.username} required />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>到期时间</span>
|
||||
<input
|
||||
name="expires_at"
|
||||
type="datetime-local"
|
||||
defaultValue={toDatetimeLocalValue(user.expires_at)}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>总流量上限 GiB</span>
|
||||
<input
|
||||
name="traffic_limit_gib"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
defaultValue={
|
||||
user.traffic_limit_bytes && !user.is_admin
|
||||
? (user.traffic_limit_bytes / 1024 / 1024 / 1024).toFixed(2)
|
||||
: ""
|
||||
}
|
||||
disabled={Boolean(user.is_admin)}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>订阅名称</span>
|
||||
<input
|
||||
name="subscription_label"
|
||||
defaultValue={user.subscription_label || user.username}
|
||||
/>
|
||||
</label>
|
||||
<label className="field full-span">
|
||||
<span>备注</span>
|
||||
<textarea name="notes" rows={3} defaultValue={user.notes} />
|
||||
</label>
|
||||
<label className="checkbox">
|
||||
<input
|
||||
name="enabled"
|
||||
type="checkbox"
|
||||
defaultChecked={Boolean(user.enabled)}
|
||||
/>
|
||||
<span>启用用户</span>
|
||||
</label>
|
||||
<button className="ghost-button" type="submit">
|
||||
保存修改
|
||||
</button>
|
||||
</form>
|
||||
<div className="instruction-block">
|
||||
<strong>订阅链接</strong>
|
||||
<p className="muted mono break-all">
|
||||
{user.subscription_token
|
||||
? buildSubscriptionUrl(user.subscription_token, origin)
|
||||
: "暂不可用"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="action-row">
|
||||
<form action={resetUserPasswordAction} className="inline-form">
|
||||
<input name="id" type="hidden" value={user.id} />
|
||||
<input name="password" type="password" placeholder="新密码" required />
|
||||
<button className="ghost-button" type="submit">
|
||||
重置密码
|
||||
</button>
|
||||
</form>
|
||||
{user.is_admin ? null : (
|
||||
<form action={deleteUserAction}>
|
||||
<input name="id" type="hidden" value={user.id} />
|
||||
<button className="danger-button" type="submit">
|
||||
删除用户
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user