wip: baseline changes before channel-conversation-task boundary implementation
This commit is contained in:
@@ -1,93 +1,439 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="目标类型">
|
||||
<el-select v-model="form.targetType">
|
||||
<el-option label="会话" value="conversation" />
|
||||
<el-option label="人员" value="person" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标" v-if="form.targetType === 'conversation'">
|
||||
<el-select v-model="selectedConversationId" placeholder="选择会话">
|
||||
<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="内容类型"><el-input v-model="form.contentType" disabled /></el-form-item>
|
||||
<el-form-item label="内容">
|
||||
<el-input v-model="textContent" type="textarea" :rows="3" placeholder="输入文本消息" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleTestSend">测试发送</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 统计卡片 -->
|
||||
<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-divider />
|
||||
<!-- 测试发送 -->
|
||||
<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-table :data="records" border>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="taskId" label="任务ID" />
|
||||
<el-table-column prop="targetType" label="目标类型" />
|
||||
<el-table-column prop="channelType" label="渠道" />
|
||||
<el-table-column prop="contentType" label="内容类型" />
|
||||
<el-table-column prop="sendStatus" label="状态">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.sendStatus === 2" type="success">成功</el-tag>
|
||||
<el-tag v-else-if="scope.row.sendStatus === 3" type="danger">失败</el-tag>
|
||||
<el-tag v-else type="info">待发送</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" />
|
||||
</el-table>
|
||||
<!-- 筛选栏 -->
|
||||
<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, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { testSendMessage, listSendRecords } from '@/api/send'
|
||||
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'
|
||||
|
||||
const form = reactive({ targetType: 'conversation', contentType: 'text', channelStrategy: { channel_type: 'wecom', strategy: 'primary' } })
|
||||
const selectedConversationId = ref<number | null>(null)
|
||||
const textContent = ref('')
|
||||
const records = ref<any[]>([])
|
||||
const conversations = ref<any[]>([])
|
||||
|
||||
const targetValue = computed(() => {
|
||||
if (form.targetType === 'conversation' && selectedConversationId.value) {
|
||||
return { conversation_id: selectedConversationId.value }
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
async function loadRecords() {
|
||||
const res: any = await listSendRecords()
|
||||
records.value = res.data || []
|
||||
interface RecordItem {
|
||||
id: number
|
||||
taskId: string
|
||||
targetType: string
|
||||
targetValue?: string
|
||||
channelType: string
|
||||
sendStatus: number
|
||||
sendTime?: string
|
||||
finishTime?: string
|
||||
contentBody?: string
|
||||
channelResult?: string
|
||||
callbackUrl?: string
|
||||
}
|
||||
|
||||
async function loadConversations() {
|
||||
const res: any = await listConversations()
|
||||
conversations.value = res.data || []
|
||||
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 testSendMessage({
|
||||
targetType: form.targetType,
|
||||
targetValue: targetValue.value,
|
||||
await testSend({
|
||||
targetType: testForm.targetType,
|
||||
targetValue,
|
||||
contentType: 'text',
|
||||
content: { text: textContent.value },
|
||||
channelStrategy: form.channelStrategy,
|
||||
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(() => {
|
||||
loadRecords()
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user