mirror of
https://github.com/schroinerxy/cloud-mail.git
synced 2026-06-21 19:35:50 +08:00
新增注册码、草稿箱、权限拦截邮件、发件菜单隐藏
This commit is contained in:
Generated
+7
-7
@@ -14,13 +14,13 @@
|
||||
"compressorjs": "^1.2.1",
|
||||
"date-time-format-timezone": "^1.0.22",
|
||||
"dayjs": "^1.11.13",
|
||||
"dexie": "^4.0.11",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.9.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"path": "^0.12.7",
|
||||
"pinia": "^3.0.2",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"postal-mime": "^2.4.3",
|
||||
"screenfull": "^6.0.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-cropper": "^1.1.4",
|
||||
@@ -1826,6 +1826,12 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/dexie": {
|
||||
"version": "4.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/dexie/-/dexie-4.0.11.tgz",
|
||||
"integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.5.0.tgz",
|
||||
@@ -2954,12 +2960,6 @@
|
||||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/postal-mime": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmmirror.com/postal-mime/-/postal-mime-2.4.3.tgz",
|
||||
"integrity": "sha512-KT8P+/We7LH48EhjmLRLEiQx+HI6zZ5TTeuzhuZkMbcJnp9mdmxm/FgiRIzmhZm8mxmk8mtisIAelq1wNwumxg==",
|
||||
"license": "MIT-0"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"compressorjs": "^1.2.1",
|
||||
"date-time-format-timezone": "^1.0.22",
|
||||
"dayjs": "^1.11.13",
|
||||
"dexie": "^4.0.11",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.9.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -51,7 +51,9 @@ http.interceptors.response.use((res) => {
|
||||
})
|
||||
reject(data)
|
||||
}
|
||||
resolve(data.data)
|
||||
setTimeout(() => {
|
||||
resolve(data.data)
|
||||
},1000)
|
||||
})
|
||||
},
|
||||
(error) => {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<div class="email-container">
|
||||
<div class="header-actions">
|
||||
<el-checkbox
|
||||
v-model="checkAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
:disabled="!emailList.length"
|
||||
@change="handleCheckAllChange"
|
||||
v-model="checkAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
:disabled="!emailList.length"
|
||||
@change="handleCheckAllChange"
|
||||
>
|
||||
</el-checkbox>
|
||||
<div class="header-left" :style="'padding-left:' + actionLeft">
|
||||
@@ -33,7 +33,7 @@
|
||||
:data-checked="item.checked"
|
||||
@click="jumpDetails(item)"
|
||||
>
|
||||
<el-checkbox v-model="item.checked" @click.stop></el-checkbox>
|
||||
<el-checkbox :class=" props.type === 'sys-email' ? 'sys-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"/>
|
||||
@@ -102,7 +102,9 @@
|
||||
</div>
|
||||
<div v-else></div>
|
||||
<span class="name">
|
||||
<span>{{ item.name }}</span>
|
||||
<span>
|
||||
<slot name="name" :email="item"> {{ item.name }}</slot>
|
||||
</span>
|
||||
<span>
|
||||
<Icon v-if="item.isStar" icon="fluent-color:star-16" width="18" height="18"/>
|
||||
</span>
|
||||
@@ -111,7 +113,11 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="email-text">
|
||||
<span class="email-subject">{{ item.subject }}</span>
|
||||
<span class="email-subject">
|
||||
<slot name="subject" :email="item">
|
||||
{{ item.subject }}
|
||||
</slot>
|
||||
</span>
|
||||
<span class="email-content">{{ htmlToText(item) }}</span>
|
||||
</div>
|
||||
<div class="user-info" v-if="showUserInfo">
|
||||
@@ -128,7 +134,7 @@
|
||||
<span>{{ item.type === 0 ? item.toEmail : item.sendEmail }}</span>
|
||||
</div>
|
||||
<div class="del-status" v-if="item.isDel">
|
||||
<el-tag type="info" size="small">已删除</el-tag>
|
||||
<el-tag type="danger" size="small">已删除</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,11 +207,15 @@ const props = defineProps({
|
||||
allowStar: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const emit = defineEmits(['jump', 'refresh-before'])
|
||||
const emit = defineEmits(['jump', 'refresh-before', 'delete-draft'])
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const uiStore = useUiStore();
|
||||
@@ -355,13 +365,19 @@ function changeAccountShow() {
|
||||
uiStore.accountShow = !uiStore.accountShow;
|
||||
}
|
||||
|
||||
|
||||
const handleDelete = () => {
|
||||
ElMessageBox.confirm('确认批量删除这些邮件吗?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
|
||||
if (props.type === 'draft') {
|
||||
const draftIds = getSelectedDraftsIds();
|
||||
emit('delete-draft', draftIds);
|
||||
return;
|
||||
}
|
||||
|
||||
const emailIds = getSelectedMailsIds();
|
||||
props.emailDelete(emailIds).then(() => {
|
||||
ElMessage({
|
||||
@@ -395,7 +411,6 @@ function addItem(email) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (props.timeSort) {
|
||||
if (noLoading.value) {
|
||||
emailList.push(email)
|
||||
@@ -430,6 +445,9 @@ function getSelectedMailsIds() {
|
||||
return emailList.filter(item => item.checked).map(item => item.emailId);
|
||||
}
|
||||
|
||||
function getSelectedDraftsIds() {
|
||||
return emailList.filter(item => item.checked).map(item => item.draftId);
|
||||
}
|
||||
|
||||
function updateCheckStatus() {
|
||||
const checkedCount = emailList.filter(item => item.checked).length;
|
||||
@@ -543,6 +561,7 @@ function loadData() {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.noLoading {
|
||||
@@ -625,13 +644,26 @@ function loadData() {
|
||||
}
|
||||
}
|
||||
|
||||
.el-checkbox {
|
||||
.checkbox {
|
||||
display: flex;
|
||||
padding-left: 15px;
|
||||
padding-right: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sys-email-checkbox {
|
||||
display: flex;
|
||||
padding-left: 15px;
|
||||
padding-right: 20px;
|
||||
justify-content: center;
|
||||
@media (min-width: 1200px) {
|
||||
justify-content: start;
|
||||
height: 100%;
|
||||
align-self: start;
|
||||
padding-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.title-column {
|
||||
@media (max-width: 1200px) {
|
||||
grid-template-columns: 1fr !important;
|
||||
@@ -642,7 +674,7 @@ function loadData() {
|
||||
.title {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
grid-template-columns: 240px 1fr;
|
||||
@media (max-width: 1199px) {
|
||||
padding-right: 15px;
|
||||
}
|
||||
@@ -702,6 +734,9 @@ function loadData() {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
@media (min-width: 1200px) {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.email-content {
|
||||
@@ -788,7 +823,7 @@ function loadData() {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
column-gap: 15px;
|
||||
column-gap: 18px;
|
||||
row-gap: 8px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted, onBeforeUnmount, watch, nextTick} from 'vue';
|
||||
import {ref, onMounted, onBeforeUnmount, watch, nextTick, shallowRef, defineEmits} from 'vue';
|
||||
import loading from "@/components/loading/index.vue";
|
||||
import {compressImage} from "@/utils/file-utils.js";
|
||||
defineExpose({
|
||||
clearEditor,
|
||||
focus
|
||||
focus,
|
||||
getContent
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
@@ -27,7 +28,7 @@ const props = defineProps({
|
||||
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
const editor = ref(null);
|
||||
const editor = shallowRef(null);
|
||||
const isInitialized = ref(false);
|
||||
const editorRef = ref(null);
|
||||
const showLoading = ref(false);
|
||||
@@ -162,6 +163,10 @@ function focus() {
|
||||
})
|
||||
}
|
||||
|
||||
function getContent() {
|
||||
return editor.value.getContent()
|
||||
}
|
||||
|
||||
|
||||
function destroyEditor() {
|
||||
if (editor.value) {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import Dexie from "dexie";
|
||||
import {useUserStore} from "@/store/user.js"
|
||||
import { watch, shallowRef } from "vue";
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
|
||||
let db = shallowRef({})
|
||||
|
||||
function createDB() {
|
||||
db.value = new Dexie(userStore.user.email);
|
||||
db.value.version(1).stores({
|
||||
draft: '++draftId,createTime'
|
||||
})
|
||||
|
||||
db.value.version(1).stores({
|
||||
att: 'draftId'
|
||||
})
|
||||
}
|
||||
|
||||
createDB()
|
||||
|
||||
watch(() => userStore.user.email,() => createDB())
|
||||
|
||||
export default db;
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="account-box">
|
||||
<div class="head-opt" >
|
||||
<Icon v-perm="'account:add'" class="icon" icon="ion:add-outline" width="23" height="23" @click="add" />
|
||||
<Icon class="icon" icon="ion:reload" width="18" height="18" @click="refresh" />
|
||||
<Icon v-perm="'account:add'" class="icon add" icon="ion:add-outline" width="23" height="23" @click="add" />
|
||||
<Icon class="icon refresh" icon="ion:reload" width="18" height="18" @click="refresh" />
|
||||
</div>
|
||||
<el-scrollbar class="scrollbar">
|
||||
<div v-infinite-scroll="getAccountList" :infinite-scroll-distance="600" :infinite-scroll-immediate="false">
|
||||
@@ -16,11 +16,12 @@
|
||||
</div>
|
||||
<div class="settings" @click.stop>
|
||||
<Icon icon="streamline-ultimate-color:copy-paste-1" width="19" height="19" @click.stop="copyAccount(item.email)"/>
|
||||
<el-dropdown>
|
||||
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399" />
|
||||
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399" v-if="showNullSetting(item)" />
|
||||
<el-dropdown v-else>
|
||||
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399" />
|
||||
<template #dropdown >
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="openSetName(item)">改名</el-dropdown-item>
|
||||
<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-menu>
|
||||
</template>
|
||||
@@ -217,6 +218,10 @@ function openSetName (accountItem) {
|
||||
setNameShow.value = true
|
||||
}
|
||||
|
||||
function showNullSetting(item) {
|
||||
return !hasPerm('email:send') && !(item.accountId !== userStore.user.accountId && hasPerm('account:delete'))
|
||||
}
|
||||
|
||||
function itemBg(accountId) {
|
||||
return accountStore.currentAccountId === accountId ? 'item-choose' : ''
|
||||
}
|
||||
@@ -388,8 +393,16 @@ path[fill="#ffdda1"] {
|
||||
.icon{
|
||||
cursor: pointer;
|
||||
}
|
||||
.icon:nth-child(2) {
|
||||
margin-left: 15px;
|
||||
.refresh {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.add {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.head-opt:not(.add) .refresh {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
.scrollbar {
|
||||
|
||||
@@ -11,11 +11,16 @@
|
||||
<Icon icon="hugeicons:mailbox-01" width="20" height="20" />
|
||||
<span class="menu-name" style="margin-left: 21px">收件箱</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="router.push({name: 'send'})" index="send"
|
||||
<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>
|
||||
</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>
|
||||
</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" />
|
||||
@@ -46,9 +51,14 @@
|
||||
</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="hugeicons:key-02" width="22" height="22" />
|
||||
<Icon icon="fluent:lock-closed-16-regular" width="22" height="22" />
|
||||
<span class="menu-name" style="margin-left: 20px">权限控制</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>
|
||||
</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" />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="header">
|
||||
<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>
|
||||
</div>
|
||||
<div class="writer-box" @click="openSend">
|
||||
<div v-perm="'email:send'" class="writer-box" @click="openSend">
|
||||
<div class="writer" >
|
||||
<Icon icon="material-symbols:edit-outline-sharp" width="22" height="22" />
|
||||
</div>
|
||||
@@ -95,10 +95,14 @@ const sendType = computed(() => {
|
||||
return '无权限'
|
||||
}
|
||||
|
||||
if (userStore.user.role.sendCount === 0) {
|
||||
if (!userStore.user.role.sendCount) {
|
||||
return '无限制'
|
||||
}
|
||||
|
||||
if (userStore.user.role.sendCount < 0) {
|
||||
return '无次数'
|
||||
}
|
||||
|
||||
if (userStore.user.role.sendType === 'day') {
|
||||
return '每天'
|
||||
}
|
||||
@@ -116,6 +120,11 @@ const sendCount = computed(() => {
|
||||
if (!userStore.user.role.sendCount) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (userStore.user.role.sendCount < 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return userStore.user.sendCount + '/' + userStore.user.role.sendCount
|
||||
})
|
||||
|
||||
@@ -248,6 +257,10 @@ function full() {
|
||||
grid-template-columns: auto auto 1fr;
|
||||
}
|
||||
|
||||
.header.not-send {
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.writer-box {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
@@ -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']">
|
||||
<keep-alive :include="['email','sys-email','send','sys-setting','star','user','role','analysis','reg-key','draft']">
|
||||
<component :is="Component" :key="route.name"/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="send" v-show="show" @click="close">
|
||||
<div class="send" v-show="show">
|
||||
<div class="write-box" @click.stop>
|
||||
<div class="title">
|
||||
<div class="title-left">
|
||||
@@ -15,7 +15,7 @@
|
||||
</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="多个邮箱用, 分开 example1.com,example2.com" >
|
||||
<template #prefix>
|
||||
<div class="item-title">收件人 </div>
|
||||
</template>
|
||||
@@ -56,7 +56,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import tinyEditor from '@/components/tiny-editor/index.vue'
|
||||
import {h, onMounted, onUnmounted, reactive, ref} from "vue";
|
||||
import {h, nextTick, onMounted, onUnmounted, reactive, ref, toRaw} from "vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
import {emailSend} from "@/request/email.js";
|
||||
@@ -66,14 +66,19 @@ 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} from "@/utils/day.js";
|
||||
import {formatDetailDate, fromNow} 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";
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
openReply
|
||||
openReply,
|
||||
openDraft
|
||||
})
|
||||
|
||||
const draftStore = userDraftStore()
|
||||
const settingStore = useSettingStore()
|
||||
const emailStore = useEmailStore();
|
||||
const accountStore = useAccountStore()
|
||||
@@ -84,6 +89,12 @@ const percent = ref(0)
|
||||
let percentMessage = null
|
||||
let sending = false
|
||||
const defValue = ref('')
|
||||
const backReply = reactive({
|
||||
receiveEmail: [],
|
||||
subject: '',
|
||||
content: '',
|
||||
sendType: ''
|
||||
})
|
||||
const form = reactive({
|
||||
sendEmail: '',
|
||||
receiveEmail: [],
|
||||
@@ -95,7 +106,8 @@ const form = reactive({
|
||||
sendType: '',
|
||||
text: '',
|
||||
emailId: 0,
|
||||
attachments: []
|
||||
attachments: [],
|
||||
draftId: null,
|
||||
})
|
||||
|
||||
function addTagChange(val) {
|
||||
@@ -110,10 +122,8 @@ function addTagChange(val) {
|
||||
if (isEmail(email) && !form.receiveEmail.includes(email)) {
|
||||
form.receiveEmail.push(email)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
function checkDistribute() {
|
||||
@@ -155,7 +165,6 @@ function chooseFile() {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const content = await fileToBase64(file)
|
||||
form.attachments.push({content, filename, size, contentType})
|
||||
}
|
||||
@@ -217,7 +226,9 @@ async function sendEmail() {
|
||||
})
|
||||
|
||||
sending = true
|
||||
close()
|
||||
|
||||
show.value = false
|
||||
|
||||
emailSend(form, (e) => {
|
||||
percent.value = Math.round((e.loaded * 98) / e.total)
|
||||
}).then(emailList => {
|
||||
@@ -225,15 +236,25 @@ async function sendEmail() {
|
||||
emailList.forEach(item => {
|
||||
emailStore.sendScroll?.addItem(item)
|
||||
})
|
||||
resetForm()
|
||||
show.value = false
|
||||
|
||||
ElNotification({
|
||||
title: '邮件已发送',
|
||||
type: "success",
|
||||
message: h('span', { style: 'color: teal' }, email.subject),
|
||||
position: 'bottom-right'
|
||||
})
|
||||
|
||||
userStore.refreshUserInfo();
|
||||
|
||||
if (form.draftId) {
|
||||
form.subject = ''
|
||||
form.content = ''
|
||||
form.receiveEmail = []
|
||||
draftStore.setDraft = {...toRaw(form)}
|
||||
}
|
||||
|
||||
resetForm()
|
||||
show.value = false
|
||||
}).catch((e) => {
|
||||
ElNotification({
|
||||
title: '发送失败',
|
||||
@@ -241,6 +262,7 @@ async function sendEmail() {
|
||||
message: h('span', { style: 'color: teal' }, e.message),
|
||||
position: 'bottom-right'
|
||||
})
|
||||
show.value = true
|
||||
}).finally(() => {
|
||||
percentMessage.close()
|
||||
percent.value = 0
|
||||
@@ -255,8 +277,13 @@ function resetForm() {
|
||||
form.content = ''
|
||||
form.manyType = null
|
||||
form.attachments = []
|
||||
form.sendType = null
|
||||
form.sendType = ''
|
||||
form.emailId = 0
|
||||
form.draftId = null
|
||||
backReply.content = ''
|
||||
backReply.subject = ''
|
||||
backReply.receiveEmail = []
|
||||
backReply.sendType = ''
|
||||
editor.value.clearEditor()
|
||||
}
|
||||
|
||||
@@ -291,7 +318,13 @@ function openReply(email) {
|
||||
</article>
|
||||
</blockquote>`
|
||||
open()
|
||||
console.log(defValue.value)
|
||||
|
||||
nextTick(() => {
|
||||
backReply.content = editor.value.getContent()
|
||||
backReply.subject = form.subject
|
||||
backReply.receiveEmail = form.receiveEmail
|
||||
backReply.sendType = form.sendType
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
@@ -299,7 +332,6 @@ function openReply(email) {
|
||||
function formatImage(content) {
|
||||
content = content || '';
|
||||
const domain = settingStore.settings.r2Domain;
|
||||
console.log(content)
|
||||
return content.replace(/{{domain}}/g, domain + '/');
|
||||
}
|
||||
|
||||
@@ -317,6 +349,14 @@ function open() {
|
||||
editor.value.focus()
|
||||
}
|
||||
|
||||
function openDraft(draft) {
|
||||
Object.assign(form,{...draft})
|
||||
defValue.value = ''
|
||||
setTimeout(() => defValue.value = form.content)
|
||||
show.value = true;
|
||||
editor.value.focus()
|
||||
}
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
close()
|
||||
@@ -332,7 +372,52 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
function close() {
|
||||
show.value = false;
|
||||
|
||||
if (form.draftId) {
|
||||
draftStore.setDraft = {...toRaw(form)}
|
||||
show.value = false
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(form.content || form.subject || form.receiveEmail.length > 0)) {
|
||||
show.value = false
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
|
||||
if (backReply.sendType === 'reply') {
|
||||
let subjectFlag = form.subject === backReply.subject
|
||||
let contentFlag = editor.value.getContent() === backReply.content
|
||||
let receiveFlag = form.receiveEmail.length === 1 && form.receiveEmail[0] === backReply.receiveEmail[0]
|
||||
if (subjectFlag && contentFlag && receiveFlag) {
|
||||
resetForm();
|
||||
close()
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ElMessageBox.confirm('是否保存草稿?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
distinguishCancelAndClose: true
|
||||
}).then( async () => {
|
||||
const formData = {...toRaw(form)}
|
||||
delete formData.draftId
|
||||
delete formData.attachments
|
||||
formData.createTime = dayjs().utc().format('YYYY-MM-DD HH:mm:ss');
|
||||
const draftId = await db.value.draft.add({...formData})
|
||||
db.value.att.add({draftId,attachments: toRaw(form.attachments)})
|
||||
draftStore.refreshList ++
|
||||
show.value = false
|
||||
}).catch((action) => {
|
||||
if (action === 'cancel') {
|
||||
show.value = false
|
||||
resetForm()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import http from '@/axios/index.js';
|
||||
|
||||
export function regKeyList(params) {
|
||||
return http.get('/regKey/list', {params:{...params}})
|
||||
}
|
||||
|
||||
export function regKeyAdd(form) {
|
||||
return http.post('/regKey/add',form)
|
||||
}
|
||||
|
||||
export function regKeyDelete(regKeyIds) {
|
||||
return http.delete('/regKey/delete?regKeyIds='+ regKeyIds)
|
||||
}
|
||||
|
||||
export function regKeyClearNotUse() {
|
||||
return http.delete('/regKey/clearNotUse')
|
||||
}
|
||||
|
||||
export function regKeyHistory(regKeyId) {
|
||||
return http.get('/regKey/history', {params:{regKeyId}})
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import http from '@/axios/index.js';
|
||||
|
||||
export function sysEmailAll(params) {
|
||||
return http.get('/sys-email/list', {params: {...params}})
|
||||
export function sysEmailList(params) {
|
||||
return http.get('/sysEmail/list', {params: {...params}})
|
||||
}
|
||||
|
||||
export function sysEmailDelete(emailIds) {
|
||||
return http.delete('/sys-email/delete?emailIds=' + emailIds)
|
||||
return http.delete('/sysEmail/delete?emailIds=' + emailIds)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,16 @@ const routes = [
|
||||
menu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/draft',
|
||||
name: 'draft',
|
||||
component: () => import('@/views/draft/index.vue'),
|
||||
meta: {
|
||||
title: '草稿箱',
|
||||
name: 'draft',
|
||||
menu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/content',
|
||||
name: 'content',
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const userDraftStore = defineStore('draft', {
|
||||
state: () => ({
|
||||
refreshList: 0,
|
||||
setDraft: {},
|
||||
})
|
||||
})
|
||||
@@ -40,4 +40,8 @@ export function formatDetailDate(time) {
|
||||
|
||||
export function tzDayjs(time) {
|
||||
return dayjs.utc(time).tz('Asia/Shanghai')
|
||||
}
|
||||
|
||||
export function toUtc() {
|
||||
return dayjs().utc()
|
||||
}
|
||||
@@ -47,6 +47,16 @@ const routers = {
|
||||
menu: true
|
||||
}
|
||||
},
|
||||
'reg-key:query': {
|
||||
path: '/sys/reg-key',
|
||||
name: 'reg-key',
|
||||
component: () => import('@/views/reg-key/index.vue'),
|
||||
meta: {
|
||||
title: '注册密钥',
|
||||
name: 'reg-key',
|
||||
menu: true
|
||||
}
|
||||
},
|
||||
'sys-email:query': {
|
||||
path: '/sys/email',
|
||||
name: 'sys-email',
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export function getTextWidth(text, font = '14px sans-serif') {
|
||||
// 强制设置 Canvas 分辨率
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 2000; // 足够大的画布
|
||||
canvas.style.width = '1000px'; // 避免 CSS 缩放影响
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.font = font;
|
||||
return ctx.measureText(text).width;
|
||||
}
|
||||
@@ -680,7 +680,7 @@ function createSendGauge() {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
gap: 20px;
|
||||
@media (max-width: 1024px) {
|
||||
@media (max-width: 1199px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
@@ -691,7 +691,7 @@ function createSendGauge() {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
padding: 25px 20px;
|
||||
padding: 21px 20px;
|
||||
.top {
|
||||
display: grid;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<emailScroll ref="scroll"
|
||||
:allow-star="false"
|
||||
:cancel-success="cancelStar"
|
||||
:getEmailList="getEmailList"
|
||||
:emailDelete="emailDelete"
|
||||
:star-add="starAdd"
|
||||
:star-cancel="starCancel"
|
||||
@jump="jumpContent"
|
||||
actionLeft="6px"
|
||||
:show-account-icon="false"
|
||||
:showStar="false"
|
||||
@delete-draft="deleteDraft"
|
||||
:type="'draft'"
|
||||
>
|
||||
<template #name="props">
|
||||
<span class="send-email" >{{props.email.receiveEmail.join(',') || '(无收件人)'}}</span>
|
||||
</template>
|
||||
<template #subject="props" >
|
||||
{{props.email.subject || '(无标题)'}}
|
||||
</template>
|
||||
</emailScroll>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import emailScroll from "@/components/email-scroll/index.vue"
|
||||
import {emailDelete} from "@/request/email.js";
|
||||
import {starAdd, starCancel} from "@/request/star.js";
|
||||
import {useEmailStore} from "@/store/email.js";
|
||||
import {defineOptions, onMounted, ref, watch, toRaw} from "vue";
|
||||
import {useUiStore} from "@/store/ui.js";
|
||||
import {userDraftStore} from "@/store/draft.js";
|
||||
import db from "@/db/db.js"
|
||||
|
||||
defineOptions({
|
||||
name: 'draft'
|
||||
})
|
||||
|
||||
const draftStore = userDraftStore();
|
||||
const uiStore = useUiStore();
|
||||
const scroll = ref({})
|
||||
const emailStore = useEmailStore();
|
||||
|
||||
watch(() => draftStore.setDraft, async () => {
|
||||
|
||||
const draft = toRaw(draftStore.setDraft)
|
||||
const draftId = draft.draftId
|
||||
const attachments = toRaw(draftStore.setDraft.attachments)
|
||||
|
||||
delete draft.draftId
|
||||
delete draft.attachments
|
||||
|
||||
if (!draft.content && !draft.subject && !(draft.receiveEmail.length > 0)) {
|
||||
await db.value.draft.delete(draftId);
|
||||
await db.value.att.delete(draftId);
|
||||
scroll.value.refreshList();
|
||||
return;
|
||||
}
|
||||
|
||||
await db.value.draft.update(draftId, draft);
|
||||
await db.value.att.update(draftId, {attachments: attachments});
|
||||
scroll.value.refreshList();
|
||||
},{
|
||||
deep: true
|
||||
})
|
||||
|
||||
watch(() => draftStore.refreshList,() => {
|
||||
scroll.value.refreshList();
|
||||
})
|
||||
|
||||
function getEmailList() {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.value.draft.orderBy('createTime').reverse().toArray().then(list => {
|
||||
resolve({list})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteDraft(draftIds) {
|
||||
await db.value.draft.bulkDelete(draftIds);
|
||||
scroll.value.refreshList();
|
||||
}
|
||||
|
||||
async function jumpContent(email) {
|
||||
const att = await db.value.att.get(email.draftId)
|
||||
email.attachments = att.attachments
|
||||
uiStore.writerRef.openDraft(email);
|
||||
}
|
||||
|
||||
function cancelStar(email) {
|
||||
emailStore.cancelStarEmailId = email.emailId
|
||||
scroll.value.deleteEmail([email.emailId])
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emailStore.starScroll = scroll
|
||||
})
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.send-email {
|
||||
font-weight: normal;
|
||||
}
|
||||
</style>
|
||||
@@ -69,6 +69,8 @@
|
||||
</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" />
|
||||
<div v-show="verifyShow"
|
||||
class="register-turnstile"
|
||||
:data-sitekey="settingStore.settings.siteKey"
|
||||
@@ -116,7 +118,8 @@ const suffix = ref('')
|
||||
const registerForm = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
confirmPassword: '',
|
||||
code: null
|
||||
})
|
||||
const domainList = settingStore.domainList;
|
||||
const registerLoading = ref(false)
|
||||
@@ -248,6 +251,20 @@ function submitRegister() {
|
||||
return
|
||||
}
|
||||
|
||||
if(settingStore.settings.regKey === 0) {
|
||||
|
||||
if (!registerForm.code) {
|
||||
|
||||
ElMessage({
|
||||
message: '注册码不能为空',
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!verifyToken && settingStore.settings.registerVerify === 0) {
|
||||
verifyShow.value = true
|
||||
if (!turnstileId) {
|
||||
@@ -263,11 +280,20 @@ function submitRegister() {
|
||||
}
|
||||
|
||||
registerLoading.value = true
|
||||
register({email: registerForm.email + suffix.value, password: registerForm.password, token: verifyToken}).then(() => {
|
||||
|
||||
const form = {
|
||||
email: registerForm.email + suffix.value,
|
||||
password: registerForm.password,
|
||||
token: verifyToken,
|
||||
code: registerForm.code
|
||||
}
|
||||
|
||||
register(form).then(() => {
|
||||
show.value = 'login'
|
||||
registerForm.email = ''
|
||||
registerForm.password = ''
|
||||
registerForm.confirmPassword = ''
|
||||
registerForm.code = ''
|
||||
registerLoading.value = false
|
||||
turnstileId = null
|
||||
verifyToken = ''
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
<template>
|
||||
<div class="reg-key">
|
||||
<div class="header-actions">
|
||||
<Icon class="icon" icon="ion:add-outline" width="23" height="23" @click="openAdd"/>
|
||||
<div class="search">
|
||||
<el-input
|
||||
v-model="params.code"
|
||||
class="search-input"
|
||||
placeholder="输入注册码搜索"
|
||||
>
|
||||
</el-input>
|
||||
</div>
|
||||
<Icon class="icon" icon="iconoir:search" @click="search" width="20" height="20"/>
|
||||
<Icon class="icon" icon="ion:reload" width="18" height="18" @click="refresh"/>
|
||||
<Icon class="icon" icon="fluent:broom-sparkle-16-regular" width="22" height="22" @click="clearNotUse"/>
|
||||
</div>
|
||||
|
||||
<el-scrollbar class="scrollbar" :style="`background: ${regKeyData.length > 0 ? '#FAFCFF;' : '#FFF'}`">
|
||||
<div class="loading" :class="regKeyLoading ? 'loading-show' : 'loading-hide'">
|
||||
<loading />
|
||||
</div>
|
||||
<div class="code-box">
|
||||
<div class="code-item" v-for="item in regKeyData">
|
||||
<div class="code-info">
|
||||
<div class="info-left">
|
||||
<div class="info-left-item">
|
||||
<span class="code" @click="copyCode(item.code)">{{item.code}}</span>
|
||||
</div>
|
||||
<div class="info-left-item">
|
||||
<div>剩余次数:</div>
|
||||
<div v-if="item.count">{{item.count}}</div>
|
||||
<el-tag v-else type="danger">已用尽</el-tag>
|
||||
</div>
|
||||
<div class="info-left-item">
|
||||
<div>权限身份:</div>
|
||||
<el-tag>{{item.roleName}}</el-tag>
|
||||
</div>
|
||||
<div class="info-left-item">
|
||||
<div>有效至期:</div>
|
||||
<div v-if="item.expireTime">{{ formatExpireTime(item.expireTime)}}</div>
|
||||
<el-tag v-else type="danger">已过期</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-right">
|
||||
<el-dropdown class="setting">
|
||||
<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-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty" v-if="regKeyData.length === 0">
|
||||
<el-empty v-if="!regKeyFirst" :image-size="isMobile ? 120 : 0" description="没有任何注册码"/>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<el-dialog v-model="showAdd" title="添加注册码">
|
||||
<div class="container">
|
||||
<el-input v-model="addForm.code" placeholder="注册码">
|
||||
<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-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="有效至期"
|
||||
/>
|
||||
<el-input-number v-model="addForm.count" :min="1" :max="99999"/>
|
||||
<el-button class="btn" type="primary" @click="submit" :loading="addLoading"
|
||||
>添加
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog class="history-list" v-model="showRegKeyHistory" title="使用记录">
|
||||
<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>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineOptions, reactive, ref, watch} from "vue"
|
||||
import {Icon} from "@iconify/vue";
|
||||
import loading from "@/components/loading/index.vue";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {roleSelectUse} from "@/request/role.js";
|
||||
import {useRoleStore} from "@/store/role.js";
|
||||
import {regKeyAdd, regKeyList, regKeyClearNotUse, regKeyDelete, regKeyHistory} from "@/request/reg-key.js";
|
||||
import { getTextWidth } from "@/utils/text.js";
|
||||
import dayjs from "dayjs";
|
||||
import {tzDayjs} from "@/utils/day.js";
|
||||
|
||||
defineOptions({
|
||||
name: 'reg-key'
|
||||
})
|
||||
|
||||
const roleStore = useRoleStore();
|
||||
const settingStore = useSettingStore();
|
||||
const params = reactive({
|
||||
code: '',
|
||||
})
|
||||
|
||||
const roleList = reactive([])
|
||||
const addLoading = ref(false)
|
||||
const showAdd = ref(false)
|
||||
const regKeyLoading = ref(true)
|
||||
const regKeyFirst = ref(true)
|
||||
const showRegKeyHistory = ref(false)
|
||||
const historyList = reactive([])
|
||||
const emailColumnWidth = ref(0)
|
||||
const createTimeColumnWidth = ref(0)
|
||||
const historyLoading = ref(false)
|
||||
const isMobile = window.innerWidth < 1025
|
||||
|
||||
const addForm = reactive({
|
||||
code: '',
|
||||
count: 1,
|
||||
roleId: null,
|
||||
expireTime: null
|
||||
})
|
||||
|
||||
const regKeyData = reactive([])
|
||||
|
||||
getList(true)
|
||||
|
||||
roleSelectUse().then(list => {
|
||||
roleList.length = 0
|
||||
roleList.push(...list)
|
||||
})
|
||||
|
||||
watch(() => roleStore.refresh, () => {
|
||||
roleSelectUse().then(list => {
|
||||
roleList.length = 0
|
||||
roleList.push(...list)
|
||||
})
|
||||
})
|
||||
|
||||
function openHistory(regKey) {
|
||||
|
||||
historyList.length = 0
|
||||
historyLoading.value = true
|
||||
regKeyHistory(regKey.regKeyId).then(list => {
|
||||
|
||||
historyList.push(...list)
|
||||
|
||||
if (list.length > 0) {
|
||||
|
||||
const email = list.reduce((a, b) =>
|
||||
a.email.length > b.email.length ? a : b
|
||||
).email;
|
||||
|
||||
emailColumnWidth.value = getTextWidth(email) + 30
|
||||
emailColumnWidth.value = emailColumnWidth.value < 300 ? emailColumnWidth.value : 300
|
||||
|
||||
const createTime = list.reduce((a, b) =>
|
||||
a.createTime.length > b.email.createTime ? a : b
|
||||
);
|
||||
createTimeColumnWidth.value = getTextWidth(createTime) + 30
|
||||
}
|
||||
|
||||
}).finally(() => {
|
||||
historyLoading.value = false
|
||||
})
|
||||
|
||||
showRegKeyHistory.value = true
|
||||
}
|
||||
|
||||
function formatUserCreateTime(regKey) {
|
||||
const createTime = tzDayjs(regKey.createTime);
|
||||
const currentYear = dayjs().year();
|
||||
const expireYear = createTime.year();
|
||||
|
||||
if (expireYear === currentYear) {
|
||||
return createTime.format('M月D日 HH:mm');
|
||||
} else {
|
||||
return createTime.format('YYYY年M月D日 HH:mm');
|
||||
}
|
||||
}
|
||||
|
||||
function formatExpireTime(expireTime) {
|
||||
|
||||
expireTime = tzDayjs(expireTime);
|
||||
const currentYear = dayjs().year();
|
||||
const expireYear = expireTime.year();
|
||||
|
||||
if (expireYear === currentYear) {
|
||||
return expireTime.format('M月D日');
|
||||
} else {
|
||||
return expireTime.format('YYYY年M月D日');
|
||||
}
|
||||
}
|
||||
function refresh() {
|
||||
params.code = null
|
||||
getList(true)
|
||||
}
|
||||
|
||||
function search() {
|
||||
getList(true)
|
||||
}
|
||||
|
||||
function getList(showLoading = false) {
|
||||
if (showLoading) {
|
||||
regKeyLoading.value = true
|
||||
}
|
||||
regKeyList(params).then(list => {
|
||||
regKeyData.length = 0
|
||||
regKeyData.push(...list)
|
||||
regKeyLoading.value = false
|
||||
regKeyFirst.value = false
|
||||
})
|
||||
}
|
||||
|
||||
async function copyCode(code) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
ElMessage({
|
||||
message: '复制成功',
|
||||
type: 'success',
|
||||
plain: true,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err);
|
||||
ElMessage({
|
||||
message: '复制失败',
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function genCode() {
|
||||
addForm.code = generateRandomCode()
|
||||
}
|
||||
|
||||
function generateRandomCode(length = 8) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function clearNotUse() {
|
||||
ElMessageBox.confirm(`确认清除所有不可用的注册码?`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
regKeyClearNotUse().then(() => {
|
||||
ElMessage({
|
||||
message: '清除成功',
|
||||
type: 'success',
|
||||
plain: true,
|
||||
})
|
||||
getList()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function submit() {
|
||||
|
||||
if (!addForm.code) {
|
||||
ElMessage({
|
||||
message: "注册码不能为空",
|
||||
type: "error",
|
||||
plain: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!addForm.roleId) {
|
||||
ElMessage({
|
||||
message: "身份类型不能为空",
|
||||
type: "error",
|
||||
plain: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!addForm.expireTime) {
|
||||
ElMessage({
|
||||
message: "有效时间不能为空",
|
||||
type: "error",
|
||||
plain: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!addForm.count) {
|
||||
ElMessage({
|
||||
message: "使用次数不能为空",
|
||||
type: "error",
|
||||
plain: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
addLoading.value = true
|
||||
regKeyAdd(addForm).then(() => {
|
||||
showAdd.value = false
|
||||
resetForm()
|
||||
ElMessage({
|
||||
message: "添加成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
getList()
|
||||
}).finally(() => {
|
||||
addLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function deleteRegKey(regKey){
|
||||
ElMessageBox.confirm(`确认删除${regKey.code}吗?`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
regKeyDelete([regKey.regKeyId]).then(() => {
|
||||
getList()
|
||||
ElMessage({
|
||||
message: "删除成功",
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function resetForm(){
|
||||
addForm.code = ''
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
genCode()
|
||||
showAdd.value = true
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.reg-key {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
height: calc(100% - 48px);
|
||||
position: relative;
|
||||
@media (max-width: 372px) {
|
||||
height: calc(100% - 85px);
|
||||
}
|
||||
.code-box {
|
||||
padding: 15px 15px 25px 15px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 15px;
|
||||
.code-item {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
transition: all 300ms;
|
||||
padding: 15px;
|
||||
.code-info {
|
||||
display: flex;
|
||||
.info-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.info-left-item {
|
||||
display: flex;
|
||||
padding-top: 5px;
|
||||
.code {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.info-left-item:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.info-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 2px;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.history-list.el-dialog) {
|
||||
min-height: 300px;
|
||||
width: 500px !important;
|
||||
@media (max-width: 540px) {
|
||||
width: calc(100% - 40px) !important;
|
||||
margin-right: 20px !important;
|
||||
margin-left: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.history-list .loading {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
z-index: 0;
|
||||
background: rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
:deep(.history-list .el-dialog__header) {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
:deep(.el-scrollbar__view) {
|
||||
height: calc(100% - 80px);
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.loading-show {
|
||||
transition: all 200ms ease 200ms;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.loading-hide {
|
||||
pointer-events: none;
|
||||
transition: all 200ms;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
width: 400px !important;
|
||||
@media (max-width: 440px) {
|
||||
width: calc(100% - 40px) !important;
|
||||
margin-right: 20px !important;
|
||||
margin-left: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.setting {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gen-code {
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
padding: 9px 15px;
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
|
||||
font-size: 18px;
|
||||
@media (max-width: 767px) {
|
||||
gap: 15px;
|
||||
}
|
||||
.search-input {
|
||||
width: min(200px, calc(100vw - 140px));
|
||||
}
|
||||
|
||||
.search {
|
||||
:deep(.el-input-group) {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__inner-wrapper:before) {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -47,10 +47,15 @@
|
||||
</el-table>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<el-dialog class="dialog" v-model="roleFormShow" :title="dialogType.title" @closed="resetForm">
|
||||
<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-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-group>
|
||||
<div class="dialog-input">
|
||||
<el-input-number placeholder="排序" :min="0" :max="9999" v-model.number="form.sort" controls-position="right" autocomplete="off" />
|
||||
</div>
|
||||
@@ -74,15 +79,18 @@
|
||||
<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" :min="0" :max="99999" size="small" placeholder="数量" >
|
||||
<el-input-number v-model="form.sendCount" controls-position="right" :max="99999" size="small" placeholder="数量" >
|
||||
</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>
|
||||
<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="数量" >
|
||||
</el-input-number>
|
||||
</span>
|
||||
</div>
|
||||
@@ -102,6 +110,7 @@ import {roleAdd, roleDelete, rolePermTree, roleRoleList, roleSet, roleSetDef} fr
|
||||
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";
|
||||
|
||||
defineOptions({
|
||||
name: 'role'
|
||||
@@ -128,9 +137,11 @@ const dialogType = reactive({
|
||||
const form = reactive({
|
||||
name: null,
|
||||
description: null,
|
||||
banEmail: [],
|
||||
banEmailType: 0,
|
||||
sendType: 'count',
|
||||
sendCount: '',
|
||||
accountCount: '',
|
||||
sendCount: 0,
|
||||
accountCount: 0,
|
||||
sort: 0,
|
||||
isDefault: 0,
|
||||
})
|
||||
@@ -145,6 +156,21 @@ rolePermTree().then(tree => {
|
||||
treeList.push(...tree)
|
||||
})
|
||||
|
||||
function banEmailAddTag(val) {
|
||||
const emails = Array.from(new Set(
|
||||
val.split(/[,,]/).map(item => item.trim()).filter(item => item)
|
||||
));
|
||||
|
||||
form.banEmail.splice(form.banEmail.length - 1, 1)
|
||||
|
||||
emails.forEach(email => {
|
||||
if (isEmail(email) && !form.banEmail.includes(email)) {
|
||||
form.banEmail.push(email)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function roleFormClick() {
|
||||
if (dialogType.type === 'add') {
|
||||
addRole()
|
||||
@@ -240,8 +266,10 @@ function resetForm() {
|
||||
form.description = null
|
||||
form.sort = 0
|
||||
form.sendType = 'count'
|
||||
form.sendCount = ''
|
||||
form.accountCount = ''
|
||||
form.sendCount = 0
|
||||
form.accountCount = 0
|
||||
form.banEmail = []
|
||||
form.banEmailType = 0
|
||||
tree.value.setCheckedKeys([])
|
||||
}
|
||||
|
||||
@@ -256,6 +284,7 @@ function openRoleSet(role) {
|
||||
form.sendType = role.sendType
|
||||
form.sendCount = role.sendCount
|
||||
form.accountCount = role.accountCount
|
||||
form.banEmail = role.banEmail
|
||||
nextTick(() => {
|
||||
tree.value.setCheckedKeys(role.permIds)
|
||||
})
|
||||
@@ -346,7 +375,7 @@ window.onresize = () => {
|
||||
padding: 9px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
gap: 18px;
|
||||
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
|
||||
font-size: 18px;
|
||||
.search {
|
||||
@@ -362,6 +391,14 @@ window.onresize = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
position: relative;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
color: gray;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.description) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -397,6 +434,12 @@ window.onresize = () => {
|
||||
.dialog-input {
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
.dialog-radio {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.dialog-input-tag {
|
||||
}
|
||||
}
|
||||
|
||||
.perm-expand {
|
||||
@@ -406,11 +449,11 @@ window.onresize = () => {
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-dialog) {
|
||||
margin-top: 15vh !important;
|
||||
margin-bottom: 20px !important;
|
||||
width: 400px !important;
|
||||
@media (max-width: 440px) {
|
||||
width: 460px !important;
|
||||
@media (max-width: 500px) {
|
||||
width: calc(100% - 40px) !important;
|
||||
margin-right: 20px !important;
|
||||
margin-left: 20px !important;
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
:show-star="false"
|
||||
show-user-info
|
||||
show-status
|
||||
actionLeft="4px"
|
||||
:show-account-icon="false"
|
||||
@jump="jumpContent"
|
||||
@refresh-before="refreshBefore"
|
||||
:type="'sys-email'"
|
||||
|
||||
>
|
||||
<template #first>
|
||||
@@ -23,7 +25,7 @@
|
||||
<div @click.stop="openSelect">
|
||||
<el-select
|
||||
ref="mySelect"
|
||||
v-model="searchType"
|
||||
v-model="params.searchType"
|
||||
placeholder="请选择"
|
||||
class="select"
|
||||
>
|
||||
@@ -59,10 +61,10 @@
|
||||
<script setup>
|
||||
import {starAdd, starCancel} from "@/request/star.js";
|
||||
import emailScroll from "@/components/email-scroll/index.vue"
|
||||
import {computed, defineOptions, reactive, ref} from "vue";
|
||||
import {computed, defineOptions, reactive, ref, watch} from "vue";
|
||||
import {useEmailStore} from "@/store/email.js";
|
||||
import {
|
||||
sysEmailAll,
|
||||
sysEmailList,
|
||||
sysEmailDelete
|
||||
} from "@/request/sys-email.js";
|
||||
import {Icon} from "@iconify/vue";
|
||||
@@ -74,7 +76,6 @@ defineOptions({
|
||||
|
||||
const emailStore = useEmailStore();
|
||||
const sysEmailScroll = ref({})
|
||||
const searchType = ref('name')
|
||||
const searchValue = ref('')
|
||||
const mySelect = ref()
|
||||
|
||||
@@ -88,15 +89,31 @@ const params = reactive({
|
||||
userEmail: null,
|
||||
accountEmail: null,
|
||||
name: null,
|
||||
subject: null
|
||||
subject: null,
|
||||
searchType: 'name'
|
||||
})
|
||||
|
||||
|
||||
const selectTitle = computed(() => {
|
||||
if (searchType.value === 'user') return '用户'
|
||||
if (searchType.value === 'account') return '邮箱'
|
||||
if (searchType.value === 'name') return '发件人'
|
||||
if (searchType.value === 'subject') return '主题'
|
||||
if (params.searchType === 'user') return '用户'
|
||||
if (params.searchType === 'account') return '邮箱'
|
||||
if (params.searchType === 'name') return '发件人'
|
||||
if (params.searchType === 'subject') return '主题'
|
||||
})
|
||||
|
||||
const paramsStar = localStorage.getItem('sys-email-params')
|
||||
if (paramsStar) {
|
||||
const locaParams = JSON.parse(paramsStar)
|
||||
params.type = locaParams.type
|
||||
params.timeSort = locaParams.timeSort
|
||||
params.status = locaParams.status
|
||||
params.searchType = locaParams.searchType
|
||||
}
|
||||
|
||||
watch(() => params, () => {
|
||||
localStorage.setItem('sys-email-params',JSON.stringify(params))
|
||||
}, {
|
||||
deep: true
|
||||
})
|
||||
|
||||
function refreshBefore() {
|
||||
@@ -107,7 +124,7 @@ function refreshBefore() {
|
||||
params.accountEmail = null
|
||||
params.name = null
|
||||
params.subject = null
|
||||
|
||||
params.searchType = 'name'
|
||||
}
|
||||
|
||||
function search() {
|
||||
@@ -117,19 +134,19 @@ function search() {
|
||||
params.name = null
|
||||
params.subject = null
|
||||
|
||||
if (searchType.value === 'user') {
|
||||
if (params.searchType === 'user') {
|
||||
params.userEmail = searchValue.value
|
||||
}
|
||||
|
||||
if (searchType.value === 'account') {
|
||||
if (params.searchType === 'account') {
|
||||
params.accountEmail = searchValue.value
|
||||
}
|
||||
|
||||
if (searchType.value === 'name') {
|
||||
if (params.searchType === 'name') {
|
||||
params.name = searchValue.value
|
||||
}
|
||||
|
||||
if (searchType.value === 'subject') {
|
||||
if (params.searchType === 'subject') {
|
||||
params.subject = searchValue.value
|
||||
}
|
||||
|
||||
@@ -151,7 +168,7 @@ function jumpContent(email) {
|
||||
|
||||
|
||||
function getEmailList(emailId, size) {
|
||||
return sysEmailAll({emailId, size, ...params})
|
||||
return sysEmailList({emailId, size, ...params})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -17,6 +17,24 @@
|
||||
v-model="setting.register"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>注册码</span></div>
|
||||
<div>
|
||||
<el-select
|
||||
@change="change"
|
||||
style="width: 80px;"
|
||||
v-model="setting.regKey"
|
||||
placeholder="Select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in regKeyOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>添加邮箱</span></div>
|
||||
<div>
|
||||
@@ -36,29 +54,6 @@
|
||||
v-model="setting.manyEmail"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<span>轮询刷新</span>
|
||||
<el-tooltip effect="dark" content="轮询请求服务器获取最新邮件">
|
||||
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-select
|
||||
@change="change"
|
||||
style="width: 80px;"
|
||||
v-model="setting.autoRefreshTime"
|
||||
placeholder="Select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<span>物理清空数据</span>
|
||||
@@ -135,6 +130,29 @@
|
||||
v-model="setting.receive"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<span>轮询刷新</span>
|
||||
<el-tooltip effect="dark" content="轮询请求服务器获取最新邮件">
|
||||
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-select
|
||||
@change="change"
|
||||
style="width: 80px;"
|
||||
v-model="setting.autoRefreshTime"
|
||||
placeholder="Select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>邮件发送</span></div>
|
||||
<div>
|
||||
@@ -145,15 +163,14 @@
|
||||
<div class="setting-item">
|
||||
<div><span>添加 Resend Token</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"/>
|
||||
</el-button>
|
||||
<el-button class="opt-button" style="margin-top: 0" @click="openResendForm" size="small" type="primary">
|
||||
<Icon icon="material-symbols:add-rounded" width="16" height="16"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item token-item" v-for="(value, key, index) in setting.resendTokens" :key="index">
|
||||
<div><span>{{ key }}</span></div>
|
||||
<div><span>{{ value }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -227,7 +244,7 @@
|
||||
<div class="setting-item">
|
||||
<div><span>Site Key</span></div>
|
||||
<div class="bot-verify">
|
||||
<span>{{ setting.siteKey || '空' }}</span>
|
||||
<span>{{ setting.siteKey }}</span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true">
|
||||
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
|
||||
</el-button>
|
||||
@@ -236,7 +253,7 @@
|
||||
<div class="setting-item">
|
||||
<div><span>Secret Key</span></div>
|
||||
<div class="bot-verify">
|
||||
<span> {{ setting.secretKey || '空' }} </span>
|
||||
<span> {{ setting.secretKey }} </span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true">
|
||||
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
|
||||
</el-button>
|
||||
@@ -279,7 +296,7 @@
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveTitle">保存</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="resendTokenFormShow" title="添加resend token" width="340" @closed="cleanResendTokenForm">
|
||||
<el-dialog v-model="resendTokenFormShow" title="添加 Resend Token" width="340" @closed="cleanResendTokenForm">
|
||||
<form>
|
||||
<el-select style="margin-bottom: 15px" v-model="resendTokenForm.domain" placeholder="Select">
|
||||
<el-option
|
||||
@@ -289,7 +306,7 @@
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input type="text" placeholder="令牌" v-model="resendTokenForm.token"/>
|
||||
<el-input type="text" placeholder="输入内容添加,不填则删除" v-model="resendTokenForm.token"/>
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveResendToken">保存</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
@@ -299,7 +316,7 @@
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveR2domain">保存</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="turnstileShow" title="添加Turnstile密钥" width="340"
|
||||
<el-dialog v-model="turnstileShow" title="添加 Turnstile 密钥" width="340"
|
||||
@closed="turnstileForm.secretKey = '';turnstileForm.siteKey = ''">
|
||||
<form>
|
||||
<el-input type="text" placeholder="siteKey" v-model="turnstileForm.siteKey"/>
|
||||
@@ -368,7 +385,7 @@
|
||||
</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="多个邮箱用, 分开 example1.com,example2.com" v-model="forwardEmail" @add-tag="emailAddTag"></el-input-tag>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
@@ -392,7 +409,7 @@
|
||||
</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="多个邮箱用, 分开 example1.com,example2.com" tag-type="success" v-model="ruleEmail" @add-tag="ruleEmailAddTag" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
@@ -406,12 +423,18 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog class="resend-table" v-model="showResendList" title="Token 列表">
|
||||
<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>
|
||||
</el-dialog>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineOptions, onMounted, reactive, ref} from "vue";
|
||||
import {computed, defineOptions, reactive, ref} from "vue";
|
||||
import {physicsDeleteAll, setBackground, settingQuery, settingSet} from "@/request/setting.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
@@ -419,9 +442,10 @@ import {useAccountStore} from "@/store/account.js";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {cvtR2Url} from "@/utils/convert.js";
|
||||
import {storeToRefs} from "pinia";
|
||||
import { debounce } from 'lodash-es'
|
||||
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";
|
||||
|
||||
defineOptions({
|
||||
name: 'sys-setting'
|
||||
@@ -440,6 +464,7 @@ const turnstileShow = ref(false)
|
||||
const tgSettingShow = ref(false)
|
||||
const thirdEmailShow = ref(false)
|
||||
const forwardRulesShow = ref(false)
|
||||
const showResendList = ref(false)
|
||||
const settingStore = useSettingStore();
|
||||
const {settings: setting} = storeToRefs(settingStore);
|
||||
const editTitle = ref('')
|
||||
@@ -455,6 +480,13 @@ const turnstileForm = reactive({
|
||||
siteKey: '',
|
||||
secretKey: ''
|
||||
})
|
||||
|
||||
const regKeyOptions = [
|
||||
{label: '开启', value: 0},
|
||||
{label: '关闭', value: 1},
|
||||
{label: '可选', value: 2},
|
||||
]
|
||||
|
||||
const options = [
|
||||
{label: '关闭', value: 0},
|
||||
{label: '3s', value: 3},
|
||||
@@ -468,12 +500,40 @@ const options = [
|
||||
const tgChatId = ref([])
|
||||
const tgBotStatus = ref(0)
|
||||
const tgBotToken = ref('')
|
||||
|
||||
const forwardEmail = ref([])
|
||||
const forwardStatus = ref(0)
|
||||
const emailColumnWidth = ref(0)
|
||||
const tokenColumnWidth = ref(0)
|
||||
|
||||
const ruleType = ref(0)
|
||||
const ruleEmail = ref([])
|
||||
const resendList = computed(() => {
|
||||
|
||||
let list = Object.keys(setting.value.resendTokens).map(key => {
|
||||
return {
|
||||
key: key,
|
||||
value: setting.value.resendTokens[key]
|
||||
};
|
||||
})
|
||||
|
||||
if (list.length > 0) {
|
||||
|
||||
const key = list.reduce((a, b) =>
|
||||
a.key.length > b.key.length ? a : b
|
||||
).key;
|
||||
|
||||
emailColumnWidth.value = getTextWidth(key) + 30
|
||||
|
||||
const value = list.reduce((a, b) =>
|
||||
a.value.length > b.value.length ? a : b
|
||||
).value;
|
||||
|
||||
tokenColumnWidth.value = getTextWidth(value) + 30
|
||||
|
||||
}
|
||||
|
||||
return list;
|
||||
});
|
||||
|
||||
settingQuery().then(settingData => {
|
||||
setting.value = settingData
|
||||
@@ -493,6 +553,10 @@ function openTgSetting() {
|
||||
tgSettingShow.value = true
|
||||
}
|
||||
|
||||
function openResendList() {
|
||||
showResendList.value = true
|
||||
}
|
||||
|
||||
function openThirdEmailSetting() {
|
||||
forwardEmail.value = []
|
||||
forwardStatus.value = setting.value.forwardStatus
|
||||
@@ -898,6 +962,24 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.resend-table.el-dialog) {
|
||||
min-height: 300px;
|
||||
width: 500px !important;
|
||||
@media (max-width: 540px) {
|
||||
width: calc(100% - 40px) !important;
|
||||
margin-right: 20px !important;
|
||||
margin-left: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.resend-table .el-dialog__header) {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
:deep(.el-table__inner-wrapper:before) {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
:deep(.cut-dialog.el-dialog) {
|
||||
width: fit-content !important;
|
||||
height: fit-content !important;
|
||||
|
||||
@@ -307,6 +307,20 @@ roleSelectUse().then(list => {
|
||||
roleList.push(...list)
|
||||
})
|
||||
|
||||
const paramsStar = localStorage.getItem('user-params')
|
||||
if (paramsStar) {
|
||||
const locaParams = JSON.parse(paramsStar)
|
||||
params.num = locaParams.num
|
||||
params.size = locaParams.size
|
||||
params.timeSort = locaParams.timeSort
|
||||
params.status = locaParams.status
|
||||
}
|
||||
|
||||
watch(() => params, () => {
|
||||
localStorage.setItem('user-params',JSON.stringify(params))
|
||||
}, {
|
||||
deep: true
|
||||
})
|
||||
|
||||
watch(() => roleStore.refresh, () => {
|
||||
roleSelectUse().then(list => {
|
||||
|
||||
+181
File diff suppressed because one or more lines are too long
-179
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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-BXq2c1cR.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DAVUbrwg.css">
|
||||
<script type="module" crossorigin src="/assets/index-B33pG0J-.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-GJlLLp1y.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading-first">
|
||||
|
||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "mail-cf",
|
||||
"name": "mail-worker",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail-cf",
|
||||
"name": "mail-worker",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.0.5",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import r2Service from '../service/r2-service';
|
||||
import app from '../hono/hono';
|
||||
|
||||
|
||||
app.get('/file/*', async (c) => {
|
||||
const key = c.req.path.split('/file/')[1];
|
||||
const obj = await r2Service.getObj(c, key);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import app from '../hono/hono';
|
||||
import result from '../model/result';
|
||||
import regKeyService from '../service/reg-key-service';
|
||||
import userContext from '../security/user-context';
|
||||
|
||||
app.post('/regKey/add', async (c) => {
|
||||
await regKeyService.add(c, await c.req.json(), await userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
})
|
||||
|
||||
app.get('/regKey/list', async (c) => {
|
||||
const list = await regKeyService.list(c, c.req.query());
|
||||
return c.json(result.ok(list));
|
||||
})
|
||||
|
||||
app.delete('/regKey/delete', async (c) => {
|
||||
await regKeyService.delete(c, c.req.query());
|
||||
return c.json(result.ok());
|
||||
})
|
||||
|
||||
app.delete('/regKey/clearNotUse', async (c) => {
|
||||
await regKeyService.clearNotUse(c);
|
||||
return c.json(result.ok());
|
||||
})
|
||||
|
||||
app.get('/regKey/history', async (c) => {
|
||||
const list = await regKeyService.history(c, c.req.query());
|
||||
return c.json(result.ok(list));
|
||||
})
|
||||
@@ -2,12 +2,12 @@ import app from '../hono/hono';
|
||||
import emailService from '../service/email-service';
|
||||
import result from '../model/result';
|
||||
|
||||
app.get('/sys-email/list',async (c) => {
|
||||
app.get('/sysEmail/list',async (c) => {
|
||||
const data = await emailService.allList(c, c.req.query());
|
||||
return c.json(result.ok(data));
|
||||
})
|
||||
|
||||
app.delete('/sys-email/delete',async (c) => {
|
||||
app.delete('/sysEmail/delete',async (c) => {
|
||||
const list = await emailService.physicsDelete(c, c.req.query());
|
||||
return c.json(result.ok(list));
|
||||
})
|
||||
|
||||
@@ -10,6 +10,10 @@ export const roleConst = {
|
||||
CLOSE: 0,
|
||||
OPEN: 1
|
||||
},
|
||||
banEmailType: {
|
||||
ALL: 0,
|
||||
CONTENT: 1
|
||||
},
|
||||
sendType: {
|
||||
COUNT: 'count',
|
||||
DAY: 'day'
|
||||
@@ -55,6 +59,11 @@ export const settingConst = {
|
||||
OPEN: 0,
|
||||
CLOSE: 1,
|
||||
},
|
||||
regKey: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1,
|
||||
OPTIONAL: 2,
|
||||
},
|
||||
receive: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1,
|
||||
|
||||
@@ -5,11 +5,12 @@ import settingService from '../service/setting-service';
|
||||
import attService from '../service/att-service';
|
||||
import constant from '../const/constant';
|
||||
import fileUtils from '../utils/file-utils';
|
||||
import { attConst, emailConst, isDel, settingConst } from '../const/entity-const';
|
||||
import { emailConst, isDel, roleConst, settingConst } from '../const/entity-const';
|
||||
import emailUtils from '../utils/email-utils';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import roleService from '../service/role-service';
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
@@ -33,7 +34,6 @@ export async function email(message, env, ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
const account = await accountService.selectByEmailIncludeDelNoCase({ env: env }, message.to);
|
||||
|
||||
const reader = message.raw.getReader();
|
||||
let content = '';
|
||||
@@ -46,6 +46,53 @@ export async function email(message, env, ctx) {
|
||||
|
||||
const email = await PostalMime.parse(content);
|
||||
|
||||
const account = await accountService.selectByEmailIncludeDelNoCase({ env: env }, message.to);
|
||||
|
||||
if (account && account.email !== env.admin) {
|
||||
|
||||
let { banEmail, banEmailType } = await roleService.selectByUserId({ env: env}, account.userId);
|
||||
|
||||
banEmail = banEmail.split(",").filter(item => item !== "")
|
||||
|
||||
for (const item of banEmail) {
|
||||
|
||||
if (item.startsWith('*@')) {
|
||||
|
||||
const banDomain = emailUtils.getDomain(item.toLowerCase())
|
||||
const receiveDomain = emailUtils.getDomain(email.from.address.toLowerCase())
|
||||
|
||||
if (banDomain === receiveDomain) {
|
||||
|
||||
if (banEmailType === roleConst.banEmailType.ALL) return
|
||||
|
||||
if (banEmailType === roleConst.banEmailType.CONTENT) {
|
||||
email.html = '邮件内容已被移除'
|
||||
email.text = '邮件内容已被移除'
|
||||
email.attachments = []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if (item.toLowerCase() === email.from.address.toLowerCase()) {
|
||||
|
||||
if (banEmailType === roleConst.banEmailType.ALL) return
|
||||
|
||||
if (banEmailType === roleConst.banEmailType.CONTENT) {
|
||||
email.html = '邮件内容已被移除'
|
||||
email.text = '邮件内容已被移除'
|
||||
email.attachments = []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const toName = email.to.find(item => item.address === message.to)?.name || '';
|
||||
|
||||
const params = {
|
||||
@@ -68,6 +115,11 @@ 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 = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
export const regKey = sqliteTable('reg_key', {
|
||||
regKeyId: integer('rege_key_id').primaryKey({ autoIncrement: true }),
|
||||
code: text('code').notNull().default(''),
|
||||
count: integer('count').notNull().default(0),
|
||||
roleId: integer('role_id').notNull().default(0),
|
||||
userId: integer('user_id').notNull().default(0),
|
||||
expireTime: text('expire_time'),
|
||||
createTime: text('create_time').notNull().default(sql`CURRENT_TIMESTAMP`)
|
||||
});
|
||||
export default regKey
|
||||
@@ -5,6 +5,8 @@ export const role = sqliteTable('role', {
|
||||
name: text('name').notNull(),
|
||||
key: text('key').notNull(),
|
||||
description: text('description'),
|
||||
banEmail: text('ban_email').notNull().default(''),
|
||||
banEmailType: integer('ban_email_type').notNull().default(0),
|
||||
sort: integer('sort'),
|
||||
isDefault: integer('is_default').default(0),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
|
||||
@@ -12,6 +12,7 @@ export const setting = sqliteTable('setting', {
|
||||
r2Domain: text('r2_domain'),
|
||||
secretKey: text('secret_key'),
|
||||
siteKey: text('site_key'),
|
||||
regKey: integer('reg_key').default(1).notNull(),
|
||||
background: text('background'),
|
||||
tgBotToken: text('tg_bot_token').default('').notNull(),
|
||||
tgChatId: text('tg_chat_id').default('').notNull(),
|
||||
|
||||
@@ -16,6 +16,7 @@ const user = sqliteTable('user', {
|
||||
device: text('device'),
|
||||
sort: text('sort').default(0),
|
||||
sendCount: text('send_count').default(0),
|
||||
regKeyId: integer('reg_key_id').default(0).notNull(),
|
||||
isDel: integer('is_del').default(0).notNull()
|
||||
});
|
||||
export default user
|
||||
|
||||
@@ -16,4 +16,5 @@ import '../api/role-api'
|
||||
import '../api/sys-email-api'
|
||||
import '../api/init-api'
|
||||
import '../api/analysis-api'
|
||||
import '../api/reg-key-api'
|
||||
export default app;
|
||||
|
||||
@@ -15,10 +15,62 @@ const init = {
|
||||
await this.v1_2DB(c);
|
||||
await this.v1_3DB(c);
|
||||
await this.v1_3_1DB(c);
|
||||
await this.v1_4DB(c);
|
||||
await settingService.refresh(c);
|
||||
return c.text('初始化成功');
|
||||
},
|
||||
|
||||
async v1_4DB(c) {
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS reg_key (
|
||||
rege_key_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL COLLATE NOCASE DEFAULT '',
|
||||
count INTEGER NOT NULL DEFAULT 0,
|
||||
role_id INTEGER NOT NULL DEFAULT 0,
|
||||
user_id INTEGER NOT NULL DEFAULT 0,
|
||||
expire_time DATETIME,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`).run();
|
||||
|
||||
// 添加不区分大小写的唯一索引
|
||||
try {
|
||||
await c.env.db.prepare(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_setting_code ON reg_key(code COLLATE NOCASE)
|
||||
`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过创建索引,原因:${e.message}`);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await c.env.db.prepare(`
|
||||
INSERT INTO perm (perm_id, name, perm_key, pid, type, sort) VALUES
|
||||
(33,'注册密钥', NULL, 0, 1, 5.1),
|
||||
(34,'密钥查看', 'reg-key:query', 33, 2, 0),
|
||||
(35,'密钥添加', 'reg-key:add', 33, 2, 1),
|
||||
(36,'密钥删除', 'reg-key:delete', 33, 2, 2)`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过数据,原因:${e.message}`);
|
||||
}
|
||||
|
||||
const ADD_COLUMN_SQL_LIST = [
|
||||
`ALTER TABLE setting ADD COLUMN reg_key INTEGER NOT NULL DEFAULT 1;`,
|
||||
`ALTER TABLE role ADD COLUMN ban_email TEXT NOT NULL DEFAULT '';`,
|
||||
`ALTER TABLE role ADD COLUMN ban_email_type INTEGER NOT NULL DEFAULT 0;`,
|
||||
`ALTER TABLE user ADD COLUMN reg_key_id INTEGER NOT NULL DEFAULT 0;`
|
||||
];
|
||||
|
||||
for (let sql of ADD_COLUMN_SQL_LIST) {
|
||||
try {
|
||||
await c.env.db.prepare(sql).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段添加,原因:${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async v1_3_1DB(c) {
|
||||
await c.env.db.prepare(`UPDATE email SET name = SUBSTR(send_email, 1, INSTR(send_email, '@') - 1) WHERE (name IS NULL OR name = '') AND type = ${emailConst.type.RECEIVE}`).run();
|
||||
},
|
||||
|
||||
@@ -29,8 +29,8 @@ const requirePerms = [
|
||||
'/role/tree',
|
||||
'/role/set',
|
||||
'/role/setDefault',
|
||||
'/sys-email/list',
|
||||
'/sys-email/delete',
|
||||
'/sysEmail/list',
|
||||
'/sysEmail/delete',
|
||||
'/setting/physicsDeleteAll',
|
||||
'/setting/setBackground',
|
||||
'/setting/set',
|
||||
@@ -41,7 +41,12 @@ const requirePerms = [
|
||||
'/user/setType',
|
||||
'/user/list',
|
||||
'/user/resetSendCount',
|
||||
'/user/add'
|
||||
'/user/add',
|
||||
'/regKey/add',
|
||||
'/regKey/list',
|
||||
'/regKey/delete',
|
||||
'/regKey/clearNotUse',
|
||||
'/regKey/history'
|
||||
];
|
||||
|
||||
const premKey = {
|
||||
@@ -62,12 +67,15 @@ const premKey = {
|
||||
'user:set-status': ['/user/setStatus'],
|
||||
'user:set-type': ['/user/setType'],
|
||||
'user:delete': ['/user/delete'],
|
||||
'sys-email:query': ['/sys-email/list'],
|
||||
'sys-email:delete': ['/sys-email/delete'],
|
||||
'sys-email:query': ['/sysEmail/list'],
|
||||
'sys-email:delete': ['/sysEmail/delete'],
|
||||
'setting:query': ['/setting/query'],
|
||||
'setting:set': ['/setting/set', '/setting/setBackground'],
|
||||
'setting:clean': ['/setting/physicsDeleteAll'],
|
||||
'analysis:query': ['/analysis/echarts']
|
||||
'analysis:query': ['/analysis/echarts'],
|
||||
'role-key:add': ['/regKey/add'],
|
||||
'role-key:query': ['/regKey/list','/regKey/history'],
|
||||
'role-key:delete': ['/regKey/delete','/regKey/clearNotUse'],
|
||||
};
|
||||
|
||||
app.use('*', async (c, next) => {
|
||||
|
||||
@@ -48,7 +48,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(`添加邮箱数量限制${roleRow.accountCount}个`, 403);
|
||||
if(userAccountCount >= roleRow.accountCount) throw new BizError(`添加邮箱数量到达限制`, 403);
|
||||
}
|
||||
|
||||
if (await settingService.isAddEmailVerify(c)) {
|
||||
@@ -181,6 +181,9 @@ const accountService = {
|
||||
|
||||
async setName(c, params, userId) {
|
||||
const { name, accountId } = params
|
||||
if (name.length > 30) {
|
||||
throw new BizError('用户名长度超出限制');
|
||||
}
|
||||
await orm(c).update(account).set({name}).where(and(eq(account.userId, userId),eq(account.accountId, accountId))).run();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,10 +5,7 @@ import { desc, count, eq, and, ne, isNotNull } from 'drizzle-orm';
|
||||
import { emailConst } from '../const/entity-const';
|
||||
import kvConst from '../const/kv-const';
|
||||
import dayjs from 'dayjs';
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
import { toUtc } from '../utils/date-uitil';
|
||||
const analysisService = {
|
||||
|
||||
async echarts(c) {
|
||||
@@ -62,7 +59,7 @@ const analysisService = {
|
||||
},
|
||||
|
||||
filterEmptyDay(data) {
|
||||
const today = dayjs().tz('Asia/Shanghai').subtract(1, 'day');
|
||||
const today = toUtc().tz('Asia/Shanghai').subtract(1, 'day');
|
||||
const previousDays = Array.from({ length: 15 }, (_, i) => {
|
||||
return today.subtract(i, 'day').format('YYYY-MM-DD');
|
||||
}).reverse();
|
||||
|
||||
@@ -150,7 +150,7 @@ const emailService = {
|
||||
}
|
||||
|
||||
if (send === settingConst.send.CLOSE) {
|
||||
throw new BizError('邮箱发送功能已停用', 403);
|
||||
throw new BizError('邮件发送功能已停用', 403);
|
||||
}
|
||||
|
||||
if (attachments.length > 0 && manyType === 'divide') {
|
||||
@@ -163,13 +163,17 @@ const emailService = {
|
||||
|
||||
if (c.env.admin !== userRow.email && roleRow.sendCount) {
|
||||
|
||||
if (roleRow.sendCount < 0) {
|
||||
throw new BizError('用户无发送次数', 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('发送次数已到达每日限制', 403);
|
||||
if (roleRow.sendType === 'count') throw new BizError('发送次数已到达限制', 403);
|
||||
}
|
||||
|
||||
if (userRow.sendCount + receiveEmail.length > roleRow.sendCount) {
|
||||
if (roleRow.sendType === 'day') throw new BizError('剩余每日发送次数不足', 403);
|
||||
if (roleRow.sendType === 'day') throw new BizError('当日剩余发送次数不足', 403);
|
||||
if (roleRow.sendType === 'count') throw new BizError('剩余发送次数不足', 403);
|
||||
}
|
||||
|
||||
@@ -178,18 +182,20 @@ const emailService = {
|
||||
|
||||
const accountRow = await accountService.selectById(c, accountId);
|
||||
|
||||
if (!accountRow) {
|
||||
throw new BizError('发件人邮箱不存在');
|
||||
}
|
||||
|
||||
const domain = emailUtils.getDomain(accountRow.email);
|
||||
const resendToken = resendTokens[domain];
|
||||
|
||||
if (!resendToken) {
|
||||
throw new BizError('resend密钥未配置');
|
||||
}
|
||||
|
||||
if (!accountRow) {
|
||||
throw new BizError('邮箱不存在');
|
||||
}
|
||||
|
||||
if (accountRow.userId !== userId) {
|
||||
throw new BizError('非当前用户所属邮箱');
|
||||
throw new BizError('发件人邮箱非当前用户所有');
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
@@ -488,7 +494,7 @@ const emailService = {
|
||||
|
||||
async allList(c, params) {
|
||||
|
||||
let { emailId, size, name, subject, accountEmail, sendEmail, userEmail, type, timeSort } = params;
|
||||
let { emailId, size, name, subject, accountEmail, userEmail, type, timeSort } = params;
|
||||
|
||||
size = Number(size);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BizError from '../error/biz-error';
|
||||
import userService from './user-service';
|
||||
import emailUtils from '../utils/email-utils';
|
||||
import { isDel, userConst } from '../const/entity-const';
|
||||
import { isDel, settingConst, userConst } from '../const/entity-const';
|
||||
import JwtUtils from '../utils/jwt-utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import KvConst from '../const/kv-const';
|
||||
@@ -14,15 +14,19 @@ import saltHashUtils from '../utils/crypto-utils';
|
||||
import cryptoUtils from '../utils/crypto-utils';
|
||||
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';
|
||||
|
||||
const loginService = {
|
||||
|
||||
async register(c, params) {
|
||||
|
||||
const { email, password, token } = params;
|
||||
const { email, password, token, code } = params;
|
||||
|
||||
if (!await settingService.isRegister(c)) {
|
||||
const {regKey, register} = await settingService.query(c)
|
||||
|
||||
if (register === settingConst.register.CLOSE) {
|
||||
throw new BizError('注册功能已关闭');
|
||||
}
|
||||
|
||||
@@ -30,7 +34,15 @@ const loginService = {
|
||||
throw new BizError('非法邮箱');
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
if (password.length > 30) {
|
||||
throw new BizError('密码长度超出限制');
|
||||
}
|
||||
|
||||
if (emailUtils.getName(email).length > 30) {
|
||||
throw new BizError('邮箱长度超出限制');
|
||||
}
|
||||
|
||||
if (password.length > 6) {
|
||||
throw new BizError('密码必须大于6位');
|
||||
}
|
||||
|
||||
@@ -38,6 +50,21 @@ const loginService = {
|
||||
throw new BizError('非法邮箱域名');
|
||||
}
|
||||
|
||||
let type = null;
|
||||
let regKeyId = 0
|
||||
|
||||
if (regKey === settingConst.regKey.OPEN) {
|
||||
const result = await this.handleOpenRegKey(c, regKey, code)
|
||||
type = result.type
|
||||
regKeyId = result.regKeyId
|
||||
}
|
||||
|
||||
if (regKey === settingConst.regKey.OPTIONAL) {
|
||||
const result = await this.handleOpenOptional(c, regKey, code)
|
||||
type = result.type
|
||||
regKeyId = result.regKeyId
|
||||
}
|
||||
|
||||
const accountRow = await accountService.selectByEmailIncludeDelNoCase(c, email);
|
||||
|
||||
if (accountRow && accountRow.isDel === isDel.DELETE) {
|
||||
@@ -45,7 +72,7 @@ const loginService = {
|
||||
}
|
||||
|
||||
if (accountRow) {
|
||||
throw new BizError('该邮箱已被其他用户绑定');
|
||||
throw new BizError('该邮箱已被注册');
|
||||
}
|
||||
|
||||
if (await settingService.isRegisterVerify(c)) {
|
||||
@@ -54,13 +81,71 @@ const loginService = {
|
||||
|
||||
const { salt, hash } = await saltHashUtils.hashPassword(password);
|
||||
|
||||
const roleRow = await roleService.selectDefaultRole(c);
|
||||
let defType = null
|
||||
|
||||
const userId = await userService.insert(c, { email, password: hash, salt, type: roleRow.roleId });
|
||||
if (!type) {
|
||||
const roleRow = await roleService.selectDefaultRole(c);
|
||||
defType = roleRow.roleId
|
||||
}
|
||||
|
||||
const userId = await userService.insert(c, { email, regKeyId,password: hash, salt, type: type || defType });
|
||||
|
||||
await userService.updateUserInfo(c, userId, true);
|
||||
|
||||
await accountService.insert(c, { userId: userId, email, name: emailUtils.getName(email) });
|
||||
|
||||
if (regKey !== settingConst.regKey.CLOSE && type) {
|
||||
await regKeyService.reduceCount(c, code, 1);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async handleOpenRegKey(c, regKey, code) {
|
||||
|
||||
if (!code) {
|
||||
throw new BizError('注册码不能为空');
|
||||
}
|
||||
|
||||
const regKeyRow = await regKeyService.selectByCode(c, code);
|
||||
|
||||
if (!regKeyRow) {
|
||||
throw new BizError('注册码不存在');
|
||||
}
|
||||
|
||||
if (regKeyRow.count <= 0) {
|
||||
throw new BizError('注册码使用次数已耗尽');
|
||||
}
|
||||
|
||||
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('注册码已过期');
|
||||
}
|
||||
|
||||
return { type: regKeyRow.roleId, regKeyId: regKeyRow.regKeyId };
|
||||
},
|
||||
|
||||
async handleOpenOptional(c, regKey, code) {
|
||||
|
||||
if (!code) {
|
||||
return null
|
||||
}
|
||||
|
||||
const regKeyRow = await regKeyService.selectByCode(c, code);
|
||||
|
||||
if (!regKeyRow) {
|
||||
return null
|
||||
}
|
||||
|
||||
const today = toUtc().tz('Asia/Shanghai').startOf('day')
|
||||
const expireTime = toUtc(regKeyRow.expireTime).tz('Asia/Shanghai').startOf('day');
|
||||
|
||||
if (regKeyRow.count <= 0 || expireTime.isBefore(today)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return { type: regKeyRow.roleId, regKeyId: regKeyRow.regKeyId };
|
||||
},
|
||||
|
||||
async login(c, params) {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import attService from './att-service';
|
||||
import constant from '../const/constant';
|
||||
|
||||
const r2Service = {
|
||||
async putObj(c, key, content, metadata) {
|
||||
await c.env.r2.put(key, content, {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import orm from '../entity/orm';
|
||||
import regKey from '../entity/reg-key';
|
||||
import { inArray, like, eq, desc, sql, or } from 'drizzle-orm';
|
||||
import roleService from './role-service';
|
||||
import BizError from '../error/biz-error';
|
||||
import { formatDetailDate, toUtc } from '../utils/date-uitil';
|
||||
import userService from './user-service';
|
||||
const regKeyService = {
|
||||
|
||||
async add(c, params, userId) {
|
||||
|
||||
let {code,roleId,count,expireTime} = params;
|
||||
|
||||
if (!code) {
|
||||
throw new BizError('注册码不能为空');
|
||||
}
|
||||
|
||||
if (!count) {
|
||||
throw new BizError('使用次数不能为空');
|
||||
}
|
||||
|
||||
if (!expireTime) {
|
||||
throw new BizError('有效时间不能为空');
|
||||
}
|
||||
|
||||
const regKeyRow = await orm(c).select().from(regKey).where(eq(regKey.code, code)).get();
|
||||
|
||||
if (regKeyRow) {
|
||||
throw new BizError('注册码已存在');
|
||||
}
|
||||
|
||||
const roleRow = roleService.selectById(c, roleId);
|
||||
if (!roleRow) {
|
||||
throw new BizError('权限身份不存在');
|
||||
}
|
||||
|
||||
expireTime = formatDetailDate(expireTime)
|
||||
|
||||
await orm(c).insert(regKey).values({code,roleId,count,userId,expireTime}).run();
|
||||
},
|
||||
|
||||
async delete(c, params) {
|
||||
let {regKeyIds} = params;
|
||||
regKeyIds = regKeyIds.split(',').map(id => Number(id));
|
||||
await orm(c).delete(regKey).where(inArray(regKey.regKeyId,regKeyIds)).run();
|
||||
},
|
||||
|
||||
async clearNotUse(c) {
|
||||
let now = formatDetailDate(toUtc().tz('Asia/Shanghai').startOf('day'))
|
||||
await orm(c).delete(regKey).where(or(eq(regKey.count, 0),sql`datetime(${regKey.expireTime}, '+8 hours') < datetime(${now})`)).run();
|
||||
},
|
||||
|
||||
selectByCode(c, code) {
|
||||
return orm(c).select().from(regKey).where(eq(regKey.code, code)).get();
|
||||
},
|
||||
|
||||
async list(c, params) {
|
||||
|
||||
const {code} = params
|
||||
let query = orm(c).select().from(regKey)
|
||||
|
||||
if (code) {
|
||||
query = query.where(like(regKey.code, `${code}%`))
|
||||
}
|
||||
|
||||
const regKeyList = await query.orderBy(desc(regKey.regKeyId)).all();
|
||||
const roleList = await roleService.roleSelectUse(c);
|
||||
|
||||
const today = toUtc().tz('Asia/Shanghai').startOf('day')
|
||||
|
||||
regKeyList.forEach(regKeyRow => {
|
||||
|
||||
const index = roleList.findIndex(roleRow => roleRow.roleId === regKeyRow.roleId)
|
||||
regKeyRow.roleName = index > -1 ? roleList[index].name : ''
|
||||
|
||||
const expireTime = toUtc(regKeyRow.expireTime).tz('Asia/Shanghai').startOf('day');
|
||||
|
||||
if (expireTime.isBefore(today)) {
|
||||
regKeyRow.expireTime = null
|
||||
}
|
||||
})
|
||||
|
||||
return regKeyList;
|
||||
},
|
||||
|
||||
async reduceCount(c, code, count) {
|
||||
await orm(c).update(regKey).set({
|
||||
count: sql`${regKey.count}
|
||||
-
|
||||
${count}`
|
||||
}).where(eq(regKey.code, code)).run();
|
||||
},
|
||||
|
||||
async history(c, params) {
|
||||
const { regKeyId } = params;
|
||||
return userService.listByRegKeyId(c, regKeyId);
|
||||
}
|
||||
}
|
||||
|
||||
export default regKeyService;
|
||||
@@ -6,12 +6,15 @@ import rolePerm from '../entity/role-perm';
|
||||
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';
|
||||
|
||||
const roleService = {
|
||||
|
||||
async add(c, params, userId) {
|
||||
|
||||
let { name, permIds } = params;
|
||||
let { name, permIds, banEmail } = params;
|
||||
|
||||
if (!name) {
|
||||
throw new BizError('身份名不能为空');
|
||||
@@ -23,7 +26,15 @@ const roleService = {
|
||||
throw new BizError('身份名已存在');
|
||||
}
|
||||
|
||||
roleRow = await orm(c).insert(role).values({...params, userId}).returning().get();
|
||||
const notEmailIndex = banEmail.findIndex(item => !verifyUtils.isEmail(item))
|
||||
|
||||
if (notEmailIndex > -1) {
|
||||
throw new BizError('非法邮箱');
|
||||
}
|
||||
|
||||
banEmail = banEmail.join(',')
|
||||
|
||||
roleRow = await orm(c).insert(role).values({...params, banEmail, userId}).returning().get();
|
||||
|
||||
if (permIds.length === 0) {
|
||||
return;
|
||||
@@ -44,6 +55,7 @@ const roleService = {
|
||||
.where(eq(perm.type, permConst.type.BUTTON)).all();
|
||||
|
||||
roleList.forEach(role => {
|
||||
role.banEmail = role.banEmail.split(",").filter(item => item !== "")
|
||||
role.permIds = permList.filter(perm => perm.roleId === role.roleId).map(perm => perm.permId);
|
||||
});
|
||||
|
||||
@@ -52,7 +64,7 @@ const roleService = {
|
||||
|
||||
async setRole(c, params) {
|
||||
|
||||
let { name, permIds, roleId } = params;
|
||||
let { name, permIds, roleId, banEmail } = params;
|
||||
|
||||
if (!name) {
|
||||
throw new BizError('名字不能为空');
|
||||
@@ -60,7 +72,15 @@ const roleService = {
|
||||
|
||||
delete params.isDefault
|
||||
|
||||
await orm(c).update(role).set({...params}).where(eq(role.roleId, roleId)).run();
|
||||
const notEmailIndex = banEmail.findIndex(item => !verifyUtils.isEmail(item))
|
||||
|
||||
if (notEmailIndex > -1) {
|
||||
throw new BizError('非法邮箱');
|
||||
}
|
||||
|
||||
banEmail = banEmail.join(',')
|
||||
|
||||
await orm(c).update(role).set({...params, banEmail}).where(eq(role.roleId, roleId)).run();
|
||||
await orm(c).delete(rolePerm).where(eq(rolePerm.roleId, roleId)).run();
|
||||
|
||||
if (permIds.length > 0) {
|
||||
@@ -126,6 +146,10 @@ const roleService = {
|
||||
.leftJoin(rolePerm, eq(perm.permId, rolePerm.permId))
|
||||
.leftJoin(role, eq(role.roleId, rolePerm.roleId))
|
||||
.where(and(eq(perm.permKey, permKey), eq(role.sendType, sendType))).all();
|
||||
},
|
||||
|
||||
selectByUserId(c, userId) {
|
||||
return orm(c).select(role).from(user).leftJoin(role, eq(role.roleId, user.type)).where(eq(user.userId, userId)).get();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -49,16 +49,6 @@ const settingService = {
|
||||
await this.refresh(c);
|
||||
},
|
||||
|
||||
async isRegister(c) {
|
||||
const { register } = await this.query(c);
|
||||
return register === settingConst.register.OPEN;
|
||||
},
|
||||
|
||||
async isReceive(c) {
|
||||
const { receive } = await this.query(c);
|
||||
return receive === settingConst.receive.OPEN;
|
||||
},
|
||||
|
||||
async isAddEmail(c) {
|
||||
const { addEmail, manyEmail } = await this.query(c);
|
||||
return addEmail === settingConst.addEmail.OPEN && manyEmail === settingConst.manyEmail.OPEN;
|
||||
@@ -125,7 +115,8 @@ const settingService = {
|
||||
siteKey: settingRow.siteKey,
|
||||
background: settingRow.background,
|
||||
loginOpacity: settingRow.loginOpacity,
|
||||
domainList:settingRow.domainList
|
||||
domainList:settingRow.domainList,
|
||||
regKey: settingRow.regKey
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -382,6 +382,15 @@ const userService = {
|
||||
await accountService.restoreByUserId(c, userId);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
listByRegKeyId(c, regKeyId) {
|
||||
return orm(c)
|
||||
.select({email: user.email,createTime: user.createTime})
|
||||
.from(user)
|
||||
.where(eq(user.regKeyId, regKeyId))
|
||||
.orderBy(desc(user.userId))
|
||||
.all();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
export function formatDetailDate(time) {
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
export function toUtc(time) {
|
||||
return dayjs.utc(time || dayjs())
|
||||
}
|
||||
@@ -31,6 +31,6 @@ crons = ["0 16 * * *"] #定时任务每天晚上12点执行
|
||||
|
||||
#[vars]
|
||||
#orm_log = false
|
||||
#domain = [] #邮件域名可可配置多个 示例: ["example1.com","example2.com"]
|
||||
#domain = [] #邮件域名可配置多个 示例: ["example1.com","example2.com"]
|
||||
#admin = "" #管理员的邮箱 示例: admin@example.com
|
||||
#jwt_secret = "" #jwt令牌的密钥,随便填一串字符串
|
||||
|
||||
Reference in New Issue
Block a user