Files
ruoyi-vue/src/views/jarvis/batchPublish/index.vue
2025-10-30 18:24:09 +08:00

1524 lines
55 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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="app-container">
<el-card class="box-card" shadow="hover">
<div slot="header" class="clearfix">
<span style="font-weight: bold; font-size: 16px;">📦 线报批量发品</span>
<el-button style="float: right; padding: 3px 10px" type="text" @click="showHistory">历史记录</el-button>
</div>
<!-- 步骤条 -->
<el-steps :active="activeStep" finish-status="success" align-center style="margin-bottom: 30px">
<el-step title="输入线报消息"></el-step>
<el-step title="选择商品"></el-step>
<el-step title="编辑商品信息"></el-step>
<el-step title="设置参数"></el-step>
<el-step title="预览确认"></el-step>
<el-step title="批量发品"></el-step>
</el-steps>
<!-- 第一步输入线报消息 -->
<div v-show="activeStep === 0" class="step-content">
<el-form label-width="100px">
<el-form-item label="线报消息">
<el-input
type="textarea"
v-model="lineReportMessage"
:rows="12"
placeholder="请粘贴线报消息内容,支持多个商品链接..."
@input="onMessageInput"
/>
<div style="margin-top: 10px; color: #909399; font-size: 12px;">
<i class="el-icon-info"></i> 支持京东商品链接SKUID等多种格式系统会自动识别并提取商品信息
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="parsing" @click="parseMessage" :disabled="!lineReportMessage.trim()">
解析商品 <i class="el-icon-arrow-right"></i>
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 第二步选择商品 -->
<div v-show="activeStep === 1" class="step-content">
<div style="margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center;">
<div>
<el-button type="primary" size="small" @click="selectAll">全选</el-button>
<el-button size="small" @click="selectNone">取消全选</el-button>
<el-tag type="info" size="small" style="margin-left: 10px;">
已选择: {{ selectedProducts.length }} / {{ parsedProducts.length }}
</el-tag>
</div>
<el-button type="success" @click="nextStep" :disabled="selectedProducts.length === 0">
下一步 <i class="el-icon-arrow-right"></i>
</el-button>
</div>
<el-table
:data="parsedProducts"
border
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center"></el-table-column>
<el-table-column label="商品图片" width="100" align="center">
<template slot-scope="scope">
<el-image
v-if="scope.row.productImage"
:src="scope.row.productImage"
:preview-src-list="[scope.row.productImage]"
style="width: 60px; height: 60px; border-radius: 4px;"
fit="cover"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="商品信息" min-width="300">
<template slot-scope="scope">
<div style="font-weight: bold; margin-bottom: 5px;">{{ scope.row.productName }}</div>
<div style="color: #666; font-size: 12px;">
<div>SKUID: {{ scope.row.skuid }}</div>
<div v-if="scope.row.shopName">店铺: {{ scope.row.shopName }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="价格" width="120" align="center">
<template slot-scope="scope">
<div style="color: #f56c6c; font-weight: bold; font-size: 16px;">¥{{ scope.row.price }}</div>
</template>
</el-table-column>
<el-table-column label="佣金" width="100" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.commissionInfo" type="success" size="small">
{{ scope.row.commissionInfo }}
</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 15px;">
<el-button @click="prevStep">
<i class="el-icon-arrow-left"></i> 上一步
</el-button>
</div>
</div>
<!-- 第三步编辑商品信息 -->
<div v-show="activeStep === 2" class="step-content">
<div style="margin-bottom: 15px;">
<el-alert type="info" :closable="false">
为每个商品设置发布价格和文案版本固定属性成色库存等将在下一步统一设置
</el-alert>
</div>
<el-table :data="selectedProducts" border style="width: 100%">
<!-- 商品图片 -->
<el-table-column label="商品图片" width="100" align="center">
<template slot-scope="scope">
<el-image
v-if="scope.row.productImage"
:src="scope.row.productImage"
style="width: 60px; height: 60px; border-radius: 4px;"
fit="cover"
/>
</template>
</el-table-column>
<!-- 商品名称 -->
<el-table-column label="商品名称" min-width="250">
<template slot-scope="scope">
<div style="font-weight: bold; margin-bottom: 5px;">{{ scope.row.productName }}</div>
<div style="color: #666; font-size: 12px;">SKUID: {{ scope.row.skuid }}</div>
</template>
</el-table-column>
<!-- 京东原价 -->
<el-table-column label="京东原价" width="100" align="center">
<template slot-scope="scope">
<span style="color: #909399;">¥{{ scope.row.price }}</span>
</template>
</el-table-column>
<!-- 发布价格可编辑 -->
<el-table-column label="发布价格" width="140" align="center">
<template slot-scope="scope">
<el-input-number
v-model="scope.row.publishPrice"
:min="0"
:precision="2"
:step="10"
size="small"
controls-position="right"
style="width: 120px;"
/>
</template>
</el-table-column>
<!-- 文案版本选择 -->
<el-table-column label="文案版本" width="200" align="center">
<template slot-scope="scope">
<el-select
v-model="scope.row.selectedWenanIndex"
size="small"
style="width: 100%;"
:disabled="!scope.row.wenan || scope.row.wenan.length === 0"
>
<el-option
v-for="(wenan, index) in scope.row.wenan"
:key="index"
:label="wenan.type"
:value="index"
/>
</el-select>
<div v-if="!scope.row.wenan || scope.row.wenan.length === 0" style="color: #999; font-size: 12px;">
无可用文案
</div>
</template>
</el-table-column>
<!-- 预览文案 -->
<el-table-column label="操作" width="100" align="center">
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="previewWenan(scope.row)"
:disabled="!scope.row.wenan || scope.row.wenan.length === 0"
>
预览文案
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 批量操作 -->
<div style="margin-top: 15px;">
<el-button size="small" @click="batchSetPrice">批量设置价格</el-button>
<el-button size="small" @click="batchSetWenan">批量选择文案版本</el-button>
</div>
<div style="margin-top: 20px;">
<el-button @click="prevStep">
<i class="el-icon-arrow-left"></i> 上一步
</el-button>
<el-button type="primary" @click="nextStep">
下一步 <i class="el-icon-arrow-right"></i>
</el-button>
</div>
</div>
<!-- 第四步设置参数 -->
<div v-show="activeStep === 3" class="step-content">
<el-form :model="publishForm" :rules="publishRules" ref="publishForm" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="任务名称" prop="taskName">
<el-input v-model="publishForm.taskName" placeholder="请输入任务名称(选填)"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="延迟上架" prop="delaySeconds">
<el-input-number v-model="publishForm.delaySeconds" :min="1" :max="60" placeholder="秒"/>
<span style="margin-left: 10px; color: #909399;">发品成功后延迟上架</span>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="选择账号及子账号" required>
<div style="border: 1px solid #dcdfe6; border-radius: 4px; padding: 15px;">
<el-checkbox-group v-model="publishForm.selectedMainAccounts" @change="onMainAccountsChange">
<div v-for="account in erpAccounts" :key="account.value" style="margin-bottom: 20px;">
<!-- 主账号复选框 -->
<el-checkbox :label="account.value" style="font-weight: bold; margin-bottom: 10px;">
{{ account.label }}
</el-checkbox>
<!-- 该主账号下的子账号选择 -->
<div v-if="publishForm.selectedMainAccounts.includes(account.value)" style="margin-left: 30px; margin-top: 10px;">
<el-select
v-model="publishForm.accountSubAccounts[account.value]"
multiple
:placeholder="`请选择${account.label}下的子账号`"
:loading="loadingSubAccounts[account.value]"
style="width: 100%;"
@visible-change="(visible) => visible && loadSubAccountsForAccount(account.value)"
>
<el-option
v-for="subAccount in subAccountsMap[account.value] || []"
:key="subAccount.value"
:label="subAccount.label"
:value="subAccount.value"
/>
</el-select>
</div>
</div>
</el-checkbox-group>
</div>
<div style="margin-top: 5px; color: #909399; font-size: 12px;">
<i class="el-icon-info"></i> 可同时选择多个主账号,每个主账号下可选择多个子账号
</div>
</el-form-item>
<el-divider content-position="left">通用参数</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="商品类型" prop="itemBizType" required>
<el-select v-model="publishForm.itemBizType" placeholder="请选择" style="width: 100%;">
<el-option label="普通商品" :value="2"/>
<el-option label="已验货" :value="0"/>
<el-option label="验货宝" :value="10"/>
<el-option label="闲鱼优品" :value="19"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="行业类型" prop="spBizType" required>
<el-select v-model="publishForm.spBizType" placeholder="请选择" style="width: 100%;">
<el-option label="手机" :value="1"/>
<el-option label="时尚" :value="2"/>
<el-option label="家电" :value="3"/>
<el-option label="数码3C" :value="9"/>
<el-option label="母婴" :value="17"/>
<el-option label="美妆" :value="18"/>
<el-option label="家居" :value="21"/>
<el-option label="其他" :value="99"/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="//" required>
<div style="display:flex; gap:8px; width:100%">
<el-select
v-model.number="publishForm.province"
placeholder="选择省"
style="flex:1"
filterable
@change="onProvinceChange"
>
<el-option
v-for="p in regionOptions.provinces"
:key="p.value"
:label="p.label"
:value="p.value"
/>
</el-select>
<el-select
v-model.number="publishForm.city"
placeholder="选择市"
style="flex:1"
filterable
:disabled="!publishForm.province"
@change="onCityChange"
>
<el-option
v-for="c in regionOptions.cities"
:key="c.value"
:label="c.label"
:value="c.value"
/>
</el-select>
<el-select
v-model.number="publishForm.district"
placeholder="选择区"
style="flex:1"
filterable
:disabled="!publishForm.city"
>
<el-option
v-for="a in regionOptions.areas"
:key="a.value"
:label="a.label"
:value="a.value"
/>
</el-select>
</div>
</el-form-item>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="类目" prop="channelCatId" required>
<el-select
v-model="publishForm.channelCatId"
filterable
placeholder="请选择类目"
:disabled="!categoryOptions.length"
:loading="categoryLoading"
style="width: 100%;"
@change="onCategoryChange"
>
<el-option
v-for="c in categoryOptions"
:key="c.value"
:label="c.label"
:value="c.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="邮费" prop="expressFee" required>
<el-input-number v-model="publishForm.expressFee" :min="0" :precision="2" placeholder="" style="width: 100%;"/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="库存" prop="stock" required>
<el-input-number v-model="publishForm.stock" :min="1" placeholder="库存数量" style="width: 100%;"/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="成色" prop="stuffStatus">
<el-select v-model="publishForm.stuffStatus" placeholder="请选择" clearable style="width: 100%;">
<el-option label="全新" :value="100"/>
<el-option label="99" :value="99"/>
<el-option label="95" :value="95"/>
<el-option label="9成新" :value="90"/>
<el-option label="8成新" :value="80"/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="服务支持" prop="serviceSupport">
<el-select v-model="publishForm.serviceSupport" multiple placeholder="请选择服务支持" style="width: 100%;">
<el-option label="七天无理由退货" value="SDR"/>
<el-option label="描述不符包邮退" value="NFR"/>
<el-option label="24小时极速发货" value="FD_24HS"/>
<el-option label="48小时极速发货" value="FD_48HS"/>
<el-option label="正品保障" value="FD_GPA"/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="prevStep">
<i class="el-icon-arrow-left"></i> 上一步
</el-button>
<el-button type="primary" @click="nextStep">
下一步:预览确认 <i class="el-icon-arrow-right"></i>
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 第五步:预览确认 -->
<div v-show="activeStep === 4" class="step-content">
<el-alert type="warning" :closable="false" style="margin-bottom: 20px;">
<template slot="title">
<strong>即将发布 {{ totalPublishCount }} 条商品</strong>
{{ selectedProducts.length }} 个商品 × {{ totalSubAccountCount }} 个子账号)
</template>
请仔细核对发布信息,确认无误后点击"确认发布"按钮。
</el-alert>
<!-- 汇总信息 -->
<el-descriptions :column="4" border style="margin-bottom: 20px;">
<el-descriptions-item label="商品数量">{{ selectedProducts.length }}</el-descriptions-item>
<el-descriptions-item label="目标账号">{{ totalSubAccountCount }}</el-descriptions-item>
<el-descriptions-item label="总发布数">{{ totalPublishCount }}</el-descriptions-item>
<el-descriptions-item label="延迟上架">{{ publishForm.delaySeconds }}秒</el-descriptions-item>
</el-descriptions>
<!-- 详细预览表格 -->
<el-table :data="previewList" border style="width: 100%" max-height="500">
<el-table-column type="index" label="#" width="50" align="center"/>
<el-table-column label="商品图片" width="80" align="center">
<template slot-scope="scope">
<el-image :src="scope.row.productImage" style="width: 50px; height: 50px;" fit="cover"/>
</template>
</el-table-column>
<el-table-column label="商品名称" min-width="200" show-overflow-tooltip>
<template slot-scope="scope">
<div style="font-weight: bold;">{{ scope.row.productName }}</div>
<div style="color: #666; font-size: 12px;">SKUID: {{ scope.row.skuid }}</div>
</template>
</el-table-column>
<el-table-column label="发布价格" width="100" align="center">
<template slot-scope="scope">
<span style="color: #f56c6c; font-weight: bold;">¥{{ scope.row.publishPrice }}</span>
</template>
</el-table-column>
<el-table-column label="目标账号" width="150" align="center">
<template slot-scope="scope">
<el-tag size="small">{{ scope.row.accountName }}</el-tag>
</template>
</el-table-column>
<el-table-column label="子账号" width="120" align="center">
<template slot-scope="scope">
{{ scope.row.subAccount }}
</template>
</el-table-column>
<el-table-column label="文案版本" width="150" align="center">
<template slot-scope="scope">
<el-tag type="success" size="small">{{ scope.row.wenanType }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="small" @click="viewFullWenan(scope.row)">
查看文案
</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 20px;">
<el-button @click="prevStep">
<i class="el-icon-arrow-left"></i> 上一步
</el-button>
<el-button type="success" @click="submitPublish" :loading="publishing">
确认发布 <i class="el-icon-check"></i>
</el-button>
</div>
</div>
<!-- 第六步:发品进度 -->
<div v-show="activeStep === 5" class="step-content">
<el-alert
v-if="publishResult.taskId"
title="批量发品任务已创建"
type="success"
:description="`任务ID: ${publishResult.taskId}`"
show-icon
:closable="false"
style="margin-bottom: 20px;"
/>
<el-progress
:percentage="publishProgress"
:status="publishProgress === 100 ? 'success' : null"
style="margin-bottom: 20px;"
/>
<el-table :data="publishItems" border style="width: 100%" v-if="publishItems.length > 0">
<el-table-column label="商品" prop="productName" min-width="200"/>
<el-table-column label="账号" prop="accountRemark" width="120"/>
<el-table-column label="状态" width="120" align="center">
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.status)" size="small">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="商品ID" prop="productId" width="150"/>
<el-table-column label="执行日志" prop="execLog" min-width="220">
<template slot-scope="scope">
<span style="white-space: pre-wrap;" v-if="scope.row.execLog">{{ scope.row.execLog }}</span>
<span v-else style="color:#909399">-</span>
</template>
</el-table-column>
<el-table-column label="错误信息" prop="errorMessage" min-width="200" :show-overflow-tooltip="true"/>
</el-table>
<div style="margin-top: 20px; text-align: center;">
<el-button type="primary" @click="reset">创建新任务</el-button>
<el-button @click="showHistory">查看历史记录</el-button>
</div>
</div>
</el-card>
<!-- 历史记录对话框 -->
<el-dialog title="批量发品历史记录" :visible.sync="historyVisible" width="80%" append-to-body>
<el-table :data="historyList" border style="width: 100%">
<el-table-column label="任务ID" prop="id" width="80"/>
<el-table-column label="任务名称" prop="taskName" min-width="150"/>
<el-table-column label="商品数" prop="selectedProducts" width="80" align="center"/>
<el-table-column label="目标账号数" width="100" align="center">
<template slot-scope="scope">
{{ parseTargetAccounts(scope.row.targetAccounts).length }}
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template slot-scope="scope">
<el-tag :type="getTaskStatusType(scope.row.status)">
{{ getTaskStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="成功/失败" width="100" align="center">
<template slot-scope="scope">
<span style="color: #67c23a;">{{ scope.row.successCount }}</span> /
<span style="color: #f56c6c;">{{ scope.row.failCount }}</span>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" width="160"/>
<el-table-column label="操作" width="180" align="center">
<template slot-scope="scope">
<el-button type="text" size="small" @click="viewTaskDetail(scope.row)">查看详情</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" size="small" @click="onRetryTask(scope.row)">重试未执行</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="historyTotal>0"
:total="historyTotal"
:page.sync="historyQuery.pageNum"
:limit.sync="historyQuery.pageSize"
@pagination="loadHistory"
/>
</el-dialog>
<!-- 任务详情对话框 -->
<el-dialog title="任务详情" :visible.sync="detailVisible" width="80%" append-to-body>
<el-descriptions v-if="currentTask" :column="3" border>
<el-descriptions-item label="任务ID">{{ currentTask.id }}</el-descriptions-item>
<el-descriptions-item label="任务名称">{{ currentTask.taskName }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getTaskStatusType(currentTask.status)">
{{ getTaskStatusText(currentTask.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="商品数">{{ currentTask.selectedProducts }}</el-descriptions-item>
<el-descriptions-item label="成功数">{{ currentTask.successCount }}</el-descriptions-item>
<el-descriptions-item label="失败数">{{ currentTask.failCount }}</el-descriptions-item>
<el-descriptions-item label="创建人">{{ currentTask.createUserName }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ currentTask.createTime }}</el-descriptions-item>
</el-descriptions>
<el-divider>发品明细</el-divider>
<el-table :data="taskItems" border style="width: 100%">
<el-table-column label="商品" prop="productName" min-width="200"/>
<el-table-column label="SKUID" prop="skuid" width="120"/>
<el-table-column label="账号" prop="accountRemark" width="120"/>
<el-table-column label="状态" width="120" align="center">
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.status)" size="small">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="商品ID" prop="productId" width="150"/>
<el-table-column label="价格" width="100" align="center">
<template slot-scope="scope">
<span v-if="scope.row.publishPrice">¥{{ (scope.row.publishPrice / 100).toFixed(2) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="执行日志" prop="execLog" min-width="220">
<template slot-scope="scope">
<span style="white-space: pre-wrap;" v-if="scope.row.execLog">{{ scope.row.execLog }}</span>
<span v-else style="color:#909399">-</span>
</template>
</el-table-column>
<el-table-column label="错误信息" prop="errorMessage" min-width="200" :show-overflow-tooltip="true"/>
</el-table>
</el-dialog>
<!-- 文案预览对话框 -->
<el-dialog title="文案预览" :visible.sync="wenanPreviewVisible" width="60%" append-to-body>
<el-descriptions v-if="currentPreviewProduct" :column="1" border>
<el-descriptions-item label="商品名称">{{ currentPreviewProduct.productName }}</el-descriptions-item>
<el-descriptions-item label="文案类型">
{{ currentPreviewProduct.wenan && currentPreviewProduct.wenan[currentPreviewProduct.selectedWenanIndex] ?
currentPreviewProduct.wenan[currentPreviewProduct.selectedWenanIndex].type : '无' }}
</el-descriptions-item>
</el-descriptions>
<el-divider>文案内容</el-divider>
<div v-if="currentPreviewProduct && currentPreviewProduct.wenan && currentPreviewProduct.wenan[currentPreviewProduct.selectedWenanIndex]"
style="white-space: pre-wrap; line-height: 1.8; padding: 15px; background: #f5f5f5; border-radius: 4px; max-height: 400px; overflow-y: auto;">
{{ currentPreviewProduct.wenan[currentPreviewProduct.selectedWenanIndex].content }}
</div>
<div v-else style="text-align: center; padding: 30px; color: #999;">
暂无文案内容
</div>
<div slot="footer">
<el-button @click="wenanPreviewVisible = false">关闭</el-button>
<el-button type="primary" @click="copyWenan"
:disabled="!currentPreviewProduct || !currentPreviewProduct.wenan || !currentPreviewProduct.wenan[currentPreviewProduct.selectedWenanIndex]">
复制文案
</el-button>
</div>
</el-dialog>
<!-- 完整文案查看对话框(预览页面使用) -->
<el-dialog title="完整文案" :visible.sync="fullWenanVisible" width="60%" append-to-body>
<el-descriptions v-if="currentFullWenan" :column="2" border>
<el-descriptions-item label="商品名称" :span="2">{{ currentFullWenan.productName }}</el-descriptions-item>
<el-descriptions-item label="发布价格">¥{{ currentFullWenan.publishPrice }}</el-descriptions-item>
<el-descriptions-item label="文案类型">{{ currentFullWenan.wenanType }}</el-descriptions-item>
</el-descriptions>
<el-divider>文案内容</el-divider>
<div style="white-space: pre-wrap; line-height: 1.8; padding: 15px; background: #f5f5f5; border-radius: 4px; max-height: 400px; overflow-y: auto;">
{{ currentFullWenan ? currentFullWenan.wenanContent : '' }}
</div>
<div slot="footer">
<el-button @click="fullWenanVisible = false">关闭</el-button>
<el-button type="primary" @click="copyFullWenan">复制文案</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { parseLineReport, batchPublish, listTasks, getTask, listItems, retryTask } from "@/api/jarvis/batchPublish";
import { getERPAccounts, getUsernames, getProvinces, getCities, getAreas, getCategories } from "@/api/system/jdorder";
import Pagination from "@/components/Pagination";
export default {
name: "BatchPublish",
components: { Pagination },
data() {
return {
// 步骤
activeStep: 0,
// 输入
lineReportMessage: "",
parsing: false,
// 商品列表
parsedProducts: [],
selectedProducts: [],
// 发品表单
publishForm: {
taskName: "",
selectedMainAccounts: [], // 选中的主账号列表
accountSubAccounts: {}, // 每个主账号对应的子账号 { appid: [subAccount1, subAccount2] }
delaySeconds: 3,
userName: "",
province: 110000, // 默认北京市
city: 110100, // 默认北京市
district: 110105, // 默认朝阳区
itemBizType: 2,
spBizType: 3,
channelCatId: "",
expressFee: 0,
stock: 1,
stuffStatus: 100,
serviceSupport: [],
channelPv: ""
},
publishRules: {
province: [{ required: true, message: "请输入省代码", trigger: "blur" }],
city: [{ required: true, message: "请输入市代码", trigger: "blur" }],
district: [{ required: true, message: "请输入区代码", trigger: "blur" }],
itemBizType: [{ required: true, message: "请选择商品类型", trigger: "change" }],
spBizType: [{ required: true, message: "请选择行业类型", trigger: "change" }],
channelCatId: [{ required: true, message: "请输入类目ID", trigger: "blur" }],
expressFee: [{ required: true, message: "请输入邮费", trigger: "blur" }],
stock: [{ required: true, message: "请输入库存", trigger: "blur" }]
},
publishing: false,
// ERP账号
erpAccounts: [],
// 子账号 - 每个主账号对应一个子账号列表
subAccountsMap: {}, // { appid: [{value, label}] }
loadingSubAccounts: {}, // { appid: boolean }
// 省市区
regionOptions: {
provinces: [],
cities: [],
areas: []
},
// 类目
categoryOptions: [],
categoryLoading: false,
// 发品结果
publishResult: {
taskId: null
},
publishProgress: 0,
publishItems: [],
refreshTimer: null,
// 历史记录
historyVisible: false,
historyList: [],
historyTotal: 0,
historyQuery: {
pageNum: 1,
pageSize: 10
},
// 任务详情
detailVisible: false,
currentTask: null,
taskItems: [],
// 文案预览
wenanPreviewVisible: false,
currentPreviewProduct: null,
// 完整文案查看
fullWenanVisible: false,
currentFullWenan: null
};
},
computed: {
// 计算总子账号数
totalSubAccountCount() {
return Object.values(this.publishForm.accountSubAccounts)
.reduce((sum, subAccounts) => sum + subAccounts.length, 0);
},
// 计算总发布数
totalPublishCount() {
return this.selectedProducts.length * this.totalSubAccountCount;
},
// 生成预览列表
previewList() {
const list = [];
for (const product of this.selectedProducts) {
for (const appid of this.publishForm.selectedMainAccounts) {
const accountName = this.erpAccounts.find(a => a.value === appid)?.label || appid;
const subAccounts = this.publishForm.accountSubAccounts[appid] || [];
for (const subAccount of subAccounts) {
const wenanType = product.wenan && product.wenan[product.selectedWenanIndex] ?
product.wenan[product.selectedWenanIndex].type : '无';
const wenanContent = product.wenan && product.wenan[product.selectedWenanIndex] ?
product.wenan[product.selectedWenanIndex].content : '';
list.push({
skuid: product.skuid,
productName: product.productName,
productImage: product.productImage,
publishPrice: product.publishPrice || product.price,
accountName: accountName,
subAccount: subAccount,
wenanType: wenanType,
wenanContent: wenanContent
});
}
}
}
return list;
}
},
created() {
this.loadERPAccounts();
this.loadProvinces();
this.loadCategories();
},
watch: {
'publishForm.itemBizType'() {
this.onItemBizTypeChange();
},
'publishForm.spBizType'() {
this.onSpBizTypeChange();
}
},
beforeDestroy() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
}
},
methods: {
// 加载ERP账号
async loadERPAccounts() {
try {
const res = await getERPAccounts();
if (res.code === 200) {
this.erpAccounts = res.data || [];
}
} catch (error) {
console.error("加载ERP账号失败", error);
}
},
// 主账号变化
onMainAccountsChange(selectedAccounts) {
// 清理未选中账号的子账号数据
Object.keys(this.publishForm.accountSubAccounts).forEach(appid => {
if (!selectedAccounts.includes(appid)) {
this.$delete(this.publishForm.accountSubAccounts, appid);
this.$delete(this.subAccountsMap, appid);
this.$delete(this.loadingSubAccounts, appid);
}
});
// 为新选中的账号初始化数据
selectedAccounts.forEach(appid => {
if (!this.publishForm.accountSubAccounts[appid]) {
this.$set(this.publishForm.accountSubAccounts, appid, []);
}
if (!this.subAccountsMap[appid]) {
this.$set(this.subAccountsMap, appid, []);
// 自动加载子账号
this.loadSubAccountsForAccount(appid);
}
});
},
// 为指定主账号加载子账号
async loadSubAccountsForAccount(appid) {
// 如果已经加载过或正在加载,则跳过
if (this.subAccountsMap[appid]?.length > 0 || this.loadingSubAccounts[appid]) {
return;
}
this.$set(this.loadingSubAccounts, appid, true);
try {
const res = await getUsernames({ appid });
if (res.code === 200) {
this.$set(this.subAccountsMap, appid, res.data || []);
if ((res.data || []).length === 0) {
const accountName = this.erpAccounts.find(a => a.value === appid)?.label || appid;
this.$modal.msgWarning(`账号"${accountName}"下暂无可用的子账号`);
}
} else {
this.$modal.msgError(res.msg || "加载子账号失败");
}
} catch (error) {
console.error("加载子账号失败", error);
this.$modal.msgError("加载子账号失败请稍后重试");
} finally {
this.$set(this.loadingSubAccounts, appid, false);
}
},
// 加载省份
async loadProvinces() {
try {
const res = await getProvinces();
if (res.code === 200) {
this.regionOptions.provinces = res.data || [];
// 默认选择第一个省份
if (!this.publishForm.province && this.regionOptions.provinces.length) {
this.publishForm.province = this.regionOptions.provinces[0].value;
await this.loadCities(this.publishForm.province, true);
}
} else {
this.$modal.msgError(res.msg || '加载省份失败');
}
} catch (error) {
console.error('加载省份失败', error);
this.$modal.msgError('加载省份失败');
}
},
// 省份变化
async onProvinceChange() {
await this.loadCities(this.publishForm.province, false);
},
// 加载城市
async loadCities(provId, echo = false) {
if (!provId) {
this.regionOptions.cities = [];
this.regionOptions.areas = [];
this.publishForm.city = null;
this.publishForm.district = null;
return;
}
try {
const res = await getCities(provId);
if (res.code === 200) {
this.regionOptions.cities = res.data || [];
// 默认选择第一个城市
if (!this.publishForm.city && this.regionOptions.cities.length) {
this.publishForm.city = this.regionOptions.cities[0].value;
await this.loadAreas(provId, this.publishForm.city, true);
} else if (this.publishForm.city) {
await this.loadAreas(provId, this.publishForm.city, true);
} else {
this.regionOptions.areas = [];
this.publishForm.district = null;
}
} else {
this.$modal.msgError(res.msg || '加载城市失败');
}
} catch (error) {
console.error('加载城市失败', error);
this.$modal.msgError('加载城市失败');
}
},
// 城市变化
async onCityChange() {
await this.loadAreas(this.publishForm.province, this.publishForm.city, false);
},
// 加载区县
async loadAreas(provId, cityId, echo = false) {
if (!provId || !cityId) {
this.regionOptions.areas = [];
this.publishForm.district = null;
return;
}
try {
const res = await getAreas(provId, cityId);
if (res.code === 200) {
this.regionOptions.areas = res.data || [];
// 默认选择第一个区县
if (!this.publishForm.district && this.regionOptions.areas.length) {
this.publishForm.district = this.regionOptions.areas[0].value;
} else if (!echo) {
this.publishForm.district = null;
}
} else {
this.$modal.msgError(res.msg || '加载区县失败');
}
} catch (error) {
console.error('加载区县失败', error);
this.$modal.msgError('加载区县失败');
}
},
// 商品类型变化
async onItemBizTypeChange() {
this.categoryOptions = [];
this.publishForm.channelCatId = '';
await this.loadCategories();
},
// 行业类型变化
async onSpBizTypeChange() {
this.categoryOptions = [];
this.publishForm.channelCatId = '';
await this.loadCategories();
},
// 加载类目
async loadCategories() {
const itemBizType = this.publishForm.itemBizType;
const spBizType = this.publishForm.spBizType;
if (!itemBizType) {
return;
}
this.categoryLoading = true;
try {
const res = await getCategories({ itemBizType, spBizType });
if (res.code === 200) {
this.categoryOptions = res.data || [];
// 如果有类目且当前没有选中,自动选择第一个
if (!this.publishForm.channelCatId && this.categoryOptions.length) {
this.publishForm.channelCatId = this.categoryOptions[0].value;
}
} else {
this.$modal.msgError(res.msg || '加载类目失败');
}
} catch (error) {
console.error('加载类目失败', error);
this.$modal.msgError('加载类目失败');
} finally {
this.categoryLoading = false;
}
},
// 类目变化
onCategoryChange() {
// 类目变化后的回调(可用于加载属性等)
},
// 输入变化
onMessageInput() {
// 可以添加实时提示
},
// 解析消息
async parseMessage() {
if (!this.lineReportMessage.trim()) {
this.$modal.msgWarning("请输入线报消息");
return;
}
this.parsing = true;
try {
const res = await parseLineReport({ message: this.lineReportMessage });
if (res.code === 200) {
this.parsedProducts = (res.data || []).map(product => {
// 初始化商品数据
return {
...product,
publishPrice: product.price || 0, // 默认发布价格=京东价格
wenan: product.wenan || [], // 文案数组
selectedWenanIndex: 0, // 默认选择第一个文案
images: product.images || [] // 图片数组
};
});
if (this.parsedProducts.length === 0) {
this.$modal.msgWarning("未能识别到商品信息请检查消息内容");
} else {
this.$modal.msgSuccess(`成功解析 ${this.parsedProducts.length} 个商品`);
this.activeStep = 1;
}
} else {
this.$modal.msgError(res.msg || "解析失败");
}
} catch (error) {
console.error("解析失败", error);
this.$modal.msgError("解析失败请稍后重试");
} finally {
this.parsing = false;
}
},
// 选择变化
handleSelectionChange(selection) {
this.selectedProducts = selection;
},
// 全选
selectAll() {
this.$refs.productTable && this.$refs.productTable.toggleAllSelection();
},
// 取消全选
selectNone() {
this.$refs.productTable && this.$refs.productTable.clearSelection();
},
// 下一步
nextStep() {
if (this.activeStep === 1 && this.selectedProducts.length === 0) {
this.$modal.msgWarning("请至少选择一个商品");
return;
}
this.activeStep++;
},
// 上一步
prevStep() {
this.activeStep--;
},
// 提交发品
submitPublish() {
this.$refs.publishForm.validate(async (valid) => {
if (!valid) {
return;
}
// 验证是否选择了主账号
if (this.publishForm.selectedMainAccounts.length === 0) {
this.$modal.msgWarning("请至少选择一个主账号");
return;
}
// 验证每个主账号是否都选择了子账号
for (const appid of this.publishForm.selectedMainAccounts) {
const subAccounts = this.publishForm.accountSubAccounts[appid] || [];
if (subAccounts.length === 0) {
const accountName = this.erpAccounts.find(a => a.value === appid)?.label || appid;
this.$modal.msgWarning(`请为账号"${accountName}"选择至少一个子账号`);
return;
}
}
// 构建账号配置列表:[{targetAccount, subAccounts}]
const accountConfigs = this.publishForm.selectedMainAccounts.map(appid => ({
targetAccount: appid,
subAccounts: this.publishForm.accountSubAccounts[appid]
}));
this.publishing = true;
try {
const request = {
taskName: this.publishForm.taskName || `批量发品-${new Date().toLocaleString()}`,
originalMessage: this.lineReportMessage,
products: this.selectedProducts.map(p => ({
skuid: p.skuid,
productName: p.productName,
price: p.price,
publishPrice: p.publishPrice, // 【新增】发布价格
productImage: p.productImage,
images: p.images || [], // 【新增】图片数组
shopName: p.shopName,
shopId: p.shopId,
commission: p.commission,
commissionShare: p.commissionShare,
commissionInfo: p.commissionInfo,
// 【新增】文案信息
wenan: p.wenan || [],
selectedWenanIndex: p.selectedWenanIndex || 0
})),
accountConfigs: accountConfigs,
delaySeconds: this.publishForm.delaySeconds,
commonParams: {
userName: accountConfigs[0].subAccounts[0],
province: this.publishForm.province,
city: this.publishForm.city,
district: this.publishForm.district,
itemBizType: this.publishForm.itemBizType,
spBizType: this.publishForm.spBizType,
channelCatId: this.publishForm.channelCatId,
expressFee: this.publishForm.expressFee,
stock: this.publishForm.stock,
stuffStatus: this.publishForm.stuffStatus,
serviceSupport: this.publishForm.serviceSupport.join(","),
channelPv: this.publishForm.channelPv
}
};
const res = await batchPublish(request);
if (res.code === 200) {
this.publishResult.taskId = res.data;
this.$modal.msgSuccess("批量发品任务已创建");
this.activeStep = 5; // 跳转到第六步(发品进度)
this.startRefreshProgress();
} else {
this.$modal.msgError(res.msg || "提交失败");
}
} catch (error) {
console.error("提交失败", error);
this.$modal.msgError("提交失败请稍后重试");
} finally {
this.publishing = false;
}
});
},
// 开始刷新进度
startRefreshProgress() {
this.refreshProgress();
this.refreshTimer = setInterval(() => {
this.refreshProgress();
}, 2000);
},
// 刷新进度
async refreshProgress() {
if (!this.publishResult.taskId) {
return;
}
try {
const res = await listItems(this.publishResult.taskId);
if (res.code === 200) {
this.publishItems = res.data || [];
// 计算进度
const total = this.publishItems.length;
const finished = this.publishItems.filter(item => item.status >= 2).length;
this.publishProgress = total > 0 ? Math.round((finished / total) * 100) : 0;
// 如果全部完成,停止刷新
if (finished === total) {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
}
}
} catch (error) {
console.error("刷新进度失败", error);
}
},
// 重置
reset() {
this.activeStep = 0;
this.lineReportMessage = "";
this.parsedProducts = [];
this.selectedProducts = [];
this.publishResult = { taskId: null };
this.publishProgress = 0;
this.publishItems = [];
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
},
// 显示历史记录
async showHistory() {
this.historyVisible = true;
await this.loadHistory();
},
// 加载历史记录
async loadHistory() {
try {
const res = await listTasks(this.historyQuery);
if (res.code === 200) {
this.historyList = res.rows || [];
this.historyTotal = res.total || 0;
}
} catch (error) {
console.error("加载历史记录失败", error);
}
},
// 查看任务详情
async viewTaskDetail(task) {
try {
const res1 = await getTask(task.id);
if (res1.code === 200) {
this.currentTask = res1.data;
}
const res2 = await listItems(task.id);
if (res2.code === 200) {
this.taskItems = res2.data || [];
}
this.detailVisible = true;
} catch (error) {
console.error("加载任务详情失败", error);
this.$modal.msgError("加载任务详情失败");
}
},
// 触发后端重试该任务的未执行明细
async onRetryTask(task) {
try {
await this.$confirm(`确定要重试任务【${task.taskName || task.id}】的未执行明细吗?`, '提示', { type: 'warning' });
} catch {
return;
}
try {
const res = await retryTask(task.id);
if (res.code === 200) {
this.$modal.msgSuccess('已触发重试');
// 刷新历史列表
this.loadHistory();
// 若详情正在显示当前任务,刷新明细
if (this.detailVisible && this.currentTask && this.currentTask.id === task.id) {
const res2 = await listItems(task.id);
if (res2.code === 200) {
this.taskItems = res2.data || [];
}
}
} else {
this.$modal.msgError(res.msg || '触发重试失败');
}
} catch (e) {
this.$modal.msgError('触发重试失败');
}
},
// 解析目标账号
parseTargetAccounts(jsonStr) {
try {
return JSON.parse(jsonStr) || [];
} catch {
return [];
}
},
// 状态相关
getStatusType(status) {
const map = {
0: "info",
1: "warning",
2: "success",
3: "danger",
4: "warning",
5: "success",
6: "danger"
};
return map[status] || "info";
},
getStatusText(status) {
const map = {
0: "待发布",
1: "发布中",
2: "发布成功",
3: "发布失败",
4: "上架中",
5: "已上架",
6: "上架失败"
};
return map[status] || "未知";
},
getTaskStatusType(status) {
const map = {
0: "info",
1: "warning",
2: "success",
3: "danger"
};
return map[status] || "info";
},
getTaskStatusText(status) {
const map = {
0: "待处理",
1: "处理中",
2: "已完成",
3: "失败"
};
return map[status] || "未知";
},
// 预览文案
previewWenan(product) {
this.currentPreviewProduct = product;
this.wenanPreviewVisible = true;
},
// 复制文案
copyWenan() {
if (!this.currentPreviewProduct || !this.currentPreviewProduct.wenan ||
!this.currentPreviewProduct.wenan[this.currentPreviewProduct.selectedWenanIndex]) {
this.$modal.msgWarning('没有可复制的文案');
return;
}
const content = this.currentPreviewProduct.wenan[this.currentPreviewProduct.selectedWenanIndex].content;
// 使用现代浏览器的 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(content).then(() => {
this.$modal.msgSuccess('文案已复制到剪贴板');
}).catch(() => {
this.fallbackCopyToClipboard(content);
});
} else {
this.fallbackCopyToClipboard(content);
}
},
// 降级复制方案
fallbackCopyToClipboard(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
this.$modal.msgSuccess('文案已复制到剪贴板');
} catch (err) {
this.$modal.msgError('复制失败,请手动复制');
}
document.body.removeChild(textArea);
},
// 批量设置价格
batchSetPrice() {
if (this.selectedProducts.length === 0) {
this.$modal.msgWarning('请先选择商品');
return;
}
this.$prompt('请输入统一的发布价格', '批量设置', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^\d+(\.\d{1,2})?$/,
inputErrorMessage: '请输入有效的价格(最多两位小数)'
}).then(({ value }) => {
this.selectedProducts.forEach(p => {
p.publishPrice = parseFloat(value);
});
this.$modal.msgSuccess('批量设置成功');
}).catch(() => {});
},
// 批量选择文案版本
batchSetWenan() {
if (this.selectedProducts.length === 0) {
this.$modal.msgWarning('请先选择商品');
return;
}
// 找到第一个有文案的商品作为参考
const firstProductWithWenan = this.selectedProducts.find(p => p.wenan && p.wenan.length > 0);
if (!firstProductWithWenan) {
this.$modal.msgWarning('所有商品都没有可用的文案');
return;
}
// 构建选项
const options = firstProductWithWenan.wenan.map((w, index) => ({
label: w.type,
value: index
}));
this.$prompt('请选择文案版本(索引)', '批量设置', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputType: 'number',
inputPlaceholder: `请输入 0 到 ${options.length - 1} 之间的数字`
}).then(({ value }) => {
const index = parseInt(value);
if (isNaN(index) || index < 0 || index >= options.length) {
this.$modal.msgError(`请输入 0 到 ${options.length - 1} 之间的数字`);
return;
}
let successCount = 0;
this.selectedProducts.forEach(p => {
if (p.wenan && p.wenan.length > index) {
p.selectedWenanIndex = index;
successCount++;
}
});
this.$modal.msgSuccess(`成功设置 ${successCount} 个商品的文案版本为:${options[index].label}`);
}).catch(() => {});
},
// 查看完整文案(预览页面使用)
viewFullWenan(row) {
this.currentFullWenan = row;
this.fullWenanVisible = true;
},
// 复制完整文案
copyFullWenan() {
if (!this.currentFullWenan || !this.currentFullWenan.wenanContent) {
this.$modal.msgWarning('没有可复制的文案');
return;
}
const content = this.currentFullWenan.wenanContent;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(content).then(() => {
this.$modal.msgSuccess('文案已复制到剪贴板');
}).catch(() => {
this.fallbackCopyToClipboard(content);
});
} else {
this.fallbackCopyToClipboard(content);
}
}
}
};
</script>
<style scoped>
.step-content {
min-height: 400px;
padding: 20px 0;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
}
.clearfix:after {
clear: both;
}
</style>