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`
|
1. **收到对接请求** → 立即用 `mcp_web_fetch` 或 `curl` 获取 `https://api.xtrader.vip/swagger/doc.json`
|
||||||
2. **检查接口是否存在** → 若 `paths["<path>"]` 或 `paths["<path>"]["<method>"]` **不存在**,则:
|
2. **第一步** → 解析 `paths["<path>"]["<method>"]` 与 `definitions`,**在对话中输出**:
|
||||||
- ❌ **禁止**自行猜测或虚构数据结构并自动对接
|
|
||||||
- ✅ **必须**在对话中明确告知用户「该接口在 doc.json 中不存在」
|
|
||||||
- ✅ **必须**向用户询问:
|
|
||||||
- 是否仍要对接?若对接,请提供**请求参数**与**响应数据结构**(或示例 JSON)
|
|
||||||
- 用户可选择**不对接**,则流程终止
|
|
||||||
- 仅在用户明确提供数据结构或确认对接后,才可进入第二步
|
|
||||||
3. **第一步**(接口存在时)→ 解析 `paths["<path>"]["<method>"]` 与 `definitions`,**在对话中输出**:
|
|
||||||
- 请求参数表(Query/Body/鉴权)
|
- 请求参数表(Query/Body/鉴权)
|
||||||
- 响应参数表(200 schema)
|
- 响应参数表(200 schema)
|
||||||
- data 的 `$ref` 对应 definitions 的**完整字段表**
|
- data 的 `$ref` 对应 definitions 的**完整字段表**
|
||||||
- 输出后**明确标注「第一步完成」**
|
- 输出后**明确标注「第一步完成」**
|
||||||
4. **第二步** → 在 `src/api/` 中根据第一步表格(或用户提供的数据结构)定义 TypeScript 类型,**不得在第一步完成前执行**
|
3. **第二步** → 在 `src/api/` 中根据第一步表格定义 TypeScript 类型,**不得在第一步完成前执行**
|
||||||
5. **第三步** → 实现请求函数并集成到页面,**不得在第二步完成前执行**
|
4. **第三步** → 实现请求函数并集成到页面,**不得在第二步完成前执行**
|
||||||
|
|
||||||
### 禁止行为
|
### 禁止行为
|
||||||
|
|
||||||
- ❌ 在对话中输出第一步结果**之前**写任何 `src/api/` 或 `src/views/` 业务代码
|
- ❌ 在对话中输出第一步结果**之前**写任何 `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,最后集成到页面
|
- [ ] **按规范顺序**:已先列出请求参数与响应参数,再建 Model,最后集成到页面
|
||||||
- [ ] 规范 URL 使用 `https://api.xtrader.vip/swagger/doc.json`,或本地缓存与之一致
|
- [ ] 规范 URL 使用 `https://api.xtrader.vip/swagger/doc.json`,或本地缓存与之一致
|
||||||
- [ ] 请求 path、method、query/body 与 `paths` 一致
|
- [ ] 请求 path、method、query/body 与 `paths` 一致
|
||||||
|
|||||||
@ -17,24 +17,7 @@
|
|||||||
| 类型 | 说明 |
|
| 类型 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `PmTagMainItem` | 接口返回的 PmTag 结构,含 children |
|
| `PmTagMainItem` | 接口返回的 PmTag 结构,含 children |
|
||||||
| `CategoryTreeNode` | 前端使用的树节点,含 id、label、slug、icon、sectionTitle、children、tagIds |
|
| `CategoryTreeNode` | 前端使用的树节点,含 id、label、slug、icon、sectionTitle、children |
|
||||||
|
|
||||||
### CategoryTreeNode.tagIds
|
|
||||||
|
|
||||||
从后端 `PmTagCatalogItem.tagId` 字段提取的标签 ID 列表,用于事件筛选:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface CategoryTreeNode {
|
|
||||||
// ... 其他字段
|
|
||||||
/** 关联的标签 ID 列表,用于事件筛选 */
|
|
||||||
tagIds?: number[]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**提取逻辑**:
|
|
||||||
- 如果 `tagId` 是数字数组,直接使用(`[1351, 1368]`)
|
|
||||||
- 如果 `tagId` 是单个数字,包装为数组(`1351` → `[1351]`)
|
|
||||||
- 如果 `tagId` 是对象或其他类型,忽略
|
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,7 @@ import {
|
|||||||
} from '@/api/event'
|
} 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)
|
const cards = res.data.list.map(mapEventItemToCard)
|
||||||
|
|
||||||
// 获取详情(需鉴权)
|
// 获取详情(需鉴权)
|
||||||
@ -49,18 +49,3 @@ clearEventListCache()
|
|||||||
1. **新增筛选参数**:在 `GetPmEventListParams` 中增加字段,并在 `getPmEventPublic` 的 query 中传入
|
1. **新增筛选参数**:在 `GetPmEventListParams` 中增加字段,并在 `getPmEventPublic` 的 query 中传入
|
||||||
2. **缓存策略**:可改为 sessionStorage 或带 TTL 的缓存
|
2. **缓存策略**:可改为 sessionStorage 或带 TTL 的缓存
|
||||||
3. **多选项展示**:`mapEventItemToCard` 已支持 multi 类型,可扩展 `EventCardOutcome` 字段
|
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 等 |
|
| market | object | 市场信息,含 marketId、clobTokenIds、outcomes、outcomePrices 等 |
|
||||||
| initialOption | 'yes' \| 'no' | 初始选中的选项 |
|
| initialOption | 'yes' \| 'no' | 初始选中的选项 |
|
||||||
| embeddedInSheet | boolean | 是否嵌入底部弹窗(移动端) |
|
| 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 切换
|
- Buy/Sell Tab 切换
|
||||||
- Market/Limit 类型、Merge/Split 菜单
|
- Market/Limit 类型、Merge/Split 菜单
|
||||||
- **Buy 模式 Amount 区**:无论余额是否充足,均显示 Amount 标签、Balance、**可编辑金额输入框**(v-text-field,带 $ 前缀,variant="outlined")、+$1/+$20/+$100/Max 快捷按钮(桌面端、嵌入弹窗、移动端弹窗一致)
|
- **Buy 模式 Amount 区**:无论余额是否充足,均显示 Amount 标签、Balance、金额输入、+$1/+$20/+$100/Max 快捷按钮(桌面端、嵌入弹窗、移动端弹窗一致)
|
||||||
- 输入框支持直接输入金额(>= 0,支持小数)
|
|
||||||
- 事件处理:`onAmountInput`、`onAmountKeydown`、`onAmountPaste`
|
|
||||||
- 余额不足时 Buy 显示 Deposit 按钮
|
- 余额不足时 Buy 显示 Deposit 按钮
|
||||||
- 25%/50%/Max 快捷份额
|
- 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
|
- 调用 market API 下单、Split、Merge
|
||||||
- **合并/拆分成功后触发事件**:`mergeSuccess`、`splitSuccess`,父组件监听后可刷新持仓列表
|
|
||||||
- **401 权限错误提示**:通过 `useAuthError().formatAuthError` 统一处理,未登录显示「请先登录」,已登录显示「权限不足」
|
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
@ -52,30 +29,10 @@ interface TradePositionItem {
|
|||||||
<TradeComponent
|
<TradeComponent
|
||||||
:market="tradeMarketPayload"
|
:market="tradeMarketPayload"
|
||||||
:initial-option="tradeInitialOption"
|
:initial-option="tradeInitialOption"
|
||||||
:positions="marketPositions"
|
|
||||||
embedded-in-sheet
|
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.*` 键国际化:
|
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
|
# Home.vue
|
||||||
|
|
||||||
**路径**:`src/views/Home.vue`
|
**路径**:`src/views/Home.vue`
|
||||||
|
**路由**:`/`,name: `home`
|
||||||
|
|
||||||
## 功能用途
|
## 功能用途
|
||||||
|
|
||||||
首页,展示分类导航栏(三层级)、事件卡片列表。支持分类筛选、搜索、下拉刷新、触底加载更多。
|
首页,展示分类 Tab、搜索、事件卡片列表。支持三层分类、下拉刷新、无限滚动、搜索历史,卡片支持单一/多选项展示。
|
||||||
|
|
||||||
## 核心能力
|
## 核心能力
|
||||||
|
|
||||||
- **分类导航**:三层级分类选择(一级 Tab、二级图标、三级 Tab)
|
- 分类:第一层 Tab、第二层图标、第三层 Tab,从 `getPmTagMain` 获取
|
||||||
- **事件列表**:卡片式展示,支持下拉刷新、触底加载
|
- 搜索:展开浮层、历史记录、`useSearchHistory`
|
||||||
- **搜索**:可按关键词搜索事件
|
- 列表:`getPmEventPublic` 分页、`mapEventItemToCard` 映射、`MarketCard` 渲染
|
||||||
- **分类筛选**:选中分类后,自动提取所有层级节点的 `tagIds` 进行事件筛选
|
- 缓存:`eventListCache` 切换页面时复用,下拉刷新时清空
|
||||||
|
- Keep-alive:`Home` 被 include,切换回来时保留状态
|
||||||
## 数据流
|
|
||||||
|
|
||||||
```
|
|
||||||
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]
|
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
无需手动调用,路由 `/` 自动加载。
|
- 访问 `/` 即可进入
|
||||||
|
- 分类切换、搜索、下拉刷新、滚动加载均自动工作
|
||||||
|
|
||||||
## 扩展方式
|
## 扩展方式
|
||||||
|
|
||||||
1. **新增分类层级**:修改 `MAX_LAYER` 常量,调整模板渲染逻辑
|
1. **新增筛选**:在搜索浮层旁增加筛选按钮,修改 `getPmEventPublic` 的 params
|
||||||
2. **自定义筛选逻辑**:修改 `activeTagIds` 计算属性
|
2. **骨架屏**:在 loading 时展示 `v-skeleton-loader`
|
||||||
3. **列表缓存策略**:调整 `getEventListCache` / `setEventListCache`
|
3. **空状态**:列表为空时展示空状态插画与文案
|
||||||
|
|||||||
@ -11,26 +11,15 @@
|
|||||||
|
|
||||||
- 分时图:ECharts 渲染,支持 Past、时间粒度切换
|
- 分时图:ECharts 渲染,支持 Past、时间粒度切换
|
||||||
- 订单簿:`OrderBook` 组件,通过 **ClobSdk** 对接 CLOB WebSocket 实时数据(全量快照、增量更新、成交推送)
|
- 订单簿:`OrderBook` 组件,通过 **ClobSdk** 对接 CLOB WebSocket 实时数据(全量快照、增量更新、成交推送)
|
||||||
- 交易:`TradeComponent`,传入 `market`、`initialOption`、`positions`(持仓数据)
|
- 交易:`TradeComponent`,传入 `market`、`initialOption`
|
||||||
- 持仓列表:通过 `getPositionList` 获取当前市场持仓,传递给 `TradeComponent` 用于计算可合并份额
|
|
||||||
- 限价订单:通过 `getOrderList` 获取当前市场未成交限价单,支持撤单
|
|
||||||
- 移动端:底部栏 + `v-bottom-sheet` 嵌入 `TradeComponent`
|
- 移动端:底部栏 + `v-bottom-sheet` 嵌入 `TradeComponent`
|
||||||
- Merge/Split:通过 `TradeComponent` 或底部菜单触发,成功后监听 `mergeSuccess`/`splitSuccess` 事件刷新持仓
|
- Merge/Split:通过 `TradeComponent` 或底部菜单触发
|
||||||
- **401 权限错误**:加载详情失败时,通过 `useAuthError().formatAuthError` 统一提示「请先登录」或「权限不足」
|
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
- 从首页卡片点击进入,或直接访问 `/trade-detail/123`
|
- 从首页卡片点击进入,或直接访问 `/trade-detail/123`
|
||||||
- 路由参数 `id` 为 Event ID,用于 `findPmEvent`
|
- 路由参数 `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` 消息
|
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 图表
|
- Profit/Loss 卡片:时间范围切换、ECharts 图表
|
||||||
- Tab:Positions、Open orders、History
|
- Tab:Positions、Open orders、History
|
||||||
- DepositDialog、WithdrawDialog 组件
|
- 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}`);
|
console.log(`[ClobSdk] 已连接到 ${this.url}`);
|
||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0;
|
||||||
this.notifyConnect(event);
|
this.notifyConnect(event);
|
||||||
setTimeout(() => {
|
this.subscribe();
|
||||||
this.subscribe();
|
|
||||||
}, 1000);
|
|
||||||
// this.subscribe();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.ws.onmessage = (event: any) => {
|
this.ws.onmessage = (event: any) => {
|
||||||
|
|||||||
@ -50,8 +50,6 @@ export interface CategoryTreeNode {
|
|||||||
updatedAt?: string
|
updatedAt?: string
|
||||||
/** 排序值,有则按从小到大排序 */
|
/** 排序值,有则按从小到大排序 */
|
||||||
sort?: number
|
sort?: number
|
||||||
/** 关联的标签 ID 列表,用于事件筛选 */
|
|
||||||
tagIds?: number[]
|
|
||||||
children?: CategoryTreeNode[]
|
children?: CategoryTreeNode[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,14 +204,6 @@ function mapCatalogToTreeNode(item: PmTagCatalogItem): CategoryTreeNode {
|
|||||||
? item.children.map(mapCatalogToTreeNode)
|
? item.children.map(mapCatalogToTreeNode)
|
||||||
: undefined
|
: undefined
|
||||||
const icon = item.icon ?? resolveCategoryIcon({ label, slug })
|
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 {
|
return {
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
@ -221,7 +211,6 @@ function mapCatalogToTreeNode(item: PmTagCatalogItem): CategoryTreeNode {
|
|||||||
icon,
|
icon,
|
||||||
sectionTitle: item.sectionTitle,
|
sectionTitle: item.sectionTitle,
|
||||||
sort: item.sort,
|
sort: item.sort,
|
||||||
tagIds,
|
|
||||||
children: children?.length ? children : undefined,
|
children: children?.length ? children : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -113,39 +113,42 @@ export interface PmEventListResponse {
|
|||||||
msg: string
|
msg: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /PmEvent/getPmEventPublic 请求参数(与 doc.json 对齐)
|
|
||||||
*/
|
|
||||||
export interface GetPmEventListParams {
|
export interface GetPmEventListParams {
|
||||||
page?: number
|
page?: number
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
keyword?: string
|
keyword?: string
|
||||||
/** 创建时间范围,如 ['2025-01-01', '2025-12-31'] */
|
/** 创建时间范围,如 ['2025-01-01', '2025-12-31'] */
|
||||||
createdAtRange?: string[]
|
createdAtRange?: string[]
|
||||||
/** 标签 ID 列表,按分类筛选,传统数组方式传递 */
|
/** clobTokenIds 对应的值,用于按市场 token 筛选;可从 market.clobTokenIds 获取 */
|
||||||
tagIds?: number[]
|
tokenid?: string | string[]
|
||||||
|
/** 标签 ID,按分类筛选 */
|
||||||
|
tagId?: number
|
||||||
|
/** 标签 slug,按分类筛选 */
|
||||||
|
tagSlug?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页获取 Event 列表(公开接口,不需要鉴权)
|
* 分页获取 Event 列表(公开接口,不需要鉴权)
|
||||||
* GET /PmEvent/getPmEventPublic
|
* GET /PmEvent/getPmEventPublic
|
||||||
*
|
*
|
||||||
* Query: page, pageSize, keyword, createdAtRange, tagIds
|
* Query: page, pageSize, keyword, createdAtRange, tokenid, tagId, tagSlug
|
||||||
* doc.json: paths["/PmEvent/getPmEventPublic"].get.parameters
|
|
||||||
*/
|
*/
|
||||||
export async function getPmEventPublic(
|
export async function getPmEventPublic(
|
||||||
params: GetPmEventListParams = {},
|
params: GetPmEventListParams = {},
|
||||||
): Promise<PmEventListResponse> {
|
): Promise<PmEventListResponse> {
|
||||||
const { page = 1, pageSize = 10, keyword, createdAtRange, tagIds } = params
|
const { page = 1, pageSize = 10, keyword, createdAtRange, tokenid, tagId, tagSlug } = params
|
||||||
const query: Record<string, string | number | number[] | string[] | undefined> = {
|
const query: Record<string, string | number | string[] | undefined> = {
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
}
|
}
|
||||||
if (keyword != null && keyword !== '') query.keyword = keyword
|
if (keyword != null && keyword !== '') query.keyword = keyword
|
||||||
if (createdAtRange != null && createdAtRange.length) query.createdAtRange = createdAtRange
|
if (createdAtRange != null && createdAtRange.length) query.createdAtRange = createdAtRange
|
||||||
if (tagIds != null && tagIds.length > 0) {
|
if (tokenid != null) {
|
||||||
query.tagIds = tagIds
|
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)
|
return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,13 +203,13 @@ export interface EventCardOutcome {
|
|||||||
title: string
|
title: string
|
||||||
/** 第一选项概率(来自 outcomePrices[0]) */
|
/** 第一选项概率(来自 outcomePrices[0]) */
|
||||||
chanceValue: number
|
chanceValue: number
|
||||||
/** Yes 价格 0–1,来自 outcomePrices[0],供交易组件使用 */
|
/** 第一选项按钮文案(来自 outcomes[0],如 Yes / Up) */
|
||||||
yesPrice?: number
|
|
||||||
/** No 价格 0–1,来自 outcomePrices[1],供交易组件使用 */
|
|
||||||
noPrice?: number
|
|
||||||
yesLabel?: string
|
yesLabel?: string
|
||||||
|
/** 第二选项按钮文案(来自 outcomes[1],如 No / Down) */
|
||||||
noLabel?: string
|
noLabel?: string
|
||||||
|
/** 可选,用于交易时区分 market */
|
||||||
marketId?: string
|
marketId?: string
|
||||||
|
/** 用于下单 tokenId,与 outcomes 顺序一致 */
|
||||||
clobTokenIds?: string[]
|
clobTokenIds?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,10 +240,6 @@ export interface EventCardItem {
|
|||||||
marketId?: string
|
marketId?: string
|
||||||
/** 用于下单 tokenId,单 market 时取自 firstMarket.clobTokenIds */
|
/** 用于下单 tokenId,单 market 时取自 firstMarket.clobTokenIds */
|
||||||
clobTokenIds?: string[]
|
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 ?? ''
|
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
|
const outcomes: EventCardOutcome[] | undefined = multi
|
||||||
? markets.map((m) => {
|
? markets.map((m) => ({
|
||||||
const { yesPrice, noPrice } = parseOutcomePrices(m)
|
title: m.question ?? '',
|
||||||
return {
|
chanceValue: marketChance(m),
|
||||||
title: m.question ?? '',
|
yesLabel: m.outcomes?.[0] ?? 'Yes',
|
||||||
chanceValue: marketChance(m),
|
noLabel: m.outcomes?.[1] ?? 'No',
|
||||||
yesPrice,
|
marketId: getMarketId(m),
|
||||||
noPrice,
|
clobTokenIds: m.clobTokenIds,
|
||||||
yesLabel: m.outcomes?.[0] ?? 'Yes',
|
}))
|
||||||
noLabel: m.outcomes?.[1] ?? 'No',
|
|
||||||
marketId: getMarketId(m),
|
|
||||||
clobTokenIds: m.clobTokenIds,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const firstPrices = firstMarket ? parseOutcomePrices(firstMarket) : { yesPrice: 0.5, noPrice: 0.5 }
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
slug: item.slug ?? undefined,
|
slug: item.slug ?? undefined,
|
||||||
@ -371,7 +349,5 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
|||||||
isNew: item.new === true,
|
isNew: item.new === true,
|
||||||
marketId: getMarketId(firstMarket),
|
marketId: getMarketId(firstMarket),
|
||||||
clobTokenIds: firstMarket?.clobTokenIds,
|
clobTokenIds: firstMarket?.clobTokenIds,
|
||||||
yesPrice: firstPrices.yesPrice,
|
|
||||||
noPrice: firstPrices.noPrice,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,8 +24,6 @@ export interface ClobSubmitOrderRequest {
|
|||||||
taker: boolean
|
taker: boolean
|
||||||
tokenID: string
|
tokenID: string
|
||||||
userID: number
|
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>(
|
export async function get<T = unknown>(
|
||||||
path: string,
|
path: string,
|
||||||
params?: Record<string, string | number | string[] | number[] | undefined>,
|
params?: Record<string, string | number | string[] | undefined>,
|
||||||
config?: RequestConfig,
|
config?: RequestConfig,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const url = new URL(path, BASE_URL || window.location.origin)
|
const url = new URL(path, BASE_URL || window.location.origin)
|
||||||
|
|||||||
@ -3,49 +3,11 @@ import { get } from './request'
|
|||||||
const USDC_DECIMALS = 1_000_000
|
const USDC_DECIMALS = 1_000_000
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getUserInfo 返回的 data 结构(2026 更新)
|
* getUserInfo 返回的 data(definitions system.SysUser)
|
||||||
* data 包含 balance、orders、positions、userInfo
|
* 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 {
|
export interface UserInfoData {
|
||||||
ID?: number
|
ID?: number
|
||||||
CreatedAt?: string
|
|
||||||
UpdatedAt?: string
|
|
||||||
userName?: string
|
userName?: string
|
||||||
nickName?: string
|
nickName?: string
|
||||||
headerImg?: string
|
headerImg?: string
|
||||||
@ -53,6 +15,8 @@ export interface UserInfoData {
|
|||||||
authorityId?: number
|
authorityId?: number
|
||||||
authority?: unknown
|
authority?: unknown
|
||||||
authorities?: unknown[]
|
authorities?: unknown[]
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
email?: string
|
email?: string
|
||||||
phone?: string
|
phone?: string
|
||||||
enable?: number
|
enable?: number
|
||||||
@ -62,20 +26,12 @@ export interface UserInfoData {
|
|||||||
promotionCode?: string
|
promotionCode?: string
|
||||||
puserId?: number
|
puserId?: number
|
||||||
remark?: string
|
remark?: string
|
||||||
originSetting?: Record<string, unknown> | null
|
originSetting?: Record<string, unknown>
|
||||||
}
|
|
||||||
|
|
||||||
/** getUserInfo 完整 data */
|
|
||||||
export interface GetUserInfoData {
|
|
||||||
balance?: UserInfoBalance
|
|
||||||
orders?: UserInfoOrder[]
|
|
||||||
positions?: unknown[]
|
|
||||||
userInfo?: UserInfoData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetUserInfoResponse {
|
export interface GetUserInfoResponse {
|
||||||
code: number
|
code: number
|
||||||
data?: GetUserInfoData
|
data?: UserInfoData
|
||||||
msg?: string
|
msg?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -181,8 +181,6 @@ const emit = defineEmits<{
|
|||||||
clobTokenIds?: string[]
|
clobTokenIds?: string[]
|
||||||
yesLabel?: string
|
yesLabel?: string
|
||||||
noLabel?: string
|
noLabel?: string
|
||||||
yesPrice?: number
|
|
||||||
noPrice?: number
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}>()
|
}>()
|
||||||
@ -212,10 +210,6 @@ const props = withDefaults(
|
|||||||
marketId?: string
|
marketId?: string
|
||||||
/** 用于下单 tokenId,单 market 时 */
|
/** 用于下单 tokenId,单 market 时 */
|
||||||
clobTokenIds?: string[]
|
clobTokenIds?: string[]
|
||||||
/** Yes 价格 0–1,供交易组件使用 */
|
|
||||||
yesPrice?: number
|
|
||||||
/** No 价格 0–1,供交易组件使用 */
|
|
||||||
noPrice?: number
|
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
marketTitle: 'Mamdan opens city-owned grocery store b...',
|
marketTitle: 'Mamdan opens city-owned grocery store b...',
|
||||||
@ -231,8 +225,6 @@ const props = withDefaults(
|
|||||||
noLabel: 'No',
|
noLabel: 'No',
|
||||||
isNew: false,
|
isNew: false,
|
||||||
marketId: undefined,
|
marketId: undefined,
|
||||||
yesPrice: undefined,
|
|
||||||
noPrice: undefined,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -314,8 +306,6 @@ function openTradeSingle(side: 'yes' | 'no') {
|
|||||||
clobTokenIds: props.clobTokenIds,
|
clobTokenIds: props.clobTokenIds,
|
||||||
yesLabel: props.yesLabel,
|
yesLabel: props.yesLabel,
|
||||||
noLabel: props.noLabel,
|
noLabel: props.noLabel,
|
||||||
yesPrice: props.yesPrice,
|
|
||||||
noPrice: props.noPrice,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,8 +318,6 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
|
|||||||
clobTokenIds: outcome.clobTokenIds,
|
clobTokenIds: outcome.clobTokenIds,
|
||||||
yesLabel: outcome.yesLabel,
|
yesLabel: outcome.yesLabel,
|
||||||
noLabel: outcome.noLabel,
|
noLabel: outcome.noLabel,
|
||||||
yesPrice: outcome.yesPrice,
|
|
||||||
noPrice: outcome.noPrice,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</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"
|
"chance": "chance"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"orderSuccess": "Order placed successfully",
|
"orderSuccess": "Order placed successfully"
|
||||||
"splitSuccess": "Split successful",
|
|
||||||
"mergeSuccess": "Merge successful"
|
|
||||||
},
|
},
|
||||||
"trade": {
|
"trade": {
|
||||||
"buy": "Buy",
|
"buy": "Buy",
|
||||||
@ -58,7 +56,6 @@
|
|||||||
"avgPrice": "Avg. Price",
|
"avgPrice": "Avg. Price",
|
||||||
"max": "Max",
|
"max": "Max",
|
||||||
"balanceLabel": "Balance",
|
"balanceLabel": "Balance",
|
||||||
"maxShares": "Max shares",
|
|
||||||
"pleaseLogin": "Please log in first",
|
"pleaseLogin": "Please log in first",
|
||||||
"pleaseSelectMarket": "Please select a market (with clobTokenIds)",
|
"pleaseSelectMarket": "Please select a market (with clobTokenIds)",
|
||||||
"userError": "User info error",
|
"userError": "User info error",
|
||||||
@ -85,24 +82,12 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"requestFailed": "Request failed",
|
"requestFailed": "Request failed",
|
||||||
"loadFailed": "Load failed",
|
"loadFailed": "Load failed",
|
||||||
"invalidId": "Invalid ID or slug",
|
"invalidId": "Invalid ID or slug"
|
||||||
"pleaseLogin": "Please log in first",
|
|
||||||
"insufficientPermission": "Insufficient permission"
|
|
||||||
},
|
},
|
||||||
"activity": {
|
"activity": {
|
||||||
"comments": "Comments",
|
"comments": "Comments",
|
||||||
"topHolders": "Top Holders",
|
"topHolders": "Top Holders",
|
||||||
"activity": "Activity",
|
"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.",
|
"noCommentsYet": "No comments yet.",
|
||||||
"topHoldersPlaceholder": "Top holders will appear here.",
|
"topHoldersPlaceholder": "Top holders will appear here.",
|
||||||
"minAmount": "Min amount",
|
"minAmount": "Min amount",
|
||||||
@ -123,8 +108,6 @@
|
|||||||
"today": "Today",
|
"today": "Today",
|
||||||
"deposit": "Deposit",
|
"deposit": "Deposit",
|
||||||
"withdraw": "Withdraw",
|
"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",
|
"profitLoss": "Profit/Loss",
|
||||||
"allTime": "All-Time",
|
"allTime": "All-Time",
|
||||||
"pl1D": "1D",
|
"pl1D": "1D",
|
||||||
|
|||||||
@ -15,9 +15,7 @@
|
|||||||
"chance": "確率"
|
"chance": "確率"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"orderSuccess": "注文が完了しました",
|
"orderSuccess": "注文が完了しました"
|
||||||
"splitSuccess": "スプリット成功",
|
|
||||||
"mergeSuccess": "マージ成功"
|
|
||||||
},
|
},
|
||||||
"trade": {
|
"trade": {
|
||||||
"buy": "買う",
|
"buy": "買う",
|
||||||
@ -58,7 +56,6 @@
|
|||||||
"avgPrice": "平均価格",
|
"avgPrice": "平均価格",
|
||||||
"max": "最大",
|
"max": "最大",
|
||||||
"balanceLabel": "残高",
|
"balanceLabel": "残高",
|
||||||
"maxShares": "最大シェア",
|
|
||||||
"pleaseLogin": "先にログインしてください",
|
"pleaseLogin": "先にログインしてください",
|
||||||
"pleaseSelectMarket": "市場を選択してください(clobTokenIds が必要)",
|
"pleaseSelectMarket": "市場を選択してください(clobTokenIds が必要)",
|
||||||
"userError": "ユーザー情報エラー",
|
"userError": "ユーザー情報エラー",
|
||||||
@ -85,24 +82,12 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"requestFailed": "リクエストに失敗しました",
|
"requestFailed": "リクエストに失敗しました",
|
||||||
"loadFailed": "読み込みに失敗しました",
|
"loadFailed": "読み込みに失敗しました",
|
||||||
"invalidId": "無効な ID または slug",
|
"invalidId": "無効な ID または slug"
|
||||||
"pleaseLogin": "先にログインしてください",
|
|
||||||
"insufficientPermission": "権限がありません"
|
|
||||||
},
|
},
|
||||||
"activity": {
|
"activity": {
|
||||||
"comments": "コメント",
|
"comments": "コメント",
|
||||||
"topHolders": "持倉トップ",
|
"topHolders": "持倉トップ",
|
||||||
"activity": "アクティビティ",
|
"activity": "アクティビティ",
|
||||||
"rules": "ルール",
|
|
||||||
"rulesDescription": "説明",
|
|
||||||
"rulesSource": "決裁ソース",
|
|
||||||
"rulesEmpty": "ルールはありません",
|
|
||||||
"mine": "マイ",
|
|
||||||
"myPositions": "ポジション",
|
|
||||||
"openOrders": "注文",
|
|
||||||
"noPositionsInMarket": "この市場にポジションはありません",
|
|
||||||
"noOpenOrdersInMarket": "この市場に未約定注文はありません",
|
|
||||||
"cancelOrder": "キャンセル",
|
|
||||||
"noCommentsYet": "コメントはまだありません",
|
"noCommentsYet": "コメントはまだありません",
|
||||||
"topHoldersPlaceholder": "持倉トップがここに表示されます",
|
"topHoldersPlaceholder": "持倉トップがここに表示されます",
|
||||||
"minAmount": "最小金額",
|
"minAmount": "最小金額",
|
||||||
@ -123,8 +108,6 @@
|
|||||||
"today": "今日",
|
"today": "今日",
|
||||||
"deposit": "入金",
|
"deposit": "入金",
|
||||||
"withdraw": "出金",
|
"withdraw": "出金",
|
||||||
"authorize": "承認",
|
|
||||||
"authorizeDesc": "取引のため USDC の使用を承認します。注文時に取引所が残高を使用できるようになります。",
|
|
||||||
"profitLoss": "損益",
|
"profitLoss": "損益",
|
||||||
"allTime": "全期間",
|
"allTime": "全期間",
|
||||||
"pl1D": "1日",
|
"pl1D": "1日",
|
||||||
|
|||||||
@ -15,9 +15,7 @@
|
|||||||
"chance": "확률"
|
"chance": "확률"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"orderSuccess": "주문이 완료되었습니다",
|
"orderSuccess": "주문이 완료되었습니다"
|
||||||
"splitSuccess": "분할 완료",
|
|
||||||
"mergeSuccess": "병합 완료"
|
|
||||||
},
|
},
|
||||||
"trade": {
|
"trade": {
|
||||||
"buy": "매수",
|
"buy": "매수",
|
||||||
@ -58,7 +56,6 @@
|
|||||||
"avgPrice": "평균 가격",
|
"avgPrice": "평균 가격",
|
||||||
"max": "최대",
|
"max": "최대",
|
||||||
"balanceLabel": "잔액",
|
"balanceLabel": "잔액",
|
||||||
"maxShares": "최대 주식",
|
|
||||||
"pleaseLogin": "먼저 로그인하세요",
|
"pleaseLogin": "먼저 로그인하세요",
|
||||||
"pleaseSelectMarket": "시장을 선택하세요 (clobTokenIds 필요)",
|
"pleaseSelectMarket": "시장을 선택하세요 (clobTokenIds 필요)",
|
||||||
"userError": "사용자 정보 오류",
|
"userError": "사용자 정보 오류",
|
||||||
@ -85,24 +82,12 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"requestFailed": "요청 실패",
|
"requestFailed": "요청 실패",
|
||||||
"loadFailed": "로드 실패",
|
"loadFailed": "로드 실패",
|
||||||
"invalidId": "잘못된 ID 또는 slug",
|
"invalidId": "잘못된 ID 또는 slug"
|
||||||
"pleaseLogin": "먼저 로그인하세요",
|
|
||||||
"insufficientPermission": "권한이 없습니다"
|
|
||||||
},
|
},
|
||||||
"activity": {
|
"activity": {
|
||||||
"comments": "댓글",
|
"comments": "댓글",
|
||||||
"topHolders": "보유자 순위",
|
"topHolders": "보유자 순위",
|
||||||
"activity": "활동",
|
"activity": "활동",
|
||||||
"rules": "규칙",
|
|
||||||
"rulesDescription": "설명",
|
|
||||||
"rulesSource": "결정 출처",
|
|
||||||
"rulesEmpty": "규칙이 없습니다",
|
|
||||||
"mine": "내 것",
|
|
||||||
"myPositions": "포지션",
|
|
||||||
"openOrders": "주문",
|
|
||||||
"noPositionsInMarket": "이 시장에 포지션이 없습니다",
|
|
||||||
"noOpenOrdersInMarket": "이 시장에 미체결 주문이 없습니다",
|
|
||||||
"cancelOrder": "취소",
|
|
||||||
"noCommentsYet": "아직 댓글이 없습니다",
|
"noCommentsYet": "아직 댓글이 없습니다",
|
||||||
"topHoldersPlaceholder": "보유자 순위가 여기에 표시됩니다",
|
"topHoldersPlaceholder": "보유자 순위가 여기에 표시됩니다",
|
||||||
"minAmount": "최소 금액",
|
"minAmount": "최소 금액",
|
||||||
@ -123,8 +108,6 @@
|
|||||||
"today": "오늘",
|
"today": "오늘",
|
||||||
"deposit": "입금",
|
"deposit": "입금",
|
||||||
"withdraw": "출금",
|
"withdraw": "출금",
|
||||||
"authorize": "승인",
|
|
||||||
"authorizeDesc": "거래를 위해 USDC 사용을 승인합니다. 주문 시 거래소가 잔액을 사용할 수 있습니다.",
|
|
||||||
"profitLoss": "손익",
|
"profitLoss": "손익",
|
||||||
"allTime": "전체",
|
"allTime": "전체",
|
||||||
"pl1D": "1일",
|
"pl1D": "1일",
|
||||||
|
|||||||
@ -15,9 +15,7 @@
|
|||||||
"chance": "概率"
|
"chance": "概率"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"orderSuccess": "下单成功",
|
"orderSuccess": "下单成功"
|
||||||
"splitSuccess": "拆分成功",
|
|
||||||
"mergeSuccess": "合并成功"
|
|
||||||
},
|
},
|
||||||
"trade": {
|
"trade": {
|
||||||
"buy": "买入",
|
"buy": "买入",
|
||||||
@ -58,7 +56,6 @@
|
|||||||
"avgPrice": "平均价",
|
"avgPrice": "平均价",
|
||||||
"max": "最大",
|
"max": "最大",
|
||||||
"balanceLabel": "余额",
|
"balanceLabel": "余额",
|
||||||
"maxShares": "最大份额",
|
|
||||||
"pleaseLogin": "请先登录",
|
"pleaseLogin": "请先登录",
|
||||||
"pleaseSelectMarket": "请先选择市场(需包含 clobTokenIds)",
|
"pleaseSelectMarket": "请先选择市场(需包含 clobTokenIds)",
|
||||||
"userError": "用户信息异常",
|
"userError": "用户信息异常",
|
||||||
@ -85,24 +82,12 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"requestFailed": "请求失败",
|
"requestFailed": "请求失败",
|
||||||
"loadFailed": "加载失败",
|
"loadFailed": "加载失败",
|
||||||
"invalidId": "无效的 ID 或 slug",
|
"invalidId": "无效的 ID 或 slug"
|
||||||
"pleaseLogin": "请先登录",
|
|
||||||
"insufficientPermission": "权限不足"
|
|
||||||
},
|
},
|
||||||
"activity": {
|
"activity": {
|
||||||
"comments": "评论",
|
"comments": "评论",
|
||||||
"topHolders": "持仓大户",
|
"topHolders": "持仓大户",
|
||||||
"activity": "动态",
|
"activity": "动态",
|
||||||
"rules": "规则",
|
|
||||||
"rulesDescription": "描述",
|
|
||||||
"rulesSource": "裁决来源",
|
|
||||||
"rulesEmpty": "暂无规则说明",
|
|
||||||
"mine": "我的",
|
|
||||||
"myPositions": "持仓",
|
|
||||||
"openOrders": "限价",
|
|
||||||
"noPositionsInMarket": "本市场暂无持仓",
|
|
||||||
"noOpenOrdersInMarket": "本市场暂无未成交订单",
|
|
||||||
"cancelOrder": "撤单",
|
|
||||||
"noCommentsYet": "暂无评论",
|
"noCommentsYet": "暂无评论",
|
||||||
"topHoldersPlaceholder": "持仓大户将在此显示",
|
"topHoldersPlaceholder": "持仓大户将在此显示",
|
||||||
"minAmount": "最小金额",
|
"minAmount": "最小金额",
|
||||||
@ -123,8 +108,6 @@
|
|||||||
"today": "今日",
|
"today": "今日",
|
||||||
"deposit": "入金",
|
"deposit": "入金",
|
||||||
"withdraw": "提现",
|
"withdraw": "提现",
|
||||||
"authorize": "授权",
|
|
||||||
"authorizeDesc": "授权 USDC 用于交易。允许交易合约在下单时使用您的余额。",
|
|
||||||
"profitLoss": "盈亏",
|
"profitLoss": "盈亏",
|
||||||
"allTime": "全部",
|
"allTime": "全部",
|
||||||
"pl1D": "1天",
|
"pl1D": "1天",
|
||||||
|
|||||||
@ -15,9 +15,7 @@
|
|||||||
"chance": "機率"
|
"chance": "機率"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"orderSuccess": "下單成功",
|
"orderSuccess": "下單成功"
|
||||||
"splitSuccess": "拆分成功",
|
|
||||||
"mergeSuccess": "合併成功"
|
|
||||||
},
|
},
|
||||||
"trade": {
|
"trade": {
|
||||||
"buy": "買入",
|
"buy": "買入",
|
||||||
@ -58,7 +56,6 @@
|
|||||||
"avgPrice": "平均價",
|
"avgPrice": "平均價",
|
||||||
"max": "最大",
|
"max": "最大",
|
||||||
"balanceLabel": "餘額",
|
"balanceLabel": "餘額",
|
||||||
"maxShares": "最大份額",
|
|
||||||
"pleaseLogin": "請先登入",
|
"pleaseLogin": "請先登入",
|
||||||
"pleaseSelectMarket": "請先選擇市場(需包含 clobTokenIds)",
|
"pleaseSelectMarket": "請先選擇市場(需包含 clobTokenIds)",
|
||||||
"userError": "用戶資訊異常",
|
"userError": "用戶資訊異常",
|
||||||
@ -85,24 +82,12 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"requestFailed": "請求失敗",
|
"requestFailed": "請求失敗",
|
||||||
"loadFailed": "載入失敗",
|
"loadFailed": "載入失敗",
|
||||||
"invalidId": "無效的 ID 或 slug",
|
"invalidId": "無效的 ID 或 slug"
|
||||||
"pleaseLogin": "請先登入",
|
|
||||||
"insufficientPermission": "權限不足"
|
|
||||||
},
|
},
|
||||||
"activity": {
|
"activity": {
|
||||||
"comments": "評論",
|
"comments": "評論",
|
||||||
"topHolders": "持倉大戶",
|
"topHolders": "持倉大戶",
|
||||||
"activity": "動態",
|
"activity": "動態",
|
||||||
"rules": "規則",
|
|
||||||
"rulesDescription": "描述",
|
|
||||||
"rulesSource": "裁決來源",
|
|
||||||
"rulesEmpty": "暫無規則說明",
|
|
||||||
"mine": "我的",
|
|
||||||
"myPositions": "持倉",
|
|
||||||
"openOrders": "限價",
|
|
||||||
"noPositionsInMarket": "本市場暫無持倉",
|
|
||||||
"noOpenOrdersInMarket": "本市場暫無未成交訂單",
|
|
||||||
"cancelOrder": "撤單",
|
|
||||||
"noCommentsYet": "暫無評論",
|
"noCommentsYet": "暫無評論",
|
||||||
"topHoldersPlaceholder": "持倉大戶將在此顯示",
|
"topHoldersPlaceholder": "持倉大戶將在此顯示",
|
||||||
"minAmount": "最小金額",
|
"minAmount": "最小金額",
|
||||||
@ -123,8 +108,6 @@
|
|||||||
"today": "今日",
|
"today": "今日",
|
||||||
"deposit": "入金",
|
"deposit": "入金",
|
||||||
"withdraw": "提現",
|
"withdraw": "提現",
|
||||||
"authorize": "授權",
|
|
||||||
"authorizeDesc": "授權 USDC 用於交易。允許交易合約在下單時使用您的餘額。",
|
|
||||||
"profitLoss": "盈虧",
|
"profitLoss": "盈虧",
|
||||||
"allTime": "全部",
|
"allTime": "全部",
|
||||||
"pl1D": "1天",
|
"pl1D": "1天",
|
||||||
|
|||||||
@ -44,14 +44,9 @@ export const useToastStore = defineStore('toast', () => {
|
|||||||
if (now - last >= DEDUP_MS) return false
|
if (now - last >= DEDUP_MS) return false
|
||||||
|
|
||||||
const msg = normalizeMsg(item.message)
|
const msg = normalizeMsg(item.message)
|
||||||
let inDisplayingIdx = -1
|
const inDisplayingIdx = displaying.value.findLastIndex(
|
||||||
for (let i = displaying.value.length - 1; i >= 0; i--) {
|
(d) => normalizeMsg(d.message) === msg && d.type === item.type
|
||||||
const d = displaying.value[i]
|
)
|
||||||
if (d && normalizeMsg(d.message) === msg && d.type === item.type) {
|
|
||||||
inDisplayingIdx = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (inDisplayingIdx >= 0) {
|
if (inDisplayingIdx >= 0) {
|
||||||
displaying.value = displaying.value.map((x, i) =>
|
displaying.value = displaying.value.map((x, i) =>
|
||||||
i === inDisplayingIdx
|
i === inDisplayingIdx
|
||||||
@ -100,7 +95,7 @@ export const useToastStore = defineStore('toast', () => {
|
|||||||
function remove(id: string) {
|
function remove(id: string) {
|
||||||
displaying.value = displaying.value.filter((d) => d.id !== id)
|
displaying.value = displaying.value.filter((d) => d.id !== id)
|
||||||
if (queue.value.length > 0) {
|
if (queue.value.length > 0) {
|
||||||
const next = queue.value[0]!
|
const next = queue.value[0]
|
||||||
queue.value = queue.value.slice(1)
|
queue.value = queue.value.slice(1)
|
||||||
displaying.value = [...displaying.value, next]
|
displaying.value = [...displaying.value, next]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { getUsdcBalance, formatUsdcBalance, getUserInfo } from '@/api/user'
|
import { getUsdcBalance, formatUsdcBalance, getUserInfo } from '@/api/user'
|
||||||
import { getUserWsUrl } from '@/api/request'
|
import { getUserWsUrl } from '@/api/request'
|
||||||
import { UserSdk, type BalanceData, type PositionData } from '../../sdk/userSocket'
|
import { UserSdk, type BalanceData } from '../../sdk/userSocket'
|
||||||
|
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
/** 用户 ID(API 可能返回 id 或 ID) */
|
/** 用户 ID(API 可能返回 id 或 ID) */
|
||||||
@ -53,7 +53,6 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
const balance = ref<string>('0.00')
|
const balance = ref<string>('0.00')
|
||||||
|
|
||||||
let userSdkRef: UserSdk | null = null
|
let userSdkRef: UserSdk | null = null
|
||||||
const positionUpdateCallbacks: ((data: PositionData & Record<string, unknown>) => void)[] = []
|
|
||||||
|
|
||||||
// 若从 storage 恢复登录态,自动连接 UserSocket
|
// 若从 storage 恢复登录态,自动连接 UserSocket
|
||||||
if (stored?.token && stored?.user) {
|
if (stored?.token && stored?.user) {
|
||||||
@ -71,15 +70,11 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
reconnectInterval: 2000,
|
reconnectInterval: 2000,
|
||||||
})
|
})
|
||||||
sdk.onBalanceUpdate((data: BalanceData) => {
|
sdk.onBalanceUpdate((data: BalanceData) => {
|
||||||
if ((data.tokenType ?? '').toUpperCase() !== 'USDC') return
|
|
||||||
const avail = data.available ?? data.amount
|
const avail = data.available ?? data.amount
|
||||||
if (avail != null) {
|
if (avail != null) {
|
||||||
balance.value = formatUsdcBalance(String(avail))
|
balance.value = formatUsdcBalance(String(avail))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
sdk.onPositionUpdate((data) => {
|
|
||||||
positionUpdateCallbacks.forEach((cb) => cb(data as PositionData & Record<string, unknown>))
|
|
||||||
})
|
|
||||||
sdk.onConnect(() => {})
|
sdk.onConnect(() => {})
|
||||||
sdk.onDisconnect(() => {})
|
sdk.onDisconnect(() => {})
|
||||||
sdk.onError((e) => {
|
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 }) {
|
function setUser(loginData: { token?: string; user?: UserInfo }) {
|
||||||
const t = loginData.token ?? ''
|
const t = loginData.token ?? ''
|
||||||
const raw = loginData.user ?? null
|
const raw = loginData.user ?? null
|
||||||
@ -165,38 +151,36 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 请求用户信息(需已登录),更新 store 中的 user 与 balance */
|
/** 请求用户信息(需已登录),更新 store 中的 user */
|
||||||
async function fetchUserInfo() {
|
async function fetchUserInfo() {
|
||||||
const headers = getAuthHeaders()
|
const headers = getAuthHeaders()
|
||||||
if (!headers) return
|
if (!headers) return
|
||||||
try {
|
try {
|
||||||
const res = await getUserInfo(headers)
|
const res = await getUserInfo(headers)
|
||||||
const data = res.data as Record<string, unknown> | undefined
|
const data = res.data as Record<string, unknown> | undefined
|
||||||
if (res.code !== 0 && res.code !== 200) return
|
// 接口返回 data.userInfo 或 data.user,取实际用户对象;若仍含 userInfo 则再取一层
|
||||||
// 更新余额:data.balance.available
|
let u = (data?.userInfo ?? data?.user ?? data) as Record<string, unknown>
|
||||||
const bal = data?.balance as { available?: string } | undefined
|
if (u?.userInfo && (u.ID == null && u.id == null)) {
|
||||||
if (bal?.available != null) {
|
u = u.userInfo as Record<string, unknown>
|
||||||
balance.value = formatUsdcBalance(String(bal.available))
|
}
|
||||||
|
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) {
|
} catch (e) {
|
||||||
console.error('[fetchUserInfo] 请求失败:', e)
|
console.error('[fetchUserInfo] 请求失败:', e)
|
||||||
}
|
}
|
||||||
@ -215,6 +199,5 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
fetchUserInfo,
|
fetchUserInfo,
|
||||||
connectUserSocket,
|
connectUserSocket,
|
||||||
disconnectUserSocket,
|
disconnectUserSocket,
|
||||||
onPositionUpdate,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -58,35 +58,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</v-card>
|
</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">
|
<v-card class="markets-list-card" elevation="0" rounded="lg">
|
||||||
<div class="markets-list">
|
<div class="markets-list">
|
||||||
@ -226,13 +197,11 @@ import { USE_MOCK_EVENT } from '../config/mock'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
import { useToastStore } from '../stores/toast'
|
import { useToastStore } from '../stores/toast'
|
||||||
import { useLocaleStore } from '../stores/locale'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const localeStore = useLocaleStore()
|
|
||||||
const { mobile } = useDisplay()
|
const { mobile } = useDisplay()
|
||||||
const isMobile = computed(() => mobile.value)
|
const isMobile = computed(() => mobile.value)
|
||||||
|
|
||||||
@ -318,11 +287,6 @@ const resolutionDate = computed(() => {
|
|||||||
return s ? s.replace(/,?\s*\d{4}$/, '').trim() || '' : ''
|
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 = [
|
const timeRanges = [
|
||||||
{ label: '1H', value: '1H' },
|
{ label: '1H', value: '1H' },
|
||||||
{ label: '6H', value: '6H' },
|
{ label: '6H', value: '6H' },
|
||||||
@ -736,14 +700,6 @@ watch(
|
|||||||
() => route.params.id,
|
() => route.params.id,
|
||||||
() => loadEventDetail(),
|
() => loadEventDetail(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 监听语言切换,语言变化时重新加载数据
|
|
||||||
watch(
|
|
||||||
() => localeStore.currentLocale,
|
|
||||||
() => {
|
|
||||||
loadEventDetail()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -943,58 +899,7 @@ watch(
|
|||||||
color: #dc2626;
|
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 {
|
.markets-list-card {
|
||||||
margin-top: 16px;
|
|
||||||
border: 1px solid #e7e7e7;
|
border: 1px solid #e7e7e7;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@ -165,8 +165,6 @@
|
|||||||
:is-new="card.isNew"
|
:is-new="card.isNew"
|
||||||
:market-id="card.marketId"
|
:market-id="card.marketId"
|
||||||
:clob-token-ids="card.clobTokenIds"
|
:clob-token-ids="card.clobTokenIds"
|
||||||
:yes-price="card.yesPrice"
|
|
||||||
:no-price="card.noPrice"
|
|
||||||
@open-trade="onCardOpenTrade"
|
@open-trade="onCardOpenTrade"
|
||||||
/>
|
/>
|
||||||
<div v-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
|
<div v-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
|
||||||
@ -304,7 +302,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineOptions({ name: 'Home' })
|
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 { useDisplay } from 'vuetify'
|
||||||
import MarketCard from '../components/MarketCard.vue'
|
import MarketCard from '../components/MarketCard.vue'
|
||||||
import TradeComponent from '../components/TradeComponent.vue'
|
import TradeComponent from '../components/TradeComponent.vue'
|
||||||
@ -328,7 +326,6 @@ import { USE_MOCK_CATEGORY } from '../config/mock'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useSearchHistory } from '../composables/useSearchHistory'
|
import { useSearchHistory } from '../composables/useSearchHistory'
|
||||||
import { useToastStore } from '../stores/toast'
|
import { useToastStore } from '../stores/toast'
|
||||||
import { useLocaleStore } from '../stores/locale'
|
|
||||||
|
|
||||||
const { mobile } = useDisplay()
|
const { mobile } = useDisplay()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@ -416,28 +413,16 @@ function findNodeById(nodes: CategoryTreeNode[], id: string): CategoryTreeNode |
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 当前选中分类的 tagIds:收集所有选中层级节点的 tagId 数组(含父级),用于事件筛选 */
|
/** 当前选中分类的 tag 筛选(取最后选中的层级,用于 API tagId/tagSlug) */
|
||||||
const activeTagIds = computed(() => {
|
const activeTagFilter = computed(() => {
|
||||||
const activeIds = layerActiveValues.value
|
const ids = layerActiveValues.value
|
||||||
const tagIdSet = new Set<number>()
|
if (ids.length === 0) return { tagId: undefined as number | undefined, tagSlug: undefined as string | undefined }
|
||||||
|
const lastId = ids[ids.length - 1]
|
||||||
// 遍历每一层选中的节点,收集所有 tagIds(含父级)
|
const root = filterVisible(categoryTree.value)
|
||||||
let currentNodes = filterVisible(categoryTree.value)
|
const node = lastId ? findNodeById(root, lastId) : undefined
|
||||||
for (let i = 0; i < activeIds.length; i++) {
|
if (!node) return { tagId: undefined, tagSlug: undefined }
|
||||||
const selectedId = activeIds[i]
|
const tagId = /^\d+$/.test(node.id) ? parseInt(node.id, 10) : undefined
|
||||||
if (!selectedId) continue
|
return { tagId, tagSlug: node.slug || undefined }
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 当前展示的层级数据:[[layer0], [layer1]?, [layer2]?] */
|
/** 当前展示的层级数据:[[layer0], [layer1]?, [layer2]?] */
|
||||||
@ -529,23 +514,12 @@ const tradeDialogMarket = ref<{
|
|||||||
clobTokenIds?: string[]
|
clobTokenIds?: string[]
|
||||||
yesLabel?: string
|
yesLabel?: string
|
||||||
noLabel?: string
|
noLabel?: string
|
||||||
yesPrice?: number
|
|
||||||
noPrice?: number
|
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const scrollRef = ref<HTMLElement | null>(null)
|
const scrollRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
function onCardOpenTrade(
|
function onCardOpenTrade(
|
||||||
side: 'yes' | 'no',
|
side: 'yes' | 'no',
|
||||||
market?: {
|
market?: { id: string; title: string; marketId?: string; yesLabel?: string; noLabel?: string },
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
marketId?: string
|
|
||||||
yesLabel?: string
|
|
||||||
noLabel?: string
|
|
||||||
yesPrice?: number
|
|
||||||
noPrice?: number
|
|
||||||
clobTokenIds?: string[]
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
tradeDialogSide.value = side
|
tradeDialogSide.value = side
|
||||||
tradeDialogMarket.value = market ?? null
|
tradeDialogMarket.value = market ?? null
|
||||||
@ -553,18 +527,6 @@ function onCardOpenTrade(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toastStore = useToastStore()
|
const toastStore = useToastStore()
|
||||||
const localeStore = useLocaleStore()
|
|
||||||
|
|
||||||
// 监听语言切换,语言变化时重新加载事件列表
|
|
||||||
watch(
|
|
||||||
() => localeStore.currentLocale,
|
|
||||||
() => {
|
|
||||||
clearEventListCache()
|
|
||||||
eventPage.value = 1
|
|
||||||
loadEvents(1, false, activeSearchKeyword.value)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
function onOrderSuccess() {
|
function onOrderSuccess() {
|
||||||
tradeDialogOpen.value = false
|
tradeDialogOpen.value = false
|
||||||
toastStore.show(t('toast.orderSuccess'))
|
toastStore.show(t('toast.orderSuccess'))
|
||||||
@ -575,14 +537,9 @@ const homeTradeMarketPayload = computed(() => {
|
|||||||
const m = tradeDialogMarket.value
|
const m = tradeDialogMarket.value
|
||||||
if (!m) return undefined
|
if (!m) return undefined
|
||||||
const marketId = m.marketId ?? m.id
|
const marketId = m.marketId ?? m.id
|
||||||
const yesPrice =
|
const chance = 50
|
||||||
m.yesPrice != null && Number.isFinite(m.yesPrice)
|
const yesPrice = Math.min(1, Math.max(0, chance / 100))
|
||||||
? Math.min(1, Math.max(0, m.yesPrice))
|
const noPrice = 1 - yesPrice
|
||||||
: 0.5
|
|
||||||
const noPrice =
|
|
||||||
m.noPrice != null && Number.isFinite(m.noPrice)
|
|
||||||
? Math.min(1, Math.max(0, m.noPrice))
|
|
||||||
: 1 - yesPrice
|
|
||||||
const outcomes =
|
const outcomes =
|
||||||
m.yesLabel != null || m.noLabel != null
|
m.yesLabel != null || m.noLabel != null
|
||||||
? [m.yesLabel ?? 'Yes', m.noLabel ?? 'No']
|
? [m.yesLabel ?? 'Yes', m.noLabel ?? 'No']
|
||||||
@ -621,13 +578,14 @@ const activeSearchKeyword = ref('')
|
|||||||
/** 请求事件列表并追加或覆盖到 eventList(公开接口,无需鉴权);成功后会更新内存缓存 */
|
/** 请求事件列表并追加或覆盖到 eventList(公开接口,无需鉴权);成功后会更新内存缓存 */
|
||||||
async function loadEvents(page: number, append: boolean, keyword?: string) {
|
async function loadEvents(page: number, append: boolean, keyword?: string) {
|
||||||
const kw = keyword !== undefined ? keyword : activeSearchKeyword.value
|
const kw = keyword !== undefined ? keyword : activeSearchKeyword.value
|
||||||
const tagIds = activeTagIds.value
|
const { tagId, tagSlug } = activeTagFilter.value
|
||||||
try {
|
try {
|
||||||
const res = await getPmEventPublic({
|
const res = await getPmEventPublic({
|
||||||
page,
|
page,
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
keyword: kw || undefined,
|
keyword: kw || undefined,
|
||||||
tagIds: tagIds.length > 0 ? tagIds : undefined,
|
tagId,
|
||||||
|
tagSlug,
|
||||||
})
|
})
|
||||||
if (res.code !== 0 && res.code !== 200) {
|
if (res.code !== 0 && res.code !== 200) {
|
||||||
throw new Error(res.msg || '请求失败')
|
throw new Error(res.msg || '请求失败')
|
||||||
@ -680,7 +638,7 @@ function checkScrollLoad() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
/** 分类树就绪后加载列表(确保 activeTagIds 已计算,与下拉刷新参数一致) */
|
/** 分类树就绪后加载列表(确保 activeTagFilter 已计算,与下拉刷新参数一致) */
|
||||||
function loadEventListAfterCategoryReady() {
|
function loadEventListAfterCategoryReady() {
|
||||||
const cached = getEventListCache()
|
const cached = getEventListCache()
|
||||||
if (cached && cached.list.length > 0) {
|
if (cached && cached.list.length > 0) {
|
||||||
|
|||||||
@ -44,78 +44,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</v-card>
|
</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 -->
|
<!-- Order Book Section -->
|
||||||
<v-card class="order-book-card" elevation="0" rounded="lg">
|
<v-card class="order-book-card" elevation="0" rounded="lg">
|
||||||
<OrderBook
|
<OrderBook
|
||||||
@ -137,37 +65,13 @@
|
|||||||
<!-- Comments / Top Holders / Activity(与左侧图表、订单簿同宽) -->
|
<!-- Comments / Top Holders / Activity(与左侧图表、订单簿同宽) -->
|
||||||
<v-card class="activity-card" elevation="0" rounded="lg">
|
<v-card class="activity-card" elevation="0" rounded="lg">
|
||||||
<v-tabs v-model="detailTab" class="detail-tabs" density="comfortable">
|
<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="holders">{{ t('activity.topHolders') }}</v-tab>
|
||||||
<v-tab value="activity">{{ t('activity.activity') }}</v-tab>
|
<v-tab value="activity">{{ t('activity.activity') }}</v-tab>
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
<v-window v-model="detailTab" class="detail-window">
|
<v-window v-model="detailTab" class="detail-window">
|
||||||
<v-window-item value="rules" class="detail-pane">
|
<v-window-item value="comments" class="detail-pane">
|
||||||
<div class="rules-pane">
|
<div class="placeholder-pane">{{ t('activity.noCommentsYet') }}</div>
|
||||||
<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>
|
</v-window-item>
|
||||||
<v-window-item value="holders" class="detail-pane">
|
<v-window-item value="holders" class="detail-pane">
|
||||||
<div class="placeholder-pane">{{ t('activity.topHoldersPlaceholder') }}</div>
|
<div class="placeholder-pane">{{ t('activity.topHoldersPlaceholder') }}</div>
|
||||||
@ -225,14 +129,7 @@
|
|||||||
<!-- 右侧:交易组件(固定宽度),传入当前市场以便 Split 调用拆单接口;移动端隐藏,改用底部栏+弹窗 -->
|
<!-- 右侧:交易组件(固定宽度),传入当前市场以便 Split 调用拆单接口;移动端隐藏,改用底部栏+弹窗 -->
|
||||||
<v-col v-if="!isMobile" cols="12" class="trade-col">
|
<v-col v-if="!isMobile" cols="12" class="trade-col">
|
||||||
<div class="trade-sidebar">
|
<div class="trade-sidebar">
|
||||||
<TradeComponent
|
<TradeComponent ref="tradeComponentRef" :market="tradeMarketPayload" :initial-option="tradeInitialOption" />
|
||||||
ref="tradeComponentRef"
|
|
||||||
:market="tradeMarketPayload"
|
|
||||||
:initial-option="tradeInitialOption"
|
|
||||||
:positions="tradePositionsForComponent"
|
|
||||||
@merge-success="onMergeSuccess"
|
|
||||||
@split-success="onSplitSuccess"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
@ -289,34 +186,11 @@
|
|||||||
ref="mobileTradeComponentRef"
|
ref="mobileTradeComponentRef"
|
||||||
:market="tradeMarketPayload"
|
:market="tradeMarketPayload"
|
||||||
:initial-option="tradeInitialOptionFromBar"
|
:initial-option="tradeInitialOptionFromBar"
|
||||||
:initial-tab="tradeInitialTabFromBar"
|
|
||||||
:positions="tradePositionsForComponent"
|
|
||||||
embedded-in-sheet
|
embedded-in-sheet
|
||||||
@order-success="onOrderSuccess"
|
@order-success="onOrderSuccess"
|
||||||
@merge-success="onMergeSuccess"
|
|
||||||
@split-success="onSplitSuccess"
|
|
||||||
/>
|
/>
|
||||||
</v-bottom-sheet>
|
</v-bottom-sheet>
|
||||||
</template>
|
</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-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
@ -329,7 +203,7 @@ import { useDisplay } from 'vuetify'
|
|||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
import type { ECharts } from 'echarts'
|
import type { ECharts } from 'echarts'
|
||||||
import OrderBook from '../components/OrderBook.vue'
|
import OrderBook from '../components/OrderBook.vue'
|
||||||
import TradeComponent, { type TradePositionItem } from '../components/TradeComponent.vue'
|
import TradeComponent from '../components/TradeComponent.vue'
|
||||||
import {
|
import {
|
||||||
findPmEvent,
|
findPmEvent,
|
||||||
getMarketId,
|
getMarketId,
|
||||||
@ -340,16 +214,6 @@ import {
|
|||||||
import { getClobWsUrl } from '../api/request'
|
import { getClobWsUrl } from '../api/request'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
import { useToastStore } from '../stores/toast'
|
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()
|
const { t } = useI18n()
|
||||||
import {
|
import {
|
||||||
@ -387,8 +251,6 @@ export type ChartIncrement = { point: ChartPoint }
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const { formatAuthError } = useAuthError()
|
|
||||||
const localeStore = useLocaleStore()
|
|
||||||
const { mobile } = useDisplay()
|
const { mobile } = useDisplay()
|
||||||
const isMobile = computed(() => mobile.value)
|
const isMobile = computed(() => mobile.value)
|
||||||
|
|
||||||
@ -460,7 +322,7 @@ async function loadEventDetail() {
|
|||||||
eventDetail.value = null
|
eventDetail.value = null
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
detailError.value = formatAuthError(e, t('error.loadFailed'))
|
detailError.value = e instanceof Error ? e.message : t('error.loadFailed')
|
||||||
eventDetail.value = null
|
eventDetail.value = null
|
||||||
} finally {
|
} finally {
|
||||||
detailLoading.value = false
|
detailLoading.value = false
|
||||||
@ -485,11 +347,6 @@ const resolutionDate = computed(() => {
|
|||||||
return s ? s.replace(/,?\s*\d{4}$/, '').trim() || 'Mar 31' : 'Mar 31'
|
return s ? s.replace(/,?\s*\d{4}$/, '').trim() || 'Mar 31' : 'Mar 31'
|
||||||
})
|
})
|
||||||
|
|
||||||
const isResolutionSourceUrl = computed(() => {
|
|
||||||
const src = eventDetail.value?.resolutionSource
|
|
||||||
return typeof src === 'string' && (src.startsWith('http://') || src.startsWith('https://'))
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 当前市场(用于交易组件与 Split 拆单):query.marketId 匹配或取第一个 */
|
/** 当前市场(用于交易组件与 Split 拆单):query.marketId 匹配或取第一个 */
|
||||||
const currentMarket = computed(() => {
|
const currentMarket = computed(() => {
|
||||||
const list = eventDetail.value?.markets ?? []
|
const list = eventDetail.value?.markets ?? []
|
||||||
@ -522,19 +379,6 @@ const orderBookAsksYes = computed(() => orderBookByToken.value[0]?.asks ?? [])
|
|||||||
const orderBookBidsYes = computed(() => orderBookByToken.value[0]?.bids ?? [])
|
const orderBookBidsYes = computed(() => orderBookByToken.value[0]?.bids ?? [])
|
||||||
const orderBookAsksNo = computed(() => orderBookByToken.value[1]?.asks ?? [])
|
const orderBookAsksNo = computed(() => orderBookByToken.value[1]?.asks ?? [])
|
||||||
const orderBookBidsNo = computed(() => orderBookByToken.value[1]?.bids ?? [])
|
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 clobLastPriceYes = computed(() => clobLastPriceByToken.value[0])
|
||||||
const clobLastPriceNo = computed(() => clobLastPriceByToken.value[1])
|
const clobLastPriceNo = computed(() => clobLastPriceByToken.value[1])
|
||||||
const clobSpreadYes = computed(() => clobSpreadByToken.value[0])
|
const clobSpreadYes = computed(() => clobSpreadByToken.value[0])
|
||||||
@ -675,12 +519,14 @@ function disconnectClob() {
|
|||||||
clobLoading.value = false
|
clobLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 传给 TradeComponent 的 market,供 Split 调用 /PmMarket/split;yesPrice/noPrice 取订单簿卖单最低价,无数据时为 0 */
|
/** 传给 TradeComponent 的 market,供 Split 调用 /PmMarket/split;接口未返回时用 query 兜底 */
|
||||||
const tradeMarketPayload = computed(() => {
|
const tradeMarketPayload = computed(() => {
|
||||||
const m = currentMarket.value
|
const m = currentMarket.value
|
||||||
const yesPrice = orderBookLowestAskYesCents.value / 100
|
|
||||||
const noPrice = orderBookLowestAskNoCents.value / 100
|
|
||||||
if (m) {
|
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 {
|
return {
|
||||||
marketId: getMarketId(m),
|
marketId: getMarketId(m),
|
||||||
yesPrice,
|
yesPrice,
|
||||||
@ -692,6 +538,9 @@ const tradeMarketPayload = computed(() => {
|
|||||||
}
|
}
|
||||||
const qId = route.query.marketId
|
const qId = route.query.marketId
|
||||||
if (qId != null && String(qId).trim() !== '') {
|
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 {
|
return {
|
||||||
marketId: String(qId).trim(),
|
marketId: String(qId).trim(),
|
||||||
yesPrice,
|
yesPrice,
|
||||||
@ -710,14 +559,8 @@ const tradeInitialOption = computed(() => {
|
|||||||
|
|
||||||
/** 移动端底部栏点击 Yes/No 时传给弹窗内 TradeComponent 的初始选项 */
|
/** 移动端底部栏点击 Yes/No 时传给弹窗内 TradeComponent 的初始选项 */
|
||||||
const tradeInitialOptionFromBar = ref<'yes' | 'no' | undefined>(undefined)
|
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)
|
const tradeSheetOpen = ref(false)
|
||||||
/** 从持仓 Sell 打开的弹窗 */
|
|
||||||
const sellDialogOpen = ref(false)
|
|
||||||
/** 从持仓 Sell 时预选的 Yes/No */
|
|
||||||
const sellInitialOption = ref<'yes' | 'no'>('yes')
|
|
||||||
/** 移动端三点菜单开关 */
|
/** 移动端三点菜单开关 */
|
||||||
const mobileMenuOpen = ref(false)
|
const mobileMenuOpen = ref(false)
|
||||||
/** 桌面端 TradeComponent 引用(Merge/Split) */
|
/** 桌面端 TradeComponent 引用(Merge/Split) */
|
||||||
@ -736,7 +579,6 @@ const noLabel = computed(() => currentMarket.value?.outcomes?.[1] ?? 'No')
|
|||||||
|
|
||||||
function openSheetWithOption(side: 'yes' | 'no') {
|
function openSheetWithOption(side: 'yes' | 'no') {
|
||||||
tradeInitialOptionFromBar.value = side
|
tradeInitialOptionFromBar.value = side
|
||||||
tradeInitialTabFromBar.value = undefined
|
|
||||||
tradeSheetOpen.value = true
|
tradeSheetOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -760,179 +602,11 @@ const toastStore = useToastStore()
|
|||||||
function onOrderSuccess() {
|
function onOrderSuccess() {
|
||||||
tradeSheetOpen.value = false
|
tradeSheetOpen.value = false
|
||||||
toastStore.show(t('toast.orderSuccess'))
|
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
|
// Comments / Top Holders / Activity
|
||||||
const detailTab = ref('rules')
|
const detailTab = ref('activity')
|
||||||
const activityMinAmount = ref<string>('0')
|
const activityMinAmount = ref<string>('0')
|
||||||
|
|
||||||
const minAmountOptions = computed(() => [
|
const minAmountOptions = computed(() => [
|
||||||
{ title: t('activity.any'), value: '0' },
|
{ title: t('activity.any'), value: '0' },
|
||||||
{ title: '$1', value: '1' },
|
{ title: '$1', value: '1' },
|
||||||
@ -1322,23 +996,6 @@ watch(
|
|||||||
{ immediate: false },
|
{ 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(() => {
|
onMounted(() => {
|
||||||
loadEventDetail()
|
loadEventDetail()
|
||||||
initChart()
|
initChart()
|
||||||
@ -1347,7 +1004,6 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unsubscribePositionUpdate()
|
|
||||||
stopDynamicUpdate()
|
stopDynamicUpdate()
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
chartInstance?.dispose()
|
chartInstance?.dispose()
|
||||||
@ -1614,30 +1270,6 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Order Book Card Styles(扁平化) */
|
/* 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 {
|
.order-book-card {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -1794,124 +1426,6 @@ onUnmounted(() => {
|
|||||||
padding: 24px 0;
|
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 {
|
.activity-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -35,15 +35,6 @@
|
|||||||
>
|
>
|
||||||
{{ t('wallet.withdraw') }}
|
{{ t('wallet.withdraw') }}
|
||||||
</v-btn>
|
</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>
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
@ -154,10 +145,7 @@
|
|||||||
<template v-if="activeTab === 'positions'">
|
<template v-if="activeTab === 'positions'">
|
||||||
<!-- 移动端:可折叠列表 -->
|
<!-- 移动端:可折叠列表 -->
|
||||||
<div v-if="mobile" class="positions-mobile-list">
|
<div v-if="mobile" class="positions-mobile-list">
|
||||||
<template v-if="positionLoading">
|
<template v-if="filteredPositions.length === 0">
|
||||||
<div class="empty-cell">{{ t('common.loading') }}</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="filteredPositions.length === 0">
|
|
||||||
<div class="empty-cell">{{ t('wallet.noPositionsFound') }}</div>
|
<div class="empty-cell">{{ t('wallet.noPositionsFound') }}</div>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
@ -259,10 +247,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="positionLoading">
|
<tr v-if="filteredPositions.length === 0">
|
||||||
<td colspan="6" class="empty-cell">{{ t('common.loading') }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-else-if="filteredPositions.length === 0">
|
|
||||||
<td colspan="6" class="empty-cell">{{ t('wallet.noPositionsFound') }}</td>
|
<td colspan="6" class="empty-cell">{{ t('wallet.noPositionsFound') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="pos in paginatedPositions" :key="pos.id" class="position-row">
|
<tr v-for="pos in paginatedPositions" :key="pos.id" class="position-row">
|
||||||
@ -331,10 +316,7 @@
|
|||||||
<template v-else-if="activeTab === 'orders'">
|
<template v-else-if="activeTab === 'orders'">
|
||||||
<!-- 移动端:挂单卡片列表 -->
|
<!-- 移动端:挂单卡片列表 -->
|
||||||
<div v-if="mobile" class="orders-mobile-list">
|
<div v-if="mobile" class="orders-mobile-list">
|
||||||
<template v-if="openOrderLoading">
|
<template v-if="filteredOpenOrders.length === 0">
|
||||||
<div class="empty-cell">{{ t('common.loading') }}</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="filteredOpenOrders.length === 0">
|
|
||||||
<div class="empty-cell">{{ t('wallet.noOpenOrdersFound') }}</div>
|
<div class="empty-cell">{{ t('wallet.noOpenOrdersFound') }}</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-for="ord in paginatedOpenOrders" :key="ord.id" class="order-mobile-card">
|
<div v-for="ord in paginatedOpenOrders" :key="ord.id" class="order-mobile-card">
|
||||||
@ -383,10 +365,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="openOrderLoading">
|
<tr v-if="filteredOpenOrders.length === 0">
|
||||||
<td colspan="8" class="empty-cell">{{ t('common.loading') }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-else-if="filteredOpenOrders.length === 0">
|
|
||||||
<td colspan="8" class="empty-cell">{{ t('wallet.noOpenOrdersFound') }}</td>
|
<td colspan="8" class="empty-cell">{{ t('wallet.noOpenOrdersFound') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="ord in paginatedOpenOrders" :key="ord.id">
|
<tr v-for="ord in paginatedOpenOrders" :key="ord.id">
|
||||||
@ -417,10 +396,7 @@
|
|||||||
<template v-else-if="activeTab === 'history'">
|
<template v-else-if="activeTab === 'history'">
|
||||||
<!-- 移动端:历史卡片列表 -->
|
<!-- 移动端:历史卡片列表 -->
|
||||||
<div v-if="mobile" class="history-mobile-list">
|
<div v-if="mobile" class="history-mobile-list">
|
||||||
<template v-if="historyLoading">
|
<template v-if="filteredHistory.length === 0">
|
||||||
<div class="empty-cell">{{ t('common.loading') }}</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="filteredHistory.length === 0">
|
|
||||||
<div class="empty-cell">{{ t('wallet.noHistoryFound') }}</div>
|
<div class="empty-cell">{{ t('wallet.noHistoryFound') }}</div>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
@ -486,10 +462,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="historyLoading">
|
<tr v-if="filteredHistory.length === 0">
|
||||||
<td colspan="3" class="empty-cell">{{ t('common.loading') }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-else-if="filteredHistory.length === 0">
|
|
||||||
<td colspan="3" class="empty-cell">{{ t('wallet.noHistoryFound') }}</td>
|
<td colspan="3" class="empty-cell">{{ t('wallet.noHistoryFound') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="h in paginatedHistory" :key="h.id">
|
<tr v-for="h in paginatedHistory" :key="h.id">
|
||||||
@ -537,33 +510,6 @@
|
|||||||
@success="onWithdrawSuccess"
|
@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 -->
|
<!-- Sell position dialog -->
|
||||||
<v-dialog
|
<v-dialog
|
||||||
v-model="sellDialogOpen"
|
v-model="sellDialogOpen"
|
||||||
@ -626,11 +572,7 @@ import type { ECharts } from 'echarts'
|
|||||||
import DepositDialog from '../components/DepositDialog.vue'
|
import DepositDialog from '../components/DepositDialog.vue'
|
||||||
import WithdrawDialog from '../components/WithdrawDialog.vue'
|
import WithdrawDialog from '../components/WithdrawDialog.vue'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
import { useLocaleStore } from '../stores/locale'
|
import { pmCancelOrder } from '../api/market'
|
||||||
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 {
|
import {
|
||||||
MOCK_TOKEN_ID,
|
MOCK_TOKEN_ID,
|
||||||
MOCK_WALLET_POSITIONS,
|
MOCK_WALLET_POSITIONS,
|
||||||
@ -638,12 +580,9 @@ import {
|
|||||||
MOCK_WALLET_HISTORY,
|
MOCK_WALLET_HISTORY,
|
||||||
} from '../api/mockData'
|
} from '../api/mockData'
|
||||||
import { USE_MOCK_WALLET } from '../config/mock'
|
import { USE_MOCK_WALLET } from '../config/mock'
|
||||||
import { CrossChainUSDTAuth } from '../../sdk/approve'
|
|
||||||
|
|
||||||
const { mobile } = useDisplay()
|
const { mobile } = useDisplay()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const { formatAuthError } = useAuthError()
|
|
||||||
const localeStore = useLocaleStore()
|
|
||||||
const portfolioBalance = computed(() => userStore.balance)
|
const portfolioBalance = computed(() => userStore.balance)
|
||||||
const profitLoss = ref('0.00')
|
const profitLoss = ref('0.00')
|
||||||
const plRange = ref('ALL')
|
const plRange = ref('ALL')
|
||||||
@ -657,7 +596,6 @@ const activeTab = ref<'positions' | 'orders' | 'history'>('positions')
|
|||||||
const search = ref('')
|
const search = ref('')
|
||||||
const depositDialogOpen = ref(false)
|
const depositDialogOpen = ref(false)
|
||||||
const withdrawDialogOpen = ref(false)
|
const withdrawDialogOpen = ref(false)
|
||||||
const authorizeDialogOpen = ref(false)
|
|
||||||
const sellDialogOpen = ref(false)
|
const sellDialogOpen = ref(false)
|
||||||
const sellPositionItem = ref<Position | null>(null)
|
const sellPositionItem = ref<Position | null>(null)
|
||||||
/** 移动端展开的持仓 id,null 表示全部折叠 */
|
/** 移动端展开的持仓 id,null 表示全部折叠 */
|
||||||
@ -741,164 +679,20 @@ interface HistoryItem {
|
|||||||
const positions = ref<Position[]>(
|
const positions = ref<Position[]>(
|
||||||
USE_MOCK_WALLET ? [...MOCK_WALLET_POSITIONS] : [],
|
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[]>(
|
const openOrders = ref<OpenOrder[]>(
|
||||||
USE_MOCK_WALLET ? [...MOCK_WALLET_ORDERS] : [],
|
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[]>(
|
const history = ref<HistoryItem[]>(
|
||||||
USE_MOCK_WALLET ? [...MOCK_WALLET_HISTORY] : [],
|
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 {
|
function matchSearch(text: string): boolean {
|
||||||
const q = search.value.trim().toLowerCase()
|
const q = search.value.trim().toLowerCase()
|
||||||
return !q || text.toLowerCase().includes(q)
|
return !q || text.toLowerCase().includes(q)
|
||||||
}
|
}
|
||||||
const filteredPositions = computed(() => {
|
const filteredPositions = computed(() => positions.value.filter((p) => matchSearch(p.market)))
|
||||||
const list = USE_MOCK_WALLET ? positions.value : positionList.value
|
const filteredOpenOrders = computed(() => openOrders.value.filter((o) => matchSearch(o.market)))
|
||||||
return list.filter((p) => matchSearch(p.market))
|
const filteredHistory = computed(() => history.value.filter((h) => matchSearch(h.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 page = ref(1)
|
const page = ref(1)
|
||||||
const itemsPerPage = ref(10)
|
const itemsPerPage = ref(10)
|
||||||
@ -908,38 +702,24 @@ function paginate<T>(list: T[]) {
|
|||||||
const start = (page.value - 1) * itemsPerPage.value
|
const start = (page.value - 1) * itemsPerPage.value
|
||||||
return list.slice(start, start + itemsPerPage.value)
|
return list.slice(start, start + itemsPerPage.value)
|
||||||
}
|
}
|
||||||
const paginatedPositions = computed(() => {
|
const paginatedPositions = computed(() => paginate(filteredPositions.value))
|
||||||
if (USE_MOCK_WALLET) return paginate(filteredPositions.value)
|
const paginatedOpenOrders = computed(() => paginate(filteredOpenOrders.value))
|
||||||
return filteredPositions.value
|
const paginatedHistory = computed(() => paginate(filteredHistory.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 totalPagesPositions = computed(() => {
|
const totalPagesPositions = computed(() =>
|
||||||
const total = USE_MOCK_WALLET ? filteredPositions.value.length : positionTotal.value
|
Math.max(1, Math.ceil(filteredPositions.value.length / itemsPerPage.value)),
|
||||||
return Math.max(1, Math.ceil(total / itemsPerPage.value))
|
)
|
||||||
})
|
const totalPagesOrders = computed(() =>
|
||||||
const totalPagesOrders = computed(() => {
|
Math.max(1, Math.ceil(filteredOpenOrders.value.length / itemsPerPage.value)),
|
||||||
const total = USE_MOCK_WALLET ? filteredOpenOrders.value.length : openOrderTotal.value
|
)
|
||||||
return Math.max(1, Math.ceil(total / itemsPerPage.value))
|
const totalPagesHistory = computed(() =>
|
||||||
})
|
Math.max(1, Math.ceil(filteredHistory.value.length / 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 currentListTotal = computed(() => {
|
const currentListTotal = computed(() => {
|
||||||
if (activeTab.value === 'positions')
|
if (activeTab.value === 'positions') return filteredPositions.value.length
|
||||||
return USE_MOCK_WALLET ? filteredPositions.value.length : positionTotal.value
|
if (activeTab.value === 'orders') return filteredOpenOrders.value.length
|
||||||
if (activeTab.value === 'orders')
|
return filteredHistory.value.length
|
||||||
return USE_MOCK_WALLET ? filteredOpenOrders.value.length : openOrderTotal.value
|
|
||||||
return USE_MOCK_WALLET ? filteredHistory.value.length : historyTotal.value
|
|
||||||
})
|
})
|
||||||
const currentTotalPages = computed(() => {
|
const currentTotalPages = computed(() => {
|
||||||
if (activeTab.value === 'positions') return totalPagesPositions.value
|
if (activeTab.value === 'positions') return totalPagesPositions.value
|
||||||
@ -953,16 +733,8 @@ const currentPageEnd = computed(() =>
|
|||||||
Math.min(page.value * itemsPerPage.value, currentListTotal.value),
|
Math.min(page.value * itemsPerPage.value, currentListTotal.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(activeTab, (tab) => {
|
watch(activeTab, () => {
|
||||||
page.value = 1
|
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], () => {
|
watch([currentListTotal, itemsPerPage], () => {
|
||||||
const maxPage = currentTotalPages.value
|
const maxPage = currentTotalPages.value
|
||||||
@ -982,34 +754,29 @@ async function cancelOrder(ord: OpenOrder) {
|
|||||||
const uid = userStore.user?.id ?? userStore.user?.ID
|
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||||
const userID = uid != null ? Number(uid) : 0
|
const userID = uid != null ? Number(uid) : 0
|
||||||
if (!Number.isFinite(userID) || userID <= 0) {
|
if (!Number.isFinite(userID) || userID <= 0) {
|
||||||
cancelOrderError.value = t('error.pleaseLogin')
|
cancelOrderError.value = '请先登录'
|
||||||
showCancelError.value = true
|
showCancelError.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const headers = userStore.getAuthHeaders()
|
const headers = userStore.getAuthHeaders()
|
||||||
if (!headers) {
|
if (!headers) {
|
||||||
cancelOrderError.value = t('error.pleaseLogin')
|
cancelOrderError.value = '请先登录'
|
||||||
showCancelError.value = true
|
showCancelError.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cancelOrderLoading.value = true
|
cancelOrderLoading.value = true
|
||||||
cancelOrderError.value = ''
|
cancelOrderError.value = ''
|
||||||
try {
|
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 (res.code === 0 || res.code === 200) {
|
||||||
if (USE_MOCK_WALLET) {
|
openOrders.value = openOrders.value.filter((o) => o.id !== ord.id)
|
||||||
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
|
|
||||||
}
|
|
||||||
userStore.fetchUsdcBalance()
|
userStore.fetchUsdcBalance()
|
||||||
} else {
|
} else {
|
||||||
cancelOrderError.value = res.msg || '取消失败'
|
cancelOrderError.value = res.msg || '取消失败'
|
||||||
showCancelError.value = true
|
showCancelError.value = true
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
cancelOrderError.value = formatAuthError(e, t('error.requestFailed'))
|
cancelOrderError.value = e instanceof Error ? e.message : 'Request failed'
|
||||||
showCancelError.value = true
|
showCancelError.value = true
|
||||||
} finally {
|
} finally {
|
||||||
cancelOrderLoading.value = false
|
cancelOrderLoading.value = false
|
||||||
@ -1017,12 +784,7 @@ async function cancelOrder(ord: OpenOrder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cancelAllOrders() {
|
function cancelAllOrders() {
|
||||||
if (USE_MOCK_WALLET) {
|
openOrders.value = []
|
||||||
openOrders.value = []
|
|
||||||
} else {
|
|
||||||
openOrderList.value = []
|
|
||||||
openOrderTotal.value = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sellReceiveAmount = computed(() => {
|
const sellReceiveAmount = computed(() => {
|
||||||
@ -1188,17 +950,7 @@ const handleResize = () => plChartInstance?.resize()
|
|||||||
|
|
||||||
watch(plRange, () => updatePlChart())
|
watch(plRange, () => updatePlChart())
|
||||||
|
|
||||||
// 监听语言切换,语言变化时重新加载数据
|
|
||||||
watch(
|
|
||||||
() => localeStore.currentLocale,
|
|
||||||
() => {
|
|
||||||
loadPositionList()
|
|
||||||
loadOpenOrders()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!USE_MOCK_WALLET && activeTab.value === 'positions') loadPositionList()
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
initPlChart()
|
initPlChart()
|
||||||
})
|
})
|
||||||
@ -1214,16 +966,6 @@ onUnmounted(() => {
|
|||||||
function onWithdrawSuccess() {
|
function onWithdrawSuccess() {
|
||||||
withdrawDialogOpen.value = false
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user