first commit

This commit is contained in:
whitechiina 2026-04-27 16:48:12 +08:00
commit 952d3be4a9
28 changed files with 7682 additions and 0 deletions

BIN
.lanhu-home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

78
App.vue Normal file
View File

@ -0,0 +1,78 @@
<script>
import {
getCurrentWebviewToken,
getCurrentWebviewUrl,
} from "./utils/webview-token";
export default {
onLaunch: function () {
console.log("App Launch");
this.logCurrentWebviewToken("launch");
},
onShow: function () {
console.log("App Show");
this.logCurrentWebviewToken("show");
},
onHide: function () {
console.log("App Hide");
},
methods: {
logCurrentWebviewToken(scene) {
const currentUrl = getCurrentWebviewUrl();
const token = getCurrentWebviewToken();
if (token) {
console.log("[webview-token][" + scene + "]", token);
return;
}
console.log("[webview-token][" + scene + "] token not found", currentUrl);
},
},
};
</script>
<style lang="scss">
@import "./styles/tokens.scss";
html,
body,
#app {
min-height: 100%;
margin: 0;
background: #191e32;
}
uni-page,
uni-page-wrapper,
uni-page-body {
min-height: 100%;
background: #191e32;
}
page {
min-height: 100%;
background: #191e32;
color: $asset-text-main;
font-family: "PingFang SC", "Helvetica Neue", Arial, sans-serif;
}
view,
text,
button,
input,
textarea,
scroll-view {
box-sizing: border-box;
}
.asset-theme {
--asset-accent: #4cc9ff;
--asset-accent-strong: #5a71ff;
--asset-success: #5ad7a1;
--asset-danger: #ff7285;
--asset-text-main: #ffffff;
--asset-text-muted: rgba(255, 255, 255, 0.78);
--asset-text-dark: #112446;
}
</style>

850
api/assets.js Normal file
View File

@ -0,0 +1,850 @@
import serviceConfig from "../config/service";
import request from "../utils/request";
function createError(message, raw) {
return {
message: message || "接口请求失败",
raw: raw,
};
}
function toNumber(value) {
const number = Number(value || 0);
return Number.isFinite(number) ? number : 0;
}
function toFixedNumber(value, digits) {
return toNumber(value).toFixed(digits);
}
function formatHomeNumber(value, digits) {
const number = toNumber(value);
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.round(number)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
function isSuccessPayload(payload) {
if (!payload || typeof payload !== "object") {
return false;
}
const statusCode =
payload.code !== undefined ? Number(payload.code) : Number(payload.status);
return statusCode === 200;
}
function unwrapPayload(payload, fallbackMessage) {
if (isSuccessPayload(payload)) {
return payload.data;
}
if (payload && typeof payload === "object") {
throw createError(
payload.msg || payload.message || fallbackMessage || "接口请求失败",
payload,
);
}
throw createError(fallbackMessage || "接口返回异常", payload);
}
async function fetchPayload(options, fallbackMessage) {
try {
const payload = await request(options);
return unwrapPayload(payload, fallbackMessage);
} catch (error) {
if (error && error.message) {
throw error;
}
throw createError(fallbackMessage || "接口请求失败", error);
}
}
function createRequestOptions(baseOptions, requestOptions) {
return Object.assign({}, baseOptions, requestOptions || {});
}
function normalizeTicker(data) {
const close = toNumber(data && (data.close || data.cnyPrice));
const lastDayClose = toNumber(data && data.lastDayClose);
const rawChange = data && data.change;
let change = typeof rawChange === "string" ? rawChange : "";
if (!change) {
if (close && lastDayClose) {
const percent = ((close - lastDayClose) / lastDayClose) * 100;
const prefix = percent >= 0 ? "+" : "";
change = prefix + percent.toFixed(2) + "%";
} else {
change = "0.00%";
}
}
return {
symbol: (data && data.symbol) || "BMT/CNY",
close: close,
cnyPrice:
(data && data.cnyPrice) || (close ? close.toFixed(2) : "0.00"),
lastDayClose: lastDayClose,
change: change,
};
}
function normalizeBalances(data) {
return {
points: toNumber(data && data.point),
power: toNumber(data && data.c_power),
bmt: toNumber(data && data.bmt_num),
withdrawableBmt: toNumber(data && data.bmt_num),
voucher: toNumber(data && data.coin),
coupon: toNumber(data && data.diamond_balance),
};
}
function buildHomeOverview(balanceData, tickerData) {
const balances = normalizeBalances(balanceData);
const ticker = normalizeTicker(tickerData);
return {
title: "数字资产",
ticker: ticker,
topStats: [
{
key: "wallet-bmt",
title: "可提取BMT",
value: toFixedNumber(balances.withdrawableBmt, 2),
unit: "BMT",
accent: "gold",
},
{
key: "ticker",
title: "BMT实时价格",
value: toFixedNumber(ticker.close || ticker.cnyPrice, 3),
unit: "CNY/BMT",
accent: "green",
},
],
quickAssets: [
{
key: "points",
title: "积分",
value: formatHomeNumber(balances.points, 0),
accent: "gold",
},
{
key: "voucher",
title: "抵用券",
value: formatHomeNumber(balances.voucher, 2),
accent: "rose",
},
{
key: "coupon",
title: "消费券",
value: formatHomeNumber(balances.coupon, 0),
accent: "teal",
},
{
key: "power",
title: "算力",
value: formatHomeNumber(balances.power, 0),
accent: "violet",
},
],
features: [
{
key: "bmt-exchange",
title: "BMT兑换",
desc: "积分与算力兑换 BMT",
accent: "mint",
},
{
key: "power-exchange",
title: "算力兑换",
desc: "抵用券与消费券兑换算力",
accent: "amber",
},
{
key: "transfer",
title: "转赠中心",
desc: "积分或算力转赠好友",
accent: "indigo",
},
{
key: "withdraw",
title: "BMT提取",
desc: "钱包中的 BMT 可提取到交易所进行交易",
accent: "pink",
},
{
key: "points-convert",
title: "积分转换",
desc: "释放中的积分转换为可用积分",
accent: "pink",
},
],
notice:
"数字资产是您在平台上的虚拟资产请谨慎管理BMT可在交易所中进行交易。",
};
}
function buildTransferTips(feePercent) {
const percentText = toNumber(feePercent) || 10;
return {
points: [
"只能转赠100的整数倍",
"凌晨0点-凌晨01点系统维护不可赠送",
"转赠系统会扣除" + percentText + "%的手续费",
],
power: [
"只能转赠1的整数倍",
"转赠系统会扣除" + percentText + "%的手续费",
],
};
}
function buildWalletList(address) {
const normalizedAddress = String(address || "").trim();
if (!normalizedAddress) {
return [];
}
return [
{
id: "default-wallet",
name: serviceConfig.WALLET_NAME,
address: normalizedAddress,
isDefault: true,
},
];
}
function buildWalletPayload(address) {
return {
wallets: buildWalletList(address),
instructions: [
"点击交易所 App 底部“资产”进入钱包页",
"搜索或输入大写字母 BMT",
"点击“充币 / 充值”进入收款地址页面",
"复制钱包地址后回填到当前页面",
],
};
}
function formatTransferRecordNumber(value) {
const number = toNumber(value);
return formatHomeNumber(number, Number.isInteger(number) ? 0 : 2);
}
function getTransferRecordUnit(item) {
return Number(item && item.type) === 0 ? "算力" : "积分";
}
function getTransferRecordTone(item) {
return Number(item && item.io_type) === 1 ? "success" : "danger";
}
function getTransferRecordTag(item) {
return Number(item && item.io_type) === 1 ? "收" : "赠";
}
function getTransferRecordDirection(item) {
return Number(item && item.io_type) === 1 ? "转入" : "转出";
}
function getTransferRecordTitle(item) {
const unit = getTransferRecordUnit(item);
return Number(item && item.io_type) === 1 ? unit + "获赠" : unit + "转赠";
}
function getTransferRecordSymbol(item) {
return Number(item && item.io_type) === 1 ? "+" : "-";
}
function getTransferRecordAmount(item) {
const numberText = formatTransferRecordNumber(item && item.num);
return getTransferRecordSymbol(item) + numberText;
}
function getTransferRecordFee(item) {
if (
item &&
item.fee !== undefined &&
item.fee !== null &&
String(item.fee).trim() !== ""
) {
return toNumber(item.fee);
}
return (toNumber(item && item.num) * toNumber(item && item.fee_percent)) / 100;
}
function getTransferRecordFeeText(item) {
const percent = toNumber(item && item.fee_percent);
const unit = getTransferRecordUnit(item);
const feeText = formatTransferRecordNumber(getTransferRecordFee(item));
if (percent > 0) {
return (
"手续费 " +
feeText +
" " +
unit +
" (" +
formatTransferRecordNumber(percent) +
"%)"
);
}
return "手续费 " + feeText + " " + unit;
}
function getTransferRecordBalance(item) {
const unit = getTransferRecordUnit(item);
return "结余 " + toFixedNumber(item && item.balance, 2) + " " + unit;
}
function getTransferRecordBalanceLabel(item) {
const unit = getTransferRecordUnit(item);
return "剩余" + unit + "" + formatTransferRecordNumber(item && item.balance);
}
function mapTransferRecords(list) {
return (Array.isArray(list) ? list : []).map(function (item) {
return {
id: item.order_sn || item.id || String(Math.random()),
title: item.title || getTransferRecordTitle(item),
subtitle: item.order_sn ? "单号 " + item.order_sn : getTransferRecordDirection(item),
time: item.add_time || "",
amount: getTransferRecordAmount(item),
balance: getTransferRecordBalance(item),
balanceLabel: getTransferRecordBalanceLabel(item),
assetLabel: getTransferRecordUnit(item),
feeText: getTransferRecordFeeText(item),
directionLabel: getTransferRecordDirection(item),
actionSymbol: getTransferRecordSymbol(item),
orderSn: item.order_sn || "",
tag: getTransferRecordTag(item),
tone: getTransferRecordTone(item),
cardTone: getTransferRecordTone(item),
};
});
}
function mapPointsConvertRecords(list) {
return (Array.isArray(list) ? list : []).map(function (item) {
const numberText = toFixedNumber(item.number, 2);
const transferCoinText = toFixedNumber(item.transfer_coin_num, 0);
const releaseTotal =
item &&
item.userBillRelease &&
item.userBillRelease.total !== undefined &&
item.userBillRelease.total !== null
? String(item.userBillRelease.total)
: "";
return {
id: String(item.id || ""),
title: item.title || "积分记录",
subtitle: "可转数量 " + transferCoinText,
time: item.add_time || "",
amount: "+" + numberText,
balance: releaseTotal ? "释放总量 " + releaseTotal : "",
tag: "积",
tone: "success",
};
});
}
function buildDefaultLedger(type, balances) {
const map = {
power: {
title: "兑换记录",
subtitle: "算力兑换记录",
},
bmt: {
title: "兑换记录",
subtitle: "BMT兑换记录",
},
withdraw: {
title: "提取记录",
subtitle: "BMT提取流水",
},
coupon: {
title: "消费券",
subtitle: "当前消费券 " + toFixedNumber(balances.coupon, 2),
},
voucher: {
title: "抵用券记录",
subtitle: "可用抵用券 " + toFixedNumber(balances.voucher, 2),
},
};
return {
type: type,
title: map[type].title,
subtitle: map[type].subtitle,
records: [],
};
}
function normalizeTransferTarget(data, fallbackId) {
if (data && typeof data === "object" && !Array.isArray(data)) {
const id = String(data.uid || data.id || fallbackId || "").trim();
if (!id) {
throw createError("未查询到好友", data);
}
return {
id: id,
nickname:
data.nickname || data.nick_name || data.username || "用户" + id,
phone: data.mobile || data.phone || "ID已通过校验",
avatar: data.avatar || data.headimg || "",
};
}
if (data === true) {
const id = String(fallbackId || "").trim();
if (!id) {
throw createError("未查询到好友", data);
}
return {
id: id,
nickname: "用户" + id,
phone: "ID已通过校验",
avatar: "",
};
}
throw createError("未查询到好友", data);
}
function sumBy(list, key) {
return (Array.isArray(list) ? list : []).reduce(function (total, item) {
return total + toNumber(item && item[key]);
}, 0);
}
async function fetchPriceData(requestOptions) {
return fetchPayload(
createRequestOptions({
url: serviceConfig.ENDPOINTS.price,
}, requestOptions),
"实时价格加载失败",
);
}
async function fetchHomeBalanceData(requestOptions) {
return fetchPayload(
createRequestOptions({
url: serviceConfig.ENDPOINTS.homeBalance,
}, requestOptions),
"首页资产加载失败",
);
}
async function fetchBmtPowerRateData(requestOptions) {
return fetchPayload(
createRequestOptions({
url: serviceConfig.ENDPOINTS.bmtRedeemPowerRate,
}, requestOptions),
"兑换比例加载失败",
);
}
async function fetchTransferFeeData(requestOptions) {
return fetchPayload(
createRequestOptions({
url: serviceConfig.ENDPOINTS.transferFee,
}, requestOptions),
"手续费比例加载失败",
);
}
async function fetchWalletAddressData(requestOptions) {
return fetchPayload(
createRequestOptions({
url: serviceConfig.ENDPOINTS.walletDetail,
}, requestOptions),
"钱包加载失败",
);
}
async function fetchPointsConvertList(requestOptions) {
return fetchPayload(
createRequestOptions({
url: serviceConfig.ENDPOINTS.pointsConvertList,
data: {
interval: serviceConfig.POINTS_CONVERT_INTERVAL,
},
}, requestOptions),
"积分转换列表加载失败",
);
}
async function fetchTransferLedgerData(requestOptions) {
return fetchPayload(
createRequestOptions({
url: serviceConfig.ENDPOINTS.transferLedger,
}, requestOptions),
"转赠记录加载失败",
);
}
export async function fetchAssetHome(requestOptions) {
const result = await Promise.all([
fetchPriceData(requestOptions),
fetchHomeBalanceData(requestOptions),
]);
return buildHomeOverview(result[1], result[0]);
}
export async function fetchPointsConvertDetail(requestOptions) {
const result = await Promise.all([
fetchHomeBalanceData(requestOptions),
fetchPointsConvertList(requestOptions),
]);
const balances = normalizeBalances(result[0]);
const pointList = Array.isArray(result[1] && result[1].list)
? result[1].list
: [];
return {
availablePoints: toFixedNumber(balances.points, 2),
pendingPoints: toFixedNumber(sumBy(pointList, "number"), 0),
ids: pointList
.map(function (item) {
return item && item.id;
})
.filter(Boolean),
tips: [
"释放中的积分转换成可用积分后方可兑换BMT",
"凌晨0点-凌晨1点积分系统维护不可兑换。",
],
};
}
export async function submitAssetPointsConvert(payload, requestOptions) {
const ids = Array.isArray(payload && payload.ids) ? payload.ids : [];
if (!ids.length) {
throw createError("暂无可转换积分");
}
await fetchPayload(
createRequestOptions({
url: serviceConfig.ENDPOINTS.pointsConvertSubmit,
method: "POST",
data: {
ids: ids.join(","),
type: 1,
},
}, requestOptions),
"积分转换失败",
);
return {
success: true,
message: "转换成功",
};
}
export async function fetchTransferDetail(requestOptions) {
const result = await Promise.all([
fetchHomeBalanceData(requestOptions),
fetchTransferFeeData(requestOptions),
]);
const balances = normalizeBalances(result[0]);
const feePercent = toNumber(result[1] && result[1].r) || 10;
return {
balances: {
points: toFixedNumber(balances.points, 0),
power: toFixedNumber(balances.power, 0),
},
feePercent: feePercent,
tips: buildTransferTips(feePercent),
};
}
export async function searchTransferUser(uid, requestOptions) {
const keyword = String(uid || "").trim();
if (!keyword) {
throw createError("请输入被赠人ID");
}
const data = await fetchPayload(
createRequestOptions({
url: serviceConfig.ENDPOINTS.transferUser,
method: "POST",
data: {
uid: keyword,
},
}, requestOptions),
"查询好友失败",
);
return normalizeTransferTarget(data, keyword);
}
export async function submitAssetTransfer(payload, requestOptions) {
const transferType = payload && payload.type === "power" ? "power" : "points";
const targetId = String(payload && payload.targetId ? payload.targetId : "").trim();
const amount = toNumber(payload && payload.amount);
if (!targetId) {
throw createError("请选择被赠送人");
}
if (!amount) {
throw createError("请输入转赠数量");
}
const result = await Promise.all([
fetchTransferFeeData(requestOptions),
fetchPayload(
createRequestOptions({
url:
transferType === "power"
? serviceConfig.ENDPOINTS.transferPowerSubmit
: serviceConfig.ENDPOINTS.transferPointsSubmit,
method: "POST",
data: {
uid: targetId,
number: String(amount),
},
}, requestOptions),
"转赠失败",
),
]);
const feePercent = toNumber(result[0] && result[0].r) || 10;
const fee = (amount * feePercent) / 100;
const received = amount - fee;
return {
success: true,
fee: toFixedNumber(fee, 2),
received: toFixedNumber(received, 2),
};
}
export async function fetchPowerExchangeDetail(requestOptions) {
const result = await Promise.all([
fetchPriceData(requestOptions),
fetchHomeBalanceData(requestOptions),
]);
const ticker = normalizeTicker(result[0]);
const balances = normalizeBalances(result[1]);
return {
ticker: ticker,
balances: {
coupon: toFixedNumber(balances.coupon, 2),
voucher: toFixedNumber(balances.voucher, 2),
power: toFixedNumber(balances.power, 0),
},
tips: [
"算力 = 抵用券或消费券 ÷ BMT实时价格",
"抵用券和消费券总数小于100券的不可兑换",
"算力用于兑换BMT使用。",
],
};
}
export async function submitAssetPowerExchange(payload, requestOptions) {
const mode = payload && payload.mode === "coupon" ? "coupon" : "voucher";
const amount = toNumber(payload && payload.amount);
if (!amount) {
throw createError("请输入兑换数量");
}
await fetchPayload(
createRequestOptions({
url: serviceConfig.ENDPOINTS.powerExchangeSubmit,
method: "POST",
data: {
type: mode === "coupon" ? 1 : 0,
number: String(amount),
},
}, requestOptions),
"算力兑换失败",
);
return {
success: true,
};
}
export async function fetchBmtExchangeDetail(requestOptions) {
const result = await Promise.all([
fetchPriceData(requestOptions),
fetchHomeBalanceData(requestOptions),
fetchBmtPowerRateData(requestOptions),
]);
const ticker = normalizeTicker(result[0]);
const balances = normalizeBalances(result[1]);
const powerRate = toNumber(result[2]);
return {
ticker: ticker,
powerRate: powerRate,
balances: {
points: toFixedNumber(balances.points, 0),
power: toFixedNumber(balances.power, 2),
bmt: toFixedNumber(balances.bmt, 2),
voucher: toFixedNumber(balances.voucher, 2),
coupon: toFixedNumber(balances.coupon, 2),
},
tips: [
"BMT=输入的积分数量,提交时会同步校验所需算力。",
"兑换所需算力按后端返回比例实时计算。",
"只能兑换100的整数倍小于100积分不可兑换。",
"凌晨0点至凌晨1点积分系统维护期间不可兑换。",
],
};
}
export async function submitAssetBmtExchange(payload, requestOptions) {
const amount = toNumber(payload && payload.amount);
if (!amount) {
throw createError("请输入积分数量");
}
await fetchPayload(
createRequestOptions({
url: serviceConfig.ENDPOINTS.bmtExchangeSubmit,
method: "POST",
data: {
number: String(amount),
},
}, requestOptions),
"BMT兑换失败",
);
return {
success: true,
};
}
export async function fetchWithdrawDetail(requestOptions) {
const result = await Promise.all([
fetchPriceData(requestOptions),
fetchHomeBalanceData(requestOptions),
fetchWalletAddressData(requestOptions),
]);
const ticker = normalizeTicker(result[0]);
const balances = normalizeBalances(result[1]);
const walletPayload = buildWalletPayload(result[2] && result[2].address);
return {
ticker: ticker,
withdrawableBmt: toFixedNumber(balances.withdrawableBmt, 2),
wallets: walletPayload.wallets,
defaultWallet: walletPayload.wallets[0] || null,
};
}
export function submitAssetWithdraw(payload, requestOptions) {
return Promise.reject(
createError("当前接口文档未提供 BMT 提取提交接口"),
);
}
export async function fetchLedgerDetail(type, requestOptions) {
if (type === "transfer") {
const data = await fetchTransferLedgerData(requestOptions);
return {
type: type,
title: "转赠记录",
subtitle: "积分与算力转赠流水",
records: mapTransferRecords(data && data.list),
};
}
if (type === "points") {
const result = await Promise.all([
fetchHomeBalanceData(requestOptions),
fetchPointsConvertList(requestOptions),
]);
const balances = normalizeBalances(result[0]);
const pointList = Array.isArray(result[1] && result[1].list)
? result[1].list
: [];
return {
type: type,
title: "我的积分",
subtitle: "可转换积分记录",
summary: {
label: "有效积分",
value: toFixedNumber(balances.points, 0),
},
records: mapPointsConvertRecords(pointList),
};
}
const homeData = await fetchHomeBalanceData(requestOptions);
const balances = normalizeBalances(homeData);
return buildDefaultLedger(type, balances);
}
export async function fetchWalletDetail(requestOptions) {
const data = await fetchWalletAddressData(requestOptions);
return buildWalletPayload(data && data.address);
}
export async function saveAssetWallet(payload, requestOptions) {
const address = String(payload && payload.address ? payload.address : "").trim();
if (!address) {
throw createError("请输入钱包地址");
}
await fetchPayload(
createRequestOptions({
url: serviceConfig.ENDPOINTS.walletSave,
method: "POST",
data: {
address: address,
},
}, requestOptions),
"保存失败",
);
return {
success: true,
};
}
export async function deleteAssetWallet(id, requestOptions) {
await fetchPayload(
createRequestOptions({
url: serviceConfig.ENDPOINTS.walletSave,
method: "POST",
data: {
address: "",
},
}, requestOptions),
"删除失败",
);
return {
success: true,
};
}

View File

@ -0,0 +1,267 @@
<template>
<view v-if="visible" class="popup-mask" @touchmove.stop.prevent="noop">
<view class="popup-mask__backdrop" @click="handleMaskClick"></view>
<view class="popup-panel" :class="'popup-panel--' + status">
<view class="popup-panel__head">
<text class="popup-panel__title">{{ title }}</text>
<view
v-if="showClose"
class="popup-panel__close"
@click="$emit('cancel')"
>
<image
class="popup-panel__close-image"
src="https://imgs.agrimedia.cn/bm-bmt/bmt-close.png"
mode="aspectFit"
></image>
</view>
</view>
<view v-if="showStatusBlock" class="popup-panel__status">
<image
class="popup-panel__status-image"
:src="statusIcon"
mode="aspectFit"
></image>
<text v-if="message" class="popup-panel__status-text">{{
message
}}</text>
</view>
<text v-else-if="message" class="popup-panel__message">{{
message
}}</text>
<text v-if="description" class="popup-panel__description">{{
description
}}</text>
<view v-if="$slots.default" class="popup-panel__body">
<slot />
</view>
<view
class="popup-panel__footer"
:class="{ 'popup-panel__footer--single': !showCancel }"
>
<view
v-if="showCancel"
class="popup-panel__btn popup-panel__btn--ghost"
@click="$emit('cancel')"
>
{{ cancelText }}
</view>
<view
class="popup-panel__btn popup-panel__btn--primary"
@click="$emit('confirm')"
>
{{ confirmText }}
</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
visible: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "提示",
},
message: {
type: String,
default: "",
},
description: {
type: String,
default: "",
},
confirmText: {
type: String,
default: "确认",
},
cancelText: {
type: String,
default: "取消",
},
showCancel: {
type: Boolean,
default: true,
},
showClose: {
type: Boolean,
default: true,
},
closeOnMask: {
type: Boolean,
default: true,
},
status: {
type: String,
default: "default",
},
},
computed: {
showStatusBlock() {
return this.status === "success" || this.status === "error";
},
statusIcon() {
if (this.status === "success") {
return "https://imgs.agrimedia.cn/bm-bmt/success.png";
}
if (this.status === "error") {
return "https://imgs.agrimedia.cn/bm-bmt/fail.png";
}
return "";
},
},
methods: {
noop() {},
handleMaskClick() {
if (this.closeOnMask) {
this.$emit("cancel");
}
},
},
};
</script>
<style lang="scss" scoped>
@import "../styles/tokens.scss";
.popup-mask {
position: fixed;
inset: 0;
z-index: 80;
display: flex;
align-items: center;
justify-content: center;
padding: 24rpx;
}
.popup-mask__backdrop {
position: absolute;
inset: 0;
background: rgba(5, 10, 22, 0.72);
}
.popup-panel {
position: relative;
width: 100%;
max-width: 600rpx;
padding: 30rpx 28rpx 32rpx;
border-radius: 28rpx;
border: 1px solid rgba(142, 157, 206, 0.14);
background: #2a2f4d;
box-shadow: 0 30rpx 70rpx rgba(0, 0, 0, 0.28);
}
.popup-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
}
.popup-panel__title {
font-size: 28rpx;
font-weight: 700;
color: #ffffff;
}
.popup-panel__close {
display: flex;
align-items: center;
justify-content: center;
width: 44rpx;
height: 44rpx;
}
.popup-panel__close-image {
width: 28rpx;
height: 28rpx;
}
.popup-panel__message {
display: block;
margin-top: 34rpx;
font-size: 24rpx;
line-height: 1.8;
color: #edf2ff;
}
.popup-panel__status {
display: flex;
align-items: center;
justify-content: center;
margin-top: 46rpx;
}
.popup-panel__status-image {
width: 54rpx;
height: 54rpx;
flex-shrink: 0;
}
.popup-panel__status-text {
margin-left: 18rpx;
font-size: 24rpx;
font-weight: 600;
color: #edf2ff;
}
.popup-panel__description {
display: block;
margin-top: 34rpx;
font-size: 20rpx;
line-height: 1.7;
text-align: center;
color: rgba(201, 209, 233, 0.86);
}
.popup-panel__body {
margin-top: 22rpx;
}
.popup-panel__footer {
display: flex;
align-items: center;
gap: 18rpx;
margin-top: 40rpx;
}
.popup-panel__footer--single {
justify-content: center;
}
.popup-panel__btn {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 72rpx;
border-radius: 12rpx;
font-size: 24rpx;
font-weight: 700;
}
.popup-panel__footer--single .popup-panel__btn {
flex: 0 0 240rpx;
}
.popup-panel__btn--ghost {
border: 1px solid rgba(157, 173, 221, 0.44);
background: rgba(77, 89, 129, 0.28);
color: #ffffff;
}
.popup-panel__btn--primary {
background: #02ABF1;
color: #ffffff;
}
</style>

