feat: 更新 API 地址,添加分类组件,优化市场页面布局和功能

This commit is contained in:
2025-12-17 20:27:58 +07:00
parent f3b7931d78
commit 5b5fcf9d44
12 changed files with 198 additions and 56 deletions

2
.env
View File

@@ -1 +1 @@
VITE_API_URL=http://192.168.1.36:9527 VITE_API_URL=http://192.168.1.54:9528

20
components.d.ts vendored
View File

@@ -24,17 +24,27 @@ declare module 'vue' {
InputLabel: typeof import('./src/components/ui/input-label/index.vue')['default'] InputLabel: typeof import('./src/components/ui/input-label/index.vue')['default']
IonApp: typeof import('@ionic/vue')['IonApp'] IonApp: typeof import('@ionic/vue')['IonApp']
IonAvatar: typeof import('@ionic/vue')['IonAvatar'] IonAvatar: typeof import('@ionic/vue')['IonAvatar']
IonBackButton: typeof import('@ionic/vue')['IonBackButton']
IonButton: typeof import('@ionic/vue')['IonButton'] IonButton: typeof import('@ionic/vue')['IonButton']
IonButtons: typeof import('@ionic/vue')['IonButtons']
IonContent: typeof import('@ionic/vue')['IonContent'] IonContent: typeof import('@ionic/vue')['IonContent']
IonDatetime: typeof import('@ionic/vue')['IonDatetime']
IonDatetimeButton: typeof import('@ionic/vue')['IonDatetimeButton']
IonHeader: typeof import('@ionic/vue')['IonHeader'] IonHeader: typeof import('@ionic/vue')['IonHeader']
IonIcon: typeof import('@ionic/vue')['IonIcon'] IonIcon: typeof import('@ionic/vue')['IonIcon']
IonInputOtp: typeof import('@ionic/vue')['IonInputOtp']
IonItem: typeof import('@ionic/vue')['IonItem'] IonItem: typeof import('@ionic/vue')['IonItem']
IonLabel: typeof import('@ionic/vue')['IonLabel'] IonLabel: typeof import('@ionic/vue')['IonLabel']
IonList: typeof import('@ionic/vue')['IonList'] IonList: typeof import('@ionic/vue')['IonList']
IonModal: typeof import('@ionic/vue')['IonModal'] IonModal: typeof import('@ionic/vue')['IonModal']
IonNote: typeof import('@ionic/vue')['IonNote']
IonPage: typeof import('@ionic/vue')['IonPage'] IonPage: typeof import('@ionic/vue')['IonPage']
IonRadio: typeof import('@ionic/vue')['IonRadio']
IonRadioGroup: typeof import('@ionic/vue')['IonRadioGroup']
IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet'] IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
IonSearchbar: typeof import('@ionic/vue')['IonSearchbar'] IonSearchbar: typeof import('@ionic/vue')['IonSearchbar']
IonSelect: typeof import('@ionic/vue')['IonSelect']
IonSelectOption: typeof import('@ionic/vue')['IonSelectOption']
IonTabBar: typeof import('@ionic/vue')['IonTabBar'] IonTabBar: typeof import('@ionic/vue')['IonTabBar']
IonTabButton: typeof import('@ionic/vue')['IonTabButton'] IonTabButton: typeof import('@ionic/vue')['IonTabButton']
IonTabs: typeof import('@ionic/vue')['IonTabs'] IonTabs: typeof import('@ionic/vue')['IonTabs']
@@ -73,17 +83,27 @@ declare global {
const InputLabel: typeof import('./src/components/ui/input-label/index.vue')['default'] const InputLabel: typeof import('./src/components/ui/input-label/index.vue')['default']
const IonApp: typeof import('@ionic/vue')['IonApp'] const IonApp: typeof import('@ionic/vue')['IonApp']
const IonAvatar: typeof import('@ionic/vue')['IonAvatar'] const IonAvatar: typeof import('@ionic/vue')['IonAvatar']
const IonBackButton: typeof import('@ionic/vue')['IonBackButton']
const IonButton: typeof import('@ionic/vue')['IonButton'] const IonButton: typeof import('@ionic/vue')['IonButton']
const IonButtons: typeof import('@ionic/vue')['IonButtons']
const IonContent: typeof import('@ionic/vue')['IonContent'] const IonContent: typeof import('@ionic/vue')['IonContent']
const IonDatetime: typeof import('@ionic/vue')['IonDatetime']
const IonDatetimeButton: typeof import('@ionic/vue')['IonDatetimeButton']
const IonHeader: typeof import('@ionic/vue')['IonHeader'] const IonHeader: typeof import('@ionic/vue')['IonHeader']
const IonIcon: typeof import('@ionic/vue')['IonIcon'] const IonIcon: typeof import('@ionic/vue')['IonIcon']
const IonInputOtp: typeof import('@ionic/vue')['IonInputOtp']
const IonItem: typeof import('@ionic/vue')['IonItem'] const IonItem: typeof import('@ionic/vue')['IonItem']
const IonLabel: typeof import('@ionic/vue')['IonLabel'] const IonLabel: typeof import('@ionic/vue')['IonLabel']
const IonList: typeof import('@ionic/vue')['IonList'] const IonList: typeof import('@ionic/vue')['IonList']
const IonModal: typeof import('@ionic/vue')['IonModal'] const IonModal: typeof import('@ionic/vue')['IonModal']
const IonNote: typeof import('@ionic/vue')['IonNote']
const IonPage: typeof import('@ionic/vue')['IonPage'] const IonPage: typeof import('@ionic/vue')['IonPage']
const IonRadio: typeof import('@ionic/vue')['IonRadio']
const IonRadioGroup: typeof import('@ionic/vue')['IonRadioGroup']
const IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet'] const IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
const IonSearchbar: typeof import('@ionic/vue')['IonSearchbar'] const IonSearchbar: typeof import('@ionic/vue')['IonSearchbar']
const IonSelect: typeof import('@ionic/vue')['IonSelect']
const IonSelectOption: typeof import('@ionic/vue')['IonSelectOption']
const IonTabBar: typeof import('@ionic/vue')['IonTabBar'] const IonTabBar: typeof import('@ionic/vue')['IonTabBar']
const IonTabButton: typeof import('@ionic/vue')['IonTabButton'] const IonTabButton: typeof import('@ionic/vue')['IonTabButton']
const IonTabs: typeof import('@ionic/vue')['IonTabs'] const IonTabs: typeof import('@ionic/vue')['IonTabs']

View File

@@ -23,7 +23,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.36:9527/api/riwa-api-types-0.0.22.tgz", "@riwa/api-types": "http://192.168.1.36:9527/api/riwa-api-types-0.0.24.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

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

@@ -1,4 +1,5 @@
import type { App } from "@riwa/api-types"; import type { App } from "@riwa/api-types";
import type { Awaitable } from "@vueuse/core";
import { treaty } from "@elysiajs/eden"; import { treaty } from "@elysiajs/eden";
import { toastController } from "@ionic/vue"; import { toastController } from "@ionic/vue";
@@ -13,10 +14,17 @@ export interface SafeClientOptions {
immediate?: boolean; immediate?: boolean;
} }
export async function safeClient<T, E>( export interface SafeClientReturn<T, E> {
data: Ref<T | null>;
error: Ref<E | null>;
refresh: () => Promise<void>;
onFetchResponse: (callback: (data: T, error: E) => void) => void;
}
export function safeClient<T, E>(
requestPromise: () => Promise<{ data: T; error: E }>, requestPromise: () => Promise<{ data: T; error: E }>,
options: SafeClientOptions = {}, options: SafeClientOptions = {},
) { ): SafeClientReturn<T, E> & Promise<SafeClientReturn<T, E>> {
const { immediate = true } = options; const { immediate = true } = options;
const data = ref<T | null>(null); const data = ref<T | null>(null);
const error = ref<E | null>(null); const error = ref<E | null>(null);
@@ -61,11 +69,20 @@ export async function safeClient<T, E>(
responseCallback = callback; responseCallback = callback;
} }
if (immediate) { const result: SafeClientReturn<T, E> = {
await execute(); data: data as Ref<T | null>,
} error: error as Ref<E | null>,
refresh: execute,
onFetchResponse,
};
return { data, error, refresh: execute, onFetchResponse }; // 创建一个 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>>;
} }
export { client }; export { client };

