初始化
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user