新增公告弹窗和无人收件开关

This commit is contained in:
eoao
2025-08-03 21:03:54 +08:00
parent 3a40cfc63b
commit 658c1776d5
43 changed files with 857 additions and 474 deletions

View File

@@ -5,9 +5,11 @@
</template>
<script setup>
import { useI18n } from "vue-i18n";
import { watch } from "vue";
import {useSettingStore} from "@/store/setting.js";
const settingStore = useSettingStore()
import zhCn from 'element-plus/es/locale/lang/zh-cn';;
import zhCn from 'element-plus/es/locale/lang/zh-cn';
const { locale } = useI18n()
locale.value = settingStore.lang
watch(() => settingStore.lang, () => locale.value = settingStore.lang)
</script>

View File

@@ -5,18 +5,18 @@ const en = {
starred: 'Starred',
settings: 'Settings',
analytics: 'Analytics',
allUsers: 'All users',
allMail: 'All mail',
allUsers: 'All Users',
allMail: 'All Mail',
permissions: 'Role',
inviteCode: 'Invite code',
SystemSettings: 'System settings',
inviteCode: 'Invite Code',
SystemSettings: 'System Settings',
noMoreData: 'No more data',
noMessagesFound: 'No messages found',
addAccount: 'Add account',
addAccount: 'Add Account',
emailAccount: 'Email',
deleteUser: 'Delete account',
deleteUser: 'Delete Account',
deleteUserBtn: 'Delete',
changePassword: 'Change password',
changePassword: 'Change Password',
newPassword: 'New password',
confirmPassword: 'Confirm password',
add: 'Add',
@@ -29,22 +29,23 @@ const en = {
changePwdBtn: 'Change',
username: 'Username',
password: 'Password',
delAccount: 'Delete account',
delAccount: 'Delete Account',
delAccountMsg: 'This will permanently delete your account and data. It cannot be reactivated.',
totalReceived: 'Total received',
totalSent: 'Total sent',
totalMailboxes: 'Total accounts',
totalUsers: 'Total users',
totalReceived: 'Total Received',
totalSent: 'Total Sent',
totalMailboxes: 'Total Accounts',
totalUsers: 'Total Users',
deleted: 'Deleted',
selectDeleted: 'Deleted',
active: 'Active',
emailSource: 'Email source',
userGrowth: 'User growth',
emailGrowth: 'Email growth',
emailSource: 'Email Source',
userGrowth: 'User Growth',
emailGrowth: 'Email Growth',
emailSent: 'Sent',
emailReceived: 'Received',
sentToday: 'Sent today',
sentToday: 'Sent Today',
total: 'Total',
growthTotalUsers: 'Total users',
growthTotalUsers: 'Total Users',
searchByEmail: 'Enter email to search',
tabEmailAddress: 'Email',
tabReceived: 'Received',
@@ -63,9 +64,9 @@ const en = {
tabSetting: 'Settings',
registrationIp: 'Registration IP',
recentIP: 'Recent IP',
recentActivity: 'Recent activity',
loginDevice: 'Login device',
loginSystem: 'Login system',
recentActivity: 'Recent Activity',
loginDevice: 'Login Device',
loginSystem: 'Login System',
browserLogin: 'Browser Login',
unauthorized: 'Unauthorized',
unlimited: 'Unlimited',
@@ -76,10 +77,10 @@ const en = {
perm: 'Role',
btnBan: 'Ban',
admin: 'Admin',
addUser: 'Add user',
addUser: 'Add User',
select: 'Select',
unknown: 'Unknown',
changePerm: 'Change role',
changePerm: 'Change Role',
from: 'From',
subject: 'Subject',
sender: 'Sender',
@@ -91,22 +92,22 @@ const en = {
order: 'Order',
default: 'Default',
description: 'Description',
removeBody: 'Remove body',
removeContent: 'Remove content',
removeAll: 'Remove all',
expand: 'Expand',
collapse: 'Collapse',
daily: 'Daily',
searchRegKeyDesc: 'Enter invite code to search',
remainingUses: 'Remaining uses',
remainingUses: 'Remaining Uses',
exhausted: 'Exhausted',
validUntil: 'Valid until',
validUntil: 'Valid Until',
expired: 'Expired',
copy: 'Copy',
history: 'History',
addRegKey: 'Add invite code',
regKey: 'Invite code',
addRegKey: 'Add Invite Code',
regKey: 'Invite Code',
noCodeFound: 'No messages found',
useHistory: 'Usage history',
useHistory: 'Usage History',
date: 'Date',
roleDesc: 'Role',
noSubject: 'No subject',
@@ -131,43 +132,43 @@ const en = {
regSwitch: 'Sign up',
loginSwitch: 'Sign in',
websiteSetting: 'Website',
websiteReg: 'Sign up',
multipleEmail: 'Multiple accounts',
multipleEmailDesc: 'Enable this feature to allow users to add multiple accounts',
physicallyWipeData: 'Physically wipe data',
physicallyWipeDataDesc: 'This action will permanently erase all deleted data',
websiteReg: 'Sign Up',
multipleEmail: 'Multiple Accounts',
multipleEmailDesc: 'Enable this feature to allow users to add multiple accounts.',
physicallyWipeData: 'Physically Wipe Data',
physicallyWipeDataDesc: 'This action will permanently erase all deleted data.',
customization: 'Customization',
websiteTitle: 'Title',
loginBoxOpacity: 'Login box opacity',
loginBoxOpacity: 'Login Box Opacity',
loginBackground: 'Background',
emailSetting: 'Email',
receiveEmails: 'Receive email',
autoRefresh: 'Auto refresh',
autoRefreshDesc: 'Automatically fetch the latest emails from the server',
sendEmail: 'Send email',
receiveEmail: 'Receive Email',
autoRefresh: 'Auto Refresh',
autoRefreshDesc: 'Automatically fetch the latest emails from the server.',
sendEmail: 'Send Email',
resendToken: 'Resend Token',
R2OS: 'R2 Object storage',
R2OS: 'R2 Object Storage',
osDomain: 'Domain',
emailPush: 'Email push',
tgBot: 'Telegram bot',
emailPush: 'Email Push',
tgBot: 'Telegram Bot',
disable: 'Disable',
disabled: 'Disabled',
otherEmail: 'Forwarding to external email',
otherEmail: 'Forwarding to External Email',
forwardingRules: 'Forwarding Rules',
forwardAll: 'All',
rules: 'Rules',
turnstileSetting: 'Turnstile',
signUpVerification: 'Sign up verification',
addEmailVerification: 'Add account verification',
signUpVerification: 'Sign Up Verification',
addEmailVerification: 'Add Account Verification',
about: 'About',
version: 'Version',
community: 'Community',
changeTitle: 'Change title',
changeTitle: 'Change Title',
addResendTokenDesc: 'Input to add; leave empty to delete.',
addOsDomain: 'Add domain',
addOsDomain: 'Add Domain',
domainDesc: 'Domain',
addTurnstileSecret: 'Add turnstile secret',
backgroundTitle: 'Change background',
backgroundTitle: 'Change Background',
tgBotDesc: 'Forward received emails to a Telegram bot',
tgBotToken: 'Bot token',
toBotTokenDesc: 'Multiple user chat_ids, separated by commas',
@@ -175,11 +176,11 @@ const en = {
otherEmailInputDesc: 'Separate multiple email addresses with commas.',
forwardingRulesDesc: 'Rule-based forwarding only forwards emails received by the specified address.',
ruleEmailsInputDesc: 'Separate multiple email addresses with commas.',
resendTokenList: 'Token list',
resendTokenList: 'Token List',
domain: 'Domain',
optional: 'Optional',
subjectInputDesc: 'Please enter the email subject.',
changeUserName: 'Change username',
changeUserName: 'Change Username',
sendSeparately: 'Separately',
send: 'Send',
reply: 'Reply',
@@ -204,9 +205,9 @@ const en = {
addSuccessMsg: 'Addition successful',
delConfirm: 'Confirm deleting {msg}?',
emptyRoleNameMsg: 'Role name cannot be empty',
changSuccessMsg: 'Changes saved successfully',
changeRoleTitle: 'Change role',
addRoleTitle: 'Add role',
saveSuccessMsg: 'Saved successfully',
changeRoleTitle: 'Change Role',
addRoleTitle: 'Add Role',
emptyUserNameMsg: 'Name cannot be empty',
delAccountConfirm: 'Confirm deleting current account and all associated data?',
clearAllDelConfirm: 'This action is irreversible. Enter <b style="font-weight: bold">DELETE</b> to proceed',
@@ -216,7 +217,7 @@ const en = {
delBackgroundConfirm: 'Confirm deleting this background?',
enable: 'Enable',
enabled: 'Enabled',
reSendConfirm: 'Confirm reset of {msg} send total?',
reSendConfirm: 'Confirm reset of {msg} send count?',
reSuccessMsg: 'Reset successful',
restoreConfirm: 'Confirm restoring {msg}?',
normalRestore: 'Normal restore',
@@ -234,7 +235,7 @@ const en = {
sendFailMsg: 'Send failed',
saveDraftConfirm: 'Save draft?',
delEmailsConfirm: 'Confirm batch delete these emails?',
sending: 'Sending Email...',
sending: 'Sending email...',
sendingErrorMsg: 'Sending in progress',
networkErrorMsg: 'Network error. Check your internet',
timeoutErrorMsg: 'Timeout. Try again later',
@@ -249,8 +250,8 @@ const en = {
supportDesc: 'Buy me tea',
featDesc: 'Feature Description',
emailInterception: 'Email Interception',
emailInterceptionDesc: '*Intercept emails by blocking entire domain using example.com to prevent users from receiving emails from certain websites.',
availableDomains: 'Available domains',
emailInterceptionDesc: 'Enter a domain or email address to prevent users from receiving emails from certain websites.',
availableDomains: 'Available Domains',
availableDomainsDesc: 'Restrict users to email domains specified. Domains not on the approved list will be blocked from registration, adding email addresses, and sending/receiving emails. If left blank, all domains will be allowed by default.',
backgroundUrlDesc: 'Image URL',
localUpload: ' Local upload',
@@ -260,6 +261,24 @@ const en = {
rulesVerify: 'Rules',
rulesVerifyTitle: 'Trigger After {count} Daily Uses per IP',
botVerifyMsg: 'Please verify that you are human',
noticeTitle: 'Notice',
noticePopup: 'Sign-in Popup',
icon: 'Icon',
position: 'Position',
offset: 'Offset',
duration: 'Duration',
topRight: 'Top Right',
topLeft: 'Top Left',
bottomRight: 'Bottom Right',
bottomLeft: 'Bottom Left',
width: 'Width',
titleDesc: 'Title',
noticeContentDesc: 'Notice content supports HTML',
verifyModuleFailed: 'Verification module failed to load. Please refresh the page',
popUp: 'Pop Up',
noRecipientTitle: 'No Recipient',
noRecipientDesc: 'Emails can be received even without a registered email address.',
preview: 'Preview'
}
export default en

View File

@@ -36,6 +36,7 @@ const zh = {
totalMailboxes: '邮箱数量',
totalUsers: '用户数量',
deleted: '删除',
selectDeleted: '已删除',
active: '正常',
emailSource: '邮件来源',
userGrowth: '用户增长',
@@ -91,7 +92,7 @@ const zh = {
order: '排序',
default: '默认',
description: '描述',
removeBody: '移除正文',
removeContent: '移除正文',
removeAll: '丢弃邮件',
expand: '展开',
collapse: '收起',
@@ -141,7 +142,7 @@ const zh = {
loginBoxOpacity: '登录透明',
loginBackground: '登录背景',
emailSetting: '邮件设置',
receiveEmails: '邮件接收',
receiveEmail: '邮件接收',
autoRefresh: '自动刷新',
autoRefreshDesc: '轮询请求服务器获取最新邮件',
sendEmail: '邮件发送',
@@ -164,7 +165,7 @@ const zh = {
community: '交流',
changeTitle: '修改标题',
addResendTokenDesc: '输入内容添加,不填则删除',
addOsDomain: '添加访问域名',
addOsDomain: '添加域名',
domainDesc: '域名',
addTurnstileSecret: '添加 Turnstile 密钥',
backgroundTitle: '设置背景',
@@ -204,7 +205,7 @@ const zh = {
addSuccessMsg: '添加成功',
delConfirm: '确认删除{msg}吗?',
emptyRoleNameMsg: '身份名不能为空',
changSuccessMsg: '修改成功',
saveSuccessMsg: '保存成功',
changeRoleTitle: '修改身份',
addRoleTitle: '添加身份',
emptyUserNameMsg: '用户名不能为空',
@@ -249,7 +250,7 @@ const zh = {
supportDesc: '请我喝杯奶茶',
featDesc: '功能说明',
emailInterception: '邮件拦截',
emailInterceptionDesc: '拦截邮件, 要拦截整个域名输入 *example.com, 可用于禁止用户接收某些网站的邮件',
emailInterceptionDesc: '输入邮箱或域名拦截邮件,可用于禁止用户接收某些网站的邮件',
availableDomains: '可用域名',
availableDomainsDesc: '限制用户只能使用指定的域名邮箱,不在配置名单内的域名会被禁止使用注册添加邮箱,接收发送邮件等功能,留空默认允许可用所有域名',
backgroundUrlDesc: '在线图片链接',
@@ -260,5 +261,24 @@ const zh = {
rulesVerify: '规则',
rulesVerifyTitle: 'IP 每天使用 {count} 次后触发',
botVerifyMsg: '请完成人机验证',
noticeTitle: '网站公告',
noticePopup: '登录弹窗',
icon: '图标',
position: '位置',
offset: '偏移距离',
duration: '显示时长',
topRight: '右上',
topLeft: '左上',
bottomRight: '右下',
bottomLeft: '左下',
width: '宽度',
titleDesc: '标题',
noticeContentDesc: '公告内容支持HTML',
verifyModuleFailed: '人机验证模块加载失败,请刷新页面',
popUp: '弹出',
noRecipientTitle: '无人收件',
noRecipientDesc: '即使没有注册的邮箱也能收到邮件',
preview: '预览'
}
export default zh

View File

@@ -12,10 +12,10 @@
</div>
<div class="opt">
<div class="send-email" @click.stop>
<Icon icon="eva:email-fill" width="22" height="22" color="#fbbd08" />
<Icon icon="eva:email-fill" width="22" height="22" color="#fccb1a" />
</div>
<div class="settings" @click.stop>
<Icon icon="streamline-ultimate-color:copy-paste-1" width="19" height="19" @click.stop="copyAccount(item.email)"/>
<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)" />
<el-dropdown v-else>
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399" />
@@ -217,7 +217,7 @@ function setName() {
}
ElMessage({
message: t('changSuccessMsg'),
message: t('saveSuccessMsg'),
type: "success",
plain: true
})

View File

@@ -94,7 +94,7 @@ const route = useRoute();
justify-content: center;
gap: 5px;
color: #ffffff;
background: linear-gradient(135deg, #1890ff, #1c6dd0);
background: linear-gradient(135deg, #1890ff, #3a80dd);
transition: all 0.3s ease;
:deep(.el-icon) {

View File

@@ -10,8 +10,19 @@
</div>
</div>
<div class="toolbar">
<div class="email">
<span>{{ userStore.user.email }}</span>
<el-dropdown>
<div class="translate icon-item">
<Icon icon="carbon:ibm-watson-language-translator" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="changeLang('zh')">简体中文</el-dropdown-item>
<el-dropdown-item @click="changeLang('en')">English</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div class="notice icon-item" @click="openNotice">
<Icon icon="streamline-plump:announcement-megaphone" />
</div>
<el-dropdown :teleported="false" popper-class="detail-dropdown" >
<div class="avatar">
@@ -28,7 +39,7 @@
<div class="user-name">
{{userStore.user.name}}
</div>
<div class="detail-email">
<div class="detail-email" @click="copyEmail(userStore.user.email)">
{{ userStore.user.email }}
</div>
<div class="detail-user-type">
@@ -136,6 +147,32 @@ const sendCount = computed(() => {
return userStore.user.sendCount + '/' + userStore.user.role.sendCount
})
async function copyEmail(email) {
try {
await navigator.clipboard.writeText(email);
ElMessage({
message: t('copySuccessMsg'),
type: 'success',
plain: true,
})
} catch (err) {
console.error(`${t('copyFailMsg')}:`, err);
ElMessage({
message: t('copyFailMsg'),
type: 'error',
plain: true,
})
}
}
function changeLang(lang) {
settingStore.lang = lang
}
function openNotice() {
uiStore.showNotice()
}
function openSend() {
uiStore.writerRef.open()
}
@@ -224,6 +261,7 @@ function formatName(email) {
text-overflow: ellipsis;
text-align: center;
color: #5c5958;
cursor: pointer;
}
.logout {
margin-top: 20px;
@@ -272,11 +310,11 @@ function formatName(email) {
justify-content: center;
margin-left: 5px;
.writer {
width: 36px;
height: 36px;
width: 34px;
height: 34px;
border-radius: 50%;
color: #ffffff;
background: linear-gradient(135deg, #1890ff, #1c6dd0);
background: linear-gradient(135deg, #1890ff, #3a80dd);
transition: all 0.3s ease;
display: flex;
align-items: center;
@@ -298,38 +336,41 @@ function formatName(email) {
.toolbar {
display: grid;
grid-template-columns: 1fr auto auto;
margin-left: auto;
@media (max-width: 1024px) {
grid-template-columns: 1fr auto;
display: flex;
justify-content: end;
gap: 15px;
@media (max-width: 767px) {
gap: 10px;
}
.full {
.icon-item {
align-self: center;
width: 30px;
height: 30px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
padding-right: 20px;
cursor: pointer;
@media (max-width: 1024px) {
display: none;
}
}
.email {
align-self: center;
font-size: 14px;
margin-right: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: bold;
width: 100%;
.icon-item:hover {
background: #F0F2F5;
}
.notice {
font-size: 22px;
margin-right: 4px;
}
.translate {
padding-top: 2px;
font-size: 21px;
}
.avatar {
display: flex;
align-items: center;
cursor: pointer;
margin-left: 10px;
.avatar-text {
height: 30px;
width: 30px;

View File

@@ -13,23 +13,74 @@
import account from '@/layout/account/index.vue'
import {useUiStore} from "@/store/ui.js";
import {useSettingStore} from "@/store/setting.js";
import {computed, onBeforeUnmount, onMounted} from "vue";
import {computed, onBeforeUnmount, onMounted, watch} from "vue";
import { useRoute } from 'vue-router'
import { hasPerm } from "@/perm/perm.js"
const props = defineProps({
openSend: Function
})
const settingStore = useSettingStore()
const uiStore = useUiStore();
const route = useRoute()
let innerWidth = window.innerWidth
let elNotification = null
const accountShow = computed(() => {
return uiStore.accountShow && settingStore.settings.manyEmail === 0
})
watch(() => uiStore.changeNotice, () => {
const settings = settingStore.settings
let data = {
notice: settings.notice,
noticeWidth: settings.noticeWidth,
noticeTitle: settings.noticeTitle,
noticeContent: settings.noticeContent,
noticeType: settings.noticeType,
noticeDuration: settings.noticeDuration,
noticePosition: settings.noticePosition,
noticeOffset: settings.noticeOffset
}
showNotice(data)
})
watch(() => uiStore.changePreview, () => {
showNotice(uiStore.previewData)
})
function showNotice(data) {
if (data.notice === 1) {
return;
}
if (elNotification) {
elNotification.close()
}
const style = document.createElement('style');
style.innerHTML = `
.custom-notice.el-notification {
--el-notification-width: min(${data.noticeWidth}px,calc(100% - 30px)) !important;
}
`;
document.head.appendChild(style);
elNotification = ElNotification({
title: data.noticeTitle,
message: `<div style="width: 100%;height: 100%;">${data.noticeContent}</div>`,
type: data.noticeType === 'none' ? '' : data.noticeType,
duration: data.noticeDuration,
position: data.noticePosition,
offset: data.noticeOffset,
dangerouslyUseHTMLString: true,
customClass: 'custom-notice'
})
}
onMounted(() => {
window.addEventListener('resize', handleResize)
handleResize()
@@ -49,7 +100,6 @@ const handleResize = () => {
}
</script>
<style lang="scss" scoped>
.block-show {

View File

@@ -5,7 +5,10 @@ export const useUiStore = defineStore('ui', {
asideShow: window.innerWidth > 1024,
accountShow: false,
backgroundLoading: true,
changeNotice: 0,
writerRef: null,
changePreview: 0,
previewData: {},
key: 0,
asideCount: {
email: 0,
@@ -13,6 +16,15 @@ export const useUiStore = defineStore('ui', {
sysEmail: 0
}
}),
actions: {
showNotice() {
this.changeNotice ++
},
previewNotice(data) {
this.previewData = data
this.changePreview ++
}
},
persist: {
pick: ['accountShow'],
},

View File

@@ -3,10 +3,10 @@ import 'dayjs/locale/zh-cn'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
import {useSettingStore} from "@/store/setting.js";
const { lang } = useSettingStore();
const settingStore = useSettingStore();
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.locale(lang === 'zh' ? 'zh-cn' : '')
dayjs.locale(settingStore.lang === 'zh' ? 'zh-cn' : '')
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
export function fromNow(date) {
@@ -16,7 +16,7 @@ export function fromNow(date) {
const diffMinutes = now.diff(d, 'minute');
const diffHours = now.diff(d, 'hour');
const isToday = now.isSame(d, 'day');
if (lang === 'zh') {
if (settingStore.lang === 'zh') {
if (isToday) {
if (diffSeconds < 60) return `几秒前`;
@@ -63,7 +63,7 @@ export function formatDetailDate(time) {
const isSameYear = now.year() === d.year();
if (lang === 'zh') {
if (settingStore.lang === 'zh') {
return d.format('YYYY年M月D日 ddd AH:mm');
} else {
return isSameYear

View File

@@ -1,4 +1,8 @@
export function isEmail(email) {
const reg = /^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/;
return reg.test(email);
}
export function isDomain(str) {
return /^(?!:\/\/)([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/.test(str);
}

View File

@@ -45,8 +45,8 @@
<el-option key="1" :label="$t('all')" value="all"/>
<el-option key="3" :label="$t('received')" value="receive"/>
<el-option key="2" :label="$t('sent')" value="send"/>
<el-option key="4" :label="$t('deleted')" value="delete"/>
<el-option key="4" :label="$t('noRecipient')" value="noone"/>
<el-option key="4" :label="$t('selectDeleted')" value="delete"/>
<el-option key="4" :label="$t('noRecipientTitle')" value="noone"/>
</el-select>
<Icon class="icon" icon="iconoir:search" @click="search" width="20" height="20"/>
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-down-outline"

View File

@@ -130,7 +130,6 @@ import {debounce} from "lodash-es";
import loading from "@/components/loading/index.vue";
import {useRoute} from "vue-router";
import { useI18n } from 'vue-i18n';
import {toUtc, tzDayjs} from "@/utils/day.js";
defineOptions({
name: 'analysis'
@@ -693,7 +692,7 @@ function createSendGauge() {
grid-template-columns: 1fr;
}
.number-item {
background: #fff;
background: var(--el-bg-color);
border-radius: 8px;
border: 1px solid var(--el-border-color);
padding: 21px 20px;

View File

@@ -81,7 +81,7 @@
data-after-interactive-callback="loadAfter"
data-before-interactive-callback="loadBefore"
>
<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-button class="btn" type="primary" @click="submitRegister" :loading="registerLoading"
>{{$t('regBtn')}}
@@ -105,6 +105,7 @@ 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 {useUiStore} from "@/store/ui.js";
import {Icon} from "@iconify/vue";
import {cvtR2Url} from "@/utils/convert.js";
import {loginUserInfo} from "@/request/my.js";
@@ -114,6 +115,7 @@ import {useI18n} from "vue-i18n";
const { t } = useI18n();
const accountStore = useAccountStore();
const userStore = useUserStore();
const uiStore = useUiStore();
const settingStore = useSettingStore();
const loginLoading = ref(false)
const show = ref('login')
@@ -220,6 +222,7 @@ const submit = () => {
router.addRoute('layout', routerData);
});
await router.replace({name: 'layout'})
uiStore.showNotice()
}).finally(() => {
loginLoading.value = false
})

View File

@@ -72,7 +72,7 @@
<el-input-tag class="dialog-input-tag" tag-type="warning" :class="form.banEmail.length === 0 ? 'dialog-input' : '' " v-model="form.banEmail" @add-tag="banEmailAddTag" type="text" :placeholder="$t('emailInterception')" autocomplete="off" />
<el-radio-group class="dialog-radio" v-model="form.banEmailType" v-if="form.banEmail.length > 0">
<el-radio :label="$t('removeAll')" :value="0" />
<el-radio :label="$t('removeBody')" :value="1" />
<el-radio :label="$t('removeContent')" :value="1" />
</el-radio-group>
<el-select
class="dialog-input"
@@ -146,7 +146,7 @@ import loading from '@/components/loading/index.vue';
import {useRoleStore} from "@/store/role.js";
import {useUserStore} from "@/store/user.js";
import {useSettingStore} from "@/store/setting.js";
import {isEmail} from "@/utils/verify-utils.js";
import {isEmail, isDomain} from "@/utils/verify-utils.js";
import {useI18n} from "vue-i18n";
defineOptions({
@@ -198,7 +198,10 @@ rolePermTree().then(tree => {
treeList.push(...tree)
})
domainOptions = domainList.map(domain => ({label: domain,value: domain}))
domainOptions = domainList.map(domain => {
const cleanDomain = domain.replace(/^@/, '');
return { label: cleanDomain, value: cleanDomain };
});
function availDomainChange() {
@@ -218,7 +221,7 @@ function banEmailAddTag(val) {
form.banEmail.splice(form.banEmail.length - 1, 1)
emails.forEach(email => {
if (isEmail(email) && !form.banEmail.includes(email)) {
if ((isEmail(email) || isDomain(email)) && !form.banEmail.includes(email)) {
form.banEmail.push(email)
}
})
@@ -236,7 +239,7 @@ function roleFormClick() {
function setDef(role) {
roleSetDef(role.roleId).then(() => {
ElMessage({
message: t('changSuccessMsg'),
message: t('saveSuccessMsg'),
type: "success",
plain: true
})
@@ -297,7 +300,7 @@ function setRole() {
permLoading.value = true
roleSet(params).then(() => {
ElMessage({
message: t('changSuccessMsg'),
message: t('saveSuccessMsg'),
type: "success",
plain: true
})
@@ -340,6 +343,7 @@ function openRoleSet(role) {
form.sendCount = role.sendCount
form.accountCount = role.accountCount
form.banEmail = role.banEmail
form.banEmailType = role.banEmailType
form.availDomain = role.availDomain
nextTick(() => {
tree.value.setCheckedKeys(role.permIds)

View File

@@ -30,21 +30,6 @@
</div>
</div>
</div>
<div class="container lang">
<div class="title">{{$t('language')}}</div>
<div>
<el-select v-model="lang" placeholder="Select" style="width: 100px">
<el-option
key="zh"
label="简体中文"
value="zh"/>
<el-option
key="en"
label="English"
value="en"/>
</el-select>
</div>
</div>
<div class="del-email" v-perm="'my:delete'">
<div class="title">{{$t('deleteUser')}}</div>
<div style="color: #585d69;">
@@ -64,33 +49,25 @@
</div>
</template>
<script setup>
import {reactive, ref, defineOptions, watch} from 'vue'
import {reactive, ref, defineOptions} from 'vue'
import {resetPassword, userDelete} from "@/request/my.js";
import {useUserStore} from "@/store/user.js";
import router from "@/router/index.js";
import { storeToRefs } from 'pinia'
import {accountSetName} from "@/request/account.js";
import {useAccountStore} from "@/store/account.js";
import {useI18n} from "vue-i18n";
import {useSettingStore} from "@/store/setting.js";
const { t } = useI18n()
const settingStore = useSettingStore()
const accountStore = useAccountStore()
const userStore = useUserStore();
const setPwdLoading = ref(false)
const setNameShow = ref(false)
const accountName = ref(null)
const { lang } = storeToRefs(settingStore)
defineOptions({
name: 'setting'
})
watch(() => lang.value,() => {
window.location.reload()
})
function showSetName() {
accountName.value = userStore.user.name
setNameShow.value = true
@@ -118,7 +95,7 @@ function setName() {
accountSetName(userStore.user.accountId,name).then(() => {
ElMessage({
message: t('changSuccessMsg'),
message: t('saveSuccessMsg'),
type: 'success',
plain: true,
})
@@ -187,7 +164,7 @@ function submitPwd() {
setPwdLoading.value = true
resetPassword(form.password).then(() => {
ElMessage({
message: t('changSuccessMsg'),
message: t('saveSuccessMsg'),
type: 'success',
plain: true,
})

View File

@@ -124,7 +124,7 @@
<div class="card-title">{{$t('emailSetting')}}</div>
<div class="card-content">
<div class="setting-item">
<div><span>{{$t('receiveEmails')}}</span></div>
<div><span>{{$t('receiveEmail')}}</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.receive"/>
@@ -160,6 +160,18 @@
v-model="setting.send"/>
</div>
</div>
<div class="setting-item">
<div>
<span>{{$t('noRecipientTitle')}}</span>
<el-tooltip effect="dark" :content="$t('noRecipientDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.noRecipient"/>
</div>
</div>
<div class="setting-item">
<div><span>{{$t('resendToken')}}</span></div>
<div>
@@ -286,12 +298,35 @@
</div>
</div>
<div class="settings-card">
<div class="card-title">{{$t('noticeTitle')}}</div>
<div class="card-content">
<div class="setting-item">
<div><span>{{$t('noticePopup')}}</span></div>
<div class="forward">
<span>{{ setting.notice === 0 ? $t('enabled') : $t('disabled') }}</span>
<el-button class="opt-button" size="small" type="primary" @click="openNoticePopupSetting">
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
</el-button>
</div>
</div>
<div class="setting-item">
<div><span>{{$t('popUp')}}</span></div>
<div class="forward">
<el-button class="opt-button" size="small" type="primary" @click="openNoticePopup">
<Icon icon="mynaui:click-solid" width="18" height="18"/>
</el-button>
</div>
</div>
</div>
</div>
<div class="settings-card about">
<div class="card-title">{{$t('about')}}</div>
<div class="card-content">
<div class="concerning-item">
<span>{{$t('version')}} :</span>
<span>v1.5.0</span>
<span>v1.6.0</span>
</div>
<div class="concerning-item">
<span>{{$t('community')}} : </span>
@@ -323,7 +358,7 @@
</div>
<!-- Dialogs remain the same -->
<el-dialog v-model="editTitleShow" :title="$t('changeTitle')" width="340" >
<el-dialog v-model="editTitleShow" :title="$t('changeTitle')" width="340" @closed="editTitle = setting.title">
<form>
<el-input type="text" :placeholder="$t('websiteTitle')" v-model="editTitle"/>
<el-button type="primary" :loading="settingLoading" @click="saveTitle">{{$t('save')}}</el-button>
@@ -474,19 +509,90 @@
<el-table-column :width="tokenColumnWidth" property="value" label="Token" fixed="right" :show-overflow-tooltip="true" />
</el-table>
</el-dialog>
<el-dialog v-model="showRegVerifyCount" :title="$t('rulesVerifyTitle',{count: regVerifyCount})" @closed="regVerifyCount = setting.regVerifyCount" >
<el-dialog v-model="regVerifyCountShow" :title="$t('rulesVerifyTitle',{count: regVerifyCount})" @closed="regVerifyCount = setting.regVerifyCount" >
<form>
<el-input-number type="text" v-model="regVerifyCount" :min="1" >
</el-input-number>
<el-button type="primary" :loading="settingLoading" @click="saveRegVerifyCount">{{$t('save')}}</el-button>
</form>
</el-dialog>
<el-dialog v-model="showAddVerifyCount" :title="$t('rulesVerifyTitle',{count: addVerifyCount})" @closed="addVerifyCount = setting.addVerifyCount">
<el-dialog v-model="addVerifyCountShow" :title="$t('rulesVerifyTitle',{count: addVerifyCount})" @closed="addVerifyCount = setting.addVerifyCount">
<form>
<el-input-number type="text" v-model="addVerifyCount" :min="1"/>
<el-button type="primary" :loading="settingLoading" @click="saveAddVerifyCount">{{$t('save')}}</el-button>
</form>
</el-dialog>
<el-dialog top="5vh" v-model="noticePopupShow" :title="$t('noticePopup')" class="notice-popup" @closed="resetNoticeForm">
<form>
<el-input v-model="noticeForm.noticeTitle" :placeholder="t('titleDesc')" />
<div class="notice-line-item" >
<el-select v-model="noticeForm.noticeType" >
<template #prefix>
<span style="margin-right: 10px">{{$t('icon')}}</span>
</template>
<el-option key="none" label="None" value="none" />
<el-option key="primary" label="Primary" value="primary" />
<el-option key="success" label="Success" value="success" />
<el-option key="warning" label="Warning" value="warning" />
<el-option key="info" label="Info" value="info" />
</el-select>
<el-select v-model="noticeForm.noticePosition" >
<template #prefix>
<span style="margin-right: 10px">{{$t('position')}}</span>
</template>
<el-option key="top-left" :label="t('topLeft')" value="top-left" />
<el-option key="top-right" :label="t('topRight')" value="top-right" />
<el-option key="bottom-left" :label="t('bottomLeft')" value="bottom-left" />
<el-option key="bottom-right" :label="t('bottomRight')" value="bottom-right" />
</el-select>
<el-input-number v-model="noticeForm.noticeWidth" >
<template #prefix >
{{$t('width')}}
</template>
<template #suffix >
px
</template>
</el-input-number>
<el-input-number v-model="noticeForm.noticeOffset" >
<template #prefix >
{{$t('offset')}}
</template>
<template #suffix >
px
</template>
</el-input-number>
<el-input-number v-model="noticeForm.noticeDuration" >
<template #prefix >
{{$t('duration')}}
</template>
<template #suffix >
ms
</template>
</el-input-number>
</div>
<div class="notice-popup-item">
<el-input
v-model="noticeForm.noticeContent"
:autosize="{ minRows: 15, maxRows: 25 }"
type="textarea"
:placeholder="t('noticeContentDesc')"
/>
</div>
</form>
<template #footer>
<div class="dialog-footer">
<el-switch v-model="noticeForm.notice" :active-value="0" :inactive-value="1" :active-text="$t('enable')" :inactive-text="$t('disable')" />
<div>
<el-button @click="previewNoticePopup">
{{$t('preview')}}
</el-button>
<el-button :loading="settingLoading" type="primary" @click="saveNoticePopup">
{{$t('save')}}
</el-button>
</div>
</div>
</template>
</el-dialog>
</el-scrollbar>
</div>
</template>
@@ -495,6 +601,7 @@
import {computed, defineOptions, reactive, ref} from "vue";
import {physicsDeleteAll, setBackground, settingQuery, settingSet} from "@/request/setting.js";
import {useSettingStore} from "@/store/setting.js";
import {useUiStore} from "@/store/ui.js";
import {useUserStore} from "@/store/user.js";
import {useAccountStore} from "@/store/account.js";
import {Icon} from "@iconify/vue";
@@ -522,10 +629,12 @@ const resendTokenFormShow = ref(false)
const r2DomainShow = ref(false)
const turnstileShow = ref(false)
const tgSettingShow = ref(false)
const noticePopupShow = ref(false)
const thirdEmailShow = ref(false)
const forwardRulesShow = ref(false)
const showResendList = ref(false)
const settingStore = useSettingStore();
const uiStore = useUiStore();
const {settings: setting} = storeToRefs(settingStore);
const editTitle = ref('')
const settingLoading = ref(false)
@@ -537,8 +646,8 @@ const showSetBackground = ref(false)
let regVerifyCount = ref(1)
let addVerifyCount = ref(1)
let backup = '{}'
const showAddVerifyCount = ref(false)
const showRegVerifyCount = ref(false)
const addVerifyCountShow = ref(false)
const regVerifyCountShow = ref(false)
const resendTokenForm = reactive({
domain: '',
token: '',
@@ -548,13 +657,24 @@ const turnstileForm = reactive({
secretKey: ''
})
const regKeyOptions = [
const noticeForm = reactive({
noticeTitle: '',
noticeContent: '',
noticeType: '',
noticeDuration: '',
noticePosition: '',
noticeOffset: 0,
notice: 0,
noticeWidth: 0
})
const regKeyOptions = computed(() => [
{label: t('enable'), value: 0},
{label: t('disable'), value: 1},
{label: t('optional'), value: 2},
]
])
const options = [
const options = computed(() => [
{label: t('disable'), value: 0},
{label: '3s', value: 3},
{label: '5s', value: 5},
@@ -562,7 +682,7 @@ const options = [
{label: '10s', value: 10},
{label: '15s', value: 15},
{label: '20s', value: 20}
]
])
const tgChatId = ref([])
const tgBotStatus = ref(0)
@@ -574,27 +694,44 @@ const tokenColumnWidth = ref(0)
const ruleType = ref(0)
const ruleEmail = ref([])
getSettings()
settingQuery().then(settingData => {
setting.value = settingData
resendTokenForm.domain = setting.value.domainList[0]
loginOpacity.value = setting.value.loginOpacity
firstLoading.value = false
backgroundUrl.value = setting.value.background?.startsWith('http') ? setting.value.background : ''
editTitle.value = setting.value.title
r2DomainInput.value = setting.value.r2Domain
addVerifyCount.value = setting.value.addVerifyCount
regVerifyCount.value = setting.value.regVerifyCount
})
function getSettings() {
settingQuery().then(settingData => {
setting.value = settingData
settingStore.domainList = settingData.domainList;
resendTokenForm.domain = setting.value.domainList[0]
loginOpacity.value = setting.value.loginOpacity
firstLoading.value = false
backgroundUrl.value = setting.value.background?.startsWith('http') ? setting.value.background : ''
editTitle.value = setting.value.title
r2DomainInput.value = setting.value.r2Domain
addVerifyCount.value = setting.value.addVerifyCount
regVerifyCount.value = setting.value.regVerifyCount
noticeForm.notice = setting.value.notice
noticeForm.noticeContent = setting.value.noticeContent
noticeForm.noticeDuration = setting.value.noticeDuration
noticeForm.noticeTitle = setting.value.noticeTitle
noticeForm.noticePosition = setting.value.noticePosition
noticeForm.noticeType = setting.value.noticeType
noticeForm.noticeOffset = setting.value.noticeOffset
noticeForm.noticeWidth = setting.value.noticeWidth
})
}
function openNoticePopup() {
uiStore.showNotice()
}
function openAddVerifyCount() {
if (settingLoading.value) return
showAddVerifyCount.value = true
addVerifyCountShow.value = true
}
function openRegVerifyCount() {
if (settingLoading.value) return
showRegVerifyCount.value = true
regVerifyCountShow.value = true
}
const resendList = computed(() => {
@@ -659,10 +796,36 @@ function openTgSetting() {
tgSettingShow.value = true
}
function openNoticePopupSetting() {
noticePopupShow.value = true
}
function openResendList() {
showResendList.value = true
}
function resetNoticeForm() {
noticeForm.notice = setting.value.notice
noticeForm.noticeContent = setting.value.noticeContent
noticeForm.noticeDuration = setting.value.noticeDuration
noticeForm.noticeTitle = setting.value.noticeTitle
noticeForm.noticePosition = setting.value.noticePosition
noticeForm.noticeType = setting.value.noticeType
noticeForm.noticeOffset = setting.value.noticeOffset
noticeForm.noticeWidth = setting.value.noticeWidth
}
function saveNoticePopup() {
noticeForm.noticeOffset = noticeForm.noticeOffset || 0
noticeForm.noticeWidth = noticeForm.noticeWidth || 0
noticeForm.noticeDuration = noticeForm.noticeDuration || 0
editSetting({...noticeForm})
}
function previewNoticePopup() {
uiStore.previewNotice({...noticeForm})
}
function openThirdEmailSetting() {
forwardEmail.value = []
forwardStatus.value = setting.value.forwardStatus
@@ -825,7 +988,7 @@ async function saveBackground() {
setting.value.background = key
showSetBackground.value = false
ElMessage({
message: t('changSuccessMsg'),
message: t('saveSuccessMsg'),
type: "success",
plain: true
})
@@ -855,7 +1018,7 @@ function openCut() {
function saveR2domain() {
const settingForm = {r2Domain: r2DomainInput.value}
editSetting(settingForm, true)
editSetting(settingForm)
}
function openResendForm() {
@@ -897,13 +1060,6 @@ function change(e) {
editSetting(settingForm, false)
}
function refresh() {
settingQuery().then(setting => {
settingStore.settings = setting;
settingStore.domainList = setting.domainList;
})
}
function saveTitle() {
editSetting({title: editTitle.value})
}
@@ -922,7 +1078,7 @@ function editSetting(settingForm, refreshStatus = true) {
settingSet(settingForm).then(() => {
settingLoading.value = false
ElMessage({
message: t('changSuccessMsg'),
message: t('saveSuccessMsg'),
type: "success",
plain: true
})
@@ -930,7 +1086,7 @@ function editSetting(settingForm, refreshStatus = true) {
accountStore.currentAccountId = userStore.user.accountId;
}
if (refreshStatus) {
refresh()
getSettings()
}
editTitleShow.value = false
r2DomainShow.value = false
@@ -939,8 +1095,9 @@ function editSetting(settingForm, refreshStatus = true) {
tgSettingShow.value = false
thirdEmailShow.value = false
forwardRulesShow.value = false
showAddVerifyCount.value = false
showRegVerifyCount.value = false
addVerifyCountShow.value = false
regVerifyCountShow.value = false
noticePopupShow.value = false
}).catch((e) => {
loginOpacity.value = setting.value.loginOpacity
setting.value = {...setting.value, ...JSON.parse(backup)}
@@ -1045,7 +1202,7 @@ function editSetting(settingForm, refreshStatus = true) {
display: grid;
grid-template-columns: auto 1fr;
gap: 10px;
font-weight: bold;
font-weight: normal;
> div:first-child {
display: flex;
align-items: center;
@@ -1069,12 +1226,14 @@ function editSetting(settingForm, refreshStatus = true) {
}
.warning {
margin-left: 5px;
margin-left: 4px;
color: gray;
cursor: pointer;
}
.cropper {
border-radius: 4px;
border: 1px solid #D4D7DE;
height: 397px;
width: 705px;
@media (max-width: 767px) {
@@ -1088,6 +1247,26 @@ function editSetting(settingForm, refreshStatus = true) {
justify-content: space-between;
}
.notice-popup-item {
margin-top: 15px;
}
.notice-line-item {
margin-top: 15px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 15px;
> * {
width: 100%;
}
@media (max-width: 840px) {
grid-template-columns: 1fr 1fr;
}
@media (max-width: 580px) {
grid-template-columns: 1fr;
}
}
.background-url {
width: min(calc(100vw - 70px), 500px);
}
@@ -1112,6 +1291,16 @@ function editSetting(settingForm, refreshStatus = true) {
}
}
:deep(.notice-popup.el-dialog) {
min-height: 300px;
width: 820px !important;
@media (max-width: 860px) {
width: calc(100% - 40px) !important;
margin-right: 20px !important;
margin-left: 20px !important;
}
}
:deep(.resend-table .el-dialog__header) {
padding-bottom: 5px;
}
@@ -1257,7 +1446,7 @@ function editSetting(settingForm, refreshStatus = true) {
}
> span:first-child {
font-weight: bold;
font-weight: normal;
padding-right: 20px;
}
}

View File

@@ -502,6 +502,7 @@ function submit() {
function formatSendType(user) {
if (user.sendAction.sendType === 'day') return t('daily')
if (user.sendAction.sendType === 'count') return t('total')
if (user.sendAction.sendType === 'ban') return t('sendBanned')
}
function formatSendCount(user) {
@@ -617,7 +618,7 @@ function httpSetStatus(user) {
userSetStatus({status: status, userId: user.userId}).then(() => {
user.status = status
ElMessage({
message: t('changSuccessMsg'),
message: t('saveSuccessMsg'),
type: "success",
plain: true
})
@@ -630,7 +631,7 @@ function setType() {
chooseUser.type = userForm.type
setTypeShow.value = false
ElMessage({
message: t('changSuccessMsg'),
message: t('saveSuccessMsg'),
type: "success",
plain: true
})
@@ -675,7 +676,7 @@ function updatePwd() {
userSetPwd({password: userForm.password, userId: userForm.userId}).then(() => {
setPwdShow.value = false
ElMessage({
message: t('changSuccessMsg'),
message: t('saveSuccessMsg'),
type: "success",
plain: true
})