新增权限域名限制和链接背景上传

This commit is contained in:
eoao
2025-07-26 23:11:00 +08:00
parent 0e86b93e0c
commit 12833cd901
36 changed files with 584 additions and 354 deletions

View File

@@ -47,15 +47,52 @@
</el-table>
</div>
</el-scrollbar>
<el-dialog top="5vh" class="dialog" v-model="roleFormShow" :title="dialogType.title" @closed="resetForm">
<el-dialog top="5vh" class="dialog" v-model="roleFormShow" @closed="resetForm">
<template #header>
<span style="font-size: 18px">{{dialogType.title}}</span>
<el-popover
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>
{{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('emailBlock')" 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('removeBody')" :value="1" />
</el-radio-group>
<el-select
class="dialog-input"
v-model="form.availDomain"
multiple
filterable
allow-create
default-first-option
:reserve-keyword="false"
tag-type="success"
:placeholder="$t('availableDomains')"
@change="availDomainChange"
>
<el-option
v-for="item in domainOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</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" />
</div>
@@ -108,6 +145,7 @@ import {roleAdd, roleDelete, rolePermTree, roleRoleList, roleSet, roleSetDef} fr
import loading from '@/components/loading/index.vue';
import {useRoleStore} from "@/store/role.js";
import {useUserStore} from "@/store/user.js";
import {useSettingStore} from "@/store/setting.js";
import {isEmail} from "@/utils/verify-utils.js";
import {useI18n} from "vue-i18n";
@@ -115,6 +153,7 @@ defineOptions({
name: 'role'
})
const { domainList } = useSettingStore();
const { t, locale } = useI18n();
const userStore = useUserStore();
const roleStore = useRoleStore();
@@ -144,8 +183,11 @@ const form = reactive({
accountCount: 0,
sort: 0,
isDefault: 0,
availDomain: []
})
let domainOptions = []
const expand = ref(false)
let chooseRole = {}
@@ -156,6 +198,18 @@ rolePermTree().then(tree => {
treeList.push(...tree)
})
domainOptions = domainList.map(domain => ({label: domain,value: domain}))
function availDomainChange() {
const index = form.availDomain.findIndex(domain => {
return !domainOptions.map(option => option.value).includes(domain)
})
if (index > -1) {
form.availDomain.splice(index,1)
}
}
function banEmailAddTag(val) {
const emails = Array.from(new Set(
val.split(/[,]/).map(item => item.trim()).filter(item => item)
@@ -270,6 +324,7 @@ function resetForm() {
form.accountCount = 0
form.banEmail = []
form.banEmailType = 0
form.availDomain = []
tree.value.setCheckedKeys([])
}
@@ -285,6 +340,7 @@ function openRoleSet(role) {
form.sendCount = role.sendCount
form.accountCount = role.accountCount
form.banEmail = role.banEmail
form.availDomain = role.availDomain
nextTick(() => {
tree.value.setCheckedKeys(role.permIds)
})
@@ -394,7 +450,7 @@ window.onresize = () => {
.warning {
position: relative;
left: 5px;
top: 5px;
top: 2px;
color: gray;
cursor: pointer;
}

View File

@@ -73,9 +73,8 @@ import {accountSetName} from "@/request/account.js";
import {useAccountStore} from "@/store/account.js";
import {useI18n} from "vue-i18n";
import {useSettingStore} from "@/store/setting.js";
import dayjs from "dayjs";
const { t, locale } = useI18n()
const { t } = useI18n()
const settingStore = useSettingStore()
const accountStore = useAccountStore()
const userStore = useUserStore();

View File

@@ -101,13 +101,13 @@
fit="cover"
>
<template #error>
<div class="error-image" @click="openCut">
<div class="error-image" @click="openSetBackground">
<Icon icon="ph:image" width="24" height="24"/>
</div>
</template>
</el-image>
<div class="background-btn">
<el-button class="opt-button" size="small" type="primary" @click="openCut">
<el-button class="opt-button" size="small" type="primary" @click="openSetBackground">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
<el-button class="opt-button" size="small" type="primary" @click="delBackground">
@@ -286,10 +286,10 @@
</div>
<div class="concerning-item">
<span>{{$t('support')}} : </span>
<el-button @click="jump('https://afdian.com/a/eoao_')" >
Afdian
<el-button @click="jump('https://support.skymail.ink')" >
{{t('supportDesc')}}
<template #icon>
<Icon color="#8261DB" icon="simple-icons:afdian" width="24" height="24" />
<Icon color="#79D6B5" icon="simple-icons:buymeacoffee" width="20" height="20" />
</template>
</el-button>
</div>
@@ -299,7 +299,7 @@
</div>
<!-- Dialogs remain the same -->
<el-dialog v-model="editTitleShow" :title="$t('changeTitle')" width="340" @closed="editTitle = ''">
<el-dialog v-model="editTitleShow" :title="$t('changeTitle')" width="340" >
<form>
<el-input type="text" :placeholder="$t('websiteTitle')" v-model="editTitle"/>
<el-button type="primary" :loading="settingLoading" @click="saveTitle">{{$t('save')}}</el-button>
@@ -319,7 +319,7 @@
<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 = ''">
<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>
@@ -334,24 +334,37 @@
</form>
</el-dialog>
<el-dialog
v-model="cutShow"
:title="$t('backgroundCropping')"
v-model="showSetBackground"
class="cut-dialog"
@closed="closedSetBackground"
>
<div class="cropper">
<vueCropper
ref="cropper"
:img="cutImage"
:fixedNumber="[16,9]"
outputType="jpeg"
:fixed="true"
:centerBox="true"
:full="true"
:autoCrop="true"
:outputSize="0.92"
></vueCropper>
</div>
<template #header>
<span style="font-size: 18px">
{{$t('backgroundTitle')}}
<el-tooltip>
<template #content>
<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-image
v-if="localUpShow"
:preview-src-list="[backgroundImage]"
show-progress
class="cropper"
fit="cover"
:src="backgroundImage"
></el-image>
<div class="cut-button">
<el-button type="primary" link @click="openCut" v-if="!localUpShow">
{{$t('localUpload')}}
</el-button>
<el-button type="primary" link @click="localUpShow = false" v-if="localUpShow">
{{$t('imageLink')}}
</el-button>
<el-button type="primary" :loading="settingLoading" @click="saveBackground">{{$t('save')}}</el-button>
</div>
</el-dialog>
@@ -387,7 +400,7 @@
<template #header>
<div class="forward-head">
<span class="forward-set-title">{{$t('otherEmail')}}</span>
<el-tooltip effect="dark" :content="$t('otherEmailDec')">
<el-tooltip effect="dark" :content="$t('otherEmailDesc')">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
@@ -454,6 +467,7 @@ 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';
defineOptions({
@@ -462,9 +476,8 @@ defineOptions({
const { t, locale } = useI18n();
const firstLoading = ref(true)
const cropper = ref()
const cutImage = ref('')
const cutShow = ref(false)
const backgroundImage = ref('')
const localUpShow = ref(false)
const accountStore = useAccountStore();
const userStore = useUserStore();
const editTitleShow = ref(false)
@@ -481,6 +494,9 @@ const editTitle = ref('')
const settingLoading = ref(false)
const r2DomainInput = ref('')
const loginOpacity = ref(0)
const backgroundUrl = ref('')
let backgroundFile = {}
const showSetBackground = ref(false)
let backup = '{}'
const resendTokenForm = reactive({
domain: '',
@@ -517,6 +533,17 @@ const tokenColumnWidth = ref(0)
const ruleType = ref(0)
const ruleEmail = ref([])
settingQuery().then(settingData => {
setting.value = settingData
resendTokenForm.domain = setting.value.domainList[0]
loginOpacity.value = setting.value.loginOpacity
firstLoading.value = false
backgroundUrl.value = setting.value.background?.startsWith('http') ? setting.value.background : ''
editTitle.value = setting.value.title
r2DomainInput.value = setting.value.r2Domain
})
const resendList = computed(() => {
let list = Object.keys(setting.value.resendTokens).map(key => {
@@ -548,13 +575,11 @@ const compareByLengthAndUpperCase = (a, b, key) => {
};
settingQuery().then(settingData => {
setting.value = settingData
resendTokenForm.domain = setting.value.domainList[0]
loginOpacity.value = setting.value.loginOpacity
firstLoading.value = false
})
function closedSetBackground() {
backgroundImage.value = ''
localUpShow.value = false
backgroundUrl.value = setting.value.background?.startsWith('http') ? setting.value.background : ''
}
function openTgSetting() {
tgBotStatus.value = setting.value.tgBotStatus
@@ -697,6 +722,7 @@ function delBackground() {
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
backgroundUrl.value = ''
setting.value.background = null
editSetting({background: null})
})
@@ -709,21 +735,43 @@ function saveTurnstileKey() {
editSetting(settingForm)
}
function saveBackground() {
settingLoading.value = true
cropper.value.getCropData(data => {
setBackground(data).then(key => {
setting.value.background = key
cutShow.value = false
async function saveBackground() {
let image = ''
if (localUpShow.value) {
image = await fileToBase64(backgroundFile,true);
} else {
if (!backgroundUrl.value.startsWith('http')) {
ElMessage({
message: t('changSuccessMsg'),
type: "success",
message: '图片地址不正确',
type: "error",
plain: true
})
}).finally(() => {
settingLoading.value = false
return
}
image = backgroundUrl.value
}
settingLoading.value = true
setBackground(image).then(key => {
setting.value.background = key
showSetBackground.value = false
ElMessage({
message: t('changSuccessMsg'),
type: "success",
plain: true
})
localUpShow.value = false
backgroundImage.value = ''
}).finally(() => {
settingLoading.value = false
})
}
function openSetBackground() {
showSetBackground.value = true
}
function openCut() {
@@ -732,14 +780,15 @@ function openCut() {
doc.setAttribute('accept', 'image/*')
doc.click()
doc.onchange = async (e) => {
cutImage.value = URL.createObjectURL(e.target.files[0])
cutShow.value = true
backgroundFile = e.target.files[0]
backgroundImage.value = URL.createObjectURL(e.target.files[0])
localUpShow.value = true
}
}
function saveR2domain() {
const settingForm = {r2Domain: r2DomainInput.value}
editSetting(settingForm)
editSetting(settingForm, true)
}
function openResendForm() {
@@ -824,7 +873,7 @@ function editSetting(settingForm, refreshStatus = true) {
thirdEmailShow.value = false
forwardRulesShow.value = false
}).catch((e) => {
console.log(e)
console.error(e)
loginOpacity.value = setting.value.loginOpacity
setting.value = {...setting.value, ...JSON.parse(backup)}
}).finally(() => {
@@ -892,6 +941,8 @@ function editSetting(settingForm, refreshStatus = true) {
flex-direction: column;
}
.settings-card {
background-color: #fff;
border-radius: 8px;
@@ -939,6 +990,14 @@ function editSetting(settingForm, refreshStatus = true) {
}
}
.title-icon.warning {
position: relative;
top: 2px;
color: gray;
cursor: pointer;
margin-left: 2px;
}
.warning {
margin-left: 5px;
color: gray;
@@ -959,6 +1018,11 @@ function editSetting(settingForm, refreshStatus = true) {
justify-content: space-between;
}
.background-url {
width: min(calc(100vw - 70px), 500px);
}
:deep(.el-dialog) {
width: 400px !important;
@media (max-width: 440px) {
@@ -1027,8 +1091,7 @@ function editSetting(settingForm, refreshStatus = true) {
padding-top: 15px;
width: 100%;
display: flex;
justify-content: end;
justify-content: space-between;
.el-button {
width: fit-content;
}

View File

@@ -644,7 +644,6 @@ function setType() {
function resetUserForm() {
userForm.password = null
userForm.userId = 0
userForm.type = 0
}
function search() {