feat: 添加资产记录和资金、交易账户视图;更新钱包状态管理和路由配置

This commit is contained in:
2026-01-06 16:01:01 +07:00
parent 3d4babea93
commit 9747f300ac
13 changed files with 325 additions and 58 deletions

2
components.d.ts vendored
View File

@@ -12,6 +12,7 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
Icon: typeof import('./src/components/Icon/index.vue')['default']
IIcBaselineDataSaverOff: typeof import('~icons/ic/baseline-data-saver-off')['default'] IIcBaselineDataSaverOff: typeof import('~icons/ic/baseline-data-saver-off')['default']
IIcBaselineDownloading: typeof import('~icons/ic/baseline-downloading')['default'] IIcBaselineDownloading: typeof import('~icons/ic/baseline-downloading')['default']
IIcBaselineInfo: typeof import('~icons/ic/baseline-info')['default'] IIcBaselineInfo: typeof import('~icons/ic/baseline-info')['default']
@@ -69,6 +70,7 @@ declare module 'vue' {
// For TSX support // For TSX support
declare global { declare global {
const Icon: typeof import('./src/components/Icon/index.vue')['default']
const IIcBaselineDataSaverOff: typeof import('~icons/ic/baseline-data-saver-off')['default'] const IIcBaselineDataSaverOff: typeof import('~icons/ic/baseline-data-saver-off')['default']
const IIcBaselineDownloading: typeof import('~icons/ic/baseline-downloading')['default'] const IIcBaselineDownloading: typeof import('~icons/ic/baseline-downloading')['default']
const IIcBaselineInfo: typeof import('~icons/ic/baseline-info')['default'] const IIcBaselineInfo: typeof import('~icons/ic/baseline-info')['default']

View File

@@ -33,7 +33,7 @@
"@elysiajs/eden": "^1.4.5", "@elysiajs/eden": "^1.4.5",
"@ionic/vue": "^8.7.11", "@ionic/vue": "^8.7.11",
"@ionic/vue-router": "^8.7.11", "@ionic/vue-router": "^8.7.11",
"@riwa/api-types": "http://192.168.1.8:9527/api/riwa-api-types-0.0.75.tgz", "@riwa/api-types": "http://192.168.1.6:9527/api/riwa-eden-0.0.78.tgz",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@vee-validate/yup": "^4.15.1", "@vee-validate/yup": "^4.15.1",
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.1.0",

12
pnpm-lock.yaml generated
View File

@@ -57,8 +57,8 @@ importers:
specifier: ^8.7.11 specifier: ^8.7.11
version: 8.7.11(@stencil/core@4.39.0)(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3)) version: 8.7.11(@stencil/core@4.39.0)(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
'@riwa/api-types': '@riwa/api-types':
specifier: http://192.168.1.8:9527/api/riwa-api-types-0.0.75.tgz specifier: http://192.168.1.6:9527/api/riwa-eden-0.0.78.tgz
version: http://192.168.1.8:9527/api/riwa-api-types-0.0.75.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3))) version: '@riwa/eden@http://192.168.1.6:9527/api/riwa-eden-0.0.78.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))'
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.18 specifier: ^4.1.18
version: 4.1.18(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) version: 4.1.18(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
@@ -2453,9 +2453,9 @@ packages:
'@remirror/core-constants@3.0.0': '@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
'@riwa/api-types@http://192.168.1.8:9527/api/riwa-api-types-0.0.75.tgz': '@riwa/eden@http://192.168.1.6:9527/api/riwa-eden-0.0.78.tgz':
resolution: {tarball: http://192.168.1.8:9527/api/riwa-api-types-0.0.75.tgz} resolution: {tarball: http://192.168.1.6:9527/api/riwa-eden-0.0.78.tgz}
version: 0.0.75 version: 0.0.78
peerDependencies: peerDependencies:
'@elysiajs/eden': ^1.4.5 '@elysiajs/eden': ^1.4.5
@@ -11491,7 +11491,7 @@ snapshots:
'@remirror/core-constants@3.0.0': {} '@remirror/core-constants@3.0.0': {}
'@riwa/api-types@http://192.168.1.8:9527/api/riwa-api-types-0.0.75.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))': '@riwa/eden@http://192.168.1.6:9527/api/riwa-eden-0.0.78.tgz(@elysiajs/eden@1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))':
dependencies: dependencies:
'@elysiajs/eden': 1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)) '@elysiajs/eden': 1.4.5(elysia@1.4.18(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3))

View File

@@ -68,6 +68,10 @@ export type UserWithdrawOrderBody = TreatyQuery<typeof client.api.withdraw.get>;
export type MarketDataStreaming = ReturnType<typeof client.api.market_data.streaming.subscribe>; export type MarketDataStreaming = ReturnType<typeof client.api.market_data.streaming.subscribe>;
export type AssetRecordBody = TreatyQuery<typeof client.api.ledger.entries.get>;
export type AssetRecordData = Treaty.Data<typeof client.api.ledger.entries.get>["data"][number];
/** /**
* 应用版本信息 * 应用版本信息
*/ */

View File

@@ -0,0 +1,22 @@
<script lang='ts' setup>
import type { ComponentInstance } from "vue";
import { Icon } from "@iconify/vue";
defineProps<{
icon: string;
}>();
const vm = getCurrentInstance()!;
function changeRef(exposed) {
vm.exposed = exposed;
}
defineExpose({} as ComponentInstance<typeof Icon>);
</script>
<template>
<Icon v-bind="{ ...$attrs, icon }" :ref="changeRef" />
</template>
<style lang='css' scoped></style>

View File

@@ -94,6 +94,22 @@ const routes: Array<RouteRecordRaw> = [
component: () => import("@/views/wallet/transfer.vue"), component: () => import("@/views/wallet/transfer.vue"),
meta: { requiresAuth: true }, meta: { requiresAuth: true },
}, },
{
path: "/wallet/funding",
component: () => import("@/views/wallet/funding.vue"),
meta: { requiresAuth: true },
},
{
path: "/wallet/trading",
component: () => import("@/views/wallet/trading.vue"),
meta: { requiresAuth: true },
},
{
path: "/asset_record/:code",
props: true,
component: () => import("@/views/wallet/asset-record.vue"),
meta: { requiresAuth: true },
},
{ {
path: "/user/settings", path: "/user/settings",
component: () => import("@/views/user-settings/index.vue"), component: () => import("@/views/user-settings/index.vue"),

View File

@@ -4,6 +4,7 @@ import { client, safeClient } from "@/api";
interface State { interface State {
totalAssetValue: TotalAssetValue; totalAssetValue: TotalAssetValue;
balances: BalancesData;
fundingBalances: BalancesData; fundingBalances: BalancesData;
tradingBalances: BalancesData; tradingBalances: BalancesData;
bankAccounts: BankAccountsData["data"]; bankAccounts: BankAccountsData["data"];
@@ -17,6 +18,7 @@ export const useWalletStore = defineStore("wallet", () => {
tradingValueUsd: "0", tradingValueUsd: "0",
totalValueUsd: "0", totalValueUsd: "0",
}, },
balances: [],
fundingBalances: [], fundingBalances: [],
tradingBalances: [], tradingBalances: [],
bankAccounts: [], bankAccounts: [],
@@ -25,8 +27,7 @@ export const useWalletStore = defineStore("wallet", () => {
async function initializeWallet() { async function initializeWallet() {
syncTotalAssetValue(); syncTotalAssetValue();
syncFundingBalances(); syncBalances();
syncTradingBalances();
syncBankAccounts(); syncBankAccounts();
syncSupportBanks(); syncSupportBanks();
} }
@@ -36,6 +37,12 @@ export const useWalletStore = defineStore("wallet", () => {
if (data.value) if (data.value)
state.totalAssetValue = data.value; state.totalAssetValue = data.value;
} }
async function syncBalances() {
const { data: balances } = await safeClient(() => client.api.wallet.balances.get(), { silent: true });
state.balances = balances.value || [];
}
async function syncFundingBalances() { async function syncFundingBalances() {
const { data: balances } = await safeClient(() => client.api.wallet.balances.get({ const { data: balances } = await safeClient(() => client.api.wallet.balances.get({
query: { accountType: "funding" }, query: { accountType: "funding" },
@@ -67,6 +74,7 @@ export const useWalletStore = defineStore("wallet", () => {
return { return {
...toRefs(state), ...toRefs(state),
initializeWallet, initializeWallet,
syncBalances,
syncFundingBalances, syncFundingBalances,
syncTradingBalances, syncTradingBalances,
syncBankAccounts, syncBankAccounts,

View File

@@ -4,59 +4,58 @@ import SolarRoundTransferHorizontalBoldDuotone from "~icons/solar/round-transfer
import { getCryptoIcon } from "@/config/crypto"; import { getCryptoIcon } from "@/config/crypto";
const walletStore = useWalletStore(); const walletStore = useWalletStore();
const { fundingBalances, totalAssetValue } = storeToRefs(walletStore); const { balances, totalAssetValue } = storeToRefs(walletStore);
</script> </script>
<template> <template>
<div> <div class="text-md font-semibold my-4">
<div class="text-md font-semibold my-4"> 资产分布
资产分布 </div>
</div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div class="asset-card"> <div class="asset-card" @click="$router.push('/wallet/funding')">
<div class="text-xs text-text-400 font-semibold flex items-center gap-1"> <div class="text-xs text-text-400 font-semibold flex items-center gap-1">
<SolarDollarMinimalisticBoldDuotone /> <SolarDollarMinimalisticBoldDuotone />
资金账户 资金账户
</div>
<div class="font-bold">
${{ totalAssetValue.fundingValueUsd }}
</div>
</div> </div>
<div class="asset-card"> <div class="font-bold">
<div class="text-xs text-text-400 font-semibold flex items-center gap-1"> ${{ totalAssetValue.fundingValueUsd }}
<SolarRoundTransferHorizontalBoldDuotone />
交易账户
</div>
<div class="font-bold">
${{ totalAssetValue.tradingValueUsd }}
</div>
</div> </div>
</div> </div>
<div class="asset-card" @click="$router.push('/wallet/trading')">
<div class="text-md font-semibold my-4"> <div class="text-xs text-text-400 font-semibold flex items-center gap-1">
资产 <SolarRoundTransferHorizontalBoldDuotone />
交易账户
</div>
<div class="font-bold">
${{ totalAssetValue.tradingValueUsd }}
</div>
</div> </div>
</div>
<ion-list lines="none" class="space-y-5 mt-2!"> <div class="text-md font-semibold my-4">
<ion-item v-for="asset, i in fundingBalances" :key="i" class=""> 资产
<div class="flex items-center space-x-3 flex-1"> </div>
<component :is="getCryptoIcon(asset.assetCode)" class="w-8 h-8" />
<div class="space-y-1"> <ion-list lines="none" class="space-y-5 mt-2!">
<div class="font-medium text-md"> <ion-item v-for="asset, i in balances" :key="i" @click="$router.push(`/asset_record/${asset.assetCode}`)">
{{ asset.assetCode }} <div class="flex items-center space-x-3 flex-1">
</div> <Icon :icon="asset.asset.iconUrl || ''" class="w-8 h-8" />
<div class="text-xs text-text-700 font-bold">
Total: ${{ asset.total }} <div class="space-y-1">
</div> <div class="font-medium text-md">
{{ asset.assetCode }}
</div>
<div class="text-xs text-text-700 font-bold">
Total: ${{ asset.total }}
</div> </div>
</div> </div>
<div class="w-fit font-bold"> </div>
${{ Number(asset.available) }} <div class="w-fit font-bold">
</div> ${{ Number(asset.available) }}
</ion-item> </div>
</ion-list> </ion-item>
</div> </ion-list>
</template> </template>
<style lang='css' scoped> <style lang='css' scoped>

View File

@@ -16,11 +16,6 @@ const totalAsset = computed(() => Number(totalAssetValue.value.totalValueUsd).to
function onCloseModal() { function onCloseModal() {
rechargeInstance.value?.$el.dismiss(null, "confirm"); rechargeInstance.value?.$el.dismiss(null, "confirm");
} }
onMounted(() => {
walletStore.syncFundingBalances();
walletStore.syncTradingBalances();
});
</script> </script>
<template> <template>

View File

@@ -0,0 +1,83 @@
<script lang='ts' setup>
import type { InfiniteScrollCustomEvent, RefresherCustomEvent } from "@ionic/vue";
import type { AssetRecordBody, AssetRecordData } from "@/api/types";
import { client, safeClient } from "@/api";
const props = defineProps<{ code: string }>();
const [query, resetQuery] = useResetRef<AssetRecordBody>({
limit: 20,
offset: 0,
assetCode: props.code,
});
const data = ref<AssetRecordData[]>([]);
const isFinished = ref(false);
async function fetchData() {
const { data: record } = await safeClient(client.api.ledger.entries.get({
query: query.value,
}));
data.value.push(...(record.value?.data || []));
isFinished.value = (record.value?.data.length || 0) < query.value.limit!;
}
function reset() {
resetQuery();
data.value = [];
isFinished.value = false;
}
async function handleRefresh(event: RefresherCustomEvent) {
reset();
await fetchData();
setTimeout(() => {
event.target.complete();
}, 500);
}
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);
}
onBeforeMount(() => {
fetchData();
});
</script>
<template>
<ion-page>
<ion-header class="ion-no-border">
<ion-toolbar class="ion-toolbar">
<ui-back-button slot="start" />
<ion-title>{{ code }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-refresher slot="fixed" @ion-refresh="handleRefresh($event)">
<ion-refresher-content />
</ion-refresher>
<div class="ion-padding-horizontal text-md font-semibold my-4">
资产记录
</div>
{{ data }}
<ion-infinite-scroll threshold="100px" @ion-infinite="handleInfinite">
<ion-infinite-scroll-content
loading-spinner="bubbles"
loading-text="加载更多中..."
/>
</ion-infinite-scroll>
</ion-content>
</ion-page>
</template>
<style lang='css' scoped></style>

View File

@@ -0,0 +1,69 @@
<script lang='ts' setup>
import { eyeOffOutline, eyeOutline } from "ionicons/icons";
const walletStore = useWalletStore();
const { fundingBalances, totalAssetValue } = storeToRefs(walletStore);
const fundingBalanceVisible = useStorage("funding-balances-visible", true);
onMounted(() => {
walletStore.syncFundingBalances();
});
</script>
<template>
<ion-page>
<ion-header class="ion-no-border">
<ion-toolbar class="ion-toolbar">
<ui-back-button slot="start" />
<ion-title>资金账户</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<div class="flex flex-col gap-1 ion-padding border-b border-text-900 mb-2">
<div class="text-sm text-gray-500 flex items-center gap-2" @click="fundingBalanceVisible = !fundingBalanceVisible">
<div class="text-md">
总资产估值
</div>
<ion-icon :icon="fundingBalanceVisible ? eyeOffOutline : eyeOutline" />
</div>
<div class="flex items-end gap-2">
<div class="text-2xl font-bold">
{{ fundingBalanceVisible ? totalAssetValue.fundingValueUsd : Array(totalAssetValue.fundingValueUsd.toString().length).fill("*").join("") }}
</div>
<div class="text-md font-bold">
USDT
</div>
</div>
</div>
<div class="ion-padding-horizontal text-md font-semibold my-4">
资产
</div>
<ion-list lines="none" class="space-y-5 mt-2!">
<ion-item v-for="asset, i in fundingBalances" :key="i" class="">
<div class="flex items-center space-x-3 flex-1">
<Icon :icon="asset.asset.iconUrl || ''" class="w-8 h-8" />
<div class="space-y-1">
<div class="font-medium text-md">
{{ asset.assetCode }}
</div>
<div class="text-xs text-text-500 font-semibold">
总共: ${{ Number(asset.total).toFixed(2) }}
</div>
<div class="text-xs text-text-500 font-semibold">
冻结: ${{ Number(asset.frozen).toFixed(2) }}
</div>
</div>
</div>
<div class="w-fit font-bold">
${{ Number(asset.available).toFixed(2) }}
</div>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<style lang='css' scoped></style>

View File

@@ -0,0 +1,70 @@
<script lang='ts' setup>
import { eyeOffOutline, eyeOutline } from "ionicons/icons";
const walletStore = useWalletStore();
const { tradingBalances, totalAssetValue } = storeToRefs(walletStore);
const tradingBalanceVisible = useStorage("trading-balances-visible", true);
onMounted(() => {
walletStore.syncTradingBalances();
});
</script>
<template>
<ion-page>
<ion-header class="ion-no-border">
<ion-toolbar class="ion-toolbar">
<ui-back-button slot="start" />
<ion-title>交易账户</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<div class="flex flex-col gap-1 ion-padding border-b border-text-900 mb-2">
<div class="text-sm text-gray-500 flex items-center gap-2" @click="tradingBalanceVisible = !tradingBalanceVisible">
<div class="text-md">
总资产估值
</div>
<ion-icon :icon="tradingBalanceVisible ? eyeOffOutline : eyeOutline" />
</div>
<div class="flex items-end gap-2">
<div class="text-2xl font-bold">
{{ tradingBalanceVisible ? totalAssetValue.tradingValueUsd : Array(totalAssetValue.tradingValueUsd.toString().length).fill("*").join("") }}
</div>
<div class="text-md font-bold">
USDT
</div>
</div>
</div>
<div class="ion-padding-horizontal text-md font-semibold my-4">
资产
</div>
<ion-list lines="none" class="space-y-5 mt-2!">
<ion-item v-for="asset, i in tradingBalances" :key="i">
<div class="flex items-center space-x-3 flex-1">
<Icon :icon="asset.asset.iconUrl || ''" class="w-8 h-8" />
<div class="space-y-1">
<div class="font-medium text-md">
{{ asset.assetCode }}
</div>
<div class="text-xs text-text-500 font-semibold">
总共: ${{ Number(asset.total).toFixed(2) }}
</div>
<div class="text-xs text-text-500 font-semibold">
冻结: ${{ Number(asset.frozen).toFixed(2) }}
</div>
</div>
</div>
<div class="w-fit font-bold">
${{ Number(asset.available).toFixed(2) }}
</div>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<style lang='css' scoped></style>

View File

@@ -104,8 +104,7 @@ async function onSubmit(values: GenericObject) {
await toast.present(); await toast.present();
// 刷新余额 // 刷新余额
walletStore.syncFundingBalances(); walletStore.syncBalances();
walletStore.syncTradingBalances();
router.back(); router.back();
} }