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

This commit is contained in:
eoao
2025-08-03 21:03:54 +08:00
parent cbefc1d6f2
commit 39da16d2fd
43 changed files with 857 additions and 474 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
NODE_ENV = 'remote'
VITE_APP_TITLE = '远程环境'
VITE_BASE_URL = 'xxxxxx'
VITE_BASE_URL = 'https://mornglow.top/api'
+1
View File
@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title></title>
<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>
</head>
<body>
+4 -4
View File
@@ -16,7 +16,7 @@
"dayjs": "^1.11.13",
"dexie": "^4.0.11",
"echarts": "^5.6.0",
"element-plus": "^2.9.5",
"element-plus": "^2.9.11",
"lodash-es": "^4.17.21",
"path": "^0.12.7",
"pinia": "^3.0.2",
@@ -1916,9 +1916,9 @@
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
},
"node_modules/element-plus": {
"version": "2.9.7",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.9.7.tgz",
"integrity": "sha512-6vjZh5SXBncLhUwJGTVKS5oDljfgGMh6J4zVTeAZK3YdMUN76FgpvHkwwFXocpJpMbii6rDYU3sgie64FyPerQ==",
"version": "2.10.4",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.10.4.tgz",
"integrity": "sha512-UD4elWHrCnp1xlPhbXmVcaKFLCRaRAY6WWRwemGfGW3ceIjXm9fSYc9RNH3AiOEA6Ds1p9ZvhCs76CR9J8Vd+A==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
+1 -1
View File
@@ -18,7 +18,7 @@
"dayjs": "^1.11.13",
"dexie": "^4.0.11",
"echarts": "^5.6.0",
"element-plus": "^2.9.5",
"element-plus": "^2.9.11",
"lodash-es": "^4.17.21",
"path": "^0.12.7",
"pinia": "^3.0.2",
+3 -1
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>
+75 -56
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
+25 -5
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
+3 -3
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
})
+1 -1
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) {
+67 -26
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;
+56 -6
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 {
+12
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'],
},
+4 -4
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
+4
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);
}
+2 -2
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"
+1 -2
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;
+4 -1
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
})
+10 -6
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)
+3 -26
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,
})
+229 -40
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;
}
}
+4 -3
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
})
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
+3 -2
View File
@@ -5,9 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title></title>
<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 type="module" crossorigin src="/assets/index-BwB6muO3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BEDjT-8v.css">
<script type="module" crossorigin src="/assets/index-DQO7jFFS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BP2DuLPL.css">
</head>
<body>
<div id="loading-first">
+4
View File
@@ -103,6 +103,10 @@ export const settingConst = {
ruleType: {
ALL: 0,
RULE: 1
},
noRecipient: {
OPEN: 0,
CLOSE: 1,
}
}
+10 -4
View File
@@ -11,6 +11,7 @@ import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import roleService from '../service/role-service';
import verifyUtils from '../utils/verify-utils';
dayjs.extend(utc);
dayjs.extend(timezone);
@@ -28,7 +29,8 @@ export async function email(message, env, ctx) {
forwardEmail,
ruleEmail,
ruleType,
r2Domain
r2Domain,
noRecipient
} = await settingService.query({ env });
if (receive === settingConst.receive.CLOSE) {
@@ -47,7 +49,11 @@ export async function email(message, env, ctx) {
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) {
@@ -61,9 +67,9 @@ export async function email(message, env, ctx) {
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());
if (banDomain === receiveDomain) {
+9
View File
@@ -25,5 +25,14 @@ export const setting = sqliteTable('setting', {
ruleType: integer('rule_type').default(0).notNull(),
loginOpacity: integer('login_opacity').default(0.88),
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
+29 -28
View File
@@ -61,38 +61,39 @@ const en = {
JWTMismatch: 'JWT secret mismatch',
perms: {
"邮件": "Email",
"邮件发送": "Send email",
"邮件删除": "Delete email",
"邮件发送": "Send Email",
"邮件删除": "Delete Email",
"邮箱侧栏": "Account",
"邮箱查看": "View account",
"邮箱添加": "Add account",
"邮箱删除": "Delete account",
"邮箱查看": "View Account",
"邮箱添加": "Add Account",
"邮箱删除": "Delete Account",
"个人设置": "Settings",
"用户注销": "Delete user",
"用户注销": "Delete User",
"分析页": "Analytics",
"数据查看": "View data",
"用户信息": "All users",
"用户查看": "View user",
"用户添加": "Add user",
"密码修改": "Change password",
"状态修改": "Change status",
"权限修改": "Change role",
"用户删除": "Delete user",
"邮件列表": "All mail",
"邮件查看": "View email",
"数据查看": "View Data",
"用户信息": "All Users",
"用户查看": "View User",
"用户添加": "Add User",
"密码修改": "Change Password",
"状态修改": "Change Status",
"权限修改": "Change Role",
"用户删除": "Delete User",
"邮件列表": "All Mail",
"邮件查看": "View Email",
"权限控制": "Role",
"身份查看": "View role",
"身份修改": "Change role",
"身份删除": "Delete role",
"注册密钥": "Invite code",
"密钥查看": "View code",
"密钥添加": "Add code",
"密钥删除": "Delete code",
"系统设置": "System settings",
"设置查看": "View settings",
"设置修改": "Change settings",
"物理清空": "Physical purge",
"发件重置": "Reset send count"
"身份添加": "Add Role",
"身份查看": "View Role",
"身份修改": "Change Role",
"身份删除": "Delete Role",
"注册密钥": "Invite Code",
"密钥查看": "View Code",
"密钥添加": "Add Code",
"密钥删除": "Delete Code",
"系统设置": "System Settings",
"设置查看": "View Settings",
"设置修改": "Change Settings",
"物理清空": "Physical Purge",
"发件重置": "Reset Send Count"
}
};
+1
View File
@@ -82,6 +82,7 @@ const zh = {
"邮件查看": "邮件查看",
"权限控制": "权限控制",
"身份查看": "身份查看",
"身份添加": "身份添加",
"身份修改": "身份修改",
"身份删除": "身份删除",
"注册密钥": "注册密钥",
+63 -22
View File
@@ -2,13 +2,14 @@ import settingService from '../service/setting-service';
import emailUtils from '../utils/email-utils';
import {emailConst} from "../const/entity-const";
import { t } from '../i18n/i18n'
const init = {
async init(c) {
const secret = c.req.param('secret');
if (secret !== c.env.jwt_secret) {
return c.text('jwt_secret 不匹配');
return c.text(t('JWTMismatch'));
}
await this.intDB(c);
@@ -25,6 +26,19 @@ const init = {
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 = [
`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;`,
@@ -34,16 +48,42 @@ const init = {
count INTEGER NOT NULL DEFAULT 1,
type INTEGER NOT NULL DEFAULT 0,
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 {
await c.env.db.prepare(sql).run();
} 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) {
@@ -97,13 +137,15 @@ const init = {
`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 {
await c.env.db.prepare(sql).run();
} catch (e) {
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;`
];
for (let sql of ADD_COLUMN_SQL_LIST) {
const promises = ADD_COLUMN_SQL_LIST.map(async (sql) => {
try {
await c.env.db.prepare(sql).run();
} catch (e) {
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();
@@ -158,13 +202,15 @@ const init = {
`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 {
await c.env.db.prepare(sql).run();
} catch (e) {
console.warn(`跳过字段添加,原因:${e.message}`);
}
}
});
await Promise.all(promises);
await this.receiveEmailToRecipient(c);
await this.initAccountName(c);
@@ -178,13 +224,6 @@ const init = {
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) {
@@ -215,13 +254,15 @@ const init = {
`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 {
await c.env.db.prepare(sql).run();
} catch (e) {
console.warn(`跳过字段添加,原因:${e.message}`);
}
}
});
await Promise.all(promises);
// 创建 perm 表并初始化
await c.env.db.prepare(`
@@ -298,7 +339,7 @@ const init = {
INSERT INTO role (
role_id, name, key, create_time, sort, description, user_id, is_default, send_count, send_type, account_count
) 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();
}
@@ -323,7 +364,8 @@ const init = {
(104, 1, 24),
(105, 1, 4),
(106, 1, 5),
(107, 1, 1)
(107, 1, 1),
(108, 1, 3)
`).run();
}
},
@@ -468,5 +510,4 @@ const init = {
await c.env.db.batch(queryList);
}
};
export default init;
+2 -17
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) {
throw new BizError(t('isDelAccount'));
@@ -92,23 +92,8 @@ const accountService = {
return accountRow;
},
selectByEmailIncludeDelNoCase(c, email) {
return orm(c)
.select()
.from(account)
.where(sql`${account.email} COLLATE NOCASE = ${email}`)
.get();
},
selectByEmailIncludeDel(c, email) {
return orm(c).select().from(account).where(eq(account.email, email)).get();
},
selectByEmail(c, email) {
return orm(c).select().from(account).where(
and(
eq(account.email, email),
eq(account.isDel, isDel.NORMAL)))
.get();
return orm(c).select().from(account).where(sql`${account.email} COLLATE NOCASE = ${email}`).get();
},
list(c, params, userId) {
+3 -3
View File
@@ -58,9 +58,9 @@ const emailService = {
)
.where(
and(
timeSort ? gt(email.emailId, emailId) : lt(email.emailId, emailId),
eq(email.accountId, accountId),
eq(email.userId, userId),
eq(email.accountId, accountId),
timeSort ? gt(email.emailId, emailId) : lt(email.emailId, emailId),
eq(email.type, type),
eq(email.isDel, isDel.NORMAL)
)
@@ -280,7 +280,7 @@ const emailService = {
if (error) {
throw new BizError(error.error);
throw new BizError(error.message);
}
html = this.imgReplace(html, null, r2Domain);
+1 -1
View File
@@ -68,7 +68,7 @@ const loginService = {
regKeyId = result?.regKeyId
}
const accountRow = await accountService.selectByEmailIncludeDelNoCase(c, email);
const accountRow = await accountService.selectByEmailIncludeDel(c, email);
if (accountRow && accountRow.isDel === isDel.DELETE) {
throw new BizError(t('isDelUser'));
+3 -7
View File
@@ -23,11 +23,7 @@ const roleService = {
let roleRow = await orm(c).select().from(role).where(eq(role.name, name)).get();
if (roleRow) {
throw new BizError(t('roleNameExist'));
}
const notEmailIndex = banEmail.findIndex(item => !verifyUtils.isEmail(item))
const notEmailIndex = banEmail.findIndex(item => (!verifyUtils.isEmail(item) && !verifyUtils.isDomain(item)))
if (notEmailIndex > -1) {
throw new BizError(t('notEmail'));
@@ -76,7 +72,7 @@ const roleService = {
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) {
throw new BizError(t('notEmail'));
@@ -168,7 +164,7 @@ const roleService = {
const availIndex = availDomain.findIndex(item => {
const domain = emailUtils.getDomain(email.toLowerCase());
const availDomainItem = emailUtils.getDomain(item.toLowerCase());
const availDomainItem = item.toLowerCase();
console.log(domain,availDomainItem)
return domain === availDomainItem
})
+9 -1
View File
@@ -139,7 +139,15 @@ const settingService = {
domainList:settingRow.domainList,
regKey: settingRow.regKey,
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,
};
}
};
+3 -1
View File
@@ -71,7 +71,7 @@ const userService = {
},
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) {
@@ -355,6 +355,8 @@ const userService = {
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) });
},
+3
View File
@@ -1,6 +1,9 @@
const verifyUtils = {
isEmail(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);
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
name = "cloud-mail"
main = "src/index.js"
compatibility_date = "2025-04-09"
compatibility_date = "2025-07-29"
keep_vars = true
[observability]
@@ -33,4 +33,4 @@ crons = ["0 16 * * *"] #定时任务每天晚上12点执行
#orm_log = false
#domain = [] #邮件域名可可配置多个 示例: ["example1.com","example2.com"]
#admin = "" #管理员的邮箱 示例: admin@example.com
#jwt_secret = "" #jwt令牌的密钥,随便填一串字符串
#jwt_secret = "" #jwt令牌的密钥,随便填一串字符串
+2 -2
View File
@@ -1,6 +1,6 @@
name = "cloud-mail-dev"
main = "src/index.js"
compatibility_date = "2025-04-09"
compatibility_date = "2025-07-29"
keep_vars = true
[observability]
@@ -32,7 +32,7 @@ run_worker_first = true
crons = ["0 16 * * *"]
[vars]
orm_log = false
orm_log = true
domain = ["example.com", "example2.com", "example3.com", "example4.com"]
admin = "admin@example.com"
jwt_secret = "b7f29a1d-18e2-4d3b-941f-f6b2c97c02fd"
+1 -1
View File
@@ -1,6 +1,6 @@
name = "cloud-mail-test"
main = "src/index.js"
compatibility_date = "2025-04-09"
compatibility_date = "2025-07-29"
keep_vars = true
[observability]
+1 -1
View File
@@ -1,6 +1,6 @@
name = "cloud-mail"
main = "src/index.js"
compatibility_date = "2025-04-09"
compatibility_date = "2025-07-29"
keep_vars = true
[observability]