fix: 权限组穿梭框保存不生效 + 弹框布局修复

1. 后端 CreateInstanceGroup 改用 input struct 接收 readonly_instance_ids/write_instance_ids
   - 旧代码直接绑定 model.InstanceGroup(不含关联ID字段),导致穿梭框数据被丢弃
   - 新代码解析后写入 instance_group_members 表
   - 与 UpdateInstanceGroup 保持一致的逻辑

2. 弹框布局修复
   - z-50 → z-[100] 避免被顶部栏遮挡
   - flex-col + overflow-y-auto 独立滚动区域
   - 添加 header bar + X 关闭按钮
   - 背景加 backdrop-blur-sm

3. 全局排查确认:其他 handler 均使用 input struct 或 map,无同类问题
This commit is contained in:
logan committed 2026-05-27 02:38:18 +08:00
1 parent 746a000e8a
commit ed582e30e6
2 files changed
+61 -13

No files matched your search

+42 -2
View File
@@ -25,15 +25,55 @@ func GetInstanceGroup(c *gin.Context) {
// POST /api/v1/instance-groups // POST /api/v1/instance-groups
func CreateInstanceGroup(c *gin.Context) { func CreateInstanceGroup(c *gin.Context) {
var group model.InstanceGroup var input struct {
if err := c.ShouldBindJSON(&group); err != nil { Name string `json:"name"`
Description *string `json:"description"`
DBType string `json:"db_type"`
IsActive *bool `json:"is_active"`
ReadonlyInstanceIDs []int `json:"readonly_instance_ids"`
WriteInstanceIDs []int `json:"write_instance_ids"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return return
} }
desc := ""
if input.Description != nil {
desc = *input.Description
}
isActive := true
if input.IsActive != nil {
isActive = *input.IsActive
}
dbType := input.DBType
if dbType == "" {
dbType = "all"
}
group := model.InstanceGroup{
Name: input.Name,
Description: &desc,
DBType: dbType,
IsActive: &isActive,
}
if err := model.DB.Create(&group).Error; err != nil { if err := model.DB.Create(&group).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
return return
} }
// Create instance members
for _, iid := range input.ReadonlyInstanceIDs {
model.DB.Create(&model.InstanceGroupMember{
GroupID: group.ID, InstanceID: iid, AccessType: "readonly",
})
}
for _, iid := range input.WriteInstanceIDs {
model.DB.Create(&model.InstanceGroupMember{
GroupID: group.ID, InstanceID: iid, AccessType: "write",
})
}
c.JSON(http.StatusCreated, enrichGroup(group)) c.JSON(http.StatusCreated, enrichGroup(group))
} }
+19 -11
View File
@@ -257,10 +257,13 @@ export default function InstanceGroupPage() {
const close = () => { setShowCreateModal(false); setShowEditModal(false); resetForm(); }; const close = () => { setShowCreateModal(false); setShowEditModal(false); resetForm(); };
const submit = mode === 'create' ? handleCreate : handleEdit; const submit = mode === 'create' ? handleCreate : handleEdit;
return show ? ( return show ? (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> <div className="fixed inset-0 z-[100] flex items-start justify-center pt-[5vh] bg-black/60 backdrop-blur-sm">
<div className="w-full max-w-3xl bg-[var(--dg-bg-primary)] rounded-xl p-6 shadow-2xl max-h-[90vh] overflow-y-auto" onClick={e => e.stopPropagation()}> <div className="w-full max-w-3xl bg-[var(--dg-bg-primary)] rounded-xl shadow-2xl flex flex-col max-h-[90vh] border border-[var(--dg-border)]" onClick={e => e.stopPropagation()}>
<h3 className="text-lg font-semibold mb-5">{mode === 'create' ? '新建权限策略' : '编辑权限策略'}</h3> <div className="flex items-center justify-between px-6 py-4 border-b border-[var(--dg-border)]">
<div className="space-y-5"> <h3 className="text-lg font-semibold">{mode === 'create' ? '新建权限策略' : '编辑权限策略'}</h3>
<button onClick={close} className="p-1.5 rounded-lg hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)] transition-colors"><X className="w-4 h-4" /></button>
</div>
<div className="overflow-y-auto px-6 py-5 space-y-5 flex-1">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium mb-1.5">策略名称 <span className="text-red-400">*</span></label> <label className="block text-sm font-medium mb-1.5">策略名称 <span className="text-red-400">*</span></label>
@@ -293,8 +296,8 @@ export default function InstanceGroupPage() {
</div> </div>
<div className="flex justify-end gap-3 pt-2"> <div className="flex justify-end gap-3 pt-2">
<button onClick={close} className="px-4 py-2 text-sm text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] transition-colors">取消</button> <button onClick={close} className="px-4 py-2 text-sm text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] transition-colors rounded-lg hover:bg-[var(--dg-bg-hover)]">取消</button>
<button onClick={submit} disabled={!formName.trim()} className="px-4 py-2 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 transition-colors"> <button onClick={submit} disabled={!formName.trim()} className="dg-btn dg-btn-primary disabled:opacity-50">
{mode === 'create' ? '创建' : '保存'} {mode === 'create' ? '创建' : '保存'}
</button> </button>
</div> </div>
@@ -438,9 +441,13 @@ export default function InstanceGroupPage() {
{/* User Modal */} {/* User Modal */}
{showUserModal && ( {showUserModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> <div className="fixed inset-0 z-[100] flex items-start justify-center pt-[10vh] bg-black/60 backdrop-blur-sm">
<div className="w-full max-w-md bg-[var(--dg-bg-primary)] rounded-xl p-6 shadow-2xl" onClick={e => e.stopPropagation()}> <div className="w-full max-w-md bg-[var(--dg-bg-primary)] rounded-xl shadow-2xl border border-[var(--dg-border)]" onClick={e => e.stopPropagation()}>
<h3 className="text-lg font-semibold mb-4">关联用户</h3> <div className="flex items-center justify-between px-6 py-4 border-b border-[var(--dg-border)]">
<h3 className="text-lg font-semibold">关联用户</h3>
<button onClick={() => setShowUserModal(false)} className="p-1.5 rounded-lg hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)] transition-colors"><X className="w-4 h-4" /></button>
</div>
<div className="px-6 py-4">
<div className="relative mb-3"> <div className="relative mb-3">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--dg-text-muted)]" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--dg-text-muted)]" />
<input value={userSearch} onChange={e => setUserSearch(e.target.value)} placeholder="搜索用户..." className="w-full pl-10 pr-4 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" /> <input value={userSearch} onChange={e => setUserSearch(e.target.value)} placeholder="搜索用户..." className="w-full pl-10 pr-4 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" />
@@ -456,9 +463,10 @@ export default function InstanceGroupPage() {
))} ))}
</div> </div>
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
<button onClick={() => setShowUserModal(false)} className="px-4 py-2 text-sm text-[var(--dg-text-secondary)]">取消</button> <button onClick={() => setShowUserModal(false)} className="px-4 py-2 text-sm text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] transition-colors rounded-lg hover:bg-[var(--dg-bg-hover)]">取消</button>
<button onClick={handleAssignUsers} disabled={selectedUserIds.length === 0} className="px-4 py-2 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50">确认 ({selectedUserIds.length})</button> <button onClick={handleAssignUsers} disabled={selectedUserIds.length === 0} className="dg-btn dg-btn-primary disabled:opacity-50">确认 ({selectedUserIds.length})</button>
</div> </div>
</div>{/* end px-6 py-4 */}
</div> </div>
</div> </div>
)} )}