新增全部用户查看删除邮箱

This commit is contained in:
eoao
2025-09-21 19:20:00 +08:00
parent 457304eafb
commit 82bd308d56
21 changed files with 244 additions and 77 deletions

8
mail-vue/jsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

View File

@@ -36,7 +36,7 @@
"terser": "^5.39.0",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.7.0",
"vite": "6.3.4",
"vite": "7.1.5",
"vite-plugin-pwa": "^1.0.3"
}
}

View File

@@ -62,7 +62,7 @@ importers:
devDependencies:
'@vitejs/plugin-vue':
specifier: ^5.2.1
version: 5.2.4(vite@6.3.4(less@4.4.1)(sass@1.91.0)(terser@5.43.1))(vue@3.5.20)
version: 5.2.4(vite@7.1.5(less@4.4.1)(sass@1.91.0)(terser@5.43.1))(vue@3.5.20)
less:
specifier: ^4.2.2
version: 4.4.1
@@ -79,11 +79,11 @@ importers:
specifier: ^28.7.0
version: 28.8.0(@babel/parser@7.28.3)(vue@3.5.20)
vite:
specifier: 6.3.4
version: 6.3.4(less@4.4.1)(sass@1.91.0)(terser@5.43.1)
specifier: 7.1.5
version: 7.1.5(less@4.4.1)(sass@1.91.0)(terser@5.43.1)
vite-plugin-pwa:
specifier: ^1.0.3
version: 1.0.3(vite@6.3.4(less@4.4.1)(sass@1.91.0)(terser@5.43.1))(workbox-build@7.3.0)(workbox-window@7.3.0)
version: 1.0.3(vite@7.1.5(less@4.4.1)(sass@1.91.0)(terser@5.43.1))(workbox-build@7.3.0)(workbox-window@7.3.0)
packages:
@@ -2217,6 +2217,10 @@ packages:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -2343,19 +2347,19 @@ packages:
'@vite-pwa/assets-generator':
optional: true
vite@6.3.4:
resolution: {integrity: sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
vite@7.1.5:
resolution: {integrity: sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
'@types/node': ^20.19.0 || >=22.12.0
jiti: '>=1.21.0'
less: '*'
less: ^4.0.0
lightningcss: ^1.21.0
sass: '*'
sass-embedded: '*'
stylus: '*'
sugarss: '*'
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
@@ -3501,9 +3505,9 @@ snapshots:
'@types/web-bluetooth@0.0.21': {}
'@vitejs/plugin-vue@5.2.4(vite@6.3.4(less@4.4.1)(sass@1.91.0)(terser@5.43.1))(vue@3.5.20)':
'@vitejs/plugin-vue@5.2.4(vite@7.1.5(less@4.4.1)(sass@1.91.0)(terser@5.43.1))(vue@3.5.20)':
dependencies:
vite: 6.3.4(less@4.4.1)(sass@1.91.0)(terser@5.43.1)
vite: 7.1.5(less@4.4.1)(sass@1.91.0)(terser@5.43.1)
vue: 3.5.20
'@vue/compiler-core@3.5.20':
@@ -4821,6 +4825,11 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -4962,25 +4971,25 @@ snapshots:
dependencies:
inherits: 2.0.3
vite-plugin-pwa@1.0.3(vite@6.3.4(less@4.4.1)(sass@1.91.0)(terser@5.43.1))(workbox-build@7.3.0)(workbox-window@7.3.0):
vite-plugin-pwa@1.0.3(vite@7.1.5(less@4.4.1)(sass@1.91.0)(terser@5.43.1))(workbox-build@7.3.0)(workbox-window@7.3.0):
dependencies:
debug: 4.4.1
pretty-bytes: 6.1.1
tinyglobby: 0.2.14
vite: 6.3.4(less@4.4.1)(sass@1.91.0)(terser@5.43.1)
vite: 7.1.5(less@4.4.1)(sass@1.91.0)(terser@5.43.1)
workbox-build: 7.3.0
workbox-window: 7.3.0
transitivePeerDependencies:
- supports-color
vite@6.3.4(less@4.4.1)(sass@1.91.0)(terser@5.43.1):
vite@7.1.5(less@4.4.1)(sass@1.91.0)(terser@5.43.1):
dependencies:
esbuild: 0.25.9
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.50.0
tinyglobby: 0.2.14
tinyglobby: 0.2.15
optionalDependencies:
fsevents: 2.3.3
less: 4.4.1

View File

@@ -66,7 +66,9 @@ http.interceptors.response.use((res) => {
})
reject(data)
}
resolve(data.data)
setTimeout(() => {
resolve(data.data)
},0)
})
},
(error) => {

View File

@@ -1,5 +1,5 @@
const en = {
inbox: 'Inbox',
'收件箱': 'Inbox',
drafts: 'Drafts',
sent: 'Sent',
starred: 'Starred',
@@ -14,6 +14,8 @@ const en = {
noMessagesFound: 'No messages found',
addAccount: 'Add Account',
emailAccount: 'Email',
account: 'Account',
userAccount: 'User Accounts',
deleteUser: 'Delete Account',
deleteUserBtn: 'Delete',
changePassword: 'Change Password',

View File

@@ -1,5 +1,5 @@
const zh = {
inbox: '收件箱',
'收件箱': '收件箱',
drafts: '草稿箱',
sent: '已发送',
starred: '星标邮件',
@@ -14,6 +14,8 @@ const zh = {
noMessagesFound: '没有任何邮件',
addAccount: '添加邮箱',
emailAccount: '邮箱',
account: '邮箱',
userAccount: '用户邮箱',
deleteUser: '删除账户',
deleteUserBtn: '删除账户',
changePassword: '修改密码',

View File

@@ -9,7 +9,7 @@
<el-menu-item @click="router.push({name: 'email'})" index="email"
:class="route.meta.name === 'email' ? 'choose-item' : ''">
<Icon icon="hugeicons:mailbox-01" width="20" height="20" />
<span class="menu-name" style="margin-left: 21px">{{$t('inbox')}}</span>
<span class="menu-name" style="margin-left: 21px">{{$t('收件箱')}}</span>
</el-menu-item>
<el-menu-item @click="router.push({name: 'send'})" index="send" v-perm="'email:send'"
:class="route.meta.name === 'send' ? 'choose-item' : ''">

View File

@@ -10,17 +10,6 @@
</div>
</div>
<div class="toolbar">
<el-dropdown>
<div class="translate icon-item">
<Icon icon="carbon:ibm-watson-language-translator"/>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="changeLang('zh')">简体中文</el-dropdown-item>
<el-dropdown-item @click="changeLang('en')">English</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div v-if="uiStore.dark" class="sun-icon icon-item" @click="openDark($event)">
<Icon icon="mingcute:sun-fill"/>
</div>
@@ -449,11 +438,6 @@ function formatName(email) {
font-size: 24px;
}
.translate {
padding-top: 2px;
font-size: 21px;
}
.avatar {
display: flex;
align-items: center;

View File

@@ -32,4 +32,12 @@ export function userRestSendCount(userId) {
export function userRestore(userId,type) {
return http.put('/user/restore', {userId,type})
}
}
export function userAllAccount(userId, num, size) {
return http.get('/user/allAccount', {params:{userId,num,size}})
}
export function userDeleteAccount(accountId) {
return http.delete('/user/deleteAccount', {params:{accountId}})
}

View File

@@ -16,7 +16,7 @@ const routes = [
name: 'email',
component: () => import('@/views/email/index.vue'),
meta: {
title: 'inbox',
title: '收件箱',
name: 'email',
menu: true
}

View File

@@ -11,8 +11,5 @@ export const useSettingStore = defineStore('setting', {
}),
actions: {
},
persist: {
pick: ['lang'],
},
}
})

View File

@@ -143,11 +143,14 @@
<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 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>
<template v-if="props.row.type !== 0">
<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>
</template>
<el-dropdown-item @click="openAccountList(props.row.userId)">{{ $t('account') }}</el-dropdown-item>
<el-dropdown-item @click="delUser(props.row)" v-if="props.row.type !== 0">{{ $t('delete') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -237,6 +240,44 @@
</el-button>
</div>
</el-dialog>
<el-dialog class="account-dialog" v-model="accountShow" :title="t('userAccount')" @closed="resetAccountList" >
<el-table :data="accountList" style="height: 480px" v-loading="accountLoading" element-loading-background="transparent" :empty-text="accountLoading ? '' : null">
<el-table-column property="email" :label="t('emailAccount')" >
<template #default="props">
<div class="email-row">{{ props.row.email }}</div>
</template>
</el-table-column>
<el-table-column property="address" :label="t('tabStatus')" :width="locale === 'en' ? 75 : 65" >
<template #default="props">
<el-tag type="primary" disable-transitions v-if="props.row.isDel === 0">{{$t('active')}}</el-tag>
<el-tag type="info" disable-transitions v-if="props.row.isDel === 1">{{$t('deleted')}}</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('action')" :width="locale === 'en' ? 75 : 65" >
<template #default="props">
<el-dropdown trigger="click">
<el-button type="primary" size="small">{{t('action')}}</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="deleteAccount(props.row)">{{ $t('delete') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<div class="account-pagination">
<el-pagination
:disabled="accountLoading"
background
layout="prev, pager, next"
:pager-count="3"
:total="accountParams.total"
@current-change="accountCurChange"
/>
</div>
</el-dialog>
</div>
</template>
@@ -249,7 +290,10 @@ import {
userSetStatus,
userSetType,
userAdd,
userRestSendCount, userRestore
userRestSendCount,
userRestore,
userDeleteAccount,
userAllAccount
} from '@/request/user.js'
import {roleSelectUse} from "@/request/role.js";
import {Icon} from "@iconify/vue";
@@ -289,6 +333,7 @@ const users = ref([])
const total = ref(0)
const first = ref(true)
const scrollbarRef = ref(null)
const accountLoading = ref(false)
const domainList = settingStore.domainList
@@ -314,6 +359,7 @@ const userForm = reactive({
})
const showAdd = ref(false)
const accountShow = ref(false)
const addLoading = ref(false);
const setTypeShow = ref(false)
const setPwdShow = ref(false)
@@ -323,6 +369,13 @@ const tableLoading = ref(true)
const roleList = reactive([])
const mySelect = ref({})
const key = ref(0)
const accountList = reactive([])
const accountParams = reactive({
size: 10,
num: 0,
total: 0,
userId: 0,
})
roleSelectUse().then(list => {
roleList.length = 0
@@ -331,11 +384,11 @@ roleSelectUse().then(list => {
const paramsStar = localStorage.getItem('user-params')
if (paramsStar) {
const locaParams = JSON.parse(paramsStar)
params.num = locaParams.num
params.size = locaParams.size
params.timeSort = locaParams.timeSort
params.status = locaParams.status
const localParams = JSON.parse(paramsStar)
params.num = localParams.num
params.size = localParams.size
params.timeSort = localParams.timeSort
params.status = localParams.status
}
watch(() => params, () => {
@@ -363,6 +416,50 @@ const filterItem = reactive({
receive: ['normal', 'del']
})
function deleteAccount(account) {
ElMessageBox.confirm(t('delConfirm', {msg: account.email}), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
userDeleteAccount(account.accountId).then(() => {
getAccountList()
ElMessage({
message: t('删除成功'),
type: "success",
plain: true
})
})
});
}
function accountCurChange(e) {
accountParams.num = e
getAccountList()
}
function resetAccountList() {
accountList.length = 0
accountParams.num = 0
accountParams.size = 10
accountParams.total = 0
}
function openAccountList(userId) {
accountParams.userId = userId
getAccountList(true)
accountShow.value = true
}
function getAccountList(loading = false) {
accountLoading.value = loading
userAllAccount(accountParams.userId,accountParams.num, accountParams.size).then(({list,total}) => {
accountList.length = 0
accountList.push(...list)
accountParams.total = total
accountLoading.value = false
})
}
function tableFilter(e) {
if (e.send) filterItem.send = e.send
@@ -820,6 +917,15 @@ function adjustWidth() {
}
}
:deep(.account-dialog) {
width: 500px !important;
@media (max-width: 540px) {
width: calc(100% - 40px) !important;
margin-right: 20px !important;
margin-left: 20px !important;
}
}
.header-actions {
padding: 9px 15px;
display: flex;
@@ -889,6 +995,12 @@ function adjustWidth() {
}
}
.account-pagination {
display: flex;
justify-content: end;
width: 100%;
}
.pagination {
margin-top: 15px;
margin-bottom: 20px;
@@ -982,15 +1094,6 @@ function adjustWidth() {
background: var(--el-bg-color);
}
:deep(.el-dialog) {
width: 400px !important;
@media (max-width: 440px) {
width: calc(100% - 40px) !important;
margin-right: 20px !important;
margin-left: 20px !important;
}
}
:deep(.cell) {
white-space: normal;
overflow: visible;
@@ -1020,4 +1123,4 @@ function adjustWidth() {
:deep(.el-message-box__container) {
align-items: start;
}
</style>
</style>

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,7 @@ import app from '../hono/hono';
import userService from '../service/user-service';
import result from '../model/result';
import userContext from '../security/user-context';
import accountService from '../service/account-service';
app.delete('/user/delete', async (c) => {
await userService.physicsDelete(c, c.req.query());
@@ -42,3 +43,15 @@ app.put('/user/restore', async (c) => {
await userService.restore(c, await c.req.json());
return c.json(result.ok());
});
app.get('/user/allAccount', async (c) => {
const data = await accountService.allAccount(c, c.req.query());
return c.json(result.ok(data));
});
app.delete('/user/deleteAccount', async (c) => {
await accountService.physicsDelete(c, c.req.query());
return c.json(result.ok());
});

View File

@@ -43,6 +43,8 @@ const requirePerms = [
'/user/list',
'/user/resetSendCount',
'/user/add',
'/user/deleteAccount',
'/user/allAccount',
'/regKey/add',
'/regKey/list',
'/regKey/delete',
@@ -61,13 +63,13 @@ const premKey = {
'role:set': ['/role/set','/role/setDefault'],
'role:query': ['/role/list', '/role/tree'],
'role:delete': ['/role/delete'],
'user:query': ['/user/list'],
'user:query': ['/user/list','/user/allAccount'],
'user:add': ['/user/add'],
'user:reset-send': ['/user/resetSendCount'],
'user:set-pwd': ['/user/setPwd'],
'user:set-status': ['/user/setStatus'],
'user:set-type': ['/user/setType'],
'user:delete': ['/user/delete'],
'user:delete': ['/user/delete','/user/deleteAccount'],
'all-email:query': ['/allEmail/list'],
'all-email:delete': ['/allEmail/delete','/allEmail/batchDelete'],
'setting:query': ['/setting/query'],

View File

@@ -5,7 +5,7 @@ import userService from './user-service';
import emailService from './email-service';
import orm from '../entity/orm';
import account from '../entity/account';
import { and, asc, eq, gt, inArray, count, sql } from 'drizzle-orm';
import { and, asc, eq, gt, inArray, count, sql, ne } from 'drizzle-orm';
import { isDel, settingConst } from '../const/entity-const';
import settingService from './setting-service';
import turnstileService from './turnstile-service';
@@ -195,7 +195,37 @@ const accountService = {
throw new BizError(t('usernameLengthLimit'));
}
await orm(c).update(account).set({name}).where(and(eq(account.userId, userId),eq(account.accountId, accountId))).run();
},
async allAccount(c, params) {
let { userId, num, size } = params
userId = Number(userId)
num = Number(num)
size = Number(size)
if (size > 30) {
size = 30;
}
num = (num - 1) * size;
const userRow = await userService.selectByIdIncludeDel(c, userId);
const list = await orm(c).select().from(account).where(and(eq(account.userId, userId),ne(account.email,userRow.email))).limit(size).offset(num);
const { total } = await orm(c).select({ total: count() }).from(account).where(eq(account.userId, userId)).get();
return { list, total }
},
async physicsDelete(c, params) {
const { accountId } = params
await emailService.physicsDeleteByAccountId(c, accountId)
await orm(c).delete(account).where(eq(account.accountId, accountId)).run();
}
};
export default accountService;

View File

@@ -191,6 +191,11 @@ const attService = {
await r2Service.delete(c, batch);
}
},
async removeByAccountId(c, accountId) {
console.log(accountId)
await this.removeAttByField(c, "account_id", [accountId])
}
};

View File

@@ -655,6 +655,11 @@ const emailService = {
await attService.removeByEmailIds(c, emailIds);
await orm(c).delete(email).where(conditions.length > 1 ? and(...conditions) : conditions[0]).run();
},
async physicsDeleteByAccountId(c, accountId) {
await attService.removeByAccountId(c, accountId);
await orm(c).delete(email).where(eq(email.accountId, accountId)).run();
}
};

View File

@@ -74,6 +74,10 @@ const userService = {
return orm(c).select().from(user).where(sql`${user.email} COLLATE NOCASE = ${email}`).get();
},
selectByIdIncludeDel(c, userId) {
return orm(c).select().from(user).where(eq(user.userId, userId)).get();
},
selectById(c, userId) {
return orm(c).select().from(user).where(
and(

6
package-lock.json generated
View File

@@ -1,6 +0,0 @@
{
"name": "cloud-mail",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}