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:
logan committed 2026-05-24 20:08:31 +08:00
1 parent 39d9600e82
commit 88d064ea95
3 files changed
+196 -181

No files matched your search

+38
View File
@@ -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)}