优化:修复持仓显示bug
This commit is contained in:
parent
ff7bc3b685
commit
45a4b2c00f
@ -21,7 +21,7 @@ Event(预测市场事件)相关接口与类型定义,对接 XTrader API
|
||||
| `PmEventMarketItem` | 市场项,含 outcomes、outcomePrices、clobTokenIds |
|
||||
| `EventCardItem` | 首页卡片所需结构,含 displayType(single/multi) |
|
||||
| `EventCardOutcome` | 多选项卡片中单个选项 |
|
||||
| `PageResult<T>` | 分页结果 |
|
||||
| `PageResult<T>` | 分页结果(来自 `@/api/types`) |
|
||||
|
||||
## 使用方式
|
||||
|
||||
@ -48,7 +48,7 @@ clearEventListCache()
|
||||
|
||||
1. **新增筛选参数**:在 `GetPmEventListParams` 中增加字段,并在 `getPmEventPublic` 的 query 中传入
|
||||
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` 覆盖
|
||||
- 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`(订单/持仓/余额推送)
|
||||
- GET 请求:支持 query 参数,自动序列化
|
||||
- POST 请求:支持 JSON body
|
||||
- 自定义 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
|
||||
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()
|
||||
// User WebSocket URL(订单/持仓/余额推送,需带 token 参数)
|
||||
const userWsUrl = getUserWsUrl()
|
||||
|
||||
// GET 请求
|
||||
const data = await get<MyResponse>('/path', { page: 1, keyword: 'x' })
|
||||
|
||||
// 带鉴权
|
||||
const data = await get<MyResponse>('/path', undefined, {
|
||||
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
|
||||
- `logout`:清空并断开 UserSocket
|
||||
- `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,用于订单/持仓/余额实时推送
|
||||
|
||||
## 使用方式
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
## 功能用途
|
||||
|
||||
首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多。
|
||||
首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多。底部 Footer 已抽成独立组件 `Footer.vue`。
|
||||
|
||||
## 核心能力
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
- **事件列表**:卡片式展示,支持下拉刷新、触底加载
|
||||
- **搜索**:可按关键词搜索事件
|
||||
- **分类筛选**:选中分类后,自动提取所有层级节点的 `tagIds` 进行事件筛选
|
||||
- **Footer**:使用 `<Footer />` 组件,包含品牌、链接、语言选择、免责声明
|
||||
|
||||
## 数据流
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
- **合并成功**:`TradeComponent` 触发 `mergeSuccess` → `onMergeSuccess()` 刷新持仓,显示 toast 提示
|
||||
- **拆分成功**:`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 映射。
|
||||
|
||||
## 扩展方式
|
||||
|
||||
|
||||
156
src/api/event.ts
156
src/api/event.ts
@ -1,12 +1,7 @@
|
||||
import { get } from './request'
|
||||
import { buildQuery, get } from './request'
|
||||
import type { PageResult } from './types'
|
||||
|
||||
/** 分页结果 */
|
||||
export interface PageResult<T> {
|
||||
list: T[]
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
}
|
||||
export type { PageResult }
|
||||
|
||||
/**
|
||||
* Event 单项结构(与 doc.json definitions["polymarket.PmEvent"] 对齐)
|
||||
@ -137,15 +132,7 @@ export async function getPmEventPublic(
|
||||
params: GetPmEventListParams = {},
|
||||
): Promise<PmEventListResponse> {
|
||||
const { page = 1, pageSize = 10, keyword, createdAtRange, tagIds } = params
|
||||
const query: Record<string, string | number | number[] | string[] | undefined> = {
|
||||
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
|
||||
}
|
||||
const query = buildQuery({ page, pageSize, keyword, createdAtRange, tagIds })
|
||||
return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
|
||||
}
|
||||
|
||||
@ -186,9 +173,7 @@ export async function findPmEvent(
|
||||
params: FindPmEventParams,
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<PmEventDetailResponse> {
|
||||
const query: Record<string, string | number> = {}
|
||||
if (params.id != null) query.ID = params.id
|
||||
if (params.slug != null && params.slug !== '') query.slug = params.slug
|
||||
const query = buildQuery({ ID: params.id, slug: params.slug })
|
||||
if (Object.keys(query).length === 0) {
|
||||
throw new Error('findPmEvent: 至少需要传 id 或 slug')
|
||||
}
|
||||
@ -276,96 +261,73 @@ function marketChance(market: PmEventMarketItem): number {
|
||||
return Math.min(100, Math.max(0, Math.round(yesPrice * 100)))
|
||||
}
|
||||
|
||||
function formatVolume(volume: number | undefined): string {
|
||||
if (volume == null || !Number.isFinite(volume)) return '$0 Vol.'
|
||||
return volume >= 1000 ? `$${(volume / 1000).toFixed(1)}k Vol.` : `$${Math.round(volume)} Vol.`
|
||||
}
|
||||
|
||||
function formatExpiresAt(endDate: string | undefined): string {
|
||||
if (!endDate) return ''
|
||||
try {
|
||||
const d = new Date(endDate)
|
||||
return !Number.isNaN(d.getTime())
|
||||
? d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
: endDate
|
||||
} catch {
|
||||
return endDate
|
||||
}
|
||||
}
|
||||
|
||||
function parseOutcomePrices(m: PmEventMarketItem): { yesPrice: number; noPrice: number } {
|
||||
const y = m?.outcomePrices?.[0]
|
||||
const n = m?.outcomePrices?.[1]
|
||||
const yesPrice =
|
||||
y != null && Number.isFinite(parseFloat(String(y)))
|
||||
? Math.min(1, Math.max(0, parseFloat(String(y))))
|
||||
: 0.5
|
||||
const noPrice =
|
||||
n != null && Number.isFinite(parseFloat(String(n)))
|
||||
? Math.min(1, Math.max(0, parseFloat(String(n))))
|
||||
: 1 - yesPrice
|
||||
return { yesPrice, noPrice }
|
||||
}
|
||||
|
||||
function mapMarketToOutcome(m: PmEventMarketItem): EventCardOutcome {
|
||||
const { yesPrice, noPrice } = parseOutcomePrices(m)
|
||||
return {
|
||||
title: m.question ?? '',
|
||||
chanceValue: marketChance(m),
|
||||
yesPrice,
|
||||
noPrice,
|
||||
yesLabel: m.outcomes?.[0] ?? 'Yes',
|
||||
noLabel: m.outcomes?.[1] ?? 'No',
|
||||
marketId: getMarketId(m),
|
||||
clobTokenIds: m.clobTokenIds,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 list 单项映射为首页 MarketCard 所需字段
|
||||
* - 单一 market 或无 market:displayType single,当前逻辑
|
||||
* - 单一 market 或无 market:displayType single
|
||||
* - 多个 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 ?? []
|
||||
const multi = markets.length > 1
|
||||
|
||||
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 {
|
||||
const d = new Date(item.endDate)
|
||||
if (!Number.isNaN(d.getTime())) {
|
||||
expiresAt = d.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
expiresAt = item.endDate
|
||||
}
|
||||
}
|
||||
|
||||
const category = item.series?.[0]?.title ?? item.tags?.[0]?.label ?? ''
|
||||
|
||||
function parseOutcomePrices(m: PmEventMarketItem): { yesPrice: number; noPrice: number } {
|
||||
const y = m?.outcomePrices?.[0]
|
||||
const n = m?.outcomePrices?.[1]
|
||||
const yesPrice =
|
||||
y != null && Number.isFinite(parseFloat(String(y)))
|
||||
? Math.min(1, Math.max(0, parseFloat(String(y))))
|
||||
: 0.5
|
||||
const noPrice =
|
||||
n != null && Number.isFinite(parseFloat(String(n)))
|
||||
? Math.min(1, Math.max(0, parseFloat(String(n))))
|
||||
: 1 - yesPrice
|
||||
return { yesPrice, noPrice }
|
||||
}
|
||||
|
||||
const outcomes: EventCardOutcome[] | undefined = multi
|
||||
? markets.map((m) => {
|
||||
const { yesPrice, noPrice } = parseOutcomePrices(m)
|
||||
return {
|
||||
title: m.question ?? '',
|
||||
chanceValue: marketChance(m),
|
||||
yesPrice,
|
||||
noPrice,
|
||||
yesLabel: m.outcomes?.[0] ?? 'Yes',
|
||||
noLabel: m.outcomes?.[1] ?? 'No',
|
||||
marketId: getMarketId(m),
|
||||
clobTokenIds: m.clobTokenIds,
|
||||
}
|
||||
})
|
||||
: undefined
|
||||
|
||||
const firstPrices = firstMarket ? parseOutcomePrices(firstMarket) : { yesPrice: 0.5, noPrice: 0.5 }
|
||||
|
||||
return {
|
||||
id,
|
||||
id: String(item.ID ?? ''),
|
||||
slug: item.slug ?? undefined,
|
||||
marketTitle,
|
||||
chanceValue,
|
||||
marketInfo,
|
||||
imageUrl,
|
||||
category,
|
||||
expiresAt,
|
||||
marketTitle: item.title ?? '',
|
||||
chanceValue: firstMarket ? marketChance(firstMarket) : 17,
|
||||
marketInfo: formatVolume(item.volume),
|
||||
imageUrl: item.image ?? item.icon ?? '',
|
||||
category: item.series?.[0]?.title ?? item.tags?.[0]?.label ?? '',
|
||||
expiresAt: formatExpiresAt(item.endDate),
|
||||
displayType: multi ? 'multi' : 'single',
|
||||
outcomes,
|
||||
outcomes: multi ? markets.map(mapMarketToOutcome) : undefined,
|
||||
yesLabel: firstMarket?.outcomes?.[0] ?? 'Yes',
|
||||
noLabel: firstMarket?.outcomes?.[1] ?? 'No',
|
||||
isNew: item.new === true,
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
import { post } from './request'
|
||||
import type { ApiResponse } from './types'
|
||||
|
||||
/** 通用响应:与 doc.json response.Response 一致 */
|
||||
export interface ApiResponse<T = unknown> {
|
||||
code: number
|
||||
data?: T
|
||||
msg: string
|
||||
}
|
||||
export type { ApiResponse }
|
||||
|
||||
/**
|
||||
* 下单请求体(/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 interface PageResult<T> {
|
||||
list: T[]
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
}
|
||||
export type { PageResult }
|
||||
|
||||
/**
|
||||
* 订单项(与 doc.json definitions["model.ClobOrder"] 对齐)
|
||||
@ -71,13 +66,16 @@ export async function getOrderList(
|
||||
): Promise<OrderListResponse> {
|
||||
const { page = 1, pageSize = 10, status, startCreatedAt, endCreatedAt, marketID, tokenID, userID } =
|
||||
params
|
||||
const query: Record<string, string | number | undefined> = { page, pageSize }
|
||||
if (status != null && Number.isFinite(status)) query.status = status
|
||||
if (startCreatedAt != null && startCreatedAt !== '') query.startCreatedAt = startCreatedAt
|
||||
if (endCreatedAt != null && endCreatedAt !== '') query.endCreatedAt = endCreatedAt
|
||||
if (marketID != null && marketID !== '') query.marketID = marketID
|
||||
if (tokenID != null && tokenID !== '') query.tokenID = tokenID
|
||||
if (userID != null && Number.isFinite(userID)) query.userID = userID
|
||||
const query = buildQuery({
|
||||
page,
|
||||
pageSize,
|
||||
status,
|
||||
startCreatedAt,
|
||||
endCreatedAt,
|
||||
marketID,
|
||||
tokenID,
|
||||
userID,
|
||||
})
|
||||
return get<OrderListResponse>('/clob/order/getOrderList', query, config)
|
||||
}
|
||||
|
||||
@ -88,12 +86,7 @@ export interface CancelOrderReq {
|
||||
userID: number
|
||||
}
|
||||
|
||||
/** 通用 API 响应 */
|
||||
export interface ApiResponse {
|
||||
code: number
|
||||
data?: unknown
|
||||
msg: string
|
||||
}
|
||||
export type { ApiResponse }
|
||||
|
||||
/**
|
||||
* POST /clob/order/cancelOrder
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
import { get } from './request'
|
||||
import { buildQuery, get } from './request'
|
||||
import type { PageResult } from './types'
|
||||
|
||||
/** 分页结果 */
|
||||
export interface PageResult<T> {
|
||||
list: T[]
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
}
|
||||
export type { PageResult }
|
||||
|
||||
/**
|
||||
* 持仓项(与 /clob/position/getPositionList 实际返回对齐)
|
||||
@ -72,12 +67,15 @@ export async function getPositionList(
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<PositionListResponse> {
|
||||
const { page = 1, pageSize = 10, startCreatedAt, endCreatedAt, marketID, tokenID, userID } = params
|
||||
const query: Record<string, string | number | undefined> = { page, pageSize }
|
||||
if (startCreatedAt != null && startCreatedAt !== '') query.startCreatedAt = startCreatedAt
|
||||
if (endCreatedAt != null && endCreatedAt !== '') query.endCreatedAt = endCreatedAt
|
||||
if (marketID != null && marketID !== '') query.marketID = marketID
|
||||
if (tokenID != null && tokenID !== '') query.tokenID = tokenID
|
||||
if (userID != null && Number.isFinite(userID)) query.userID = userID
|
||||
const query = buildQuery({
|
||||
page,
|
||||
pageSize,
|
||||
startCreatedAt,
|
||||
endCreatedAt,
|
||||
marketID,
|
||||
tokenID,
|
||||
userID,
|
||||
})
|
||||
return get<PositionListResponse>('/clob/position/getPositionList', query, config)
|
||||
}
|
||||
|
||||
@ -105,6 +103,8 @@ export interface PositionDisplayItem {
|
||||
lockedSharesNum?: number
|
||||
/** 可卖份额数值(来自 available),available > 0 表示有订单可卖,此为最大可卖份额 */
|
||||
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 costUsd = costRaw / SCALE
|
||||
const shares = `${size} shares`
|
||||
const outcomeWord = pos.outcome === 'No' ? 'No' : 'Yes'
|
||||
const pillClass = outcomeWord === 'No' ? 'pill-down' : 'pill-yes'
|
||||
const outcome = pos.outcome ?? 'Yes'
|
||||
const pillClass =
|
||||
outcome === 'No' || outcome === 'Down' || outcome === 'Below' ? 'pill-down' : 'pill-yes'
|
||||
const value = `$${costUsd.toFixed(2)}`
|
||||
const bet = value
|
||||
const toWin = `$${size.toFixed(2)}`
|
||||
const outcomeTag = `${outcomeWord} —`
|
||||
const outcomeTag = `${outcome} —`
|
||||
const locked = lockRaw > 0
|
||||
const lockedSharesNum = lockRaw > 0 ? lockRaw / SCALE : undefined
|
||||
return {
|
||||
@ -138,10 +139,11 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
|
||||
bet,
|
||||
toWin,
|
||||
value,
|
||||
sellOutcome: outcomeWord,
|
||||
outcomeWord,
|
||||
sellOutcome: outcome,
|
||||
outcomeWord: outcome,
|
||||
outcomeTag,
|
||||
outcomePillClass: pillClass,
|
||||
outcome,
|
||||
locked,
|
||||
lockedSharesNum,
|
||||
availableSharesNum: availableNum >= 0 ? availableNum : undefined,
|
||||
|
||||
@ -1,28 +1,50 @@
|
||||
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 =
|
||||
typeof import.meta !== 'undefined' &&
|
||||
(import.meta as unknown as { env?: Record<string, string> }).env?.VITE_API_BASE_URL
|
||||
? (import.meta as unknown as { env: Record<string, string> }).env.VITE_API_BASE_URL
|
||||
: 'https://api.xtrader.vip'
|
||||
(import.meta as { env?: { VITE_API_BASE_URL?: string } }).env?.VITE_API_BASE_URL ??
|
||||
'https://api.xtrader.vip'
|
||||
|
||||
/** CLOB WebSocket URL,与 REST API 同源 */
|
||||
export function getClobWsUrl(): string {
|
||||
const base = BASE_URL || (typeof window !== 'undefined' ? window.location.origin : 'https://api.xtrader.vip')
|
||||
const FALLBACK_BASE =
|
||||
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 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 {
|
||||
const base = BASE_URL || (typeof window !== 'undefined' ? window.location.origin : 'https://api.xtrader.vip')
|
||||
const url = new URL(base)
|
||||
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
return `${protocol}//${url.host}/clob/ws/user`
|
||||
return getWsUrl('/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 {
|
||||
|
||||
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', () => {
|
||||
const stored = loadStored()
|
||||
const token = ref<string>(stored?.token ?? '')
|
||||
@ -110,18 +125,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
const raw = loginData.user ?? null
|
||||
token.value = t
|
||||
if (raw) {
|
||||
const rawId = raw.ID ?? raw.id
|
||||
const numId =
|
||||
typeof rawId === 'number'
|
||||
? rawId
|
||||
: rawId != null
|
||||
? parseInt(String(rawId), 10)
|
||||
: undefined
|
||||
user.value = {
|
||||
...raw,
|
||||
id: rawId ?? numId,
|
||||
ID: Number.isFinite(numId) ? numId : raw.ID,
|
||||
}
|
||||
const { id, numId } = parseUserId(raw)
|
||||
user.value = { ...raw, id, ID: numId ?? raw.ID }
|
||||
} else {
|
||||
user.value = null
|
||||
}
|
||||
@ -181,20 +186,14 @@ export const useUserStore = defineStore('user', () => {
|
||||
// 更新用户信息:data.userInfo
|
||||
const u = (data?.userInfo ?? data?.user ?? data) as Record<string, unknown> | undefined
|
||||
if (!u) return
|
||||
const rawId = u.ID ?? u.id
|
||||
const numId =
|
||||
typeof rawId === 'number'
|
||||
? rawId
|
||||
: rawId != null
|
||||
? parseInt(String(rawId), 10)
|
||||
: undefined
|
||||
const { id, numId } = parseUserId(u as { id?: number | string; ID?: number })
|
||||
user.value = {
|
||||
...u,
|
||||
userName: (u.userName ?? u.username) as string | undefined,
|
||||
nickName: (u.nickName ?? u.nickname) as string | undefined,
|
||||
headerImg: (u.headerImg ?? u.avatar ?? u.avatarUrl) as string | undefined,
|
||||
id: (rawId ?? numId) as number | string | undefined,
|
||||
ID: Number.isFinite(numId) ? numId : undefined,
|
||||
id,
|
||||
ID: numId,
|
||||
} as UserInfo
|
||||
if (token.value && user.value) saveToStorage(token.value, user.value)
|
||||
} catch (e) {
|
||||
|
||||
@ -223,82 +223,7 @@
|
||||
</v-bottom-sheet>
|
||||
</v-container>
|
||||
|
||||
<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>
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -306,6 +231,7 @@
|
||||
defineOptions({ name: 'Home' })
|
||||
import { ref, onMounted, onUnmounted, onActivated, onDeactivated, nextTick, computed, watch } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import Footer from '../components/Footer.vue'
|
||||
import MarketCard from '../components/MarketCard.vue'
|
||||
import TradeComponent from '../components/TradeComponent.vue'
|
||||
import {
|
||||
@ -519,7 +445,6 @@ const noMoreEvents = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const footerLang = ref('English')
|
||||
const tradeDialogOpen = ref(false)
|
||||
const tradeDialogSide = ref<'yes' | 'no'>('yes')
|
||||
const tradeDialogMarket = ref<{
|
||||
@ -1186,197 +1111,10 @@ onActivated(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
/* 页面布局:flex 列,Footer 通过 margin-top: auto 贴底 */
|
||||
.home-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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>
|
||||
|
||||
@ -818,13 +818,13 @@ function onSplitSuccess() {
|
||||
loadMarketPositions()
|
||||
}
|
||||
|
||||
/** 从持仓项点击 Sell:弹出交易组件并切到 Sell、对应 Yes/No。是否可卖由 available 判断,available > 0 才可卖。 */
|
||||
/** 从持仓项点击 Sell:弹出交易组件并切到 Sell、对应 Yes/No。持仓类型用 outcome 字段与 noLabel 匹配。 */
|
||||
function openSellFromPosition(pos: PositionDisplayItem) {
|
||||
if (pos.availableSharesNum == null || pos.availableSharesNum <= 0) {
|
||||
toastStore.show(t('activity.noAvailableSharesToSell'), 'warning')
|
||||
return
|
||||
}
|
||||
const option = pos.outcomeWord === 'No' ? 'no' : 'yes'
|
||||
const option = pos.outcome === noLabel.value ? 'no' : 'yes'
|
||||
if (isMobile.value) {
|
||||
tradeInitialOptionFromBar.value = option
|
||||
tradeInitialTabFromBar.value = 'sell'
|
||||
@ -914,11 +914,12 @@ const marketPositionsFiltered = computed(() =>
|
||||
}),
|
||||
)
|
||||
|
||||
/** 转为 TradeComponent 所需的 TradePositionItem[];sharesNum 用 available(最大可卖份额),available > 0 表示有订单可卖 */
|
||||
const tradePositionsForComponent = computed<TradePositionItem[]>(() =>
|
||||
marketPositionsFiltered.value.map((p) => ({
|
||||
/** 转为 TradeComponent 所需的 TradePositionItem[];sharesNum 用 available(最大可卖份额),available > 0 表示有订单可卖;outcome 与 noLabel 匹配则为 No */
|
||||
const tradePositionsForComponent = computed<TradePositionItem[]>(() => {
|
||||
const no = noLabel.value
|
||||
return marketPositionsFiltered.value.map((p) => ({
|
||||
id: p.id,
|
||||
outcomeWord: (p.outcomeWord === 'No' ? 'No' : 'Yes') as 'Yes' | 'No',
|
||||
outcomeWord: (p.outcome === no ? 'No' : 'Yes') as 'Yes' | 'No',
|
||||
shares: p.shares,
|
||||
sharesNum:
|
||||
p.availableSharesNum != null && Number.isFinite(p.availableSharesNum)
|
||||
@ -926,7 +927,7 @@ const tradePositionsForComponent = computed<TradePositionItem[]>(() =>
|
||||
: parseFloat(p.shares?.replace(/[^0-9.]/g, '')) || undefined,
|
||||
locked: p.locked,
|
||||
}))
|
||||
)
|
||||
})
|
||||
|
||||
async function loadMarketPositions() {
|
||||
const marketID = currentMarketId.value
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user