新增邮件发送和管理员功能

This commit is contained in:
eoao
2025-05-29 17:38:50 +08:00
parent de742d31e7
commit 2341d14b2d
364 changed files with 24202 additions and 1079 deletions

View File

@@ -1,6 +1,5 @@
import {useSettingStore} from "@/store/setting.js";
export function cvtR2Url(key) {
const settingStore = useSettingStore();
return 'https://' + settingStore.settings.r2Domain + '/' + key
return settingStore.settings.r2Domain + '/' + key
}

43
mail-vue/src/utils/day.js Normal file
View File

@@ -0,0 +1,43 @@
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.locale('zh-cn')
export function fromNow(date) {
const d = dayjs.utc(date).tz('Asia/Shanghai');
const now = dayjs();
const diffSeconds = now.diff(d, 'second');
const diffMinutes = now.diff(d, 'minute');
const diffHours = now.diff(d, 'hour');
const isToday = now.isSame(d, 'day');
if (isToday) {
if (diffSeconds < 60) return `几秒前`;
if (diffMinutes < 60) return `${diffMinutes}分钟前`;
if (diffHours >= 1 && diffHours < 2) return '1小时前';
return d.format('HH:mm');
}
else if (now.subtract(1, 'day').isSame(d, 'day')) {
return `昨天 ${d.format('HH:mm')}`;
}
else if (now.subtract(2, 'day').isSame(d, 'day')) {
return `前天 ${d.format('HH:mm')}`;
}
return d.year() === now.year()
? d.format('M月D日')
: d.format('YYYY/M/D');
}
export function formatDetailDate(time) {
return dayjs.utc(time).format('YYYY年M月D日 ddd AH:mm')
}
export function tzDayjs(time) {
return dayjs.utc(time).tz('Asia/Shanghai')
}

View File

@@ -1,3 +1,5 @@
import Compressor from "compressorjs";
export function getExtName(fileName) {
const index = fileName.lastIndexOf('.')
return index !== -1 ? fileName.slice(index + 1).toLowerCase() : ''
@@ -10,4 +12,38 @@ export function formatBytes(bytes) {
const i = Math.floor(Math.log(bytes) / Math.log(k));
const size = (bytes / Math.pow(k, i)).toFixed(2);
return `${size} ${units[i]}`;
}
}
export function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
});
}
export function base64Size(base64String) {
const padding = (base64String.match(/=*$/) || [''])[0].length;
const base64Length = base64String.length;
return (base64Length * 3) / 4 - padding;
}
export function compressImage(file, quality = 0.6) {
return new Promise((resolve, reject) => {
new Compressor(file, {
quality,
mimeType: 'image/jpeg',
convertSize: 1024 * 1024,
success(result) {
resolve(result);
},
error(err) {
reject(err);
},
});
});
}

View File

@@ -0,0 +1,16 @@
import {getExtName} from "@/utils/file-utils.js";
export function getIconByName(filename) {
const extName = getExtName(filename)
if (['zip', 'rar', '7z', 'tar', 'tgz'].includes(extName)) return 'octicon:file-zip-24';
if (['png', 'jpg', 'jpeg','gif','webp'].includes(extName)) return 'mingcute:pic-line';
if (['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv'].includes(extName)) return 'fluent:video-clip-24-regular';
if (['txt', 'doc', 'docx', 'md','ini','conf'].includes(extName)) return 'hugeicons:google-doc'
if (['xls', 'csv', 'xlsx'].includes(extName)) return 'codicon:table';
if (['mp3', 'wav', 'aac', 'ogg', 'flac', 'm4a'].includes(extName)) return 'mynaui:music';
if (['.ppt', 'pptx', 'pps', 'potx', 'pot'].includes(extName)) return 'lsicon:file-ppt-filled'
if (extName === 'pdf') return 'hugeicons:pdf-02';
if (extName === 'apk') return 'proicons:android';
if (extName === 'exe') return 'bi:filetype-exe';
return "hugeicons:attachment-01"
}

View File

@@ -0,0 +1,71 @@
import { useUserStore } from "@/store/user.js";
import { useSettingStore } from "@/store/setting.js";
import { useAccountStore } from "@/store/account.js";
import { loginUserInfo } from "@/request/my.js";
import { permsToRouter } from "@/utils/perm.js";
import router from "@/router";
import { settingQuery } from "@/request/setting.js";
import {cvtR2Url} from "@/utils/convert.js";
import {ElMessage} from "element-plus";
export async function init() {
document.title = '-'
const settingStore = useSettingStore();
const userStore = useUserStore();
const accountStore = useAccountStore();
const token = localStorage.getItem('token');
let setting = null;
if (token) {
const userPromise = loginUserInfo().catch(e => {
console.error(e);
return null;
});
const [s, user] = await Promise.all([settingQuery(), userPromise]);
setting = s;
settingStore.settings = setting;
settingStore.domainList = setting.domainList;
document.title = setting.title;
if (user) {
accountStore.currentAccountId = user.accountId;
userStore.user = user;
const routers = permsToRouter(user.permKeys);
routers.forEach(routerData => {
router.addRoute('layout', routerData);
});
}
} else {
setting = await settingQuery();
settingStore.settings = setting;
settingStore.domainList = setting.domainList;
document.title = setting.title;
}
const loading = document.getElementById('loading-first');
if (!setting.background) {
loading.remove();
return;
}
const img = new Image();
img.src = cvtR2Url(setting.background);
img.onload = () => {
loading.remove();
};
img.onerror = () => {
console.warn('背景图片加载失败:', img.src);
loading.remove();
};
}

View File

@@ -0,0 +1,60 @@
import {useUserStore} from "@/store/user.js";
export default function hasPerm(permKey) {
const {permKeys} = useUserStore().user;
return permKeys.includes('*') || permKeys.includes(permKey);
}
export function permsToRouter(permKeys) {
const routerList = []
Object.keys(routers).forEach(perm => {
if (permKeys.includes(perm) || permKeys.includes('*')) {
routerList.push(routers[perm])
}
})
return routerList;
}
const routers = {
'user:query': {
path: '/sys/user',
name: 'user',
component: () => import('@/views/user/index.vue'),
meta: {
title: '用户列表',
name: 'user',
menu: true
}
},
'role:query': {
path: '/sys/role',
name: 'role',
component: () => import('@/views/role/index.vue'),
meta: {
title: '权限控制',
name: 'role',
menu: true
}
},
'setting:query': {
path: '/sys/setting',
name: 'sys-setting',
component: () => import('@/views/sys-setting/index.vue'),
meta: {
title: '系统设置',
name: 'sys-setting',
menu: true
}
},
'sys-email:query': {
path: '/sys/email',
name: 'sys-email',
component: () => import('@/views/sys-email/index.vue'),
meta: {
title: '邮箱列表',
name: 'sys-email',
menu: true
}
}
}