mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 13:14:11 +00:00
feat(clients): sortable table columns
Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
84fbd23f1b
commit
1d299ac396
1 changed files with 58 additions and 8 deletions
|
|
@ -428,15 +428,65 @@ function expiryColor(row) {
|
||||||
return 'green';
|
return 'green';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortState = ref({ column: null, order: null });
|
||||||
|
|
||||||
|
function sortableCol(col, key) {
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
sorter: true,
|
||||||
|
showSorterTooltip: false,
|
||||||
|
sortOrder: sortState.value.column === key ? sortState.value.order : null,
|
||||||
|
sortDirections: ['ascend', 'descend'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortFns = {
|
||||||
|
enable: (a, b) => Number(a.enable) - Number(b.enable),
|
||||||
|
email: (a, b) => (a.email || '').localeCompare(b.email || ''),
|
||||||
|
inboundIds: (a, b) => (a.inboundIds?.length || 0) - (b.inboundIds?.length || 0),
|
||||||
|
traffic: (a, b) => {
|
||||||
|
const ua = (a.traffic?.up || 0) + (a.traffic?.down || 0);
|
||||||
|
const ub = (b.traffic?.up || 0) + (b.traffic?.down || 0);
|
||||||
|
return ua - ub;
|
||||||
|
},
|
||||||
|
remaining: (a, b) => {
|
||||||
|
const ra = a.totalGB > 0 ? a.totalGB - ((a.traffic?.up || 0) + (a.traffic?.down || 0)) : Infinity;
|
||||||
|
const rb = b.totalGB > 0 ? b.totalGB - ((b.traffic?.up || 0) + (b.traffic?.down || 0)) : Infinity;
|
||||||
|
return ra - rb;
|
||||||
|
},
|
||||||
|
expiryTime: (a, b) => {
|
||||||
|
const ea = a.expiryTime > 0 ? a.expiryTime : Infinity;
|
||||||
|
const eb = b.expiryTime > 0 ? b.expiryTime : Infinity;
|
||||||
|
return ea - eb;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedClients = computed(() => {
|
||||||
|
const { column, order } = sortState.value;
|
||||||
|
const rows = filteredClients.value;
|
||||||
|
if (!column || !order) return rows;
|
||||||
|
const fn = sortFns[column];
|
||||||
|
if (!fn) return rows;
|
||||||
|
const sorted = [...rows].sort(fn);
|
||||||
|
return order === 'descend' ? sorted.reverse() : sorted;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onTableChange(_pag, _filters, sorter) {
|
||||||
|
sortState.value = {
|
||||||
|
column: sorter?.columnKey || sorter?.field || null,
|
||||||
|
order: sorter?.order || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const columns = computed(() => [
|
const columns = computed(() => [
|
||||||
{ title: t('pages.clients.actions') || 'Actions', key: 'actions', width: 200 },
|
{ title: t('pages.clients.actions') || 'Actions', key: 'actions', width: 200 },
|
||||||
{ title: t('pages.clients.enabled') || 'Enabled', key: 'enable', width: 80 },
|
sortableCol({ title: t('pages.clients.enabled') || 'Enabled', key: 'enable', width: 80 }, 'enable'),
|
||||||
{ title: t('pages.clients.online') || 'Online', key: 'online', width: 90 },
|
{ title: t('pages.clients.online') || 'Online', key: 'online', width: 90 },
|
||||||
{ title: t('pages.clients.client') || 'Client', key: 'email' },
|
sortableCol({ title: t('pages.clients.client') || 'Client', key: 'email' }, 'email'),
|
||||||
{ title: t('pages.clients.attachedInbounds') || 'Attached inbounds', key: 'inboundIds' },
|
sortableCol({ title: t('pages.clients.attachedInbounds') || 'Attached inbounds', key: 'inboundIds' }, 'inboundIds'),
|
||||||
{ title: t('pages.clients.traffic') || 'Traffic', key: 'traffic' },
|
sortableCol({ title: t('pages.clients.traffic') || 'Traffic', key: 'traffic' }, 'traffic'),
|
||||||
{ title: t('pages.clients.remaining') || 'Remaining', key: 'remaining', width: 130 },
|
sortableCol({ title: t('pages.clients.remaining') || 'Remaining', key: 'remaining', width: 130 }, 'remaining'),
|
||||||
{ title: t('pages.clients.duration') || 'Duration', key: 'expiryTime' },
|
sortableCol({ title: t('pages.clients.duration') || 'Duration', key: 'expiryTime' }, 'expiryTime'),
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -594,10 +644,10 @@ const columns = computed(() => [
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-table v-if="!isMobile" :columns="columns" :data-source="filteredClients" :loading="loading" row-key="email"
|
<a-table v-if="!isMobile" :columns="columns" :data-source="sortedClients" :loading="loading" row-key="email"
|
||||||
:row-selection="rowSelection"
|
:row-selection="rowSelection"
|
||||||
:pagination="{ pageSize: 20, showSizeChanger: true, pageSizeOptions: ['10', '20', '50', '100'] }"
|
:pagination="{ pageSize: 20, showSizeChanger: true, pageSizeOptions: ['10', '20', '50', '100'] }"
|
||||||
size="small">
|
size="small" @change="onTableChange">
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'email'">
|
<template v-if="column.key === 'email'">
|
||||||
<div class="email-cell">
|
<div class="email-cell">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue