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

@@ -1,188 +1,224 @@
<template>
<el-dialog
title="H-TF订单自动写入配置"
:visible.sync="visible"
width="600px"
width="900px"
:close-on-click-modal="false"
@close="handleClose"
top="5vh"
>
<!-- 配置状态 -->
<el-alert
v-if="config.isConfigured"
title="配置已完成"
type="success"
:description="config.hint"
show-icon
:closable="false"
style="margin-bottom: 20px;"
/>
<el-alert
v-else
title="配置未完成"
type="warning"
:description="config.hint"
show-icon
:closable="false"
style="margin-bottom: 20px;"
/>
<!-- 自定义标题 -->
<div slot="title" class="dialog-title">
<i class="el-icon-setting"></i>
<span>H-TF订单自动写入配置</span>
<el-tag v-if="config.isConfigured" type="success" size="mini" style="margin-left: 10px;">
<i class="el-icon-success"></i> 已配置
</el-tag>
<el-tag v-else type="warning" size="mini" style="margin-left: 10px;">
<i class="el-icon-warning"></i> 未配置
</el-tag>
</div>
<!-- 授权状态 -->
<el-card shadow="never" style="margin-bottom: 20px;">
<div slot="header">
<span>授权状态</span>
</div>
<el-form label-width="120px">
<el-form-item label="授权状态:">
<el-tag v-if="config.hasAccessToken" type="success">{{ config.accessTokenStatus }}</el-tag>
<el-tag v-else type="danger">{{ config.accessTokenStatus }}</el-tag>
<el-button
v-if="!config.hasAccessToken"
type="primary"
size="mini"
style="margin-left: 10px;"
@click="handleAuth"
>
立即授权
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 文档配置 -->
<el-card shadow="never">
<div slot="header">
<span>目标文档配置</span>
</div>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item label="文件ID" prop="fileId">
<el-input
v-model="form.fileId"
placeholder="请输入腾讯文档的文件ID"
clearable
>
<div class="config-container">
<!-- 左侧配置表单 -->
<div class="config-left">
<!-- 授权状态 -->
<div class="config-section">
<div class="section-header">
<i class="el-icon-key"></i>
<span>授权状态</span>
</div>
<div class="auth-status">
<el-tag v-if="config.hasAccessToken" type="success" size="medium">
<i class="el-icon-circle-check"></i> {{ config.accessTokenStatus }}
</el-tag>
<el-tag v-else type="danger" size="medium">
<i class="el-icon-circle-close"></i> {{ config.accessTokenStatus }}
</el-tag>
<el-button
slot="append"
icon="el-icon-search"
@click="handleFetchSheets"
:disabled="!form.fileId"
v-if="!config.hasAccessToken"
type="primary"
size="small"
icon="el-icon-unlock"
@click="handleAuth"
>
获取工作表
立即授权
</el-button>
</el-input>
<div style="font-size: 12px; color: #909399; margin-top: 5px;">
例如DUW50RUprWXh2TGJK从腾讯文档URL中获取
</div>
</el-form-item>
</div>
<el-form-item label="工作表ID" prop="sheetId">
<el-select
v-if="sheetList.length > 0"
v-model="form.sheetId"
placeholder="请选择工作表"
style="width: 100%;"
clearable
>
<el-option
v-for="sheet in sheetList"
:key="sheet.sheetId"
:label="sheet.title"
:value="sheet.sheetId"
/>
</el-select>
<el-input
v-else
v-model="form.sheetId"
placeholder="请输入工作表ID或点击'获取工作表'按钮选择"
clearable
/>
<div style="font-size: 12px; color: #909399; margin-top: 5px;">
例如BB08J2
<!-- 文档配置表单 -->
<div class="config-section">
<div class="section-header">
<i class="el-icon-document"></i>
<span>目标文档</span>
</div>
</el-form-item>
<el-form ref="form" :model="form" :rules="rules" label-width="100px" size="small">
<el-form-item label="文件ID" prop="fileId">
<el-input
v-model="form.fileId"
placeholder="例如DUW50RUprWXh2TGJK"
clearable
>
<el-button
slot="append"
icon="el-icon-search"
@click="handleFetchSheets"
:disabled="!form.fileId"
>
获取工作表
</el-button>
</el-input>
</el-form-item>
<el-form-item label="表头行号:" prop="headerRow">
<el-input-number
v-model="form.headerRow"
:min="1"
placeholder="表头所在的行号"
style="width: 100%;"
/>
<div style="font-size: 12px; color: #909399; margin-top: 5px;">
<i class="el-icon-info"></i>
表头所在的行包含"单号""物流单号"等列名默认为2
</div>
</el-form-item>
<el-form-item label="工作表ID" prop="sheetId">
<el-select
v-if="sheetList.length > 0"
v-model="form.sheetId"
placeholder="请选择工作表"
style="width: 100%;"
clearable
>
<el-option
v-for="sheet in sheetList"
:key="sheet.sheetId"
:label="sheet.title"
:value="sheet.sheetId"
>
<span style="float: left">{{ sheet.title }}</span>
<span style="float: right; color: #8492a6; font-size: 12px;">{{ sheet.sheetId }}</span>
</el-option>
</el-select>
<el-input
v-else
v-model="form.sheetId"
placeholder="例如BB08J2"
clearable
/>
</el-form-item>
<el-form-item label="数据起始行:" prop="startRow">
<el-input-number
v-model="form.startRow"
:min="1"
placeholder="数据从第几行开始"
style="width: 100%;"
/>
<div style="font-size: 12px; color: #909399; margin-top: 5px;">
<i class="el-icon-info"></i>
数据从第几行开始默认为3表示第3行是第一条数据
</div>
</el-form-item>
</el-form>
</el-card>
<!-- 同步进度显示 -->
<el-card v-if="config.progressHint" shadow="never" style="margin-top: 15px;">
<div slot="header" style="display: flex; align-items: center;">
<i class="el-icon-data-line" style="margin-right: 8px;"></i>
<span>同步进度</span>
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="表头行号" prop="headerRow">
<el-input-number
v-model="form.headerRow"
:min="1"
controls-position="right"
style="width: 100%;"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="起始行号" prop="startRow">
<el-input-number
v-model="form.startRow"
:min="1"
controls-position="right"
style="width: 100%;"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</div>
<el-alert
:title="config.progressHint"
:type="config.currentProgress ? 'success' : 'info'"
:closable="false"
show-icon
>
<template v-if="config.currentProgress">
<div style="margin-top: 10px; font-size: 13px;">
<p style="margin: 5px 0;">
<i class="el-icon-circle-check" style="color: #67c23a;"></i>
当前进度已处理到第 <strong>{{ config.currentProgress }}</strong>
</p>
<p style="margin: 5px 0;">
<i class="el-icon-right"></i>
下次同步
<span v-if="config.currentProgress <= (form.startRow + 49)">
将从第 <strong>{{ form.startRow }}</strong> 行重新开始进度较小
</span>
<span v-else-if="config.currentProgress > (form.startRow + 100)">
将从第 <strong>{{ config.currentProgress - 100 }}</strong> 行开始回溯100行防止遗漏
</span>
<span v-else>
将从第 <strong>{{ form.startRow }}</strong> 行重新开始
</span>
</p>
<p style="margin: 5px 0; color: #909399; font-size: 12px;">
<i class="el-icon-info"></i>
系统会自动回溯检查确保不遗漏数据如需完全重置请点击"清除配置"
</p>
<!-- 右侧状态信息 -->
<div class="config-right">
<!-- 配置状态提示 -->
<div class="status-card">
<div class="status-icon" :class="config.isConfigured ? 'success' : 'warning'">
<i :class="config.isConfigured ? 'el-icon-success' : 'el-icon-warning'"></i>
</div>
</template>
</el-alert>
</el-card>
<div class="status-text">
<div class="status-title">{{ config.isConfigured ? '配置完成' : '配置未完成' }}</div>
<div class="status-desc">{{ config.hint }}</div>
</div>
</div>
<!-- 同步进度 -->
<div v-if="config.progressHint" class="progress-card">
<div class="card-header">
<i class="el-icon-data-line"></i>
<span>同步进度</span>
</div>
<div class="progress-content">
<div v-if="config.currentProgress" class="progress-detail">
<div class="progress-item">
<span class="label">当前进度</span>
<span class="value"> {{ config.currentProgress }} </span>
</div>
<div class="progress-item">
<span class="label">下次同步</span>
<span class="value">
<template v-if="config.currentProgress <= (form.startRow + 49)">
{{ form.startRow }}
</template>
<template v-else-if="config.currentProgress > (form.startRow + 100)">
{{ config.currentProgress - 100 }}
</template>
<template v-else>
{{ form.startRow }}
</template>
</span>
</div>
<div class="progress-hint">
<i class="el-icon-info"></i>
系统自动回溯检查防止遗漏
</div>
</div>
<div v-else class="no-progress">
{{ config.progressHint }}
</div>
</div>
</div>
<!-- 快速帮助 -->
<div class="help-card">
<div class="card-header">
<i class="el-icon-question"></i>
<span>配置说明</span>
</div>
<div class="help-content">
<div class="help-item">
<i class="el-icon-check"></i>
<span>文件ID从腾讯文档URL中获取</span>
</div>
<div class="help-item">
<i class="el-icon-check"></i>
<span>点击"获取工作表"自动加载</span>
</div>
<div class="help-item">
<i class="el-icon-check"></i>
<span>表头行号默认为第2行</span>
</div>
<div class="help-item">
<i class="el-icon-check"></i>
<span>数据起始行默认为第3行</span>
</div>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div slot="footer">
<el-button @click="showOperationLogs = true" icon="el-icon-document" type="info" plain>
查看操作日志
</el-button>
<el-button @click="handleTest" :loading="testLoading" icon="el-icon-setting">
测试配置
</el-button>
<el-button @click="handleClear" :loading="clearLoading" type="danger" plain>
清除配置
</el-button>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSave" :loading="saveLoading">
保存配置
</el-button>
<div slot="footer" class="footer-buttons">
<div class="footer-left">
<el-button @click="showOperationLogs = true" icon="el-icon-document" size="small">
操作日志
</el-button>
<el-button @click="handleTest" :loading="testLoading" icon="el-icon-setting" size="small">
测试配置
</el-button>
</div>
<div class="footer-right">
<el-button @click="handleClear" :loading="clearLoading" type="danger" plain size="small">
清除配置
</el-button>
<el-button @click="handleClose" size="small">取消</el-button>
<el-button type="primary" @click="handleSave" :loading="saveLoading" size="small">
<i class="el-icon-check"></i> 保存配置
</el-button>
</div>
</div>
<!-- 操作日志查看对话框 -->
@@ -450,8 +486,264 @@ export default {
</script>
<style scoped>
.el-card {
border: 1px solid #EBEEF5;
/* 标题样式 */
.dialog-title {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 500;
}
.dialog-title i {
margin-right: 8px;
font-size: 18px;
}
/* 容器布局 */
.config-container {
display: flex;
gap: 20px;
min-height: 400px;
}
.config-left {
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
}
.config-right {
width: 300px;
display: flex;
flex-direction: column;
gap: 15px;
}
/* 配置区块 */
.config-section {
background: #f5f7fa;
border-radius: 6px;
padding: 15px;
}
.section-header {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid #e4e7ed;
}
.section-header i {
margin-right: 6px;
font-size: 16px;
color: #409eff;
}
/* 授权状态 */
.auth-status {
display: flex;
align-items: center;
gap: 10px;
}
/* 状态卡片 */
.status-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
padding: 20px;
color: white;
display: flex;
align-items: center;
gap: 15px;
box-shadow: 0 2px 12px rgba(102, 126, 234, 0.3);
}
.status-card.warning {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.status-icon {
width: 48px;
height: 48px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
.status-icon.success {
background: rgba(103, 194, 58, 0.2);
}
.status-icon.warning {
background: rgba(230, 162, 60, 0.2);
}
.status-text {
flex: 1;
}
.status-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 5px;
}
.status-desc {
font-size: 12px;
opacity: 0.9;
line-height: 1.5;
}
/* 进度卡片 */
.progress-card {
background: white;
border: 1px solid #e4e7ed;
border-radius: 6px;
overflow: hidden;
}
.card-header {
background: #f5f7fa;
padding: 12px 15px;
font-size: 14px;
font-weight: 500;
color: #303133;
display: flex;
align-items: center;
border-bottom: 1px solid #e4e7ed;
}
.card-header i {
margin-right: 6px;
color: #409eff;
}
.progress-content {
padding: 15px;
}
.progress-detail {
display: flex;
flex-direction: column;
gap: 10px;
}
.progress-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #f0f9ff;
border-radius: 4px;
border-left: 3px solid #409eff;
}
.progress-item .label {
font-size: 13px;
color: #606266;
}
.progress-item .value {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.progress-hint {
font-size: 12px;
color: #909399;
padding: 8px 12px;
background: #fef0f0;
border-radius: 4px;
border-left: 3px solid #f56c6c;
display: flex;
align-items: center;
gap: 5px;
}
.no-progress {
font-size: 13px;
color: #909399;
text-align: center;
padding: 10px;
}
/* 帮助卡片 */
.help-card {
background: white;
border: 1px solid #e4e7ed;
border-radius: 6px;
overflow: hidden;
}
.help-content {
padding: 15px;
display: flex;
flex-direction: column;
gap: 10px;
}
.help-item {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
color: #606266;
line-height: 1.6;
}
.help-item i {
color: #67c23a;
margin-top: 2px;
flex-shrink: 0;
}
/* 底部按钮 */
.footer-buttons {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
}
.footer-left,
.footer-right {
display: flex;
gap: 8px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.config-container {
flex-direction: column;
}
.config-right {
width: 100%;
}
}
/* Element UI 覆盖样式 */
.config-section >>> .el-form-item {
margin-bottom: 18px;
}
.config-section >>> .el-form-item__label {
font-weight: 500;
color: #606266;
}
.config-section >>> .el-input-number {
width: 100%;
}
</style>

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>

View File

@@ -45,6 +45,7 @@
<el-button size="small" icon="el-icon-refresh" @click="resetQuery">重置</el-button>
<el-button type="warning" size="small" icon="el-icon-download" @click="handleExport" v-hasPermi="['system:jdorder:export']">导出</el-button>
<el-button type="success" size="small" icon="el-icon-setting" @click="showAutoWriteConfig = true" title="配置H-TF订单自动写入腾讯文档">腾峰文档配置</el-button>
<el-button type="info" size="small" icon="el-icon-monitor" @click="showPushMonitor = true" title="查看推送监控和历史记录">推送监控</el-button>
<el-button type="primary" size="small" icon="el-icon-refresh-right" @click="handleBatchSyncLogistics" :loading="batchSyncLoading" title="批量同步物流链接到腾讯文档">
<i v-if="!batchSyncLoading"></i>
一键发货到腾峰
@@ -351,6 +352,9 @@
<!-- H-TF订单自动写入配置 -->
<tencent-doc-auto-write-config v-model="showAutoWriteConfig" @config-updated="handleAutoConfigUpdated" />
<!-- 腾讯文档推送监控 -->
<tencent-doc-push-monitor v-model="showPushMonitor" />
</div>
</template>
@@ -359,12 +363,14 @@ import { listJDOrders, updateJDOrder, delJDOrder, fetchLogisticsManually } from
import { fillLogisticsByOrderNo, getTokenStatus, getTencentDocAuthUrl, testUserInfo, getAutoWriteConfig } from '@/api/jarvis/tendoc'
import ListLayout from '@/components/ListLayout'
import TencentDocAutoWriteConfig from './components/TencentDocAutoWriteConfig'
import TencentDocPushMonitor from './components/TencentDocPushMonitor'
export default {
name: 'JDOrderList',
components: {
ListLayout,
TencentDocAutoWriteConfig
TencentDocAutoWriteConfig,
TencentDocPushMonitor
},
data() {
return {
@@ -404,6 +410,7 @@ export default {
tokenStatusChecked: false,
// H-TF订单自动写入配置
showAutoWriteConfig: false,
showPushMonitor: false,
// 批量同步loading状态
batchSyncLoading: false,
// 获取物流信息对话框