新增邮件发送和管理员功能
This commit is contained in:
21
mail-vue/src/views/404/index.vue
Normal file
21
mail-vue/src/views/404/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
411
mail-vue/src/views/role/index.vue
Normal file
411
mail-vue/src/views/role/index.vue
Normal 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>
|
||||
82
mail-vue/src/views/send/index.vue
Normal file
82
mail-vue/src/views/send/index.vue
Normal 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>
|
||||
@@ -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";
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
|
||||
216
mail-vue/src/views/sys-email/index.vue
Normal file
216
mail-vue/src/views/sys-email/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
973
mail-vue/src/views/user/index.vue
Normal file
973
mail-vue/src/views/user/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user