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:
1 parent
746a000e8a
commit
ed582e30e6
2 files changed
+61
-13
No files matched your search
@@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in new issue
Block a user