feat: 优化 OAuth 账号导入流程

This commit is contained in:
shaw
2026-05-08 11:36:09 +08:00
parent a466e80ed6
commit fda1ed459d
16 changed files with 1900 additions and 74 deletions
+129 -58
View File
@@ -14,7 +14,6 @@
<AccountTableActions
:loading="loading"
@refresh="handleManualRefresh"
@sync="showSync = true"
@create="showCreate = true"
>
<template #after>
@@ -23,7 +22,7 @@
<button
@click="
showAutoRefreshDropdown = !showAutoRefreshDropdown;
showColumnDropdown = false
showAccountToolsDropdown = false
"
class="btn btn-secondary px-2 md:px-3"
:title="t('admin.accounts.autoRefresh')"
@@ -63,68 +62,100 @@
</div>
</div>
<!-- Error Passthrough Rules -->
<button
@click="showErrorPassthrough = true"
class="btn btn-secondary"
:title="t('admin.errorPassthrough.title')"
>
<Icon name="shield" size="md" class="mr-1.5" />
<span class="hidden md:inline">{{ t('admin.errorPassthrough.title') }}</span>
</button>
<!-- TLS Fingerprint Profiles -->
<button
@click="showTLSFingerprintProfiles = true"
class="btn btn-secondary"
:title="t('admin.tlsFingerprintProfiles.title')"
>
<Icon name="lock" size="md" class="mr-1.5" />
<span class="hidden md:inline">{{ t('admin.tlsFingerprintProfiles.title') }}</span>
</button>
<!-- Column Settings Dropdown -->
<div class="relative" ref="columnDropdownRef">
<!-- More Tools Dropdown -->
<div class="relative" ref="accountToolsDropdownRef">
<button
@click="
showColumnDropdown = !showColumnDropdown;
showAccountToolsDropdown = !showAccountToolsDropdown;
showAutoRefreshDropdown = false
"
class="btn btn-secondary px-2 md:px-3"
:title="t('admin.users.columnSettings')"
:title="t('admin.accounts.moreActions')"
>
<svg class="h-4 w-4 md:mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 4.5v15m6-15v15m-10.875 0h15.75c.621 0 1.125-.504 1.125-1.125V5.625c0-.621-.504-1.125-1.125-1.125H4.125C3.504 4.5 3 5.004 3 5.625v12.75c0 .621.504 1.125 1.125 1.125z" />
</svg>
<span class="hidden md:inline">{{ t('admin.users.columnSettings') }}</span>
<Icon name="more" size="sm" class="md:mr-1.5" />
<span class="hidden md:inline">{{ t('admin.accounts.moreActions') }}</span>
<Icon name="chevronDown" size="xs" class="ml-1 hidden md:inline" />
</button>
<!-- Dropdown menu -->
<div
v-if="showColumnDropdown"
class="absolute right-0 z-50 mt-2 w-48 origin-top-right rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800"
v-if="showAccountToolsDropdown"
class="absolute right-0 z-50 mt-2 w-[min(20rem,calc(100vw-2rem))] origin-top-right overflow-hidden rounded-lg border border-gray-200 bg-white shadow-xl dark:border-gray-700 dark:bg-gray-800"
>
<div class="max-h-80 overflow-y-auto p-2">
<button
v-for="col in toggleableColumns"
:key="col.key"
@click="toggleColumn(col.key)"
class="flex w-full items-center justify-between rounded-md px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
>
<span>{{ col.label }}</span>
<Icon v-if="isColumnVisible(col.key)" name="check" size="sm" class="text-primary-500" />
<div class="max-h-[70vh] overflow-y-auto p-2">
<div class="px-2 py-2">
<div class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
{{ t('admin.accounts.dataActions') }}
</div>
</div>
<button class="account-tools-menu-item" @click="openSyncFromCrs">
<span class="account-tools-menu-icon bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-300">
<Icon name="sync" size="sm" />
</span>
<span class="flex-1 text-left">{{ t('admin.accounts.syncFromCrs') }}</span>
</button>
<button class="account-tools-menu-item" @click="openImportData">
<span class="account-tools-menu-icon bg-emerald-50 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-300">
<Icon name="upload" size="sm" />
</span>
<span class="flex-1 text-left">{{ t('admin.accounts.dataImport') }}</span>
</button>
<button class="account-tools-menu-item" @click="openExportDataDialogFromMenu">
<span class="account-tools-menu-icon bg-violet-50 text-violet-600 dark:bg-violet-900/30 dark:text-violet-300">
<Icon name="download" size="sm" />
</span>
<span class="flex-1 text-left">
{{ selIds.length ? t('admin.accounts.dataExportSelected') : t('admin.accounts.dataExport') }}
</span>
<span
v-if="selIds.length"
class="rounded-full bg-primary-100 px-2 py-0.5 text-xs font-medium text-primary-700 dark:bg-primary-900/40 dark:text-primary-300"
>
{{ t('admin.accounts.selectedCount', { count: selIds.length }) }}
</span>
</button>
<div class="my-2 border-t border-gray-100 dark:border-gray-700"></div>
<div class="px-2 py-2">
<div class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
{{ t('admin.accounts.toolActions') }}
</div>
</div>
<button class="account-tools-menu-item" @click="openErrorPassthrough">
<span class="account-tools-menu-icon bg-amber-50 text-amber-600 dark:bg-amber-900/30 dark:text-amber-300">
<Icon name="shield" size="sm" />
</span>
<span class="flex-1 text-left">{{ t('admin.errorPassthrough.title') }}</span>
</button>
<button class="account-tools-menu-item" @click="openTLSFingerprintProfiles">
<span class="account-tools-menu-icon bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-200">
<Icon name="lock" size="sm" />
</span>
<span class="flex-1 text-left">{{ t('admin.tlsFingerprintProfiles.title') }}</span>
</button>
<div class="my-2 border-t border-gray-100 dark:border-gray-700"></div>
<div class="px-2 py-2">
<div class="flex items-center justify-between gap-3">
<span class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
{{ t('admin.accounts.viewColumns') }}
</span>
<Icon name="grid" size="sm" class="text-gray-400" />
</div>
</div>
<div class="grid grid-cols-1 gap-1">
<button
v-for="col in toggleableColumns"
:key="col.key"
@click="toggleColumn(col.key)"
class="flex w-full items-center justify-between rounded-md px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
>
<span class="truncate">{{ col.label }}</span>
<Icon v-if="isColumnVisible(col.key)" name="check" size="sm" class="text-primary-500" />
</button>
</div>
</div>
</div>
</div>
</template>
<template #beforeCreate>
<button @click="showImportData = true" class="btn btn-secondary">
{{ t('admin.accounts.dataImport') }}
</button>
<button @click="openExportDataDialog" class="btn btn-secondary">
{{ selIds.length ? t('admin.accounts.dataExportSelected') : t('admin.accounts.dataExport') }}
</button>
</template>
</AccountTableActions>
</div>
<div
@@ -457,9 +488,9 @@ const togglingSchedulable = ref<number | null>(null)
const menu = reactive<{show:boolean, acc:Account|null, pos:{top:number, left:number}|null}>({ show: false, acc: null, pos: null })
const exportingData = ref(false)
// Column settings
const showColumnDropdown = ref(false)
const columnDropdownRef = ref<HTMLElement | null>(null)
// Account tools dropdown
const showAccountToolsDropdown = ref(false)
const accountToolsDropdownRef = ref<HTMLElement | null>(null)
const hiddenColumns = reactive<Set<string>>(new Set())
const DEFAULT_HIDDEN_COLUMNS = ['today_stats', 'proxy', 'notes', 'priority', 'rate_multiplier']
const HIDDEN_COLUMNS_KEY = 'account-hidden-columns'
@@ -820,7 +851,8 @@ const isAnyModalOpen = computed(() => {
showTest.value ||
showStats.value ||
showSchedulePanel.value ||
showErrorPassthrough.value
showErrorPassthrough.value ||
showTLSFingerprintProfiles.value
)
})
@@ -931,6 +963,35 @@ const handleManualRefresh = async () => {
usageManualRefreshToken.value += 1
}
const closeAccountToolsDropdown = () => {
showAccountToolsDropdown.value = false
}
const openSyncFromCrs = () => {
closeAccountToolsDropdown()
showSync.value = true
}
const openImportData = () => {
closeAccountToolsDropdown()
showImportData.value = true
}
const openExportDataDialogFromMenu = () => {
closeAccountToolsDropdown()
openExportDataDialog()
}
const openErrorPassthrough = () => {
closeAccountToolsDropdown()
showErrorPassthrough.value = true
}
const openTLSFingerprintProfiles = () => {
closeAccountToolsDropdown()
showTLSFingerprintProfiles.value = true
}
const syncPendingListChanges = async () => {
hasPendingListSync.value = false
await load()
@@ -944,7 +1005,7 @@ const { pause: pauseAutoRefresh, resume: resumeAutoRefresh } = useIntervalFn(
if (document.hidden) return
if (loading.value || autoRefreshFetching.value) return
if (isAnyModalOpen.value) return
if (menu.show) return
if (menu.show || showAccountToolsDropdown.value || showAutoRefreshDropdown.value) return
if (inAutoRefreshSilentWindow()) {
autoRefreshCountdown.value = Math.max(
0,
@@ -1572,11 +1633,11 @@ const handleScroll = () => {
menu.show = false
}
// 点击外部关闭列设置下拉菜单
// 点击外部关闭顶部下拉菜单
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement
if (columnDropdownRef.value && !columnDropdownRef.value.contains(target)) {
showColumnDropdown.value = false
if (accountToolsDropdownRef.value && !accountToolsDropdownRef.value.contains(target)) {
showAccountToolsDropdown.value = false
}
if (autoRefreshDropdownRef.value && !autoRefreshDropdownRef.value.contains(target)) {
showAutoRefreshDropdown.value = false
@@ -1608,3 +1669,13 @@ onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
.account-tools-menu-item {
@apply flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700;
}
.account-tools-menu-icon {
@apply inline-flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md;
}
</style>