优化:修复持仓显示bug
This commit is contained in:
parent
ff7bc3b685
commit
45a4b2c00f
@ -21,7 +21,7 @@ Event(预测市场事件)相关接口与类型定义,对接 XTrader API
|
|||||||
| `PmEventMarketItem` | 市场项,含 outcomes、outcomePrices、clobTokenIds |
|
| `PmEventMarketItem` | 市场项,含 outcomes、outcomePrices、clobTokenIds |
|
||||||
| `EventCardItem` | 首页卡片所需结构,含 displayType(single/multi) |
|
| `EventCardItem` | 首页卡片所需结构,含 displayType(single/multi) |
|
||||||
| `EventCardOutcome` | 多选项卡片中单个选项 |
|
| `EventCardOutcome` | 多选项卡片中单个选项 |
|
||||||
| `PageResult<T>` | 分页结果 |
|
| `PageResult<T>` | 分页结果(来自 `@/api/types`) |
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ clearEventListCache()
|
|||||||
|
|
||||||
1. **新增筛选参数**:在 `GetPmEventListParams` 中增加字段,并在 `getPmEventPublic` 的 query 中传入
|
1. **新增筛选参数**:在 `GetPmEventListParams` 中增加字段,并在 `getPmEventPublic` 的 query 中传入
|
||||||
2. **缓存策略**:可改为 sessionStorage 或带 TTL 的缓存
|
2. **缓存策略**:可改为 sessionStorage 或带 TTL 的缓存
|
||||||
3. **多选项展示**:`mapEventItemToCard` 已支持 multi 类型,可扩展 `EventCardOutcome` 字段
|
3. **多选项展示**:`mapEventItemToCard` 已支持 multi 类型,内部拆分为 `formatVolume`、`formatExpiresAt`、`parseOutcomePrices`、`mapMarketToOutcome` 等小函数,可扩展 `EventCardOutcome` 字段
|
||||||
|
|
||||||
## 参数传递方式
|
## 参数传递方式
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 功能用途
|
## 功能用途
|
||||||
|
|
||||||
交易与市场相关接口:下单、取消订单、Split(USDC 换 Yes+No)、Merge(Yes+No 换 USDC)。对接 CLOB Gateway 与 PmMarket 接口。
|
交易与市场相关接口:下单、取消订单、Split(USDC 换 Yes+No)、Merge(Yes+No 换 USDC)。对接 CLOB Gateway 与 PmMarket 接口。`ApiResponse` 来自 `@/api/types`。
|
||||||
|
|
||||||
## 核心能力
|
## 核心能力
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 功能用途
|
## 功能用途
|
||||||
|
|
||||||
订单相关 API:获取订单列表、取消订单,以及将 `ClobOrderItem` 映射为展示项(History、Open Orders)。
|
订单相关 API:获取订单列表、取消订单,以及将 `ClobOrderItem` 映射为展示项(History、Open Orders)。`PageResult`、`ApiResponse` 来自 `@/api/types`,通过 `buildQuery` 构建请求参数。
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
|
|||||||
18
docs/api/position.md
Normal file
18
docs/api/position.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# position.ts
|
||||||
|
|
||||||
|
**路径**:`src/api/position.ts`
|
||||||
|
|
||||||
|
## 功能用途
|
||||||
|
|
||||||
|
持仓相关 API:分页获取持仓列表,以及将 `ClobPositionItem` 映射为钱包展示项。`PageResult` 来自 `@/api/types`,使用 `buildQuery` 构建请求参数。
|
||||||
|
|
||||||
|
## 核心能力
|
||||||
|
|
||||||
|
- `getPositionList`:分页获取持仓列表(需鉴权)
|
||||||
|
- `mapPositionToDisplayItem`:将接口项转为展示结构(含 locked、availableSharesNum、outcome 等);`outcome` 保留 API 原始值(如 "Up"/"Down"、"Yes"/"No"),供 TradeDetail 与市场 outcomes 匹配
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getPositionList, mapPositionToDisplayItem } from '@/api/position'
|
||||||
|
```
|
||||||
@ -4,31 +4,32 @@
|
|||||||
|
|
||||||
## 功能用途
|
## 功能用途
|
||||||
|
|
||||||
HTTP 请求基础封装,提供 `get` 和 `post` 方法,支持自定义请求头(如鉴权 `x-token`、`x-user-id`)。所有 API 模块均通过此文件发起请求。
|
HTTP 请求基础封装,提供 `get`、`post`、`buildQuery` 方法,以及 WebSocket URL 生成。所有 API 模块均通过此文件发起请求。
|
||||||
|
|
||||||
## 核心能力
|
## 核心能力
|
||||||
|
|
||||||
- 统一 BASE_URL:默认 `https://api.xtrader.vip`,可通过环境变量 `VITE_API_BASE_URL` 覆盖
|
- 统一 BASE_URL:默认 `https://api.xtrader.vip`,可通过环境变量 `VITE_API_BASE_URL` 覆盖
|
||||||
- CLOB WebSocket URL:`getClobWsUrl()` 返回与 REST API 同源的 `ws(s)://host/clob/ws`
|
- **buildQuery**:过滤空值(undefined、''、[]、NaN)后构建 query 对象,供 GET 请求使用
|
||||||
|
- CLOB WebSocket URL:`getClobWsUrl()` 返回 `ws(s)://host/clob/ws`
|
||||||
- User WebSocket URL:`getUserWsUrl()` 返回 `ws(s)://host/clob/ws/user`(订单/持仓/余额推送)
|
- User WebSocket URL:`getUserWsUrl()` 返回 `ws(s)://host/clob/ws/user`(订单/持仓/余额推送)
|
||||||
- GET 请求:支持 query 参数,自动序列化
|
- GET 请求:支持 query 参数,自动序列化
|
||||||
- POST 请求:支持 JSON body
|
- POST 请求:支持 JSON body
|
||||||
- 自定义 headers:通过 `RequestConfig.headers` 传入
|
- 自定义 headers:通过 `RequestConfig.headers` 传入
|
||||||
- **Accept-Language**:所有 GET/POST 请求自动附带标准 `Accept-Language` 头,值为当前 vue-i18n 的 `locale`(如 `zh-CN`、`en`),可在 `config.headers` 中覆盖
|
- **Accept-Language**:所有 GET/POST 请求自动附带当前 vue-i18n 的 `locale`
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { get, post, getClobWsUrl, getUserWsUrl } from '@/api/request'
|
import { get, post, buildQuery, getClobWsUrl, getUserWsUrl } from '@/api/request'
|
||||||
|
|
||||||
// CLOB WebSocket URL(与 REST 同源)
|
// 构建 query(自动过滤空值)
|
||||||
|
const query = buildQuery({ page: 1, pageSize: 10, keyword, tagIds })
|
||||||
|
const data = await get<MyResponse>('/path', query)
|
||||||
|
|
||||||
|
// CLOB / User WebSocket URL
|
||||||
const clobWsUrl = getClobWsUrl()
|
const clobWsUrl = getClobWsUrl()
|
||||||
// User WebSocket URL(订单/持仓/余额推送,需带 token 参数)
|
|
||||||
const userWsUrl = getUserWsUrl()
|
const userWsUrl = getUserWsUrl()
|
||||||
|
|
||||||
// GET 请求
|
|
||||||
const data = await get<MyResponse>('/path', { page: 1, keyword: 'x' })
|
|
||||||
|
|
||||||
// 带鉴权
|
// 带鉴权
|
||||||
const data = await get<MyResponse>('/path', undefined, {
|
const data = await get<MyResponse>('/path', undefined, {
|
||||||
headers: { 'x-token': token, 'x-user-id': userId },
|
headers: { 'x-token': token, 'x-user-id': userId },
|
||||||
|
|||||||
53
docs/api/types.md
Normal file
53
docs/api/types.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# types.ts
|
||||||
|
|
||||||
|
**路径**:`src/api/types.ts`
|
||||||
|
|
||||||
|
## 功能用途
|
||||||
|
|
||||||
|
公共 API 类型定义,供 event、order、position、market 等模块复用,避免重复声明。
|
||||||
|
|
||||||
|
## 核心类型
|
||||||
|
|
||||||
|
### PageResult\<T\>
|
||||||
|
|
||||||
|
分页结果通用结构:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface PageResult<T> {
|
||||||
|
list: T[]
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ApiResponse\<T\>
|
||||||
|
|
||||||
|
通用 API 响应:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ApiResponse<T = unknown> {
|
||||||
|
code: number
|
||||||
|
data?: T
|
||||||
|
msg: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { PageResult, ApiResponse } from '@/api/types'
|
||||||
|
|
||||||
|
// 在接口模块中导出供外部使用
|
||||||
|
export type { PageResult }
|
||||||
|
export interface MyListResponse {
|
||||||
|
code: number
|
||||||
|
data: PageResult<MyItem>
|
||||||
|
msg: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 扩展方式
|
||||||
|
|
||||||
|
- 新增通用类型时在此文件添加
|
||||||
|
- 各 API 模块通过 `export type { Xxx }` 重新导出,保持向后兼容
|
||||||
38
docs/components/Footer.md
Normal file
38
docs/components/Footer.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Footer.vue
|
||||||
|
|
||||||
|
**路径**:`src/components/Footer.vue`
|
||||||
|
|
||||||
|
## 功能用途
|
||||||
|
|
||||||
|
首页底部 Footer 组件,展示 Polymarket 品牌、支持/社交链接、Polymarket 链接、法律声明、语言选择与社交图标。
|
||||||
|
|
||||||
|
## 核心能力
|
||||||
|
|
||||||
|
- 品牌 Logo 与 Slogan
|
||||||
|
- 两列链接:Support & Social、Polymarket
|
||||||
|
- 底部法律链接、语言选择(v-select)、社交图标
|
||||||
|
- 免责声明文案
|
||||||
|
- 响应式布局(移动端垂直排列)
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div class="home-page">
|
||||||
|
<!-- 主内容 -->
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Footer from '@/components/Footer.vue'
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
父容器需设置 `display: flex; flex-direction: column; min-height: 100vh`,Footer 通过 `margin-top: auto` 贴底。
|
||||||
|
|
||||||
|
## 扩展方式
|
||||||
|
|
||||||
|
1. **多语言**:将 `footerLang` 与 `useLocaleStore` 联动
|
||||||
|
2. **链接配置**:抽成 props 或常量数组,便于维护
|
||||||
|
3. **社交图标**:可改为 props 传入,支持外部配置
|
||||||
@ -14,7 +14,8 @@
|
|||||||
- `setUser`:设置登录数据并持久化,登录成功后自动连接 UserSocket
|
- `setUser`:设置登录数据并持久化,登录成功后自动连接 UserSocket
|
||||||
- `logout`:清空并断开 UserSocket
|
- `logout`:清空并断开 UserSocket
|
||||||
- `getAuthHeaders`:返回 `{ 'x-token', 'x-user-id' }`,未登录时返回 `undefined`
|
- `getAuthHeaders`:返回 `{ 'x-token', 'x-user-id' }`,未登录时返回 `undefined`
|
||||||
- `fetchUserInfo`、`fetchUsdcBalance`:拉取并更新用户信息与余额;`fetchUserInfo` 兼容多种 API 字段名
|
- `fetchUserInfo`、`fetchUsdcBalance`:拉取并更新用户信息与余额;`fetchUserInfo` 兼容多种 API 字段名(id/ID、userName/username 等)
|
||||||
|
- 内部 `parseUserId`:从 API 返回的 user 对象解析 id/ID,兼容 number 与 string,供 `setUser` 与 `fetchUserInfo` 复用
|
||||||
- `connectUserSocket`、`disconnectUserSocket`:连接/断开 `sdk/userSocket` 的 UserSdk,用于订单/持仓/余额实时推送
|
- `connectUserSocket`、`disconnectUserSocket`:连接/断开 `sdk/userSocket` 的 UserSdk,用于订单/持仓/余额实时推送
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 功能用途
|
## 功能用途
|
||||||
|
|
||||||
首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多。
|
首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多。底部 Footer 已抽成独立组件 `Footer.vue`。
|
||||||
|
|
||||||
## 核心能力
|
## 核心能力
|
||||||
|
|
||||||
@ -12,6 +12,7 @@
|
|||||||
- **事件列表**:卡片式展示,支持下拉刷新、触底加载
|
- **事件列表**:卡片式展示,支持下拉刷新、触底加载
|
||||||
- **搜索**:可按关键词搜索事件
|
- **搜索**:可按关键词搜索事件
|
||||||
- **分类筛选**:选中分类后,自动提取所有层级节点的 `tagIds` 进行事件筛选
|
- **分类筛选**:选中分类后,自动提取所有层级节点的 `tagIds` 进行事件筛选
|
||||||
|
- **Footer**:使用 `<Footer />` 组件,包含品牌、链接、语言选择、免责声明
|
||||||
|
|
||||||
## 数据流
|
## 数据流
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
- **合并成功**:`TradeComponent` 触发 `mergeSuccess` → `onMergeSuccess()` 刷新持仓,显示 toast 提示
|
- **合并成功**:`TradeComponent` 触发 `mergeSuccess` → `onMergeSuccess()` 刷新持仓,显示 toast 提示
|
||||||
- **拆分成功**:`TradeComponent` 触发 `splitSuccess` → `onSplitSuccess()` 刷新持仓
|
- **拆分成功**:`TradeComponent` 触发 `splitSuccess` → `onSplitSuccess()` 刷新持仓
|
||||||
|
|
||||||
持仓刷新调用 `loadMarketPositions()`,通过 `/clob/position/getPositionList` 接口获取最新持仓数据,并根据 `tokenId` 匹配 Yes/No 方向。
|
持仓刷新调用 `loadMarketPositions()`,通过 `/clob/position/getPositionList` 接口获取最新持仓数据。**持仓类型用 API 返回的 `outcome` 字段匹配**(如 "Up"/"Down"、"Yes"/"No"),与当前市场的 `outcomes[0]`、`outcomes[1]` 对应,用于 Sell 选项与 TradeComponent 的 positions 映射。
|
||||||
|
|
||||||
## 扩展方式
|
## 扩展方式
|
||||||
|
|
||||||
|
|||||||
114
src/api/event.ts
114
src/api/event.ts
@ -1,12 +1,7 @@
|
|||||||
import { get } from './request'
|
import { buildQuery, get } from './request'
|
||||||
|
import type { PageResult } from './types'
|
||||||
|
|
||||||
/** 分页结果 */
|
export type { PageResult }
|
||||||
export interface PageResult<T> {
|
|
||||||
list: T[]
|
|
||||||
page: number
|
|
||||||
pageSize: number
|
|
||||||
total: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event 单项结构(与 doc.json definitions["polymarket.PmEvent"] 对齐)
|
* Event 单项结构(与 doc.json definitions["polymarket.PmEvent"] 对齐)
|
||||||
@ -137,15 +132,7 @@ export async function getPmEventPublic(
|
|||||||
params: GetPmEventListParams = {},
|
params: GetPmEventListParams = {},
|
||||||
): Promise<PmEventListResponse> {
|
): Promise<PmEventListResponse> {
|
||||||
const { page = 1, pageSize = 10, keyword, createdAtRange, tagIds } = params
|
const { page = 1, pageSize = 10, keyword, createdAtRange, tagIds } = params
|
||||||
const query: Record<string, string | number | number[] | string[] | undefined> = {
|
const query = buildQuery({ page, pageSize, keyword, createdAtRange, tagIds })
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
}
|
|
||||||
if (keyword != null && keyword !== '') query.keyword = keyword
|
|
||||||
if (createdAtRange != null && createdAtRange.length) query.createdAtRange = createdAtRange
|
|
||||||
if (tagIds != null && tagIds.length > 0) {
|
|
||||||
query.tagIds = tagIds
|
|
||||||
}
|
|
||||||
return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
|
return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,9 +173,7 @@ 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 = buildQuery({ ID: params.id, slug: params.slug })
|
||||||
if (params.id != null) query.ID = params.id
|
|
||||||
if (params.slug != null && params.slug !== '') query.slug = params.slug
|
|
||||||
if (Object.keys(query).length === 0) {
|
if (Object.keys(query).length === 0) {
|
||||||
throw new Error('findPmEvent: 至少需要传 id 或 slug')
|
throw new Error('findPmEvent: 至少需要传 id 或 slug')
|
||||||
}
|
}
|
||||||
@ -276,54 +261,24 @@ function marketChance(market: PmEventMarketItem): number {
|
|||||||
return Math.min(100, Math.max(0, Math.round(yesPrice * 100)))
|
return Math.min(100, Math.max(0, Math.round(yesPrice * 100)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function formatVolume(volume: number | undefined): string {
|
||||||
* 将 list 单项映射为首页 MarketCard 所需字段
|
if (volume == null || !Number.isFinite(volume)) return '$0 Vol.'
|
||||||
* - 单一 market 或无 market:displayType single,当前逻辑
|
return volume >= 1000 ? `$${(volume / 1000).toFixed(1)}k Vol.` : `$${Math.round(volume)} Vol.`
|
||||||
* - 多个 markets:displayType multi,outcomes 为每项标题+概率,卡片内左右滑动切换
|
}
|
||||||
*/
|
|
||||||
export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
|
||||||
const id = String(item.ID ?? '')
|
|
||||||
const marketTitle = item.title ?? ''
|
|
||||||
const imageUrl = item.image ?? item.icon ?? ''
|
|
||||||
|
|
||||||
const markets = item.markets ?? []
|
function formatExpiresAt(endDate: string | undefined): string {
|
||||||
const multi = markets.length > 1
|
if (!endDate) return ''
|
||||||
|
|
||||||
let chanceValue = 17
|
|
||||||
const firstMarket = markets[0]
|
|
||||||
if (firstMarket?.outcomePrices?.[0] != null) {
|
|
||||||
chanceValue = marketChance(firstMarket)
|
|
||||||
}
|
|
||||||
|
|
||||||
let marketInfo = '$0 Vol.'
|
|
||||||
if (item.volume != null && Number.isFinite(item.volume)) {
|
|
||||||
const v = item.volume
|
|
||||||
if (v >= 1000) {
|
|
||||||
marketInfo = `$${(v / 1000).toFixed(1)}k Vol.`
|
|
||||||
} else {
|
|
||||||
marketInfo = `$${Math.round(v)} Vol.`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let expiresAt = ''
|
|
||||||
if (item.endDate) {
|
|
||||||
try {
|
try {
|
||||||
const d = new Date(item.endDate)
|
const d = new Date(endDate)
|
||||||
if (!Number.isNaN(d.getTime())) {
|
return !Number.isNaN(d.getTime())
|
||||||
expiresAt = d.toLocaleDateString('en-US', {
|
? d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||||
month: 'short',
|
: endDate
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
expiresAt = item.endDate
|
return endDate
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const category = item.series?.[0]?.title ?? item.tags?.[0]?.label ?? ''
|
function parseOutcomePrices(m: PmEventMarketItem): { yesPrice: number; noPrice: number } {
|
||||||
|
|
||||||
function parseOutcomePrices(m: PmEventMarketItem): { yesPrice: number; noPrice: number } {
|
|
||||||
const y = m?.outcomePrices?.[0]
|
const y = m?.outcomePrices?.[0]
|
||||||
const n = m?.outcomePrices?.[1]
|
const n = m?.outcomePrices?.[1]
|
||||||
const yesPrice =
|
const yesPrice =
|
||||||
@ -335,10 +290,9 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
|||||||
? Math.min(1, Math.max(0, parseFloat(String(n))))
|
? Math.min(1, Math.max(0, parseFloat(String(n))))
|
||||||
: 1 - yesPrice
|
: 1 - yesPrice
|
||||||
return { yesPrice, noPrice }
|
return { yesPrice, noPrice }
|
||||||
}
|
}
|
||||||
|
|
||||||
const outcomes: EventCardOutcome[] | undefined = multi
|
function mapMarketToOutcome(m: PmEventMarketItem): EventCardOutcome {
|
||||||
? markets.map((m) => {
|
|
||||||
const { yesPrice, noPrice } = parseOutcomePrices(m)
|
const { yesPrice, noPrice } = parseOutcomePrices(m)
|
||||||
return {
|
return {
|
||||||
title: m.question ?? '',
|
title: m.question ?? '',
|
||||||
@ -350,22 +304,30 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
|||||||
marketId: getMarketId(m),
|
marketId: getMarketId(m),
|
||||||
clobTokenIds: m.clobTokenIds,
|
clobTokenIds: m.clobTokenIds,
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
: undefined
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 list 单项映射为首页 MarketCard 所需字段
|
||||||
|
* - 单一 market 或无 market:displayType single
|
||||||
|
* - 多个 markets:displayType multi,outcomes 为每项标题+概率,卡片内左右滑动切换
|
||||||
|
*/
|
||||||
|
export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
||||||
|
const markets = item.markets ?? []
|
||||||
|
const multi = markets.length > 1
|
||||||
|
const firstMarket = markets[0]
|
||||||
const firstPrices = firstMarket ? parseOutcomePrices(firstMarket) : { yesPrice: 0.5, noPrice: 0.5 }
|
const firstPrices = firstMarket ? parseOutcomePrices(firstMarket) : { yesPrice: 0.5, noPrice: 0.5 }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id: String(item.ID ?? ''),
|
||||||
slug: item.slug ?? undefined,
|
slug: item.slug ?? undefined,
|
||||||
marketTitle,
|
marketTitle: item.title ?? '',
|
||||||
chanceValue,
|
chanceValue: firstMarket ? marketChance(firstMarket) : 17,
|
||||||
marketInfo,
|
marketInfo: formatVolume(item.volume),
|
||||||
imageUrl,
|
imageUrl: item.image ?? item.icon ?? '',
|
||||||
category,
|
category: item.series?.[0]?.title ?? item.tags?.[0]?.label ?? '',
|
||||||
expiresAt,
|
expiresAt: formatExpiresAt(item.endDate),
|
||||||
displayType: multi ? 'multi' : 'single',
|
displayType: multi ? 'multi' : 'single',
|
||||||
outcomes,
|
outcomes: multi ? markets.map(mapMarketToOutcome) : undefined,
|
||||||
yesLabel: firstMarket?.outcomes?.[0] ?? 'Yes',
|
yesLabel: firstMarket?.outcomes?.[0] ?? 'Yes',
|
||||||
noLabel: firstMarket?.outcomes?.[1] ?? 'No',
|
noLabel: firstMarket?.outcomes?.[1] ?? 'No',
|
||||||
isNew: item.new === true,
|
isNew: item.new === true,
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
import { post } from './request'
|
import { post } from './request'
|
||||||
|
import type { ApiResponse } from './types'
|
||||||
|
|
||||||
/** 通用响应:与 doc.json response.Response 一致 */
|
export type { ApiResponse }
|
||||||
export interface ApiResponse<T = unknown> {
|
|
||||||
code: number
|
|
||||||
data?: T
|
|
||||||
msg: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下单请求体(/clob/gateway/submitOrder)
|
* 下单请求体(/clob/gateway/submitOrder)
|
||||||
|
|||||||
@ -1,12 +1,7 @@
|
|||||||
import { get, post } from './request'
|
import { buildQuery, get, post } from './request'
|
||||||
|
import type { ApiResponse, PageResult } from './types'
|
||||||
|
|
||||||
/** 分页结果 */
|
export type { PageResult }
|
||||||
export interface PageResult<T> {
|
|
||||||
list: T[]
|
|
||||||
page: number
|
|
||||||
pageSize: number
|
|
||||||
total: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单项(与 doc.json definitions["model.ClobOrder"] 对齐)
|
* 订单项(与 doc.json definitions["model.ClobOrder"] 对齐)
|
||||||
@ -71,13 +66,16 @@ export async function getOrderList(
|
|||||||
): Promise<OrderListResponse> {
|
): Promise<OrderListResponse> {
|
||||||
const { page = 1, pageSize = 10, status, startCreatedAt, endCreatedAt, marketID, tokenID, userID } =
|
const { page = 1, pageSize = 10, status, startCreatedAt, endCreatedAt, marketID, tokenID, userID } =
|
||||||
params
|
params
|
||||||
const query: Record<string, string | number | undefined> = { page, pageSize }
|
const query = buildQuery({
|
||||||
if (status != null && Number.isFinite(status)) query.status = status
|
page,
|
||||||
if (startCreatedAt != null && startCreatedAt !== '') query.startCreatedAt = startCreatedAt
|
pageSize,
|
||||||
if (endCreatedAt != null && endCreatedAt !== '') query.endCreatedAt = endCreatedAt
|
status,
|
||||||
if (marketID != null && marketID !== '') query.marketID = marketID
|
startCreatedAt,
|
||||||
if (tokenID != null && tokenID !== '') query.tokenID = tokenID
|
endCreatedAt,
|
||||||
if (userID != null && Number.isFinite(userID)) query.userID = userID
|
marketID,
|
||||||
|
tokenID,
|
||||||
|
userID,
|
||||||
|
})
|
||||||
return get<OrderListResponse>('/clob/order/getOrderList', query, config)
|
return get<OrderListResponse>('/clob/order/getOrderList', query, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,12 +86,7 @@ export interface CancelOrderReq {
|
|||||||
userID: number
|
userID: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通用 API 响应 */
|
export type { ApiResponse }
|
||||||
export interface ApiResponse {
|
|
||||||
code: number
|
|
||||||
data?: unknown
|
|
||||||
msg: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /clob/order/cancelOrder
|
* POST /clob/order/cancelOrder
|
||||||
|
|||||||
@ -1,12 +1,7 @@
|
|||||||
import { get } from './request'
|
import { buildQuery, get } from './request'
|
||||||
|
import type { PageResult } from './types'
|
||||||
|
|
||||||
/** 分页结果 */
|
export type { PageResult }
|
||||||
export interface PageResult<T> {
|
|
||||||
list: T[]
|
|
||||||
page: number
|
|
||||||
pageSize: number
|
|
||||||
total: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 持仓项(与 /clob/position/getPositionList 实际返回对齐)
|
* 持仓项(与 /clob/position/getPositionList 实际返回对齐)
|
||||||
@ -72,12 +67,15 @@ export async function getPositionList(
|
|||||||
config?: { headers?: Record<string, string> },
|
config?: { headers?: Record<string, string> },
|
||||||
): Promise<PositionListResponse> {
|
): Promise<PositionListResponse> {
|
||||||
const { page = 1, pageSize = 10, startCreatedAt, endCreatedAt, marketID, tokenID, userID } = params
|
const { page = 1, pageSize = 10, startCreatedAt, endCreatedAt, marketID, tokenID, userID } = params
|
||||||
const query: Record<string, string | number | undefined> = { page, pageSize }
|
const query = buildQuery({
|
||||||
if (startCreatedAt != null && startCreatedAt !== '') query.startCreatedAt = startCreatedAt
|
page,
|
||||||
if (endCreatedAt != null && endCreatedAt !== '') query.endCreatedAt = endCreatedAt
|
pageSize,
|
||||||
if (marketID != null && marketID !== '') query.marketID = marketID
|
startCreatedAt,
|
||||||
if (tokenID != null && tokenID !== '') query.tokenID = tokenID
|
endCreatedAt,
|
||||||
if (userID != null && Number.isFinite(userID)) query.userID = userID
|
marketID,
|
||||||
|
tokenID,
|
||||||
|
userID,
|
||||||
|
})
|
||||||
return get<PositionListResponse>('/clob/position/getPositionList', query, config)
|
return get<PositionListResponse>('/clob/position/getPositionList', query, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,6 +103,8 @@ export interface PositionDisplayItem {
|
|||||||
lockedSharesNum?: number
|
lockedSharesNum?: number
|
||||||
/** 可卖份额数值(来自 available),available > 0 表示有订单可卖,此为最大可卖份额 */
|
/** 可卖份额数值(来自 available),available > 0 表示有订单可卖,此为最大可卖份额 */
|
||||||
availableSharesNum?: number
|
availableSharesNum?: number
|
||||||
|
/** 原始 outcome 字段(API 返回,如 "Up"/"Down" 或 "Yes"/"No"),用于与市场 outcomes 匹配 */
|
||||||
|
outcome?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,12 +122,13 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
|
|||||||
const availableNum = availableRaw / SCALE
|
const availableNum = availableRaw / SCALE
|
||||||
const costUsd = costRaw / SCALE
|
const costUsd = costRaw / SCALE
|
||||||
const shares = `${size} shares`
|
const shares = `${size} shares`
|
||||||
const outcomeWord = pos.outcome === 'No' ? 'No' : 'Yes'
|
const outcome = pos.outcome ?? 'Yes'
|
||||||
const pillClass = outcomeWord === 'No' ? 'pill-down' : 'pill-yes'
|
const pillClass =
|
||||||
|
outcome === 'No' || outcome === 'Down' || outcome === 'Below' ? 'pill-down' : 'pill-yes'
|
||||||
const value = `$${costUsd.toFixed(2)}`
|
const value = `$${costUsd.toFixed(2)}`
|
||||||
const bet = value
|
const bet = value
|
||||||
const toWin = `$${size.toFixed(2)}`
|
const toWin = `$${size.toFixed(2)}`
|
||||||
const outcomeTag = `${outcomeWord} —`
|
const outcomeTag = `${outcome} —`
|
||||||
const locked = lockRaw > 0
|
const locked = lockRaw > 0
|
||||||
const lockedSharesNum = lockRaw > 0 ? lockRaw / SCALE : undefined
|
const lockedSharesNum = lockRaw > 0 ? lockRaw / SCALE : undefined
|
||||||
return {
|
return {
|
||||||
@ -138,10 +139,11 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
|
|||||||
bet,
|
bet,
|
||||||
toWin,
|
toWin,
|
||||||
value,
|
value,
|
||||||
sellOutcome: outcomeWord,
|
sellOutcome: outcome,
|
||||||
outcomeWord,
|
outcomeWord: outcome,
|
||||||
outcomeTag,
|
outcomeTag,
|
||||||
outcomePillClass: pillClass,
|
outcomePillClass: pillClass,
|
||||||
|
outcome,
|
||||||
locked,
|
locked,
|
||||||
lockedSharesNum,
|
lockedSharesNum,
|
||||||
availableSharesNum: availableNum >= 0 ? availableNum : undefined,
|
availableSharesNum: availableNum >= 0 ? availableNum : undefined,
|
||||||
|
|||||||
@ -1,28 +1,50 @@
|
|||||||
import { i18n } from '@/plugins/i18n'
|
import { i18n } from '@/plugins/i18n'
|
||||||
|
|
||||||
/**
|
/** 请求基础 URL,默认 https://api.xtrader.vip,可通过环境变量 VITE_API_BASE_URL 覆盖 */
|
||||||
* 请求基础 URL,默认 https://api.xtrader.vip,可通过环境变量 VITE_API_BASE_URL 覆盖
|
|
||||||
*/
|
|
||||||
const BASE_URL =
|
const BASE_URL =
|
||||||
typeof import.meta !== 'undefined' &&
|
(import.meta as { env?: { VITE_API_BASE_URL?: string } }).env?.VITE_API_BASE_URL ??
|
||||||
(import.meta as unknown as { env?: Record<string, string> }).env?.VITE_API_BASE_URL
|
'https://api.xtrader.vip'
|
||||||
? (import.meta as unknown as { env: Record<string, string> }).env.VITE_API_BASE_URL
|
|
||||||
: 'https://api.xtrader.vip'
|
|
||||||
|
|
||||||
/** CLOB WebSocket URL,与 REST API 同源 */
|
const FALLBACK_BASE =
|
||||||
export function getClobWsUrl(): string {
|
typeof window !== 'undefined' ? window.location.origin : 'https://api.xtrader.vip'
|
||||||
const base = BASE_URL || (typeof window !== 'undefined' ? window.location.origin : 'https://api.xtrader.vip')
|
|
||||||
|
/** 生成 WebSocket URL,与 REST API 同源 */
|
||||||
|
function getWsUrl(path: string): string {
|
||||||
|
const base = BASE_URL || FALLBACK_BASE
|
||||||
const url = new URL(base)
|
const url = new URL(base)
|
||||||
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
return `${protocol}//${url.host}/clob/ws`
|
return `${protocol}//${url.host}${path.startsWith('/') ? path : `/${path}`}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/** User WebSocket URL(订单/持仓/余额推送),与 REST API 同源 */
|
/** CLOB WebSocket URL */
|
||||||
|
export function getClobWsUrl(): string {
|
||||||
|
return getWsUrl('/clob/ws')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** User WebSocket URL(订单/持仓/余额推送) */
|
||||||
export function getUserWsUrl(): string {
|
export function getUserWsUrl(): string {
|
||||||
const base = BASE_URL || (typeof window !== 'undefined' ? window.location.origin : 'https://api.xtrader.vip')
|
return getWsUrl('/clob/ws/user')
|
||||||
const url = new URL(base)
|
}
|
||||||
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
||||||
return `${protocol}//${url.host}/clob/ws/user`
|
/**
|
||||||
|
* 过滤空值后构建 query 对象,供 GET 请求使用
|
||||||
|
* - undefined/null 跳过
|
||||||
|
* - 空字符串 '' 跳过
|
||||||
|
* - 空数组 [] 跳过
|
||||||
|
* - NaN 跳过
|
||||||
|
*/
|
||||||
|
export function buildQuery(
|
||||||
|
params: Record<string, string | number | string[] | number[] | undefined | null>,
|
||||||
|
): Record<string, string | number | string[] | number[]> {
|
||||||
|
const result: Record<string, string | number | string[] | number[]> = {}
|
||||||
|
for (const [k, v] of Object.entries(params)) {
|
||||||
|
if (v === undefined || v === null) continue
|
||||||
|
if (typeof v === 'string' && v === '') continue
|
||||||
|
if (Array.isArray(v) && v.length === 0) continue
|
||||||
|
if (typeof v === 'number' && !Number.isFinite(v)) continue
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestConfig {
|
export interface RequestConfig {
|
||||||
|
|||||||
19
src/api/types.ts
Normal file
19
src/api/types.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* 公共 API 类型定义
|
||||||
|
* 供 event、order、position 等模块复用
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 分页结果 */
|
||||||
|
export interface PageResult<T> {
|
||||||
|
list: T[]
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通用 API 响应 */
|
||||||
|
export interface ApiResponse<T = unknown> {
|
||||||
|
code: number
|
||||||
|
data?: T
|
||||||
|
msg: string
|
||||||
|
}
|
||||||
274
src/components/Footer.vue
Normal file
274
src/components/Footer.vue
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
<template>
|
||||||
|
<footer class="home-footer">
|
||||||
|
<div class="footer-inner">
|
||||||
|
<div class="footer-top">
|
||||||
|
<div class="footer-brand">
|
||||||
|
<div class="footer-logo">
|
||||||
|
<span class="logo-mark">M</span>
|
||||||
|
<span class="logo-text">Polymarket</span>
|
||||||
|
</div>
|
||||||
|
<p class="footer-slogan">The World's Largest Prediction Market™</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-links-row">
|
||||||
|
<div class="footer-col">
|
||||||
|
<h4 class="footer-col-title">Support & Social</h4>
|
||||||
|
<ul class="footer-link-list">
|
||||||
|
<li><a href="#">Contact us</a></li>
|
||||||
|
<li><a href="#">Learn</a></li>
|
||||||
|
<li><a href="#">X (Twitter)</a></li>
|
||||||
|
<li><a href="#">Instagram</a></li>
|
||||||
|
<li><a href="#">Discord</a></li>
|
||||||
|
<li><a href="#">TikTok</a></li>
|
||||||
|
<li><a href="#">News</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="footer-col">
|
||||||
|
<h4 class="footer-col-title">Polymarket</h4>
|
||||||
|
<ul class="footer-link-list">
|
||||||
|
<li><a href="#">Accuracy</a></li>
|
||||||
|
<li><a href="#">Activity</a></li>
|
||||||
|
<li><a href="#">Leaderboard</a></li>
|
||||||
|
<li><a href="#">Rewards</a></li>
|
||||||
|
<li><a href="#">Press</a></li>
|
||||||
|
<li><a href="#">Careers</a></li>
|
||||||
|
<li><a href="#">APIs</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<div class="footer-legal-links">
|
||||||
|
<a href="#">Adventure One QSS Inc. © 2026</a>
|
||||||
|
<span class="sep">/</span>
|
||||||
|
<a href="#">Privacy</a>
|
||||||
|
<span class="sep">/</span>
|
||||||
|
<a href="#">Terms of Use</a>
|
||||||
|
<span class="sep">/</span>
|
||||||
|
<a href="#">Help Center</a>
|
||||||
|
<span class="sep">/</span>
|
||||||
|
<a href="#">Docs</a>
|
||||||
|
</div>
|
||||||
|
<div class="footer-lang-social">
|
||||||
|
<v-select
|
||||||
|
v-model="footerLang"
|
||||||
|
:items="['English']"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
variant="outlined"
|
||||||
|
class="footer-lang-select"
|
||||||
|
/>
|
||||||
|
<div class="footer-social-icons">
|
||||||
|
<v-icon size="20">mdi-email-outline</v-icon>
|
||||||
|
<v-icon size="20">mdi-twitter</v-icon>
|
||||||
|
<v-icon size="20">mdi-instagram</v-icon>
|
||||||
|
<v-icon size="20">mdi-discord</v-icon>
|
||||||
|
<v-icon size="20">mdi-music</v-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="footer-disclaimer">
|
||||||
|
Polymarket operates globally through separate legal entities. Polymarket US is operated by
|
||||||
|
QCX LLC d/b/a Polymarket US, a CFTC-regulated Designated Contract Market. This
|
||||||
|
international platform is not regulated by the CFTC and operates independently. Trading
|
||||||
|
involves substantial risk of loss. See our
|
||||||
|
<a href="#">Terms of Service & Privacy Policy</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: 'Footer' })
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const footerLang = ref('English')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-footer {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #374151;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
margin-top: auto;
|
||||||
|
padding: 48px 24px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-inner {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-top {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 48px;
|
||||||
|
padding-bottom: 32px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-brand {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-mark {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-slogan {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 64px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-col-title {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link-list li {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link-list a {
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link-list a:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding-top: 24px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-legal-links {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-legal-links a {
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-legal-links a:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-legal-links .sep {
|
||||||
|
margin: 0 8px;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-lang-social {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-lang-select {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-lang-select :deep(.v-field) {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-social-icons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-social-icons .v-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-social-icons .v-icon:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-disclaimer {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
margin: 0;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-disclaimer a {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-disclaimer a:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.home-footer {
|
||||||
|
padding: 32px 16px 24px;
|
||||||
|
}
|
||||||
|
.footer-top {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
|
.footer-links-row {
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
.footer-bottom {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -42,6 +42,21 @@ function clearStorage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 从 API 返回的 user 对象解析 id/ID,兼容 number 与 string */
|
||||||
|
function parseUserId(raw: { id?: number | string; ID?: number } | null | undefined): {
|
||||||
|
id: number | string | undefined
|
||||||
|
numId: number | undefined
|
||||||
|
} {
|
||||||
|
const rawId = raw?.ID ?? raw?.id
|
||||||
|
const numId =
|
||||||
|
typeof rawId === 'number'
|
||||||
|
? rawId
|
||||||
|
: rawId != null
|
||||||
|
? parseInt(String(rawId), 10)
|
||||||
|
: undefined
|
||||||
|
return { id: rawId ?? numId, numId: Number.isFinite(numId) ? numId : undefined }
|
||||||
|
}
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', () => {
|
export const useUserStore = defineStore('user', () => {
|
||||||
const stored = loadStored()
|
const stored = loadStored()
|
||||||
const token = ref<string>(stored?.token ?? '')
|
const token = ref<string>(stored?.token ?? '')
|
||||||
@ -110,18 +125,8 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
const raw = loginData.user ?? null
|
const raw = loginData.user ?? null
|
||||||
token.value = t
|
token.value = t
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const rawId = raw.ID ?? raw.id
|
const { id, numId } = parseUserId(raw)
|
||||||
const numId =
|
user.value = { ...raw, id, ID: numId ?? raw.ID }
|
||||||
typeof rawId === 'number'
|
|
||||||
? rawId
|
|
||||||
: rawId != null
|
|
||||||
? parseInt(String(rawId), 10)
|
|
||||||
: undefined
|
|
||||||
user.value = {
|
|
||||||
...raw,
|
|
||||||
id: rawId ?? numId,
|
|
||||||
ID: Number.isFinite(numId) ? numId : raw.ID,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
user.value = null
|
user.value = null
|
||||||
}
|
}
|
||||||
@ -181,20 +186,14 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
// 更新用户信息:data.userInfo
|
// 更新用户信息:data.userInfo
|
||||||
const u = (data?.userInfo ?? data?.user ?? data) as Record<string, unknown> | undefined
|
const u = (data?.userInfo ?? data?.user ?? data) as Record<string, unknown> | undefined
|
||||||
if (!u) return
|
if (!u) return
|
||||||
const rawId = u.ID ?? u.id
|
const { id, numId } = parseUserId(u as { id?: number | string; ID?: number })
|
||||||
const numId =
|
|
||||||
typeof rawId === 'number'
|
|
||||||
? rawId
|
|
||||||
: rawId != null
|
|
||||||
? parseInt(String(rawId), 10)
|
|
||||||
: undefined
|
|
||||||
user.value = {
|
user.value = {
|
||||||
...u,
|
...u,
|
||||||
userName: (u.userName ?? u.username) as string | undefined,
|
userName: (u.userName ?? u.username) as string | undefined,
|
||||||
nickName: (u.nickName ?? u.nickname) as string | undefined,
|
nickName: (u.nickName ?? u.nickname) as string | undefined,
|
||||||
headerImg: (u.headerImg ?? u.avatar ?? u.avatarUrl) as string | undefined,
|
headerImg: (u.headerImg ?? u.avatar ?? u.avatarUrl) as string | undefined,
|
||||||
id: (rawId ?? numId) as number | string | undefined,
|
id,
|
||||||
ID: Number.isFinite(numId) ? numId : undefined,
|
ID: numId,
|
||||||
} as UserInfo
|
} as UserInfo
|
||||||
if (token.value && user.value) saveToStorage(token.value, user.value)
|
if (token.value && user.value) saveToStorage(token.value, user.value)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -223,82 +223,7 @@
|
|||||||
</v-bottom-sheet>
|
</v-bottom-sheet>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
<footer class="home-footer">
|
<Footer />
|
||||||
<div class="footer-inner">
|
|
||||||
<div class="footer-top">
|
|
||||||
<div class="footer-brand">
|
|
||||||
<div class="footer-logo">
|
|
||||||
<span class="logo-mark">M</span>
|
|
||||||
<span class="logo-text">Polymarket</span>
|
|
||||||
</div>
|
|
||||||
<p class="footer-slogan">The World's Largest Prediction Market™</p>
|
|
||||||
</div>
|
|
||||||
<div class="footer-links-row">
|
|
||||||
<div class="footer-col">
|
|
||||||
<h4 class="footer-col-title">Support & Social</h4>
|
|
||||||
<ul class="footer-link-list">
|
|
||||||
<li><a href="#">Contact us</a></li>
|
|
||||||
<li><a href="#">Learn</a></li>
|
|
||||||
<li><a href="#">X (Twitter)</a></li>
|
|
||||||
<li><a href="#">Instagram</a></li>
|
|
||||||
<li><a href="#">Discord</a></li>
|
|
||||||
<li><a href="#">TikTok</a></li>
|
|
||||||
<li><a href="#">News</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="footer-col">
|
|
||||||
<h4 class="footer-col-title">Polymarket</h4>
|
|
||||||
<ul class="footer-link-list">
|
|
||||||
<li><a href="#">Accuracy</a></li>
|
|
||||||
<li><a href="#">Activity</a></li>
|
|
||||||
<li><a href="#">Leaderboard</a></li>
|
|
||||||
<li><a href="#">Rewards</a></li>
|
|
||||||
<li><a href="#">Press</a></li>
|
|
||||||
<li><a href="#">Careers</a></li>
|
|
||||||
<li><a href="#">APIs</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="footer-bottom">
|
|
||||||
<div class="footer-legal-links">
|
|
||||||
<a href="#">Adventure One QSS Inc. © 2026</a>
|
|
||||||
<span class="sep">/</span>
|
|
||||||
<a href="#">Privacy</a>
|
|
||||||
<span class="sep">/</span>
|
|
||||||
<a href="#">Terms of Use</a>
|
|
||||||
<span class="sep">/</span>
|
|
||||||
<a href="#">Help Center</a>
|
|
||||||
<span class="sep">/</span>
|
|
||||||
<a href="#">Docs</a>
|
|
||||||
</div>
|
|
||||||
<div class="footer-lang-social">
|
|
||||||
<v-select
|
|
||||||
v-model="footerLang"
|
|
||||||
:items="['English']"
|
|
||||||
density="compact"
|
|
||||||
hide-details
|
|
||||||
variant="outlined"
|
|
||||||
class="footer-lang-select"
|
|
||||||
/>
|
|
||||||
<div class="footer-social-icons">
|
|
||||||
<v-icon size="20">mdi-email-outline</v-icon>
|
|
||||||
<v-icon size="20">mdi-twitter</v-icon>
|
|
||||||
<v-icon size="20">mdi-instagram</v-icon>
|
|
||||||
<v-icon size="20">mdi-discord</v-icon>
|
|
||||||
<v-icon size="20">mdi-music</v-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="footer-disclaimer">
|
|
||||||
Polymarket operates globally through separate legal entities. Polymarket US is operated by
|
|
||||||
QCX LLC d/b/a Polymarket US, a CFTC-regulated Designated Contract Market. This
|
|
||||||
international platform is not regulated by the CFTC and operates independently. Trading
|
|
||||||
involves substantial risk of loss. See our
|
|
||||||
<a href="#">Terms of Service & Privacy Policy</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -306,6 +231,7 @@
|
|||||||
defineOptions({ name: 'Home' })
|
defineOptions({ name: 'Home' })
|
||||||
import { ref, onMounted, onUnmounted, onActivated, onDeactivated, nextTick, computed, watch } from 'vue'
|
import { ref, onMounted, onUnmounted, onActivated, onDeactivated, nextTick, computed, watch } from 'vue'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
import Footer from '../components/Footer.vue'
|
||||||
import MarketCard from '../components/MarketCard.vue'
|
import MarketCard from '../components/MarketCard.vue'
|
||||||
import TradeComponent from '../components/TradeComponent.vue'
|
import TradeComponent from '../components/TradeComponent.vue'
|
||||||
import {
|
import {
|
||||||
@ -519,7 +445,6 @@ const noMoreEvents = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
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<{
|
const tradeDialogMarket = ref<{
|
||||||
@ -1186,197 +1111,10 @@ onActivated(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Footer */
|
/* 页面布局:flex 列,Footer 通过 margin-top: auto 贴底 */
|
||||||
.home-page {
|
.home-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-footer {
|
|
||||||
width: 100%;
|
|
||||||
background-color: #374151;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 48px 24px 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-inner {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-top {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 48px;
|
|
||||||
padding-bottom: 32px;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-brand {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-logo {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-mark {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 18px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-text {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-slogan {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 64px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-col-title {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-link-list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-link-list li {
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-link-list a {
|
|
||||||
color: rgba(255, 255, 255, 0.75);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-link-list a:hover {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-bottom {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
padding-top: 24px;
|
|
||||||
padding-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-legal-links {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-legal-links a {
|
|
||||||
color: rgba(255, 255, 255, 0.75);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-legal-links a:hover {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-legal-links .sep {
|
|
||||||
margin: 0 8px;
|
|
||||||
color: rgba(255, 255, 255, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-lang-social {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-lang-select {
|
|
||||||
max-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-lang-select :deep(.v-field) {
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-social-icons {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-social-icons .v-icon {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-social-icons .v-icon:hover {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-disclaimer {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
margin: 0;
|
|
||||||
max-width: 720px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-disclaimer a {
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-disclaimer a:hover {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.home-footer {
|
|
||||||
padding: 32px 16px 24px;
|
|
||||||
}
|
|
||||||
.footer-top {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 32px;
|
|
||||||
padding-bottom: 24px;
|
|
||||||
}
|
|
||||||
.footer-links-row {
|
|
||||||
gap: 32px;
|
|
||||||
}
|
|
||||||
.footer-bottom {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -818,13 +818,13 @@ function onSplitSuccess() {
|
|||||||
loadMarketPositions()
|
loadMarketPositions()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从持仓项点击 Sell:弹出交易组件并切到 Sell、对应 Yes/No。是否可卖由 available 判断,available > 0 才可卖。 */
|
/** 从持仓项点击 Sell:弹出交易组件并切到 Sell、对应 Yes/No。持仓类型用 outcome 字段与 noLabel 匹配。 */
|
||||||
function openSellFromPosition(pos: PositionDisplayItem) {
|
function openSellFromPosition(pos: PositionDisplayItem) {
|
||||||
if (pos.availableSharesNum == null || pos.availableSharesNum <= 0) {
|
if (pos.availableSharesNum == null || pos.availableSharesNum <= 0) {
|
||||||
toastStore.show(t('activity.noAvailableSharesToSell'), 'warning')
|
toastStore.show(t('activity.noAvailableSharesToSell'), 'warning')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const option = pos.outcomeWord === 'No' ? 'no' : 'yes'
|
const option = pos.outcome === noLabel.value ? 'no' : 'yes'
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
tradeInitialOptionFromBar.value = option
|
tradeInitialOptionFromBar.value = option
|
||||||
tradeInitialTabFromBar.value = 'sell'
|
tradeInitialTabFromBar.value = 'sell'
|
||||||
@ -914,11 +914,12 @@ const marketPositionsFiltered = computed(() =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
/** 转为 TradeComponent 所需的 TradePositionItem[];sharesNum 用 available(最大可卖份额),available > 0 表示有订单可卖 */
|
/** 转为 TradeComponent 所需的 TradePositionItem[];sharesNum 用 available(最大可卖份额),available > 0 表示有订单可卖;outcome 与 noLabel 匹配则为 No */
|
||||||
const tradePositionsForComponent = computed<TradePositionItem[]>(() =>
|
const tradePositionsForComponent = computed<TradePositionItem[]>(() => {
|
||||||
marketPositionsFiltered.value.map((p) => ({
|
const no = noLabel.value
|
||||||
|
return marketPositionsFiltered.value.map((p) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
outcomeWord: (p.outcomeWord === 'No' ? 'No' : 'Yes') as 'Yes' | 'No',
|
outcomeWord: (p.outcome === no ? 'No' : 'Yes') as 'Yes' | 'No',
|
||||||
shares: p.shares,
|
shares: p.shares,
|
||||||
sharesNum:
|
sharesNum:
|
||||||
p.availableSharesNum != null && Number.isFinite(p.availableSharesNum)
|
p.availableSharesNum != null && Number.isFinite(p.availableSharesNum)
|
||||||
@ -926,7 +927,7 @@ const tradePositionsForComponent = computed<TradePositionItem[]>(() =>
|
|||||||
: parseFloat(p.shares?.replace(/[^0-9.]/g, '')) || undefined,
|
: parseFloat(p.shares?.replace(/[^0-9.]/g, '')) || undefined,
|
||||||
locked: p.locked,
|
locked: p.locked,
|
||||||
}))
|
}))
|
||||||
)
|
})
|
||||||
|
|
||||||
async function loadMarketPositions() {
|
async function loadMarketPositions() {
|
||||||
const marketID = currentMarketId.value
|
const marketID = currentMarketId.value
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user