View File

@ -0,0 +1,126 @@
<template>
<view class="asset-shell" :style="{ '--asset-shell-side-width': sideWidth }">
<view class="asset-shell__nav">
<view class="asset-shell__side">
<view v-if="backable" class="asset-shell__back" @click="handleBack">
<image
class="asset-shell__back-image"
src="https://imgs.agrimedia.cn/bm-bmt/bacn-icon.png"
mode="aspectFit"
></image>
</view>
</view>
<text class="asset-shell__title">{{ title }}</text>
<view class="asset-shell__side asset-shell__side--right">
<slot name="right">
<text
v-if="rightText"
class="asset-shell__action"
@click="$emit('right-click')"
>
{{ rightText }}
</text>
</slot>
</view>
</view>
<slot />
</view>
</template>
<script>
export default {
props: {
title: {
type: String,
default: "",
},
rightText: {
type: String,
default: "",
},
backable: {
type: Boolean,
default: true,
},
sideWidth: {
type: String,
default: "120rpx",
},
},
methods: {
handleBack() {
if (getCurrentPages().length > 1) {
uni.navigateBack();
return;
}
uni.reLaunch({
url: "/pages/index/index",
});
},
},
};
</script>
<style lang="scss" scoped>
@import "../styles/tokens.scss";
.asset-shell {
position: sticky;
top: 0;
z-index: 20;
padding: calc(env(safe-area-inset-top) + 20rpx) 14rpx 20rpx;
background: #191e32;
}
.asset-shell__nav {
display: flex;
align-items: center;
min-height: 48rpx;
}
.asset-shell__side {
display: flex;
align-items: center;
width: var(--asset-shell-side-width);
flex: 0 0 var(--asset-shell-side-width);
min-width: 0;
}
.asset-shell__side--right {
justify-content: flex-end;
width: 48rpx;
height: 48rpx;
}
.asset-shell__back {
display: flex;
align-items: center;
justify-content: center;
width: 48rpx;
height: 48rpx;
}
.asset-shell__back-image {
width: 48rpx;
height: 48rpx;
}
.asset-shell__title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 500;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #ffffff;
}
.asset-shell__action {
font-size: 24rpx;
font-weight: 600;
color: rgba(232, 239, 255, 0.86);
}
</style>

View File

@ -0,0 +1,184 @@
<template>
<view>
<view v-if="list.length">
<view v-for="item in list" :key="item.id" class="record-item">
<view
class="record-item__badge"
:class="'record-item__badge--' + (item.tone || 'info')"
>
{{ item.tag || "记" }}
</view>
<view class="record-item__content">
<view class="record-item__head">
<text class="record-item__title">{{ item.title }}</text>
<text
class="record-item__amount"
:class="'record-item__amount--' + (item.tone || 'info')"
>
{{ item.amount }}
</text>
</view>
<text v-if="item.subtitle" class="record-item__subtitle">{{
item.subtitle
}}</text>
<view class="record-item__meta">
<text class="record-item__time">{{ item.time }}</text>
<text v-if="item.balance" class="record-item__balance">{{
item.balance
}}</text>
</view>
</view>
</view>
</view>
<view v-else class="record-empty">
<text class="record-empty__title">{{ emptyTitle }}</text>
<text class="record-empty__desc">{{ emptyDesc }}</text>
</view>
</view>
</template>
<script>
export default {
props: {
list: {
type: Array,
default() {
return [];
},
},
emptyTitle: {
type: String,
default: "暂无记录",
},
emptyDesc: {
type: String,
default: "当前暂无流水数据。",
},
},
};
</script>
<style lang="scss" scoped>
@import "../styles/tokens.scss";
.record-item {
display: flex;
align-items: flex-start;
padding: 24rpx 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.record-item:last-child {
border-bottom: 0;
padding-bottom: 0;
}
.record-item__badge {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
margin-right: 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 700;
color: #ffffff;
flex-shrink: 0;
}
.record-item__badge--success {
background: linear-gradient(135deg, #4fe0b5 0%, #36b98c 100%);
}
.record-item__badge--danger {
background: linear-gradient(135deg, #ff8091 0%, #ff5d6e 100%);
}
.record-item__badge--info {
background: linear-gradient(135deg, #5a71ff 0%, #4cc9ff 100%);
}
.record-item__content {
flex: 1;
min-width: 0;
}
.record-item__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.record-item__title {
flex: 1;
min-width: 0;
margin-right: 20rpx;
font-size: 28rpx;
line-height: 1.6;
font-weight: 600;
color: $asset-text-main;
}
.record-item__amount {
font-size: 28rpx;
font-weight: 700;
}
.record-item__amount--success {
color: $asset-success;
}
.record-item__amount--danger {
color: $asset-danger;
}
.record-item__amount--info {
color: $asset-accent-strong;
}
.record-item__subtitle {
display: block;
margin-top: 10rpx;
font-size: 22rpx;
line-height: 1.6;
color: rgba(255, 255, 255, 0.64);
}
.record-item__meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 14rpx;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.58);
}
.record-item__time {
margin-right: 20rpx;
}
.record-item__balance {
text-align: right;
}
.record-empty {
padding: 48rpx 20rpx;
text-align: center;
}
.record-empty__title {
display: block;
font-size: 30rpx;
font-weight: 700;
color: $asset-text-main;
}
.record-empty__desc {
display: block;
margin-top: 16rpx;
font-size: 24rpx;
line-height: 1.7;
color: rgba(255, 255, 255, 0.62);
}
</style>

View File

@ -0,0 +1,101 @@
<template>
<asset-confirm-popup
:visible="visible"
:title="title"
:confirm-text="confirmText"
:cancel-text="cancelText"
:show-cancel="showCancel"
:show-close="showClose"
:close-on-mask="closeOnMask"
@cancel="$emit('cancel')"
@confirm="$emit('confirm')"
>
<view class="wallet-dialog">
<text v-if="message" class="wallet-dialog__message">{{ message }}</text>
<view v-if="address" class="wallet-dialog__address">{{ address }}</view>
<text v-if="description" class="wallet-dialog__description">{{
description
}}</text>
</view>
</asset-confirm-popup>
</template>
<script>
import AssetConfirmPopup from "./asset-confirm-popup.vue";
export default {
components: {
AssetConfirmPopup,
},
props: {
visible: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "提示",
},
message: {
type: String,
default: "",
},
address: {
type: String,
default: "",
},
description: {
type: String,
default: "",
},
confirmText: {
type: String,
default: "确认",
},
cancelText: {
type: String,
default: "取消",
},
showCancel: {
type: Boolean,
default: true,
},
showClose: {
type: Boolean,
default: true,
},
closeOnMask: {
type: Boolean,
default: false,
},
},
};
</script>
<style lang="scss" scoped>
.wallet-dialog__message {
display: block;
font-size: 24rpx;
line-height: 1.6;
color: #edf2ff;
}
.wallet-dialog__address {
margin-top: 20rpx;
padding: 20rpx 18rpx;
border-radius: 10rpx;
background: #191E32;
font-size: 24rpx;
line-height: 1.6;
word-break: break-all;
color: #ffffff;
}
.wallet-dialog__description {
display: block;
margin-top: 20rpx;
font-size: 22rpx;
line-height: 1.7;
color: rgba(201, 209, 233, 0.82);
}
</style>

25
config/service.js Normal file
View File

@ -0,0 +1,25 @@
const serviceConfig = {
BASE_URL: "https://tpoint.agrimedia.cn",
TIMEOUT: 10000,
WALLET_NAME: "海南农综交易所",
POINTS_CONVERT_INTERVAL: "0,999999999",
ENDPOINTS: {
price: "/api/hn/getPrice",
homeBalance: "/api/hn/getAllBalance",
powerExchangeSubmit: "/api/hn/redeem/power",
bmtRedeemPowerRate: "/api/hn/redeem/getRedeemPowerRate",
bmtExchangeSubmit: "/api/hn/redeem/redeem_bmt",
transferFee: "/api/hn/transfer/getProp",
transferUser: "/api/hn/transfer/getUserInfo",
transferPowerSubmit: "/api/hn/transfer/transferPower",
transferPointsSubmit: "/api/hn/transfer/transferPoint",
transferLedger: "/api/hn/transfer/transferList",
walletDetail: "/api/hn/wallet/getWalletAddress",
walletSave: "/api/hn/wallet/saveAddress",
pointsConvertList: "/api/integral/transferList",
pointsConvertSubmit: "/api/integral/doTransfer",
pointsConvertInfo: "/api/integral/transferInfo",
},
};
export default serviceConfig;

29
index.html Normal file
View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
<style>
html,
body,
#app {
min-height: 100%;
margin: 0;
background: #08162e;
}
</style>
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>

