修改登录页面

This commit is contained in:
eoao
2025-06-04 00:20:53 +08:00
parent 3190b47bb9
commit dac0383bab
24 changed files with 221 additions and 143 deletions

View File

@@ -18,6 +18,8 @@
[**👉在线演示**](https://skymail.ink)
[**👉小白保姆教程-界面部署**](https://doc.skymail.ink)
| ![](demo/demo1.png) | ![](demo/demo2.png) |
|---------------------|---------------------|
| ![](demo/demo3.png) | ![](demo/demo4.png) |
@@ -76,8 +78,6 @@
## 使用教程
[**👉小白保姆教程-界面部署**](https://doc.skymail.ink)
### 环境要求
@@ -140,7 +140,7 @@ jwt_secret = "" #登录身份令牌的密钥,随便填一串字符串
npm run deploy
```
然后进入域名管理->电子邮件->路由规则->Catch-all 地址. 这里选择发送到 worker, 然后选择创建的worker
然后在Cloudflare->域名管理->电子邮件->路由规则->Catch-all 地址. 这里选择发送到 worker, 然后选择创建的worker
浏览器输入 https://<你的项目域名>/api/init/<你的jwt_secret> 初始化或更新 d1和kv数据库
@@ -148,9 +148,9 @@ npm run deploy
**邮件发送**
在resend官网注册后验证你的域名并创建ApiKey回到项目网站设置页面添加 resend token
在resend注册后验证你的域名并创建ApiKey 回到项目网站设置页面添加 resend token
Webhooks 添加回调地址https://<你的项目域名>/api/webhooks
在resend Webhooks 添加回调地址https://<你的项目域名>/api/webhooks
勾选✅ (email.bounced email.complained email.delivered email.delivery_delayed)
@@ -165,7 +165,7 @@ npm run dev
```
浏览器输入http://127.0.0.1:8787/api/init/<你的jwt_secret>初始化d1和kv数据库
本地运行r2域名可设置为http://127.0.0.1:8787/api/file
本地运行项目页面设置r2域名可设置为http://127.0.0.1:8787/api/file
## 目录结构

View File

@@ -1,11 +1,11 @@
{
"name": "video-admin-vue",
"name": "mail-vue",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "video-admin-vue",
"name": "mail-vue",
"version": "0.0.0",
"dependencies": {
"@iconify/vue": "^4.3.0",
@@ -17,6 +17,7 @@
"date-time-format-timezone": "^1.0.22",
"dayjs": "^1.11.13",
"element-plus": "^2.9.5",
"lodash-es": "^4.17.21",
"path": "^0.12.7",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0",

View File

@@ -19,6 +19,7 @@
"date-time-format-timezone": "^1.0.22",
"dayjs": "^1.11.13",
"element-plus": "^2.9.5",
"lodash-es": "^4.17.21",
"path": "^0.12.7",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0",

View File

@@ -610,7 +610,7 @@ function loadData() {
@media (max-width: 1199px) {
padding-right: 15px;
}
@media (max-width: 991px) {
@media (max-width: 1024px) {
grid-template-columns: 1fr;
gap: 4px;
}
@@ -630,7 +630,7 @@ function loadData() {
display: grid;
gap: 5px;
grid-template-columns: auto 1fr;
@media (min-width: 992px) {
@media (min-width: 1024px) {
grid-template-columns: 1fr;
> span:last-child {
display: none;
@@ -719,7 +719,7 @@ function loadData() {
width: 40px;
}
@media (max-width: 991px) {
@media (max-width: 1024px) {
.pc-star {
display: none;
}

View File

@@ -334,7 +334,7 @@ function submit() {
.item {
background-color: #fff;
border-radius: 8px;
padding: 12px;
padding: 12px 10px;
margin-bottom: 10px;
margin-left: 10px;
margin-right: 10px;

View File

@@ -52,11 +52,6 @@
</el-menu>
</div>
</el-scrollbar>
<div class="github">
<a href="https://github.com/LaziestRen/cloud-mail">
<Icon icon="codicon:github-inverted" width="28" height="28" />
</a>
</div>
</template>
<script setup>
@@ -75,7 +70,7 @@ const route = useRoute();
.title {
margin: 15px 10px;
height: 50px;
border-radius: 8px;
border-radius: 6px;
display: flex;
position: relative;
font-size: 16px;
@@ -110,7 +105,7 @@ const route = useRoute();
.el-menu-item {
margin: 5px 10px !important;
border-radius: 8px;
border-radius: 6px;
height: 36px;
padding: 10px !important;
}
@@ -148,7 +143,7 @@ const route = useRoute();
border-right: 0;
width: 250px;
@media (max-width: 1199px) {
width: 230px;
width: 250px;
}
}

View File

@@ -29,7 +29,7 @@
{{ userStore.user.email }}
</div>
<div class="detail-user-type">
<el-tag type="warning">{{userStore.user.role.name}}</el-tag>
<el-tag >{{userStore.user.role.name}}</el-tag>
</div>
<div class="action-info">
<div>
@@ -38,14 +38,14 @@
</div>
<div>
<div>
<span v-if="sendCount !== null" style="margin-right: 5px">{{ sendCount }}</span>
<el-tag v-if="!hasPerm('email:send')" type="warning">{{sendType}}</el-tag>
<el-tag v-else type="success">{{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>
<span v-if="accountCount && hasPerm('account:add')" style="margin-right: 5px">{{ accountCount }}</span>
<el-tag v-if="!accountCount && hasPerm('account:add')" type="success">无限制</el-tag>
<el-tag v-if="!hasPerm('account:add')" type="warning">无权限</el-tag>
<el-tag v-if="!accountCount && hasPerm('account:add')" >无限制</el-tag>
<el-tag v-if="!hasPerm('account:add')" >无权限</el-tag>
</div>
</div>
</div>
@@ -84,6 +84,11 @@ const sendType = computed(() => {
if (!hasPerm('email:send')) {
return '无权限'
}
if (userStore.user.role.sendCount === 0) {
return '无限制'
}
if (userStore.user.role.sendType === 'day') {
return '每天'
}
@@ -99,7 +104,7 @@ const sendCount = computed(() => {
}
if (!userStore.user.role.sendCount) {
return '无限制'
return null
}
return userStore.user.sendCount + '/' + userStore.user.role.sendCount
})
@@ -143,7 +148,7 @@ function formatName(email) {
}
:deep(.el-popper.is-pure) {
border-radius: 8px;
border-radius: 10px;
}
.user-details {
@@ -196,20 +201,21 @@ function formatName(email) {
padding-right: 10px;
padding-bottom: 10px;
.el-button {
height: 26px;
border-radius: 8px;
height: 28px;
width: 100%;
}
}
.details-avatar {
margin-top: 20px;
height: 45px;
width: 45px;
height: 40px;
width: 40px;
border: 1px solid #ccc;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
border-radius: 10px;
}
}

View File

@@ -31,10 +31,10 @@ import writer from '@/layout/write/index.vue'
const uiStore = useUiStore();
const writerRef = ref({})
const isMobile = ref(window.innerWidth < 992)
const isMobile = ref(window.innerWidth < 1024)
const handleResize = () => {
isMobile.value = window.innerWidth < 992
uiStore.asideShow = window.innerWidth >= 991;
isMobile.value = window.innerWidth < 1024
uiStore.asideShow = window.innerWidth >= 1024;
}
onMounted(() => {
@@ -66,7 +66,7 @@ onBeforeUnmount(() => {
transform: translateX(0);
transition: all 100ms ease;
z-index: 101;
@media (max-width: 991px) {
@media (max-width: 1024px) {
position: fixed;
top: 0;
left: 0;

View File

@@ -78,7 +78,7 @@ const handleResize = () => {
@media (max-width: 767px) {
position: fixed;
z-index: 100;
width: 229px;
width: 250px;
}
}
@@ -87,8 +87,8 @@ const handleResize = () => {
position: fixed;
transform: translateX(-100%);
opacity: 0;
@media (max-width: 991px) {
width: 229px;
@media (max-width: 1024px) {
width: 250px;
z-index: 100;
}
}
@@ -99,7 +99,7 @@ const handleResize = () => {
grid-template-columns: 250px 1fr;
height: calc(100% - 60px);
@media (max-width: 1200px) {
grid-template-columns: 229px 1fr;
grid-template-columns: 250px 1fr;
}
@media (max-width: 767px) {
grid-template-columns: 1fr;

View File

@@ -285,13 +285,13 @@ function close() {
border-radius: 8px;
display: flex;
flex-direction: column;
@media (max-width: 991px) {
@media (max-width: 1024px) {
width: 100%;
height: 100%;
border-radius: 0;
}
@media (min-width: 992px) {
@media (min-width: 1024px) {
height: min(800px, calc(100vh - 60px));
}

View File

@@ -114,7 +114,7 @@ router.afterEach((to) => {
}
}
if (window.innerWidth < 992) {
if (window.innerWidth < 1024) {
uiStore.asideShow = false
}
})

View File

@@ -4,7 +4,8 @@ export const useSettingStore = defineStore('setting', {
state: () => ({
domainList: [],
settings: {
r2Domain: ''
r2Domain: '',
loginOpacity: 1.00,
}
}),
actions: {

View File

@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
export const useUiStore = defineStore('ui', {
state: () => ({
asideShow: window.innerWidth > 991,
asideShow: window.innerWidth > 1024,
accountShow: false,
backgroundLoading: true,
writerRef: null,

View File

@@ -65,20 +65,20 @@ button, input, select, textarea {
}
.tox .tox-dialog--width-lg {
@media (min-width: 992px) {
@media (min-width: 1024px) {
height: 850px !important;
}
}
.tox .tox-dialog__body-content {
max-height: min(850px, calc(100vh - 110px)) !important;
@media (min-width: 992px) {
@media (min-width: 1024px) {
max-height: min(850px, calc(100vh - 110px));
}
}
.tox .tox-dialog__body-content {
@media (min-width: 992px) {
@media (min-width: 1024px) {
max-height: min(850px, calc(100vh - 110px)) !important;
}
}

View File

@@ -1,5 +1,5 @@
<template>
<div id="login-box" :style="background">
<div id="login-box">
<div id="background-wrap" v-if="!settingStore.settings.background">
<div class="x1 cloud"></div>
<div class="x2 cloud"></div>
@@ -7,17 +7,14 @@
<div class="x4 cloud"></div>
<div class="x5 cloud"></div>
</div>
<div v-else :style="background"></div>
<div class="form-wrapper">
<div class="container">
<span class="form-title">{{ settingStore.settings.title }}</span>
<div class="custom-style" v-if="settingStore.settings.register === 0">
<el-segmented v-model="show" :options="options"/>
</div>
<span class="form-title">{{settingStore.settings.title}}</span>
<span class="form-desc" v-if="show === 'login'">请输入你的账号信息以开始使用邮箱系统</span>
<span class="form-desc" v-else>请输入你的账号密码以开始注册邮箱系统</span>
<div v-if="show === 'login'">
<el-input v-model="form.email" type="text" placeholder="邮箱" autocomplete="off">
<template #prefix>
<Icon icon="weui:email-outlined" width="22" height="22"/>
</template>
<el-input class="email-input" v-model="form.email" type="text" placeholder="邮箱" autocomplete="off">
<template #append>
<div @click.stop="openSelect">
<el-select
@@ -41,19 +38,13 @@
</template>
</el-input>
<el-input v-model="form.password" placeholder="密码" type="password" autocomplete="off">
<template #prefix>
<Icon icon="carbon:password" width="22" height="22"/>
</template>
</el-input>
<el-button class="btn" type="primary" @click="submit" :loading="loginLoading"
>登录
</el-button>
</div>
<div v-else>
<el-input v-model="registerForm.email" type="text" placeholder="邮箱" autocomplete="off">
<template #prefix>
<Icon icon="weui:email-outlined" width="22" height="22"/>
</template>
<el-input class="email-input" v-model="registerForm.email" type="text" placeholder="邮箱" autocomplete="off">
<template #append>
<div @click.stop="openSelect">
<el-select
@@ -76,34 +67,21 @@
</div>
</template>
</el-input>
<el-input v-model="registerForm.password" placeholder="密码" type="password" autocomplete="off">
<template #prefix>
<Icon icon="carbon:password" width="22" height="22"/>
</template>
</el-input>
<el-input v-model="registerForm.confirmPassword" placeholder="确认密码" type="password" autocomplete="off">
<template #prefix>
<Icon icon="carbon:password" width="22" height="22"/>
</template>
</el-input>
<el-input v-model="registerForm.password" placeholder="密码" type="password" autocomplete="off" />
<el-input v-model="registerForm.confirmPassword" placeholder="确认密码" type="password" autocomplete="off" />
<div v-show="verifyShow"
class="register-turnstile"
:data-sitekey="settingStore.settings.siteKey"
data-callback="onTurnstileSuccess"
></div>
<el-button class="btn" type="primary" @click="submitRegister" :loading="registerLoading"
>注册
</el-button>
</div>
<div class="switch" @click="show = 'register'" v-if="show === 'login'">还有没有账号? <span>创建账号</span></div>
<div class="switch" @click="show = 'login'" v-else>已有账号? <span>去登录</span></div>
</div>
</div>
<el-dialog
v-model="verifyShow"
title="验证你是不是人"
width="332"
align-center
>
<div
class="register-turnstile"
:data-sitekey="settingStore.settings.siteKey"
data-callback="onTurnstileSuccess"
></div>
</el-dialog>
</div>
</template>
@@ -132,7 +110,6 @@ const form = reactive({
password: '',
});
const options = [{label: '登录', value: 'login'}, {label: '注册', value: 'register'}];
const mySelect = ref()
const suffix = ref('')
const registerForm = reactive({
@@ -147,6 +124,19 @@ const verifyShow = ref(false)
let verifyToken = ''
let turnstileId = ''
window.onTurnstileSuccess = (token) => {
verifyToken = token;
setTimeout(() => {
verifyShow.value = false
}, 2000)
};
const loginOpacity = computed(() => {
return `rgba(255, 255, 255, ${settingStore.settings.loginOpacity})`
})
const background = computed(() => {
return settingStore.settings.background ? {
@@ -157,12 +147,6 @@ const background = computed(() => {
} : ''
})
window.onTurnstileSuccess = (token) => {
verifyToken = token;
setTimeout(() => {
verifyShow.value = false
}, 1500)
};
const openSelect = () => {
mySelect.value.toggleMenu()
@@ -302,53 +286,103 @@ function submitRegister() {
</script>
<style>
.el-select-dropdown__item {
padding: 0 15px;
}
.no-autofill-pwd {
.el-input__inner {
-webkit-text-security: disc !important;
}
}
</style>
<style lang="scss" scoped>
.form-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
right: 0;
height: 100%;
z-index: 110;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
@media (max-width: 767px) {
width: 100%;
}
}
.container {
max-width: 380px;
background: #FFFFFF;
padding: 20px;
margin: 0 20px;
border-radius: 8px;
background: v-bind(loginOpacity);
padding-left: 40px;
padding-right: 40px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 450px;
height: 100%;
border: 1px solid #e4e7ed;
box-shadow: var(--el-box-shadow-light);
.switch {
padding-top: 10px;
font-size: 14px;
@media (max-width: 1024px) {
padding: 20px 18px;
border-radius: 6px;
width: 384px;
margin-left: 18px;
}
@media (max-width: 767px) {
padding: 20px 18px;
border-radius: 6px;
height: fit-content;
width: 100%;
margin-right: 18px;
margin-left: 18px;
}
.btn {
height: 36px;
width: 100%;
border-radius: 6px;
}
.btn {
width: 100%;
.form-desc {
margin-top: 5px;
margin-bottom: 18px;
color: #71717a;
}
.form-title {
font-weight: bold;
font-size: 18px;
margin-bottom: 20px;
font-size: 22px !important;
}
.switch {
margin-top: 20px;
text-align: center;
span {
color: #006be6;
cursor: pointer;
}
}
:deep(.el-input__wrapper) {
border-radius: 6px;
}
.email-input :deep(.el-input__wrapper){
border-radius: 6px 0 0 6px;
}
.el-input {
height: 38px;
width: 100%;
margin-bottom: 15px;
margin-bottom: 18px;
}
}
:deep(.el-select-dropdown__item) {
padding: 0 10px;
}
.setting-icon {
position: relative;
@@ -358,7 +392,13 @@ function submitRegister() {
:deep(.el-input-group__append) {
padding: 0 !important;
padding-left: 8px !important;
padding-right: 4px !important;
background: #FFFFFF;
border-radius: 0 8px 8px 0;
}
.register-turnstile {
margin-bottom: 18px;
}
.select {
@@ -378,6 +418,8 @@ function submitRegister() {
width: 180px;
}
#login-box {
background: linear-gradient(to bottom, #2980b9, #6dd5fa, #fff);
color: #333;
@@ -386,6 +428,8 @@ function submitRegister() {
margin: 0;
padding: 0;
overflow-x: hidden;
display: grid;
grid-template-columns: 1fr;
}

View File

@@ -242,6 +242,7 @@ function openRoleSet(role) {
roleFormShow.value = true
form.sort = role.sort
form.name = role.name
form.description = role.description
form.sendType = role.sendType
form.sendCount = role.sendCount
form.accountCount = role.accountCount

View File

@@ -85,6 +85,12 @@
</el-button>
</div>
</div>
<div class="setting-item">
<div class="title-item"><span>登录透明</span></div>
<div>
<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>登录背景</span></div>
<div>
@@ -211,16 +217,21 @@
</div>
<div class="concerning-item">
<span>交流:</span>
<el-button @click="jumpTelegram">
<el-button @click="jump('https://t.me/cloud_mail_tg')">
telegram
<template #icon>
<Icon icon="logos:telegram" width="30" height="30"/>
</template>
</el-button>
<el-button @click="jump('https://github.com/LaziestRen/cloud-mail')">
github
<template #icon>
<Icon icon="codicon:github-inverted" width="22" height="22" />
</template>
</el-button>
</div>
</div>
</div>
</div>
@@ -296,6 +307,7 @@ import {Icon} from "@iconify/vue";
import {compressImage} from "@/utils/file-utils.js";
import {cvtR2Url} from "@/utils/convert.js";
import {storeToRefs} from "pinia";
import { debounce } from 'lodash-es'
defineOptions({
name: 'sys-setting'
@@ -315,7 +327,8 @@ const {settings: setting} = storeToRefs(settingStore);
const editTitle = ref('')
const settingLoading = ref(false)
const r2DomainInput = ref('')
let backup = {}
const loginOpacity = ref(0)
let backup = '{}'
const resendTokenForm = reactive({
domain: '',
token: '',
@@ -336,6 +349,18 @@ const options = [
onMounted(() => {
resendTokenForm.domain = settingStore.domainList[0];
loginOpacity.value = settingStore.settings.loginOpacity
})
function doOpacityChange() {
const form = {...setting.value}
form.loginOpacity = loginOpacity.value
editSetting(form,true)
}
const opacityChange = debounce(doOpacityChange, 1000, {
leading: false,
trailing: true
})
function physicsDeleteAllData() {
@@ -468,14 +493,15 @@ function saveTitle() {
editSetting({title: editTitle.value})
}
function jumpTelegram() {
function jump(href) {
const doc = document.createElement('a')
doc.href = 'https://t.me/cloud_mail_tg'
doc.href = href
doc.target = '_blank'
doc.click()
}
function editSetting(settingForm, refreshStatus = true) {
if (settingLoading.value) return
settingLoading.value = true
@@ -497,6 +523,7 @@ function editSetting(settingForm, refreshStatus = true) {
resendTokenFormShow.value = false
turnstileShow.value = false
}).catch(() => {
loginOpacity.value = setting.value.loginOpacity
setting.value = {...setting.value, ...JSON.parse(backup)}
}).finally(() => {
settingLoading.value = false
@@ -535,8 +562,8 @@ function editSetting(settingForm, refreshStatus = true) {
border-radius: 4px;
border: 1px solid #e4e7ed;
@media (max-width: 500px) {
width: 140px;
height: 79px;
width: 150px;
height: 83px;
}
}
@@ -702,7 +729,7 @@ function editSetting(settingForm, refreshStatus = true) {
display: grid;
gap: 10px;
grid-template-columns: 1fr auto;
align-items: center;
span {
overflow: hidden;
white-space: nowrap;

View File

@@ -728,7 +728,7 @@ adjustWidth()
function adjustWidth() {
const width = window.innerWidth
statusShow.value = width > 1060
statusShow.value = width > 1090
createTimeShow.value = width > 1200
accountNumShow.value = width > 650
sendNumShow.value = width > 685

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,8 @@
<title></title>
<link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml">
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module" crossorigin src="/assets/index-BMdBWW7j.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Blmh8UBo.css">
<script type="module" crossorigin src="/assets/index-DQ5PlDSG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-6pbK1Sk3.css">
</head>
<body>
<div id="loading-first">

View File

@@ -13,6 +13,7 @@ export const setting = sqliteTable('setting', {
secretKey: text('secret_key'),
siteKey: text('site_key'),
background: text('background'),
loginOpacity: integer('login_opacity').default(0.9),
resendTokens: text('resend_tokens').default("{}").notNull(),
});
export default setting

View File

@@ -29,6 +29,7 @@ const initService = {
`ALTER TABLE setting ADD COLUMN site_key TEXT;`,
`ALTER TABLE setting ADD COLUMN secret_key TEXT;`,
`ALTER TABLE setting ADD COLUMN background TEXT;`,
`ALTER TABLE setting ADD COLUMN login_opacity INTEGER NOT NULL DEFAULT 0.88;`,
`ALTER TABLE user ADD COLUMN create_ip TEXT;`,
`ALTER TABLE user ADD COLUMN active_ip TEXT;`,

View File

@@ -58,7 +58,7 @@ const loginService = {
const userId = await userService.insert(c, { email, password: hash, salt, type: roleRow.roleId });
await userService.updateUserInfo(c, userId);
await userService.updateUserInfo(c, userId, true);
await accountService.insert(c, { userId: userId, email });
},