diff --git a/.cursor/skills/xtrader-api-docs/SKILL.md b/.cursor/skills/xtrader-api-docs/SKILL.md index 5330879..2c79a82 100644 --- a/.cursor/skills/xtrader-api-docs/SKILL.md +++ b/.cursor/skills/xtrader-api-docs/SKILL.md @@ -7,6 +7,20 @@ description: Interprets the XTrader API from the Swagger 2.0 spec at https://api 规范来源:[OpenAPI 规范](https://api.xtrader.vip/swagger/doc.json)(Swagger 2.0)。Swagger UI:。 +--- + +## ⚠️ 强制执行:接口接入三步流程 + +**接入任意 XTrader 接口时,必须严格按以下顺序执行,不得跳过、不得调换、不得合并步骤。** + +| 步骤 | 动作 | 强制要求 | +|------|------|----------| +| **第一步** | 从 doc.json 整理并**在对话中输出**请求参数表、响应参数表、definitions 完整结构 | 在输出第一步结果**之前**,不得写任何业务代码;必须用 `mcp_web_fetch` 或 curl 获取 doc.json,解析 `paths` 与 `definitions` | +| **第二步** | 根据第一步整理出的结构,在 `src/api/` 中定义 TypeScript 类型 | 必须等第一步输出完成后再执行;Model 必须与 definitions 对应 | +| **第三步** | 实现请求函数并集成到页面 | 必须等第二步完成后再执行 | + +**违反后果**:若跳过第一步直接写代码,会导致类型与接口文档不一致、遗漏字段或误用参数。 + ## 规范地址与格式 - **规范 URL**:`https://api.xtrader.vip/swagger/doc.json` @@ -49,11 +63,11 @@ Swagger UI 页面(如 [PmEvent findPmEvent](https://api.xtrader.vip/swagger/in ## 接口集成规范(必须按顺序执行) -接入任意 XTrader 接口时,**必须**按以下顺序执行,不得跳过或调换。 +接入任意 XTrader 接口时,**必须**按以下顺序执行,不得跳过或调换。**第一步的输出必须在对话中可见,才能进入第二步。** ### 第一步:列出请求参数与响应参数 -在写代码前,先从 doc.json 中整理并列出: +**在写任何代码之前**,先用 `mcp_web_fetch` 或 curl 获取 `https://api.xtrader.vip/swagger/doc.json`,然后整理并**在对话中输出**: 1. **请求参数** - **Query**:`paths[""][""].parameters` 中 `in: "query"` 的项(name、type、required、description)。 @@ -63,9 +77,9 @@ Swagger UI 页面(如 [PmEvent findPmEvent](https://api.xtrader.vip/swagger/in 2. **响应参数** - 取 `paths[""][""].responses["200"].schema`。 - 若有 `allOf`,合并得到根结构(通常为 `code`、`data`、`msg`)。 - - 对 `data` 及其他嵌套对象的 `$ref`,到 `definitions` 中查完整结构并列出字段(名称、类型、说明)。 + - 对 `data` 及其他嵌套对象的 `$ref`,到 `definitions` 中查完整结构并**列出所有字段**(名称、类型、说明)。 -输出形式可为表格或结构化列表,便于第二步写类型。 +**输出形式**:表格或结构化列表,便于第二步写类型。**未完成第一步输出前,禁止进入第二步。** ### 第二步:根据响应数据创建 Model 类 diff --git a/.env.example b/.env.example index cf34f1e..f97eb53 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,9 @@ # 连接测试服务器时复制本文件为 .env 并取消下一行注释: # VITE_API_BASE_URL=http://192.168.3.21:8888 # +# WebSocket 路径前缀(可选)。若 CLOB WS 在 /api/clob/ws 且 API base 无 /api,则设: +# VITE_WS_PATH_PREFIX=/api +# # 生产打包/部署时自动使用 .env.production 中的 https://api.xtrader.vip # SSH 部署(npm run deploy),不配置时使用默认值 diff --git a/src/App.vue b/src/App.vue index e096ddd..4aac431 100644 --- a/src/App.vue +++ b/src/App.vue @@ -32,7 +32,12 @@ onMounted(() => { PolyMarket - + Login - - - - - + + + + + diff --git a/src/api/category.ts b/src/api/category.ts index d4cf841..4cb4d90 100644 --- a/src/api/category.ts +++ b/src/api/category.ts @@ -1,5 +1,29 @@ import { get } from './request' +/** + * 接口返回的 PmTag 结构(definitions polymarket.PmTag) + * doc.json definitions["polymarket.PmTag"] 完整字段 + * 注:主分类树可能返回 children(递归同结构),definitions 未声明,需兼容 + */ +export interface PmTagMainItem { + ID?: number + label?: string + slug?: string + createdAt?: string + createdBy?: number + forceShow?: boolean + publishedAt?: string + requiresTranslation?: boolean + updatedAt?: string + updatedBy?: number + /** 树形子节点,后端可能返回,definitions 未声明 */ + children?: PmTagMainItem[] + /** 以下为后端可能返回的扩展字段,definitions 未声明 */ + icon?: string + sectionTitle?: string + forceHide?: boolean +} + /** 分类树节点(与后端返回结构一致) */ export interface CategoryTreeNode { id: string @@ -92,9 +116,11 @@ export interface CategoryTreeResponse { * data 可能为数组,或 { list: [] } 等格式,统一转为 CategoryTreeNode[] */ export async function getCategoryTree(): Promise { - const res = await get<{ code: number; data: CategoryTreeNode[] | { list?: CategoryTreeNode[] }; msg: string }>( - '/PmTag/getPmTagPublic' - ) + const res = await get<{ + code: number + data: CategoryTreeNode[] | { list?: CategoryTreeNode[] } + msg: string + }>('/PmTag/getPmTagPublic') let data: CategoryTreeNode[] = [] const raw = res.data if (Array.isArray(raw)) { @@ -108,3 +134,32 @@ export async function getCategoryTree(): Promise { } return { code: res.code, data, msg: res.msg } } + +/** 将 PmTagMainItem 转为 CategoryTreeNode */ +function mapPmTagToTreeNode(item: PmTagMainItem): CategoryTreeNode { + const rawId = item.ID + const id = rawId != null ? String(rawId) : (item.slug ?? item.label ?? '') + const children = Array.isArray(item.children) ? item.children.map(mapPmTagToTreeNode) : undefined + return { + id, + label: item.label ?? '', + slug: item.slug ?? '', + icon: item.icon, + sectionTitle: item.sectionTitle, + forceShow: item.forceShow, + forceHide: item.forceHide, + children: children?.length ? children : undefined, + } +} + +/** + * 获取主分类(首页顶部分类 Tab 数据) + * GET /PmTag/getPmTagMain + * + * 不需要鉴权,返回带 children 的树形结构,最多三层 + */ +export async function getPmTagMain(): Promise { + const res = await get<{ code: number; data: PmTagMainItem[]; msg: string }>('/PmTag/getPmTagMain') + const data: CategoryTreeNode[] = Array.isArray(res.data) ? res.data.map(mapPmTagToTreeNode) : [] + return { code: res.code, data, msg: res.msg } +} diff --git a/src/api/event.ts b/src/api/event.ts index b484da4..f948a13 100644 --- a/src/api/event.ts +++ b/src/api/event.ts @@ -82,7 +82,7 @@ export function getMarketId(m: PmEventMarketItem | null | undefined): string | u /** 从市场项取 clobTokenId,outcomeIndex 0=Yes/第一选项,1=No/第二选项 */ export function getClobTokenId( m: PmEventMarketItem | null | undefined, - outcomeIndex: 0 | 1 = 0 + outcomeIndex: 0 | 1 = 0, ): string | undefined { if (!m?.clobTokenIds?.length) return undefined const id = m.clobTokenIds[outcomeIndex] @@ -131,7 +131,7 @@ export interface GetPmEventListParams { * tokenid 对应 market.clobTokenIds 中的值,可传单个或数组 */ export async function getPmEventPublic( - params: GetPmEventListParams = {} + params: GetPmEventListParams = {}, ): Promise { const { page = 1, pageSize = 10, keyword, createdAtRange, tokenid } = params const query: Record = { @@ -182,7 +182,7 @@ export interface FindPmEventParams { */ export async function findPmEvent( params: FindPmEventParams, - config?: { headers?: Record } + config?: { headers?: Record }, ): Promise { const query: Record = {} if (params.id != null) query.ID = params.id @@ -254,7 +254,7 @@ export function setEventListCache(data: { page: number total: number pageSize: number - }) { +}) { eventListCache = data } @@ -304,7 +304,11 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem { try { const d = new Date(item.endDate) if (!Number.isNaN(d.getTime())) { - expiresAt = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + expiresAt = d.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }) } } catch { expiresAt = item.endDate diff --git a/src/api/market.ts b/src/api/market.ts index 39f1bc7..57412e2 100644 --- a/src/api/market.ts +++ b/src/api/market.ts @@ -32,7 +32,7 @@ export interface ClobSubmitOrderRequest { */ export async function pmOrderPlace( data: ClobSubmitOrderRequest, - config?: { headers?: Record } + config?: { headers?: Record }, ): Promise { return post('/clob/gateway/submitOrder', data, config) } @@ -52,7 +52,7 @@ export interface ClobCancelOrderRequest { */ export async function pmCancelOrder( data: ClobCancelOrderRequest, - config?: { headers?: Record } + config?: { headers?: Record }, ): Promise { return post('/clob/gateway/cancelOrder', data, config) } @@ -85,7 +85,7 @@ export interface PmMarketMergeRequest { */ export async function pmMarketMerge( data: PmMarketMergeRequest, - config?: { headers?: Record } + config?: { headers?: Record }, ): Promise { return post('/PmMarket/merge', data, config) } @@ -97,7 +97,7 @@ export async function pmMarketMerge( */ export async function pmMarketSplit( data: PmMarketSplitRequest, - config?: { headers?: Record } + config?: { headers?: Record }, ): Promise { return post('/PmMarket/split', data, config) } diff --git a/src/api/mockEventList.ts b/src/api/mockEventList.ts index 31ed6d8..8286492 100644 --- a/src/api/mockEventList.ts +++ b/src/api/mockEventList.ts @@ -124,14 +124,47 @@ export const MOCK_EVENT_LIST: PmEventListItem[] = [ endDate: '2026-02-10T23:59:59.000Z', new: true, markets: [ - { ID: 90051, question: '260-279', outcomes: ['Yes', 'No'], outcomePrices: [0.01, 0.99], volume: 120000 }, - { ID: 90052, question: '280-299', outcomes: ['Yes', 'No'], outcomePrices: [0.42, 0.58], volume: 939379 }, - { ID: 90053, question: '300-319', outcomes: ['Yes', 'No'], outcomePrices: [0.45, 0.55], volume: 850000 }, - { ID: 90054, question: '320-339', outcomes: ['Yes', 'No'], outcomePrices: [0.16, 0.84], volume: 320000 }, - { ID: 90055, question: '340-359', outcomes: ['Yes', 'No'], outcomePrices: [0.08, 0.92], volume: 180000 }, + { + ID: 90051, + question: '260-279', + outcomes: ['Yes', 'No'], + outcomePrices: [0.01, 0.99], + volume: 120000, + }, + { + ID: 90052, + question: '280-299', + outcomes: ['Yes', 'No'], + outcomePrices: [0.42, 0.58], + volume: 939379, + }, + { + ID: 90053, + question: '300-319', + outcomes: ['Yes', 'No'], + outcomePrices: [0.45, 0.55], + volume: 850000, + }, + { + ID: 90054, + question: '320-339', + outcomes: ['Yes', 'No'], + outcomePrices: [0.16, 0.84], + volume: 320000, + }, + { + ID: 90055, + question: '340-359', + outcomes: ['Yes', 'No'], + outcomePrices: [0.08, 0.92], + volume: 180000, + }, ], series: [{ ID: 5, title: 'Culture', ticker: 'CULTURE' }], - tags: [{ label: 'Tech', slug: 'tech' }, { label: 'Twitter', slug: 'twitter' }], + tags: [ + { label: 'Tech', slug: 'tech' }, + { label: 'Twitter', slug: 'twitter' }, + ], }, // 6. 多 market:总统候选人胜选概率(多候选人) { @@ -145,9 +178,27 @@ export const MOCK_EVENT_LIST: PmEventListItem[] = [ endDate: '2028-11-07T23:59:59.000Z', new: true, markets: [ - { ID: 90061, question: 'Democrat nominee', outcomes: ['Yes', 'No'], outcomePrices: [0.48, 0.52], volume: 3200000 }, - { ID: 90062, question: 'Republican nominee', outcomes: ['Yes', 'No'], outcomePrices: [0.45, 0.55], volume: 3100000 }, - { ID: 90063, question: 'Third party / Independent', outcomes: ['Yes', 'No'], outcomePrices: [0.07, 0.93], volume: 800000 }, + { + ID: 90061, + question: 'Democrat nominee', + outcomes: ['Yes', 'No'], + outcomePrices: [0.48, 0.52], + volume: 3200000, + }, + { + ID: 90062, + question: 'Republican nominee', + outcomes: ['Yes', 'No'], + outcomePrices: [0.45, 0.55], + volume: 3100000, + }, + { + ID: 90063, + question: 'Third party / Independent', + outcomes: ['Yes', 'No'], + outcomePrices: [0.07, 0.93], + volume: 800000, + }, ], series: [{ ID: 6, title: 'Politics', ticker: 'POL' }], tags: [{ label: 'Election', slug: 'election' }], @@ -164,10 +215,34 @@ export const MOCK_EVENT_LIST: PmEventListItem[] = [ endDate: '2026-05-31T23:59:59.000Z', new: false, markets: [ - { ID: 90071, question: 'Boston Celtics', outcomes: ['Yes', 'No'], outcomePrices: [0.35, 0.65], volume: 280000 }, - { ID: 90072, question: 'Milwaukee Bucks', outcomes: ['Yes', 'No'], outcomePrices: [0.28, 0.72], volume: 220000 }, - { ID: 90073, question: 'Philadelphia 76ers', outcomes: ['Yes', 'No'], outcomePrices: [0.18, 0.82], volume: 150000 }, - { ID: 90074, question: 'New York Knicks', outcomes: ['Yes', 'No'], outcomePrices: [0.12, 0.88], volume: 120000 }, + { + ID: 90071, + question: 'Boston Celtics', + outcomes: ['Yes', 'No'], + outcomePrices: [0.35, 0.65], + volume: 280000, + }, + { + ID: 90072, + question: 'Milwaukee Bucks', + outcomes: ['Yes', 'No'], + outcomePrices: [0.28, 0.72], + volume: 220000, + }, + { + ID: 90073, + question: 'Philadelphia 76ers', + outcomes: ['Yes', 'No'], + outcomePrices: [0.18, 0.82], + volume: 150000, + }, + { + ID: 90074, + question: 'New York Knicks', + outcomes: ['Yes', 'No'], + outcomePrices: [0.12, 0.88], + volume: 120000, + }, ], series: [{ ID: 7, title: 'Sports', ticker: 'SPORT' }], tags: [{ label: 'NBA', slug: 'nba' }], diff --git a/src/api/request.ts b/src/api/request.ts index 48be2d8..401ca96 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -1,9 +1,11 @@ /** * 请求基础 URL,默认 https://api.xtrader.vip,可通过环境变量 VITE_API_BASE_URL 覆盖 */ -const BASE_URL = typeof import.meta !== 'undefined' && (import.meta as unknown as { env?: Record }).env?.VITE_API_BASE_URL - ? (import.meta as unknown as { env: Record }).env.VITE_API_BASE_URL - : 'https://api.xtrader.vip' +const BASE_URL = + typeof import.meta !== 'undefined' && + (import.meta as unknown as { env?: Record }).env?.VITE_API_BASE_URL + ? (import.meta as unknown as { env: Record }).env.VITE_API_BASE_URL + : 'https://api.xtrader.vip' export interface RequestConfig { /** 请求头,如 { 'x-token': token, 'x-user-id': userId } */ @@ -16,7 +18,7 @@ export interface RequestConfig { export async function get( path: string, params?: Record, - config?: RequestConfig + config?: RequestConfig, ): Promise { const url = new URL(path, BASE_URL || window.location.origin) if (params) { @@ -46,7 +48,7 @@ export async function get( export async function post( path: string, body?: unknown, - config?: RequestConfig + config?: RequestConfig, ): Promise { const url = new URL(path, BASE_URL || window.location.origin) const headers: Record = { diff --git a/src/api/user.ts b/src/api/user.ts index 8c3c95b..f256cf5 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -26,7 +26,9 @@ export function formatUsdcBalance(raw: string): string { * 查询 USDC 余额,需鉴权(x-token) * amount、available 需除以 1000000 得到实际 USDC */ -export async function getUsdcBalance(authHeaders: Record): Promise { +export async function getUsdcBalance( + authHeaders: Record, +): Promise { const res = await get('/user/getUsdcBalance', undefined, { headers: authHeaders, }) diff --git a/src/components/DepositDialog.vue b/src/components/DepositDialog.vue index cae601b..ff144c5 100644 --- a/src/components/DepositDialog.vue +++ b/src/components/DepositDialog.vue @@ -81,13 +81,7 @@
- QR Code + QR Code
@@ -101,7 +95,10 @@ Connect Exchange @@ -122,7 +127,7 @@
${{ amount.toFixed(2) }}
- +
+$1 @@ -133,12 +138,7 @@
- - Deposit - + Deposit @@ -276,7 +276,12 @@

