Initial commit: tkmind admin frontend (React + Vite).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-06-16 19:44:31 +08:00
commit 8ad1a8a8da
30 changed files with 5981 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { createAdminUser, updateAdminUser } from '../../api/client';
import { useAdminUsers } from '../hooks/useAdminUsers';
import { formatYuan } from '../utils/format';
export function UsersPage() {
const { users, loading, error, reload, setError } = useAdminUsers();
const [message, setMessage] = useState<string | null>(null);
const [newUser, setNewUser] = useState({
username: '',
password: '',
displayName: '',
workspaceRoot: '',
balanceCents: 500,
});
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
setMessage(null);
setError(null);
try {
await createAdminUser({
username: newUser.username,
password: newUser.password,
displayName: newUser.displayName || undefined,
workspaceRoot: newUser.workspaceRoot || undefined,
balanceCents: Number(newUser.balanceCents),
});
setNewUser({
username: '',
password: '',
displayName: '',
workspaceRoot: '',
balanceCents: 500,
});
setMessage('用户已创建');
await reload();
} catch (err) {
setError(err instanceof Error ? err.message : '创建失败');
}
};
return (
<div className="admin-page">
<div className="admin-page-head">
<h2></h2>
<p className="muted"></p>
</div>
{error && <p className="banner banner-error">{error}</p>}
{message && <p className="banner banner-info">{message}</p>}
<section className="admin-card">
<h2></h2>
<form className="admin-form" onSubmit={handleCreate}>
<input
placeholder="用户名"
autoComplete="username"
value={newUser.username}
onChange={(e) => setNewUser((s) => ({ ...s, username: e.target.value }))}
/>
<input
placeholder="密码"
type="password"
autoComplete="new-password"
value={newUser.password}
onChange={(e) => setNewUser((s) => ({ ...s, password: e.target.value }))}
/>
<input
placeholder="显示名称"
value={newUser.displayName}
onChange={(e) => setNewUser((s) => ({ ...s, displayName: e.target.value }))}
/>
<input
placeholder="工作目录(可选)"
value={newUser.workspaceRoot}
onChange={(e) => setNewUser((s) => ({ ...s, workspaceRoot: e.target.value }))}
/>
<input
placeholder="初始余额(分)"
type="number"
value={newUser.balanceCents}
onChange={(e) =>
setNewUser((s) => ({ ...s, balanceCents: Number(e.target.value) }))
}
/>
<button type="submit" className="send-btn">
</button>
</form>
</section>
<section className="admin-card">
<div className="admin-card-head">
<h2></h2>
<button type="button" className="ghost-btn" onClick={() => void reload()}>
</button>
</div>
{loading ? (
<p className="muted"></p>
) : (
<div className="admin-table-wrap">
<table className="admin-table">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td>
<div>{user.displayName}</div>
<div className="muted">@{user.username}</div>
</td>
<td>{user.role}</td>
<td>{user.status}</td>
<td>¥{formatYuan(user.balanceCents)}</td>
<td className="mono">{user.workspaceRoot}</td>
<td className="admin-actions">
<Link to={`/admin/users/${user.id}`} className="ghost-btn admin-inline-link">
</Link>
{user.role === 'user' && (
<button
type="button"
className="ghost-btn"
onClick={() =>
void updateAdminUser(user.id, {
status: user.status === 'active' ? 'disabled' : 'active',
}).then(reload)
}
>
{user.status === 'active' ? '禁用' : '启用'}
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
</div>
);
}