497 lines
22 KiB
TypeScript
497 lines
22 KiB
TypeScript
|
|
import { useEffect, useMemo, useState } from 'react';
|
|||
|
|
import axios from 'axios';
|
|||
|
|
import { ChevronLeft, ChevronRight, Pencil, Plus, RefreshCw, Search, Trash2, UserRound, X } from 'lucide-react';
|
|||
|
|
import { APP_ENDPOINTS } from '../../../config/env';
|
|||
|
|
import { LucentDrawer } from '../../../components/lucent/LucentDrawer';
|
|||
|
|
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
|||
|
|
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
|
|||
|
|
import type { BotState } from '../../../types/bot';
|
|||
|
|
import type { SysRoleSummary, SysUserSummary } from '../../../types/sys';
|
|||
|
|
import { normalizePlatformPageSize, readCachedPlatformPageSize, writeCachedPlatformPageSize } from '../../../utils/platformPageSize';
|
|||
|
|
|
|||
|
|
interface UserManagementPageProps {
|
|||
|
|
isZh: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface UserDraft {
|
|||
|
|
username: string;
|
|||
|
|
display_name: string;
|
|||
|
|
password: string;
|
|||
|
|
role_id: number;
|
|||
|
|
is_active: boolean;
|
|||
|
|
bot_ids: string[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const emptyDraft: UserDraft = {
|
|||
|
|
username: '',
|
|||
|
|
display_name: '',
|
|||
|
|
password: '',
|
|||
|
|
role_id: 0,
|
|||
|
|
is_active: true,
|
|||
|
|
bot_ids: [],
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function formatLastLogin(value: string | null | undefined, isZh: boolean) {
|
|||
|
|
if (!value) return isZh ? '从未登录' : 'Never logged in';
|
|||
|
|
const parsed = new Date(value);
|
|||
|
|
if (Number.isNaN(parsed.getTime())) return value;
|
|||
|
|
return parsed.toLocaleString(isZh ? 'zh-CN' : 'en-US', {
|
|||
|
|
hour12: false,
|
|||
|
|
month: '2-digit',
|
|||
|
|
day: '2-digit',
|
|||
|
|
hour: '2-digit',
|
|||
|
|
minute: '2-digit',
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function UserManagementPage({ isZh }: UserManagementPageProps) {
|
|||
|
|
const { notify, confirm } = useLucentPrompt();
|
|||
|
|
const [items, setItems] = useState<SysUserSummary[]>([]);
|
|||
|
|
const [roles, setRoles] = useState<SysRoleSummary[]>([]);
|
|||
|
|
const [bots, setBots] = useState<BotState[]>([]);
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [saving, setSaving] = useState(false);
|
|||
|
|
const [togglingUserId, setTogglingUserId] = useState<number | null>(null);
|
|||
|
|
const [search, setSearch] = useState('');
|
|||
|
|
const [page, setPage] = useState(1);
|
|||
|
|
const [pageSize, setPageSize] = useState(() => readCachedPlatformPageSize(10));
|
|||
|
|
const [editorOpen, setEditorOpen] = useState(false);
|
|||
|
|
const [editingUserId, setEditingUserId] = useState<number | null>(null);
|
|||
|
|
const [draft, setDraft] = useState<UserDraft>(emptyDraft);
|
|||
|
|
|
|||
|
|
const roleOptions = useMemo(
|
|||
|
|
() => roles.filter((role) => role.is_active).sort((a, b) => a.sort_order - b.sort_order || a.id - b.id),
|
|||
|
|
[roles],
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const loadAll = async () => {
|
|||
|
|
setLoading(true);
|
|||
|
|
try {
|
|||
|
|
const [usersRes, rolesRes, botsRes, settingsRes] = await Promise.all([
|
|||
|
|
axios.get<{ items: SysUserSummary[] }>(`${APP_ENDPOINTS.apiBase}/sys/users`),
|
|||
|
|
axios.get<{ items: SysRoleSummary[] }>(`${APP_ENDPOINTS.apiBase}/sys/roles`),
|
|||
|
|
axios.get<BotState[]>(`${APP_ENDPOINTS.apiBase}/bots`),
|
|||
|
|
axios.get<{ page_size?: number }>(`${APP_ENDPOINTS.apiBase}/platform/settings`),
|
|||
|
|
]);
|
|||
|
|
setItems(Array.isArray(usersRes.data?.items) ? usersRes.data.items : []);
|
|||
|
|
setRoles(Array.isArray(rolesRes.data?.items) ? rolesRes.data.items : []);
|
|||
|
|
setBots(Array.isArray(botsRes.data) ? botsRes.data : []);
|
|||
|
|
const normalized = normalizePlatformPageSize(settingsRes.data?.page_size, readCachedPlatformPageSize(10));
|
|||
|
|
writeCachedPlatformPageSize(normalized);
|
|||
|
|
setPageSize(normalized);
|
|||
|
|
} catch (error: any) {
|
|||
|
|
notify(error?.response?.data?.detail || (isZh ? '读取用户管理失败。' : 'Failed to load user management.'), { tone: 'error' });
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
void loadAll();
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
setPage(1);
|
|||
|
|
}, [search, pageSize, items.length]);
|
|||
|
|
|
|||
|
|
const filteredItems = useMemo(() => {
|
|||
|
|
const keyword = search.trim().toLowerCase();
|
|||
|
|
if (!keyword) return items;
|
|||
|
|
return items.filter((item) =>
|
|||
|
|
[item.username, item.display_name, item.role?.name, item.role?.role_key].some((value) =>
|
|||
|
|
String(value || '').toLowerCase().includes(keyword),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}, [items, search]);
|
|||
|
|
|
|||
|
|
const pageCount = Math.max(1, Math.ceil(filteredItems.length / pageSize));
|
|||
|
|
const currentPage = Math.min(page, pageCount);
|
|||
|
|
const pagedItems = useMemo(
|
|||
|
|
() => filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize),
|
|||
|
|
[currentPage, filteredItems, pageSize],
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const openCreate = () => {
|
|||
|
|
setEditingUserId(null);
|
|||
|
|
setDraft({
|
|||
|
|
...emptyDraft,
|
|||
|
|
role_id: roleOptions[0]?.id || 0,
|
|||
|
|
bot_ids: [],
|
|||
|
|
});
|
|||
|
|
setEditorOpen(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const openEdit = (item: SysUserSummary) => {
|
|||
|
|
setEditingUserId(item.id);
|
|||
|
|
setDraft({
|
|||
|
|
username: item.username,
|
|||
|
|
display_name: item.display_name || item.username,
|
|||
|
|
password: '',
|
|||
|
|
role_id: Number(item.role?.id || roleOptions[0]?.id || 0),
|
|||
|
|
is_active: item.is_active !== false,
|
|||
|
|
bot_ids: [...(item.bot_ids || [])],
|
|||
|
|
});
|
|||
|
|
setEditorOpen(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleSave = async () => {
|
|||
|
|
const payload = {
|
|||
|
|
display_name: draft.display_name.trim(),
|
|||
|
|
password: draft.password,
|
|||
|
|
role_id: Number(draft.role_id || 0),
|
|||
|
|
is_active: draft.is_active,
|
|||
|
|
bot_ids: [...draft.bot_ids],
|
|||
|
|
};
|
|||
|
|
if (!payload.display_name) {
|
|||
|
|
notify(isZh ? '请填写显示名称。' : 'Display name is required.', { tone: 'warning' });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!payload.role_id) {
|
|||
|
|
notify(isZh ? '请选择角色。' : 'Select a role.', { tone: 'warning' });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (editingUserId == null) {
|
|||
|
|
if (!draft.username.trim()) {
|
|||
|
|
notify(isZh ? '请填写用户名。' : 'Username is required.', { tone: 'warning' });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (draft.password.trim().length < 6) {
|
|||
|
|
notify(isZh ? '初始化密码至少 6 位。' : 'Initial password must be at least 6 characters.', { tone: 'warning' });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
setSaving(true);
|
|||
|
|
try {
|
|||
|
|
if (editingUserId == null) {
|
|||
|
|
await axios.post(`${APP_ENDPOINTS.apiBase}/sys/users`, {
|
|||
|
|
username: draft.username.trim().toLowerCase(),
|
|||
|
|
...payload,
|
|||
|
|
});
|
|||
|
|
notify(isZh ? '用户已创建。' : 'User created.', { tone: 'success' });
|
|||
|
|
} else {
|
|||
|
|
await axios.put(`${APP_ENDPOINTS.apiBase}/sys/users/${editingUserId}`, payload);
|
|||
|
|
notify(isZh ? '用户已更新。' : 'User updated.', { tone: 'success' });
|
|||
|
|
}
|
|||
|
|
setEditorOpen(false);
|
|||
|
|
setDraft(emptyDraft);
|
|||
|
|
setEditingUserId(null);
|
|||
|
|
await loadAll();
|
|||
|
|
} catch (error: any) {
|
|||
|
|
notify(error?.response?.data?.detail || (isZh ? '保存用户失败。' : 'Failed to save user.'), { tone: 'error' });
|
|||
|
|
} finally {
|
|||
|
|
setSaving(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleToggleActive = async (item: SysUserSummary, checked: boolean) => {
|
|||
|
|
const roleId = Number(item.role?.id || 0);
|
|||
|
|
if (!roleId) {
|
|||
|
|
notify(isZh ? '当前用户未绑定有效角色,无法修改启停状态。' : 'This user has no valid role assigned.', { tone: 'warning' });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
setTogglingUserId(item.id);
|
|||
|
|
try {
|
|||
|
|
await axios.put(`${APP_ENDPOINTS.apiBase}/sys/users/${item.id}`, {
|
|||
|
|
display_name: String(item.display_name || item.username || '').trim() || item.username,
|
|||
|
|
password: '',
|
|||
|
|
role_id: roleId,
|
|||
|
|
is_active: checked,
|
|||
|
|
bot_ids: [...(item.bot_ids || [])],
|
|||
|
|
});
|
|||
|
|
setItems((prev) => prev.map((entry) => (entry.id === item.id ? { ...entry, is_active: checked } : entry)));
|
|||
|
|
notify(checked ? (isZh ? '用户已启用。' : 'User enabled.') : (isZh ? '用户已停用。' : 'User disabled.'), { tone: 'success' });
|
|||
|
|
} catch (error: any) {
|
|||
|
|
notify(error?.response?.data?.detail || (isZh ? '更新用户状态失败。' : 'Failed to update user status.'), { tone: 'error' });
|
|||
|
|
await loadAll();
|
|||
|
|
} finally {
|
|||
|
|
setTogglingUserId(null);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDelete = async (item: SysUserSummary) => {
|
|||
|
|
const ok = await confirm({
|
|||
|
|
title: isZh ? '删除用户' : 'Delete User',
|
|||
|
|
message: isZh ? `确认删除用户 ${item.display_name || item.username}?` : `Delete user ${item.display_name || item.username}?`,
|
|||
|
|
tone: 'warning',
|
|||
|
|
});
|
|||
|
|
if (!ok) return;
|
|||
|
|
try {
|
|||
|
|
await axios.delete(`${APP_ENDPOINTS.apiBase}/sys/users/${item.id}`);
|
|||
|
|
notify(isZh ? '用户已删除。' : 'User deleted.', { tone: 'success' });
|
|||
|
|
await loadAll();
|
|||
|
|
} catch (error: any) {
|
|||
|
|
notify(error?.response?.data?.detail || (isZh ? '删除用户失败。' : 'Failed to delete user.'), { tone: 'error' });
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="platform-page-stack">
|
|||
|
|
<section className="panel stack skill-market-page-shell admin-management-shell">
|
|||
|
|
<div className="skill-market-admin-toolbar">
|
|||
|
|
<div className="ops-searchbar platform-searchbar skill-market-search">
|
|||
|
|
<input
|
|||
|
|
className="input ops-search-input ops-search-input-with-icon"
|
|||
|
|
type="search"
|
|||
|
|
value={search}
|
|||
|
|
onChange={(event) => setSearch(event.target.value)}
|
|||
|
|
placeholder={isZh ? '搜索用户名、显示名或角色...' : 'Search username, display name, or role...'}
|
|||
|
|
aria-label={isZh ? '搜索用户' : 'Search users'}
|
|||
|
|
/>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className="ops-search-inline-btn"
|
|||
|
|
onClick={() => setSearch('')}
|
|||
|
|
title={search.trim() ? (isZh ? '清空搜索' : 'Clear search') : (isZh ? '搜索用户' : 'Search users')}
|
|||
|
|
aria-label={search.trim() ? (isZh ? '清空搜索' : 'Clear search') : (isZh ? '搜索用户' : 'Search users')}
|
|||
|
|
>
|
|||
|
|
{search.trim() ? <X size={14} /> : <Search size={14} />}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="skill-market-admin-actions">
|
|||
|
|
<button className="btn btn-secondary btn-sm" type="button" disabled={loading} onClick={() => void loadAll()}>
|
|||
|
|
{loading ? <RefreshCw size={14} className="animate-spin" /> : <RefreshCw size={14} />}
|
|||
|
|
<span style={{ marginLeft: 6 }}>{isZh ? '刷新' : 'Refresh'}</span>
|
|||
|
|
</button>
|
|||
|
|
<button className="btn btn-primary btn-sm" type="button" onClick={openCreate}>
|
|||
|
|
<Plus size={14} />
|
|||
|
|
<span style={{ marginLeft: 6 }}>{isZh ? '新增用户' : 'Add User'}</span>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="admin-table-shell">
|
|||
|
|
<table className="admin-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>{isZh ? '用户' : 'User'}</th>
|
|||
|
|
<th>{isZh ? '角色' : 'Role'}</th>
|
|||
|
|
<th>{isZh ? 'Bot 绑定' : 'Bot Binding'}</th>
|
|||
|
|
<th>{isZh ? '最近登录' : 'Last Login'}</th>
|
|||
|
|
<th>{isZh ? '状态' : 'Status'}</th>
|
|||
|
|
<th>{isZh ? '操作' : 'Actions'}</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
{pagedItems.length === 0 ? (
|
|||
|
|
<tr>
|
|||
|
|
<td className="admin-table-empty" colSpan={6}>
|
|||
|
|
{filteredItems.length === 0
|
|||
|
|
? (isZh ? '当前没有可展示的用户。' : 'No users available right now.')
|
|||
|
|
: (isZh ? '当前页没有用户。' : 'No users on this page.')}
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
) : (
|
|||
|
|
pagedItems.map((item) => {
|
|||
|
|
const busy = togglingUserId === item.id;
|
|||
|
|
return (
|
|||
|
|
<tr key={item.id}>
|
|||
|
|
<td>
|
|||
|
|
<div className="admin-table-name">
|
|||
|
|
<strong>{item.display_name || item.username}</strong>
|
|||
|
|
<span className="mono">{item.username}</span>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td>
|
|||
|
|
<div className="admin-table-secondary">
|
|||
|
|
<strong>{item.role?.name || (isZh ? '未分配角色' : 'No role')}</strong>
|
|||
|
|
<span className="mono">{item.role?.role_key || '-'}</span>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td>
|
|||
|
|
<div className="admin-table-secondary">
|
|||
|
|
<strong>{isZh ? `已绑定 ${item.bot_ids?.length || 0} 个` : `${item.bot_ids?.length || 0} bound`}</strong>
|
|||
|
|
<span className="mono">{(item.bot_ids || []).slice(0, 2).join(', ') || '-'}</span>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td>{formatLastLogin(item.last_login_at, isZh)}</td>
|
|||
|
|
<td>
|
|||
|
|
<div className="admin-status-cell">
|
|||
|
|
<label className="admin-switch" title={item.is_active ? (isZh ? '停用' : 'Disable') : (isZh ? '启用' : 'Enable')}>
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={item.is_active}
|
|||
|
|
disabled={busy}
|
|||
|
|
onChange={(event) => void handleToggleActive(item, event.target.checked)}
|
|||
|
|
/>
|
|||
|
|
<span className="admin-switch-track" />
|
|||
|
|
</label>
|
|||
|
|
<span>{item.is_active ? (isZh ? '启用' : 'Active') : (isZh ? '停用' : 'Disabled')}</span>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td>
|
|||
|
|
<div className="admin-table-actions">
|
|||
|
|
<LucentIconButton
|
|||
|
|
className="btn btn-secondary btn-sm icon-btn"
|
|||
|
|
onClick={() => openEdit(item)}
|
|||
|
|
tooltip={isZh ? '编辑用户' : 'Edit user'}
|
|||
|
|
aria-label={isZh ? '编辑用户' : 'Edit user'}
|
|||
|
|
>
|
|||
|
|
<Pencil size={14} />
|
|||
|
|
</LucentIconButton>
|
|||
|
|
<LucentIconButton
|
|||
|
|
className="btn btn-danger btn-sm icon-btn"
|
|||
|
|
onClick={() => void handleDelete(item)}
|
|||
|
|
tooltip={isZh ? '删除用户' : 'Delete user'}
|
|||
|
|
aria-label={isZh ? '删除用户' : 'Delete user'}
|
|||
|
|
>
|
|||
|
|
<Trash2 size={14} />
|
|||
|
|
</LucentIconButton>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
);
|
|||
|
|
})
|
|||
|
|
)}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="skill-market-pager">
|
|||
|
|
<span className="pager-status">
|
|||
|
|
{isZh ? `第 ${currentPage} / ${pageCount} 页,共 ${filteredItems.length} 个用户` : `Page ${currentPage} / ${pageCount}, ${filteredItems.length} users`}
|
|||
|
|
</span>
|
|||
|
|
<div className="platform-usage-pager-actions">
|
|||
|
|
<LucentIconButton
|
|||
|
|
className="btn btn-secondary btn-sm icon-btn pager-icon-btn"
|
|||
|
|
type="button"
|
|||
|
|
disabled={currentPage <= 1}
|
|||
|
|
onClick={() => setPage((value) => Math.max(1, value - 1))}
|
|||
|
|
tooltip={isZh ? '上一页' : 'Previous'}
|
|||
|
|
aria-label={isZh ? '上一页' : 'Previous'}
|
|||
|
|
>
|
|||
|
|
<ChevronLeft size={16} />
|
|||
|
|
</LucentIconButton>
|
|||
|
|
<LucentIconButton
|
|||
|
|
className="btn btn-secondary btn-sm icon-btn pager-icon-btn"
|
|||
|
|
type="button"
|
|||
|
|
disabled={currentPage >= pageCount}
|
|||
|
|
onClick={() => setPage((value) => Math.min(pageCount, value + 1))}
|
|||
|
|
tooltip={isZh ? '下一页' : 'Next'}
|
|||
|
|
aria-label={isZh ? '下一页' : 'Next'}
|
|||
|
|
>
|
|||
|
|
<ChevronRight size={16} />
|
|||
|
|
</LucentIconButton>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<LucentDrawer
|
|||
|
|
open={editorOpen}
|
|||
|
|
size="default"
|
|||
|
|
title={editingUserId == null ? (isZh ? '新增用户' : 'Add User') : (isZh ? '编辑用户' : 'Edit User')}
|
|||
|
|
description={isZh ? '统一使用 Drawer 管理账号、角色和启停状态。' : 'Use the same Drawer interaction to manage account profile, role, and status.'}
|
|||
|
|
onClose={() => setEditorOpen(false)}
|
|||
|
|
bodyClassName="admin-management-drawer-body"
|
|||
|
|
closeLabel={isZh ? '关闭面板' : 'Close panel'}
|
|||
|
|
footer={(
|
|||
|
|
<div className="row-between">
|
|||
|
|
<div className="field-label">
|
|||
|
|
{editingUserId == null
|
|||
|
|
? (isZh ? '创建后可继续在列表中修改密码、角色和启停状态。' : 'After creation, password, role, and status can be updated from the list.')
|
|||
|
|
: (isZh ? '留空密码表示保持原密码不变。' : 'Leave password empty to keep the existing password.')}
|
|||
|
|
</div>
|
|||
|
|
<button className="btn btn-primary" type="button" onClick={() => void handleSave()} disabled={saving}>
|
|||
|
|
{saving ? <RefreshCw size={14} className="animate-spin" /> : <UserRound size={14} />}
|
|||
|
|
<span style={{ marginLeft: 6 }}>{saving ? (isZh ? '保存中...' : 'Saving...') : (editingUserId == null ? (isZh ? '创建用户' : 'Create User') : (isZh ? '保存修改' : 'Save Changes'))}</span>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
<section className="platform-node-native-panel">
|
|||
|
|
<div className="platform-node-native-panel-title">{isZh ? '账户信息' : 'Account Details'}</div>
|
|||
|
|
<div className="platform-node-editor-grid">
|
|||
|
|
<label className="field">
|
|||
|
|
<span className="field-label">{isZh ? '用户名' : 'Username'}</span>
|
|||
|
|
<input
|
|||
|
|
className="input mono"
|
|||
|
|
value={draft.username}
|
|||
|
|
disabled={editingUserId != null}
|
|||
|
|
onChange={(event) => setDraft((prev) => ({ ...prev, username: event.target.value.trim().toLowerCase() }))}
|
|||
|
|
placeholder={isZh ? '例如 admin_ops' : 'For example: admin_ops'}
|
|||
|
|
/>
|
|||
|
|
</label>
|
|||
|
|
<label className="field">
|
|||
|
|
<span className="field-label">{isZh ? '显示名称' : 'Display Name'}</span>
|
|||
|
|
<input
|
|||
|
|
className="input"
|
|||
|
|
value={draft.display_name}
|
|||
|
|
onChange={(event) => setDraft((prev) => ({ ...prev, display_name: event.target.value }))}
|
|||
|
|
placeholder={isZh ? '例如 运维管理员' : 'For example: Ops Admin'}
|
|||
|
|
/>
|
|||
|
|
</label>
|
|||
|
|
<label className="field">
|
|||
|
|
<span className="field-label">{editingUserId == null ? (isZh ? '初始化密码' : 'Initial Password') : (isZh ? '重置密码' : 'Reset Password')}</span>
|
|||
|
|
<input
|
|||
|
|
className="input"
|
|||
|
|
type="password"
|
|||
|
|
value={draft.password}
|
|||
|
|
onChange={(event) => setDraft((prev) => ({ ...prev, password: event.target.value }))}
|
|||
|
|
placeholder={editingUserId == null ? (isZh ? '至少 6 位' : 'At least 6 characters') : (isZh ? '留空则不修改' : 'Leave empty to keep unchanged')}
|
|||
|
|
/>
|
|||
|
|
</label>
|
|||
|
|
<label className="field">
|
|||
|
|
<span className="field-label">{isZh ? '所属角色' : 'Role'}</span>
|
|||
|
|
<select
|
|||
|
|
className="input"
|
|||
|
|
value={draft.role_id || ''}
|
|||
|
|
onChange={(event) => setDraft((prev) => ({ ...prev, role_id: Number(event.target.value || 0) }))}
|
|||
|
|
>
|
|||
|
|
<option value="">{isZh ? '请选择角色' : 'Select role'}</option>
|
|||
|
|
{roleOptions.map((role) => (
|
|||
|
|
<option key={role.id} value={role.id}>
|
|||
|
|
{role.name}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</label>
|
|||
|
|
<label className="field platform-node-editor-span-2">
|
|||
|
|
<span className="field-label">{isZh ? 'Bot 绑定' : 'Bot Bindings'}</span>
|
|||
|
|
<div className="admin-user-bot-grid">
|
|||
|
|
{bots.length === 0 ? (
|
|||
|
|
<div className="field-label">{isZh ? '暂无可绑定 Bot。' : 'No bots available for binding.'}</div>
|
|||
|
|
) : bots.map((bot) => {
|
|||
|
|
const checked = draft.bot_ids.includes(bot.id);
|
|||
|
|
return (
|
|||
|
|
<label key={bot.id} className={`admin-user-bot-option ${checked ? 'is-active' : ''}`}>
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={checked}
|
|||
|
|
onChange={(event) => {
|
|||
|
|
setDraft((prev) => ({
|
|||
|
|
...prev,
|
|||
|
|
bot_ids: event.target.checked
|
|||
|
|
? Array.from(new Set([...prev.bot_ids, bot.id]))
|
|||
|
|
: prev.bot_ids.filter((item) => item !== bot.id),
|
|||
|
|
}));
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<span className="admin-user-bot-option-copy">
|
|||
|
|
<strong>{bot.name || bot.id}</strong>
|
|||
|
|
<small className="mono">{bot.id}</small>
|
|||
|
|
</span>
|
|||
|
|
</label>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</label>
|
|||
|
|
<label className="field platform-node-editor-span-2">
|
|||
|
|
<span className="field-label">{isZh ? '启停状态' : 'Status'}</span>
|
|||
|
|
<div className="admin-switch-row">
|
|||
|
|
<label className="admin-switch" title={draft.is_active ? (isZh ? '停用' : 'Disable') : (isZh ? '启用' : 'Enable')}>
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={draft.is_active}
|
|||
|
|
onChange={(event) => setDraft((prev) => ({ ...prev, is_active: event.target.checked }))}
|
|||
|
|
/>
|
|||
|
|
<span className="admin-switch-track" />
|
|||
|
|
</label>
|
|||
|
|
<span>{draft.is_active ? (isZh ? '启用' : 'Active') : (isZh ? '停用' : 'Disabled')}</span>
|
|||
|
|
</div>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
</LucentDrawer>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|