Commit e276ec19 authored by 贾慧斌's avatar 贾慧斌

fix: views 调整为pages

parent d69e5f3a
<template>
<div class="goods">
<cr-card
:thumb="item.goodsImage"
:class="['goods_item', { 'is-over': topicState === 2 || item.goodsCount == 0 }]"
img-height="116"
@click="handleBtnClick"
>
<template #title>
<img v-if="item.goodsTypeImage" :src="item.goodsTypeImage" class="goods_title-icon" />
<div class="goods_item--title">{{ item.goodsName }}</div>
<div v-if="item.sellPoint" class="goods_item--attr">{{ item.sellPoint }}</div>
<div
v-if="item.stockAndProgressInfo && item.stockAndProgressInfo.goodsCount < 1"
class="goods_item--sold-out"
>
<div>
<div class="goods_item--sold-out__cn">已售罄</div>
<div class="goods_item--sold-out__en">SOLD OUT</div>
</div>
</div>
</template>
<template #desc>
<div class="goods_item--tag">
<TagItem v-for="(tag, i) in item.tagList" :key="i" :item="tag" />
</div>
<div class="goods_item--price">
<div v-if="item.discountShow.discountPrice" class="goods_item--price__normal-price">
<span>{{ item.discountShow.discountPrice }}</span>
</div>
<div v-if="item.discountShow.skuInfoSalePrice" class="goods_item--price__origin-price">
原价:¥{{ item.discountShow.skuInfoSalePrice }}
</div>
</div>
<div class="goods_progress-box">
<cr-progress
v-if="!topicCfg.hasStop"
class="goods_progress"
stroke-width="5"
color="#E82424"
track-color="#D8D8D8"
:show-pivot="false"
:percentage="handleProgressByStatus(item)"
/>
<div v-if="item.stockAndProgressInfo" class="goods_progress-box__desc">
<span v-if="item.stockAndProgressInfo.goodsCount > 0">
已售{{ item.stockAndProgressInfo.saleCount }}
</span>
<span v-else>已售罄</span>
</div>
</div>
</template>
<template #footer>
<div class="shopcart" @click.stop="mxAddShopCart(item)" />
</template>
</cr-card>
</div>
</template>
<script>
import localStorage from '@/service/localStorage.service';
import mixinCart from '@/mixins/cart.mixin';
import TagItem from '@/components/TagItem.vue';
export default {
components: {
TagItem
},
mixins: [mixinCart],
props: {
item: {
type: Object,
default: () => ({})
},
topicCfg: {
type: Object,
default: () => ({})
},
topicState: {
type: Number,
default: 0
}
},
data() {
return {
hasLogin: false
};
},
created() {
this.hasLogin = !!localStorage.get('vccToken');
},
methods: {
handleBtnClick() {
if (!this.item) return;
if (!this.hasLogin) {
this.$router.push({ name: 'Login' });
return;
}
// const { activityId } = this;
// const { goodsSpecialId, activityTemplateId, templateDetailId } = this.topicCfg;
// const { activityTemplateId, activityInfoId } = this.topicCfg;
const { goodsId } = this.item;
if (this.item.goodsCount !== 0) {
this.$router.push({
path: '/goodDetail',
query: {
skuNo: goodsId
// templateDetailId: activityTemplateId,
// activityId: activityInfoId
}
});
} else {
this.$notify({ type: 'warning', message: '该商品已抢完' });
}
},
handleProgressByStatus(item) {
if (!item) return 0;
let percentage = 0;
const { saleCount, goodsCount } = item.stockAndProgressInfo || {};
percentage = (saleCount / (goodsCount + saleCount)) * 100;
percentage = percentage < 0 ? 0 : percentage;
const v = Math.round(percentage * 100) / 100;
return v;
}
}
};
</script>
<style lang="less" scoped>
@import '~@/style/mixins.less';
.goods {
&_title-icon {
width: auto;
height: 15px;
vertical-align: -2px;
margin-right: 3px;
}
&_item {
border-radius: 6px;
background-color: #fff;
margin: 10px 0;
position: relative;
padding: 0 12px;
&--title {
.mx_multline_overwrite(1);
color: #333;
font-weight: 400;
font-size: 13px;
height: 20px;
padding-top: 2px;
box-sizing: border-box;
line-height: 18px;
}
&--attr {
color: #999;
font-size: 12px;
margin-top: 5px;
}
&--sold-out {
position: absolute;
top: 18px;
left: 18px;
width: 76px;
height: 76px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.6);
color: #fff;
transform: rotate(-20deg);
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
&__cn {
font-size: 14px;
font-weight: 500;
line-height: 1;
text-align: center;
}
&__en {
font-size: 9px;
line-height: 1;
text-align: center;
}
}
&--tag {
display: flex;
flex-wrap: wrap;
line-height: 1;
height: 32px;
margin-top: 7px;
// .title-mixin();
// .mx_multline_overwrite(2);
// height: 28px;
}
&--price {
display: flex;
align-items: center;
height: 18px;
line-height: 14;
&__normal-price {
font-size: 18px;
font-weight: bold;
color: #ec3333;
line-height: 18;
&::before {
content: '¥';
font-size: 14px;
}
}
&__origin-price {
font-size: 11px;
color: #999;
margin-left: 6px;
line-height: 11;
}
}
@{deep} .cr-card {
&__header__thumb {
background-color: #f7f8fa;
border-radius: 7px;
overflow: hidden;
position: relative;
}
&__footer {
margin-top: 0;
padding-top: 0;
position: relative;
}
}
&.is-over {
@{deep} .cr-card__header__content {
filter: grayscale(100%);
}
.good-list_btn {
background: #ccc;
&::after {
display: none;
}
}
}
&.is-hide {
display: none;
}
}
&_btn {
position: absolute;
right: 10px;
bottom: 11px;
background-image: linear-gradient(269deg, #ff5d00 12%, #ff1900 86%);
border-width: 0;
padding: 0 17px;
height: 24px;
line-height: 26px;
z-index: 2;
overflow: hidden;
&::after {
content: '';
position: absolute;
width: 19px;
height: 24px;
top: 0;
background: url(../../../../assets/images/activity/btn-light.png) center no-repeat;
background-size: contain;
left: -100%;
transform: scale(1.3);
animation: moveTo 1.5s infinite linear;
z-index: 3;
}
}
&_progress-box {
display: flex;
align-items: center;
line-height: 1;
margin-top: 3px;
&__desc {
margin-left: 4px;
color: #999;
font-size: 12px;
}
}
&_progress {
width: 62px;
overflow: hidden;
border-radius: 5px;
}
.shopcart {
position: absolute;
right: 6px;
bottom: 3px;
flex-shrink: 0;
width: 30px;
height: 30px;
background-image: url('https://img.lkbang.net/shopcart-seckill.8a1cc2e.png');
background-size: 100%;
background-repeat: no-repeat;
}
}
@keyframes moveTo {
from {
left: -100%;
}
to {
left: 100%;
}
}
</style>
<template>
<div class="panic-buying-box" :style="{ backgroundImage: `url(${backgroundImage})` }">
<template v-if="activityInfo && activityInfo.activityInfoId">
<div v-if="showTime" class="panic-buying-box--timer">
<div>{{ timeBefore }}</div>
<div class="panic-buying-box--timer__time">
<cr-count-down :time="time" :format="format" @finish="getActivityInfo" />
</div>
</div>
<div class="panic-buying-wrapper">
<cr-tabs
v-model="value"
title-bg-color="rgba(0,0,0,0)"
title-active-color="#fff"
color="#fff"
@change="onTabChange"
>
<cr-tab
v-for="category in categoryList"
:key="category.categoryId"
:title="category.categoryName"
/>
</cr-tabs>
</div>
<div class="panic-buying-body">
<cr-list
v-if="!reload"
v-model="loading"
:finished="finished"
finished-text="没有更多了"
:immediate-check="false"
offset="10"
@load="getGoodsList"
>
<GoodsItem
v-for="(item, index) in goodsList"
:key="index"
:item="item"
:topic-cfg="activityInfo"
/>
</cr-list>
</div>
</template>
<div v-else class="panic-buying-box--empty">
<cr-loading v-if="!activityLoadFinished" size="24px">加载中...</cr-loading>
<cr-empty v-else :image="empyImg" image-size="4rem" description="没有找到相关活动~" />
</div>
</div>
</template>
<script>
import { panicBuyingApi } from '@/api/activity';
import empyImg from '@/assets/images/electronicInvoice/empy.png';
import GoodsItem from './components/GoodsItem.vue';
export default {
components: {
GoodsItem
},
data() {
return {
activityInfoId: 0,
backgroundImage: '',
activityInfo: {},
categoryList: [],
goodsList: [],
loading: true,
activityLoading: false,
activityLoadFinished: false,
isShowDay: true,
time: 0,
showTime: false,
empyImg,
day: '',
timeBefore: '距结束',
format: '{h}时{m}分{s}秒',
pageNo: 1,
pageSize: 10,
categoryId: 0,
reload: false,
finished: false,
value: ''
};
},
created() {
this.getActivityInfo();
},
methods: {
// 处理时间
dealTime() {
const atime = this.activityInfo.activityTimeInfo;
if (!atime) {
return;
}
const eTime = atime.endTime - atime.currentTime;
const sTime = atime.currentTime - atime.startTime;
let time = 0;
if (eTime > 0) {
this.showTime = true;
this.timeBefore = '距结束';
time = eTime;
} else if (sTime > 0) {
this.showTime = true;
this.timeBefore = '距开始';
time = sTime;
}
this.showTime = eTime > 0 || sTime > 0;
if (this.showTime) {
this.isShowDay = time > 24 * 60 * 60 * 1000;
this.time = time;
if (this.isShowDay) {
this.format = '{d}天{h}时{m}分';
} else {
this.format = '{h}时{m}分{s}秒';
}
}
},
// 获取抢购活动信息
async getActivityInfo() {
const timer = setTimeout(() => {
clearTimeout(timer);
if (!this.activityLoadFinished) {
this.activityLoading = true;
}
}, 500);
this.activityInfoId = this.$route.query.id;
const [res] = await panicBuyingApi.activityInfo(this.activityInfoId);
this.activityLoadFinished = true;
this.activityLoading = false;
if (res && res.activityInfo) {
this.activityInfo = res.activityInfo;
this.backgroundImage = res.activityInfo.img || '';
this.dealTime();
}
this.categoryList = res?.categoryList || [];
if (this.categoryList && this.categoryList.length) {
this.onTabChange(0);
}
},
// 获取feed流商品
async getGoodsList() {
this.loading = true;
this.pageNo++;
const params = {
activityInfoId: this.activityInfoId,
categoryId: this.categoryId,
pageNo: this.pageNo,
pageSize: this.pageSize
};
const [res] = await panicBuyingApi.goodsFeed(params);
this.loading = false;
if (res) {
this.finished = !res.hasNext;
this.goodsList = [...this.goodsList, ...res.records];
} else {
this.finished = true;
}
},
onTabChange(index) {
this.finished = false;
const category = this.categoryList[index];
this.categoryId = category.categoryId;
this.pageNo = 0;
this.goodsList = [];
this.getGoodsList();
}
}
};
</script>
<style lang="less" scoped>
.panic-buying-box {
padding: 0;
min-height: 100vh;
background-size: 100%;
background-repeat: no-repeat;
position: relative;
&--timer {
position: absolute;
right: 12px;
top: 12px;
height: 20px;
line-height: 20px;
min-width: 80px;
border-radius: 10px;
color: #fff;
background: url('https://img.lkbang.net/icon-timer.bb39d4bce08a28d.bb39d4bc.png') no-repeat;
background-position: 3px center;
background-size: 16px 14px;
background-color: rgba(0, 0, 0, 0.3);
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px 0 23px;
&__day {
margin-left: 4px;
}
&__time {
width: 82px;
padding-left: 4px;
word-break: keep-all;
}
/deep/ .cr-count-down {
color: #fff;
font-size: 12px;
}
}
/deep/ .cr-loading--text {
color: #999;
}
&--empty {
text-align: center;
height: 50px;
line-height: 50px;
font-size: 14px;
color: #999;
}
}
.panic-buying-wrapper {
padding-top: 106px;
/deep/ .cr-tabs__item--active {
font-size: 16px;
}
}
.panic-buying-body {
margin-top: 7px;
background-color: #fff;
border-radius: 28px 28px 0px 0px;
min-height: calc(100vh - 167px);
padding-top: 10px;
color: #333;
}
</style>
<template>
<div>
<cr-popup
:value="show"
:style="{ backgroundColor: 'transparent' }"
:closeable="closeable"
close-icon="close"
class="gift"
@close="onClose"
>
<div v-show="show" class="gift-container">
<cr-image v-if="imgUrl" class="gift-img" :src="imgUrl" width="221px" height="171px" />
<div class="gift-tip" v-html="txt" />
<div class="gift-btns">
<cr-button shape="circle" @click="go">{{ btnTxt }}</cr-button>
<a href="javascript:;" @click="onClose">知道了</a>
</div>
</div>
</cr-popup>
</div>
</template>
<script>
const DIALOG_CLOSE_EVENT = 'close';
export default {
name: 'GiftPopup',
props: {
value: null,
txt: String,
imgUrl: String,
closeable: Boolean,
btnTxt: {
type: String,
default: '确定'
}
},
data() {
return {
show: false
};
},
watch: {
value: {
immediate: true,
handler(val) {
this.show = val;
}
}
},
methods: {
onClose() {
this.$emit(DIALOG_CLOSE_EVENT, 1);
},
go() {
this.$emit(DIALOG_CLOSE_EVENT, 2);
}
}
};
</script>
<style lang="less" scoped>
.gift {
overflow: initial;
@{deep} .cr-popup--close {
top: -50px;
right: 0px;
color: @white;
font-size: 29px;
z-index: 103;
}
&-container {
width: 289px;
height: 320px;
position: relative;
background-color: #fff;
border-radius: 6px;
padding: 20px 0;
}
&-tip {
font-size: 16px;
color: #666666;
text-align: center;
line-height: 22px;
min-height: 44px;
}
&-img {
margin: 0 auto 20px;
display: block;
}
&-btns {
margin-top: 16px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.cr-button {
color: #fff;
height: 40px;
width: 210px;
background-image: linear-gradient(269deg, #ff5d00 12%, #ff1900 86%);
border-width: 0;
}
a {
color: #ec3333;
font-size: 14px;
margin-top: 15px;
}
}
}
</style>
/*
* @Description:
* @Date: 2020-12-01 18:06:18
* @LastEditors: guang.wu
* @LastEditTime: 2023-04-28 17:20:37
*/
import { parseTime } from '@/service/utils.service';
export function handleRemainTime(start, end) {
const startTime = (start ? new Date(start.replace(/-|\./g, '/')) : new Date()).getTime();
const endTime = (end ? new Date(end.replace(/-|\./g, '/')) : new Date()).getTime();
return [endTime - startTime, startTime, endTime];
}
export function handleDateFormat(start, end, month) {
const dateArr = handleRemainTime(start, end);
const isDay = dateArr[0] > 60 * 60 * 1000 * 23;
const formatExp = month ? '{m}月{d}日' : isDay ? '{m}月{d}日' : '{h}:{i}';
return {
isDay,
dateFormat: parseTime(dateArr[1], formatExp, true),
dateTime: parseTime(dateArr[1], '{h}:{i}')
};
}
export function handleRemainTimeForSeckillV2(start, end) {
const startTime = (start ? new Date(start.replace(/-|\./g, '/')) : new Date()).getTime();
const endTime = (end ? new Date(end.replace(/-|\./g, '/')) : new Date()).getTime();
return [endTime - startTime, startTime, endTime];
}
export function handleDateFormatForSeckillV2(start, end, month) {
const dateArr = handleRemainTime(start, end);
const isDay = dateArr[0] > 60 * 60 * 1000 * 23;
const formatExp = isDay ? '{m}月{d}日' : '{h}:{i}';
let dateFormat = parseTime(dateArr[1], formatExp, month);
if (formatExp === '{h}:{i}' && dateFormat.split(':')[1] === '00') {
dateFormat = parseTime(dateArr[1], '{h}:00', false);
}
return {
isDay,
dateFormat,
dateTime: parseTime(dateArr[1], '{h}:{i}')
};
}
@import "../../../style/mixins.less";
@import "../../../style/var.less";
.productDetail {
background-color: #f7f7f7;
width: 100%;
margin-bottom: 50px;
.iphonex-fix-padding();
.my-swipe {
width: 100%;
height: 350px;
.cr-swipe-item {
text-align: center;
width: 100%;
height: 350px;
}
.custom-indicator {
width: 30px;
height: 17px;
border-radius: 10px;
opacity: 0.4;
background: #000000;
position: absolute;
right: 15px;
bottom: 9px;
font-size: 10px;
text-align: center;
line-height: 17px;
color: #ffffff;
}
}
.price {
background-color: #fff;
display: flex;
align-items: baseline;
&__num {
width: 230px;
display: flex;
align-items: baseline;
padding: 10px 0 10px 12px;
position: relative;
&-tag {
display: none;
position: relative;
z-index: 2;
align-self: center;
font-size: 12px;
margin-right: 4px;
}
&-bg {
display: none;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
&-present {
z-index: 2;
color: #ec3333;
font-size: 30px;
span {
font-size: 13px;
}
}
&-original {
z-index: 2;
margin-left: 4px;
color: #999999;
font-size: 13px;
.through-line-mixin(#999);
}
}
&__desc {
color: #999999;
text-align: right;
font-size: 14px;
flex: 1;
align-self: center;
padding: 0 12px 0 0;
&-count {
padding: 10px 0;
}
&-countdown {
display: none;
padding: 4px 0 5px;
h5 {
font-size: 13px;
color: #ec1500;
margin: 5px 0 4px;
}
.countdown-mixin(#fff, #e82424, #e82424, 16px, 400);
}
}
&.spike {
background-color: #fffbe8;
.price {
&__num {
&-bg {
display: block;
height: 70px !important;
width: 257px !important;
top: -10px;
}
&-present {
color: #fff;
}
&-original {
color: rgba(255, 255, 255, 0.65);
.through-line-mixin(rgba(255, 255, 255, 0.65));
}
}
&__desc {
&-countdown {
display: block;
}
}
}
}
}
.title-wrap {
background-color: #fff;
padding: 11px 10px 10px;
}
.title {
display: block;
font-size: 16px;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
&_tag{
display: inline-block;
width: auto;
height: 15px;
vertical-align: -1px;
}
}
.info {
margin-top: 10px;
background-color: #fff;
.address {
margin-right: 5px;
vertical-align: middle;
}
.cr-cell {
padding-top: 10px;
line-height: 18px;
}
@{deep} .cr-cell__value {
margin-left: 20px;
text-align: left;
word-break: break-all;
flex: 1;
}
// .service {
// padding-right: 10px;
// border-right: 1px solid #f7f7f7;
// vertical-align: middle;
// }
// .service:not(:first-child) {
// padding-left: 10px;
// }
.service:not(:last-child)::after {
content: "";
display: inline-block;
width: 1px;
background: #f7f7f7;
height: 15px;
margin: 0px 10px;
// margin-top: -2px;
vertical-align: bottom;
}
}
.divider {
font-size: 12px;
padding: 0 40px;
border-color: #f7f7f7;
}
@{deep} .cr-cell__title {
font-size: 13px;
color: #999999;
flex: none;
}
@{deep} .cr-cell__value {
font-size: 13px;
color: #333333;
flex: none;
}
@{deep} .cr-cell:not(:last-child)::after {
border: none;
}
.detail {
@{deep} .cr-cell__title {
width: 60px;
}
@{deep} .cr-cell__value {
margin-left: 30px;
color: #666666;
width: 250px;
text-align: left;
}
margin-bottom: 10px;
}
.type {
padding-right: 10px;
}
.wrap-reco {
margin-top: 24px;
min-height: 55px;
}
}
.addr-show {
display: flex;
align-items: center;
span {
flex: 1;
line-height: 18px;
height: 18px;
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.arrow {
margin-left: auto;
}
}
.info-detail {
background-color: #fff;
margin-bottom: 12px;
.info {
margin-top: 0;
}
h4 {
font-size: 16px;
padding: 14px 14px 4px;
}
@{deep} .cr-hairline--top-bottom::after {
border-width: 0;
}
@{deep} .cr-cell__title {
width: 77px;
}
@{deep} .cr-cell__value {
color: #666;
}
@{deep} .cr-cell-group__title {
color: #333;
}
}
.activity-good-action {
height: 58px;
.iphonex-fix-padding();
@{deep} .cr-goods-action {
&__left {
padding: 0 2px;
&-icon {
font-size: 24px;
color: #333;
}
&-txt {
color: #999;
font-size: 10px;
}
}
&__dot {
border: 1.2px solid #fff;
position: absolute;
right: 5px;
font-size: 10px;
line-height: 11px;
padding: 2px 4px 1px 4px;
top: 9px;
text-align: center;
}
&__content {
padding: 0;
}
&__right {
&-btn {
height: 40px;
line-height: 40px;
font-weight: 600;
margin-right: 10px;
&:first-child {
background-color: #fff;
border-color: #ec1500;
color: #ec1500;
}
&:last-child {
background: linear-gradient(270deg,
#ff5d00 0%,
#ff1900 100%);
border-width: 0;
}
}
}
}
}
This diff is collapsed.
@import "../../../style/index.less";
@font-face {
font-family: "din";
src: url("../../../style/DIN.ttf") format("truetype");
}
div {
box-sizing: border-box;
}
.container{
width: 100%;
// overflow: hidden;
// background: url('https://img.lkbang.net/seckill-bg.1aa5d2ca.png');
background-size: 100%;
background-repeat: no-repeat;
position: relative;
box-sizing: border-box;
padding-top: 80px;
&--loading {
text-align: center;
/deep/ .cr-loading--text {
color: #999;
}
}
.rules{
position: absolute;
right: 0;
top: 17px;
width: 44px;
height: 25px;
border-top-left-radius: 13px;
border-bottom-left-radius: 13px;
background: rgba(0,0,0, 0.148);
background-blend-mode: normal;
font-size: 12px;
color:#fff;
text-align: center;
padding-left: 4px;
box-sizing: border-box;
line-height: 25px;
}
.sticky{
position: sticky;
top: 0;
// background: url('https://img.lkbang.net/seckill-bg.1aa5d2ca.png');
background-size: 100%;
background-repeat: no-repeat;
background-position: 0 -80px;
z-index: 2;
}
.timers{
width: 100%;
height: 48px;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
background: #f7f8fa;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #999;
.desc{
font-size: 14px;
margin-right: 8px;
}
.day{
font-size: 10px;
color:#000201;
font-weight: bold;
}
.hours, .mins, .secs{
width: 16px;
height: 16px;
border-radius: 3.2px;
background: #000201;
line-height: 16px;
text-align: center;
color:#fff;
margin-left: 2px;
margin-right: 2px;
}
}
.good-list{
background: #f7f8fa;
padding: 0 10px 10px;
.good-item{
margin-top: 12px;
width: 100%;
height: 118px;
background:#fff;
padding: 8px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: space-between;
&:first-child{
margin-top: 0;
}
.good-img{
flex-shrink: 0;
width: 102px;
height: 102px;
margin-right: 8px;
position: relative;
img{
width: 100%;
height: 100%;
display: block;
border-radius: 6px;
}
.soldout{
width: 76px;
height: 76px;
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
}
.good-info{
width: calc(100% - 110px);
flex-shrink: 0;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
.top-part{
width: 100%;
.title{
width: 100%;
line-height: 19px;
overflow:hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #333;
font-size: 13px;
}
.point{
margin-top: 4px;
width: 100%;
line-height: 12px;
overflow:hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #999;
font-size: 12px;
}
}
.bottom-part{
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.info{
width: calc(100% - 38px);
display: flex;
flex-direction: column;
.price{
display: flex;
align-items: center;
&-content{
span{
font-weight: bold;
color: #ec3333;
&.unit{
font-size: 12px;
}
&.int{
font-size: 18px;
}
&.float{
font-size: 12px;
}
}
}
.tags {
margin-left: 5px;
position: relative;
padding: 0 6px 0 10px;
height: 18px;
font-size: 12px;
color: #fff;
text-align: center;
line-height: 18px;
border-radius: 4px;
border-bottom-left-radius: 0;
background: linear-gradient(270deg, #fe3d31 0%, #ff5d42 100%);
&.outTime {
background: linear-gradient(270deg, #31c087 0%, #76d09b 100%);
}
.icon {
width: 12px;
height: 20px;
display: block;
position: absolute;
left: 0;
bottom: 0;
}
}
}
.stock{
margin-top: 6px;
display: flex;
align-items: center;
.good-list_progress{
width: 62px;
margin-right: 8px;
}
span{
color: #999;
font-size: 12px;
}
/deep/ .cr-progress__progress{
background: linear-gradient(270deg, #FF5D00 0%, #EC1500 100%) !important;
}
}
}
.shopcart{
margin-left: 8px;
flex-shrink: 0;
width: 30px;
height: 30px;
background-image: url('https://img.lkbang.net/shopcart-seckill.8a1cc2e.png');
background-size: 100%;
background-repeat: no-repeat;
}
}
}
}
}
}
.seckill-title {
height: 60px;
width: 100%;
border-radius: 8px 8px 0 0;
.scene-less-4 {
width: 100%;
height: 60px;
display: flex;
align-items: center;
padding: 0 8px;
.scene-item {
flex: 1;
height: 60px;
// background: pink;
margin: 0 2px;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 8px 0;
box-sizing: border-box;
align-items: center;
&.selected {
.scene-desc {
color: @red;
background: @white;
padding: 0 12px;
border-radius: 9px;
font-weight: bold;
.timers {
padding: 0;
}
}
.scene-name {
color: #fff;
}
.timers {
background: @white;
color: @red;
}
}
.scene-name {
color: rgba(255, 255, 255, 0.85);
font-family: "din";
font-size: 18px;
text-align: center;
}
.scene-desc {
height: 18px;
line-height: 18px;
font-size: 12px;
color: rgba(255, 255, 255, 0.85);
text-align: center;
display: flex;
justify-content: center;
align-items: center;
.timers {
margin: 0 auto;
padding: 0 8px;
}
// margin-top: @padding-base;
}
}
}
.scene-over-4 {
width: 100%;
height: 60px;
display: inline-flex;
align-items: center;
justify-content: flex-start;
padding: 0 8px;
overflow-x: auto;
overflow-y: hidden;
flex-wrap: nowrap;
&::-webkit-scrollbar {
display: none;
}
// overflow-y: hidden;
.scene-item {
min-width: 72px;
flex-shrink: 0;
height: 60px;
// background: pink;
margin: 0 2px;
display: inline-flex;
flex-direction: column;
justify-content: space-between;
padding: 8px 0;
align-items: center;
white-space: nowrap;
&.selected {
.scene-desc {
color: @red;
background: @white;
padding: 0 12px;
border-radius: 9px;
font-weight: bold;
.timers {
padding: 0;
}
}
.scene-name {
color: #fff;
}
.timers {
background: @white;
color: @red;
}
}
.scene-name {
color: rgba(255, 255, 255, 0.85);
font-family: "din";
font-size: 18px;
text-align: center;
}
.scene-desc {
height: 18px;
line-height: 18px;
font-size: 12px;
color: rgba(255, 255, 255, 0.85);
text-align: center;
display: flex;
justify-content: center;
align-items: center;
.timers {
margin: 0 auto;
padding: 0 8px;
}
// margin-top: @padding-base;
}
}
}
}
\ No newline at end of file
This diff is collapsed.
<template>
<div class="home-list">
<div class="home-list-container">
<div v-for="(item, index) in goodsList" :key="item.goodsId" class="Hl-container-item">
<cr-image
class="Hlc-item-img"
:src="item.goods.goodsImage | Img2Thumb"
lazy-load
@click="goDetail(item.goods, index)"
/>
<div class="Hlc-item-info">
<div class="Hlc-item-info-title" @click="goDetail(item.goods, index)">
<span class="Hlc-item-info-title-name">{{ item.goods.goodsName }}</span>
<div
v-if="item.goods.tagList && item.goods.tagList.length"
class="Hlc-item-info-title-active"
>
<div v-for="(itm, idx) in item.goods.tagList" :key="idx">
<span>{{ itm.name }}</span>
</div>
</div>
</div>
<div class="Hlc-item-info-count">
<div class="Hlc-item-info-count-info">
<div class="Hlc-item-info-count-left">
<div class="Hlc-item-info-count-left-price">
<span>¥{{ item.goods.goodsSalePrice }}</span>
<span v-if="item.goods.goodsPrice">¥{{ item.goods.goodsPrice }}</span>
</div>
<div v-if="item.goods.discountShow" class="Hlc-item-info-count-left-activity">
<span
:style="{
color: colorJudgment(item.goods.discountShow)
}"
{{ item.goods.discountShow.discountPrice }}</span
>
<img :src="item.goods.discountShow.icon" />
</div>
</div>
<div class="Hlc-item-info-count-right">
<span>已售{{ item.goods.saleCount }}</span>
</div>
</div>
<div class="Hlc-item-info-count-icon">
<span class="iconfont icon-cart" @click="addProduct(item)" />
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { goodAdd } from '@/api/shopCart.api';
import { colorJudgment } from '@/constants/max';
export default {
name: 'FullMinusoodsList',
props: {
goodsList: {
type: Array,
default: () => []
}
},
data() {
return {
colorJudgment
};
},
methods: {
goDetail(goods) {
const query = {
skuNo: goods.goodsId
};
this.$router.push({ name: 'goodDetail', query });
},
async addProduct(good) {
const shopCartBaseList = [
{
skuId: good.goods.goodsId,
skuNum: 1,
skuSource: good.goods.goodsType,
type: 1
}
];
await goodAdd({ shopCartBaseList, selected: true });
this.$emit('addProduct', good.goods.goodsSalePrice);
this.$toast(`购物车添加成功`);
}
}
};
</script>
<style lang="less" scoped>
.home-list {
background: @white;
padding: 0 @padding-md - 2;
// background: @gray-6;
&-container {
.Hl-container-item {
display: flex;
margin: @padding-lg 0;
&:last-child {
margin-bottom: 0;
}
.Hlc-item-img {
width: 135px !important;
height: 135px !important;
border-radius: @border-radius-sm;
}
.Hlc-item-info {
width: 225px;
height: 130px;
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: @padding-sd;
&-title {
&-name {
.text-size(13);
line-height: 18px;
color: @black;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
&-active {
font-size: 0;
padding-top: @padding-unit;
overflow: hidden;
height: 43px;
div {
display: inline-block;
span {
display: inline-block;
border: 1px solid @border-red;
font-size: 10px;
color: @red;
border-radius: 5px;
padding: 2px 5px;
margin: 2px 2px 2px 0;
}
}
}
}
&-count {
display: flex;
justify-content: space-between;
&-left {
color: @red-light;
margin: 1px;
font-size: 0;
display: flex;
align-items: center;
&-price {
margin-right: 2px;
span {
&:nth-child(1) {
.text-size(10);
color: @red-light;
font-weight: bold;
}
&:nth-child(2) {
margin-right: @padding-unit / 2;
.text-size(14);
line-height: 16px;
color: @red-light;
font-weight: bold;
}
&:nth-child(3) {
.text-size(10);
color: @gray-4;
text-align: right;
}
}
}
&-activity {
display: flex;
align-items: flex-end;
margin-top: 3px;
span {
font-size: 12px;
color: #ed7d08;
}
img {
width: auto;
height: 12px;
}
}
}
&-right {
.text-size(11);
line-height: 12px;
color: @gray-4;
}
}
}
}
}
}
</style>
.home-container {
// height: 1000px;
height: calc(100vh - 164px);
// margin-bottom: 60px;
}
.head {
background-color: #f7f8fa;
margin: 9px 11.5px 0 11.5px;
.activity-desc{
font-size: 13px;color: #333;
}
.endTime-second{
display: flex;
align-items: center;
margin: 12px 12px 12px 0;
&-tip{
font-size:13px;
color: #333;
}
.colon {
display: inline-block;
margin: 0 4px;
color: #ee0a24;
}
.block {
display: inline-block;
width: 22px;
color: #fff;
font-size: 12px;
text-align: center;
background-color: #ee0a24;
border-radius: 6px;
}
}
}
.main{
margin: 103px auto;
color:#999;
img{
width: 180px;
}
&-empty{
display: flex;
justify-content: center
}
.main-empty-tip{
display: flex;
justify-content: center;
font-size:14px;
}
}
.goods-list{
padding-top: 10px;
background-color:#fff;
margin-bottom: 60px;
}
.footer {
width: 100%;
height: 60px;
position: fixed;
bottom: 0;
z-index: 100;
align-items: center;
background-color: #fff;
border-top: 1px solid #fff;
display: flex;
}
.countdown {
color: #ec1500;
border-radius: 4px;
width: 300px;
}
.toCartBtn {
position: absolute;
bottom: 10px;
right: 12px;
background-image: linear-gradient(to left, #ff5d00, #ff1900);
height: 40px;
color: #fff;
border-radius: 20px;
}
.bottom-text {
display: flex;
flex-direction: column;
margin-left: 15px;
p {
font-size:12px;
span{
color: #ec1500;
font-weight: bold;
}
.total-amt{
font-size: 18px;
}
}
.bottom-text-one{
color: #333;
}
.bottom-text-two{
color: #999;
}
}
.btn-small {
width: 130px;
}
.btn-big {
width: 343px;
}
\ No newline at end of file
<template>
<div>
<search-panel ref="panel" @clear="clearKeywords" @enter="searchEnterEvent" />
<search-tab @change="searchTabChange" />
<div v-if="activityInfo.endTimeSecond" class="head">
<p class="activity-desc">{{ activityInfo.activityDesc }}</p>
<div class="endTime-second">
<span class="endTime-second-tip">距结束</span>
<cr-count-down
v-if="activityInfo.endTimeSecond"
:time="activityInfo.endTimeSecond"
@finish="timeup"
>
<template #default="timeData">
<span class="block">{{ timeData.d }}</span>
<span class="colon"></span>
<span class="block">{{ timeData.h }}</span>
<span class="colon"></span>
<span class="block">{{ timeData.m }}</span>
<span class="colon"></span>
<span class="block">{{ timeData.s }}</span>
<span class="colon"></span>
</template>
</cr-count-down>
</div>
</div>
<scroll-view class="home-container" :to-top="scrollTop" @toBottom="loadData" @scroll="onScroll">
<div v-if="!goodsList.length && isShowEmptyImg" class="main">
<div class="main-empty">
<img src="../../assets/images/activity/commodity.png" alt="" />
</div>
<div class="main-empty-tip">没有更多啦!</div>
</div>
<div v-if="goodsList.length" class="goods-list">
<full-minus-goods-list :goods-list.sync="goodsList" @addProduct="addProduct" />
<loadMore :status="loadingType" />
</div>
</scroll-view>
<div class="footer">
<div v-if="!!activityInfo.endTimeSecond" class="bottom-text">
<p class="bottom-text-one">
小计:<span></span><span class="total-amt"> {{ activityInfo.totalAmt }}</span>
</p>
<p class="bottom-text-two">
还差<span>{{ activityInfo.needAmt }}</span
>即可享受{{ desc }}的优惠
</p>
</div>
<button
class="toCartBtn"
:class="!!activityInfo.endTimeSecond ? 'btn-small' : 'btn-big'"
@click="toCart"
>
去购物车
</button>
</div>
</div>
</template>
<script>
import fullMinusGoodsList from './components/fullMinusGoodsList.vue';
import { homeSearch, getActivityInfo } from '@/api/home.api.js';
import ScrollView from '@/components/scrollView';
import loadMore from '@/components/loadMore';
import SearchTab from '@/components/search/SearchTab';
import SearchPanel from '@/components/search/SearchPanel';
export default {
name: 'ActivityProduct',
components: {
fullMinusGoodsList,
ScrollView,
loadMore,
SearchTab,
SearchPanel
},
data() {
return {
goodsList: [],
loadingType: 'loading',
desc: '',
isShowEmptyImg: false,
searchForm: {
searchText: undefined,
activityType: '1', // 活动类型1: 满减 2:金刚位专题
activityId: '',
pageNo: 0,
pageSize: 20,
rankType: 1, // 排序类型 1:综合; 2:价格倒序; 3: 价格 正序; 4. 销量倒序; 5: 销量正序;
searchId: undefined // 搜索批次ID(首次搜索不传)
},
activityInfo: {}, // 活动信息
scrollTop: 0
};
},
computed: {
showDay() {
return this.activityInfo.endTimeSecond > 86400;
}
},
activated() {
const currnetScrollTop = this.scrollTop;
this.scrollTop = 0;
this.$nextTick(() => {
this.scrollTop = currnetScrollTop;
});
},
mounted() {
this.searchForm.activityId = this.$route.query.activityId;
this.desc = this.$route.query.activityDesc;
this.loadData();
},
methods: {
timeup() {
this.activityInfo.endTimeSecond = 0;
this.searchForm.activityId = '';
this.pageNo = 0;
this.activity = null;
this.loadData();
},
async loadData() {
this.hasRecommend = false;
if (this.loadingType === 'noMore' || this.progress === 'loading') {
return;
}
this.progress = 'loading';
this.searchForm.pageNo++;
const [data] = await getActivityInfo({
activityId: this.searchForm.activityId,
activityType: this.searchForm.activityType,
queryShopCartSku: true
});
// 活动信息赋值
this.$set(this, 'activityInfo', { ...data.activityInfo });
await this.getList();
this.progress = 'finished';
},
/**
* 加入购物车
*/
addProduct(val) {
this.activityInfo.totalAmt = Math.round((+this.activityInfo.totalAmt + +val) * 100) / 100;
this.activityInfo.needAmt =
+this.activityInfo.needAmt - +val > 0
? Math.round((this.activityInfo.needAmt - +val) * 100) / 100
: 0;
},
toCart() {
this.$router.push('/shopcart');
},
/**
* 搜索---删除关键字
*/
clearKeywords() {
this.searchForm.searchText = '';
},
/**
* 搜索
*/
async search() {
// this.isSearch = true;
this.goodsList = [];
this.searchForm.pageNo = 1;
this.getList();
},
/**
* 获取商品列表
*/
async getList() {
this.loadingType = 'loading';
const [res] = await homeSearch(this.searchForm);
this.hasRecommend = res.hasRecommend;
if (res.goodsList) {
this.goodsList = [...this.goodsList, ...res.goodsList];
}
if (!this.goodsList.length) {
this.isShowEmptyImg = true;
}
this.searchForm.searchId = res.searchId;
this.loadingType = res.hasNext ? 'more' : 'noMore';
},
/**
* 修改排序
*/
searchTabChange(rankType) {
this.searchForm.rankType = rankType;
this.search();
},
/**
* 回车搜索
*/
searchEnterEvent(keyWords) {
this.searchForm.searchText = keyWords;
this.search();
},
/**
* 滑动
*/
onScroll(top) {
this.scrollTop = top;
}
}
};
</script>
<style lang="less" src="./index.less" scoped />
<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 {
padding-bottom: 50px;
.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-size(13);
}
.detail {
display: inline-block;
word-break: break-all;
overflow-wrap: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
color: @gray-5;
.text-size(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.trim="address.receiverName"
class="address-input"
name="收货人"
label="收货人"
:max-length="20"
placeholder="请输入收货人姓名,最多20个字"
@input="stripscript"
/>
<cr-field
v-model.trim="address.receiverPhoneNo"
class="address-input"
name="手机号码"
label="手机号码"
:max-length="11"
placeholder="请输入收货人手机号码"
/>
<cr-field class="address-input area" name="所在地区" label="所在地区">
<template #input>
<popup-area-tab-picker
ref="areaPicker"
v-model.trim="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.trim="address.detail"
class="address-input-textarea"
placeholder="如道路、门牌号、小区、楼栋号、单元室等"
:autosize="{ minRows: 3, maxRows: 10 }"
:maxlength="50"
@change="detailScript"
/>
</template>
</cr-field>
<cr-cell title="设置默认地址" class="address-input">
<cr-switch
v-model.trim="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';
import { isPhone } from '@/service/validation.service';
const ADDRESS_ADD = 'addressAdd';
function replaceAddrStr(str = '', length) {
return str.replace(/[^a-zA-Z0-9_\u4e00-\u9fa5-]/g, '').substr(0, length);
}
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.params.order || false;
if (this.type === 'edit') {
const data = { ...this.$route.params.editAddress };
if (data) {
data.addrFullName = data.addrFullName
?.substring(0, data.addrFullName.length - data.detail.length)
.replace(/()/g, '');
this.address = data;
}
}
},
methods: {
stripscript(s) {
this.$nextTick(() => {
this.address.receiverName = replaceAddrStr(s, 20);
});
},
detailScript(s) {
const v = s?.target?.value;
this.$nextTick(() => {
this.address.detail = replaceAddrStr(v, 50);
});
},
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;
}
const phoneLen = 11;
if (data.receiverPhoneNo.length !== phoneLen || !isPhone(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' ? '修改' : '添加'}成功`);
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;
}
}
.address-input {
overflow: hidden;
}
.area {
height: 46px;
overflow: hidden;
}
</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-size(16);
}
}
</style>
<template>
<div class="bill-card">
<div class="content">
<div class="left-content">
<div v-if="status === PAY_COMPLETE" class="pay-off-box">
<h3 class="pay-off">本月账单已还清</h3>
<p class="zan">好棒,点个赞</p>
</div>
<div v-else class="arrears-box">
<p class="arrears-title">剩余待还(元)</p>
<p class="arrears-text">
<span class="money">{{ orderAmount }}</span>
<span v-if="status === PAY_BACK" class="text">还款中…</span>
</p>
<p v-if="status === OVERDUE" class="overdue">
已逾期
</p>
<p v-if="status !== OVERDUE && lastDay" class="last-day">
最后还款日{{ lastDay }},请在还款日前还款
</p>
<p class="to-details" @click="handleToDetails">
查看详情>
</p>
</div>
</div>
<div v-if="status === PAY_COMPLETE || status === OVERDUE" class="right-content">
<div v-if="status === PAY_COMPLETE" class="zan-img">
<img :src="payBackZan" alt="" />
</div>
<div v-if="status === OVERDUE" class="overdue-img">
<img v-if="isCalendar" :src="groupImg" alt="" />
<img v-else :src="payBackYuQi" alt="" />
</div>
</div>
</div>
</div>
</template>
<script>
import payBackZan from '@/assets/images/bill/payback_zan.png';
import payBackYuQi from '@/assets/images/bill/payback_yuqi.png';
import groupImg from '@/assets/images/bill/group.png';
import { SHOULD_ALSO, PAY_BACK, OVERDUE, PAY_COMPLETE } from '@/constants/bill';
export default {
components: {},
props: {
// 已还清0/应还1/还款中2/逾期3
status: {
type: Number,
default: 0
},
isCalendar: {
type: Boolean,
default: false
},
orderAmount: {
type: String,
default: ''
},
lastDay: {
type: String,
default: ''
},
repayAmountDetail: {
type: Object,
default: () => {}
}
},
data() {
return {
billActive: SHOULD_ALSO,
payBackZan,
payBackYuQi,
groupImg,
PAY_BACK,
OVERDUE,
PAY_COMPLETE
};
},
mounted() {},
methods: {
handleToDetails() {
this.$router.push({
name: 'payBackDetails',
params: {
...this.repayAmountDetail,
amount: this.orderAmount
}
});
}
}
};
</script>
<style lang="less" scoped>
.bill-card {
padding: 0 12px;
z-index: 2;
position: relative;
}
.content {
display: flex;
height: 160px;
background: @white;
padding: 0 28px 0 24px;
width: 100%;
box-sizing: border-box;
justify-content: space-between;
box-shadow: 1px 8px 12px #c5c2c2;
.arrears-box {
margin-top: 18px;
color: @black;
.arrears-title {
.text-size(12);
}
.arrears-text {
font-size: 30px;
margin: 3px 0 12px 0;
}
.money {
font-weight: bold;
}
.overdue {
color: #f41c1c;
.text-size(20);
margin-bottom: 17px;
}
.last-day {
.text-size(12);
margin-bottom: 10px;
color: @gray-4;
}
.to-details {
.text-size(12);
color: @black;
}
}
.pay-off-box {
margin-left: 4px;
margin-top: 48px;
margin-left: 4px;
}
.pay-off {
.text-size(26);
color: @black;
margin-bottom: 2px;
}
.zan {
color: @black;
.text-size(14);
}
.right-content {
width: 100px;
height: 100px;
div {
box-sizing: border-box;
display: flex;
}
}
.zan-img {
margin-top: 30px;
padding: 0 10px;
}
.overdue-img {
margin-top: 30px;
display: flex;
padding: 0 20px;
}
.right-content {
img {
justify-content: center;
align-items: center;
width: 100%;
}
}
}
</style>
<template>
<!-- -->
<div v-if="!isPaidOff" class="button-group">
<!-- 逾期 -->
<cr-button v-if="isOverdue" class="button" type="primary" @click="handleToImmediate"
>立即还款</cr-button
>
<!-- 单按钮 -->
<template v-if="!isOverdue && !isStages">
<cr-button
v-if="billActive === shouldBill || isOverdue"
:disabled="isPayInProgress"
class="button"
type="primary"
@click="handleToImmediate"
>立即还款</cr-button
>
<cr-button
v-else
:disabled="isPayInProgress"
class="button"
type="primary"
@click="handleToAdvance"
>{{ billHead.forwardName }}</cr-button
>
</template>
<!-- 双按钮 -->
<template v-if="!isOverdue && isStages">
<div v-if="billActive === shouldBill" class="advance-stages">
<cr-button
:disabled="isPayInProgress"
class="button"
color="#faab0c"
@click="handleToImmediate"
>立即还款</cr-button
>
<cr-button
:disabled="isPayInProgress"
class="button stages-btn"
type="primary"
@click="handleToStages"
>{{ billHead.installmentName }}</cr-button
>
</div>
<div v-else class="stages">
<cr-button
:disabled="isPayInProgress"
class="button"
color="#faab0c"
@click="handleToAdvance"
>{{ billHead.forwardName }}</cr-button
>
<cr-button
:disabled="isPayInProgress"
class="button stages-btn"
type="primary"
@click="handleToStages"
>{{ billHead.installmentName }}</cr-button
>
</div>
</template>
</div>
</template>
<script>
import localStorage from '@/service/localStorage.service';
import { PAY_BACK, OVERDUE, PAY_COMPLETE } from '@/constants/bill';
export default {
components: {},
props: {
billHead: {
type: Object,
default: () => {}
},
// 已还清0/应还1/还款中2/逾期3
selectStatus: {
type: [String, Number],
default: ''
},
// 0 应还 1 待还
billActive: {
type: [String, Number],
default: ''
},
balanceAmount: {
type: [String, Number],
default: ''
},
billNo: {
type: [String, Number],
default: ''
},
addPrice: {
type: [String, Number],
default: ''
}
},
data() {
return {
shouldBill: 0,
awaitBill: 1
};
},
computed: {
isPayInProgress() {
return this.selectStatus === PAY_BACK;
},
isStages() {
return (
this.billHead.installmentUrl &&
this.billHead.installmentUrl !== '' &&
this.billActive === this.shouldBill
);
},
isForwardUrl() {
return this.billHead.forwardUrl && this.billHead.forwardUrl !== '';
},
isOverdue() {
return this.selectStatus === OVERDUE;
},
isPaidOff() {
return this.selectStatus === PAY_COMPLETE;
},
// isOverdueAwait() {
// return this.isOverdue && this.billActive === this.awaitBill;
// },
repaymentUrl() {
return `${this.formatUrl(
this.billHead.forwardUrl
)}&repayFinishedForwardSfqUrl=${encodeURIComponent(window.location.href)}`;
}
},
methods: {
handleToImmediate() {
window.location.href = this.repaymentUrl;
},
handleToAdvance() {
window.location.href = this.repaymentUrl;
},
handleToStages() {
this.$router.push({
name: 'stages',
query: {
billNo: this.billNo
}
});
},
formatUrl(url) {
return url
?.replace(/\{billNumber\}/g, this.billNo)
?.replace(/\{itemPrice\}/g, this.balanceAmount)
?.replace(/\{isBothBtn\}/g, this.getBothBtn())
?.replace(/\{addPrice\}/g, this.billActive && this.addPrice)
.replace(/\{token\}/g, localStorage.get('vccToken'));
},
getBothBtn() {
let isBothBtn = true;
if (this.isOverdue || !this.isStages) {
isBothBtn = false;
}
return isBothBtn;
}
}
};
</script>
<style lang="less" scoped>
.button-group {
position: fixed;
bottom: 0px;
left: 0;
right: 0;
height: 50px;
background: @white;
padding: 5px 16px;
box-sizing: border-box;
display: flex;
.advance-stages,
.stages {
width: 100%;
display: flex;
}
.button {
border-radius: 20px;
width: 100%;
display: block;
height: 40px;
}
.advance-stages {
.button {
width: 50%;
border-radius: 0;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
}
.stages-btn {
border-radius: 0;
border-top-right-radius: 20px;
border-bottom-right-radius: 20px;
width: 50%;
}
}
.stages {
.button {
border-radius: 0;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
width: 50%;
}
.stages-btn {
border-radius: 0;
border-top-right-radius: 20px;
border-bottom-right-radius: 20px;
}
}
}
</style>
<template>
<div class="circle-bar" :style="barStyle">
<div class="circle-bar-left" :style="rectLeftStyle" />
<div class="circle-bar-right" :style="rectRightStyle" />
</div>
</template>
<script>
export default {
components: {},
props: {
radius: {
type: Number,
default: 16
},
bgColor: {
type: String,
default: '#fff'
},
barColor: {
type: String,
default: '#ed6a0c'
},
percent: {
type: Number,
default: 10
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
leftRotate: 0,
rightRotate: 0,
rightBg: ''
};
},
computed: {
barStyle() {
return {
width: `${this.radius}px`,
height: `${this.radius}px`,
background: this.barColor,
border: `1px solid ${this.barColor}`
};
},
rectLeftStyle() {
const { width, height } = this.barStyle;
return {
clip: `rect(0, ${this.radius / 2}px, auto, 0)`,
background: this.bgColor,
width,
height,
transform: `rotate(${this.leftRotate}deg)`
};
},
rectRightStyle() {
const { width, height } = this.barStyle;
return {
clip: `rect(0, auto, auto, ${this.radius / 2}px)`,
background: this.rightBg,
width,
height,
transform: `rotate(${this.rightRotate}deg)`
};
}
},
watch: {
bgColor: {
handler() {
this.rightBg = this.bgColor;
},
immediate: true
}
},
mounted() {
if (this.percent <= 50) {
this.rightRotate = this.percent * 3.6;
} else {
this.rightBg = this.barColor;
this.rightRotate = 0;
this.leftRotate = (this.percent - 50) * 3.6;
}
},
methods: {}
};
</script>
<style lang="less" scoped>
.circle-bar {
position: relative;
}
.circle-bar * {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
.circle-bar,
.circle-bar > * {
border-radius: 50%;
}
</style>
<template>
<div class="bill-content">
<h3 v-if="!overdue" class="title">
{{ title }}
<span v-if="billTradeCycle" class="title-text">入账周期:{{ billTradeCycle }}</span>
</h3>
<p v-else class="tip">逾期后每天会收取您逾期滞纳金并上报征信!请尽快还款!</p>
<slot />
</div>
</template>
<script>
export default {
components: {},
props: {
title: {
type: String,
default: '账单分期'
},
billTradeCycle: {
type: String,
default: ''
},
overdue: {
type: Boolean,
default: false
}
},
data() {
return {};
}
};
</script>
<style lang="less" scoped>
.title {
font-size: 16px;
color: #333333;
margin-left: 20px;
margin-top: 24px;
position: relative;
}
.tip {
color: #f41c1c;
.text-size(12);
margin-left: 20px;
margin-top: 24px;
}
.title-text {
font-size: 12px;
color: #999999;
text-align: right;
position: absolute;
right: 20px;
}
</style>
<template>
<div class="bill-item">
<div class="item" @click="handleToDetails">
<div class="left">
<div v-if="itemData.stages" class="step">
<circle-step v-bind="false && disabled" :percent="percent" />
</div>
<div :class="[itemData.overdue && 'overdue-reverse']">
<p class="title">{{ itemData.title }}</p>
<p class="text ellipsis">
{{ itemData.text }}
</p>
</div>
</div>
<div class="right">
<p class="title">{{ itemData.rightTitle }}</p>
<p :class="['text', itemData.overdue && 'overdue-text']">
{{ itemData.rightText }}<span v-if="!isDetails" class="arrow">></span>
</p>
</div>
</div>
</div>
</template>
<script>
import circleStep from './circleStep';
import { getMonthAdd0 } from '@/service/utils.service';
import localStorage from '@/service/localStorage.service';
export default {
components: {
circleStep
},
props: {
itemData: {
type: Object,
default: () => {}
},
isStages: {
type: Boolean,
default: false
},
isOverdue: {
type: Boolean,
default: false
},
userId: {
type: String,
default: ''
},
isDetails: {
type: Boolean,
default: false
}
},
data() {
return {
billActive: 0,
disabled: {
bgColor: '#fff',
barColor: '#d8d8d8'
}
};
},
computed: {
percent() {
return (this.itemData.currTerm / this.itemData.term) * 100;
}
},
mounted() {},
methods: {
handleToDetails() {
if (this.isOverdue && !this.isDetails) {
const { year, month, billNo } = this.itemData || {};
const date = year + getMonthAdd0(month) + '01';
this.$router.push({
name: 'billDetails',
query: {
yearMonth: date,
billNo,
userId: this.userId
}
});
return;
}
let url = this.itemData.orderUrl;
if (this.isStages) {
url = this.itemData.installmentUrl;
}
window.location.href = this.formatUrl(url);
},
formatUrl(url) {
return url?.replace(/\{token\}/g, localStorage.get('vccToken'));
}
}
};
</script>
<style lang="less" scoped>
.bill-item {
padding: 0 20px;
box-sizing: border-box;
}
.item {
display: flex;
width: 100%;
justify-content: space-between;
padding-top: 16px;
box-sizing: border-box;
border-bottom: 1px solid #d8d8d8;
height: 72px;
}
.left {
display: flex;
.step {
margin-right: 5px;
}
.title {
color: @gray-5;
.text-size(12);
}
.text {
margin-top: 11px;
color: @font-color-dark;
.text-size(14);
max-width: 190px;
}
.overdue-reverse {
display: flex;
flex-direction: column-reverse;
.title {
margin-top: 11px;
margin-bottom: 7px;
}
.text {
margin-top: 0;
.text-size(16);
}
}
}
.right {
.title {
color: @gray-4;
.text-size(12);
height: 18px;
}
.text {
margin-top: 11px;
color: @font-color-dark;
text-align: right;
.text-size(14);
.arrow {
color: @gray-4;
margin-left: 8px;
}
}
}
.overdue-text {
font-size: 12px !important;
color: #f41c1c !important;
}
</style>
.bill {
background: #fff;
min-height: 100%;
box-sizing: border-box;
padding-bottom: 50px;
position: relative;
}
.bg {
position: absolute;
top: 0;
height: 164px;
left: 0;
right: 0;
background: linear-gradient(
269deg
, #ff5d00 12%, #ff1900 86%);
&.history{
height: 44px;
}
}
.empty {
font-size: 16px;
margin-top: 80px;
text-align: center;
}
.history-item {
color: #999;
display: flex;
justify-content: space-between;
height: 81px;
border-bottom: 1px solid #d8d8d8;
padding-top: 15px;
box-sizing: border-box;
margin-left: 20px;
padding-right: 20px;
position: relative;
z-index: 2;
.time {
font-size: 14px;
color: #333333;
}
.type {
font-size: 14px;
color: #333333;
margin-top: 6px;
&.overdue {
color: #F41C1C;
}
}
.money {
font-size: 14px;
color: #333333;
text-align: right;
margin-top: 20px;
.arrow {
margin-left: 5px;
color: #d8d8d8;
font-size: 16px;
}
}
}
::v-deep {
.cr-hairline--top-bottom {
&:after{
border-color: #fff !important;
}
}
.cr-collapse-item__content {
padding: 0;
}
.cr-sticky {
& > div{
height: 44px;
overflow: hidden;
}
}
}
<template>
<div class="bill">
<div :class="['bg', billActive === history && 'history']" />
<cr-tabs v-model="billActive" sticky v-bind="tabsConfig">
<cr-tab :title="billHead.shouldBillDesc">
<bill-card v-bind="sCardData" />
<list-content v-if="isOverdue" overdue>
<list-item
v-for="(item, idx) in overdueBillList"
:key="idx"
:item-data="item"
:is-overdue="true"
:user-id="userInfo.userId"
/>
</list-content>
<list-content v-if="sNoAmortizeOrderList.length > 0" title="账单分期">
<list-item
v-for="(item, idx) in sNoAmortizeOrderList"
:key="idx"
:item-data="item"
:is-stages="true"
/>
</list-content>
<list-content
v-if="sAmortizeOrderList.length > 0"
title="本月账单明细"
:bill-trade-cycle="shouldBill.billTradeCycle"
>
<list-item v-for="(item, idx) in sAmortizeOrderList" :key="idx" :item-data="item" />
</list-content>
</cr-tab>
<cr-tab :title="billHead.awaitBillDesc">
<bill-card v-bind="aCardData" />
<list-content v-if="isOverdue" overdue>
<list-item
v-for="(item, idx) in aOverdueBillList"
:key="idx"
:item-data="item"
:is-overdue="true"
:user-id="userInfo.userId"
/>
</list-content>
<list-content v-if="aNoAmortizeOrderList.length > 0" title="账单分期">
<list-item
v-for="(item, idx) in aNoAmortizeOrderList"
:key="idx"
:item-data="item"
:is-stages="true"
/>
</list-content>
<list-content
v-if="aAmortizeOrderList.length > 0"
title="本月账单明细"
:bill-trade-cycle="awaitBill.billTradeCycle"
>
<list-item v-for="(item, idx) in aAmortizeOrderList" :key="idx" :item-data="item" />
</list-content>
</cr-tab>
<cr-tab title="历史账单">
<cr-collapse v-if="historyBillList.length > 0" v-model="historyActive">
<cr-collapse-item
v-for="(item, idx) in historyBillList"
:key="idx"
:title="item.billYear"
:name="idx"
>
<div
v-for="(h, hIdx) in item.value"
:key="hIdx"
class="history-item"
@click="handleToDetails(h)"
>
<div class="text">
<p class="time">{{ h.billMonth }}月账单</p>
<p class="type">
{{ getStatus(h.status) }}
</p>
</div>
<div class="money">
{{ h.billAmount }}
<i class="arrow iconfont icon-arrow" />
</div>
</div>
</cr-collapse-item>
</cr-collapse>
<div v-else class="empty">暂无历史账单</div>
</cr-tab>
</cr-tabs>
<botton-group
v-if="isButton"
:bill-head="billHead"
:select-status="selectStatus"
:bill-active="billActive"
:balance-amount="balanceAmount"
:bill-no="billNo"
:add-price="shouldBill.balanceAmount"
/>
</div>
</template>
<script>
import billCard from './components/billCard';
import bottonGroup from './components/bottonGroup';
import listContent from './components/listContent';
import listItem from './components/listItem';
import { getBill, getHistoryBill } from '@/api/bill.api';
import { getMonthAdd0 } from '@/service/utils.service';
import { mapState } from 'vuex';
import dayjs from 'dayjs';
import {
AWAIT_BILL,
SHOULD_BILL,
HISTORY_BILL,
SHOULD_ALSO,
PAY_BACK,
OVERDUE,
PAY_COMPLETE
} from '@/constants/bill';
export default {
components: {
billCard,
bottonGroup,
listItem,
listContent
},
data() {
return {
tabsConfig: {
'title-inactive-color': '#eee',
'title-bg-color': 'transparent',
'title-active-color': '#fff',
'sticky-active-color': '#fff',
'sticky-inactive-color': '#eee',
'sticky-line-color': '#fff',
'sticky-bg-color': 'linear-gradient(269deg, #ff5d00 12%, #ff1900 86%)',
color: '#fff'
},
awaitBill: {
month: 0
},
shouldBill: {
month: 0
},
billHead: {},
overdueBillList: [],
aAmortizeOrderList: [],
aNoAmortizeOrderList: [],
sNoAmortizeOrderList: [],
sAmortizeOrderList: [],
billActive: 0,
historyActive: [0],
isAdvance: false,
isStages: true,
history: 2,
aCardData: {},
sCardData: {},
historyBillList: [],
aOverdueBillList: []
};
},
computed: {
...mapState({
userInfo: state => state.pay.userInfo || {}
}),
isOverdue() {
return this.shouldBill.status === OVERDUE;
},
isButton() {
return !this.historyStatus;
},
shouldStatus() {
return this.billActive === SHOULD_BILL;
},
awaitStatus() {
return this.billActive === AWAIT_BILL;
},
historyStatus() {
return this.billActive === HISTORY_BILL;
},
selectStatus() {
let status;
if (this.shouldStatus) {
status = this.shouldBill.status;
}
if (this.awaitStatus) {
status = this.awaitBill.status;
}
return status;
},
balanceAmount() {
let balanceAmount;
if (this.shouldStatus) {
balanceAmount = this.shouldBill.balanceAmount;
}
if (this.awaitStatus) {
balanceAmount = this.awaitBill.balanceAmount;
}
return balanceAmount;
},
billNo() {
let billNo;
if (this.shouldStatus) {
billNo = this.shouldBill.billNo;
}
if (this.awaitStatus) {
billNo = this.awaitBill.billNo;
}
return billNo;
}
},
mounted() {
this.init();
},
methods: {
async init() {
const [billRes] = await getBill({
userId: this.userInfo.userId
});
if (billRes) {
const { awaitBill, shouldBill, billHead } = billRes || {};
this.awaitBill = awaitBill;
this.shouldBill = shouldBill;
this.billHead = billHead;
this.overdueBillList = this.formatOverdue(shouldBill.overdueBillList);
this.aOverdueBillList = this.formatOverdue(awaitBill.overdueBillList);
this.sNoAmortizeOrderList = this.formatNoAmortize(shouldBill.noAmortizeOrderList);
this.sAmortizeOrderList = this.formatAmortize(shouldBill.amortizeOrderList);
this.aAmortizeOrderList = this.formatAmortize(awaitBill.amortizeOrderList);
this.aNoAmortizeOrderList = this.formatNoAmortize(awaitBill.noAmortizeOrderList);
this.sCardData = this.formtCardData(shouldBill);
this.aCardData = this.formtCardData(awaitBill);
const [historyBillRes] = await getHistoryBill();
const { historyBillList } = historyBillRes || {};
this.historyBillList = this.formatHistory(historyBillList);
}
},
handleToDetails(item) {
const { year, month, billNo } = item || {};
const date = year + getMonthAdd0(month) + '01';
this.$router.push({
name: 'billDetails',
query: {
yearMonth: date,
billNo,
userId: this.userInfo.userId
}
});
},
formatOverdue(data) {
return (
data?.map(item => {
const { amount, billName, billNo } = item;
return {
title: amount,
text: billName,
rightText: billNo,
overdue: true,
...item
};
}) || []
);
},
formtCardData(data) {
const { lastDay, status, balanceAmount, repayAmountDetail } = data;
return {
status,
orderAmount: balanceAmount,
lastDay: dayjs(lastDay).format('YYYY-MM-DD HH:mm'),
repayAmountDetail
};
},
formatAmortize(data) {
return (
data?.map(item => {
const { orderName, orderAmount, orderTime, orderUrl, refundFlag } = item;
return {
title: dayjs(orderTime).format('YYYY-MM-DD HH:mm'),
text: orderName,
rightTitle: refundFlag,
rightText: orderAmount,
orderUrl
};
}) || []
);
},
formatNoAmortize(data) {
return (
data?.map(item => {
const { orderName, orderAmount, term, currTerm } = item;
const title = `第${currTerm}/${term}期`;
return {
title,
text: orderName,
rightText: orderAmount,
term,
currTerm,
stages: true,
...item
};
}) || []
);
},
formatHistory(data) {
let result = [];
data?.forEach(item => {
let { billYear } = item;
if (!result[billYear]) {
result[billYear] = {
billYear,
value: []
};
}
result[billYear].value.push({ ...item });
});
return Object.values(result).reverse();
},
getStatus(type) {
let str = '';
switch (type) {
case SHOULD_ALSO:
str = '应还';
break;
case PAY_BACK:
str = '还款中';
break;
case OVERDUE:
str = '逾期';
break;
case PAY_COMPLETE:
str = '已完成';
break;
}
return str;
}
}
};
</script>
<style lang="less" src="./index.less" scoped></style>
.details-container {
background: #fff;
min-height: 100%;
box-sizing: border-box;
}
.bg {
position: fixed;
top: 0;
height: 120px;
left: 0;
right: 0;
background: linear-gradient(
269deg
, #ff5d00 12%, #ff1900 86%);
}
<template>
<div class="details-container">
<div class="bg" />
<bill-card v-bind="cardData" />
<list-content v-if="isOverdue" overdue>
<template v-for="(item, idx) in overdueOrderList">
<list-item :key="idx" :item-data="item" :is-overdue="true" :is-details="true" />
</template>
</list-content>
<list-content v-if="noAmortizeOrderList.length > 0" title="账单分期">
<template v-for="(item, idx) in noAmortizeOrderList">
<list-item :key="idx" :item-data="item" :is-stages="true" />
</template>
</list-content>
<list-content
v-if="amortizeOrderList.length > 0"
title="本月账单明细"
:bill-trade-cycle="bill.billTradeCycle"
>
<template v-for="(item, idx) in amortizeOrderList">
<list-item :key="idx" :item-data="item" />
</template>
</list-content>
</div>
</template>
<script>
import billCard from '../bill/components/billCard';
import listContent from '../bill/components/listContent';
import listItem from '../bill/components/listItem';
import { getDetailBill } from '@/api/bill.api';
import dayjs from 'dayjs';
export default {
components: {
billCard,
listItem,
listContent
},
data() {
return {
bill: {},
amortizeOrderList: [],
overdueOrderList: [],
noAmortizeOrderList: [],
cardData: {}
};
},
computed: {
isOverdue() {
return this.bill.status === 3;
}
},
mounted() {
this.init();
},
methods: {
async init() {
const { yearMonth, billNo, userId } = this.$route.query;
try {
const [res] = await getDetailBill({
yearMonth,
billNo,
userId
});
const { bill } = res || {};
this.bill = bill;
this.amortizeOrderList = this.formatAmortize(bill.amortizeOrderList);
this.overdueOrderList = this.formatOverdue(bill.overdueOrderList);
this.noAmortizeOrderList = this.formatNoAmortize(bill.noAmortizeOrderList);
this.cardData = this.formtCardData(bill);
} catch (err) {
console.log(err);
}
},
formatOverdue(data) {
return (
data?.map(item => {
const { amount, billName, billNo } = item;
return {
title: amount,
text: billName,
rightText: billNo,
overdue: true,
...item
};
}) || []
);
},
formatAmortize(data) {
return (
data?.map(item => {
const { orderName, orderAmount, orderTime } = item;
return {
title: dayjs(orderTime).format('YYYY-MM-DD HH:mm'),
text: orderName,
rightText: orderAmount,
...item
};
}) || []
);
},
formatNoAmortize(data) {
return (
data?.map(item => {
const { orderName, orderAmount, term, currTerm } = item;
const title = `第${currTerm}/${term}期`;
return {
title,
text: orderName,
rightText: orderAmount,
term,
currTerm,
stages: true,
...item
};
}) || []
);
},
formtCardData(data) {
const { status, balanceAmount, repayAmountDetail } = data;
return {
status: Number(status),
orderAmount: balanceAmount.toString(),
repayAmountDetail
};
}
}
};
</script>
<style lang="less" src="./index.less" scoped></style>
.stages-container {
min-height: 100%;
background: #fff;
}
.stages-money {
height: 128px;
background: linear-gradient(
269deg
, #ff5d00 12%, #ff1900 86%);
width: 100%;
box-sizing: border-box;
text-align: center;
padding-top: 31px;
color: #fff;
.text{
.text-size(12)
}
.money{
font-size: 30px;
margin-top: 9px;
}
border-bottom: 8px solid #f8f8f8;
}
.stages-trem {
box-sizing: border-box;
padding-left: 20px;
.item{
display: flex;
align-items: center;
height: 68px;
width: 100%;
}
.content {
border-bottom: 1px solid #F0F2F5;
height: 68px;
width: 307px;
padding-top: 10px;
box-sizing: border-box;
.trem{
font-size: 15px;
color: #333333;
line-height: 17px;
margin-bottom: 12px;
}
.each {
font-size: 13px;
color: #999999;
line-height: 17px;
}
}
}
.btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
// height: 50px;
box-sizing: border-box;
padding: 5px 16px;
.is-agree {
margin-bottom: 5px;
.text{
font-size: 10px;
i{
color: @red;
font-style: normal;
}
}
}
}
.button {
border-radius: 20px;
display: block;
width: 100%;
}
.pop-content {
padding: 0 20px;
box-sizing: border-box;
margin-bottom: 188px;
}
.stages-title {
line-height: 48px;
text-align: center;
font-size: 16px;
color: #323233;
}
.pop-item {
display: flex;
border-bottom: 1px solid #F0F2F5;
justify-content: space-between;
height: 48px;
.title,.text{
line-height: 48px;
display: block;
font-size: 14px;
color: #999999;
}
.text-content {
padding-top: 10px;
box-sizing: border-box;
span{
color: #333;
font-size: 14px;
display: block;
}
.tip{
font-size: 12px;
margin-top: 5px;
color: #999;
text-align: right;
}
}
.text{
color: #333333;
}
}
.pop-btn{
width: 100%;
height: 50px;
box-sizing: border-box;
border-top: 1px solid #F0F2F5;
padding: 5px 16px;
}
::v-deep .cr-radio__icon {
margin-top: -5px;
}
::v-deep{
.tip-block {
position: relative;
z-index: 100!important;
.cr-dialog--header,.cr-dialog--message {
padding: 10px 20px;
font-size:14px;
color: #333333;
}
.cr-dialog--message {
padding-top: 0;
text-align: justify;
}
}
}
<template>
<div class="stages-container">
<div class="stages-money">
<p class="text">可分期金额</p>
<h4 class="money">{{ stages.installmentAmount }}</h4>
</div>
<div class="stages-trem">
<cr-radio-group v-model="tremActive" direction="vertical">
<cr-radio v-for="(item, idx) in stages.installmentList" :key="idx" class="item" :name="idx">
<div class="content">
<p class="trem">{{ item.term }}期 每期{{ item.amount }}</p>
<p class="each">每期综合服务费{{ item.fee }}</p>
</div>
</cr-radio>
</cr-radio-group>
</div>
<div class="btn">
<div class="is-agree">
<cr-checkbox v-model="isAgree" shape="round">
<span class="text"
>勾选同意<i @click.stop="handleToContract">《分期服务确认书》</i></span
></cr-checkbox
>
</div>
<cr-button class="button" type="primary" @click="handleStages">确认分期</cr-button>
</div>
<cr-popup v-model="isPop" class="pop" round closeable position="bottom">
<h2 class="stages-title">确认分期</h2>
<div class="pop-content">
<div class="pop-item">
<span class="title">分期金额</span>
<span class="text">
{{ stages.installmentAmount }}
</span>
</div>
<div class="pop-item">
<span class="title">分期期数</span>
<span class="text"> {{ selectStages.term }}</span>
</div>
<div class="pop-item">
<span class="title">每期应还</span>
<div class="text-content">
<span> {{ selectStages.amount }}元(含手续费{{ selectStages.fee }}元) </span>
<p class="tip" @click="handleOpenFee">含综合服务费{{ selectStages.fee }}</p>
</div>
</div>
<div class="pop-item">
<span class="title">年化利率</span>
<span class="text">
{{ selectStages.irr }}
</span>
</div>
<div class="pop-item">
<span class="title">还款日</span>
<span class="text"> {{ selectStages.repayDay }} </span>
</div>
</div>
<div class="pop-btn">
<cr-button class="button" type="primary" @click="handleStagesSure">确认</cr-button>
</div>
</cr-popup>
<div class="tip-block">
<cr-dialog v-model="showDialog" confirm-button-text="知道了">
<div slot="title" class="tip-title">
费用说明
</div>
<div>
综合服务费:包括依据用户与资金提供方以及相关担保方、保险方的约定,按期向资金提供方缴纳的利息、分期服务费等,以及向担保方缴纳的担保费、保险费等在内的全部资金成本。除还款本金以及该金额外用户无需就贷款事项支付其他任何费用。
</div>
</cr-dialog>
</div>
</div>
</template>
<script>
import { getAmortizeDetail, getAmortize } from '@/api/bill.api';
export default {
components: {},
data() {
return {
tremActive: 0,
isPop: false,
stages: {},
billNo: '',
isAgree: false,
showDialog: false
};
},
computed: {
selectStages() {
return this.stages.installmentList[this.tremActive];
}
},
mounted() {
this.init();
},
methods: {
async init() {
const { billNo } = this.$route.query;
this.billNo = billNo;
const [res] = await getAmortizeDetail({ billNo });
if (res) {
this.stages = res || {};
}
},
handleOpenFee() {
this.showDialog = true;
// this.$dialog({
// title: '',
// message:
// '综合服务费:包括依据用户与资金提供方以及相关担保方、保险方的约定,按期向资金提供方缴纳的利息、分期服务费等,以及向担保方缴纳的担保费、保险费等在内的全部资金成本。除还款本金以及该金额外用户无需就贷款事项支付其他任何费用。',
// showCancelButton: false,
// confirmButtonText: '知道了'
// });
},
handleToContract() {
window.location.href = this.selectStages?.contractList[0].url;
},
handleStages() {
if (!this.isAgree) {
this.$toast('请仔细阅读并同意相关协议');
return;
}
this.isPop = true;
},
async handleStagesSure() {
const [res] = await getAmortize({
billNo: this.billNo,
term: this.selectStages.term,
signContract: true
});
if (res) {
this.isPop = false;
this.$router.replace({
name: 'stagesSuccess',
params: {
...res
}
});
// this.$dialog({
// message: this.stages.text,
// showCancelButton: false,
// confirmButtonText: '知道了',
// onConfirm: () => {
// }
// });
}
}
}
};
</script>
<style lang="less" src="./index.less" scoped></style>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<template>
<div class="footer">
<div class="footer-wrapper">
<cr-checkbox v-model="checked" shape="round" @change="checkboxAllChange">全选</cr-checkbox>
<cr-button type="primary" plain hairline shape="circle" @click="deleteCheckboxEvent">
删除
</cr-button>
</div>
</div>
</template>
<script>
const UPDATE_ALL = 'update:checkbox-all';
const CHECK_ALL_EVENT = 'change';
const DELETE_EVENT = 'delete';
export default {
props: {
checkboxAll: {
type: Boolean,
default: false
}
},
data() {
return {
checked: false
};
},
watch: {
checkboxAll(val) {
this.checked = val;
}
},
methods: {
checkboxAllChange(value) {
console.log('=============>checkboxAllChange', value);
this.$emit(UPDATE_ALL, value);
this.$emit(CHECK_ALL_EVENT, value);
// if (value) {
// this.$parent.selectAll();
// } else {
// this.$parent.selectInverse();
// }
},
deleteCheckboxEvent() {
this.$emit(DELETE_EVENT);
}
}
};
</script>
<style lang="less" scoped>
.footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: @white;
box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.06);
backdrop-filter: blur(11px);
z-index: 2;
/deep/ .cr-checkbox__label {
color: @gray-4;
}
&-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
.cr-button {
width: 130px;
/deep/ .cr-button__text {
font-weight: bold;
}
}
}
}
</style>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
.border-bottom(@b: 0, @t: inherit, @bColor: @grey-border) {
position: relative;
&::after {
content: '';
position: absolute;
width: 100%;
height: 1px;
background-color: @bColor;
transform: scaleY(0.5);
left: 0;
bottom: @b;
top: @t;
}
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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