Merge pull request #326 from geminiwen/main
feat(admin): 添加账号管理和订阅管理的列设置功能
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<slot name="before"></slot>
|
||||||
<button @click="$emit('refresh')" :disabled="loading" class="btn btn-secondary">
|
<button @click="$emit('refresh')" :disabled="loading" class="btn btn-secondary">
|
||||||
<Icon name="refresh" size="md" :class="[loading ? 'animate-spin' : '']" />
|
<Icon name="refresh" size="md" :class="[loading ? 'animate-spin' : '']" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -673,6 +673,7 @@ export default {
|
|||||||
updating: 'Updating...',
|
updating: 'Updating...',
|
||||||
columns: {
|
columns: {
|
||||||
user: 'User',
|
user: 'User',
|
||||||
|
email: 'Email',
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
notes: 'Notes',
|
notes: 'Notes',
|
||||||
role: 'Role',
|
role: 'Role',
|
||||||
@@ -1093,6 +1094,7 @@ export default {
|
|||||||
todayStats: 'Today Stats',
|
todayStats: 'Today Stats',
|
||||||
groups: 'Groups',
|
groups: 'Groups',
|
||||||
usageWindows: 'Usage Windows',
|
usageWindows: 'Usage Windows',
|
||||||
|
proxy: 'Proxy',
|
||||||
lastUsed: 'Last Used',
|
lastUsed: 'Last Used',
|
||||||
expiresAt: 'Expires At',
|
expiresAt: 'Expires At',
|
||||||
actions: 'Actions'
|
actions: 'Actions'
|
||||||
|
|||||||
@@ -1142,6 +1142,7 @@ export default {
|
|||||||
todayStats: '今日统计',
|
todayStats: '今日统计',
|
||||||
groups: '分组',
|
groups: '分组',
|
||||||
usageWindows: '用量窗口',
|
usageWindows: '用量窗口',
|
||||||
|
proxy: '代理',
|
||||||
lastUsed: '最近使用',
|
lastUsed: '最近使用',
|
||||||
expiresAt: '过期时间',
|
expiresAt: '过期时间',
|
||||||
actions: '操作'
|
actions: '操作'
|
||||||
|
|||||||
@@ -15,7 +15,40 @@
|
|||||||
@refresh="load"
|
@refresh="load"
|
||||||
@sync="showSync = true"
|
@sync="showSync = true"
|
||||||
@create="showCreate = true"
|
@create="showCreate = true"
|
||||||
/>
|
>
|
||||||
|
<template #before>
|
||||||
|
<!-- Column Settings Dropdown -->
|
||||||
|
<div class="relative" ref="columnDropdownRef">
|
||||||
|
<button
|
||||||
|
@click="showColumnDropdown = !showColumnDropdown"
|
||||||
|
class="btn btn-secondary px-2 md:px-3"
|
||||||
|
:title="t('admin.users.columnSettings')"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<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" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</AccountTableActions>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #table>
|
<template #table>
|
||||||
@@ -54,6 +87,15 @@
|
|||||||
<template #cell-usage="{ row }">
|
<template #cell-usage="{ row }">
|
||||||
<AccountUsageCell :account="row" />
|
<AccountUsageCell :account="row" />
|
||||||
</template>
|
</template>
|
||||||
|
<template #cell-proxy="{ row }">
|
||||||
|
<div v-if="row.proxy" class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">{{ row.proxy.name }}</span>
|
||||||
|
<span v-if="row.proxy.country_code" class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
({{ row.proxy.country_code }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-sm text-gray-400 dark:text-dark-500">-</span>
|
||||||
|
</template>
|
||||||
<template #cell-rate_multiplier="{ row }">
|
<template #cell-rate_multiplier="{ row }">
|
||||||
<span class="text-sm font-mono text-gray-700 dark:text-gray-300">
|
<span class="text-sm font-mono text-gray-700 dark:text-gray-300">
|
||||||
{{ (row.rate_multiplier ?? 1).toFixed(2) }}x
|
{{ (row.rate_multiplier ?? 1).toFixed(2) }}x
|
||||||
@@ -143,6 +185,7 @@ import AccountTodayStatsCell from '@/components/account/AccountTodayStatsCell.vu
|
|||||||
import AccountGroupsCell from '@/components/account/AccountGroupsCell.vue'
|
import AccountGroupsCell from '@/components/account/AccountGroupsCell.vue'
|
||||||
import AccountCapacityCell from '@/components/account/AccountCapacityCell.vue'
|
import AccountCapacityCell from '@/components/account/AccountCapacityCell.vue'
|
||||||
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
||||||
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
import { formatDateTime, formatRelativeTime } from '@/utils/format'
|
import { formatDateTime, formatRelativeTime } from '@/utils/format'
|
||||||
import type { Account, Proxy, Group } from '@/types'
|
import type { Account, Proxy, Group } from '@/types'
|
||||||
|
|
||||||
@@ -171,12 +214,54 @@ const statsAcc = ref<Account | null>(null)
|
|||||||
const togglingSchedulable = ref<number | null>(null)
|
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 menu = reactive<{show:boolean, acc:Account|null, pos:{top:number, left:number}|null}>({ show: false, acc: null, pos: null })
|
||||||
|
|
||||||
|
// Column settings
|
||||||
|
const showColumnDropdown = ref(false)
|
||||||
|
const columnDropdownRef = ref<HTMLElement | null>(null)
|
||||||
|
const hiddenColumns = reactive<Set<string>>(new Set())
|
||||||
|
const DEFAULT_HIDDEN_COLUMNS = ['proxy', 'notes', 'priority', 'rate_multiplier']
|
||||||
|
const HIDDEN_COLUMNS_KEY = 'account-hidden-columns'
|
||||||
|
|
||||||
|
const loadSavedColumns = () => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(HIDDEN_COLUMNS_KEY)
|
||||||
|
if (saved) {
|
||||||
|
const parsed = JSON.parse(saved) as string[]
|
||||||
|
parsed.forEach(key => hiddenColumns.add(key))
|
||||||
|
} else {
|
||||||
|
DEFAULT_HIDDEN_COLUMNS.forEach(key => hiddenColumns.add(key))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load saved columns:', e)
|
||||||
|
DEFAULT_HIDDEN_COLUMNS.forEach(key => hiddenColumns.add(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveColumnsToStorage = () => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(HIDDEN_COLUMNS_KEY, JSON.stringify([...hiddenColumns]))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save columns:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleColumn = (key: string) => {
|
||||||
|
if (hiddenColumns.has(key)) {
|
||||||
|
hiddenColumns.delete(key)
|
||||||
|
} else {
|
||||||
|
hiddenColumns.add(key)
|
||||||
|
}
|
||||||
|
saveColumnsToStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const isColumnVisible = (key: string) => !hiddenColumns.has(key)
|
||||||
|
|
||||||
const { items: accounts, loading, params, pagination, load, reload, debouncedReload, handlePageChange, handlePageSizeChange } = useTableLoader<Account, any>({
|
const { items: accounts, loading, params, pagination, load, reload, debouncedReload, handlePageChange, handlePageSizeChange } = useTableLoader<Account, any>({
|
||||||
fetchFn: adminAPI.accounts.list,
|
fetchFn: adminAPI.accounts.list,
|
||||||
initialParams: { platform: '', type: '', status: '', search: '' }
|
initialParams: { platform: '', type: '', status: '', search: '' }
|
||||||
})
|
})
|
||||||
|
|
||||||
const cols = computed(() => {
|
// All available columns
|
||||||
|
const allColumns = computed(() => {
|
||||||
const c = [
|
const c = [
|
||||||
{ key: 'select', label: '', sortable: false },
|
{ key: 'select', label: '', sortable: false },
|
||||||
{ key: 'name', label: t('admin.accounts.columns.name'), sortable: true },
|
{ key: 'name', label: t('admin.accounts.columns.name'), sortable: true },
|
||||||
@@ -189,11 +274,12 @@ const cols = computed(() => {
|
|||||||
if (!authStore.isSimpleMode) {
|
if (!authStore.isSimpleMode) {
|
||||||
c.push({ key: 'groups', label: t('admin.accounts.columns.groups'), sortable: false })
|
c.push({ key: 'groups', label: t('admin.accounts.columns.groups'), sortable: false })
|
||||||
}
|
}
|
||||||
c.push(
|
c.push(
|
||||||
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
|
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
|
||||||
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
|
{ key: 'proxy', label: t('admin.accounts.columns.proxy'), sortable: false },
|
||||||
{ key: 'rate_multiplier', label: t('admin.accounts.columns.billingRateMultiplier'), sortable: true },
|
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
|
||||||
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
|
{ key: 'rate_multiplier', label: t('admin.accounts.columns.billingRateMultiplier'), sortable: true },
|
||||||
|
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
|
||||||
{ key: 'expires_at', label: t('admin.accounts.columns.expiresAt'), sortable: true },
|
{ key: 'expires_at', label: t('admin.accounts.columns.expiresAt'), sortable: true },
|
||||||
{ key: 'notes', label: t('admin.accounts.columns.notes'), sortable: false },
|
{ key: 'notes', label: t('admin.accounts.columns.notes'), sortable: false },
|
||||||
{ key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false }
|
{ key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false }
|
||||||
@@ -201,6 +287,18 @@ const cols = computed(() => {
|
|||||||
return c
|
return c
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Columns that can be toggled (exclude select, name, and actions)
|
||||||
|
const toggleableColumns = computed(() =>
|
||||||
|
allColumns.value.filter(col => col.key !== 'select' && col.key !== 'name' && col.key !== 'actions')
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filtered columns based on visibility
|
||||||
|
const cols = computed(() =>
|
||||||
|
allColumns.value.filter(col =>
|
||||||
|
col.key === 'select' || col.key === 'name' || col.key === 'actions' || !hiddenColumns.has(col.key)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const handleEdit = (a: Account) => { edAcc.value = a; showEdit.value = true }
|
const handleEdit = (a: Account) => { edAcc.value = a; showEdit.value = true }
|
||||||
const openMenu = (a: Account, e: MouseEvent) => {
|
const openMenu = (a: Account, e: MouseEvent) => {
|
||||||
menu.acc = a
|
menu.acc = a
|
||||||
@@ -403,12 +501,21 @@ const isExpired = (value: number | null) => {
|
|||||||
return value * 1000 <= Date.now()
|
return value * 1000 <= Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滚动时关闭菜单
|
// 滚动时关闭操作菜单(不关闭列设置下拉菜单)
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
menu.show = false
|
menu.show = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 点击外部关闭列设置下拉菜单
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
if (columnDropdownRef.value && !columnDropdownRef.value.contains(target)) {
|
||||||
|
showColumnDropdown.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
loadSavedColumns()
|
||||||
load()
|
load()
|
||||||
try {
|
try {
|
||||||
const [p, g] = await Promise.all([adminAPI.proxies.getAll(), adminAPI.groups.getAll()])
|
const [p, g] = await Promise.all([adminAPI.proxies.getAll(), adminAPI.groups.getAll()])
|
||||||
@@ -418,9 +525,11 @@ onMounted(async () => {
|
|||||||
console.error('Failed to load proxies/groups:', error)
|
console.error('Failed to load proxies/groups:', error)
|
||||||
}
|
}
|
||||||
window.addEventListener('scroll', handleScroll, true)
|
window.addEventListener('scroll', handleScroll, true)
|
||||||
|
document.addEventListener('click', handleClickOutside)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('scroll', handleScroll, true)
|
window.removeEventListener('scroll', handleScroll, true)
|
||||||
|
document.removeEventListener('click', handleClickOutside)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -85,6 +85,57 @@
|
|||||||
|
|
||||||
<!-- Right: Actions -->
|
<!-- Right: Actions -->
|
||||||
<div class="ml-auto flex flex-wrap items-center justify-end gap-3">
|
<div class="ml-auto flex flex-wrap items-center justify-end gap-3">
|
||||||
|
<!-- Column Settings Dropdown -->
|
||||||
|
<div class="relative" ref="columnDropdownRef">
|
||||||
|
<button
|
||||||
|
@click="showColumnDropdown = !showColumnDropdown"
|
||||||
|
class="btn btn-secondary px-2 md:px-3"
|
||||||
|
:title="t('admin.users.columnSettings')"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<div class="p-2">
|
||||||
|
<!-- User column mode selection -->
|
||||||
|
<div class="mb-2 border-b border-gray-200 pb-2 dark:border-gray-700">
|
||||||
|
<div class="px-3 py-1 text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.subscriptions.columns.user') }}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="setUserColumnMode('email')"
|
||||||
|
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>{{ t('admin.users.columns.email') }}</span>
|
||||||
|
<Icon v-if="userColumnMode === 'email'" name="check" size="sm" class="text-primary-500" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="setUserColumnMode('username')"
|
||||||
|
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>{{ t('admin.users.columns.username') }}</span>
|
||||||
|
<Icon v-if="userColumnMode === 'username'" name="check" size="sm" class="text-primary-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Other columns toggle -->
|
||||||
|
<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" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="loadSubscriptions"
|
@click="loadSubscriptions"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@@ -110,12 +161,18 @@
|
|||||||
class="flex h-8 w-8 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900/30"
|
class="flex h-8 w-8 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900/30"
|
||||||
>
|
>
|
||||||
<span class="text-sm font-medium text-primary-700 dark:text-primary-300">
|
<span class="text-sm font-medium text-primary-700 dark:text-primary-300">
|
||||||
{{ row.user?.email?.charAt(0).toUpperCase() || '?' }}
|
{{ userColumnMode === 'email'
|
||||||
|
? (row.user?.email?.charAt(0).toUpperCase() || '?')
|
||||||
|
: (row.user?.username?.charAt(0).toUpperCase() || '?')
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="font-medium text-gray-900 dark:text-white">{{
|
<span class="font-medium text-gray-900 dark:text-white">
|
||||||
row.user?.email || t('admin.redeem.userPrefix', { id: row.user_id })
|
{{ userColumnMode === 'email'
|
||||||
}}</span>
|
? (row.user?.email || t('admin.redeem.userPrefix', { id: row.user_id }))
|
||||||
|
: (row.user?.username || '-')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -545,8 +602,43 @@ import Icon from '@/components/icons/Icon.vue'
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
const columns = computed<Column[]>(() => [
|
// User column display mode: 'email' or 'username'
|
||||||
{ key: 'user', label: t('admin.subscriptions.columns.user'), sortable: true },
|
const userColumnMode = ref<'email' | 'username'>('email')
|
||||||
|
const USER_COLUMN_MODE_KEY = 'subscription-user-column-mode'
|
||||||
|
|
||||||
|
const loadUserColumnMode = () => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(USER_COLUMN_MODE_KEY)
|
||||||
|
if (saved === 'email' || saved === 'username') {
|
||||||
|
userColumnMode.value = saved
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load user column mode:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveUserColumnMode = () => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(USER_COLUMN_MODE_KEY, userColumnMode.value)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save user column mode:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUserColumnMode = (mode: 'email' | 'username') => {
|
||||||
|
userColumnMode.value = mode
|
||||||
|
saveUserColumnMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// All available columns
|
||||||
|
const allColumns = computed<Column[]>(() => [
|
||||||
|
{
|
||||||
|
key: 'user',
|
||||||
|
label: userColumnMode.value === 'email'
|
||||||
|
? t('admin.subscriptions.columns.user')
|
||||||
|
: t('admin.users.columns.username'),
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
{ key: 'group', label: t('admin.subscriptions.columns.group'), sortable: true },
|
{ key: 'group', label: t('admin.subscriptions.columns.group'), sortable: true },
|
||||||
{ key: 'usage', label: t('admin.subscriptions.columns.usage'), sortable: false },
|
{ key: 'usage', label: t('admin.subscriptions.columns.usage'), sortable: false },
|
||||||
{ key: 'expires_at', label: t('admin.subscriptions.columns.expires'), sortable: true },
|
{ key: 'expires_at', label: t('admin.subscriptions.columns.expires'), sortable: true },
|
||||||
@@ -554,6 +646,69 @@ const columns = computed<Column[]>(() => [
|
|||||||
{ key: 'actions', label: t('admin.subscriptions.columns.actions'), sortable: false }
|
{ key: 'actions', label: t('admin.subscriptions.columns.actions'), sortable: false }
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// Columns that can be toggled (exclude user and actions which are always visible)
|
||||||
|
const toggleableColumns = computed(() =>
|
||||||
|
allColumns.value.filter(col => col.key !== 'user' && col.key !== 'actions')
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hidden columns set
|
||||||
|
const hiddenColumns = reactive<Set<string>>(new Set())
|
||||||
|
|
||||||
|
// Default hidden columns
|
||||||
|
const DEFAULT_HIDDEN_COLUMNS: string[] = []
|
||||||
|
|
||||||
|
// localStorage key
|
||||||
|
const HIDDEN_COLUMNS_KEY = 'subscription-hidden-columns'
|
||||||
|
|
||||||
|
// Load saved column settings
|
||||||
|
const loadSavedColumns = () => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(HIDDEN_COLUMNS_KEY)
|
||||||
|
if (saved) {
|
||||||
|
const parsed = JSON.parse(saved) as string[]
|
||||||
|
parsed.forEach(key => hiddenColumns.add(key))
|
||||||
|
} else {
|
||||||
|
DEFAULT_HIDDEN_COLUMNS.forEach(key => hiddenColumns.add(key))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load saved columns:', e)
|
||||||
|
DEFAULT_HIDDEN_COLUMNS.forEach(key => hiddenColumns.add(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save column settings to localStorage
|
||||||
|
const saveColumnsToStorage = () => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(HIDDEN_COLUMNS_KEY, JSON.stringify([...hiddenColumns]))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save columns:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle column visibility
|
||||||
|
const toggleColumn = (key: string) => {
|
||||||
|
if (hiddenColumns.has(key)) {
|
||||||
|
hiddenColumns.delete(key)
|
||||||
|
} else {
|
||||||
|
hiddenColumns.add(key)
|
||||||
|
}
|
||||||
|
saveColumnsToStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if column is visible
|
||||||
|
const isColumnVisible = (key: string) => !hiddenColumns.has(key)
|
||||||
|
|
||||||
|
// Filtered columns for display
|
||||||
|
const columns = computed<Column[]>(() =>
|
||||||
|
allColumns.value.filter(col =>
|
||||||
|
col.key === 'user' || col.key === 'actions' || !hiddenColumns.has(col.key)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Column dropdown state
|
||||||
|
const showColumnDropdown = ref(false)
|
||||||
|
const columnDropdownRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
// Filter options
|
// Filter options
|
||||||
const statusOptions = computed(() => [
|
const statusOptions = computed(() => [
|
||||||
{ value: '', label: t('admin.subscriptions.allStatus') },
|
{ value: '', label: t('admin.subscriptions.allStatus') },
|
||||||
@@ -949,14 +1104,19 @@ const formatResetTime = (windowStart: string, period: 'daily' | 'weekly' | 'mont
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle click outside to close user dropdown
|
// Handle click outside to close dropdowns
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
const target = event.target as HTMLElement
|
const target = event.target as HTMLElement
|
||||||
if (!target.closest('[data-assign-user-search]')) showUserDropdown.value = false
|
if (!target.closest('[data-assign-user-search]')) showUserDropdown.value = false
|
||||||
if (!target.closest('[data-filter-user-search]')) showFilterUserDropdown.value = false
|
if (!target.closest('[data-filter-user-search]')) showFilterUserDropdown.value = false
|
||||||
|
if (columnDropdownRef.value && !columnDropdownRef.value.contains(target)) {
|
||||||
|
showColumnDropdown.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
loadUserColumnMode()
|
||||||
|
loadSavedColumns()
|
||||||
loadSubscriptions()
|
loadSubscriptions()
|
||||||
loadGroups()
|
loadGroups()
|
||||||
document.addEventListener('click', handleClickOutside)
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
|||||||
Reference in New Issue
Block a user