This commit is contained in:
2025-11-07 14:40:47 +08:00
parent a411e42094
commit 9672e191e1
4 changed files with 1199 additions and 169 deletions

View File

@@ -0,0 +1,678 @@
<template>
<el-dialog
title="腾讯文档推送监控"
:visible.sync="visible"
width="1200px"
:close-on-click-modal="false"
@close="handleClose"
top="5vh"
>
<div class="push-monitor">
<!-- 倒计时和状态卡片 -->
<el-card class="countdown-card" shadow="hover">
<div class="countdown-header">
<div class="header-left">
<i class="el-icon-timer"></i>
<span class="title">自动推送倒计时</span>
</div>
<div class="header-right">
<el-tag v-if="pushStatus.isScheduled" type="warning" size="medium">
<i class="el-icon-loading"></i> 等待推送中
</el-tag>
<el-tag v-else type="info" size="medium">
<i class="el-icon-circle-check"></i> 无待推送任务
</el-tag>
</div>
</div>
<div class="countdown-content">
<div class="countdown-display" :class="{active: pushStatus.isScheduled}">
<div class="time-box">
<span class="time-value">{{ countdownDisplay.minutes }}</span>
<span class="time-label"></span>
</div>
<span class="time-separator">:</span>
<div class="time-box">
<span class="time-value">{{ countdownDisplay.seconds }}</span>
<span class="time-label"></span>
</div>
</div>
<div class="countdown-info">
<div v-if="pushStatus.scheduledTime" class="info-item">
<i class="el-icon-time"></i>
<span>预计推送时间{{ formatDateTime(pushStatus.scheduledTime) }}</span>
</div>
<div v-if="pushStatus.lastSuccessRecord" class="info-item">
<i class="el-icon-success"></i>
<span>上次推送{{ formatDateTime(pushStatus.lastSuccessRecord.endTime) }}</span>
<el-tag size="mini" type="success" style="margin-left: 10px;">
成功 {{ pushStatus.lastSuccessRecord.successCount }}
</el-tag>
</div>
</div>
<div class="countdown-actions">
<el-button
type="primary"
icon="el-icon-upload2"
:loading="pushing"
@click="handleTriggerPushNow"
>
立即推送
</el-button>
<el-button
type="warning"
icon="el-icon-close"
:disabled="!pushStatus.isScheduled"
@click="handleCancelPush"
>
取消推送
</el-button>
<el-button
icon="el-icon-refresh"
@click="loadPushStatus"
>
刷新状态
</el-button>
</div>
</div>
</el-card>
<!-- 推送记录列表 -->
<el-card class="records-card" shadow="hover">
<div slot="header" class="records-header">
<div class="header-left">
<i class="el-icon-document-copy"></i>
<span class="title">推送记录</span>
<el-tag size="mini" type="info" style="margin-left: 10px;">
{{ batchRecords.length }}
</el-tag>
</div>
<div class="header-right">
<el-button type="text" icon="el-icon-refresh" @click="loadBatchRecords">刷新</el-button>
</div>
</div>
<el-timeline v-if="batchRecords.length > 0">
<el-timeline-item
v-for="record in batchRecords"
:key="record.batchId"
:timestamp="formatDateTime(record.createTime)"
placement="top"
:type="getRecordType(record.status)"
:icon="getRecordIcon(record.status)"
>
<el-card class="record-item" shadow="hover">
<div class="record-summary" @click="toggleRecordDetail(record.batchId)">
<div class="summary-left">
<el-tag :type="getStatusTagType(record.status)" size="small">
{{ getStatusText(record.status) }}
</el-tag>
<span class="trigger-source">
{{ getTriggerSourceText(record.triggerSource) }}
</span>
<span class="record-stats">
<i class="el-icon-check" style="color: #67c23a;"></i> {{ record.successCount }}
<i class="el-icon-remove-outline" style="color: #e6a23c; margin-left: 10px;"></i> {{ record.skipCount }}
<i class="el-icon-close" style="color: #f56c6c; margin-left: 10px;"></i> {{ record.errorCount }}
</span>
</div>
<div class="summary-right">
<span class="record-range"> {{ record.startRow }} - {{ record.endRow }}</span>
<span v-if="record.durationMs" class="record-duration">
耗时 {{ formatDuration(record.durationMs) }}
</span>
<i :class="expandedRecords.includes(record.batchId) ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
</div>
</div>
<!-- 详情展开区域 -->
<el-collapse-transition>
<div v-if="expandedRecords.includes(record.batchId)" class="record-detail">
<el-divider></el-divider>
<div v-if="record.resultMessage" class="detail-message">
<div class="message-label">结果消息</div>
<div class="message-content">{{ record.resultMessage }}</div>
</div>
<div v-if="record.errorMessage" class="detail-error">
<div class="error-label">错误信息</div>
<div class="error-content">{{ record.errorMessage }}</div>
</div>
<!-- 操作日志列表 -->
<div v-if="record.operationLogs && record.operationLogs.length > 0" class="operation-logs">
<div class="logs-header">
<i class="el-icon-document"></i>
<span>操作日志{{ record.operationLogs.length }} </span>
</div>
<el-table
:data="record.operationLogs"
size="mini"
max-height="300"
stripe
>
<el-table-column prop="orderNo" label="订单号" width="150" />
<el-table-column prop="operationType" label="操作类型" width="100" />
<el-table-column prop="targetRow" label="目标行" width="80" />
<el-table-column prop="logisticsLink" label="物流链接" min-width="150" show-overflow-tooltip />
<el-table-column prop="operationStatus" label="状态" width="80">
<template slot-scope="scope">
<el-tag :type="scope.row.operationStatus === 'SUCCESS' ? 'success' : 'danger'" size="mini">
{{ scope.row.operationStatus }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="errorMessage" label="错误信息" min-width="150" show-overflow-tooltip />
</el-table>
</div>
</div>
</el-collapse-transition>
</el-card>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无推送记录"></el-empty>
</el-card>
</div>
</el-dialog>
</template>
<script>
import {
getPushStatus,
getBatchPushRecords,
triggerPushNow,
cancelPendingPush
} from '@/api/jarvis/tendoc'
export default {
name: 'TencentDocPushMonitor',
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return {
visible: false,
pushing: false,
pushStatus: {
isScheduled: false,
scheduledTime: null,
remainingSeconds: 0,
remainingMs: 0,
countdownText: '无定时任务',
lastSuccessRecord: null
},
countdownDisplay: {
minutes: '00',
seconds: '00'
},
batchRecords: [],
expandedRecords: [],
countdownTimer: null,
refreshTimer: null
}
},
watch: {
value(val) {
this.visible = val
if (val) {
this.init()
} else {
this.destroy()
}
},
visible(val) {
this.$emit('input', val)
}
},
methods: {
async init() {
await this.loadPushStatus()
await this.loadBatchRecords()
this.startCountdown()
this.startAutoRefresh()
},
destroy() {
this.stopCountdown()
this.stopAutoRefresh()
},
async loadPushStatus() {
try {
const res = await getPushStatus()
if (res.code === 200) {
this.pushStatus = res.data
this.updateCountdownDisplay()
}
} catch (e) {
console.error('加载推送状态失败', e)
}
},
async loadBatchRecords() {
try {
const res = await getBatchPushRecords({ limit: 20 })
if (res.code === 200) {
this.batchRecords = res.data || []
}
} catch (e) {
console.error('加载推送记录失败', e)
}
},
async handleTriggerPushNow() {
try {
await this.$confirm('确定要立即执行推送吗?', '提示', {
type: 'warning'
})
this.pushing = true
const res = await triggerPushNow()
if (res.code === 200) {
this.$message.success('推送已触发')
setTimeout(() => {
this.loadPushStatus()
this.loadBatchRecords()
}, 2000)
} else {
this.$message.error(res.msg || '触发推送失败')
}
} catch (e) {
if (e !== 'cancel') {
this.$message.error('触发推送失败: ' + (e.message || '未知错误'))
}
} finally {
this.pushing = false
}
},
async handleCancelPush() {
try {
await this.$confirm('确定要取消待推送任务吗?', '提示', {
type: 'warning'
})
const res = await cancelPendingPush()
if (res.code === 200) {
this.$message.success('已取消待推送任务')
this.loadPushStatus()
} else {
this.$message.error(res.msg || '取消失败')
}
} catch (e) {
if (e !== 'cancel') {
this.$message.error('取消失败: ' + (e.message || '未知错误'))
}
}
},
toggleRecordDetail(batchId) {
const index = this.expandedRecords.indexOf(batchId)
if (index > -1) {
this.expandedRecords.splice(index, 1)
} else {
this.expandedRecords.push(batchId)
}
},
startCountdown() {
this.stopCountdown()
this.countdownTimer = setInterval(() => {
if (this.pushStatus.remainingSeconds > 0) {
this.pushStatus.remainingSeconds--
this.pushStatus.remainingMs = this.pushStatus.remainingSeconds * 1000
this.updateCountdownDisplay()
} else if (this.pushStatus.isScheduled) {
// 倒计时结束,刷新状态
this.loadPushStatus()
this.loadBatchRecords()
}
}, 1000)
},
stopCountdown() {
if (this.countdownTimer) {
clearInterval(this.countdownTimer)
this.countdownTimer = null
}
},
startAutoRefresh() {
this.stopAutoRefresh()
// 每30秒自动刷新一次状态
this.refreshTimer = setInterval(() => {
this.loadPushStatus()
this.loadBatchRecords()
}, 30000)
},
stopAutoRefresh() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer)
this.refreshTimer = null
}
},
updateCountdownDisplay() {
const seconds = this.pushStatus.remainingSeconds || 0
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
this.countdownDisplay.minutes = String(minutes).padStart(2, '0')
this.countdownDisplay.seconds = String(secs).padStart(2, '0')
},
formatDateTime(dateTime) {
if (!dateTime) return '-'
return this.$moment(dateTime).format('YYYY-MM-DD HH:mm:ss')
},
formatDuration(ms) {
if (!ms) return '-'
const seconds = Math.floor(ms / 1000)
const minutes = Math.floor(seconds / 60)
const secs = seconds % 60
return minutes > 0 ? `${minutes}${secs}` : `${secs}`
},
getStatusText(status) {
const statusMap = {
'RUNNING': '执行中',
'SUCCESS': '成功',
'PARTIAL': '部分成功',
'FAILED': '失败'
}
return statusMap[status] || status
},
getStatusTagType(status) {
const typeMap = {
'RUNNING': 'warning',
'SUCCESS': 'success',
'PARTIAL': 'warning',
'FAILED': 'danger'
}
return typeMap[status] || 'info'
},
getRecordType(status) {
const typeMap = {
'SUCCESS': 'success',
'PARTIAL': 'warning',
'FAILED': 'danger',
'RUNNING': 'primary'
}
return typeMap[status] || 'info'
},
getRecordIcon(status) {
const iconMap = {
'SUCCESS': 'el-icon-success',
'PARTIAL': 'el-icon-warning',
'FAILED': 'el-icon-error',
'RUNNING': 'el-icon-loading'
}
return iconMap[status] || 'el-icon-info'
},
getTriggerSourceText(source) {
const sourceMap = {
'DELAYED_TIMER': '延迟定时器',
'USER': '用户手动',
'SYSTEM': '系统'
}
return sourceMap[source] || source
},
handleClose() {
this.visible = false
this.expandedRecords = []
}
},
beforeDestroy() {
this.destroy()
}
}
</script>
<style scoped>
.push-monitor {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 倒计时卡片 */
.countdown-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.countdown-card >>> .el-card__body {
padding: 0;
}
.countdown-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.countdown-header .header-left {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 500;
}
.countdown-header .header-left i {
font-size: 24px;
}
.countdown-content {
padding: 30px 20px 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.countdown-display {
display: flex;
align-items: center;
gap: 15px;
font-size: 48px;
font-weight: bold;
opacity: 0.5;
transition: all 0.3s;
}
.countdown-display.active {
opacity: 1;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.time-box {
display: flex;
flex-direction: column;
align-items: center;
min-width: 80px;
padding: 10px 20px;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
}
.time-value {
font-size: 48px;
line-height: 1;
}
.time-label {
font-size: 14px;
margin-top: 5px;
opacity: 0.8;
}
.time-separator {
font-size: 36px;
opacity: 0.6;
}
.countdown-info {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
max-width: 600px;
}
.info-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
padding: 8px 15px;
background: rgba(255, 255, 255, 0.1);
border-radius: 6px;
}
.info-item i {
font-size: 16px;
}
.countdown-actions {
display: flex;
gap: 10px;
}
/* 推送记录卡片 */
.records-card {
flex: 1;
}
.records-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.records-header .header-left {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 500;
}
.records-header .header-left i {
font-size: 20px;
color: #409eff;
}
.record-item {
cursor: pointer;
transition: all 0.3s;
}
.record-item:hover {
transform: translateY(-2px);
}
.record-summary {
display: flex;
justify-content: space-between;
align-items: center;
}
.summary-left,
.summary-right {
display: flex;
align-items: center;
gap: 15px;
}
.trigger-source {
color: #909399;
font-size: 13px;
}
.record-stats {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
}
.record-range,
.record-duration {
color: #606266;
font-size: 13px;
}
.record-detail {
margin-top: 15px;
}
.detail-message,
.detail-error {
margin-bottom: 15px;
}
.message-label,
.error-label {
font-weight: 500;
margin-bottom: 5px;
color: #606266;
}
.message-content {
padding: 10px;
background: #f0f9ff;
border-left: 3px solid #409eff;
border-radius: 4px;
font-size: 13px;
}
.error-content {
padding: 10px;
background: #fef0f0;
border-left: 3px solid #f56c6c;
border-radius: 4px;
font-size: 13px;
color: #f56c6c;
}
.operation-logs {
margin-top: 15px;
}
.logs-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
margin-bottom: 10px;
color: #606266;
}
.logs-header i {
color: #409eff;
}
</style>