1
This commit is contained in:
@@ -130,6 +130,24 @@ export function transferWithGift(data) {
|
||||
})
|
||||
}
|
||||
|
||||
// 批量创建礼金券
|
||||
export function batchCreateGiftCoupons(data) {
|
||||
return request({
|
||||
url: '/jarvis/jdorder/batchCreateGiftCoupons',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 文本URL替换(批量创建礼金并替换)
|
||||
export function replaceUrlsWithGiftCoupons(data) {
|
||||
return request({
|
||||
url: '/jarvis/jdorder/replaceUrlsWithGiftCoupons',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 导出JD订单列表
|
||||
export function exportJDOrders(query) {
|
||||
return request({
|
||||
|
||||
492
src/views/system/giftcoupon/batch.vue
Normal file
492
src/views/system/giftcoupon/batch.vue
Normal file
@@ -0,0 +1,492 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="never">
|
||||
<div slot="header">
|
||||
<span style="font-weight: bold; font-size: 18px;">批量创建礼金并替换URL</span>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<span style="color: #909399; font-size: 14px;">支持批量创建20张1元面额礼金券,并自动将文本中的URL替换为包含礼金的推广链接</span>
|
||||
</div>
|
||||
|
||||
<!-- 商品信息配置区域 -->
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="120px" style="margin-bottom: 20px;">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商品链接/SKU" prop="materialUrl">
|
||||
<el-input
|
||||
v-model="form.materialUrl"
|
||||
placeholder="请输入商品链接或SKU ID,必须先点击查询按钮获取商品信息"
|
||||
@keyup.enter.native="queryProductInfo"
|
||||
>
|
||||
<el-button slot="append" icon="el-icon-search" @click="queryProductInfo" :loading="queryLoading">查询</el-button>
|
||||
</el-input>
|
||||
<div style="color: #E6A23C; font-size: 12px; margin-top: 5px;">
|
||||
<i class="el-icon-warning"></i>
|
||||
<strong>重要:</strong>请先点击"查询"按钮获取商品信息(特别是SKU ID),然后再创建礼金。
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商品名称">
|
||||
<el-input v-model="form.skuName" placeholder="查询商品信息后自动填充" :disabled="true" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="商品类型">
|
||||
<el-input :value="form.owner === 'g' ? '自营' : (form.owner === 'pop' ? 'POP' : '未查询')" disabled>
|
||||
<template slot="prepend">
|
||||
<i :class="form.owner === 'g' ? 'el-icon-success' : (form.owner === 'pop' ? 'el-icon-warning' : 'el-icon-question')"
|
||||
:style="{color: form.owner === 'g' ? '#67C23A' : (form.owner === 'pop' ? '#409EFF' : '#909399')}"></i>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="礼金金额(元)" prop="amount">
|
||||
<el-input-number v-model="form.amount" :min="1" :max="50" :precision="2" :step="0.01" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="每张礼金数量" prop="quantity">
|
||||
<el-input-number v-model="form.quantity" :min="1" :max="100" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="批量创建数量" prop="batchSize">
|
||||
<el-input-number v-model="form.batchSize" :min="1" :max="100" style="width: 100%" />
|
||||
<div style="color: #909399; font-size: 12px; margin-top: 5px;">默认20张</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<!-- 文本输入区域 -->
|
||||
<el-divider content-position="left">文本内容</el-divider>
|
||||
<el-form-item label="输入文本" style="margin-bottom: 10px;">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
v-model="content"
|
||||
placeholder="请输入包含京东商品链接的文本内容,系统会自动识别URL并替换为包含礼金的推广链接"
|
||||
style="margin-bottom: 10px;"
|
||||
/>
|
||||
<div style="color: #909399; font-size: 12px; margin-bottom: 10px;">
|
||||
<i class="el-icon-info"></i>
|
||||
系统会自动识别文本中的京东链接(包括 https://item.jd.com、u.jd.com 等格式),并批量创建对应数量的礼金券进行替换
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<span style="color: #909399; margin-right: 10px;">已识别URL数量: <strong style="color: #409EFF;">{{ detectedUrls.length }}</strong></span>
|
||||
<el-button type="primary" @click="handleReplace" :loading="processing" :disabled="detectedUrls.length === 0 || !form.queryResult">
|
||||
<i class="el-icon-magic-stick"></i> 批量创建并替换
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 进度显示 -->
|
||||
<el-card v-if="processing" shadow="never" style="margin-top: 20px;">
|
||||
<el-progress :percentage="progress" :status="progressStatus" :stroke-width="20">
|
||||
<template slot="format">
|
||||
{{ progressText }}
|
||||
</template>
|
||||
</el-progress>
|
||||
<div style="margin-top: 10px; text-align: center; color: #909399;">
|
||||
{{ progressDetail }}
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 结果展示区域 -->
|
||||
<el-card v-if="result && result.replacedContent" shadow="never" style="margin-top: 20px;">
|
||||
<div slot="header">
|
||||
<span style="font-weight: bold;">替换结果</span>
|
||||
<el-button style="float: right; padding: 3px 0;" type="text" @click="copyResult">
|
||||
<i class="el-icon-document-copy"></i> 复制结果
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<el-row :gutter="20" style="margin-bottom: 20px;">
|
||||
<el-col :span="6">
|
||||
<el-statistic title="总URL数" :value="result.totalUrls || 0" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="成功替换" :value="result.replacedCount || 0">
|
||||
<template slot="suffix">
|
||||
<span style="color: #67C23A;">个</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="失败数量" :value="(result.totalUrls || 0) - (result.replacedCount || 0)">
|
||||
<template slot="suffix">
|
||||
<span style="color: #F56C6C;">个</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="成功率" :value="result.totalUrls > 0 ? Math.round((result.replacedCount / result.totalUrls) * 100) : 0">
|
||||
<template slot="suffix">
|
||||
<span style="color: #409EFF;">%</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 替换后的文本 -->
|
||||
<el-form-item label="替换后的文本">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
v-model="result.replacedContent"
|
||||
readonly
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 替换详情表格 -->
|
||||
<el-table :data="result.replacements || []" border style="margin-top: 20px;" max-height="300">
|
||||
<el-table-column type="index" label="序号" width="60" />
|
||||
<el-table-column label="原始URL" prop="originalUrl" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column label="新URL" prop="newUrl" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.success ? 'success' : 'danger'">
|
||||
{{ scope.row.success ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="礼金Key" prop="giftCouponKey" width="200" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.giftCouponKey">{{ scope.row.giftCouponKey }}</span>
|
||||
<span v-else style="color: #909399;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="copyToClipboard(scope.row.newUrl || scope.row.originalUrl)"
|
||||
:disabled="!scope.row.success"
|
||||
>
|
||||
复制新URL
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generatePromotionContent, replaceUrlsWithGiftCoupons } from '@/api/system/jdorder'
|
||||
|
||||
export default {
|
||||
name: 'BatchGiftCoupon',
|
||||
data() {
|
||||
return {
|
||||
content: '',
|
||||
form: {
|
||||
materialUrl: '',
|
||||
skuName: '',
|
||||
owner: 'g',
|
||||
amount: 1.0, // 默认1元
|
||||
quantity: 1, // 默认每个礼金数量为1
|
||||
batchSize: 20, // 默认批量创建20张
|
||||
queryResult: null // 保存查询到的商品信息
|
||||
},
|
||||
rules: {
|
||||
materialUrl: [{ required: true, message: '请输入商品链接或SKU', trigger: 'blur' }],
|
||||
amount: [{ required: true, message: '请输入礼金金额', trigger: 'blur' }],
|
||||
quantity: [{ required: true, message: '请输入礼金数量', trigger: 'blur' }],
|
||||
batchSize: [{ required: true, message: '请输入批量创建数量', trigger: 'blur' }]
|
||||
},
|
||||
queryLoading: false,
|
||||
processing: false,
|
||||
progress: 0,
|
||||
progressText: '',
|
||||
progressDetail: '',
|
||||
progressStatus: '',
|
||||
result: null,
|
||||
detectedUrls: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
content(newVal) {
|
||||
this.detectUrls(newVal)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 查询商品信息 */
|
||||
async queryProductInfo() {
|
||||
const materialUrl = this.form.materialUrl.trim()
|
||||
if (!materialUrl) {
|
||||
this.$modal.msgWarning('请输入商品链接或SKU ID')
|
||||
return
|
||||
}
|
||||
|
||||
this.queryLoading = true
|
||||
try {
|
||||
const res = await generatePromotionContent({ promotionContent: materialUrl })
|
||||
|
||||
if (res && res.code === 200) {
|
||||
let productInfo = null
|
||||
try {
|
||||
let resultStr = res.data || res.msg
|
||||
let parsed = null
|
||||
if (typeof resultStr === 'string') {
|
||||
parsed = JSON.parse(resultStr)
|
||||
} else {
|
||||
parsed = resultStr
|
||||
}
|
||||
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
productInfo = parsed[0]
|
||||
} else if (parsed && typeof parsed === 'object') {
|
||||
if (parsed.list && Array.isArray(parsed.list) && parsed.list.length > 0) {
|
||||
productInfo = parsed.list[0]
|
||||
} else if (parsed.data && Array.isArray(parsed.data) && parsed.data.length > 0) {
|
||||
productInfo = parsed.data[0]
|
||||
} else if (parsed.materialUrl || parsed.owner || parsed.skuName) {
|
||||
productInfo = parsed
|
||||
}
|
||||
}
|
||||
|
||||
if (productInfo) {
|
||||
this.form.queryResult = productInfo
|
||||
this.form.skuName = productInfo.skuName || productInfo.title || productInfo.productName || productInfo.cleanSkuName || ''
|
||||
const ownerValue = productInfo.owner || (productInfo.popId ? 'pop' : 'g') || 'g'
|
||||
this.form.owner = ownerValue === 'p' ? 'pop' : (ownerValue === 'pop' ? 'pop' : 'g')
|
||||
|
||||
this.$modal.msgSuccess('商品信息查询成功:' + (this.form.owner === 'g' ? '自营' : 'POP'))
|
||||
} else {
|
||||
this.$modal.msgWarning('未找到商品信息,请检查链接是否正确')
|
||||
}
|
||||
} catch (e) {
|
||||
this.$modal.msgWarning('返回数据格式异常:' + e.message)
|
||||
}
|
||||
} else {
|
||||
this.$modal.msgError('查询商品信息失败:' + (res.msg || '未知错误'))
|
||||
}
|
||||
} catch (e) {
|
||||
this.$modal.msgError('查询失败:' + (e.message || '未知错误'))
|
||||
} finally {
|
||||
this.queryLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
/** 检测文本中的URL */
|
||||
detectUrls(text) {
|
||||
if (!text || text.trim().length === 0) {
|
||||
this.detectedUrls = []
|
||||
return
|
||||
}
|
||||
|
||||
const urlPattern = /(https?:\/\/[^\s]+)|(u\.jd\.com\/[^\s]+)/gi
|
||||
const urls = []
|
||||
let match
|
||||
|
||||
while ((match = urlPattern.exec(text)) !== null) {
|
||||
const url = match[0]
|
||||
if (url && !urls.includes(url.trim())) {
|
||||
urls.push(url.trim())
|
||||
}
|
||||
}
|
||||
|
||||
this.detectedUrls = urls
|
||||
},
|
||||
|
||||
/** 批量创建并替换 */
|
||||
async handleReplace() {
|
||||
if (!this.$refs.form) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$refs.form.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
if (!this.form.queryResult) {
|
||||
this.$modal.msgWarning('请先查询商品信息')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.detectedUrls.length === 0) {
|
||||
this.$modal.msgWarning('文本中未找到URL,无需替换')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.detectedUrls.length > 100) {
|
||||
this.$modal.msgError('检测到的URL数量超过100个,请分批处理')
|
||||
return
|
||||
}
|
||||
|
||||
// 确认操作
|
||||
try {
|
||||
await this.$confirm(
|
||||
`检测到 ${this.detectedUrls.length} 个URL,将批量创建 ${this.detectedUrls.length} 张 ${this.form.amount} 元礼金券并替换URL。是否继续?`,
|
||||
'确认操作',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
this.processing = true
|
||||
this.progress = 0
|
||||
this.progressText = '准备中...'
|
||||
this.progressDetail = ''
|
||||
this.progressStatus = ''
|
||||
this.result = null
|
||||
|
||||
try {
|
||||
// 构建请求参数
|
||||
const params = {
|
||||
content: this.content,
|
||||
amount: this.form.amount,
|
||||
quantity: this.form.quantity,
|
||||
batchSize: this.detectedUrls.length, // 根据检测到的URL数量自动设置
|
||||
owner: this.form.owner || 'g',
|
||||
skuName: this.form.skuName || ''
|
||||
}
|
||||
|
||||
// 根据查询结果设置skuId或materialUrl
|
||||
if (this.form.queryResult) {
|
||||
const queryResult = this.form.queryResult
|
||||
const isPop = this.form.owner === 'pop'
|
||||
|
||||
if (isPop) {
|
||||
if (queryResult.materialUrl) {
|
||||
params.materialUrl = queryResult.materialUrl
|
||||
} else if ((queryResult.skuId || queryResult.skuid) && /^\d+$/.test(String(queryResult.skuId || queryResult.skuid))) {
|
||||
params.skuId = String(queryResult.skuId || queryResult.skuid)
|
||||
}
|
||||
} else {
|
||||
const skuIdValue = queryResult.skuId || queryResult.skuid
|
||||
if (skuIdValue && /^\d+$/.test(String(skuIdValue))) {
|
||||
params.skuId = String(skuIdValue)
|
||||
} else if (queryResult.materialUrl) {
|
||||
params.materialUrl = queryResult.materialUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有设置skuId或materialUrl,使用用户输入的
|
||||
if (!params.skuId && !params.materialUrl) {
|
||||
const materialUrl = this.form.materialUrl.trim()
|
||||
if (materialUrl) {
|
||||
if (/^\d+$/.test(materialUrl)) {
|
||||
params.skuId = materialUrl
|
||||
} else {
|
||||
params.materialUrl = materialUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('批量替换请求参数:', params)
|
||||
|
||||
// 调用替换接口
|
||||
this.progress = 30
|
||||
this.progressText = '正在批量创建礼金券...'
|
||||
this.progressDetail = '这可能需要几分钟时间,请耐心等待'
|
||||
|
||||
const res = await replaceUrlsWithGiftCoupons(params)
|
||||
|
||||
this.progress = 80
|
||||
this.progressText = '正在处理结果...'
|
||||
|
||||
if (res && res.code === 200 && res.data) {
|
||||
this.result = res.data
|
||||
this.progress = 100
|
||||
this.progressStatus = 'success'
|
||||
this.progressText = '完成!'
|
||||
this.progressDetail = `成功替换 ${this.result.replacedCount || 0} / ${this.result.totalUrls || 0} 个URL`
|
||||
|
||||
this.$modal.msgSuccess(`批量替换完成!成功 ${this.result.replacedCount || 0} / ${this.result.totalUrls || 0} 个`)
|
||||
} else {
|
||||
this.progress = 100
|
||||
this.progressStatus = 'exception'
|
||||
this.progressText = '失败'
|
||||
this.progressDetail = res.msg || '未知错误'
|
||||
this.$modal.msgError('批量替换失败:' + (res.msg || '未知错误'))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('批量替换异常', e)
|
||||
this.progress = 100
|
||||
this.progressStatus = 'exception'
|
||||
this.progressText = '失败'
|
||||
this.progressDetail = e.message || '未知错误'
|
||||
|
||||
let errorMsg = '未知错误'
|
||||
if (e.response && e.response.data) {
|
||||
errorMsg = e.response.data.msg || e.response.data.message || JSON.stringify(e.response.data)
|
||||
} else if (e.message) {
|
||||
errorMsg = e.message
|
||||
}
|
||||
|
||||
this.$modal.msgError('批量替换失败:' + errorMsg)
|
||||
} finally {
|
||||
this.processing = false
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/** 复制结果 */
|
||||
copyResult() {
|
||||
if (this.result && this.result.replacedContent) {
|
||||
this.copyToClipboard(this.result.replacedContent)
|
||||
}
|
||||
},
|
||||
|
||||
/** 复制到剪贴板 */
|
||||
copyToClipboard(text) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
this.$modal.msgSuccess('复制成功')
|
||||
}).catch(() => {
|
||||
this.fallbackCopyToClipboard(text)
|
||||
})
|
||||
} else {
|
||||
this.fallbackCopyToClipboard(text)
|
||||
}
|
||||
},
|
||||
|
||||
/** 降级复制方法 */
|
||||
fallbackCopyToClipboard(text) {
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = text
|
||||
textArea.style.position = 'fixed'
|
||||
textArea.style.opacity = '0'
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
this.$modal.msgSuccess('复制成功')
|
||||
} catch (err) {
|
||||
this.$modal.msgError('复制失败')
|
||||
}
|
||||
document.body.removeChild(textArea)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.el-divider {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.el-statistic {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user