{{ orderError }}

- + {{ actionButtonText }} @@ -299,40 +304,95 @@ - Market - Limit + Market + Limit - Merge - Split + Merge + Split - Market - Limit + Market + Limit - Merge - Split + Merge + Split - - @@ -563,17 +803,30 @@ - +

Merge shares

- + mdi-close

- Merge a share of Yes and No to get 1 USDC. You can do this to save cost when trying to get rid of a position. + Merge a share of Yes and No to get 1 USDC. You can do this to save cost when trying to get + rid of a position.

@@ -591,7 +844,9 @@ Available shares: {{ availableMergeShares }}

-

Please select a market first (e.g. click Buy Yes/No on a market).

+

+ Please select a market first (e.g. click Buy Yes/No on a market). +

{{ mergeError }}

@@ -611,17 +866,30 @@ - +

Split

- + mdi-close

- Use USDC to get one share of Yes and one share of No for this market. 1 USDC ≈ 1 complete set. + Use USDC to get one share of Yes and one share of No for this market. 1 USDC ≈ 1 complete + set.

@@ -636,7 +904,9 @@ class="split-amount-input" />
-

Please select a market first (e.g. click Buy Yes/No on a market).

+

+ Please select a market first (e.g. click Buy Yes/No on a market). +

{{ splitError }}

