增加:钱包页面

This commit is contained in:
ivan 2026-02-08 14:50:38 +08:00
parent 3117e34238
commit cb52d8340f
4 changed files with 385 additions and 11 deletions

View File

@ -14,12 +14,32 @@ const currentRoute = computed(() => {
<template>
<v-app>
<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-btn v-if="!userStore.isLoggedIn" text to="/login" :class="{ active: currentRoute === '/login' }">
Login
</v-btn>
<v-menu v-else location="bottom" :close-on-content-click="false">
<template v-else>
<v-btn
class="balance-btn"
variant="text"
min-width="auto"
padding="4 12"
@click="$router.push('/wallet')"
>
<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">
@ -33,6 +53,7 @@ const currentRoute = computed(() => {
<v-list-item title="退出登录" @click="userStore.logout()" />
</v-list>
</v-menu>
</template>
</v-app-bar>
<v-main>
<router-view />
@ -46,4 +67,18 @@ const currentRoute = computed(() => {
font-weight: bold;
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>

View File

@ -3,6 +3,7 @@ import Home from '../views/Home.vue'
import Trade from '../views/Trade.vue'
import Login from '../views/Login.vue'
import TradeDetail from '../views/TradeDetail.vue'
import Wallet from '../views/Wallet.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -26,6 +27,11 @@ const router = createRouter({
path: '/trade-detail/:id',
name: 'trade-detail',
component: TradeDetail
},
{
path: '/wallet',
name: 'wallet',
component: Wallet
}
],
})

View File

@ -43,6 +43,8 @@ export const useUserStore = defineStore('user', () => {
const isLoggedIn = computed(() => !!token.value && !!user.value)
const avatarUrl = computed(() => user.value?.headerImg ?? '')
/** 钱包余额显示,如 "0.00",可从接口更新 */
const balance = ref<string>('0.00')
function setUser(loginData: { token?: string; user?: UserInfo }) {
const t = loginData.token ?? ''
@ -59,5 +61,5 @@ export const useUserStore = defineStore('user', () => {
clearStorage()
}
return { token, user, isLoggedIn, avatarUrl, setUser, logout }
return { token, user, isLoggedIn, avatarUrl, balance, setUser, logout }
})

331
src/views/Wallet.vue Normal file
View 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>