新增:拆分订单和合并订单接口对接
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
|
||||
# 连接测试服务器 192.168.3.21:8888 时复制本文件为 .env 或 .env.local 并取消下一行注释:
|
||||
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
|
||||
# 连接测试服务器 192.168.3.21:8888 时复制本文件为 .env 或 .env.local 并取消下一行注释:
|
||||
# API 基础地址(开发环境 npm run dev)
|
||||
# 不设置时默认 https://api.xtrader.vip
|
||||
# 连接测试服务器时复制本文件为 .env 并取消下一行注释:
|
||||
# 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>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>deploy</name>
|
||||
<description>将 PolyClientVuetify 项目打包并通过 SSH 部署到远程服务器。用户说「部署」「发布」时使用此 skill。</description>
|
||||
<location>project</location>
|
||||
</skill>
|
||||
|
||||
</available_skills>
|
||||
<!-- SKILLS_TABLE_END -->
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"deploy": "node --env-file=.env --env-file=.env.production scripts/deploy.mjs",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
"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">
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useUserStore } from './stores/user'
|
||||
|
||||
@ -9,6 +9,12 @@ const userStore = useUserStore()
|
||||
const currentRoute = computed(() => {
|
||||
return route.path
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (userStore.isLoggedIn) {
|
||||
userStore.fetchUsdcBalance()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@ -55,18 +55,30 @@ export interface PmEventListItem {
|
||||
* - outcomePrices: 各选项价格(首项为第一选项概率,如 Yes/Up)
|
||||
*/
|
||||
export interface PmEventMarketItem {
|
||||
/** 市场 ID(部分接口返回大写 ID) */
|
||||
ID?: number
|
||||
/** 市场 ID(部分接口或 JSON 序列化为小写 id) */
|
||||
id?: number
|
||||
question?: string
|
||||
slug?: string
|
||||
/** 选项展示文案,与 outcomePrices 顺序一致 */
|
||||
outcomes?: string[]
|
||||
/** 各选项价格,outcomes[0] 对应 outcomePrices[0] */
|
||||
outcomePrices?: string[] | number[]
|
||||
/** 市场对应的 clob token id 与 outcomePrices 和outcomes顺序一致 */
|
||||
clobTokenIds:string[]
|
||||
endDate?: string
|
||||
volume?: number
|
||||
[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 常用字段 */
|
||||
export interface PmEventSeriesItem {
|
||||
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
|
||||
*
|
||||
* 请求参数(Query):
|
||||
* - ID: number,必填,Event 主键
|
||||
* 鉴权:需在 headers 中传 x-token
|
||||
* - ID: number,可选
|
||||
* - slug: string,可选
|
||||
* - ID 与 slug 至少传一个,可同时传
|
||||
* 鉴权:需在 headers 中传 x-token、x-user-id
|
||||
*
|
||||
* 响应(200):PmEventDetailResponse { code, data: PmEventListItem, msg }
|
||||
*/
|
||||
export async function findPmEvent(
|
||||
id: number,
|
||||
params: FindPmEventParams,
|
||||
config?: { headers?: Record<string, string> }
|
||||
): 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 {
|
||||
id: string
|
||||
/** Event slug,用于 findPmEvent 传参 */
|
||||
slug?: string
|
||||
marketTitle: string
|
||||
chanceValue: number
|
||||
marketInfo: string
|
||||
@ -178,6 +211,8 @@ export interface EventCardItem {
|
||||
noLabel?: string
|
||||
/** 是否显示 NEW 角标 */
|
||||
isNew?: boolean
|
||||
/** 当前市场 ID(单 market 时为第一个 market 的 ID,供交易/Split 使用) */
|
||||
marketId?: string
|
||||
}
|
||||
|
||||
/** 内存缓存:列表数据,切换页面时复用,下拉刷新时清空 */
|
||||
@ -262,12 +297,13 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
||||
chanceValue: marketChance(m),
|
||||
yesLabel: m.outcomes?.[0] ?? 'Yes',
|
||||
noLabel: m.outcomes?.[1] ?? 'No',
|
||||
marketId: m.ID != null ? String(m.ID) : undefined,
|
||||
marketId: getMarketId(m),
|
||||
}))
|
||||
: undefined
|
||||
|
||||
return {
|
||||
id,
|
||||
slug: item.slug ?? undefined,
|
||||
marketTitle,
|
||||
chanceValue,
|
||||
marketInfo,
|
||||
@ -279,5 +315,6 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
||||
yesLabel: firstMarket?.outcomes?.[0] ?? 'Yes',
|
||||
noLabel: firstMarket?.outcomes?.[1] ?? 'No',
|
||||
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)
|
||||
*/
|
||||
export interface PmMarketSplitRequest {
|
||||
/** 市场 ID */
|
||||
marketId: string
|
||||
/** 要 split 的 USDC 金额 */
|
||||
amount: number
|
||||
marketID: string
|
||||
/** 要 split 的 USDC 金额(字符串) */
|
||||
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 接口
|
||||
* - 需鉴权:请求头 x-token
|
||||
* - 需鉴权:请求头 x-token、x-user-id
|
||||
* - 使用 VITE_API_BASE_URL 时可设为 http://192.168.3.21:8888 连接测试服
|
||||
*/
|
||||
export async function pmMarketSplit(
|
||||
|
||||
@ -6,7 +6,7 @@ const BASE_URL = typeof import.meta !== 'undefined' && (import.meta as unknown a
|
||||
: 'https://api.xtrader.vip'
|
||||
|
||||
export interface RequestConfig {
|
||||
/** 请求头,如 { 'x-token': token } */
|
||||
/** 请求头,如 { 'x-token': token, 'x-user-id': userId } */
|
||||
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
|
||||
marketInfo: string
|
||||
id: string
|
||||
/** Event slug,用于 findPmEvent 传参 */
|
||||
slug?: string
|
||||
imageUrl?: string
|
||||
category?: string
|
||||
expiresAt?: string
|
||||
@ -194,6 +196,8 @@ const props = withDefaults(
|
||||
noLabel?: string
|
||||
/** 是否显示 + NEW */
|
||||
isNew?: boolean
|
||||
/** 当前市场 ID(单 market 时供交易/Split 使用) */
|
||||
marketId?: string
|
||||
}>(),
|
||||
{
|
||||
marketTitle: 'Mamdan opens city-owned grocery store b...',
|
||||
@ -208,6 +212,7 @@ const props = withDefaults(
|
||||
yesLabel: 'Yes',
|
||||
noLabel: 'No',
|
||||
isNew: false,
|
||||
marketId: undefined,
|
||||
}
|
||||
)
|
||||
|
||||
@ -262,7 +267,10 @@ const semiProgressColor = computed(() => {
|
||||
|
||||
const navigateToDetail = () => {
|
||||
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
|
||||
}
|
||||
router.push({
|
||||
@ -274,12 +282,14 @@ const navigateToDetail = () => {
|
||||
marketInfo: props.marketInfo || undefined,
|
||||
expiresAt: props.expiresAt || undefined,
|
||||
chance: String(props.chanceValue),
|
||||
...(props.marketId && { marketId: props.marketId }),
|
||||
...(props.slug && { slug: props.slug }),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@ -579,9 +579,19 @@
|
||||
Available shares: {{ availableMergeShares }}
|
||||
<button type="button" class="merge-max-link" @click="setMergeMax">Max</button>
|
||||
</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-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
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@ -624,7 +634,7 @@
|
||||
block
|
||||
class="split-submit-btn"
|
||||
:loading="splitLoading"
|
||||
:disabled="splitLoading || !props.market?.marketId || splitAmount <= 0"
|
||||
:disabled="splitLoading || splitAmount <= 0"
|
||||
@click="submitSplit"
|
||||
>
|
||||
Split
|
||||
@ -638,7 +648,7 @@
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { pmMarketSplit } from '../api/market'
|
||||
import { pmMarketMerge, pmMarketSplit } from '../api/market'
|
||||
|
||||
const { mobile } = useDisplay()
|
||||
const userStore = useUserStore()
|
||||
@ -663,19 +673,42 @@ const props = withDefaults(
|
||||
// 移动端:底部栏与弹出层
|
||||
const sheetOpen = ref(false)
|
||||
|
||||
// Merge shares dialog
|
||||
// Merge shares dialog:对接 /PmMarket/merge
|
||||
const mergeDialogOpen = ref(false)
|
||||
const mergeAmount = ref(0)
|
||||
const availableMergeShares = ref(0)
|
||||
const mergeLoading = ref(false)
|
||||
const mergeError = ref('')
|
||||
function openMergeDialog() {
|
||||
mergeAmount.value = 0
|
||||
mergeError.value = ''
|
||||
mergeDialogOpen.value = true
|
||||
}
|
||||
function setMergeMax() {
|
||||
mergeAmount.value = availableMergeShares.value
|
||||
}
|
||||
function submitMerge() {
|
||||
mergeDialogOpen.value = false
|
||||
async function submitMerge() {
|
||||
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
|
||||
@ -684,19 +717,20 @@ const splitAmount = ref(0)
|
||||
const splitLoading = ref(false)
|
||||
const splitError = ref('')
|
||||
function openSplitDialog() {
|
||||
splitAmount.value = 0
|
||||
splitAmount.value = splitAmount.value > 0 ? splitAmount.value : 1
|
||||
splitError.value = ''
|
||||
splitDialogOpen.value = true
|
||||
}
|
||||
async function submitSplit() {
|
||||
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
|
||||
splitError.value = ''
|
||||
try {
|
||||
const res = await pmMarketSplit(
|
||||
{ marketId, amount: splitAmount.value },
|
||||
{ headers: userStore.token ? { 'x-token': userStore.token } : undefined }
|
||||
{ marketID: marketId, usdcAmount: String(splitAmount.value) },
|
||||
{ headers: userStore.getAuthHeaders() }
|
||||
)
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
splitDialogOpen.value = false
|
||||
@ -766,20 +800,29 @@ const actionButtonText = computed(() => {
|
||||
|
||||
function applyInitialOption(option: 'yes' | 'no') {
|
||||
selectedOption.value = option
|
||||
syncLimitPriceFromMarket()
|
||||
}
|
||||
|
||||
/** 根据当前 props.market 与 selectedOption 同步 limitPrice(组件显示或 market 更新时调用) */
|
||||
function syncLimitPriceFromMarket() {
|
||||
const yesP = props.market?.yesPrice ?? 0.19
|
||||
const noP = props.market?.noPrice ?? 0.82
|
||||
limitPrice.value = option === 'yes' ? yesP : noP
|
||||
limitPrice.value = selectedOption.value === 'yes' ? yesP : noP
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.initialOption) applyInitialOption(props.initialOption)
|
||||
else if (props.market) syncLimitPriceFromMarket()
|
||||
})
|
||||
watch(() => props.initialOption, (option) => {
|
||||
if (option) applyInitialOption(option)
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => props.market, (m) => {
|
||||
if (m && props.initialOption) applyInitialOption(props.initialOption)
|
||||
if (m) {
|
||||
if (props.initialOption) applyInitialOption(props.initialOption)
|
||||
else syncLimitPriceFromMarket()
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// Methods
|
||||
@ -1366,6 +1409,20 @@ function submitOrder() {
|
||||
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 {
|
||||
padding: 16px 20px 20px;
|
||||
padding-top: 8px;
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { getUsdcBalance, formatUsdcBalance } from '@/api/user'
|
||||
|
||||
export interface UserInfo {
|
||||
/** 用户 ID(API 可能返回 id 或 ID) */
|
||||
id?: number | string
|
||||
ID?: number
|
||||
headerImg?: string
|
||||
nickName?: string
|
||||
userName?: string
|
||||
@ -62,5 +65,29 @@ export const useUserStore = defineStore('user', () => {
|
||||
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 type { ECharts } from 'echarts'
|
||||
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 { useUserStore } from '../stores/user'
|
||||
|
||||
@ -152,7 +152,7 @@ const tradeMarketPayload = computed(() => {
|
||||
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: m.ID != null ? String(m.ID) : undefined,
|
||||
marketId: getMarketId(m),
|
||||
yesPrice,
|
||||
noPrice,
|
||||
title: m.question,
|
||||
@ -472,29 +472,38 @@ function goToTradeDetail(market: PmEventMarketItem, side?: 'yes' | 'no') {
|
||||
marketInfo: formatVolume(market.volume),
|
||||
chance: String(marketChance(market)),
|
||||
...(side && { side }),
|
||||
...(eventDetail.value?.slug && { slug: eventDetail.value.slug }),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function loadEventDetail() {
|
||||
const idRaw = route.params.id
|
||||
const id = typeof idRaw === 'string' ? parseInt(idRaw, 10) : Number(idRaw)
|
||||
if (!Number.isFinite(id) || id < 1) {
|
||||
detailError.value = '无效的 ID'
|
||||
const idStr = String(idRaw ?? '').trim()
|
||||
if (!idStr) {
|
||||
detailError.value = '无效的 ID 或 slug'
|
||||
eventDetail.value = null
|
||||
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
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const res = await findPmEvent(id, {
|
||||
headers: userStore.token ? { 'x-token': userStore.token } : undefined,
|
||||
const res = await findPmEvent(params, {
|
||||
headers: userStore.getAuthHeaders(),
|
||||
})
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
eventDetail.value = res.data ?? null
|
||||
detailError.value = null
|
||||
} else {
|
||||
const fallback = getMockEventById(id)
|
||||
const fallback = isNumericId ? getMockEventById(numId) : null
|
||||
if (fallback) {
|
||||
eventDetail.value = fallback
|
||||
detailError.value = null
|
||||
@ -504,7 +513,7 @@ async function loadEventDetail() {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const fallback = getMockEventById(id)
|
||||
const fallback = isNumericId ? getMockEventById(numId) : null
|
||||
if (fallback) {
|
||||
eventDetail.value = fallback
|
||||
detailError.value = null
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
v-for="card in eventList"
|
||||
:key="card.id"
|
||||
:id="card.id"
|
||||
:slug="card.slug"
|
||||
:market-title="card.marketTitle"
|
||||
:chance-value="card.chanceValue"
|
||||
:market-info="card.marketInfo"
|
||||
@ -28,6 +29,7 @@
|
||||
:yes-label="card.yesLabel"
|
||||
:no-label="card.noLabel"
|
||||
:is-new="card.isNew"
|
||||
:market-id="card.marketId"
|
||||
@open-trade="onCardOpenTrade"
|
||||
/>
|
||||
<div v-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
|
||||
@ -68,6 +70,7 @@
|
||||
>
|
||||
<TradeComponent
|
||||
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
|
||||
:market="homeTradeMarketPayload"
|
||||
:initial-option="tradeDialogSide"
|
||||
/>
|
||||
</v-dialog>
|
||||
@ -78,6 +81,7 @@
|
||||
>
|
||||
<TradeComponent
|
||||
:key="`trade-${tradeDialogMarket?.id}-${tradeDialogSide}`"
|
||||
:market="homeTradeMarketPayload"
|
||||
:initial-option="tradeDialogSide"
|
||||
embedded-in-sheet
|
||||
/>
|
||||
@ -198,14 +202,26 @@ const noMoreEvents = computed(() => {
|
||||
const footerLang = ref('English')
|
||||
const tradeDialogOpen = ref(false)
|
||||
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)
|
||||
|
||||
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
|
||||
tradeDialogMarket.value = market ?? null
|
||||
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)
|
||||
let observer: IntersectionObserver | null = null
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
@ -66,7 +66,9 @@ import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { BrowserProvider } from 'ethers'
|
||||
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 userStore = useUserStore()
|
||||
@ -165,33 +167,24 @@ const connectWithWallet = async () => {
|
||||
|
||||
console.log('Signature:', signature)
|
||||
|
||||
// Call login API
|
||||
const loginResponse = await fetch('https://api.xtrader.vip/base/walletLogin', {
|
||||
//http://localhost:8080 //https://api.xtrader.vip
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
// Call login API(使用 BASE_URL,可通过 VITE_API_BASE_URL 配置)
|
||||
const loginData = await post<{ code: number; data?: { token: string; user?: UserInfo } }>(
|
||||
'/base/walletLogin',
|
||||
{
|
||||
Message: message1,
|
||||
nonce,
|
||||
signature,
|
||||
walletAddress,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!loginResponse.ok) {
|
||||
throw new Error('Login failed. Please try again.')
|
||||
}
|
||||
|
||||
const loginData = await loginResponse.json()
|
||||
}
|
||||
)
|
||||
console.log('Login API response:', loginData)
|
||||
|
||||
if (loginData.code === 0 && loginData.data) {
|
||||
userStore.setUser({
|
||||
token: loginData.data.token,
|
||||
user: loginData.data.user ?? null,
|
||||
user: loginData.data.user,
|
||||
})
|
||||
userStore.fetchUsdcBalance()
|
||||
}
|
||||
|
||||
router.push('/')
|
||||
|
||||
@ -110,10 +110,13 @@
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- 右侧:交易组件(固定宽度) -->
|
||||
<!-- 右侧:交易组件(固定宽度),传入当前市场以便 Split 调用拆单接口 -->
|
||||
<v-col cols="12" class="trade-col">
|
||||
<div class="trade-sidebar">
|
||||
<TradeComponent />
|
||||
<TradeComponent
|
||||
:market="tradeMarketPayload"
|
||||
:initial-option="tradeInitialOption"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -127,7 +130,7 @@ import * as echarts from 'echarts'
|
||||
import type { ECharts } from 'echarts'
|
||||
import OrderBook from '../components/OrderBook.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'
|
||||
|
||||
/**
|
||||
@ -184,17 +187,25 @@ function formatExpiresAt(endDate: string | undefined): string {
|
||||
|
||||
async function loadEventDetail() {
|
||||
const idRaw = route.params.id
|
||||
const id = typeof idRaw === 'string' ? parseInt(idRaw, 10) : Number(idRaw)
|
||||
if (!Number.isFinite(id) || id < 1) {
|
||||
detailError.value = '无效的 ID'
|
||||
const idStr = String(idRaw ?? '').trim()
|
||||
if (!idStr) {
|
||||
detailError.value = '无效的 ID 或 slug'
|
||||
eventDetail.value = null
|
||||
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
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const res = await findPmEvent(id, {
|
||||
headers: userStore.token ? { 'x-token': userStore.token } : undefined
|
||||
const res = await findPmEvent(params, {
|
||||
headers: userStore.getAuthHeaders()
|
||||
})
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
eventDetail.value = res.data ?? null
|
||||
@ -228,6 +239,55 @@ const resolutionDate = computed(() => {
|
||||
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
|
||||
const detailTab = ref('activity')
|
||||
const activityMinAmount = ref<string>('0')
|
||||
|
||||
@ -504,7 +504,7 @@ interface Position {
|
||||
/** 从 avgNow "72¢ → 0.5¢" 解析出 [avg, now] */
|
||||
function parseAvgNow(avgNow: string): [string, string] {
|
||||
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 {
|
||||
id: string
|
||||
|
||||
@ -1,25 +1,14 @@
|
||||
// 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 { fileURLToPath, URL } from 'node:url'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
// 1. 导入插件
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
// 2. 将此插件添加到插件数组中
|
||||
@ -32,4 +21,7 @@ export default defineConfig({
|
||||
define: {
|
||||
'process.env': {},
|
||||
},
|
||||
server: {
|
||||
host: true, // 监听 0.0.0.0,允许局域网内其他设备访问
|
||||
},
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user