feat(filter): 在日志页面顶部集成发件人黑白名单管理面板
Some checks are pending
Security Checks / security (push) Waiting to run
Some checks are pending
Security Checks / security (push) Waiting to run
This commit is contained in:
parent
bbb671c5ab
commit
a717183e55
@ -1,16 +1,26 @@
|
||||
.log-settings {
|
||||
.log-settings-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.log-settings {
|
||||
min-height: 480px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 8px 10px 4px 2px;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 只为日志页建立确定高度,滚动限制在表格内,不改变其他设置页。 */
|
||||
:global(.settings-tabs .el-tab-pane:has(> .log-settings)) {
|
||||
/* 只为日志页建立确定高度,滚动限制在容器内,不改变其他设置页。 */
|
||||
:global(.settings-tabs .el-tab-pane:has(> .log-settings-container)) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@ -105,7 +115,7 @@
|
||||
|
||||
.log-content {
|
||||
flex: 1;
|
||||
min-height: 120px;
|
||||
min-height: 260px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -361,4 +371,4 @@
|
||||
.log-categories :deep(.el-radio-button__inner) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,173 +1,179 @@
|
||||
<template>
|
||||
<section class="log-settings">
|
||||
<header class="log-heading">
|
||||
<h2>日志记录</h2>
|
||||
<span class="log-retention">保留 30 天</span>
|
||||
</header>
|
||||
<el-radio-group v-model="category" class="log-categories" aria-label="日志分类">
|
||||
<el-radio-button v-for="item in categories" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div class="log-filters">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="datetimerange"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
aria-label="日志时间范围"
|
||||
/>
|
||||
<el-input v-if="!isRuntime && canFilterActor" v-model="actorId" clearable placeholder="操作者编号" aria-label="操作者编号" @keyup.enter="search" />
|
||||
<div class="log-filter-actions">
|
||||
<el-button :disabled="loading" @click="recent">最近 24 小时</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="search">查询</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-alert v-if="error" :title="error" type="error" :closable="false" show-icon />
|
||||
<div v-loading="loading" class="log-content" :aria-busy="loading">
|
||||
<div class="log-table-heading">
|
||||
<div class="log-table-title">
|
||||
<strong>{{ currentCategory }}</strong>
|
||||
<span class="log-file-meta">最新在前 · 每页 10 条</span>
|
||||
</div>
|
||||
<div v-if="isRuntime" class="log-summary" aria-label="本页日志统计">
|
||||
<span>本页 {{ runtimeSummary.total }} 条</span>
|
||||
<span class="log-stat-info"><i aria-hidden="true" />信息 {{ runtimeSummary.info }}</span>
|
||||
<span class="log-stat-warning"><i aria-hidden="true" />警告 {{ runtimeSummary.warning }}</span>
|
||||
<span class="log-stat-danger"><i aria-hidden="true" />错误 {{ runtimeSummary.error }}</span>
|
||||
<div class="log-settings-container">
|
||||
<!-- 日志上方:发件人黑白名单管理(支持添加、删除、拉黑/转白,仅当前用户维护自身名单) -->
|
||||
<SenderFilterManagement />
|
||||
|
||||
<section class="log-settings">
|
||||
<header class="log-heading">
|
||||
<h2>日志记录</h2>
|
||||
<span class="log-retention">保留 30 天</span>
|
||||
</header>
|
||||
<el-radio-group v-model="category" class="log-categories" aria-label="日志分类">
|
||||
<el-radio-button v-for="item in categories" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div class="log-filters">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="datetimerange"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
aria-label="日志时间范围"
|
||||
/>
|
||||
<el-input v-if="!isRuntime && canFilterActor" v-model="actorId" clearable placeholder="操作者编号" aria-label="操作者编号" @keyup.enter="search" />
|
||||
<div class="log-filter-actions">
|
||||
<el-button :disabled="loading" @click="recent">最近 24 小时</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="search">查询</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table
|
||||
v-if="isRuntime"
|
||||
:data="runtimeRows"
|
||||
class="log-table runtime-table"
|
||||
height="100%"
|
||||
row-key="sequence"
|
||||
:row-class-name="runtimeRowClass"
|
||||
empty-text="暂无运行日志"
|
||||
@row-click="row => runtimeDetail = row"
|
||||
<el-alert v-if="error" :title="error" type="error" :closable="false" show-icon />
|
||||
<div v-loading="loading" class="log-content" :aria-busy="loading">
|
||||
<div class="log-table-heading">
|
||||
<div class="log-table-title">
|
||||
<strong>{{ currentCategory }}</strong>
|
||||
<span class="log-file-meta">最新在前 · 每页 10 条</span>
|
||||
</div>
|
||||
<div v-if="isRuntime" class="log-summary" aria-label="本页日志统计">
|
||||
<span>本页 {{ runtimeSummary.total }} 条</span>
|
||||
<span class="log-stat-info"><i aria-hidden="true" />信息 {{ runtimeSummary.info }}</span>
|
||||
<span class="log-stat-warning"><i aria-hidden="true" />警告 {{ runtimeSummary.warning }}</span>
|
||||
<span class="log-stat-danger"><i aria-hidden="true" />错误 {{ runtimeSummary.error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-table
|
||||
v-if="isRuntime"
|
||||
:data="runtimeRows"
|
||||
class="log-table runtime-table"
|
||||
height="100%"
|
||||
row-key="sequence"
|
||||
:row-class-name="runtimeRowClass"
|
||||
empty-text="暂无运行日志"
|
||||
@row-click="row => runtimeDetail = row"
|
||||
>
|
||||
<el-table-column label="时间(本地)" width="116">
|
||||
<template #default="{ row }">
|
||||
<div class="log-time"><strong>{{ row.time }}</strong><span>{{ row.date }}</span></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="级别" width="76" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type" size="small" effect="light" class="log-level-tag">{{ row.label }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作与结果说明" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<strong class="log-action">{{ row.action }}</strong>
|
||||
<div class="log-message">{{ row.outcome }}</div>
|
||||
<div v-if="row.object" class="log-source">对象:{{ row.object }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="92" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="resultType(row.result)" size="small" class="log-level-tag">{{ resultLabel(row.result) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="详情" width="60" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :aria-label="`查看第 ${row.sequence} 条日志详情`" @click.stop="runtimeDetail = row">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table v-else :data="items" class="log-table" height="100%" row-key="id" stripe empty-text="暂无日志记录" @row-click="item => detail = item">
|
||||
<el-table-column label="时间(本地)" width="116">
|
||||
<template #default="{ row }">
|
||||
<div class="log-time"><strong>{{ timeParts(row.createdAt).time }}</strong><span>{{ timeParts(row.createdAt).date }}</span></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作与结果说明" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<strong class="log-action">{{ actionLabel(row.action) }}</strong>
|
||||
<div class="log-message">{{ displayOutcome(row) }}</div>
|
||||
<div class="log-source">{{ displayObject(row) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作者" min-width="85" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ displayActor(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="82" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="resultType(row.result)" size="small" class="log-level-tag">{{ resultLabel(row.result) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="详情" width="60" align="center">
|
||||
<template #default="{ row }"><el-button link type="primary" @click.stop="detail = row">查看</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<footer class="log-pagination">
|
||||
<span aria-live="polite">{{ pageRange }}</span>
|
||||
<div class="log-page-actions">
|
||||
<el-button size="small" :disabled="loading || offset === 0" @click="previous">上一页</el-button>
|
||||
<el-button size="small" :disabled="loading || !hasMore" @click="next">下一页</el-button>
|
||||
</div>
|
||||
</footer>
|
||||
<el-dialog
|
||||
:model-value="Boolean(runtimeDetail)"
|
||||
title="运行日志详情"
|
||||
width="min(760px, 92vw)"
|
||||
append-to-body
|
||||
@close="runtimeDetail = null"
|
||||
>
|
||||
<el-table-column label="时间(本地)" width="116">
|
||||
<template #default="{ row }">
|
||||
<div class="log-time"><strong>{{ row.time }}</strong><span>{{ row.date }}</span></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="级别" width="76" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type" size="small" effect="light" class="log-level-tag">{{ row.label }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作与结果说明" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<strong class="log-action">{{ row.action }}</strong>
|
||||
<div class="log-message">{{ row.outcome }}</div>
|
||||
<div v-if="row.object" class="log-source">对象:{{ row.object }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="92" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="resultType(row.result)" size="small" class="log-level-tag">{{ resultLabel(row.result) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="详情" width="60" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :aria-label="`查看第 ${row.sequence} 条日志详情`" @click.stop="runtimeDetail = row">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table v-else :data="items" class="log-table" height="100%" row-key="id" stripe empty-text="暂无日志记录" @row-click="item => detail = item">
|
||||
<el-table-column label="时间(本地)" width="116">
|
||||
<template #default="{ row }">
|
||||
<div class="log-time"><strong>{{ timeParts(row.createdAt).time }}</strong><span>{{ timeParts(row.createdAt).date }}</span></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作与结果说明" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<strong class="log-action">{{ actionLabel(row.action) }}</strong>
|
||||
<div class="log-message">{{ displayOutcome(row) }}</div>
|
||||
<div class="log-source">{{ displayObject(row) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作者" min-width="85" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ displayActor(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="82" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="resultType(row.result)" size="small" class="log-level-tag">{{ resultLabel(row.result) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="详情" width="60" align="center">
|
||||
<template #default="{ row }"><el-button link type="primary" @click.stop="detail = row">查看</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<footer class="log-pagination">
|
||||
<span aria-live="polite">{{ pageRange }}</span>
|
||||
<div class="log-page-actions">
|
||||
<el-button size="small" :disabled="loading || offset === 0" @click="previous">上一页</el-button>
|
||||
<el-button size="small" :disabled="loading || !hasMore" @click="next">下一页</el-button>
|
||||
</div>
|
||||
</footer>
|
||||
<el-dialog
|
||||
:model-value="Boolean(runtimeDetail)"
|
||||
title="运行日志详情"
|
||||
width="min(760px, 92vw)"
|
||||
append-to-body
|
||||
@close="runtimeDetail = null"
|
||||
>
|
||||
<div v-if="runtimeDetail" class="runtime-detail">
|
||||
<div class="runtime-detail-heading">
|
||||
<el-tag :type="runtimeDetail.type" effect="light">{{ runtimeDetail.label }}</el-tag>
|
||||
<span>{{ formatTime(runtimeDetail.createdAt) }}</span>
|
||||
<span class="runtime-detail-sequence">第 {{ runtimeDetail.sequence }} 条</span>
|
||||
<div v-if="runtimeDetail" class="runtime-detail">
|
||||
<div class="runtime-detail-heading">
|
||||
<el-tag :type="runtimeDetail.type" effect="light">{{ runtimeDetail.label }}</el-tag>
|
||||
<span>{{ formatTime(runtimeDetail.createdAt) }}</span>
|
||||
<span class="runtime-detail-sequence">第 {{ runtimeDetail.sequence }} 条</span>
|
||||
</div>
|
||||
<dl class="log-detail-grid runtime-detail-meta">
|
||||
<dt>触发操作</dt><dd>{{ runtimeDetail.action }}</dd>
|
||||
<dt>操作对象</dt><dd>{{ runtimeDetail.object || '未记录' }}</dd>
|
||||
<dt>操作结果</dt><dd>{{ resultLabel(runtimeDetail.result) }}</dd>
|
||||
<dt>结果说明</dt><dd>{{ runtimeDetail.outcome }}</dd>
|
||||
<dt>代码来源</dt><dd>{{ runtimeDetail.source || '未记录' }}</dd>
|
||||
<template v-if="runtimeDetail.requestId">
|
||||
<dt>请求标识</dt><dd>{{ runtimeDetail.requestId }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
<h3>完整内容</h3>
|
||||
<pre class="runtime-detail-message">{{ runtimeDetail.message }}</pre>
|
||||
<details class="runtime-raw">
|
||||
<summary>原始记录</summary>
|
||||
<pre>{{ runtimeDetail.raw }}</pre>
|
||||
</details>
|
||||
</div>
|
||||
<dl class="log-detail-grid runtime-detail-meta">
|
||||
<dt>触发操作</dt><dd>{{ runtimeDetail.action }}</dd>
|
||||
<dt>操作对象</dt><dd>{{ runtimeDetail.object || '未记录' }}</dd>
|
||||
<dt>操作结果</dt><dd>{{ resultLabel(runtimeDetail.result) }}</dd>
|
||||
<dt>结果说明</dt><dd>{{ runtimeDetail.outcome }}</dd>
|
||||
<dt>代码来源</dt><dd>{{ runtimeDetail.source || '未记录' }}</dd>
|
||||
<template v-if="runtimeDetail.requestId">
|
||||
<dt>请求标识</dt><dd>{{ runtimeDetail.requestId }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
<h3>完整内容</h3>
|
||||
<pre class="runtime-detail-message">{{ runtimeDetail.message }}</pre>
|
||||
<details class="runtime-raw">
|
||||
<summary>原始记录</summary>
|
||||
<pre>{{ runtimeDetail.raw }}</pre>
|
||||
</details>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog :model-value="Boolean(detail)" title="日志详情" width="min(760px, 92vw)" append-to-body @close="detail = null">
|
||||
<template v-if="detail">
|
||||
<p>{{ formatTime(detail.createdAt) }} · {{ displayActor(detail) }} · {{ actionLabel(detail.action) }}</p>
|
||||
<dl class="log-detail-grid">
|
||||
<dt>触发操作</dt><dd>{{ actionLabel(detail.action) }}</dd>
|
||||
<dt>触发来源</dt><dd>{{ details.trigger || '未记录' }}</dd>
|
||||
<dt>{{ category === 'mail' ? '处理阶段' : '操作对象' }}</dt><dd>{{ displayObject(detail) }}</dd>
|
||||
<dt>操作结果</dt><dd>{{ resultLabel(detail.result) }}</dd>
|
||||
<dt>结果说明</dt><dd>{{ displayOutcome(detail) }}</dd>
|
||||
</dl>
|
||||
<dl v-if="category === 'mail'" class="log-detail-grid">
|
||||
<dt>发件人</dt><dd>{{ details.sender || '—' }}</dd>
|
||||
<dt>收件人数</dt><dd>{{ details.recipientCount }}</dd>
|
||||
<dt>邮件大小</dt><dd>{{ details.bytes }} 字节</dd>
|
||||
<dt>对端接收人数</dt><dd>{{ details.acceptedCount ?? '未记录' }}</dd>
|
||||
</dl>
|
||||
<dl v-else class="log-detail-grid">
|
||||
<dt>权限角色</dt><dd>{{ details.actorRole || '—' }}</dd>
|
||||
<dt>结果代码</dt><dd>{{ details.errorCode ?? '—' }}</dd>
|
||||
<dt>HTTP 状态</dt><dd>{{ details.httpStatus || '未记录' }}</dd>
|
||||
</dl>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</el-dialog>
|
||||
<el-dialog :model-value="Boolean(detail)" title="日志详情" width="min(760px, 92vw)" append-to-body @close="detail = null">
|
||||
<template v-if="detail">
|
||||
<p>{{ formatTime(detail.createdAt) }} · {{ displayActor(detail) }} · {{ actionLabel(detail.action) }}</p>
|
||||
<dl class="log-detail-grid">
|
||||
<dt>触发操作</dt><dd>{{ actionLabel(detail.action) }}</dd>
|
||||
<dt>触发来源</dt><dd>{{ details.trigger || '未记录' }}</dd>
|
||||
<dt>{{ category === 'mail' ? '处理阶段' : '操作对象' }}</dt><dd>{{ displayObject(detail) }}</dd>
|
||||
<dt>操作结果</dt><dd>{{ resultLabel(detail.result) }}</dd>
|
||||
<dt>结果说明</dt><dd>{{ displayOutcome(detail) }}</dd>
|
||||
</dl>
|
||||
<dl v-if="category === 'mail'" class="log-detail-grid">
|
||||
<dt>发件人</dt><dd>{{ details.sender || '—' }}</dd>
|
||||
<dt>收件人数</dt><dd>{{ details.recipientCount }}</dd>
|
||||
<dt>邮件大小</dt><dd>{{ details.bytes }} 字节</dd>
|
||||
<dt>对端接收人数</dt><dd>{{ details.acceptedCount ?? '未记录' }}</dd>
|
||||
</dl>
|
||||
<dl v-else class="log-detail-grid">
|
||||
<dt>权限角色</dt><dd>{{ details.actorRole || '—' }}</dd>
|
||||
<dt>结果代码</dt><dd>{{ details.errorCode ?? '—' }}</dd>
|
||||
<dt>HTTP 状态</dt><dd>{{ details.httpStatus || '未记录' }}</dd>
|
||||
</dl>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useLogs } from './LogSettings.js'
|
||||
import SenderFilterManagement from '../SenderFilterManagement/SenderFilterManagement.vue'
|
||||
|
||||
const {
|
||||
category, categories, loading, error, items, offset,
|
||||
|
||||
@ -0,0 +1,102 @@
|
||||
.sender-filter-management {
|
||||
background: var(--el-bg-color-overlay, #ffffff);
|
||||
border: 1px solid var(--el-border-color-light, #e4e7ed);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.sf-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.sf-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sf-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary, #303133);
|
||||
}
|
||||
|
||||
.sf-badges {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sf-badge {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sf-quick-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sf-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sf-tabs {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sf-search-input {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.sf-table-wrapper {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sf-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sf-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sf-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sf-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.sf-quick-add {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.sf-quick-add .el-input {
|
||||
width: 100% !important;
|
||||
}
|
||||
.sf-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.sf-search-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,193 @@
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { http } from '@/utils/axios'
|
||||
|
||||
export function useSenderFilters() {
|
||||
const loading = ref(false)
|
||||
const filterList = ref([])
|
||||
const total = ref(0)
|
||||
const whitelistCount = ref(0)
|
||||
const blacklistCount = ref(0)
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const searchKeyword = ref('')
|
||||
const currentTab = ref('all') // 'all' | 'whitelist' | 'blacklist'
|
||||
|
||||
const addForm = reactive({
|
||||
email: '',
|
||||
filter_type: 'whitelist',
|
||||
})
|
||||
const addLoading = ref(false)
|
||||
|
||||
const loadFilters = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
page_size: pageSize.value,
|
||||
keyword: searchKeyword.value.trim(),
|
||||
}
|
||||
if (currentTab.value === 'whitelist' || currentTab.value === 'blacklist') {
|
||||
params.filter_type = currentTab.value
|
||||
}
|
||||
const res = await http.get('/api/v1/email/sender-filters', { params })
|
||||
if (res.errorNo === 0 && res.data) {
|
||||
filterList.value = Array.isArray(res.data.list) ? res.data.list : []
|
||||
total.value = res.data.total || 0
|
||||
whitelistCount.value = res.data.whitelist_count || 0
|
||||
blacklistCount.value = res.data.blacklist_count || 0
|
||||
} else {
|
||||
filterList.value = []
|
||||
total.value = 0
|
||||
if (res.errorNo !== 0) {
|
||||
ElMessage.error(res.errorMsg || '加载名单失败')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
filterList.value = []
|
||||
total.value = 0
|
||||
ElMessage.error(error?.message || '加载名单失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1
|
||||
loadFilters()
|
||||
}
|
||||
|
||||
const handleTabChange = () => {
|
||||
currentPage.value = 1
|
||||
loadFilters()
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page
|
||||
loadFilters()
|
||||
}
|
||||
|
||||
const handleAdd = async () => {
|
||||
const email = addForm.email.trim()
|
||||
if (!email) {
|
||||
ElMessage.warning('请输入发件人邮件地址')
|
||||
return
|
||||
}
|
||||
addLoading.value = true
|
||||
try {
|
||||
const res = await http.post('/api/v1/email/sender-filters', {
|
||||
email,
|
||||
filter_type: addForm.filter_type,
|
||||
})
|
||||
if (res.errorNo === 0) {
|
||||
ElMessage.success(addForm.filter_type === 'whitelist' ? '已成功添加至信任白名单' : '已成功添加至拦截黑名单')
|
||||
addForm.email = ''
|
||||
loadFilters()
|
||||
} else {
|
||||
ElMessage.error(res.errorMsg || '添加失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || '添加失败')
|
||||
} finally {
|
||||
addLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggle = async (row) => {
|
||||
const targetType = row.filter_type === 'whitelist' ? 'blacklist' : 'whitelist'
|
||||
const actionText = targetType === 'blacklist' ? '拉入黑名单' : '转为白名单'
|
||||
const tip = targetType === 'blacklist'
|
||||
? `确定将发件人 "${row.email}" 拉入黑名单吗?后续来自该地址的邮件将受到强拦截与风险警示提示。`
|
||||
: `确定将发件人 "${row.email}" 转为信任白名单吗?`
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(tip, `确认${actionText}`, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: targetType === 'blacklist' ? 'warning' : 'info',
|
||||
})
|
||||
const res = await http.post('/api/v1/email/sender-filters/toggle', {
|
||||
email: row.email,
|
||||
target_type: targetType,
|
||||
})
|
||||
if (res.errorNo === 0) {
|
||||
ElMessage.success(`已将 ${row.email} ${actionText}`)
|
||||
loadFilters()
|
||||
} else {
|
||||
ElMessage.error(res.errorMsg || `${actionText}失败`)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') {
|
||||
ElMessage.error(e?.message || `${actionText}失败`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定从名单中移除发件人 "${row.email}" 吗?`,
|
||||
'确认移除',
|
||||
{
|
||||
confirmButtonText: '确定移除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
const res = await http.post('/api/v1/email/sender-filters/delete', {
|
||||
email: row.email,
|
||||
id: row.id,
|
||||
})
|
||||
if (res.errorNo === 0) {
|
||||
ElMessage.success('已移除发件人规则')
|
||||
loadFilters()
|
||||
} else {
|
||||
ElMessage.error(res.errorMsg || '移除失败')
|
||||
}
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') {
|
||||
ElMessage.error(e?.message || '移除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const d = new Date(dateStr)
|
||||
if (isNaN(d.getTime())) return dateStr
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
const year = d.getFullYear()
|
||||
const month = pad(d.getMonth() + 1)
|
||||
const day = pad(d.getDate())
|
||||
const hours = pad(d.getHours())
|
||||
const minutes = pad(d.getMinutes())
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadFilters()
|
||||
})
|
||||
|
||||
return {
|
||||
loading,
|
||||
filterList,
|
||||
total,
|
||||
whitelistCount,
|
||||
blacklistCount,
|
||||
currentPage,
|
||||
pageSize,
|
||||
searchKeyword,
|
||||
currentTab,
|
||||
addForm,
|
||||
addLoading,
|
||||
loadFilters,
|
||||
handleSearch,
|
||||
handleTabChange,
|
||||
handlePageChange,
|
||||
handleAdd,
|
||||
handleToggle,
|
||||
handleDelete,
|
||||
formatDate,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<div class="sender-filter-management" v-loading="loading">
|
||||
<!-- 面板标题与快捷操作栏 -->
|
||||
<div class="sf-header">
|
||||
<div class="sf-title-group">
|
||||
<h3 class="sf-title">发件人黑白名单管理</h3>
|
||||
<div class="sf-badges">
|
||||
<el-tag type="success" effect="plain" class="sf-badge">
|
||||
信任白名单: {{ whitelistCount }}
|
||||
</el-tag>
|
||||
<el-tag type="danger" effect="plain" class="sf-badge">
|
||||
拦截黑名单: {{ blacklistCount }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sf-quick-add">
|
||||
<el-input
|
||||
v-model="addForm.email"
|
||||
placeholder="输入发件人邮箱 (如 friend@example.com)"
|
||||
clearable
|
||||
size="default"
|
||||
style="width: 280px"
|
||||
@keyup.enter="handleAdd"
|
||||
/>
|
||||
<el-select
|
||||
v-model="addForm.filter_type"
|
||||
size="default"
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option label="信任白名单" value="whitelist" />
|
||||
<el-option label="拦截黑名单" value="blacklist" />
|
||||
</el-select>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="default"
|
||||
:loading="addLoading"
|
||||
@click="handleAdd"
|
||||
>
|
||||
添加名单
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选与搜索工具条 -->
|
||||
<div class="sf-toolbar">
|
||||
<el-radio-group
|
||||
v-model="currentTab"
|
||||
size="small"
|
||||
class="sf-tabs"
|
||||
@change="handleTabChange"
|
||||
>
|
||||
<el-radio-button value="all">全部 ({{ total }})</el-radio-button>
|
||||
<el-radio-button value="whitelist">仅白名单 ({{ whitelistCount }})</el-radio-button>
|
||||
<el-radio-button value="blacklist">仅黑名单 ({{ blacklistCount }})</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div class="sf-search">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索邮箱地址..."
|
||||
clearable
|
||||
size="small"
|
||||
class="sf-search-input"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #append>
|
||||
<el-button size="small" @click="handleSearch">搜索</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 规则表格 -->
|
||||
<div class="sf-table-wrapper">
|
||||
<el-table
|
||||
:data="filterList"
|
||||
stripe
|
||||
size="small"
|
||||
class="sf-table"
|
||||
empty-text="暂无匹配的发件人规则"
|
||||
>
|
||||
<el-table-column prop="email" label="发件人邮箱" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="名单类型" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.filter_type === 'whitelist'"
|
||||
type="success"
|
||||
size="small"
|
||||
effect="light"
|
||||
>
|
||||
信任白名单
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-else
|
||||
type="danger"
|
||||
size="small"
|
||||
effect="light"
|
||||
>
|
||||
拦截黑名单
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更新/添加时间" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ formatDate(row.updated_at || row.created_at) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="管理操作" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="sf-actions">
|
||||
<el-button
|
||||
v-if="row.filter_type === 'whitelist'"
|
||||
type="warning"
|
||||
link
|
||||
size="small"
|
||||
@click="handleToggle(row)"
|
||||
>
|
||||
拉入黑名单
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="success"
|
||||
link
|
||||
size="small"
|
||||
@click="handleToggle(row)"
|
||||
>
|
||||
转为白名单
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
移除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="total > pageSize" class="sf-pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
size="small"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSenderFilters } from './SenderFilterManagement.js'
|
||||
import './SenderFilterManagement.css'
|
||||
|
||||
const {
|
||||
loading,
|
||||
filterList,
|
||||
total,
|
||||
whitelistCount,
|
||||
blacklistCount,
|
||||
currentPage,
|
||||
pageSize,
|
||||
searchKeyword,
|
||||
currentTab,
|
||||
addForm,
|
||||
addLoading,
|
||||
loadFilters,
|
||||
handleSearch,
|
||||
handleTabChange,
|
||||
handlePageChange,
|
||||
handleAdd,
|
||||
handleToggle,
|
||||
handleDelete,
|
||||
formatDate,
|
||||
} = useSenderFilters()
|
||||
</script>
|
||||
Loading…
Reference in New Issue
Block a user