新增:入金和出金
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-timeframe">Today</div>
|
||||
<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
|
||||
</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
|
||||
</v-btn>
|
||||
</div>
|
||||
@ -52,12 +64,7 @@
|
||||
<v-icon size="18" class="info-icon">mdi-information-outline</v-icon>
|
||||
</div>
|
||||
<div class="card-timeframe">All-Time</div>
|
||||
<div class="polymarket-logo">
|
||||
<span class="logo-p">P</span>
|
||||
</div>
|
||||
<div class="pl-bar">
|
||||
<div class="pl-bar-fill" />
|
||||
</div>
|
||||
<div ref="plChartRef" class="pl-chart"></div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -119,11 +126,25 @@
|
||||
</v-table>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<DepositDialog
|
||||
v-model="depositDialogOpen"
|
||||
:balance="portfolioBalance"
|
||||
/>
|
||||
<WithdrawDialog
|
||||
v-model="withdrawDialogOpen"
|
||||
:balance="portfolioBalance"
|
||||
@success="onWithdrawSuccess"
|
||||
/>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<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'
|
||||
|
||||
const userStore = useUserStore()
|
||||
@ -133,7 +154,131 @@ const plRange = ref('ALL')
|
||||
const plTimeRanges = ['1D', '1W', '1M', 'ALL']
|
||||
const activeTab = ref('positions')
|
||||
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 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>
|
||||
|
||||
<style scoped>
|
||||
@ -233,35 +378,10 @@ const positions = ref<{ id: string; market: string; avgNow: string; bet: string;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.polymarket-logo {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.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;
|
||||
.pl-chart {
|
||||
height: 120px;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.wallet-section {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user