新增:钱包页交易盈亏曲线,并对齐 Polymarket 双卡布局。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
马丁 2026-07-26 17:16:15 +08:00
parent d6f1cd956e
commit d3f237168c
10 changed files with 579 additions and 189 deletions

42
src/api/pnlCurve.ts Normal file
View File

@ -0,0 +1,42 @@
import { get } from './request'
import type { ApiResponse } from './types'
export type PnlCurveRange = '1D' | '1W' | '1M' | '1Y' | 'YTD' | 'ALL'
export interface PnlCurvePoint {
ts: number
pnl: number
}
export interface PnlCurveData {
range: PnlCurveRange
startTs: number
endTs: number
currentPnl: number
changeAmount: number
changePercent: number
noHistory: boolean
points: PnlCurvePoint[]
}
export interface PnlCurveResponse extends ApiResponse {
data: PnlCurveData
}
/**
* GET /uas/getPnlCurveClient
* 线 USDC
*/
export async function getPnlCurve(
range: PnlCurveRange,
config?: { headers?: Record<string, string> },
): Promise<PnlCurveResponse> {
return get<PnlCurveResponse>('/uas/getPnlCurveClient', { range }, config)
}
export const PNL_CURVE_RANGES: PnlCurveRange[] = ['1D', '1W', '1M', '1Y', 'YTD', 'ALL']
export function formatPnlAmount(amount: number): string {
const sign = amount > 0 ? '+' : amount < 0 ? '-' : ''
return `${sign}$${Math.abs(amount).toFixed(2)}`
}

View File

