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

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

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

View File

@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title></title> <title></title>
<link rel="icon" href="./src/assets/favicon.svg" type="image/svg+xml"> <link rel="icon" href="./src/assets/favicon.svg" type="image/svg+xml">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</head> </head>
<body> <body>

View File

@@ -16,7 +16,7 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dexie": "^4.0.11", "dexie": "^4.0.11",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"element-plus": "^2.9.5", "element-plus": "^2.9.11",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"path": "^0.12.7", "path": "^0.12.7",
"pinia": "^3.0.2", "pinia": "^3.0.2",
@@ -1916,9 +1916,9 @@
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
}, },
"node_modules/element-plus": { "node_modules/element-plus": {
"version": "2.9.7", "version": "2.10.4",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.9.7.tgz", "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.10.4.tgz",
"integrity": "sha512-6vjZh5SXBncLhUwJGTVKS5oDljfgGMh6J4zVTeAZK3YdMUN76FgpvHkwwFXocpJpMbii6rDYU3sgie64FyPerQ==", "integrity": "sha512-UD4elWHrCnp1xlPhbXmVcaKFLCRaRAY6WWRwemGfGW3ceIjXm9fSYc9RNH3AiOEA6Ds1p9ZvhCs76CR9J8Vd+A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ctrl/tinycolor": "^3.4.1", "@ctrl/tinycolor": "^3.4.1",

View File

@@ -18,7 +18,7 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dexie": "^4.0.11", "dexie": "^4.0.11",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"element-plus": "^2.9.5", "element-plus": "^2.9.11",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"path": "^0.12.7", "path": "^0.12.7",
"pinia": "^3.0.2", "pinia": "^3.0.2",

View File

@@ -5,9 +5,11 @@
</template> </template>
<script setup> <script setup>
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { watch } from "vue";
import {useSettingStore} from "@/store/setting.js"; import {useSettingStore} from "@/store/setting.js";
const settingStore = useSettingStore() 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() const { locale } = useI18n()
locale.value = settingStore.lang locale.value = settingStore.lang
watch(() => settingStore.lang, () => locale.value = settingStore.lang)
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,8 +10,19 @@
</div> </div>
</div> </div>
<div class="toolbar"> <div class="toolbar">
<div class="email"> <el-dropdown>
<span>{{ userStore.user.email }}</span> <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> </div>
<el-dropdown :teleported="false" popper-class="detail-dropdown" > <el-dropdown :teleported="false" popper-class="detail-dropdown" >
<div class="avatar"> <div class="avatar">
@@ -28,7 +39,7 @@
<div class="user-name"> <div class="user-name">
{{userStore.user.name}} {{userStore.user.name}}
</div> </div>
<div class="detail-email"> <div class="detail-email" @click="copyEmail(userStore.user.email)">
{{ userStore.user.email }} {{ userStore.user.email }}
</div> </div>
<div class="detail-user-type"> <div class="detail-user-type">
@@ -136,6 +147,32 @@ const sendCount = computed(() => {
return userStore.user.sendCount + '/' + userStore.user.role.sendCount 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() { function openSend() {
uiStore.writerRef.open() uiStore.writerRef.open()
} }
@@ -224,6 +261,7 @@ function formatName(email) {
text-overflow: ellipsis; text-overflow: ellipsis;
text-align: center; text-align: center;
color: #5c5958; color: #5c5958;
cursor: pointer;
} }
.logout { .logout {
margin-top: 20px; margin-top: 20px;
@@ -272,11 +310,11 @@ function formatName(email) {
justify-content: center; justify-content: center;
margin-left: 5px; margin-left: 5px;
.writer { .writer {
width: 36px; width: 34px;
height: 36px; height: 34px;
border-radius: 50%; border-radius: 50%;
color: #ffffff; color: #ffffff;
background: linear-gradient(135deg, #1890ff, #1c6dd0); background: linear-gradient(135deg, #1890ff, #3a80dd);
transition: all 0.3s ease; transition: all 0.3s ease;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -298,38 +336,41 @@ function formatName(email) {
.toolbar { .toolbar {
display: grid; display: flex;
grid-template-columns: 1fr auto auto; justify-content: end;
margin-left: auto; gap: 15px;
@media (max-width: 1024px) { @media (max-width: 767px) {
grid-template-columns: 1fr auto; gap: 10px;
} }
.full { .icon-item {
align-self: center;
width: 30px;
height: 30px;
border-radius: 4px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding-right: 20px;
cursor: pointer; cursor: pointer;
@media (max-width: 1024px) {
display: none;
}
} }
.email {
align-self: center; .icon-item:hover {
font-size: 14px; background: #F0F2F5;
margin-right: 10px; }
overflow: hidden;
white-space: nowrap; .notice {
text-overflow: ellipsis; font-size: 22px;
font-weight: bold; margin-right: 4px;
width: 100%; }
.translate {
padding-top: 2px;
font-size: 21px;
} }
.avatar { .avatar {
display: flex; display: flex;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
margin-left: 10px;
.avatar-text { .avatar-text {
height: 30px; height: 30px;
width: 30px; width: 30px;

View File

@@ -13,23 +13,74 @@
import account from '@/layout/account/index.vue' import account from '@/layout/account/index.vue'
import {useUiStore} from "@/store/ui.js"; import {useUiStore} from "@/store/ui.js";
import {useSettingStore} from "@/store/setting.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 { useRoute } from 'vue-router'
import { hasPerm } from "@/perm/perm.js" import { hasPerm } from "@/perm/perm.js"
const props = defineProps({
openSend: Function
})
const settingStore = useSettingStore() const settingStore = useSettingStore()
const uiStore = useUiStore(); const uiStore = useUiStore();
const route = useRoute() const route = useRoute()
let innerWidth = window.innerWidth let innerWidth = window.innerWidth
let elNotification = null
const accountShow = computed(() => { const accountShow = computed(() => {
return uiStore.accountShow && settingStore.settings.manyEmail === 0 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(() => { onMounted(() => {
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
handleResize() handleResize()
@@ -49,7 +100,6 @@ const handleResize = () => {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.block-show { .block-show {

View File

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

View File

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

View File

@@ -2,3 +2,7 @@ export function isEmail(email) {
const reg = /^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/; const reg = /^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/;
return reg.test(email); 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="1" :label="$t('all')" value="all"/>
<el-option key="3" :label="$t('received')" value="receive"/> <el-option key="3" :label="$t('received')" value="receive"/>
<el-option key="2" :label="$t('sent')" value="send"/> <el-option key="2" :label="$t('sent')" value="send"/>
<el-option key="4" :label="$t('deleted')" value="delete"/> <el-option key="4" :label="$t('selectDeleted')" value="delete"/>
<el-option key="4" :label="$t('noRecipient')" value="noone"/> <el-option key="4" :label="$t('noRecipientTitle')" value="noone"/>
</el-select> </el-select>
<Icon class="icon" icon="iconoir:search" @click="search" width="20" height="20"/> <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" <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 loading from "@/components/loading/index.vue";
import {useRoute} from "vue-router"; import {useRoute} from "vue-router";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import {toUtc, tzDayjs} from "@/utils/day.js";
defineOptions({ defineOptions({
name: 'analysis' name: 'analysis'
@@ -693,7 +692,7 @@ function createSendGauge() {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.number-item { .number-item {
background: #fff; background: var(--el-bg-color);
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--el-border-color); border: 1px solid var(--el-border-color);
padding: 21px 20px; padding: 21px 20px;

View File

@@ -81,7 +81,7 @@
data-after-interactive-callback="loadAfter" data-after-interactive-callback="loadAfter"
data-before-interactive-callback="loadBefore" 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> </div>
<el-button class="btn" type="primary" @click="submitRegister" :loading="registerLoading" <el-button class="btn" type="primary" @click="submitRegister" :loading="registerLoading"
>{{$t('regBtn')}} >{{$t('regBtn')}}
@@ -105,6 +105,7 @@ import {isEmail} from "@/utils/verify-utils.js";
import {useSettingStore} from "@/store/setting.js"; import {useSettingStore} from "@/store/setting.js";
import {useAccountStore} from "@/store/account.js"; import {useAccountStore} from "@/store/account.js";
import {useUserStore} from "@/store/user.js"; import {useUserStore} from "@/store/user.js";
import {useUiStore} from "@/store/ui.js";
import {Icon} from "@iconify/vue"; import {Icon} from "@iconify/vue";
import {cvtR2Url} from "@/utils/convert.js"; import {cvtR2Url} from "@/utils/convert.js";
import {loginUserInfo} from "@/request/my.js"; import {loginUserInfo} from "@/request/my.js";
@@ -114,6 +115,7 @@ import {useI18n} from "vue-i18n";
const { t } = useI18n(); const { t } = useI18n();
const accountStore = useAccountStore(); const accountStore = useAccountStore();
const userStore = useUserStore(); const userStore = useUserStore();
const uiStore = useUiStore();
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const loginLoading = ref(false) const loginLoading = ref(false)
const show = ref('login') const show = ref('login')
@@ -220,6 +222,7 @@ const submit = () => {
router.addRoute('layout', routerData); router.addRoute('layout', routerData);
}); });
await router.replace({name: 'layout'}) await router.replace({name: 'layout'})
uiStore.showNotice()
}).finally(() => { }).finally(() => {
loginLoading.value = false 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-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-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('removeAll')" :value="0" />
<el-radio :label="$t('removeBody')" :value="1" /> <el-radio :label="$t('removeContent')" :value="1" />
</el-radio-group> </el-radio-group>
<el-select <el-select
class="dialog-input" class="dialog-input"
@@ -146,7 +146,7 @@ import loading from '@/components/loading/index.vue';
import {useRoleStore} from "@/store/role.js"; import {useRoleStore} from "@/store/role.js";
import {useUserStore} from "@/store/user.js"; import {useUserStore} from "@/store/user.js";
import {useSettingStore} from "@/store/setting.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"; import {useI18n} from "vue-i18n";
defineOptions({ defineOptions({
@@ -198,7 +198,10 @@ rolePermTree().then(tree => {
treeList.push(...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() { function availDomainChange() {
@@ -218,7 +221,7 @@ function banEmailAddTag(val) {
form.banEmail.splice(form.banEmail.length - 1, 1) form.banEmail.splice(form.banEmail.length - 1, 1)
emails.forEach(email => { emails.forEach(email => {
if (isEmail(email) && !form.banEmail.includes(email)) { if ((isEmail(email) || isDomain(email)) && !form.banEmail.includes(email)) {
form.banEmail.push(email) form.banEmail.push(email)
} }
}) })
@@ -236,7 +239,7 @@ function roleFormClick() {
function setDef(role) { function setDef(role) {
roleSetDef(role.roleId).then(() => { roleSetDef(role.roleId).then(() => {
ElMessage({ ElMessage({
message: t('changSuccessMsg'), message: t('saveSuccessMsg'),
type: "success", type: "success",
plain: true plain: true
}) })
@@ -297,7 +300,7 @@ function setRole() {
permLoading.value = true permLoading.value = true
roleSet(params).then(() => { roleSet(params).then(() => {
ElMessage({ ElMessage({
message: t('changSuccessMsg'), message: t('saveSuccessMsg'),
type: "success", type: "success",
plain: true plain: true
}) })
@@ -340,6 +343,7 @@ function openRoleSet(role) {
form.sendCount = role.sendCount form.sendCount = role.sendCount
form.accountCount = role.accountCount form.accountCount = role.accountCount
form.banEmail = role.banEmail form.banEmail = role.banEmail
form.banEmailType = role.banEmailType
form.availDomain = role.availDomain form.availDomain = role.availDomain
nextTick(() => { nextTick(() => {
tree.value.setCheckedKeys(role.permIds) tree.value.setCheckedKeys(role.permIds)

View File

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

View File

@@ -124,7 +124,7 @@
<div class="card-title">{{$t('emailSetting')}}</div> <div class="card-title">{{$t('emailSetting')}}</div>
<div class="card-content"> <div class="card-content">
<div class="setting-item"> <div class="setting-item">
<div><span>{{$t('receiveEmails')}}</span></div> <div><span>{{$t('receiveEmail')}}</span></div>
<div> <div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1" <el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.receive"/> v-model="setting.receive"/>
@@ -160,6 +160,18 @@
v-model="setting.send"/> v-model="setting.send"/>
</div> </div>
</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 class="setting-item">
<div><span>{{$t('resendToken')}}</span></div> <div><span>{{$t('resendToken')}}</span></div>
<div> <div>
@@ -286,12 +298,35 @@
</div> </div>
</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="settings-card about">
<div class="card-title">{{$t('about')}}</div> <div class="card-title">{{$t('about')}}</div>
<div class="card-content"> <div class="card-content">
<div class="concerning-item"> <div class="concerning-item">
<span>{{$t('version')}} :</span> <span>{{$t('version')}} :</span>
<span>v1.5.0</span> <span>v1.6.0</span>
</div> </div>
<div class="concerning-item"> <div class="concerning-item">
<span>{{$t('community')}} : </span> <span>{{$t('community')}} : </span>
@@ -323,7 +358,7 @@
</div> </div>
<!-- Dialogs remain the same --> <!-- 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> <form>
<el-input type="text" :placeholder="$t('websiteTitle')" v-model="editTitle"/> <el-input type="text" :placeholder="$t('websiteTitle')" v-model="editTitle"/>
<el-button type="primary" :loading="settingLoading" @click="saveTitle">{{$t('save')}}</el-button> <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-column :width="tokenColumnWidth" property="value" label="Token" fixed="right" :show-overflow-tooltip="true" />
</el-table> </el-table>
</el-dialog> </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> <form>
<el-input-number type="text" v-model="regVerifyCount" :min="1" > <el-input-number type="text" v-model="regVerifyCount" :min="1" >
</el-input-number> </el-input-number>
<el-button type="primary" :loading="settingLoading" @click="saveRegVerifyCount">{{$t('save')}}</el-button> <el-button type="primary" :loading="settingLoading" @click="saveRegVerifyCount">{{$t('save')}}</el-button>
</form> </form>
</el-dialog> </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> <form>
<el-input-number type="text" v-model="addVerifyCount" :min="1"/> <el-input-number type="text" v-model="addVerifyCount" :min="1"/>
<el-button type="primary" :loading="settingLoading" @click="saveAddVerifyCount">{{$t('save')}}</el-button> <el-button type="primary" :loading="settingLoading" @click="saveAddVerifyCount">{{$t('save')}}</el-button>
</form> </form>
</el-dialog> </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> </el-scrollbar>
</div> </div>
</template> </template>
@@ -495,6 +601,7 @@
import {computed, defineOptions, reactive, ref} from "vue"; import {computed, defineOptions, reactive, ref} from "vue";
import {physicsDeleteAll, setBackground, settingQuery, settingSet} from "@/request/setting.js"; import {physicsDeleteAll, setBackground, settingQuery, settingSet} from "@/request/setting.js";
import {useSettingStore} from "@/store/setting.js"; import {useSettingStore} from "@/store/setting.js";
import {useUiStore} from "@/store/ui.js";
import {useUserStore} from "@/store/user.js"; import {useUserStore} from "@/store/user.js";
import {useAccountStore} from "@/store/account.js"; import {useAccountStore} from "@/store/account.js";
import {Icon} from "@iconify/vue"; import {Icon} from "@iconify/vue";
@@ -522,10 +629,12 @@ const resendTokenFormShow = ref(false)
const r2DomainShow = ref(false) const r2DomainShow = ref(false)
const turnstileShow = ref(false) const turnstileShow = ref(false)
const tgSettingShow = ref(false) const tgSettingShow = ref(false)
const noticePopupShow = ref(false)
const thirdEmailShow = ref(false) const thirdEmailShow = ref(false)
const forwardRulesShow = ref(false) const forwardRulesShow = ref(false)
const showResendList = ref(false) const showResendList = ref(false)
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const uiStore = useUiStore();
const {settings: setting} = storeToRefs(settingStore); const {settings: setting} = storeToRefs(settingStore);
const editTitle = ref('') const editTitle = ref('')
const settingLoading = ref(false) const settingLoading = ref(false)
@@ -537,8 +646,8 @@ const showSetBackground = ref(false)
let regVerifyCount = ref(1) let regVerifyCount = ref(1)
let addVerifyCount = ref(1) let addVerifyCount = ref(1)
let backup = '{}' let backup = '{}'
const showAddVerifyCount = ref(false) const addVerifyCountShow = ref(false)
const showRegVerifyCount = ref(false) const regVerifyCountShow = ref(false)
const resendTokenForm = reactive({ const resendTokenForm = reactive({
domain: '', domain: '',
token: '', token: '',
@@ -548,13 +657,24 @@ const turnstileForm = reactive({
secretKey: '' 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('enable'), value: 0},
{label: t('disable'), value: 1}, {label: t('disable'), value: 1},
{label: t('optional'), value: 2}, {label: t('optional'), value: 2},
] ])
const options = [ const options = computed(() => [
{label: t('disable'), value: 0}, {label: t('disable'), value: 0},
{label: '3s', value: 3}, {label: '3s', value: 3},
{label: '5s', value: 5}, {label: '5s', value: 5},
@@ -562,7 +682,7 @@ const options = [
{label: '10s', value: 10}, {label: '10s', value: 10},
{label: '15s', value: 15}, {label: '15s', value: 15},
{label: '20s', value: 20} {label: '20s', value: 20}
] ])
const tgChatId = ref([]) const tgChatId = ref([])
const tgBotStatus = ref(0) const tgBotStatus = ref(0)
@@ -574,27 +694,44 @@ const tokenColumnWidth = ref(0)
const ruleType = ref(0) const ruleType = ref(0)
const ruleEmail = ref([]) const ruleEmail = ref([])
getSettings()
settingQuery().then(settingData => { function getSettings() {
setting.value = settingData settingQuery().then(settingData => {
resendTokenForm.domain = setting.value.domainList[0] setting.value = settingData
loginOpacity.value = setting.value.loginOpacity settingStore.domainList = settingData.domainList;
firstLoading.value = false resendTokenForm.domain = setting.value.domainList[0]
backgroundUrl.value = setting.value.background?.startsWith('http') ? setting.value.background : '' loginOpacity.value = setting.value.loginOpacity
editTitle.value = setting.value.title firstLoading.value = false
r2DomainInput.value = setting.value.r2Domain backgroundUrl.value = setting.value.background?.startsWith('http') ? setting.value.background : ''
addVerifyCount.value = setting.value.addVerifyCount editTitle.value = setting.value.title
regVerifyCount.value = setting.value.regVerifyCount 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() { function openAddVerifyCount() {
if (settingLoading.value) return if (settingLoading.value) return
showAddVerifyCount.value = true addVerifyCountShow.value = true
} }
function openRegVerifyCount() { function openRegVerifyCount() {
if (settingLoading.value) return if (settingLoading.value) return
showRegVerifyCount.value = true regVerifyCountShow.value = true
} }
const resendList = computed(() => { const resendList = computed(() => {
@@ -659,10 +796,36 @@ function openTgSetting() {
tgSettingShow.value = true tgSettingShow.value = true
} }
function openNoticePopupSetting() {
noticePopupShow.value = true
}
function openResendList() { function openResendList() {
showResendList.value = true 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() { function openThirdEmailSetting() {
forwardEmail.value = [] forwardEmail.value = []
forwardStatus.value = setting.value.forwardStatus forwardStatus.value = setting.value.forwardStatus
@@ -825,7 +988,7 @@ async function saveBackground() {
setting.value.background = key setting.value.background = key
showSetBackground.value = false showSetBackground.value = false
ElMessage({ ElMessage({
message: t('changSuccessMsg'), message: t('saveSuccessMsg'),
type: "success", type: "success",
plain: true plain: true
}) })
@@ -855,7 +1018,7 @@ function openCut() {
function saveR2domain() { function saveR2domain() {
const settingForm = {r2Domain: r2DomainInput.value} const settingForm = {r2Domain: r2DomainInput.value}
editSetting(settingForm, true) editSetting(settingForm)
} }
function openResendForm() { function openResendForm() {
@@ -897,13 +1060,6 @@ function change(e) {
editSetting(settingForm, false) editSetting(settingForm, false)
} }
function refresh() {
settingQuery().then(setting => {
settingStore.settings = setting;
settingStore.domainList = setting.domainList;
})
}
function saveTitle() { function saveTitle() {
editSetting({title: editTitle.value}) editSetting({title: editTitle.value})
} }
@@ -922,7 +1078,7 @@ function editSetting(settingForm, refreshStatus = true) {
settingSet(settingForm).then(() => { settingSet(settingForm).then(() => {
settingLoading.value = false settingLoading.value = false
ElMessage({ ElMessage({
message: t('changSuccessMsg'), message: t('saveSuccessMsg'),
type: "success", type: "success",
plain: true plain: true
}) })
@@ -930,7 +1086,7 @@ function editSetting(settingForm, refreshStatus = true) {
accountStore.currentAccountId = userStore.user.accountId; accountStore.currentAccountId = userStore.user.accountId;
} }
if (refreshStatus) { if (refreshStatus) {
refresh() getSettings()
} }
editTitleShow.value = false editTitleShow.value = false
r2DomainShow.value = false r2DomainShow.value = false
@@ -939,8 +1095,9 @@ function editSetting(settingForm, refreshStatus = true) {
tgSettingShow.value = false tgSettingShow.value = false
thirdEmailShow.value = false thirdEmailShow.value = false
forwardRulesShow.value = false forwardRulesShow.value = false
showAddVerifyCount.value = false addVerifyCountShow.value = false
showRegVerifyCount.value = false regVerifyCountShow.value = false
noticePopupShow.value = false
}).catch((e) => { }).catch((e) => {
loginOpacity.value = setting.value.loginOpacity loginOpacity.value = setting.value.loginOpacity
setting.value = {...setting.value, ...JSON.parse(backup)} setting.value = {...setting.value, ...JSON.parse(backup)}
@@ -1045,7 +1202,7 @@ function editSetting(settingForm, refreshStatus = true) {
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
gap: 10px; gap: 10px;
font-weight: bold; font-weight: normal;
> div:first-child { > div:first-child {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1069,12 +1226,14 @@ function editSetting(settingForm, refreshStatus = true) {
} }
.warning { .warning {
margin-left: 5px; margin-left: 4px;
color: gray; color: gray;
cursor: pointer; cursor: pointer;
} }
.cropper { .cropper {
border-radius: 4px;
border: 1px solid #D4D7DE;
height: 397px; height: 397px;
width: 705px; width: 705px;
@media (max-width: 767px) { @media (max-width: 767px) {
@@ -1088,6 +1247,26 @@ function editSetting(settingForm, refreshStatus = true) {
justify-content: space-between; 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 { .background-url {
width: min(calc(100vw - 70px), 500px); 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) { :deep(.resend-table .el-dialog__header) {
padding-bottom: 5px; padding-bottom: 5px;
} }
@@ -1257,7 +1446,7 @@ function editSetting(settingForm, refreshStatus = true) {
} }
> span:first-child { > span:first-child {
font-weight: bold; font-weight: normal;
padding-right: 20px; padding-right: 20px;
} }
} }

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,9 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title></title> <title></title>
<link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml"> <link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module" crossorigin src="/assets/index-BwB6muO3.js"></script> <script type="module" crossorigin src="/assets/index-DQO7jFFS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BEDjT-8v.css"> <link rel="stylesheet" crossorigin href="/assets/index-BP2DuLPL.css">
</head> </head>
<body> <body>
<div id="loading-first"> <div id="loading-first">

View File

@@ -103,6 +103,10 @@ export const settingConst = {
ruleType: { ruleType: {
ALL: 0, ALL: 0,
RULE: 1 RULE: 1
},
noRecipient: {
OPEN: 0,
CLOSE: 1,
} }
} }

View File

@@ -11,6 +11,7 @@ import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone'; import timezone from 'dayjs/plugin/timezone';
import roleService from '../service/role-service'; import roleService from '../service/role-service';
import verifyUtils from '../utils/verify-utils';
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
@@ -28,7 +29,8 @@ export async function email(message, env, ctx) {
forwardEmail, forwardEmail,
ruleEmail, ruleEmail,
ruleType, ruleType,
r2Domain r2Domain,
noRecipient
} = await settingService.query({ env }); } = await settingService.query({ env });
if (receive === settingConst.receive.CLOSE) { if (receive === settingConst.receive.CLOSE) {
@@ -47,7 +49,11 @@ export async function email(message, env, ctx) {
const email = await PostalMime.parse(content); const email = await PostalMime.parse(content);
const account = await accountService.selectByEmailIncludeDelNoCase({ env: env }, message.to); const account = await accountService.selectByEmailIncludeDel({ env: env }, message.to);
if (!account && noRecipient === settingConst.noRecipient.CLOSE) {
return;
}
if (account && account.email !== env.admin) { if (account && account.email !== env.admin) {
@@ -61,9 +67,9 @@ export async function email(message, env, ctx) {
for (const item of banEmail) { for (const item of banEmail) {
if (item.startsWith('*@')) { if (verifyUtils.isDomain(item)) {
const banDomain = emailUtils.getDomain(item.toLowerCase()); const banDomain = item.toLowerCase();
const receiveDomain = emailUtils.getDomain(email.from.address.toLowerCase()); const receiveDomain = emailUtils.getDomain(email.from.address.toLowerCase());
if (banDomain === receiveDomain) { if (banDomain === receiveDomain) {

View File

@@ -25,5 +25,14 @@ export const setting = sqliteTable('setting', {
ruleType: integer('rule_type').default(0).notNull(), ruleType: integer('rule_type').default(0).notNull(),
loginOpacity: integer('login_opacity').default(0.88), loginOpacity: integer('login_opacity').default(0.88),
resendTokens: text('resend_tokens').default("{}").notNull(), resendTokens: text('resend_tokens').default("{}").notNull(),
noticeTitle: text('notice_title').default('').notNull(),
noticeContent: text('notice_content').default('').notNull(),
noticeType: text('notice_type').default('').notNull(),
noticeDuration: integer('notice_duration').default(0).notNull(),
noticePosition: text('notice_position').default('').notNull(),
noticeOffset: integer('notice_offset').default(0).notNull(),
noticeWidth: integer('notice_width').default(400).notNull(),
notice: integer('notice').default(0).notNull(),
noRecipient: integer('no_recipient').default(1).notNull()
}); });
export default setting export default setting

View File

@@ -61,38 +61,39 @@ const en = {
JWTMismatch: 'JWT secret mismatch', JWTMismatch: 'JWT secret mismatch',
perms: { perms: {
"邮件": "Email", "邮件": "Email",
"邮件发送": "Send email", "邮件发送": "Send Email",
"邮件删除": "Delete email", "邮件删除": "Delete Email",
"邮箱侧栏": "Account", "邮箱侧栏": "Account",
"邮箱查看": "View account", "邮箱查看": "View Account",
"邮箱添加": "Add account", "邮箱添加": "Add Account",
"邮箱删除": "Delete account", "邮箱删除": "Delete Account",
"个人设置": "Settings", "个人设置": "Settings",
"用户注销": "Delete user", "用户注销": "Delete User",
"分析页": "Analytics", "分析页": "Analytics",
"数据查看": "View data", "数据查看": "View Data",
"用户信息": "All users", "用户信息": "All Users",
"用户查看": "View user", "用户查看": "View User",
"用户添加": "Add user", "用户添加": "Add User",
"密码修改": "Change password", "密码修改": "Change Password",
"状态修改": "Change status", "状态修改": "Change Status",
"权限修改": "Change role", "权限修改": "Change Role",
"用户删除": "Delete user", "用户删除": "Delete User",
"邮件列表": "All mail", "邮件列表": "All Mail",
"邮件查看": "View email", "邮件查看": "View Email",
"权限控制": "Role", "权限控制": "Role",
"身份查看": "View role", "身份添加": "Add Role",
"身份修改": "Change role", "身份查看": "View Role",
"身份删除": "Delete role", "身份修改": "Change Role",
"注册密钥": "Invite code", "身份删除": "Delete Role",
"密钥查看": "View code", "注册密钥": "Invite Code",
"密钥添加": "Add code", "密钥查看": "View Code",
"密钥删除": "Delete code", "密钥添加": "Add Code",
"系统设置": "System settings", "密钥删除": "Delete Code",
"设置查看": "View settings", "系统设置": "System Settings",
"设置修改": "Change settings", "设置查看": "View Settings",
"物理清空": "Physical purge", "设置修改": "Change Settings",
"发件重置": "Reset send count" "物理清空": "Physical Purge",
"发件重置": "Reset Send Count"
} }
}; };

View File

@@ -82,6 +82,7 @@ const zh = {
"邮件查看": "邮件查看", "邮件查看": "邮件查看",
"权限控制": "权限控制", "权限控制": "权限控制",
"身份查看": "身份查看", "身份查看": "身份查看",
"身份添加": "身份添加",
"身份修改": "身份修改", "身份修改": "身份修改",
"身份删除": "身份删除", "身份删除": "身份删除",
"注册密钥": "注册密钥", "注册密钥": "注册密钥",

View File

@@ -2,13 +2,14 @@ import settingService from '../service/setting-service';
import emailUtils from '../utils/email-utils'; import emailUtils from '../utils/email-utils';
import {emailConst} from "../const/entity-const"; import {emailConst} from "../const/entity-const";
import { t } from '../i18n/i18n' import { t } from '../i18n/i18n'
const init = { const init = {
async init(c) { async init(c) {
const secret = c.req.param('secret'); const secret = c.req.param('secret');
if (secret !== c.env.jwt_secret) { if (secret !== c.env.jwt_secret) {
return c.text('jwt_secret 不匹配'); return c.text(t('JWTMismatch'));
} }
await this.intDB(c); await this.intDB(c);
@@ -25,6 +26,19 @@ const init = {
async v1_6DB(c) { async v1_6DB(c) {
const noticeContent = '<div style="color: teal;margin-bottom: 5px;">欢迎使用 Cloud Mail 🎉 </div >\n' +
'本项目仅供学习交流,禁止用于违法业务\n' +
'<br>\n' +
'请遵守当地法规,作者不承担任何法律责任\n' +
'<div style="display: flex;gap: 18px;margin-top: 10px;">\n' +
'<a href="https://github.com/eoao/cloud-mail" target="_blank" >\n' +
'<img src="https://api.iconify.design/codicon:github-inverted.svg" alt="GitHub" width="25" height="25" />\n' +
'</a>\n' +
'<a href="https://t.me/cloud_mail_tg" target="_blank" >\n' +
'<img src="https://api.iconify.design/logos:telegram.svg" alt="GitHub" width="25" height="25" />\n' +
'</a>\n' +
'</div>\n'
const ADD_COLUMN_SQL_LIST = [ const ADD_COLUMN_SQL_LIST = [
`ALTER TABLE setting ADD COLUMN reg_verify_count INTEGER NOT NULL DEFAULT 1;`, `ALTER TABLE setting ADD COLUMN reg_verify_count INTEGER NOT NULL DEFAULT 1;`,
`ALTER TABLE setting ADD COLUMN add_verify_count INTEGER NOT NULL DEFAULT 1;`, `ALTER TABLE setting ADD COLUMN add_verify_count INTEGER NOT NULL DEFAULT 1;`,
@@ -34,16 +48,42 @@ const init = {
count INTEGER NOT NULL DEFAULT 1, count INTEGER NOT NULL DEFAULT 1,
type INTEGER NOT NULL DEFAULT 0, type INTEGER NOT NULL DEFAULT 0,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP update_time DATETIME DEFAULT CURRENT_TIMESTAMP
)` )`,
`ALTER TABLE setting ADD COLUMN notice_title TEXT NOT NULL DEFAULT '公告';`,
`ALTER TABLE setting ADD COLUMN notice_content TEXT NOT NULL DEFAULT '';`,
`ALTER TABLE setting ADD COLUMN notice_type TEXT NOT NULL DEFAULT 'none';`,
`ALTER TABLE setting ADD COLUMN notice_duration INTEGER NOT NULL DEFAULT 0;`,
`ALTER TABLE setting ADD COLUMN notice_offset INTEGER NOT NULL DEFAULT 0;`,
`ALTER TABLE setting ADD COLUMN notice_position TEXT NOT NULL DEFAULT 'top-right';`,
`ALTER TABLE setting ADD COLUMN notice_width INTEGER NOT NULL DEFAULT 340;`,
`ALTER TABLE setting ADD COLUMN notice INTEGER NOT NULL DEFAULT 0;`,
`ALTER TABLE setting ADD COLUMN no_recipient INTEGER NOT NULL DEFAULT 1;`,
`UPDATE role SET avail_domain = '';`,
`UPDATE role SET ban_email = '';`,
`CREATE INDEX IF NOT EXISTS idx_email_user_id_account_id ON email(user_id, account_id);`
]; ];
for (let sql of ADD_COLUMN_SQL_LIST) { const promises = ADD_COLUMN_SQL_LIST.map(async (sql) => {
try { try {
await c.env.db.prepare(sql).run(); await c.env.db.prepare(sql).run();
} catch (e) { } catch (e) {
console.warn(`过字段添加,原因:${e.message}`); console.warn(`过字段,原因:${e.message}`);
} }
});
await Promise.all(promises);
await c.env.db.prepare(`UPDATE setting SET notice_content = ? WHERE notice_content = '';`).bind(noticeContent).run();
try {
await c.env.db.batch([
c.env.db.prepare(`DROP INDEX IF EXISTS idx_account_email`),
c.env.db.prepare(`DROP INDEX IF EXISTS idx_user_email`),
c.env.db.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS idx_account_email_nocase ON account (email COLLATE NOCASE)`),
c.env.db.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS idx_user_email_nocase ON user (email COLLATE NOCASE)`)
]);
} catch (e) {
console.error(e.message)
} }
}, },
async v1_5DB(c) { async v1_5DB(c) {
@@ -97,13 +137,15 @@ const init = {
`ALTER TABLE user ADD COLUMN reg_key_id INTEGER NOT NULL DEFAULT 0;` `ALTER TABLE user ADD COLUMN reg_key_id INTEGER NOT NULL DEFAULT 0;`
]; ];
for (let sql of ADD_COLUMN_SQL_LIST) { const promises = ADD_COLUMN_SQL_LIST.map(async (sql) => {
try { try {
await c.env.db.prepare(sql).run(); await c.env.db.prepare(sql).run();
} catch (e) { } catch (e) {
console.warn(`跳过字段添加,原因:${e.message}`); console.warn(`跳过字段添加,原因:${e.message}`);
} }
} });
await Promise.all(promises);
}, },
@@ -123,13 +165,15 @@ const init = {
`ALTER TABLE setting ADD COLUMN rule_type INTEGER NOT NULL DEFAULT 0;` `ALTER TABLE setting ADD COLUMN rule_type INTEGER NOT NULL DEFAULT 0;`
]; ];
for (let sql of ADD_COLUMN_SQL_LIST) { const promises = ADD_COLUMN_SQL_LIST.map(async (sql) => {
try { try {
await c.env.db.prepare(sql).run(); await c.env.db.prepare(sql).run();
} catch (e) { } catch (e) {
console.warn(`跳过字段添加,原因:${e.message}`); console.warn(`跳过字段添加,原因:${e.message}`);
} }
} });
await Promise.all(promises);
const nameColumn = await c.env.db.prepare(`SELECT * FROM pragma_table_info('email') WHERE name = 'to_email' limit 1`).first(); const nameColumn = await c.env.db.prepare(`SELECT * FROM pragma_table_info('email') WHERE name = 'to_email' limit 1`).first();
@@ -158,13 +202,15 @@ const init = {
`ALTER TABLE email ADD COLUMN relation TEXT NOT NULL DEFAULT '';` `ALTER TABLE email ADD COLUMN relation TEXT NOT NULL DEFAULT '';`
]; ];
for (let sql of ADD_COLUMN_SQL_LIST) { const promises = ADD_COLUMN_SQL_LIST.map(async (sql) => {
try { try {
await c.env.db.prepare(sql).run(); await c.env.db.prepare(sql).run();
} catch (e) { } catch (e) {
console.warn(`跳过字段添加,原因:${e.message}`); console.warn(`跳过字段添加,原因:${e.message}`);
} }
} });
await Promise.all(promises);
await this.receiveEmailToRecipient(c); await this.receiveEmailToRecipient(c);
await this.initAccountName(c); await this.initAccountName(c);
@@ -178,13 +224,6 @@ const init = {
console.warn(`跳过数据,原因:${e.message}`); console.warn(`跳过数据,原因:${e.message}`);
} }
try {
await c.env.db.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS idx_account_email ON account (email)`).run();
await c.env.db.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS idx_user_email ON user (email)`).run();
} catch (e) {
console.warn(`跳过添加唯一邮箱索引,原因:${e.message}`);
}
}, },
async v1_1DB(c) { async v1_1DB(c) {
@@ -215,13 +254,15 @@ const init = {
`ALTER TABLE attachments ADD COLUMN type INTEGER NOT NULL DEFAULT 0;` `ALTER TABLE attachments ADD COLUMN type INTEGER NOT NULL DEFAULT 0;`
]; ];
for (let sql of ADD_COLUMN_SQL_LIST) { const promises = ADD_COLUMN_SQL_LIST.map(async (sql) => {
try { try {
await c.env.db.prepare(sql).run(); await c.env.db.prepare(sql).run();
} catch (e) { } catch (e) {
console.warn(`跳过字段添加,原因:${e.message}`); console.warn(`跳过字段添加,原因:${e.message}`);
} }
} });
await Promise.all(promises);
// 创建 perm 表并初始化 // 创建 perm 表并初始化
await c.env.db.prepare(` await c.env.db.prepare(`
@@ -298,7 +339,7 @@ const init = {
INSERT INTO role ( INSERT INTO role (
role_id, name, key, create_time, sort, description, user_id, is_default, send_count, send_type, account_count role_id, name, key, create_time, sort, description, user_id, is_default, send_count, send_type, account_count
) VALUES ( ) VALUES (
1, '普通用户', NULL, '0000-00-00 00:00:00', 0, '只有普通使用权限', 0, 1, NULL, 'count', 10 1, '普通用户', NULL, '0000-00-00 00:00:00', 0, '只有普通使用权限', 0, 1, NULL, 'ban', 10
) )
`).run(); `).run();
} }
@@ -323,7 +364,8 @@ const init = {
(104, 1, 24), (104, 1, 24),
(105, 1, 4), (105, 1, 4),
(106, 1, 5), (106, 1, 5),
(107, 1, 1) (107, 1, 1),
(108, 1, 3)
`).run(); `).run();
} }
}, },
@@ -468,5 +510,4 @@ const init = {
await c.env.db.batch(queryList); await c.env.db.batch(queryList);
} }
}; };
export default init; export default init;

View File

@@ -40,7 +40,7 @@ const accountService = {
} }
let accountRow = await this.selectByEmailIncludeDelNoCase(c, email); let accountRow = await this.selectByEmailIncludeDel(c, email);
if (accountRow && accountRow.isDel === isDel.DELETE) { if (accountRow && accountRow.isDel === isDel.DELETE) {
throw new BizError(t('isDelAccount')); throw new BizError(t('isDelAccount'));
@@ -92,23 +92,8 @@ const accountService = {
return accountRow; return accountRow;
}, },
selectByEmailIncludeDelNoCase(c, email) {
return orm(c)
.select()
.from(account)
.where(sql`${account.email} COLLATE NOCASE = ${email}`)
.get();
},
selectByEmailIncludeDel(c, email) { selectByEmailIncludeDel(c, email) {
return orm(c).select().from(account).where(eq(account.email, email)).get(); return orm(c).select().from(account).where(sql`${account.email} COLLATE NOCASE = ${email}`).get();
},
selectByEmail(c, email) {
return orm(c).select().from(account).where(
and(
eq(account.email, email),
eq(account.isDel, isDel.NORMAL)))
.get();
}, },
list(c, params, userId) { list(c, params, userId) {

View File

@@ -58,9 +58,9 @@ const emailService = {
) )
.where( .where(
and( and(
timeSort ? gt(email.emailId, emailId) : lt(email.emailId, emailId),
eq(email.accountId, accountId),
eq(email.userId, userId), eq(email.userId, userId),
eq(email.accountId, accountId),
timeSort ? gt(email.emailId, emailId) : lt(email.emailId, emailId),
eq(email.type, type), eq(email.type, type),
eq(email.isDel, isDel.NORMAL) eq(email.isDel, isDel.NORMAL)
) )
@@ -280,7 +280,7 @@ const emailService = {
if (error) { if (error) {
throw new BizError(error.error); throw new BizError(error.message);
} }
html = this.imgReplace(html, null, r2Domain); html = this.imgReplace(html, null, r2Domain);

View File

@@ -68,7 +68,7 @@ const loginService = {
regKeyId = result?.regKeyId regKeyId = result?.regKeyId
} }
const accountRow = await accountService.selectByEmailIncludeDelNoCase(c, email); const accountRow = await accountService.selectByEmailIncludeDel(c, email);
if (accountRow && accountRow.isDel === isDel.DELETE) { if (accountRow && accountRow.isDel === isDel.DELETE) {
throw new BizError(t('isDelUser')); throw new BizError(t('isDelUser'));

View File

@@ -23,11 +23,7 @@ const roleService = {
let roleRow = await orm(c).select().from(role).where(eq(role.name, name)).get(); let roleRow = await orm(c).select().from(role).where(eq(role.name, name)).get();
if (roleRow) { const notEmailIndex = banEmail.findIndex(item => (!verifyUtils.isEmail(item) && !verifyUtils.isDomain(item)))
throw new BizError(t('roleNameExist'));
}
const notEmailIndex = banEmail.findIndex(item => !verifyUtils.isEmail(item))
if (notEmailIndex > -1) { if (notEmailIndex > -1) {
throw new BizError(t('notEmail')); throw new BizError(t('notEmail'));
@@ -76,7 +72,7 @@ const roleService = {
delete params.isDefault delete params.isDefault
const notEmailIndex = banEmail.findIndex(item => !verifyUtils.isEmail(item)) const notEmailIndex = banEmail.findIndex(item => (!verifyUtils.isEmail(item) && !verifyUtils.isDomain(item)))
if (notEmailIndex > -1) { if (notEmailIndex > -1) {
throw new BizError(t('notEmail')); throw new BizError(t('notEmail'));
@@ -168,7 +164,7 @@ const roleService = {
const availIndex = availDomain.findIndex(item => { const availIndex = availDomain.findIndex(item => {
const domain = emailUtils.getDomain(email.toLowerCase()); const domain = emailUtils.getDomain(email.toLowerCase());
const availDomainItem = emailUtils.getDomain(item.toLowerCase()); const availDomainItem = item.toLowerCase();
console.log(domain,availDomainItem) console.log(domain,availDomainItem)
return domain === availDomainItem return domain === availDomainItem
}) })

View File

@@ -139,7 +139,15 @@ const settingService = {
domainList:settingRow.domainList, domainList:settingRow.domainList,
regKey: settingRow.regKey, regKey: settingRow.regKey,
regVerifyOpen: settingRow.regVerifyOpen, regVerifyOpen: settingRow.regVerifyOpen,
addVerifyOpen: settingRow.addVerifyOpen addVerifyOpen: settingRow.addVerifyOpen,
noticeTitle: settingRow.noticeTitle,
noticeContent: settingRow.noticeContent,
noticeType: settingRow.noticeType,
noticeDuration: settingRow.noticeDuration,
noticePosition: settingRow.noticePosition,
noticeWidth: settingRow.noticeWidth,
noticeOffset: settingRow.noticeOffset,
notice: settingRow.notice,
}; };
} }
}; };

View File

@@ -71,7 +71,7 @@ const userService = {
}, },
selectByEmailIncludeDel(c, email) { selectByEmailIncludeDel(c, email) {
return orm(c).select().from(user).where(eq(user.email, email)).get(); return orm(c).select().from(user).where(sql`${user.email} COLLATE NOCASE = ${email}`).get();
}, },
selectById(c, userId) { selectById(c, userId) {
@@ -355,6 +355,8 @@ const userService = {
const userId = await userService.insert(c, { email, password: hash, salt, type }); const userId = await userService.insert(c, { email, password: hash, salt, type });
await userService.updateUserInfo(c, userId, true);
await accountService.insert(c, { userId: userId, email, type, name: emailUtils.getName(email) }); await accountService.insert(c, { userId: userId, email, type, name: emailUtils.getName(email) });
}, },

View File

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

View File

@@ -1,6 +1,6 @@
name = "cloud-mail" name = "cloud-mail"
main = "src/index.js" main = "src/index.js"
compatibility_date = "2025-04-09" compatibility_date = "2025-07-29"
keep_vars = true keep_vars = true
[observability] [observability]

View File

@@ -1,6 +1,6 @@
name = "cloud-mail-dev" name = "cloud-mail-dev"
main = "src/index.js" main = "src/index.js"
compatibility_date = "2025-04-09" compatibility_date = "2025-07-29"
keep_vars = true keep_vars = true
[observability] [observability]
@@ -32,7 +32,7 @@ run_worker_first = true
crons = ["0 16 * * *"] crons = ["0 16 * * *"]
[vars] [vars]
orm_log = false orm_log = true
domain = ["example.com", "example2.com", "example3.com", "example4.com"] domain = ["example.com", "example2.com", "example3.com", "example4.com"]
admin = "admin@example.com" admin = "admin@example.com"
jwt_secret = "b7f29a1d-18e2-4d3b-941f-f6b2c97c02fd" jwt_secret = "b7f29a1d-18e2-4d3b-941f-f6b2c97c02fd"

View File

@@ -1,6 +1,6 @@
name = "cloud-mail-test" name = "cloud-mail-test"
main = "src/index.js" main = "src/index.js"
compatibility_date = "2025-04-09" compatibility_date = "2025-07-29"
keep_vars = true keep_vars = true
[observability] [observability]

View File

@@ -1,6 +1,6 @@
name = "cloud-mail" name = "cloud-mail"
main = "src/index.js" main = "src/index.js"
compatibility_date = "2025-04-09" compatibility_date = "2025-07-29"
keep_vars = true keep_vars = true
[observability] [observability]