bm-bmt/pages/assets/bmt-exchange.vue

967 lines
23 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 asset-theme bmt-page">
<asset-page-shell title="BMT兑换" />
<view class="asset-scroll bmt-scroll">
<view class="hero-card">
<view class="hero-card__content">
<view class="hero-card__coin-row">
<view class="hero-card__coin">
<image
class="hero-card__coin-image"
:src="pageIcon('bmt')"
mode="aspectFit"
></image>
</view>
<text class="hero-card__coin-text">BMT</text>
</view>
<text class="hero-card__label">可用BMT:</text>
<text class="hero-card__value asset-number-font">{{ displayBmt }}</text>
</view>
</view>
<view class="panel-card info-card">
<view class="info-row">
<view class="info-row__left">
<image
class="info-row__icon"
:src="pageIcon('voucher')"
mode="aspectFit"
></image>
<text class="info-row__label">我的算力</text>
</view>
<text class="info-row__value asset-number-font">{{ displayVoucher }}</text>
</view>
<view class="info-row">
<view class="info-row__left">
<image
class="info-row__icon"
:src="pageIcon('coupon')"
mode="aspectFit"
></image>
<text class="info-row__label">可用积分</text>
</view>
<text class="info-row__value asset-number-font">{{ displayCoupon }}</text>
</view>
<view class="info-row">
<view class="info-row__left">
<image
class="info-row__icon"
:src="pageIcon('bmtr')"
mode="aspectFit"
></image>
<text class="info-row__label">BMTR数量</text>
</view>
<text class="info-row__value asset-number-font">{{ displayBmtr }}</text>
</view>
<view class="info-row info-row--last">
<view class="info-row__left">
<image
class="info-row__icon"
src="https://imgs.agrimedia.cn/bm-bmt/b.png"
mode="aspectFit"
></image>
<text class="info-row__label">BMT实时价格</text>
</view>
<view class="info-row__price">
<text class="info-row__value asset-number-font">{{ displayPrice }}</text>
<text class="info-row__unit">BMT/CNY</text>
</view>
</view>
<view class="info-card__estimate">
<text>全部兑换预估可得</text>
<text class="info-card__estimate-value asset-number-font">{{ allEstimateBmt }}</text>
<text>BMT</text>
</view>
</view>
<view class="panel-card form-card">
<view class="form-card__title">
<image
class="form-card__title-icon"
src="https://imgs.agrimedia.cn/bm-bmt/b.png"
mode="aspectFit"
></image>
<text class="form-card__title-text">BMT兑换</text>
</view>
<view class="scheme-tabs">
<view
class="scheme-tab"
:class="{ 'scheme-tab--active': redeemType === 0 }"
@click="selectRedeemType(0)"
>
<text>{{ loadingRedeemType === 0 ? "加载中..." : "方案1" }}</text>
</view>
<view
class="scheme-tab"
:class="{ 'scheme-tab--active': redeemType === 1 }"
@click="selectRedeemType(1)"
>
<text>{{ loadingRedeemType === 1 ? "加载中..." : "方案2" }}</text>
</view>
</view>
<view class="form-input">
<decimal-input
v-model="form.points"
class="form-input__field asset-number-font"
:digit="0"
placeholder="请输入积分数量"
placeholder-style="color: rgba(177, 187, 214, 0.42);"
/>
<text class="form-input__suffix">可用积分</text>
</view>
<view class="preview-card">
<view class="preview-card__row">
<view class="preview-card__left">
<text class="preview-card__label">预估兑换BMT</text>
</view>
<text class="preview-card__value asset-number-font">{{ estimateBmt }}</text>
</view>
<view class="preview-card__row">
<view class="preview-card__left">
<text class="preview-card__label" style="color: #fff"
>消耗算力</text
>
</view>
<text class="preview-card__value preview-card__value--light asset-number-font">{{
powerCost
}}</text>
</view>
<view class="preview-card__row">
<view class="preview-card__left">
<text class="preview-card__label preview-card__label--light"
>消耗BMTR</text
>
</view>
<text class="preview-card__value preview-card__value--light asset-number-font">{{
bmtrCost
}}</text>
</view>
</view>
<view class="tips-block">
<text class="tips-block__title">兑换说明:</text>
<view
v-for="(tip, index) in normalizedTips"
:key="tip"
class="tips-block__item"
>
<text class="tips-block__index asset-number-font">{{ index + 1 }}.</text>
<text class="tips-block__text asset-number-font">{{ tip }}</text>
</view>
</view>
</view>
<view class="action-wrap">
<view
class="action-button"
@touchend.stop.prevent="openConfirm"
@tap.stop="openConfirm"
@click.stop="openConfirm"
>确认兑换</view
>
<view class="action-link" @click="openLedger">兑换记录</view>
</view>
</view>
<asset-confirm-popup
:visible="confirmVisible"
title="兑换提示"
:message="
'确认使用 ' + formatAmount(pointsValue, 0) + ' 积分兑换 BMT 吗?'
"
@cancel="confirmVisible = false"
@confirm="submit"
>
<view class="popup-line">
<text class="meta-pair__label">预估兑换BMT:</text>
<text class="meta-pair__value asset-number-font">{{ estimateBmt }}</text>
</view>
<view class="popup-line">
<text class="meta-pair__label">消耗可用积分:</text>
<text class="meta-pair__value asset-number-font">{{ formatAmount(pointsValue, 0) }}</text>
</view>
<view class="popup-line">
<text class="meta-pair__label">消耗算力:</text>
<text class="meta-pair__value asset-number-font">{{ powerCost }}</text>
</view>
<view class="popup-line">
<text class="meta-pair__label">消耗BMTR:</text>
<text class="meta-pair__value asset-number-font">{{ bmtrCost }}</text>
</view>
</asset-confirm-popup>
</view>
</template>
<script>
import AssetConfirmPopup from "../../components/asset-confirm-popup.vue";
import AssetPageShell from "../../components/asset-page-shell.vue";
import DecimalInput from "../../components/DecimalInput/DecimalInput.vue";
import {
fetchBmtExchangeDetail,
fetchBmtRedeemConfig,
fetchBmtRedeemPowerRateNew,
submitAssetBmtExchange,
} from "../../api/assets";
const REDEEM_POINT_STEP = 100;
export default {
components: {
AssetConfirmPopup,
AssetPageShell,
DecimalInput,
},
data() {
return {
detail: {
ticker: {},
balances: {},
exchangeConfig: {},
schemeOneConfig: {},
tips: [],
},
hasShown: false,
form: {
points: "",
},
confirmVisible: false,
submitting: false,
redeemType: 0,
schemeOneConfigLoaded: false,
loadingRedeemType: null,
};
},
onLoad() {
this.loadPage(true);
},
onShow() {
if (this.hasShown) {
this.loadPage();
return;
}
this.hasShown = true;
},
computed: {
pointsValue() {
return Number(this.form.points || 0);
},
configPointNumber() {
return Number(this.detail.exchangeConfig.point || 0);
},
configPowerNumber() {
return Number(this.detail.exchangeConfig.cPower || 0);
},
configBmtrNumber() {
return Number(this.detail.exchangeConfig.bmtr || 0);
},
redeemNumNumber() {
return Number(this.detail.exchangeConfig.redeemNum || 0);
},
schemeOnePowerNumber() {
return Number(this.detail.schemeOneConfig.power || 0);
},
schemeOneBmtrNumber() {
return Number(this.detail.schemeOneConfig.bmtr || 0);
},
activePowerCostPerPoint() {
if (this.redeemType === 0) {
return this.schemeOnePowerNumber;
}
if (!this.configPointNumber) {
return 0;
}
return this.configPowerNumber / this.configPointNumber;
},
activeBmtrCostPerPoint() {
if (this.redeemType === 0) {
return this.schemeOneBmtrNumber;
}
if (!this.configPointNumber) {
return 0;
}
return this.configBmtrNumber / this.configPointNumber;
},
canCalculateBmt() {
return this.redeemType === 0
? this.priceNumber > 0
: this.configPointNumber > 0 && this.redeemNumNumber > 0;
},
estimateBmt() {
if (!this.pointsValue || !this.canCalculateBmt) {
return "0";
}
return this.formatAmount(this.calculateBmt(this.pointsValue), 2);
},
powerCostNumber() {
if (!this.pointsValue || !this.activePowerCostPerPoint) {
return 0;
}
return this.pointsValue * this.activePowerCostPerPoint;
},
powerCost() {
return this.formatTruncatedAmount(this.powerCostNumber, 2);
},
bmtrCostNumber() {
if (!this.pointsValue || !this.activeBmtrCostPerPoint) {
return 0;
}
return this.pointsValue * this.activeBmtrCostPerPoint;
},
bmtrCost() {
return this.formatTruncatedAmount(this.bmtrCostNumber, 2);
},
allEstimateBmt() {
const points = Number(this.detail.balances.points || 0);
if (!this.canCalculateBmt) {
return "0.00";
}
let exchangeablePoints = points;
const power = Number(this.detail.balances.power || 0);
const bmtr = Number(this.detail.balances.bmtr || 0);
if (this.activePowerCostPerPoint > 0) {
exchangeablePoints = Math.min(
exchangeablePoints,
power / this.activePowerCostPerPoint,
);
}
if (this.activeBmtrCostPerPoint > 0) {
exchangeablePoints = Math.min(
exchangeablePoints,
bmtr / this.activeBmtrCostPerPoint,
);
}
const truncatedBmt =
Math.floor(this.calculateBmt(Math.max(0, exchangeablePoints)) * 100) /
100;
return this.formatAmount(truncatedBmt, 2);
},
priceNumber() {
return Number(
this.detail.ticker.close || 0,
);
},
displayBmt() {
return this.detail.balances.bmt;
},
displayVoucher() {
return this.detail.balances.power;
},
displayCoupon() {
return this.detail.balances.points;
},
displayBmtr() {
return this.formatAmount(this.detail.balances.bmtr, 2);
},
displayPrice() {
return this.priceNumber ? this.priceNumber : "0.00";
},
normalizedTips() {
const tips =
this.detail.tips && this.detail.tips.length
? this.detail.tips.slice()
: [
`预估BMT数=输入可用积分数÷${this.configPointNumber}×${this.redeemNumNumber}。`,
"",
`只能兑换${REDEEM_POINT_STEP}的整数倍,小于${REDEEM_POINT_STEP}积分不可兑换;`,
"凌晨0点-凌晨1点积分系统维护不可兑换;",
];
tips[1] =
this.redeemType === 0
? `方案1消耗算力=输入可用积分数×${this.schemeOnePowerNumber},消耗BMTR=输入可用积分数×${this.schemeOneBmtrNumber};`
: `方案2消耗算力=输入可用积分数÷${this.configPointNumber}×${this.configPowerNumber},消耗BMTR=输入可用积分数÷${this.configPointNumber}×${this.configBmtrNumber};`;
tips[0] =
this.redeemType === 0
? "方案1预估BMT数=输入可用积分数÷BMT实时价格。"
: `方案2预估BMT数=(输入可用积分数÷${this.configPointNumber},保留4位小数)×${this.redeemNumNumber},结果保留2位小数。`;
return tips;
},
},
methods: {
calculateBmt(points) {
const pointAmount = Number(points || 0);
if (!pointAmount) {
return 0;
}
if (this.redeemType === 0) {
return this.priceNumber ? pointAmount / this.priceNumber : 0;
}
if (!this.configPointNumber || !this.redeemNumNumber) {
return 0;
}
const convertedPoints = Number(
(pointAmount / this.configPointNumber).toFixed(4),
);
return convertedPoints * this.redeemNumNumber;
},
pageIcon(type) {
const iconMap = {
power: "https://imgs.agrimedia.cn/bm-bmt/s.png",
bmt: "https://imgs.agrimedia.cn/bm-bmt/b-w.png",
transfer: "https://imgs.agrimedia.cn/bm-bmt/z.png",
withdraw: "https://imgs.agrimedia.cn/bm-bmt/t.png",
voucher: "https://imgs.agrimedia.cn/bm-bmt/quan-icon-g%20%281%29.png",
points: "https://imgs.agrimedia.cn/bm-bmt/j.png",
coupon: "https://imgs.agrimedia.cn/bm-bmt/xiaofei-icon-o.png",
bmtr: "https://imgs.agrimedia.cn/webimg/202607171808386371853.svg",
};
return iconMap[type] || "";
},
formatAmount(value, digits) {
const number = Number(value || 0);
if (!Number.isFinite(number)) {
return digits > 0 ? Number(0).toFixed(digits) : "0";
}
if (digits > 0) {
const fixed = number.toFixed(digits);
const parts = fixed.split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join(".");
}
return Math.floor(number)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
},
formatTruncatedAmount(value, digits) {
const number = Number(value || 0);
const precision = Number(digits || 0);
if (!Number.isFinite(number)) {
return precision > 0 ? Number(0).toFixed(precision) : "0";
}
const factor = Math.pow(10, precision);
const truncated = Math.floor(Math.max(0, number) * factor) / factor;
return this.formatAmount(truncated, precision);
},
async loadPage(showLoading) {
try {
this.detail = await fetchBmtExchangeDetail(
showLoading
? {
showLoading: true,
loadingText: "加载中",
}
: null,
);
this.schemeOneConfigLoaded = Boolean(
this.detail.schemeOneConfigLoaded,
);
} catch (error) {
uni.showToast({
title: error.message || "页面加载失败",
icon: "none",
});
}
},
async selectRedeemType(type) {
const nextType = Number(type) === 0 ? 0 : 1;
if (nextType === this.redeemType || this.loadingRedeemType !== null) {
return;
}
this.loadingRedeemType = nextType;
try {
const configResult = await Promise.all([
fetchBmtRedeemConfig({
showLoading: true,
loadingText: "刷新兑换配置",
}),
fetchBmtRedeemPowerRateNew({
showLoading: true,
loadingText: "刷新兑换配置",
}),
]);
this.detail.exchangeConfig = configResult[0];
this.detail.schemeOneConfig = configResult[1];
this.schemeOneConfigLoaded = true;
this.redeemType = nextType;
} catch (error) {
uni.showToast({
title: error.message || "兑换方案配置加载失败",
icon: "none",
});
} finally {
this.loadingRedeemType = null;
}
},
openConfirm() {
if (this.submitting || this.confirmVisible) {
return;
}
this.confirmVisible = true;
},
async submit() {
if (this.submitting) {
return;
}
if (!this.pointsValue) {
uni.showToast({
title: "请输入积分数量",
icon: "none",
});
return;
}
if (!this.configPointNumber) {
uni.showToast({
title: "兑换配置加载失败,请稍后重试",
icon: "none",
});
return;
}
if (this.pointsValue < REDEEM_POINT_STEP) {
uni.showToast({
title: `小于${REDEEM_POINT_STEP}积分不可兑换`,
icon: "none",
});
return;
}
if (this.pointsValue % REDEEM_POINT_STEP !== 0) {
uni.showToast({
title: `只能兑换${REDEEM_POINT_STEP}的整数倍`,
icon: "none",
});
return;
}
if (this.pointsValue > Number(this.detail.balances.points || 0)) {
uni.showToast({
title: "可用积分不足",
icon: "none",
});
return;
}
if (this.powerCostNumber > Number(this.detail.balances.power || 0)) {
uni.showToast({
title: "可用算力不足",
icon: "none",
});
return;
}
if (this.bmtrCostNumber > Number(this.detail.balances.bmtr || 0)) {
uni.showToast({
title: "BMTR数量不足",
icon: "none",
});
return;
}
this.submitting = true;
try {
await submitAssetBmtExchange({
amount: this.pointsValue,
type: this.redeemType,
}, {
showLoading: true,
loadingText: "兑换中",
});
this.confirmVisible = false;
this.form.points = "";
uni.showToast({
title: "兑换成功",
icon: "none",
});
this.loadPage();
} catch (error) {
uni.showToast({
title: error.message || "兑换失败",
icon: "none",
});
} finally {
this.submitting = false;
}
},
openLedger() {
uni.navigateTo({
url: "/pages/assets/ledger?type=bmt",
});
},
},
};
</script>
<style lang="scss" scoped>
@import "../../styles/tokens.scss";
@import "../../styles/common.scss";
.bmt-page {
min-height: 100vh;
background: #191e32;
}
.bmt-scroll {
padding: 8rpx 20rpx calc(env(safe-area-inset-bottom) + 36rpx);
}
.panel-card,
.hero-card {
border-radius: 12rpx;
background: #20263e;
}
.hero-card {
position: relative;
min-height: 240rpx;
padding: 24rpx 28rpx;
overflow: hidden;
background: #20263e url("https://imgs.agrimedia.cn/bm-bmt/bmt2-header.png")
no-repeat center top / 100% 240rpx;
}
.hero-card__content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
}
.hero-card__coin-row {
display: flex;
align-items: center;
}
.hero-card__coin {
display: flex;
align-items: center;
justify-content: center;
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: linear-gradient(135deg, #18c5ff 0%, #1496ff 100%);
}
.hero-card__coin-image {
width: 30rpx;
height: 30rpx;
}
.hero-card__coin-text {
margin-left: 16rpx;
font-size: 32rpx;
font-weight: 500;
color: #ffffff;
}
.hero-card__label {
margin-top: 34rpx;
font-size: 28rpx;
color: #9ba7ce;
}
.hero-card__value {
margin-top: 8rpx;
font-size: 48rpx;
font-weight: 800;
color: #ffffff;
line-height: 1.1;
}
.panel-card {
margin-top: 18rpx;
padding: 0 26rpx;
}
.info-card {
padding-top: 2rpx;
padding-bottom: 14rpx;
}
.info-row {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 96rpx;
border-bottom: 1rpx solid #4e5a82;
}
.info-row--last {
border-bottom: 1rpx solid #4e5a82;
}
.info-row__left {
display: flex;
align-items: center;
min-width: 0;
}
.info-row__icon {
width: 34rpx;
height: 34rpx;
flex-shrink: 0;
}
.info-row__label {
margin-left: 20rpx;
font-size: 28rpx;
font-weight: 400;
color: #eef2ff;
}
.info-row__value {
font-size: 32rpx;
font-weight: 700;
color: #ffffff;
}
.info-row__price {
display: flex;
align-items: baseline;
}
.info-row__unit {
margin-left: 10rpx;
font-size: 22rpx;
color: rgba(184, 193, 218, 0.88);
}
.info-card__estimate {
display: flex;
justify-content: flex-end;
align-items: baseline;
padding-top: 18rpx;
font-size: 28rpx;
color: #1ad296;
}
.info-card__estimate-value {
margin: 0 10rpx;
font-size: 32rpx;
font-weight: 800;
color: #2fe2a4;
}
.form-card {
padding-top: 18rpx;
padding-bottom: 20rpx;
}
.form-card__title {
display: flex;
align-items: center;
}
.form-card__title-icon {
width: 34rpx;
height: 34rpx;
flex-shrink: 0;
}
.form-card__title-text {
margin-left: 14rpx;
font-size: 28rpx;
font-weight: 700;
color: #ffffff;
}
.scheme-tabs {
display: flex;
margin-top: 20rpx;
padding: 6rpx;
border-radius: 10rpx;
background: #191e32;
}
.scheme-tab {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
min-width: 0;
height: 64rpx;
border-radius: 8rpx;
font-size: 26rpx;
color: rgba(177, 187, 214, 0.76);
}
.scheme-tab--active {
background: #20b6f5;
font-weight: 700;
color: #ffffff;
}
.form-input {
display: flex;
align-items: center;
margin-top: 20rpx;
padding: 0 20rpx;
border-radius: 10rpx;
background: #191e32;
}
.form-input__icon {
width: 34rpx;
height: 34rpx;
margin-right: 18rpx;
flex-shrink: 0;
}
.form-input__field {
flex: 1;
height: 98rpx;
font-size: 30rpx;
font-weight: 700;
color: #ffffff;
}
.form-input__suffix {
margin-left: 16rpx;
font-size: 22rpx;
font-weight: 600;
color: #ffffff;
}
.preview-card {
margin-top: 18rpx;
padding: 12rpx 26rpx;
border-radius: 8rpx;
background: rgba(26, 210, 150, 0.1);
}
.preview-card__row {
display: flex;
align-items: center;
justify-content: space-between;
}
.preview-card__row + .preview-card__row {
margin-top: 8rpx;
}
.preview-card__left {
display: flex;
align-items: center;
}
.preview-card__icon {
width: 30rpx;
height: 30rpx;
margin-right: 10rpx;
flex-shrink: 0;
}
.preview-card__label {
font-size: 26rpx;
color: rgba(26, 210, 150, 1);
}
.preview-card__label--light {
color: #ffffff;
}
.preview-card__value {
font-size: 26rpx;
font-weight: 800;
color: #1ad296;
}
.preview-card__value--light {
color: #ffffff;
}
.tips-block {
margin-top: 24rpx;
}
.tips-block__title {
display: block;
font-size: 24rpx;
color: rgba(190, 197, 219, 0.92);
}
.tips-block__item {
display: flex;
margin-top: 10rpx;
}
.tips-block__index,
.tips-block__text {
font-size: 24rpx;
color: #929dbf;
line-height: 44rpx;
color: rgba(166, 175, 204, 0.92);
}
.tips-block__index {
margin-right: 6rpx;
}
.tips-block__text {
flex: 1;
}
.action-wrap {
position: relative;
z-index: 30;
padding: 36rpx 22rpx 0;
}
.action-button {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 88rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #20b6f5 0%, #1ca5e3 100%);
box-shadow: 0 16rpx 30rpx rgba(20, 119, 214, 0.16);
font-size: 30rpx;
font-weight: 700;
color: #ffffff;
cursor: pointer;
touch-action: manipulation;
}
.action-link {
margin-top: 26rpx;
text-align: center;
font-weight: 500;
font-size: 28rpx;
color: #1fb4ff;
}
.popup-line {
margin-top: 14rpx;
}
</style>