[client] add table and filter
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<v-app-bar :elevation="5">
|
||||
<v-icon v-if="isMobile" icon="mdi-menu" @click="$emit('toggleDrawer')" />
|
||||
<span v-else style="width: 24px"></span>
|
||||
<v-app-bar-title :text="$t(<string>$router.currentRoute.value.name)" class="align-center text-center " />
|
||||
<v-app-bar-title :text="$t(<string>route.name)" class="align-center text-center " />
|
||||
<v-btn prepend-icon="mdi-content-save" v-if="stateChange" :text="$t('actions.save')" @click="saveChanges"></v-btn>
|
||||
<v-icon icon="mdi-theme-light-dark" @click="toggleTheme()" style="margin: 0 10px;"></v-icon>
|
||||
</v-app-bar>
|
||||
@@ -13,9 +13,11 @@ import { computed, ref } from "vue"
|
||||
import { useTheme } from "vuetify"
|
||||
import { FindDiff } from "@/plugins/utils"
|
||||
import Data from "@/store/modules/data"
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
defineProps(['isMobile'])
|
||||
|
||||
const route = useRoute();
|
||||
const theme = useTheme()
|
||||
const darkMode = ref(localStorage.getItem('theme') == "dark")
|
||||
|
||||
|
||||
@@ -56,7 +56,8 @@ export function updateConfigs(configs: Config, newUserName: string): Config {
|
||||
|
||||
export function randomConfigs(user: string): Config {
|
||||
const mixedPassword = RandomUtil.randomSeq(10)
|
||||
const ssPassword = RandomUtil.randomShadowsocksPassword(32)
|
||||
const ssPassword16 = RandomUtil.randomShadowsocksPassword(16)
|
||||
const ssPassword32 = RandomUtil.randomShadowsocksPassword(32)
|
||||
const uuid = RandomUtil.randomUUID()
|
||||
return {
|
||||
mixed: {
|
||||
@@ -73,11 +74,15 @@ export function randomConfigs(user: string): Config {
|
||||
},
|
||||
shadowsocks: {
|
||||
name: user,
|
||||
password: ssPassword,
|
||||
password: ssPassword32,
|
||||
},
|
||||
shadowsocks16: {
|
||||
name: user,
|
||||
password: ssPassword16,
|
||||
},
|
||||
shadowtls: {
|
||||
name: user,
|
||||
password: ssPassword,
|
||||
password: ssPassword32,
|
||||
},
|
||||
vmess: {
|
||||
name: user,
|
||||
|
||||
+272
-39
@@ -29,19 +29,91 @@
|
||||
<v-btn color="primary" @click="showModal(-1)">{{ $t('actions.add') }}</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-select
|
||||
hide-details
|
||||
variant="underlined"
|
||||
density="compact"
|
||||
:label="$t('filter')"
|
||||
:items="filterItems"
|
||||
v-model="filter">
|
||||
</v-select>
|
||||
<v-menu v-model="filterMenu" :close-on-content-click="false" location="bottom center">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('filter') }}
|
||||
<v-badge color="error" dot v-if="filterSettings.enabled" floating />
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-select
|
||||
variant="underlined"
|
||||
density="compact"
|
||||
:label="$t('type')"
|
||||
:items="filterItems"
|
||||
v-model="filterSettings.state">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-select
|
||||
variant="underlined"
|
||||
density="compact"
|
||||
:label="$t('client.group')"
|
||||
:items="[ {title: $t('all'), value: '-'}, ...groups.map(g => ({ title: g.length>0 ? g : $t('none'), value: g}))]"
|
||||
v-model="filterSettings.group">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
variant="underlined"
|
||||
density="compact"
|
||||
:label="$t('client.name')"
|
||||
v-model="filterSettings.text">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<v-card-actions>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="outlined"
|
||||
@click="clearFilter"
|
||||
>
|
||||
{{ $t('actions.del') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="tonal"
|
||||
@click="doFilter"
|
||||
>
|
||||
{{ $t('actions.update') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-switch v-model="tableView" color="primary" hide-details>
|
||||
<template v-slot:label><v-icon icon="mdi-table"></v-icon></template>
|
||||
</v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<template v-for="(item, index) in clients" :key="item.id">
|
||||
<v-col cols="12" sm="4" md="3" lg="2" :style="checkFilter(item)? '' : 'opacity: .2'">
|
||||
<template v-for="group in groups" v-if="!tableView">
|
||||
<v-row>
|
||||
<v-col class="v-card-subtitle">
|
||||
{{ group.length>0 ? group : $t('none') }}
|
||||
<v-badge :content="(filterSettings.enabled ? filterSettings.filteredClients : clients).filter(c => c.group == group).length" inline color="info" />
|
||||
<v-icon
|
||||
:icon="openedGroups.includes(group) ? 'mdi-arrow-collapse-up' : 'mdi-arrow-collapse-down'"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="toggleGroupOpen(group)"
|
||||
></v-icon>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="openedGroups.includes(group)">
|
||||
<template v-for="item in (filterSettings.enabled ? filterSettings.filteredClients : clients).filter(c => c.group == group)" :key="item.id">
|
||||
<v-col cols="12" sm="4" md="3" lg="2">
|
||||
<v-card rounded="xl" elevation="5" min-width="200">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
@@ -49,7 +121,7 @@
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-switch color="primary"
|
||||
v-model="clients[index].enable"
|
||||
v-model="item.enable"
|
||||
@update:model-value="buildInboundsUsers(item.inbounds)"
|
||||
hideDetails density="compact" />
|
||||
</v-col>
|
||||
@@ -98,7 +170,7 @@
|
||||
<v-row>
|
||||
<v-col>{{ $t('online') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
<template v-if="onlines[index]">
|
||||
<template v-if="isOnline(item.name).value">
|
||||
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip>
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
@@ -107,16 +179,16 @@
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions style="padding: 0;">
|
||||
<v-btn icon="mdi-account-edit" @click="showModal(index)">
|
||||
<v-btn icon="mdi-account-edit" @click="showModal(item.id)">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
|
||||
<v-tooltip activator="parent" location="top" :text="$t('actions.edit') + item.id"></v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn style="margin-inline-start:0;" icon="mdi-account-minus" color="warning" @click="delOverlay[index] = true">
|
||||
<v-btn style="margin-inline-start:0;" icon="mdi-account-minus" color="warning" @click="delOverlay[clients.findIndex(c => c.id == item.id)] = true">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" :text="$t('actions.del')"></v-tooltip>
|
||||
</v-btn>
|
||||
<v-overlay
|
||||
v-model="delOverlay[index]"
|
||||
v-model="delOverlay[clients.findIndex(c => c.id == item.id)]"
|
||||
contained
|
||||
class="align-center justify-center"
|
||||
>
|
||||
@@ -124,12 +196,12 @@
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>{{ $t('confirm') }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="error" variant="outlined" @click="delClient(index)">{{ $t('yes') }}</v-btn>
|
||||
<v-btn color="success" variant="outlined" @click="delOverlay[index] = false">{{ $t('no') }}</v-btn>
|
||||
<v-btn color="error" variant="outlined" @click="delClient(item.id)">{{ $t('yes') }}</v-btn>
|
||||
<v-btn color="success" variant="outlined" @click="delOverlay[clients.findIndex(c => c.id == item.id)] = false">{{ $t('no') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-overlay>
|
||||
<v-btn icon="mdi-qrcode" @click="showQrCode(index)">
|
||||
<v-btn icon="mdi-qrcode" @click="showQrCode(item.id)">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" text="QR-Code"></v-tooltip>
|
||||
</v-btn>
|
||||
@@ -141,8 +213,109 @@
|
||||
</v-card>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-row v-else>
|
||||
<v-col cols="12">
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="filterSettings.enabled ? filterSettings.filteredClients : clients"
|
||||
:hide-default-footer="filterSettings.enabled ? filterSettings.filteredClients.length<=10 : clients.length<=10"
|
||||
hide-no-data
|
||||
fixed-header
|
||||
:group-by="groupBy"
|
||||
item-value="name"
|
||||
:mobile="smAndDown"
|
||||
mobile-breakpoint="sm"
|
||||
width="100%"
|
||||
class="elevation-3 rounded"
|
||||
>
|
||||
<template v-slot:group-header="{ item, columns, toggleGroup, isGroupOpen }">
|
||||
<tr>
|
||||
<td :colspan="columns.length" @click="toggleGroup(item)" style="min-height: fit-content; text-align: center;">
|
||||
<v-icon :icon="isGroupOpen(item) ? '$expand' : '$next'"></v-icon>
|
||||
{{ item.value.length>0 ? item.value : $t('none') }}
|
||||
<v-badge :content="(filterSettings.enabled ? filterSettings.filteredClients : clients).filter(c => c.group == item.value).length" inline color="success" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-slot:item.volume="{ item }">
|
||||
<div class="text-start">
|
||||
<v-chip
|
||||
size="small"
|
||||
label
|
||||
>{{ item.volume == 0 ? $t('unlimited') : HumanReadable.sizeFormat(item.volume) }}</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:item.expiry="{ item }">
|
||||
<div class="text-start">
|
||||
<v-chip
|
||||
size="small"
|
||||
label
|
||||
>{{ item.expiry == 0 ? $t('unlimited') : HumanReadable.remainedDays(item.expiry)?? $t('date.expired') }}</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:item.online="{ item }">
|
||||
<div class="text-start">
|
||||
<template v-if="isOnline(item.name).value">
|
||||
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip>
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-icon
|
||||
class="me-2"
|
||||
@click="showModal(item.id)"
|
||||
>
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
<v-menu
|
||||
v-model="delOverlay[clients.findIndex(c => c.id == item.id)]"
|
||||
:close-on-content-click="false"
|
||||
location="top center"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon
|
||||
class="me-2"
|
||||
color="error"
|
||||
v-bind="props"
|
||||
>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-card :title="$t('actions.del')" rounded="lg">
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>{{ $t('confirm') }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="error" variant="outlined" @click="delClient(item.id)">{{ $t('yes') }}</v-btn>
|
||||
<v-btn color="success" variant="outlined" @click="delOverlay[clients.findIndex(c => c.id == item.id)] = false">{{ $t('no') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<v-icon
|
||||
class="me-2"
|
||||
@click="showQrCode(item.id)"
|
||||
>
|
||||
mdi-qrcode
|
||||
</v-icon>
|
||||
<v-icon icon="mdi-chart-line" @click="showStats(item.name)" v-if="v2rayStats.users.includes(item.name)">
|
||||
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<style>
|
||||
.v-data-table__tr--mobile td {
|
||||
height: fit-content;
|
||||
min-height: 36px !important;
|
||||
}
|
||||
.v-data-table__tr--mobile td div {
|
||||
width:max-content;
|
||||
}
|
||||
</style>
|
||||
<script lang="ts" setup>
|
||||
import Data from '@/store/modules/data'
|
||||
import ClientModal from '@/layouts/modals/Client.vue'
|
||||
@@ -156,13 +329,16 @@ import { Link, LinkUtil } from '@/plugins/link'
|
||||
import { HumanReadable } from '@/plugins/utils'
|
||||
import { i18n } from '@/locales'
|
||||
import { push } from 'notivue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const { smAndDown } = useDisplay()
|
||||
|
||||
const clients = computed((): any[] => {
|
||||
return Data().clients
|
||||
})
|
||||
|
||||
const onlines = computed(() => {
|
||||
return Data().onlines.user ? clients.value.map(c => Data().onlines.user.includes(c.name)) : []
|
||||
const isOnline = (cname: string) => computed(() => {
|
||||
return Data().onlines?.user ? Data().onlines.user.includes(cname) : false
|
||||
})
|
||||
|
||||
const appConfig = computed((): Config => {
|
||||
@@ -184,9 +360,19 @@ const inboundTags = computed((): string[] => {
|
||||
|
||||
const groups = computed((): string[] => {
|
||||
if (!clients.value) return []
|
||||
if (filterSettings?.value.enabled) return Array.from(new Set(filterSettings.value.filteredClients?.map(c => c.group)))
|
||||
return Array.from(new Set(clients.value?.map(c => c.group)))
|
||||
})
|
||||
const filter = ref("")
|
||||
|
||||
const filterMenu = ref(false)
|
||||
const filterSettings = ref({
|
||||
enabled: false,
|
||||
state: '',
|
||||
group: '-',
|
||||
text: '',
|
||||
filteredClients: <any[]>[]
|
||||
})
|
||||
const tableView = ref(false)
|
||||
|
||||
const filterItems = [
|
||||
{ title: i18n.global.t('none'), value: '' },
|
||||
@@ -195,18 +381,20 @@ const filterItems = [
|
||||
{ title: i18n.global.t('online'), value: 'online' },
|
||||
]
|
||||
|
||||
const checkFilter = (c:any) :boolean => {
|
||||
switch (filter.value) {
|
||||
case "disable":
|
||||
return !c.enable
|
||||
case "expired":
|
||||
return HumanReadable.remainedDays(c.expiry) == null
|
||||
case "online":
|
||||
return Data().onlines?.user?.includes(c.name)
|
||||
default:
|
||||
return true
|
||||
const headers = [
|
||||
{ title: i18n.global.t('client.name'), key: 'name' },
|
||||
{ title: i18n.global.t('client.desc'), key: 'desc', sortable: false },
|
||||
{ title: i18n.global.t('actions.action'), key: 'actions', sortable: false},
|
||||
{ title: i18n.global.t('stats.volume'), key: 'volume' },
|
||||
{ title: i18n.global.t('date.expiry'), key: 'expiry' },
|
||||
{ title: i18n.global.t('online'), key: 'online' },
|
||||
{ key: 'data-table-group', width: 0 },
|
||||
]
|
||||
const groupBy = [
|
||||
{
|
||||
key: 'group'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const modal = ref({
|
||||
visible: false,
|
||||
@@ -217,7 +405,8 @@ const modal = ref({
|
||||
|
||||
const delOverlay = ref(new Array<boolean>(clients.value.length).fill(false))
|
||||
|
||||
const showModal = (index: number) => {
|
||||
const showModal = (id: number) => {
|
||||
const index = id == -1 ? -1 : clients.value.findIndex(c => c.id == id)
|
||||
modal.value.index = index
|
||||
modal.value.data = index == -1 ? '' : JSON.stringify(clients.value[index])
|
||||
modal.value.stats = index == -1 ? false : v2rayStats.value.users.includes(clients.value[index].name)
|
||||
@@ -316,8 +505,8 @@ const updateLinks = (c:Client):Link[] => {
|
||||
|
||||
return links
|
||||
}
|
||||
const delClient = (clientIndex: number) => {
|
||||
const id = clients.value[clientIndex].id
|
||||
const delClient = (id: number) => {
|
||||
const clientIndex = clients.value.findIndex(c => c.id === id)
|
||||
const oldData = createClient(clients.value[clientIndex])
|
||||
|
||||
// Delete stats if exists and will be orphaned
|
||||
@@ -338,8 +527,9 @@ const qrcode = ref({
|
||||
index: 0,
|
||||
})
|
||||
|
||||
const showQrCode = (index: number) => {
|
||||
qrcode.value.index = index
|
||||
const showQrCode = (id: number) => {
|
||||
const clientIndex = clients.value.findIndex(c => c.id === id)
|
||||
qrcode.value.index = clientIndex
|
||||
qrcode.value.visible = true
|
||||
}
|
||||
const closeQrCode = () => {
|
||||
@@ -359,4 +549,47 @@ const showStats = (tag: string) => {
|
||||
const closeStats = () => {
|
||||
stats.value.visible = false
|
||||
}
|
||||
|
||||
var openedGroups = ref(<string[]>[""])
|
||||
|
||||
const toggleGroupOpen = (g: string) => {
|
||||
const index = openedGroups.value.findIndex(og => og == g)
|
||||
index == -1 ? openedGroups.value.push(g) : openedGroups.value.splice(index,1)
|
||||
}
|
||||
|
||||
const doFilter = () => {
|
||||
let filteredClients = clients.value.slice()
|
||||
if (filterSettings.value.group != '-') {
|
||||
filteredClients = filteredClients.filter(c => c.group == filterSettings.value.group)
|
||||
}
|
||||
if (filterSettings.value.text.length>0) {
|
||||
const txt = filterSettings.value.text
|
||||
filteredClients = filteredClients.filter(c => c.name.search(txt) != -1 || c.desc.search(txt) != -1)
|
||||
}
|
||||
switch (filterSettings.value.state) {
|
||||
case "disable":
|
||||
filteredClients = filteredClients.filter(c => c.enable == false)
|
||||
break
|
||||
case "expired":
|
||||
filteredClients = filteredClients.filter(c => HumanReadable.remainedDays(c.expiry) == null)
|
||||
break
|
||||
case "online":
|
||||
filteredClients = filteredClients.filter(c => Data().onlines?.user?.includes(c.name))
|
||||
break
|
||||
}
|
||||
filterSettings.value.filteredClients = filteredClients
|
||||
filterSettings.value.enabled = true
|
||||
filterMenu.value = false
|
||||
}
|
||||
|
||||
const clearFilter = () => {
|
||||
filterSettings.value = {
|
||||
enabled: false,
|
||||
state: '',
|
||||
group: '-',
|
||||
text: '',
|
||||
filteredClients: <any[]>[]
|
||||
}
|
||||
filterMenu.value = false
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user