新增:交易详情
This commit is contained in:
parent
e882c74449
commit
6f633178de
32
package-lock.json
generated
32
package-lock.json
generated
@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/font": "^7.4.47",
|
"@mdi/font": "^7.4.47",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
"echarts": "^6.0.0",
|
||||||
"ethers": "^6.16.0",
|
"ethers": "^6.16.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"siwe": "^3.0.0",
|
"siwe": "^3.0.0",
|
||||||
@ -4196,6 +4197,22 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/echarts": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0",
|
||||||
|
"zrender": "6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/echarts/node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/editorconfig": {
|
"node_modules/editorconfig": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmmirror.com/editorconfig/-/editorconfig-1.0.4.tgz",
|
"resolved": "https://registry.npmmirror.com/editorconfig/-/editorconfig-1.0.4.tgz",
|
||||||
@ -9163,6 +9180,21 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zrender": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zrender/node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||||
|
"license": "0BSD"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/font": "^7.4.47",
|
"@mdi/font": "^7.4.47",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
"echarts": "^6.0.0",
|
||||||
"ethers": "^6.16.0",
|
"ethers": "^6.16.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"siwe": "^3.0.0",
|
"siwe": "^3.0.0",
|
||||||
|
|||||||
@ -48,7 +48,7 @@ const progressPercentage = computed(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 4px;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-track {
|
.progress-track {
|
||||||
@ -67,6 +67,6 @@ const progressPercentage = computed(() => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #0066cc;
|
background-color: #0066cc;
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
border-radius: 4px;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-card class="market-card" elevation="0" :rounded="'lg'">
|
<v-card class="market-card" elevation="0" :rounded="'lg'" @click="navigateToDetail">
|
||||||
<div class="market-card-content">
|
<div class="market-card-content">
|
||||||
<!-- Top Section -->
|
<!-- Top Section -->
|
||||||
<div class="top-section">
|
<div class="top-section">
|
||||||
<!-- Market Image and Title -->
|
<!-- Market Image and Title -->
|
||||||
<div class="image-title-container">
|
<div class="image-title-container">
|
||||||
<v-avatar class="market-image" :size="40" :color="'#f0f0f0'" :rounded="'sm'">
|
<v-avatar class="market-image" :size="40" color="#f0f0f0" rounded="sm">
|
||||||
<!-- Placeholder for market image -->
|
<v-img v-if="props.imageUrl" :src="props.imageUrl" cover />
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
<v-card-title class="market-title" :text="true" :shrink="true">
|
<v-card-title class="market-title" :text="true" :shrink="true">
|
||||||
{{ marketTitle }}
|
{{ marketTitle }}
|
||||||
@ -55,6 +55,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
marketTitle: {
|
marketTitle: {
|
||||||
@ -69,6 +72,25 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '$155k Vol.',
|
default: '$155k Vol.',
|
||||||
},
|
},
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: '1',
|
||||||
|
},
|
||||||
|
/** 市场图片 URL,由卡片传入供详情页展示 */
|
||||||
|
imageUrl: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
/** 分类标签,如 "Economy · World" */
|
||||||
|
category: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
/** 结算/到期日期,如 "Mar 31, 2026" */
|
||||||
|
expiresAt: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算进度条颜色,从红色(0%)到绿色(100%)
|
// 计算进度条颜色,从红色(0%)到绿色(100%)
|
||||||
@ -77,6 +99,21 @@ const progressColor = computed(() => {
|
|||||||
const hue = (props.chanceValue / 100) * 120
|
const hue = (props.chanceValue / 100) * 120
|
||||||
return `hsl(${hue}, 100%, 50%)`
|
return `hsl(${hue}, 100%, 50%)`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 跳转到交易详情页面,将标题、图片等通过 query 传入
|
||||||
|
const navigateToDetail = () => {
|
||||||
|
router.push({
|
||||||
|
path: `/trade-detail/${props.id}`,
|
||||||
|
query: {
|
||||||
|
title: props.marketTitle,
|
||||||
|
imageUrl: props.imageUrl || undefined,
|
||||||
|
category: props.category || undefined,
|
||||||
|
marketInfo: props.marketInfo || undefined,
|
||||||
|
expiresAt: props.expiresAt || undefined,
|
||||||
|
chance: String(props.chanceValue),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -88,6 +125,14 @@ const progressColor = computed(() => {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #e7e7e7;
|
border: 1px solid #e7e7e7;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
border-color: #d0d0d0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-card-content {
|
.market-card-content {
|
||||||
|
|||||||
@ -395,8 +395,11 @@ const maxBidsTotal = computed(() => {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 深度图:固定宽度,进度条长度表示深度比例,不占满整行 */
|
||||||
.order-progress {
|
.order-progress {
|
||||||
flex: 1;
|
flex: 0 0 auto;
|
||||||
|
width: 120px;
|
||||||
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asks-price {
|
.asks-price {
|
||||||
@ -454,11 +457,8 @@ const maxBidsTotal = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.order-progress {
|
.order-progress {
|
||||||
flex: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.depth-chart {
|
|
||||||
width: 80px;
|
width: 80px;
|
||||||
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-list-header-price {
|
.order-list-header-price {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
import Home from '../views/Home.vue'
|
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'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
@ -20,6 +21,11 @@ const router = createRouter({
|
|||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
component: Login
|
component: Login
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/trade-detail/:id',
|
||||||
|
name: 'trade-detail',
|
||||||
|
component: TradeDetail
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -126,6 +126,8 @@ const connectWithWallet = async () => {
|
|||||||
const issuedAt = new Date().toISOString()
|
const issuedAt = new Date().toISOString()
|
||||||
const provider = new BrowserProvider(window.ethereum)
|
const provider = new BrowserProvider(window.ethereum)
|
||||||
const signer = await provider.getSigner()
|
const signer = await provider.getSigner()
|
||||||
|
const nonce = new Date().getTime().toString()
|
||||||
|
|
||||||
const siwe = new SiweMessage({
|
const siwe = new SiweMessage({
|
||||||
scheme,
|
scheme,
|
||||||
domain,
|
domain,
|
||||||
@ -134,6 +136,7 @@ const connectWithWallet = async () => {
|
|||||||
uri: origin,
|
uri: origin,
|
||||||
version: '1',
|
version: '1',
|
||||||
chainId: chainId,
|
chainId: chainId,
|
||||||
|
nonce,
|
||||||
})
|
})
|
||||||
const message = siwe.prepareMessage()
|
const message = siwe.prepareMessage()
|
||||||
// 去掉message 头部的http://
|
// 去掉message 头部的http://
|
||||||
@ -145,7 +148,7 @@ const connectWithWallet = async () => {
|
|||||||
// console.log('Cleaned Siwe message:', cleanedMessage)
|
// console.log('Cleaned Siwe message:', cleanedMessage)
|
||||||
|
|
||||||
// Generate nonce for security
|
// Generate nonce for security
|
||||||
const nonce = siwe.nonce //Math.floor(Math.random() * 1000000).toString()
|
// const nonce = siwe.nonce //Math.floor(Math.random() * 1000000).toString()
|
||||||
|
|
||||||
// Construct Siwe message according to EIP-4361 standard
|
// Construct Siwe message according to EIP-4361 standard
|
||||||
// const message = `${domain} wants you to sign in with your Ethereum account:\n${walletAddress}\n\n${statement}\n\nURI: ${origin}\nVersion: ${version}\nChain ID: ${parseInt(chainId, 16)}\nNonce: ${nonce}\nIssued At: ${issuedAt}`
|
// const message = `${domain} wants you to sign in with your Ethereum account:\n${walletAddress}\n\n${statement}\n\nURI: ${origin}\nVersion: ${version}\nChain ID: ${parseInt(chainId, 16)}\nNonce: ${nonce}\nIssued At: ${issuedAt}`
|
||||||
@ -205,34 +208,10 @@ const connectWithWallet = async () => {
|
|||||||
min-height: calc(100vh - 80px);
|
min-height: calc(100vh - 80px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PC端样式 - 固定大小 */
|
.login-card {
|
||||||
@media (min-width: 600px) {
|
border-radius: 12px;
|
||||||
.login-card {
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 12px;
|
padding: 24px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 24px;
|
|
||||||
max-width: 480px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 手机端样式 - 铺满页面 */
|
|
||||||
@media (max-width: 599px) {
|
|
||||||
.login-container {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-row {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card {
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
padding: 32px 24px;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-title {
|
.login-title {
|
||||||
|
|||||||
600
src/views/TradeDetail.vue
Normal file
600
src/views/TradeDetail.vue
Normal file
@ -0,0 +1,600 @@
|
|||||||
|
<template>
|
||||||
|
<v-container class="trade-detail-container">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<!-- 分时图卡片(Polymarket 样式) -->
|
||||||
|
<v-card class="chart-card polymarket-chart" elevation="0" rounded="lg">
|
||||||
|
<!-- 顶部:标题、当前概率、Past / 日期 -->
|
||||||
|
<div class="chart-header">
|
||||||
|
<h1 class="chart-title">{{ marketTitle }}</h1>
|
||||||
|
<div class="chart-controls-row">
|
||||||
|
<v-btn variant="text" size="small" class="past-btn">Past ▾</v-btn>
|
||||||
|
<v-btn class="date-pill" size="small" rounded="pill">{{ resolutionDate }}</v-btn>
|
||||||
|
</div>
|
||||||
|
<div class="chart-chance">{{ currentChance }}% chance</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<div ref="chartContainerRef" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部:成交量/到期日 | 时间粒度 -->
|
||||||
|
<div class="chart-footer">
|
||||||
|
<div class="chart-footer-left">
|
||||||
|
<span class="chart-volume">{{ marketVolume }} Vol.</span>
|
||||||
|
<span v-if="marketExpiresAt" class="chart-expires">| {{ marketExpiresAt }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-time-ranges">
|
||||||
|
<v-btn
|
||||||
|
v-for="r in timeRanges"
|
||||||
|
:key="r.value"
|
||||||
|
:class="['time-range-btn', { active: selectedTimeRange === r.value }]"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="selectTimeRange(r.value)"
|
||||||
|
>
|
||||||
|
{{ r.label }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Order Book Section -->
|
||||||
|
<v-card class="order-book-card" elevation="0" rounded="lg" style="margin-top: 32px">
|
||||||
|
<OrderBook />
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import type { ECharts } from 'echarts'
|
||||||
|
import OrderBook from '../components/OrderBook.vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分时图服务端推送数据格式约定
|
||||||
|
*
|
||||||
|
* 图表所需单点格式(与 ECharts 时间轴一致):
|
||||||
|
* [timestamp, value]
|
||||||
|
* - timestamp: number,Unix 毫秒时间戳
|
||||||
|
* - value: number,当前概率/价格(如 0–100 的百分比)
|
||||||
|
*
|
||||||
|
* 服务端可选用两种推送方式:
|
||||||
|
*
|
||||||
|
* 1) 全量快照(切换时间粒度或首次加载时):
|
||||||
|
* { range?: string, data: [number, number][] }
|
||||||
|
* - range: 可选,'1H' | '6H' | '1D' | '1W' | '1M' | 'ALL'
|
||||||
|
* - data: 按时间升序的 [timestamp, value] 数组
|
||||||
|
*
|
||||||
|
* 2) 增量点(实时推送最新点):
|
||||||
|
* { point: [number, number] }
|
||||||
|
* - point: 单点 [timestamp, value],前端会追加并按当前 range 保留最近 N 个点
|
||||||
|
*
|
||||||
|
* 示例(WebSocket 消息体):
|
||||||
|
* 全量: { "data": [[1707206400000, 25.5], [1707210000000, 26.1], ...] }
|
||||||
|
* 增量: { "point": [1707213600000, 27.2] }
|
||||||
|
*/
|
||||||
|
export type ChartPoint = [number, number]
|
||||||
|
export type ChartSnapshot = { range?: string; data: ChartPoint[] }
|
||||||
|
export type ChartIncrement = { point: ChartPoint }
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// 卡片传入:标题、成交量、到期日
|
||||||
|
const marketTitle = computed(
|
||||||
|
() => (route.query.title as string) || 'U.S. anti-cartel ground operation in Mexico by March 31?'
|
||||||
|
)
|
||||||
|
const marketVolume = computed(() => (route.query.marketInfo as string) || '$398,719')
|
||||||
|
const marketExpiresAt = computed(() => (route.query.expiresAt as string) || 'Mar 31, 2026')
|
||||||
|
const resolutionDate = computed(() => {
|
||||||
|
const s = marketExpiresAt.value
|
||||||
|
return s ? s.replace(/,?\s*\d{4}$/, '').trim() || 'Mar 31' : 'Mar 31'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 时间粒度
|
||||||
|
const selectedTimeRange = ref('ALL')
|
||||||
|
const timeRanges = [
|
||||||
|
{ label: '1H', value: '1H' },
|
||||||
|
{ label: '6H', value: '6H' },
|
||||||
|
{ label: '1D', value: '1D' },
|
||||||
|
{ label: '1W', value: '1W' },
|
||||||
|
{ label: '1M', value: '1M' },
|
||||||
|
{ label: 'ALL', value: 'ALL' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 时间轴数据 [timestamp, value][],按粒度生成
|
||||||
|
function generateData(range: string): [number, number][] {
|
||||||
|
const now = Date.now()
|
||||||
|
const data: [number, number][] = []
|
||||||
|
let stepMs: number
|
||||||
|
let count: number
|
||||||
|
switch (range) {
|
||||||
|
case '1H':
|
||||||
|
stepMs = 60 * 1000
|
||||||
|
count = 60
|
||||||
|
break
|
||||||
|
case '6H':
|
||||||
|
stepMs = 10 * 60 * 1000
|
||||||
|
count = 36
|
||||||
|
break
|
||||||
|
case '1D':
|
||||||
|
stepMs = 60 * 60 * 1000
|
||||||
|
count = 24
|
||||||
|
break
|
||||||
|
case '1W':
|
||||||
|
stepMs = 24 * 60 * 60 * 1000
|
||||||
|
count = 7
|
||||||
|
break
|
||||||
|
case '1M':
|
||||||
|
case 'ALL':
|
||||||
|
stepMs = 24 * 60 * 60 * 1000
|
||||||
|
count = 30
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
stepMs = 60 * 60 * 1000
|
||||||
|
count = 24
|
||||||
|
}
|
||||||
|
let value = 15 + Math.random() * 25
|
||||||
|
for (let i = count; i >= 0; i--) {
|
||||||
|
const t = now - i * stepMs
|
||||||
|
value = Math.max(10, Math.min(90, value + (Math.random() - 0.5) * 6))
|
||||||
|
data.push([t, Math.round(value * 10) / 10])
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartContainerRef = ref<HTMLElement | null>(null)
|
||||||
|
const data = ref<[number, number][]>([])
|
||||||
|
let chartInstance: ECharts | null = null
|
||||||
|
let dynamicInterval: number | undefined
|
||||||
|
|
||||||
|
const currentChance = computed(() => {
|
||||||
|
const d = data.value
|
||||||
|
const last = d.length > 0 ? d[d.length - 1] : undefined
|
||||||
|
return last != null ? last[1] : 20
|
||||||
|
})
|
||||||
|
|
||||||
|
const lineColor = '#2563eb'
|
||||||
|
|
||||||
|
function buildOption(chartData: [number, number][]) {
|
||||||
|
const lastIndex = chartData.length - 1
|
||||||
|
return {
|
||||||
|
animation: false,
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
formatter: function (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[1] : p.value
|
||||||
|
return (
|
||||||
|
date.getDate() +
|
||||||
|
'/' +
|
||||||
|
(date.getMonth() + 1) +
|
||||||
|
'/' +
|
||||||
|
date.getFullYear() +
|
||||||
|
' : ' +
|
||||||
|
val +
|
||||||
|
'%'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
axisPointer: { animation: false },
|
||||||
|
},
|
||||||
|
grid: { left: 16, right: 48, top: 16, bottom: 28, containLabel: false },
|
||||||
|
xAxis: {
|
||||||
|
type: 'time',
|
||||||
|
axisLine: { lineStyle: { color: '#e5e7eb' } },
|
||||||
|
axisLabel: { color: '#6b7280', fontSize: 11 },
|
||||||
|
axisTick: { show: false },
|
||||||
|
splitLine: { show: false },
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
position: 'right',
|
||||||
|
boundaryGap: [0, '100%'],
|
||||||
|
axisLine: { show: false },
|
||||||
|
axisTick: { show: false },
|
||||||
|
axisLabel: { color: '#6b7280', fontSize: 11, formatter: '{value}%' },
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: { type: 'dashed', color: '#e5e7eb' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'probability',
|
||||||
|
type: 'line',
|
||||||
|
showSymbol: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: (_: unknown, params: { dataIndex?: number }) =>
|
||||||
|
params?.dataIndex === lastIndex ? 8 : 0,
|
||||||
|
data: chartData,
|
||||||
|
smooth: true,
|
||||||
|
lineStyle: { width: 2, color: lineColor },
|
||||||
|
itemStyle: { color: lineColor, borderColor: '#fff', borderWidth: 2 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initChart() {
|
||||||
|
if (!chartContainerRef.value) return
|
||||||
|
data.value = generateData(selectedTimeRange.value)
|
||||||
|
chartInstance = echarts.init(chartContainerRef.value)
|
||||||
|
chartInstance.setOption(buildOption(data.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChartData() {
|
||||||
|
data.value = generateData(selectedTimeRange.value)
|
||||||
|
if (chartInstance) chartInstance.setOption(buildOption(data.value), { replaceMerge: ['series'] })
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTimeRange(range: string) {
|
||||||
|
selectedTimeRange.value = range
|
||||||
|
updateChartData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaxPoints(range: string): number {
|
||||||
|
switch (range) {
|
||||||
|
case '1H': return 60
|
||||||
|
case '6H': return 36
|
||||||
|
case '1D': return 24
|
||||||
|
case '1W': return 7
|
||||||
|
case '1M':
|
||||||
|
case 'ALL': return 30
|
||||||
|
default: return 24
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDynamicUpdate() {
|
||||||
|
dynamicInterval = window.setInterval(() => {
|
||||||
|
const list = [...data.value]
|
||||||
|
const last = list[list.length - 1]
|
||||||
|
if (!last) return
|
||||||
|
const nextVal = Math.max(10, Math.min(90, last[1] + (Math.random() - 0.5) * 4))
|
||||||
|
const nextT = Date.now()
|
||||||
|
list.push([nextT, Math.round(nextVal * 10) / 10])
|
||||||
|
const max = getMaxPoints(selectedTimeRange.value)
|
||||||
|
data.value = list.slice(-max)
|
||||||
|
if (chartInstance) chartInstance.setOption(buildOption(data.value), { replaceMerge: ['series'] })
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDynamicUpdate() {
|
||||||
|
if (dynamicInterval) {
|
||||||
|
clearInterval(dynamicInterval)
|
||||||
|
dynamicInterval = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedTimeRange, () => updateChartData())
|
||||||
|
|
||||||
|
const handleResize = () => chartInstance?.resize()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initChart()
|
||||||
|
startDynamicUpdate()
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopDynamicUpdate()
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
chartInstance?.dispose()
|
||||||
|
chartInstance = null
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.trade-detail-container {
|
||||||
|
padding: 24px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trade-detail-card {
|
||||||
|
padding: 32px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e7e7e7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trade-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #000000;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-image-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-image {
|
||||||
|
border: 2px solid #e7e7e7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chance-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chance-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chance-label {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #808080;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-yes {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #e6f9e6;
|
||||||
|
height: 56px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-text-yes {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #008000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-no {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #ffe6e6;
|
||||||
|
height: 56px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-text-no {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-info-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
padding: 24px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-section {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #000000;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
height: 56px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Polymarket 样式分时图卡片 */
|
||||||
|
.chart-card.polymarket-chart {
|
||||||
|
margin-top: 32px;
|
||||||
|
padding: 20px 24px 16px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e7e7e7;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-controls-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.past-btn {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-pill {
|
||||||
|
background-color: #111827 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-chance {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 320px;
|
||||||
|
min-height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-footer-left {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-expires {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-time-ranges {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-range-btn {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: none;
|
||||||
|
min-width: 36px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-range-btn.active {
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-range-btn:hover {
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Order Book Card Styles */
|
||||||
|
.order-book-card {
|
||||||
|
padding: 0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e7e7e7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 599px) {
|
||||||
|
.trade-detail-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trade-detail-card {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trade-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-image-container {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chance-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-info-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card.polymarket-chart {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive order book adjustments */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.order-book-card {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user