Compare commits
No commits in common. "a084780180857d52056ae67d854b807072a05ca6" and "8c455ba00a10369d8f84275b2a404325ad70bbed" have entirely different histories.
a084780180
...
8c455ba00a
@ -1,70 +0,0 @@
|
||||
---
|
||||
description: Trae 开发规则,定义如何使用 Trae 进行项目开发和维护
|
||||
globs: src/**/*
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Trae 开发规则
|
||||
|
||||
**使用 Trae 进行开发时必须遵循的规则**,确保代码质量和开发效率。
|
||||
|
||||
## 开发流程
|
||||
|
||||
1. **需求分析**:理解用户需求,明确功能边界
|
||||
2. **代码实现**:按照项目框架规范实现功能
|
||||
3. **测试验证**:确保功能正常工作
|
||||
4. **文档更新**:同步更新相关文档
|
||||
5. **代码检查**:运行 lint 和类型检查
|
||||
|
||||
## 代码规范
|
||||
|
||||
### 1. 代码风格
|
||||
- 遵循项目现有的代码风格(Prettier + ESLint)
|
||||
- 使用 TypeScript 严格模式
|
||||
- 避免使用 `any` 类型
|
||||
- 函数和变量命名清晰,符合语义
|
||||
|
||||
### 2. 组件开发
|
||||
- 使用 Vue 3 Composition API
|
||||
- 组件文件使用 PascalCase 命名
|
||||
- 样式使用 `<style scoped>`
|
||||
- 组件 props 和 emits 必须有类型定义
|
||||
|
||||
### 3. API 开发
|
||||
- 遵循 XTrader API 接入规范
|
||||
- 使用统一的请求封装(`src/api/request.ts`)
|
||||
- 为每个接口定义完整的 TypeScript 类型
|
||||
- 处理错误和边界情况
|
||||
|
||||
## 工具使用
|
||||
|
||||
### 1. 代码生成
|
||||
- 使用 Trae 的代码生成功能创建组件、接口等
|
||||
- 生成的代码必须符合项目规范
|
||||
- 手动调整生成的代码,确保质量
|
||||
|
||||
### 2. 调试工具
|
||||
- 使用 Trae 的调试功能排查问题
|
||||
- 利用 Trae 的搜索功能快速定位代码
|
||||
- 结合浏览器开发工具进行前端调试
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **模块化开发**:将功能拆分为小的、可维护的模块
|
||||
2. **代码复用**:提取公共组件和工具函数
|
||||
3. **性能优化**:注意组件渲染性能,避免不必要的重渲染
|
||||
4. **安全性**:注意处理用户输入,避免安全漏洞
|
||||
5. **可访问性**:确保界面对所有用户可访问
|
||||
|
||||
## 检查清单
|
||||
|
||||
开发完成后,确保:
|
||||
|
||||
- [ ] 代码符合项目框架规范
|
||||
- [ ] 功能正常工作
|
||||
- [ ] 文档已同步更新
|
||||
- [ ] 无 lint 和类型错误
|
||||
- [ ] 代码风格一致
|
||||
- [ ] 测试通过
|
||||
|
||||
遵循这些规则,使用 Trae 可以更高效、更规范地进行项目开发。
|
||||
@ -1,154 +0,0 @@
|
||||
---
|
||||
name: trae-development
|
||||
version: 1.0.0
|
||||
description: 提供 Trae 开发相关的功能,包括代码生成、调试、搜索等工具的使用指南。当用户需要使用 Trae 进行开发时使用此技能。
|
||||
author: Trae Team
|
||||
---
|
||||
|
||||
# Trae 开发技能
|
||||
|
||||
## 功能介绍
|
||||
|
||||
本技能提供 Trae 开发环境的使用指南,帮助开发者更高效地使用 Trae 进行项目开发和维护。
|
||||
|
||||
## 使用场景
|
||||
|
||||
- 代码生成:创建新组件、接口、页面等
|
||||
- 代码搜索:快速定位代码和文件
|
||||
- 代码分析:理解现有代码结构
|
||||
- 调试工具:排查和解决问题
|
||||
- 项目管理:项目结构和规范管理
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 代码生成
|
||||
|
||||
#### 组件生成
|
||||
|
||||
**使用方式**:
|
||||
- 在命令面板中输入 `Trae: Generate Component`
|
||||
- 选择组件类型(如 Vue 组件、TypeScript 接口等)
|
||||
- 输入组件名称和相关选项
|
||||
- Trae 会自动生成符合项目规范的代码
|
||||
|
||||
**生成的组件结构**:
|
||||
- Vue 组件:包含 template、script setup 和 scoped style
|
||||
- TypeScript 接口:包含完整的类型定义
|
||||
- API 模块:包含请求函数和类型定义
|
||||
|
||||
### 2. 代码搜索
|
||||
|
||||
**使用方式**:
|
||||
- 在命令面板中输入 `Trae: Search Code`
|
||||
- 输入搜索关键词
|
||||
- Trae 会显示所有匹配的代码片段
|
||||
- 点击结果可直接跳转到对应文件
|
||||
|
||||
**搜索范围**:
|
||||
- 项目代码库
|
||||
- 依赖库
|
||||
- 文档
|
||||
|
||||
### 3. 代码分析
|
||||
|
||||
**使用方式**:
|
||||
- 在命令面板中输入 `Trae: Analyze Code`
|
||||
- 选择分析类型(如依赖分析、性能分析等)
|
||||
- Trae 会生成详细的分析报告
|
||||
|
||||
**分析内容**:
|
||||
- 代码复杂度
|
||||
- 依赖关系
|
||||
- 性能瓶颈
|
||||
- 代码质量
|
||||
|
||||
### 4. 调试工具
|
||||
|
||||
**使用方式**:
|
||||
- 在命令面板中输入 `Trae: Debug Code`
|
||||
- 设置断点或选择调试模式
|
||||
- 运行调试会话
|
||||
- Trae 会提供详细的调试信息
|
||||
|
||||
**调试功能**:
|
||||
- 断点设置
|
||||
- 变量监视
|
||||
- 调用栈查看
|
||||
- 表达式求值
|
||||
|
||||
### 5. 项目管理
|
||||
|
||||
**使用方式**:
|
||||
- 在命令面板中输入 `Trae: Project Management`
|
||||
- 选择管理功能(如依赖管理、版本控制等)
|
||||
- 执行相应的管理操作
|
||||
|
||||
**管理功能**:
|
||||
- 依赖安装和更新
|
||||
- 版本控制操作
|
||||
- 项目配置管理
|
||||
- 构建和部署
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **代码组织**:
|
||||
- 按照项目框架规范组织代码
|
||||
- 使用清晰的命名和目录结构
|
||||
- 遵循模块化开发原则
|
||||
|
||||
2. **代码质量**:
|
||||
- 定期运行 lint 和类型检查
|
||||
- 编写单元测试
|
||||
- 保持代码风格一致
|
||||
|
||||
3. **开发效率**:
|
||||
- 使用 Trae 的代码生成功能
|
||||
- 利用搜索功能快速定位代码
|
||||
- 结合调试工具排查问题
|
||||
|
||||
4. **团队协作**:
|
||||
- 遵循统一的代码规范
|
||||
- 及时更新文档
|
||||
- 提交有意义的代码注释
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何使用 Trae 生成组件?
|
||||
|
||||
**A**: 在命令面板中输入 `Trae: Generate Component`,选择组件类型和配置选项,Trae 会自动生成符合项目规范的代码。
|
||||
|
||||
### Q: 如何搜索项目中的代码?
|
||||
|
||||
**A**: 在命令面板中输入 `Trae: Search Code`,输入搜索关键词,Trae 会显示所有匹配的代码片段。
|
||||
|
||||
### Q: 如何调试代码?
|
||||
|
||||
**A**: 在命令面板中输入 `Trae: Debug Code`,设置断点或选择调试模式,运行调试会话查看详细信息。
|
||||
|
||||
### Q: 如何分析项目依赖?
|
||||
|
||||
**A**: 在命令面板中输入 `Trae: Analyze Code`,选择依赖分析,Trae 会生成详细的依赖关系报告。
|
||||
|
||||
## 配置选项
|
||||
|
||||
Trae 开发技能支持以下配置选项:
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| `trae.codeGeneration.style` | 代码生成风格 | `project` |
|
||||
| `trae.search.scope` | 搜索范围 | `project` |
|
||||
| `trae.analysis.level` | 分析深度 | `medium` |
|
||||
| `trae.debug.enabled` | 启用调试 | `true` |
|
||||
|
||||
## 版本历史
|
||||
|
||||
- **1.0.0**:初始版本,包含代码生成、搜索、分析和调试功能
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 使用 Trae 开发技能时,请确保项目已经正确初始化
|
||||
- 生成的代码可能需要根据具体需求进行调整
|
||||
- 调试功能需要在开发模式下使用
|
||||
- 分析功能可能会消耗一定的系统资源
|
||||
|
||||
通过合理使用 Trae 开发技能,可以显著提高开发效率和代码质量,为项目开发提供有力支持。
|
||||
@ -16,38 +16,19 @@ description: Interprets the XTrader API from the Swagger 2.0 spec at https://api
|
||||
### 强制动作序列(必须依次执行)
|
||||
|
||||
1. **收到对接请求** → 立即用 `mcp_web_fetch` 或 `curl` 获取 `https://api.xtrader.vip/swagger/doc.json`
|
||||
2. **检查接口是否存在** → 若 `paths["<path>"]` 或 `paths["<path>"]["<method>"]` **不存在**,则:
|
||||
- ❌ **禁止**自行猜测或虚构数据结构并自动对接
|
||||
- ✅ **必须**在对话中明确告知用户「该接口在 doc.json 中不存在」
|
||||
- ✅ **必须**向用户询问:
|
||||
- 是否仍要对接?若对接,请提供**请求参数**与**响应数据结构**(或示例 JSON)
|
||||
- 用户可选择**不对接**,则流程终止
|
||||
- 仅在用户明确提供数据结构或确认对接后,才可进入第二步
|
||||
3. **第一步**(接口存在时)→ 解析 `paths["<path>"]["<method>"]` 与 `definitions`,**在对话中输出**:
|
||||
2. **第一步** → 解析 `paths["<path>"]["<method>"]` 与 `definitions`,**在对话中输出**:
|
||||
- 请求参数表(Query/Body/鉴权)
|
||||
- 响应参数表(200 schema)
|
||||
- data 的 `$ref` 对应 definitions 的**完整字段表**
|
||||
- 输出后**明确标注「第一步完成」**
|
||||
4. **第二步** → 在 `src/api/` 中根据第一步表格(或用户提供的数据结构)定义 TypeScript 类型,**不得在第一步完成前执行**
|
||||
5. **第三步** → 实现请求函数并集成到页面,**不得在第二步完成前执行**
|
||||
3. **第二步** → 在 `src/api/` 中根据第一步表格定义 TypeScript 类型,**不得在第一步完成前执行**
|
||||
4. **第三步** → 实现请求函数并集成到页面,**不得在第二步完成前执行**
|
||||
|
||||
### 禁止行为
|
||||
|
||||
- ❌ 在对话中输出第一步结果**之前**写任何 `src/api/` 或 `src/views/` 业务代码
|
||||
- ❌ 跳过第一步直接定义类型或实现请求
|
||||
- ❌ 合并步骤(如边输出边写代码)
|
||||
- ❌ **接口文档不存在时**:自行猜测数据结构并自动对接;必须向用户询问后再决定是否对接
|
||||
|
||||
### 接口文档不存在时的处理流程
|
||||
|
||||
当 `paths["<path>"]` 在 doc.json 中**不存在**时:
|
||||
|
||||
1. **立即停止**:不写任何对接代码,不猜测数据结构
|
||||
2. **明确告知**:在对话中说明「该接口在 Swagger doc.json 中不存在」
|
||||
3. **向用户询问**:
|
||||
- 是否仍要对接?若对接,请提供请求参数与响应数据结构(或示例 JSON)
|
||||
- 用户可选择**不对接**,则流程终止
|
||||
4. **仅在用户明确提供**后,才继续执行第二步、第三步
|
||||
|
||||
### 第一步输出模板(必须包含)
|
||||
|
||||
@ -182,7 +163,6 @@ Swagger UI 页面(如 [PmEvent findPmEvent](https://api.xtrader.vip/swagger/in
|
||||
|
||||
## 简要检查清单
|
||||
|
||||
- [ ] **接口存在性**:若 doc.json 中无该 path,已向用户询问数据结构,未擅自猜测对接
|
||||
- [ ] **按规范顺序**:已先列出请求参数与响应参数,再建 Model,最后集成到页面
|
||||
- [ ] 规范 URL 使用 `https://api.xtrader.vip/swagger/doc.json`,或本地缓存与之一致
|
||||
- [ ] 请求 path、method、query/body 与 `paths` 一致
|
||||
|
||||
@ -17,24 +17,7 @@
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| `PmTagMainItem` | 接口返回的 PmTag 结构,含 children |
|
||||
| `CategoryTreeNode` | 前端使用的树节点,含 id、label、slug、icon、sectionTitle、children、tagIds |
|
||||
|
||||
### CategoryTreeNode.tagIds
|
||||
|
||||
从后端 `PmTagCatalogItem.tagId` 字段提取的标签 ID 列表,用于事件筛选:
|
||||
|
||||
```typescript
|
||||
interface CategoryTreeNode {
|
||||
// ... 其他字段
|
||||
/** 关联的标签 ID 列表,用于事件筛选 */
|
||||
tagIds?: number[]
|
||||
}
|
||||
```
|
||||
|
||||
**提取逻辑**:
|
||||
- 如果 `tagId` 是数字数组,直接使用(`[1351, 1368]`)
|
||||
- 如果 `tagId` 是单个数字,包装为数组(`1351` → `[1351]`)
|
||||
- 如果 `tagId` 是对象或其他类型,忽略
|
||||
| `CategoryTreeNode` | 前端使用的树节点,含 id、label、slug、icon、sectionTitle、children |
|
||||
|
||||
## 使用方式
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ import {
|
||||
} from '@/api/event'
|
||||
|
||||
// 获取列表
|
||||
const res = await getPmEventPublic({ page: 1, pageSize: 10, tagIds: [1, 2] })
|
||||
const res = await getPmEventPublic({ page: 1, pageSize: 10, tagSlug: 'crypto' })
|
||||
const cards = res.data.list.map(mapEventItemToCard)
|
||||
|
||||
// 获取详情(需鉴权)
|
||||
@ -49,18 +49,3 @@ clearEventListCache()
|
||||
1. **新增筛选参数**:在 `GetPmEventListParams` 中增加字段,并在 `getPmEventPublic` 的 query 中传入
|
||||
2. **缓存策略**:可改为 sessionStorage 或带 TTL 的缓存
|
||||
3. **多选项展示**:`mapEventItemToCard` 已支持 multi 类型,可扩展 `EventCardOutcome` 字段
|
||||
|
||||
## 参数传递方式
|
||||
|
||||
### tagIds 参数(数组)
|
||||
|
||||
`tagIds` 使用传统数组方式传递,不再是逗号分隔的字符串:
|
||||
|
||||
```typescript
|
||||
// 正确方式 - 直接传递数组
|
||||
const res = await getPmEventPublic({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
tagIds: [1, 2, 3] // 会作为多个同名参数传递:?tagIds=1&tagIds=2&tagIds=3
|
||||
})
|
||||
```
|
||||
|
||||
@ -13,38 +13,15 @@
|
||||
| market | object | 市场信息,含 marketId、clobTokenIds、outcomes、outcomePrices 等 |
|
||||
| initialOption | 'yes' \| 'no' | 初始选中的选项 |
|
||||
| embeddedInSheet | boolean | 是否嵌入底部弹窗(移动端) |
|
||||
| positions | TradePositionItem[] | 当前市场持仓列表,用于计算可合并份额(Merge 功能) |
|
||||
|
||||
### TradePositionItem
|
||||
|
||||
```typescript
|
||||
interface TradePositionItem {
|
||||
id: string
|
||||
outcomeWord: 'Yes' | 'No' // 持仓方向
|
||||
shares: string // 份额展示文本,如 "5 shares"
|
||||
sharesNum?: number // 份数数值(可选,优先使用)
|
||||
}
|
||||
```
|
||||
|
||||
**可合并份额计算逻辑**:取 Yes 和 No 持仓份数的最小值(成对数量)。例如:Yes 持仓 10 份,No 持仓 8 份,则可合并份额为 8。
|
||||
|
||||
## 核心能力
|
||||
|
||||
- Buy/Sell Tab 切换
|
||||
- Market/Limit 类型、Merge/Split 菜单
|
||||
- **Buy 模式 Amount 区**:无论余额是否充足,均显示 Amount 标签、Balance、**可编辑金额输入框**(v-text-field,带 $ 前缀,variant="outlined")、+$1/+$20/+$100/Max 快捷按钮(桌面端、嵌入弹窗、移动端弹窗一致)
|
||||
- 输入框支持直接输入金额(>= 0,支持小数)
|
||||
- 事件处理:`onAmountInput`、`onAmountKeydown`、`onAmountPaste`
|
||||
- **Buy 模式 Amount 区**:无论余额是否充足,均显示 Amount 标签、Balance、金额输入、+$1/+$20/+$100/Max 快捷按钮(桌面端、嵌入弹窗、移动端弹窗一致)
|
||||
- 余额不足时 Buy 显示 Deposit 按钮
|
||||
- 25%/50%/Max 快捷份额
|
||||
- **Sell 模式 UI 优化**:
|
||||
- Shares 标签与 Max shares 提示同行显示(`max-shares-inline`)
|
||||
- 输入框独占一行(`shares-input-wrapper`)
|
||||
- 25%/50%/Max 按钮独立一行(`sell-shares-buttons`)
|
||||
- 整体布局更清晰:`Shares Max: 2` → `[输入框]` → `[25%][50%][Max]`
|
||||
- 调用 market API 下单、Split、Merge
|
||||
- **合并/拆分成功后触发事件**:`mergeSuccess`、`splitSuccess`,父组件监听后可刷新持仓列表
|
||||
- **401 权限错误提示**:通过 `useAuthError().formatAuthError` 统一处理,未登录显示「请先登录」,已登录显示「权限不足」
|
||||
|
||||
## 使用方式
|
||||
|
||||
@ -52,30 +29,10 @@ interface TradePositionItem {
|
||||
<TradeComponent
|
||||
:market="tradeMarketPayload"
|
||||
:initial-option="tradeInitialOption"
|
||||
:positions="marketPositions"
|
||||
embedded-in-sheet
|
||||
/>
|
||||
```
|
||||
|
||||
**positions 示例**:
|
||||
```typescript
|
||||
const marketPositions = [
|
||||
{ id: '1', outcomeWord: 'Yes', shares: '10 shares', sharesNum: 10 },
|
||||
{ id: '2', outcomeWord: 'No', shares: '8 shares', sharesNum: 8 }
|
||||
]
|
||||
// 可合并份额 = min(10, 8) = 8
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
| 事件名 | 说明 |
|
||||
|--------|------|
|
||||
| `optionChange` | 选项切换(yes/no) |
|
||||
| `orderSuccess` | 下单成功 |
|
||||
| `mergeSuccess` | 合并份额成功,父组件应监听并刷新持仓 |
|
||||
| `splitSuccess` | 拆分份额成功,父组件应监听并刷新持仓 |
|
||||
| `submit` | 提交订单前的回调,携带订单 payload |
|
||||
|
||||
## 国际化
|
||||
|
||||
Merge/Split 弹窗文案均通过 `trade.*` 键国际化:
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
# useAuthError.ts
|
||||
|
||||
**路径**:`src/composables/useAuthError.ts`
|
||||
|
||||
## 功能用途
|
||||
|
||||
统一处理 HTTP 401(Unauthorized)等权限相关错误,根据登录状态返回用户友好的提示文案:未登录时提示「请先登录」,已登录时提示「权限不足」。
|
||||
|
||||
## 核心能力
|
||||
|
||||
- `formatAuthError(err, fallback)`:将异常转换为展示文案
|
||||
- 若错误信息包含 `401` 或 `Unauthorized`:根据 `userStore.isLoggedIn` 返回 `error.pleaseLogin` 或 `error.insufficientPermission`
|
||||
- 否则返回原始错误信息或 `fallback`
|
||||
|
||||
## 使用方式
|
||||
|
||||
```typescript
|
||||
import { useAuthError } from '@/composables/useAuthError'
|
||||
|
||||
const { formatAuthError } = useAuthError()
|
||||
|
||||
try {
|
||||
await someApiCall()
|
||||
} catch (e) {
|
||||
errorMessage.value = formatAuthError(e, t('error.requestFailed'))
|
||||
}
|
||||
```
|
||||
|
||||
## 国际化
|
||||
|
||||
依赖 `error.pleaseLogin`、`error.insufficientPermission`,在各 `locales/*.json` 的 `error` 下配置。
|
||||
|
||||
## 扩展方式
|
||||
|
||||
1. **扩展状态码**:在 `formatAuthError` 中增加对 403 等的判断
|
||||
2. **统一拦截**:在 `request.ts` 层统一处理 401,自动跳转登录或弹 toast
|
||||
@ -1,69 +1,27 @@
|
||||
# Home.vue
|
||||
|
||||
**路径**:`src/views/Home.vue`
|
||||
**路由**:`/`,name: `home`
|
||||
|
||||
## 功能用途
|
||||
|
||||
首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多。
|
||||
首页,展示分类 Tab、搜索、事件卡片列表。支持三层分类、下拉刷新、无限滚动、搜索历史,卡片支持单一/多选项展示。
|
||||
|
||||
## 核心能力
|
||||
|
||||
- **分类导航**:三层级分类选择(一级 Tab、二级图标、三级 Tab)
|
||||
- **事件列表**:卡片式展示,支持下拉刷新、触底加载
|
||||
- **搜索**:可按关键词搜索事件
|
||||
- **分类筛选**:选中分类后,自动提取所有层级节点的 `tagIds` 进行事件筛选
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
PmTagCatalogItem.tagId = [1351]
|
||||
↓ mapCatalogToTreeNode
|
||||
CategoryTreeNode.tagIds = [1351]
|
||||
↓ 用户选择分类(如:政治 → 特朗普)
|
||||
activeTagIds = [1351, 1368] // 合并所有选中层级的 tagIds
|
||||
↓ getPmEventPublic
|
||||
?tagIds=1351&tagIds=1368
|
||||
```
|
||||
|
||||
## 核心计算属性
|
||||
|
||||
### activeTagIds
|
||||
|
||||
收集所有选中层级节点的 `tagIds`(含父级),用于事件筛选:
|
||||
|
||||
```typescript
|
||||
const activeTagIds = computed(() => {
|
||||
const activeIds = layerActiveValues.value
|
||||
const tagIdSet = new Set<number>()
|
||||
|
||||
// 遍历每一层选中的节点,收集所有 tagIds(含父级)
|
||||
let currentNodes = filterVisible(categoryTree.value)
|
||||
for (let i = 0; i < activeIds.length; i++) {
|
||||
const selectedId = activeIds[i]
|
||||
if (!selectedId) continue
|
||||
|
||||
const node = currentNodes.find((n) => n.id === selectedId)
|
||||
if (node?.tagIds && node.tagIds.length > 0) {
|
||||
node.tagIds.forEach((id) => tagIdSet.add(id))
|
||||
}
|
||||
|
||||
currentNodes = filterVisible(node?.children)
|
||||
}
|
||||
|
||||
return Array.from(tagIdSet) // 去重后的数组
|
||||
})
|
||||
```
|
||||
|
||||
**示例**:
|
||||
- 选中「政治」(tagIds: [1351])→ activeTagIds = [1351]
|
||||
- 选中「政治 → 特朗普」(tagIds: [1351] + [1368])→ activeTagIds = [1351, 1368]
|
||||
- 分类:第一层 Tab、第二层图标、第三层 Tab,从 `getPmTagMain` 获取
|
||||
- 搜索:展开浮层、历史记录、`useSearchHistory`
|
||||
- 列表:`getPmEventPublic` 分页、`mapEventItemToCard` 映射、`MarketCard` 渲染
|
||||
- 缓存:`eventListCache` 切换页面时复用,下拉刷新时清空
|
||||
- Keep-alive:`Home` 被 include,切换回来时保留状态
|
||||
|
||||
## 使用方式
|
||||
|
||||
无需手动调用,路由 `/` 自动加载。
|
||||
- 访问 `/` 即可进入
|
||||
- 分类切换、搜索、下拉刷新、滚动加载均自动工作
|
||||
|
||||
## 扩展方式
|
||||
|
||||
1. **新增分类层级**:修改 `MAX_LAYER` 常量,调整模板渲染逻辑
|
||||
2. **自定义筛选逻辑**:修改 `activeTagIds` 计算属性
|
||||
3. **列表缓存策略**:调整 `getEventListCache` / `setEventListCache`
|
||||
1. **新增筛选**:在搜索浮层旁增加筛选按钮,修改 `getPmEventPublic` 的 params
|
||||
2. **骨架屏**:在 loading 时展示 `v-skeleton-loader`
|
||||
3. **空状态**:列表为空时展示空状态插画与文案
|
||||
|
||||
@ -11,26 +11,15 @@
|
||||
|
||||
- 分时图:ECharts 渲染,支持 Past、时间粒度切换
|
||||
- 订单簿:`OrderBook` 组件,通过 **ClobSdk** 对接 CLOB WebSocket 实时数据(全量快照、增量更新、成交推送)
|
||||
- 交易:`TradeComponent`,传入 `market`、`initialOption`、`positions`(持仓数据)
|
||||
- 持仓列表:通过 `getPositionList` 获取当前市场持仓,传递给 `TradeComponent` 用于计算可合并份额
|
||||
- 限价订单:通过 `getOrderList` 获取当前市场未成交限价单,支持撤单
|
||||
- 交易:`TradeComponent`,传入 `market`、`initialOption`
|
||||
- 移动端:底部栏 + `v-bottom-sheet` 嵌入 `TradeComponent`
|
||||
- Merge/Split:通过 `TradeComponent` 或底部菜单触发,成功后监听 `mergeSuccess`/`splitSuccess` 事件刷新持仓
|
||||
- **401 权限错误**:加载详情失败时,通过 `useAuthError().formatAuthError` 统一提示「请先登录」或「权限不足」
|
||||
- Merge/Split:通过 `TradeComponent` 或底部菜单触发
|
||||
|
||||
## 使用方式
|
||||
|
||||
- 从首页卡片点击进入,或直接访问 `/trade-detail/123`
|
||||
- 路由参数 `id` 为 Event ID,用于 `findPmEvent`
|
||||
|
||||
## 持仓刷新机制
|
||||
|
||||
- **下单成功**:`TradeComponent` 触发 `orderSuccess` → `onOrderSuccess()` 刷新持仓和未成交订单
|
||||
- **合并成功**:`TradeComponent` 触发 `mergeSuccess` → `onMergeSuccess()` 刷新持仓,显示 toast 提示
|
||||
- **拆分成功**:`TradeComponent` 触发 `splitSuccess` → `onSplitSuccess()` 刷新持仓
|
||||
|
||||
持仓刷新调用 `loadMarketPositions()`,通过 `/clob/position/getPositionList` 接口获取最新持仓数据,并根据 `tokenId` 匹配 Yes/No 方向。
|
||||
|
||||
## 扩展方式
|
||||
|
||||
1. **订单簿**:已通过 `sdk/clobSocket.ts` 的 ClobSdk 对接 CLOB WebSocket,使用 **Yes/No token ID** 订阅 `price_size_all`、`price_size_delta`、`trade` 消息
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
- Profit/Loss 卡片:时间范围切换、ECharts 图表
|
||||
- Tab:Positions、Open orders、History
|
||||
- DepositDialog、WithdrawDialog 组件
|
||||
- **401 权限错误**:取消订单等接口失败时,通过 `useAuthError().formatAuthError` 统一提示「请先登录」或「权限不足」
|
||||
|
||||
## 使用方式
|
||||
|
||||
|
||||
358
sdk/approve.ts
358
sdk/approve.ts
@ -1,358 +0,0 @@
|
||||
// 跨链USDT授权核心方法 - 修复版(无需外部ethers库)
|
||||
// 适用于支持EIP-1193标准的钱包(如MetaMask)
|
||||
|
||||
interface ChainConfig {
|
||||
chainId: string;
|
||||
name: string;
|
||||
usdtAddress: string;
|
||||
rpcUrl: string;
|
||||
explorer: string;
|
||||
}
|
||||
|
||||
// 链配置
|
||||
const chains: Record<string, ChainConfig> = {
|
||||
eth: {
|
||||
chainId: '0x1', // Ethereum Mainnet
|
||||
name: 'Ethereum',
|
||||
usdtAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
||||
rpcUrl: 'https://mainnet.infura.io/v3/',
|
||||
explorer: 'https://etherscan.io'
|
||||
},
|
||||
bnb: {
|
||||
chainId: '0x38', // Binance Smart Chain Mainnet
|
||||
name: 'Binance Smart Chain',
|
||||
usdtAddress: '0x55d398326f99059fF775485246999027B3197955',
|
||||
rpcUrl: 'https://bsc-dataseed.binance.org/',
|
||||
explorer: 'https://bscscan.com'
|
||||
},
|
||||
polygon: {
|
||||
chainId: '0x89', // Polygon Mainnet
|
||||
name: 'Polygon',
|
||||
usdtAddress: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
|
||||
rpcUrl: 'https://polygon-rpc.com/',
|
||||
explorer: 'https://polygonscan.com'
|
||||
}
|
||||
};
|
||||
|
||||
// USDT ABI
|
||||
const USDT_ABI = [
|
||||
"function approve(address spender, uint256 value) public returns (bool)",
|
||||
"function allowance(address owner, address spender) view returns (uint256)",
|
||||
"function balanceOf(address owner) view returns (uint256)"
|
||||
];
|
||||
|
||||
// 将数字转换为十六进制字符串
|
||||
function numberToHex(num: number | string): string {
|
||||
return '0x' + BigInt(num).toString(16);
|
||||
}
|
||||
|
||||
// 将十进制数字转换为带指定小数位的整数形式
|
||||
function parseUnits(value: string, decimals: number): string {
|
||||
const [integerPart = '0', decimalPart = ''] = value.split('.');
|
||||
let result = integerPart.replace(/^0+/, '') || '0'; // 移除前导零
|
||||
|
||||
if (decimalPart.length > decimals) {
|
||||
throw new Error(`小数位数超过限制: ${decimals}`);
|
||||
}
|
||||
|
||||
const paddedDecimal = decimalPart.padEnd(decimals, '0');
|
||||
result += paddedDecimal;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 验证钱包连接
|
||||
function validateWallet(): boolean {
|
||||
if (typeof window === 'undefined' || typeof (window as any).ethereum === 'undefined') {
|
||||
throw new Error('请在支持以太坊的钱包浏览器中运行此代码');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取当前钱包账户
|
||||
async function getAccount(): Promise<string> {
|
||||
try {
|
||||
const accounts = await (window as any).ethereum.request({
|
||||
method: 'eth_requestAccounts'
|
||||
});
|
||||
return accounts[0];
|
||||
} catch (error) {
|
||||
console.error('用户拒绝授权:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前网络ID
|
||||
async function getCurrentNetworkId(): Promise<string> {
|
||||
try {
|
||||
const chainId = await (window as any).ethereum.request({
|
||||
method: 'eth_chainId'
|
||||
});
|
||||
return chainId;
|
||||
} catch (error) {
|
||||
console.error('获取网络ID失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 切换网络
|
||||
async function switchNetwork(chainId: string): Promise<void> {
|
||||
const chainName = Object.values(chains).find(c => c.chainId === chainId)?.name || 'Unknown Network';
|
||||
|
||||
try {
|
||||
await (window as any).ethereum.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: chainId }],
|
||||
});
|
||||
console.log(`已切换到${chainName}`);
|
||||
} catch (switchError: any) {
|
||||
if (switchError.code === 4902) {
|
||||
// 网络不存在,尝试添加网络
|
||||
const config = Object.values(chains).find(c => c.chainId === chainId);
|
||||
if (!config) {
|
||||
throw new Error(`不支持的网络: ${chainId}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await (window as any).ethereum.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [{
|
||||
chainId: chainId,
|
||||
chainName: config.name,
|
||||
nativeCurrency: {
|
||||
name: chainId === '0x38' ? 'BNB' : chainId === '0x89' ? 'MATIC' : 'ETH',
|
||||
symbol: chainId === '0x38' ? 'BNB' : chainId === '0x89' ? 'MATIC' : 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: [config.rpcUrl],
|
||||
blockExplorerUrls: [config.explorer]
|
||||
}],
|
||||
});
|
||||
console.log(`已添加${chainName}`);
|
||||
} catch (addError) {
|
||||
console.error('添加网络失败:', addError);
|
||||
throw addError;
|
||||
}
|
||||
} else {
|
||||
console.error('切换网络失败:', switchError);
|
||||
throw switchError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 编码函数调用数据
|
||||
function encodeFunctionCall(abiFragment: string, args: any[]): string {
|
||||
// 简化的函数签名计算
|
||||
const funcSignature = abiFragment.substring(0, abiFragment.indexOf('('));
|
||||
const params = abiFragment.substring(abiFragment.indexOf('(') + 1, abiFragment.lastIndexOf(')'));
|
||||
|
||||
// 为 approve(address,uint256) 函数构建调用数据
|
||||
if (funcSignature === 'approve' && params === 'address,uint256') {
|
||||
// 函数选择器: keccak256("approve(address,uint256)") 的前4字节
|
||||
// 已知为 0x095ea7b3
|
||||
let data = '0x095ea7b3';
|
||||
|
||||
// 地址参数(补零到32字节)
|
||||
let addr = args[0].toLowerCase();
|
||||
if (addr.startsWith('0x')) addr = addr.substring(2);
|
||||
data += addr.padStart(64, '0');
|
||||
|
||||
// 数量参数(补零到32字节)
|
||||
let amount = BigInt(args[1]).toString(16);
|
||||
data += amount.padStart(64, '0');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// 为 allowance(address,address) 函数构建调用数据
|
||||
if (funcSignature === 'allowance' && params === 'address,address') {
|
||||
// 函数选择器: keccak256("allowance(address,address)") 的前4字节
|
||||
// 已知为 0xdd62ed3e
|
||||
let data = '0xdd62ed3e';
|
||||
|
||||
// 第一个地址参数
|
||||
let owner = args[0].toLowerCase();
|
||||
if (owner.startsWith('0x')) owner = owner.substring(2);
|
||||
data += owner.padStart(64, '0');
|
||||
|
||||
// 第二个地址参数
|
||||
let spender = args[1].toLowerCase();
|
||||
if (spender.startsWith('0x')) spender = spender.substring(2);
|
||||
data += spender.padStart(64, '0');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// 为 balanceOf(address) 函数构建调用数据
|
||||
if (funcSignature === 'balanceOf' && params === 'address') {
|
||||
// 函数选择器: keccak256("balanceOf(address)") 的前4字节
|
||||
// 已知为 0x70a08231
|
||||
let data = '0x70a08231';
|
||||
|
||||
// 地址参数(补零到32字节)
|
||||
let addr = args[0].toLowerCase();
|
||||
if (addr.startsWith('0x')) addr = addr.substring(2);
|
||||
data += addr.padStart(64, '0');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
throw new Error('不支持的ABI片段: ' + abiFragment);
|
||||
}
|
||||
|
||||
// 授权USDT
|
||||
async function authorizeUSDT(
|
||||
chain: 'eth' | 'bnb' | 'polygon',
|
||||
spenderAddress: string,
|
||||
amount: string
|
||||
): Promise<{ success: boolean; transactionHash?: string; error?: any }> {
|
||||
try {
|
||||
// 验证参数
|
||||
if (!isValidAddress(spenderAddress)) {
|
||||
throw new Error('无效的被授权地址');
|
||||
}
|
||||
|
||||
if (isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) {
|
||||
throw new Error('授权金额必须大于0');
|
||||
}
|
||||
|
||||
// 获取钱包账户
|
||||
const account = await getAccount();
|
||||
|
||||
// 获取当前网络
|
||||
const currentNetworkId = await getCurrentNetworkId();
|
||||
const targetChainConfig = chains[chain];
|
||||
if (!targetChainConfig) throw new Error(`不支持的链: ${chain}`);
|
||||
|
||||
// 检查是否需要切换网络
|
||||
if (currentNetworkId !== targetChainConfig.chainId) {
|
||||
await switchNetwork(targetChainConfig.chainId);
|
||||
}
|
||||
|
||||
// 将金额转换为带6位小数的单位(USDT有6位小数)
|
||||
const parsedAmount = parseUnits(amount, 6);
|
||||
|
||||
// 构建交易数据
|
||||
const data = encodeFunctionCall('approve(address,uint256)', [spenderAddress, parsedAmount]);
|
||||
|
||||
// 构建交易对象
|
||||
const tx = {
|
||||
from: account,
|
||||
to: targetChainConfig.usdtAddress,
|
||||
data: data
|
||||
};
|
||||
|
||||
// 发送交易
|
||||
const transactionHash = await (window as any).ethereum.request({
|
||||
method: 'eth_sendTransaction',
|
||||
params: [tx]
|
||||
});
|
||||
|
||||
console.log(`正在${targetChainConfig.name}上发送授权交易...`);
|
||||
console.log('交易哈希:', transactionHash);
|
||||
|
||||
// 等待交易确认
|
||||
// 注意:这里我们只返回交易哈希,实际等待确认需要轮询或监听
|
||||
return {
|
||||
success: true,
|
||||
transactionHash: transactionHash
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('授权失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 查询USDT余额
|
||||
async function getUSDTBalance(chain: 'eth' | 'bnb' | 'polygon', account: string): Promise<string> {
|
||||
try {
|
||||
const targetChainConfig = chains[chain];
|
||||
if (!targetChainConfig) throw new Error(`不支持的链: ${chain}`);
|
||||
|
||||
// 构建调用数据
|
||||
const data = encodeFunctionCall('balanceOf(address)', [account]);
|
||||
|
||||
// 执行调用
|
||||
const result = await (window as any).ethereum.request({
|
||||
method: 'eth_call',
|
||||
params: [{
|
||||
to: targetChainConfig.usdtAddress,
|
||||
data: data
|
||||
}, 'latest']
|
||||
});
|
||||
|
||||
// 解析结果(十六进制转十进制)
|
||||
const balance = BigInt(result).toString();
|
||||
|
||||
// 转换为带6位小数的格式
|
||||
if (balance.length <= 6) {
|
||||
return '0.' + balance.padStart(6, '0');
|
||||
} else {
|
||||
const integerPart = balance.slice(0, -6);
|
||||
const decimalPart = balance.slice(-6).replace(/0+$/, ''); // 移除末尾零
|
||||
return decimalPart ? `${integerPart}.${decimalPart}` : integerPart;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询余额失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询当前授权额度
|
||||
async function getAllowance(
|
||||
chain: 'eth' | 'bnb' | 'polygon',
|
||||
owner: string,
|
||||
spender: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
const targetChainConfig = chains[chain];
|
||||
if (!targetChainConfig) throw new Error(`不支持的链: ${chain}`);
|
||||
|
||||
// 构建调用数据
|
||||
const data = encodeFunctionCall('allowance(address,address)', [owner, spender]);
|
||||
|
||||
// 执行调用
|
||||
const result = await (window as any).ethereum.request({
|
||||
method: 'eth_call',
|
||||
params: [{
|
||||
to: targetChainConfig.usdtAddress,
|
||||
data: data
|
||||
}, 'latest']
|
||||
});
|
||||
|
||||
// 解析结果(十六进制转十进制)
|
||||
const allowance = BigInt(result).toString();
|
||||
|
||||
// 转换为带6位小数的格式
|
||||
if (allowance.length <= 6) {
|
||||
return '0.' + allowance.padStart(6, '0');
|
||||
} else {
|
||||
const integerPart = allowance.slice(0, -6);
|
||||
const decimalPart = allowance.slice(-6).replace(/0+$/, ''); // 移除末尾零
|
||||
return decimalPart ? `${integerPart}.${decimalPart}` : integerPart;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询授权额度失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证地址格式
|
||||
function isValidAddress(address: string): boolean {
|
||||
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
||||
}
|
||||
|
||||
// 导出方法供UI调用
|
||||
export const CrossChainUSDTAuth = {
|
||||
authorizeUSDT,
|
||||
getUSDTBalance,
|
||||
getAllowance
|
||||
};
|
||||
|
||||
// 使用示例:
|
||||
// await CrossChainUSDTAuth.authorizeUSDT('eth', '0x1234...', '100')
|
||||
// await CrossChainUSDTAuth.getUSDTBalance('eth', '0x1234...')
|
||||
// await CrossChainUSDTAuth.getAllowance('eth', '0x1234...', '0x5678...')
|
||||
@ -126,10 +126,7 @@ export class ClobSdk {
|
||||
console.log(`[ClobSdk] 已连接到 ${this.url}`);
|
||||
this.reconnectAttempts = 0;
|
||||
this.notifyConnect(event);
|
||||
setTimeout(() => {
|
||||
this.subscribe();
|
||||
}, 1000);
|
||||
// this.subscribe();
|
||||
this.subscribe();
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event: any) => {
|
||||
|
||||
@ -50,8 +50,6 @@ export interface CategoryTreeNode {
|
||||
updatedAt?: string
|
||||
/** 排序值,有则按从小到大排序 */
|
||||
sort?: number
|
||||
/** 关联的标签 ID 列表,用于事件筛选 */
|
||||
tagIds?: number[]
|
||||
children?: CategoryTreeNode[]
|
||||
}
|
||||
|
||||
@ -206,14 +204,6 @@ function mapCatalogToTreeNode(item: PmTagCatalogItem): CategoryTreeNode {
|
||||
? item.children.map(mapCatalogToTreeNode)
|
||||
: undefined
|
||||
const icon = item.icon ?? resolveCategoryIcon({ label, slug })
|
||||
|
||||
// 提取 tagIds:优先使用数组形式的 tagId
|
||||
const tagIds = Array.isArray(item.tagId)
|
||||
? item.tagId.filter((v): v is number => typeof v === 'number')
|
||||
: typeof item.tagId === 'number'
|
||||
? [item.tagId]
|
||||
: undefined
|
||||
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
@ -221,7 +211,6 @@ function mapCatalogToTreeNode(item: PmTagCatalogItem): CategoryTreeNode {
|
||||
icon,
|
||||
sectionTitle: item.sectionTitle,
|
||||
sort: item.sort,
|
||||
tagIds,
|
||||
children: children?.length ? children : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,39 +113,42 @@ export interface PmEventListResponse {
|
||||
msg: string
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /PmEvent/getPmEventPublic 请求参数(与 doc.json 对齐)
|
||||
*/
|
||||
export interface GetPmEventListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
/** 创建时间范围,如 ['2025-01-01', '2025-12-31'] */
|
||||
createdAtRange?: string[]
|
||||
/** 标签 ID 列表,按分类筛选,传统数组方式传递 */
|
||||
tagIds?: number[]
|
||||
/** clobTokenIds 对应的值,用于按市场 token 筛选;可从 market.clobTokenIds 获取 */
|
||||
tokenid?: string | string[]
|
||||
/** 标签 ID,按分类筛选 */
|
||||
tagId?: number
|
||||
/** 标签 slug,按分类筛选 */
|
||||
tagSlug?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取 Event 列表(公开接口,不需要鉴权)
|
||||
* GET /PmEvent/getPmEventPublic
|
||||
*
|
||||
* Query: page, pageSize, keyword, createdAtRange, tagIds
|
||||
* doc.json: paths["/PmEvent/getPmEventPublic"].get.parameters
|
||||
* Query: page, pageSize, keyword, createdAtRange, tokenid, tagId, tagSlug
|
||||
*/
|
||||
export async function getPmEventPublic(
|
||||
params: GetPmEventListParams = {},
|
||||
): Promise<PmEventListResponse> {
|
||||
const { page = 1, pageSize = 10, keyword, createdAtRange, tagIds } = params
|
||||
const query: Record<string, string | number | number[] | string[] | undefined> = {
|
||||
const { page = 1, pageSize = 10, keyword, createdAtRange, tokenid, tagId, tagSlug } = params
|
||||
const query: Record<string, string | number | string[] | undefined> = {
|
||||
page,
|
||||
pageSize,
|
||||
}
|
||||
if (keyword != null && keyword !== '') query.keyword = keyword
|
||||
if (createdAtRange != null && createdAtRange.length) query.createdAtRange = createdAtRange
|
||||
if (tagIds != null && tagIds.length > 0) {
|
||||
query.tagIds = tagIds
|
||||
if (tokenid != null) {
|
||||
query.tokenid = Array.isArray(tokenid) ? tokenid : [tokenid]
|
||||
}
|
||||
// if (tagId != null && Number.isFinite(tagId)) query.tagId = tagId
|
||||
// if (tagSlug != null && tagSlug !== '') query.tagSlug = tagSlug
|
||||
|
||||
return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
|
||||
}
|
||||
|
||||
@ -200,13 +203,13 @@ export interface EventCardOutcome {
|
||||
title: string
|
||||
/** 第一选项概率(来自 outcomePrices[0]) */
|
||||
chanceValue: number
|
||||
/** Yes 价格 0–1,来自 outcomePrices[0],供交易组件使用 */
|
||||
yesPrice?: number
|
||||
/** No 价格 0–1,来自 outcomePrices[1],供交易组件使用 */
|
||||
noPrice?: number
|
||||
/** 第一选项按钮文案(来自 outcomes[0],如 Yes / Up) */
|
||||
yesLabel?: string
|
||||
/** 第二选项按钮文案(来自 outcomes[1],如 No / Down) */
|
||||
noLabel?: string
|
||||
/** 可选,用于交易时区分 market */
|
||||
marketId?: string
|
||||
/** 用于下单 tokenId,与 outcomes 顺序一致 */
|
||||
clobTokenIds?: string[]
|
||||
}
|
||||
|
||||
@ -237,10 +240,6 @@ export interface EventCardItem {
|
||||
marketId?: string
|
||||
/** 用于下单 tokenId,单 market 时取自 firstMarket.clobTokenIds */
|
||||
clobTokenIds?: string[]
|
||||
/** Yes 价格 0–1,来自 outcomePrices[0],供交易组件使用 */
|
||||
yesPrice?: number
|
||||
/** No 价格 0–1,来自 outcomePrices[1],供交易组件使用 */
|
||||
noPrice?: number
|
||||
}
|
||||
|
||||
/** 内存缓存:列表数据,切换页面时复用,下拉刷新时清空 */
|
||||
@ -323,38 +322,17 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
||||
|
||||
const category = item.series?.[0]?.title ?? item.tags?.[0]?.label ?? ''
|
||||
|
||||
function parseOutcomePrices(m: PmEventMarketItem): { yesPrice: number; noPrice: number } {
|
||||
const y = m?.outcomePrices?.[0]
|
||||
const n = m?.outcomePrices?.[1]
|
||||
const yesPrice =
|
||||
y != null && Number.isFinite(parseFloat(String(y)))
|
||||
? Math.min(1, Math.max(0, parseFloat(String(y))))
|
||||
: 0.5
|
||||
const noPrice =
|
||||
n != null && Number.isFinite(parseFloat(String(n)))
|
||||
? Math.min(1, Math.max(0, parseFloat(String(n))))
|
||||
: 1 - yesPrice
|
||||
return { yesPrice, noPrice }
|
||||
}
|
||||
|
||||
const outcomes: EventCardOutcome[] | undefined = multi
|
||||
? markets.map((m) => {
|
||||
const { yesPrice, noPrice } = parseOutcomePrices(m)
|
||||
return {
|
||||
title: m.question ?? '',
|
||||
chanceValue: marketChance(m),
|
||||
yesPrice,
|
||||
noPrice,
|
||||
yesLabel: m.outcomes?.[0] ?? 'Yes',
|
||||
noLabel: m.outcomes?.[1] ?? 'No',
|
||||
marketId: getMarketId(m),
|
||||
clobTokenIds: m.clobTokenIds,
|
||||
}
|
||||
})
|
||||
? markets.map((m) => ({
|
||||
title: m.question ?? '',
|
||||
chanceValue: marketChance(m),
|
||||
yesLabel: m.outcomes?.[0] ?? 'Yes',
|
||||
noLabel: m.outcomes?.[1] ?? 'No',
|
||||
marketId: getMarketId(m),
|
||||
clobTokenIds: m.clobTokenIds,
|
||||
}))
|
||||
: undefined
|
||||
|
||||
const firstPrices = firstMarket ? parseOutcomePrices(firstMarket) : { yesPrice: 0.5, noPrice: 0.5 }
|
||||
|
||||
return {
|
||||
id,
|
||||
slug: item.slug ?? undefined,
|
||||
@ -371,7 +349,5 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
||||
isNew: item.new === true,
|
||||
marketId: getMarketId(firstMarket),
|
||||
clobTokenIds: firstMarket?.clobTokenIds,
|
||||
yesPrice: firstPrices.yesPrice,
|
||||
noPrice: firstPrices.noPrice,
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,8 +24,6 @@ export interface ClobSubmitOrderRequest {
|
||||
taker: boolean
|
||||
tokenID: string
|
||||
userID: number
|
||||
/** 市场 ID */
|
||||
marketID: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
230
src/api/order.ts
230
src/api/order.ts
@ -1,230 +0,0 @@
|
||||
import { get, post } from './request'
|
||||
|
||||
/** 分页结果 */
|
||||
export interface PageResult<T> {
|
||||
list: T[]
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单项(与 doc.json definitions["model.ClobOrder"] 对齐)
|
||||
* GET /clob/order/getOrderList 列表项
|
||||
*/
|
||||
export interface ClobOrderItem {
|
||||
ID: number
|
||||
assetID?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
expiration?: number
|
||||
feeRateBps?: number
|
||||
market?: string
|
||||
orderType?: number
|
||||
originalSize?: number
|
||||
outcome?: string
|
||||
price?: number
|
||||
side?: number
|
||||
sizeMatched?: number
|
||||
status?: number
|
||||
userID?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** 订单列表响应 */
|
||||
export interface OrderListResponse {
|
||||
code: number
|
||||
data: PageResult<ClobOrderItem>
|
||||
msg: string
|
||||
}
|
||||
|
||||
/** 订单状态:1=未成交(Live),2=已成交,0=已取消等 */
|
||||
export const OrderStatus = {
|
||||
Live: 1,
|
||||
Matched: 2,
|
||||
Cancelled: 0,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* GET /clob/order/getOrderList 请求参数
|
||||
*/
|
||||
export interface GetOrderListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
/** 订单状态筛选:1=未成交 */
|
||||
status?: number
|
||||
startCreatedAt?: string
|
||||
endCreatedAt?: string
|
||||
marketID?: string
|
||||
tokenID?: string
|
||||
userID?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取订单列表
|
||||
* GET /clob/order/getOrderList
|
||||
* 需鉴权:x-token、x-user-id
|
||||
*/
|
||||
export async function getOrderList(
|
||||
params: GetOrderListParams = {},
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<OrderListResponse> {
|
||||
const { page = 1, pageSize = 10, status, startCreatedAt, endCreatedAt, marketID, tokenID, userID } =
|
||||
params
|
||||
const query: Record<string, string | number | undefined> = { page, pageSize }
|
||||
if (status != null && Number.isFinite(status)) query.status = status
|
||||
if (startCreatedAt != null && startCreatedAt !== '') query.startCreatedAt = startCreatedAt
|
||||
if (endCreatedAt != null && endCreatedAt !== '') query.endCreatedAt = endCreatedAt
|
||||
if (marketID != null && marketID !== '') query.marketID = marketID
|
||||
if (tokenID != null && tokenID !== '') query.tokenID = tokenID
|
||||
if (userID != null && Number.isFinite(userID)) query.userID = userID
|
||||
return get<OrderListResponse>('/clob/order/getOrderList', query, config)
|
||||
}
|
||||
|
||||
/** 取消订单请求体(request.CancelOrderReq) */
|
||||
export interface CancelOrderReq {
|
||||
orderID: number
|
||||
tokenID: string
|
||||
userID: number
|
||||
}
|
||||
|
||||
/** 通用 API 响应 */
|
||||
export interface ApiResponse {
|
||||
code: number
|
||||
data?: unknown
|
||||
msg: string
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /clob/order/cancelOrder
|
||||
* 撤销订单(撮合引擎),需鉴权 x-token
|
||||
*/
|
||||
export async function cancelOrder(
|
||||
data: CancelOrderReq,
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<ApiResponse> {
|
||||
return post<ApiResponse>('/clob/order/cancelOrder', data, config)
|
||||
}
|
||||
|
||||
/** 钱包 History 展示项(与 Wallet.vue HistoryItem 一致) */
|
||||
export interface HistoryDisplayItem {
|
||||
id: string
|
||||
market: string
|
||||
side: 'Yes' | 'No'
|
||||
activity: string
|
||||
value: string
|
||||
activityDetail?: string
|
||||
profitLoss?: string
|
||||
profitLossNegative?: boolean
|
||||
timeAgo?: string
|
||||
avgPrice?: string
|
||||
shares?: string
|
||||
iconChar?: string
|
||||
iconClass?: string
|
||||
}
|
||||
|
||||
/** Side: Buy=1, Sell=2 */
|
||||
const Side = { Buy: 1, Sell: 2 } as const
|
||||
|
||||
function formatTimeAgo(createdAt: string | undefined): string {
|
||||
if (!createdAt) return ''
|
||||
const d = new Date(createdAt)
|
||||
const now = Date.now()
|
||||
const diff = now - d.getTime()
|
||||
if (diff < 60000) return 'Just now'
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)} minutes ago`
|
||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)} hours ago`
|
||||
if (diff < 604800000) return `${Math.floor(diff / 86400000)} days ago`
|
||||
return d.toLocaleDateString()
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 ClobOrderItem 映射为钱包 History 展示项
|
||||
* price 为整数(已乘 10000),sizeMatched 为已成交份额
|
||||
*/
|
||||
export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem {
|
||||
const id = String(order.ID ?? '')
|
||||
const market = order.market ?? ''
|
||||
const outcome = order.outcome ?? 'Yes'
|
||||
const sideNum = order.side ?? Side.Buy
|
||||
const sideLabel = sideNum === Side.Sell ? 'Sell' : 'Buy'
|
||||
const activity = `${sideLabel} ${outcome}`
|
||||
const priceBps = order.price ?? 0
|
||||
const priceCents = Math.round(priceBps / 100)
|
||||
const size = order.sizeMatched ?? order.originalSize ?? 0
|
||||
const valueUsd = (priceBps / 10000) * size
|
||||
const value = `$${valueUsd.toFixed(2)}`
|
||||
const verb = sideNum === Side.Sell ? 'Sold' : 'Bought'
|
||||
const activityDetail = `${verb} ${size} ${outcome} at ${priceCents}¢`
|
||||
const avgPrice = `${priceCents}¢`
|
||||
const timeAgo = formatTimeAgo(order.createdAt)
|
||||
return {
|
||||
id,
|
||||
market,
|
||||
side: outcome === 'No' ? 'No' : 'Yes',
|
||||
activity,
|
||||
value,
|
||||
activityDetail,
|
||||
profitLoss: value,
|
||||
profitLossNegative: false,
|
||||
timeAgo,
|
||||
avgPrice,
|
||||
shares: String(size),
|
||||
}
|
||||
}
|
||||
|
||||
/** 钱包 Open Orders 展示项(与 Wallet.vue OpenOrder 一致) */
|
||||
export interface OpenOrderDisplayItem {
|
||||
id: string
|
||||
market: string
|
||||
side: 'Yes' | 'No'
|
||||
outcome: string
|
||||
price: string
|
||||
filled: string
|
||||
total: string
|
||||
expiration: string
|
||||
actionLabel?: string
|
||||
filledDisplay?: string
|
||||
orderID?: number
|
||||
tokenID?: string
|
||||
}
|
||||
|
||||
/** OrderType GTC=0 表示 Until Cancelled */
|
||||
const OrderType = { GTC: 0, GTD: 1 } as const
|
||||
|
||||
/**
|
||||
* 将 ClobOrderItem 映射为钱包 Open Orders 展示项(未成交订单)
|
||||
* price 为整数(已乘 10000)
|
||||
*/
|
||||
export function mapOrderToOpenOrderItem(order: ClobOrderItem): OpenOrderDisplayItem {
|
||||
const id = String(order.ID ?? '')
|
||||
const market = order.market ?? ''
|
||||
const sideNum = order.side ?? Side.Buy
|
||||
const side = sideNum === Side.Sell ? 'No' : 'Yes'
|
||||
const outcome = order.outcome || (side === 'Yes' ? 'Yes' : 'No')
|
||||
const priceBps = order.price ?? 0
|
||||
const priceCents = Math.round(priceBps / 100)
|
||||
const price = `${priceCents}¢`
|
||||
const originalSize = order.originalSize ?? 0
|
||||
const sizeMatched = order.sizeMatched ?? 0
|
||||
const filled = `${sizeMatched}/${originalSize}`
|
||||
const totalUsd = (priceBps / 10000) * originalSize
|
||||
const total = `$${totalUsd.toFixed(2)}`
|
||||
const expiration =
|
||||
order.orderType === OrderType.GTC ? 'Until Cancelled' : order.expiration?.toString() ?? ''
|
||||
const actionLabel = sideNum === Side.Buy ? `Buy ${outcome}` : `Sell ${outcome}`
|
||||
return {
|
||||
id,
|
||||
market,
|
||||
side,
|
||||
outcome,
|
||||
price,
|
||||
filled,
|
||||
total,
|
||||
expiration,
|
||||
actionLabel,
|
||||
filledDisplay: filled,
|
||||
orderID: order.ID,
|
||||
tokenID: order.assetID,
|
||||
}
|
||||
}
|
||||
@ -1,133 +0,0 @@
|
||||
import { get } from './request'
|
||||
|
||||
/** 分页结果 */
|
||||
export interface PageResult<T> {
|
||||
list: T[]
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 持仓项(与 /clob/position/getPositionList 实际返回对齐)
|
||||
* size、available、cost 为字符串,6 位小数(除以 1000000 得实际值)
|
||||
*/
|
||||
export interface ClobPositionItem {
|
||||
ID?: number
|
||||
userID?: number
|
||||
marketID?: string
|
||||
tokenId?: string
|
||||
/** 份额,字符串,6 位小数 */
|
||||
size?: string
|
||||
/** 可用份额,字符串,6 位小数 */
|
||||
available?: string
|
||||
/** 成本,字符串,6 位小数 */
|
||||
cost?: string
|
||||
/** 方向:Yes | No */
|
||||
outcome?: string
|
||||
version?: number
|
||||
CreatedAt?: string
|
||||
UpdatedAt?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** 份额/金额 6 位小数 */
|
||||
const SCALE = 1_000_000
|
||||
|
||||
function parsePosNum(s: string | number | undefined): number {
|
||||
if (s == null) return 0
|
||||
const n = typeof s === 'string' ? parseFloat(s) : Number(s)
|
||||
return Number.isFinite(n) ? n : 0
|
||||
}
|
||||
|
||||
/** 持仓列表响应 */
|
||||
export interface PositionListResponse {
|
||||
code: number
|
||||
data: PageResult<ClobPositionItem>
|
||||
msg: string
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /clob/position/getPositionList 请求参数
|
||||
*/
|
||||
export interface GetPositionListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
startCreatedAt?: string
|
||||
endCreatedAt?: string
|
||||
marketID?: string
|
||||
tokenID?: string
|
||||
userID?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取持仓列表
|
||||
* GET /clob/position/getPositionList
|
||||
* 需鉴权:x-token、x-user-id
|
||||
*/
|
||||
export async function getPositionList(
|
||||
params: GetPositionListParams = {},
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<PositionListResponse> {
|
||||
const { page = 1, pageSize = 10, startCreatedAt, endCreatedAt, marketID, tokenID, userID } = params
|
||||
const query: Record<string, string | number | undefined> = { page, pageSize }
|
||||
if (startCreatedAt != null && startCreatedAt !== '') query.startCreatedAt = startCreatedAt
|
||||
if (endCreatedAt != null && endCreatedAt !== '') query.endCreatedAt = endCreatedAt
|
||||
if (marketID != null && marketID !== '') query.marketID = marketID
|
||||
if (tokenID != null && tokenID !== '') query.tokenID = tokenID
|
||||
if (userID != null && Number.isFinite(userID)) query.userID = userID
|
||||
return get<PositionListResponse>('/clob/position/getPositionList', query, config)
|
||||
}
|
||||
|
||||
/** 钱包 Positions 展示项(与 Wallet.vue Position 一致) */
|
||||
export interface PositionDisplayItem {
|
||||
id: string
|
||||
market: string
|
||||
shares: string
|
||||
avgNow: string
|
||||
bet: string
|
||||
toWin: string
|
||||
value: string
|
||||
valueChange?: string
|
||||
valueChangePct?: string
|
||||
valueChangeLoss?: boolean
|
||||
sellOutcome?: string
|
||||
outcomeWord?: string
|
||||
iconChar?: string
|
||||
iconClass?: string
|
||||
outcomeTag?: string
|
||||
outcomePillClass?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 ClobPositionItem 映射为钱包 Position 展示项
|
||||
* size、available、cost 为 6 位小数字符串
|
||||
*/
|
||||
export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplayItem {
|
||||
const id = String(pos.ID ?? '')
|
||||
const market = pos.marketID ?? ''
|
||||
const sizeRaw = parsePosNum(pos.size ?? pos.available)
|
||||
const costRaw = parsePosNum(pos.cost)
|
||||
const size = sizeRaw / SCALE
|
||||
const costUsd = costRaw / SCALE
|
||||
const shares = `${size} shares`
|
||||
const outcomeWord = pos.outcome === 'No' ? 'No' : 'Yes'
|
||||
const pillClass = outcomeWord === 'No' ? 'pill-down' : 'pill-yes'
|
||||
const value = `$${costUsd.toFixed(2)}`
|
||||
const bet = value
|
||||
const toWin = `$${size.toFixed(2)}`
|
||||
const outcomeTag = `${outcomeWord} —`
|
||||
return {
|
||||
id,
|
||||
market,
|
||||
shares,
|
||||
avgNow: '—',
|
||||
bet,
|
||||
toWin,
|
||||
value,
|
||||
sellOutcome: outcomeWord,
|
||||
outcomeWord,
|
||||
outcomeTag,
|
||||
outcomePillClass: pillClass,
|
||||
}
|
||||
}
|
||||
@ -35,7 +35,7 @@ export interface RequestConfig {
|
||||
*/
|
||||
export async function get<T = unknown>(
|
||||
path: string,
|
||||
params?: Record<string, string | number | string[] | number[] | undefined>,
|
||||
params?: Record<string, string | number | string[] | undefined>,
|
||||
config?: RequestConfig,
|
||||
): Promise<T> {
|
||||
const url = new URL(path, BASE_URL || window.location.origin)
|
||||
|
||||
@ -3,49 +3,11 @@ import { get } from './request'
|
||||
const USDC_DECIMALS = 1_000_000
|
||||
|
||||
/**
|
||||
* getUserInfo 返回的 data 结构(2026 更新)
|
||||
* data 包含 balance、orders、positions、userInfo
|
||||
* getUserInfo 返回的 data(definitions system.SysUser)
|
||||
* doc.json definitions["system.SysUser"] 完整字段
|
||||
*/
|
||||
|
||||
/** 余额项(data.balance) */
|
||||
export interface UserInfoBalance {
|
||||
ID?: number
|
||||
CreatedAt?: string
|
||||
UpdatedAt?: string
|
||||
userID?: number
|
||||
marketID?: string
|
||||
tokenType?: string
|
||||
tokenID?: string
|
||||
amount?: string
|
||||
available?: string
|
||||
locked?: string
|
||||
version?: number
|
||||
}
|
||||
|
||||
/** 订单项(data.orders[]) */
|
||||
export interface UserInfoOrder {
|
||||
ID?: number
|
||||
CreatedAt?: string
|
||||
UpdatedAt?: string
|
||||
userID?: number
|
||||
market?: string
|
||||
status?: number
|
||||
assetID?: string
|
||||
side?: number
|
||||
price?: number
|
||||
originalSize?: number
|
||||
sizeMatched?: number
|
||||
outcome?: string
|
||||
expiration?: number
|
||||
orderType?: number
|
||||
feeRateBps?: number
|
||||
}
|
||||
|
||||
/** 用户信息(data.userInfo) */
|
||||
export interface UserInfoData {
|
||||
ID?: number
|
||||
CreatedAt?: string
|
||||
UpdatedAt?: string
|
||||
userName?: string
|
||||
nickName?: string
|
||||
headerImg?: string
|
||||
@ -53,6 +15,8 @@ export interface UserInfoData {
|
||||
authorityId?: number
|
||||
authority?: unknown
|
||||
authorities?: unknown[]
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
email?: string
|
||||
phone?: string
|
||||
enable?: number
|
||||
@ -62,20 +26,12 @@ export interface UserInfoData {
|
||||
promotionCode?: string
|
||||
puserId?: number
|
||||
remark?: string
|
||||
originSetting?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
/** getUserInfo 完整 data */
|
||||
export interface GetUserInfoData {
|
||||
balance?: UserInfoBalance
|
||||
orders?: UserInfoOrder[]
|
||||
positions?: unknown[]
|
||||
userInfo?: UserInfoData
|
||||
originSetting?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface GetUserInfoResponse {
|
||||
code: number
|
||||
data?: GetUserInfoData
|
||||
data?: UserInfoData
|
||||
msg?: string
|
||||
}
|
||||
|
||||
|
||||
@ -181,8 +181,6 @@ const emit = defineEmits<{
|
||||
clobTokenIds?: string[]
|
||||
yesLabel?: string
|
||||
noLabel?: string
|
||||
yesPrice?: number
|
||||
noPrice?: number
|
||||
},
|
||||
]
|
||||
}>()
|
||||
@ -212,10 +210,6 @@ const props = withDefaults(
|
||||
marketId?: string
|
||||
/** 用于下单 tokenId,单 market 时 */
|
||||
clobTokenIds?: string[]
|
||||
/** Yes 价格 0–1,供交易组件使用 */
|
||||
yesPrice?: number
|
||||
/** No 价格 0–1,供交易组件使用 */
|
||||
noPrice?: number
|
||||
}>(),
|
||||
{
|
||||
marketTitle: 'Mamdan opens city-owned grocery store b...',
|
||||
@ -231,8 +225,6 @@ const props = withDefaults(
|
||||
noLabel: 'No',
|
||||
isNew: false,
|
||||
marketId: undefined,
|
||||
yesPrice: undefined,
|
||||
noPrice: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
@ -314,8 +306,6 @@ function openTradeSingle(side: 'yes' | 'no') {
|
||||
clobTokenIds: props.clobTokenIds,
|
||||
yesLabel: props.yesLabel,
|
||||
noLabel: props.noLabel,
|
||||
yesPrice: props.yesPrice,
|
||||
noPrice: props.noPrice,
|
||||
})
|
||||
}
|
||||
|
||||
@ -328,8 +318,6 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
|
||||
clobTokenIds: outcome.clobTokenIds,
|
||||
yesLabel: outcome.yesLabel,
|
||||
noLabel: outcome.noLabel,
|
||||
yesPrice: outcome.yesPrice,
|
||||
noPrice: outcome.noPrice,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,20 +0,0 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
/**
|
||||
* 401 等权限错误时:未登录提示请先登录,已登录提示权限不足
|
||||
*/
|
||||
export function useAuthError() {
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserStore()
|
||||
|
||||
function formatAuthError(err: unknown, fallback: string): string {
|
||||
const msg = err instanceof Error ? err.message : String(err ?? fallback)
|
||||
if (/401|Unauthorized/i.test(msg)) {
|
||||
return userStore.isLoggedIn ? t('error.insufficientPermission') : t('error.pleaseLogin')
|
||||
}
|
||||
return msg || fallback
|
||||
}
|
||||
|
||||
return { formatAuthError }
|
||||
}
|
||||
@ -15,9 +15,7 @@
|
||||
"chance": "chance"
|
||||
},
|
||||
"toast": {
|
||||
"orderSuccess": "Order placed successfully",
|
||||
"splitSuccess": "Split successful",
|
||||
"mergeSuccess": "Merge successful"
|
||||
"orderSuccess": "Order placed successfully"
|
||||
},
|
||||
"trade": {
|
||||
"buy": "Buy",
|
||||
@ -58,7 +56,6 @@
|
||||
"avgPrice": "Avg. Price",
|
||||
"max": "Max",
|
||||
"balanceLabel": "Balance",
|
||||
"maxShares": "Max shares",
|
||||
"pleaseLogin": "Please log in first",
|
||||
"pleaseSelectMarket": "Please select a market (with clobTokenIds)",
|
||||
"userError": "User info error",
|
||||
@ -85,24 +82,12 @@
|
||||
"error": {
|
||||
"requestFailed": "Request failed",
|
||||
"loadFailed": "Load failed",
|
||||
"invalidId": "Invalid ID or slug",
|
||||
"pleaseLogin": "Please log in first",
|
||||
"insufficientPermission": "Insufficient permission"
|
||||
"invalidId": "Invalid ID or slug"
|
||||
},
|
||||
"activity": {
|
||||
"comments": "Comments",
|
||||
"topHolders": "Top Holders",
|
||||
"activity": "Activity",
|
||||
"rules": "Rules",
|
||||
"rulesDescription": "Description",
|
||||
"rulesSource": "Resolution source",
|
||||
"rulesEmpty": "No rules available.",
|
||||
"mine": "Mine",
|
||||
"myPositions": "Positions",
|
||||
"openOrders": "Orders",
|
||||
"noPositionsInMarket": "No positions in this market.",
|
||||
"noOpenOrdersInMarket": "No open orders in this market.",
|
||||
"cancelOrder": "Cancel",
|
||||
"noCommentsYet": "No comments yet.",
|
||||
"topHoldersPlaceholder": "Top holders will appear here.",
|
||||
"minAmount": "Min amount",
|
||||
@ -123,8 +108,6 @@
|
||||
"today": "Today",
|
||||
"deposit": "Deposit",
|
||||
"withdraw": "Withdraw",
|
||||
"authorize": "Authorize",
|
||||
"authorizeDesc": "Authorize USDC spending for trading. This allows the exchange contract to use your balance when placing orders.",
|
||||
"profitLoss": "Profit/Loss",
|
||||
"allTime": "All-Time",
|
||||
"pl1D": "1D",
|
||||
|
||||
@ -15,9 +15,7 @@
|
||||
"chance": "確率"
|
||||
},
|
||||
"toast": {
|
||||
"orderSuccess": "注文が完了しました",
|
||||
"splitSuccess": "スプリット成功",
|
||||
"mergeSuccess": "マージ成功"
|
||||
"orderSuccess": "注文が完了しました"
|
||||
},
|
||||
"trade": {
|
||||
"buy": "買う",
|
||||
@ -58,7 +56,6 @@
|
||||
"avgPrice": "平均価格",
|
||||
"max": "最大",
|
||||
"balanceLabel": "残高",
|
||||
"maxShares": "最大シェア",
|
||||
"pleaseLogin": "先にログインしてください",
|
||||
"pleaseSelectMarket": "市場を選択してください(clobTokenIds が必要)",
|
||||
"userError": "ユーザー情報エラー",
|
||||
@ -85,24 +82,12 @@
|
||||
"error": {
|
||||
"requestFailed": "リクエストに失敗しました",
|
||||
"loadFailed": "読み込みに失敗しました",
|
||||
"invalidId": "無効な ID または slug",
|
||||
"pleaseLogin": "先にログインしてください",
|
||||
"insufficientPermission": "権限がありません"
|
||||
"invalidId": "無効な ID または slug"
|
||||
},
|
||||
"activity": {
|
||||
"comments": "コメント",
|
||||
"topHolders": "持倉トップ",
|
||||
"activity": "アクティビティ",
|
||||
"rules": "ルール",
|
||||
"rulesDescription": "説明",
|
||||
"rulesSource": "決裁ソース",
|
||||
"rulesEmpty": "ルールはありません",
|
||||
"mine": "マイ",
|
||||
"myPositions": "ポジション",
|
||||
"openOrders": "注文",
|
||||
"noPositionsInMarket": "この市場にポジションはありません",
|
||||
"noOpenOrdersInMarket": "この市場に未約定注文はありません",
|
||||
"cancelOrder": "キャンセル",
|
||||
"noCommentsYet": "コメントはまだありません",
|
||||
"topHoldersPlaceholder": "持倉トップがここに表示されます",
|
||||
"minAmount": "最小金額",
|
||||
@ -123,8 +108,6 @@
|
||||
"today": "今日",
|
||||
"deposit": "入金",
|
||||
"withdraw": "出金",
|
||||
"authorize": "承認",
|
||||
"authorizeDesc": "取引のため USDC の使用を承認します。注文時に取引所が残高を使用できるようになります。",
|
||||
"profitLoss": "損益",
|
||||
"allTime": "全期間",
|
||||
"pl1D": "1日",
|
||||
|
||||
@ -15,9 +15,7 @@
|
||||
"chance": "확률"
|
||||
},
|
||||
"toast": {
|
||||
"orderSuccess": "주문이 완료되었습니다",
|
||||
"splitSuccess": "분할 완료",
|
||||
"mergeSuccess": "병합 완료"
|
||||
"orderSuccess": "주문이 완료되었습니다"
|
||||
},
|
||||
"trade": {
|
||||
"buy": "매수",
|
||||
@ -58,7 +56,6 @@
|
||||
"avgPrice": "평균 가격",
|
||||
"max": "최대",
|
||||
"balanceLabel": "잔액",
|
||||
"maxShares": "최대 주식",
|
||||
"pleaseLogin": "먼저 로그인하세요",
|
||||
"pleaseSelectMarket": "시장을 선택하세요 (clobTokenIds 필요)",
|
||||
"userError": "사용자 정보 오류",
|
||||
@ -85,24 +82,12 @@
|
||||
"error": {
|
||||
"requestFailed": "요청 실패",
|
||||
"loadFailed": "로드 실패",
|
||||
"invalidId": "잘못된 ID 또는 slug",
|
||||
"pleaseLogin": "먼저 로그인하세요",
|
||||
"insufficientPermission": "권한이 없습니다"
|
||||
"invalidId": "잘못된 ID 또는 slug"
|
||||
},
|
||||
"activity": {
|
||||
"comments": "댓글",
|
||||
"topHolders": "보유자 순위",
|
||||
"activity": "활동",
|
||||
"rules": "규칙",
|
||||
"rulesDescription": "설명",
|
||||
"rulesSource": "결정 출처",
|
||||
"rulesEmpty": "규칙이 없습니다",
|
||||
"mine": "내 것",
|
||||
"myPositions": "포지션",
|
||||
"openOrders": "주문",
|
||||
"noPositionsInMarket": "이 시장에 포지션이 없습니다",
|
||||
"noOpenOrdersInMarket": "이 시장에 미체결 주문이 없습니다",
|
||||
"cancelOrder": "취소",
|
||||
"noCommentsYet": "아직 댓글이 없습니다",
|
||||
"topHoldersPlaceholder": "보유자 순위가 여기에 표시됩니다",
|
||||
"minAmount": "최소 금액",
|
||||
@ -123,8 +108,6 @@
|
||||
"today": "오늘",
|
||||
"deposit": "입금",
|
||||
"withdraw": "출금",
|
||||
"authorize": "승인",
|
||||
"authorizeDesc": "거래를 위해 USDC 사용을 승인합니다. 주문 시 거래소가 잔액을 사용할 수 있습니다.",
|
||||
"profitLoss": "손익",
|
||||
"allTime": "전체",
|
||||
"pl1D": "1일",
|
||||
|
||||
@ -15,9 +15,7 @@
|
||||
"chance": "概率"
|
||||
},
|
||||
"toast": {
|
||||
"orderSuccess": "下单成功",
|
||||
"splitSuccess": "拆分成功",
|
||||
"mergeSuccess": "合并成功"
|
||||
"orderSuccess": "下单成功"
|
||||
},
|
||||
"trade": {
|
||||
"buy": "买入",
|
||||
@ -58,7 +56,6 @@
|
||||
"avgPrice": "平均价",
|
||||
"max": "最大",
|
||||
"balanceLabel": "余额",
|
||||
"maxShares": "最大份额",
|
||||
"pleaseLogin": "请先登录",
|
||||
"pleaseSelectMarket": "请先选择市场(需包含 clobTokenIds)",
|
||||
"userError": "用户信息异常",
|
||||
@ -85,24 +82,12 @@
|
||||
"error": {
|
||||
"requestFailed": "请求失败",
|
||||
"loadFailed": "加载失败",
|
||||
"invalidId": "无效的 ID 或 slug",
|
||||
"pleaseLogin": "请先登录",
|
||||
"insufficientPermission": "权限不足"
|
||||
"invalidId": "无效的 ID 或 slug"
|
||||
},
|
||||
"activity": {
|
||||
"comments": "评论",
|
||||
"topHolders": "持仓大户",
|
||||
"activity": "动态",
|
||||
"rules": "规则",
|
||||
"rulesDescription": "描述",
|
||||
"rulesSource": "裁决来源",
|
||||
"rulesEmpty": "暂无规则说明",
|
||||
"mine": "我的",
|
||||
"myPositions": "持仓",
|
||||
"openOrders": "限价",
|
||||
"noPositionsInMarket": "本市场暂无持仓",
|
||||
"noOpenOrdersInMarket": "本市场暂无未成交订单",
|
||||
"cancelOrder": "撤单",
|
||||
"noCommentsYet": "暂无评论",
|
||||
"topHoldersPlaceholder": "持仓大户将在此显示",
|
||||
"minAmount": "最小金额",
|
||||
@ -123,8 +108,6 @@
|
||||
"today": "今日",
|
||||
"deposit": "入金",
|
||||
"withdraw": "提现",
|
||||
"authorize": "授权",
|
||||
"authorizeDesc": "授权 USDC 用于交易。允许交易合约在下单时使用您的余额。",
|
||||
"profitLoss": "盈亏",
|
||||
"allTime": "全部",
|
||||
"pl1D": "1天",
|
||||
|
||||
@ -15,9 +15,7 @@
|
||||
"chance": "機率"
|
||||
},
|
||||
"toast": {
|
||||
"orderSuccess": "下單成功",
|
||||
"splitSuccess": "拆分成功",
|
||||
"mergeSuccess": "合併成功"
|
||||
"orderSuccess": "下單成功"
|
||||
},
|
||||
"trade": {
|
||||
"buy": "買入",
|
||||
@ -58,7 +56,6 @@
|
||||
"avgPrice": "平均價",
|
||||
"max": "最大",
|
||||
"balanceLabel": "餘額",
|
||||
"maxShares": "最大份額",
|
||||
"pleaseLogin": "請先登入",
|
||||
"pleaseSelectMarket": "請先選擇市場(需包含 clobTokenIds)",
|
||||
"userError": "用戶資訊異常",
|
||||
@ -85,24 +82,12 @@
|
||||
"error": {
|
||||
"requestFailed": "請求失敗",
|
||||
"loadFailed": "載入失敗",
|
||||
"invalidId": "無效的 ID 或 slug",
|
||||
"pleaseLogin": "請先登入",
|
||||
"insufficientPermission": "權限不足"
|
||||
"invalidId": "無效的 ID 或 slug"
|
||||
},
|
||||
"activity": {
|
||||
"comments": "評論",
|
||||
"topHolders": "持倉大戶",
|
||||
"activity": "動態",
|
||||
"rules": "規則",
|
||||
"rulesDescription": "描述",
|
||||
"rulesSource": "裁決來源",
|
||||
"rulesEmpty": "暫無規則說明",
|
||||
"mine": "我的",
|
||||
"myPositions": "持倉",
|
||||
"openOrders": "限價",
|
||||
"noPositionsInMarket": "本市場暫無持倉",
|
||||
"noOpenOrdersInMarket": "本市場暫無未成交訂單",
|
||||
"cancelOrder": "撤單",
|
||||
"noCommentsYet": "暫無評論",
|
||||
"topHoldersPlaceholder": "持倉大戶將在此顯示",
|
||||
"minAmount": "最小金額",
|
||||
@ -123,8 +108,6 @@
|
||||
"today": "今日",
|
||||
"deposit": "入金",
|
||||
"withdraw": "提現",
|
||||
"authorize": "授權",
|
||||
"authorizeDesc": "授權 USDC 用於交易。允許交易合約在下單時使用您的餘額。",
|
||||
"profitLoss": "盈虧",
|
||||
"allTime": "全部",
|
||||
"pl1D": "1天",
|
||||
|
||||
@ -44,14 +44,9 @@ export const useToastStore = defineStore('toast', () => {
|
||||
if (now - last >= DEDUP_MS) return false
|
||||
|
||||
const msg = normalizeMsg(item.message)
|
||||
let inDisplayingIdx = -1
|
||||
for (let i = displaying.value.length - 1; i >= 0; i--) {
|
||||
const d = displaying.value[i]
|
||||
if (d && normalizeMsg(d.message) === msg && d.type === item.type) {
|
||||
inDisplayingIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
const inDisplayingIdx = displaying.value.findLastIndex(
|
||||
(d) => normalizeMsg(d.message) === msg && d.type === item.type
|
||||
)
|
||||
if (inDisplayingIdx >= 0) {
|
||||
displaying.value = displaying.value.map((x, i) =>
|
||||
i === inDisplayingIdx
|
||||
@ -100,7 +95,7 @@ export const useToastStore = defineStore('toast', () => {
|
||||
function remove(id: string) {
|
||||
displaying.value = displaying.value.filter((d) => d.id !== id)
|
||||
if (queue.value.length > 0) {
|
||||
const next = queue.value[0]!
|
||||
const next = queue.value[0]
|
||||
queue.value = queue.value.slice(1)
|
||||
displaying.value = [...displaying.value, next]
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { getUsdcBalance, formatUsdcBalance, getUserInfo } from '@/api/user'
|
||||
import { getUserWsUrl } from '@/api/request'
|
||||
import { UserSdk, type BalanceData, type PositionData } from '../../sdk/userSocket'
|
||||
import { UserSdk, type BalanceData } from '../../sdk/userSocket'
|
||||
|
||||
export interface UserInfo {
|
||||
/** 用户 ID(API 可能返回 id 或 ID) */
|
||||
@ -53,7 +53,6 @@ export const useUserStore = defineStore('user', () => {
|
||||
const balance = ref<string>('0.00')
|
||||
|
||||
let userSdkRef: UserSdk | null = null
|
||||
const positionUpdateCallbacks: ((data: PositionData & Record<string, unknown>) => void)[] = []
|
||||
|
||||
// 若从 storage 恢复登录态,自动连接 UserSocket
|
||||
if (stored?.token && stored?.user) {
|
||||
@ -71,15 +70,11 @@ export const useUserStore = defineStore('user', () => {
|
||||
reconnectInterval: 2000,
|
||||
})
|
||||
sdk.onBalanceUpdate((data: BalanceData) => {
|
||||
if ((data.tokenType ?? '').toUpperCase() !== 'USDC') return
|
||||
const avail = data.available ?? data.amount
|
||||
if (avail != null) {
|
||||
balance.value = formatUsdcBalance(String(avail))
|
||||
}
|
||||
})
|
||||
sdk.onPositionUpdate((data) => {
|
||||
positionUpdateCallbacks.forEach((cb) => cb(data as PositionData & Record<string, unknown>))
|
||||
})
|
||||
sdk.onConnect(() => {})
|
||||
sdk.onDisconnect(() => {})
|
||||
sdk.onError((e) => {
|
||||
@ -96,15 +91,6 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 订阅 position_update 推送,返回取消订阅函数 */
|
||||
function onPositionUpdate(cb: (data: PositionData & Record<string, unknown>) => void): () => void {
|
||||
positionUpdateCallbacks.push(cb)
|
||||
return () => {
|
||||
const i = positionUpdateCallbacks.indexOf(cb)
|
||||
if (i >= 0) positionUpdateCallbacks.splice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function setUser(loginData: { token?: string; user?: UserInfo }) {
|
||||
const t = loginData.token ?? ''
|
||||
const raw = loginData.user ?? null
|
||||
@ -165,38 +151,36 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 请求用户信息(需已登录),更新 store 中的 user 与 balance */
|
||||
/** 请求用户信息(需已登录),更新 store 中的 user */
|
||||
async function fetchUserInfo() {
|
||||
const headers = getAuthHeaders()
|
||||
if (!headers) return
|
||||
try {
|
||||
const res = await getUserInfo(headers)
|
||||
const data = res.data as Record<string, unknown> | undefined
|
||||
if (res.code !== 0 && res.code !== 200) return
|
||||
// 更新余额:data.balance.available
|
||||
const bal = data?.balance as { available?: string } | undefined
|
||||
if (bal?.available != null) {
|
||||
balance.value = formatUsdcBalance(String(bal.available))
|
||||
// 接口返回 data.userInfo 或 data.user,取实际用户对象;若仍含 userInfo 则再取一层
|
||||
let u = (data?.userInfo ?? data?.user ?? data) as Record<string, unknown>
|
||||
if (u?.userInfo && (u.ID == null && u.id == null)) {
|
||||
u = u.userInfo as Record<string, unknown>
|
||||
}
|
||||
if ((res.code === 0 || res.code === 200) && u) {
|
||||
const rawId = u.ID ?? u.id
|
||||
const numId =
|
||||
typeof rawId === 'number'
|
||||
? rawId
|
||||
: rawId != null
|
||||
? parseInt(String(rawId), 10)
|
||||
: undefined
|
||||
user.value = {
|
||||
...u,
|
||||
userName: (u.userName ?? u.username) as string | undefined,
|
||||
nickName: (u.nickName ?? u.nickname) as string | undefined,
|
||||
headerImg: (u.headerImg ?? u.avatar ?? u.avatarUrl) as string | undefined,
|
||||
id: (rawId ?? numId) as number | string | undefined,
|
||||
ID: Number.isFinite(numId) ? numId : undefined,
|
||||
} as UserInfo
|
||||
if (token.value && user.value) saveToStorage(token.value, user.value)
|
||||
}
|
||||
// 更新用户信息:data.userInfo
|
||||
const u = (data?.userInfo ?? data?.user ?? data) as Record<string, unknown> | undefined
|
||||
if (!u) return
|
||||
const rawId = u.ID ?? u.id
|
||||
const numId =
|
||||
typeof rawId === 'number'
|
||||
? rawId
|
||||
: rawId != null
|
||||
? parseInt(String(rawId), 10)
|
||||
: undefined
|
||||
user.value = {
|
||||
...u,
|
||||
userName: (u.userName ?? u.username) as string | undefined,
|
||||
nickName: (u.nickName ?? u.nickname) as string | undefined,
|
||||
headerImg: (u.headerImg ?? u.avatar ?? u.avatarUrl) as string | undefined,
|
||||
id: (rawId ?? numId) as number | string | undefined,
|
||||
ID: Number.isFinite(numId) ? numId : undefined,
|
||||
} as UserInfo
|
||||
if (token.value && user.value) saveToStorage(token.value, user.value)
|
||||
} catch (e) {
|
||||
console.error('[fetchUserInfo] 请求失败:', e)
|
||||
}
|
||||
@ -215,6 +199,5 @@ export const useUserStore = defineStore('user', () => {
|
||||
fetchUserInfo,
|
||||
connectUserSocket,
|
||||
disconnectUserSocket,
|
||||
onPositionUpdate,
|
||||
}
|
||||
})
|
||||
|
||||
@ -58,35 +58,6 @@
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<!-- 规则说明(描述 + 裁决来源) -->
|
||||
<v-card
|
||||
v-if="eventDetail?.description || eventDetail?.resolutionSource"
|
||||
class="rules-card"
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
>
|
||||
<div class="rules-pane">
|
||||
<div v-if="eventDetail?.description" class="rules-section">
|
||||
<h3 class="rules-title">{{ t('activity.rulesDescription') }}</h3>
|
||||
<div class="rules-text">{{ eventDetail.description }}</div>
|
||||
</div>
|
||||
<div v-if="eventDetail?.resolutionSource" class="rules-section">
|
||||
<h3 class="rules-title">{{ t('activity.rulesSource') }}</h3>
|
||||
<a
|
||||
v-if="isResolutionSourceUrl"
|
||||
:href="eventDetail.resolutionSource"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="rules-link"
|
||||
>
|
||||
{{ eventDetail.resolutionSource }}
|
||||
<v-icon size="14">mdi-open-in-new</v-icon>
|
||||
</a>
|
||||
<div v-else class="rules-text">{{ eventDetail.resolutionSource }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<!-- 市场列表 -->
|
||||
<v-card class="markets-list-card" elevation="0" rounded="lg">
|
||||
<div class="markets-list">
|
||||
@ -226,13 +197,11 @@ import { USE_MOCK_EVENT } from '../config/mock'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { useToastStore } from '../stores/toast'
|
||||
import { useLocaleStore } from '../stores/locale'
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const localeStore = useLocaleStore()
|
||||
const { mobile } = useDisplay()
|
||||
const isMobile = computed(() => mobile.value)
|
||||
|
||||
@ -318,11 +287,6 @@ const resolutionDate = computed(() => {
|
||||
return s ? s.replace(/,?\s*\d{4}$/, '').trim() || '' : ''
|
||||
})
|
||||
|
||||
const isResolutionSourceUrl = computed(() => {
|
||||
const src = eventDetail.value?.resolutionSource
|
||||
return typeof src === 'string' && (src.startsWith('http://') || src.startsWith('https://'))
|
||||
})
|
||||
|
||||
const timeRanges = [
|
||||
{ label: '1H', value: '1H' },
|
||||
{ label: '6H', value: '6H' },
|
||||
@ -736,14 +700,6 @@ watch(
|
||||
() => route.params.id,
|
||||
() => loadEventDetail(),
|
||||
)
|
||||
|
||||
// 监听语言切换,语言变化时重新加载数据
|
||||
watch(
|
||||
() => localeStore.currentLocale,
|
||||
() => {
|
||||
loadEventDetail()
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -943,58 +899,7 @@ watch(
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.rules-card {
|
||||
margin-top: 16px;
|
||||
border: 1px solid #e7e7e7;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.rules-card .rules-pane {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.rules-card .rules-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.rules-card .rules-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.rules-card .rules-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
margin: 0 0 8px 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.rules-card .rules-text {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.rules-card .rules-link {
|
||||
font-size: 14px;
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.rules-card .rules-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markets-list-card {
|
||||
margin-top: 16px;
|
||||
border: 1px solid #e7e7e7;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
@ -165,8 +165,6 @@
|
||||
:is-new="card.isNew"
|
||||
:market-id="card.marketId"
|
||||
:clob-token-ids="card.clobTokenIds"
|
||||
:yes-price="card.yesPrice"
|
||||
:no-price="card.noPrice"
|
||||
@open-trade="onCardOpenTrade"
|
||||
/>
|
||||
<div v-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
|
||||
@ -304,7 +302,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'Home' })
|
||||
import { ref, onMounted, onUnmounted, onActivated, onDeactivated, nextTick, computed, watch } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, onActivated, onDeactivated, nextTick, computed } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import MarketCard from '../components/MarketCard.vue'
|
||||
import TradeComponent from '../components/TradeComponent.vue'
|
||||
@ -328,7 +326,6 @@ import { USE_MOCK_CATEGORY } from '../config/mock'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSearchHistory } from '../composables/useSearchHistory'
|
||||
import { useToastStore } from '../stores/toast'
|
||||
import { useLocaleStore } from '../stores/locale'
|
||||
|
||||
const { mobile } = useDisplay()
|
||||
const { t } = useI18n()
|
||||
@ -416,28 +413,16 @@ function findNodeById(nodes: CategoryTreeNode[], id: string): CategoryTreeNode |
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 当前选中分类的 tagIds:收集所有选中层级节点的 tagId 数组(含父级),用于事件筛选 */
|
||||
const activeTagIds = computed(() => {
|
||||
const activeIds = layerActiveValues.value
|
||||
const tagIdSet = new Set<number>()
|
||||
|
||||
// 遍历每一层选中的节点,收集所有 tagIds(含父级)
|
||||
let currentNodes = filterVisible(categoryTree.value)
|
||||
for (let i = 0; i < activeIds.length; i++) {
|
||||
const selectedId = activeIds[i]
|
||||
if (!selectedId) continue
|
||||
|
||||
const node = currentNodes.find((n) => n.id === selectedId)
|
||||
if (node?.tagIds && node.tagIds.length > 0) {
|
||||
// 合并当前选中节点的 tagIds
|
||||
node.tagIds.forEach((id) => tagIdSet.add(id))
|
||||
}
|
||||
|
||||
// 进入下一层
|
||||
currentNodes = filterVisible(node?.children)
|
||||
}
|
||||
|
||||
return Array.from(tagIdSet)
|
||||
/** 当前选中分类的 tag 筛选(取最后选中的层级,用于 API tagId/tagSlug) */
|
||||
const activeTagFilter = computed(() => {
|
||||
const ids = layerActiveValues.value
|
||||
if (ids.length === 0) return { tagId: undefined as number | undefined, tagSlug: undefined as string | undefined }
|
||||
const lastId = ids[ids.length - 1]
|
||||
const root = filterVisible(categoryTree.value)
|
||||
const node = lastId ? findNodeById(root, lastId) : undefined
|
||||
if (!node) return { tagId: undefined, tagSlug: undefined }
|
||||
const tagId = /^\d+$/.test(node.id) ? parseInt(node.id, 10) : undefined
|
||||
return { tagId, tagSlug: node.slug || undefined }
|
||||
})
|
||||
|
||||
/** 当前展示的层级数据:[[layer0], [layer1]?, [layer2]?] */
|
||||
@ -529,23 +514,12 @@ const tradeDialogMarket = ref<{
|
||||
clobTokenIds?: string[]
|
||||
yesLabel?: string
|
||||
noLabel?: string
|
||||
yesPrice?: number
|
||||
noPrice?: number
|
||||
} | null>(null)
|
||||
const scrollRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function onCardOpenTrade(
|
||||
side: 'yes' | 'no',
|
||||
market?: {
|
||||
id: string
|
||||
title: string
|
||||
marketId?: string
|
||||
yesLabel?: string
|
||||
noLabel?: string
|
||||
yesPrice?: number
|
||||
noPrice?: number
|
||||
clobTokenIds?: string[]
|
||||
},
|
||||
market?: { id: string; title: string; marketId?: string; yesLabel?: string; noLabel?: string },
|
||||
) {
|
||||
tradeDialogSide.value = side
|
||||
tradeDialogMarket.value = market ?? null
|
||||
@ -553,18 +527,6 @@ function onCardOpenTrade(
|
||||
}
|
||||
|
||||
const toastStore = useToastStore()
|
||||
const localeStore = useLocaleStore()
|
||||
|
||||
// 监听语言切换,语言变化时重新加载事件列表
|
||||
watch(
|
||||
() => localeStore.currentLocale,
|
||||
() => {
|
||||
clearEventListCache()
|
||||
eventPage.value = 1
|
||||
loadEvents(1, false, activeSearchKeyword.value)
|
||||
},
|
||||
)
|
||||
|
||||
function onOrderSuccess() {
|
||||
tradeDialogOpen.value = false
|
||||
toastStore.show(t('toast.orderSuccess'))
|
||||
@ -575,14 +537,9 @@ const homeTradeMarketPayload = computed(() => {
|
||||
const m = tradeDialogMarket.value
|
||||
if (!m) return undefined
|
||||
const marketId = m.marketId ?? m.id
|
||||
const yesPrice =
|
||||
m.yesPrice != null && Number.isFinite(m.yesPrice)
|
||||
? Math.min(1, Math.max(0, m.yesPrice))
|
||||
: 0.5
|
||||
const noPrice =
|
||||
m.noPrice != null && Number.isFinite(m.noPrice)
|
||||
? Math.min(1, Math.max(0, m.noPrice))
|
||||
: 1 - yesPrice
|
||||
const chance = 50
|
||||
const yesPrice = Math.min(1, Math.max(0, chance / 100))
|
||||
const noPrice = 1 - yesPrice
|
||||
const outcomes =
|
||||
m.yesLabel != null || m.noLabel != null
|
||||
? [m.yesLabel ?? 'Yes', m.noLabel ?? 'No']
|
||||
@ -621,13 +578,14 @@ const activeSearchKeyword = ref('')
|
||||
/** 请求事件列表并追加或覆盖到 eventList(公开接口,无需鉴权);成功后会更新内存缓存 */
|
||||
async function loadEvents(page: number, append: boolean, keyword?: string) {
|
||||
const kw = keyword !== undefined ? keyword : activeSearchKeyword.value
|
||||
const tagIds = activeTagIds.value
|
||||
const { tagId, tagSlug } = activeTagFilter.value
|
||||
try {
|
||||
const res = await getPmEventPublic({
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
keyword: kw || undefined,
|
||||
tagIds: tagIds.length > 0 ? tagIds : undefined,
|
||||
tagId,
|
||||
tagSlug,
|
||||
})
|
||||
if (res.code !== 0 && res.code !== 200) {
|
||||
throw new Error(res.msg || '请求失败')
|
||||
@ -680,7 +638,7 @@ function checkScrollLoad() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
/** 分类树就绪后加载列表(确保 activeTagIds 已计算,与下拉刷新参数一致) */
|
||||
/** 分类树就绪后加载列表(确保 activeTagFilter 已计算,与下拉刷新参数一致) */
|
||||
function loadEventListAfterCategoryReady() {
|
||||
const cached = getEventListCache()
|
||||
if (cached && cached.list.length > 0) {
|
||||
|
||||
@ -44,78 +44,6 @@
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<!-- 持仓 / 限价(订单簿上方) -->
|
||||
<v-card class="positions-orders-card" elevation="0" rounded="lg">
|
||||
<v-tabs v-model="positionsOrdersTab" class="positions-orders-tabs" density="comfortable">
|
||||
<v-tab value="positions">{{ t('activity.myPositions') }}</v-tab>
|
||||
<v-tab value="orders">{{ t('activity.openOrders') }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="positionsOrdersTab" class="positions-orders-window">
|
||||
<v-window-item value="positions" class="detail-pane">
|
||||
<div v-if="positionLoading" class="placeholder-pane">{{ t('common.loading') }}</div>
|
||||
<div v-else-if="marketPositionsFiltered.length === 0" class="placeholder-pane">
|
||||
{{ t('activity.noPositionsInMarket') }}
|
||||
</div>
|
||||
<div v-else class="positions-list">
|
||||
<div
|
||||
v-for="pos in marketPositionsFiltered"
|
||||
:key="pos.id"
|
||||
class="position-row-item"
|
||||
>
|
||||
<div class="position-row-main">
|
||||
<span :class="['position-outcome-pill', pos.outcomePillClass]">{{ pos.outcomeTag }}</span>
|
||||
<span class="position-shares">{{ pos.shares }}</span>
|
||||
<span class="position-value">{{ pos.value }}</span>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
size="small"
|
||||
color="primary"
|
||||
class="position-sell-btn"
|
||||
@click="openSellFromPosition(pos)"
|
||||
>
|
||||
{{ t('trade.sell') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="position-row-meta">{{ pos.bet }} → {{ pos.toWin }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-window-item>
|
||||
<v-window-item value="orders" class="detail-pane">
|
||||
<div v-if="openOrderLoading" class="placeholder-pane">{{ t('common.loading') }}</div>
|
||||
<div v-else-if="marketOpenOrders.length === 0" class="placeholder-pane">
|
||||
{{ t('activity.noOpenOrdersInMarket') }}
|
||||
</div>
|
||||
<div v-else class="orders-list">
|
||||
<div
|
||||
v-for="ord in marketOpenOrders"
|
||||
:key="ord.id"
|
||||
class="order-row-item"
|
||||
>
|
||||
<div class="order-row-main">
|
||||
<span :class="['order-side-pill', ord.side === 'Yes' ? 'side-yes' : 'side-no']">
|
||||
{{ ord.actionLabel || `Buy ${ord.outcome}` }}
|
||||
</span>
|
||||
<span class="order-price">{{ ord.price }}</span>
|
||||
<span class="order-filled">{{ ord.filled }}</span>
|
||||
<span class="order-total">{{ ord.total }}</span>
|
||||
</div>
|
||||
<div class="order-row-actions">
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
:disabled="cancelOrderLoading"
|
||||
@click="cancelMarketOrder(ord)"
|
||||
>
|
||||
{{ t('activity.cancelOrder') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card>
|
||||
|
||||
<!-- Order Book Section -->
|
||||
<v-card class="order-book-card" elevation="0" rounded="lg">
|
||||
<OrderBook
|
||||
@ -137,37 +65,13 @@
|
||||
<!-- Comments / Top Holders / Activity(与左侧图表、订单簿同宽) -->
|
||||
<v-card class="activity-card" elevation="0" rounded="lg">
|
||||
<v-tabs v-model="detailTab" class="detail-tabs" density="comfortable">
|
||||
<v-tab value="rules">{{ t('activity.rules') }}</v-tab>
|
||||
<v-tab value="comments">{{ t('activity.comments') }}</v-tab>
|
||||
<v-tab value="holders">{{ t('activity.topHolders') }}</v-tab>
|
||||
<v-tab value="activity">{{ t('activity.activity') }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="detailTab" class="detail-window">
|
||||
<v-window-item value="rules" class="detail-pane">
|
||||
<div class="rules-pane">
|
||||
<div v-if="!eventDetail?.description && !eventDetail?.resolutionSource" class="placeholder-pane">
|
||||
{{ t('activity.rulesEmpty') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="eventDetail?.description" class="rules-section">
|
||||
<h3 class="rules-title">{{ t('activity.rulesDescription') }}</h3>
|
||||
<div class="rules-text">{{ eventDetail.description }}</div>
|
||||
</div>
|
||||
<div v-if="eventDetail?.resolutionSource" class="rules-section">
|
||||
<h3 class="rules-title">{{ t('activity.rulesSource') }}</h3>
|
||||
<a
|
||||
v-if="isResolutionSourceUrl"
|
||||
:href="eventDetail.resolutionSource"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="rules-link"
|
||||
>
|
||||
{{ eventDetail.resolutionSource }}
|
||||
<v-icon size="14">mdi-open-in-new</v-icon>
|
||||
</a>
|
||||
<div v-else class="rules-text">{{ eventDetail.resolutionSource }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<v-window-item value="comments" class="detail-pane">
|
||||
<div class="placeholder-pane">{{ t('activity.noCommentsYet') }}</div>
|
||||
</v-window-item>
|
||||
<v-window-item value="holders" class="detail-pane">
|
||||
<div class="placeholder-pane">{{ t('activity.topHoldersPlaceholder') }}</div>
|
||||
@ -225,14 +129,7 @@
|
||||
<!-- 右侧:交易组件(固定宽度),传入当前市场以便 Split 调用拆单接口;移动端隐藏,改用底部栏+弹窗 -->
|
||||
<v-col v-if="!isMobile" cols="12" class="trade-col">
|
||||
<div class="trade-sidebar">
|
||||
<TradeComponent
|
||||
ref="tradeComponentRef"
|
||||
:market="tradeMarketPayload"
|
||||
:initial-option="tradeInitialOption"
|
||||
:positions="tradePositionsForComponent"
|
||||
@merge-success="onMergeSuccess"
|
||||
@split-success="onSplitSuccess"
|
||||
/>
|
||||
<TradeComponent ref="tradeComponentRef" :market="tradeMarketPayload" :initial-option="tradeInitialOption" />
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
@ -289,34 +186,11 @@
|
||||
ref="mobileTradeComponentRef"
|
||||
:market="tradeMarketPayload"
|
||||
:initial-option="tradeInitialOptionFromBar"
|
||||
:initial-tab="tradeInitialTabFromBar"
|
||||
:positions="tradePositionsForComponent"
|
||||
embedded-in-sheet
|
||||
@order-success="onOrderSuccess"
|
||||
@merge-success="onMergeSuccess"
|
||||
@split-success="onSplitSuccess"
|
||||
/>
|
||||
</v-bottom-sheet>
|
||||
</template>
|
||||
|
||||
<!-- 从持仓点击 Sell 弹出的交易组件(桌面/移动端通用) -->
|
||||
<v-dialog
|
||||
v-model="sellDialogOpen"
|
||||
max-width="420"
|
||||
content-class="trade-detail-sell-dialog"
|
||||
transition="dialog-transition"
|
||||
>
|
||||
<TradeComponent
|
||||
v-if="sellDialogOpen"
|
||||
:market="tradeMarketPayload"
|
||||
:initial-option="sellInitialOption"
|
||||
:initial-tab="'sell'"
|
||||
:positions="tradePositionsForComponent"
|
||||
@order-success="onSellOrderSuccess"
|
||||
@merge-success="onMergeSuccess"
|
||||
@split-success="onSplitSuccess"
|
||||
/>
|
||||
</v-dialog>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
@ -329,7 +203,7 @@ import { useDisplay } from 'vuetify'
|
||||
import * as echarts from 'echarts'
|
||||
import type { ECharts } from 'echarts'
|
||||
import OrderBook from '../components/OrderBook.vue'
|
||||
import TradeComponent, { type TradePositionItem } from '../components/TradeComponent.vue'
|
||||
import TradeComponent from '../components/TradeComponent.vue'
|
||||
import {
|
||||
findPmEvent,
|
||||
getMarketId,
|
||||
@ -340,16 +214,6 @@ import {
|
||||
import { getClobWsUrl } from '../api/request'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { useToastStore } from '../stores/toast'
|
||||
import { useLocaleStore } from '../stores/locale'
|
||||
import { useAuthError } from '../composables/useAuthError'
|
||||
import { getPositionList, mapPositionToDisplayItem, type PositionDisplayItem } from '../api/position'
|
||||
import {
|
||||
getOrderList,
|
||||
mapOrderToOpenOrderItem,
|
||||
OrderStatus,
|
||||
type OpenOrderDisplayItem,
|
||||
} from '../api/order'
|
||||
import { cancelOrder as apiCancelOrder } from '../api/order'
|
||||
|
||||
const { t } = useI18n()
|
||||
import {
|
||||
@ -387,8 +251,6 @@ export type ChartIncrement = { point: ChartPoint }
|
||||
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const { formatAuthError } = useAuthError()
|
||||
const localeStore = useLocaleStore()
|
||||
const { mobile } = useDisplay()
|
||||
const isMobile = computed(() => mobile.value)
|
||||
|
||||
@ -460,7 +322,7 @@ async function loadEventDetail() {
|
||||
eventDetail.value = null
|
||||
}
|
||||
} catch (e) {
|
||||
detailError.value = formatAuthError(e, t('error.loadFailed'))
|
||||
detailError.value = e instanceof Error ? e.message : t('error.loadFailed')
|
||||
eventDetail.value = null
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
@ -485,11 +347,6 @@ const resolutionDate = computed(() => {
|
||||
return s ? s.replace(/,?\s*\d{4}$/, '').trim() || 'Mar 31' : 'Mar 31'
|
||||
})
|
||||
|
||||
const isResolutionSourceUrl = computed(() => {
|
||||
const src = eventDetail.value?.resolutionSource
|
||||
return typeof src === 'string' && (src.startsWith('http://') || src.startsWith('https://'))
|
||||
})
|
||||
|
||||
/** 当前市场(用于交易组件与 Split 拆单):query.marketId 匹配或取第一个 */
|
||||
const currentMarket = computed(() => {
|
||||
const list = eventDetail.value?.markets ?? []
|
||||
@ -522,19 +379,6 @@ const orderBookAsksYes = computed(() => orderBookByToken.value[0]?.asks ?? [])
|
||||
const orderBookBidsYes = computed(() => orderBookByToken.value[0]?.bids ?? [])
|
||||
const orderBookAsksNo = computed(() => orderBookByToken.value[1]?.asks ?? [])
|
||||
const orderBookBidsNo = computed(() => orderBookByToken.value[1]?.bids ?? [])
|
||||
|
||||
/** 订单簿 Yes 卖单最低价(分),无数据时为 0 */
|
||||
const orderBookLowestAskYesCents = computed(() => {
|
||||
const asks = orderBookAsksYes.value
|
||||
if (!asks.length) return 0
|
||||
return Math.min(...asks.map((a) => a.price))
|
||||
})
|
||||
/** 订单簿 No 卖单最低价(分),无数据时为 0 */
|
||||
const orderBookLowestAskNoCents = computed(() => {
|
||||
const asks = orderBookAsksNo.value
|
||||
if (!asks.length) return 0
|
||||
return Math.min(...asks.map((a) => a.price))
|
||||
})
|
||||
const clobLastPriceYes = computed(() => clobLastPriceByToken.value[0])
|
||||
const clobLastPriceNo = computed(() => clobLastPriceByToken.value[1])
|
||||
const clobSpreadYes = computed(() => clobSpreadByToken.value[0])
|
||||
@ -675,12 +519,14 @@ function disconnectClob() {
|
||||
clobLoading.value = false
|
||||
}
|
||||
|
||||
/** 传给 TradeComponent 的 market,供 Split 调用 /PmMarket/split;yesPrice/noPrice 取订单簿卖单最低价,无数据时为 0 */
|
||||
/** 传给 TradeComponent 的 market,供 Split 调用 /PmMarket/split;接口未返回时用 query 兜底 */
|
||||
const tradeMarketPayload = computed(() => {
|
||||
const m = currentMarket.value
|
||||
const yesPrice = orderBookLowestAskYesCents.value / 100
|
||||
const noPrice = orderBookLowestAskNoCents.value / 100
|
||||
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,
|
||||
@ -692,6 +538,9 @@ const tradeMarketPayload = computed(() => {
|
||||
}
|
||||
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,
|
||||
@ -710,14 +559,8 @@ const tradeInitialOption = computed(() => {
|
||||
|
||||
/** 移动端底部栏点击 Yes/No 时传给弹窗内 TradeComponent 的初始选项 */
|
||||
const tradeInitialOptionFromBar = ref<'yes' | 'no' | undefined>(undefined)
|
||||
/** 移动端弹窗初始 Tab:从持仓 Sell 打开时为 'sell',从底部栏 Yes/No 打开时为 undefined(默认 Buy) */
|
||||
const tradeInitialTabFromBar = ref<'buy' | 'sell' | undefined>(undefined)
|
||||
/** 移动端交易弹窗开关 */
|
||||
const tradeSheetOpen = ref(false)
|
||||
/** 从持仓 Sell 打开的弹窗 */
|
||||
const sellDialogOpen = ref(false)
|
||||
/** 从持仓 Sell 时预选的 Yes/No */
|
||||
const sellInitialOption = ref<'yes' | 'no'>('yes')
|
||||
/** 移动端三点菜单开关 */
|
||||
const mobileMenuOpen = ref(false)
|
||||
/** 桌面端 TradeComponent 引用(Merge/Split) */
|
||||
@ -736,7 +579,6 @@ const noLabel = computed(() => currentMarket.value?.outcomes?.[1] ?? 'No')
|
||||
|
||||
function openSheetWithOption(side: 'yes' | 'no') {
|
||||
tradeInitialOptionFromBar.value = side
|
||||
tradeInitialTabFromBar.value = undefined
|
||||
tradeSheetOpen.value = true
|
||||
}
|
||||
|
||||
@ -760,179 +602,11 @@ const toastStore = useToastStore()
|
||||
function onOrderSuccess() {
|
||||
tradeSheetOpen.value = false
|
||||
toastStore.show(t('toast.orderSuccess'))
|
||||
loadMarketPositions()
|
||||
loadMarketOpenOrders()
|
||||
}
|
||||
|
||||
/** 合并成功后刷新持仓(根据 tokenId 更新本地持仓数据) */
|
||||
function onMergeSuccess() {
|
||||
toastStore.show(t('toast.mergeSuccess'))
|
||||
loadMarketPositions()
|
||||
}
|
||||
|
||||
/** 拆分成功后刷新持仓 */
|
||||
function onSplitSuccess() {
|
||||
loadMarketPositions()
|
||||
}
|
||||
|
||||
/** 从持仓项点击 Sell:弹出交易组件并切到 Sell、对应 Yes/No。移动端直接开底部弹窗,桌面端开 Dialog */
|
||||
function openSellFromPosition(pos: PositionDisplayItem) {
|
||||
const option = pos.outcomeWord === 'No' ? 'no' : 'yes'
|
||||
if (isMobile.value) {
|
||||
tradeInitialOptionFromBar.value = option
|
||||
tradeInitialTabFromBar.value = 'sell'
|
||||
tradeSheetOpen.value = true
|
||||
} else {
|
||||
sellInitialOption.value = option
|
||||
sellDialogOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function onSellOrderSuccess() {
|
||||
sellDialogOpen.value = false
|
||||
onOrderSuccess()
|
||||
}
|
||||
|
||||
// 当前市场的 marketID,用于筛选持仓和订单
|
||||
const currentMarketId = computed(() => getMarketId(currentMarket.value))
|
||||
|
||||
// 持仓列表(仅当前市场)
|
||||
const marketPositions = ref<PositionDisplayItem[]>([])
|
||||
const positionLoading = ref(false)
|
||||
|
||||
/** 过滤掉份额为 0 的持仓项 */
|
||||
const marketPositionsFiltered = computed(() =>
|
||||
marketPositions.value.filter((p) => {
|
||||
const n = parseFloat(p.shares?.replace(/[^0-9.]/g, '') ?? '')
|
||||
return Number.isFinite(n) && n > 0
|
||||
}),
|
||||
)
|
||||
|
||||
/** 转为 TradeComponent 所需的 TradePositionItem[],保证 outcomeWord 为 'Yes' | 'No'(仅含份额>0) */
|
||||
const tradePositionsForComponent = computed<TradePositionItem[]>(() =>
|
||||
marketPositionsFiltered.value.map((p) => ({
|
||||
id: p.id,
|
||||
outcomeWord: (p.outcomeWord === 'No' ? 'No' : 'Yes') as 'Yes' | 'No',
|
||||
shares: p.shares,
|
||||
sharesNum: parseFloat(p.shares?.replace(/[^0-9.]/g, '')) || undefined,
|
||||
}))
|
||||
)
|
||||
|
||||
async function loadMarketPositions() {
|
||||
const marketID = currentMarketId.value
|
||||
if (!marketID) {
|
||||
marketPositions.value = []
|
||||
return
|
||||
}
|
||||
const headers = userStore.getAuthHeaders()
|
||||
if (!headers) {
|
||||
marketPositions.value = []
|
||||
return
|
||||
}
|
||||
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||
const userID = uid != null ? Number(uid) : undefined
|
||||
if (!userID || !Number.isFinite(userID)) {
|
||||
marketPositions.value = []
|
||||
return
|
||||
}
|
||||
positionLoading.value = true
|
||||
try {
|
||||
const res = await getPositionList(
|
||||
{ page: 1, pageSize: 50, marketID, userID },
|
||||
{ headers },
|
||||
)
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
marketPositions.value = res.data?.list?.map(mapPositionToDisplayItem) ?? []
|
||||
} else {
|
||||
marketPositions.value = []
|
||||
}
|
||||
} catch {
|
||||
marketPositions.value = []
|
||||
} finally {
|
||||
positionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 限价未成交订单(仅当前市场)
|
||||
const marketOpenOrders = ref<OpenOrderDisplayItem[]>([])
|
||||
const openOrderLoading = ref(false)
|
||||
|
||||
async function loadMarketOpenOrders() {
|
||||
const marketID = currentMarketId.value
|
||||
if (!marketID) {
|
||||
marketOpenOrders.value = []
|
||||
return
|
||||
}
|
||||
const headers = userStore.getAuthHeaders()
|
||||
if (!headers) {
|
||||
marketOpenOrders.value = []
|
||||
return
|
||||
}
|
||||
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||
const userID = uid != null ? Number(uid) : undefined
|
||||
if (!userID || !Number.isFinite(userID)) {
|
||||
marketOpenOrders.value = []
|
||||
return
|
||||
}
|
||||
openOrderLoading.value = true
|
||||
try {
|
||||
const res = await getOrderList(
|
||||
{ page: 1, pageSize: 50, marketID, userID, status: OrderStatus.Live },
|
||||
{ headers },
|
||||
)
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
const list = res.data?.list ?? []
|
||||
const liveOnly = list.filter((o) => (o.status ?? 1) === OrderStatus.Live)
|
||||
marketOpenOrders.value = liveOnly.map(mapOrderToOpenOrderItem)
|
||||
} else {
|
||||
marketOpenOrders.value = []
|
||||
}
|
||||
} catch {
|
||||
marketOpenOrders.value = []
|
||||
} finally {
|
||||
openOrderLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const cancelOrderLoading = ref(false)
|
||||
|
||||
async function cancelMarketOrder(ord: OpenOrderDisplayItem) {
|
||||
const orderID = ord.orderID ?? 0
|
||||
const tokenID = ord.tokenID ?? ''
|
||||
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||
const userID = uid != null ? Number(uid) : 0
|
||||
if (!Number.isFinite(userID) || userID <= 0 || !tokenID) return
|
||||
const headers = userStore.getAuthHeaders()
|
||||
if (!headers) return
|
||||
cancelOrderLoading.value = true
|
||||
try {
|
||||
const res = await apiCancelOrder({ orderID, tokenID, userID }, { headers })
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
marketOpenOrders.value = marketOpenOrders.value.filter((o) => o.id !== ord.id)
|
||||
userStore.fetchUsdcBalance()
|
||||
}
|
||||
} finally {
|
||||
cancelOrderLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 持仓/限价 tab(订单簿上方)
|
||||
const positionsOrdersTab = ref<'positions' | 'orders'>('positions')
|
||||
|
||||
// 切换持仓/限价 tab 或市场变化时加载
|
||||
watch(
|
||||
[() => positionsOrdersTab.value, currentMarketId],
|
||||
([tab]) => {
|
||||
if (tab === 'positions') loadMarketPositions()
|
||||
else if (tab === 'orders') loadMarketOpenOrders()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// Comments / Top Holders / Activity
|
||||
const detailTab = ref('rules')
|
||||
const detailTab = ref('activity')
|
||||
const activityMinAmount = ref<string>('0')
|
||||
|
||||
const minAmountOptions = computed(() => [
|
||||
{ title: t('activity.any'), value: '0' },
|
||||
{ title: '$1', value: '1' },
|
||||
@ -1322,23 +996,6 @@ watch(
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
// 监听语言切换,语言变化时重新加载数据
|
||||
watch(
|
||||
() => localeStore.currentLocale,
|
||||
() => {
|
||||
loadEventDetail()
|
||||
loadMarketPositions()
|
||||
},
|
||||
)
|
||||
|
||||
// 订阅 position_update,收到当前市场的推送时刷新持仓
|
||||
const unsubscribePositionUpdate = userStore.onPositionUpdate((data) => {
|
||||
const marketID = data.marketID ?? (data as Record<string, unknown>).market_id
|
||||
if (marketID && String(marketID) === String(currentMarketId.value)) {
|
||||
loadMarketPositions()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadEventDetail()
|
||||
initChart()
|
||||
@ -1347,7 +1004,6 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribePositionUpdate()
|
||||
stopDynamicUpdate()
|
||||
window.removeEventListener('resize', handleResize)
|
||||
chartInstance?.dispose()
|
||||
@ -1614,30 +1270,6 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
/* Order Book Card Styles(扁平化) */
|
||||
.positions-orders-card {
|
||||
margin-top: 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.positions-orders-tabs {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.positions-orders-tabs :deep(.v-tab) {
|
||||
text-transform: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.positions-orders-window {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.positions-orders-card .detail-pane {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.order-book-card {
|
||||
margin-top: 16px;
|
||||
padding: 0;
|
||||
@ -1794,124 +1426,6 @@ onUnmounted(() => {
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.rules-pane {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.rules-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.rules-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.rules-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
margin: 0 0 8px 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.rules-text {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.rules-link {
|
||||
font-size: 14px;
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.rules-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 持仓 / 限价订单列表 */
|
||||
.positions-list,
|
||||
.orders-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.position-row-item,
|
||||
.order-row-item {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.position-row-item:last-child,
|
||||
.order-row-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.position-row-main,
|
||||
.order-row-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.position-sell-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.position-outcome-pill,
|
||||
.order-side-pill {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.position-outcome-pill.pill-yes,
|
||||
.order-side-pill.side-yes {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.position-outcome-pill.pill-down,
|
||||
.order-side-pill.side-no {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.position-shares,
|
||||
.position-value,
|
||||
.order-price,
|
||||
.order-filled,
|
||||
.order-total {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.position-row-meta {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.order-row-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.order-row-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -35,15 +35,6 @@
|
||||
>
|
||||
{{ t('wallet.withdraw') }}
|
||||
</v-btn>
|
||||
<!-- <v-btn
|
||||
variant="outlined"
|
||||
color="grey"
|
||||
class="action-btn"
|
||||
prepend-icon="mdi-shield-check-outline"
|
||||
@click="onAuthorizeClick"
|
||||
>
|
||||
{{ t('wallet.authorize') }}
|
||||
</v-btn> -->
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@ -154,10 +145,7 @@
|
||||
<template v-if="activeTab === 'positions'">
|
||||
<!-- 移动端:可折叠列表 -->
|
||||
<div v-if="mobile" class="positions-mobile-list">
|
||||
<template v-if="positionLoading">
|
||||
<div class="empty-cell">{{ t('common.loading') }}</div>
|
||||
</template>
|
||||
<template v-else-if="filteredPositions.length === 0">
|
||||
<template v-if="filteredPositions.length === 0">
|
||||
<div class="empty-cell">{{ t('wallet.noPositionsFound') }}</div>
|
||||
</template>
|
||||
<div
|
||||
@ -259,10 +247,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="positionLoading">
|
||||
<td colspan="6" class="empty-cell">{{ t('common.loading') }}</td>
|
||||
</tr>
|
||||
<tr v-else-if="filteredPositions.length === 0">
|
||||
<tr v-if="filteredPositions.length === 0">
|
||||
<td colspan="6" class="empty-cell">{{ t('wallet.noPositionsFound') }}</td>
|
||||
</tr>
|
||||
<tr v-for="pos in paginatedPositions" :key="pos.id" class="position-row">
|
||||
@ -331,10 +316,7 @@
|
||||
<template v-else-if="activeTab === 'orders'">
|
||||
<!-- 移动端:挂单卡片列表 -->
|
||||
<div v-if="mobile" class="orders-mobile-list">
|
||||
<template v-if="openOrderLoading">
|
||||
<div class="empty-cell">{{ t('common.loading') }}</div>
|
||||
</template>
|
||||
<template v-else-if="filteredOpenOrders.length === 0">
|
||||
<template v-if="filteredOpenOrders.length === 0">
|
||||
<div class="empty-cell">{{ t('wallet.noOpenOrdersFound') }}</div>
|
||||
</template>
|
||||
<div v-for="ord in paginatedOpenOrders" :key="ord.id" class="order-mobile-card">
|
||||
@ -383,10 +365,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="openOrderLoading">
|
||||
<td colspan="8" class="empty-cell">{{ t('common.loading') }}</td>
|
||||
</tr>
|
||||
<tr v-else-if="filteredOpenOrders.length === 0">
|
||||
<tr v-if="filteredOpenOrders.length === 0">
|
||||
<td colspan="8" class="empty-cell">{{ t('wallet.noOpenOrdersFound') }}</td>
|
||||
</tr>
|
||||
<tr v-for="ord in paginatedOpenOrders" :key="ord.id">
|
||||
@ -417,10 +396,7 @@
|
||||
<template v-else-if="activeTab === 'history'">
|
||||
<!-- 移动端:历史卡片列表 -->
|
||||
<div v-if="mobile" class="history-mobile-list">
|
||||
<template v-if="historyLoading">
|
||||
<div class="empty-cell">{{ t('common.loading') }}</div>
|
||||
</template>
|
||||
<template v-else-if="filteredHistory.length === 0">
|
||||
<template v-if="filteredHistory.length === 0">
|
||||
<div class="empty-cell">{{ t('wallet.noHistoryFound') }}</div>
|
||||
</template>
|
||||
<div
|
||||
@ -486,10 +462,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="historyLoading">
|
||||
<td colspan="3" class="empty-cell">{{ t('common.loading') }}</td>
|
||||
</tr>
|
||||
<tr v-else-if="filteredHistory.length === 0">
|
||||
<tr v-if="filteredHistory.length === 0">
|
||||
<td colspan="3" class="empty-cell">{{ t('wallet.noHistoryFound') }}</td>
|
||||
</tr>
|
||||
<tr v-for="h in paginatedHistory" :key="h.id">
|
||||
@ -537,33 +510,6 @@
|
||||
@success="onWithdrawSuccess"
|
||||
/>
|
||||
|
||||
<!-- 授权弹窗 -->
|
||||
<v-dialog
|
||||
v-model="authorizeDialogOpen"
|
||||
max-width="420"
|
||||
persistent
|
||||
transition="dialog-transition"
|
||||
>
|
||||
<v-card rounded="lg">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-2">mdi-shield-check-outline</v-icon>
|
||||
{{ t('wallet.authorize') }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
{{ t('wallet.authorizeDesc') }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="authorizeDialogOpen = false">
|
||||
{{ t('deposit.close') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="flat" @click="submitAuthorize">
|
||||
{{ t('wallet.authorize') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Sell position dialog -->
|
||||
<v-dialog
|
||||
v-model="sellDialogOpen"
|
||||
@ -626,11 +572,7 @@ import type { ECharts } from 'echarts'
|
||||
import DepositDialog from '../components/DepositDialog.vue'
|
||||
import WithdrawDialog from '../components/WithdrawDialog.vue'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { useLocaleStore } from '../stores/locale'
|
||||
import { useAuthError } from '../composables/useAuthError'
|
||||
import { cancelOrder as apiCancelOrder } from '../api/order'
|
||||
import { getOrderList, mapOrderToHistoryItem, mapOrderToOpenOrderItem, OrderStatus } from '../api/order'
|
||||
import { getPositionList, mapPositionToDisplayItem } from '../api/position'
|
||||
import { pmCancelOrder } from '../api/market'
|
||||
import {
|
||||
MOCK_TOKEN_ID,
|
||||
MOCK_WALLET_POSITIONS,
|
||||
@ -638,12 +580,9 @@ import {
|
||||
MOCK_WALLET_HISTORY,
|
||||
} from '../api/mockData'
|
||||
import { USE_MOCK_WALLET } from '../config/mock'
|
||||
import { CrossChainUSDTAuth } from '../../sdk/approve'
|
||||
|
||||
const { mobile } = useDisplay()
|
||||
const userStore = useUserStore()
|
||||
const { formatAuthError } = useAuthError()
|
||||
const localeStore = useLocaleStore()
|
||||
const portfolioBalance = computed(() => userStore.balance)
|
||||
const profitLoss = ref('0.00')
|
||||
const plRange = ref('ALL')
|
||||
@ -657,7 +596,6 @@ const activeTab = ref<'positions' | 'orders' | 'history'>('positions')
|
||||
const search = ref('')
|
||||
const depositDialogOpen = ref(false)
|
||||
const withdrawDialogOpen = ref(false)
|
||||
const authorizeDialogOpen = ref(false)
|
||||
const sellDialogOpen = ref(false)
|
||||
const sellPositionItem = ref<Position | null>(null)
|
||||
/** 移动端展开的持仓 id,null 表示全部折叠 */
|
||||
@ -741,164 +679,20 @@ interface HistoryItem {
|
||||
const positions = ref<Position[]>(
|
||||
USE_MOCK_WALLET ? [...MOCK_WALLET_POSITIONS] : [],
|
||||
)
|
||||
/** 持仓列表(API 数据,非 mock 时使用) */
|
||||
const positionList = ref<Position[]>([])
|
||||
const positionTotal = ref(0)
|
||||
const positionLoading = ref(false)
|
||||
|
||||
async function loadPositionList() {
|
||||
if (USE_MOCK_WALLET) return
|
||||
const headers = userStore.getAuthHeaders()
|
||||
if (!headers) {
|
||||
positionList.value = []
|
||||
positionTotal.value = 0
|
||||
return
|
||||
}
|
||||
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||
const userID = uid != null ? Number(uid) : undefined
|
||||
if (!userID || !Number.isFinite(userID)) {
|
||||
positionList.value = []
|
||||
positionTotal.value = 0
|
||||
return
|
||||
}
|
||||
positionLoading.value = true
|
||||
try {
|
||||
const res = await getPositionList(
|
||||
{ page: page.value, pageSize: itemsPerPage.value, userID },
|
||||
{ headers },
|
||||
)
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
const list = res.data?.list ?? []
|
||||
positionList.value = list.map(mapPositionToDisplayItem)
|
||||
positionTotal.value = res.data?.total ?? 0
|
||||
} else {
|
||||
positionList.value = []
|
||||
positionTotal.value = 0
|
||||
}
|
||||
} catch {
|
||||
positionList.value = []
|
||||
positionTotal.value = 0
|
||||
} finally {
|
||||
positionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openOrders = ref<OpenOrder[]>(
|
||||
USE_MOCK_WALLET ? [...MOCK_WALLET_ORDERS] : [],
|
||||
)
|
||||
/** 未成交订单(API 数据,非 mock 时使用) */
|
||||
const openOrderList = ref<OpenOrder[]>([])
|
||||
const openOrderTotal = ref(0)
|
||||
const openOrderLoading = ref(false)
|
||||
|
||||
async function loadOpenOrders() {
|
||||
if (USE_MOCK_WALLET) return
|
||||
const headers = userStore.getAuthHeaders()
|
||||
if (!headers) {
|
||||
openOrderList.value = []
|
||||
openOrderTotal.value = 0
|
||||
return
|
||||
}
|
||||
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||
const userID = uid != null ? Number(uid) : undefined
|
||||
if (!userID || !Number.isFinite(userID)) {
|
||||
openOrderList.value = []
|
||||
openOrderTotal.value = 0
|
||||
return
|
||||
}
|
||||
openOrderLoading.value = true
|
||||
try {
|
||||
const res = await getOrderList(
|
||||
{
|
||||
page: page.value,
|
||||
pageSize: itemsPerPage.value,
|
||||
userID,
|
||||
status: OrderStatus.Live,
|
||||
},
|
||||
{ headers },
|
||||
)
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
const list = res.data?.list ?? []
|
||||
const openOnly = list.filter((o) => (o.status ?? 1) === OrderStatus.Live)
|
||||
openOrderList.value = openOnly.map(mapOrderToOpenOrderItem)
|
||||
openOrderTotal.value = openOnly.length
|
||||
} else {
|
||||
openOrderList.value = []
|
||||
openOrderTotal.value = 0
|
||||
}
|
||||
} catch {
|
||||
openOrderList.value = []
|
||||
openOrderTotal.value = 0
|
||||
} finally {
|
||||
openOrderLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const history = ref<HistoryItem[]>(
|
||||
USE_MOCK_WALLET ? [...MOCK_WALLET_HISTORY] : [],
|
||||
)
|
||||
/** 订单历史(API 数据,非 mock 时使用) */
|
||||
const historyList = ref<HistoryItem[]>([])
|
||||
const historyTotal = ref(0)
|
||||
const historyLoading = ref(false)
|
||||
|
||||
async function loadHistoryOrders() {
|
||||
if (USE_MOCK_WALLET) return
|
||||
const headers = userStore.getAuthHeaders()
|
||||
if (!headers) {
|
||||
historyList.value = []
|
||||
historyTotal.value = 0
|
||||
return
|
||||
}
|
||||
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||
const userID = uid != null ? Number(uid) : undefined
|
||||
if (!userID || !Number.isFinite(userID)) {
|
||||
historyList.value = []
|
||||
historyTotal.value = 0
|
||||
return
|
||||
}
|
||||
historyLoading.value = true
|
||||
try {
|
||||
const res = await getOrderList(
|
||||
{
|
||||
page: page.value,
|
||||
pageSize: itemsPerPage.value,
|
||||
userID,
|
||||
},
|
||||
{ headers },
|
||||
)
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
const list = res.data?.list ?? []
|
||||
historyList.value = list.map(mapOrderToHistoryItem)
|
||||
historyTotal.value = res.data?.total ?? 0
|
||||
} else {
|
||||
historyList.value = []
|
||||
historyTotal.value = 0
|
||||
}
|
||||
} catch {
|
||||
historyList.value = []
|
||||
historyTotal.value = 0
|
||||
} finally {
|
||||
historyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function matchSearch(text: string): boolean {
|
||||
const q = search.value.trim().toLowerCase()
|
||||
return !q || text.toLowerCase().includes(q)
|
||||
}
|
||||
const filteredPositions = computed(() => {
|
||||
const list = USE_MOCK_WALLET ? positions.value : positionList.value
|
||||
return list.filter((p) => matchSearch(p.market))
|
||||
})
|
||||
const filteredOpenOrders = computed(() => {
|
||||
const list = USE_MOCK_WALLET ? openOrders.value : openOrderList.value
|
||||
return list.filter((o) => matchSearch(o.market))
|
||||
})
|
||||
const filteredHistory = computed(() => {
|
||||
const list = USE_MOCK_WALLET ? history.value : historyList.value
|
||||
return list.filter((h) => matchSearch(h.market))
|
||||
})
|
||||
const filteredPositions = computed(() => positions.value.filter((p) => matchSearch(p.market)))
|
||||
const filteredOpenOrders = computed(() => openOrders.value.filter((o) => matchSearch(o.market)))
|
||||
const filteredHistory = computed(() => history.value.filter((h) => matchSearch(h.market)))
|
||||
|
||||
const page = ref(1)
|
||||
const itemsPerPage = ref(10)
|
||||
@ -908,38 +702,24 @@ function paginate<T>(list: T[]) {
|
||||
const start = (page.value - 1) * itemsPerPage.value
|
||||
return list.slice(start, start + itemsPerPage.value)
|
||||
}
|
||||
const paginatedPositions = computed(() => {
|
||||
if (USE_MOCK_WALLET) return paginate(filteredPositions.value)
|
||||
return filteredPositions.value
|
||||
})
|
||||
const paginatedOpenOrders = computed(() => {
|
||||
if (USE_MOCK_WALLET) return paginate(filteredOpenOrders.value)
|
||||
return filteredOpenOrders.value
|
||||
})
|
||||
const paginatedHistory = computed(() => {
|
||||
if (USE_MOCK_WALLET) return paginate(filteredHistory.value)
|
||||
return filteredHistory.value
|
||||
})
|
||||
const paginatedPositions = computed(() => paginate(filteredPositions.value))
|
||||
const paginatedOpenOrders = computed(() => paginate(filteredOpenOrders.value))
|
||||
const paginatedHistory = computed(() => paginate(filteredHistory.value))
|
||||
|
||||
const totalPagesPositions = computed(() => {
|
||||
const total = USE_MOCK_WALLET ? filteredPositions.value.length : positionTotal.value
|
||||
return Math.max(1, Math.ceil(total / itemsPerPage.value))
|
||||
})
|
||||
const totalPagesOrders = computed(() => {
|
||||
const total = USE_MOCK_WALLET ? filteredOpenOrders.value.length : openOrderTotal.value
|
||||
return Math.max(1, Math.ceil(total / itemsPerPage.value))
|
||||
})
|
||||
const totalPagesHistory = computed(() => {
|
||||
const total = USE_MOCK_WALLET ? filteredHistory.value.length : historyTotal.value
|
||||
return Math.max(1, Math.ceil(total / itemsPerPage.value))
|
||||
})
|
||||
const totalPagesPositions = computed(() =>
|
||||
Math.max(1, Math.ceil(filteredPositions.value.length / itemsPerPage.value)),
|
||||
)
|
||||
const totalPagesOrders = computed(() =>
|
||||
Math.max(1, Math.ceil(filteredOpenOrders.value.length / itemsPerPage.value)),
|
||||
)
|
||||
const totalPagesHistory = computed(() =>
|
||||
Math.max(1, Math.ceil(filteredHistory.value.length / itemsPerPage.value)),
|
||||
)
|
||||
|
||||
const currentListTotal = computed(() => {
|
||||
if (activeTab.value === 'positions')
|
||||
return USE_MOCK_WALLET ? filteredPositions.value.length : positionTotal.value
|
||||
if (activeTab.value === 'orders')
|
||||
return USE_MOCK_WALLET ? filteredOpenOrders.value.length : openOrderTotal.value
|
||||
return USE_MOCK_WALLET ? filteredHistory.value.length : historyTotal.value
|
||||
if (activeTab.value === 'positions') return filteredPositions.value.length
|
||||
if (activeTab.value === 'orders') return filteredOpenOrders.value.length
|
||||
return filteredHistory.value.length
|
||||
})
|
||||
const currentTotalPages = computed(() => {
|
||||
if (activeTab.value === 'positions') return totalPagesPositions.value
|
||||
@ -953,16 +733,8 @@ const currentPageEnd = computed(() =>
|
||||
Math.min(page.value * itemsPerPage.value, currentListTotal.value),
|
||||
)
|
||||
|
||||
watch(activeTab, (tab) => {
|
||||
watch(activeTab, () => {
|
||||
page.value = 1
|
||||
if (tab === 'positions' && !USE_MOCK_WALLET) loadPositionList()
|
||||
if (tab === 'orders' && !USE_MOCK_WALLET) loadOpenOrders()
|
||||
if (tab === 'history' && !USE_MOCK_WALLET) loadHistoryOrders()
|
||||
})
|
||||
watch([page, itemsPerPage], () => {
|
||||
if (activeTab.value === 'positions' && !USE_MOCK_WALLET) loadPositionList()
|
||||
if (activeTab.value === 'orders' && !USE_MOCK_WALLET) loadOpenOrders()
|
||||
if (activeTab.value === 'history' && !USE_MOCK_WALLET) loadHistoryOrders()
|
||||
})
|
||||
watch([currentListTotal, itemsPerPage], () => {
|
||||
const maxPage = currentTotalPages.value
|
||||
@ -982,34 +754,29 @@ async function cancelOrder(ord: OpenOrder) {
|
||||
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||
const userID = uid != null ? Number(uid) : 0
|
||||
if (!Number.isFinite(userID) || userID <= 0) {
|
||||
cancelOrderError.value = t('error.pleaseLogin')
|
||||
cancelOrderError.value = '请先登录'
|
||||
showCancelError.value = true
|
||||
return
|
||||
}
|
||||
const headers = userStore.getAuthHeaders()
|
||||
if (!headers) {
|
||||
cancelOrderError.value = t('error.pleaseLogin')
|
||||
cancelOrderError.value = '请先登录'
|
||||
showCancelError.value = true
|
||||
return
|
||||
}
|
||||
cancelOrderLoading.value = true
|
||||
cancelOrderError.value = ''
|
||||
try {
|
||||
const res = await apiCancelOrder({ orderID, tokenID, userID }, { headers })
|
||||
const res = await pmCancelOrder({ orderID, tokenID, userID }, { headers })
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
if (USE_MOCK_WALLET) {
|
||||
openOrders.value = openOrders.value.filter((o) => o.id !== ord.id)
|
||||
} else {
|
||||
openOrderList.value = openOrderList.value.filter((o) => o.id !== ord.id)
|
||||
openOrderTotal.value = openOrderList.value.length
|
||||
}
|
||||
openOrders.value = openOrders.value.filter((o) => o.id !== ord.id)
|
||||
userStore.fetchUsdcBalance()
|
||||
} else {
|
||||
cancelOrderError.value = res.msg || '取消失败'
|
||||
showCancelError.value = true
|
||||
}
|
||||
} catch (e) {
|
||||
cancelOrderError.value = formatAuthError(e, t('error.requestFailed'))
|
||||
cancelOrderError.value = e instanceof Error ? e.message : 'Request failed'
|
||||
showCancelError.value = true
|
||||
} finally {
|
||||
cancelOrderLoading.value = false
|
||||
@ -1017,12 +784,7 @@ async function cancelOrder(ord: OpenOrder) {
|
||||
}
|
||||
|
||||
function cancelAllOrders() {
|
||||
if (USE_MOCK_WALLET) {
|
||||
openOrders.value = []
|
||||
} else {
|
||||
openOrderList.value = []
|
||||
openOrderTotal.value = 0
|
||||
}
|
||||
openOrders.value = []
|
||||
}
|
||||
|
||||
const sellReceiveAmount = computed(() => {
|
||||
@ -1188,17 +950,7 @@ const handleResize = () => plChartInstance?.resize()
|
||||
|
||||
watch(plRange, () => updatePlChart())
|
||||
|
||||
// 监听语言切换,语言变化时重新加载数据
|
||||
watch(
|
||||
() => localeStore.currentLocale,
|
||||
() => {
|
||||
loadPositionList()
|
||||
loadOpenOrders()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (!USE_MOCK_WALLET && activeTab.value === 'positions') loadPositionList()
|
||||
nextTick(() => {
|
||||
initPlChart()
|
||||
})
|
||||
@ -1214,16 +966,6 @@ onUnmounted(() => {
|
||||
function onWithdrawSuccess() {
|
||||
withdrawDialogOpen.value = false
|
||||
}
|
||||
|
||||
function onAuthorizeClick() {
|
||||
authorizeDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function submitAuthorize() {
|
||||
// TODO: 对接 USDC 授权接口(approve CLOB 合约)
|
||||
// authorizeDialogOpen.value = false
|
||||
await CrossChainUSDTAuth.authorizeUSDT('eth', '0x024b7270Ee9c0Fc0de2E00a979d146255E0e9C00', '100')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user