@@ -682,7 +952,7 @@ const props = withDefaults( /** 从外部传入的市场数据(如 EventMarkets 点击 Yes/No 传入),yesPrice/noPrice 为 0–1 */ market?: TradeMarketPayload }>(), - { initialOption: undefined, embeddedInSheet: false, market: undefined } + { initialOption: undefined, embeddedInSheet: false, market: undefined }, ) // 移动端:底部栏与弹出层 @@ -711,7 +981,7 @@ async function submitMerge() { try { const res = await pmMarketMerge( { marketID: marketId, amount: String(mergeAmount.value) }, - { headers: userStore.getAuthHeaders() } + { headers: userStore.getAuthHeaders() }, ) if (res.code === 0 || res.code === 200) { mergeDialogOpen.value = false @@ -745,7 +1015,7 @@ async function submitSplit() { try { const res = await pmMarketSplit( { marketID: marketId, usdcAmount: String(splitAmount.value) }, - { headers: userStore.getAuthHeaders() } + { headers: userStore.getAuthHeaders() }, ) if (res.code === 0 || res.code === 200) { splitDialogOpen.value = false @@ -759,12 +1029,8 @@ async function submitSplit() { } } -const yesPriceCents = computed(() => - props.market ? Math.round(props.market.yesPrice * 100) : 19 -) -const noPriceCents = computed(() => - props.market ? Math.round(props.market.noPrice * 100) : 82 -) +const yesPriceCents = computed(() => (props.market ? Math.round(props.market.yesPrice * 100) : 19)) +const noPriceCents = computed(() => (props.market ? Math.round(props.market.noPrice * 100) : 82)) function openSheet(option: 'yes' | 'no') { handleOptionChange(option) @@ -792,15 +1058,17 @@ const orderError = ref('') // Emits const emit = defineEmits<{ optionChange: [option: 'yes' | 'no'] - submit: [payload: { - side: 'buy' | 'sell' - option: 'yes' | 'no' - limitPrice: number - shares: number - expirationEnabled: boolean - expirationTime: string - marketId?: string - }] + submit: [ + payload: { + side: 'buy' | 'sell' + option: 'yes' | 'no' + limitPrice: number + shares: number + expirationEnabled: boolean + expirationTime: string + marketId?: string + }, + ] }>() // Computed properties @@ -836,17 +1104,25 @@ onMounted(() => { if (props.initialOption) applyInitialOption(props.initialOption) else if (props.market) syncLimitPriceFromMarket() }) -watch(() => props.initialOption, (option) => { - if (option) applyInitialOption(option) -}, { immediate: true }) +watch( + () => props.initialOption, + (option) => { + if (option) applyInitialOption(option) + }, + { immediate: true }, +) -watch(() => props.market, (m) => { - if (m) { - orderError.value = '' - if (props.initialOption) applyInitialOption(props.initialOption) - else syncLimitPriceFromMarket() - } -}, { deep: true }) +watch( + () => props.market, + (m) => { + if (m) { + orderError.value = '' + if (props.initialOption) applyInitialOption(props.initialOption) + else syncLimitPriceFromMarket() + } + }, + { deep: true }, +) // Methods const handleOptionChange = (option: 'yes' | 'no') => { @@ -1031,7 +1307,7 @@ async function submitOrder() { tokenID: tokenId, userID: userIdNum, }, - { headers } + { headers }, ) if (res.code === 0 || res.code === 200) { userStore.fetchUsdcBalance() @@ -1668,4 +1944,4 @@ async function submitOrder() { text-transform: none; font-weight: 600; } - \ No newline at end of file + diff --git a/src/components/WithdrawDialog.vue b/src/components/WithdrawDialog.vue index 0c53ea5..6ebed2e 100644 --- a/src/components/WithdrawDialog.vue +++ b/src/components/WithdrawDialog.vue @@ -110,7 +110,7 @@ const props = withDefaults( modelValue: boolean balance: string }>(), - { balance: '0.00' } + { balance: '0.00' }, ) const emit = defineEmits<{ 'update:modelValue': [value: boolean]; success: [] }>() @@ -145,7 +145,11 @@ const hasValidDestination = computed(() => { }) const canSubmit = computed( - () => amountNum.value > 0 && amountNum.value <= balanceNum.value && hasValidDestination.value && !amountError.value + () => + amountNum.value > 0 && + amountNum.value <= balanceNum.value && + hasValidDestination.value && + !amountError.value, ) function shortAddress(addr: string) { @@ -188,7 +192,8 @@ async function submitWithdraw() { submitting.value = true try { await new Promise((r) => setTimeout(r, 800)) - const dest = destinationType.value === 'wallet' ? connectedAddress.value : customAddress.value.trim() + const dest = + destinationType.value === 'wallet' ? connectedAddress.value : customAddress.value.trim() console.log('Withdraw', { amount: amount.value, network: selectedNetwork.value, to: dest }) emit('success') close() @@ -206,7 +211,7 @@ watch( customAddress.value = '' connectedAddress.value = '' } - } + }, ) diff --git a/src/plugins/vuetify.ts b/src/plugins/vuetify.ts index ff94be6..76b2d55 100644 --- a/src/plugins/vuetify.ts +++ b/src/plugins/vuetify.ts @@ -25,8 +25,8 @@ export default createVuetify({ success: '#34A853', warning: '#FBBC05', surface: '#FFFFFF', - background: '#F5F5F5' - } + background: '#F5F5F5', + }, }, dark: { dark: true, @@ -39,9 +39,9 @@ export default createVuetify({ success: '#4CAF50', warning: '#FFC107', surface: '#1E1E1E', - background: '#121212' - } - } - } - } -}) \ No newline at end of file + background: '#121212', + }, + }, + }, + }, +}) diff --git a/src/router/index.ts b/src/router/index.ts index 2bb2c29..7464580 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -12,33 +12,33 @@ const router = createRouter({ { path: '/', name: 'home', - component: Home + component: Home, }, { path: '/trade', name: 'trade', - component: Trade + component: Trade, }, { path: '/login', name: 'login', - component: Login + component: Login, }, { path: '/trade-detail/:id', name: 'trade-detail', - component: TradeDetail + component: TradeDetail, }, { path: '/event/:id/markets', name: 'event-markets', - component: EventMarkets + component: EventMarkets, }, { path: '/wallet', name: 'wallet', - component: Wallet - } + component: Wallet, + }, ], scrollBehavior(to, from, savedPosition) { if (savedPosition && from?.name) return savedPosition diff --git a/src/stores/user.ts b/src/stores/user.ts index 6ee6e67..bd205fe 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -89,5 +89,15 @@ export const useUserStore = defineStore('user', () => { } } - return { token, user, isLoggedIn, avatarUrl, balance, setUser, logout, getAuthHeaders, fetchUsdcBalance } + return { + token, + user, + isLoggedIn, + avatarUrl, + balance, + setUser, + logout, + getAuthHeaders, + fetchUsdcBalance, + } }) diff --git a/src/views/EventMarkets.vue b/src/views/EventMarkets.vue index 288f5fb..23a7ce7 100644 --- a/src/views/EventMarkets.vue +++ b/src/views/EventMarkets.vue @@ -123,7 +123,13 @@ import { useRoute, useRouter } from 'vue-router' import * as echarts from 'echarts' import type { ECharts } from 'echarts' import TradeComponent from '../components/TradeComponent.vue' -import { findPmEvent, getMarketId, type FindPmEventParams, 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' @@ -220,7 +226,16 @@ const chartData = ref([]) let chartInstance: ECharts | null = null let dynamicInterval: number | undefined -const LINE_COLORS = ['#2563eb', '#dc2626', '#16a34a', '#ca8a04', '#9333ea', '#0891b2', '#ea580c', '#4f46e5'] +const LINE_COLORS = [ + '#2563eb', + '#dc2626', + '#16a34a', + '#ca8a04', + '#9333ea', + '#0891b2', + '#ea580c', + '#4f46e5', +] const MOBILE_BREAKPOINT = 600 function getStepAndCount(range: string): { stepMs: number; count: number } { @@ -371,7 +386,8 @@ function initChart() { function updateChartData() { chartData.value = generateAllData() const w = chartContainerRef.value?.clientWidth - if (chartInstance) chartInstance.setOption(buildOption(chartData.value, w), { replaceMerge: ['series'] }) + if (chartInstance) + chartInstance.setOption(buildOption(chartData.value, w), { replaceMerge: ['series'] }) } function selectTimeRange(range: string) { @@ -396,7 +412,8 @@ function startDynamicUpdate() { }) chartData.value = nextData const w = chartContainerRef.value?.clientWidth - if (chartInstance) chartInstance.setOption(buildOption(chartData.value, w), { replaceMerge: ['series'] }) + if (chartInstance) + chartInstance.setOption(buildOption(chartData.value, w), { replaceMerge: ['series'] }) }, 3000) } @@ -491,7 +508,7 @@ async function loadEventDetail() { const slugFromQuery = (route.query.slug as string)?.trim() const params: FindPmEventParams = { id: isNumericId ? numId : undefined, - slug: isNumericId ? (slugFromQuery || undefined) : idStr, + slug: isNumericId ? slugFromQuery || undefined : idStr, } detailError.value = null @@ -552,11 +569,11 @@ watch( if (dynamicInterval == null) startDynamicUpdate() }) } - } + }, ) watch( () => route.params.id, - () => loadEventDetail() + () => loadEventDetail(), ) diff --git a/src/views/Home.vue b/src/views/Home.vue index 932374a..84ffa09 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -23,7 +23,13 @@ > mdi-magnify - + mdi-filter-outline
@@ -44,7 +50,14 @@ @blur="onSearchBlur" @keydown.enter="onSearchSubmit" /> - + mdi-close @@ -67,7 +80,11 @@ :key="`${item}-${idx}`" class="home-search-history-item" > - -
+
-
+ - - - - - - - - + + + + + + + +
@@ -287,7 +307,7 @@ import { clearEventListCache, type EventCardItem, } from '../api/event' -import { getCategoryTree, MOCK_CATEGORY_TREE, type CategoryTreeNode } from '../api/category' +import { getPmTagMain, MOCK_CATEGORY_TREE, type CategoryTreeNode } from '../api/category' import { useSearchHistory } from '../composables/useSearchHistory' const { mobile } = useDisplay() @@ -434,16 +454,27 @@ const loadingMore = ref(false) const noMoreEvents = computed(() => { if (eventList.value.length === 0) return false - return eventList.value.length >= eventTotal.value || eventPage.value * eventPageSize.value >= eventTotal.value + return ( + eventList.value.length >= eventTotal.value || + eventPage.value * eventPageSize.value >= eventTotal.value + ) }) const footerLang = ref('English') const tradeDialogOpen = ref(false) const tradeDialogSide = ref<'yes' | 'no'>('yes') -const tradeDialogMarket = ref<{ id: string; title: string; marketId?: string; clobTokenIds?: string[] } | null>(null) +const tradeDialogMarket = ref<{ + id: string + title: string + marketId?: string + clobTokenIds?: string[] +} | null>(null) const scrollRef = ref(null) -function onCardOpenTrade(side: 'yes' | 'no', market?: { id: string; title: string; marketId?: string }) { +function onCardOpenTrade( + side: 'yes' | 'no', + market?: { id: string; title: string; marketId?: string }, +) { tradeDialogSide.value = side tradeDialogMarket.value = market ?? null tradeDialogOpen.value = true @@ -492,9 +523,7 @@ const activeSearchKeyword = ref('') async function loadEvents(page: number, append: boolean, keyword?: string) { const kw = keyword !== undefined ? keyword : activeSearchKeyword.value try { - const res = await getPmEventPublic( - { page, pageSize: PAGE_SIZE, keyword: kw || undefined } - ) + const res = await getPmEventPublic({ page, pageSize: PAGE_SIZE, keyword: kw || undefined }) if (res.code !== 0 && res.code !== 200) { throw new Error(res.msg || '请求失败') } @@ -548,12 +577,12 @@ function checkScrollLoad() { onMounted(() => { /** 开发时设为 true 可始终使用模拟数据查看一二三层 UI */ - const USE_MOCK_CATEGORY = true + const USE_MOCK_CATEGORY = false if (USE_MOCK_CATEGORY) { categoryTree.value = MOCK_CATEGORY_TREE initCategorySelection() } else { - getCategoryTree() + getPmTagMain() .then((res) => { if (res.code === 0 || res.code === 200) { const data = res.data @@ -586,7 +615,7 @@ onMounted(() => { if (!entries[0]?.isIntersecting) return loadMore() }, - { root: null, rootMargin: `${SCROLL_LOAD_THRESHOLD}px`, threshold: 0 } + { root: null, rootMargin: `${SCROLL_LOAD_THRESHOLD}px`, threshold: 0 }, ) observer.observe(sentinel) } @@ -715,7 +744,6 @@ onUnmounted(() => { padding: 0; } - .home-subtitle { margin-bottom: 40px; } @@ -802,7 +830,9 @@ onUnmounted(() => { .home-search-overlay-enter-active, .home-search-overlay-leave-active { - transition: opacity 0.15s ease, transform 0.15s ease; + transition: + opacity 0.15s ease, + transform 0.15s ease; } .home-search-overlay-enter-from, @@ -935,7 +965,9 @@ onUnmounted(() => { color: #64748b; font-size: 12px; cursor: pointer; - transition: background-color 0.2s, color 0.2s; + transition: + background-color 0.2s, + color 0.2s; } .home-category-icon-item:hover { diff --git a/src/views/Login.vue b/src/views/Login.vue index 3c6ddbc..e278344 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -140,7 +140,7 @@ const connectWithWallet = async () => { uri: origin, version: '1', chainId: chainId, - nonce, + nonce, }) const message = siwe.prepareMessage() // 去掉message 头部的http:// @@ -175,7 +175,7 @@ const connectWithWallet = async () => { nonce, signature, walletAddress, - } + }, ) console.log('Login API response:', loginData) diff --git a/src/views/Trade.vue b/src/views/Trade.vue index ca2d01d..4c11c7d 100644 --- a/src/views/Trade.vue +++ b/src/views/Trade.vue @@ -3,7 +3,7 @@

Trading

- + diff --git a/src/views/TradeDetail.vue b/src/views/TradeDetail.vue index 0efeaed..5e3bf2a 100644 --- a/src/views/TradeDetail.vue +++ b/src/views/TradeDetail.vue @@ -80,18 +80,21 @@
-
+
- +
{{ item.user }} {{ item.action }} - + {{ item.amount }} {{ item.side }} at {{ item.price }} @@ -113,10 +116,7 @@
- +
@@ -130,7 +130,12 @@ import * as echarts from 'echarts' import type { ECharts } from 'echarts' import OrderBook from '../components/OrderBook.vue' import TradeComponent from '../components/TradeComponent.vue' -import { findPmEvent, getMarketId, type FindPmEventParams, type PmEventListItem } from '../api/event' +import { + findPmEvent, + getMarketId, + type FindPmEventParams, + type PmEventListItem, +} from '../api/event' import { useUserStore } from '../stores/user' /** @@ -198,14 +203,14 @@ async function loadEventDetail() { const slugFromQuery = (route.query.slug as string)?.trim() const params: FindPmEventParams = { id: isNumericId ? numId : undefined, - slug: isNumericId ? (slugFromQuery || undefined) : idStr, + slug: isNumericId ? slugFromQuery || undefined : idStr, } detailError.value = null detailLoading.value = true try { const res = await findPmEvent(params, { - headers: userStore.getAuthHeaders() + headers: userStore.getAuthHeaders(), }) if (res.code === 0 || res.code === 200) { eventDetail.value = res.data ?? null @@ -313,12 +318,72 @@ interface ActivityItem { time: number } const activityList = ref([ - { id: 'a1', user: 'Scottp1887', avatarClass: 'avatar-gradient-1', action: 'bought', side: 'Yes', amount: 914, price: '32.8¢', total: '$300', time: Date.now() - 10 * 60 * 1000 }, - { id: 'a2', user: 'Outrageous-Budd...', avatarClass: 'avatar-gradient-2', action: 'sold', side: 'No', amount: 20, price: '70.0¢', total: '$14', time: Date.now() - 29 * 60 * 1000 }, - { id: 'a3', user: '0xbc7db42d5ea9a...', avatarClass: 'avatar-gradient-3', action: 'bought', side: 'Yes', amount: 5, price: '68.0¢', total: '$3.40', time: Date.now() - 60 * 60 * 1000 }, - { id: 'a4', user: 'FINNISH-FEMBOY', avatarClass: 'avatar-gradient-4', action: 'sold', side: 'No', amount: 57, price: '35.0¢', total: '$20', time: Date.now() - 2 * 60 * 60 * 1000 }, - { id: 'a5', user: 'CryptoWhale', avatarClass: 'avatar-gradient-1', action: 'bought', side: 'No', amount: 143, price: '28.0¢', total: '$40', time: Date.now() - 3 * 60 * 60 * 1000 }, - { id: 'a6', user: 'PolyTrader', avatarClass: 'avatar-gradient-2', action: 'sold', side: 'Yes', amount: 30, price: '72.0¢', total: '$21.60', time: Date.now() - 5 * 60 * 60 * 1000 }, + { + id: 'a1', + user: 'Scottp1887', + avatarClass: 'avatar-gradient-1', + action: 'bought', + side: 'Yes', + amount: 914, + price: '32.8¢', + total: '$300', + time: Date.now() - 10 * 60 * 1000, + }, + { + id: 'a2', + user: 'Outrageous-Budd...', + avatarClass: 'avatar-gradient-2', + action: 'sold', + side: 'No', + amount: 20, + price: '70.0¢', + total: '$14', + time: Date.now() - 29 * 60 * 1000, + }, + { + id: 'a3', + user: '0xbc7db42d5ea9a...', + avatarClass: 'avatar-gradient-3', + action: 'bought', + side: 'Yes', + amount: 5, + price: '68.0¢', + total: '$3.40', + time: Date.now() - 60 * 60 * 1000, + }, + { + id: 'a4', + user: 'FINNISH-FEMBOY', + avatarClass: 'avatar-gradient-4', + action: 'sold', + side: 'No', + amount: 57, + price: '35.0¢', + total: '$20', + time: Date.now() - 2 * 60 * 60 * 1000, + }, + { + id: 'a5', + user: 'CryptoWhale', + avatarClass: 'avatar-gradient-1', + action: 'bought', + side: 'No', + amount: 143, + price: '28.0¢', + total: '$40', + time: Date.now() - 3 * 60 * 60 * 1000, + }, + { + id: 'a6', + user: 'PolyTrader', + avatarClass: 'avatar-gradient-2', + action: 'sold', + side: 'Yes', + amount: 30, + price: '72.0¢', + total: '$21.60', + time: Date.now() - 5 * 60 * 60 * 1000, + }, ]) const filteredActivity = computed(() => { @@ -461,7 +526,10 @@ function buildOption(chartData: [number, number][], containerWidth?: number) { tooltip: { trigger: 'axis', formatter: function (params: unknown) { - const p = (Array.isArray(params) ? params[0] : params) as { name: string | number; value: unknown } + const p = (Array.isArray(params) ? params[0] : params) as { + name: string | number + value: unknown + } const date = new Date(p.name as number) const val = Array.isArray(p.value) ? p.value[1] : p.value return ( @@ -525,7 +593,8 @@ function initChart() { function updateChartData() { data.value = generateData(selectedTimeRange.value) const w = chartContainerRef.value?.clientWidth - if (chartInstance) chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] }) + if (chartInstance) + chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] }) } function selectTimeRange(range: string) { @@ -535,13 +604,19 @@ function selectTimeRange(range: string) { function getMaxPoints(range: string): number { switch (range) { - case '1H': return 60 - case '6H': return 36 - case '1D': return 24 - case '1W': return 7 + case '1H': + return 60 + case '6H': + return 36 + case '1D': + return 24 + case '1W': + return 7 case '1M': - case 'ALL': return 30 - default: return 24 + case 'ALL': + return 30 + default: + return 24 } } @@ -556,7 +631,8 @@ function startDynamicUpdate() { const max = getMaxPoints(selectedTimeRange.value) data.value = list.slice(-max) const w = chartContainerRef.value?.clientWidth - if (chartInstance) chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] }) + if (chartInstance) + chartInstance.setOption(buildOption(data.value, w), { replaceMerge: ['series'] }) }, 3000) } @@ -580,7 +656,7 @@ const handleResize = () => { watch( () => route.params.id, () => loadEventDetail(), - { immediate: false } + { immediate: false }, ) onMounted(() => { @@ -1030,8 +1106,13 @@ onUnmounted(() => { } @keyframes live-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } } .activity-list { diff --git a/src/views/Wallet.vue b/src/views/Wallet.vue index efc9f9a..1d20bab 100644 --- a/src/views/Wallet.vue +++ b/src/views/Wallet.vue @@ -88,7 +88,13 @@ prepend-inner-icon="mdi-magnify" />