977 lines
25 KiB
Vue
977 lines
25 KiB
Vue
<template>
|
||
<el-dialog
|
||
title="腾讯文档推送监控"
|
||
:visible="visible"
|
||
@update:visible="handleVisibleChange"
|
||
:width="isMobile ? '100%' : '1200px'"
|
||
:close-on-click-modal="false"
|
||
@close="handleClose"
|
||
:top="isMobile ? '0' : '5vh'"
|
||
:fullscreen="isMobile"
|
||
:custom-class="isMobile ? 'mobile-push-monitor-dialog' : ''"
|
||
:modal-append-to-body="true"
|
||
:append-to-body="true"
|
||
>
|
||
<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"
|
||
:size="isMobile ? 'small' : 'default'"
|
||
@click="handleTriggerPushNow"
|
||
>
|
||
立即推送
|
||
</el-button>
|
||
<el-button
|
||
type="warning"
|
||
icon="el-icon-close"
|
||
:disabled="!pushStatus.isScheduled"
|
||
:size="isMobile ? 'small' : 'default'"
|
||
@click="handleCancelPush"
|
||
>
|
||
取消推送
|
||
</el-button>
|
||
<el-button
|
||
icon="el-icon-refresh"
|
||
:size="isMobile ? 'small' : 'default'"
|
||
@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.loadingDetail" class="loading-detail" v-loading="true" element-loading-text="正在加载详情...">
|
||
<div style="height: 100px;"></div>
|
||
</div>
|
||
|
||
<template v-else>
|
||
<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 v-else class="no-logs">
|
||
<i class="el-icon-info"></i>
|
||
<span>暂无操作日志</span>
|
||
</div>
|
||
</template>
|
||
</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,
|
||
getBatchPushRecordDetail,
|
||
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
|
||
}
|
||
},
|
||
computed: {
|
||
isMobile() {
|
||
if (this.$store?.getters?.device === 'mobile') {
|
||
return true
|
||
}
|
||
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
},
|
||
watch: {
|
||
value(val) {
|
||
if (this.visible !== val) {
|
||
this.visible = val
|
||
if (val) {
|
||
this.init()
|
||
} else {
|
||
this.destroy()
|
||
}
|
||
}
|
||
}
|
||
},
|
||
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()
|
||
console.log('=== 推送状态响应 ===', res)
|
||
if (res.code === 200) {
|
||
console.log('推送状态数据:', res.data)
|
||
console.log('isScheduled:', res.data.isScheduled)
|
||
console.log('remainingSeconds:', res.data.remainingSeconds)
|
||
console.log('scheduledTime:', res.data.scheduledTime)
|
||
|
||
// 重要:使用解构赋值,确保 remainingSeconds 被正确赋值
|
||
this.pushStatus = {
|
||
...res.data,
|
||
remainingSeconds: parseInt(res.data.remainingSeconds) || 0
|
||
}
|
||
|
||
this.updateCountdownDisplay()
|
||
console.log('倒计时显示:', this.countdownDisplay)
|
||
console.log('pushStatus.remainingSeconds 已更新为:', this.pushStatus.remainingSeconds)
|
||
} else {
|
||
console.error('API返回错误:', res)
|
||
}
|
||
} catch (e) {
|
||
console.error('加载推送状态失败', e)
|
||
}
|
||
},
|
||
|
||
async loadBatchRecords() {
|
||
try {
|
||
const res = await getBatchPushRecords({ limit: 20 })
|
||
console.log('加载推送记录响应:', res)
|
||
if (res.code === 200) {
|
||
const records = res.data || []
|
||
// 确保每条记录都有 operationLogs 字段(即使为空数组)
|
||
records.forEach(record => {
|
||
if (!record.hasOwnProperty('operationLogs')) {
|
||
this.$set(record, 'operationLogs', [])
|
||
}
|
||
// 重置详情加载标记,允许重新加载
|
||
this.$set(record, 'detailLoaded', false)
|
||
})
|
||
this.batchRecords = records
|
||
console.log('推送记录数量:', records.length)
|
||
records.forEach(r => {
|
||
console.log(`记录 ${r.batchId}: operationLogs数量=${r.operationLogs ? r.operationLogs.length : 'undefined'}`)
|
||
})
|
||
}
|
||
} 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 || '未知错误'))
|
||
}
|
||
}
|
||
},
|
||
|
||
async toggleRecordDetail(batchId) {
|
||
const index = this.expandedRecords.indexOf(batchId)
|
||
if (index > -1) {
|
||
// 收起
|
||
this.expandedRecords.splice(index, 1)
|
||
} else {
|
||
// 展开 - 加载详情
|
||
this.expandedRecords.push(batchId)
|
||
await this.loadRecordDetail(batchId)
|
||
}
|
||
},
|
||
|
||
async loadRecordDetail(batchId) {
|
||
try {
|
||
const record = this.batchRecords.find(r => r.batchId === batchId)
|
||
if (!record) return
|
||
|
||
// 如果已经明确加载过详情(有 loadingDetail 标记且已完成),则不再重复加载
|
||
// 注意:即使 operationLogs 为空数组,也可能是数据确实为空,需要重新加载确认
|
||
if (record.detailLoaded) {
|
||
return
|
||
}
|
||
|
||
// 显示加载状态
|
||
this.$set(record, 'loadingDetail', true)
|
||
|
||
const res = await getBatchPushRecordDetail(batchId)
|
||
console.log('加载推送详情响应:', res)
|
||
console.log('响应数据:', JSON.stringify(res.data, null, 2))
|
||
if (res.code === 200 && res.data) {
|
||
// 更新记录的详细信息
|
||
const operationLogs = res.data.operationLogs || []
|
||
console.log('操作日志数量:', operationLogs.length, 'batchId:', batchId)
|
||
console.log('操作日志详情:', operationLogs)
|
||
this.$set(record, 'operationLogs', operationLogs)
|
||
this.$set(record, 'errorMessage', res.data.errorMessage)
|
||
// 使用 resultMessage 字段,如果没有则使用 remark
|
||
this.$set(record, 'resultMessage', res.data.resultMessage || res.data.remark)
|
||
// 标记已加载详情
|
||
this.$set(record, 'detailLoaded', true)
|
||
|
||
// 如果操作日志为空,输出警告信息用于调试
|
||
if (operationLogs.length === 0) {
|
||
console.warn('操作日志为空 - batchId:', batchId, '记录数据:', res.data)
|
||
}
|
||
} else {
|
||
this.$message.warning('加载详情失败: ' + (res.msg || '未知错误'))
|
||
}
|
||
} catch (e) {
|
||
console.error('加载推送详情失败', e)
|
||
this.$message.error('加载详情失败: ' + (e.message || '未知错误'))
|
||
} finally {
|
||
const record = this.batchRecords.find(r => r.batchId === batchId)
|
||
if (record) {
|
||
this.$set(record, 'loadingDetail', false)
|
||
}
|
||
}
|
||
},
|
||
|
||
startCountdown() {
|
||
this.stopCountdown()
|
||
|
||
// 立即更新一次显示
|
||
this.updateCountdownDisplay()
|
||
|
||
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
|
||
console.log('更新倒计时显示 - remainingSeconds:', seconds)
|
||
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')
|
||
console.log('倒计时显示更新为:', this.countdownDisplay.minutes + ':' + this.countdownDisplay.seconds)
|
||
},
|
||
|
||
formatDateTime(dateTime) {
|
||
if (!dateTime) return '-'
|
||
try {
|
||
// 处理多种时间格式
|
||
const date = new Date(dateTime)
|
||
if (isNaN(date.getTime())) return dateTime
|
||
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
const hours = String(date.getHours()).padStart(2, '0')
|
||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||
} catch (e) {
|
||
console.error('格式化时间失败:', e, dateTime)
|
||
return dateTime
|
||
}
|
||
},
|
||
|
||
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 = []
|
||
this.$emit('input', false)
|
||
},
|
||
|
||
handleVisibleChange(val) {
|
||
this.visible = val
|
||
this.$emit('input', val)
|
||
if (!val) {
|
||
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;
|
||
}
|
||
|
||
.loading-detail {
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: #909399;
|
||
}
|
||
|
||
.no-logs {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
padding: 30px;
|
||
color: #909399;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.no-logs i {
|
||
font-size: 18px;
|
||
}
|
||
|
||
/* 移动端适配 */
|
||
@media (max-width: 768px) {
|
||
.push-monitor {
|
||
gap: 12px;
|
||
}
|
||
|
||
.countdown-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
padding: 12px;
|
||
}
|
||
|
||
.countdown-header .header-left {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.countdown-header .header-left i {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.countdown-content {
|
||
padding: 20px 12px 12px;
|
||
gap: 15px;
|
||
}
|
||
|
||
.countdown-display {
|
||
font-size: 32px;
|
||
gap: 10px;
|
||
}
|
||
|
||
.time-box {
|
||
min-width: 60px;
|
||
padding: 8px 12px;
|
||
}
|
||
|
||
.time-value {
|
||
font-size: 32px;
|
||
}
|
||
|
||
.time-separator {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.countdown-info {
|
||
gap: 8px;
|
||
}
|
||
|
||
.info-item {
|
||
font-size: 12px;
|
||
padding: 6px 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.countdown-actions {
|
||
flex-direction: column;
|
||
width: 100%;
|
||
gap: 8px;
|
||
}
|
||
|
||
.countdown-actions .el-button {
|
||
width: 100%;
|
||
margin: 0;
|
||
}
|
||
|
||
.records-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
}
|
||
|
||
.records-header .header-left {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.record-summary {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
}
|
||
|
||
.summary-left,
|
||
.summary-right {
|
||
width: 100%;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.record-stats {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.trigger-source,
|
||
.record-range,
|
||
.record-duration {
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
/* 移动端全屏弹窗样式 */
|
||
::v-deep .mobile-push-monitor-dialog {
|
||
margin: 0 !important;
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
max-width: 100% !important;
|
||
max-height: 100% !important;
|
||
}
|
||
|
||
::v-deep .mobile-push-monitor-dialog .el-dialog {
|
||
margin: 0 !important;
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
max-width: 100% !important;
|
||
max-height: 100% !important;
|
||
border-radius: 0 !important;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
::v-deep .mobile-push-monitor-dialog .el-dialog__body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 12px;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
::v-deep .mobile-push-monitor-dialog .el-dialog__header {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid #e4e7ed;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
::v-deep .mobile-push-monitor-dialog .el-dialog__title {
|
||
font-size: 16px;
|
||
}
|
||
</style>
|
||
|