This commit is contained in:
chermack
2026-07-26 00:09:57 +08:00
commit 059be96536
134 changed files with 19678 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
<template>
<router-view />
</template>
+25
View File
@@ -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')
}
+17
View File
@@ -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)
}
+22
View File
@@ -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')
}
+33
View File
@@ -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)
}
+25
View File
@@ -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}`)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

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>
+4
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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

+122
View File
@@ -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',
},
},
};
+122
View File
@@ -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: '取消',
},
},
};
+127
View File
@@ -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'
}
}
+42
View File
@@ -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
+147
View File
@@ -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: '连接服务器失败'
}
}
+42
View File
@@ -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')
+95
View File
@@ -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
+19
View File
@@ -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)
})
})
}
}
+3
View File
@@ -0,0 +1,3 @@
export default {
sidebar: state => state.app.sidebar
}
+15
View File
@@ -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
})
+5
View File
@@ -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
}
+36
View File
@@ -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)
}
}
+16
View File
@@ -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
}
+66
View File
@@ -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;
}
+28
View File
@@ -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%;
}
+171
View File
@@ -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;
}
}
+209
View File
@@ -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;
}
+34
View File
@@ -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)
}
+84
View File
@@ -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,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') 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 })
}
}
}
}
}
+218
View File
@@ -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>
+630
View File
@@ -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>