优化:打包细节优化
This commit is contained in:
parent
51a5dcc89d
commit
77d4b05839
58
src/App.vue
58
src/App.vue
@ -1,15 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, watch, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useUserStore } from './stores/user'
|
||||
import { useLocaleStore } from './stores/locale'
|
||||
import type { LocaleCode } from './plugins/i18n'
|
||||
import Toast from './components/Toast.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const localeStore = useLocaleStore()
|
||||
const localeMenuOpen = ref(false)
|
||||
|
||||
function chooseLocale(loc: LocaleCode) {
|
||||
localeStore.setLocale(loc)
|
||||
localeMenuOpen.value = false
|
||||
}
|
||||
const display = useDisplay()
|
||||
|
||||
const currentRoute = computed(() => route.path)
|
||||
@ -18,11 +27,13 @@ const showBottomNav = computed(() => {
|
||||
if (!display.smAndDown.value) return false
|
||||
return currentRoute.value === '/' || currentRoute.value === '/search' || currentRoute.value === '/profile'
|
||||
})
|
||||
const mineTargetPath = computed(() => '/profile')
|
||||
const mineTargetPath = computed(() =>
|
||||
userStore.isLoggedIn ? '/profile' : '/login',
|
||||
)
|
||||
const bottomNavValue = computed({
|
||||
get() {
|
||||
const p = currentRoute.value
|
||||
if (p.startsWith('/profile')) return '/profile'
|
||||
if (p.startsWith('/profile') || p === '/login') return '/profile'
|
||||
if (p.startsWith('/search')) return '/search'
|
||||
return '/'
|
||||
},
|
||||
@ -68,14 +79,43 @@ watch(
|
||||
</v-btn>
|
||||
<v-app-bar-title v-if="currentRoute === '/'">TestMarket</v-app-bar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<template v-if="!userStore.isLoggedIn">
|
||||
<v-menu
|
||||
v-model="localeMenuOpen"
|
||||
:close-on-content-click="true"
|
||||
location="bottom"
|
||||
transition="scale-transition"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
class="locale-btn"
|
||||
:aria-label="t('profile.selectLanguage')"
|
||||
v-bind="props"
|
||||
>
|
||||
<v-icon>mdi-earth</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
v-for="opt in localeStore.localeOptions"
|
||||
:key="opt.value"
|
||||
:active="localeStore.currentLocale === opt.value"
|
||||
@click="chooseLocale(opt.value)"
|
||||
>
|
||||
<v-list-item-title>{{ opt.label }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-btn
|
||||
v-if="!userStore.isLoggedIn"
|
||||
text
|
||||
to="/login"
|
||||
:class="{ active: currentRoute === '/login' }"
|
||||
>
|
||||
{{ t('common.login') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-btn
|
||||
class="balance-btn"
|
||||
@ -122,15 +162,15 @@ watch(
|
||||
>
|
||||
<v-btn value="/" :ripple="false">
|
||||
<v-icon size="24">mdi-home-outline</v-icon>
|
||||
<span>Home</span>
|
||||
<span>{{ t('nav.home') }}</span>
|
||||
</v-btn>
|
||||
<v-btn value="/search" :ripple="false">
|
||||
<v-icon size="24">mdi-magnify</v-icon>
|
||||
<span>Search</span>
|
||||
<span>{{ t('nav.search') }}</span>
|
||||
</v-btn>
|
||||
<v-btn value="/profile" :ripple="false">
|
||||
<v-icon size="24">mdi-account-outline</v-icon>
|
||||
<span>Mine</span>
|
||||
<span>{{ t('nav.mine') }}</span>
|
||||
</v-btn>
|
||||
</v-bottom-navigation>
|
||||
|
||||
@ -187,6 +227,10 @@ watch(
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
|
||||
.locale-btn {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
|
||||
/* 底部导航:整条栏上方淡投影;z-index 确保覆盖滚动条,显示在滚动条上层 */
|
||||
:deep(.v-bottom-navigation) {
|
||||
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06);
|
||||
|
||||
73
src/api/apiApp.ts
Normal file
73
src/api/apiApp.ts
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* API 应用管理接口
|
||||
* doc.json: /AA/getApiAppList、/AA/createApiApp 等
|
||||
*/
|
||||
|
||||
import { get, post } from './request'
|
||||
import { buildQuery } from './request'
|
||||
import type { PageResult } from './types'
|
||||
|
||||
/** model.ApiApp:API 应用项 */
|
||||
export interface ApiApp {
|
||||
ID?: number
|
||||
appKey: string
|
||||
appSecret: string
|
||||
desc?: string
|
||||
status: boolean
|
||||
userId: number
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface GetApiAppListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
createdAtRange?: string[]
|
||||
}
|
||||
|
||||
export interface ApiAppListResponse {
|
||||
code: number
|
||||
data?: PageResult<ApiApp>
|
||||
msg: string
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /AA/getApiAppList
|
||||
* 分页获取 API 应用列表,需鉴权(x-token、x-user-id)
|
||||
*/
|
||||
export async function getApiAppList(
|
||||
params: GetApiAppListParams = {},
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<ApiAppListResponse> {
|
||||
const { page = 1, pageSize = 10, keyword, createdAtRange } = params
|
||||
const query = buildQuery({ page, pageSize, keyword, createdAtRange })
|
||||
return get<ApiAppListResponse>('/AA/getApiAppList', query, config)
|
||||
}
|
||||
|
||||
/** 创建 API 应用请求体(appKey/appSecret 由后端生成时可传空) */
|
||||
export interface CreateApiAppBody {
|
||||
desc?: string
|
||||
status: boolean
|
||||
userId: number
|
||||
appKey?: string
|
||||
appSecret?: string
|
||||
}
|
||||
|
||||
export interface CreateApiAppResponse {
|
||||
code: number
|
||||
data?: ApiApp
|
||||
msg: string
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /AA/createApiApp
|
||||
* 创建 API 应用,需鉴权。成功后返回新创建的 ApiApp(含 appKey、appSecret)
|
||||
*/
|
||||
export async function createApiApp(
|
||||
body: CreateApiAppBody,
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<CreateApiAppResponse> {
|
||||
return post<CreateApiAppResponse>('/AA/createApiApp', body, config)
|
||||
}
|
||||
@ -19,6 +19,38 @@ export interface MockOrderBookRow {
|
||||
shares: number
|
||||
}
|
||||
|
||||
/** 生成随机订单簿数据,每次进入页面不同;保证卖单最低价 > 买单最高价 */
|
||||
export function generateRandomOrderBook(): {
|
||||
asks: MockOrderBookRow[]
|
||||
bids: MockOrderBookRow[]
|
||||
lastPrice: number
|
||||
spread: number
|
||||
} {
|
||||
const r = () => Math.random()
|
||||
|
||||
// 买单最高价 25–35,卖单最低价 = 买单最高价 + 1 + spread,保证有价差
|
||||
const highestBid = Math.floor(25 + r() * 11)
|
||||
const spread = Math.max(1, Math.floor(1 + r() * 3))
|
||||
const lowestAsk = highestBid + spread
|
||||
|
||||
const askPrices = Array.from({ length: 9 }, (_, i) => lowestAsk + i)
|
||||
const asks: MockOrderBookRow[] = askPrices.map((p) => ({
|
||||
price: p,
|
||||
shares: Math.round((500 + r() * 3000) * 10) / 10,
|
||||
}))
|
||||
|
||||
const bidPrices = Array.from({ length: 12 }, (_, i) => highestBid - i)
|
||||
const bids: MockOrderBookRow[] = bidPrices.map((p) => ({
|
||||
price: Math.max(1, p),
|
||||
shares: Math.round((200 + r() * 3000) * 10) / 10,
|
||||
}))
|
||||
|
||||
const lastPrice = Math.floor((highestBid + lowestAsk) / 2)
|
||||
|
||||
return { asks, bids, lastPrice, spread }
|
||||
}
|
||||
|
||||
/** @deprecated 使用 generateRandomOrderBook() 获取随机数据 */
|
||||
export const MOCK_ORDER_BOOK_ASKS: MockOrderBookRow[] = [
|
||||
{ price: 45, shares: 1000.0 },
|
||||
{ price: 44, shares: 2500.0 },
|
||||
@ -31,6 +63,7 @@ export const MOCK_ORDER_BOOK_ASKS: MockOrderBookRow[] = [
|
||||
{ price: 37, shares: 300.0 },
|
||||
]
|
||||
|
||||
/** @deprecated 使用 generateRandomOrderBook() 获取随机数据 */
|
||||
export const MOCK_ORDER_BOOK_BIDS: MockOrderBookRow[] = [
|
||||
{ price: 36, shares: 200.0 },
|
||||
{ price: 35, shares: 500.0 },
|
||||
|
||||
@ -24,7 +24,11 @@
|
||||
<!-- Order Book Content -->
|
||||
<div class="order-book-content">
|
||||
<!-- Order List -->
|
||||
<div class="order-list">
|
||||
<div v-if="loading" class="order-book-loading">
|
||||
<v-progress-circular indeterminate size="32" width="2" />
|
||||
<span>{{ t('trade.orderBookConnecting') }}</span>
|
||||
</div>
|
||||
<div v-else class="order-list">
|
||||
<div class="order-list-header">
|
||||
<div class="order-list-header-spacer"></div>
|
||||
<div class="order-list-header-price">{{ t('trade.orderBookPrice') }}</div>
|
||||
@ -71,8 +75,14 @@
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="order-book-footer">
|
||||
<div class="last-price">{{ t('trade.orderBookLast') }}: {{ displayLastPrice }}¢</div>
|
||||
<div class="spread">{{ t('trade.orderBookSpread') }}: {{ displaySpread }}¢</div>
|
||||
<div class="last-price">
|
||||
{{ t('trade.orderBookLast') }}:
|
||||
{{ typeof displayLastPrice === 'number' ? `${displayLastPrice}¢` : displayLastPrice }}
|
||||
</div>
|
||||
<div class="spread">
|
||||
{{ t('trade.orderBookSpread') }}:
|
||||
{{ typeof displaySpread === 'number' ? `${displaySpread}¢` : displaySpread }}
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
@ -81,12 +91,7 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import HorizontalProgressBar from './HorizontalProgressBar.vue'
|
||||
import {
|
||||
MOCK_ORDER_BOOK_ASKS,
|
||||
MOCK_ORDER_BOOK_BIDS,
|
||||
MOCK_ORDER_BOOK_LAST_PRICE,
|
||||
MOCK_ORDER_BOOK_SPREAD,
|
||||
} from '../api/mockData'
|
||||
import { generateRandomOrderBook } from '../api/mockData'
|
||||
import { USE_MOCK_ORDER_BOOK } from '../config/mock'
|
||||
|
||||
const { t } = useI18n()
|
||||
@ -145,13 +150,15 @@ const props = withDefaults(
|
||||
// State:up = Yes 交易,down = No 交易
|
||||
const activeTrade = ref('up')
|
||||
|
||||
const internalAsks = ref<OrderBookRow[]>([...MOCK_ORDER_BOOK_ASKS])
|
||||
const internalBids = ref<OrderBookRow[]>([...MOCK_ORDER_BOOK_BIDS])
|
||||
const internalLastPrice = ref(MOCK_ORDER_BOOK_LAST_PRICE)
|
||||
const internalSpread = ref(MOCK_ORDER_BOOK_SPREAD)
|
||||
const initMock = generateRandomOrderBook()
|
||||
const internalAsks = ref<OrderBookRow[]>([...initMock.asks])
|
||||
const internalBids = ref<OrderBookRow[]>([...initMock.bids])
|
||||
const internalLastPrice = ref(initMock.lastPrice)
|
||||
const internalSpread = ref(initMock.spread)
|
||||
|
||||
// 根据 activeTrade 选择当前 tab 对应的数据
|
||||
// 根据 activeTrade 选择当前 tab 对应的数据;loading 时不显示模拟数据
|
||||
const asks = computed(() => {
|
||||
if (props.loading) return []
|
||||
const isYes = activeTrade.value === 'up'
|
||||
const fromYes = isYes ? props.asksYes : props.asksNo
|
||||
const fromLegacy = props.asks
|
||||
@ -162,6 +169,7 @@ const asks = computed(() => {
|
||||
return USE_MOCK_ORDER_BOOK ? internalAsks.value : []
|
||||
})
|
||||
const bids = computed(() => {
|
||||
if (props.loading) return []
|
||||
const isYes = activeTrade.value === 'up'
|
||||
const fromYes = isYes ? props.bidsYes : props.bidsNo
|
||||
const fromLegacy = props.bids
|
||||
@ -172,6 +180,7 @@ const bids = computed(() => {
|
||||
return USE_MOCK_ORDER_BOOK ? internalBids.value : []
|
||||
})
|
||||
const displayLastPrice = computed(() => {
|
||||
if (props.loading) return '—'
|
||||
const isYes = activeTrade.value === 'up'
|
||||
const fromYesNo = isYes ? props.lastPriceYes : props.lastPriceNo
|
||||
if (fromYesNo != null) return fromYesNo
|
||||
@ -179,6 +188,7 @@ const displayLastPrice = computed(() => {
|
||||
return USE_MOCK_ORDER_BOOK ? internalLastPrice.value : 0
|
||||
})
|
||||
const displaySpread = computed(() => {
|
||||
if (props.loading) return '—'
|
||||
const isYes = activeTrade.value === 'up'
|
||||
const fromYesNo = isYes ? props.spreadYes : props.spreadNo
|
||||
if (fromYesNo != null) return fromYesNo
|
||||
@ -299,6 +309,7 @@ const maxOrderBookTotal = computed(() => {
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
border: 1px solid #e7e7e7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.horizontal-progress-bar {
|
||||
@ -450,6 +461,18 @@ const maxOrderBookTotal = computed(() => {
|
||||
background-color: #7acc7a;
|
||||
}
|
||||
|
||||
.order-book-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
min-height: 120px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.order-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"search": "Search",
|
||||
"mine": "Mine"
|
||||
},
|
||||
"common": {
|
||||
"login": "Login",
|
||||
"logout": "Log out",
|
||||
@ -10,6 +15,7 @@
|
||||
"delete": "Delete",
|
||||
"clear": "Clear",
|
||||
"loading": "Loading...",
|
||||
"noData": "No data",
|
||||
"more": "More",
|
||||
"user": "User",
|
||||
"chance": "chance"
|
||||
@ -19,6 +25,7 @@
|
||||
"cryptoPrice": "Crypto Price"
|
||||
},
|
||||
"toast": {
|
||||
"createKeySuccess": "API Key created successfully",
|
||||
"orderSuccess": "Order placed successfully",
|
||||
"splitSuccess": "Split successful",
|
||||
"mergeSuccess": "Merge successful",
|
||||
@ -86,7 +93,19 @@
|
||||
"home": {
|
||||
"searchHistory": "Search history",
|
||||
"searchPlaceholder": "Search",
|
||||
"loadMore": "Load more"
|
||||
"loadMore": "Load more",
|
||||
"noMore": "No more"
|
||||
},
|
||||
"eventMarkets": {
|
||||
"allMarkets": "All markets",
|
||||
"past": "Past ▾",
|
||||
"marketsCount": "{n} markets",
|
||||
"volumeZero": "$0 Vol.",
|
||||
"marketPlaceholder": "Market",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"priceZero": "0¢",
|
||||
"moreActions": "More actions"
|
||||
},
|
||||
"searchPage": {
|
||||
"title": "Search",
|
||||
@ -112,6 +131,25 @@
|
||||
"pleaseLogin": "Please log in first",
|
||||
"insufficientPermission": "Insufficient permission"
|
||||
},
|
||||
"login": {
|
||||
"title": "Sign In",
|
||||
"desc": "Connect your wallet to sign in",
|
||||
"connecting": "Connecting...",
|
||||
"connectBtn": "Connect with Wallet",
|
||||
"signUp": "Don't have an account? Sign Up",
|
||||
"walletNotInstalled": "Ethereum wallet is not installed. Please install MetaMask, TokenPocket or another Ethereum wallet to continue.",
|
||||
"invalidAddress": "Invalid wallet address format. Please check your wallet connection.",
|
||||
"connectFailed": "Failed to connect with wallet. Please check your wallet and try again.",
|
||||
"siweStatement": "Sign in to TestMarket"
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API Key Management",
|
||||
"createBtn": "Create Key",
|
||||
"copy": "Copy",
|
||||
"delete": "Delete",
|
||||
"keyDefault": "Key #{n}",
|
||||
"defaultDesc": "API Key"
|
||||
},
|
||||
"activity": {
|
||||
"comments": "Comments",
|
||||
"topHolders": "Top Holders",
|
||||
@ -248,6 +286,8 @@
|
||||
"newUserName": "Username",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"nameSaving": "Saving...",
|
||||
"nameSaveFailed": "Failed to update username",
|
||||
"nameRequired": "Username is required",
|
||||
"nameTooLong": "Username must be at most {max} characters",
|
||||
"nameInvalidFormat": "Username may only contain letters, numbers, and underscores",
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "ホーム",
|
||||
"search": "検索",
|
||||
"mine": "マイ"
|
||||
},
|
||||
"common": {
|
||||
"login": "ログイン",
|
||||
"logout": "ログアウト",
|
||||
@ -10,6 +15,7 @@
|
||||
"delete": "削除",
|
||||
"clear": "クリア",
|
||||
"loading": "読み込み中...",
|
||||
"noData": "データがありません",
|
||||
"more": "その他",
|
||||
"user": "ユーザー",
|
||||
"chance": "確率"
|
||||
@ -19,6 +25,7 @@
|
||||
"cryptoPrice": "暗号資産価格"
|
||||
},
|
||||
"toast": {
|
||||
"createKeySuccess": "API Key を作成しました",
|
||||
"orderSuccess": "注文が完了しました",
|
||||
"splitSuccess": "スプリット成功",
|
||||
"mergeSuccess": "マージ成功",
|
||||
@ -86,7 +93,19 @@
|
||||
"home": {
|
||||
"searchHistory": "検索履歴",
|
||||
"searchPlaceholder": "検索",
|
||||
"loadMore": "もっと読み込む"
|
||||
"loadMore": "もっと読み込む",
|
||||
"noMore": "これ以上ありません"
|
||||
},
|
||||
"eventMarkets": {
|
||||
"allMarkets": "全マーケット",
|
||||
"past": "過去 ▾",
|
||||
"marketsCount": "{n} 件のマーケット",
|
||||
"volumeZero": "$0 出来高",
|
||||
"marketPlaceholder": "マーケット",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"priceZero": "0¢",
|
||||
"moreActions": "その他の操作"
|
||||
},
|
||||
"searchPage": {
|
||||
"title": "検索",
|
||||
@ -112,6 +131,25 @@
|
||||
"pleaseLogin": "先にログインしてください",
|
||||
"insufficientPermission": "権限がありません"
|
||||
},
|
||||
"login": {
|
||||
"title": "サインイン",
|
||||
"desc": "ウォレットを接続してサインイン",
|
||||
"connecting": "接続中...",
|
||||
"connectBtn": "ウォレットで接続",
|
||||
"signUp": "アカウントをお持ちでないですか?新規登録",
|
||||
"walletNotInstalled": "イーサリアムウォレットがインストールされていません。MetaMask、TokenPocket などのウォレットをインストールしてください。",
|
||||
"invalidAddress": "ウォレットアドレスの形式が無効です。接続を確認してください。",
|
||||
"connectFailed": "ウォレットの接続に失敗しました。ウォレットを確認して再試行してください。",
|
||||
"siweStatement": "TestMarket にサインイン"
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API Key 管理",
|
||||
"createBtn": "Key を作成",
|
||||
"copy": "コピー",
|
||||
"delete": "削除",
|
||||
"keyDefault": "Key #{n}",
|
||||
"defaultDesc": "API Key"
|
||||
},
|
||||
"activity": {
|
||||
"comments": "コメント",
|
||||
"topHolders": "持倉トップ",
|
||||
@ -248,6 +286,8 @@
|
||||
"newUserName": "新しいユーザー名",
|
||||
"cancel": "キャンセル",
|
||||
"save": "保存",
|
||||
"nameSaving": "保存中...",
|
||||
"nameSaveFailed": "ユーザー名の更新に失敗しました",
|
||||
"nameRequired": "ユーザー名は必須です",
|
||||
"nameTooLong": "ユーザー名は {max} 文字以内にしてください",
|
||||
"nameInvalidFormat": "ユーザー名は半角英字、数字、アンダースコア(_)のみで入力してください",
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "홈",
|
||||
"search": "검색",
|
||||
"mine": "마이"
|
||||
},
|
||||
"common": {
|
||||
"login": "로그인",
|
||||
"logout": "로그아웃",
|
||||
@ -10,6 +15,7 @@
|
||||
"delete": "삭제",
|
||||
"clear": "지우기",
|
||||
"loading": "로딩 중...",
|
||||
"noData": "데이터 없음",
|
||||
"more": "더보기",
|
||||
"user": "사용자",
|
||||
"chance": "확률"
|
||||
@ -19,6 +25,7 @@
|
||||
"cryptoPrice": "암호화폐 가격"
|
||||
},
|
||||
"toast": {
|
||||
"createKeySuccess": "API Key가 생성되었습니다",
|
||||
"orderSuccess": "주문이 완료되었습니다",
|
||||
"splitSuccess": "분할 완료",
|
||||
"mergeSuccess": "병합 완료",
|
||||
@ -86,7 +93,19 @@
|
||||
"home": {
|
||||
"searchHistory": "검색 기록",
|
||||
"searchPlaceholder": "검색",
|
||||
"loadMore": "더 불러오기"
|
||||
"loadMore": "더 불러오기",
|
||||
"noMore": "더 이상 없음"
|
||||
},
|
||||
"eventMarkets": {
|
||||
"allMarkets": "전체 마켓",
|
||||
"past": "과거 ▾",
|
||||
"marketsCount": "{n}개 마켓",
|
||||
"volumeZero": "$0 거래량",
|
||||
"marketPlaceholder": "마켓",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"priceZero": "0¢",
|
||||
"moreActions": "더보기"
|
||||
},
|
||||
"searchPage": {
|
||||
"title": "검색",
|
||||
@ -112,6 +131,25 @@
|
||||
"pleaseLogin": "먼저 로그인하세요",
|
||||
"insufficientPermission": "권한이 없습니다"
|
||||
},
|
||||
"login": {
|
||||
"title": "로그인",
|
||||
"desc": "지갑을 연결하여 로그인",
|
||||
"connecting": "연결 중...",
|
||||
"connectBtn": "지갑 연결",
|
||||
"signUp": "계정이 없으신가요? 회원가입",
|
||||
"walletNotInstalled": "이더리움 지갑이 설치되어 있지 않습니다. MetaMask, TokenPocket 등 지갑을 설치해 주세요.",
|
||||
"invalidAddress": "지갑 주소 형식이 올바르지 않습니다. 지갑 연결을 확인해 주세요.",
|
||||
"connectFailed": "지갑 연결에 실패했습니다. 지갑을 확인하고 다시 시도해 주세요.",
|
||||
"siweStatement": "TestMarket 로그인"
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API Key 관리",
|
||||
"createBtn": "Key 생성",
|
||||
"copy": "복사",
|
||||
"delete": "삭제",
|
||||
"keyDefault": "Key #{n}",
|
||||
"defaultDesc": "API Key"
|
||||
},
|
||||
"activity": {
|
||||
"comments": "댓글",
|
||||
"topHolders": "보유자 순위",
|
||||
@ -248,6 +286,8 @@
|
||||
"newUserName": "새 사용자 이름",
|
||||
"cancel": "취소",
|
||||
"save": "저장",
|
||||
"nameSaving": "저장 중...",
|
||||
"nameSaveFailed": "사용자 이름 업데이트 실패",
|
||||
"nameRequired": "사용자 이름은 필수입니다",
|
||||
"nameTooLong": "사용자 이름은 {max}자 이하여야 합니다",
|
||||
"nameInvalidFormat": "사용자 이름은 영문자, 숫자, 밑줄(_)만 사용할 수 있습니다",
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "首页",
|
||||
"search": "搜索",
|
||||
"mine": "我的"
|
||||
},
|
||||
"common": {
|
||||
"login": "登录",
|
||||
"logout": "退出登录",
|
||||
@ -10,6 +15,7 @@
|
||||
"delete": "删除",
|
||||
"clear": "清空",
|
||||
"loading": "加载中...",
|
||||
"noData": "暂无数据",
|
||||
"more": "更多操作",
|
||||
"user": "用户",
|
||||
"chance": "概率"
|
||||
@ -19,6 +25,7 @@
|
||||
"cryptoPrice": "加密货币价格"
|
||||
},
|
||||
"toast": {
|
||||
"createKeySuccess": "API Key 创建成功",
|
||||
"orderSuccess": "下单成功",
|
||||
"splitSuccess": "拆分成功",
|
||||
"mergeSuccess": "合并成功",
|
||||
@ -86,7 +93,19 @@
|
||||
"home": {
|
||||
"searchHistory": "搜索历史",
|
||||
"searchPlaceholder": "Search",
|
||||
"loadMore": "加载更多"
|
||||
"loadMore": "加载更多",
|
||||
"noMore": "没有更多了"
|
||||
},
|
||||
"eventMarkets": {
|
||||
"allMarkets": "全部市场",
|
||||
"past": "过去 ▾",
|
||||
"marketsCount": "{n} 个市场",
|
||||
"volumeZero": "$0 成交量",
|
||||
"marketPlaceholder": "市场",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"priceZero": "0¢",
|
||||
"moreActions": "更多操作"
|
||||
},
|
||||
"searchPage": {
|
||||
"title": "搜索",
|
||||
@ -112,6 +131,25 @@
|
||||
"pleaseLogin": "请先登录",
|
||||
"insufficientPermission": "权限不足"
|
||||
},
|
||||
"login": {
|
||||
"title": "登录",
|
||||
"desc": "连接钱包以登录",
|
||||
"connecting": "连接中...",
|
||||
"connectBtn": "连接钱包",
|
||||
"signUp": "没有账号?立即注册",
|
||||
"walletNotInstalled": "未检测到以太坊钱包。请安装 MetaMask、TokenPocket 或其他以太坊钱包后重试。",
|
||||
"invalidAddress": "钱包地址格式无效,请检查钱包连接。",
|
||||
"connectFailed": "连接钱包失败,请检查钱包后重试。",
|
||||
"siweStatement": "登录 TestMarket"
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API Key 管理",
|
||||
"createBtn": "创建 Key",
|
||||
"copy": "复制",
|
||||
"delete": "删除",
|
||||
"keyDefault": "Key #{n}",
|
||||
"defaultDesc": "API Key"
|
||||
},
|
||||
"activity": {
|
||||
"comments": "评论",
|
||||
"topHolders": "持仓大户",
|
||||
@ -248,6 +286,8 @@
|
||||
"newUserName": "用户名",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"nameSaving": "保存中...",
|
||||
"nameSaveFailed": "用户名更新失败",
|
||||
"nameRequired": "用户名不能为空",
|
||||
"nameTooLong": "用户名不能超过 {max} 个字符",
|
||||
"nameInvalidFormat": "用户名只能包含字母、数字与下划线(a-z / 0-9 / _)",
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
{
|
||||
"nav": {
|
||||
"home": "首頁",
|
||||
"search": "搜尋",
|
||||
"mine": "我的"
|
||||
},
|
||||
"common": {
|
||||
"login": "登入",
|
||||
"logout": "登出",
|
||||
@ -10,6 +15,7 @@
|
||||
"delete": "刪除",
|
||||
"clear": "清空",
|
||||
"loading": "載入中...",
|
||||
"noData": "暫無數據",
|
||||
"more": "更多操作",
|
||||
"user": "用戶",
|
||||
"chance": "機率"
|
||||
@ -19,6 +25,7 @@
|
||||
"cryptoPrice": "加密貨幣價格"
|
||||
},
|
||||
"toast": {
|
||||
"createKeySuccess": "API Key 創建成功",
|
||||
"orderSuccess": "下單成功",
|
||||
"splitSuccess": "拆分成功",
|
||||
"mergeSuccess": "合併成功",
|
||||
@ -86,7 +93,19 @@
|
||||
"home": {
|
||||
"searchHistory": "搜尋歷史",
|
||||
"searchPlaceholder": "Search",
|
||||
"loadMore": "載入更多"
|
||||
"loadMore": "載入更多",
|
||||
"noMore": "沒有更多了"
|
||||
},
|
||||
"eventMarkets": {
|
||||
"allMarkets": "全部市場",
|
||||
"past": "過去 ▾",
|
||||
"marketsCount": "{n} 個市場",
|
||||
"volumeZero": "$0 成交量",
|
||||
"marketPlaceholder": "市場",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"priceZero": "0¢",
|
||||
"moreActions": "更多操作"
|
||||
},
|
||||
"searchPage": {
|
||||
"title": "搜尋",
|
||||
@ -112,6 +131,25 @@
|
||||
"pleaseLogin": "請先登入",
|
||||
"insufficientPermission": "權限不足"
|
||||
},
|
||||
"login": {
|
||||
"title": "登入",
|
||||
"desc": "連接錢包以登入",
|
||||
"connecting": "連接中...",
|
||||
"connectBtn": "連接錢包",
|
||||
"signUp": "沒有帳號?立即註冊",
|
||||
"walletNotInstalled": "未偵測到以太坊錢包。請安裝 MetaMask、TokenPocket 或其他以太坊錢包後重試。",
|
||||
"invalidAddress": "錢包地址格式無效,請檢查錢包連接。",
|
||||
"connectFailed": "連接錢包失敗,請檢查錢包後重試。",
|
||||
"siweStatement": "登入 TestMarket"
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API Key 管理",
|
||||
"createBtn": "創建 Key",
|
||||
"copy": "複製",
|
||||
"delete": "刪除",
|
||||
"keyDefault": "Key #{n}",
|
||||
"defaultDesc": "API Key"
|
||||
},
|
||||
"activity": {
|
||||
"comments": "評論",
|
||||
"topHolders": "持倉大戶",
|
||||
@ -248,6 +286,8 @@
|
||||
"newUserName": "使用者名稱",
|
||||
"cancel": "取消",
|
||||
"save": "儲存",
|
||||
"nameSaving": "儲存中...",
|
||||
"nameSaveFailed": "使用者名稱更新失敗",
|
||||
"nameRequired": "使用者名稱不能为空",
|
||||
"nameTooLong": "使用者名稱不能超過 {max} 個字元",
|
||||
"nameInvalidFormat": "使用者名稱只能包含字母、數字與底線(_)",
|
||||
|
||||
@ -2,36 +2,211 @@
|
||||
<div class="api-key-page">
|
||||
<div class="api-key-screen">
|
||||
<header class="api-key-header">
|
||||
<h1 class="api-key-title">API Key 管理</h1>
|
||||
<button class="create-btn" type="button">创建 Key</button>
|
||||
<h1 class="api-key-title">{{ t('apiKey.title') }}</h1>
|
||||
<button
|
||||
class="create-btn"
|
||||
type="button"
|
||||
:disabled="creating"
|
||||
@click="handleCreate"
|
||||
>
|
||||
{{ t('apiKey.createBtn') }}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section class="api-key-list">
|
||||
<article v-for="item in apiKeys" :key="item.id" class="api-key-item">
|
||||
<p class="item-name">{{ item.name }}</p>
|
||||
<p class="item-value">{{ item.key }}</p>
|
||||
<section v-if="loading && apiKeys.length === 0" class="api-key-loading">
|
||||
<v-progress-circular indeterminate size="32" width="2" />
|
||||
<span>{{ t('common.loading') }}</span>
|
||||
</section>
|
||||
|
||||
<section v-else-if="loadError" class="api-key-error">
|
||||
<p class="error-text">{{ loadError }}</p>
|
||||
</section>
|
||||
|
||||
<section v-else class="api-key-list">
|
||||
<article v-for="(item, idx) in apiKeys" :key="item.ID ?? idx" class="api-key-item">
|
||||
<p class="item-name">{{ item.desc || item.appKey || t('apiKey.keyDefault', { n: idx + 1 }) }}</p>
|
||||
<p class="item-value">{{ item.appKey }}</p>
|
||||
<div class="item-actions">
|
||||
<button class="action-btn action-copy" type="button">复制</button>
|
||||
<button class="action-btn action-delete" type="button">删除</button>
|
||||
<button
|
||||
class="action-btn action-copy"
|
||||
type="button"
|
||||
@click="copyKey(item.appKey)"
|
||||
>
|
||||
{{ t('apiKey.copy') }}
|
||||
</button>
|
||||
<button class="action-btn action-delete" type="button">{{ t('apiKey.delete') }}</button>
|
||||
</div>
|
||||
</article>
|
||||
<div ref="loadMoreSentinelRef" class="load-more-sentinel" aria-hidden="true"></div>
|
||||
</section>
|
||||
|
||||
<div v-if="loadingMore" class="api-key-loading-more">
|
||||
<v-progress-circular indeterminate size="24" width="2" />
|
||||
<span>{{ t('searchPage.loadMore') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface ApiKeyItem {
|
||||
id: string
|
||||
name: string
|
||||
key: string
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getApiAppList, createApiApp, type ApiApp } from '../api/apiApp'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { useToastStore } from '../stores/toast'
|
||||
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
const toastStore = useToastStore()
|
||||
|
||||
const apiKeys = ref<ApiApp[]>([])
|
||||
const loading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
const creating = ref(false)
|
||||
const loadError = ref('')
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const loadMoreSentinelRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const PAGE_SIZE = 12
|
||||
|
||||
const noMore = computed(
|
||||
() =>
|
||||
apiKeys.value.length >= total.value ||
|
||||
(apiKeys.value.length > 0 && apiKeys.value.length < PAGE_SIZE),
|
||||
)
|
||||
|
||||
async function fetchList(append: boolean) {
|
||||
const headers = userStore.getAuthHeaders()
|
||||
if (!headers) {
|
||||
loadError.value = t('error.pleaseLogin')
|
||||
return
|
||||
}
|
||||
|
||||
const nextPage = append ? page.value : 1
|
||||
if (append) loadingMore.value = true
|
||||
else loading.value = true
|
||||
loadError.value = ''
|
||||
|
||||
try {
|
||||
const res = await getApiAppList(
|
||||
{ page: nextPage, pageSize: PAGE_SIZE },
|
||||
{ headers },
|
||||
)
|
||||
if (res.code !== 0 && res.code !== 200) {
|
||||
loadError.value = res.msg || t('error.loadFailed')
|
||||
return
|
||||
}
|
||||
const list = res.data?.list ?? []
|
||||
const dataTotal = res.data?.total ?? 0
|
||||
total.value = dataTotal
|
||||
page.value = res.data?.page ?? nextPage
|
||||
|
||||
if (append) {
|
||||
apiKeys.value = [...apiKeys.value, ...list]
|
||||
} else {
|
||||
apiKeys.value = list
|
||||
}
|
||||
} catch (e) {
|
||||
loadError.value = e instanceof Error ? e.message : t('error.loadFailed')
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const apiKeys: ApiKeyItem[] = [
|
||||
{ id: '1', name: 'Key #1', key: 'pk_live_8f2a9d1c5e7b44a8b1c2d3e4f5a6b7c8' },
|
||||
{ id: '2', name: 'Key #2', key: 'pk_test_4d8f8a0c2e49f6h0j2k4m8n6p0r2i4v6' },
|
||||
{ id: '3', name: 'Key #3', key: 'pk_live_1a3c5e7g8i1k3m5o7q9s1u8w5y7z9b1d' },
|
||||
]
|
||||
async function handleCreate() {
|
||||
const headers = userStore.getAuthHeaders()
|
||||
if (!headers) {
|
||||
loadError.value = t('error.pleaseLogin')
|
||||
return
|
||||
}
|
||||
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||
if (uid == null) {
|
||||
loadError.value = t('error.pleaseLogin')
|
||||
return
|
||||
}
|
||||
|
||||
creating.value = true
|
||||
try {
|
||||
const res = await createApiApp(
|
||||
{
|
||||
appKey: '',
|
||||
appSecret: '',
|
||||
status: true,
|
||||
userId: Number(uid),
|
||||
desc: t('apiKey.defaultDesc'),
|
||||
},
|
||||
{ headers },
|
||||
)
|
||||
if (res.code !== 0 && res.code !== 200) {
|
||||
toastStore.show(res.msg || t('error.loadFailed'), 'error')
|
||||
return
|
||||
}
|
||||
toastStore.show(t('toast.createKeySuccess'))
|
||||
await fetchList(false)
|
||||
} catch (e) {
|
||||
toastStore.show(
|
||||
e instanceof Error ? e.message : t('error.loadFailed'),
|
||||
'error',
|
||||
)
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function copyKey(key: string) {
|
||||
if (!key) return
|
||||
navigator.clipboard
|
||||
.writeText(key)
|
||||
.then(() => {
|
||||
toastStore.show(t('deposit.copied'))
|
||||
})
|
||||
.catch(() => {
|
||||
toastStore.show(t('profile.copyFailed'), 'error')
|
||||
})
|
||||
}
|
||||
|
||||
let loadMoreObserver: IntersectionObserver | null = null
|
||||
const LOAD_MORE_THRESHOLD = 200
|
||||
|
||||
async function loadMore() {
|
||||
if (loadingMore.value || noMore.value || apiKeys.value.length === 0) return
|
||||
await fetchList(true)
|
||||
}
|
||||
|
||||
function setupLoadMoreObserver() {
|
||||
const sentinel = loadMoreSentinelRef.value
|
||||
const scrollEl = document.querySelector('[data-main-scroll]')
|
||||
if (!sentinel || !scrollEl || loadMoreObserver) return
|
||||
loadMoreObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (!entries[0]?.isIntersecting) return
|
||||
loadMore()
|
||||
},
|
||||
{ root: scrollEl, rootMargin: `${LOAD_MORE_THRESHOLD}px`, threshold: 0 },
|
||||
)
|
||||
loadMoreObserver.observe(sentinel)
|
||||
}
|
||||
|
||||
function removeLoadMoreObserver() {
|
||||
const sentinel = loadMoreSentinelRef.value
|
||||
if (loadMoreObserver && sentinel) loadMoreObserver.unobserve(sentinel)
|
||||
loadMoreObserver = null
|
||||
}
|
||||
|
||||
onMounted(() => fetchList(false))
|
||||
|
||||
watch(
|
||||
() => apiKeys.value.length > 0 && !noMore.value,
|
||||
(shouldObserve) => {
|
||||
removeLoadMoreObserver()
|
||||
if (shouldObserve) nextTick(setupLoadMoreObserver)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onUnmounted(removeLoadMoreObserver)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -82,6 +257,26 @@ const apiKeys: ApiKeyItem[] = [
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.create-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.api-key-loading,
|
||||
.api-key-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
min-height: 120px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.api-key-list {
|
||||
width: 100%;
|
||||
@ -147,4 +342,20 @@ const apiKeys: ApiKeyItem[] = [
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.load-more-sentinel {
|
||||
height: 1px;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.api-key-loading-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -25,15 +25,15 @@
|
||||
rounded="lg"
|
||||
>
|
||||
<div class="chart-header">
|
||||
<h1 class="chart-title">{{ eventDetail?.title || 'All markets' }}</h1>
|
||||
<h1 class="chart-title">{{ eventDetail?.title || t('eventMarkets.allMarkets') }}</h1>
|
||||
<p v-if="eventDetail.series?.length || eventDetail.tags?.length" class="chart-meta">
|
||||
{{ categoryText }}
|
||||
</p>
|
||||
<div class="chart-controls-row">
|
||||
<v-btn variant="text" size="small" class="past-btn">Past ▾</v-btn>
|
||||
<v-btn variant="text" size="small" class="past-btn">{{ t('eventMarkets.past') }}</v-btn>
|
||||
<v-btn class="date-pill" size="small" rounded="pill">{{ resolutionDate }}</v-btn>
|
||||
</div>
|
||||
<p class="chart-legend-hint">{{ markets.length }} 个市场</p>
|
||||
<p class="chart-legend-hint">{{ t('eventMarkets.marketsCount', { n: markets.length }) }}</p>
|
||||
</div>
|
||||
<div class="chart-wrapper">
|
||||
<div v-if="chartLoading" class="chart-loading-overlay">
|
||||
@ -43,7 +43,7 @@
|
||||
</div>
|
||||
<div class="chart-footer">
|
||||
<div class="chart-footer-left">
|
||||
<span class="chart-volume">{{ eventVolume || '$0 Vol.' }}</span>
|
||||
<span class="chart-volume">{{ eventVolume || t('eventMarkets.volumeZero') }}</span>
|
||||
<span v-if="marketExpiresAt" class="chart-expires">| {{ marketExpiresAt }}</span>
|
||||
</div>
|
||||
<div class="chart-time-ranges">
|
||||
@ -101,7 +101,7 @@
|
||||
@click="goToTradeDetail(market)"
|
||||
>
|
||||
<div class="market-row-main">
|
||||
<span class="market-question">{{ market.question || 'Market' }}</span>
|
||||
<span class="market-question">{{ market.question || t('eventMarkets.marketPlaceholder') }}</span>
|
||||
<span class="market-chance">{{ marketChance(market) }}%</span>
|
||||
</div>
|
||||
<div class="market-row-vol">{{ formatVolume(market.volume) }}</div>
|
||||
@ -152,7 +152,7 @@
|
||||
rounded="sm"
|
||||
@click="openSheetWithOption('yes')"
|
||||
>
|
||||
{{ barMarket ? yesLabel(barMarket) : 'Yes' }} {{ barMarket ? yesPrice(barMarket) : '0¢' }}
|
||||
{{ barMarket ? yesLabel(barMarket) : t('eventMarkets.yes') }} {{ barMarket ? yesPrice(barMarket) : t('eventMarkets.priceZero') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="mobile-bar-btn mobile-bar-no"
|
||||
@ -160,7 +160,7 @@
|
||||
rounded="sm"
|
||||
@click="openSheetWithOption('no')"
|
||||
>
|
||||
{{ barMarket ? noLabel(barMarket) : 'No' }} {{ barMarket ? noPrice(barMarket) : '0¢' }}
|
||||
{{ barMarket ? noLabel(barMarket) : t('eventMarkets.no') }} {{ barMarket ? noPrice(barMarket) : t('eventMarkets.priceZero') }}
|
||||
</v-btn>
|
||||
<v-menu
|
||||
v-model="mobileMenuOpen"
|
||||
@ -175,7 +175,7 @@
|
||||
variant="flat"
|
||||
icon
|
||||
rounded="pill"
|
||||
aria-label="更多操作"
|
||||
:aria-label="t('eventMarkets.moreActions')"
|
||||
>
|
||||
<v-icon size="20">mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
@ -291,7 +291,7 @@ const categoryText = computed(() => {
|
||||
})
|
||||
|
||||
function formatVolume(volume: number | undefined): string {
|
||||
if (volume == null || !Number.isFinite(volume)) return '$0 Vol.'
|
||||
if (volume == null || !Number.isFinite(volume)) return t('eventMarkets.volumeZero')
|
||||
if (volume >= 1000) return `$${(volume / 1000).toFixed(1)}k Vol.`
|
||||
return `$${Math.round(volume)} Vol.`
|
||||
}
|
||||
@ -381,7 +381,7 @@ async function loadChartFromApi(): Promise<ChartSeriesItem[]> {
|
||||
const market = list[i]
|
||||
if (!market) continue
|
||||
const yesTokenId = getClobTokenId(market, 0)
|
||||
const base = (market.question || 'Market').slice(0, 32)
|
||||
const base = (market.question || t('eventMarkets.marketPlaceholder')).slice(0, 32)
|
||||
const baseName = base + (base.length >= 32 ? '…' : '')
|
||||
const name = list.length > 1 ? `${baseName} (${i + 1}/${list.length})` : baseName
|
||||
if (!yesTokenId) {
|
||||
|
||||
@ -147,8 +147,12 @@
|
||||
:no-price="card.noPrice"
|
||||
@open-trade="onCardOpenTrade"
|
||||
/>
|
||||
<div v-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
|
||||
暂无数据
|
||||
<div v-if="eventListLoading" class="home-list-empty home-list-loading">
|
||||
<v-progress-circular indeterminate size="40" width="2" />
|
||||
<span>{{ t('common.loading') }}</span>
|
||||
</div>
|
||||
<div v-else-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
|
||||
{{ t('common.noData') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="eventList.length > 0" class="load-more-footer">
|
||||
@ -157,7 +161,7 @@
|
||||
<v-progress-circular indeterminate size="24" width="2" />
|
||||
<span>{{ t('common.loading') }}</span>
|
||||
</div>
|
||||
<div v-else-if="noMoreEvents" class="no-more-tip">没有更多了</div>
|
||||
<div v-else-if="noMoreEvents" class="no-more-tip">{{ t('home.noMore') }}</div>
|
||||
<v-btn
|
||||
v-else
|
||||
class="load-more-btn"
|
||||
@ -426,6 +430,8 @@ const eventPage = ref(1)
|
||||
const eventTotal = ref(0)
|
||||
const eventPageSize = ref(PAGE_SIZE)
|
||||
const loadingMore = ref(false)
|
||||
/** 首屏/刷新时加载事件列表(非追加) */
|
||||
const eventListLoading = ref(false)
|
||||
|
||||
const noMoreEvents = computed(() => {
|
||||
if (eventList.value.length === 0) return false
|
||||
@ -472,13 +478,17 @@ const localeStore = useLocaleStore()
|
||||
|
||||
/** 加载分类树(接口或 mock),完成后初始化选中并触发事件列表加载 */
|
||||
async function loadCategory() {
|
||||
function loadEventListAfterCategoryReady() {
|
||||
const cached = getEventListCache()
|
||||
if (cached && cached.list.length > 0) {
|
||||
eventList.value = cached.list
|
||||
eventPage.value = cached.page
|
||||
eventTotal.value = cached.total
|
||||
eventPageSize.value = cached.pageSize
|
||||
const hasCachedEvents = cached && cached.list.length > 0
|
||||
if (!hasCachedEvents) eventListLoading.value = true
|
||||
|
||||
function loadEventListAfterCategoryReady() {
|
||||
if (hasCachedEvents) {
|
||||
eventList.value = cached!.list
|
||||
eventPage.value = cached!.page
|
||||
eventTotal.value = cached!.total
|
||||
eventPageSize.value = cached!.pageSize
|
||||
eventListLoading.value = false
|
||||
} else {
|
||||
loadEvents(1, false)
|
||||
}
|
||||
@ -569,6 +579,7 @@ const activeSearchKeyword = ref('')
|
||||
async function loadEvents(page: number, append: boolean, keyword?: string) {
|
||||
const kw = keyword !== undefined ? keyword : activeSearchKeyword.value
|
||||
const tagIds = activeTagIds.value
|
||||
if (!append) eventListLoading.value = true
|
||||
try {
|
||||
const res = await getPmEventPublic({
|
||||
page,
|
||||
@ -601,6 +612,8 @@ async function loadEvents(page: number, append: boolean, keyword?: string) {
|
||||
})
|
||||
} catch (e) {
|
||||
if (!append) eventList.value = []
|
||||
} finally {
|
||||
if (!append) eventListLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -751,6 +764,14 @@ onActivated(() => {
|
||||
padding: 48px 16px;
|
||||
}
|
||||
|
||||
.home-list-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.home-list > * {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
<v-row justify="center" align="center" class="login-row">
|
||||
<v-col cols="12" sm="10" md="6" lg="4">
|
||||
<v-card class="login-card" elevation="0" rounded="lg">
|
||||
<v-card-title class="login-title">Sign In</v-card-title>
|
||||
<v-card-title class="login-title">{{ t('login.title') }}</v-card-title>
|
||||
<v-card-text class="login-body">
|
||||
<p class="login-desc">Connect your wallet to sign in</p>
|
||||
<p class="login-desc">{{ t('login.desc') }}</p>
|
||||
<v-btn
|
||||
class="wallet-btn"
|
||||
color="success"
|
||||
@ -17,7 +17,7 @@
|
||||
:disabled="isConnecting"
|
||||
>
|
||||
<v-icon start>mdi-wallet</v-icon>
|
||||
{{ isConnecting ? 'Connecting...' : 'Connect with Wallet' }}
|
||||
{{ isConnecting ? t('login.connecting') : t('login.connectBtn') }}
|
||||
</v-btn>
|
||||
|
||||
<div v-if="errorMessage" class="error-message">
|
||||
@ -25,7 +25,7 @@
|
||||
</div>
|
||||
|
||||
<div class="login-links">
|
||||
<router-link to="/register">Don't have an account? Sign Up</router-link>
|
||||
<router-link to="/register">{{ t('login.signUp') }}</router-link>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@ -37,6 +37,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { BrowserProvider } from 'ethers'
|
||||
import { SiweMessage } from 'siwe'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
@ -44,6 +45,7 @@ import type { UserInfo } from '@/stores/user'
|
||||
import { post } from '@/api/request'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
const isConnecting = ref(false)
|
||||
const errorMessage = ref('')
|
||||
@ -54,9 +56,7 @@ const connectWithWallet = async () => {
|
||||
|
||||
try {
|
||||
if (!window.ethereum) {
|
||||
throw new Error(
|
||||
'Ethereum wallet is not installed. Please install MetaMask, TokenPocket or another Ethereum wallet to continue.',
|
||||
)
|
||||
throw new Error('WALLET_NOT_INSTALLED')
|
||||
}
|
||||
|
||||
const accounts = await window.ethereum.request({
|
||||
@ -66,7 +66,7 @@ const connectWithWallet = async () => {
|
||||
const walletAddress = accounts[0]
|
||||
|
||||
if (!walletAddress || !/^0x[0-9a-fA-F]{40}$/.test(walletAddress)) {
|
||||
throw new Error('Invalid wallet address format. Please check your wallet connection.')
|
||||
throw new Error('INVALID_ADDRESS')
|
||||
}
|
||||
|
||||
const chainId = await window.ethereum.request({
|
||||
@ -85,7 +85,7 @@ const connectWithWallet = async () => {
|
||||
scheme,
|
||||
domain,
|
||||
address: signer.address,
|
||||
statement: 'Sign in to TestMarket',
|
||||
statement: t('login.siweStatement'),
|
||||
uri: origin,
|
||||
version: '1',
|
||||
chainId: chainId,
|
||||
@ -121,10 +121,16 @@ const connectWithWallet = async () => {
|
||||
}
|
||||
|
||||
router.push('/')
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error('Error connecting to wallet:', error)
|
||||
errorMessage.value =
|
||||
error.message || 'Failed to connect with wallet. Please check your wallet and try again.'
|
||||
const msg = error instanceof Error ? error.message : ''
|
||||
if (msg === 'WALLET_NOT_INSTALLED') {
|
||||
errorMessage.value = t('login.walletNotInstalled')
|
||||
} else if (msg === 'INVALID_ADDRESS') {
|
||||
errorMessage.value = t('login.invalidAddress')
|
||||
} else {
|
||||
errorMessage.value = msg || t('login.connectFailed')
|
||||
}
|
||||
} finally {
|
||||
isConnecting.value = false
|
||||
}
|
||||
|
||||
@ -88,7 +88,14 @@
|
||||
{{ t('profile.cancel') }}
|
||||
</button>
|
||||
<button class="name-dialog-save-btn" type="button" :disabled="isSaving" @click="saveName">
|
||||
{{ t('profile.save') }}
|
||||
<v-progress-circular
|
||||
v-if="isSaving"
|
||||
indeterminate
|
||||
size="18"
|
||||
width="2"
|
||||
class="save-btn-spinner"
|
||||
/>
|
||||
<span v-else>{{ t('profile.save') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</v-card>
|
||||
@ -274,6 +281,7 @@ async function saveName() {
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
nameError.value = null
|
||||
try {
|
||||
const authHeaders = userStore.getAuthHeaders()
|
||||
if (!authHeaders) {
|
||||
@ -283,17 +291,22 @@ async function saveName() {
|
||||
|
||||
const res = await setSelfUsername(authHeaders, { username: trimmed })
|
||||
if (res.code !== 0 && res.code !== 200) {
|
||||
nameError.value = res.msg || t('error.requestFailed')
|
||||
const errMsg = res.msg || t('profile.nameSaveFailed')
|
||||
nameError.value = errMsg
|
||||
toastStore.show(errMsg, 'error')
|
||||
return
|
||||
}
|
||||
|
||||
// 接口成功后刷新用户信息,确保 userName/headerImg 等字段一致
|
||||
await userStore.fetchUserInfo()
|
||||
toastStore.show(t('profile.nameSaved'), 'success')
|
||||
toastStore.show(t('profile.nameSaved'))
|
||||
editNameDialogOpen.value = false
|
||||
} catch (e) {
|
||||
const errMsg = e instanceof Error ? e.message : t('profile.nameSaveFailed')
|
||||
nameError.value = errMsg
|
||||
toastStore.show(errMsg, 'error')
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
nameError.value = null
|
||||
}
|
||||
}
|
||||
|
||||
@ -682,6 +695,7 @@ onMounted(() => {
|
||||
|
||||
.name-dialog-save-btn {
|
||||
height: 34px;
|
||||
min-width: 72px;
|
||||
border-radius: 10px;
|
||||
border: 0;
|
||||
background: #5b5bd6;
|
||||
@ -690,10 +704,18 @@ onMounted(() => {
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
padding: 0 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.name-dialog-save-btn:disabled {
|
||||
opacity: 0.6;
|
||||
opacity: 0.8;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.save-btn-spinner {
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -39,6 +39,10 @@
|
||||
<div ref="loadMoreSentinelRef" class="load-more-sentinel" aria-hidden="true"></div>
|
||||
</section>
|
||||
<div v-else-if="searchError" class="search-error search-error--block">{{ searchError }}</div>
|
||||
<section v-else-if="searching" class="search-empty search-empty--loading">
|
||||
<v-progress-circular indeterminate size="40" width="2" />
|
||||
<p class="search-empty-text">{{ t('searchPage.searching') }}</p>
|
||||
</section>
|
||||
<section v-else class="search-empty">
|
||||
<p class="search-empty-text">{{ t('searchPage.noResults') }}</p>
|
||||
</section>
|
||||
@ -66,8 +70,10 @@
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<div v-if="searching" class="search-loading">{{ t('searchPage.searching') }}</div>
|
||||
<div v-else-if="loadingMore" class="search-loading">{{ t('searchPage.loadMore') }}</div>
|
||||
<div v-if="loadingMore" class="search-loading">
|
||||
<v-progress-circular indeterminate size="20" width="2" />
|
||||
<span>{{ t('searchPage.loadMore') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -547,7 +553,11 @@ function openResult(item: SearchResultItem) {
|
||||
.search-error {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
padding: 0 4px;
|
||||
padding: 12px 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-error {
|
||||
@ -568,6 +578,13 @@ function openResult(item: SearchResultItem) {
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
.search-empty--loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-empty-text {
|
||||
margin: 0;
|
||||
|
||||
@ -1,6 +1,16 @@
|
||||
<template>
|
||||
<v-container fluid class="trade-detail-container">
|
||||
<v-row align="stretch" no-gutters class="trade-detail-row">
|
||||
<v-pull-to-refresh class="trade-detail-pull-refresh" @load="onRefresh">
|
||||
<div class="trade-detail-pull-refresh-inner">
|
||||
<!-- findPmEvent 请求中:仅显示 loading -->
|
||||
<v-card v-if="detailLoading && !eventDetail" class="trade-detail-loading-card" elevation="0" rounded="lg">
|
||||
<div class="trade-detail-loading-placeholder">
|
||||
<v-progress-circular indeterminate color="primary" size="48" />
|
||||
<p>{{ t('common.loading') }}</p>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<v-row v-else align="stretch" no-gutters class="trade-detail-row">
|
||||
<!-- 左侧:分时图 + 订单簿(宽度弹性) -->
|
||||
<v-col cols="12" class="chart-col">
|
||||
<!-- 分时图卡片(Polymarket 样式) -->
|
||||
@ -86,7 +96,10 @@
|
||||
</v-tabs>
|
||||
<v-window v-model="positionsOrdersTab" class="positions-orders-window">
|
||||
<v-window-item value="positions" class="detail-pane">
|
||||
<div v-if="positionLoading" class="placeholder-pane">{{ t('common.loading') }}</div>
|
||||
<div v-if="positionLoading" class="placeholder-pane placeholder-pane--loading">
|
||||
<v-progress-circular indeterminate size="36" width="2" />
|
||||
<span>{{ t('common.loading') }}</span>
|
||||
</div>
|
||||
<div v-else-if="marketPositionsFiltered.length === 0" class="placeholder-pane">
|
||||
{{ t('activity.noPositionsInMarket') }}
|
||||
</div>
|
||||
@ -133,7 +146,10 @@
|
||||
</div>
|
||||
</v-window-item>
|
||||
<v-window-item value="orders" class="detail-pane">
|
||||
<div v-if="openOrderLoading" class="placeholder-pane">{{ t('common.loading') }}</div>
|
||||
<div v-if="openOrderLoading" class="placeholder-pane placeholder-pane--loading">
|
||||
<v-progress-circular indeterminate size="36" width="2" />
|
||||
<span>{{ t('common.loading') }}</span>
|
||||
</div>
|
||||
<div v-else-if="marketOpenOrders.length === 0" class="placeholder-pane">
|
||||
{{ t('activity.noOpenOrdersInMarket') }}
|
||||
</div>
|
||||
@ -309,6 +325,8 @@
|
||||
/>
|
||||
</v-dialog>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-pull-to-refresh>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
@ -477,6 +495,14 @@ async function loadEventDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
function onRefresh({ done }: { done: () => void }) {
|
||||
Promise.all([
|
||||
loadEventDetail(),
|
||||
loadMarketPositions(),
|
||||
loadMarketOpenOrders(),
|
||||
]).finally(() => done())
|
||||
}
|
||||
|
||||
// 标题、成交量、到期日:优先接口详情,其次卡片 query,最后占位
|
||||
const marketTitle = computed(() => {
|
||||
if (eventDetail.value?.title) return eventDetail.value.title
|
||||
@ -1316,6 +1342,26 @@ watch(
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
// 内容显示时初始化图表(chart 容器在 v-else 内,loading 时不存在)
|
||||
watch(
|
||||
() => [detailLoading.value, eventDetail.value] as const,
|
||||
([loading, detail]) => {
|
||||
if (loading) {
|
||||
chartInstance?.remove()
|
||||
chartInstance = null
|
||||
chartSeries = null
|
||||
return
|
||||
}
|
||||
if (detail) {
|
||||
nextTick(() => {
|
||||
initChart()
|
||||
updateChartData()
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 监听语言切换,语言变化时重新加载数据
|
||||
watch(
|
||||
() => localeStore.currentLocale,
|
||||
@ -1334,8 +1380,7 @@ const unsubscribePositionUpdate = userStore.onPositionUpdate((data) => {
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadEventDetail().then(() => updateChartData())
|
||||
initChart()
|
||||
loadEventDetail()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
@ -1360,6 +1405,37 @@ onUnmounted(() => {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.trade-detail-pull-refresh {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.trade-detail-pull-refresh-inner {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.trade-detail-loading-card {
|
||||
padding: 48px;
|
||||
text-align: center;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.trade-detail-loading-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.trade-detail-loading-placeholder p {
|
||||
margin: 0;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@ -1804,6 +1880,13 @@ onUnmounted(() => {
|
||||
font-size: 14px;
|
||||
padding: 24px 0;
|
||||
}
|
||||
.placeholder-pane--loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.rules-pane {
|
||||
padding-top: 16px;
|
||||
|
||||
@ -64,7 +64,10 @@
|
||||
<v-card class="table-card wallet-design-card" elevation="0" rounded="lg">
|
||||
<template v-if="activeTab === 'positions'">
|
||||
<div class="positions-mobile-list">
|
||||
<div v-if="positionLoading" class="empty-cell">{{ t('common.loading') }}</div>
|
||||
<div v-if="positionLoading" class="empty-cell empty-cell--loading">
|
||||
<v-progress-circular indeterminate size="36" width="2" />
|
||||
<span>{{ t('common.loading') }}</span>
|
||||
</div>
|
||||
<div v-else-if="filteredPositions.length === 0" class="empty-cell">
|
||||
{{ t('wallet.noPositionsFound') }}
|
||||
</div>
|
||||
@ -140,7 +143,10 @@
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'orders'">
|
||||
<div class="orders-mobile-list">
|
||||
<div v-if="openOrderLoading" class="empty-cell">{{ t('common.loading') }}</div>
|
||||
<div v-if="openOrderLoading" class="empty-cell empty-cell--loading">
|
||||
<v-progress-circular indeterminate size="36" width="2" />
|
||||
<span>{{ t('common.loading') }}</span>
|
||||
</div>
|
||||
<div v-else-if="filteredOpenOrders.length === 0" class="empty-cell">
|
||||
{{ t('wallet.noOpenOrdersFound') }}
|
||||
</div>
|
||||
@ -199,7 +205,10 @@
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'history'">
|
||||
<div class="history-mobile-list">
|
||||
<div v-if="historyLoading" class="empty-cell">{{ t('common.loading') }}</div>
|
||||
<div v-if="historyLoading" class="empty-cell empty-cell--loading">
|
||||
<v-progress-circular indeterminate size="36" width="2" />
|
||||
<span>{{ t('common.loading') }}</span>
|
||||
</div>
|
||||
<div v-else-if="filteredHistory.length === 0" class="empty-cell">
|
||||
{{ t('wallet.noHistoryFound') }}
|
||||
</div>
|
||||
@ -275,7 +284,10 @@
|
||||
</template>
|
||||
<template v-else-if="activeTab === 'withdrawals'">
|
||||
<div class="withdrawals-mobile-list">
|
||||
<div v-if="withdrawalsLoading" class="empty-cell">{{ t('common.loading') }}</div>
|
||||
<div v-if="withdrawalsLoading" class="empty-cell empty-cell--loading">
|
||||
<v-progress-circular indeterminate size="36" width="2" />
|
||||
<span>{{ t('common.loading') }}</span>
|
||||
</div>
|
||||
<div v-else-if="displayedWithdrawals.length === 0" class="empty-cell">
|
||||
{{ t('wallet.noWithdrawalsFound') }}
|
||||
</div>
|
||||
@ -708,39 +720,6 @@ const positions = ref<Position[]>(USE_MOCK_WALLET ? [...MOCK_WALLET_POSITIONS] :
|
||||
const withdrawalsList = ref<SettlementRequestClientItem[]>([])
|
||||
const withdrawalsTotal = ref(0)
|
||||
const withdrawalsLoading = ref(false)
|
||||
const previewWithdrawals = ref<SettlementRequestClientItem[]>([
|
||||
{
|
||||
ID: -1,
|
||||
CreatedAt: '2024-01-15T14:30:25Z',
|
||||
chain: 'Polygon',
|
||||
amount: 2500000000,
|
||||
fee: '0.00',
|
||||
status: WITHDRAW_STATUS.PENDING,
|
||||
tokenAddress: '0x27fa...79ab',
|
||||
tokenSymbol: 'USDT',
|
||||
},
|
||||
{
|
||||
ID: -2,
|
||||
CreatedAt: '2024-01-14T09:16:53Z',
|
||||
chain: 'Bitcoin',
|
||||
amount: 850000000,
|
||||
fee: '0.0065',
|
||||
status: WITHDRAW_STATUS.SUCCESS,
|
||||
tokenAddress: 'bc1q...49x7n',
|
||||
tokenSymbol: 'BTC',
|
||||
},
|
||||
{
|
||||
ID: -3,
|
||||
CreatedAt: '2024-01-12T11:26:03Z',
|
||||
chain: 'Ethereum',
|
||||
amount: 5260000000,
|
||||
fee: '0.05',
|
||||
status: WITHDRAW_STATUS.REJECTED,
|
||||
tokenAddress: '0x7f42...f9d9',
|
||||
tokenSymbol: 'ETH',
|
||||
},
|
||||
])
|
||||
|
||||
/** 持仓列表(API 数据,非 mock 时使用) */
|
||||
const positionList = ref<Position[]>([])
|
||||
const positionTotal = ref(0)
|
||||
@ -912,9 +891,7 @@ async function loadWithdrawals() {
|
||||
}
|
||||
}
|
||||
|
||||
const displayedWithdrawals = computed(() =>
|
||||
withdrawalsList.value.length > 0 ? withdrawalsList.value : previewWithdrawals.value,
|
||||
)
|
||||
const displayedWithdrawals = computed(() => withdrawalsList.value)
|
||||
|
||||
function formatWithdrawAmount(amount: number | undefined): string {
|
||||
return amountToUsdcDisplay(amount)
|
||||
@ -2527,6 +2504,13 @@ async function submitAuthorize() {
|
||||
padding: 48px 16px !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
.empty-cell--loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 提现记录:与 history 一致,用 margin-bottom 控制间距,不用 gap */
|
||||
.withdrawals-mobile-list {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user