Files
ruoyi-vue/src/views/system/jdorder/orderList.vue
2025-11-20 23:38:06 +08:00

1466 lines
55 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

<template>
<div>
<list-layout>
<!-- 搜索区域 -->
<template #search>
<el-form :inline="true" :model="queryParams" label-width="80px">
<el-form-item label="备注">
<el-input v-model="queryParams.remark" placeholder="单据备注" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="订单号">
<el-input v-model="queryParams.orderSearch" placeholder="订单号/第三方单号/分销标识" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="分销标记">
<el-input v-model="queryParams.distributionMark" placeholder="分销标记" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="型号">
<el-input v-model="queryParams.modelNumber" placeholder="型号" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="下单人">
<el-input v-model="queryParams.buyer" placeholder="下单人" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="地址">
<el-input v-model="queryParams.address" placeholder="收货地址" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="状态">
<el-input v-model="queryParams.status" placeholder="备注/状态" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="下单时间">
<el-date-picker
v-model="dateRange"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
size="small"
range-separator=""
@change="handleDateRangeChange"
/>
</el-form-item>
<el-form-item label="完成日期">
<el-checkbox v-model="queryParams.hasFinishTime" @change="handleQuery">仅显示已完成订单</el-checkbox>
</el-form-item>
<el-form-item label="退款状态">
<el-select v-model="queryParams.isRefunded" placeholder="全部" clearable size="small" style="width: 120px;">
<el-option label="已退款" :value="1" />
<el-option label="未退款" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="退款到账">
<el-select v-model="queryParams.isRefundReceived" placeholder="全部" clearable size="small" style="width: 120px;">
<el-option label="已到账" :value="1" />
<el-option label="未到账" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="后返到账">
<el-select v-model="queryParams.isRebateReceived" placeholder="全部" clearable size="small" style="width: 120px;">
<el-option label="已到账" :value="1" />
<el-option label="未到账" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="small" icon="el-icon-search" @click="handleQuery">搜索</el-button>
<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>
一键发货到腾峰
</el-button>
<el-button type="success" size="small" icon="el-icon-check" @click="handleBatchMarkRebateReceived" :loading="batchMarkLoading" title="批量将赔付金额大于0的订单标记为后返到账仅执行一次">
批量标记后返到账
</el-button>
<el-button type="warning" size="small" icon="el-icon-sort" @click="handleReverseSyncThirdPartyOrderNo" :loading="reverseSyncLoading" title="从腾讯文档第850行开始通过物流链接反向匹配订单将腾讯文档的单号列值写入到订单的第三方单号字段">
反向同步第三方单号
</el-button>
<el-button type="primary" size="small" icon="el-icon-document-copy" @click="handleCopyExcelText" :loading="copyExcelTextLoading" title="生成录单格式文本Tab分隔可直接粘贴到Excel">
复制录单格式
</el-button>
</el-form-item>
</el-form>
</template>
<!-- 表格区域 -->
<template #table>
<el-table
:data="list"
v-loading="loading"
border
stripe
:default-sort="{prop: 'createTime', order: 'descending'}"
@sort-change="handleSortChange"
style="width: 100%;"
class="order-table">
<!-- 核心信息列 -->
<el-table-column label="内部单号" prop="remark" width="140" sortable fixed="left"/>
<el-table-column label="订单号" prop="orderId" width="180"/>
<el-table-column label="第三方单号" prop="thirdPartyOrderNo" width="150">
<template slot-scope="scope">
<span v-if="scope.row.thirdPartyOrderNo">{{ scope.row.thirdPartyOrderNo }}</span>
<span v-else style="color: #999;">-</span>
</template>
</el-table-column>
<!-- 业务信息列 -->
<el-table-column label="标记" prop="distributionMark" width="100"/>
<el-table-column label="型号" prop="modelNumber" width="160"/>
<el-table-column label="地址" prop="address" min-width="280" show-overflow-tooltip/>
<!-- 金额信息列 -->
<el-table-column label="付款金额" prop="paymentAmount" width="110" align="right">
<template slot-scope="scope">{{ toYuan(scope.row.paymentAmount) }}</template>
</el-table-column>
<el-table-column label="后返金额" prop="rebateAmount" width="110" align="right">
<template slot-scope="scope">{{ toYuan(scope.row.rebateAmount) }}</template>
</el-table-column>
<el-table-column label="下单人" prop="buyer" width="100"/>
<!-- 退款状态标签列三行显示 -->
<el-table-column label="退款状态" width="160" align="left">
<template slot-scope="scope">
<div style="display: flex; flex-direction: column; gap: 6px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="min-width: 80px; font-size: 12px; color: #606266;">是否退款</span>
<el-tag
:type="scope.row.isRefunded === 1 ? 'warning' : 'info'"
size="small"
:title="scope.row.isRefunded === 1 && scope.row.refundDate ? '退款日期:' + parseTime(scope.row.refundDate) : ''"
@click.native="toggleRefunded(scope.row)"
style="cursor: pointer; flex: 1;">
{{ scope.row.isRefunded === 1 ? '已退款' : '未退款' }}
</el-tag>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="min-width: 80px; font-size: 12px; color: #606266;">退款给我</span>
<el-tag
:type="scope.row.isRefundReceived === 1 ? 'success' : 'info'"
size="small"
:title="scope.row.isRefundReceived === 1 && scope.row.refundReceivedDate ? '退款到账日期:' + parseTime(scope.row.refundReceivedDate) : ''"
@click.native="toggleRefundReceived(scope.row)"
style="cursor: pointer; flex: 1;">
{{ scope.row.isRefundReceived === 1 ? '退款到账' : '未到账' }}
</el-tag>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="min-width: 80px; font-size: 12px; color: #606266;">后返到账</span>
<el-tag
:type="scope.row.isRebateReceived === 1 ? 'success' : 'info'"
size="small"
:title="scope.row.isRebateReceived === 1 && scope.row.rebateReceivedDate ? '后返到账日期:' + parseTime(scope.row.rebateReceivedDate) : ''"
@click.native="toggleRebateReceived(scope.row)"
style="cursor: pointer; flex: 1;">
{{ scope.row.isRebateReceived === 1 ? '后返到账' : '未到账' }}
</el-tag>
</div>
</div>
</template>
</el-table-column>
<!-- 时间信息列 -->
<el-table-column label="创建时间" prop="createTime" width="160" sortable="custom">
<template slot-scope="scope">{{ parseTime(scope.row.createTime) }}</template>
</el-table-column>
<el-table-column label="完成时间" prop="finishTime" width="160">
<template slot-scope="scope">{{ parseTime(scope.row.finishTime) || '-' }}</template>
</el-table-column>
<!-- 其他信息列可折叠 -->
<el-table-column label="备注/状态" prop="status" min-width="120" show-overflow-tooltip/>
<el-table-column label="订单状态" prop="orderStatus" width="100" align="center">
<template slot-scope="scope">
<el-tag
v-if="scope.row.orderStatus != null"
:type="getOrderStatusType(scope.row.orderStatus)"
size="small">
{{ getOrderStatusText(scope.row.orderStatus) }}
</el-tag>
<span v-else style="color: #999;">-</span>
</template>
</el-table-column>
<!-- 操作列统一放在最右侧 -->
<el-table-column label="操作" fixed="right" width="280" align="center">
<template slot-scope="scope">
<div style="display: flex; flex-wrap: wrap; gap: 4px; justify-content: center;">
<!-- 复制操作 -->
<el-button
type="text"
size="mini"
icon="el-icon-copy-document"
@click="copyToClipboard(scope.row.orderId)"
title="复制订单号">
订单号
</el-button>
<el-button
v-if="scope.row.thirdPartyOrderNo"
type="text"
size="mini"
icon="el-icon-copy-document"
@click="copyToClipboard(scope.row.thirdPartyOrderNo)"
title="复制第三方单号">
第三方
</el-button>
<el-button
type="text"
size="mini"
icon="el-icon-copy-document"
@click="copyToClipboard(scope.row.address)"
title="复制地址">
地址
</el-button>
<el-button
v-if="scope.row.logisticsLink"
type="text"
size="mini"
icon="el-icon-copy-document"
@click="copyToClipboard(scope.row.logisticsLink)"
title="复制物流链接">
物流
</el-button>
<!-- 业务操作 -->
<el-button
type="text"
size="mini"
@click="copyReturnInfo(scope.row)"
title="复制退货信息">
退货复制
</el-button>
<el-button
type="text"
size="mini"
style="color: #67C23A;"
@click="handleFetchLogistics(scope.row)"
:title="scope.row.distributionMark">
获取物流
</el-button>
<!-- 统计开关 -->
<el-switch
v-model="scope.row.isCountEnabled"
:active-value="1"
:inactive-value="0"
@change="handleCountEnabledChange(scope.row)"
active-text="统计"
inactive-text=""
style="margin-left: 4px;">
</el-switch>
<!-- 删除操作 -->
<el-button
type="text"
size="mini"
style="color: #f56c6c;"
@click="handleDelete(scope.row)"
title="删除订单">
删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</template>
<!-- 分页区域 -->
<template #pagination>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</template>
</list-layout>
<!-- 获取物流信息对话框 -->
<el-dialog
title="获取物流信息(调试)"
:visible.sync="fetchLogisticsDialogVisible"
width="800px"
:close-on-click-modal="false"
>
<div v-loading="fetchLogisticsLoading">
<el-alert
v-if="fetchLogisticsResult"
:title="fetchLogisticsResult.success ? '获取成功' : '获取失败'"
:type="fetchLogisticsResult.success ? 'success' : 'error'"
:closable="false"
style="margin-bottom: 20px;"
/>
<el-form label-width="120px" v-if="fetchLogisticsResult">
<el-form-item label="错误信息" v-if="fetchLogisticsResult.error">
<el-alert
:title="fetchLogisticsResult.error"
type="error"
:closable="false"
/>
</el-form-item>
<el-form-item label="订单ID" v-if="fetchLogisticsResult.orderId">
<span>{{ fetchLogisticsResult.orderId }}</span>
</el-form-item>
<el-form-item label="订单号" v-if="fetchLogisticsResult.orderNo">
<span>{{ fetchLogisticsResult.orderNo }}</span>
</el-form-item>
<el-form-item label="分销标识" v-if="fetchLogisticsResult.distributionMark">
<span>{{ fetchLogisticsResult.distributionMark }}</span>
</el-form-item>
<el-form-item label="物流链接" v-if="fetchLogisticsResult.logisticsLink">
<el-input
:value="fetchLogisticsResult.logisticsLink"
readonly
type="textarea"
:rows="2"
/>
</el-form-item>
<el-form-item label="请求URL" v-if="fetchLogisticsResult.requestUrl">
<el-input
:value="fetchLogisticsResult.requestUrl"
readonly
type="textarea"
:rows="2"
/>
</el-form-item>
<el-form-item label="返回数据(原始)" v-if="fetchLogisticsResult.responseRaw || fetchLogisticsResult.responseData">
<el-input
:value="fetchLogisticsResult.responseRaw || JSON.stringify(fetchLogisticsResult.responseData, null, 2)"
readonly
type="textarea"
:rows="10"
style="font-family: monospace; font-size: 12px;"
/>
</el-form-item>
<el-form-item label="返回数据(解析后)" v-if="fetchLogisticsResult.responseData">
<el-input
:value="JSON.stringify(fetchLogisticsResult.responseData, null, 2)"
readonly
type="textarea"
:rows="10"
style="font-family: monospace; font-size: 12px;"
/>
</el-form-item>
</el-form>
<div v-else style="text-align: center; padding: 40px;">
<span style="color: #999;">正在获取物流信息...</span>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="fetchLogisticsDialogVisible = false">关闭</el-button>
<el-button
type="primary"
@click="copyFetchLogisticsResult"
v-if="fetchLogisticsResult"
>
复制结果
</el-button>
</div>
</el-dialog>
<!-- 同步物流对话框 -->
<el-dialog
title="同步物流到腾讯文档"
:visible.sync="syncLogisticsDialogVisible"
width="600px"
:close-on-click-modal="false"
>
<el-form :model="syncLogisticsForm" label-width="140px">
<el-form-item label="文件ID" required>
<el-input
v-model="syncLogisticsForm.fileId"
placeholder="从腾讯文档URL中获取例如Dxxxxxxxxxxxxx"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="工作表ID" required>
<el-input
v-model="syncLogisticsForm.sheetId"
placeholder="从腾讯文档URL中获取例如BB08J2"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="表头行号">
<el-input-number
v-model="syncLogisticsForm.headerRow"
:min="1"
style="width: 100%"
/>
<div style="font-size: 12px; color: #999; margin-top: 4px;">
表头所在行号默认第1行
</div>
</el-form-item>
<el-form-item label="单号列索引(可选)">
<el-input-number
v-model="syncLogisticsForm.orderNoColumn"
:min="0"
style="width: 100%"
placeholder="不填写则自动识别"
/>
<div style="font-size: 12px; color: #999; margin-top: 4px;">
单号列的索引从0开始不填写则自动识别
</div>
</el-form-item>
<el-form-item label="物流链接列索引(可选)">
<el-input-number
v-model="syncLogisticsForm.logisticsLinkColumn"
:min="0"
style="width: 100%"
placeholder="不填写则自动识别"
/>
<div style="font-size: 12px; color: #999; margin-top: 4px;">
物流链接列的索引从0开始不填写则自动识别
</div>
</el-form-item>
<el-form-item label="说明">
<div style="font-size: 12px; color: #666; line-height: 1.6;">
<p>1. 点击"同步物流"如果没有token会自动打开授权页面</p>
<p>2. 完成授权后点击"开始同步"即可自动刷新token并同步数据</p>
<p>3. 文件ID和工作表ID可以从腾讯文档URL中获取</p>
<p>4. 系统会自动从上次处理的最大行数-100开始读取避免重复处理历史数据</p>
<p>5. 配置会自动保存下次使用时无需重新填写</p>
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="syncLogisticsDialogVisible = false">取消</el-button>
<el-button
type="primary"
:loading="syncLogisticsLoading"
@click="handleSyncLogisticsSubmit"
>
开始同步
</el-button>
</div>
</el-dialog>
<!-- H-TF订单自动写入配置 -->
<tencent-doc-auto-write-config v-model="showAutoWriteConfig" @config-updated="handleAutoConfigUpdated" />
<!-- 腾讯文档推送监控 -->
<tencent-doc-push-monitor v-model="showPushMonitor" />
</div>
</template>
<script>
import { listJDOrders, updateJDOrder, delJDOrder, fetchLogisticsManually, batchMarkRebateReceived, generateExcelText } from '@/api/system/jdorder'
import { fillLogisticsByOrderNo, getTokenStatus, getTencentDocAuthUrl, testUserInfo, getAutoWriteConfig, reverseSyncThirdPartyOrderNo } 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,
TencentDocPushMonitor
},
data() {
return {
loading: false,
list: [],
total: 0,
dateRange: [],
queryParams: {
pageNum: 1,
pageSize: 50,
remark: undefined,
orderSearch: undefined,
distributionMark: undefined,
modelNumber: undefined,
link: undefined,
buyer: undefined,
address: undefined,
status: undefined,
beginTime: null,
endTime: null,
orderBy: 'create_time',
isAsc: 'desc',
hasFinishTime: false,
isRefunded: undefined,
isRefundReceived: undefined,
isRebateReceived: undefined
},
// 同步物流对话框
syncLogisticsDialogVisible: false,
syncLogisticsLoading: false,
syncLogisticsForm: {
fileId: '',
sheetId: '',
headerRow: 1,
orderNoColumn: null,
logisticsLinkColumn: null
},
currentOrder: null,
tokenValid: false,
tokenStatusChecked: false,
// H-TF订单自动写入配置
showAutoWriteConfig: false,
showPushMonitor: false,
// 批量同步loading状态
batchSyncLoading: false,
// 批量标记后返到账loading状态
batchMarkLoading: false,
// 反向同步第三方单号loading状态
reverseSyncLoading: false,
// 获取物流信息对话框
fetchLogisticsDialogVisible: false,
fetchLogisticsLoading: false,
fetchLogisticsResult: null,
// 复制录单格式loading状态
copyExcelTextLoading: false
}
},
created() {
// 设置默认日期为今天
this.setDefaultDateRange()
this.getListWithFallback()
// 监听腾讯文档授权回调消息
this.handleOAuthMessage = (event) => {
if (event.data && event.data.type === 'tendoc_oauth_callback') {
if (event.data.success) {
this.$message.success(event.data.message || '授权成功!')
// 刷新token状态
this.checkTokenStatus()
} else {
this.$message.error(event.data.message || '授权失败')
}
}
}
window.addEventListener('message', this.handleOAuthMessage)
},
beforeDestroy() {
// 移除消息监听器
if (this.handleOAuthMessage) {
window.removeEventListener('message', this.handleOAuthMessage)
}
},
methods: {
/** 设置默认日期范围为今天 */
setDefaultDateRange() {
const today = new Date()
const todayStr = this.formatDate(today)
this.dateRange = [todayStr, todayStr]
this.queryParams.beginTime = todayStr
this.queryParams.endTime = todayStr
},
/** 格式化日期为 yyyy-MM-dd 格式 */
formatDate(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
getList() {
this.loading = true
listJDOrders(this.queryParams).then(res => {
const list = (res.rows || res.data || [])
// 为退款相关字段设置默认值
this.list = list.map(item => ({
...item,
isRefunded: item.isRefunded != null ? item.isRefunded : 0,
isRefundReceived: item.isRefundReceived != null ? item.isRefundReceived : 0,
isRebateReceived: item.isRebateReceived != null ? item.isRebateReceived : 0
}))
this.total = res.total || 0
this.loading = false
}).catch(() => { this.loading = false })
},
/** 智能查询列表,如果今天数据为空则查询昨天 */
async getListWithFallback() {
this.loading = true
try {
const res = await listJDOrders(this.queryParams)
let list = (res.rows || res.data || [])
// 为退款相关字段设置默认值
list = list.map(item => ({
...item,
isRefunded: item.isRefunded != null ? item.isRefunded : 0,
isRefundReceived: item.isRefundReceived != null ? item.isRefundReceived : 0,
isRebateReceived: item.isRebateReceived != null ? item.isRebateReceived : 0
}))
this.list = list
this.total = res.total || 0
// 如果今天的数据为空,且是默认查询(没有手动选择日期),则尝试查询昨天
if (this.list.length === 0 && this.isDefaultQuery()) {
this.$message.info('今天暂无慢单数据,正在查询昨天的数据...')
// 获取昨天的日期
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
const yesterdayStr = this.formatDate(yesterday)
// 更新查询参数为昨天
this.queryParams.beginTime = yesterdayStr
this.queryParams.endTime = yesterdayStr
this.dateRange = [yesterdayStr, yesterdayStr]
// 查询昨天的数据
const yesterdayRes = await listJDOrders(this.queryParams)
let yesterdayList = (yesterdayRes.rows || yesterdayRes.data || [])
// 为退款相关字段设置默认值
yesterdayList = yesterdayList.map(item => ({
...item,
isRefunded: item.isRefunded != null ? item.isRefunded : 0,
isRefundReceived: item.isRefundReceived != null ? item.isRefundReceived : 0,
isRebateReceived: item.isRebateReceived != null ? item.isRebateReceived : 0
}))
this.list = yesterdayList
this.total = yesterdayRes.total || 0
if (this.list.length > 0) {
this.$message.success(`已查询到昨天(${yesterdayStr})的慢单数据`)
} else {
this.$message.warning('昨天也没有慢单数据')
}
}
this.loading = false
} catch (error) {
this.loading = false
this.$message.error('查询失败,请稍后重试')
}
},
/** 判断是否为默认查询(今天的数据) */
isDefaultQuery() {
const today = new Date()
const todayStr = this.formatDate(today)
return this.queryParams.beginTime === todayStr && this.queryParams.endTime === todayStr
},
handleQuery() {
this.prepareQueryParams()
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 50,
remark: undefined,
orderSearch: undefined,
distributionMark: undefined,
modelNumber: undefined,
link: undefined,
buyer: undefined,
address: undefined,
status: undefined,
beginTime: null,
endTime: null,
orderBy: 'create_time',
isAsc: 'desc',
hasFinishTime: false,
isRefunded: undefined,
isRefundReceived: undefined,
isRebateReceived: undefined
}
// 重置排序为默认降序
this.queryParams.orderBy = 'create_time'
this.queryParams.isAsc = 'desc'
// 重置后重新设置默认日期为今天
this.setDefaultDateRange()
this.getListWithFallback()
},
/** 日期范围变化处理 */
handleDateRangeChange(dates) {
if (dates && dates.length === 2) {
this.queryParams.beginTime = dates[0]
this.queryParams.endTime = dates[1]
} else {
this.queryParams.beginTime = null
this.queryParams.endTime = null
}
},
/** 表格排序变化处理 */
handleSortChange(sortInfo) {
if (sortInfo && sortInfo.prop === 'createTime') {
if (sortInfo.order === 'ascending') {
this.queryParams.orderBy = 'create_time'
this.queryParams.isAsc = 'asc'
} else if (sortInfo.order === 'descending') {
this.queryParams.orderBy = 'create_time'
this.queryParams.isAsc = 'desc'
} else {
// 取消排序,恢复默认
this.queryParams.orderBy = 'create_time'
this.queryParams.isAsc = 'desc'
}
// 重新查询数据
this.queryParams.pageNum = 1
this.getList()
}
},
toYuan(n) {
if (n == null || n === '') return ''
const num = Number(n)
if (Number.isNaN(num)) return n
return num.toFixed(2)
},
/** 导出按钮操作 */
handleExport() {
this.prepareQueryParams()
this.download('/system/jdorder/export', this.queryParams, `京东订单数据_${new Date().getTime()}.xlsx`)
},
/** 复制到剪贴板 */
copyToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) {
// 使用现代 Clipboard API
navigator.clipboard.writeText(text).then(() => {
this.$message.success('已复制到剪贴板')
}).catch(() => {
this.fallbackCopyTextToClipboard(text)
})
} else {
// 降级方案
this.fallbackCopyTextToClipboard(text)
}
},
/** 降级复制方案 */
fallbackCopyTextToClipboard(text) {
const textArea = document.createElement('textarea')
textArea.value = text
textArea.style.position = 'fixed'
textArea.style.left = '-999999px'
textArea.style.top = '-999999px'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
try {
const successful = document.execCommand('copy')
if (successful) {
this.$message.success('已复制到剪贴板')
} else {
this.$message.error('复制失败,请手动复制')
}
} catch (err) {
this.$message.error('复制失败,请手动复制')
}
document.body.removeChild(textArea)
},
/** 处理统计开关变化 */
handleCountEnabledChange(row) {
// 调用后端API更新数据库
updateJDOrder(row).then(() => {
this.$message.success(`订单 ${row.remark} 的统计状态已更新为:${row.isCountEnabled ? '参与统计' : '不参与统计'}`)
}).catch(() => {
this.$message.error('更新失败,请稍后重试')
// 恢复原状态
row.isCountEnabled = row.isCountEnabled ? 0 : 1
})
},
/** 切换退款状态(标签点击) */
toggleRefunded(row) {
const oldValue = row.isRefunded
row.isRefunded = row.isRefunded === 1 ? 0 : 1
// 如果设置为"是",自动设置当前日期
if (row.isRefunded === 1 && !row.refundDate) {
row.refundDate = new Date()
}
// 如果设置为"否",清空日期
if (row.isRefunded === 0) {
row.refundDate = null
}
// 调用后端API更新数据库
updateJDOrder(row).then(() => {
this.$message.success(`订单 ${row.remark} 的退款状态已更新`)
}).catch(() => {
this.$message.error('更新失败,请稍后重试')
// 恢复原状态
row.isRefunded = oldValue
if (row.isRefunded === 0) {
row.refundDate = null
}
})
},
/** 处理是否退款开关变化 */
handleRefundedChange(row) {
// 如果设置为"是",自动设置当前日期
if (row.isRefunded === 1 && !row.refundDate) {
row.refundDate = new Date()
}
// 如果设置为"否",清空日期
if (row.isRefunded === 0) {
row.refundDate = null
}
// 调用后端API更新数据库
updateJDOrder(row).then(() => {
this.$message.success(`订单 ${row.remark} 的退款状态已更新`)
}).catch(() => {
this.$message.error('更新失败,请稍后重试')
// 恢复原状态
row.isRefunded = row.isRefunded ? 0 : 1
if (row.isRefunded === 0) {
row.refundDate = null
}
})
},
/** 切换退款到账状态(标签点击) */
toggleRefundReceived(row) {
const oldValue = row.isRefundReceived
row.isRefundReceived = row.isRefundReceived === 1 ? 0 : 1
// 如果设置为"是",自动设置当前日期
if (row.isRefundReceived === 1 && !row.refundReceivedDate) {
row.refundReceivedDate = new Date()
}
// 如果设置为"否",清空日期
if (row.isRefundReceived === 0) {
row.refundReceivedDate = null
}
// 调用后端API更新数据库
updateJDOrder(row).then(() => {
this.$message.success(`订单 ${row.remark} 的退款到账状态已更新`)
}).catch(() => {
this.$message.error('更新失败,请稍后重试')
// 恢复原状态
row.isRefundReceived = oldValue
if (row.isRefundReceived === 0) {
row.refundReceivedDate = null
}
})
},
/** 处理是否退款到账开关变化 */
handleRefundReceivedChange(row) {
// 如果设置为"是",自动设置当前日期
if (row.isRefundReceived === 1 && !row.refundReceivedDate) {
row.refundReceivedDate = new Date()
}
// 如果设置为"否",清空日期
if (row.isRefundReceived === 0) {
row.refundReceivedDate = null
}
// 调用后端API更新数据库
updateJDOrder(row).then(() => {
this.$message.success(`订单 ${row.remark} 的退款到账状态已更新`)
}).catch(() => {
this.$message.error('更新失败,请稍后重试')
// 恢复原状态
row.isRefundReceived = row.isRefundReceived ? 0 : 1
if (row.isRefundReceived === 0) {
row.refundReceivedDate = null
}
})
},
/** 切换后返到账状态(标签点击) */
toggleRebateReceived(row) {
const oldValue = row.isRebateReceived
row.isRebateReceived = row.isRebateReceived === 1 ? 0 : 1
// 如果设置为"是",自动设置当前日期
if (row.isRebateReceived === 1 && !row.rebateReceivedDate) {
row.rebateReceivedDate = new Date()
}
// 如果设置为"否",清空日期
if (row.isRebateReceived === 0) {
row.rebateReceivedDate = null
}
// 调用后端API更新数据库
updateJDOrder(row).then(() => {
this.$message.success(`订单 ${row.remark} 的后返到账状态已更新`)
}).catch(() => {
this.$message.error('更新失败,请稍后重试')
// 恢复原状态
row.isRebateReceived = oldValue
if (row.isRebateReceived === 0) {
row.rebateReceivedDate = null
}
})
},
/** 处理后返到账开关变化 */
handleRebateReceivedChange(row) {
// 如果设置为"是",自动设置当前日期
if (row.isRebateReceived === 1 && !row.rebateReceivedDate) {
row.rebateReceivedDate = new Date()
}
// 如果设置为"否",清空日期
if (row.isRebateReceived === 0) {
row.rebateReceivedDate = null
}
// 调用后端API更新数据库
updateJDOrder(row).then(() => {
this.$message.success(`订单 ${row.remark} 的后返到账状态已更新`)
}).catch(() => {
this.$message.error('更新失败,请稍后重试')
// 恢复原状态
row.isRebateReceived = row.isRebateReceived ? 0 : 1
if (row.isRebateReceived === 0) {
row.rebateReceivedDate = null
}
})
},
/** 删除单条记录(需输入随机确认码) */
async handleDelete(row) {
const code = String(Math.floor(100000 + Math.random() * 900000))
try {
await this.$prompt(`请输入确认码以删除该订单:${code}`, '删除确认', {
confirmButtonText: '删除',
cancelButtonText: '取消',
inputPlaceholder: '请输入上方确认码',
inputValidator: (value) => {
if (value === code) return true
return '确认码不匹配,请重新输入'
},
inputErrorMessage: '确认码不匹配',
type: 'warning',
dangerouslyUseHTMLString: false
})
} catch (e) {
return
}
this.loading = true
try {
await delJDOrder(row.id)
this.$message.success('删除成功')
this.getList()
} catch (e) {
this.$message.error('删除失败,请稍后重试')
} finally {
this.loading = false
}
},
/** 批量同步物流到腾讯文档 */
async handleBatchSyncLogistics() {
try {
// 先检查配置是否完整
const configRes = await getAutoWriteConfig()
if (configRes.code !== 200 || !configRes.data || !configRes.data.isConfigured) {
this.$confirm('检测到尚未完成H-TF自动写入配置是否现在配置', '提示', {
type: 'warning'
}).then(() => {
this.showAutoWriteConfig = true
})
return
}
const config = configRes.data
// 构建确认消息
let confirmMsg = '批量同步将从腾讯文档中搜索订单并填充物流链接\n\n'
confirmMsg += `文档: ${config.fileId}\n`
confirmMsg += `工作表: ${config.sheetId}\n`
confirmMsg += `表头行: ${config.headerRow}\n`
confirmMsg += `数据起始行: ${config.startRow}\n`
if (config.currentProgress) {
confirmMsg += `\n当前进度: 第 ${config.currentProgress}\n`
confirmMsg += config.progressHint + '\n'
}
confirmMsg += `\n防重推送: ${config.skipPushedOrders ? '已启用(跳过已推送订单)' : '已禁用'}\n`
confirmMsg += '\n⚠ 注意:\n'
confirmMsg += '- 系统会自动跳过已有物流链接的行\n'
confirmMsg += '- 系统会自动跳过已标记为"已推送"的订单\n'
confirmMsg += '- 所有操作都会记录到操作日志\n'
// 确认同步
await this.$confirm(confirmMsg, '批量同步物流确认', {
type: 'info',
confirmButtonText: '开始同步',
cancelButtonText: '取消'
})
this.batchSyncLoading = true
// 调用批量同步API
const res = await fillLogisticsByOrderNo({})
if (res.code === 200) {
const data = res.data || {}
this.$notify({
title: '批量同步完成',
message: `✓ 成功填充: ${data.filledCount || 0}\n⊙ 跳过: ${data.skippedCount || 0}\n✗ 错误: ${data.errorCount || 0}\n\n${data.message || ''}`,
type: 'success',
duration: 10000,
dangerouslyUseHTMLString: false
})
} else {
this.$message.error(res.msg || '同步失败')
}
} catch (e) {
if (e !== 'cancel') {
this.$message.error('操作失败:' + (e.message || '未知错误'))
console.error('批量同步失败', e)
}
} finally {
this.batchSyncLoading = false
}
},
/** 打开授权页面并等待授权完成 */
async openAuthAndWait() {
try {
// 获取授权URL
const authUrlRes = await getTencentDocAuthUrl()
if (authUrlRes.code !== 200 || !authUrlRes.data) {
this.$message.error('获取授权URL失败')
return
}
const authUrl = authUrlRes.data
// 打开授权页面
this.$message.info('正在打开授权页面,请完成授权后继续...')
const width = 600
const height = 700
const left = (window.screen.width - width) / 2
const top = (window.screen.height - height) / 2
const authWindow = window.open(
authUrl,
'腾讯文档授权',
`width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`
)
// 监听窗口关闭事件备用方案如果postMessage失效
const checkWindowClosed = setInterval(() => {
if (authWindow.closed) {
clearInterval(checkWindowClosed)
// 等待1秒后检查token状态作为备用验证
setTimeout(async () => {
try {
const tokenStatusRes = await getTokenStatus()
if (tokenStatusRes.data && tokenStatusRes.data.hasToken) {
// 如果已经通过postMessage收到成功消息这里不再重复提示
} else {
// 如果没有收到postMessage可能是授权失败
this.$message.warning('授权未完成,请重新尝试')
}
} catch (e) {
// 静默处理,避免重复提示
}
}, 1000)
}
}, 1000)
} catch (e) {
this.$message.error('打开授权页面失败: ' + (e.message || '未知错误'))
console.error('打开授权页面失败', e)
}
},
/** 检查后端token状态 */
async checkTokenStatus() {
try {
const res = await getTokenStatus()
if (res.code === 200 && res.data) {
this.tokenValid = res.data.hasToken === true
} else {
this.tokenValid = false
}
} catch (e) {
console.error('检查token状态失败', e)
this.tokenValid = false
} finally {
this.tokenStatusChecked = true
}
},
/** 测试获取用户信息接口 */
async handleTestUserInfo() {
try {
this.$message.info('正在测试用户信息接口...')
const res = await testUserInfo()
if (res.code === 200) {
this.$message.success('测试成功!用户信息:' + JSON.stringify(res.data, null, 2))
// 显示详细信息
this.$alert(
'<pre style="text-align: left; max-height: 400px; overflow: auto;">' +
JSON.stringify(res.data, null, 2) +
'</pre>',
'用户信息测试结果',
{
dangerouslyUseHTMLString: true,
confirmButtonText: '确定',
type: 'success'
}
)
} else {
this.$message.error('测试失败:' + (res.msg || '未知错误'))
}
} catch (e) {
this.$message.error('测试失败:' + (e.message || '未知错误'))
console.error('测试用户信息失败', e)
}
},
/** 同步物流链接 */
async handleSyncLogisticsSubmit() {
try {
// 后端会自动从配置中读取 fileId、sheetId、startRow 等参数
// 前端只需传递空对象即可
const res = await fillLogisticsByOrderNo({})
if (res.code === 200) {
const data = res.data || {}
this.$message.success(
`同步成功!成功填充 ${data.filledCount || 0} 条,跳过 ${data.skippedCount || 0} 条,错误 ${data.errorCount || 0}`
)
} else {
this.$message.error(res.msg || '同步失败')
}
} catch (e) {
this.$message.error('同步失败:' + (e.message || '未知错误'))
console.error('同步物流失败', e)
}
},
/** 手动获取物流信息 */
async handleFetchLogistics(row) {
// 检查物流链接
if (!row.logisticsLink || !row.logisticsLink.trim()) {
this.$message.warning('该订单暂无物流链接')
return
}
this.fetchLogisticsDialogVisible = true
this.fetchLogisticsLoading = true
this.fetchLogisticsResult = null
try {
const res = await fetchLogisticsManually({ orderId: row.id })
if (res.code === 200) {
this.fetchLogisticsResult = {
success: true,
...res.data
}
this.$message.success('获取物流信息成功,数据已记录到日志文件')
} else {
this.fetchLogisticsResult = {
success: false,
error: res.msg || '获取失败'
}
this.$message.error(res.msg || '获取物流信息失败')
}
} catch (e) {
this.fetchLogisticsResult = {
success: false,
error: e.message || '请求异常'
}
this.$message.error('获取物流信息失败: ' + (e.message || '未知错误'))
console.error('获取物流信息失败', e)
} finally {
this.fetchLogisticsLoading = false
}
},
/** 复制获取物流信息的结果 */
copyFetchLogisticsResult() {
if (!this.fetchLogisticsResult) return
const resultText = JSON.stringify(this.fetchLogisticsResult, null, 2)
this.copyToClipboard(resultText)
},
/** H-TF订单自动写入配置更新后的回调 */
handleAutoConfigUpdated() {
this.$message.success('H-TF订单自动写入配置已更新')
},
/** 批量标记后返到账(赔付金额>0的订单 */
async handleBatchMarkRebateReceived() {
try {
// 确认操作
await this.$confirm(
'此操作将批量将赔付金额大于0的订单标记为后返到账。\n\n' +
'⚠️ 注意:此操作只应执行一次,用于处理历史数据。\n\n' +
'是否继续?',
'批量标记后返到账',
{
type: 'warning',
confirmButtonText: '确定执行',
cancelButtonText: '取消'
}
)
this.batchMarkLoading = true
const res = await batchMarkRebateReceived()
if (res && (res.code === 200 || res.msg === '操作成功' || res.msg === '查询成功')) {
this.$message.success(res.msg || '批量标记后返到账完成,请查看控制台日志')
// 刷新列表
this.getList()
} else {
this.$message.error(res && res.msg ? res.msg : '批量标记失败')
}
} catch (e) {
if (e !== 'cancel') {
this.$message.error('批量标记失败: ' + (e.message || '未知错误'))
console.error('批量标记后返到账失败', e)
}
} finally {
this.batchMarkLoading = false
}
},
/** 获取订单状态文本 */
getOrderStatusText(status) {
const statusMap = {
'-100': '无变化',
'-1': '未知',
2: '无效-拆单',
3: '无效-取消',
4: '无效-京东帮帮主订单',
5: '无效-账号异常',
6: '无效-赠品类目不返佣',
7: '无效-校园订单',
8: '无效-企业订单',
9: '无效-团购订单',
11: '无效-乡村推广员下单',
13: '违规订单-其他',
14: '无效-来源与备案网址不符',
15: '待付款',
16: '已付款',
17: '已完成',
19: '无效-佣金比例为0',
20: '无效-此复购订单对应的首购订单无效',
21: '无效-云店订单',
22: '无效-PLUS会员佣金比例为0',
23: '无效-支付有礼',
24: '已付定金',
25: '违规订单-流量劫持',
26: '违规订单-流量异常',
27: '违规订单-违反京东平台规则',
28: '违规订单-多笔交易异常',
29: '无效-跨屏跨店',
30: '无效-累计件数超出类目上限',
31: '无效-黑名单sku',
33: '超市卡充值订单',
34: '无效-推卡订单无效'
}
return statusMap[status] || `状态${status}`
},
/** 获取订单状态标签类型 */
getOrderStatusType(status) {
// 取消状态(优先级最高)
if (status === 3) return 'danger' // 无效-取消(红色,优先级高于违规)
// 正常状态
if (status === 16) return 'success' // 已付款
if (status === 17) return 'success' // 已完成
if (status === 15) return 'warning' // 待付款
if (status === 24) return 'warning' // 已付定金
// 违规状态
if ([13, 25, 26, 27, 28].includes(status)) return 'warning' // 违规订单(黄色,优先级低于取消)
// 无效状态
if ([2, 4, 5, 6, 7, 8, 9, 11, 14, 19, 20, 21, 22, 23, 29, 30, 31, 34].includes(status)) return 'info' // 无效订单
if (status === 33) return 'success' // 超市卡充值订单
return 'info' // 其他状态
},
/** 准备查询参数:去除空格并在关键词搜索时清空日期 */
prepareQueryParams() {
const textKeys = ['remark', 'orderSearch', 'distributionMark', 'modelNumber', 'link', 'buyer', 'address', 'status']
let hasKeyword = false
textKeys.forEach(key => {
const value = this.queryParams[key]
if (typeof value === 'string') {
const trimmed = value.trim()
if (trimmed) {
this.queryParams[key] = trimmed
hasKeyword = true
} else {
this.queryParams[key] = undefined
}
}
})
if (hasKeyword) {
this.dateRange = []
this.queryParams.beginTime = null
this.queryParams.endTime = null
}
},
/** 退货复制 */
copyReturnInfo(row) {
const toLine = (v) => (v == null ? '' : String(v).trim())
const parts = []
// 既有顺序保持不变
parts.push(toLine(row.remark)) // 内部单号
parts.push(toLine(row.orderId)) // 订单号
parts.push(toLine(row.modelNumber)) // 型号
parts.push(toLine(row.thirdPartyOrderNo)) // 第三方单号
parts.push(toLine(row.distributionMark)) // 分销标记
parts.push(toLine(row.address)) // 地址
// 新增:金额与物流
parts.push(row.paymentAmount != null && row.paymentAmount !== '' ? this.toYuan(row.paymentAmount) : '') // 付款金额
parts.push(row.rebateAmount != null && row.rebateAmount !== '' ? this.toYuan(row.rebateAmount) : '') // 后返金额
parts.push(toLine(row.logisticsLink)) // 物流链接
// 末尾:下单人
parts.push(toLine(row.buyer)) // 下单人
const text = parts.join('\n')
this.copyToClipboard(text)
},
/** 复制录单格式文本到剪贴板 */
async handleCopyExcelText() {
try {
this.prepareQueryParams()
this.copyExcelTextLoading = true
// 调用后端接口生成Excel格式文本
const res = await generateExcelText(this.queryParams)
if (res.code === 200 && res.data) {
// 复制到剪贴板
const text = res.data
if (text === '暂无订单数据') {
this.$message.warning('暂无订单数据,请调整查询条件')
return
}
this.copyToClipboard(text)
this.$message.success('已复制到剪贴板可以直接粘贴到Excel')
} else {
this.$message.error(res.msg || '生成失败')
}
} catch (e) {
this.$message.error('复制失败:' + (e.message || '未知错误'))
console.error('复制录单格式失败', e)
} finally {
this.copyExcelTextLoading = false
}
},
/** 反向同步第三方单号 */
async handleReverseSyncThirdPartyOrderNo() {
try {
// 先检查配置是否完整
const configRes = await getAutoWriteConfig()
if (configRes.code !== 200 || !configRes.data || !configRes.data.isConfigured) {
this.$confirm('检测到尚未完成H-TF自动写入配置是否现在配置', '提示', {
type: 'warning'
}).then(() => {
this.showAutoWriteConfig = true
})
return
}
const config = configRes.data
// 构建确认消息
let confirmMsg = '反向同步将从腾讯文档第850行开始读取物流单号列\n'
confirmMsg += '通过物流链接匹配本地订单,将腾讯文档的单号列值写入到订单的第三方单号字段\n\n'
confirmMsg += `文档: ${config.fileId}\n`
confirmMsg += `工作表: ${config.sheetId}\n`
confirmMsg += `起始行: 850\n`
confirmMsg += `结束行: 2500\n`
confirmMsg += `处理行数: 1651行\n`
confirmMsg += '\n⚠ 注意:\n'
confirmMsg += '- 只更新订单的第三方单号字段,不会清除任何数据\n'
confirmMsg += '- 跳过物流链接为空的行\n'
confirmMsg += '- 跳过单号为空的行\n'
confirmMsg += '- 如果订单已有第三方单号且与文档中的不同,将跳过\n'
confirmMsg += '- 所有操作都会记录到操作日志\n'
// 确认同步
await this.$confirm(confirmMsg, '反向同步第三方单号确认', {
type: 'info',
confirmButtonText: '开始同步',
cancelButtonText: '取消'
})
this.reverseSyncLoading = true
// 调用反向同步API
const res = await reverseSyncThirdPartyOrderNo({
startRow: 850,
endRow: 2500 // 从850行到2500行
})
if (res.code === 200) {
const data = res.data || {}
this.$notify({
title: '反向同步完成',
message: `✓ 成功: ${data.successCount || 0}\n⊙ 跳过: ${data.skippedCount || 0}\n✗ 错误: ${data.errorCount || 0}\n\n${data.message || ''}`,
type: 'success',
duration: 10000,
dangerouslyUseHTMLString: false
})
// 刷新列表
this.getList()
} else {
this.$message.error(res.msg || '同步失败')
}
} catch (e) {
if (e !== 'cancel') {
this.$message.error('操作失败:' + (e.message || '未知错误'))
console.error('反向同步失败', e)
}
} finally {
this.reverseSyncLoading = false
}
}
}
}
</script>
<style scoped>
/* 优化表格滚动条 */
.order-table ::v-deep .el-table__body-wrapper {
scrollbar-width: thick; /* Firefox */
scrollbar-color: #c1c1c1 #f1f1f1; /* Firefox */
}
.order-table ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
width: 12px;
height: 12px;
}
.order-table ::v-deep .el-table__body-wrapper::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 6px;
}
.order-table ::v-deep .el-table__body-wrapper::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 6px;
border: 2px solid #f1f1f1;
}
.order-table ::v-deep .el-table__body-wrapper::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 优化标签样式 */
.order-table ::v-deep .el-tag {
transition: all 0.3s;
user-select: none;
}
.order-table ::v-deep .el-tag:hover {
opacity: 0.8;
transform: scale(1.05);
}
/* 优化操作按钮布局 */
.order-table ::v-deep .el-table__fixed-right {
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
}
/* 优化表格单元格内容 */
.order-table ::v-deep .el-table td {
padding: 8px 0;
}
.order-table ::v-deep .el-table th {
padding: 10px 0;
background-color: #fafafa;
font-weight: 600;
}
/* 优化固定列 */
.order-table ::v-deep .el-table__fixed-left {
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
}
</style>