initial commit
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<v-app-bar :elevation="5">
|
||||
<v-icon v-if="isMobile" icon="mdi-menu" @click="$emit('toggleDrawer')" />
|
||||
<v-app-bar-title :text="$t(<string>$router.currentRoute.value.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>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref,watch } from "vue"
|
||||
import { useTheme } from "vuetify"
|
||||
import { FindDiff } from "@/plugins/utils"
|
||||
import Data from "@/store/modules/data"
|
||||
|
||||
defineProps(['isMobile'])
|
||||
|
||||
const theme = useTheme()
|
||||
const darkMode = ref(localStorage.getItem('theme') == "dark")
|
||||
|
||||
const store = Data()
|
||||
|
||||
const toggleTheme = () => {
|
||||
darkMode.value = !darkMode.value
|
||||
theme.global.name.value = darkMode.value ? "dark" : "light"
|
||||
localStorage.setItem('theme', theme.global.name.value)
|
||||
}
|
||||
|
||||
const saveChanges = () => {
|
||||
store.pushData()
|
||||
}
|
||||
|
||||
const oldData = computed((): any => {
|
||||
return {config: store.oldData.config, clients: store.oldData.clients}
|
||||
})
|
||||
|
||||
const newData = computed((): any => {
|
||||
return {config: store.config, clients: store.clients}
|
||||
})
|
||||
|
||||
const stateChange = computed((): any => {
|
||||
return !FindDiff.deepCompare(newData.value,oldData.value)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<v-app style="overflow: auto;">
|
||||
<drawer :isMobile="isMobile" :displayDrawer="displayDrawer" @toggleDrawer="toggleDrawer" />
|
||||
<default-bar :isMobile="isMobile" @toggleDrawer="toggleDrawer" />
|
||||
<default-view />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import DefaultBar from './AppBar.vue'
|
||||
import Drawer from './Drawer.vue'
|
||||
import DefaultView from './View.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const { smAndDown } = useDisplay()
|
||||
const displayDrawer = ref(false)
|
||||
|
||||
const toggleDrawer = () => {
|
||||
displayDrawer.value = !displayDrawer.value
|
||||
}
|
||||
|
||||
const isMobile = computed( ():boolean =>{
|
||||
displayDrawer.value = !smAndDown.value
|
||||
return smAndDown.value
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
v-model="showDrawer"
|
||||
:temporary="isMobile"
|
||||
:expand-on-hover="!isMobile"
|
||||
:rail="!isMobile"
|
||||
:permanent="!isMobile"
|
||||
@click="isMobile ? $emit('toggleDrawer') : null"
|
||||
>
|
||||
<v-list-item
|
||||
height="63"
|
||||
prepend-avatar="@/assets/logo.svg"
|
||||
title="S-UI"
|
||||
>
|
||||
<template v-slot:append v-if="isMobile">
|
||||
<v-icon icon="mdi-close" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-list density="compact" nav>
|
||||
<v-list-item link
|
||||
v-for="item in menu"
|
||||
:key="item.title"
|
||||
:to="item.path"
|
||||
:active="router.currentRoute.value.path == item.path">
|
||||
<template v-slot:prepend>
|
||||
<v-icon :icon="item.icon"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title v-text="$t(item.title)"></v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<template v-slot:append>
|
||||
<v-list-item prepend-icon="mdi-logout" :title="$t('menu.logout')" @click="logout"></v-list-item>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import router from '@/router'
|
||||
import HttpUtil from '@/plugins/httputil'
|
||||
|
||||
const props = defineProps(['isMobile','displayDrawer'])
|
||||
|
||||
const showDrawer = computed((): boolean => {
|
||||
return props.displayDrawer
|
||||
})
|
||||
|
||||
const menu = [
|
||||
{ title: 'pages.home', icon: 'mdi-home', path: '/' },
|
||||
{ title: 'pages.inbounds', icon: 'mdi-cloud-download', path: '/inbounds' },
|
||||
{ title: 'pages.clients', icon: 'mdi-account-multiple', path: '/clients' },
|
||||
{ title: 'pages.outbounds', icon: 'mdi-cloud-upload', path: '/outbounds' },
|
||||
{ title: 'pages.rules', icon: 'mdi-routes', path: '/rules' },
|
||||
{ title: 'pages.basics', icon: 'mdi-application-cog', path: '/basics' },
|
||||
{ title: 'pages.settings', icon: 'mdi-cog', path: '/settings' },
|
||||
]
|
||||
|
||||
const logout = async () => {
|
||||
const response = await HttpUtil.get('/api/logout')
|
||||
if(response.success){
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.v-main {
|
||||
margin: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.client') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px;">
|
||||
<v-container style="padding: 0;">
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
align-tabs="center"
|
||||
>
|
||||
<v-tab value="t1">{{ $t('client.basics') }}</v-tab>
|
||||
<v-tab value="t2">{{ $t('client.config') }}</v-tab>
|
||||
<v-tab value="t3">{{ $t('client.links') }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="tab">
|
||||
<v-window-item value="t1">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" v-model="client.enable" :label="$t('enable')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="client.name" :label="$t('client.name')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model.number="Volume" type="number" min="0" :label="$t('stats.volume')" suffix="GiB" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<DatePick :expiry="expDate" @submit="setDate" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-combobox
|
||||
v-model="clientInbounds"
|
||||
:items="inboundTags"
|
||||
:label="$t('client.inboundTags')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<v-switch v-model="clientStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
<v-window-item value="t2">
|
||||
<v-row v-for="(value, key) in clientConfig" :key="key">
|
||||
<v-col cols="12" md="3" align="end" align-self="center">
|
||||
{{ key }}
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-if="value.password != undefined"
|
||||
label="Password"
|
||||
v-model="value.password"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
v-if="value.uuid != undefined"
|
||||
label="UUID"
|
||||
v-model="value.uuid"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
v-if="value.flow != undefined"
|
||||
label="Flow"
|
||||
v-model="value.flow"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
v-if="value.auth_str != undefined"
|
||||
label="Auth"
|
||||
v-model="value.auth_str"
|
||||
hide-details>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
<v-window-item value="t3">
|
||||
<v-row v-for="(lnk, index) in links">
|
||||
<v-col cols="auto">{{ index + 1 }}</v-col>
|
||||
<v-col style="direction: ltr; overflow-y: hidden;">{{ lnk.uri }}</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn color="primary" @click="extLinks.push({ type: 'external', uri: ''})">{{ $t('actions.add') }} {{ $t('client.external') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-for="(lnk, index) in extLinks">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
dir="ltr"
|
||||
:label="$t('client.external') + ' ' + (index+1)"
|
||||
append-icon="mdi-delete"
|
||||
@click:append="extLinks.splice(index,1)"
|
||||
placeholder="<protocol>://<data>"
|
||||
v-model="lnk.uri" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn color="primary" @click="subLinks.push({ type: 'sub', uri: ''})">{{ $t('actions.add') }} {{ $t('client.sub') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-for="(lnk, index) in subLinks">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
dir="ltr"
|
||||
:label="$t('client.sub') + ' ' + (index+1)"
|
||||
append-icon="mdi-delete"
|
||||
@click:append="subLinks.splice(index,1)"
|
||||
placeholder="http[s]://<domain>[:]<port>/<path>"
|
||||
v-model="lnk.uri" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="outlined"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="tonal"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Link } from '@/plugins/link'
|
||||
import { createClient, randomConfigs, updateConfigs } from '@/types/clients'
|
||||
import DatePick from '@/components/DateTime.vue'
|
||||
|
||||
export default {
|
||||
props: ['visible', 'data', 'index', 'inboundTags', 'stats'],
|
||||
emits: ['close', 'save'],
|
||||
data() {
|
||||
return {
|
||||
client: createClient(),
|
||||
title: "add",
|
||||
clientStats: false,
|
||||
tab: "t1",
|
||||
clientConfig: <any>[],
|
||||
links: <Link[]>[],
|
||||
extLinks: <Link[]>[],
|
||||
subLinks: <Link[]>[],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData() {
|
||||
if (this.$props.index != -1) {
|
||||
const newData = JSON.parse(this.$props.data)
|
||||
this.client = createClient(newData)
|
||||
this.title = "edit"
|
||||
this.clientConfig = JSON.parse(this.client.config)
|
||||
}
|
||||
else {
|
||||
this.client = createClient()
|
||||
this.title = "add"
|
||||
this.clientConfig = randomConfigs('client')
|
||||
}
|
||||
this.clientStats = this.$props.stats
|
||||
const allLinks = <Link[]>JSON.parse(this.client.links)
|
||||
this.links = allLinks.filter(l => l.type == 'local')
|
||||
this.extLinks = allLinks.filter(l => l.type == 'external')
|
||||
this.subLinks = allLinks.filter(l => l.type == 'sub')
|
||||
this.tab = "t1"
|
||||
},
|
||||
closeModal() {
|
||||
this.updateData() // reset
|
||||
this.$emit('close')
|
||||
},
|
||||
saveChanges() {
|
||||
this.client.config = updateConfigs(JSON.stringify(this.clientConfig), this.client.name)
|
||||
this.client.links = JSON.stringify([
|
||||
...this.links,
|
||||
...this.extLinks.filter(l => l.uri != ''),
|
||||
...this.subLinks.filter(l => l.uri != '')])
|
||||
this.$emit('save', this.client, this.clientStats)
|
||||
},
|
||||
setDate(newDate:number){
|
||||
this.client.expiry = newDate
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
clientInbounds: {
|
||||
get() { return this.client.inbounds == "" ? [] : this.client.inbounds.split(',') },
|
||||
set(newValue:string[]) { this.client.inbounds = newValue.length == 0 ? "" : newValue.join(',') }
|
||||
},
|
||||
expDate: {
|
||||
get() { return this.client.expiry},
|
||||
set(v:any) { this.client.expiry = v }
|
||||
},
|
||||
Volume: {
|
||||
get() { return this.client.volume == 0 ? 0 : (this.client.volume / (1024 ** 3)) },
|
||||
set(v:number) { this.client.volume = v > 0 ? v*(1024 ** 3) : 0 }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) { if (newValue) {
|
||||
this.updateData()
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { DatePick },
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.inbound') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
width="100"
|
||||
:label="$t('type')"
|
||||
:items="Object.keys(inTypes).map((key,index) => ({title: key, value: Object.values(inTypes)[index]}))"
|
||||
v-model="inbound.type"
|
||||
@update:modelValue="changeType">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="inbound.tag" :label="$t('in.tag')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Listen :inbound="inbound" />
|
||||
<Direct v-if="inbound.type == inTypes.Direct" :inbound="inbound" />
|
||||
<Shadowsocks v-if="inbound.type == inTypes.Shadowsocks" :inbound="inbound" />
|
||||
<Hysteria v-if="inbound.type == inTypes.Hysteria" :inbound="inbound" />
|
||||
<Hysteria2 v-if="inbound.type == inTypes.Hysteria2" :inbound="inbound" />
|
||||
<Naive v-if="inbound.type == inTypes.Naive" :inbound="inbound" />
|
||||
<ShadowTls v-if="inbound.type == inTypes.ShadowTLS" :inbound="inbound" />
|
||||
<Tuic v-if="inbound.type == inTypes.TUIC" :inbound="inbound" />
|
||||
<TProxy v-if="inbound.type == inTypes.TProxy" :inbound="inbound" />
|
||||
<Transport v-if="Object.hasOwn(inbound,'transport')" :inbound="inbound" />
|
||||
<Users v-if="HasOptionalUser.includes(inbound.type)" :inbound="inbound" :id="id" />
|
||||
<InTls v-if="Object.hasOwn(inbound,'tls')" :inbound="inbound" />
|
||||
<InMulitiplex v-if="Object.hasOwn(inbound,'multiplex')" :inbound="inbound" />
|
||||
<v-switch v-model="inboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="text"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('actions.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="text"
|
||||
@click="saveChanges"
|
||||
>
|
||||
{{ $t('actions.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { InTypes, createInbound } from '@/types/inbounds'
|
||||
import Listen from '@/components/Listen.vue'
|
||||
import Direct from '@/components/protocols/Direct.vue'
|
||||
import Users from '@/components/Users.vue'
|
||||
import Shadowsocks from '@/components/protocols/Shadowsocks.vue'
|
||||
import Hysteria from '@/components/protocols/Hysteria.vue'
|
||||
import Hysteria2 from '@/components/protocols/Hysteria2.vue'
|
||||
import Naive from '@/components/protocols/Naive.vue'
|
||||
import ShadowTls from '@/components/protocols/ShadowTls.vue'
|
||||
import Tuic from '@/components/protocols/Tuic.vue'
|
||||
import InTls from '@/components/InTLS.vue'
|
||||
import TProxy from '@/components/protocols/TProxy.vue'
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
import InMulitiplex from '@/components/InMulitiplex.vue'
|
||||
import Transport from '@/components/Transport.vue'
|
||||
export default {
|
||||
props: ['visible', 'data', 'id', 'stats'],
|
||||
emits: ['close', 'save'],
|
||||
data() {
|
||||
return {
|
||||
inbound: createInbound("direct",{ "tag": "" }),
|
||||
title: "add",
|
||||
inTypes: InTypes,
|
||||
inboundStats: false,
|
||||
HasOptionalUser: [InTypes.Mixed,InTypes.SOCKS,InTypes.HTTP,InTypes.Shadowsocks],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData() {
|
||||
if (this.$props.id != -1) {
|
||||
const newData = JSON.parse(this.$props.data)
|
||||
this.inbound = createInbound(newData.type, newData)
|
||||
this.title = "edit"
|
||||
}
|
||||
else {
|
||||
const port = RandomUtil.randomIntRange(10000, 60000)
|
||||
this.inbound = createInbound("mixed",{ tag: "in-"+port ,listen: "::", listen_port: port })
|
||||
this.title = "add"
|
||||
}
|
||||
this.inboundStats = this.$props.stats
|
||||
},
|
||||
changeType() {
|
||||
const prevConfig = { tag: this.inbound.tag ,listen: this.inbound.listen, listen_port: this.inbound.listen_port }
|
||||
this.inbound = createInbound(this.inbound.type, prevConfig)
|
||||
},
|
||||
closeModal() {
|
||||
this.updateData() // reset
|
||||
this.$emit('close')
|
||||
},
|
||||
saveChanges() {
|
||||
this.$emit('save', this.inbound, this.inboundStats)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
if (newValue) {
|
||||
this.updateData()
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { Listen, InTls, Hysteria2, Naive, Direct, Shadowsocks, Users, Hysteria, ShadowTls, TProxy, InMulitiplex, Tuic, Transport }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.v-card-subtitle {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid gray;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="400">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>QrCode</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="1"><v-icon icon="mdi-close-box" @click="$emit('close')" ></v-icon></v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col style="text-align: center;" @click="copyToClipboard(clientSub)">
|
||||
<v-chip>{{ $t('setting.sub') }}</v-chip>
|
||||
<QrcodeVue :value="clientSub" :size="300" :margin="1" style="border-radius: 1rem;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-for="l in clientLinks">
|
||||
<v-col style="text-align: center;" @click="copyToClipboard(l.uri)">
|
||||
<v-chip>{{ l.remark }}</v-chip><br />
|
||||
<QrcodeVue :value="l.uri" :size="300" :margin="1" style="border-radius: 1rem;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import Data from '@/store/modules/data'
|
||||
import Clipboard from 'clipboard'
|
||||
import Message from '@/store/modules/message'
|
||||
import { i18n } from '@/locales'
|
||||
|
||||
export default {
|
||||
props: ['index'],
|
||||
data() {
|
||||
return {
|
||||
msg: Message(),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(txt:string) {
|
||||
|
||||
const clipboard = new Clipboard('.clipboard-btn', {
|
||||
text: () => txt
|
||||
});
|
||||
|
||||
clipboard.on('success', () => {
|
||||
clipboard.destroy()
|
||||
this.msg.showMessage(i18n.global.t('copyToClipboard') + " : " + i18n.global.t('success'),'success',5000)
|
||||
})
|
||||
|
||||
clipboard.on('error', () => {
|
||||
clipboard.destroy()
|
||||
this.msg.showMessage(i18n.global.t('copyToClipboard') + " : " + i18n.global.t('failed'),'error',5000)
|
||||
})
|
||||
|
||||
// Perform click on hidden button to trigger copy
|
||||
const hiddenButton = document.createElement('button');
|
||||
hiddenButton.className = 'clipboard-btn';
|
||||
document.body.appendChild(hiddenButton);
|
||||
hiddenButton.click();
|
||||
document.body.removeChild(hiddenButton);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
clients() { return Data().clients },
|
||||
client() {
|
||||
if ( typeof this.$props.index != 'number' ) return <any>{}
|
||||
return this.clients[this.$props.index]
|
||||
},
|
||||
clientSub() {
|
||||
return Data().subURI + this.client.name
|
||||
},
|
||||
clientLinks() {
|
||||
return JSON.parse(this.client.links?? "[]")
|
||||
}
|
||||
},
|
||||
components: { QrcodeVue }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg" :loading="loading" color="background">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>
|
||||
{{ $t('stats.graphTitle') + " - " + $t('objects.' + resource) + " : " + tag }}
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto"><v-icon icon="mdi-close" @click="$emit('close')"></v-icon></v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px;">
|
||||
<v-container id="container">
|
||||
<v-alert :text="$t('noData')" type="warning" variant="outlined" v-if="alert"></v-alert>
|
||||
<Line v-if="loaded" :data="usage" :options="<any>options" />
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { i18n } from '@/locales'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { HumanReadable } from '@/plugins/utils'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
} from 'chart.js'
|
||||
import { ref } from 'vue'
|
||||
import { Line } from 'vue-chartjs'
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
)
|
||||
ChartJS.defaults.font.family = 'Vazirmatn'
|
||||
export default {
|
||||
components: {
|
||||
Line
|
||||
},
|
||||
props: ['visible','resource','tag'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
alert: false,
|
||||
intervalId: <any>0,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
text: (ctx:any) => {
|
||||
const {axis = 'xy', intersect, mode} = ctx.chart.options.interaction;
|
||||
return 'Mode: ' + mode + ', axis: ' + axis + ', intersect: ' + intersect;
|
||||
},
|
||||
footer: (items:any[]) => {
|
||||
return HumanReadable.sizeFormat(items.reduce((acc, c) => acc + c.raw, 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
grid: {
|
||||
color: () => { return this.$vuetify.theme.current.colors.secondary },
|
||||
},
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(label:any, index: number) {
|
||||
return label == 0 ? 0 : HumanReadable.sizeFormat(label,0)
|
||||
},
|
||||
count: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
usage: ref(<any>{}),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData(limit: number) {
|
||||
this.loading = true
|
||||
const data = await HttpUtils.get('/api/stats', { resource: this.resource, tag: this.tag, limit: limit })
|
||||
if (data.success && data.obj) {
|
||||
const obj = <any[]>data.obj
|
||||
const l = String(i18n.global.locale) == 'fa' ? "fa-IR" : "en-US"
|
||||
const oneStep = limit * 3600 * 1000 / 360 // Each 10 sec
|
||||
const now = new Date().getTime()
|
||||
const steps = <number[]>[]
|
||||
for (let i = 360; i >= 0; i--) {
|
||||
steps.push(now - (oneStep * i))
|
||||
}
|
||||
const labels = <string[]>[]
|
||||
const uplinkData = <number[]>[]
|
||||
const downlinkData = <number[]>[]
|
||||
for (let i = 1; i<360; i++) {
|
||||
labels.push(this.genLable(steps[i],l))
|
||||
let upSum:number
|
||||
let downSum:number
|
||||
const upTraffics = obj.filter(o => o.direction && o.dateTime*1000 < steps[i] && o.dateTime*1000 > steps[i-1]).map((o:any) => o.traffic)
|
||||
upSum = upTraffics.length>0 ? upTraffics.reduce(u => u) : null
|
||||
const downTraffics = obj.filter(o => !o.direction && o.dateTime*1000 < steps[i] && o.dateTime*1000 > steps[i-1]).map((o:any) => o.traffic)
|
||||
downSum = downTraffics.length>0 ? downTraffics.reduce(d => d) : null
|
||||
uplinkData.push(upSum)
|
||||
downlinkData.push(downSum)
|
||||
}
|
||||
this.usage = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: i18n.global.t('stats.upload'),
|
||||
backgroundColor: 'rgba(255, 165, 0, 0.4)',
|
||||
borderColor: 'rgba(255, 165, 0)',
|
||||
fill: true,
|
||||
data: uplinkData
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('stats.download'),
|
||||
backgroundColor: 'rgba(0, 128, 0, 0.2)',
|
||||
borderColor: 'rgba(0, 128, 0)',
|
||||
fill: true,
|
||||
data: downlinkData
|
||||
}
|
||||
],
|
||||
}
|
||||
this.loaded = true
|
||||
} else {
|
||||
this.alert = true
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
genLable(step:number, locale: string) {
|
||||
return new Date(step).toLocaleString(locale,{
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
const limit = 1
|
||||
this.loadData(limit)
|
||||
this.intervalId = setInterval(() => {
|
||||
this.loadData(limit)
|
||||
}, 10000)
|
||||
} else {
|
||||
this.loaded = false
|
||||
this.alert = false
|
||||
this.usage.labels = []
|
||||
if (this.usage.datasets) {
|
||||
this.usage.datasets[0].data = []
|
||||
this.usage.datasets[1].data = []
|
||||
}
|
||||
if (this.intervalId && this.intervalId != 0) {
|
||||
clearInterval(this.intervalId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user