Files
memind_adm/src/admin/pages/UserDetailPage.tsx
T
2026-06-30 20:26:33 +08:00

227 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { Link, Navigate, useParams } from 'react-router-dom';
import { getAdminUser, rechargeUser, updateAdminUser } from '../../api/client';
import { CapabilitySettings } from '../../components/CapabilitySettings';
import { PolicySettings } from '../../components/PolicySettings';
import { SkillSettings } from '../../components/SkillSettings';
import { useAdminUsers } from '../hooks/useAdminUsers';
import { formatYuan } from '../utils/format';
import type { PortalUser } from '../../types';
function formatBytes(bytes: number) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
export function UserDetailPage() {
const { userId = '' } = useParams();
const { users, reload, setError } = useAdminUsers();
const [user, setUser] = useState<PortalUser | null>(null);
const [loading, setLoading] = useState(true);
const [error, setLocalError] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [recharge, setRecharge] = useState({ amountYuan: '10', note: '' });
const [spaceQuotaMb, setSpaceQuotaMb] = useState('5');
useEffect(() => {
if (!user?.spaceQuotaBytes) return;
setSpaceQuotaMb(String(Math.max(1, Math.round(user.spaceQuotaBytes / 1024 / 1024))));
}, [user?.spaceQuotaBytes]);
useEffect(() => {
let cancelled = false;
if (!userId) return;
setLoading(true);
setLocalError(null);
void getAdminUser(userId)
.then((nextUser) => {
if (cancelled) return;
setUser(nextUser);
})
.catch((err) => {
if (cancelled) return;
const fallbackUser = users.find((row) => row.id === userId) ?? null;
if (fallbackUser) {
setUser(fallbackUser);
setLocalError(null);
return;
}
setLocalError(err instanceof Error ? err.message : '加载用户失败');
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [userId, users]);
const handleRecharge = async (e: React.FormEvent) => {
e.preventDefault();
if (!user) return;
setMessage(null);
setLocalError(null);
setError(null);
try {
const amountCents = Math.round(Number(recharge.amountYuan) * 100);
const nextUser = await rechargeUser(user.id, amountCents, recharge.note || undefined);
setUser(nextUser);
setMessage('充值成功');
setRecharge({ amountYuan: '10', note: '' });
await reload();
} catch (err) {
setLocalError(err instanceof Error ? err.message : '充值失败');
}
};
if (!loading && !user && !error) {
return <Navigate to="/users" replace />;
}
const currentQuotaMb = Math.max(1, Math.round((user?.spaceQuotaBytes ?? 0) / 1024 / 1024));
const handleSpaceQuotaSave = async (e: React.FormEvent) => {
e.preventDefault();
if (!user) return;
setMessage(null);
setLocalError(null);
setError(null);
try {
const quotaMb = Math.floor(Number(spaceQuotaMb));
const nextUser = await updateAdminUser(user.id, {
spaceQuotaBytes: quotaMb * 1024 * 1024,
});
setUser(nextUser);
setMessage('空间已更新');
await reload();
} catch (err) {
setLocalError(err instanceof Error ? err.message : '空间更新失败');
}
};
return (
<div className="admin-page">
<div className="admin-page-head">
<Link to="/users" className="ghost-btn admin-back-inline">
</Link>
<h2>{user?.displayName ?? '用户详情'}</h2>
{user && <p className="muted">@{user.username} · {user.role} · {user.status}</p>}
</div>
{error && <p className="banner banner-error">{error}</p>}
{message && <p className="banner banner-info">{message}</p>}
{loading || !user ? (
<p className="muted"></p>
) : (
<>
<section className="admin-card">
<div className="admin-card-head">
<h2></h2>
{user.role === 'user' && (
<button
type="button"
className="ghost-btn"
onClick={() => {
setLocalError(null);
void updateAdminUser(user.id, {
status: user.status === 'active' ? 'disabled' : 'active',
}).then((nextUser) => {
setUser(nextUser);
void reload();
}).catch((err) => {
setLocalError(err instanceof Error ? err.message : '状态更新失败');
});
}}
>
{user.status === 'active' ? '禁用' : '启用'}
</button>
)}
</div>
<dl className="admin-dl">
<div>
<dt></dt>
<dd>¥{formatYuan(user.balanceCents)}</dd>
</div>
<div>
<dt></dt>
<dd>
{user.spaceQuotaBytes ? formatBytes(user.spaceQuotaBytes) : '—'}
{user.spaceUsedBytes !== undefined && (
<span className="muted">
{' '}
· {formatBytes(user.spaceUsedBytes)} · {' '}
{formatBytes(user.spaceAvailableBytes ?? 0)}
</span>
)}
</dd>
</div>
<div>
<dt></dt>
<dd className="mono">{user.workspaceRoot}</dd>
</div>
</dl>
</section>
{user.role === 'user' && (
<>
<section className="admin-card">
<h2></h2>
<form className="admin-form" onSubmit={handleRecharge}>
<input
placeholder="金额(元)"
value={recharge.amountYuan}
onChange={(e) => setRecharge((s) => ({ ...s, amountYuan: e.target.value }))}
/>
<input
placeholder="备注"
value={recharge.note}
onChange={(e) => setRecharge((s) => ({ ...s, note: e.target.value }))}
/>
<button type="submit" className="send-btn">
</button>
</form>
</section>
<section className="admin-card">
<h2></h2>
<p className="muted">
MB
</p>
<form className="admin-form" onSubmit={handleSpaceQuotaSave}>
<input
placeholder="空间总额(MB"
type="number"
min="1"
value={spaceQuotaMb}
onChange={(e) => setSpaceQuotaMb(e.target.value)}
/>
<button type="submit" className="send-btn">
</button>
</form>
<p className="muted"> {currentQuotaMb} MB</p>
</section>
<CapabilitySettings users={users} userId={user.id} userOnly />
<SkillSettings users={users} userId={user.id} userOnly />
<PolicySettings users={users} userId={user.id} userOnly />
</>
)}
{user.role === 'admin' && (
<section className="admin-card">
<p className="muted">
goose auto
</p>
</section>
)}
</>
)}
</div>
);
}