新增注册码、草稿箱、权限拦截邮件、发件菜单隐藏
This commit is contained in:
@@ -51,7 +51,9 @@ http.interceptors.response.use((res) => {
|
||||
})
|
||||
reject(data)
|
||||
}
|
||||
resolve(data.data)
|
||||
setTimeout(() => {
|
||||
resolve(data.data)
|
||||
},1000)
|
||||
})
|
||||
},
|
||||
(error) => {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<div class="email-container">
|
||||
<div class="header-actions">
|
||||
<el-checkbox
|
||||
v-model="checkAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
:disabled="!emailList.length"
|
||||
@change="handleCheckAllChange"
|
||||
v-model="checkAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
:disabled="!emailList.length"
|
||||
@change="handleCheckAllChange"
|
||||
>
|
||||
</el-checkbox>
|
||||
<div class="header-left" :style="'padding-left:' + actionLeft">
|
||||
@@ -33,7 +33,7 @@
|
||||
:data-checked="item.checked"
|
||||
@click="jumpDetails(item)"
|
||||
>
|
||||
<el-checkbox v-model="item.checked" @click.stop></el-checkbox>
|
||||
<el-checkbox :class=" props.type === 'sys-email' ? 'sys-email-checkbox' : 'checkbox'" v-model="item.checked" @click.stop></el-checkbox>
|
||||
<div @click.stop="starChange(item)" class="pc-star" v-if="showStar">
|
||||
<Icon v-if="item.isStar" icon="fluent-color:star-16" width="20" height="20"/>
|
||||
<Icon v-else icon="solar:star-line-duotone" width="18" height="18"/>
|
||||
@@ -102,7 +102,9 @@
|
||||
</div>
|
||||
<div v-else></div>
|
||||
<span class="name">
|
||||
<span>{{ item.name }}</span>
|
||||
<span>
|
||||
<slot name="name" :email="item"> {{ item.name }}</slot>
|
||||
</span>
|
||||
<span>
|
||||
<Icon v-if="item.isStar" icon="fluent-color:star-16" width="18" height="18"/>
|
||||
</span>
|
||||
@@ -111,7 +113,11 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="email-text">
|
||||
<span class="email-subject">{{ item.subject }}</span>
|
||||
<span class="email-subject">
|
||||
<slot name="subject" :email="item">
|
||||
{{ item.subject }}
|
||||
</slot>
|
||||
</span>
|
||||
<span class="email-content">{{ htmlToText(item) }}</span>
|
||||
</div>
|
||||
<div class="user-info" v-if="showUserInfo">
|
||||
@@ -128,7 +134,7 @@
|
||||
<span>{{ item.type === 0 ? item.toEmail : item.sendEmail }}</span>
|
||||
</div>
|
||||
<div class="del-status" v-if="item.isDel">
|
||||
<el-tag type="info" size="small">已删除</el-tag>
|
||||
<el-tag type="danger" size="small">已删除</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,11 +207,15 @@ const props = defineProps({
|
||||
allowStar: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const emit = defineEmits(['jump', 'refresh-before'])
|
||||
const emit = defineEmits(['jump', 'refresh-before', 'delete-draft'])
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const uiStore = useUiStore();
|
||||
@@ -355,13 +365,19 @@ function changeAccountShow() {
|
||||
uiStore.accountShow = !uiStore.accountShow;
|
||||
}
|
||||
|
||||
|
||||
const handleDelete = () => {
|
||||
ElMessageBox.confirm('确认批量删除这些邮件吗?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
|
||||
if (props.type === 'draft') {
|
||||
const draftIds = getSelectedDraftsIds();
|
||||
emit('delete-draft', draftIds);
|
||||
return;
|
||||
}
|
||||
|
||||
const emailIds = getSelectedMailsIds();
|
||||
props.emailDelete(emailIds).then(() => {
|
||||
ElMessage({
|
||||
@@ -395,7 +411,6 @@ function addItem(email) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (props.timeSort) {
|
||||
if (noLoading.value) {
|
||||
emailList.push(email)
|
||||
@@ -430,6 +445,9 @@ function getSelectedMailsIds() {
|
||||
return emailList.filter(item => item.checked).map(item => item.emailId);
|
||||
}
|
||||
|
||||
function getSelectedDraftsIds() {
|
||||
return emailList.filter(item => item.checked).map(item => item.draftId);
|
||||
}
|
||||
|
||||
function updateCheckStatus() {
|
||||
const checkedCount = emailList.filter(item => item.checked).length;
|
||||
@@ -543,6 +561,7 @@ function loadData() {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.noLoading {
|
||||
@@ -625,13 +644,26 @@ function loadData() {
|
||||
}
|
||||
}
|
||||
|
||||
.el-checkbox {
|
||||
.checkbox {
|
||||
display: flex;
|
||||
padding-left: 15px;
|
||||
padding-right: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sys-email-checkbox {
|
||||
display: flex;
|
||||
padding-left: 15px;
|
||||
padding-right: 20px;
|
||||
justify-content: center;
|
||||
@media (min-width: 1200px) {
|
||||
justify-content: start;
|
||||
height: 100%;
|
||||
align-self: start;
|
||||
padding-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.title-column {
|
||||
@media (max-width: 1200px) {
|
||||
grid-template-columns: 1fr !important;
|
||||
@@ -642,7 +674,7 @@ function loadData() {
|
||||
.title {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
grid-template-columns: 240px 1fr;
|
||||
@media (max-width: 1199px) {
|
||||
padding-right: 15px;
|
||||
}
|
||||
@@ -702,6 +734,9 @@ function loadData() {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
@media (min-width: 1200px) {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.email-content {
|
||||
@@ -788,7 +823,7 @@ function loadData() {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
column-gap: 15px;
|
||||
column-gap: 18px;
|
||||
row-gap: 8px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted, onBeforeUnmount, watch, nextTick} from 'vue';
|
||||
import {ref, onMounted, onBeforeUnmount, watch, nextTick, shallowRef, defineEmits} from 'vue';
|
||||
import loading from "@/components/loading/index.vue";
|
||||
import {compressImage} from "@/utils/file-utils.js";
|
||||
defineExpose({
|
||||
clearEditor,
|
||||
focus
|
||||
focus,
|
||||
getContent
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
@@ -27,7 +28,7 @@ const props = defineProps({
|
||||
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
const editor = ref(null);
|
||||
const editor = shallowRef(null);
|
||||
const isInitialized = ref(false);
|
||||
const editorRef = ref(null);
|
||||
const showLoading = ref(false);
|
||||
@@ -162,6 +163,10 @@ function focus() {
|
||||
})
|
||||
}
|
||||
|
||||
function getContent() {
|
||||
return editor.value.getContent()
|
||||
}
|
||||
|
||||
|
||||
function destroyEditor() {
|
||||
if (editor.value) {
|
||||
|
||||
25
mail-vue/src/db/db.js
Normal file
25
mail-vue/src/db/db.js
Normal 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;
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="account-box">
|
||||
<div class="head-opt" >
|
||||
<Icon v-perm="'account:add'" class="icon" icon="ion:add-outline" width="23" height="23" @click="add" />
|
||||
<Icon class="icon" icon="ion:reload" width="18" height="18" @click="refresh" />
|
||||
<Icon v-perm="'account:add'" class="icon add" icon="ion:add-outline" width="23" height="23" @click="add" />
|
||||
<Icon class="icon refresh" icon="ion:reload" width="18" height="18" @click="refresh" />
|
||||
</div>
|
||||
<el-scrollbar class="scrollbar">
|
||||
<div v-infinite-scroll="getAccountList" :infinite-scroll-distance="600" :infinite-scroll-immediate="false">
|
||||
@@ -16,11 +16,12 @@
|
||||
</div>
|
||||
<div class="settings" @click.stop>
|
||||
<Icon icon="streamline-ultimate-color:copy-paste-1" width="19" height="19" @click.stop="copyAccount(item.email)"/>
|
||||
<el-dropdown>
|
||||
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399" />
|
||||
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399" v-if="showNullSetting(item)" />
|
||||
<el-dropdown v-else>
|
||||
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399" />
|
||||
<template #dropdown >
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="openSetName(item)">改名</el-dropdown-item>
|
||||
<el-dropdown-menu >
|
||||
<el-dropdown-item v-if="hasPerm('email:send')" @click="openSetName(item)">改名</el-dropdown-item>
|
||||
<el-dropdown-item v-if="item.accountId !== userStore.user.accountId && hasPerm('account:delete')" @click="remove(item)">删除</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -217,6 +218,10 @@ function openSetName (accountItem) {
|
||||
setNameShow.value = true
|
||||
}
|
||||
|
||||
function showNullSetting(item) {
|
||||
return !hasPerm('email:send') && !(item.accountId !== userStore.user.accountId && hasPerm('account:delete'))
|
||||
}
|
||||
|
||||
function itemBg(accountId) {
|
||||
return accountStore.currentAccountId === accountId ? 'item-choose' : ''
|
||||
}
|
||||
@@ -388,8 +393,16 @@ path[fill="#ffdda1"] {
|
||||
.icon{
|
||||
cursor: pointer;
|
||||
}
|
||||
.icon:nth-child(2) {
|
||||
margin-left: 15px;
|
||||
.refresh {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.add {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.head-opt:not(.add) .refresh {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
.scrollbar {
|
||||
|
||||
@@ -11,11 +11,16 @@
|
||||
<Icon icon="hugeicons:mailbox-01" width="20" height="20" />
|
||||
<span class="menu-name" style="margin-left: 21px">收件箱</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="router.push({name: 'send'})" index="send"
|
||||
<el-menu-item @click="router.push({name: 'send'})" index="send" v-perm="'email:send'"
|
||||
:class="route.meta.name === 'send' ? 'choose-item' : ''">
|
||||
<Icon icon="cil:send" width="20" height="20" />
|
||||
<span class="menu-name" style="margin-left: 21px">已发送</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="router.push({name: 'draft'})" index="draft" v-perm="'email:send'"
|
||||
:class="route.meta.name === 'draft' ? 'choose-item' : ''">
|
||||
<Icon icon="ep:document" width="19" height="19" />
|
||||
<span class="menu-name" style="margin-left: 22px">草稿箱</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="router.push({name: 'star'})" index="star"
|
||||
:class="route.meta.name === 'star' ? 'choose-item' : ''">
|
||||
<Icon icon="solar:star-line-duotone" width="20" height="20" />
|
||||
@@ -46,9 +51,14 @@
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="router.push({name: 'role'})" index="setting" v-perm="'role:query'"
|
||||
:class="route.meta.name === 'role' ? 'choose-item' : ''">
|
||||
<Icon icon="hugeicons:key-02" width="22" height="22" />
|
||||
<Icon icon="fluent:lock-closed-16-regular" width="22" height="22" />
|
||||
<span class="menu-name" style="margin-left: 20px">权限控制</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="router.push({name: 'reg-key'})" index="reg-key" v-perm="'reg-key:query'"
|
||||
:class="route.meta.name === 'reg-key' ? 'choose-item' : ''">
|
||||
<Icon icon="fluent:fingerprint-20-filled" width="22" height="22" />
|
||||
<span class="menu-name" style="margin-left: 20px">注册密钥</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="router.push({name: 'sys-setting'})" index="sys-setting" v-perm="'setting:query'"
|
||||
:class="route.meta.name === 'sys-setting' ? 'choose-item' : ''">
|
||||
<Icon icon="eos-icons:system-ok-outlined" width="18" height="18" />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="header">
|
||||
<div class="header" :class="!hasPerm('email:send') ? 'not-send' : ''">
|
||||
<div class="header-btn">
|
||||
<hanburger @click="changeAside"></hanburger>
|
||||
<span class="breadcrumb-item">{{ route.meta.title }}</span>
|
||||
</div>
|
||||
<div class="writer-box" @click="openSend">
|
||||
<div v-perm="'email:send'" class="writer-box" @click="openSend">
|
||||
<div class="writer" >
|
||||
<Icon icon="material-symbols:edit-outline-sharp" width="22" height="22" />
|
||||
</div>
|
||||
@@ -95,10 +95,14 @@ const sendType = computed(() => {
|
||||
return '无权限'
|
||||
}
|
||||
|
||||
if (userStore.user.role.sendCount === 0) {
|
||||
if (!userStore.user.role.sendCount) {
|
||||
return '无限制'
|
||||
}
|
||||
|
||||
if (userStore.user.role.sendCount < 0) {
|
||||
return '无次数'
|
||||
}
|
||||
|
||||
if (userStore.user.role.sendType === 'day') {
|
||||
return '每天'
|
||||
}
|
||||
@@ -116,6 +120,11 @@ const sendCount = computed(() => {
|
||||
if (!userStore.user.role.sendCount) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (userStore.user.role.sendCount < 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return userStore.user.sendCount + '/' + userStore.user.role.sendCount
|
||||
})
|
||||
|
||||
@@ -248,6 +257,10 @@ function full() {
|
||||
grid-template-columns: auto auto 1fr;
|
||||
}
|
||||
|
||||
.header.not-send {
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.writer-box {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div :class="accountShow && hasPerm('account:query') ? 'block-show' : 'block-hide'" @click="uiStore.accountShow = false"></div>
|
||||
<account :class="accountShow && hasPerm('account:query') ? 'show' : 'hide'" />
|
||||
<router-view class="main-view" v-slot="{ Component,route }">
|
||||
<keep-alive :include="['email','sys-email','send','sys-setting','star','user','role','analysis']">
|
||||
<keep-alive :include="['email','sys-email','send','sys-setting','star','user','role','analysis','reg-key','draft']">
|
||||
<component :is="Component" :key="route.name"/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="send" v-show="show" @click="close">
|
||||
<div class="send" v-show="show">
|
||||
<div class="write-box" @click.stop>
|
||||
<div class="title">
|
||||
<div class="title-left">
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<el-input-tag @add-tag="addTagChange" tag-type="primary" size="default" v-model="form.receiveEmail" placeholder="多邮个箱用, 分开 example1.com,example2.com" >
|
||||
<el-input-tag @add-tag="addTagChange" tag-type="primary" size="default" v-model="form.receiveEmail" placeholder="多个邮箱用, 分开 example1.com,example2.com" >
|
||||
<template #prefix>
|
||||
<div class="item-title">收件人 </div>
|
||||
</template>
|
||||
@@ -56,7 +56,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import tinyEditor from '@/components/tiny-editor/index.vue'
|
||||
import {h, onMounted, onUnmounted, reactive, ref} from "vue";
|
||||
import {h, nextTick, onMounted, onUnmounted, reactive, ref, toRaw} from "vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
import {emailSend} from "@/request/email.js";
|
||||
@@ -66,14 +66,19 @@ import {useEmailStore} from "@/store/email.js";
|
||||
import {fileToBase64, formatBytes} from "@/utils/file-utils.js";
|
||||
import {getIconByName} from "@/utils/icon-utils.js";
|
||||
import sendPercent from "@/components/send-percent/index.vue"
|
||||
import {formatDetailDate} from "@/utils/day.js";
|
||||
import {formatDetailDate, fromNow} from "@/utils/day.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {userDraftStore} from "@/store/draft.js";
|
||||
import db from "@/db/db.js";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
openReply
|
||||
openReply,
|
||||
openDraft
|
||||
})
|
||||
|
||||
const draftStore = userDraftStore()
|
||||
const settingStore = useSettingStore()
|
||||
const emailStore = useEmailStore();
|
||||
const accountStore = useAccountStore()
|
||||
@@ -84,6 +89,12 @@ const percent = ref(0)
|
||||
let percentMessage = null
|
||||
let sending = false
|
||||
const defValue = ref('')
|
||||
const backReply = reactive({
|
||||
receiveEmail: [],
|
||||
subject: '',
|
||||
content: '',
|
||||
sendType: ''
|
||||
})
|
||||
const form = reactive({
|
||||
sendEmail: '',
|
||||
receiveEmail: [],
|
||||
@@ -95,7 +106,8 @@ const form = reactive({
|
||||
sendType: '',
|
||||
text: '',
|
||||
emailId: 0,
|
||||
attachments: []
|
||||
attachments: [],
|
||||
draftId: null,
|
||||
})
|
||||
|
||||
function addTagChange(val) {
|
||||
@@ -110,10 +122,8 @@ function addTagChange(val) {
|
||||
if (isEmail(email) && !form.receiveEmail.includes(email)) {
|
||||
form.receiveEmail.push(email)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
function checkDistribute() {
|
||||
@@ -155,7 +165,6 @@ function chooseFile() {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const content = await fileToBase64(file)
|
||||
form.attachments.push({content, filename, size, contentType})
|
||||
}
|
||||
@@ -217,7 +226,9 @@ async function sendEmail() {
|
||||
})
|
||||
|
||||
sending = true
|
||||
close()
|
||||
|
||||
show.value = false
|
||||
|
||||
emailSend(form, (e) => {
|
||||
percent.value = Math.round((e.loaded * 98) / e.total)
|
||||
}).then(emailList => {
|
||||
@@ -225,15 +236,25 @@ async function sendEmail() {
|
||||
emailList.forEach(item => {
|
||||
emailStore.sendScroll?.addItem(item)
|
||||
})
|
||||
resetForm()
|
||||
show.value = false
|
||||
|
||||
ElNotification({
|
||||
title: '邮件已发送',
|
||||
type: "success",
|
||||
message: h('span', { style: 'color: teal' }, email.subject),
|
||||
position: 'bottom-right'
|
||||
})
|
||||
|
||||
userStore.refreshUserInfo();
|
||||
|
||||
if (form.draftId) {
|
||||
form.subject = ''
|
||||
form.content = ''
|
||||
form.receiveEmail = []
|
||||
draftStore.setDraft = {...toRaw(form)}
|
||||
}
|
||||
|
||||
resetForm()
|
||||
show.value = false
|
||||
}).catch((e) => {
|
||||
ElNotification({
|
||||
title: '发送失败',
|
||||
@@ -241,6 +262,7 @@ async function sendEmail() {
|
||||
message: h('span', { style: 'color: teal' }, e.message),
|
||||
position: 'bottom-right'
|
||||
})
|
||||
show.value = true
|
||||
}).finally(() => {
|
||||
percentMessage.close()
|
||||
percent.value = 0
|
||||
@@ -255,8 +277,13 @@ function resetForm() {
|
||||
form.content = ''
|
||||
form.manyType = null
|
||||
form.attachments = []
|
||||
form.sendType = null
|
||||
form.sendType = ''
|
||||
form.emailId = 0
|
||||
form.draftId = null
|
||||
backReply.content = ''
|
||||
backReply.subject = ''
|
||||
backReply.receiveEmail = []
|
||||
backReply.sendType = ''
|
||||
editor.value.clearEditor()
|
||||
}
|
||||
|
||||
@@ -291,7 +318,13 @@ function openReply(email) {
|
||||
</article>
|
||||
</blockquote>`
|
||||
open()
|
||||
console.log(defValue.value)
|
||||
|
||||
nextTick(() => {
|
||||
backReply.content = editor.value.getContent()
|
||||
backReply.subject = form.subject
|
||||
backReply.receiveEmail = form.receiveEmail
|
||||
backReply.sendType = form.sendType
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
@@ -299,7 +332,6 @@ function openReply(email) {
|
||||
function formatImage(content) {
|
||||
content = content || '';
|
||||
const domain = settingStore.settings.r2Domain;
|
||||
console.log(content)
|
||||
return content.replace(/{{domain}}/g, domain + '/');
|
||||
}
|
||||
|
||||
@@ -317,6 +349,14 @@ function open() {
|
||||
editor.value.focus()
|
||||
}
|
||||
|
||||
function openDraft(draft) {
|
||||
Object.assign(form,{...draft})
|
||||
defValue.value = ''
|
||||
setTimeout(() => defValue.value = form.content)
|
||||
show.value = true;
|
||||
editor.value.focus()
|
||||
}
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
close()
|
||||
@@ -332,7 +372,52 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
function close() {
|
||||
show.value = false;
|
||||
|
||||
if (form.draftId) {
|
||||
draftStore.setDraft = {...toRaw(form)}
|
||||
show.value = false
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(form.content || form.subject || form.receiveEmail.length > 0)) {
|
||||
show.value = false
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
|
||||
if (backReply.sendType === 'reply') {
|
||||
let subjectFlag = form.subject === backReply.subject
|
||||
let contentFlag = editor.value.getContent() === backReply.content
|
||||
let receiveFlag = form.receiveEmail.length === 1 && form.receiveEmail[0] === backReply.receiveEmail[0]
|
||||
if (subjectFlag && contentFlag && receiveFlag) {
|
||||
resetForm();
|
||||
close()
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ElMessageBox.confirm('是否保存草稿?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
distinguishCancelAndClose: true
|
||||
}).then( async () => {
|
||||
const formData = {...toRaw(form)}
|
||||
delete formData.draftId
|
||||
delete formData.attachments
|
||||
formData.createTime = dayjs().utc().format('YYYY-MM-DD HH:mm:ss');
|
||||
const draftId = await db.value.draft.add({...formData})
|
||||
db.value.att.add({draftId,attachments: toRaw(form.attachments)})
|
||||
draftStore.refreshList ++
|
||||
show.value = false
|
||||
}).catch((action) => {
|
||||
if (action === 'cancel') {
|
||||
show.value = false
|
||||
resetForm()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
21
mail-vue/src/request/reg-key.js
Normal file
21
mail-vue/src/request/reg-key.js
Normal 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}})
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import http from '@/axios/index.js';
|
||||
|
||||
export function sysEmailAll(params) {
|
||||
return http.get('/sys-email/list', {params: {...params}})
|
||||
export function sysEmailList(params) {
|
||||
return http.get('/sysEmail/list', {params: {...params}})
|
||||
}
|
||||
|
||||
export function sysEmailDelete(emailIds) {
|
||||
return http.delete('/sys-email/delete?emailIds=' + emailIds)
|
||||
return http.delete('/sysEmail/delete?emailIds=' + emailIds)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,16 @@ const routes = [
|
||||
menu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/draft',
|
||||
name: 'draft',
|
||||
component: () => import('@/views/draft/index.vue'),
|
||||
meta: {
|
||||
title: '草稿箱',
|
||||
name: 'draft',
|
||||
menu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/content',
|
||||
name: 'content',
|
||||
|
||||
8
mail-vue/src/store/draft.js
Normal file
8
mail-vue/src/store/draft.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const userDraftStore = defineStore('draft', {
|
||||
state: () => ({
|
||||
refreshList: 0,
|
||||
setDraft: {},
|
||||
})
|
||||
})
|
||||
@@ -40,4 +40,8 @@ export function formatDetailDate(time) {
|
||||
|
||||
export function tzDayjs(time) {
|
||||
return dayjs.utc(time).tz('Asia/Shanghai')
|
||||
}
|
||||
|
||||
export function toUtc() {
|
||||
return dayjs().utc()
|
||||
}
|
||||
@@ -47,6 +47,16 @@ const routers = {
|
||||
menu: true
|
||||
}
|
||||
},
|
||||
'reg-key:query': {
|
||||
path: '/sys/reg-key',
|
||||
name: 'reg-key',
|
||||
component: () => import('@/views/reg-key/index.vue'),
|
||||
meta: {
|
||||
title: '注册密钥',
|
||||
name: 'reg-key',
|
||||
menu: true
|
||||
}
|
||||
},
|
||||
'sys-email:query': {
|
||||
path: '/sys/email',
|
||||
name: 'sys-email',
|
||||
|
||||
9
mail-vue/src/utils/text.js
Normal file
9
mail-vue/src/utils/text.js
Normal 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;
|
||||
}
|
||||
@@ -680,7 +680,7 @@ function createSendGauge() {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
gap: 20px;
|
||||
@media (max-width: 1024px) {
|
||||
@media (max-width: 1199px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
@@ -691,7 +691,7 @@ function createSendGauge() {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
padding: 25px 20px;
|
||||
padding: 21px 20px;
|
||||
.top {
|
||||
display: grid;
|
||||
justify-content: space-between;
|
||||
|
||||
104
mail-vue/src/views/draft/index.vue
Normal file
104
mail-vue/src/views/draft/index.vue
Normal 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>
|
||||
@@ -69,6 +69,8 @@
|
||||
</el-input>
|
||||
<el-input v-model="registerForm.password" placeholder="密码" type="password" autocomplete="off" />
|
||||
<el-input v-model="registerForm.confirmPassword" placeholder="确认密码" type="password" autocomplete="off" />
|
||||
<el-input v-if="settingStore.settings.regKey === 0" v-model="registerForm.code" placeholder="注册码" type="text" autocomplete="off" />
|
||||
<el-input v-if="settingStore.settings.regKey === 2" v-model="registerForm.code" placeholder="注册码(可选)" type="text" autocomplete="off" />
|
||||
<div v-show="verifyShow"
|
||||
class="register-turnstile"
|
||||
:data-sitekey="settingStore.settings.siteKey"
|
||||
@@ -116,7 +118,8 @@ const suffix = ref('')
|
||||
const registerForm = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
confirmPassword: '',
|
||||
code: null
|
||||
})
|
||||
const domainList = settingStore.domainList;
|
||||
const registerLoading = ref(false)
|
||||
@@ -248,6 +251,20 @@ function submitRegister() {
|
||||
return
|
||||
}
|
||||
|
||||
if(settingStore.settings.regKey === 0) {
|
||||
|
||||
if (!registerForm.code) {
|
||||
|
||||
ElMessage({
|
||||
message: '注册码不能为空',
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!verifyToken && settingStore.settings.registerVerify === 0) {
|
||||
verifyShow.value = true
|
||||
if (!turnstileId) {
|
||||
@@ -263,11 +280,20 @@ function submitRegister() {
|
||||
}
|
||||
|
||||
registerLoading.value = true
|
||||
register({email: registerForm.email + suffix.value, password: registerForm.password, token: verifyToken}).then(() => {
|
||||
|
||||
const form = {
|
||||
email: registerForm.email + suffix.value,
|
||||
password: registerForm.password,
|
||||
token: verifyToken,
|
||||
code: registerForm.code
|
||||
}
|
||||
|
||||
register(form).then(() => {
|
||||
show.value = 'login'
|
||||
registerForm.email = ''
|
||||
registerForm.password = ''
|
||||
registerForm.confirmPassword = ''
|
||||
registerForm.code = ''
|
||||
registerLoading.value = false
|
||||
turnstileId = null
|
||||
verifyToken = ''
|
||||
|
||||
528
mail-vue/src/views/reg-key/index.vue
Normal file
528
mail-vue/src/views/reg-key/index.vue
Normal 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>
|
||||
@@ -47,10 +47,15 @@
|
||||
</el-table>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<el-dialog class="dialog" v-model="roleFormShow" :title="dialogType.title" @closed="resetForm">
|
||||
<el-dialog top="5vh" class="dialog" v-model="roleFormShow" :title="dialogType.title" @closed="resetForm">
|
||||
<div class="dialog-box">
|
||||
<el-input class="dialog-input" v-model="form.name" type="text" :maxlength="12" placeholder="身份名称" autocomplete="off" />
|
||||
<el-input class="dialog-input" v-model="form.description" :maxlength="30" type="text" placeholder="描述" autocomplete="off" />
|
||||
<el-input-tag class="dialog-input-tag" tag-type="warning" :class="form.banEmail.length === 0 ? 'dialog-input' : '' " v-model="form.banEmail" @add-tag="banEmailAddTag" type="text" placeholder="输入邮箱拦截收件, 拦截所有前缀 *@example.com" autocomplete="off" />
|
||||
<el-radio-group class="dialog-radio" v-model="form.banEmailType" v-if="form.banEmail.length > 0">
|
||||
<el-radio label="丢弃邮件" :value="0" />
|
||||
<el-radio label="移除正文" :value="1" />
|
||||
</el-radio-group>
|
||||
<div class="dialog-input">
|
||||
<el-input-number placeholder="排序" :min="0" :max="9999" v-model.number="form.sort" controls-position="right" autocomplete="off" />
|
||||
</div>
|
||||
@@ -74,15 +79,18 @@
|
||||
<div>
|
||||
<span>{{node.label}}</span>
|
||||
<span class="send-num" v-if="data.permKey === 'email:send'" @click.stop>
|
||||
<el-input-number v-model="form.sendCount" controls-position="right" :min="0" :max="99999" size="small" placeholder="数量" >
|
||||
<el-input-number v-model="form.sendCount" controls-position="right" :max="99999" size="small" placeholder="数量" >
|
||||
</el-input-number>
|
||||
<el-select v-model="form.sendType" placeholder="Select" size="small" style="width: 60px;margin-left: 5px;">
|
||||
<el-option label="总数" value="count" />
|
||||
<el-option label="每天" value="day" />
|
||||
</el-select>
|
||||
<el-tooltip effect="dark" content="零无限制 负数无次数">
|
||||
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span class="send-num" v-if="data.permKey === 'account:add'" @click.stop>
|
||||
<el-input-number v-model="form.accountCount" controls-position="right" :min="0" :max="99999" size="small" placeholder="数量" >
|
||||
<el-input-number v-model="form.accountCount" controls-position="right" :min="0" :max="99999" size="small" placeholder="数量" >
|
||||
</el-input-number>
|
||||
</span>
|
||||
</div>
|
||||
@@ -102,6 +110,7 @@ import {roleAdd, roleDelete, rolePermTree, roleRoleList, roleSet, roleSetDef} fr
|
||||
import loading from '@/components/loading/index.vue';
|
||||
import {useRoleStore} from "@/store/role.js";
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
import {isEmail} from "@/utils/verify-utils.js";
|
||||
|
||||
defineOptions({
|
||||
name: 'role'
|
||||
@@ -128,9 +137,11 @@ const dialogType = reactive({
|
||||
const form = reactive({
|
||||
name: null,
|
||||
description: null,
|
||||
banEmail: [],
|
||||
banEmailType: 0,
|
||||
sendType: 'count',
|
||||
sendCount: '',
|
||||
accountCount: '',
|
||||
sendCount: 0,
|
||||
accountCount: 0,
|
||||
sort: 0,
|
||||
isDefault: 0,
|
||||
})
|
||||
@@ -145,6 +156,21 @@ rolePermTree().then(tree => {
|
||||
treeList.push(...tree)
|
||||
})
|
||||
|
||||
function banEmailAddTag(val) {
|
||||
const emails = Array.from(new Set(
|
||||
val.split(/[,,]/).map(item => item.trim()).filter(item => item)
|
||||
));
|
||||
|
||||
form.banEmail.splice(form.banEmail.length - 1, 1)
|
||||
|
||||
emails.forEach(email => {
|
||||
if (isEmail(email) && !form.banEmail.includes(email)) {
|
||||
form.banEmail.push(email)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function roleFormClick() {
|
||||
if (dialogType.type === 'add') {
|
||||
addRole()
|
||||
@@ -240,8 +266,10 @@ function resetForm() {
|
||||
form.description = null
|
||||
form.sort = 0
|
||||
form.sendType = 'count'
|
||||
form.sendCount = ''
|
||||
form.accountCount = ''
|
||||
form.sendCount = 0
|
||||
form.accountCount = 0
|
||||
form.banEmail = []
|
||||
form.banEmailType = 0
|
||||
tree.value.setCheckedKeys([])
|
||||
}
|
||||
|
||||
@@ -256,6 +284,7 @@ function openRoleSet(role) {
|
||||
form.sendType = role.sendType
|
||||
form.sendCount = role.sendCount
|
||||
form.accountCount = role.accountCount
|
||||
form.banEmail = role.banEmail
|
||||
nextTick(() => {
|
||||
tree.value.setCheckedKeys(role.permIds)
|
||||
})
|
||||
@@ -346,7 +375,7 @@ window.onresize = () => {
|
||||
padding: 9px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
gap: 18px;
|
||||
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
|
||||
font-size: 18px;
|
||||
.search {
|
||||
@@ -362,6 +391,14 @@ window.onresize = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
position: relative;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
color: gray;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.description) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -397,6 +434,12 @@ window.onresize = () => {
|
||||
.dialog-input {
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
.dialog-radio {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.dialog-input-tag {
|
||||
}
|
||||
}
|
||||
|
||||
.perm-expand {
|
||||
@@ -406,11 +449,11 @@ window.onresize = () => {
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-dialog) {
|
||||
margin-top: 15vh !important;
|
||||
margin-bottom: 20px !important;
|
||||
width: 400px !important;
|
||||
@media (max-width: 440px) {
|
||||
width: 460px !important;
|
||||
@media (max-width: 500px) {
|
||||
width: calc(100% - 40px) !important;
|
||||
margin-right: 20px !important;
|
||||
margin-left: 20px !important;
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
:show-star="false"
|
||||
show-user-info
|
||||
show-status
|
||||
actionLeft="4px"
|
||||
:show-account-icon="false"
|
||||
@jump="jumpContent"
|
||||
@refresh-before="refreshBefore"
|
||||
:type="'sys-email'"
|
||||
|
||||
>
|
||||
<template #first>
|
||||
@@ -23,7 +25,7 @@
|
||||
<div @click.stop="openSelect">
|
||||
<el-select
|
||||
ref="mySelect"
|
||||
v-model="searchType"
|
||||
v-model="params.searchType"
|
||||
placeholder="请选择"
|
||||
class="select"
|
||||
>
|
||||
@@ -59,10 +61,10 @@
|
||||
<script setup>
|
||||
import {starAdd, starCancel} from "@/request/star.js";
|
||||
import emailScroll from "@/components/email-scroll/index.vue"
|
||||
import {computed, defineOptions, reactive, ref} from "vue";
|
||||
import {computed, defineOptions, reactive, ref, watch} from "vue";
|
||||
import {useEmailStore} from "@/store/email.js";
|
||||
import {
|
||||
sysEmailAll,
|
||||
sysEmailList,
|
||||
sysEmailDelete
|
||||
} from "@/request/sys-email.js";
|
||||
import {Icon} from "@iconify/vue";
|
||||
@@ -74,7 +76,6 @@ defineOptions({
|
||||
|
||||
const emailStore = useEmailStore();
|
||||
const sysEmailScroll = ref({})
|
||||
const searchType = ref('name')
|
||||
const searchValue = ref('')
|
||||
const mySelect = ref()
|
||||
|
||||
@@ -88,15 +89,31 @@ const params = reactive({
|
||||
userEmail: null,
|
||||
accountEmail: null,
|
||||
name: null,
|
||||
subject: null
|
||||
subject: null,
|
||||
searchType: 'name'
|
||||
})
|
||||
|
||||
|
||||
const selectTitle = computed(() => {
|
||||
if (searchType.value === 'user') return '用户'
|
||||
if (searchType.value === 'account') return '邮箱'
|
||||
if (searchType.value === 'name') return '发件人'
|
||||
if (searchType.value === 'subject') return '主题'
|
||||
if (params.searchType === 'user') return '用户'
|
||||
if (params.searchType === 'account') return '邮箱'
|
||||
if (params.searchType === 'name') return '发件人'
|
||||
if (params.searchType === 'subject') return '主题'
|
||||
})
|
||||
|
||||
const paramsStar = localStorage.getItem('sys-email-params')
|
||||
if (paramsStar) {
|
||||
const locaParams = JSON.parse(paramsStar)
|
||||
params.type = locaParams.type
|
||||
params.timeSort = locaParams.timeSort
|
||||
params.status = locaParams.status
|
||||
params.searchType = locaParams.searchType
|
||||
}
|
||||
|
||||
watch(() => params, () => {
|
||||
localStorage.setItem('sys-email-params',JSON.stringify(params))
|
||||
}, {
|
||||
deep: true
|
||||
})
|
||||
|
||||
function refreshBefore() {
|
||||
@@ -107,7 +124,7 @@ function refreshBefore() {
|
||||
params.accountEmail = null
|
||||
params.name = null
|
||||
params.subject = null
|
||||
|
||||
params.searchType = 'name'
|
||||
}
|
||||
|
||||
function search() {
|
||||
@@ -117,19 +134,19 @@ function search() {
|
||||
params.name = null
|
||||
params.subject = null
|
||||
|
||||
if (searchType.value === 'user') {
|
||||
if (params.searchType === 'user') {
|
||||
params.userEmail = searchValue.value
|
||||
}
|
||||
|
||||
if (searchType.value === 'account') {
|
||||
if (params.searchType === 'account') {
|
||||
params.accountEmail = searchValue.value
|
||||
}
|
||||
|
||||
if (searchType.value === 'name') {
|
||||
if (params.searchType === 'name') {
|
||||
params.name = searchValue.value
|
||||
}
|
||||
|
||||
if (searchType.value === 'subject') {
|
||||
if (params.searchType === 'subject') {
|
||||
params.subject = searchValue.value
|
||||
}
|
||||
|
||||
@@ -151,7 +168,7 @@ function jumpContent(email) {
|
||||
|
||||
|
||||
function getEmailList(emailId, size) {
|
||||
return sysEmailAll({emailId, size, ...params})
|
||||
return sysEmailList({emailId, size, ...params})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -17,6 +17,24 @@
|
||||
v-model="setting.register"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>注册码</span></div>
|
||||
<div>
|
||||
<el-select
|
||||
@change="change"
|
||||
style="width: 80px;"
|
||||
v-model="setting.regKey"
|
||||
placeholder="Select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in regKeyOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>添加邮箱</span></div>
|
||||
<div>
|
||||
@@ -36,29 +54,6 @@
|
||||
v-model="setting.manyEmail"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<span>轮询刷新</span>
|
||||
<el-tooltip effect="dark" content="轮询请求服务器获取最新邮件">
|
||||
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-select
|
||||
@change="change"
|
||||
style="width: 80px;"
|
||||
v-model="setting.autoRefreshTime"
|
||||
placeholder="Select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<span>物理清空数据</span>
|
||||
@@ -135,6 +130,29 @@
|
||||
v-model="setting.receive"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<span>轮询刷新</span>
|
||||
<el-tooltip effect="dark" content="轮询请求服务器获取最新邮件">
|
||||
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-select
|
||||
@change="change"
|
||||
style="width: 80px;"
|
||||
v-model="setting.autoRefreshTime"
|
||||
placeholder="Select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>邮件发送</span></div>
|
||||
<div>
|
||||
@@ -145,15 +163,14 @@
|
||||
<div class="setting-item">
|
||||
<div><span>添加 Resend Token</span></div>
|
||||
<div>
|
||||
<el-button class="opt-button" style="margin-top: 0" @click="openResendList" size="small" type="primary">
|
||||
<Icon icon="ic:round-list" width="18" height="18"/>
|
||||
</el-button>
|
||||
<el-button class="opt-button" style="margin-top: 0" @click="openResendForm" size="small" type="primary">
|
||||
<Icon icon="material-symbols:add-rounded" width="16" height="16"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item token-item" v-for="(value, key, index) in setting.resendTokens" :key="index">
|
||||
<div><span>{{ key }}</span></div>
|
||||
<div><span>{{ value }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -227,7 +244,7 @@
|
||||
<div class="setting-item">
|
||||
<div><span>Site Key</span></div>
|
||||
<div class="bot-verify">
|
||||
<span>{{ setting.siteKey || '空' }}</span>
|
||||
<span>{{ setting.siteKey }}</span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true">
|
||||
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
|
||||
</el-button>
|
||||
@@ -236,7 +253,7 @@
|
||||
<div class="setting-item">
|
||||
<div><span>Secret Key</span></div>
|
||||
<div class="bot-verify">
|
||||
<span> {{ setting.secretKey || '空' }} </span>
|
||||
<span> {{ setting.secretKey }} </span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true">
|
||||
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
|
||||
</el-button>
|
||||
@@ -279,7 +296,7 @@
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveTitle">保存</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="resendTokenFormShow" title="添加resend token" width="340" @closed="cleanResendTokenForm">
|
||||
<el-dialog v-model="resendTokenFormShow" title="添加 Resend Token" width="340" @closed="cleanResendTokenForm">
|
||||
<form>
|
||||
<el-select style="margin-bottom: 15px" v-model="resendTokenForm.domain" placeholder="Select">
|
||||
<el-option
|
||||
@@ -289,7 +306,7 @@
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input type="text" placeholder="令牌" v-model="resendTokenForm.token"/>
|
||||
<el-input type="text" placeholder="输入内容添加,不填则删除" v-model="resendTokenForm.token"/>
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveResendToken">保存</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
@@ -299,7 +316,7 @@
|
||||
<el-button type="primary" :loading="settingLoading" @click="saveR2domain">保存</el-button>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="turnstileShow" title="添加Turnstile密钥" width="340"
|
||||
<el-dialog v-model="turnstileShow" title="添加 Turnstile 密钥" width="340"
|
||||
@closed="turnstileForm.secretKey = '';turnstileForm.siteKey = ''">
|
||||
<form>
|
||||
<el-input type="text" placeholder="siteKey" v-model="turnstileForm.siteKey"/>
|
||||
@@ -368,7 +385,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="forward-set-body">
|
||||
<el-input-tag tag-type="warning" placeholder="多邮个箱用, 分开 example1.com,example2.com" v-model="forwardEmail" @add-tag="emailAddTag"></el-input-tag>
|
||||
<el-input-tag tag-type="warning" placeholder="多个邮箱用, 分开 example1.com,example2.com" v-model="forwardEmail" @add-tag="emailAddTag"></el-input-tag>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
@@ -392,7 +409,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="forward-set-body">
|
||||
<el-input-tag placeholder="多邮个箱用, 分开 example1.com,example2.com" tag-type="success" v-model="ruleEmail" @add-tag="ruleEmailAddTag" />
|
||||
<el-input-tag placeholder="多个邮箱用, 分开 example1.com,example2.com" tag-type="success" v-model="ruleEmail" @add-tag="ruleEmailAddTag" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
@@ -406,12 +423,18 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog class="resend-table" v-model="showResendList" title="Token 列表">
|
||||
<el-table :data="resendList" >
|
||||
<el-table-column :min-width="emailColumnWidth" property="key" label="域名" :show-overflow-tooltip="true" />
|
||||
<el-table-column :width="tokenColumnWidth" property="value" label="token" fixed="right" :show-overflow-tooltip="true" />
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineOptions, onMounted, reactive, ref} from "vue";
|
||||
import {computed, defineOptions, reactive, ref} from "vue";
|
||||
import {physicsDeleteAll, setBackground, settingQuery, settingSet} from "@/request/setting.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
@@ -419,9 +442,10 @@ import {useAccountStore} from "@/store/account.js";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {cvtR2Url} from "@/utils/convert.js";
|
||||
import {storeToRefs} from "pinia";
|
||||
import { debounce } from 'lodash-es'
|
||||
import {debounce} from 'lodash-es'
|
||||
import {isEmail} from "@/utils/verify-utils.js";
|
||||
import loading from "@/components/loading/index.vue";
|
||||
import {getTextWidth} from "@/utils/text.js";
|
||||
|
||||
defineOptions({
|
||||
name: 'sys-setting'
|
||||
@@ -440,6 +464,7 @@ const turnstileShow = ref(false)
|
||||
const tgSettingShow = ref(false)
|
||||
const thirdEmailShow = ref(false)
|
||||
const forwardRulesShow = ref(false)
|
||||
const showResendList = ref(false)
|
||||
const settingStore = useSettingStore();
|
||||
const {settings: setting} = storeToRefs(settingStore);
|
||||
const editTitle = ref('')
|
||||
@@ -455,6 +480,13 @@ const turnstileForm = reactive({
|
||||
siteKey: '',
|
||||
secretKey: ''
|
||||
})
|
||||
|
||||
const regKeyOptions = [
|
||||
{label: '开启', value: 0},
|
||||
{label: '关闭', value: 1},
|
||||
{label: '可选', value: 2},
|
||||
]
|
||||
|
||||
const options = [
|
||||
{label: '关闭', value: 0},
|
||||
{label: '3s', value: 3},
|
||||
@@ -468,12 +500,40 @@ const options = [
|
||||
const tgChatId = ref([])
|
||||
const tgBotStatus = ref(0)
|
||||
const tgBotToken = ref('')
|
||||
|
||||
const forwardEmail = ref([])
|
||||
const forwardStatus = ref(0)
|
||||
const emailColumnWidth = ref(0)
|
||||
const tokenColumnWidth = ref(0)
|
||||
|
||||
const ruleType = ref(0)
|
||||
const ruleEmail = ref([])
|
||||
const resendList = computed(() => {
|
||||
|
||||
let list = Object.keys(setting.value.resendTokens).map(key => {
|
||||
return {
|
||||
key: key,
|
||||
value: setting.value.resendTokens[key]
|
||||
};
|
||||
})
|
||||
|
||||
if (list.length > 0) {
|
||||
|
||||
const key = list.reduce((a, b) =>
|
||||
a.key.length > b.key.length ? a : b
|
||||
).key;
|
||||
|
||||
emailColumnWidth.value = getTextWidth(key) + 30
|
||||
|
||||
const value = list.reduce((a, b) =>
|
||||
a.value.length > b.value.length ? a : b
|
||||
).value;
|
||||
|
||||
tokenColumnWidth.value = getTextWidth(value) + 30
|
||||
|
||||
}
|
||||
|
||||
return list;
|
||||
});
|
||||
|
||||
settingQuery().then(settingData => {
|
||||
setting.value = settingData
|
||||
@@ -493,6 +553,10 @@ function openTgSetting() {
|
||||
tgSettingShow.value = true
|
||||
}
|
||||
|
||||
function openResendList() {
|
||||
showResendList.value = true
|
||||
}
|
||||
|
||||
function openThirdEmailSetting() {
|
||||
forwardEmail.value = []
|
||||
forwardStatus.value = setting.value.forwardStatus
|
||||
@@ -898,6 +962,24 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.resend-table.el-dialog) {
|
||||
min-height: 300px;
|
||||
width: 500px !important;
|
||||
@media (max-width: 540px) {
|
||||
width: calc(100% - 40px) !important;
|
||||
margin-right: 20px !important;
|
||||
margin-left: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.resend-table .el-dialog__header) {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
:deep(.el-table__inner-wrapper:before) {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
:deep(.cut-dialog.el-dialog) {
|
||||
width: fit-content !important;
|
||||
height: fit-content !important;
|
||||
|
||||
@@ -307,6 +307,20 @@ roleSelectUse().then(list => {
|
||||
roleList.push(...list)
|
||||
})
|
||||
|
||||
const paramsStar = localStorage.getItem('user-params')
|
||||
if (paramsStar) {
|
||||
const locaParams = JSON.parse(paramsStar)
|
||||
params.num = locaParams.num
|
||||
params.size = locaParams.size
|
||||
params.timeSort = locaParams.timeSort
|
||||
params.status = locaParams.status
|
||||
}
|
||||
|
||||
watch(() => params, () => {
|
||||
localStorage.setItem('user-params',JSON.stringify(params))
|
||||
}, {
|
||||
deep: true
|
||||
})
|
||||
|
||||
watch(() => roleStore.refresh, () => {
|
||||
roleSelectUse().then(list => {
|
||||
|
||||
Reference in New Issue
Block a user