View File

@@ -7,6 +7,10 @@ export type DepositFiatBody = Parameters<typeof client.api.deposit.fiat.post>[0]
assetCode: AssetCodeEnum; assetCode: AssetCodeEnum;
}; };
export type TreatyQuery<T> = T extends (...args: any[]) => any
? NonNullable<NonNullable<Parameters<T>[0]>["query"]>
: never;
export type DepositFiatData = Treaty.Data<typeof client.api.deposit.fiat.post>; export type DepositFiatData = Treaty.Data<typeof client.api.deposit.fiat.post>;
export type BalancesData = Treaty.Data<typeof client.api.asset.balances.get>; export type BalancesData = Treaty.Data<typeof client.api.asset.balances.get>;
@@ -18,18 +22,22 @@ export type WithdrawBody = Omit<Parameters<typeof client.api.withdraw.post>[0],
export type UserProfileData = Treaty.Data<typeof client.api.user.profile.get>["userProfile"]; export type UserProfileData = Treaty.Data<typeof client.api.user.profile.get>["userProfile"];
export type UpdateUserProfileBody = NonNullable<Parameters<typeof client.api.user.profile.put>[0]>; export type UpdateUserProfileBody = TreatyQuery<typeof client.api.user.profile.put>;
export type RwaIssuanceProductsData = Treaty.Data<typeof client.api.rwa.issuance.products.bundle.post>; export type RwaIssuanceProductsData = Treaty.Data<typeof client.api.rwa.issuance.products.bundle.post>;
export type RwaIssuanceProductBody = NonNullable<Parameters<typeof client.api.rwa.issuance.products.bundle.post>[0]>; export type RwaIssuanceProductBody = TreatyQuery<typeof client.api.rwa.issuance.products.bundle.post>;
export type RwaIssuanceCategoriesData = Treaty.Data<typeof client.api.rwa.issuance.categories.get>; export type RwaIssuanceCategoriesData = Treaty.Data<typeof client.api.rwa.issuance.categories.get>;
export type BankAccountsData = Treaty.Data<typeof client.api.bank_account.get>; export type BankAccountsData = Treaty.Data<typeof client.api.bank_account.get>;
export type BankAccountBody = Parameters<typeof client.api.bank_account.post>[0]; export type BankAccountBody = TreatyQuery<typeof client.api.bank_account.post>;
export type BankAccountData = Treaty.Data<typeof client.api.bank_account.post>; export type BankAccountData = Treaty.Data<typeof client.api.bank_account.post>;
export type SupportBanksData = Treaty.Data<typeof client.api.bank_account.banks.get>; export type SupportBanksData = Treaty.Data<typeof client.api.bank_account.banks.get>;
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>;

