init
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function version() {
|
||||
return request.get('/common/version')
|
||||
}
|
||||
|
||||
export function serverInfo() {
|
||||
return request.get('/common/serverInfo')
|
||||
}
|
||||
|
||||
export function setLoginInfo(data) {
|
||||
return request.post('/common/loginInfo', data)
|
||||
}
|
||||
|
||||
export function setClashRules(data) {
|
||||
return request.post('/common/clashRules', data)
|
||||
}
|
||||
|
||||
export function getClashRules() {
|
||||
return request.get('/common/clashRules')
|
||||
}
|
||||
|
||||
export function resetClashRules() {
|
||||
return request.delete('/common/clashRules')
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function setQuota(data) {
|
||||
return request.post('/trojan/data', data)
|
||||
}
|
||||
|
||||
export function cleanData(id) {
|
||||
return request.delete(`/trojan/data?id=${id}`)
|
||||
}
|
||||
|
||||
export function getResetDay() {
|
||||
return request.get('/trojan/data/resetDay')
|
||||
}
|
||||
|
||||
export function updateResetDay(data) {
|
||||
return request.post('/trojan/data/resetDay', data)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 登录接口
|
||||
export function login(data) {
|
||||
return request.post('/auth/login', data)
|
||||
}
|
||||
// 注册接口
|
||||
export function register(data) {
|
||||
return request.post('/auth/register', data)
|
||||
}
|
||||
// 检查有没有初始化接口
|
||||
export function check() {
|
||||
return request.get('/auth/check')
|
||||
}
|
||||
// 重置密码接口
|
||||
export function resetPass(data) {
|
||||
return request.post('/auth/reset_pass', data)
|
||||
}
|
||||
// 获取请求用户名
|
||||
export function loginUser() {
|
||||
return request.get('/auth/loginUser')
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function start() {
|
||||
return request.post('/trojan/start')
|
||||
}
|
||||
|
||||
export function stop() {
|
||||
return request.post('/trojan/stop')
|
||||
}
|
||||
|
||||
export function restart() {
|
||||
return request.post('/trojan/restart')
|
||||
}
|
||||
|
||||
export function update() {
|
||||
return request.post('/trojan/update')
|
||||
}
|
||||
|
||||
export function getLoglevel() {
|
||||
return request.get('/trojan/loglevel')
|
||||
}
|
||||
|
||||
export function setLoglevel(data) {
|
||||
return request.post('/trojan/loglevel', data)
|
||||
}
|
||||
|
||||
export function setDomain(data) {
|
||||
return request.post('/trojan/domain', data)
|
||||
}
|
||||
|
||||
export function trojanSwitch(data) {
|
||||
return request.post('/trojan/switch', data)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function userList() {
|
||||
return request.get('/trojan/user')
|
||||
}
|
||||
|
||||
export function addUser(data) {
|
||||
return request.post('/trojan/user', data)
|
||||
}
|
||||
|
||||
export function updateUser(data) {
|
||||
return request.post('/trojan/user/update', data)
|
||||
}
|
||||
|
||||
export function delUser(id) {
|
||||
return request.delete(`/trojan/user?id=${id}`)
|
||||
}
|
||||
|
||||
export function setExpire(data) {
|
||||
return request.post('/trojan/user/expire', data)
|
||||
}
|
||||
|
||||
export function cancelExpire(id) {
|
||||
return request.delete(`/trojan/user/expire?id=${id}`)
|
||||
}
|
||||
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<el-breadcrumb class="app-breadcrumb" separator="/">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
|
||||
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">
|
||||
{{ generateTitle(item.meta.title) }}
|
||||
</span>
|
||||
<a v-else @click.prevent="handleLink(item)">{{ generateTitle(item.meta.title) }}</a>
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
levelList: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.getBreadcrumb()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getBreadcrumb()
|
||||
},
|
||||
methods: {
|
||||
generateTitle(title) {
|
||||
const hasKey = this.$t('route.' + title)
|
||||
if (hasKey) {
|
||||
const translatedTitle = this.$t('route.' + title)
|
||||
return translatedTitle
|
||||
}
|
||||
return title
|
||||
},
|
||||
getBreadcrumb() {
|
||||
// only show routes with meta.title
|
||||
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
|
||||
const first = matched[0]
|
||||
if (!this.isDashboard(first)) {
|
||||
matched = [{ path: '/dashboard', meta: { title: 'dashboard' } }].concat(matched)
|
||||
}
|
||||
|
||||
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
|
||||
},
|
||||
isDashboard(route) {
|
||||
const name = route && route.name
|
||||
if (!name) {
|
||||
return false
|
||||
}
|
||||
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
|
||||
},
|
||||
handleLink(item) {
|
||||
const { redirect, path } = item
|
||||
if (redirect) {
|
||||
this.$router.push(redirect)
|
||||
return
|
||||
}
|
||||
this.$router.push(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-breadcrumb.el-breadcrumb {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 50px;
|
||||
margin-left: 8px;
|
||||
|
||||
.no-redirect {
|
||||
color: #97a8be;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div style="padding: 0 15px;" @click="toggleClick">
|
||||
<svg
|
||||
:class="{'is-active':isActive}"
|
||||
class="hamburger"
|
||||
viewBox="0 0 1024 1024"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="64"
|
||||
height="64"
|
||||
>
|
||||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Hamburger',
|
||||
props: {
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleClick() {
|
||||
this.$emit('toggleClick')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hamburger {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.hamburger.is-active {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<svg
|
||||
:class="svgClass"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use :xlink:href="iconName"></use>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'svg-icon',
|
||||
props: {
|
||||
iconClass: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
className: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
return `#icon-${this.iconClass}`
|
||||
},
|
||||
svgClass() {
|
||||
if (this.className) {
|
||||
return 'svg-icon ' + this.className
|
||||
} else {
|
||||
return 'svg-icon'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
const Mixins = import.meta.globEager("./svg/*.svg")
|
||||
export default {
|
||||
mixins: Object.values(Mixins).map((v) => v.default)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M71.984 44.815H115.9L71.984 9.642v35.173zM16.094.05h63.875l47.906 38.37v76.74c0 3.392-1.682 6.645-4.677 9.044-2.995 2.399-7.056 3.746-11.292 3.746H16.094c-4.236 0-8.297-1.347-11.292-3.746-2.995-2.399-4.677-5.652-4.677-9.044V12.84C.125 5.742 7.23.05 16.094.05zm71.86 102.32V89.58h-71.86v12.79h71.86zm23.952-25.58V64H16.094v12.79h95.812z"/></svg>
|
||||
|
After Width: | Height: | Size: 418 B |
@@ -0,0 +1 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>
|
||||
|
After Width: | Height: | Size: 944 B |
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>
|
||||
|
After Width: | Height: | Size: 623 B |
@@ -0,0 +1 @@
|
||||
<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>
|
||||
|
After Width: | Height: | Size: 440 B |
@@ -0,0 +1,122 @@
|
||||
export default {
|
||||
name: 'en',
|
||||
el: {
|
||||
colorpicker: {
|
||||
confirm: 'OK',
|
||||
clear: 'Clear',
|
||||
},
|
||||
datepicker: {
|
||||
now: 'Now',
|
||||
today: 'Today',
|
||||
cancel: 'Cancel',
|
||||
clear: 'Clear',
|
||||
confirm: 'OK',
|
||||
selectDate: 'Select date',
|
||||
selectTime: 'Select time',
|
||||
startDate: 'Start Date',
|
||||
startTime: 'Start Time',
|
||||
endDate: 'End Date',
|
||||
endTime: 'End Time',
|
||||
prevYear: 'Previous Year',
|
||||
nextYear: 'Next Year',
|
||||
prevMonth: 'Previous Month',
|
||||
nextMonth: 'Next Month',
|
||||
year: '',
|
||||
month1: 'January',
|
||||
month2: 'February',
|
||||
month3: 'March',
|
||||
month4: 'April',
|
||||
month5: 'May',
|
||||
month6: 'June',
|
||||
month7: 'July',
|
||||
month8: 'August',
|
||||
month9: 'September',
|
||||
month10: 'October',
|
||||
month11: 'November',
|
||||
month12: 'December',
|
||||
week: 'week',
|
||||
weeks: {
|
||||
sun: 'Sun',
|
||||
mon: 'Mon',
|
||||
tue: 'Tue',
|
||||
wed: 'Wed',
|
||||
thu: 'Thu',
|
||||
fri: 'Fri',
|
||||
sat: 'Sat',
|
||||
},
|
||||
months: {
|
||||
jan: 'Jan',
|
||||
feb: 'Feb',
|
||||
mar: 'Mar',
|
||||
apr: 'Apr',
|
||||
may: 'May',
|
||||
jun: 'Jun',
|
||||
jul: 'Jul',
|
||||
aug: 'Aug',
|
||||
sep: 'Sep',
|
||||
oct: 'Oct',
|
||||
nov: 'Nov',
|
||||
dec: 'Dec',
|
||||
},
|
||||
},
|
||||
select: {
|
||||
loading: 'Loading',
|
||||
noMatch: 'No matching data',
|
||||
noData: 'No data',
|
||||
placeholder: 'Select',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'No matching data',
|
||||
loading: 'Loading',
|
||||
placeholder: 'Select',
|
||||
noData: 'No data',
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Go to',
|
||||
pagesize: '/page',
|
||||
total: 'Total {total}',
|
||||
pageClassifier: '',
|
||||
deprecationWarning: 'Deprecated usages detected, please refer to the el-pagination documentation for more details',
|
||||
},
|
||||
messagebox: {
|
||||
title: 'Message',
|
||||
confirm: 'OK',
|
||||
cancel: 'Cancel',
|
||||
error: 'Illegal input',
|
||||
},
|
||||
upload: {
|
||||
deleteTip: 'press delete to remove',
|
||||
delete: 'Delete',
|
||||
preview: 'Preview',
|
||||
continue: 'Continue',
|
||||
},
|
||||
table: {
|
||||
emptyText: 'No Data',
|
||||
confirmFilter: 'Confirm',
|
||||
resetFilter: 'Reset',
|
||||
clearFilter: 'All',
|
||||
sumText: 'Sum',
|
||||
},
|
||||
tree: {
|
||||
emptyText: 'No Data',
|
||||
},
|
||||
transfer: {
|
||||
noMatch: 'No matching data',
|
||||
noData: 'No data',
|
||||
titles: ['List 1', 'List 2'],
|
||||
filterPlaceholder: 'Enter keyword',
|
||||
noCheckedFormat: '{total} items',
|
||||
hasCheckedFormat: '{checked}/{total} checked', // to be translated
|
||||
},
|
||||
image: {
|
||||
error: 'FAILED',
|
||||
},
|
||||
pageHeader: {
|
||||
title: 'Back', // to be translated
|
||||
},
|
||||
popconfirm: {
|
||||
confirmButtonText: 'Yes',
|
||||
cancelButtonText: 'No',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
export default {
|
||||
name: 'zh-cn',
|
||||
el: {
|
||||
colorpicker: {
|
||||
confirm: '确定',
|
||||
clear: '清空',
|
||||
},
|
||||
datepicker: {
|
||||
now: '此刻',
|
||||
today: '今天',
|
||||
cancel: '取消',
|
||||
clear: '清空',
|
||||
confirm: '确定',
|
||||
selectDate: '选择日期',
|
||||
selectTime: '选择时间',
|
||||
startDate: '开始日期',
|
||||
startTime: '开始时间',
|
||||
endDate: '结束日期',
|
||||
endTime: '结束时间',
|
||||
prevYear: '前一年',
|
||||
nextYear: '后一年',
|
||||
prevMonth: '上个月',
|
||||
nextMonth: '下个月',
|
||||
year: '年',
|
||||
month1: '1 月',
|
||||
month2: '2 月',
|
||||
month3: '3 月',
|
||||
month4: '4 月',
|
||||
month5: '5 月',
|
||||
month6: '6 月',
|
||||
month7: '7 月',
|
||||
month8: '8 月',
|
||||
month9: '9 月',
|
||||
month10: '10 月',
|
||||
month11: '11 月',
|
||||
month12: '12 月',
|
||||
// week: '周次',
|
||||
weeks: {
|
||||
sun: '日',
|
||||
mon: '一',
|
||||
tue: '二',
|
||||
wed: '三',
|
||||
thu: '四',
|
||||
fri: '五',
|
||||
sat: '六',
|
||||
},
|
||||
months: {
|
||||
jan: '一月',
|
||||
feb: '二月',
|
||||
mar: '三月',
|
||||
apr: '四月',
|
||||
may: '五月',
|
||||
jun: '六月',
|
||||
jul: '七月',
|
||||
aug: '八月',
|
||||
sep: '九月',
|
||||
oct: '十月',
|
||||
nov: '十一月',
|
||||
dec: '十二月',
|
||||
},
|
||||
},
|
||||
select: {
|
||||
loading: '加载中',
|
||||
noMatch: '无匹配数据',
|
||||
noData: '无数据',
|
||||
placeholder: '请选择',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: '无匹配数据',
|
||||
loading: '加载中',
|
||||
placeholder: '请选择',
|
||||
noData: '暂无数据',
|
||||
},
|
||||
pagination: {
|
||||
goto: '前往',
|
||||
pagesize: '条/页',
|
||||
total: '共 {total} 条',
|
||||
pageClassifier: '页',
|
||||
deprecationWarning: '你使用了一些已被废弃的用法,请参考 el-pagination 的官方文档',
|
||||
},
|
||||
messagebox: {
|
||||
title: '提示',
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
error: '输入的数据不合法!',
|
||||
},
|
||||
upload: {
|
||||
deleteTip: '按 delete 键可删除',
|
||||
delete: '删除',
|
||||
preview: '查看图片',
|
||||
continue: '继续上传',
|
||||
},
|
||||
table: {
|
||||
emptyText: '暂无数据',
|
||||
confirmFilter: '筛选',
|
||||
resetFilter: '重置',
|
||||
clearFilter: '全部',
|
||||
sumText: '合计',
|
||||
},
|
||||
tree: {
|
||||
emptyText: '暂无数据',
|
||||
},
|
||||
transfer: {
|
||||
noMatch: '无匹配数据',
|
||||
noData: '无数据',
|
||||
titles: ['列表 1', '列表 2'],
|
||||
filterPlaceholder: '请输入搜索内容',
|
||||
noCheckedFormat: '共 {total} 项',
|
||||
hasCheckedFormat: '已选 {checked}/{total} 项',
|
||||
},
|
||||
image: {
|
||||
error: '加载失败',
|
||||
},
|
||||
pageHeader: {
|
||||
title: '返回',
|
||||
},
|
||||
popconfirm: {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
export default {
|
||||
ok: 'ok',
|
||||
cancel: 'cancel',
|
||||
reset: 'reset',
|
||||
inputPass: 'input password',
|
||||
inputPassAgain: 'input password again',
|
||||
passLessError: "password number can't be less than 5",
|
||||
passDifferentError: 'The two inputs are inconsistent!',
|
||||
setupPass: 'setup admin password',
|
||||
inputNotNull: "username or password can't be null!",
|
||||
route: {
|
||||
dashboard: 'Dashboard',
|
||||
trojan: 'Trojan',
|
||||
user: 'User'
|
||||
},
|
||||
navbar: {
|
||||
version: 'System Version',
|
||||
setting: 'System Setting',
|
||||
password: 'Reset Password',
|
||||
title: 'Change Title',
|
||||
importExport: 'Import Export',
|
||||
resetDay: 'Change Reset Day',
|
||||
clashRules: 'Change Clash Rules',
|
||||
resetTitle: 'Change data reset day of the month',
|
||||
changeTitle: 'Change login page title',
|
||||
changeRules: 'Change clash rules',
|
||||
changeTitleSuccess: 'Change title success!',
|
||||
changeRulesSuccess: 'Change clash rules success!',
|
||||
resetRulesSuccess: 'Reset clash rules success!',
|
||||
changeDaySuccess: 'Change data reset day success!',
|
||||
closeResetSuccess: 'Close data reset success!',
|
||||
inputTitle: 'Input login page title',
|
||||
versionTitle: 'Trojan-manager Version',
|
||||
passwordTitle: 'Change admin password',
|
||||
resetSuccess: 'reset password success!',
|
||||
meanClose: '0 means close data reset',
|
||||
exportTip: 'export all database record to csv file',
|
||||
importTip: 'import csv to database',
|
||||
importSuccess: 'Import Success',
|
||||
importCsv: 'import csv file',
|
||||
exportCsv: 'export csv file'
|
||||
},
|
||||
dashboard: {
|
||||
memory: 'memory',
|
||||
disk: 'disk',
|
||||
trojanVersion: 'Trojan Version',
|
||||
trojanUser: 'Trojan User',
|
||||
trojanUptime: 'Trojan Uptime',
|
||||
netSpeed: 'Network Speed',
|
||||
netCount: 'Connection Count',
|
||||
load: 'Load',
|
||||
upload: 'upload',
|
||||
download: 'download',
|
||||
total: 'total',
|
||||
day: ' day',
|
||||
hour: ' hour',
|
||||
minute: ' minute',
|
||||
second: ' second'
|
||||
},
|
||||
trojan: {
|
||||
restartSuccess: 'restart trojan success!',
|
||||
startSuccess: 'start trojan success!',
|
||||
stopSuccess: 'stop trojan success!',
|
||||
updateSuccess: 'update trojan success!',
|
||||
loglevelSuccess: 'set loglevel success!',
|
||||
switchSuccess: 'switch trojan type success!'
|
||||
},
|
||||
user: {
|
||||
upload: 'upload',
|
||||
download: 'download',
|
||||
total: 'total',
|
||||
quota: 'quota',
|
||||
expiryDate: 'expiryDate',
|
||||
setExpire: 'setExpire',
|
||||
editExpire: 'editExpire',
|
||||
cancelExpire: 'cancelExpire',
|
||||
reset: 'reset data',
|
||||
unlimit: 'unlimited',
|
||||
search: 'search username',
|
||||
limitData: 'limit data',
|
||||
modifyUser: 'modify user',
|
||||
trojanShareLink: 'trojan share link',
|
||||
clashShareLink: 'clash share link',
|
||||
importClash: 'import to clash',
|
||||
meanUnlimit: '-1 means unlimited',
|
||||
inputUsername: 'username',
|
||||
inputPassword: 'password',
|
||||
operate: 'operate',
|
||||
addUser: 'add trojan user',
|
||||
patchLimitUser: 'patch limit user',
|
||||
patchDelUser: 'patch delete user?',
|
||||
modifyUser2: 'modify user ',
|
||||
userpass: 'username and password',
|
||||
delUser: 'delete user ',
|
||||
patchReset: 'patch reset data?',
|
||||
resetUser: 'reset user ',
|
||||
data: ' data?',
|
||||
limitUser: 'limit user ',
|
||||
limitUser2: 'limit user ',
|
||||
setupUser: 'setup user ',
|
||||
limitSuccess: ' data success!',
|
||||
delUser1: 'delete user ',
|
||||
success: ' success!',
|
||||
resetUser1: 'reset user ',
|
||||
limitAdmin: "can't create username is admin",
|
||||
passLimit: "password doesn't support chinese!",
|
||||
addUser2: 'add user ',
|
||||
week: 'a week',
|
||||
month: 'a month',
|
||||
season: 'one season',
|
||||
halfYear: 'half a year',
|
||||
year: 'one year',
|
||||
preset: 'preset',
|
||||
days: 'days',
|
||||
setExpireSuccess: 'setup expire success!',
|
||||
cancelExpireSuccess: 'cancel expire success!',
|
||||
remaining: 'remaining days'
|
||||
},
|
||||
request: {
|
||||
requestError: 'request error',
|
||||
authFail: 'auth fail, please login again',
|
||||
accessDenied: 'access denied',
|
||||
notFound: '404 not found',
|
||||
serverError: 'server error',
|
||||
connectError: 'connect error'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import elementEnLocale from './element/en' // element-ui lang
|
||||
import elementZhLocale from './element/zh-cn'// element-ui lang
|
||||
import enLocale from './en'
|
||||
import zhLocale from './zh'
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
...enLocale,
|
||||
...elementEnLocale
|
||||
},
|
||||
zh: {
|
||||
...zhLocale,
|
||||
...elementZhLocale
|
||||
}
|
||||
}
|
||||
export function getLanguage() {
|
||||
const chooseLanguage = localStorage.getItem('language')
|
||||
if (chooseLanguage) return chooseLanguage
|
||||
|
||||
// if has not choose language
|
||||
const language = (navigator.language || navigator.browserLanguage).toLowerCase()
|
||||
const locales = Object.keys(messages)
|
||||
for (const locale of locales) {
|
||||
if (language.indexOf(locale) > -1) {
|
||||
return locale
|
||||
}
|
||||
}
|
||||
return 'en'
|
||||
}
|
||||
const i18n = createI18n({
|
||||
// set locale
|
||||
// options: en | zh
|
||||
locale: getLanguage(),
|
||||
// set locale messages
|
||||
messages: messages,
|
||||
fallbackLocale: 'en',
|
||||
silentFallbackWarn: true,
|
||||
silentTranslationWarn: true
|
||||
})
|
||||
|
||||
export default i18n
|
||||
@@ -0,0 +1,147 @@
|
||||
export default {
|
||||
ok: '确定',
|
||||
cancel: '取消',
|
||||
reset: '重置',
|
||||
login: '登录',
|
||||
register: '注册',
|
||||
log: '日志',
|
||||
edit: '编辑',
|
||||
share: '分享',
|
||||
terminal: '终端',
|
||||
refresh: '刷新',
|
||||
restart: '重启',
|
||||
start: '启动',
|
||||
stop: '暂停',
|
||||
update: '更新',
|
||||
add: '添加',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
loglevel: '日志等级',
|
||||
all: '所有',
|
||||
type: '类型',
|
||||
line: '初始行数',
|
||||
choice: '请选择',
|
||||
latest: '最新日志',
|
||||
delete: '删除',
|
||||
inputPass: '输入密码',
|
||||
inputPassAgain: '请再次输入密码',
|
||||
passLessError: '密码不能小于5位',
|
||||
passDifferentError: '两次输入不一致!',
|
||||
setupPass: '设置admin密码',
|
||||
inputNotNull: '用户名或密码不能为空!',
|
||||
route: {
|
||||
dashboard: '首页',
|
||||
trojan: 'trojan管理',
|
||||
user: '用户管理'
|
||||
},
|
||||
navbar: {
|
||||
version: '系统版本',
|
||||
setting: '系统设置',
|
||||
password: '修改密码',
|
||||
title: '修改标题',
|
||||
clashRules: '修改规则',
|
||||
importExport: '导入导出',
|
||||
resetDay: '改重置日',
|
||||
resetTitle: '修改流量重置日',
|
||||
changeTitle: '修改登录页标题',
|
||||
changeRules: '修改clash规则',
|
||||
inputTitle: '输入登录页标题',
|
||||
changeTitleSuccess: '修改登录页标题成功!',
|
||||
changeRulesSuccess: '修改clash规则成功!',
|
||||
resetRulesSuccess: '重置clash规则成功!',
|
||||
changeDaySuccess: '修改流量重置日成功!',
|
||||
closeResetSuccess: '关闭流量重置成功',
|
||||
versionTitle: 'trojan管理程序版本',
|
||||
passwordTitle: '变更管理员密码',
|
||||
resetSuccess: '重置密码成功!',
|
||||
meanClose: '0代表关闭流量重置',
|
||||
exportTip: '导出数据库里所有记录到csv文件',
|
||||
importTip: '导入csv文件数据到数据库',
|
||||
importSuccess: '导入成功',
|
||||
importCsv: '导入csv文件',
|
||||
exportCsv: '导出csv文件'
|
||||
},
|
||||
dashboard: {
|
||||
memory: '内存',
|
||||
disk: '硬盘',
|
||||
trojanVersion: 'trojan 版本',
|
||||
trojanUser: 'trojan 用户数',
|
||||
trojanUptime: 'trojan 已运行',
|
||||
netSpeed: '网速',
|
||||
netCount: '连接数',
|
||||
load: '服务器负载',
|
||||
upload: '上传',
|
||||
download: '下载',
|
||||
total: '总共',
|
||||
day: '天',
|
||||
hour: '时',
|
||||
minute: '分',
|
||||
second: '秒'
|
||||
},
|
||||
trojan: {
|
||||
restartSuccess: '重启trojan成功!',
|
||||
startSuccess: '启动trojan成功!',
|
||||
stopSuccess: '停止trojan成功!',
|
||||
updateSuccess: '更新trojan成功!',
|
||||
loglevelSuccess: '设置日志等级成功!',
|
||||
switchSuccess: '切换trojan类型成功!'
|
||||
},
|
||||
user: {
|
||||
upload: '上传流量',
|
||||
download: '下载流量',
|
||||
total: '总流量',
|
||||
quota: '流量限制',
|
||||
expiryDate: '到期日期',
|
||||
setExpire: '设置限期',
|
||||
editExpire: '修改限期',
|
||||
cancelExpire: '取消限期',
|
||||
reset: '重置流量',
|
||||
unlimit: '无',
|
||||
search: '输入用户名搜索',
|
||||
limitData: '限制流量',
|
||||
modifyUser: '修改账密',
|
||||
trojanShareLink: 'trojan链接',
|
||||
clashShareLink: 'clash链接',
|
||||
importClash: '导到clash',
|
||||
meanUnlimit: '-1代表无流量限制',
|
||||
inputUsername: '输入用户名',
|
||||
inputPassword: '输入密码',
|
||||
operate: '操作',
|
||||
addUser: '新增trojan用户',
|
||||
patchLimitUser: '限制以下用户流量',
|
||||
patchDelUser: '确定批量删除以下用户?',
|
||||
modifyUser2: '修改用户 ',
|
||||
userpass: '的用户名和密码',
|
||||
delUser: '确定删除用户 ',
|
||||
patchReset: '确定重置以下用户流量?',
|
||||
resetUser: '确定重置用户 ',
|
||||
data: ' 的流量?',
|
||||
limitUser: '确定限制用户 ',
|
||||
limitUser2: '限制用户 ',
|
||||
limitSuccess: '流量成功!',
|
||||
delUser1: '删除用户',
|
||||
success: '成功!',
|
||||
resetUser1: '重置用户',
|
||||
limitAdmin: '不能创建用户名为admin的用户!',
|
||||
passLimit: '密码不支持中文!',
|
||||
addUser2: '新增用户',
|
||||
week: '单周',
|
||||
month: '单月',
|
||||
season: '单季',
|
||||
halfYear: '半年',
|
||||
year: '一年',
|
||||
preset: '预设',
|
||||
days: '天数',
|
||||
setExpireSuccess: '设置用户限期成功!',
|
||||
cancelExpireSuccess: '取消用户限期成功!',
|
||||
remaining: '剩余天数'
|
||||
},
|
||||
request: {
|
||||
requestError: '请求出错',
|
||||
authFail: '授权失败,请重新登录',
|
||||
accessDenied: '拒绝访问',
|
||||
notFound: '请求错误,未找到该资源',
|
||||
serverError: '服务端错误',
|
||||
connectError: '连接服务器失败'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from '@/App'
|
||||
import ElementPlus from 'element-plus'
|
||||
import '@/styles/index.scss' // global css
|
||||
import store from '@/store/index'
|
||||
import router from '@/router/index'
|
||||
import i18n from './lang/index' // internationalization
|
||||
import 'virtual:svg-icons-register'
|
||||
import SvgIcon from '@/components/SvgIcon'// svg组件
|
||||
import '@/icons'
|
||||
|
||||
const whiteList = ['/login', '/register'] // no redirect whitelist
|
||||
const adminList = ['/trojan'] // need admin role
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
if (store.state.UserToken) {
|
||||
if (store.state.isAdmin === null) {
|
||||
await store.dispatch('loginUser')
|
||||
}
|
||||
if (!store.state.isAdmin) {
|
||||
router.options.routes.map((x) => {
|
||||
if (adminList.indexOf(x.path) !== -1) {
|
||||
x.hidden = true
|
||||
}
|
||||
})
|
||||
}
|
||||
if (to.path === '/login') {
|
||||
// if is logged in, redirect to the home page
|
||||
next({ path: '/' })
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
next()
|
||||
} else {
|
||||
next('/login')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
createApp(App).use(ElementPlus).use(router).use(store).use(i18n).component('svg-icon', SvgIcon).mount('#app')
|
||||
@@ -0,0 +1,95 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
/* Layout */
|
||||
import Layout from '@/views/layout'
|
||||
|
||||
|
||||
/**
|
||||
* Note: sub-menu only appear when route children.length >= 1
|
||||
* Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
|
||||
*
|
||||
* hidden: true if set true, item will not show in the sidebar(default is false)
|
||||
* alwaysShow: true if set true, will always show the root menu
|
||||
* if not set alwaysShow, when item has more than one children route,
|
||||
* it will becomes nested mode, otherwise not show the root menu
|
||||
* redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
|
||||
* name:'router-name' the name is used by <keep-alive> (must set!!!)
|
||||
* meta : {
|
||||
roles: ['admin','editor'] control the page roles (you can set multiple roles)
|
||||
title: 'title' the name show in sidebar and breadcrumb (recommend set)
|
||||
icon: 'svg-name' the icon show in the sidebar
|
||||
breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
|
||||
activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* constantRoutes
|
||||
* a base page that does not have permission requirements
|
||||
* all roles can be accessed
|
||||
*/
|
||||
export const constantRoutes = [
|
||||
{
|
||||
path: '/login',
|
||||
component: () => import('@/views/login/login'),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
component: () => import('@/views/login/register'),
|
||||
hidden: true
|
||||
},
|
||||
|
||||
{
|
||||
path: '/404',
|
||||
component: () => import('@/views/errorPage/404'),
|
||||
hidden: true
|
||||
},
|
||||
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/dashboard',
|
||||
children: [{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/dashboard/index'),
|
||||
meta: { title: 'dashboard', icon: 'dashboard' }
|
||||
}]
|
||||
},
|
||||
|
||||
{
|
||||
path: '/trojan',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'trojan',
|
||||
component: () => import('@/views/trojan/index'),
|
||||
meta: { title: 'trojan', icon: 'documentation' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'user',
|
||||
component: () => import('@/views/user/index'),
|
||||
meta: { title: 'user', icon: 'user' }
|
||||
}
|
||||
]
|
||||
},
|
||||
// 404 page must be placed at the end !!!
|
||||
{ path: '/:pathMatch(.*)*', redirect: '/404', hidden: true }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
scrollBehavior: () => ({ y: 0 }),
|
||||
routes: constantRoutes
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,19 @@
|
||||
import { loginUser } from '@/api/permission'
|
||||
|
||||
export default {
|
||||
loginUser({ commit }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
loginUser().then(res => {
|
||||
if (res.code === 200) {
|
||||
commit('SET_ADMIN', res.data.username === 'admin')
|
||||
} else if (res.code === 201) {
|
||||
commit('LOGIN_OUT')
|
||||
location.reload()
|
||||
}
|
||||
resolve(res)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
sidebar: state => state.app.sidebar
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createStore } from 'vuex'
|
||||
|
||||
import state from './state'
|
||||
import getters from './getters'
|
||||
import modules from './modules'
|
||||
import actions from './actions'
|
||||
import mutations from './mutations'
|
||||
|
||||
export default createStore({
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions,
|
||||
modules
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import app from './modules/app'
|
||||
|
||||
export default {
|
||||
app
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { getLanguage } from '@/lang/index'
|
||||
|
||||
const state = {
|
||||
sidebar: {
|
||||
opened: Object.prototype.hasOwnProperty.call(localStorage, 'sidebarStatus') ? localStorage.getItem('sidebarStatus') === 0 : true,
|
||||
withoutAnimation: false
|
||||
},
|
||||
language: getLanguage(),
|
||||
device: 'desktop'
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
TOGGLE_SIDEBAR: state => {
|
||||
state.sidebar.opened = !state.sidebar.opened
|
||||
state.sidebar.withoutAnimation = false
|
||||
if (state.sidebar.opened) {
|
||||
localStorage.setItem('sidebarStatus', 1)
|
||||
} else {
|
||||
localStorage.setItem('sidebarStatus', 0)
|
||||
}
|
||||
},
|
||||
CLOSE_SIDEBAR: (state, withoutAnimation) => {
|
||||
localStorage.setItem('sidebarStatus', 0)
|
||||
state.sidebar.opened = false
|
||||
state.sidebar.withoutAnimation = withoutAnimation
|
||||
},
|
||||
TOGGLE_DEVICE: (state, device) => {
|
||||
state.device = device
|
||||
},
|
||||
SET_LANGUAGE: (state, language) => {
|
||||
state.language = language
|
||||
localStorage.setItem('language', language)
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
toggleSideBar({ commit }) {
|
||||
commit('TOGGLE_SIDEBAR')
|
||||
},
|
||||
closeSideBar({ commit }, { withoutAnimation }) {
|
||||
commit('CLOSE_SIDEBAR', withoutAnimation)
|
||||
},
|
||||
toggleDevice({ commit }, device) {
|
||||
commit('TOGGLE_DEVICE', device)
|
||||
},
|
||||
setLanguage({ commit }, language) {
|
||||
commit('SET_LANGUAGE', language)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
mutations,
|
||||
actions
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export default {
|
||||
LOGIN_IN(state, token) {
|
||||
state.UserToken = token
|
||||
},
|
||||
LOGIN_OUT(state) {
|
||||
state.UserToken = ''
|
||||
},
|
||||
SET_ADMIN(state, bool) {
|
||||
state.isAdmin = bool
|
||||
},
|
||||
SET_TITLE(state, docTitle) {
|
||||
state.docTitle = docTitle
|
||||
localStorage.setItem('docTitle', state.docTitle)
|
||||
},
|
||||
SET_WIDTH(state, width) {
|
||||
state.dialogWidth = width
|
||||
},
|
||||
SET_NPROGRESS(state, bool) {
|
||||
state.nprogress = bool
|
||||
},
|
||||
SET_NOERROR(state, bool) {
|
||||
state.noerror = bool
|
||||
},
|
||||
SET_LINE(state, line) {
|
||||
state.line = line
|
||||
localStorage.setItem('line', state.line)
|
||||
},
|
||||
SET_LOGLEVEL(state, loglevel) {
|
||||
state.loglevel = loglevel
|
||||
localStorage.setItem('loglevel', state.loglevel)
|
||||
},
|
||||
SET_TYPE(state, type) {
|
||||
state.type = type
|
||||
localStorage.setItem('type', state.type)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
get UserToken() {
|
||||
return localStorage.getItem('token')
|
||||
},
|
||||
set UserToken(value) {
|
||||
localStorage.setItem('token', value)
|
||||
},
|
||||
loglevel: Object.prototype.hasOwnProperty.call(localStorage, 'loglevel') ? parseInt(localStorage.getItem('loglevel')) : 1,
|
||||
line: Object.prototype.hasOwnProperty.call(localStorage, 'line') ? parseInt(localStorage.getItem('line')) : 300,
|
||||
type: Object.prototype.hasOwnProperty.call(localStorage, 'type') ? localStorage.getItem('type') : 'trojan',
|
||||
docTitle: Object.prototype.hasOwnProperty.call(localStorage, 'docTitle') ? localStorage.getItem('docTitle') : '',
|
||||
dialogWidth: '25%',
|
||||
nprogress: true,
|
||||
noerror: false,
|
||||
isAdmin: null
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// cover some element-ui styles
|
||||
|
||||
.el-breadcrumb__inner,
|
||||
.el-breadcrumb__inner a {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
// dropdown
|
||||
.el-dropdown-menu {
|
||||
a {
|
||||
display: block
|
||||
}
|
||||
}
|
||||
|
||||
// to fix el-date-picker css style
|
||||
.el-range-separator {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
::-webkit-scrollbar {
|
||||
/*滚动条整体部分,其中的属性有width,height,background,border等*/
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
/*滚动条两端的按钮,可以用display:none让其不显示*/
|
||||
display: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
/*滚动条里面可以拖动的那部分*/
|
||||
background: #cee0f0;
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, .4);
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner, ::-webkit-scrollbar-resizer,
|
||||
::-webkit-scrollbar-track, ::-webkit-scrollbar-track-piece {
|
||||
background: #F0F5FB;
|
||||
}
|
||||
}
|
||||
|
||||
.ruleDialog {
|
||||
.el-dialog__body {
|
||||
padding: 0;
|
||||
}
|
||||
.el-textarea__inner {
|
||||
padding: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item .el-menu-tooltip__trigger {
|
||||
display: inline
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
flex-wrap: nowrap
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
@import './normalize.scss';
|
||||
@import './variables.scss';
|
||||
@import './mixin.scss';
|
||||
@import './transition.scss';
|
||||
@import './element-ui.scss';
|
||||
@import './sidebar.scss';
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
a:focus,
|
||||
a:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a,
|
||||
a:focus,
|
||||
a:hover {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.clearfix {
|
||||
&:after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
content: " ";
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// main-container global css
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@mixin clearfix {
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin scrollBar {
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background: #d3dce6;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #99a9bf;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin relative {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
@charset "utf-8";
|
||||
|
||||
html {
|
||||
|
||||
color: #000;
|
||||
background: #fff;
|
||||
overflow-y: scroll;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
html * {
|
||||
outline: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
body,
|
||||
div,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ul,
|
||||
ol,
|
||||
li,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
pre,
|
||||
code,
|
||||
form,
|
||||
fieldset,
|
||||
legend,
|
||||
input,
|
||||
textarea,
|
||||
p,
|
||||
blockquote,
|
||||
th,
|
||||
td,
|
||||
hr,
|
||||
button,
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
section {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
fieldset,
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
abbr,
|
||||
acronym {
|
||||
border: 0;
|
||||
font-variant: normal;
|
||||
}
|
||||
|
||||
del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
address,
|
||||
caption,
|
||||
cite,
|
||||
code,
|
||||
dfn,
|
||||
em,
|
||||
th,
|
||||
i,
|
||||
var {
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
caption,
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 100%;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
q:before,
|
||||
q:after {
|
||||
content: '';
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
ins,
|
||||
a,
|
||||
a:active,
|
||||
a:visited,
|
||||
a:link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.clearfix {
|
||||
&:after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
visibility: hidden;
|
||||
;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
#app {
|
||||
|
||||
.main-container {
|
||||
min-height: 100%;
|
||||
transition: margin-left .28s;
|
||||
margin-left: $sideBarWidth;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
transition: width 0.28s;
|
||||
width: $sideBarWidth !important;
|
||||
background-color: $menuBg;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
font-size: 0px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
overflow: hidden;
|
||||
|
||||
// reset element-ui css
|
||||
.horizontal-collapse-transition {
|
||||
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
|
||||
}
|
||||
|
||||
.scrollbar-wrapper {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.el-scrollbar__bar.is-vertical {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.el-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.has-logo {
|
||||
.el-scrollbar {
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
}
|
||||
|
||||
.is-horizontal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border: none;
|
||||
height: 100%;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
// menu hover
|
||||
.submenu-title-noDropdown,
|
||||
.el-submenu__title {
|
||||
&:hover {
|
||||
background-color: $menuHover !important;
|
||||
}
|
||||
}
|
||||
|
||||
.is-active>.el-submenu__title {
|
||||
color: $subMenuActiveText !important;
|
||||
}
|
||||
|
||||
& .nest-menu .el-submenu>.el-submenu__title,
|
||||
& .el-submenu .el-menu-item {
|
||||
min-width: $sideBarWidth !important;
|
||||
background-color: $subMenuBg !important;
|
||||
|
||||
&:hover {
|
||||
background-color: $subMenuHover !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hideSidebar {
|
||||
.sidebar-container {
|
||||
width: 54px !important;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
margin-left: 54px;
|
||||
}
|
||||
|
||||
.submenu-title-noDropdown {
|
||||
padding: 0 !important;
|
||||
position: relative;
|
||||
|
||||
.el-tooltip {
|
||||
padding: 0 !important;
|
||||
|
||||
.svg-icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-submenu {
|
||||
overflow: hidden;
|
||||
|
||||
&>.el-submenu__title {
|
||||
padding: 0 !important;
|
||||
|
||||
.svg-icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.el-submenu__icon-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu--collapse {
|
||||
.el-submenu {
|
||||
&>.el-submenu__title {
|
||||
&>span {
|
||||
height: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu--collapse .el-menu .el-submenu {
|
||||
min-width: $sideBarWidth !important;
|
||||
}
|
||||
|
||||
// mobile responsive
|
||||
.mobile {
|
||||
.main-container {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
transition: transform .28s;
|
||||
width: $sideBarWidth !important;
|
||||
}
|
||||
|
||||
&.hideSidebar {
|
||||
.sidebar-container {
|
||||
pointer-events: none;
|
||||
transition-duration: 0.3s;
|
||||
transform: translate3d(-$sideBarWidth, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.withoutAnimation {
|
||||
|
||||
.main-container,
|
||||
.sidebar-container {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// when menu collapsed
|
||||
.el-menu--vertical {
|
||||
&>.el-menu {
|
||||
.svg-icon {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.nest-menu .el-submenu>.el-submenu__title,
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
// you can use $subMenuHover
|
||||
background-color: $menuHover !important;
|
||||
}
|
||||
}
|
||||
|
||||
// the scroll bar appears when the subMenu is too long
|
||||
>.el-menu--popup {
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background: #d3dce6;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #99a9bf;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// global transition css
|
||||
|
||||
/* fade */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.28s;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* fade-transform */
|
||||
.fade-transform-leave-active,
|
||||
.fade-transform-enter-active {
|
||||
transition: all .5s;
|
||||
}
|
||||
|
||||
.fade-transform-enter {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
/* breadcrumb transition */
|
||||
.breadcrumb-enter-active,
|
||||
.breadcrumb-leave-active {
|
||||
transition: all .5s;
|
||||
}
|
||||
|
||||
.breadcrumb-enter,
|
||||
.breadcrumb-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.breadcrumb-move {
|
||||
transition: all .5s;
|
||||
}
|
||||
|
||||
.breadcrumb-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// sidebar
|
||||
$menuText:#bfcbd9;
|
||||
$menuActiveText:#409EFF;
|
||||
$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
|
||||
|
||||
$menuBg:#304156;
|
||||
$menuHover:#263445;
|
||||
|
||||
$subMenuBg:#1f2d3d;
|
||||
$subMenuHover:#001528;
|
||||
|
||||
$sideBarWidth: 210px;
|
||||
|
||||
// the :export directive is the magic sauce for webpack
|
||||
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
|
||||
:export {
|
||||
menuText: $menuText;
|
||||
menuActiveText: $menuActiveText;
|
||||
subMenuActiveText: $subMenuActiveText;
|
||||
menuBg: $menuBg;
|
||||
menuHover: $menuHover;
|
||||
subMenuBg: $subMenuBg;
|
||||
subMenuHover: $subMenuHover;
|
||||
sideBarWidth: $sideBarWidth;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
export function readablizeBytes(bytes) {
|
||||
if (bytes === 0) {
|
||||
return '0'
|
||||
}
|
||||
const s = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']
|
||||
const e = Math.floor(Math.log(bytes) / Math.log(1024))
|
||||
const r = bytes / Math.pow(1024, Math.floor(e))
|
||||
return ((r + '').indexOf('.') !== -1 ? r.toFixed(2) : r) + ' ' + s[e]
|
||||
}
|
||||
|
||||
export function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
export function isValidIP(ip) {
|
||||
var reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
|
||||
return reg.test(ip)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isExternal(path) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path)
|
||||
}
|
||||
|
||||
export function base64Encode(str) {
|
||||
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(str))
|
||||
}
|
||||
|
||||
export function base64Decode(str) {
|
||||
return CryptoJS.enc.Base64.parse(str).toString(CryptoJS.enc.Utf8)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import axios from 'axios'
|
||||
import store from '@/store/index.js'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import NProgress from 'nprogress'
|
||||
import i18n from '@/lang'
|
||||
|
||||
NProgress.configure({ showSpinner: false }) // NProgress Configuration
|
||||
|
||||
function validateStatus(status) {
|
||||
const { t } = i18n.global
|
||||
switch (status) {
|
||||
case 400:
|
||||
ElMessage.error(t('request.requestError'))
|
||||
break
|
||||
case 401:
|
||||
ElMessage.warning({
|
||||
message: t('request.authFail')
|
||||
})
|
||||
store.commit('LOGIN_OUT')
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 1000)
|
||||
return
|
||||
case 403:
|
||||
ElMessage.warning({
|
||||
ElMessage: t('request.accessDenied')
|
||||
})
|
||||
break
|
||||
case 404:
|
||||
ElMessage.warning({
|
||||
ElMessage: t('request.notFound')
|
||||
})
|
||||
break
|
||||
case 500:
|
||||
if (!store.state.noerror) {
|
||||
ElMessage.warning({
|
||||
ElMessage: t('request.serverError')
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
return status >= 200 && status < 300
|
||||
}
|
||||
|
||||
var instance = axios.create({
|
||||
timeout: 8000,
|
||||
baseURL: process.env.NODE_ENV === 'production' ? '/' : '/api',
|
||||
validateStatus
|
||||
})
|
||||
|
||||
const progressWhiteList = ['/auth/loginUser'] // no progress whitelist
|
||||
// 添加请求拦截器
|
||||
instance.interceptors.request.use(
|
||||
function(config) {
|
||||
if (store.state.nprogress && progressWhiteList.indexOf(config.url) === -1) {
|
||||
NProgress.start()
|
||||
}
|
||||
// 请求头添加token
|
||||
if (store.state.UserToken) {
|
||||
config.headers.Authorization = `Bearer ${store.state.UserToken}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
function(error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器即异常处理
|
||||
instance.interceptors.response.use(
|
||||
response => {
|
||||
NProgress.done()
|
||||
return response.data
|
||||
},
|
||||
err => {
|
||||
if (store.state.nprogress && err.response === undefined && !store.state.noerror) {
|
||||
ElMessage.error(i18n.global.t('request.connectError'))
|
||||
}
|
||||
NProgress.done()
|
||||
return Promise.reject(err.response)
|
||||
}
|
||||
)
|
||||
|
||||
export default instance
|
||||
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row v-if="isAdmin">
|
||||
<el-col :span='24'>
|
||||
<el-card shadow="hover">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="12">
|
||||
<el-row>
|
||||
<el-col :span="12" style="text-align: center">
|
||||
<el-progress type="dashboard" :percentage="cpu.percentage" :color="cpu.color"></el-progress>
|
||||
<div>CPU</div>
|
||||
</el-col>
|
||||
<el-col :span="12" style="text-align: center">
|
||||
<el-progress type="dashboard" :percentage="memory.percentage" :color="memory.color"></el-progress>
|
||||
<div>{{ $t('dashboard.memory') }}: {{memory.used}}/{{memory.total}}</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<el-row>
|
||||
<el-col :span="12" style="text-align: center">
|
||||
<el-progress type="dashboard" :percentage="swap.percentage" :color="swap.color"></el-progress>
|
||||
<div>swap: {{swap.used}}/{{swap.total}}</div>
|
||||
</el-col>
|
||||
<el-col :span="12" style="text-align: center">
|
||||
<el-progress type="dashboard" :percentage="disk.percentage" :color="disk.color"></el-progress>
|
||||
<div>{{ $t('dashboard.disk') }}: {{disk.used}}/{{disk.total}}</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="12">
|
||||
<el-card class="home-card" shadow="hover">
|
||||
<el-row>
|
||||
<el-col :span="10">
|
||||
<b>{{ $t('dashboard.trojanVersion') }}: </b>
|
||||
</el-col>
|
||||
<el-col :span="12" style="padding-top:1px">
|
||||
{{ trojanVersion }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<el-card class="home-card" shadow="hover">
|
||||
<el-row>
|
||||
<el-col :span="10">
|
||||
<b>{{ $t('dashboard.trojanUser') }}:</b>
|
||||
</el-col>
|
||||
<el-col :span="12" style="padding-top:1px">
|
||||
<el-link type='primary' @click="navigate('/user')">{{ userList.length }}</el-link>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="12">
|
||||
<el-card class="home-card" shadow="hover">
|
||||
<el-row>
|
||||
<el-col :span="10">
|
||||
<b>{{ $t('dashboard.trojanUptime') }}:</b>
|
||||
</el-col>
|
||||
<el-col :span="12" style="padding-top:1px">
|
||||
{{ trojanUptime }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" v-if="isAdmin">
|
||||
<el-card class="home-card" shadow="hover">
|
||||
<el-row>
|
||||
<el-col :span="10">
|
||||
<b>{{ $t('dashboard.load') }}:</b>
|
||||
</el-col>
|
||||
<el-tooltip class="item" effect="dark" content="load1, load5, load15" placement="top-start">
|
||||
<el-col :span="12" style="padding-top:1px">
|
||||
{{ load }}
|
||||
</el-col>
|
||||
</el-tooltip>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="12" v-if="isAdmin">
|
||||
<el-card class="home-card" shadow="hover">
|
||||
<el-row>
|
||||
<el-col :span="10">
|
||||
<b>{{ $t('dashboard.netSpeed') }}:</b>
|
||||
</el-col>
|
||||
<el-col :span="12" style="padding-top:1px">
|
||||
<i class="el-icon-top" style="margin-right: 8px">{{ netSpeed.up }}</i>
|
||||
<i class="el-icon-bottom">{{ netSpeed.down }}</i>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" v-if="isAdmin">
|
||||
<el-card class="home-card" shadow="hover">
|
||||
<el-row>
|
||||
<el-col :span="10">
|
||||
<b>{{ $t('dashboard.netCount') }}:</b>
|
||||
</el-col>
|
||||
<el-tooltip class="item" effect="dark" content="tcp / udp" placement="top-start">
|
||||
<el-col :span="12" style="padding-top:1px">
|
||||
{{ netCount }}
|
||||
</el-col>
|
||||
</el-tooltip>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row style="margin-top:10px">
|
||||
<el-col :span='7'>
|
||||
<el-card shadow="hover">
|
||||
{{ $t('dashboard.upload') }}:
|
||||
<el-tag effect="dark" type="success">{{ uploadData }}</el-tag>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span='7' :offset='1'>
|
||||
<el-card shadow="hover">
|
||||
{{ $t('dashboard.download') }}:
|
||||
<el-tag effect="dark" type="success">{{ downloadData }}</el-tag>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span='7' :offset='1'>
|
||||
<el-card shadow="hover">
|
||||
{{ $t('dashboard.total') }}:
|
||||
<el-tag effect="dark" type="success">{{ totalData }}</el-tag>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { version, serverInfo } from '@/api/common'
|
||||
import { userList } from '@/api/user'
|
||||
import { readablizeBytes } from '@/utils/common'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
timer: null,
|
||||
trojanVersion: '',
|
||||
trojanUptime: '',
|
||||
keyOffset: 0,
|
||||
valueOffset: 0,
|
||||
userList: [],
|
||||
downloadData: 0,
|
||||
uploadData: 0,
|
||||
totalData: 0,
|
||||
cpu: { percentage: 0, color: '' },
|
||||
memory: { percentage: 0, used: 0, total: 0, color: '' },
|
||||
swap: { percentage: 0, used: 0, total: 0, color: '' },
|
||||
disk: { percentage: 0, used: 0, total: 0, color: '' },
|
||||
load: '',
|
||||
netSpeed: { up: '', down: '' },
|
||||
netCount: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['isAdmin'])
|
||||
},
|
||||
created() {
|
||||
this.$store.commit('SET_NPROGRESS', false)
|
||||
this.setOffset()
|
||||
this.getVersion()
|
||||
this.getUserList()
|
||||
},
|
||||
mounted() {
|
||||
if (this.isAdmin) {
|
||||
this.getServerInfo()
|
||||
}
|
||||
this.timer = setInterval(() => {
|
||||
if (this.isAdmin) {
|
||||
this.getServerInfo()
|
||||
}
|
||||
this.getVersion()
|
||||
this.getUserList()
|
||||
}, 6000)
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
this.setOffset()
|
||||
})()
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
this.$store.commit('SET_NPROGRESS', true)
|
||||
clearInterval(this.timer)
|
||||
},
|
||||
methods: {
|
||||
setOffset() {
|
||||
const clientWith = document.body.clientWidth
|
||||
if (clientWith < 1000) {
|
||||
this.keyOffset = 1
|
||||
this.valueOffset = 2
|
||||
this.iconShow = false
|
||||
} else {
|
||||
this.keyOffset = 0
|
||||
this.valueOffset = 0
|
||||
this.iconShow = true
|
||||
}
|
||||
},
|
||||
navigate(path) {
|
||||
this.$router.push({ path: path })
|
||||
},
|
||||
getServerInfo() {
|
||||
serverInfo().then((res) => {
|
||||
const data = res.Data
|
||||
this.cpu.percentage = parseFloat(data.cpu[0].toFixed(2))
|
||||
this.cpu.color = this.computeColor(this.cpu.percentage)
|
||||
this.memory = this.computePercent(data.memory)
|
||||
this.swap = this.computePercent(data.swap)
|
||||
this.disk = this.computePercent(data.disk)
|
||||
this.netSpeed.up = readablizeBytes(data.speed.Up) + '/s'
|
||||
this.netSpeed.down = readablizeBytes(data.speed.Down) + '/s'
|
||||
this.netCount = data.netCount.tcp + ' / ' + data.netCount.udp
|
||||
this.load = data.load.load1 + ', ' + data.load.load5 + ', ' + data.load.load15
|
||||
})
|
||||
},
|
||||
computePercent(data) {
|
||||
const percent = parseFloat(data.usedPercent.toFixed(2))
|
||||
return {
|
||||
percentage: percent,
|
||||
used: readablizeBytes(data.used),
|
||||
total: readablizeBytes(data.total),
|
||||
color: this.computeColor(percent)
|
||||
}
|
||||
},
|
||||
computeColor(percent) {
|
||||
if (percent < 80) {
|
||||
return '#67C23A'
|
||||
} else if (percent < 90) {
|
||||
return '#E6A23C'
|
||||
} else {
|
||||
return '#F56C6C'
|
||||
}
|
||||
},
|
||||
async getUserList() {
|
||||
const result = await userList()
|
||||
if (result.Msg === 'success') {
|
||||
const data = result.Data
|
||||
this.userList = data.userList
|
||||
let download = 0; let upload = 0
|
||||
for (let i = 0; i < this.userList.length; i++) {
|
||||
download += this.userList[i].Download
|
||||
upload += this.userList[i].Upload
|
||||
}
|
||||
this.downloadData = readablizeBytes(download)
|
||||
this.uploadData = readablizeBytes(upload)
|
||||
this.totalData = readablizeBytes(download + upload)
|
||||
} else {
|
||||
this.$message.error(result.Msg)
|
||||
}
|
||||
},
|
||||
async getVersion() {
|
||||
const result = await version()
|
||||
const data = result.Data
|
||||
this.trojanVersion = data.trojanVersion
|
||||
this.trojanUptime = this.parseUptime(data.trojanUptime)
|
||||
},
|
||||
parseUptime(uptime) {
|
||||
let result = ''
|
||||
if (uptime.indexOf('-') !== -1) {
|
||||
const splitInfo = uptime.split('-')
|
||||
result += splitInfo[0] + `${this.$t('dashboard.day')} `
|
||||
const timeInfo = splitInfo[1].split(':')
|
||||
result += timeInfo[0] + `${this.$t('dashboard.hour')} `
|
||||
result += timeInfo[1] + `${this.$t('dashboard.minute')} `
|
||||
result += timeInfo[2] + `${this.$t('dashboard.second')} `
|
||||
} else {
|
||||
const splitInfo = uptime.split(':')
|
||||
if (splitInfo.length === 3) {
|
||||
result += splitInfo[0] + `${this.$t('dashboard.hour')} `
|
||||
result += splitInfo[1] + `${this.$t('dashboard.minute')} `
|
||||
result += splitInfo[2] + `${this.$t('dashboard.second')} `
|
||||
} else if (splitInfo.length === 2) {
|
||||
result += splitInfo[0] + `${this.$t('dashboard.minute')} `
|
||||
result += splitInfo[1] + `${this.$t('dashboard.second')} `
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home-icon {
|
||||
font-size: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
.home-card {
|
||||
// margin-top:10px;
|
||||
padding:3px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<div class="wscn-http404-container">
|
||||
<div class="wscn-http404">
|
||||
<div class="pic-404">
|
||||
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
|
||||
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
|
||||
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
|
||||
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
|
||||
</div>
|
||||
<div class="bullshit">
|
||||
<div class="bullshit__oops">OOPS!</div>
|
||||
<div class="bullshit__info">All rights reserved
|
||||
<a style="color:#20a0ff" href="https://vuejs.org" target="_blank">vuejs</a>
|
||||
</div>
|
||||
<div class="bullshit__headline">{{ message }}</div>
|
||||
<div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div>
|
||||
<a href="" class="bullshit__return-home">Back to home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Page404',
|
||||
computed: {
|
||||
message() {
|
||||
return 'The webmaster said that you can not enter this page...'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#app {
|
||||
> div {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
.wscn-http404-container{
|
||||
transform: translate(-50%,-50%);
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
}
|
||||
.wscn-http404 {
|
||||
position: relative;
|
||||
width: 1200px;
|
||||
padding: 0 50px;
|
||||
overflow: hidden;
|
||||
.pic-404 {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 600px;
|
||||
overflow: hidden;
|
||||
&__parent {
|
||||
width: 100%;
|
||||
}
|
||||
&__child {
|
||||
position: absolute;
|
||||
&.left {
|
||||
width: 80px;
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
animation-name: cloudLeft;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
&.mid {
|
||||
width: 46px;
|
||||
top: 10px;
|
||||
left: 420px;
|
||||
opacity: 0;
|
||||
animation-name: cloudMid;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
&.right {
|
||||
width: 62px;
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
opacity: 0;
|
||||
animation-name: cloudRight;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
@keyframes cloudLeft {
|
||||
0% {
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 33px;
|
||||
left: 188px;
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
top: 81px;
|
||||
left: 92px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 97px;
|
||||
left: 60px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes cloudMid {
|
||||
0% {
|
||||
top: 10px;
|
||||
left: 420px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 40px;
|
||||
left: 360px;
|
||||
opacity: 1;
|
||||
}
|
||||
70% {
|
||||
top: 130px;
|
||||
left: 180px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 160px;
|
||||
left: 120px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes cloudRight {
|
||||
0% {
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 120px;
|
||||
left: 460px;
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
top: 180px;
|
||||
left: 340px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 200px;
|
||||
left: 300px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.bullshit {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 300px;
|
||||
padding: 30px 0;
|
||||
overflow: hidden;
|
||||
&__oops {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
line-height: 40px;
|
||||
color: #1482f0;
|
||||
opacity: 0;
|
||||
margin-bottom: 20px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__headline {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
color: #222;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
margin-bottom: 10px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.1s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__info {
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
color: grey;
|
||||
opacity: 0;
|
||||
margin-bottom: 30px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.2s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__return-home {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 110px;
|
||||
height: 36px;
|
||||
background: #1482f0;
|
||||
border-radius: 100px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
opacity: 0;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
cursor: pointer;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
transform: translateY(60px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition appear name="fade-transform" mode="out-in">
|
||||
<component :is="Component" :key="key" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AppMain',
|
||||
computed: {
|
||||
key() {
|
||||
return this.$route.path
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-main {
|
||||
transition: margin-left 0.25s;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
padding: 60px 20px 5px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,416 @@
|
||||
<template>
|
||||
<div class="navbar">
|
||||
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
|
||||
|
||||
<breadcrumb class="breadcrumb-container" />
|
||||
|
||||
<div class="right-menu">
|
||||
<div>
|
||||
<el-dropdown trigger="click" >
|
||||
<el-button style="margin-top:13px" :icon="ArrowDown" link/>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="systemVersion(); versionVisible=true">{{ $t('navbar.version') }}</el-dropdown-item>
|
||||
<el-dropdown placement='right-start' class="el-dropdown-menu__item" v-if="isAdmin">
|
||||
<span>
|
||||
{{ $t('navbar.setting') }}
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="getTitle(); loginVisible=true">{{ $t('navbar.title') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="dialogVisible=true">{{ $t('navbar.password') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="getRules(); rulesVisible=true">{{ $t('navbar.clashRules') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="importExportVisible=true">{{ $t('navbar.importExport') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="getResetDay(); resetDayVisible=true">{{ $t('navbar.resetDay') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dropdown-item @click="handleSetLanguage('zh')" divided>中文</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleSetLanguage('en')">English</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dialog :modal="false" :title="$t('navbar.changeTitle')" v-model="loginVisible" :width="dialogWidth">
|
||||
<el-input type="text" v-model="title" :placeholder="$t('navbar.inputTitle')" @keyup.enter="handleLoginInfo"/>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="loginVisible = false">{{ $t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleLoginInfo()">{{ $t('ok') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog class="ruleDialog" :modal="false" :title="$t('navbar.changeRules')" v-model="rulesVisible" :width="clashDialogWidth" :show-close="false">
|
||||
<el-input type="textarea" v-model="rules" :rows="12" :placeholder="$t('navbar.inputTitle')"/>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="rulesVisible = false">{{ $t('cancel') }}</el-button>
|
||||
<el-button @click="resetClashRules()">{{ $t('reset') }}</el-button>
|
||||
<el-button type="primary" @click="handleClashRules()">{{ $t('ok') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog :modal="false" :title="$t('navbar.versionTitle')" v-model="versionVisible" :width="dialogWidth">
|
||||
<p> version: {{ versionList.version }} </p>
|
||||
<p> gitVersion: {{ versionList.gitVersion.slice(0,7) }} </p>
|
||||
<p> buildDate: {{ versionList.buildDate }} </p>
|
||||
<p> goVersion: {{ versionList.goVersion }} </p>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="versionVisible = false">{{ $t('ok') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog :modal="false" :title="$t('navbar.resetTitle')" v-model="resetDayVisible" :width="dialogWidth">
|
||||
<el-tooltip effect="dark" :content="$t('navbar.meanClose')" placement="top">
|
||||
<el-input-number size="small" v-model="resetDay" :min=0 :max=31></el-input-number>
|
||||
</el-tooltip>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="resetDayVisible = false">{{ $t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleResetDay()">{{ $t('ok') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog :modal="false" :title="$t('navbar.importExport')" v-model="importExportVisible" :width="dialogWidth">
|
||||
<el-tooltip effect="dark" :content="$t('navbar.exportTip')" placement="top">
|
||||
<el-button type="primary" @click="downloadCsv(); importExportVisible=false">{{ $t('navbar.exportCsv') }}</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="dark" :content="$t('navbar.importTip')" placement="top">
|
||||
<el-upload accept=".csv" :action="uploadUrl" :on-success="uploadSuccess">
|
||||
<el-button type="primary">{{ $t('navbar.importCsv') }}</el-button>
|
||||
</el-upload>
|
||||
</el-tooltip>
|
||||
</el-dialog>
|
||||
<el-dialog :modal="false" :title="$t('navbar.passwordTitle')" v-model="dialogVisible" :width="dialogWidth">
|
||||
<el-form :model="form" :rules="registerRules" ref="form" label-position="left">
|
||||
<el-form-item prop="password1">
|
||||
<el-input name="password1" :type="pwdType" v-model="form.password1" :placeholder="$t('inputPass')" show-password/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password2">
|
||||
<el-input name="password2" :type="pwdType" v-model="form.password2" :placeholder="$t('inputPassAgain')" show-password/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">{{ $t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="dialogVisible = false; changePass()">{{ $t('ok') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<div @click="loginOut">
|
||||
<span class="iconfont icon-quit"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ElMessage } from "element-plus"
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import Breadcrumb from '@/components/Breadcrumb'
|
||||
import Hamburger from '@/components/Hamburger'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { sleep } from '@/utils/common'
|
||||
import { resetPass, check } from '@/api/permission'
|
||||
import { version, setLoginInfo, getClashRules, setClashRules, resetClashRules } from '@/api/common'
|
||||
import { getResetDay, updateResetDay } from '@/api/data'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
return {
|
||||
ArrowDown
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const validatePass = (rule, value, callback) => {
|
||||
if (value.length < 5) {
|
||||
callback(new Error(this.$t('passLessError')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
return {
|
||||
versionList: {
|
||||
version: '',
|
||||
buildDate: '',
|
||||
gitVersion: '',
|
||||
goVersion: ''
|
||||
},
|
||||
pwdType: 'password',
|
||||
dialogVisible: false,
|
||||
versionVisible: false,
|
||||
resetDayVisible: false,
|
||||
importExportVisible: false,
|
||||
loginVisible: false,
|
||||
rulesVisible: false,
|
||||
title: '',
|
||||
rules: '',
|
||||
resetDay: 1,
|
||||
form: {
|
||||
password1: '',
|
||||
password2: ''
|
||||
},
|
||||
registerRules: {
|
||||
password1: [
|
||||
{ required: true, trigger: 'blur', validator: validatePass }
|
||||
],
|
||||
password2: [
|
||||
{ required: true, trigger: 'blur', validator: validatePass }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Breadcrumb,
|
||||
Hamburger
|
||||
},
|
||||
created() {
|
||||
document.title = this.docTitle
|
||||
},
|
||||
computed: {
|
||||
...mapState(['docTitle', 'isAdmin', 'dialogWidth']),
|
||||
...mapGetters([
|
||||
'sidebar'
|
||||
]),
|
||||
clashDialogWidth: () => {
|
||||
const clientWidth = document.body.clientWidth
|
||||
let clashWidth = '42%'
|
||||
if (clientWidth < 600) {
|
||||
clashWidth = '95%'
|
||||
} else if (clientWidth >= 600 && clientWidth < 1000) {
|
||||
clashWidth = '80%'
|
||||
}
|
||||
return clashWidth
|
||||
},
|
||||
uploadUrl: () => {
|
||||
return `${process.env.NODE_ENV === 'production' ? `${location.origin}` : 'api'}/trojan/import`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSetLanguage(lang) {
|
||||
this.$i18n.locale = lang
|
||||
this.$store.dispatch('app/setLanguage', lang)
|
||||
ElMessage({
|
||||
message: lang === 'zh' ? '切换语言成功' : 'Switch Language Success',
|
||||
type: 'success'
|
||||
})
|
||||
},
|
||||
toggleSideBar() {
|
||||
this.$store.dispatch('app/toggleSideBar')
|
||||
},
|
||||
downloadCsv() {
|
||||
const prefix = process.env.NODE_ENV === 'production' ? `${location.origin}` : 'api'
|
||||
const downloadUrl = `${prefix}/trojan/export?token=${this.$store.state.UserToken}`
|
||||
window.open(downloadUrl)
|
||||
},
|
||||
uploadSuccess(res) {
|
||||
if (res.Msg === 'success') {
|
||||
ElMessage({
|
||||
message: this.$t('navbar.importSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
ElMessage.error(res.Msg)
|
||||
}
|
||||
},
|
||||
async getTitle() {
|
||||
const result = await check()
|
||||
this.title = result.data.title
|
||||
},
|
||||
async getResetDay() {
|
||||
const result = await getResetDay()
|
||||
this.resetDay = result.Data.resetDay
|
||||
},
|
||||
async handleResetDay() {
|
||||
const formData = new FormData()
|
||||
formData.set('day', this.resetDay)
|
||||
const result = await updateResetDay(formData)
|
||||
if (result.Msg === 'success') {
|
||||
if (this.resetDay === 0) {
|
||||
ElMessage({
|
||||
message: this.$t('navbar.closeResetSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
ElMessage({
|
||||
message: this.$t('navbar.changeDaySuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(result.Msg)
|
||||
}
|
||||
this.resetDayVisible = false
|
||||
},
|
||||
async handleLoginInfo() {
|
||||
const formData = new FormData()
|
||||
formData.set('title', this.title)
|
||||
const result = await setLoginInfo(formData)
|
||||
if (result.Msg === 'success') {
|
||||
ElMessage({
|
||||
message: this.$t('navbar.changeTitleSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
document.title = this.title
|
||||
this.$store.commit('SET_TITLE', this.title)
|
||||
} else {
|
||||
ElMessage.error(result.Msg)
|
||||
}
|
||||
this.loginVisible = false
|
||||
},
|
||||
async getRules() {
|
||||
const result = await getClashRules()
|
||||
this.rules = result.Data
|
||||
},
|
||||
async resetClashRules() {
|
||||
const result = await resetClashRules()
|
||||
if (result.Msg === 'success') {
|
||||
ElMessage({
|
||||
message: this.$t('navbar.resetRulesSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
this.rules = result.Data
|
||||
} else {
|
||||
ElMessage.error(result.Msg)
|
||||
}
|
||||
},
|
||||
async handleClashRules() {
|
||||
const formData = new FormData()
|
||||
formData.set('rules', this.rules)
|
||||
const result = await setClashRules(formData)
|
||||
if (result.Msg === 'success') {
|
||||
ElMessage({
|
||||
message: this.$t('navbar.changeRulesSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
ElMessage.error(result.Msg)
|
||||
}
|
||||
this.rulesVisible = false
|
||||
},
|
||||
async systemVersion() {
|
||||
const result = await version()
|
||||
this.versionList = result.Data
|
||||
},
|
||||
async changePass() {
|
||||
const formData = new FormData()
|
||||
if (this.form.password1 !== this.form.password2) {
|
||||
ElMessage.error(this.$t('passDifferentError'))
|
||||
return
|
||||
} else {
|
||||
formData.set('password', CryptoJS.SHA224(this.form.password1).toString())
|
||||
}
|
||||
try {
|
||||
await resetPass(formData)
|
||||
ElMessage({
|
||||
message: this.$t('navbar.resetSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
await sleep(1000 * 2)
|
||||
this.$store.commit('LOGIN_OUT')
|
||||
this.$router.replace('/login').catch()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
loginOut() {
|
||||
this.$store.commit('LOGIN_OUT')
|
||||
/* 防止切换角色时addRoutes重复添加路由导致出现警告 */
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@font-face {font-family: "iconfont";
|
||||
src: url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAz8AAsAAAAAEpQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAAQwAAAFZW7kiTY21hcAAAAYAAAACiAAACLmU6ObFnbHlmAAACJAAACHkAAAtIt91jDGhlYWQAAAqgAAAAMAAAADYRunP+aGhlYQAACtAAAAAgAAAAJAhuBRNobXR4AAAK8AAAAB0AAAAsLXn//mxvY2EAAAsQAAAAGAAAABgPfBL+bWF4cAAACygAAAAfAAAAIAEcAO1uYW1lAAALSAAAAUUAAAJtPlT+fXBvc3QAAAyQAAAAawAAAJLeETAdeJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2BkUWKcwMDKwMHUyXSGgYGhH0IzvmYwYuRgYGBiYGVmwAoC0lxTGBwYKp7VMTf8b2CIYW5kaAAKM4LkAOekDBYAeJzFkrEOwjAMRC9pKFAxMLB36Fj+rUuGLu3MZzDxafcb5Rx3IWKGi14kXxTHsgPgAKARd5GA8EKA6Sk3FL9BV/yErPiGq5yEmWBkz4EjJ2au26bT2l2KWysoS73M7ZT5hIgWZ9V1LPW0X+7/SOF/T3/qUvbHHmkamHdUIuGYz+jYRNk76ig4OLDz0VGXwclRv8Hs2I/g4mgG4OogvQF8cjFyAAB4nHVWe4xUVxk/3zn3nvu+d+Y+Z+7MnefODNuF2dfcGRbc3VDp0oVuKZGyrUQNKbHFpjVsI9jQdiGAkrRCNdHEpDFKjX1QLWppGlNj25jQKBiTEjH6jwQfiY1/NfGf7lz87gwYjOneu+c153zn+77z+/3OJSIhN66xt1mOOGQdmSRbyX2EAB+DmkkjqLY6bToGXlX0AtdkrXqrKtVrbTYLQY27/lS30wy4xC0woQTT1aluq01bEHfm6GaY8iOAfCH8jN0o2ux5UHOt0slkOz0LXrletOY2JIvr592piiMf1m07b9vPyVwUZUoFy4THAl8RFZUnPxSt0Hu7PErLoOdb4T0PGpWCve9U5/GoESgAR4+CU6iYL81nwyy+T4W+Y+eljCHnQqM+4sLhv2o5R4+a1wn+CRjrL9kHbB8pkd3kq+Q7hDhzEM9BrwSBi3H4gT/d7eFQG1omSK7faNYsaDUx1jJ49TTkYNqrm+D6m8FL45+F6TQJrXo8PQed5hjEvW4HcHlTqqG9Mk2brWZ9aFzopTnqxZ2mVK01W+3Bxn6AxVR3HHBejE18L1iOcP7EyfNCPidxXVZtLSoJV8/+4Krgwp9kX95xj+Jl5OWtmmVpW4elFVi3d5dlaG8/Itw11rBVpujWy0eKXtS7o+raTN4L1BXhb6YDVjfZUri6yEAUAjdwRl75l2/rVU8FqVbiT+yA98Ax0Qvh/MkN9zeMUOZgtR/pnr0qoDOm80cZHZEzvrwc4e4QA5a7tExG2/XfbnFZ9hPH3sTGoeSMWXagL4wXS12Iyrvsn64IjnVWt7/3xee1OzQwnVxh8165kR0vWZSB5QobPTwzjmd2lP2EHSMWKZNp0iOL5EFEaLXGJW+IuVlMvISHkvYRdpjfNkAV+9n09zmYT8eybWCdeDLGQww8TH6c7XRxse9J1eHKW5boe8mqogPoChxTdP2DtxQNQFOGFaz2qZNHWDsUwaBfhqwI+fLrkQ+Jo5mmRi9o5s//Zz28bKiaYhiKphq/AV3FCgdUnb4Iobv2BbcAqnY559NfuYW1DwE2+MXXLY3tTQ/x/G3TiYR5+JpA2BPI0xbZRx4jT5Ir5C+YiRR3LQSqJKa4DER/qic1mhinh/FjF4GZRu8gEuM0Gd1eq9mdx2VlGFTTvSm/DH6AuA+yaKqZGpKyKRem5mmz1cMeGkIrbcRFGzq4pAQu7w0g2+s2saojTbBuQ12qoRpwqTUYMnGPgThIgTvYI6UEpHYB6+FppQ4NtkJXxocexRL6OmSlCWyvaxhu8lp4V5i85um6B7uxeTr5vaZ5eaiFsF7Tcqg0Iz+SbQBbcbiqikwuS77qZHKc2eq4lOHcd01RA0XLlZIrhTqAr1uasU7U+eV4AWAh7qRl1/ANfJ0wvAJaWTJNRoUylwUhny/KVOXruUrlYi4URFksC5RaplTgKS54TrbSyRX8QQhzOFnhGwaT83kmyhwnM8OSiyL/+6Z7Ae7dtGkJYIm+JLuGozyt4OsYrvy0qq5fEUphcrLcUVdEKOfhSBQnd7OMsV7PMu46ruJjZEzUZCmjmaCYpqSplAKIVk7OaCtap5ScrFmyaCu+/eYgLEoXOvHCK7qfD3QotMIFQ5CjXF7UxbJIDYv7XAGZ57lpURzReZgr3ApVKeRC0cAMUMuSQq7rPJAsnYoVwRBz+Sid9hW6hLHQQUnYQGS/ST8iOVIls4jNbtxBlWulFOPRgJBBNYVjo4otBY8aqvhzFpHR9KcnEA69CRRgiceQNUE4A1SeHVu7NjYrA8CWCVae2AJrX2blbEFK5pJZqcAm1q7lKgC/EDkXGaWMARcE7EOFngIqCqMxQDwqiEzqJ7gYjVA6A1kr+TeoVrb/Dajk4M/Jx5hBACowLIWkkauQYSzsRfoq0UhA6gPFaXa6vWoKffZJ7eN937Rtk/7TsG3jE9r0S3YhRWrBTj76/1a6LUWuv8vOsy2kQ7YQItbSHDbTW7XXLSG50qfEpuboMLttzGNQYp4rpWmuO0NxQ2nLupzS5fuWDlVr+Yk7R/UwK2oL60YBug8f/9bxR7qj7saaLmQK+uid4/la5fDSzgdU03Eix5mvlle2L+2e2re8GEmCH8qTizv2LKw+1Ok8tLqwZ8fIXE4OfUEuLi7vm9q9tH2lUoF3HANW0yCTY4Yz1O1vs3PsUVTtBhkjj5LD5LuYxVqzfvN29Wq8PuT5kOoDtjcGGtbwUlETW81B4K1u+p/OjZvdm5N9yU1lxuVeaolj5BYMbKQ5EIdSB7cJF1pNBT7uphb5IFHpBd2qteqoeKk77H5RE9mFU6cuIL3EkUlUlMi9ePr0RTcaAZjsF1HMFfoHLPu/c1SJKUwSgKkSMASPoLmNE7sOvcCACZRrlImMK6IkOrOfCwyF0Ua+fzQcoXAA/P0nGDuxf1Am2wyZPiVbzA0++6lMRrG1jC2IGTdwBdteV929H79v6DPoELr1DIWJkXJbevYNxt54VmqXRyaeFHGmuLiYVu+4qsY1rpoAAcqTLvLa4rrFB4C9cMi2NY3nfaCAw1TTRV1w6we3NbbU8xUcrOTDx38M/7jlE5ZrH8qaIc/OfPpARZUECcmD97HARMMf+/7+AS1uJDfOCgr7/OCLEXmuAF4/MFmP0+sjJXr6OYjXrseGhI9ZZzrw56DZSHHpmtBkeD7DT0Rpgn2czLgzbpJAxrByR1XmVLSdyMRmRB+OWgA7tYrL+r+NxorFsagDrvl107sEx+Gii2sOmC5lnvWzVS0KqL4Tku1RM10Kb8JOnQbRr70IF0LUf/eS6brmJSADbt94i73P7saWg+wmTraaZfXszSeue/VsFapedYadXjm4MoPPQfpq/wzd2H+fbetH9Hr/NETJdTiTHDx3jpL+npX/AJjxus8AAAB4nGNgZGBgAGKtnxol8fw2Xxm4WRhA4DqbBJz+/+9/A8tE5kYgl4OBCSQKAApJCfl4nGNgZGBgbvjfwBDD2vn/3/9/LBMZgCIogBsAuMkHiXicY2FgYGB+ycDAwoDArJ2ofBb2//9ANAA/KwOjAAAAAAAAAAB2AVAB1AMmA4wDygQsBRoFegWkeJxjYGRgYOBmeMjAzgACTEDMBYQMDP/BfAYAIA0CCgB4nGWPTU7DMBCFX/oHpBKqqGCH5AViASj9EatuWFRq911036ZOmyqJI8et1ANwHo7ACTgC3IA78EgnmzaWx9+8eWNPANzgBx6O3y33kT1cMjtyDRe4F65TfxBukF+Em2jjVbhF/U3YxzOmwm10YXmD17hi9oR3YQ8dfAjXcI1P4Tr1L+EG+Vu4iTv8CrfQ8erCPuZeV7iNRy/2x1YvnF6p5UHFockikzm/gple75KFrdLqnGtbxCZTg6BfSVOdaVvdU+zXQ+ciFVmTqgmrOkmMyq3Z6tAFG+fyUa8XiR6EJuVYY/62xgKOcQWFJQ6MMUIYZIjK6Og7VWb0r7FDwl57Vj3N53RbFNT/c4UBAvTPXFO6stJ5Ok+BPV8bUnV0K27LnpQ0kV7NSRKyQl7WtlRC6gE2ZVeOEXpc0Yk/KGdI/wAJWm7IAAAAeJxtx0ESgjAMBdB8KKKA3MRDZUqsmaGpVsrA7VnI0rd7VNFPR//1qFDDocEFLa64oUOPgbCNTzU2L4/IxkGGlCfJZ8Z3TlPxy9lGIutcG6/uU3Rpv5JX9eJeKcp9L7arhVDYZiU6AL4JICUA') format('woff');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family:"iconfont" !important;
|
||||
font-size:16px;
|
||||
font-style:normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-quit:before { content: "\e67d"; }
|
||||
|
||||
.navbar {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
position: relative;
|
||||
transition: left 0.25s;
|
||||
.hamburger-container {
|
||||
line-height: 46px;
|
||||
height: 100%;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
transition: background .3s;
|
||||
-webkit-tap-highlight-color:transparent;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, .025)
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.right-menu {
|
||||
height: 100%;
|
||||
float: right;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: -1px;
|
||||
bottom: 0px;
|
||||
> div {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
padding: 0 15px;
|
||||
cursor: pointer;
|
||||
&:hover::after {
|
||||
transform-origin: 0 0;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
&:first-child:before {
|
||||
border: none;
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: #ef4747;
|
||||
transform: scaleX(0);
|
||||
transform-origin: right 0;
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 20px;
|
||||
top: 50%;
|
||||
left: -8px;
|
||||
margin-top: -10px;
|
||||
border-left: 1px solid #ccc;
|
||||
}
|
||||
.iconfont {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
color: #758eb5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<component :is="type" v-bind="linkProps(to)">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isExternal } from '@/utils/common'
|
||||
export default {
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isExternal() {
|
||||
return isExternal(this.to)
|
||||
},
|
||||
type() {
|
||||
if (this.isExternal) {
|
||||
return 'a'
|
||||
}
|
||||
return 'router-link'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkProps(to) {
|
||||
if (this.isExternal) {
|
||||
return {
|
||||
href: to,
|
||||
target: '_blank',
|
||||
rel: 'noopener'
|
||||
}
|
||||
}
|
||||
return {
|
||||
to: to
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div v-if="!item.hidden">
|
||||
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
|
||||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
|
||||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
|
||||
<svg-icon :icon-class="onlyOneChild.meta.icon"/>
|
||||
<template #title>{{ generateTitle(onlyOneChild.meta.title) }}</template>
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isExternal } from '@/utils/common'
|
||||
import AppLink from './Link'
|
||||
|
||||
export default {
|
||||
name: 'SidebarItem',
|
||||
components: { AppLink },
|
||||
props: {
|
||||
// route object
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isNest: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
|
||||
// TODO: refactor with render function
|
||||
this.onlyOneChild = null
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
generateTitle(title) {
|
||||
const hasKey = this.$t('route.' + title)
|
||||
if (hasKey) {
|
||||
const translatedTitle = this.$t('route.' + title)
|
||||
return translatedTitle
|
||||
}
|
||||
return title
|
||||
},
|
||||
hasOneShowingChild(children = [], parent) {
|
||||
const showingChildren = children.filter(item => {
|
||||
if (item.hidden) {
|
||||
return false
|
||||
} else {
|
||||
// Temp set(will be used if only has one showing child)
|
||||
this.onlyOneChild = item
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// When there is only one child router, the child router is displayed by default
|
||||
if (showingChildren.length === 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Show parent if there are no child router to display
|
||||
if (showingChildren.length === 0) {
|
||||
this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
resolvePath(routePath) {
|
||||
if (isExternal(routePath)) {
|
||||
return routePath
|
||||
}
|
||||
if (isExternal(this.basePath)) {
|
||||
return this.basePath
|
||||
}
|
||||
return `${this.basePath}/${routePath}`.replace('//', '/')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div :class="{'has-logo':false}">
|
||||
<el-scrollbar wrap-class="scrollbar-wrapper">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapse"
|
||||
background-color="#304156"
|
||||
text-color="#bfcbd9"
|
||||
:unique-opened="false"
|
||||
active-text-color="#409EFF"
|
||||
:collapse-transition="false"
|
||||
mode="vertical"
|
||||
>
|
||||
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import SidebarItem from './SidebarItem'
|
||||
|
||||
export default {
|
||||
components: { SidebarItem },
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'sidebar'
|
||||
]),
|
||||
routes() {
|
||||
return this.$router.options.routes
|
||||
},
|
||||
activeMenu() {
|
||||
const route = this.$route
|
||||
const { meta, path } = route
|
||||
// if set path, the sidebar will highlight the path you set
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu
|
||||
}
|
||||
return path
|
||||
},
|
||||
isCollapse() {
|
||||
return !this.sidebar.opened
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as Navbar } from './Navbar'
|
||||
export { default as Sidebar } from './Sidebar'
|
||||
export { default as AppMain } from './AppMain'
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper">
|
||||
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||
<sidebar class="sidebar-container" />
|
||||
<div class="main-container">
|
||||
<div :class="{'fixed-header':true}">
|
||||
<navbar />
|
||||
</div>
|
||||
<app-main />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Navbar, Sidebar, AppMain } from './components'
|
||||
import ResizeMixin from './mixin/ResizeHandler'
|
||||
|
||||
export default {
|
||||
name: 'Layout',
|
||||
components: {
|
||||
Navbar,
|
||||
Sidebar,
|
||||
AppMain
|
||||
},
|
||||
mixins: [ResizeMixin],
|
||||
computed: {
|
||||
sidebar() {
|
||||
return this.$store.state.app.sidebar
|
||||
},
|
||||
device() {
|
||||
return this.$store.state.app.device
|
||||
},
|
||||
classObj() {
|
||||
return {
|
||||
hideSidebar: !this.sidebar.opened,
|
||||
openSidebar: this.sidebar.opened,
|
||||
withoutAnimation: this.sidebar.withoutAnimation,
|
||||
mobile: this.device === 'mobile'
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setDialogWidth()
|
||||
window.onresize = () => {
|
||||
this.setDialogWidth()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClickOutside() {
|
||||
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
|
||||
},
|
||||
setDialogWidth() {
|
||||
const clientWidth = document.body.clientWidth
|
||||
if (clientWidth < 600) {
|
||||
this.dialogWidth = '80%'
|
||||
} else if (clientWidth >= 600 && clientWidth < 1000) {
|
||||
this.dialogWidth = '50%'
|
||||
} else {
|
||||
this.dialogWidth = '25%'
|
||||
}
|
||||
this.$store.commit('SET_WIDTH', this.dialogWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/mixin.scss";
|
||||
@import "@/styles/variables.scss";
|
||||
|
||||
.app-wrapper {
|
||||
// @include clearfix;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
&.mobile.openSidebar{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
.drawer-bg {
|
||||
background: #000;
|
||||
opacity: 0.3;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
width: calc(100% - #{$sideBarWidth});
|
||||
transition: width 0.28s;
|
||||
}
|
||||
|
||||
.hideSidebar .fixed-header {
|
||||
width: calc(100% - 54px)
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
import store from '@/store'
|
||||
|
||||
const { body } = document
|
||||
const WIDTH = 992 // refer to Bootstrap's responsive design
|
||||
|
||||
export default {
|
||||
watch: {
|
||||
$route() {
|
||||
if (this.device === 'mobile' && this.sidebar.opened) {
|
||||
store.dispatch('app/closeSideBar', { withoutAnimation: false })
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
window.addEventListener('resize', this.$_resizeHandler)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.$_resizeHandler)
|
||||
},
|
||||
mounted() {
|
||||
const isMobile = this.$_isMobile()
|
||||
if (isMobile) {
|
||||
store.dispatch('app/toggleDevice', 'mobile')
|
||||
store.dispatch('app/closeSideBar', { withoutAnimation: true })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// use $_ for mixins properties
|
||||
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
|
||||
$_isMobile() {
|
||||
const rect = body.getBoundingClientRect()
|
||||
return rect.width - 1 < WIDTH
|
||||
},
|
||||
$_resizeHandler() {
|
||||
if (!document.hidden) {
|
||||
const isMobile = this.$_isMobile()
|
||||
store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
|
||||
|
||||
if (isMobile) {
|
||||
store.dispatch('app/closeSideBar', { withoutAnimation: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<el-form ref="loginForm" :model="loginForm" class="login-form" autocomplete="on" label-position="left">
|
||||
|
||||
<div class="title-container">
|
||||
<h3 class="title"> {{title}} </h3>
|
||||
</div>
|
||||
|
||||
<el-form-item prop="username">
|
||||
<span class="svg-container">
|
||||
<svg-icon icon-class="user" />
|
||||
</span>
|
||||
<el-input
|
||||
ref="username"
|
||||
v-model="loginForm.username"
|
||||
placeholder="Username"
|
||||
name="username"
|
||||
type="text"
|
||||
tabindex="1"
|
||||
auto-complete="on"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-tooltip v-model:visible="capsTooltip" content="Caps lock is On" placement="right" manual>
|
||||
<el-form-item prop="password">
|
||||
<span class="svg-container">
|
||||
<svg-icon icon-class="password" />
|
||||
</span>
|
||||
<el-input
|
||||
:key="passwordType"
|
||||
ref="password"
|
||||
v-model="loginForm.password"
|
||||
:type="passwordType"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
tabindex="2"
|
||||
auto-complete="on"
|
||||
@keyup="checkCapslock"
|
||||
@blur="capsTooltip = false"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
<span class="show-pwd" @click="showPwd">
|
||||
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
|
||||
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.prevent="handleLogin">{{ $t('login') }}</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { check, login } from '@/api/permission'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loginForm: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
loading: false,
|
||||
capsTooltip: false,
|
||||
passwordType: 'password',
|
||||
title: ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
document.title = this.docTitle
|
||||
check().then((res) => {
|
||||
if (res.code === 201) {
|
||||
this.$router.replace('/register').catch()
|
||||
} else {
|
||||
this.title = res.data.title
|
||||
document.title = this.title
|
||||
this.$store.commit('SET_TITLE', this.title)
|
||||
}
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
...mapState(['docTitle'])
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.password.focus()
|
||||
},
|
||||
methods: {
|
||||
showPwd() {
|
||||
if (this.passwordType === 'password') {
|
||||
this.passwordType = ''
|
||||
} else {
|
||||
this.passwordType = 'password'
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.$refs.password.focus()
|
||||
})
|
||||
},
|
||||
checkCapslock(e) {
|
||||
const { key } = e
|
||||
this.capsTooltip = key && key.length === 1 && (key >= 'A' && key <= 'Z')
|
||||
},
|
||||
async handleLogin() {
|
||||
if (this.loginForm.username === '' || this.loginForm.password === '') {
|
||||
this.$message.error(this.$t('inputNotNull'))
|
||||
return
|
||||
}
|
||||
const loginInfo = Object.assign({}, this.loginForm)
|
||||
loginInfo.password = CryptoJS.SHA224(this.loginForm.password).toString()
|
||||
const data = await login(loginInfo)
|
||||
const token = data.token
|
||||
let isAdmin = false
|
||||
if (this.loginForm.username === 'admin') {
|
||||
isAdmin = true
|
||||
}
|
||||
this.$store.commit('SET_ADMIN', isAdmin)
|
||||
this.$store.commit('LOGIN_IN', token)
|
||||
this.$router.replace('/').catch()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
$bg:#283443;
|
||||
$light_gray:#fff;
|
||||
$cursor: #fff;
|
||||
|
||||
/* reset element-ui css */
|
||||
.login-container {
|
||||
.el-input {
|
||||
height: 47px;
|
||||
width: 92%;
|
||||
position: static;
|
||||
|
||||
.el-input__wrapper {
|
||||
padding:0;
|
||||
box-shadow:none;
|
||||
}
|
||||
|
||||
input {
|
||||
background: $bg;
|
||||
border: 0px;
|
||||
-webkit-appearance: none;
|
||||
border-radius: 0px;
|
||||
padding: 12px 5px 12px 15px;
|
||||
color: $light_gray;
|
||||
height: 47px;
|
||||
caret-color: $cursor;
|
||||
|
||||
&:-webkit-autofill {
|
||||
box-shadow: 0 0 0px 1000px $bg inset !important;
|
||||
-webkit-text-fill-color: $cursor !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
color: #454545;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$bg:#2d3a4b;
|
||||
$dark_gray:#889aa4;
|
||||
$light_gray:#eee;
|
||||
|
||||
.login-container {
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
background-color: $bg;
|
||||
overflow: hidden;
|
||||
|
||||
.login-form {
|
||||
position: relative;
|
||||
width: 520px;
|
||||
max-width: 100%;
|
||||
padding: 160px 35px 0;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.svg-container {
|
||||
padding: 6px 5px 6px 15px;
|
||||
color: $dark_gray;
|
||||
vertical-align: middle;
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
position: relative;
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
color: $light_gray;
|
||||
margin: 0px auto 40px auto;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.show-pwd {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 7px;
|
||||
font-size: 16px;
|
||||
color: $dark_gray;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="register-container">
|
||||
<el-form class="register-form" :model="form" :rules="registerRules" ref="form" label-position="left">
|
||||
<div class="title-container">
|
||||
<h3 class="title">{{ $t('setupPass') }}</h3>
|
||||
</div>
|
||||
<el-form-item prop="password1">
|
||||
<span class="svg-container">
|
||||
<svg-icon icon-class="password" />
|
||||
</span>
|
||||
<el-input name="password1" :type="pwdType" v-model="form.password1" :placeholder="$t('inputPass')" show-password/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password2">
|
||||
<span class="svg-container">
|
||||
<svg-icon icon-class="password"></svg-icon>
|
||||
</span>
|
||||
<el-input name="password2" :type="pwdType" @keyup.enter="register" v-model="form.password2" :placeholder="$t('inputPassAgain')" show-password/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" style="width:100%;" :loading="loading" @click.prevent="register">
|
||||
{{ $t('register') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { register, check } from '@/api/permission'
|
||||
import CryptoJS from 'crypto-js'
|
||||
export default {
|
||||
data() {
|
||||
const validatePass = (rule, value, callback) => {
|
||||
if (value.length < 5) {
|
||||
callback(new Error(this.$t('passLessError')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
return {
|
||||
form: {
|
||||
password1: '',
|
||||
password2: ''
|
||||
},
|
||||
registerRules: {
|
||||
password1: [
|
||||
{ required: true, trigger: 'blur', validator: validatePass }
|
||||
],
|
||||
password2: [
|
||||
{ required: true, trigger: 'blur', validator: validatePass }
|
||||
]
|
||||
},
|
||||
loading: false,
|
||||
pwdType: 'password'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['docTitle'])
|
||||
},
|
||||
created() {
|
||||
document.title = this.docTitle
|
||||
check().then((res) => {
|
||||
if (res.code === 200) {
|
||||
this.$store.commit('LOGIN_OUT')
|
||||
this.$router.replace('/login').catch()
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
async register() {
|
||||
const formData = new FormData()
|
||||
if (this.form.password1 !== this.form.password2) {
|
||||
this.$message.error(this.$t('passDifferentError'))
|
||||
return
|
||||
} else {
|
||||
formData.set('password', CryptoJS.SHA224(this.form.password1).toString())
|
||||
}
|
||||
await register(formData)
|
||||
this.$router.replace('/login').catch()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* 修复input 背景不协调 和光标变色 */
|
||||
/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
|
||||
|
||||
$bg:#283443;
|
||||
$light_gray:#fff;
|
||||
$cursor: #fff;
|
||||
|
||||
/* reset element-ui css */
|
||||
.register-container {
|
||||
.el-input {
|
||||
height: 47px;
|
||||
width: 92%;
|
||||
position: static;
|
||||
|
||||
.el-input__wrapper {
|
||||
padding:0;
|
||||
box-shadow:none;
|
||||
}
|
||||
|
||||
.el-input__suffix {
|
||||
background: $bg;
|
||||
}
|
||||
|
||||
input {
|
||||
background: $bg;
|
||||
border: 0px;
|
||||
-webkit-appearance: none;
|
||||
border-radius: 0px;
|
||||
padding: 12px 5px 12px 15px;
|
||||
color: $light_gray;
|
||||
height: 47px;
|
||||
caret-color: $cursor;
|
||||
|
||||
&:-webkit-autofill {
|
||||
box-shadow: 0 0 0px 1000px $bg inset !important;
|
||||
-webkit-text-fill-color: $cursor !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
color: #454545;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$bg:#2d3a4b;
|
||||
$dark_gray:#889aa4;
|
||||
$light_gray:#eee;
|
||||
|
||||
.register-container {
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
background-color: $bg;
|
||||
overflow: hidden;
|
||||
|
||||
.register-form {
|
||||
position: relative;
|
||||
width: 520px;
|
||||
max-width: 100%;
|
||||
padding: 160px 35px 0;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.svg-container {
|
||||
padding: 6px 5px 6px 15px;
|
||||
color: $dark_gray;
|
||||
vertical-align: middle;
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
position: relative;
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
color: $light_gray;
|
||||
margin: 0px auto 40px auto;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,356 @@
|
||||
<template>
|
||||
<div :style="mainStyle">
|
||||
<el-form :inline="true" style="margin-top: 15px">
|
||||
<el-form-item>
|
||||
<el-button-group>
|
||||
<el-button type="primary" :icon="Refresh" @click="update()">{{ textShow($t('update')) }}</el-button>
|
||||
<el-button type="primary" :icon="VideoPlay" @click="start()">{{ textShow($t('start')) }}</el-button>
|
||||
<el-button type="primary" :icon="VideoPause" @click="stop()">{{ textShow($t('stop')) }}</el-button>
|
||||
<el-button type="primary" :icon="RefreshRight" @click="restart()">{{ textShow($t('restart')) }}</el-button>
|
||||
</el-button-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('type')">
|
||||
<el-select v-model="type" :placeholder="$t('choice')" filterable @change="trojanSwitch()" style="width: 110px;">
|
||||
<el-option
|
||||
v-for="item in typeOptions"
|
||||
:key="item.label"
|
||||
:label="item.label"
|
||||
:value="item.label">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('loglevel')">
|
||||
<el-select v-model="loglevel" :placeholder="$t('choice')" filterable @change="setLoglevel()" style="width: 110px;">
|
||||
<el-option
|
||||
v-for="item in loglevelOptions"
|
||||
:key="item.label"
|
||||
:label="item.label"
|
||||
:value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('line')">
|
||||
<el-select v-model="line" :placeholder="$t('choice')" filterable @change="getLog()" style="width: 110px;">
|
||||
<el-option
|
||||
v-for="item in logLineOptions"
|
||||
:key="item.label"
|
||||
:label="item.label"
|
||||
:value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('latest')">
|
||||
<el-switch v-model="isFollow"></el-switch>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<textarea id="logshow" readonly="readonly" class="el-textarea__inner"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from '@/store/index.js'
|
||||
import { mapState } from 'vuex'
|
||||
import { version } from '@/api/common'
|
||||
import { Refresh, VideoPlay, VideoPause, RefreshRight } from '@element-plus/icons-vue'
|
||||
import { start, stop, restart, update, getLoglevel, setLoglevel, trojanSwitch } from '@/api/trojan'
|
||||
export default {
|
||||
setup() {
|
||||
return {
|
||||
Refresh,
|
||||
VideoPlay,
|
||||
VideoPause,
|
||||
RefreshRight
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timer: null,
|
||||
isFollow: true,
|
||||
ws: null,
|
||||
scrollHeight: 0,
|
||||
fontSize: 13,
|
||||
mainStyle: {
|
||||
height: 0
|
||||
},
|
||||
typeOptions: [
|
||||
{
|
||||
label: 'trojan'
|
||||
},
|
||||
{
|
||||
label: 'trojan-go'
|
||||
}
|
||||
],
|
||||
loglevelOptions: [
|
||||
{
|
||||
label: 'ALL',
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
label: 'INFO',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: 'WARN',
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
label: 'ERROR',
|
||||
value: 3
|
||||
},
|
||||
{
|
||||
label: 'FATAL',
|
||||
value: 4
|
||||
},
|
||||
{
|
||||
label: 'OFF',
|
||||
value: 5
|
||||
}
|
||||
],
|
||||
logLineOptions: [
|
||||
{
|
||||
label: '100',
|
||||
value: 100
|
||||
},
|
||||
{
|
||||
label: '300',
|
||||
value: 300
|
||||
},
|
||||
{
|
||||
label: '1000',
|
||||
value: 1000
|
||||
},
|
||||
{
|
||||
label: '5000',
|
||||
value: 5000
|
||||
},
|
||||
{
|
||||
label: this.$t('all'),
|
||||
value: -1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$store.commit('SET_NOERROR', true)
|
||||
this.mainStyle.height = (document.body.clientHeight - 85) + 'px'
|
||||
window.onresize = () => {
|
||||
this.mainStyle.height = (document.body.clientHeight - 85) + 'px'
|
||||
}
|
||||
const textarea = document.getElementById('logshow')
|
||||
// 监听这个dom的scroll事件
|
||||
textarea.addEventListener('scroll', () => {
|
||||
if (textarea.scrollHeight > textarea.clientHeight && textarea.scrollTop < this.scrollHeight) {
|
||||
this.isFollow = false
|
||||
} else {
|
||||
this.isFollow = true
|
||||
}
|
||||
}, true)
|
||||
this.getLog()
|
||||
this.getLoglevel()
|
||||
this.getTrojanType()
|
||||
},
|
||||
computed: {
|
||||
...mapState(['line', 'loglevel', 'type', 'dialogWidth']),
|
||||
line: {
|
||||
get() {
|
||||
return this.$store.state.line
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('SET_LINE', val)
|
||||
}
|
||||
},
|
||||
loglevel: {
|
||||
get() {
|
||||
return this.$store.state.loglevel
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('SET_LOGLEVEL', val)
|
||||
}
|
||||
},
|
||||
type: {
|
||||
get() {
|
||||
return this.$store.state.type
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('SET_TYPE', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
this.$store.commit('SET_NOERROR', false)
|
||||
clearInterval(this.timer)
|
||||
this.timer = null
|
||||
this.ws.close()
|
||||
},
|
||||
methods: {
|
||||
textShow(text) {
|
||||
if (this.dialogWidth === '80%') {
|
||||
return ''
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
},
|
||||
async setLoglevel() {
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.set('level', this.loglevel)
|
||||
const result = await setLoglevel(formData)
|
||||
if (result.Msg === 'success') {
|
||||
this.$message({
|
||||
message: this.$t('trojan.loglevelSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.getLog()
|
||||
}
|
||||
},
|
||||
async getLoglevel() {
|
||||
const result = await getLoglevel()
|
||||
if (result.Msg === 'success') {
|
||||
this.loglevel = result.Data.loglevel
|
||||
} else {
|
||||
this.$message.error(result.Msg)
|
||||
}
|
||||
},
|
||||
async getTrojanType() {
|
||||
const result = await version()
|
||||
if (result.Msg === 'success') {
|
||||
this.type = result.Data.trojanType
|
||||
} else {
|
||||
this.$message.error(result.Msg)
|
||||
}
|
||||
},
|
||||
async start() {
|
||||
try {
|
||||
const result = await start()
|
||||
if (result.Msg === 'success') {
|
||||
this.$message({
|
||||
message: this.$t('trojan.startSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.getLog()
|
||||
}
|
||||
},
|
||||
async stop() {
|
||||
const result = await stop()
|
||||
if (result.Msg === 'success') {
|
||||
this.$message({
|
||||
message: this.$t('trojan.stopSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
},
|
||||
async restart() {
|
||||
try {
|
||||
const result = await restart()
|
||||
if (result.Msg === 'success') {
|
||||
this.$message({
|
||||
message: this.$t('trojan.restartSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.getLog()
|
||||
}
|
||||
},
|
||||
async update() {
|
||||
try {
|
||||
const result = await update()
|
||||
if (result.Msg === 'success') {
|
||||
this.$message({
|
||||
message: this.$t('trojan.updateSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.getLog()
|
||||
}
|
||||
},
|
||||
async trojanSwitch() {
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.set('type', this.type)
|
||||
const result = await trojanSwitch(formData)
|
||||
if (result.Msg === 'success') {
|
||||
this.$message({
|
||||
message: this.$t('trojan.switchSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
this.$message.error(result.Msg)
|
||||
}
|
||||
} catch (e) {
|
||||
this.getLog()
|
||||
this.$message({
|
||||
message: this.$t('trojan.switchSuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
},
|
||||
getLog() {
|
||||
this.isFollow = true
|
||||
if (this.ws != null) {
|
||||
this.ws.close()
|
||||
clearInterval(this.timer)
|
||||
this.timer = null
|
||||
}
|
||||
const textarea = document.getElementById('logshow')
|
||||
textarea.innerText = ''
|
||||
const prefix = process.env.NODE_ENV === 'production' ? '' : '/ws'
|
||||
const protocol = document.location.protocol === 'http:' ? 'ws' : 'wss'
|
||||
this.ws = new WebSocket(`${protocol}://${location.host}${prefix}/trojan/log?line=${this.line}&token=${store.state.UserToken}`)
|
||||
this.ws.onopen = function () {
|
||||
console.log('ws connected!')
|
||||
}
|
||||
this.ws.onmessage = function(e) {
|
||||
textarea.append(e.data)
|
||||
}
|
||||
this.ws.onerror = function(e) {
|
||||
console.log('ws error: ' + e)
|
||||
}
|
||||
this.ws.onclose = function() {
|
||||
console.log('ws closed')
|
||||
}
|
||||
textarea.scrollTop = textarea.scrollHeight
|
||||
this.scrollHeight = textarea.scrollTop
|
||||
this.timer = setInterval(() => {
|
||||
if (this.isFollow) {
|
||||
textarea.scrollTop = textarea.scrollHeight
|
||||
this.scrollHeight = textarea.scrollTop
|
||||
}
|
||||
}, 1000)
|
||||
// detect available wheel event
|
||||
// 各个厂商的高版本浏览器都支持"wheel"
|
||||
// Webkit 和 IE一定支持"mousewheel"
|
||||
// "DOMMouseScroll" 用于低版本的firefox
|
||||
const wheelSupport = 'onwheel' in document.createElement('div') ? 'wheel' : document.onmousewheel !== undefined ? 'mousewheel' : 'DOMMouseScroll'
|
||||
const logshow = document.getElementById('logshow')
|
||||
logshow.addEventListener(wheelSupport, (e) => {
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
if (e.deltaY < 0) {
|
||||
logshow.style.fontSize = ++this.fontSize + 'px'
|
||||
} else {
|
||||
logshow.style.fontSize = --this.fontSize + 'px'
|
||||
}
|
||||
}
|
||||
}, { passive: false })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#logshow {
|
||||
width: 100%;
|
||||
height: 92%;
|
||||
padding: 10px;
|
||||
background-color: black;
|
||||
color:white;
|
||||
font-size: 13px;
|
||||
cursor: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,630 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form :inline="true" label-width="80px">
|
||||
<el-form-item>
|
||||
<el-button-group>
|
||||
<el-button type="primary" :icon="Refresh" @click="refresh()">{{ textShow($t('refresh')) }}</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="commonType=2;userInfo.username='';userInfo.password='';userVisible=true" v-if="isAdmin">{{ textShow($t('add')) }}</el-button>
|
||||
<el-button type="primary" :icon="RefreshLeft" @click="copySelection=multipleSelection;patchButton=true;commonType=1;confirmVisible=true" v-if="isAdmin">{{ textShow($t('user.reset')) }}</el-button>
|
||||
<el-button type="primary" :icon="Scissor" @click="copySelection=multipleSelection;patchButton=true;quotaVisible=true" v-if="isAdmin">{{ textShow($t('user.limitData')) }}</el-button>
|
||||
<el-button type="danger" :icon="Delete" @click="copySelection=multipleSelection;patchButton=true;commonType=0;confirmVisible=true" v-if="isAdmin">{{ textShow($t('delete')) }}</el-button>
|
||||
</el-button-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table
|
||||
:data="dataList.filter(data => !search || data.Username.toLowerCase().includes(search.toLowerCase()))" :height="clientHeight" @selection-change="handleSelectionChange" class="tableShow">
|
||||
<el-table-column
|
||||
type="selection"
|
||||
width="55" v-if="isAdmin">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('username')"
|
||||
prop="Username"
|
||||
>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('password')"
|
||||
:formatter="passwordFormatter">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('user.upload')"
|
||||
:formatter="uploadFormatter" :sort-method="uploadSort" sortable>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('user.download')"
|
||||
:formatter="downloadFormatter" :sort-method="downloadSort" sortable>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('user.total')"
|
||||
:formatter="totalFormatter" :sort-method="totalSort" sortable>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('user.quota')"
|
||||
:formatter="quotaFormatter">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('user.expiryDate')">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.ExpiryDate === ''">{{ $t('user.unlimit') }}</div>
|
||||
<el-popover trigger="hover" placement="top" v-else>
|
||||
<p>{{ $t('user.remaining') }}: {{ calculateDay(scope.row.ExpiryDate) }}</p>
|
||||
<template #reference>
|
||||
<div class="name-wrapper">
|
||||
<el-tag>{{ scope.row.ExpiryDate === '' ? $t('user.unlimit') : scope.row.ExpiryDate }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
width="170"
|
||||
align="center">
|
||||
<template #header>
|
||||
<el-input
|
||||
v-model="search"
|
||||
:placeholder="$t('user.search')" v-if="isAdmin"/>
|
||||
<div v-if="!isAdmin">{{ $t('user.operate') }}</div>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-dropdown v-if="isAdmin">
|
||||
<el-button type="primary" link style="margin-top:2.6px">
|
||||
{{ $t('edit') }}
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="userItem=scope.row; patchButton=false; quotaVisible=true">{{ $t('user.limitData') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="userItem=scope.row; commonType=1; patchButton=false; confirmVisible=true">{{ $t('user.reset') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="userItem=scope.row; handelEditUser()">{{ $t('user.modifyUser') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="userItem=scope.row; expiryShow=$t('user.setExpire'); expiryVisible=true" v-if="scope.row.ExpiryDate === ''">{{ $t('user.setExpire') }}</el-dropdown-item>
|
||||
<div v-else>
|
||||
<el-dropdown-item @click="userItem=scope.row; expiryShow=$t('user.editExpire'); expiryVisible=true">{{ $t('user.editExpire') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="userItem=scope.row; cancelUserExpire()">{{ $t('user.cancelExpire') }}</el-dropdown-item>
|
||||
</div>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dropdown style="margin-left:5px">
|
||||
<el-button type="primary" link style="margin-top:2.6px">
|
||||
{{ $t('share') }}
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="userItem=scope.row;commonType=4;handleShare()">{{ $t('user.trojanShareLink') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="userItem=scope.row;commonType=5;handleShare()">{{ $t('user.clashShareLink') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="userItem=scope.row;handleClash()">{{ $t('user.importClash') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button style="margin-left:5px;" v-if="isAdmin" type="primary" link
|
||||
@click="userItem=scope.row;commonType=0;patchButton=false;confirmVisible=true"
|
||||
>{{ $t('delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-dialog :title="commonTitle" v-model="userVisible" :width="dialogWidth">
|
||||
<el-input type="text" v-model="userInfo.username" :placeholder="$root.$t('user.inputUsername')" @keyup.enter="commonType === 2? handleAddUser(): handleUpdateUser()"/>
|
||||
<el-input type="text" v-model="userInfo.password" :placeholder="$root.$t('user.inputPassword')" @keyup.enter="commonType === 2? handleAddUser(): handleUpdateUser()"/>
|
||||
<template #footer>
|
||||
<el-button @click="userVisible = false">{{ $root.$t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="commonType === 2? handleAddUser(): handleUpdateUser()">{{ $root.$t('ok') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog :title="commonTitle" v-model="confirmVisible" :width="dialogWidth">
|
||||
{{ editUser }}
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="confirmVisible = false;copySelection=[];">{{ $root.$t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="confirmVisible = false; patchButton ? handlePatchOpera(): handleOpera()">{{ $root.$t('ok') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog :title="quotaText" v-model="quotaVisible" :width="dialogWidth">
|
||||
{{ editUser }}
|
||||
<el-divider v-if="editUser != ''"></el-divider>
|
||||
<el-tooltip effect="dark" :content="$t('user.meanUnlimit')" placement="top">
|
||||
<el-input-number v-model="quota" :min="-1" :precision="0"></el-input-number>
|
||||
</el-tooltip>
|
||||
<el-select v-model="quotaUnit" :placeholder="$t('choice')" style="margin-left: 5px; width:80px">
|
||||
<el-option
|
||||
v-for="item in quotaOptions"
|
||||
:key="item.value"
|
||||
:label="item.value"
|
||||
:value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="quotaVisible=false">{{ $root.$t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="quotaVisible=false; handleSetQuota()">{{ $root.$t('ok') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog :title="commonTitle" v-model="qrcodeVisible" :width="dialogWidth" @close="closeQRCode">
|
||||
<div id="qrcode" ref="qrcode" class="qrcodeCenter"></div>
|
||||
<p class="qrcodeCenter"> {{ shareLink }} </p>
|
||||
</el-dialog>
|
||||
<el-dialog :title="expiryShow" v-model="expiryVisible" :width="dialogWidth">
|
||||
<el-form>
|
||||
<el-form-item :label="$t('user.preset')">
|
||||
<el-select v-model="useDays" :placeholder="$t('choice')" filterable style="width: 130px;">
|
||||
<el-option
|
||||
v-for="item in expiryDateOptions"
|
||||
:key="item.label"
|
||||
:label="item.label"
|
||||
:value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('user.days')">
|
||||
<el-input-number v-model="useDays" :min=0></el-input-number>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="expiryVisible = false">{{ $root.$t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="expiryVisible=false; setUserExpire()">{{ $root.$t('ok') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { userList, addUser, delUser, updateUser, setExpire, cancelExpire } from '@/api/user'
|
||||
import { Refresh, Plus, RefreshLeft, Scissor, Delete } from '@element-plus/icons-vue'
|
||||
import { setQuota, cleanData } from '@/api/data'
|
||||
import { setDomain, restart } from '@/api/trojan'
|
||||
import { readablizeBytes, isValidIP, base64Encode, base64Decode } from '@/utils/common'
|
||||
import { mapState } from 'vuex'
|
||||
import * as QRCode from 'easyqrcodejs'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
return {
|
||||
Refresh,
|
||||
Plus,
|
||||
RefreshLeft,
|
||||
Scissor,
|
||||
Delete
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
domain: '',
|
||||
port: 0,
|
||||
shareLink: '',
|
||||
dataList: [],
|
||||
multipleSelection: [],
|
||||
copySelection: [],
|
||||
clientHeight: 0,
|
||||
userVisible: false,
|
||||
confirmVisible: false,
|
||||
quotaVisible: false,
|
||||
qrcodeVisible: false,
|
||||
expiryVisible: false,
|
||||
expiryShow: '',
|
||||
patchButton: false,
|
||||
// 确认框类型: 0 删除, 1 重置流量, 2 新增用户, 3 修改用户, 4 trojan链接, 5 clash链接
|
||||
commonType: 0,
|
||||
userItem: null,
|
||||
quota: -1,
|
||||
quotaUnit: 'MB',
|
||||
useDays: 7,
|
||||
quotaOptions: [
|
||||
{
|
||||
value: 'MB'
|
||||
},
|
||||
{
|
||||
value: 'GB'
|
||||
}
|
||||
],
|
||||
expiryDateOptions: [
|
||||
{
|
||||
label: this.$t('user.week'),
|
||||
value: 7
|
||||
},
|
||||
{
|
||||
label: this.$t('user.month'),
|
||||
value: 30
|
||||
},
|
||||
{
|
||||
label: this.$t('user.season'),
|
||||
value: 90
|
||||
},
|
||||
{
|
||||
label: this.$t('user.halfYear'),
|
||||
value: 183
|
||||
},
|
||||
{
|
||||
label: this.$t('user.year'),
|
||||
value: 365
|
||||
}
|
||||
],
|
||||
userInfo: {
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['dialogWidth', 'isAdmin']),
|
||||
commonTitle: function() {
|
||||
let text = ''
|
||||
if (this.commonType === 4) {
|
||||
text = this.$t('user.trojanShareLink')
|
||||
} else if (this.commonType === 5) {
|
||||
text = this.$t('user.clashShareLink')
|
||||
} else if (this.commonType === 2) {
|
||||
text = this.$t('user.addUser')
|
||||
} else if (this.commonType === 3) {
|
||||
text = this.$t('user.modifyUser2') + this.userItem.Username + this.$t('user.userpass')
|
||||
} else if (this.commonType === 0) {
|
||||
if (this.patchButton) {
|
||||
text = this.$t('user.patchDelUser')
|
||||
} else if (this.userItem !== null) {
|
||||
text = this.$t('user.delUser') + this.userItem.Username + ' ?'
|
||||
}
|
||||
} else if (this.commonType === 1) {
|
||||
if (this.patchButton) {
|
||||
text = this.$t('user.patchReset')
|
||||
} else if (this.userItem !== null) {
|
||||
text = this.$t('user.resetUser') + this.userItem.Username + this.$t('user.data')
|
||||
}
|
||||
}
|
||||
return text
|
||||
},
|
||||
editUser: function() {
|
||||
if (this.patchButton) {
|
||||
let result = ''
|
||||
for (let i = 0; i < this.copySelection.length; i++) {
|
||||
result += ', ' + this.copySelection[i].Username
|
||||
}
|
||||
return result.substring(1)
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
quotaText: function() {
|
||||
if (this.patchButton) {
|
||||
return `${this.$t('user.patchLimitUser')}`
|
||||
} else {
|
||||
if (this.userItem !== null) {
|
||||
return `${this.$t('user.limitUser')} ${this.userItem.Username} ${this.$t('user.data')}`
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.refresh()
|
||||
this.clientHeight = document.body.clientHeight - 120
|
||||
},
|
||||
mounted() {
|
||||
window.onresize = () => {
|
||||
this.clientHeight = document.body.clientHeight - 120
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSelectionChange(val) {
|
||||
this.multipleSelection = val
|
||||
},
|
||||
passwordFormatter(row) {
|
||||
return base64Decode(row.Password)
|
||||
},
|
||||
quotaFormatter(row) {
|
||||
return row.Quota === -1 ? this.$t('user.unlimit') : readablizeBytes(row.Quota)
|
||||
},
|
||||
expiryFormatter(row) {
|
||||
return row.ExpiryDate === '' ? this.$t('user.unlimit') : row.ExpiryDate
|
||||
},
|
||||
uploadFormatter(row) {
|
||||
return readablizeBytes(row.Upload)
|
||||
},
|
||||
downloadFormatter(row) {
|
||||
return readablizeBytes(row.Download)
|
||||
},
|
||||
totalFormatter(row) {
|
||||
return readablizeBytes(row.Download + row.Upload)
|
||||
},
|
||||
uploadSort(a, b) {
|
||||
return a.Upload - b.Upload
|
||||
},
|
||||
downloadSort(a, b) {
|
||||
return a.Download - b.Download
|
||||
},
|
||||
totalSort(a, b) {
|
||||
return (a.Download + a.Upload) - (b.Download + b.Upload)
|
||||
},
|
||||
handleShare() {
|
||||
if (this.commonType === 4) {
|
||||
let remark = encodeURIComponent(`${this.domain}:${this.port}`)
|
||||
this.shareLink = `trojan://${base64Decode(this.userItem.Password)}@${this.domain}:${this.port}#${remark}`
|
||||
} else if (this.commonType === 5) {
|
||||
let userInfo = base64Encode(`{"user": "${this.userItem.Username}", "pass": "${base64Decode(this.userItem.Password)}"}`)
|
||||
let protocol = `${window.location.hostname}` === '127.0.0.1' ? 'https:': `${window.location.protocol}`
|
||||
this.shareLink = `${protocol}//${this.domain}:${this.port}/trojan/user/subscribe?token=${userInfo}`
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
// eslint-disable-next-line
|
||||
new QRCode(this.$refs.qrcode, {
|
||||
width: 200,
|
||||
height: 200,
|
||||
text: this.shareLink
|
||||
})
|
||||
})
|
||||
this.qrcodeVisible = true
|
||||
},
|
||||
handleClash() {
|
||||
let userInfo = base64Encode(`{"user": "${this.userItem.Username}", "pass": "${base64Decode(this.userItem.Password)}"}`)
|
||||
let url = `${window.location.origin}/trojan/user/subscribe?token=${userInfo}`
|
||||
window.location.href = `clash://install-config?url=${url}`
|
||||
},
|
||||
handelEditUser() {
|
||||
this.userInfo.username = this.userItem.Username
|
||||
this.userInfo.password = base64Decode(this.userItem.Password)
|
||||
this.commonType = 3
|
||||
this.userVisible = true
|
||||
},
|
||||
textShow(text) {
|
||||
if (this.dialogWidth === '80%') {
|
||||
return ''
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
},
|
||||
closeQRCode() {
|
||||
this.$refs.qrcode.innerHTML = ''
|
||||
},
|
||||
calculateDay(day) {
|
||||
return dayjs(day).diff(dayjs(dayjs().format('YYYY-MM-DD')), 'day')
|
||||
},
|
||||
async setUserExpire() {
|
||||
const formData = new FormData()
|
||||
formData.set('id', this.userItem.ID)
|
||||
formData.set('useDays', this.useDays)
|
||||
const result = await setExpire(formData)
|
||||
if (result.Msg === 'success') {
|
||||
this.$message({
|
||||
message: `${this.$t('user.setExpireSuccess')}`,
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
this.$message.error(result.Msg)
|
||||
}
|
||||
this.userItem = null
|
||||
this.refresh()
|
||||
},
|
||||
async cancelUserExpire() {
|
||||
const result = await cancelExpire(this.userItem.ID)
|
||||
if (result.Msg === 'success') {
|
||||
this.$message({
|
||||
message: `${this.$t('user.cancelExpireSuccess')}`,
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
this.$message.error(result.Msg)
|
||||
}
|
||||
this.userItem = null
|
||||
this.refresh()
|
||||
},
|
||||
async requestQuota() {
|
||||
const formData = new FormData()
|
||||
formData.set('id', this.userItem.ID)
|
||||
formData.set('quota', this.quota)
|
||||
const result = await setQuota(formData)
|
||||
if (result.Msg === 'success') {
|
||||
this.$message({
|
||||
message: `${this.$t('user.limitUser2')} ${this.userItem.Username} ${this.$t('user.limitSuccess')}`,
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
this.$message.error(result.Msg)
|
||||
}
|
||||
},
|
||||
async handleSetQuota() {
|
||||
if (this.quota !== -1) {
|
||||
if (this.quotaUnit === 'MB') {
|
||||
this.quota = this.quota * 1024 * 1024
|
||||
} else if (this.quotaUnit === 'GB') {
|
||||
this.quota = this.quota * 1024 * 1024 * 1024
|
||||
}
|
||||
}
|
||||
if (this.patchButton) {
|
||||
for (let i = 0; i < this.copySelection.length; i++) {
|
||||
this.userItem = this.copySelection[i]
|
||||
await this.requestQuota()
|
||||
}
|
||||
} else {
|
||||
await this.requestQuota()
|
||||
}
|
||||
this.userItem = null
|
||||
this.quota = 0
|
||||
this.refresh()
|
||||
},
|
||||
async handlePatchOpera() {
|
||||
let successText = ''
|
||||
let result = null
|
||||
if (this.commonType === 0) {
|
||||
this.$store.commit('SET_NOERROR', true)
|
||||
}
|
||||
for (let i = 0; i < this.copySelection.length; i++) {
|
||||
this.userItem = this.copySelection[i]
|
||||
if (this.commonType === 0) {
|
||||
try {
|
||||
result = await delUser(this.userItem.ID)
|
||||
} catch {
|
||||
result = { Msg: 'success' }
|
||||
}
|
||||
successText = `${this.$t('user.delUser1')}${this.userItem.Username}${this.$t('user.success')}`
|
||||
} else if (this.commonType === 1) {
|
||||
result = await cleanData(this.userItem.ID)
|
||||
successText = `${this.$t('user.resetUser1')}${this.userItem.Username}${this.$t('user.success')}`
|
||||
}
|
||||
if (result.Msg === 'success') {
|
||||
this.$message({
|
||||
message: successText,
|
||||
type: 'success'
|
||||
})
|
||||
this.userItem = null
|
||||
} else {
|
||||
this.$message.error(result.Msg)
|
||||
}
|
||||
}
|
||||
if (this.commonType === 0) {
|
||||
this.$store.commit('SET_NOERROR', false)
|
||||
}
|
||||
this.refresh()
|
||||
},
|
||||
async handleOpera() {
|
||||
let successText = ''
|
||||
let result = null
|
||||
if (this.commonType === 0) {
|
||||
this.$store.commit('SET_NOERROR', true)
|
||||
try {
|
||||
result = await delUser(this.userItem.ID)
|
||||
} catch {
|
||||
result = { Msg: 'success' }
|
||||
}
|
||||
this.$store.commit('SET_NOERROR', false)
|
||||
successText = `${this.$t('user.delUser1')}${this.userItem.Username}${this.$t('user.success')}`
|
||||
} else if (this.commonType === 1) {
|
||||
result = await cleanData(this.userItem.ID)
|
||||
successText = `${this.$t('user.resetUser1')}${this.userItem.Username}${this.$t('user.success')}`
|
||||
}
|
||||
if (result.Msg === 'success') {
|
||||
this.$message({
|
||||
message: successText,
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
this.$message.error(result.Msg)
|
||||
}
|
||||
this.refresh()
|
||||
},
|
||||
async handleUpdateUser() {
|
||||
if (this.userInfo.username === '' || this.userInfo.password === '') {
|
||||
this.$message.error(this.$t('inputNotNull'))
|
||||
return
|
||||
}
|
||||
if (this.userInfo.username === 'admin') {
|
||||
this.$message.error(this.$t('user.limitAdmin'))
|
||||
return
|
||||
}
|
||||
const formData = new FormData()
|
||||
formData.set('id', this.userItem.ID)
|
||||
formData.set('username', this.userInfo.username)
|
||||
try {
|
||||
formData.set('password', base64Encode(this.userInfo.password))
|
||||
} catch (e) {
|
||||
this.$message.error(this.$t('user.passLimit'))
|
||||
return
|
||||
}
|
||||
const result = await updateUser(formData)
|
||||
this.userVisible = false
|
||||
this.refresh()
|
||||
if (result.Msg === 'success') {
|
||||
this.$message({
|
||||
message: `${this.$t('user.modifyUser2')}${this.userInfo.username}${this.$t('user.success')}`,
|
||||
type: 'success'
|
||||
})
|
||||
this.$store.commit('SET_NOERROR', true)
|
||||
try {
|
||||
await restart().catch()
|
||||
} catch(e) {
|
||||
this.$store.commit('SET_NOERROR', false)
|
||||
}
|
||||
} else {
|
||||
this.$message.error(result.Msg)
|
||||
}
|
||||
},
|
||||
async handleAddUser() {
|
||||
if (this.userInfo.username === '' || this.userInfo.password === '') {
|
||||
this.$message.error(this.$t('inputNotNull'))
|
||||
return
|
||||
}
|
||||
if (this.userInfo.username === 'admin') {
|
||||
this.$message.error(this.$t('user.limitAdmin'))
|
||||
return
|
||||
}
|
||||
const formData = new FormData()
|
||||
formData.set('username', this.userInfo.username)
|
||||
try {
|
||||
formData.set('password', base64Encode(this.userInfo.password))
|
||||
} catch (e) {
|
||||
this.$message.error(this.$t('user.passLimit'))
|
||||
return
|
||||
}
|
||||
const result = await addUser(formData)
|
||||
if (result.Msg === 'success') {
|
||||
this.$message({
|
||||
message: `${this.$t('user.addUser2')}${this.userInfo.username}${this.$t('user.success')}`,
|
||||
type: 'success'
|
||||
})
|
||||
this.userInfo.username = ''
|
||||
this.userInfo.password = ''
|
||||
} else {
|
||||
this.$message.error(result.Msg)
|
||||
}
|
||||
this.userVisible = false
|
||||
this.refresh()
|
||||
},
|
||||
refresh() {
|
||||
this.dataList = []
|
||||
this.domain = ''
|
||||
this.getUserList()
|
||||
},
|
||||
async getUserList() {
|
||||
const result = await userList()
|
||||
if (result.Msg === 'success') {
|
||||
if (result.Data.userList === null) {
|
||||
this.dataList = []
|
||||
} else {
|
||||
this.dataList = result.Data.userList
|
||||
}
|
||||
this.port = result.Data.port
|
||||
if (result.Data.domain !== '') {
|
||||
this.domain = result.Data.domain
|
||||
} else {
|
||||
const hostname = window.location.hostname
|
||||
if (!isValidIP(hostname)) {
|
||||
this.domain = hostname
|
||||
const formData = new FormData()
|
||||
formData.set('domain', this.domain)
|
||||
this.$store.commit('SET_NPROGRESS', false)
|
||||
this.$store.commit('SET_NOERROR', true)
|
||||
try {
|
||||
await setDomain(formData)
|
||||
} catch (e) {
|
||||
this.$store.commit('SET_NPROGRESS', true)
|
||||
this.$store.commit('SET_NOERROR', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.$message.error(result.Msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.qrcodeCenter {
|
||||
margin: 0 auto;
|
||||
width: 200px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.tableShow {
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
::-webkit-scrollbar {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||