初始化

This commit is contained in:
zg
2026-07-15 16:13:27 +08:00
parent 6d6f559793
commit c4023c1623
276 changed files with 42622 additions and 0 deletions

View File

@@ -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>