Files
2026-07-15 16:13:27 +08:00

358 lines
12 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="email-page">
<!-- 搜索区域 -->
<el-card shadow="never" class="search-card">
<template #header>
<div class="card-header-title">
<el-icon><Search /></el-icon>
<span>搜索条件</span>
</div>
</template>
<el-form :model="query" inline class="search-form">
<el-form-item label="关键词">
<el-input v-model="query.keyword" placeholder="收件人或主题" clearable @keyup.enter="handleSearch" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="query.status" placeholder="全部" clearable style="width:120px">
<el-option label="全部" value="" />
<el-option label="成功" :value="1" />
<el-option label="失败" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button-group>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
<el-button @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
</el-button-group>
</el-form-item>
</el-form>
</el-card>
<!-- 操作栏 -->
<div class="page-toolbar">
<div class="toolbar-left">
<el-button type="primary" @click="openSendDialog" v-permission="'system:email:send'">
<el-icon><Promotion /></el-icon>发送邮件
</el-button>
</div>
<div class="toolbar-right">
<el-button @click="fetchData" :icon="RefreshRight" :loading="loading">刷新</el-button>
</div>
</div>
<!-- 发送记录表格 -->
<el-card shadow="never">
<el-table :data="list" v-loading="loading" stripe class="email-table">
<el-table-column prop="toAddress" label="收件人" min-width="180" show-overflow-tooltip />
<el-table-column prop="subject" label="主题" min-width="200" show-overflow-tooltip />
<el-table-column label="格式" width="80" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.contentType === 'html' ? '' : 'info'">
{{ row.contentType === 'html' ? 'HTML' : '文本' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.status === 1 ? 'success' : 'danger'" effect="dark">
{{ row.status === 1 ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="发送时间" width="170" />
<el-table-column label="操作" width="120" align="center" class-name="ops-col">
<template #default="{ row }">
<div class="actions">
<el-button size="small" text @click="showDetail(row)" class="btn-text">
<el-icon><View /></el-icon><span class="btn-label">详情</span>
</el-button>
<el-popconfirm title="确定删除?" @confirm="del(row.id)">
<template #reference>
<el-button size="small" text type="danger" class="btn-text">
<el-icon><Delete /></el-icon><span class="btn-label">删除</span>
</el-button>
</template>
</el-popconfirm>
</div>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无邮件发送记录" :image-size="100" />
</template>
</el-table>
<div class="pagination-wrapper" v-if="total > 0">
<el-pagination
v-model:current-page="page"
:page-size="pageSize"
:total="total"
layout="prev, pager, next"
@current-change="fetchData"
small
/>
</div>
</el-card>
<!-- 发送弹窗 -->
<el-dialog v-model="sendVisible" title="发送邮件" width="620px" destroy-on-close class="send-dialog">
<el-form :model="sendForm" label-width="70px">
<el-form-item label="收件人" required>
<el-input v-model="sendForm.to" placeholder="user@example.com" />
</el-form-item>
<el-form-item label="主题" required>
<el-input v-model="sendForm.subject" placeholder="邮件主题" />
</el-form-item>
<el-form-item label="格式">
<el-radio-group v-model="sendForm.contentType">
<el-radio value="html">HTML</el-radio>
<el-radio value="text">纯文本</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="内容" required>
<el-input v-model="sendForm.content" type="textarea" rows="8" placeholder="邮件内容HTML格式支持标签" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="sendVisible = false">取消</el-button>
<el-button type="primary" :loading="sending" @click="doSend">发送</el-button>
</template>
</el-dialog>
<!-- 详情弹窗 -->
<el-dialog v-model="detailVisible" title="邮件详情" width="660px" destroy-on-close class="detail-dialog">
<template v-if="detail">
<div class="detail-meta">
<span>收件人{{ detail.toAddress }}</span>
<span>时间{{ detail.createdAt }}</span>
<el-tag size="small" :type="detail.status === 1 ? 'success' : 'danger'" effect="dark">
{{ detail.status === 1 ? '成功' : '失败' }}
</el-tag>
</div>
<div class="detail-subject">{{ detail.subject }}</div>
<div class="detail-body" v-if="detail.contentType === 'html'" v-html="detail.content" />
<pre class="detail-body" v-else>{{ detail.content }}</pre>
<div v-if="detail.errorMsg" class="detail-error">错误{{ detail.errorMsg }}</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, RefreshLeft, RefreshRight, Promotion, View, Delete } from '@element-plus/icons-vue'
import request from '@/utils/request'
interface EmailItem {
id: string; toAddress: string; subject: string
content: string; contentType: string; status: number
errorMsg: string; createdAt: string
}
const loading = ref(false)
const list = ref<EmailItem[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(10)
// 搜索条件
const query = reactive({
keyword: '',
status: '' as number | '',
})
const sendVisible = ref(false)
const sending = ref(false)
const sendForm = reactive({ to: '', subject: '', content: '', contentType: 'html' })
const detailVisible = ref(false)
const detail = ref<EmailItem | null>(null)
async function fetchData() {
loading.value = true
try {
const res = await request.post('/zgapi/v1/admin/email/page', {
page: page.value,
pageSize: pageSize.value,
keyword: query.keyword || undefined,
status: query.status !== '' ? query.status : undefined,
})
list.value = res.data?.records || []
total.value = res.data?.total || 0
} catch { /* 拦截器已提示 */ } finally { loading.value = false }
}
function handleSearch() {
page.value = 1
fetchData()
}
function handleReset() {
query.keyword = ''
query.status = ''
page.value = 1
fetchData()
}
function openSendDialog() {
sendForm.to = ''; sendForm.subject = ''; sendForm.content = ''; sendForm.contentType = 'html'
sendVisible.value = true
}
async function doSend() {
if (!sendForm.to || !sendForm.subject || !sendForm.content) {
ElMessage.warning('请填写完整信息'); return
}
sending.value = true
try {
await request.post('/zgapi/v1/admin/email/send', { ...sendForm })
ElMessage.success('邮件已发送')
sendVisible.value = false
fetchData()
} catch { /* 拦截器已提示 */ } finally { sending.value = false }
}
async function showDetail(row: EmailItem) {
try {
const res = await request.get(`/zgapi/v1/admin/email/${row.id}`)
detail.value = res.data
detailVisible.value = true
} catch { /* ignore */ }
}
async function del(id: string) {
try {
await request.delete(`/zgapi/v1/admin/email/${id}`)
ElMessage.success('已删除')
if (list.value.length === 1 && page.value > 1) page.value--
fetchData()
} catch { /* ignore */ }
}
onMounted(() => fetchData())
</script>
<style scoped>
.email-page { max-width: 1100px; }
/* ====== 搜索卡片 ====== */
.search-card { margin-bottom: 16px; }
.card-header-title {
display: flex; align-items: center; gap: 6px;
font-size: 14px; font-weight: 600; color: var(--text-primary);
}
.search-form { margin-bottom: 0; }
.search-form .el-form-item { margin-bottom: 0; }
/* ====== 工具栏 ====== */
.page-toolbar {
display: flex; justify-content: space-between;
align-items: center; margin-bottom: 16px;
}
.toolbar-left { display: flex; gap: 8px; }
.toolbar-right { display: flex; gap: 8px; }
/* ====== 表格 ====== */
.email-table { width: 100%; }
.actions { display: flex; gap: 0; justify-content: center; }
.btn-text { padding: 5px 6px; }
/* ====== 分页 ====== */
.pagination-wrapper {
display: flex; justify-content: center; margin-top: 20px;
}
/* ====== 详情弹窗 ====== */
.detail-meta {
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
padding-bottom: 12px; border-bottom: 1px solid var(--border-color, #e4e7ed);
font-size: 13px; color: var(--text-muted, #909399);
}
.detail-subject {
font-size: 18px; font-weight: 600; color: var(--text-primary, #303133);
margin: 16px 0 12px; word-break: break-all;
}
.detail-body {
background: var(--bg-page, #f5f7fa); padding: 16px;
border-radius: 8px; font-size: 14px; line-height: 1.7;
max-height: 400px; overflow: auto; word-break: break-word;
}
.detail-body pre { white-space: pre-wrap; word-break: break-all; margin: 0; font-family: inherit; }
.detail-error {
margin-top: 12px; padding: 10px; color: #ef4444;
background: #fef2f2; border-radius: 6px; font-size: 13px; word-break: break-all;
}
/* ====== 移动端适配 ====== */
@media (max-width: 768px) {
/* 搜索表单 — 垂直排列 */
.search-form {
display: flex; flex-direction: column; gap: 8px;
}
.search-form .el-form-item {
width: 100%; margin-right: 0; display: block;
}
.search-form .el-input,
.search-form .el-select {
width: 100% !important;
}
.search-form .el-button-group {
width: 100%; display: flex;
}
.search-form .el-button-group .el-button {
flex: 1;
}
/* 工具栏 — 垂直排列 */
.page-toolbar {
flex-direction: column; gap: 10px; align-items: stretch;
}
.toolbar-left, .toolbar-right {
width: 100%;
}
.toolbar-left .el-button,
.toolbar-right .el-button {
width: 100%;
}
/* 表格 — 横向滚动 + 缩小列宽 */
.email-table {
font-size: 13px;
}
/* 操作列 — 手机端只显示图标 */
.btn-label { display: none; }
.btn-text { padding: 5px 4px; }
.actions { gap: 0; }
/* 弹窗 — 占满屏幕 */
:deep(.send-dialog), :deep(.detail-dialog) {
width: 95vw !important; max-width: 95vw;
}
:deep(.send-dialog .el-dialog__body),
:deep(.detail-dialog .el-dialog__body) {
padding: 16px;
}
/* 详情内容适配 */
.detail-subject { font-size: 16px; }
.detail-body {
max-height: 300px; font-size: 13px; padding: 12px;
}
/* 分页居中 */
.pagination-wrapper {
justify-content: center; flex-wrap: wrap;
}
}
@media (max-width: 480px) {
.email-table { font-size: 12px; }
.detail-meta { flex-direction: column; align-items: flex-start; gap: 6px; }
}
</style>