feat: 更新capp-eden依赖至0.0.21,优化资产详情页面的记录展示和分页控制逻辑

This commit is contained in:
2026-01-19 05:01:22 +07:00
parent 9cc2b2bea1
commit 3a405cae93
3 changed files with 197 additions and 124 deletions

14
pnpm-lock.yaml generated
View File

@@ -52,8 +52,8 @@ catalogs:
specifier: 8.0.0 specifier: 8.0.0
version: 8.0.0 version: 8.0.0
'@capp/eden': '@capp/eden':
specifier: http://192.168.1.2:9538/api/capp-eden-0.0.19.tgz specifier: http://192.168.1.2:9538/api/capp-eden-0.0.21.tgz
version: 0.0.19 version: 0.0.21
'@cloudflare/workers-types': '@cloudflare/workers-types':
specifier: ^4.20260113.0 specifier: ^4.20260113.0
version: 4.20260116.0 version: 4.20260116.0
@@ -298,7 +298,7 @@ importers:
version: 8.0.0(@capacitor/core@8.0.0) version: 8.0.0(@capacitor/core@8.0.0)
'@capp/eden': '@capp/eden':
specifier: 'catalog:' specifier: 'catalog:'
version: http://192.168.1.2:9538/api/capp-eden-0.0.19.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))) version: http://192.168.1.2:9538/api/capp-eden-0.0.21.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))
'@elysiajs/eden': '@elysiajs/eden':
specifier: 'catalog:' specifier: 'catalog:'
version: 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)) version: 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))
@@ -1182,9 +1182,9 @@ packages:
'@capacitor/synapse@1.0.4': '@capacitor/synapse@1.0.4':
resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==} resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==}
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.19.tgz': '@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.21.tgz':
resolution: {tarball: http://192.168.1.2:9538/api/capp-eden-0.0.19.tgz} resolution: {tarball: http://192.168.1.2:9538/api/capp-eden-0.0.21.tgz}
version: 0.0.19 version: 0.0.21
peerDependencies: peerDependencies:
'@elysiajs/eden': ^1.4.6 '@elysiajs/eden': ^1.4.6
@@ -6903,7 +6903,7 @@ snapshots:
'@capacitor/synapse@1.0.4': {} '@capacitor/synapse@1.0.4': {}
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.19.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))': '@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.21.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))':
dependencies: dependencies:
'@elysiajs/eden': 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)) '@elysiajs/eden': 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))

View File

@@ -18,7 +18,7 @@ catalog:
'@capacitor/keyboard': 8.0.0 '@capacitor/keyboard': 8.0.0
'@capacitor/share': ^8.0.0 '@capacitor/share': ^8.0.0
'@capacitor/status-bar': 8.0.0 '@capacitor/status-bar': 8.0.0
'@capp/eden': http://192.168.1.2:9538/api/capp-eden-0.0.19.tgz '@capp/eden': http://192.168.1.2:9538/api/capp-eden-0.0.21.tgz
'@cloudflare/workers-types': ^4.20260113.0 '@cloudflare/workers-types': ^4.20260113.0
'@elysiajs/eden': ^1.4.6 '@elysiajs/eden': ^1.4.6
'@faker-js/faker': ^10.2.0 '@faker-js/faker': ^10.2.0

View File