@ -0,0 +1,296 @@
<template>
<section class="card pnl-curve-card">
<div class="pnl-head">
<div class="pnl-head-left">
<span class="pnl-dot" aria-hidden="true" />
<span class="pnl-title">{{ t('wallet.pnlCurve.title') }}</span>
</div>
<div class="pnl-range-row">
<button
v-for="r in ranges"
:key="r"
type="button"
class="range-btn"
:class="{ active: activeRange === r }"
@click="setRange(r)"
>
{{ t(`wallet.pnlCurve.range.${r}`) }}
</button>
</div>
</div>
<div class="pnl-value-row">
<div class="pnl-amount-wrap">
<div class="pnl-amount" :class="{ up: currentPnl > 0, down: currentPnl < 0 }">
{{ formatPnlAmount(currentPnl) }}
</div>
</div>
<div class="pnl-period">{{ t(`wallet.pnlCurve.period.${activeRange}`) }}</div>
</div>
<div ref="chartRef" class="pnl-chart" :aria-label="t('wallet.pnlCurve.title')">
<v-progress-circular
v-if="loading"
indeterminate
size="28"
width="2"
class="pnl-loading"
/>
</div>
</section>
</template>
<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import {
AreaSeries,
createChart,
LastPriceAnimationMode,
LineType,
type IChartApi,
type ISeriesApi,
} from 'lightweight-charts'
import {
PNL_CURVE_RANGES,
formatPnlAmount,
getPnlCurve,
type PnlCurveRange,
} from '@/api/pnlCurve'
import { toLwcData } from '@/composables/useLightweightChart'
import { useUserStore } from '@/stores/user'
import { useToastStore } from '@/stores/toast'
const { t } = useI18n()
const userStore = useUserStore()
const toastStore = useToastStore()
const ranges = PNL_CURVE_RANGES
const activeRange = ref<PnlCurveRange>('1D')
const loading = ref(false)
const currentPnl = ref(0)
const chartRef = ref<HTMLElement | null>(null)
let chartInstance: IChartApi | null = null
let chartSeries: ISeriesApi<'Area'> | null = null
const LINE_UP = '#2E5CFF'
const LINE_DOWN = '#dc2626'
function seriesColors(pnl: number) {
const color = pnl >= 0 ? LINE_UP : LINE_DOWN
return {
topColor: color + '33',
bottomColor: color + '00',
lineColor: color,
}
}
async function loadCurve() {
if (!userStore.isLoggedIn) return
const headers = userStore.getAuthHeaders()
if (!headers) return
loading.value = true
try {
const res = await getPnlCurve(activeRange.value, { headers })
if (res.code !== 0 && res.code !== 200) {
toastStore.show(res.msg || t('wallet.pnlCurve.loadFailed'), 'error')
return
}
const data = res.data
currentPnl.value = data.currentPnl ?? 0
const points: [number, number][] = (data.points ?? []).map((p) => [p.ts, p.pnl])
if (chartSeries) {
chartSeries.applyOptions(seriesColors(currentPnl.value))
chartSeries.setData(toLwcData(points))
chartInstance?.timeScale().fitContent()
}
} catch (e) {
const msg = e instanceof Error ? e.message : t('wallet.pnlCurve.loadFailed')
toastStore.show(msg, 'error')
} finally {
loading.value = false
}
}
function setRange(r: PnlCurveRange) {
if (activeRange.value === r) return
activeRange.value = r
}
function initChart() {
if (!chartRef.value) return
const el = chartRef.value
chartInstance = createChart(el, {
width: el.clientWidth,
height: 140,
layout: {
background: { color: '#ffffff' },
textColor: '#9ca3af',
fontFamily: 'inherit',
fontSize: 9,
attributionLogo: false,
},
grid: { vertLines: { visible: false }, horzLines: { visible: false } },
leftPriceScale: { visible: false },
rightPriceScale: { visible: false, borderVisible: false },
timeScale: { visible: false, borderVisible: false },
handleScroll: false,
handleScale: false,
crosshair: {
vertLine: { visible: true, labelVisible: false },
horzLine: { visible: false, labelVisible: false },
},
})
const colors = seriesColors(0)
chartSeries = chartInstance.addSeries(AreaSeries, {
...colors,
lineWidth: 2,
lineType: LineType.Curved,
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
priceFormat: { type: 'price', precision: 2 },
crosshairMarkerVisible: true,
})
}
function handleResize() {
if (chartInstance && chartRef.value) {
chartInstance.resize(chartRef.value.clientWidth, 140)
}
}
watch(activeRange, () => {
loadCurve()
})
onMounted(async () => {
await nextTick()
initChart()
await loadCurve()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
chartInstance?.remove()
chartInstance = null
chartSeries = null
})
</script>
<style scoped lang="scss">
.pnl-curve-card {
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
min-width: 0;
height: 100%;
box-sizing: border-box;
}
.pnl-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
}
.pnl-head-left {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.pnl-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #9ca3af;
flex-shrink: 0;
}
.pnl-title {
color: #111827;
font-size: 15px;
font-weight: 600;
line-height: 1.2;
}
.pnl-range-row {
display: flex;
flex-wrap: wrap;
gap: 2px;
}
.range-btn {
height: 28px;
padding: 0 10px;
border-radius: 6px;
border: none;
background: transparent;
color: #6b7280;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.range-btn.active {
background: #e8efff;
color: #2e5cff;
}
.pnl-value-row {
display: flex;
flex-direction: column;
gap: 4px;
}
.pnl-amount-wrap {
display: flex;
align-items: center;
gap: 6px;
}
.pnl-amount {
color: #111827;
font-size: 32px;
font-weight: 700;
line-height: 1.1;
}
.pnl-amount.up {
color: #16a34a;
}
.pnl-amount.down {
color: #dc2626;
}
.pnl-period {
color: #9ca3af;
font-size: 12px;
font-weight: 500;
}
.pnl-chart {
position: relative;
width: 100%;
height: 140px;
margin-top: auto;
}
.pnl-loading {
position: absolute;
inset: 0;
margin: auto;
color: #2e5cff !important;
}
</style>

View File

@ -1,10 +1,11 @@
<template> <template>
<v-btn <v-btn
color="default" color="default"
variant="outlined" variant="flat"
class="action-btn design-tu-action" class="action-btn"
@click="dialogOpen = true" @click="dialogOpen = true"
> >
<v-icon size="18" start>mdi-arrow-down</v-icon>
{{ t('wallet.deposit') }} {{ t('wallet.deposit') }}
</v-btn> </v-btn>
<Teleport to="body"> <Teleport to="body">
@ -28,22 +29,8 @@ defineExpose({ open })
</script> </script>
<style scoped> <style scoped>
.design-tu-action {
flex: 1;
height: 72px;
border-radius: 12px;
border: 1px solid #e5e7eb;
background: #fff;
color: #111827;
font-size: 20px;
font-weight: 600;
}
.design-tu-action :deep(.v-btn__content) {
font-size: 20px;
}
.action-btn { .action-btn {
text-transform: none; text-transform: none;
letter-spacing: 0;
} }
</style> </style>

View File

