新增支持API添加用户,查询邮件

This commit is contained in:
eoao
2025-08-10 11:37:46 +08:00
parent 2437ff4599
commit 46127a8d67
38 changed files with 894 additions and 713 deletions

View File

@@ -1,3 +1,3 @@
NODE_ENV = 'remote'
VITE_APP_TITLE = '远程环境'
VITE_BASE_URL = 'https://mornglow.top/api'
VITE_BASE_URL = ''

View File

@@ -133,6 +133,7 @@ const en = {
loginSwitch: 'Sign in',
websiteSetting: 'Website',
websiteReg: 'Sign Up',
loginDomain: 'Sign-In Box Domain',
multipleEmail: 'Multiple Accounts',
multipleEmailDesc: 'Enable this feature to allow users to add multiple accounts.',
physicallyWipeData: 'Physically Wipe Data',
@@ -262,7 +263,7 @@ const en = {
rulesVerifyTitle: 'Trigger After {count} Daily Uses per IP',
botVerifyMsg: 'Please verify that you are human',
noticeTitle: 'Notice',
noticePopup: 'Sign-in Popup',
noticePopup: 'Sign-In Popup',
icon: 'Icon',
position: 'Position',
offset: 'Offset',
@@ -278,7 +279,9 @@ const en = {
popUp: 'Pop Up',
noRecipientTitle: 'No Recipient',
noRecipientDesc: 'Emails can be received even without a registered email address.',
preview: 'Preview'
preview: 'Preview',
help: 'Help',
document: 'Document'
}
export default en

View File

@@ -18,7 +18,7 @@ const zh = {
deleteUserBtn: '删除账户',
changePassword: '修改密码',
newPassword: '新的密码',
confirmPassword: '旧的密码',
confirmPassword: '确认密码',
add: '添加',
manage: '管理',
rename: '改名',
@@ -133,6 +133,7 @@ const zh = {
loginSwitch: '去登录',
websiteSetting: '网站设置',
websiteReg: '用户注册',
loginDomain: '登录框域名',
multipleEmail: '多号模式',
multipleEmailDesc: '开启后账号栏出现一个用户可以添加多个邮箱',
physicallyWipeData: '物理清空数据',
@@ -246,7 +247,7 @@ const zh = {
totalUserAccount: '{msg} 个',
sendBanned: '已禁用',
wrote: '来信',
support: '助',
support: '助',
supportDesc: '请我喝杯奶茶',
featDesc: '功能说明',
emailInterception: '邮件拦截',
@@ -278,7 +279,9 @@ const zh = {
popUp: '弹出',
noRecipientTitle: '无人收件',
noRecipientDesc: '即使没有注册的邮箱也能收到邮件',
preview: '预览'
preview: '预览',
help: '帮助',
document: '项目文档'
}
export default zh
export default zh

View File

@@ -1,28 +1,33 @@
<template>
<div class="account-box">
<div class="head-opt" >
<Icon v-perm="'account:add'" class="icon add" icon="ion:add-outline" width="23" height="23" @click="add" />
<Icon class="icon refresh" icon="ion:reload" width="18" height="18" @click="refresh" />
<div class="head-opt">
<Icon v-perm="'account:add'" class="icon add" icon="ion:add-outline" width="23" height="23" @click="add"/>
<Icon class="icon refresh" icon="ion:reload" width="18" height="18" @click="refresh"/>
</div>
<el-scrollbar class="scrollbar">
<div v-infinite-scroll="getAccountList" :infinite-scroll-distance="600" :infinite-scroll-immediate="false">
<el-card class="item" :class="itemBg(item.accountId)" v-for="item in accounts" :key="item.accountId" @click="changeAccount(item)">
<div class="account" >
<div v-infinite-scroll="getAccountList" :infinite-scroll-distance="600" :infinite-scroll-immediate="false">
<el-card class="item" :class="itemBg(item.accountId)" v-for="item in accounts" :key="item.accountId"
@click="changeAccount(item)">
<div class="account">
{{ item.email }}
</div>
<div class="opt">
<div class="send-email" @click.stop>
<Icon icon="eva:email-fill" width="22" height="22" color="#fccb1a" />
<Icon icon="eva:email-fill" width="22" height="22" color="#fccb1a"/>
</div>
<div class="settings" @click.stop>
<Icon icon="fluent-color:clipboard-24" width="22" height="22" @click.stop="copyAccount(item.email)"/>
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399" v-if="showNullSetting(item)" />
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399"
v-if="showNullSetting(item)"/>
<el-dropdown v-else>
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399" />
<template #dropdown >
<el-dropdown-menu >
<el-dropdown-item v-if="hasPerm('email:send')" @click="openSetName(item)">{{$t('rename')}}</el-dropdown-item>
<el-dropdown-item v-if="item.accountId !== userStore.user.accountId && hasPerm('account:delete')" @click="remove(item)">{{$t('delete')}}</el-dropdown-item>
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399"/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="hasPerm('email:send')" @click="openSetName(item)">{{ $t('rename') }}
</el-dropdown-item>
<el-dropdown-item v-if="item.accountId !== userStore.user.accountId && hasPerm('account:delete')"
@click="remove(item)">{{ $t('delete') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -32,13 +37,13 @@
<!-- Initial Loading Skeleton -->
<template v-if="loading">
<el-skeleton v-for="i in 3" :key="i" animated>
<el-skeleton v-for="i in 3" :key="i" animated>
<template #template>
<el-card class="item">
<el-skeleton-item variant="p" style="width: 70%; height: 20px; margin-bottom: 20px" />
<el-skeleton-item variant="p" style="width: 70%; height: 20px; margin-bottom: 20px"/>
<div style="display: flex; justify-content: space-between">
<el-skeleton-item variant="text" style="width: 20px" />
<el-skeleton-item variant="text" style="width: 20px" />
<el-skeleton-item variant="text" style="width: 20px"/>
<el-skeleton-item variant="text" style="width: 20px"/>
</div>
</el-card>
</template>
@@ -50,10 +55,10 @@
<el-skeleton animated>
<template #template>
<el-card class="item">
<el-skeleton-item variant="p" style="width: 70%; height: 20px; margin-bottom: 20px" />
<el-skeleton-item variant="p" style="width: 70%; height: 20px; margin-bottom: 20px"/>
<div style="display: flex; justify-content: space-between">
<el-skeleton-item variant="text" style="width: 20px" />
<el-skeleton-item variant="text" style="width: 20px" />
<el-skeleton-item variant="text" style="width: 20px"/>
<el-skeleton-item variant="text" style="width: 20px"/>
</div>
</el-card>
</template>
@@ -61,43 +66,43 @@
</template>
<div class="noLoading" v-if="noLoading && accounts.length > 0">
<div>{{$t('noMoreData')}}</div>
<div>{{ $t('noMoreData') }}</div>
</div>
<div class="empty" v-if="noLoading && accounts.length === 0">
<el-empty :description="$t('noMessagesFound')" />
<el-empty :description="$t('noMessagesFound')"/>
</div>
</div>
</el-scrollbar>
<el-dialog v-model="showAdd" :title="$t('addAccount')" >
<div class="container">
<el-input v-model="addForm.email" ref="addRef" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
<template #append>
<div @click.stop="openSelect">
<el-select
ref="mySelect"
v-model="addForm.suffix"
:placeholder="$t('select')"
class="select"
>
<el-option
v-for="item in domainList"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<div style="color: #333">
<span >{{addForm.suffix}}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20" />
</div>
<el-dialog v-model="showAdd" :title="$t('addAccount')">
<div class="container">
<el-input v-model="addForm.email" ref="addRef" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
<template #append>
<div @click.stop="openSelect">
<el-select
ref="mySelect"
v-model="addForm.suffix"
:placeholder="$t('select')"
class="select"
>
<el-option
v-for="item in domainList"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<div style="color: #333">
<span>{{ addForm.suffix }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
</template>
</el-input>
<el-button class="btn" type="primary" @click="submit" :loading="addLoading"
>{{$t('add')}}
</el-button>
</div>
</div>
</template>
</el-input>
<el-button class="btn" type="primary" @click="submit" :loading="addLoading"
>{{ $t('add') }}
</el-button>
</div>
<div
class="add-email-turnstile"
:class="verifyShow ? 'turnstile-show' : 'turnstile-hide'"
@@ -105,15 +110,15 @@
data-callback="onTurnstileSuccess"
data-error-callback="onTurnstileError"
>
<span style="font-size: 12px;color: #F56C6C" v-if="botJsError">人机验证模块加载失败,请刷新浏览器</span>
<span style="font-size: 12px;color: #F56C6C" v-if="botJsError">{{ $t('verifyModuleFailed') }}</span>
</div>
</el-dialog>
<el-dialog v-model="setNameShow" :title="$t('changeUserName')" >
<el-dialog v-model="setNameShow" :title="$t('changeUserName')">
<div class="container">
<el-input v-model="accountName" type="text" :placeholder="$t('username')" autocomplete="off">
</el-input>
<el-button class="btn" type="primary" @click="setName" :loading="setNameLoading"
>{{$t('save')}}
>{{ $t('save') }}
</el-button>
</div>
</el-dialog>
@@ -127,10 +132,10 @@ import {isEmail} from "@/utils/verify-utils.js";
import {useSettingStore} from "@/store/setting.js";
import {useAccountStore} from "@/store/account.js";
import {useUserStore} from "@/store/user.js";
import { hasPerm } from "@/perm/perm.js"
import {hasPerm} from "@/perm/perm.js"
import {useI18n} from "vue-i18n";
const { t } = useI18n();
const {t} = useI18n();
const userStore = useUserStore();
const accountStore = useAccountStore();
const settingStore = useSettingStore();
@@ -150,6 +155,7 @@ let account = null
let turnstileId = null
const botJsError = ref(false)
let verifyToken = ''
let verifyErrorCount = 0
const addForm = reactive({
email: '',
suffix: settingStore.domainList[0]
@@ -175,14 +181,20 @@ const openSelect = () => {
}
window.onTurnstileError = (e) => {
console.log('人机验加载失败')
nextTick(() => {
if (!turnstileId) {
turnstileId = window.turnstile.render('.register-turnstile')
} else {
window.turnstile.reset(turnstileId);
}
})
if (verifyErrorCount >= 4) {
return
}
verifyErrorCount++
console.warn('人机验加载失败', e)
setTimeout(() => {
nextTick(() => {
if (!turnstileId) {
turnstileId = window.turnstile.render('.add-email-turnstile')
} else {
window.turnstile.reset(turnstileId);
}
})
}, 1500)
};
window.onTurnstileSuccess = (token) => {
@@ -208,7 +220,7 @@ function setName() {
}
setNameLoading.value = true
accountSetName(account.accountId,name).then(() => {
accountSetName(account.accountId, name).then(() => {
account.name = name
setNameShow.value = false
@@ -221,12 +233,12 @@ function setName() {
type: "success",
plain: true
})
}).finally(()=> {
}).finally(() => {
setNameLoading.value = false
})
}
function openSetName (accountItem) {
function openSetName(accountItem) {
accountName.value = accountItem.name
account = accountItem
setNameShow.value = true
@@ -241,7 +253,7 @@ function itemBg(accountId) {
}
function remove(account) {
ElMessageBox.confirm(t('delConfirm',{msg: account.email}), {
ElMessageBox.confirm(t('delConfirm', {msg: account.email}), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
@@ -272,6 +284,7 @@ function refresh() {
accounts.splice(0, accounts.length)
getAccountList()
}
function changeAccount(account) {
accountStore.currentAccountId = account.accountId
accountStore.currentAccount = account
@@ -281,7 +294,7 @@ function add() {
showAdd.value = true
setTimeout(() => {
addRef.value.focus()
},100)
}, 100)
}
async function copyAccount(account) {
@@ -308,11 +321,11 @@ function getAccountList() {
if (accounts.length === 0) {
loading.value = true
}else {
} else {
followLoading.value = true
}
accountList(queryParams.accountId,queryParams.size).then(list => {
accountList(queryParams.accountId, queryParams.size).then(list => {
if (list.length < queryParams.size) {
noLoading.value = true
}
@@ -331,9 +344,9 @@ function getAccountList() {
}
function submit() {
function submit() {
if (!addForm.email){
if (!addForm.email) {
ElMessage({
message: t('emptyEmailMsg'),
type: "error",
@@ -342,7 +355,7 @@ function submit() {
return
}
if (!isEmail(addForm.email+addForm.suffix)) {
if (!isEmail(addForm.email + addForm.suffix)) {
ElMessage({
message: t('notEmailMsg'),
type: "error",
@@ -376,7 +389,7 @@ function submit() {
}
addLoading.value = true
accountAdd(addForm.email+addForm.suffix,verifyToken).then(account => {
accountAdd(addForm.email + addForm.suffix, verifyToken).then(account => {
addLoading.value = false
showAdd.value = false
addForm.email = ''
@@ -418,6 +431,7 @@ path[fill="#ffdda1"] {
background-color: #FFF;
height: 100%;
overflow: hidden;
.head-opt {
display: flex;
align-items: center;
@@ -425,9 +439,11 @@ path[fill="#ffdda1"] {
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
padding-left: 10px;
padding-right: 10px;
.icon{
.icon {
cursor: pointer;
}
.refresh {
margin-left: 10px;
}
@@ -440,6 +456,7 @@ path[fill="#ffdda1"] {
margin-left: 5px;
}
}
.scrollbar {
width: 100%;
height: calc(100% - 38px);
@@ -447,12 +464,14 @@ path[fill="#ffdda1"] {
@media (max-width: 767px) {
height: calc(100% - 98px);
}
.empty {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.noLoading {
display: flex;
justify-content: center;
@@ -461,6 +480,7 @@ path[fill="#ffdda1"] {
color: gray;
}
}
.btn {
width: 100%;
margin-top: 15px;
@@ -474,6 +494,7 @@ path[fill="#ffdda1"] {
margin-left: 10px;
margin-right: 10px;
cursor: pointer;
.account {
font-weight: 600;
margin-bottom: 20px;
@@ -488,11 +509,13 @@ path[fill="#ffdda1"] {
justify-content: space-between;
font-size: 12px;
color: #888;
.settings {
display: flex;
align-items: center;
gap: 10px;
}
.send-email {
display: flex;
align-items: center;
@@ -503,8 +526,9 @@ path[fill="#ffdda1"] {
padding: 0;
}
}
.item:first-child{
margin-top: 10px ;
.item:first-child {
margin-top: 10px;
}
.item-choose {
@@ -560,4 +584,4 @@ path[fill="#ffdda1"] {
position: fixed;
}
</style>
</style>

View File

@@ -1,5 +1,6 @@
import {createRouter, createWebHistory} from 'vue-router'
import {useUiStore} from "@/store/ui.js";
import {replace} from "lodash-es";
const routes = [
{
@@ -81,7 +82,7 @@ router.beforeEach(async (to, from, next) => {
if (!token && to.name !== 'login') {
return next({
name: 'login'
name: 'login',
})
}
@@ -89,6 +90,10 @@ router.beforeEach(async (to, from, next) => {
return next()
}
if (token && to.name === 'login') {
next(from.path)
}
next()
})

View File

@@ -1,14 +1,14 @@
<template>
<div v-if="analysisLoading" class="analysis-loading">
<loading />
<loading/>
</div>
<el-scrollbar v-else style="height: 100%;">
<el-scrollbar v-else style="height: 100%;">
<div class="analysis" :key="boxKey">
<div class="number">
<div class="number-item">
<div class="top">
<div class="left">
<div>{{$t('totalReceived')}}</div>
<div>{{ $t('totalReceived') }}</div>
<div>
<el-statistic :formatter="value => Math.round(value)" :value="receiveData"/>
</div>
@@ -20,14 +20,14 @@
</div>
</div>
<div class="delete-ratio">
<div>{{$t('active')}} <span class="normal">{{numberCount.normalReceiveTotal}}</span></div>
<div>{{$t('deleted')}} <span class="deleted">{{numberCount.delReceiveTotal}}</span></div>
<div>{{ $t('active') }} <span class="normal">{{ numberCount.normalReceiveTotal }}</span></div>
<div>{{ $t('deleted') }} <span class="deleted">{{ numberCount.delReceiveTotal }}</span></div>
</div>
</div>
<div class="number-item">
<div class="top">
<div class="left">
<div>{{$t('totalSent')}}</div>
<div>{{ $t('totalSent') }}</div>
<div>
<el-statistic :formatter="value => Math.round(value)" :value="sendData"/>
</div>
@@ -39,14 +39,14 @@
</div>
</div>
<div class="delete-ratio">
<div>{{$t('active')}} <span class="normal">{{numberCount.normalSendTotal}}</span></div>
<div>{{$t('deleted')}} <span class="deleted">{{numberCount.delSendTotal}}</span></div>
<div>{{ $t('active') }} <span class="normal">{{ numberCount.normalSendTotal }}</span></div>
<div>{{ $t('deleted') }} <span class="deleted">{{ numberCount.delSendTotal }}</span></div>
</div>
</div>
<div class="number-item">
<div class="top">
<div class="left">
<div>{{$t('totalMailboxes')}}</div>
<div>{{ $t('totalMailboxes') }}</div>
<div>
<el-statistic :formatter="value => Math.round(value)" :value="accountData"/>
</div>
@@ -58,14 +58,14 @@
</div>
</div>
<div class="delete-ratio">
<div>{{$t('active')}} <span class="normal">{{numberCount.normalAccountTotal}}</span></div>
<div>{{$t('deleted')}} <span class="deleted">{{numberCount.delAccountTotal}}</span></div>
<div>{{ $t('active') }} <span class="normal">{{ numberCount.normalAccountTotal }}</span></div>
<div>{{ $t('deleted') }} <span class="deleted">{{ numberCount.delAccountTotal }}</span></div>
</div>
</div>
<div class="number-item">
<div class="top">
<div class="left">
<div>{{$t('totalUsers')}}</div>
<div>{{ $t('totalUsers') }}</div>
<div>
<el-statistic :formatter="value => Math.round(value)" :value="userData"/>
</div>
@@ -77,19 +77,19 @@
</div>
</div>
<div class="delete-ratio">
<div>{{$t('active')}} <span class="normal">{{numberCount.normalUserTotal}}</span></div>
<div>{{$t('deleted')}} <span class="deleted">{{numberCount.delUserTotal}}</span></div>
<div>{{ $t('active') }} <span class="normal">{{ numberCount.normalUserTotal }}</span></div>
<div>{{ $t('deleted') }} <span class="deleted">{{ numberCount.delUserTotal }}</span></div>
</div>
</div>
</div>
<div class="picture">
<div class="picture-item">
<div class="title" style="display: flex;justify-content: space-between;">
<span>{{$t('emailSource')}}</span>
<span>{{ $t('emailSource') }}</span>
<span class="source-button" v-if="false">
<el-radio-group v-model="checkedSourceType" >
<el-radio-button label="发件人" value="sender" />
<el-radio-button label="邮箱" value="email" />
<el-radio-group v-model="checkedSourceType">
<el-radio-button label="发件人" value="sender"/>
<el-radio-button label="邮箱" value="email"/>
</el-radio-group>
</span>
</div>
@@ -98,7 +98,7 @@
</div>
</div>
<div class="picture-item">
<div class="title">{{$t('userGrowth')}}</div>
<div class="title">{{ $t('userGrowth') }}</div>
<div class="increase-line">
</div>
@@ -106,11 +106,11 @@
</div>
<div class="picture-cs">
<div class="picture-cs-item">
<div class="title">{{$t('emailGrowth')}}</div>
<div class="title">{{ $t('emailGrowth') }}</div>
<div class="email-column"></div>
</div>
<div class="picture-cs-item">
<div class="title">{{$t('sentToday')}}</div>
<div class="title">{{ $t('sentToday') }}</div>
<div class="send-count"></div>
</div>
</div>
@@ -129,13 +129,13 @@ import {useUiStore} from "@/store/ui.js";
import {debounce} from "lodash-es";
import loading from "@/components/loading/index.vue";
import {useRoute} from "vue-router";
import { useI18n } from 'vue-i18n';
import {useI18n} from 'vue-i18n';
defineOptions({
name: 'analysis'
})
const { t } = useI18n();
const {t} = useI18n();
const route = useRoute();
const uiStore = useUiStore()
const checkedSourceType = ref('sender')
@@ -218,13 +218,12 @@ onMounted(() => {
}
})
userLineData.xdata = data.userDayCount.map(item => dayjs(item.date).format("M.D"))
userLineData.xdata = data.userDayCount.map(item => dayjs(item.date).format("M.D"));
userLineData.sdata = data.userDayCount.map(item => item.total)
emailColumnData.daysData = data.emailDayCount.receiveDayCount.map(item => dayjs(item.date).format("M.D"))
emailColumnData.receiveData = data.emailDayCount.receiveDayCount.map(item => item.total)
emailColumnData.sendData = data.emailDayCount.sendDayCount.map(item => item.total)
daySendTotal = data.daySendTotal
analysisLoading.value = false
initPicture();
@@ -234,8 +233,8 @@ onMounted(() => {
})
function initPicture() {
if(route.name !== 'analysis') return
boxKey.value ++
if (route.name !== 'analysis') return
boxKey.value++
setTimeout(() => {
createSenderPie()
createIncreaseLine()
@@ -282,7 +281,7 @@ function setStyle() {
const measureCtx = document.createElement('canvas').getContext('2d');
measureCtx.font = '12px sans-serif';
function truncateTextByWidth(text,maxWidth = 140) {
function truncateTextByWidth(text, maxWidth = 140) {
let width = measureCtx.measureText(text).width;
if (width <= maxWidth) return text;
@@ -311,22 +310,22 @@ function createSenderPie() {
return `${params.marker} ${params.name} ${params.value} (${params.percent}%)`;
}
},
legend: {
type: 'scroll',
orient: 'vertical',
left: '10',
top: '20',
formatter: function (name) {
return truncateTextByWidth(name)
}
},
legend: {
type: 'scroll',
orient: 'vertical',
left: '10',
top: '20',
formatter: function (name) {
return truncateTextByWidth(name)
}
},
series: [
{
data: senderData.value,
name: '',
type: 'pie',
radius: ['40%', '65%'],
center: [ senderPieLeft, '45%'],
center: [senderPieLeft, '45%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 4,
@@ -378,7 +377,7 @@ function createIncreaseLine() {
formatter: function (params) {
let result = ''
params.forEach(item => {
result = `${item.marker} ${t('growthTotalUsers')} ${item.value}`;
result = `${item.marker} ${t('growthTotalUsers')} ${item.value}`;
});
return result;
},
@@ -438,7 +437,7 @@ function createIncreaseLine() {
},
boundaryGap: [0, 0.1],
max: (params) => {
if (params.max < 8 ) {
if (params.max < 8) {
return 10
}
},
@@ -493,6 +492,21 @@ function createIncreaseLine() {
]
};
increaseLine.setOption(option);
let max = increaseLine.getModel().getComponent('yAxis', 0).axis.scale.getExtent()[1];
let left = 35
if (max > 99) left = 42
if (max > 999) left = 51
if (max > 9999) left = 58
if (max > 99999) left = 66
increaseLine.setOption({
grid: {
left: left
}
});
}
function createEmailColumnChart() {
@@ -537,7 +551,7 @@ function createEmailColumnChart() {
},
yAxis: {
max: (params) => {
if (params.max < 8 ) {
if (params.max < 8) {
return 10
}
},
@@ -584,7 +598,7 @@ function createEmailColumnChart() {
}
function createSendGauge() {
if(sendGauge) {
if (sendGauge) {
sendGauge.dispose()
}
sendGauge = echarts.init(document.querySelector(".send-count"));
@@ -649,6 +663,7 @@ function createSendGauge() {
margin-top: 10px;
font-size: 28px;
}
.percentage-label {
display: block;
margin-top: 10px;
@@ -663,6 +678,7 @@ function createSendGauge() {
align-items: center;
justify-content: center;
}
.analysis {
height: 100%;
padding: 20px 20px 30px;
@@ -674,12 +690,14 @@ function createSendGauge() {
padding: 15px 15px 30px;
gap: 15px
}
.title {
margin-top: 10px;
margin-left: 15px;
font-size: 18px;
font-weight: 500;
}
.number {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
@@ -691,11 +709,13 @@ function createSendGauge() {
@media (max-width: 767px) {
grid-template-columns: 1fr;
}
.number-item {
background: var(--el-bg-color);
border-radius: 8px;
border: 1px solid var(--el-border-color);
padding: 21px 20px;
.top {
display: grid;
justify-content: space-between;
@@ -719,6 +739,7 @@ function createSendGauge() {
.right {
display: grid;
align-items: center;
.count-icon {
top: 3px;
position: relative;
@@ -740,6 +761,7 @@ function createSendGauge() {
justify-content: start;
gap: 20px;
padding-top: 5px;
.normal {
width: fit-content;
color: var(--el-color-success);
@@ -768,24 +790,29 @@ function createSendGauge() {
@media (max-width: 1024px) {
gap: 15px;
}
.picture-item {
background: #fff;
border-radius: 8px;
border: 1px solid var(--el-border-color);
.source-button {
padding-right: 15px;
display: flex;
align-items: start;
:deep(.el-radio-button__inner) {
padding: 6px 10px;
}
}
.sender-pie {
height: 350px;
@media (max-width: 767px) {
height: 200px;
}
}
.increase-line {
height: 350px;
@media (max-width: 767px) {
@@ -803,16 +830,19 @@ function createSendGauge() {
grid-template-columns: 1fr;
gap: 15px;
}
.picture-cs-item {
background: #fff;
border-radius: 8px;
border: 1px solid var(--el-border-color);
.send-count {
height: 350px;
@media (max-width: 767px) {
height: 320px;
}
}
.email-column {
height: 350px;
@media (max-width: 767px) {

View File

@@ -9,89 +9,96 @@
</div>
<div v-else :style="background"></div>
<div class="form-wrapper">
<div class="container">
<span class="form-title">{{settingStore.settings.title}}</span>
<span class="form-desc" v-if="show === 'login'">{{$t('loginTitle')}}</span>
<span class="form-desc" v-else>{{$t('regTitle')}}</span>
<div v-show="show === 'login'">
<el-input class="email-input" v-model="form.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
<template #append>
<div @click.stop="openSelect">
<el-select
v-if="show === 'login'"
ref="mySelect"
v-model="suffix"
:placeholder="$t('select')"
class="select"
>
<el-option
v-for="item in domainList"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<div style="color: #333">
<span>{{ suffix }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
<div class="container">
<span class="form-title">{{ settingStore.settings.title }}</span>
<span class="form-desc" v-if="show === 'login'">{{ $t('loginTitle') }}</span>
<span class="form-desc" v-else>{{ $t('regTitle') }}</span>
<div v-show="show === 'login'">
<el-input :class="settingStore.settings.loginDomain === 0 ? 'email-input' : ''" v-model="form.email"
type="text" :placeholder="$t('emailAccount')" autocomplete="off">
<template #append v-if="settingStore.settings.loginDomain === 0">
<div @click.stop="openSelect">
<el-select
v-if="show === 'login'"
ref="mySelect"
v-model="suffix"
:placeholder="$t('select')"
class="select"
>
<el-option
v-for="item in domainList"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<div style="color: #333">
<span>{{ suffix }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
</template>
</el-input>
<el-input v-model="form.password" :placeholder="$t('password')" type="password" autocomplete="off">
</el-input>
<el-button class="btn" type="primary" @click="submit" :loading="loginLoading"
>{{$t('loginBtn')}}
</el-button>
</div>
<div v-show="show !== 'login'">
<el-input class="email-input" v-model="registerForm.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
<template #append>
<div @click.stop="openSelect">
<el-select
v-if="show !== 'login'"
ref="mySelect"
v-model="suffix"
:placeholder="$t('select')"
class="select"
>
<el-option
v-for="item in domainList"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<div style="color: #333">
<span>{{ suffix }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
</div>
</template>
</el-input>
<el-input v-model="registerForm.password" :placeholder="$t('password')" type="password" autocomplete="off" />
<el-input v-model="registerForm.confirmPassword" :placeholder="$t('confirmPwd')" type="password" autocomplete="off" />
<el-input v-if="settingStore.settings.regKey === 0" v-model="registerForm.code" :placeholder="$t('regKey')" type="text" autocomplete="off" />
<el-input v-if="settingStore.settings.regKey === 2" v-model="registerForm.code" :placeholder="$t('regKeyOptional')" type="text" autocomplete="off" />
<div v-show="verifyShow"
class="register-turnstile"
:data-sitekey="settingStore.settings.siteKey"
data-callback="onTurnstileSuccess"
data-error-callback="onTurnstileError"
data-after-interactive-callback="loadAfter"
data-before-interactive-callback="loadBefore"
>
<span style="font-size: 12px;color: #F56C6C" v-if="botJsError">{{$t('verifyModuleFailed')}}</span>
</div>
<el-button class="btn" type="primary" @click="submitRegister" :loading="registerLoading"
>{{$t('regBtn')}}
</el-button>
</div>
<template v-if="settingStore.settings.register === 0">
<div class="switch" @click="show = 'register'" v-if="show === 'login'">{{$t('noAccount')}} <span>{{$t('regSwitch')}}</span></div>
<div class="switch" @click="show = 'login'" v-else>{{$t('hasAccount')}} <span>{{$t('loginSwitch')}}</span></div>
</template>
</div>
</template>
</el-input>
<el-input v-model="form.password" :placeholder="$t('password')" type="password" autocomplete="off">
</el-input>
<el-button class="btn" type="primary" @click="submit" :loading="loginLoading"
>{{ $t('loginBtn') }}
</el-button>
</div>
<div v-show="show !== 'login'">
<el-input class="email-input" v-model="registerForm.email" type="text" :placeholder="$t('emailAccount')"
autocomplete="off">
<template #append>
<div @click.stop="openSelect">
<el-select
v-if="show !== 'login'"
ref="mySelect"
v-model="suffix"
:placeholder="$t('select')"
class="select"
>
<el-option
v-for="item in domainList"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<div style="color: #333">
<span>{{ suffix }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
</div>
</template>
</el-input>
<el-input v-model="registerForm.password" :placeholder="$t('password')" type="password" autocomplete="off"/>
<el-input v-model="registerForm.confirmPassword" :placeholder="$t('confirmPwd')" type="password"
autocomplete="off"/>
<el-input v-if="settingStore.settings.regKey === 0" v-model="registerForm.code" :placeholder="$t('regKey')"
type="text" autocomplete="off"/>
<el-input v-if="settingStore.settings.regKey === 2" v-model="registerForm.code"
:placeholder="$t('regKeyOptional')" type="text" autocomplete="off"/>
<div v-show="verifyShow"
class="register-turnstile"
:data-sitekey="settingStore.settings.siteKey"
data-callback="onTurnstileSuccess"
data-error-callback="onTurnstileError"
data-after-interactive-callback="loadAfter"
data-before-interactive-callback="loadBefore"
>
<span style="font-size: 12px;color: #F56C6C" v-if="botJsError">{{ $t('verifyModuleFailed') }}</span>
</div>
<el-button class="btn" type="primary" @click="submitRegister" :loading="registerLoading"
>{{ $t('regBtn') }}
</el-button>
</div>
<template v-if="settingStore.settings.register === 0">
<div class="switch" @click="show = 'register'" v-if="show === 'login'">{{ $t('noAccount') }}
<span>{{ $t('regSwitch') }}</span></div>
<div class="switch" @click="show = 'login'" v-else>{{ $t('hasAccount') }} <span>{{ $t('loginSwitch') }}</span>
</div>
</template>
</div>
</div>
</div>
</template>
@@ -112,7 +119,7 @@ import {loginUserInfo} from "@/request/my.js";
import {permsToRouter} from "@/perm/perm.js";
import {useI18n} from "vue-i18n";
const { t } = useI18n();
const {t} = useI18n();
const accountStore = useAccountStore();
const userStore = useUserStore();
const uiStore = useUiStore();
@@ -139,20 +146,27 @@ const verifyShow = ref(false)
let verifyToken = ''
let turnstileId = null
let botJsError = ref(false)
let verifyErrorCount = 0
window.onTurnstileSuccess = (token) => {
verifyToken = token;
};
window.onTurnstileError = (e) => {
console.log('人机验加载失败')
nextTick(() => {
if (!turnstileId) {
turnstileId = window.turnstile.render('.register-turnstile')
} else {
window.turnstile.reset(turnstileId);
}
})
if (verifyErrorCount >= 4) {
return
}
verifyErrorCount++
console.warn('人机验加载失败', e)
setTimeout(() => {
nextTick(() => {
if (!turnstileId) {
turnstileId = window.turnstile.render('.register-turnstile')
} else {
window.turnstile.reset(turnstileId);
}
})
}, 1500)
};
window.loadAfter = (e) => {
@@ -193,7 +207,9 @@ const submit = () => {
return
}
if (!isEmail(form.email + suffix.value)) {
let email = form.email + (settingStore.settings.loginDomain === 0 ? suffix.value : '');
if (!isEmail(email)) {
ElMessage({
message: t('notEmailMsg'),
type: 'error',
@@ -212,7 +228,7 @@ const submit = () => {
}
loginLoading.value = true
login(form.email + suffix.value, form.password).then(async data => {
login(email, form.password).then(async data => {
localStorage.setItem('token', data.token)
const user = await loginUserInfo();
accountStore.currentAccountId = user.accountId;
@@ -277,7 +293,7 @@ function submitRegister() {
return
}
if(settingStore.settings.regKey === 0) {
if (settingStore.settings.regKey === 0) {
if (!registerForm.code) {
@@ -414,6 +430,7 @@ function submitRegister() {
margin-right: 18px;
margin-left: 18px;
}
.btn {
height: 36px;
width: 100%;
@@ -434,6 +451,7 @@ function submitRegister() {
.switch {
margin-top: 20px;
text-align: center;
span {
color: #006be6;
cursor: pointer;
@@ -444,7 +462,7 @@ function submitRegister() {
border-radius: 6px;
}
.email-input :deep(.el-input__wrapper){
.email-input :deep(.el-input__wrapper) {
border-radius: 6px 0 0 6px;
}
@@ -452,6 +470,7 @@ function submitRegister() {
height: 38px;
width: 100%;
margin-bottom: 18px;
:deep(.el-input__inner) {
height: 36px;
}
@@ -497,7 +516,6 @@ function submitRegister() {
}
#login-box {
background: linear-gradient(to bottom, #2980b9, #6dd5fa, #fff);
color: #333;

View File

@@ -41,8 +41,8 @@
</div>
<el-dialog v-model="pwdShow" :title="$t('changePassword')" width="340">
<div class="update-pwd">
<el-input type="password" :placeholder="$t('newPassword')" v-model="form.password"/>
<el-input type="password" :placeholder="$t('confirmPassword')" v-model="form.newPwd"/>
<el-input type="password" :placeholder="$t('newPassword')" v-model="form.password" autocomplete="off"/>
<el-input type="password" :placeholder="$t('confirmPassword')" v-model="form.newPwd" autocomplete="off"/>
<el-button type="primary" :loading="setPwdLoading" @click="submitPwd">{{$t('save')}}</el-button>
</div>
</el-dialog>
@@ -256,4 +256,4 @@ function submitPwd() {
gap: 20px;
}
}
</style>
</style>

View File

@@ -17,6 +17,13 @@
v-model="setting.register"/>
</div>
</div>
<div class="setting-item">
<div><span>{{$t('loginDomain')}}</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.loginDomain"/>
</div>
</div>
<div class="setting-item">
<div><span>{{$t('regKey')}}</span></div>
<div>
@@ -326,7 +333,7 @@
<div class="card-content">
<div class="concerning-item">
<span>{{$t('version')}} :</span>
<span>v1.6.0</span>
<span>v1.7.0</span>
</div>
<div class="concerning-item">
<span>{{$t('community')}} : </span>
@@ -345,13 +352,22 @@
</div>
<div class="concerning-item">
<span>{{$t('support')}} : </span>
<el-button @click="jump('https://support.skymail.ink')" >
<el-button @click="jump('https://doc.skymail.ink/support.html')" >
{{t('supportDesc')}}
<template #icon>
<Icon color="#79D6B5" icon="simple-icons:buymeacoffee" width="20" height="20" />
</template>
</el-button>
</div>
<div class="concerning-item">
<span>{{$t('help')}} : </span>
<el-button @click="jump('https://doc.skymail.ink')" >
{{t('document')}}
<template #icon>
<Icon color="#79D6B5" icon="fluent-color:document-32" width="18" height="18" />
</template>
</el-button>
</div>
</div>
</div>
</div>
@@ -1150,8 +1166,8 @@ function editSetting(settingForm, refreshStatus = true) {
}
.background {
width: 230px;
height: 120px;
width: 250px;
height: 140px;
border-radius: 4px;
border: 1px solid #e4e7ed;
@media (max-width: 500px) {
@@ -1178,11 +1194,7 @@ function editSetting(settingForm, refreshStatus = true) {
overflow: hidden;
}
@media (min-width: 885px) {
.about {
height: 210px;
}
}
.card-title {
font-size: 15px;
@@ -1501,4 +1513,4 @@ form .el-button {
<style>
.el-popper.is-dark {
}
</style>
</style>