Files
ruoyi-vue/src/views/system/jdorder/index.vue
2025-08-25 01:46:41 +08:00

728 lines
26 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">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>一键转链</span>
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form :model="form" label-width="120px" label-position="top">
<el-form-item label="输入内容">
<el-input
v-model="form.inputContent"
type="textarea"
:rows="10"
placeholder="请输入需要转链的内容,如商品链接、商品名称等"
style="width: 100%"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleGenerate" :loading="loading">
生成转链内容
</el-button>
<el-button @click="handleClear">清空</el-button>
</el-form-item>
</el-form>
</el-col>
<el-col :span="12">
<el-form label-position="top">
<el-form-item label="通用文案">
<el-input
:value="generalCopy"
type="textarea"
:rows="10"
readonly
placeholder="暂无通用文案"
style="width: 100%"
/>
</el-form-item>
<div style="margin-top: 10px;">
<el-button type="success" @click="handleCopyText(generalCopy)" :disabled="!generalCopy">复制通用文案</el-button>
</div>
</el-form>
</el-col>
</el-row>
<div v-if="result" style="margin-top: 20px;">
<h4>转链结果</h4>
<!-- 商品列表 -->
<div v-if="parsedResult && Array.isArray(parsedResult) && parsedResult.length > 0" style="margin-bottom: 20px;">
<h5>商品列表 ({{ parsedResult.length }}个商品)</h5>
<el-tabs v-model="activeProductTab" type="card">
<el-tab-pane
v-for="(product, productIndex) in parsedResult"
:key="productIndex"
:label="`商品${productIndex + 1}`"
:name="productIndex"
>
<!-- 商品基本信息 -->
<el-descriptions :column="2" border style="margin-bottom: 20px;">
<el-descriptions-item label="商品名称">{{ product.skuName || '-' }}</el-descriptions-item>
<el-descriptions-item label="店铺">{{ product.shopName || '-' }}</el-descriptions-item>
<el-descriptions-item label="佣金">¥{{ product.commission || '-' }}</el-descriptions-item>
<el-descriptions-item label="佣金比例">{{ product.commissionShare || '-' }}%</el-descriptions-item>
<el-descriptions-item label="SPUID">{{ product.spuid || '-' }}</el-descriptions-item>
<el-descriptions-item label="商品链接">
<a :href="product.url || '#'" target="_blank" style="color: #409EFF;">{{ product.url || '-' }}</a>
</el-descriptions-item>
</el-descriptions>
<!-- 文案版本 -->
<div v-if="product.wenan && Array.isArray(product.wenan) && product.wenan.length > 0" style="margin-bottom: 20px;">
<h6>文案版本 ({{ product.wenan.length }})</h6>
<el-tabs v-model="activeWenanTab[productIndex]" type="border-card">
<el-tab-pane
v-for="(wenan, wenanIndex) in product.wenan"
:key="wenanIndex"
:label="wenan.type || `版本${wenanIndex + 1}`"
:name="wenanIndex"
>
<el-input
:value="wenan.content || ''"
type="textarea"
:rows="8"
readonly
style="width: 100%"
/>
<div style="margin-top: 10px;">
<el-button type="success" @click="handleCopyText(wenan.content || '')">复制此版本</el-button>
<el-button type="primary" style="margin-left: 8px;" @click="openPublish(product, productIndex)">发品</el-button>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 商品图片 -->
<div v-if="product.images && Array.isArray(product.images) && product.images.length > 0" style="margin-bottom: 20px;">
<h6>商品图片 ({{ product.images.length }})</h6>
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<div
v-for="(image, imageIndex) in product.images"
:key="imageIndex"
style="text-align: center;"
>
<img
:src="image"
:alt="`商品${productIndex + 1}图片${imageIndex + 1}`"
style="width: 120px; height: 120px; object-fit: cover; border: 1px solid #ddd; border-radius: 4px;"
@click="handlePreviewImage(image)"
/>
<div style="margin-top: 5px; font-size: 12px; color: #666;">
<el-button type="text" size="mini" @click="handleCopyImageUrl(image)">复制链接</el-button>
</div>
</div>
</div>
</div>
<!-- 发品操作 -->
<div style="margin-top: 10px; display:flex; gap:8px;">
<el-button type="primary" @click="openPublish(product, productIndex)">发品</el-button>
<el-button @click="handleAddToFavorites(product)">加入常用</el-button>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 原始数据 -->
<div style="margin-top: 20px;">
<h5>原始数据</h5>
<el-input
:value="result"
type="textarea"
:rows="6"
readonly
style="width: 100%"
/>
<div style="margin-top: 10px;">
<el-button type="success" @click="handleCopy">复制原始数据</el-button>
</div>
</div>
</div>
</el-card>
<!-- 公共发品对话框组件 -->
<PublishDialog :visible.sync="publishDialogVisible" :initial-data="publishInitialData" @success="handlePublishSuccess" />
</div>
</template>
<script>
import { generatePromotionContent } from "@/api/system/jdorder";
import { addToFavorites, getBySkuid } from "@/api/system/favoriteProduct";
import { addToFavoritesAfterPublishFromTransfer } from "@/utils/publishHelper";
import PublishDialog from '@/components/PublishDialog.vue'
export default {
name: "Jdorder",
components: { PublishDialog },
data() {
return {
form: {
inputContent: ""
},
loading: false,
result: "",
parsedResult: {},
// 当前选中的商品
activeProductTab: 0,
// 每个商品的文案标签页状态
activeWenanTab: {},
publishDialogVisible: false,
publishInitialData: {},
// 记录正在发品的商品,用于发品成功后自动加入常用
currentPublishProduct: null,
regionOptions: {
provinces: [],
cities: [],
areas: []
},
categoryOptions: [],
categoryLoading: false,
userNameOptions: [],
userNameLoading: false,
erpAccountsOptions: [],
erpAccountLoading: false,
pvOptions: [],
selectedPv: {},
itemBizTypeOptions: [
{ label: '普通商品', value: 2 },
{ label: '已验货', value: 0 },
{ label: '验货宝', value: 10 },
{ label: '闲鱼优品', value: 19 },
{ label: '闲鱼特卖', value: 24 },
{ label: '品牌捡漏', value: 26 }
],
spBizTypeOptions: [
{ label: '手机', value: 1 },
{ label: '时尚', value: 2 },
{ label: '家电', value: 3 },
{ label: '乐器', value: 8 },
{ label: '数码3C', value: 9 },
{ label: '奢品', value: 16 },
{ label: '母婴', value: 17 },
{ label: '美妆', value: 18 },
{ label: '珠宝', value: 19 },
{ label: '游戏', value: 20 },
{ label: '家居', value: 21 },
{ label: '虚拟', value: 22 },
{ label: '图书', value: 24 },
{ label: '食品', value: 27 },
{ label: '玩具', value: 28 },
{ label: '其他', value: 99 }
],
stuffStatusOptions: [
{ label: '不传', value: null },
{ label: '全新', value: 100 },
{ label: '99新', value: 99 },
{ label: '95新', value: 95 },
{ label: '9成新', value: 90 },
{ label: '8成新', value: 80 },
{ label: '7成新', value: 70 },
{ label: '6成新', value: 60 },
{ label: '5成新', value: 50 }
],
serviceSupportOptions: [
{ label: '七天无理由退货', value: 'SDR' },
{ label: '描述不符包邮退', value: 'NFR' },
{ label: '描述不符全额退(虚拟)', value: 'VNR' },
{ label: '10分钟极速发货(虚拟)', value: 'FD_10MS' },
{ label: '24小时极速发货', value: 'FD_24HS' },
{ label: '48小时极速发货', value: 'FD_48HS' },
{ label: '正品保障', value: 'FD_GPA' }
]
};
},
mounted() {},
computed: {
// 提取通用文案:优先取第一个商品中类型包含“通用”的文案;否则取第一个文案
generalCopy() {
const data = this.parsedResult;
try {
if (Array.isArray(data) && data.length) {
const productIndex = typeof this.activeProductTab === 'number' ? this.activeProductTab : 0;
const product = data[productIndex] || data[0];
const wenanList = Array.isArray(product && product.wenan) ? product.wenan : [];
if (wenanList.length) {
const found = wenanList.find(w => (w && (w.type || '').includes('通用')));
const chosen = found || wenanList[0];
return (chosen && chosen.content) ? chosen.content : '';
}
}
if (data && data.generalCopy) {
return String(data.generalCopy || '');
}
} catch (e) { /* 忽略提取异常 */ }
return '';
}
},
watch: {
'publishDialog.form.itemBizType'(val) {
// 冗余联动,防止@change未触发的情况
this.onItemBizTypeChange();
},
'publishDialog.form.spBizType'(val) {
this.onSpBizTypeChange();
},
'publishDialog.form.appid'(val) {
this.onAppidChange();
},
'publishDialog.form.channelCatId'(val) {
// 兜底:类目变更(包括编程方式设置)时,自动拉取属性
this.loadProperties();
}
},
methods: {
async handleAddToFavorites(product) {
try {
const payload = {
skuid: product.skuid || product.skuId || product.spuid || '',
productName: product.skuName || product.title || '',
shopName: product.shopName || '',
productUrl: product.materialUrl || product.url || '',
productImage: Array.isArray(product.images) && product.images.length ? product.images[0] : '',
price: product.price != null ? String(product.price) : (product.lowestCouponPrice != null ? String(product.lowestCouponPrice) : ''),
commissionInfo: product.commission != null ? String(product.commission) : '',
remark: '来自一键转链'
}
const res = await addToFavorites(payload)
if (res && (res.code === 200 || res.msg === '操作成功')) {
this.$modal.msgSuccess('已加入常用');
} else {
this.$modal.msgError(res && res.msg ? res.msg : '加入常用失败');
}
} catch (e) {
this.$modal.msgError('加入常用失败');
}
},
handleGenerate() {
if (!this.form.inputContent.trim()) {
this.$modal.msgError("请输入需要转链的内容");
return;
}
this.loading = true;
generatePromotionContent({
promotionContent: this.form.inputContent.trim()
}).then(response => {
this.loading = false;
if (response.code === 200) {
this.result = response.msg || response.data || "转链成功";
try {
if (typeof this.result === 'string') {
this.parsedResult = JSON.parse(this.result);
} else {
this.parsedResult = this.result;
}
// 初始化每个商品的文案标签页状态
if (Array.isArray(this.parsedResult)) {
this.parsedResult.forEach((_, index) => {
this.$set(this.activeWenanTab, index, 0);
});
}
} catch (e) {
console.log('数据解析失败,使用原始数据');
this.parsedResult = {};
}
this.$modal.msgSuccess("转链内容生成成功");
} else {
this.$modal.msgError(response.msg || "转链失败");
}
}).catch(error => {
this.loading = false;
console.error('转链失败:', error);
this.$modal.msgError("转链失败,请稍后重试");
});
},
handleClear() {
this.form.inputContent = "";
this.result = "";
this.parsedResult = {};
this.activeProductTab = 0;
this.activeWenanTab = {};
},
handleCopy() {
if (this.result) {
if (navigator.clipboard) {
navigator.clipboard.writeText(this.result).then(() => {
this.$modal.msgSuccess("复制成功");
}).catch(() => {
this.fallbackCopy();
});
} else {
this.fallbackCopy();
}
}
},
fallbackCopy() {
const textArea = document.createElement("textarea");
textArea.value = this.result;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
this.$modal.msgSuccess("复制成功");
} catch (err) {
this.$modal.msgError("复制失败");
}
document.body.removeChild(textArea);
},
handleCopyText(text) {
if (text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => {
this.$modal.msgSuccess("复制成功");
}).catch(() => {
this.fallbackCopyText(text);
});
} else {
this.fallbackCopyText(text);
}
}
},
fallbackCopyText(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
this.$modal.msgSuccess("复制成功");
} catch (err) {
this.$modal.msgError("复制失败");
}
document.body.removeChild(textArea);
},
handleCopyImageUrl(imageUrl) {
if (imageUrl) {
if (navigator.clipboard) {
navigator.clipboard.writeText(imageUrl).then(() => {
this.$modal.msgSuccess("图片链接复制成功");
}).catch(() => {
this.fallbackCopyText(imageUrl);
});
} else {
this.fallbackCopyText(imageUrl);
}
}
},
handlePreviewImage(imageUrl) {
window.open(imageUrl, '_blank');
},
openPublish(product, productIndex) {
const wenanIndex = this.activeWenanTab[productIndex] || 0;
const wenanOptions = Array.isArray(product.wenan) ? product.wenan.map((w, i) => ({ label: w.type || `版本${i+1}`, content: w.content || '' })) : [];
// 记录当前发品的商品
this.currentPublishProduct = product;
this.publishInitialData = {
title: product.skuName || '',
content: (product && product.wenan && product.wenan[wenanIndex] ? (product.wenan[wenanIndex].content || '') : ''),
images: Array.isArray(product.images) ? product.images : [],
originalPrice: this.guessYuanPrice(product),
wenanOptions
};
this.publishDialogVisible = true;
},
async handlePublishSuccess(res) {
try {
const p = this.currentPublishProduct || {};
await addToFavoritesAfterPublishFromTransfer(p, res);
this.$store && this.$store.dispatch && this.$store.dispatch('app/triggerFavoriteProductRefresh');
} catch (e) { }
},
onWenanChange(val) {
if (this.publishDialog.wenanOptions && this.publishDialog.wenanOptions[val]) {
this.publishDialog.form.content = this.publishDialog.wenanOptions[val].content || '';
}
},
selectAllImages(flag) {
(this.publishDialog.productImages || []).forEach(it => { it.selected = !!flag; });
},
invertSelection() {
(this.publishDialog.productImages || []).forEach(it => { it.selected = !it.selected; });
},
// 从解析到的商品信息中推测原价(元)
guessYuanPrice(product) {
// 常见字段尝试price、oriPrice、originPrice、jdPrice、marketPrice 等(单位可能为元)
const candidates = [
product && product.price,
product && product.oriPrice,
product && product.originPrice,
product && product.jdPrice,
product && product.marketPrice,
product && product.opPrice,
].filter(v => v != null);
for (const v of candidates) {
const n = Number(v);
if (!Number.isNaN(n) && n > 0) {
return n;
}
}
return null;
},
async loadProvinces(echo = true) {
try {
const res = await getProvinces();
if (res.code === 200) this.regionOptions.provinces = res.data || []; else this.$modal.msgError(res.msg || '加载省份失败');
} catch (e) { this.$modal.msgError('加载省份失败'); }
if (echo && this.publishDialog.form.province) {
await this.loadCities(this.publishDialog.form.province, true);
} else {
this.regionOptions.cities = []; this.regionOptions.areas = [];
this.publishDialog.form.city = null; this.publishDialog.form.district = null;
}
},
async onProvinceChange() {
const provId = this.publishDialog.form.province;
await this.loadCities(provId, false);
},
async onCityChange() {
const provId = this.publishDialog.form.province; const cityId = this.publishDialog.form.city;
await this.loadAreas(provId, cityId, false);
},
async loadCities(provId, echo = false) {
if (!provId) {
this.regionOptions.cities = []; this.regionOptions.areas = [];
this.publishDialog.form.city = null; this.publishDialog.form.district = null;
return;
}
try {
const res = await getCities(provId);
if (res.code === 200) this.regionOptions.cities = res.data || []; else this.$modal.msgError(res.msg || '加载城市失败');
} catch (e) { this.$modal.msgError('加载城市失败'); }
if (echo && this.publishDialog.form.city) {
await this.loadAreas(provId, this.publishDialog.form.city, true);
} else {
this.regionOptions.areas = []; this.publishDialog.form.district = null;
}
},
async loadAreas(provId, cityId, echo = false) {
if (!provId || !cityId) {
this.regionOptions.areas = []; this.publishDialog.form.district = null;
return;
}
try {
const res = await getAreas(provId, cityId);
if (res.code === 200) this.regionOptions.areas = res.data || []; else this.$modal.msgError(res.msg || '加载区县失败');
} catch (e) { this.$modal.msgError('加载区县失败'); }
if (!echo) {
this.publishDialog.form.district = null;
}
},
async onItemBizTypeChange() {
this.categoryOptions = [];
this.publishDialog.form.channelCatId = '';
await this.loadCategories();
},
async onSpBizTypeChange() {
this.categoryOptions = [];
this.publishDialog.form.channelCatId = '';
await this.loadCategories();
},
async loadCategories() {
const itemBizType = this.publishDialog.form.itemBizType;
const spBizType = this.publishDialog.form.spBizType;
const appid = this.publishDialog.form.appid;
if (!itemBizType) return;
this.categoryLoading = true;
try {
const res = await getCategories({ itemBizType, spBizType, appid });
if (res.code === 200) this.categoryOptions = res.data || []; else this.$modal.msgError(res.msg || '加载类目失败');
} catch (e) { this.$modal.msgError('加载类目失败'); }
this.categoryLoading = false;
// 若已有选中的类目,或列表首项存在,则尝试自动拉取属性
if (this.publishDialog.form.channelCatId) {
this.loadProperties();
} else if (this.categoryOptions.length) {
this.publishDialog.form.channelCatId = this.categoryOptions[0].value;
this.loadProperties();
}
},
async loadUsernames() {
this.userNameLoading = true;
try {
const res = await getUsernames({ pageNum: 1, pageSize: 200, appid: this.publishDialog.form.appid });
if (res.code === 200) this.userNameOptions = res.data || []; else this.$modal.msgError(res.msg || '加载会员名失败');
} catch (e) { this.$modal.msgError('加载会员名失败'); }
this.userNameLoading = false;
if (!this.publishDialog.form.userName && this.userNameOptions.length) {
// 如未选择默认填第一个
this.publishDialog.form.userName = this.userNameOptions[0].value;
}
},
async loadERPAccounts() {
this.erpAccountLoading = true;
try {
const res = await getERPAccounts();
if (res.code === 200) this.erpAccountsOptions = res.data || []; else this.$modal.msgError(res.msg || '加载应用失败');
} catch (e) { this.$modal.msgError('加载应用失败'); }
this.erpAccountLoading = false;
if (!this.publishDialog.form.appid && this.erpAccountsOptions.length) {
this.publishDialog.form.appid = this.erpAccountsOptions[0].value;
}
},
onAppidChange() {
// 切换账号后,重新拉取与账号相关的下拉
this.publishDialog.form.userName = '';
this.loadUsernames();
this.loadCategories();
this.loadProperties();
},
async loadProperties() {
const f = this.publishDialog.form;
if (!f.itemBizType || !f.spBizType || !f.channelCatId) {
this.pvOptions = []; this.selectedPv = {}; return;
}
try {
const res = await getProperties({ itemBizType: f.itemBizType, spBizType: f.spBizType, channelCatId: f.channelCatId, appid: f.appid });
if (res.code === 200) {
this.pvOptions = res.data || [];
const keep = { ...this.selectedPv };
this.selectedPv = {};
(this.pvOptions || []).forEach(p => { if (keep[p.propertyId]) this.selectedPv[p.propertyId] = keep[p.propertyId]; });
} else {
this.$modal.msgError(res.msg || '加载属性失败');
}
} catch (e) {
this.$modal.msgError('加载属性失败');
}
},
submitPublish() {
this.$refs.publishForm.validate(valid => {
if (!valid) return;
const f = this.publishDialog.form;
const selectedImages = (this.publishDialog.productImages || [])
.filter(it => it.selected)
.map(it => it.url)
.filter(Boolean);
const extraImages = String(f.extraImagesText || '')
.split(/\n+/)
.map(s => s.trim())
.filter(Boolean);
const images = [...selectedImages, ...extraImages];
if (!images.length) {
this.$modal.msgError('请至少选择或填写一张图片');
return;
}
let channelPv = undefined;
if (f.channelPvJson && f.channelPvJson.trim()) {
try { channelPv = JSON.parse(f.channelPvJson); } catch (e) { this.$modal.msgError('属性JSON格式不正确'); return; }
}
const payload = {
appid: f.appid || undefined,
title: f.title,
content: f.content,
images: images,
whiteImages: f.whiteImages || undefined,
userName: f.userName,
province: f.province,
city: f.city,
district: f.district,
serviceSupport: (f.serviceSupport && f.serviceSupport.length) ? f.serviceSupport.join(',') : undefined,
price: cents(f.price),
originalPrice: f.originalPrice != null ? cents(f.originalPrice) : undefined,
expressFee: cents(f.expressFee),
stock: f.stock,
outerId: f.outerId || undefined,
itemBizType: f.itemBizType,
spBizType: f.spBizType,
channelCatId: f.channelCatId,
stuffStatus: f.stuffStatus || undefined,
channelPv: channelPv
};
function cents(yuan) {
const n = Number(yuan);
if (Number.isNaN(n)) return undefined;
return Math.round(n * 100);
}
this.publishDialog.loading = true;
createProductByPromotion(payload).then(res => {
this.publishDialog.loading = false;
if (res.code === 200) {
// 成功反馈包含生成的outerId
try {
const outerId = res.data && (res.data.outerId || (res.data.data && res.data.data.outerId))
if (outerId) {
this.$modal.msgSuccess(`发品成功,商家编码:${outerId}`)
} else {
this.$modal.msgSuccess('发品提交成功')
}
} catch (e) {
this.$modal.msgSuccess('发品提交成功')
}
this.publishDialog.visible = false;
} else {
this.$modal.msgError(res.msg || '发品失败');
}
}).catch(err => {
this.publishDialog.loading = false;
console.error('发品失败', err);
this.$modal.msgError('发品失败,请稍后重试');
});
});
}
}
};
</script>
<style scoped>
.box-card {
margin: 20px;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
}
.clearfix:after {
clear: both;
}
.img-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.img-item {
border: 1px solid #e5e5e5;
border-radius: 4px;
padding: 6px;
text-align: center;
}
.img-item img {
width: 100%;
height: 100px;
object-fit: cover;
border-radius: 4px;
}
.img-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 6px;
}
</style>