增加:钱包页面
This commit is contained in:
parent
3117e34238
commit
cb52d8340f
55
src/App.vue
55
src/App.vue
@ -14,25 +14,46 @@ const currentRoute = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<v-app-bar color="primary" dark>
|
<v-app-bar color="primary" dark>
|
||||||
<v-app-bar-title>PolyMarket</v-app-bar-title>
|
<v-btn
|
||||||
|
v-if="currentRoute !== '/'"
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
class="back-btn"
|
||||||
|
aria-label="返回"
|
||||||
|
@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-spacer></v-spacer>
|
||||||
<v-btn v-if="!userStore.isLoggedIn" text to="/login" :class="{ active: currentRoute === '/login' }">
|
<v-btn v-if="!userStore.isLoggedIn" text to="/login" :class="{ active: currentRoute === '/login' }">
|
||||||
Login
|
Login
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-menu v-else location="bottom" :close-on-content-click="false">
|
<template v-else>
|
||||||
<template #activator="{ props }">
|
<v-btn
|
||||||
<v-btn v-bind="props" icon variant="text" class="avatar-btn">
|
class="balance-btn"
|
||||||
<v-avatar size="36" color="primary">
|
variant="text"
|
||||||
<v-img v-if="userStore.avatarUrl" :src="userStore.avatarUrl" cover alt="avatar" />
|
min-width="auto"
|
||||||
<v-icon v-else>mdi-account</v-icon>
|
padding="4 12"
|
||||||
</v-avatar>
|
@click="$router.push('/wallet')"
|
||||||
</v-btn>
|
>
|
||||||
</template>
|
<span class="balance-text">${{ userStore.balance }}</span>
|
||||||
|
</v-btn>
|
||||||
|
<v-menu location="bottom" :close-on-content-click="false">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn v-bind="props" icon variant="text" class="avatar-btn">
|
||||||
|
<v-avatar size="36" color="primary">
|
||||||
|
<v-img v-if="userStore.avatarUrl" :src="userStore.avatarUrl" cover alt="avatar" />
|
||||||
|
<v-icon v-else>mdi-account</v-icon>
|
||||||
|
</v-avatar>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
<v-list density="compact">
|
<v-list density="compact">
|
||||||
<v-list-item :title="userStore.user?.nickName || userStore.user?.userName || 'User'" disabled />
|
<v-list-item :title="userStore.user?.nickName || userStore.user?.userName || 'User'" disabled />
|
||||||
<v-list-item title="退出登录" @click="userStore.logout()" />
|
<v-list-item title="退出登录" @click="userStore.logout()" />
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
|
</template>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<v-main>
|
<v-main>
|
||||||
<router-view />
|
<router-view />
|
||||||
@ -46,4 +67,18 @@ const currentRoute = computed(() => {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.balance-btn {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-text {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import Home from '../views/Home.vue'
|
|||||||
import Trade from '../views/Trade.vue'
|
import Trade from '../views/Trade.vue'
|
||||||
import Login from '../views/Login.vue'
|
import Login from '../views/Login.vue'
|
||||||
import TradeDetail from '../views/TradeDetail.vue'
|
import TradeDetail from '../views/TradeDetail.vue'
|
||||||
|
import Wallet from '../views/Wallet.vue'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
@ -26,6 +27,11 @@ const router = createRouter({
|
|||||||
path: '/trade-detail/:id',
|
path: '/trade-detail/:id',
|
||||||
name: 'trade-detail',
|
name: 'trade-detail',
|
||||||
component: TradeDetail
|
component: TradeDetail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/wallet',
|
||||||
|
name: 'wallet',
|
||||||
|
component: Wallet
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -43,6 +43,8 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
|
|
||||||
const isLoggedIn = computed(() => !!token.value && !!user.value)
|
const isLoggedIn = computed(() => !!token.value && !!user.value)
|
||||||
const avatarUrl = computed(() => user.value?.headerImg ?? '')
|
const avatarUrl = computed(() => user.value?.headerImg ?? '')
|
||||||
|
/** 钱包余额显示,如 "0.00",可从接口更新 */
|
||||||
|
const balance = ref<string>('0.00')
|
||||||
|
|
||||||
function setUser(loginData: { token?: string; user?: UserInfo }) {
|
function setUser(loginData: { token?: string; user?: UserInfo }) {
|
||||||
const t = loginData.token ?? ''
|
const t = loginData.token ?? ''
|
||||||
@ -59,5 +61,5 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
clearStorage()
|
clearStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
return { token, user, isLoggedIn, avatarUrl, setUser, logout }
|
return { token, user, isLoggedIn, avatarUrl, balance, setUser, logout }
|
||||||
})
|
})
|
||||||
|
|||||||
331
src/views/Wallet.vue
Normal file
331
src/views/Wallet.vue
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
<template>
|
||||||
|
<v-container class="wallet-container">
|
||||||
|
<!-- 顶部:Portfolio + Profit/Loss 卡片 -->
|
||||||
|
<v-row class="wallet-cards">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card class="wallet-card portfolio-card" elevation="0" rounded="lg">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
Portfolio
|
||||||
|
<v-icon size="16" class="title-icon">mdi-eye-off-outline</v-icon>
|
||||||
|
</span>
|
||||||
|
<div class="balance-badge">
|
||||||
|
<v-icon size="14">mdi-sack</v-icon>
|
||||||
|
<span>${{ portfolioBalance }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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">
|
||||||
|
Deposit
|
||||||
|
</v-btn>
|
||||||
|
<v-btn variant="outlined" color="grey" class="action-btn" prepend-icon="mdi-arrow-up">
|
||||||
|
Withdraw
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card class="wallet-card pl-card" elevation="0" rounded="lg">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
<v-icon size="16" color="success">mdi-triangle-small-up</v-icon>
|
||||||
|
Profit/Loss
|
||||||
|
</span>
|
||||||
|
<div class="pl-tabs">
|
||||||
|
<v-btn
|
||||||
|
v-for="t in plTimeRanges"
|
||||||
|
:key="t"
|
||||||
|
:variant="plRange === t ? 'flat' : 'text'"
|
||||||
|
:color="plRange === t ? 'primary' : undefined"
|
||||||
|
size="small"
|
||||||
|
class="pl-tab"
|
||||||
|
@click="plRange = t"
|
||||||
|
>
|
||||||
|
{{ t }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-value-row">
|
||||||
|
<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="polymarket-logo">
|
||||||
|
<span class="logo-p">P</span>
|
||||||
|
</div>
|
||||||
|
<div class="pl-bar">
|
||||||
|
<div class="pl-bar-fill" />
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 下方: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-tabs>
|
||||||
|
<div class="toolbar">
|
||||||
|
<v-text-field
|
||||||
|
v-model="search"
|
||||||
|
placeholder="Search"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
variant="outlined"
|
||||||
|
rounded
|
||||||
|
class="search-field"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
/>
|
||||||
|
<v-btn variant="outlined" size="small" class="filter-btn">
|
||||||
|
<v-icon size="18">mdi-filter</v-icon>
|
||||||
|
Current value
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<v-card class="table-card" elevation="0" rounded="lg">
|
||||||
|
<v-table class="positions-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">MARKET</th>
|
||||||
|
<th class="text-left">
|
||||||
|
AVG → NOW
|
||||||
|
<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">
|
||||||
|
VALUE
|
||||||
|
<v-icon size="14" class="th-icon">mdi-chevron-down</v-icon>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="positions.length === 0">
|
||||||
|
<td colspan="5" class="empty-cell">
|
||||||
|
No positions found.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="pos in positions" :key="pos.id">
|
||||||
|
<td>{{ pos.market }}</td>
|
||||||
|
<td>{{ pos.avgNow }}</td>
|
||||||
|
<td>{{ pos.bet }}</td>
|
||||||
|
<td>{{ pos.toWin }}</td>
|
||||||
|
<td>{{ pos.value }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const portfolioBalance = computed(() => userStore.balance)
|
||||||
|
const profitLoss = ref('0.00')
|
||||||
|
const plRange = ref('ALL')
|
||||||
|
const plTimeRanges = ['1D', '1W', '1M', 'ALL']
|
||||||
|
const activeTab = ref('positions')
|
||||||
|
const search = ref('')
|
||||||
|
const positions = ref<{ id: string; market: string; avgNow: string; bet: string; toWin: string; value: string }[]>([])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wallet-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-cards {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-card {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-icon {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #059669;
|
||||||
|
background-color: #d1fae5;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value-row .card-value {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-timeframe {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-tab {
|
||||||
|
min-width: 40px;
|
||||||
|
text-transform: none;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-section {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-tabs {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-field {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-field :deep(.v-field) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
text-transform: none;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positions-table {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positions-table th {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.th-icon {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 2px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positions-table td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-cell {
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
padding: 48px 16px !important;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user