22
main.js Normal file
View File

@ -0,0 +1,22 @@
import App from './App'
// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
return {
app
}
}
// #endif

72
manifest.json Normal file
View File

@ -0,0 +1,72 @@
{
"name" : "白马交易所",
"appid" : "__UNI__3EC3CC8",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* */
"modules" : {},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios" : {},
/* SDK */
"sdkConfigs" : {}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "2"
}

75
pages.json Normal file
View File

@ -0,0 +1,75 @@
{
"pages": [
//pageshttps://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom",
"backgroundColor": "#191E32"
}
},
{
"path": "pages/assets/transfer",
"style": {
"navigationStyle": "custom",
"backgroundColor": "#191E32"
}
},
{
"path": "pages/assets/power-exchange",
"style": {
"navigationStyle": "custom",
"backgroundColor": "#191E32"
}
},
{
"path": "pages/assets/bmt-exchange",
"style": {
"navigationStyle": "custom",
"backgroundColor": "#191E32"
}
},
{
"path": "pages/assets/withdraw",
"style": {
"navigationStyle": "custom",
"backgroundColor": "#191E32"
}
},
{
"path": "pages/assets/points-convert",
"style": {
"navigationStyle": "custom",
"backgroundColor": "#191E32"
}
},
{
"path": "pages/assets/ledger",
"style": {
"navigationStyle": "custom",
"backgroundColor": "#191E32"
}
},
{
"path": "pages/assets/wallet",
"style": {
"navigationStyle": "custom",
"backgroundColor": "#191E32"
}
},
{
"path": "pages/assets/wallet-form",
"style": {
"navigationStyle": "custom",
"backgroundColor": "#191E32"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "数字资产",
"navigationBarBackgroundColor": "#191E32",
"backgroundColor": "#191E32"
},
"uniIdRouter": {}
}

View File

