feat: 添加持仓数据类型和组件,支持持仓信息的展示与刷新功能

This commit is contained in:
2026-01-08 00:36:15 +07:00
parent babd307b60
commit 6a0077b14c
4 changed files with 150 additions and 3 deletions

View File

@@ -76,6 +76,10 @@ export type TradableData = Treaty.Data<typeof client.api.rwa.tokenization.tradab
export type TradableBody = TreatyQuery<typeof client.api.rwa.tokenization.tradable_products.get>; export type TradableBody = TreatyQuery<typeof client.api.rwa.tokenization.tradable_products.get>;
export type HoldingsData = Treaty.Data<typeof client.api.rwa.holdings.get>;
export type HoldingItem = HoldingsData extends { data: Array<infer T> } ? T : HoldingsData extends Array<infer T> ? T : never;
/** /**
* 应用版本信息 * 应用版本信息
*/ */

View File

@@ -304,6 +304,22 @@
}, },
"loadMore": "Load More" "loadMore": "Load More"
}, },
"holdings": {
"title": "My Holdings",
"empty": "No holdings yet",
"nameCode": "Name/Code",
"quantity": "Quantity",
"totalValue": "Total Value",
"purchaseDate": "Purchase Date",
"status": {
"active": "Active",
"tokenized": "Tokenized",
"locked": "Locked"
},
"units": {
"shares": "shares"
}
},
"market": { "market": {
"title": "Market", "title": "Market",
"search": { "search": {

View File

@@ -310,6 +310,22 @@
}, },
"loadMore": "加载更多" "loadMore": "加载更多"
}, },
"holdings": {
"title": "我的持仓",
"empty": "暂无持仓订单",
"nameCode": "名称/编号",
"quantity": "数量",
"totalValue": "总价值",
"purchaseDate": "购买日期",
"status": {
"active": "持有中",
"tokenized": "已代币化",
"locked": "锁定中"
},
"units": {
"shares": "份"
}
},
"market": { "market": {
"title": "市场", "title": "市场",
"search": { "search": {

View File

@@ -1,11 +1,122 @@
<script lang='ts' setup> <script lang='ts' setup>
import type { InfiniteScrollCustomEvent, RefresherCustomEvent } from "@ionic/vue";
import type { HoldingItem } from "@/api/types";
import CryptocurrencyColorNuls from "~icons/cryptocurrency-color/nuls";
import { client, safeClient } from "@/api"; import { client, safeClient } from "@/api";
const { data } = safeClient(client.api.rwa.holdings.get()); const { t } = useI18n();
const holdingsData = ref<HoldingItem[]>([]);
const isLoading = ref(false);
async function fetchHoldings() {
isLoading.value = true;
const { data } = await safeClient(() => client.api.rwa.holdings.get());
if (data.value) {
holdingsData.value = data.value.data || [];
}
isLoading.value = false;
}
async function handleRefresh(event: RefresherCustomEvent) {
await fetchHoldings();
event.target.complete();
}
function getStatusColor(status: string) {
switch (status) {
case "active":
return "success";
case "tokenized":
return "primary";
case "locked":
return "warning";
default:
return "medium";
}
}
function getStatusText(status: HoldingItem["status"]) {
const statusMap = {
active: t("holdings.status.active"),
tokenized: t("holdings.status.tokenized"),
locked: t("holdings.status.locked"),
};
return statusMap[status];
}
onMounted(() => {
fetchHoldings();
});
</script> </script>
<template> <template>
{{ data }} <ion-refresher slot="fixed" @ion-refresh="handleRefresh($event)">
<ion-refresher-content />
</ion-refresher>
<div v-if="isLoading && holdingsData.length === 0" class="flex justify-center items-center h-64">
<ion-spinner />
</div>
<div v-else-if="holdingsData.length === 0" class="flex flex-col justify-center items-center h-64">
<div class="text-text-500">
{{ t('holdings.empty') }}
</div>
</div>
<div v-else class="space-y-3">
<div
v-for="item in holdingsData"
:key="item.id"
class="bg-faint rounded-xl p-3"
>
<div class="flex justify-between items-center mb-3">
<div class="flex items-center gap-2">
<span class="text-sm font-medium">{{ item.product.name }}/{{ item.product.code }}</span>
<ion-badge :color="getStatusColor(item.status)" class="text-[10px] px-1.5 py-0.5">
{{ getStatusText(item.status) }}
</ion-badge>
</div>
<!-- operations -->
</div>
<div class="grid grid-cols-2 gap-2 mb-2">
<div class="flex justify-between text-xs">
<span class="text-(--ion-text-color-step-400)">单价</span>
<span class="text-(--ion-text-color) font-medium">{{ Number(item.quantity).toFixed(2) }}</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-(--ion-text-color-step-400)">数量</span>
<span class="text-(--ion-text-color) font-medium">{{ Number(item.quantity).toFixed(2) }}</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-(--ion-text-color-step-400)">阶段</span>
<span class="text-(--ion-text-color) font-medium">{{ item.edition.editionName }}</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-(--ion-text-color-step-400)">总额</span>
<span class="text-(--ion-text-color) font-medium">{{ item.subscriptionOrder.totalAmount }} USDT</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-(--ion-text-color-step-400)">总分红</span>
<span class="text-(--ion-text-color) font-medium">{{ Number(item.totalDividendReceived).toFixed(2) }} USDT</span>
</div>
</div>
<div class="pt-2">
<span class="text-[11px] text-(--ion-text-color-step-500)">{{ useDateFormat(item.createdAt, 'YYYY/MM/DD HH:mm') }}</span>
</div>
</div>
</div>
</template> </template>
<style lang='css' scoped></style> <style lang='css' scoped>
ion-grid {
cursor: pointer;
transition: background-color 0.2s;
}
ion-grid:active {
background-color: var(--ion-color-light);
}
</style>