新增:对接顶部分类接口

This commit is contained in:
ivan 2026-02-14 11:58:44 +08:00
parent f83f0100e0
commit 0aa04471f1
24 changed files with 1058 additions and 345 deletions

View File

@ -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 类

View File

@ -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不配置时使用默认值

View File

@ -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>

View File

@ -1,5 +1,29 @@
import { get } from './request' import { get } from './request'
/**
* PmTag definitions polymarket.PmTag
* doc.json definitions["polymarket.PmTag"]
* childrendefinitions
*/
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 }
}

View File

@ -82,7 +82,7 @@ export function getMarketId(m: PmEventMarketItem | null | undefined): string | u
/** 从市场项取 clobTokenIdoutcomeIndex 0=Yes/第一选项1=No/第二选项 */ /** 从市场项取 clobTokenIdoutcomeIndex 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

View File

@ -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)
} }

View File

@ -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' }],

View File

@ -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> = {

View File

@ -26,7 +26,9 @@ export function formatUsdcBalance(raw: string): string {
* USDC x-token * USDC x-token
* amountavailable 1000000 USDC * amountavailable 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,
}) })

View File

@ -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>

View File

@ -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) {

View File

@ -28,7 +28,7 @@
<div class="order-list-header-total">TOTAL</div> <div class="order-list-header-total">TOTAL</div>
</div> </div>
<!-- Asks Orders --> <!-- Asks Orders -->
<div v-for="(ask, index) in asksWithCumulativeTotal" :key="index" class="order-item"> <div v-for="(ask, index) in asksWithCumulativeTotal" :key="index" class="order-item">
<div class="order-progress"> <div class="order-progress">
<HorizontalProgressBar <HorizontalProgressBar
@ -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;

View File

@ -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>
@ -122,7 +127,7 @@
</div> </div>
<div class="amount-value">${{ amount.toFixed(2) }}</div> <div class="amount-value">${{ amount.toFixed(2) }}</div>
</div> </div>
<!-- Amount Buttons --> <!-- Amount Buttons -->
<div class="amount-buttons"> <div class="amount-buttons">
<v-btn class="amount-btn" @click="adjustAmount(1)">+$1</v-btn> <v-btn class="amount-btn" @click="adjustAmount(1)">+$1</v-btn>
@ -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 为 01 */ /** 从外部传入的市场数据(如 EventMarkets 点击 Yes/No 传入yesPrice/noPrice 为 01 */
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()
@ -1668,4 +1944,4 @@ async function submitOrder() {
text-transform: none; text-transform: none;
font-weight: 600; font-weight: 600;
} }
</style> </style>

View File

@ -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>

View File

@ -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',
} },
} },
} },
} },
}) })

View File

@ -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

View File

@ -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,
}
}) })

View File

@ -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>

View File

@ -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 &amp; 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 &amp; 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 {

View File

@ -140,7 +140,7 @@ const connectWithWallet = async () => {
uri: origin, uri: origin,
version: '1', version: '1',
chainId: chainId, chainId: chainId,
nonce, nonce,
}) })
const message = siwe.prepareMessage() const message = siwe.prepareMessage()
// message http:// // message http://
@ -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)

View File

@ -3,7 +3,7 @@
<v-row justify="center" align="center" class="trade-header"> <v-row justify="center" align="center" class="trade-header">
<h1 class="trade-title">Trading</h1> <h1 class="trade-title">Trading</h1>
</v-row> </v-row>
<v-row justify="center" align="center" class="trade-content"> <v-row justify="center" align="center" class="trade-content">
<v-col cols="12" md="10" lg="8"> <v-col cols="12" md="10" lg="8">
<v-card class="trade-card"> <v-card class="trade-card">

View File

@ -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 {

View File

@ -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
View File

@ -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
} }