新增邮件发送和管理员功能

This commit is contained in:
eoao
2025-05-29 17:38:50 +08:00
parent de742d31e7
commit 2341d14b2d
364 changed files with 24202 additions and 1079 deletions

View File

@@ -0,0 +1,21 @@
<script setup>
import router from "@/router/index.js";
</script>
<template>
<div class="box-404">
<el-empty class="empty" description="404错误, 找不到页面" >
<el-button type="primary" @click="router.replace({name: 'layout'})">返回首页</el-button>
</el-empty>
</div>
</template>
<style scoped lang="scss">
.box-404 {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
</style>

View File

@@ -2,9 +2,11 @@
<div class="box">
<div class="header-actions">
<Icon class="icon" icon="material-symbols-light:arrow-back-ios-new" width="20" height="20" @click="handleBack"/>
<Icon class="icon" icon="uiw:delete" width="16" height="16" @click="handleDelete"/>
<Icon class="icon" @click="changeStar" v-if="email.isStar" icon="fluent-color:star-16" width="20" height="20"/>
<Icon class="icon" @click="changeStar" v-else icon="solar:star-line-duotone" width="18" height="18"/>
<Icon v-perm="'email:delete'" class="icon" icon="uiw:delete" width="16" height="16" @click="handleDelete"/>
<template v-if="emailStore.contentData.showStar">
<Icon class="icon" @click="changeStar" v-if="email.isStar" icon="fluent-color:star-16" width="20" height="20"/>
<Icon class="icon" @click="changeStar" v-else icon="solar:star-line-duotone" width="18" height="18"/>
</template>
</div>
<div></div>
<el-scrollbar class="scrollbar">
@@ -26,95 +28,112 @@
<div>{{ formatDetailDate(email.createTime) }}</div>
</div>
</div>
<div class="icon">
</div>
<el-alert v-if="email.status === 3" :closable="false" :title="'发送失败: ' + toMessage(email.message)" class="email-msg" type="error" show-icon />
<el-alert v-if="email.status === 4" :closable="false" title="被标记为垃圾邮件" class="email-msg" type="warning" show-icon />
<el-alert v-if="email.status === 5" :closable="false" title="邮件发送被延迟" class="email-msg" type="warning" show-icon />
</div>
<el-scrollbar class="htm-scrollbar">
<div v-html="email.content"/>
<el-scrollbar class="htm-scrollbar" :class="email.attList.length === 0 ? 'bottom-distance' : ''">
<ShadowHtml :html="formatImage(email)" />
</el-scrollbar>
<div class="att" v-if="atts.length > 0">
<div class="att-title">附件列表</div>
<div class="att-item" v-for="att in atts" :key="att.attId">
<img v-if="isImage(att.filename)" class="att-image" :src="cvtR2Url(att.key)"/>
<div class="att-icon" v-else>
<Icon :icon="toIcon(att.filename)" width="20" height="20"/>
</div>
<div class="att-name">
{{ att.filename }}
</div>
<div>{{ formatBytes(att.size) }}</div>
<div class="down-icon att-icon">
<a style="color: #333" :href="cvtR2Url(att.key)" download>
<Icon icon="system-uicons:push-down" width="22" height="22"/>
</a>
<div class="att" v-if="email.attList.length > 0">
<div class="att-title">
<span>附件列表</span>
<span> {{email.attList.length}} </span>
</div>
<div class="att-box">
<div class="att-item" v-for="att in email.attList" :key="att.attId">
<div class="att-icon" @click="showImage(att.key)">
<Icon :icon="getIconByName(att.filename)" width="20" height="20"/>
</div>
<el-tooltip
effect="dark"
:content="att.filename"
>
<div class="att-name" @click="showImage(att.key)">
{{ att.filename }}
</div>
</el-tooltip>
<div style="color: rgba(24, 36, 48, 0.6);">{{ formatBytes(att.size) }}</div>
<div class="opt-icon att-icon">
<Icon v-if="isImage(att.filename)" icon="hugeicons:view" width="22" height="22" @click="showImage(att.key)"/>
<a :href="cvtR2Url(att.key)" download>
<Icon icon="system-uicons:push-down" width="22" height="22"/>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</el-scrollbar>
<el-image-viewer
v-if="showPreview"
:url-list="srcList"
show-progress
@close="showPreview = false"
/>
</div>
</template>
<script setup>
import {reactive, watch} from "vue";
import ShadowHtml from '@/components/shadow-html/index.vue'
import {reactive, ref, watch} from "vue";
import {useRouter} from 'vue-router'
import {ElMessage, ElMessageBox} from 'element-plus'
import {attList, emailDelete} from "@/request/email.js";
import {emailDelete} from "@/request/email.js";
import {Icon} from "@iconify/vue";
import {useEmailStore} from "@/store/email.js";
import {useAccountStore} from "@/store/account.js";
import day from "@/day/day.js";
import {formatDetailDate} from "@/utils/day.js";
import {starAdd, starCancel} from "@/request/star.js";
import {getExtName, formatBytes} from "@/utils/file-utils.js";
import {cvtR2Url} from "@/utils/convert-utils.js";
import {cvtR2Url} from "@/utils/convert.js";
import {getIconByName} from "@/utils/icon-utils.js";
import {useSettingStore} from "@/store/setting.js";
import {sysEmailDelete} from "@/request/sys-email.js";
const settingStore = useSettingStore();
const accountStore = useAccountStore();
const emailStore = useEmailStore();
const router = useRouter()
const email = emailStore.readEmail
const atts = reactive([])
attList(email.emailId).then(list => {
atts.push(...list)
})
const email = emailStore.contentData.email
const showPreview = ref(false)
const srcList = reactive([])
watch(() => accountStore.currentAccountId, () => {
handleBack()
})
function toIcon(filename) {
const extName = getExtName(filename)
if (['zip', 'rar', '7z', 'tar', 'tgz'].includes(extName)) return 'hugeicons:file-zip';
if (['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv'].includes(extName)) return 'fluent:video-clip-24-regular';
if (['txt', 'doc', 'docx', 'md'].includes(extName)) return 'hugeicons:google-doc'
if (['xls', 'csv', 'xlsx'].includes(extName)) return 'codicon:table';
if (['mp3', 'wav', 'aac', 'ogg', 'flac', 'm4a'].includes(extName)) return 'mynaui:music';
if (['.ppt', 'pptx', 'pps', 'potx', 'pot'].includes(extName)) return 'lsicon:file-ppt-filled'
if (extName === 'pdf') return 'hugeicons:pdf-02';
if (extName === 'apk') return 'proicons:android';
if (extName === 'exe') return 'bi:filetype-exe';
return "hugeicons:attachment-01"
function toMessage(message) {
return message ? JSON.parse(message).message : '';
}
function formatImage(email) {
let content = email.content || email.text || ''
const domain = settingStore.settings.r2Domain;
return content.replace('{{domain}}', domain + '/');
}
function showImage(key) {
if (!isImage(key)) return;
const url = cvtR2Url(key)
srcList.length = 0
srcList.push(url)
showPreview.value = true
}
function isImage(filename) {
return ['png', 'jpg', 'jpeg', 'bmp', 'gif'].includes(getExtName(filename))
}
const formatDetailDate = (time) => {
return day(time).format('YYYY年M月D日 ddd AH:mm')
}
function changeStar() {
if (email.isStar) {
email.isStar = 0;
starCancel(email.emailId).then(() => {
email.isStar = 0;
emailStore.emailScroll?.editEmailStar(email.emailId, 0)
emailStore.starScroll?.deleteEmail([email.emailId])
}).catch((e) => {
console.error(e)
@@ -124,8 +143,7 @@ function changeStar() {
email.isStar = 1;
starAdd(email.emailId).then(() => {
email.isStar = 1;
emailStore.emailScroll?.editEmailStar(email.emailId, 1)
emailStore.starScroll?.addItemStar(email)
emailStore.starScroll?.addItem(email)
}).catch((e) => {
console.error(e)
email.isStar = 0;
@@ -143,14 +161,27 @@ const handleDelete = () => {
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
emailDelete(email.value.emailId).then(() => {
ElMessage({
message: '删除成功',
type: 'success',
plain: true,
if (emailStore.contentData.delType === 'logic') {
emailDelete(email.emailId).then(() => {
ElMessage({
message: '删除成功',
type: 'success',
plain: true,
})
emailStore.deleteIds = [email.emailId]
})
emailStore.deleteIds = [email.value.emailId]
})
} else {
sysEmailDelete(email.emailId).then(() => {
ElMessage({
message: '删除成功',
type: 'success',
plain: true,
})
emailStore.deleteIds = [email.emailId]
})
}
router.back()
})
}
@@ -165,7 +196,7 @@ const handleDelete = () => {
padding: 9px 15px;
display: flex;
align-items: center;
gap: 28px;
gap: 20px;
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
font-size: 18px;
@@ -193,8 +224,6 @@ const handleDelete = () => {
}
.htm-scrollbar {
padding-bottom: 15px;
word-break: break-all;
}
.content {
@@ -202,28 +231,41 @@ const handleDelete = () => {
flex-direction: column;
.att {
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #e7e9ec;
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 30px;
margin-bottom: 30px;
border: 1px solid #e4e7ed;
box-shadow: var(--el-box-shadow-light);
padding: 10px;
border-radius: 4px;
width: fit-content;
.att-box {
min-width: min(410px,calc(100vw - 53px));
display: grid;
gap: 10px;
grid-template-rows: 1fr;
}
.att-title {
font-weight: bold;
margin-bottom: 5px;
display: flex;
justify-content: space-between;
span:first-child {
font-weight: bold;
}
}
.att-item {
cursor: pointer;
div {
align-self: center;
}
padding: 10px;
padding: 5px 8px;
border-radius: 4px;
align-self: start;
border: 1px solid #e7e9ec;
display: grid;
grid-template-columns: auto 1fr auto 20px;
grid-template-columns: auto 1fr auto auto;
gap: 10px;
.att-icon {
@@ -231,7 +273,7 @@ const handleDelete = () => {
}
.att-name {
margin-right: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -244,29 +286,36 @@ const handleDelete = () => {
object-fit: contain;
}
.down-icon {
.opt-icon {
color: rgba(24, 36, 48, 0.6);
align-items: center;
display: flex;
gap: 8px;
cursor: pointer;
a {
color: rgba(24, 36, 48, 0.6);
align-items: center;
display: flex;
}
}
}
}
.email-info {
display: flex;
justify-content: space-between;
border-bottom: 1px solid #e7e9ec;
margin-bottom: 15px;
/*.date {
display: flex;
flex-wrap: wrap;
align-content: flex-start;
}*/
.date {
color: #585d69;
margin-bottom: 6px;
}
.email-msg {
max-width: 400px;
width: fit-content;
margin-bottom: 15px;
}
.send {
display: flex;
margin-bottom: 6px;
@@ -278,7 +327,7 @@ const handleDelete = () => {
}
.send-name-title {
padding-right: 10px;
padding-right: 5px;
}
}
@@ -305,4 +354,9 @@ const handleDelete = () => {
}
}
.bottom-distance {
margin-bottom: 30px;
}
</style>

View File

@@ -5,18 +5,33 @@
:getEmailList="getEmailList"
:emailDelete="emailDelete"
:star-add="starAdd"
:star-cancel="starCancel"/>
:star-cancel="starCancel"
:time-sort="params.timeSort"
actionLeft="4px"
@jump="jumpContent"
>
<template #first>
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-down-outline"
v-if="params.timeSort === 0" width="28" height="28"/>
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-up-outline" v-else
width="28" height="28"/>
</template>
</emailScroll>
</template>
<script setup>
import {useAccountStore} from "@/store/account.js";
import {useEmailStore} from "@/store/email.js";
import {useSettingStore} from "@/store/setting.js";
import {useUiStore} from "@/store/ui.js";
import emailScroll from "@/components/email-scroll/index.vue"
import {emailList, emailDelete, emailLatest} from "@/request/email.js";
import {starAdd, starCancel} from "@/request/star.js";
import {defineOptions, onMounted, ref, watch} from "vue";
import {defineOptions, onMounted, reactive, ref, watch} from "vue";
import {sleep} from "@/utils/time-utils.js";
import router from "@/router/index.js";
import {Icon} from "@iconify/vue";
defineOptions({
name: 'email'
@@ -26,6 +41,9 @@ const emailStore = useEmailStore();
const accountStore = useAccountStore();
const settingStore = useSettingStore();
const scroll = ref({})
const params = reactive({
timeSort: 0,
})
onMounted(() => {
emailStore.emailScroll = scroll;
@@ -37,16 +55,39 @@ watch(() => accountStore.currentAccountId, () => {
scroll.value.refreshList();
})
function changeTimeSort() {
params.timeSort = params.timeSort ? 0 : 1
scroll.value.refreshList();
}
function jumpContent(email) {
emailStore.contentData.email = email
emailStore.contentData.delType = 'logic'
emailStore.contentData.showStar = true
router.push('/content')
}
const existIds = new Set();
async function latest() {
while (true) {
const latestId = scroll.value.emailList[0]?.emailId ?? 0
const latestId = scroll.value.latestEmail?.emailId || 0
if (!scroll.value.firstLoad && settingStore.settings.autoRefreshTime) {
try {
const accountId = accountStore.currentAccountId
const list = await emailLatest(latestId,accountId)
if (accountId === accountStore.currentAccountId) {
scroll.value.emailList.unshift(...list)
const curTimeSort = params.timeSort
const list = await emailLatest(latestId, accountId)
if (accountId === accountStore.currentAccountId && params.timeSort === curTimeSort) {
if (list.length > 0) {
list.forEach(email => {
existIds.add(email.emailId)
scroll.value.addItem(email)
})
}
}
} catch (e) {
console.error(e)
@@ -57,7 +98,7 @@ async function latest() {
}
function addStar(email) {
emailStore.starScroll?.addItemStar(email)
emailStore.starScroll?.addItem(email)
}
function cancelStar(email) {
@@ -65,8 +106,12 @@ function cancelStar(email) {
}
function getEmailList(emailId, size) {
return emailList(accountStore.currentAccountId, emailId, size)
return emailList(accountStore.currentAccountId, emailId, params.timeSort, size, 0)
}
</script>
<style>
.icon {
cursor: pointer;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div id="box">
<div id="background-wrap">
<div id="login-box" :style="background">
<div id="background-wrap" v-if="!settingStore.settings.background">
<div class="x1 cloud"></div>
<div class="x2 cloud"></div>
<div class="x3 cloud"></div>
@@ -8,19 +8,18 @@
<div class="x5 cloud"></div>
</div>
<div class="form-wrapper">
<el-form autocomplete="off">
<div class="container" >
<div class="container">
<span class="form-title">{{ settingStore.settings.title }}</span>
<div class="custom-style" v-if="settingStore.settings.register === 0">
<el-segmented v-model="show" :options="options" />
<el-segmented v-model="show" :options="options"/>
</div>
<div v-if="show === 'login'">
<el-input v-model="form.email" type="text" placeholder="邮箱" autocomplete="off">
<template #prefix>
<Icon icon="weui:email-outlined" width="22" height="22" />
<Icon icon="weui:email-outlined" width="22" height="22"/>
</template>
<template #append>
<div @click.stop="openSelect">
<div @click.stop="openSelect">
<el-select
ref="mySelect"
v-model="suffix"
@@ -35,28 +34,28 @@
/>
</el-select>
<div style="color: #333">
<span >{{suffix}}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20" />
<span>{{ suffix }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
</div>
</template>
</el-input>
<el-input v-model="form.password" placeholder="密码" type="password" autocomplete="off">
<template #prefix>
<Icon icon="carbon:password" width="22" height="22" />
<Icon icon="carbon:password" width="22" height="22"/>
</template>
</el-input>
<el-button class="btn" type="primary" @click="submit" :loading="uiStore.loginLoading"
<el-button class="btn" type="primary" @click="submit" :loading="loginLoading"
>登录
</el-button>
</div>
<div v-else>
<el-input v-model="registerForm.email" type="text" placeholder="邮箱" autocomplete="off">
<template #prefix>
<Icon icon="weui:email-outlined" width="22" height="22" />
<Icon icon="weui:email-outlined" width="22" height="22"/>
</template>
<template #append>
<div @click.stop="openSelect">
<div @click.stop="openSelect">
<el-select
ref="mySelect"
v-model="suffix"
@@ -71,20 +70,20 @@
/>
</el-select>
<div style="color: #333">
<span>{{suffix}}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20" />
<span>{{ suffix }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
</div>
</template>
</el-input>
<el-input v-model="registerForm.password" placeholder="密码" type="password" autocomplete="off">
<template #prefix>
<Icon icon="carbon:password" width="22" height="22" />
<Icon icon="carbon:password" width="22" height="22"/>
</template>
</el-input>
<el-input v-model="registerForm.confirmPassword" placeholder="确认密码" type="password" autocomplete="off">
<template #prefix>
<Icon icon="carbon:password" width="22" height="22" />
<Icon icon="carbon:password" width="22" height="22"/>
</template>
</el-input>
<el-button class="btn" type="primary" @click="submitRegister" :loading="registerLoading"
@@ -92,7 +91,6 @@
</el-button>
</div>
</div>
</el-form>
</div>
<el-dialog
v-model="verifyShow"
@@ -111,24 +109,30 @@
<script setup>
import router from "@/router";
import {nextTick, reactive, ref} from "vue";
import {computed, nextTick, reactive, ref} from "vue";
import {login} from "@/request/login.js";
import {register} from "@/request/login.js";
import {ElMessage} from 'element-plus'
import {isEmail} from "@/utils/verify-utils.js";
import {useUiStore} from "@/store/ui.js";
import {useSettingStore} from "@/store/setting.js";
import {useAccountStore} from "@/store/account.js";
import {useUserStore} from "@/store/user.js";
import {Icon} from "@iconify/vue";
import {cvtR2Url} from "@/utils/convert.js";
import {loginUserInfo} from "@/request/my.js";
import {permsToRouter} from "@/utils/perm.js";
const accountStore = useAccountStore();
const userStore = useUserStore();
const settingStore = useSettingStore();
const uiStore = useUiStore()
const loginLoading = ref(false)
const show = ref('login')
const form = reactive({
email: '',
password: '',
});
const options = [{label: '登录', value: 'login'},{label: '注册', value: 'register'}];
const options = [{label: '登录', value: 'login'}, {label: '注册', value: 'register'}];
const mySelect = ref()
const suffix = ref('')
const registerForm = reactive({
@@ -136,18 +140,28 @@ const registerForm = reactive({
password: '',
confirmPassword: ''
})
const domainList = settingStore.domainList;
const domainList = settingStore.domainList;
const registerLoading = ref(false)
suffix.value = domainList[0]
const verifyShow = ref(false)
let verifyToken = ''
let turnstileId = ''
const background = computed(() => {
return settingStore.settings.background ? {
'background-image': `url(${cvtR2Url(settingStore.settings.background)})`,
'background-repeat': 'no-repeat',
'background-size': 'cover',
'background-position': 'center'
} : ''
})
window.onTurnstileSuccess = (token) => {
verifyToken = token;
setTimeout(() => {
verifyShow.value = false
},1500)
}, 1500)
};
const openSelect = () => {
@@ -184,21 +198,19 @@ const submit = () => {
return
}
if (form.password.length < 6) {
ElMessage({
message: '密码最少六位',
type: 'error',
plain: true,
})
return
}
uiStore.loginLoading = true
login(form.email+suffix.value, form.password).then(data => {
loginLoading.value = true
login(form.email + suffix.value, form.password).then(async data => {
localStorage.setItem('token', data.token)
router.replace({name: 'layout'})
}).catch(() => {
uiStore.loginLoading = false
const user = await loginUserInfo();
accountStore.currentAccountId = user.accountId;
userStore.user = user;
const routers = permsToRouter(user.permKeys);
routers.forEach(routerData => {
router.addRoute('layout', routerData);
});
await router.replace({name: 'layout'})
}).finally(() => {
loginLoading.value = false
})
}
@@ -255,7 +267,7 @@ function submitRegister() {
verifyShow.value = true
if (!turnstileId) {
nextTick(() => {
turnstileId = window.turnstile.render('.register-turnstile')
turnstileId = window.turnstile.render('.register-turnstile')
})
} else {
nextTick(() => {
@@ -266,7 +278,7 @@ function submitRegister() {
}
registerLoading.value = true
register({email: registerForm.email + suffix.value, password: registerForm.password,token: verifyToken}).then(() => {
register({email: registerForm.email + suffix.value, password: registerForm.password, token: verifyToken}).then(() => {
show.value = 'login'
registerForm.email = ''
registerForm.password = ''
@@ -305,14 +317,17 @@ function submitRegister() {
}
.container {
max-width: 380px;
background: #FFFFFF;
padding: 20px;
margin: 0 20px;
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid #e4e7ed;
box-shadow: var(--el-box-shadow-light);
.switch {
padding-top: 10px;
font-size: 14px;
@@ -334,10 +349,6 @@ function submitRegister() {
}
}
form{
max-width: 410px;
padding: 0 20px;
}
.setting-icon {
position: relative;
@@ -357,15 +368,17 @@ form{
opacity: 0;
pointer-events: none;
}
.custom-style {
margin-bottom: 10px;
}
.custom-style .el-segmented {
--el-border-radius-base: 8px;
--el-border-radius-base: 6px;
width: 180px;
}
#box {
#login-box {
background: linear-gradient(to bottom, #2980b9, #6dd5fa, #fff);
color: #333;
font: 100% Arial, sans-serif;
@@ -375,6 +388,7 @@ form{
overflow-x: hidden;
}
#background-wrap {
height: 100%;
z-index: 0;

View File

@@ -0,0 +1,411 @@
<template>
<div class="perm-box">
<div class="header-actions">
<Icon class="icon" icon="ion:add-outline" width="23" height="23" @click="openAddRole" />
<Icon class="icon" icon="ion:reload" width="18" height="18" @click="refresh" />
</div>
<div class="loading" v-if="tableLoading">
<loading/>
</div>
<el-scrollbar v-else class="perm-scrollbar">
<div>
<el-table
:data="roles"
style="width: 100%;"
>
<el-table-column width="10" />
<el-table-column label="身份名称" prop="name" :min-width="roleWidth">
<template #default="props">
<div class="role-name">
<span >{{props.row.name}}</span>
<span v-if="props.row.isDefault"><el-tag class="def-tag" type="success">默认</el-tag></span>
</div>
</template>
</el-table-column>
<el-table-column label="排序" :width="sortWidth" prop="sort"/>
<el-table-column v-if="desShow" label="描述" min-width="200" prop="description" >
<template #default="props">
<div class="description">
<span >{{props.row.description}}</span>
</div>
</template>
</el-table-column>
<el-table-column label="操作" :width="settingWidth">
<template #default="props">
<el-dropdown trigger="click">
<el-button size="small" type="primary">操作</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openRoleSet(props.row)">修改</el-dropdown-item>
<el-dropdown-item @click="setDef(props.row)">默认</el-dropdown-item>
<el-dropdown-item @click="delRole(props.row)">删除</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</div>
</el-scrollbar>
<el-dialog 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" />
<div class="dialog-input">
<el-input-number placeholder="排序" :min="0" :max="9999" v-model.number="form.sort" controls-position="right" autocomplete="off" />
</div>
<el-segmented size="small" class="perm-expand" @change="expandChange" v-model="expand" :options="options" />
<el-tree
:expand-on-click-node="false"
:check-on-click-node="false"
ref="tree"
:data="treeList"
show-checkbox
node-key="permId"
:default-expand-all="expand"
:props="{
label: 'name'
}"
>
<template #default="{ node, data }">
<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>
<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>
</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>
</span>
</div>
</template>
</el-tree>
<el-button class="btn" type="primary" :loading="permLoading" @click="roleFormClick"
>保存
</el-button>
</div>
</el-dialog>
</div>
</template>
<script setup>
import {Icon} from "@iconify/vue";
import {defineOptions, nextTick, reactive, ref} from "vue";
import {roleAdd, roleDelete, rolePermTree, roleRoleList, roleSet, roleSetDef} from "@/request/role.js";
import {ElMessage, ElMessageBox} from "element-plus";
import loading from '@/components/loading/index.vue';
import {useRoleStore} from "@/store/role.js";
defineOptions({
name: 'role'
})
const roleStore = useRoleStore();
const roleFormShow = ref(false)
const treeList = reactive([])
const roles = ref([])
const tree = ref({})
const permLoading = ref(false)
const tableLoading = ref(false)
const desShow = ref(true)
const settingWidth = ref(null)
const sortWidth = ref(null)
const roleWidth = ref(200)
const dialogType = reactive({
title: '',
type: ''
})
const form = reactive({
name: null,
description: null,
sendType: 'count',
sendCount: '',
accountCount: '',
sort: 0,
isDefault: 0,
})
const expand = ref(false)
const options = [{ label: '展开',value: true }, { label:'收起',value: false}]
let chooseRole = {}
refresh()
rolePermTree().then(tree => {
treeList.push(...tree)
})
function roleFormClick() {
if (dialogType.type === 'add') {
addRole()
} else {
setRole()
}
}
function setDef(role) {
roleSetDef(role.roleId).then(() => {
ElMessage({
message: "设置成功",
type: "success",
plain: true
})
getRoleList()
})
}
function delRole(role) {
ElMessageBox.confirm(`确认删除 ${role.name} 吗?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
roleDelete(role.roleId).then(() => {
ElMessage({
message: "删除成功",
type: "success",
plain: true
})
getRoleList()
roleStore.refreshSelect()
})
});
}
function expandChange(e) {
if (e) {
const nodes = tree.value?.store.nodesMap;
for (const key in nodes) {
nodes[key].expanded = true;
}
} else {
const nodes = tree.value?.store.nodesMap;
for (const key in nodes) {
nodes[key].expanded = false;
}
}
}
function setRole() {
if (!form.name) {
ElMessage({
message: "身份名不能为空",
type: "error",
plain: true
})
return
}
const params = {...form,roleId: chooseRole.roleId}
const checkedId = tree.value.getCheckedKeys()
const halfId = tree.value.getHalfCheckedKeys()
params.permIds = [...checkedId, ...halfId]
permLoading.value = true
roleSet(params).then(() => {
ElMessage({
message: "修改成功",
type: "success",
plain: true
})
roleFormShow.value = false
getRoleList()
}).finally(() => {
permLoading.value = false
})
}
function resetForm() {
form.name = null
form.description = null
form.sort = 0
form.sendType = 'count'
form.sendCount = ''
form.accountCount = ''
tree.value.setCheckedKeys([])
}
function openRoleSet(role) {
chooseRole = role
dialogType.title = '修改身份'
dialogType.type = 'set'
roleFormShow.value = true
form.sort = role.sort
form.name = role.name
form.sendType = role.sendType
form.sendCount = role.sendCount
form.accountCount = role.accountCount
nextTick(() => {
tree.value.setCheckedKeys(role.permIds)
})
}
function openAddRole() {
dialogType.title = '添加身份'
dialogType.type = 'add'
roleFormShow.value = true
}
function addRole() {
const params = {...form}
const checkedId = tree.value.getCheckedKeys()
const halfId = tree.value.getHalfCheckedKeys()
params.permIds = [...checkedId, ...halfId]
permLoading.value = true
roleAdd(params).then(() => {
ElMessage({
message: "添加成功",
type: "success",
plain: true
})
roleFormShow.value = false
getRoleList()
roleStore.refreshSelect()
}).finally(() => {
permLoading.value = false
})
}
function refresh() {
tableLoading.value = true
roles.length = 0
getRoleList()
}
function getRoleList() {
roleRoleList().then(list => {
roles.value = list
}).finally(() => {
tableLoading.value = false
})
}
function adjustWidth() {
desShow.value = window.innerWidth > 767
settingWidth.value = window.innerWidth < 480 ? 75 : null
sortWidth.value = window.innerWidth < 480 ? 75 : null
roleWidth.value = window.innerWidth < 480 ? 180 : 200
}
adjustWidth()
window.onresize = () => {
adjustWidth()
};
</script>
<style scoped lang="scss">
.perm-box {
height: 100%;
overflow: hidden;
width: 100%;
.perm-scrollbar {
height: 100%;
}
}
.send-num {
margin-left: 10px;
.el-input-number {
width: 95px;
}
}
.def-tag {
margin-left: 10px;
height: 20px;
}
.header-actions {
padding: 9px 15px;
display: flex;
align-items: center;
gap: 20px;
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
font-size: 18px;
.search {
:deep(.el-input-group) {
height: 28px;
}
:deep(.el-input__inner) {
height: 28px;
}
}
.icon {
cursor: pointer;
}
}
:deep(.description) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.loading {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.role-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.description {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.el-segmented--small .el-segmented__item) {
border-radius: 8px !important;
overflow: hidden;
}
.dialog-box {
.dialog-input {
margin-bottom: 15px !important;
}
}
.perm-expand {
margin-bottom: 10px;
--el-border-radius-base: 8px;
}
:deep(.el-dialog) {
margin-top: 15vh !important;
margin-bottom: 20px !important;
width: 400px !important;
@media (max-width: 440px) {
width: calc(100% - 40px) !important;
margin-right: 20px !important;
margin-left: 20px !important;
}
}
.btn {
width: 100%;
margin-top: 15px;
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<emailScroll ref="sendScroll"
:cancel-success="cancelStar"
:star-success="addStar"
:getEmailList="getEmailList"
:emailDelete="emailDelete"
:star-add="starAdd"
show-status
actionLeft="4px"
:star-cancel="starCancel"
@jump="jumpContent"
:time-sort="params.timeSort"
>
<template #first>
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-down-outline"
v-if="params.timeSort === 0" width="28" height="28"/>
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-up-outline" v-else
width="28" height="28"/>
</template>
</emailScroll>
</template>
<script setup>
import {useAccountStore} from "@/store/account.js";
import {useEmailStore} from "@/store/email.js";
import emailScroll from "@/components/email-scroll/index.vue"
import {emailList, emailDelete} from "@/request/email.js";
import {starAdd, starCancel} from "@/request/star.js";
import {defineOptions, onMounted, reactive, ref, watch} from "vue";
import router from "@/router/index.js";
import {Icon} from "@iconify/vue";
defineOptions({
name: 'send'
})
const emailStore = useEmailStore();
const accountStore = useAccountStore();
const sendScroll = ref({})
const params = reactive({
timeSort: 0,
})
onMounted(() => {
emailStore.sendScroll = sendScroll;
})
watch(() => accountStore.currentAccountId, () => {
sendScroll.value.refreshList();
})
function changeTimeSort() {
params.timeSort = params.timeSort ? 0 : 1
sendScroll.value.refreshList();
}
function jumpContent(email) {
emailStore.contentData.email = email
emailStore.contentData.delType = 'logic'
emailStore.contentData.showStar = true
router.push('/content')
}
function addStar(email) {
emailStore.starScroll?.addItem(email)
}
function cancelStar(email) {
emailStore.starScroll?.deleteEmail([email.emailId])
}
function getEmailList(emailId, size) {
return emailList(accountStore.currentAccountId, emailId, params.timeSort, size, 1)
}
</script>
<style scoped>
.icon {
cursor: pointer;
}
</style>

View File

@@ -13,7 +13,7 @@
</div>
</div>
</div>
<div class="del-email">
<div class="del-email" v-perm="'my:delete'">
<div class="title">删除账户</div>
<div style="color: #585d69;">
此操作将永久删除您的账户及其所有数据无法恢复
@@ -23,20 +23,18 @@
</div>
</div>
<el-dialog v-model="pwdShow" title="修改密码" width="340">
<form>
<div class="update-pwd">
<el-input type="password" placeholder="新的密码" v-model="form.password"/>
<el-input type="password" placeholder="确认密码" v-model="form.newPwd"/>
<el-button type="primary" :loading="setPwdLoading" @click="submitPwd">保存</el-button>
</div>
</form>
</el-dialog>
</div>
</template>
<script setup>
import {defineOptions} from "vue";
import {reactive, ref} from 'vue'
import {resetPassword, userDelete} from "@/request/user.js";
import {resetPassword, userDelete} from "@/request/my.js";
import {ElMessage, ElMessageBox} from "element-plus";
import {useUserStore} from "@/store/user.js";
import router from "@/router/index.js";

View File

@@ -5,7 +5,11 @@
:getEmailList="starList"
:emailDelete="emailDelete"
:star-add="starAdd"
:star-cancel="starCancel"/>
:star-cancel="starCancel"
@jump="jumpContent"
actionLeft="6px"
:show-account-icon="false"
/>
</template>
<script setup>
@@ -14,6 +18,7 @@ import {emailDelete} from "@/request/email.js";
import {starAdd, starCancel, starList} from "@/request/star.js";
import {useEmailStore} from "@/store/email.js";
import {defineOptions, onMounted, ref} from "vue";
import router from "@/router/index.js";
defineOptions({
name: 'star'
@@ -22,8 +27,14 @@ defineOptions({
const scroll = ref({})
const emailStore = useEmailStore();
function jumpContent(email) {
emailStore.contentData.email = email
emailStore.contentData.delType = 'logic'
emailStore.contentData.showStar = true
router.push('/content')
}
function cancelStar(email) {
emailStore.emailScroll?.editEmailStar(email.emailId, 0)
scroll.value.deleteEmail([email.emailId])
}

View File

@@ -0,0 +1,216 @@
<template>
<div class="email-list-box">
<emailScroll ref="sysEmailScroll"
:get-emailList="getEmailList"
:email-delete="sysEmailDelete"
:star-add="starAdd"
:star-cancel="starCancel"
:show-star="false"
show-user-info
show-status
:show-account-icon="false"
@jump="jumpContent"
@refresh-before="refreshBefore"
>
<template #first>
<el-input
v-model="searchValue"
placeholder="输入内容查询"
class="search-input"
>
<template #prefix>
<div @click.stop="openSelect">
<el-select
ref="mySelect"
v-model="searchType"
placeholder="请选择"
class="select"
>
<el-option key="1" label="用户" :value="'user'"/>
<el-option key="2" label="邮箱" :value="'account'"/>
<el-option key="3" label="发件人" :value="'name'"/>
<el-option key="4" label="主题" :value="'subject'"/>
</el-select>
<div style="color: #333;display: flex;">
<span>{{ selectTitle }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
</div>
</template>
</el-input>
<el-select v-model="params.type" placeholder="Select" class="status-select">
<el-option key="1" label="全部" value="all"/>
<el-option key="2" label="发件" value="send"/>
<el-option key="3" label="收件" value="receive"/>
<el-option key="4" label="删除" value="delete"/>
</el-select>
<Icon class="icon" icon="iconoir:search" @click="search" width="20" height="20"/>
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-down-outline"
v-if="params.timeSort === 0" width="28" height="28"/>
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-up-outline" v-else
width="28" height="28"/>
</template>
</emailScroll>
</div>
</template>
<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 {useEmailStore} from "@/store/email.js";
import {
sysEmailAll,
sysEmailDelete
} from "@/request/sys-email.js";
import {Icon} from "@iconify/vue";
import router from "@/router/index.js";
defineOptions({
name: 'sys-email'
})
const emailStore = useEmailStore();
const sysEmailScroll = ref({})
const searchType = ref('user')
const searchValue = ref('')
const mySelect = ref()
const openSelect = () => {
mySelect.value.toggleMenu()
}
const params = reactive({
timeSort: 0,
type: 'all',
userEmail: null,
accountEmail: null,
name: null,
subject: null
})
const selectTitle = computed(() => {
if (searchType.value === 'user') return '用户'
if (searchType.value === 'account') return '邮箱'
if (searchType.value === 'name') return '发件人'
if (searchType.value === 'subject') return '主题'
})
function refreshBefore() {
searchValue.value = null
params.timeSort = 0
params.type = 'all'
params.userEmail = null
params.accountEmail = null
params.name = null
params.subject = null
}
function search() {
params.userEmail = null
params.accountEmail = null
params.name = null
params.subject = null
if (searchType.value === 'user') {
params.userEmail = searchValue.value
}
if (searchType.value === 'account') {
params.accountEmail = searchValue.value
}
if (searchType.value === 'name') {
params.name = searchValue.value
}
if (searchType.value === 'subject') {
params.subject = searchValue.value
}
sysEmailScroll.value.refreshList();
}
function changeTimeSort() {
params.timeSort = params.timeSort ? 0 : 1
sysEmailScroll.value.refreshList();
}
function jumpContent(email) {
emailStore.contentData.email = email
emailStore.contentData.delType = 'physics'
emailStore.contentData.showStar = false
router.push('/content')
}
function getEmailList(emailId, size) {
return sysEmailAll({emailId, size, ...params})
}
</script>
<style scoped lang="scss">
.email-list-box {
height: 100%;
width: 100%;
overflow: hidden;
}
.search {
padding-top: 5px;
padding-bottom: 5px;
}
.select {
position: absolute;
width: 40px;
opacity: 0;
pointer-events: none;
}
:deep(.header-actions) {
padding-top: 8px;
padding-bottom: 8px;
}
.search-input {
width: min(280px,calc(100vw - 140px));
height: 28px;
.setting-icon {
position: relative;
top: 3px;
}
}
.status-select {
margin-bottom: 2px;
width: 80px;
:deep(.el-select__wrapper) {
min-height: 28px;
}
}
.input-with-select {
max-width: 200px;
border-radius: 0 4px 4px 0;
}
:deep(.input-with-select .el-input-group__append) {
background-color: var(--el-fill-color-blank);
}
:deep(.el-select__wrapper) {
padding: 2px 10px;
min-height: 28px;
}
.icon {
cursor: pointer;
}
</style>

View File

@@ -1,177 +1,486 @@
<template>
<div class="box">
<div class="setting">
<div class="title">网站设置</div>
<div class="setting-item">
<div><span>用户注册</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.register"/>
</div>
</div>
<div class="setting-item">
<div><span>添加邮箱</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.addEmail"/>
</div>
</div>
<div class="setting-item">
<div><span>邮件接收</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.receive"/>
</div>
</div>
<div class="setting-item">
<div><span>注册验证</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.registerVerify"/>
</div>
</div>
<div class="setting-item">
<div><span>添加验证</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.addEmailVerify"/>
</div>
</div>
<div class="setting-item">
<div>
<span>多号模式</span>
<el-popover
content="开启后账号栏出现一个用户可以添加多个邮箱"
>
<template #reference>
<Icon class="warning" icon="fe:warning" width="20" height="20" />
</template>
</el-popover>
</div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.manyEmail"/>
</div>
</div>
<div class="setting-item ">
<div>
<span>轮询刷新</span>
<el-popover
content="轮询请求服务器获取最新邮件人多可能会超出免费额度"
>
<template #reference>
<Icon class="warning" icon="fe:warning" width="20" height="20" />
</template>
</el-popover>
</div>
<div class="item-select">
<el-select
@change="change"
style="width: 80px;"
v-model="setting.autoRefreshTime"
placeholder="Select"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</div>
<div class="setting-item">
<div class="title-item"><span>网站标题</span></div>
<div class="email-title">
<div>{{ setting.title }}</div>
<div @click="editTitleShow = true">
<Icon icon="iconamoon:edit-light" width="24" height="24"/>
<div class="settings-container">
<el-scrollbar class="scroll">
<div class="card-grid">
<!-- Website Settings Card -->
<div class="settings-card">
<div class="card-title">网站设置</div>
<div class="card-content">
<div class="setting-item">
<div><span>用户注册</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.register"/>
</div>
</div>
<div class="setting-item">
<div><span>添加邮箱</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.addEmail"/>
</div>
</div>
<div class="setting-item">
<div>
<span>多号模式</span>
<el-tooltip effect="dark" content="开启后账号栏出现一个用户可以添加多个邮箱">
<Icon class="warning" icon="fe:warning" width="20" height="20"/>
</el-tooltip>
</div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
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="20" height="20"/>
</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>
<el-tooltip effect="dark" content="该操作会物理清空所有已被删除的数据">
<Icon class="warning" icon="fe:warning" width="20" height="20"/>
</el-tooltip>
</div>
<div>
<el-button class="opt-button" style="margin-top: 0" @click="physicsDeleteAllData" size="small"
type="primary">
<Icon icon="material-symbols:delete-outline-rounded" width="16" height="16"/>
</el-button>
</div>
</div>
</div>
</div>
<!-- Personalization Settings Card -->
<div class="settings-card">
<div class="card-title">个性化设置</div>
<div class="card-content">
<div class="setting-item">
<div class="title-item"><span>网站标题</span></div>
<div class="email-title">
<span>{{ setting.title }}</span>
<el-button class="opt-button" size="small" type="primary" @click="editTitleShow = true">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
</div>
</div>
<div class="setting-item personalized">
<div><span>登录背景</span></div>
<div>
<el-image
class="background"
:src="cvtR2Url(setting.background)"
:preview-src-list="[cvtR2Url(setting.background)]"
show-progress
fit="cover"
>
<template #error>
<div class="error-image" @click="openCut">
<Icon icon="ph:image" width="24" height="24"/>
</div>
</template>
</el-image>
<div class="background-btn">
<el-button class="opt-button" size="small" type="primary" @click="openCut">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
<el-button class="opt-button" size="small" type="primary" @click="delBackground">
<Icon icon="material-symbols:delete-outline-rounded" width="16" height="16"/>
</el-button>
</div>
</div>
</div>
</div>
</div>
<!-- Email Sending Settings Card -->
<div class="settings-card">
<div class="card-title">邮件设置</div>
<div class="card-content">
<div class="setting-item">
<div><span>邮件接收</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.receive"/>
</div>
</div>
<div class="setting-item">
<div><span>邮件发送</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.send"/>
</div>
</div>
<div class="setting-item">
<div><span>添加resend令牌</span></div>
<div>
<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>
<!-- R2 Object Storage Card -->
<div class="settings-card">
<div class="card-title">R2对象存储</div>
<div class="card-content">
<div class="setting-item">
<div><span>访问域名</span></div>
<div class="r2domain">
<span>{{ setting.r2Domain || '空' }}</span>
<el-button class="opt-button" size="small" type="primary" @click="r2DomainShow = true">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
</div>
</div>
</div>
</div>
<!-- Turnstile Verification Card -->
<div class="settings-card">
<div class="card-title">Turnstile 人机验证</div>
<div class="card-content">
<div class="setting-item">
<div><span>注册验证</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.registerVerify"/>
</div>
</div>
<div class="setting-item">
<div><span>添加验证</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.addEmailVerify"/>
</div>
</div>
<div class="setting-item">
<div><span>siteKey</span></div>
<div class="bot-verify">
<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>
</div>
</div>
<div class="setting-item">
<div><span>secretKey</span></div>
<div class="bot-verify">
<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>
</div>
</div>
</div>
</div>
<div class="settings-card">
<div class="card-title">关于</div>
<div class="card-content">
<div class="concerning-item">
<span>版本:</span>
<span>v1.1.0</span>
</div>
<div class="concerning-item">
<span>交流:</span>
<el-button @click="jumpTelegram">
telegram
<template #icon>
<Icon icon="logos:telegram" width="30" height="30"/>
</template>
</el-button>
</div>
</div>
</div>
</div>
</div>
<el-dialog v-model="editTitleShow" title="修改标题" width="340" @close="editTitle = ''">
<form>
<el-input type="text" placeholder="网站标题" v-model="editTitle"/>
<el-button type="primary" :loading="editTitleLoading" @click="saveTitle">保存</el-button>
</form>
</el-dialog>
<!-- Dialogs remain the same -->
<el-dialog v-model="editTitleShow" title="修改标题" width="340" @closed="editTitle = ''">
<form>
<el-input type="text" placeholder="网站标题" v-model="editTitle"/>
<el-button type="primary" :loading="settingLoading" @click="saveTitle">保存</el-button>
</form>
</el-dialog>
<el-dialog v-model="resendTokenFormShow" title="添加resend令牌" width="340" @closed="cleanResendTokenForm">
<form>
<el-select style="margin-bottom: 15px" v-model="resendTokenForm.domain" placeholder="Select">
<el-option
v-for="item in settingStore.domainList"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<el-input type="text" placeholder="令牌" v-model="resendTokenForm.token"/>
<el-button type="primary" :loading="settingLoading" @click="saveResendToken">保存</el-button>
</form>
</el-dialog>
<el-dialog v-model="r2DomainShow" title="添加R2访问域名" width="340" @closed="r2DomainInput = ''">
<form>
<el-input type="text" placeholder="R2访问域名" v-model="r2DomainInput"/>
<el-button type="primary" :loading="settingLoading" @click="saveR2domain">保存</el-button>
</form>
</el-dialog>
<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"/>
<el-input type="text" style="margin-top: 15px" placeholder="secretKey" v-model="turnstileForm.secretKey"/>
<el-button type="primary" :loading="settingLoading" @click="saveTurnstileKey">保存</el-button>
</form>
</el-dialog>
<el-dialog
v-model="cutShow"
title="背景截图"
class="cut-dialog"
>
<div class="cropper">
<vueCropper
ref="cropper"
:img="cutImage"
:fixedNumber="[16,9]"
outputType="jpeg"
:fixed="true"
:centerBox="true"
:full="true"
:autoCrop="true"
:outputSize="0.9"
></vueCropper>
</div>
<div class="cut-button">
<el-button type="primary" :loading="settingLoading" @click="saveBackground">保存</el-button>
</div>
</el-dialog>
</el-scrollbar>
</div>
</template>
<script setup>
import {defineOptions} from "vue";
import {ref} from 'vue'
import {settingSet} from "@/request/setting.js";
import {ElMessage} from "element-plus";
import {defineOptions, nextTick, onMounted, reactive, ref} from "vue";
import {physicsDeleteAll, setBackground, settingQuery, settingSet} from "@/request/setting.js";
import {ElMessage, ElMessageBox} from "element-plus";
import {useSettingStore} from "@/store/setting.js";
import {useUserStore} from "@/store/user.js";
import {useAccountStore} from "@/store/account.js";
import {Icon} from "@iconify/vue";
import {compressImage} from "@/utils/file-utils.js";
import {cvtR2Url} from "@/utils/convert.js";
import {storeToRefs} from "pinia";
defineOptions({
name: 'sys-setting'
})
const cropper = ref()
const cutImage = ref('')
const cutShow = ref(false)
const accountStore = useAccountStore();
const userStore = useUserStore();
const editTitleShow = ref(false)
const resendTokenFormShow = ref(false)
const r2DomainShow = ref(false)
const turnstileShow = ref(false)
const settingStore = useSettingStore();
let setting = ref(settingStore.settings)
const {settings: setting} = storeToRefs(settingStore);
const editTitle = ref('')
const editTitleLoading = ref(false)
const settingLoading = ref(false)
const r2DomainInput = ref('')
let backup = {}
const resendTokenForm = reactive({
domain: '',
token: '',
})
const turnstileForm = reactive({
siteKey: '',
secretKey: ''
})
const options = [
{
label: '关闭',
value: 0,
},
{
label: '3s',
value: 3,
},
{
label: '5s',
value: 5,
},
{
label: '7s',
value: 7,
},
{
label: '10s',
value: 10,
},
{
label: '15s',
value: 15,
},
{
label: '20s',
value: 20,
}
{label: '关闭', value: 0},
{label: '3s', value: 3},
{label: '5s', value: 5},
{label: '7s', value: 7},
{label: '10s', value: 10},
{label: '15s', value: 15},
{label: '20s', value: 20}
]
function beforeChange() {
onMounted(() => {
resendTokenForm.domain = settingStore.domainList[0];
})
function physicsDeleteAllData() {
ElMessageBox.prompt('此操作不可逆转, 输入 <b style="font-weight: bold">确认删除</b> 继续操作', {
confirmButtonText: '确定',
cancelButtonText: '取消',
dangerouslyUseHTMLString: true,
title: '警告',
type: 'warning',
inputPattern: /^确认删除$/,
inputErrorMessage: '请输入确认删除',
}).then(() => {
physicsDeleteAll().then(() => {
ElMessage({
message: "删除成功",
type: "success",
plain: true
})
})
})
}
function delBackground() {
ElMessageBox.confirm('确定要删除这个背景吗?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
setting.value.background = null
editSetting({background: null})
})
}
function saveTurnstileKey() {
const settingForm = {}
settingForm.siteKey = turnstileForm.siteKey
settingForm.secretKey = turnstileForm.secretKey
editSetting(settingForm)
}
function saveBackground() {
settingLoading.value = true
cropper.value.getCropData(data => {
setBackground(data).then(key => {
setting.value.background = key
cutShow.value = false
ElMessage({
message: "设置成功",
type: "success",
plain: true
})
}).finally(() => {
settingLoading.value = false
})
})
}
function openCut() {
const doc = document.createElement('input')
doc.setAttribute('type', 'file')
doc.setAttribute('accept', 'image/*')
doc.click()
doc.onchange = async (e) => {
const image = await compressImage(e.target.files[0], 0.9)
cutImage.value = URL.createObjectURL(image)
cutShow.value = true
}
}
function saveR2domain() {
const settingForm = {r2Domain: r2DomainInput.value}
if (settingForm.r2Domain && !settingForm.r2Domain.startsWith('http')) {
ElMessage({
message: "域名必须以http或https开头",
type: "error",
plain: true
})
return;
}
editSetting(settingForm)
}
function openResendForm() {
resendTokenFormShow.value = true
}
function saveResendToken() {
const settingForm = {
resendTokens: {}
}
const domain = resendTokenForm.domain.slice(1)
settingForm.resendTokens[domain] = resendTokenForm.token
editSetting(settingForm)
}
function backupSetting() {
const settingForm = {...setting.value}
delete settingForm.resendTokens
delete settingForm.siteKey
delete settingForm.secretKey
backup = JSON.stringify(setting.value)
}
function cleanResendTokenForm() {
resendTokenForm.token = ''
}
function beforeChange() {
if (settingLoading.value) return false
backupSetting()
return true
}
function change() {
editSetting(setting)
function change(e) {
const settingForm = {...setting.value}
delete settingForm.siteKey
delete settingForm.secretKey
delete settingForm.resendTokens
editSetting(settingForm, false)
}
function refresh() {
settingQuery().then(setting => {
settingStore.settings = setting;
settingStore.domainList = setting.domainList;
})
}
function saveTitle() {
backup = JSON.stringify(setting.value)
setting.value.title = editTitle.value
editSetting(setting)
editSetting({title: editTitle.value})
}
function editSetting(setting) {
editTitleLoading.value = true
settingSet(setting.value).then(() => {
editTitleLoading.value = false
function jumpTelegram() {
const doc = document.createElement('a')
doc.href = 'https://t.me/cloud_mail_tg'
doc.target = '_blank'
doc.click()
}
function editSetting(settingForm, refreshStatus = true) {
if (settingLoading.value) return
settingLoading.value = true
settingSet(settingForm).then(() => {
settingLoading.value = false
ElMessage({
message: "设置成功",
type: "success",
@@ -180,69 +489,256 @@ function editSetting(setting) {
if (setting.value.manyEmail === 1) {
accountStore.currentAccountId = userStore.user.accountId;
}
if (refreshStatus) {
refresh()
}
editTitleShow.value = false
r2DomainShow.value = false
resendTokenFormShow.value = false
turnstileShow.value = false
}).catch(() => {
editTitleLoading.value = false
setting.value = JSON.parse(backup)
setting.value = {...setting.value, ...JSON.parse(backup)}
}).finally(() => {
settingLoading.value = false
})
}
</script>
<style scoped lang="scss">
.box {
.settings-container {
height: 100%;
overflow: hidden;
}
padding: 40px 40px;
.scroll {
width: 100%;
}
.title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
padding: 20px;
gap: 20px;
@media (max-width: 500px) {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
@media (max-width: 767px) {
gap: 15px;
padding: 15px;
}
}
.background {
width: 230px;
height: 120px;
border-radius: 4px;
border: 1px solid #e4e7ed;
@media (max-width: 500px) {
width: 140px;
height: 79px;
}
}
.background-btn {
display: flex;
gap: 10px;
flex-direction: column;
}
.settings-card {
background-color: #fff;
border-radius: 4px;
border: 1px solid #e4e7ed;
transition: all 300ms;
box-shadow: var(--el-box-shadow-light);
overflow: hidden;
}
.card-title {
font-size: 16px;
font-weight: bold;
padding: 10px 20px;
border-bottom: 1px solid #e6e6e6;
}
.card-content {
padding: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.setting-item {
display: grid;
grid-template-columns: auto 1fr;
gap: 10px;
font-weight: bold;
> div:first-child {
display: flex;
align-items: center;
gap: 5px;
}
.setting {
font-size: 14px;
> div:last-child {
display: grid;
grid-template-columns: 1fr auto;
justify-items: flex-end;
font-weight: normal;
}
}
.warning {
margin-left: 5px;
color: gray;
cursor: pointer;
}
.cropper {
height: 397px;
width: 705px;
@media (max-width: 767px) {
width: calc(100vw - 60px);
height: calc((100vw - 60px) * 9 / 16);
}
}
:deep(.cut-dialog.el-dialog) {
width: fit-content !important;
height: fit-content !important;
}
.error-image {
background: #f5f7fa;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.cut-button {
padding-top: 15px;
width: 100%;
display: flex;
justify-content: end;
.el-button {
width: fit-content;
}
}
.bot-verify {
display: grid;
grid-template-columns: 1fr auto;
span {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 40px;
align-items: center;
}
.setting-item {
display: flex;
gap: 140px;
font-weight: bold;
position: relative;
@media (max-width: 767px) {
gap: 80px;
}
.el-button {
width: 48px;
margin: 0 0 0 10px;
}
}
>div:first-child {
display: flex;
justify-content: center;
align-items: center;
}
.opt-button {
width: fit-content !important;
}
.r2domain {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.el-button {
width: 48px;
margin: 0 0 0 10px;
}
}
.personalized {
align-items: start;
> div:last-child {
display: flex;
justify-content: end;
.el-button {
margin-left: 10px;
margin-top: 0;
}
}
}
.warning {
position: absolute;
left: 65px;
color: gray;
.concerning-item {
display: flex;
align-items: center;
:deep(.el-button) {
padding: 0 10px;
i {
font-size: 22px;
}
}
> span:first-child {
font-weight: bold;
padding-right: 20px;
}
}
.email-title {
font-weight: normal !important;
display: flex;
display: grid;
gap: 10px;
div:first-child {
display: flex;
align-items: center;
grid-template-columns: 1fr auto;
span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.el-button {
margin-top: 0;
}
}
.el-button {
.token-item {
padding-top: 0;
div:last-child {
font-weight: normal;
}
}
form .el-button {
margin-top: 15px;
width: 100%;
}
.el-switch {
height: 28px;
}
:deep(.el-button--small) {
margin-top: 2px !important;
margin-bottom: 2px !important;
height: 24px;
}
:deep(.el-select__wrapper) {
min-height: 28px;
}
</style>

View File

@@ -0,0 +1,973 @@
<template>
<div class="user-box">
<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.email"
class="search-input"
placeholder="输入邮箱名搜索"
>
</el-input>
</div>
<el-select v-model="params.status" placeholder="Select" class="status-select">
<el-option :key="-1" label="全部" :value="-1"/>
<el-option :key="0" label="正常" :value="0"/>
<el-option :key="1" label="封禁" :value="1"/>
<el-option :key="-2" label="删除" :value="-2"/>
</el-select>
<Icon class="icon" icon="iconoir:search" @click="search" width="20" height="20"/>
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-down-outline"
v-if="params.timeSort === 1" width="28" height="28"/>
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-up-outline" v-else width="28"
height="28"/>
<Icon class="icon" icon="ion:reload" width="18" height="18" @click="refresh"/>
<Icon class="icon" icon="pepicons-pencil:expand" width="26" height="26" @click="changeExpand"/>
</div>
<el-scrollbar ref="scrollbarRef" class="scrollbar">
<div>
<div class="loading" :class="tableLoading ? 'loading-show' : 'loading-hide'">
<loading/>
</div>
<el-table
@filter-change="tableFilter"
:empty-text="first ? '' : null"
:default-expand-all="expandStatus"
:data="users"
:preserve-expanded-content="preserveExpanded"
style="width: 100%;"
:key="key"
>
<el-table-column :width="expandWidth" type="expand">
<template #default="props">
<div class="details">
<div v-if="!sendNumShow"><span class="details-item-title">发件数量:</span>{{ props.row.sendEmailCount }}
</div>
<div v-if="!accountNumShow"><span class="details-item-title">邮箱数量:</span>{{
props.row.accountCount
}}
</div>
<div v-if="!createTimeShow"><span class="details-item-title">注册时间:</span>{{
tzDayjs(props.row.createTime).format('YYYY-MM-DD HH:mm:ss')
}}
</div>
<div v-if="!typeShow"><span class="details-item-title">身份类型:</span> {{ toRoleName(props.row.type) }}
</div>
<div v-if="!statusShow">
<span class="details-item-title">状态:</span>
<el-tag disable-transitions v-if="props.row.isDel === 1" type="info">删除</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 0" type="primary">正常</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 1" type="danger">封禁</el-tag>
</div>
<div><span class="details-item-title">注册IP:</span>{{ props.row.createIp || '未知' }}</div>
<div><span class="details-item-title">近期IP:</span>{{ props.row.activeIp || '未知' }}</div>
<div><span class="details-item-title">近期活动:</span>{{
props.row.activeTime ? tzDayjs(props.row.activeTime).format('YYYY-MM-DD') : '未知'
}}
</div>
<div><span class="details-item-title">登录设备:</span>{{ props.row.device || '未知' }}</div>
<div><span class="details-item-title">登录系统:</span>{{ props.row.os || '未知' }}</div>
<div><span class="details-item-title">登录浏览器:</span>{{ props.row.browser || '未知' }}</div>
<div>
<span class="details-item-title">发件次数:</span>
<span>{{ formatSendCount(props.row) }}</span>
<el-tag style="margin-left: 10px" v-if="props.row.sendAction.hasPerm" type="success">
{{ formatSendType(props.row) }}
</el-tag>
<el-button size="small" style="margin-left: 10px"
v-if="props.row.sendAction.hasPerm && props.row.sendAction.sendCount"
@click="resetSendCount(props.row)" type="primary">重置
</el-button>
</div>
</div>
</template>
</el-table-column>
<el-table-column show-overflow-tooltip :tooltip-formatter="tableRowFormatter" label="用户邮箱" :min-width="emailWidth">
<template #default="props">
<div class="email-row">{{ props.row.email }}</div>
</template>
</el-table-column>
<el-table-column :formatter="formatterReceive" label-class-name="receive" column-key="receive"
:filtered-value="filteredValue" :filters="filters" :width="receiveWidth" label="收件数量"
prop="receiveEmailCount"/>
<el-table-column :formatter="formatterSend" label-class-name="send" column-key="send"
:filtered-value="filteredValue" :filters="filters" v-if="sendNumShow" label="发件数量"
prop="sendEmailCount"/>
<el-table-column :formatter="formatterAccount" label-class-name="account" column-key="account"
:filtered-value="filteredValue" :filters="filters" v-if="accountNumShow" label="邮箱数量"
prop="accountCount"/>
<el-table-column v-if="createTimeShow" label="注册时间" min-width="160" prop="createTime">
<template #default="props">
{{ tzDayjs(props.row.createTime).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column v-if="statusShow" min-width="60px" label="状态" prop="status">
<template #default="props">
<el-tag disable-transitions v-if="props.row.isDel === 1" type="info">删除</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 0" type="primary">正常</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 1" type="danger">封禁</el-tag>
</template>
</el-table-column>
<el-table-column v-if="typeShow" label="身份类型" min-width="140" prop="type">
<template #default="props">
<div class="type">
{{ toRoleName(props.row.type) }}
</div>
</template>
</el-table-column>
<el-table-column label="设置" :width="settingWidth">
<template #default="props">
<el-dropdown trigger="click">
<el-button size="small" type="primary">操作</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openSetPwd(props.row)">改密</el-dropdown-item>
<el-dropdown-item @click="openSetType(props.row)">权限</el-dropdown-item>
<el-dropdown-item v-if="props.row.isDel !== 1" @click="setStatus(props.row)">
{{ setStatusName(props.row) }}
</el-dropdown-item>
<el-dropdown-item v-else @click="restore(props.row)">恢复</el-dropdown-item>
<el-dropdown-item @click="delUser(props.row)">删除</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<div class="pagination" v-if="total > 10">
<el-pagination
:size="pageSize"
:current-page="params.num"
:page-size="params.size"
:pager-count="pagerCount"
:page-sizes="[10, 15, 20, 25, 30, 50]"
background
:layout="layout"
:total="total"
@size-change="sizeChange"
@current-change="numChange"
/>
<el-pagination
v-if="phonePageShow"
:size="pageSize"
:current-page="params.num"
:page-size="params.size"
:pager-count="pagerCount"
:page-sizes="[10, 15, 20, 25, 30, 50]"
background
layout="sizes, total"
:total="total"
@size-change="sizeChange"
@current-change="numChange"
/>
</div>
</div>
</el-scrollbar>
<el-dialog class="dialog" v-model="setPwdShow" title="修改密码" @closed="resetUserForm">
<div class="dialog-box">
<el-input v-model="userForm.password" type="password" placeholder="密码" autocomplete="off">
</el-input>
<el-button class="btn" type="primary" :loading="settingLoading" @click="updatePwd"
>保存
</el-button>
</div>
</el-dialog>
<el-dialog class="dialog" v-model="setTypeShow" title="设置权限" @closed="resetUserForm">
<div class="dialog-box">
<el-select v-model="userForm.type" placeholder="Select">
<el-option v-for="item in roleList" :label="item.name" :value="item.roleId" :key="item.roleId"/>
</el-select>
<el-button class="btn" :loading="settingLoading" type="primary" @click="setType"
>保存
</el-button>
</div>
</el-dialog>
<el-dialog v-model="showAdd" title="添加用户">
<div class="container">
<el-input v-model="addForm.email" type="text" placeholder="邮箱" autocomplete="off">
<template #append>
<div @click.stop="openSelect">
<el-select
ref="mySelect"
v-model="addForm.suffix"
placeholder="请选择"
class="select"
>
<el-option
v-for="item in domainList"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<div style="color: #333">
<span>{{ addForm.suffix }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
</div>
</template>
</el-input>
<el-input type="password" v-model="addForm.password" placeholder="密码"/>
<el-select v-model="addForm.type" placeholder="身份类型">
<el-option v-for="item in roleList" :label="item.name" :value="item.roleId" :key="item.roleId"/>
</el-select>
<el-button class="btn" type="primary" @click="submit" :loading="addLoading"
>添加
</el-button>
</div>
</el-dialog>
</div>
</template>
<script setup>
import {defineOptions, h, reactive, ref, watch} from 'vue'
import {
userList,
userDelete,
userSetPwd,
userSetStatus,
userSetType,
userAdd,
userRestSendCount, userRestore
} from '@/request/user.js'
import {roleSelectUse} from "@/request/role.js";
import {Icon} from "@iconify/vue";
import {ElMessage, ElMessageBox, ElRadio, ElRadioGroup} from "element-plus";
import loading from "@/components/loading/index.vue";
import {tzDayjs} from "@/utils/day.js";
import {useSettingStore} from "@/store/setting.js";
import {isEmail} from "@/utils/verify-utils.js";
import {useRoleStore} from "@/store/role.js";
defineOptions({
name: 'user'
})
const roleStore = useRoleStore()
const settingStore = useSettingStore()
const filteredValue = ['normal', 'del']
const filters = [{text: '正常', value: 'normal'}, {text: '删除', value: 'del'}]
const preserveExpanded = ref(false)
const emailWidth = ref(230)
const expandWidth = ref(40)
const settingWidth = ref(null)
const sendNumShow = ref(true)
const accountNumShow = ref(true)
const createTimeShow = ref(true)
const statusShow = ref(true)
const typeShow = ref(true)
const receiveWidth = ref(null)
const phonePageShow = ref(false)
const layout = ref('prev, pager, next, sizes, total')
const pageSize = ref('')
const expandStatus = ref(false)
const users = ref([])
const total = ref(0)
const first = ref(true)
const scrollbarRef = ref(null)
const domainList = settingStore.domainList
const addForm = reactive({
email: '',
suffix: settingStore.domainList[0],
password: '',
type: null,
})
const params = reactive({
email: '',
num: 1,
size: 15,
timeSort: 0,
status: -1
})
let chooseUser = {}
const userForm = reactive({
password: null,
type: -1,
userId: 0,
})
const showAdd = ref(false)
const addLoading = ref(false);
const setTypeShow = ref(false)
const setPwdShow = ref(false)
const pagerCount = ref(10)
const settingLoading = ref(false)
const tableLoading = ref(true)
const roleList = reactive([])
const mySelect = ref({})
const key = ref(0)
roleSelectUse().then(list => {
roleList.length = 0
roleList.push(...list)
})
watch(() => roleStore.refresh, () => {
roleSelectUse().then(list => {
roleList.length = 0
roleList.push(...list)
})
})
getUserList()
const filterItem = reactive({
send: ['normal', 'del'],
account: ['normal', 'del'],
receive: ['normal', 'del']
})
function tableFilter(e) {
if (e.send) filterItem.send = e.send
if (e.account) filterItem.account = e.account
if (e.receive) filterItem.receive = e.receive
}
function formatterSend(e) {
if (filterItem.send.length === 2) {
return e.sendEmailCount + e.delSendEmailCount
}
if (filterItem.send.includes('normal')) {
return e.sendEmailCount
}
if (filterItem.send.includes('del')) {
return e.delSendEmailCount
}
return 0
}
function formatterAccount(e) {
if (filterItem.account.length === 2) {
return e.accountCount + e.delAccountCount
}
if (filterItem.account.includes('normal')) {
return e.accountCount
}
if (filterItem.account.includes('del')) {
return e.delAccountCount
}
return 0
}
function formatterReceive(e) {
if (filterItem.receive.length === 2) {
return e.receiveEmailCount + e.delReceiveEmailCount
}
if (filterItem.receive.includes('normal')) {
return e.receiveEmailCount
}
if (filterItem.receive.includes('del')) {
return e.delReceiveEmailCount
}
return 0
}
function setStatusName(user) {
if (user.isDel === 1) return '恢复'
if (user.status === 0) return '禁用'
if (user.status === 1) return '启用'
}
const tableRowFormatter = (data) => {
return data.row.email
}
function changeExpand() {
expandStatus.value = !expandStatus.value
key.value++
}
const openSelect = () => {
mySelect.value.toggleMenu()
}
function resetAddForm() {
addForm.email = ''
addForm.suffix = settingStore.domainList[0]
addForm.type = null
addForm.password = ''
}
function openAdd() {
showAdd.value = true
}
function submit() {
if (!addForm.email) {
ElMessage({
message: "邮箱不能为空",
type: "error",
plain: true
})
return
}
if (!isEmail(addForm.email + addForm.suffix)) {
ElMessage({
message: "非法邮箱",
type: "error",
plain: true
})
return
}
if (!addForm.password) {
ElMessage({
message: "密码不能为空",
type: "error",
plain: true
})
return
}
if (addForm.password.length < 6) {
ElMessage({
message: "密码至少六位",
type: "error",
plain: true
})
return
}
if (!addForm.type) {
ElMessage({
message: "身份类型不能为空",
type: "error",
plain: true
})
return
}
addLoading.value = true
const form = {...addForm}
form.email = form.email + form.suffix
userAdd(form).then(() => {
addLoading.value = false
showAdd.value = false
ElMessage({
message: "添加成功",
type: "success",
plain: true
})
resetAddForm()
getUserList(false)
}).finally(res => {
addLoading.value = false
})
}
function formatSendType(user) {
if (user.sendAction.sendType === 'day') return '每天'
if (user.sendAction.sendType === 'count') return '总数'
}
function formatSendCount(user) {
if (!user.sendAction.hasPerm) {
return '无权限'
}
if (!user.sendAction.sendCount) {
return '无限制';
}
return user.sendCount + '/' + user.sendAction.sendCount + '次'
}
function toRoleName(type) {
if (type === 0) {
return '超级管理员'
}
const index = roleList.findIndex(role => role.roleId === type)
if (index > -1) {
return roleList[index].name
}
return ""
}
function resetSendCount(user) {
ElMessageBox.confirm(`确认重置${user.email}发件次数吗?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
userRestSendCount(user.userId).then(() => {
ElMessage({
message: "重置成功",
type: "success",
plain: true
})
user.sendCount = 0
})
});
}
function delUser(user) {
ElMessageBox.confirm(`确认删除${user.email}吗?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
userDelete(user.userId).then(() => {
ElMessage({
message: "删除成功",
type: "success",
plain: true
})
getUserList(false)
})
});
}
function restore(user) {
const type = ref(0)
ElMessageBox.confirm( null, {
confirmButtonText: '确定',
cancelButtonText: '取消',
message: () => h('div', [
h('div', { class: 'mb-2' }, `确认要恢复 ${user.email}`),
h(ElRadioGroup, {
modelValue: type.value,
'onUpdate:modelValue': (val) => (type.value = val),
}, [
h(ElRadio, { label: 'option1', value: 0 }, '普通恢复'),
h(ElRadio, { label: 'option2', value: 1 }, '包括已删除的数据'),
])
]),
type: 'warning'
}).then(() => {
userRestore(user.userId,type.value).then(() => {
user.isDel = 0
ElMessage({
message: "恢复成功",
type: "success",
plain: true
})
})
});
}
function setStatus(user) {
if (user.status === 0) {
ElMessageBox.confirm(`确认禁用 ${user.email} 吗?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
httpSetStatus(user)
});
} else {
httpSetStatus(user)
}
}
function httpSetStatus(user) {
let status = user.status ? 0 : 1
userSetStatus({status: status, userId: user.userId}).then(() => {
user.status = status
ElMessage({
message: "设置成功",
type: "success",
plain: true
})
})
}
function setType() {
settingLoading.value = true
userSetType({type: userForm.type, userId: userForm.userId}).then(() => {
chooseUser.type = userForm.type
setTypeShow.value = false
ElMessage({
message: "设置成功",
type: "success",
plain: true
})
}).finally(() => {
settingLoading.value = false
})
}
function resetUserForm() {
userForm.password = null
userForm.userId = 0
userForm.type = 0
}
function search() {
params.num = 1
getUserList()
}
function updatePwd() {
if (userForm.password.length < 6) {
ElMessage({
message: '密码不能为空',
type: 'error',
plain: true,
})
return
}
if (userForm.password.length < 6) {
ElMessage({
message: '密码最少六位',
type: 'error',
plain: true,
})
return
}
settingLoading.value = true
userSetPwd({password: userForm.password, userId: userForm.userId}).then(() => {
setPwdShow.value = false
ElMessage({
message: "设置成功",
type: "success",
plain: true
})
}).finally(() => {
settingLoading.value = false
})
}
function openSetType(user) {
chooseUser = user
userForm.userId = user.userId
userForm.type = user.type
setTypeShow.value = true
}
function openSetPwd(user) {
userForm.userId = user.userId
setPwdShow.value = true
}
function refresh() {
params.email = ''
params.num = 1
params.status = -1
params.timeSort = 0
getUserList();
roleSelectUse().then(list => {
roleList.length = 0
roleList.push(...list)
})
}
function changeTimeSort() {
params.num = 1
params.timeSort = params.timeSort ? 0 : 1
getUserList()
}
function numChange(num) {
params.num = num
getUserList()
}
function sizeChange(size) {
params.size = size
getUserList()
}
function getUserList(loading = true) {
tableLoading.value = loading
const newParams = {...params}
if (newParams.status === -2) {
delete newParams.status
newParams.isDel = 1
}
userList(newParams).then(data => {
users.value = data.list
total.value = data.total
scrollbarRef.value?.setScrollTop(0);
}).finally(() => {
tableLoading.value = false
first.value = false
})
}
window.onresize = () => {
adjustWidth()
};
adjustWidth()
function adjustWidth() {
const width = window.innerWidth
statusShow.value = width > 1060
createTimeShow.value = width > 1200
accountNumShow.value = width > 650
sendNumShow.value = width > 685
typeShow.value = width > 767
emailWidth.value = width > 480 ? 230 : null
settingWidth.value = width < 480 ? 75 : null
expandWidth.value = width < 480 ? 25 : 40
pagerCount.value = width < 768 ? 7 : 11
receiveWidth.value = width < 480 ? 90 : null
layout.value = width < 768 ? 'pager' : 'prev, pager, next,sizes, total'
phonePageShow.value = width < 768
pageSize.value = width < 380 ? 'small' : ''
}
</script>
<style>
.el-message-box__container {
align-items: start !important;
}
.el-message-box__message {
word-break: break-all;
}
</style>
<style lang="scss" scoped>
.user-box {
overflow: hidden;
height: 100%;
}
:deep(.el-dialog) {
width: 400px !important;
@media (max-width: 440px) {
width: calc(100% - 40px) !important;
margin-right: 20px !important;
margin-left: 20px !important;
}
}
.header-actions {
padding: 9px 15px;
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
font-size: 18px;
.search-input {
width: min(200px, calc(100vw - 140px));
}
.search {
:deep(.el-input-group) {
height: 28px;
}
:deep(.el-input__inner) {
height: 28px;
}
}
.icon {
cursor: pointer;
}
}
.container {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
}
.type {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.choose-star {
color: var(--el-color-primary)
}
.scrollbar {
width: 100%;
overflow: auto;
height: calc(100% - 50px);
@media (max-width: 464px) {
height: calc(100% - 90px);
}
}
.details {
padding: 20px;
display: grid;
gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
.details-item-title {
white-space: pre;
color: #909399;
font-weight: bold;
padding-right: 10px;
}
}
.pagination {
margin-top: 15px;
margin-bottom: 20px;
padding-right: 30px;
width: 100%;
display: flex;
flex-direction: column;
justify-content: end;
gap: 10px;
@media (max-width: 767px) {
padding-right: 10px;
}
.el-pagination {
align-self: end;
}
}
.email-row {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.status-select {
width: 80px;
:deep(.el-select__wrapper) {
min-height: 28px;
}
}
.dialog {
.dialog-box {
.el-button {
width: 100%;
margin-top: 15px;
}
}
}
.select {
position: absolute;
right: 30px;
width: 100px;
opacity: 0;
pointer-events: none;
}
.loading {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.8);
left: 0;
z-index: 1000;
top: 0;
width: 100%;
height: 100%;
}
.loading-show {
transition: all 200ms ease 200ms;
opacity: 1;
}
.loading-hide {
pointer-events: none;
transition: all 200ms;
opacity: 0;
}
.setting-icon {
position: relative;
top: 6px;
}
.btn {
width: 100%;
}
:deep(.el-pagination .el-select) {
width: 100px;
background: #FFF;
}
:deep(.el-input-group__append) {
padding: 0 !important;
padding-left: 8px !important;
background: #FFFFFF;
}
:deep(.el-dialog) {
width: 400px !important;
@media (max-width: 440px) {
width: calc(100% - 40px) !important;
margin-right: 20px !important;
margin-left: 20px !important;
}
}
:deep(.cell) {
white-space: normal;
overflow: visible;
text-overflow: clip;
}
:deep(.receive .cell) {
white-space: nowrap;
}
:deep(.send .cell) {
white-space: nowrap;
}
:deep(.account .cell) {
white-space: nowrap;
}
:deep(.el-table th.el-table__cell>.cell.highlight) {
color: #909399;
}
:deep(.el-table__inner-wrapper:before) {
background: #fff;
}
:deep(.el-message-box__container) {
align-items: start;
}
</style>