Compare commits

..

2 Commits

Author SHA1 Message Date
ivan
e41d6b5b0d 新增:国际化 2026-02-25 21:08:23 +08:00
ivan
359059bff6 新增:购买成功后的提示 2026-02-25 18:40:46 +08:00
21 changed files with 1417 additions and 249 deletions

71
package-lock.json generated
View File

@ -15,6 +15,7 @@
"pinia": "^3.0.4",
"siwe": "^3.0.0",
"vue": "^3.5.27",
"vue-i18n": "^11.2.8",
"vue-router": "^5.0.1",
"vuetify": "^4.0.0-beta.0",
"ws": "^8.19.0"
@ -1396,6 +1397,50 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@intlify/core-base": {
"version": "11.2.8",
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.2.8.tgz",
"integrity": "sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "11.2.8",
"@intlify/shared": "11.2.8"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.2.8",
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.2.8.tgz",
"integrity": "sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.2.8",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "11.2.8",
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.2.8.tgz",
"integrity": "sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -8719,6 +8764,32 @@
"node": ">=10"
}
},
"node_modules/vue-i18n": {
"version": "11.2.8",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.2.8.tgz",
"integrity": "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.2.8",
"@intlify/shared": "11.2.8",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-i18n/node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/vue-router": {
"version": "5.0.2",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-5.0.2.tgz",

View File

@ -25,6 +25,7 @@
"pinia": "^3.0.4",
"siwe": "^3.0.0",
"vue": "^3.5.27",
"vue-i18n": "^11.2.8",
"vue-router": "^5.0.1",
"vuetify": "^4.0.0-beta.0",
"ws": "^8.19.0"

View File

@ -1,9 +1,14 @@
<script setup lang="ts">
import { computed, onMounted, watch, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useUserStore } from './stores/user'
import { useLocaleStore } from './stores/locale'
import Toast from './components/Toast.vue'
const route = useRoute()
const { t } = useI18n()
const localeStore = useLocaleStore()
const router = useRouter()
const userStore = useUserStore()
@ -40,20 +45,36 @@ watch(
icon
variant="text"
class="back-btn"
aria-label="返回"
:aria-label="t('common.back')"
@click="$router.back()"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-app-bar-title v-if="currentRoute === '/'">PolyMarket</v-app-bar-title>
<v-spacer></v-spacer>
<v-menu location="bottom" :close-on-content-click="true" class="locale-menu">
<template #activator="{ props }">
<v-btn v-bind="props" icon variant="text" class="locale-btn" :aria-label="t('common.more')">
<v-icon size="20">mdi-translate</v-icon>
</v-btn>
</template>
<v-list density="compact">
<v-list-item
v-for="opt in localeStore.localeOptions"
:key="opt.value"
:title="opt.label"
:active="localeStore.currentLocale === opt.value"
@click="localeStore.setLocale(opt.value)"
/>
</v-list>
</v-menu>
<v-btn
v-if="!userStore.isLoggedIn"
text
to="/login"
:class="{ active: currentRoute === '/login' }"
>
Login
{{ t('common.login') }}
</v-btn>
<template v-else>
<v-btn
@ -77,10 +98,10 @@ watch(
</template>
<v-list density="compact">
<v-list-item
:title="userStore.user?.nickName || userStore.user?.userName || 'User'"
:title="userStore.user?.nickName || userStore.user?.userName || t('common.user')"
disabled
/>
<v-list-item title="退出登录" @click="userStore.logout()" />
<v-list-item :title="t('common.logout')" @click="userStore.logout()" />
</v-list>
</v-menu>
</template>
@ -92,6 +113,7 @@ watch(
</keep-alive>
</router-view>
</v-main>
<Toast />
</v-app>
</template>

View File

@ -9,9 +9,9 @@
>
<v-card rounded="lg">
<div class="deposit-header">
<h2 class="deposit-title">Deposit</h2>
<p class="deposit-balance">Polymarket Balance: ${{ balance }}</p>
<v-btn icon variant="text" class="close-btn" aria-label="关闭" @click="close">
<h2 class="deposit-title">{{ t('deposit.title') }}</h2>
<p class="deposit-balance">{{ t('deposit.polymarketBalance') }} ${{ balance }}</p>
<v-btn icon variant="text" class="close-btn" :aria-label="t('deposit.close')" @click="close">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
@ -29,8 +29,8 @@
<v-icon size="28">mdi-lightning-bolt</v-icon>
</div>
<div class="method-info">
<div class="method-name">Transfer Crypto</div>
<div class="method-desc">No limit Instant</div>
<div class="method-name">{{ t('deposit.transferCrypto') }}</div>
<div class="method-desc">{{ t('deposit.noLimit') }} {{ t('deposit.instant') }}</div>
</div>
<v-icon class="method-arrow">mdi-chevron-right</v-icon>
</v-card>
@ -44,8 +44,8 @@
<v-icon size="28">mdi-link-variant</v-icon>
</div>
<div class="method-info">
<div class="method-name">Connect Exchange</div>
<div class="method-desc">No limit 2 min</div>
<div class="method-name">{{ t('deposit.connectExchange') }}</div>
<div class="method-desc">{{ t('deposit.noLimit') }} {{ t('deposit.twoMin') }}</div>
</div>
<v-icon class="method-arrow">mdi-chevron-right</v-icon>
</v-card>
@ -58,26 +58,26 @@
<v-btn icon variant="text" size="small" @click="step = 'method'">
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<span class="step-title">Transfer Crypto</span>
<span class="step-title">{{ t('deposit.transferCrypto') }}</span>
</div>
<v-select
v-model="selectedNetwork"
:items="networks"
item-title="label"
item-value="id"
label="Network"
:label="t('deposit.network')"
variant="outlined"
density="comfortable"
hide-details
class="network-select"
/>
<p class="support-tip">Supported: USDC, ETH</p>
<p class="support-tip">{{ t('deposit.supportedTip') }}</p>
<div class="address-box">
<label class="address-label">Deposit address</label>
<label class="address-label">{{ t('deposit.depositAddress') }}</label>
<div class="address-row">
<code class="address-code">{{ depositAddressShort }}</code>
<v-btn size="small" variant="tonal" @click="copyAddress">
{{ copied ? 'Copied' : 'Copy' }}
{{ copied ? t('deposit.copied') : t('deposit.copy') }}
</v-btn>
</div>
<div class="qr-wrap">
@ -92,12 +92,11 @@
<v-btn icon variant="text" size="small" @click="step = 'method'">
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<span class="step-title">Connect Exchange</span>
<span class="step-title">{{ t('deposit.connectExchange') }}</span>
</div>
<template v-if="!exchangeConnected">
<p class="connect-desc">
Connect your wallet to deposit. Send USDC or ETH to your deposit address after
connecting.
{{ t('deposit.connectDesc') }}
</p>
<div class="wallet-buttons">
<v-btn
@ -109,29 +108,29 @@
@click="connectMetaMask"
>
<v-icon start>mdi-wallet</v-icon>
MetaMask
{{ t('deposit.metaMask') }}
</v-btn>
<v-btn class="wallet-btn" variant="outlined" rounded="lg" block disabled>
<v-icon start>mdi-wallet</v-icon>
Coinbase Wallet (Coming soon)
{{ t('deposit.coinbaseComingSoon') }}
</v-btn>
<v-btn class="wallet-btn" variant="outlined" rounded="lg" block disabled>
<v-icon start>mdi-wallet</v-icon>
WalletConnect (Coming soon)
{{ t('deposit.walletConnectComingSoon') }}
</v-btn>
</div>
</template>
<template v-else>
<p class="connected-tip">
<v-icon color="success" size="18">mdi-check-circle</v-icon>
Connected. Send USDC or ETH to the address below.
{{ t('deposit.connectedTip') }}
</p>
<div class="address-box">
<label class="address-label">Deposit address</label>
<label class="address-label">{{ t('deposit.depositAddress') }}</label>
<div class="address-row">
<code class="address-code">{{ depositAddressShort }}</code>
<v-btn size="small" variant="tonal" @click="copyAddress">
{{ copied ? 'Copied' : 'Copy' }}
{{ copied ? t('deposit.copied') : t('deposit.copy') }}
</v-btn>
</div>
<div class="qr-wrap">
@ -147,7 +146,9 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = withDefaults(
defineProps<{
modelValue: boolean
@ -204,7 +205,7 @@ async function copyAddress() {
async function connectMetaMask() {
if (!window.ethereum) {
alert('Please install MetaMask or another Web3 wallet.')
alert(t('deposit.installMetaMask'))
return
}
connecting.value = true

View File

@ -36,7 +36,7 @@
</svg>
<div class="semi-progress-inner">
<span class="chance-value">{{ chanceValue }}%</span>
<span class="chance-label">chance</span>
<span class="chance-label">{{ t('common.chance') }}</span>
</div>
</div>
</div>
@ -164,9 +164,11 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import type { EventCardOutcome } from '../api/event'
const { t } = useI18n()
const router = useRouter()
const emit = defineEmits<{
openTrade: [

View File

@ -2,11 +2,11 @@
<v-card class="order-book" elevation="0">
<!-- Header -->
<div class="order-book-header">
<h3 class="order-book-title">Order Book</h3>
<h3 class="order-book-title">{{ t('trade.orderBook') }}</h3>
<div class="order-book-vol">
<span v-if="connected" class="live-badge">
<span class="live-dot"></span>
Live
{{ t('activity.live') }}
</span>
<span v-else-if="loading" class="loading-badge">连接中...</span>
<span v-else class="vol-text">$4.4k Vol.</span>
@ -17,8 +17,8 @@
<!-- Trade Tabs -->
<div class="trade-tabs-container">
<v-tabs v-model="activeTrade" class="trade-tabs" density="comfortable">
<v-tab value="up" class="trade-tab">Trade Up</v-tab>
<v-tab value="down" class="trade-tab">Trade Down</v-tab>
<v-tab value="up" class="trade-tab">{{ t('trade.buyLabel', { label: props.yesLabel }) }}</v-tab>
<v-tab value="down" class="trade-tab">{{ t('trade.buyLabel', { label: props.noLabel }) }}</v-tab>
</v-tabs>
</div>
@ -80,8 +80,11 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import HorizontalProgressBar from './HorizontalProgressBar.vue'
const { t } = useI18n()
export interface OrderBookRow {
price: number
shares: number
@ -95,6 +98,10 @@ const props = withDefaults(
spread?: number
loading?: boolean
connected?: boolean
/** 市场 Yes 选项文案,来自 market.outcomes[0] */
yesLabel?: string
/** 市场 No 选项文案,来自 market.outcomes[1] */
noLabel?: string
}>(),
{
asks: () => [],
@ -103,6 +110,8 @@ const props = withDefaults(
spread: undefined,
loading: false,
connected: false,
yesLabel: 'Yes',
noLabel: 'No',
},
)

143
src/components/Toast.vue Normal file
View File

@ -0,0 +1,143 @@
<template>
<Teleport to="body">
<div class="toast-container">
<TransitionGroup name="toast" tag="div" class="toast-stack">
<div
v-for="item in toastStore.displaying"
:key="item.id"
:class="['toast-item', `toast-item--${item.type}`]"
role="status"
aria-live="polite"
>
<span class="toast-message">
{{ item.count > 1 ? `${item.message} (×${item.count})` : item.message }}
</span>
</div>
</TransitionGroup>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { useToastStore } from '@/stores/toast'
import type { ToastItem } from '@/stores/toast'
const toastStore = useToastStore()
const TIMEOUT_MS = 3000
const timers = new Map<string, ReturnType<typeof setTimeout>>()
function scheduleRemove(item: ToastItem) {
const t = setTimeout(() => {
toastStore.remove(item.id)
timers.delete(item.id)
}, TIMEOUT_MS)
timers.set(item.id, t)
}
function syncTimers() {
for (const item of toastStore.displaying) {
const existing = timers.get(item.id)
if (existing) clearTimeout(existing)
scheduleRemove(item)
}
for (const [id] of timers) {
if (!toastStore.displaying.some((d) => d.id === id)) {
clearTimeout(timers.get(id)!)
timers.delete(id)
}
}
}
onMounted(() => {
syncTimers()
const stop = toastStore.$subscribe(syncTimers)
onUnmounted(() => {
stop()
timers.forEach((t) => clearTimeout(t))
timers.clear()
})
})
</script>
<style scoped>
.toast-container {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 10000;
pointer-events: none;
/* 避开顶部导航栏(约 64px+ 安全区域 */
padding: max(72px, calc(64px + env(safe-area-inset-top))) 16px 16px;
display: flex;
justify-content: center;
}
.toast-stack {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
width: min(520px, calc(100vw - 32px));
}
.toast-item {
position: relative;
width: 100%;
padding: 12px 28px;
border-radius: 8px;
backdrop-filter: blur(8px);
text-align: center;
font-size: 14px;
font-weight: 500;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
pointer-events: auto;
}
.toast-item--success {
background: rgba(76, 175, 80, 0.65);
color: rgba(255, 255, 255, 0.95);
}
.toast-item--error {
background: rgba(244, 67, 54, 0.65);
color: rgba(255, 255, 255, 0.95);
}
.toast-item--warning {
background: rgba(255, 152, 0, 0.65);
color: rgba(33, 33, 33, 0.95);
}
.toast-item--info {
background: rgba(33, 150, 243, 0.65);
color: rgba(255, 255, 255, 0.95);
}
.toast-message {
display: block;
width: 100%;
}
/* Transition */
.toast-enter-active,
.toast-leave-active {
transition: all 0.25s ease;
}
.toast-enter-from {
opacity: 0;
transform: translateY(-12px);
}
.toast-leave-to {
opacity: 0;
transform: translateY(-8px);
}
/* 下方 Toast 在上方消失时平缓上移 */
.toast-move {
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
</style>

View File

@ -4,29 +4,29 @@
<!-- Header -->
<div class="header">
<v-tabs v-model="activeTab" class="buy-sell-tabs minimal-tabs" density="compact">
<v-tab value="buy" class="minimal-tab">Buy</v-tab>
<v-tab value="sell" class="minimal-tab">Sell</v-tab>
<v-tab value="buy" class="minimal-tab">{{ t('trade.buy') }}</v-tab>
<v-tab value="sell" class="minimal-tab">{{ t('trade.sell') }}</v-tab>
</v-tabs>
<v-menu>
<template v-slot:activator="{ props, isActive }">
<v-btn v-bind="props" class="limit-btn" text end>
{{ limitType }}
{{ limitTypeDisplay }}
<v-icon right>{{ isActive ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item @click="limitType = 'Market'">
<v-list-item-title>Market</v-list-item-title>
<v-list-item-title>{{ t('trade.market') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="limitType = 'Limit'">
<v-list-item-title>Limit</v-list-item-title>
<v-list-item-title>{{ t('trade.limit') }}</v-list-item-title>
</v-list-item>
<v-divider></v-divider>
<v-list-item @click="openMergeDialog">
<v-list-item-title>Merge</v-list-item-title>
<v-list-item-title>{{ t('trade.merge') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="openSplitDialog">
<v-list-item-title>Split</v-list-item-title>
<v-list-item-title>{{ t('trade.split') }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
@ -61,8 +61,8 @@
<div class="input-group">
<div class="amount-header">
<div>
<span class="label amount-label">Amount</span>
<span class="balance-label">Balance ${{ balance.toFixed(2) }}</span>
<span class="label amount-label">{{ t('trade.amount') }}</span>
<span class="balance-label">{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span>
</div>
<div class="amount-value">${{ amount.toFixed(2) }}</div>
</div>
@ -70,7 +70,7 @@
<v-btn class="amount-btn" @click="adjustAmount(1)">+$1</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(20)">+$20</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">{{ t('trade.max') }}</v-btn>
</div>
</div>
</template>
@ -78,7 +78,7 @@
<template v-if="activeTab === 'sell'">
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<span class="label">{{ t('trade.shares') }}</span>
<div class="shares-input">
<v-text-field
:model-value="shares"
@ -96,7 +96,7 @@
<div class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">{{ t('trade.max') }}</v-btn>
</div>
</div>
</template>
@ -106,11 +106,11 @@
<!-- Buy模式 -->
<template v-if="activeTab === 'buy'">
<div class="total-row">
<span class="label">Total</span>
<span class="label">{{ t('trade.total') }}</span>
<span class="total-value">${{ totalPrice }}</span>
</div>
<div class="total-row">
<span class="label">To win</span>
<span class="label">{{ t('trade.toWin') }}</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ toWinValue }}
@ -120,7 +120,7 @@
<!-- Sell模式 -->
<template v-else>
<div class="total-row">
<span class="label">You'll receive</span>
<span class="label">{{ t('trade.youllReceive') }}</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ totalPrice }}
@ -128,7 +128,7 @@
</div>
<div class="total-row avg-price-row">
<span class="label">
Avg. Price {{ avgPriceCents }}¢
{{ t('trade.avgPrice') }} {{ avgPriceCents }}¢
<v-icon size="14" class="info-icon">mdi-information-outline</v-icon>
</span>
</div>
@ -136,13 +136,13 @@
</div>
<p v-if="orderError" class="order-error">{{ orderError }}</p>
<!-- Action Button: Buy 余额足够显示 Buy Yes/No不足显示 DepositSell 只显示 Sell Yes/No -->
<!-- Action Button: Buy 余额足够显示 Buy Yes/No不足显示 {{ t('trade.deposit') }}Sell 只显示 Sell Yes/No -->
<v-btn
v-if="activeTab === 'buy' && showDepositForBuy"
class="deposit-btn"
@click="deposit"
>
Deposit
{{ t('trade.deposit') }}
</v-btn>
<v-btn
v-else
@ -182,8 +182,8 @@
<div class="input-group">
<div class="amount-header">
<div>
<span class="label amount-label">Amount</span>
<span class="balance-label">Balance ${{ balance.toFixed(2) }}</span>
<span class="label amount-label">{{ t('trade.amount') }}</span>
<span class="balance-label">{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span>
</div>
<div class="amount-value">${{ amount.toFixed(2) }}</div>
</div>
@ -193,27 +193,27 @@
<v-btn class="amount-btn" @click="adjustAmount(1)">+$1</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(20)">+$20</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">{{ t('trade.max') }}</v-btn>
</div>
<!-- To win份数 × 1U -->
<div v-if="amount > 0" class="total-row amount-to-win-row">
<span class="label">To win</span>
<span class="label">{{ t('trade.toWin') }}</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ toWinValue }}
</span>
</div>
</div>
<!-- Buy 余额不足时显示 Deposit -->
<v-btn class="deposit-btn" @click="deposit">Deposit</v-btn>
<!-- Buy 余额不足时显示 {{ t('trade.deposit') }} -->
<v-btn class="deposit-btn" @click="deposit">{{ t('trade.deposit') }}</v-btn>
</template>
<!-- Sell: Shares + To receive + Avg. Price只显示 Sell Yes/No Deposit -->
<!-- Sell: Shares + To receive + Avg. Price只显示 Sell Yes/No {{ t('trade.deposit') }} -->
<template v-else>
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<span class="label">{{ t('trade.shares') }}</span>
<div class="shares-input">
<v-text-field
:model-value="shares"
@ -231,12 +231,12 @@
<div class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">{{ t('trade.max') }}</v-btn>
</div>
</div>
<div class="total-section">
<div class="total-row">
<span class="label">You'll receive</span>
<span class="label">{{ t('trade.youllReceive') }}</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ totalPrice }}
@ -244,7 +244,7 @@
</div>
<div class="total-row avg-price-row">
<span class="label">
Avg. Price {{ avgPriceCents }}¢
{{ t('trade.avgPrice') }} {{ avgPriceCents }}¢
<v-icon size="14" class="info-icon">mdi-information-outline</v-icon>
</span>
</div>
@ -287,7 +287,7 @@
<!-- Limit Price -->
<div class="input-group limit-price-group">
<div class="limit-price-header">
<span class="label">Limit Price</span>
<span class="label">{{ t('trade.limitPrice') }}</span>
<div class="price-input">
<v-btn class="adjust-btn" icon @click="decreasePrice">
<v-icon>mdi-minus</v-icon>
@ -316,7 +316,7 @@
<!-- Shares -->
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<span class="label">{{ t('trade.shares') }}</span>
<div class="shares-input">
<v-text-field
:model-value="shares"
@ -343,7 +343,7 @@
<div v-else class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">{{ t('trade.max') }}</v-btn>
</div>
<div v-if="activeTab === 'buy'" class="matching-info">
<v-icon size="14">mdi-information</v-icon>
@ -354,7 +354,7 @@
<!-- Set Expiration -->
<div class="input-group expiration-group">
<div class="expiration-header">
<span class="label">Set Expiration</span>
<span class="label">{{ t('trade.setExpiration') }}</span>
<v-switch v-model="expirationEnabled" class="expiration-switch" hide-details></v-switch>
</div>
<!-- Expiration Time Dropdown -->
@ -362,6 +362,8 @@
v-if="expirationEnabled"
v-model="expirationTime"
:items="expirationOptions"
item-title="title"
item-value="value"
class="expiration-select"
hide-details
density="compact"
@ -373,11 +375,11 @@
<!-- Buy模式 -->
<template v-if="activeTab === 'buy'">
<div class="total-row">
<span class="label">Total</span>
<span class="label">{{ t('trade.total') }}</span>
<span class="total-value">${{ totalPrice }}</span>
</div>
<div class="total-row">
<span class="label">To win</span>
<span class="label">{{ t('trade.toWin') }}</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ toWinValue }}
@ -387,7 +389,7 @@
<!-- Sell模式 -->
<template v-else>
<div class="total-row">
<span class="label">You'll receive</span>
<span class="label">{{ t('trade.youllReceive') }}</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ totalPrice }}
@ -414,29 +416,29 @@
<div class="trade-component trade-sheet-inner">
<div class="header">
<v-tabs v-model="activeTab" class="buy-sell-tabs minimal-tabs" density="compact">
<v-tab value="buy" class="minimal-tab">Buy</v-tab>
<v-tab value="sell" class="minimal-tab">Sell</v-tab>
<v-tab value="buy" class="minimal-tab">{{ t('trade.buy') }}</v-tab>
<v-tab value="sell" class="minimal-tab">{{ t('trade.sell') }}</v-tab>
</v-tabs>
<v-menu class="limit-dropdown hide-in-mobile-sheet">
<template v-slot:activator="{ props: limitProps, isActive }">
<v-btn v-bind="limitProps" class="limit-btn" text end>
{{ limitType }}
{{ limitTypeDisplay }}
<v-icon right>{{ isActive ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item @click="limitType = 'Market'"
><v-list-item-title>Market</v-list-item-title></v-list-item
><v-list-item-title>{{ t('trade.market') }}</v-list-item-title></v-list-item
>
<v-list-item @click="limitType = 'Limit'"
><v-list-item-title>Limit</v-list-item-title></v-list-item
><v-list-item-title>{{ t('trade.limit') }}</v-list-item-title></v-list-item
>
<v-divider></v-divider>
<v-list-item @click="openMergeDialog"
><v-list-item-title>Merge</v-list-item-title></v-list-item
><v-list-item-title>{{ t('trade.merge') }}</v-list-item-title></v-list-item
>
<v-list-item @click="openSplitDialog"
><v-list-item-title>Split</v-list-item-title></v-list-item
><v-list-item-title>{{ t('trade.split') }}</v-list-item-title></v-list-item
>
</v-list>
</v-menu>
@ -464,8 +466,8 @@
<div class="input-group">
<div class="amount-header">
<div>
<span class="label amount-label">Amount</span>
<span class="balance-label">Balance ${{ balance.toFixed(2) }}</span>
<span class="label amount-label">{{ t('trade.amount') }}</span>
<span class="balance-label">{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span>
</div>
<div class="amount-value">${{ amount.toFixed(2) }}</div>
</div>
@ -473,7 +475,7 @@
<v-btn class="amount-btn" @click="adjustAmount(1)">+$1</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(20)">+$20</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">{{ t('trade.max') }}</v-btn>
</div>
</div>
</template>
@ -481,7 +483,7 @@
<template v-if="activeTab === 'sell'">
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<span class="label">{{ t('trade.shares') }}</span>
<div class="shares-input">
<v-text-field
:model-value="shares"
@ -499,17 +501,17 @@
<div class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">{{ t('trade.max') }}</v-btn>
</div>
</div>
</template>
<div class="total-section">
<template v-if="activeTab === 'buy'">
<div class="total-row">
<span class="label">Total</span><span class="total-value">${{ totalPrice }}</span>
<span class="label">{{ t('trade.total') }}</span><span class="total-value">${{ totalPrice }}</span>
</div>
<div class="total-row">
<span class="label">To win</span
<span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span
>
@ -517,7 +519,7 @@
</template>
<template v-else>
<div class="total-row">
<span class="label">You'll receive</span
<span class="label">{{ t('trade.youllReceive') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{
totalPrice
@ -526,7 +528,7 @@
</div>
<div class="total-row avg-price-row">
<span class="label">
Avg. Price {{ avgPriceCents }}¢
{{ t('trade.avgPrice') }} {{ avgPriceCents }}¢
<v-icon size="14" class="info-icon">mdi-information-outline</v-icon>
</span>
</div>
@ -538,7 +540,7 @@
class="deposit-btn"
@click="deposit"
>
Deposit
{{ t('trade.deposit') }}
</v-btn>
<v-btn
v-else
@ -571,8 +573,8 @@
<div class="input-group">
<div class="amount-header">
<div>
<span class="label amount-label">Amount</span
><span class="balance-label">Balance ${{ balance.toFixed(2) }}</span>
<span class="label amount-label">{{ t('trade.amount') }}</span
><span class="balance-label">{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span>
</div>
<div class="amount-value">${{ amount.toFixed(2) }}</div>
</div>
@ -580,23 +582,23 @@
<v-btn class="amount-btn" @click="adjustAmount(1)">+$1</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(20)">+$20</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">{{ t('trade.max') }}</v-btn>
</div>
<div v-if="amount > 0" class="total-row amount-to-win-row">
<span class="label">To win</span>
<span class="label">{{ t('trade.toWin') }}</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ toWinValue }}
</span>
</div>
</div>
<v-btn class="deposit-btn" @click="deposit">Deposit</v-btn>
<v-btn class="deposit-btn" @click="deposit">{{ t('trade.deposit') }}</v-btn>
</template>
<!-- Sell: Shares + To receive + Avg. Price -->
<template v-else>
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<span class="label">{{ t('trade.shares') }}</span>
<div class="shares-input">
<v-text-field
:model-value="shares"
@ -614,12 +616,12 @@
<div class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">{{ t('trade.max') }}</v-btn>
</div>
</div>
<div class="total-section">
<div class="total-row">
<span class="label">You'll receive</span>
<span class="label">{{ t('trade.youllReceive') }}</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ totalPrice }}
@ -627,7 +629,7 @@
</div>
<div class="total-row avg-price-row">
<span class="label">
Avg. Price {{ avgPriceCents }}¢
{{ t('trade.avgPrice') }} {{ avgPriceCents }}¢
<v-icon size="14" class="info-icon">mdi-information-outline</v-icon>
</span>
</div>
@ -662,7 +664,7 @@
</div>
<div class="input-group limit-price-group">
<div class="limit-price-header">
<span class="label">Limit Price</span>
<span class="label">{{ t('trade.limitPrice') }}</span>
<div class="price-input">
<v-btn class="adjust-btn" icon @click="decreasePrice"
><v-icon>mdi-minus</v-icon></v-btn
@ -689,7 +691,7 @@
</div>
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<span class="label">{{ t('trade.shares') }}</span>
<div class="shares-input">
<v-text-field
:model-value="shares"
@ -713,12 +715,12 @@
<div v-else class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">{{ t('trade.max') }}</v-btn>
</div>
</div>
<div class="input-group expiration-group">
<div class="expiration-header">
<span class="label">Set expiration</span>
<span class="label">{{ t('trade.setExpiration') }}</span>
<v-switch
v-model="expirationEnabled"
class="expiration-switch"
@ -730,6 +732,8 @@
v-if="expirationEnabled"
v-model="expirationTime"
:items="expirationOptions"
item-title="title"
item-value="value"
class="expiration-select"
hide-details
density="compact"
@ -738,10 +742,10 @@
<div class="total-section">
<template v-if="activeTab === 'buy'">
<div class="total-row">
<span class="label">Total</span><span class="total-value">${{ totalPrice }}</span>
<span class="label">{{ t('trade.total') }}</span><span class="total-value">${{ totalPrice }}</span>
</div>
<div class="total-row">
<span class="label">To win</span
<span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span
>
@ -749,7 +753,7 @@
</template>
<template v-else>
<div class="total-row">
<span class="label">You'll receive</span
<span class="label">{{ t('trade.youllReceive') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ totalPrice }}</span
>
@ -781,7 +785,7 @@
block
@click="openSheet('yes')"
>
Buy {{ yesLabel }} {{ yesPriceCents }}¢
{{ t('trade.buyLabel', { label: yesLabel }) }} {{ yesPriceCents }}¢
</v-btn>
<v-btn
class="mobile-bar-btn mobile-bar-no"
@ -791,7 +795,7 @@
block
@click="openSheet('no')"
>
Buy {{ noLabel }} {{ noPriceCents }}¢
{{ t('trade.buyLabel', { label: noLabel }) }} {{ noPriceCents }}¢
</v-btn>
</div>
@ -800,8 +804,8 @@
<div class="trade-component trade-sheet-inner">
<div class="header">
<v-tabs v-model="activeTab" class="buy-sell-tabs minimal-tabs" density="compact">
<v-tab value="buy" class="minimal-tab">Buy</v-tab>
<v-tab value="sell" class="minimal-tab">Sell</v-tab>
<v-tab value="buy" class="minimal-tab">{{ t('trade.buy') }}</v-tab>
<v-tab value="sell" class="minimal-tab">{{ t('trade.sell') }}</v-tab>
</v-tabs>
<v-menu class="limit-dropdown hide-in-mobile-sheet">
<template v-slot:activator="{ props: limitProps, isActive }">
@ -819,10 +823,10 @@
>
<v-divider></v-divider>
<v-list-item @click="openMergeDialog"
><v-list-item-title>Merge</v-list-item-title></v-list-item
><v-list-item-title>{{ t('trade.merge') }}</v-list-item-title></v-list-item
>
<v-list-item @click="openSplitDialog"
><v-list-item-title>Split</v-list-item-title></v-list-item
><v-list-item-title>{{ t('trade.split') }}</v-list-item-title></v-list-item
>
</v-list>
</v-menu>
@ -850,8 +854,8 @@
<div class="input-group">
<div class="amount-header">
<div>
<span class="label amount-label">Amount</span>
<span class="balance-label">Balance ${{ balance.toFixed(2) }}</span>
<span class="label amount-label">{{ t('trade.amount') }}</span>
<span class="balance-label">{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span>
</div>
<div class="amount-value">${{ amount.toFixed(2) }}</div>
</div>
@ -859,7 +863,7 @@
<v-btn class="amount-btn" @click="adjustAmount(1)">+$1</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(20)">+$20</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">{{ t('trade.max') }}</v-btn>
</div>
</div>
</template>
@ -867,7 +871,7 @@
<template v-if="activeTab === 'sell'">
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<span class="label">{{ t('trade.shares') }}</span>
<div class="shares-input">
<v-text-field
:model-value="shares"
@ -885,7 +889,7 @@
<div class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">{{ t('trade.max') }}</v-btn>
</div>
</div>
</template>
@ -896,7 +900,7 @@
><span class="total-value">${{ totalPrice }}</span>
</div>
<div class="total-row">
<span class="label">To win</span
<span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span
>
@ -904,7 +908,7 @@
</template>
<template v-else>
<div class="total-row">
<span class="label">You'll receive</span
<span class="label">{{ t('trade.youllReceive') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{
totalPrice
@ -913,7 +917,7 @@
</div>
<div class="total-row avg-price-row">
<span class="label">
Avg. Price {{ avgPriceCents }}¢
{{ t('trade.avgPrice') }} {{ avgPriceCents }}¢
<v-icon size="14" class="info-icon">mdi-information-outline</v-icon>
</span>
</div>
@ -925,7 +929,7 @@
class="deposit-btn"
@click="deposit"
>
Deposit
{{ t('trade.deposit') }}
</v-btn>
<v-btn
v-else
@ -958,8 +962,8 @@
<div class="input-group">
<div class="amount-header">
<div>
<span class="label amount-label">Amount</span
><span class="balance-label">Balance ${{ balance.toFixed(2) }}</span>
<span class="label amount-label">{{ t('trade.amount') }}</span
><span class="balance-label">{{ t('trade.balanceLabel') }} ${{ balance.toFixed(2) }}</span>
</div>
<div class="amount-value">${{ amount.toFixed(2) }}</div>
</div>
@ -967,23 +971,23 @@
<v-btn class="amount-btn" @click="adjustAmount(1)">+$1</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(20)">+$20</v-btn>
<v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn>
<v-btn class="amount-btn" @click="setMaxAmount">{{ t('trade.max') }}</v-btn>
</div>
<div v-if="amount > 0" class="total-row amount-to-win-row">
<span class="label">To win</span>
<span class="label">{{ t('trade.toWin') }}</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ toWinValue }}
</span>
</div>
</div>
<v-btn class="deposit-btn" @click="deposit">Deposit</v-btn>
<v-btn class="deposit-btn" @click="deposit">{{ t('trade.deposit') }}</v-btn>
</template>
<!-- Sell: Shares + To receive + Avg. Price -->
<template v-else>
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<span class="label">{{ t('trade.shares') }}</span>
<div class="shares-input">
<v-text-field
:model-value="shares"
@ -1001,12 +1005,12 @@
<div class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">{{ t('trade.max') }}</v-btn>
</div>
</div>
<div class="total-section">
<div class="total-row">
<span class="label">You'll receive</span>
<span class="label">{{ t('trade.youllReceive') }}</span>
<span class="to-win-value">
<v-icon size="16" color="green">mdi-currency-usd</v-icon>
{{ totalPrice }}
@ -1014,7 +1018,7 @@
</div>
<div class="total-row avg-price-row">
<span class="label">
Avg. Price {{ avgPriceCents }}¢
{{ t('trade.avgPrice') }} {{ avgPriceCents }}¢
<v-icon size="14" class="info-icon">mdi-information-outline</v-icon>
</span>
</div>
@ -1049,7 +1053,7 @@
</div>
<div class="input-group limit-price-group">
<div class="limit-price-header">
<span class="label">Limit Price</span>
<span class="label">{{ t('trade.limitPrice') }}</span>
<div class="price-input">
<v-btn class="adjust-btn" icon @click="decreasePrice"
><v-icon>mdi-minus</v-icon></v-btn
@ -1075,7 +1079,7 @@
</div>
<div class="input-group shares-group">
<div class="shares-header">
<span class="label">Shares</span>
<span class="label">{{ t('trade.shares') }}</span>
<div class="shares-input">
<v-text-field
:model-value="shares"
@ -1099,12 +1103,12 @@
<div v-else class="shares-buttons">
<v-btn class="share-btn" @click="setSharesPercentage(25)">25%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(50)">50%</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">Max</v-btn>
<v-btn class="share-btn" @click="setSharesPercentage(100)">{{ t('trade.max') }}</v-btn>
</div>
</div>
<div class="input-group expiration-group">
<div class="expiration-header">
<span class="label">Set expiration</span>
<span class="label">{{ t('trade.setExpiration') }}</span>
<v-switch
v-model="expirationEnabled"
class="expiration-switch"
@ -1116,6 +1120,8 @@
v-if="expirationEnabled"
v-model="expirationTime"
:items="expirationOptions"
item-title="title"
item-value="value"
class="expiration-select"
hide-details
density="compact"
@ -1124,10 +1130,10 @@
<div class="total-section">
<template v-if="activeTab === 'buy'">
<div class="total-row">
<span class="label">Total</span><span class="total-value">${{ totalPrice }}</span>
<span class="label">{{ t('trade.total') }}</span><span class="total-value">${{ totalPrice }}</span>
</div>
<div class="total-row">
<span class="label">To win</span
<span class="label">{{ t('trade.toWin') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{ toWinValue }}</span
>
@ -1135,7 +1141,7 @@
</template>
<template v-else>
<div class="total-row">
<span class="label">You'll receive</span
<span class="label">{{ t('trade.youllReceive') }}</span
><span class="to-win-value"
><v-icon size="16" color="green">mdi-currency-usd</v-icon> {{
totalPrice
@ -1284,12 +1290,14 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
import { useUserStore } from '../stores/user'
import { pmMarketMerge, pmMarketSplit, pmOrderPlace } from '../api/market'
import { OrderType, Side } from '../api/constants'
const { mobile } = useDisplay()
const { t } = useI18n()
const userStore = useUserStore()
/** 限价单允许的 135 个价格档位01 区间规则19/1090/1009900/99109990/99919999 */
@ -1443,7 +1451,10 @@ const selectedOption = ref<'yes' | 'no'>(props.initialOption ?? 'no')
const limitPrice = ref(0.82) //
const shares = ref(20) //
const expirationTime = ref('5m') //
const expirationOptions = ref(['5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h', '1d', '2d', '3d']) //
const EXPIRATION_VALUES = ['5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h', '1d', '2d', '3d'] as const
const expirationOptions = computed(() =>
EXPIRATION_VALUES.map((v) => ({ title: t(`trade.expiration.${v}`), value: v })),
)
// Market mode state
const isMarketMode = computed(() => limitType.value === 'Market')
@ -1461,6 +1472,7 @@ const orderError = ref('')
// Emits
const emit = defineEmits<{
optionChange: [option: 'yes' | 'no']
orderSuccess: []
submit: [
payload: {
side: 'buy' | 'sell'
@ -1502,11 +1514,13 @@ const toWinValue = computed(() => {
return (shares.value * 1).toFixed(2)
})
const limitTypeDisplay = computed(() =>
limitType.value === 'Market' ? t('trade.market') : t('trade.limit'),
)
const actionButtonText = computed(() => {
const label = selectedOption.value === 'yes' ? yesLabel.value : noLabel.value
const tab = activeTab.value
const tabCapitalized = tab.charAt(0).toUpperCase() + tab.slice(1)
return `${tabCapitalized} ${label}`
return tab === 'buy' ? t('trade.buyLabel', { label }) : t('trade.sellLabel', { label })
})
function applyInitialOption(option: 'yes' | 'no') {
@ -1668,9 +1682,9 @@ const canAffordBuy = computed(() => {
return bal >= cost
})
/** Buy 模式且余额不足时显示 Deposit否则显示 Buy Yes/No */
const showDepositForBuy = computed(() => !canAffordBuy.value)
/** Buy 模式且余额不足时显示 Deposit否则显示 Buy Yes/No */
const deposit = () => {
console.log('Depositing amount:', amount.value)
// API
@ -1710,12 +1724,12 @@ async function submitOrder() {
emit('submit', payload)
if (!tokenId) {
orderError.value = '请先选择市场(需包含 clobTokenIds'
orderError.value = t('trade.pleaseSelectMarket')
return
}
const headers = userStore.getAuthHeaders()
if (!headers) {
orderError.value = '请先登录'
orderError.value = t('trade.pleaseLogin')
return
}
const uid = userStore?.user?.ID ?? 0
@ -1728,7 +1742,7 @@ async function submitOrder() {
: 0
if (!Number.isFinite(userIdNum) || userIdNum <= 0) {
console.warn('[submitOrder] 用户信息异常: user=', userStore.user, 'uid=', uid)
orderError.value = '用户信息异常'
orderError.value = t('trade.userError')
return
}
@ -1764,11 +1778,12 @@ async function submitOrder() {
)
if (res.code === 0 || res.code === 200) {
userStore.fetchUsdcBalance()
emit('orderSuccess')
} else {
orderError.value = res.msg || '下单失败'
orderError.value = res.msg || t('trade.orderFailed')
}
} catch (e) {
orderError.value = e instanceof Error ? e.message : 'Request failed'
orderError.value = e instanceof Error ? e.message : t('error.requestFailed')
} finally {
orderLoading.value = false
}

View File

@ -9,9 +9,9 @@
>
<v-card rounded="lg">
<div class="withdraw-header">
<h2 class="withdraw-title">Withdraw</h2>
<p class="withdraw-balance">Polymarket Balance: ${{ balance }}</p>
<v-btn icon variant="text" class="close-btn" aria-label="关闭" @click="close">
<h2 class="withdraw-title">{{ t('withdraw.title') }}</h2>
<p class="withdraw-balance">{{ t('withdraw.polymarketBalance') }} ${{ balance }}</p>
<v-btn icon variant="text" class="close-btn" :aria-label="t('withdraw.close')" @click="close">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
@ -21,16 +21,16 @@
type="number"
min="0"
step="0.01"
label="Amount (USD)"
:label="t('withdraw.amountUsd')"
variant="outlined"
density="comfortable"
hide-details
class="amount-field"
placeholder="0.00"
:placeholder="t('withdraw.amountPlaceholder')"
@keypress="allowDecimal"
>
<template #append-inner>
<v-btn variant="text" size="small" class="max-btn" @click="setMax">Max</v-btn>
<v-btn variant="text" size="small" class="max-btn" @click="setMax">{{ t('withdraw.max') }}</v-btn>
</template>
</v-text-field>
@ -39,7 +39,7 @@
:items="networks"
item-title="label"
item-value="id"
label="Network"
:label="t('withdraw.network')"
variant="outlined"
density="comfortable"
hide-details
@ -47,10 +47,10 @@
/>
<div class="destination-section">
<label class="section-label">Withdraw to</label>
<label class="section-label">{{ t('withdraw.withdrawTo') }}</label>
<v-radio-group v-model="destinationType" hide-details class="destination-radio">
<v-radio value="wallet" label="Connected wallet" />
<v-radio value="address" label="Custom address" />
<v-radio value="wallet" :label="t('withdraw.connectedWallet')" />
<v-radio value="address" :label="t('withdraw.customAddress')" />
</v-radio-group>
<template v-if="destinationType === 'wallet'">
<v-btn
@ -63,7 +63,7 @@
@click="connectWallet"
>
<v-icon start>mdi-wallet</v-icon>
Connect Wallet
{{ t('withdraw.connectWallet') }}
</v-btn>
<div v-else class="connected-address">
<v-icon color="success" size="18">mdi-check-circle</v-icon>
@ -73,11 +73,11 @@
<v-text-field
v-else
v-model="customAddress"
label="Wallet address"
:label="t('withdraw.walletAddress')"
variant="outlined"
density="comfortable"
hide-details
placeholder="0x..."
:placeholder="t('withdraw.addressPlaceholder')"
class="address-field"
/>
</div>
@ -95,7 +95,7 @@
:disabled="!canSubmit"
@click="submitWithdraw"
>
Withdraw
{{ t('withdraw.title') }}
</v-btn>
</v-card-text>
</v-card>
@ -104,7 +104,9 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = withDefaults(
defineProps<{
modelValue: boolean
@ -134,8 +136,8 @@ const amountNum = computed(() => parseFloat(amount.value) || 0)
const amountError = computed(() => {
if (!amount.value) return ''
if (amountNum.value <= 0) return 'Amount must be greater than 0'
if (amountNum.value > balanceNum.value) return 'Insufficient balance'
if (amountNum.value <= 0) return t('withdraw.amountMustBePositive')
if (amountNum.value > balanceNum.value) return t('withdraw.insufficientBalance')
return ''
})
@ -173,7 +175,7 @@ function allowDecimal(e: KeyboardEvent) {
async function connectWallet() {
if (!window.ethereum) {
alert('Please install MetaMask or another Web3 wallet.')
alert(t('deposit.installMetaMask'))
return
}
connecting.value = true

172
src/locales/en.json Normal file
View File

@ -0,0 +1,172 @@
{
"common": {
"login": "Login",
"logout": "Log out",
"balance": "Balance",
"back": "Back",
"search": "Search",
"filter": "Filter",
"collapse": "Collapse",
"delete": "Delete",
"clear": "Clear",
"loading": "Loading...",
"more": "More",
"user": "User",
"chance": "chance"
},
"toast": {
"orderSuccess": "Order placed successfully"
},
"trade": {
"buy": "Buy",
"sell": "Sell",
"orderBook": "Order Book",
"buyLabel": "Buy {label}",
"sellLabel": "Sell {label}",
"merge": "Merge",
"split": "Split",
"market": "Market",
"limit": "Limit",
"deposit": "Deposit",
"amount": "Amount",
"shares": "Shares",
"limitPrice": "Limit Price",
"setExpiration": "Set expiration",
"total": "Total",
"toWin": "To win",
"youllReceive": "You'll receive",
"avgPrice": "Avg. Price",
"max": "Max",
"balanceLabel": "Balance",
"pleaseLogin": "Please log in first",
"pleaseSelectMarket": "Please select a market (with clobTokenIds)",
"userError": "User info error",
"orderFailed": "Order failed",
"expiration": {
"5m": "5m",
"15m": "15m",
"30m": "30m",
"1h": "1h",
"2h": "2h",
"4h": "4h",
"8h": "8h",
"12h": "12h",
"1d": "1d",
"2d": "2d",
"3d": "3d"
}
},
"home": {
"searchHistory": "Search history",
"searchPlaceholder": "Search",
"loadMore": "Load more"
},
"error": {
"requestFailed": "Request failed",
"loadFailed": "Load failed",
"invalidId": "Invalid ID or slug"
},
"activity": {
"comments": "Comments",
"topHolders": "Top Holders",
"activity": "Activity",
"noCommentsYet": "No comments yet.",
"topHoldersPlaceholder": "Top holders will appear here.",
"minAmount": "Min amount",
"any": "Any",
"live": "Live",
"bought": "bought",
"sold": "sold",
"at": "at",
"viewTransaction": "View transaction",
"justNow": "just now",
"minutesAgo": "{n}m ago",
"hoursAgo": "{n}h ago",
"daysAgo": "{n}d ago",
"weeksAgo": "{n}w ago"
},
"wallet": {
"portfolio": "Portfolio",
"today": "Today",
"deposit": "Deposit",
"withdraw": "Withdraw",
"profitLoss": "Profit/Loss",
"allTime": "All-Time",
"pl1D": "1D",
"pl1W": "1W",
"pl1M": "1M",
"plAll": "ALL",
"positions": "Positions",
"openOrders": "Open orders",
"history": "History",
"searchPlaceholder": "Search",
"currentValue": "Current value",
"closeLosses": "Close Losses",
"all": "All",
"newest": "Newest",
"export": "Export",
"cancelAll": "Cancel all",
"noPositionsFound": "No positions found.",
"noOpenOrdersFound": "No open orders found.",
"noHistoryFound": "You haven't traded any polymarkets yet",
"market": "Market",
"avgNow": "AVG → NOW",
"bet": "BET",
"toWin": "TO WIN",
"value": "VALUE",
"action": "ACTION",
"side": "SIDE",
"outcome": "OUTCOME",
"price": "PRICE",
"filled": "FILLED",
"total": "TOTAL",
"expiration": "EXPIRATION",
"activity": "ACTIVITY",
"view": "View",
"expirationLabel": "Expiration:"
},
"deposit": {
"title": "Deposit",
"polymarketBalance": "Polymarket Balance:",
"transferCrypto": "Transfer Crypto",
"connectExchange": "Connect Exchange",
"noLimit": "No limit",
"instant": "Instant",
"twoMin": "2 min",
"network": "Network",
"supportedTip": "Supported: USDC, ETH",
"depositAddress": "Deposit address",
"copy": "Copy",
"copied": "Copied",
"connectDesc": "Connect your wallet to deposit. Send USDC or ETH to your deposit address after connecting.",
"connectedTip": "Connected. Send USDC or ETH to the address below.",
"metaMask": "MetaMask",
"coinbaseComingSoon": "Coinbase Wallet (Coming soon)",
"walletConnectComingSoon": "WalletConnect (Coming soon)",
"close": "Close",
"installMetaMask": "Please install MetaMask or another Web3 wallet."
},
"withdraw": {
"title": "Withdraw",
"polymarketBalance": "Polymarket Balance:",
"amountUsd": "Amount (USD)",
"max": "Max",
"network": "Network",
"withdrawTo": "Withdraw to",
"connectedWallet": "Connected wallet",
"customAddress": "Custom address",
"connectWallet": "Connect Wallet",
"walletAddress": "Wallet address",
"amountPlaceholder": "0.00",
"addressPlaceholder": "0x...",
"amountMustBePositive": "Amount must be greater than 0",
"insufficientBalance": "Insufficient balance",
"close": "Close"
},
"locale": {
"zh": "简体中文",
"en": "English",
"ja": "日本語",
"ko": "한국어"
}
}

172
src/locales/ja.json Normal file
View File

@ -0,0 +1,172 @@
{
"common": {
"login": "ログイン",
"logout": "ログアウト",
"balance": "残高",
"back": "戻る",
"search": "検索",
"filter": "フィルター",
"collapse": "閉じる",
"delete": "削除",
"clear": "クリア",
"loading": "読み込み中...",
"more": "その他",
"user": "ユーザー",
"chance": "確率"
},
"toast": {
"orderSuccess": "注文が完了しました"
},
"trade": {
"buy": "買う",
"sell": "売る",
"orderBook": "オーダーブック",
"buyLabel": "{label}を買う",
"sellLabel": "{label}を売る",
"merge": "マージ",
"split": "スプリット",
"market": "成行",
"limit": "指値",
"deposit": "入金",
"amount": "金額",
"shares": "シェア",
"limitPrice": "指値",
"setExpiration": "有効期限を設定",
"total": "合計",
"toWin": "獲得見込み",
"youllReceive": "受け取り額",
"avgPrice": "平均価格",
"max": "最大",
"balanceLabel": "残高",
"pleaseLogin": "先にログインしてください",
"pleaseSelectMarket": "市場を選択してくださいclobTokenIds が必要)",
"userError": "ユーザー情報エラー",
"orderFailed": "注文に失敗しました",
"expiration": {
"5m": "5分",
"15m": "15分",
"30m": "30分",
"1h": "1時間",
"2h": "2時間",
"4h": "4時間",
"8h": "8時間",
"12h": "12時間",
"1d": "1日",
"2d": "2日",
"3d": "3日"
}
},
"home": {
"searchHistory": "検索履歴",
"searchPlaceholder": "検索",
"loadMore": "もっと読み込む"
},
"error": {
"requestFailed": "リクエストに失敗しました",
"loadFailed": "読み込みに失敗しました",
"invalidId": "無効な ID または slug"
},
"activity": {
"comments": "コメント",
"topHolders": "持倉トップ",
"activity": "アクティビティ",
"noCommentsYet": "コメントはまだありません",
"topHoldersPlaceholder": "持倉トップがここに表示されます",
"minAmount": "最小金額",
"any": "任意",
"live": "ライブ",
"bought": "購入",
"sold": "売却",
"at": "で",
"viewTransaction": "取引を表示",
"justNow": "たった今",
"minutesAgo": "{n}分前",
"hoursAgo": "{n}時間前",
"daysAgo": "{n}日前",
"weeksAgo": "{n}週間前"
},
"wallet": {
"portfolio": "ポートフォリオ",
"today": "今日",
"deposit": "入金",
"withdraw": "出金",
"profitLoss": "損益",
"allTime": "全期間",
"pl1D": "1日",
"pl1W": "1週",
"pl1M": "1月",
"plAll": "全て",
"positions": "ポジション",
"openOrders": "未約定",
"history": "履歴",
"searchPlaceholder": "検索",
"currentValue": "現在価値",
"closeLosses": "損失決済",
"all": "全て",
"newest": "新しい順",
"export": "エクスポート",
"cancelAll": "一括キャンセル",
"noPositionsFound": "ポジションがありません",
"noOpenOrdersFound": "未約定注文がありません",
"noHistoryFound": "まだ取引履歴がありません",
"market": "市場",
"avgNow": "平均→現在",
"bet": "ベット",
"toWin": "獲得",
"value": "価値",
"action": "操作",
"side": "方向",
"outcome": "結果",
"price": "価格",
"filled": "約定",
"total": "合計",
"expiration": "有効期限",
"activity": "アクティビティ",
"view": "表示",
"expirationLabel": "有効期限:"
},
"deposit": {
"title": "入金",
"polymarketBalance": "Polymarket 残高:",
"transferCrypto": "暗号資産を送金",
"connectExchange": "取引所を接続",
"noLimit": "制限なし",
"instant": "即時",
"twoMin": "約2分",
"network": "ネットワーク",
"supportedTip": "対応USDC、ETH",
"depositAddress": "入金アドレス",
"copy": "コピー",
"copied": "コピーしました",
"connectDesc": "入金するにはウォレットを接続してください。接続後、USDC または ETH を入金アドレスに送金してください。",
"connectedTip": "接続済み。以下のアドレスに USDC または ETH を送金してください。",
"metaMask": "MetaMask",
"coinbaseComingSoon": "Coinbase Wallet近日公開",
"walletConnectComingSoon": "WalletConnect近日公開",
"close": "閉じる",
"installMetaMask": "MetaMask または他の Web3 ウォレットをインストールしてください。"
},
"withdraw": {
"title": "出金",
"polymarketBalance": "Polymarket 残高:",
"amountUsd": "金額 (USD)",
"max": "最大",
"network": "ネットワーク",
"withdrawTo": "出金先",
"connectedWallet": "接続済みウォレット",
"customAddress": "カスタムアドレス",
"connectWallet": "ウォレットを接続",
"walletAddress": "ウォレットアドレス",
"amountPlaceholder": "0.00",
"addressPlaceholder": "0x...",
"amountMustBePositive": "金額は 0 より大きい必要があります",
"insufficientBalance": "残高不足",
"close": "閉じる"
},
"locale": {
"zh": "简体中文",
"en": "English",
"ja": "日本語",
"ko": "한국어"
}
}

172
src/locales/ko.json Normal file
View File

@ -0,0 +1,172 @@
{
"common": {
"login": "로그인",
"logout": "로그아웃",
"balance": "잔액",
"back": "뒤로",
"search": "검색",
"filter": "필터",
"collapse": "접기",
"delete": "삭제",
"clear": "지우기",
"loading": "로딩 중...",
"more": "더보기",
"user": "사용자",
"chance": "확률"
},
"toast": {
"orderSuccess": "주문이 완료되었습니다"
},
"trade": {
"buy": "매수",
"sell": "매도",
"orderBook": "호가창",
"buyLabel": "{label} 매수",
"sellLabel": "{label} 매도",
"merge": "병합",
"split": "분할",
"market": "시장가",
"limit": "지정가",
"deposit": "입금",
"amount": "금액",
"shares": "주식",
"limitPrice": "지정가",
"setExpiration": "만료 설정",
"total": "합계",
"toWin": "획득 예상",
"youllReceive": "수령 예정",
"avgPrice": "평균 가격",
"max": "최대",
"balanceLabel": "잔액",
"pleaseLogin": "먼저 로그인하세요",
"pleaseSelectMarket": "시장을 선택하세요 (clobTokenIds 필요)",
"userError": "사용자 정보 오류",
"orderFailed": "주문 실패",
"expiration": {
"5m": "5분",
"15m": "15분",
"30m": "30분",
"1h": "1시간",
"2h": "2시간",
"4h": "4시간",
"8h": "8시간",
"12h": "12시간",
"1d": "1일",
"2d": "2일",
"3d": "3일"
}
},
"home": {
"searchHistory": "검색 기록",
"searchPlaceholder": "검색",
"loadMore": "더 불러오기"
},
"error": {
"requestFailed": "요청 실패",
"loadFailed": "로드 실패",
"invalidId": "잘못된 ID 또는 slug"
},
"activity": {
"comments": "댓글",
"topHolders": "보유자 순위",
"activity": "활동",
"noCommentsYet": "아직 댓글이 없습니다",
"topHoldersPlaceholder": "보유자 순위가 여기에 표시됩니다",
"minAmount": "최소 금액",
"any": "모두",
"live": "실시간",
"bought": "매수",
"sold": "매도",
"at": "에",
"viewTransaction": "거래 보기",
"justNow": "방금",
"minutesAgo": "{n}분 전",
"hoursAgo": "{n}시간 전",
"daysAgo": "{n}일 전",
"weeksAgo": "{n}주 전"
},
"wallet": {
"portfolio": "포트폴리오",
"today": "오늘",
"deposit": "입금",
"withdraw": "출금",
"profitLoss": "손익",
"allTime": "전체",
"pl1D": "1일",
"pl1W": "1주",
"pl1M": "1월",
"plAll": "전체",
"positions": "포지션",
"openOrders": "미체결",
"history": "내역",
"searchPlaceholder": "검색",
"currentValue": "현재 가치",
"closeLosses": "손실 청산",
"all": "전체",
"newest": "최신순",
"export": "내보내기",
"cancelAll": "전체 취소",
"noPositionsFound": "포지션이 없습니다",
"noOpenOrdersFound": "미체결 주문이 없습니다",
"noHistoryFound": "아직 거래 내역이 없습니다",
"market": "시장",
"avgNow": "평균→현재",
"bet": "베팅",
"toWin": "획득",
"value": "가치",
"action": "작업",
"side": "방향",
"outcome": "결과",
"price": "가격",
"filled": "체결",
"total": "합계",
"expiration": "만료",
"activity": "활동",
"view": "보기",
"expirationLabel": "만료:"
},
"deposit": {
"title": "입금",
"polymarketBalance": "Polymarket 잔액:",
"transferCrypto": "암호화폐 전송",
"connectExchange": "거래소 연결",
"noLimit": "제한 없음",
"instant": "즉시",
"twoMin": "약 2분",
"network": "네트워크",
"supportedTip": "지원: USDC, ETH",
"depositAddress": "입금 주소",
"copy": "복사",
"copied": "복사됨",
"connectDesc": "입금하려면 지갑을 연결하세요. 연결 후 USDC 또는 ETH를 입금 주소로 보내세요.",
"connectedTip": "연결됨. 아래 주소로 USDC 또는 ETH를 보내세요.",
"metaMask": "MetaMask",
"coinbaseComingSoon": "Coinbase Wallet (출시 예정)",
"walletConnectComingSoon": "WalletConnect (출시 예정)",
"close": "닫기",
"installMetaMask": "MetaMask 또는 다른 Web3 지갑을 설치해 주세요."
},
"withdraw": {
"title": "출금",
"polymarketBalance": "Polymarket 잔액:",
"amountUsd": "금액 (USD)",
"max": "최대",
"network": "네트워크",
"withdrawTo": "출금 대상",
"connectedWallet": "연결된 지갑",
"customAddress": "사용자 지정 주소",
"connectWallet": "지갑 연결",
"walletAddress": "지갑 주소",
"amountPlaceholder": "0.00",
"addressPlaceholder": "0x...",
"amountMustBePositive": "금액은 0보다 커야 합니다",
"insufficientBalance": "잔액 부족",
"close": "닫기"
},
"locale": {
"zh": "简体中文",
"en": "English",
"ja": "日本語",
"ko": "한국어"
}
}

172
src/locales/zh-CN.json Normal file
View File

@ -0,0 +1,172 @@
{
"common": {
"login": "登录",
"logout": "退出登录",
"balance": "余额",
"back": "返回",
"search": "搜索",
"filter": "筛选",
"collapse": "收起",
"delete": "删除",
"clear": "清空",
"loading": "加载中...",
"more": "更多操作",
"user": "用户",
"chance": "概率"
},
"toast": {
"orderSuccess": "下单成功"
},
"trade": {
"buy": "买入",
"sell": "卖出",
"orderBook": "订单簿",
"buyLabel": "买{label}",
"sellLabel": "卖{label}",
"merge": "合并",
"split": "拆分",
"market": "市价",
"limit": "限价",
"deposit": "入金",
"amount": "金额",
"shares": "份额",
"limitPrice": "限价",
"setExpiration": "设置到期",
"total": "合计",
"toWin": "可获得",
"youllReceive": "您将收到",
"avgPrice": "平均价",
"max": "最大",
"balanceLabel": "余额",
"pleaseLogin": "请先登录",
"pleaseSelectMarket": "请先选择市场(需包含 clobTokenIds",
"userError": "用户信息异常",
"orderFailed": "下单失败",
"expiration": {
"5m": "5分钟",
"15m": "15分钟",
"30m": "30分钟",
"1h": "1小时",
"2h": "2小时",
"4h": "4小时",
"8h": "8小时",
"12h": "12小时",
"1d": "1天",
"2d": "2天",
"3d": "3天"
}
},
"home": {
"searchHistory": "搜索历史",
"searchPlaceholder": "Search",
"loadMore": "加载更多"
},
"error": {
"requestFailed": "请求失败",
"loadFailed": "加载失败",
"invalidId": "无效的 ID 或 slug"
},
"activity": {
"comments": "评论",
"topHolders": "持仓大户",
"activity": "动态",
"noCommentsYet": "暂无评论",
"topHoldersPlaceholder": "持仓大户将在此显示",
"minAmount": "最小金额",
"any": "任意",
"live": "实时",
"bought": "买入",
"sold": "卖出",
"at": "以",
"viewTransaction": "查看交易",
"justNow": "刚刚",
"minutesAgo": "{n}分钟前",
"hoursAgo": "{n}小时前",
"daysAgo": "{n}天前",
"weeksAgo": "{n}周前"
},
"wallet": {
"portfolio": "资产组合",
"today": "今日",
"deposit": "入金",
"withdraw": "提现",
"profitLoss": "盈亏",
"allTime": "全部",
"pl1D": "1天",
"pl1W": "1周",
"pl1M": "1月",
"plAll": "全部",
"positions": "持仓",
"openOrders": "未成交",
"history": "历史",
"searchPlaceholder": "搜索",
"currentValue": "当前价值",
"closeLosses": "平仓亏损",
"all": "全部",
"newest": "最新",
"export": "导出",
"cancelAll": "全部撤单",
"noPositionsFound": "暂无持仓",
"noOpenOrdersFound": "暂无未成交订单",
"noHistoryFound": "您还未进行过任何交易",
"market": "市场",
"avgNow": "均价→现价",
"bet": "投注",
"toWin": "可赢",
"value": "价值",
"action": "操作",
"side": "方向",
"outcome": "结果",
"price": "价格",
"filled": "成交",
"total": "合计",
"expiration": "到期",
"activity": "活动",
"view": "查看",
"expirationLabel": "到期"
},
"deposit": {
"title": "入金",
"polymarketBalance": "Polymarket 余额:",
"transferCrypto": "转账加密货币",
"connectExchange": "连接交易所",
"noLimit": "无限制",
"instant": "即时到账",
"twoMin": "约 2 分钟",
"network": "网络",
"supportedTip": "支持USDC、ETH",
"depositAddress": "充值地址",
"copy": "复制",
"copied": "已复制",
"connectDesc": "连接钱包以充值。连接后,将 USDC 或 ETH 发送至您的充值地址。",
"connectedTip": "已连接。请将 USDC 或 ETH 发送至下方地址。",
"metaMask": "MetaMask",
"coinbaseComingSoon": "Coinbase Wallet即将推出",
"walletConnectComingSoon": "WalletConnect即将推出",
"close": "关闭",
"installMetaMask": "请安装 MetaMask 或其他 Web3 钱包。"
},
"withdraw": {
"title": "提现",
"polymarketBalance": "Polymarket 余额:",
"amountUsd": "金额 (USD)",
"max": "最大",
"network": "网络",
"withdrawTo": "提现至",
"connectedWallet": "已连接钱包",
"customAddress": "自定义地址",
"connectWallet": "连接钱包",
"walletAddress": "钱包地址",
"amountPlaceholder": "0.00",
"addressPlaceholder": "0x...",
"amountMustBePositive": "金额必须大于 0",
"insufficientBalance": "余额不足",
"close": "关闭"
},
"locale": {
"zh": "简体中文",
"en": "English",
"ja": "日本語",
"ko": "한국어"
}
}

View File

@ -4,11 +4,13 @@ import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import vuetify from './plugins/vuetify'
import { i18n } from './plugins/i18n'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(i18n)
app.use(vuetify)
app.mount('#app')

44
src/plugins/i18n.ts Normal file
View File

@ -0,0 +1,44 @@
import { createI18n } from 'vue-i18n'
import zhCN from '../locales/zh-CN.json'
import en from '../locales/en.json'
import ja from '../locales/ja.json'
import ko from '../locales/ko.json'
export type LocaleCode = 'zh-CN' | 'en' | 'ja' | 'ko'
const LOCALE_STORAGE_KEY = 'poly-locale'
export const defaultLocale: LocaleCode = 'zh-CN'
function loadSavedLocale(): LocaleCode {
try {
const saved = localStorage.getItem(LOCALE_STORAGE_KEY)
if (saved && ['zh-CN', 'en', 'ja', 'ko'].includes(saved)) {
return saved as LocaleCode
}
} catch {
//
}
return defaultLocale
}
export const i18n = createI18n({
legacy: false,
locale: loadSavedLocale(),
fallbackLocale: 'en',
messages: {
'zh-CN': zhCN as Record<string, unknown>,
en: en as Record<string, unknown>,
ja: ja as Record<string, unknown>,
ko: ko as Record<string, unknown>,
},
})
export function setLocale(locale: LocaleCode) {
i18n.global.locale.value = locale
try {
localStorage.setItem(LOCALE_STORAGE_KEY, locale)
} catch {
//
}
}

21
src/stores/locale.ts Normal file
View File

@ -0,0 +1,21 @@
import { computed } from 'vue'
import { defineStore } from 'pinia'
import type { LocaleCode } from '@/plugins/i18n'
import { i18n, setLocale as setI18nLocale } from '@/plugins/i18n'
export const useLocaleStore = defineStore('locale', () => {
const currentLocale = computed(() => i18n.global.locale.value as LocaleCode)
const localeOptions: { value: LocaleCode; label: string }[] = [
{ value: 'zh-CN', label: '简体中文' },
{ value: 'en', label: 'English' },
{ value: 'ja', label: '日本語' },
{ value: 'ko', label: '한국어' },
]
function setLocale(loc: LocaleCode) {
setI18nLocale(loc)
}
return { currentLocale, localeOptions, setLocale }
})

105
src/stores/toast.ts Normal file
View File

@ -0,0 +1,105 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
export type ToastType = 'success' | 'error' | 'info' | 'warning'
export interface ToastItem {
id: string
message: string
type: ToastType
count: number
addedAt: number
}
const DEDUP_MS = 3000
const MAX_VISIBLE = 3
let idCounter = 0
function genId() {
return `toast-${++idCounter}`
}
function normalizeMsg(msg: string) {
return msg.trim().replace(/\s+/g, ' ')
}
export const useToastStore = defineStore('toast', () => {
/** 当前正在显示的 Toast最多 MAX_VISIBLE 条) */
const displaying = ref<ToastItem[]>([])
/** 待显示的队列 */
const queue = ref<ToastItem[]>([])
/** 每种 message+type 最后一次出现的时间(添加或合并时更新) */
const lastSeenAt = ref<Record<string, number>>({})
function mergeKey(msg: string, type: ToastType) {
return `${type}:${normalizeMsg(msg)}`
}
/** 尝试合并:同 message+type 且在 DEDUP_MS 内(以最后一次为准)合并为 count+1 */
function tryMerge(item: ToastItem): boolean {
const now = Date.now()
const key = mergeKey(item.message, item.type)
const last = lastSeenAt.value[key] ?? 0
if (now - last >= DEDUP_MS) return false
const msg = normalizeMsg(item.message)
const inDisplayingIdx = displaying.value.findLastIndex(
(d) => normalizeMsg(d.message) === msg && d.type === item.type
)
if (inDisplayingIdx >= 0) {
displaying.value = displaying.value.map((x, i) =>
i === inDisplayingIdx
? { ...x, count: x.count + 1, addedAt: now }
: x
)
lastSeenAt.value[key] = now
return true
}
const inQueueReversed = [...queue.value].reverse()
const inQueueIdx = inQueueReversed.findIndex(
(q) => normalizeMsg(q.message) === msg && q.type === item.type
)
if (inQueueIdx >= 0) {
const origIdx = queue.value.length - 1 - inQueueIdx
queue.value = queue.value.map((x, i) =>
i === origIdx ? { ...x, count: x.count + 1, addedAt: now } : x
)
lastSeenAt.value[key] = now
return true
}
return false
}
function show(msg: string, toastType: ToastType = 'success') {
const item: ToastItem = {
id: genId(),
message: msg.trim(),
type: toastType,
count: 1,
addedAt: Date.now(),
}
if (tryMerge(item)) return
const key = mergeKey(item.message, item.type)
lastSeenAt.value[key] = item.addedAt
if (displaying.value.length < MAX_VISIBLE) {
displaying.value = [...displaying.value, item]
} else {
queue.value = [...queue.value, item]
}
}
/** 某条 Toast 关闭后调用 */
function remove(id: string) {
displaying.value = displaying.value.filter((d) => d.id !== id)
if (queue.value.length > 0) {
const next = queue.value[0]
queue.value = queue.value.slice(1)
displaying.value = [...displaying.value, next]
}
}
return { displaying, queue, show, remove }
})

View File

@ -3,7 +3,7 @@
<v-card v-if="detailLoading && !eventDetail" class="loading-card" elevation="0" rounded="lg">
<div class="loading-placeholder">
<v-progress-circular indeterminate color="primary" size="48" />
<p>加载中...</p>
<p>{{ t('common.loading') }}</p>
</div>
</v-card>
@ -155,10 +155,10 @@
</template>
<v-list density="compact">
<v-list-item @click="openMergeFromBar">
<v-list-item-title>Merge</v-list-item-title>
<v-list-item-title>{{ t('trade.merge') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="openSplitFromBar">
<v-list-item-title>Split</v-list-item-title>
<v-list-item-title>{{ t('trade.split') }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
@ -174,6 +174,7 @@
:initial-option="tradeInitialOption"
embedded-in-sheet
@submit="onTradeSubmit"
@order-success="onOrderSuccess"
/>
</v-bottom-sheet>
</template>
@ -197,9 +198,12 @@ import {
type PmEventMarketItem,
} from '../api/event'
import { MOCK_EVENT_LIST } from '../api/mockEventList'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '../stores/user'
import { useToastStore } from '../stores/toast'
const route = useRoute()
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
const { mobile } = useDisplay()
@ -559,6 +563,12 @@ function onTradeSubmit(payload: {
console.log('Trade submit', payload)
}
const toastStore = useToastStore()
function onOrderSuccess() {
tradeSheetOpen.value = false
toastStore.show(t('toast.orderSuccess'))
}
function marketChance(market: PmEventMarketItem): number {
const raw = market?.outcomePrices?.[0]
if (raw == null) return 0
@ -611,7 +621,7 @@ async function loadEventDetail() {
const idRaw = route.params.id
const idStr = String(idRaw ?? '').trim()
if (!idStr) {
detailError.value = '无效的 ID 或 slug'
detailError.value = t('error.invalidId')
eventDetail.value = null
return
}
@ -638,7 +648,7 @@ async function loadEventDetail() {
eventDetail.value = fallback
detailError.value = null
} else {
detailError.value = res.msg || '加载失败'
detailError.value = res.msg || t('error.loadFailed')
eventDetail.value = null
}
}
@ -648,7 +658,7 @@ async function loadEventDetail() {
eventDetail.value = fallback
detailError.value = null
} else {
detailError.value = e instanceof Error ? e.message : '加载失败'
detailError.value = e instanceof Error ? e.message : t('error.loadFailed')
eventDetail.value = null
}
} finally {

View File

@ -18,7 +18,7 @@
variant="text"
size="small"
class="home-category-action-btn"
aria-label="搜索"
:aria-label="t('common.search')"
@click="expandSearch"
>
<v-icon size="20">mdi-magnify</v-icon>
@ -28,7 +28,7 @@
variant="text"
size="small"
class="home-category-action-btn"
aria-label="筛选"
:aria-label="t('common.filter')"
>
<v-icon size="20">mdi-filter-outline</v-icon>
</v-btn>
@ -43,7 +43,7 @@
v-model="searchKeyword"
density="compact"
hide-details
placeholder="Search"
:placeholder="t('home.searchPlaceholder')"
prepend-inner-icon="mdi-magnify"
variant="outlined"
class="home-search-overlay-field"
@ -55,7 +55,7 @@
variant="text"
size="small"
class="home-search-close-btn"
aria-label="收起"
:aria-label="t('common.collapse')"
@click="collapseSearch"
>
<v-icon size="18">mdi-close</v-icon>
@ -63,7 +63,7 @@
</div>
<div v-if="searchHistoryList.length > 0" class="home-search-history">
<div class="home-search-history-header">
<span class="home-search-history-title">搜索历史</span>
<span class="home-search-history-title">{{ t('home.searchHistory') }}</span>
<v-btn
variant="text"
size="x-small"
@ -71,7 +71,7 @@
class="home-search-history-clear"
@click="searchHistory.clearAll"
>
清空
{{ t('common.clear') }}
</v-btn>
</div>
<ul class="home-search-history-list">
@ -92,7 +92,7 @@
variant="text"
size="x-small"
class="home-search-history-delete"
aria-label="删除"
:aria-label="t('common.delete')"
@click.stop="searchHistory.remove(idx)"
>
<v-icon size="16">mdi-close</v-icon>
@ -175,7 +175,7 @@
<div ref="sentinelRef" class="load-more-sentinel" aria-hidden="true" />
<div v-if="loadingMore" class="load-more-indicator">
<v-progress-circular indeterminate size="24" width="2" />
<span>加载中...</span>
<span>{{ t('common.loading') }}</span>
</div>
<div v-else-if="noMoreEvents" class="no-more-tip">没有更多了</div>
<v-btn
@ -186,7 +186,7 @@
:disabled="loadingMore"
@click="loadMore"
>
加载更多
{{ t('home.loadMore') }}
</v-btn>
</div>
</div>
@ -207,6 +207,7 @@
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
:market="homeTradeMarketPayload"
:initial-option="tradeDialogSide"
@order-success="onOrderSuccess"
/>
</v-dialog>
<v-bottom-sheet v-else v-model="tradeDialogOpen" content-class="trade-bottom-sheet">
@ -215,6 +216,7 @@
:market="homeTradeMarketPayload"
:initial-option="tradeDialogSide"
embedded-in-sheet
@order-success="onOrderSuccess"
/>
</v-bottom-sheet>
</v-container>
@ -320,9 +322,12 @@ import {
resolveCategoryIconColor,
type CategoryTreeNode,
} from '../api/category'
import { useI18n } from 'vue-i18n'
import { useSearchHistory } from '../composables/useSearchHistory'
import { useToastStore } from '../stores/toast'
const { mobile } = useDisplay()
const { t } = useI18n()
const searchHistory = useSearchHistory()
const searchHistoryList = computed(() => searchHistory.list.value)
const isMobile = computed(() => mobile.value)
@ -520,6 +525,12 @@ function onCardOpenTrade(
tradeDialogOpen.value = true
}
const toastStore = useToastStore()
function onOrderSuccess() {
tradeDialogOpen.value = false
toastStore.show(t('toast.orderSuccess'))
}
/** 传给 TradeComponent 的 marketHome 弹窗/底部栏),供 Split、下单等使用 */
const homeTradeMarketPayload = computed(() => {
const m = tradeDialogMarket.value

View File

@ -8,14 +8,14 @@
<!-- 顶部标题当前概率Past / 日期 -->
<div class="chart-header">
<h1 class="chart-title">
{{ detailLoading && !eventDetail ? '加载中...' : marketTitle }}
{{ detailLoading && !eventDetail ? t('common.loading') : marketTitle }}
</h1>
<p v-if="detailError" class="chart-error">{{ detailError }}</p>
<div class="chart-controls-row">
<v-btn variant="text" size="small" class="past-btn">Past </v-btn>
<v-btn class="date-pill" size="small" rounded="pill">{{ resolutionDate }}</v-btn>
</div>
<div class="chart-chance">{{ currentChance }}% chance</div>
<div class="chart-chance">{{ currentChance }}% {{ t('common.chance') }}</div>
</div>
<!-- 图表区域 -->
@ -53,22 +53,24 @@
:spread="clobSpread"
:loading="clobLoading"
:connected="clobConnected"
:yes-label="yesLabel"
:no-label="noLabel"
/>
</v-card>
<!-- Comments / Top Holders / Activity与左侧图表订单簿同宽 -->
<v-card class="activity-card" elevation="0" rounded="lg">
<v-tabs v-model="detailTab" class="detail-tabs" density="comfortable">
<v-tab value="comments">Comments</v-tab>
<v-tab value="holders">Top Holders</v-tab>
<v-tab value="activity">Activity</v-tab>
<v-tab value="comments">{{ t('activity.comments') }}</v-tab>
<v-tab value="holders">{{ t('activity.topHolders') }}</v-tab>
<v-tab value="activity">{{ t('activity.activity') }}</v-tab>
</v-tabs>
<v-window v-model="detailTab" class="detail-window">
<v-window-item value="comments" class="detail-pane">
<div class="placeholder-pane">No comments yet.</div>
<div class="placeholder-pane">{{ t('activity.noCommentsYet') }}</div>
</v-window-item>
<v-window-item value="holders" class="detail-pane">
<div class="placeholder-pane">Top holders will appear here.</div>
<div class="placeholder-pane">{{ t('activity.topHoldersPlaceholder') }}</div>
</v-window-item>
<v-window-item value="activity" class="detail-pane">
<div class="activity-toolbar">
@ -78,12 +80,12 @@
density="compact"
hide-details
variant="outlined"
label="Min amount"
:label="t('activity.minAmount')"
class="min-amount-select"
/>
<span class="live-badge">
<span class="live-dot"></span>
Live
{{ t('activity.live') }}
</span>
</div>
<div class="activity-list">
@ -98,18 +100,18 @@
</div>
<div class="activity-body">
<span class="activity-user">{{ item.user }}</span>
<span class="activity-action">{{ item.action }}</span>
<span class="activity-action">{{ t(item.action === 'bought' ? 'activity.bought' : 'activity.sold') }}</span>
<span
:class="['activity-amount', item.side === 'Yes' ? 'amount-yes' : 'amount-no']"
>
{{ item.amount }} {{ item.side }}
</span>
<span class="activity-price">at {{ item.price }}</span>
<span class="activity-price">{{ t('activity.at') }} {{ item.price }}</span>
<span class="activity-total">({{ item.total }})</span>
</div>
<div class="activity-meta">
<span class="activity-time">{{ formatTimeAgo(item.time) }}</span>
<a href="#" class="activity-link" aria-label="View transaction" @click.prevent>
<a href="#" class="activity-link" :aria-label="t('activity.viewTransaction')" @click.prevent>
<v-icon size="16">mdi-open-in-new</v-icon>
</a>
</div>
@ -167,10 +169,10 @@
</template>
<v-list density="compact">
<v-list-item @click="openMergeFromBar">
<v-list-item-title>Merge</v-list-item-title>
<v-list-item-title>{{ t('trade.merge') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="openSplitFromBar">
<v-list-item-title>Split</v-list-item-title>
<v-list-item-title>{{ t('trade.split') }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
@ -181,6 +183,7 @@
:market="tradeMarketPayload"
:initial-option="tradeInitialOptionFromBar"
embedded-in-sheet
@order-success="onOrderSuccess"
/>
</v-bottom-sheet>
</template>
@ -191,6 +194,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
import * as echarts from 'echarts'
import type { ECharts } from 'echarts'
@ -205,6 +209,9 @@ import {
} from '../api/event'
import { getClobWsUrl } from '../api/request'
import { useUserStore } from '../stores/user'
import { useToastStore } from '../stores/toast'
const { t } = useI18n()
import {
ClobSdk,
type PriceSizePolyMsg,
@ -269,7 +276,7 @@ async function loadEventDetail() {
const idRaw = route.params.id
const idStr = String(idRaw ?? '').trim()
if (!idStr) {
detailError.value = '无效的 ID 或 slug'
detailError.value = t('error.invalidId')
eventDetail.value = null
return
}
@ -290,11 +297,11 @@ async function loadEventDetail() {
if (res.code === 0 || res.code === 200) {
eventDetail.value = res.data ?? null
} else {
detailError.value = res.msg || '加载失败'
detailError.value = res.msg || t('error.loadFailed')
eventDetail.value = null
}
} catch (e) {
detailError.value = e instanceof Error ? e.message : '加载失败'
detailError.value = e instanceof Error ? e.message : t('error.loadFailed')
eventDetail.value = null
} finally {
detailLoading.value = false
@ -526,16 +533,22 @@ function openSplitFromBar() {
})
}
const toastStore = useToastStore()
function onOrderSuccess() {
tradeSheetOpen.value = false
toastStore.show(t('toast.orderSuccess'))
}
// Comments / Top Holders / Activity
const detailTab = ref('activity')
const activityMinAmount = ref<string>('0')
const minAmountOptions = [
{ title: 'Any', value: '0' },
const minAmountOptions = computed(() => [
{ title: t('activity.any'), value: '0' },
{ title: '$1', value: '1' },
{ title: '$10', value: '10' },
{ title: '$100', value: '100' },
{ title: '$500', value: '500' },
]
])
interface ActivityItem {
id: string
@ -628,11 +641,11 @@ const filteredActivity = computed(() => {
function formatTimeAgo(ts: number): string {
const sec = Math.floor((Date.now() - ts) / 1000)
if (sec < 60) return 'just now'
if (sec < 3600) return `${Math.floor(sec / 60)}m ago`
if (sec < 86400) return `${Math.floor(sec / 3600)}h ago`
if (sec < 604800) return `${Math.floor(sec / 86400)}d ago`
return `${Math.floor(sec / 604800)}w ago`
if (sec < 60) return t('activity.justNow')
if (sec < 3600) return t('activity.minutesAgo', { n: Math.floor(sec / 60) })
if (sec < 86400) return t('activity.hoursAgo', { n: Math.floor(sec / 3600) })
if (sec < 604800) return t('activity.daysAgo', { n: Math.floor(sec / 86400) })
return t('activity.weeksAgo', { n: Math.floor(sec / 604800) })
}
//

View File

@ -6,7 +6,7 @@
<v-card class="wallet-card portfolio-card" elevation="0" rounded="lg">
<div class="card-header">
<span class="card-title">
Portfolio
{{ t('wallet.portfolio') }}
<v-icon size="16" class="title-icon">mdi-eye-off-outline</v-icon>
</span>
<div class="balance-badge">
@ -15,7 +15,7 @@
</div>
</div>
<div class="card-value">${{ portfolioBalance }}</div>
<div class="card-timeframe">Today</div>
<div class="card-timeframe">{{ t('wallet.today') }}</div>
<div class="card-actions">
<v-btn
color="primary"
@ -24,7 +24,7 @@
prepend-icon="mdi-arrow-down"
@click="depositDialogOpen = true"
>
Deposit
{{ t('wallet.deposit') }}
</v-btn>
<v-btn
variant="outlined"
@ -33,7 +33,7 @@
prepend-icon="mdi-arrow-up"
@click="withdrawDialogOpen = true"
>
Withdraw
{{ t('wallet.withdraw') }}
</v-btn>
</div>
</v-card>
@ -43,19 +43,19 @@
<div class="card-header">
<span class="card-title">
<v-icon size="16" color="success">mdi-triangle-small-up</v-icon>
Profit/Loss
{{ t('wallet.profitLoss') }}
</span>
<div class="pl-tabs">
<v-btn
v-for="t in plTimeRanges"
:key="t"
:variant="plRange === t ? 'flat' : 'text'"
:color="plRange === t ? 'primary' : undefined"
v-for="tr in plTimeRanges"
:key="tr.value"
:variant="plRange === tr.value ? 'flat' : 'text'"
:color="plRange === tr.value ? 'primary' : undefined"
size="small"
class="pl-tab"
@click="plRange = t"
@click="plRange = tr.value"
>
{{ t }}
{{ tr.label }}
</v-btn>
</div>
</div>
@ -63,7 +63,7 @@
<span class="card-value">${{ profitLoss }}</span>
<v-icon size="18" class="info-icon">mdi-information-outline</v-icon>
</div>
<div class="card-timeframe">All-Time</div>
<div class="card-timeframe">{{ t('wallet.allTime') }}</div>
<div ref="plChartRef" class="pl-chart"></div>
</v-card>
</v-col>
@ -72,14 +72,14 @@
<!-- 下方Positions / Open orders / History -->
<div class="wallet-section">
<v-tabs v-model="activeTab" class="wallet-tabs" density="comfortable">
<v-tab value="positions">Positions</v-tab>
<v-tab value="orders">Open orders</v-tab>
<v-tab value="history">History</v-tab>
<v-tab value="positions">{{ t('wallet.positions') }}</v-tab>
<v-tab value="orders">{{ t('wallet.openOrders') }}</v-tab>
<v-tab value="history">{{ t('wallet.history') }}</v-tab>
</v-tabs>
<div class="toolbar">
<v-text-field
v-model="search"
placeholder="Search"
:placeholder="t('wallet.searchPlaceholder')"
density="compact"
hide-details
variant="outlined"
@ -96,34 +96,34 @@
@click="closeLosses"
>
<v-icon size="18">mdi-delete-outline</v-icon>
Close Losses
{{ t('wallet.closeLosses') }}
</v-btn>
<v-btn variant="outlined" size="small" class="filter-btn">
<v-icon size="18">mdi-filter</v-icon>
All
{{ t('wallet.all') }}
</v-btn>
<v-btn variant="outlined" size="small" class="filter-btn">
<v-icon size="18">mdi-sort</v-icon>
Newest
{{ t('wallet.newest') }}
</v-btn>
<v-btn variant="outlined" size="small" class="filter-btn" icon>
<v-icon size="18">mdi-calendar</v-icon>
</v-btn>
<v-btn variant="outlined" size="small" class="filter-btn">
<v-icon size="18">mdi-download</v-icon>
Export
{{ t('wallet.export') }}
</v-btn>
</template>
<template v-else-if="activeTab === 'positions'">
<v-btn variant="outlined" size="small" class="filter-btn">
<v-icon size="18">mdi-filter</v-icon>
Current value
{{ t('wallet.currentValue') }}
</v-btn>
</template>
<template v-else-if="activeTab === 'orders'">
<v-btn variant="outlined" size="small" class="filter-btn">
<v-icon size="18">mdi-filter</v-icon>
Market
{{ t('wallet.market') }}
</v-btn>
<v-btn
variant="outlined"
@ -132,12 +132,12 @@
@click="cancelAllOrders"
>
<v-icon size="18">mdi-close</v-icon>
Cancel all
{{ t('wallet.cancelAll') }}
</v-btn>
</template>
<v-btn v-else variant="outlined" size="small" class="filter-btn">
<v-icon size="18">mdi-filter</v-icon>
Market
{{ t('wallet.market') }}
</v-btn>
</div>
<v-card class="table-card" elevation="0" rounded="lg">
@ -146,7 +146,7 @@
<!-- 移动端可折叠列表 -->
<div v-if="mobile" class="positions-mobile-list">
<template v-if="filteredPositions.length === 0">
<div class="empty-cell">No positions found.</div>
<div class="empty-cell">{{ t('wallet.noPositionsFound') }}</div>
</template>
<div
v-for="pos in paginatedPositions"
@ -213,7 +213,7 @@
size="small"
class="position-sell-btn"
@click="sellPosition(pos.id)"
>Sell</v-btn
>{{ t('trade.sell') }}</v-btn
>
<v-btn
icon
@ -232,23 +232,23 @@
<v-table v-else class="wallet-table positions-table-full">
<thead>
<tr>
<th class="text-left">MARKET</th>
<th class="text-left">{{ t('wallet.market') }}</th>
<th class="text-left">
AVG NOW
{{ t('wallet.avgNow') }}
<v-icon size="14" class="th-icon">mdi-information-outline</v-icon>
</th>
<th class="text-left">BET</th>
<th class="text-left">TO WIN</th>
<th class="text-left">{{ t('wallet.bet') }}</th>
<th class="text-left">{{ t('wallet.toWin') }}</th>
<th class="text-left">
VALUE
{{ t('wallet.value') }}
<v-icon size="14" class="th-icon">mdi-chevron-down</v-icon>
</th>
<th class="text-right">ACTION</th>
<th class="text-right">{{ t('wallet.action') }}</th>
</tr>
</thead>
<tbody>
<tr v-if="filteredPositions.length === 0">
<td colspan="6" class="empty-cell">No positions found.</td>
<td colspan="6" class="empty-cell">{{ t('wallet.noPositionsFound') }}</td>
</tr>
<tr v-for="pos in paginatedPositions" :key="pos.id" class="position-row">
<td class="cell-market">
@ -296,7 +296,7 @@
size="small"
class="position-sell-btn"
@click="sellPosition(pos.id)"
>Sell</v-btn
>{{ t('trade.sell') }}</v-btn
>
<v-btn
icon
@ -317,7 +317,7 @@
<!-- 移动端挂单卡片列表 -->
<div v-if="mobile" class="orders-mobile-list">
<template v-if="filteredOpenOrders.length === 0">
<div class="empty-cell">No open orders found.</div>
<div class="empty-cell">{{ t('wallet.noOpenOrdersFound') }}</div>
</template>
<div v-for="ord in paginatedOpenOrders" :key="ord.id" class="order-mobile-card">
<div class="order-mobile-icon" :class="ord.iconClass">
@ -346,7 +346,7 @@
<v-icon size="20">mdi-close</v-icon>
</v-btn>
<div class="order-mobile-filled">{{ ord.filledDisplay || ord.filled }}</div>
<div class="order-mobile-expiry">Expiration: {{ ord.expiration }}</div>
<div class="order-mobile-expiry">{{ t('wallet.expirationLabel') }} {{ ord.expiration }}</div>
</div>
</div>
</div>
@ -354,19 +354,19 @@
<v-table v-else class="wallet-table">
<thead>
<tr>
<th class="text-left">MARKET</th>
<th class="text-left">SIDE</th>
<th class="text-left">OUTCOME</th>
<th class="text-left">PRICE</th>
<th class="text-left">FILLED</th>
<th class="text-left">TOTAL</th>
<th class="text-left">EXPIRATION</th>
<th class="text-right">ACTION</th>
<th class="text-left">{{ t('wallet.market') }}</th>
<th class="text-left">{{ t('wallet.side') }}</th>
<th class="text-left">{{ t('wallet.outcome') }}</th>
<th class="text-left">{{ t('wallet.price') }}</th>
<th class="text-left">{{ t('wallet.filled') }}</th>
<th class="text-left">{{ t('wallet.total') }}</th>
<th class="text-left">{{ t('wallet.expiration') }}</th>
<th class="text-right">{{ t('wallet.action') }}</th>
</tr>
</thead>
<tbody>
<tr v-if="filteredOpenOrders.length === 0">
<td colspan="8" class="empty-cell">No open orders found.</td>
<td colspan="8" class="empty-cell">{{ t('wallet.noOpenOrdersFound') }}</td>
</tr>
<tr v-for="ord in paginatedOpenOrders" :key="ord.id">
<td class="cell-market">{{ ord.market }}</td>
@ -397,7 +397,7 @@
<!-- 移动端历史卡片列表 -->
<div v-if="mobile" class="history-mobile-list">
<template v-if="filteredHistory.length === 0">
<div class="empty-cell">You haven't traded any polymarkets yet</div>
<div class="empty-cell">{{ t('wallet.noHistoryFound') }}</div>
</template>
<div
v-for="h in paginatedHistory"
@ -437,7 +437,7 @@
size="small"
class="history-view-btn"
@click="viewHistory(h.id)"
>View</v-btn
>{{ t('wallet.view') }}</v-btn
>
<v-btn
icon
@ -456,14 +456,14 @@
<v-table v-else class="wallet-table">
<thead>
<tr>
<th class="text-left">ACTIVITY</th>
<th class="text-left">MARKET</th>
<th class="text-left">VALUE</th>
<th class="text-left">{{ t('wallet.activity') }}</th>
<th class="text-left">{{ t('wallet.market') }}</th>
<th class="text-left">{{ t('wallet.value') }}</th>
</tr>
</thead>
<tbody>
<tr v-if="filteredHistory.length === 0">
<td colspan="3" class="empty-cell">You haven't traded any polymarkets yet</td>
<td colspan="3" class="empty-cell">{{ t('wallet.noHistoryFound') }}</td>
</tr>
<tr v-for="h in paginatedHistory" :key="h.id">
<td>
@ -563,7 +563,10 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
const { t } = useI18n()
import * as echarts from 'echarts'
import type { ECharts } from 'echarts'
import DepositDialog from '../components/DepositDialog.vue'
@ -576,7 +579,12 @@ const userStore = useUserStore()
const portfolioBalance = computed(() => userStore.balance)
const profitLoss = ref('0.00')
const plRange = ref('ALL')
const plTimeRanges = ['1D', '1W', '1M', 'ALL']
const plTimeRanges = computed(() => [
{ label: t('wallet.pl1D'), value: '1D' },
{ label: t('wallet.pl1W'), value: '1W' },
{ label: t('wallet.pl1M'), value: '1M' },
{ label: t('wallet.plAll'), value: 'ALL' },
])
const activeTab = ref<'positions' | 'orders' | 'history'>('positions')
const search = ref('')
const depositDialogOpen = ref(false)