only table in clients page & better load and save

This commit is contained in:
Alireza Ahmadi
2025-01-18 10:28:04 +01:00
parent 67582015d3
commit f18345b30d
14 changed files with 91 additions and 259 deletions
+2 -2
View File
@@ -258,11 +258,11 @@ func (a *APIHandler) loadData(c *gin.Context) (interface{}, error) {
func (a *APIHandler) loadPartialData(c *gin.Context, objs []string) error { func (a *APIHandler) loadPartialData(c *gin.Context, objs []string) error {
data := make(map[string]interface{}, 0) data := make(map[string]interface{}, 0)
id := c.Query("id")
for _, obj := range objs { for _, obj := range objs {
switch obj { switch obj {
case "inbounds": case "inbounds":
id := c.Query("id")
inbounds, err := a.InboundService.Get(id) inbounds, err := a.InboundService.Get(id)
if err != nil { if err != nil {
return err return err
@@ -287,7 +287,7 @@ func (a *APIHandler) loadPartialData(c *gin.Context, objs []string) error {
} }
data[obj] = tlsConfigs data[obj] = tlsConfigs
case "clients": case "clients":
clients, err := a.ClientService.GetAll() clients, err := a.ClientService.Get(id)
if err != nil { if err != nil {
return err return err
} }
+2 -2
View File
@@ -26,9 +26,9 @@ type Client struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Enable bool `json:"enable" form:"enable"` Enable bool `json:"enable" form:"enable"`
Name string `json:"name" form:"name"` Name string `json:"name" form:"name"`
Config json.RawMessage `json:"config" form:"config"` Config json.RawMessage `json:"config,omitempty" form:"config"`
Inbounds json.RawMessage `json:"inbounds" form:"inbounds"` Inbounds json.RawMessage `json:"inbounds" form:"inbounds"`
Links json.RawMessage `json:"links" form:"links"` Links json.RawMessage `json:"links,omitempty" form:"links"`
Volume int64 `json:"volume" form:"volume"` Volume int64 `json:"volume" form:"volume"`
Expiry int64 `json:"expiry" form:"expiry"` Expiry int64 `json:"expiry" form:"expiry"`
Down int64 `json:"down" form:"down"` Down int64 `json:"down" form:"down"`
+22 -4
View File
@@ -16,14 +16,32 @@ type ClientService struct {
InboundService InboundService
} }
func (s *ClientService) GetAll() ([]model.Client, error) { func (s *ClientService) Get(id string) (*[]model.Client, error) {
if id == "" {
return s.GetAll()
}
return s.getById(id)
}
func (s *ClientService) getById(id string) (*[]model.Client, error) {
db := database.GetDB() db := database.GetDB()
clients := []model.Client{} var client []model.Client
err := db.Model(model.Client{}).Scan(&clients).Error err := db.Model(model.Client{}).Where("id in ?", strings.Split(id, ",")).Scan(&client).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return clients, nil
return &client, nil
}
func (s *ClientService) GetAll() (*[]model.Client, error) {
db := database.GetDB()
var clients []model.Client
err := db.Model(model.Client{}).Select("`id`, `enable`, `name`, `desc`, `group`, `inbounds`, `up`, `down`, `volume`, `expiry`").Scan(&clients).Error
if err != nil {
return nil, err
}
return &clients, nil
} }
func (s *ClientService) Save(tx *gorm.DB, act string, data json.RawMessage, hostname string) ([]uint, error) { func (s *ClientService) Save(tx *gorm.DB, act string, data json.RawMessage, hostname string) ([]uint, error) {
+2
View File
@@ -120,6 +120,7 @@ func (s *ConfigService) Save(obj string, act string, data json.RawMessage, login
var err error var err error
var inboundIds []uint var inboundIds []uint
var inboundId uint var inboundId uint
var objs []string = []string{obj}
db := database.GetDB() db := database.GetDB()
tx := db.Begin() tx := db.Begin()
@@ -134,6 +135,7 @@ func (s *ConfigService) Save(obj string, act string, data json.RawMessage, login
switch obj { switch obj {
case "clients": case "clients":
inboundIds, err = s.ClientService.Save(tx, act, data, hostname) inboundIds, err = s.ClientService.Save(tx, act, data, hostname)
objs = append(objs, "inbounds")
case "tls": case "tls":
inboundIds, err = s.TlsService.Save(tx, act, data) inboundIds, err = s.TlsService.Save(tx, act, data)
case "inbounds": case "inbounds":
-2
View File
@@ -244,9 +244,7 @@ func hysteria2Link(
if downmbps, ok := inbound["down_mbps"].(string); ok { if downmbps, ok := inbound["down_mbps"].(string); ok {
params["down_mbps"] = downmbps params["down_mbps"] = downmbps
} }
fmt.Printf("%+v\n", addr["tls"])
if tls, ok := addr["tls"].(map[string]interface{}); ok { if tls, ok := addr["tls"].(map[string]interface{}); ok {
fmt.Printf("%+v\n", tls)
if sni, ok := tls["server_name"].(string); ok { if sni, ok := tls["server_name"].(string); ok {
params["sni"] = sni params["sni"] = sni
} }
+18 -9
View File
@@ -1,12 +1,18 @@
<template> <template>
<v-dialog transition="dialog-bottom-transition" width="800"> <v-dialog transition="dialog-bottom-transition" width="800">
<v-card class="rounded-lg"> <v-card class="rounded-lg" :loading="loading">
<v-card-title> <v-card-title>
{{ $t('actions.' + title) + " " + $t('objects.client') }} {{ $t('actions.' + title) + " " + $t('objects.client') }}
</v-card-title> </v-card-title>
<v-divider></v-divider> <v-divider></v-divider>
<v-skeleton-loader
class="mx-auto border"
width="95%"
type="card, text, divider, list-item-two-line"
v-if="loading"
></v-skeleton-loader>
<v-card-text style="padding: 0 16px; overflow-y: scroll;"> <v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-container style="padding: 0;"> <v-container style="padding: 0;" :hidden="loading">
<v-tabs <v-tabs
v-model="tab" v-model="tab"
align-tabs="center" align-tabs="center"
@@ -178,12 +184,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { createClient, randomConfigs, updateConfigs, Link } from '@/types/clients' import { createClient, randomConfigs, updateConfigs, Link, Client } from '@/types/clients'
import DatePick from '@/components/DateTime.vue' import DatePick from '@/components/DateTime.vue'
import { HumanReadable } from '@/plugins/utils' import { HumanReadable } from '@/plugins/utils'
import Data from '@/store/modules/data';
export default { export default {
props: ['visible', 'data', 'id', 'inboundTags', 'groups'], props: ['visible', 'id', 'inboundTags', 'groups'],
emits: ['close', 'save'], emits: ['close', 'save'],
data() { data() {
return { return {
@@ -198,21 +205,23 @@ export default {
} }
}, },
methods: { methods: {
updateData() { async updateData() {
if (this.$props.id > 0) { if (this.$props.id > 0) {
const newData = JSON.parse(this.$props.data) this.loading = true
const newData = await Data().loadClients(this.$props.id)
this.client = createClient(newData) this.client = createClient(newData)
this.title = "edit" this.title = "edit"
this.clientConfig = this.client.config this.clientConfig = this.client.config
this.loading = false
} }
else { else {
this.client = createClient() this.client = createClient()
this.title = "add" this.title = "add"
this.clientConfig = randomConfigs('client') this.clientConfig = randomConfigs('client')
} }
this.links = this.client.links.filter(l => l.type == 'local') this.links = this.client.links?.filter(l => l.type == 'local')?? []
this.extLinks = this.client.links.filter(l => l.type == 'external') this.extLinks = this.client.links?.filter(l => l.type == 'external')?? []
this.subLinks = this.client.links.filter(l => l.type == 'sub') this.subLinks = this.client.links?.filter(l => l.type == 'sub')?? []
this.tab = "t1" this.tab = "t1"
}, },
closeModal() { closeModal() {
@@ -61,7 +61,6 @@
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
<pre dir="ltr">{{ bulkData }}</pre>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
+11 -3
View File
@@ -5,6 +5,7 @@ import { i18n } from '@/locales'
import { Inbound } from '@/types/inbounds' import { Inbound } from '@/types/inbounds'
import { Outbound } from '@/types/outbounds' import { Outbound } from '@/types/outbounds'
import { Endpoint } from '@/types/endpoints' import { Endpoint } from '@/types/endpoints'
import { Client } from '@/types/clients'
const Data = defineStore('Data', { const Data = defineStore('Data', {
state: () => ({ state: () => ({
@@ -13,10 +14,10 @@ const Data = defineStore('Data', {
subURI: "", subURI: "",
onlines: {inbound: <string[]>[], outbound: <string[]>[], user: <string[]>[]}, onlines: {inbound: <string[]>[], outbound: <string[]>[], user: <string[]>[]},
config: <any>{}, config: <any>{},
inbounds: <Inbound[]>[], inbounds: <any[]>[],
outbounds: <Outbound[]>[], outbounds: <Outbound[]>[],
endpoints: <Endpoint[]>[], endpoints: <Endpoint[]>[],
clients: [], clients: <any>[],
tlsConfigs: <any[]>[], tlsConfigs: <any[]>[],
}), }),
actions: { actions: {
@@ -55,7 +56,14 @@ const Data = defineStore('Data', {
} }
return <Inbound[]>[] return <Inbound[]>[]
}, },
async save (object: string, action: string, data: any): Promise<boolean> { async loadClients(id: number): Promise<Client> {
const options = id > 0 ? {id: id} : {}
const msg = await HttpUtils.get('api/clients', options)
if(msg.success) {
return <Client>msg.obj.clients[0]??{}
}
return <Client>{}
},
let postData = { let postData = {
object: object, object: object,
action: action, action: action,
+2 -2
View File
@@ -10,9 +10,9 @@ export interface Client {
id?: number id?: number
enable: boolean enable: boolean
name: string name: string
config: Config config?: Config
inbounds: number[] inbounds: number[]
links: Link[] links?: Link[]
volume: number volume: number
expiry: number expiry: number
up: number up: number
+12 -53
View File
@@ -52,32 +52,6 @@ interface InboundBasics extends Listen {
out_json?: any out_json?: any
} }
interface UsernamePass {
username: string
password: string
}
interface NamePass {
name: string
password: string
}
interface NameUUID {
name: string
uuid: string
}
interface NameAuth {
name: string
auth_str: string
}
interface VmessUser extends NameUUID {
alterId: number
}
interface VlessUser extends NameUUID {
flow: string
}
interface TuicUser extends NameUUID {
password?: string
}
interface ShadowTLSHandShake extends Dial { interface ShadowTLSHandShake extends Dial {
server: string server: string
server_port: number server_port: number
@@ -88,30 +62,21 @@ export interface Direct extends InboundBasics {
override_address?: string override_address?: string
override_port?: number override_port?: number
} }
export interface Mixed extends InboundBasics { export interface Mixed extends InboundBasics {}
users?: UsernamePass[] export interface SOCKS extends InboundBasics {}
} export interface HTTP extends InboundBasics {}
export interface SOCKS extends InboundBasics {
users?: UsernamePass[]
}
export interface HTTP extends InboundBasics {
users?: UsernamePass[]
}
export interface Shadowsocks extends InboundBasics { export interface Shadowsocks extends InboundBasics {
method: string method: string
password: string password: string
network?: "udp" | "tcp" network?: "udp" | "tcp"
users?: NamePass[]
multiplex?: iMultiplex multiplex?: iMultiplex
} }
export interface VMess extends InboundBasics { export interface VMess extends InboundBasics {
users: VmessUser[]
tls: iTls tls: iTls
multiplex?: iMultiplex multiplex?: iMultiplex
transport?: Transport transport?: Transport
} }
export interface Trojan extends InboundBasics { export interface Trojan extends InboundBasics {
users: NamePass[]
tls: iTls tls: iTls
fallback?: { fallback?: {
server: string server: string
@@ -121,14 +86,12 @@ export interface Trojan extends InboundBasics {
transport?: Transport transport?: Transport
} }
export interface Naive extends InboundBasics { export interface Naive extends InboundBasics {
users: UsernamePass[]
tls: iTls, tls: iTls,
} }
export interface Hysteria extends InboundBasics { export interface Hysteria extends InboundBasics {
up_mbps: number up_mbps: number
down_mbps: number down_mbps: number
obfs?: string obfs?: string
users: NameAuth[]
recv_window_conn?: number recv_window_conn?: number
recv_window_client?: number recv_window_client?: number
max_conn_client?: number max_conn_client?: number
@@ -137,7 +100,6 @@ export interface Hysteria extends InboundBasics {
export interface ShadowTLS extends InboundBasics { export interface ShadowTLS extends InboundBasics {
version: 1|2|3 version: 1|2|3
password?: string password?: string
users?: NamePass[]
handshake: ShadowTLSHandShake handshake: ShadowTLSHandShake
handshake_for_server_name?: { handshake_for_server_name?: {
[server_name: string]: ShadowTLSHandShake [server_name: string]: ShadowTLSHandShake
@@ -145,12 +107,10 @@ export interface ShadowTLS extends InboundBasics {
strict_mode?: boolean strict_mode?: boolean
} }
export interface VLESS extends InboundBasics { export interface VLESS extends InboundBasics {
users: VlessUser[]
multiplex?: iMultiplex multiplex?: iMultiplex
transport?: Transport transport?: Transport
} }
export interface TUIC extends InboundBasics { export interface TUIC extends InboundBasics {
users: TuicUser[]
congestion_control: ""|"cubic"|"new_reno"|"bbr" congestion_control: ""|"cubic"|"new_reno"|"bbr"
auth_timeout?: string auth_timeout?: string
zero_rtt_handshake?: boolean zero_rtt_handshake?: boolean
@@ -163,7 +123,6 @@ export interface Hysteria2 extends InboundBasics {
type?: "salamander" type?: "salamander"
password: string password: string
} }
users: NamePass[]
ignore_client_bandwidth?: boolean ignore_client_bandwidth?: boolean
masquerade?: string | { masquerade?: string | {
type: string type: string
@@ -245,7 +204,7 @@ type userEnabledTypes = {
vless: VLESS vless: VLESS
} }
export const inboundWithUsers = ['mixed', 'socks:', 'http', 'shadowsocks', 'vmess', 'trojan', 'naive', 'hysteria', 'shadowtls', 'tuic', 'hysteria2', 'vless'] export const inboundWithUsers = ['mixed', 'socks', 'http', 'shadowsocks', 'vmess', 'trojan', 'naive', 'hysteria', 'shadowtls', 'tuic', 'hysteria2', 'vless']
// Create union type from userEnabledTypes // Create union type from userEnabledTypes
type InboundWithUser = userEnabledTypes[keyof userEnabledTypes] type InboundWithUser = userEnabledTypes[keyof userEnabledTypes]
@@ -257,14 +216,14 @@ const defaultValues: Record<InType, Inbound> = {
socks: <SOCKS>{ type: InTypes.SOCKS }, socks: <SOCKS>{ type: InTypes.SOCKS },
http: <HTTP>{ type: InTypes.HTTP, tls_id: 0 }, http: <HTTP>{ type: InTypes.HTTP, tls_id: 0 },
shadowsocks: <Shadowsocks>{ type: InTypes.Shadowsocks, method: 'none', multiplex: {} }, shadowsocks: <Shadowsocks>{ type: InTypes.Shadowsocks, method: 'none', multiplex: {} },
vmess: <VMess>{ type: InTypes.VMess, users: <VmessUser[]>[], tls_id: 0, multiplex: {}, transport: {} }, vmess: <VMess>{ type: InTypes.VMess, tls_id: 0, multiplex: {}, transport: {} },
trojan: <Trojan>{ type: InTypes.Trojan, users: <NamePass[]>[], tls_id: 0, multiplex: {}, transport: {} }, trojan: <Trojan>{ type: InTypes.Trojan, tls_id: 0, multiplex: {}, transport: {} },
naive: <Naive>{ type: InTypes.Naive, users: <UsernamePass[]>[], tls_id: 0 }, naive: <Naive>{ type: InTypes.Naive, tls_id: 0 },
hysteria: <Hysteria>{ type: InTypes.Hysteria, users: <NameAuth[]>[], up_mbps: 100, down_mbps: 100, tls_id: 0 }, hysteria: <Hysteria>{ type: InTypes.Hysteria, up_mbps: 100, down_mbps: 100, tls_id: 0 },
shadowtls: <ShadowTLS>{ type: InTypes.ShadowTLS, version: 3, users: <NamePass[]>[], handshake: {}, handshake_for_server_name: {} }, shadowtls: <ShadowTLS>{ type: InTypes.ShadowTLS, version: 3, handshake: {}, handshake_for_server_name: {} },
tuic: <TUIC>{ type: InTypes.TUIC, users: <TuicUser[]>[], congestion_control: "cubic", tls_id: 0 }, tuic: <TUIC>{ type: InTypes.TUIC, congestion_control: "cubic", tls_id: 0 },
hysteria2: <Hysteria2>{ type: InTypes.Hysteria2, users: <NamePass[]>[], tls_id: 0 }, hysteria2: <Hysteria2>{ type: InTypes.Hysteria2, tls_id: 0 },
vless: <VLESS>{ type: InTypes.VLESS, users: <VlessUser[]>[], tls_id: 0, multiplex: {}, transport: {} }, vless: <VLESS>{ type: InTypes.VLESS, tls_id: 0, multiplex: {}, transport: {} },
tun: <Tun>{ type: InTypes.Tun, mtu: 9000, stack: 'system', udp_timeout: '5m', auto_route: false }, tun: <Tun>{ type: InTypes.Tun, mtu: 9000, stack: 'system', udp_timeout: '5m', auto_route: false },
redirect: <Redirect>{ type: InTypes.Redirect }, redirect: <Redirect>{ type: InTypes.Redirect },
tproxy: <TProxy>{ type: InTypes.TProxy }, tproxy: <TProxy>{ type: InTypes.TProxy },
-4
View File
@@ -264,10 +264,6 @@ const outboundTags = computed((): string[] => {
return [...Data().outbounds?.map((o:any) => o.tag), ...Data().endpoints?.map((e:any) => e.tag)] return [...Data().outbounds?.map((o:any) => o.tag), ...Data().endpoints?.map((e:any) => e.tag)]
}) })
const clientNames = computed((): string[] => {
return Data().clients.map((c:any) => c.name)
})
const levels = ["trace", "debug", "info", "warn", "error", "fatal", "panic"] const levels = ["trace", "debug", "info", "warn", "error", "fatal", "panic"]
const dnsServersTags = computed((): string[] => { const dnsServersTags = computed((): string[] => {
+12 -155
View File
@@ -4,7 +4,6 @@
v-model="modal.visible" v-model="modal.visible"
:visible="modal.visible" :visible="modal.visible"
:id="modal.id" :id="modal.id"
:data="modal.data"
:groups="groups" :groups="groups"
:inboundTags="inboundTags" :inboundTags="inboundTags"
@close="closeModal" @close="closeModal"
@@ -116,129 +115,8 @@
</v-card> </v-card>
</v-menu> </v-menu>
</v-col> </v-col>
<v-col cols="auto">
<v-btn hide-details variant="text" icon @click="toggleClientView">
<v-icon :icon="tableView ? 'mdi-table-eye' : 'mdi-table-eye-off'" :color="tableView ? 'primary' : ''"></v-icon>
</v-btn>
</v-col>
</v-row>
<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>
<v-col>{{ item.name }}</v-col>
<v-spacer></v-spacer>
<v-col cols="auto">
<v-switch color="primary"
v-model="item.enable"
hideDetails density="compact" />
</v-col>
</v-row>
</v-card-title>
<v-card-subtitle style="margin-top: -20px;">
<v-row>
<v-col>{{ item.desc }}</v-col>
</v-row>
</v-card-subtitle>
<v-card-text>
<v-row>
<v-col>{{ $t('pages.inbounds') }}</v-col>
<v-col>
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="item.inbounds != ''">
<span v-for="i in item.inbounds">{{ inbounds.find(inb => inb.id == i)?.tag }}<br /></span>
</v-tooltip>
{{ item.inbounds.length }}
</v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col>{{ $t('stats.volume') }}</v-col>
<v-col>
{{ item.volume == 0 ? $t('unlimited') : HumanReadable.sizeFormat(item.volume) }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('date.expiry') }}</v-col>
<v-col>
{{ HumanReadable.remainedDays(item.expiry) }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('stats.usage') }}</v-col>
<v-col>
<v-tooltip activator="parent" location="bottom">
{{ $t('stats.upload') }}:{{ HumanReadable.sizeFormat(item.up) }}<br />
{{ $t('stats.download') }}:{{ HumanReadable.sizeFormat(item.down) }}<br />
<template v-if="item.volume>0">
{{ $t('remained') }}: {{ HumanReadable.sizeFormat(item.volume - (item.up + item.down)) }}
</template>
</v-tooltip>
{{ HumanReadable.sizeFormat(item.up + item.down) }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('online') }}</v-col>
<v-col>
<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>
</v-col>
</v-row>
</v-card-text>
<v-divider></v-divider>
<v-card-actions style="padding: 0;">
<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-btn>
<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[clients.findIndex(c => c.id == item.id)]"
contained
class="align-center justify-center"
>
<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-overlay>
<v-btn icon="mdi-qrcode" @click="showQrCode(item.id)">
<v-icon />
<v-tooltip activator="parent" location="top" text="QR-Code"></v-tooltip>
</v-btn>
<v-btn icon="mdi-chart-line" @click="showStats(item.name)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</template>
</v-row>
</template>
<v-row v-else>
<v-col cols="12"> <v-col cols="12">
<v-data-table <v-data-table
:headers="headers" :headers="headers"
@@ -246,21 +124,19 @@
:hide-default-footer="filterSettings.enabled ? filterSettings.filteredClients.length<=10 : clients.length<=10" :hide-default-footer="filterSettings.enabled ? filterSettings.filteredClients.length<=10 : clients.length<=10"
hide-no-data hide-no-data
fixed-header fixed-header
:group-by="groupBy"
item-value="name" item-value="name"
:mobile="smAndDown" :mobile="smAndDown"
mobile-breakpoint="sm" mobile-breakpoint="sm"
width="100%" width="100%"
class="elevation-3 rounded" class="elevation-3 rounded"
> >
<template v-slot:group-header="{ item, columns, toggleGroup, isGroupOpen }"> <template v-slot:item.inbounds="{ item }">
<tr> <span>
<td :colspan="columns.length" @click="toggleGroup(item)" style="min-height: fit-content; text-align: center;"> <v-tooltip activator="parent" dir="ltr" location="start" v-if="item.inbounds != ''">
<v-icon :icon="isGroupOpen(item) ? '$expand' : '$next'"></v-icon> <span v-for="i in item.inbounds">{{ inbounds.find(inb => inb.id == i)?.tag }}<br /></span>
{{ item.value.length>0 ? item.value : $t('none') }} </v-tooltip>
<v-badge :content="(filterSettings.enabled ? filterSettings.filteredClients : clients).filter(c => c.group == item.value).length" inline color="success" /> {{ item.inbounds.length }}
</td> </span>
</tr>
</template> </template>
<template v-slot:item.volume="{ item }"> <template v-slot:item.volume="{ item }">
<div class="text-start"> <div class="text-start">
@@ -363,13 +239,13 @@ const isOnline = (cname: string) => computed(() => {
return Data().onlines?.user ? Data().onlines.user.includes(cname) : false return Data().onlines?.user ? Data().onlines.user.includes(cname) : false
}) })
const inbounds = computed((): Inbound[] => { const inbounds = computed((): any[] => {
return <Inbound[]> Data().inbounds?? [] return Data().inbounds?? []
}) })
const inboundTags = computed((): any[] => { const inboundTags = computed((): any[] => {
if (!inbounds.value) return [] if (!inbounds.value) return []
return inbounds.value?.filter(i => i.tag != "" && inboundWithUsers.includes(i.type)).map(i => { return { title: i.tag, value: i.id } }) return inbounds.value?.filter(i => i.tag != "" && i.users).map(i => { return { title: i.tag, value: i.id } })
}) })
const groups = computed((): string[] => { const groups = computed((): string[] => {
@@ -387,12 +263,6 @@ const filterSettings = ref({
text: '', text: '',
filteredClients: <any[]>[] filteredClients: <any[]>[]
}) })
const tableView = ref(localStorage.getItem('clientView') == 'table')
const toggleClientView = () => {
localStorage.setItem('clientView',tableView.value ? 'tile' : 'table')
tableView.value = !tableView.value
}
const filterItems = [ const filterItems = [
{ title: i18n.global.t('none'), value: '' }, { title: i18n.global.t('none'), value: '' },
@@ -403,30 +273,24 @@ const filterItems = [
const headers = [ const headers = [
{ title: i18n.global.t('client.name'), key: 'name' }, { title: i18n.global.t('client.name'), key: 'name' },
{ title: i18n.global.t('client.desc'), key: 'desc', sortable: false }, { title: i18n.global.t('client.group'), key: 'group' },
{ title: i18n.global.t('pages.inbounds'), key: 'inbounds', width: 10 },
{ title: i18n.global.t('actions.action'), key: 'actions', sortable: false}, { title: i18n.global.t('actions.action'), key: 'actions', sortable: false},
{ title: i18n.global.t('stats.volume'), key: 'volume' }, { title: i18n.global.t('stats.volume'), key: 'volume' },
{ title: i18n.global.t('date.expiry'), key: 'expiry' }, { title: i18n.global.t('date.expiry'), key: 'expiry' },
{ title: i18n.global.t('online'), key: 'online' }, { title: i18n.global.t('online'), key: 'online' },
{ key: 'data-table-group', width: 0 }, { key: 'data-table-group', width: 0 },
] ]
const groupBy = [
{
key: 'group'
}
]
const modal = ref({ const modal = ref({
visible: false, visible: false,
id: 0, id: 0,
data: "",
}) })
const delOverlay = ref(new Array<boolean>(clients.value.length).fill(false)) const delOverlay = ref(new Array<boolean>(clients.value.length).fill(false))
const showModal = async (id: number) => { const showModal = async (id: number) => {
modal.value.id = id modal.value.id = id
modal.value.data = id == 0 ? '' : JSON.stringify(clients.value.findLast(o => o.id == id))
modal.value.visible = true modal.value.visible = true
} }
const closeModal = () => { const closeModal = () => {
@@ -481,13 +345,6 @@ const closeStats = () => {
stats.value.visible = false 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 = () => { const doFilter = () => {
let filteredClients = clients.value.slice() let filteredClients = clients.value.slice()
if (filterSettings.value.group != '-') { if (filterSettings.value.group != '-') {
+7 -16
View File
@@ -51,11 +51,11 @@
<v-row> <v-row>
<v-col>{{ $t('pages.clients') }}</v-col> <v-col>{{ $t('pages.clients') }}</v-col>
<v-col> <v-col>
<template v-if="inboundWithUsers.includes(item.type)"> <template v-if="item.users">
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="findInboundUsers(item).length > 0"> <v-tooltip activator="parent" dir="ltr" location="bottom" v-if="item.users.length > 0">
<span v-for="u in findInboundUsers(item)">{{ u }}<br /></span> <span v-for="u in item.users">{{ u }}<br /></span>
</v-tooltip> </v-tooltip>
{{ findInboundUsers(item).length }} {{ item.users.length }}
</template> </template>
<template v-else>-</template> <template v-else>-</template>
</v-col> </v-col>
@@ -110,8 +110,7 @@ import InboundVue from '@/layouts/modals/Inbound.vue'
import Stats from '@/layouts/modals/Stats.vue' import Stats from '@/layouts/modals/Stats.vue'
import { Config } from '@/types/config' import { Config } from '@/types/config'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { Inbound, inboundWithUsers } from '@/types/inbounds' import { Inbound } from '@/types/inbounds'
import { Client } from '@/types/clients'
import { i18n } from '@/locales' import { i18n } from '@/locales'
import { push } from 'notivue' import { push } from 'notivue'
@@ -135,10 +134,6 @@ const outTags = computed((): string[] => {
return appConfig.value.outbounds?.map(i => i.tag) return appConfig.value.outbounds?.map(i => i.tag)
}) })
const clients = computed((): Client[] => {
return <Client[]> Data().clients
})
const onlines = computed(() => { const onlines = computed(() => {
return Data().onlines.inbound?? [] return Data().onlines.inbound?? []
}) })
@@ -157,7 +152,7 @@ const showModal = (id: number) => {
const closeModal = () => { const closeModal = () => {
modal.value.visible = false modal.value.visible = false
} }
const saveModal = async (data:Inbound) => { const saveModal = async (data:Inbound, initUsers?: number[]) => {
// Check duplicate tag // Check duplicate tag
const oldInbound = modal.value.id > 0 ? inbounds.value.findLast(i => i.id == modal.value.id) : null const oldInbound = modal.value.id > 0 ? inbounds.value.findLast(i => i.id == modal.value.id) : null
if (data.tag != oldInbound?.tag && inTags.value.includes(data.tag)) { if (data.tag != oldInbound?.tag && inTags.value.includes(data.tag)) {
@@ -168,7 +163,7 @@ const saveModal = async (data:Inbound) => {
} }
// save data // save data
const success = await Data().save("inbounds", modal.value.id == 0 ? "new" : "edit", data) const success = await Data().save("inbounds", modal.value.id == 0 ? "new" : "edit", data, initUsers)
if (success) modal.value.visible = false if (success) modal.value.visible = false
} }
@@ -180,10 +175,6 @@ const delInbound = async (id: number) => {
if (success) delOverlay.value[index] = false if (success) delOverlay.value[index] = false
} }
const findInboundUsers = (i: Inbound): string[] => {
return clients.value.filter(c => c.inbounds.includes(i.id)).map(c => c.name)
}
const stats = ref({ const stats = ref({
visible: false, visible: false,
resource: "inbound", resource: "inbound",
-5
View File
@@ -89,7 +89,6 @@ import TlsVue from '@/layouts/modals/Tls.vue'
import Data from '@/store/modules/data' import Data from '@/store/modules/data'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { Inbound } from '@/types/inbounds' import { Inbound } from '@/types/inbounds'
import { Client } from '@/types/clients'
import { tls } from '@/types/tls' import { tls } from '@/types/tls'
const tlsConfigs = computed((): any[] => { const tlsConfigs = computed((): any[] => {
@@ -104,10 +103,6 @@ const tlsInbounds = (id: number): string[] => {
return inbounds.value.filter(i => i.tls_id == id).map(i => i.tag) return inbounds.value.filter(i => i.tls_id == id).map(i => i.tag)
} }
const clients = computed((): any[] => {
return <Client[]>Data().clients
})
const modal = ref({ const modal = ref({
visible: false, visible: false,
id: 0, id: 0,