feat: 权限组重构为一组双Transfer(只读+写入) + 用户管理加权限组关联
后端: - 新增 instance_group_members 中间表 (group_id + instance_id + access_type) - 一个权限组同时包含 readonly 和 write 两类实例 - API create/update 支持 readonly_instance_ids + write_instance_ids - Schema Response 返回 readonly_instances + write_instances 分类型列表 - 20 个测试全部通过 前端: - InstanceGroupPage: 创建/编辑都是上下两个 Transfer 同时配置只读和写入 - 列表卡片显示 [N只读 · M写入] 标签 - 详情页分只读实例和写入实例两块展示 - UsersPage 编辑用户弹框加权限组多选 - instanceGroup.ts API 适配新接口
This commit is contained in:
1 parent
39d9600e82
commit
88d064ea95
3 files changed
+196
-181
No files matched your search
@@ -6,6 +6,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { userApi } from '@/api/user';
|
||||
import { instanceGroupApi, type InstanceGroup } from '@/api/instanceGroup';
|
||||
|
||||
/* ───────── Types ───────── */
|
||||
|
||||
@@ -636,11 +637,32 @@ function EditUserModal({ user, onClose, onUpdated, setToast }: { user: UserItem;
|
||||
const [role, setRole] = useState(user.role);
|
||||
const [isActive, setIsActive] = useState(user.is_active);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [groups, setGroups] = useState<InstanceGroup[]>([]);
|
||||
const [selectedGroupIds, setSelectedGroupIds] = useState<number[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
instanceGroupApi.list().then((data: InstanceGroup[]) => {
|
||||
setGroups(data);
|
||||
// Reverse lookup: find which groups contain this user
|
||||
const matched = data
|
||||
.filter(g => g.users?.some(u => u.id === user.id))
|
||||
.map(g => g.id);
|
||||
setSelectedGroupIds(matched);
|
||||
}).catch(() => {});
|
||||
}, [user.id]);
|
||||
|
||||
const toggleGroup = (gid: number) => {
|
||||
setSelectedGroupIds(prev => prev.includes(gid) ? prev.filter(id => id !== gid) : [...prev, gid]);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await userApi.update(user.id, { display_name: displayName, email: email || undefined, role, is_active: isActive });
|
||||
// Update group associations
|
||||
if (selectedGroupIds.length >= 0) {
|
||||
await instanceGroupApi.updateUserGroups(user.id, selectedGroupIds);
|
||||
}
|
||||
setToast({ message: '用户更新成功', type: 'success' });
|
||||
onUpdated();
|
||||
} catch {
|
||||
@@ -684,6 +706,22 @@ function EditUserModal({ user, onClose, onUpdated, setToast }: { user: UserItem;
|
||||
<option value="guest">访客</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">权限组</label>
|
||||
{groups.length === 0 ? (
|
||||
<div className="text-xs text-[var(--dg-text-muted)] py-2">暂无可选权限组</div>
|
||||
) : (
|
||||
<div className="space-y-1.5 max-h-36 overflow-y-auto">
|
||||
{groups.map(g => (
|
||||
<label key={g.id} className={`flex items-center gap-2.5 px-3 py-2 rounded-lg cursor-pointer transition-colors text-sm ${selectedGroupIds.includes(g.id) ? 'bg-blue-500/10 text-blue-400' : 'hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]'}`}>
|
||||
<input type="checkbox" checked={selectedGroupIds.includes(g.id)} onChange={() => toggleGroup(g.id)} className="rounded" />
|
||||
<span className="flex-1 truncate">{g.name}</span>
|
||||
<span className="text-[10px] text-[var(--dg-text-muted)]">{g.readonly_instance_count || 0}R / {g.write_instance_count || 0}W</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setIsActive(!isActive)}
|
||||
|
||||
Reference in new issue
Block a user