Files
ruoyi-vue/src/views/system/jdorder/orderList.vue
2026-04-07 11:05:50 +08:00

3528 lines
133 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 class="jd-order-list-root">
<list-layout>
<!-- 搜索区域 -->
<template #search>
<div class="jd-order-search-panel">
<!-- 移动端搜索表单部分 -->
<mobile-search-form
:model="queryParams"
@search="handleQuery"
@reset="resetQuery"
@quick-search="handleQuickSearch"
>
<template #form="{ expanded }">
<el-form
:model="queryParams"
:inline="true"
class="jd-order-filter-form"
label-width="76px"
label-position="left"
>
<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 label="点过价保">
<el-select v-model="queryParams.isPriceProtected" 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.isInvoiceOpened" 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.isReviewPosted" 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="rebateRemarkScreen"
placeholder="全部"
clearable
size="small"
style="width: 160px;"
@change="onRebateRemarkScreenChange"
>
<el-option label="有导入记录" value="any" />
<el-option label="含异常项" value="abnormal" />
<el-option label="无异常项" value="ok" />
<el-option label="未关联后返表" value="noUploadLink" />
</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-form-item>
</el-form>
</template>
</mobile-search-form>
<!-- 操作按钮区域移动端单独显示 -->
<div class="action-buttons-section mobile-only">
<mobile-button-group
:buttons="actionButtons"
:primary-count="2"
/>
</div>
<!-- 桌面端克制的工具栏低饱和 + plain -->
<div class="desktop-action-buttons desktop-only jd-order-toolbar">
<el-button class="jd-tb-muted" plain size="small" icon="el-icon-setting" @click="showAutoWriteConfig = true" title="配置H-TF订单自动写入腾讯文档">腾峰文档配置</el-button>
<el-button class="jd-tb-muted" plain size="small" icon="el-icon-monitor" @click="showPushMonitor = true" title="查看推送监控和历史记录">推送监控</el-button>
<el-button class="jd-tb-muted" plain size="small" icon="el-icon-user" @click="showTouserConfig = true" title="配置分销标识对应的企业微信接收人">接收人配置</el-button>
<el-button type="primary" plain size="small" icon="el-icon-refresh-right" @click="handleBatchSyncLogistics" :loading="batchSyncLoading" title="批量同步物流链接到腾讯文档">一键发货到腾峰</el-button>
<el-button class="jd-tb-muted" plain size="small" icon="el-icon-check" @click="handleBatchMarkRebateReceived" :loading="batchMarkLoading" title="批量将赔付金额大于0的订单标记为后返到账仅执行一次">批量标记后返到账</el-button>
<el-button class="jd-tb-muted" plain size="small" icon="el-icon-sort" @click="handleReverseSyncThirdPartyOrderNo" :loading="reverseSyncLoading" title="从腾讯文档第850行开始通过物流链接反向匹配订单将腾讯文档的单号列值写入到订单的第三方单号字段">反向同步第三方单号</el-button>
<el-button v-if="!isMobile" type="primary" plain size="small" icon="el-icon-document-copy" @click="handleBatchCopyExcelText" :disabled="selectedRows.length === 0" title="批量复制选中订单的录单格式Excel可粘贴">批量复制录单格式</el-button>
<el-button v-if="!isMobile" type="primary" plain size="small" icon="el-icon-upload2" @click="rebateImportDialogVisible = true" title="可一次选多个 Excel提交后由后台依次导入详情见后返上传记录">导入后返表</el-button>
<el-button v-if="!isMobile" class="jd-tb-muted" plain size="small" icon="el-icon-folder-opened" @click="openRebateUploadRecordDialog" title="查看历史上传的后返表原件并可重新下载">后返上传记录</el-button>
<el-button v-if="!isMobile" class="jd-tb-muted" plain size="small" icon="el-icon-document-copy" @click="handleBatchCopyRebateText" :disabled="selectedRows.length === 0" title="批量复制选中订单的后返录表格式Excel可粘贴">批量复制后返录表</el-button>
<el-button v-if="!isMobile" class="jd-tb-muted" plain size="small" icon="el-icon-document-copy" @click="handleBatchCopySichuanCommerceText" :disabled="selectedRows.length === 0" title="批量复制选中订单的四川商贸录表格式(日期 型号 数量 地址 价格 备注 是否安排 物流)">四川商贸录表</el-button>
<el-button v-if="!isMobile" class="jd-tb-muted" plain size="small" icon="el-icon-refresh" @click="handleBatchRecalcProfit" :disabled="selectedRows.length === 0" title="清除售价/利润手动锁定并按规则重算(依赖型号配置与当前付款、后返)">批量重算利润</el-button>
</div>
</div>
</template>
<!-- 表格区域 -->
<template #table>
<div class="jd-order-table-page">
<!-- 移动端卡片列表 -->
<div v-if="isMobile" class="mobile-order-list" v-loading="loading">
<div class="profit-summary-bar mobile-profit-summary">
<div>统计<b>{{ profitSummaryLabel }}</b></div>
<div>后返合计 <b>{{ toYuan(profitSummaryRebateTotal) }}</b> · 利润合计 <b>{{ profitSummaryProfitTotal === '' ? '—' : toYuan(profitSummaryProfitTotal) }}</b></div>
</div>
<div
v-for="row in list"
:key="row.id"
class="mobile-order-card"
>
<!-- 卡片头部 -->
<div class="card-header">
<div class="header-left">
<div class="order-id">{{ row.remark || row.orderId }}</div>
<el-tag
v-if="row.orderStatus != null"
:type="getOrderStatusType(row.orderStatus)"
size="mini"
style="margin-left: 8px;">
{{ getOrderStatusText(row.orderStatus) }}
</el-tag>
</div>
<div class="header-right">
<el-dropdown
trigger="click"
placement="left"
@command="handleActionCommand"
:popper-class="'mobile-action-dropdown'">
<el-button
type="primary"
size="mini"
icon="el-icon-more"
circle
class="mobile-action-btn">
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
:command="{action: 'copyOrderId', row: row}"
icon="el-icon-copy-document">
复制订单号
</el-dropdown-item>
<el-dropdown-item
v-if="row.thirdPartyOrderNo"
:command="{action: 'copyThirdParty', row: row}"
icon="el-icon-copy-document">
复制第三方单号
</el-dropdown-item>
<el-dropdown-item
:command="{action: 'copyAddress', row: row}"
icon="el-icon-copy-document">
复制地址
</el-dropdown-item>
<el-dropdown-item
v-if="row.logisticsLink"
:command="{action: 'copyLogistics', row: row}"
icon="el-icon-copy-document">
复制物流链接
</el-dropdown-item>
<el-dropdown-item
:command="{action: 'copyReturn', row: row}"
icon="el-icon-document-copy">
退货复制
</el-dropdown-item>
<el-dropdown-item
:command="{action: 'copyExcel', row: row}"
icon="el-icon-document-copy"
:class="{'is-copied': isExcelTextCopied(row.id)}">
{{ isExcelTextCopied(row.id) ? '✓ 录单格式(已复制)' : '录单格式' }}
</el-dropdown-item>
<el-dropdown-item
:command="{action: 'copyRebate', row: row}"
icon="el-icon-document-copy">
后返录表
</el-dropdown-item>
<el-dropdown-item
:command="{action: 'fetchLogistics', row: row}"
icon="el-icon-truck"
style="color: #67C23A;">
获取物流
</el-dropdown-item>
<el-dropdown-item divided>
<div style="display: flex; align-items: center; justify-content: space-between; padding: 0 10px;">
<span>统计</span>
<el-switch
v-model="row.isCountEnabled"
:active-value="1"
:inactive-value="0"
@change="handleCountEnabledChange(row)"
size="mini"
style="margin-left: 10px;">
</el-switch>
</div>
</el-dropdown-item>
<el-dropdown-item
:command="{action: 'delete', row: row}"
icon="el-icon-delete"
divided
style="color: #f56c6c;">
删除
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<!-- 卡片内容 -->
<div class="card-content">
<div class="field-row">
<span class="field-label">订单号</span>
<span class="field-value">{{ row.orderId }}</span>
</div>
<div class="field-row" v-if="row.thirdPartyOrderNo">
<span class="field-label">第三方单号</span>
<span class="field-value">{{ row.thirdPartyOrderNo }}</span>
</div>
<div class="field-row">
<span class="field-label">标记</span>
<span class="field-value">{{ row.distributionMark || '-' }}</span>
</div>
<div class="field-row">
<span class="field-label">型号</span>
<span class="field-value">{{ row.modelNumber || '-' }}</span>
</div>
<div class="field-row">
<span class="field-label">地址</span>
<span class="field-value address-text">{{ row.address || '-' }}</span>
</div>
<div class="field-row">
<span class="field-label">付款金额</span>
<span class="field-value amount">{{ toYuan(row.paymentAmount) }}</span>
</div>
<div class="field-row">
<span class="field-label">后返金额</span>
<span class="field-value amount">{{ toYuan(row.rebateAmount) }}</span>
</div>
<div class="field-row" v-if="row.distributionMark === 'F'">
<span class="field-label">售价渠道</span>
<span class="field-value">
<el-select v-model="row.sellingPriceType" placeholder="渠道" size="mini" clearable style="width: 120px;" @change="onOrderSellingPriceTypeChange(row)">
<el-option label="直款" value="direct" />
<el-option label="闲鱼" value="xianyu" />
</el-select>
</span>
</div>
<div class="field-row" v-if="row.distributionMark === 'F'">
<span class="field-label">售价</span>
<span class="field-value">
<el-input-number v-model="row.sellingPrice" :min="0" :step="1" :precision="2" size="mini" controls-position="right" style="width: 130px;" @change="onOrderSellingPriceChange(row)" />
<el-tag v-if="row.sellingPriceManual === 1" type="info" size="mini" style="margin-left: 6px;">手填</el-tag>
</span>
</div>
<div class="field-row" v-if="row.distributionMark === 'F' || row.distributionMark === 'H-TF'">
<span class="field-label">利润</span>
<span class="field-value">
<el-input-number v-model="row.profit" :step="1" :precision="2" size="mini" controls-position="right" style="width: 130px;" @change="onOrderProfitChange(row)" />
<el-tag v-if="row.profitManual === 1" type="warning" size="mini" style="margin-left: 6px;">手填</el-tag>
</span>
</div>
<div class="field-row" v-else-if="row.profit != null && row.profit !== ''">
<span class="field-label">利润</span>
<span class="field-value amount">{{ toYuan(row.profit) }}</span>
</div>
<div class="field-row" v-if="row.distributionMark === 'F' || row.distributionMark === 'H-TF'">
<span class="field-label">快捷</span>
<span class="field-value">
<el-button v-if="row.distributionMark === 'F'" type="text" size="mini" @click="fillSellingPriceFromConfig(row)">按型号填价</el-button>
<el-button type="text" size="mini" @click="recalcOrderProfitOnly(row)">重算利润</el-button>
</span>
</div>
<div class="field-row" v-if="row.rebateRemarkJson">
<span class="field-label">后返备注</span>
<span class="field-value">
<el-tag v-if="row.rebateRemarkHasAbnormal === 1" type="danger" size="mini">异常</el-tag>
<el-tag v-else-if="row.rebateRemarkHasAbnormal === 0" type="success" size="mini">正常</el-tag>
<span style="margin-left: 6px;">{{ rebateRemarkCount(row) }} 条记录</span>
</span>
</div>
<div class="field-row">
<span class="field-label">下单人</span>
<span class="field-value">{{ row.buyer || '-' }}</span>
</div>
<!-- 退款状态 -->
<div class="field-row refund-status">
<span class="field-label">退款状态</span>
<div class="field-value status-tags">
<el-tag
:type="row.isRefunded === 1 ? 'warning' : 'info'"
size="mini"
@click.native="toggleRefunded(row)"
style="cursor: pointer; margin-right: 8px;">
{{ row.isRefunded === 1 ? '已退款' : '未退款' }}
</el-tag>
<el-tag
:type="row.isRefundReceived === 1 ? 'success' : 'info'"
size="mini"
@click.native="toggleRefundReceived(row)"
style="cursor: pointer; margin-right: 8px;">
{{ row.isRefundReceived === 1 ? '退款到账' : '未到账' }}
</el-tag>
<el-tag
:type="row.isRebateReceived === 1 ? 'success' : 'info'"
size="mini"
@click.native="toggleRebateReceived(row)"
style="cursor: pointer; margin-right: 8px;">
{{ row.isRebateReceived === 1 ? '后返到账' : '未到账' }}
</el-tag>
<el-tag
:type="row.isPriceProtected === 1 ? 'warning' : 'info'"
size="mini"
@click.native="togglePriceProtected(row)"
style="cursor: pointer; margin-right: 8px;">
{{ row.isPriceProtected === 1 ? '点过价保' : '未点价保' }}
</el-tag>
<el-tag
:type="row.isInvoiceOpened === 1 ? 'success' : 'info'"
size="mini"
@click.native="toggleInvoiceOpened(row)"
style="cursor: pointer; margin-right: 8px;">
{{ row.isInvoiceOpened === 1 ? '开过专票' : '未开专票' }}
</el-tag>
<el-tag
:type="row.isReviewPosted === 1 ? 'success' : 'info'"
size="mini"
@click.native="toggleReviewPosted(row)"
style="cursor: pointer;">
{{ row.isReviewPosted === 1 ? '晒过评价' : '未晒评价' }}
</el-tag>
</div>
</div>
<div class="field-row">
<span class="field-label">创建时间</span>
<span class="field-value">{{ parseTime(row.createTime) }}</span>
</div>
<div class="field-row" v-if="row.finishTime">
<span class="field-label">完成时间</span>
<span class="field-value">{{ parseTime(row.finishTime) }}</span>
</div>
<div class="field-row" v-if="row.status">
<span class="field-label">备注/状态</span>
<span class="field-value">{{ row.status }}</span>
</div>
</div>
</div>
<el-empty v-if="!loading && list.length === 0" description="暂无数据" :image-size="100" />
</div>
<!-- 桌面端统计条 + 表格height 固定表头滚动条在表体 -->
<div v-else class="jd-order-table-wrap">
<div ref="profitSummaryBar" class="profit-summary-bar sticky-summary">
<span class="profit-summary-item">统计范围<b>{{ profitSummaryLabel }}</b></span>
<span class="profit-summary-item">后返合计<b>{{ toYuan(profitSummaryRebateTotal) }}</b></span>
<span class="profit-summary-item">利润合计<b>{{ profitSummaryProfitTotal === '' ? '—' : toYuan(profitSummaryProfitTotal) }}</b></span>
</div>
<el-table
:data="list"
v-loading="loading"
border
stripe
height="100%"
:default-sort="{prop: 'createTime', order: 'descending'}"
@sort-change="handleSortChange"
@selection-change="handleSelectionChange"
style="width: 100%;"
class="order-table jd-order-el-table">
<!-- 多选列仅桌面端显示 -->
<el-table-column v-if="!isMobile" type="selection" width="55" fixed="left" align="center"/>
<!-- 核心信息列 -->
<el-table-column label="内部单号" prop="remark" width="120" sortable :fixed="isMobile ? false : 'left'"/>
<el-table-column label="订单号" prop="orderId" width="160"/>
<el-table-column label="第三方单号" prop="thirdPartyOrderNo" width="140">
<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="56" align="center"/>
<el-table-column label="型号" prop="modelNumber" width="140"/>
<el-table-column label="地址" prop="address" min-width="220" show-overflow-tooltip class-name="address-cell">
<template slot-scope="scope">
<span style="font-weight: bold; display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ scope.row.address }}</span>
</template>
</el-table-column>
<!-- 金额信息列 -->
<el-table-column label="付款金额" prop="paymentAmount" width="90" align="right">
<template slot-scope="scope">{{ toYuan(scope.row.paymentAmount) }}</template>
</el-table-column>
<el-table-column label="后返金额" prop="rebateAmount" width="90" align="right">
<template slot-scope="scope">{{ toYuan(scope.row.rebateAmount) }}</template>
</el-table-column>
<el-table-column label="售价渠道" prop="sellingPriceType" width="96" align="center">
<template slot-scope="scope">
<template v-if="scope.row.distributionMark === 'F'">
<el-select v-model="scope.row.sellingPriceType" placeholder="—" size="mini" clearable style="width: 88px;" @change="onOrderSellingPriceTypeChange(scope.row)">
<el-option label="直款" value="direct" />
<el-option label="闲鱼" value="xianyu" />
</el-select>
</template>
<span v-else style="color: #c0c4cc;"></span>
</template>
</el-table-column>
<el-table-column label="售价" prop="sellingPrice" width="128" align="right">
<template slot-scope="scope">
<template v-if="scope.row.distributionMark === 'F'">
<el-input-number v-model="scope.row.sellingPrice" :min="0" :step="1" :precision="2" size="mini" controls-position="right" class="jd-order-input-money" @change="onOrderSellingPriceChange(scope.row)" />
</template>
<span v-else>{{ scope.row.sellingPrice != null ? toYuan(scope.row.sellingPrice) : '' }}</span>
</template>
</el-table-column>
<el-table-column label="利润" prop="profit" width="128" align="right">
<template slot-scope="scope">
<template v-if="scope.row.distributionMark === 'F' || scope.row.distributionMark === 'H-TF'">
<el-input-number v-model="scope.row.profit" :step="1" :precision="2" size="mini" controls-position="right" class="jd-order-input-money" @change="onOrderProfitChange(scope.row)" />
</template>
<span v-else>{{ scope.row.profit != null ? toYuan(scope.row.profit) : '' }}</span>
</template>
</el-table-column>
<el-table-column label="快捷操作" width="90" align="center">
<template slot-scope="scope">
<template v-if="scope.row.distributionMark === 'F'">
<el-button type="text" size="mini" @click="fillSellingPriceFromConfig(scope.row)">填价</el-button>
<el-button type="text" size="mini" @click="recalcOrderProfitOnly(scope.row)">重算</el-button>
</template>
<template v-else-if="scope.row.distributionMark === 'H-TF'">
<el-button type="text" size="mini" @click="recalcOrderProfitOnly(scope.row)">重算</el-button>
</template>
<span v-else style="color: #c0c4cc;"></span>
</template>
</el-table-column>
<el-table-column label="后返备注" prop="rebateRemarkJson" min-width="160" show-overflow-tooltip>
<template slot-scope="scope">
<template v-if="!scope.row.rebateRemarkJson">
<span style="color: #c0c4cc;">-</span>
</template>
<template v-else>
<el-tag v-if="scope.row.rebateRemarkHasAbnormal === 1" type="danger" size="mini">异常</el-tag>
<el-tag v-else-if="scope.row.rebateRemarkHasAbnormal === 0" type="success" size="mini">正常</el-tag>
<el-popover trigger="click" placement="left" width="420">
<div class="rebate-remark-popover">
<div
v-for="(it, idx) in parseRebateRemarks(scope.row)"
:key="idx"
class="rebate-remark-item"
>
<div class="rebate-remark-title">
<b>{{ it.documentTitle || '(未命名)' }}</b>
<span class="rebate-remark-time">{{ formatRebateRemarkTime(it.uploadTime) }}</span>
</div>
<div>是否返现{{ it.whetherRebate !== undefined && it.whetherRebate !== '' ? it.whetherRebate : '(空)' }}
<el-tag v-if="it.abnormal" type="danger" size="mini">异常</el-tag>
<el-tag v-else type="success" size="mini">正常</el-tag>
</div>
<div>返现金额{{ it.rebateAmount !== undefined && it.rebateAmount !== '' ? it.rebateAmount : '-' }}</div>
</div>
</div>
<el-button slot="reference" type="text" size="small">明细{{ rebateRemarkCount(scope.row) }}</el-button>
</el-popover>
</template>
</template>
</el-table-column>
<el-table-column label="备注/状态" prop="status" min-width="100" show-overflow-tooltip/>
<el-table-column label="下单人" prop="buyer" width="80"/>
<!-- 退款状态更早版本为单列纵向标签列表非六列拆分 -->
<el-table-column label="退款状态" min-width="248" align="left" class-name="refund-status-column">
<template slot-scope="scope">
<div class="refund-status-stack">
<div class="refund-status-col">
<div class="refund-status-row">
<span class="refund-status-label">是否退款</span>
<el-tag
:type="scope.row.isRefunded === 1 ? 'warning' : 'info'"
size="mini"
effect="plain"
:title="scope.row.isRefunded === 1 && scope.row.refundDate ? '退款日期:' + parseTime(scope.row.refundDate) : ''"
@click.native="toggleRefunded(scope.row)"
class="refund-status-tag">
{{ scope.row.isRefunded === 1 ? '已退款' : '未退款' }}
</el-tag>
</div>
<div class="refund-status-row">
<span class="refund-status-label">退款到账</span>
<el-tag
:type="scope.row.isRefundReceived === 1 ? 'success' : 'info'"
size="mini"
effect="plain"
:title="scope.row.isRefundReceived === 1 && scope.row.refundReceivedDate ? '退款到账日期:' + parseTime(scope.row.refundReceivedDate) : ''"
@click.native="toggleRefundReceived(scope.row)"
class="refund-status-tag">
{{ scope.row.isRefundReceived === 1 ? '已到账' : '未到账' }}
</el-tag>
</div>
<div class="refund-status-row">
<span class="refund-status-label">后返到账</span>
<el-tag
:type="scope.row.isRebateReceived === 1 ? 'success' : 'info'"
size="mini"
effect="plain"
:title="scope.row.isRebateReceived === 1 && scope.row.rebateReceivedDate ? '后返到账日期:' + parseTime(scope.row.rebateReceivedDate) : ''"
@click.native="toggleRebateReceived(scope.row)"
class="refund-status-tag">
{{ scope.row.isRebateReceived === 1 ? '已到账' : '未到账' }}
</el-tag>
</div>
</div>
<div class="refund-status-col">
<div class="refund-status-row">
<span class="refund-status-label">价保</span>
<el-tag
:type="scope.row.isPriceProtected === 1 ? 'warning' : 'info'"
size="mini"
effect="plain"
:title="scope.row.isPriceProtected === 1 && scope.row.priceProtectedDate ? '价保日期:' + parseTime(scope.row.priceProtectedDate) : ''"
@click.native="togglePriceProtected(scope.row)"
class="refund-status-tag">
{{ scope.row.isPriceProtected === 1 ? '已点' : '未点' }}
</el-tag>
</div>
<div class="refund-status-row">
<span class="refund-status-label">专票</span>
<el-tag
:type="scope.row.isInvoiceOpened === 1 ? 'success' : 'info'"
size="mini"
effect="plain"
:title="scope.row.isInvoiceOpened === 1 && scope.row.invoiceOpenedDate ? '开票日期:' + parseTime(scope.row.invoiceOpenedDate) : ''"
@click.native="toggleInvoiceOpened(scope.row)"
class="refund-status-tag">
{{ scope.row.isInvoiceOpened === 1 ? '已开' : '未开' }}
</el-tag>
</div>
<div class="refund-status-row">
<span class="refund-status-label">评价</span>
<el-tag
:type="scope.row.isReviewPosted === 1 ? 'success' : 'info'"
size="mini"
effect="plain"
:title="scope.row.isReviewPosted === 1 && scope.row.reviewPostedDate ? '评价日期:' + parseTime(scope.row.reviewPostedDate) : ''"
@click.native="toggleReviewPosted(scope.row)"
class="refund-status-tag">
{{ scope.row.isReviewPosted === 1 ? '已晒' : '未晒' }}
</el-tag>
</div>
</div>
</div>
</template>
</el-table-column>
<!-- 时间信息列 -->
<el-table-column label="创建时间" prop="createTime" width="140" sortable="custom">
<template slot-scope="scope">{{ parseTime(scope.row.createTime) }}</template>
</el-table-column>
<el-table-column label="完成时间" prop="finishTime" width="130">
<template slot-scope="scope">{{ parseTime(scope.row.finishTime) || '-' }}</template>
</el-table-column>
<!-- 其他信息列可折叠 -->
<el-table-column label="订单状态" prop="orderStatus" width="80" align="center">
<template slot-scope="scope">
<el-tag
v-if="scope.row.orderStatus != null"
:type="getOrderStatusType(scope.row.orderStatus)"
size="small">
{{ getOrderStatusTextShort(scope.row.orderStatus) }}
</el-tag>
<span v-else style="color: #999;">-</span>
</template>
</el-table-column>
<!-- 操作列统一放在最右侧 -->
<el-table-column label="操作" fixed="right" :width="isMobile ? 60 : 288" align="center" class-name="action-column">
<template slot-scope="scope">
<!-- 移动端悬浮操作按钮 -->
<div v-if="isMobile" class="mobile-action-wrapper">
<el-dropdown
trigger="click"
placement="left"
@command="handleActionCommand"
:popper-class="'mobile-action-dropdown'">
<el-button
type="primary"
size="mini"
icon="el-icon-more"
circle
class="mobile-action-btn">
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
:command="{action: 'copyOrderId', row: scope.row}"
icon="el-icon-copy-document">
复制订单号
</el-dropdown-item>
<el-dropdown-item
v-if="scope.row.thirdPartyOrderNo"
:command="{action: 'copyThirdParty', row: scope.row}"
icon="el-icon-copy-document">
复制第三方单号
</el-dropdown-item>
<el-dropdown-item
:command="{action: 'copyAddress', row: scope.row}"
icon="el-icon-copy-document">
复制地址
</el-dropdown-item>
<el-dropdown-item
v-if="scope.row.logisticsLink"
:command="{action: 'copyLogistics', row: scope.row}"
icon="el-icon-copy-document">
复制物流链接
</el-dropdown-item>
<el-dropdown-item
:command="{action: 'copyReturn', row: scope.row}"
icon="el-icon-document-copy">
退货复制
</el-dropdown-item>
<el-dropdown-item
:command="{action: 'copyExcel', row: scope.row}"
icon="el-icon-document-copy"
:class="{'is-copied': isExcelTextCopied(scope.row.id)}">
{{ isExcelTextCopied(scope.row.id) ? '✓ 录单格式(已复制)' : '录单格式' }}
</el-dropdown-item>
<el-dropdown-item
:command="{action: 'copyRebate', row: scope.row}"
icon="el-icon-document-copy">
后返录表
</el-dropdown-item>
<el-dropdown-item
:command="{action: 'fetchLogistics', row: scope.row}"
icon="el-icon-truck"
style="color: #67C23A;">
获取物流
</el-dropdown-item>
<el-dropdown-item divided>
<div style="display: flex; align-items: center; justify-content: space-between; padding: 0 10px;">
<span>统计</span>
<el-switch
v-model="scope.row.isCountEnabled"
:active-value="1"
:inactive-value="0"
@change="handleCountEnabledChange(scope.row)"
size="mini"
style="margin-left: 10px;">
</el-switch>
</div>
</el-dropdown-item>
<el-dropdown-item
:command="{action: 'delete', row: scope.row}"
icon="el-icon-delete"
divided
style="color: #f56c6c;">
删除
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<!-- 桌面端分行 + 轻量链接样式降低杂乱感 -->
<div v-else class="desktop-action-buttons-wrapper jd-action-cell">
<div class="jd-action-row jd-action-row--copy">
<el-button type="text" size="mini" class="jd-act-link" icon="el-icon-copy-document" @click="copyToClipboard(scope.row.orderId)" title="复制订单号">单号</el-button>
<el-button v-if="scope.row.thirdPartyOrderNo" type="text" size="mini" class="jd-act-link" icon="el-icon-copy-document" @click="copyToClipboard(scope.row.thirdPartyOrderNo)" title="复制第三方单号">三方</el-button>
<el-button type="text" size="mini" class="jd-act-link" icon="el-icon-copy-document" @click="copyToClipboard(scope.row.address)" title="复制地址">地址</el-button>
<el-button v-if="scope.row.logisticsLink" type="text" size="mini" class="jd-act-link" icon="el-icon-copy-document" @click="copyToClipboard(scope.row.logisticsLink)" title="复制物流链接">物流</el-button>
<el-button type="text" size="mini" class="jd-act-link" @click="copyReturnInfo(scope.row)" title="复制退货信息">退货</el-button>
</div>
<div class="jd-action-row jd-action-row--doc">
<el-button type="text" size="mini" class="jd-act-link jd-act-link--accent" :class="{ 'jd-act-done': isExcelTextCopied(scope.row.id) }" icon="el-icon-document-copy" @click="copySingleOrderExcelText(scope.row)" :title="isExcelTextCopied(scope.row.id) ? '已复制录单格式' : '复制录单格式'">录单</el-button>
<el-button type="text" size="mini" class="jd-act-link jd-act-link--accent" icon="el-icon-document-copy" @click="copyRebateRecordText(scope.row)" title="复制后返录表">后返</el-button>
<el-button type="text" size="mini" class="jd-act-link jd-act-link--success" @click="handleFetchLogistics(scope.row)" title="获取物流">物流拉取</el-button>
</div>
<div class="jd-action-row jd-action-row--meta">
<el-switch
v-model="scope.row.isCountEnabled"
:active-value="1"
:inactive-value="0"
@change="handleCountEnabledChange(scope.row)"
active-text="统计"
inactive-text=""
size="mini" />
<el-button type="text" size="mini" class="jd-act-link jd-act-link--danger" @click="handleDelete(scope.row)" title="删除订单">删除</el-button>
</div>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<!-- 分页区域 -->
<template #pagination>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="handleMainPagination"
class="list-pagination"
/>
</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" />
<!-- 分销标识接收人配置 -->
<distribution-mark-touser-config v-model="showTouserConfig" @config-updated="handleTouserConfigUpdated" />
<!-- 导入跟团返现 / 后返 Excel支持多文件一次提交后台依次处理 -->
<el-dialog
title="导入后返表(跟团返现等)"
:visible.sync="rebateImportDialogVisible"
width="520px"
append-to-body
@closed="onRebateImportDialogClosed"
>
<p style="color: #909399; font-size: 13px; margin: 0 0 12px;">
表头需包含单号订单号是否返现返现金额列优先总共返现可选<strong>多个</strong> Excel提交后台处理后由服务端依次导入不弹逐条结果请到后返上传记录查看
</p>
<el-form label-width="88px" size="small">
<el-form-item label="文档标题">
<el-input
v-model="rebateImportTitle"
placeholder="仅选 1 个文件时可用;多文件时各文件用各自文件名"
clearable
/>
</el-form-item>
<el-form-item label="Excel">
<el-upload
ref="rebateExcelUploader"
drag
multiple
:show-file-list="true"
:limit="30"
:auto-upload="false"
accept=".xlsx,.xls"
:disabled="rebateImportLoading"
:on-exceed="onRebateExcelExceed"
>
<i class="el-icon-upload" />
<div class="el-upload__text">拖到此处<em>点击选择</em>可多选</div>
<div slot="tip" class="el-upload__tip">单次最多 30 个文件仅解析每个文件的第一个工作表</div>
</el-upload>
<el-button
type="primary"
size="small"
:loading="rebateImportLoading"
style="margin-top: 10px"
@click="submitRebateExcelBatch"
>提交后台处理</el-button>
</el-form-item>
</el-form>
</el-dialog>
<!-- 后返表上传记录含重新下载 -->
<el-dialog
title="后返表上传记录"
:visible.sync="rebateUploadRecordDialogVisible"
width="960px"
append-to-body
@open="fetchRebateUploadList"
>
<el-table v-loading="rebateUploadLoading" :data="rebateUploadList" border size="small" max-height="420">
<el-table-column label="ID" prop="id" width="70" />
<el-table-column label="文档标题" prop="documentTitle" min-width="140" show-overflow-tooltip />
<el-table-column label="原始文件名" prop="originalFilename" min-width="160" show-overflow-tooltip />
<el-table-column label="大小" width="88" align="right">
<template slot-scope="scope">{{ formatUploadFileSize(scope.row.fileSize) }}</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.importStatus === 2" type="success" size="mini">已处理</el-tag>
<el-tag v-else type="info" size="mini">未成功</el-tag>
</template>
</el-table-column>
<el-table-column label="数据行" prop="dataRows" width="72" align="center" />
<el-table-column label="更新订单" prop="updatedOrders" width="88" align="center" />
<el-table-column label="未匹配" prop="notFoundCount" width="72" align="center" />
<el-table-column label="上传人" prop="createBy" width="100" show-overflow-tooltip />
<el-table-column label="时间" prop="createTime" width="155" />
<el-table-column label="操作" width="148" align="center" fixed="right">
<template slot-scope="scope">
<el-button
type="text"
size="small"
:disabled="!scope.row.filePath"
@click="downloadRebateUploadFile(scope.row)"
>下载</el-button>
<el-button
type="text"
size="small"
@click="confirmDeleteRebateUpload(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="rebateUploadTotal > 0"
:total="rebateUploadTotal"
:page.sync="rebateUploadQuery.pageNum"
:limit.sync="rebateUploadQuery.pageSize"
@pagination="fetchRebateUploadList"
/>
</el-dialog>
</div>
</template>
<script>
import { listJDOrders, getJDOrder, updateJDOrder, delJDOrder, fetchLogisticsManually, batchMarkRebateReceived, generateExcelText, importGroupRebateExcelBatch, listGroupRebateExcelUploads, deleteGroupRebateUpload, recalcProfitBatch, syncAutoProfitBatch } from '@/api/system/jdorder'
import { fillLogisticsByOrderNo, getTokenStatus, getTencentDocAuthUrl, testUserInfo, getAutoWriteConfig, reverseSyncThirdPartyOrderNo } from '@/api/jarvis/tendoc'
import { mapGetters } from 'vuex'
import ListLayout from '@/components/ListLayout'
import MobileSearchForm from '@/components/MobileSearchForm'
import MobileButtonGroup from '@/components/MobileButtonGroup'
import TencentDocAutoWriteConfig from './components/TencentDocAutoWriteConfig'
import TencentDocPushMonitor from './components/TencentDocPushMonitor'
import DistributionMarkTouserConfig from './components/DistributionMarkTouserConfig'
export default {
name: 'JDOrderList',
components: {
ListLayout,
TencentDocAutoWriteConfig,
TencentDocPushMonitor,
DistributionMarkTouserConfig,
MobileSearchForm,
MobileButtonGroup
},
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,
isPriceProtected: undefined,
isInvoiceOpened: undefined,
isReviewPosted: undefined,
hasRebateRemark: undefined,
rebateRemarkHasAbnormal: undefined,
rebateWithoutUploadLink: undefined
},
rebateRemarkScreen: undefined,
rebateImportDialogVisible: false,
rebateImportTitle: '',
rebateImportLoading: false,
rebateUploadRecordDialogVisible: false,
rebateUploadLoading: false,
rebateUploadList: [],
rebateUploadTotal: 0,
rebateUploadQuery: {
pageNum: 1,
pageSize: 10
},
// 同步物流对话框
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,
showTouserConfig: false,
// 批量同步loading状态
batchSyncLoading: false,
// 批量标记后返到账loading状态
batchMarkLoading: false,
// 反向同步第三方单号loading状态
reverseSyncLoading: false,
// 获取物流信息对话框
fetchLogisticsDialogVisible: false,
fetchLogisticsLoading: false,
fetchLogisticsResult: null,
// 复制录单格式loading状态
copyExcelTextLoading: false,
// 已复制录单格式的订单ID集合页面级缓存刷新后消失
copiedExcelTextOrderIds: new Set(),
// 选中的行数据
selectedRows: [],
// 列表加载后自动同步利润(防快速翻页乱序)
syncAutoProfitSeq: 0
}
},
computed: {
...mapGetters(['device']),
isMobile() {
// 只在移动端返回true桌面端严格返回false
if (this.device === 'mobile') {
return true
}
// 如果device不是mobile检查窗口宽度用于响应式
if (typeof window !== 'undefined' && window.innerWidth < 768) {
return true
}
return false
},
profitSummaryRows() {
return this.selectedRows && this.selectedRows.length > 0 ? this.selectedRows : this.list
},
profitSummaryLabel() {
return this.selectedRows && this.selectedRows.length > 0
? `已选 ${this.selectedRows.length}`
: `本页 ${this.list.length}`
},
profitSummaryRebateTotal() {
return this.profitSummaryRows.reduce((s, r) => s + (Number(r.rebateAmount) || 0), 0)
},
profitSummaryProfitTotal() {
let has = false
const t = this.profitSummaryRows.reduce((s, r) => {
if (r.profit == null || r.profit === '') return s
has = true
return s + (Number(r.profit) || 0)
}, 0)
return has ? t : ''
},
actionButtons() {
// 移动端只保留推送监控按钮
if (this.isMobile) {
return [
{ key: 'monitor', label: '推送监控', type: 'success', icon: 'el-icon-monitor', handler: () => { this.showPushMonitor = true } }
]
}
// 桌面端显示所有按钮
return [
{ key: 'export', label: '导出', type: 'success', icon: 'el-icon-download', handler: () => this.handleExport() },
{ key: 'config', label: '腾峰文档配置', type: 'warning', icon: 'el-icon-setting', handler: () => { this.showAutoWriteConfig = true } },
{ key: 'monitor', label: '推送监控', type: 'success', icon: 'el-icon-monitor', handler: () => { this.showPushMonitor = true } },
{ key: 'touser', label: '接收人配置', type: 'warning', icon: 'el-icon-user', handler: () => { this.showTouserConfig = true } },
// { key: 'sync', label: '一键发货到腾峰', type: 'warning', icon: 'el-icon-refresh-right', handler: () => this.handleBatchSyncLogistics(), loading: this.batchSyncLoading },
// { key: 'mark', label: '批量标记后返到账', type: 'warning', icon: 'el-icon-check', handler: () => this.handleBatchMarkRebateReceived(), loading: this.batchMarkLoading },
// { key: 'reverse', label: '反向同步第三方单号', type: 'warning', icon: 'el-icon-sort', handler: () => this.handleReverseSyncThirdPartyOrderNo(), loading: this.reverseSyncLoading }
]
}
},
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: {
handleMainPagination() {
this.getList()
this.$nextTick(() => {
const el = this.$refs.profitSummaryBar
if (el && typeof el.scrollIntoView === 'function') {
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
} else if (typeof window !== 'undefined') {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
})
},
/** 处理移动端操作菜单命令 */
handleActionCommand({ action, row }) {
switch (action) {
case 'copyOrderId':
this.copyToClipboard(row.orderId)
break
case 'copyThirdParty':
this.copyToClipboard(row.thirdPartyOrderNo)
break
case 'copyAddress':
this.copyToClipboard(row.address)
break
case 'copyLogistics':
this.copyToClipboard(row.logisticsLink)
break
case 'copyReturn':
this.copyReturnInfo(row)
break
case 'copyExcel':
this.copySingleOrderExcelText(row)
break
case 'copyRebate':
this.copyRebateRecordText(row)
break
case 'fetchLogistics':
this.handleFetchLogistics(row)
break
case 'delete':
this.handleDelete(row)
break
}
},
/** 快速搜索 */
handleQuickSearch(keyword) {
if (!keyword) {
this.handleQuery()
return
}
// 快速搜索:在订单号、备注、下单人等字段中搜索
this.queryParams.orderSearch = keyword
this.queryParams.remark = keyword
this.queryParams.buyer = keyword
this.handleQuery()
},
/** 设置默认日期范围为今天 */
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}`
},
/** 列表行默认值(退款开关、利润手填标记等) */
normalizeOrderListItem(item) {
return {
...item,
isRefunded: item.isRefunded != null ? item.isRefunded : 0,
isRefundReceived: item.isRefundReceived != null ? item.isRefundReceived : 0,
isRebateReceived: item.isRebateReceived != null ? item.isRebateReceived : 0,
isPriceProtected: item.isPriceProtected != null ? item.isPriceProtected : 0,
isInvoiceOpened: item.isInvoiceOpened != null ? item.isInvoiceOpened : 0,
isReviewPosted: item.isReviewPosted != null ? item.isReviewPosted : 0,
sellingPriceManual: item.sellingPriceManual != null ? item.sellingPriceManual : 0,
profitManual: item.profitManual != null ? item.profitManual : 0
}
},
assignListFromResponse(res) {
const list = (res.rows || res.data || [])
this.list = list.map(item => this.normalizeOrderListItem(item))
this.total = res.total || 0
},
/** 本页数据与库中规则对齐:仅未锁定利润的订单可能写库;有更新则静默拉一次列表(不再递归同步) */
runSyncAutoProfitAfterListLoad() {
const ids = this.list.map(r => r.id).filter(id => id != null)
if (!ids.length) return
const seq = ++this.syncAutoProfitSeq
if (this._syncAutoProfitTimer) clearTimeout(this._syncAutoProfitTimer)
this._syncAutoProfitTimer = setTimeout(() => {
syncAutoProfitBatch(ids)
.then(res => {
if (seq !== this.syncAutoProfitSeq) return
const u = res && res.data && typeof res.data.updated === 'number' ? res.data.updated : 0
if (u <= 0) return
return listJDOrders(this.queryParams).then(r2 => {
if (seq !== this.syncAutoProfitSeq) return
this.assignListFromResponse(r2)
})
})
.catch(() => {})
}, 120)
},
getList() {
this.loading = true
listJDOrders(this.queryParams).then(res => {
this.assignListFromResponse(res)
this.loading = false
this.runSyncAutoProfitAfterListLoad()
}).catch(() => { this.loading = false })
},
/** 智能查询列表,如果今天数据为空则查询昨天 */
async getListWithFallback() {
this.loading = true
try {
const res = await listJDOrders(this.queryParams)
this.assignListFromResponse(res)
// 如果今天的数据为空,且是默认查询(没有手动选择日期),则尝试查询昨天
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)
this.assignListFromResponse(yesterdayRes)
if (this.list.length > 0) {
this.$message.success(`已查询到昨天(${yesterdayStr})的慢单数据`)
} else {
this.$message.warning('昨天也没有慢单数据')
}
}
this.loading = false
this.runSyncAutoProfitAfterListLoad()
} 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()
},
/** 后返备注列表筛选 */
onRebateRemarkScreenChange(v) {
this.queryParams.hasRebateRemark = undefined
this.queryParams.rebateRemarkHasAbnormal = undefined
this.queryParams.rebateWithoutUploadLink = undefined
if (v === 'any') {
this.queryParams.hasRebateRemark = true
} else if (v === 'abnormal') {
this.queryParams.rebateRemarkHasAbnormal = 1
} else if (v === 'ok') {
this.queryParams.hasRebateRemark = true
this.queryParams.rebateRemarkHasAbnormal = 0
} else if (v === 'noUploadLink') {
this.queryParams.rebateWithoutUploadLink = true
}
this.queryParams.pageNum = 1
this.prepareQueryParams()
this.getList()
},
parseRebateRemarks(row) {
if (!row || !row.rebateRemarkJson) return []
try {
const arr = JSON.parse(row.rebateRemarkJson)
return Array.isArray(arr) ? arr : []
} catch (e) {
return []
}
},
rebateRemarkCount(row) {
return this.parseRebateRemarks(row).length
},
formatRebateRemarkTime(ts) {
if (ts == null || ts === '') return ''
const d = new Date(typeof ts === 'number' ? ts : Number(ts))
if (Number.isNaN(d.getTime())) return ''
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const h = String(d.getHours()).padStart(2, '0')
const min = String(d.getMinutes()).padStart(2, '0')
return `${y}-${m}-${day} ${h}:${min}`
},
clearRebateExcelUploaderFiles() {
this.$nextTick(() => {
const u = this.$refs.rebateExcelUploader
if (u && typeof u.clearFiles === 'function') {
u.clearFiles()
}
})
},
onRebateImportDialogClosed() {
this.rebateImportTitle = ''
this.clearRebateExcelUploaderFiles()
},
onRebateExcelExceed() {
this.$message.warning('单次最多选择 30 个文件')
},
submitRebateExcelBatch() {
const u = this.$refs.rebateExcelUploader
const uploadFiles = (u && u.uploadFiles) ? u.uploadFiles : []
const files = uploadFiles.map((f) => f.raw).filter(Boolean)
if (!files.length) {
this.$message.warning('请先选择 Excel 文件')
return
}
const formData = new FormData()
files.forEach((file) => formData.append('files', file))
const title = (this.rebateImportTitle || '').trim()
if (files.length === 1 && title) {
formData.append('documentTitle', title)
}
this.rebateImportLoading = true
importGroupRebateExcelBatch(formData)
.then((res) => {
const data = res.data || {}
const msg = data.message || res.msg || '已处理'
if (data.failCount > 0) {
this.$message.warning(msg)
} else {
this.$message.success(msg)
}
this.clearRebateExcelUploaderFiles()
this.rebateImportDialogVisible = false
this.getList()
})
.catch((e) => {
this.$message.error(e.message || '提交失败')
})
.finally(() => {
this.rebateImportLoading = false
})
},
openRebateUploadRecordDialog() {
this.rebateUploadQuery.pageNum = 1
this.rebateUploadRecordDialogVisible = true
},
fetchRebateUploadList() {
this.rebateUploadLoading = true
listGroupRebateExcelUploads(this.rebateUploadQuery).then(res => {
this.rebateUploadList = res.rows || []
this.rebateUploadTotal = res.total || 0
}).finally(() => {
this.rebateUploadLoading = false
})
},
formatUploadFileSize(n) {
if (n == null || n === '') return '-'
const x = Number(n)
if (Number.isNaN(x) || x < 0) return '-'
if (x < 1024) return `${x} B`
if (x < 1024 * 1024) return `${(x / 1024).toFixed(1)} KB`
return `${(x / 1024 / 1024).toFixed(2)} MB`
},
downloadRebateUploadFile(row) {
const name = row.originalFilename || `rebate_upload_${row.id}.xlsx`
this.download(`/system/jdorder/groupRebateUpload/download/${row.id}`, {}, name)
},
confirmDeleteRebateUpload(row) {
const title = row.documentTitle || row.originalFilename || ('ID ' + row.id)
this.$confirm(
`将删除上传记录「${title}」,并尝试从订单中移除本次导入写入的后返备注。历史导入(无上传批次标记)可能无法自动清除订单备注,是否继续?`,
'删除后返上传记录',
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }
).then(() => {
return deleteGroupRebateUpload(row.id)
}).then((res) => {
const data = res.data || {}
const msg = data.message || res.msg || '已删除'
this.$message.success(msg)
this.fetchRebateUploadList()
this.getList()
}).catch((e) => {
if (e !== 'cancel') {
this.$message.error((e && e.message) || '删除失败')
}
})
},
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,
isPriceProtected: undefined,
isInvoiceOpened: undefined,
isReviewPosted: undefined,
hasRebateRemark: undefined,
rebateRemarkHasAbnormal: undefined,
rebateWithoutUploadLink: undefined
}
this.rebateRemarkScreen = 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)
},
/** 录单 Excel成本 = 下单付款 - 后返(与后台利润口径一致) */
formatExcelCost(row) {
if (!row) return ''
const hasPay = row.paymentAmount != null && row.paymentAmount !== ''
const hasReb = row.rebateAmount != null && row.rebateAmount !== ''
if (!hasPay && !hasReb) return ''
const pay = hasPay ? Number(row.paymentAmount) : 0
const reb = hasReb ? Number(row.rebateAmount) : 0
return (pay - reb).toFixed(2)
},
/** 保存后拉取单条,展示服务端重算后的利润/售价 */
patchOrderRowFromServer(id) {
if (id == null) return
getJDOrder(id)
.then(res => {
const raw = res && res.data
if (!raw) return
const idx = this.list.findIndex(r => r.id === id)
if (idx === -1) return
const merged = this.normalizeOrderListItem({ ...this.list[idx], ...raw })
this.$set(this.list, idx, merged)
})
.catch(() => {})
},
persistOrderRow(row, successMsg) {
return updateJDOrder(row)
.then(() => {
if (successMsg) this.$message.success(successMsg)
this.patchOrderRowFromServer(row.id)
})
.catch(() => {
this.$message.error('保存失败')
this.getList()
})
},
onOrderSellingPriceTypeChange(row) {
row.sellingPriceManual = 0
row.profitManual = 0
this.persistOrderRow(row)
},
onOrderSellingPriceChange(row) {
row.sellingPriceManual = 1
row.profitManual = 0
this.persistOrderRow(row)
},
onOrderProfitChange(row) {
row.profitManual = 1
this.persistOrderRow(row)
},
fillSellingPriceFromConfig(row) {
row.sellingPriceManual = 0
row.profitManual = 0
this.persistOrderRow(row, '已按型号配置回填售价并重算利润')
},
recalcOrderProfitOnly(row) {
row.profitManual = 0
this.persistOrderRow(row, '已重算利润')
},
handleBatchRecalcProfit() {
if (!this.selectedRows || this.selectedRows.length === 0) {
this.$message.warning('请先勾选订单')
return
}
this.loading = true
recalcProfitBatch(this.selectedRows.map(r => r.id))
.then(() => {
this.$message.success('批量重算完成')
this.getList()
})
.catch((e) => {
this.$message.error((e && e.message) || '批量重算失败')
})
.finally(() => {
this.loading = false
})
},
/** 导出按钮操作 */
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
}
})
},
/** 切换点过价保状态(标签点击) */
togglePriceProtected(row) {
const oldValue = row.isPriceProtected
row.isPriceProtected = row.isPriceProtected === 1 ? 0 : 1
// 如果设置为"是",自动设置当前日期
if (row.isPriceProtected === 1 && !row.priceProtectedDate) {
row.priceProtectedDate = new Date()
}
// 如果设置为"否",清空日期
if (row.isPriceProtected === 0) {
row.priceProtectedDate = null
}
// 调用后端API更新数据库
updateJDOrder(row).then(() => {
this.$message.success(`订单 ${row.remark} 的点过价保状态已更新`)
}).catch(() => {
this.$message.error('更新失败,请稍后重试')
// 恢复原状态
row.isPriceProtected = oldValue
if (row.isPriceProtected === 0) {
row.priceProtectedDate = null
}
})
},
/** 切换开过专票状态(标签点击) */
toggleInvoiceOpened(row) {
const oldValue = row.isInvoiceOpened
row.isInvoiceOpened = row.isInvoiceOpened === 1 ? 0 : 1
// 如果设置为"是",自动设置当前日期
if (row.isInvoiceOpened === 1 && !row.invoiceOpenedDate) {
row.invoiceOpenedDate = new Date()
}
// 如果设置为"否",清空日期
if (row.isInvoiceOpened === 0) {
row.invoiceOpenedDate = null
}
// 调用后端API更新数据库
updateJDOrder(row).then(() => {
this.$message.success(`订单 ${row.remark} 的开过专票状态已更新`)
}).catch(() => {
this.$message.error('更新失败,请稍后重试')
// 恢复原状态
row.isInvoiceOpened = oldValue
if (row.isInvoiceOpened === 0) {
row.invoiceOpenedDate = null
}
})
},
/** 切换晒过评价状态(标签点击) */
toggleReviewPosted(row) {
const oldValue = row.isReviewPosted
row.isReviewPosted = row.isReviewPosted === 1 ? 0 : 1
// 如果设置为"是",自动设置当前日期
if (row.isReviewPosted === 1 && !row.reviewPostedDate) {
row.reviewPostedDate = new Date()
}
// 如果设置为"否",清空日期
if (row.isReviewPosted === 0) {
row.reviewPostedDate = null
}
// 调用后端API更新数据库
updateJDOrder(row).then(() => {
this.$message.success(`订单 ${row.remark} 的晒过评价状态已更新`)
}).catch(() => {
this.$message.error('更新失败,请稍后重试')
// 恢复原状态
row.isReviewPosted = oldValue
if (row.isReviewPosted === 0) {
row.reviewPostedDate = 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订单自动写入配置已更新')
},
/** 分销标识接收人配置更新后的回调 */
handleTouserConfigUpdated() {
this.$message.success('分销标识接收人配置已更新')
},
/** 批量标记后返到账(赔付金额>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}`
},
/** 获取订单状态文本(短版,用于表格窄列) */
getOrderStatusTextShort(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: '黑名单',
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']
textKeys.forEach(key => {
const value = this.queryParams[key]
if (typeof value === 'string') {
const trimmed = value.trim()
this.queryParams[key] = trimmed || undefined
}
})
},
/** 退货复制 */
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)
},
/** 复制单条订单的录单格式文本到剪贴板 */
copySingleOrderExcelText(row) {
try {
// 日期格式yyyy/MM/dd
let dateStr = ''
if (row.orderTime) {
const date = new Date(row.orderTime)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
dateStr = `${year}/${month}/${day}`
}
// 多多单号(第三方单号,如果没有则使用内部单号)
const duoduoOrderNo = (row.thirdPartyOrderNo && row.thirdPartyOrderNo.trim())
? row.thirdPartyOrderNo : (row.remark || '')
// 型号
const modelNumber = row.modelNumber || ''
// 数量固定为1
const quantity = '1'
// 地址
const address = row.address || ''
// 姓名(从地址中提取,地址格式通常是"姓名 电话 详细地址"
let buyer = ''
if (address) {
// 提取地址中的第一个词作为姓名
const addressParts = address.trim().split(/\s+/)
if (addressParts.length > 0) {
buyer = addressParts[0]
}
}
const sellingPriceStr = row.sellingPrice != null && row.sellingPrice !== '' ? Number(row.sellingPrice).toFixed(2) : '0'
const costStr = this.formatExcelCost(row)
const profitStr = row.profit != null && row.profit !== '' ? Number(row.profit).toFixed(2) : ''
// 京东单号
const orderId = row.orderId || ''
// 物流链接
const logisticsLink = row.logisticsLink || ''
// 下单付款
const paymentAmountStr = row.paymentAmount
? row.paymentAmount.toFixed(2) : ''
// 后返
const rebateAmountStr = row.rebateAmount
? row.rebateAmount.toFixed(2) : ''
const shopName = ''
// 按顺序拼接:日期、多多单号、型号、数量、姓名、地址、售价、成本、利润、京东单号、物流、下单付款、后返
const text = [
dateStr,
duoduoOrderNo,
shopName,
modelNumber,
quantity,
buyer,
address,
sellingPriceStr,
costStr,
profitStr,
orderId,
logisticsLink,
paymentAmountStr,
rebateAmountStr
].join('\t')
this.copyToClipboard(text)
// 记录已复制的订单ID
if (row.id) {
this.copiedExcelTextOrderIds.add(row.id)
}
this.$message.success('已复制到剪贴板可以直接粘贴到Excel')
} catch (e) {
this.$message.error('复制失败:' + (e.message || '未知错误'))
console.error('复制单条订单录单格式失败', e)
}
},
/** 检查订单是否已复制录单格式 */
isExcelTextCopied(orderId) {
return orderId && this.copiedExcelTextOrderIds.has(orderId)
},
/** 复制后返录表格式文本到剪贴板 */
copyRebateRecordText(row) {
try {
// 前5列发过运营、需要重发运营、已经重发、需要二次重发运营、二次重发
const emptyCols = []
// 单号orderId
const orderId = row.orderId || ''
// 型号modelNumber
const modelNumber = row.modelNumber || ''
// 返现金额(团长):空
const leaderRebateAmount = ''
// 晒单金额(主图没标):空
const reviewRebateAmount = ''
// 总共返现rebateAmount整数格式
const totalRebateAmount = row.rebateAmount
? Math.round(row.rebateAmount).toString() : ''
// 确认收货日期finishTime格式yyyy/MM/dd
let finishDateStr = ''
if (row.finishTime) {
const finishDate = new Date(row.finishTime)
const year = finishDate.getFullYear()
const month = String(finishDate.getMonth() + 1).padStart(2, '0')
const day = String(finishDate.getDate()).padStart(2, '0')
finishDateStr = `${year}/${month}/${day}`
}
// 认领人buyer
const buyer = row.buyer || ''
// 下单日期orderTime格式yyyyMMdd
let orderDateStr = ''
if (row.orderTime) {
const orderDate = new Date(row.orderTime)
const year = orderDate.getFullYear()
const month = String(orderDate.getMonth() + 1).padStart(2, '0')
const day = String(orderDate.getDate()).padStart(2, '0')
orderDateStr = `${year}${month}${day}`
}
// 按顺序拼接:发过运营、需要重发运营、已经重发、需要二次重发运营、二次重发、单号、型号、返现金额(团长)、晒单金额(主图没标)、总共返现、确认收货日期、认领人、下单日期
const text = [
...emptyCols,
orderId,
modelNumber,
leaderRebateAmount,
reviewRebateAmount,
totalRebateAmount,
finishDateStr,
buyer,
orderDateStr
].join('\t')
this.copyToClipboard(text)
this.$message.success('已复制后返录表格式到剪贴板可以直接粘贴到Excel')
} catch (e) {
this.$message.error('复制失败:' + (e.message || '未知错误'))
console.error('复制后返录表格式失败', e)
}
},
/** 复制录单格式文本到剪贴板(批量) */
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
}
},
/** 处理表格选择变化 */
handleSelectionChange(selection) {
this.selectedRows = selection
},
/** 批量复制录单格式 */
handleBatchCopyExcelText() {
if (!this.selectedRows || this.selectedRows.length === 0) {
this.$message.warning('请先选择要复制的订单')
return
}
try {
const lines = []
// 遍历选中的每一行,生成录单格式文本
this.selectedRows.forEach(row => {
// 日期格式yyyy/MM/dd
let dateStr = ''
if (row.orderTime) {
const date = new Date(row.orderTime)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
dateStr = `${year}/${month}/${day}`
}
// 多多单号(第三方单号,如果没有则使用内部单号)
const duoduoOrderNo = (row.thirdPartyOrderNo && row.thirdPartyOrderNo.trim())
? row.thirdPartyOrderNo : (row.remark || '')
// 型号
const modelNumber = row.modelNumber || ''
// 数量固定为1
const quantity = '1'
// 地址
const address = row.address || ''
// 姓名(从地址中提取,地址格式通常是"姓名 电话 详细地址"
let buyer = ''
if (address) {
// 提取地址中的第一个词作为姓名
const addressParts = address.trim().split(/\s+/)
if (addressParts.length > 0) {
buyer = addressParts[0]
}
}
const sellingPriceStr = row.sellingPrice != null && row.sellingPrice !== '' ? Number(row.sellingPrice).toFixed(2) : '0'
const costStr = this.formatExcelCost(row)
const profitStr = row.profit != null && row.profit !== '' ? Number(row.profit).toFixed(2) : ''
// 京东单号
const orderId = row.orderId || ''
// 物流链接
const logisticsLink = row.logisticsLink || ''
// 下单付款
const paymentAmountStr = row.paymentAmount
? row.paymentAmount.toFixed(2) : ''
// 后返
const rebateAmountStr = row.rebateAmount
? row.rebateAmount.toFixed(2) : ''
const shopName = ''
// 按顺序拼接:日期、多多单号、型号、数量、姓名、地址、售价、成本、利润、京东单号、物流、下单付款、后返
const text = [
dateStr,
duoduoOrderNo,
shopName,
modelNumber,
quantity,
buyer,
address,
sellingPriceStr,
costStr,
profitStr,
orderId,
logisticsLink,
paymentAmountStr,
rebateAmountStr
].join('\t')
lines.push(text)
})
// 将所有行用换行符连接
const finalText = lines.join('\n')
this.copyToClipboard(finalText)
// 记录已复制的订单ID
this.selectedRows.forEach(row => {
if (row.id) {
this.copiedExcelTextOrderIds.add(row.id)
}
})
this.$message.success(`已复制 ${this.selectedRows.length} 条订单的录单格式到剪贴板可以直接粘贴到Excel`)
} catch (e) {
this.$message.error('批量复制失败:' + (e.message || '未知错误'))
console.error('批量复制录单格式失败', e)
}
},
/** 批量复制后返录表 */
handleBatchCopyRebateText() {
if (!this.selectedRows || this.selectedRows.length === 0) {
this.$message.warning('请先选择要复制的订单')
return
}
try {
const lines = []
// 遍历选中的每一行,生成后返录表格式文本
this.selectedRows.forEach(row => {
// 前5列发过运营、需要重发运营、已经重发、需要二次重发运营、二次重发
const emptyCols = ['']
// 单号orderId
const orderId = row.orderId || ''
// 型号modelNumber
const modelNumber = row.modelNumber || ''
// 返现金额(团长):空
const leaderRebateAmount = ''
// 晒单金额(主图没标):空
const reviewRebateAmount = ''
// 总共返现rebateAmount整数格式
const totalRebateAmount = row.rebateAmount
? Math.round(row.rebateAmount).toString() : ''
// 确认收货日期finishTime格式yyyy/MM/dd
let finishDateStr = ''
if (row.finishTime) {
const finishDate = new Date(row.finishTime)
const year = finishDate.getFullYear()
const month = String(finishDate.getMonth() + 1).padStart(2, '0')
const day = String(finishDate.getDate()).padStart(2, '0')
finishDateStr = `${year}/${month}/${day}`
}
// 认领人buyer
const buyer = row.buyer || ''
// 下单日期orderTime格式 yyyy/MM/dd如 2020/03/03
let orderDateStr = ''
if (row.orderTime) {
const orderDate = new Date(row.orderTime)
const year = orderDate.getFullYear()
const month = String(orderDate.getMonth() + 1).padStart(2, '0')
const day = String(orderDate.getDate()).padStart(2, '0')
orderDateStr = `${year}/${month}/${day}`
}
// 按顺序拼接:发过运营、需要重发运营、已经重发、需要二次重发运营、二次重发、单号、型号、返现金额(团长)、晒单金额(主图没标)、总共返现、确认收货日期、认领人、下单日期
const text = [
orderId,
modelNumber,
leaderRebateAmount,
reviewRebateAmount,
totalRebateAmount,
finishDateStr,
buyer,
orderDateStr
].join('\t')
lines.push(text)
})
// 将所有行用换行符连接
const finalText = lines.join('\n')
this.copyToClipboard(finalText)
this.$message.success(`已复制 ${this.selectedRows.length} 条订单的后返录表格式到剪贴板可以直接粘贴到Excel`)
} catch (e) {
this.$message.error('批量复制失败:' + (e.message || '未知错误'))
console.error('批量复制后返录表格式失败', e)
}
},
/** 批量复制四川商贸录表(格式:日期 型号 数量 地址 价格 备注 是否安排 物流) */
handleBatchCopySichuanCommerceText() {
if (!this.selectedRows || this.selectedRows.length === 0) {
this.$message.warning('请先选择要复制的订单')
return
}
try {
const header = ['日期', '型号', '数量', '地址', '价格', '备注', '是否安排', '物流'].join('\t')
const lines = [header]
this.selectedRows.forEach(row => {
// 日期格式yyyy/MM/dd
let dateStr = ''
if (row.orderTime) {
const date = new Date(row.orderTime)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
dateStr = `${year}/${month}/${day}`
}
const modelNumber = row.modelNumber || ''
const quantity = (row.productCount != null && row.productCount !== '') ? String(row.productCount) : '1'
const address = row.address || ''
const priceStr = row.paymentAmount != null ? row.paymentAmount.toFixed(2) : ''
const remark = row.remark || ''
const arranged = '' // 是否安排,留空由用户填写
const logistics = row.logisticsLink || ''
const text = [dateStr, modelNumber, quantity, address, priceStr, remark, arranged, logistics].join('\t')
lines.push(text)
})
const finalText = lines.join('\n')
this.copyToClipboard(finalText)
this.$message.success(`已复制 ${this.selectedRows.length} 条订单的四川商贸录表格式到剪贴板含表头可直接粘贴到Excel`)
} catch (e) {
this.$message.error('批量复制失败:' + (e.message || '未知错误'))
console.error('批量复制四川商贸录表失败', e)
}
}
}
}
</script>
<style scoped>
.jd-order-list-root {
--jd-accent: #2563eb;
--jd-accent-hover: #1d4ed8;
--jd-border: #e5e7eb;
--jd-text: #111827;
--jd-muted: #6b7280;
--jd-surface: #ffffff;
--jd-header: #f8fafc;
--jd-row-hover: #f8fafc;
--jd-row-stripe: #fafbfc;
}
/* 搜索区:更疏、更干净 */
.jd-order-list-root ::v-deep .search-section {
background: linear-gradient(180deg, #fafbfc 0%, #ffffff 100%);
border-bottom: 1px solid var(--jd-border);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
padding: 18px 20px 14px;
}
.jd-order-search-panel {
display: flex;
flex-direction: column;
gap: 0;
}
.jd-order-filter-form ::v-deep .el-form-item {
margin-right: 14px;
margin-bottom: 12px;
}
.jd-order-filter-form ::v-deep .el-form-item__label {
color: var(--jd-muted);
font-weight: 500;
font-size: 13px;
}
.jd-order-filter-form ::v-deep .el-input__inner,
.jd-order-filter-form ::v-deep .el-range-input {
border-radius: 6px;
}
/* 桌面工具栏:低饱和、统一圆角 */
.jd-order-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px 0 4px;
border-top: 1px solid var(--jd-border);
margin-top: 8px;
}
.jd-order-toolbar ::v-deep .el-button {
border-radius: 6px;
font-weight: 500;
letter-spacing: 0.01em;
}
.jd-order-toolbar ::v-deep .el-button--primary.is-plain {
color: var(--jd-accent);
border-color: #bfdbfe;
background: #eff6ff;
}
.jd-order-toolbar ::v-deep .el-button--primary.is-plain:hover {
color: #fff;
background: var(--jd-accent);
border-color: var(--jd-accent);
}
.jd-order-toolbar ::v-deep .jd-tb-muted.is-plain {
color: var(--jd-muted);
border-color: var(--jd-border);
background: var(--jd-surface);
}
.jd-order-toolbar ::v-deep .jd-tb-muted.is-plain:hover {
color: var(--jd-text);
border-color: #cbd5e1;
background: var(--jd-row-hover);
}
/* 表格滚动条 */
.order-table ::v-deep .el-table__body-wrapper {
scrollbar-width: thin;
scrollbar-color: #cbd5e1 #f1f5f9;
}
.order-table ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.order-table ::v-deep .el-table__body-wrapper::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 5px;
}
.order-table ::v-deep .el-table__body-wrapper::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 5px;
border: 2px solid #f1f5f9;
}
.order-table ::v-deep .el-table__body-wrapper::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
.order-table ::v-deep .el-tag {
transition: background-color 0.15s, border-color 0.15s, color 0.15s;
user-select: none;
border-radius: 4px;
}
.order-table ::v-deep .el-tag:hover {
filter: brightness(0.97);
}
.order-table ::v-deep .el-table {
border-radius: 8px;
overflow: hidden;
font-size: 13px;
color: var(--jd-text);
border: 1px solid var(--jd-border);
}
.order-table ::v-deep .el-table--border::after,
.order-table ::v-deep .el-table--group::after,
.order-table ::v-deep .el-table::before {
background-color: var(--jd-border);
}
.order-table ::v-deep .el-table__fixed-right-patch {
background: var(--jd-header);
}
.order-table ::v-deep .el-table__header-wrapper th {
background: var(--jd-header) !important;
color: #374151;
font-weight: 600;
font-size: 12px;
letter-spacing: 0.04em;
text-transform: none;
}
.order-table ::v-deep .el-table th.is-leaf {
border-bottom: 1px solid var(--jd-border);
}
.order-table ::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
background: var(--jd-row-stripe) !important;
}
.order-table ::v-deep .el-table__body tr:hover > td {
background-color: var(--jd-row-hover) !important;
}
.order-table ::v-deep .el-table__fixed-right {
box-shadow: -4px 0 16px rgba(15, 23, 42, 0.06);
}
.order-table ::v-deep .el-table td {
padding: 9px 0;
border-color: var(--jd-border);
}
.order-table ::v-deep .el-table .address-cell {
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-table ::v-deep .el-table th {
padding: 11px 0;
}
.order-table ::v-deep .el-table__fixed-left {
box-shadow: 4px 0 16px rgba(15, 23, 42, 0.06);
}
/* 退款状态:双列压缩行高 */
.refund-status-stack {
display: flex;
flex-direction: row;
gap: 14px;
align-items: flex-start;
}
.refund-status-col {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
min-width: 0;
}
.refund-status-row {
display: flex;
align-items: center;
gap: 6px;
}
.refund-status-label {
flex-shrink: 0;
width: 52px;
font-size: 11px;
font-weight: 500;
color: var(--jd-muted);
line-height: 1.2;
}
.refund-status-tag {
cursor: pointer;
flex: 0 1 auto;
max-width: 72px;
min-width: 52px;
padding: 0 6px !important;
height: 22px !important;
line-height: 20px !important;
font-size: 11px !important;
justify-content: center;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 操作列 */
.jd-action-cell {
text-align: left;
padding: 4px 0;
}
.jd-action-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 2px 4px;
margin-bottom: 4px;
}
.jd-action-row:last-child {
margin-bottom: 0;
}
.order-table ::v-deep .jd-act-link {
margin: 0 !important;
padding: 2px 6px !important;
border-radius: 4px;
color: #64748b !important;
font-size: 12px;
font-weight: 500;
}
.order-table ::v-deep .jd-act-link:hover {
color: var(--jd-text) !important;
background: #f1f5f9 !important;
}
.order-table ::v-deep .jd-act-link--accent {
color: var(--jd-accent) !important;
}
.order-table ::v-deep .jd-act-link--accent:hover {
background: #eff6ff !important;
}
.order-table ::v-deep .jd-act-done {
color: #059669 !important;
}
.order-table ::v-deep .jd-act-link--success {
color: #059669 !important;
}
.order-table ::v-deep .jd-act-link--danger {
color: #dc2626 !important;
}
.order-table ::v-deep .jd-act-link--danger:hover {
background: #fef2f2 !important;
}
.order-table ::v-deep .jd-action-row--meta .el-switch {
margin-right: 6px;
}
.order-table ::v-deep .el-table .jd-order-input-money {
width: 118px !important;
}
/* 桌面端:占满 ListLayout 表格区,内部表格滚动,表头固定 */
.jd-order-table-page {
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
.jd-order-table-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.jd-order-el-table {
flex: 1;
min-height: 0;
}
.jd-order-table-wrap .profit-summary-bar {
flex-shrink: 0;
}
/* 移动端卡片列表样式 */
.mobile-order-list {
padding: 12px;
background: #f5f5f5;
min-height: 300px;
-webkit-overflow-scrolling: touch;
}
.mobile-order-card {
background: #fff;
border-radius: 12px;
margin-bottom: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.mobile-order-card:active {
transform: scale(0.98);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
}
.mobile-order-card:last-child {
margin-bottom: 0;
}
.mobile-order-card .card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.mobile-order-card .header-left {
flex: 1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.mobile-order-card .order-id {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.mobile-order-card .header-right {
flex-shrink: 0;
margin-left: 12px;
}
.mobile-order-card .card-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.mobile-order-card .field-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
min-height: 24px;
}
.mobile-order-card .field-label {
font-size: 13px;
color: #909399;
min-width: 80px;
flex-shrink: 0;
margin-right: 12px;
}
.mobile-order-card .field-value {
flex: 1;
font-size: 14px;
color: #303133;
text-align: right;
word-break: break-all;
}
.mobile-order-card .field-value.address-text {
font-weight: 500;
}
.mobile-order-card .field-value.amount {
font-weight: 600;
color: #e6a23c;
}
.mobile-order-card .refund-status .field-value {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
align-items: center;
}
.mobile-order-card .status-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: flex-end;
}
.rebate-remark-popover .rebate-remark-item {
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
font-size: 13px;
line-height: 1.5;
}
.rebate-remark-popover .rebate-remark-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.rebate-remark-popover .rebate-remark-title {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.rebate-remark-popover .rebate-remark-time {
color: #909399;
font-size: 12px;
font-weight: normal;
flex-shrink: 0;
}
/* 移动端操作按钮样式 - 只在移动端生效 */
@media (max-width: 768px) {
.mobile-action-wrapper {
display: flex;
justify-content: center;
align-items: center;
}
.mobile-action-btn {
width: 36px;
height: 36px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
/* 移动端下拉菜单样式 */
::v-deep .mobile-action-dropdown {
min-width: 180px;
max-width: 240px;
}
::v-deep .mobile-action-dropdown .el-dropdown-menu__item {
padding: 12px 20px;
font-size: 14px;
line-height: 1.5;
}
::v-deep .mobile-action-dropdown .el-dropdown-menu__item.is-copied {
color: #67C23A;
}
::v-deep .mobile-action-dropdown .el-dropdown-menu__item--divided {
border-top: 1px solid #e4e7ed;
margin-top: 4px;
padding-top: 8px;
}
/* 移动端隐藏操作列标题 */
.order-table ::v-deep .action-column .el-table__column-filter-trigger,
.order-table ::v-deep .action-column .cell {
padding: 8px 4px;
}
.order-table ::v-deep .el-table__fixed-right {
width: 60px !important;
}
.order-table ::v-deep .el-table__fixed-right-patch {
width: 60px !important;
}
/* 隐藏桌面端表格 */
.order-table {
display: none;
}
}
/* 桌面端操作按钮优化布局 - 所有按钮一行显示 */
.desktop-action-buttons-wrapper {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
padding: 0;
box-sizing: border-box;
flex-wrap: wrap;
gap: 2px;
}
.desktop-action-buttons-wrapper .action-row-all {
display: flex;
flex-wrap: wrap;
gap: 2px;
justify-content: flex-start;
align-items: center;
width: 100%;
line-height: 1.2;
}
.desktop-action-buttons-wrapper .el-button {
padding: 3px 6px !important;
font-size: 12px !important;
white-space: nowrap !important;
margin: 0 !important;
height: 24px !important;
line-height: 1.2 !important;
min-height: 24px !important;
border: none !important;
flex-shrink: 0;
display: inline-flex !important;
align-items: center !important;
}
.desktop-action-buttons-wrapper .el-switch {
margin-left: 4px !important;
flex-shrink: 0;
}
/* 桌面端下拉菜单样式 */
::v-deep .desktop-action-dropdown {
min-width: 160px;
}
::v-deep .desktop-action-dropdown .el-dropdown-menu__item {
padding: 10px 20px;
font-size: 13px;
}
::v-deep .desktop-action-dropdown .el-dropdown-menu__item.is-copied {
color: #67C23A;
}
/* 桌面端确保操作列正常显示 */
@media (min-width: 769px) {
.order-table ::v-deep .action-column {
width: 380px !important;
}
.order-table ::v-deep .el-table__fixed-right {
width: 380px !important;
}
.order-table ::v-deep .el-table__fixed-right-patch {
width: 380px !important;
}
/* 隐藏移动端按钮 */
.mobile-action-wrapper {
display: none !important;
}
/* 优化表格单元格高度和布局 */
.order-table ::v-deep .action-column .cell {
padding: 6px 4px !important;
line-height: 1.3 !important;
overflow: visible !important;
}
/* 确保操作按钮容器正确显示 */
.order-table ::v-deep .action-column .desktop-action-buttons-wrapper {
width: 100%;
box-sizing: border-box;
}
}
/* 操作按钮区域 */
.action-buttons-section {
margin-top: 12px;
margin-bottom: 12px;
}
/* 移动端和桌面端按钮组显示控制 */
@media (max-width: 768px) {
.desktop-only {
display: none !important;
}
.action-buttons-section.mobile-only {
display: block;
}
}
@media (min-width: 769px) {
.mobile-only {
display: none !important;
}
.desktop-action-buttons.desktop-only {
display: block;
}
}
.desktop-action-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
/* 移动端优化 */
@media (max-width: 768px) {
.action-buttons-section.mobile-only {
margin-top: 8px;
margin-bottom: 8px;
display: block;
}
.desktop-action-buttons.desktop-only {
display: none !important;
}
/* 移动端隐藏分页或简化显示 */
.list-pagination {
display: none; /* 移动端隐藏分页,因为很少用 */
}
}
/* 桌面端显示桌面按钮,隐藏移动端按钮 */
@media (min-width: 769px) {
.action-buttons-section.mobile-only {
display: none !important;
}
.desktop-action-buttons.desktop-only {
display: flex !important;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
}
.profit-summary-bar {
margin: 0 0 12px 0;
padding: 12px 16px;
background: var(--jd-header, #f8fafc);
border: 1px solid var(--jd-border, #e5e7eb);
border-radius: 8px;
font-size: 13px;
color: var(--jd-muted, #6b7280);
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
}
.profit-summary-bar b {
color: var(--jd-text, #111827);
font-weight: 600;
}
.sticky-summary {
position: sticky;
top: 0;
z-index: 20;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
}
.profit-summary-item {
margin-right: 20px;
}
.mobile-profit-summary {
font-size: 12px;
line-height: 1.5;
}
.mobile-profit-summary div + div {
margin-top: 4px;
}
/* 本页:避免外层 .table-section 与表格内部双纵向滚动,横向滚动仍由表体承担 */
.jd-order-list-root ::v-deep .table-section {
overflow-x: auto;
overflow-y: hidden;
}
</style>