Files
ruoyi-vue/src/components/PublishDialog.vue
2025-10-09 00:40:50 +08:00

499 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>
<el-dialog title="发品" :visible.sync="internalVisible" width="1200px" :close-on-click-modal="false" :close-on-press-escape="false" :destroy-on-close="false" append-to-body @close="handleClose">
<el-form :model="form" :rules="rules" ref="publishForm" label-width="110px">
<el-form-item label="文案版本" v-if="wenanOptions && wenanOptions.length > 0">
<el-select v-model="form.wenanIndex" placeholder="选择文案版本" @change="onWenanChange" style="width:100%">
<el-option v-for="(opt, idx) in wenanOptions" :key="idx" :label="opt.label" :value="idx" />
</el-select>
</el-form-item>
<el-form-item label="闲管家账号" v-if="!hideAppid">
<el-select v-model="form.appid" filterable placeholder="选择ERP应用" :loading="erpAccountLoading" @change="onAppidChange">
<el-option v-for="a in erpAccountsOptions" :key="a.value" :label="a.label" :value="a.value" />
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" maxlength="34" show-word-limit />
</el-form-item>
<el-form-item label="文案内容" prop="content">
<el-input type="textarea" :rows="6" v-model="form.content" />
</el-form-item>
<el-form-item label="选择图片" v-if="productImages && productImages.length">
<div class="img-grid">
<div class="img-item" v-for="(img, idx) in productImages" :key="idx">
<img :src="img.url" :alt="`图片${idx+1}`" @click="handlePreviewImage(img.url)" />
<div class="img-actions">
<el-checkbox v-model="img.selected">使用</el-checkbox>
<el-button type="text" size="mini" @click="handleCopyImageUrl(img.url)">复制</el-button>
</div>
</div>
</div>
<div style="margin-top:8px;">
<el-button size="mini" @click="selectAllImages(true)">全选</el-button>
<el-button size="mini" @click="selectAllImages(false)">全不选</el-button>
<el-button size="mini" @click="invertSelection">反选</el-button>
</div>
</el-form-item>
<el-form-item label="额外图片链接">
<el-input type="textarea" :rows="3" v-model="form.extraImagesText" placeholder="每行一条图片URL" />
</el-form-item>
<el-form-item label="白底图">
<el-input v-model="form.whiteImages" placeholder="可选图片URL" />
</el-form-item>
<el-form-item label="服务项">
<el-select v-model="form.serviceSupport" multiple collapse-tags placeholder="可多选">
<el-option v-for="opt in serviceSupportOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item label="会员名" prop="userName">
<el-select v-model="form.userName" filterable placeholder="选择会员名" :loading="userNameLoading">
<el-option v-for="u in userNameOptions" :key="u.value" :label="u.label" :value="u.value" />
</el-select>
</el-form-item>
<el-form-item label="省/市/区" required>
<div style="display:flex; gap:8px; width:100%">
<el-select v-model.number="form.province" placeholder="选择省" style="flex:1" filterable @change="onProvinceChange">
<el-option v-for="p in regionOptions.provinces" :key="p.value" :label="p.label" :value="p.value" />
</el-select>
<el-select v-model.number="form.city" placeholder="选择市" style="flex:1" filterable :disabled="!form.province" @change="onCityChange">
<el-option v-for="c in regionOptions.cities" :key="c.value" :label="c.label" :value="c.value" />
</el-select>
<el-select v-model.number="form.district" placeholder="选择区" style="flex:1" filterable :disabled="!form.city">
<el-option v-for="a in regionOptions.areas" :key="a.value" :label="a.label" :value="a.value" />
</el-select>
</div>
</el-form-item>
<el-form-item label="价格(元)" prop="price">
<el-input v-model.number="form.price" type="number" min="0.01" step="0.01" />
</el-form-item>
<el-form-item label="原价(元)">
<el-input v-model.number="form.originalPrice" type="number" min="0" step="0.01" />
</el-form-item>
<el-form-item label="运费(元)" prop="expressFee">
<el-input v-model.number="form.expressFee" type="number" min="0" step="0.01" />
</el-form-item>
<el-form-item label="库存" prop="stock">
<el-input v-model.number="form.stock" type="number" min="1" />
</el-form-item>
<el-form-item label="商家编码">
<el-input v-model="form.outerId" maxlength="64" />
</el-form-item>
<el-form-item label="商品类型" prop="itemBizType">
<el-select v-model.number="form.itemBizType" filterable @change="onItemBizTypeChange">
<el-option v-for="opt in itemBizTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item label="行业类型" prop="spBizType">
<el-select v-model.number="form.spBizType" filterable @change="onSpBizTypeChange">
<el-option v-for="opt in spBizTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item label="类目ID" prop="channelCatId">
<el-select v-model="form.channelCatId" filterable placeholder="请选择类目" :disabled="!categoryOptions.length" :loading="categoryLoading" @change="loadProperties">
<el-option v-for="c in categoryOptions" :key="c.value" :label="c.label" :value="c.value" />
</el-select>
</el-form-item>
<el-form-item label="商品属性">
<div v-if="pvOptions.length" style="display:flex; flex-direction:column; gap:8px;">
<div v-for="(p, pi) in pvOptions" :key="p.propertyId" style="display:flex; gap:8px; align-items:center;">
<span style="width:90px; text-align:right; color:#666;">{{ p.propertyName }}:</span>
<el-select v-model="selectedPv[p.propertyId]" clearable filterable placeholder="请选择" style="flex:1" @change="onPvChange">
<el-option v-for="v in p.values" :key="v.valueId" :label="v.valueName" :value="v.valueId" />
</el-select>
</div>
</div>
<div v-else style="color:#999;">无属性或请选择类型和类目后加载</div>
</el-form-item>
<el-form-item label="成色">
<el-select v-model.number="form.stuffStatus" clearable filterable placeholder="可选">
<el-option v-for="opt in stuffStatusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item label="属性JSON">
<el-input type="textarea" :rows="3" v-model="form.channelPvJson" placeholder='示例: [{"property_id":"p","property_name":"颜色","value_id":"v","value_name":"红"}]' />
</el-form-item>
</el-form>
<el-alert
v-if="createdProduct"
:title="`商品ID${createdProduct.productId || '-'}`"
type="success"
:closable="false"
show-icon
style="margin: 10px 0;"
>
<template slot="description">
<div style="display:flex; align-items:center; flex-wrap:wrap; gap:12px;">
<span>状态{{ createdProduct.productStatus || '-' }}</span>
<span>商家编码{{ createdProduct.outerId || '-' }}</span>
<el-button type="text" size="mini" @click="copyText(String(createdProduct.productId || ''))">复制ID</el-button>
<el-button type="text" size="mini" @click="copyText(String(createdProduct.outerId || ''))">复制商家编码</el-button>
</div>
</template>
</el-alert>
<div slot="footer" class="dialog-footer">
<el-button @click="closeDialog"> </el-button>
<el-button type="primary" :loading="loading" @click="submitPublish"> </el-button>
<el-button
type="warning"
:disabled="!createdProduct || !createdProduct.productId"
:loading="publishLoading"
@click="publishNow"
> </el-button>
</div>
</el-dialog>
</template>
<script>
import { createProductByPromotion, publishProduct, getProvinces, getCities, getAreas, getCategories, getUsernames, getERPAccounts, getProperties } from "@/api/system/jdorder";
export default {
name: 'PublishDialog',
props: {
visible: { type: Boolean, default: false },
initialData: { type: Object, default: () => ({}) },
hideAppid: { type: Boolean, default: false }
},
data() {
return {
internalVisible: false,
loading: false,
publishLoading: false,
createdProduct: null,
wenanOptions: [],
productImages: [],
form: {
appid: '',
userName: '',
province: null,
city: null,
district: null,
title: '',
content: '',
wenanIndex: 0,
extraImagesText: '',
whiteImages: '',
serviceSupport: ['NFR'],
price: null,
originalPrice: null,
expressFee: 0,
stock: 999,
outerId: '',
itemBizType: 2,
spBizType: 3,
channelCatId: '',
stuffStatus: 100,
channelPvJson: ''
},
rules: {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
content: [{ required: true, message: '请输入文案内容', trigger: 'blur' }],
userName: [{ required: true, message: '请输入闲鱼会员名', trigger: 'blur' }],
price: [{ required: true, message: '请输入价格(分)', trigger: 'blur' }],
expressFee: [{ required: true, message: '请输入运费(分)', trigger: 'blur' }],
stock: [{ required: true, message: '请输入库存', trigger: 'blur' }],
itemBizType: [{ required: true, message: '请选择商品类型', trigger: 'change' }],
spBizType: [{ required: true, message: '请选择行业类型', trigger: 'change' }],
channelCatId: [{ required: true, message: '请输入类目ID', trigger: 'blur' }]
},
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' }
]
}
},
watch: {
visible: {
immediate: true,
handler(v) { this.internalVisible = v; if (v) { this.bootstrap(); } }
},
'form.itemBizType'(val) { this.onItemBizTypeChange(); },
'form.spBizType'(val) { this.onSpBizTypeChange(); },
'form.appid'(val) { this.onAppidChange(); },
'form.channelCatId'(val) { this.loadProperties(); }
},
methods: {
copyText(text) {
const val = String(text || '').trim();
if (!val) { this.$modal.msgWarning('无可复制的内容'); return; }
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(val).then(() => { this.$modal.msgSuccess('复制成功'); }).catch(() => { this.fallbackCopy(val); });
} else {
this.fallbackCopy(val);
}
},
fallbackCopy(text) {
const ta = document.createElement('textarea');
ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px';
document.body.appendChild(ta); ta.focus(); ta.select();
try { document.execCommand('copy'); this.$modal.msgSuccess('复制成功'); } catch(e){ this.$modal.msgError('复制失败'); }
document.body.removeChild(ta);
},
async bootstrap() {
// 初始化表单与文案/图片
const d = this.initialData || {};
this.wenanOptions = Array.isArray(d.wenanOptions) ? d.wenanOptions : [];
this.form.title = d.title || '';
this.form.content = d.content || '';
this.form.wenanIndex = 0;
if (Array.isArray(d.images)) {
this.productImages = d.images.map(u => ({ url: u, selected: true }));
} else { this.productImages = []; }
// 预估原价
if (typeof d.originalPrice === 'number') {
this.form.originalPrice = d.originalPrice;
}
// 预设:会员名、省市区
if (d.userName) this.form.userName = d.userName;
if (d.province) this.form.province = d.province;
if (d.city) this.form.city = d.city;
if (d.district) this.form.district = d.district;
await this.loadProvinces();
await this.loadERPAccounts();
await this.loadUsernames();
await this.loadCategories();
this.$nextTick(() => this.$refs.publishForm && this.$refs.publishForm.clearValidate());
},
onWenanChange(val) {
if (this.wenanOptions && this.wenanOptions[val]) {
this.form.content = this.wenanOptions[val].content || '';
}
},
selectAllImages(flag) { (this.productImages || []).forEach(it => { it.selected = !!flag; }); },
invertSelection() { (this.productImages || []).forEach(it => { it.selected = !it.selected; }); },
handleCopyImageUrl(imageUrl) { if (navigator.clipboard) { navigator.clipboard.writeText(imageUrl).then(() => { this.$modal.msgSuccess('图片链接复制成功'); }).catch(() => { this.$message.error('复制失败'); }); } },
handlePreviewImage(imageUrl) { window.open(imageUrl, '_blank'); },
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 (!this.form.province && this.regionOptions.provinces.length) {
this.form.province = this.regionOptions.provinces[0].value;
}
if (this.form.province) { await this.loadCities(this.form.province, true); } else { this.regionOptions.cities = []; this.regionOptions.areas = []; this.form.city = null; this.form.district = null; }
},
async onProvinceChange() { await this.loadCities(this.form.province, false); },
async onCityChange() { await this.loadAreas(this.form.province, this.form.city, false); },
async loadCities(provId, echo = false) {
if (!provId) { this.regionOptions.cities = []; this.regionOptions.areas = []; this.form.city = null; this.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 (!this.form.city && this.regionOptions.cities.length) {
this.form.city = this.regionOptions.cities[0].value;
}
if (this.form.city) { await this.loadAreas(provId, this.form.city, true); } else { this.regionOptions.areas = []; this.form.district = null; }
},
async loadAreas(provId, cityId, echo = false) {
if (!provId || !cityId) { this.regionOptions.areas = []; this.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 (!this.form.district && this.regionOptions.areas.length) {
this.form.district = this.regionOptions.areas[0].value;
} else if (!echo) {
this.form.district = null;
}
},
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;
// 如果隐藏appid选择则不强制赋值给表单由后端使用默认账号
if (!this.hideAppid && !this.form.appid && this.erpAccountsOptions.length) {
this.form.appid = this.erpAccountsOptions[0].value;
}
},
onAppidChange() { this.form.userName = ''; this.loadUsernames(); this.loadCategories(); this.loadProperties(); },
async loadUsernames() {
this.userNameLoading = true;
try { const res = await getUsernames({ pageNum: 1, pageSize: 200, appid: this.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.form.userName && this.userNameOptions.length) { this.form.userName = this.userNameOptions[0].value; }
},
async onItemBizTypeChange() { this.categoryOptions = []; this.form.channelCatId = ''; await this.loadCategories(); },
async onSpBizTypeChange() { this.categoryOptions = []; this.form.channelCatId = ''; await this.loadCategories(); },
async loadCategories() {
const itemBizType = this.form.itemBizType; const spBizType = this.form.spBizType; const appid = this.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.form.channelCatId) { this.loadProperties(); } else if (this.categoryOptions.length) { this.form.channelCatId = this.categoryOptions[0].value; this.loadProperties(); }
},
async loadProperties() {
const f = this.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('加载属性失败'); }
},
onPvChange() {
// 属性值变更时的回调(可用于调试或联动逻辑)
},
submitPublish() {
this.$refs.publishForm.validate(valid => {
if (!valid) return;
const f = this.form;
const selectedImages = (this.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; }
} else if (this.selectedPv && Object.keys(this.selectedPv).length) {
// 从 pvOptions 中获取完整的属性信息property_id, property_name, value_id, value_name
channelPv = [];
Object.keys(this.selectedPv).forEach(pid => {
const valueId = this.selectedPv[pid];
if (!valueId) return; // 跳过未选择的属性
// 从 pvOptions 中找到对应的属性
const property = this.pvOptions.find(p => String(p.propertyId) === String(pid));
if (!property) return;
// 从属性的 values 中找到对应的值
const value = property.values && property.values.find(v => String(v.valueId) === String(valueId));
if (!value) return;
// 构建完整的4个字段
channelPv.push({
property_id: pid,
property_name: property.propertyName,
value_id: valueId,
value_name: value.valueName
});
});
if (channelPv.length === 0) channelPv = undefined;
}
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,
// 后端字段为下划线命名
channel_pv: channelPv
};
function cents(yuan){ const n = Number(yuan); if (Number.isNaN(n)) return undefined; return Math.round(n*100); }
this.loading = true;
createProductByPromotion(payload).then(async res => {
this.loading = false;
if (res.code === 200) {
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('发品提交成功'); }
// 记录创建成功的商品,保留弹窗供手动“上架”
const productId = this.extractProductId(res.data) || (res.data && (res.data.product_id || (res.data.data && res.data.data.product_id)));
const productStatus = res.data && (res.data.product_status || (res.data.data && res.data.data.product_status));
const outerId2 = res.data && (res.data.outerId || res.data.outer_id || (res.data.data && (res.data.data.outerId || res.data.data.outer_id)));
this.createdProduct = { productId, productStatus, outerId: outerId2 };
this.$emit('success', res);
} else { this.$modal.msgError(res.msg || '发品失败'); }
}).catch(err => { this.loading = false; console.error('发品失败', err); this.$modal.msgError('发品失败,请稍后重试'); });
});
},
async publishNow() {
if (!this.createdProduct || !this.createdProduct.productId) return;
this.publishLoading = true;
try {
const pubRes = await publishProduct({ productId: this.createdProduct.productId, userName: this.form.userName, appid: this.form.appid });
if (pubRes && pubRes.code === 200) {
const code = (pubRes.data && pubRes.data.code) ?? pubRes.code;
if (code === 0 || code === 200) this.$modal.msgSuccess('上架成功'); else this.$modal.msgWarning('上架已提交或状态未知');
} else {
this.$modal.msgError((pubRes && pubRes.msg) || '上架失败');
}
} catch(e) {
this.$modal.msgError('上架失败');
}
this.publishLoading = false;
},
extractProductId(resp) {
try {
if (!resp) return null;
if (typeof resp === 'object') {
if (resp.productId) return Number(resp.productId);
if (resp.data && resp.data.productId) return Number(resp.data.productId);
if (resp.data && resp.data.id) return Number(resp.data.id);
}
} catch (e) { return null; }
return null;
},
closeDialog() { this.$emit('update:visible', false); this.internalVisible = false; },
handleClose() { this.closeDialog(); }
}
}
</script>
<style scoped>
.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>