Commit 5b008700 authored by beisir's avatar beisir

feat:增加地址页面,确认订单页面,订单列表页

parent 1f0c7ba8
import config from '@/config';
import http from '@/service/httpDecorator';
const { talosHost } = config;
export default {
saveAddress(data) {
return http.post(`${talosHost}/api/kdsp/addr/receiver/add_edit`, data);
},
getAddressList() {
return http.get(`${talosHost}/api/kdsp/addr/receiver/list`);
},
deleteAddress(addrReceiverId) {
return http.post(`${talosHost}/api/kdsp/addr/receiver/delete`, {
addrReceiverId
});
},
// 地址级联查询
zoneList: params => {
return http.get(`${talosHost}/api/kdsp/addr/query?addrId=${params}`);
}
};
import config from '@/config';
import http from '@/service/httpDecorator';
import { encryption } from '@/service/encrypt';
const { talosHost } = config;
export default {
// 订单列表
orderList(data) {
// TODO 测试
return http.get(`${talosHost}/api/kdsp/order-info/list/query`, { params: data });
},
// 用户订单确认收货接口
orderConfirm(data) {
return http.post(`${talosHost}/api/kdsp/order-info/receipt/confirm`, data);
},
// 用户订单取消接口【未付款】
orderCancel(data) {
return http.post(`${talosHost}/api/kdsp/order-info/cancel`, data);
},
// 用户订单取消接口【未发货】
orderUnfilledCancel(data) {
return http.get(`${talosHost}/api/kdsp/order-info/unfilled/cancel`, { params: data });
},
// 订单取消原因查询接口
orderCancelReason(params) {
return http.get(`${talosHost}/api/kdsp/order-info/cancel-reason/query`, { params });
},
// 订单详情查询接口
orderDetail(data) {
return http.get(`${talosHost}/api/kdsp/order-info/detail/query`, { params: data });
},
// 物流详情查询接口
expressDetail(data) {
return http.get(`${talosHost}/api/kdsp/logistics/list`, { params: data });
},
// 我的 查询各状态订单数量
orderInfo() {
return http.get(`${talosHost}/api/kdsp/order-info/status-number`);
},
confirmOrder(data) {
return http.post(`${talosHost}/api/kdsp/shop-cart/order-confirm/page/v2`, data);
},
orderSubmit(data) {
return http.post(`${talosHost}/api/kdsp/order-info/e/submit`, encryption(data));
},
// 物流公司列表
companyList() {
return http.get(`${talosHost}/api/kdsp/logistics/kd100/company-list`);
},
// 售后原因查询
applyReason(data) {
return http.get(`${talosHost}/api/kdsp/after-service/apply-reason`, { params: data });
},
// 售后退款金额试算
refundAmount(data) {
return http.get(`${talosHost}/api/kdsp/after-service/refund-amount`, { params: data });
},
// 提交申请售后
applySubmit(data) {
return http.post(`${talosHost}/api/kdsp/after-service/apply`, data);
},
// 订单申请退款
refundKepler(data) {
return http.post(`${talosHost}/api/kdsp/order-info/apply-refund`, data);
},
// 查看售后详情
afterDetail(data) {
return http.get(`${talosHost}/api/kdsp/after-service/detail`, { params: data });
},
// 售后取消
applyCancel(data) {
return http.post(`${talosHost}/api/kdsp/after-service/cancel`, data);
},
// 售后退货物流单信息填写 after-service/add-delivery-no
addDeliveryNo(data) {
return http.post(`${talosHost}/api/kdsp/after-service/add-delivery-no`, data);
},
// 售后列表 after-service/list
afterList(data) {
return http.get(`${talosHost}/api/kdsp/after-service/service/list`, { params: data });
},
// 售后催办 after-service/reminder
afterReminder(data) {
return http.get(`${talosHost}/api/kdsp/after-service/reminder`, { params: data });
},
//查询售后是否在有效期
applyExpired(data) {
return http.get(`${talosHost}/api/kdsp/after-service/expired`, { params: data });
},
// 获取七牛token
getQiniuToken(data) {
return http.get(`${talosHost}/api/kdsp/common/upload/token`, { params: data });
}
};
import request from '@/service/httpDecorator';
import config from '@/config';
import uiTrack from '@/service/sa.service';
const { talosHost } = config;
const queryPayInfo = function(data) {
return request.post(`${talosHost}/open/checkout`, data);
};
const prepay = async function(data) {
const scDeviceId = await uiTrack.run('saDeviceId');
return request.post(`${talosHost}/open/checkout/prepay`, data, {
customHeader: {
scDeviceId
},
hideToast: true
});
};
const pay = async function(data) {
const scDeviceId = await uiTrack.run('saDeviceId');
return request.post(`${talosHost}/open/checkout/pay`, data, {
customHeader: {
scDeviceId
},
hideToast: true
});
};
const queryPayStatus = function(data) {
return request.post(`${talosHost}/open/checkout/pay_status/query`, data);
};
const sendSms = function(data) {
return request.post(`${talosHost}/open/checkout/send_sms`, data);
};
const desSalt = function() {
return request.get(`${talosHost}/vcc/account/salt`);
};
const h5AppyUrl = function() {
return request.get(`${talosHost}/vcc/xyqb_mall/app_url`);
};
const getCoupon = async function(params) {
const scDeviceId = await uiTrack.run('saDeviceId');
return request.post(`${talosHost}/open/checkout/pay_coupon_list`, params, {
customHeader: {
scDeviceId
}
});
};
const ocrFaceId = function(params) {
return request.post(`${talosHost}/open/checkout/ocr_faceId`, params);
};
const getGoodsList = async function(data) {
const scDeviceId = await uiTrack.run('saDeviceId');
return request.get(`${talosHost}/vcc/xyqb/recommend/goods-list`, {
params: data,
hideLoading: true,
customHeader: {
scDeviceId
}
});
};
// KA流程节点
const kaGetNextUrl = function() {
return request.get(`${talosHost}/api/kdsp/ka/process/get-next-url`);
};
const reissueContract = function(params) {
return request.post(`${talosHost}/open/checkout/payReissueContract`, params);
};
export default {
// 商品详情页面
desSalt() {
return request.get(`${talosHost}/vcc/account/salt`);
}
};
const getCouponList = function(params) {
return request.get(`${talosHost}/api/kdsp/appconfig/pay-succ-page/guide-stream`, { params });
};
export {
pay,
prepay,
sendSms,
desSalt,
h5AppyUrl,
getCoupon,
ocrFaceId,
queryPayInfo,
getGoodsList,
kaGetNextUrl,
getCouponList,
queryPayStatus,
reissueContract
};
<template>
<div>
<div v-if="type === 'shoppingCar'" class="goods-bottom">
<div class="goods-bottom-icon" @click="goHome">
<span class="iconfont icon-home" />
<span>首页</span>
</div>
<!-- <navigator v-if="type === 'shoppingCar'" url="/pages/cart/cart" class="goods-bottom-icon">
<text class="iconfont icon-cart-card" />
<text>购物车</text>
</navigator>
<button class="goods-bottom-car">加入购物车</button> -->
<button class="goods-bottom-buy" :class="{ disabled: disabled }" @click="buy">
{{ shopBtnName }}
</button>
</div>
<div v-if="type === 'submitOrder'" class="goods-bottom">
<div class="goods-bottom-text">
<div class="Gb-text-top">
<span>合计:</span>
<span>¥</span>
<span>{{ info.totalPayFee || '0.00' }}</span>
</div>
</div>
<button class="goods-bottom-buy confirm-order-btn" @click="buy">提交订单</button>
</div>
</div>
</template>
<script>
export default {
name: 'BottomNav',
props: {
type: {
type: String,
default: 'shopCar' // shoppingCar 购物车;settlement 结算;submitOrder 确认订单
},
shopBtnName: {
type: String,
default: '立即购买' // shoppingCar 购物车;settlement 结算;submitOrder 确认订单
},
info: {
type: Object,
default() {
return {};
}
},
disabled: {
type: Boolean,
default: false
},
goodsId: {
type: String,
default: ''
}
},
methods: {
buy() {
this.$emit('buy');
},
goHome() {
setTimeout(() => {
this.$router.push({ name: 'home' });
}, 500);
}
}
};
</script>
<style lang="less" scoped>
.goods-bottom {
position: fixed;
left: 0;
bottom: 0;
display: flex;
justify-content: space-evenly;
align-items: center;
width: 100%;
height: 30px;
padding: 10px 0;
background: @white;
border-top: 2px solid #f7f7f7;
&-radio {
display: flex;
align-items: center;
justify-content: space-around;
padding-top: 8px;
width: 140px;
radio {
transform: scale(0.7);
}
span {
font-size: 24px;
color: #999999;
}
}
&-text {
display: flex;
flex-direction: column;
align-items: flex-end;
flex: 1;
margin: 0 20px;
.Gb-text-top {
display: flex;
align-items: center;
span {
&:nth-child(1) {
padding-top: 4px;
.text-12();
color: #333333;
}
&:nth-child(2) {
padding-top: 4px;
.text-12();
color: #ec3333;
}
&:nth-child(3) {
.text-18();
font-weight: bold;
color: #ec3333;
}
}
}
.Gb-text-bottom {
.text-12();
color: #999999;
}
}
&-icon {
display: flex;
align-items: center;
flex-direction: column;
span {
&:first-child {
.text-20();
}
&:last-child {
.text-10();
color: #999999;
}
}
}
&-car {
border: @border-sm solid @border-color-search;
background: @white;
color: @font-color-search;
}
&-buy {
color: @white;
background-image: @background-primary;
}
button {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
width: 305px;
height: 40px;
font-size: 17px;
font-weight: bold;
border-radius: 20px;
&.disabled {
opacity: 0.7;
}
}
}
.confirm-order-btn {
width: 130px !important;
margin-right: 10px !important;
}
</style>
<template>
<div class="coupon-list">
<h3 class="coupon-title">选择{{ feeTitle }}</h3>
<div class="coupon-list__list">
<template v-if="list.length">
<div v-for="(info, index) in list" :key="index" @click="handleSelectCoupon(info.id)">
<div v-if="couponType == 4" class="minus-list__item">
<div class="minus-list__left">
<p>{{ info.activityDesc }}</p>
<span>活动有效期:{{ info.startDate }}-{{ info.endDate }}</span>
</div>
<div class="minus-list__right">
<cr-icon v-if="info.isSelect" type="checked" color="#ec3333" size="25px" />
<cr-icon v-else type="circle" color="#dcdcdc" size="25px" />
</div>
</div>
<div v-else class="coupon-list__item">
<img :src="info.iconUrl" class="coupon-list__tag" />
<div class="coupon-list__left">
<div class="coupon-list__amount">
<span>{{ currency }}</span>
{{ info.faceValue }}
</div>
<div class="coupon-list__limit">{{ info.limitDesc }}</div>
</div>
<div class="coupon-list__content">
<div class="coupon-list__desc">{{ info.name }}</div>
<div class="coupon-list__date">{{ info.couponValidTime }}</div>
</div>
<div class="coupon-list__right">
<cr-icon v-if="info.isSelect" type="checked" color="#ec3333" size="25px" />
<cr-icon v-else type="circle" color="#dcdcdc" size="25px" />
</div>
</div>
</div>
</template>
<div v-else class="coupon-list__empty"><empty text="暂无优惠券" /></div>
</div>
<div v-if="list.length" class="coupon-list__bottom">
<button @click="handleRadioSubmit">确定</button>
</div>
</div>
</template>
<script>
import { getCouponChooseList } from '@/api/groupBuy';
import { TITLE_LIST } from '@/constants/order';
import Empty from './empty';
export default {
name: 'CouponList',
components: {
Empty
},
props: {
couponId: {
type: [String, Number],
default: 0
},
couponType: [String, Number],
currency: {
type: String,
default: '¥'
},
goodsInfo: {
type: Array,
default: () => []
},
couponActivityInfoList: {
type: Array,
default: () => []
}
},
data() {
return {
list: []
};
},
computed: {
selectedCoupon() {
return this.list.filter(l => {
if (l.isSelect) return l;
});
},
feeTitle() {
return TITLE_LIST[this.couponType];
}
},
watch: {
couponId: {
handler() {
this.getCouponChooseList();
},
immediate: true
},
couponType: {
handler() {
this.getCouponChooseList();
},
immediate: true
}
},
methods: {
handleSelectCoupon(num) {
this.list.map(item => {
if (item.id == num) {
item.isSelect = !item.isSelect;
} else {
item.isSelect = false;
}
});
},
handleRadioSubmit() {
this.$emit('coupon-select', this.selectedCoupon);
},
async getCouponChooseList() {
if (!this.goodsInfo.length) return;
this.list = [];
const selectedSkuList = this.goodsInfo.map(item => {
const { skuSource, count, skuId } = item;
return {
skuNo: skuId,
skuNum: count,
skuSource
};
});
const selectedCouponActivityList = [];
this.couponActivityInfoList.forEach(item => {
selectedCouponActivityList.push({
couponActivityType: item.couponActivityType,
couponActivityUseId: item.couponActivityUseId
});
});
const [res] = await getCouponChooseList({
selectedSkuList,
couponActivityType: this.couponType,
selectedCouponActivityList
});
if ((res && res.coupons) || (res && res.activityList)) {
const data = this.couponType == 4 ? res.activityList : res.coupons;
data.forEach(u => {
u.id = this.couponType == 4 ? u.activityId : u.pickupId;
if (u.id == this.couponId) u.isSelect = true;
else u.isSelect = false;
});
this.list = data;
}
}
}
};
</script>
<style lang="less" scoped>
.coupon-title {
text-align: center;
margin: 0 auto;
margin-top: 15px;
width: 50%;
.text-18();
}
.coupon-list {
&__bottom {
padding: 10px 20px 15px 20px;
position: relative;
box-sizing: border-box;
button {
width: 100%;
background-image: linear-gradient(269deg, #ff5d00 12%, #ff1900 86%);
border-radius: 80px;
height: 40px;
.text-14();
line-height: 40px;
color: #fff;
}
}
&__list {
padding: 10px 10px 0;
overflow-y: scroll;
overflow-x: hidden;
height: 50vh;
}
&__scroll {
height: 100%;
}
&__empty {
padding-top: 50px;
}
&__item {
position: relative;
display: flex;
align-items: center;
background: #ffece6;
border-radius: 6px;
padding: 18px 10px;
margin-bottom: 10px;
&--disabled {
-webkit-filter: grayscale(100%) opacity(0.8); /* Chrome, Safari, Opera */
filter: grayscale(100%) opacity(0.8);
.cr-coupon-list__right {
display: none;
}
}
}
&__tag {
position: absolute;
top: 0;
left: 0;
width: 44px;
height: 16px;
}
&__left {
text-align: center;
color: #ec3333;
}
&__amount {
font-size: 30px;
font-weight: 500;
line-height: 36px;
margin: 0;
text {
font-size: 16px;
line-height: 22px;
}
}
&__limit {
.text-12();
color: #ec3333;
line-height: 17px;
margin: 0;
}
&__content {
padding: 0 18px;
}
&__desc {
.text-14();
line-height: 20px;
font-weight: 500;
color: #333;
margin-bottom: 3px;
}
&__date {
.text-12();
color: #666666;
line-height: 17px;
}
&__right {
margin-left: auto;
.radio {
width: 30px;
height: 30px;
display: block;
transform: scale(0.8);
background-color: red !important;
}
}
}
::v-deep .cr-icon {
line-height: 28px !important;
}
.minus-list {
&__list {
padding: 10px 10px 0;
overflow-y: scroll;
overflow-x: hidden;
height: 50vh;
}
&__item {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 6px;
padding: 6px 10px;
margin-bottom: 10px;
}
&__left {
.text-14();
span {
color: @font-color-base;
.text-12();
}
}
}
</style>
<template>
<view class="empty-content">
<image src="https://img.lkbang.net/xcx/empty@2x.png" mode="aspectFit" />
<text v-if="text">{{ text }}</text>
</view>
</template>
<script>
export default {
props: {
src: {
type: String,
default: 'empty'
},
text: {
type: String,
default: '暂无订单~'
}
},
data() {
return {
typeSrc: {
empty: ''
}
};
},
computed: {
setSrc() {
return this.typeSrc[this.src];
}
}
};
</script>
<style lang="less">
.empty-content {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin: 0 10px 10px;
padding: 10px;
height: 211px;
background: @white;
border-radius: 6px;
image {
margin-bottom: 10px;
width: 150px;
height: 150px;
}
text {
font-size: 16px;
color: #333333;
}
}
</style>
<template>
<div class="address-picker-wrap">
<div
:class="[
'address-picker-wrap-txt',
{ placeholder: !pickerValShow && placeholder === '省市区县、乡镇等' }
]"
@click="openPopup"
>
{{ pickerValShow || placeholder }}
</div>
<cr-popup
v-model="show"
position="bottom"
closeable
:close-on-click-overlay="false"
class="popup-head"
>
<div class="popup-title">请选择所在地区</div>
<cr-tabs
ref="tabs"
v-model="active"
class="popup-tab"
:ellipsis="true"
color="#ee4155"
@click="handleTabClick"
>
<cr-tab :title="provinceName">
<ul class="area-panel">
<li
v-for="(it, idx) in areaData[0]"
:key="idx"
:class="['area-panel__item', { active: provinceTag.addrId === it.addrId }]"
@click.self="handleItemSelect(0, it)"
>
{{ it.addrName }}
</li>
</ul>
</cr-tab>
<cr-tab :title="cityName">
<ul class="area-panel">
<li
v-for="(it, idx) in areaData[1]"
:key="idx"
:class="['area-panel__item', { active: cityTag.addrId === it.addrId }]"
@click.self="handleItemSelect(1, it)"
>
{{ it.addrName }}
</li>
</ul>
</cr-tab>
<cr-tab :title="distName">
<ul class="area-panel">
<li
v-for="(it, idx) in areaData[2]"
:key="idx"
:class="['area-panel__item', { active: distTag.addrId === it.addrId }]"
@click.self="handleItemSelect(2, it)"
>
{{ it.addrName }}
</li>
</ul>
</cr-tab>
<cr-tab :title="townName">
<ul class="area-panel">
<li
v-for="(it, idx) in areaData[3]"
:key="idx"
:class="['area-panel__item', { active: townTag.addrId === it.addrId }]"
@click.self="handleItemSelect(3, it)"
>
{{ it.addrName }}
</li>
</ul>
</cr-tab>
</cr-tabs>
</cr-popup>
</div>
</template>
<script>
import address from '@/api/address.api';
const areaDefault = ['请选择', '请选择', '请选择', '请选择'];
const tagMap = ['provinceTag', 'cityTag', 'distTag', 'townTag'];
export default {
name: 'PopupAreaTabPicker',
props: {
value: null,
placeholder: [String, Array]
// placeholder: {
// type: String,
// default: '请选择'
// }
},
data() {
return {
show: false,
active: 1, // 定位cur下标
selectData: [],
cityTag: {},
provinceTag: {},
distTag: {},
townTag: {},
areaData: [[], [], [], []], // 缓存四个tab数据列表
pickerValShow: '',
addressValue: []
};
},
computed: {
provinceName() {
return this.areaData[0].length > 0 ? this.provinceTag.addrName : '';
},
cityName() {
return this.areaData[1].length > 0 ? this.cityTag.addrName : '';
},
distName() {
return this.areaData[2].length > 0 ? this.distTag.addrName : '';
},
townName() {
return this.areaData[3].length > 0 ? this.townTag.addrName : '';
}
},
watch: {
show: {
immediate: true,
handler(val) {
this.$emit('popup', val);
}
}
},
mounted() {
this.init();
},
methods: {
handleTabClick(name) {
// 清空当前选中tab之后的选中文字
for (let i = name + 1; i < 4; i++) {
this[tagMap[i]] = {
addrId: '',
addrLevel: i,
addrName: areaDefault[i]
};
}
},
handleItemSelect(index, it) {
const selectData = JSON.parse(JSON.stringify(this.selectData));
const active = index + 1;
selectData[index] = it;
this[tagMap[index]] = it;
for (let i = index + 1; i < 4; i++) {
this[tagMap[i]] = {
addrId: '',
addrLevel: i,
addrName: areaDefault[i]
};
selectData[i] = {
addrId: '',
addrName: '',
addrLevel: i
};
this.areaData[i] = [];
}
this.getAddrList(it.addrId, active);
this.$set(this, 'selectData', selectData);
if (index === 3) {
this.show = false;
this.submit();
}
},
// 获取地址列表
async getAddrList(addrId = '', index = 0) {
const areaData = JSON.parse(JSON.stringify(this.areaData)); // 浅拷贝
const res = await address.zoneList(addrId); // 获取地址列表默认取一级
const addrList = res[0].addrList || [];
areaData[index] = addrList;
if (addrList && addrList.length) {
// 点击最后一级别addrId 所得addrList数据为空
this.active = index;
} else {
this.show = false; // 隐藏底部弹出层
this.submit(); // 提交选中数据
}
this.$set(this, 'areaData', areaData);
},
submit() {
const address = this.selectData.filter(item => item.addrId);
const addressName = address.map(item => item.addrName);
const addressValue = address.map(item => item.addrId);
this.pickerValShow = addressName.join(',');
this.$emit('input', addressValue);
this.$emit('finish', {
id: addressValue,
items: this.selectData
});
},
init() {
this.selectData = [];
this.areaData = [[], [], [], []];
this.pickerValShow = '';
this.selectData[0] = {
addrId: '',
addrName: '',
addrLevel: '1'
};
for (let i = 0; i < 4; i++) {
this[tagMap[i]] = {
addrId: '',
addrLevel: i + 1,
addrName: areaDefault[i]
};
}
this.getAddrList();
},
openPopup() {
this.show = true;
this.$refs.tabs.setLine();
if (this.value) {
this.active = this.addressValue.length - 1;
} else {
this.active = 0;
}
},
reset() {
this.init();
}
}
};
</script>
<style lang="less" scoped>
// 弹出框头部
.popup-head {
border-radius: 6px 6px 0 0;
.popup-title {
.text-16();
font-weight: @font-weight-bold;
color: @black;
text-align: center;
padding: 10px 0;
}
.popup-tab {
@{deep} .cr-tabs__item {
color: #000;
}
@{deep} .cr-tabs__item--active {
color: #ec1500;
}
}
}
.address-picker-wrap {
width: 100%;
&-txt {
width: 100%;
font-size: 15px;
color: #333333;
&.placeholder {
color: #b2b2b2;
opacity: 0.7;
}
}
@{deep} .cr-popup--close-top-right {
position: absolute !important;
}
@{deep} .cr-popup--close {
z-index: 999;
color: #333;
font-size: 18px;
padding: 10px;
top: 4px;
right: 4px;
}
.cr-tabs {
&__wrap {
border-bottom: 1px solid #eeeeee;
}
&__item {
flex: none;
width: 70px;
height: 40px;
font-size: 13px;
text-align: center;
line-height: 40px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
line-clamp: 1;
-webkit-box-orient: vertical;
}
}
}
.area-panel {
height: 300px;
overflow-x: scroll;
overflow-y: scroll;
&__item {
padding: 10px 15px;
font-size: 14px;
color: #333;
&.active {
color: #ee4155;
}
}
}
</style>
<template>
<div class="uni-numbox">
<div class="uni-numbox-minus" @click="_calcValue('subtract')">
<span class="yticon" :class="canSubtract ? 'uni-numbox-disabled' : ''"> - </span>
</div>
<input class="uni-numbox-value" type="number" :disabled="disabled" :value="value" />
<div class="uni-numbox-plus" @click="_calcValue('add')">
<span class="yticon" :class="canAdd ? 'uni-numbox-disabled' : ''"> + </span>
</div>
</div>
</template>
<script>
export default {
name: 'Stepper',
props: {
value: {
type: Number,
default: 0
},
disabled: {
type: Boolean,
default: false
},
index: {
type: Number,
default: 0
},
canAdd: Boolean,
canSubtract: Boolean,
min: {
type: Number,
default: -Infinity
},
max: {
type: Number,
default: Infinity
}
},
data() {
return {
inputValue: this.value
};
},
methods: {
_calcValue(type) {
if (type === 'subtract') {
if (this.canSubtract || this.inputValue >= this.min) {
this.$emit('change', { type, index: this.index });
}
} else if (type === 'add') {
if (this.canAdd || this.inputValue <= this.max) {
this.$emit('change', { type, index: this.index });
}
}
}
}
};
</script>
<style lang="less" scoped>
.uni-numbox {
display: flex;
justify-content: flex-start;
align-items: center;
height: 24px;
background: #fff;
}
.uni-numbox-minus,
.uni-numbox-plus {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
background-color: #f2f3f5;
width: 24px;
height: 100%;
text-align: center;
position: relative;
}
.uni-numbox-minus .yticon,
.uni-numbox-plus .yticon {
.text-14();
color: #555;
}
.uni-numbox-minus {
border-right: none;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
.uni-numbox-plus {
border-left: none;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
.uni-numbox-value {
position: relative;
background-color: #f2f3f5;
width: 38px;
height: 24px;
text-align: center;
padding: 0;
.text-14();
margin: 0 2px;
}
.uni-numbox-disabled.yticon {
color: #d6d6d6;
}
</style>
export const APP_ID = '102';
export const PUBLIC_KEY = `
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCGf4wnNJXHXM54wsmxTwhpiPaAp27zM
3QsrWu1+GOMoCDmAEOb2jYjyNhwBIeV9eY0fwoK+MfWBywbonypyXs1j5l/mTmFRQ8IZY
+xZF0t01cornoMpKJncoNiwqc9OnsiwGPqzIs/iLalBonppqhWSn0g99vFS2qgr0WnOWl
qLQIDAQAB
-----END PUBLIC KEY-----
`;
export const PRIVATE_KEY = `
-----BEGIN RSA PRIVATE KEY-----
MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIC46noXi7VhxDMCQNK3x
k7uVQICaIwhQ6Ye5c5XYaM9IgUhFUioQZ3llelM3oAnYqLuAZMU0POcNqm8qmr7I3fc8A
SZZKm5XUm6i/PI++xT+5c9zhHHsHumoSoTJ/bpS8xNhDFz/X2JQTIn6izFtBBnsMJRx5K
aJWXYebk1RyCVAgMBAAECgYA27bGxE+ccKXINykJbKOzItc80ok52raMuejTaTlNt0yJ3
SdzJOnN1q4jDG3g++4+Nsz6cwt8/dUOmPsoCCNTjMDUg40Cl8NopPMXvo/INwd2vJSEFr
JyC629pl3N7E18Iqjdt8jMy/zEMSDMPh7+NDtSDQqfQpeT0w31/Q1qdwQJBALtk/xIIPN
/zyCZcAIKtXvp0FrHslEL55q01kV6bwhyfAnyz+GcGJwxPWOKJ1FRCnzjOZRc3ygekMZU
WGLjszDkCQQCv2Qo/q97pjw4QwxsTwuuQKlLhAuBr2W0eTsnou9tk3i7PJX/xQXlccp0C
FRWAe3xdpYtapUiMJ8zpKkw+PC89AkBdn9UMRk9buKmL+LVMlJ/6U5uvIzrjx4UnjrblZ
L5znEIa2bPGjT1fGhmXfTM3Md3o/L1m/zmR3cfj65lIcw6JAkB0uf6qmz0CDnrIt6pOqN
HTRbT0NgOgs5hSSPyQJ7sPrsilqn/ONqcHrfD5A+PdAJtiUlQ5nIOWvYiwseogWbUFAkB
y6/cB1HblHJzuyJwaAW9vLNmbqeKW5DhU2QG7Jp1uUgk4GcopDAgFWlAjbQcBCGDyStHH
YxINn+qpKviRkeFf
-----END RSA PRIVATE KEY-----
`;
export const DISCOUNT = 1;
export const FREIGHT = 2;
export const CASH = 3;
export const REDUCTION = 4;
export const HAS_DISCOUNT = 2;
export const TITLE_LIST = {
[DISCOUNT]: '优惠券',
[FREIGHT]: '运费券',
[CASH]: '现金券',
[REDUCTION]: '活动优惠'
};
import store from '@/store';
export default {
data() {
return {
prevRoute: ''
};
},
// ! 注意:组件名称需要与route中的name一致,route中只支持驼峰
beforeRouteEnter(to, from, next) {
store.commit('ADD_KEEP_ALIVE', to.name);
next(vm => {
if (!vm.prevRoute) vm.prevRoute = from.name;
});
},
beforeRouteLeave(to, from, next) {
this.prevRoute === to.name && store.commit('DEL_KEEP_ALIVE', from.name);
next();
}
};
...@@ -10,5 +10,38 @@ module.exports = [ ...@@ -10,5 +10,38 @@ module.exports = [
name: 'groupBuyList', name: 'groupBuyList',
component: () => import('../views/goodsList/index.vue'), component: () => import('../views/goodsList/index.vue'),
meta: { title: '0元购大牌' } meta: { title: '0元购大牌' }
},
{
path: '/orderList',
name: 'orderList',
meta: {
title: '我的订单'
},
component: () => import('../views/orderList/index.vue')
},
{
path: '/createOrder',
name: 'createOrder',
meta: {
title: '确认订单',
keepAlive: true // 需要缓存
},
component: () => import('../views/order/createOrder')
},
{
path: '/address',
name: 'address',
meta: {
title: '我的地址'
},
component: () => import('../views/address/address')
},
{
path: '/addressManage',
name: 'addressManage',
meta: {
title: '新增地址'
},
component: () => import('../views/address/addressManage')
} }
]; ];
/*
* @Description: 数据加密,aes加密数据主体,rsa加密aes密钥,aes密钥前端自己存储
* @Date: 2020-12-08 11:08:28
* @LastEditors: gzw
* @LastEditTime: 2021-07-07 17:44:22
*/
import { cipher as AES, util as UTIL, pki as PKI, md as SHA1 } from 'node-forge';
import uuidv1 from 'uuid/v1';
import { parseTime } from './utils.service';
import { APP_ID, PUBLIC_KEY, PRIVATE_KEY } from '@/config/encrypt.config';
// import CryptoJS from 'crypto-js';
// import pay from '@/api/pay.api';
/**
* @description: 数据加密
* @param {String} data 数据源
* @return {String} 加密后的数据base64
*/
export function encryption(data = '') {
if (!data) return null;
const key = generateRandomStr(16);
const iv = key; // 后台约定iv与key一致
const plaintext = typeof data === 'object' ? JSON.stringify(data) : data;
const body = encryptDataByAes(plaintext, key, iv); // AES加密数据
const encryptKey = encryptDataByPb(key); // RSA公钥加密AES密钥
const signData = generateSign(plaintext);
return {
appId: APP_ID,
body,
encryptKey,
...signData
};
}
/**
* @description: 数据加密
* @message {String} message 数据源
* @return {String} 加密后的数据16进制
*/
// export async function encryptByDESModeEBC(message) {
// console.log(message);
// const [{ payPwdSalt }] = await pay.desSalt();
// var keyHex = CryptoJS.enc.Utf8.parse(payPwdSalt);
// var encrypted = CryptoJS.DES.encrypt(message, keyHex, {
// mode: CryptoJS.mode.ECB,
// padding: CryptoJS.pad.Pkcs7
// });
// return encrypted.ciphertext.toString().toLocaleUpperCase();
// }
/**
* @description: AES加密数据,默认CBC, Pki#cs7
* @param {String} txt 数据源
* @param {String} key 密钥,16位字符串
* @param {String} iv 初始化向量
* @return {String} 加密后的数据base64
*/
function encryptDataByAes(txt, key, iv) {
const cipher = AES.createCipher('AES-CBC', key);
cipher.start({ iv });
cipher.update(UTIL.createBuffer(txt, 'utf8'));
cipher.finish();
const ciphertext = cipher.output.getBytes();
return buffer2Base64(ciphertext);
}
/**
* @description: 使用RSA公钥加密数据
* @param {String} txt 数据源
* @return {String} 加密后的数据base64
*/
function encryptDataByPb(txt) {
const publicKey = PKI.publicKeyFromPem(PUBLIC_KEY);
const pbData = publicKey.encrypt(txt);
return buffer2Base64(pbData);
}
/**
* @description: RSA私钥+SHA1生成签名
* 签名组成结构nonce+appid+timestamp+body
* @param {String} txt 数据源
* @return {Object} 生成的sign数据,时间戳、Nonce
*/
function generateSign(txt) {
const timestamp = parseTime('');
const nonce = generateNonce();
const privateKey = PKI.privateKeyFromPem(PRIVATE_KEY);
const md = SHA1.sha1.create();
md.update(nonce + APP_ID + timestamp + txt, 'utf8');
let sign = privateKey.sign(md);
sign = buffer2Base64(sign);
return {
timestamp,
nonce,
sign
};
}
/**
* @description: buffer转base64
* @param {Buffer} buf buffer源数据
* @return {String} base64 字符串
*/
function buffer2Base64(buf) {
return UTIL.encode64(buf);
}
/**
* @description: 生成nonce(uuid)
* 规则:以当前时间的uuid作为name,以随机生成的uuid作为namespace,生成最终的uuid
* @return {String} 生成的uuid
*/
function generateNonce() {
return uuidv1();
}
/**
* @description: 生成随机字符串
* @param {Number} n 位数
* @return {String} 生成的字符串
*/
function generateRandomStr(n) {
const len = n || 32;
const chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz0123456789';
const maxPos = chars.length;
let pwd = '';
for (let i = 0; i < len; i++) {
pwd += chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
}
import { isAndroid, isIOS } from './validation.service'; import { isAndroid, isIOS } from './validation.service';
import Vue from 'vue';
export const EventBus = new Vue();
/** /**
* 替换邮箱字符 * 替换邮箱字符
* @param {String} email 输入字符串 * @param {String} email 输入字符串
......
...@@ -4,7 +4,8 @@ const state = { ...@@ -4,7 +4,8 @@ const state = {
header: true, header: true,
title: '支付中心', title: '支付中心',
loading: false, loading: false,
meta: {} meta: {},
keepAliveMap: []
}; };
// getters // getters
...@@ -23,6 +24,9 @@ const actions = { ...@@ -23,6 +24,9 @@ const actions = {
}, },
change_loading({ commit }, loading) { change_loading({ commit }, loading) {
commit(types.CHANGE_LOADING, loading); commit(types.CHANGE_LOADING, loading);
},
add_keep_alive({ commit }, name) {
commit(types.ADD_KEEP_ALIVE, name);
} }
}; };
...@@ -46,6 +50,20 @@ const mutations = { ...@@ -46,6 +50,20 @@ const mutations = {
for (let i in meta) { for (let i in meta) {
state.meta[i] = meta[i]; state.meta[i] = meta[i];
} }
},
[types.ADD_KEEP_ALIVE](state, name) {
const keepAliveMap = state.keepAliveMap;
keepAliveMap.push(name);
state.keepAliveMap = [...new Set(keepAliveMap)];
},
[types.DEL_KEEP_ALIVE](state, name) {
state.keepAliveMap.splice(
state.keepAliveMap.findIndex(item => item === name),
1
);
},
[types.CLEAR_KEEP_ALIVE](state) {
state.keepAliveMap = [];
} }
}; };
......
...@@ -2,3 +2,7 @@ export const CHANGE_HEADER = 'CHANGE_HEADER'; ...@@ -2,3 +2,7 @@ export const CHANGE_HEADER = 'CHANGE_HEADER';
export const CHANGE_TITLE = 'CHANGE_TITLE'; export const CHANGE_TITLE = 'CHANGE_TITLE';
export const CHANGE_META = 'CHANGE_META'; export const CHANGE_META = 'CHANGE_META';
export const CHANGE_LOADING = 'CHANGE_LOADING'; export const CHANGE_LOADING = 'CHANGE_LOADING';
export const ADD_KEEP_ALIVE = 'ADD_KEEP_ALIVE';
export const DEL_KEEP_ALIVE = 'DEL_KEEP_ALIVE';
export const CLEAR_KEEP_ALIVE = 'CLEAR_KEEP_ALIVE';
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
@red-dark: #ee0a24; @red-dark: #ee0a24;
@orange: #faab0c; @orange: #faab0c;
@red-btn: #ff5d00; @red-btn: #ff5d00;
@green: #07C160;
@grey-border: #f2f3f5; @grey-border: #f2f3f5;
// Gradient Colors // Gradient Colors
......
<template>
<div class="address">
<EmptyAddress v-if="isEmpty" />
<div v-else>
<div v-for="(item, index) in addressList" :key="index" class="card" @click="selectIt(item)">
<div class="card__left">
<div class="info">
<span v-if="item.addrDefault" class="tag">默认</span>
<span class="name">{{ item.receiverName }}</span>
<span class="phone">{{ item.receiverPhoneNo }}</span>
</div>
<div class="detail">{{ item.addrFullName }}</div>
</div>
<div v-if="edit" class="card__right" @click.stop="toEdit(item)">
<button>编辑</button>
</div>
</div>
</div>
<div class="bar-bottom">
<button class="add-btn" type="default" @click="toAdd">
添加新收货地址
</button>
</div>
</div>
</template>
<script>
import EmptyAddress from './emptyAddress';
import address from '@/api/address.api';
import localStorage from '@/service/localStorage.service';
export default {
components: {
EmptyAddress
},
data() {
return {
addressList: [],
edit: true,
isSelect: false,
order: ''
};
},
computed: {
isEmpty() {
return !this.addressList.length;
}
},
mounted() {
this.isSelect = this.$route.query.source == 1;
this.loadAddress();
},
methods: {
toEdit(item) {
const params = {
editAddress: item
};
this.$router.push({
name: 'addressManage',
params,
query: {
type: 'edit'
}
});
},
selectIt(e) {
if (!this.isSelect) return;
const addressList = { ...e, noAddr: true };
localStorage.set('addressList', addressList);
this.$router.go(-1); // 返回上一页
// this.$router.push({ name: 'createOrder' });
},
toAdd() {
this.$router.push({ name: 'addressManage' });
},
async loadAddress() {
const [res] = await address.getAddressList();
this.addressList = res && res.addrReceiverList ? res.addrReceiverList : [];
if (!this.addressList.length) {
localStorage.remove('addressList');
}
}
}
};
</script>
<style lang="less" scoped>
.address {
.card {
background-color: @white;
border-radius: @border-radius-sm;
padding: @padding-sm 0 @padding-sm @padding-xs + 2;
margin: 10px;
display: flex;
.divider {
display: inline-block;
}
.card__left {
flex: 1;
.info {
display: flex;
align-items: center;
margin-bottom: @padding-unit + 4;
}
.tag,
.name {
margin-right: 5px;
}
.tag {
background-image: linear-gradient(269deg, #ff4b00 12%, #ff7705 86%);
color: @white;
border-radius: @border-width-base * 3;
font-size: 11px;
letter-spacing: -0.27px;
width: 36px;
height: 16px;
line-height: 16px;
text-align: center;
}
.name,
.phone {
color: @black;
.text-13();
}
.detail {
display: inline-block;
word-break: break-all;
span-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
color: @gray-5;
.text-12();
}
}
.card__right {
flex: none;
border-left: 1px solid rgba(216, 216, 216, 0.3);
display: flex;
align-items: center;
margin-left: 12px;
button {
background: #fff;
border: none;
font-size: 13px;
color: @black;
padding: 0 @padding-xs + 4 0 @padding-xs + 2;
}
}
}
.bar-bottom {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 30px;
background-color: #fff;
padding: 10px;
display: flex;
align-items: center;
.add-btn {
background: linear-gradient(269deg, #ff4b00 12%, #ff7705 86%);
color: #fff;
font-size: 14px;
font-weight: bold;
border-radius: 20px;
flex: 1;
height: 40px;
line-height: 40px;
}
}
}
</style>
<template>
<div>
<div class="address">
<cr-form>
<cr-field
v-model="address.receiverName"
class="address-input"
name="收货人"
label="收货人"
placeholder="请输入收货人姓名"
/>
<cr-field
v-model="address.receiverPhoneNo"
class="address-input"
name="手机号码"
label="手机号码"
placeholder="请输入收货人手机号码"
/>
<cr-field class="address-input" name="所在地区" label="所在地区">
<template #input>
<popup-area-tab-picker
ref="areaPicker"
v-model="address.addrFullName"
:placeholder="address.addrFullName || '省市区县、乡镇等'"
@finish="handleAreaSelect"
@popup="handlePickerShow"
/>
</template>
</cr-field>
<cr-field
class="address-input"
name="详细地址"
label="详细地址"
placeholder="如道路、门牌号、小区、楼栋号、单元室等"
>
<template #input>
<textarea
v-model="address.detail"
class="address-input-textarea"
placeholder="如道路、门牌号、小区、楼栋号、单元室等"
:autosize="{ minRows: 3, maxRows: 10 }"
/>
</template>
</cr-field>
<cr-cell title="设置默认地址" class="address-input">
<cr-switch
v-model="address.addrDefault"
size="20px"
:active-value="1"
:inactive-value="0"
/>
</cr-cell>
</cr-form>
</div>
<div v-if="type === 'edit'" class="row actions">
<span class="btn-delete" @click="onDelete">删除收货地址 </span>
</div>
<cr-button
shape="circle"
block
type="danger"
class="address-list__form-btn"
@click="confirmBebounce"
>保存</cr-button
>
</div>
</template>
<script>
import address from '@/api/address.api';
import { EventBus } from '@/service/utils.service';
import PopupAreaTabPicker from '@/components/popupAreaTabPicker';
const ADDRESS_ADD = 'addressAdd';
export default {
components: {
PopupAreaTabPicker
},
data() {
return {
type: '', // 是否是编辑
addressCode: '',
address: {
receiverName: '',
receiverPhoneNo: '',
addrFullName: '',
detail: '',
addrDefault: 1,
addrReceiverId: ''
},
pickerState: false,
addressSource: [],
backTimer: null,
order: ''
};
},
mounted() {
this.type = this.$route.query.type || '';
this.order = this.$route.query.type || false;
console.log(this.order);
if (this.type === 'edit') {
const data = { ...this.$route.params.editAddress };
data.addrFullName = data.addrFullName
.substring(0, data.addrFullName.length - data.detail.length)
.replace(/()/g, '');
this.address = data;
}
},
methods: {
handlePickerShow(e) {
this.pickerState = e;
},
handleAreaSelect(source) {
// 监听弹出框子组件传出的地址
this.addressSource = source.items;
},
confirmBebounce() {
if (this.backTimer) clearTimeout(this.backTimer);
this.backTimer = setTimeout(() => {
this.confirm();
}, 600);
},
async confirm() {
const data = this.address;
if (!data.receiverName) {
this.$toast('请填写收货人姓名');
return;
}
if (!/(^1[3|4|5|7|8][0-9]{9}$)/.test(data.receiverPhoneNo)) {
this.$toast('请输入正确的手机号码');
return;
}
if (!data.addrFullName) {
this.$toast('请选择所在地区');
return;
}
if (!data.detail) {
this.$toast('请填写详细地址');
return;
}
const param = Object.assign({}, this.address);
if (this.setDef) {
param.addrDefault = 1;
}
delete param.addrFullName;
if (this.addressSource.length) {
param.provinceId = this.addressSource[0].addrId;
param.cityId = this.addressSource[1].addrId;
param.countyId = this.addressSource[2].addrId;
param.townId = (this.addressSource[3] && this.addressSource[3].addrId) || '';
}
const [res] = await address.saveAddress(param);
if (res) {
// 不是编辑状态则从
this.type !== 'edit' && EventBus.$emit(ADDRESS_ADD, res); // 不是编辑状态,向详情页面派发事件到返回值
const addressList = { ...param, addrReceiverId: res.addrReceiverId };
this.$toast(`地址${this.type == 'edit' ? '修改' : '添加'}成功`);
console.log(this.order);
if (this.order) {
this.$router.push({
name: 'createOrder',
params: { addressList, orderAdder: this.order }
});
return;
}
setTimeout(() => {
// 返回之前页面
this.$router.go(-1);
}, 400);
}
},
onDelete() {
const _this = this;
this.$dialog({
title: '',
message: '确认删除该地址?',
onConfirm: function() {
_this.deleteAddr();
}
});
},
async deleteAddr() {
const [res] = await address.deleteAddress(this.address.addrReceiverId);
if (res) {
this.$toast('地址删除成功');
setTimeout(() => {
this.$router.go(-1);
}, 400);
}
}
}
};
</script>
<style lang="less" scoped>
.address {
&-input {
font-size: 15px;
color: #666;
&-textarea {
border: 0;
resize: none;
min-height: 42px;
width: 85%;
font-size: 15px;
}
}
&-list__form-btn {
margin: 60px auto;
width: 90%;
background: @gradient-red;
}
}
.actions {
padding-left: 20px;
.btn-delete {
padding: 12px 0;
font-size: 14px;
color: @font-color-search;
}
}
</style>
<template>
<div class="address-empty">
<img src="https://img.lkbang.net/xcx/empty-address.png" />
<span>暂未添加地址哦!</span>
</div>
</template>
<style lang="less" scoped>
.address-empty {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 105px;
img {
width: 125px;
height: 125px;
display: block;
margin-bottom: 15px;
font-size: 16px;
}
span {
color: @black;
.text-16();
}
}
</style>
<template>
<div class="fee-container">
<div class="goods">
<div class="fee-statistic">
<span>商品总额</span>
<span class="fee-statistic__bold"> ¥{{ calcFeeInfo.totalSkuFee || '0.00' }} </span>
</div>
<div class="fee-statistic">
<span>运费</span>
<span>¥{{ calcFeeInfo.totalFreightFee || '0.00' }}</span>
</div>
</div>
<div class="coupon">
<div
v-for="(item, idx) in couponActivityInfoList"
:key="idx"
:class="[
'fee-statistic',
'freight-discount',
idx != couponActivityInfoList.length - 1 && 'mg-bottom-20'
]"
@click="handleCouponShow(item)"
>
<span>{{ feeTitle(item.couponActivityType) }}</span>
<span
:class="{
'free-ship': !isAvailableFn(item.hasCouponActivity),
'has-coupon': isAvailableFn(item.hasCouponActivity)
}"
>
{{
calcFeeInfo.totalFreightFee == '0.00' && item.couponActivityType == FREIGHT
? '已包邮'
: isAvailableFn(item.hasCouponActivity)
? item.couponActivityAmt
? `-¥${(+item.couponActivityAmt).toFixed(2)}`
: `选择可用${feeTitle(item.couponActivityType)}`
: `无可使用的${feeTitle(item.couponActivityType)}`
}}
</span>
<i
v-if="
isAvailableFn(item.hasCouponActivity) &&
item.couponActivityAmt &&
item.couponActivityAmt != '0.00'
"
class="iconfont icon-arrow"
/>
</div>
</div>
</div>
</template>
<script>
import { DISCOUNT, FREIGHT, CASH, REDUCTION, HAS_DISCOUNT, TITLE_LIST } from '@/constants/order';
export default {
name: 'CouponInfo',
props: {
couponActivityInfoList: {
type: Array,
default: () => []
},
calcFeeInfo: {
type: Object,
default: () => {}
}
},
data() {
return {
hasDiscount: HAS_DISCOUNT,
DISCOUNT,
FREIGHT,
CASH,
REDUCTION
};
},
methods: {
handleCouponShow(data) {
this.$emit('coupon-show', data);
},
isAvailableFn(type) {
return type === this.hasDiscount;
},
feeTitle(type) {
return TITLE_LIST[type];
}
}
};
</script>
<style scoped lang="less">
.mg-bottom-20 {
margin-bottom: @padding-lg;
}
.goods,
.coupon {
background: @white;
padding: @padding-sm @padding-xs;
border-radius: @border-radius-md;
margin-bottom: @padding-sm;
}
.fee {
&-statistic {
display: flex;
justify-content: flex-end;
align-items: center;
span {
.text-13();
&:first-child {
color: @gray-4;
margin-right: auto;
}
&:last-child {
color: @black;
}
&.free-ship {
color: @gray-4;
}
&.has-coupon {
color: @black;
}
}
&__bold {
color: @red !important;
}
.icon-arrow {
.text-12();
color: @gray-4;
}
}
}
</style>
This diff is collapsed.
<template>
<cr-popup
v-model="show"
closeable
round
position="bottom"
get-container="body"
class="cancel-popup"
>
<div class="Cp__head">
<div class="Cp__head-title">请选择原因</div>
<!-- <div class="Cp__head-desc">订单一旦取消,无法恢复,金额/积分将原路返还</div> -->
</div>
<div class="Cp__tip">订单一旦取消,无法恢复,金额/积分将原路返还</div>
<cr-radio-group v-model="reasonType" class="Cp__list-wrap">
<cr-cell-group class="Cp__list">
<cr-cell
v-for="(item, index) in reasonList"
:key="index"
:title="item.cancelReason"
@click="onCellClick(item.cancelReasonType)"
>
<template #right-icon>
<cr-radio ref="checkboxes" :name="item.cancelReasonType" class="Cp__list-radio" />
</template>
</cr-cell>
</cr-cell-group>
</cr-radio-group>
<div class="Cp__button">
<cr-button block type="primary" shape="circle" @click="handleRadioSubmit">提交</cr-button>
</div>
</cr-popup>
</template>
<script>
const EVENT_INPUT = 'input';
const EVENT_COMPLETE = 'complete';
import orderApi from '@/api/order.api';
export default {
name: 'CancelPopup',
props: {
value: Boolean,
orderInfo: {
type: Object,
default: () => {}
}
},
data() {
return {
show: false,
reasonType: '',
reasonList: [
{
cancelReasonType: 1,
cancelReason: '收货地址填错了'
},
{
cancelReasonType: 2,
cancelReason: '忘记支付密码/余额不足'
},
{
cancelReasonType: 3,
cancelReason: '无法正常支付'
},
{
cancelReasonType: 4,
cancelReason: '不想买了'
},
{
cancelReasonType: 5,
cancelReason: '其他原因'
}
]
};
},
computed: {
reason() {
return this.reasonList.find(item => item.cancelReasonType === this.reasonType);
}
},
watch: {
value(val) {
this.show = val;
},
show(val) {
this.$emit(EVENT_INPUT, val);
}
},
mounted() {
this.getReasonList();
},
methods: {
onCellClick(name) {
this.reasonType = name;
},
async getReasonList() {
const [res] = await orderApi.orderCancelReason();
this.reasonList = res.cancelReasonList;
},
handleRadioSubmit() {
const status = this.orderInfo.orderStatus;
if (status == 21) {
this.orderUnfilledCancel();
} else {
this.orderCancel();
}
},
// 已付款,待发货
async orderUnfilledCancel() {
const [res] = await orderApi.orderUnfilledCancel({
orderNo: this.orderInfo.orderNo,
...this.reason
});
if (res) {
if (this.orderInfo.skuList[0].skuSource === 2) {
this.$router.push({
path: '/refundKepler',
query: { orderNo: this.orderInfo.orderNo, refundAmount: this.orderInfo.totalFee }
});
} else {
this.$toast('已取消');
}
this.$emit(EVENT_INPUT, false);
this.$emit(EVENT_COMPLETE);
}
},
// 未付款
async orderCancel() {
const [res] = await orderApi.orderCancel({
orderNo: this.orderInfo.orderNo,
...this.reason
});
if (res) {
this.$toast('已取消');
this.$emit(EVENT_INPUT, false);
this.$emit(EVENT_COMPLETE);
}
}
}
};
</script>
<style lang="less" scoped>
.order-list {
padding: 0 @padding-sm;
}
.cancel-popup {
@{deep} .cr-popup--close-top-right {
top: 18px;
right: 14px;
}
}
.Cp {
&__head {
text-align: center;
height: 47px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0 @padding-md;
border-bottom: 1px solid @grey-border;
&-title {
.text-16();
color: @text-color;
font-weight: bold;
}
&-desc {
.text-13();
color: @text-grey;
}
}
&__list {
flex: 1;
&-radio {
margin-right: 0;
}
}
&__button {
border-top: 1px solid @grey-border;
padding: @padding-sm - 2 @padding-xs;
}
&__tip {
width: 343px;
margin: @padding-xs auto;
background-color: @gray-1;
border-radius: @border-radius-md;
color: @gray-5;
.text-12();
text-align: center;
padding: @padding-sm @padding-xs;
}
}
</style>
.Ol {
&__item {
.text-12();
background-color: @white;
border-radius: @border-radius-md;
margin-bottom: @padding-sm;
}
&__head {
padding: @padding-sm @padding-xs;
border-bottom: 1px solid @grey-border;
display: flex;
justify-content: space-between;
&-left, &-right {
display: flex;
align-items: center;
}
&-image {
position: relative;
display: inline-block;
width: 20px;
height: 25px;
text-align: center;
margin-right: 4px;
}
&-red {
color: @red;
}
&-fail {
color: @text-grey;
}
&-success {
color: @green;
}
&-count {
padding-right: @padding-unit;
}
&-avator {
margin-left: @padding-unit - 2;
margin-top: @padding-unit - 2;
}
&-group {
position: absolute;
left: 2px;
top: -2px;
}
&-name {
color: @text-grey;
}
&-state {
color: @red;
}
}
&__foot {
border-top: 1px solid @grey-border;
padding: @padding-sm - 2 @padding-xs;
display: flex;
align-items: center;
justify-content: space-between;
&-text {
color: @text-grey;
}
&-btns {
text-align: right;
.cr-button {
margin-left: @padding-unit;
min-width: 86px;
}
}
}
&__noData {
.cr-button {
background: transparent;
}
}
}
.Ol {
&__body {
&-item {
padding: @padding-sm - 2 @padding-xs;
display: flex;
border-bottom: 1px solid @grey-border;
&:last-child {
border-bottom: 0;
}
}
&-image {
position: relative;
}
&-img {
width: 85px;
height: 85px;
border-radius: 8px;
overflow: hidden;
}
&-tag {
position: absolute;
top: 0;
left: 0;
font-size: @font-size-12;
color: @white;
width: 45px;
height: 18px;
line-height: 18px;
text-align: center;
background-image: linear-gradient(269deg, #ff5d00 12%, #ff1900 86%);
border-radius: 6px 0 6px 0;
}
&-content {
margin-left: @padding-sm - 2;
flex: 1;
}
&-row {
display: flex;
justify-content: space-between;
}
&-title {
.text-13();
color: @text-color;
flex: 1;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
&-salePrice {
.text-13();
color: @text-color;
width: 78px;
text-align: right;
.Ol__body-skus {
display: block;
}
}
&-count {
.text-12();
display: block;
margin-left: auto;
color: @text-grey;
}
&-sku {
&s {
min-height: 12px;
margin-bottom: @padding-unit;
.cr-button {
margin-left: auto;
.text-12();
min-width: 68px;
}
}
.text-12();
display: flex;
align-items: center;
color: @text-grey;
a {
.text-12();
margin-left: auto;
color: @red;
}
}
&-val {
color: @gray-5 !important;
}
}
}
\ No newline at end of file
<template>
<div class="order-list">
<cr-list
v-if="list.length"
v-model="listLoading"
:finished="listFinished"
finished-text="没有更多了"
@load="onLoad"
>
<div v-for="(item, index) in list" :key="index" class="Ol__item" @click="toDetail(item)">
<div class="Ol__head">
<div class="Ol__head-left">
<span class="Ol__head-image">
<cr-image
class="Ol__head-avator"
width="85%"
src="https://activitystatic.lkbang.net/mall-free-ui/img/avator_02.png"
/>
<cr-image class="Ol__head-group" src="@/assets/images/group.png" />
</span>
<span class="Ol__head-name">胡**</span>
</div>
<div class="Ol__head-right">
<template v-if="index % 2 === 0">
<span class="Ol__head-count">拼团中,差<b class="Ol__head-red">1人</b>成团</span>
<count-down :end-time="gpCountDown" />
</template>
<template v-else-if="index % 3 === 0">
<span class="Ol__head-success">已成团,等待发货!</span>
</template>
<template v-else>
<span class="Ol__head-fail">拼团失败</span>
</template>
<!-- <span class="Ol__head-state">{{ item.orderStatusText }}</span> -->
</div>
</div>
<div class="Ol__body">
<list-item
:list="item"
:show-after="item.orderStatus == 31 || item.orderStatus == 41"
:after-show="false"
/>
</div>
<div class="Ol__foot">
<div class="Ol__foot-text">拼团成功即可退款0元拿</div>
<div class="Ol__foot-btns">
<!-- <cr-button
size="small"
plain
type="default"
shape="circle"
@click.stop="onOptionClick({ ...item, index }, 'cancel')"
>
取消订单
</cr-button> -->
<cr-button
size="small"
plain
type="primary"
shape="circle"
@click.stop="onOptionClick({ ...item, index }, 'pay')"
>
付款
</cr-button>
</div>
</div>
</div>
</cr-list>
<div v-else class="Ol__noData">
<cr-empty
image="https://img.lkbang.net/xcx/empty@2x.png"
image-size="4rem"
description="暂无订单~"
/>
</div>
</div>
</template>
<script>
import ListItem from './ListItem.vue';
const EVENT_LOADING = 'load';
const EVENT_CLICK = 'option-click';
import countDown from '@/components/countDown';
export default {
name: 'OrderSkuList',
components: {
countDown,
ListItem
},
props: {
list: {
type: Array,
default: () => []
},
loading: Boolean,
finished: Boolean
},
data() {
return {
gpCountDown: new Date('2021-09-10 00:00:00').getTime(),
listLoading: false,
listFinished: false
};
},
watch: {
loading(val) {
this.listLoading = val;
},
finished(val) {
this.listFinished = val;
}
},
methods: {
toDetail(order) {
this.$router.push({ path: '/orderDetail', query: { orderNo: order.orderNo } });
},
onLoad() {
this.$emit(EVENT_LOADING);
},
onOptionClick(orderInfo, eventType) {
this.$emit(EVENT_CLICK, { orderInfo, eventType });
},
toHome() {
this.$router.replace({ path: '/home' });
}
}
};
</script>
<style lang="less" scoped>
@import './List.less';
.order-list {
padding: 0 @padding-sm;
.cr-empty {
text-align: center;
}
@{deep} .cr-empty__image {
height: auto !important;
}
}
</style>
<template functional>
<div class="Ol__body">
<div
v-for="(it, idx) in props.list.skuList"
:key="idx"
class="Ol__body-item"
@click="
() => {
props.canClick && parent.handleItemClick(it);
}
"
>
<div class="Ol__body-image">
<cr-image
:src="it.imageUrl | replaceUrl"
width="2.266667rem"
height="2.266667rem"
class="Ol__body-img"
/>
<span class="Ol__body-tag">3人团</span>
</div>
<div class="Ol__body-content">
<div class="Ol__body-row">
<span class="Ol__body-title">
{{ it.skuName }}
</span>
<span v-if="it.salePrice" class="Ol__body-salePrice">
<span class="Ol__body-skus">¥{{ it.salePrice }}</span>
<span class="Ol__body-count">{{ it.count }}</span>
</span>
</div>
<div class="Ol__body-skus">
<span class="Ol__body-sku">{{ it.skuAttr }}</span>
</div>
<div v-if="props.showAfter && (it.havingService || props.afterShow)" class="Ol__body-skus">
<span class="Ol__body-sku">
<cr-button
size="mini"
plain
:type="it.havingService ? 'warning' : props.afterShow ? 'default' : ''"
shape="circle"
@click.stop="parent.onOptionClick({ ...props.list, currentSku: it }, 'refund')"
>
{{ it.havingService ? '售后中' : props.afterShow ? '售后' : '' }}
</cr-button>
</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ListItem',
filters: {
replaceUrl(url) {
return url.replace('http://', 'https://');
}
},
props: {
list: {
type: Object,
default: () => {}
},
showAfter: Boolean,
afterShow: Boolean,
canClick: Boolean
}
};
</script>
<style lang="less" scoped>
@import './List.less';
</style>
.cr-tabs {
margin-top: 0 !important;
& @{deep} > .cr-sticky {
z-index: 3;
& > div {
z-index: 3;
}
}
}
\ No newline at end of file
<template>
<div class="page page__nopad">
<cr-tabs
v-model="currentTab"
title-inactive-color="#333"
sticky-inactive-color="#333"
sticky
z-index="4"
@change="handleTabChange"
>
<cr-tab v-for="(item, index) in navList" :key="index" :title="item.title" :name="index">
<list
:list="item.list"
:loading="item.loading"
:finished="item.finished"
@load="handleLoad"
@option-click="handleOptionClick"
/>
</cr-tab>
</cr-tabs>
<cancel-popup
v-model="showCancelPopup"
:order-info="currentOrder"
@complete="handleCancelState"
/>
<cr-action-sheet
v-model="showExpressPopup"
title="请选择物流单号"
cancel-txt="关闭"
round
:actions="expressActions"
@click="toExpress"
/>
</div>
</template>
<script>
import orderApi from '@/api/order.api';
import List from './components/List';
import { isApp } from '@/service/validation.service';
import CancelPopup from './components/CancelPopup';
import localStorage from '@/service/localStorage.service';
const commonParams = {
loading: false,
finished: false,
page: 1,
pageSize: 10,
list: []
};
export default {
name: 'OrderList',
components: {
List,
CancelPopup
},
data() {
return {
isApp,
showCancelPopup: false,
showExpressPopup: false,
currentTab: 0,
navList: [
{
state: 0,
title: '全部',
...JSON.parse(JSON.stringify(commonParams))
},
{
state: 11,
title: '待付款',
...JSON.parse(JSON.stringify(commonParams))
},
{
state: 21,
title: '待发货',
...JSON.parse(JSON.stringify(commonParams))
}
],
currentOrder: {},
expressActions: [],
orderStatusInfo: {}
};
},
mounted() {
this.getStateQuery();
this.getList();
// this.$track.registeredEvents('H5_2B_MyOrderPageExposure');
},
methods: {
getStateQuery() {
const { state } = this.$route.params || {};
state && (this.currentTab = Number(state));
},
handleTabChange(name) {
this.currentTab = name;
this.getList();
},
handleLoad() {
this.getList();
},
handleOptionClick(info) {
const { orderInfo } = info || {};
const { cashCouponCancelNote, showCashCouponCancelNote } = orderInfo || {};
this.currentOrder = orderInfo;
switch (info.eventType) {
case 'cancel':
this.orderCancelPopup();
break;
case 'pay':
this.toPay();
break;
case 'notify':
this.orderNotify();
break;
case 'again':
this.toGoods();
break;
case 'logistics':
this.expressActionsCheck();
break;
case 'confirm':
this.orderConfirmPopup();
break;
case 'toRefund':
this.orderRefundPopup(cashCouponCancelNote, showCashCouponCancelNote);
break;
case 'refund':
this.toRefund();
break;
default:
this.currentOrder = {};
break;
}
},
orderRefundPopup(cashCouponCancelNote, showCashCouponCancelNote) {
let tips = {
message: this.currentOrder.cancelNote || '宝贝马上就要发货了,您真的要取消吗?',
confirmButtonText: '仍要取消',
cancelButtonText: '留下商品',
onConfirm: () => {
this.showCancelPopup = true;
}
};
if (
this.currentOrder.skuList[0].skuSource === 5 ||
this.currentOrder.skuList[0].skuSource === 6
) {
tips = {
message: '该商品暂不支持取消,您可以在收货时直接拒收,钱款原路退回付款账户中',
confirmButtonText: '知道了',
showCancelButton: false
};
}
if (showCashCouponCancelNote) {
tips = {
message: cashCouponCancelNote,
confirmButtonText: '确认退货',
cancelButtonText: '不退了',
onConfirm: () => {
this.showCancelPopup = true;
}
};
}
this.$dialog(tips);
},
orderCancelPopup() {
this.showCancelPopup = true;
},
// 售后中
toRefund() {
if (this.currentOrder.currentSku.havingService) {
localStorage.set('orderInfo', this.currentOrder);
this.$router.push({
name: 'refundDetail'
});
return;
}
if ([2, 5, 6].indexOf(this.currentOrder.currentSku.skuSource) > -1) {
this.$dialog({
message: '您购买的商品确认收货后才可以申请售后哦!',
confirmButtonText: '知道了',
showCancelButton: false
});
return;
}
},
toPay() {
this.$router.push({ name: 'pay', query: { orderNo: this.currentOrder.orderNo } });
},
toGoods() {
this.$router.push({
path: '/goodDetail',
query: { skuNo: this.currentOrder.skuList[0].skuNo }
});
},
orderConfirmPopup() {
if (!this.currentOrder.orderNo) return;
this.$dialog({
title: '确定该订单已收货吗?',
onConfirm: () => {
orderApi
.orderConfirm({
orderNo: this.currentOrder.orderNo,
status: 41
})
.then(res => {
if (res[0]) {
this.updateCurrentOrder(41, '交易完成');
this.$toast('操作成功');
}
});
}
});
},
handleCancelState() {
this.updateCurrentOrder(51, '交易关闭');
},
updateCurrentOrder(state, txt) {
this.$set(this.navList[this.currentTab].list[this.currentOrder.index], `orderStatus`, state);
this.$set(
this.navList[this.currentTab].list[this.currentOrder.index],
`orderStatusText`,
txt
);
},
expressActionsCheck() {
const { expressInfoList } = this.currentOrder;
if (expressInfoList.length > 1) {
this.expressActions = expressInfoList.map(item => {
return {
name: item.deliveryNo,
deliveryNo: item.deliveryNo,
expressCompanyCode: item.expressCompanyCode
};
});
this.showExpressPopup = true;
} else {
this.toExpress(expressInfoList[0]);
}
},
toExpress(item) {
this.$router.push({
path: '/expressDetail',
query: { ...item, deliveryOrderId: this.currentOrder.orderNo }
});
},
orderNotify() {
this.$toast.success('已通知卖家');
},
setNavListData(key, val) {
this.$set(this.navList[this.currentTab], key, val);
},
async getList() {
let { finished, page, pageSize, list, state: orderStatus } = this.navList[this.currentTab];
if (finished) return;
this.setNavListData('loading', true);
const [res] = await orderApi.orderList({
pageNo: page,
pageSize,
orderStatus
});
if (res) {
this.setNavListData('loading', false);
list = [...list, ...res.orderList];
this.setNavListData('list', list);
if (!res.hasNext) {
this.setNavListData('finished', true);
} else {
page++;
this.setNavListData('page', page);
}
}
}
}
};
</script>
<style lang="less" src="./index.less" scoped></style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment