feat: 更新依赖版本,优化路由组件,重构文件预览和订单面板,添加市场组件

This commit is contained in:
2026-01-12 00:00:49 +07:00
parent ba9f88a38e
commit 405ca17664
18 changed files with 241 additions and 111 deletions

View File

@@ -34,7 +34,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.7:9527/api/riwa-eden-0.0.123.tgz", "@riwa/api-types": "http://192.168.1.7:9527/api/riwa-eden-0.0.125.tgz",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@vee-validate/zod": "^4.15.1", "@vee-validate/zod": "^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.7:9527/api/riwa-eden-0.0.123.tgz specifier: http://192.168.1.7:9527/api/riwa-eden-0.0.125.tgz
version: '@riwa/eden@http://192.168.1.7:9527/api/riwa-eden-0.0.123.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.7:9527/api/riwa-eden-0.0.125.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))
@@ -2832,9 +2832,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/eden@http://192.168.1.7:9527/api/riwa-eden-0.0.123.tgz': '@riwa/eden@http://192.168.1.7:9527/api/riwa-eden-0.0.125.tgz':
resolution: {tarball: http://192.168.1.7:9527/api/riwa-eden-0.0.123.tgz} resolution: {tarball: http://192.168.1.7:9527/api/riwa-eden-0.0.125.tgz}
version: 0.0.123 version: 0.0.125
peerDependencies: peerDependencies:
'@elysiajs/eden': ^1.4.5 '@elysiajs/eden': ^1.4.5
@@ -12199,7 +12199,7 @@ snapshots:
'@remirror/core-constants@3.0.0': {} '@remirror/core-constants@3.0.0': {}
'@riwa/eden@http://192.168.1.7:9527/api/riwa-eden-0.0.123.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.7:9527/api/riwa-eden-0.0.125.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

@@ -185,41 +185,29 @@ const routes: Array<RouteRecordRaw> = [
{ {
path: "/trade-rwa/:id", path: "/trade-rwa/:id",
props: true, props: true,
component: () => import("@/views/trade-rwa/outlet.vue"),
meta: { requiresAuth: true },
children: [
{
path: "",
component: () => import("@/views/trade-rwa/index.vue"), component: () => import("@/views/trade-rwa/index.vue"),
meta: { requiresAuth: true }, meta: { requiresAuth: true },
}, },
{ {
path: "edit", path: "/trade-rwa/:id/edit",
name: "trade-rwa-edit", name: "trade-rwa-edit",
props: true,
component: () => import("@/views/trade-rwa/edit.vue"), component: () => import("@/views/trade-rwa/edit.vue"),
meta: { requiresAuth: true }, meta: { requiresAuth: true },
}, },
],
},
{ {
path: "/trade-tokenized/:id", path: "/trade-tokenized/:id",
props: true, props: true,
component: () => import("@/views/trade-tokenized/outlet.vue"),
meta: { requiresAuth: true },
children: [
{
path: "",
component: () => import("@/views/trade-tokenized/index.vue"), component: () => import("@/views/trade-tokenized/index.vue"),
meta: { requiresAuth: true }, meta: { requiresAuth: true },
}, },
{ {
path: "edit", path: "/trade-tokenized/:id/edit",
name: "trade-tokenized-edit", name: "trade-tokenized-edit",
props: true,
component: () => import("@/views/trade-tokenized/edit.vue"), component: () => import("@/views/trade-tokenized/edit.vue"),
meta: { requiresAuth: true }, meta: { requiresAuth: true },
}, },
],
},
{ {
path: "/market/orders", path: "/market/orders",
component: () => import("@/views/market/orders.vue"), component: () => import("@/views/market/orders.vue"),

View File

@@ -0,0 +1,22 @@
<script lang='ts' setup>
import IcRoundDownload from "~icons/ic/round-download";
import { client, safeClient } from "@/api";
const props = defineProps<{
id: string;
}>();
const { data } = await safeClient(client.api.file_storage({ id: props.id }).get());
</script>
<template>
<div>
<div>{{ data?.fileName }}</div>
<div>
<IcRoundDownload />
</div>
</div>
</template>
<style lang='css' scoped></style>

View File

@@ -125,7 +125,7 @@ function handleSubmit(values: GenericObject) {
</div> </div>
<div> <div>
<Field v-slot="{ field }" name="proofDocuments"> <Field v-slot="{ field }" name="proofDocumentIds">
<ui-file-upload <ui-file-upload
v-bind="field" v-bind="field"
:fetch-options="{ :fetch-options="{
@@ -136,10 +136,9 @@ function handleSubmit(values: GenericObject) {
:max-files="5" :max-files="5"
:max-size="10" :max-size="10"
accept="application/pdf,image/*,.doc,.docx" accept="application/pdf,image/*,.doc,.docx"
@update:model-value="field.onChange($event.join(','))"
/> />
</Field> </Field>
<ErrorMessage name="proofDocuments" /> <ErrorMessage name="proofDocumentIds" />
</div> </div>
<ion-button type="submit" expand="block" shape="round" color="success"> <ion-button type="submit" expand="block" shape="round" color="success">

View File

@@ -1,9 +1,12 @@
<script lang='ts' setup> <script lang='ts' setup>
import type { RwaData } from "@/api/types"; import type { RwaData } from "@/api/types";
import { client, safeClient } from "@/api";
const props = defineProps<{ const props = defineProps<{
data: RwaData | null; data: RwaData | null;
}>(); }>();
const { data: file } = safeClient(client.api.file_storage({ id: props.data?.product.proofDocumentIds || "" }).get());
</script> </script>
<template> <template>
@@ -13,9 +16,7 @@ const props = defineProps<{
<div class="font-semibold"> <div class="font-semibold">
相关文档 相关文档
</div> </div>
<div class="text-xs mt-2"> <a class="link" :href="file?.publicUrl || ''" target="_blank" rel="noopener noreferrer">{{ file?.fileName }}</a>
{{ data?.product.proofDocuments }}
</div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,5 +1,4 @@
<script lang='ts' setup> <script lang='ts' setup>
import type { RwaData } from "@/api/types";
import { toastController } from "@ionic/vue"; import { toastController } from "@ionic/vue";
import CryptocurrencyColorNuls from "~icons/cryptocurrency-color/nuls"; import CryptocurrencyColorNuls from "~icons/cryptocurrency-color/nuls";
import IcSharpEditNote from "~icons/ic/sharp-edit-note"; import IcSharpEditNote from "~icons/ic/sharp-edit-note";
@@ -8,9 +7,11 @@ import RwaAbout from "./components/about.vue";
import RwaBase from "./components/base.vue"; import RwaBase from "./components/base.vue";
const props = defineProps<{ const props = defineProps<{
data: RwaData; id: string;
}>(); }>();
const { data } = safeClient(client.api.rwa.subscription.available_editions({ editionId: props.id }).get());
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
@@ -18,7 +19,7 @@ const model = useTemplateRef<ModalInstance>("model");
async function handleSubscribe(val: number) { async function handleSubscribe(val: number) {
await safeClient(client.api.rwa.subscription.apply.post({ await safeClient(client.api.rwa.subscription.apply.post({
editionId: props.data.id, editionId: data.value!.id,
quantity: String(val), quantity: String(val),
})); }));
const toast = await toastController.create({ const toast = await toastController.create({
@@ -72,7 +73,7 @@ function gotoEdit() {
<ui-tab-pane name="overview" :title="t('market.tradeRwa.tabs.overview')"> <ui-tab-pane name="overview" :title="t('market.tradeRwa.tabs.overview')">
<RwaBase :data="data" /> <RwaBase :data="data" />
</ui-tab-pane> </ui-tab-pane>
<ui-tab-pane name="about" title="相关文档"> <ui-tab-pane name="about" title="相关文档" lazy>
<RwaAbout :data="data" /> <RwaAbout :data="data" />
</ui-tab-pane> </ui-tab-pane>
</ui-tabs> </ui-tabs>

View File

@@ -1,27 +0,0 @@
<script lang='ts' setup>
import { client, safeClient } from "@/api";
const props = defineProps<{
id: string;
}>();
const { data } = safeClient(client.api.rwa.subscription.available_editions({ editionId: props.id }).get());
</script>
<template>
<ion-page>
<ion-header>
<ion-toolbar class="ion-toolbar">
<ui-back-button slot="start" />
<ion-title>
{{ data?.product.code }}
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true" class="ion-padding">
<RouterView :data="data" />
</ion-content>
</ion-page>
</template>
<style scoped></style>

View File

@@ -14,7 +14,7 @@ const props = defineProps<{
相关文档 相关文档
</div> </div>
<div class="text-xs mt-2"> <div class="text-xs mt-2">
{{ data?.proofDocuments }} {{ data?.proofDocumentIds }}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -14,7 +14,7 @@ const props = defineProps<{
相关文档 相关文档
</div> </div>
<div class="text-xs mt-2"> <div class="text-xs mt-2">
{{ data?.product?.proofDocuments }} {{ data?.product?.proofDocumentIds }}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -10,10 +10,9 @@ const { t } = useI18n();
<template> <template>
<div class="mt-2"> <div class="mt-2">
<!-- Rwa about -->
<div> <div>
<div class="font-semibold"> <div class="font-semibold">
{{ t('market.tradeRwa.about') }} 关于
</div> </div>
<div class="text-xs mt-2"> <div class="text-xs mt-2">
{{ data?.product?.description || t('market.tradeRwa.noDescription') }} {{ data?.product?.description || t('market.tradeRwa.noDescription') }}
@@ -31,7 +30,7 @@ const { t } = useI18n();
</ion-col> </ion-col>
<ion-col> <ion-col>
<div class="label"> <div class="label">
{{ t('market.tradeRwa.fields.valuation') }} 市值
</div> </div>
<div>${{ formatAmountWithUnit(data?.product?.estimatedValue) }}</div> <div>${{ formatAmountWithUnit(data?.product?.estimatedValue) }}</div>
</ion-col> </ion-col>
@@ -64,4 +63,10 @@ const { t } = useI18n();
</div> </div>
</template> </template>
<style lang='css' scoped></style> <style lang='css' scoped>
@reference "tailwindcss";
.label {
@apply text-(--ion-text-color-step-300) mb-1 text-sm;
}
</style>

View File

@@ -0,0 +1,35 @@
<script lang='ts' setup>
import type { ChartingLibraryWidgetOptions } from "#/charting_library";
import type { TradableData } from "@/api/types";
import type { TradingViewInst } from "@/tradingview";
import { client, safeClient } from "@/api";
import { TradingViewChart } from "@/tradingview";
import OrderBook from "./order-book.vue";
const props = defineProps<{
data: TradableData | null;
}>();
const tradingviewOptions: Partial<ChartingLibraryWidgetOptions> = {
disabled_features: [
"create_volume_indicator_by_default",
],
};
const tradingViewInst = useTemplateRef<TradingViewInst>("tradingViewInst");
const symbol = computed(() => props.data?.asset?.tradingPairsAsBase[0].symbol || "");
const { data } = await safeClient(client.api.trading_pairs.orderbook.get({ query: { symbol: symbol.value, depth: 30 } }));
</script>
<template>
<TradingViewChart v-if="symbol" ref="tradingViewInst" class="mb-5" height="300px" :symbol="symbol" :options="tradingviewOptions" />
<ui-tabs size="small" class="my-3">
<ui-tab-pane name="order_book" title="订单表">
<OrderBook :symbol="symbol" />
</ui-tab-pane>
</ui-tabs>
</template>
<style lang='css' scoped></style>

View File

@@ -0,0 +1,125 @@
<script lang='ts' setup>
import { client, safeClient } from "@/api";
const props = defineProps<{ symbol: string }>();
const { data } = await safeClient(client.api.trading_pairs.orderbook.get({ query: { symbol: props.symbol, depth: 30 } }));
// 计算买单和卖单的最大数量,用于计算占比
const maxBidAmount = computed(() => {
if (!data.value?.bids)
return 0;
return Math.max(...data.value.bids.map(item => Number(item[1])));
});
const maxAskAmount = computed(() => {
if (!data.value?.asks)
return 0;
return Math.max(...data.value.asks.map(item => Number(item[1])));
});
// 计算每个订单的占比百分比
function getBidPercentage(amount: string) {
const num = Number(amount);
return maxBidAmount.value > 0 ? (num / maxBidAmount.value) * 100 : 0;
}
function getAskPercentage(amount: string) {
const num = Number(amount);
return maxAskAmount.value > 0 ? (num / maxAskAmount.value) * 100 : 0;
}
// 格式化价格
function formatPrice(price: string) {
return Number(price).toFixed(2);
}
// 格式化数量
function formatAmount(amount: string) {
return Number(amount).toFixed(4);
}
</script>
<template>
<div class="h-full w-full overflow-hidden bg-(--ion-background-color)">
<div class="grid h-full grid-cols-2 gap-0.5">
<!-- 买单区域 (Bids) -->
<div class="flex flex-col">
<!-- 标题 -->
<div class="grid grid-cols-2 gap-2 border-b border-(--ion-border-color) px-3 py-2 text-xs text-(--ion-color-medium)">
<div class="text-left">
价格
</div>
<div class="text-right">
数量
</div>
</div>
<!-- 买单列表 -->
<div class="flex-1 overflow-y-auto">
<div
v-for="(bid, index) in data?.bids"
:key="index"
class="relative grid grid-cols-2 gap-2 px-3 py-1 text-xs"
>
<!-- 背景色显示占比 -->
<div
class="absolute inset-y-0 right-0 bg-(--ion-color-success)/10"
:style="{ width: `${getBidPercentage(bid[1])}%` }"
/>
<!-- 价格 -->
<div class="relative z-1 text-left text-(--ion-color-success)">
{{ formatPrice(bid[0]) }}
</div>
<!-- 数量 -->
<div class="relative z-1 text-right text-(--ion-color-step-600)">
{{ formatAmount(bid[1]) }}
</div>
</div>
</div>
</div>
<!-- 卖单区域 (Asks) -->
<div class="flex flex-col">
<!-- 标题 -->
<div class="grid grid-cols-2 gap-2 border-b border-(--ion-border-color) px-3 py-2 text-xs text-(--ion-color-medium)">
<div class="text-left">
价格
</div>
<div class="text-right">
数量
</div>
</div>
<!-- 卖单列表 -->
<div class="flex-1 overflow-y-auto">
<div
v-for="(ask, index) in data?.asks"
:key="index"
class="relative grid grid-cols-2 gap-2 px-3 py-1 text-xs"
>
<!-- 背景色显示占比 -->
<div
class="absolute inset-y-0 right-0 bg-(--ion-color-danger)/10"
:style="{ width: `${getAskPercentage(ask[1])}%` }"
/>
<!-- 价格 -->
<div class="relative z-1 text-left text-(--ion-color-danger)">
{{ formatPrice(ask[0]) }}
</div>
<!-- 数量 -->
<div class="relative z-1 text-right text-(--ion-color-step-600)">
{{ formatAmount(ask[1]) }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang='css' scoped></style>

View File

@@ -1,19 +1,22 @@
<script lang='ts' setup> <script lang='ts' setup>
import type { TradableData } from "@/api/types";
import CryptocurrencyColorNuls from "~icons/cryptocurrency-color/nuls"; import CryptocurrencyColorNuls from "~icons/cryptocurrency-color/nuls";
import { client, safeClient } from "@/api";
import { TradeTypeEnum } from "@/api/enum"; import { TradeTypeEnum } from "@/api/enum";
import About from "./components/about.vue"; import About from "./components/about.vue";
import Base from "./components/base.vue"; import Base from "./components/base.vue";
import Market from "./components/market.vue";
const props = defineProps<{ const props = defineProps<{
data: TradableData; id: string;
}>(); }>();
const { data } = safeClient(client.api.rwa.tokenization.tradable_products({ id: props.id }).get());
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
function gotoTrade(mode: TradeTypeEnum) { function gotoTrade(mode: TradeTypeEnum) {
router.push(`/layout/trade?mode=${mode}&symbol=${props.data.asset?.tradingPairsAsBase[0].symbol}`); router.push(`/layout/trade?mode=${mode}&symbol=${data.value?.asset?.tradingPairsAsBase[0].symbol}`);
} }
</script> </script>
@@ -50,6 +53,9 @@ function gotoTrade(mode: TradeTypeEnum) {
</div> </div>
<ui-tabs size="small" class="my-3"> <ui-tabs size="small" class="my-3">
<ui-tab-pane name="market" title="行情">
<Market :data="data" />
</ui-tab-pane>
<ui-tab-pane name="overview" :title="t('market.tradeRwa.tabs.overview')"> <ui-tab-pane name="overview" :title="t('market.tradeRwa.tabs.overview')">
<Base :data="data" /> <Base :data="data" />
</ui-tab-pane> </ui-tab-pane>

View File

@@ -1,27 +0,0 @@
<script lang='ts' setup>
import { client, safeClient } from "@/api";
const props = defineProps<{
id: string;
}>();
const { data } = safeClient(client.api.rwa.tokenization.tradable_products({ id: props.id }).get());
</script>
<template>
<ion-page>
<ion-header>
<ion-toolbar class="ion-toolbar">
<ui-back-button slot="start" />
<ion-title>
{{ data?.product?.code }}
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true" class="ion-padding">
<RouterView :data="data" />
</ion-content>
</ion-page>
</template>
<style scoped></style>

View File

@@ -7,10 +7,12 @@ import { orderStatusMap } from "../config";
type Item = Treaty.Data<typeof client.api.spot_order.list.get>["orders"][number]; type Item = Treaty.Data<typeof client.api.spot_order.list.get>["orders"][number];
type TabType = "current" | "history"; type TabType = "current" | "history";
const props = defineProps<{ symbol: string }>();
const activeTab = ref<TabType>("current"); const activeTab = ref<TabType>("current");
const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{ item: Item }>(); const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{ item: Item }>();
const { data } = await safeClient(client.api.spot_order.list.get()); const { data } = await safeClient(client.api.spot_order.list.get({ query: { symbol: props.symbol } }));
const currentOrders = computed(() => const currentOrders = computed(() =>
data.value?.orders.filter(order => order.status !== "filled" && order.status !== "cancelled") || [], data.value?.orders.filter(order => order.status !== "filled" && order.status !== "cancelled") || [],
); );

View File

@@ -146,7 +146,7 @@ async function handleSubmit() {
</div> </div>
<div class="mt-6 px-4 pb-4"> <div class="mt-6 px-4 pb-4">
<OrdersPanel /> <OrdersPanel :symbol="form.symbol" />
</div> </div>
</ion-content> </ion-content>
</ion-page> </ion-page>

View File

@@ -32,7 +32,7 @@ async function handleScan() {
}); });
if (result) { if (result) {
console.log("扫描结果:", result.text); console.log("扫描结果:", result);
// TODO: 根据扫描结果进行相应处理 // TODO: 根据扫描结果进行相应处理
// 例如:跳转到对应页面、显示信息等 // 例如:跳转到对应页面、显示信息等
} }