深度优化 ui

This commit is contained in:
2026-09-29 11:34:18 +08:00
parent 916a4f5ffc
commit 3faf960f35
27 changed files with 6510 additions and 2232 deletions
+205 -142
View File
@@ -1,14 +1,15 @@
import { ActionForm } from "@/components/action-form";
import { CopyButton } from "@/components/copy-button";
import { FilterList } from "@/components/filter-list";
import { Input } from "@/components/ui/input";
import { SubmitButton } from "@/components/submit-button";
import { getRequestOrigin } from "@/lib/request-origin";
import { formatLocalDateTime } from "@/lib/datetime";
import {
buildSubscriptionUrl,
describeRemainingTraffic,
} from "@/lib/subscription";
import {
formatBytes,
listUsers,
toDatetimeLocalValue,
} from "@/lib/store";
import { formatBytes, listUsers, toDatetimeLocalValue } from "@/lib/store";
import {
createUserAction,
@@ -31,9 +32,14 @@ function getUserPills(user: {
? { label: "管理员", className: "status-pill status-pill--ink" }
: null,
user.expires_at
? { label: formatLocalDateTime(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));
].filter((pill): pill is { label: string; className: string } =>
Boolean(pill),
);
}
export default async function UsersPage() {
@@ -46,18 +52,12 @@ export default async function UsersPage() {
<span className="eyebrow">用户</span>
<h2>用户管理</h2>
<div className="badge-row">
<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">
<div className="stat-tile">
<span>用户数</span>
<strong>{users.length}</strong>
</div>
<div className="stat-tile">
<span>启用中</span>
<strong>{users.filter((user) => user.enabled).length}</strong>
<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>
</section>
@@ -70,22 +70,30 @@ export default async function UsersPage() {
</div>
<span className="status-pill status-pill--warm">展开</span>
</summary>
<form action={createUserAction} className="dense-form-grid dense-form-grid--users">
<ActionForm
action={createUserAction}
className="dense-form-grid dense-form-grid--users"
>
<label className="field">
<span>用户名</span>
<input name="username" required />
<Input name="username" required />
</label>
<label className="field">
<span>密码</span>
<input name="password" type="password" required />
<Input name="password" type="password" required />
</label>
<label className="field">
<span>到期时间</span>
<input name="expires_at" type="datetime-local" />
<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" />
<Input
name="traffic_limit_gib"
type="number"
min="0"
step="0.1"
/>
</label>
<label className="field dense-field-wide">
<span>备注</span>
@@ -95,141 +103,196 @@ export default async function UsersPage() {
<input name="enabled" type="checkbox" defaultChecked />
<span>立即启用</span>
</label>
<button className="primary-button" type="submit">
创建用户
</button>
</form>
<SubmitButton variant="default">创建用户</SubmitButton>
</ActionForm>
</details>
</section>
<section className="panel-card dense-panel">
<div className="section-title">
<h3>用户列表</h3>
<span className="status-pill status-pill--neutral">{users.length} 个账户</span>
<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) => {
<FilterList
className="entity-table entity-table--users"
placeholder="搜索用户名、ID、备注…"
statuses={[
{ value: "enabled", label: "已启用" },
{ value: "disabled", label: "已禁用" },
]}
header={
<div className="entity-table__head user-row-grid">
<span>用户</span>
<span>状态</span>
<span>用量</span>
<span>剩余额度</span>
<span>在线</span>
<span>操作</span>
</div>
}
items={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}
return {
id: user.id,
text: `${user.username} ${user.auth_id} ${user.notes}`,
status: user.enabled ? "enabled" : "disabled",
content: (
<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="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">
</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 className="when-closed">编辑</span>
<span className="when-open">收起</span>
</span>
</summary>
<div className="entity-table__details">
<div className="dense-code-line">
<span>订阅链接</span>
<div className="copy-line">
<code>{subscriptionUrl}</code>
{user.subscription_token && (
<CopyButton value={subscriptionUrl} />
)}
</div>
</div>
<ActionForm
action={updateUserAction}
className="dense-form-grid dense-form-grid--users"
>
<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}>
<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>
<SubmitButton variant="outline">保存修改</SubmitButton>
</ActionForm>
<div className="entity-action-strip">
<ActionForm
action={resetUserPasswordAction}
className="inline-form inline-form--compact"
>
<input name="id" type="hidden" value={user.id} />
<button className="danger-button" type="submit">
删除用户
</button>
</form>
)}
<Input
name="password"
type="password"
aria-label="新密码"
autoComplete="new-password"
placeholder="新密码"
required
/>
<SubmitButton variant="outline">重置密码</SubmitButton>
</ActionForm>
{user.is_admin ? null : (
<ActionForm action={deleteUserAction}>
<input name="id" type="hidden" value={user.id} />
<SubmitButton
variant="destructive"
confirm={`将永久删除用户「${user.username}」,此用户将无法继续连接。`}
>
删除用户
</SubmitButton>
</ActionForm>
)}
</div>
</div>
</div>
</details>
);
</details>
),
};
})}
</div>
/>
</section>
</main>
);
}
}