新增深色模式

This commit is contained in:
eoao
2025-08-22 00:13:56 +08:00
parent d3325f0fe3
commit ff8ba81661
180 changed files with 1867 additions and 3346 deletions

View File

@@ -144,7 +144,7 @@
</div>
</div>
</div>
<div class="loading" :class="loading ? 'loading-show' : 'loading-hide'">
<div class="loading" :class="loading ? 'loading-show' : 'loading-hide'" :style="firstLoad ? 'background: transparent' : ''">
<Loading/>
</div>
<div class="follow-loading" v-if="followLoading">
@@ -535,12 +535,11 @@ function loadData() {
<style lang="scss" scoped>
.email-container {
border-radius: 8px;
display: grid;
grid-template-rows: auto 1fr;
padding: 0;
font-size: 14px;
color: #2e2e2e;
color: var(--el-text-color-primary);
overflow: hidden;
height: 100%;
}
@@ -582,7 +581,7 @@ function loadData() {
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.8);
background: var(--loadding-background);
height: 100%;
width: 100%;
position: absolute;
@@ -607,7 +606,7 @@ function loadData() {
display: flex;
padding: 8px 0;
justify-content: space-between;
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
box-shadow: var(--header-actions-border);
cursor: pointer;
align-items: center;
position: relative;
@@ -621,7 +620,7 @@ function loadData() {
column-gap: 10px;
margin-top: 5px;
margin-bottom: 5px;
color: rgba(25, 41, 59, 0.4);
color: var(--email-scroll-content-color);
.user, .account {
overflow: hidden;
@@ -683,8 +682,8 @@ function loadData() {
}
.email-sender {
font-weight: bold;
color: #1a1a1a;
font-weight: bold;;
color: var(--el-text-color-primary);
display: grid;
grid-template-columns: auto 1fr auto;
@@ -714,7 +713,6 @@ function loadData() {
.phone-time {
font-weight: normal;
font-size: 12px;
color: #333 !important;
@media (min-width: 1200px) {
display: none;
}
@@ -722,7 +720,6 @@ function loadData() {
}
.email-text {
color: #333;
display: grid;
grid-template-columns: auto 1fr;
@media (max-width: 1199px) {
@@ -743,7 +740,7 @@ function loadData() {
white-space: nowrap;
text-overflow: ellipsis;
padding-left: 10px;
color: rgba(25, 41, 59, 0.4);
color: var(--email-scroll-content-color);
@media (max-width: 1199px) {
padding-left: 0;
margin-top: 0;
@@ -760,14 +757,13 @@ function loadData() {
display: flex;
padding-left: 15px;
align-items: center;
color: #333;
@media (max-width: 1199px) {
display: none;
}
}
&:hover {
background-color: #F2F6FC;
background-color: var(--email-hover-background);
z-index: 0;
}
@@ -815,9 +811,7 @@ function loadData() {
align-items: center;
gap: 15px;
padding: 3px 15px;
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
box-shadow: var(--header-actions-border);
.header-left {
display: flex;
flex-wrap: wrap;
@@ -825,6 +819,7 @@ function loadData() {
column-gap: 18px;
row-gap: 8px;
padding-left: 2px;
color: var(--el-text-color-primary);;
}
.header-right {
@@ -832,7 +827,7 @@ function loadData() {
grid-template-columns: auto auto;
align-items: start;
height: 100%;
color: var(--el-text-color-primary);;
.email-count {
white-space: nowrap;
margin-top: 6px;

View File

@@ -57,6 +57,7 @@ function updateContent() {
}
.shadow-content {
background: #FFFFFF;
width: fit-content;
height: fit-content;
min-width: 100%;

View File

@@ -1,15 +1,18 @@
<template>
<div class="editor-box" :class="showLoading ? 'editor-box-loading' : ''">
<loading class="loading" v-if="showLoading" />
<loading class="loading" v-if="showLoading"/>
<textarea v-else style="outline: none" :id="editorId" ref="editorRef"></textarea>
</div>
</template>
<script setup>
import {ref, onMounted, onBeforeUnmount, watch, nextTick, shallowRef, defineEmits} from 'vue';
import {ref, onMounted, onBeforeUnmount, watch, nextTick, shallowRef, defineEmits, computed} from 'vue';
import loading from "@/components/loading/index.vue";
import {compressImage} from "@/utils/file-utils.js";
import { useI18n } from 'vue-i18n'
import {useI18n} from 'vue-i18n'
import {useUiStore} from '@/store/ui.js'
import {useSettingStore} from '@/store/setting.js'
defineExpose({
clearEditor,
focus,
@@ -28,12 +31,14 @@ const props = defineProps({
});
const { locale } = useI18n()
const {locale} = useI18n()
const emit = defineEmits(['change']);
const editor = shallowRef(null);
const isInitialized = ref(false);
const editorRef = ref(null);
const showLoading = ref(false);
const uiStore = useUiStore();
const settingStore = useSettingStore();
onMounted(() => {
initTinyMCE();
@@ -49,6 +54,23 @@ watch(() => props.defValue, (newValue) => {
}
});
watch(() => [uiStore.dark, settingStore.lang], () => {
destroyEditor();
initEditor();
});
const language = computed(() => {
if (locale.value === 'zh') {
return 'zh_CN'
}
if (locale.value === 'zhTW') {
return 'zh_TW'
}
return 'en'
})
function clearEditor() {
if (editor.value) {
editor.value.setContent('');
@@ -75,6 +97,12 @@ function initEditor() {
height: "100%",
auto_focus: true,
forced_root_block: 'div',
skin: `${uiStore.dark ? 'oxide-dark' : 'oxide'}`,
content_css: `/tinymce/css/index.css,${uiStore.dark ? 'dark' : 'default'}`,
content_style: `:root {
--scrollbar-track-color: ${uiStore.dark ? '#141414' : '#FFFFFF'};
--scrollbar-thumb-color: ${uiStore.dark ? '#8D9095' : '#A8ABB2'};
}`,
plugins: 'link image advlist lists emoticons fullscreen table preview code',
toolbar: 'bold emoticons forecolor backcolor italic fontsize | alignleft aligncenter alignright alignjustify | outdent indent | bullist numlist | link image | table code preview fullscreen',
toolbar_mode: 'scrolling',
@@ -83,37 +111,11 @@ function initEditor() {
},
font_size_formats: '8px 10px 12px 14px 16px 18px 24px 36px',
emoticons_search: false,
language: locale.value === 'zh' ? 'zh_CN' : 'en',
language_url: '/tinymce/langs/zh_CN.js',
language: language.value,
language_load: true,
menubar: false,
license_key: 'gpl',
noneditable_class: 'mceNonEditable',
content_style: ` .tox-dialog__body-content { margin: 0 !important; }
img { max-width: 100% !important; height: auto !important; }
body {margin: 10px 8px 0 5px !important; font-family: 'HarmonyOS'; font-size: 14px;}
@media (pointer: fine) and (hover: hover) {
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
cursor: pointer;
}
}
.mce-item-table:not([border]), .mce-item-table:not([border]) caption, .mce-item-table:not([border]) td, .mce-item-table:not([border]) th, .mce-item-table[border="0"], .mce-item-table[border="0"] caption, .mce-item-table[border="0"] td, .mce-item-table[border="0"] th, table[style*="border-width: 0px"], table[style*="border-width: 0px"] caption, table[style*="border-width: 0px"] td, table[style*="border-width: 0px"] th {
border: none;
}
`,
setup: (ed) => {
editor.value = ed;
ed.on('init', () => {
@@ -122,7 +124,7 @@ function initEditor() {
});
ed.on('input change', () => {
const content = ed.getContent();
const text = ed.getContent({ format: 'text' });
const text = ed.getContent({format: 'text'});
emit('change', content, text);
});
},
@@ -133,7 +135,7 @@ function initEditor() {
image_description: false,
link_title: false,
dialog_type: 'none',
file_picker_callback: (callback, value, meta) => {
file_picker_callback: (callback, value, meta) => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
@@ -144,12 +146,12 @@ function initEditor() {
const reader = new FileReader();
reader.onload = () => {
const id = 'blobid' + (new Date()).getTime();
const blobCache = tinymce.activeEditor.editorUpload.blobCache;
const blobCache = tinymce.activeEditor.editorUpload.blobCache;
const base64 = reader.result.split(',')[1];
const blobInfo = blobCache.create(id, file, base64);
blobCache.add(blobInfo);
callback(blobInfo.blobUri(), { title: file.name });
callback(blobInfo.blobUri(), {title: file.name});
}
reader.readAsDataURL(file);
});
@@ -202,7 +204,7 @@ function destroyEditor() {
padding-right: 15px;
padding-left: 15px;
padding-bottom: 15px;
background: #FFFFFF;
background: var(--el-bg-color);
@media (max-width: 767px) {
padding-right: 10px;
padding-left: 10px;
@@ -228,4 +230,4 @@ function destroyEditor() {
display: none;
}
</style>
</style>

View File

@@ -1,6 +1,7 @@
import {useUserStore} from "@/store/user.js";
import {useSettingStore} from "@/store/setting.js";
import {useAccountStore} from "@/store/account.js";
import {useUiStore} from "@/store/ui.js";
import {loginUserInfo} from "@/request/my.js";
import {permsToRouter} from "@/perm/perm.js";
import router from "@/router";
@@ -24,6 +25,10 @@ export async function init() {
i18n.global.locale.value = settingStore.lang
const uiStore = useUiStore();
let doc = document.querySelector("html");
doc.setAttribute('class', uiStore.dark ? 'dark' : '');
let setting = null;
if (token) {

View File

@@ -40,7 +40,7 @@
<el-skeleton v-for="i in 3" :key="i" animated>
<template #template>
<el-card class="item">
<el-skeleton-item variant="p" style="width: 70%; height: 20px; margin-bottom: 20px"/>
<el-skeleton-item variant="p" style="width: 70%; height: 20px; margin-bottom: 25px"/>
<div style="display: flex; justify-content: space-between">
<el-skeleton-item variant="text" style="width: 20px"/>
<el-skeleton-item variant="text" style="width: 20px"/>
@@ -92,7 +92,7 @@
:value="item"
/>
</el-select>
<div style="color: #333">
<div>
<span>{{ addForm.suffix }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
@@ -428,7 +428,7 @@ path[fill="#ffdda1"] {
.account-box {
border-right: 1px solid var(--el-border-color) !important;
background-color: #FFF;
background-color: var(--el-bg-color);
height: 100%;
overflow: hidden;
@@ -436,7 +436,7 @@ path[fill="#ffdda1"] {
display: flex;
align-items: center;
height: 38px;
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
box-shadow: var(--header-actions-border);
padding-left: 10px;
padding-right: 10px;
@@ -487,7 +487,7 @@ path[fill="#ffdda1"] {
}
.item {
background-color: #fff;
background-color: var(--el-bg-color);
border-radius: 8px;
padding: 12px 10px;
margin-bottom: 10px;
@@ -532,7 +532,7 @@ path[fill="#ffdda1"] {
}
.item-choose {
background: var(--el-color-primary-light-8);
background: var(--choose-account-background);
}
}
@@ -545,7 +545,7 @@ path[fill="#ffdda1"] {
:deep(.el-input-group__append) {
padding: 0 !important;
padding-left: 8px !important;
background: #FFFFFF;
background: var(--el-bg-color);
}
:deep(.el-dialog) {
@@ -567,7 +567,7 @@ path[fill="#ffdda1"] {
:deep(.el-pagination .el-select) {
width: 100px;
background: #FFF;
background: var(--el-bg-color);
}
.add-email-turnstile {

View File

@@ -143,15 +143,15 @@ const route = useRoute();
:deep(.el-scrollbar__wrap--hidden-default ) {
background: #001529 !important;
background: var(--aside-backgound) !important;
}
:deep(.el-menu-item) {
background: #001529;
background: var(--aside-backgound);
}
:deep(.el-menu) {
background: #001529;
background: var(--aside-backgound);
}
.el-menu {
@@ -163,22 +163,11 @@ const route = useRoute();
}
:deep(.el-divider__text) {
background: #001529;
background: var(--aside-backgound);
color: #FFFFFF;
}
.scroll {
}
.github {
position: absolute;
width: 100%;
bottom: 10px;
display: flex;
justify-content: center;
a{
color: #fff;
}
}
</style>

View File

@@ -5,14 +5,14 @@
<span class="breadcrumb-item">{{ $t(route.meta.title) }}</span>
</div>
<div v-perm="'email:send'" class="writer-box" @click="openSend">
<div class="writer" >
<Icon icon="material-symbols:edit-outline-sharp" width="22" height="22" />
<div class="writer">
<Icon icon="material-symbols:edit-outline-sharp" width="22" height="22"/>
</div>
</div>
<div class="toolbar">
<el-dropdown>
<div class="translate icon-item">
<Icon icon="carbon:ibm-watson-language-translator" />
<Icon icon="carbon:ibm-watson-language-translator"/>
</div>
<template #dropdown>
<el-dropdown-menu>
@@ -21,15 +21,21 @@
</el-dropdown-menu>
</template>
</el-dropdown>
<div class="notice icon-item" @click="openNotice">
<Icon icon="streamline-plump:announcement-megaphone" />
<div v-if="uiStore.dark" class="sun-icon icon-item" @click="openDark($event)">
<Icon icon="mingcute:sun-fill"/>
</div>
<el-dropdown :teleported="false" popper-class="detail-dropdown" >
<div v-else class="dark-icon icon-item" @click="openDark($event)">
<Icon icon="solar:moon-linear"/>
</div>
<div class="notice icon-item" @click="openNotice">
<Icon icon="streamline-plump:announcement-megaphone"/>
</div>
<el-dropdown :teleported="false" popper-class="detail-dropdown">
<div class="avatar">
<div class="avatar-text">
<div>{{ formatName(userStore.user.email) }}</div>
</div>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="24" height="24" />
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="24" height="24"/>
</div>
<template #dropdown>
<div class="user-details">
@@ -37,35 +43,38 @@
{{ formatName(userStore.user.email) }}
</div>
<div class="user-name">
{{userStore.user.name}}
{{ userStore.user.name }}
</div>
<div class="detail-email" @click="copyEmail(userStore.user.email)">
{{ userStore.user.email }}
</div>
<div class="detail-user-type">
<el-tag >{{$t(userStore.user.role.name)}}</el-tag>
<el-tag>{{ $t(userStore.user.role.name) }}</el-tag>
</div>
<div class="action-info">
<div>
<span style="margin-right: 10px">{{$t('sendCount')}}</span>
<span style="margin-right: 10px">{{$t('accountCount')}}</span>
<span style="margin-right: 10px">{{ $t('sendCount') }}</span>
<span style="margin-right: 10px">{{ $t('accountCount') }}</span>
</div>
<div>
<div>
<span v-if="sendCount" style="margin-right: 5px" >{{ sendCount }}</span>
<el-tag v-if="!hasPerm('email:send')" >{{sendType}}</el-tag>
<el-tag v-else >{{sendType}}</el-tag>
<span v-if="sendCount" style="margin-right: 5px">{{ sendCount }}</span>
<el-tag v-if="!hasPerm('email:send')">{{ sendType }}</el-tag>
<el-tag v-else>{{ sendType }}</el-tag>
</div>
<div>
<el-tag v-if="settingStore.settings.manyEmail || settingStore.settings.addEmail" >{{$t('disabled')}}</el-tag>
<span v-else-if="accountCount && hasPerm('account:add')" style="margin-right: 5px">{{ $t('totalUserAccount',{msg: accountCount}) }}</span>
<el-tag v-else-if="!accountCount && hasPerm('account:add')" >{{$t('unlimited')}}</el-tag>
<el-tag v-else-if="!hasPerm('account:add')" >{{$t('unauthorized')}}</el-tag>
<el-tag v-if="settingStore.settings.manyEmail || settingStore.settings.addEmail">
{{ $t('disabled') }}
</el-tag>
<span v-else-if="accountCount && hasPerm('account:add')"
style="margin-right: 5px">{{ $t('totalUserAccount', {msg: accountCount}) }}</span>
<el-tag v-else-if="!accountCount && hasPerm('account:add')">{{ $t('unlimited') }}</el-tag>
<el-tag v-else-if="!hasPerm('account:add')">{{ $t('unauthorized') }}</el-tag>
</div>
</div>
</div>
<div class="logout">
<el-button type="primary" :loading="logoutLoading" @click="clickLogout">{{$t('logOut')}}</el-button>
<el-button type="primary" :loading="logoutLoading" @click="clickLogout">{{ $t('logOut') }}</el-button>
</div>
</div>
</template>
@@ -77,17 +86,18 @@
<script setup>
import router from "@/router";
import hanburger from '@/components/hamburger/index.vue'
import { logout} from "@/request/login.js";
import {logout} from "@/request/login.js";
import {Icon} from "@iconify/vue";
import {useUiStore} from "@/store/ui.js";
import {useUserStore} from "@/store/user.js";
import { useRoute } from "vue-router";
import {useRoute} from "vue-router";
import {computed, ref} from "vue";
import {useSettingStore} from "@/store/setting.js";
import { hasPerm } from "@/perm/perm.js"
import {hasPerm} from "@/perm/perm.js"
import {useI18n} from "vue-i18n";
import {setExtend} from "@/utils/day.js"
const { t } = useI18n();
const {t} = useI18n();
const route = useRoute();
const settingStore = useSettingStore();
const userStore = useUserStore();
@@ -174,6 +184,44 @@ function openNotice() {
uiStore.showNotice()
}
function openDark(e) {
const nextIsDark = !uiStore.dark
const root = document.documentElement
if (!document.startViewTransition) {
switchDark(nextIsDark, root);
return
}
const x = e.clientX
const y = e.clientY
const maxX = Math.max(x, window.innerWidth - x)
const maxY = Math.max(y, window.innerHeight - y)
const endRadius = Math.hypot(maxX, maxY)
// 标记切换目标,供 CSS 选择器使用
root.setAttribute('data-theme-to', nextIsDark ? 'dark' : 'light')
root.style.setProperty('--vt-x', `${x}px`)
root.style.setProperty('--vt-y', `${y}px`)
root.style.setProperty('--vt-end-radius', `${endRadius + 10}px`)
const transition = document.startViewTransition(() => {
switchDark(nextIsDark, root);
})
transition.finished.finally(() => {
// 清理标记
root.removeAttribute('data-theme-to')
})
}
function switchDark(nextIsDark, root) {
root.setAttribute('class', nextIsDark ? 'dark' : '')
uiStore.dark = nextIsDark
}
function openSend() {
uiStore.writerRef.open()
}
@@ -197,7 +245,11 @@ function formatName(email) {
}
</script>
<style>
.detail-dropdown {
color: var(--el-text-color-primary) !important;
}
</style>
<style lang="scss" scoped>
.breadcrumb-item {
@@ -213,10 +265,10 @@ function formatName(email) {
.user-details {
width: 250px;
font-size: 14px;
color: #333;
display: grid;
grid-template-columns: 1fr;
justify-items: center;
.user-name {
font-weight: bold;
margin-top: 10px;
@@ -228,6 +280,7 @@ function formatName(email) {
text-overflow: ellipsis;
text-align: center;
}
.detail-user-type {
margin-top: 10px;
}
@@ -237,16 +290,19 @@ function formatName(email) {
display: grid;
grid-template-columns: auto auto;
margin-top: 10px;
>div:first-child {
> div:first-child {
display: grid;
align-items: center;
gap: 10px;
}
>div:last-child {
> div:last-child {
display: grid;
gap: 10px;
text-align: center;
>div {
> div {
display: flex;
align-items: center;
}
@@ -261,26 +317,31 @@ function formatName(email) {
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
color: #5c5958;
color: var(--regular-text-color);
cursor: pointer;
}
.logout {
margin-top: 20px;
width: 100%;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 10px;
.el-button {
border-radius: 6px;
height: 28px;
width: 100%;
}
}
.details-avatar {
margin-top: 20px;
height: 40px;
width: 40px;
border: 1px solid #ccc;
background: var(--el-bg-color);
color: var(--el-text-color-primary);
border: 1px solid var(--dark-border);
font-size: 18px;
display: flex;
align-items: center;
@@ -290,7 +351,6 @@ function formatName(email) {
}
.header {
text-align: right;
font-size: 12px;
@@ -310,8 +370,9 @@ function formatName(email) {
align-items: center;
justify-content: center;
margin-left: 5px;
.writer {
width: 34px;
width: 34px;
height: 34px;
border-radius: 50%;
color: #ffffff;
@@ -320,6 +381,7 @@ function formatName(email) {
display: flex;
align-items: center;
justify-content: center;
.writer-text {
margin-left: 15px;
font-size: 14px;
@@ -334,8 +396,6 @@ function formatName(email) {
height: 100%;
}
.toolbar {
display: flex;
justify-content: end;
@@ -343,6 +403,7 @@ function formatName(email) {
@media (max-width: 767px) {
gap: 10px;
}
.icon-item {
align-self: center;
width: 30px;
@@ -355,7 +416,7 @@ function formatName(email) {
}
.icon-item:hover {
background: #F0F2F5;
background: var(--base-fill);
}
.notice {
@@ -363,6 +424,14 @@ function formatName(email) {
margin-right: 4px;
}
.dark-icon {
font-size: 20px;
}
.sun-icon {
font-size: 24px;
}
.translate {
padding-top: 2px;
font-size: 21px;
@@ -372,14 +441,17 @@ function formatName(email) {
display: flex;
align-items: center;
cursor: pointer;
.avatar-text {
background: var(--el-bg-color);
color: var(--el-text-color-primary);
height: 30px;
width: 30px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
border: 1px solid #ccc;
border: 1px solid var(--dark-border);
}
.setting-icon {
@@ -391,6 +463,7 @@ function formatName(email) {
}
}
.el-tooltip__trigger:first-child:focus-visible {
outline: unset;
}

View File

@@ -60,8 +60,8 @@ onBeforeUnmount(() => {
}
.aside-show {
-webkit-box-shadow: 3px 0 5px rgba(0, 21, 41, .35);
box-shadow: 3px 0 5px rgba(0, 21, 41, 0.35);
-webkit-box-shadow: var(--aside-right-border);
box-shadow: var(--aside-right-border);
transform: translateX(0);
transition: all 100ms ease;
z-index: 101;
@@ -71,7 +71,7 @@ onBeforeUnmount(() => {
left: 0;
z-index: 101;
height: 100%;
background: #fff;
background: var(--el-bg-color);
}
}
@@ -91,7 +91,7 @@ onBeforeUnmount(() => {
.main-container {
min-height: 100%;
background: #FFFFFF;
background: var(--el-bg-color);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
@@ -101,8 +101,8 @@ onBeforeUnmount(() => {
}
.el-header {
background: #FFFFFF;
border-bottom: solid 1px var(--el-menu-border-color);
background: var(--el-bg-color);
border-bottom: solid 1px var(--el-border-color);
padding: 0 0 0 0;
}

View File

@@ -164,7 +164,7 @@ const handleResize = () => {
.main-view {
background: #FFFFFF;
background: var(--el-bg-color);
}
@@ -176,7 +176,7 @@ const handleResize = () => {
align-items: center;
width: 100%;
.tag {
background: #FFFFFF;
background: var(--el-bg-color);
margin-left: 5px;
}
}

View File

@@ -436,7 +436,7 @@ function close() {
justify-content: center;
.write-box {
background: #FFFFFF;
background: var(--el-bg-color);
width: min(1200px,calc(100% - 80px));
box-shadow: var(--el-box-shadow-light);
border: 1px solid var(--el-border-color-light);

View File

@@ -5,6 +5,7 @@ import './style.css';
import { init } from '@/init/init.js';
import { createPinia } from 'pinia';
import piniaPersistedState from 'pinia-plugin-persistedstate';
import 'element-plus/theme-chalk/dark/css-vars.css'
import perm from "@/perm/perm.js";
const pinia = createPinia().use(piniaPersistedState)
import i18n from "@/i18n/index.js";

View File

@@ -1,6 +1,5 @@
import {createRouter, createWebHistory} from 'vue-router'
import {useUiStore} from "@/store/ui.js";
import {replace} from "lodash-es";
const routes = [
{
@@ -91,7 +90,7 @@ router.beforeEach(async (to, from, next) => {
}
if (token && to.name === 'login') {
next(from.path)
return next(from.path)
}
next()

View File

@@ -10,6 +10,7 @@ export const useUiStore = defineStore('ui', {
changePreview: 0,
previewData: {},
key: 0,
dark: false,
asideCount: {
email: 0,
send: 0,
@@ -26,6 +27,6 @@ export const useUiStore = defineStore('ui', {
}
},
persist: {
pick: ['accountShow'],
pick: ['accountShow','dark'],
},
})
})

View File

@@ -1,4 +1,3 @@
* {
margin: 0;
padding: 0;
@@ -15,26 +14,20 @@ html, body {
height: 100%;
}
@font-face {
font-family: 'HarmonyOS';
src: url('@/assets/fonts/HarmonyOS_Sans_SC_Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
:deep(.el-input__inner:focus) {
background-color: transparent !important;
border-color: #dcdfe6 !important;
}
.el-table thead th {
font-weight: bold !important;
}
body {
font-family: 'HarmonyOS', -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif;
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
line-height: 1.5;
color: #333;
background-color: #fff;
color: var(--el-text-color-primary);
background-color: var(--el-bg-color);
font-size: 14px;
}
@@ -64,6 +57,27 @@ button, input, select, textarea {
outline: none;
}
.el-message {
white-space: nowrap !important;
}
.message-bottom {
top: auto !important;
bottom: 20px !important;
.el-icon {
display: none !important;
}
}
.el-input-group__append, .el-input-group__prepend {
color: var(--el-text-color-primary) !important;
}
.tox .tox-dialog__iframe.tox-dialog__iframe--opaque {
background-color: var(--el-bg-color) !important;
}
.tox .tox-dialog--width-lg {
height: 850px !important;
@media (max-width: 1024px) {
@@ -89,10 +103,6 @@ button, input, select, textarea {
max-height: 220px !important;
}
.el-message {
white-space: nowrap !important;
}
:root {
--el-color-primary: #1890ff;
--el-color-primary-dark-2: #1064c0;
@@ -100,17 +110,65 @@ button, input, select, textarea {
--el-color-primary-light-5: #69c0ff;
--el-color-primary-light-7: #91d5ff;
--el-color-primary-light-9: #e6f7ff;
--el-text-color-regular: #333;
--extra-light-fill: #FAFCFF;
--light-ill: #F5F7FA;
--light-border: #E4E7ED;
--header-actions-border: inset 0 -1px 0 0 var(--el-border-color-lighter);
--aside-backgound: #001529;
--loadding-background: rgba(255, 255, 255, 0.8);
--dark-border: #CDD0D6;
--base-fill: #F0F2F5;
--regular-text-color: #585d69;
--light-border-color: #e7e9ec;
--aside-right-border: 3px 0 5px rgba(0, 21, 41, .35);
--choose-account-background: var(--el-color-primary-light-8);
--el-text-color-regular: var(--el-text-color-primary);
--email-scroll-content-color: rgba(25, 41, 59, 0.4);
--email-hover-background: #F2F6FC;
--login-border: none;
--form-desc-color: #71717a;
--login-switch-color: #006be6;
--scrollbar-track-color: #A8ABB2;
--base-border-color: #DCDFE6;
--secondary-text-color: #909399;
--message-block-color: rgba(0, 0, 0, 0);
--vt-x: 50%;
--vt-y: 50%;
--vt-duration: 520ms;
--vt-easing: ease-out;
}
.message-bottom {
top: auto !important;
bottom: 20px !important;
.el-icon {
display: none !important;
}
.dark {
--extra-light-fill: #191919;
--light-ill: #262727;
--light-border: #414243;
--aside-backgound: #141414;
--header-actions-border: inset 0 -1px 0 0 var(--el-border-color-lighter);
--loadding-background: rgba(0, 0, 0, 0.3);
--dark-border: #636466;
--base-fill: #303030;
--regular-text-color: #bfbdbc;
--light-border-color: #414243;
--aside-right-border: 3px 0 5px #0A0A0A;
--choose-account-background: #39393A;
--email-scroll-content-color: rgba(255, 255, 255, 0.3);
--email-hover-background: #1D1E1F;
--login-border: none;
--form-desc-color: #8D9095;
--login-switch-color: rgb(102, 177, 255);
--scrollbar-track-color: #8D9095;
--base-border-color: #4C4D4F;
--secondary-text-color: #A3A6AD;
--message-block-color: rgba(30, 30, 30, 0.2);
--el-border-color: #414243 !important;
}
.el-input {
--el-input-text-color: var(--el-text-color-primary) !important;
}
@media (pointer: fine) and (hover: hover) {
/* 整个滚动条 */
::-webkit-scrollbar {
@@ -120,14 +178,54 @@ button, input, select, textarea {
/* 滚动条轨道 */
::-webkit-scrollbar-track {
background: #f1f1f1;
background: var(--el-bg-color);
border-radius: 10px;
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
background: #888;
background: var(--scrollbar-track-color);
border-radius: 10px;
cursor: pointer;
}
}
}
/* 视图过渡,基于径向剪裁 */
html.dark,
html:not(.dark) {
view-transition-name: root;
}
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
/* 层级控制,确保对应方向动画在上层可见 */
html[data-theme-to="dark"]::view-transition-new(root) { z-index: 2; }
html[data-theme-to="dark"]::view-transition-old(root) { z-index: 1; }
html[data-theme-to="light"]::view-transition-old(root) { z-index: 2; }
html[data-theme-to="light"]::view-transition-new(root) { z-index: 1; }
/* 切到暗色:新视图从点击点向外扩散 */
html[data-theme-to="dark"]::view-transition-new(root) {
clip-path: circle(0px at var(--vt-x) var(--vt-y));
animation: vt-radial-in var(--vt-duration) var(--vt-easing) forwards;
}
/* 切到浅色:旧视图向点击点收缩 */
html[data-theme-to="light"]::view-transition-old(root) {
clip-path: circle(var(--vt-end-radius, 0px) at var(--vt-x) var(--vt-y));
animation: vt-radial-out var(--vt-duration) var(--vt-easing) forwards;
}
@keyframes vt-radial-in {
from { clip-path: circle(0px at var(--vt-x) var(--vt-y)); }
to { clip-path: circle(var(--vt-end-radius, 0px) at var(--vt-x) var(--vt-y)); }
}
@keyframes vt-radial-out {
from { clip-path: circle(var(--vt-end-radius, 0px) at var(--vt-x) var(--vt-y)); }
to { clip-path: circle(0px at var(--vt-x) var(--vt-y)); }
}

View File

@@ -34,7 +34,7 @@
<el-option key="1" :label="$t('user')" :value="'user'"/>
<el-option key="2" :label="$t('selectEmail')" :value="'account'"/>
</el-select>
<div style="color: #333;display: flex;">
<div class="search-type">
<span>{{ selectTitle }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
@@ -194,6 +194,11 @@ function getEmailList(emailId, size) {
pointer-events: none;
}
.search-type {
display: flex;
color: var(--el-text-color-regular);
}
:deep(.header-actions) {
padding-top: 8px;
padding-bottom: 8px;

View File

@@ -121,7 +121,7 @@
<script setup>
import {Icon} from "@iconify/vue";
import {useTransition} from "@vueuse/core";
import {defineOptions, onActivated, onDeactivated, onMounted, reactive, ref, watch} from "vue";
import {defineOptions, onActivated, onDeactivated, onMounted, reactive, ref, watch, computed} from "vue";
import echarts from "@/echarts/index.js";
import dayjs from "dayjs";
import {analysisEcharts} from "@/request/analysis.js";
@@ -185,6 +185,17 @@ const emailColumnData = {
daysData: []
}
const topic = computed(() => ({
color: uiStore.dark ? '#E5EAF3' : '#303133',
background: uiStore.dark ? '#141414' : '#FFFFFF',
borderColor: uiStore.dark ? '#141414' : '#FFFFFF',
scaleLineColor: uiStore.dark ? '#636466' : '#CDD0D6',
crossColor: uiStore.dark ? '#8D9095' : '#A8ABB2',
axisColor: uiStore.dark ? '#A3A6AD' : '#909399',
splitLineColor: uiStore.dark ? '#58585B' : '#D4D7DE',
gaugeSplitLine: uiStore.dark ? '#CFD3DC' : '#606266',
containerBackground: uiStore.dark ? '#6C6E72' : '#E6EBF8'
}))
let daySendTotal = 0
let leaveWidth = 0
let senderPie = null
@@ -194,6 +205,7 @@ let sendGauge = null
let first = true
let boxKey = ref(0)
let senderPieLeft = window.innerWidth < 500 ? `${window.innerWidth - 110}` : '72%'
let analysisDark = uiStore.dark
onMounted(() => {
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -232,17 +244,6 @@ onMounted(() => {
})
function initPicture() {
if (route.name !== 'analysis') return
boxKey.value++
setTimeout(() => {
createSenderPie()
createIncreaseLine()
createEmailColumnChart();
createSendGauge();
})
}
const widthChange = debounce(initPicture, 500, {
leading: false,
trailing: true
@@ -261,6 +262,9 @@ onActivated(() => {
widthChange()
} else if (!senderPie) {
widthChange()
} else if (analysisDark !== uiStore.dark) {
initPicture()
analysisDark = uiStore.dark
}
})
@@ -273,6 +277,23 @@ window.onresize = () => {
widthChange()
}
watch(() => uiStore.dark, () => {
if (route.name !== 'analysis') return
analysisDark = uiStore.dark
initPicture()
})
function initPicture() {
if (route.name !== 'analysis') return
boxKey.value++
setTimeout(() => {
createSenderPie()
createIncreaseLine()
createEmailColumnChart();
createSendGauge();
})
}
function setStyle() {
senderPieLeft = window.innerWidth < 500 ? `${window.innerWidth - 110}` : '72%'
emailColumnData.barWidth = window.innerWidth > 767 ? '40%' : '60%'
@@ -301,11 +322,14 @@ function createSenderPie() {
if (senderPie) {
senderPie.dispose()
}
senderPie = echarts.init(document.querySelector(".sender-pie"))
let option = {
tooltip: {
trigger: 'item',
textStyle: {
color: topic.value.color
},
backgroundColor: topic.value.background,
formatter: params => {
return `${params.marker} ${params.name} ${params.value} (${params.percent}%)`;
}
@@ -315,6 +339,9 @@ function createSenderPie() {
orient: 'vertical',
left: '10',
top: '20',
textStyle: {
color: topic.value.color
},
formatter: function (name) {
return truncateTextByWidth(name)
}
@@ -329,7 +356,7 @@ function createSenderPie() {
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderColor: topic.value.borderColor,
borderWidth: 2
},
label: {
@@ -370,7 +397,10 @@ function createIncreaseLine() {
axisPointer: {
type: 'cross', // 指示器的类型为交叉型,适用于折线图等
crossStyle: {
color: '#999' // 设置指示器线的颜色
color: topic.value.crossColor// 设置指示器线的颜色
},
lineStyle: {
color: topic.value.crossColor // ← 竖线颜色
},
axis: 'x',
},
@@ -381,12 +411,12 @@ function createIncreaseLine() {
});
return result;
},
backgroundColor: '#fff', // 设置背景颜色
borderColor: '#ccc', // 设置边框颜色
backgroundColor: topic.value.background, // 设置背景颜色
borderColor: topic.value.splitLineColor, // 设置边框颜色
borderWidth: 1, // 设置边框宽度
padding: 10, // 设置内边距
textStyle: {
color: '#333', // 设置文字颜色
color: topic.value.color, // 设置文字颜色
}
},
grid: {
@@ -402,7 +432,7 @@ function createIncreaseLine() {
show: false,
alignWithLabel: false, // 刻度线与标签对齐,
lineStyle: {
color: 'red',
color: topic.value.axisColor,
}
},
axisPointer: {
@@ -412,7 +442,7 @@ function createIncreaseLine() {
},
axisLine: {
lineStyle: {
color: '#909399',
color: topic.value.axisColor,
width: 1,
type: 'solid'
}
@@ -427,6 +457,7 @@ function createIncreaseLine() {
}
return value;
},
},
boundaryGap: false,
},
@@ -444,7 +475,7 @@ function createIncreaseLine() {
axisLine: {
show: true,
lineStyle: {
color: '#909399',
color: topic.value.axisColor,
width: 1,
}
},
@@ -460,7 +491,7 @@ function createIncreaseLine() {
show: true, // 显示网格线
lineStyle: {
type: 'dashed', // 设置网格线为虚线
color: '#ccc' // 设置虚线的颜色
color: topic.value.scaleLineColor // 设置虚线的颜色
}
}
},
@@ -519,6 +550,10 @@ function createEmailColumnChart() {
const option = {
tooltip: {
textStyle: {
color: topic.value.color
},
backgroundColor: topic.value.background,
formatter: function (params) {
params.marker
return `${params.marker} ${params.seriesName}: ${params.value}`
@@ -526,7 +561,10 @@ function createEmailColumnChart() {
},
legend: {
data: [t('emailReceived'), t('emailSent')],
top: '0'
top: '0',
textStyle: {
color: topic.value.color, // 图例文字颜色
}
},
grid: {
left: '18',
@@ -539,12 +577,12 @@ function createEmailColumnChart() {
type: 'category',
data: emailColumnData.daysData,
axisTick: {
show: false
show: false,
},
axisLine: {
show: true,
lineStyle: {
color: '#909399',
color: topic.value.axisColor,
width: 1,
}
},
@@ -555,6 +593,21 @@ function createEmailColumnChart() {
return 10
}
},
splitLine: {
show: true,
lineStyle: {
color: topic.value.splitLineColor, // ← 横线颜色
type: 'solid', // dashed=虚线solid=实线
width: 1
}
},
axisLine: {
show: true,
lineStyle: {
color: topic.value.axisColor,
width: 0,
}
},
type: 'value',
boundaryGap: [0, 0.1],
},
@@ -603,7 +656,12 @@ function createSendGauge() {
}
sendGauge = echarts.init(document.querySelector(".send-count"));
let option = {
tooltip: {},
tooltip: {
textStyle: {
color: topic.value.color
},
backgroundColor: topic.value.background
},
series: [{
name: t('sentToday'),
type: 'gauge',
@@ -622,31 +680,39 @@ function createSendGauge() {
color: '#3CB2FF'
}
},
axisLabel: {
color: topic.value.gaugeSplitLine,
},
// 轴线背景色(新增)
axisLine: {
roundCap: true,
lineStyle: {
color: [[1, '#E6EBF8']]
color: [[1, topic.value.containerBackground]]
}
},
splitLine: {
lineStyle: {
color: topic.value.gaugeSplitLine, // 大刻度线颜色
}
},
// 刻度颜色(新增)
axisTick: {
lineStyle: {
color: '#999'
color: topic.value.axisColor
}
},
// 中心文字颜色(新增)
detail: {
valueAnimation: true,
formatter: '{value}',
color: '#333' // 黑色文字
color: topic.value.color // 黑色文字
},
data: [{
value: daySendTotal,
name: t('total'),
// 名称标签颜色(新增)
title: {
color: '#333' // 灰色标签
color: topic.value.color // 灰色标签
}
}]
}],
@@ -683,7 +749,7 @@ function createSendGauge() {
height: 100%;
padding: 20px 20px 30px;
gap: 20px;
background: #FAFCFF;
background: var(--extra-light-fill);
display: grid;
grid-auto-rows: min-content;
@media (max-width: 1024px) {
@@ -727,6 +793,10 @@ function createSendGauge() {
gap: 5px;
grid-auto-rows: min-content;
> div:first-child {
font-size: 15px;
}
> div:last-child {
font-size: 13px;
}
@@ -761,18 +831,19 @@ function createSendGauge() {
justify-content: start;
gap: 20px;
padding-top: 5px;
font-size: 14px;
.normal {
width: fit-content;
color: var(--el-color-success);
font-weight: bold;
font-weight: bold;;
margin-left: 3px;
}
.deleted {
width: fit-content;
color: var(--el-color-danger);
font-weight: bold;
font-weight: bold;;
margin-left: 3px;
}
}
@@ -792,7 +863,7 @@ function createSendGauge() {
}
.picture-item {
background: #fff;
background: var(--el-bg-color);
border-radius: 8px;
border: 1px solid var(--el-border-color);
@@ -832,7 +903,7 @@ function createSendGauge() {
}
.picture-cs-item {
background: #fff;
background: var(--el-bg-color);
border-radius: 8px;
border: 1px solid var(--el-border-color);

View File

@@ -34,7 +34,7 @@
<el-alert v-if="email.status === 5" :closable="false" :title="$t('delayed')" class="email-msg" type="warning" show-icon />
</div>
<el-scrollbar class="htm-scrollbar" :class="email.attList.length === 0 ? 'bottom-distance' : ''">
<ShadowHtml :html="formatImage(email.content)" v-if="email.content" />
<ShadowHtml class="shadow-html" :html="formatImage(email.content)" v-if="email.content" />
<pre v-else class="email-text" >{{email.text}}</pre>
</el-scrollbar>
<div class="att" v-if="email.attList.length > 0">
@@ -51,7 +51,7 @@
<div class="att-name" @click="showImage(att.key)">
{{ att.filename }}
</div>
<div style="color: rgba(24, 36, 48, 0.6);">{{ formatBytes(att.size) }}</div>
<div class="att-size">{{ 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>
@@ -208,7 +208,7 @@ const handleDelete = () => {
display: flex;
align-items: center;
gap: 20px;
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
box-shadow: var(--header-actions-border);
font-size: 18px;
.star {
display: flex;
@@ -281,7 +281,7 @@ const handleDelete = () => {
padding: 5px 8px;
border-radius: 4px;
align-self: start;
border: 1px solid #e7e9ec;
border: 1px solid var(--base-border-color);
display: grid;
grid-template-columns: auto 1fr auto auto;
gap: 10px;
@@ -290,6 +290,10 @@ const handleDelete = () => {
display: grid;
}
.att-size {
color: var(--secondary-text-color);
}
.att-name {
margin-right: 10px;
white-space: nowrap;
@@ -305,13 +309,13 @@ const handleDelete = () => {
}
.opt-icon {
color: rgba(24, 36, 48, 0.6);
color: var(--secondary-text-color);
align-items: center;
display: flex;
gap: 8px;
cursor: pointer;
a {
color: rgba(24, 36, 48, 0.6);
color: var(--secondary-text-color);
align-items: center;
display: flex;
}
@@ -321,14 +325,14 @@ const handleDelete = () => {
.email-info {
border-bottom: 1px solid #e7e9ec;
border-bottom: 1px solid var(--light-border-color);
margin-bottom: 20px;
padding-bottom: 8px;
@media (max-width: 1024px) {
margin-bottom: 15px;
}
.date {
color: #585d69;
color: var(--regular-text-color);
margin-bottom: 6px;
}
@@ -343,7 +347,7 @@ const handleDelete = () => {
margin-bottom: 6px;
.send-name {
color: #585d69;
color: var(--regular-text-color);
display: flex;
flex-wrap: wrap;
}
@@ -361,7 +365,7 @@ const handleDelete = () => {
word-break: break-word;
}
span:nth-child(2) {
color: #585d69;
color: var(--regular-text-color);
}
}
@@ -380,6 +384,17 @@ const handleDelete = () => {
}
}
.shadow-html::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--message-block-color); /* 半透明黑色蒙层 */
pointer-events: none; /* 不影响点击 */
}
.email-text {
font-family: inherit;
white-space: pre-wrap;

View File

@@ -32,7 +32,7 @@
:value="item"
/>
</el-select>
<div style="color: #333">
<div style="color: var(--el-text-color-primary)">
<span>{{ suffix }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
@@ -64,7 +64,7 @@
:value="item"
/>
</el-select>
<div style="color: #333">
<div>
<span>{{ suffix }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
@@ -178,7 +178,8 @@ window.loadBefore = (e) => {
}
const loginOpacity = computed(() => {
return `rgba(255, 255, 255, ${settingStore.settings.loginOpacity})`
const opacity = settingStore.settings.loginOpacity
return uiStore.dark ? `rgba(0, 0, 0, ${opacity})` : `rgba(255, 255, 255, ${opacity})`
})
const background = computed(() => {
@@ -415,7 +416,7 @@ function submitRegister() {
justify-content: center;
width: 450px;
height: 100%;
border: 1px solid #e4e7ed;
border-left: 1px solid var(--login-border);
box-shadow: var(--el-box-shadow-light);
@media (max-width: 1024px) {
padding: 20px 18px;
@@ -423,6 +424,7 @@ function submitRegister() {
margin-left: 18px;
}
@media (max-width: 767px) {
border: 1px solid var(--login-border);
padding: 20px 18px;
border-radius: 6px;
height: fit-content;
@@ -440,7 +442,7 @@ function submitRegister() {
.form-desc {
margin-top: 5px;
margin-bottom: 18px;
color: #71717a;
color: var(--form-desc-color);
}
.form-title {
@@ -453,17 +455,19 @@ function submitRegister() {
text-align: center;
span {
color: #006be6;
color: var(--login-switch-color);
cursor: pointer;
}
}
:deep(.el-input__wrapper) {
border-radius: 6px;
background: var(--el-bg-color);
}
.email-input :deep(.el-input__wrapper) {
border-radius: 6px 0 0 6px;
background: var(--el-bg-color);
}
.el-input {
@@ -490,7 +494,7 @@ function submitRegister() {
padding: 0 !important;
padding-left: 8px !important;
padding-right: 4px !important;
background: #FFFFFF;
background: var(--el-bg-color);
border-radius: 0 8px 8px 0;
}
@@ -518,7 +522,6 @@ function submitRegister() {
#login-box {
background: linear-gradient(to bottom, #2980b9, #6dd5fa, #fff);
color: #333;
font: 100% Arial, sans-serif;
height: 100%;
margin: 0;

View File

@@ -15,40 +15,40 @@
<Icon class="icon" icon="fluent:broom-sparkle-16-regular" width="22" height="22" @click="clearNotUse"/>
</div>
<el-scrollbar class="scrollbar" :style="`background: ${regKeyData.length > 0 ? '#FAFCFF;' : '#FFF'}`">
<div class="loading" :class="regKeyLoading ? 'loading-show' : 'loading-hide'">
<loading />
<el-scrollbar class="scrollbar">
<div class="loading" :class="regKeyLoading ? 'loading-show' : 'loading-hide'" :style="regKeyFirst ? 'background: transparent' : ''">
<loading/>
</div>
<div class="code-box">
<div class="code-item" v-for="item in regKeyData">
<div class="code-info">
<div class="info-left">
<div class="info-left-item">
<span class="code" @click="copyCode(item.code)">{{item.code}}</span>
<span class="code" @click="copyCode(item.code)">{{ item.code }}</span>
</div>
<div class="info-left-item">
<div>{{$t('remainingUses')}}</div>
<div v-if="item.count">{{item.count}}</div>
<el-tag v-else type="danger">{{$t('exhausted')}}</el-tag>
<div>{{ $t('remainingUses') }}</div>
<div v-if="item.count">{{ item.count }}</div>
<el-tag v-else type="danger">{{ $t('exhausted') }}</el-tag>
</div>
<div class="info-left-item">
<div>{{$t('roleDesc')}}</div>
<el-tag>{{item.roleName}}</el-tag>
<div>{{ $t('roleDesc') }}</div>
<el-tag>{{ item.roleName }}</el-tag>
</div>
<div class="info-left-item">
<div>{{$t('validUntil')}}</div>
<div v-if="item.expireTime">{{ formatExpireTime(item.expireTime)}}</div>
<el-tag v-else type="danger">{{$t('expired')}}</el-tag>
<div>{{ $t('validUntil') }}</div>
<div v-if="item.expireTime">{{ formatExpireTime(item.expireTime) }}</div>
<el-tag v-else type="danger">{{ $t('expired') }}</el-tag>
</div>
</div>
<div class="info-right">
<el-dropdown class="setting">
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399" />
<template #dropdown >
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399"/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="copyCode(item.code)">{{$t('copy')}}</el-dropdown-item>
<el-dropdown-item @click="openHistory(item)">{{$t('history')}}</el-dropdown-item>
<el-dropdown-item @click="deleteRegKey(item)">{{$t('delete')}}</el-dropdown-item>
<el-dropdown-item @click="copyCode(item.code)">{{ $t('copy') }}</el-dropdown-item>
<el-dropdown-item @click="openHistory(item)">{{ $t('history') }}</el-dropdown-item>
<el-dropdown-item @click="deleteRegKey(item)">{{ $t('delete') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -64,7 +64,7 @@
<div class="container">
<el-input v-model="addForm.code" :placeholder="$t('regKey')">
<template #suffix>
<Icon @click.stop="genCode" class="gen-code" icon="bitcoin-icons:refresh-filled" width="24" height="24" />
<Icon @click.stop="genCode" class="gen-code" icon="bitcoin-icons:refresh-filled" width="24" height="24"/>
</template>
</el-input>
<el-select v-model="addForm.roleId" :placeholder="$t('roleDesc')">
@@ -77,31 +77,33 @@
/>
<el-input-number v-model="addForm.count" :min="1" :max="99999"/>
<el-button class="btn" type="primary" @click="submit" :loading="addLoading"
>{{$t('add')}}
>{{ $t('add') }}
</el-button>
</div>
</el-dialog>
<el-dialog class="history-list" v-model="showRegKeyHistory" :title="$t('useHistory')">
<div class="loading" :class="historyLoading ? 'loading-show' : 'loading-hide'">
<loading />
<loading/>
</div>
<el-table v-if="!historyLoading" :data="historyList" :fit="true" style="height: 100%" >
<el-table-column :min-width="emailColumnWidth" property="email" :label="$t('user')" :show-overflow-tooltip="true" />
<el-table-column :width="createTimeColumnWidth" :formatter="formatUserCreateTime" property="createTime" :label="$t('date')" fixed="right" :show-overflow-tooltip="true" />
<el-table v-if="!historyLoading" :data="historyList" :fit="true" style="height: 100%">
<el-table-column :min-width="emailColumnWidth" property="email" :label="$t('user')"
:show-overflow-tooltip="true"/>
<el-table-column :width="createTimeColumnWidth" :formatter="formatUserCreateTime" property="createTime"
:label="$t('date')" fixed="right" :show-overflow-tooltip="true"/>
</el-table>
</el-dialog>
</div>
</template>
<script setup>
import {defineOptions, reactive, ref, watch} from "vue"
import {defineOptions, nextTick, reactive, ref, watch} from "vue"
import {Icon} from "@iconify/vue";
import loading from "@/components/loading/index.vue";
import {useSettingStore} from "@/store/setting.js";
import {roleSelectUse} from "@/request/role.js";
import {useRoleStore} from "@/store/role.js";
import {regKeyAdd, regKeyList, regKeyClearNotUse, regKeyDelete, regKeyHistory} from "@/request/reg-key.js";
import { getTextWidth } from "@/utils/text.js";
import {getTextWidth} from "@/utils/text.js";
import dayjs from "dayjs";
import {tzDayjs} from "@/utils/day.js";
import {useI18n} from "vue-i18n";
@@ -116,7 +118,7 @@ const params = reactive({
code: '',
})
const { t } = useI18n()
const {t} = useI18n()
const roleList = reactive([])
const addLoading = ref(false)
const showAdd = ref(false)
@@ -193,15 +195,7 @@ function formatUserCreateTime(regKey) {
const currentYear = dayjs().year();
const expireYear = createTime.year();
if(settingStore.lang === 'zh') {
if (expireYear === currentYear) {
return createTime.format('M月D日 HH:mm');
} else {
return createTime.format('YYYY年M月D日 HH:mm');
}
} else {
if (settingStore.lang === 'en') {
if (expireYear === currentYear) {
return createTime.format('MMM D, HH:mm');
@@ -209,6 +203,14 @@ function formatUserCreateTime(regKey) {
return createTime.format('MMM D, YYYY HH:mm');
}
} else {
if (expireYear === currentYear) {
return createTime.format('M月D日 HH:mm');
} else {
return createTime.format('YYYY年M月D日 HH:mm');
}
}
}
@@ -218,16 +220,18 @@ function formatExpireTime(expireTime) {
const currentYear = dayjs().year();
const expireYear = expireDate.year();
if (settingStore.lang === 'zh') {
return expireYear === currentYear
? expireDate.format('M月D日')
: expireDate.format('YYYY年M月D日');
} else {
if (settingStore.lang === 'en') {
return expireYear === currentYear
? expireDate.format('MMM D')
: expireDate.format('MMM D, YYYY');
} else {
return expireYear === currentYear
? expireDate.format('M月D日')
: expireDate.format('YYYY年M月D日');
}
}
@@ -248,7 +252,9 @@ function getList(showLoading = false) {
regKeyData.length = 0
regKeyData.push(...list)
regKeyLoading.value = false
regKeyFirst.value = false
setTimeout(() => {
regKeyFirst.value = false
},200)
})
}
@@ -353,8 +359,8 @@ function submit() {
})
}
function deleteRegKey(regKey){
ElMessageBox.confirm(t('delConfirm',{msg: regKey.code}), {
function deleteRegKey(regKey) {
ElMessageBox.confirm(t('delConfirm', {msg: regKey.code}), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
@@ -370,7 +376,7 @@ function deleteRegKey(regKey){
});
}
function resetForm(){
function resetForm() {
addForm.code = ''
}
@@ -390,30 +396,37 @@ function openAdd() {
.scrollbar {
height: calc(100% - 48px);
position: relative;
background: var(--extra-light-fill);
@media (max-width: 372px) {
height: calc(100% - 85px);
}
.code-box {
padding: 15px 15px 25px 15px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 15px;
.code-item {
background-color: #fff;
background: var(--el-bg-color);
border-radius: 8px;
border: 1px solid var(--el-border-color);
transition: all 300ms;
transition: all 200ms;
padding: 15px;
.code-info {
display: flex;
.info-left {
flex: 1;
min-width: 0;
.info-left-item {
display: flex;
padding-top: 5px;
.code {
font-weight: bold;
font-weight: bold;;
font-size: 16px;
white-space: nowrap;
overflow: hidden;
@@ -479,7 +492,7 @@ function openAdd() {
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.8);
background: var(--loadding-background);
z-index: 2;
}
@@ -529,6 +542,7 @@ function openAdd() {
@media (max-width: 767px) {
gap: 15px;
}
.search-input {
width: min(200px, calc(100vw - 140px));
}
@@ -549,7 +563,7 @@ function openAdd() {
}
:deep(.el-table__inner-wrapper:before) {
background: #fff;
background: var(--el-bg-color);
}
</style>
</style>

View File

@@ -1,78 +1,82 @@
<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" />
<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="$t('role')" 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" >{{$t('default')}}</el-tag></span>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('order')" :width="sortWidth" prop="sort"/>
<el-table-column v-if="desShow" :label="$t('description')" 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="$t('tabSetting')" :width="settingWidth">
<template #default="props">
<el-dropdown trigger="click">
<el-button size="small" type="primary">{{$t('action')}}</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openRoleSet(props.row)">{{$t('change')}}</el-dropdown-item>
<el-dropdown-item @click="setDef(props.row)">{{$t('default')}}</el-dropdown-item>
<el-dropdown-item @click="delRole(props.row)">{{$t('delete')}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<el-scrollbar class="perm-scrollbar">
<div class="loading" :class="tableLoading ? 'loading-show' : 'loading-hide'"
:style="first ? 'background: transparent' : ''">
<loading/>
</div>
<el-table
:data="roles"
style="height: 100%;"
:empty-text="''"
>
<el-table-column width="10"/>
<el-table-column :label="$t('role')" 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">{{ $t('default') }}</el-tag></span>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('order')" :width="sortWidth" prop="sort"/>
<el-table-column v-if="desShow" :label="$t('description')" 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="$t('tabSetting')" :width="settingWidth">
<template #default="props">
<el-dropdown trigger="click">
<el-button size="small" type="primary">{{ $t('action') }}</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openRoleSet(props.row)">{{ $t('change') }}</el-dropdown-item>
<el-dropdown-item @click="setDef(props.row)">{{ $t('default') }}</el-dropdown-item>
<el-dropdown-item @click="delRole(props.row)">{{ $t('delete') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</el-scrollbar>
<el-dialog top="5vh" class="dialog" v-model="roleFormShow" @closed="resetForm">
<template #header>
<span style="font-size: 18px">{{dialogType.title}}</span>
<span style="font-size: 18px">{{ dialogType.title }}</span>
<el-popover
width ="340"
width="340"
:title="t('featDesc')"
placement="bottom"
>
<template #reference>
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</template>
<div style="font-weight: bold;margin-bottom: 2px;">{{t('emailInterception')}}</div>
<div>{{t('emailInterceptionDesc')}}</div>
<div style="font-weight: bold;margin-top: 10px;margin-bottom: 2px;">{{t('availableDomains')}}</div>
<div style="font-weight: bold;;margin-bottom: 2px;">{{ t('emailInterception') }}</div>
<div>{{ t('emailInterceptionDesc') }}</div>
<div style="font-weight: bold;;margin-top: 10px;margin-bottom: 2px;">{{ t('availableDomains') }}</div>
<div>
{{t('availableDomainsDesc')}}
{{ t('availableDomainsDesc') }}
</div>
</el-popover>
</template>
<div class="dialog-box">
<el-input class="dialog-input" v-model="form.name" type="text" :maxlength="12" :placeholder="$t('roleName')" autocomplete="off" />
<el-input class="dialog-input" v-model="form.description" :maxlength="30" type="text" :placeholder="$t('description')" autocomplete="off" />
<el-input-tag class="dialog-input-tag" tag-type="warning" :class="form.banEmail.length === 0 ? 'dialog-input' : '' " v-model="form.banEmail" @add-tag="banEmailAddTag" type="text" :placeholder="$t('emailInterception')" autocomplete="off" />
<el-input class="dialog-input" v-model="form.name" type="text" :maxlength="12" :placeholder="$t('roleName')"
autocomplete="off"/>
<el-input class="dialog-input" v-model="form.description" :maxlength="30" type="text"
:placeholder="$t('description')" autocomplete="off"/>
<el-input-tag class="dialog-input-tag" tag-type="warning"
:class="form.banEmail.length === 0 ? 'dialog-input' : '' " v-model="form.banEmail"
@add-tag="banEmailAddTag" type="text" :placeholder="$t('emailInterception')" autocomplete="off"/>
<el-radio-group class="dialog-radio" v-model="form.banEmailType" v-if="form.banEmail.length > 0">
<el-radio :label="$t('removeAll')" :value="0" />
<el-radio :label="$t('removeContent')" :value="1" />
<el-radio :label="$t('removeAll')" :value="0"/>
<el-radio :label="$t('removeContent')" :value="1"/>
</el-radio-group>
<el-select
class="dialog-input"
@@ -94,11 +98,12 @@
/>
</el-select>
<div class="dialog-input">
<el-input-number :placeholder="$t('order')" :min="0" :max="9999" v-model.number="form.sort" controls-position="right" autocomplete="off" />
<el-input-number :placeholder="$t('order')" :min="0" :max="9999" v-model.number="form.sort"
controls-position="right" autocomplete="off"/>
</div>
<el-radio-group v-model="expand" size="small" @change="expandChange" class="perm-expand">
<el-radio-button :label="$t('expand')" :value="true" />
<el-radio-button :label="$t('collapse')" :value="false" />
<el-radio-button :label="$t('expand')" :value="true"/>
<el-radio-button :label="$t('collapse')" :value="false"/>
</el-radio-group>
<el-tree
:expand-on-click-node="false"
@@ -114,25 +119,28 @@
>
<template #default="{ node, data }">
<div>
<span>{{node.label}}</span>
<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="$t('total')" >
<el-input-number v-model="form.sendCount" controls-position="right" :min="0" :max="99999" size="small"
:placeholder="$t('total')">
</el-input-number>
<el-select v-model="form.sendType" placeholder="Select" size="small" style="width: 65px;margin-left: 5px;">
<el-option :label="$t('total')" value="count" />
<el-option :label="$t('daily')" value="day" />
<el-option :label="$t('btnBan')" value="ban" />
<el-select v-model="form.sendType" placeholder="Select" size="small"
style="width: 65px;margin-left: 5px;">
<el-option :label="$t('total')" value="count"/>
<el-option :label="$t('daily')" value="day"/>
<el-option :label="$t('btnBan')" value="ban"/>
</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="$t('total')" >
<el-input-number v-model="form.accountCount" controls-position="right" :min="0" :max="99999"
size="small" :placeholder="$t('total')">
</el-input-number>
</span>
</div>
</template>
</el-tree>
<el-button class="btn" type="primary" :loading="permLoading" @click="roleFormClick"
>{{$t('save')}}
>{{ $t('save') }}
</el-button>
</div>
</el-dialog>
@@ -153,8 +161,8 @@ defineOptions({
name: 'role'
})
const { domainList } = useSettingStore();
const { t, locale } = useI18n();
const {domainList} = useSettingStore();
const {t, locale} = useI18n();
const userStore = useUserStore();
const roleStore = useRoleStore();
const roleFormShow = ref(false)
@@ -167,6 +175,7 @@ const desShow = ref(true)
const settingWidth = ref(null)
const sortWidth = ref(null)
const roleWidth = ref(200)
const first = ref(true)
const dialogType = reactive({
title: '',
@@ -200,7 +209,7 @@ rolePermTree().then(tree => {
domainOptions = domainList.map(domain => {
const cleanDomain = domain.replace(/^@/, '');
return { label: cleanDomain, value: cleanDomain };
return {label: cleanDomain, value: cleanDomain};
});
@@ -209,7 +218,7 @@ function availDomainChange() {
return !domainOptions.map(option => option.value).includes(domain)
})
if (index > -1) {
form.availDomain.splice(index,1)
form.availDomain.splice(index, 1)
}
}
@@ -292,7 +301,7 @@ function setRole() {
return
}
const params = {...form,roleId: chooseRole.roleId}
const params = {...form, roleId: chooseRole.roleId}
const checkedId = tree.value.getCheckedKeys()
const halfId = tree.value.getHalfCheckedKeys()
params.permIds = [...checkedId, ...halfId]
@@ -390,6 +399,9 @@ function getRoleList() {
roles.value = list
}).finally(() => {
tableLoading.value = false
setTimeout(() => {
first.value = false
}, 200)
})
}
@@ -414,6 +426,7 @@ window.onresize = () => {
height: 100%;
overflow: hidden;
width: 100%;
.perm-scrollbar {
height: 100%;
}
@@ -421,6 +434,7 @@ window.onresize = () => {
.send-num {
margin-left: 10px;
.el-input-number {
width: 95px;
}
@@ -436,16 +450,19 @@ window.onresize = () => {
display: flex;
align-items: center;
gap: 18px;
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
box-shadow: var(--header-actions-border);
font-size: 18px;
.search {
:deep(.el-input-group) {
height: 28px;
}
:deep(.el-input__inner) {
height: 28px;
}
}
.icon {
cursor: pointer;
}
@@ -466,10 +483,25 @@ window.onresize = () => {
}
.loading {
height: 100%;
height: calc(100% - 41px);
width: 100%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
background: var(--loadding-background);
z-index: 2;
}
.loading-show {
transition: all 200ms ease 200ms;
opacity: 1;
}
.loading-hide {
pointer-events: none;
transition: all 200ms;
opacity: 0;
}
.role-name {
@@ -494,15 +526,17 @@ window.onresize = () => {
.dialog-input {
margin-bottom: 15px !important;
}
.dialog-radio {
margin-top: 5px;
margin-bottom: 5px;
}
.dialog-input-tag {
}
}
.perm-expand {
.perm-expand {
margin-bottom: 5px;
--el-border-radius-base: 4px;
position: relative;
@@ -520,8 +554,13 @@ window.onresize = () => {
}
}
:deep(.el-scrollbar__view) {
height: 100%;
}
.btn {
width: 100%;
margin-top: 15px;
}
</style>
</style>

View File

@@ -32,7 +32,7 @@
</div>
<div class="del-email" v-perm="'my:delete'">
<div class="title">{{$t('deleteUser')}}</div>
<div style="color: #585d69;">
<div style="color: var(--regular-text-color);">
{{$t('delAccountMsg')}}
</div>
<div>

View File

@@ -1,31 +1,31 @@
<template>
<div class="settings-container">
<div v-if="firstLoading" class="loading">
<loading />
<loading/>
</div>
<el-scrollbar class="scroll" v-else >
<el-scrollbar class="scroll" v-else>
<div class="scroll-body">
<div class="card-grid">
<!-- Website Settings Card -->
<div class="settings-card">
<div class="card-title">{{$t('websiteSetting')}}</div>
<div class="card-title">{{ $t('websiteSetting') }}</div>
<div class="card-content">
<div class="setting-item">
<div><span>{{$t('websiteReg')}}</span></div>
<div><span>{{ $t('websiteReg') }}</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>{{$t('loginDomain')}}</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.loginDomain"/>
</div>
</div>
<div class="setting-item">
<div><span>{{$t('regKey')}}</span></div>
<div><span>{{ $t('loginDomain') }}</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.loginDomain"/>
</div>
</div>
<div class="setting-item">
<div><span>{{ $t('regKey') }}</span></div>
<div>
<el-select
@change="change"
@@ -43,7 +43,7 @@
</div>
</div>
<div class="setting-item">
<div><span>{{$t('addAccount')}}</span></div>
<div><span>{{ $t('addAccount') }}</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.addEmail"/>
@@ -51,7 +51,7 @@
</div>
<div class="setting-item">
<div>
<span>{{$t('multipleEmail')}}</span>
<span>{{ $t('multipleEmail') }}</span>
<el-tooltip effect="dark" :content="$t('multipleEmailDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
@@ -63,7 +63,7 @@
</div>
<div class="setting-item">
<div>
<span>{{$t('physicallyWipeData')}}</span>
<span>{{ $t('physicallyWipeData') }}</span>
<el-tooltip effect="dark" :content="$t('physicallyWipeDataDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
@@ -80,10 +80,10 @@
<!-- Personalization Settings Card -->
<div class="settings-card">
<div class="card-title">{{$t('customization')}}</div>
<div class="card-title">{{ $t('customization') }}</div>
<div class="card-content">
<div class="setting-item">
<div class="title-item"><span>{{$t('websiteTitle')}}</span></div>
<div class="title-item"><span>{{ $t('websiteTitle') }}</span></div>
<div class="email-title">
<span>{{ setting.title }}</span>
<el-button class="opt-button" size="small" type="primary" @click="editTitleShow = true">
@@ -92,13 +92,14 @@
</div>
</div>
<div class="setting-item">
<div class="title-item"><span>{{$t('loginBoxOpacity')}}</span></div>
<div class="title-item"><span>{{ $t('loginBoxOpacity') }}</span></div>
<div>
<el-input-number size="small" v-model="loginOpacity" @change="opacityChange" :precision="2" :step="0.01" :max="1" :min="0" />
<el-input-number size="small" v-model="loginOpacity" @change="opacityChange" :precision="2"
:step="0.01" :max="1" :min="0"/>
</div>
</div>
<div class="setting-item personalized">
<div><span>{{$t('loginBackground')}}</span></div>
<div><span>{{ $t('loginBackground') }}</span></div>
<div>
<el-image
class="background"
@@ -128,10 +129,10 @@
<!-- Email Sending Settings Card -->
<div class="settings-card">
<div class="card-title">{{$t('emailSetting')}}</div>
<div class="card-title">{{ $t('emailSetting') }}</div>
<div class="card-content">
<div class="setting-item">
<div><span>{{$t('receiveEmail')}}</span></div>
<div><span>{{ $t('receiveEmail') }}</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.receive"/>
@@ -139,7 +140,7 @@
</div>
<div class="setting-item">
<div>
<span>{{$t('autoRefresh')}}</span>
<span>{{ $t('autoRefresh') }}</span>
<el-tooltip effect="dark" :content="$t('autoRefreshDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
@@ -161,7 +162,7 @@
</div>
</div>
<div class="setting-item">
<div><span>{{$t('sendEmail')}}</span></div>
<div><span>{{ $t('sendEmail') }}</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.send"/>
@@ -169,7 +170,7 @@
</div>
<div class="setting-item">
<div>
<span>{{$t('noRecipientTitle')}}</span>
<span>{{ $t('noRecipientTitle') }}</span>
<el-tooltip effect="dark" :content="$t('noRecipientDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
@@ -180,12 +181,14 @@
</div>
</div>
<div class="setting-item">
<div><span>{{$t('resendToken')}}</span></div>
<div><span>{{ $t('resendToken') }}</span></div>
<div>
<el-button class="opt-button" style="margin-top: 0" @click="openResendList" size="small" type="primary">
<el-button class="opt-button" style="margin-top: 0" @click="openResendList" size="small"
type="primary">
<Icon icon="ic:round-list" width="18" height="18"/>
</el-button>
<el-button class="opt-button" style="margin-top: 0" @click="openResendForm" size="small" type="primary">
<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>
@@ -195,10 +198,10 @@
<!-- R2 Object Storage Card -->
<div class="settings-card">
<div class="card-title">{{$t('R2OS')}}</div>
<div class="card-title">{{ $t('R2OS') }}</div>
<div class="card-content">
<div class="setting-item">
<div><span>{{$t('osDomain')}}</span></div>
<div><span>{{ $t('osDomain') }}</span></div>
<div class="r2domain">
<span>{{ setting.r2Domain || '' }}</span>
<el-button class="opt-button" size="small" type="primary" @click="r2DomainShow = true">
@@ -210,10 +213,10 @@
</div>
<div class="settings-card">
<div class="card-title">{{$t('emailPush')}}</div>
<div class="card-title">{{ $t('emailPush') }}</div>
<div class="card-content">
<div class="setting-item">
<div><span>{{$t('tgBot')}}</span></div>
<div><span>{{ $t('tgBot') }}</span></div>
<div class="forward">
<span>{{ setting.tgBotStatus === 0 ? $t('enabled') : $t('disabled') }}</span>
<el-button class="opt-button" size="small" type="primary" @click="openTgSetting">
@@ -222,7 +225,7 @@
</div>
</div>
<div class="setting-item">
<div><span>{{$t('otherEmail')}}</span></div>
<div><span>{{ $t('otherEmail') }}</span></div>
<div class="forward">
<span>{{ setting.forwardStatus === 0 ? $t('enabled') : $t('disabled') }}</span>
<el-button class="opt-button" size="small" type="primary" @click="openThirdEmailSetting">
@@ -231,7 +234,7 @@
</div>
</div>
<div class="setting-item">
<div><span>{{$t('forwardingRules')}}</span></div>
<div><span>{{ $t('forwardingRules') }}</span></div>
<div class="forward">
<span>{{ setting.ruleType === 0 ? $t('forwardAll') : $t('rules') }}</span>
<el-button class="opt-button" size="small" type="primary" @click="openForwardRules">
@@ -244,10 +247,10 @@
<!-- Turnstile Verification Card -->
<div class="settings-card">
<div class="card-title">{{$t('turnstileSetting')}}</div>
<div class="card-title">{{ $t('turnstileSetting') }}</div>
<div class="card-content">
<div class="setting-item">
<div><span>{{$t('signUpVerification')}}</span></div>
<div><span>{{ $t('signUpVerification') }}</span></div>
<div>
<el-button class="opt-button" size="small" type="primary" @click="openRegVerifyCount">
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
@@ -259,14 +262,14 @@
placeholder="Select"
class="bot-verify-select"
>
<el-option key="1" :value="0" :label="$t('enable')" />
<el-option key="1" :value="1" :label="$t('disable')" />
<el-option key="1" :value="2" :label="$t('rulesVerify')" />
<el-option key="1" :value="0" :label="$t('enable')"/>
<el-option key="1" :value="1" :label="$t('disable')"/>
<el-option key="1" :value="2" :label="$t('rulesVerify')"/>
</el-select>
</div>
</div>
<div class="setting-item">
<div><span>{{$t('addEmailVerification')}}</span></div>
<div><span>{{ $t('addEmailVerification') }}</span></div>
<div>
<el-button class="opt-button" size="small" type="primary" @click="openAddVerifyCount">
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
@@ -278,9 +281,9 @@
placeholder="Select"
class="bot-verify-select"
>
<el-option key="1" :value="0" :label="$t('enable')" />
<el-option key="1" :value="1" :label="$t('disable')" />
<el-option key="1" :value="2" :label="$t('rulesVerify')" />
<el-option key="1" :value="0" :label="$t('enable')"/>
<el-option key="1" :value="1" :label="$t('disable')"/>
<el-option key="1" :value="2" :label="$t('rulesVerify')"/>
</el-select>
</div>
</div>
@@ -306,10 +309,10 @@
</div>
<div class="settings-card">
<div class="card-title">{{$t('noticeTitle')}}</div>
<div class="card-title">{{ $t('noticeTitle') }}</div>
<div class="card-content">
<div class="setting-item">
<div><span>{{$t('noticePopup')}}</span></div>
<div><span>{{ $t('noticePopup') }}</span></div>
<div class="forward">
<span>{{ setting.notice === 0 ? $t('enabled') : $t('disabled') }}</span>
<el-button class="opt-button" size="small" type="primary" @click="openNoticePopupSetting">
@@ -318,7 +321,7 @@
</div>
</div>
<div class="setting-item">
<div><span>{{$t('popUp')}}</span></div>
<div><span>{{ $t('popUp') }}</span></div>
<div class="forward">
<el-button class="opt-button" size="small" type="primary" @click="openNoticePopup">
<Icon icon="mynaui:click-solid" width="18" height="18"/>
@@ -329,18 +332,25 @@
</div>
<div class="settings-card about">
<div class="card-title">{{$t('about')}}</div>
<div class="card-title">{{ $t('about') }}</div>
<div class="card-content">
<div class="concerning-item">
<span>{{$t('version')}} :</span>
<span>v1.7.0</span>
<span>{{ $t('version') }} :</span>
<el-badge is-dot :hidden="!hasUpdate">
<el-button @click="jump('https://github.com/eoao/cloud-mail/releases')">
{{ currentVersion }}
<template #icon>
<Icon icon="qlementine-icons:version-control-16" style="font-size: 20px" color="#1890FF"/>
</template>
</el-button>
</el-badge>
</div>
<div class="concerning-item">
<span>{{$t('community')}} : </span>
<span>{{ $t('community') }} : </span>
<el-button @click="jump('https://github.com/eoao/cloud-mail')">
Github
<template #icon>
<Icon icon="codicon:github-inverted" width="22" height="22" />
<Icon icon="codicon:github-inverted" width="22" height="22"/>
</template>
</el-button>
<el-button @click="jump('https://t.me/cloud_mail_tg')">
@@ -351,23 +361,23 @@
</el-button>
</div>
<div class="concerning-item">
<span>{{$t('support')}} : </span>
<el-button @click="jump('https://doc.skymail.ink/support.html')" >
{{t('supportDesc')}}
<span>{{ $t('support') }} : </span>
<el-button @click="jump('https://doc.skymail.ink/support.html')">
{{ t('supportDesc') }}
<template #icon>
<Icon color="#79D6B5" icon="simple-icons:buymeacoffee" width="20" height="20" />
<Icon color="#79D6B5" icon="simple-icons:buymeacoffee" width="20" height="20"/>
</template>
</el-button>
</div>
<div class="concerning-item">
<span>{{ $t('help') }} : </span>
<el-button @click="jump('https://doc.skymail.ink')">
{{ t('document') }}
<template #icon>
<Icon color="#79D6B5" icon="fluent-color:document-32" width="18" height="18"/>
</template>
</el-button>
</div>
<div class="concerning-item">
<span>{{$t('help')}} : </span>
<el-button @click="jump('https://doc.skymail.ink')" >
{{t('document')}}
<template #icon>
<Icon color="#79D6B5" icon="fluent-color:document-32" width="18" height="18" />
</template>
</el-button>
</div>
</div>
</div>
</div>
@@ -377,7 +387,7 @@
<el-dialog v-model="editTitleShow" :title="$t('changeTitle')" width="340" @closed="editTitle = setting.title">
<form>
<el-input type="text" :placeholder="$t('websiteTitle')" v-model="editTitle"/>
<el-button type="primary" :loading="settingLoading" @click="saveTitle">{{$t('save')}}</el-button>
<el-button type="primary" :loading="settingLoading" @click="saveTitle">{{ $t('save') }}</el-button>
</form>
</el-dialog>
<el-dialog v-model="resendTokenFormShow" :title="$t('resendToken')" width="340" @closed="cleanResendTokenForm">
@@ -391,13 +401,14 @@
/>
</el-select>
<el-input type="text" :placeholder="$t('addResendTokenDesc')" v-model="resendTokenForm.token"/>
<el-button type="primary" :loading="settingLoading" @click="saveResendToken">{{$t('save')}}</el-button>
<el-button type="primary" :loading="settingLoading" @click="saveResendToken">{{ $t('save') }}</el-button>
</form>
</el-dialog>
<el-dialog v-model="r2DomainShow" :title="$t('addOsDomain')" width="340" @closed="r2DomainInput = setting.r2Domain">
<el-dialog v-model="r2DomainShow" :title="$t('addOsDomain')" width="340"
@closed="r2DomainInput = setting.r2Domain">
<form>
<el-input type="text" :placeholder="$t('domainDesc')" v-model="r2DomainInput"/>
<el-button type="primary" :loading="settingLoading" @click="saveR2domain">{{$t('save')}}</el-button>
<el-button type="primary" :loading="settingLoading" @click="saveR2domain">{{ $t('save') }}</el-button>
</form>
</el-dialog>
<el-dialog v-model="turnstileShow" :title="$t('addTurnstileSecret')" width="340"
@@ -405,7 +416,7 @@
<form>
<el-input type="text" placeholder="Site Key" v-model="turnstileForm.siteKey"/>
<el-input type="text" style="margin-top: 15px" placeholder="Secret Key" v-model="turnstileForm.secretKey"/>
<el-button type="primary" :loading="settingLoading" @click="saveTurnstileKey">{{$t('save')}}</el-button>
<el-button type="primary" :loading="settingLoading" @click="saveTurnstileKey">{{ $t('save') }}</el-button>
</form>
</el-dialog>
<el-dialog
@@ -415,16 +426,17 @@
>
<template #header>
<span style="font-size: 18px">
{{$t('backgroundTitle')}}
{{ $t('backgroundTitle') }}
<el-tooltip>
<template #content>
<span>{{$t('backgroundWarning')}}</span>
<span>{{ $t('backgroundWarning') }}</span>
</template>
<Icon class="title-icon warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</span>
</template>
<el-input :placeholder="$t('backgroundUrlDesc')" v-model="backgroundUrl" v-if="!localUpShow" class="background-url" />
<el-input :placeholder="$t('backgroundUrlDesc')" v-model="backgroundUrl" v-if="!localUpShow"
class="background-url"/>
<el-image
v-if="localUpShow"
:preview-src-list="[backgroundImage]"
@@ -435,12 +447,12 @@
></el-image>
<div class="cut-button">
<el-button type="primary" link @click="openCut" v-if="!localUpShow">
{{$t('localUpload')}}
{{ $t('localUpload') }}
</el-button>
<el-button type="primary" link @click="localUpShow = false" v-if="localUpShow">
{{$t('imageLink')}}
{{ $t('imageLink') }}
</el-button>
<el-button type="primary" :loading="settingLoading" @click="saveBackground">{{$t('save')}}</el-button>
<el-button type="primary" :loading="settingLoading" @click="saveBackground">{{ $t('save') }}</el-button>
</div>
</el-dialog>
<el-dialog
@@ -449,7 +461,7 @@
>
<template #header>
<div class="forward-head">
<span class="forward-set-title">{{$t('tgBot')}}</span>
<span class="forward-set-title">{{ $t('tgBot') }}</span>
<el-tooltip effect="dark" :content="$t('tgBotDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
@@ -457,13 +469,15 @@
</template>
<div class="forward-set-body">
<el-input :placeholder="$t('tgBotToken')" v-model="tgBotToken"></el-input>
<el-input-tag tag-type="warning" :placeholder="$t('toBotTokenDesc')" v-model="tgChatId" @add-tag="addChatTag" ></el-input-tag>
<el-input-tag tag-type="warning" :placeholder="$t('toBotTokenDesc')" v-model="tgChatId"
@add-tag="addChatTag"></el-input-tag>
</div>
<template #footer>
<div class="dialog-footer">
<el-switch v-model="tgBotStatus" :active-value="0" :inactive-value="1" :active-text="$t('enable')" :inactive-text="$t('disable')" />
<el-switch v-model="tgBotStatus" :active-value="0" :inactive-value="1" :active-text="$t('enable')"
:inactive-text="$t('disable')"/>
<el-button :loading="settingLoading" type="primary" @click="tgBotSave">
{{$t('save')}}
{{ $t('save') }}
</el-button>
</div>
</template>
@@ -474,20 +488,22 @@
>
<template #header>
<div class="forward-head">
<span class="forward-set-title">{{$t('otherEmail')}}</span>
<span class="forward-set-title">{{ $t('otherEmail') }}</span>
<el-tooltip effect="dark" :content="$t('otherEmailDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
</template>
<div class="forward-set-body">
<el-input-tag tag-type="warning" :placeholder="$t('otherEmailInputDesc')" v-model="forwardEmail" @add-tag="emailAddTag"></el-input-tag>
<el-input-tag tag-type="warning" :placeholder="$t('otherEmailInputDesc')" v-model="forwardEmail"
@add-tag="emailAddTag"></el-input-tag>
</div>
<template #footer>
<div class="dialog-footer">
<el-switch v-model="forwardStatus" :active-value="0" :inactive-value="1" :active-text="$t('enable')" :inactive-text="$t('disable')" />
<el-switch v-model="forwardStatus" :active-value="0" :inactive-value="1" :active-text="$t('enable')"
:inactive-text="$t('disable')"/>
<el-button :loading="settingLoading" type="primary" @click="forwardEmailSave">
{{$t('save')}}
{{ $t('save') }}
</el-button>
</div>
</template>
@@ -497,91 +513,97 @@
class="forward-dialog"
>
<template #header>
<div class="forward-head">
<span class="forward-set-title">{{$t('forwardingRules')}}</span>
<el-tooltip effect="dark" :content="$t('forwardingRulesDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
<div class="forward-head">
<span class="forward-set-title">{{ $t('forwardingRules') }}</span>
<el-tooltip effect="dark" :content="$t('forwardingRulesDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
</template>
<div class="forward-set-body">
<el-input-tag :placeholder="$t('ruleEmailsInputDesc')" tag-type="success" v-model="ruleEmail" @add-tag="ruleEmailAddTag" />
<el-input-tag :placeholder="$t('ruleEmailsInputDesc')" tag-type="success" v-model="ruleEmail"
@add-tag="ruleEmailAddTag"/>
</div>
<template #footer>
<div class="dialog-footer">
<el-radio-group v-model="ruleType">
<el-radio :value="0" >{{$t('forwardAll')}}</el-radio>
<el-radio :value="1" >{{$t('rules')}}</el-radio>
<el-radio :value="0">{{ $t('forwardAll') }}</el-radio>
<el-radio :value="1">{{ $t('rules') }}</el-radio>
</el-radio-group>
<el-button :loading="settingLoading" type="primary" @click="ruleEmailSave">
{{$t('save')}}
{{ $t('save') }}
</el-button>
</div>
</template>
</el-dialog>
<el-dialog class="resend-table" v-model="showResendList" :title="$t('resendTokenList')">
<el-table :data="resendList" >
<el-table-column :min-width="emailColumnWidth" property="key" :label="$t('domain')" :show-overflow-tooltip="true" />
<el-table-column :width="tokenColumnWidth" property="value" label="Token" fixed="right" :show-overflow-tooltip="true" />
<el-table :data="resendList">
<el-table-column :min-width="emailColumnWidth" property="key" :label="$t('domain')"
:show-overflow-tooltip="true"/>
<el-table-column :width="tokenColumnWidth" property="value" label="Token" fixed="right"
:show-overflow-tooltip="true"/>
</el-table>
</el-dialog>
<el-dialog v-model="regVerifyCountShow" :title="$t('rulesVerifyTitle',{count: regVerifyCount})" @closed="regVerifyCount = setting.regVerifyCount" >
<el-dialog v-model="regVerifyCountShow" :title="$t('rulesVerifyTitle',{count: regVerifyCount})"
@closed="regVerifyCount = setting.regVerifyCount">
<form>
<el-input-number type="text" v-model="regVerifyCount" :min="1" >
<el-input-number type="text" v-model="regVerifyCount" :min="1">
</el-input-number>
<el-button type="primary" :loading="settingLoading" @click="saveRegVerifyCount">{{$t('save')}}</el-button>
<el-button type="primary" :loading="settingLoading" @click="saveRegVerifyCount">{{ $t('save') }}</el-button>
</form>
</el-dialog>
<el-dialog v-model="addVerifyCountShow" :title="$t('rulesVerifyTitle',{count: addVerifyCount})" @closed="addVerifyCount = setting.addVerifyCount">
<el-dialog v-model="addVerifyCountShow" :title="$t('rulesVerifyTitle',{count: addVerifyCount})"
@closed="addVerifyCount = setting.addVerifyCount">
<form>
<el-input-number type="text" v-model="addVerifyCount" :min="1"/>
<el-button type="primary" :loading="settingLoading" @click="saveAddVerifyCount">{{$t('save')}}</el-button>
<el-button type="primary" :loading="settingLoading" @click="saveAddVerifyCount">{{ $t('save') }}</el-button>
</form>
</el-dialog>
<el-dialog top="5vh" v-model="noticePopupShow" :title="$t('noticePopup')" class="notice-popup" @closed="resetNoticeForm">
<el-dialog top="5vh" v-model="noticePopupShow" :title="$t('noticePopup')" class="notice-popup"
@closed="resetNoticeForm">
<form>
<el-input v-model="noticeForm.noticeTitle" :placeholder="t('titleDesc')" />
<div class="notice-line-item" >
<el-select v-model="noticeForm.noticeType" >
<el-input v-model="noticeForm.noticeTitle" :placeholder="t('titleDesc')"/>
<div class="notice-line-item">
<el-select v-model="noticeForm.noticeType">
<template #prefix>
<span style="margin-right: 10px">{{$t('icon')}}</span>
<span style="margin-right: 10px">{{ $t('icon') }}</span>
</template>
<el-option key="none" label="None" value="none" />
<el-option key="primary" label="Primary" value="primary" />
<el-option key="success" label="Success" value="success" />
<el-option key="warning" label="Warning" value="warning" />
<el-option key="info" label="Info" value="info" />
<el-option key="none" label="None" value="none"/>
<el-option key="primary" label="Primary" value="primary"/>
<el-option key="success" label="Success" value="success"/>
<el-option key="warning" label="Warning" value="warning"/>
<el-option key="info" label="Info" value="info"/>
</el-select>
<el-select v-model="noticeForm.noticePosition" >
<el-select v-model="noticeForm.noticePosition">
<template #prefix>
<span style="margin-right: 10px">{{$t('position')}}</span>
<span style="margin-right: 10px">{{ $t('position') }}</span>
</template>
<el-option key="top-left" :label="t('topLeft')" value="top-left" />
<el-option key="top-right" :label="t('topRight')" value="top-right" />
<el-option key="bottom-left" :label="t('bottomLeft')" value="bottom-left" />
<el-option key="bottom-right" :label="t('bottomRight')" value="bottom-right" />
<el-option key="top-left" :label="t('topLeft')" value="top-left"/>
<el-option key="top-right" :label="t('topRight')" value="top-right"/>
<el-option key="bottom-left" :label="t('bottomLeft')" value="bottom-left"/>
<el-option key="bottom-right" :label="t('bottomRight')" value="bottom-right"/>
</el-select>
<el-input-number v-model="noticeForm.noticeWidth" >
<template #prefix >
{{$t('width')}}
<el-input-number v-model="noticeForm.noticeWidth">
<template #prefix>
{{ $t('width') }}
</template>
<template #suffix >
<template #suffix>
px
</template>
</el-input-number>
<el-input-number v-model="noticeForm.noticeOffset" >
<template #prefix >
{{$t('offset')}}
<el-input-number v-model="noticeForm.noticeOffset">
<template #prefix>
{{ $t('offset') }}
</template>
<template #suffix >
<template #suffix>
px
</template>
</el-input-number>
<el-input-number v-model="noticeForm.noticeDuration" >
<template #prefix >
{{$t('duration')}}
<el-input-number v-model="noticeForm.noticeDuration">
<template #prefix>
{{ $t('duration') }}
</template>
<template #suffix >
<template #suffix>
ms
</template>
</el-input-number>
@@ -597,13 +619,14 @@
</form>
<template #footer>
<div class="dialog-footer">
<el-switch v-model="noticeForm.notice" :active-value="0" :inactive-value="1" :active-text="$t('enable')" :inactive-text="$t('disable')" />
<el-switch v-model="noticeForm.notice" :active-value="0" :inactive-value="1" :active-text="$t('enable')"
:inactive-text="$t('disable')"/>
<div>
<el-button @click="previewNoticePopup">
{{$t('preview')}}
{{ $t('preview') }}
</el-button>
<el-button :loading="settingLoading" type="primary" @click="saveNoticePopup">
{{$t('save')}}
{{ $t('save') }}
</el-button>
</div>
</div>
@@ -627,14 +650,18 @@ import {debounce} from 'lodash-es'
import {isEmail} from "@/utils/verify-utils.js";
import loading from "@/components/loading/index.vue";
import {getTextWidth} from "@/utils/text.js";
import { fileToBase64 } from "@/utils/file-utils.js"
import { useI18n } from 'vue-i18n';
import {fileToBase64} from "@/utils/file-utils.js"
import {useI18n} from 'vue-i18n';
import axios from "axios";
defineOptions({
name: 'sys-setting'
})
const { t, locale } = useI18n();
const currentVersion = 'v1.7.0'
const hasUpdate = ref(false)
let getUpdateErrorCount = 1;
const {t, locale} = useI18n();
const firstLoading = ref(true)
const backgroundImage = ref('')
const localUpShow = ref(false)
@@ -711,6 +738,7 @@ const ruleType = ref(0)
const ruleEmail = ref([])
getSettings()
getUpdate()
function getSettings() {
settingQuery().then(settingData => {
@@ -772,8 +800,22 @@ const resendList = computed(() => {
return list;
});
function getUpdate() {
if (getUpdateErrorCount > 5 || !getUpdateErrorCount) return
axios.get('https://api.github.com/repos/eoao/cloud-mail/releases/latest').then(({data}) => {
hasUpdate.value = data.name !== currentVersion
getUpdateErrorCount = 0
}).catch(e => {
getUpdateErrorCount++
setTimeout(() => {
getUpdate()
}, 2000)
console.error('检查更新失败:', e)
})
}
function saveAddVerifyCount() {
if(!addVerifyCount.value) {
if (!addVerifyCount.value) {
addVerifyCount.value = 1
}
editSetting({addVerifyCount: addVerifyCount.value})
@@ -832,9 +874,9 @@ function resetNoticeForm() {
}
function saveNoticePopup() {
noticeForm.noticeOffset = noticeForm.noticeOffset || 0
noticeForm.noticeWidth = noticeForm.noticeWidth || 0
noticeForm.noticeDuration = noticeForm.noticeDuration || 0
noticeForm.noticeOffset = noticeForm.noticeOffset || 0
noticeForm.noticeWidth = noticeForm.noticeWidth || 0
noticeForm.noticeDuration = noticeForm.noticeDuration || 0
editSetting({...noticeForm})
}
@@ -899,9 +941,9 @@ function addChatTag(val) {
tgChatId.value.splice(tgChatId.value.length - 1, 1)
chatIds.forEach(id => {
if (!isNaN(Number(id))) {
tgChatId.value.push(id)
}
if (!isNaN(Number(id))) {
tgChatId.value.push(id)
}
})
}
@@ -934,7 +976,7 @@ function ruleEmailSave() {
function doOpacityChange() {
const form = {}
form.loginOpacity = loginOpacity.value
editSetting(form,true)
editSetting(form, true)
}
const opacityChange = debounce(doOpacityChange, 1000, {
@@ -986,7 +1028,7 @@ async function saveBackground() {
let image = ''
if (localUpShow.value) {
image = await fileToBase64(backgroundFile,true);
image = await fileToBase64(backgroundFile, true);
} else {
if (backgroundUrl.value && !backgroundUrl.value.startsWith('http')) {
ElMessage({
@@ -1127,7 +1169,8 @@ function editSetting(settingForm, refreshStatus = true) {
.settings-container {
height: 100%;
overflow: hidden;
background: #FAFCFF !important;
background: var(--extra-light-fill) !important;
.loading {
display: flex;
align-items: center;
@@ -1139,9 +1182,11 @@ function editSetting(settingForm, refreshStatus = true) {
.scroll {
width: 100%;
min-height: 100%;
:deep(.el-scrollbar__view) {
height: 100%;
}
.scroll-body {
min-height: 100%;
display: flex;
@@ -1169,7 +1214,7 @@ function editSetting(settingForm, refreshStatus = true) {
width: 250px;
height: 140px;
border-radius: 4px;
border: 1px solid #e4e7ed;
border: 1px solid var(--light-border);
@media (max-width: 500px) {
width: 150px;
height: 83px;
@@ -1187,7 +1232,7 @@ function editSetting(settingForm, refreshStatus = true) {
}
.settings-card {
background-color: #fff;
background-color: var(--el-bg-color);
border-radius: 8px;
border: 1px solid var(--el-border-color);
transition: all 300ms;
@@ -1195,7 +1240,6 @@ function editSetting(settingForm, refreshStatus = true) {
}
.card-title {
font-size: 15px;
font-weight: bold;
@@ -1215,6 +1259,7 @@ function editSetting(settingForm, refreshStatus = true) {
grid-template-columns: auto 1fr;
gap: 10px;
font-weight: normal;
> div:first-child {
display: flex;
align-items: center;
@@ -1232,14 +1277,13 @@ function editSetting(settingForm, refreshStatus = true) {
.title-icon.warning {
position: relative;
top: 2px;
color: gray;
cursor: pointer;
margin-left: 2px;
}
.warning {
margin-left: 4px;
color: gray;
color: grey;
cursor: pointer;
}
@@ -1268,9 +1312,11 @@ function editSetting(settingForm, refreshStatus = true) {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 15px;
> * {
width: 100%;
}
@media (max-width: 840px) {
grid-template-columns: 1fr 1fr;
}
@@ -1318,7 +1364,7 @@ function editSetting(settingForm, refreshStatus = true) {
}
:deep(.el-table__inner-wrapper:before) {
background: #fff;
background: var(--el-bg-color);
}
:deep(.cut-dialog.el-dialog) {
@@ -1340,17 +1386,18 @@ function editSetting(settingForm, refreshStatus = true) {
.forward-head {
display: flex;
align-items: center;
.forward-set-title {
top: 1px;
position: relative;
font-size: 16px;
font-weight: bold;
font-weight: bold;;
}
}
}
.error-image {
background: #f5f7fa;
background: var(--light-ill);
height: 100%;
display: flex;
align-items: center;
@@ -1363,6 +1410,7 @@ function editSetting(settingForm, refreshStatus = true) {
width: 100%;
display: flex;
justify-content: space-between;
.el-button {
width: fit-content;
}
@@ -1374,6 +1422,7 @@ function editSetting(settingForm, refreshStatus = true) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
span {
display: flex;
align-items: center;
@@ -1393,6 +1442,7 @@ function editSetting(settingForm, refreshStatus = true) {
display: flex;
flex-direction: column;
gap: 15px;
.el-switch {
align-self: end;
}
@@ -1451,6 +1501,7 @@ function editSetting(settingForm, refreshStatus = true) {
:deep(.el-button) {
padding: 0 10px;
font-weight: normal;
i {
font-size: 22px;
@@ -1469,6 +1520,7 @@ function editSetting(settingForm, refreshStatus = true) {
gap: 10px;
grid-template-columns: 1fr auto;
align-items: center;
span {
overflow: hidden;
white-space: nowrap;

View File

@@ -10,7 +10,8 @@
>
</el-input>
</div>
<el-select v-model="params.status" placeholder="Select" class="status-select" :style="`width: ${locale === 'en' ? 95 : 80 }px`">
<el-select v-model="params.status" placeholder="Select" class="status-select"
:style="`width: ${locale === 'en' ? 95 : 80 }px`">
<el-option :key="-1" :label="$t('all')" :value="-1"/>
<el-option :key="0" :label="$t('active')" :value="0"/>
<el-option :key="1" :label="$t('banned')" :value="1"/>
@@ -26,7 +27,8 @@
</div>
<el-scrollbar ref="scrollbarRef" class="scrollbar">
<div>
<div class="loading" :class="tableLoading ? 'loading-show' : 'loading-hide'">
<div class="loading" :class="tableLoading ? 'loading-show' : 'loading-hide'"
:style="first ? 'background: transparent' : ''">
<loading/>
</div>
<el-table
@@ -41,60 +43,78 @@
<el-table-column :width="expandWidth" type="expand">
<template #default="props">
<div class="details">
<div v-if="!sendNumShow"><span class="details-item-title">{{$t('tabSent')}}:</span>{{ props.row.sendEmailCount }}
<div v-if="!sendNumShow"><span
class="details-item-title">{{ $t('tabSent') }}:</span>{{ props.row.sendEmailCount }}
</div>
<div v-if="!accountNumShow"><span class="details-item-title">{{$t('tabMailboxes')}}:</span>{{
<div v-if="!accountNumShow"><span class="details-item-title">{{ $t('tabMailboxes') }}:</span>{{
props.row.accountCount
}}
</div>
<div v-if="!createTimeShow"><span class="details-item-title">{{$t('tabRegisteredAt')}}:</span>{{
<div v-if="!createTimeShow"><span class="details-item-title">{{ $t('tabRegisteredAt') }}:</span>{{
tzDayjs(props.row.createTime).format('YYYY-MM-DD HH:mm')
}}
</div>
<div v-if="!typeShow"><span class="details-item-title">{{$t('perm')}}:</span> {{ toRoleName(props.row.type) }}
<div v-if="!typeShow"><span class="details-item-title">{{ $t('perm') }}:</span>
{{ toRoleName(props.row.type) }}
</div>
<div v-if="!statusShow">
<span class="details-item-title">{{$t('tabStatus')}}:</span>
<el-tag disable-transitions v-if="props.row.isDel === 1" type="info">{{$t('deleted')}}</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 0" type="primary">{{$t('active')}}</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 1" type="danger">{{$t('banned')}}</el-tag>
<span class="details-item-title">{{ $t('tabStatus') }}:</span>
<el-tag disable-transitions v-if="props.row.isDel === 1" type="info">{{ $t('deleted') }}</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 0" type="primary">{{ $t('active') }}
</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 1" type="danger">{{ $t('banned') }}
</el-tag>
</div>
<div><span class="details-item-title">{{$t('registrationIp')}}:</span>{{ props.row.createIp || $t('unknown') }}</div>
<div><span class="details-item-title">{{$t('recentIP')}}:</span>{{ props.row.activeIp || $t('unknown') }}</div>
<div><span class="details-item-title">{{$t('recentActivity')}}:</span>{{
<div><span class="details-item-title">{{ $t('registrationIp') }}:</span>{{
props.row.createIp || $t('unknown')
}}
</div>
<div><span class="details-item-title">{{ $t('recentIP') }}:</span>{{
props.row.activeIp || $t('unknown')
}}
</div>
<div><span class="details-item-title">{{ $t('recentActivity') }}:</span>{{
props.row.activeTime ? tzDayjs(props.row.activeTime).format('YYYY-MM-DD') : $t('unknown')
}}
</div>
<div><span class="details-item-title">{{$t('loginDevice')}}:</span>{{ props.row.device || $t('unknown') }}</div>
<div><span class="details-item-title">{{$t('loginSystem')}}:</span>{{ props.row.os || $t('unknown') }}</div>
<div><span class="details-item-title">{{$t('browserLogin')}}:</span>{{ props.row.browser || $t('unknown') }}</div>
<div><span
class="details-item-title">{{ $t('loginDevice') }}:</span>{{ props.row.device || $t('unknown') }}
</div>
<div><span class="details-item-title">{{ $t('loginSystem') }}:</span>{{ props.row.os || $t('unknown') }}
</div>
<div><span
class="details-item-title">{{ $t('browserLogin') }}:</span>{{ props.row.browser || $t('unknown') }}
</div>
<div>
<span class="details-item-title">{{$t('sendEmail')}}:</span>
<span class="details-item-title">{{ $t('sendEmail') }}:</span>
<span>{{ formatSendCount(props.row) }}</span>
<el-tag style="margin-left: 10px" v-if="props.row.sendAction.hasPerm" >
<el-tag style="margin-left: 10px" v-if="props.row.sendAction.hasPerm">
{{ 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">{{$t('reset')}}
@click="resetSendCount(props.row)" type="primary">{{ $t('reset') }}
</el-button>
</div>
</div>
</template>
</el-table-column>
<el-table-column show-overflow-tooltip :tooltip-formatter="tableRowFormatter" :label="$t('tabEmailAddress')" :min-width="emailWidth">
<el-table-column show-overflow-tooltip :tooltip-formatter="tableRowFormatter" :label="$t('tabEmailAddress')"
: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="$t('tabReceived')"
:filtered-value="filteredValue" :filters="filters" :width="receiveWidth"
:label="$t('tabReceived')"
prop="receiveEmailCount"/>
<el-table-column :formatter="formatterSend" label-class-name="send" column-key="send"
:filtered-value="filteredValue" :filters="filters" v-if="sendNumShow" :label="$t('tabSent')"
prop="sendEmailCount"/>
<el-table-column :formatter="formatterAccount" label-class-name="account" column-key="account"
:filtered-value="filteredValue" :filters="filters" v-if="accountNumShow" :label="$t('tabMailboxes')"
:filtered-value="filteredValue" :filters="filters" v-if="accountNumShow"
:label="$t('tabMailboxes')"
prop="accountCount"/>
<el-table-column v-if="createTimeShow" :label="$t('tabRegisteredAt')" min-width="160" prop="createTime">
<template #default="props">
@@ -103,9 +123,9 @@
</el-table-column>
<el-table-column v-if="statusShow" min-width="60px" :label="$t('tabStatus')" prop="status">
<template #default="props">
<el-tag disable-transitions v-if="props.row.isDel === 1" type="info">{{$t('deleted')}}</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 0" type="primary">{{$t('active')}}</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 1" type="danger">{{$t('banned')}}</el-tag>
<el-tag disable-transitions v-if="props.row.isDel === 1" type="info">{{ $t('deleted') }}</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 0" type="primary">{{ $t('active') }}</el-tag>
<el-tag disable-transitions v-else-if="props.row.status === 1" type="danger">{{ $t('banned') }}</el-tag>
</template>
</el-table-column>
<el-table-column v-if="typeShow" :label="$t('tabRole')" min-width="140" prop="type">
@@ -118,16 +138,16 @@
<el-table-column :label="$t('tabSetting')" :width="settingWidth">
<template #default="props">
<el-dropdown trigger="click">
<el-button size="small" type="primary">{{$t('action')}}</el-button>
<el-button size="small" type="primary">{{ $t('action') }}</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openSetPwd(props.row)">{{$t('chgPwd')}}</el-dropdown-item>
<el-dropdown-item @click="openSetType(props.row)">{{$t('perm')}}</el-dropdown-item>
<el-dropdown-item @click="openSetPwd(props.row)">{{ $t('chgPwd') }}</el-dropdown-item>
<el-dropdown-item @click="openSetType(props.row)">{{ $t('perm') }}</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)">{{$t('restore')}}</el-dropdown-item>
<el-dropdown-item @click="delUser(props.row)">{{$t('delete')}}</el-dropdown-item>
<el-dropdown-item v-else @click="restore(props.row)">{{ $t('restore') }}</el-dropdown-item>
<el-dropdown-item @click="delUser(props.row)">{{ $t('delete') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -168,18 +188,18 @@
<el-input v-model="userForm.password" type="password" :placeholder="$t('newPassword')" autocomplete="off">
</el-input>
<el-button class="btn" type="primary" :loading="settingLoading" @click="updatePwd"
>{{$t('save')}}
>{{ $t('save') }}
</el-button>
</div>
</el-dialog>
<el-dialog class="dialog" v-model="setTypeShow" :title="$t('changePerm')" @closed="resetUserForm">
<div class="dialog-box">
<el-input disabled :model-value="$t('admin')" v-if="userForm.type === 0" />
<el-select v-else v-model="userForm.type" placeholder="Select" >
<el-input disabled :model-value="$t('admin')" v-if="userForm.type === 0"/>
<el-select v-else 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 :disabled="userForm.type === 0" class="btn" :loading="settingLoading" type="primary" @click="setType"
>{{$t('save')}}
>{{ $t('save') }}
</el-button>
</div>
</el-dialog>
@@ -201,7 +221,7 @@
:value="item"
/>
</el-select>
<div style="color: #333">
<div>
<span>{{ addForm.suffix }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
@@ -213,7 +233,7 @@
<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"
>{{$t('add')}}
>{{ $t('add') }}
</el-button>
</div>
</el-dialog>
@@ -239,13 +259,13 @@ import {useSettingStore} from "@/store/setting.js";
import {isEmail} from "@/utils/verify-utils.js";
import {useRoleStore} from "@/store/role.js";
import {useUserStore} from "@/store/user.js";
import { useI18n } from 'vue-i18n';
import {useI18n} from 'vue-i18n';
defineOptions({
name: 'user'
})
const { t, locale } = useI18n();
const {t, locale} = useI18n();
const roleStore = useRoleStore()
const userStore = useUserStore()
const settingStore = useSettingStore()
@@ -319,7 +339,7 @@ if (paramsStar) {
}
watch(() => params, () => {
localStorage.setItem('user-params',JSON.stringify(params))
localStorage.setItem('user-params', JSON.stringify(params))
}, {
deep: true
})
@@ -535,7 +555,7 @@ function toRoleName(type) {
function resetSendCount(user) {
ElMessageBox.confirm(t('reSendConfirm',{msg: user.email}), {
ElMessageBox.confirm(t('reSendConfirm', {msg: user.email}), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
@@ -552,7 +572,7 @@ function resetSendCount(user) {
}
function delUser(user) {
ElMessageBox.confirm(t('delConfirm',{msg: user.email}), {
ElMessageBox.confirm(t('delConfirm', {msg: user.email}), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
@@ -572,22 +592,22 @@ function restore(user) {
const type = ref(0)
ElMessageBox.confirm( null, {
ElMessageBox.confirm(null, {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
message: () => h('div', [
h('div', { class: 'mb-2' }, t('restoreConfirm', {msg: user.email})),
h('div', {class: 'mb-2'}, t('restoreConfirm', {msg: user.email})),
h(ElRadioGroup, {
modelValue: type.value,
'onUpdate:modelValue': (val) => (type.value = val),
}, [
h(ElRadio, { label: 'option1', value: 0 }, t('normalRestore')),
h(ElRadio, { label: 'option2', value: 1 }, t('allRestore')),
h(ElRadio, {label: 'option1', value: 0}, t('normalRestore')),
h(ElRadio, {label: 'option2', value: 1}, t('allRestore')),
])
]),
type: 'warning'
}).then(() => {
userRestore(user.userId,type.value).then(() => {
userRestore(user.userId, type.value).then(() => {
user.isDel = 0
ElMessage({
message: t('restoreSuccessMsg'),
@@ -601,7 +621,7 @@ function restore(user) {
function setStatus(user) {
if (user.status === 0) {
ElMessageBox.confirm(t('banRestore',{ msg: user.email }), {
ElMessageBox.confirm(t('banRestore', {msg: user.email}), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
@@ -740,7 +760,9 @@ function getUserList(loading = true) {
scrollbarRef.value?.setScrollTop(0);
}).finally(() => {
tableLoading.value = false
first.value = false
setTimeout(() => {
first.value = false
}, 200)
})
}
@@ -778,12 +800,6 @@ function adjustWidth() {
word-break: break-all;
}
.el-table-filter__bottom {
button:last-child {
display: none;
}
}
.el-table-filter__content {
min-width: 0;
}
@@ -810,7 +826,7 @@ function adjustWidth() {
gap: 15px;
flex-wrap: wrap;
align-items: center;
box-shadow: inset 0 -1px 0 0 rgba(100, 121, 143, 0.12);
box-shadow: var(--header-actions-border);
font-size: 18px;
.search-input {
@@ -864,6 +880,7 @@ function adjustWidth() {
@media (max-width: 767px) {
padding-left: 35px;
}
.details-item-title {
white-space: pre;
color: #909399;
@@ -925,7 +942,7 @@ function adjustWidth() {
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.8);
background-color: var(--loadding-background);
left: 0;
z-index: 2;
top: 0;
@@ -956,13 +973,13 @@ function adjustWidth() {
:deep(.el-pagination .el-select) {
width: 100px;
background: #FFF;
background: var(--el-bg-color);
}
:deep(.el-input-group__append) {
padding: 0 !important;
padding-left: 8px !important;
background: #FFFFFF;
background: var(--el-bg-color);
}
:deep(.el-dialog) {
@@ -997,7 +1014,7 @@ function adjustWidth() {
}
:deep(.el-table__inner-wrapper:before) {
background: #fff;
background: var(--el-bg-color);
}
:deep(.el-message-box__container) {