新增:持仓数据接口对接
This commit is contained in:
parent
45a4b2c00f
commit
d377dd3523
@ -4,12 +4,62 @@
|
|||||||
|
|
||||||
## 功能用途
|
## 功能用途
|
||||||
|
|
||||||
持仓相关 API:分页获取持仓列表,以及将 `ClobPositionItem` 映射为钱包展示项。`PageResult` 来自 `@/api/types`,使用 `buildQuery` 构建请求参数。
|
持仓相关 API:分页获取持仓列表,以及将 `ClobPositionItem` 映射为钱包展示项。`PageResult` 来自 `@/api/types`,使用 `buildQuery` 构建请求参数。接口定义以 Swagger doc.json 为准。
|
||||||
|
|
||||||
## 核心能力
|
## 核心能力
|
||||||
|
|
||||||
- `getPositionList`:分页获取持仓列表(需鉴权)
|
- `getPositionList`:分页获取持仓列表(需鉴权 x-token、x-user-id);返回项含 `needClaim`、`market`(内嵌市场 question、outcomes、outcomePrices)
|
||||||
- `mapPositionToDisplayItem`:将接口项转为展示结构(含 locked、availableSharesNum、outcome 等);`outcome` 保留 API 原始值(如 "Up"/"Down"、"Yes"/"No"),供 TradeDetail 与市场 outcomes 匹配
|
- `mapPositionToDisplayItem`:将接口项转为展示结构;`market` 优先用 `market.question`,否则用 marketID;`avgNow` 有 `market.outcomePrices` 时展示「AVG → NOW」格式;`iconChar`/`iconClass`/`imageUrl` 用于展示图标(market.image 优先)
|
||||||
|
|
||||||
|
## GET /clob/position/getPositionList
|
||||||
|
|
||||||
|
### 请求参数(Query)
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| page | number | 否 | 页码 |
|
||||||
|
| pageSize | number | 否 | 每页数量 |
|
||||||
|
| startCreatedAt | string | 否 | 开始时间 |
|
||||||
|
| endCreatedAt | string | 否 | 结束时间 |
|
||||||
|
| marketID | string | 否 | 市场ID |
|
||||||
|
| tokenID | string | 否 | Token ID |
|
||||||
|
| userID | number | 否 | 用户ID |
|
||||||
|
|
||||||
|
### 响应
|
||||||
|
|
||||||
|
`{ code, data, msg }`,`data` 为 `PageResult<ClobPositionItem>`。
|
||||||
|
|
||||||
|
### ClobPositionItem(实际返回结构)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| ID | number | 主键 |
|
||||||
|
| userID | number | 用户ID |
|
||||||
|
| marketID | number \| string | 市场ID |
|
||||||
|
| tokenId | string | Token ID |
|
||||||
|
| size | string | 份额(6 位小数) |
|
||||||
|
| available | string | 可用份额 |
|
||||||
|
| lock | string | 锁单数量 |
|
||||||
|
| cost | string | 成本 |
|
||||||
|
| outcome | string | 方向 |
|
||||||
|
| version | number | 版本号 |
|
||||||
|
| needClaim | boolean | 是否待领取结算 |
|
||||||
|
| market | ClobPositionMarket | 内嵌市场详情(question、outcomes、outcomePrices 等) |
|
||||||
|
| createdAt | string | 创建时间 |
|
||||||
|
| updatedAt | string | 更新时间 |
|
||||||
|
|
||||||
|
### ClobPositionMarket(market 字段)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| ID | number | 市场ID |
|
||||||
|
| question | string | 市场问题 |
|
||||||
|
| slug | string | 市场 slug |
|
||||||
|
| image | string | 市场图片 URL |
|
||||||
|
| icon | string | 市场图标 URL |
|
||||||
|
| outcomes | string[] | 选项(如 ["Up", "Down"]) |
|
||||||
|
| outcomePrices | string[] \| number[] | 各选项当前价格 |
|
||||||
|
| clobTokenIds | string[] | CLOB Token ID 列表 |
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
|
|||||||
102
src/api/chart.ts
Normal file
102
src/api/chart.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* 市场概率/价格历史图表 API 类型定义
|
||||||
|
* 用于 TradeDetail.vue 折线图接入真实接口时的请求与响应约定
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ApiResponse } from './types'
|
||||||
|
|
||||||
|
/** 图表使用的基础数据格式:[时间戳(ms), 概率值(0-100)][] */
|
||||||
|
export type ChartDataPoint = [number, number]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间范围,与前端 Time Range 按钮一致
|
||||||
|
* - 1H / 6H / 1D / 1W / 1M:对应时间窗口
|
||||||
|
* - ALL:全部历史
|
||||||
|
*/
|
||||||
|
export type ChartTimeRange = '1H' | '6H' | '1D' | '1W' | '1M' | 'ALL'
|
||||||
|
|
||||||
|
/** 请求参数:按市场 + 时间范围拉取历史 */
|
||||||
|
export interface ChartHistoryParams {
|
||||||
|
/** 市场 ID(或 tokenId / conditionId,以实际后端为准) */
|
||||||
|
marketID: string
|
||||||
|
/** 时间范围 */
|
||||||
|
range: ChartTimeRange
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单条历史点(接口可任选一种格式,由 normalizer 统一转成 ChartDataPoint)
|
||||||
|
* 推荐:timestamp 毫秒 + value 为 0–100 的概率
|
||||||
|
*/
|
||||||
|
export interface ChartHistoryItem {
|
||||||
|
/** 时间:Unix 毫秒时间戳 */
|
||||||
|
timestamp: number
|
||||||
|
/** 概率/价格:0–100 表示百分比,或 0–1 表示小数(normalizer 会乘 100) */
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 可选:接口用秒级时间戳 + 小数概率 */
|
||||||
|
export interface ChartHistoryItemAlt {
|
||||||
|
/** 时间:Unix 秒级时间戳 */
|
||||||
|
t?: number
|
||||||
|
/** 概率:0–1 小数 */
|
||||||
|
probability?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接口响应体建议结构
|
||||||
|
* 后端可返回 data 为 ChartHistoryItem[] 或 ChartHistoryItemAlt[] 或 ChartDataPoint[]
|
||||||
|
*/
|
||||||
|
export interface ChartHistoryResponse {
|
||||||
|
code?: number
|
||||||
|
data?: ChartHistoryItem[] | ChartHistoryItemAlt[] | ChartDataPoint[]
|
||||||
|
msg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将接口返回的 data 转成 ECharts 使用的 [timestamp, value][]
|
||||||
|
* - 支持 { timestamp, value }(value 0–100 或 0–1)
|
||||||
|
* - 支持 { t, probability }(t 为秒则乘 1000,probability 0–1 乘 100)
|
||||||
|
* - 已是 [number, number][] 则按需只做排序
|
||||||
|
*/
|
||||||
|
export function normalizeChartData(
|
||||||
|
raw: ChartHistoryResponse['data']
|
||||||
|
): ChartDataPoint[] {
|
||||||
|
if (!raw || !Array.isArray(raw)) return []
|
||||||
|
|
||||||
|
const out: ChartDataPoint[] = []
|
||||||
|
|
||||||
|
for (const item of raw) {
|
||||||
|
if (Array.isArray(item) && item.length >= 2 && typeof item[0] === 'number' && typeof item[1] === 'number') {
|
||||||
|
out.push([item[0], item[1]])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const obj = item as Record<string, unknown>
|
||||||
|
if (typeof obj.timestamp === 'number' && typeof obj.value === 'number') {
|
||||||
|
const v = (obj.value as number) <= 1 ? (obj.value as number) * 100 : (obj.value as number)
|
||||||
|
out.push([obj.timestamp as number, v])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (typeof obj.t === 'number' && typeof obj.probability === 'number') {
|
||||||
|
const tMs = (obj.t as number) < 1e12 ? (obj.t as number) * 1000 : (obj.t as number)
|
||||||
|
out.push([tMs, (obj.probability as number) * 100])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.sort((a, b) => a[0] - b[0])
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例:请求市场历史数据(需根据实际后端路径实现)
|
||||||
|
* GET /api/market/{marketID}/chart?range=1D
|
||||||
|
* 或 POST /api/chart/history body: { marketID, range }
|
||||||
|
*/
|
||||||
|
export async function fetchChartHistory(
|
||||||
|
_params: ChartHistoryParams,
|
||||||
|
_config?: { headers?: Record<string, string> }
|
||||||
|
): Promise<ApiResponse<ChartHistoryItem[]>> {
|
||||||
|
// TODO: 替换为真实 request.get/post 调用
|
||||||
|
// const res = await get<ChartHistoryResponse>(`/market/${params.marketID}/chart`, { params: { range: params.range } })
|
||||||
|
// return { ...res, data: normalizeChartData(res.data) }
|
||||||
|
return { code: 0, data: [], msg: '' }
|
||||||
|
}
|
||||||
@ -70,6 +70,10 @@ export interface MockPosition {
|
|||||||
valueChangeLoss?: boolean
|
valueChangeLoss?: boolean
|
||||||
sellOutcome?: string
|
sellOutcome?: string
|
||||||
outcomeWord?: string
|
outcomeWord?: string
|
||||||
|
/** 用于领取结算(mock 时可选,便于展示未结算行) */
|
||||||
|
marketID?: string
|
||||||
|
tokenID?: string
|
||||||
|
needClaim?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MockOpenOrder {
|
export interface MockOpenOrder {
|
||||||
@ -123,6 +127,9 @@ export const MOCK_WALLET_POSITIONS: MockPosition[] = [
|
|||||||
valueChangeLoss: true,
|
valueChangeLoss: true,
|
||||||
sellOutcome: 'Down',
|
sellOutcome: 'Down',
|
||||||
outcomeWord: 'Down',
|
outcomeWord: 'Down',
|
||||||
|
marketID: 'mock-market-1',
|
||||||
|
tokenID: MOCK_TOKEN_ID,
|
||||||
|
needClaim: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'p2',
|
id: 'p2',
|
||||||
|
|||||||
@ -1,16 +1,34 @@
|
|||||||
import { buildQuery, get } from './request'
|
import { buildQuery, get, post } from './request'
|
||||||
|
import type { ApiResponse } from './types'
|
||||||
import type { PageResult } from './types'
|
import type { PageResult } from './types'
|
||||||
|
|
||||||
export type { PageResult }
|
export type { PageResult }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 持仓项(与 /clob/position/getPositionList 实际返回对齐)
|
* 持仓项内嵌的市场信息(getPositionList 返回的 market 字段)
|
||||||
* size、available、cost 为字符串,6 位小数(除以 1000000 得实际值)
|
*/
|
||||||
|
export interface ClobPositionMarket {
|
||||||
|
ID?: number
|
||||||
|
question?: string
|
||||||
|
slug?: string
|
||||||
|
image?: string
|
||||||
|
icon?: string
|
||||||
|
outcomes?: string[]
|
||||||
|
outcomePrices?: string[] | number[]
|
||||||
|
clobTokenIds?: string[]
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 持仓项(与 getPositionList 实际返回对齐)
|
||||||
|
* size、available、cost、lock 为字符串,6 位小数(除以 1000000 得实际值)
|
||||||
|
* marketID 可能为 number 或 string;market 为内嵌市场详情
|
||||||
*/
|
*/
|
||||||
export interface ClobPositionItem {
|
export interface ClobPositionItem {
|
||||||
ID?: number
|
ID?: number
|
||||||
userID?: number
|
userID?: number
|
||||||
marketID?: string
|
/** 市场 ID,可能为 number 或 string */
|
||||||
|
marketID?: number | string
|
||||||
tokenId?: string
|
tokenId?: string
|
||||||
/** 份额,字符串,6 位小数 */
|
/** 份额,字符串,6 位小数 */
|
||||||
size?: string
|
size?: string
|
||||||
@ -20,9 +38,16 @@ export interface ClobPositionItem {
|
|||||||
lock?: string
|
lock?: string
|
||||||
/** 成本,字符串,6 位小数 */
|
/** 成本,字符串,6 位小数 */
|
||||||
cost?: string
|
cost?: string
|
||||||
/** 方向:Yes | No */
|
/** 方向:Yes | No 或 Up | Down 等 */
|
||||||
outcome?: string
|
outcome?: string
|
||||||
version?: number
|
version?: number
|
||||||
|
/** 是否待领取结算 */
|
||||||
|
needClaim?: boolean
|
||||||
|
/** 内嵌市场详情(问题、outcomes、outcomePrices 等) */
|
||||||
|
market?: ClobPositionMarket
|
||||||
|
/** 创建时间 */
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
CreatedAt?: string
|
CreatedAt?: string
|
||||||
UpdatedAt?: string
|
UpdatedAt?: string
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
@ -79,11 +104,37 @@ export async function getPositionList(
|
|||||||
return get<PositionListResponse>('/clob/position/getPositionList', query, config)
|
return get<PositionListResponse>('/clob/position/getPositionList', query, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领取结算请求体(与 doc.json definitions.request.PositionClaimRequest 一致)
|
||||||
|
* POST /clob/position/claimPosition
|
||||||
|
*/
|
||||||
|
export interface ClaimPositionRequest {
|
||||||
|
/** 市场 ID 列表,与 tokenID 一一对应 */
|
||||||
|
marketID: string[]
|
||||||
|
/** Token ID 列表(如 CLOB tokenId),与 marketID 一一对应 */
|
||||||
|
tokenID: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 领取结算(已结束市场的赢利持仓)
|
||||||
|
* POST /clob/position/claimPosition
|
||||||
|
* Body: { marketID: string[], tokenID: string[] }
|
||||||
|
* 需鉴权:x-token、x-user-id
|
||||||
|
*/
|
||||||
|
export async function claimPosition(
|
||||||
|
data: ClaimPositionRequest,
|
||||||
|
config?: { headers?: Record<string, string> },
|
||||||
|
): Promise<ApiResponse> {
|
||||||
|
return post<ApiResponse>('/clob/position/claimPosition', data, config)
|
||||||
|
}
|
||||||
|
|
||||||
/** 钱包 Positions 展示项(与 Wallet.vue Position 一致) */
|
/** 钱包 Positions 展示项(与 Wallet.vue Position 一致) */
|
||||||
export interface PositionDisplayItem {
|
export interface PositionDisplayItem {
|
||||||
id: string
|
id: string
|
||||||
|
/** 市场标题(优先 market.question,否则 marketID) */
|
||||||
market: string
|
market: string
|
||||||
shares: string
|
shares: string
|
||||||
|
/** AVG • NOW 格式,如 "50¢ → 39¢";有 market.outcomePrices 时展示当前价 */
|
||||||
avgNow: string
|
avgNow: string
|
||||||
bet: string
|
bet: string
|
||||||
toWin: string
|
toWin: string
|
||||||
@ -105,15 +156,43 @@ export interface PositionDisplayItem {
|
|||||||
availableSharesNum?: number
|
availableSharesNum?: number
|
||||||
/** 原始 outcome 字段(API 返回,如 "Up"/"Down" 或 "Yes"/"No"),用于与市场 outcomes 匹配 */
|
/** 原始 outcome 字段(API 返回,如 "Up"/"Down" 或 "Yes"/"No"),用于与市场 outcomes 匹配 */
|
||||||
outcome?: string
|
outcome?: string
|
||||||
|
/** 市场图片 URL(market.image),有则优先展示 */
|
||||||
|
imageUrl?: string
|
||||||
|
/** 市场 ID(用于 claimPosition),统一为 string */
|
||||||
|
marketID?: string
|
||||||
|
/** Token ID(用于 claimPosition,API 返回 tokenId) */
|
||||||
|
tokenID?: string
|
||||||
|
/** 是否待领取/未结算(needClaim 为 true 时显示领取按钮) */
|
||||||
|
needClaim?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 market.outcomes 和 outcome 找到对应 outcomePrices 的索引
|
||||||
|
*/
|
||||||
|
function getOutcomePriceIndex(
|
||||||
|
outcomes: string[] | undefined,
|
||||||
|
outcome: string,
|
||||||
|
): number {
|
||||||
|
if (!outcomes?.length) return 0
|
||||||
|
const idx = outcomes.findIndex((o) => o === outcome)
|
||||||
|
return idx >= 0 ? idx : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据 outcome 返回图标字符 */
|
||||||
|
function getOutcomeIconChar(outcome: string): string {
|
||||||
|
const o = outcome?.trim() || ''
|
||||||
|
if (o === 'Up' || o === 'Yes' || o === 'Above') return '↑'
|
||||||
|
if (o === 'Down' || o === 'No' || o === 'Below') return '↓'
|
||||||
|
return o.charAt(0)?.toUpperCase() || '•'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 ClobPositionItem 映射为钱包 Position 展示项
|
* 将 ClobPositionItem 映射为钱包 Position 展示项
|
||||||
* size、available、cost 为 6 位小数字符串
|
* size、available、cost 为 6 位小数字符串;market 有 question 时优先展示问题
|
||||||
*/
|
*/
|
||||||
export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplayItem {
|
export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplayItem {
|
||||||
const id = String(pos.ID ?? '')
|
const id = String(pos.ID ?? '')
|
||||||
const market = pos.marketID ?? ''
|
const market = pos.market?.question ?? String(pos.marketID ?? pos.market?.ID ?? '')
|
||||||
const sizeRaw = parsePosNum(pos.size ?? pos.available)
|
const sizeRaw = parsePosNum(pos.size ?? pos.available)
|
||||||
const availableRaw = parsePosNum(pos.available)
|
const availableRaw = parsePosNum(pos.available)
|
||||||
const costRaw = parsePosNum(pos.cost)
|
const costRaw = parsePosNum(pos.cost)
|
||||||
@ -131,11 +210,32 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
|
|||||||
const outcomeTag = `${outcome} —`
|
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
|
||||||
|
|
||||||
|
// AVG:成本/份额;NOW:market.outcomePrices 中对应 outcome 的当前价
|
||||||
|
let avgNow = '—'
|
||||||
|
if (size > 0 && costUsd > 0) {
|
||||||
|
const avgCents = Math.round((costUsd / size) * 100)
|
||||||
|
const avgStr = `${avgCents}¢`
|
||||||
|
const prices = pos.market?.outcomePrices
|
||||||
|
if (prices?.length) {
|
||||||
|
const idx = getOutcomePriceIndex(pos.market?.outcomes, outcome)
|
||||||
|
const nowVal = typeof prices[idx] === 'string' ? parseFloat(prices[idx] as string) : Number(prices[idx])
|
||||||
|
const nowCents = Number.isFinite(nowVal) ? Math.round(nowVal * 100) : null
|
||||||
|
avgNow = nowCents != null ? `${avgStr} → ${nowCents}¢` : avgStr
|
||||||
|
} else {
|
||||||
|
avgNow = avgStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const marketID = String(pos.marketID ?? pos.market?.ID ?? '')
|
||||||
|
const tokenID = pos.tokenId ?? (pos as { tokenID?: string }).tokenID ?? ''
|
||||||
|
const imageUrl = (pos.market?.image ?? pos.market?.icon) as string | undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
market,
|
market,
|
||||||
shares,
|
shares,
|
||||||
avgNow: '—',
|
avgNow,
|
||||||
bet,
|
bet,
|
||||||
toWin,
|
toWin,
|
||||||
value,
|
value,
|
||||||
@ -144,8 +244,14 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
|
|||||||
outcomeTag,
|
outcomeTag,
|
||||||
outcomePillClass: pillClass,
|
outcomePillClass: pillClass,
|
||||||
outcome,
|
outcome,
|
||||||
|
iconChar: getOutcomeIconChar(outcome),
|
||||||
|
iconClass: pillClass,
|
||||||
|
imageUrl: imageUrl || undefined,
|
||||||
locked,
|
locked,
|
||||||
lockedSharesNum,
|
lockedSharesNum,
|
||||||
availableSharesNum: availableNum >= 0 ? availableNum : undefined,
|
availableSharesNum: availableNum >= 0 ? availableNum : undefined,
|
||||||
|
marketID: marketID || undefined,
|
||||||
|
tokenID: tokenID || undefined,
|
||||||
|
needClaim: pos.needClaim,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,8 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"orderSuccess": "Order placed successfully",
|
"orderSuccess": "Order placed successfully",
|
||||||
"splitSuccess": "Split successful",
|
"splitSuccess": "Split successful",
|
||||||
"mergeSuccess": "Merge successful"
|
"mergeSuccess": "Merge successful",
|
||||||
|
"claimSuccess": "Claim successful"
|
||||||
},
|
},
|
||||||
"trade": {
|
"trade": {
|
||||||
"buy": "Buy",
|
"buy": "Buy",
|
||||||
@ -163,7 +164,9 @@
|
|||||||
"expiration": "EXPIRATION",
|
"expiration": "EXPIRATION",
|
||||||
"activity": "ACTIVITY",
|
"activity": "ACTIVITY",
|
||||||
"view": "View",
|
"view": "View",
|
||||||
"expirationLabel": "Expiration:"
|
"expirationLabel": "Expiration:",
|
||||||
|
"youWon": "You won ${amount}",
|
||||||
|
"claim": "Claim"
|
||||||
},
|
},
|
||||||
"deposit": {
|
"deposit": {
|
||||||
"title": "Deposit",
|
"title": "Deposit",
|
||||||
|
|||||||
@ -17,7 +17,8 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"orderSuccess": "注文が完了しました",
|
"orderSuccess": "注文が完了しました",
|
||||||
"splitSuccess": "スプリット成功",
|
"splitSuccess": "スプリット成功",
|
||||||
"mergeSuccess": "マージ成功"
|
"mergeSuccess": "マージ成功",
|
||||||
|
"claimSuccess": "受け取り完了"
|
||||||
},
|
},
|
||||||
"trade": {
|
"trade": {
|
||||||
"buy": "買う",
|
"buy": "買う",
|
||||||
@ -163,7 +164,9 @@
|
|||||||
"expiration": "有効期限",
|
"expiration": "有効期限",
|
||||||
"activity": "アクティビティ",
|
"activity": "アクティビティ",
|
||||||
"view": "表示",
|
"view": "表示",
|
||||||
"expirationLabel": "有効期限:"
|
"expirationLabel": "有効期限:",
|
||||||
|
"youWon": "獲得 $${amount}",
|
||||||
|
"claim": "受け取る"
|
||||||
},
|
},
|
||||||
"deposit": {
|
"deposit": {
|
||||||
"title": "入金",
|
"title": "入金",
|
||||||
|
|||||||
@ -17,7 +17,8 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"orderSuccess": "주문이 완료되었습니다",
|
"orderSuccess": "주문이 완료되었습니다",
|
||||||
"splitSuccess": "분할 완료",
|
"splitSuccess": "분할 완료",
|
||||||
"mergeSuccess": "병합 완료"
|
"mergeSuccess": "병합 완료",
|
||||||
|
"claimSuccess": "수령 완료"
|
||||||
},
|
},
|
||||||
"trade": {
|
"trade": {
|
||||||
"buy": "매수",
|
"buy": "매수",
|
||||||
@ -163,7 +164,9 @@
|
|||||||
"expiration": "만료",
|
"expiration": "만료",
|
||||||
"activity": "활동",
|
"activity": "활동",
|
||||||
"view": "보기",
|
"view": "보기",
|
||||||
"expirationLabel": "만료:"
|
"expirationLabel": "만료:",
|
||||||
|
"youWon": "당첨 $${amount}",
|
||||||
|
"claim": "수령"
|
||||||
},
|
},
|
||||||
"deposit": {
|
"deposit": {
|
||||||
"title": "입금",
|
"title": "입금",
|
||||||
|
|||||||
@ -17,7 +17,8 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"orderSuccess": "下单成功",
|
"orderSuccess": "下单成功",
|
||||||
"splitSuccess": "拆分成功",
|
"splitSuccess": "拆分成功",
|
||||||
"mergeSuccess": "合并成功"
|
"mergeSuccess": "合并成功",
|
||||||
|
"claimSuccess": "领取成功"
|
||||||
},
|
},
|
||||||
"trade": {
|
"trade": {
|
||||||
"buy": "买入",
|
"buy": "买入",
|
||||||
@ -163,7 +164,9 @@
|
|||||||
"expiration": "到期",
|
"expiration": "到期",
|
||||||
"activity": "活动",
|
"activity": "活动",
|
||||||
"view": "查看",
|
"view": "查看",
|
||||||
"expirationLabel": "到期"
|
"expirationLabel": "到期",
|
||||||
|
"youWon": "您赢得 ${amount}",
|
||||||
|
"claim": "领取"
|
||||||
},
|
},
|
||||||
"deposit": {
|
"deposit": {
|
||||||
"title": "入金",
|
"title": "入金",
|
||||||
|
|||||||
@ -17,7 +17,8 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"orderSuccess": "下單成功",
|
"orderSuccess": "下單成功",
|
||||||
"splitSuccess": "拆分成功",
|
"splitSuccess": "拆分成功",
|
||||||
"mergeSuccess": "合併成功"
|
"mergeSuccess": "合併成功",
|
||||||
|
"claimSuccess": "領取成功"
|
||||||
},
|
},
|
||||||
"trade": {
|
"trade": {
|
||||||
"buy": "買入",
|
"buy": "買入",
|
||||||
@ -163,7 +164,9 @@
|
|||||||
"expiration": "到期",
|
"expiration": "到期",
|
||||||
"activity": "活動",
|
"activity": "活動",
|
||||||
"view": "查看",
|
"view": "查看",
|
||||||
"expirationLabel": "到期"
|
"expirationLabel": "到期",
|
||||||
|
"youWon": "您贏得 ${amount}",
|
||||||
|
"claim": "領取"
|
||||||
},
|
},
|
||||||
"deposit": {
|
"deposit": {
|
||||||
"title": "入金",
|
"title": "入金",
|
||||||
|
|||||||
@ -62,6 +62,13 @@
|
|||||||
:key="pos.id"
|
:key="pos.id"
|
||||||
class="position-row-item"
|
class="position-row-item"
|
||||||
>
|
>
|
||||||
|
<div class="position-row-header">
|
||||||
|
<div class="position-row-icon" :class="pos.iconClass">
|
||||||
|
<img v-if="pos.imageUrl" :src="pos.imageUrl" alt="" class="position-row-icon-img" />
|
||||||
|
<span v-else class="position-row-icon-char">{{ pos.iconChar || '•' }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="position-row-title">{{ pos.market }}</span>
|
||||||
|
</div>
|
||||||
<div class="position-row-main">
|
<div class="position-row-main">
|
||||||
<span :class="['position-outcome-pill', pos.outcomePillClass]">{{ pos.outcomeTag }}</span>
|
<span :class="['position-outcome-pill', pos.outcomePillClass]">{{ pos.outcomeTag }}</span>
|
||||||
<span v-if="pos.locked" class="position-lock-badge">
|
<span v-if="pos.locked" class="position-lock-badge">
|
||||||
@ -355,6 +362,12 @@ import {
|
|||||||
type OpenOrderDisplayItem,
|
type OpenOrderDisplayItem,
|
||||||
} from '../api/order'
|
} from '../api/order'
|
||||||
import { cancelOrder as apiCancelOrder } from '../api/order'
|
import { cancelOrder as apiCancelOrder } from '../api/order'
|
||||||
|
import {
|
||||||
|
normalizeChartData,
|
||||||
|
fetchChartHistory,
|
||||||
|
type ChartDataPoint,
|
||||||
|
type ChartTimeRange,
|
||||||
|
} from '../api/chart'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
import {
|
import {
|
||||||
@ -1162,8 +1175,8 @@ const timeRanges = [
|
|||||||
{ label: 'ALL', value: 'ALL' },
|
{ label: 'ALL', value: 'ALL' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 时间轴数据 [timestamp, value][],按粒度生成
|
// 图表数据格式:[时间戳(ms), 概率(0-100)][]。接口约定见 src/api/chart.ts(ChartHistoryParams / ChartHistoryItem / normalizeChartData)
|
||||||
function generateData(range: string): [number, number][] {
|
function generateData(range: string): ChartDataPoint[] {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const data: [number, number][] = []
|
const data: [number, number][] = []
|
||||||
let stepMs: number
|
let stepMs: number
|
||||||
@ -1338,6 +1351,15 @@ function initChart() {
|
|||||||
chartInstance.setOption(buildOption(data.value, w))
|
chartInstance.setOption(buildOption(data.value, w))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 从接口拉取图表数据(接入时在 updateChartData 中调用并赋给 data.value) */
|
||||||
|
async function loadChartFromApi(marketId: string): Promise<ChartDataPoint[]> {
|
||||||
|
const res = await fetchChartHistory(
|
||||||
|
{ marketID: marketId, range: selectedTimeRange.value as ChartTimeRange }
|
||||||
|
)
|
||||||
|
return normalizeChartData(res.data ?? [])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用接口时:在 updateChartData 内先 await loadChartFromApi(marketId),再 setOption;暂无接口时用 generateData
|
||||||
function updateChartData() {
|
function updateChartData() {
|
||||||
data.value = generateData(selectedTimeRange.value)
|
data.value = generateData(selectedTimeRange.value)
|
||||||
const w = chartContainerRef.value?.clientWidth
|
const w = chartContainerRef.value?.clientWidth
|
||||||
@ -1967,6 +1989,54 @@ onUnmounted(() => {
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.position-row-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-row-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-row-icon.pill-yes {
|
||||||
|
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-row-icon.pill-down {
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-row-icon-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-row-icon-char {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-row-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 1.3;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.position-row-main,
|
.position-row-main,
|
||||||
.order-row-main {
|
.order-row-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -78,6 +78,30 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 未结算汇总:单条展示,多条用 +N,结算按钮不显示图标 -->
|
||||||
|
<v-row v-if="unsettledCount > 0" class="wallet-settlement-row">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card class="settlement-card" elevation="0" rounded="lg">
|
||||||
|
<div class="settlement-inner">
|
||||||
|
<div class="settlement-label">
|
||||||
|
{{ t('wallet.youWon', { amount: unsettledTotalText }) }}
|
||||||
|
<span v-if="unsettledCount > 1" class="settlement-plus-n">+{{ unsettledCount - 1 }}</span>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
class="settlement-claim-btn"
|
||||||
|
:loading="claimLoading"
|
||||||
|
:disabled="claimLoading"
|
||||||
|
@click="onClaimSettlement"
|
||||||
|
>
|
||||||
|
{{ t('wallet.claim') }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
<!-- 下方:Positions / Open orders / History -->
|
<!-- 下方:Positions / Open orders / History -->
|
||||||
<div class="wallet-section">
|
<div class="wallet-section">
|
||||||
<v-tabs v-model="activeTab" class="wallet-tabs" density="comfortable">
|
<v-tabs v-model="activeTab" class="wallet-tabs" density="comfortable">
|
||||||
@ -169,7 +193,9 @@
|
|||||||
>
|
>
|
||||||
<div class="position-mobile-row">
|
<div class="position-mobile-row">
|
||||||
<div class="position-icon" :class="pos.iconClass">
|
<div class="position-icon" :class="pos.iconClass">
|
||||||
<span class="position-icon-char">{{ pos.iconChar }}</span>
|
<img v-if="pos.imageUrl" :src="pos.imageUrl" alt="" class="position-icon-img" />
|
||||||
|
<v-icon v-else-if="pos.icon" size="20" class="position-icon-svg">{{ pos.icon }}</v-icon>
|
||||||
|
<span v-else class="position-icon-char">{{ pos.iconChar || '•' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="position-mobile-main">
|
<div class="position-mobile-main">
|
||||||
<div class="position-mobile-title">{{ pos.market }}</div>
|
<div class="position-mobile-title">{{ pos.market }}</div>
|
||||||
@ -269,10 +295,9 @@
|
|||||||
<td class="cell-market">
|
<td class="cell-market">
|
||||||
<div class="position-market-cell">
|
<div class="position-market-cell">
|
||||||
<div class="position-icon" :class="pos.iconClass">
|
<div class="position-icon" :class="pos.iconClass">
|
||||||
<v-icon v-if="pos.icon" size="20" class="position-icon-svg">{{
|
<img v-if="pos.imageUrl" :src="pos.imageUrl" alt="" class="position-icon-img" />
|
||||||
pos.icon
|
<v-icon v-else-if="pos.icon" size="20" class="position-icon-svg">{{ pos.icon }}</v-icon>
|
||||||
}}</v-icon>
|
<span v-else class="position-icon-char">{{ pos.iconChar || '•' }}</span>
|
||||||
<span v-else class="position-icon-char">{{ pos.iconChar }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="position-market-info">
|
<div class="position-market-info">
|
||||||
<span class="position-market-title">{{ pos.market }}</span>
|
<span class="position-market-title">{{ pos.market }}</span>
|
||||||
@ -587,7 +612,9 @@
|
|||||||
<v-card-text class="sell-dialog-body">
|
<v-card-text class="sell-dialog-body">
|
||||||
<div class="sell-dialog-icon-wrap">
|
<div class="sell-dialog-icon-wrap">
|
||||||
<div class="position-icon" :class="sellPositionItem.iconClass">
|
<div class="position-icon" :class="sellPositionItem.iconClass">
|
||||||
<span class="position-icon-char">{{ sellPositionItem.iconChar }}</span>
|
<img v-if="sellPositionItem.imageUrl" :src="sellPositionItem.imageUrl" alt="" class="position-icon-img" />
|
||||||
|
<v-icon v-else-if="sellPositionItem.icon" size="20" class="position-icon-svg">{{ sellPositionItem.icon }}</v-icon>
|
||||||
|
<span v-else class="position-icon-char">{{ sellPositionItem.iconChar || '•' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="sell-dialog-title">Sell {{ sellPositionItem.sellOutcome || 'Position' }}</h3>
|
<h3 class="sell-dialog-title">Sell {{ sellPositionItem.sellOutcome || 'Position' }}</h3>
|
||||||
@ -630,7 +657,7 @@ import { useLocaleStore } from '../stores/locale'
|
|||||||
import { useAuthError } from '../composables/useAuthError'
|
import { useAuthError } from '../composables/useAuthError'
|
||||||
import { cancelOrder as apiCancelOrder } from '../api/order'
|
import { cancelOrder as apiCancelOrder } from '../api/order'
|
||||||
import { getOrderList, mapOrderToHistoryItem, mapOrderToOpenOrderItem, OrderStatus } from '../api/order'
|
import { getOrderList, mapOrderToHistoryItem, mapOrderToOpenOrderItem, OrderStatus } from '../api/order'
|
||||||
import { getPositionList, mapPositionToDisplayItem } from '../api/position'
|
import { getPositionList, mapPositionToDisplayItem, claimPosition } from '../api/position'
|
||||||
import {
|
import {
|
||||||
MOCK_TOKEN_ID,
|
MOCK_TOKEN_ID,
|
||||||
MOCK_WALLET_POSITIONS,
|
MOCK_WALLET_POSITIONS,
|
||||||
@ -639,6 +666,7 @@ import {
|
|||||||
} from '../api/mockData'
|
} from '../api/mockData'
|
||||||
import { USE_MOCK_WALLET } from '../config/mock'
|
import { USE_MOCK_WALLET } from '../config/mock'
|
||||||
import { CrossChainUSDTAuth } from '../../sdk/approve'
|
import { CrossChainUSDTAuth } from '../../sdk/approve'
|
||||||
|
import { useToastStore } from '../stores/toast'
|
||||||
|
|
||||||
const { mobile } = useDisplay()
|
const { mobile } = useDisplay()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@ -655,6 +683,59 @@ const plTimeRanges = computed(() => [
|
|||||||
])
|
])
|
||||||
const activeTab = ref<'positions' | 'orders' | 'history'>('positions')
|
const activeTab = ref<'positions' | 'orders' | 'history'>('positions')
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
|
/** 当前展示的持仓列表(mock 或 API) */
|
||||||
|
const currentPositionList = computed(() =>
|
||||||
|
USE_MOCK_WALLET ? positions.value : positionList.value,
|
||||||
|
)
|
||||||
|
/** 未结算项:从持仓列表中筛出可领取的(有 marketID+tokenID;若后端有 needClaim 则仅 needClaim 为 true) */
|
||||||
|
const unsettledItems = computed(() => {
|
||||||
|
const list = currentPositionList.value
|
||||||
|
return list
|
||||||
|
.filter(
|
||||||
|
(p) =>
|
||||||
|
p.marketID &&
|
||||||
|
p.tokenID &&
|
||||||
|
(p.needClaim === undefined || p.needClaim === true),
|
||||||
|
)
|
||||||
|
.map((p) => {
|
||||||
|
const amount = parseFloat(String(p.value).replace(/[^0-9.-]/g, '')) || 0
|
||||||
|
return { marketID: p.marketID!, tokenID: p.tokenID!, amount }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const unsettledCount = computed(() => unsettledItems.value.length)
|
||||||
|
const unsettledTotalText = computed(() => {
|
||||||
|
const sum = unsettledItems.value.reduce((a, b) => a + b.amount, 0)
|
||||||
|
return sum.toFixed(2)
|
||||||
|
})
|
||||||
|
const claimLoading = ref(false)
|
||||||
|
const toastStore = useToastStore()
|
||||||
|
async function onClaimSettlement() {
|
||||||
|
const items = unsettledItems.value
|
||||||
|
if (items.length === 0) return
|
||||||
|
const headers = userStore.getAuthHeaders()
|
||||||
|
if (!headers) {
|
||||||
|
toastStore.show(t('trade.pleaseLogin'), 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
claimLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await claimPosition(
|
||||||
|
{ marketID: items.map((i) => i.marketID), tokenID: items.map((i) => i.tokenID) },
|
||||||
|
{ headers },
|
||||||
|
)
|
||||||
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
toastStore.show(t('toast.claimSuccess'))
|
||||||
|
userStore.fetchUsdcBalance()
|
||||||
|
if (activeTab.value === 'positions') loadPositionList()
|
||||||
|
} else {
|
||||||
|
toastStore.show(res.msg || t('error.requestFailed'), 'error')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toastStore.show(formatAuthError(e, t('error.requestFailed')), 'error')
|
||||||
|
} finally {
|
||||||
|
claimLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
const depositDialogOpen = ref(false)
|
const depositDialogOpen = ref(false)
|
||||||
const withdrawDialogOpen = ref(false)
|
const withdrawDialogOpen = ref(false)
|
||||||
const authorizeDialogOpen = ref(false)
|
const authorizeDialogOpen = ref(false)
|
||||||
@ -679,6 +760,7 @@ interface Position {
|
|||||||
icon?: string
|
icon?: string
|
||||||
iconChar?: string
|
iconChar?: string
|
||||||
iconClass?: string
|
iconClass?: string
|
||||||
|
imageUrl?: string
|
||||||
outcomeTag?: string
|
outcomeTag?: string
|
||||||
outcomePillClass?: string
|
outcomePillClass?: string
|
||||||
shares: string
|
shares: string
|
||||||
@ -693,6 +775,12 @@ interface Position {
|
|||||||
sellOutcome?: string
|
sellOutcome?: string
|
||||||
/** 移动端副标题 "on Up/Down to win" 中的词 */
|
/** 移动端副标题 "on Up/Down to win" 中的词 */
|
||||||
outcomeWord?: string
|
outcomeWord?: string
|
||||||
|
/** 市场 ID(从持仓列表来,用于领取结算) */
|
||||||
|
marketID?: string
|
||||||
|
/** Token ID(从持仓列表来,用于领取结算) */
|
||||||
|
tokenID?: string
|
||||||
|
/** 是否待领取/未结算(后端可选,无则按有 marketID+tokenID 视为可领取) */
|
||||||
|
needClaim?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从 avgNow "72¢ → 0.5¢" 解析出 [avg, now] */
|
/** 从 avgNow "72¢ → 0.5¢" 解析出 [avg, now] */
|
||||||
@ -1332,6 +1420,47 @@ async function submitAuthorize() {
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 未结算汇总:单条 item,多条 +N,领取按钮无图标 */
|
||||||
|
.wallet-settlement-row {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-card {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-label {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-plus-n {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 6px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #6b7280;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-claim-btn {
|
||||||
|
text-transform: none;
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
.wallet-section {
|
.wallet-section {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
@ -1430,6 +1559,13 @@ async function submitAuthorize() {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.position-icon-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.position-market-info {
|
.position-market-info {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user