461 lines
16 KiB
Vue
461 lines
16 KiB
Vue
<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;">一键操作:粘贴文案 → 自动创建礼金 → 输出替换后的文案</span>
|
||
</div>
|
||
|
||
<!-- 配置区域 -->
|
||
<el-form :model="form" :rules="rules" ref="form" inline style="margin-bottom: 15px;">
|
||
<el-form-item label="礼金金额" prop="amount">
|
||
<el-input-number v-model="form.amount" :min="1" :max="50" :precision="2" :step="0.01" style="width: 120px" />
|
||
<span style="margin-left: 5px; color: #909399;">元</span>
|
||
</el-form-item>
|
||
<el-form-item label="每张数量" prop="quantity">
|
||
<el-input-number v-model="form.quantity" :min="1" :max="100" style="width: 120px" />
|
||
<span style="margin-left: 5px; color: #909399;">个</span>
|
||
</el-form-item>
|
||
<el-form-item label="商品类型" prop="owner">
|
||
<el-radio-group v-model="form.owner">
|
||
<el-radio label="g">自营</el-radio>
|
||
<el-radio label="pop">POP</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item style="margin-left: 20px;">
|
||
<el-tag v-if="detectedUrls.length > 0" type="success" size="medium">
|
||
<i class="el-icon-link"></i> 已识别 {{ detectedUrls.length }} 个URL
|
||
</el-tag>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<!-- 左右两个文本框 -->
|
||
<el-row :gutter="20">
|
||
<!-- 左侧:输入文案 -->
|
||
<el-col :span="12">
|
||
<div class="text-panel">
|
||
<div class="panel-header">
|
||
<span style="font-weight: bold; font-size: 16px;">
|
||
<i class="el-icon-edit-outline"></i> 输入原始文案
|
||
</span>
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
@click="handleProcess"
|
||
:loading="processing"
|
||
:disabled="detectedUrls.length === 0"
|
||
>
|
||
<i class="el-icon-magic-stick"></i> 一键生成 ({{ detectedUrls.length }}个)
|
||
</el-button>
|
||
</div>
|
||
<el-input
|
||
type="textarea"
|
||
:rows="25"
|
||
v-model="content"
|
||
placeholder="💡 将包含京东链接的完整推广文案粘贴到这里 示例: 🔴【海尔电热水器】11月9号晚8好价~ ✅9折券:商品页面直接领取 1️⃣海尔无镁棒BK5PLUS(60升) 下单:https://u.jd.com/T1G7978 👉300券+388红包 2️⃣海尔无镁棒BK5PLUS(80升) 下单:https://u.jd.com/TrG7lCN 👉300券+388红包 ✨ 系统会自动识别所有京东链接并替换!"
|
||
class="textarea-input"
|
||
/>
|
||
</div>
|
||
</el-col>
|
||
|
||
<!-- 右侧:输出结果 -->
|
||
<el-col :span="12">
|
||
<div class="text-panel">
|
||
<div class="panel-header">
|
||
<span style="font-weight: bold; font-size: 16px;">
|
||
<i class="el-icon-document-checked"></i> 替换后的文案
|
||
</span>
|
||
<el-button
|
||
type="success"
|
||
size="small"
|
||
@click="copyResult"
|
||
:disabled="!result || !result.replacedContent"
|
||
>
|
||
<i class="el-icon-document-copy"></i> 复制结果
|
||
</el-button>
|
||
</div>
|
||
<el-input
|
||
v-if="!processing && result && result.replacedContent"
|
||
type="textarea"
|
||
:rows="25"
|
||
v-model="result.replacedContent"
|
||
readonly
|
||
class="textarea-output"
|
||
/>
|
||
<div v-else-if="processing" class="loading-container">
|
||
<el-progress
|
||
:percentage="progress"
|
||
:status="progressStatus"
|
||
:stroke-width="15"
|
||
style="width: 80%;"
|
||
>
|
||
<template slot="format">
|
||
{{ progressText }}
|
||
</template>
|
||
</el-progress>
|
||
<div style="margin-top: 15px; color: #909399; font-size: 14px;">
|
||
{{ progressDetail }}
|
||
</div>
|
||
</div>
|
||
<div v-else class="empty-container">
|
||
<i class="el-icon-document" style="font-size: 64px; color: #DCDFE6; margin-bottom: 10px;"></i>
|
||
<div style="color: #909399; font-size: 14px;">点击左侧"一键生成"按钮后,替换结果将显示在这里</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 统计信息 -->
|
||
<div v-if="result && result.replacedContent" style="margin-top: 15px;">
|
||
<el-alert
|
||
:type="result.replacedCount === result.totalUrls ? 'success' : (result.replacedCount > 0 ? 'warning' : 'error')"
|
||
:closable="false"
|
||
>
|
||
<template slot="title">
|
||
<span style="font-weight: bold;">
|
||
<i :class="result.replacedCount === result.totalUrls ? 'el-icon-success' : (result.replacedCount > 0 ? 'el-icon-warning' : 'el-icon-error')"></i> 处理完成!
|
||
成功替换 {{ result.replacedCount || 0 }} / {{ result.totalUrls || 0 }} 个URL
|
||
</span>
|
||
</template>
|
||
</el-alert>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 详细结果展示区域 -->
|
||
<el-row v-if="result && result.replacements && result.replacements.length > 0" style="margin-top: 20px;">
|
||
<el-col :span="24">
|
||
<el-card shadow="never">
|
||
<div slot="header">
|
||
<span style="font-weight: bold; font-size: 16px;">
|
||
<i class="el-icon-document"></i> 详细处理结果
|
||
</span>
|
||
<el-button
|
||
type="text"
|
||
size="small"
|
||
style="float: right;"
|
||
@click="showDetailResults = !showDetailResults"
|
||
>
|
||
{{ showDetailResults ? '收起' : '展开' }}
|
||
<i :class="showDetailResults ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
|
||
</el-button>
|
||
</div>
|
||
<div v-show="showDetailResults">
|
||
<el-table
|
||
:data="result.replacements"
|
||
stripe
|
||
border
|
||
style="width: 100%"
|
||
:default-sort="{prop: 'index', order: 'ascending'}"
|
||
>
|
||
<el-table-column prop="index" label="序号" width="80" align="center" sortable />
|
||
<el-table-column prop="skuName" label="商品名称" min-width="200" show-overflow-tooltip>
|
||
<template slot-scope="scope">
|
||
<span v-if="scope.row.skuName">{{ scope.row.skuName }}</span>
|
||
<span v-else style="color: #909399;">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="originalUrl" label="原始链接" min-width="250" show-overflow-tooltip>
|
||
<template slot-scope="scope">
|
||
<el-link :href="scope.row.originalUrl" target="_blank" type="primary" :underline="false">
|
||
{{ scope.row.originalUrl }}
|
||
</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="newUrl" label="新链接" min-width="250" show-overflow-tooltip>
|
||
<template slot-scope="scope">
|
||
<el-link
|
||
v-if="scope.row.success && scope.row.newUrl"
|
||
:href="scope.row.newUrl"
|
||
target="_blank"
|
||
type="success"
|
||
:underline="false"
|
||
>
|
||
{{ scope.row.newUrl }}
|
||
</el-link>
|
||
<span v-else style="color: #909399;">未替换</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="success" label="状态" width="100" align="center">
|
||
<template slot-scope="scope">
|
||
<el-tag :type="scope.row.success ? 'success' : 'danger'" size="small">
|
||
<i :class="scope.row.success ? 'el-icon-success' : 'el-icon-error'"></i>
|
||
{{ scope.row.success ? '成功' : '失败' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="error" label="错误信息" min-width="300" show-overflow-tooltip>
|
||
<template slot-scope="scope">
|
||
<span v-if="scope.row.error" style="color: #F56C6C;">{{ scope.row.error }}</span>
|
||
<span v-else style="color: #67C23A;">✓ 处理成功</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="giftCouponKey" label="礼金券Key" min-width="150" show-overflow-tooltip>
|
||
<template slot-scope="scope">
|
||
<span v-if="scope.row.giftCouponKey" style="color: #409EFF;">{{ scope.row.giftCouponKey }}</span>
|
||
<span v-else style="color: #909399;">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { replaceUrlsWithGiftCoupons } from '@/api/system/jdorder'
|
||
|
||
export default {
|
||
name: 'BatchGiftCoupon',
|
||
data() {
|
||
return {
|
||
content: '',
|
||
form: {
|
||
amount: 1.8,
|
||
quantity: 12,
|
||
owner: 'g'
|
||
},
|
||
rules: {
|
||
amount: [{ required: true, message: '请输入礼金金额', trigger: 'blur' }],
|
||
quantity: [{ required: true, message: '请输入礼金数量', trigger: 'blur' }]
|
||
},
|
||
processing: false,
|
||
progress: 0,
|
||
progressText: '',
|
||
progressDetail: '',
|
||
progressStatus: '',
|
||
result: null,
|
||
detectedUrls: [],
|
||
showDetailResults: true
|
||
}
|
||
},
|
||
watch: {
|
||
content(newVal) {
|
||
this.detectUrls(newVal)
|
||
}
|
||
},
|
||
methods: {
|
||
/** 检测文本中的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 handleProcess() {
|
||
if (!this.$refs.form) {
|
||
return
|
||
}
|
||
|
||
this.$refs.form.validate(async (valid) => {
|
||
if (!valid) 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} 个商品链接,将自动批量创建 ${this.detectedUrls.length} 张 ${this.form.amount} 元礼金券并替换文案中的URL。是否继续?`,
|
||
'确认批量创建',
|
||
{
|
||
confirmButtonText: '确定创建',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
|
||
this.processing = true
|
||
this.progress = 20
|
||
this.progressText = '正在处理...'
|
||
this.progressDetail = '后端正在为每个URL单独创建礼金券,请耐心等待...'
|
||
this.progressStatus = ''
|
||
this.result = null
|
||
|
||
try {
|
||
// 调用后端接口,后端会为每个URL单独处理
|
||
const params = {
|
||
content: this.content,
|
||
amount: this.form.amount,
|
||
quantity: this.form.quantity,
|
||
owner: this.form.owner || 'g'
|
||
}
|
||
|
||
this.progress = 40
|
||
const res = await replaceUrlsWithGiftCoupons(params)
|
||
|
||
this.progress = 80
|
||
|
||
if (res && res.code === 200 && res.data) {
|
||
this.result = res.data
|
||
|
||
const successCount = this.result.replacedCount || 0
|
||
const totalCount = this.result.totalUrls || 0
|
||
|
||
this.progress = 100
|
||
this.progressStatus = successCount === totalCount ? 'success' : (successCount > 0 ? 'warning' : 'exception')
|
||
this.progressText = successCount === totalCount ? '完成!' : '部分成功'
|
||
this.progressDetail = `成功替换 ${successCount} / ${totalCount} 个URL`
|
||
|
||
if (successCount > 0) {
|
||
this.$modal.msgSuccess(`✅ 批量替换完成!成功 ${successCount} / ${totalCount} 个`)
|
||
} else {
|
||
this.$modal.msgError('批量替换失败,所有URL处理均失败')
|
||
}
|
||
} 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;
|
||
}
|
||
|
||
.text-panel {
|
||
border: 2px solid #DCDFE6;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.panel-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 15px 20px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
}
|
||
|
||
.textarea-input, .textarea-output {
|
||
border: none !important;
|
||
border-radius: 0 !important;
|
||
}
|
||
|
||
.textarea-input >>> textarea {
|
||
border: none !important;
|
||
border-radius: 0 !important;
|
||
font-size: 14px;
|
||
line-height: 1.8;
|
||
padding: 20px;
|
||
}
|
||
|
||
.textarea-output >>> textarea {
|
||
border: none !important;
|
||
border-radius: 0 !important;
|
||
font-size: 14px;
|
||
line-height: 1.8;
|
||
padding: 20px;
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
.loading-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 550px;
|
||
padding: 40px;
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
.empty-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 550px;
|
||
background: #fafafa;
|
||
}
|
||
</style>
|