邮件审核功能
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled

This commit is contained in:
hahwu 2026-05-11 11:40:33 +08:00
parent a6427daaab
commit 2ac2d198a3
4 changed files with 271 additions and 87 deletions

View File

@ -5,6 +5,7 @@ export interface MailData {
ServerId: number;
PageSize?: number;
CurrentPage?: number;
audit_id?: number;
mail_id?: number;
title: string;
subtitle?: string;
@ -27,6 +28,11 @@ export interface MailData {
subTitle_es_latam?: string;
content_es_latam?: string;
min_level?: number;
status?: number;
applicant?: string;
reviewer?: string;
review_remark?: string;
review_time?: number;
}
export interface MailListParam {
@ -44,6 +50,14 @@ export async function addMailApi(data:MailData) {
return requestClient.post('/mail/send', data);
}
export async function deleteMailApi(AppId:number, ServerId:number, mail_id:number) {
return requestClient.post('/mail/delete', {mail_id:mail_id, AppId:AppId, ServerId:ServerId});
export async function approveMailApi(audit_id:number, review_remark = '') {
return requestClient.post('/mail/approve', { audit_id, review_remark });
}
export async function rejectMailApi(audit_id:number, review_remark = '') {
return requestClient.post('/mail/reject', { audit_id, review_remark });
}
export async function deleteMailApi(data: Pick<MailData, 'AppId' | 'ServerId' | 'audit_id' | 'mail_id'>) {
return requestClient.post('/mail/delete', data);
}

View File

@ -1,9 +1,10 @@
<script lang="ts" setup>
import type { MailData } from '#/api/core/mail';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import {Image} from "ant-design-vue";
import { formatItems } from '#/store/util';
import { Image } from 'ant-design-vue';
import { formatItems, formatUTC8Time } from '#/store/util';
import { Tag } from 'ant-design-vue';
import {ref} from "vue";
import { ref } from 'vue';
interface MailItem {
Id: string | number;
Num: number;
@ -34,11 +35,6 @@ const [Form, FormApi] = useVbenForm({
label: '邮件标题',
rules: 'required',
},
{
component: 'Input',
fieldName: 'Subtitle',
label: '邮件副标题',
},
{
component: 'Textarea',
fieldName: 'Content',
@ -55,11 +51,6 @@ const [Form, FormApi] = useVbenForm({
label: '英文邮件标题',
rules: 'required',
},
{
component: 'Input',
fieldName: 'SubtitleEN',
label: '英文邮件副标题',
},
{
component: 'Textarea',
fieldName: 'ContentEN',
@ -190,13 +181,55 @@ const [Form, FormApi] = useVbenForm({
triggerFields: ['send_type'],
},
},
{
component: 'Input',
fieldName: 'status_text',
label: '审核状态',
},
{
component: 'Input',
fieldName: 'applicant',
label: '申请人',
},
{
component: 'Input',
fieldName: 'reviewer',
label: '审核人',
},
{
component: 'Input',
fieldName: 'review_time_text',
label: '审核时间',
},
{
component: 'Textarea',
fieldName: 'review_remark',
label: '审核备注',
componentProps: {
rows: 3,
},
},
],
});
function getStatusText(status?: number) {
if (status === 1) {
return '待审核';
}
if (status === 2) {
return '已通过';
}
if (status === 3) {
return '已驳回';
}
return '已发送';
}
const [Modal, modalApi] = useVbenModal({
confirmText: '提交',
showConfirmButton: false,
onOpened: async () => {
const modalData = modalApi.getData();
const modalData = modalApi.getData<MailData>();
FormApi.setValues({
Title: modalData.title,
Subtitle: modalData.subtitle,
@ -215,8 +248,15 @@ const [Modal, modalApi] = useVbenModal({
mail_type: modalData.mail_type === 1 ? '普通邮件' : '节日邮件',
send_type: modalData.send_type === 1 ? '全服邮件' : '个人邮件',
ToUids: modalData.to_uids,
status_text: getStatusText(modalData.status),
applicant: modalData.applicant || '-',
reviewer: modalData.reviewer || '-',
review_time_text: modalData.review_time
? formatUTC8Time(modalData.review_time)
: '-',
review_remark: modalData.review_remark || '-',
});
items = formatItems(modalData.items);
items.value = formatItems(modalData.items);
FormApi.updateSchema([
{
component: 'Input',
@ -303,6 +343,31 @@ const [Modal, modalApi] = useVbenModal({
disabled: true,
fieldName: 'send_type',
},
{
component: 'Input',
disabled: true,
fieldName: 'status_text',
},
{
component: 'Input',
disabled: true,
fieldName: 'applicant',
},
{
component: 'Input',
disabled: true,
fieldName: 'reviewer',
},
{
component: 'Input',
disabled: true,
fieldName: 'review_time_text',
},
{
component: 'Textarea',
disabled: true,
fieldName: 'review_remark',
},
]);
},
});

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { useVbenForm, useVbenModal, VbenIcon } from '@vben/common-ui';
import { Select, Input, SelectOption, SelectOptGroup } from 'ant-design-vue';
import { Select, Input, SelectOption, SelectOptGroup, message } from 'ant-design-vue';
import dayjs from 'dayjs';
import { ref } from 'vue';
import { addMailApi } from '#/api/core/mail';
@ -29,11 +29,6 @@ const [Form, FormApi] = useVbenForm({
label: '邮件标题',
rules: 'required',
},
{
component: 'Input',
fieldName: 'Subtitle',
label: '邮件副标题',
},
{
component: 'Textarea',
fieldName: 'Content',
@ -50,11 +45,6 @@ const [Form, FormApi] = useVbenForm({
label: '英文邮件标题',
rules: 'required',
},
{
component: 'Input',
fieldName: 'SubtitleEN',
label: '英文邮件副标题',
},
{
component: 'Textarea',
fieldName: 'ContentEN',
@ -71,11 +61,6 @@ const [Form, FormApi] = useVbenForm({
label: '葡萄牙(巴西)邮件标题',
rules: 'required',
},
{
component: 'Input',
fieldName: 'SubtitlePTBR',
label: '葡萄牙(巴西)邮件副标题',
},
{
component: 'Textarea',
fieldName: 'ContentPTBR',
@ -92,11 +77,6 @@ const [Form, FormApi] = useVbenForm({
label: '西班牙(拉美)邮件标题',
rules: 'required',
},
{
component: 'Input',
fieldName: 'SubtitleESLatam',
label: '西班牙(拉美)邮件副标题',
},
{
component: 'Textarea',
fieldName: 'ContentESLatam',
@ -210,7 +190,7 @@ const [Form, FormApi] = useVbenForm({
],
});
const [Modal, modalApi] = useVbenModal({
confirmText: '提交',
confirmText: '提交审核',
onOpened: () => {
items.value = [];
},
@ -241,7 +221,7 @@ const [Modal, modalApi] = useVbenModal({
const modalData = modalApi.getData();
const param: MailData = {
AppId: modalData.AppId,
ServerId: modalData.ServerId,
ServerId: modalData.ServerId || 1,
title: Title,
subtitle: Subtitle,
content: Content,
@ -264,6 +244,7 @@ const [Modal, modalApi] = useVbenModal({
send_type: send_type,
};
await addMailApi(param);
message.success('邮件已提交审核');
modalApi.close();
},
});
@ -288,7 +269,7 @@ defineOptions({
});
</script>
<template>
<Modal :width="800" title="添加邮件">
<Modal :width="800" title="新增邮件申请">
<Form>
<template #Items>
<span>Id:</span>

View File

@ -1,22 +1,56 @@
<script setup lang="ts">
import { Page } from '@vben/common-ui';
import { Button, Card, Space, Image, Tag } from 'ant-design-vue';
import { useAccess } from '@vben/access';
import { Button, Card, Image, message, Modal, Space, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import type { VxeGridListeners, VxeGridProps } from '#/adapter/vxe-table';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { VbenFormProps } from '#/adapter/form';
import type { MailData } from '#/api/core/mail';
import { getServerListApi, getAppListApi } from '#/api/core/server';
import { getMailListApi, deleteMailApi } from '#/api/core/mail';
import {
approveMailApi,
deleteMailApi,
getMailListApi,
rejectMailApi,
} from '#/api/core/mail';
import type { AppData, ServerData } from '#/api/core/server';
import { useVbenModal } from '@vben/common-ui';
import { onMounted, ref } from 'vue';
import AddMailModal from './mail-info.vue';
import DetailMailModal from './mail-detail.vue';
import { getItemUrl, formatUTC8Time } from '#/store/util';
import { $t } from '#/locales'
import { $t } from '#/locales';
const appList = ref<AppData[]>([]);
const ServerList = ref<ServerData[]>([]);
const { hasAccessByRoles } = useAccess();
function getStatusText(status?: number) {
if (status === 1) {
return '待审核';
}
if (status === 2) {
return '已通过';
}
if (status === 3) {
return '已驳回';
}
return '已发送';
}
function getStatusColor(status?: number) {
if (status === 1) {
return 'processing';
}
if (status === 2) {
return 'success';
}
if (status === 3) {
return 'error';
}
return 'default';
}
const formOptions: VbenFormProps = {
//
collapsed: false,
@ -67,20 +101,36 @@ const formOptions: VbenFormProps = {
};
const gridOptions: VxeGridProps<MailData> = {
columns: [
{ field: 'mail_id', title: 'id' },
{ field: 'audit_id', title: '审核单ID', width: 100 },
{ field: 'mail_id', title: '邮件ID', width: 100 },
{ field: 'title', title: '邮件标题' },
// { field: 'title_en', title: '' },
{ field: 'subtitle', title: '邮件副标题' },
// { field: 'subtitleEN', title: '' },
{ field: 'content', title: '邮件内容' },
// { field: 'content_en', title: '' },
{ field: 'items', title: '道具', slots: { default: 'items' } },
{
field: 'start_time',
title: '开始时间',
formatter: ({ cellValue }) =>
cellValue ? formatUTC8Time(cellValue) : '',
slots: { header: 'time_header' },
{
field: 'status',
title: '审核状态',
slots: { default: 'status' },
},
{
field: 'applicant',
title: '申请人',
},
{
field: 'reviewer',
title: '审核人',
},
{
field: 'review_time',
title: '审核时间',
formatter: ({ cellValue }) =>
cellValue ? formatUTC8Time(cellValue) : '-',
},
{
field: 'start_time',
title: '开始时间',
formatter: ({ cellValue }) =>
cellValue ? formatUTC8Time(cellValue) : '',
slots: { header: 'time_header' },
},
{
field: 'mail_type',
@ -98,8 +148,13 @@ const gridOptions: VxeGridProps<MailData> = {
formatter: ({ cellValue }) => (cellValue ? cellValue : 0),
},
{ field: 'to_uids', title: '接收者' },
{ field: 'create_time', title: '创建时间' },
{ slots: { default: 'action' }, title: '操作', width: 100 },
{
field: 'create_time',
title: '申请时间',
formatter: ({ cellValue }) =>
cellValue ? formatUTC8Time(cellValue) : '-',
},
{ slots: { default: 'action' }, title: '操作', width: 280, fixed: 'right' },
],
minHeight: '650px',
pagerConfig: {},
@ -110,10 +165,10 @@ const gridOptions: VxeGridProps<MailData> = {
},
ajax: {
query: async ({ page }, formValues) => {
let AppId = parseInt(formValues.AppId, 10);
const AppId = Number(formValues.AppId || appList.value[0]?.AppId || 1);
return await getMailListApi({
AppId: AppId,
ServerId: 1,
ServerId: Number(formValues.ServerId || 1),
PageSize: page.pageSize,
CurrentPage: page.currentPage,
});
@ -125,18 +180,7 @@ const gridOptions: VxeGridProps<MailData> = {
isHover: true,
},
};
const gridEvents: VxeGridListeners<MailData> = {
cellClick: ({ row, column }) => {
if (column.field !== 'mail_id') {
return;
}
AddMailApi2.setData(row);
AddMailApi2.open();
// message.info(`cell-click: ${row.title}`);
},
};
const [Grid, GridApi] = useVbenVxeGrid({
gridEvents,
formOptions,
gridOptions,
});
@ -178,22 +222,74 @@ onMounted(async () => {
});
async function addMail() {
//console.log('addMail');
const Value = await GridApi.formApi.getValues();
AddMailApi.setData({ AppId: Value.AppId, ServerId: Value.ServerId });
AddMailApi.setData({
AppId: Number(Value.AppId || appList.value[0]?.AppId || 1),
ServerId: Number(Value.ServerId || 1),
});
AddMailApi.open();
}
async function deleteRow(row: MailData) {
//console.log(row);
GridApi.setLoading(true);
if (typeof row.mail_id == 'number') {
await deleteMailApi(row.AppId, row.ServerId, row.mail_id);
} else {
//console.error('Invalid mail_id:', row.mail_id);
function showDetail(row: MailData) {
AddMailApi2.setData(row);
AddMailApi2.open();
}
async function refreshGrid() {
await GridApi.query();
}
function approveRow(row: MailData) {
const auditId = row.audit_id;
if (!auditId) {
message.error('缺少审核单ID');
return;
}
GridApi.setLoading(false);
GridApi.query();
Modal.confirm({
title: '确认通过这条邮件审核吗?',
async onOk() {
await approveMailApi(auditId);
message.success('审核通过');
await refreshGrid();
},
});
}
function rejectRow(row: MailData) {
const auditId = row.audit_id;
if (!auditId) {
message.error('缺少审核单ID');
return;
}
Modal.confirm({
title: '确认驳回这条邮件申请吗?',
async onOk() {
await rejectMailApi(auditId);
message.success('已驳回');
await refreshGrid();
},
});
}
async function deleteRow(row: MailData) {
Modal.confirm({
title: '确认删除这条邮件记录吗?',
async onOk() {
GridApi.setLoading(true);
try {
await deleteMailApi({
AppId: row.AppId,
ServerId: row.ServerId,
audit_id: row.audit_id,
mail_id: row.mail_id,
});
message.success('删除成功');
await refreshGrid();
} finally {
GridApi.setLoading(false);
}
},
});
}
function fromatItems(items: string) {
@ -215,15 +311,18 @@ function fromatItems(items: string) {
<Page auto-content-height>
<AddMailM class="w-[50%]" />
<AddMailM2 class="w-[50%]" />
<Card class="mb-5" title="邮件操作">
<Card class="mb-5" title="邮件审核">
<Space>
<Button @click="addMail">新增邮件</Button>
<Button v-if="hasAccessByRoles(['super', 'AC7001'])" @click="addMail">新增邮件申请</Button>
</Space>
</Card>
<Grid>
</Card>
<Grid>
<template #time_header>
开始时间 <span style="color: red">(UTC+8)</span>
</template>
<template #status="{ row }">
<Tag :color="getStatusColor(row.status)">{{ getStatusText(row.status) }}</Tag>
</template>
<template #items="{row}">
<div class="flex flex-wrap items-center justify-center">
<div v-for="item in fromatItems(row.items)" :key="item.Id" class="flex items-center gap-1">
@ -240,7 +339,32 @@ function fromatItems(items: string) {
</div>
</template>
<template #action="{ row }">
<Button type="default" @click="deleteRow(row)">删除</Button>
<Space>
<Button type="primary" @click="showDetail(row)">详情</Button>
<Button
v-if="row.status === 1 && hasAccessByRoles(['super', 'AC7004'])"
type="primary"
@click="approveRow(row)"
>
通过
</Button>
<Button
v-if="row.status === 1 && hasAccessByRoles(['super', 'AC7005'])"
danger
type="primary"
@click="rejectRow(row)"
>
驳回
</Button>
<Button
v-if="hasAccessByRoles(['super', 'AC7003'])"
danger
type="primary"
@click="deleteRow(row)"
>
删除
</Button>
</Space>
</template>
</Grid>
</Page>