Files
ruoyi-vue/src/views/system/giftcoupon/batch.vue
2025-11-09 00:46:13 +08:00

461 lines
16 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="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="💡 将包含京东链接的完整推广文案粘贴到这里&#10;&#10;示例:&#10;🔴【海尔电热水器】11月9号晚8好价&#10;✅9折券商品页面直接领取&#10;&#10;1⃣海尔无镁棒BK5PLUS60升&#10;下单https://u.jd.com/T1G7978&#10;👉300券+388红包&#10;&#10;2⃣海尔无镁棒BK5PLUS80升&#10;下单https://u.jd.com/TrG7lCN&#10;👉300券+388红包&#10;&#10;✨ 系统会自动识别所有京东链接并替换!"
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>