mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
feat: add sortable columns to client sub-table inside inbounds
This commit is contained in:
parent
2928b52b04
commit
1d3ac2c50f
1 changed files with 135 additions and 12 deletions
|
|
@ -8,6 +8,8 @@ import {
|
||||||
RetweetOutlined,
|
RetweetOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EllipsisOutlined,
|
EllipsisOutlined,
|
||||||
|
CaretUpOutlined,
|
||||||
|
CaretDownOutlined,
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { Modal } from 'ant-design-vue';
|
import { Modal } from 'ant-design-vue';
|
||||||
|
|
||||||
|
|
@ -50,14 +52,55 @@ const inbound = computed(() => props.dbInbound.toInbound());
|
||||||
const clients = computed(() => inbound.value?.clients || []);
|
const clients = computed(() => inbound.value?.clients || []);
|
||||||
|
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const paginatedClients = computed(() => {
|
const sortState = ref({ column: null, order: null });
|
||||||
if (!props.pageSize || props.pageSize <= 0) return clients.value;
|
|
||||||
const start = (currentPage.value - 1) * props.pageSize;
|
const sortFns = {
|
||||||
return clients.value.slice(start, start + props.pageSize);
|
client: (a, b) => (a.email || '').localeCompare(b.email || ''),
|
||||||
|
enable: (a, b) => Number(a.enable) - Number(b.enable),
|
||||||
|
online: (a, b) => {
|
||||||
|
const aOn = !!a.email && props.onlineClients.includes(a.email);
|
||||||
|
const bOn = !!b.email && props.onlineClients.includes(b.email);
|
||||||
|
return Number(aOn) - Number(bOn);
|
||||||
|
},
|
||||||
|
traffic: (a, b) => (getSum(a.email) - getSum(b.email)),
|
||||||
|
remained: (a, b) => (getRem(a.email) - getRem(b.email)),
|
||||||
|
alltime: (a, b) => (getAllTime(a.email) - getAllTime(b.email)),
|
||||||
|
expiry: (a, b) => (a.expiryTime || Infinity) - (b.expiryTime || Infinity),
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedClients = computed(() => {
|
||||||
|
const { column, order } = sortState.value;
|
||||||
|
if (!column || !order) return clients.value;
|
||||||
|
const fn = sortFns[column];
|
||||||
|
if (!fn) return clients.value;
|
||||||
|
const sorted = [...clients.value].sort(fn);
|
||||||
|
return order === 'descend' ? sorted.reverse() : sorted;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const paginatedClients = computed(() => {
|
||||||
|
if (!props.pageSize || props.pageSize <= 0) return sortedClients.value;
|
||||||
|
const start = (currentPage.value - 1) * props.pageSize;
|
||||||
|
return sortedClients.value.slice(start, start + props.pageSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleSort(col) {
|
||||||
|
const cur = sortState.value;
|
||||||
|
if (cur.column !== col) {
|
||||||
|
sortState.value = { column: col, order: 'ascend' };
|
||||||
|
} else if (cur.order === 'ascend') {
|
||||||
|
sortState.value = { column: col, order: 'descend' };
|
||||||
|
} else {
|
||||||
|
sortState.value = { column: null, order: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortIndicator(col) {
|
||||||
|
if (sortState.value.column !== col) return null;
|
||||||
|
return sortState.value.order === 'ascend' ? 'ascend' : 'descend';
|
||||||
|
}
|
||||||
|
|
||||||
watch([clients, () => props.pageSize], () => {
|
watch([clients, () => props.pageSize], () => {
|
||||||
const total = clients.value.length;
|
const total = sortedClients.value.length;
|
||||||
const size = props.pageSize > 0 ? props.pageSize : (total || 1);
|
const size = props.pageSize > 0 ? props.pageSize : (total || 1);
|
||||||
const maxPage = Math.max(1, Math.ceil(total / size));
|
const maxPage = Math.max(1, Math.ceil(total / size));
|
||||||
if (currentPage.value > maxPage) currentPage.value = maxPage;
|
if (currentPage.value > maxPage) currentPage.value = maxPage;
|
||||||
|
|
@ -282,13 +325,55 @@ function confirmBulkDelete() {
|
||||||
@change="(e) => selectAll(e.target.checked)" />
|
@change="(e) => selectAll(e.target.checked)" />
|
||||||
</div>
|
</div>
|
||||||
<div class="cell cell-actions">{{ t('pages.settings.actions') }}</div>
|
<div class="cell cell-actions">{{ t('pages.settings.actions') }}</div>
|
||||||
<div class="cell cell-enable">{{ t('enable') }}</div>
|
<div class="cell cell-enable sortable" @click="toggleSort('enable')">
|
||||||
<div class="cell cell-online">{{ t('online') }}</div>
|
{{ t('enable') }}
|
||||||
<div class="cell cell-client">{{ t('pages.inbounds.client') }}</div>
|
<span class="sort-icons">
|
||||||
<div class="cell cell-traffic">{{ t('pages.inbounds.traffic') }}</div>
|
<CaretUpOutlined :class="['sort-icon', { active: sortIndicator('enable') === 'ascend' }]" />
|
||||||
<div class="cell cell-remained">{{ t('remained') }}</div>
|
<CaretDownOutlined :class="['sort-icon', { active: sortIndicator('enable') === 'descend' }]" />
|
||||||
<div class="cell cell-alltime">{{ t('pages.inbounds.allTimeTraffic') }}</div>
|
</span>
|
||||||
<div class="cell cell-expiry">{{ t('pages.inbounds.expireDate') }}</div>
|
</div>
|
||||||
|
<div class="cell cell-online sortable" @click="toggleSort('online')">
|
||||||
|
{{ t('online') }}
|
||||||
|
<span class="sort-icons">
|
||||||
|
<CaretUpOutlined :class="['sort-icon', { active: sortIndicator('online') === 'ascend' }]" />
|
||||||
|
<CaretDownOutlined :class="['sort-icon', { active: sortIndicator('online') === 'descend' }]" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="cell cell-client sortable" @click="toggleSort('client')">
|
||||||
|
{{ t('pages.inbounds.client') }}
|
||||||
|
<span class="sort-icons">
|
||||||
|
<CaretUpOutlined :class="['sort-icon', { active: sortIndicator('client') === 'ascend' }]" />
|
||||||
|
<CaretDownOutlined :class="['sort-icon', { active: sortIndicator('client') === 'descend' }]" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="cell cell-traffic sortable" @click="toggleSort('traffic')">
|
||||||
|
{{ t('pages.inbounds.traffic') }}
|
||||||
|
<span class="sort-icons">
|
||||||
|
<CaretUpOutlined :class="['sort-icon', { active: sortIndicator('traffic') === 'ascend' }]" />
|
||||||
|
<CaretDownOutlined :class="['sort-icon', { active: sortIndicator('traffic') === 'descend' }]" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="cell cell-remained sortable" @click="toggleSort('remained')">
|
||||||
|
{{ t('remained') }}
|
||||||
|
<span class="sort-icons">
|
||||||
|
<CaretUpOutlined :class="['sort-icon', { active: sortIndicator('remained') === 'ascend' }]" />
|
||||||
|
<CaretDownOutlined :class="['sort-icon', { active: sortIndicator('remained') === 'descend' }]" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="cell cell-alltime sortable" @click="toggleSort('alltime')">
|
||||||
|
{{ t('pages.inbounds.allTimeTraffic') }}
|
||||||
|
<span class="sort-icons">
|
||||||
|
<CaretUpOutlined :class="['sort-icon', { active: sortIndicator('alltime') === 'ascend' }]" />
|
||||||
|
<CaretDownOutlined :class="['sort-icon', { active: sortIndicator('alltime') === 'descend' }]" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="cell cell-expiry sortable" @click="toggleSort('expiry')">
|
||||||
|
{{ t('pages.inbounds.expireDate') }}
|
||||||
|
<span class="sort-icons">
|
||||||
|
<CaretUpOutlined :class="['sort-icon', { active: sortIndicator('expiry') === 'ascend' }]" />
|
||||||
|
<CaretDownOutlined :class="['sort-icon', { active: sortIndicator('expiry') === 'descend' }]" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="client in paginatedClients" :key="rowKey(client)" class="client-row"
|
<div v-for="client in paginatedClients" :key="rowKey(client)" class="client-row"
|
||||||
|
|
@ -620,6 +705,36 @@ function confirmBulkDelete() {
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sortable {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: opacity 120ms ease;
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-icons {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 0.5;
|
||||||
|
margin-left: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-icon {
|
||||||
|
font-size: 9px;
|
||||||
|
opacity: 0.35;
|
||||||
|
transition: opacity 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-icon.active {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--ant-color-primary, #1677ff);
|
||||||
|
}
|
||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
/* allow grid children to shrink instead of overflowing */
|
/* allow grid children to shrink instead of overflowing */
|
||||||
|
|
@ -655,6 +770,14 @@ function confirmBulkDelete() {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.client-list-header .cell-traffic,
|
||||||
|
.client-list-header .cell-expiry {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.client-list-header .cell {
|
.client-list-header .cell {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue