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
|
||||
func CreateInstanceGroup(c *gin.Context) {
|
||||
var group model.InstanceGroup
|
||||
if err := c.ShouldBindJSON(&group); err != nil {
|
||||
var input struct {
|
||||
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()})
|
||||
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 {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
@@ -257,10 +257,13 @@ export default function InstanceGroupPage() {
|
||||
const close = () => { setShowCreateModal(false); setShowEditModal(false); resetForm(); };
|
||||
const submit = mode === 'create' ? handleCreate : handleEdit;
|
||||
return show ? (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<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()}>
|
||||
<h3 className="text-lg font-semibold mb-5">{mode === 'create' ? '新建权限策略' : '编辑权限策略'}</h3>
|
||||
<div className="space-y-5">
|
||||
<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 shadow-2xl flex flex-col max-h-[90vh] border border-[var(--dg-border)]" onClick={e => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--dg-border)]">
|
||||
<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>
|
||||
<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 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={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={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="dg-btn dg-btn-primary disabled:opacity-50">
|
||||
{mode === 'create' ? '创建' : '保存'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -438,9 +441,13 @@ export default function InstanceGroupPage() {
|
||||
|
||||
{/* User Modal */}
|
||||
{showUserModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="w-full max-w-md bg-[var(--dg-bg-primary)] rounded-xl p-6 shadow-2xl" onClick={e => e.stopPropagation()}>
|
||||
<h3 className="text-lg font-semibold mb-4">关联用户</h3>
|
||||
<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 shadow-2xl border border-[var(--dg-border)]" onClick={e => e.stopPropagation()}>
|
||||
<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">
|
||||
<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" />
|
||||
@@ -456,9 +463,10 @@ export default function InstanceGroupPage() {
|
||||
))}
|
||||
</div>
|
||||
<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={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={() => 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="dg-btn dg-btn-primary disabled:opacity-50">确认 ({selectedUserIds.length})</button>
|
||||
</div>
|
||||
</div>{/* end px-6 py-4 */}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user