新增中英文切换

This commit is contained in:
eoao
2025-07-22 22:02:23 +08:00
parent 249edb9e19
commit bbcff4908d
64 changed files with 1951 additions and 1298 deletions

View File

@@ -21,9 +21,9 @@
"path": "^0.12.7",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0",
"screenfull": "^6.0.2",
"vue": "^3.5.13",
"vue-cropper": "^1.1.4",
"vue-i18n": "^11.1.10",
"vue-router": "^4.5.0"
},
"devDependencies": {
@@ -571,6 +571,50 @@
"vue": ">=3"
}
},
"node_modules/@intlify/core-base": {
"version": "11.1.10",
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.1.10.tgz",
"integrity": "sha512-JhRb40hD93Vk0BgMgDc/xMIFtdXPHoytzeK6VafBNOj6bb6oUZrGamXkBKecMsmGvDQQaPRGG2zpa25VCw8pyw==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "11.1.10",
"@intlify/shared": "11.1.10"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.1.10",
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.1.10.tgz",
"integrity": "sha512-TABl3c8tSLWbcD+jkQTyBhrnW251dzqW39MPgEUCsd69Ua3ceoimsbIzvkcPzzZvt1QDxNkenMht+5//V3JvLQ==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.1.10",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "11.1.10",
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.1.10.tgz",
"integrity": "sha512-6ZW/f3Zzjxfa1Wh0tYQI5pLKUtU+SY7l70pEG+0yd0zjcsYcK0EBt6Fz30Dy0tZhEqemziQQy2aNU3GJzyrMUA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
@@ -3186,17 +3230,6 @@
"license": "ISC",
"optional": true
},
"node_modules/screenfull": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/screenfull/-/screenfull-6.0.2.tgz",
"integrity": "sha512-AQdy8s4WhNvUZ6P8F6PB21tSPIYKniic+Ogx0AacBMjKP1GUHN2E9URxQHtCusiwxudnCKkdy4GrHXPPJSkCCw==",
"engines": {
"node": "^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/scule": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz",
@@ -3815,6 +3848,26 @@
"integrity": "sha512-5m98vBsCEI9rbS4JxELxXidtAui3qNyTHLHg67Qbn7g8cg+E6LcnC+hh3SM/p94x6mFh6KRxT1ttnta+wCYqWA==",
"license": "ISC"
},
"node_modules/vue-i18n": {
"version": "11.1.10",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.1.10.tgz",
"integrity": "sha512-C+IwnSg8QDSOAox0gdFYP5tsKLx5jNWxiawNoiNB/Tw4CReXmM1VJMXbduhbrEzAFLhreqzfDocuSVjGbxQrag==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.1.10",
"@intlify/shared": "11.1.10",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-router": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz",

View File

@@ -23,9 +23,9 @@
"path": "^0.12.7",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0",
"screenfull": "^6.0.2",
"vue": "^3.5.13",
"vue-cropper": "^1.1.4",
"vue-i18n": "^11.1.10",
"vue-router": "^4.5.0"
},
"devDependencies": {

View File

@@ -1,9 +1,13 @@
<template>
<el-config-provider :locale="locale">
<router-view/>
<el-config-provider :locale="settingStore.lang === 'zh' ? zhCn : null">
<router-view />
</el-config-provider>
</template>
<script setup>
import zhCn from 'element-plus/es/locale/lang/zh-cn'
let locale=zhCn
import { useI18n } from "vue-i18n";
import {useSettingStore} from "@/store/setting.js";
const settingStore = useSettingStore()
import zhCn from 'element-plus/es/locale/lang/zh-cn';
const { locale } = useI18n()
locale.value = settingStore.lang
</script>

View File

@@ -1,12 +1,16 @@
import axios from "axios";
import router from "@/router";
import i18n from "@/i18n/index.js";
import {useSettingStore} from "@/store/setting.js";
let http = axios.create({
baseURL: import.meta.env.VITE_BASE_URL
});
http.interceptors.request.use(config => {
const { lang } = useSettingStore();
config.headers.Authorization = `${localStorage.getItem('token')}`
config.headers['accept-language'] = lang
return config
})
@@ -62,7 +66,7 @@ http.interceptors.response.use((res) => {
return Promise.reject(error)
} else if (error.message.includes('Network Error')) {
ElMessage({
message: '网络错误,请检查网络连接',
message: i18n.global.t('networkErrorMsg'),
type: 'error',
plain: true,
grouping: true,
@@ -70,7 +74,7 @@ http.interceptors.response.use((res) => {
})
} else if (error.code === 'ECONNABORTED') {
ElMessage({
message: '请求超时,请稍后重试',
message: i18n.global.t('timeoutErrorMsg'),
type: 'error',
plain: true,
grouping: true
@@ -78,7 +82,7 @@ http.interceptors.response.use((res) => {
ElMessage.error('')
} else if (error.response) {
ElMessage({
message: `服务器繁忙`,
message: i18n.global.t('serverBusyErrorMsg'),
type: 'error',
plain: true,
grouping: true,
@@ -86,7 +90,7 @@ http.interceptors.response.use((res) => {
})
} else {
ElMessage({
message: '请求失败,请稍后再试',
message: i18n.global.t('reqFailErrorMsg'),
type: 'error',
plain: true,
grouping: true,

View File

@@ -18,7 +18,7 @@
</div>
<div class="header-right">
<span class="email-count" v-if="total"> {{ total }} </span>
<span class="email-count" v-if="total">{{$t('emailCount', {total: total})}}</span>
<Icon v-if="showAccountIcon" class="more-icon icon" width="16" height="16" icon="akar-icons:dot-grid-fill"
@click="changeAccountShow"/>
</div>
@@ -33,7 +33,7 @@
:data-checked="item.checked"
@click="jumpDetails(item)"
>
<el-checkbox :class=" props.type === 'sys-email' ? 'sys-email-checkbox' : 'checkbox'" v-model="item.checked" @click.stop></el-checkbox>
<el-checkbox :class=" props.type === 'all-email' ? 'all-email-checkbox' : 'checkbox'" v-model="item.checked" @click.stop></el-checkbox>
<div @click.stop="starChange(item)" class="pc-star" v-if="showStar">
<Icon v-if="item.isStar" icon="fluent-color:star-16" width="20" height="20"/>
<Icon v-else icon="solar:star-line-duotone" width="18" height="18"/>
@@ -45,7 +45,7 @@
<div class="email-status" v-if="showStatus">
<el-tooltip v-if="item.status === 0"
effect="dark"
content="已接收"
:content="$t('received')"
>
<Icon icon="ic:round-mark-email-read" style="color: #67C23A" width="20" height="20"/>
/>
@@ -53,7 +53,7 @@
</el-tooltip>
<el-tooltip v-if="item.status === 1"
effect="dark"
content="已发送"
:content="$t('sent')"
>
<Icon icon="bi:send-arrow-up-fill" style="color: #67C23A" width="20" height="20"
/>
@@ -61,7 +61,7 @@
</el-tooltip>
<el-tooltip v-if="item.status === 2"
effect="dark"
content="发送成功"
:content="$t('delivered')"
>
<Icon icon="bi:send-check-fill" style="color: #67C23A" width="20"
height="20"/>
@@ -69,7 +69,7 @@
</el-tooltip>
<el-tooltip v-if="item.status === 3"
effect="dark"
content="发送失败"
:content="$t('bounced')"
>
<Icon icon="bi:send-x-fill" style="color: #F56C6C" width="20"
height="20"/>
@@ -77,7 +77,7 @@
</el-tooltip>
<el-tooltip v-if="item.status === 4"
effect="dark"
content="被标记垃圾邮件"
:content="$t('complained')"
>
<Icon icon="bi:send-exclamation-fill" style="color:#FBBD08" width="20"
height="20"/>
@@ -85,7 +85,7 @@
</el-tooltip>
<el-tooltip v-if="item.status === 5"
effect="dark"
content="发送延迟"
:content="$t('delayed')"
>
<Icon icon="bi:send-arrow-up-fill" style="color:#FBBD08" width="20"
height="20"/>
@@ -93,7 +93,7 @@
</el-tooltip>
<el-tooltip v-if="item.status === 7"
effect="dark"
content="无人收件"
:content="$t('NoRecipient')"
>
<Icon icon="ic:round-mark-email-read" style="color:#FBBD08" width="20"
height="20"/>
@@ -134,7 +134,7 @@
<span>{{ item.type === 0 ? item.toEmail : item.sendEmail }}</span>
</div>
<div class="del-status" v-if="item.isDel">
<el-tag type="danger" size="small">已删除</el-tag>
<el-tag type="danger" size="small">{{$t('deleted')}}</el-tag>
</div>
</div>
</div>
@@ -151,10 +151,10 @@
<Loading/>
</div>
<div class="noLoading" v-if="noLoading && emailList.length > 0">
<div>没有更多数据了</div>
<div>{{$t('noMoreData')}}</div>
</div>
<div class="empty" v-if="noLoading && emailList.length === 0">
<el-empty :image-size="isMobile ? 120 : 0" description="没有任何邮件"/>
<el-empty :image-size="isMobile ? 120 : 0" :description="$t('noMessagesFound')"/>
</div>
</div>
</el-scrollbar>
@@ -172,6 +172,7 @@ import {useEmailStore} from "@/store/email.js";
import {useUiStore} from "@/store/ui.js";
import {useSettingStore} from "@/store/setting.js";
import {fromNow} from "@/utils/day.js";
import {useI18n} from "vue-i18n";
const props = defineProps({
getEmailList: Function,
@@ -216,7 +217,7 @@ const props = defineProps({
const emit = defineEmits(['jump', 'refresh-before', 'delete-draft'])
const { t } = useI18n()
const settingStore = useSettingStore()
const uiStore = useUiStore();
const emailStore = useEmailStore();
@@ -260,7 +261,6 @@ onBeforeRouteLeave(() => {
scrollTop = scroll.value.scrollTop
})
watch(
() => emailList.map(item => item.checked),
() => {
@@ -282,7 +282,6 @@ watch(() => emailStore.cancelStarEmailId, () => {
emailList.forEach(email => {
if (email.emailId === emailStore.cancelStarEmailId) {
email.isStar = 0
console.log('取消')
}
})
})
@@ -366,9 +365,9 @@ function changeAccountShow() {
}
const handleDelete = () => {
ElMessageBox.confirm('确认批量删除这些邮件吗?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
ElMessageBox.confirm(t('delEmailsConfirm'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
@@ -381,7 +380,7 @@ const handleDelete = () => {
const emailIds = getSelectedMailsIds();
props.emailDelete(emailIds).then(() => {
ElMessage({
message: '删除成功',
message: t('delSuccessMsg'),
type: 'success',
plain: true
})
@@ -651,7 +650,7 @@ function loadData() {
justify-content: center;
}
.sys-email-checkbox {
.all-email-checkbox {
display: flex;
padding-left: 15px;
padding-right: 20px;

View File

@@ -3,15 +3,15 @@
<Icon icon="line-md:loading-loop" width="18" height="18" />
<div>
<span>{{value}}%</span>
<span style="margin-left: 5px">邮件正在发送中</span>
<span style="margin-left: 5px">{{desc}}</span>
</div>
</div>
</template>
<script setup>
import {Icon} from "@iconify/vue";
defineProps({
value: [Number,String]
value: [Number,String],
desc: [String]
})
</script>

View File

@@ -1,21 +0,0 @@
import { useUserStore } from "@/store/user.js";
export default {
mounted(el, binding) {
const userStore = useUserStore();
const permKeys = userStore.user.permKeys;
const value = binding.value;
if (permKeys.includes('*')) {
return;
}
const hasPermission = Array.isArray(value)
? value.some(key => permKeys.includes(key))
: permKeys.includes(value);
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el);
}
}
}

252
mail-vue/src/i18n/en.js Normal file
View File

@@ -0,0 +1,252 @@
const en = {
inbox: 'Inbox',
drafts: 'Drafts',
sent: 'Sent',
starred: 'Starred',
settings: 'Settings',
analytics: 'Analytics',
allUsers: 'All users',
allMail: 'All mail',
permissions: 'Role',
inviteCode: 'Invite code',
SystemSettings: 'System settings',
noMoreData: 'No more data',
noMessagesFound: 'No messages found',
addAccount: 'Add account',
emailAccount: 'Email',
deleteUser: 'Delete account',
deleteUserBtn: 'Delete',
changePassword: 'Change password',
newPassword: 'New password',
confirmPassword: 'Confirm password',
add: 'Add',
manage: 'Manage',
rename: 'Rename',
delete: 'Delete',
save: 'Save',
profile: 'Profile',
change: 'Change',
changePwdBtn: 'Change',
username: 'Username',
password: 'Password',
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',
deleted: 'Deleted',
active: 'Active',
emailSource: 'Email source',
userGrowth: 'User growth',
emailGrowth: 'Email growth',
emailSent: 'Sent',
emailReceived: 'Received',
sentToday: 'Sent today',
total: 'Total',
growthTotalUsers: 'Total users',
searchByEmail: 'Enter email to search',
tabEmailAddress: 'Email',
tabReceived: 'Received',
tabSent: 'Sent',
tabMailboxes: 'Accounts',
tabRegisteredAt: 'Registered at',
tabStatus: 'Status',
tabRole: 'Role',
roleName: 'Name',
role: 'Name',
all: 'All',
normal: 'Normal',
banned: 'Banned',
reset: 'Reset',
restore: 'Restore',
tabSetting: 'Settings',
registrationIp: 'Registration IP',
recentIP: 'Recent IP',
recentActivity: 'Recent activity',
loginDevice: 'Login device',
loginSystem: 'Login system',
browserLogin: 'Browser Login',
noPerm: 'Unauthorized',
noLimit: 'Unlimited',
blocked: 'Blocked',
sendCount: 'Send email : ',
accountCount: 'Add account : ',
action: 'Action',
chgPwd: 'Pwd',
perm: 'Role',
btnBan: 'Ban',
admin: 'Admin',
addUser: 'Add user',
select: 'Select',
unknown: 'Unknown',
changePerm: 'Change role',
from: 'From',
subject: 'Subject',
sender: 'Sender',
user: 'User',
searchByContent: 'Enter text to search',
noRecipient: 'No recipient',
received: 'Received',
selectEmail: 'Email',
order: 'Order',
default: 'Default',
description: 'Description',
removeBody: 'Remove body',
removeAll: 'Remove all',
expand: 'Expand',
collapse: 'Collapse',
daily: 'Daily',
emailBlock: '*Intercept all incoming emails to example.com',
searchRegKeyDesc: 'Enter invite code to search',
remainingUses: 'Remaining uses',
exhausted: 'Exhausted',
validUntil: 'Valid until',
expired: 'Expired',
copy: 'Copy',
history: 'History',
addRegKey: 'Add invite code',
regKey: 'Invite code',
noCodeFound: 'No messages found',
useHistory: 'Usage history',
date: 'Date',
roleDesc: 'Role',
noSubject: 'No subject',
recipient: 'To',
delivered: 'Delivered',
complained: 'Complained',
delayed: 'Delayed',
bounced: 'Bounced',
attachments: 'Attachments',
attCount: 'Total {total}',
emailCount: 'Total {total}',
error404: '404 Not Found',
home: 'Home',
loginBtn: 'Sign in',
regBtn: 'Sign up',
loginTitle: 'Sign in to your account to access email',
regTitle: 'Sign up for an account to use email',
confirmPwd: 'Confirm password',
regKeyOptional: 'Invite code (Optional)',
noAccount: 'Don\'t have an account? \u200B',
hasAccount: 'Already have an account? \u200B',
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',
customization: 'Customization',
websiteTitle: 'Title',
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',
resendToken: 'Resend Token',
R2OS: 'R2 Object storage',
osDomain: 'Domain',
emailPush: 'Email push',
tgBot: 'Telegram bot',
disable: 'Disable',
disabled: 'Disabled',
otherEmail: 'Forwarding to external email',
forwardingRules: 'Forwarding Rules',
forwardAll: 'All',
rules: 'Rules',
turnstileSetting: 'Turnstile',
signUpVerification: 'Sign up verification',
addEmailVerification: 'Add account verification',
about: 'About',
version: 'Version',
community: 'Community',
changeTitle: 'Change title',
addResendTokenDesc: 'Input to add; leave empty to delete.',
addOsDomain: 'Add domain',
domainDesc: 'Domain',
addTurnstileSecret: 'Add turnstile secret',
backgroundCropping: 'Background cropping',
tgBotDesc: 'Forward received emails to a Telegram bot',
tgBotToken: 'Bot token',
toBotTokenDesc: 'Multiple user chat_ids, separated by commas',
otherEmailDec: 'emails can be forwarded to other providers, but must be verified via cloudflare',
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',
domain: 'Domain',
optional: 'Optional',
subjectInputDesc: 'Please enter the email subject.',
changeUserName: 'Change username',
sendSeparately: 'Separately',
send: 'Send',
reply: 'Reply',
confirm: 'Confirm',
cancel: 'Cancel',
delEmailConfirm: 'Confirm deleting this email?',
delSuccessMsg: 'Deleted successfully',
emptyEmailMsg: 'Email cannot be empty',
notEmailMsg: 'Invalid email',
emptyPwdMsg: 'Password cannot be empty',
pwdLengthMsg: 'Password must be at least 6 characters',
confirmPwdFailMsg: 'The two passwords do not match',
emptyRegKeyMsg: 'Invite code cannot be empty',
regSuccessMsg: 'Sign up successful',
copySuccessMsg: 'Copied successfully',
copyFailMsg: 'Copy failed',
clearRegKey: 'Confirm clearing all invalid invite codes?',
clearSuccess: 'Cleared successfully',
emptyRole: 'Role cannot be empty',
emptyTimeMsg: 'Valid until time cannot be empty',
emptyCountMsg: 'Available count cannot be empty',
addSuccessMsg: 'Addition successful',
delConfirm: 'Confirm deleting {msg}?',
emptyRoleNameMsg: 'Role name cannot be empty',
changSuccessMsg: 'Changes 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',
warning: 'Warning',
delInputPattern: 'DELETE',
inputErrorMessage: 'Please enter DELETE to confirm',
delBackgroundConfirm: 'Confirm deleting this background?',
enable: 'Enable',
enabled: 'Enabled',
reSendConfirm: 'Confirm reset of {msg} send total?',
reSuccessMsg: 'Reset successful',
restoreConfirm: 'Confirm restoring {msg}?',
normalRestore: 'Normal restore',
allRestore: 'Includes deleted data',
restoreSuccessMsg: 'Restore successful',
banRestore: 'Confirm banning {msg}?',
signOut: 'Sign out',
clearContentConfirm: 'Are you sure to clear all content?',
attLimitMsg: 'Attachment size limit: 28MB',
emptyRecipientMsg: 'Recipient email cannot be empty',
emptySubjectMsg: 'Subject cannot be empty',
emptyContentMsg: 'Content cannot be empty',
noSeparateSendMsg: 'Separate sending does not support attachments yet',
sendSuccessMsg: 'Send successful',
sendFailMsg: 'Send failed',
saveDraftConfirm: 'Save draft?',
delEmailsConfirm: 'Confirm batch delete these emails?',
sending: 'Sending Email...',
sendingErrorMsg: 'Sending in progress',
networkErrorMsg: 'Network error. Check your internet',
timeoutErrorMsg: 'Timeout. Try again later',
serverBusyErrorMsg: 'Server busy. Please try again later.',
reqFailErrorMsg: 'Request failed. Try again later.',
mailDetails: 'Message',
language: 'Language',
totalUserAccount: '{msg}',
sendBanned: 'Banned',
wrote: 'wrote'
}
export default en

View File

@@ -0,0 +1,12 @@
import { createI18n } from 'vue-i18n';
import en from './en.js'
import zh from './zh.js'
const i18n = createI18n({
legacy: false,
messages: {
en,
zh
},
});
export default i18n;

251
mail-vue/src/i18n/zh.js Normal file
View File

@@ -0,0 +1,251 @@
const zh = {
inbox: '收件箱',
drafts: '草稿箱',
sent: '已发送',
starred: '星标邮件',
settings: '个人设置',
analytics: '分析页',
allUsers: '用户列表',
allMail: '全部邮件',
permissions: '权限控制',
inviteCode: '注册密钥',
SystemSettings: '系统设置',
noMoreData: '没有更多数据了',
noMessagesFound: '没有任何邮件',
addAccount: '添加邮箱',
emailAccount: '邮箱',
deleteUser: '删除账户',
deleteUserBtn: '删除账户',
changePassword: '修改密码',
newPassword: '新的密码',
confirmPassword: '旧的密码',
add: '添加',
manage: '管理',
rename: '改名',
delete: '删除',
save: '保存',
profile: '个人信息',
change: '修改',
changePwdBtn: '修改密码',
username: '用户名',
password: '密码',
delAccount: '删除账户',
delAccountMsg: '此操作将永久删除您的账户及其所有数据,无法恢复',
totalReceived: '收件数量',
totalSent: '发送数量',
totalMailboxes: '邮箱数量',
totalUsers: '用户数量',
deleted: '已删除',
active: '正常',
emailSource: '邮件来源',
userGrowth: '用户增长',
emailGrowth: '邮件增长',
emailSent: '发送',
emailReceived: '接收',
sentToday: '今日发件',
total: '次数',
growthTotalUsers: '用户数',
searchByEmail: '输入邮箱搜索',
tabEmailAddress: '用户邮箱',
tabReceived: '收件数量',
tabSent: '发件数量',
tabMailboxes: '邮箱数量',
tabRegisteredAt: '注册时间',
tabStatus: '状态',
tabRole: '权限身份',
roleName: '名字',
role: '权限身份',
all: '全部',
normal: '正常',
banned: '封禁',
reset: '重置',
restore: '恢复',
tabSetting: '设置',
registrationIp: '注册IP',
recentIP: '近期IP',
recentActivity: '近期活动',
loginDevice: '登录设备',
loginSystem: '登录系统',
browserLogin: '登录浏览器',
noPerm: '无权限',
noLimit: '无限制',
blocked: '被禁用',
sendCount: '邮件发送 :',
accountCount: '邮箱添加 :',
action: '操作',
chgPwd: '改密',
perm: '权限',
btnBan: '禁用',
admin: '超级管理员',
addUser: '添加用户',
select: '请选择',
unknown: '未知',
changePerm: '修改权限',
from: '发件人',
subject: '主题',
sender: '发件人',
user: '用户',
searchByContent: '输入内容查询',
noRecipient: '无收件人',
received: '已接收',
selectEmail: '邮箱',
order: '排序',
default: '默认',
description: '描述',
removeBody: '移除正文',
removeAll: '丢弃邮件',
expand: '展开',
collapse: '收起',
daily: '每天',
emailBlock: '输入邮箱拦截收件, 拦截所有前缀 *example.com',
searchRegKeyDesc: '输入注册码搜索',
remainingUses: '剩余次数',
exhausted: '已用尽',
validUntil: '有效至期',
expired: '已过期',
copy: '复制',
history: '记录',
addRegKey: '添加注册码',
regKey: '注册码',
noCodeFound: '没有任何注册码',
useHistory: '使用记录',
date: '时间',
roleDesc: '权限身份',
noSubject: '无主题',
recipient: '收件人',
delivered: '发送成功',
complained: '被标记为垃圾',
delayed: '发送被延迟',
bounced: '发送失败',
attachments: '附件列表',
attCount: '共 {total} 个',
emailCount: '共 {total} 封',
error404: '404错误, 找不到页面',
home: '返回首页',
loginBtn: '登录',
regBtn: '注册',
loginTitle: '输入账号信息以开始使用邮箱系统',
regTitle: '输入账号密码以开始注册邮箱系统',
confirmPwd: '确认密码',
regKeyOptional: '注册码(可选)',
noAccount: '还没有账号?',
hasAccount: '已有账号?',
regSwitch: '创建账号',
loginSwitch: '去登录',
websiteSetting: '网站设置',
websiteReg: '用户注册',
multipleEmail: '多号模式',
multipleEmailDesc: '开启后账号栏出现一个用户可以添加多个邮箱',
physicallyWipeData: '物理清空数据',
physicallyWipeDataDesc: '该操作会物理清空所有已被删除的数据',
customization: '个性化设置',
websiteTitle: '网站标题',
loginBoxOpacity: '登录透明',
loginBackground: '登录背景',
emailSetting: '邮件设置',
receiveEmails: '邮件接收',
autoRefresh: '自动刷新',
autoRefreshDesc: '轮询请求服务器获取最新邮件',
sendEmail: '邮件发送',
resendToken: '添加 Resend Token',
R2OS: 'R2 对象存储',
osDomain: '访问域名',
emailPush: '邮件推送',
tgBot: 'Telegram 机器人',
disable: '关闭',
disabled: '已关闭',
otherEmail: '第三方邮箱',
forwardingRules: '转发规则',
forwardAll: '全部转发',
rules: '规则转发',
turnstileSetting: 'Turnstile 人机验证',
signUpVerification: '注册验证',
addEmailVerification: '添加验证',
about: '关于',
version: '版本',
community: '交流',
changeTitle: '修改标题',
addResendTokenDesc: '输入内容添加,不填则删除',
addOsDomain: '添加访对象存储问域名',
domainDesc: '域名',
addTurnstileSecret: '添加 Turnstile 密钥',
backgroundCropping: '背景截图',
tgBotDesc: '可以将接收的邮件转发到Tg机器人',
tgBotToken: '机器人 token',
toBotTokenDesc: '用户 chat_id 多个用,分开',
otherEmailDec: '可以将邮件转到其他服务商邮箱需要在cloudflare验证邮箱',
otherEmailInputDesc: '多个邮箱用, 分开',
forwardingRulesDesc: '规则转发只会转发设置邮箱所接收的邮件',
ruleEmailsInputDesc: '多个邮箱用, 分开',
resendTokenList: 'Token 列表',
domain: '域名',
optional: '可选',
subjectInputDesc: '请输入邮件主题',
changeUserName: '修改用户名',
sendSeparately: '分别发送',
send: '发送',
reply: '回复',
confirm: '确定',
cancel: '取消',
delEmailConfirm: '确认删除该邮件吗?',
delSuccessMsg: '删除成功',
emptyEmailMsg: '邮箱不能为空',
notEmailMsg: '输入的邮箱不合法',
emptyPwdMsg: '密码不能为空',
pwdLengthMsg: '密码最少六位',
confirmPwdFailMsg: '两次密码输入不一致',
emptyRegKeyMsg: '注册码不能为空',
regSuccessMsg: '注册成功',
copySuccessMsg: '复制成功',
copyFailMsg: '复制失败',
clearRegKey: '确认清除所有不可用的注册码?',
clearSuccess: '清除成功',
emptyRole: '身份类型不能为空',
emptyTimeMsg: '有效时间不能为空',
emptyCountMsg: '可用次数不能为空',
addSuccessMsg: '添加成功',
delConfirm: '确认删除{msg}吗?',
emptyRoleNameMsg: '身份名不能为空',
changSuccessMsg: '修改成功',
changeRoleTitle: '修改身份',
addRoleTitle: '添加身份',
emptyUserNameMsg: '用户名不能为空',
delAccountConfirm: '确认删除当前账号及所有数据吗?',
clearAllDelConfirm: '此操作不可逆转, 输入 <b style="font-weight: bold">确认删除</b> 继续操作',
warning: '警告',
delInputPattern: '确认删除',
inputErrorMessage: '请输入确认删除',
delBackgroundConfirm: '确定要删除这个背景吗?',
enable: '启用',
enabled: '已启用',
reSendConfirm: '确认重置 {msg} 发件次数吗?',
reSuccessMsg: '重置成功',
restoreConfirm: '确认要恢复 {msg} 吗?',
normalRestore: '普通恢复',
allRestore: '包括已删除的数据',
restoreSuccessMsg: '恢复成功',
banRestore: '确认禁用 {msg} 吗?',
signOut: '退出',
clearContentConfirm: '确定要清空所有内容吗?',
attLimitMsg: '附件大小限制28mb',
emptyRecipientMsg: '收件人邮箱地址不能为空',
emptySubjectMsg: '主题不能为空',
emptyContentMsg: '邮件正文不能为空',
noSeparateSendMsg: '分别发送暂时不支持附件',
sendSuccessMsg: '发送成功',
sendFailMsg: '发送失败',
saveDraftConfirm: '是否保存草稿?',
delEmailsConfirm: '确认批量删除这些邮件吗?',
sending: '邮件正在发送中',
sendingErrorMsg: '邮件正在发送中',
networkErrorMsg: '网络错误,请检查网络连接',
timeoutErrorMsg: '请求超时,请稍后重试',
serverBusyErrorMsg: '服务器繁忙,请稍后重试',
reqFailErrorMsg: '请求失败,请稍后再试',
mailDetails: '邮件详情',
language: '网站语言',
totalUserAccount: '{msg} 个',
sendBan: '已禁用',
wrote: '来信'
}
export default zh

View File

@@ -1,10 +1,10 @@
import { useUserStore } from "@/store/user.js";
import { useSettingStore } from "@/store/setting.js";
import { useAccountStore } from "@/store/account.js";
import { loginUserInfo } from "@/request/my.js";
import { permsToRouter } from "@/utils/perm.js";
import {useUserStore} from "@/store/user.js";
import {useSettingStore} from "@/store/setting.js";
import {useAccountStore} from "@/store/account.js";
import {loginUserInfo} from "@/request/my.js";
import {permsToRouter} from "@/perm/perm.js";
import router from "@/router";
import { websiteConfig } from "@/request/setting.js";
import {websiteConfig} from "@/request/setting.js";
import {cvtR2Url} from "@/utils/convert.js";
export async function init() {
@@ -16,6 +16,12 @@ export async function init() {
const token = localStorage.getItem('token');
console.log(settingStore.lang)
if (!settingStore.lang) {
settingStore.lang = navigator.language.split('-')[0]
}
let setting = null;
if (token) {

View File

@@ -21,8 +21,8 @@
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399" />
<template #dropdown >
<el-dropdown-menu >
<el-dropdown-item v-if="hasPerm('email:send')" @click="openSetName(item)">改名</el-dropdown-item>
<el-dropdown-item v-if="item.accountId !== userStore.user.accountId && hasPerm('account:delete')" @click="remove(item)">删除</el-dropdown-item>
<el-dropdown-item v-if="hasPerm('email:send')" @click="openSetName(item)">{{$t('rename')}}</el-dropdown-item>
<el-dropdown-item v-if="item.accountId !== userStore.user.accountId && hasPerm('account:delete')" @click="remove(item)">{{$t('delete')}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -61,23 +61,23 @@
</template>
<div class="noLoading" v-if="noLoading && accounts.length > 0">
<div>没有更多数据了</div>
<div>{{$t('noMoreData')}}</div>
</div>
<div class="empty" v-if="noLoading && accounts.length === 0">
<el-empty description="没有任何邮件" />
<el-empty :description="$t('noMessagesFound')" />
</div>
</div>
</el-scrollbar>
<el-dialog v-model="showAdd" title="添加邮箱" >
<el-dialog v-model="showAdd" :title="$t('addAccount')" >
<div class="container">
<el-input v-model="addForm.email" ref="addRef" type="text" placeholder="邮箱" autocomplete="off">
<el-input v-model="addForm.email" ref="addRef" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
<template #append>
<div @click.stop="openSelect">
<el-select
ref="mySelect"
v-model="addForm.suffix"
placeholder="请选择"
:placeholder="$t('select')"
class="select"
>
<el-option
@@ -95,7 +95,7 @@
</template>
</el-input>
<el-button class="btn" type="primary" @click="submit" :loading="addLoading"
>添加
>{{$t('add')}}
</el-button>
</div>
<div
@@ -105,12 +105,12 @@
data-callback="onTurnstileSuccess"
></div>
</el-dialog>
<el-dialog v-model="setNameShow" title="修改用户名" >
<el-dialog v-model="setNameShow" :title="$t('changeUserName')" >
<div class="container">
<el-input v-model="accountName" type="text" placeholder="名字" autocomplete="off">
<el-input v-model="accountName" type="text" :placeholder="$t('username')" autocomplete="off">
</el-input>
<el-button class="btn" type="primary" @click="setName" :loading="setNameLoading"
>保存
>{{$t('save')}}
</el-button>
</div>
</el-dialog>
@@ -124,8 +124,10 @@ import {isEmail} from "@/utils/verify-utils.js";
import {useSettingStore} from "@/store/setting.js";
import {useAccountStore} from "@/store/account.js";
import {useUserStore} from "@/store/user.js";
import hasPerm from "@/utils/perm.js";
import { hasPerm } from "@/perm/perm.js"
import {useI18n} from "vue-i18n";
const { t } = useI18n();
const userStore = useUserStore();
const accountStore = useAccountStore();
const settingStore = useSettingStore();
@@ -186,7 +188,7 @@ function setName() {
if (!name) {
ElMessage({
message: '用户名不能为空',
message: t('emptyUserNameMsg'),
type: 'error',
plain: true,
})
@@ -203,7 +205,7 @@ function setName() {
}
ElMessage({
message: "保存成功",
message: t('changSuccessMsg'),
type: "success",
plain: true
})
@@ -227,9 +229,9 @@ function itemBg(accountId) {
}
function remove(account) {
ElMessageBox.confirm(`确认删除${account.email}吗?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
ElMessageBox.confirm(t('delConfirm',{msg: account.email}), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
accountDelete(account.accountId).then(() => {
@@ -239,7 +241,7 @@ function remove(account) {
getAccountList()
}
ElMessage({
message: '删除成功',
message: t('delSuccessMsg'),
type: 'success',
plain: true,
})
@@ -274,14 +276,14 @@ async function copyAccount(account) {
try {
await navigator.clipboard.writeText(account);
ElMessage({
message: '复制成功',
message: t('copySuccessMsg'),
type: 'success',
plain: true,
})
} catch (err) {
console.error('复制失败:', err);
console.error(`${t('copyFailMsg')}:`, err);
ElMessage({
message: '复制失败',
message: t('copyFailMsg'),
type: 'error',
plain: true,
})
@@ -321,7 +323,7 @@ function submit() {
if (!addForm.email){
ElMessage({
message: "邮箱不能为空",
message: t('emptyEmailMsg'),
type: "error",
plain: true
})
@@ -330,7 +332,7 @@ function submit() {
if (!isEmail(addForm.email+addForm.suffix)) {
ElMessage({
message: "非法邮箱",
message: t('notEmailMsg'),
type: "error",
plain: true
})
@@ -356,7 +358,7 @@ function submit() {
accounts.push(account)
verifyToken = ''
ElMessage({
message: "添加成功",
message: t('addSuccessMsg'),
type: "success",
plain: true
})

View File

@@ -9,60 +9,60 @@
<el-menu-item @click="router.push({name: 'email'})" index="email"
:class="route.meta.name === 'email' ? 'choose-item' : ''">
<Icon icon="hugeicons:mailbox-01" width="20" height="20" />
<span class="menu-name" style="margin-left: 21px">收件箱</span>
<span class="menu-name" style="margin-left: 21px">{{$t('inbox')}}</span>
</el-menu-item>
<el-menu-item @click="router.push({name: 'send'})" index="send" v-perm="'email:send'"
:class="route.meta.name === 'send' ? 'choose-item' : ''">
<Icon icon="cil:send" width="20" height="20" />
<span class="menu-name" style="margin-left: 21px">已发送</span>
<span class="menu-name" style="margin-left: 21px">{{$t('sent')}}</span>
</el-menu-item>
<el-menu-item @click="router.push({name: 'draft'})" index="draft" v-perm="'email:send'"
:class="route.meta.name === 'draft' ? 'choose-item' : ''">
<Icon icon="ep:document" width="19" height="19" />
<span class="menu-name" style="margin-left: 22px">草稿箱</span>
<span class="menu-name" style="margin-left: 22px">{{$t('drafts')}}</span>
</el-menu-item>
<el-menu-item @click="router.push({name: 'star'})" index="star"
:class="route.meta.name === 'star' ? 'choose-item' : ''">
<Icon icon="solar:star-line-duotone" width="20" height="20" />
<span class="menu-name" style="margin-left: 21px">星标邮件</span>
<span class="menu-name" style="margin-left: 21px">{{$t('starred')}}</span>
</el-menu-item>
<el-menu-item @click="router.push({name: 'setting'})" index="setting"
:class="route.meta.name === 'setting' ? 'choose-item' : ''">
<Icon icon="fluent:settings-48-regular" width="20" height="20" />
<span class="menu-name" style="margin-left: 21px">个人设置</span>
<span class="menu-name" style="margin-left: 21px">{{$t('settings')}}</span>
</el-menu-item>
<div class="manage-title" v-perm="['user:query','role:query','setting:query','analysis:query']">
<div>管理</div>
<div>{{$t('manage')}}</div>
</div>
<el-menu-item @click="router.push({name: 'analysis'})" index="analysis" v-perm="'analysis:query'"
:class="route.meta.name === 'analysis' ? 'choose-item' : ''">
<Icon icon="fluent:data-pie-20-regular" width="24" height="24" />
<span class="menu-name" style="margin-left: 18px">分析页</span>
<span class="menu-name" style="margin-left: 18px">{{$t('analytics')}}</span>
</el-menu-item>
<el-menu-item @click="router.push({name: 'user'})" index="setting" v-perm="'user:query'"
:class="route.meta.name === 'user' ? 'choose-item' : ''">
<Icon icon="si:user-alt-2-line" width="20" height="20" />
<span class="menu-name" style="margin-left: 21px">用户列表</span>
<span class="menu-name" style="margin-left: 21px">{{$t('allUsers')}}</span>
</el-menu-item>
<el-menu-item @click="router.push({name: 'sys-email'})" index="sys-email" v-perm="'sys-email:query'"
:class="route.meta.name === 'sys-email' ? 'choose-item' : ''">
<el-menu-item @click="router.push({name: 'all-email'})" index="all-email" v-perm="'all-email:query'"
:class="route.meta.name === 'all-email' ? 'choose-item' : ''">
<Icon icon="fluent:mail-list-28-regular" width="22" height="22" />
<span class="menu-name" style="margin-left: 20px">邮件列表</span>
<span class="menu-name" style="margin-left: 20px">{{$t('allMail')}}</span>
</el-menu-item>
<el-menu-item @click="router.push({name: 'role'})" index="setting" v-perm="'role:query'"
:class="route.meta.name === 'role' ? 'choose-item' : ''">
<Icon icon="fluent:lock-closed-16-regular" width="22" height="22" />
<span class="menu-name" style="margin-left: 20px">权限控制</span>
<span class="menu-name" style="margin-left: 20px">{{$t('permissions')}}</span>
</el-menu-item>
<el-menu-item @click="router.push({name: 'reg-key'})" index="reg-key" v-perm="'reg-key:query'"
:class="route.meta.name === 'reg-key' ? 'choose-item' : ''">
<Icon icon="fluent:fingerprint-20-filled" width="22" height="22" />
<span class="menu-name" style="margin-left: 20px">注册密钥</span>
<span class="menu-name" style="margin-left: 20px">{{$t('inviteCode')}}</span>
</el-menu-item>
<el-menu-item @click="router.push({name: 'sys-setting'})" index="sys-setting" v-perm="'setting:query'"
:class="route.meta.name === 'sys-setting' ? 'choose-item' : ''">
<Icon icon="eos-icons:system-ok-outlined" width="18" height="18" />
<span class="menu-name" style="margin-left: 24px">系统设置</span>
<span class="menu-name" style="margin-left: 24px">{{$t('SystemSettings')}}</span>
</el-menu-item>
</el-menu>
</div>

View File

@@ -2,7 +2,7 @@
<div class="header" :class="!hasPerm('email:send') ? 'not-send' : ''">
<div class="header-btn">
<hanburger @click="changeAside"></hanburger>
<span class="breadcrumb-item">{{ route.meta.title }}</span>
<span class="breadcrumb-item">{{ $t(route.meta.title) }}</span>
</div>
<div v-perm="'email:send'" class="writer-box" @click="openSend">
<div class="writer" >
@@ -32,12 +32,12 @@
{{ userStore.user.email }}
</div>
<div class="detail-user-type">
<el-tag >{{userStore.user.role.name}}</el-tag>
<el-tag >{{$t(userStore.user.role.name)}}</el-tag>
</div>
<div class="action-info">
<div>
<span style="margin-right: 10px">邮件发送 :</span>
<span style="margin-right: 10px">邮箱添加 :</span>
<span style="margin-right: 10px">{{$t('sendCount')}}</span>
<span style="margin-right: 10px">{{$t('accountCount')}}</span>
</div>
<div>
<div>
@@ -46,22 +46,19 @@
<el-tag v-else >{{sendType}}</el-tag>
</div>
<div>
<el-tag v-if="settingStore.settings.manyEmail || settingStore.settings.addEmail" >已关闭</el-tag>
<span v-else-if="accountCount && hasPerm('account:add')" style="margin-right: 5px">{{ accountCount }}</span>
<el-tag v-else-if="!accountCount && hasPerm('account:add')" >无限制</el-tag>
<el-tag v-else-if="!hasPerm('account:add')" >无权限</el-tag>
<el-tag v-if="settingStore.settings.manyEmail || settingStore.settings.addEmail" >{{$t('disabled')}}</el-tag>
<span v-else-if="accountCount && hasPerm('account:add')" style="margin-right: 5px">{{ $t('totalUserAccount',{msg: accountCount}) }}</span>
<el-tag v-else-if="!accountCount && hasPerm('account:add')" >{{$t('noLimit')}}</el-tag>
<el-tag v-else-if="!hasPerm('account:add')" >{{$t('blocked')}}</el-tag>
</div>
</div>
</div>
<div class="logout">
<el-button type="primary" :loading="logoutLoading" @click="clickLogout">退出</el-button>
<el-button type="primary" :loading="logoutLoading" @click="clickLogout">{{$t('signOut')}}</el-button>
</div>
</div>
</template>
</el-dropdown>
<div class="full" @click="full">
<Icon icon="iconamoon:screen-full-light" width="22" height="22" />
</div>
</div>
</div>
</template>
@@ -76,9 +73,10 @@ import {useUserStore} from "@/store/user.js";
import { useRoute } from "vue-router";
import {computed, ref} from "vue";
import {useSettingStore} from "@/store/setting.js";
import hasPerm from "@/utils/perm.js";
import screenfull from "screenfull";
import { hasPerm } from "@/perm/perm.js"
import {useI18n} from "vue-i18n";
const { t } = useI18n();
const route = useRoute();
const settingStore = useSettingStore();
const userStore = useUserStore();
@@ -91,23 +89,27 @@ const accountCount = computed(() => {
const sendType = computed(() => {
if (settingStore.settings.send === 1) {
return t('disabled')
}
if (!hasPerm('email:send')) {
return '无权限'
return t('noPerm')
}
if (!userStore.user.role.sendCount) {
return '无限制'
return t('noLimit')
}
if (userStore.user.role.sendCount < 0) {
return '无次数'
return t('blocked')
}
if (userStore.user.role.sendType === 'day') {
return '每天'
return t('daily')
}
if (userStore.user.role.sendType === 'count') {
return '次数'
return t('total')
}
})
@@ -150,10 +152,6 @@ function formatName(email) {
return email[0]?.toUpperCase() || ''
}
function full() {
screenfull.toggle();
}
</script>
<style lang="scss" scoped>
@@ -339,11 +337,8 @@ function full() {
.setting-icon {
position: relative;
top: 0;
margin-right: 5px;
margin-right: 10px;
bottom: 10px;
@media (max-width: 1024px) {
margin-right: 10px;
}
}
}

View File

@@ -3,7 +3,7 @@
<div :class="accountShow && hasPerm('account:query') ? 'block-show' : 'block-hide'" @click="uiStore.accountShow = false"></div>
<account :class="accountShow && hasPerm('account:query') ? 'show' : 'hide'" />
<router-view class="main-view" v-slot="{ Component,route }">
<keep-alive :include="['email','sys-email','send','sys-setting','star','user','role','analysis','reg-key','draft']">
<keep-alive :include="['email','all-email','send','sys-setting','star','user','role','analysis','reg-key','draft']">
<component :is="Component" :key="route.name"/>
</keep-alive>
</router-view>
@@ -15,7 +15,7 @@ import {useUiStore} from "@/store/ui.js";
import {useSettingStore} from "@/store/setting.js";
import {computed, onBeforeUnmount, onMounted} from "vue";
import { useRoute } from 'vue-router'
import hasPerm from "@/utils/perm.js";
import { hasPerm } from "@/perm/perm.js"
const props = defineProps({
openSend: Function

View File

@@ -6,7 +6,7 @@
<span class="title-text">
<Icon icon="hugeicons:quill-write-01" width="28" height="28" />
</span>
<span class="sender">发件人:</span>
<span class="sender">{{$t('sender')}}:</span>
<span class="sender-name">{{form.name}}</span>
<span class="send-email"><{{form.sendEmail}}></span>
</div>
@@ -15,17 +15,17 @@
</div>
</div>
<div class="container">
<el-input-tag @add-tag="addTagChange" tag-type="primary" size="default" v-model="form.receiveEmail" placeholder="多个邮箱用, 分开 example1.com,example2.com" >
<el-input-tag @add-tag="addTagChange" tag-type="primary" size="default" v-model="form.receiveEmail" :placeholder="$t('ruleEmailsInputDesc')" >
<template #prefix>
<div class="item-title">收件人 </div>
<div class="item-title">{{$t('recipient')}} </div>
</template>
<template #suffix>
<span class="distribute" :class="form.manyType ? 'checked' : ''" @click.stop="checkDistribute" >分别发送</span>
<span class="distribute" :class="form.manyType ? 'checked' : ''" @click.stop="checkDistribute" >{{$t('sendSeparately')}}</span>
</template>
</el-input-tag>
<el-input v-model="form.subject" placeholder="请输入邮件主题">
<el-input v-model="form.subject" :placeholder="$t('subjectInputDesc')">
<template #prefix>
<div class="item-title">主题 </div>
<div class="item-title">{{$t('subject')}} </div>
</template>
</el-input>
<tinyEditor :def-value="defValue" ref="editor" @change="change"/>
@@ -46,8 +46,8 @@
</div>
</div>
<div>
<el-button type="primary" @click="sendEmail" v-if="form.sendType === 'reply'">回复</el-button>
<el-button type="primary" @click="sendEmail" v-else >发送</el-button>
<el-button type="primary" @click="sendEmail" v-if="form.sendType === 'reply'">{{$t('reply')}}</el-button>
<el-button type="primary" @click="sendEmail" v-else >{{$t('send')}}</el-button>
</div>
</div>
</div>
@@ -66,11 +66,12 @@ import {useEmailStore} from "@/store/email.js";
import {fileToBase64, formatBytes} from "@/utils/file-utils.js";
import {getIconByName} from "@/utils/icon-utils.js";
import sendPercent from "@/components/send-percent/index.vue"
import {formatDetailDate, fromNow} from "@/utils/day.js";
import {formatDetailDate} from "@/utils/day.js";
import {useSettingStore} from "@/store/setting.js";
import {userDraftStore} from "@/store/draft.js";
import db from "@/db/db.js";
import dayjs from "dayjs";
import {useI18n} from "vue-i18n";
defineExpose({
open,
@@ -78,6 +79,7 @@ defineExpose({
openDraft
})
const { t } = useI18n()
const draftStore = userDraftStore()
const settingStore = useSettingStore()
const emailStore = useEmailStore();
@@ -131,9 +133,9 @@ function checkDistribute() {
}
function clearContent() {
ElMessageBox.confirm('确定要清空邮件吗?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
ElMessageBox.confirm(t('clearContentConfirm'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
resetForm()
@@ -159,7 +161,7 @@ function chooseFile() {
const TotalSize = form.attachments.reduce((acc, item) => acc + item.size, 0);
if ((TotalSize + size) > 29360128) {
ElMessage({
message: '附件文件大小限制28mb',
message: t('attLimitMsg'),
type: 'error',
plain: true,
})
@@ -174,7 +176,7 @@ async function sendEmail() {
if (form.receiveEmail.length === 0) {
ElMessage({
message: '收件人邮箱地址不能为空',
message: t('emptyRecipientMsg'),
type: 'error',
plain: true,
})
@@ -183,7 +185,7 @@ async function sendEmail() {
if (!form.subject) {
ElMessage({
message: '主题不能为空',
message: t('emptySubjectMsg'),
type: 'error',
plain: true,
})
@@ -192,7 +194,7 @@ async function sendEmail() {
if (!form.content) {
ElMessage({
message: '正文不能为空',
message: t('emptyContentMsg'),
type: 'error',
plain: true,
})
@@ -201,7 +203,7 @@ async function sendEmail() {
if (form.manyType === 'divide' && form.attachments.length > 0) {
ElMessage({
message: '分别发送暂时不支持附件',
message: t('noSeparateSendMsg'),
type: 'error',
plain: true,
})
@@ -210,7 +212,7 @@ async function sendEmail() {
if (sending) {
ElMessage({
message: '邮件正在发送中',
message: t('sendingErrorMsg'),
type: 'error',
plain: true,
})
@@ -218,7 +220,7 @@ async function sendEmail() {
}
percentMessage = ElMessage({
message: () => h(sendPercent, { value: percent.value }),
message: () => h(sendPercent, { value: percent.value,desc: t('sending') }),
dangerouslyUseHTMLString: true,
plain: true,
duration: 0,
@@ -238,7 +240,7 @@ async function sendEmail() {
})
ElNotification({
title: '邮件已发送',
title: t('sendSuccessMsg'),
type: "success",
message: h('span', { style: 'color: teal' }, email.subject),
position: 'bottom-right'
@@ -257,7 +259,7 @@ async function sendEmail() {
show.value = false
}).catch((e) => {
ElNotification({
title: '发送失败',
title: t('sendFailMsg'),
type: e.code === 403 ? 'warning' : 'error',
message: h('span', { style: 'color: teal' }, e.message),
position: 'bottom-right'
@@ -310,7 +312,7 @@ function openReply(email) {
<div></div>
<div>
<br>
${ formatDetailDate(email.createTime) }${email.name} &lt${email.sendEmail}&gt 来信:
${ formatDetailDate(email.createTime) } ${email.name} &lt${email.sendEmail}&gt ${t('wrote')}:
</div>
<blockquote class="mceNonEditable" style="margin: 0 0 0 0.8ex;border-left: 1px solid rgb(204,204,204);padding-left: 1ex;">
<articl>
@@ -397,9 +399,9 @@ function close() {
}
}
ElMessageBox.confirm('是否保存草稿?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
ElMessageBox.confirm(t('saveDraftConfirm'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning',
distinguishCancelAndClose: true
}).then( async () => {

View File

@@ -7,11 +7,12 @@ import 'vue-cropper/dist/index.css'
import { init } from '@/init/init.js';
import { createPinia } from 'pinia';
import piniaPersistedState from 'pinia-plugin-persistedstate';
import perm from "@/directives/perm.js";
import perm from "@/perm/perm.js";
const pinia = createPinia().use(piniaPersistedState)
import i18n from "@/i18n/index.js";
const app = createApp(App).use(pinia)
await init()
app.use(router).use(VueCropper).directive('perm',perm)
app.use(router).use(VueCropper).use(i18n).directive('perm',perm)
app.config.devtools = true;
app.mount('#app');

View File

@@ -1,6 +1,26 @@
import {useUserStore} from "@/store/user.js";
export default function hasPerm(permKey) {
export default {
mounted(el, binding) {
const userStore = useUserStore();
const permKeys = userStore.user.permKeys;
const value = binding.value;
if (permKeys.includes('*')) {
return;
}
const hasPermission = Array.isArray(value)
? value.some(key => permKeys.includes(key))
: permKeys.includes(value);
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el);
}
}
}
export function hasPerm(permKey) {
const {permKeys} = useUserStore().user;
return permKeys.includes('*') || permKeys.includes(permKey);
}
@@ -10,71 +30,71 @@ export function permsToRouter(permKeys) {
const routerList = []
Object.keys(routers).forEach(perm => {
if (permKeys.includes(perm) || permKeys.includes('*')) {
routerList.push(routers[perm])
routerList.push(...routers[perm])
}
})
return routerList;
}
const routers = {
'user:query': {
path: '/sys/user',
'user:query': [{
path: '/all-users',
name: 'user',
component: () => import('@/views/user/index.vue'),
meta: {
title: '用户列表',
title: 'allUsers',
name: 'user',
menu: true
}
},
'role:query': {
path: '/sys/role',
}],
'role:query': [{
path: '/role',
name: 'role',
component: () => import('@/views/role/index.vue'),
meta: {
title: '权限控制',
title: 'permissions',
name: 'role',
menu: true
}
},
'setting:query': {
path: '/sys/setting',
}],
'setting:query': [{
path: '/sys-setting',
name: 'sys-setting',
component: () => import('@/views/sys-setting/index.vue'),
meta: {
title: '系统设置',
title: 'SystemSettings',
name: 'sys-setting',
menu: true
}
},
'reg-key:query': {
path: '/sys/reg-key',
}],
'reg-key:query': [{
path: '/invite-code',
name: 'reg-key',
component: () => import('@/views/reg-key/index.vue'),
meta: {
title: '注册密钥',
title: 'inviteCode',
name: 'reg-key',
menu: true
}
},
'sys-email:query': {
path: '/sys/email',
name: 'sys-email',
component: () => import('@/views/sys-email/index.vue'),
}],
'all-email:query': [{
path: '/all-mail',
name: 'all-email',
component: () => import('@/views/all-email/index.vue'),
meta: {
title: '邮件列表',
name: 'sys-email',
title: 'allMail',
name: 'all-email',
menu: true
}
},
'analysis:query': {
}],
'analysis:query': [{
path: '/analysis',
name: 'analysis',
component: () => import('@/views/analysis/index.vue'),
meta: {
title: '分析页',
title: 'analytics',
name: 'analysis',
menu: true
}
}
}]
}

View File

@@ -0,0 +1,9 @@
import http from '@/axios/index.js';
export function allEmailList(params) {
return http.get('/allEmail/list', {params: {...params}})
}
export function allEmailDelete(emailIds) {
return http.delete('/allEmail/delete?emailIds=' + emailIds)
}

View File

@@ -1,5 +1,5 @@
import http from '@/axios/index.js'
export function analysisEcharts() {
return http.get('/analysis/echarts');
export function analysisEcharts(timeZone) {
return http.get('/analysis/echarts',{params: {timeZone}});
}

View File

@@ -1,9 +0,0 @@
import http from '@/axios/index.js';
export function sysEmailList(params) {
return http.get('/sysEmail/list', {params: {...params}})
}
export function sysEmailDelete(emailIds) {
return http.delete('/sysEmail/delete?emailIds=' + emailIds)
}

View File

@@ -1,39 +1,39 @@
import {createRouter, createWebHistory} from 'vue-router'
import {createRouter, createWebHistory} from 'vue-router'
import {useUiStore} from "@/store/ui.js";
const routes = [
{
path: '/',
name: 'layout',
redirect: '/email',
redirect: '/inbox',
component: () => import('@/layout/index.vue'),
children: [
{
path: '/email',
path: '/inbox',
name: 'email',
component: () => import('@/views/email/index.vue'),
meta: {
title: '收件箱',
title: 'inbox',
name: 'email',
menu: true
}
},
{
path: '/send',
path: '/sent',
name: 'send',
component: () => import('@/views/send/index.vue'),
meta: {
title: '已发送',
title: 'sent',
name: 'send',
menu: true
}
},
{
path: '/draft',
path: '/drafts',
name: 'draft',
component: () => import('@/views/draft/index.vue'),
meta: {
title: '草稿箱',
title: 'drafts',
name: 'draft',
menu: true
}
@@ -43,27 +43,27 @@ const routes = [
name: 'content',
component: () => import('@/views/content/index.vue'),
meta: {
title: '邮件详情',
title: 'mailDetails',
name: 'content',
menu: false
}
},
{
path: '/setting',
path: '/settings',
name: 'setting',
component: () => import('@/views/setting/index.vue'),
meta: {
title: '个人设置',
title: 'settings',
name: 'setting',
menu: true
}
},
{
path: '/star',
path: '/starred',
name: 'star',
component: () => import('@/views/star/index.vue'),
meta: {
title: '星标邮件',
title: 'starred',
name: 'star',
menu: true
}

View File

@@ -6,9 +6,13 @@ export const useSettingStore = defineStore('setting', {
settings: {
r2Domain: '',
loginOpacity: 1.00,
}
},
lang: '',
}),
actions: {
}
},
persist: {
pick: ['lang'],
},
})

View File

@@ -6,6 +6,7 @@ export const useUiStore = defineStore('ui', {
accountShow: false,
backgroundLoading: true,
writerRef: null,
key: 0,
asideCount: {
email: 0,
send: 0,

View File

@@ -89,6 +89,10 @@ button, input, select, textarea {
max-height: 220px !important;
}
.el-message {
white-space: nowrap !important;
}
:root {
--el-color-primary: #1890ff;
--el-color-primary-dark-2: #1064c0;

View File

@@ -2,46 +2,80 @@ import dayjs from 'dayjs'
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();
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.locale('zh-cn')
dayjs.locale(lang === 'zh' ? 'zh-cn' : '')
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
export function fromNow(date) {
const d = dayjs.utc(date).tz('Asia/Shanghai');
const d = dayjs.utc(date).tz(timeZone);
const now = dayjs();
const diffSeconds = now.diff(d, 'second');
const diffMinutes = now.diff(d, 'minute');
const diffHours = now.diff(d, 'hour');
const isToday = now.isSame(d, 'day');
if (lang === 'zh') {
if (isToday) {
if (diffSeconds < 60) return `几秒前`;
if (diffMinutes < 60) return `${diffMinutes}分钟前`;
if (diffHours >= 1 && diffHours < 2) return '1小时前';
return d.format('HH:mm');
}
else if (now.subtract(1, 'day').isSame(d, 'day')) {
return `昨天 ${d.format('HH:mm')}`;
}
else if (now.subtract(2, 'day').isSame(d, 'day')) {
return `前天 ${d.format('HH:mm')}`;
}
return d.year() === now.year()
? d.format('M月D日')
: d.format('YYYY/M/D');
} else {
if (isToday) {
if (diffSeconds < 60) return `Just now`;
if (diffMinutes < 60) return `${diffMinutes} min ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
return d.format('HH:mm');
}
if (now.subtract(1, 'day').isSame(d, 'day')) {
return d.format('hh:mm A');
}
return d.year() === now.year()
? d.format('MMM D')
: d.format('YYYY/MM/DD');
if (isToday) {
if (diffSeconds < 60) return `几秒前`;
if (diffMinutes < 60) return `${diffMinutes}分钟前`;
if (diffHours >= 1 && diffHours < 2) return '1小时前';
return d.format('HH:mm');
}
else if (now.subtract(1, 'day').isSame(d, 'day')) {
return `昨天 ${d.format('HH:mm')}`;
}
else if (now.subtract(2, 'day').isSame(d, 'day')) {
return `前天 ${d.format('HH:mm')}`;
}
return d.year() === now.year()
? d.format('M月D日')
: d.format('YYYY/M/D');
}
export function formatDetailDate(time) {
return dayjs.utc(time).tz('Asia/Shanghai').format('YYYY年M月D日 ddd AH:mm')
const d = dayjs.utc(time).tz(timeZone);
const now = dayjs();
const isSameYear = now.year() === d.year();
if (lang === 'zh') {
return d.format('YYYY年M月D日 ddd AH:mm');
} else {
return isSameYear
? d.format('ddd, MMM D, h:mm A')
: d.format('ddd, MMM D, YYYY, h:mm A');
}
}
export function tzDayjs(time) {
return dayjs.utc(time).tz('Asia/Shanghai')
return dayjs.utc(time).tz(timeZone)
}
export function toUtc() {
return dayjs().utc()
export function toUtc(time) {
return dayjs(time).utc()
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="box-404">
<el-empty :image-size="isPhone ? 120 : 0" class="empty" description="404错误, 找不到页面" >
<el-button type="primary" @click="router.replace({name: 'layout'})">返回首页</el-button>
<el-empty :image-size="isPhone ? 120 : 0" class="empty" :description="$t('error404')" >
<el-button type="primary" @click="router.replace({name: 'layout'})">{{$t('home')}}</el-button>
</el-empty>
</div>
</template>

View File

@@ -2,7 +2,7 @@
<div class="email-list-box">
<emailScroll ref="sysEmailScroll"
:get-emailList="getEmailList"
:email-delete="sysEmailDelete"
:email-delete="allEmailDelete"
:star-add="starAdd"
:star-cancel="starCancel"
:show-star="false"
@@ -12,13 +12,13 @@
:show-account-icon="false"
@jump="jumpContent"
@refresh-before="refreshBefore"
:type="'sys-email'"
:type="'all-email'"
>
<template #first>
<el-input
v-model="searchValue"
placeholder="输入内容查询"
:placeholder="$t('searchByContent')"
class="search-input"
>
<template #prefix>
@@ -26,13 +26,13 @@
<el-select
ref="mySelect"
v-model="params.searchType"
placeholder="请选择"
:placeholder="$t('select')"
class="select"
>
<el-option key="3" label="发件人" :value="'name'"/>
<el-option key="4" label="主题" :value="'subject'"/>
<el-option key="1" label="用户" :value="'user'"/>
<el-option key="2" label="邮箱" :value="'account'"/>
<el-option key="3" :label="$t('sender')" :value="'name'"/>
<el-option key="4" :label="$t('subject')" :value="'subject'"/>
<el-option key="1" :label="$t('user')" :value="'user'"/>
<el-option key="2" :label="$t('selectEmail')" :value="'account'"/>
</el-select>
<div style="color: #333;display: flex;">
<span>{{ selectTitle }}</span>
@@ -42,11 +42,11 @@
</template>
</el-input>
<el-select v-model="params.type" placeholder="Select" class="status-select">
<el-option key="1" label="全部" value="all"/>
<el-option key="3" label="已接收" value="receive"/>
<el-option key="2" label="已发送" value="send"/>
<el-option key="4" label="已删除" value="delete"/>
<el-option key="4" label="无人收件" value="noone"/>
<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-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"
@@ -64,16 +64,18 @@ import emailScroll from "@/components/email-scroll/index.vue"
import {computed, defineOptions, reactive, ref, watch} from "vue";
import {useEmailStore} from "@/store/email.js";
import {
sysEmailList,
sysEmailDelete
} from "@/request/sys-email.js";
allEmailList,
allEmailDelete
} from "@/request/all-email.js";
import {Icon} from "@iconify/vue";
import router from "@/router/index.js";
import { useI18n } from 'vue-i18n';
defineOptions({
name: 'sys-email'
name: 'all-email'
})
const { t } = useI18n();
const emailStore = useEmailStore();
const sysEmailScroll = ref({})
const searchValue = ref('')
@@ -95,13 +97,13 @@ const params = reactive({
const selectTitle = computed(() => {
if (params.searchType === 'user') return '用户'
if (params.searchType === 'account') return '邮箱'
if (params.searchType === 'name') return '发件人'
if (params.searchType === 'subject') return '主题'
if (params.searchType === 'user') return t('user')
if (params.searchType === 'account') return t('selectEmail')
if (params.searchType === 'name') return t('sender')
if (params.searchType === 'subject') return t('subject')
})
const paramsStar = localStorage.getItem('sys-email-params')
const paramsStar = localStorage.getItem('all-email-params')
if (paramsStar) {
const locaParams = JSON.parse(paramsStar)
params.type = locaParams.type
@@ -111,7 +113,7 @@ if (paramsStar) {
}
watch(() => params, () => {
localStorage.setItem('sys-email-params',JSON.stringify(params))
localStorage.setItem('all-email-params',JSON.stringify(params))
}, {
deep: true
})
@@ -163,12 +165,12 @@ function jumpContent(email) {
emailStore.contentData.delType = 'physics'
emailStore.contentData.showStar = false
emailStore.contentData.showReply = false
router.push('/content')
router.push({name: 'content'})
}
function getEmailList(emailId, size) {
return sysEmailList({emailId, size, ...params})
return allEmailList({emailId, size, ...params})
}
</script>

View File

@@ -8,7 +8,7 @@
<div class="number-item">
<div class="top">
<div class="left">
<div>收件数量</div>
<div>{{$t('totalReceived')}}</div>
<div>
<el-statistic :formatter="value => Math.round(value)" :value="receiveData"/>
</div>
@@ -20,14 +20,14 @@
</div>
</div>
<div class="delete-ratio">
<div>正常 <span class="normal">{{numberCount.normalReceiveTotal}}</span></div>
<div>删除 <span class="deleted">{{numberCount.delReceiveTotal}}</span></div>
<div>{{$t('active')}} <span class="normal">{{numberCount.normalReceiveTotal}}</span></div>
<div>{{$t('deleted')}} <span class="deleted">{{numberCount.delReceiveTotal}}</span></div>
</div>
</div>
<div class="number-item">
<div class="top">
<div class="left">
<div>发件数量</div>
<div>{{$t('totalSent')}}</div>
<div>
<el-statistic :formatter="value => Math.round(value)" :value="sendData"/>
</div>
@@ -39,14 +39,14 @@
</div>
</div>
<div class="delete-ratio">
<div>正常 <span class="normal">{{numberCount.normalSendTotal}}</span></div>
<div>删除 <span class="deleted">{{numberCount.delSendTotal}}</span></div>
<div>{{$t('active')}} <span class="normal">{{numberCount.normalSendTotal}}</span></div>
<div>{{$t('deleted')}} <span class="deleted">{{numberCount.delSendTotal}}</span></div>
</div>
</div>
<div class="number-item">
<div class="top">
<div class="left">
<div>邮箱数量</div>
<div>{{$t('totalMailboxes')}}</div>
<div>
<el-statistic :formatter="value => Math.round(value)" :value="accountData"/>
</div>
@@ -58,14 +58,14 @@
</div>
</div>
<div class="delete-ratio">
<div>正常 <span class="normal">{{numberCount.normalAccountTotal}}</span></div>
<div>删除 <span class="deleted">{{numberCount.delAccountTotal}}</span></div>
<div>{{$t('active')}} <span class="normal">{{numberCount.normalAccountTotal}}</span></div>
<div>{{$t('deleted')}} <span class="deleted">{{numberCount.delAccountTotal}}</span></div>
</div>
</div>
<div class="number-item">
<div class="top">
<div class="left">
<div>用户数量</div>
<div>{{$t('totalUsers')}}</div>
<div>
<el-statistic :formatter="value => Math.round(value)" :value="userData"/>
</div>
@@ -77,15 +77,15 @@
</div>
</div>
<div class="delete-ratio">
<div>正常 <span class="normal">{{numberCount.normalUserTotal}}</span></div>
<div>删除 <span class="deleted">{{numberCount.delUserTotal}}</span></div>
<div>{{$t('active')}} <span class="normal">{{numberCount.normalUserTotal}}</span></div>
<div>{{$t('deleted')}} <span class="deleted">{{numberCount.delUserTotal}}</span></div>
</div>
</div>
</div>
<div class="picture">
<div class="picture-item">
<div class="title" style="display: flex;justify-content: space-between;">
<span>邮件来源</span>
<span>{{$t('emailSource')}}</span>
<span class="source-button" v-if="false">
<el-radio-group v-model="checkedSourceType" >
<el-radio-button label="发件人" value="sender" />
@@ -98,7 +98,7 @@
</div>
</div>
<div class="picture-item">
<div class="title">用户增长</div>
<div class="title">{{$t('userGrowth')}}</div>
<div class="increase-line">
</div>
@@ -106,11 +106,11 @@
</div>
<div class="picture-cs">
<div class="picture-cs-item">
<div class="title">邮件增长</div>
<div class="title">{{$t('emailGrowth')}}</div>
<div class="email-column"></div>
</div>
<div class="picture-cs-item">
<div class="title">今日发件</div>
<div class="title">{{$t('sentToday')}}</div>
<div class="send-count"></div>
</div>
</div>
@@ -129,11 +129,14 @@ import {useUiStore} from "@/store/ui.js";
import {debounce} from "lodash-es";
import loading from "@/components/loading/index.vue";
import {useRoute} from "vue-router";
import { useI18n } from 'vue-i18n';
import {toUtc, tzDayjs} from "@/utils/day.js";
defineOptions({
name: 'analysis'
})
const { t } = useI18n();
const route = useRoute();
const uiStore = useUiStore()
const checkedSourceType = ref('sender')
@@ -194,7 +197,9 @@ let boxKey = ref(0)
let senderPieLeft = window.innerWidth < 500 ? `${window.innerWidth - 110}` : '72%'
onMounted(() => {
analysisEcharts().then(data => {
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
analysisEcharts(timeZone).then(data => {
receiveTotal.value = data.numberCount.receiveTotal
sendTotal.value = data.numberCount.sendTotal
accountTotal.value = data.numberCount.accountTotal
@@ -374,7 +379,7 @@ function createIncreaseLine() {
formatter: function (params) {
let result = ''
params.forEach(item => {
result = `${item.marker} 用户数: ${item.value}`;
result = `${item.marker} ${t('growthTotalUsers')} ${item.value}`;
});
return result;
},
@@ -507,7 +512,7 @@ function createEmailColumnChart() {
}
},
legend: {
data: ['接收', '发送'],
data: [t('emailReceived'), t('emailSent')],
top: '0'
},
grid: {
@@ -542,7 +547,7 @@ function createEmailColumnChart() {
},
series: [
{
name: '接收',
name: t('emailReceived'),
type: 'bar',
stack: 'total', // 堆叠组标识(必须相同)
barWidth: '60%',
@@ -559,7 +564,7 @@ function createEmailColumnChart() {
}
},
{
name: '发送',
name: t('emailSent'),
type: 'bar',
stack: 'total', // 堆叠组标识(必须相同)
emphasis: {
@@ -587,7 +592,7 @@ function createSendGauge() {
let option = {
tooltip: {},
series: [{
name: '今日发件',
name: t('sentToday'),
type: 'gauge',
max: 100,
// 进度条颜色(新增)
@@ -625,7 +630,7 @@ function createSendGauge() {
},
data: [{
value: daySendTotal,
name: '次数',
name: t('total'),
// 名称标签颜色(新增)
title: {
color: '#333' // 灰色标签

View File

@@ -18,20 +18,20 @@
<div class="content">
<div class="email-info">
<div>
<div class="send"><span class="send-source">发件人</span>
<div class="send"><span class="send-source">{{$t('from')}}</span>
<div class="send-name">
<span class="send-name-title">{{ email.name }}</span>
<span><{{ email.sendEmail }}></span>
</div>
</div>
<div class="receive"><span class="source">收件人</span><span class="receive-email">{{ formateReceive(email.recipient) }}</span></div>
<div class="receive"><span class="source">{{$t('recipient')}}</span><span class="receive-email">{{ formateReceive(email.recipient) }}</span></div>
<div class="date">
<div>{{ formatDetailDate(email.createTime) }}</div>
</div>
</div>
<el-alert v-if="email.status === 3" :closable="false" :title="'发送失败: ' + toMessage(email.message)" class="email-msg" type="error" show-icon />
<el-alert v-if="email.status === 4" :closable="false" title="被标记为垃圾邮件" class="email-msg" type="warning" show-icon />
<el-alert v-if="email.status === 5" :closable="false" title="邮件发送被延迟" class="email-msg" type="warning" show-icon />
<el-alert v-if="email.status === 3" :closable="false" :title="`${$t('bounced')} ` + toMessage(email.message)" class="email-msg" type="error" show-icon />
<el-alert v-if="email.status === 4" :closable="false" :title="$t('complained')" class="email-msg" type="warning" show-icon />
<el-alert v-if="email.status === 5" :closable="false" :title="$t('delayed')" class="email-msg" type="warning" show-icon />
</div>
<el-scrollbar class="htm-scrollbar" :class="email.attList.length === 0 ? 'bottom-distance' : ''">
<ShadowHtml :html="formatImage(email.content)" v-if="email.content" />
@@ -39,8 +39,8 @@
</el-scrollbar>
<div class="att" v-if="email.attList.length > 0">
<div class="att-title">
<span>附件列表</span>
<span> {{email.attList.length}} </span>
<span>{{$t('attachments')}}</span>
<span>{{$t('attCount',{total: email.attList.length})}}</span>
</div>
<div class="att-box">
@@ -87,8 +87,9 @@ import {getExtName, formatBytes} from "@/utils/file-utils.js";
import {cvtR2Url} from "@/utils/convert.js";
import {getIconByName} from "@/utils/icon-utils.js";
import {useSettingStore} from "@/store/setting.js";
import {sysEmailDelete} from "@/request/sys-email.js";
import {allEmailDelete} from "@/request/all-email.js";
import {useUiStore} from "@/store/ui.js";
import {useI18n} from "vue-i18n";
const uiStore = useUiStore();
const settingStore = useSettingStore();
@@ -99,7 +100,7 @@ const email = emailStore.contentData.email
const showPreview = ref(false)
const srcList = reactive([])
const { t } = useI18n()
watch(() => accountStore.currentAccountId, () => {
handleBack()
})
@@ -166,15 +167,15 @@ const handleBack = () => {
}
const handleDelete = () => {
ElMessageBox.confirm('确认删除该邮件吗?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
ElMessageBox.confirm(t('delEmailConfirm'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
if (emailStore.contentData.delType === 'logic') {
emailDelete(email.emailId).then(() => {
ElMessage({
message: '删除成功',
message: t('delSuccessMsg'),
type: 'success',
plain: true,
})
@@ -182,9 +183,9 @@ const handleDelete = () => {
})
} else {
sysEmailDelete(email.emailId).then(() => {
allEmailDelete(email.emailId).then(() => {
ElMessage({
message: '删除成功',
message: t('delSuccessMsg'),
type: 'success',
plain: true,
})

View File

@@ -14,10 +14,10 @@
:type="'draft'"
>
<template #name="props">
<span class="send-email" >{{props.email.receiveEmail.join(',') || '(无收件人)'}}</span>
<span class="send-email" >{{props.email.receiveEmail.join(',') || '('+$t('noRecipient')+')'}}</span>
</template>
<template #subject="props" >
{{props.email.subject || '(无标题)'}}
{{props.email.subject || '('+$t('noSubject')+')'}}
</template>
</emailScroll>
</template>

View File

@@ -11,16 +11,16 @@
<div class="form-wrapper">
<div class="container">
<span class="form-title">{{settingStore.settings.title}}</span>
<span class="form-desc" v-if="show === 'login'">请输入账号信息以开始使用邮箱系统</span>
<span class="form-desc" v-else>请输入账号密码以开始注册邮箱系统</span>
<span class="form-desc" v-if="show === 'login'">{{$t('loginTitle')}}</span>
<span class="form-desc" v-else>{{$t('regTitle')}}</span>
<div v-if="show === 'login'">
<el-input class="email-input" v-model="form.email" type="text" placeholder="邮箱" autocomplete="off">
<el-input class="email-input" v-model="form.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
<template #append>
<div @click.stop="openSelect">
<el-select
ref="mySelect"
v-model="suffix"
placeholder="请选择"
:placeholder="$t('select')"
class="select"
>
<el-option
@@ -37,20 +37,20 @@
</div>
</template>
</el-input>
<el-input v-model="form.password" placeholder="密码" type="password" autocomplete="off">
<el-input v-model="form.password" :placeholder="$t('password')" type="password" autocomplete="off">
</el-input>
<el-button class="btn" type="primary" @click="submit" :loading="loginLoading"
>登录
>{{$t('loginBtn')}}
</el-button>
</div>
<div v-else>
<el-input class="email-input" v-model="registerForm.email" type="text" placeholder="邮箱" autocomplete="off">
<el-input class="email-input" v-model="registerForm.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
<template #append>
<div @click.stop="openSelect">
<el-select
ref="mySelect"
v-model="suffix"
placeholder="请选择"
:placeholder="$t('select')"
class="select"
>
<el-option
@@ -67,22 +67,22 @@
</div>
</template>
</el-input>
<el-input v-model="registerForm.password" placeholder="密码" type="password" autocomplete="off" />
<el-input v-model="registerForm.confirmPassword" placeholder="确认密码" type="password" autocomplete="off" />
<el-input v-if="settingStore.settings.regKey === 0" v-model="registerForm.code" placeholder="注册码" type="text" autocomplete="off" />
<el-input v-if="settingStore.settings.regKey === 2" v-model="registerForm.code" placeholder="注册码(可选)" type="text" autocomplete="off" />
<el-input v-model="registerForm.password" :placeholder="$t('password')" type="password" autocomplete="off" />
<el-input v-model="registerForm.confirmPassword" :placeholder="$t('confirmPwd')" type="password" autocomplete="off" />
<el-input v-if="settingStore.settings.regKey === 0" v-model="registerForm.code" :placeholder="$t('regKey')" type="text" autocomplete="off" />
<el-input v-if="settingStore.settings.regKey === 2" v-model="registerForm.code" :placeholder="$t('regKeyOptional')" type="text" autocomplete="off" />
<div v-show="verifyShow"
class="register-turnstile"
:data-sitekey="settingStore.settings.siteKey"
data-callback="onTurnstileSuccess"
></div>
<el-button class="btn" type="primary" @click="submitRegister" :loading="registerLoading"
>注册
>{{$t('regBtn')}}
</el-button>
</div>
<template v-if="settingStore.settings.register === 0">
<div class="switch" @click="show = 'register'" v-if="show === 'login'">还没有账号? <span>创建账号</span></div>
<div class="switch" @click="show = 'login'" v-else>已有账号? <span>去登录</span></div>
<div class="switch" @click="show = 'register'" v-if="show === 'login'">{{$t('noAccount')}} <span>{{$t('regSwitch')}}</span></div>
<div class="switch" @click="show = 'login'" v-else>{{$t('hasAccount')}} <span>{{$t('loginSwitch')}}</span></div>
</template>
</div>
</div>
@@ -101,8 +101,10 @@ import {useUserStore} from "@/store/user.js";
import {Icon} from "@iconify/vue";
import {cvtR2Url} from "@/utils/convert.js";
import {loginUserInfo} from "@/request/my.js";
import {permsToRouter} from "@/utils/perm.js";
import {permsToRouter} from "@/perm/perm.js";
import {useI18n} from "vue-i18n";
const { t } = useI18n();
const accountStore = useAccountStore();
const userStore = useUserStore();
const settingStore = useSettingStore();
@@ -161,7 +163,7 @@ const submit = () => {
if (!form.email) {
ElMessage({
message: '邮箱不能为空',
message: t('emptyEmailMsg'),
type: 'error',
plain: true,
})
@@ -170,7 +172,7 @@ const submit = () => {
if (!isEmail(form.email + suffix.value)) {
ElMessage({
message: '输入的邮箱不合法',
message: t('notEmailMsg'),
type: 'error',
plain: true,
})
@@ -179,7 +181,7 @@ const submit = () => {
if (!form.password) {
ElMessage({
message: '密码不能为空',
message: t('emptyPwdMsg'),
type: 'error',
plain: true,
})
@@ -207,7 +209,7 @@ function submitRegister() {
if (!registerForm.email) {
ElMessage({
message: '邮箱不能为空',
message: t('emptyEmailMsg'),
type: 'error',
plain: true,
})
@@ -216,7 +218,7 @@ function submitRegister() {
if (!isEmail(registerForm.email + suffix.value)) {
ElMessage({
message: '输入的邮箱不合法',
message: t('notEmailMsg'),
type: 'error',
plain: true,
})
@@ -225,7 +227,7 @@ function submitRegister() {
if (!registerForm.password) {
ElMessage({
message: '密码不能为空',
message: t('emptyPwdMsg'),
type: 'error',
plain: true,
})
@@ -234,7 +236,7 @@ function submitRegister() {
if (registerForm.password.length < 6) {
ElMessage({
message: '密码最少六位',
message: t('pwdLengthMsg'),
type: 'error',
plain: true,
})
@@ -244,7 +246,7 @@ function submitRegister() {
if (registerForm.password !== registerForm.confirmPassword) {
ElMessage({
message: '两次密码输入不一致',
message: t('confirmPwdFailMsg'),
type: 'error',
plain: true,
})
@@ -256,7 +258,7 @@ function submitRegister() {
if (!registerForm.code) {
ElMessage({
message: '注册码不能为空',
message: t('emptyRegKeyMsg'),
type: 'error',
plain: true,
})
@@ -298,7 +300,7 @@ function submitRegister() {
turnstileId = null
verifyToken = ''
ElMessage({
message: '注册成功',
message: t('regSuccessMsg'),
type: 'success',
plain: true,
})

View File

@@ -6,7 +6,7 @@
<el-input
v-model="params.code"
class="search-input"
placeholder="输入注册码搜索"
:placeholder="$t('searchRegKeyDesc')"
>
</el-input>
</div>
@@ -27,18 +27,18 @@
<span class="code" @click="copyCode(item.code)">{{item.code}}</span>
</div>
<div class="info-left-item">
<div>剩余次数</div>
<div>{{$t('remainingUses')}}</div>
<div v-if="item.count">{{item.count}}</div>
<el-tag v-else type="danger">已用尽</el-tag>
<el-tag v-else type="danger">{{$t('exhausted')}}</el-tag>
</div>
<div class="info-left-item">
<div>权限身份</div>
<div>{{$t('roleDesc')}}</div>
<el-tag>{{item.roleName}}</el-tag>
</div>
<div class="info-left-item">
<div>有效至期</div>
<div>{{$t('validUntil')}}</div>
<div v-if="item.expireTime">{{ formatExpireTime(item.expireTime)}}</div>
<el-tag v-else type="danger">已过期</el-tag>
<el-tag v-else type="danger">{{$t('expired')}}</el-tag>
</div>
</div>
<div class="info-right">
@@ -46,9 +46,9 @@
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399" />
<template #dropdown >
<el-dropdown-menu>
<el-dropdown-item @click="copyCode(item.code)">复制</el-dropdown-item>
<el-dropdown-item @click="openHistory(item)">记录</el-dropdown-item>
<el-dropdown-item @click="deleteRegKey(item)">删除</el-dropdown-item>
<el-dropdown-item @click="copyCode(item.code)">{{$t('copy')}}</el-dropdown-item>
<el-dropdown-item @click="openHistory(item)">{{$t('history')}}</el-dropdown-item>
<el-dropdown-item @click="deleteRegKey(item)">{{$t('delete')}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -57,37 +57,37 @@
</div>
</div>
<div class="empty" v-if="regKeyData.length === 0">
<el-empty v-if="!regKeyFirst" :image-size="isMobile ? 120 : 0" description="没有任何注册码"/>
<el-empty v-if="!regKeyFirst" :image-size="isMobile ? 120 : 0" :description="$t('noCodeFound')"/>
</div>
</el-scrollbar>
<el-dialog v-model="showAdd" title="添加注册码">
<el-dialog v-model="showAdd" :title="$t('addRegKey')">
<div class="container">
<el-input v-model="addForm.code" placeholder="注册码">
<el-input v-model="addForm.code" :placeholder="$t('regKey')">
<template #suffix>
<Icon @click.stop="genCode" class="gen-code" icon="bitcoin-icons:refresh-filled" width="24" height="24" />
</template>
</el-input>
<el-select v-model="addForm.roleId" placeholder="身份类型">
<el-select v-model="addForm.roleId" :placeholder="$t('roleDesc')">
<el-option v-for="item in roleList" :label="item.name" :value="item.roleId" :key="item.roleId"/>
</el-select>
<el-date-picker
v-model="addForm.expireTime"
type="date"
placeholder="有效至期"
:placeholder="$t('validUntil')"
/>
<el-input-number v-model="addForm.count" :min="1" :max="99999"/>
<el-button class="btn" type="primary" @click="submit" :loading="addLoading"
>添加
>{{$t('add')}}
</el-button>
</div>
</el-dialog>
<el-dialog class="history-list" v-model="showRegKeyHistory" title="使用记录">
<el-dialog class="history-list" v-model="showRegKeyHistory" :title="$t('useHistory')">
<div class="loading" :class="historyLoading ? 'loading-show' : 'loading-hide'">
<loading />
</div>
<el-table v-if="!historyLoading" :data="historyList" :fit="true" style="height: 100%" >
<el-table-column :min-width="emailColumnWidth" property="email" label="用户" :show-overflow-tooltip="true" />
<el-table-column :width="createTimeColumnWidth" :formatter="formatUserCreateTime" property="createTime" label="时间" fixed="right" :show-overflow-tooltip="true" />
<el-table-column :min-width="emailColumnWidth" property="email" :label="$t('user')" :show-overflow-tooltip="true" />
<el-table-column :width="createTimeColumnWidth" :formatter="formatUserCreateTime" property="createTime" :label="$t('date')" fixed="right" :show-overflow-tooltip="true" />
</el-table>
</el-dialog>
</div>
@@ -104,6 +104,7 @@ import {regKeyAdd, regKeyList, regKeyClearNotUse, regKeyDelete, regKeyHistory} f
import { getTextWidth } from "@/utils/text.js";
import dayjs from "dayjs";
import {tzDayjs} from "@/utils/day.js";
import {useI18n} from "vue-i18n";
defineOptions({
name: 'reg-key'
@@ -115,6 +116,7 @@ const params = reactive({
code: '',
})
const { t } = useI18n()
const roleList = reactive([])
const addLoading = ref(false)
const showAdd = ref(false)
@@ -185,25 +187,44 @@ function formatUserCreateTime(regKey) {
const currentYear = dayjs().year();
const expireYear = createTime.year();
if (expireYear === currentYear) {
return createTime.format('M月D日 HH:mm');
if(settingStore.lang === 'zh') {
if (expireYear === currentYear) {
return createTime.format('M月D日 HH:mm');
} else {
return createTime.format('YYYY年M月D日 HH:mm');
}
} else {
return createTime.format('YYYY年M月D日 HH:mm');
if (createYear === currentYear) {
return createTime.format('MMM D, HH:mm');
} else {
return createTime.format('MMM D, YYYY HH:mm');
}
}
}
function formatExpireTime(expireTime) {
expireTime = tzDayjs(expireTime);
const expireDate = tzDayjs(expireTime);
const currentYear = dayjs().year();
const expireYear = expireTime.year();
const expireYear = expireDate.year();
if (expireYear === currentYear) {
return expireTime.format('M月D日');
if (settingStore.lang === 'zh') {
return expireYear === currentYear
? expireDate.format('M月D日')
: expireDate.format('YYYY年M月D日');
} else {
return expireTime.format('YYYY年M月D日');
return expireYear === currentYear
? expireDate.format('MMM D')
: expireDate.format('MMM D, YYYY');
}
}
function refresh() {
params.code = null
getList(true)
@@ -229,7 +250,7 @@ async function copyCode(code) {
try {
await navigator.clipboard.writeText(code);
ElMessage({
message: '复制成功',
message: t('copySuccessMsg'),
type: 'success',
plain: true,
})
@@ -257,14 +278,14 @@ function generateRandomCode(length = 8) {
}
function clearNotUse() {
ElMessageBox.confirm(`确认清除所有不可用的注册码?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
ElMessageBox.confirm(t('clearRegKey'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
regKeyClearNotUse().then(() => {
ElMessage({
message: '清除成功',
message: t('clearSuccess'),
type: 'success',
plain: true,
})
@@ -277,7 +298,7 @@ function submit() {
if (!addForm.code) {
ElMessage({
message: "注册码不能为空",
message: $('emptyRegKeyMsg'),
type: "error",
plain: true
})
@@ -286,7 +307,7 @@ function submit() {
if (!addForm.roleId) {
ElMessage({
message: "身份类型不能为空",
message: t('emptyRole'),
type: "error",
plain: true
})
@@ -295,7 +316,7 @@ function submit() {
if (!addForm.expireTime) {
ElMessage({
message: "有效时间不能为空",
message: t('emptyTimeMsg'),
type: "error",
plain: true
})
@@ -304,7 +325,7 @@ function submit() {
if (!addForm.count) {
ElMessage({
message: "使用次数不能为空",
message: t('emptyCountMsg'),
type: "error",
plain: true
})
@@ -316,7 +337,7 @@ function submit() {
showAdd.value = false
resetForm()
ElMessage({
message: "添加成功",
message: t('addSuccessMsg'),
type: "success",
plain: true
})
@@ -327,15 +348,15 @@ function submit() {
}
function deleteRegKey(regKey){
ElMessageBox.confirm(`确认删除${regKey.code}吗?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
ElMessageBox.confirm(t('delConfirm',{msg: regKey.code}), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
regKeyDelete([regKey.regKeyId]).then(() => {
getList()
ElMessage({
message: "删除成功",
message: t('delSuccessMsg'),
type: "success",
plain: true
})

View File

@@ -14,31 +14,31 @@
style="width: 100%;"
>
<el-table-column width="10" />
<el-table-column label="身份名称" prop="name" :min-width="roleWidth">
<el-table-column :label="$t('roleName')" prop="name" :min-width="roleWidth">
<template #default="props">
<div class="role-name">
<span >{{props.row.name}}</span>
<span v-if="props.row.isDefault"><el-tag class="def-tag" >默认</el-tag></span>
<span v-if="props.row.isDefault"><el-tag class="def-tag" >{{$t('default')}}</el-tag></span>
</div>
</template>
</el-table-column>
<el-table-column label="排序" :width="sortWidth" prop="sort"/>
<el-table-column v-if="desShow" label="描述" min-width="200" prop="description" >
<el-table-column :label="$t('order')" :width="sortWidth" prop="sort"/>
<el-table-column v-if="desShow" :label="$t('description')" min-width="200" prop="description" >
<template #default="props">
<div class="description">
<span >{{props.row.description}}</span>
</div>
</template>
</el-table-column>
<el-table-column label="操作" :width="settingWidth">
<el-table-column :label="$t('tabSetting')" :width="settingWidth">
<template #default="props">
<el-dropdown trigger="click">
<el-button size="small" type="primary">操作</el-button>
<el-button size="small" type="primary">{{$t('action')}}</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openRoleSet(props.row)">修改</el-dropdown-item>
<el-dropdown-item @click="setDef(props.row)">默认</el-dropdown-item>
<el-dropdown-item @click="delRole(props.row)">删除</el-dropdown-item>
<el-dropdown-item @click="openRoleSet(props.row)">{{$t('change')}}</el-dropdown-item>
<el-dropdown-item @click="setDef(props.row)">{{$t('default')}}</el-dropdown-item>
<el-dropdown-item @click="delRole(props.row)">{{$t('delete')}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -49,19 +49,19 @@
</el-scrollbar>
<el-dialog top="5vh" class="dialog" v-model="roleFormShow" :title="dialogType.title" @closed="resetForm">
<div class="dialog-box">
<el-input class="dialog-input" v-model="form.name" type="text" :maxlength="12" placeholder="身份名称" autocomplete="off" />
<el-input class="dialog-input" v-model="form.description" :maxlength="30" type="text" placeholder="描述" 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="输入邮箱拦截收件, 拦截所有前缀 *@example.com" autocomplete="off" />
<el-input class="dialog-input" v-model="form.name" type="text" :maxlength="12" :placeholder="$t('roleName')" autocomplete="off" />
<el-input class="dialog-input" v-model="form.description" :maxlength="30" type="text" :placeholder="$t('description')" 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('emailBlock')" autocomplete="off" />
<el-radio-group class="dialog-radio" v-model="form.banEmailType" v-if="form.banEmail.length > 0">
<el-radio label="丢弃邮件" :value="0" />
<el-radio label="移除正文" :value="1" />
<el-radio :label="$t('removeAll')" :value="0" />
<el-radio :label="$t('removeBody')" :value="1" />
</el-radio-group>
<div class="dialog-input">
<el-input-number placeholder="排序" :min="0" :max="9999" v-model.number="form.sort" controls-position="right" autocomplete="off" />
<el-input-number :placeholder="$t('order')" :min="0" :max="9999" v-model.number="form.sort" controls-position="right" autocomplete="off" />
</div>
<el-radio-group v-model="expand" size="small" @change="expandChange" class="perm-expand">
<el-radio-button label="展开" :value="true" />
<el-radio-button label="收起" :value="false" />
<el-radio-button :label="$t('expand')" :value="true" />
<el-radio-button :label="$t('collapse')" :value="false" />
</el-radio-group>
<el-tree
:expand-on-click-node="false"
@@ -79,25 +79,23 @@
<div>
<span>{{node.label}}</span>
<span class="send-num" v-if="data.permKey === 'email:send'" @click.stop>
<el-input-number v-model="form.sendCount" controls-position="right" :max="99999" size="small" placeholder="数量" >
<el-input-number v-model="form.sendCount" controls-position="right" :max="99999" size="small" :placeholder="$t('total')" >
</el-input-number>
<el-select v-model="form.sendType" placeholder="Select" size="small" style="width: 60px;margin-left: 5px;">
<el-option label="总数" value="count" />
<el-option label="每天" value="day" />
<el-select v-model="form.sendType" placeholder="Select" size="small" style="width: 65px;margin-left: 5px;">
<el-option :label="$t('total')" value="count" />
<el-option :label="$t('daily')" value="day" />
<el-option :label="$t('btnBan')" value="ban" />
</el-select>
<el-tooltip effect="dark" content="零无限制 负数无次数">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</span>
<span class="send-num" v-if="data.permKey === 'account:add'" @click.stop>
<el-input-number v-model="form.accountCount" controls-position="right" :min="0" :max="99999" size="small" placeholder="数量" >
<el-input-number v-model="form.accountCount" controls-position="right" :min="0" :max="99999" size="small" :placeholder="$t('total')" >
</el-input-number>
</span>
</div>
</template>
</el-tree>
<el-button class="btn" type="primary" :loading="permLoading" @click="roleFormClick"
>保存
>{{$t('save')}}
</el-button>
</div>
</el-dialog>
@@ -111,11 +109,13 @@ import loading from '@/components/loading/index.vue';
import {useRoleStore} from "@/store/role.js";
import {useUserStore} from "@/store/user.js";
import {isEmail} from "@/utils/verify-utils.js";
import {useI18n} from "vue-i18n";
defineOptions({
name: 'role'
})
const { t, locale } = useI18n();
const userStore = useUserStore();
const roleStore = useRoleStore();
const roleFormShow = ref(false)
@@ -182,7 +182,7 @@ function roleFormClick() {
function setDef(role) {
roleSetDef(role.roleId).then(() => {
ElMessage({
message: "设置成功",
message: t('changSuccessMsg'),
type: "success",
plain: true
})
@@ -191,14 +191,14 @@ function setDef(role) {
}
function delRole(role) {
ElMessageBox.confirm(`确认删除 ${role.name} 吗?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
ElMessageBox.confirm(t('delConfirm', {msg: role.name}), {
confirmButtonText: t('confirm'),
cancelButtonText: t('confirm'),
type: 'warning'
}).then(() => {
roleDelete(role.roleId).then(() => {
ElMessage({
message: "删除成功",
message: t('copySuccessMsg'),
type: "success",
plain: true
})
@@ -228,7 +228,7 @@ function setRole() {
if (!form.name) {
ElMessage({
message: "身份名不能为空",
message: t('emptyRoleNameMsg'),
type: "error",
plain: true
})
@@ -243,7 +243,7 @@ function setRole() {
permLoading.value = true
roleSet(params).then(() => {
ElMessage({
message: "修改成功",
message: t('changSuccessMsg'),
type: "success",
plain: true
})
@@ -275,7 +275,7 @@ function resetForm() {
function openRoleSet(role) {
chooseRole = role
dialogType.title = '修改身份'
dialogType.title = t('changeRoleTitle')
dialogType.type = 'set'
roleFormShow.value = true
form.sort = role.sort
@@ -292,7 +292,7 @@ function openRoleSet(role) {
function openAddRole() {
dialogType.title = '添加身份'
dialogType.title = t('addRoleTitle')
dialogType.type = 'add'
roleFormShow.value = true
}
@@ -306,7 +306,7 @@ function addRole() {
permLoading.value = true
roleAdd(params).then(() => {
ElMessage({
message: "添加成功",
message: t('addSuccessMsg'),
type: "success",
plain: true
})
@@ -335,7 +335,7 @@ function getRoleList() {
function adjustWidth() {
desShow.value = window.innerWidth > 767
settingWidth.value = window.innerWidth < 480 ? 75 : null
settingWidth.value = window.innerWidth < 480 ? (locale.value === 'en' ? 85 : 75) : null
sortWidth.value = window.innerWidth < 480 ? 75 : null
roleWidth.value = window.innerWidth < 480 ? 180 : 200
}

View File

@@ -1,72 +1,97 @@
<template>
<div class="box">
<div class="pass">
<div class="title">账户与密码</div>
<div class="pass-item">
<div>用户名</div>
<div class="container">
<div class="title">{{$t('profile')}}</div>
<div class="item">
<div>{{$t('username')}}</div>
<div>
<span v-if="setNameShow" class="edit-name-input">
<el-input v-model="accountName" ></el-input>
<span class="edit-name" @click="setName">
保存
{{$t('save')}}
</span>
</span>
<span v-else class="user-name">
<span >{{ userStore.user.name }}</span>
<span class="edit-name" @click="showSetName">
修改
{{$t('change')}}
</span>
</span>
</div>
</div>
<div class="pass-item">
<div>邮箱</div>
<div class="item">
<div>{{$t('emailAccount')}}</div>
<div>{{ userStore.user.email }}</div>
</div>
<div class="pass-item">
<div>密码</div>
<div class="item">
<div>{{$t('password')}}</div>
<div>
<el-button type="primary" @click="pwdShow = true">修改密码</el-button>
<el-button type="primary" @click="pwdShow = true">{{$t('changePwdBtn')}}</el-button>
</div>
</div>
</div>
<div class="del-email" v-perm="'my:delete'">
<div class="title">删除账户</div>
<div style="color: #585d69;">
此操作将永久删除您的账户及其所有数据无法恢复
</div>
<div class="container lang">
<div class="title">{{$t('language')}}</div>
<div>
<el-button type="primary" @click="deleteConfirm">删除账户</el-button>
<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>
<el-dialog v-model="pwdShow" title="修改密码" width="340">
<div class="del-email" v-perm="'my:delete'">
<div class="title">{{$t('deleteUser')}}</div>
<div style="color: #585d69;">
{{$t('delAccountMsg')}}
</div>
<div>
<el-button type="primary" @click="deleteConfirm">{{$t('deleteUserBtn')}}</el-button>
</div>
</div>
<el-dialog v-model="pwdShow" :title="$t('changePassword')" width="340">
<div class="update-pwd">
<el-input type="password" placeholder="新的密码" v-model="form.password"/>
<el-input type="password" placeholder="确认密码" v-model="form.newPwd"/>
<el-button type="primary" :loading="setPwdLoading" @click="submitPwd">保存</el-button>
<el-input type="password" :placeholder="$t('newPassword')" v-model="form.password"/>
<el-input type="password" :placeholder="$t('confirmPassword')" v-model="form.newPwd"/>
<el-button type="primary" :loading="setPwdLoading" @click="submitPwd">{{$t('save')}}</el-button>
</div>
</el-dialog>
</div>
</template>
<script setup>
import {defineOptions} from "vue";
import {reactive, ref} from 'vue'
import {reactive, ref, defineOptions, watch} 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";
import dayjs from "dayjs";
const { t, locale } = 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
@@ -76,7 +101,7 @@ function setName() {
if (!accountName.value) {
ElMessage({
message: '用户名不能为空',
message: t('emptyUserNameMsg'),
type: 'error',
plain: true,
})
@@ -94,7 +119,7 @@ function setName() {
accountSetName(userStore.user.accountId,name).then(() => {
ElMessage({
message: '修改成功',
message: t('changSuccessMsg'),
type: 'success',
plain: true,
})
@@ -113,16 +138,16 @@ const form = reactive({
})
const deleteConfirm = () => {
ElMessageBox.confirm('确认删除当前账号及所有数据吗?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
ElMessageBox.confirm(t('delAccountConfirm'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
userDelete().then(() => {
localStorage.removeItem('token');
router.replace('/login');
ElMessage({
message: '删除成功',
message: t('delSuccessMsg'),
type: 'success',
plain: true,
})
@@ -135,7 +160,7 @@ function submitPwd() {
if (!form.password) {
ElMessage({
message: '密码不能为空',
message: t('emptyPwdMsg'),
type: 'error',
plain: true,
})
@@ -144,7 +169,7 @@ function submitPwd() {
if (form.password.length < 6) {
ElMessage({
message: '密码不能小于6位',
message: t('pwdLengthMsg'),
type: 'error',
plain: true,
})
@@ -153,7 +178,7 @@ function submitPwd() {
if (form.password !== form.newPwd) {
ElMessage({
message: '两次密码输入不一致',
message: t('confirmPwdFailMsg'),
type: 'error',
plain: true,
})
@@ -163,7 +188,7 @@ function submitPwd() {
setPwdLoading.value = true
resetPassword(form.password).then(() => {
ElMessage({
message: '修改成功',
message: t('changSuccessMsg'),
type: 'success',
plain: true,
})
@@ -197,13 +222,13 @@ function submitPwd() {
font-weight: bold;
}
.pass {
.container {
font-size: 14px;
display: grid;
gap: 20px;
margin-bottom: 40px;
.pass-item {
.item {
display: grid;
grid-template-columns: 50px 1fr;
gap: 140px;

View File

@@ -8,21 +8,21 @@
<div class="card-grid">
<!-- Website Settings Card -->
<div class="settings-card">
<div class="card-title">网站设置</div>
<div class="card-title">{{$t('websiteSetting')}}</div>
<div class="card-content">
<div class="setting-item">
<div><span>用户注册</span></div>
<div><span>{{$t('websiteReg')}}</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.register"/>
</div>
</div>
<div class="setting-item">
<div><span>注册码</span></div>
<div><span>{{$t('regKey')}}</span></div>
<div>
<el-select
@change="change"
style="width: 80px;"
:style="`width: ${ locale === 'en' ? 100 : 80 }px;`"
v-model="setting.regKey"
placeholder="Select"
>
@@ -36,7 +36,7 @@
</div>
</div>
<div class="setting-item">
<div><span>添加邮箱</span></div>
<div><span>{{$t('addAccount')}}</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.addEmail"/>
@@ -44,8 +44,8 @@
</div>
<div class="setting-item">
<div>
<span>多号模式</span>
<el-tooltip effect="dark" content="开启后账号栏出现一个用户可以添加多个邮箱">
<span>{{$t('multipleEmail')}}</span>
<el-tooltip effect="dark" :content="$t('multipleEmailDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
@@ -56,8 +56,8 @@
</div>
<div class="setting-item">
<div>
<span>物理清空数据</span>
<el-tooltip effect="dark" content="该操作会物理清空所有已被删除的数据">
<span>{{$t('physicallyWipeData')}}</span>
<el-tooltip effect="dark" :content="$t('physicallyWipeDataDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
@@ -73,10 +73,10 @@
<!-- Personalization Settings Card -->
<div class="settings-card">
<div class="card-title">个性化设置</div>
<div class="card-title">{{$t('customization')}}</div>
<div class="card-content">
<div class="setting-item">
<div class="title-item"><span>网站标题</span></div>
<div class="title-item"><span>{{$t('websiteTitle')}}</span></div>
<div class="email-title">
<span>{{ setting.title }}</span>
<el-button class="opt-button" size="small" type="primary" @click="editTitleShow = true">
@@ -85,13 +85,13 @@
</div>
</div>
<div class="setting-item">
<div class="title-item"><span>登录透明</span></div>
<div class="title-item"><span>{{$t('loginBoxOpacity')}}</span></div>
<div>
<el-input-number size="small" v-model="loginOpacity" @change="opacityChange" :precision="2" :step="0.01" :max="1" :min="0" />
</div>
</div>
<div class="setting-item personalized">
<div><span>登录背景</span></div>
<div><span>{{$t('loginBackground')}}</span></div>
<div>
<el-image
class="background"
@@ -121,10 +121,10 @@
<!-- Email Sending Settings Card -->
<div class="settings-card">
<div class="card-title">邮件设置</div>
<div class="card-title">{{$t('emailSetting')}}</div>
<div class="card-content">
<div class="setting-item">
<div><span>邮件接收</span></div>
<div><span>{{$t('receiveEmails')}}</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.receive"/>
@@ -132,15 +132,15 @@
</div>
<div class="setting-item">
<div>
<span>轮询刷新</span>
<el-tooltip effect="dark" content="轮询请求服务器获取最新邮件">
<span>{{$t('autoRefresh')}}</span>
<el-tooltip effect="dark" :content="$t('autoRefreshDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
<div>
<el-select
@change="change"
style="width: 80px;"
:style="`width: ${ locale === 'en' ? 100 : 80 }px;`"
v-model="setting.autoRefreshTime"
placeholder="Select"
>
@@ -154,14 +154,14 @@
</div>
</div>
<div class="setting-item">
<div><span>邮件发送</span></div>
<div><span>{{$t('sendEmail')}}</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.send"/>
</div>
</div>
<div class="setting-item">
<div><span>添加 Resend Token</span></div>
<div><span>{{$t('resendToken')}}</span></div>
<div>
<el-button class="opt-button" style="margin-top: 0" @click="openResendList" size="small" type="primary">
<Icon icon="ic:round-list" width="18" height="18"/>
@@ -176,12 +176,12 @@
<!-- R2 Object Storage Card -->
<div class="settings-card">
<div class="card-title">R2对象存储</div>
<div class="card-title">{{$t('R2OS')}}</div>
<div class="card-content">
<div class="setting-item">
<div><span>访问域名</span></div>
<div><span>{{$t('osDomain')}}</span></div>
<div class="r2domain">
<span>{{ setting.r2Domain || '' }}</span>
<span>{{ setting.r2Domain || '' }}</span>
<el-button class="opt-button" size="small" type="primary" @click="r2DomainShow = true">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
@@ -191,30 +191,30 @@
</div>
<div class="settings-card">
<div class="card-title">邮件推送</div>
<div class="card-title">{{$t('emailPush')}}</div>
<div class="card-content">
<div class="setting-item">
<div><span>Telegram 机器人</span></div>
<div><span>{{$t('tgBot')}}</span></div>
<div class="forward">
<span>{{ setting.tgBotStatus === 0 ? '已开启' : '已关闭' }}</span>
<span>{{ setting.tgBotStatus === 0 ? $t('enabled') : $t('disabled') }}</span>
<el-button class="opt-button" size="small" type="primary" @click="openTgSetting">
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
</el-button>
</div>
</div>
<div class="setting-item">
<div><span>第三方邮箱</span></div>
<div><span>{{$t('otherEmail')}}</span></div>
<div class="forward">
<span>{{ setting.forwardStatus === 0 ? '已开启' : '已关闭' }}</span>
<span>{{ setting.forwardStatus === 0 ? $t('enabled') : $t('disabled') }}</span>
<el-button class="opt-button" size="small" type="primary" @click="openThirdEmailSetting">
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
</el-button>
</div>
</div>
<div class="setting-item">
<div><span>转发规则</span></div>
<div><span>{{$t('forwardingRules')}}</span></div>
<div class="forward">
<span>{{ setting.ruleType === 0 ? '全部转发' : '规则转发' }}</span>
<span>{{ setting.ruleType === 0 ? $t('forwardAll') : $t('rules') }}</span>
<el-button class="opt-button" size="small" type="primary" @click="openForwardRules">
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
</el-button>
@@ -225,17 +225,17 @@
<!-- Turnstile Verification Card -->
<div class="settings-card">
<div class="card-title">Turnstile 人机验证</div>
<div class="card-title">{{$t('turnstileSetting')}}</div>
<div class="card-content">
<div class="setting-item">
<div><span>注册验证</span></div>
<div><span>{{$t('signUpVerification')}}</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.registerVerify"/>
</div>
</div>
<div class="setting-item">
<div><span>添加验证</span></div>
<div><span>{{$t('addEmailVerification')}}</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.addEmailVerify"/>
@@ -263,14 +263,14 @@
</div>
<div class="settings-card about">
<div class="card-title">关于</div>
<div class="card-title">{{$t('about')}}</div>
<div class="card-content">
<div class="concerning-item">
<span>版本:</span>
<span>v1.4.0</span>
<span>{{$t('version')}} :</span>
<span>v1.3.1</span>
</div>
<div class="concerning-item">
<span>交流:</span>
<span>{{$t('community')}} : </span>
<el-button @click="jump('https://t.me/cloud_mail_tg')">
telegram
<template #icon>
@@ -290,13 +290,13 @@
</div>
<!-- Dialogs remain the same -->
<el-dialog v-model="editTitleShow" title="修改标题" width="340" @closed="editTitle = ''">
<el-dialog v-model="editTitleShow" :title="$t('changeTitle')" width="340" @closed="editTitle = ''">
<form>
<el-input type="text" placeholder="网站标题" v-model="editTitle"/>
<el-button type="primary" :loading="settingLoading" @click="saveTitle">保存</el-button>
<el-input type="text" :placeholder="$t('websiteTitle')" v-model="editTitle"/>
<el-button type="primary" :loading="settingLoading" @click="saveTitle">{{$t('save')}}</el-button>
</form>
</el-dialog>
<el-dialog v-model="resendTokenFormShow" title="添加 Resend Token" width="340" @closed="cleanResendTokenForm">
<el-dialog v-model="resendTokenFormShow" :title="$t('resendToken')" width="340" @closed="cleanResendTokenForm">
<form>
<el-select style="margin-bottom: 15px" v-model="resendTokenForm.domain" placeholder="Select">
<el-option
@@ -306,27 +306,27 @@
:value="item"
/>
</el-select>
<el-input type="text" placeholder="输入内容添加,不填则删除" v-model="resendTokenForm.token"/>
<el-button type="primary" :loading="settingLoading" @click="saveResendToken">保存</el-button>
<el-input type="text" :placeholder="$t('addResendTokenDesc')" v-model="resendTokenForm.token"/>
<el-button type="primary" :loading="settingLoading" @click="saveResendToken">{{$t('save')}}</el-button>
</form>
</el-dialog>
<el-dialog v-model="r2DomainShow" title="添加R2访问域名" width="340" @closed="r2DomainInput = ''">
<el-dialog v-model="r2DomainShow" :title="$t('addOsDomain')" width="340" @closed="r2DomainInput = ''">
<form>
<el-input type="text" placeholder="R2访问域名" v-model="r2DomainInput"/>
<el-button type="primary" :loading="settingLoading" @click="saveR2domain">保存</el-button>
<el-input type="text" :placeholder="$t('domainDesc')" v-model="r2DomainInput"/>
<el-button type="primary" :loading="settingLoading" @click="saveR2domain">{{$t('save')}}</el-button>
</form>
</el-dialog>
<el-dialog v-model="turnstileShow" title="添加 Turnstile 密钥" width="340"
<el-dialog v-model="turnstileShow" :title="$t('addTurnstileSecret')" width="340"
@closed="turnstileForm.secretKey = '';turnstileForm.siteKey = ''">
<form>
<el-input type="text" placeholder="siteKey" v-model="turnstileForm.siteKey"/>
<el-input type="text" style="margin-top: 15px" placeholder="secretKey" v-model="turnstileForm.secretKey"/>
<el-button type="primary" :loading="settingLoading" @click="saveTurnstileKey">保存</el-button>
<el-input type="text" placeholder="Site Key" v-model="turnstileForm.siteKey"/>
<el-input type="text" style="margin-top: 15px" placeholder="Secret Key" v-model="turnstileForm.secretKey"/>
<el-button type="primary" :loading="settingLoading" @click="saveTurnstileKey">{{$t('save')}}</el-button>
</form>
</el-dialog>
<el-dialog
v-model="cutShow"
title="背景截图"
:title="$t('backgroundCropping')"
class="cut-dialog"
>
<div class="cropper">
@@ -343,31 +343,30 @@
></vueCropper>
</div>
<div class="cut-button">
<el-button type="primary" :loading="settingLoading" @click="saveBackground">保存</el-button>
<el-button type="primary" :loading="settingLoading" @click="saveBackground">{{$t('save')}}</el-button>
</div>
</el-dialog>
<el-dialog
v-model="tgSettingShow"
title="Telegram 机器人"
class="forward-dialog"
>
<template #header>
<div class="forward-head">
<span class="forward-set-title">Telegram 机器人</span>
<el-tooltip effect="dark" content="可以将接收的邮件转发到Tg机器人">
<span class="forward-set-title">{{$t('tgBot')}}</span>
<el-tooltip effect="dark" :content="$t('tgBotDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
</template>
<div class="forward-set-body">
<el-input placeholder="机器人 token" v-model="tgBotToken"></el-input>
<el-input-tag tag-type="warning" placeholder="用户 chat_id 多个用,分开 12345,54321" v-model="tgChatId" @add-tag="addChatTag" ></el-input-tag>
<el-input :placeholder="$t('tgBotToken')" v-model="tgBotToken"></el-input>
<el-input-tag tag-type="warning" :placeholder="$t('toBotTokenDesc')" v-model="tgChatId" @add-tag="addChatTag" ></el-input-tag>
</div>
<template #footer>
<div class="dialog-footer">
<el-switch v-model="tgBotStatus" :active-value="0" :inactive-value="1" active-text="开启" inactive-text="关闭" />
<el-switch v-model="tgBotStatus" :active-value="0" :inactive-value="1" :active-text="$t('enable')" :inactive-text="$t('disable')" />
<el-button :loading="settingLoading" type="primary" @click="tgBotSave">
保存
{{$t('save')}}
</el-button>
</div>
</template>
@@ -378,20 +377,20 @@
>
<template #header>
<div class="forward-head">
<span class="forward-set-title">第三方邮箱</span>
<el-tooltip effect="dark" content="可以将邮件转到其他服务商邮箱需要在cloudflare验证邮箱">
<span class="forward-set-title">{{$t('otherEmail')}}</span>
<el-tooltip effect="dark" :content="$t('otherEmailDec')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
</template>
<div class="forward-set-body">
<el-input-tag tag-type="warning" placeholder="多个邮箱用, 分开 example1.com,example2.com" v-model="forwardEmail" @add-tag="emailAddTag"></el-input-tag>
<el-input-tag tag-type="warning" :placeholder="$t('otherEmailInputDesc')" v-model="forwardEmail" @add-tag="emailAddTag"></el-input-tag>
</div>
<template #footer>
<div class="dialog-footer">
<el-switch v-model="forwardStatus" :active-value="0" :inactive-value="1" active-text="开启" inactive-text="关闭" />
<el-switch v-model="forwardStatus" :active-value="0" :inactive-value="1" :active-text="$t('enable')" :inactive-text="$t('disable')" />
<el-button :loading="settingLoading" type="primary" @click="forwardEmailSave">
保存
{{$t('save')}}
</el-button>
</div>
</template>
@@ -402,31 +401,31 @@
>
<template #header>
<div class="forward-head">
<span class="forward-set-title">转发规则</span>
<el-tooltip effect="dark" content="规则转发只会转发设置邮箱所接收的邮件">
<span class="forward-set-title">{{$t('forwardingRules')}}</span>
<el-tooltip effect="dark" :content="$t('forwardingRulesDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
</template>
<div class="forward-set-body">
<el-input-tag placeholder="多个邮箱用, 分开 example1.com,example2.com" tag-type="success" v-model="ruleEmail" @add-tag="ruleEmailAddTag" />
<el-input-tag :placeholder="$t('ruleEmailsInputDesc')" tag-type="success" v-model="ruleEmail" @add-tag="ruleEmailAddTag" />
</div>
<template #footer>
<div class="dialog-footer">
<el-radio-group v-model="ruleType">
<el-radio :value="0" >全部转发</el-radio>
<el-radio :value="1" >规则转发</el-radio>
<el-radio :value="0" >{{$t('forwardAll')}}</el-radio>
<el-radio :value="1" >{{$t('rules')}}</el-radio>
</el-radio-group>
<el-button :loading="settingLoading" type="primary" @click="ruleEmailSave">
保存
{{$t('save')}}
</el-button>
</div>
</template>
</el-dialog>
<el-dialog class="resend-table" v-model="showResendList" title="Token 列表">
<el-dialog class="resend-table" v-model="showResendList" :title="$t('resendTokenList')">
<el-table :data="resendList" >
<el-table-column :min-width="emailColumnWidth" property="key" label="域名" :show-overflow-tooltip="true" />
<el-table-column :width="tokenColumnWidth" property="value" label="token" fixed="right" :show-overflow-tooltip="true" />
<el-table-column :min-width="emailColumnWidth" property="key" :label="$t('domain')" :show-overflow-tooltip="true" />
<el-table-column :width="tokenColumnWidth" property="value" label="Token" fixed="right" :show-overflow-tooltip="true" />
</el-table>
</el-dialog>
</el-scrollbar>
@@ -446,11 +445,13 @@ import {debounce} from 'lodash-es'
import {isEmail} from "@/utils/verify-utils.js";
import loading from "@/components/loading/index.vue";
import {getTextWidth} from "@/utils/text.js";
import { useI18n } from 'vue-i18n';
defineOptions({
name: 'sys-setting'
})
const { t, locale } = useI18n();
const firstLoading = ref(true)
const cropper = ref()
const cutImage = ref('')
@@ -482,13 +483,13 @@ const turnstileForm = reactive({
})
const regKeyOptions = [
{label: '开启', value: 0},
{label: '关闭', value: 1},
{label: '可选', value: 2},
{label: t('enable'), value: 0},
{label: t('disable'), value: 1},
{label: t('optional'), value: 2},
]
const options = [
{label: '关闭', value: 0},
{label: t('disable'), value: 0},
{label: '3s', value: 3},
{label: '5s', value: 5},
{label: '7s', value: 7},
@@ -658,18 +659,18 @@ const opacityChange = debounce(doOpacityChange, 1000, {
})
function physicsDeleteAllData() {
ElMessageBox.prompt('此操作不可逆转, 输入 <b style="font-weight: bold">确认删除</b> 继续操作', {
confirmButtonText: '确定',
cancelButtonText: '取消',
ElMessageBox.prompt(t('clearAllDelConfirm'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
dangerouslyUseHTMLString: true,
title: '警告',
title: t('warning'),
type: 'warning',
inputPattern: /^确认删除$/,
inputErrorMessage: '请输入确认删除',
inputPattern: new RegExp(`^${t('delInputPattern')}$`),
inputErrorMessage: t('inputErrorMessage'),
}).then(() => {
physicsDeleteAll().then(() => {
ElMessage({
message: "删除成功",
message: t('delSuccessMsg'),
type: "success",
plain: true
})
@@ -678,9 +679,9 @@ function physicsDeleteAllData() {
}
function delBackground() {
ElMessageBox.confirm('确定要删除这个背景吗?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
ElMessageBox.confirm(t('delBackgroundConfirm'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
setting.value.background = null
@@ -702,7 +703,7 @@ function saveBackground() {
setting.value.background = key
cutShow.value = false
ElMessage({
message: "设置成功",
message: t('changSuccessMsg'),
type: "success",
plain: true
})
@@ -725,14 +726,6 @@ function openCut() {
function saveR2domain() {
const settingForm = {r2Domain: r2DomainInput.value}
if (settingForm.r2Domain && !settingForm.r2Domain.startsWith('http')) {
ElMessage({
message: "域名必须以http或https开头",
type: "error",
plain: true
})
return;
}
editSetting(settingForm)
}
@@ -800,7 +793,7 @@ function editSetting(settingForm, refreshStatus = true) {
settingSet(settingForm).then(() => {
settingLoading.value = false
ElMessage({
message: "设置成功",
message: t('changSuccessMsg'),
type: "success",
plain: true
})

View File

@@ -6,15 +6,15 @@
<el-input
v-model="params.email"
class="search-input"
placeholder="输入邮箱名搜索"
:placeholder="$t('searchByEmail')"
>
</el-input>
</div>
<el-select v-model="params.status" placeholder="Select" class="status-select">
<el-option :key="-1" label="全部" :value="-1"/>
<el-option :key="0" label="正常" :value="0"/>
<el-option :key="1" label="封禁" :value="1"/>
<el-option :key="-2" label="删除" :value="-2"/>
<el-option :key="-1" :label="$t('all')" :value="-1"/>
<el-option :key="0" :label="$t('active')" :value="0"/>
<el-option :key="1" :label="$t('banned')" :value="1"/>
<el-option :key="-2" :label="$t('deleted')" :value="-2"/>
</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"
@@ -41,93 +41,93 @@
<el-table-column :width="expandWidth" type="expand">
<template #default="props">
<div class="details">
<div v-if="!sendNumShow"><span class="details-item-title">发件数量:</span>{{ props.row.sendEmailCount }}
<div v-if="!sendNumShow"><span class="details-item-title">{{$t('tabSent')}}:</span>{{ props.row.sendEmailCount }}
</div>
<div v-if="!accountNumShow"><span class="details-item-title">邮箱数量:</span>{{
<div v-if="!accountNumShow"><span class="details-item-title">{{$t('tabMailboxes')}}:</span>{{
props.row.accountCount
}}
</div>
<div v-if="!createTimeShow"><span class="details-item-title">注册时间:</span>{{
tzDayjs(props.row.createTime).format('YYYY-MM-DD HH:mm:ss')
<div v-if="!createTimeShow"><span class="details-item-title">{{$t('tabRegisteredAt')}}:</span>{{
tzDayjs(props.row.createTime).format('YYYY-MM-DD HH:mm')
}}
</div>
<div v-if="!typeShow"><span class="details-item-title">身份类型:</span> {{ toRoleName(props.row.type) }}
<div v-if="!typeShow"><span class="details-item-title">{{$t('perm')}}:</span> {{ toRoleName(props.row.type) }}
</div>
<div v-if="!statusShow">
<span class="details-item-title">状态:</span>
<el-tag disable-transitions v-if="props.row.isDel === 1" type="info">删除</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 0" type="primary">正常</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 1" type="danger">封禁</el-tag>
<span class="details-item-title">{{$t('tabStatus')}}:</span>
<el-tag disable-transitions v-if="props.row.isDel === 1" type="info">{{$t('deleted')}}</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 0" type="primary">{{$t('active')}}</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 1" type="danger">{{$t('banned')}}</el-tag>
</div>
<div><span class="details-item-title">注册IP:</span>{{ props.row.createIp || '未知' }}</div>
<div><span class="details-item-title">近期IP:</span>{{ props.row.activeIp || '未知' }}</div>
<div><span class="details-item-title">近期活动:</span>{{
props.row.activeTime ? tzDayjs(props.row.activeTime).format('YYYY-MM-DD') : '未知'
<div><span class="details-item-title">{{$t('registrationIp')}}:</span>{{ props.row.createIp || $t('unknown') }}</div>
<div><span class="details-item-title">{{$t('recentIP')}}:</span>{{ props.row.activeIp || $t('unknown') }}</div>
<div><span class="details-item-title">{{$t('recentActivity')}}:</span>{{
props.row.activeTime ? tzDayjs(props.row.activeTime).format('YYYY-MM-DD') : $t('unknown')
}}
</div>
<div><span class="details-item-title">登录设备:</span>{{ props.row.device || '未知' }}</div>
<div><span class="details-item-title">登录系统:</span>{{ props.row.os || '未知' }}</div>
<div><span class="details-item-title">登录浏览器:</span>{{ props.row.browser || '未知' }}</div>
<div><span class="details-item-title">{{$t('loginDevice')}}:</span>{{ props.row.device || $t('unknown') }}</div>
<div><span class="details-item-title">{{$t('loginSystem')}}:</span>{{ props.row.os || $t('unknown') }}</div>
<div><span class="details-item-title">{{$t('browserLogin')}}:</span>{{ props.row.browser || $t('unknown') }}</div>
<div>
<span class="details-item-title">发件次数:</span>
<span class="details-item-title">{{$t('sendEmail')}}:</span>
<span>{{ formatSendCount(props.row) }}</span>
<el-tag style="margin-left: 10px" v-if="props.row.sendAction.hasPerm" >
{{ formatSendType(props.row) }}
</el-tag>
<el-button size="small" style="margin-left: 10px"
v-if="props.row.sendAction.hasPerm && props.row.sendAction.sendCount"
@click="resetSendCount(props.row)" type="primary">重置
@click="resetSendCount(props.row)" type="primary">{{$t('reset')}}
</el-button>
</div>
</div>
</template>
</el-table-column>
<el-table-column show-overflow-tooltip :tooltip-formatter="tableRowFormatter" label="用户邮箱" :min-width="emailWidth">
<el-table-column show-overflow-tooltip :tooltip-formatter="tableRowFormatter" :label="$t('tabEmailAddress')" :min-width="emailWidth">
<template #default="props">
<div class="email-row">{{ props.row.email }}</div>
</template>
</el-table-column>
<el-table-column :formatter="formatterReceive" label-class-name="receive" column-key="receive"
:filtered-value="filteredValue" :filters="filters" :width="receiveWidth" label="收件数量"
:filtered-value="filteredValue" :filters="filters" :width="receiveWidth" :label="$t('tabReceived')"
prop="receiveEmailCount"/>
<el-table-column :formatter="formatterSend" label-class-name="send" column-key="send"
:filtered-value="filteredValue" :filters="filters" v-if="sendNumShow" label="发件数量"
:filtered-value="filteredValue" :filters="filters" v-if="sendNumShow" :label="$t('tabSent')"
prop="sendEmailCount"/>
<el-table-column :formatter="formatterAccount" label-class-name="account" column-key="account"
:filtered-value="filteredValue" :filters="filters" v-if="accountNumShow" label="邮箱数量"
:filtered-value="filteredValue" :filters="filters" v-if="accountNumShow" :label="$t('tabMailboxes')"
prop="accountCount"/>
<el-table-column v-if="createTimeShow" label="注册时间" min-width="160" prop="createTime">
<el-table-column v-if="createTimeShow" :label="$t('tabRegisteredAt')" min-width="160" prop="createTime">
<template #default="props">
{{ tzDayjs(props.row.createTime).format('YYYY-MM-DD HH:mm:ss') }}
{{ tzDayjs(props.row.createTime).format('YYYY-MM-DD HH:mm') }}
</template>
</el-table-column>
<el-table-column v-if="statusShow" min-width="60px" label="状态" prop="status">
<el-table-column v-if="statusShow" min-width="60px" :label="$t('tabStatus')" prop="status">
<template #default="props">
<el-tag disable-transitions v-if="props.row.isDel === 1" type="info">删除</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 0" type="primary">正常</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 1" type="danger">封禁</el-tag>
<el-tag disable-transitions v-if="props.row.isDel === 1" type="info">{{$t('deleted')}}</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 0" type="primary">{{$t('active')}}</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 1" type="danger">{{$t('banned')}}</el-tag>
</template>
</el-table-column>
<el-table-column v-if="typeShow" label="身份类型" min-width="140" prop="type">
<el-table-column v-if="typeShow" :label="$t('tabRole')" min-width="140" prop="type">
<template #default="props">
<div class="type">
{{ toRoleName(props.row.type) }}
</div>
</template>
</el-table-column>
<el-table-column label="设置" :width="settingWidth">
<el-table-column :label="$t('tabSetting')" :width="settingWidth">
<template #default="props">
<el-dropdown trigger="click">
<el-button size="small" type="primary">操作</el-button>
<el-button size="small" type="primary">{{$t('action')}}</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openSetPwd(props.row)">改密</el-dropdown-item>
<el-dropdown-item @click="openSetType(props.row)">权限</el-dropdown-item>
<el-dropdown-item @click="openSetPwd(props.row)">{{$t('chgPwd')}}</el-dropdown-item>
<el-dropdown-item @click="openSetType(props.row)">{{$t('perm')}}</el-dropdown-item>
<el-dropdown-item v-if="props.row.isDel !== 1" @click="setStatus(props.row)">
{{ setStatusName(props.row) }}
</el-dropdown-item>
<el-dropdown-item v-else @click="restore(props.row)">恢复</el-dropdown-item>
<el-dropdown-item @click="delUser(props.row)">删除</el-dropdown-item>
<el-dropdown-item v-else @click="restore(props.row)">{{$t('restore')}}</el-dropdown-item>
<el-dropdown-item @click="delUser(props.row)">{{$t('delete')}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -163,35 +163,35 @@
</div>
</div>
</el-scrollbar>
<el-dialog class="dialog" v-model="setPwdShow" title="修改密码" @closed="resetUserForm">
<el-dialog class="dialog" v-model="setPwdShow" :title="$t('changePassword')" @closed="resetUserForm">
<div class="dialog-box">
<el-input v-model="userForm.password" type="password" placeholder="密码" autocomplete="off">
<el-input v-model="userForm.password" type="password" :placeholder="$t('newPassword')" autocomplete="off">
</el-input>
<el-button class="btn" type="primary" :loading="settingLoading" @click="updatePwd"
>保存
>{{$t('save')}}
</el-button>
</div>
</el-dialog>
<el-dialog class="dialog" v-model="setTypeShow" title="设置权限" @closed="resetUserForm">
<el-dialog class="dialog" v-model="setTypeShow" :title="$t('changePerm')" @closed="resetUserForm">
<div class="dialog-box">
<el-input disabled model-value="超级管理员" v-if="userForm.type === 0" />
<el-input disabled :model-value="$t('admin')" v-if="userForm.type === 0" />
<el-select v-else v-model="userForm.type" placeholder="Select" >
<el-option v-for="item in roleList" :label="item.name" :value="item.roleId" :key="item.roleId"/>
</el-select>
<el-button :disabled="userForm.type === 0" class="btn" :loading="settingLoading" type="primary" @click="setType"
>保存
>{{$t('save')}}
</el-button>
</div>
</el-dialog>
<el-dialog v-model="showAdd" title="添加用户">
<el-dialog v-model="showAdd" :title="$t('addUser')">
<div class="container">
<el-input v-model="addForm.email" type="text" placeholder="邮箱" autocomplete="off">
<el-input v-model="addForm.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
<template #append>
<div @click.stop="openSelect">
<el-select
ref="mySelect"
v-model="addForm.suffix"
placeholder="请选择"
:placeholder="$t('select')"
class="select"
>
<el-option
@@ -208,12 +208,12 @@
</div>
</template>
</el-input>
<el-input type="password" v-model="addForm.password" placeholder="密码"/>
<el-select v-model="addForm.type" placeholder="身份类型">
<el-input type="password" v-model="addForm.password" :placeholder="$t('password')"/>
<el-select v-model="addForm.type" :placeholder="$t('perm')">
<el-option v-for="item in roleList" :label="item.name" :value="item.roleId" :key="item.roleId"/>
</el-select>
<el-button class="btn" type="primary" @click="submit" :loading="addLoading"
>添加
>{{$t('add')}}
</el-button>
</div>
</el-dialog>
@@ -239,16 +239,18 @@ import {useSettingStore} from "@/store/setting.js";
import {isEmail} from "@/utils/verify-utils.js";
import {useRoleStore} from "@/store/role.js";
import {useUserStore} from "@/store/user.js";
import { useI18n } from 'vue-i18n';
defineOptions({
name: 'user'
})
const { t, locale } = useI18n();
const roleStore = useRoleStore()
const userStore = useUserStore()
const settingStore = useSettingStore()
const filteredValue = ['normal', 'del']
const filters = [{text: '正常', value: 'normal'}, {text: '删除', value: 'del'}]
const filters = [{text: t('active'), value: 'normal'}, {text: t('deleted'), value: 'del'}]
const preserveExpanded = ref(false)
const emailWidth = ref(230)
const expandWidth = ref(40)
@@ -402,9 +404,9 @@ function formatterReceive(e) {
}
function setStatusName(user) {
if (user.isDel === 1) return '恢复'
if (user.status === 0) return '禁用'
if (user.status === 1) return '启用'
if (user.isDel === 1) return t('restore')
if (user.status === 0) return t('btnBan')
if (user.status === 1) return t('enable')
}
const tableRowFormatter = (data) => {
@@ -435,7 +437,7 @@ function submit() {
if (!addForm.email) {
ElMessage({
message: "邮箱不能为空",
message: t('emptyEmailMsg'),
type: "error",
plain: true
})
@@ -444,7 +446,7 @@ function submit() {
if (!isEmail(addForm.email + addForm.suffix)) {
ElMessage({
message: "非法邮箱",
message: t('notEmailMsg'),
type: "error",
plain: true
})
@@ -453,7 +455,7 @@ function submit() {
if (!addForm.password) {
ElMessage({
message: "密码不能为空",
message: t('emptyPwdMsg'),
type: "error",
plain: true
})
@@ -462,7 +464,7 @@ function submit() {
if (addForm.password.length < 6) {
ElMessage({
message: "密码至少六位",
message: t('pwdLengthMsg'),
type: "error",
plain: true
})
@@ -471,7 +473,7 @@ function submit() {
if (!addForm.type) {
ElMessage({
message: "身份类型不能为空",
message: t('emptyRole'),
type: "error",
plain: true
})
@@ -485,7 +487,7 @@ function submit() {
addLoading.value = false
showAdd.value = false
ElMessage({
message: "添加成功",
message: t('addSuccessMsg'),
type: "success",
plain: true
})
@@ -498,27 +500,29 @@ function submit() {
function formatSendType(user) {
if (user.sendAction.sendType === 'day') return '每天'
if (user.sendAction.sendType === 'count') return '总数'
if (user.sendAction.sendType === 'day') return t('daily')
if (user.sendAction.sendType === 'count') return t('total')
}
function formatSendCount(user) {
if (!user.sendAction.hasPerm) {
return '无权限'
return t('noPerm')
}
if (!user.sendAction.sendCount) {
return '无限制';
return t('noLimit');
}
return user.sendCount + '/' + user.sendAction.sendCount + '次'
let count = user.sendCount + '/' + user.sendAction.sendCount
return count
}
function toRoleName(type) {
if (type === 0) {
return '超级管理员'
return t('admin')
}
const index = roleList.findIndex(role => role.roleId === type)
@@ -530,14 +534,14 @@ function toRoleName(type) {
function resetSendCount(user) {
ElMessageBox.confirm(`确认重置${user.email}发件次数吗?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
ElMessageBox.confirm(t('reSendConfirm',{msg: user.email}), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
userRestSendCount(user.userId).then(() => {
ElMessage({
message: "重置成功",
message: t('reSuccessMsg'),
type: "success",
plain: true
})
@@ -547,14 +551,14 @@ function resetSendCount(user) {
}
function delUser(user) {
ElMessageBox.confirm(`确认删除${user.email}吗?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
ElMessageBox.confirm(t('delConfirm',{msg: user.email}), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
userDelete(user.userId).then(() => {
ElMessage({
message: "删除成功",
message: t('delSuccessMsg'),
type: "success",
plain: true
})
@@ -568,16 +572,16 @@ function restore(user) {
const type = ref(0)
ElMessageBox.confirm( null, {
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
message: () => h('div', [
h('div', { class: 'mb-2' }, `确认要恢复 ${user.email}`),
h('div', { class: 'mb-2' }, t('restoreConfirm', {msg: user.email})),
h(ElRadioGroup, {
modelValue: type.value,
'onUpdate:modelValue': (val) => (type.value = val),
}, [
h(ElRadio, { label: 'option1', value: 0 }, '普通恢复'),
h(ElRadio, { label: 'option2', value: 1 }, '包括已删除的数据'),
h(ElRadio, { label: 'option1', value: 0 }, t('normalRestore')),
h(ElRadio, { label: 'option2', value: 1 }, t('allRestore')),
])
]),
type: 'warning'
@@ -585,7 +589,7 @@ function restore(user) {
userRestore(user.userId,type.value).then(() => {
user.isDel = 0
ElMessage({
message: "恢复成功",
message: t('restoreSuccessMsg'),
type: "success",
plain: true
})
@@ -596,9 +600,9 @@ function restore(user) {
function setStatus(user) {
if (user.status === 0) {
ElMessageBox.confirm(`确认禁用 ${user.email} 吗?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
ElMessageBox.confirm(t('banRestore',{ msg: user.email }), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
httpSetStatus(user)
@@ -613,7 +617,7 @@ function httpSetStatus(user) {
userSetStatus({status: status, userId: user.userId}).then(() => {
user.status = status
ElMessage({
message: "设置成功",
message: t('changSuccessMsg'),
type: "success",
plain: true
})
@@ -626,7 +630,7 @@ function setType() {
chooseUser.type = userForm.type
setTypeShow.value = false
ElMessage({
message: "设置成功",
message: t('changSuccessMsg'),
type: "success",
plain: true
})
@@ -652,7 +656,7 @@ function updatePwd() {
if (!userForm.password) {
ElMessage({
message: '密码不能为空',
message: t('emptyPwdMsg'),
type: 'error',
plain: true,
})
@@ -661,7 +665,7 @@ function updatePwd() {
if (userForm.password.length < 6) {
ElMessage({
message: '密码最少六位',
message: t('pwdLengthMsg'),
type: 'error',
plain: true,
})
@@ -672,7 +676,7 @@ function updatePwd() {
userSetPwd({password: userForm.password, userId: userForm.userId}).then(() => {
setPwdShow.value = false
ElMessage({
message: "设置成功",
message: t('changSuccessMsg'),
type: "success",
plain: true
})
@@ -754,7 +758,7 @@ function adjustWidth() {
sendNumShow.value = width > 685
typeShow.value = width > 767
emailWidth.value = width > 480 ? 230 : null
settingWidth.value = width < 480 ? 75 : null
settingWidth.value = width < 480 ? (locale.value === 'en' ? 85 : 75) : null
expandWidth.value = width < 480 ? 25 : 40
pagerCount.value = width < 768 ? 7 : 11
receiveWidth.value = width < 480 ? 90 : null
@@ -894,7 +898,7 @@ function adjustWidth() {
}
.status-select {
width: 80px;
width: 95px;
:deep(.el-select__wrapper) {
min-height: 28px;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,8 @@
<title></title>
<link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml">
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module" crossorigin src="/assets/index-DlA47rIk.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BkPsvh6f.css">
<script type="module" crossorigin src="/assets/index-Cjzn1ZdJ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dx0iKe9o.css">
</head>
<body>
<div id="loading-first">

File diff suppressed because it is too large Load Diff

View File

@@ -14,10 +14,11 @@
"wrangler": "^4.7.0"
},
"dependencies": {
"@cloudflare/vite-plugin": "^1.0.5",
"@cloudflare/vite-plugin": "1.6.0",
"dayjs": "^1.11.13",
"drizzle-orm": "^0.42.0",
"hono": "^4.7.5",
"i18next": "^25.3.2",
"linkedom": "^0.18.10",
"postal-mime": "^2.4.3",
"resend": "^4.5.1",

View File

@@ -2,12 +2,12 @@ import app from '../hono/hono';
import emailService from '../service/email-service';
import result from '../model/result';
app.get('/sysEmail/list',async (c) => {
app.get('/allEmail/list',async (c) => {
const data = await emailService.allList(c, c.req.query());
return c.json(result.ok(data));
})
app.delete('/sysEmail/delete',async (c) => {
app.delete('/allEmail/delete',async (c) => {
const list = await emailService.physicsDelete(c, c.req.query());
return c.json(result.ok(list));
})

View File

@@ -3,6 +3,6 @@ import analysisService from '../service/analysis-service';
import result from '../model/result';
app.get('/analysis/echarts', async (c) => {
const data = await analysisService.echarts(c);
const data = await analysisService.echarts(c, c.req.query());
return c.json(result.ok(data));
})

View File

@@ -6,7 +6,7 @@ const constant = {
ATTACHMENT_PREFIX: 'attachments/',
BACKGROUND_PREFIX: 'static/background/',
ADMIN_ROLE: {
name: '超级管理员',
name: 'admin',
sendCount: 0,
sendType: 'count',
accountCount: 0

View File

@@ -46,53 +46,53 @@ const analysisDao = {
return results[0];
},
async userDayCount(c) {
async userDayCount(c, diffHours) {
const { results } = await c.env.db.prepare(`
SELECT
DATE(create_time,'+8 hours') AS date,
DATE(create_time,'+${diffHours} hours') AS date,
COUNT(*) AS total
FROM
user
WHERE
DATE(create_time,'+8 hours') BETWEEN DATE('now', '-15 days', '+8 hours') AND DATE('now','-1 day','+8 hours')
DATE(create_time,'+${diffHours} hours') BETWEEN DATE('now', '-15 days', '+${diffHours} hours') AND DATE('now','-1 day','+${diffHours} hours')
GROUP BY
DATE(create_time,'+8 hours')
DATE(create_time,'+${diffHours} hours')
ORDER BY
date ASC
`).all();
return results;
},
async receiveDayCount(c) {
async receiveDayCount(c, diffHours) {
const { results } = await c.env.db.prepare(`
SELECT
DATE(create_time,'+8 hours') AS date,
DATE(create_time,'+${diffHours} hours') AS date,
COUNT(*) AS total
FROM
email
WHERE
DATE(create_time,'+8 hours') BETWEEN DATE('now', '-15 days', '+8 hours') AND DATE('now','-1 day','+8 hours')
DATE(create_time,'+${diffHours} hours') BETWEEN DATE('now', '-15 days', '+${diffHours} hours') AND DATE('now','-1 day','+${diffHours} hours')
AND type = 0
GROUP BY
DATE(create_time,'+8 hours')
DATE(create_time,'+${diffHours} hours')
ORDER BY
date ASC
`).all();
return results;
},
async sendDayCount(c) {
async sendDayCount(c, diffHours) {
const { results } = await c.env.db.prepare(`
SELECT
DATE(create_time,'+8 hours') AS date,
DATE(create_time,'+${diffHours} hours') AS date,
COUNT(*) AS total
FROM
email
WHERE
DATE(create_time,'+8 hours') BETWEEN DATE('now', '-15 days', '+8 hours') AND DATE('now','-1 day','+8 hours')
DATE(create_time,'+${diffHours} hours') BETWEEN DATE('now', '-15 days', '+${diffHours} hours') AND DATE('now','-1 day','+${diffHours} hours')
AND type = 1
GROUP BY
DATE(create_time,'+8 hours')
DATE(create_time,'+${diffHours} hours')
ORDER BY
date ASC
`).all();

View File

@@ -66,8 +66,8 @@ export async function email(message, env, ctx) {
if (banEmailType === roleConst.banEmailType.ALL) return
if (banEmailType === roleConst.banEmailType.CONTENT) {
email.html = '邮件内容已被移除'
email.text = '邮件内容已被移除'
email.html = 'removed'
email.text = 'removed'
email.attachments = []
}
@@ -80,8 +80,8 @@ export async function email(message, env, ctx) {
if (banEmailType === roleConst.banEmailType.ALL) return
if (banEmailType === roleConst.banEmailType.CONTENT) {
email.html = '邮件内容已被移除'
email.text = '邮件内容已被移除'
email.html = 'removed'
email.text = 'removed'
email.attachments = []
}
@@ -115,11 +115,6 @@ export async function email(message, env, ctx) {
status: emailConst.status.SAVING
};
let headers = message.headers
console.log(headers.get('X-Cf-Spamh-Score'))
console.log(email)
const attachments = [];
const cidAttachments = [];

View File

@@ -13,7 +13,7 @@ import '../api/resend-api';
import '../api/user-api';
import '../api/my-api';
import '../api/role-api'
import '../api/sys-email-api'
import '../api/all-email-api'
import '../api/init-api'
import '../api/analysis-api'
import '../api/reg-key-api'

View File

@@ -0,0 +1,93 @@
const en = {
IncorrectPwd: 'Incorrect password',
addAccountDisabled: 'Add email account function is disabled',
regDisabled: 'Sign up is disabled',
emptyEmail: 'Email cannot be empty',
notEmail: 'Invalid email',
notExistDomain: 'Email domain does not exist',
isDelAccount: 'This Email has been deleted',
isRegAccount: 'This Email is already signed up',
accountLimit: 'Email account limit reached',
delMyAccount: 'Cannot delete your own email account',
noUserAccount: 'This email does not belong to the current user',
usernameLengthLimit: 'Username length exceeds the limit',
noOsDomainSendPic: 'Cannot send body images: R2 domain not configured',
noOsSendPic: 'Cannot send body images: R2 object storage not configured',
noOsDomainSendAtt: 'Cannot send attachments: R2 domain not configured',
noOsSendAtt: 'Cannot send attachments: R2 object storage not configured',
disabledSend: 'Email sending is disabled',
noSeparateSend: 'Attachments are not supported in separate sending',
userNoSendTotal: 'User has no remaining sends',
daySendLimit: 'Daily send limit reached',
totalSendLimit: 'Total send limit reached',
daySendLack: 'Not enough remaining sends today',
totalSendLack: 'Not enough total remaining sends',
senderAccountNotExist: 'Sender email does not exist',
noResendToken: 'Resend API token not configured',
sendEmailNotCurUser: 'Sender email does not belong to current user',
notExistEmailReply: 'Mail does not exist and cannot be replied to',
pwdLengthLimit: 'Password length exceeds the limit',
emailLengthLimit: 'Email length exceeds the limit',
pwdMinLengthLimit: 'Password must be at least 6 characters',
notEmailDomain: 'Invalid email domain',
emptyRegKey: 'Invite code cannot be empty',
notExistRegKey: 'Invite code does not exist',
noRegKeyTotal: 'Invite code usage limit reached',
regKeyExpire: 'Invite code has expired',
emailAndPwdEmpty: 'Email and password cannot be empty',
notExistUser: 'Email does not exist',
isDelUser: 'This email has been deleted',
isBanUser: 'This email has been banned',
regKeyUseCount: 'Usage count cannot be empty',
emptyRegKeyExpire: 'Valid until time cannot be empty',
isExistRegKye: 'Invite code already exists',
roleNotExist: 'Role does not exist',
emptyRoleName: 'Role name cannot be empty',
roleNameExist: 'Role name already exists',
delDefRole: 'Default role cannot be deleted',
notJsonDomain: 'Environment variable "domain" must be in JSON format',
noOsUpBack: 'Cannot upload background: R2 object storage not configured',
noOsDomainUpBack: 'Cannot upload background: R2 domain not configured',
starNotExistEmail: 'Starred email does not exist',
emptyBotToken: 'Verification token cannot be empty',
botVerifyFail: 'Bot verification failed, please try again',
authExpired: 'Authentication expired, please log in again',
unauthorized: 'Unauthorized',
perms: {
"邮件": "Email",
"邮件发送": "Send email",
"邮件删除": "Delete email",
"邮箱侧栏": "Account",
"邮箱查看": "View account",
"邮箱添加": "Add account",
"邮箱删除": "Delete account",
"个人设置": "Settings",
"用户注销": "Delete user",
"分析页": "Analytics",
"数据查看": "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"
}
};
export default en;

View File

@@ -0,0 +1,30 @@
import i18next from 'i18next';
import zh from './zh.js'
import en from './en.js'
import app from '../hono/hono';
app.use('*', async (c, next) => {
const lang = c.req.header('accept-language').split('-')[0]
i18next.init({
lng: lang,
});
return await next()
})
const resources = {
en: {
translation: en
},
zh: {
translation: zh,
},
};
i18next.init({
fallbackLng: 'en',
resources,
});
export const t = (key) => i18next.t(key)
export default i18next;

View File

@@ -0,0 +1,93 @@
const zh = {
IncorrectPwd: '密码输入错误',
addAccountDisabled: '添加邮箱功能已关闭',
regDisabled: '注册功能已关闭',
emptyEmail: '邮箱不能为空',
notEmail: '非法邮箱',
notExistDomain: '不存在的邮箱域名',
isDelAccount: '该邮箱已被注销',
isRegAccount: '该邮箱已被注册',
accountLimit: '添加邮箱数量到达限制',
delMyAccount: '不可以删除自己的邮箱',
noUserAccount: '该邮箱不属于当前用户',
usernameLengthLimit: '用户名长度超出限制',
noOsDomainSendPic: 'r2域名未配置不能发送正文图片',
noOsSendPic: 'r2对象存储未配置不能发送正文图片',
noOsDomainSendAtt: 'r2域名未配置不能发送附件',
noOsSendAtt: 'r2对象存储未配置不能发送附件',
disabledSend: '邮件发送功能已停用',
noSeparateSend: '分别发送暂时不支持附件',
userNoSendTotal: '用户无发送次数',
daySendLimit: '发送次数已到达每日限制',
totalSendLimit: '发送次数已到达限制',
daySendLack: '当日剩余发送次数不足',
totalSendLack: '剩余发送次数不足',
senderAccountNotExist: '发件人邮箱不存在',
noResendToken: 'resend密钥未配置',
sendEmailNotCurUser: '发件人邮箱非当前用户所有',
notExistEmailReply: '邮件不存在无法回复',
pwdLengthLimit: '密码长度超出限制',
emailLengthLimit: '邮箱长度超出限制',
pwdMinLengthLimit: '密码不能小于6位',
notEmailDomain: '非法邮箱域名',
emptyRegKey: '注册码不能为空',
notExistRegKey: '注册码不存在',
noRegKeyTotal: '注册码使用次数已耗尽',
regKeyExpire: '注册码已过期',
emailAndPwdEmpty: '邮箱和密码不能为空',
notExistUser: '该邮箱不存在',
isDelUser: '该邮箱已被注销',
isBanUser: '该邮箱已被禁用',
regKeyUseCount: '使用次数不能为空',
emptyRegKeyExpire: '有效时间不能为空',
isExistRegKye: '注册码已存在',
roleNotExist: '权限身份不存在',
emptyRoleName: '身份名不能为空',
roleNameExist: '身份名已存在',
delDefRole: '默认身份不能删除',
notJsonDomain: '环境变量domain必须是JSON类型',
noOsUpBack: 'r2对象存储未配置不能上传背景',
noOsDomainUpBack: 'r2域名未配置不上传背景',
starNotExistEmail: '星标的邮件不存在',
emptyBotToken: '验证token不能为空',
botVerifyFail: '人机验证失败,请重试',
authExpired: '身份认证失效,请重新登录',
unauthorized: '权限不足',
perms: {
"邮件": "邮件",
"邮件发送": "邮件发送",
"邮件删除": "邮件删除",
"邮箱侧栏": "邮箱侧栏",
"邮箱查看": "邮箱查看",
"邮箱添加": "邮箱添加",
"邮箱删除": "邮箱删除",
"个人设置": "个人设置",
"用户注销": "用户注销",
"分析页": "分析页",
"数据查看": "数据查看",
"用户信息": "用户信息",
"用户查看": "用户查看",
"用户添加": "用户添加",
"密码修改": "密码修改",
"状态修改": "状态修改",
"权限修改": "权限修改",
"用户删除": "用户删除",
"邮件列表": "全部邮件",
"邮件查看": "邮件查看",
"权限控制": "权限控制",
"身份查看": "身份查看",
"身份修改": "身份修改",
"身份删除": "身份删除",
"注册密钥": "注册密钥",
"密钥查看": "密钥查看",
"密钥添加": "密钥添加",
"密钥删除": "密钥删除",
"系统设置": "系统设置",
"设置查看": "设置查看",
"设置修改": "设置修改",
"物理清空": "物理清空",
'发件重置': '发件重置'
}
}
export default zh

View File

@@ -16,10 +16,16 @@ const init = {
await this.v1_3DB(c);
await this.v1_3_1DB(c);
await this.v1_4DB(c);
await this.v1_5DB(c);
await settingService.refresh(c);
return c.text('初始化成功');
},
async v1_5DB(c) {
await c.env.db.prepare(`UPDATE perm SET perm_key = 'sys-email:list' WHERE perm_key = 'all-email:list'`).run();
await c.env.db.prepare(`UPDATE perm SET perm_key = 'sys-email:delete' WHERE perm_key = 'all-email:delete'`).run();
},
async v1_4DB(c) {
await c.env.db.prepare(`
CREATE TABLE IF NOT EXISTS reg_key (

View File

@@ -5,6 +5,7 @@ import KvConst from '../const/kv-const';
import dayjs from 'dayjs';
import userService from '../service/user-service';
import permService from '../service/perm-service';
import { t } from '../i18n/i18n'
import app from '../hono/hono';
const exclude = [
@@ -29,8 +30,8 @@ const requirePerms = [
'/role/tree',
'/role/set',
'/role/setDefault',
'/sysEmail/list',
'/sysEmail/delete',
'/allEmail/list',
'/allEmail/delete',
'/setting/physicsDeleteAll',
'/setting/setBackground',
'/setting/set',
@@ -67,8 +68,8 @@ const premKey = {
'user:set-status': ['/user/setStatus'],
'user:set-type': ['/user/setType'],
'user:delete': ['/user/delete'],
'sys-email:query': ['/sysEmail/list'],
'sys-email:delete': ['/sysEmail/delete'],
'all-email:query': ['/allEmail/list'],
'all-email:delete': ['/allEmail/delete'],
'setting:query': ['/setting/query'],
'setting:set': ['/setting/set', '/setting/setBackground'],
'setting:clean': ['/setting/physicsDeleteAll'],
@@ -100,18 +101,18 @@ app.use('*', async (c, next) => {
const result = await jwtUtils.verifyToken(c, jwt);
if (!result) {
throw new BizError('身份认证失效,请重新登录', 401);
throw new BizError(t('authExpired'), 401);
}
const { userId, token } = result;
const authInfo = await c.env.kv.get(KvConst.AUTH_INFO + userId, { type: 'json' });
if (!authInfo) {
throw new BizError('身份认证失效,请重新登录', 401);
throw new BizError(t('authExpired'), 401);
}
if (!authInfo.tokens.includes(token)) {
throw new BizError('身份认证失效,请重新登录', 401);
throw new BizError(t('authExpired'), 401);
}
const permIndex = requirePerms.findIndex(item => {
@@ -129,7 +130,7 @@ app.use('*', async (c, next) => {
});
if (userPermIndex === -1 && authInfo.user.email !== c.env.admin) {
throw new BizError('权限不足', 403);
throw new BizError(t('unauthorized'), 403);
}
}

View File

@@ -10,37 +10,39 @@ import { isDel } from '../const/entity-const';
import settingService from './setting-service';
import turnstileService from './turnstile-service';
import roleService from './role-service';
import { t } from '../i18n/i18n';
const accountService = {
async add(c, params, userId) {
if (!await settingService.isAddEmail(c)) {
throw new BizError('添加邮箱功能已关闭');
throw new BizError(t('addAccountDisabled'));
}
let { email, token } = params;
if (!email) {
throw new BizError('邮箱不能为空');
throw new BizError(t('emptyEmail'));
}
if (!verifyUtils.isEmail(email)) {
throw new BizError('非法邮箱');
throw new BizError(t('notEmail'));
}
if (!c.env.domain.includes(emailUtils.getDomain(email))) {
throw new BizError('不存在的邮箱域名');
throw new BizError(t('notExistDomain'));
}
const accountRow = await this.selectByEmailIncludeDelNoCase(c, email);
if (accountRow && accountRow.isDel === isDel.DELETE) {
throw new BizError('该邮箱已被注销');
throw new BizError(t('isDelAccount'));
}
if (accountRow) {
throw new BizError('该邮箱已被注册');
throw new BizError(t('isRegAccount'));
}
const userRow = await userService.selectById(c, userId);
@@ -48,7 +50,7 @@ const accountService = {
if (roleRow.accountCount && userRow.email !== c.env.admin) {
const userAccountCount = await accountService.countUserAccount(c, userId)
if(userAccountCount >= roleRow.accountCount) throw new BizError(`添加邮箱数量到达限制`, 403);
if(userAccountCount >= roleRow.accountCount) throw new BizError(t('accountLimit'), 403);
}
if (await settingService.isAddEmailVerify(c)) {
@@ -109,11 +111,11 @@ const accountService = {
const accountRow = await this.selectById(c, accountId);
if (accountRow.email === user.email) {
throw new BizError('不可以删除自己的邮箱');
throw new BizError(t('delMyAccount'));
}
if (accountRow.userId !== user.userId) {
throw new BizError('该邮箱不属于当前用户');
throw new BizError(t('noUserAccount'));
}
await orm(c).update(account).set({ isDel: isDel.DELETE }).where(
@@ -182,7 +184,7 @@ const accountService = {
async setName(c, params, userId) {
const { name, accountId } = params
if (name.length > 30) {
throw new BizError('用户名长度超出限制');
throw new BizError(t('usernameLengthLimit'));
}
await orm(c).update(account).set({name}).where(and(eq(account.userId, userId),eq(account.accountId, accountId))).run();
}

View File

@@ -8,7 +8,21 @@ import dayjs from 'dayjs';
import { toUtc } from '../utils/date-uitil';
const analysisService = {
async echarts(c) {
async echarts(c, params) {
const { timeZone } = params;
let utcDate = toUtc().startOf('day');
let localDate = utcDate.tz(timeZone);
utcDate = dayjs(utcDate.format('YYYY-MM-DD HH:mm:ss'))
localDate = dayjs(localDate.format('YYYY-MM-DD HH:mm:ss'))
//获取时差
const diffHours = localDate.diff(utcDate, 'hour',true);
const [
@@ -30,17 +44,17 @@ const analysisService = {
.limit(6),
analysisDao.userDayCount(c),
analysisDao.receiveDayCount(c),
analysisDao.sendDayCount(c),
analysisDao.userDayCount(c, diffHours),
analysisDao.receiveDayCount(c, diffHours),
analysisDao.sendDayCount(c, diffHours),
c.env.kv.get(kvConst.SEND_DAY_COUNT + dayjs().format('YYYY-MM-DD')),
]);
const userDayCount = this.filterEmptyDay(userDayCountRaw);
const receiveDayCount = this.filterEmptyDay(receiveDayCountRaw);
const sendDayCount = this.filterEmptyDay(sendDayCountRaw);
const userDayCount = this.filterEmptyDay(userDayCountRaw, timeZone);
const receiveDayCount = this.filterEmptyDay(receiveDayCountRaw, timeZone);
const sendDayCount = this.filterEmptyDay(sendDayCountRaw, timeZone);
const daySendTotal = daySendTotalRaw || 0;
@@ -58,8 +72,8 @@ const analysisService = {
};
},
filterEmptyDay(data) {
const today = toUtc().tz('Asia/Shanghai').subtract(1, 'day');
filterEmptyDay(data, timeZone) {
const today = toUtc().tz(timeZone).subtract(1, 'day');
const previousDays = Array.from({ length: 15 }, (_, i) => {
return today.subtract(i, 'day').format('YYYY-MM-DD');
}).reverse();

View File

@@ -16,6 +16,7 @@ import user from '../entity/user';
import starService from './star-service';
import dayjs from 'dayjs';
import kvConst from '../const/kv-const';
import { t } from '../i18n/i18n'
const emailService = {
@@ -134,27 +135,27 @@ const emailService = {
let { attDataList, html } = await attService.toImageUrlHtml(c, content, r2Domain);
if (attDataList.length > 0 && !r2Domain) {
throw new BizError('r2域名未配置不能发送正文图片');
throw new BizError(t('noOsDomainSendPic'));
}
if (attDataList.length > 0 && !c.env.r2) {
throw new BizError('r2对象存储未配置不能发送正文图片');
throw new BizError(t('noOsSendPic'));
}
if (attachments.length > 0 && !r2Domain) {
throw new BizError('r2域名未配置不能发送附件');
throw new BizError(t('noOsDomainSendAtt'));
}
if (attachments.length > 0 && !c.env.r2) {
throw new BizError('r2对象存储未配置不能发送附件');
throw new BizError(t('noOsSendAtt'));
}
if (send === settingConst.send.CLOSE) {
throw new BizError('邮件发送功能已停用', 403);
throw new BizError(t('disabledSend'), 403);
}
if (attachments.length > 0 && manyType === 'divide') {
throw new BizError('分别发送暂时不支持附件');
throw new BizError(t('noSeparateSend'));
}
@@ -164,17 +165,17 @@ const emailService = {
if (c.env.admin !== userRow.email && roleRow.sendCount) {
if (roleRow.sendCount < 0) {
throw new BizError('用户无发送次数', 403);
throw new BizError(t('userNoSendTotal'), 403);
}
if (userRow.sendCount >= roleRow.sendCount) {
if (roleRow.sendType === 'day') throw new BizError('发送次数已到达每日限制', 403);
if (roleRow.sendType === 'count') throw new BizError('发送次数已到达限制', 403);
if (roleRow.sendType === 'day') throw new BizError(t('daySendLimit'), 403);
if (roleRow.sendType === 'count') throw new BizError(t('totalSendLimit'), 403);
}
if (userRow.sendCount + receiveEmail.length > roleRow.sendCount) {
if (roleRow.sendType === 'day') throw new BizError('当日剩余发送次数不足', 403);
if (roleRow.sendType === 'count') throw new BizError('剩余发送次数不足', 403);
if (roleRow.sendType === 'day') throw new BizError(t('daySendLack'), 403);
if (roleRow.sendType === 'count') throw new BizError(t('totalSendLack'), 403);
}
}
@@ -183,19 +184,19 @@ const emailService = {
const accountRow = await accountService.selectById(c, accountId);
if (!accountRow) {
throw new BizError('发件人邮箱不存在');
throw new BizError(t('senderAccountNotExist'));
}
const domain = emailUtils.getDomain(accountRow.email);
const resendToken = resendTokens[domain];
if (!resendToken) {
throw new BizError('resend密钥未配置');
throw new BizError(t('noResendToken'));
}
if (accountRow.userId !== userId) {
throw new BizError('发件人邮箱非当前用户所有');
throw new BizError(t('sendEmailNotCurUser'));
}
if (!name) {
@@ -211,7 +212,7 @@ const emailService = {
emailRow = await this.selectById(c, emailId);
if (!emailRow) {
throw new BizError('邮件不存在无法回复');
throw new BizError(t('notExistEmailReply'));
}
}

View File

@@ -16,7 +16,8 @@ import turnstileService from './turnstile-service';
import roleService from './role-service';
import regKeyService from './reg-key-service';
import dayjs from 'dayjs';
import { formatDetailDate, toUtc } from '../utils/date-uitil';
import { toUtc } from '../utils/date-uitil';
import { t } from '../i18n/i18n.js';
const loginService = {
@@ -27,27 +28,27 @@ const loginService = {
const {regKey, register} = await settingService.query(c)
if (register === settingConst.register.CLOSE) {
throw new BizError('注册功能已关闭');
throw new BizError(t('regDisabled'));
}
if (!verifyUtils.isEmail(email)) {
throw new BizError('非法邮箱');
throw new BizError(t('notEmail'));
}
if (password.length > 30) {
throw new BizError('密码长度超出限制');
throw new BizError(t('pwdLengthLimit'));
}
if (emailUtils.getName(email).length > 30) {
throw new BizError('邮箱长度超出限制');
throw new BizError(t('emailLengthLimit'));
}
if (password.length < 6) {
throw new BizError('密码至少6位');
throw new BizError(t('pwdMinLengthLimit'));
}
if (!c.env.domain.includes(emailUtils.getDomain(email))) {
throw new BizError('非法邮箱域名');
throw new BizError(t('notEmailDomain'));
}
let type = null;
@@ -68,11 +69,11 @@ const loginService = {
const accountRow = await accountService.selectByEmailIncludeDelNoCase(c, email);
if (accountRow && accountRow.isDel === isDel.DELETE) {
throw new BizError('该邮箱已被注销');
throw new BizError(t('isDelUser'));
}
if (accountRow) {
throw new BizError('该邮箱已被注册');
throw new BizError(t('isRegAccount'));
}
if (await settingService.isRegisterVerify(c)) {
@@ -103,24 +104,24 @@ const loginService = {
async handleOpenRegKey(c, regKey, code) {
if (!code) {
throw new BizError('注册码不能为空');
throw new BizError(t('emptyRegKey'));
}
const regKeyRow = await regKeyService.selectByCode(c, code);
if (!regKeyRow) {
throw new BizError('注册码不存在');
throw new BizError(t('notExistRegKey'));
}
if (regKeyRow.count <= 0) {
throw new BizError('注册码使用次数已耗尽');
throw new BizError(t('noRegKeyCount'));
}
const today = toUtc().tz('Asia/Shanghai').startOf('day')
const expireTime = toUtc(regKeyRow.expireTime).tz('Asia/Shanghai').startOf('day');
if (expireTime.isBefore(today)) {
throw new BizError('注册码已过期');
throw new BizError(t('regKeyExpire'));
}
return { type: regKeyRow.roleId, regKeyId: regKeyRow.regKeyId };
@@ -153,25 +154,25 @@ const loginService = {
const { email, password } = params;
if (!email || !password) {
throw new BizError('邮箱和密码不能为空');
throw new BizError(t('emailAndPwdEmpty'));
}
const userRow = await userService.selectByEmailIncludeDel(c, email);
if (!userRow) {
throw new BizError('该用户不存在');
throw new BizError(t('notExistUser'));
}
if(userRow.isDel === isDel.DELETE) {
throw new BizError('该用户已被注销');
throw new BizError(t('isDelUser'));
}
if(userRow.status === userConst.status.BAN) {
throw new BizError('该用户已被禁用');
throw new BizError(t('isBanUser'));
}
if (!await cryptoUtils.verifyPassword(password, userRow.salt, userRow.password)) {
throw new BizError('密码输入错误');
throw new BizError(t('IncorrectPwd'));
}
const uuid = uuidv4();

View File

@@ -5,13 +5,19 @@ import rolePerm from '../entity/role-perm';
import user from '../entity/user';
import role from '../entity/role';
import { permConst } from '../const/entity-const';
import { t } from '../i18n/i18n'
const permService = {
async tree(c) {
const pList = await orm(c).select().from(perm).where(eq(perm.pid, 0)).orderBy(asc(perm.sort)).all();
const cList = await orm(c).select().from(perm).where(ne(perm.pid, 0)).orderBy(asc(perm.sort)).all();
cList.forEach(cItem => {
cItem.name = t('perms.' + cItem.name)
})
pList.forEach(pItem => {
pItem.name = t('perms.' + pItem.name)
pItem.children = cList.filter(cItem => cItem.pid === pItem.permId)
})
return pList;

View File

@@ -5,6 +5,8 @@ import roleService from './role-service';
import BizError from '../error/biz-error';
import { formatDetailDate, toUtc } from '../utils/date-uitil';
import userService from './user-service';
import { t } from '../i18n/i18n.js';
const regKeyService = {
async add(c, params, userId) {
@@ -12,26 +14,26 @@ const regKeyService = {
let {code,roleId,count,expireTime} = params;
if (!code) {
throw new BizError('注册码不能为空');
throw new BizError(t('emptyRegKey'));
}
if (!count) {
throw new BizError('使用次数不能为空');
throw new BizError(t('emptyRegKey'));
}
if (!expireTime) {
throw new BizError('有效时间不能为空');
throw new BizError(t('emptyRegKeyExpire'));
}
const regKeyRow = await orm(c).select().from(regKey).where(eq(regKey.code, code)).get();
if (regKeyRow) {
throw new BizError('注册码已存在');
throw new BizError(t('isExistRegKye'));
}
const roleRow = roleService.selectById(c, roleId);
if (!roleRow) {
throw new BizError('权限身份不存在');
throw new BizError(t('roleNotExist'));
}
expireTime = formatDetailDate(expireTime)

View File

@@ -7,8 +7,8 @@ import perm from '../entity/perm';
import { permConst, roleConst } from '../const/entity-const';
import userService from './user-service';
import user from '../entity/user';
import emailUtils from '../utils/email-utils';
import verifyUtils from '../utils/verify-utils';
import { t } from '../i18n/i18n.js';
const roleService = {
@@ -17,19 +17,19 @@ const roleService = {
let { name, permIds, banEmail } = params;
if (!name) {
throw new BizError('身份名不能为空');
throw new BizError(t('emptyRoleName'));
}
let roleRow = await orm(c).select().from(role).where(eq(role.name, name)).get();
if (roleRow) {
throw new BizError('身份名已存在');
throw new BizError(t('roleNameExist'));
}
const notEmailIndex = banEmail.findIndex(item => !verifyUtils.isEmail(item))
if (notEmailIndex > -1) {
throw new BizError('非法邮箱');
throw new BizError(t('notEmail'));
}
banEmail = banEmail.join(',')
@@ -67,7 +67,7 @@ const roleService = {
let { name, permIds, roleId, banEmail } = params;
if (!name) {
throw new BizError('名字不能为空');
throw new BizError(t('emptyRoleName'));
}
delete params.isDefault
@@ -75,7 +75,7 @@ const roleService = {
const notEmailIndex = banEmail.findIndex(item => !verifyUtils.isEmail(item))
if (notEmailIndex > -1) {
throw new BizError('非法邮箱');
throw new BizError(t('notEmail'));
}
banEmail = banEmail.join(',')
@@ -97,11 +97,11 @@ const roleService = {
const roleRow = await orm(c).select().from(role).where(eq(role.roleId, roleId)).get();
if (!roleRow) {
throw new BizError('身份不存在');
throw new BizError(t('notExist'));
}
if (roleRow.isDefault) {
throw new BizError('默认身份不能删除');
throw new BizError(t('delDefRole'));
}
const defRoleRow = await orm(c).select().from(role).where(eq(role.isDefault, roleConst.isDefault.OPEN)).get();
@@ -124,7 +124,7 @@ const roleService = {
async setDefault(c, params) {
const roleRow = await orm(c).select().from(role).where(eq(role.roleId, params.roleId)).get();
if (!roleRow) {
throw new BizError('身份不存在');
throw new BizError(t('roleNotExist'));
}
await orm(c).update(role).set({ isDefault: 0 }).run();
await orm(c).update(role).set({ isDefault: 1 }).where(eq(role.roleId, params.roleId)).run();

View File

@@ -9,6 +9,7 @@ import accountService from './account-service';
import userService from './user-service';
import constant from '../const/constant';
import BizError from '../error/biz-error';
import { t } from '../i18n/i18n'
const settingService = {
@@ -22,7 +23,7 @@ const settingService = {
const setting = await c.env.kv.get(KvConst.SETTING, { type: 'json' });
let domainList = c.env.domain;
if (typeof domainList === 'string') {
throw new BizError('环境变量domain必须是JSON类型');
throw new BizError(t('notJsonDomain'));
}
domainList = domainList.map(item => '@' + item);
setting.domainList = domainList;
@@ -70,11 +71,11 @@ const settingService = {
if (!c.env.r2) {
throw new BizError('r2对象存储未配置不能上传背景');
throw new BizError(t('noOsUpBack'));
}
if (!settingRow.r2Domain) {
throw new BizError('r2域名未配置不上传背景');
throw new BizError(t('noOsDomainUpBack'));
}
const { background } = params;

View File

@@ -6,17 +6,17 @@ import { and, desc, eq, lt, sql, inArray } from 'drizzle-orm';
import email from '../entity/email';
import { isDel } from '../const/entity-const';
import attService from "./att-service";
import { t } from '../i18n/i18n'
const starService = {
async add(c, params, userId) {
const { emailId } = params;
const email = await emailService.selectById(c, emailId);
if (!email) {
throw new BizError('星标的邮件不存在');
throw new BizError(t('starNotExistEmail'));
}
if (!email.userId === userId) {
throw new BizError('星标的邮件非当前用户所有');
throw new BizError(t('starNotExistEmail'));
}
const exist = await orm(c).select().from(star).where(
and(

View File

@@ -1,12 +1,13 @@
import BizError from '../error/biz-error';
import settingService from './setting-service';
import { t } from '../i18n/i18n'
const turnstileService = {
async verify(c, token) {
if (!token) {
throw new BizError('验证token不能为空');
throw new BizError(t('emptyBotToken'));
}
const settingRow = await settingService.query(c)
@@ -26,7 +27,7 @@ const turnstileService = {
const result = await res.json();
if (!result.success) {
throw new BizError('人机验证失败,请重试',400)
throw new BizError(t('botVerifyFail'),400)
}
}
};

View File

@@ -2,7 +2,7 @@ import BizError from '../error/biz-error';
import accountService from './account-service';
import orm from '../entity/orm';
import user from '../entity/user';
import { and, asc, count, desc, eq, inArray, like, sql } from 'drizzle-orm';
import { and, asc, count, desc, eq, inArray, sql } from 'drizzle-orm';
import { emailConst, isDel, roleConst, userConst } from '../const/entity-const';
import kvConst from '../const/kv-const';
import KvConst from '../const/kv-const';
@@ -15,6 +15,7 @@ import roleService from './role-service';
import emailUtils from '../utils/email-utils';
import saltHashUtils from '../utils/crypto-utils';
import constant from '../const/constant';
import { t } from '../i18n/i18n'
const userService = {
@@ -50,7 +51,7 @@ const userService = {
const { password } = params;
if (password < 6) {
throw new BizError('密码不能小于6位');
throw new BizError(t('pwdMinLengthLimit'));
}
const { salt, hash } = await cryptoUtils.hashPassword(password);
await orm(c).update(user).set({ password: hash, salt: salt }).where(eq(user.userId, userId)).run();
@@ -295,7 +296,7 @@ const userService = {
const roleRow = await roleService.selectById(c, type);
if (!roleRow) {
throw new BizError('身份不存在');
throw new BizError(t('roleNotExist'));
}
await orm(c)
@@ -327,27 +328,27 @@ const userService = {
const { email, type, password } = params;
if (!c.env.domain.includes(emailUtils.getDomain(email))) {
throw new BizError('非法邮箱域名');
throw new BizError(t('notEmailDomain'));
}
if (password.length < 6) {
throw new BizError('密码必须大于6位');
throw new BizError(t('pwdMinLengthLimit'));
}
const accountRow = await accountService.selectByEmailIncludeDel(c, email);
if (accountRow && accountRow.isDel === isDel.DELETE) {
throw new BizError('该邮箱已被注销');
throw new BizError(t('isDelUser'));
}
if (accountRow) {
throw new BizError('该邮箱已被注册');
throw new BizError(t('isRegAccount'));
}
const role = roleService.selectById(c, type);
if (!role) {
throw new BizError('权限身份不存在');
throw new BizError(t('roleNotExist'));
}
const { salt, hash } = await saltHashUtils.hashPassword(password);