feat: 更新环境配置,添加 API 地址,优化数据获取逻辑,支持振动反馈功能

This commit is contained in:
2025-12-18 22:23:18 +07:00
parent 6ceb80e6f2
commit f570cbce84
20 changed files with 259 additions and 92 deletions

1
.env
View File

@@ -1 +0,0 @@
VITE_API_URL=http://192.168.1.36:9527

1
.env.development Normal file
View File

@@ -0,0 +1 @@
VITE_API_URL=http://192.168.1.33:9528

1
.env.production Normal file
View File

@@ -0,0 +1 @@
VITE_API_URL=http://192.168.1.33:9527

View File

@@ -69,6 +69,7 @@ API 集成:
使用 TypeScript 严格模式,避免 any 类型
组件命名使用 PascalCase文件名使用 PascalCase 或 kebab-case
使用 @/ 作为 src 目录的别名
颜色主题使用ion-color 变量,支持深色模式和浅色模式
2. 项目结构
```
src/

5
auto-imports.d.ts vendored
View File

@@ -199,6 +199,7 @@ declare global {
const useFullscreen: typeof import('@vueuse/core').useFullscreen
const useGamepad: typeof import('@vueuse/core').useGamepad
const useGeolocation: typeof import('@vueuse/core').useGeolocation
const useHaptics: typeof import('./src/composables/useVibrate').useHaptics
const useI18n: typeof import('vue-i18n').useI18n
const useId: typeof import('vue').useId
const useIdle: typeof import('@vueuse/core').useIdle
@@ -333,6 +334,9 @@ declare global {
export type { Series, TData, WeightChartOptions, TradingViewData, TradingViewOptions } from './src/composables/useTradingView'
import('./src/composables/useTradingView')
// @ts-ignore
export type { HapticsOptions } from './src/composables/useVibrate'
import('./src/composables/useVibrate')
// @ts-ignore
export type { PageInstance, InputInstance, ModalInstance, FormInstance } from './src/utils/ionic-helper'
import('./src/utils/ionic-helper')
}
@@ -535,6 +539,7 @@ declare module 'vue' {
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useHaptics: UnwrapRef<typeof import('./src/composables/useVibrate')['useHaptics']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>

14
components.d.ts vendored
View File

@@ -20,11 +20,15 @@ declare module 'vue' {
IonBackButton: typeof import('@ionic/vue')['IonBackButton']
IonButton: typeof import('@ionic/vue')['IonButton']
IonButtons: typeof import('@ionic/vue')['IonButtons']
IonCol: typeof import('@ionic/vue')['IonCol']
IonContent: typeof import('@ionic/vue')['IonContent']
IonDatetime: typeof import('@ionic/vue')['IonDatetime']
IonDatetimeButton: typeof import('@ionic/vue')['IonDatetimeButton']
IonGrid: typeof import('@ionic/vue')['IonGrid']
IonHeader: typeof import('@ionic/vue')['IonHeader']
IonIcon: typeof import('@ionic/vue')['IonIcon']
IonInfiniteScroll: typeof import('@ionic/vue')['IonInfiniteScroll']
IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent']
IonInputOtp: typeof import('@ionic/vue')['IonInputOtp']
IonItem: typeof import('@ionic/vue')['IonItem']
IonLabel: typeof import('@ionic/vue')['IonLabel']
@@ -34,7 +38,10 @@ declare module 'vue' {
IonPage: typeof import('@ionic/vue')['IonPage']
IonRadio: typeof import('@ionic/vue')['IonRadio']
IonRadioGroup: typeof import('@ionic/vue')['IonRadioGroup']
IonRefresher: typeof import('@ionic/vue')['IonRefresher']
IonRefresherContent: typeof import('@ionic/vue')['IonRefresherContent']
IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
IonRow: typeof import('@ionic/vue')['IonRow']
IonSearchbar: typeof import('@ionic/vue')['IonSearchbar']
IonSelect: typeof import('@ionic/vue')['IonSelect']
IonSelectOption: typeof import('@ionic/vue')['IonSelectOption']
@@ -70,11 +77,15 @@ declare global {
const IonBackButton: typeof import('@ionic/vue')['IonBackButton']
const IonButton: typeof import('@ionic/vue')['IonButton']
const IonButtons: typeof import('@ionic/vue')['IonButtons']
const IonCol: typeof import('@ionic/vue')['IonCol']
const IonContent: typeof import('@ionic/vue')['IonContent']
const IonDatetime: typeof import('@ionic/vue')['IonDatetime']
const IonDatetimeButton: typeof import('@ionic/vue')['IonDatetimeButton']
const IonGrid: typeof import('@ionic/vue')['IonGrid']
const IonHeader: typeof import('@ionic/vue')['IonHeader']
const IonIcon: typeof import('@ionic/vue')['IonIcon']
const IonInfiniteScroll: typeof import('@ionic/vue')['IonInfiniteScroll']
const IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent']
const IonInputOtp: typeof import('@ionic/vue')['IonInputOtp']
const IonItem: typeof import('@ionic/vue')['IonItem']
const IonLabel: typeof import('@ionic/vue')['IonLabel']
@@ -84,7 +95,10 @@ declare global {
const IonPage: typeof import('@ionic/vue')['IonPage']
const IonRadio: typeof import('@ionic/vue')['IonRadio']
const IonRadioGroup: typeof import('@ionic/vue')['IonRadioGroup']
const IonRefresher: typeof import('@ionic/vue')['IonRefresher']
const IonRefresherContent: typeof import('@ionic/vue')['IonRefresherContent']
const IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
const IonRow: typeof import('@ionic/vue')['IonRow']
const IonSearchbar: typeof import('@ionic/vue')['IonSearchbar']
const IonSelect: typeof import('@ionic/vue')['IonSelect']
const IonSelectOption: typeof import('@ionic/vue')['IonSelectOption']

View File

@@ -10,6 +10,7 @@
"preview": "vite preview",
"run:ios": "ionic capacitor run ios -l --external",
"run:android": "ionic capacitor run android -l --external",
"proxy": "ionic config set -g proxy http://192.168.1.36:9527",
"test:e2e": "cypress run",
"test:unit": "vitest",
"lint": "eslint",
@@ -28,7 +29,7 @@
"@elysiajs/eden": "^1.4.5",
"@ionic/vue": "^8.7.11",
"@ionic/vue-router": "^8.7.11",
"@riwa/api-types": "http://192.168.1.36:9527/api/riwa-api-types-0.0.29.tgz",
"@riwa/api-types": "http://192.168.1.33:9527/api/riwa-api-types-0.0.30.tgz",
"@tailwindcss/vite": "^4.1.18",
"@vee-validate/yup": "^4.15.1",
"@vueuse/core": "^14.1.0",

12
pnpm-lock.yaml generated
View File

@@ -45,8 +45,8 @@ importers:
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))
'@riwa/api-types':
specifier: http://192.168.1.36:9527/api/riwa-api-types-0.0.29.tgz
version: http://192.168.1.36:9527/api/riwa-api-types-0.0.29.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)))
specifier: http://192.168.1.33:9527/api/riwa-api-types-0.0.30.tgz
version: http://192.168.1.33:9527/api/riwa-api-types-0.0.30.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':
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))
@@ -1302,9 +1302,9 @@ packages:
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
'@riwa/api-types@http://192.168.1.36:9527/api/riwa-api-types-0.0.29.tgz':
resolution: {tarball: http://192.168.1.36:9527/api/riwa-api-types-0.0.29.tgz}
version: 0.0.29
'@riwa/api-types@http://192.168.1.33:9527/api/riwa-api-types-0.0.30.tgz':
resolution: {tarball: http://192.168.1.33:9527/api/riwa-api-types-0.0.30.tgz}
version: 0.0.30
peerDependencies:
'@elysiajs/eden': ^1.4.5
@@ -6341,7 +6341,7 @@ snapshots:
'@pkgr/core@0.2.9': {}
'@riwa/api-types@http://192.168.1.36:9527/api/riwa-api-types-0.0.29.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/api-types@http://192.168.1.33:9527/api/riwa-api-types-0.0.30.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:
'@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

@@ -1,5 +1,5 @@
import type { App } from "@riwa/api-types";
import type { Awaitable } from "@vueuse/core";
import type { WatchSource } from "vue";
import { treaty } from "@elysiajs/eden";
import { toastController } from "@ionic/vue";
@@ -12,26 +12,42 @@ const client = treaty<App>(window.location.origin, {
export interface SafeClientOptions {
silent?: boolean;
immediate?: boolean;
watchSource?: WatchSource; // 用于监听的响应式数据源
}
export interface SafeClientReturn<T, E> {
data: Ref<T | null>;
error: Ref<E | null>;
refresh: () => Promise<void>;
isPending: Ref<boolean>;
execute: () => Promise<void>;
onFetchResponse: (callback: (data: T, error: E) => void) => void;
stopWatching?: () => void;
}
export function safeClient<T, E>(
requestPromise: () => Promise<{ data: T; error: E }>,
requestPromise: (() => Promise<{ data: T; error: E }>) | Promise<{ data: T; error: E }>,
options: SafeClientOptions = {},
): SafeClientReturn<T, E> & Promise<SafeClientReturn<T, E>> {
const { immediate = true } = options;
const { immediate = true, watchSource } = options;
const data = ref<T | null>(null);
const error = ref<E | null>(null);
const isPending = ref(false);
let responseCallback: ((data: T, error: E) => void) | null = null;
let stopWatcher: (() => void) | undefined;
const execute = async () => {
const res = await requestPromise();
isPending.value = true;
let request: () => Promise<{ data: T; error: E }>;
if (typeof requestPromise !== "function") {
request = () => Promise.resolve(requestPromise);
}
else {
request = requestPromise;
}
const res = await request().finally(() => {
isPending.value = false;
});
if (res.error) {
let errMsg = "";
@@ -44,6 +60,7 @@ export function safeClient<T, E>(
else if (res.error && "value" in (res.error as unknown as object)) {
errMsg = String((res.error as unknown as { value: string }).value);
}
// if(res.error && typeof res.error === 'object' && 'err' in res.error) {
if (!options.silent) {
const toast = await toastController.create({
message: errMsg,
@@ -69,17 +86,35 @@ export function safeClient<T, E>(
responseCallback = callback;
}
function stopWatching() {
if (stopWatcher) {
stopWatcher();
stopWatcher = undefined;
}
}
// 如果提供了 watchSource则监听其变化
if (watchSource) {
stopWatcher = watch(
watchSource,
() => {
execute();
},
{ immediate: false }, // 不立即执行,避免与 immediate 选项冲突
);
}
const result: SafeClientReturn<T, E> = {
data: data as Ref<T | null>,
error: error as Ref<E | null>,
refresh: execute,
isPending,
execute,
onFetchResponse,
stopWatching,
};
// 创建一个 Promise 并在其上添加属性
const promise = immediate ? execute().then(() => result) : Promise.resolve(result);
// 将 result 的属性添加到 promise 上
Object.assign(promise, result);
return promise as SafeClientReturn<T, E> & Promise<SafeClientReturn<T, E>>;

View File

@@ -45,3 +45,5 @@ export type SupportBanksData = Treaty.Data<typeof client.api.bank_account.banks.
export type AvailableSubscriptionData = Treaty.Data<typeof client.api.rwa.subscription.available_editions.get>;
export type AvailableSubscriptionBody = TreatyQuery<typeof client.api.rwa.subscription.available_editions.get>;
export type RwaData = Treaty.Data<typeof client.api.rwa.subscription.available_editions.get>;

View File

@@ -3,14 +3,20 @@ import type { FieldBindingObject } from "vee-validate";
interface Props extends FieldBindingObject {
label?: string;
formatterValue?: (value: string) => string;
format?: (value: string) => string;
}
const props = defineProps<Props>();
function handleChange(value: string) {
const formattedValue = useDateFormat(value, "YYYY/MM/DD").value;
const formattedValue = props.formatterValue ? props.formatterValue(value) : new Date(value).toISOString();
props.onChange(formattedValue);
}
function formatDisplay(value: string) {
return props.format ? props.format(value) : useDateFormat(value, "YYYY/MM/DD").value;
}
</script>
<template>
@@ -20,7 +26,7 @@ function handleChange(value: string) {
</ion-label>
<ion-datetime-button datetime="datetime" color="primary">
<div slot="date-target">
{{ props.value }}
{{ formatDisplay(props.value) }}
</div>
</ion-datetime-button>
<ion-modal :keep-contents-mounted="true">

View File

@@ -12,6 +12,10 @@
--border-radius: 8px;
}
.ui-header {
background: var(--ion-color-primary-contrast);
}
ion-datetime.ui-datetime {
--background: rgb(255 255 255);
--background-rgb: 255, 255, 255;

View File

@@ -9,7 +9,6 @@ import Done from "./done.vue";
import IssuePeriod from "./issue-period.vue";
const { t } = useI18n();
const now = useNow();
const { data: categories, onFetchResponse } = await safeClient(() => client.api.rwa.issuance.categories.get());
const step = useRouteQuery<number>("step", 1, { transform: v => Number(v), mode: "push" });
@@ -22,9 +21,9 @@ const initialData: RwaIssuanceProductBody = {
editions: [
{
editionName: "",
launchDate: useDateFormat(now.value, "YYYY/MM/DD").value,
launchDate: new Date(),
perUserLimit: "",
subscriptionDeadline: useDateFormat(now.value, "YYYY/MM/DD").value,
subscriptionDeadline: new Date(),
totalSupply: "",
unitPrice: "",
dividendRate: "",

View File

@@ -18,9 +18,9 @@ const now = useNow();
const initialIssuePeriod: RwaIssuanceProductBody["editions"][0] = {
editionName: "",
launchDate: useDateFormat(now.value, "YYYY/MM/DD").value,
launchDate: new Date(),
perUserLimit: "",
subscriptionDeadline: useDateFormat(now.value, "YYYY/MM/DD").value,
subscriptionDeadline: new Date(),
totalSupply: "",
unitPrice: "",
dividendRate: "",
@@ -47,6 +47,11 @@ const schema = toTypedSchema(yup.object({
function handleSubmit(values: GenericObject) {
emit("submit", values.editions);
}
function handleChange(event: Event) {
debugger;
const input = event.target as HTMLInputElement;
return new Date(input.value);
}
</script>
<template>
@@ -71,6 +76,7 @@ function handleSubmit(values: GenericObject) {
<ui-datetime
v-bind="field"
:label="t('asset.issue.apply.launchDate')"
:formatter-value="(val) => new Date(val).toISOString()"
/>
</template>
</Field>
@@ -113,6 +119,7 @@ function handleSubmit(values: GenericObject) {
<ui-datetime
v-bind="field"
:label="t('asset.issue.apply.subscriptionDeadline')"
:formatter-value="(val) => new Date(val).toISOString()"
/>
</template>
</Field>

View File

@@ -7,7 +7,7 @@ const { data: categories } = await safeClient(() => client.api.rwa.issuance.cate
</script>
<template>
<ion-content :scroll-x="true" :scroll-y="false" class="w-full h-10">
<ion-content :scroll-x="true" :scroll-y="false" class="w-full h-6">
<div class="flex items-center whitespace-nowrap space-x-4">
<div class="item" :class="{ active: model === '' }" @click="model = ''">
全部

View File

@@ -0,0 +1,71 @@
<script lang='ts' setup>
import type { RwaData } from "@/api/types";
defineProps<{
data: RwaData["data"];
}>();
</script>
<template>
<ion-list lines="none" class="space-y-2">
<ion-item>
<ion-grid>
<ion-row class="ion-align-items-center text-xs">
<ion-col size="6">
<div>名称/代码</div>
</ion-col>
<ion-col>
<div class="text-right">
阶段
</div>
</ion-col>
<ion-col>
<div class="text-right">
发行日期
</div>
</ion-col>
<ion-col>
<div class="text-right">
申购单价
</div>
</ion-col>
</ion-row>
</ion-grid>
</ion-item>
<ion-item v-for="item in data" :key="item.id">
<ion-grid>
<ion-row class="ion-align-items-center space-y-5">
<ion-col size="6">
<div>
<div class="font-semibold mb-1 truncate">
{{ item.product?.name }}
</div>
<p class="text-xs text-text-500 font-bold">
{{ item.product?.code }}
</p>
</div>
</ion-col>
<ion-col>
<div class="text-xs text-right">
{{ item.editionName }}
</div>
</ion-col>
<ion-col>
<div class="text-xs text-right">
{{ useDateFormat(item.launchDate!, 'MM/DD') }}
</div>
</ion-col>
<ion-col>
<div v-if="item.unitPrice" class="text-right">
<div class="text-lg font-bold text-primary">
${{ Number(item.unitPrice) }}
</div>
</div>
</ion-col>
</ion-row>
</ion-grid>
</ion-item>
</ion-list>
</template>
<style lang='css' scoped></style>

View File

@@ -1,80 +1,86 @@
<script setup lang="ts">
import type { AvailableSubscriptionBody } from "@/api/types";
import type { InfiniteScrollCustomEvent, RefresherCustomEvent } from "@ionic/vue";
import type { AvailableSubscriptionBody, RwaData } from "@/api/types";
import { client, safeClient } from "@/api";
import Category from "./components/category.vue";
import RwaList from "./components/rwa-list.vue";
const [query] = useResetRef<AvailableSubscriptionBody>({
pageIndex: 1,
pageSize: 20,
limit: 20,
offset: 0,
categoryId: "",
});
const { data, refresh } = safeClient(() => client.api.rwa.subscription.available_editions.get({
query: query.value,
}));
const tradingViewContainer = useTemplateRef<HTMLDivElement>("tradingViewContainer");
const rwaData = ref<RwaData["data"]>([]);
const isFinished = ref(false);
const tradingViewData = ref<any[]>([{ open: 10, high: 10.63, low: 9.49, close: 9.55, time: 1642427876 }, { open: 9.55, high: 10.30, low: 9.42, close: 9.94, time: 1642514276 }, { open: 9.94, high: 10.17, low: 9.92, close: 9.78, time: 1642600676 }, { open: 9.78, high: 10.59, low: 9.18, close: 9.51, time: 1642687076 }, { open: 9.51, high: 10.46, low: 9.10, close: 10.17, time: 1642773476 }, { open: 10.17, high: 10.96, low: 10.16, close: 10.47, time: 1642859876 }, { open: 10.47, high: 11.39, low: 10.40, close: 10.81, time: 1642946276 }, { open: 10.81, high: 11.60, low: 10.30, close: 10.75, time: 1643032676 }, { open: 10.75, high: 11.60, low: 10.49, close: 10.93, time: 1643119076 }, { open: 10.93, high: 11.53, low: 10.76, close: 10.96, time: 1643205476 }]);
async function fetchRwaData() {
const { data } = await safeClient(() => client.api.rwa.subscription.available_editions.get({
query: query.value,
}));
rwaData.value.push(...(data.value?.data || []));
isFinished.value = (data.value?.data.length || 0) < query.value.limit!;
}
function resetRwaData() {
query.value.offset = 0;
rwaData.value = [];
isFinished.value = false;
}
async function handleRefresh(event: RefresherCustomEvent) {
resetRwaData();
await fetchRwaData();
setTimeout(() => {
event.target.complete();
}, 500);
}
setInterval(() => {
const lastData = tradingViewData.value[tradingViewData.value.length - 1];
const lastClose = lastData.close;
const lastTime = lastData.time;
const changePercent = (Math.random() - 0.5) * 0.04;
const open = lastClose;
const close = open * (1 + changePercent);
const volatility = Math.random() * 0.02;
const high = Math.max(open, close) * (1 + volatility);
const low = Math.min(open, close) * (1 - volatility);
const newTime = lastTime + 60;
const newData = {
open: Number(open.toFixed(2)),
high: Number(high.toFixed(2)),
low: Number(low.toFixed(2)),
close: Number(close.toFixed(2)),
time: newTime,
};
tradingViewData.value.push(newData);
if (tradingViewData.value.length > 50) {
tradingViewData.value.shift();
async function handleInfinite(event: InfiniteScrollCustomEvent) {
if (isFinished.value) {
event.target.complete();
event.target.disabled = true;
return;
}
}, 1000);
query.value.offset! += query.value.limit!;
await fetchRwaData();
setTimeout(() => {
event.target.complete();
}, 500);
}
const { resize } = useTradingView(tradingViewContainer, {
data: tradingViewData,
watch(() => query.value.categoryId, async () => {
resetRwaData();
await fetchRwaData();
});
watch(query, () => {
refresh();
}, { deep: true });
onBeforeMount(() => {
fetchRwaData();
});
</script>
<template>
<IonPage>
<IonHeader>
<IonToolbar class="ui-toolbar">
<ion-title>Market</ion-title>
</IonToolbar>
<ion-toolbar class="ui-toolbar">
<ion-searchbar />
</ion-toolbar>
<IonHeader class="ion-padding ui-header">
<ion-searchbar placeholder=" Search" />
<Category v-model="query!.categoryId" />
</IonHeader>
<IonContent :fullscreen="true" class="ion-padding">
<ui-tabs sticky>
<ui-tab-pane name="spot" title="Digital Products" class="py-5">
<Category v-model="query!.categoryId" />
<ion-content :scroll-y="true" />
<div ref="tradingViewContainer" class="w-full h-80" />
</ui-tab-pane>
<ui-tab-pane name="futures" title="Tokenized Products" class="py-5">
<div>Futures Market Content</div>
</ui-tab-pane>
</ui-tabs>
<ion-refresher slot="fixed" @ion-refresh="handleRefresh($event)">
<ion-refresher-content />
</ion-refresher>
<RwaList :data="rwaData" />
<ion-infinite-scroll threshold="100px" @ion-infinite="handleInfinite">
<ion-infinite-scroll-content
loading-spinner="bubbles"
loading-text="加载更多..."
/>
</ion-infinite-scroll>
</IonContent>
</IonPage>
</template>
<style scoped>
ion-searchbar {
padding: 0;
}
</style>

View File

@@ -13,7 +13,7 @@ const { t } = useI18n();
const router = useRouter();
const { updateBankAccounts } = useWalletStore();
const { data, refresh, onFetchResponse } = await safeClient(() => client.api.bank_account.get());
const { data, execute, onFetchResponse } = await safeClient(() => client.api.bank_account.get());
const bankCards = computed(() => data.value?.data || []);
onFetchResponse((data) => {
@@ -64,7 +64,7 @@ async function handleSetDefault(card: any) {
});
await toast.present();
refresh();
execute();
}
// 删除银行卡
@@ -89,7 +89,7 @@ async function handleDeleteCard(card: any) {
color: "success",
});
await toast.present();
refresh();
execute();
},
},
],
@@ -99,7 +99,7 @@ async function handleDeleteCard(card: any) {
}
onUpdated(() => {
refresh();
execute();
});
</script>
@@ -115,7 +115,7 @@ onUpdated(() => {
<IonContent :fullscreen="true">
<div class="min-h-full">
<div v-if="bankCards?.length === 0" class="flex flex-col items-center justify-center min-h-[60vh] p-8 text-center">
<div class="w-20 h-20 rounded-full bg-linear-to-br from-indigo-500 to-purple-600 flex items-center justify-center mb-6 shadow-lg shadow-indigo-500/30">
<div class="w-20 h-20 rounded-full bg-[#171717] flex items-center justify-center mb-6 shadow-lg">
<ion-icon :icon="cardOutline" class="text-4xl text-white" />
</div>
<h3 class="text-xl font-semibold text-#151515 dark:text-white mb-2">
@@ -126,7 +126,7 @@ onUpdated(() => {
</p>
<ion-button
expand="block"
class="h-12 font-semibold"
color="success"
@click="handleAddCard"
>
<ion-icon slot="start" :icon="addOutline" />

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { RefresherCustomEvent } from "@ionic/vue";
import { notificationsOutline, scanOutline, settingsOutline } from "ionicons/icons";
import Asset from "./components/asset.vue";
import IssuingAsset from "./components/issuing-asset.vue";
@@ -6,6 +7,17 @@ import MyRevenue from "./components/my-revenue.vue";
import TradeSettings from "./components/trade-settings.vue";
import UserInfo from "./components/user-info.vue";
import WalletCard from "./components/wallet-card.vue";
const { vibrate } = useHaptics();
const walletStore = useWalletStore();
async function handleRefresh(event: RefresherCustomEvent) {
vibrate();
await walletStore.initializeWallet();
setTimeout(() => {
event.target.complete();
}, 500);
}
</script>
<template>
@@ -26,6 +38,10 @@ import WalletCard from "./components/wallet-card.vue";
</ion-toolbar>
</ion-header>
<IonContent :fullscreen="true" class="ion-padding">
<ion-refresher slot="fixed" @ion-refresh="handleRefresh($event)">
<ion-refresher-content />
</ion-refresher>
<div class="flex flex-col space-y-5">
<UserInfo />
<WalletCard />

View File

@@ -1,4 +1,5 @@
import path from "node:path";
import process from "node:process";
import tailwindcss from "@tailwindcss/vite";
import legacy from "@vitejs/plugin-legacy";
import vue from "@vitejs/plugin-vue";
@@ -11,9 +12,7 @@ import { IonicResolver } from "unplugin-vue-components/resolvers";
import components from "unplugin-vue-components/vite";
import { defineConfig } from "vite";
const env = dotenv.config({ path: `.env` }).parsed as Record<string, string>;
console.log("🚀 ~ file: vite.config.ts ~ env:", env);
dotenv.config({ path: `.env.${process.env.NODE_ENV}` });
// https://vitejs.dev/config/
export default defineConfig({
@@ -45,7 +44,7 @@ export default defineConfig({
host: true,
proxy: {
"/api": {
target: env.VITE_API_URL,
target: process.env.VITE_API_URL,
changeOrigin: true,
},
},