1524 lines
55 KiB
Vue
1524 lines
55 KiB
Vue
<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>
|
||
|