新增:入金和出金
This commit is contained in:
parent
cb52d8340f
commit
830b8c594d
407
src/components/DepositDialog.vue
Normal file
407
src/components/DepositDialog.vue
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
<template>
|
||||||
|
<v-dialog
|
||||||
|
:model-value="modelValue"
|
||||||
|
max-width="480"
|
||||||
|
persistent
|
||||||
|
scrollable
|
||||||
|
content-class="deposit-dialog"
|
||||||
|
@update:model-value="$emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<v-card-text class="deposit-body">
|
||||||
|
<!-- 选择充值方式 -->
|
||||||
|
<template v-if="step === 'method'">
|
||||||
|
<div class="method-cards">
|
||||||
|
<v-card
|
||||||
|
class="method-card"
|
||||||
|
variant="outlined"
|
||||||
|
rounded="lg"
|
||||||
|
@click="selectMethod('crypto')"
|
||||||
|
>
|
||||||
|
<div class="method-icon crypto-icon">
|
||||||
|
<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>
|
||||||
|
<v-icon class="method-arrow">mdi-chevron-right</v-icon>
|
||||||
|
</v-card>
|
||||||
|
<v-card
|
||||||
|
class="method-card"
|
||||||
|
variant="outlined"
|
||||||
|
rounded="lg"
|
||||||
|
@click="selectMethod('exchange')"
|
||||||
|
>
|
||||||
|
<div class="method-icon exchange-icon">
|
||||||
|
<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>
|
||||||
|
<v-icon class="method-arrow">mdi-chevron-right</v-icon>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Transfer Crypto:选择网络 + 充值地址 -->
|
||||||
|
<template v-else-if="step === 'crypto'">
|
||||||
|
<div class="step-header">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<v-select
|
||||||
|
v-model="selectedNetwork"
|
||||||
|
:items="networks"
|
||||||
|
item-title="label"
|
||||||
|
item-value="id"
|
||||||
|
label="Network"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details
|
||||||
|
class="network-select"
|
||||||
|
/>
|
||||||
|
<p class="support-tip">Supported: USDC, ETH</p>
|
||||||
|
<div class="address-box">
|
||||||
|
<label class="address-label">Deposit address</label>
|
||||||
|
<div class="address-row">
|
||||||
|
<code class="address-code">{{ depositAddressShort }}</code>
|
||||||
|
<v-btn size="small" variant="tonal" @click="copyAddress">
|
||||||
|
{{ copied ? 'Copied' : 'Copy' }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<div class="qr-wrap">
|
||||||
|
<img
|
||||||
|
:src="qrCodeUrl"
|
||||||
|
alt="QR Code"
|
||||||
|
class="qr-img"
|
||||||
|
width="120"
|
||||||
|
height="120"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Connect Exchange:连接钱包后显示地址 -->
|
||||||
|
<template v-else-if="step === 'exchange'">
|
||||||
|
<div class="step-header">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<template v-if="!exchangeConnected">
|
||||||
|
<p class="connect-desc">Connect your wallet to deposit. Send USDC or ETH to your deposit address after connecting.</p>
|
||||||
|
<div class="wallet-buttons">
|
||||||
|
<v-btn
|
||||||
|
class="wallet-btn"
|
||||||
|
variant="outlined"
|
||||||
|
rounded="lg"
|
||||||
|
block
|
||||||
|
:loading="connecting"
|
||||||
|
@click="connectMetaMask"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-wallet</v-icon>
|
||||||
|
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)
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
class="wallet-btn"
|
||||||
|
variant="outlined"
|
||||||
|
rounded="lg"
|
||||||
|
block
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-wallet</v-icon>
|
||||||
|
WalletConnect (Coming soon)
|
||||||
|
</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.
|
||||||
|
</p>
|
||||||
|
<div class="address-box">
|
||||||
|
<label class="address-label">Deposit address</label>
|
||||||
|
<div class="address-row">
|
||||||
|
<code class="address-code">{{ depositAddressShort }}</code>
|
||||||
|
<v-btn size="small" variant="tonal" @click="copyAddress">
|
||||||
|
{{ copied ? 'Copied' : 'Copy' }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<div class="qr-wrap">
|
||||||
|
<img :src="qrCodeUrl" alt="QR Code" class="qr-img" width="120" height="120" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
balance: string
|
||||||
|
}>(),
|
||||||
|
{ balance: '0.00' }
|
||||||
|
)
|
||||||
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||||
|
|
||||||
|
const DEPOSIT_ADDRESS = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e'
|
||||||
|
|
||||||
|
const step = ref<'method' | 'crypto' | 'exchange'>('method')
|
||||||
|
const selectedNetwork = ref('ethereum')
|
||||||
|
const copied = ref(false)
|
||||||
|
const exchangeConnected = ref(false)
|
||||||
|
const connecting = ref(false)
|
||||||
|
|
||||||
|
const networks = [
|
||||||
|
{ id: 'ethereum', label: 'Ethereum' },
|
||||||
|
{ id: 'polygon', label: 'Polygon' },
|
||||||
|
{ id: 'arbitrum', label: 'Arbitrum One' },
|
||||||
|
{ id: 'optimism', label: 'Optimism' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const depositAddressShort = computed(() => {
|
||||||
|
const a = DEPOSIT_ADDRESS
|
||||||
|
return a ? `${a.slice(0, 6)}...${a.slice(-4)}` : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const qrCodeUrl = computed(() => {
|
||||||
|
return `https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=${encodeURIComponent(DEPOSIT_ADDRESS)}`
|
||||||
|
})
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectMethod(m: 'crypto' | 'exchange') {
|
||||||
|
step.value = m
|
||||||
|
if (m === 'exchange') exchangeConnected.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyAddress() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(DEPOSIT_ADDRESS)
|
||||||
|
copied.value = true
|
||||||
|
setTimeout(() => { copied.value = false }, 2000)
|
||||||
|
} catch {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectMetaMask() {
|
||||||
|
if (!window.ethereum) {
|
||||||
|
alert('Please install MetaMask or another Web3 wallet.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
connecting.value = true
|
||||||
|
try {
|
||||||
|
await window.ethereum.request({ method: 'eth_requestAccounts' })
|
||||||
|
exchangeConnected.value = true
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
connecting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open) => {
|
||||||
|
if (!open) {
|
||||||
|
step.value = 'method'
|
||||||
|
exchangeConnected.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.deposit-header {
|
||||||
|
position: relative;
|
||||||
|
padding: 20px 20px 8px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.deposit-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.deposit-balance {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deposit-body {
|
||||||
|
padding: 20px;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-cards {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.method-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.method-card:hover {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
.method-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 14px;
|
||||||
|
}
|
||||||
|
.crypto-icon {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
.exchange-icon {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
.method-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.method-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.method-desc {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.method-arrow {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.step-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-select {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.support-tip {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-box {
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.address-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6b7280;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.address-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.address-code {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #374151;
|
||||||
|
background: #fff;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.qr-wrap {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.qr-img {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-desc {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.wallet-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.wallet-btn {
|
||||||
|
text-transform: none;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.connected-tip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #059669;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
301
src/components/WithdrawDialog.vue
Normal file
301
src/components/WithdrawDialog.vue
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
<template>
|
||||||
|
<v-dialog
|
||||||
|
:model-value="modelValue"
|
||||||
|
max-width="480"
|
||||||
|
persistent
|
||||||
|
scrollable
|
||||||
|
content-class="withdraw-dialog"
|
||||||
|
@update:model-value="$emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<v-card-text class="withdraw-body">
|
||||||
|
<v-text-field
|
||||||
|
v-model="amount"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
label="Amount (USD)"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details
|
||||||
|
class="amount-field"
|
||||||
|
placeholder="0.00"
|
||||||
|
@keypress="allowDecimal"
|
||||||
|
>
|
||||||
|
<template #append-inner>
|
||||||
|
<v-btn variant="text" size="small" class="max-btn" @click="setMax">Max</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="selectedNetwork"
|
||||||
|
:items="networks"
|
||||||
|
item-title="label"
|
||||||
|
item-value="id"
|
||||||
|
label="Network"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details
|
||||||
|
class="network-select"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="destination-section">
|
||||||
|
<label class="section-label">Withdraw to</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-group>
|
||||||
|
<template v-if="destinationType === 'wallet'">
|
||||||
|
<v-btn
|
||||||
|
v-if="!connectedAddress"
|
||||||
|
class="connect-wallet-btn"
|
||||||
|
variant="outlined"
|
||||||
|
rounded="lg"
|
||||||
|
block
|
||||||
|
:loading="connecting"
|
||||||
|
@click="connectWallet"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-wallet</v-icon>
|
||||||
|
Connect Wallet
|
||||||
|
</v-btn>
|
||||||
|
<div v-else class="connected-address">
|
||||||
|
<v-icon color="success" size="18">mdi-check-circle</v-icon>
|
||||||
|
<code>{{ shortAddress(connectedAddress) }}</code>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<v-text-field
|
||||||
|
v-else
|
||||||
|
v-model="customAddress"
|
||||||
|
label="Wallet address"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details
|
||||||
|
placeholder="0x..."
|
||||||
|
class="address-field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="amountError" class="error-msg">{{ amountError }}</div>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
class="withdraw-btn"
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
rounded="lg"
|
||||||
|
:loading="submitting"
|
||||||
|
:disabled="!canSubmit"
|
||||||
|
@click="submitWithdraw"
|
||||||
|
>
|
||||||
|
Withdraw
|
||||||
|
</v-btn>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
balance: string
|
||||||
|
}>(),
|
||||||
|
{ balance: '0.00' }
|
||||||
|
)
|
||||||
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean]; success: [] }>()
|
||||||
|
|
||||||
|
const amount = ref('')
|
||||||
|
const selectedNetwork = ref('ethereum')
|
||||||
|
const destinationType = ref<'wallet' | 'address'>('wallet')
|
||||||
|
const customAddress = ref('')
|
||||||
|
const connectedAddress = ref('')
|
||||||
|
const connecting = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
const networks = [
|
||||||
|
{ id: 'ethereum', label: 'Ethereum' },
|
||||||
|
{ id: 'polygon', label: 'Polygon' },
|
||||||
|
{ id: 'arbitrum', label: 'Arbitrum One' },
|
||||||
|
{ id: 'optimism', label: 'Optimism' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const balanceNum = computed(() => parseFloat(props.balance) || 0)
|
||||||
|
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'
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasValidDestination = computed(() => {
|
||||||
|
if (destinationType.value === 'wallet') return !!connectedAddress.value
|
||||||
|
return /^0x[a-fA-F0-9]{40}$/.test(customAddress.value.trim())
|
||||||
|
})
|
||||||
|
|
||||||
|
const canSubmit = computed(
|
||||||
|
() => amountNum.value > 0 && amountNum.value <= balanceNum.value && hasValidDestination.value && !amountError.value
|
||||||
|
)
|
||||||
|
|
||||||
|
function shortAddress(addr: string) {
|
||||||
|
return addr ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMax() {
|
||||||
|
amount.value = props.balance
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowDecimal(e: KeyboardEvent) {
|
||||||
|
const key = e.key
|
||||||
|
if (['Backspace', 'Tab', 'Delete', 'ArrowLeft', 'ArrowRight'].includes(key)) return
|
||||||
|
if (key === '.' && (e.target as HTMLInputElement).value.includes('.')) e.preventDefault()
|
||||||
|
if (key !== '.' && !/^\d$/.test(key)) e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectWallet() {
|
||||||
|
if (!window.ethereum) {
|
||||||
|
alert('Please install MetaMask or another Web3 wallet.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
connecting.value = true
|
||||||
|
try {
|
||||||
|
const accounts = (await window.ethereum.request({ method: 'eth_requestAccounts' })) as string[]
|
||||||
|
connectedAddress.value = accounts[0] || ''
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
connecting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitWithdraw() {
|
||||||
|
if (!canSubmit.value) return
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await new Promise((r) => setTimeout(r, 800))
|
||||||
|
const dest = destinationType.value === 'wallet' ? connectedAddress.value : customAddress.value.trim()
|
||||||
|
console.log('Withdraw', { amount: amount.value, network: selectedNetwork.value, to: dest })
|
||||||
|
emit('success')
|
||||||
|
close()
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open) => {
|
||||||
|
if (!open) {
|
||||||
|
amount.value = ''
|
||||||
|
destinationType.value = 'wallet'
|
||||||
|
customAddress.value = ''
|
||||||
|
connectedAddress.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.withdraw-header {
|
||||||
|
position: relative;
|
||||||
|
padding: 20px 20px 8px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.withdraw-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.withdraw-balance {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.withdraw-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-field {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.max-btn {
|
||||||
|
text-transform: none;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-select {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.destination-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.section-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.destination-radio {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.destination-radio :deep(.v-radio-group) {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.connect-wallet-btn {
|
||||||
|
text-transform: none;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.connected-address {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f0fdf4;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.connected-address code {
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
.address-field {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-msg {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #dc2626;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.withdraw-btn {
|
||||||
|
text-transform: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -17,10 +17,22 @@
|
|||||||
<div class="card-value">${{ portfolioBalance }}</div>
|
<div class="card-value">${{ portfolioBalance }}</div>
|
||||||
<div class="card-timeframe">Today</div>
|
<div class="card-timeframe">Today</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<v-btn color="primary" variant="flat" class="action-btn" prepend-icon="mdi-arrow-down">
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
class="action-btn"
|
||||||
|
prepend-icon="mdi-arrow-down"
|
||||||
|
@click="depositDialogOpen = true"
|
||||||
|
>
|
||||||
Deposit
|
Deposit
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn variant="outlined" color="grey" class="action-btn" prepend-icon="mdi-arrow-up">
|
<v-btn
|
||||||
|
variant="outlined"
|
||||||
|
color="grey"
|
||||||
|
class="action-btn"
|
||||||
|
prepend-icon="mdi-arrow-up"
|
||||||
|
@click="withdrawDialogOpen = true"
|
||||||
|
>
|
||||||
Withdraw
|
Withdraw
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@ -52,12 +64,7 @@
|
|||||||
<v-icon size="18" class="info-icon">mdi-information-outline</v-icon>
|
<v-icon size="18" class="info-icon">mdi-information-outline</v-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-timeframe">All-Time</div>
|
<div class="card-timeframe">All-Time</div>
|
||||||
<div class="polymarket-logo">
|
<div ref="plChartRef" class="pl-chart"></div>
|
||||||
<span class="logo-p">P</span>
|
|
||||||
</div>
|
|
||||||
<div class="pl-bar">
|
|
||||||
<div class="pl-bar-fill" />
|
|
||||||
</div>
|
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
@ -119,11 +126,25 @@
|
|||||||
</v-table>
|
</v-table>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DepositDialog
|
||||||
|
v-model="depositDialogOpen"
|
||||||
|
:balance="portfolioBalance"
|
||||||
|
/>
|
||||||
|
<WithdrawDialog
|
||||||
|
v-model="withdrawDialogOpen"
|
||||||
|
:balance="portfolioBalance"
|
||||||
|
@success="onWithdrawSuccess"
|
||||||
|
/>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import type { ECharts } from 'echarts'
|
||||||
|
import DepositDialog from '../components/DepositDialog.vue'
|
||||||
|
import WithdrawDialog from '../components/WithdrawDialog.vue'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@ -133,7 +154,131 @@ const plRange = ref('ALL')
|
|||||||
const plTimeRanges = ['1D', '1W', '1M', 'ALL']
|
const plTimeRanges = ['1D', '1W', '1M', 'ALL']
|
||||||
const activeTab = ref('positions')
|
const activeTab = ref('positions')
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
|
const depositDialogOpen = ref(false)
|
||||||
|
const withdrawDialogOpen = ref(false)
|
||||||
const positions = ref<{ id: string; market: string; avgNow: string; bet: string; toWin: string; value: string }[]>([])
|
const positions = ref<{ id: string; market: string; avgNow: string; bet: string; toWin: string; value: string }[]>([])
|
||||||
|
|
||||||
|
const plChartRef = ref<HTMLElement | null>(null)
|
||||||
|
let plChartInstance: ECharts | null = null
|
||||||
|
|
||||||
|
/** 根据时间范围生成盈亏折线数据 [timestamp, pnl] */
|
||||||
|
function generatePlData(range: string): [number, number][] {
|
||||||
|
const now = Date.now()
|
||||||
|
let stepMs: number
|
||||||
|
let count: number
|
||||||
|
switch (range) {
|
||||||
|
case '1D':
|
||||||
|
stepMs = 60 * 60 * 1000
|
||||||
|
count = 24
|
||||||
|
break
|
||||||
|
case '1W':
|
||||||
|
stepMs = 24 * 60 * 60 * 1000
|
||||||
|
count = 7
|
||||||
|
break
|
||||||
|
case '1M':
|
||||||
|
stepMs = 24 * 60 * 60 * 1000
|
||||||
|
count = 30
|
||||||
|
break
|
||||||
|
case 'ALL':
|
||||||
|
default:
|
||||||
|
stepMs = 24 * 60 * 60 * 1000
|
||||||
|
count = 30
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const data: [number, number][] = []
|
||||||
|
let pnl = 0
|
||||||
|
for (let i = count; i >= 0; i--) {
|
||||||
|
const t = now - i * stepMs
|
||||||
|
pnl += (Math.random() - 0.48) * 20
|
||||||
|
data.push([t, Math.round(pnl * 100) / 100])
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPlChartOption(chartData: [number, number][]) {
|
||||||
|
const lastPoint = chartData[chartData.length - 1]
|
||||||
|
const lastVal = lastPoint != null ? lastPoint[1] : 0
|
||||||
|
const lineColor = lastVal >= 0 ? '#059669' : '#dc2626'
|
||||||
|
return {
|
||||||
|
animation: false,
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
formatter: (params: unknown) => {
|
||||||
|
const p = (Array.isArray(params) ? params[0] : params) as { name: string | number; value: unknown }
|
||||||
|
const date = new Date(p.name as number)
|
||||||
|
const val = Array.isArray(p.value) ? (p.value as number[])[1] : p.value
|
||||||
|
const sign = Number(val) >= 0 ? '+' : ''
|
||||||
|
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}<br/>${sign}$${Number(val).toFixed(2)}`
|
||||||
|
},
|
||||||
|
axisPointer: { animation: false },
|
||||||
|
},
|
||||||
|
grid: { left: 8, right: 8, top: 8, bottom: 24, containLabel: false },
|
||||||
|
xAxis: {
|
||||||
|
type: 'time',
|
||||||
|
axisLine: { show: false },
|
||||||
|
axisTick: { show: false },
|
||||||
|
axisLabel: { color: '#9ca3af', fontSize: 10 },
|
||||||
|
splitLine: { show: false },
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
position: 'right',
|
||||||
|
axisLine: { show: false },
|
||||||
|
axisTick: { show: false },
|
||||||
|
axisLabel: { color: '#9ca3af', fontSize: 10, formatter: (v: number) => `$${v}` },
|
||||||
|
splitLine: { lineStyle: { type: 'dashed', color: '#e5e7eb' } },
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
data: chartData,
|
||||||
|
smooth: true,
|
||||||
|
showSymbol: false,
|
||||||
|
lineStyle: { width: 2, color: lineColor },
|
||||||
|
areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: lineColor + '40' }, { offset: 1, color: lineColor + '08' }]) },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plChartData = ref<[number, number][]>([])
|
||||||
|
|
||||||
|
function updatePlChart() {
|
||||||
|
plChartData.value = generatePlData(plRange.value)
|
||||||
|
const last = plChartData.value[plChartData.value.length - 1]
|
||||||
|
if (last) profitLoss.value = last[1].toFixed(2)
|
||||||
|
if (plChartInstance) plChartInstance.setOption(buildPlChartOption(plChartData.value), { replaceMerge: ['series'] })
|
||||||
|
}
|
||||||
|
|
||||||
|
function initPlChart() {
|
||||||
|
if (!plChartRef.value) return
|
||||||
|
plChartData.value = generatePlData(plRange.value)
|
||||||
|
const last = plChartData.value[plChartData.value.length - 1]
|
||||||
|
if (last) profitLoss.value = last[1].toFixed(2)
|
||||||
|
plChartInstance = echarts.init(plChartRef.value)
|
||||||
|
plChartInstance.setOption(buildPlChartOption(plChartData.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = () => plChartInstance?.resize()
|
||||||
|
|
||||||
|
watch(plRange, () => updatePlChart())
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
initPlChart()
|
||||||
|
})
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
plChartInstance?.dispose()
|
||||||
|
plChartInstance = null
|
||||||
|
})
|
||||||
|
|
||||||
|
function onWithdrawSuccess() {
|
||||||
|
withdrawDialogOpen.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -233,35 +378,10 @@ const positions = ref<{ id: string; market: string; avgNow: string; bet: string;
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.polymarket-logo {
|
.pl-chart {
|
||||||
margin-bottom: 12px;
|
height: 120px;
|
||||||
}
|
width: 100%;
|
||||||
|
margin-top: 4px;
|
||||||
.logo-p {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
background: linear-gradient(135deg, #1a73e8 0%, #34a853 100%);
|
|
||||||
color: white;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 14px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-bar {
|
|
||||||
height: 6px;
|
|
||||||
background: #e5e7eb;
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-bar-fill {
|
|
||||||
height: 100%;
|
|
||||||
width: 40%;
|
|
||||||
background: linear-gradient(90deg, #93c5fd 0%, #bfdbfe 100%);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallet-section {
|
.wallet-section {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user