diff --git a/App.vue b/App.vue
index 7ab8448..ce1913b 100644
--- a/App.vue
+++ b/App.vue
@@ -4,14 +4,144 @@ import {
refreshCurrentWebviewToken,
} from "./utils/webview-token";
+let numberFontSupportPromise = null;
+let numberFontSupportState = null;
+
+function hasBrowserDom() {
+ return typeof window !== "undefined" && typeof document !== "undefined";
+}
+
+function toggleNumberFontReadyClass(enabled) {
+ if (!hasBrowserDom() || !document.documentElement) {
+ return;
+ }
+
+ document.documentElement.classList.toggle(
+ "asset-din-font-ready",
+ Boolean(enabled),
+ );
+}
+
+function waitForBodyReady() {
+ return new Promise((resolve) => {
+ if (!hasBrowserDom() || document.body) {
+ resolve();
+ return;
+ }
+
+ const finish = () => resolve();
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", finish, {
+ once: true,
+ });
+ return;
+ }
+
+ setTimeout(finish, 0);
+ });
+}
+
+function createMeasureNode(fontFamily) {
+ const node = document.createElement("span");
+ node.textContent = "0123456789";
+ node.style.position = "absolute";
+ node.style.left = "-99999px";
+ node.style.top = "-99999px";
+ node.style.visibility = "hidden";
+ node.style.whiteSpace = "nowrap";
+ node.style.fontSize = "32px";
+ node.style.fontWeight = "800";
+ node.style.fontFamily = fontFamily;
+ return node;
+}
+
+function verifyDinFontRender() {
+ if (!hasBrowserDom() || !document.body) {
+ return false;
+ }
+
+ const container = document.createElement("div");
+ container.style.position = "absolute";
+ container.style.left = "-99999px";
+ container.style.top = "-99999px";
+ container.style.visibility = "hidden";
+ container.style.pointerEvents = "none";
+
+ const fallbackFamilies = ["monospace", "sans-serif", "serif"];
+ const loadedWidths = [];
+ const fallbackWidths = [];
+
+ fallbackFamilies.forEach((family) => {
+ const fallbackNode = createMeasureNode(family);
+ const loadedNode = createMeasureNode('"DIN-Bold", ' + family);
+
+ container.appendChild(fallbackNode);
+ container.appendChild(loadedNode);
+ });
+
+ document.body.appendChild(container);
+
+ Array.prototype.forEach.call(container.childNodes, (node, index) => {
+ const width = node.getBoundingClientRect().width;
+ if (index % 2 === 0) {
+ fallbackWidths.push(width);
+ return;
+ }
+
+ loadedWidths.push(width);
+ });
+
+ const isFontAvailable = fallbackWidths.some((width, index) => {
+ return Math.abs(loadedWidths[index] - width) > 0.1;
+ });
+
+ document.body.removeChild(container);
+
+ return isFontAvailable;
+}
+
+async function ensureNumberFontSupport() {
+ if (!hasBrowserDom()) {
+ return false;
+ }
+
+ if (!document.fonts || typeof document.fonts.load !== "function") {
+ return false;
+ }
+
+ await waitForBodyReady();
+
+ const loadSucceeded = await Promise.race([
+ document.fonts
+ .load('800 32px "DIN-Bold"', "0123456789")
+ .then((fonts) => Array.isArray(fonts) ? fonts.length > 0 : true)
+ .catch(() => false),
+ new Promise((resolve) => {
+ setTimeout(() => resolve(false), 2500);
+ }),
+ ]);
+
+ if (
+ !loadSucceeded ||
+ !document.fonts.check('800 32px "DIN-Bold"', "0123456789")
+ ) {
+ return false;
+ }
+
+ return verifyDinFontRender();
+}
+
export default {
onLaunch: function () {
console.log("App Launch");
this.logCurrentWebviewToken("launch");
+ this.syncNumberFontSupport();
},
onShow: function () {
console.log("App Show");
this.logCurrentWebviewToken("show");
+ this.syncNumberFontSupport();
},
onHide: function () {
console.log("App Hide");
@@ -28,6 +158,31 @@ export default {
console.log("[webview-token][" + scene + "] token not found", currentUrl);
},
+ async syncNumberFontSupport() {
+ if (!hasBrowserDom()) {
+ return;
+ }
+
+ if (numberFontSupportState !== null) {
+ toggleNumberFontReadyClass(numberFontSupportState);
+ return;
+ }
+
+ if (!numberFontSupportPromise) {
+ numberFontSupportPromise = ensureNumberFontSupport()
+ .then((result) => {
+ numberFontSupportState = Boolean(result);
+ return numberFontSupportState;
+ })
+ .catch(() => {
+ numberFontSupportState = false;
+ return false;
+ });
+ }
+
+ const canUseDinFont = await numberFontSupportPromise;
+ toggleNumberFontReadyClass(canUseDinFont);
+ },
},
};
@@ -38,20 +193,38 @@ export default {
@font-face {
font-family: "DIN-Bold";
src:
+ local("DIN-Bold"),
+ local("DIN Bold"),
+ local("DIN Alternate Bold"),
url("/static/fonts/din-bold-2.ttf") format("truetype"),
url("https://imgs.agrimedia.cn/din-bold-2.ttf") format("truetype");
font-style: normal;
font-weight: 800;
- font-display: swap;
+ font-display: optional;
+}
+
+:root {
+ --asset-number-font-family:
+ "Helvetica Neue",
+ Arial,
+ "PingFang SC",
+ sans-serif;
+ --asset-number-font-family-din:
+ "DIN-Bold",
+ DIN,
+ "DIN Alternate",
+ "Helvetica Neue",
+ Arial,
+ "PingFang SC",
+ sans-serif;
+}
+
+.asset-din-font-ready {
+ --asset-number-font-family: var(--asset-number-font-family-din);
}
.asset-number-font {
- font-family:
- "DIN-Bold",
- "PingFang SC",
- "Helvetica Neue",
- Arial,
- sans-serif !important;
+ font-family: var(--asset-number-font-family) !important;
font-variant-numeric: tabular-nums;
}
diff --git a/api/assets.js b/api/assets.js
index 8691f31..da958cb 100644
--- a/api/assets.js
+++ b/api/assets.js
@@ -254,7 +254,7 @@ function buildTransferTips(feePercent) {
return {
points: [
"只能转赠100的整数倍",
- "凌晨0点-凌晨01点系统维护不可赠送",
+ "凌晨00:00至02:00为系统维护时段不可赠送",
"转赠系统会扣除" + percentText + "%的手续费",
],
power: [
@@ -873,7 +873,7 @@ function getTransferRecordFee(item) {
function getTransferRecordFeeText(item) {
const percent = toNumber(item && item.fee_percent);
const unit = getTransferRecordUnit(item);
- const feeText = formatTransferRecordNumber(getTransferRecordFee(item));
+ const feeText = formatHomeNumber(getTransferRecordFee(item), 2);
if (percent > 0) {
return (
@@ -1368,6 +1368,15 @@ async function fetchBmtPowerRateData(requestOptions) {
);
}
+async function fetchPowerExchangeMuitData(requestOptions) {
+ return fetchPayload(
+ createRequestOptions({
+ url: serviceConfig.ENDPOINTS.powerExchangeMuit,
+ }, requestOptions),
+ "兑换倍率加载失败",
+ );
+}
+
function resolveRedeemPowerRate(data) {
const sourceValue =
data && typeof data === "object"
@@ -1397,6 +1406,45 @@ async function fetchTransferFeeData(requestOptions) {
);
}
+async function fetchWithdrawRateData(requestOptions) {
+ return fetchPayload(
+ createRequestOptions({
+ url: serviceConfig.ENDPOINTS.withdrawRate,
+ }, requestOptions),
+ "提现费率加载失败",
+ );
+}
+
+function resolvePercentRate(data) {
+ let sourceValue =
+ data && typeof data === "object"
+ ? pickFirstValue(data, [
+ "r",
+ "rate",
+ "fee_rate",
+ "feeRate",
+ "withdraw_rate",
+ "withdrawRate",
+ "prop",
+ "value",
+ "num",
+ "number",
+ ])
+ : data;
+
+ if (typeof sourceValue === "string") {
+ sourceValue = sourceValue.replace(/%/g, "").trim();
+ }
+
+ const rate = toNumber(sourceValue);
+
+ if (rate > 0 && rate < 1) {
+ return rate * 100;
+ }
+
+ return rate;
+}
+
async function fetchWalletAddressData(requestOptions) {
return fetchPayload(
createRequestOptions({
@@ -1406,6 +1454,24 @@ async function fetchWalletAddressData(requestOptions) {
);
}
+async function fetchCoinIndexData(requestOptions) {
+ return fetchPayload(
+ createRequestOptions({
+ url: serviceConfig.ENDPOINTS.coinIndex,
+ }, requestOptions),
+ "抵用券信息加载失败",
+ );
+}
+
+async function fetchUserProfileData(requestOptions) {
+ return fetchPayload(
+ createRequestOptions({
+ url: serviceConfig.ENDPOINTS.userProfile,
+ }, requestOptions),
+ "用户信息加载失败",
+ );
+}
+
function normalizePointsConvertInterval(value) {
const rawInterval = String(
value === undefined || value === null ? "" : value,
@@ -1511,6 +1577,40 @@ export async function fetchAssetHome(requestOptions) {
return overview;
}
+export async function fetchVoucherBrokerLinkData(requestOptions) {
+ const data = await fetchCoinIndexData(requestOptions);
+ const userId = pickFirstValue(data, ["user_id", "uid", "id"]);
+ const balance = pickFirstValue(data, ["balance", "coin", "total"]);
+
+ if (userId === "") {
+ throw createError("未获取到用户信息", data);
+ }
+
+ return {
+ userId: String(userId),
+ balance: balance === "" ? "0" : String(balance),
+ };
+}
+
+export async function fetchCouponRedeemLinkData(requestOptions) {
+ const userInfo = await fetchUserProfileData(requestOptions);
+ const canRedeem = pickFirstValue(userInfo, [
+ "is_show_merchant_button",
+ "canRedeem",
+ "can_redeem",
+ ]);
+ const targetUrl =
+ serviceConfig.HTTP_REQUEST_URL +
+ "/MD/pages/redeemVoucher/index?canRedeem=" +
+ encodeURIComponent(canRedeem === "" ? "0" : String(canRedeem));
+
+ return {
+ canRedeem: canRedeem === "" ? "0" : String(canRedeem),
+ targetUrl: targetUrl,
+ encodedTargetUrl: encodeURIComponent(targetUrl),
+ };
+}
+
export async function fetchPointsConvertHome(month, requestOptions) {
const result = await Promise.all([
fetchHomeBalanceData(requestOptions),
@@ -1696,18 +1796,21 @@ export async function fetchPowerExchangeDetail(requestOptions) {
const result = await Promise.all([
fetchPriceData(requestOptions),
fetchHomeBalanceData(requestOptions),
+ fetchPowerExchangeMuitData(requestOptions),
]);
const ticker = normalizeTicker(result[0]);
const balances = normalizeBalances(result[1]);
+ const exchangeMultiplier = resolveRedeemPowerRate(result[2]) || 1;
return {
ticker: ticker,
+ exchangeMultiplier: exchangeMultiplier,
balances: {
coupon: toFixedNumber(balances.coupon, 2),
voucher: toFixedNumber(balances.voucher, 2),
power: toFixedNumber(balances.power, 0),
},
tips: [
- "算力 = 抵用券或消费券 × BMT实时价格;",
+ "算力 = 抵用券或消费券 × BMT实时价格 × 兑换倍率;",
"抵用券和消费券总数小于100券的不可兑换;",
"算力用于兑换BMT使用。",
],
@@ -1765,7 +1868,7 @@ export async function fetchBmtExchangeDetail(requestOptions) {
"预估 BMT数= 输入可用积分数÷BMT 实时价格。",
`兑换会同步消耗算力, 1 点可用积分,需同步消耗${powerRate}点算力。`,
"仅支持 100 的正整数倍, 不足 100的可用积分无法发起兑换。",
- "凌晨 00:00 至 01:00 为积分系统维护时段,期间暂停兑换服务,请避开该时间段操作。",
+ "凌晨00:00至02:00为积分维护时段",
],
};
}
@@ -1798,14 +1901,23 @@ export async function fetchWithdrawDetail(requestOptions) {
fetchPriceData(requestOptions),
fetchHomeBalanceData(requestOptions),
fetchWalletAddressData(requestOptions),
+ fetchWithdrawRateData(requestOptions),
]);
const ticker = normalizeTicker(result[0]);
const balances = normalizeBalances(result[1]);
const walletPayload = buildWalletPayload(result[2] && result[2].address);
+ const withdrawRate = resolvePercentRate(result[3]);
+
+ setHomeTickerCache(ticker);
return {
ticker: ticker,
withdrawableBmt: toFixedNumber(balances.withdrawableBmt, 2),
+ withdrawRate: withdrawRate,
+ balances: {
+ voucher: balances.voucher,
+ coupon: balances.coupon,
+ },
wallets: walletPayload.wallets,
defaultWallet: walletPayload.wallets[0] || null,
};
diff --git a/config/service.js b/config/service.js
index f96d1cc..666a4ed 100644
--- a/config/service.js
+++ b/config/service.js
@@ -1,5 +1,10 @@
+const HOST_URL = "https://tpoint.agrimedia.cn";
+// const HOST_URL = window.location.protocol + "//" + window.location.host;
+
+
const serviceConfig = {
- BASE_URL: "https://tpoint.agrimedia.cn",
+ BASE_URL: HOST_URL,
+ HTTP_REQUEST_URL: HOST_URL,
TIMEOUT: 10000,
WALLET_NAME: "海南农综交易所",
POINTS_CONVERT_INTERVAL: "0,20",
@@ -7,17 +12,20 @@ const serviceConfig = {
price: "/api/hn/getPrice",
homeBalance: "/api/hn/getAllBalance",
powerExchangeSubmit: "/api/hn/redeem/power",
+ powerExchangeMuit: "/api/hn/redeem/getMuit",
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",
walletFlowList: "/api/hn/wallet_flow/getList",
redeemRecordList: "/api/hn/redeem/redeemList",
+ coinIndex: "/api/coin/index",
+ userProfile: "/api/user",
walletDetail: "/api/hn/wallet/getWalletAddress",
walletSave: "/api/hn/wallet/saveAddress",
+ withdrawRate: "/api/hn/wallet/getRate",
withdrawSubmit: "/api/hn/wallet/withdraw",
pointsConvertHistoryList: "/api/integral/getTransferList",
pointsConvertAvailableList: "/api/integral/transferList",
diff --git a/pages/assets/ledger.vue b/pages/assets/ledger.vue
index c9c9020..ac6c5dc 100644
--- a/pages/assets/ledger.vue
+++ b/pages/assets/ledger.vue
@@ -66,7 +66,9 @@
{{
item.balanceLabel || item.balance
}}
- {{ item.feeText }}
+ {{
+ item.feeDisplayText || item.feeText
+ }}
+
+
+ {{
+ loadMoreText
+ }}
+
@@ -99,6 +105,10 @@ export default {
availablePoints: "0",
records: [],
},
+ page: 1,
+ pageSize: 10,
+ hasMore: false,
+ loadingMore: false,
};
},
computed: {
@@ -115,20 +125,87 @@ export default {
filteredRecords() {
return Array.isArray(this.detail.records) ? this.detail.records : [];
},
+ showLoadMoreState() {
+ return this.filteredRecords.length > 0;
+ },
+ loadMoreText() {
+ if (this.loadingMore) {
+ return "加载中...";
+ }
+
+ if (this.hasMore) {
+ return "上拉加载更多";
+ }
+
+ return "没有更多了";
+ },
},
onLoad() {
this.selectedMonth = this.currentMonth();
- this.loadPage(true);
+ this.resetPagingState();
+ this.loadPage(true, 1);
},
onShow() {
if (this.hasShown) {
- this.loadPage();
+ this.resetPagingState();
+ this.loadPage(false, 1);
return;
}
this.hasShown = true;
},
+ onReachBottom() {
+ this.loadMore();
+ },
methods: {
+ resetPagingState() {
+ this.page = 1;
+ this.hasMore = false;
+ this.loadingMore = false;
+ },
+ updatePaging(result, requestedPage, receivedLength) {
+ const pagination =
+ result && result.pagination && typeof result.pagination === "object"
+ ? result.pagination
+ : null;
+ this.page =
+ Number(pagination && pagination.page) || Number(requestedPage) || this.page || 1;
+
+ if (pagination && typeof pagination.hasMore === "boolean") {
+ this.hasMore = pagination.hasMore;
+ return;
+ }
+
+ this.hasMore = receivedLength >= this.pageSize;
+ },
+ mergeRecords(currentRecords, nextRecords) {
+ const mergedMap = {};
+ const mergedList = [];
+ const appendItem = (item) => {
+ const key =
+ String(item && item.id ? item.id : "").trim() ||
+ [
+ item && item.orderSn,
+ item && item.time,
+ item && item.title,
+ item && item.amount,
+ ]
+ .map((value) => String(value || ""))
+ .join("-");
+
+ if (!key || mergedMap[key]) {
+ return;
+ }
+
+ mergedMap[key] = true;
+ mergedList.push(item);
+ };
+
+ (Array.isArray(currentRecords) ? currentRecords : []).forEach(appendItem);
+ (Array.isArray(nextRecords) ? nextRecords : []).forEach(appendItem);
+
+ return mergedList;
+ },
currentMonth() {
const currentDate = new Date();
return (
@@ -161,28 +238,71 @@ export default {
event && event.detail ? event.detail.value : 0,
);
this.selectedMonth = this.monthOptions[nextIndex] || this.activeMonth;
- this.loadPage();
+ this.resetPagingState();
+ this.loadPage(true, 1);
},
- async loadPage(showLoading) {
+ async loadPage(showLoading, targetPage) {
+ const page = Number(targetPage || 1);
+ const isLoadMore = page > 1;
+
+ if (isLoadMore && this.loadingMore) {
+ return;
+ }
+
+ if (isLoadMore) {
+ this.loadingMore = true;
+ }
+
try {
- const result = await fetchPointsConvertHome(
- this.activeMonth,
- showLoading
+ const requestOptions =
+ !isLoadMore && showLoading
? {
showLoading: true,
loadingText: "加载中",
}
- : null,
+ : null;
+ const result = await fetchPointsConvertHome(
+ this.activeMonth,
+ {
+ page: page,
+ pageSize: this.pageSize,
+ },
+ requestOptions,
);
- this.detail = result;
+ const incomingRecords = Array.isArray(result.records) ? result.records : [];
+ const previousCount = this.filteredRecords.length;
+ const mergedRecords = isLoadMore
+ ? this.mergeRecords(this.filteredRecords, incomingRecords)
+ : incomingRecords;
+
+ this.detail = Object.assign({}, this.detail, result, {
+ records: mergedRecords,
+ });
+
+ this.updatePaging(result, page, incomingRecords.length);
+
+ if (isLoadMore && mergedRecords.length <= previousCount) {
+ this.hasMore = false;
+ }
} catch (error) {
uni.showToast({
- title: error.message || "页面加载失败",
+ title: error.message || (isLoadMore ? "加载更多失败" : "页面加载失败"),
icon: "none",
});
+ } finally {
+ if (isLoadMore) {
+ this.loadingMore = false;
+ }
}
},
+ loadMore() {
+ if (this.loadingMore || !this.hasMore) {
+ return;
+ }
+
+ this.loadPage(false, this.page + 1);
+ },
openConvertList() {
uni.navigateTo({
url: "/pages/assets/points-convert-list",
@@ -406,6 +526,18 @@ export default {
color: rgba(255, 255, 255, 0.96);
}
+.record-load-more {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 28rpx 0 8rpx;
+}
+
+.record-load-more__text {
+ font-size: 24rpx;
+ color: rgba(179, 189, 214, 0.78);
+}
+
.empty-card {
margin-top: 22rpx;
padding: 60rpx 28rpx;
diff --git a/pages/assets/power-exchange.vue b/pages/assets/power-exchange.vue
index e85a870..82d16b2 100644
--- a/pages/assets/power-exchange.vue
+++ b/pages/assets/power-exchange.vue
@@ -165,6 +165,7 @@ export default {
detail: {
ticker: {},
balances: {},
+ exchangeMultiplier: 1,
tips: [],
},
hasShown: false,
@@ -216,12 +217,19 @@ export default {
this.detail.ticker.close || this.detail.ticker.cnyPrice || 0,
);
},
+ exchangeMultiplierNumber() {
+ const value = Number(this.detail.exchangeMultiplier || 1);
+ return value > 0 ? value : 1;
+ },
estimatePower() {
if (!this.amountValue || !this.priceNumber) {
return "0";
}
- return this.formatAmount(this.amountValue * this.priceNumber, 2);
+ return this.formatAmount(
+ this.amountValue * this.priceNumber * this.exchangeMultiplierNumber,
+ 2,
+ );
},
allEstimatePower() {
if (!this.priceNumber) {
@@ -232,16 +240,19 @@ export default {
Number(this.detail.balances.voucher || 0) +
Number(this.detail.balances.coupon || 0);
- return this.formatAmount(totalTickets / this.priceNumber, 2);
+ return this.formatAmount(
+ totalTickets * this.priceNumber * this.exchangeMultiplierNumber,
+ 2,
+ );
},
displayPower() {
- return this.formatAmount(this.detail.balances.power, 0);
+ return this.formatAmount(this.detail.balances.power, 2);
},
displayVoucher() {
- return this.formatAmount(this.detail.balances.voucher, 0);
+ return this.formatAmount(this.detail.balances.voucher, 2);
},
displayCoupon() {
- return this.formatAmount(this.detail.balances.coupon, 0);
+ return this.formatAmount(this.detail.balances.coupon, 2);
},
displayPrice() {
return this.priceNumber ? this.priceNumber : "0.00";
@@ -252,7 +263,7 @@ export default {
}
return [
- "算力 = 抵用券或消费券 * BMT实时价格;",
+ "算力 = 抵用券或消费券 * BMT实时价格 * 兑换倍率;",
"抵用券和消费券总数小于100券的不可兑换;",
"算力用于兑换BMT使用。",
];
@@ -489,7 +500,7 @@ export default {
font-size: 32rpx;
font-weight: 800;
line-height: 1.1;
- font-family: "DIN-Bold", DIN, sans-serif;
+ font-family: var(--asset-number-font-family);
color: #ffffff;
}
diff --git a/pages/assets/transfer.vue b/pages/assets/transfer.vue
index b72bd35..0079e21 100644
--- a/pages/assets/transfer.vue
+++ b/pages/assets/transfer.vue
@@ -273,7 +273,7 @@ export default {
const received = this.amountValue
? this.amountValue * (1 - this.feePercent / 100)
: 0;
- return this.formatAmount(received, 0);
+ return this.formatAmount(received, 2);
},
amountPlaceholder() {
return this.activeTab === "power"
diff --git a/pages/assets/withdraw.vue b/pages/assets/withdraw.vue
index 7bf04b9..94ebc2d 100644
--- a/pages/assets/withdraw.vue
+++ b/pages/assets/withdraw.vue
@@ -45,7 +45,9 @@
- 提取手续费(1%)
+ 提取手续费({{ withdrawRateLabel }})
{{ feeText }}
@@ -112,7 +114,19 @@
}}
+
+