Files
sino-mci/admin-web/src/views/SendRecord.vue

440 lines
13 KiB
Vue

<template>
<div>
<!-- 统计卡片 -->
<el-row :gutter="16">
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="今日发送总数" :value="statistics.total" />
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="成功" :value="statistics.successCount">
<template #suffix>
<el-tag type="success" size="small">成功</el-tag>
</template>
</el-statistic>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="失败" :value="statistics.failedCount">
<template #suffix>
<el-tag type="danger" size="small">失败</el-tag>
</template>
</el-statistic>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<el-statistic title="待发送" :value="statistics.pendingCount">
<template #suffix>
<el-tag type="info" size="small">待发送</el-tag>
</template>
</el-statistic>
</el-card>
</el-col>
</el-row>
<!-- 测试发送 -->
<el-card class="mt-4" shadow="never">
<template #header>
<span>测试发送</span>
</template>
<el-form :model="testForm" label-width="80px" inline>
<el-form-item label="目标类型">
<el-select v-model="testForm.targetType" style="width: 140px">
<el-option label="会话" value="conversation" />
<el-option label="人员" value="person" />
</el-select>
</el-form-item>
<el-form-item label="目标" v-if="testForm.targetType === 'conversation'" style="width: 280px">
<el-select v-model="selectedConversationId" placeholder="选择会话" clearable>
<el-option
v-for="c in conversations"
:key="c.id"
:label="c.conversationName"
:value="c.id"
/>
</el-select>
</el-form-item>
<el-form-item label="目标" v-if="testForm.targetType === 'person'" style="width: 280px">
<el-input v-model="personIdInput" placeholder="输入 person_id" />
</el-form-item>
<el-form-item label="渠道">
<el-select v-model="testForm.channelStrategy.channel_type" style="width: 140px">
<el-option label="企微" value="wecom" />
<el-option label="微信个人号" value="wechat_personal" />
</el-select>
</el-form-item>
<el-form-item label="内容">
<el-input v-model="textContent" placeholder="输入文本消息" style="width: 260px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleTestSend">测试发送</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 筛选栏 -->
<el-card class="mt-4" shadow="never">
<el-form :model="query" inline>
<el-form-item label="任务 ID">
<el-input v-model="query.taskId" placeholder="SND..." clearable style="width: 220px" />
</el-form-item>
<el-form-item label="发送时间">
<el-date-picker
v-model="dateRange"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD[T]HH:mm:ss"
style="width: 360px"
/>
</el-form-item>
<el-form-item label="渠道">
<el-select v-model="query.channelType" placeholder="全部" clearable style="width: 140px">
<el-option label="企微" value="wecom" />
<el-option label="微信个人号" value="wechat_personal" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="query.status" placeholder="全部" clearable style="width: 120px">
<el-option label="待发送" :value="0" />
<el-option label="发送中" :value="1" />
<el-option label="成功" :value="2" />
<el-option label="失败" :value="3" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 记录列表 -->
<el-card class="mt-4" shadow="never">
<el-table :data="records" border v-loading="loading">
<el-table-column prop="taskId" label="任务 ID" min-width="180" show-overflow-tooltip />
<el-table-column prop="targetType" label="目标类型" width="110" />
<el-table-column prop="channelType" label="渠道" width="120" />
<el-table-column prop="sendStatus" label="状态" width="100">
<template #default="{ row }">
<el-tag v-if="row.sendStatus === 0" type="info">待发送</el-tag>
<el-tag v-else-if="row.sendStatus === 1" type="warning">发送中</el-tag>
<el-tag v-else-if="row.sendStatus === 2" type="success">成功</el-tag>
<el-tag v-else-if="row.sendStatus === 3" type="danger">失败</el-tag>
<el-tag v-else type="info">未知</el-tag>
</template>
</el-table-column>
<el-table-column prop="sendTime" label="发送时间" min-width="160" />
<el-table-column prop="finishTime" label="完成时间" min-width="160" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
<el-button
v-if="row.sendStatus === 0"
link
type="danger"
@click="handleCancel(row.taskId)"
>
取消
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-bar">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.size"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="loadRecords"
/>
</div>
</el-card>
<!-- 详情抽屉 -->
<el-drawer v-model="detailVisible" title="发送详情" size="520px">
<div v-if="currentRecord" class="detail-content">
<div class="detail-item">
<span class="detail-label">任务 ID</span>
<span class="detail-value">{{ currentRecord.taskId }}</span>
</div>
<div class="detail-item">
<span class="detail-label">目标类型</span>
<span class="detail-value">{{ currentRecord.targetType }}</span>
</div>
<div class="detail-item">
<span class="detail-label">目标值</span>
<pre>{{ formatJson(currentRecord.targetValue) }}</pre>
</div>
<div class="detail-item">
<span class="detail-label">渠道</span>
<span class="detail-value">{{ currentRecord.channelType }}</span>
</div>
<div class="detail-item">
<span class="detail-label">内容</span>
<pre>{{ formatJson(currentRecord.contentBody) }}</pre>
</div>
<div class="detail-item">
<span class="detail-label">渠道结果</span>
<pre>{{ formatJson(currentRecord.channelResult) }}</pre>
</div>
<div class="detail-item">
<span class="detail-label">回调地址</span>
<span class="detail-value">{{ currentRecord.callbackUrl || '-' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">发送时间</span>
<span class="detail-value">{{ currentRecord.sendTime || '-' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">完成时间</span>
<span class="detail-value">{{ currentRecord.finishTime || '-' }}</span>
</div>
</div>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
queryRecords,
getStatistics,
cancelTask,
testSend,
type QueryRecordsParams,
} from '@/api/sendRecord'
import { listConversations } from '@/api/channel'
interface RecordItem {
id: number
taskId: string
targetType: string
targetValue?: string
channelType: string
sendStatus: number
sendTime?: string
finishTime?: string
contentBody?: string
channelResult?: string
callbackUrl?: string
}
const statistics = reactive({
total: 0,
pendingCount: 0,
sendingCount: 0,
successCount: 0,
failedCount: 0,
})
const query = reactive<QueryRecordsParams>({
taskId: '',
channelType: '',
status: '',
})
const dateRange = ref<string[]>([])
const pagination = reactive({
page: 1,
size: 20,
total: 0,
})
const records = ref<RecordItem[]>([])
const loading = ref(false)
const detailVisible = ref(false)
const currentRecord = ref<RecordItem | null>(null)
const testForm = reactive({
targetType: 'conversation',
contentType: 'text',
channelStrategy: { channel_type: 'wecom', strategy: 'primary' },
})
const selectedConversationId = ref<number | null>(null)
const personIdInput = ref('')
const textContent = ref('')
const conversations = ref<any[]>([])
function buildTargetValue() {
if (testForm.targetType === 'conversation' && selectedConversationId.value) {
return { conversation_id: selectedConversationId.value }
}
if (testForm.targetType === 'person' && personIdInput.value) {
return { person_id: Number(personIdInput.value) }
}
return {}
}
async function loadRecords() {
loading.value = true
try {
const params: QueryRecordsParams = {
page: pagination.page,
size: pagination.size,
taskId: query.taskId || undefined,
channelType: query.channelType || undefined,
status: query.status === '' ? undefined : query.status,
}
if (dateRange.value && dateRange.value.length === 2) {
params.start = dateRange.value[0]
params.end = dateRange.value[1]
}
const res: any = await queryRecords(params)
const data = res.data || {}
records.value = data.records || data || []
pagination.total = data.total || records.value.length
} catch (e: any) {
ElMessage.error(e.message)
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadRecords()
}
function handleReset() {
query.taskId = ''
query.channelType = ''
query.status = ''
dateRange.value = []
pagination.page = 1
loadRecords()
}
function handleSizeChange() {
pagination.page = 1
loadRecords()
}
async function loadStatistics() {
const now = new Date()
const start = formatDateTime(new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0))
const end = formatDateTime(now)
try {
const res: any = await getStatistics(start, end)
Object.assign(statistics, res.data || {})
} catch (e: any) {
ElMessage.error(e.message)
}
}
async function handleTestSend() {
const targetValue = buildTargetValue()
if (Object.keys(targetValue).length === 0) {
ElMessage.warning('请选择或填写目标')
return
}
try {
await testSend({
targetType: testForm.targetType,
targetValue,
contentType: 'text',
content: { text: textContent.value },
channelStrategy: testForm.channelStrategy,
})
ElMessage.success('测试发送已提交')
textContent.value = ''
await loadRecords()
await loadStatistics()
} catch (e: any) {
ElMessage.error(e.message)
}
}
async function handleCancel(taskId: string) {
try {
await ElMessageBox.confirm('确定取消该任务的待发送记录?', '提示', { type: 'warning' })
await cancelTask(taskId)
ElMessage.success('取消成功')
await loadRecords()
await loadStatistics()
} catch (e: any) {
if (e !== 'cancel') {
ElMessage.error(e.message || '取消失败')
}
}
}
function openDetail(row: RecordItem) {
currentRecord.value = row
detailVisible.value = true
}
function formatJson(value?: string) {
if (!value) return '-'
try {
return JSON.stringify(JSON.parse(value), null, 2)
} catch {
return value
}
}
function formatDateTime(date: Date): string {
const pad = (n: number) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
async function loadConversations() {
try {
const res: any = await listConversations()
conversations.value = res.data || []
} catch (e: any) {
ElMessage.error(e.message)
}
}
onMounted(() => {
loadConversations()
loadRecords()
loadStatistics()
})
</script>
<style scoped>
.mt-4 {
margin-top: 16px;
}
.pagination-bar {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.detail-content {
font-size: 14px;
}
.detail-item {
margin-bottom: 16px;
}
.detail-label {
display: block;
color: #606266;
margin-bottom: 6px;
}
.detail-value {
color: #303133;
word-break: break-all;
}
pre {
margin: 0;
padding: 12px;
background: #f5f7fa;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
</style>