Files
ruoyi-vue/src/views/system/jd-instruction/fadan-quick-record.vue
2026-04-09 12:52:32 +08:00

455 lines
15 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 fadan-quick-record" :class="{ 'fadan-quick-record--mobile': isMobile }">
<el-card class="box-card">
<el-form :model="form" label-position="top" class="fadan-form">
<el-form-item label="分销标记">
<div class="mark-row">
<span class="mark-prefix" aria-hidden="true">F-</span>
<el-input
v-model="form.markSuffix"
class="mark-suffix-input"
placeholder="后缀,如 李旭、闲鱼;留空则仅为 F"
clearable
/>
</div>
</el-form-item>
<el-form-item label="型号" required>
<el-input v-model="form.model" placeholder="必填" clearable />
</el-form-item>
<el-form-item label="下单地址" required>
<el-input v-model="form.address" type="textarea" :rows="3" placeholder="必填,与中控「生」指令第 6 行一致" clearable />
</el-form-item>
<el-form-item label="转链链接(可空)">
<el-input v-model="form.link" type="textarea" :rows="2" placeholder="可空;留空则后台按型号尝试自动填充京粉链接" clearable />
</el-form-item>
<el-form-item label="数量">
<el-input-number v-model="form.qty" :min="1" :max="99" controls-position="right" :style="qtyInputStyle" />
</el-form-item>
<el-divider content-position="left">落库必填与中控录单一致</el-divider>
<el-form-item label="下单人">
<el-input v-model="form.buyer" placeholder="落库必填" clearable />
</el-form-item>
<el-form-item label="下单付款(元)">
<el-input v-model="form.paymentText" placeholder="落库必填,如 2999.00" clearable />
</el-form-item>
<el-form-item label="后返金额(元)">
<el-input v-model="form.rebateText" placeholder="不写则按 0.00 落库" clearable />
</el-form-item>
<el-form-item label="订单号(京东)">
<el-input v-model="form.orderIdText" placeholder="落库必填,与表单「订单号(需填)」一致" clearable />
</el-form-item>
<el-form-item label="物流链接">
<el-input v-model="form.logisticsLink" type="textarea" :rows="2" placeholder="落库必填" clearable />
</el-form-item>
<div class="btn-row">
<el-button type="primary" size="medium" :loading="loading" @click="generate">录单</el-button>
<el-button size="medium" @click="resetForm">重置表单</el-button>
<el-button v-if="resultText" size="medium" @click="copyResult">复制录单</el-button>
</div>
</el-form>
<template v-if="resultText">
<el-divider content-position="left">录单结果</el-divider>
<el-input v-model="resultText" type="textarea" :rows="isMobile ? 16 : 20" readonly class="result-area" />
</template>
</el-card>
<el-dialog
title="地址/单号重复验证"
:visible.sync="verifyDialogVisible"
:width="dialogWidth"
:close-on-click-modal="false"
:close-on-press-escape="false"
append-to-body
>
<div class="verify-body">
<el-alert :title="verifyMessage" type="warning" :closable="false" />
<div class="verify-code">{{ verifyCode }}</div>
<el-input v-model="verifyInput" placeholder="请输入上方四位验证码" maxlength="4" @keyup.enter.native="handleVerify" />
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="verifyDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="verifyLoading" @click="handleVerify">确认录单</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { executeInstruction, executeInstructionWithForce } from '@/api/system/instruction'
export default {
name: 'FadanQuickRecord',
props: {
isMobile: {
type: Boolean,
default: false
}
},
data() {
return {
form: {
markSuffix: '',
model: '',
address: '',
link: '',
qty: 1,
buyer: '',
paymentText: '',
rebateText: '',
orderIdText: '',
logisticsLink: ''
},
loading: false,
resultText: '',
resultListFromLast: [],
verifyDialogVisible: false,
verifyCode: '',
verifyInput: '',
verifyMessage: '',
verifyLoading: false,
pendingCommand: ''
}
},
computed: {
dialogWidth() {
return this.isMobile ? '92%' : '480px'
},
qtyInputStyle() {
return this.isMobile
? { width: '100%' }
: { width: '200px' }
}
},
methods: {
buildDistributionMark() {
let s = (this.form.markSuffix || '').trim()
if (s.startsWith('-')) {
s = s.slice(1).trim()
}
if (!s) {
return 'F'
}
return 'F-' + s
},
/** 地址压成一行:换行/多空格合并为单空格,去掉中英文逗号 */
compressAddressOneLine(raw) {
if (raw == null) return ''
return String(raw)
.replace(/[,]/g, '')
.replace(/\s+/g, ' ')
.trim()
},
buildCommand() {
const mark = this.buildDistributionMark()
const model = (this.form.model || '').trim()
const address = this.compressAddressOneLine(this.form.address)
if (!model) {
this.$modal.msgError('请填写型号')
return null
}
if (!address) {
this.$modal.msgError('请填写地址')
return null
}
const link = (this.form.link || '').trim()
const qty = Number(this.form.qty) > 0 ? Number(this.form.qty) : 1
const lines = ['生', mark, model, link, String(qty), address]
return lines.join('\n')
},
injectOptional(raw) {
if (!raw) return raw
let t = String(raw).replace(/\r\n/g, '\n')
const buyer = (this.form.buyer || '').trim()
const pay = (this.form.paymentText || '').trim()
const rebate = (this.form.rebateText || '').trim()
const orderId = (this.form.orderIdText || '').trim()
const logistics = (this.form.logisticsLink || '').trim()
if (buyer) {
t = t.replace(/(下单人(需填):)\n\n/, `$1\n${buyer}\n\n`)
}
if (pay) {
t = t.replace(/(下单付款(注意核对):)\n\n/, `$1\n${pay}\n\n`)
}
if (rebate) {
t = t.replace(/(后返金额(注意核对):)\n\n/, `$1\n${rebate}\n\n`)
} else {
t = t.replace(/(后返金额(注意核对):)\n\n/, `$1\n0.00\n\n`)
}
if (orderId) {
t = t.replace(/(订单号(需填):)\n\n/, `$1\n${orderId}\n\n`)
}
if (logistics) {
t = t.replace(/(物流链接(需填):)\n\n/, `$1\n${logistics}\n\n`)
}
return t
},
formBodyLooksPersistable(text) {
const t = (text || '').trim()
return t.startsWith('单')
},
canPersistOrderFields() {
const buyer = (this.form.buyer || '').trim()
const pay = (this.form.paymentText || '').trim()
const orderId = (this.form.orderIdText || '').trim()
const logistics = (this.form.logisticsLink || '').trim()
return !!(buyer && pay && orderId && logistics)
},
formatInstructionData(data) {
if (Array.isArray(data)) {
return data.length ? data.join('\n\n') : ''
}
if (typeof data === 'string') {
return data
}
return ''
},
isPersistFailureText(text) {
const s = String(text || '')
return s.includes('[炸弹]') || s.includes('录单警告')
},
applyPersistResult(mergedBody, persistMsg) {
const msg = persistMsg || ''
this.resultText = (mergedBody || '') + '\n\n--- 落库回执 ---\n' + msg
if (this.isPersistFailureText(msg)) {
this.$modal.msgError('落库未成功,请查看回执')
} else {
this.$modal.msgSuccess('已生成表单并落库')
}
},
extractFirstResponse(data) {
if (Array.isArray(data)) {
return data.length ? String(data[0]) : ''
}
if (typeof data === 'string') {
return data
}
return ''
},
checkDuplicateError(resultList) {
if (!resultList || !resultList.length) return false
for (let i = 0; i < resultList.length; i++) {
const s = resultList[i]
if (typeof s === 'string' && (
s.includes('ERROR_CODE:ADDRESS_DUPLICATE') ||
s.includes('ERROR_CODE:ORDER_NUMBER_DUPLICATE')
)) {
return true
}
}
return false
},
showVerifyDialog(cmd) {
this.pendingCommand = cmd
this.verifyCode = String(Math.floor(1000 + Math.random() * 9000))
this.verifyInput = ''
let hasOrder = false
let hasAddr = false
const list = this.resultListFromLast || []
list.forEach(s => {
if (typeof s !== 'string') return
if (s.includes('ERROR_CODE:ORDER_NUMBER_DUPLICATE')) hasOrder = true
if (s.includes('ERROR_CODE:ADDRESS_DUPLICATE')) hasAddr = true
})
if (hasOrder && hasAddr) {
this.verifyMessage = '检测到订单编号与地址可能重复,输入验证码后可强制录单'
} else if (hasOrder) {
this.verifyMessage = '检测到订单编号可能重复,输入验证码后可强制录单'
} else {
this.verifyMessage = '检测到地址可能重复,输入验证码后可强制录单'
}
this.verifyDialogVisible = true
},
async generate() {
const cmd = this.buildCommand()
if (!cmd) return
this.loading = true
this.resultText = ''
try {
const res = await executeInstruction({ command: cmd })
if (!(res && (res.code === 200 || res.msg === '操作成功'))) {
this.$modal.msgError((res && res.msg) || '生成表单失败')
return
}
const list = Array.isArray(res.data) ? res.data : (res.data ? [String(res.data)] : [])
if (this.checkDuplicateError(list)) {
this.resultListFromLast = list
this.showVerifyDialog(cmd)
return
}
const rawForm = this.extractFirstResponse(res.data)
const merged = this.injectOptional(rawForm)
if (!this.formBodyLooksPersistable(merged)) {
this.resultText = merged
this.$modal.msgError('生成结果不是可落库表单,请重试或联系管理员')
return
}
if (!this.canPersistOrderFields()) {
this.resultText = merged
this.$modal.msgWarning('已生成录单文案。落库需填写:下单人、下单付款、订单号、物流链接(后返可不填,将按 0.00')
return
}
const res2 = await executeInstruction({ command: merged })
if (!(res2 && (res2.code === 200 || res2.msg === '操作成功'))) {
this.$modal.msgError((res2 && res2.msg) || '落库失败')
return
}
this.applyPersistResult(merged, this.formatInstructionData(res2.data))
} catch (e) {
this.$modal.msgError('请求失败')
} finally {
this.loading = false
}
},
async handleVerify() {
if (!this.verifyInput || this.verifyInput.length !== 4) {
this.$modal.msgError('请输入四位验证码')
return
}
if (this.verifyInput !== this.verifyCode) {
this.$modal.msgError('验证码不正确')
this.verifyInput = ''
return
}
this.verifyLoading = true
try {
const res = await executeInstructionWithForce({ command: this.pendingCommand })
if (!(res && (res.code === 200 || res.msg === '操作成功'))) {
this.$modal.msgError((res && res.msg) || '执行失败')
return
}
const merged = this.injectOptional(this.extractFirstResponse(res.data))
if (!this.formBodyLooksPersistable(merged)) {
this.resultText = merged
this.verifyDialogVisible = false
this.$modal.msgError('生成结果不是可落库表单')
return
}
if (!this.canPersistOrderFields()) {
this.resultText = merged
this.verifyDialogVisible = false
this.$modal.msgWarning('已强制生成表单。落库仍需填写:下单人、下单付款、订单号、物流链接')
return
}
const res2 = await executeInstruction({ command: merged })
this.verifyDialogVisible = false
if (!(res2 && (res2.code === 200 || res2.msg === '操作成功'))) {
this.$modal.msgError((res2 && res2.msg) || '落库失败')
return
}
this.applyPersistResult(merged, this.formatInstructionData(res2.data))
} catch (e) {
this.$modal.msgError('请求失败')
} finally {
this.verifyLoading = false
}
},
resetForm() {
this.form = {
markSuffix: '',
model: '',
address: '',
link: '',
qty: 1,
buyer: '',
paymentText: '',
rebateText: '',
orderIdText: '',
logisticsLink: ''
}
this.resultText = ''
},
copyResult() {
const t = this.resultText || ''
if (!t) return
if (navigator.clipboard) {
navigator.clipboard.writeText(t).then(() => {
this.$modal.msgSuccess('已复制')
}).catch(() => this.fallbackCopy(t))
} else {
this.fallbackCopy(t)
}
},
fallbackCopy(text) {
const ta = document.createElement('textarea')
ta.value = text
document.body.appendChild(ta)
ta.focus()
ta.select()
try {
document.execCommand('copy')
this.$modal.msgSuccess('已复制')
} catch (e) {
this.$modal.msgError('复制失败')
}
document.body.removeChild(ta)
}
}
}
</script>
<style scoped>
.fadan-quick-record {
padding-bottom: 16px;
}
.fadan-quick-record--mobile .box-card {
margin: 0 12px 20px;
}
.fadan-quick-record:not(.fadan-quick-record--mobile) .box-card {
max-width: 960px;
}
.fadan-form ::v-deep .el-form-item {
margin-bottom: 14px;
}
.mark-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.mark-prefix {
flex-shrink: 0;
color: #606266;
font-weight: 500;
user-select: none;
}
.mark-suffix-input {
flex: 1;
min-width: 0;
}
.btn-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 8px;
}
.result-area ::v-deep .el-textarea__inner {
font-family: Consolas, 'Courier New', monospace;
font-size: 13px;
}
.verify-body .verify-code {
text-align: center;
font-size: 26px;
font-weight: 700;
color: #409eff;
margin: 16px 0;
}
@media (max-width: 768px) {
.fadan-quick-record--mobile .box-card {
margin: 0 8px 16px;
}
}
</style>