-import { ref, reactive, computed, onMounted } from 'vue'
+import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { useAuthStore } from '@/stores/auth'
@@ -202,7 +202,56 @@ const cols = computed(() => {
})
const handleEdit = (a: Account) => { edAcc.value = a; showEdit.value = true }
-const openMenu = (a: Account, e: MouseEvent) => { menu.acc = a; menu.pos = { top: e.clientY, left: e.clientX - 200 }; menu.show = true }
+const openMenu = (a: Account, e: MouseEvent) => {
+ menu.acc = a
+
+ const target = e.currentTarget as HTMLElement
+ if (target) {
+ const rect = target.getBoundingClientRect()
+ const menuWidth = 200
+ const menuHeight = 240
+ const padding = 8
+ const viewportWidth = window.innerWidth
+ const viewportHeight = window.innerHeight
+
+ let left, top
+
+ if (viewportWidth < 768) {
+ // 居中显示,水平位置
+ left = Math.max(padding, Math.min(
+ rect.left + rect.width / 2 - menuWidth / 2,
+ viewportWidth - menuWidth - padding
+ ))
+
+ // 优先显示在按钮下方
+ top = rect.bottom + 4
+
+ // 如果下方空间不够,显示在上方
+ if (top + menuHeight > viewportHeight - padding) {
+ top = rect.top - menuHeight - 4
+ // 如果上方也不够,就贴在视口顶部
+ if (top < padding) {
+ top = padding
+ }
+ }
+ } else {
+ left = Math.max(padding, Math.min(
+ e.clientX - menuWidth,
+ viewportWidth - menuWidth - padding
+ ))
+ top = e.clientY
+ if (top + menuHeight > viewportHeight - padding) {
+ top = viewportHeight - menuHeight - padding
+ }
+ }
+
+ menu.pos = { top, left }
+ } else {
+ menu.pos = { top: e.clientY, left: e.clientX - 200 }
+ }
+
+ menu.show = true
+}
const toggleSel = (id: number) => { const i = selIds.value.indexOf(id); if(i === -1) selIds.value.push(id); else selIds.value.splice(i, 1) }
const selectPage = () => { selIds.value = [...new Set([...selIds.value, ...accounts.value.map(a => a.id)])] }
const handleBulkDelete = async () => { if(!confirm(t('common.confirm'))) return; try { await Promise.all(selIds.value.map(id => adminAPI.accounts.delete(id))); selIds.value = []; reload() } catch (error) { console.error('Failed to bulk delete accounts:', error) } }
@@ -360,5 +409,14 @@ const isExpired = (value: number | null) => {
return value * 1000 <= Date.now()
}
-onMounted(async () => { load(); try { const [p, g] = await Promise.all([adminAPI.proxies.getAll(), adminAPI.groups.getAll()]); proxies.value = p; groups.value = g } catch (error) { console.error('Failed to load proxies/groups:', error) } })
+// 滚动时关闭菜单
+const handleScroll = () => {
+ menu.show = false
+}
+
+onMounted(async () => { load(); try { const [p, g] = await Promise.all([adminAPI.proxies.getAll(), adminAPI.groups.getAll()]); proxies.value = p; groups.value = g } catch (error) { console.error('Failed to load proxies/groups:', error) }; window.addEventListener('scroll', handleScroll, true) })
+
+onUnmounted(() => {
+ window.removeEventListener('scroll', handleScroll, true)
+})
diff --git a/frontend/src/views/admin/UsersView.vue b/frontend/src/views/admin/UsersView.vue
index 838378a5..4a8dba30 100644
--- a/frontend/src/views/admin/UsersView.vue
+++ b/frontend/src/views/admin/UsersView.vue
@@ -3,11 +3,11 @@
-
+
-
+
-
+
-
-
-
-
-
+
+
+
+
-
-
-
+
+
-
+
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-