🐛 修复mfa2登录首字符为0时无法输入的bug&前端支持激活plus

This commit is contained in:
chaos-zhu 2024-12-22 17:45:22 +08:00
parent 9df142ccde
commit cbc6fa02ac
11 changed files with 500 additions and 144 deletions

View File

@ -3,11 +3,13 @@ const axios = require('axios')
const speakeasy = require('speakeasy')
const QRCode = require('qrcode')
const version = require('../../package.json').version
const getLicenseInfo = require('../utils/get-plus')
const { plusServer1, plusServer2 } = require('../utils/plus-server')
const { sendNoticeAsync } = require('../utils/notify')
const { RSADecryptAsync, AESEncryptAsync, SHA1Encrypt } = require('../utils/encrypt')
const { getNetIPInfo } = require('../utils/tools')
const { KeyDB, LogDB, PlusDB } = require('../utils/db-class')
const keyDB = new KeyDB().getInstance()
const logDB = new LogDB().getInstance()
const plusDB = new PlusDB().getInstance()
@ -175,6 +177,7 @@ const getPlusInfo = async ({ res }) => {
}
const getPlusDiscount = async ({ res } = {}) => {
if (process.env.EXEC_ENV === 'local') return res.success({ discount: false })
const servers = [plusServer1, plusServer2]
for (const server of servers) {
try {
@ -195,6 +198,18 @@ const getPlusDiscount = async ({ res } = {}) => {
}
}
const getPlusConf = async ({ res }) => {
const { key } = await plusDB.findOneAsync({}) || {}
res.success({ data: key || '', msg: 'success' })
}
const updatePlusKey = async ({ res, request }) => {
const { body: { key } } = request
const { success, msg } = await getLicenseInfo(key)
if (!success) return res.fail({ msg })
res.success({ msg: 'success' })
}
module.exports = {
login,
getpublicKey,
@ -205,5 +220,7 @@ module.exports = {
enableMFA2,
disableMFA2,
getPlusInfo,
getPlusDiscount
getPlusDiscount,
getPlusConf,
updatePlusKey
}

View File

@ -1,6 +1,6 @@
const { getSSHList, addSSH, updateSSH, removeSSH, getCommand, decryptPrivateKey } = require('../controller/ssh')
const { getHostList, addHost, updateHost, batchUpdateHost, removeHost, importHost } = require('../controller/host')
const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMFA2Code, enableMFA2, disableMFA2, getPlusInfo, getPlusDiscount } = require('../controller/user')
const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMFA2Code, enableMFA2, disableMFA2, getPlusInfo, getPlusDiscount, getPlusConf, updatePlusKey } = require('../controller/user')
const { getNotifyConfig, updateNotifyConfig, getNotifyList, updateNotifyList } = require('../controller/notify')
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
const { getScriptList, getLocalScriptList, addScript, updateScriptList, removeScript, batchRemoveScript, importScript } = require('../controller/scripts')
@ -121,6 +121,16 @@ const user = [
method: 'get',
path: '/plus-discount',
controller: getPlusDiscount
},
{
method: 'get',
path: '/plus-conf',
controller: getPlusConf
},
{
method: 'post',
path: '/plus-conf',
controller: updatePlusKey
}
]
const notify = [

View File

@ -3,10 +3,16 @@ const { getLocalNetIP } = require('./tools')
const { AESEncryptAsync } = require('./encrypt')
const version = require('../../package.json').version
const { plusServer1, plusServer2 } = require('./plus-server')
const { PlusDB } = require('./db-class')
const plusDB = new PlusDB().getInstance()
async function getLicenseInfo() {
let key = process.env.PLUS_KEY
if (!key || typeof key !== 'string' || key.length < 20) return
async function getLicenseInfo(key = '') {
const { key: plusKey } = await plusDB.findOneAsync({}) || {}
// console.log('plusKey: ', plusKey)
// console.log('key: ', key)
// console.log('process.env.PLUS_KEY: ', process.env.PLUS_KEY)
key = key || plusKey || process.env.PLUS_KEY
if (!key || key.length < 16) return { success: false, msg: 'Invalid Plus Key' }
let ip = ''
if (global.serverIp && (Date.now() - global.getServerIpLastTime) / 1000 / 60 < 60) {
ip = global.serverIp
@ -20,7 +26,7 @@ async function getLicenseInfo() {
if (!ip) {
consola.error('activate plus failed: get public ip failed')
global.serverIp = ''
return
return { success: false, msg: 'get public ip failed' }
}
try {
let response
@ -64,9 +70,7 @@ async function getLicenseInfo() {
let { decryptKey, expiryDate, usedIPCount, maxIPs, usedIPs } = data
decryptKey = await AESEncryptAsync(decryptKey)
consola.success('activate plus success')
const { PlusDB } = require('./db-class')
const plusData = { key, decryptKey, expiryDate, usedIPCount, maxIPs, usedIPs }
const plusDB = new PlusDB().getInstance()
let count = await plusDB.countAsync({})
if (count === 0) {
await plusDB.insertAsync(plusData)
@ -74,14 +78,16 @@ async function getLicenseInfo() {
await plusDB.removeAsync({}, { multi: true })
await plusDB.insertAsync(plusData)
}
return { success: true, msg: '激活成功' }
}
consola.error('activate plus failed: ', data)
return { success: false, msg: '激活失败' }
} catch (error) {
consola.error(`activate plus failed: ${ error.message || error.errMsg?.message }`)
if (error.clear) {
const { PlusDB } = require('./db-class')
const plusDB = new PlusDB().getInstance()
await plusDB.removeAsync({}, { multi: true })
}
return { success: false, msg: error.message || error.errMsg?.message }
}
}

View File

@ -126,5 +126,11 @@ export default {
},
getEasynodeVersion() {
return axios({ url: '/version', method: 'get' })
},
getPlusConf() {
return axios({ url: '/plus-conf', method: 'get' })
},
updatePlusKey(data) {
return axios({ url: '/plus-conf', method: 'post', data })
}
}

View File

@ -0,0 +1,206 @@
<template>
<div class="comparison-container">
<!-- 基础版卡片 -->
<el-card class="comparison-card basic-card">
<template #header>
<div class="card-header">
<span class="title">基础功能(免费)</span>
<el-tag size="small">Basic</el-tag>
</div>
</template>
<div class="feature-list">
<div v-for="(feature, index) in basicFeatures" :key="index" class="feature-item">
<el-icon>
<Check />
</el-icon>
<span>{{ feature }}</span>
</div>
</div>
</el-card>
<!-- Plus版卡片 -->
<el-card class="comparison-card plus-card">
<template #header>
<div class="card-header">
<div>
<span class="title">Plus专属功能</span>
<span class="link" style="margin-right: 15px;" @click="() => plusTipsShow = true">Plus说明</span>
</div>
<el-tag type="success" size="small">PLUS</el-tag>
</div>
</template>
<div class="feature-list">
<div v-for="(feature, index) in plusFeatures" :key="index" class="feature-item plus">
<el-icon color="#67c23a">
<Check />
</el-icon>
<span>{{ feature }}</span>
</div>
</div>
</el-card>
<el-dialog
v-model="plusTipsShow"
title="Plus说明"
top="20vh"
width="30%"
:append-to-body="false"
:close-on-click-modal="false"
>
<div class="about_content">
<p style="line-height: 2;letter-spacing: 1px;">
<!-- <strong style="color: #F56C6C;font-weight: 600;">PLUS说明:</strong><br> -->
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<strong>EasyNode</strong>最初是一个简单的Web终端工具随着用户群的不断扩大功能需求也日益增长为了实现大家的功能需求我投入了大量的业余时间进行开发和维护
一直在为爱发电渐渐的也没了开发的动力
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;为了项目的可持续发展<strong>后续</strong>版本开始推出<strong>PLUS</strong>版本具体特性鼠标悬浮右上角PLUS图标查看后续特性功能开发也会优先在<strong>PLUS</strong>版本中实现但即使不升级到<strong>PLUS</strong>也不会影响到<strong>EasyNode</strong>的基础功能使用注意:
暂不支持纯内网用户激活PLUS功能
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="text-decoration: underline;">
为了感谢前期赞赏过的用户, <strong>PLUS</strong>功能正式发布前所有进行过赞赏的用户无论金额大小均可联系作者TG: <a
class="link"
href="https://t.me/chaoszhu"
target="_blank"
>@chaoszhu</a> 凭打赏记录免费获取永久<strong>PLUS</strong>授权码
</span>
</p>
<div class="about_footer">
<el-button type="info" @click="plusTipsShow = false">关闭</el-button>
<el-button type="primary" @click="handlePlusSupport">购买Plus Key</el-button>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Check } from '@element-plus/icons-vue'
import { handlePlusSupport } from '@/utils'
const plusTipsShow = ref(false)
//
const basicFeatures = [
'服务器管理',
'服务器导入导出',
'服务器分组',
'凭据管理',
'脚本库',
'批量连接',
'批量指令',
'通知方式(有限制)',
]
// Plus
const plusFeatures = [
'包含基础版全部功能',
'服务器跳板机功能,支持任意数量服务器的连续跳板',
'批量修改实例配置(优化版)',
'脚本库批量导出导入',
'凭据管理支持解密带密码保护的密钥',
'通知方式无限制',
'本地socket断开自动重连',
'功能需求更高开发优先级',
]
</script>
<style scoped lang="scss">
.comparison-container {
display: flex;
gap: 20px;
padding-bottom: 20px;
max-width: 1200px;
}
.comparison-card {
flex: 1;
min-width: 300px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.title {
font-size: 18px;
font-weight: bold;
margin-right: 10px;
}
.link {
text-decoration: underline;
}
.feature-list {
// padding: 10px 0;
line-height: 1.3;
}
.feature-item {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 0;
/* border-bottom: 1px solid #eee; */
border-bottom: 1px solid var(--el-border-color);
}
.feature-item:last-child {
border-bottom: none;
}
.feature-item .el-icon {
color: #409EFF;
}
.plus-card .feature-item.plus {
color: #67c23a;
}
.about_content {
h1 {
font-size: 24px;
font-weight: 600;
margin: 20px 0;
}
p {
line-height: 1.8;
margin: 12px 0;
font-size: 14px;
}
.link {
color: #409EFF;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.conspicuous {
color: #F56C6C;
font-weight: 500;
}
.about_footer {
display: flex;
justify-content: center;
margin-top: 20px;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.comparison-container {
flex-direction: column;
}
.comparison-card {
width: 100%;
}
}
</style>

View File

@ -37,107 +37,26 @@
</el-dropdown-menu>
</template>
</el-dropdown>
<el-popover placement="left" :width="320" trigger="hover">
<template #reference>
<div class="plus_icon_wrapper">
<img
class="plus_icon"
src="@/assets/plus.png"
alt="PLUS"
:style="{ filter: isPlusActive ? 'grayscale(0%)' : 'grayscale(100%)' }"
>
<img
v-if="!isPlusActive && discount"
class="discount_badge"
src="@/assets/discount.png"
alt="Discount"
>
</div>
</template>
<template #default>
<div class="plus_content_wrap">
<!-- Plus 激活状态信息 -->
<div v-if="isPlusActive" class="plus_status">
<div class="status_header">
<el-icon>
<CircleCheckFilled />
</el-icon>
<span>Plus专属功能已激活</span>
</div>
<div class="status_info">
<div class="info_item">
<span class="label">到期时间:</span>
<span class="value holder">{{ plusInfo.expiryDate }}</span>
</div>
<div class="info_item">
<span class="label">授权IP数:</span>
<span class="value">{{ plusInfo.maxIPs }}</span>
</div>
<div class="info_item">
<span class="label">已授权IP数:</span>
<span class="value">{{ plusInfo.usedIPCount }}</span>
</div>
<div class="info_item ip_list">
<span class="label">已授权IP:</span>
<div class="ip_tags">
<el-tag
v-for="ip in plusInfo.usedIPs"
:key="ip"
size="small"
class="ip_tag"
>
{{ ip }}
</el-tag>
</div>
</div>
</div>
</div>
<div class="plus_benefits" :class="{ active: isPlusActive }" @click="handlePlus">
<span v-if="!isPlusActive" class="support_btn" @click="handlePlusSupport">激活PLUS</span>
<div v-if="!isPlusActive && discount" class="discount_content" @click="handlePlusSupport">
<el-tag type="warning" effect="dark">
<el-icon><Promotion /></el-icon>
<span>{{ discountContent || '限时优惠活动' }}</span>
</el-tag>
</div>
<div class="benefits_header">
<el-icon>
<StarFilled />
</el-icon>
<span>Plus功能介绍</span>
</div>
<div class="current_benefits">
<div v-for="plusFeature in plusFeatures" :key="plusFeature" class="benefit_item">
<el-icon>
<Star />
</el-icon>
<span>{{ plusFeature }}</span>
</div>
</div>
<!-- <div class="coming_soon">
<div class="soon_header">开发中的PLUS功能</div>
<div class="current_benefits">
<div v-for="soonFeature in soonFeatures" :key="soonFeature" class="benefit_item">
<el-icon>
<Star />
</el-icon>
<span>{{ soonFeature }}</span>
</div>
</div>
</div> -->
</div>
</div>
</template>
</el-popover>
<div class="plus_icon_wrapper" @click="gotoPlusPage">
<img
class="plus_icon"
src="@/assets/plus.png"
alt="PLUS"
:style="{ filter: isPlusActive ? 'grayscale(0%)' : 'grayscale(100%)' }"
>
<img
v-if="!isPlusActive && discount"
class="discount_badge"
src="@/assets/discount.png"
alt="Discount"
>
</div>
</div>
<el-dialog
v-model="visible"
title="版本更新"
top="10vh"
top="20vh"
width="30%"
:append-to-body="false"
:close-on-click-modal="false"
@ -163,20 +82,8 @@
<p>
TG更新通知频道<a class="link" href="https://t.me/easynode_notify" target="_blank">https://t.me/easynode_notify</a>
</p>
<p style="line-height: 2;letter-spacing: 1px;">
<strong style="color: #F56C6C;font-weight: 600;">PLUS说明:</strong><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<strong>EasyNode</strong>最初是一个简单的Web终端工具随着用户群的不断扩大功能需求也日益增长为了实现大家的功能需求我投入了大量的业余时间进行开发和维护
一直在为爱发电渐渐的也没了开发的动力
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;为了项目的可持续发展<strong>3.0.0</strong>版本开始推出了<strong>PLUS</strong>版本具体特性鼠标悬浮右上角PLUS图标查看后续特性功能开发也会优先在<strong>PLUS</strong>版本中实现但即使不升级到<strong>PLUS</strong>也不会影响到<strong>EasyNode</strong>的基础功能使用注意: 暂不支持纯内网用户激活PLUS功能
<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="text-decoration: underline;">
为了感谢前期赞赏过的用户, <strong>PLUS</strong>功能正式发布前所有进行过赞赏的用户无论金额大小均可联系作者TG: <a class="link" href="https://t.me/chaoszhu" target="_blank">@chaoszhu</a> 凭打赏记录免费获取永久<strong>PLUS</strong>授权码
</span>
</p>
<div class="about_footer">
<el-button type="info" @click="visible = false">关闭</el-button>
<el-button v-if="!isPlusActive" type="primary" @click="handlePlusSupport">激活PLUS</el-button>
</div>
</div>
</el-dialog>
@ -198,12 +105,14 @@
<script setup>
import { ref, getCurrentInstance, computed, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import { User, Sunny, Moon, Fold, CircleCheckFilled, Star, StarFilled, Promotion } from '@element-plus/icons-vue'
import packageJson from '../../package.json'
import MenuList from './menuList.vue'
import { handlePlusSupport } from '@/utils'
const { proxy: { $router, $store, $api, $message } } = getCurrentInstance()
const router = useRouter()
const visible = ref(false)
const checkVersionErr = ref(false)
const currentVersion = ref(`v${ packageJson.version }`)
@ -212,20 +121,6 @@ const menuCollapse = ref(false)
const discount = ref(false)
const discountContent = ref('')
const plusFeatures = [
'跳板机功能,拯救被墙实例与龟速终端输入',
'本地socket断开自动重连,无需手动重新连接',
'批量修改实例配置(优化版)',
'脚本库批量导出导入',
'凭据管理支持解密带密码保护的密钥',
'提出的功能需求享有更高的开发优先级',
]
// const soonFeatures = [
// '',
// '',
// '',
// ]
const isNew = computed(() => latestVersion.value && latestVersion.value !== currentVersion.value)
const user = computed(() => $store.user)
const title = computed(() => $store.title)
@ -249,8 +144,8 @@ const handleLogout = () => {
$router.push('/login')
}
const handlePlusSupport = () => {
window.open('https://en.221022.xyz/buy-plus', '_blank')
const gotoPlusPage = () => {
router.push('/setting?tabKey=plus')
}
async function checkLatestVersion() {

View File

@ -101,7 +101,7 @@ const useStore = defineStore({
this.$patch({ localScriptList })
},
async getPlusInfo() {
const { data: plusInfo } = await $api.getPlusInfo()
const { data: plusInfo = {} } = await $api.getPlusInfo()
if (plusInfo?.expiryDate) {
const isPlusActive = new Date(plusInfo.expiryDate) > new Date()
this.$patch({ isPlusActive })
@ -111,6 +111,9 @@ const useStore = defineStore({
}
plusInfo.expiryDate = dayjs(plusInfo.expiryDate).format('YYYY-MM-DD')
plusInfo.expiryDate?.startsWith('9999') && (plusInfo.expiryDate = '永久授权')
this.$patch({ plusInfo })
} else {
this.$patch({ isPlusActive: false })
}
this.$patch({ plusInfo })
},

View File

@ -149,3 +149,7 @@ export const isMobile = () => {
let userAgent = navigator.userAgent || navigator.vendor || window.opera
return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent)
}
export const handlePlusSupport = () => {
window.open('https://en.221022.xyz/buy-plus', '_blank')
}

View File

@ -46,7 +46,7 @@
</el-form-item>
<el-form-item prop="mfa2Token" label="MFA2验证码">
<el-input
v-model.trim.number="loginForm.mfa2Token"
v-model="loginForm.mfa2Token"
type="text"
placeholder="MFA2应用上的6位数字(未设置可忽略)"
autocomplete="off"
@ -103,7 +103,7 @@ const loginForm = reactive({
const rules = reactive({
loginName: { required: true, message: '需输入用户名', trigger: 'change' },
pwd: { required: true, message: '需输入密码', trigger: 'change' },
mfa2Token: { required: false, message: '需输入码', trigger: 'change' }
mfa2Token: { required: false, message: '需输入MFA2验证码', trigger: 'change' }
})
const handleLogin = () => {
@ -124,7 +124,7 @@ const handleLogin = () => {
if (ciphertext === -1) return $message.error({ message: '公钥加载失败', center: true })
loading.value = true
try {
let { data, msg } = await $api.login({ loginName, ciphertext, jwtExpires, mfa2Token })
let { data, msg } = await $api.login({ loginName, ciphertext, jwtExpires, mfa2Token: Number(mfa2Token) })
const { token } = data
$store.setJwtToken(token, expireEnum.ONE_SESSION === expireTime.value)
$store.setUser(loginName)

View File

@ -0,0 +1,181 @@
<template>
<el-form
ref="formRef"
class="plus-form"
:model="formData"
:rules="rules"
:hide-required-asterisk="true"
label-suffix=""
label-width="86px"
:show-message="false"
@submit.prevent
>
<el-form-item label="Plus Key" prop="key">
<el-input
v-model.trim="formData.key"
clearable
placeholder=""
autocomplete="off"
@keyup.enter.prevent="handleUpdate"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleUpdate">立即激活</el-button>
<el-button type="success" @click="handlePlusSupport">购买Plus</el-button>
</el-form-item>
</el-form>
<!-- Plus 激活状态信息 -->
<div v-if="isPlusActive" class="plus_status">
<div class="status_header">
<span>Plus专属功能已激活</span>
</div>
<div class="status_info">
<div class="info_item">
<span class="label">到期时间:</span>
<span class="value holder">{{ plusInfo.expiryDate }}</span>
</div>
<div class="info_item">
<span class="label">授权IP数:</span>
<span class="value">{{ plusInfo.maxIPs }}</span>
</div>
<div class="info_item">
<span class="label">已授权IP数:</span>
<span class="value">{{ plusInfo.usedIPCount }}</span>
</div>
<div class="info_item ip_list">
<span class="label">已授权IP:</span>
<div class="ip_tags">
<el-tag
v-for="ip in plusInfo.usedIPs"
:key="ip"
size="small"
class="ip_tag"
>
{{ ip }}
</el-tag>
</div>
</div>
</div>
</div>
<PlusTable />
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance, computed } from 'vue'
import { ElMessageBox } from 'element-plus'
import { handlePlusSupport } from '@/utils'
import PlusTable from '@/components/plus-table.vue'
const { proxy: { $api, $message, $store } } = getCurrentInstance()
const errCount = ref(Number(localStorage.getItem('plusErrCount') || 0))
const loading = ref(false)
const formRef = ref(null)
const formData = reactive({
key: ''
})
const rules = reactive({
key: { required: true, message: '输入Plus Key', trigger: 'change' }
})
const plusInfo = computed(() => $store.plusInfo)
const isPlusActive = computed(() => $store.isPlusActive)
const handleUpdate = () => {
formRef.value.validate()
.then(async () => {
try {
loading.value = true
let { key } = formData
await $api.updatePlusKey({ key })
$message({ type: 'success', center: true, message: '激活成功,感谢支持' })
localStorage.setItem('plusErrCount', 0)
} catch (error) {
localStorage.setItem('plusErrCount', ++errCount.value)
if (errCount.value > 3) {
ElMessageBox.confirm(
'激活失败请确认key正确(20位不规则字符串)有疑问请tg联系@chaoszhu。',
'Warning',
{
showCancelButton : false,
confirmButtonText: '确认',
type: 'warning'
}
)
}
}
})
.finally(() => {
loading.value = false
$store.getPlusInfo()
})
}
const getPlusConf = async () => {
try {
loading.value = true
let { data } = await $api.getPlusConf()
formData.key = data
} catch (error) {
$message({ type: 'error', center: true, message: error.message })
} finally {
loading.value = false
}
}
onMounted(() => {
getPlusConf()
})
</script>
<style lang="scss" scoped>
.plus-form {
width: 500px;
}
.plus_status {
margin-bottom: 15px;
.status_header {
display: flex;
align-items: center;
color: #67c23a;
margin-bottom: 10px;
.el-icon {
margin-right: 5px;
}
}
.status_info {
.info_item {
display: flex;
margin: 5px 0;
font-size: 13px;
.label {
color: #909399;
width: 80px;
}
.holder {
color: #EED183;
}
&.ip_list {
flex-direction: column;
.ip_tags {
margin-top: 5px;
.ip_tag {
margin: 2px;
}
}
}
}
}
}
</style>

View File

@ -1,33 +1,61 @@
<template>
<div class="setting_container">
<el-tabs tab-position="top">
<el-tab-pane label="修改密码" lazy>
<el-tabs v-model="tabKey" tab-position="top">
<el-tab-pane label="修改密码" name="user">
<User />
</el-tab-pane>
<el-tab-pane label="登录日志">
<el-tab-pane label="登录日志" name="record" lazy>
<Record />
</el-tab-pane>
<el-tab-pane label="全局通知" lazy>
<el-tab-pane label="全局通知" name="notify">
<GlobalNotify />
</el-tab-pane>
<el-tab-pane label="通知配置" lazy>
<el-tab-pane label="通知配置" name="notify-config">
<NotifyConfig />
</el-tab-pane>
<el-tab-pane label="Plus激活" name="plus">
<UserPlus />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { watch, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import GlobalNotify from './components/global-notify.vue'
// import EmailList from './components/email-list.vue'
import Record from './components/record.vue'
import User from './components/user.vue'
import NotifyConfig from './components/notify-config.vue'
import UserPlus from './components/user-plus.vue'
// tabkey
const route = useRoute()
const router = useRouter()
const tabKey = computed({
get() {
return route.query.tabKey || 'user'
},
set(newVal) {
router.push({ query: { tabKey: newVal } })
}
})
watch(() => tabKey.value, (newVal) => {
router.push({ query: { tabKey: newVal } })
})
const handleTabClick = (tab) => {
router.push({ query: { tabKey: tab.props.name } })
}
</script>
<style lang="scss" scoped>
.setting_container {
height: 100%;
padding: 20px;
overflow: auto;
}
</style>