新增:对接顶部分类接口
This commit is contained in:
parent
f83f0100e0
commit
0aa04471f1
@ -7,6 +7,20 @@ description: Interprets the XTrader API from the Swagger 2.0 spec at https://api
|
|||||||
|
|
||||||
规范来源:[OpenAPI 规范](https://api.xtrader.vip/swagger/doc.json)(Swagger 2.0)。Swagger UI:<https://api.xtrader.vip/swagger/index.html>。
|
规范来源:[OpenAPI 规范](https://api.xtrader.vip/swagger/doc.json)(Swagger 2.0)。Swagger UI:<https://api.xtrader.vip/swagger/index.html>。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 强制执行:接口接入三步流程
|
||||||
|
|
||||||
|
**接入任意 XTrader 接口时,必须严格按以下顺序执行,不得跳过、不得调换、不得合并步骤。**
|
||||||
|
|
||||||
|
| 步骤 | 动作 | 强制要求 |
|
||||||
|
|------|------|----------|
|
||||||
|
| **第一步** | 从 doc.json 整理并**在对话中输出**请求参数表、响应参数表、definitions 完整结构 | 在输出第一步结果**之前**,不得写任何业务代码;必须用 `mcp_web_fetch` 或 curl 获取 doc.json,解析 `paths` 与 `definitions` |
|
||||||
|
| **第二步** | 根据第一步整理出的结构,在 `src/api/` 中定义 TypeScript 类型 | 必须等第一步输出完成后再执行;Model 必须与 definitions 对应 |
|
||||||
|
| **第三步** | 实现请求函数并集成到页面 | 必须等第二步完成后再执行 |
|
||||||
|
|
||||||
|
**违反后果**:若跳过第一步直接写代码,会导致类型与接口文档不一致、遗漏字段或误用参数。
|
||||||
|
|
||||||
## 规范地址与格式
|
## 规范地址与格式
|
||||||
|
|
||||||
- **规范 URL**:`https://api.xtrader.vip/swagger/doc.json`
|
- **规范 URL**:`https://api.xtrader.vip/swagger/doc.json`
|
||||||
@ -49,11 +63,11 @@ Swagger UI 页面(如 [PmEvent findPmEvent](https://api.xtrader.vip/swagger/in
|
|||||||
|
|
||||||
## 接口集成规范(必须按顺序执行)
|
## 接口集成规范(必须按顺序执行)
|
||||||
|
|
||||||
接入任意 XTrader 接口时,**必须**按以下顺序执行,不得跳过或调换。
|
接入任意 XTrader 接口时,**必须**按以下顺序执行,不得跳过或调换。**第一步的输出必须在对话中可见,才能进入第二步。**
|
||||||
|
|
||||||
### 第一步:列出请求参数与响应参数
|
### 第一步:列出请求参数与响应参数
|
||||||
|
|
||||||
在写代码前,先从 doc.json 中整理并列出:
|
**在写任何代码之前**,先用 `mcp_web_fetch` 或 curl 获取 `https://api.xtrader.vip/swagger/doc.json`,然后整理并**在对话中输出**:
|
||||||
|
|
||||||
1. **请求参数**
|
1. **请求参数**
|
||||||
- **Query**:`paths["<path>"]["<method>"].parameters` 中 `in: "query"` 的项(name、type、required、description)。
|
- **Query**:`paths["<path>"]["<method>"].parameters` 中 `in: "query"` 的项(name、type、required、description)。
|
||||||
@ -63,9 +77,9 @@ Swagger UI 页面(如 [PmEvent findPmEvent](https://api.xtrader.vip/swagger/in
|
|||||||
2. **响应参数**
|
2. **响应参数**
|
||||||
- 取 `paths["<path>"]["<method>"].responses["200"].schema`。
|
- 取 `paths["<path>"]["<method>"].responses["200"].schema`。
|
||||||
- 若有 `allOf`,合并得到根结构(通常为 `code`、`data`、`msg`)。
|
- 若有 `allOf`,合并得到根结构(通常为 `code`、`data`、`msg`)。
|
||||||
- 对 `data` 及其他嵌套对象的 `$ref`,到 `definitions` 中查完整结构并列出字段(名称、类型、说明)。
|
- 对 `data` 及其他嵌套对象的 `$ref`,到 `definitions` 中查完整结构并**列出所有字段**(名称、类型、说明)。
|
||||||
|
|
||||||
输出形式可为表格或结构化列表,便于第二步写类型。
|
**输出形式**:表格或结构化列表,便于第二步写类型。**未完成第一步输出前,禁止进入第二步。**
|
||||||
|
|
||||||
### 第二步:根据响应数据创建 Model 类
|
### 第二步:根据响应数据创建 Model 类
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,9 @@
|
|||||||
# 连接测试服务器时复制本文件为 .env 并取消下一行注释:
|
# 连接测试服务器时复制本文件为 .env 并取消下一行注释:
|
||||||
# VITE_API_BASE_URL=http://192.168.3.21:8888
|
# VITE_API_BASE_URL=http://192.168.3.21:8888
|
||||||
#
|
#
|
||||||
|
# WebSocket 路径前缀(可选)。若 CLOB WS 在 /api/clob/ws 且 API base 无 /api,则设:
|
||||||
|
# VITE_WS_PATH_PREFIX=/api
|
||||||
|
#
|
||||||
# 生产打包/部署时自动使用 .env.production 中的 https://api.xtrader.vip
|
# 生产打包/部署时自动使用 .env.production 中的 https://api.xtrader.vip
|
||||||
|
|
||||||
# SSH 部署(npm run deploy),不配置时使用默认值
|
# SSH 部署(npm run deploy),不配置时使用默认值
|
||||||
|
|||||||
20
src/App.vue
20
src/App.vue
@ -32,7 +32,12 @@ onMounted(() => {
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
<v-app-bar-title v-if="currentRoute === '/'">PolyMarket</v-app-bar-title>
|
<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>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -54,11 +59,14 @@ onMounted(() => {
|
|||||||
</v-avatar>
|
</v-avatar>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-list density="compact">
|
<v-list density="compact">
|
||||||
<v-list-item :title="userStore.user?.nickName || userStore.user?.userName || 'User'" disabled />
|
<v-list-item
|
||||||
<v-list-item title="退出登录" @click="userStore.logout()" />
|
:title="userStore.user?.nickName || userStore.user?.userName || 'User'"
|
||||||
</v-list>
|
disabled
|
||||||
</v-menu>
|
/>
|
||||||
|
<v-list-item title="退出登录" @click="userStore.logout()" />
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
</template>
|
</template>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<v-main>
|
<v-main>
|
||||||
|
|||||||
@ -1,5 +1,29 @@
|
|||||||
import { get } from './request'
|
import { get } from './request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口返回的 PmTag 结构(definitions polymarket.PmTag)
|
||||||
|
* doc.json definitions["polymarket.PmTag"] 完整字段
|
||||||
|
* 注:主分类树可能返回 children(递归同结构),definitions 未声明,需兼容
|
||||||
|
*/
|
||||||
|
export interface PmTagMainItem {
|
||||||
|
ID?: number
|
||||||
|
label?: string
|
||||||
|
slug?: string
|
||||||
|
createdAt?: string
|
||||||
|
createdBy?: number
|
||||||
|
forceShow?: boolean
|
||||||
|
publishedAt?: string
|
||||||
|
requiresTranslation?: boolean
|
||||||
|
updatedAt?: string
|
||||||
|
updatedBy?: number
|
||||||
|
/** 树形子节点,后端可能返回,definitions 未声明 */
|
||||||
|
children?: PmTagMainItem[]
|
||||||
|
/** 以下为后端可能返回的扩展字段,definitions 未声明 */
|
||||||
|
icon?: string
|
||||||
|
sectionTitle?: string
|
||||||
|
forceHide?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/** 分类树节点(与后端返回结构一致) */
|
/** 分类树节点(与后端返回结构一致) */
|
||||||
export interface CategoryTreeNode {
|
export interface CategoryTreeNode {
|
||||||
id: string
|
id: string
|
||||||
@ -92,9 +116,11 @@ export interface CategoryTreeResponse {
|
|||||||
* data 可能为数组,或 { list: [] } 等格式,统一转为 CategoryTreeNode[]
|
* data 可能为数组,或 { list: [] } 等格式,统一转为 CategoryTreeNode[]
|
||||||
*/
|
*/
|
||||||
export async function getCategoryTree(): Promise<CategoryTreeResponse> {
|
export async function getCategoryTree(): Promise<CategoryTreeResponse> {
|
||||||
const res = await get<{ code: number; data: CategoryTreeNode[] | { list?: CategoryTreeNode[] }; msg: string }>(
|
const res = await get<{
|
||||||
'/PmTag/getPmTagPublic'
|
code: number
|
||||||
)
|
data: CategoryTreeNode[] | { list?: CategoryTreeNode[] }
|
||||||
|
msg: string
|
||||||
|
}>('/PmTag/getPmTagPublic')
|
||||||
let data: CategoryTreeNode[] = []
|
let data: CategoryTreeNode[] = []
|
||||||
const raw = res.data
|
const raw = res.data
|
||||||
if (Array.isArray(raw)) {
|
if (Array.isArray(raw)) {
|
||||||
@ -108,3 +134,32 @@ export async function getCategoryTree(): Promise<CategoryTreeResponse> {
|
|||||||
}
|
}
|
||||||
return { code: res.code, data, msg: res.msg }
|
return { code: res.code, data, msg: res.msg }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将 PmTagMainItem 转为 CategoryTreeNode */
|
||||||
|
function mapPmTagToTreeNode(item: PmTagMainItem): CategoryTreeNode {
|
||||||
|
const rawId = item.ID
|
||||||
|
const id = rawId != null ? String(rawId) : (item.slug ?? item.label ?? '')
|
||||||
|
const children = Array.isArray(item.children) ? item.children.map(mapPmTagToTreeNode) : undefined
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
label: item.label ?? '',
|
||||||
|
slug: item.slug ?? '',
|
||||||
|
icon: item.icon,
|
||||||
|
sectionTitle: item.sectionTitle,
|
||||||
|
forceShow: item.forceShow,
|
||||||
|
forceHide: item.forceHide,
|
||||||
|
children: children?.length ? children : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取主分类(首页顶部分类 Tab 数据)
|
||||||
|
* GET /PmTag/getPmTagMain
|
||||||
|
*
|
||||||
|
* 不需要鉴权,返回带 children 的树形结构,最多三层
|
||||||
|
*/
|
||||||
|
export async function getPmTagMain(): Promise<CategoryTreeResponse> {
|
||||||
|
const res = await get<{ code: number; data: PmTagMainItem[]; msg: string }>('/PmTag/getPmTagMain')
|
||||||
|
const data: CategoryTreeNode[] = Array.isArray(res.data) ? res.data.map(mapPmTagToTreeNode) : []
|
||||||
|
return { code: res.code, data, msg: res.msg }
|
||||||
|
}
|
||||||
|
|||||||
@ -82,7 +82,7 @@ export function getMarketId(m: PmEventMarketItem | null | undefined): string | u
|
|||||||
/** 从市场项取 clobTokenId,outcomeIndex 0=Yes/第一选项,1=No/第二选项 */
|
/** 从市场项取 clobTokenId,outcomeIndex 0=Yes/第一选项,1=No/第二选项 */
|
||||||
export function getClobTokenId(
|
export function getClobTokenId(
|
||||||
m: PmEventMarketItem | null | undefined,
|
m: PmEventMarketItem | null | undefined,
|
||||||
outcomeIndex: 0 | 1 = 0
|
outcomeIndex: 0 | 1 = 0,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
if (!m?.clobTokenIds?.length) return undefined
|
if (!m?.clobTokenIds?.length) return undefined
|
||||||
const id = m.clobTokenIds[outcomeIndex]
|
const id = m.clobTokenIds[outcomeIndex]
|
||||||
@ -131,7 +131,7 @@ export interface GetPmEventListParams {
|
|||||||
* tokenid 对应 market.clobTokenIds 中的值,可传单个或数组
|
* tokenid 对应 market.clobTokenIds 中的值,可传单个或数组
|
||||||
*/
|
*/
|
||||||
export async function getPmEventPublic(
|
export async function getPmEventPublic(
|
||||||
params: GetPmEventListParams = {}
|
params: GetPmEventListParams = {},
|
||||||
): Promise<PmEventListResponse> {
|
): Promise<PmEventListResponse> {
|
||||||
const { page = 1, pageSize = 10, keyword, createdAtRange, tokenid } = params
|
const { page = 1, pageSize = 10, keyword, createdAtRange, tokenid } = params
|
||||||
const query: Record<string, string | number | string[] | undefined> = {
|
const query: Record<string, string | number | string[] | undefined> = {
|
||||||
@ -182,7 +182,7 @@ export interface FindPmEventParams {
|
|||||||
*/
|
*/
|
||||||
export async function findPmEvent(
|
export async function findPmEvent(
|
||||||
params: FindPmEventParams,
|
params: FindPmEventParams,
|
||||||
config?: { headers?: Record<string, string> }
|
config?: { headers?: Record<string, string> },
|
||||||
): Promise<PmEventDetailResponse> {
|
): Promise<PmEventDetailResponse> {
|
||||||
const query: Record<string, string | number> = {}
|
const query: Record<string, string | number> = {}
|
||||||
if (params.id != null) query.ID = params.id
|
if (params.id != null) query.ID = params.id
|
||||||
@ -254,7 +254,7 @@ export function setEventListCache(data: {
|
|||||||
page: number
|
page: number
|
||||||
total: number
|
total: number
|
||||||
pageSize: number
|
pageSize: number
|
||||||
}) {
|
}) {
|
||||||
eventListCache = data
|
eventListCache = data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,7 +304,11 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
|||||||
try {
|
try {
|
||||||
const d = new Date(item.endDate)
|
const d = new Date(item.endDate)
|
||||||
if (!Number.isNaN(d.getTime())) {
|
if (!Number.isNaN(d.getTime())) {
|
||||||
expiresAt = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
expiresAt = d.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
expiresAt = item.endDate
|
expiresAt = item.endDate
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export interface ClobSubmitOrderRequest {
|
|||||||
*/
|
*/
|
||||||
export async function pmOrderPlace(
|
export async function pmOrderPlace(
|
||||||
data: ClobSubmitOrderRequest,
|
data: ClobSubmitOrderRequest,
|
||||||
config?: { headers?: Record<string, string> }
|
config?: { headers?: Record<string, string> },
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
return post<ApiResponse>('/clob/gateway/submitOrder', data, config)
|
return post<ApiResponse>('/clob/gateway/submitOrder', data, config)
|
||||||
}
|
}
|
||||||
@ -52,7 +52,7 @@ export interface ClobCancelOrderRequest {
|
|||||||
*/
|
*/
|
||||||
export async function pmCancelOrder(
|
export async function pmCancelOrder(
|
||||||
data: ClobCancelOrderRequest,
|
data: ClobCancelOrderRequest,
|
||||||
config?: { headers?: Record<string, string> }
|
config?: { headers?: Record<string, string> },
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
return post<ApiResponse>('/clob/gateway/cancelOrder', data, config)
|
return post<ApiResponse>('/clob/gateway/cancelOrder', data, config)
|
||||||
}
|
}
|
||||||
@ -85,7 +85,7 @@ export interface PmMarketMergeRequest {
|
|||||||
*/
|
*/
|
||||||
export async function pmMarketMerge(
|
export async function pmMarketMerge(
|
||||||
data: PmMarketMergeRequest,
|
data: PmMarketMergeRequest,
|
||||||
config?: { headers?: Record<string, string> }
|
config?: { headers?: Record<string, string> },
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
return post<ApiResponse>('/PmMarket/merge', data, config)
|
return post<ApiResponse>('/PmMarket/merge', data, config)
|
||||||
}
|
}
|
||||||
@ -97,7 +97,7 @@ export async function pmMarketMerge(
|
|||||||
*/
|
*/
|
||||||
export async function pmMarketSplit(
|
export async function pmMarketSplit(
|
||||||
data: PmMarketSplitRequest,
|
data: PmMarketSplitRequest,
|
||||||
config?: { headers?: Record<string, string> }
|
config?: { headers?: Record<string, string> },
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
return post<ApiResponse>('/PmMarket/split', data, config)
|
return post<ApiResponse>('/PmMarket/split', data, config)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -124,14 +124,47 @@ export const MOCK_EVENT_LIST: PmEventListItem[] = [
|
|||||||
endDate: '2026-02-10T23:59:59.000Z',
|
endDate: '2026-02-10T23:59:59.000Z',
|
||||||
new: true,
|
new: true,
|
||||||
markets: [
|
markets: [
|
||||||
{ ID: 90051, question: '260-279', outcomes: ['Yes', 'No'], outcomePrices: [0.01, 0.99], volume: 120000 },
|
{
|
||||||
{ ID: 90052, question: '280-299', outcomes: ['Yes', 'No'], outcomePrices: [0.42, 0.58], volume: 939379 },
|
ID: 90051,
|
||||||
{ ID: 90053, question: '300-319', outcomes: ['Yes', 'No'], outcomePrices: [0.45, 0.55], volume: 850000 },
|
question: '260-279',
|
||||||
{ ID: 90054, question: '320-339', outcomes: ['Yes', 'No'], outcomePrices: [0.16, 0.84], volume: 320000 },
|
outcomes: ['Yes', 'No'],
|
||||||
{ ID: 90055, question: '340-359', outcomes: ['Yes', 'No'], outcomePrices: [0.08, 0.92], volume: 180000 },
|
outcomePrices: [0.01, 0.99],
|
||||||
|
volume: 120000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 90052,
|
||||||
|
question: '280-299',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.42, 0.58],
|
||||||
|
volume: 939379,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 90053,
|
||||||
|
question: '300-319',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.45, 0.55],
|
||||||
|
volume: 850000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 90054,
|
||||||
|
question: '320-339',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.16, 0.84],
|
||||||
|
volume: 320000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 90055,
|
||||||
|
question: '340-359',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.08, 0.92],
|
||||||
|
volume: 180000,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
series: [{ ID: 5, title: 'Culture', ticker: 'CULTURE' }],
|
series: [{ ID: 5, title: 'Culture', ticker: 'CULTURE' }],
|
||||||
tags: [{ label: 'Tech', slug: 'tech' }, { label: 'Twitter', slug: 'twitter' }],
|
tags: [
|
||||||
|
{ label: 'Tech', slug: 'tech' },
|
||||||
|
{ label: 'Twitter', slug: 'twitter' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
// 6. 多 market:总统候选人胜选概率(多候选人)
|
// 6. 多 market:总统候选人胜选概率(多候选人)
|
||||||
{
|
{
|
||||||
@ -145,9 +178,27 @@ export const MOCK_EVENT_LIST: PmEventListItem[] = [
|
|||||||
endDate: '2028-11-07T23:59:59.000Z',
|
endDate: '2028-11-07T23:59:59.000Z',
|
||||||
new: true,
|
new: true,
|
||||||
markets: [
|
markets: [
|
||||||
{ ID: 90061, question: 'Democrat nominee', outcomes: ['Yes', 'No'], outcomePrices: [0.48, 0.52], volume: 3200000 },
|
{
|
||||||
{ ID: 90062, question: 'Republican nominee', outcomes: ['Yes', 'No'], outcomePrices: [0.45, 0.55], volume: 3100000 },
|
ID: 90061,
|
||||||
{ ID: 90063, question: 'Third party / Independent', outcomes: ['Yes', 'No'], outcomePrices: [0.07, 0.93], volume: 800000 },
|
question: 'Democrat nominee',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.48, 0.52],
|
||||||
|
volume: 3200000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 90062,
|
||||||
|
question: 'Republican nominee',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.45, 0.55],
|
||||||
|
volume: 3100000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 90063,
|
||||||
|
question: 'Third party / Independent',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.07, 0.93],
|
||||||
|
volume: 800000,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
series: [{ ID: 6, title: 'Politics', ticker: 'POL' }],
|
series: [{ ID: 6, title: 'Politics', ticker: 'POL' }],
|
||||||
tags: [{ label: 'Election', slug: 'election' }],
|
tags: [{ label: 'Election', slug: 'election' }],
|
||||||
@ -164,10 +215,34 @@ export const MOCK_EVENT_LIST: PmEventListItem[] = [
|
|||||||
endDate: '2026-05-31T23:59:59.000Z',
|
endDate: '2026-05-31T23:59:59.000Z',
|
||||||
new: false,
|
new: false,
|
||||||
markets: [
|
markets: [
|
||||||
{ ID: 90071, question: 'Boston Celtics', outcomes: ['Yes', 'No'], outcomePrices: [0.35, 0.65], volume: 280000 },
|
{
|
||||||
{ ID: 90072, question: 'Milwaukee Bucks', outcomes: ['Yes', 'No'], outcomePrices: [0.28, 0.72], volume: 220000 },
|
ID: 90071,
|
||||||
{ ID: 90073, question: 'Philadelphia 76ers', outcomes: ['Yes', 'No'], outcomePrices: [0.18, 0.82], volume: 150000 },
|
question: 'Boston Celtics',
|
||||||
{ ID: 90074, question: 'New York Knicks', outcomes: ['Yes', 'No'], outcomePrices: [0.12, 0.88], volume: 120000 },
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.35, 0.65],
|
||||||
|
volume: 280000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 90072,
|
||||||
|
question: 'Milwaukee Bucks',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.28, 0.72],
|
||||||
|
volume: 220000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 90073,
|
||||||
|
question: 'Philadelphia 76ers',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.18, 0.82],
|
||||||
|
volume: 150000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 90074,
|
||||||
|
question: 'New York Knicks',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.12, 0.88],
|
||||||
|
volume: 120000,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
series: [{ ID: 7, title: 'Sports', ticker: 'SPORT' }],
|
series: [{ ID: 7, title: 'Sports', ticker: 'SPORT' }],
|
||||||
tags: [{ label: 'NBA', slug: 'nba' }],
|
tags: [{ label: 'NBA', slug: 'nba' }],
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* 请求基础 URL,默认 https://api.xtrader.vip,可通过环境变量 VITE_API_BASE_URL 覆盖
|
* 请求基础 URL,默认 https://api.xtrader.vip,可通过环境变量 VITE_API_BASE_URL 覆盖
|
||||||
*/
|
*/
|
||||||
const BASE_URL = typeof import.meta !== 'undefined' && (import.meta as unknown as { env?: Record<string, string> }).env?.VITE_API_BASE_URL
|
const BASE_URL =
|
||||||
? (import.meta as unknown as { env: Record<string, string> }).env.VITE_API_BASE_URL
|
typeof import.meta !== 'undefined' &&
|
||||||
: 'https://api.xtrader.vip'
|
(import.meta as unknown as { env?: Record<string, string> }).env?.VITE_API_BASE_URL
|
||||||
|
? (import.meta as unknown as { env: Record<string, string> }).env.VITE_API_BASE_URL
|
||||||
|
: 'https://api.xtrader.vip'
|
||||||
|
|
||||||
export interface RequestConfig {
|
export interface RequestConfig {
|
||||||
/** 请求头,如 { 'x-token': token, 'x-user-id': userId } */
|
/** 请求头,如 { 'x-token': token, 'x-user-id': userId } */
|
||||||
@ -16,7 +18,7 @@ export interface RequestConfig {
|
|||||||
export async function get<T = unknown>(
|
export async function get<T = unknown>(
|
||||||
path: string,
|
path: string,
|
||||||
params?: Record<string, string | number | string[] | undefined>,
|
params?: Record<string, string | number | string[] | undefined>,
|
||||||
config?: RequestConfig
|
config?: RequestConfig,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const url = new URL(path, BASE_URL || window.location.origin)
|
const url = new URL(path, BASE_URL || window.location.origin)
|
||||||
if (params) {
|
if (params) {
|
||||||
@ -46,7 +48,7 @@ export async function get<T = unknown>(
|
|||||||
export async function post<T = unknown>(
|
export async function post<T = unknown>(
|
||||||
path: string,
|
path: string,
|
||||||
body?: unknown,
|
body?: unknown,
|
||||||
config?: RequestConfig
|
config?: RequestConfig,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const url = new URL(path, BASE_URL || window.location.origin)
|
const url = new URL(path, BASE_URL || window.location.origin)
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
|
|||||||
@ -26,7 +26,9 @@ export function formatUsdcBalance(raw: string): string {
|
|||||||
* 查询 USDC 余额,需鉴权(x-token)
|
* 查询 USDC 余额,需鉴权(x-token)
|
||||||
* amount、available 需除以 1000000 得到实际 USDC
|
* amount、available 需除以 1000000 得到实际 USDC
|
||||||
*/
|
*/
|
||||||
export async function getUsdcBalance(authHeaders: Record<string, string>): Promise<GetUsdcBalanceResponse> {
|
export async function getUsdcBalance(
|
||||||
|
authHeaders: Record<string, string>,
|
||||||
|
): Promise<GetUsdcBalanceResponse> {
|
||||||
const res = await get<GetUsdcBalanceResponse>('/user/getUsdcBalance', undefined, {
|
const res = await get<GetUsdcBalanceResponse>('/user/getUsdcBalance', undefined, {
|
||||||
headers: authHeaders,
|
headers: authHeaders,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -81,13 +81,7 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
<div class="qr-wrap">
|
<div class="qr-wrap">
|
||||||
<img
|
<img :src="qrCodeUrl" alt="QR Code" class="qr-img" width="120" height="120" />
|
||||||
:src="qrCodeUrl"
|
|
||||||
alt="QR Code"
|
|
||||||
class="qr-img"
|
|
||||||
width="120"
|
|
||||||
height="120"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -101,7 +95,10 @@
|
|||||||
<span class="step-title">Connect Exchange</span>
|
<span class="step-title">Connect Exchange</span>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="!exchangeConnected">
|
<template v-if="!exchangeConnected">
|
||||||
<p class="connect-desc">Connect your wallet to deposit. Send USDC or ETH to your deposit address after connecting.</p>
|
<p class="connect-desc">
|
||||||
|
Connect your wallet to deposit. Send USDC or ETH to your deposit address after
|
||||||
|
connecting.
|
||||||
|
</p>
|
||||||
<div class="wallet-buttons">
|
<div class="wallet-buttons">
|
||||||
<v-btn
|
<v-btn
|
||||||
class="wallet-btn"
|
class="wallet-btn"
|
||||||
@ -114,23 +111,11 @@
|
|||||||
<v-icon start>mdi-wallet</v-icon>
|
<v-icon start>mdi-wallet</v-icon>
|
||||||
MetaMask
|
MetaMask
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn class="wallet-btn" variant="outlined" rounded="lg" block disabled>
|
||||||
class="wallet-btn"
|
|
||||||
variant="outlined"
|
|
||||||
rounded="lg"
|
|
||||||
block
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
<v-icon start>mdi-wallet</v-icon>
|
<v-icon start>mdi-wallet</v-icon>
|
||||||
Coinbase Wallet (Coming soon)
|
Coinbase Wallet (Coming soon)
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn class="wallet-btn" variant="outlined" rounded="lg" block disabled>
|
||||||
class="wallet-btn"
|
|
||||||
variant="outlined"
|
|
||||||
rounded="lg"
|
|
||||||
block
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
<v-icon start>mdi-wallet</v-icon>
|
<v-icon start>mdi-wallet</v-icon>
|
||||||
WalletConnect (Coming soon)
|
WalletConnect (Coming soon)
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@ -168,7 +153,7 @@ const props = withDefaults(
|
|||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
balance: string
|
balance: string
|
||||||
}>(),
|
}>(),
|
||||||
{ balance: '0.00' }
|
{ balance: '0.00' },
|
||||||
)
|
)
|
||||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||||
|
|
||||||
@ -209,7 +194,9 @@ async function copyAddress() {
|
|||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(DEPOSIT_ADDRESS)
|
await navigator.clipboard.writeText(DEPOSIT_ADDRESS)
|
||||||
copied.value = true
|
copied.value = true
|
||||||
setTimeout(() => { copied.value = false }, 2000)
|
setTimeout(() => {
|
||||||
|
copied.value = false
|
||||||
|
}, 2000)
|
||||||
} catch {
|
} catch {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
@ -238,7 +225,7 @@ watch(
|
|||||||
step.value = 'method'
|
step.value = 'method'
|
||||||
exchangeConnected.value = false
|
exchangeConnected.value = false
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -127,7 +127,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="carousel-dots">
|
<div class="carousel-dots">
|
||||||
<button
|
<button
|
||||||
v-for="(_, idx) in (props.outcomes ?? [])"
|
v-for="(_, idx) in props.outcomes ?? []"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
type="button"
|
type="button"
|
||||||
:class="['carousel-dot', { active: currentSlide === idx }]"
|
:class="['carousel-dot', { active: currentSlide === idx }]"
|
||||||
@ -171,7 +171,13 @@ const router = useRouter()
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
openTrade: [
|
openTrade: [
|
||||||
side: 'yes' | 'no',
|
side: 'yes' | 'no',
|
||||||
market?: { id: string; title: string; marketId?: string; outcomeTitle?: string; clobTokenIds?: string[] }
|
market?: {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
marketId?: string
|
||||||
|
outcomeTitle?: string
|
||||||
|
clobTokenIds?: string[]
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@ -215,12 +221,10 @@ const props = withDefaults(
|
|||||||
noLabel: 'No',
|
noLabel: 'No',
|
||||||
isNew: false,
|
isNew: false,
|
||||||
marketId: undefined,
|
marketId: undefined,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const isMulti = computed(
|
const isMulti = computed(() => props.displayType === 'multi' && (props.outcomes?.length ?? 0) > 1)
|
||||||
() => props.displayType === 'multi' && (props.outcomes?.length ?? 0) > 1
|
|
||||||
)
|
|
||||||
const currentSlide = ref(0)
|
const currentSlide = ref(0)
|
||||||
const outcomeCount = computed(() => (props.outcomes ?? []).length)
|
const outcomeCount = computed(() => (props.outcomes ?? []).length)
|
||||||
|
|
||||||
@ -256,14 +260,14 @@ const semiProgressColor = computed(() => {
|
|||||||
return rgbToHex(
|
return rgbToHex(
|
||||||
COLOR_RED.r + (COLOR_ORANGE_YELLOW.r - COLOR_RED.r) * u,
|
COLOR_RED.r + (COLOR_ORANGE_YELLOW.r - COLOR_RED.r) * u,
|
||||||
COLOR_RED.g + (COLOR_ORANGE_YELLOW.g - COLOR_RED.g) * u,
|
COLOR_RED.g + (COLOR_ORANGE_YELLOW.g - COLOR_RED.g) * u,
|
||||||
COLOR_RED.b + (COLOR_ORANGE_YELLOW.b - COLOR_RED.b) * u
|
COLOR_RED.b + (COLOR_ORANGE_YELLOW.b - COLOR_RED.b) * u,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const u = (t - 0.5) * 2
|
const u = (t - 0.5) * 2
|
||||||
return rgbToHex(
|
return rgbToHex(
|
||||||
COLOR_ORANGE_YELLOW.r + (COLOR_GREEN.r - COLOR_ORANGE_YELLOW.r) * u,
|
COLOR_ORANGE_YELLOW.r + (COLOR_GREEN.r - COLOR_ORANGE_YELLOW.r) * u,
|
||||||
COLOR_ORANGE_YELLOW.g + (COLOR_GREEN.g - COLOR_ORANGE_YELLOW.g) * u,
|
COLOR_ORANGE_YELLOW.g + (COLOR_GREEN.g - COLOR_ORANGE_YELLOW.g) * u,
|
||||||
COLOR_ORANGE_YELLOW.b + (COLOR_GREEN.b - COLOR_ORANGE_YELLOW.b) * u
|
COLOR_ORANGE_YELLOW.b + (COLOR_GREEN.b - COLOR_ORANGE_YELLOW.b) * u,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -613,7 +617,9 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
|
|||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
color: #333;
|
color: #333;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s, color 0.2s;
|
transition:
|
||||||
|
background-color 0.2s,
|
||||||
|
color 0.2s;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.carousel-arrow:hover:not(:disabled) {
|
.carousel-arrow:hover:not(:disabled) {
|
||||||
|
|||||||
@ -367,7 +367,6 @@ const maxBidsTotal = computed(() => {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.asks-label,
|
.asks-label,
|
||||||
.bids-label {
|
.bids-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@ -86,7 +86,12 @@
|
|||||||
|
|
||||||
<p v-if="orderError" class="order-error">{{ orderError }}</p>
|
<p v-if="orderError" class="order-error">{{ orderError }}</p>
|
||||||
<!-- Action Button -->
|
<!-- Action Button -->
|
||||||
<v-btn class="action-btn" :loading="orderLoading" :disabled="orderLoading" @click="submitOrder">
|
<v-btn
|
||||||
|
class="action-btn"
|
||||||
|
:loading="orderLoading"
|
||||||
|
:disabled="orderLoading"
|
||||||
|
@click="submitOrder"
|
||||||
|
>
|
||||||
{{ actionButtonText }}
|
{{ actionButtonText }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
@ -133,12 +138,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Deposit Button -->
|
<!-- Deposit Button -->
|
||||||
<v-btn
|
<v-btn class="deposit-btn" @click="deposit"> Deposit </v-btn>
|
||||||
class="deposit-btn"
|
|
||||||
@click="deposit"
|
|
||||||
>
|
|
||||||
Deposit
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -276,7 +276,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="orderError" class="order-error">{{ orderError }}</p>
|
<p v-if="orderError" class="order-error">{{ orderError }}</p>
|
||||||
<v-btn class="action-btn" :loading="orderLoading" :disabled="orderLoading" @click="submitOrder">
|
<v-btn
|
||||||
|
class="action-btn"
|
||||||
|
:loading="orderLoading"
|
||||||
|
:disabled="orderLoading"
|
||||||
|
@click="submitOrder"
|
||||||
|
>
|
||||||
{{ actionButtonText }}
|
{{ actionButtonText }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
@ -299,40 +304,95 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-list>
|
<v-list>
|
||||||
<v-list-item @click="limitType = 'Market'"><v-list-item-title>Market</v-list-item-title></v-list-item>
|
<v-list-item @click="limitType = 'Market'"
|
||||||
<v-list-item @click="limitType = 'Limit'"><v-list-item-title>Limit</v-list-item-title></v-list-item>
|
><v-list-item-title>Market</v-list-item-title></v-list-item
|
||||||
|
>
|
||||||
|
<v-list-item @click="limitType = 'Limit'"
|
||||||
|
><v-list-item-title>Limit</v-list-item-title></v-list-item
|
||||||
|
>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
<v-list-item @click="openMergeDialog"><v-list-item-title>Merge</v-list-item-title></v-list-item>
|
<v-list-item @click="openMergeDialog"
|
||||||
<v-list-item @click="openSplitDialog"><v-list-item-title>Split</v-list-item-title></v-list-item>
|
><v-list-item-title>Merge</v-list-item-title></v-list-item
|
||||||
|
>
|
||||||
|
<v-list-item @click="openSplitDialog"
|
||||||
|
><v-list-item-title>Split</v-list-item-title></v-list-item
|
||||||
|
>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="isMarketMode">
|
<template v-if="isMarketMode">
|
||||||
<template v-if="balance > 0">
|
<template v-if="balance > 0">
|
||||||
<div class="price-options hide-in-mobile-sheet">
|
<div class="price-options hide-in-mobile-sheet">
|
||||||
<v-btn class="yes-btn" :class="{ active: selectedOption === 'yes' }" text @click="handleOptionChange('yes')">Yes {{ yesPriceCents }}¢</v-btn>
|
<v-btn
|
||||||
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ noPriceCents }}¢</v-btn>
|
class="yes-btn"
|
||||||
|
:class="{ active: selectedOption === 'yes' }"
|
||||||
|
text
|
||||||
|
@click="handleOptionChange('yes')"
|
||||||
|
>Yes {{ yesPriceCents }}¢</v-btn
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
class="no-btn"
|
||||||
|
:class="{ active: selectedOption === 'no' }"
|
||||||
|
text
|
||||||
|
@click="handleOptionChange('no')"
|
||||||
|
>No {{ noPriceCents }}¢</v-btn
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="total-section">
|
<div class="total-section">
|
||||||
<template v-if="activeTab === 'buy'">
|
<template v-if="activeTab === 'buy'">
|
||||||
<div class="total-row"><span class="label">Total</span><span class="total-value">${{ totalPrice }}</span></div>
|
<div class="total-row">
|
||||||
<div class="total-row"><span class="label">To win</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> $20</span></div>
|
<span class="label">Total</span><span class="total-value">${{ totalPrice }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="total-row">
|
||||||
|
<span class="label">To win</span
|
||||||
|
><span class="to-win-value"
|
||||||
|
><v-icon size="16" color="green">mdi-currency-usd</v-icon> $20</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="total-row"><span class="label">You'll receive</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ totalPrice }}</span></div>
|
<div class="total-row">
|
||||||
|
<span class="label">You'll receive</span
|
||||||
|
><span class="to-win-value"
|
||||||
|
><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{
|
||||||
|
totalPrice
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="orderError" class="order-error">{{ orderError }}</p>
|
<p v-if="orderError" class="order-error">{{ orderError }}</p>
|
||||||
<v-btn class="action-btn" :loading="orderLoading" :disabled="orderLoading" @click="submitOrder">{{ actionButtonText }}</v-btn>
|
<v-btn
|
||||||
|
class="action-btn"
|
||||||
|
:loading="orderLoading"
|
||||||
|
:disabled="orderLoading"
|
||||||
|
@click="submitOrder"
|
||||||
|
>{{ actionButtonText }}</v-btn
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="price-options hide-in-mobile-sheet">
|
<div class="price-options hide-in-mobile-sheet">
|
||||||
<v-btn class="yes-btn" :class="{ active: selectedOption === 'yes' }" text @click="handleOptionChange('yes')">Yes {{ yesPriceCents }}¢</v-btn>
|
<v-btn
|
||||||
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ noPriceCents }}¢</v-btn>
|
class="yes-btn"
|
||||||
|
:class="{ active: selectedOption === 'yes' }"
|
||||||
|
text
|
||||||
|
@click="handleOptionChange('yes')"
|
||||||
|
>Yes {{ yesPriceCents }}¢</v-btn
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
class="no-btn"
|
||||||
|
:class="{ active: selectedOption === 'no' }"
|
||||||
|
text
|
||||||
|
@click="handleOptionChange('no')"
|
||||||
|
>No {{ noPriceCents }}¢</v-btn
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="amount-header">
|
<div class="amount-header">
|
||||||
<div><span class="label amount-label">Amount</span><span class="balance-label">Balance ${{ balance.toFixed(2) }}</span></div>
|
<div>
|
||||||
|
<span class="label amount-label">Amount</span
|
||||||
|
><span class="balance-label">Balance ${{ balance.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
<div class="amount-value">${{ amount.toFixed(2) }}</div>
|
<div class="amount-value">${{ amount.toFixed(2) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="amount-buttons">
|
<div class="amount-buttons">
|
||||||
@ -347,16 +407,44 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="price-options hide-in-mobile-sheet">
|
<div class="price-options hide-in-mobile-sheet">
|
||||||
<v-btn class="yes-btn" :class="{ active: selectedOption === 'yes' }" text @click="handleOptionChange('yes')">Yes {{ yesPriceCents }}¢</v-btn>
|
<v-btn
|
||||||
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ noPriceCents }}¢</v-btn>
|
class="yes-btn"
|
||||||
|
:class="{ active: selectedOption === 'yes' }"
|
||||||
|
text
|
||||||
|
@click="handleOptionChange('yes')"
|
||||||
|
>Yes {{ yesPriceCents }}¢</v-btn
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
class="no-btn"
|
||||||
|
:class="{ active: selectedOption === 'no' }"
|
||||||
|
text
|
||||||
|
@click="handleOptionChange('no')"
|
||||||
|
>No {{ noPriceCents }}¢</v-btn
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group limit-price-group">
|
<div class="input-group limit-price-group">
|
||||||
<div class="limit-price-header">
|
<div class="limit-price-header">
|
||||||
<span class="label">Limit Price</span>
|
<span class="label">Limit Price</span>
|
||||||
<div class="price-input">
|
<div class="price-input">
|
||||||
<v-btn class="adjust-btn" icon @click="decreasePrice"><v-icon>mdi-minus</v-icon></v-btn>
|
<v-btn class="adjust-btn" icon @click="decreasePrice"
|
||||||
<v-text-field :model-value="limitPrice" type="number" min="0" max="1" step="0.01" class="price-input-field" hide-details density="compact" @update:model-value="onLimitPriceInput" @keydown="onLimitPriceKeydown" @paste="onLimitPricePaste"></v-text-field>
|
><v-icon>mdi-minus</v-icon></v-btn
|
||||||
<v-btn class="adjust-btn" icon @click="increasePrice"><v-icon>mdi-plus</v-icon></v-btn>
|
>
|
||||||
|
<v-text-field
|
||||||
|
:model-value="limitPrice"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
class="price-input-field"
|
||||||
|
hide-details
|
||||||
|
density="compact"
|
||||||
|
@update:model-value="onLimitPriceInput"
|
||||||
|
@keydown="onLimitPriceKeydown"
|
||||||
|
@paste="onLimitPricePaste"
|
||||||
|
></v-text-field>
|
||||||
|
<v-btn class="adjust-btn" icon @click="increasePrice"
|
||||||
|
><v-icon>mdi-plus</v-icon></v-btn
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -364,7 +452,17 @@
|
|||||||
<div class="shares-header">
|
<div class="shares-header">
|
||||||
<span class="label">Shares</span>
|
<span class="label">Shares</span>
|
||||||
<div class="shares-input">
|
<div class="shares-input">
|
||||||
<v-text-field :model-value="shares" type="number" min="1" class="shares-input-field" hide-details density="compact" @update:model-value="onSharesInput" @keydown="onSharesKeydown" @paste="onSharesPaste"></v-text-field>
|
<v-text-field
|
||||||
|
:model-value="shares"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
class="shares-input-field"
|
||||||
|
hide-details
|
||||||
|
density="compact"
|
||||||
|
@update:model-value="onSharesInput"
|
||||||
|
@keydown="onSharesKeydown"
|
||||||
|
@paste="onSharesPaste"
|
||||||
|
></v-text-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="activeTab === 'buy'" class="shares-buttons">
|
<div v-if="activeTab === 'buy'" class="shares-buttons">
|
||||||
@ -382,7 +480,12 @@
|
|||||||
<div class="input-group expiration-group">
|
<div class="input-group expiration-group">
|
||||||
<div class="expiration-header">
|
<div class="expiration-header">
|
||||||
<span class="label">Set expiration</span>
|
<span class="label">Set expiration</span>
|
||||||
<v-switch v-model="expirationEnabled" class="expiration-switch" hide-details color="primary"></v-switch>
|
<v-switch
|
||||||
|
v-model="expirationEnabled"
|
||||||
|
class="expiration-switch"
|
||||||
|
hide-details
|
||||||
|
color="primary"
|
||||||
|
></v-switch>
|
||||||
</div>
|
</div>
|
||||||
<v-select
|
<v-select
|
||||||
v-if="expirationEnabled"
|
v-if="expirationEnabled"
|
||||||
@ -395,15 +498,33 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="total-section">
|
<div class="total-section">
|
||||||
<template v-if="activeTab === 'buy'">
|
<template v-if="activeTab === 'buy'">
|
||||||
<div class="total-row"><span class="label">Total</span><span class="total-value">${{ totalPrice }}</span></div>
|
<div class="total-row">
|
||||||
<div class="total-row"><span class="label">To win</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> $20</span></div>
|
<span class="label">Total</span><span class="total-value">${{ totalPrice }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="total-row">
|
||||||
|
<span class="label">To win</span
|
||||||
|
><span class="to-win-value"
|
||||||
|
><v-icon size="16" color="green">mdi-currency-usd</v-icon> $20</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="total-row"><span class="label">You'll receive</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ totalPrice }}</span></div>
|
<div class="total-row">
|
||||||
|
<span class="label">You'll receive</span
|
||||||
|
><span class="to-win-value"
|
||||||
|
><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ totalPrice }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="orderError" class="order-error">{{ orderError }}</p>
|
<p v-if="orderError" class="order-error">{{ orderError }}</p>
|
||||||
<v-btn class="action-btn" :loading="orderLoading" :disabled="orderLoading" @click="submitOrder">{{ actionButtonText }}</v-btn>
|
<v-btn
|
||||||
|
class="action-btn"
|
||||||
|
:loading="orderLoading"
|
||||||
|
:disabled="orderLoading"
|
||||||
|
@click="submitOrder"
|
||||||
|
>{{ actionButtonText }}</v-btn
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
@ -451,64 +572,148 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-list>
|
<v-list>
|
||||||
<v-list-item @click="limitType = 'Market'"><v-list-item-title>Market</v-list-item-title></v-list-item>
|
<v-list-item @click="limitType = 'Market'"
|
||||||
<v-list-item @click="limitType = 'Limit'"><v-list-item-title>Limit</v-list-item-title></v-list-item>
|
><v-list-item-title>Market</v-list-item-title></v-list-item
|
||||||
|
>
|
||||||
|
<v-list-item @click="limitType = 'Limit'"
|
||||||
|
><v-list-item-title>Limit</v-list-item-title></v-list-item
|
||||||
|
>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
<v-list-item @click="openMergeDialog"><v-list-item-title>Merge</v-list-item-title></v-list-item>
|
<v-list-item @click="openMergeDialog"
|
||||||
<v-list-item @click="openSplitDialog"><v-list-item-title>Split</v-list-item-title></v-list-item>
|
><v-list-item-title>Merge</v-list-item-title></v-list-item
|
||||||
|
>
|
||||||
|
<v-list-item @click="openSplitDialog"
|
||||||
|
><v-list-item-title>Split</v-list-item-title></v-list-item
|
||||||
|
>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="isMarketMode">
|
<template v-if="isMarketMode">
|
||||||
<template v-if="balance > 0">
|
<template v-if="balance > 0">
|
||||||
<div class="price-options hide-in-mobile-sheet">
|
<div class="price-options hide-in-mobile-sheet">
|
||||||
<v-btn class="yes-btn" :class="{ active: selectedOption === 'yes' }" text @click="handleOptionChange('yes')">Yes {{ yesPriceCents }}¢</v-btn>
|
<v-btn
|
||||||
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ noPriceCents }}¢</v-btn>
|
class="yes-btn"
|
||||||
|
:class="{ active: selectedOption === 'yes' }"
|
||||||
|
text
|
||||||
|
@click="handleOptionChange('yes')"
|
||||||
|
>Yes {{ yesPriceCents }}¢</v-btn
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
class="no-btn"
|
||||||
|
:class="{ active: selectedOption === 'no' }"
|
||||||
|
text
|
||||||
|
@click="handleOptionChange('no')"
|
||||||
|
>No {{ noPriceCents }}¢</v-btn
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="total-section">
|
<div class="total-section">
|
||||||
<template v-if="activeTab === 'buy'">
|
<template v-if="activeTab === 'buy'">
|
||||||
<div class="total-row"><span class="label">Total</span><span class="total-value">${{ totalPrice }}</span></div>
|
<div class="total-row">
|
||||||
<div class="total-row"><span class="label">To win</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> $20</span></div>
|
<span class="label">Total</span
|
||||||
|
><span class="total-value">${{ totalPrice }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="total-row">
|
||||||
|
<span class="label">To win</span
|
||||||
|
><span class="to-win-value"
|
||||||
|
><v-icon size="16" color="green">mdi-currency-usd</v-icon> $20</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="total-row"><span class="label">You'll receive</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ totalPrice }}</span></div>
|
<div class="total-row">
|
||||||
|
<span class="label">You'll receive</span
|
||||||
|
><span class="to-win-value"
|
||||||
|
><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{
|
||||||
|
totalPrice
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<p v-if="orderError" class="order-error">{{ orderError }}</p>
|
||||||
|
<v-btn
|
||||||
|
class="action-btn"
|
||||||
|
:loading="orderLoading"
|
||||||
|
:disabled="orderLoading"
|
||||||
|
@click="submitOrder"
|
||||||
|
>{{ actionButtonText }}</v-btn
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="price-options hide-in-mobile-sheet">
|
||||||
|
<v-btn
|
||||||
|
class="yes-btn"
|
||||||
|
:class="{ active: selectedOption === 'yes' }"
|
||||||
|
text
|
||||||
|
@click="handleOptionChange('yes')"
|
||||||
|
>Yes {{ yesPriceCents }}¢</v-btn
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
class="no-btn"
|
||||||
|
:class="{ active: selectedOption === 'no' }"
|
||||||
|
text
|
||||||
|
@click="handleOptionChange('no')"
|
||||||
|
>No {{ noPriceCents }}¢</v-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="amount-header">
|
||||||
|
<div>
|
||||||
|
<span class="label amount-label">Amount</span
|
||||||
|
><span class="balance-label">Balance ${{ balance.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="amount-value">${{ amount.toFixed(2) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="amount-buttons">
|
||||||
|
<v-btn class="amount-btn" @click="adjustAmount(1)">+$1</v-btn>
|
||||||
|
<v-btn class="amount-btn" @click="adjustAmount(20)">+$20</v-btn>
|
||||||
|
<v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn>
|
||||||
|
<v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<v-btn class="deposit-btn" @click="deposit">Deposit</v-btn>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
|
||||||
<p v-if="orderError" class="order-error">{{ orderError }}</p>
|
|
||||||
<v-btn class="action-btn" :loading="orderLoading" :disabled="orderLoading" @click="submitOrder">{{ actionButtonText }}</v-btn>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="price-options hide-in-mobile-sheet">
|
<div class="price-options hide-in-mobile-sheet">
|
||||||
<v-btn class="yes-btn" :class="{ active: selectedOption === 'yes' }" text @click="handleOptionChange('yes')">Yes {{ yesPriceCents }}¢</v-btn>
|
<v-btn
|
||||||
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ noPriceCents }}¢</v-btn>
|
class="yes-btn"
|
||||||
|
:class="{ active: selectedOption === 'yes' }"
|
||||||
|
text
|
||||||
|
@click="handleOptionChange('yes')"
|
||||||
|
>Yes {{ yesPriceCents }}¢</v-btn
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
class="no-btn"
|
||||||
|
:class="{ active: selectedOption === 'no' }"
|
||||||
|
text
|
||||||
|
@click="handleOptionChange('no')"
|
||||||
|
>No {{ noPriceCents }}¢</v-btn
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group limit-price-group">
|
||||||
<div class="amount-header">
|
|
||||||
<div><span class="label amount-label">Amount</span><span class="balance-label">Balance ${{ balance.toFixed(2) }}</span></div>
|
|
||||||
<div class="amount-value">${{ amount.toFixed(2) }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="amount-buttons">
|
|
||||||
<v-btn class="amount-btn" @click="adjustAmount(1)">+$1</v-btn>
|
|
||||||
<v-btn class="amount-btn" @click="adjustAmount(20)">+$20</v-btn>
|
|
||||||
<v-btn class="amount-btn" @click="adjustAmount(100)">+$100</v-btn>
|
|
||||||
<v-btn class="amount-btn" @click="setMaxAmount">Max</v-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<v-btn class="deposit-btn" @click="deposit">Deposit</v-btn>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="price-options hide-in-mobile-sheet">
|
|
||||||
<v-btn class="yes-btn" :class="{ active: selectedOption === 'yes' }" text @click="handleOptionChange('yes')">Yes {{ yesPriceCents }}¢</v-btn>
|
|
||||||
<v-btn class="no-btn" :class="{ active: selectedOption === 'no' }" text @click="handleOptionChange('no')">No {{ noPriceCents }}¢</v-btn>
|
|
||||||
</div>
|
|
||||||
<div class="input-group limit-price-group">
|
|
||||||
<div class="limit-price-header">
|
<div class="limit-price-header">
|
||||||
<span class="label">Limit Price</span>
|
<span class="label">Limit Price</span>
|
||||||
<div class="price-input">
|
<div class="price-input">
|
||||||
<v-btn class="adjust-btn" icon @click="decreasePrice"><v-icon>mdi-minus</v-icon></v-btn>
|
<v-btn class="adjust-btn" icon @click="decreasePrice"
|
||||||
<v-text-field :model-value="limitPrice" type="number" min="0" max="1" step="0.01" class="price-input-field" hide-details density="compact" @update:model-value="onLimitPriceInput" @keydown="onLimitPriceKeydown" @paste="onLimitPricePaste"></v-text-field>
|
><v-icon>mdi-minus</v-icon></v-btn
|
||||||
<v-btn class="adjust-btn" icon @click="increasePrice"><v-icon>mdi-plus</v-icon></v-btn>
|
>
|
||||||
|
<v-text-field
|
||||||
|
:model-value="limitPrice"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
class="price-input-field"
|
||||||
|
hide-details
|
||||||
|
density="compact"
|
||||||
|
@update:model-value="onLimitPriceInput"
|
||||||
|
@keydown="onLimitPriceKeydown"
|
||||||
|
@paste="onLimitPricePaste"
|
||||||
|
></v-text-field>
|
||||||
|
<v-btn class="adjust-btn" icon @click="increasePrice"
|
||||||
|
><v-icon>mdi-plus</v-icon></v-btn
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -516,7 +721,17 @@
|
|||||||
<div class="shares-header">
|
<div class="shares-header">
|
||||||
<span class="label">Shares</span>
|
<span class="label">Shares</span>
|
||||||
<div class="shares-input">
|
<div class="shares-input">
|
||||||
<v-text-field :model-value="shares" type="number" min="1" class="shares-input-field" hide-details density="compact" @update:model-value="onSharesInput" @keydown="onSharesKeydown" @paste="onSharesPaste"></v-text-field>
|
<v-text-field
|
||||||
|
:model-value="shares"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
class="shares-input-field"
|
||||||
|
hide-details
|
||||||
|
density="compact"
|
||||||
|
@update:model-value="onSharesInput"
|
||||||
|
@keydown="onSharesKeydown"
|
||||||
|
@paste="onSharesPaste"
|
||||||
|
></v-text-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="activeTab === 'buy'" class="shares-buttons">
|
<div v-if="activeTab === 'buy'" class="shares-buttons">
|
||||||
@ -534,7 +749,12 @@
|
|||||||
<div class="input-group expiration-group">
|
<div class="input-group expiration-group">
|
||||||
<div class="expiration-header">
|
<div class="expiration-header">
|
||||||
<span class="label">Set expiration</span>
|
<span class="label">Set expiration</span>
|
||||||
<v-switch v-model="expirationEnabled" class="expiration-switch" hide-details color="primary"></v-switch>
|
<v-switch
|
||||||
|
v-model="expirationEnabled"
|
||||||
|
class="expiration-switch"
|
||||||
|
hide-details
|
||||||
|
color="primary"
|
||||||
|
></v-switch>
|
||||||
</div>
|
</div>
|
||||||
<v-select
|
<v-select
|
||||||
v-if="expirationEnabled"
|
v-if="expirationEnabled"
|
||||||
@ -547,15 +767,35 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="total-section">
|
<div class="total-section">
|
||||||
<template v-if="activeTab === 'buy'">
|
<template v-if="activeTab === 'buy'">
|
||||||
<div class="total-row"><span class="label">Total</span><span class="total-value">${{ totalPrice }}</span></div>
|
<div class="total-row">
|
||||||
<div class="total-row"><span class="label">To win</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> $20</span></div>
|
<span class="label">Total</span><span class="total-value">${{ totalPrice }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="total-row">
|
||||||
|
<span class="label">To win</span
|
||||||
|
><span class="to-win-value"
|
||||||
|
><v-icon size="16" color="green">mdi-currency-usd</v-icon> $20</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="total-row"><span class="label">You'll receive</span><span class="to-win-value"><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{ totalPrice }}</span></div>
|
<div class="total-row">
|
||||||
|
<span class="label">You'll receive</span
|
||||||
|
><span class="to-win-value"
|
||||||
|
><v-icon size="16" color="green">mdi-currency-usd</v-icon> ${{
|
||||||
|
totalPrice
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="orderError" class="order-error">{{ orderError }}</p>
|
<p v-if="orderError" class="order-error">{{ orderError }}</p>
|
||||||
<v-btn class="action-btn" :loading="orderLoading" :disabled="orderLoading" @click="submitOrder">{{ actionButtonText }}</v-btn>
|
<v-btn
|
||||||
|
class="action-btn"
|
||||||
|
:loading="orderLoading"
|
||||||
|
:disabled="orderLoading"
|
||||||
|
@click="submitOrder"
|
||||||
|
>{{ actionButtonText }}</v-btn
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
@ -563,17 +803,30 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Merge shares dialog(与桌面/移动端分支并列,始终挂载才能响应 openMergeDialog) -->
|
<!-- Merge shares dialog(与桌面/移动端分支并列,始终挂载才能响应 openMergeDialog) -->
|
||||||
<v-dialog v-model="mergeDialogOpen" max-width="440" persistent content-class="merge-dialog" transition="dialog-transition">
|
<v-dialog
|
||||||
|
v-model="mergeDialogOpen"
|
||||||
|
max-width="440"
|
||||||
|
persistent
|
||||||
|
content-class="merge-dialog"
|
||||||
|
transition="dialog-transition"
|
||||||
|
>
|
||||||
<v-card class="merge-dialog-card" rounded="lg">
|
<v-card class="merge-dialog-card" rounded="lg">
|
||||||
<div class="merge-dialog-header">
|
<div class="merge-dialog-header">
|
||||||
<h3 class="merge-dialog-title">Merge shares</h3>
|
<h3 class="merge-dialog-title">Merge shares</h3>
|
||||||
<v-btn icon variant="text" size="small" class="merge-dialog-close" @click="mergeDialogOpen = false">
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
class="merge-dialog-close"
|
||||||
|
@click="mergeDialogOpen = false"
|
||||||
|
>
|
||||||
<v-icon>mdi-close</v-icon>
|
<v-icon>mdi-close</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
<v-card-text class="merge-dialog-body">
|
<v-card-text class="merge-dialog-body">
|
||||||
<p class="merge-dialog-desc">
|
<p class="merge-dialog-desc">
|
||||||
Merge a share of Yes and No to get 1 USDC. You can do this to save cost when trying to get rid of a position.
|
Merge a share of Yes and No to get 1 USDC. You can do this to save cost when trying to get
|
||||||
|
rid of a position.
|
||||||
</p>
|
</p>
|
||||||
<div class="merge-amount-row">
|
<div class="merge-amount-row">
|
||||||
<label class="merge-amount-label">Amount</label>
|
<label class="merge-amount-label">Amount</label>
|
||||||
@ -591,7 +844,9 @@
|
|||||||
Available shares: {{ availableMergeShares }}
|
Available shares: {{ availableMergeShares }}
|
||||||
<button type="button" class="merge-max-link" @click="setMergeMax">Max</button>
|
<button type="button" class="merge-max-link" @click="setMergeMax">Max</button>
|
||||||
</p>
|
</p>
|
||||||
<p v-if="!props.market?.marketId" class="merge-no-market">Please select a market first (e.g. click Buy Yes/No on a market).</p>
|
<p v-if="!props.market?.marketId" class="merge-no-market">
|
||||||
|
Please select a market first (e.g. click Buy Yes/No on a market).
|
||||||
|
</p>
|
||||||
<p v-if="mergeError" class="merge-error">{{ mergeError }}</p>
|
<p v-if="mergeError" class="merge-error">{{ mergeError }}</p>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions class="merge-dialog-actions">
|
<v-card-actions class="merge-dialog-actions">
|
||||||
@ -611,17 +866,30 @@
|
|||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
<!-- Split dialog:对接 /PmMarket/split -->
|
<!-- Split dialog:对接 /PmMarket/split -->
|
||||||
<v-dialog v-model="splitDialogOpen" max-width="440" persistent content-class="split-dialog" transition="dialog-transition">
|
<v-dialog
|
||||||
|
v-model="splitDialogOpen"
|
||||||
|
max-width="440"
|
||||||
|
persistent
|
||||||
|
content-class="split-dialog"
|
||||||
|
transition="dialog-transition"
|
||||||
|
>
|
||||||
<v-card class="split-dialog-card" rounded="lg">
|
<v-card class="split-dialog-card" rounded="lg">
|
||||||
<div class="split-dialog-header">
|
<div class="split-dialog-header">
|
||||||
<h3 class="split-dialog-title">Split</h3>
|
<h3 class="split-dialog-title">Split</h3>
|
||||||
<v-btn icon variant="text" size="small" class="split-dialog-close" @click="splitDialogOpen = false">
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
class="split-dialog-close"
|
||||||
|
@click="splitDialogOpen = false"
|
||||||
|
>
|
||||||
<v-icon>mdi-close</v-icon>
|
<v-icon>mdi-close</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
<v-card-text class="split-dialog-body">
|
<v-card-text class="split-dialog-body">
|
||||||
<p class="split-dialog-desc">
|
<p class="split-dialog-desc">
|
||||||
Use USDC to get one share of Yes and one share of No for this market. 1 USDC ≈ 1 complete set.
|
Use USDC to get one share of Yes and one share of No for this market. 1 USDC ≈ 1 complete
|
||||||
|
set.
|
||||||
</p>
|
</p>
|
||||||
<div class="split-amount-row">
|
<div class="split-amount-row">
|
||||||
<label class="split-amount-label">Amount (USDC)</label>
|
<label class="split-amount-label">Amount (USDC)</label>
|
||||||
@ -636,7 +904,9 @@
|
|||||||
class="split-amount-input"
|
class="split-amount-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!props.market?.marketId" class="split-no-market">Please select a market first (e.g. click Buy Yes/No on a market).</p>
|
<p v-if="!props.market?.marketId" class="split-no-market">
|
||||||
|
Please select a market first (e.g. click Buy Yes/No on a market).
|
||||||
|
</p>
|
||||||
<p v-if="splitError" class="split-error">{{ splitError }}</p>
|
<p v-if="splitError" class="split-error">{{ splitError }}</p>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions class="split-dialog-actions">
|
<v-card-actions class="split-dialog-actions">
|
||||||
@ -682,7 +952,7 @@ const props = withDefaults(
|
|||||||
/** 从外部传入的市场数据(如 EventMarkets 点击 Yes/No 传入),yesPrice/noPrice 为 0–1 */
|
/** 从外部传入的市场数据(如 EventMarkets 点击 Yes/No 传入),yesPrice/noPrice 为 0–1 */
|
||||||
market?: TradeMarketPayload
|
market?: TradeMarketPayload
|
||||||
}>(),
|
}>(),
|
||||||
{ initialOption: undefined, embeddedInSheet: false, market: undefined }
|
{ initialOption: undefined, embeddedInSheet: false, market: undefined },
|
||||||
)
|
)
|
||||||
|
|
||||||
// 移动端:底部栏与弹出层
|
// 移动端:底部栏与弹出层
|
||||||
@ -711,7 +981,7 @@ async function submitMerge() {
|
|||||||
try {
|
try {
|
||||||
const res = await pmMarketMerge(
|
const res = await pmMarketMerge(
|
||||||
{ marketID: marketId, amount: String(mergeAmount.value) },
|
{ marketID: marketId, amount: String(mergeAmount.value) },
|
||||||
{ headers: userStore.getAuthHeaders() }
|
{ headers: userStore.getAuthHeaders() },
|
||||||
)
|
)
|
||||||
if (res.code === 0 || res.code === 200) {
|
if (res.code === 0 || res.code === 200) {
|
||||||
mergeDialogOpen.value = false
|
mergeDialogOpen.value = false
|
||||||
@ -745,7 +1015,7 @@ async function submitSplit() {
|
|||||||
try {
|
try {
|
||||||
const res = await pmMarketSplit(
|
const res = await pmMarketSplit(
|
||||||
{ marketID: marketId, usdcAmount: String(splitAmount.value) },
|
{ marketID: marketId, usdcAmount: String(splitAmount.value) },
|
||||||
{ headers: userStore.getAuthHeaders() }
|
{ headers: userStore.getAuthHeaders() },
|
||||||
)
|
)
|
||||||
if (res.code === 0 || res.code === 200) {
|
if (res.code === 0 || res.code === 200) {
|
||||||
splitDialogOpen.value = false
|
splitDialogOpen.value = false
|
||||||
@ -759,12 +1029,8 @@ async function submitSplit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const yesPriceCents = computed(() =>
|
const yesPriceCents = computed(() => (props.market ? Math.round(props.market.yesPrice * 100) : 19))
|
||||||
props.market ? Math.round(props.market.yesPrice * 100) : 19
|
const noPriceCents = computed(() => (props.market ? Math.round(props.market.noPrice * 100) : 82))
|
||||||
)
|
|
||||||
const noPriceCents = computed(() =>
|
|
||||||
props.market ? Math.round(props.market.noPrice * 100) : 82
|
|
||||||
)
|
|
||||||
|
|
||||||
function openSheet(option: 'yes' | 'no') {
|
function openSheet(option: 'yes' | 'no') {
|
||||||
handleOptionChange(option)
|
handleOptionChange(option)
|
||||||
@ -792,15 +1058,17 @@ const orderError = ref('')
|
|||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
optionChange: [option: 'yes' | 'no']
|
optionChange: [option: 'yes' | 'no']
|
||||||
submit: [payload: {
|
submit: [
|
||||||
side: 'buy' | 'sell'
|
payload: {
|
||||||
option: 'yes' | 'no'
|
side: 'buy' | 'sell'
|
||||||
limitPrice: number
|
option: 'yes' | 'no'
|
||||||
shares: number
|
limitPrice: number
|
||||||
expirationEnabled: boolean
|
shares: number
|
||||||
expirationTime: string
|
expirationEnabled: boolean
|
||||||
marketId?: string
|
expirationTime: string
|
||||||
}]
|
marketId?: string
|
||||||
|
},
|
||||||
|
]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
@ -836,17 +1104,25 @@ onMounted(() => {
|
|||||||
if (props.initialOption) applyInitialOption(props.initialOption)
|
if (props.initialOption) applyInitialOption(props.initialOption)
|
||||||
else if (props.market) syncLimitPriceFromMarket()
|
else if (props.market) syncLimitPriceFromMarket()
|
||||||
})
|
})
|
||||||
watch(() => props.initialOption, (option) => {
|
watch(
|
||||||
if (option) applyInitialOption(option)
|
() => props.initialOption,
|
||||||
}, { immediate: true })
|
(option) => {
|
||||||
|
if (option) applyInitialOption(option)
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
watch(() => props.market, (m) => {
|
watch(
|
||||||
if (m) {
|
() => props.market,
|
||||||
orderError.value = ''
|
(m) => {
|
||||||
if (props.initialOption) applyInitialOption(props.initialOption)
|
if (m) {
|
||||||
else syncLimitPriceFromMarket()
|
orderError.value = ''
|
||||||
}
|
if (props.initialOption) applyInitialOption(props.initialOption)
|
||||||
}, { deep: true })
|
else syncLimitPriceFromMarket()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const handleOptionChange = (option: 'yes' | 'no') => {
|
const handleOptionChange = (option: 'yes' | 'no') => {
|
||||||
@ -1031,7 +1307,7 @@ async function submitOrder() {
|
|||||||
tokenID: tokenId,
|
tokenID: tokenId,
|
||||||
userID: userIdNum,
|
userID: userIdNum,
|
||||||
},
|
},
|
||||||
{ headers }
|
{ headers },
|
||||||
)
|
)
|
||||||
if (res.code === 0 || res.code === 200) {
|
if (res.code === 0 || res.code === 200) {
|
||||||
userStore.fetchUsdcBalance()
|
userStore.fetchUsdcBalance()
|
||||||
|
|||||||
@ -110,7 +110,7 @@ const props = withDefaults(
|
|||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
balance: string
|
balance: string
|
||||||
}>(),
|
}>(),
|
||||||
{ balance: '0.00' }
|
{ balance: '0.00' },
|
||||||
)
|
)
|
||||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean]; success: [] }>()
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean]; success: [] }>()
|
||||||
|
|
||||||
@ -145,7 +145,11 @@ const hasValidDestination = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const canSubmit = computed(
|
const canSubmit = computed(
|
||||||
() => amountNum.value > 0 && amountNum.value <= balanceNum.value && hasValidDestination.value && !amountError.value
|
() =>
|
||||||
|
amountNum.value > 0 &&
|
||||||
|
amountNum.value <= balanceNum.value &&
|
||||||
|
hasValidDestination.value &&
|
||||||
|
!amountError.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
function shortAddress(addr: string) {
|
function shortAddress(addr: string) {
|
||||||
@ -188,7 +192,8 @@ async function submitWithdraw() {
|
|||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
await new Promise((r) => setTimeout(r, 800))
|
await new Promise((r) => setTimeout(r, 800))
|
||||||
const dest = destinationType.value === 'wallet' ? connectedAddress.value : customAddress.value.trim()
|
const dest =
|
||||||
|
destinationType.value === 'wallet' ? connectedAddress.value : customAddress.value.trim()
|
||||||
console.log('Withdraw', { amount: amount.value, network: selectedNetwork.value, to: dest })
|
console.log('Withdraw', { amount: amount.value, network: selectedNetwork.value, to: dest })
|
||||||
emit('success')
|
emit('success')
|
||||||
close()
|
close()
|
||||||
@ -206,7 +211,7 @@ watch(
|
|||||||
customAddress.value = ''
|
customAddress.value = ''
|
||||||
connectedAddress.value = ''
|
connectedAddress.value = ''
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -25,8 +25,8 @@ export default createVuetify({
|
|||||||
success: '#34A853',
|
success: '#34A853',
|
||||||
warning: '#FBBC05',
|
warning: '#FBBC05',
|
||||||
surface: '#FFFFFF',
|
surface: '#FFFFFF',
|
||||||
background: '#F5F5F5'
|
background: '#F5F5F5',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
dark: true,
|
dark: true,
|
||||||
@ -39,9 +39,9 @@ export default createVuetify({
|
|||||||
success: '#4CAF50',
|
success: '#4CAF50',
|
||||||
warning: '#FFC107',
|
warning: '#FFC107',
|
||||||
surface: '#1E1E1E',
|
surface: '#1E1E1E',
|
||||||
background: '#121212'
|
background: '#121212',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
@ -12,33 +12,33 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'home',
|
name: 'home',
|
||||||
component: Home
|
component: Home,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/trade',
|
path: '/trade',
|
||||||
name: 'trade',
|
name: 'trade',
|
||||||
component: Trade
|
component: Trade,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
component: Login
|
component: Login,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/trade-detail/:id',
|
path: '/trade-detail/:id',
|
||||||
name: 'trade-detail',
|
name: 'trade-detail',
|
||||||
component: TradeDetail
|
component: TradeDetail,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/event/:id/markets',
|
path: '/event/:id/markets',
|
||||||
name: 'event-markets',
|
name: 'event-markets',
|
||||||
component: EventMarkets
|
component: EventMarkets,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/wallet',
|
path: '/wallet',
|
||||||
name: 'wallet',
|
name: 'wallet',
|
||||||
component: Wallet
|
component: Wallet,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
scrollBehavior(to, from, savedPosition) {
|
scrollBehavior(to, from, savedPosition) {
|
||||||
if (savedPosition && from?.name) return savedPosition
|
if (savedPosition && from?.name) return savedPosition
|
||||||
|
|||||||
@ -89,5 +89,15 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { token, user, isLoggedIn, avatarUrl, balance, setUser, logout, getAuthHeaders, fetchUsdcBalance }
|
return {
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
isLoggedIn,
|
||||||
|
avatarUrl,
|
||||||
|
balance,
|
||||||
|
setUser,
|
||||||
|
logout,
|
||||||
|
getAuthHeaders,
|
||||||
|
fetchUsdcBalance,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -123,7 +123,13 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
import type { ECharts } from 'echarts'
|
import type { ECharts } from 'echarts'
|
||||||
import TradeComponent from '../components/TradeComponent.vue'
|
import TradeComponent from '../components/TradeComponent.vue'
|
||||||
import { findPmEvent, getMarketId, type FindPmEventParams, type PmEventListItem, type PmEventMarketItem } from '../api/event'
|
import {
|
||||||
|
findPmEvent,
|
||||||
|
getMarketId,
|
||||||
|
type FindPmEventParams,
|
||||||
|
type PmEventListItem,
|
||||||
|
type PmEventMarketItem,
|
||||||
|
} from '../api/event'
|
||||||
import { MOCK_EVENT_LIST } from '../api/mockEventList'
|
import { MOCK_EVENT_LIST } from '../api/mockEventList'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
@ -220,7 +226,16 @@ const chartData = ref<ChartSeriesItem[]>([])
|
|||||||
let chartInstance: ECharts | null = null
|
let chartInstance: ECharts | null = null
|
||||||
let dynamicInterval: number | undefined
|
let dynamicInterval: number | undefined
|
||||||
|
|
||||||
const LINE_COLORS = ['#2563eb', '#dc2626', '#16a34a', '#ca8a04', '#9333ea', '#0891b2', '#ea580c', '#4f46e5']
|
const LINE_COLORS = [
|
||||||
|
'#2563eb',
|
||||||
|
'#dc2626',
|
||||||
|
'#16a34a',
|
||||||
|
'#ca8a04',
|
||||||
|
'#9333ea',
|
||||||
|
'#0891b2',
|
||||||
|
'#ea580c',
|
||||||
|
'#4f46e5',
|
||||||
|
]
|
||||||
const MOBILE_BREAKPOINT = 600
|
const MOBILE_BREAKPOINT = 600
|
||||||
|
|
||||||
function getStepAndCount(range: string): { stepMs: number; count: number } {
|
function getStepAndCount(range: string): { stepMs: number; count: number } {
|
||||||
@ -371,7 +386,8 @@ function initChart() {
|
|||||||
function updateChartData() {
|
function updateChartData() {
|
||||||
chartData.value = generateAllData()
|
chartData.value = generateAllData()
|
||||||
const w = chartContainerRef.value?.clientWidth
|
const w = chartContainerRef.value?.clientWidth
|
||||||
if (chartInstance) chartInstance.setOption(buildOption(chartData.value, w), { replaceMerge: ['series'] })
|
if (chartInstance)
|
||||||
|
chartInstance.setOption(buildOption(chartData.value, w), { replaceMerge: ['series'] })
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTimeRange(range: string) {
|
function selectTimeRange(range: string) {
|
||||||
@ -396,7 +412,8 @@ function startDynamicUpdate() {
|
|||||||
})
|
})
|
||||||
chartData.value = nextData
|
chartData.value = nextData
|
||||||
const w = chartContainerRef.value?.clientWidth
|
const w = chartContainerRef.value?.clientWidth
|
||||||
if (chartInstance) chartInstance.setOption(buildOption(chartData.value, w), { replaceMerge: ['series'] })
|
if (chartInstance)
|
||||||
|
chartInstance.setOption(buildOption(chartData.value, w), { replaceMerge: ['series'] })
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,7 +508,7 @@ async function loadEventDetail() {
|
|||||||
const slugFromQuery = (route.query.slug as string)?.trim()
|
const slugFromQuery = (route.query.slug as string)?.trim()
|
||||||
const params: FindPmEventParams = {
|
const params: FindPmEventParams = {
|
||||||
id: isNumericId ? numId : undefined,
|
id: isNumericId ? numId : undefined,
|
||||||
slug: isNumericId ? (slugFromQuery || undefined) : idStr,
|
slug: isNumericId ? slugFromQuery || undefined : idStr,
|
||||||
}
|
}
|
||||||
|
|
||||||
detailError.value = null
|
detailError.value = null
|
||||||
@ -552,11 +569,11 @@ watch(
|
|||||||
if (dynamicInterval == null) startDynamicUpdate()
|
if (dynamicInterval == null) startDynamicUpdate()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
watch(
|
watch(
|
||||||
() => route.params.id,
|
() => route.params.id,
|
||||||
() => loadEventDetail()
|
() => loadEventDetail(),
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,13 @@
|
|||||||
>
|
>
|
||||||
<v-icon size="20">mdi-magnify</v-icon>
|
<v-icon size="20">mdi-magnify</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn icon variant="text" size="small" class="home-category-action-btn" aria-label="筛选">
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
class="home-category-action-btn"
|
||||||
|
aria-label="筛选"
|
||||||
|
>
|
||||||
<v-icon size="20">mdi-filter-outline</v-icon>
|
<v-icon size="20">mdi-filter-outline</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@ -44,7 +50,14 @@
|
|||||||
@blur="onSearchBlur"
|
@blur="onSearchBlur"
|
||||||
@keydown.enter="onSearchSubmit"
|
@keydown.enter="onSearchSubmit"
|
||||||
/>
|
/>
|
||||||
<v-btn icon variant="text" size="small" class="home-search-close-btn" aria-label="收起" @click="collapseSearch">
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
class="home-search-close-btn"
|
||||||
|
aria-label="收起"
|
||||||
|
@click="collapseSearch"
|
||||||
|
>
|
||||||
<v-icon size="18">mdi-close</v-icon>
|
<v-icon size="18">mdi-close</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@ -67,7 +80,11 @@
|
|||||||
:key="`${item}-${idx}`"
|
:key="`${item}-${idx}`"
|
||||||
class="home-search-history-item"
|
class="home-search-history-item"
|
||||||
>
|
>
|
||||||
<button type="button" class="home-search-history-text" @click="selectHistoryItem(item)">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="home-search-history-text"
|
||||||
|
@click="selectHistoryItem(item)"
|
||||||
|
>
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</button>
|
</button>
|
||||||
<v-btn
|
<v-btn
|
||||||
@ -105,7 +122,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="categoryLayers.length >= 3" class="home-category-layer home-category-layer--third">
|
<div
|
||||||
|
v-if="categoryLayers.length >= 3"
|
||||||
|
class="home-category-layer home-category-layer--third"
|
||||||
|
>
|
||||||
<v-tabs
|
<v-tabs
|
||||||
:model-value="layerActiveValues[2]"
|
:model-value="layerActiveValues[2]"
|
||||||
class="home-tab-bar home-tab-bar--compact"
|
class="home-tab-bar home-tab-bar--compact"
|
||||||
@ -166,37 +186,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-pull-to-refresh>
|
</v-pull-to-refresh>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PC:对话框;手机:底部 sheet,直接显示交易表单 -->
|
<!-- PC:对话框;手机:底部 sheet,直接显示交易表单 -->
|
||||||
<v-dialog
|
<v-dialog
|
||||||
v-if="!isMobile"
|
v-if="!isMobile"
|
||||||
v-model="tradeDialogOpen"
|
v-model="tradeDialogOpen"
|
||||||
max-width="420"
|
max-width="420"
|
||||||
scrollable
|
scrollable
|
||||||
content-class="trade-dialog trade-dialog--bare"
|
content-class="trade-dialog trade-dialog--bare"
|
||||||
transition="dialog-transition"
|
transition="dialog-transition"
|
||||||
@click:outside="tradeDialogOpen = false"
|
@click:outside="tradeDialogOpen = false"
|
||||||
>
|
>
|
||||||
<TradeComponent
|
<TradeComponent
|
||||||
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
|
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
|
||||||
:market="homeTradeMarketPayload"
|
:market="homeTradeMarketPayload"
|
||||||
:initial-option="tradeDialogSide"
|
:initial-option="tradeDialogSide"
|
||||||
/>
|
/>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
<v-bottom-sheet
|
<v-bottom-sheet v-else v-model="tradeDialogOpen" content-class="trade-bottom-sheet">
|
||||||
v-else
|
<TradeComponent
|
||||||
v-model="tradeDialogOpen"
|
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
|
||||||
content-class="trade-bottom-sheet"
|
:market="homeTradeMarketPayload"
|
||||||
>
|
:initial-option="tradeDialogSide"
|
||||||
<TradeComponent
|
embedded-in-sheet
|
||||||
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
|
/>
|
||||||
:market="homeTradeMarketPayload"
|
</v-bottom-sheet>
|
||||||
:initial-option="tradeDialogSide"
|
</v-container>
|
||||||
embedded-in-sheet
|
|
||||||
/>
|
|
||||||
</v-bottom-sheet>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<footer class="home-footer">
|
<footer class="home-footer">
|
||||||
<div class="footer-inner">
|
<div class="footer-inner">
|
||||||
@ -266,7 +282,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="footer-disclaimer">
|
<p class="footer-disclaimer">
|
||||||
Polymarket operates globally through separate legal entities. Polymarket US is operated by QCX LLC d/b/a Polymarket US, a CFTC-regulated Designated Contract Market. This international platform is not regulated by the CFTC and operates independently. Trading involves substantial risk of loss. See our <a href="#">Terms of Service & Privacy Policy</a>.
|
Polymarket operates globally through separate legal entities. Polymarket US is operated by
|
||||||
|
QCX LLC d/b/a Polymarket US, a CFTC-regulated Designated Contract Market. This
|
||||||
|
international platform is not regulated by the CFTC and operates independently. Trading
|
||||||
|
involves substantial risk of loss. See our
|
||||||
|
<a href="#">Terms of Service & Privacy Policy</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
@ -287,7 +307,7 @@ import {
|
|||||||
clearEventListCache,
|
clearEventListCache,
|
||||||
type EventCardItem,
|
type EventCardItem,
|
||||||
} from '../api/event'
|
} from '../api/event'
|
||||||
import { getCategoryTree, MOCK_CATEGORY_TREE, type CategoryTreeNode } from '../api/category'
|
import { getPmTagMain, MOCK_CATEGORY_TREE, type CategoryTreeNode } from '../api/category'
|
||||||
import { useSearchHistory } from '../composables/useSearchHistory'
|
import { useSearchHistory } from '../composables/useSearchHistory'
|
||||||
|
|
||||||
const { mobile } = useDisplay()
|
const { mobile } = useDisplay()
|
||||||
@ -434,16 +454,27 @@ const loadingMore = ref(false)
|
|||||||
|
|
||||||
const noMoreEvents = computed(() => {
|
const noMoreEvents = computed(() => {
|
||||||
if (eventList.value.length === 0) return false
|
if (eventList.value.length === 0) return false
|
||||||
return eventList.value.length >= eventTotal.value || eventPage.value * eventPageSize.value >= eventTotal.value
|
return (
|
||||||
|
eventList.value.length >= eventTotal.value ||
|
||||||
|
eventPage.value * eventPageSize.value >= eventTotal.value
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const footerLang = ref('English')
|
const footerLang = ref('English')
|
||||||
const tradeDialogOpen = ref(false)
|
const tradeDialogOpen = ref(false)
|
||||||
const tradeDialogSide = ref<'yes' | 'no'>('yes')
|
const tradeDialogSide = ref<'yes' | 'no'>('yes')
|
||||||
const tradeDialogMarket = ref<{ id: string; title: string; marketId?: string; clobTokenIds?: string[] } | null>(null)
|
const tradeDialogMarket = ref<{
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
marketId?: string
|
||||||
|
clobTokenIds?: string[]
|
||||||
|
} | null>(null)
|
||||||
const scrollRef = ref<HTMLElement | null>(null)
|
const scrollRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
function onCardOpenTrade(side: 'yes' | 'no', market?: { id: string; title: string; marketId?: string }) {
|
function onCardOpenTrade(
|
||||||
|
side: 'yes' | 'no',
|
||||||
|
market?: { id: string; title: string; marketId?: string },
|
||||||
|
) {
|
||||||
tradeDialogSide.value = side
|
tradeDialogSide.value = side
|
||||||
tradeDialogMarket.value = market ?? null
|
tradeDialogMarket.value = market ?? null
|
||||||
tradeDialogOpen.value = true
|
tradeDialogOpen.value = true
|
||||||
@ -492,9 +523,7 @@ const activeSearchKeyword = ref('')
|
|||||||
async function loadEvents(page: number, append: boolean, keyword?: string) {
|
async function loadEvents(page: number, append: boolean, keyword?: string) {
|
||||||
const kw = keyword !== undefined ? keyword : activeSearchKeyword.value
|
const kw = keyword !== undefined ? keyword : activeSearchKeyword.value
|
||||||
try {
|
try {
|
||||||
const res = await getPmEventPublic(
|
const res = await getPmEventPublic({ page, pageSize: PAGE_SIZE, keyword: kw || undefined })
|
||||||
{ page, pageSize: PAGE_SIZE, keyword: kw || undefined }
|
|
||||||
)
|
|
||||||
if (res.code !== 0 && res.code !== 200) {
|
if (res.code !== 0 && res.code !== 200) {
|
||||||
throw new Error(res.msg || '请求失败')
|
throw new Error(res.msg || '请求失败')
|
||||||
}
|
}
|
||||||
@ -548,12 +577,12 @@ function checkScrollLoad() {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
/** 开发时设为 true 可始终使用模拟数据查看一二三层 UI */
|
/** 开发时设为 true 可始终使用模拟数据查看一二三层 UI */
|
||||||
const USE_MOCK_CATEGORY = true
|
const USE_MOCK_CATEGORY = false
|
||||||
if (USE_MOCK_CATEGORY) {
|
if (USE_MOCK_CATEGORY) {
|
||||||
categoryTree.value = MOCK_CATEGORY_TREE
|
categoryTree.value = MOCK_CATEGORY_TREE
|
||||||
initCategorySelection()
|
initCategorySelection()
|
||||||
} else {
|
} else {
|
||||||
getCategoryTree()
|
getPmTagMain()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.code === 0 || res.code === 200) {
|
if (res.code === 0 || res.code === 200) {
|
||||||
const data = res.data
|
const data = res.data
|
||||||
@ -586,7 +615,7 @@ onMounted(() => {
|
|||||||
if (!entries[0]?.isIntersecting) return
|
if (!entries[0]?.isIntersecting) return
|
||||||
loadMore()
|
loadMore()
|
||||||
},
|
},
|
||||||
{ root: null, rootMargin: `${SCROLL_LOAD_THRESHOLD}px`, threshold: 0 }
|
{ root: null, rootMargin: `${SCROLL_LOAD_THRESHOLD}px`, threshold: 0 },
|
||||||
)
|
)
|
||||||
observer.observe(sentinel)
|
observer.observe(sentinel)
|
||||||
}
|
}
|
||||||
@ -715,7 +744,6 @@ onUnmounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.home-subtitle {
|
.home-subtitle {
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
@ -802,7 +830,9 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.home-search-overlay-enter-active,
|
.home-search-overlay-enter-active,
|
||||||
.home-search-overlay-leave-active {
|
.home-search-overlay-leave-active {
|
||||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
transition:
|
||||||
|
opacity 0.15s ease,
|
||||||
|
transform 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-search-overlay-enter-from,
|
.home-search-overlay-enter-from,
|
||||||
@ -935,7 +965,9 @@ onUnmounted(() => {
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s, color 0.2s;
|
transition:
|
||||||
|
background-color 0.2s,
|
||||||
|
color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-category-icon-item:hover {
|
.home-category-icon-item:hover {
|
||||||
|
|||||||
@ -175,7 +175,7 @@ const connectWithWallet = async () => {
|
|||||||
nonce,
|
nonce,
|
||||||
signature,
|
signature,
|
||||||
walletAddress,
|
walletAddress,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
console.log('Login API response:', loginData)
|
console.log('Login API response:', loginData)
|
||||||
|
|
||||||
|
|||||||
@ -80,18 +80,21 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-list">
|
<div class="activity-list">
|
||||||
<div
|
<div v-for="item in filteredActivity" :key="item.id" class="activity-item">
|
||||||
v-for="item in filteredActivity"
|
|
||||||
:key="item.id"
|
|
||||||
class="activity-item"
|
|
||||||
>
|
|
||||||
<div class="activity-avatar" :class="item.avatarClass">
|
<div class="activity-avatar" :class="item.avatarClass">
|
||||||
<img v-if="item.avatarUrl" :src="item.avatarUrl" :alt="item.user" class="avatar-img" />
|
<img
|
||||||
|
v-if="item.avatarUrl"
|
||||||
|
:src="item.avatarUrl"
|
||||||
|
:alt="item.user"
|
||||||
|
class="avatar-img"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-body">
|
<div class="activity-body">
|
||||||
<span class="activity-user">{{ item.user }}</span>
|
<span class="activity-user">{{ item.user }}</span>
|
||||||
<span class="activity-action">{{ item.action }}</span>
|
<span class="activity-action">{{ item.action }}</span>
|
||||||
<span :class="['activity-amount', item.side === 'Yes' ? 'amount-yes' : 'amount-no']">
|
<span
|
||||||
|
:class="['activity-amount', item.side === 'Yes' ? 'amount-yes' : 'amount-no']"
|
||||||
|
>
|
||||||
{{ item.amount }} {{ item.side }}
|
{{ item.amount }} {{ item.side }}
|
||||||
</span>
|
</span>
|
||||||
<span class="activity-price">at {{ item.price }}</span>
|
<span class="activity-price">at {{ item.price }}</span>
|
||||||
@ -113,10 +116,7 @@
|
|||||||
<!-- 右侧:交易组件(固定宽度),传入当前市场以便 Split 调用拆单接口 -->
|
<!-- 右侧:交易组件(固定宽度),传入当前市场以便 Split 调用拆单接口 -->
|
||||||
<v-col cols="12" class="trade-col">
|
<v-col cols="12" class="trade-col">
|
||||||
<div class="trade-sidebar">
|
<div class="trade-sidebar">
|
||||||
<TradeComponent
|
<TradeComponent :market="tradeMarketPayload" :initial-option="tradeInitialOption" />
|
||||||
:market="tradeMarketPayload"
|
|
||||||
:initial-option="tradeInitialOption"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
@ -130,7 +130,12 @@ import * as echarts from 'echarts'
|
|||||||
import type { ECharts } from 'echarts'
|
import type { ECharts } from 'echarts'
|
||||||
import OrderBook from '../components/OrderBook.vue'
|
import OrderBook from '../components/OrderBook.vue'
|
||||||
import TradeComponent from '../components/TradeComponent.vue'
|
import TradeComponent from '../components/TradeComponent.vue'
|
||||||
import { findPmEvent, getMarketId, type FindPmEventParams, type PmEventListItem } from '../api/event'
|
import {
|
||||||
|
findPmEvent,
|
||||||
|
getMarketId,
|
||||||
|
type FindPmEventParams,
|
||||||
|
type PmEventListItem,
|
||||||
|
} from '../api/event'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -198,14 +203,14 @@ async function loadEventDetail() {
|
|||||||
const slugFromQuery = (route.query.slug as string)?.trim()
|
const slugFromQuery = (route.query.slug as string)?.trim()
|
||||||
const params: FindPmEventParams = {
|
const params: FindPmEventParams = {
|
||||||
id: isNumericId ? numId : undefined,
|
id: isNumericId ? numId : undefined,
|
||||||
slug: isNumericId ? (slugFromQuery || undefined) : idStr,
|
slug: isNumericId ? slugFromQuery || undefined : idStr,
|
||||||
}
|
}
|
||||||
|
|
||||||
detailError.value = null
|
detailError.value = null
|
||||||
detailLoading.value = true
|
detailLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await findPmEvent(params, {
|
const res = await findPmEvent(params, {
|
||||||
headers: userStore.getAuthHeaders()
|
headers: userStore.getAuthHeaders(),
|
||||||
})
|
})
|
||||||
if (res.code === 0 || res.code === 200) {
|
if (res.code === 0 || res.code === 200) {
|
||||||
eventDetail.value = res.data ?? null
|
eventDetail.value = res.data ?? null
|
||||||
@ -313,12 +318,72 @@ interface ActivityItem {
|
|||||||
time: number
|
time: number
|
||||||
}
|
}
|
||||||
const activityList = ref<ActivityItem[]>([
|
const activityList = ref<ActivityItem[]>([
|
||||||
{ id: 'a1', user: 'Scottp1887', avatarClass: 'avatar-gradient-1', action: 'bought', side: 'Yes', amount: 914, price: '32.8¢', total: '$300', time: Date.now() - 10 * 60 * 1000 },
|
{
|
||||||
{ id: 'a2', user: 'Outrageous-Budd...', avatarClass: 'avatar-gradient-2', action: 'sold', side: 'No', amount: 20, price: '70.0¢', total: '$14', time: Date.now() - 29 * 60 * 1000 },
|
id: 'a1',
|
||||||
{ id: 'a3', user: '0xbc7db42d5ea9a...', avatarClass: 'avatar-gradient-3', action: 'bought', side: 'Yes', amount: 5, price: '68.0¢', total: '$3.40', time: Date.now() - 60 * 60 * 1000 },
|
user: 'Scottp1887',
|
||||||
{ id: 'a4', user: 'FINNISH-FEMBOY', avatarClass: 'avatar-gradient-4', action: 'sold', side: 'No', amount: 57, price: '35.0¢', total: '$20', time: Date.now() - 2 * 60 * 60 * 1000 },
|
avatarClass: 'avatar-gradient-1',
|
||||||
{ id: 'a5', user: 'CryptoWhale', avatarClass: 'avatar-gradient-1', action: 'bought', side: 'No', amount: 143, price: '28.0¢', total: '$40', time: Date.now() - 3 * 60 * 60 * 1000 },
|
action: 'bought',
|
||||||
{ id: 'a6', user: 'PolyTrader', avatarClass: 'avatar-gradient-2', action: 'sold', side: 'Yes', amount: 30, price: '72.0¢', total: '$21.60', time: Date.now() - 5 * 60 * 60 * 1000 },
|
side: 'Yes',
|
||||||
|
amount: 914,
|
||||||
|
price: '32.8¢',
|
||||||
|
total: '$300',
|
||||||
|
time: Date.now() - 10 * 60 * 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'a2',
|
||||||
|
user: 'Outrageous-Budd...',
|
||||||
|
avatarClass: 'avatar-gradient-2',
|
||||||
|
action: 'sold',
|
||||||
|
side: 'No',
|
||||||
|
amount: 20,
|
||||||
|
price: '70.0¢',
|
||||||
|
total: '$14',
|
||||||
|
time: Date.now() - 29 * 60 * 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'a3',
|
||||||
|
user: '0xbc7db42d5ea9a...',
|
||||||
|
avatarClass: 'avatar-gradient-3',
|
||||||
|
action: 'bought',
|
||||||
|
side: 'Yes',
|
||||||
|
amount: 5,
|
||||||
|
price: '68.0¢',
|
||||||
|
total: '$3.40',
|
||||||
|
time: Date.now() - 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'a4',
|
||||||
|
user: 'FINNISH-FEMBOY',
|
||||||
|
avatarClass: 'avatar-gradient-4',
|
||||||
|
action: 'sold',
|
||||||
|
side: 'No',
|
||||||
|
amount: 57,
|
||||||
|
price: '35.0¢',
|
||||||
|
total: '$20',
|
||||||
|
time: Date.now() - 2 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'a5',
|
||||||
|
user: 'CryptoWhale',
|
||||||
|
avatarClass: 'avatar-gradient-1',
|
||||||
|
action: 'bought',
|
||||||
|
side: 'No',
|
||||||
|
amount: 143,
|
||||||
|
price: '28.0¢',
|
||||||
|
total: '$40',
|
||||||
|
time: Date.now() - 3 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'a6',
|
||||||
|
user: 'PolyTrader',
|
||||||
|
avatarClass: 'avatar-gradient-2',
|
||||||
|
action: 'sold',
|
||||||
|
side: 'Yes',
|
||||||
|
amount: 30,
|
||||||
|
price: '72.0¢',
|
||||||
|
total: '$21.60',
|
||||||
|
time: Date.now() - 5 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const filteredActivity = computed(() => {
|
const filteredActivity = computed(() => {
|
||||||
@ -461,7 +526,10 @@ function buildOption(chartData: [number, number][], containerWidth?: number) {
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
formatter: function (params: unknown) {
|
formatter: function (params: unknown) {
|
||||||
const p = (Array.isArray(params) ? params[0] : params) as { name: string | number; value: unknown }
|
const p = (Array.isArray(params) ? params[0] : params) as {
|
||||||
|
name: string | number
|
||||||
|
value: unknown
|
||||||
|
}
|
||||||
const date = new Date(p.name as number)
|
const date = new Date(p.name as number)
|
||||||
const val = Array.isArray(p.value) ? p.value[1] : p.value
|
const val = Array.isArray(p.value) ? p.value[1] : p.value
|
||||||
return (
|
return (
|
||||||
@ -525,7 +593,8 @@ function initChart() {
|
|||||||
function updateChartData() {
|
function updateChartData() {
|
||||||
data.value = generateData(selectedTimeRange.value)
|
data.value = generateData(selectedTimeRange.value)
|
||||||
const w = chartContainerRef.value?.clientWidth
|
const w = chartContainerRef.value?.clientWidth
|
||||||
if (chartInstance) chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] })
|
if (chartInstance)
|
||||||
|
chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] })
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTimeRange(range: string) {
|
function selectTimeRange(range: string) {
|
||||||
@ -535,13 +604,19 @@ function selectTimeRange(range: string) {
|
|||||||
|
|
||||||
function getMaxPoints(range: string): number {
|
function getMaxPoints(range: string): number {
|
||||||
switch (range) {
|
switch (range) {
|
||||||
case '1H': return 60
|
case '1H':
|
||||||
case '6H': return 36
|
return 60
|
||||||
case '1D': return 24
|
case '6H':
|
||||||
case '1W': return 7
|
return 36
|
||||||
|
case '1D':
|
||||||
|
return 24
|
||||||
|
case '1W':
|
||||||
|
return 7
|
||||||
case '1M':
|
case '1M':
|
||||||
case 'ALL': return 30
|
case 'ALL':
|
||||||
default: return 24
|
return 30
|
||||||
|
default:
|
||||||
|
return 24
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -556,7 +631,8 @@ function startDynamicUpdate() {
|
|||||||
const max = getMaxPoints(selectedTimeRange.value)
|
const max = getMaxPoints(selectedTimeRange.value)
|
||||||
data.value = list.slice(-max)
|
data.value = list.slice(-max)
|
||||||
const w = chartContainerRef.value?.clientWidth
|
const w = chartContainerRef.value?.clientWidth
|
||||||
if (chartInstance) chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] })
|
if (chartInstance)
|
||||||
|
chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] })
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -580,7 +656,7 @@ const handleResize = () => {
|
|||||||
watch(
|
watch(
|
||||||
() => route.params.id,
|
() => route.params.id,
|
||||||
() => loadEventDetail(),
|
() => loadEventDetail(),
|
||||||
{ immediate: false }
|
{ immediate: false },
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -1030,8 +1106,13 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes live-pulse {
|
@keyframes live-pulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%,
|
||||||
50% { opacity: 0.5; }
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-list {
|
.activity-list {
|
||||||
|
|||||||
@ -88,7 +88,13 @@
|
|||||||
prepend-inner-icon="mdi-magnify"
|
prepend-inner-icon="mdi-magnify"
|
||||||
/>
|
/>
|
||||||
<template v-if="activeTab === 'history'">
|
<template v-if="activeTab === 'history'">
|
||||||
<v-btn v-if="mobile" variant="outlined" size="small" class="filter-btn filter-btn-close-losses" @click="closeLosses">
|
<v-btn
|
||||||
|
v-if="mobile"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
class="filter-btn filter-btn-close-losses"
|
||||||
|
@click="closeLosses"
|
||||||
|
>
|
||||||
<v-icon size="18">mdi-delete-outline</v-icon>
|
<v-icon size="18">mdi-delete-outline</v-icon>
|
||||||
Close Losses
|
Close Losses
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@ -119,7 +125,12 @@
|
|||||||
<v-icon size="18">mdi-filter</v-icon>
|
<v-icon size="18">mdi-filter</v-icon>
|
||||||
Market
|
Market
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn variant="outlined" size="small" class="filter-btn filter-btn-cancel" @click="cancelAllOrders">
|
<v-btn
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
class="filter-btn filter-btn-cancel"
|
||||||
|
@click="cancelAllOrders"
|
||||||
|
>
|
||||||
<v-icon size="18">mdi-close</v-icon>
|
<v-icon size="18">mdi-close</v-icon>
|
||||||
Cancel all
|
Cancel all
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@ -151,33 +162,66 @@
|
|||||||
<div class="position-mobile-main">
|
<div class="position-mobile-main">
|
||||||
<div class="position-mobile-title">{{ pos.market }}</div>
|
<div class="position-mobile-title">{{ pos.market }}</div>
|
||||||
<div class="position-mobile-sub">
|
<div class="position-mobile-sub">
|
||||||
{{ pos.bet }} on {{ pos.outcomeWord || pos.sellOutcome || 'Position' }} to win {{ pos.toWin }}
|
{{ pos.bet }} on {{ pos.outcomeWord || pos.sellOutcome || 'Position' }} to win
|
||||||
|
{{ pos.toWin }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="position-mobile-right">
|
<div class="position-mobile-right">
|
||||||
<div class="position-value">{{ pos.value }}</div>
|
<div class="position-value">{{ pos.value }}</div>
|
||||||
<div v-if="pos.valueChange != null" :class="['position-value-change', pos.valueChangeLoss ? 'value-loss' : 'value-gain']">
|
<div
|
||||||
{{ pos.valueChange }}{{ pos.valueChangePct != null ? ` (${pos.valueChangePct})` : '' }}
|
v-if="pos.valueChange != null"
|
||||||
|
:class="[
|
||||||
|
'position-value-change',
|
||||||
|
pos.valueChangeLoss ? 'value-loss' : 'value-gain',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ pos.valueChange
|
||||||
|
}}{{ pos.valueChangePct != null ? ` (${pos.valueChangePct})` : '' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 展开内容:AVG • NOW + 操作按钮 -->
|
<!-- 展开内容:AVG • NOW + 操作按钮 -->
|
||||||
<div v-show="expandedPositionId === pos.id" class="position-mobile-expanded" @click.stop>
|
<div
|
||||||
|
v-show="expandedPositionId === pos.id"
|
||||||
|
class="position-mobile-expanded"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
<div class="position-mobile-avg-row">
|
<div class="position-mobile-avg-row">
|
||||||
<span class="avg-now-label">AVG • NOW</span>
|
<span class="avg-now-label">AVG • NOW</span>
|
||||||
<span class="avg-now-values">
|
<span class="avg-now-values">
|
||||||
<template v-if="parseAvgNow(pos.avgNow)[1]">
|
<template v-if="parseAvgNow(pos.avgNow)[1]">
|
||||||
{{ parseAvgNow(pos.avgNow)[0] }}
|
{{ parseAvgNow(pos.avgNow)[0] }}
|
||||||
<v-icon v-if="pos.valueChangeLoss" size="14" color="error" class="avg-now-arrow">mdi-chevron-down</v-icon>
|
<v-icon
|
||||||
<v-icon v-else size="14" color="success" class="avg-now-arrow">mdi-chevron-up</v-icon>
|
v-if="pos.valueChangeLoss"
|
||||||
|
size="14"
|
||||||
|
color="error"
|
||||||
|
class="avg-now-arrow"
|
||||||
|
>mdi-chevron-down</v-icon
|
||||||
|
>
|
||||||
|
<v-icon v-else size="14" color="success" class="avg-now-arrow"
|
||||||
|
>mdi-chevron-up</v-icon
|
||||||
|
>
|
||||||
{{ parseAvgNow(pos.avgNow)[1] }}
|
{{ parseAvgNow(pos.avgNow)[1] }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else>{{ pos.avgNow }}</template>
|
<template v-else>{{ pos.avgNow }}</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="position-mobile-actions">
|
<div class="position-mobile-actions">
|
||||||
<v-btn color="primary" variant="flat" size="small" class="position-sell-btn" @click="sellPosition(pos.id)">Sell</v-btn>
|
<v-btn
|
||||||
<v-btn icon variant="text" size="small" class="position-share-btn" @click="sharePosition(pos.id)">
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
size="small"
|
||||||
|
class="position-sell-btn"
|
||||||
|
@click="sellPosition(pos.id)"
|
||||||
|
>Sell</v-btn
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
class="position-share-btn"
|
||||||
|
@click="sharePosition(pos.id)"
|
||||||
|
>
|
||||||
<v-icon size="18">mdi-share-variant</v-icon>
|
<v-icon size="18">mdi-share-variant</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@ -210,13 +254,20 @@
|
|||||||
<td class="cell-market">
|
<td class="cell-market">
|
||||||
<div class="position-market-cell">
|
<div class="position-market-cell">
|
||||||
<div class="position-icon" :class="pos.iconClass">
|
<div class="position-icon" :class="pos.iconClass">
|
||||||
<v-icon v-if="pos.icon" size="20" class="position-icon-svg">{{ pos.icon }}</v-icon>
|
<v-icon v-if="pos.icon" size="20" class="position-icon-svg">{{
|
||||||
|
pos.icon
|
||||||
|
}}</v-icon>
|
||||||
<span v-else class="position-icon-char">{{ pos.iconChar }}</span>
|
<span v-else class="position-icon-char">{{ pos.iconChar }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="position-market-info">
|
<div class="position-market-info">
|
||||||
<span class="position-market-title">{{ pos.market }}</span>
|
<span class="position-market-title">{{ pos.market }}</span>
|
||||||
<div class="position-meta">
|
<div class="position-meta">
|
||||||
<span v-if="pos.outcomeTag" class="position-outcome-pill" :class="pos.outcomePillClass">{{ pos.outcomeTag }}</span>
|
<span
|
||||||
|
v-if="pos.outcomeTag"
|
||||||
|
class="position-outcome-pill"
|
||||||
|
:class="pos.outcomePillClass"
|
||||||
|
>{{ pos.outcomeTag }}</span
|
||||||
|
>
|
||||||
<span class="position-shares">{{ pos.shares }}</span>
|
<span class="position-shares">{{ pos.shares }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -227,13 +278,33 @@
|
|||||||
<td>{{ pos.toWin }}</td>
|
<td>{{ pos.toWin }}</td>
|
||||||
<td class="cell-value">
|
<td class="cell-value">
|
||||||
<div class="position-value">{{ pos.value }}</div>
|
<div class="position-value">{{ pos.value }}</div>
|
||||||
<div v-if="pos.valueChange != null" :class="['position-value-change', pos.valueChangeLoss ? 'value-loss' : 'value-gain']">
|
<div
|
||||||
{{ pos.valueChange }}{{ pos.valueChangePct != null ? ` (${pos.valueChangePct})` : '' }}
|
v-if="pos.valueChange != null"
|
||||||
|
:class="[
|
||||||
|
'position-value-change',
|
||||||
|
pos.valueChangeLoss ? 'value-loss' : 'value-gain',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ pos.valueChange
|
||||||
|
}}{{ pos.valueChangePct != null ? ` (${pos.valueChangePct})` : '' }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right cell-actions">
|
<td class="text-right cell-actions">
|
||||||
<v-btn color="primary" variant="flat" size="small" class="position-sell-btn" @click="sellPosition(pos.id)">Sell</v-btn>
|
<v-btn
|
||||||
<v-btn icon variant="text" size="small" class="position-share-btn" @click="sharePosition(pos.id)">
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
size="small"
|
||||||
|
class="position-sell-btn"
|
||||||
|
@click="sellPosition(pos.id)"
|
||||||
|
>Sell</v-btn
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
class="position-share-btn"
|
||||||
|
@click="sharePosition(pos.id)"
|
||||||
|
>
|
||||||
<v-icon size="18">mdi-share-variant</v-icon>
|
<v-icon size="18">mdi-share-variant</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</td>
|
</td>
|
||||||
@ -254,13 +325,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="order-mobile-main">
|
<div class="order-mobile-main">
|
||||||
<div class="order-mobile-title">{{ ord.market }}</div>
|
<div class="order-mobile-title">{{ ord.market }}</div>
|
||||||
<div class="order-mobile-action" :class="ord.side === 'Yes' ? 'side-yes' : 'side-no'">
|
<div
|
||||||
|
class="order-mobile-action"
|
||||||
|
:class="ord.side === 'Yes' ? 'side-yes' : 'side-no'"
|
||||||
|
>
|
||||||
{{ ord.actionLabel || `Buy ${ord.outcome}` }}
|
{{ ord.actionLabel || `Buy ${ord.outcome}` }}
|
||||||
</div>
|
</div>
|
||||||
<div class="order-mobile-price">{{ ord.price }} • {{ ord.total }}</div>
|
<div class="order-mobile-price">{{ ord.price }} • {{ ord.total }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-mobile-right">
|
<div class="order-mobile-right">
|
||||||
<v-btn icon variant="text" size="small" class="order-cancel-icon" color="error" :disabled="cancelOrderLoading" @click.stop="cancelOrder(ord)">
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
class="order-cancel-icon"
|
||||||
|
color="error"
|
||||||
|
:disabled="cancelOrderLoading"
|
||||||
|
@click.stop="cancelOrder(ord)"
|
||||||
|
>
|
||||||
<v-icon size="20">mdi-close</v-icon>
|
<v-icon size="20">mdi-close</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<div class="order-mobile-filled">{{ ord.filledDisplay || ord.filled }}</div>
|
<div class="order-mobile-filled">{{ ord.filledDisplay || ord.filled }}</div>
|
||||||
@ -297,7 +379,14 @@
|
|||||||
<td>{{ ord.total }}</td>
|
<td>{{ ord.total }}</td>
|
||||||
<td>{{ ord.expiration }}</td>
|
<td>{{ ord.expiration }}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<v-btn variant="text" size="small" color="error" :disabled="cancelOrderLoading" @click="cancelOrder(ord)">Cancel</v-btn>
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
:disabled="cancelOrderLoading"
|
||||||
|
@click="cancelOrder(ord)"
|
||||||
|
>Cancel</v-btn
|
||||||
|
>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -326,7 +415,9 @@
|
|||||||
<div class="history-mobile-activity">{{ h.activityDetail || h.activity }}</div>
|
<div class="history-mobile-activity">{{ h.activityDetail || h.activity }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="history-mobile-right">
|
<div class="history-mobile-right">
|
||||||
<span :class="['history-mobile-pl', h.profitLossNegative ? 'pl-loss' : 'pl-gain']">
|
<span
|
||||||
|
:class="['history-mobile-pl', h.profitLossNegative ? 'pl-loss' : 'pl-gain']"
|
||||||
|
>
|
||||||
{{ h.profitLoss ?? h.value }}
|
{{ h.profitLoss ?? h.value }}
|
||||||
</span>
|
</span>
|
||||||
<div class="history-mobile-time">{{ h.timeAgo || '' }}</div>
|
<div class="history-mobile-time">{{ h.timeAgo || '' }}</div>
|
||||||
@ -341,8 +432,20 @@
|
|||||||
<span v-if="h.shares" class="history-detail-label">SHARES {{ h.shares }}</span>
|
<span v-if="h.shares" class="history-detail-label">SHARES {{ h.shares }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="history-mobile-actions">
|
<div class="history-mobile-actions">
|
||||||
<v-btn variant="outlined" size="small" class="history-view-btn" @click="viewHistory(h.id)">View</v-btn>
|
<v-btn
|
||||||
<v-btn icon variant="text" size="small" class="position-share-btn" @click="shareHistory(h.id)">
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
class="history-view-btn"
|
||||||
|
@click="viewHistory(h.id)"
|
||||||
|
>View</v-btn
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
class="position-share-btn"
|
||||||
|
@click="shareHistory(h.id)"
|
||||||
|
>
|
||||||
<v-icon size="18">mdi-open-in-new</v-icon>
|
<v-icon size="18">mdi-open-in-new</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@ -400,10 +503,7 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DepositDialog
|
<DepositDialog v-model="depositDialogOpen" :balance="portfolioBalance" />
|
||||||
v-model="depositDialogOpen"
|
|
||||||
:balance="portfolioBalance"
|
|
||||||
/>
|
|
||||||
<WithdrawDialog
|
<WithdrawDialog
|
||||||
v-model="withdrawDialogOpen"
|
v-model="withdrawDialogOpen"
|
||||||
:balance="portfolioBalance"
|
:balance="portfolioBalance"
|
||||||
@ -411,10 +511,22 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Sell position dialog -->
|
<!-- Sell position dialog -->
|
||||||
<v-dialog v-model="sellDialogOpen" max-width="440" persistent content-class="sell-dialog" transition="dialog-transition">
|
<v-dialog
|
||||||
|
v-model="sellDialogOpen"
|
||||||
|
max-width="440"
|
||||||
|
persistent
|
||||||
|
content-class="sell-dialog"
|
||||||
|
transition="dialog-transition"
|
||||||
|
>
|
||||||
<v-card v-if="sellPositionItem" class="sell-dialog-card" rounded="lg">
|
<v-card v-if="sellPositionItem" class="sell-dialog-card" rounded="lg">
|
||||||
<div class="sell-dialog-header">
|
<div class="sell-dialog-header">
|
||||||
<v-btn icon variant="text" size="small" class="sell-dialog-close" @click="closeSellDialog">
|
<v-btn
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
class="sell-dialog-close"
|
||||||
|
@click="closeSellDialog"
|
||||||
|
>
|
||||||
<v-icon>mdi-close</v-icon>
|
<v-icon>mdi-close</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@ -605,7 +717,8 @@ const positions = ref<Position[]>([
|
|||||||
outcomeWord: 'Yes',
|
outcomeWord: 'Yes',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
const MOCK_TOKEN_ID = '59966088656508531737144108943848781534186324373509174641856486864137458635937'
|
const MOCK_TOKEN_ID =
|
||||||
|
'59966088656508531737144108943848781534186324373509174641856486864137458635937'
|
||||||
const openOrders = ref<OpenOrder[]>([
|
const openOrders = ref<OpenOrder[]>([
|
||||||
{
|
{
|
||||||
id: 'o1',
|
id: 'o1',
|
||||||
@ -690,7 +803,13 @@ const history = ref<HistoryItem[]>([
|
|||||||
iconChar: '₿',
|
iconChar: '₿',
|
||||||
iconClass: 'position-icon-btc',
|
iconClass: 'position-icon-btc',
|
||||||
},
|
},
|
||||||
{ id: 'h4', market: 'Will ETH merge complete by Q3?', side: 'No', activity: 'Sell No', value: '$35.20' },
|
{
|
||||||
|
id: 'h4',
|
||||||
|
market: 'Will ETH merge complete by Q3?',
|
||||||
|
side: 'No',
|
||||||
|
activity: 'Sell No',
|
||||||
|
value: '$35.20',
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
function matchSearch(text: string): boolean {
|
function matchSearch(text: string): boolean {
|
||||||
@ -713,9 +832,15 @@ const paginatedPositions = computed(() => paginate(filteredPositions.value))
|
|||||||
const paginatedOpenOrders = computed(() => paginate(filteredOpenOrders.value))
|
const paginatedOpenOrders = computed(() => paginate(filteredOpenOrders.value))
|
||||||
const paginatedHistory = computed(() => paginate(filteredHistory.value))
|
const paginatedHistory = computed(() => paginate(filteredHistory.value))
|
||||||
|
|
||||||
const totalPagesPositions = computed(() => Math.max(1, Math.ceil(filteredPositions.value.length / itemsPerPage.value)))
|
const totalPagesPositions = computed(() =>
|
||||||
const totalPagesOrders = computed(() => Math.max(1, Math.ceil(filteredOpenOrders.value.length / itemsPerPage.value)))
|
Math.max(1, Math.ceil(filteredPositions.value.length / itemsPerPage.value)),
|
||||||
const totalPagesHistory = computed(() => Math.max(1, Math.ceil(filteredHistory.value.length / itemsPerPage.value)))
|
)
|
||||||
|
const totalPagesOrders = computed(() =>
|
||||||
|
Math.max(1, Math.ceil(filteredOpenOrders.value.length / itemsPerPage.value)),
|
||||||
|
)
|
||||||
|
const totalPagesHistory = computed(() =>
|
||||||
|
Math.max(1, Math.ceil(filteredHistory.value.length / itemsPerPage.value)),
|
||||||
|
)
|
||||||
|
|
||||||
const currentListTotal = computed(() => {
|
const currentListTotal = computed(() => {
|
||||||
if (activeTab.value === 'positions') return filteredPositions.value.length
|
if (activeTab.value === 'positions') return filteredPositions.value.length
|
||||||
@ -727,10 +852,16 @@ const currentTotalPages = computed(() => {
|
|||||||
if (activeTab.value === 'orders') return totalPagesOrders.value
|
if (activeTab.value === 'orders') return totalPagesOrders.value
|
||||||
return totalPagesHistory.value
|
return totalPagesHistory.value
|
||||||
})
|
})
|
||||||
const currentPageStart = computed(() => currentListTotal.value === 0 ? 0 : (page.value - 1) * itemsPerPage.value + 1)
|
const currentPageStart = computed(() =>
|
||||||
const currentPageEnd = computed(() => Math.min(page.value * itemsPerPage.value, currentListTotal.value))
|
currentListTotal.value === 0 ? 0 : (page.value - 1) * itemsPerPage.value + 1,
|
||||||
|
)
|
||||||
|
const currentPageEnd = computed(() =>
|
||||||
|
Math.min(page.value * itemsPerPage.value, currentListTotal.value),
|
||||||
|
)
|
||||||
|
|
||||||
watch(activeTab, () => { page.value = 1 })
|
watch(activeTab, () => {
|
||||||
|
page.value = 1
|
||||||
|
})
|
||||||
watch([currentListTotal, itemsPerPage], () => {
|
watch([currentListTotal, itemsPerPage], () => {
|
||||||
const maxPage = currentTotalPages.value
|
const maxPage = currentTotalPages.value
|
||||||
if (page.value > maxPage) page.value = Math.max(1, maxPage)
|
if (page.value > maxPage) page.value = Math.max(1, maxPage)
|
||||||
@ -877,7 +1008,10 @@ function buildPlChartOption(chartData: [number, number][]) {
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
formatter: (params: unknown) => {
|
formatter: (params: unknown) => {
|
||||||
const p = (Array.isArray(params) ? params[0] : params) as { name: string | number; value: unknown }
|
const p = (Array.isArray(params) ? params[0] : params) as {
|
||||||
|
name: string | number
|
||||||
|
value: unknown
|
||||||
|
}
|
||||||
const date = new Date(p.name as number)
|
const date = new Date(p.name as number)
|
||||||
const val = Array.isArray(p.value) ? (p.value as number[])[1] : p.value
|
const val = Array.isArray(p.value) ? (p.value as number[])[1] : p.value
|
||||||
const sign = Number(val) >= 0 ? '+' : ''
|
const sign = Number(val) >= 0 ? '+' : ''
|
||||||
@ -908,7 +1042,12 @@ function buildPlChartOption(chartData: [number, number][]) {
|
|||||||
smooth: true,
|
smooth: true,
|
||||||
showSymbol: false,
|
showSymbol: false,
|
||||||
lineStyle: { width: 2, color: lineColor },
|
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' }]) },
|
areaStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: lineColor + '40' },
|
||||||
|
{ offset: 1, color: lineColor + '08' },
|
||||||
|
]),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@ -920,7 +1059,8 @@ function updatePlChart() {
|
|||||||
plChartData.value = generatePlData(plRange.value)
|
plChartData.value = generatePlData(plRange.value)
|
||||||
const last = plChartData.value[plChartData.value.length - 1]
|
const last = plChartData.value[plChartData.value.length - 1]
|
||||||
if (last) profitLoss.value = last[1].toFixed(2)
|
if (last) profitLoss.value = last[1].toFixed(2)
|
||||||
if (plChartInstance) plChartInstance.setOption(buildPlChartOption(plChartData.value), { replaceMerge: ['series'] })
|
if (plChartInstance)
|
||||||
|
plChartInstance.setOption(buildPlChartOption(plChartData.value), { replaceMerge: ['series'] })
|
||||||
}
|
}
|
||||||
|
|
||||||
function initPlChart() {
|
function initPlChart() {
|
||||||
|
|||||||
5
src/vite-env.d.ts
vendored
5
src/vite-env.d.ts
vendored
@ -10,10 +10,7 @@ declare module '*.vue' {
|
|||||||
declare interface Window {
|
declare interface Window {
|
||||||
ethereum?: {
|
ethereum?: {
|
||||||
isMetaMask?: boolean
|
isMetaMask?: boolean
|
||||||
request: (args: {
|
request: (args: { method: string; params?: any[] }) => Promise<any>
|
||||||
method: string
|
|
||||||
params?: any[]
|
|
||||||
}) => Promise<any>
|
|
||||||
on: (event: string, callback: (...args: any[]) => void) => void
|
on: (event: string, callback: (...args: any[]) => void) => void
|
||||||
removeListener: (event: string, callback: (...args: any[]) => void) => void
|
removeListener: (event: string, callback: (...args: any[]) => void) => void
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user