@ -0,0 +1,694 @@
<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">{{ 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">{{ 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">{{ displayCoupon }}</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">{{ displayPrice }}</text>
<text class="info-row__unit">CNY/BMT</text>
</view>
</view>
<view class="info-card__estimate">
<text>全部兑换预估可得</text>
<text class="info-card__estimate-value">{{ 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="form-input">
<input
v-model="form.points"
class="form-input__field"
type="number"
placeholder="请输入积分数量"
placeholder-class="form-input__placeholder"
/>
<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">{{ 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">{{
powerCost
}}</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">{{ index + 1 }}.</text>
<text class="tips-block__text">{{ tip }}</text>
</view>
</view>
</view>
<view class="action-wrap">
<view class="action-button" @click="confirmVisible = true"
>确认兑换</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">输入积分</text>
<text class="meta-pair__value">{{ formatAmount(pointsValue, 0) }}</text>
</view>
<view class="popup-line">
<text class="meta-pair__label">预估兑换BMT</text>
<text class="meta-pair__value">{{ estimateBmt }}</text>
</view>
<view class="popup-line">
<text class="meta-pair__label">消耗算力</text>
<text class="meta-pair__value">{{ powerCost }}</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 {
fetchBmtExchangeDetail,
submitAssetBmtExchange,
} from "../../api/assets";
export default {
components: {
AssetConfirmPopup,
AssetPageShell,
},
data() {
return {
detail: {
ticker: {},
balances: {},
tips: [],
},
hasShown: false,
form: {
points: "",
},
confirmVisible: false,
submitting: false,
};
},
onLoad() {
this.loadPage(true);
},
onShow() {
if (this.hasShown) {
this.loadPage();
return;
}
this.hasShown = true;
},
computed: {
pointsValue() {
return Number(this.form.points || 0);
},
powerRateNumber() {
return Number(this.detail.powerRate || 0);
},
estimateBmt() {
if (!this.pointsValue) {
return "0";
}
return this.formatAmount(this.pointsValue, 0);
},
powerCost() {
if (!this.pointsValue || !this.powerRateNumber) {
return "0.00";
}
return (this.pointsValue * this.powerRateNumber).toFixed(2);
},
allEstimateBmt() {
const pointsLimit = Number(this.detail.balances.points || 0);
const powerLimit = this.powerRateNumber
? Number(this.detail.balances.power || 0) / this.powerRateNumber
: pointsLimit;
const available = Math.max(0, Math.min(pointsLimit, powerLimit || 0));
return this.formatAmount(available, 0);
},
priceNumber() {
return Number(
this.detail.ticker.close || this.detail.ticker.cnyPrice || 0,
);
},
displayBmt() {
return this.formatAmount(this.detail.balances.bmt, 2);
},
displayVoucher() {
return this.formatAmount(this.detail.balances.voucher, 0);
},
displayCoupon() {
return this.formatAmount(this.detail.balances.coupon, 2);
},
displayPrice() {
return this.priceNumber ? this.priceNumber.toFixed(2) : "0.00";
},
normalizedTips() {
if (this.detail.tips && this.detail.tips.length) {
return this.detail.tips;
}
return [
"1.BMT=积分-BMT实时价格",
"2.只能兑换100的整数倍小于100积分不可兑换",
"3.凌晨0点-凌晨1点积分系统维护不可兑换",
"4.可用积分不足可以将释放中的积分转换成可用积分0。",
];
},
},
methods: {
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",
};
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, ",");
},
async loadPage(showLoading) {
try {
this.detail = await fetchBmtExchangeDetail(
showLoading
? {
showLoading: true,
loadingText: "加载中",
}
: null,
);
} catch (error) {
uni.showToast({
title: error.message || "页面加载失败",
icon: "none",
});
}
},
async submit() {
if (this.submitting) {
return;
}
if (!this.pointsValue) {
uni.showToast({
title: "请输入积分数量",
icon: "none",
});
return;
}
if (this.pointsValue < 100) {
uni.showToast({
title: "小于100积分不可兑换",
icon: "none",
});
return;
}
if (this.pointsValue % 100 !== 0) {
uni.showToast({
title: "只能兑换100的整数倍",
icon: "none",
});
return;
}
if (this.pointsValue > Number(this.detail.balances.points || 0)) {
uni.showToast({
title: "可用积分不足",
icon: "none",
});
return;
}
if (Number(this.powerCost) > Number(this.detail.balances.power || 0)) {
uni.showToast({
title: "可用算力不足",
icon: "none",
});
return;
}
this.submitting = true;
try {
await submitAssetBmtExchange({
amount: this.pointsValue,
}, {
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;
}
.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: 32rpx;
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;
}
.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__placeholder {
color: rgba(177, 187, 214, 0.42);
}
.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__value {
font-size: 26rpx;
font-weight: 800;
color: #1ad296;
}
.preview-card__value--light {
color: #ffffff;
}
.tips-block {
margin-top: 24rpx;
padding: 0rpx 10rpx;
}
.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 {
padding: 36rpx 22rpx 0;
}
.action-button {
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;
}
.action-link {
margin-top: 26rpx;
text-align: center;
font-weight: 500;
font-size: 28rpx;
color: #1fb4ff;
}
.popup-line {
margin-top: 14rpx;
}
</style>

474
pages/assets/ledger.vue Normal file
View File

@ -0,0 +1,474 @@
<template>
<view class="asset-page asset-theme ledger-page">
<asset-page-shell :title="ledger.title || '记录'" />
<view class="asset-scroll" :class="{ 'ledger-scroll': isTransferLedger }">
<template v-if="isTransferLedger">
<view class="transfer-ledger-head">
<text class="transfer-ledger-head__eyebrow">最近记录</text>
<text class="transfer-ledger-head__desc">{{
ledger.subtitle || "积分与算力转赠流水"
}}</text>
</view>
<view v-if="transferRecords.length" class="transfer-ledger-list">
<view
v-for="item in transferRecords"
:key="item.id"
class="transfer-record-card"
>
<view
class="transfer-record-card__icon"
:class="
'transfer-record-card__icon--' +
(item.cardTone || item.tone || 'info')
"
>
<text class="transfer-record-card__icon-text">{{
item.actionSymbol || "·"
}}</text>
</view>
<view class="transfer-record-card__content">
<view class="transfer-record-card__row transfer-record-card__row--top">
<view class="transfer-record-card__main">
<text class="transfer-record-card__title">{{
item.title || "转赠记录"
}}</text>
<text
v-if="item.orderSn || item.subtitle"
class="transfer-record-card__order"
>
{{ item.orderSn ? "单号 " + item.orderSn : item.subtitle }}
</text>
</view>
<view class="transfer-record-card__amount-box">
<text class="transfer-record-card__asset">{{
item.assetLabel
}}</text>
<text
class="transfer-record-card__amount"
:class="
'transfer-record-card__amount--' +
(item.cardTone || item.tone || 'info')
"
>
{{ item.amount }}
</text>
</view>
</view>
<view
class="transfer-record-card__row transfer-record-card__row--middle"
>
<text class="transfer-record-card__balance">{{
item.balanceLabel || item.balance
}}</text>
<text class="transfer-record-card__fee">{{ item.feeText }}</text>
</view>
<view
class="transfer-record-card__row transfer-record-card__row--bottom"
>
<text class="transfer-record-card__time">{{ item.time }}</text>
<text
class="transfer-record-card__direction"
:class="
'transfer-record-card__direction--' +
(item.cardTone || item.tone || 'info')
"
>
{{ item.directionLabel }}
</text>
</view>
</view>
</view>
</view>
<view v-else class="transfer-empty">
<text class="transfer-empty__title">暂无转赠记录</text>
<text class="transfer-empty__desc">
当前还没有积分或算力的转赠流水
</text>
</view>
</template>
<template v-else>
<view class="page-hero">
<view class="page-hero__mark">
<view class="page-hero__ring page-hero__ring--outer"></view>
<view class="page-hero__ring page-hero__ring--inner"></view>
<text class="page-hero__text">{{ pageMark }}</text>
</view>
<text class="page-hero__value">{{ ledger.title || "记录" }}</text>
<text v-if="ledger.subtitle" class="page-hero__desc">{{
ledger.subtitle
}}</text>
</view>
<view class="section glass-panel panel-block">
<text class="section-label">{{ ledger.title }}</text>
<text class="section-subtitle">{{ ledger.subtitle }}</text>
</view>
<view v-if="ledger.summary" class="section glass-panel panel-block">
<text class="section-label">{{ ledger.summary.label }}</text>
<text class="summary-hero">{{ ledger.summary.value }}</text>
</view>
<view
v-if="ledger.tabs && ledger.tabs.length"
class="section paper-panel panel-block"
>
<view class="tab-row tab-row--light">
<view
v-for="tab in ledger.tabs"
:key="tab.key"
class="tab-chip tab-chip--light"
:class="{ 'is-active': activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.label }}
</view>
</view>
<view class="record-wrapper">
<asset-record-list :list="currentList" />
</view>
</view>
<view v-else class="section paper-panel panel-block">
<asset-record-list :list="ledger.records || []" />
</view>
</template>
</view>
</view>
</template>
<script>
import AssetPageShell from "../../components/asset-page-shell.vue";
import AssetRecordList from "../../components/asset-record-list.vue";
import { fetchLedgerDetail } from "../../api/assets";
export default {
components: {
AssetPageShell,
AssetRecordList,
},
data() {
return {
type: "transfer",
ledger: {
title: "",
subtitle: "",
records: [],
tabs: [],
},
activeTab: "",
};
},
computed: {
isTransferLedger() {
return this.type === "transfer";
},
currentList() {
if (!this.ledger.recordsByTab || !this.activeTab) {
return [];
}
return this.ledger.recordsByTab[this.activeTab] || [];
},
transferRecords() {
return Array.isArray(this.ledger.records) ? this.ledger.records : [];
},
pageMark() {
const markMap = {
transfer: "⇄",
power: "⚡",
bmt: "D",
withdraw: "B",
coupon: "券",
voucher: "抵",
points: "积",
};
return markMap[this.type] || "记";
},
},
onLoad(options) {
this.type = (options && options.type) || "transfer";
this.loadPage(true);
},
methods: {
async loadPage(showLoading) {
try {
const result = await fetchLedgerDetail(
this.type,
showLoading
? {
showLoading: true,
loadingText: "加载中",
}
: null,
);
this.ledger = result;
if (result.tabs && result.tabs.length) {
this.activeTab = result.tabs[0].key;
}
} catch (error) {
uni.showToast({
title: error.message || "记录加载失败",
icon: "none",
});
}
},
},
};
</script>
<style lang="scss" scoped>
@import "../../styles/tokens.scss";
@import "../../styles/common.scss";
.ledger-page {
min-height: 100vh;
background: #191e32;
}
.ledger-scroll {
padding: 12rpx 14rpx calc(env(safe-area-inset-bottom) + 36rpx);
}
.section {
margin-top: 16rpx;
}
.summary-hero {
display: block;
margin-top: 16rpx;
font-size: 56rpx;
font-weight: 700;
color: $asset-text-main;
}
.tab-row--light {
background: rgba(17, 27, 54, 0.54);
}
.tab-chip--light {
color: rgba(255, 255, 255, 0.64);
}
.record-wrapper {
margin-top: 18rpx;
}
.transfer-ledger-head {
padding: 10rpx 6rpx 14rpx;
}
.transfer-ledger-head__eyebrow {
display: block;
font-size: 34rpx;
font-weight: 700;
color: #ffffff;
}
.transfer-ledger-head__desc {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
line-height: 1.6;
color: rgba(188, 197, 223, 0.82);
}
.transfer-ledger-list {
margin-top: 6rpx;
}
.transfer-record-card {
display: flex;
align-items: flex-start;
margin-top: 18rpx;
padding: 24rpx 20rpx;
border-radius: 16rpx;
background: #242944;
box-shadow: 0 14rpx 30rpx rgba(8, 13, 30, 0.14);
}
.transfer-record-card:first-child {
margin-top: 0;
}
.transfer-record-card__icon {
display: flex;
align-items: center;
justify-content: center;
width: 72rpx;
height: 72rpx;
margin-right: 18rpx;
border-radius: 50%;
flex-shrink: 0;
}
.transfer-record-card__icon--success {
background: rgba(41, 215, 164, 0.16);
color: #29d7a4;
}
.transfer-record-card__icon--danger {
background: rgba(255, 139, 93, 0.16);
color: #ff8b5d;
}
.transfer-record-card__icon--info {
background: rgba(32, 182, 245, 0.16);
color: #20b6f5;
}
.transfer-record-card__icon-text {
font-size: 38rpx;
font-weight: 700;
line-height: 1;
}
.transfer-record-card__content {
flex: 1;
min-width: 0;
}
.transfer-record-card__row {
display: flex;
justify-content: space-between;
}
.transfer-record-card__row--top {
align-items: flex-start;
}
.transfer-record-card__row--middle,
.transfer-record-card__row--bottom {
align-items: center;
margin-top: 18rpx;
}
.transfer-record-card__main {
flex: 1;
min-width: 0;
margin-right: 18rpx;
}
.transfer-record-card__title {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #ffffff;
}
.transfer-record-card__order {
display: block;
margin-top: 10rpx;
font-size: 22rpx;
line-height: 1.5;
color: rgba(167, 177, 207, 0.9);
word-break: break-all;
}
.transfer-record-card__amount-box {
display: flex;
align-items: baseline;
justify-content: flex-end;
flex-wrap: wrap;
min-width: 0;
}
.transfer-record-card__asset {
margin-right: 10rpx;
font-size: 24rpx;
color: rgba(191, 200, 224, 0.76);
}
.transfer-record-card__amount {
font-size: 34rpx;
font-weight: 700;
line-height: 1;
}
.transfer-record-card__amount--success {
color: #29d7a4;
}
.transfer-record-card__amount--danger {
color: #ff8b5d;
}
.transfer-record-card__amount--info {
color: #20b6f5;
}
.transfer-record-card__balance,
.transfer-record-card__time {
flex: 1;
min-width: 0;
font-size: 24rpx;
line-height: 1.5;
color: rgba(200, 208, 231, 0.82);
}
.transfer-record-card__time {
font-size: 22rpx;
color: rgba(159, 170, 202, 0.88);
}
.transfer-record-card__fee {
margin-left: 20rpx;
font-size: 24rpx;
line-height: 1.5;
text-align: right;
color: #5ad7a1;
}
.transfer-record-card__direction {
margin-left: 20rpx;
padding: 8rpx 18rpx;
border-radius: 999rpx;
font-size: 20rpx;
font-weight: 600;
}
.transfer-record-card__direction--success {
background: rgba(41, 215, 164, 0.14);
color: #29d7a4;
}
.transfer-record-card__direction--danger {
background: rgba(255, 139, 93, 0.14);
color: #ff8b5d;
}
.transfer-record-card__direction--info {
background: rgba(32, 182, 245, 0.14);
color: #20b6f5;
}
.transfer-empty {
margin-top: 20rpx;
padding: 48rpx 28rpx;
border-radius: 16rpx;
background: #242944;
text-align: center;
box-shadow: 0 14rpx 30rpx rgba(8, 13, 30, 0.14);
}
.transfer-empty__title {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #ffffff;
}
.transfer-empty__desc {
display: block;
margin-top: 16rpx;
font-size: 24rpx;
line-height: 1.6;
color: rgba(188, 197, 223, 0.78);
}
</style>

View File

@ -0,0 +1,337 @@
<template>
<view class="asset-page asset-theme points-page">
<asset-page-shell title="积分转换" />
<view class="asset-scroll points-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('points')"
mode="aspectFit"
></image>
</view>
<text class="hero-card__coin-text">积分</text>
</view>
<text class="hero-card__label">可用积分:</text>
<text class="hero-card__value">{{ displayAvailablePoints }}</text>
</view>
</view>
<view class="pending-card">
<text class="pending-card__label">未转换积分释放中的积分</text>
<text class="pending-card__value">{{ displayPendingPoints }}</text>
</view>
<view class="tips-block">
<view
v-for="(tip, index) in detail.tips"
:key="tip"
class="tips-block__item"
>
<text class="tips-block__index">{{ index + 1 }}.</text>
<text class="tips-block__text">{{ tip }}</text>
</view>
</view>
<view class="action-wrap">
<view
class="action-button"
:class="{ 'action-button--disabled': !canSubmit || submitting }"
@click="submit"
>
{{ submitting ? "转换中..." : "转换可用积分" }}
</view>
</view>
</view>
</view>
</template>
<script>
import AssetPageShell from "../../components/asset-page-shell.vue";
import {
fetchPointsConvertDetail,
submitAssetPointsConvert,
} from "../../api/assets";
export default {
components: {
AssetPageShell,
},
data() {
return {
hasShown: false,
detail: {
availablePoints: "0.00",
pendingPoints: "0",
ids: [],
tips: [],
},
submitting: false,
};
},
onLoad() {
this.loadPage(true);
},
onShow() {
if (this.hasShown) {
this.loadPage();
return;
}
this.hasShown = true;
},
computed: {
pendingPointsValue() {
return Number(this.detail.pendingPoints || 0);
},
canSubmit() {
return this.pendingPointsValue > 0;
},
displayAvailablePoints() {
return this.formatAmount(this.detail.availablePoints, 2);
},
displayPendingPoints() {
return this.formatAmount(this.detail.pendingPoints, 0);
},
},
methods: {
pageIcon(type) {
const iconMap = {
points: "https://imgs.agrimedia.cn/bm-bmt/j-w.png",
voucher: "https://imgs.agrimedia.cn/bm-bmt/quan-icon.png",
coupon: "https://imgs.agrimedia.cn/bm-bmt/xiaofei-icon.png",
power: "https://imgs.agrimedia.cn/bm-bmt/s.png",
bmt: "https://imgs.agrimedia.cn/bm-bmt/b.png",
};
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, ",");
},
async loadPage(showLoading) {
try {
this.detail = await fetchPointsConvertDetail(
showLoading
? {
showLoading: true,
loadingText: "加载中",
}
: null,
);
} catch (error) {
uni.showToast({
title: error.message || "页面加载失败",
icon: "none",
});
}
},
async submit() {
if (this.submitting) {
return;
}
if (!this.canSubmit) {
uni.showToast({
title: "暂无可转换积分",
icon: "none",
});
return;
}
this.submitting = true;
try {
const result = await submitAssetPointsConvert({
pendingPoints: this.pendingPointsValue,
ids: this.detail.ids,
}, {
showLoading: true,
loadingText: "转换中",
});
uni.showToast({
title: result.message || "转换成功",
icon: "none",
});
this.loadPage();
} catch (error) {
uni.showToast({
title: error.message || "转换失败",
icon: "none",
});
} finally {
this.submitting = false;
}
},
},
};
</script>
<style lang="scss" scoped>
@import "../../styles/tokens.scss";
@import "../../styles/common.scss";
.points-page {
min-height: 100vh;
background: #191e32;
}
.points-scroll {
min-height: calc(100vh - env(safe-area-inset-top) - 104rpx);
display: flex;
flex-direction: column;
padding: 10rpx 14rpx calc(env(safe-area-inset-bottom) + 36rpx);
}
.hero-card,
.pending-card {
border-radius: 8rpx;
box-shadow: 0 12rpx 24rpx rgba(8, 13, 30, 0.12);
}
.hero-card {
position: relative;
min-height: 240rpx;
padding: 24rpx 28rpx;
overflow: hidden;
background: #242944 url("https://imgs.agrimedia.cn/bm-bmt/jifen-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, #f85ca9 0%, #de4d95 100%);
box-shadow: 0 12rpx 20rpx rgba(230, 90, 156, 0.2);
}
.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: 38rpx;
font-size: 28rpx;
color: #9ba7ce;
}
.hero-card__value {
margin-top: 8rpx;
font-size: 48rpx;
font-weight: 800;
line-height: 1.08;
color: #ffffff;
}
.pending-card {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20rpx;
padding: 30rpx 26rpx;
background: rgba(59, 49, 67, 0.92);
}
.pending-card__label,
.pending-card__value {
font-size: 26rpx;
font-weight: 600;
color: #ff885d;
}
.pending-card__value {
font-size: 28rpx;
}
.tips-block {
margin-top: 20rpx;
padding: 0 2rpx;
}
.tips-block__item {
display: flex;
align-items: flex-start;
margin-top: 10rpx;
}
.tips-block__item:first-child {
margin-top: 0;
}
.tips-block__index,
.tips-block__text {
font-size: 24rpx;
font-weight: 400;
line-height: 1.75;
color: rgba(173, 184, 216, 0.82);
}
.tips-block__index {
flex-shrink: 0;
margin-right: 6rpx;
}
.action-wrap {
margin-top: auto;
padding: 120rpx 24rpx 0;
}
.action-button {
display: flex;
align-items: center;
justify-content: center;
height: 90rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #22c2ff 0%, #159de6 100%);
box-shadow: 0 16rpx 30rpx rgba(31, 169, 243, 0.2);
font-size: 32rpx;
font-weight: 700;
color: #ffffff;
}
.action-button--disabled {
opacity: 0.56;
}
</style>

View File

@ -0,0 +1,743 @@
<template>
<view class="asset-page asset-theme power-page">
<asset-page-shell title="算力兑换" />
<view class="asset-scroll power-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('power')"
mode="aspectFit"
></image>
</view>
<text class="hero-card__coin-text">算力</text>
</view>
<text class="hero-card__label">已有算力:</text>
<text class="hero-card__value">{{ displayPower }}</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">{{ 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">{{ displayCoupon }}</text>
</view>
<view class="info-row info-row--last">
<view class="info-row__left">
<image
class="info-row__icon"
:src="pageIcon('bmt')"
mode="aspectFit"
></image>
<text class="info-row__label">BMT实时价格</text>
</view>
<view class="info-row__price">
<text class="info-row__value">{{ displayPrice }}</text>
<text class="info-row__unit">RMB/BMT</text>
</view>
</view>
<view class="info-card__estimate">
<text>全部兑换预估可得</text>
<text class="info-card__estimate-value">{{ allEstimatePower }}</text>
<text>算力</text>
</view>
</view>
<view class="panel-card form-card">
<view class="form-card__title">
<image
class="form-card__title-icon"
:src="pageIcon('power')"
mode="aspectFit"
></image>
<text class="form-card__title-text">算力兑换</text>
</view>
<view class="mode-tabs">
<view
v-for="item in modeList"
:key="item.value"
class="mode-tabs__item"
:class="{ 'is-active': form.mode === item.value }"
@click="form.mode = item.value"
>
{{ item.label }}
</view>
</view>
<view class="form-input">
<input
v-model="form.amount"
class="form-input__field"
type="number"
:placeholder="'请输入' + currentModeLabel + '数量'"
placeholder-class="form-input__placeholder"
/>
<text class="form-input__suffix">{{ currentModeLabel }}</text>
</view>
<view class="preview-card">
<view class="preview-card__row">
<view class="preview-card__left">
<text class="preview-card__label">预估兑换算力</text>
</view>
<text class="preview-card__value">{{ estimatePower }}</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">{{ index + 1 }}.</text>
<text class="tips-block__text">{{ tip }}</text>
</view>
</view>
</view>
<view class="action-wrap">
<view class="action-button" @click="openConfirmDialog">确认兑换</view>
<view class="action-link" @click="openLedger">兑换记录</view>
</view>
</view>
<asset-confirm-popup
:visible="dialog.visible"
:title="dialog.title"
:message="dialog.message"
:description="dialog.description"
:confirm-text="dialog.confirmText"
:cancel-text="dialog.cancelText"
:show-cancel="dialog.showCancel"
:status="dialog.status"
@cancel="closeDialog"
@confirm="handleDialogConfirm"
/>
</view>
</template>
<script>
import AssetConfirmPopup from "../../components/asset-confirm-popup.vue";
import AssetPageShell from "../../components/asset-page-shell.vue";
import {
fetchPowerExchangeDetail,
submitAssetPowerExchange,
} from "../../api/assets";
export default {
components: {
AssetConfirmPopup,
AssetPageShell,
},
data() {
return {
modeList: [
{ label: "抵用券兑换", value: "voucher" },
{ label: "消费券兑换", value: "coupon" },
],
detail: {
ticker: {},
balances: {},
tips: [],
},
hasShown: false,
form: {
mode: "voucher",
amount: "",
},
dialog: {
visible: false,
title: "确认兑换",
message: "",
description: "",
confirmText: "确认",
cancelText: "取消",
showCancel: true,
status: "default",
action: "",
},
submitting: false,
};
},
onLoad() {
this.loadPage(true);
},
onShow() {
if (this.hasShown) {
this.loadPage();
return;
}
this.hasShown = true;
},
computed: {
amountValue() {
return Number(this.form.amount || 0);
},
currentModeLabel() {
return this.form.mode === "coupon" ? "消费券" : "抵用券";
},
currentModeBalance() {
if (this.form.mode === "coupon") {
return Number(this.detail.balances.coupon || 0);
}
return Number(this.detail.balances.voucher || 0);
},
priceNumber() {
return Number(
this.detail.ticker.close || this.detail.ticker.cnyPrice || 0,
);
},
estimatePower() {
if (!this.amountValue || !this.priceNumber) {
return "0";
}
return this.formatAmount(this.amountValue / this.priceNumber, 0);
},
allEstimatePower() {
if (!this.priceNumber) {
return "0";
}
const totalTickets =
Number(this.detail.balances.voucher || 0) +
Number(this.detail.balances.coupon || 0);
return this.formatAmount(totalTickets / this.priceNumber, 0);
},
displayPower() {
return this.formatAmount(this.detail.balances.power, 2);
},
displayVoucher() {
return this.formatAmount(this.detail.balances.voucher, 0);
},
displayCoupon() {
return this.formatAmount(this.detail.balances.coupon, 2);
},
displayPrice() {
return this.priceNumber ? this.priceNumber.toFixed(2) : "0.00";
},
normalizedTips() {
if (this.detail.tips && this.detail.tips.length) {
return this.detail.tips;
}
return [
"算力 = 抵用券或消费券 ÷ BMT实时价格",
"抵用券和消费券总数小于100券的不可兑换",
"算力用于兑换BMT使用。",
];
},
},
methods: {
pageIcon(type) {
const iconMap = {
power: "https://imgs.agrimedia.cn/bm-bmt/s.png",
bmt: "https://imgs.agrimedia.cn/bm-bmt/b.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",
};
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, ",");
},
async loadPage(showLoading) {
try {
this.detail = await fetchPowerExchangeDetail(
showLoading
? {
showLoading: true,
loadingText: "加载中",
}
: null,
);
} catch (error) {
this.openResultDialog(
"error",
"兑换失败!",
error.message || "页面加载失败",
);
}
},
openDialog(payload) {
this.dialog = {
...this.dialog,
...payload,
visible: true,
};
},
closeDialog() {
this.dialog.visible = false;
this.dialog.action = "";
},
openResultDialog(status, message, description) {
this.openDialog({
title: "确认兑换",
message: message,
description: description || "",
confirmText: "确认",
cancelText: "取消",
showCancel: false,
status: status,
action: "close",
});
},
openConfirmDialog() {
if (!this.amountValue) {
this.openResultDialog("error", "兑换失败!", "请输入兑换券数");
return;
}
if (this.amountValue < 100) {
this.openResultDialog(
"error",
"兑换失败!",
"抵用券和消费券总数小于100券的不可兑换",
);
return;
}
if (this.amountValue > this.currentModeBalance) {
this.openResultDialog(
"error",
"兑换失败!",
"当前可用" + this.currentModeLabel + "不足",
);
return;
}
this.openDialog({
title: "确认兑换",
message:
"确认使用" +
this.formatAmount(this.amountValue, 0) +
this.currentModeLabel +
"兑换算力?",
description: "",
confirmText: "确认",
cancelText: "取消",
showCancel: true,
status: "default",
action: "submit",
});
},
async submit() {
if (this.submitting) {
return;
}
this.submitting = true;
try {
await submitAssetPowerExchange({
mode: this.form.mode,
amount: this.amountValue,
}, {
showLoading: true,
loadingText: "兑换中",
});
this.form.amount = "";
await this.loadPage();
this.openResultDialog("success", "兑换成功!", "");
} catch (error) {
this.openResultDialog(
"error",
"兑换失败!",
error.message || "系统繁忙,请稍后再试",
);
} finally {
this.submitting = false;
}
},
handleDialogConfirm() {
if (this.dialog.action === "submit") {
this.closeDialog();
this.submit();
return;
}
this.closeDialog();
},
openLedger() {
uni.navigateTo({
url: "/pages/assets/ledger?type=power",
});
},
},
};
</script>
<style lang="scss" scoped>
@import "../../styles/tokens.scss";
@import "../../styles/common.scss";
.power-page {
min-height: 100vh;
background: #191e32;
}
.power-scroll {
padding: 8rpx 20rpx calc(env(safe-area-inset-bottom) + 36rpx);
}
.panel-card,
.hero-card {
border-radius: 12rpx;
background: #20263e;
box-shadow: 0 12rpx 24rpx rgba(8, 13, 30, 0.12);
}
.hero-card {
position: relative;
min-height: 240rpx;
padding: 24rpx 28rpx;
overflow: hidden;
background: #20263e url("https://imgs.agrimedia.cn/bm-bmt/suanli-bgs.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, #33ddb0 0%, #24c38f 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: 32rpx;
font-weight: 800;
line-height: 1.1;
color: #ffffff;
}
.panel-card {
margin-top: 18rpx;
padding: 0 16rpx;
}
.info-card {
padding-top: 2rpx;
padding-bottom: 14rpx;
}
.info-row {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 104rpx;
border-bottom: 1px solid rgba(136, 148, 193, 0.34);
}
.info-row--last {
border-bottom: 1px solid rgba(136, 148, 193, 0.34);
}
.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;
align-items: baseline;
justify-content: flex-end;
padding-top: 18rpx;
font-size: 28rpx;
color: #1ad296;
}
.info-card__estimate-value {
margin: 0 10rpx;
font-size: 32rpx;
font-weight: 800;
color: #1ad296;
}
.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;
}
.mode-tabs {
display: flex;
width: 400rpx;
margin: 26rpx auto 0;
padding: 4rpx;
border-radius: 999rpx;
background: rgba(102, 115, 160, 0.28);
box-shadow: inset 0 0 0 1px rgba(165, 177, 216, 0.16);
}
.mode-tabs__item {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 60rpx;
border-radius: 999rpx;
font-size: 28rpx;
font-weight: 600;
color: rgba(210, 219, 242, 0.72);
}
.mode-tabs__item.is-active {
background: #02abf1;
color: #ffffff;
}
.form-input {
display: flex;
align-items: center;
margin-top: 34rpx;
padding: 0 20rpx;
border-radius: 10rpx;
background: #191e32;
}
.form-input__field {
flex: 1;
height: 98rpx;
font-size: 30rpx;
font-weight: 700;
color: #ffffff;
}
.form-input__placeholder {
color: rgba(177, 187, 214, 0.42);
}
.form-input__suffix {
margin-left: 16rpx;
font-size: 28rpx;
font-weight: 600;
color: #ffffff;
}
.preview-card {
margin-top: 18rpx;
padding: 14rpx 18rpx;
border-radius: 8rpx;
background: linear-gradient(180deg, #21455a 0%, #1f4358 100%);
}
.preview-card__row {
display: flex;
align-items: center;
justify-content: space-between;
}
.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(229, 252, 255, 0.94);
}
.preview-card__value {
font-size: 28rpx;
font-weight: 800;
color: #1df0b8;
}
.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-weight: 400;
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 {
padding: 36rpx 22rpx 0;
}
.action-button {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 90rpx;
font-weight: 500;
font-size: 30rpx;
color: #ffffff;
border-radius: 46rpx;
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;
}
.action-link {
margin-top: 26rpx;
text-align: center;
font-weight: 500;
font-size: 28rpx;
color: #1fb4ff;
}
</style>

793
pages/assets/transfer.vue Normal file
View File