View File

@@ -7,6 +7,8 @@ interface TabsProps {
closable?: boolean; closable?: boolean;
addable?: boolean; addable?: boolean;
placement?: "top" | "bottom" | "left" | "right"; placement?: "top" | "bottom" | "left" | "right";
sticky?: boolean;
stickyTop?: string | number;
tabStyle?: string | Record<string, any>; tabStyle?: string | Record<string, any>;
tabClass?: string; tabClass?: string;
paneStyle?: string | Record<string, any>; paneStyle?: string | Record<string, any>;
@@ -27,6 +29,8 @@ const props = withDefaults(defineProps<TabsProps>(), {
closable: false, closable: false,
addable: false, addable: false,
placement: "top", placement: "top",
sticky: false,
stickyTop: 0,
}); });
const emit = defineEmits<TabsEmits>(); const emit = defineEmits<TabsEmits>();
@@ -229,14 +233,34 @@ const tabsClasses = computed(() => [
`ui-tabs--${props.placement}`, `ui-tabs--${props.placement}`,
{ {
"ui-tabs--animated": props.animated, "ui-tabs--animated": props.animated,
"ui-tabs--sticky": props.sticky,
}, },
]); ]);
// 计算sticky样式
const stickyStyle = computed(() => {
if (!props.sticky)
return {};
const topValue = typeof props.stickyTop === "number"
? `${props.stickyTop}px`
: props.stickyTop;
return {
position: "sticky" as const,
top: topValue,
zIndex: 100,
};
});
</script> </script>
<template> <template>
<div :class="tabsClasses"> <div :class="tabsClasses">
<!-- 标签头部 --> <div
<div class="ui-tabs__nav-wrapper" :class="[`ui-tabs__nav-wrapper--${placement}`]"> class="ui-tabs__nav-wrapper"
:class="[`ui-tabs__nav-wrapper--${placement}`]"
:style="props.sticky ? stickyStyle : {}"
>
<div <div
ref="tabsRef" ref="tabsRef"
class="ui-tabs__nav" class="ui-tabs__nav"
@@ -304,11 +328,11 @@ const tabsClasses = computed(() => [
.ui-tabs { .ui-tabs {
@apply w-full; @apply w-full;
--ui-tabs-primary: var(--ion-color-primary, #007aff); --ui-tabs-primary: var(--ion-color-primary);
--ui-tabs-primary-rgb: var(--ion-color-primary-rgb, 0, 122, 255); --ui-tabs-primary-rgb: var(--ion-color-primary-rgb);
--ui-tabs-background: var(--ion-background-color, #ffffff); --ui-tabs-background: var(--ion-background-color);
--ui-tabs-text: var(--ion-text-color, #000000); --ui-tabs-text: var(--ion-text-color);
--ui-tabs-text-rgb: var(--ion-text-color-rgb, 0, 0, 0); --ui-tabs-text-rgb: var(--ion-text-color-rgb);
} }
/* 导航包装器 */ /* 导航包装器 */
@@ -316,7 +340,15 @@ const tabsClasses = computed(() => [
@apply relative; @apply relative;
} }
.ui-tabs__nav-wrapper--top { /* Sticky 布局 */
.ui-tabs--sticky .ui-tabs__nav-wrapper {
background-color: var(--ui-tabs-background);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.ui-tabs--sticky.ui-tabs--animated .ui-tabs__nav-wrapper {
transition: background-color 0.2s ease;
} }
.ui-tabs__nav-wrapper--bottom { .ui-tabs__nav-wrapper--bottom {
@@ -365,13 +397,13 @@ const tabsClasses = computed(() => [
color: var(--ion-color-medium, #6b7280); color: var(--ion-color-medium, #6b7280);
} }
.ui-tab:hover { .ui-tab:hover:not(.ui-tab--disabled):not(.ui-tab--active) {
color: var(--ion-color-dark, #374151); color: var(--ion-color-primary) !important;
} }
.ui-tab--active { .ui-tab--active {
@apply font-medium; @apply font-medium;
color: var(--ui-tabs-primary); color: var(--ion-color-primary) !important;
} }
.ui-tab--disabled { .ui-tab--disabled {
@@ -385,11 +417,7 @@ const tabsClasses = computed(() => [
} }
.ui-tab--bar.ui-tab--active { .ui-tab--bar.ui-tab--active {
border-color: var(--ui-tabs-primary); border-color: var(--ion-color-primary) !important;
}
.ui-tab--bar:hover:not(.ui-tab--disabled):not(.ui-tab--active) {
background-color: rgba(var(--ui-tabs-primary-rgb), 0.05);
} }
/* Line 类型 */ /* Line 类型 */
@@ -397,10 +425,6 @@ const tabsClasses = computed(() => [
@apply py-3; @apply py-3;
} }
.ui-tab--line:hover:not(.ui-tab--disabled):not(.ui-tab--active) {
background-color: rgba(var(--ui-tabs-primary-rgb), 0.05);
}
/* Segment 类型 */ /* Segment 类型 */
.ui-tab--segment { .ui-tab--segment {
@apply px-3 py-2 rounded-md; @apply px-3 py-2 rounded-md;
@@ -413,10 +437,6 @@ const tabsClasses = computed(() => [
0 1px 2px 0 rgba(0, 0, 0, 0.06); 0 1px 2px 0 rgba(0, 0, 0, 0.06);
} }
.ui-tab--segment:hover:not(.ui-tab--disabled):not(.ui-tab--active) {
background-color: rgba(var(--ui-tabs-primary-rgb), 0.1);
}
/* 尺寸变化 */ /* 尺寸变化 */
.ui-tab--small { .ui-tab--small {
@apply text-sm; @apply text-sm;
@@ -574,6 +594,8 @@ const tabsClasses = computed(() => [
/* Dark mode 支持 */ /* Dark mode 支持 */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.ui-tabs { .ui-tabs {
--ui-tabs-primary: var(--ion-color-primary, #ffffff);
--ui-tabs-primary-rgb: var(--ion-color-primary-rgb, 255, 255, 255);
--ui-tabs-background: var(--ion-background-color, #1a1a1a); --ui-tabs-background: var(--ion-background-color, #1a1a1a);
--ui-tabs-text: var(--ion-text-color, #ffffff); --ui-tabs-text: var(--ion-text-color, #ffffff);
} }

View File

@@ -2,13 +2,13 @@
import type { GenericObject } from "vee-validate"; import type { GenericObject } from "vee-validate";
import type { RwaIssuanceCategoriesData, RwaIssuanceProductBody } from "@/api/types"; import type { RwaIssuanceCategoriesData, RwaIssuanceProductBody } from "@/api/types";
import { toTypedSchema } from "@vee-validate/yup"; import { toTypedSchema } from "@vee-validate/yup";
import { ErrorMessage, Field, FieldArray, Form } from "vee-validate"; import { ErrorMessage, Field, Form } from "vee-validate";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import * as yup from "yup"; import * as yup from "yup";
const props = defineProps<{ const props = defineProps<{
initialData: RwaIssuanceProductBody["product"]; initialData: RwaIssuanceProductBody["product"];
categories: RwaIssuanceCategoriesData; categories: RwaIssuanceCategoriesData | null;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: "next", values: GenericObject): void; (e: "next", values: GenericObject): void;
@@ -64,7 +64,7 @@ function handleSubmit(values: GenericObject) {
<Field name="categoryId" type="text"> <Field name="categoryId" type="text">
<template #default="{ field }"> <template #default="{ field }">
<ion-select class="ui-select" interface="action-sheet" toggle-icon="" v-bind="field" :label="t('asset.issue.apply.productType')" :placeholder="t('asset.issue.apply.chooseProductType')"> <ion-select class="ui-select" interface="action-sheet" toggle-icon="" v-bind="field" :label="t('asset.issue.apply.productType')" :placeholder="t('asset.issue.apply.chooseProductType')">
<ion-select-option v-for="item in categories" :key="item.id" :value="item.id"> <ion-select-option v-for="item in categories?.data" :key="item.id" :value="item.id">
{{ item.name }} {{ item.name }}
</ion-select-option> </ion-select-option>
</ion-select> </ion-select>

View File

@@ -17,7 +17,7 @@ const initialData: RwaIssuanceProductBody = {
product: { product: {
name: "", name: "",
code: "", code: "",
categoryId: categories.value!.length > 0 ? categories.value![0].id : "", categoryId: categories.value?.data && categories.value?.data!.length > 0 ? categories.value?.data![0].id : "",
}, },
editions: [ editions: [
{ {
@@ -55,7 +55,7 @@ async function handleSubmit(editions: RwaIssuanceProductBody["editions"]) {
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<IonContent :fullscreen="true" class="ion-padding"> <IonContent :fullscreen="true" class="ion-padding">
<Base v-if="step === 1" :initial-data="form.product" :categories="categories || []" @next="handleNext" /> <Base v-if="step === 1" :initial-data="form.product" :categories="categories" @next="handleNext" />
<IssuePeriod v-else-if="step === 2" :initial-data="form.editions" @submit="handleSubmit" /> <IssuePeriod v-else-if="step === 2" :initial-data="form.editions" @submit="handleSubmit" />
<Done v-else /> <Done v-else />
</IonContent> </IonContent>

View File

@@ -0,0 +1,46 @@
<script lang='ts' setup>
import { client, safeClient } from "@/api";
const model = defineModel({ type: String, default: "" });
const { data: categories } = await safeClient(() => client.api.rwa.issuance.categories.get());
</script>
<template>
<ion-content :scroll-x="true" :scroll-y="false" class="w-full h-10">
<div class="flex items-center whitespace-nowrap space-x-4">
<div class="item" :class="{ active: model === '' }" @click="model = ''">
全部
</div>
<div
v-for="item in categories?.data"
:key="item.id"
class="item"
:class="{ active: model === item.id }"
@click="model = item.id"
>
{{ item.name }}
</div>
</div>
</ion-content>
</template>
<style scoped>
@reference "tailwindcss";
ion-content::part(scroll) {
scrollbar-width: none;
}
.item {
@apply px-3 py-1 rounded-full text-xs transition-all;
}
@media (prefers-color-scheme: dark) {
.item {
@apply bg-(--ion-color-step-800);
}
}
.item.active {
@apply bg-(--ion-color-success) text-white;
}
</style>

View File

@@ -1,21 +1,43 @@
<script setup lang="ts"> <script setup lang="ts">
import type { AvailableSubscriptionBody } from "@/api/types";
import { client, safeClient } from "@/api";
import Category from "./components/category.vue";
const [query] = useResetRef<AvailableSubscriptionBody>({
status: "open",
pageIndex: 1,
pageSize: 20,
categoryId: "",
});
const { data, refresh } = safeClient(() => client.api.rwa.subscription.available_editions.get({
query: query.value,
}));
watch(query, () => {
refresh();
}, { deep: true });
</script> </script>
<template> <template>
<IonPage> <IonPage>
<IonHeader> <IonHeader>
<IonToolbar> <IonToolbar class="ui-toolbar">
<IonTitle>Trade</IonTitle> <ion-title>Market</ion-title>
</IonToolbar>
</IonHeader>
<IonContent :fullscreen="true">
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">
Trade
</IonTitle>
</IonToolbar> </IonToolbar>
<ion-toolbar class="ui-toolbar">
<ion-searchbar />
</ion-toolbar>
</IonHeader> </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" />
</ui-tab-pane>
<ui-tab-pane name="futures" title="Tokenized Products" class="py-5">
<div>Futures Market Content</div>
</ui-tab-pane>
</ui-tabs>
</IonContent> </IonContent>
</IonPage> </IonPage>
</template> </template>

View File

@@ -51,6 +51,13 @@ const config: Config = {
"align-items": "center", "align-items": "center",
"justify-content": "center", "justify-content": "center",
}, },
".scrollbar-hide": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
"&::-webkit-scrollbar": {
display: "none",
},
},
}); });
}, },
], ],