Files
xk-admin/apps/web-antd/src/views/system/oa-robot/components/test-result-modal.vue

310 lines
8.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import dayjs from 'dayjs';
import {
Alert,
Descriptions,
DescriptionsItem as DescItem,
Tag,
Timeline,
TimelineItem,
TypographyParagraph,
TypographyText,
TypographyTitle,
} from 'ant-design-vue';
/**
* OA 机器人「测试发送结果」独立弹窗
*
* 设计要点:
* - 顶部 Descriptions全局上下文机器人、平台、时间、测试内容、@
* - 底部 Timeline每种消息类型一个节点展示成功/失败 + 真实请求体 + 响应体
* - 请求体来自后端 results[].request渠道 buildPayload 的真实 HTTP body已脱敏
* - 暗色模式全部走 antd 原生组件 + CSS 变量
*
* 数据来源modalApi.getData(),由 test-send-modal.vue 的 onFinished 注入
*/
interface TestResultItem {
message_type: string;
name: string;
success: boolean;
message: string;
/** 真实 HTTP 请求体(后端脱敏后) */
request?: any;
response: any;
cost_ms: number;
}
/** 测试结果列表(由外部传入) */
const testResults = ref<TestResultItem[]>([]);
/** 测试汇总提示(由外部传入) */
const testSummary = ref('');
/** 机器人名称(上下文展示) */
const robotName = ref('');
/** 机器人 ID上下文展示 */
const robotId = ref<number>();
/** 平台编码(上下文展示) */
const platformCode = ref('');
/** 测试内容(上下文展示) */
const testContent = ref('');
/** 是否 @ 全员(上下文展示) */
const testAtAll = ref(false);
/** @ 手机号列表(上下文展示) */
const testMobiles = ref<string[]>([]);
/** 发送完成时间戳(秒,上下文展示) */
const sentAt = ref<number>();
/**
* @ 配置摘要Descriptions 一行展示用)
*/
const atSummary = computed(() => {
if (testAtAll.value) return '@ 全员';
if (testMobiles.value.length === 0) return '无';
return testMobiles.value.join('、');
});
/**
* 格式化时间戳为可读字符串(秒级 → dayjs
*/
function formatTime(ts?: number): string {
return ts ? dayjs(ts * 1000).format('YYYY-MM-DD HH:mm:ss') : '-';
}
/**
* 把对象格式化为可读 JSON 字符串(空对象显示「无」)
* 用于请求体 / 响应体的 <pre> 展示
*/
function formatJson(data: any): string {
if (data === null || data === undefined) return '无';
if (typeof data === 'string') {
// 后端偶发已是 JSON 字符串,尝试美化
try {
return JSON.stringify(JSON.parse(data), null, 2);
} catch {
return data || '无';
}
}
if (typeof data === 'object' && Object.keys(data).length === 0) return '无';
try {
return JSON.stringify(data, null, 2);
} catch {
return String(data);
}
}
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData<any>();
testResults.value = data?.results ?? [];
testSummary.value = data?.summary ?? '';
robotName.value = data?.robotName ?? '';
robotId.value = data?.robotId;
platformCode.value = data?.platformCode ?? '';
testContent.value = data?.testContent ?? '';
testAtAll.value = !!data?.testAtAll;
testMobiles.value = data?.testMobiles ?? [];
sentAt.value = data?.sentAt;
const suffix = robotName.value ? ` - ${robotName.value}` : '';
modalApi.setState({
title: `测试发送结果${suffix}`,
confirmText: '知道了',
});
}
},
});
</script>
<template>
<Modal title="测试发送结果" class="w-[56%]">
<!-- 1. 汇总提示 -->
<Alert
v-if="testSummary"
:type="
testResults.length === 0
? 'info'
: testResults.every((r) => r.success)
? 'success'
: testResults.some((r) => r.success)
? 'warning'
: 'error'
"
:message="testSummary"
show-icon
style="margin-bottom: 16px"
/>
<!-- 2. 发送详情全局上下文 -->
<Descriptions
title="发送详情"
:column="1"
size="small"
bordered
style="margin-bottom: 16px"
>
<DescItem label="机器人">
{{ robotName || '-' }}
<span v-if="robotId" style="color: var(--ant-color-text-tertiary)">
(id: {{ robotId }})
</span>
</DescItem>
<DescItem label="平台">
<Tag v-if="platformCode" color="processing">{{ platformCode }}</Tag>
<span v-else>-</span>
</DescItem>
<DescItem label="发送时间">{{ formatTime(sentAt) }}</DescItem>
<DescItem label="测试内容">
<TypographyParagraph
style="margin: 0; white-space: pre-wrap; word-break: break-all"
>
{{ testContent || '-' }}
</TypographyParagraph>
</DescItem>
<DescItem label="@ 配置">{{ atSummary }}</DescItem>
</Descriptions>
<!-- 3. 各类型发送结果含真实请求体 / 响应体 -->
<TypographyTitle :level="5" style="margin-bottom: 12px">
消息类型发送结果
</TypographyTitle>
<Timeline v-if="testResults.length > 0">
<TimelineItem
v-for="(item, idx) in testResults"
:key="`${item.message_type}-${idx}`"
:color="item.success ? 'green' : 'red'"
>
<!-- 标题行名称 + 类型 Tag + 成功/失败 Tag + 耗时 -->
<div class="timeline-row">
<span class="timeline-name">{{ item.name }}</span>
<Tag class="timeline-type">{{ item.message_type }}</Tag>
<Tag :color="item.success ? 'success' : 'error'">
{{ item.success ? '成功' : '失败' }}
</Tag>
<span v-if="item.cost_ms" class="timeline-cost">
{{ item.cost_ms }}ms
</span>
</div>
<!-- 渠道返回文案成功 errmsg / 失败原因 -->
<div v-if="item.message" class="timeline-msg" :class="{ error: !item.success }">
{{ item.message }}
</div>
<!-- 真实请求体渠道 buildPayload 结果已脱敏 -->
<div class="payload-block">
<TypographyText type="secondary" class="payload-label">
请求体
</TypographyText>
<pre class="payload-pre">{{ formatJson(item.request) }}</pre>
</div>
<!-- 第三方响应体 -->
<div class="payload-block">
<TypographyText type="secondary" class="payload-label">
响应体
</TypographyText>
<pre class="payload-pre">{{ formatJson(item.response) }}</pre>
</div>
</TimelineItem>
</Timeline>
<TypographyText v-else type="secondary">
无发送结果
</TypographyText>
</Modal>
</template>
<style scoped lang="scss">
/*
* 暗色适配:颜色一律用 antd CSS 变量
* pre 限高 + overflow避免超长 JSON尤其 image base64 截断前)撑爆弹窗
*/
.timeline-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.timeline-name {
font-weight: 500;
color: var(--ant-color-text, #1d2129);
}
.timeline-type {
margin: 0;
}
.timeline-cost {
color: var(--ant-color-text-tertiary, #86909c);
font-size: 12px;
}
.timeline-msg {
margin-top: 4px;
font-size: 12px;
line-height: 1.5;
word-break: break-all;
color: var(--ant-color-text-secondary, #4e5969);
&.error {
color: var(--ant-color-error, #f53f3f);
}
}
.payload-block {
margin-top: 8px;
}
.payload-label {
display: block;
font-size: 12px;
margin-bottom: 4px;
}
.payload-pre {
margin: 0;
padding: 8px 10px;
max-height: 180px;
overflow: auto;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
border-radius: 4px;
border: 1px solid var(--ant-color-border, #d9d9d9);
background: var(--ant-color-fill-quaternary, rgba(0, 0, 0, 0.02));
color: var(--ant-color-text, #1d2129);
}
/* 暗色:强制浅色字,避免 CSS 变量未注入时落入 #1d2129 黑底黑字 */
html.dark .payload-pre,
.dark .payload-pre {
color: rgba(255, 255, 255, 0.88);
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.16);
}
html.dark .timeline-name,
.dark .timeline-name {
color: rgba(255, 255, 255, 0.88);
}
html.dark .timeline-msg:not(.error),
.dark .timeline-msg:not(.error) {
color: rgba(255, 255, 255, 0.65);
}
</style>