@ -0,0 +1,793 @@
<template>
<view class="asset-page asset-theme transfer-page">
<asset-page-shell title="转赠中心" />
<view class="asset-scroll transfer-scroll">
<view class="mode-tabs">
<view
v-for="tab in tabs"
:key="tab.value"
class="mode-tabs__item"
:class="{ 'is-active': activeTab === tab.value }"
@click="switchTab(tab.value)"
>
{{ tab.label }}
</view>
</view>
<view class="panel-card transfer-card">
<view class="section-head">
<image
class="section-head__icon"
:src="pageIcon(activeTab === 'power' ? 'power' : 'points')"
mode="aspectFit"
></image>
<text class="section-head__title">{{ currentTabLabel }}转赠</text>
<text class="section-head__meta"
>可用{{ currentBalanceDisplay }}</text
>
</view>
<view class="input-bar">
<input
v-model="form.amount"
class="input-bar__field"
type="number"
:placeholder="amountPlaceholder"
placeholder-class="input-bar__placeholder"
/>
<text class="input-bar__action" @click="fillAll">全部赠送</text>
</view>
<view class="tips-list">
<view
v-for="(tip, index) in currentTips"
:key="tip"
class="tips-list__item"
>
<text class="tips-list__index">{{ index + 1 }}.</text>
<text class="tips-list__text">{{ tip }}</text>
</view>
</view>
</view>
<view class="transfer-split">
<image
class="transfer-split__icon"
src="https://imgs.agrimedia.cn/bm-bmt/z.png"
mode="aspectFit"
></image>
</view>
<view class="panel-card recipient-card">
<view class="section-head">
<view class="recipient-card__title-icon">
<view class="recipient-card__title-head"></view>
<view class="recipient-card__title-body"></view>
</view>
<text class="section-head__title">被赠送人</text>
</view>
<view class="input-bar recipient-search">
<input
v-model="searchId"
class="input-bar__field"
type="number"
placeholder="请输入被赠人ID"
placeholder-class="input-bar__placeholder"
/>
<text class="input-bar__action" @click="queryTarget">查询</text>
</view>
<view
v-if="currentTarget.id"
class="target-card"
@click="selectCurrent"
>
<view class="target-card__top">
<text class="target-card__id">ID: {{ currentTarget.id }}</text>
<view class="target-card__select">
<text class="target-card__select-text">选择</text>
<view class="target-card__select-dot"></view>
</view>
</view>
<view class="target-card__line"></view>
<view class="target-card__bottom">
<view class="target-card__profile">
<view
class="target-card__avatar"
:class="{
'target-card__avatar--image': !!currentTarget.avatar,
}"
>
<image
v-if="currentTarget.avatar"
class="target-card__avatar-image"
:src="currentTarget.avatar"
mode="aspectFill"
></image>
<text v-else class="target-card__avatar-text">{{
targetInitial(currentTarget.nickname)
}}</text>
</view>
<text class="target-card__name">{{
currentTarget.nickname
}}</text>
</view>
<text class="target-card__phone">{{ currentTarget.phone }}</text>
</view>
</view>
</view>
<view class="action-wrap">
<view class="action-button" @click="openConfirm">确认转赠</view>
<view class="action-link" @click="openLedger">转赠记录</view>
</view>
</view>
<asset-confirm-popup
:visible="confirmVisible"
title="确认转赠"
message="确认转赠以下资产给好友?"
confirm-text="确认"
cancel-text="取消"
@cancel="confirmVisible = false"
@confirm="submit"
>
<view class="confirm-detail">
<text class="confirm-detail__summary">
转赠{{ currentTabLabel }}{{ amountDisplay }}
<text class="confirm-detail__summary-sub"
>实际到账{{ receivedDisplay }}</text
>
</text>
<view class="confirm-detail__target">
<text class="confirm-detail__id">ID: {{ currentTarget.id }}</text>
<view class="confirm-detail__line"></view>
<view class="confirm-detail__profile">
<view class="target-card__profile">
<view
class="target-card__avatar"
:class="{
'target-card__avatar--image': !!currentTarget.avatar,
}"
>
<image
v-if="currentTarget.avatar"
class="target-card__avatar-image"
:src="currentTarget.avatar"
mode="aspectFill"
></image>
<text v-else class="target-card__avatar-text">{{
targetInitial(currentTarget.nickname)
}}</text>
</view>
<text class="target-card__name">{{
currentTarget.nickname
}}</text>
</view>
<text class="target-card__phone">{{ currentTarget.phone }}</text>
</view>
</view>
<text class="confirm-detail__desc">
请仔细校对被赠送账号信息转赠成功后无法追回
</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 {
fetchTransferDetail,
searchTransferUser,
submitAssetTransfer,
} from "../../api/assets";
export default {
components: {
AssetConfirmPopup,
AssetPageShell,
},
data() {
return {
tabs: [
{
label: "积分转赠",
value: "points",
},
{
label: "算力转赠",
value: "power",
},
],
activeTab: "points",
targets: [],
selectedIndex: 0,
balances: {
points: "0",
power: "0",
},
feePercent: 10,
tips: {
points: [],
power: [],
},
form: {
amount: "",
},
searchId: "",
confirmVisible: false,
submitting: false,
};
},
onLoad(options) {
if (options && options.mode === "power") {
this.activeTab = "power";
}
this.loadPage(true);
},
computed: {
currentTabLabel() {
return this.activeTab === "power" ? "算力" : "积分";
},
currentBalance() {
return Number(this.balances[this.activeTab] || 0);
},
currentBalanceDisplay() {
return this.formatAmount(this.currentBalance, 0);
},
currentTips() {
const currentTips = this.tips[this.activeTab] || [];
if (currentTips.length) {
return currentTips;
}
if (this.activeTab === "power") {
return [
"只能转赠1的整数倍",
"转赠系统会扣除" + this.feePercent + "%的手续费",
];
}
return [
"只能转赠100的整数倍",
"凌晨0点-凌晨01点系统维护不可赠送",
"转赠系统会扣除" + this.feePercent + "%的手续费",
];
},
currentTarget() {
return this.targets[this.selectedIndex] || {};
},
amountValue() {
return Number(this.form.amount || 0);
},
amountDisplay() {
return this.formatAmount(this.amountValue, 0);
},
receivedDisplay() {
const received = this.amountValue
? this.amountValue * (1 - this.feePercent / 100)
: 0;
return this.formatAmount(received, 0);
},
amountPlaceholder() {
return this.activeTab === "power"
? "请输入1的整数倍"
: "请输入100的整数倍";
},
},
methods: {
pageIcon(type) {
const iconMap = {
power: "https://imgs.agrimedia.cn/bm-bmt/s.png",
bmt: "https://imgs.agrimedia.cn/bm-bmt/b.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.png",
points: "https://imgs.agrimedia.cn/bm-bmt/j.png",
coupon: "https://imgs.agrimedia.cn/bm-bmt/xiaofei-icon.png",
};
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, ",");
},
targetInitial(name) {
return String(name || "友").slice(0, 1);
},
async loadPage(showLoading) {
try {
const result = await fetchTransferDetail(
showLoading
? {
showLoading: true,
loadingText: "加载中",
}
: null,
);
this.balances = result.balances || this.balances;
this.feePercent = Number(result.feePercent || 10);
this.tips = result.tips || this.tips;
} catch (error) {
uni.showToast({
title: error.message || "转赠页加载失败",
icon: "none",
});
}
},
switchTab(value) {
this.activeTab = value;
this.form.amount = "";
},
fillAll() {
this.form.amount = String(this.currentBalance || 0);
},
async queryTarget() {
const keyword = String(this.searchId || "").trim();
if (!keyword) {
uni.showToast({
title: "请输入被赠人ID",
icon: "none",
});
return;
}
try {
const target = await searchTransferUser(keyword);
this.targets = [target];
this.selectedIndex = 0;
this.searchId = target.id;
} catch (error) {
uni.showToast({
title: error.message || "未查询到好友",
icon: "none",
});
}
},
selectCurrent() {
if (this.currentTarget && this.currentTarget.id) {
this.searchId = this.currentTarget.id;
}
},
openConfirm() {
if (!this.currentTarget.id) {
uni.showToast({
title: "请先查询被赠送人",
icon: "none",
});
return;
}
if (!this.amountValue) {
uni.showToast({
title: "请输入转赠数量",
icon: "none",
});
return;
}
if (this.amountValue > this.currentBalance) {
uni.showToast({
title: "可用数量不足",
icon: "none",
});
return;
}
if (this.activeTab === "points" && this.amountValue % 100 !== 0) {
uni.showToast({
title: "积分只能转赠100的整数倍",
icon: "none",
});
return;
}
if (this.activeTab === "power" && !Number.isInteger(this.amountValue)) {
uni.showToast({
title: "算力只能转赠1的整数倍",
icon: "none",
});
return;
}
this.confirmVisible = true;
},
async submit() {
if (this.submitting) {
return;
}
this.submitting = true;
try {
const result = await submitAssetTransfer({
type: this.activeTab,
targetId: this.currentTarget.id,
amount: this.amountValue,
}, {
showLoading: true,
loadingText: "转赠中",
});
this.confirmVisible = false;
uni.showToast({
title: "转赠成功,到账 " + result.received,
icon: "none",
});
this.form.amount = "";
this.loadPage();
} catch (error) {
uni.showToast({
title: error.message || "转赠失败",
icon: "none",
});
} finally {
this.submitting = false;
}
},
openLedger() {
uni.navigateTo({
url: "/pages/assets/ledger?type=transfer",
});
},
},
};
</script>
<style lang="scss" scoped>
@import "../../styles/tokens.scss";
@import "../../styles/common.scss";
.transfer-page {
min-height: 100vh;
background: #191e32;
}
.transfer-scroll {
padding: 10rpx 22rpx calc(env(safe-area-inset-bottom) + 36rpx);
}
.mode-tabs {
display: flex;
width: 400rpx;
margin: 26rpx auto 0;
padding: 4rpx;
border-radius: 999rpx;
background: rgba(102, 115, 160, 0.28);
box-shadow: inset 0 0 0 1px rgba(165, 177, 216, 0.16);
}
.mode-tabs__item {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 60rpx;
border-radius: 999rpx;
font-size: 28rpx;
font-weight: 600;
color: rgba(210, 219, 242, 0.72);
}
.mode-tabs__item.is-active {
background: #02abf1;
color: #ffffff;
}
.panel-card {
margin-top: 34rpx;
padding: 22rpx 20rpx 24rpx;
border-radius: 12rpx;
background: #242944;
box-shadow: 0 12rpx 24rpx rgba(8, 13, 30, 0.12);
}
.section-head {
display: flex;
align-items: center;
}
.section-head__icon {
width: 28rpx;
height: 28rpx;
flex-shrink: 0;
}
.section-head__title {
margin-left: 20rpx;
font-size: 28rpx;
font-weight: 700;
color: #ffffff;
}
.section-head__meta {
margin-left: 12rpx;
font-size: 26rpx;
color: #9ba7ce;
}
.input-bar {
display: flex;
align-items: center;
margin-top: 22rpx;
padding: 0 18rpx;
border-radius: 10rpx;
background: #191e32;
}
.input-bar__field {
flex: 1;
height: 84rpx;
font-size: 28rpx;
color: #ffffff;
}
.input-bar__placeholder {
color: rgba(170, 179, 204, 0.66);
}
.input-bar__action {
margin-left: 20rpx;
font-size: 28rpx;
font-weight: 600;
color: #20b6f5;
}
.tips-list {
margin-top: 20rpx;
}
.tips-list__item {
display: flex;
margin-top: 10rpx;
}
.tips-list__index,
.tips-list__text {
font-size: 24rpx;
color: #929dbf;
line-height: 44rpx;
color: rgba(175, 184, 211, 0.92);
}
.tips-list__index {
margin-right: 6rpx;
}
.tips-list__text {
flex: 1;
}
.transfer-split {
display: flex;
align-items: center;
justify-content: center;
margin-top: 18rpx;
}
.transfer-split__icon {
width: 36rpx;
height: 36rpx;
transform: rotate(90deg);
}
.recipient-card {
margin-top: 18rpx;
}
.recipient-card__title-icon {
position: relative;
width: 28rpx;
height: 28rpx;
flex-shrink: 0;
}
.recipient-card__title-head {
position: absolute;
left: 8rpx;
top: 0;
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #18aef2;
}
.recipient-card__title-body {
position: absolute;
left: 3rpx;
bottom: 2rpx;
width: 22rpx;
height: 14rpx;
border-radius: 12rpx 12rpx 8rpx 8rpx;
background: #18aef2;
}
.recipient-search {
margin-top: 24rpx;
}
.target-card {
margin-top: 18rpx;
padding: 18rpx 18rpx 16rpx;
border-radius: 10rpx;
background: #191e32;
font-size: 28rpx;
}
.target-card__top {
display: flex;
align-items: center;
justify-content: space-between;
}
.target-card__id {
font-weight: 600;
color: #ffffff;
}
.target-card__select {
display: flex;
align-items: center;
}
.target-card__select-text {
margin-right: 12rpx;
font-size: 28rpx;
color: rgba(244, 248, 255, 0.88);
}
.target-card__select-dot {
display: flex;
align-items: center;
justify-content: center;
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: #20b6f5;
font-size: 16rpx;
font-weight: 700;
color: #ffffff;
}
.target-card__line {
height: 1px;
margin: 18rpx 0 16rpx;
background: rgba(197, 206, 233, 0.3);
}
.target-card__bottom,
.confirm-detail__profile {
display: flex;
align-items: center;
justify-content: space-between;
}
.target-card__profile {
display: flex;
align-items: center;
}
.target-card__avatar {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: linear-gradient(135deg, #ffbf86 0%, #f38b66 100%);
overflow: hidden;
flex-shrink: 0;
}
.target-card__avatar--image {
background: transparent;
}
.target-card__avatar-image {
width: 100%;
height: 100%;
}
.target-card__avatar-text {
font-weight: 700;
color: #ffffff;
}
.target-card__name {
margin-left: 16rpx;
color: #ffffff;
}
.target-card__phone {
margin-left: 18rpx;
color: rgba(244, 248, 255, 0.88);
}
.action-wrap {
padding: 86rpx 20rpx 0;
}
.action-button {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 90rpx;
font-weight: 500;
font-size: 30rpx;
color: #ffffff;
border-radius: 46rpx;
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;
}
.action-link {
margin-top: 28rpx;
text-align: center;
font-weight: 500;
font-size: 28rpx;
color: #1fb4ff;
}
.confirm-detail__summary {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
font-weight: 700;
text-align: center;
color: #ff8b5d;
}
.confirm-detail__summary-sub {
color: #ff8b5d;
}
.confirm-detail__target {
margin-top: 18rpx;
padding: 18rpx;
border-radius: 10rpx;
background: #191e32;
}
.confirm-detail__id {
font-size: 22rpx;
font-weight: 600;
color: #ffffff;
}
.confirm-detail__line {
height: 1px;
margin: 18rpx 0 16rpx;
background: rgba(197, 206, 233, 0.3);
}
.confirm-detail__desc {
display: block;
margin-top: 18rpx;
font-size: 18rpx;
line-height: 1.65;
color: rgba(186, 194, 218, 0.88);
}
</style>

View File

@ -0,0 +1,367 @@
<template>
<view class="asset-page asset-theme wallet-form-page">
<asset-page-shell title="钱包地址" />
<view class="asset-scroll wallet-form-scroll">
<view class="wallet-form-panel">
<view class="wallet-form-panel__head">
<view class="wallet-form-panel__title">
<view class="wallet-form-panel__title-icon">
<view class="wallet-form-panel__title-tab"></view>
<view class="wallet-form-panel__title-body"></view>
</view>
<text class="wallet-form-panel__title-text">当前钱包地址</text>
</view>
<text class="wallet-form-panel__subtitle">我在交易所的钱包地址</text>
</view>
<view class="wallet-input">
<input
v-model="form.address"
class="wallet-input__field"
type="text"
:placeholder="form.id ? '粘贴新钱包地址' : '粘贴新钱包地址'"
placeholder-class="wallet-input__placeholder"
/>
<text class="wallet-input__paste" @click="paste">粘贴</text>
</view>
<text class="wallet-form-panel__helper">
钱包地址请在海南农综APP中获取
</text>
</view>
<view class="wallet-form-actions">
<view class="wallet-button wallet-button--primary" @click="openConfirm">
保存
</view>
<view
class="wallet-button wallet-button--secondary"
@click="handleBack"
>
返回
</view>
</view>
</view>
<wallet-action-popup
:visible="confirmVisible"
:title="form.id ? '修改钱包' : '新增钱包'"
:message="form.id ? '确认修改钱包新地址' : '确认保存钱包地址'"
:address="trimmedAddress"
description="请仔细核对钱包地址,以免造成财产损失"
confirm-text="确认"
cancel-text="取消"
@cancel="confirmVisible = false"
@confirm="submit"
/>
<view v-if="successVisible" class="wallet-success">
<image
class="wallet-success__icon"
src="https://imgs.agrimedia.cn/bm-bmt/success.png"
mode="aspectFit"
></image>
<text class="wallet-success__text">{{
form.id ? "修改成功!" : "保存成功!"
}}</text>
</view>
</view>
</template>
<script>
import AssetPageShell from "../../components/asset-page-shell.vue";
import WalletActionPopup from "../../components/wallet-action-popup.vue";
import { fetchWalletDetail, saveAssetWallet } from "../../api/assets";
export default {
components: {
AssetPageShell,
WalletActionPopup,
},
data() {
return {
form: {
id: "",
name: "海南农综交易所",
address: "",
},
confirmVisible: false,
successVisible: false,
submitting: false,
};
},
onLoad(options) {
this.form.id = (options && options.id) || "";
this.loadPage(true);
},
computed: {
trimmedAddress() {
return String(this.form.address || "").trim();
},
},
methods: {
async loadPage(showLoading) {
try {
const result = await fetchWalletDetail(
showLoading
? {
showLoading: true,
loadingText: "加载中",
}
: null,
);
if (!this.form.id) {
return;
}
for (let i = 0; i < result.wallets.length; i += 1) {
if (result.wallets[i].id === this.form.id) {
this.form.name = result.wallets[i].name;
this.form.address = result.wallets[i].address;
}
}
} catch (error) {
uni.showToast({
title: error.message || "页面加载失败",
icon: "none",
});
}
},
handleBack() {
if (getCurrentPages().length > 1) {
uni.navigateBack();
return;
}
uni.reLaunch({
url: "/pages/index/index",
});
},
paste() {
const that = this;
uni.getClipboardData({
success: function (result) {
that.form.address = result.data || "";
},
});
},
openConfirm() {
if (!this.trimmedAddress) {
uni.showToast({
title: "请先粘贴钱包地址",
icon: "none",
});
return;
}
this.confirmVisible = true;
},
async submit() {
if (this.submitting) {
return;
}
this.submitting = true;
try {
await saveAssetWallet({
id: this.form.id,
name: this.form.name,
address: this.trimmedAddress,
}, {
showLoading: true,
loadingText: "保存中",
});
this.confirmVisible = false;
this.successVisible = true;
setTimeout(() => {
this.successVisible = false;
this.handleBack();
}, 700);
} catch (error) {
uni.showToast({
title: error.message || "保存失败",
icon: "none",
});
} finally {
this.submitting = false;
}
},
},
};
</script>
<style lang="scss" scoped>
@import "../../styles/tokens.scss";
@import "../../styles/common.scss";
.wallet-form-page {
min-height: 100vh;
background: #191e32;
}
.wallet-form-scroll {
min-height: calc(100vh - env(safe-area-inset-top) - 104rpx);
display: flex;
flex-direction: column;
padding: 10rpx 14rpx calc(env(safe-area-inset-bottom) + 36rpx);
}
.wallet-form-panel {
padding: 22rpx 20rpx 24rpx;
border-radius: 12rpx;
background: #242944;
box-shadow: 0 12rpx 24rpx rgba(8, 13, 30, 0.12);
}
.wallet-form-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
}
.wallet-form-panel__title {
display: flex;
align-items: center;
min-width: 0;
}
.wallet-form-panel__title-icon {
position: relative;
width: 34rpx;
height: 28rpx;
margin-right: 10rpx;
flex-shrink: 0;
}
.wallet-form-panel__title-tab {
position: absolute;
left: 5rpx;
top: -4rpx;
width: 16rpx;
height: 10rpx;
border-radius: 6rpx 6rpx 0 0;
background: #b48eff;
}
.wallet-form-panel__title-body {
position: absolute;
left: 0;
bottom: 0;
width: 34rpx;
height: 22rpx;
border-radius: 6rpx;
background: linear-gradient(135deg, #b48eff 0%, #8c6ef0 100%);
}
.wallet-form-panel__title-text {
font-size: 26rpx;
font-weight: 700;
color: #ffffff;
}
.wallet-form-panel__subtitle {
margin-left: 16rpx;
max-width: 280rpx;
font-size: 22rpx;
line-height: 1.4;
text-align: right;
flex-shrink: 0;
color: rgba(174, 183, 211, 0.88);
}
.wallet-input {
display: flex;
align-items: center;
margin-top: 18rpx;
padding: 0 20rpx;
height: 94rpx;
border-radius: 10rpx;
background: #191e32;
}
.wallet-input__field {
flex: 1;
min-width: 0;
height: 94rpx;
font-size: 22rpx;
color: #ffffff;
}
.wallet-input__placeholder {
color: rgba(154, 163, 197, 0.82);
}
.wallet-input__paste {
margin-left: 18rpx;
font-size: 24rpx;
font-weight: 700;
color: #1fb7f9;
}
.wallet-form-panel__helper {
display: block;
margin-top: 18rpx;
font-size: 22rpx;
line-height: 1.6;
color: rgba(174, 183, 211, 0.82);
}
.wallet-form-actions {
display: flex;
gap: 20rpx;
margin-top: 180rpx;
padding: 0 10rpx;
}
.wallet-button {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 92rpx;
border-radius: 999rpx;
font-size: 32rpx;
font-weight: 700;
}
.wallet-button--primary {
background: linear-gradient(135deg, #22c2ff 0%, #159de6 100%);
box-shadow: 0 16rpx 30rpx rgba(31, 169, 243, 0.2);
color: #ffffff;
}
.wallet-button--secondary {
border: 1px solid rgba(154, 164, 200, 0.64);
background: rgba(67, 77, 118, 0.92);
color: #ffffff;
}
.wallet-success {
position: fixed;
left: 54rpx;
right: 54rpx;
bottom: 250rpx;
z-index: 90;
display: flex;
align-items: center;
justify-content: center;
padding: 24rpx 28rpx;
border-radius: 20rpx;
background: #242944;
box-shadow: 0 24rpx 60rpx rgba(0, 0, 0, 0.26);
}
.wallet-success__icon {
width: 44rpx;
height: 44rpx;
margin-right: 16rpx;
}
.wallet-success__text {
font-size: 26rpx;
font-weight: 700;
color: #ffffff;
}
</style>

371
pages/assets/wallet.vue Normal file
View File

@ -0,0 +1,371 @@
<template>
<view class="asset-page asset-theme wallet-page">
<asset-page-shell title="钱包地址" />
<view class="asset-scroll wallet-scroll">
<view class="wallet-panel">
<view class="wallet-panel__head">
<view class="wallet-panel__title">
<view class="wallet-panel__title-icon">
<view class="wallet-panel__title-tab"></view>
<view class="wallet-panel__title-body"></view>
</view>
<text class="wallet-panel__title-text">当前钱包地址</text>
</view>
<text class="wallet-panel__subtitle">我在交易所的钱包地址</text>
</view>
<view v-if="currentWallet" class="wallet-panel__content">
<view class="wallet-panel__address-box">{{
currentWallet.address
}}</view>
<view class="wallet-panel__footer">
<text class="wallet-panel__name">{{ currentWallet.name }}</text>
<text
class="wallet-panel__copy"
@click="copy(currentWallet.address)"
>复制地址</text
>
</view>
</view>
<view v-else class="wallet-panel__empty">
<text class="wallet-panel__empty-title">暂未设置钱包地址</text>
<text class="wallet-panel__empty-desc">
新增后即可用于 BMT 提取和后续交易所对接
</text>
</view>
</view>
<view class="wallet-actions">
<view class="wallet-button wallet-button--primary" @click="openForm()">
{{ currentWallet ? "修改钱包地址" : "新增钱包地址" }}
</view>
<view
class="wallet-button wallet-button--secondary"
@click="handleBack"
>
返回
</view>
</view>
<text
v-if="currentWallet"
class="wallet-delete"
@click="openDeleteDialog"
>
删除地址
</text>
</view>
<wallet-action-popup
:visible="deleteDialogVisible"
title="删除钱包"
message="确认删除钱包地址"
:address="currentWallet ? currentWallet.address : ''"
confirm-text="确认"
cancel-text="取消"
@cancel="deleteDialogVisible = false"
@confirm="remove"
/>
</view>
</template>
<script>
import AssetPageShell from "../../components/asset-page-shell.vue";
import WalletActionPopup from "../../components/wallet-action-popup.vue";
import { fetchWalletDetail, deleteAssetWallet } from "../../api/assets";
export default {
components: {
AssetPageShell,
WalletActionPopup,
},
data() {
return {
hasShown: false,
wallets: [],
deleteDialogVisible: false,
removing: false,
};
},
onLoad() {
this.loadPage(true);
},
onShow() {
if (this.hasShown) {
this.loadPage();
return;
}
this.hasShown = true;
},
computed: {
currentWallet() {
for (let i = 0; i < this.wallets.length; i += 1) {
if (this.wallets[i].isDefault) {
return this.wallets[i];
}
}
return this.wallets[0] || null;
},
},
methods: {
async loadPage(showLoading) {
try {
const result = await fetchWalletDetail(
showLoading
? {
showLoading: true,
loadingText: "加载中",
}
: null,
);
this.wallets = result.wallets || [];
} catch (error) {
uni.showToast({
title: error.message || "钱包加载失败",
icon: "none",
});
}
},
handleBack() {
if (getCurrentPages().length > 1) {
uni.navigateBack();
return;
}
uni.reLaunch({
url: "/pages/index/index",
});
},
copy(address) {
uni.setClipboardData({
data: address,
success() {
uni.showToast({
title: "已复制",
icon: "none",
});
},
});
},
openForm() {
let url = "/pages/assets/wallet-form";
if (this.currentWallet && this.currentWallet.id) {
url += "?id=" + this.currentWallet.id;
}
uni.navigateTo({
url: url,
});
},
openDeleteDialog() {
if (!this.currentWallet) {
return;
}
this.deleteDialogVisible = true;
},
async remove() {
if (!this.currentWallet) {
return;
}
if (this.removing) {
return;
}
this.removing = true;
try {
await deleteAssetWallet(this.currentWallet.id, {
showLoading: true,
loadingText: "删除中",
});
this.deleteDialogVisible = false;
uni.showToast({
title: "删除成功",
icon: "none",
});
this.loadPage();
} catch (error) {
uni.showToast({
title: error.message || "删除失败",
icon: "none",
});
} finally {
this.removing = false;
}
},
},
};
</script>
<style lang="scss" scoped>
@import "../../styles/tokens.scss";
@import "../../styles/common.scss";
.wallet-page {
min-height: 100vh;
background: #191e32;
}
.wallet-scroll {
min-height: calc(100vh - env(safe-area-inset-top) - 104rpx);
display: flex;
flex-direction: column;
padding: 10rpx 14rpx calc(env(safe-area-inset-bottom) + 36rpx);
}
.wallet-panel {
padding: 22rpx 20rpx 24rpx;
border-radius: 12rpx;
background: #242944;
box-shadow: 0 12rpx 24rpx rgba(8, 13, 30, 0.12);
}
.wallet-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
}
.wallet-panel__title {
display: flex;
align-items: center;
min-width: 0;
}
.wallet-panel__title-icon {
position: relative;
width: 34rpx;
height: 28rpx;
margin-right: 10rpx;
flex-shrink: 0;
}
.wallet-panel__title-tab {
position: absolute;
left: 5rpx;
top: -4rpx;
width: 16rpx;
height: 10rpx;
border-radius: 6rpx 6rpx 0 0;
background: #b48eff;
}
.wallet-panel__title-body {
position: absolute;
left: 0;
bottom: 0;
width: 34rpx;
height: 22rpx;
border-radius: 6rpx;
background: linear-gradient(135deg, #b48eff 0%, #8c6ef0 100%);
}
.wallet-panel__title-text {
font-size: 28rpx;
font-weight: 700;
color: #ffffff;
}
.wallet-panel__subtitle {
margin-left: 16rpx;
max-width: 280rpx;
font-size: 26rpx;
line-height: 1.4;
text-align: right;
flex-shrink: 0;
color: rgba(174, 183, 211, 0.88);
}
.wallet-panel__content,
.wallet-panel__empty {
margin-top: 18rpx;
}
.wallet-panel__address-box {
padding: 30rpx 24rpx;
border-radius: 8rpx;
background: rgba(53, 62, 94, 1);
font-size: 26rpx;
line-height: 1.6;
word-break: break-all;
color: #ffffff;
}
.wallet-panel__footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20rpx;
}
.wallet-panel__name {
font-size: 26rpx;
color: rgba(196, 205, 231, 0.88);
}
.wallet-panel__copy {
font-size: 26rpx;
font-weight: 700;
color: #1fb7f9;
}
.wallet-panel__empty-title {
display: block;
font-size: 26rpx;
font-weight: 700;
color: #ffffff;
}
.wallet-panel__empty-desc {
display: block;
margin-top: 12rpx;
font-size: 22rpx;
line-height: 1.7;
color: rgba(174, 183, 211, 0.82);
}
.wallet-actions {
display: flex;
gap: 20rpx;
margin-top: 180rpx;
padding: 0 10rpx;
}
.wallet-button {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 92rpx;
border-radius: 999rpx;
font-size: 32rpx;
font-weight: 700;
}
.wallet-button--primary {
background: linear-gradient(135deg, #22c2ff 0%, #159de6 100%);
box-shadow: 0 16rpx 30rpx rgba(31, 169, 243, 0.2);
color: #ffffff;
}
.wallet-button--secondary {
border: 1px solid rgba(154, 164, 200, 0.64);
background: rgba(67, 77, 118, 0.92);
color: #ffffff;
}
.wallet-delete {
margin-top: auto;
padding: 140rpx 0 10rpx;
text-align: center;
font-size: 28rpx;
font-weight: 700;
color: #1fb7f9;
}
</style>

667
pages/assets/withdraw.vue Normal file
View File

@ -0,0 +1,667 @@
<template>
<view class="asset-page asset-theme withdraw-page">
<asset-page-shell title="提取BMT" />
<view class="asset-scroll withdraw-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="https://imgs.agrimedia.cn/bm-bmt/b-w.png"
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">{{ displayWithdrawable }}</text>
<text class="hero-card__price"
>(BMT实时价格 {{ displayPrice }}CNY/BMT)</text
>
</view>
</view>
<view class="panel-card amount-card">
<view class="section-head">
<image
class="section-head__icon"
:src="pageIcon('bmt')"
mode="aspectFit"
></image>
<text class="section-head__title">BMT提取数量</text>
</view>
<view class="amount-input">
<input
v-model="form.amount"
class="amount-input__field"
type="number"
placeholder="请输入提取数量"
placeholder-class="amount-input__placeholder"
/>
<text class="amount-input__action" @click="fillAll">全部提取</text>
</view>
<view class="fee-card">
<text class="fee-card__label">提取手续费1%</text>
<text class="fee-card__value">{{ feeText }}</text>
</view>
</view>
<view class="panel-card wallet-card">
<view class="wallet-card__head">
<view class="section-head">
<view class="wallet-card__title-icon">
<view class="wallet-card__title-tab"></view>
<view class="wallet-card__title-body"></view>
</view>
<text class="section-head__title">钱包地址</text>
</view>
<text class="wallet-card__link" @click="openWallet"
>修改钱包地址</text
>
</view>
<view v-if="detail.defaultWallet" class="wallet-box">
<view class="wallet-box__head">
<text class="wallet-box__name">{{
detail.defaultWallet.name
}}</text>
<view class="wallet-box__actions">
<text class="wallet-box__action" @click="toggleWalletVisible">{{
walletVisible ? "隐藏" : "显示"
}}</text>
<text class="wallet-box__action" @click="copyWallet">复制</text>
</view>
</view>
<text class="wallet-box__address">{{ walletDisplayAddress }}</text>
</view>
<view v-else class="wallet-box wallet-box--empty" @click="openWallet">
<text class="wallet-box__plus"></text>
<text class="wallet-box__empty-text">添加钱包地址</text>
</view>
<text class="wallet-card__desc">
请仔细核对钱包地址一经转出将无法追回
</text>
</view>
<view class="action-wrap">
<view class="action-button" @click="openConfirm">确定</view>
<view class="action-link" @click="openLedger">提取记录</view>
</view>
</view>
<asset-confirm-popup
:visible="confirmVisible"
title="确认提取"
:message="'确认提取 ' + amountDisplay + ' BMT 到以下钱包吗?'"
confirm-text="确认"
cancel-text="取消"
@cancel="confirmVisible = false"
@confirm="submit"
>
<view class="popup-detail">
<view class="popup-detail__row">
<text class="meta-pair__label">钱包地址</text>
<text class="meta-pair__value popup-address">{{
detail.defaultWallet ? detail.defaultWallet.address : "未设置"
}}</text>
</view>
<view class="popup-detail__row">
<text class="meta-pair__label">手续费</text>
<text class="meta-pair__value">{{ feeText }}</text>
</view>
<view class="popup-detail__row">
<text class="meta-pair__label">预计到账</text>
<text class="meta-pair__value">{{ actualText }}</text>
</view>
</view>
</asset-confirm-popup>
</view>
</template>
<script>
import AssetConfirmPopup from "../../components/asset-confirm-popup.vue";
import AssetPageShell from "../../components/asset-page-shell.vue";
import { fetchWithdrawDetail, submitAssetWithdraw } from "../../api/assets";
export default {
components: {
AssetConfirmPopup,
AssetPageShell,
},
data() {
return {
detail: {
ticker: {},
withdrawableBmt: "0",
defaultWallet: null,
},
hasShown: false,
form: {
amount: "",
},
confirmVisible: false,
walletVisible: false,
submitting: false,
};
},
onLoad() {
this.loadPage(true);
},
onShow() {
if (this.hasShown) {
this.loadPage();
return;
}
this.hasShown = true;
},
computed: {
amountValue() {
return Number(this.form.amount || 0);
},
amountDisplay() {
return this.formatAmount(this.amountValue, 2);
},
feeText() {
return this.amountValue
? this.formatAmount(this.amountValue * 0.01, 2) + " BMT"
: "0.00 BMT";
},
actualText() {
return this.amountValue
? this.formatAmount(this.amountValue * 0.99, 2) + " BMT"
: "0.00 BMT";
},
displayWithdrawable() {
return this.formatAmount(this.detail.withdrawableBmt, 2);
},
displayPrice() {
const price = Number(
this.detail.ticker.cnyPrice || this.detail.ticker.close || 0,
);
return price ? price.toFixed(2) : "0.00";
},
walletDisplayAddress() {
if (!this.detail.defaultWallet || !this.detail.defaultWallet.address) {
return "";
}
const address = this.detail.defaultWallet.address;
if (this.walletVisible || address.length <= 16) {
return address;
}
return address.slice(0, 8) + "..." + address.slice(-8);
},
},
methods: {
pageIcon(type) {
const iconMap = {
power: "https://imgs.agrimedia.cn/bm-bmt/s.png",
bmt: "https://imgs.agrimedia.cn/bm-bmt/b.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.png",
points: "https://imgs.agrimedia.cn/bm-bmt/j.png",
coupon: "https://imgs.agrimedia.cn/bm-bmt/xiaofei-icon.png",
};
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, ",");
},
async loadPage(showLoading) {
try {
this.detail = await fetchWithdrawDetail(
showLoading
? {
showLoading: true,
loadingText: "加载中",
}
: null,
);
this.walletVisible = false;
} catch (error) {
uni.showToast({
title: error.message || "页面加载失败",
icon: "none",
});
}
},
fillAll() {
this.form.amount = this.detail.withdrawableBmt || "0";
},
toggleWalletVisible() {
this.walletVisible = !this.walletVisible;
},
copyWallet() {
if (!this.detail.defaultWallet || !this.detail.defaultWallet.address) {
return;
}
uni.setClipboardData({
data: this.detail.defaultWallet.address,
showToast: false,
success: () => {
uni.showToast({
title: "钱包地址已复制",
icon: "none",
});
},
});
},
openConfirm() {
if (!this.detail.defaultWallet) {
uni.showToast({
title: "请先添加钱包地址",
icon: "none",
});
return;
}
if (!this.amountValue) {
uni.showToast({
title: "请输入提取数量",
icon: "none",
});
return;
}
if (this.amountValue > Number(this.detail.withdrawableBmt || 0)) {
uni.showToast({
title: "可提取BMT不足",
icon: "none",
});
return;
}
this.confirmVisible = true;
},
async submit() {
if (this.submitting) {
return;
}
if (!this.detail.defaultWallet) {
uni.showToast({
title: "请先添加钱包地址",
icon: "none",
});
return;
}
if (!this.amountValue) {
uni.showToast({
title: "请输入提取数量",
icon: "none",
});
return;
}
this.submitting = true;
try {
await submitAssetWithdraw({
amount: this.amountValue,
walletId: this.detail.defaultWallet.id,
}, {
showLoading: true,
loadingText: "提交中",
});
this.confirmVisible = false;
this.form.amount = "";
uni.showToast({
title: "提取申请成功",
icon: "none",
});
this.loadPage();
} catch (error) {
uni.showToast({
title: error.message || "提取失败",
icon: "none",
});
} finally {
this.submitting = false;
}
},
openWallet() {
uni.navigateTo({
url: "/pages/assets/wallet",
});
},
openLedger() {
uni.navigateTo({
url: "/pages/assets/ledger?type=withdraw",
});
},
},
};
</script>
<style lang="scss" scoped>
@import "../../styles/tokens.scss";
@import "../../styles/common.scss";
.withdraw-page {
min-height: 100vh;
background: #191e32;
}
.withdraw-scroll {
padding: 8rpx 14rpx calc(env(safe-area-inset-bottom) + 36rpx);
}
.panel-card,
.hero-card {
border-radius: 12rpx;
background: #242944;
box-shadow: 0 12rpx 24rpx rgba(8, 13, 30, 0.12);
}
.hero-card {
position: relative;
min-height: 240rpx;
padding: 24rpx 28rpx;
overflow: hidden;
background: #242944 url("https://imgs.agrimedia.cn/bm-bmt/qianbao-bg.png")
no-repeat top / 100% 240rpx;
background-size: cover;
}
.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: 42rpx;
font-weight: 800;
line-height: 1.1;
color: #ffffff;
}
.hero-card__price {
margin-top: 14rpx;
font-size: 28rpx;
color: #29dca3;
}
.panel-card {
margin-top: 18rpx;
padding: 22rpx 20rpx 24rpx;
}
.section-head {
display: flex;
align-items: center;
}
.section-head__icon {
width: 28rpx;
height: 28rpx;
flex-shrink: 0;
}
.section-head__title {
margin-left: 14rpx;
font-size: 28rpx;
font-weight: 700;
color: #ffffff;
}
.amount-input {
display: flex;
align-items: center;
margin-top: 22rpx;
padding: 0 20rpx;
border-radius: 10rpx;
background: rgba(25, 30, 50, 1);
}
.amount-input__field {
flex: 1;
height: 84rpx;
font-size: 36rpx;
font-weight: 700;
color: #ffffff;
}
.amount-input__placeholder {
color: rgba(170, 179, 204, 0.66);
font-size: 28rpx;
}
.amount-input__action {
margin-left: 20rpx;
font-size: 28rpx;
font-weight: 500;
color: #20b6f5;
}
.fee-card {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 18rpx;
padding: 16rpx 20rpx;
border-radius: 8rpx;
background: rgba(96, 67, 88, 0.44);
}
.fee-card__label {
font-size: 26rpx;
color: #ff9367;
}
.fee-card__value {
font-size: 24rpx;
font-weight: 700;
color: #ff9367;
}
.wallet-card__head {
display: flex;
align-items: center;
justify-content: space-between;
}
.wallet-card__title-icon {
position: relative;
width: 34rpx;
height: 28rpx;
margin-right: 2rpx;
flex-shrink: 0;
}
.wallet-card__title-tab {
position: absolute;
left: 5rpx;
top: -4rpx;
width: 16rpx;
height: 10rpx;
border-radius: 6rpx 6rpx 0 0;
background: #b48eff;
}
.wallet-card__title-body {
position: absolute;
left: 0;
bottom: 0;
width: 34rpx;
height: 22rpx;
border-radius: 6rpx;
background: linear-gradient(135deg, #b48eff 0%, #8c6ef0 100%);
}
.wallet-card__link {
font-size: 26rpx;
font-weight: 600;
color: #20b6f5;
}
.wallet-box {
margin-top: 18rpx;
padding: 18rpx 20rpx;
border-radius: 8rpx;
background: #353e5e;
}
.wallet-box__head {
display: flex;
align-items: center;
justify-content: space-between;
}
.wallet-box__name {
font-size: 26rpx;
color: rgba(232, 239, 255, 0.88);
}
.wallet-box__actions {
display: flex;
align-items: center;
}
.wallet-box__action {
margin-left: 24rpx;
font-size: 26rpx;
font-weight: 400;
color: #20b6f5;
}
.wallet-box__address {
margin-top: 18rpx;
font-size: 26rpx;
line-height: 1.6;
color: #ffffff;
word-break: break-all;
}
.wallet-box--empty {
display: flex;
align-items: center;
justify-content: center;
min-height: 120rpx;
background: #353e5e;
}
.wallet-box__plus {
margin-right: 14rpx;
font-size: 46rpx;
line-height: 1;
color: #20b6f5;
}
.wallet-box__empty-text {
font-size: 22rpx;
font-weight: 600;
color: #20b6f5;
}
.wallet-card__desc {
display: block;
margin-top: 22rpx;
font-size: 26rpx;
line-height: 1.65;
color: rgba(170, 179, 204, 0.9);
}
.action-wrap {
padding: 86rpx 20rpx 0;
}
.action-button {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 90rpx;
font-weight: 500;
font-size: 30rpx;
color: #ffffff;
border-radius: 46rpx;
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;
}
.action-link {
margin-top: 28rpx;
text-align: center;
font-weight: 500;
font-size: 28rpx;
color: #1fb4ff;
}
.popup-detail__row {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-top: 14rpx;
}
.popup-address {
max-width: 320rpx;
text-align: right;
word-break: break-all;
}
</style>

620
pages/index/index.vue Normal file
View File

@ -0,0 +1,620 @@
<template>
<view class="asset-page asset-theme home-page">
<view class="home-topbar">
<view class="home-topbar__side"></view>
<text class="home-topbar__title">{{ overview.title || "数字资产" }}</text>
<view class="home-wallet-btn" @click="openWallet">
<view class="home-wallet-btn__icon">
<image
src="https://imgs.agrimedia.cn/bm-bmt/qianbao.png"
mode="widthFix"
></image>
</view>
<text class="home-wallet-btn__text">钱包</text>
</view>
</view>
<view class="asset-scroll home-scroll">
<view class="home-hero">
<view class="brand-banner"></view>
<view class="stat-panel">
<view
v-for="item in overview.topStats"
:key="item.key"
class="stat-card"
:class="'stat-card--' + item.accent"
>
<view class="stat-card__body">
<text class="stat-card__label">{{ item.title }}</text>
<view class="stat-card__value-row">
<text class="stat-card__value">{{ item.value }}</text>
<text class="stat-card__unit">{{ item.unit }}</text>
</view>
</view>
</view>
</view>
</view>
<view class="home-section">
<view class="home-section__head">
<text class="home-section__title">我的资产</text>
</view>
<view class="asset-grid">
<view
v-for="item in overview.quickAssets"
:key="item.key"
class="asset-mini-card"
:class="'asset-mini-card--' + item.accent"
@click="openQuickAsset(item)"
>
<image
class="asset-mini-card__bg"
:src="quickAssetBg(item.key)"
mode="aspectFill"
></image>
<view class="asset-mini-card__head">
<view class="asset-mini-card__icon">
<image
class="asset-mini-card__icon-image"
:src="quickAssetIcon(item.key)"
mode="aspectFit"
></image>
</view>
<text class="asset-mini-card__label">{{ item.title }}</text>
</view>
<text class="asset-mini-card__value">{{ item.value }}</text>
</view>
</view>
</view>
<view class="home-section">
<view class="home-section__head">
<text class="home-section__title">功能中心</text>
</view>
<view class="feature-list">
<view
v-for="feature in overview.features"
:key="feature.key"
class="feature-list__item"
@click="openFeature(feature.key)"
>
<view class="feature-list__main">
<view class="feature-list__icon">
<image
class="feature-list__icon-image"
:src="featureIcon(feature.key)"
mode="aspectFit"
></image>
</view>
<text class="feature-list__title">{{ feature.title }}</text>
</view>
<image class="feature-list__arrow" src="https://imgs.agrimedia.cn/bm-bmt/more.png"></image>
</view>
</view>
</view>
<view class="notice-bar">
<image
class="notice-bar__icon"
src="https://imgs.agrimedia.cn/bm-bmt/tips.png"
mode="widthFix"
></image>
<text class="notice-bar__text">{{ overview.notice }}</text>
</view>
</view>
</view>
</template>
<script>
import { fetchAssetHome } from "../../api/assets";
export default {
data() {
return {
overview: {
title: "数字资产",
heroTitle: "BM数字资产管理",
topStats: [],
quickAssets: [],
features: [],
notice: "",
},
};
},
onLoad() {
this.loadPage();
},
onShow() {
this.loadPage();
},
methods: {
async loadPage() {
try {
this.overview = await fetchAssetHome();
} catch (error) {
uni.showToast({
title: error.message || "首页加载失败",
icon: "none",
});
}
},
statIcon(key) {
const iconMap = {
"wallet-bmt": "https://imgs.agrimedia.cn/bm-bmt/hong-l.png",
ticker: "https://imgs.agrimedia.cn/bm-bmt/hong-r.png",
};
return iconMap[key] || "";
},
quickAssetIcon(key) {
const iconMap = {
points: "https://imgs.agrimedia.cn/bm-bmt/jifen-icon.png",
voucher: "https://imgs.agrimedia.cn/bm-bmt/quan-icon.png",
coupon: "https://imgs.agrimedia.cn/bm-bmt/xiaofei-icon.png",
power: "https://imgs.agrimedia.cn/bm-bmt/suanli-icon.png",
};
return iconMap[key] || "";
},
quickAssetBg(key) {
const bgMap = {
points: "https://imgs.agrimedia.cn/bm-bmt/jifen-bg.png",
voucher: "https://imgs.agrimedia.cn/bm-bmt/quan-bg.png",
coupon: "https://imgs.agrimedia.cn/bm-bmt/xiaofei-bg.png",
power: "https://imgs.agrimedia.cn/bm-bmt/suanli-bg.png",
};
return bgMap[key] || "";
},
featureIcon(key) {
const iconMap = {
"bmt-exchange": "https://imgs.agrimedia.cn/bm-bmt/b.png",
"power-exchange": "https://imgs.agrimedia.cn/bm-bmt/s.png",
transfer: "https://imgs.agrimedia.cn/bm-bmt/z.png",
withdraw: "https://imgs.agrimedia.cn/bm-bmt/t.png",
"points-convert": "https://imgs.agrimedia.cn/bm-bmt/j.png",
};
return iconMap[key] || "";
},
openFeature(key) {
const urlMap = {
transfer: "/pages/assets/transfer",
"power-exchange": "/pages/assets/power-exchange",
"bmt-exchange": "/pages/assets/bmt-exchange",
withdraw: "/pages/assets/withdraw",
"points-convert": "/pages/assets/points-convert",
};
const targetUrl = urlMap[key];
if (!targetUrl) {
uni.showToast({
title: "功能开发中",
icon: "none",
});
return;
}
uni.navigateTo({
url: targetUrl,
});
},
openQuickAsset(item) {
const urlMap = {
points: "/pages/assets/ledger?type=points",
voucher: "/pages/assets/ledger?type=voucher",
coupon: "/pages/assets/ledger?type=coupon",
power: "/pages/assets/ledger?type=power",
};
const targetUrl = urlMap[item.key];
if (!targetUrl) {
return;
}
uni.navigateTo({
url: targetUrl,
});
},
openWallet() {
uni.navigateTo({
url: "/pages/assets/wallet",
});
},
},
};
</script>
<style lang="scss" scoped>
@import "../../styles/tokens.scss";
@import "../../styles/common.scss";
.home-page {
background: #191e32;
}
.home-topbar {
display: grid;
grid-template-columns: 136rpx 1fr 136rpx;
align-items: center;
padding: calc(env(safe-area-inset-top) + 20rpx) 16rpx 8rpx;
}
.home-topbar__side {
width: 136rpx;
height: 1rpx;
}
.home-topbar__title {
text-align: center;
font-size: 36rpx;
font-weight: 700;
color: #ffffff;
letter-spacing: 1rpx;
}
.home-wallet-btn {
justify-self: end;
display: flex;
align-items: center;
height: 52rpx;
color: rgba(229, 235, 255, 0.82);
}
.home-wallet-btn__icon {
width: 30rpx;
height: 32rpx;
margin-right: 8rpx;
image {
width: 30rpx;
height: 32rpx;
}
}
.home-wallet-btn__text {
font-size: 28rpx;
color: rgba(229, 235, 255, 0.82);
}
.home-scroll {
padding: 12rpx 20rpx 24rpx;
}
.home-hero {
position: relative;
margin-bottom: 26rpx;
}
.brand-banner {
position: relative;
overflow: hidden;
height: 380rpx;
border-radius: 28rpx;
background:
linear-gradient(
180deg,
rgba(5, 12, 28, 0.06) 0%,
rgba(5, 12, 28, 0.16) 100%
),
url("https://imgs.agrimedia.cn/bm-bmt/home2-bg.png")
no-repeat;
border: 1px solid rgba(112, 135, 196, 0.18);
background-size: contain;
box-shadow: 0 24rpx 48rpx rgba(7, 12, 32, 0.28);
}
.brand-banner::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 26%);
pointer-events: none;
}
.brand-banner::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 120rpx;
background: linear-gradient(180deg, rgba(4, 10, 24, 0), rgba(4, 10, 24, 0.18));
pointer-events: none;
}
.stat-panel {
position: absolute;
z-index: 2;
left: 20rpx;
right: 20rpx;
bottom: 24rpx;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20rpx;
padding: 0;
background: transparent;
}
.stat-card {
position: relative;
overflow: hidden;
min-height: 126rpx;
padding: 22rpx 22rpx 20rpx;
border-radius: 8rpx;
background: linear-gradient(
180deg,
rgba(42, 55, 82, 0.98) 0%,
rgba(39, 51, 76, 0.98) 100%
);
border: 1px solid rgba(121, 139, 190, 0.16);
box-shadow: 0 14rpx 24rpx rgba(5, 11, 29, 0.2);
}
.stat-card::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent 34%);
pointer-events: none;
}
.stat-card__icon-box {
position: absolute;
right: 16rpx;
top: 16rpx;
display: flex;
align-items: center;
justify-content: center;
width: 56rpx;
height: 56rpx;
border-radius: 8rpx;
border: 1px solid rgba(255, 255, 255, 0.08);
flex-shrink: 0;
}
.stat-card__icon-box--gold {
background: linear-gradient(
135deg,
rgba(20, 182, 255, 0.22) 0%,
rgba(20, 182, 255, 0.08) 100%
);
}
.stat-card__icon-box--green {
background: linear-gradient(
135deg,
rgba(46, 233, 167, 0.22) 0%,
rgba(46, 233, 167, 0.08) 100%
);
}
.stat-card__body {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
flex: 1;
min-width: 0;
min-height: 84rpx;
margin-left: 0;
padding-right: 56rpx;
}
.stat-card__label {
display: block;
font-size: 26rpx;
font-weight: 500;
line-height: 1.25;
white-space: nowrap;
}
.stat-card--gold .stat-card__label {
color: #19b9ff;
}
.stat-card--green .stat-card__label {
color: #28e0a6;
}
.stat-card__value-row {
display: flex;
align-items: baseline;
flex-wrap: wrap;
margin-top: 16rpx;
min-width: 0;
}
.stat-card__value {
font-size: 32rpx;
font-weight: 800;
color: #ffffff;
line-height: 1;
}
.stat-card__unit {
margin-left: 8rpx;
font-size: 22rpx;
line-height: 1.2;
color: rgba(170, 181, 208, 0.92);
white-space: nowrap;
}
.home-section {
margin-top: 28rpx;
}
.home-section__head {
margin-bottom: 16rpx;
}
.home-section__title {
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
}
.asset-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16rpx;
}
.asset-mini-card {
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: space-between;
min-width: 0;
padding: 18rpx 20rpx;
border-radius: 12rpx;
box-shadow: 0 12rpx 24rpx rgba(10, 16, 37, 0.16);
background: rgba(35, 45, 79, 0.9);
}
.asset-mini-card__bg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.asset-mini-card--blue {
background: linear-gradient(135deg, #10acee 0%, #1aa4ff 100%);
}
.asset-mini-card--green {
background: linear-gradient(135deg, #25d8a4 0%, #36e08f 100%);
}
.asset-mini-card--orange {
background: linear-gradient(135deg, #ff8d58 0%, #ff7d7d 100%);
}
.asset-mini-card--violet {
background: linear-gradient(135deg, #a667f3 0%, #b27cff 100%);
}
.asset-mini-card__head,
.asset-mini-card__value {
position: relative;
z-index: 1;
}
.asset-mini-card__head {
display: flex;
font-size: 26rpx;
align-items: center;
}
.asset-mini-card__icon {
display: flex;
align-items: center;
justify-content: center;
width: 38rpx;
height: 38rpx;
flex-shrink: 0;
}
.asset-mini-card__icon-image {
width: 32rpx;
height: 32rpx;
}
.asset-mini-card__label {
margin-left: 14rpx;
font-size: 26rpx;
font-weight: 500;
color: rgba(255, 255, 255, 0.94);
white-space: nowrap;
}
.asset-mini-card__value {
display: block;
font-size: 32rpx;
font-weight: 800;
text-align: right;
color: #ffffff;
}
.feature-list {
display: grid;
gap: 16rpx;
}
.feature-list__item {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 100rpx;
padding: 0 22rpx;
border-radius: 12rpx;
background: #242944;
box-shadow: 0 12rpx 22rpx rgba(8, 13, 30, 0.14);
}
.feature-list__main {
display: flex;
align-items: center;
min-width: 0;
}
.feature-list__icon {
display: flex;
align-items: center;
justify-content: center;
width: 36rpx;
height: 36rpx;
flex-shrink: 0;
}
.feature-list__icon-image {
width: 32rpx;
height: 32rpx;
}
.feature-list__title {
margin-left: 20rpx;
font-size: 28rpx;
font-weight: 400;
color: #f3f6ff;
}
.feature-list__arrow {
margin-left: 16rpx;
width: 26rpx;
height: 26rpx;
line-height: 1;
}
.notice-bar {
display: flex;
align-items: flex-start;
margin-top: 20rpx;
padding: 0 2rpx;
}
.notice-bar__icon {
display: flex;
align-items: center;
justify-content: center;
width: 28rpx;
height: 28rpx;
margin-right: 8rpx;
margin-top: 4rpx;
border-radius: 50%;
background: linear-gradient(180deg, #ffd764 0%, #ffbb2e 100%);
font-size: 16rpx;
font-weight: 700;
color: #ffffff;
flex-shrink: 0;
}
.notice-bar__text {
font-size: 24rpx;
line-height: 1.65;
color: rgba(200, 207, 227, 0.82);
}
</style>

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

509
styles/common.scss Normal file
View File

@ -0,0 +1,509 @@
@import "./tokens.scss";
.asset-page {
background: #191e32;
color: $asset-text-main;
}
.section-label {
margin: 0 0 20rpx;
font-size: 36rpx;
font-weight: 600;
color: $asset-text-main;
}
.section-subtitle {
margin-top: 8rpx;
font-size: 24rpx;
line-height: 1.6;
color: $asset-text-muted;
}
.glass-panel {
border: 1px solid rgba(143, 167, 207, 0.16);
border-radius: 16rpx;
background: linear-gradient(
180deg,
rgba(54, 62, 96, 0.94),
rgba(39, 50, 84, 0.92)
);
box-shadow: $asset-shadow;
}
.paper-panel {
border: 1px solid rgba(149, 162, 220, 0.16);
border-radius: 16rpx;
background: linear-gradient(
180deg,
rgba(54, 62, 96, 0.94),
rgba(39, 50, 84, 0.92)
);
box-shadow: $asset-shadow;
color: $asset-text-main;
}
.panel-block {
padding: 20rpx 18rpx;
}
.hero-chip-row {
display: flex;
flex-wrap: wrap;
margin: 20rpx 0 0;
}
.hero-chip {
display: inline-flex;
align-items: center;
padding: 12rpx 18rpx;
margin: 0 16rpx 16rpx 0;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.08);
font-size: 22rpx;
color: $asset-text-main;
}
.hero-chip__dot {
width: 12rpx;
height: 12rpx;
margin-right: 12rpx;
border-radius: 50%;
background: $asset-accent;
box-shadow: 0 0 18rpx rgba(76, 201, 255, 0.6);
}
.summary-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-gap: 20rpx;
}
.summary-card {
padding: 24rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(143, 167, 207, 0.12);
}
.summary-card__title {
font-size: 24rpx;
color: $asset-text-muted;
}
.summary-card__value {
display: flex;
align-items: baseline;
margin-top: 14rpx;
font-size: 44rpx;
font-weight: 700;
line-height: 1;
}
.summary-card__unit {
margin-left: 10rpx;
font-size: 24rpx;
font-weight: 500;
color: $asset-text-subtle;
}
.summary-card__desc {
margin-top: 14rpx;
font-size: 22rpx;
line-height: 1.5;
color: $asset-text-subtle;
}
.feature-list {
margin-top: 8rpx;
}
.feature-cell {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.feature-cell:last-child {
border-bottom: 0;
padding-bottom: 0;
}
.feature-cell__main {
flex: 1;
min-width: 0;
}
.feature-cell__title {
font-size: 30rpx;
font-weight: 600;
color: $asset-text-main;
}
.feature-cell__desc {
margin-top: 8rpx;
font-size: 22rpx;
line-height: 1.5;
color: rgba(255, 255, 255, 0.6);
}
.feature-cell__arrow {
margin-left: 20rpx;
font-size: 36rpx;
font-weight: 600;
color: rgba(255, 255, 255, 0.4);
}
.action-row {
display: flex;
align-items: center;
}
.tab-row {
display: flex;
padding: 6rpx;
border-radius: 999rpx;
background: rgba(17, 27, 54, 0.54);
}
.tab-chip {
flex: 1;
padding: 16rpx 0;
border-radius: 999rpx;
text-align: center;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.64);
}
.tab-chip.is-active {
background: linear-gradient(135deg, #7e6cff 0%, #6b56f6 100%);
color: #ffffff;
box-shadow: 0 14rpx 22rpx rgba(93, 79, 225, 0.22);
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-gap: 18rpx;
}
.info-box {
padding: 20rpx;
border-radius: 14rpx;
background: rgba(29, 39, 68, 0.52);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.info-box__label {
font-size: 22rpx;
color: $asset-text-subtle;
}
.info-box__value {
margin-top: 10rpx;
font-size: 34rpx;
font-weight: 700;
color: $asset-text-main;
}
.field-card {
padding: 18rpx;
border-radius: 14rpx;
background: rgba(29, 39, 68, 0.52);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.field-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.field-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.62);
}
.field-value {
font-size: 28rpx;
font-weight: 600;
color: $asset-text-main;
}
.field-helper {
margin-top: 10rpx;
font-size: 22rpx;
line-height: 1.6;
color: rgba(255, 255, 255, 0.6);
}
.text-input {
width: 100%;
margin-top: 16rpx;
padding: 20rpx 22rpx;
border-radius: 14rpx;
background: rgba(18, 25, 48, 0.72);
border: 1px solid rgba(255, 255, 255, 0.1);
font-size: 30rpx;
color: $asset-text-main;
}
.chip-selector {
display: flex;
flex-wrap: wrap;
margin-top: 16rpx;
}
.chip-selector__item {
padding: 14rpx 24rpx;
margin: 0 16rpx 16rpx 0;
border-radius: 999rpx;
background: rgba(18, 25, 48, 0.72);
border: 1px solid rgba(255, 255, 255, 0.08);
font-size: 24rpx;
color: rgba(255, 255, 255, 0.64);
}
.chip-selector__item.is-active {
background: rgba(126, 108, 255, 0.22);
border-color: rgba(126, 108, 255, 0.28);
color: $asset-text-main;
}
.hint-list {
margin: 8rpx 0 0;
}
.hint-item {
display: flex;
margin-top: 14rpx;
font-size: 22rpx;
line-height: 1.7;
color: rgba(255, 255, 255, 0.62);
}
.hint-item__index {
margin-right: 10rpx;
color: $asset-accent-strong;
font-weight: 600;
}
.button-row {
display: flex;
align-items: center;
}
.button-row .secondary-button {
margin-right: 20rpx;
}
.primary-button {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 84rpx;
border: 0;
border-radius: 999rpx;
background: linear-gradient(135deg, #7e6cff 0%, #6b56f6 100%);
box-shadow: 0 14rpx 24rpx rgba(93, 79, 225, 0.22);
font-size: 30rpx;
font-weight: 700;
color: #ffffff;
}
.secondary-button {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 84rpx;
border-radius: 999rpx;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.06);
font-size: 30rpx;
font-weight: 600;
color: $asset-text-main;
}
.button-link {
font-size: 24rpx;
font-weight: 600;
color: rgba(221, 230, 255, 0.86);
}
.meta-pair {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 18rpx;
font-size: 24rpx;
}
.meta-pair__label {
color: rgba(255, 255, 255, 0.62);
}
.meta-pair__value {
color: $asset-text-main;
font-weight: 600;
}
.danger-text {
color: $asset-danger;
}
.success-text {
color: $asset-success;
}
.warning-text {
color: $asset-warning;
}
.wallet-card {
padding: 20rpx 18rpx;
border-radius: 16rpx;
background: linear-gradient(
180deg,
rgba(54, 62, 96, 0.94),
rgba(39, 50, 84, 0.92)
);
border: 1px solid rgba(149, 162, 220, 0.16);
}
.wallet-name {
font-size: 28rpx;
font-weight: 700;
color: $asset-text-main;
}
.wallet-address {
margin-top: 14rpx;
font-size: 24rpx;
line-height: 1.7;
word-break: break-all;
color: $asset-text-muted;
}
.wallet-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20rpx;
}
.wallet-badge {
padding: 8rpx 16rpx;
border-radius: 999rpx;
background: rgba(126, 108, 255, 0.18);
font-size: 20rpx;
color: #dfe7ff;
}
.empty-panel {
padding: 36rpx 28rpx;
border: 1px solid rgba(149, 162, 220, 0.16);
border-radius: 16rpx;
background: linear-gradient(
180deg,
rgba(54, 62, 96, 0.94),
rgba(39, 50, 84, 0.92)
);
text-align: center;
}
.empty-title {
font-size: 30rpx;
font-weight: 700;
color: $asset-text-main;
}
.empty-desc {
margin-top: 14rpx;
font-size: 24rpx;
line-height: 1.7;
color: $asset-text-muted;
}
.page-hero {
display: flex;
flex-direction: column;
align-items: center;
padding: 12rpx 0 20rpx;
}
.page-hero__mark {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 110rpx;
height: 110rpx;
border-radius: 50%;
background: linear-gradient(180deg, #1f4a72 0%, #1a3464 100%);
}
.page-hero__ring {
position: absolute;
left: 50%;
top: 50%;
border-radius: 50%;
transform: translate(-50%, -50%);
}
.page-hero__ring--outer {
width: 110rpx;
height: 110rpx;
border: 4rpx solid rgba(63, 220, 255, 0.9);
box-shadow: 0 0 0 6rpx rgba(80, 225, 255, 0.08);
}
.page-hero__ring--inner {
width: 88rpx;
height: 88rpx;
border: 3rpx solid rgba(98, 231, 255, 0.42);
}
.page-hero__text {
position: relative;
z-index: 1;
font-size: 52rpx;
font-weight: 800;
font-style: italic;
color: #42d9ff;
}
.page-hero__value {
margin-top: 16rpx;
font-size: 24rpx;
font-weight: 700;
color: $asset-text-main;
}
.page-hero__desc {
margin-top: 8rpx;
font-size: 20rpx;
line-height: 1.6;
color: rgba(255, 255, 255, 0.66);
text-align: center;
}
.submit-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx 0 8rpx;
}
.submit-wrap .primary-button {
width: 360rpx;
}
.record-link {
margin-top: 18rpx;
font-size: 22rpx;
text-decoration: underline;
color: rgba(222, 232, 255, 0.86);
}

27
styles/tokens.scss Normal file
View File

@ -0,0 +1,27 @@
$asset-bg-deep: #08162e;
$asset-bg-mid: #10264a;
$asset-bg-soft: #173664;
$asset-panel: rgba(18, 39, 77, 0.96);
$asset-panel-soft: rgba(21, 45, 88, 0.9);
$asset-paper: #edf4ff;
$asset-paper-card: #ffffff;
$asset-paper-line: rgba(34, 64, 110, 0.12);
$asset-text-main: #ffffff;
$asset-text-muted: rgba(255, 255, 255, 0.78);
$asset-text-subtle: #8fa7cf;
$asset-text-dark: #112446;
$asset-text-grey: #6e83a7;
$asset-accent: #4cc9ff;
$asset-accent-strong: #5a71ff;
$asset-accent-soft: rgba(76, 201, 255, 0.12);
$asset-success: #5ad7a1;
$asset-warning: #ffbf66;
$asset-danger: #ff7285;
$asset-radius-lg: 28rpx;
$asset-radius-xl: 36rpx;
$asset-shadow: 0 24rpx 60rpx rgba(0, 0, 0, 0.18);
$asset-shadow-soft: 0 18rpx 40rpx rgba(8, 22, 46, 0.12);
$asset-space-sm: 16rpx;
$asset-space-md: 24rpx;
$asset-space-lg: 32rpx;
$asset-space-xl: 40rpx;

13
uni.promisify.adaptor.js Normal file
View File

@ -0,0 +1,13 @@
uni.addInterceptor({
returnValue (res) {
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
return res;
}
return new Promise((resolve, reject) => {
res.then((res) => {
if (!res) return resolve(res)
return res[0] ? reject(res[0]) : resolve(res[1])
});
});
},
});

76
uni.scss Normal file
View File

@ -0,0 +1,76 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
/* 边框颜色 */
$uni-border-color:#c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16px;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px;

100
utils/request.js Normal file
View File

@ -0,0 +1,100 @@
import serviceConfig from "../config/service";
import { getCurrentWebviewToken } from "./webview-token";
let loadingCount = 0;
function isAbsoluteUrl(url) {
return /^https?:\/\//i.test(url || "");
}
function buildRequestUrl(url) {
if (isAbsoluteUrl(url)) {
return url;
}
return serviceConfig.BASE_URL + url;
}
function showGlobalLoading(options) {
if (!options || !options.showLoading) {
return false;
}
loadingCount += 1;
if (loadingCount === 1) {
uni.showLoading({
title: options.loadingText || "加载中",
mask: options.loadingMask !== false,
});
}
return true;
}
function hideGlobalLoading(shouldHide) {
if (!shouldHide) {
return;
}
loadingCount = Math.max(0, loadingCount - 1);
if (loadingCount === 0) {
uni.hideLoading();
}
}
export default function request(options) {
const method = options.method || "GET";
const defaultContentType =
method === "POST"
? "application/x-www-form-urlencoded; charset=UTF-8"
: "application/json";
const token = getCurrentWebviewToken();
const requestHeader = Object.assign(
{
"Content-Type": defaultContentType,
},
options.header || {},
);
if (token) {
const bearerToken = "Bearer " + token;
requestHeader["Authori-zation"] = bearerToken;
requestHeader.Authorization = bearerToken;
}
const shouldHandleLoading = showGlobalLoading(options);
return new Promise(function (resolve, reject) {
uni.request({
url: buildRequestUrl(options.url),
method: method,
data: options.data || {},
header: requestHeader,
timeout: serviceConfig.TIMEOUT,
success: function (response) {
const statusCode = response.statusCode || 0;
if (statusCode >= 200 && statusCode < 300) {
resolve(response.data);
return;
}
reject({
statusCode: statusCode,
message: (response.data && response.data.message) || "接口请求失败",
raw: response,
});
},
fail: function (error) {
reject({
statusCode: 0,
message: error.errMsg || "网络异常",
raw: error,
});
},
complete: function () {
hideGlobalLoading(shouldHandleLoading);
},
});
});
}

62
utils/webview-token.js Normal file
View File

@ -0,0 +1,62 @@
function findTokenInText(text) {
if (!text) {
return "";
}
const queryIndex = text.indexOf("?");
if (queryIndex === -1) {
return "";
}
const rawQueryText = text.slice(queryIndex + 1);
const hashIndex = rawQueryText.indexOf("#");
const queryText =
hashIndex === -1 ? rawQueryText : rawQueryText.slice(0, hashIndex);
const search = new URLSearchParams(queryText);
return search.get("token") || "";
}
export function extractTokenFromUrl(url) {
if (!url) {
return "";
}
const directToken = findTokenInText(url);
if (directToken) {
return directToken;
}
const hashIndex = url.indexOf("#");
if (hashIndex === -1) {
return "";
}
return findTokenInText(url.slice(hashIndex + 1));
}
export function getCurrentWebviewUrl() {
// #ifdef H5
return window.location.href || "";
// #endif
// #ifdef APP-PLUS
if (typeof plus === "undefined" || !plus.webview) {
return "";
}
const currentWebview =
plus.webview.currentWebview() || plus.webview.getLaunchWebview();
if (!currentWebview || typeof currentWebview.getURL !== "function") {
return "";
}
return currentWebview.getURL() || "";
// #endif
return "";
}
export function getCurrentWebviewToken() {
return extractTokenFromUrl(getCurrentWebviewUrl());
}