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

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

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