feat: 添加收益明细类型和相关数据结构,优化收益记录展示逻辑

This commit is contained in:
2026-01-11 01:28:26 +07:00
parent 739430693e
commit 1c0d3e7288
7 changed files with 180 additions and 211 deletions

View File

@@ -90,6 +90,10 @@ export type NewData = Treaty.Data<typeof client.api.news.get>["data"][number];
export type EaringsSummaryData = Treaty.Data<typeof client.api.earnings.summary.post>; export type EaringsSummaryData = Treaty.Data<typeof client.api.earnings.summary.post>;
export type EaringsDetailData = Treaty.Data<typeof client.api.earnings.details.post>["data"][number];
export type EaringsDetailBody = TreatyBody<typeof client.api.earnings.details.post>;
/** /**
* 应用版本信息 * 应用版本信息
*/ */

View File

@@ -247,7 +247,18 @@
"totalRevenue": "Total Revenue", "totalRevenue": "Total Revenue",
"monthlyRevenue": "Monthly Revenue", "monthlyRevenue": "Monthly Revenue",
"pendingRevenue": "Pending Revenue", "pendingRevenue": "Pending Revenue",
"revenueDetails": "Revenue Details" "revenueDetails": "Revenue Details",
"types": {
"all": "All",
"dividend": "Dividend",
"staking": "Staking",
"new_user_reward": "New User Reward",
"referral_reward": "Referral Reward",
"trading_fee_rebate": "Trading Fee Rebate",
"deposit_rebate": "Deposit Rebate",
"deposit_reward": "Deposit Reward",
"other": "Other"
}
} }
}, },
"income": { "income": {

View File

@@ -253,7 +253,18 @@
"totalRevenue": "总收益", "totalRevenue": "总收益",
"monthlyRevenue": "月度收益", "monthlyRevenue": "月度收益",
"pendingRevenue": "待确认收益", "pendingRevenue": "待确认收益",
"revenueDetails": "收益明细" "revenueDetails": "收益明细",
"types": {
"all": "全部",
"dividend": "分红收益",
"staking": "质押收益",
"new_user_reward": "新用户奖励",
"referral_reward": "推荐奖励",
"trading_fee_rebate": "交易返佣",
"deposit_rebate": "存款返佣",
"deposit_reward": "存款奖励",
"other": "其他"
}
} }
}, },
"income": { "income": {

View File

@@ -337,7 +337,17 @@ const stickyStyle = computed(() => {
/* 导航包装器 */ /* 导航包装器 */
.ui-tabs__nav-wrapper { .ui-tabs__nav-wrapper {
@apply relative w-fit mb-4; @apply relative w-full mb-4 overflow-x-auto;
}
/* 隐藏滚动条但保持滚动功能 */
.ui-tabs__nav-wrapper::-webkit-scrollbar {
display: none;
}
.ui-tabs__nav-wrapper {
-ms-overflow-style: none;
scrollbar-width: none;
} }
/* Sticky 布局 */ /* Sticky 布局 */
@@ -365,7 +375,9 @@ const stickyStyle = computed(() => {
/* 导航容器 */ /* 导航容器 */
.ui-tabs__nav { .ui-tabs__nav {
@apply relative flex; @apply relative inline-flex;
min-width: min-content;
white-space: nowrap;
} }
.ui-tabs__nav--bar, .ui-tabs__nav--bar,
@@ -395,6 +407,7 @@ const stickyStyle = computed(() => {
.ui-tab { .ui-tab {
@apply relative flex items-center justify-between cursor-pointer transition-all duration-200; @apply relative flex items-center justify-between cursor-pointer transition-all duration-200;
color: var(--ion-color-medium, #6b7280); color: var(--ion-color-medium, #6b7280);
flex-shrink: 0;
} }
.ui-tab:hover:not(.ui-tab--disabled):not(.ui-tab--active) { .ui-tab:hover:not(.ui-tab--disabled):not(.ui-tab--active) {

View File

@@ -5,6 +5,7 @@ import CryptocurrencyColorNuls from "~icons/cryptocurrency-color/nuls";
import { client, safeClient } from "@/api"; import { client, safeClient } from "@/api";
const { t } = useI18n(); const { t } = useI18n();
const { vibrate } = useHaptics();
const holdingsData = ref<HoldingItem[]>([]); const holdingsData = ref<HoldingItem[]>([]);
const isLoading = ref(false); const isLoading = ref(false);
@@ -18,9 +19,11 @@ async function fetchHoldings() {
isLoading.value = false; isLoading.value = false;
} }
async function handleRefresh(event: RefresherCustomEvent) { function handleRefresh(event: RefresherCustomEvent) {
await fetchHoldings(); vibrate();
fetchHoldings().finally(() => {
event.target.complete(); event.target.complete();
});
} }
function getStatusColor(status: string) { function getStatusColor(status: string) {

View File

@@ -0,0 +1,10 @@
export enum RevenueRecordTypeEnum {
dividend = "dividend",
staking = "staking",
new_user_reward = "new_user_reward",
referral_reward = "referral_reward",
trading_fee_rebate = "trading_fee_rebate",
deposit_rebate = "deposit_rebate",
deposit_reward = "deposit_reward",
other = "other",
}

View File

@@ -1,172 +1,78 @@
<script lang='ts' setup> <script lang='ts' setup>
import type { RefresherCustomEvent } from "@ionic/vue"; import type { InfiniteScrollCustomEvent, RefresherCustomEvent } from "@ionic/vue";
import type { EaringsDetailBody, EaringsDetailData } from "@/api/types";
import { client, safeClient } from "@/api";
import { RevenueRecordTypeEnum } from "./enum";
const { t } = useI18n(); const { t } = useI18n();
const { vibrate } = useHaptics(); const { vibrate } = useHaptics();
const loading = ref(true); const isLoading = ref(true);
const selectedType = ref("all"); const isFinished = ref(false);
const searchQuery = ref(""); const records = ref<Array<EaringsDetailData>>([]);
const [query] = useResetRef<EaringsDetailBody>({
// 收益类型选项 limit: 20,
const typeOptions = [ offset: 0,
{ value: "all", label: "全部" }, type: undefined,
{ value: "dividend", label: "分红收益" },
{ value: "appreciation", label: "资产增值" },
{ value: "trade", label: "交易收益" },
];
// 收益记录数据
const allRecords = ref([
{
id: "1",
type: "dividend",
typeName: "分红收益",
assetName: "纽约曼哈顿中心公寓",
assetCode: "NYC-001",
amount: 520.50,
date: "2025-12-27 10:30",
status: "completed",
},
{
id: "2",
type: "appreciation",
typeName: "资产增值",
assetName: "旧金山商业地产",
assetCode: "SF-002",
amount: 320.80,
date: "2025-12-27 09:15",
status: "completed",
},
{
id: "3",
type: "trade",
typeName: "交易收益",
assetName: "洛杉矶住宅楼",
assetCode: "LA-003",
amount: 215.50,
date: "2025-12-26 16:45",
status: "completed",
},
{
id: "4",
type: "dividend",
typeName: "分红收益",
assetName: "迈阿密海景别墅",
assetCode: "MIA-004",
amount: 680.20,
date: "2025-12-26 14:20",
status: "completed",
},
{
id: "5",
type: "appreciation",
typeName: "资产增值",
assetName: "芝加哥写字楼",
assetCode: "CHI-005",
amount: 450.60,
date: "2025-12-25 11:30",
status: "completed",
},
{
id: "6",
type: "trade",
typeName: "交易收益",
assetName: "波士顿商业中心",
assetCode: "BOS-006",
amount: 890.30,
date: "2025-12-24 15:20",
status: "completed",
},
{
id: "7",
type: "dividend",
typeName: "分红收益",
assetName: "西雅图科技园区",
assetCode: "SEA-007",
amount: 720.40,
date: "2025-12-23 13:10",
status: "completed",
},
{
id: "8",
type: "appreciation",
typeName: "资产增值",
assetName: "达拉斯住宅区",
assetCode: "DAL-008",
amount: 380.90,
date: "2025-12-22 10:50",
status: "completed",
},
{
id: "9",
type: "trade",
typeName: "交易收益",
assetName: "奥斯汀高端公寓",
assetCode: "AUS-009",
amount: 1250.20,
date: "2025-12-21 16:30",
status: "completed",
},
{
id: "10",
type: "dividend",
typeName: "分红收益",
assetName: "休斯顿商业综合体",
assetCode: "HOU-010",
amount: 580.70,
date: "2025-12-20 14:15",
status: "completed",
},
]);
// 筛选后的记录
const filteredRecords = computed(() => {
let records = allRecords.value;
// 按类型筛选
if (selectedType.value !== "all") {
records = records.filter(item => item.type === selectedType.value);
}
// 按搜索关键词筛选
if (searchQuery.value.trim()) {
const query = searchQuery.value.toLowerCase();
records = records.filter(item =>
item.assetName.toLowerCase().includes(query)
|| item.assetCode.toLowerCase().includes(query)
|| item.typeName.toLowerCase().includes(query),
);
}
return records;
}); });
async function loadData() { const typeOptions = computed(() => [
loading.value = true; { value: undefined, label: t("asset.revenue.types.all") },
useTimeoutFn(() => { { value: RevenueRecordTypeEnum.dividend, label: t("asset.revenue.types.dividend") },
loading.value = false; { value: RevenueRecordTypeEnum.staking, label: t("asset.revenue.types.staking") },
}, 800); { value: RevenueRecordTypeEnum.new_user_reward, label: t("asset.revenue.types.new_user_reward") },
{ value: RevenueRecordTypeEnum.referral_reward, label: t("asset.revenue.types.referral_reward") },
{ value: RevenueRecordTypeEnum.trading_fee_rebate, label: t("asset.revenue.types.trading_fee_rebate") },
{ value: RevenueRecordTypeEnum.deposit_rebate, label: t("asset.revenue.types.deposit_rebate") },
{ value: RevenueRecordTypeEnum.deposit_reward, label: t("asset.revenue.types.deposit_reward") },
{ value: RevenueRecordTypeEnum.other, label: t("asset.revenue.types.other") },
]);
function resetData() {
query.value.offset = 0;
records.value = [];
isFinished.value = false;
}
async function fetchData() {
isLoading.value = true;
const { data } = await safeClient(client.api.earnings.details.post(query.value));
records.value = data.value?.data || [];
isFinished.value = (data.value?.data.length || 0) < query.value.limit!;
isLoading.value = false;
} }
async function handleRefresh(event: RefresherCustomEvent) { async function handleRefresh(event: RefresherCustomEvent) {
vibrate(); vibrate();
useTimeoutFn(() => { resetData();
fetchData().finally(() => {
event.target.complete(); event.target.complete();
}, 800); });
}
async function handleInfinite(event: InfiniteScrollCustomEvent) {
if (isFinished.value) {
event.target.complete();
event.target.disabled = true;
return;
}
query.value.offset! += query.value.limit!;
await fetchData();
setTimeout(() => {
event.target.complete();
}, 500);
}
function changeType(val) {
resetData();
if (val === "all") {
query.value.type = undefined;
}
else {
query.value.type = val as RevenueRecordTypeEnum;
}
fetchData();
} }
function getTypeColor(type: string) { onBeforeMount(() => {
const colors: Record<string, string> = { fetchData();
dividend: "success",
appreciation: "primary",
trade: "warning",
};
return colors[type] || "medium";
}
onMounted(() => {
loadData();
}); });
</script> </script>
@@ -180,25 +86,29 @@ onMounted(() => {
</ion-header> </ion-header>
<ion-content :fullscreen="true"> <ion-content :fullscreen="true">
<ion-searchbar v-model="searchQuery" :placeholder="t('myIssues.search')" />
<ion-refresher slot="fixed" @ion-refresh="handleRefresh($event)"> <ion-refresher slot="fixed" @ion-refresh="handleRefresh($event)">
<ion-refresher-content /> <ion-refresher-content />
</ion-refresher> </ion-refresher>
<div> <ui-tabs
<ui-tabs size="small" class="ion-padding-horizontal"> size="small"
class="ion-padding-horizontal py-2"
:model-value="query.type || 'all'"
@update:model-value="changeType"
>
<ui-tab-pane <ui-tab-pane
v-for="option in typeOptions" v-for="option in typeOptions"
:key="option.value" :key="option.value"
:name="option.value" :name="option.value || 'all'"
:title="option.label" :title="option.label"
/> />
</ui-tabs> </ui-tabs>
<ion-list> <ui-empty v-if="!isLoading && records.length === 0" />
<ion-list v-else>
<ion-item <ion-item
v-for="item in filteredRecords" v-for="item in records"
:key="item.id" :key="item.id"
lines="full" lines="full"
> >
@@ -206,25 +116,22 @@ onMounted(() => {
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center gap-2 mb-2"> <div class="flex items-center gap-2 mb-2">
<ion-badge <ion-badge class="text-xs px-2 py-0.5">
:color="getTypeColor(item.type)" {{ item.type }}
class="text-xs px-2 py-0.5"
>
{{ item.typeName }}
</ion-badge> </ion-badge>
<ion-badge color="success" class="text-xs px-2 py-0.5"> <ion-badge color="success" class="text-xs px-2 py-0.5">
已完成 已完成
</ion-badge> </ion-badge>
</div> </div>
<div class="text-base font-semibold mb-1"> <div class="text-base font-semibold mb-1">
{{ item.assetName }} {{ item.relatedAssetName }}
</div> </div>
<div class="text-xs text-text-400 mb-2"> <div class="text-xs text-text-400 mb-2">
{{ item.assetCode }} {{ item.relatedAssetCode }}
</div> </div>
<div class="flex items-center gap-1.5 text-xs text-text-400"> <div class="flex items-center gap-1.5 text-xs text-text-400">
<i-ic-outline-access-time class="text-sm" /> <i-ic-outline-access-time class="text-sm" />
<span>{{ item.date }}</span> <span>{{ useDateFormat(item.createdAt, 'YYYY/MM/DD HH:mm') }}</span>
</div> </div>
</div> </div>
<div class="text-right ml-4"> <div class="text-right ml-4">
@@ -236,9 +143,19 @@ onMounted(() => {
</div> </div>
</ion-item> </ion-item>
</ion-list> </ion-list>
</div>
<ion-infinite-scroll threshold="100px" @ion-infinite="handleInfinite">
<ion-infinite-scroll-content
loading-spinner="bubbles"
loading-text="加载中..."
/>
</ion-infinite-scroll>
</ion-content> </ion-content>
</ion-page> </ion-page>
</template> </template>
<style lang='css' scoped></style> <style lang='css' scoped>
:deep(.ui-tabs__nav-wrapper) {
margin-bottom: 0;
}
</style>