@@ -1,50 +1,115 @@
<script lang='ts' setup> <script lang='ts' setup>
import dayjs from "dayjs";
import { arrowDownCircleOutline, arrowUpCircleOutline, listOutline, walletOutline } from "ionicons/icons"; import { arrowDownCircleOutline, arrowUpCircleOutline, listOutline, walletOutline } from "ionicons/icons";
import { client, safeClient } from "@/api"; import { client, safeClient } from "@/api";
const walletStore = useWalletStore(); const walletStore = useWalletStore();
const balanceWallet = await walletStore.getWalletByType("balance"); const balanceWallet = await walletStore.getWalletByType("balance");
const profitWallet = await walletStore.getWalletByType("profit"); const profitWallet = await walletStore.getWalletByType("profit");
const { data } = await safeClient(client.api.ledger.entries.get());
const [allQuery] = useResetRef<any>({
pageIndex: 1,
pageSize: 10,
});
const [incomeQuery] = useResetRef<any>({
entryType: "subscription_unlock",
pageIndex: 1,
pageSize: 10,
});
const [investmentQuery] = useResetRef<any>({
entryType: "subscription_purchase",
pageIndex: 1,
pageSize: 10,
});
const { data: allRecords, execute: executeAll } = safeClient(() => client.api.ledger.entries.get({ query: { ...allQuery.value } }));
const { data: incomeRecords, execute: executeIncome } = safeClient(() => client.api.ledger.entries.get({ query: { ...incomeQuery.value } }));
const { data: investmentRecords, execute: executeInvestment } = safeClient(() => client.api.ledger.entries.get({ query: { ...investmentQuery.value } }));
// 当前选中的标签 // 当前选中的标签
const selectedTab = ref<"all" | "income" | "investment">("all"); const selectedTab = ref<"all" | "income" | "investment">("all");
interface TransactionRecord { // 获取当前标签对应的数据源和执行函数
id: number; const currentData = computed(() => {
type: "income" | "investment" | "withdraw" | "recharge"; switch (selectedTab.value) {
title: string; case "all":
amount: number; return allRecords;
time: string; case "income":
status: "success" | "pending" | "failed"; return incomeRecords;
description?: string; case "investment":
return investmentRecords;
default:
return allRecords;
}
});
const currentExecute = computed(() => {
switch (selectedTab.value) {
case "all":
return executeAll;
case "income":
return executeIncome;
case "investment":
return executeInvestment;
default:
return executeAll;
}
});
const currentQuery = computed(() => {
switch (selectedTab.value) {
case "all":
return allQuery;
case "income":
return incomeQuery;
case "investment":
return investmentQuery;
default:
return allQuery;
}
});
// 当前标签的记录列表API返回的已分页数据
const filteredRecords = computed(() => {
return currentData.value.value?.data || [];
});
// 全部记录数量
const totalRecords = computed(() => {
return currentData.value.value?.pagination.total || 0;
});
// 总页数
const totalPages = computed(() => {
return Math.ceil(totalRecords.value / currentQuery.value.value.pageSize);
});
// 当前页码
const currentPage = computed(() => {
return currentQuery.value.value.pageIndex;
});
function handleTabChange(event: CustomEvent) {
selectedTab.value = event.detail.value;
// 切换标签时重置到第一页
currentQuery.value.value.pageIndex = 1;
currentExecute.value();
} }
// 筛选后的记录 async function goToPage(page: number) {
const filteredRecords = computed(() => { if (page >= 1 && page <= totalPages.value) {
if (selectedTab.value === "all") { currentQuery.value.value.pageIndex = page;
return allRecords.value; await currentExecute.value();
} }
else if (selectedTab.value === "income") { }
return allRecords.value.filter(r => r.type === "income" || r.type === "recharge");
}
else {
return allRecords.value.filter(r => r.type === "investment" || r.type === "withdraw");
}
});
// 统计数据 function prevPage() {
const totalIncome = computed(() => { goToPage(currentPage.value - 1);
return allRecords.value }
.filter(r => r.amount > 0)
.reduce((sum, r) => sum + r.amount, 0);
});
const totalInvestment = computed(() => { function nextPage() {
return Math.abs(allRecords.value goToPage(currentPage.value + 1);
.filter(r => r.amount < 0) }
.reduce((sum, r) => sum + r.amount, 0));
});
function getRecordIcon(type: string) { function getRecordIcon(type: string) {
if (type === "income" || type === "recharge") { if (type === "income" || type === "recharge") {
@@ -70,8 +135,9 @@ function getTypeName(type: string) {
return names[type] || type; return names[type] || type;
} }
function formatAmount(amount: number) { function formatAmount(amount: string) {
return amount >= 0 ? `+${amount.toFixed(2)}` : amount.toFixed(2); const num = Number.parseFloat(amount);
return num >= 0 ? `+${num.toFixed(2)}` : num.toFixed(2);
} }
</script> </script>
@@ -100,58 +166,26 @@ function formatAmount(amount: number) {
</div> </div>
</div> </div>
</div> </div>
<div class="stats-section">
<div class="stat-item">
<div class="stat-label">
累计收益
</div>
<div class="stat-value income">
+¥{{ totalIncome.toFixed(2) }}
</div>
</div>
<div class="stat-divider" />
<div class="stat-item">
<div class="stat-label">
累计投资
</div>
<div class="stat-value investment">
-¥{{ totalInvestment.toFixed(2) }}
</div>
</div>
</div>
</div> </div>
<!-- 标签切换 --> <!-- 标签切换 -->
<div class="tabs-container"> <div class="tabs-container">
<div class="tabs"> <ion-segment :value="selectedTab" class="tab-segment" @ion-change="handleTabChange">
<div <ion-segment-button value="all">
class="tab" <ion-label>全部记录</ion-label>
:class="{ active: selectedTab === 'all' }" </ion-segment-button>
@click="selectedTab = 'all'" <ion-segment-button value="income">
> <ion-label>收益记录</ion-label>
全部记录 </ion-segment-button>
</div> <ion-segment-button value="investment">
<div <ion-label>投资记录</ion-label>
class="tab" </ion-segment-button>
:class="{ active: selectedTab === 'income' }" </ion-segment>
@click="selectedTab = 'income'"
>
收益记录
</div>
<div
class="tab"
:class="{ active: selectedTab === 'investment' }"
@click="selectedTab = 'investment'"
>
投资记录
</div>
</div>
</div> </div>
<!-- 记录列表 --> <!-- 记录列表 -->
<div class="records-list"> <div class="records-list">
<div v-if="data?.data.length === 0" class="empty-state"> <div v-if="filteredRecords.length === 0" class="empty-state">
<empty title="暂无记录"> <empty title="暂无记录">
<template #icon> <template #icon>
<ion-icon :icon="listOutline" class="empty-icon" /> <ion-icon :icon="listOutline" class="empty-icon" />
@@ -160,41 +194,57 @@ function formatAmount(amount: number) {
</div> </div>
<div v-else class="ion-padding-horizontal"> <div v-else class="ion-padding-horizontal">
<div <div
v-for="record in data?.data" v-for="record in filteredRecords"
:key="record.id" :key="record.id"
class="record-item" class="record-item"
> >
<div class="record-left"> <div class="record-left">
<ion-icon
:icon="getRecordIcon(record.type)"
class="record-icon"
:class="getRecordColor(record.type)"
/>
<div class="record-info"> <div class="record-info">
<div class="record-title"> <div class="record-title">
{{ record.title }} {{ record.memo || '无标题' }}
</div> </div>
<div class="record-desc"> <div class="record-desc">
{{ record.description }} {{ record.walletType.name }}
</div> </div>
<div class="record-time"> <div class="record-time">
{{ record.time }} {{ dayjs(record.createdAt).format('YYYY-MM-DD HH:mm') }}
</div> </div>
</div> </div>
</div> </div>
<div class="record-right"> <div class="record-right">
<div <div
class="record-amount" class="record-amount"
:class="record.amount >= 0 ? 'income' : 'expense'"
> >
{{ formatAmount(record.amount) }} {{ formatAmount(record.amount) }}
</div> </div>
<div class="record-type">
{{ getTypeName(record.type) }}
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 分页控制 -->
<div v-if="totalRecords > 0 && totalPages > 1" class="pagination-section">
<div class="pagination-info">
{{ totalRecords }} {{ currentPage }}/{{ totalPages }}
</div>
<div class="pagination-buttons">
<ion-button
fill="outline"
size="small"
:disabled="currentPage === 1"
@click="prevPage"
>
上一页
</ion-button>
<ion-button
fill="outline"
size="small"
:disabled="currentPage === totalPages"
@click="nextPage"
>
下一页
</ion-button>
</div>
</div>
</div> </div>
</ion-content> </ion-content>
</ion-page> </ion-page>
@@ -277,35 +327,20 @@ function formatAmount(amount: number) {
z-index: 10; z-index: 10;
} }
.tabs { .tab-segment {
display: flex; margin-bottom: 16px;
gap: 8px; --background: #f5f5f5;
border-radius: 12px;
padding: 4px;
} }
.tab { .tab-segment ion-segment-button {
flex: 1; --indicator-color: #c41e3a;
text-align: center; --color: #666;
padding: 12px 16px; --color-checked: white;
font-size: 15px; min-height: 36px;
font-size: 14px;
font-weight: 500; font-weight: 500;
color: #666;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tab.active {
color: #c41e3a;
}
.tab.active::after {
content: "";
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: #c41e3a;
} }
.records-list { .records-list {
@@ -393,4 +428,42 @@ function formatAmount(amount: number) {
font-size: 80px; font-size: 80px;
color: #ddd; color: #ddd;
} }
.pagination-section {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid #e8e8e8;
}
.pagination-info {
text-align: center;
font-size: 13px;
color: #666;
margin-bottom: 12px;
}
.pagination-buttons {
display: flex;
justify-content: center;
gap: 12px;
}
.pagination-buttons ion-button {
--border-color: #c41e3a;
--color: #c41e3a;
--border-radius: 8px;
font-size: 14px;
font-weight: 500;
min-width: 80px;
}
.pagination-buttons ion-button:not([disabled]):hover {
--background: rgba(196, 30, 58, 0.05);
}
.pagination-buttons ion-button[disabled] {
--border-color: #ddd;
--color: #999;
opacity: 0.5;
}
</style> </style>