fix: 修复代码审查报告中的4个关键问题
1. 资源管理冗余(ForwardGemini双重Close) - 错误分支读取body后立即关闭原始body,用内存副本重新包装 - defer添加nil guard,避免重复关闭 - fallback成功时显式关闭旧body,确保连接释放 2. Schema校验丢失(cleanJSONSchema移除字段无感知) - 新增schemaCleaningWarningsEnabled()支持环境变量控制 - 实现warnSchemaKeyRemovedOnce()在非release模式下告警 - 移除关键验证字段时输出warning,包含key和path 3. UI响应式风险(UsersView操作菜单硬编码定位) - 菜单改为先粗定位、渲染后测量、再clamp到视口内 - 添加max-height + overflow-auto,超出时可滚动 - 增强交互:点击其它位置/滚动/resize自动关闭或重新定位 4. 身份补丁干扰(TransformClaudeToGemini默认注入) - 新增TransformOptions + TransformClaudeToGeminiWithOptions - 系统设置新增enable_identity_patch、identity_patch_prompt - 完整打通handler/dto/service/frontend配置链路 - 默认保持启用,向后兼容现有行为 测试: - 后端单测全量通过:go test ./... - 前端类型检查通过:npm run typecheck
This commit is contained in:
@@ -34,6 +34,10 @@ export interface SystemSettings {
|
||||
turnstile_enabled: boolean
|
||||
turnstile_site_key: string
|
||||
turnstile_secret_key: string
|
||||
|
||||
// Identity patch configuration (Claude -> Gemini)
|
||||
enable_identity_patch: boolean
|
||||
identity_patch_prompt: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -756,7 +756,10 @@ const form = reactive<SystemSettings>({
|
||||
// Cloudflare Turnstile
|
||||
turnstile_enabled: false,
|
||||
turnstile_site_key: '',
|
||||
turnstile_secret_key: ''
|
||||
turnstile_secret_key: '',
|
||||
// Identity patch (Claude -> Gemini)
|
||||
enable_identity_patch: true,
|
||||
identity_patch_prompt: ''
|
||||
})
|
||||
|
||||
function handleLogoUpload(event: Event) {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</TablePageLayout>
|
||||
|
||||
<Teleport to="body">
|
||||
<div v-if="activeMenuId !== null && menuPosition" class="action-menu-content fixed z-[9999] w-48 overflow-hidden rounded-xl bg-white shadow-lg ring-1 ring-black/5 dark:bg-dark-800" :style="{ top: menuPosition.top + 'px', left: menuPosition.left + 'px' }">
|
||||
<div v-if="activeMenuId !== null && menuPosition" ref="actionMenuEl" class="action-menu-content fixed z-[9999] w-48 max-h-[calc(100vh-16px)] overflow-auto rounded-xl bg-white shadow-lg ring-1 ring-black/5 dark:bg-dark-800" :style="{ top: menuPosition.top + 'px', left: menuPosition.left + 'px' }">
|
||||
<div class="py-1">
|
||||
<template v-for="user in users" :key="user.id">
|
||||
<template v-if="user.id === activeMenuId">
|
||||
@@ -63,7 +63,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'; import { useAppStore } from '@/stores/app'
|
||||
import { adminAPI } from '@/api/admin'; import { useTableLoader } from '@/composables/useTableLoader'
|
||||
import type { User } from '@/types'
|
||||
@@ -83,6 +83,7 @@ const { items: users, loading, params, pagination, load, reload, handlePageChang
|
||||
const showCreateModal = ref(false); const showEditModal = ref(false); const showDeleteDialog = ref(false); const showApiKeysModal = ref(false)
|
||||
const editingUser = ref<User | null>(null); const deletingUser = ref<User | null>(null); const viewingUser = ref<User | null>(null)
|
||||
const activeMenuId = ref<number | null>(null); const menuPosition = ref<{ top: number; left: number } | null>(null)
|
||||
const actionMenuEl = ref<HTMLElement | null>(null)
|
||||
const showAllowedGroupsModal = ref(false); const allowedGroupsUser = ref<User | null>(null); const showBalanceModal = ref(false); const balanceUser = ref<User | null>(null); const balanceOperation = ref<'add' | 'subtract'>('add')
|
||||
const columns = computed(() => [{ key: 'email', label: t('admin.users.columns.user'), sortable: true }, { key: 'role', label: t('admin.users.columns.role'), sortable: true }, { key: 'balance', label: t('admin.users.columns.balance'), sortable: true }, { key: 'status', label: t('admin.users.columns.status'), sortable: true }, { key: 'actions', label: t('admin.users.columns.actions') }])
|
||||
|
||||
@@ -98,7 +99,47 @@ const handleDeposit = (u: User) => { balanceUser.value = u; balanceOperation.val
|
||||
const handleWithdraw = (u: User) => { balanceUser.value = u; balanceOperation.value = 'subtract'; showBalanceModal.value = true }
|
||||
const closeBalanceModal = () => { showBalanceModal.value = false; balanceUser.value = null }
|
||||
const handleToggleStatus = async (user: User) => { const next = user.status === 'active' ? 'disabled' : 'active'; try { await adminAPI.users.toggleStatus(user.id, next as any); appStore.showSuccess(t('common.success')); load() } catch {} }
|
||||
const openActionMenu = (u: User, e: MouseEvent) => { if (activeMenuId.value === u.id) { activeMenuId.value = null; menuPosition.value = null } else { activeMenuId.value = u.id; menuPosition.value = { top: e.clientY, left: e.clientX - 150 } } }
|
||||
const repositionActionMenu = (triggerRect?: DOMRect) => {
|
||||
if (!menuPosition.value || !actionMenuEl.value) return
|
||||
const rect = actionMenuEl.value.getBoundingClientRect()
|
||||
const margin = 8
|
||||
let top = menuPosition.value.top
|
||||
let left = menuPosition.value.left
|
||||
|
||||
if (triggerRect) {
|
||||
const spaceBelow = window.innerHeight - triggerRect.bottom
|
||||
const spaceAbove = triggerRect.top
|
||||
if (rect.height > spaceBelow && spaceAbove > spaceBelow) top = Math.max(margin, triggerRect.top - rect.height - 4)
|
||||
}
|
||||
|
||||
if (left + rect.width + margin > window.innerWidth) left = window.innerWidth - rect.width - margin
|
||||
if (left < margin) left = margin
|
||||
if (top + rect.height + margin > window.innerHeight) top = window.innerHeight - rect.height - margin
|
||||
if (top < margin) top = margin
|
||||
|
||||
menuPosition.value = { top, left }
|
||||
}
|
||||
const openActionMenu = async (u: User, e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (activeMenuId.value === u.id) { closeActionMenu(); return }
|
||||
|
||||
const actionMenuWidthPx = 192 // w-48
|
||||
const triggerEl = e.currentTarget as HTMLElement | null
|
||||
const triggerRect = triggerEl?.getBoundingClientRect()
|
||||
|
||||
activeMenuId.value = u.id
|
||||
if (triggerRect) menuPosition.value = { top: triggerRect.bottom + 4, left: triggerRect.right - actionMenuWidthPx }
|
||||
else menuPosition.value = { top: e.clientY, left: e.clientX - actionMenuWidthPx }
|
||||
|
||||
await nextTick()
|
||||
repositionActionMenu(triggerRect)
|
||||
}
|
||||
const closeActionMenu = () => { activeMenuId.value = null; menuPosition.value = null }
|
||||
onMounted(load)
|
||||
|
||||
const handleDocumentClick = (evt: MouseEvent) => { if (activeMenuId.value === null) return; const target = evt.target as Node | null; if (target && actionMenuEl.value?.contains(target)) return; closeActionMenu() }
|
||||
const handleWindowResize = () => repositionActionMenu()
|
||||
const handleAnyScroll = () => closeActionMenu()
|
||||
|
||||
onMounted(() => { load(); document.addEventListener('click', handleDocumentClick); window.addEventListener('resize', handleWindowResize); window.addEventListener('scroll', handleAnyScroll, true) })
|
||||
onBeforeUnmount(() => { document.removeEventListener('click', handleDocumentClick); window.removeEventListener('resize', handleWindowResize); window.removeEventListener('scroll', handleAnyScroll, true) })
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user