新增中英文切换
This commit is contained in:
77
mail-vue/package-lock.json
generated
77
mail-vue/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
252
mail-vue/src/i18n/en.js
Normal 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
|
||||
12
mail-vue/src/i18n/index.js
Normal file
12
mail-vue/src/i18n/index.js
Normal 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
251
mail-vue/src/i18n/zh.js
Normal 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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} <${email.sendEmail}> 来信:
|
||||
${ formatDetailDate(email.createTime) } ${email.name} <${email.sendEmail}> ${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 () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
9
mail-vue/src/request/all-email.js
Normal file
9
mail-vue/src/request/all-email.js
Normal 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)
|
||||
}
|
||||
@@ -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}});
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -6,9 +6,13 @@ export const useSettingStore = defineStore('setting', {
|
||||
settings: {
|
||||
r2Domain: '',
|
||||
loginOpacity: 1.00,
|
||||
}
|
||||
},
|
||||
lang: '',
|
||||
}),
|
||||
actions: {
|
||||
|
||||
}
|
||||
},
|
||||
persist: {
|
||||
pick: ['lang'],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ export const useUiStore = defineStore('ui', {
|
||||
accountShow: false,
|
||||
backgroundLoading: true,
|
||||
writerRef: null,
|
||||
key: 0,
|
||||
asideCount: {
|
||||
email: 0,
|
||||
send: 0,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' // 灰色标签
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
1
mail-worker/dist/assets/index-BkPsvh6f.css
vendored
1
mail-worker/dist/assets/index-BkPsvh6f.css
vendored
File diff suppressed because one or more lines are too long
181
mail-worker/dist/assets/index-DlA47rIk.js
vendored
181
mail-worker/dist/assets/index-DlA47rIk.js
vendored
File diff suppressed because one or more lines are too long
4
mail-worker/dist/index.html
vendored
4
mail-worker/dist/index.html
vendored
@@ -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">
|
||||
|
||||
633
mail-worker/package-lock.json
generated
633
mail-worker/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
})
|
||||
@@ -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));
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ const constant = {
|
||||
ATTACHMENT_PREFIX: 'attachments/',
|
||||
BACKGROUND_PREFIX: 'static/background/',
|
||||
ADMIN_ROLE: {
|
||||
name: '超级管理员',
|
||||
name: 'admin',
|
||||
sendCount: 0,
|
||||
sendType: 'count',
|
||||
accountCount: 0
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
93
mail-worker/src/i18n/en.js
Normal file
93
mail-worker/src/i18n/en.js
Normal 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;
|
||||
30
mail-worker/src/i18n/i18n.js
Normal file
30
mail-worker/src/i18n/i18n.js
Normal 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;
|
||||
93
mail-worker/src/i18n/zh.js
Normal file
93
mail-worker/src/i18n/zh.js
Normal 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
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user