新增:对接顶部分类接口
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>。
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 强制执行:接口接入三步流程
|
||||
|
||||
**接入任意 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`
|
||||
@ -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. **请求参数**
|
||||
- **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. **响应参数**
|
||||
- 取 `paths["<path>"]["<method>"].responses["200"].schema`。
|
||||
- 若有 `allOf`,合并得到根结构(通常为 `code`、`data`、`msg`)。
|
||||
- 对 `data` 及其他嵌套对象的 `$ref`,到 `definitions` 中查完整结构并列出字段(名称、类型、说明)。
|
||||
- 对 `data` 及其他嵌套对象的 `$ref`,到 `definitions` 中查完整结构并**列出所有字段**(名称、类型、说明)。
|
||||
|
||||
输出形式可为表格或结构化列表,便于第二步写类型。
|
||||
**输出形式**:表格或结构化列表,便于第二步写类型。**未完成第一步输出前,禁止进入第二步。**
|
||||
|
||||
### 第二步:根据响应数据创建 Model 类
|
||||
|
||||
|
||||
@ -3,6 +3,9 @@
|
||||
# 连接测试服务器时复制本文件为 .env 并取消下一行注释:
|
||||
# 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
|
||||
|
||||
# SSH 部署(npm run deploy),不配置时使用默认值
|
||||
|
||||
12
src/App.vue
12
src/App.vue
@ -32,7 +32,12 @@ onMounted(() => {
|
||||
</v-btn>
|
||||
<v-app-bar-title v-if="currentRoute === '/'">PolyMarket</v-app-bar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="!userStore.isLoggedIn" text to="/login" :class="{ active: currentRoute === '/login' }">
|
||||
<v-btn
|
||||
v-if="!userStore.isLoggedIn"
|
||||
text
|
||||
to="/login"
|
||||
:class="{ active: currentRoute === '/login' }"
|
||||
>
|
||||
Login
|
||||
</v-btn>
|
||||
<template v-else>
|
||||
@ -55,7 +60,10 @@ onMounted(() => {
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item :title="userStore.user?.nickName || userStore.user?.userName || 'User'" disabled />
|
||||
<v-list-item
|
||||
:title="userStore.user?.nickName || userStore.user?.userName || 'User'"
|
||||
disabled
|
||||
/>
|
||||
<v-list-item title="退出登录" @click="userStore.logout()" />
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
@ -1,5 +1,29 @@
|
||||
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 {
|
||||
id: string
|
||||
@ -92,9 +116,11 @@ export interface CategoryTreeResponse {
|
||||
* data 可能为数组,或 { list: [] } 等格式,统一转为 CategoryTreeNode[]
|
||||
*/
|
||||
export async function getCategoryTree(): Promise<CategoryTreeResponse> {
|
||||
const res = await get<{ code: number; data: CategoryTreeNode[] | { list?: CategoryTreeNode[] }; msg: string }>(
|
||||
'/PmTag/getPmTagPublic'
|
||||
)
|
||||
const res = await get<{
|
||||
code: number
|
||||
data: CategoryTreeNode[] | { list?: CategoryTreeNode[] }
|
||||
msg: string
|
||||
}>('/PmTag/getPmTagPublic')
|
||||
let data: CategoryTreeNode[] = []
|
||||
const raw = res.data
|
||||
if (Array.isArray(raw)) {
|
||||
@ -108,3 +134,32 @@ export async function getCategoryTree(): Promise<CategoryTreeResponse> {
|
||||
}
|
||||
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/第二选项 */
|
||||
export function getClobTokenId(
|
||||
m: PmEventMarketItem | null | undefined,
|
||||
outcomeIndex: 0 | 1 = 0
|
||||
outcomeIndex: 0 | 1 = 0,
|
||||
): string | undefined {
|
||||
if (!m?.clobTokenIds?.length) return undefined
|
||||
const id = m.clobTokenIds[outcomeIndex]
|
||||
@ -131,7 +131,7 @@ export interface GetPmEventListParams {
|
||||
* tokenid 对应 market.clobTokenIds 中的值,可传单个或数组
|
||||
*/
|
||||
export async function getPmEventPublic(
|
||||
params: GetPmEventListParams = {}
|
||||
params: GetPmEventListParams = {},
|
||||
): Promise<PmEventListResponse> {
|
||||
const { page = 1, pageSize = 10, keyword, createdAtRange, tokenid } = params
|
||||
const query: Record<string, string | number | string[] | undefined> = {
|
||||
@ -182,7 +182,7 @@ export interface FindPmEventParams {
|
||||
*/
|
||||
export async function findPmEvent(
|
||||
params: FindPmEventParams,
|
||||
config?: { headers?: Record<string, string> }
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<PmEventDetailResponse> {
|
||||
const query: Record<string, string | number> = {}
|
||||
if (params.id != null) query.ID = params.id
|
||||
@ -304,7 +304,11 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
||||
try {
|
||||
const d = new Date(item.endDate)
|
||||
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 {
|
||||
expiresAt = item.endDate
|
||||
|
||||
@ -32,7 +32,7 @@ export interface ClobSubmitOrderRequest {
|
||||
*/
|
||||
export async function pmOrderPlace(
|
||||
data: ClobSubmitOrderRequest,
|
||||
config?: { headers?: Record<string, string> }
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<ApiResponse> {
|
||||
return post<ApiResponse>('/clob/gateway/submitOrder', data, config)
|
||||
}
|
||||
@ -52,7 +52,7 @@ export interface ClobCancelOrderRequest {
|
||||
*/
|
||||
export async function pmCancelOrder(
|
||||
data: ClobCancelOrderRequest,
|
||||
config?: { headers?: Record<string, string> }
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<ApiResponse> {
|
||||
return post<ApiResponse>('/clob/gateway/cancelOrder', data, config)
|
||||
}
|
||||
@ -85,7 +85,7 @@ export interface PmMarketMergeRequest {
|
||||
*/
|
||||
export async function pmMarketMerge(
|
||||
data: PmMarketMergeRequest,
|
||||
config?: { headers?: Record<string, string> }
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<ApiResponse> {
|
||||
return post<ApiResponse>('/PmMarket/merge', data, config)
|
||||
}
|
||||
@ -97,7 +97,7 @@ export async function pmMarketMerge(
|
||||
*/
|
||||
export async function pmMarketSplit(
|
||||
data: PmMarketSplitRequest,
|
||||
config?: { headers?: Record<string, string> }
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<ApiResponse> {
|
||||
return post<ApiResponse>('/PmMarket/split', data, config)
|
||||
}
|
||||
|
||||
@ -124,14 +124,47 @@ export const MOCK_EVENT_LIST: PmEventListItem[] = [
|
||||
endDate: '2026-02-10T23:59:59.000Z',
|
||||
new: true,
|
||||
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: 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 },
|
||||
{
|
||||
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: 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' }],
|
||||
tags: [{ label: 'Tech', slug: 'tech' }, { label: 'Twitter', slug: 'twitter' }],
|
||||
tags: [
|
||||
{ label: 'Tech', slug: 'tech' },
|
||||
{ label: 'Twitter', slug: 'twitter' },
|
||||
],
|
||||
},
|
||||
// 6. 多 market:总统候选人胜选概率(多候选人)
|
||||
{
|
||||
@ -145,9 +178,27 @@ export const MOCK_EVENT_LIST: PmEventListItem[] = [
|
||||
endDate: '2028-11-07T23:59:59.000Z',
|
||||
new: true,
|
||||
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: 90063, question: 'Third party / Independent', outcomes: ['Yes', 'No'], outcomePrices: [0.07, 0.93], volume: 800000 },
|
||||
{
|
||||
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: 90063,
|
||||
question: 'Third party / Independent',
|
||||
outcomes: ['Yes', 'No'],
|
||||
outcomePrices: [0.07, 0.93],
|
||||
volume: 800000,
|
||||
},
|
||||
],
|
||||
series: [{ ID: 6, title: 'Politics', ticker: 'POL' }],
|
||||
tags: [{ label: 'Election', slug: 'election' }],
|
||||
@ -164,10 +215,34 @@ export const MOCK_EVENT_LIST: PmEventListItem[] = [
|
||||
endDate: '2026-05-31T23:59:59.000Z',
|
||||
new: false,
|
||||
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: 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 },
|
||||
{
|
||||
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: 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' }],
|
||||
tags: [{ label: 'NBA', slug: 'nba' }],
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
/**
|
||||
* 请求基础 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 =
|
||||
typeof import.meta !== 'undefined' &&
|
||||
(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'
|
||||
|
||||
@ -16,7 +18,7 @@ export interface RequestConfig {
|
||||
export async function get<T = unknown>(
|
||||
path: string,
|
||||
params?: Record<string, string | number | string[] | undefined>,
|
||||
config?: RequestConfig
|
||||
config?: RequestConfig,
|
||||
): Promise<T> {
|
||||
const url = new URL(path, BASE_URL || window.location.origin)
|
||||
if (params) {
|
||||
@ -46,7 +48,7 @@ export async function get<T = unknown>(
|
||||
export async function post<T = unknown>(
|
||||
path: string,
|
||||
body?: unknown,
|
||||
config?: RequestConfig
|
||||
config?: RequestConfig,
|
||||
): Promise<T> {
|
||||
const url = new URL(path, BASE_URL || window.location.origin)
|
||||
const headers: Record<string, string> = {
|
||||
|
||||
@ -26,7 +26,9 @@ export function formatUsdcBalance(raw: string): string {
|
||||
* 查询 USDC 余额,需鉴权(x-token)
|
||||
* 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, {
|
||||
headers: authHeaders,
|
||||
})
|
||||
|
||||
@ -81,13 +81,7 @@
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="qr-wrap">
|
||||
<img
|
||||
:src="qrCodeUrl"
|
||||
alt="QR Code"
|
||||
class="qr-img"
|
||||
width="120"
|
||||
height="120"
|
||||
/>
|
||||
<img :src="qrCodeUrl" alt="QR Code" class="qr-img" width="120" height="120" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -101,7 +95,10 @@
|
||||
<span class="step-title">Connect Exchange</span>
|
||||
</div>
|
||||
<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">
|
||||
<v-btn
|
||||
class="wallet-btn"
|
||||
@ -114,23 +111,11 @@
|
||||
<v-icon start>mdi-wallet</v-icon>
|
||||
MetaMask
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="wallet-btn"
|
||||
variant="outlined"
|
||||
rounded="lg"
|
||||
block
|
||||
disabled
|
||||
>
|
||||
<v-btn class="wallet-btn" variant="outlined" rounded="lg" block disabled>
|
||||
<v-icon start>mdi-wallet</v-icon>
|
||||
Coinbase Wallet (Coming soon)
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="wallet-btn"
|
||||
variant="outlined"
|
||||
rounded="lg"
|
||||
block
|
||||
disabled
|
||||
>
|
||||
<v-btn class="wallet-btn" variant="outlined" rounded="lg" block disabled>
|
||||
<v-icon start>mdi-wallet</v-icon>
|
||||
WalletConnect (Coming soon)
|
||||
</v-btn>
|
||||
@ -168,7 +153,7 @@ const props = withDefaults(
|
||||
modelValue: boolean
|
||||
balance: string
|
||||
}>(),
|
||||
{ balance: '0.00' }
|
||||
{ balance: '0.00' },
|
||||
)
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
|
||||
@ -209,7 +194,9 @@ async function copyAddress() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(DEPOSIT_ADDRESS)
|
||||
copied.value = true
|
||||
setTimeout(() => { copied.value = false }, 2000)
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 2000)
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
@ -238,7 +225,7 @@ watch(
|
||||
step.value = 'method'
|
||||
exchangeConnected.value = false
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@ -127,7 +127,7 @@
|
||||
</button>
|
||||
<div class="carousel-dots">
|
||||
<button
|
||||
v-for="(_, idx) in (props.outcomes ?? [])"
|
||||
v-for="(_, idx) in props.outcomes ?? []"
|
||||
:key="idx"
|
||||
type="button"
|
||||
:class="['carousel-dot', { active: currentSlide === idx }]"
|
||||
@ -171,7 +171,13 @@ const router = useRouter()
|
||||
const emit = defineEmits<{
|
||||
openTrade: [
|
||||
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',
|
||||
isNew: false,
|
||||
marketId: undefined,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const isMulti = computed(
|
||||
() => props.displayType === 'multi' && (props.outcomes?.length ?? 0) > 1
|
||||
)
|
||||
const isMulti = computed(() => props.displayType === 'multi' && (props.outcomes?.length ?? 0) > 1)
|
||||
const currentSlide = ref(0)
|
||||
const outcomeCount = computed(() => (props.outcomes ?? []).length)
|
||||
|
||||
@ -256,14 +260,14 @@ const semiProgressColor = computed(() => {
|
||||
return rgbToHex(
|
||||
COLOR_RED.r + (COLOR_ORANGE_YELLOW.r - COLOR_RED.r) * 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
|
||||
return rgbToHex(
|
||||
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.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;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
color 0.2s;
|
||||
box-shadow: none;
|
||||
}
|
||||
.carousel-arrow:hover:not(:disabled) {
|
||||
|
||||
@ -367,7 +367,6 @@ const maxBidsTotal = computed(() => {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
.asks-label,
|
||||
.bids-label {
|
||||
font-size: 12px;
|
||||
|
||||
@ -86,7 +86,12 @@
|
||||
|
||||
<p v-if="orderError" class="order-error">{{ orderError }}</p>
|
||||
<!-- 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 }}
|
||||
</v-btn>
|
||||
</template>
|
||||
@ -133,12 +138,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Deposit Button -->
|
||||
<v-btn
|
||||
class="deposit-btn"
|
||||
@click="deposit"
|
||||
>
|
||||
Deposit
|
||||
</v-btn>
|
||||
<v-btn class="deposit-btn" @click="deposit"> Deposit </v-btn>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@ -276,7 +276,12 @@
|
||||
</div>
|
||||
|
||||
<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 }}
|
||||
</v-btn>
|
||||
</template>
|
||||
@ -299,40 +304,95 @@
|
||||
</v-btn>
|
||||
</template>
|
||||
<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 = 'Limit'"><v-list-item-title>Limit</v-list-item-title></v-list-item>
|
||||
<v-list-item @click="limitType = 'Market'"
|
||||
><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-list-item @click="openMergeDialog"><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-item @click="openMergeDialog"
|
||||
><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-menu>
|
||||
</div>
|
||||
<template v-if="isMarketMode">
|
||||
<template v-if="balance > 0">
|
||||
<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>
|
||||
<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="total-section">
|
||||
<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"><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>
|
||||
<div class="total-row">
|
||||
<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 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>
|
||||
<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>
|
||||
<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>
|
||||
<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">
|
||||
@ -347,16 +407,44 @@
|
||||
</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>
|
||||
<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">
|
||||
<span class="label">Limit Price</span>
|
||||
<div class="price-input">
|
||||
<v-btn class="adjust-btn" icon @click="decreasePrice"><v-icon>mdi-minus</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>
|
||||
<v-btn class="adjust-btn" icon @click="decreasePrice"
|
||||
><v-icon>mdi-minus</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>
|
||||
@ -364,7 +452,17 @@
|
||||
<div class="shares-header">
|
||||
<span class="label">Shares</span>
|
||||
<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 v-if="activeTab === 'buy'" class="shares-buttons">
|
||||
@ -382,7 +480,12 @@
|
||||
<div class="input-group expiration-group">
|
||||
<div class="expiration-header">
|
||||
<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>
|
||||
<v-select
|
||||
v-if="expirationEnabled"
|
||||
@ -395,15 +498,33 @@
|
||||
</div>
|
||||
<div class="total-section">
|
||||
<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"><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>
|
||||
<div class="total-row">
|
||||
<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 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>
|
||||
<v-btn
|
||||
class="action-btn"
|
||||
:loading="orderLoading"
|
||||
:disabled="orderLoading"
|
||||
@click="submitOrder"
|
||||
>{{ actionButtonText }}</v-btn
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</v-sheet>
|
||||
@ -451,40 +572,96 @@
|
||||
</v-btn>
|
||||
</template>
|
||||
<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 = 'Limit'"><v-list-item-title>Limit</v-list-item-title></v-list-item>
|
||||
<v-list-item @click="limitType = 'Market'"
|
||||
><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-list-item @click="openMergeDialog"><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-item @click="openMergeDialog"
|
||||
><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-menu>
|
||||
</div>
|
||||
<template v-if="isMarketMode">
|
||||
<template v-if="balance > 0">
|
||||
<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>
|
||||
<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="total-section">
|
||||
<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"><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>
|
||||
<div class="total-row">
|
||||
<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 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>
|
||||
<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>
|
||||
<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>
|
||||
<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">
|
||||
@ -499,16 +676,44 @@
|
||||
</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>
|
||||
<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">
|
||||
<span class="label">Limit Price</span>
|
||||
<div class="price-input">
|
||||
<v-btn class="adjust-btn" icon @click="decreasePrice"><v-icon>mdi-minus</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>
|
||||
<v-btn class="adjust-btn" icon @click="decreasePrice"
|
||||
><v-icon>mdi-minus</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>
|
||||
@ -516,7 +721,17 @@
|
||||
<div class="shares-header">
|
||||
<span class="label">Shares</span>
|
||||
<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 v-if="activeTab === 'buy'" class="shares-buttons">
|
||||
@ -534,7 +749,12 @@
|
||||
<div class="input-group expiration-group">
|
||||
<div class="expiration-header">
|
||||
<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>
|
||||
<v-select
|
||||
v-if="expirationEnabled"
|
||||
@ -547,15 +767,35 @@
|
||||
</div>
|
||||
<div class="total-section">
|
||||
<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"><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>
|
||||
<div class="total-row">
|
||||
<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 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>
|
||||
<v-btn
|
||||
class="action-btn"
|
||||
:loading="orderLoading"
|
||||
:disabled="orderLoading"
|
||||
@click="submitOrder"
|
||||
>{{ actionButtonText }}</v-btn
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</v-sheet>
|
||||
@ -563,17 +803,30 @@
|
||||
</template>
|
||||
|
||||
<!-- 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">
|
||||
<div class="merge-dialog-header">
|
||||
<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-btn>
|
||||
</div>
|
||||
<v-card-text class="merge-dialog-body">
|
||||
<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>
|
||||
<div class="merge-amount-row">
|
||||
<label class="merge-amount-label">Amount</label>
|
||||
@ -591,7 +844,9 @@
|
||||
Available shares: {{ availableMergeShares }}
|
||||
<button type="button" class="merge-max-link" @click="setMergeMax">Max</button>
|
||||
</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>
|
||||
</v-card-text>
|
||||
<v-card-actions class="merge-dialog-actions">
|
||||
@ -611,17 +866,30 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 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">
|
||||
<div class="split-dialog-header">
|
||||
<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-btn>
|
||||
</div>
|
||||
<v-card-text class="split-dialog-body">
|
||||
<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>
|
||||
<div class="split-amount-row">
|
||||
<label class="split-amount-label">Amount (USDC)</label>
|
||||
@ -636,7 +904,9 @@
|
||||
class="split-amount-input"
|
||||
/>
|
||||
</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>
|
||||
</v-card-text>
|
||||
<v-card-actions class="split-dialog-actions">
|
||||
@ -682,7 +952,7 @@ const props = withDefaults(
|
||||
/** 从外部传入的市场数据(如 EventMarkets 点击 Yes/No 传入),yesPrice/noPrice 为 0–1 */
|
||||
market?: TradeMarketPayload
|
||||
}>(),
|
||||
{ initialOption: undefined, embeddedInSheet: false, market: undefined }
|
||||
{ initialOption: undefined, embeddedInSheet: false, market: undefined },
|
||||
)
|
||||
|
||||
// 移动端:底部栏与弹出层
|
||||
@ -711,7 +981,7 @@ async function submitMerge() {
|
||||
try {
|
||||
const res = await pmMarketMerge(
|
||||
{ marketID: marketId, amount: String(mergeAmount.value) },
|
||||
{ headers: userStore.getAuthHeaders() }
|
||||
{ headers: userStore.getAuthHeaders() },
|
||||
)
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
mergeDialogOpen.value = false
|
||||
@ -745,7 +1015,7 @@ async function submitSplit() {
|
||||
try {
|
||||
const res = await pmMarketSplit(
|
||||
{ marketID: marketId, usdcAmount: String(splitAmount.value) },
|
||||
{ headers: userStore.getAuthHeaders() }
|
||||
{ headers: userStore.getAuthHeaders() },
|
||||
)
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
splitDialogOpen.value = false
|
||||
@ -759,12 +1029,8 @@ async function submitSplit() {
|
||||
}
|
||||
}
|
||||
|
||||
const yesPriceCents = computed(() =>
|
||||
props.market ? Math.round(props.market.yesPrice * 100) : 19
|
||||
)
|
||||
const noPriceCents = computed(() =>
|
||||
props.market ? Math.round(props.market.noPrice * 100) : 82
|
||||
)
|
||||
const yesPriceCents = computed(() => (props.market ? Math.round(props.market.yesPrice * 100) : 19))
|
||||
const noPriceCents = computed(() => (props.market ? Math.round(props.market.noPrice * 100) : 82))
|
||||
|
||||
function openSheet(option: 'yes' | 'no') {
|
||||
handleOptionChange(option)
|
||||
@ -792,7 +1058,8 @@ const orderError = ref('')
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
optionChange: [option: 'yes' | 'no']
|
||||
submit: [payload: {
|
||||
submit: [
|
||||
payload: {
|
||||
side: 'buy' | 'sell'
|
||||
option: 'yes' | 'no'
|
||||
limitPrice: number
|
||||
@ -800,7 +1067,8 @@ const emit = defineEmits<{
|
||||
expirationEnabled: boolean
|
||||
expirationTime: string
|
||||
marketId?: string
|
||||
}]
|
||||
},
|
||||
]
|
||||
}>()
|
||||
|
||||
// Computed properties
|
||||
@ -836,17 +1104,25 @@ onMounted(() => {
|
||||
if (props.initialOption) applyInitialOption(props.initialOption)
|
||||
else if (props.market) syncLimitPriceFromMarket()
|
||||
})
|
||||
watch(() => props.initialOption, (option) => {
|
||||
watch(
|
||||
() => props.initialOption,
|
||||
(option) => {
|
||||
if (option) applyInitialOption(option)
|
||||
}, { immediate: true })
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(() => props.market, (m) => {
|
||||
watch(
|
||||
() => props.market,
|
||||
(m) => {
|
||||
if (m) {
|
||||
orderError.value = ''
|
||||
if (props.initialOption) applyInitialOption(props.initialOption)
|
||||
else syncLimitPriceFromMarket()
|
||||
}
|
||||
}, { deep: true })
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// Methods
|
||||
const handleOptionChange = (option: 'yes' | 'no') => {
|
||||
@ -1031,7 +1307,7 @@ async function submitOrder() {
|
||||
tokenID: tokenId,
|
||||
userID: userIdNum,
|
||||
},
|
||||
{ headers }
|
||||
{ headers },
|
||||
)
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
userStore.fetchUsdcBalance()
|
||||
|
||||
@ -110,7 +110,7 @@ const props = withDefaults(
|
||||
modelValue: boolean
|
||||
balance: string
|
||||
}>(),
|
||||
{ balance: '0.00' }
|
||||
{ balance: '0.00' },
|
||||
)
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean]; success: [] }>()
|
||||
|
||||
@ -145,7 +145,11 @@ const hasValidDestination = 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) {
|
||||
@ -188,7 +192,8 @@ async function submitWithdraw() {
|
||||
submitting.value = true
|
||||
try {
|
||||
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 })
|
||||
emit('success')
|
||||
close()
|
||||
@ -206,7 +211,7 @@ watch(
|
||||
customAddress.value = ''
|
||||
connectedAddress.value = ''
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@ -25,8 +25,8 @@ export default createVuetify({
|
||||
success: '#34A853',
|
||||
warning: '#FBBC05',
|
||||
surface: '#FFFFFF',
|
||||
background: '#F5F5F5'
|
||||
}
|
||||
background: '#F5F5F5',
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
dark: true,
|
||||
@ -39,9 +39,9 @@ export default createVuetify({
|
||||
success: '#4CAF50',
|
||||
warning: '#FFC107',
|
||||
surface: '#1E1E1E',
|
||||
background: '#121212'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
background: '#121212',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -12,33 +12,33 @@ const router = createRouter({
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: Home
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: '/trade',
|
||||
name: 'trade',
|
||||
component: Trade
|
||||
component: Trade,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login
|
||||
component: Login,
|
||||
},
|
||||
{
|
||||
path: '/trade-detail/:id',
|
||||
name: 'trade-detail',
|
||||
component: TradeDetail
|
||||
component: TradeDetail,
|
||||
},
|
||||
{
|
||||
path: '/event/:id/markets',
|
||||
name: 'event-markets',
|
||||
component: EventMarkets
|
||||
component: EventMarkets,
|
||||
},
|
||||
{
|
||||
path: '/wallet',
|
||||
name: 'wallet',
|
||||
component: Wallet
|
||||
}
|
||||
component: Wallet,
|
||||
},
|
||||
],
|
||||
scrollBehavior(to, from, 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 type { ECharts } from 'echarts'
|
||||
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 { useUserStore } from '../stores/user'
|
||||
|
||||
@ -220,7 +226,16 @@ const chartData = ref<ChartSeriesItem[]>([])
|
||||
let chartInstance: ECharts | null = null
|
||||
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
|
||||
|
||||
function getStepAndCount(range: string): { stepMs: number; count: number } {
|
||||
@ -371,7 +386,8 @@ function initChart() {
|
||||
function updateChartData() {
|
||||
chartData.value = generateAllData()
|
||||
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) {
|
||||
@ -396,7 +412,8 @@ function startDynamicUpdate() {
|
||||
})
|
||||
chartData.value = nextData
|
||||
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)
|
||||
}
|
||||
|
||||
@ -491,7 +508,7 @@ async function loadEventDetail() {
|
||||
const slugFromQuery = (route.query.slug as string)?.trim()
|
||||
const params: FindPmEventParams = {
|
||||
id: isNumericId ? numId : undefined,
|
||||
slug: isNumericId ? (slugFromQuery || undefined) : idStr,
|
||||
slug: isNumericId ? slugFromQuery || undefined : idStr,
|
||||
}
|
||||
|
||||
detailError.value = null
|
||||
@ -552,11 +569,11 @@ watch(
|
||||
if (dynamicInterval == null) startDynamicUpdate()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
watch(
|
||||
() => route.params.id,
|
||||
() => loadEventDetail()
|
||||
() => loadEventDetail(),
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@ -23,7 +23,13 @@
|
||||
>
|
||||
<v-icon size="20">mdi-magnify</v-icon>
|
||||
</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-btn>
|
||||
</div>
|
||||
@ -44,7 +50,14 @@
|
||||
@blur="onSearchBlur"
|
||||
@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-btn>
|
||||
</div>
|
||||
@ -67,7 +80,11 @@
|
||||
:key="`${item}-${idx}`"
|
||||
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 }}
|
||||
</button>
|
||||
<v-btn
|
||||
@ -105,7 +122,10 @@
|
||||
</button>
|
||||
</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
|
||||
:model-value="layerActiveValues[2]"
|
||||
class="home-tab-bar home-tab-bar--compact"
|
||||
@ -184,11 +204,7 @@
|
||||
:initial-option="tradeDialogSide"
|
||||
/>
|
||||
</v-dialog>
|
||||
<v-bottom-sheet
|
||||
v-else
|
||||
v-model="tradeDialogOpen"
|
||||
content-class="trade-bottom-sheet"
|
||||
>
|
||||
<v-bottom-sheet v-else v-model="tradeDialogOpen" content-class="trade-bottom-sheet">
|
||||
<TradeComponent
|
||||
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
|
||||
:market="homeTradeMarketPayload"
|
||||
@ -266,7 +282,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</footer>
|
||||
@ -287,7 +307,7 @@ import {
|
||||
clearEventListCache,
|
||||
type EventCardItem,
|
||||
} 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'
|
||||
|
||||
const { mobile } = useDisplay()
|
||||
@ -434,16 +454,27 @@ const loadingMore = ref(false)
|
||||
|
||||
const noMoreEvents = computed(() => {
|
||||
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 tradeDialogOpen = ref(false)
|
||||
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)
|
||||
|
||||
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
|
||||
tradeDialogMarket.value = market ?? null
|
||||
tradeDialogOpen.value = true
|
||||
@ -492,9 +523,7 @@ const activeSearchKeyword = ref('')
|
||||
async function loadEvents(page: number, append: boolean, keyword?: string) {
|
||||
const kw = keyword !== undefined ? keyword : activeSearchKeyword.value
|
||||
try {
|
||||
const res = await getPmEventPublic(
|
||||
{ page, pageSize: PAGE_SIZE, keyword: kw || undefined }
|
||||
)
|
||||
const res = await getPmEventPublic({ page, pageSize: PAGE_SIZE, keyword: kw || undefined })
|
||||
if (res.code !== 0 && res.code !== 200) {
|
||||
throw new Error(res.msg || '请求失败')
|
||||
}
|
||||
@ -548,12 +577,12 @@ function checkScrollLoad() {
|
||||
|
||||
onMounted(() => {
|
||||
/** 开发时设为 true 可始终使用模拟数据查看一二三层 UI */
|
||||
const USE_MOCK_CATEGORY = true
|
||||
const USE_MOCK_CATEGORY = false
|
||||
if (USE_MOCK_CATEGORY) {
|
||||
categoryTree.value = MOCK_CATEGORY_TREE
|
||||
initCategorySelection()
|
||||
} else {
|
||||
getCategoryTree()
|
||||
getPmTagMain()
|
||||
.then((res) => {
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
const data = res.data
|
||||
@ -586,7 +615,7 @@ onMounted(() => {
|
||||
if (!entries[0]?.isIntersecting) return
|
||||
loadMore()
|
||||
},
|
||||
{ root: null, rootMargin: `${SCROLL_LOAD_THRESHOLD}px`, threshold: 0 }
|
||||
{ root: null, rootMargin: `${SCROLL_LOAD_THRESHOLD}px`, threshold: 0 },
|
||||
)
|
||||
observer.observe(sentinel)
|
||||
}
|
||||
@ -715,7 +744,6 @@ onUnmounted(() => {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
.home-subtitle {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
@ -802,7 +830,9 @@ onUnmounted(() => {
|
||||
|
||||
.home-search-overlay-enter-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,
|
||||
@ -935,7 +965,9 @@ onUnmounted(() => {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
color 0.2s;
|
||||
}
|
||||
|
||||
.home-category-icon-item:hover {
|
||||
|
||||
@ -175,7 +175,7 @@ const connectWithWallet = async () => {
|
||||
nonce,
|
||||
signature,
|
||||
walletAddress,
|
||||
}
|
||||
},
|
||||
)
|
||||
console.log('Login API response:', loginData)
|
||||
|
||||
|
||||
@ -80,18 +80,21 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="activity-list">
|
||||
<div
|
||||
v-for="item in filteredActivity"
|
||||
:key="item.id"
|
||||
class="activity-item"
|
||||
>
|
||||
<div v-for="item in filteredActivity" :key="item.id" class="activity-item">
|
||||
<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 class="activity-body">
|
||||
<span class="activity-user">{{ item.user }}</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 }}
|
||||
</span>
|
||||
<span class="activity-price">at {{ item.price }}</span>
|
||||
@ -113,10 +116,7 @@
|
||||
<!-- 右侧:交易组件(固定宽度),传入当前市场以便 Split 调用拆单接口 -->
|
||||
<v-col cols="12" class="trade-col">
|
||||
<div class="trade-sidebar">
|
||||
<TradeComponent
|
||||
:market="tradeMarketPayload"
|
||||
:initial-option="tradeInitialOption"
|
||||
/>
|
||||
<TradeComponent :market="tradeMarketPayload" :initial-option="tradeInitialOption" />
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -130,7 +130,12 @@ import * as echarts from 'echarts'
|
||||
import type { ECharts } from 'echarts'
|
||||
import OrderBook from '../components/OrderBook.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'
|
||||
|
||||
/**
|
||||
@ -198,14 +203,14 @@ async function loadEventDetail() {
|
||||
const slugFromQuery = (route.query.slug as string)?.trim()
|
||||
const params: FindPmEventParams = {
|
||||
id: isNumericId ? numId : undefined,
|
||||
slug: isNumericId ? (slugFromQuery || undefined) : idStr,
|
||||
slug: isNumericId ? slugFromQuery || undefined : idStr,
|
||||
}
|
||||
|
||||
detailError.value = null
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const res = await findPmEvent(params, {
|
||||
headers: userStore.getAuthHeaders()
|
||||
headers: userStore.getAuthHeaders(),
|
||||
})
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
eventDetail.value = res.data ?? null
|
||||
@ -313,12 +318,72 @@ interface ActivityItem {
|
||||
time: number
|
||||
}
|
||||
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: '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 },
|
||||
{
|
||||
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: '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(() => {
|
||||
@ -461,7 +526,10 @@ function buildOption(chartData: [number, number][], containerWidth?: number) {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
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 val = Array.isArray(p.value) ? p.value[1] : p.value
|
||||
return (
|
||||
@ -525,7 +593,8 @@ function initChart() {
|
||||
function updateChartData() {
|
||||
data.value = generateData(selectedTimeRange.value)
|
||||
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) {
|
||||
@ -535,13 +604,19 @@ function selectTimeRange(range: string) {
|
||||
|
||||
function getMaxPoints(range: string): number {
|
||||
switch (range) {
|
||||
case '1H': return 60
|
||||
case '6H': return 36
|
||||
case '1D': return 24
|
||||
case '1W': return 7
|
||||
case '1H':
|
||||
return 60
|
||||
case '6H':
|
||||
return 36
|
||||
case '1D':
|
||||
return 24
|
||||
case '1W':
|
||||
return 7
|
||||
case '1M':
|
||||
case 'ALL': return 30
|
||||
default: return 24
|
||||
case 'ALL':
|
||||
return 30
|
||||
default:
|
||||
return 24
|
||||
}
|
||||
}
|
||||
|
||||
@ -556,7 +631,8 @@ function startDynamicUpdate() {
|
||||
const max = getMaxPoints(selectedTimeRange.value)
|
||||
data.value = list.slice(-max)
|
||||
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)
|
||||
}
|
||||
|
||||
@ -580,7 +656,7 @@ const handleResize = () => {
|
||||
watch(
|
||||
() => route.params.id,
|
||||
() => loadEventDetail(),
|
||||
{ immediate: false }
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
@ -1030,8 +1106,13 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
@keyframes live-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
|
||||
@ -88,7 +88,13 @@
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
/>
|
||||
<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>
|
||||
Close Losses
|
||||
</v-btn>
|
||||
@ -119,7 +125,12 @@
|
||||
<v-icon size="18">mdi-filter</v-icon>
|
||||
Market
|
||||
</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>
|
||||
Cancel all
|
||||
</v-btn>
|
||||
@ -151,33 +162,66 @@
|
||||
<div class="position-mobile-main">
|
||||
<div class="position-mobile-title">{{ pos.market }}</div>
|
||||
<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 class="position-mobile-right">
|
||||
<div class="position-value">{{ pos.value }}</div>
|
||||
<div v-if="pos.valueChange != null" :class="['position-value-change', pos.valueChangeLoss ? 'value-loss' : 'value-gain']">
|
||||
{{ pos.valueChange }}{{ pos.valueChangePct != null ? ` (${pos.valueChangePct})` : '' }}
|
||||
<div
|
||||
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>
|
||||
<!-- 展开内容: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">
|
||||
<span class="avg-now-label">AVG • NOW</span>
|
||||
<span class="avg-now-values">
|
||||
<template v-if="parseAvgNow(pos.avgNow)[1]">
|
||||
{{ 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-else size="14" color="success" class="avg-now-arrow">mdi-chevron-up</v-icon>
|
||||
<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] }}
|
||||
</template>
|
||||
<template v-else>{{ pos.avgNow }}</template>
|
||||
</span>
|
||||
</div>
|
||||
<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 icon variant="text" size="small" class="position-share-btn" @click="sharePosition(pos.id)">
|
||||
<v-btn
|
||||
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-btn>
|
||||
</div>
|
||||
@ -210,13 +254,20 @@
|
||||
<td class="cell-market">
|
||||
<div class="position-market-cell">
|
||||
<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>
|
||||
</div>
|
||||
<div class="position-market-info">
|
||||
<span class="position-market-title">{{ pos.market }}</span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -227,13 +278,33 @@
|
||||
<td>{{ pos.toWin }}</td>
|
||||
<td class="cell-value">
|
||||
<div class="position-value">{{ pos.value }}</div>
|
||||
<div v-if="pos.valueChange != null" :class="['position-value-change', pos.valueChangeLoss ? 'value-loss' : 'value-gain']">
|
||||
{{ pos.valueChange }}{{ pos.valueChangePct != null ? ` (${pos.valueChangePct})` : '' }}
|
||||
<div
|
||||
v-if="pos.valueChange != null"
|
||||
:class="[
|
||||
'position-value-change',
|
||||
pos.valueChangeLoss ? 'value-loss' : 'value-gain',
|
||||
]"
|
||||
>
|
||||
{{ pos.valueChange
|
||||
}}{{ pos.valueChangePct != null ? ` (${pos.valueChangePct})` : '' }}
|
||||
</div>
|
||||
</td>
|
||||
<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 icon variant="text" size="small" class="position-share-btn" @click="sharePosition(pos.id)">
|
||||
<v-btn
|
||||
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-btn>
|
||||
</td>
|
||||
@ -254,13 +325,24 @@
|
||||
</div>
|
||||
<div class="order-mobile-main">
|
||||
<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}` }}
|
||||
</div>
|
||||
<div class="order-mobile-price">{{ ord.price }} • {{ ord.total }}</div>
|
||||
</div>
|
||||
<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-btn>
|
||||
<div class="order-mobile-filled">{{ ord.filledDisplay || ord.filled }}</div>
|
||||
@ -297,7 +379,14 @@
|
||||
<td>{{ ord.total }}</td>
|
||||
<td>{{ ord.expiration }}</td>
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -326,7 +415,9 @@
|
||||
<div class="history-mobile-activity">{{ h.activityDetail || h.activity }}</div>
|
||||
</div>
|
||||
<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 }}
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="history-mobile-actions">
|
||||
<v-btn 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-btn
|
||||
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-btn>
|
||||
</div>
|
||||
@ -400,10 +503,7 @@
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<DepositDialog
|
||||
v-model="depositDialogOpen"
|
||||
:balance="portfolioBalance"
|
||||
/>
|
||||
<DepositDialog v-model="depositDialogOpen" :balance="portfolioBalance" />
|
||||
<WithdrawDialog
|
||||
v-model="withdrawDialogOpen"
|
||||
:balance="portfolioBalance"
|
||||
@ -411,10 +511,22 @@
|
||||
/>
|
||||
|
||||
<!-- 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">
|
||||
<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-btn>
|
||||
</div>
|
||||
@ -605,7 +717,8 @@ const positions = ref<Position[]>([
|
||||
outcomeWord: 'Yes',
|
||||
},
|
||||
])
|
||||
const MOCK_TOKEN_ID = '59966088656508531737144108943848781534186324373509174641856486864137458635937'
|
||||
const MOCK_TOKEN_ID =
|
||||
'59966088656508531737144108943848781534186324373509174641856486864137458635937'
|
||||
const openOrders = ref<OpenOrder[]>([
|
||||
{
|
||||
id: 'o1',
|
||||
@ -690,7 +803,13 @@ const history = ref<HistoryItem[]>([
|
||||
iconChar: '₿',
|
||||
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 {
|
||||
@ -713,9 +832,15 @@ const paginatedPositions = computed(() => paginate(filteredPositions.value))
|
||||
const paginatedOpenOrders = computed(() => paginate(filteredOpenOrders.value))
|
||||
const paginatedHistory = computed(() => paginate(filteredHistory.value))
|
||||
|
||||
const totalPagesPositions = computed(() => Math.max(1, Math.ceil(filteredPositions.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 totalPagesPositions = computed(() =>
|
||||
Math.max(1, Math.ceil(filteredPositions.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(() => {
|
||||
if (activeTab.value === 'positions') return filteredPositions.value.length
|
||||
@ -727,10 +852,16 @@ const currentTotalPages = computed(() => {
|
||||
if (activeTab.value === 'orders') return totalPagesOrders.value
|
||||
return totalPagesHistory.value
|
||||
})
|
||||
const currentPageStart = computed(() => currentListTotal.value === 0 ? 0 : (page.value - 1) * itemsPerPage.value + 1)
|
||||
const currentPageEnd = computed(() => Math.min(page.value * itemsPerPage.value, currentListTotal.value))
|
||||
const currentPageStart = computed(() =>
|
||||
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], () => {
|
||||
const maxPage = currentTotalPages.value
|
||||
if (page.value > maxPage) page.value = Math.max(1, maxPage)
|
||||
@ -877,7 +1008,10 @@ function buildPlChartOption(chartData: [number, number][]) {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
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 val = Array.isArray(p.value) ? (p.value as number[])[1] : p.value
|
||||
const sign = Number(val) >= 0 ? '+' : ''
|
||||
@ -908,7 +1042,12 @@ function buildPlChartOption(chartData: [number, number][]) {
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
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)
|
||||
const last = plChartData.value[plChartData.value.length - 1]
|
||||
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() {
|
||||
|
||||
5
src/vite-env.d.ts
vendored
5
src/vite-env.d.ts
vendored
@ -10,10 +10,7 @@ declare module '*.vue' {
|
||||
declare interface Window {
|
||||
ethereum?: {
|
||||
isMetaMask?: boolean
|
||||
request: (args: {
|
||||
method: string
|
||||
params?: any[]
|
||||
}) => Promise<any>
|
||||
request: (args: { method: string; params?: any[] }) => Promise<any>
|
||||
on: (event: string, callback: (...args: any[]) => void) => void
|
||||
removeListener: (event: string, callback: (...args: any[]) => void) => void
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user