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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
<div :class="accountShow && hasPerm('account:query') ? 'block-show' : 'block-hide'" @click="uiStore.accountShow = false"></div>
<account :class="accountShow && hasPerm('account:query') ? 'show' : 'hide'" />
<router-view class="main-view" v-slot="{ Component,route }">
<keep-alive :include="['email','sys-email','send','sys-setting','star','user','role','analysis']">
<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>

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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 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 = ''

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

View File

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

View File

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

View File

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