新增注册码、草稿箱、权限拦截邮件、发件菜单隐藏

This commit is contained in:
eoao
2025-07-15 20:09:56 +08:00
parent 7e5a383bc6
commit 254325431b
55 changed files with 1819 additions and 361 deletions

View File

@@ -14,13 +14,13 @@
"compressorjs": "^1.2.1", "compressorjs": "^1.2.1",
"date-time-format-timezone": "^1.0.22", "date-time-format-timezone": "^1.0.22",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dexie": "^4.0.11",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"element-plus": "^2.9.5", "element-plus": "^2.9.5",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"path": "^0.12.7", "path": "^0.12.7",
"pinia": "^3.0.2", "pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0", "pinia-plugin-persistedstate": "^4.2.0",
"postal-mime": "^2.4.3",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-cropper": "^1.1.4", "vue-cropper": "^1.1.4",
@@ -1826,6 +1826,12 @@
"node": ">=0.10" "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": { "node_modules/dotenv": {
"version": "16.5.0", "version": "16.5.0",
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.5.0.tgz", "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.5.0.tgz",
@@ -2954,12 +2960,6 @@
"pathe": "^2.0.3" "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": { "node_modules/postcss": {
"version": "8.5.3", "version": "8.5.3",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz",

View File

@@ -16,6 +16,7 @@
"compressorjs": "^1.2.1", "compressorjs": "^1.2.1",
"date-time-format-timezone": "^1.0.22", "date-time-format-timezone": "^1.0.22",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dexie": "^4.0.11",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"element-plus": "^2.9.5", "element-plus": "^2.9.5",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",

View File

@@ -51,7 +51,9 @@ http.interceptors.response.use((res) => {
}) })
reject(data) reject(data)
} }
resolve(data.data) setTimeout(() => {
resolve(data.data)
},1000)
}) })
}, },
(error) => { (error) => {

View File

@@ -2,10 +2,10 @@
<div class="email-container"> <div class="email-container">
<div class="header-actions"> <div class="header-actions">
<el-checkbox <el-checkbox
v-model="checkAll" v-model="checkAll"
:indeterminate="isIndeterminate" :indeterminate="isIndeterminate"
:disabled="!emailList.length" :disabled="!emailList.length"
@change="handleCheckAllChange" @change="handleCheckAllChange"
> >
</el-checkbox> </el-checkbox>
<div class="header-left" :style="'padding-left:' + actionLeft"> <div class="header-left" :style="'padding-left:' + actionLeft">
@@ -33,7 +33,7 @@
:data-checked="item.checked" :data-checked="item.checked"
@click="jumpDetails(item)" @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"> <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-if="item.isStar" icon="fluent-color:star-16" width="20" height="20"/>
<Icon v-else icon="solar:star-line-duotone" width="18" height="18"/> <Icon v-else icon="solar:star-line-duotone" width="18" height="18"/>
@@ -102,7 +102,9 @@
</div> </div>
<div v-else></div> <div v-else></div>
<span class="name"> <span class="name">
<span>{{ item.name }}</span> <span>
<slot name="name" :email="item"> {{ item.name }}</slot>
</span>
<span> <span>
<Icon v-if="item.isStar" icon="fluent-color:star-16" width="18" height="18"/> <Icon v-if="item.isStar" icon="fluent-color:star-16" width="18" height="18"/>
</span> </span>
@@ -111,7 +113,11 @@
</div> </div>
<div> <div>
<div class="email-text"> <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> <span class="email-content">{{ htmlToText(item) }}</span>
</div> </div>
<div class="user-info" v-if="showUserInfo"> <div class="user-info" v-if="showUserInfo">
@@ -128,7 +134,7 @@
<span>{{ item.type === 0 ? item.toEmail : item.sendEmail }}</span> <span>{{ item.type === 0 ? item.toEmail : item.sendEmail }}</span>
</div> </div>
<div class="del-status" v-if="item.isDel"> <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> </div>
</div> </div>
@@ -201,11 +207,15 @@ const props = defineProps({
allowStar: { allowStar: {
type: Boolean, type: Boolean,
default: true default: true
},
type: {
type: String,
default: ''
} }
}) })
const emit = defineEmits(['jump', 'refresh-before']) const emit = defineEmits(['jump', 'refresh-before', 'delete-draft'])
const settingStore = useSettingStore() const settingStore = useSettingStore()
const uiStore = useUiStore(); const uiStore = useUiStore();
@@ -355,13 +365,19 @@ function changeAccountShow() {
uiStore.accountShow = !uiStore.accountShow; uiStore.accountShow = !uiStore.accountShow;
} }
const handleDelete = () => { const handleDelete = () => {
ElMessageBox.confirm('确认批量删除这些邮件吗?', { ElMessageBox.confirm('确认批量删除这些邮件吗?', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
if (props.type === 'draft') {
const draftIds = getSelectedDraftsIds();
emit('delete-draft', draftIds);
return;
}
const emailIds = getSelectedMailsIds(); const emailIds = getSelectedMailsIds();
props.emailDelete(emailIds).then(() => { props.emailDelete(emailIds).then(() => {
ElMessage({ ElMessage({
@@ -395,7 +411,6 @@ function addItem(email) {
return return
} }
if (props.timeSort) { if (props.timeSort) {
if (noLoading.value) { if (noLoading.value) {
emailList.push(email) emailList.push(email)
@@ -430,6 +445,9 @@ function getSelectedMailsIds() {
return emailList.filter(item => item.checked).map(item => item.emailId); return emailList.filter(item => item.checked).map(item => item.emailId);
} }
function getSelectedDraftsIds() {
return emailList.filter(item => item.checked).map(item => item.draftId);
}
function updateCheckStatus() { function updateCheckStatus() {
const checkedCount = emailList.filter(item => item.checked).length; const checkedCount = emailList.filter(item => item.checked).length;
@@ -543,6 +561,7 @@ function loadData() {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100%; height: 100%;
width: 100%;
} }
.noLoading { .noLoading {
@@ -625,13 +644,26 @@ function loadData() {
} }
} }
.el-checkbox { .checkbox {
display: flex; display: flex;
padding-left: 15px; padding-left: 15px;
padding-right: 20px; padding-right: 20px;
justify-content: center; 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 { .title-column {
@media (max-width: 1200px) { @media (max-width: 1200px) {
grid-template-columns: 1fr !important; grid-template-columns: 1fr !important;
@@ -642,7 +674,7 @@ function loadData() {
.title { .title {
flex: 1; flex: 1;
display: grid; display: grid;
grid-template-columns: 220px 1fr; grid-template-columns: 240px 1fr;
@media (max-width: 1199px) { @media (max-width: 1199px) {
padding-right: 15px; padding-right: 15px;
} }
@@ -702,6 +734,9 @@ function loadData() {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
@media (min-width: 1200px) {
padding-left: 5px;
}
} }
.email-content { .email-content {
@@ -788,7 +823,7 @@ function loadData() {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
column-gap: 15px; column-gap: 18px;
row-gap: 8px; row-gap: 8px;
padding-left: 2px; padding-left: 2px;
} }

View File

@@ -6,12 +6,13 @@
</template> </template>
<script setup> <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 loading from "@/components/loading/index.vue";
import {compressImage} from "@/utils/file-utils.js"; import {compressImage} from "@/utils/file-utils.js";
defineExpose({ defineExpose({
clearEditor, clearEditor,
focus focus,
getContent
}) })
const props = defineProps({ const props = defineProps({
@@ -27,7 +28,7 @@ const props = defineProps({
const emit = defineEmits(['change']); const emit = defineEmits(['change']);
const editor = ref(null); const editor = shallowRef(null);
const isInitialized = ref(false); const isInitialized = ref(false);
const editorRef = ref(null); const editorRef = ref(null);
const showLoading = ref(false); const showLoading = ref(false);
@@ -162,6 +163,10 @@ function focus() {
}) })
} }
function getContent() {
return editor.value.getContent()
}
function destroyEditor() { function destroyEditor() {
if (editor.value) { if (editor.value) {

25
mail-vue/src/db/db.js Normal file
View File

@@ -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;

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="account-box"> <div class="account-box">
<div class="head-opt" > <div class="head-opt" >
<Icon v-perm="'account:add'" class="icon" icon="ion:add-outline" width="23" height="23" @click="add" /> <Icon v-perm="'account:add'" class="icon add" icon="ion:add-outline" width="23" height="23" @click="add" />
<Icon class="icon" icon="ion:reload" width="18" height="18" @click="refresh" /> <Icon class="icon refresh" icon="ion:reload" width="18" height="18" @click="refresh" />
</div> </div>
<el-scrollbar class="scrollbar"> <el-scrollbar class="scrollbar">
<div v-infinite-scroll="getAccountList" :infinite-scroll-distance="600" :infinite-scroll-immediate="false"> <div v-infinite-scroll="getAccountList" :infinite-scroll-distance="600" :infinite-scroll-immediate="false">
@@ -16,11 +16,12 @@
</div> </div>
<div class="settings" @click.stop> <div class="settings" @click.stop>
<Icon icon="streamline-ultimate-color:copy-paste-1" width="19" height="19" @click.stop="copyAccount(item.email)"/> <Icon icon="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" v-if="showNullSetting(item)" />
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399" /> <el-dropdown v-else>
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399" />
<template #dropdown > <template #dropdown >
<el-dropdown-menu> <el-dropdown-menu >
<el-dropdown-item @click="openSetName(item)">改名</el-dropdown-item> <el-dropdown-item v-if="hasPerm('email:send')" @click="openSetName(item)">改名</el-dropdown-item>
<el-dropdown-item v-if="item.accountId !== userStore.user.accountId && hasPerm('account:delete')" @click="remove(item)">删除</el-dropdown-item> <el-dropdown-item v-if="item.accountId !== userStore.user.accountId && hasPerm('account:delete')" @click="remove(item)">删除</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@@ -217,6 +218,10 @@ function openSetName (accountItem) {
setNameShow.value = true setNameShow.value = true
} }
function showNullSetting(item) {
return !hasPerm('email:send') && !(item.accountId !== userStore.user.accountId && hasPerm('account:delete'))
}
function itemBg(accountId) { function itemBg(accountId) {
return accountStore.currentAccountId === accountId ? 'item-choose' : '' return accountStore.currentAccountId === accountId ? 'item-choose' : ''
} }
@@ -388,8 +393,16 @@ path[fill="#ffdda1"] {
.icon{ .icon{
cursor: pointer; cursor: pointer;
} }
.icon:nth-child(2) { .refresh {
margin-left: 15px; margin-left: 10px;
}
.add {
margin-left: 2px;
}
.head-opt:not(.add) .refresh {
margin-left: 5px;
} }
} }
.scrollbar { .scrollbar {

View File

@@ -11,11 +11,16 @@
<Icon icon="hugeicons:mailbox-01" width="20" height="20" /> <Icon icon="hugeicons:mailbox-01" width="20" height="20" />
<span class="menu-name" style="margin-left: 21px">收件箱</span> <span class="menu-name" style="margin-left: 21px">收件箱</span>
</el-menu-item> </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' : ''"> :class="route.meta.name === 'send' ? 'choose-item' : ''">
<Icon icon="cil:send" width="20" height="20" /> <Icon icon="cil:send" width="20" height="20" />
<span class="menu-name" style="margin-left: 21px">已发送</span> <span class="menu-name" style="margin-left: 21px">已发送</span>
</el-menu-item> </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" <el-menu-item @click="router.push({name: 'star'})" index="star"
:class="route.meta.name === 'star' ? 'choose-item' : ''"> :class="route.meta.name === 'star' ? 'choose-item' : ''">
<Icon icon="solar:star-line-duotone" width="20" height="20" /> <Icon icon="solar:star-line-duotone" width="20" height="20" />
@@ -46,9 +51,14 @@
</el-menu-item> </el-menu-item>
<el-menu-item @click="router.push({name: 'role'})" index="setting" v-perm="'role:query'" <el-menu-item @click="router.push({name: 'role'})" index="setting" v-perm="'role:query'"
:class="route.meta.name === 'role' ? 'choose-item' : ''"> :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> <span class="menu-name" style="margin-left: 20px">权限控制</span>
</el-menu-item> </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'" <el-menu-item @click="router.push({name: 'sys-setting'})" index="sys-setting" v-perm="'setting:query'"
:class="route.meta.name === 'sys-setting' ? 'choose-item' : ''"> :class="route.meta.name === 'sys-setting' ? 'choose-item' : ''">
<Icon icon="eos-icons:system-ok-outlined" width="18" height="18" /> <Icon icon="eos-icons:system-ok-outlined" width="18" height="18" />

View File

@@ -1,10 +1,10 @@
<template> <template>
<div class="header"> <div class="header" :class="!hasPerm('email:send') ? 'not-send' : ''">
<div class="header-btn"> <div class="header-btn">
<hanburger @click="changeAside"></hanburger> <hanburger @click="changeAside"></hanburger>
<span class="breadcrumb-item">{{ route.meta.title }}</span> <span class="breadcrumb-item">{{ route.meta.title }}</span>
</div> </div>
<div class="writer-box" @click="openSend"> <div v-perm="'email:send'" class="writer-box" @click="openSend">
<div class="writer" > <div class="writer" >
<Icon icon="material-symbols:edit-outline-sharp" width="22" height="22" /> <Icon icon="material-symbols:edit-outline-sharp" width="22" height="22" />
</div> </div>
@@ -95,10 +95,14 @@ const sendType = computed(() => {
return '无权限' return '无权限'
} }
if (userStore.user.role.sendCount === 0) { if (!userStore.user.role.sendCount) {
return '无限制' return '无限制'
} }
if (userStore.user.role.sendCount < 0) {
return '无次数'
}
if (userStore.user.role.sendType === 'day') { if (userStore.user.role.sendType === 'day') {
return '每天' return '每天'
} }
@@ -116,6 +120,11 @@ const sendCount = computed(() => {
if (!userStore.user.role.sendCount) { if (!userStore.user.role.sendCount) {
return null return null
} }
if (userStore.user.role.sendCount < 0) {
return null
}
return userStore.user.sendCount + '/' + userStore.user.role.sendCount return userStore.user.sendCount + '/' + userStore.user.role.sendCount
}) })
@@ -248,6 +257,10 @@ function full() {
grid-template-columns: auto auto 1fr; grid-template-columns: auto auto 1fr;
} }
.header.not-send {
grid-template-columns: auto 1fr;
}
.writer-box { .writer-box {
cursor: pointer; cursor: pointer;
display: flex; display: flex;

View File

@@ -3,7 +3,7 @@
<div :class="accountShow && hasPerm('account:query') ? 'block-show' : 'block-hide'" @click="uiStore.accountShow = false"></div> <div :class="accountShow && hasPerm('account:query') ? 'block-show' : 'block-hide'" @click="uiStore.accountShow = false"></div>
<account :class="accountShow && hasPerm('account:query') ? 'show' : 'hide'" /> <account :class="accountShow && hasPerm('account:query') ? 'show' : 'hide'" />
<router-view class="main-view" v-slot="{ Component,route }"> <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"/> <component :is="Component" :key="route.name"/>
</keep-alive> </keep-alive>
</router-view> </router-view>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="send" v-show="show" @click="close"> <div class="send" v-show="show">
<div class="write-box" @click.stop> <div class="write-box" @click.stop>
<div class="title"> <div class="title">
<div class="title-left"> <div class="title-left">
@@ -15,7 +15,7 @@
</div> </div>
</div> </div>
<div class="container"> <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> <template #prefix>
<div class="item-title">收件人 </div> <div class="item-title">收件人 </div>
</template> </template>
@@ -56,7 +56,7 @@
</template> </template>
<script setup> <script setup>
import tinyEditor from '@/components/tiny-editor/index.vue' 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 {Icon} from "@iconify/vue";
import {useUserStore} from "@/store/user.js"; import {useUserStore} from "@/store/user.js";
import {emailSend} from "@/request/email.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 {fileToBase64, formatBytes} from "@/utils/file-utils.js";
import {getIconByName} from "@/utils/icon-utils.js"; import {getIconByName} from "@/utils/icon-utils.js";
import sendPercent from "@/components/send-percent/index.vue" 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 {useSettingStore} from "@/store/setting.js";
import {userDraftStore} from "@/store/draft.js";
import db from "@/db/db.js";
import dayjs from "dayjs";
defineExpose({ defineExpose({
open, open,
openReply openReply,
openDraft
}) })
const draftStore = userDraftStore()
const settingStore = useSettingStore() const settingStore = useSettingStore()
const emailStore = useEmailStore(); const emailStore = useEmailStore();
const accountStore = useAccountStore() const accountStore = useAccountStore()
@@ -84,6 +89,12 @@ const percent = ref(0)
let percentMessage = null let percentMessage = null
let sending = false let sending = false
const defValue = ref('') const defValue = ref('')
const backReply = reactive({
receiveEmail: [],
subject: '',
content: '',
sendType: ''
})
const form = reactive({ const form = reactive({
sendEmail: '', sendEmail: '',
receiveEmail: [], receiveEmail: [],
@@ -95,7 +106,8 @@ const form = reactive({
sendType: '', sendType: '',
text: '', text: '',
emailId: 0, emailId: 0,
attachments: [] attachments: [],
draftId: null,
}) })
function addTagChange(val) { function addTagChange(val) {
@@ -110,10 +122,8 @@ function addTagChange(val) {
if (isEmail(email) && !form.receiveEmail.includes(email)) { if (isEmail(email) && !form.receiveEmail.includes(email)) {
form.receiveEmail.push(email) form.receiveEmail.push(email)
} }
}) })
} }
function checkDistribute() { function checkDistribute() {
@@ -155,7 +165,6 @@ function chooseFile() {
}) })
return return
} }
const content = await fileToBase64(file) const content = await fileToBase64(file)
form.attachments.push({content, filename, size, contentType}) form.attachments.push({content, filename, size, contentType})
} }
@@ -217,7 +226,9 @@ async function sendEmail() {
}) })
sending = true sending = true
close()
show.value = false
emailSend(form, (e) => { emailSend(form, (e) => {
percent.value = Math.round((e.loaded * 98) / e.total) percent.value = Math.round((e.loaded * 98) / e.total)
}).then(emailList => { }).then(emailList => {
@@ -225,15 +236,25 @@ async function sendEmail() {
emailList.forEach(item => { emailList.forEach(item => {
emailStore.sendScroll?.addItem(item) emailStore.sendScroll?.addItem(item)
}) })
resetForm()
show.value = false
ElNotification({ ElNotification({
title: '邮件已发送', title: '邮件已发送',
type: "success", type: "success",
message: h('span', { style: 'color: teal' }, email.subject), message: h('span', { style: 'color: teal' }, email.subject),
position: 'bottom-right' position: 'bottom-right'
}) })
userStore.refreshUserInfo(); userStore.refreshUserInfo();
if (form.draftId) {
form.subject = ''
form.content = ''
form.receiveEmail = []
draftStore.setDraft = {...toRaw(form)}
}
resetForm()
show.value = false
}).catch((e) => { }).catch((e) => {
ElNotification({ ElNotification({
title: '发送失败', title: '发送失败',
@@ -241,6 +262,7 @@ async function sendEmail() {
message: h('span', { style: 'color: teal' }, e.message), message: h('span', { style: 'color: teal' }, e.message),
position: 'bottom-right' position: 'bottom-right'
}) })
show.value = true
}).finally(() => { }).finally(() => {
percentMessage.close() percentMessage.close()
percent.value = 0 percent.value = 0
@@ -255,8 +277,13 @@ function resetForm() {
form.content = '' form.content = ''
form.manyType = null form.manyType = null
form.attachments = [] form.attachments = []
form.sendType = null form.sendType = ''
form.emailId = 0 form.emailId = 0
form.draftId = null
backReply.content = ''
backReply.subject = ''
backReply.receiveEmail = []
backReply.sendType = ''
editor.value.clearEditor() editor.value.clearEditor()
} }
@@ -291,7 +318,13 @@ function openReply(email) {
</article> </article>
</blockquote>` </blockquote>`
open() 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) { function formatImage(content) {
content = content || ''; content = content || '';
const domain = settingStore.settings.r2Domain; const domain = settingStore.settings.r2Domain;
console.log(content)
return content.replace(/{{domain}}/g, domain + '/'); return content.replace(/{{domain}}/g, domain + '/');
} }
@@ -317,6 +349,14 @@ function open() {
editor.value.focus() 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) => { const handleKeyDown = (event) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
close() close()
@@ -332,7 +372,52 @@ onUnmounted(() => {
}); });
function close() { 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> </script>

View File

@@ -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}})
}

View File

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

View File

@@ -28,6 +28,16 @@ const routes = [
menu: true menu: true
} }
}, },
{
path: '/draft',
name: 'draft',
component: () => import('@/views/draft/index.vue'),
meta: {
title: '草稿箱',
name: 'draft',
menu: true
}
},
{ {
path: '/content', path: '/content',
name: 'content', name: 'content',

View File

@@ -0,0 +1,8 @@
import { defineStore } from 'pinia'
export const userDraftStore = defineStore('draft', {
state: () => ({
refreshList: 0,
setDraft: {},
})
})

View File

@@ -41,3 +41,7 @@ export function formatDetailDate(time) {
export function tzDayjs(time) { export function tzDayjs(time) {
return dayjs.utc(time).tz('Asia/Shanghai') return dayjs.utc(time).tz('Asia/Shanghai')
} }
export function toUtc() {
return dayjs().utc()
}

View File

@@ -47,6 +47,16 @@ const routers = {
menu: true 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': { 'sys-email:query': {
path: '/sys/email', path: '/sys/email',
name: 'sys-email', name: 'sys-email',

View File

@@ -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;
}

View File

@@ -680,7 +680,7 @@ function createSendGauge() {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px; gap: 20px;
@media (max-width: 1024px) { @media (max-width: 1199px) {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 15px; gap: 15px;
} }
@@ -691,7 +691,7 @@ function createSendGauge() {
background: #fff; background: #fff;
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--el-border-color); border: 1px solid var(--el-border-color);
padding: 25px 20px; padding: 21px 20px;
.top { .top {
display: grid; display: grid;
justify-content: space-between; justify-content: space-between;

View File

@@ -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>

View File

@@ -69,6 +69,8 @@
</el-input> </el-input>
<el-input v-model="registerForm.password" placeholder="密码" type="password" autocomplete="off" /> <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-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" <div v-show="verifyShow"
class="register-turnstile" class="register-turnstile"
:data-sitekey="settingStore.settings.siteKey" :data-sitekey="settingStore.settings.siteKey"
@@ -116,7 +118,8 @@ const suffix = ref('')
const registerForm = reactive({ const registerForm = reactive({
email: '', email: '',
password: '', password: '',
confirmPassword: '' confirmPassword: '',
code: null
}) })
const domainList = settingStore.domainList; const domainList = settingStore.domainList;
const registerLoading = ref(false) const registerLoading = ref(false)
@@ -248,6 +251,20 @@ function submitRegister() {
return return
} }
if(settingStore.settings.regKey === 0) {
if (!registerForm.code) {
ElMessage({
message: '注册码不能为空',
type: 'error',
plain: true,
})
return
}
}
if (!verifyToken && settingStore.settings.registerVerify === 0) { if (!verifyToken && settingStore.settings.registerVerify === 0) {
verifyShow.value = true verifyShow.value = true
if (!turnstileId) { if (!turnstileId) {
@@ -263,11 +280,20 @@ function submitRegister() {
} }
registerLoading.value = true 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' show.value = 'login'
registerForm.email = '' registerForm.email = ''
registerForm.password = '' registerForm.password = ''
registerForm.confirmPassword = '' registerForm.confirmPassword = ''
registerForm.code = ''
registerLoading.value = false registerLoading.value = false
turnstileId = null turnstileId = null
verifyToken = '' verifyToken = ''

View File

@@ -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>

View File

@@ -47,10 +47,15 @@
</el-table> </el-table>
</div> </div>
</el-scrollbar> </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"> <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.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 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"> <div class="dialog-input">
<el-input-number placeholder="排序" :min="0" :max="9999" v-model.number="form.sort" controls-position="right" autocomplete="off" /> <el-input-number placeholder="排序" :min="0" :max="9999" v-model.number="form.sort" controls-position="right" autocomplete="off" />
</div> </div>
@@ -74,15 +79,18 @@
<div> <div>
<span>{{node.label}}</span> <span>{{node.label}}</span>
<span class="send-num" v-if="data.permKey === 'email:send'" @click.stop> <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-input-number>
<el-select v-model="form.sendType" placeholder="Select" size="small" style="width: 60px;margin-left: 5px;"> <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="count" />
<el-option label="每天" value="day" /> <el-option label="每天" value="day" />
</el-select> </el-select>
<el-tooltip effect="dark" content="零无限制 负数无次数">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</span> </span>
<span class="send-num" v-if="data.permKey === 'account:add'" @click.stop> <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> </el-input-number>
</span> </span>
</div> </div>
@@ -102,6 +110,7 @@ import {roleAdd, roleDelete, rolePermTree, roleRoleList, roleSet, roleSetDef} fr
import loading from '@/components/loading/index.vue'; import loading from '@/components/loading/index.vue';
import {useRoleStore} from "@/store/role.js"; import {useRoleStore} from "@/store/role.js";
import {useUserStore} from "@/store/user.js"; import {useUserStore} from "@/store/user.js";
import {isEmail} from "@/utils/verify-utils.js";
defineOptions({ defineOptions({
name: 'role' name: 'role'
@@ -128,9 +137,11 @@ const dialogType = reactive({
const form = reactive({ const form = reactive({
name: null, name: null,
description: null, description: null,
banEmail: [],
banEmailType: 0,
sendType: 'count', sendType: 'count',
sendCount: '', sendCount: 0,
accountCount: '', accountCount: 0,
sort: 0, sort: 0,
isDefault: 0, isDefault: 0,
}) })
@@ -145,6 +156,21 @@ rolePermTree().then(tree => {
treeList.push(...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() { function roleFormClick() {
if (dialogType.type === 'add') { if (dialogType.type === 'add') {
addRole() addRole()
@@ -240,8 +266,10 @@ function resetForm() {
form.description = null form.description = null
form.sort = 0 form.sort = 0
form.sendType = 'count' form.sendType = 'count'
form.sendCount = '' form.sendCount = 0
form.accountCount = '' form.accountCount = 0
form.banEmail = []
form.banEmailType = 0
tree.value.setCheckedKeys([]) tree.value.setCheckedKeys([])
} }
@@ -256,6 +284,7 @@ function openRoleSet(role) {
form.sendType = role.sendType form.sendType = role.sendType
form.sendCount = role.sendCount form.sendCount = role.sendCount
form.accountCount = role.accountCount form.accountCount = role.accountCount
form.banEmail = role.banEmail
nextTick(() => { nextTick(() => {
tree.value.setCheckedKeys(role.permIds) tree.value.setCheckedKeys(role.permIds)
}) })
@@ -346,7 +375,7 @@ window.onresize = () => {
padding: 9px 15px; padding: 9px 15px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20px; gap: 18px;
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12); box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
font-size: 18px; font-size: 18px;
.search { .search {
@@ -362,6 +391,14 @@ window.onresize = () => {
} }
} }
.warning {
position: relative;
left: 5px;
top: 5px;
color: gray;
cursor: pointer;
}
:deep(.description) { :deep(.description) {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -397,6 +434,12 @@ window.onresize = () => {
.dialog-input { .dialog-input {
margin-bottom: 15px !important; margin-bottom: 15px !important;
} }
.dialog-radio {
margin-top: 5px;
margin-bottom: 5px;
}
.dialog-input-tag {
}
} }
.perm-expand { .perm-expand {
@@ -406,11 +449,11 @@ window.onresize = () => {
bottom: 5px; bottom: 5px;
} }
:deep(.el-dialog) { :deep(.el-dialog) {
margin-top: 15vh !important;
margin-bottom: 20px !important; margin-bottom: 20px !important;
width: 400px !important; width: 460px !important;
@media (max-width: 440px) { @media (max-width: 500px) {
width: calc(100% - 40px) !important; width: calc(100% - 40px) !important;
margin-right: 20px !important; margin-right: 20px !important;
margin-left: 20px !important; margin-left: 20px !important;

View File

@@ -8,9 +8,11 @@
:show-star="false" :show-star="false"
show-user-info show-user-info
show-status show-status
actionLeft="4px"
:show-account-icon="false" :show-account-icon="false"
@jump="jumpContent" @jump="jumpContent"
@refresh-before="refreshBefore" @refresh-before="refreshBefore"
:type="'sys-email'"
> >
<template #first> <template #first>
@@ -23,7 +25,7 @@
<div @click.stop="openSelect"> <div @click.stop="openSelect">
<el-select <el-select
ref="mySelect" ref="mySelect"
v-model="searchType" v-model="params.searchType"
placeholder="请选择" placeholder="请选择"
class="select" class="select"
> >
@@ -59,10 +61,10 @@
<script setup> <script setup>
import {starAdd, starCancel} from "@/request/star.js"; import {starAdd, starCancel} from "@/request/star.js";
import emailScroll from "@/components/email-scroll/index.vue" 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 {useEmailStore} from "@/store/email.js";
import { import {
sysEmailAll, sysEmailList,
sysEmailDelete sysEmailDelete
} from "@/request/sys-email.js"; } from "@/request/sys-email.js";
import {Icon} from "@iconify/vue"; import {Icon} from "@iconify/vue";
@@ -74,7 +76,6 @@ defineOptions({
const emailStore = useEmailStore(); const emailStore = useEmailStore();
const sysEmailScroll = ref({}) const sysEmailScroll = ref({})
const searchType = ref('name')
const searchValue = ref('') const searchValue = ref('')
const mySelect = ref() const mySelect = ref()
@@ -88,15 +89,31 @@ const params = reactive({
userEmail: null, userEmail: null,
accountEmail: null, accountEmail: null,
name: null, name: null,
subject: null subject: null,
searchType: 'name'
}) })
const selectTitle = computed(() => { const selectTitle = computed(() => {
if (searchType.value === 'user') return '用户' if (params.searchType === 'user') return '用户'
if (searchType.value === 'account') return '邮箱' if (params.searchType === 'account') return '邮箱'
if (searchType.value === 'name') return '发件人' if (params.searchType === 'name') return '发件人'
if (searchType.value === 'subject') 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() { function refreshBefore() {
@@ -107,7 +124,7 @@ function refreshBefore() {
params.accountEmail = null params.accountEmail = null
params.name = null params.name = null
params.subject = null params.subject = null
params.searchType = 'name'
} }
function search() { function search() {
@@ -117,19 +134,19 @@ function search() {
params.name = null params.name = null
params.subject = null params.subject = null
if (searchType.value === 'user') { if (params.searchType === 'user') {
params.userEmail = searchValue.value params.userEmail = searchValue.value
} }
if (searchType.value === 'account') { if (params.searchType === 'account') {
params.accountEmail = searchValue.value params.accountEmail = searchValue.value
} }
if (searchType.value === 'name') { if (params.searchType === 'name') {
params.name = searchValue.value params.name = searchValue.value
} }
if (searchType.value === 'subject') { if (params.searchType === 'subject') {
params.subject = searchValue.value params.subject = searchValue.value
} }
@@ -151,7 +168,7 @@ function jumpContent(email) {
function getEmailList(emailId, size) { function getEmailList(emailId, size) {
return sysEmailAll({emailId, size, ...params}) return sysEmailList({emailId, size, ...params})
} }
</script> </script>

View File

@@ -17,6 +17,24 @@
v-model="setting.register"/> v-model="setting.register"/>
</div> </div>
</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 class="setting-item">
<div><span>添加邮箱</span></div> <div><span>添加邮箱</span></div>
<div> <div>
@@ -36,29 +54,6 @@
v-model="setting.manyEmail"/> v-model="setting.manyEmail"/>
</div> </div>
</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 class="setting-item">
<div> <div>
<span>物理清空数据</span> <span>物理清空数据</span>
@@ -135,6 +130,29 @@
v-model="setting.receive"/> v-model="setting.receive"/>
</div> </div>
</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 class="setting-item">
<div><span>邮件发送</span></div> <div><span>邮件发送</span></div>
<div> <div>
@@ -145,15 +163,14 @@
<div class="setting-item"> <div class="setting-item">
<div><span>添加 Resend Token</span></div> <div><span>添加 Resend Token</span></div>
<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"> <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"/> <Icon icon="material-symbols:add-rounded" width="16" height="16"/>
</el-button> </el-button>
</div> </div>
</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>
</div> </div>
@@ -227,7 +244,7 @@
<div class="setting-item"> <div class="setting-item">
<div><span>Site Key</span></div> <div><span>Site Key</span></div>
<div class="bot-verify"> <div class="bot-verify">
<span>{{ setting.siteKey || '空' }}</span> <span>{{ setting.siteKey }}</span>
<el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true"> <el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true">
<Icon icon="lsicon:edit-outline" width="16" height="16"/> <Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button> </el-button>
@@ -236,7 +253,7 @@
<div class="setting-item"> <div class="setting-item">
<div><span>Secret Key</span></div> <div><span>Secret Key</span></div>
<div class="bot-verify"> <div class="bot-verify">
<span> {{ setting.secretKey || '空' }} </span> <span> {{ setting.secretKey }} </span>
<el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true"> <el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true">
<Icon icon="lsicon:edit-outline" width="16" height="16"/> <Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button> </el-button>
@@ -279,7 +296,7 @@
<el-button type="primary" :loading="settingLoading" @click="saveTitle">保存</el-button> <el-button type="primary" :loading="settingLoading" @click="saveTitle">保存</el-button>
</form> </form>
</el-dialog> </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> <form>
<el-select style="margin-bottom: 15px" v-model="resendTokenForm.domain" placeholder="Select"> <el-select style="margin-bottom: 15px" v-model="resendTokenForm.domain" placeholder="Select">
<el-option <el-option
@@ -289,7 +306,7 @@
:value="item" :value="item"
/> />
</el-select> </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> <el-button type="primary" :loading="settingLoading" @click="saveResendToken">保存</el-button>
</form> </form>
</el-dialog> </el-dialog>
@@ -299,7 +316,7 @@
<el-button type="primary" :loading="settingLoading" @click="saveR2domain">保存</el-button> <el-button type="primary" :loading="settingLoading" @click="saveR2domain">保存</el-button>
</form> </form>
</el-dialog> </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 = ''"> @closed="turnstileForm.secretKey = '';turnstileForm.siteKey = ''">
<form> <form>
<el-input type="text" placeholder="siteKey" v-model="turnstileForm.siteKey"/> <el-input type="text" placeholder="siteKey" v-model="turnstileForm.siteKey"/>
@@ -368,7 +385,7 @@
</div> </div>
</template> </template>
<div class="forward-set-body"> <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> </div>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
@@ -392,7 +409,7 @@
</div> </div>
</template> </template>
<div class="forward-set-body"> <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> </div>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
@@ -406,12 +423,18 @@
</div> </div>
</template> </template>
</el-dialog> </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> </el-scrollbar>
</div> </div>
</template> </template>
<script setup> <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 {physicsDeleteAll, setBackground, settingQuery, settingSet} from "@/request/setting.js";
import {useSettingStore} from "@/store/setting.js"; import {useSettingStore} from "@/store/setting.js";
import {useUserStore} from "@/store/user.js"; import {useUserStore} from "@/store/user.js";
@@ -419,9 +442,10 @@ import {useAccountStore} from "@/store/account.js";
import {Icon} from "@iconify/vue"; import {Icon} from "@iconify/vue";
import {cvtR2Url} from "@/utils/convert.js"; import {cvtR2Url} from "@/utils/convert.js";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import { debounce } from 'lodash-es' import {debounce} from 'lodash-es'
import {isEmail} from "@/utils/verify-utils.js"; import {isEmail} from "@/utils/verify-utils.js";
import loading from "@/components/loading/index.vue"; import loading from "@/components/loading/index.vue";
import {getTextWidth} from "@/utils/text.js";
defineOptions({ defineOptions({
name: 'sys-setting' name: 'sys-setting'
@@ -440,6 +464,7 @@ const turnstileShow = ref(false)
const tgSettingShow = ref(false) const tgSettingShow = ref(false)
const thirdEmailShow = ref(false) const thirdEmailShow = ref(false)
const forwardRulesShow = ref(false) const forwardRulesShow = ref(false)
const showResendList = ref(false)
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const {settings: setting} = storeToRefs(settingStore); const {settings: setting} = storeToRefs(settingStore);
const editTitle = ref('') const editTitle = ref('')
@@ -455,6 +480,13 @@ const turnstileForm = reactive({
siteKey: '', siteKey: '',
secretKey: '' secretKey: ''
}) })
const regKeyOptions = [
{label: '开启', value: 0},
{label: '关闭', value: 1},
{label: '可选', value: 2},
]
const options = [ const options = [
{label: '关闭', value: 0}, {label: '关闭', value: 0},
{label: '3s', value: 3}, {label: '3s', value: 3},
@@ -468,12 +500,40 @@ const options = [
const tgChatId = ref([]) const tgChatId = ref([])
const tgBotStatus = ref(0) const tgBotStatus = ref(0)
const tgBotToken = ref('') const tgBotToken = ref('')
const forwardEmail = ref([]) const forwardEmail = ref([])
const forwardStatus = ref(0) const forwardStatus = ref(0)
const emailColumnWidth = ref(0)
const tokenColumnWidth = ref(0)
const ruleType = ref(0) const ruleType = ref(0)
const ruleEmail = ref([]) 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 => { settingQuery().then(settingData => {
setting.value = settingData setting.value = settingData
@@ -493,6 +553,10 @@ function openTgSetting() {
tgSettingShow.value = true tgSettingShow.value = true
} }
function openResendList() {
showResendList.value = true
}
function openThirdEmailSetting() { function openThirdEmailSetting() {
forwardEmail.value = [] forwardEmail.value = []
forwardStatus.value = setting.value.forwardStatus 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) { :deep(.cut-dialog.el-dialog) {
width: fit-content !important; width: fit-content !important;
height: fit-content !important; height: fit-content !important;

View File

@@ -307,6 +307,20 @@ roleSelectUse().then(list => {
roleList.push(...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, () => { watch(() => roleStore.refresh, () => {
roleSelectUse().then(list => { roleSelectUse().then(list => {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1,11 +1,11 @@
{ {
"name": "mail-cf", "name": "mail-worker",
"version": "0.0.0", "version": "0.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mail-cf", "name": "mail-worker",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "^1.0.5", "@cloudflare/vite-plugin": "^1.0.5",

View File

@@ -1,7 +1,6 @@
import r2Service from '../service/r2-service'; import r2Service from '../service/r2-service';
import app from '../hono/hono'; import app from '../hono/hono';
app.get('/file/*', async (c) => { app.get('/file/*', async (c) => {
const key = c.req.path.split('/file/')[1]; const key = c.req.path.split('/file/')[1];
const obj = await r2Service.getObj(c, key); const obj = await r2Service.getObj(c, key);

View File

@@ -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));
})

View File

@@ -2,12 +2,12 @@ import app from '../hono/hono';
import emailService from '../service/email-service'; import emailService from '../service/email-service';
import result from '../model/result'; 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()); const data = await emailService.allList(c, c.req.query());
return c.json(result.ok(data)); 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()); const list = await emailService.physicsDelete(c, c.req.query());
return c.json(result.ok(list)); return c.json(result.ok(list));
}) })

View File

@@ -10,6 +10,10 @@ export const roleConst = {
CLOSE: 0, CLOSE: 0,
OPEN: 1 OPEN: 1
}, },
banEmailType: {
ALL: 0,
CONTENT: 1
},
sendType: { sendType: {
COUNT: 'count', COUNT: 'count',
DAY: 'day' DAY: 'day'
@@ -55,6 +59,11 @@ export const settingConst = {
OPEN: 0, OPEN: 0,
CLOSE: 1, CLOSE: 1,
}, },
regKey: {
OPEN: 0,
CLOSE: 1,
OPTIONAL: 2,
},
receive: { receive: {
OPEN: 0, OPEN: 0,
CLOSE: 1, CLOSE: 1,

View File

@@ -5,11 +5,12 @@ import settingService from '../service/setting-service';
import attService from '../service/att-service'; import attService from '../service/att-service';
import constant from '../const/constant'; import constant from '../const/constant';
import fileUtils from '../utils/file-utils'; 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 emailUtils from '../utils/email-utils';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc' import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone' import timezone from 'dayjs/plugin/timezone'
import roleService from '../service/role-service';
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
@@ -33,7 +34,6 @@ export async function email(message, env, ctx) {
return; return;
} }
const account = await accountService.selectByEmailIncludeDelNoCase({ env: env }, message.to);
const reader = message.raw.getReader(); const reader = message.raw.getReader();
let content = ''; let content = '';
@@ -46,6 +46,53 @@ export async function email(message, env, ctx) {
const email = await PostalMime.parse(content); const email = await PostalMime.parse(content);
const account = await accountService.selectByEmailIncludeDelNoCase({ env: env }, message.to);
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 toName = email.to.find(item => item.address === message.to)?.name || '';
const params = { const params = {
@@ -68,6 +115,11 @@ export async function email(message, env, ctx) {
status: emailConst.status.SAVING status: emailConst.status.SAVING
}; };
let headers = message.headers
console.log(headers.get('X-Cf-Spamh-Score'))
console.log(email)
const attachments = []; const attachments = [];
const cidAttachments = []; const cidAttachments = [];

View File

@@ -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

View File

@@ -5,6 +5,8 @@ export const role = sqliteTable('role', {
name: text('name').notNull(), name: text('name').notNull(),
key: text('key').notNull(), key: text('key').notNull(),
description: text('description'), description: text('description'),
banEmail: text('ban_email').notNull().default(''),
banEmailType: integer('ban_email_type').notNull().default(0),
sort: integer('sort'), sort: integer('sort'),
isDefault: integer('is_default').default(0), isDefault: integer('is_default').default(0),
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(), createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),

View File

@@ -12,6 +12,7 @@ export const setting = sqliteTable('setting', {
r2Domain: text('r2_domain'), r2Domain: text('r2_domain'),
secretKey: text('secret_key'), secretKey: text('secret_key'),
siteKey: text('site_key'), siteKey: text('site_key'),
regKey: integer('reg_key').default(1).notNull(),
background: text('background'), background: text('background'),
tgBotToken: text('tg_bot_token').default('').notNull(), tgBotToken: text('tg_bot_token').default('').notNull(),
tgChatId: text('tg_chat_id').default('').notNull(), tgChatId: text('tg_chat_id').default('').notNull(),

View File

@@ -16,6 +16,7 @@ const user = sqliteTable('user', {
device: text('device'), device: text('device'),
sort: text('sort').default(0), sort: text('sort').default(0),
sendCount: text('send_count').default(0), sendCount: text('send_count').default(0),
regKeyId: integer('reg_key_id').default(0).notNull(),
isDel: integer('is_del').default(0).notNull() isDel: integer('is_del').default(0).notNull()
}); });
export default user export default user

View File

@@ -16,4 +16,5 @@ import '../api/role-api'
import '../api/sys-email-api' import '../api/sys-email-api'
import '../api/init-api' import '../api/init-api'
import '../api/analysis-api' import '../api/analysis-api'
import '../api/reg-key-api'
export default app; export default app;

View File

@@ -15,10 +15,62 @@ const init = {
await this.v1_2DB(c); await this.v1_2DB(c);
await this.v1_3DB(c); await this.v1_3DB(c);
await this.v1_3_1DB(c); await this.v1_3_1DB(c);
await this.v1_4DB(c);
await settingService.refresh(c); await settingService.refresh(c);
return c.text('初始化成功'); 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) { 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(); 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();
}, },

View File

@@ -29,8 +29,8 @@ const requirePerms = [
'/role/tree', '/role/tree',
'/role/set', '/role/set',
'/role/setDefault', '/role/setDefault',
'/sys-email/list', '/sysEmail/list',
'/sys-email/delete', '/sysEmail/delete',
'/setting/physicsDeleteAll', '/setting/physicsDeleteAll',
'/setting/setBackground', '/setting/setBackground',
'/setting/set', '/setting/set',
@@ -41,7 +41,12 @@ const requirePerms = [
'/user/setType', '/user/setType',
'/user/list', '/user/list',
'/user/resetSendCount', '/user/resetSendCount',
'/user/add' '/user/add',
'/regKey/add',
'/regKey/list',
'/regKey/delete',
'/regKey/clearNotUse',
'/regKey/history'
]; ];
const premKey = { const premKey = {
@@ -62,12 +67,15 @@ const premKey = {
'user:set-status': ['/user/setStatus'], 'user:set-status': ['/user/setStatus'],
'user:set-type': ['/user/setType'], 'user:set-type': ['/user/setType'],
'user:delete': ['/user/delete'], 'user:delete': ['/user/delete'],
'sys-email:query': ['/sys-email/list'], 'sys-email:query': ['/sysEmail/list'],
'sys-email:delete': ['/sys-email/delete'], 'sys-email:delete': ['/sysEmail/delete'],
'setting:query': ['/setting/query'], 'setting:query': ['/setting/query'],
'setting:set': ['/setting/set', '/setting/setBackground'], 'setting:set': ['/setting/set', '/setting/setBackground'],
'setting:clean': ['/setting/physicsDeleteAll'], '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) => { app.use('*', async (c, next) => {

View File

@@ -48,7 +48,7 @@ const accountService = {
if (roleRow.accountCount && userRow.email !== c.env.admin) { if (roleRow.accountCount && userRow.email !== c.env.admin) {
const userAccountCount = await accountService.countUserAccount(c, userId) 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)) { if (await settingService.isAddEmailVerify(c)) {
@@ -181,6 +181,9 @@ const accountService = {
async setName(c, params, userId) { async setName(c, params, userId) {
const { name, accountId } = params 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(); await orm(c).update(account).set({name}).where(and(eq(account.userId, userId),eq(account.accountId, accountId))).run();
} }
}; };

View File

@@ -5,10 +5,7 @@ import { desc, count, eq, and, ne, isNotNull } from 'drizzle-orm';
import { emailConst } from '../const/entity-const'; import { emailConst } from '../const/entity-const';
import kvConst from '../const/kv-const'; import kvConst from '../const/kv-const';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone' import { toUtc } from '../utils/date-uitil';
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
dayjs.extend(timezone)
const analysisService = { const analysisService = {
async echarts(c) { async echarts(c) {
@@ -62,7 +59,7 @@ const analysisService = {
}, },
filterEmptyDay(data) { 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) => { const previousDays = Array.from({ length: 15 }, (_, i) => {
return today.subtract(i, 'day').format('YYYY-MM-DD'); return today.subtract(i, 'day').format('YYYY-MM-DD');
}).reverse(); }).reverse();

View File

@@ -150,7 +150,7 @@ const emailService = {
} }
if (send === settingConst.send.CLOSE) { if (send === settingConst.send.CLOSE) {
throw new BizError('邮发送功能已停用', 403); throw new BizError('邮发送功能已停用', 403);
} }
if (attachments.length > 0 && manyType === 'divide') { if (attachments.length > 0 && manyType === 'divide') {
@@ -163,13 +163,17 @@ const emailService = {
if (c.env.admin !== userRow.email && roleRow.sendCount) { if (c.env.admin !== userRow.email && roleRow.sendCount) {
if (roleRow.sendCount < 0) {
throw new BizError('用户无发送次数', 403);
}
if (userRow.sendCount >= roleRow.sendCount) { if (userRow.sendCount >= 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); if (roleRow.sendType === 'count') throw new BizError('发送次数已到达限制', 403);
} }
if (userRow.sendCount + receiveEmail.length > roleRow.sendCount) { 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); if (roleRow.sendType === 'count') throw new BizError('剩余发送次数不足', 403);
} }
@@ -178,18 +182,20 @@ const emailService = {
const accountRow = await accountService.selectById(c, accountId); const accountRow = await accountService.selectById(c, accountId);
if (!accountRow) {
throw new BizError('发件人邮箱不存在');
}
const domain = emailUtils.getDomain(accountRow.email); const domain = emailUtils.getDomain(accountRow.email);
const resendToken = resendTokens[domain]; const resendToken = resendTokens[domain];
if (!resendToken) { if (!resendToken) {
throw new BizError('resend密钥未配置'); throw new BizError('resend密钥未配置');
} }
if (!accountRow) {
throw new BizError('邮箱不存在');
}
if (accountRow.userId !== userId) { if (accountRow.userId !== userId) {
throw new BizError('非当前用户所属邮箱'); throw new BizError('发件人邮箱非当前用户所');
} }
if (!name) { if (!name) {
@@ -488,7 +494,7 @@ const emailService = {
async allList(c, params) { 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); size = Number(size);

View File

@@ -1,7 +1,7 @@
import BizError from '../error/biz-error'; import BizError from '../error/biz-error';
import userService from './user-service'; import userService from './user-service';
import emailUtils from '../utils/email-utils'; 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 JwtUtils from '../utils/jwt-utils';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import KvConst from '../const/kv-const'; import KvConst from '../const/kv-const';
@@ -14,15 +14,19 @@ import saltHashUtils from '../utils/crypto-utils';
import cryptoUtils from '../utils/crypto-utils'; import cryptoUtils from '../utils/crypto-utils';
import turnstileService from './turnstile-service'; import turnstileService from './turnstile-service';
import roleService from './role-service'; import roleService from './role-service';
import regKeyService from './reg-key-service';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { formatDetailDate, toUtc } from '../utils/date-uitil';
const loginService = { const loginService = {
async register(c, params) { 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('注册功能已关闭'); throw new BizError('注册功能已关闭');
} }
@@ -30,7 +34,15 @@ const loginService = {
throw new BizError('非法邮箱'); 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位'); throw new BizError('密码必须大于6位');
} }
@@ -38,6 +50,21 @@ const loginService = {
throw new BizError('非法邮箱域名'); 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); const accountRow = await accountService.selectByEmailIncludeDelNoCase(c, email);
if (accountRow && accountRow.isDel === isDel.DELETE) { if (accountRow && accountRow.isDel === isDel.DELETE) {
@@ -45,7 +72,7 @@ const loginService = {
} }
if (accountRow) { if (accountRow) {
throw new BizError('该邮箱已被其他用户绑定'); throw new BizError('该邮箱已被注册');
} }
if (await settingService.isRegisterVerify(c)) { if (await settingService.isRegisterVerify(c)) {
@@ -54,13 +81,71 @@ const loginService = {
const { salt, hash } = await saltHashUtils.hashPassword(password); 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 userService.updateUserInfo(c, userId, true);
await accountService.insert(c, { userId: userId, email, name: emailUtils.getName(email) }); 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) { async login(c, params) {

View File

@@ -1,6 +1,3 @@
import attService from './att-service';
import constant from '../const/constant';
const r2Service = { const r2Service = {
async putObj(c, key, content, metadata) { async putObj(c, key, content, metadata) {
await c.env.r2.put(key, content, { await c.env.r2.put(key, content, {

View File

@@ -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;

View File

@@ -6,12 +6,15 @@ import rolePerm from '../entity/role-perm';
import perm from '../entity/perm'; import perm from '../entity/perm';
import { permConst, roleConst } from '../const/entity-const'; import { permConst, roleConst } from '../const/entity-const';
import userService from './user-service'; import userService from './user-service';
import user from '../entity/user';
import emailUtils from '../utils/email-utils';
import verifyUtils from '../utils/verify-utils';
const roleService = { const roleService = {
async add(c, params, userId) { async add(c, params, userId) {
let { name, permIds } = params; let { name, permIds, banEmail } = params;
if (!name) { if (!name) {
throw new BizError('身份名不能为空'); throw new BizError('身份名不能为空');
@@ -23,7 +26,15 @@ const roleService = {
throw new BizError('身份名已存在'); 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) { if (permIds.length === 0) {
return; return;
@@ -44,6 +55,7 @@ const roleService = {
.where(eq(perm.type, permConst.type.BUTTON)).all(); .where(eq(perm.type, permConst.type.BUTTON)).all();
roleList.forEach(role => { roleList.forEach(role => {
role.banEmail = role.banEmail.split(",").filter(item => item !== "")
role.permIds = permList.filter(perm => perm.roleId === role.roleId).map(perm => perm.permId); role.permIds = permList.filter(perm => perm.roleId === role.roleId).map(perm => perm.permId);
}); });
@@ -52,7 +64,7 @@ const roleService = {
async setRole(c, params) { async setRole(c, params) {
let { name, permIds, roleId } = params; let { name, permIds, roleId, banEmail } = params;
if (!name) { if (!name) {
throw new BizError('名字不能为空'); throw new BizError('名字不能为空');
@@ -60,7 +72,15 @@ const roleService = {
delete params.isDefault 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(); await orm(c).delete(rolePerm).where(eq(rolePerm.roleId, roleId)).run();
if (permIds.length > 0) { if (permIds.length > 0) {
@@ -126,6 +146,10 @@ const roleService = {
.leftJoin(rolePerm, eq(perm.permId, rolePerm.permId)) .leftJoin(rolePerm, eq(perm.permId, rolePerm.permId))
.leftJoin(role, eq(role.roleId, rolePerm.roleId)) .leftJoin(role, eq(role.roleId, rolePerm.roleId))
.where(and(eq(perm.permKey, permKey), eq(role.sendType, sendType))).all(); .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();
} }
}; };

View File

@@ -49,16 +49,6 @@ const settingService = {
await this.refresh(c); 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) { async isAddEmail(c) {
const { addEmail, manyEmail } = await this.query(c); const { addEmail, manyEmail } = await this.query(c);
return addEmail === settingConst.addEmail.OPEN && manyEmail === settingConst.manyEmail.OPEN; return addEmail === settingConst.addEmail.OPEN && manyEmail === settingConst.manyEmail.OPEN;
@@ -125,7 +115,8 @@ const settingService = {
siteKey: settingRow.siteKey, siteKey: settingRow.siteKey,
background: settingRow.background, background: settingRow.background,
loginOpacity: settingRow.loginOpacity, loginOpacity: settingRow.loginOpacity,
domainList:settingRow.domainList domainList:settingRow.domainList,
regKey: settingRow.regKey
}; };
} }
}; };

View File

@@ -382,6 +382,15 @@ const userService = {
await accountService.restoreByUserId(c, userId); 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();
} }
}; };

View File

@@ -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())
}

View File

@@ -31,6 +31,6 @@ crons = ["0 16 * * *"] #定时任务每天晚上12点执行
#[vars] #[vars]
#orm_log = false #orm_log = false
#domain = [] #邮件域名可配置多个 示例: ["example1.com","example2.com"] #domain = [] #邮件域名可配置多个 示例: ["example1.com","example2.com"]
#admin = "" #管理员的邮箱 示例: admin@example.com #admin = "" #管理员的邮箱 示例: admin@example.com
#jwt_secret = "" #jwt令牌的密钥,随便填一串字符串 #jwt_secret = "" #jwt令牌的密钥,随便填一串字符串