bm-bmt/pages/assets/balance-center.vue

903 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="asset-page balance-page" @click="closePanels">
<asset-page-shell title="余额" />
<view class="asset-scroll balance-scroll">
<view class="balance-header">
<view class="balance-header__card">
<image
class="balance-header__bg"
src="https://imgs.agrimedia.cn/shop/header-yongjin.png"
mode="aspectFill"
></image>
<view class="balance-header__content">
<view class="balance-header__summary">
<view class="balance-header__summary-item">
<text class="balance-header__label">余额</text>
<text class="balance-header__value asset-number-font">{{
summary.balance
}}</text>
</view>
<view class="balance-header__summary-item">
<text class="balance-header__label">冻结佣金</text>
<text class="balance-header__value asset-number-font">{{
summary.frozen
}}</text>
</view>
</view>
<image
class="balance-header__icon"
src="https://imgs.agrimedia.cn/shop/yongjin-icon.png"
mode="aspectFit"
></image>
</view>
</view>
</view>
<view class="balance-toolbar">
<view class="balance-toolbar__row">
<view class="balance-toolbar__item" @click.stop="openDatePopup">
<text class="balance-toolbar__text">{{ currentDateLabel }}</text>
<image
class="balance-toolbar__icon"
src="https://imgs.agrimedia.cn/shop/select-icon.png"
mode="widthFix"
></image>
</view>
<view class="balance-toolbar__item" @click.stop="openStatusPopup">
<text class="balance-toolbar__text">{{
statusOptions[pmIndex].label
}}</text>
<image
class="balance-toolbar__icon"
src="https://imgs.agrimedia.cn/shop/select-icon.png"
mode="widthFix"
></image>
</view>
</view>
</view>
<view class="balance-list">
<view v-if="sections.length" class="balance-records">
<view
v-for="section in sections"
:key="section.key"
class="balance-group"
>
<text class="balance-group__date">{{ section.label }}</text>
<view class="balance-group__items">
<view
v-for="item in section.items"
:key="item.id"
class="balance-item"
>
<view class="balance-item__head">
<view class="balance-item__title-row">
<text class="balance-item__title">{{ item.title }}</text>
<text
class="balance-item__amount asset-number-font"
:class="
item.amountTone === 'income'
? 'balance-item__amount--income'
: 'balance-item__amount--expense'
"
>
{{ item.amount }}
</text>
</view>
<view
v-for="row in item.detailRows"
:key="row.label + row.value"
class="balance-item__line"
>
<text class="balance-item__line-label">{{ row.label }}</text>
<text class="balance-item__line-value asset-number-font">{{
row.value
}}</text>
</view>
<view v-if="item.merchantName" class="balance-item__line">
<text class="balance-item__line-label"></text>
<text class="balance-item__line-value">{{
item.merchantName
}}</text>
</view>
<text
v-if="item.note"
class="balance-item__note"
:class="{
'balance-item__note--refund': item.noteTone === 'expense',
'balance-item__note--danger': item.noteTone === 'danger',
}"
>
{{ item.note }}
</text>
</view>
</view>
</view>
</view>
<view class="balance-loading">
<text class="balance-loading__text asset-number-font">{{
loadMoreText
}}</text>
</view>
</view>
<view v-else class="balance-empty">
<image
class="balance-empty__image"
src="https://imgs.agrimedia.cn/shop/no-datas.png"
mode="aspectFit"
></image>
<text class="balance-empty__text">暂无数据</text>
</view>
</view>
</view>
<view
v-if="datePopupVisible"
class="sheet-mask"
@click="datePopupVisible = false"
></view>
<view
v-if="datePopupVisible"
class="sheet-panel date-sheet"
@click.stop
>
<view class="date-sheet__title">
<text>交易日期</text>
<text class="date-sheet__close" @click="datePopupVisible = false">×</text>
</view>
<view class="date-sheet__block">
<text class="date-sheet__block-title">选择时间</text>
<view class="date-sheet__preset-row">
<view
v-for="(item, index) in timeRanges"
:key="item.key"
class="date-sheet__preset"
:class="{ 'is-active': clickIndex === index }"
@click="clickTime(index)"
>
{{ item.label }}
</view>
</view>
<view class="date-sheet__custom" @click="clickTime(4)">
<view
class="date-sheet__custom-tag"
:class="{ 'is-active': clickIndex === 4 }"
>
自定义时间
</view>
<text class="date-sheet__custom-desc">
最长可查询时间跨度一年的交易
</text>
</view>
</view>
<view v-if="clickIndex === 4" class="date-sheet__range">
<picker
class="date-sheet__picker"
mode="date"
:value="startValue"
@change="handleStartDateChange"
>
<view class="date-sheet__picker-inner">{{ startValue }}</view>
</picker>
<text class="date-sheet__range-separator">至</text>
<picker
class="date-sheet__picker"
mode="date"
:value="endValue"
@change="handleEndDateChange"
>
<view class="date-sheet__picker-inner">{{ endValue }}</view>
</picker>
</view>
<view class="date-sheet__confirm" @click="confirmDateFilter">确定</view>
</view>
<view
v-if="statusPopupVisible"
class="sheet-mask"
@click="statusPopupVisible = false"
></view>
<view
v-if="statusPopupVisible"
class="sheet-panel status-sheet"
@click.stop
>
<view
v-for="(item, index) in statusOptions"
:key="item.value + item.label"
class="status-sheet__item"
:class="{ 'is-active': pmIndex === index }"
@click="confirmStatus(index)"
>
{{ item.label }}
</view>
</view>
</view>
</template>
<script>
import AssetPageShell from "../../components/asset-page-shell.vue";
import { fetchBalanceCenterPage } from "../../api/assets";
function padDateUnit(value) {
return value < 10 ? "0" + value : String(value);
}
function formatDate(date) {
return [
date.getFullYear(),
padDateUnit(date.getMonth() + 1),
padDateUnit(date.getDate()),
].join("-");
}
function createTodayDate() {
const date = new Date();
date.setHours(0, 0, 0, 0);
return date;
}
export default {
components: {
AssetPageShell,
},
data() {
const today = formatDate(createTodayDate());
return {
summary: {
balance: "0",
totalExtract: "0",
frozen: "0",
},
form: {
pm: "",
start_time: "",
end_time: "",
},
statusOptions: [
{ label: "全部", value: "" },
{ label: "佣金获得", value: "1" },
{ label: "佣金支出", value: "0" },
],
timeRanges: [
{ key: "today", label: "今天" },
{ key: "yesterday", label: "昨天" },
{ key: "week", label: "近7天" },
{ key: "month", label: "近30天" },
],
pmIndex: 0,
clickIndex: "",
startValue: today,
endValue: today,
datePopupVisible: false,
statusPopupVisible: false,
sections: [],
page: 1,
pageSize: 10,
hasMore: false,
loadingMore: false,
};
},
computed: {
currentDateLabel() {
if (this.clickIndex === "" || this.clickIndex === 4) {
return this.startValue + "~" + this.endValue;
}
return this.timeRanges[this.clickIndex]
? this.timeRanges[this.clickIndex].label
: this.startValue + "~" + this.endValue;
},
loadMoreText() {
if (this.loadingMore) {
return "加载中...";
}
if (this.hasMore) {
return "加载更多";
}
return "暂无更多";
},
},
onLoad() {
this.loadPage(true, 1);
},
onReachBottom() {
this.loadMore();
},
methods: {
closePanels() {
this.datePopupVisible = false;
this.statusPopupVisible = false;
},
openDatePopup() {
this.statusPopupVisible = false;
this.datePopupVisible = true;
},
openStatusPopup() {
this.datePopupVisible = false;
this.statusPopupVisible = true;
},
getRangeDates(index) {
const endDate = createTodayDate();
const startDate = new Date(endDate.getTime());
if (index === 0) {
return {
start: formatDate(startDate),
end: formatDate(endDate),
};
}
if (index === 1) {
startDate.setDate(startDate.getDate() - 1);
return {
start: formatDate(startDate),
end: formatDate(startDate),
};
}
startDate.setDate(startDate.getDate() - (index === 2 ? 6 : 29));
return {
start: formatDate(startDate),
end: formatDate(endDate),
};
},
clickTime(index) {
this.clickIndex = index;
if (index === 4) {
this.form.start_time = this.startValue;
this.form.end_time = this.endValue;
return;
}
const range = this.getRangeDates(index);
this.form.start_time = range.start;
this.form.end_time = range.end;
},
handleStartDateChange(event) {
const value = event && event.detail ? event.detail.value : "";
this.startValue = value || this.startValue;
this.form.start_time = this.startValue;
},
handleEndDateChange(event) {
const value = event && event.detail ? event.detail.value : "";
this.endValue = value || this.endValue;
this.form.end_time = this.endValue;
},
confirmDateFilter() {
if (this.clickIndex === 4) {
this.form.start_time = this.startValue;
this.form.end_time = this.endValue;
}
this.datePopupVisible = false;
this.reloadList();
},
confirmStatus(index) {
const target = this.statusOptions[index];
this.pmIndex = index;
this.form.pm = target ? target.value : "";
this.statusPopupVisible = false;
this.reloadList();
},
reloadList() {
this.sections = [];
this.page = 1;
this.hasMore = false;
this.loadingMore = false;
this.loadPage(true, 1);
},
mergeSections(currentSections, nextSections) {
const sectionMap = {};
const mergedSections = [];
const appendSection = (section) => {
if (!section || !section.key) {
return;
}
if (!sectionMap[section.key]) {
sectionMap[section.key] = {
key: section.key,
label: section.label,
items: [],
};
mergedSections.push(sectionMap[section.key]);
}
const targetSection = sectionMap[section.key];
const itemMap = {};
targetSection.items.forEach((item) => {
itemMap[item.id] = true;
});
(section.items || []).forEach((item) => {
if (!item || itemMap[item.id]) {
return;
}
itemMap[item.id] = true;
targetSection.items.push(item);
});
};
(currentSections || []).forEach(appendSection);
(nextSections || []).forEach(appendSection);
return mergedSections;
},
async loadPage(showLoading, targetPage) {
const page = Number(targetPage || 1);
const isLoadMore = page > 1;
if (isLoadMore && (this.loadingMore || !this.hasMore)) {
return;
}
if (isLoadMore) {
this.loadingMore = true;
}
try {
const requestOptions =
!isLoadMore && showLoading
? {
showLoading: true,
loadingText: "加载中",
}
: null;
const result = await fetchBalanceCenterPage(
{
pm: this.form.pm,
start_time: this.form.start_time,
end_time: this.form.end_time,
},
{
page: page,
pageSize: this.pageSize,
},
requestOptions,
);
const nextSections = Array.isArray(result.sections) ? result.sections : [];
this.summary = Object.assign({}, this.summary, result.summary || {});
this.sections = isLoadMore
? this.mergeSections(this.sections, nextSections)
: nextSections;
const pagination = result.pagination || {};
this.page = Number(pagination.page || page || 1);
this.hasMore = Boolean(pagination.hasMore);
} catch (error) {
uni.showToast({
title: error.message || (isLoadMore ? "加载更多失败" : "页面加载失败"),
icon: "none",
});
} finally {
if (isLoadMore) {
this.loadingMore = false;
}
}
},
loadMore() {
if (this.loadingMore || !this.hasMore) {
return;
}
this.loadPage(false, this.page + 1);
},
},
};
</script>
<style lang="scss" scoped>
@import "../../styles/tokens.scss";
@import "../../styles/common.scss";
.balance-page {
min-height: 100vh;
background: #f5f5f5;
}
.balance-scroll {
padding: 16rpx 12rpx calc(env(safe-area-inset-bottom) + 36rpx);
}
.balance-header {
margin-top: 4rpx;
}
.balance-header__card {
position: relative;
width: 100%;
height: 187rpx;
overflow: hidden;
border-radius: 24rpx;
}
.balance-header__bg {
width: 100%;
height: 187rpx;
}
.balance-header__content {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 28rpx;
}
.balance-header__summary {
display: flex;
align-items: center;
gap: 56rpx;
}
.balance-header__summary-item {
min-width: 0;
}
.balance-header__label {
display: block;
font-weight: 600;
font-size: 32rpx;
color: #ffffff;
text-align: center;
}
.balance-header__value {
display: block;
margin-top: 12rpx;
font-size: 42rpx;
font-weight: 800;
color: #ffffff;
line-height: 1;
}
.balance-header__icon {
width: 176rpx;
height: 160rpx;
flex-shrink: 0;
}
.balance-toolbar {
padding: 20rpx 10rpx 12rpx;
}
.balance-toolbar__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.balance-toolbar__item {
display: flex;
align-items: center;
min-width: 0;
}
.balance-toolbar__text {
font-size: 24rpx;
color: #000;
}
.balance-toolbar__icon {
width: 30rpx;
height: 30rpx;
margin-left: 10rpx;
flex-shrink: 0;
}
.balance-list {
width: 100%;
}
.balance-group {
margin-bottom: 20rpx;
padding: 28rpx 24rpx;
border-radius: 22rpx;
background: #ffffff;
}
.balance-group__date {
display: block;
padding: 0;
font-weight: 500;
font-size: 30rpx;
color: #666666;
line-height: 36rpx;
}
.balance-group__items {
margin-top: 14rpx;
}
.balance-item + .balance-item {
margin-top: 18rpx;
padding-top: 18rpx;
border-top: 1px solid rgba(20, 40, 72, 0.08);
}
.balance-item__title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
width: 100%;
margin-bottom: 16rpx;
}
.balance-item__title {
flex: 1;
min-width: 0;
font-weight: 500;
font-size: 30rpx;
color: #000000;
line-height: 1.45;
}
.balance-item__amount {
flex-shrink: 0;
font-weight: 800;
font-size: 36rpx;
line-height: 1;
}
.balance-item__amount--income {
color: #fc2838;
}
.balance-item__amount--expense {
color: #07c261;
}
.balance-item__line {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 12rpx;
font-weight: 400;
font-size: 26rpx;
color: #666666;
line-height: 36rpx;
}
.balance-item__line-label {
min-width: 120rpx;
}
.balance-item__line-value {
flex: 1;
min-width: 0;
text-align: right;
word-break: break-all;
}
.balance-item__note {
display: block;
margin-top: 6rpx;
font-size: 26rpx;
line-height: 36rpx;
}
.balance-item__note--refund {
color: #07c261;
}
.balance-item__note--danger {
color: #ff7a45;
}
.balance-loading {
display: flex;
justify-content: center;
padding: 8rpx 0 0;
}
.balance-loading__text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.72);
}
.balance-empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 180rpx 0 60rpx;
}
.balance-empty__image {
width: 328rpx;
height: 338rpx;
}
.balance-empty__text {
margin-top: 36rpx;
font-weight: 400;
font-size: 32rpx;
color: #999999;
}
.sheet-mask {
position: fixed;
inset: 0;
z-index: 80;
background: rgba(0, 0, 0, 0.42);
}
.sheet-panel {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 81;
background: #ffffff;
border-radius: 20rpx 20rpx 0 0;
}
.date-sheet {
padding: 20rpx 20rpx calc(env(safe-area-inset-bottom) + 30rpx);
}
.date-sheet__title {
position: relative;
text-align: center;
font-size: 32rpx;
font-weight: 500;
color: #000000;
}
.date-sheet__close {
position: absolute;
top: 4rpx;
right: 20rpx;
font-size: 42rpx;
line-height: 1;
}
.date-sheet__block {
margin-top: 26rpx;
}
.date-sheet__block-title {
display: block;
margin-bottom: 20rpx;
font-size: 28rpx;
color: #000000;
}
.date-sheet__preset-row {
display: flex;
justify-content: space-between;
gap: 12rpx;
}
.date-sheet__preset {
flex: 1;
height: 50rpx;
line-height: 50rpx;
text-align: center;
border-radius: 10rpx;
border: 2rpx solid #ed151b;
font-size: 22rpx;
color: #ed151b;
}
.date-sheet__preset.is-active {
background: #ed151b;
color: #ffffff;
}
.date-sheet__custom {
display: flex;
align-items: center;
margin-top: 20rpx;
margin-bottom: 30rpx;
color: #ed151b;
}
.date-sheet__custom-tag {
height: 50rpx;
padding: 0 20rpx;
line-height: 50rpx;
text-align: center;
border-radius: 10rpx;
border: 2rpx solid #ed151b;
font-size: 22rpx;
}
.date-sheet__custom-tag.is-active {
background: #ed151b;
color: #ffffff;
}
.date-sheet__custom-desc {
margin-left: 20rpx;
font-size: 22rpx;
color: #666666;
}
.date-sheet__range {
display: flex;
align-items: center;
justify-content: space-between;
}
.date-sheet__picker {
width: 260rpx;
height: 60rpx;
border: 2rpx solid #ed151b;
border-radius: 10rpx;
overflow: hidden;
}
.date-sheet__picker-inner {
width: 100%;
height: 60rpx;
line-height: 60rpx;
text-align: center;
font-size: 24rpx;
color: #000000;
}
.date-sheet__range-separator {
font-size: 24rpx;
color: #000000;
}
.date-sheet__confirm {
width: 100%;
height: 70rpx;
margin-top: 30rpx;
margin-bottom: 30rpx;
border-radius: 10rpx;
background: #ed151b;
line-height: 70rpx;
text-align: center;
font-size: 28rpx;
color: #ffffff;
}
.status-sheet {
padding: 16rpx 0 calc(env(safe-area-inset-bottom) + 20rpx);
}
.status-sheet__item {
height: 88rpx;
line-height: 88rpx;
text-align: center;
font-size: 28rpx;
color: #333333;
}
.status-sheet__item.is-active {
color: #ed151b;
font-weight: 600;
}
</style>