新增:拆分订单和合并订单接口对接
This commit is contained in:
parent
e14bd3bc23
commit
297d2d1c56
47
.cursor/skills/deploy/SKILL.md
Normal file
47
.cursor/skills/deploy/SKILL.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
name: deploy
|
||||||
|
description: 将 PolyClientVuetify 项目打包并通过 SSH 部署到远程服务器。用户说「部署」「发布」时使用此 skill。
|
||||||
|
---
|
||||||
|
|
||||||
|
# 项目打包并部署
|
||||||
|
|
||||||
|
将 PolyClientVuetify 项目构建后通过 SSH rsync 部署到 `root@38.246.250.238:/opt/1panel/www/sites/pm.xtrader.vip/index`。
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
在项目根目录执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
1. **打包**:执行 `npm run build`,生成 `dist/` 目录
|
||||||
|
- 生产构建自动使用 `.env.production`,API 地址为 `https://api.xtrader.vip`
|
||||||
|
2. **SSH 部署**:使用 rsync 将 `dist/` 同步到远程目录
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
- 本机已配置 SSH 免密登录 `root@38.246.250.238`,或可交互输入密码
|
||||||
|
- 远程目录 `/opt/1panel/www/sites/pm.xtrader.vip/index` 需存在且有写权限
|
||||||
|
|
||||||
|
## 环境变量(可选)
|
||||||
|
|
||||||
|
在 `.env` 或 `.env.local` 中配置,不配置时使用默认值:
|
||||||
|
|
||||||
|
| 变量 | 默认值 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `DEPLOY_HOST` | 38.246.250.238 | 部署目标主机 |
|
||||||
|
| `DEPLOY_USER` | root | SSH 用户 |
|
||||||
|
| `DEPLOY_PATH` | /opt/1panel/www/sites/pm.xtrader.vip/index | 远程目录 |
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
- **部署脚本**:`scripts/deploy.mjs`
|
||||||
|
- **构建输出**:`dist/`(由 Vite 生成)
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
- **Permission denied**:确认 SSH 密钥已添加到远程服务器(`ssh-copy-id root@38.246.250.238`)
|
||||||
|
- **目录不存在**:在远程服务器执行 `mkdir -p /opt/1panel/www/sites/pm.xtrader.vip/index`
|
||||||
5
.env
5
.env
@ -1,3 +1,8 @@
|
|||||||
# API 基础地址,不设置时默认 https://api.xtrader.vip
|
# API 基础地址,不设置时默认 https://api.xtrader.vip
|
||||||
# 连接测试服务器 192.168.3.21:8888 时复制本文件为 .env 或 .env.local 并取消下一行注释:
|
# 连接测试服务器 192.168.3.21:8888 时复制本文件为 .env 或 .env.local 并取消下一行注释:
|
||||||
VITE_API_BASE_URL=http://192.168.3.21:8888
|
VITE_API_BASE_URL=http://192.168.3.21:8888
|
||||||
|
|
||||||
|
# SSH 部署(npm run deploy),可选覆盖
|
||||||
|
# DEPLOY_HOST=38.246.250.238
|
||||||
|
# DEPLOY_USER=root
|
||||||
|
# DEPLOY_PATH=/opt/1panel/www/sites/pm.xtrader.vip/index
|
||||||
|
|||||||
12
.env.example
12
.env.example
@ -1,3 +1,11 @@
|
|||||||
# API 基础地址,不设置时默认 https://api.xtrader.vip
|
# API 基础地址(开发环境 npm run dev)
|
||||||
# 连接测试服务器 192.168.3.21:8888 时复制本文件为 .env 或 .env.local 并取消下一行注释:
|
# 不设置时默认 https://api.xtrader.vip
|
||||||
|
# 连接测试服务器时复制本文件为 .env 并取消下一行注释:
|
||||||
# VITE_API_BASE_URL=http://192.168.3.21:8888
|
# VITE_API_BASE_URL=http://192.168.3.21:8888
|
||||||
|
#
|
||||||
|
# 生产打包/部署时自动使用 .env.production 中的 https://api.xtrader.vip
|
||||||
|
|
||||||
|
# SSH 部署(npm run deploy),不配置时使用默认值
|
||||||
|
# DEPLOY_HOST=38.246.250.238
|
||||||
|
# DEPLOY_USER=root
|
||||||
|
# DEPLOY_PATH=/opt/1panel/www/sites/pm.xtrader.vip/index
|
||||||
|
|||||||
2
.env.production
Normal file
2
.env.production
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# 生产环境 API 地址(npm run build / npm run deploy 时自动使用)
|
||||||
|
VITE_API_BASE_URL=https://api.xtrader.vip
|
||||||
@ -136,6 +136,12 @@ Usage notes:
|
|||||||
<location>project</location>
|
<location>project</location>
|
||||||
</skill>
|
</skill>
|
||||||
|
|
||||||
|
<skill>
|
||||||
|
<name>deploy</name>
|
||||||
|
<description>将 PolyClientVuetify 项目打包并通过 SSH 部署到远程服务器。用户说「部署」「发布」时使用此 skill。</description>
|
||||||
|
<location>project</location>
|
||||||
|
</skill>
|
||||||
|
|
||||||
</available_skills>
|
</available_skills>
|
||||||
<!-- SKILLS_TABLE_END -->
|
<!-- SKILLS_TABLE_END -->
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"deploy": "node --env-file=.env --env-file=.env.production scripts/deploy.mjs",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:unit": "vitest",
|
"test:unit": "vitest",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
|
|||||||
75
scripts/deploy.mjs
Normal file
75
scripts/deploy.mjs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* 将项目打包并部署到远程服务器
|
||||||
|
* 使用方式:npm run deploy
|
||||||
|
* 通过 SSH rsync 部署到 root@38.246.250.238:/opt/1panel/www/sites/pm.xtrader.vip/index
|
||||||
|
*
|
||||||
|
* 环境变量(可选):DEPLOY_HOST、DEPLOY_USER、DEPLOY_PATH
|
||||||
|
* 依赖:rsync(系统自带)、ssh 免密或密钥
|
||||||
|
*/
|
||||||
|
import { execSync, spawnSync } from 'child_process'
|
||||||
|
import { dirname, join } from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const projectRoot = join(__dirname, '..')
|
||||||
|
const distDir = join(projectRoot, 'dist')
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
host: process.env.DEPLOY_HOST || '38.246.250.238',
|
||||||
|
user: process.env.DEPLOY_USER || 'root',
|
||||||
|
path: process.env.DEPLOY_PATH || '/opt/1panel/www/sites/pm.xtrader.vip/index',
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = `${config.user}@${config.host}:${config.path}`
|
||||||
|
|
||||||
|
function build() {
|
||||||
|
const apiUrl = process.env.VITE_API_BASE_URL || 'https://api.xtrader.vip'
|
||||||
|
console.log(`📦 正在打包项目(.env.production API: ${apiUrl})...`)
|
||||||
|
execSync('npm run build', {
|
||||||
|
cwd: projectRoot,
|
||||||
|
stdio: 'inherit',
|
||||||
|
// 继承 process.env(已由 --env-file=.env.production 加载生产配置)
|
||||||
|
env: process.env,
|
||||||
|
})
|
||||||
|
console.log('✅ 打包完成\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function deploy() {
|
||||||
|
const rsyncCheck = spawnSync('which', ['rsync'], { encoding: 'utf8' })
|
||||||
|
if (rsyncCheck.status !== 0) {
|
||||||
|
console.error('❌ 未找到 rsync')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔌 正在通过 SSH 部署到 ${target} ...`)
|
||||||
|
const result = spawnSync(
|
||||||
|
'rsync',
|
||||||
|
[
|
||||||
|
'-avz',
|
||||||
|
'--delete',
|
||||||
|
'--exclude=.DS_Store',
|
||||||
|
`${distDir}/`,
|
||||||
|
`${target}/`,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
cwd: projectRoot,
|
||||||
|
stdio: 'inherit',
|
||||||
|
encoding: 'utf8',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
console.error('❌ 部署失败')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n🎉 部署成功!')
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
build()
|
||||||
|
deploy()
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useUserStore } from './stores/user'
|
import { useUserStore } from './stores/user'
|
||||||
|
|
||||||
@ -9,6 +9,12 @@ const userStore = useUserStore()
|
|||||||
const currentRoute = computed(() => {
|
const currentRoute = computed(() => {
|
||||||
return route.path
|
return route.path
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (userStore.isLoggedIn) {
|
||||||
|
userStore.fetchUsdcBalance()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@ -55,18 +55,30 @@ export interface PmEventListItem {
|
|||||||
* - outcomePrices: 各选项价格(首项为第一选项概率,如 Yes/Up)
|
* - outcomePrices: 各选项价格(首项为第一选项概率,如 Yes/Up)
|
||||||
*/
|
*/
|
||||||
export interface PmEventMarketItem {
|
export interface PmEventMarketItem {
|
||||||
|
/** 市场 ID(部分接口返回大写 ID) */
|
||||||
ID?: number
|
ID?: number
|
||||||
|
/** 市场 ID(部分接口或 JSON 序列化为小写 id) */
|
||||||
|
id?: number
|
||||||
question?: string
|
question?: string
|
||||||
slug?: string
|
slug?: string
|
||||||
/** 选项展示文案,与 outcomePrices 顺序一致 */
|
/** 选项展示文案,与 outcomePrices 顺序一致 */
|
||||||
outcomes?: string[]
|
outcomes?: string[]
|
||||||
/** 各选项价格,outcomes[0] 对应 outcomePrices[0] */
|
/** 各选项价格,outcomes[0] 对应 outcomePrices[0] */
|
||||||
outcomePrices?: string[] | number[]
|
outcomePrices?: string[] | number[]
|
||||||
|
/** 市场对应的 clob token id 与 outcomePrices 和outcomes顺序一致 */
|
||||||
|
clobTokenIds:string[]
|
||||||
endDate?: string
|
endDate?: string
|
||||||
volume?: number
|
volume?: number
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 从市场项取 marketId,兼容 ID / id */
|
||||||
|
export function getMarketId(m: PmEventMarketItem | null | undefined): string | undefined {
|
||||||
|
if (!m) return undefined
|
||||||
|
const raw = m.ID ?? m.id
|
||||||
|
return raw != null ? String(raw) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
/** 对应 definitions polymarket.PmSeries 常用字段 */
|
/** 对应 definitions polymarket.PmSeries 常用字段 */
|
||||||
export interface PmEventSeriesItem {
|
export interface PmEventSeriesItem {
|
||||||
ID?: number
|
ID?: number
|
||||||
@ -128,20 +140,39 @@ export interface PmEventDetailResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用 id 查询 Event 详情
|
* findPmEvent 请求参数(Query)
|
||||||
|
* doc.json: ID 与 slug 支持同时传入,至少传一个
|
||||||
|
*/
|
||||||
|
export interface FindPmEventParams {
|
||||||
|
/** Event 主键(数字 ID) */
|
||||||
|
id?: number
|
||||||
|
/** Event 的 slug 标识 */
|
||||||
|
slug?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用 id 和/或 slug 查询 Event 详情
|
||||||
* GET /PmEvent/findPmEvent
|
* GET /PmEvent/findPmEvent
|
||||||
*
|
*
|
||||||
* 请求参数(Query):
|
* 请求参数(Query):
|
||||||
* - ID: number,必填,Event 主键
|
* - ID: number,可选
|
||||||
* 鉴权:需在 headers 中传 x-token
|
* - slug: string,可选
|
||||||
|
* - ID 与 slug 至少传一个,可同时传
|
||||||
|
* 鉴权:需在 headers 中传 x-token、x-user-id
|
||||||
*
|
*
|
||||||
* 响应(200):PmEventDetailResponse { code, data: PmEventListItem, msg }
|
* 响应(200):PmEventDetailResponse { code, data: PmEventListItem, msg }
|
||||||
*/
|
*/
|
||||||
export async function findPmEvent(
|
export async function findPmEvent(
|
||||||
id: number,
|
params: FindPmEventParams,
|
||||||
config?: { headers?: Record<string, string> }
|
config?: { headers?: Record<string, string> }
|
||||||
): Promise<PmEventDetailResponse> {
|
): Promise<PmEventDetailResponse> {
|
||||||
return get<PmEventDetailResponse>('/PmEvent/findPmEvent', { ID: id }, config)
|
const query: Record<string, string | number> = {}
|
||||||
|
if (params.id != null) query.ID = params.id
|
||||||
|
if (params.slug != null && params.slug !== '') query.slug = params.slug
|
||||||
|
if (Object.keys(query).length === 0) {
|
||||||
|
throw new Error('findPmEvent: 至少需要传 id 或 slug')
|
||||||
|
}
|
||||||
|
return get<PmEventDetailResponse>('/PmEvent/findPmEvent', query, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 多选项卡片中单个选项(用于左右滑动切换) */
|
/** 多选项卡片中单个选项(用于左右滑动切换) */
|
||||||
@ -163,6 +194,8 @@ export interface EventCardOutcome {
|
|||||||
*/
|
*/
|
||||||
export interface EventCardItem {
|
export interface EventCardItem {
|
||||||
id: string
|
id: string
|
||||||
|
/** Event slug,用于 findPmEvent 传参 */
|
||||||
|
slug?: string
|
||||||
marketTitle: string
|
marketTitle: string
|
||||||
chanceValue: number
|
chanceValue: number
|
||||||
marketInfo: string
|
marketInfo: string
|
||||||
@ -178,6 +211,8 @@ export interface EventCardItem {
|
|||||||
noLabel?: string
|
noLabel?: string
|
||||||
/** 是否显示 NEW 角标 */
|
/** 是否显示 NEW 角标 */
|
||||||
isNew?: boolean
|
isNew?: boolean
|
||||||
|
/** 当前市场 ID(单 market 时为第一个 market 的 ID,供交易/Split 使用) */
|
||||||
|
marketId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 内存缓存:列表数据,切换页面时复用,下拉刷新时清空 */
|
/** 内存缓存:列表数据,切换页面时复用,下拉刷新时清空 */
|
||||||
@ -262,12 +297,13 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
|||||||
chanceValue: marketChance(m),
|
chanceValue: marketChance(m),
|
||||||
yesLabel: m.outcomes?.[0] ?? 'Yes',
|
yesLabel: m.outcomes?.[0] ?? 'Yes',
|
||||||
noLabel: m.outcomes?.[1] ?? 'No',
|
noLabel: m.outcomes?.[1] ?? 'No',
|
||||||
marketId: m.ID != null ? String(m.ID) : undefined,
|
marketId: getMarketId(m),
|
||||||
}))
|
}))
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
slug: item.slug ?? undefined,
|
||||||
marketTitle,
|
marketTitle,
|
||||||
chanceValue,
|
chanceValue,
|
||||||
marketInfo,
|
marketInfo,
|
||||||
@ -279,5 +315,6 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
|||||||
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,
|
||||||
|
marketId: getMarketId(firstMarket),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,19 +8,41 @@ export interface ApiResponse<T = unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split 请求体(测试服务器 192.168.3.21:8888 的 /PmMarket/split)
|
* Split 请求体(/PmMarket/split)
|
||||||
* 用 USDC 兑换该市场的 Yes+No 份额(1 USDC ≈ 1 Yes + 1 No)
|
* 用 USDC 兑换该市场的 Yes+No 份额(1 USDC ≈ 1 Yes + 1 No)
|
||||||
*/
|
*/
|
||||||
export interface PmMarketSplitRequest {
|
export interface PmMarketSplitRequest {
|
||||||
/** 市场 ID */
|
/** 市场 ID */
|
||||||
marketId: string
|
marketID: string
|
||||||
/** 要 split 的 USDC 金额 */
|
/** 要 split 的 USDC 金额(字符串) */
|
||||||
amount: number
|
usdcAmount: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge 请求体(/PmMarket/merge)
|
||||||
|
* 合并 Yes + No 份额得到 USDC(1 Yes + 1 No ≈ 1 USDC)
|
||||||
|
*/
|
||||||
|
export interface PmMarketMergeRequest {
|
||||||
|
/** 市场 ID */
|
||||||
|
marketID: string
|
||||||
|
/** 合并份额数量(字符串) */
|
||||||
|
amount: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /PmMarket/merge
|
||||||
|
* 合并 Yes、No 份额为 USDC,需鉴权
|
||||||
|
*/
|
||||||
|
export async function pmMarketMerge(
|
||||||
|
data: PmMarketMergeRequest,
|
||||||
|
config?: { headers?: Record<string, string> }
|
||||||
|
): Promise<ApiResponse> {
|
||||||
|
return post<ApiResponse>('/PmMarket/merge', data, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 调用测试服务器 /PmMarket/split 接口
|
* 调用测试服务器 /PmMarket/split 接口
|
||||||
* - 需鉴权:请求头 x-token
|
* - 需鉴权:请求头 x-token、x-user-id
|
||||||
* - 使用 VITE_API_BASE_URL 时可设为 http://192.168.3.21:8888 连接测试服
|
* - 使用 VITE_API_BASE_URL 时可设为 http://192.168.3.21:8888 连接测试服
|
||||||
*/
|
*/
|
||||||
export async function pmMarketSplit(
|
export async function pmMarketSplit(
|
||||||
|
|||||||
@ -6,7 +6,7 @@ const BASE_URL = typeof import.meta !== 'undefined' && (import.meta as unknown a
|
|||||||
: 'https://api.xtrader.vip'
|
: 'https://api.xtrader.vip'
|
||||||
|
|
||||||
export interface RequestConfig {
|
export interface RequestConfig {
|
||||||
/** 请求头,如 { 'x-token': token } */
|
/** 请求头,如 { 'x-token': token, 'x-user-id': userId } */
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
34
src/api/user.ts
Normal file
34
src/api/user.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { get } from './request'
|
||||||
|
|
||||||
|
const USDC_DECIMALS = 1_000_000
|
||||||
|
|
||||||
|
/** getUsdcBalance 返回的 data 结构 */
|
||||||
|
export interface UsdcBalanceData {
|
||||||
|
amount: string
|
||||||
|
available: string
|
||||||
|
locked: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetUsdcBalanceResponse {
|
||||||
|
code: number
|
||||||
|
data?: UsdcBalanceData
|
||||||
|
msg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将接口返回的原始数值(需除以 1000000)转为显示用字符串 */
|
||||||
|
export function formatUsdcBalance(raw: string): string {
|
||||||
|
const n = Number(raw) / USDC_DECIMALS
|
||||||
|
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /user/getUsdcBalance
|
||||||
|
* 查询 USDC 余额,需鉴权(x-token)
|
||||||
|
* amount、available 需除以 1000000 得到实际 USDC
|
||||||
|
*/
|
||||||
|
export async function getUsdcBalance(authHeaders: Record<string, string>): Promise<GetUsdcBalanceResponse> {
|
||||||
|
const res = await get<GetUsdcBalanceResponse>('/user/getUsdcBalance', undefined, {
|
||||||
|
headers: authHeaders,
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
@ -181,6 +181,8 @@ const props = withDefaults(
|
|||||||
chanceValue: number
|
chanceValue: number
|
||||||
marketInfo: string
|
marketInfo: string
|
||||||
id: string
|
id: string
|
||||||
|
/** Event slug,用于 findPmEvent 传参 */
|
||||||
|
slug?: string
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
category?: string
|
category?: string
|
||||||
expiresAt?: string
|
expiresAt?: string
|
||||||
@ -194,6 +196,8 @@ const props = withDefaults(
|
|||||||
noLabel?: string
|
noLabel?: string
|
||||||
/** 是否显示 + NEW */
|
/** 是否显示 + NEW */
|
||||||
isNew?: boolean
|
isNew?: boolean
|
||||||
|
/** 当前市场 ID(单 market 时供交易/Split 使用) */
|
||||||
|
marketId?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
marketTitle: 'Mamdan opens city-owned grocery store b...',
|
marketTitle: 'Mamdan opens city-owned grocery store b...',
|
||||||
@ -208,6 +212,7 @@ const props = withDefaults(
|
|||||||
yesLabel: 'Yes',
|
yesLabel: 'Yes',
|
||||||
noLabel: 'No',
|
noLabel: 'No',
|
||||||
isNew: false,
|
isNew: false,
|
||||||
|
marketId: undefined,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -262,7 +267,10 @@ const semiProgressColor = computed(() => {
|
|||||||
|
|
||||||
const navigateToDetail = () => {
|
const navigateToDetail = () => {
|
||||||
if (props.displayType === 'multi' && (props.outcomes?.length ?? 0) > 1) {
|
if (props.displayType === 'multi' && (props.outcomes?.length ?? 0) > 1) {
|
||||||
router.push({ path: `/event/${props.id}/markets` })
|
router.push({
|
||||||
|
path: `/event/${props.id}/markets`,
|
||||||
|
query: { ...(props.slug && { slug: props.slug }) },
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
router.push({
|
router.push({
|
||||||
@ -274,12 +282,14 @@ const navigateToDetail = () => {
|
|||||||
marketInfo: props.marketInfo || undefined,
|
marketInfo: props.marketInfo || undefined,
|
||||||
expiresAt: props.expiresAt || undefined,
|
expiresAt: props.expiresAt || undefined,
|
||||||
chance: String(props.chanceValue),
|
chance: String(props.chanceValue),
|
||||||
|
...(props.marketId && { marketId: props.marketId }),
|
||||||
|
...(props.slug && { slug: props.slug }),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTradeSingle(side: 'yes' | 'no') {
|
function openTradeSingle(side: 'yes' | 'no') {
|
||||||
emit('openTrade', side, { id: props.id, title: props.marketTitle })
|
emit('openTrade', side, { id: props.id, title: props.marketTitle, marketId: props.marketId })
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
|
function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
|
||||||
|
|||||||
@ -579,9 +579,19 @@
|
|||||||
Available shares: {{ availableMergeShares }}
|
Available shares: {{ availableMergeShares }}
|
||||||
<button type="button" class="merge-max-link" @click="setMergeMax">Max</button>
|
<button type="button" class="merge-max-link" @click="setMergeMax">Max</button>
|
||||||
</p>
|
</p>
|
||||||
|
<p v-if="!props.market?.marketId" class="merge-no-market">Please select a market first (e.g. click Buy Yes/No on a market).</p>
|
||||||
|
<p v-if="mergeError" class="merge-error">{{ mergeError }}</p>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions class="merge-dialog-actions">
|
<v-card-actions class="merge-dialog-actions">
|
||||||
<v-btn color="primary" variant="flat" block class="merge-submit-btn" @click="submitMerge">
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
block
|
||||||
|
class="merge-submit-btn"
|
||||||
|
:loading="mergeLoading"
|
||||||
|
:disabled="mergeLoading || mergeAmount <= 0"
|
||||||
|
@click="submitMerge"
|
||||||
|
>
|
||||||
Merge Shares
|
Merge Shares
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
@ -624,7 +634,7 @@
|
|||||||
block
|
block
|
||||||
class="split-submit-btn"
|
class="split-submit-btn"
|
||||||
:loading="splitLoading"
|
:loading="splitLoading"
|
||||||
:disabled="splitLoading || !props.market?.marketId || splitAmount <= 0"
|
:disabled="splitLoading || splitAmount <= 0"
|
||||||
@click="submitSplit"
|
@click="submitSplit"
|
||||||
>
|
>
|
||||||
Split
|
Split
|
||||||
@ -638,7 +648,7 @@
|
|||||||
import { ref, computed, watch, onMounted } from 'vue'
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
import { pmMarketSplit } from '../api/market'
|
import { pmMarketMerge, pmMarketSplit } from '../api/market'
|
||||||
|
|
||||||
const { mobile } = useDisplay()
|
const { mobile } = useDisplay()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@ -663,19 +673,42 @@ const props = withDefaults(
|
|||||||
// 移动端:底部栏与弹出层
|
// 移动端:底部栏与弹出层
|
||||||
const sheetOpen = ref(false)
|
const sheetOpen = ref(false)
|
||||||
|
|
||||||
// Merge shares dialog
|
// Merge shares dialog:对接 /PmMarket/merge
|
||||||
const mergeDialogOpen = ref(false)
|
const mergeDialogOpen = ref(false)
|
||||||
const mergeAmount = ref(0)
|
const mergeAmount = ref(0)
|
||||||
const availableMergeShares = ref(0)
|
const availableMergeShares = ref(0)
|
||||||
|
const mergeLoading = ref(false)
|
||||||
|
const mergeError = ref('')
|
||||||
function openMergeDialog() {
|
function openMergeDialog() {
|
||||||
mergeAmount.value = 0
|
mergeAmount.value = 0
|
||||||
|
mergeError.value = ''
|
||||||
mergeDialogOpen.value = true
|
mergeDialogOpen.value = true
|
||||||
}
|
}
|
||||||
function setMergeMax() {
|
function setMergeMax() {
|
||||||
mergeAmount.value = availableMergeShares.value
|
mergeAmount.value = availableMergeShares.value
|
||||||
}
|
}
|
||||||
function submitMerge() {
|
async function submitMerge() {
|
||||||
mergeDialogOpen.value = false
|
const marketId = props.market?.marketId
|
||||||
|
if (mergeAmount.value <= 0) return
|
||||||
|
if (!marketId) return
|
||||||
|
mergeLoading.value = true
|
||||||
|
mergeError.value = ''
|
||||||
|
try {
|
||||||
|
const res = await pmMarketMerge(
|
||||||
|
{ marketID: marketId, amount: String(mergeAmount.value) },
|
||||||
|
{ headers: userStore.getAuthHeaders() }
|
||||||
|
)
|
||||||
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
mergeDialogOpen.value = false
|
||||||
|
userStore.fetchUsdcBalance()
|
||||||
|
} else {
|
||||||
|
mergeError.value = res.msg || 'Merge failed'
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
mergeError.value = e instanceof Error ? e.message : 'Request failed'
|
||||||
|
} finally {
|
||||||
|
mergeLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split dialog:对接测试服务器 /PmMarket/split
|
// Split dialog:对接测试服务器 /PmMarket/split
|
||||||
@ -684,19 +717,20 @@ const splitAmount = ref(0)
|
|||||||
const splitLoading = ref(false)
|
const splitLoading = ref(false)
|
||||||
const splitError = ref('')
|
const splitError = ref('')
|
||||||
function openSplitDialog() {
|
function openSplitDialog() {
|
||||||
splitAmount.value = 0
|
splitAmount.value = splitAmount.value > 0 ? splitAmount.value : 1
|
||||||
splitError.value = ''
|
splitError.value = ''
|
||||||
splitDialogOpen.value = true
|
splitDialogOpen.value = true
|
||||||
}
|
}
|
||||||
async function submitSplit() {
|
async function submitSplit() {
|
||||||
const marketId = props.market?.marketId
|
const marketId = props.market?.marketId
|
||||||
if (!marketId || splitAmount.value <= 0) return
|
if (splitAmount.value <= 0) return
|
||||||
|
if (!marketId) return // 无市场时仅依赖模板中的 split-no-market 提示,不重复设置 splitError
|
||||||
splitLoading.value = true
|
splitLoading.value = true
|
||||||
splitError.value = ''
|
splitError.value = ''
|
||||||
try {
|
try {
|
||||||
const res = await pmMarketSplit(
|
const res = await pmMarketSplit(
|
||||||
{ marketId, amount: splitAmount.value },
|
{ marketID: marketId, usdcAmount: String(splitAmount.value) },
|
||||||
{ headers: userStore.token ? { 'x-token': userStore.token } : undefined }
|
{ headers: userStore.getAuthHeaders() }
|
||||||
)
|
)
|
||||||
if (res.code === 0 || res.code === 200) {
|
if (res.code === 0 || res.code === 200) {
|
||||||
splitDialogOpen.value = false
|
splitDialogOpen.value = false
|
||||||
@ -766,20 +800,29 @@ const actionButtonText = computed(() => {
|
|||||||
|
|
||||||
function applyInitialOption(option: 'yes' | 'no') {
|
function applyInitialOption(option: 'yes' | 'no') {
|
||||||
selectedOption.value = option
|
selectedOption.value = option
|
||||||
|
syncLimitPriceFromMarket()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据当前 props.market 与 selectedOption 同步 limitPrice(组件显示或 market 更新时调用) */
|
||||||
|
function syncLimitPriceFromMarket() {
|
||||||
const yesP = props.market?.yesPrice ?? 0.19
|
const yesP = props.market?.yesPrice ?? 0.19
|
||||||
const noP = props.market?.noPrice ?? 0.82
|
const noP = props.market?.noPrice ?? 0.82
|
||||||
limitPrice.value = option === 'yes' ? yesP : noP
|
limitPrice.value = selectedOption.value === 'yes' ? yesP : noP
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.initialOption) applyInitialOption(props.initialOption)
|
if (props.initialOption) applyInitialOption(props.initialOption)
|
||||||
|
else if (props.market) syncLimitPriceFromMarket()
|
||||||
})
|
})
|
||||||
watch(() => props.initialOption, (option) => {
|
watch(() => props.initialOption, (option) => {
|
||||||
if (option) applyInitialOption(option)
|
if (option) applyInitialOption(option)
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
watch(() => props.market, (m) => {
|
watch(() => props.market, (m) => {
|
||||||
if (m && props.initialOption) applyInitialOption(props.initialOption)
|
if (m) {
|
||||||
|
if (props.initialOption) applyInitialOption(props.initialOption)
|
||||||
|
else syncLimitPriceFromMarket()
|
||||||
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
@ -1366,6 +1409,20 @@ function submitOrder() {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.merge-no-market,
|
||||||
|
.merge-error {
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merge-no-market {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.merge-error {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
.merge-dialog-actions {
|
.merge-dialog-actions {
|
||||||
padding: 16px 20px 20px;
|
padding: 16px 20px 20px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { getUsdcBalance, formatUsdcBalance } from '@/api/user'
|
||||||
|
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
|
/** 用户 ID(API 可能返回 id 或 ID) */
|
||||||
id?: number | string
|
id?: number | string
|
||||||
|
ID?: number
|
||||||
headerImg?: string
|
headerImg?: string
|
||||||
nickName?: string
|
nickName?: string
|
||||||
userName?: string
|
userName?: string
|
||||||
@ -62,5 +65,29 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
clearStorage()
|
clearStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
return { token, user, isLoggedIn, avatarUrl, balance, setUser, logout }
|
/** 鉴权请求头:x-token 与 x-user-id,未登录时返回 undefined */
|
||||||
|
function getAuthHeaders(): Record<string, string> | undefined {
|
||||||
|
if (!token.value || !user.value) return undefined
|
||||||
|
const uid = user.value.id ?? user.value.ID
|
||||||
|
return {
|
||||||
|
'x-token': token.value,
|
||||||
|
...(uid != null && uid !== '' && { 'x-user-id': String(uid) }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 请求 USDC 余额(需已登录),amount/available 除以 1000000 后更新余额显示 */
|
||||||
|
async function fetchUsdcBalance() {
|
||||||
|
const headers = getAuthHeaders()
|
||||||
|
if (!headers) return
|
||||||
|
try {
|
||||||
|
const res = await getUsdcBalance(headers)
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
balance.value = formatUsdcBalance(res.data.available)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[fetchUsdcBalance] 请求失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { token, user, isLoggedIn, avatarUrl, balance, setUser, logout, getAuthHeaders, fetchUsdcBalance }
|
||||||
})
|
})
|
||||||
|
|||||||
@ -123,7 +123,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
import type { ECharts } from 'echarts'
|
import type { ECharts } from 'echarts'
|
||||||
import TradeComponent from '../components/TradeComponent.vue'
|
import TradeComponent from '../components/TradeComponent.vue'
|
||||||
import { findPmEvent, type PmEventListItem, type PmEventMarketItem } from '../api/event'
|
import { findPmEvent, getMarketId, type FindPmEventParams, type PmEventListItem, type PmEventMarketItem } from '../api/event'
|
||||||
import { MOCK_EVENT_LIST } from '../api/mockEventList'
|
import { MOCK_EVENT_LIST } from '../api/mockEventList'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ const tradeMarketPayload = computed(() => {
|
|||||||
const yesPrice = yesRaw != null && Number.isFinite(Number(yesRaw)) ? Number(yesRaw) : 0.5
|
const yesPrice = yesRaw != null && Number.isFinite(Number(yesRaw)) ? Number(yesRaw) : 0.5
|
||||||
const noPrice = noRaw != null && Number.isFinite(Number(noRaw)) ? Number(noRaw) : 0.5
|
const noPrice = noRaw != null && Number.isFinite(Number(noRaw)) ? Number(noRaw) : 0.5
|
||||||
return {
|
return {
|
||||||
marketId: m.ID != null ? String(m.ID) : undefined,
|
marketId: getMarketId(m),
|
||||||
yesPrice,
|
yesPrice,
|
||||||
noPrice,
|
noPrice,
|
||||||
title: m.question,
|
title: m.question,
|
||||||
@ -472,29 +472,38 @@ function goToTradeDetail(market: PmEventMarketItem, side?: 'yes' | 'no') {
|
|||||||
marketInfo: formatVolume(market.volume),
|
marketInfo: formatVolume(market.volume),
|
||||||
chance: String(marketChance(market)),
|
chance: String(marketChance(market)),
|
||||||
...(side && { side }),
|
...(side && { side }),
|
||||||
|
...(eventDetail.value?.slug && { slug: eventDetail.value.slug }),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadEventDetail() {
|
async function loadEventDetail() {
|
||||||
const idRaw = route.params.id
|
const idRaw = route.params.id
|
||||||
const id = typeof idRaw === 'string' ? parseInt(idRaw, 10) : Number(idRaw)
|
const idStr = String(idRaw ?? '').trim()
|
||||||
if (!Number.isFinite(id) || id < 1) {
|
if (!idStr) {
|
||||||
detailError.value = '无效的 ID'
|
detailError.value = '无效的 ID 或 slug'
|
||||||
eventDetail.value = null
|
eventDetail.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const numId = parseInt(idStr, 10)
|
||||||
|
const isNumericId = Number.isFinite(numId) && String(numId) === idStr && numId >= 1
|
||||||
|
const slugFromQuery = (route.query.slug as string)?.trim()
|
||||||
|
const params: FindPmEventParams = {
|
||||||
|
id: isNumericId ? numId : undefined,
|
||||||
|
slug: isNumericId ? (slugFromQuery || undefined) : idStr,
|
||||||
|
}
|
||||||
|
|
||||||
detailError.value = null
|
detailError.value = null
|
||||||
detailLoading.value = true
|
detailLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await findPmEvent(id, {
|
const res = await findPmEvent(params, {
|
||||||
headers: userStore.token ? { 'x-token': userStore.token } : undefined,
|
headers: userStore.getAuthHeaders(),
|
||||||
})
|
})
|
||||||
if (res.code === 0 || res.code === 200) {
|
if (res.code === 0 || res.code === 200) {
|
||||||
eventDetail.value = res.data ?? null
|
eventDetail.value = res.data ?? null
|
||||||
detailError.value = null
|
detailError.value = null
|
||||||
} else {
|
} else {
|
||||||
const fallback = getMockEventById(id)
|
const fallback = isNumericId ? getMockEventById(numId) : null
|
||||||
if (fallback) {
|
if (fallback) {
|
||||||
eventDetail.value = fallback
|
eventDetail.value = fallback
|
||||||
detailError.value = null
|
detailError.value = null
|
||||||
@ -504,7 +513,7 @@ async function loadEventDetail() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const fallback = getMockEventById(id)
|
const fallback = isNumericId ? getMockEventById(numId) : null
|
||||||
if (fallback) {
|
if (fallback) {
|
||||||
eventDetail.value = fallback
|
eventDetail.value = fallback
|
||||||
detailError.value = null
|
detailError.value = null
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
v-for="card in eventList"
|
v-for="card in eventList"
|
||||||
:key="card.id"
|
:key="card.id"
|
||||||
:id="card.id"
|
:id="card.id"
|
||||||
|
:slug="card.slug"
|
||||||
:market-title="card.marketTitle"
|
:market-title="card.marketTitle"
|
||||||
:chance-value="card.chanceValue"
|
:chance-value="card.chanceValue"
|
||||||
:market-info="card.marketInfo"
|
:market-info="card.marketInfo"
|
||||||
@ -28,6 +29,7 @@
|
|||||||
:yes-label="card.yesLabel"
|
:yes-label="card.yesLabel"
|
||||||
:no-label="card.noLabel"
|
:no-label="card.noLabel"
|
||||||
:is-new="card.isNew"
|
:is-new="card.isNew"
|
||||||
|
:market-id="card.marketId"
|
||||||
@open-trade="onCardOpenTrade"
|
@open-trade="onCardOpenTrade"
|
||||||
/>
|
/>
|
||||||
<div v-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
|
<div v-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
|
||||||
@ -68,6 +70,7 @@
|
|||||||
>
|
>
|
||||||
<TradeComponent
|
<TradeComponent
|
||||||
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
|
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
|
||||||
|
:market="homeTradeMarketPayload"
|
||||||
:initial-option="tradeDialogSide"
|
:initial-option="tradeDialogSide"
|
||||||
/>
|
/>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
@ -78,6 +81,7 @@
|
|||||||
>
|
>
|
||||||
<TradeComponent
|
<TradeComponent
|
||||||
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
|
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
|
||||||
|
:market="homeTradeMarketPayload"
|
||||||
:initial-option="tradeDialogSide"
|
:initial-option="tradeDialogSide"
|
||||||
embedded-in-sheet
|
embedded-in-sheet
|
||||||
/>
|
/>
|
||||||
@ -198,14 +202,26 @@ const noMoreEvents = computed(() => {
|
|||||||
const footerLang = ref('English')
|
const footerLang = ref('English')
|
||||||
const tradeDialogOpen = ref(false)
|
const tradeDialogOpen = ref(false)
|
||||||
const tradeDialogSide = ref<'yes' | 'no'>('yes')
|
const tradeDialogSide = ref<'yes' | 'no'>('yes')
|
||||||
const tradeDialogMarket = ref<{ id: string; title: string } | null>(null)
|
const tradeDialogMarket = ref<{ id: string; title: string; marketId?: string } | null>(null)
|
||||||
const scrollRef = ref<HTMLElement | null>(null)
|
const scrollRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
function onCardOpenTrade(side: 'yes' | 'no', market?: { id: string; title: string }) {
|
function onCardOpenTrade(side: 'yes' | 'no', market?: { id: string; title: string; marketId?: string }) {
|
||||||
tradeDialogSide.value = side
|
tradeDialogSide.value = side
|
||||||
tradeDialogMarket.value = market ?? null
|
tradeDialogMarket.value = market ?? null
|
||||||
tradeDialogOpen.value = true
|
tradeDialogOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 传给 TradeComponent 的 market(Home 弹窗/底部栏),供 Split 等使用;优先用 marketId,无则用事件 id */
|
||||||
|
const homeTradeMarketPayload = computed(() => {
|
||||||
|
const m = tradeDialogMarket.value
|
||||||
|
if (!m) return undefined
|
||||||
|
const marketId = m.marketId ?? m.id
|
||||||
|
const chance = 50
|
||||||
|
const yesPrice = Math.min(1, Math.max(0, chance / 100))
|
||||||
|
const noPrice = 1 - yesPrice
|
||||||
|
return { marketId, yesPrice, noPrice, title: m.title }
|
||||||
|
})
|
||||||
|
|
||||||
const sentinelRef = ref<HTMLElement | null>(null)
|
const sentinelRef = ref<HTMLElement | null>(null)
|
||||||
let observer: IntersectionObserver | null = null
|
let observer: IntersectionObserver | null = null
|
||||||
let resizeObserver: ResizeObserver | null = null
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
|||||||
@ -66,7 +66,9 @@ import { ref } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { BrowserProvider } from 'ethers'
|
import { BrowserProvider } from 'ethers'
|
||||||
import { SiweMessage } from 'siwe'
|
import { SiweMessage } from 'siwe'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import type { UserInfo } from '@/stores/user'
|
||||||
|
import { post } from '@/api/request'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@ -165,33 +167,24 @@ const connectWithWallet = async () => {
|
|||||||
|
|
||||||
console.log('Signature:', signature)
|
console.log('Signature:', signature)
|
||||||
|
|
||||||
// Call login API
|
// Call login API(使用 BASE_URL,可通过 VITE_API_BASE_URL 配置)
|
||||||
const loginResponse = await fetch('https://api.xtrader.vip/base/walletLogin', {
|
const loginData = await post<{ code: number; data?: { token: string; user?: UserInfo } }>(
|
||||||
//http://localhost:8080 //https://api.xtrader.vip
|
'/base/walletLogin',
|
||||||
method: 'POST',
|
{
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
Message: message1,
|
Message: message1,
|
||||||
nonce,
|
nonce,
|
||||||
signature,
|
signature,
|
||||||
walletAddress,
|
walletAddress,
|
||||||
}),
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
if (!loginResponse.ok) {
|
|
||||||
throw new Error('Login failed. Please try again.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginData = await loginResponse.json()
|
|
||||||
console.log('Login API response:', loginData)
|
console.log('Login API response:', loginData)
|
||||||
|
|
||||||
if (loginData.code === 0 && loginData.data) {
|
if (loginData.code === 0 && loginData.data) {
|
||||||
userStore.setUser({
|
userStore.setUser({
|
||||||
token: loginData.data.token,
|
token: loginData.data.token,
|
||||||
user: loginData.data.user ?? null,
|
user: loginData.data.user,
|
||||||
})
|
})
|
||||||
|
userStore.fetchUsdcBalance()
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push('/')
|
router.push('/')
|
||||||
|
|||||||
@ -110,10 +110,13 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<!-- 右侧:交易组件(固定宽度) -->
|
<!-- 右侧:交易组件(固定宽度),传入当前市场以便 Split 调用拆单接口 -->
|
||||||
<v-col cols="12" class="trade-col">
|
<v-col cols="12" class="trade-col">
|
||||||
<div class="trade-sidebar">
|
<div class="trade-sidebar">
|
||||||
<TradeComponent />
|
<TradeComponent
|
||||||
|
:market="tradeMarketPayload"
|
||||||
|
:initial-option="tradeInitialOption"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
@ -127,7 +130,7 @@ import * as echarts from 'echarts'
|
|||||||
import type { ECharts } from 'echarts'
|
import type { ECharts } from 'echarts'
|
||||||
import OrderBook from '../components/OrderBook.vue'
|
import OrderBook from '../components/OrderBook.vue'
|
||||||
import TradeComponent from '../components/TradeComponent.vue'
|
import TradeComponent from '../components/TradeComponent.vue'
|
||||||
import { findPmEvent, type PmEventListItem } from '../api/event'
|
import { findPmEvent, getMarketId, type FindPmEventParams, type PmEventListItem } from '../api/event'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -184,17 +187,25 @@ function formatExpiresAt(endDate: string | undefined): string {
|
|||||||
|
|
||||||
async function loadEventDetail() {
|
async function loadEventDetail() {
|
||||||
const idRaw = route.params.id
|
const idRaw = route.params.id
|
||||||
const id = typeof idRaw === 'string' ? parseInt(idRaw, 10) : Number(idRaw)
|
const idStr = String(idRaw ?? '').trim()
|
||||||
if (!Number.isFinite(id) || id < 1) {
|
if (!idStr) {
|
||||||
detailError.value = '无效的 ID'
|
detailError.value = '无效的 ID 或 slug'
|
||||||
eventDetail.value = null
|
eventDetail.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const numId = parseInt(idStr, 10)
|
||||||
|
const isNumericId = Number.isFinite(numId) && String(numId) === idStr && numId >= 1
|
||||||
|
const slugFromQuery = (route.query.slug as string)?.trim()
|
||||||
|
const params: FindPmEventParams = {
|
||||||
|
id: isNumericId ? numId : undefined,
|
||||||
|
slug: isNumericId ? (slugFromQuery || undefined) : idStr,
|
||||||
|
}
|
||||||
|
|
||||||
detailError.value = null
|
detailError.value = null
|
||||||
detailLoading.value = true
|
detailLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await findPmEvent(id, {
|
const res = await findPmEvent(params, {
|
||||||
headers: userStore.token ? { 'x-token': userStore.token } : undefined
|
headers: userStore.getAuthHeaders()
|
||||||
})
|
})
|
||||||
if (res.code === 0 || res.code === 200) {
|
if (res.code === 0 || res.code === 200) {
|
||||||
eventDetail.value = res.data ?? null
|
eventDetail.value = res.data ?? null
|
||||||
@ -228,6 +239,55 @@ const resolutionDate = computed(() => {
|
|||||||
return s ? s.replace(/,?\s*\d{4}$/, '').trim() || 'Mar 31' : 'Mar 31'
|
return s ? s.replace(/,?\s*\d{4}$/, '').trim() || 'Mar 31' : 'Mar 31'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 当前市场(用于交易组件与 Split 拆单):query.marketId 匹配或取第一个 */
|
||||||
|
const currentMarket = computed(() => {
|
||||||
|
const list = eventDetail.value?.markets ?? []
|
||||||
|
if (list.length === 0) return null
|
||||||
|
const qId = route.query.marketId
|
||||||
|
if (qId != null && String(qId).trim() !== '') {
|
||||||
|
const qStr = String(qId).trim()
|
||||||
|
const found = list.find((m) => getMarketId(m) === qStr)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
return list[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 传给 TradeComponent 的 market,供 Split 调用 /PmMarket/split;接口未返回时用 query 兜底 */
|
||||||
|
const tradeMarketPayload = computed(() => {
|
||||||
|
const m = currentMarket.value
|
||||||
|
if (m) {
|
||||||
|
const yesRaw = m.outcomePrices?.[0]
|
||||||
|
const noRaw = m.outcomePrices?.[1]
|
||||||
|
const yesPrice = yesRaw != null && Number.isFinite(Number(yesRaw)) ? Number(yesRaw) : 0.5
|
||||||
|
const noPrice = noRaw != null && Number.isFinite(Number(noRaw)) ? Number(noRaw) : 0.5
|
||||||
|
return {
|
||||||
|
marketId: getMarketId(m),
|
||||||
|
yesPrice,
|
||||||
|
noPrice,
|
||||||
|
title: m.question,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const qId = route.query.marketId
|
||||||
|
if (qId != null && String(qId).trim() !== '') {
|
||||||
|
const chance = route.query.chance != null ? Number(route.query.chance) : NaN
|
||||||
|
const yesPrice = Number.isFinite(chance) ? Math.min(1, Math.max(0, chance / 100)) : 0.5
|
||||||
|
const noPrice = Number.isFinite(chance) ? 1 - yesPrice : 0.5
|
||||||
|
return {
|
||||||
|
marketId: String(qId).trim(),
|
||||||
|
yesPrice,
|
||||||
|
noPrice,
|
||||||
|
title: (route.query.title as string) || undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const tradeInitialOption = computed(() => {
|
||||||
|
const side = route.query.side
|
||||||
|
if (side === 'yes' || side === 'no') return side
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
// Comments / Top Holders / Activity
|
// Comments / Top Holders / Activity
|
||||||
const detailTab = ref('activity')
|
const detailTab = ref('activity')
|
||||||
const activityMinAmount = ref<string>('0')
|
const activityMinAmount = ref<string>('0')
|
||||||
|
|||||||
@ -504,7 +504,7 @@ interface Position {
|
|||||||
/** 从 avgNow "72¢ → 0.5¢" 解析出 [avg, now] */
|
/** 从 avgNow "72¢ → 0.5¢" 解析出 [avg, now] */
|
||||||
function parseAvgNow(avgNow: string): [string, string] {
|
function parseAvgNow(avgNow: string): [string, string] {
|
||||||
const parts = avgNow.split(' → ')
|
const parts = avgNow.split(' → ')
|
||||||
return parts.length >= 2 ? [parts[0].trim(), parts[1].trim()] : [avgNow, '']
|
return parts.length >= 2 ? [parts[0]!.trim(), parts[1]!.trim()] : [avgNow, '']
|
||||||
}
|
}
|
||||||
interface OpenOrder {
|
interface OpenOrder {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@ -1,25 +1,14 @@
|
|||||||
// import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
// import { defineConfig } from 'vite'
|
|
||||||
// import vue from '@vitejs/plugin-vue'
|
|
||||||
// import vueJsx from '@vitejs/plugin-vue-jsx'
|
|
||||||
// import vueDevTools from 'vite-plugin-vue-devtools'
|
|
||||||
|
|
||||||
// // https://vite.dev/config/
|
|
||||||
// export default defineConfig({
|
|
||||||
// plugins: [vue(), vueJsx(), vueDevTools()],
|
|
||||||
// resolve: {
|
|
||||||
// alias: {
|
|
||||||
// '@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
// 1. 导入插件
|
|
||||||
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
// 2. 将此插件添加到插件数组中
|
// 2. 将此插件添加到插件数组中
|
||||||
@ -32,4 +21,7 @@ export default defineConfig({
|
|||||||
define: {
|
define: {
|
||||||
'process.env': {},
|
'process.env': {},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
host: true, // 监听 0.0.0.0,允许局域网内其他设备访问
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user