Authored by 邱骏

入驻商家调价

<template>
<img v-lazy="currentSrc" :alt="alt" v-if="!refresh" />
<img v-lazy='currentSrc' :alt='alt' v-if='!refresh' />
</template>
<script>
export default {
name: "ImgSize",
name: 'ImgSize',
props: {
src: String,
width: Number,
... ... @@ -26,10 +26,10 @@ export default {
},
computed: {
currentSrc() {
return (this.src || "")
.replace("http://", "//")
.replace("{width}", this.width)
.replace("{height}", this.height);
return (this.src || '')
.replace('http://', '//')
.replace('{width}', this.width || '')
.replace('{height}', this.height || '');
}
}
};
... ...
<template>
<CubeInput v-bind="$attrs" v-bind:value="value" v-on="inputListeners" :maxlength="8" class="input-number">
<span slot="prepend">
<slot name="prepend"></slot>
</span>
<span slot="append">
<slot name="append"></slot>
</span>
</CubeInput>
</template>
<script>
import {Input} from 'cube-ui';
export default {
name: 'InputUfo',
props: ['value'],
computed: {
inputListeners() {
return Object.assign({},
this.$listeners,
{
input: (value) => {
this.$emit('input', value);
}
}
);
}
},
components: {CubeInput: Input}
};
</script>
<style lang="scss" scoped>
.input-number {
margin-bottom: 15px;
background-color: #f5f5f5;
border-radius: 10px;
font-size: 32px;
&:after {
border-radius: 20px;
border-color: #f5f5f5;
}
/deep/ .cube-input-field {
color: #000;
}
}
</style>
... ...
<template>
<Modal v-model="visiable" sure-text="调整售价" cancel-text="取消" :loading="postLoading"
class="modal" :transfer="true" @on-sure="onSure" @on-cancel="onCancel"
>
<div class="change-price-modal">
<p class="modal-title">{{leastPriceMsg}}</p>
<InputUfo type="number" placeholder="定价需以9结尾 例如1999" :maxlength="8" class="ipt-number" v-model="chgPrice">
<span class="prepend" slot="prepend">¥</span>
</InputUfo>
<p class="tips" v-show="errorTip">{{errorTip}}</p>
<p class="price-line">
<span class="title">平台费用:</span>
<span class="price">{{platformFee}}</span>
</p>
<p class="price-line">
<span class="title">银行转账费用:</span>
<span class="price">{{bankTransferFee}}</span>
</p>
<p class="price-line total">
<span class="title">实际收入:</span>
<span class="price">{{income}}</span>
</p>
</div>
</Modal>
</template>
<script>
import Modal from './modal';
import InputUfo from './input-ufo';
import {debounce, get} from 'lodash';
import {createNamespacedHelpers} from 'vuex';
const {mapState, mapActions} = createNamespacedHelpers('order/priceChange');
export default {
name: 'modalPrice',
components: {Modal, InputUfo},
data() {
return {
visiable: false,
skc: {},
platformFee: '-¥0', // 平台费用
bankTransferFee: '-¥0', // 银行转账费用
income: '¥0', // 收入
errorTip: '',
chgPrice: '',
calced: false,
};
},
mounted() {
// debounce防抖动,输入数字后延迟500毫秒提交
this.inputChange = debounce(this.onChange.bind(this), 500);
},
watch: {
chgPrice(newVal) {
this.calced = false;
this.platformFee = '-¥0';
this.bankTransferFee = '-¥0';
this.income = '¥0';
this.inputChange(newVal);
}
},
computed: {
...mapState(['fetchingChangePrice']),
leastPriceMsg() {
if (this.skc.leastPrice) {
return `当前${this.skc.sizeName}码最低售价: ¥${this.skc.leastPrice}`;
} else if (this.skc.suggestMinPrice && this.skc.suggestMaxPrice) {
return `当前${this.skc.sizeName}码建议售价: ¥${this.skc.suggestMinPrice} - ¥${this.skc.suggestMaxPrice}`;
}
return `当前${this.skc.sizeName}码最低售价: ¥-`;
},
postLoading() {
console.log(this.fetchingChangePrice, this.calced);
return this.fetchingChangePrice || !this.calced;
}
},
methods: {
...mapActions(['postCalcPrice']),
resetPrices() {
this.prices = [];
},
show({skc, product}) {
this.chgPrice = '';
this.errorTip = '';
this.prices = [];
this.skc = skc;
this.product = product;
this.visiable = true;
this.calced = false;
},
hide() {
this.skc = {};
this.product = {};
this.visiable = false;
},
onChange(price) {
if (!this.visiable) {
return;
}
if (this.checkPrice(price)) {
this.calcPrice(price);
}
},
async calcPrice(price) {
const result = await this.postCalcPrice({
product_id: this.product.productId,
storage_id: this.skc.storageId,
new_price: price,
old_price: this.skc.price,
num: this.skc.storageNum,
skupType: this.skc.attributes
});
if (result && result.code === 200) {
this.platformFee = get(result, 'data.platformFee.amount', '');
this.bankTransferFee = get(result, 'data.bankTransferFee', '');
this.income = get(result, 'data.income', '');
this.calced = true;
} else {
if (result.message) {
this.errorTip = result.message;
}
this.$createToast({
txt: result.message,
type: 'warn',
}).show();
}
},
checkPrice(price) {
let valid = false;
if (!price) {
this.errorTip = '没有价格';
return false;
} else if (!/^\d+$/.test(price)) {
this.errorTip = '价格只能为正整数';
} else if (!/9$/.test(price)) {
this.errorTip = '出售价格必须以9结尾';
} else if (this.skc.minPrice && price < this.skc.minPrice) {
this.errorTip = '您的出价过低';
} else if (this.skc.maxPrice && price > this.skc.maxPrice) {
this.errorTip = '您的出价格过高';
} else if (+price === this.skc.price) {
this.errorTip = '前后价格没有变化';
} else if (this.skc.suggestMaxPrice && price > this.skc.suggestMaxPrice) {
this.errorTip = '超出建议价将被限制展示,建议下调至合理价格区间';
valid = true;
} else if (this.skc.leastPrice && price > this.skc.leastPrice) {
this.errorTip = '您的出价高于最低售价';
valid = true;
} else {
this.errorTip = '';
valid = true;
}
return valid;
},
onSure() {
if (!this.checkPrice(this.chgPrice)) {
this.$createToast({
txt: this.errorTip,
type: 'warn',
}).show();
} else {
this.$emit('on-change-price', {skc: this.skc, price: this.chgPrice});
}
},
onInput(val) {
this.$emit('input', val);
},
onCancel() {}
}
};
</script>
<style lang="scss" scoped>
.change-price-modal {
.tips {
color: #d0021b;
font-size: 24px;
margin-bottom: 20px;
}
.price-line {
margin-bottom: 20px;
color: #999;
font-size: 24px;
display: flex;
.title {
width: 50%;
}
.price {
width: 50%;
text-align: right;
}
&.total {
color: #000;
}
}
.ipt-number {
/deep/ .prepend {
width: 40px;
margin-left: 20px;
text-align: left;
}
}
.modal-title {
line-height: 100px;
text-align: center;
}
}
</style>
... ...
<template>
<Modal
v-model="visiable"
:loading="fetchingNoSale"
:transfer="true"
sure-text="不卖了"
cancel-text="继续出售"
@on-sure="onSure"
>
<div class="change-price-modal" v-if="skc.storageNum > 1">
<p class="modal-title">选择你要下架的数量</p>
<InputUfo v-model="unStockNum" :maxlength="8" :readonly="true" class="inp-unstock">
<i slot="prepend" class="iconfont iconplus-minus" @touchend="onChangeNum(-1)"></i>
<i slot="append" class="iconfont iconi-add" @touchend="onChangeNum(1)"></i>
</InputUfo>
<p class="stock-txt">
目前还有{{storageNum}}个库存
</p>
<p class="tips">
下架商品保证金会被释放
</p>
</div>
<div class="change-price-modal">
<p class="modal-title">您确定不卖此商品吗?</p>
</div>
</Modal>
</template>
<script>
import Modal from './modal';
import InputUfo from './input-ufo';
import {createNamespacedHelpers} from 'vuex';
const {mapState} = createNamespacedHelpers('order/priceChange');
export default {
name: 'ModalUnstock',
components: {Modal, InputUfo},
data() {
return {
visiable: false,
skc: {},
unStockNum: 1,
storageNum: 1
};
},
computed: {
...mapState(['fetchingNoSale'])
},
methods: {
show({skc}) {
this.skc = skc;
this.unStockNum = 1;
this.storageNum = skc.storageNum;
this.visiable = true;
},
hide() {
this.skc = {};
this.storageNum = 1;
this.unStockNum = 1;
this.visiable = false;
},
onSure() {
this.$emit('on-no-sale', {skc: this.skc, num: this.unStockNum});
},
onInput(val) {
this.$emit('input', val);
},
onChangeNum(num) {
const value = this.unStockNum + num;
if (value <= 0 || value > this.storageNum) {
return;
}
this.unStockNum = value;
}
}
};
</script>
<style lang="scss" scoped>
.change-price-modal {
.stock-txt {
margin: 20px 0 10px 0;
}
.tips {
margin-bottom: 10px;
color: #999;
}
.inp-unstock {
margin-bottom: 10px;
/deep/ .iconfont {
padding: 34px 40px;
width: 112px;
font-size: 32px;
color: #999;
font-weight: 600;
}
/deep/ .cube-input-field {
text-align: center;
}
}
.modal-title {
line-height: 100px;
text-align: center;
}
}
</style>
... ...
<template>
<div class="modal-box" v-show="value" v-transfer-dom :data-transfer="transfer">
<div class="modal-mask"></div>
<div class="modal-wrap">
<div class="modal modal-content">
<div class="modal-body">
<slot>
<div class="text">{{title}}</div>
</slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button class="btn" :class="{active: loading}" type="button" @click="onSure">{{sureText}}</button>
<button class="btn" type="button" @click="onCancel">{{cancelText}}</button>
</slot>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Modal',
props: {
transfer: [Boolean],
title: String,
value: Boolean,
loading: Boolean,
sureText: {
type: String,
default: '确认'
},
cancelText: {
type: String,
default: '取消'
}
},
methods: {
onCancel() {
this.$emit('on-cancel');
this.$emit('input', false);
},
onSure() {
if (!this.loading) {
this.$emit('on-sure');
}
}
},
};
</script>
<style lang="scss">
.modal-box {
font-size: 24px;
}
.modal-mask {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.4);
height: 100%;
z-index: 99;
&-hidden {
display: none;
}
}
.modal {
width: auto;
margin: 0 auto;
position: relative;
outline: none;
&-hidden {
display: none !important;
}
&-wrap {
position: fixed;
overflow: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 99;
-webkit-overflow-scrolling: touch;
outline: 0;
display: flex;
align-items: center;
}
&-content {
position: relative;
background-color: #fff;
/*top: 314px;*/
border: 0;
width: 600px;
background-clip: padding-box;
& .text {
text-align: center;
padding-top: 30px;
padding-bottom: 30px;
}
}
&-body {
padding: 16px 38px;
}
&-footer {
width: 100%;
border-top: 1px solid #eee;
display: flex;
button {
width: 50%;
overflow: hidden;
height: 100px;
background: none;
border: none;
&:first-child {
color: #c94353;
font-weight: 500;
}
}
button + button {
margin-left: 8px;
margin-bottom: 0;
border-left: 1px solid #eee;
}
}
}
</style>
... ...
... ... @@ -21,11 +21,9 @@ export default {
},
methods: {
onChangePrice(params) {
console.log(params);
this.$emit('on-change-price', params);
},
onNoSale(params) {
console.log(params);
this.$emit('on-no-sale', params);
}
},
... ...
... ... @@ -5,7 +5,9 @@
<div class="title">出售中</div>
<!--商品详情-->
<div class="product" @click="onClickProduct">
<ImgSize class="pro-img" :src="productInfo.imageUrl" :width="200" :height="200"></ImgSize>
<div class="pro-img">
<ImgSize :src="productInfo.imageUrl" :width="200"></ImgSize>
</div>
<div class="pro-info">
<p class="pro-name">{{productInfo.productName}}</p>
<p class="stock-info">
... ... @@ -23,6 +25,16 @@
></ProductList>
</div>
</ScrollView>
<ModalUnstock
v-if="modalLoad"
ref="modalUnstock"
@on-no-sale="onNoSaleSure"
></ModalUnstock>
<ModalPrice
v-if="modalLoad"
ref="modalPrice"
@on-change-price="onChangePriceSure"
></ModalPrice>
</LayoutApp>
</template>
... ... @@ -34,11 +46,15 @@ import Logo from './components/logo';
import {createNamespacedHelpers} from 'vuex';
import ImgSize from '../../../components/img-size';
import ProductList from './components/product-list';
import Modal from './components/modal';
import ModalUnstock from './components/modal-unstock';
import {get} from 'lodash';
import ModalPrice from './components/modal-price';
const {mapState, mapActions, mapMutations} = createNamespacedHelpers('order/priceChange')
export default {
components: {ProductList, ImgSize, ScrollView, LayoutApp, Logo},
components: {ModalPrice, ModalUnstock, Modal, ProductList, ImgSize, ScrollView, LayoutApp, Logo},
name: 'PriceChange',
data() {
return {
... ... @@ -63,26 +79,117 @@ export default {
},
mounted() {
this.modalLoad = true;
console.log(this.$route.params);
},
computed: {
...mapState(['productInfo', 'skcs']),
},
methods: {
...mapMutations(['MERGE_CHANGEPRICE_DATA']),
...mapActions(['fetchProduct', 'postNoSale', 'postChangePrice']),
async onPullingUp() {
const beginCount = this.skcs.length;
const result = await this.fetchProduct({
productId: this.$route.params.orderId,
page: this.page + 1,
pageSize: this.pageSize
});
const afterCount = this.skcs.length;
if (afterCount > beginCount) {
this.page++;
}
const noMore = get(result, 'data.data', []).length;
this.$refs.scroll.forceUpdate(noMore > 0);
},
async onPullingDown() {
onPullingDown() {
this.page = 1;
this.fetchProduct({
productId: this.$route.params.orderId,
page: 1,
pageSize: this.pageSize,
refresh: true
}).then(() => {
this.$refs.scroll.forceUpdate();
});
},
/**
* 刷新商品及尺码信息
* @param isNoSale 是否为点击不卖了之后的刷新
* @returns {Promise<void>}
*/
async refreshProduct(isNoSale) {
const result = await this.fetchProduct({
productId: this.$route.params.orderId,
page: this.page,
pageSize: this.pageSize,
refresh: true
});
if (isNoSale && !get(result, 'data.productInfo')) {
this.$yoho.finishPage({});
}
},
onClickProduct() {
},
onChangePrice(skc) {
this.$refs.modalPrice.show({skc, product: this.productInfo});
},
onNoSale(skc) {
this.$refs.modalUnstock.show({skc});
},
async onNoSaleSure({skc, num}) {
console.log(skc, num);
const result = await this.postNoSale({
product_id: this.productInfo.productId,
storage_id: skc.storageId,
old_price: skc.price,
num: num,
skupType: skc.attributes
});
if (result.code === 200) {
this.$refs.modalUnstock.hide();
this.$createToast({
txt: '下架成功',
type: 'correct'
}).show();
this.refreshProduct(true);
} else {
this.$createToast({
txt: result.message || '下架失败',
type: 'warn'
}).show();
}
},
async onChangePriceSure({skc, price}) {
console.log(skc, price);
const result = await this.postChangePrice({
product_id: this.productInfo.productId,
storage_id: skc.storageId,
new_price: price,
old_price: skc.price,
num: skc.storageNum,
skupType: skc.attributes
});
if (result && result.code === 200) {
this.$refs.modalPrice.hide();
this.$createToast({
txt: '调价成功',
type: 'success'
}).show();
this.refreshProduct();
} else {
this.$createToast({
txt: result.message || '调价失败',
type: 'warn',
}).show();
}
}
}
};
... ... @@ -108,6 +215,14 @@ export default {
.pro-img {
width: 200px;
height: 200px;
overflow: hidden;
display: flex;
align-items: center;
img {
width: 100%;
height: auto;
}
}
.logo-wrapper {
... ...
... ... @@ -37,8 +37,6 @@ export default {
});
}
console.log(result.data);
if (result && result.code === 200) {
commit(Types.FETCH_ORDER_PRODUCT_SUCCESS, {
order: result.data,
... ... @@ -59,6 +57,7 @@ export default {
commit(Types.POST_NOSALE_REQUEST);
const result = await this.$api.get('/api/ufo/sellerOrder/batchDownShelf', payload);
console.log('下架=', result);
if (result && result.code === 200) {
commit(Types.POST_NOSALE_SUCCESS, {
order: result.data
... ... @@ -100,7 +99,7 @@ export default {
order: result.data
});
} else {
commit(Types.POST_CHANGEPRICE_FAILED)
commit(Types.POST_CHANGEPRICE_FAILED);
}
return result || {};
... ...
... ... @@ -39,7 +39,7 @@ exports.devServer = (app, cb) => {
};
clientConfig.entry.app = ['./build/client-hot.js', clientConfig.entry.app];
clientConfig.output.publicPath = 'http://2.yohobuy.com:6005/';
clientConfig.output.publicPath = 'http://m.yohobuy.com:6005/';
clientConfig.output.filename = '[name].js';
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
... ...
... ... @@ -33,8 +33,8 @@ module.exports = {
appVersion: '6.9.12', // 调用api的版本
appName: 'xianyu-ufo-app-node',
port: 6001,
siteUrl: '//2.yohobuy.com',
assetUrl: '//2.yohobuy.com:5001/yohoapp-node/',
siteUrl: '//m.yohobuy.com',
assetUrl: '//m.yohobuy.com:5001/yohoapp-node/',
testCode: 'yoho4946abcdef#$%&!@',
domains,
yohoVerifyUdid: 'ca5c462a-e28b-407d-8061-5e204398e3cc',
... ...
... ... @@ -30,6 +30,7 @@ module.exports = {
// 获取调价商品及尺码信息
'/api/ufo/seller/entryGoodsSizeList': {
auth: true,
accessLog: true,
checkSign: true,
ufo: true,
... ... @@ -43,6 +44,7 @@ module.exports = {
// 调价功能中的下架商品(不卖了)
'/api/ufo/sellerOrder/batchDownShelf': {
auth: true,
accessLog: true,
checkSign: true,
ufo: true,
... ... @@ -54,7 +56,10 @@ module.exports = {
num: {type: Number}
}
},
// 预先试算价格,看是否符合调价标准
'/api/ufo/sellerOrder/computeAdjustPrice': {
auth: true,
accessLog: true,
checkSign: true,
ufo: true,
... ... @@ -70,6 +75,7 @@ module.exports = {
// 调价
'/api/ufo/sellerOrder/batchAdjustPrice': {
auth: true,
accessLog: true,
checkSign: true,
ufo: true,
... ...