@ -234,6 +234,7 @@
}, },
"wallet": { "wallet": {
"availableAssets": "Available Assets", "availableAssets": "Available Assets",
"availableForTrading": "Available to trade",
"portfolio": "Portfolio", "portfolio": "Portfolio",
"cash": "Cash", "cash": "Cash",
"today": "Today", "today": "Today",
@ -247,6 +248,26 @@
"pl1W": "1W", "pl1W": "1W",
"pl1M": "1M", "pl1M": "1M",
"plAll": "ALL", "plAll": "ALL",
"pnlCurve": {
"title": "Profit/Loss",
"loadFailed": "Failed to load P&L curve",
"range": {
"1D": "1D",
"1W": "1W",
"1M": "1M",
"1Y": "1Y",
"YTD": "YTD",
"ALL": "ALL"
},
"period": {
"1D": "Past 24 hours",
"1W": "Past week",
"1M": "Past month",
"1Y": "Past year",
"YTD": "Year to date",
"ALL": "All time"
}
},
"positions": "Positions", "positions": "Positions",
"openOrders": "Open orders", "openOrders": "Open orders",
"history": "History", "history": "History",

View File

@ -234,6 +234,7 @@
}, },
"wallet": { "wallet": {
"availableAssets": "利用可能資産", "availableAssets": "利用可能資産",
"availableForTrading": "取引可能",
"portfolio": "ポートフォリオ", "portfolio": "ポートフォリオ",
"cash": "現金", "cash": "現金",
"today": "今日", "today": "今日",
@ -247,6 +248,26 @@
"pl1W": "1週", "pl1W": "1週",
"pl1M": "1月", "pl1M": "1月",
"plAll": "全て", "plAll": "全て",
"pnlCurve": {
"title": "損益",
"loadFailed": "損益曲線の読み込みに失敗しました",
"range": {
"1D": "1日",
"1W": "1週",
"1M": "1ヶ月",
"1Y": "1年",
"YTD": "年初来",
"ALL": "全て"
},
"period": {
"1D": "過去24時間",
"1W": "過去1週間",
"1M": "過去1ヶ月",
"1Y": "過去1年",
"YTD": "年初来",
"ALL": "全期間"
}
},
"positions": "ポジション", "positions": "ポジション",
"openOrders": "未約定", "openOrders": "未約定",
"history": "履歴", "history": "履歴",

View File

@ -234,6 +234,7 @@
}, },
"wallet": { "wallet": {
"availableAssets": "사용 가능 자산", "availableAssets": "사용 가능 자산",
"availableForTrading": "거래 가능",
"portfolio": "포트폴리오", "portfolio": "포트폴리오",
"cash": "현금", "cash": "현금",
"today": "오늘", "today": "오늘",
@ -247,6 +248,26 @@
"pl1W": "1주", "pl1W": "1주",
"pl1M": "1월", "pl1M": "1월",
"plAll": "전체", "plAll": "전체",
"pnlCurve": {
"title": "손익",
"loadFailed": "손익 곡선 로드 실패",
"range": {
"1D": "1일",
"1W": "1주",
"1M": "1개월",
"1Y": "1년",
"YTD": "연초 이후",
"ALL": "전체"
},
"period": {
"1D": "지난 24시간",
"1W": "지난 1주",
"1M": "지난 1개월",
"1Y": "지난 1년",
"YTD": "연초 이후",
"ALL": "전체 기간"
}
},
"positions": "포지션", "positions": "포지션",
"openOrders": "미체결", "openOrders": "미체결",
"history": "내역", "history": "내역",

View File

@ -234,6 +234,7 @@
}, },
"wallet": { "wallet": {
"availableAssets": "可用资产", "availableAssets": "可用资产",
"availableForTrading": "可用于交易",
"portfolio": "资产组合", "portfolio": "资产组合",
"cash": "现金", "cash": "现金",
"today": "今日", "today": "今日",
@ -247,6 +248,26 @@
"pl1W": "1周", "pl1W": "1周",
"pl1M": "1月", "pl1M": "1月",
"plAll": "全部", "plAll": "全部",
"pnlCurve": {
"title": "盈亏",
"loadFailed": "盈亏曲线加载失败",
"range": {
"1D": "1天",
"1W": "1周",
"1M": "1个月",
"1Y": "1年",
"YTD": "年初至今",
"ALL": "全部"
},
"period": {
"1D": "过去 24 小时",
"1W": "过去 1 周",
"1M": "过去 1 个月",
"1Y": "过去 1 年",
"YTD": "年初至今",
"ALL": "全部时间"
}
},
"positions": "持仓", "positions": "持仓",
"openOrders": "未成交", "openOrders": "未成交",
"history": "历史", "history": "历史",

View File

@ -234,6 +234,7 @@
}, },
"wallet": { "wallet": {
"availableAssets": "可用資產", "availableAssets": "可用資產",
"availableForTrading": "可用於交易",
"portfolio": "資產組合", "portfolio": "資產組合",
"cash": "現金", "cash": "現金",
"today": "今日", "today": "今日",
@ -247,6 +248,26 @@
"pl1W": "1週", "pl1W": "1週",
"pl1M": "1月", "pl1M": "1月",
"plAll": "全部", "plAll": "全部",
"pnlCurve": {
"title": "盈虧",
"loadFailed": "盈虧曲線載入失敗",
"range": {
"1D": "1天",
"1W": "1週",
"1M": "1個月",
"1Y": "1年",
"YTD": "年初至今",
"ALL": "全部"
},
"period": {
"1D": "過去 24 小時",
"1W": "過去 1 週",
"1M": "過去 1 個月",
"1Y": "過去 1 年",
"YTD": "年初至今",
"ALL": "全部時間"
}
},
"positions": "持倉", "positions": "持倉",
"openOrders": "未成交", "openOrders": "未成交",
"history": "歷史", "history": "歷史",

View File

@ -32,9 +32,6 @@
</span> </span>
</button> --> </button> -->
</section> </section>
<AssetCurveCard />
<section class="card wallet-card"> <section class="card wallet-card">
<div class="wallet-head"> <div class="wallet-head">
<span class="wallet-title">{{ t('profile.walletOverview') }}</span> <span class="wallet-title">{{ t('profile.walletOverview') }}</span>
@ -136,7 +133,6 @@ import { useUserStore, type UserInfo } from '../stores/user'
import { setSelfUsername, setSelfHeaderImg } from '@/api/user' import { setSelfUsername, setSelfHeaderImg } from '@/api/user'
import { getLockMoney } from '@/api/earnActivity' import { getLockMoney } from '@/api/earnActivity'
import { upload } from '@/api/fileUpload' import { upload } from '@/api/fileUpload'
import AssetCurveCard from '@/components/AssetCurveCard.vue'
import type { LocaleCode } from '@/plugins/i18n' import type { LocaleCode } from '@/plugins/i18n'
interface SettingItem { interface SettingItem {

View File

@ -2,23 +2,36 @@
<v-container class="wallet-container"> <v-container class="wallet-container">
<div class="wallet-mobile-frame"> <div class="wallet-mobile-frame">
<div class="wallet-mobile-header">{{ t('wallet.portfolio') }}</div> <div class="wallet-mobile-header">{{ t('wallet.portfolio') }}</div>
<v-card class="wallet-card portfolio-card design-tu-asset" elevation="0" rounded="lg"> <div class="wallet-top-row">
<div class="card-header"> <v-card class="wallet-card portfolio-card" elevation="0" rounded="lg">
<span class="card-title">{{ t('wallet.portfolio') }}</span> <div class="portfolio-head">
</div> <div class="portfolio-title-wrap">
<div class="card-value"> <v-icon size="18" class="portfolio-title-icon">mdi-wallet-outline</v-icon>
<div class="main-balance">${{ portfolioBalance }}</div> <span class="card-title">{{ t('wallet.portfolio') }}</span>
<div class="available-balance"> </div>
({{ t('wallet.availableAssets') }}: ${{ userStore.balance }}) <div class="portfolio-available">
<span class="portfolio-available-label">{{ t('wallet.availableForTrading') }}</span>
<span class="portfolio-available-value">${{ userStore.balance }}</span>
</div>
</div> </div>
</div> <div class="card-value">
</v-card> <div class="main-balance">${{ portfolioBalance }}</div>
<div class="card-actions design-tu-actions"> </div>
<WalletDepositModule /> <div class="card-actions portfolio-actions">
<v-btn color="default" variant="outlined" class="action-btn design-tu-action" <div class="portfolio-deposit">
@click="withdrawDialogOpen = true"> <WalletDepositModule />
{{ t('wallet.withdraw') }} </div>
</v-btn> <v-btn
variant="outlined"
class="action-btn portfolio-withdraw-btn"
@click="withdrawDialogOpen = true"
>
<v-icon size="18" start>mdi-arrow-up</v-icon>
{{ t('wallet.withdraw') }}
</v-btn>
</div>
</v-card>
<PnlCurveCard class="wallet-pnl-card" />
</div> </div>
<v-card v-if="unsettledCount > 0" class="settlement-card design-tu-settlement" elevation="0" rounded="lg"> <v-card v-if="unsettledCount > 0" class="settlement-card design-tu-settlement" elevation="0" rounded="lg">
@ -401,18 +414,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue' import { ref, computed, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
const { t } = useI18n() const { t } = useI18n()
import { createChart, AreaSeries, LineType, LastPriceAnimationMode } from 'lightweight-charts'
import { toLwcData } from '../composables/useLightweightChart'
import WalletDepositModule from '../components/WalletDepositModule.vue' import WalletDepositModule from '../components/WalletDepositModule.vue'
import WalletDepositHistoryPanel from '../components/WalletDepositHistoryPanel.vue' import WalletDepositHistoryPanel from '../components/WalletDepositHistoryPanel.vue'
import WithdrawDialog from '../components/WithdrawDialog.vue' import WithdrawDialog from '../components/WithdrawDialog.vue'
import MarqueeTitle from '../components/MarqueeTitle.vue' import MarqueeTitle from '../components/MarqueeTitle.vue'
import PnlCurveCard from '../components/PnlCurveCard.vue'
import { useUserStore } from '../stores/user' import { useUserStore } from '../stores/user'
import { useLocaleStore } from '../stores/locale' import { useLocaleStore } from '../stores/locale'
import { useAuthError } from '../composables/useAuthError' import { useAuthError } from '../composables/useAuthError'
@ -467,14 +479,6 @@ const { formatAuthError } = useAuthError()
const localeStore = useLocaleStore() const localeStore = useLocaleStore()
const portfolioBalance = computed(() => userStore.totalAssetValue) const portfolioBalance = computed(() => userStore.totalAssetValue)
const positionsValueText = computed(() => userStore.positionsValue.toFixed(2)) const positionsValueText = computed(() => userStore.positionsValue.toFixed(2))
const profitLoss = ref('0.00')
const plRange = ref('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' | 'depositHistory' | 'withdrawals'>( const activeTab = ref<'positions' | 'orders' | 'history' | 'depositHistory' | 'withdrawals'>(
'positions', 'positions',
) )
@ -1257,96 +1261,6 @@ function shareHistory(id: string) {
// TODO: // TODO:
} }
const plChartRef = ref<HTMLElement | null>(null)
let plChartInstance: ReturnType<typeof createChart> | null = null
let plChartSeries: ReturnType<ReturnType<typeof createChart>['addSeries']> | null = null
/**
* 资产变化折线图数据 [timestamp_ms, pnl]
* 暂无接口时返回真实格式的时间序列数值均为 0有接口后改为从 API 拉取并在此处做分时过滤
*/
function getPlChartData(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][] = []
for (let i = count; i >= 0; i--) {
const t = now - i * stepMs
data.push([t, 0])
}
return data
}
const plChartData = ref<[number, number][]>([])
function updatePlChart() {
plChartData.value = getPlChartData(plRange.value)
const last = plChartData.value[plChartData.value.length - 1]
if (last != null) profitLoss.value = last[1].toFixed(2)
if (plChartSeries) plChartSeries.setData(toLwcData(plChartData.value))
}
function initPlChart() {
if (!plChartRef.value) return
plChartData.value = getPlChartData(plRange.value)
const last = plChartData.value[plChartData.value.length - 1]
if (last != null) profitLoss.value = last[1].toFixed(2)
const el = plChartRef.value
plChartInstance = createChart(el, {
width: el.clientWidth,
height: 160,
layout: {
background: { color: '#ffffff' },
textColor: '#9ca3af',
fontFamily: 'inherit',
fontSize: 9,
attributionLogo: false,
},
grid: { vertLines: { visible: false }, horzLines: { color: '#f3f4f6' } },
rightPriceScale: { borderVisible: false },
timeScale: { borderVisible: false },
handleScroll: false,
handleScale: false,
crosshair: { vertLine: { labelVisible: false }, horzLine: { labelVisible: true } },
})
const color = '#4b5563'
plChartSeries = plChartInstance.addSeries(AreaSeries, {
topColor: color + '40',
bottomColor: color + '08',
lineColor: color,
lineWidth: 2,
lineType: LineType.Curved,
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
priceFormat: { type: 'price', precision: 2 },
})
plChartSeries.setData(toLwcData(plChartData.value))
}
const handleResize = () => {
if (plChartInstance && plChartRef.value) plChartInstance.resize(plChartRef.value.clientWidth, 160)
}
watch(plRange, () => updatePlChart())
// //
watch( watch(
() => localeStore.currentLocale, () => localeStore.currentLocale,
@ -1358,17 +1272,6 @@ watch(
onMounted(() => { onMounted(() => {
if (!USE_MOCK_WALLET && activeTab.value === 'positions') loadPositionList() if (!USE_MOCK_WALLET && activeTab.value === 'positions') loadPositionList()
nextTick(() => {
initPlChart()
})
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
plChartInstance?.remove()
plChartInstance = null
plChartSeries = null
}) })
function onWithdrawSuccess() { function onWithdrawSuccess() {
@ -1391,13 +1294,11 @@ async function submitAuthorize() {
<style scoped lang="scss"> <style scoped lang="scss">
.wallet-container { .wallet-container {
width: 100%; width: 100%;
max-width: 100%; max-width: 1080px;
margin: 0 auto; margin: 0 auto;
padding: 16px; padding: 16px;
background: #fcfcfc; background: #fcfcfc;
box-sizing: border-box; box-sizing: border-box;
margin: 0 auto;
max-width: 1440px;
margin-top: $header-height; margin-top: $header-height;
} }
@ -1414,46 +1315,122 @@ async function submitAuthorize() {
line-height: 1.2; line-height: 1.2;
} }
.wallet-top-row {
display: flex;
flex-direction: column;
gap: 16px;
}
.wallet-pnl-card {
flex: 1;
min-width: 0;
height: 100%;
}
@media (min-width: 900px) {
.wallet-top-row {
flex-direction: row;
align-items: stretch;
}
.portfolio-card,
.wallet-pnl-card {
flex: 1 1 0;
}
}
.wallet-card { .wallet-card {
padding: 16px; padding: 20px;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
border-radius: 16px; border-radius: 12px;
height: auto; height: auto;
box-shadow: none; box-shadow: none;
background: #fff;
} }
.design-tu-asset { .portfolio-card {
background: #5b5bd6; display: flex;
border: 0; flex-direction: column;
gap: 16px;
min-width: 0;
} }
.design-tu-asset .card-title, .portfolio-head {
.design-tu-asset .card-value { display: flex;
color: #fff; align-items: flex-start;
justify-content: space-between;
gap: 12px;
} }
.design-tu-asset .card-title { .portfolio-title-wrap {
display: inline-flex;
align-items: center;
gap: 6px;
}
.portfolio-title-icon {
color: #6b7280;
}
.portfolio-available {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
.portfolio-available-label {
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: #9ca3af;
line-height: 1.2;
} }
.design-tu-actions { .portfolio-available-value {
font-size: 24px;
font-weight: 600;
color: #111827;
line-height: 1.2;
}
.portfolio-actions {
margin-top: auto;
gap: 10px; gap: 10px;
} }
.design-tu-action { .portfolio-deposit {
flex: 1; flex: 1;
height: 72px; display: flex;
border-radius: 12px; min-width: 0;
border: 1px solid #e5e7eb; }
background: #fff;
color: #111827; .portfolio-deposit :deep(.action-btn),
font-size: 20px; .portfolio-withdraw-btn {
flex: 1;
width: 100%;
height: 44px;
border-radius: 8px;
text-transform: none;
font-size: 14px;
font-weight: 600;
letter-spacing: 0;
}
.portfolio-deposit :deep(.action-btn) {
border: none !important;
background: #2e5cff !important;
color: #fff !important;
}
.portfolio-deposit :deep(.v-btn__content) {
font-size: 14px;
font-weight: 600; font-weight: 600;
} }
.design-tu-action :deep(.v-btn__content) { .portfolio-withdraw-btn {
font-size: 20px; border: 1px solid #e5e7eb !important;
background: #fff !important;
color: #111827 !important;
} }
.design-tu-settlement { .design-tu-settlement {
@ -1471,43 +1448,30 @@ async function submitAuthorize() {
padding: 0 12px; padding: 0 12px;
} }
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.card-title { .card-title {
font-size: 14px; font-size: 15px;
font-weight: 500; font-weight: 600;
color: #111827; color: #111827;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
line-height: 1.2;
} }
.card-value { .card-value {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
flex: 1;
} }
.main-balance { .main-balance {
font-size: 32px; font-size: 32px;
font-weight: 700; font-weight: 700;
color: inherit; color: #111827;
line-height: 1.1; line-height: 1.1;
} }
.available-balance {
font-size: 16px;
font-weight: 500;
opacity: 0.8;
color: inherit;
line-height: 1.2;
}
.card-actions { .card-actions {
display: flex; display: flex;
gap: 10px; gap: 10px;