|
@@ -67,9 +67,29 @@ const emit = defineEmits([
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
// ============ Toolbar / search & filter =============================
|
|
// ============ Toolbar / search & filter =============================
|
|
|
-const enableFilter = ref(false);
|
|
|
|
|
-const searchKey = ref('');
|
|
|
|
|
-const filterBy = ref('');
|
|
|
|
|
|
|
+const FILTER_STATE_KEY = 'inboundsFilterState';
|
|
|
|
|
+const savedFilterState = (() => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return JSON.parse(localStorage.getItem(FILTER_STATE_KEY) || '{}');
|
|
|
|
|
+ } catch (_e) {
|
|
|
|
|
+ return {};
|
|
|
|
|
+ }
|
|
|
|
|
+})();
|
|
|
|
|
+const enableFilter = ref(!!savedFilterState.enableFilter);
|
|
|
|
|
+const searchKey = ref(savedFilterState.searchKey || '');
|
|
|
|
|
+const filterBy = ref(savedFilterState.filterBy || '');
|
|
|
|
|
+const protocolFilter = ref(savedFilterState.protocolFilter || '');
|
|
|
|
|
+const nodeFilter = ref(savedFilterState.nodeFilter || '');
|
|
|
|
|
+
|
|
|
|
|
+watch([enableFilter, searchKey, filterBy, protocolFilter, nodeFilter], () => {
|
|
|
|
|
+ localStorage.setItem(FILTER_STATE_KEY, JSON.stringify({
|
|
|
|
|
+ enableFilter: enableFilter.value,
|
|
|
|
|
+ searchKey: searchKey.value,
|
|
|
|
|
+ filterBy: filterBy.value,
|
|
|
|
|
+ protocolFilter: protocolFilter.value,
|
|
|
|
|
+ nodeFilter: nodeFilter.value,
|
|
|
|
|
+ }));
|
|
|
|
|
+});
|
|
|
|
|
|
|
|
// Toggle the filter mode — flip cleans the other input.
|
|
// Toggle the filter mode — flip cleans the other input.
|
|
|
function onToggleFilter() {
|
|
function onToggleFilter() {
|
|
@@ -77,6 +97,35 @@ function onToggleFilter() {
|
|
|
else filterBy.value = '';
|
|
else filterBy.value = '';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const protocolOptions = computed(() => {
|
|
|
|
|
+ const values = new Set(props.dbInbounds.map((i) => i.protocol).filter(Boolean));
|
|
|
|
|
+ return [...values].sort();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const nodeOptions = computed(() => {
|
|
|
|
|
+ const values = new Map();
|
|
|
|
|
+ if (props.dbInbounds.some((i) => i.nodeId == null)) {
|
|
|
|
|
+ values.set('local', t('pages.inbounds.localPanel'));
|
|
|
|
|
+ }
|
|
|
|
|
+ for (const dbInbound of props.dbInbounds) {
|
|
|
|
|
+ if (dbInbound.nodeId == null) continue;
|
|
|
|
|
+ const node = props.nodesById.get(dbInbound.nodeId);
|
|
|
|
|
+ values.set(String(dbInbound.nodeId), node?.name || `#${dbInbound.nodeId}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ return [...values.entries()].map(([value, label]) => ({ value, label }));
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+function applySecondaryFilters(rows) {
|
|
|
|
|
+ return rows.filter((dbInbound) => {
|
|
|
|
|
+ if (protocolFilter.value && dbInbound.protocol !== protocolFilter.value) return false;
|
|
|
|
|
+ if (nodeFilter.value) {
|
|
|
|
|
+ const nodeValue = dbInbound.nodeId == null ? 'local' : String(dbInbound.nodeId);
|
|
|
|
|
+ if (nodeValue !== nodeFilter.value) return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ============ Search / filter projection =============================
|
|
// ============ Search / filter projection =============================
|
|
|
// Mirrors the legacy logic: when searching, keep inbounds that match
|
|
// Mirrors the legacy logic: when searching, keep inbounds that match
|
|
|
// anywhere (deep search); when filtering, keep inbounds that have at
|
|
// anywhere (deep search); when filtering, keep inbounds that have at
|
|
@@ -99,7 +148,7 @@ function projectInbound(dbInbound, predicate) {
|
|
|
|
|
|
|
|
const visibleInbounds = computed(() => {
|
|
const visibleInbounds = computed(() => {
|
|
|
if (enableFilter.value) {
|
|
if (enableFilter.value) {
|
|
|
- if (ObjectUtil.isEmpty(filterBy.value)) return [...props.dbInbounds];
|
|
|
|
|
|
|
+ if (ObjectUtil.isEmpty(filterBy.value)) return applySecondaryFilters([...props.dbInbounds]);
|
|
|
const out = [];
|
|
const out = [];
|
|
|
for (const dbInbound of props.dbInbounds) {
|
|
for (const dbInbound of props.dbInbounds) {
|
|
|
const c = props.clientCount[dbInbound.id];
|
|
const c = props.clientCount[dbInbound.id];
|
|
@@ -107,15 +156,15 @@ const visibleInbounds = computed(() => {
|
|
|
const list = c[filterBy.value];
|
|
const list = c[filterBy.value];
|
|
|
out.push(projectInbound(dbInbound, (client) => list.includes(client.email)));
|
|
out.push(projectInbound(dbInbound, (client) => list.includes(client.email)));
|
|
|
}
|
|
}
|
|
|
- return out;
|
|
|
|
|
|
|
+ return applySecondaryFilters(out);
|
|
|
}
|
|
}
|
|
|
- if (ObjectUtil.isEmpty(searchKey.value)) return [...props.dbInbounds];
|
|
|
|
|
|
|
+ if (ObjectUtil.isEmpty(searchKey.value)) return applySecondaryFilters([...props.dbInbounds]);
|
|
|
const out = [];
|
|
const out = [];
|
|
|
for (const dbInbound of props.dbInbounds) {
|
|
for (const dbInbound of props.dbInbounds) {
|
|
|
if (!ObjectUtil.deepSearch(dbInbound, searchKey.value)) continue;
|
|
if (!ObjectUtil.deepSearch(dbInbound, searchKey.value)) continue;
|
|
|
out.push(projectInbound(dbInbound, (client) => ObjectUtil.deepSearch(client, searchKey.value)));
|
|
out.push(projectInbound(dbInbound, (client) => ObjectUtil.deepSearch(client, searchKey.value)));
|
|
|
}
|
|
}
|
|
|
- return out;
|
|
|
|
|
|
|
+ return applySecondaryFilters(out);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// ============ Sorting =================================================
|
|
// ============ Sorting =================================================
|
|
@@ -319,6 +368,18 @@ function showQrCodeMenu(dbInbound) {
|
|
|
<a-radio-button value="expiring">{{ t('depletingSoon') }}</a-radio-button>
|
|
<a-radio-button value="expiring">{{ t('depletingSoon') }}</a-radio-button>
|
|
|
<a-radio-button value="online">{{ t('online') }}</a-radio-button>
|
|
<a-radio-button value="online">{{ t('online') }}</a-radio-button>
|
|
|
</a-radio-group>
|
|
</a-radio-group>
|
|
|
|
|
+ <a-select v-model:value="protocolFilter" allow-clear :placeholder="t('pages.inbounds.protocol')"
|
|
|
|
|
+ :size="isMobile ? 'small' : 'middle'" :style="{ width: '150px' }">
|
|
|
|
|
+ <a-select-option v-for="protocol in protocolOptions" :key="protocol" :value="protocol">
|
|
|
|
|
+ {{ protocol }}
|
|
|
|
|
+ </a-select-option>
|
|
|
|
|
+ </a-select>
|
|
|
|
|
+ <a-select v-if="nodeOptions.length > 0" v-model:value="nodeFilter" allow-clear
|
|
|
|
|
+ :placeholder="t('pages.inbounds.node')" :size="isMobile ? 'small' : 'middle'" :style="{ width: '170px' }">
|
|
|
|
|
+ <a-select-option v-for="node in nodeOptions" :key="node.value" :value="node.value">
|
|
|
|
|
+ {{ node.label }}
|
|
|
|
|
+ </a-select-option>
|
|
|
|
|
+ </a-select>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- ====================== Mobile: card list ======================= -->
|
|
<!-- ====================== Mobile: card list ======================= -->
|