优化多个地方

This commit is contained in:
eoao
2025-09-03 08:31:44 +08:00
parent 0c3f0dae3a
commit 3264f0d25a
30 changed files with 156 additions and 89 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
NODE_ENV = 'dev'
VITE_APP_TITLE = '开发环境'
VITE_BASE_URL = 'http://127.0.0.1:8787/api'
VITE_BASE_URL = 'http://127.0.0.1:8787/api'
VITE_PWA_NAME = 'Cloud Mail'
+1
View File
@@ -1,3 +1,4 @@
NODE_ENV = 'eo'
VITE_APP_TITLE = 'eo环境'
VITE_BASE_URL = ''
VITE_PWA_NAME = 'Cloud Mail'
+1
View File
@@ -1,4 +1,5 @@
NODE_ENV = 'release'
VITE_APP_TITLE = '发布环境'
VITE_BASE_URL = '/api'
VITE_PWA_NAME = 'Cloud Mail'
VITE_OUT_DIR = ../mail-worker/dist
+1
View File
@@ -1,3 +1,4 @@
NODE_ENV = 'remote'
VITE_APP_TITLE = '远程环境'
VITE_BASE_URL = ''
VITE_PWA_NAME = 'Cloud Mail'
+18 -18
View File
@@ -238,7 +238,7 @@ let scrollTop = 0
const latestEmail = ref(null)
const scrollbarRef = ref(null)
let reqLock = false
let isMobile = innerWidth < 1025
let isMobile = innerWidth < 1367
let skeletonRows = 0
const queryParam = reactive({
emailId: 0,
@@ -632,7 +632,7 @@ function loadData() {
margin-top: 5px;
margin-bottom: 2px;
color: var(--email-scroll-content-color);
@media (max-width: 1199px) {
@media (max-width: 1366px) {
flex-direction: column;
}
@@ -673,7 +673,7 @@ function loadData() {
padding-left: 15px;
padding-right: 20px;
justify-content: center;
@media (min-width: 1200px) {
@media (min-width: 1367px) {
justify-content: start;
height: 100%;
align-self: start;
@@ -682,7 +682,7 @@ function loadData() {
}
.title-column {
@media (max-width: 1199px) {
@media (max-width: 1366px) {
grid-template-columns: 1fr !important;
gap: 4px !important;
}
@@ -692,10 +692,10 @@ function loadData() {
flex: 1;
display: grid;
grid-template-columns: 240px 1fr;
@media (max-width: 1199px) {
@media (max-width: 1366px) {
padding-right: 15px;
}
@media (max-width: 1024px) {
@media (max-width: 1366px) {
grid-template-columns: 1fr;
gap: 4px;
}
@@ -710,7 +710,7 @@ function loadData() {
display: flex;
flex-direction: column;
align-content: center;
@media (max-width: 1199px) {
@media (max-width: 1366px) {
flex-direction: row;
gap: 5px;
}
@@ -720,7 +720,7 @@ function loadData() {
display: grid;
gap: 5px;
grid-template-columns: auto 1fr;
@media (min-width: 1024px) {
@media (min-width: 1366px) {
grid-template-columns: 1fr;
> span:last-child {
display: none;
@@ -745,7 +745,7 @@ function loadData() {
.phone-time {
font-weight: normal;
font-size: 12px;
@media (min-width: 1200px) {
@media (min-width: 1367px) {
display: none;
}
}
@@ -755,7 +755,7 @@ function loadData() {
.text-skeleton-one {
width: 80%;
height: 16px;
@media (max-width: 1199px) {
@media (max-width: 1366px) {
width: 40%;
}
@media (max-width: 767px) {
@@ -766,10 +766,10 @@ function loadData() {
.text-skeleton-two {
width: min(300px, 100%);
height: 16px;
@media (min-width: 1200px) {
@media (min-width: 1367px) {
display: none;
}
@media (max-width: 1199px) {
@media (max-width: 1366px) {
width: 100%;
}
}
@@ -778,7 +778,7 @@ function loadData() {
.email-text {
display: grid;
grid-template-columns: auto 1fr;
@media (max-width: 1199px) {
@media (max-width: 1366px) {
grid-template-columns: 1fr;
}
@@ -786,7 +786,7 @@ function loadData() {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@media (min-width: 1200px) {
@media (min-width: 1367px) {
padding-left: 5px;
}
}
@@ -797,7 +797,7 @@ function loadData() {
text-overflow: ellipsis;
padding-left: 10px;
color: var(--email-scroll-content-color);
@media (max-width: 1199px) {
@media (max-width: 1366px) {
padding-left: 0;
margin-top: 0;
}
@@ -813,13 +813,13 @@ function loadData() {
display: flex;
padding-left: 15px;
align-items: center;
@media (max-width: 1199px) {
@media (max-width: 1366px) {
display: none;
}
}
.email-right-skeleton {
@media (max-width: 1199px) {
@media (max-width: 1366px) {
display: none;
}
}
@@ -844,7 +844,7 @@ function loadData() {
width: 40px;
}
@media (max-width: 1024px) {
@media (max-width: 1366px) {
.pc-star {
display: none;
}
@@ -115,7 +115,7 @@ import {Icon} from "@iconify/vue";
width: 40px;
}
@media (max-width: 1024px) {
@media (max-width: 1366px) {
.pc-star {
display: none;
}
+1 -4
View File
@@ -156,10 +156,7 @@ const route = useRoute();
.el-menu {
border-right: 0;
width: 250px;
@media (max-width: 1199px) {
width: 250px;
}
width: 260px;
}
:deep(.el-divider__text) {
+12 -2
View File
@@ -30,8 +30,8 @@
<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">
<el-dropdown ref="userinfoRef" @visible-change="e => userInfoShow = e" :teleported="false" popper-class="detail-dropdown">
<div class="avatar" @click="userInfoHide" >
<div class="avatar-text">
<div>{{ formatName(userStore.user.email) }}</div>
</div>
@@ -103,6 +103,8 @@ const settingStore = useSettingStore();
const userStore = useUserStore();
const uiStore = useUiStore();
const logoutLoading = ref(false)
const userInfoShow = ref(false)
const userinfoRef = ref({})
const accountCount = computed(() => {
return userStore.user.role.accountCount
@@ -157,6 +159,14 @@ const sendCount = computed(() => {
return userStore.user.sendCount + '/' + userStore.user.role.sendCount
})
function userInfoHide(e) {
if (userInfoShow.value) {
userinfoRef.value.handleClose()
} else {
userinfoRef.value.handleOpen()
}
}
async function copyEmail(email) {
try {
await navigator.clipboard.writeText(email);
+2 -5
View File
@@ -128,7 +128,7 @@ const handleResize = () => {
@media (max-width: 767px) {
position: fixed;
z-index: 100;
width: 250px;
width: 260px;
}
}
@@ -138,7 +138,7 @@ const handleResize = () => {
transform: translateX(-100%);
opacity: 0;
@media (max-width: 1024px) {
width: 250px;
width: 260px;
z-index: 100;
}
}
@@ -148,9 +148,6 @@ const handleResize = () => {
display: grid;
grid-template-columns: 260px 1fr;
height: calc(100% - 60px);
@media (max-width: 1200px) {
grid-template-columns: 250px 1fr;
}
@media (max-width: 767px) {
grid-template-columns: 1fr;
}
+2 -1
View File
@@ -440,7 +440,7 @@ function close() {
.write-box {
background: var(--el-bg-color);
width: min(1200px, calc(100% - 80px));
width: min(1367px, calc(100% - 80px));
box-shadow: var(--el-box-shadow-light);
border: 1px solid var(--el-border-color-light);
transition: var(--el-transition-duration);
@@ -453,6 +453,7 @@ function close() {
width: 100%;
height: 100%;
border-radius: 0;
border: 0;
padding-top: 10px;
}
+3 -2
View File
@@ -94,7 +94,7 @@ router.beforeEach((to, from, next) => {
// 延迟 50ms 才启动进度条
timer = setTimeout(() => {
NProgress.start()
}, 50)
}, 100)
const token = localStorage.getItem('token')
@@ -116,7 +116,7 @@ router.beforeEach((to, from, next) => {
})
function loadBackground(next) {
console.log(131231)
const settingStore = useSettingStore();
const src = cvtR2Url(settingStore.settings.background);
@@ -150,6 +150,7 @@ router.afterEach((to) => {
if (window.innerWidth < 1025) {
uiStore.asideShow = false
}
})
export default router
-1
View File
@@ -13,7 +13,6 @@ export const useSettingStore = defineStore('setting', {
},
persist: {
storage: sessionStorage,
pick: ['lang'],
},
})
+18 -1
View File
@@ -21,4 +21,21 @@ export function cvtR2Url(key) {
domain = domain.slice(0, -1);
}
return domain + '/' + key
}
}
export function toOssDomain(domain) {
if (!domain) {
return null
}
if (!domain.startsWith('http')) {
return 'https://' + domain
}
if (domain.endsWith("/")) {
domain = domain.slice(0, -1);
}
return domain
}
+1 -1
View File
@@ -768,7 +768,7 @@ function createSendGauge() {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
@media (max-width: 1199px) {
@media (max-width: 1366px) {
grid-template-columns: 1fr 1fr;
gap: 15px;
}
+4 -3
View File
@@ -84,7 +84,7 @@ import {useAccountStore} from "@/store/account.js";
import {formatDetailDate} from "@/utils/day.js";
import {starAdd, starCancel} from "@/request/star.js";
import {getExtName, formatBytes} from "@/utils/file-utils.js";
import {cvtR2Url} from "@/utils/convert.js";
import {cvtR2Url,toOssDomain} from "@/utils/convert.js";
import {getIconByName} from "@/utils/icon-utils.js";
import {useSettingStore} from "@/store/setting.js";
import {allEmailDelete} from "@/request/all-email.js";
@@ -116,7 +116,8 @@ function toMessage(message) {
function formatImage(content) {
content = content || '';
const domain = settingStore.settings.r2Domain;
return content.replace(/{{domain}}/g, domain + '/');
console.log(domain)
return content.replace(/{{domain}}/g, toOssDomain(domain) + '/');
}
function showImage(key) {
@@ -258,7 +259,7 @@ const handleDelete = () => {
border-radius: 6px;
width: fit-content;
.att-box {
min-width: min(410px,calc(100vw - 53px));
min-width: min(410px,calc(100vw - 60px));
max-width: 600px;
display: grid;
gap: 12px;
+1 -1
View File
@@ -775,7 +775,7 @@ adjustWidth()
function adjustWidth() {
const width = window.innerWidth
statusShow.value = width > 1090
createTimeShow.value = width > 1200
createTimeShow.value = width > 1367
accountNumShow.value = width > 650
sendNumShow.value = width > 685
typeShow.value = width > 767
+5 -2
View File
@@ -20,8 +20,8 @@ export default defineConfig(({mode}) => {
registerType: 'autoUpdate', // 配置 service worker 的注册方式
includeAssets: ['favicon.svg', 'robots.txt'], // 指定需要包含的静态资源
manifest: {
name: 'Cloud Mail',
short_name: 'Cloud Mail',
name: env.VITE_PWA_NAME,
short_name: env.VITE_PWA_NAME,
background_color: '#FFFFFF',
theme_color: '#FFFFFF',
icons: [
@@ -32,6 +32,9 @@ export default defineConfig(({mode}) => {
}
],
},
workbox: {
navigateFallbackDenylist: [/^\/api\/init/]
}
}),
AutoImport({
resolvers: [ElementPlusResolver()],
+2 -1
View File
@@ -12,6 +12,7 @@ import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import roleService from '../service/role-service';
import verifyUtils from '../utils/verify-utils';
import r2Service from '../service/r2-service';
dayjs.extend(utc);
dayjs.extend(timezone);
@@ -152,7 +153,7 @@ export async function email(message, env, ctx) {
attachment.accountId = emailRow.accountId;
});
if (attachments.length > 0 && env.r2) {
if (attachments.length > 0 && await r2Service.hasOSS({env})) {
await attService.addAtt({ env }, attachments);
}
+3 -3
View File
@@ -14,15 +14,15 @@ app.onError((err, c) => {
}
if (err.message === `Cannot read properties of undefined (reading 'get')`) {
return c.text('初始化失败:KV数据库未绑定或变量名错误 Initialization failed: KV database not bound or invalid variable name');
return c.json(result.fail('初始化失败:KV数据库未绑定或变量名错误'));
}
if (err.message === `Cannot read properties of undefined (reading 'put')`) {
return c.text('初始化失败:KV数据库未绑定或变量名错误 Initialization failed: KV database not bound or invalid variable name');
return c.json(result.fail('初始化失败:KV数据库未绑定或变量名错误'));
}
if (err.message === `Cannot read properties of undefined (reading 'prepare')`) {
return c.text('初始化失败:D1数据库未绑定或变量名错误 Initialization failed: D1 database not bound or invalid variable name');
return c.json(result.fail('初始化失败:D1数据库未绑定或变量名错误'));
}
return c.json(result.fail(err.message, err.code));
-1
View File
@@ -96,7 +96,6 @@ const en = {
"系统设置": "System Settings",
"设置查看": "View Settings",
"设置修改": "Change Settings",
"物理清空": "Physical Purge",
"发件重置": "Reset Send Count"
}
};
-1
View File
@@ -96,7 +96,6 @@ const zh = {
"系统设置": "系统设置",
"设置查看": "设置查看",
"设置修改": "设置修改",
"物理清空": "物理清空",
'发件重置': '发件重置'
}
}
+2 -2
View File
@@ -21,7 +21,6 @@ const init = {
await this.v1_5DB(c);
await this.v1_6DB(c);
await this.v1_7DB(c);
await this.v1_7DB(c);
await this.v2DB(c);
await settingService.refresh(c);
return c.text(t('initSuccess'));
@@ -34,7 +33,8 @@ const init = {
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN region TEXT NOT NULL DEFAULT '';`),
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN endpoint TEXT NOT NULL DEFAULT '';`),
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN s3_access_key TEXT NOT NULL DEFAULT '';`),
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN s3_secret_key TEXT NOT NULL DEFAULT '';`)
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN s3_secret_key TEXT NOT NULL DEFAULT '';`),
c.env.db.prepare(`DELETE FROM perm WHERE perm_key = 'setting:clean'`)
]);
} catch (e) {
console.error(e.message)
+19 -9
View File
@@ -1,21 +1,31 @@
import orm from '../entity/orm';
import { att } from '../entity/att';
import { and, eq, isNull, inArray, notInArray } from 'drizzle-orm';
import { and, eq, isNull, inArray } from 'drizzle-orm';
import r2Service from './r2-service';
import constant from '../const/constant';
import fileUtils from '../utils/file-utils';
import { attConst } from '../const/entity-const';
import { parseHTML } from 'linkedom';
import domainUtils from '../utils/domain-uitls';
const attService = {
async addAtt(c, attachments) {
for (let attachment of attachments) {
await r2Service.putObj(c, attachment.key, attachment.content, {
let metadate = {
contentType: attachment.mimeType,
contentDisposition: `attachment; filename="${attachment.filename}"`
});
}
if (!attachment.contentId) {
metadate.contentDisposition = `attachment; filename="${attachment.filename}"`
} else {
metadate.contentDisposition = `inline; filename="${attachment.filename}"`
metadate.cacheControl = `max-age=604800`
}
await r2Service.putObj(c, attachment.key, attachment.content, metadate);
}
await orm(c).insert(att).values(attachments).run();
@@ -49,7 +59,7 @@ const attService = {
const file = fileUtils.base64ToFile(src);
const buff = await file.arrayBuffer();
const key = constant.ATTACHMENT_PREFIX + await fileUtils.getBuffHash(buff) + fileUtils.getExtFileName(file.name);
img.setAttribute('src', r2Domain + '/' + key);
img.setAttribute('src', domainUtils.toOssDomain(r2Domain) + '/' + key);
const attData = {};
attData.key = key;
@@ -108,7 +118,9 @@ const attService = {
attData.accountId = accountId;
attData.type = attConst.type.EMBED;
await r2Service.putObj(c, attData.key, attData.buff, {
contentType: attData.mimeType
contentType: attData.mimeType,
cacheControl: `max-age=604800`,
contentDisposition: `inline; filename="${attData.filename}"`
});
}
@@ -153,9 +165,7 @@ const attService = {
await this.batchDelete(c, delKeyList);
}
const delAttSql = fieldValues.map(value => c.env.db.prepare(`DELETE
FROM attachments
WHERE ${fieldName} = ?`).bind(value));
const delAttSql = fieldValues.map(value => c.env.db.prepare(`DELETE FROM attachments WHERE ${fieldName} = ?`).bind(value));
await c.env.db.batch(delAttSql);
},
+3
View File
@@ -18,6 +18,7 @@ import dayjs from 'dayjs';
import kvConst from '../const/kv-const';
import { t } from '../i18n/i18n'
import r2Service from './r2-service';
import domainUtils from '../utils/domain-uitls';
const emailService = {
@@ -407,6 +408,8 @@ const emailService = {
}
r2domain = domainUtils.toOssDomain(r2domain)
if (src && src.startsWith(r2domain + '/')) {
img.setAttribute('src', src.replace(r2domain + '/', '{{domain}}'));
}
+17 -3
View File
@@ -9,9 +9,23 @@ const s3Service = {
const { bucket } = await settingService.query(c);
await client.send(
new PutObjectCommand({ Bucket: bucket, Key: key, Body: content, ...metadata })
)
let obj = { Bucket: bucket, Key: key, Body: content,
CacheControl: metadata.cacheControl
}
if (metadata.cacheControl) {
obj.CacheControl = metadata.cacheControl
}
if (metadata.contentDisposition) {
obj.ContentDisposition = metadata.contentDisposition
}
if (metadata.contentType) {
obj.ContentType = metadata.contentType
}
await client.send(new PutObjectCommand(obj))
},
async deleteObj(c,keys) {
+3 -1
View File
@@ -119,7 +119,9 @@ const settingService = {
await r2Service.putObj(c, background, arrayBuffer, {
contentType: file.type
contentType: file.type,
cacheControl: `public, max-age=31536000, immutable`,
contentDisposition: `inline; filename="${file.name}"`
});
}
+3
View File
@@ -34,3 +34,6 @@ crons = ["0 16 * * *"] #定时任务每天晚上12点执行
#domain = [] #邮件域名可可配置多个 示例: ["example1.com","example2.com"]
#admin = "" #管理员的邮箱 示例: admin@example.com
#jwt_secret = "" #jwt令牌的密钥,随便填一串字符串
[build]
command = "pnpm --prefix ../mail-vue install && pnpm --prefix ../mail-vue run build"
+4 -3
View File
@@ -18,9 +18,10 @@ database_id = "a4c1a63a-6ef5-4e6d-8e8c-b6d9e8feb810"
binding = "kv"
id = "2io01d4b299e481b9de060ece9e7785c"
[[r2_buckets]]
binding = "r2"
bucket_name = "email"
#[[r2_buckets]]
#binding = "r2"
#bucket_name = "email"
[vars]
orm_log = false
+21 -17
View File
@@ -1,36 +1,40 @@
name = "cloud-mail-test"
name = "test-mail"
main = "src/index.js"
compatibility_date = "2025-06-04"
keep_vars = true
[observability]
enabled = true
head_sampling_rate = 1
[[d1_databases]]
binding = "db"
database_name = "email-test"
database_id = ""
binding = "db" #d1数据库绑定名默认不可修改
database_name = "test-email" #d1数据库名字
database_id = "e65f099d-796d-4eaa-8dff-529b368b20db" #d1数据库id
[[kv_namespaces]]
binding = "kv"
id = ""
binding = "kv" #kv绑定名默认不可修改
id = "9be10cd058e04c55b526f8c0c116f50c" #kv数据库id
[[r2_buckets]]
binding = "r2"
bucket_name = "email-test"
#(可选)
#[[r2_buckets]]
#binding = "r2" #r2对象存储绑定名默认不可修改
#bucket_name = "test-email" #r2对象存储桶的名字
[assets]
binding = "assets"
directory = "./dist"
binding = "assets" #静态资源绑定名默认不可修改
directory = "./dist" #前端vue项目打包的静态资源存放位置,默认dist
not_found_handling = "single-page-application"
run_worker_first = true
[triggers]
crons = ["0 16 * * *"]
crons = ["0 16 * * *"] #定时任务每天晚上12点执行
[vars]
orm_log = true
domain = ["", ""]
admin = ""
jwt_secret = ""
orm_log = false
domain = ["example.com"] #邮件域名可可配置多个 示例: ["example1.com","example2.com"]
admin = "admin@example.com" #管理员的邮箱 示例: admin@example.com
jwt_secret = "b7f29a1d-18e2-4d3b-941f-f6b2c97c02fd" #jwt令牌的密钥,随便填一串字符串
[build]
command = "pnpm --prefix ../mail-vue install && pnpm --prefix ../mail-vue run build"
+6 -5
View File
@@ -8,16 +8,17 @@ enabled = true
#[[d1_databases]]
#binding = "db" #d1数据库绑定名默认不可修改
#database_name = "" #d1数据库名字
#database_id = "" #d1数据库id
#database_name = "email" #d1数据库名字
#database_id = "b1b1a63a-6ef5-4e6d-8e8c-b6d9e8feb810" #d1数据库id
#
#[[kv_namespaces]]
#binding = "kv" #kv绑定名默认不可修改
#id = "" #kv数据库id
#id = "0fa01d4b299e481b9de060ece9e7785c" #kv数据库id
##(可选)
#[[r2_buckets]]
#binding = "r2" #r2对象存储绑定名默认不可修改
#bucket_name = "" #r2对象存储桶的名字
#bucket_name = "email" #r2对象存储桶的名字
[assets]
binding = "assets" #静态资源绑定名默认不可修改