Initial commit: Monisuo - 虚拟货币模拟交易平台
功能模块: - 用户注册/登录/KYC - 资金账户/交易账户 - 实时行情/币种管理 - 即时交易/充提审核 - 管理后台 技术栈: - 后端: SpringBoot 2.2.4 + MyBatis Plus - 前端: uni-app x (Vue3 + UTS) - 数据库: MySQL Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
60
app/App.uvue
Normal file
60
app/App.uvue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="uts">
|
||||
export default {
|
||||
onLaunch: function () {
|
||||
console.log('模拟所APP启动')
|
||||
this.checkLoginStatus()
|
||||
},
|
||||
onShow: function () {
|
||||
console.log('APP显示')
|
||||
},
|
||||
onHide: function () {
|
||||
console.log('APP隐藏')
|
||||
},
|
||||
methods: {
|
||||
checkLoginStatus () {
|
||||
const token = uni.getStorageSync('token') as string
|
||||
if (token === null || token === '') {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/uni.scss';
|
||||
|
||||
page {
|
||||
background: $bg-color-dark;
|
||||
font-size: $font-size-base;
|
||||
color: $text-color-primary;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: $spacing-base;
|
||||
}
|
||||
|
||||
.text-primary { color: $primary-color; }
|
||||
.text-success { color: $success-color; }
|
||||
.text-warning { color: $warning-color; }
|
||||
.text-danger { color: $danger-color; }
|
||||
|
||||
.bg-primary { background: $primary-color; }
|
||||
.bg-card { background: $bg-color-card; }
|
||||
|
||||
.flex { display: flex; }
|
||||
.flex-center { display: flex; justify-content: center; align-items: center; }
|
||||
.flex-between { display: flex; justify-content: space-between; align-items: center; }
|
||||
.flex-column { display: flex; flex-direction: column; }
|
||||
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.mt-sm { margin-top: $spacing-sm; }
|
||||
.mt-base { margin-top: $spacing-base; }
|
||||
.mt-lg { margin-top: $spacing-lg; }
|
||||
.mb-sm { margin-bottom: $spacing-sm; }
|
||||
.mb-base { margin-bottom: $spacing-base; }
|
||||
.mb-lg { margin-bottom: $spacing-lg; }
|
||||
</style>
|
||||
176
app/PACKAGING.md
Normal file
176
app/PACKAGING.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# 模拟所APP - 打包发布指南
|
||||
|
||||
## 📋 完整流程
|
||||
|
||||
### 第一步:部署后端到服务器
|
||||
|
||||
1. **打包后端项目**
|
||||
```bash
|
||||
cd d:/workspace/project/com-rattan-spccloud
|
||||
mvn clean package -DskipTests
|
||||
```
|
||||
|
||||
2. **上传JAR包到服务器**
|
||||
```bash
|
||||
# 生成的JAR包位置
|
||||
target/com-rattan-spccloud-1.0.jar
|
||||
|
||||
# 使用scp上传到服务器
|
||||
scp target/com-rattan-spccloud-1.0.jar root@8.155.172.147:/opt/monisuo/
|
||||
```
|
||||
|
||||
3. **在服务器上启动后端**
|
||||
```bash
|
||||
# SSH连接服务器
|
||||
ssh root@8.155.172.147
|
||||
|
||||
# 启动服务
|
||||
cd /opt/monisuo
|
||||
nohup java -jar com-rattan-spccloud-1.0.jar --spring.profiles.active=dev > app.log 2>&1 &
|
||||
|
||||
# 查看日志
|
||||
tail -f app.log
|
||||
```
|
||||
|
||||
4. **确保服务器防火墙开放9010端口**
|
||||
```bash
|
||||
# 检查端口
|
||||
netstat -tlnp | grep 9010
|
||||
|
||||
# 如果使用firewalld
|
||||
firewall-cmd --zone=public --add-port=9010/tcp --permanent
|
||||
firewall-cmd --reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 第二步:初始化数据库
|
||||
|
||||
在服务器上执行SQL脚本:
|
||||
```bash
|
||||
mysql -u monisuo -pJPJ8wYicSGC8aRnk monisuo < /opt/monisuo/init.sql
|
||||
```
|
||||
|
||||
或者使用Navicat等工具连接数据库执行 `sql/init.sql`
|
||||
|
||||
---
|
||||
|
||||
### 第三步:打包前端APP
|
||||
|
||||
#### 方式一:使用HBuilderX(推荐)
|
||||
|
||||
1. **下载安装 HBuilderX Alpha版**
|
||||
- 下载地址:https://www.dcloud.io/hbuilderx.html
|
||||
- 选择 **Alpha版**(uni-app x需要Alpha版)
|
||||
|
||||
2. **导入项目**
|
||||
- 打开HBuilderX
|
||||
- 文件 → 导入 → 从本地目录导入
|
||||
- 选择 `d:\workspace\project\com-rattan-spccloud\app` 目录
|
||||
|
||||
3. **配置manifest.json**
|
||||
- 在HBuilderX中打开 `manifest.json`
|
||||
- 填写应用信息:
|
||||
- App名称:模拟所
|
||||
- App描述:虚拟货币模拟交易平台
|
||||
- 版本号:1.0.0
|
||||
|
||||
4. **运行调试(可选)**
|
||||
- 连接Android手机(开启USB调试)
|
||||
- 运行 → 运行到手机或模拟器 → 运行到Android App基座
|
||||
|
||||
5. **云端打包**
|
||||
- 发行 → 原生App-云打包
|
||||
- 选择平台:Android
|
||||
- 勾选"使用DCloud公用证书"(测试用)
|
||||
- 点击"打包"
|
||||
- 等待打包完成,下载APK
|
||||
|
||||
#### 方式二:本地打包
|
||||
|
||||
1. **生成本地打包资源**
|
||||
- 发行 → 原生App-本地打包 → 生成本地打包App资源
|
||||
|
||||
2. **使用Android Studio打包**
|
||||
- 打开Android Studio
|
||||
- 导入生成的项目
|
||||
- Build → Build Bundle(s) / APK(s) → Build APK(s)
|
||||
|
||||
---
|
||||
|
||||
### 第四步:安装APK到手机
|
||||
|
||||
#### 方式一:直接安装
|
||||
1. 将APK文件传到手机
|
||||
2. 点击APK文件安装
|
||||
3. 允许安装未知来源应用
|
||||
|
||||
#### 方式二:通过HBuilderX安装
|
||||
1. 手机连接电脑
|
||||
2. 运行 → 运行到手机或模拟器 → 运行到Android App基座
|
||||
3. 选择已连接的设备
|
||||
|
||||
---
|
||||
|
||||
## 🔧 常见问题
|
||||
|
||||
### 1. 网络请求失败
|
||||
- 检查服务器防火墙是否开放9010端口
|
||||
- 检查API地址是否正确(`app/api/request.uts`中的BASE_URL)
|
||||
- 确保手机和服务器网络连通
|
||||
|
||||
### 2. 安装失败
|
||||
- 开启手机"允许安装未知来源应用"
|
||||
- 卸载旧版本后再安装新版本
|
||||
|
||||
### 3. 登录失败
|
||||
- 检查数据库是否初始化成功
|
||||
- 检查后端服务是否正常运行
|
||||
- 查看后端日志:`tail -f /opt/monisuo/app.log`
|
||||
|
||||
---
|
||||
|
||||
## 📱 API地址配置
|
||||
|
||||
修改 `app/api/request.uts` 文件:
|
||||
|
||||
```typescript
|
||||
// 开发环境(本地测试)
|
||||
const BASE_URL: string = 'http://localhost:9010'
|
||||
|
||||
// 生产环境(服务器部署)
|
||||
const BASE_URL: string = 'http://8.155.172.147:9010'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速测试
|
||||
|
||||
如果暂时没有服务器,可以使用内网穿透工具:
|
||||
|
||||
1. **使用ngrok**
|
||||
```bash
|
||||
ngrok http 9010
|
||||
```
|
||||
|
||||
2. **修改API地址为ngrok提供的地址**
|
||||
```typescript
|
||||
const BASE_URL: string = 'https://xxxx.ngrok.io'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 预置账号
|
||||
|
||||
| 类型 | 账号 | 密码 |
|
||||
|-----|------|------|
|
||||
| 管理员 | admin | admin123 |
|
||||
| 管理员 | superadmin | admin123 |
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- [uni-app x 文档](https://doc.dcloud.net.cn/uni-app-x/)
|
||||
- [HBuilderX 下载](https://www.dcloud.io/hbuilderx.html)
|
||||
- [云打包说明](https://ask.dcloud.net.cn/article/37979)
|
||||
203
app/README.md
Normal file
203
app/README.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# 藤编企业移动端应用 (uni-app x)
|
||||
|
||||
基于 **uni-app x** 开发的跨平台移动端应用,**原生支持 Android、iOS、鸿蒙系统**。
|
||||
|
||||
## 🚀 技术栈
|
||||
|
||||
- **uni-app x** - 下一代跨平台框架
|
||||
- **Vue 3** - 前端框架
|
||||
- **UTS** - Uni Type Script(类 TypeScript)
|
||||
- **Pinia** - 状态管理
|
||||
- **Vite** - 构建工具
|
||||
- **Sass** - CSS 预处理器
|
||||
|
||||
## 📱 支持平台
|
||||
|
||||
| 平台 | 支持状态 | 打包格式 |
|
||||
|------|---------|---------|
|
||||
| **Android** | ✅ 原生支持 | APK |
|
||||
| **iOS** | ✅ 原生支持 | IPA |
|
||||
| **鸿蒙 (HarmonyOS)** | ✅ 原生支持 | HAP |
|
||||
| **H5** | ✅ 支持 | - |
|
||||
| **微信小程序** | ✅ 支持 | - |
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
app/
|
||||
├── api/ # API 接口封装 (UTS)
|
||||
│ ├── index.uts # 接口统一导出
|
||||
│ ├── request.uts # 请求封装
|
||||
│ └── user.uts # 用户相关接口
|
||||
├── components/ # 公共组件 (.uvue)
|
||||
├── pages/ # 页面 (.uvue)
|
||||
│ ├── index/ # 首页
|
||||
│ ├── login/ # 登录页
|
||||
│ └── mine/ # 我的页面
|
||||
├── static/ # 静态资源
|
||||
│ └── tabbar/ # 底部导航图标
|
||||
├── store/ # 状态管理 (Pinia)
|
||||
├── utils/ # 工具函数
|
||||
├── App.uvue # 应用入口
|
||||
├── main.uts # 入口文件
|
||||
├── manifest.json # 应用配置(含鸿蒙配置)
|
||||
├── pages.json # 页面路由配置
|
||||
├── uni.scss # 全局样式变量
|
||||
├── vite.config.ts # Vite 配置
|
||||
├── index.html # H5 入口页面
|
||||
└── package.json # 依赖配置
|
||||
```
|
||||
|
||||
## 🛠️ 开发环境
|
||||
|
||||
### 环境要求
|
||||
|
||||
- **Node.js** >= 18.0
|
||||
- **HBuilderX** Alpha 版本(用于打包)
|
||||
- **Android Studio**(Android 打包)
|
||||
- **Xcode**(iOS 打包,仅 macOS)
|
||||
- **DevEco Studio**(鸿蒙打包)
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
cd app
|
||||
npm install
|
||||
```
|
||||
|
||||
### 开发运行
|
||||
|
||||
```bash
|
||||
# H5 开发
|
||||
npm run dev:h5
|
||||
|
||||
# Android 开发
|
||||
npm run dev:app-android
|
||||
|
||||
# iOS 开发
|
||||
npm run dev:app-ios
|
||||
|
||||
# 鸿蒙开发
|
||||
npm run dev:app-harmony
|
||||
```
|
||||
|
||||
### 构建打包
|
||||
|
||||
```bash
|
||||
# H5 构建
|
||||
npm run build:h5
|
||||
|
||||
# Android APK 构建
|
||||
npm run build:app-android
|
||||
|
||||
# iOS IPA 构建
|
||||
npm run build:app-ios
|
||||
|
||||
# 鸿蒙 HAP 构建
|
||||
npm run build:app-harmony
|
||||
```
|
||||
|
||||
## 📦 使用 HBuilderX 打包
|
||||
|
||||
### 1. 安装 HBuilderX Alpha
|
||||
|
||||
下载地址:https://www.dcloud.io/hbuilderx.html
|
||||
|
||||
> **注意:uni-app x 需要使用 Alpha 版本的 HBuilderX**
|
||||
|
||||
### 2. 导入项目
|
||||
|
||||
1. 打开 HBuilderX Alpha
|
||||
2. 选择「文件」->「导入」->「从本地目录导入」
|
||||
3. 选择 `app` 目录
|
||||
|
||||
### 3. 运行调试
|
||||
|
||||
- **Android**: 连接手机,选择「运行」->「运行到手机或模拟器」
|
||||
- **iOS**: 连接 iPhone,选择「运行」->「运行到手机或模拟器」
|
||||
- **鸿蒙**: 连接鸿蒙设备,选择「运行」->「运行到手机或模拟器」
|
||||
|
||||
### 4. 云端打包
|
||||
|
||||
1. 选择「发行」->「原生App-云打包」
|
||||
2. 选择打包平台(Android/iOS/鸿蒙)
|
||||
3. 填写证书信息
|
||||
4. 点击「打包」
|
||||
|
||||
### 5. 本地打包
|
||||
|
||||
#### Android
|
||||
1. 选择「发行」->「原生App-本地打包」->「生成本地打包App资源」
|
||||
2. 使用 Android Studio 打开生成的项目
|
||||
3. 构建 APK
|
||||
|
||||
#### iOS
|
||||
1. 选择「发行」->「原生App-本地打包」->「生成本地打包App资源」
|
||||
2. 使用 Xcode 打开生成的项目
|
||||
3. 构建 IPA
|
||||
|
||||
#### 鸿蒙
|
||||
1. 选择「发行」->「原生App-本地打包」->「生成本地打包App资源」
|
||||
2. 使用 DevEco Studio 打开生成的项目
|
||||
3. 构建 HAP
|
||||
|
||||
## 🔧 API 配置
|
||||
|
||||
修改 `api/request.uts` 中的 `BASE_URL`:
|
||||
|
||||
```typescript
|
||||
const BASE_URL: string = 'http://your-server:9010'
|
||||
```
|
||||
|
||||
## 🆕 UTS 语法说明
|
||||
|
||||
UTS (Uni Type Script) 是 uni-app x 的开发语言,语法类似 TypeScript:
|
||||
|
||||
```typescript
|
||||
// 变量声明
|
||||
const name: string = '藤编企业'
|
||||
const count: number = 100
|
||||
|
||||
// 函数定义
|
||||
func add(a: number, b: number): number {
|
||||
return a + b
|
||||
}
|
||||
|
||||
// 类型定义
|
||||
type User = {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const userName = ref('')
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const fullName = computed((): string => {
|
||||
return `${firstName.value} ${lastName.value}`
|
||||
})
|
||||
```
|
||||
|
||||
## 📝 开发注意事项
|
||||
|
||||
1. **文件后缀**:
|
||||
- 页面文件使用 `.uvue`
|
||||
- 脚本文件使用 `.uts`
|
||||
|
||||
2. **静态资源**:请替换 `static/` 目录下的占位图片
|
||||
|
||||
3. **鸿蒙开发**:需要安装 DevEco Studio 和鸿蒙 SDK
|
||||
|
||||
4. **调试**:推荐使用真机调试,模拟器可能有性能差异
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- [uni-app x 官方文档](https://doc.dcloud.net.cn/uni-app-x/)
|
||||
- [UTS 语法指南](https://doc.dcloud.net.cn/uni-app-x/uts/)
|
||||
- [HBuilderX Alpha 下载](https://www.dcloud.io/hbuilderx.html)
|
||||
- [鸿蒙开发者中心](https://developer.harmonyos.com/)
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License
|
||||
43
app/api/asset.uts
Normal file
43
app/api/asset.uts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 资产API
|
||||
*/
|
||||
import { get, post } from './request.uts'
|
||||
|
||||
/**
|
||||
* 获取资产总览
|
||||
*/
|
||||
export func getAssetOverview (): Promise<any> {
|
||||
return get('/api/asset/overview', null)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资金账户
|
||||
*/
|
||||
export func getFundAccount (): Promise<any> {
|
||||
return get('/api/asset/fund', null)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取交易账户
|
||||
*/
|
||||
export func getTradeAccounts (): Promise<any> {
|
||||
return get('/api/asset/trade', null)
|
||||
}
|
||||
|
||||
/**
|
||||
* 资金划转
|
||||
*/
|
||||
export func transfer (direction: number, amount: string): Promise<any> {
|
||||
return post('/api/asset/transfer', { direction, amount } as UTSJSONObject)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资金流水
|
||||
*/
|
||||
export func getFlows (flowType: number | null, pageNum: number, pageSize: number): Promise<any> {
|
||||
const params: UTSJSONObject = { pageNum: pageNum, pageSize: pageSize }
|
||||
if (flowType !== null) {
|
||||
params['flowType'] = flowType
|
||||
}
|
||||
return get('/api/asset/flow', params)
|
||||
}
|
||||
36
app/api/fund.uts
Normal file
36
app/api/fund.uts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 充提API
|
||||
*/
|
||||
import { get, post } from './request.uts'
|
||||
|
||||
/**
|
||||
* 申请充值
|
||||
*/
|
||||
export func deposit (amount: string, remark: string | null): Promise<any> {
|
||||
return post('/api/fund/deposit', { amount, remark } as UTSJSONObject)
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请提现
|
||||
*/
|
||||
export func withdraw (amount: string, remark: string | null): Promise<any> {
|
||||
return post('/api/fund/withdraw', { amount, remark } as UTSJSONObject)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
*/
|
||||
export func cancelOrder (orderNo: string): Promise<any> {
|
||||
return post('/api/fund/cancel', { orderNo } as UTSJSONObject)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取充提记录
|
||||
*/
|
||||
export func getOrders (type: number | null, pageNum: number, pageSize: number): Promise<any> {
|
||||
const params: UTSJSONObject = { pageNum: pageNum, pageSize: pageSize }
|
||||
if (type !== null) {
|
||||
params['type'] = type
|
||||
}
|
||||
return get('/api/fund/orders', params)
|
||||
}
|
||||
9
app/api/index.uts
Normal file
9
app/api/index.uts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* API 统一导出
|
||||
*/
|
||||
export * from './request.uts'
|
||||
export * from './user.uts'
|
||||
export * from './market.uts'
|
||||
export * from './asset.uts'
|
||||
export * from './trade.uts'
|
||||
export * from './fund.uts'
|
||||
25
app/api/market.uts
Normal file
25
app/api/market.uts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 行情API
|
||||
*/
|
||||
import { get } from './request.uts'
|
||||
|
||||
/**
|
||||
* 获取币种列表
|
||||
*/
|
||||
export func getCoinList (): Promise<any> {
|
||||
return get('/api/market/list', null)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取币种详情
|
||||
*/
|
||||
export func getCoinDetail (code: string): Promise<any> {
|
||||
return get('/api/market/detail', { code } as UTSJSONObject)
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索币种
|
||||
*/
|
||||
export func searchCoins (keyword: string): Promise<any> {
|
||||
return get('/api/market/search', { keyword } as UTSJSONObject)
|
||||
}
|
||||
130
app/api/request.uts
Normal file
130
app/api/request.uts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 网络请求封装 - 模拟所APP
|
||||
*/
|
||||
|
||||
// API 基础地址(生产环境服务器地址)
|
||||
const BASE_URL: string = 'http://8.155.172.147:5010'
|
||||
|
||||
// 请求超时时间
|
||||
const TIMEOUT: number = 30000
|
||||
|
||||
// 响应数据类型
|
||||
type ResponseData = {
|
||||
code: string
|
||||
msg: string
|
||||
data: any
|
||||
}
|
||||
|
||||
// 请求配置类型
|
||||
type RequestOptions = {
|
||||
url: string
|
||||
method?: string
|
||||
data?: UTSJSONObject | null
|
||||
header?: UTSJSONObject | null
|
||||
timeout?: number
|
||||
loading?: boolean
|
||||
loadingText?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求拦截器
|
||||
*/
|
||||
func requestInterceptor (config: RequestOptions): RequestOptions {
|
||||
const token = uni.getStorageSync('token') as string
|
||||
if (token !== null && token !== '') {
|
||||
config.header = {
|
||||
...config.header,
|
||||
'Authorization': `Bearer ${token}`
|
||||
} as UTSJSONObject
|
||||
}
|
||||
|
||||
config.header = {
|
||||
'Content-Type': 'application/json',
|
||||
...config.header
|
||||
} as UTSJSONObject
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应拦截器
|
||||
*/
|
||||
func responseInterceptor (response: UniRequestSuccessCallbackResult): Promise<ResponseData> {
|
||||
const statusCode: number = response.statusCode
|
||||
const data = response.data as ResponseData
|
||||
|
||||
if (statusCode === 200) {
|
||||
if (data.code === '0000') {
|
||||
return Promise.resolve(data)
|
||||
} else if (data.code === '0002') {
|
||||
uni.removeStorageSync('token')
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
return Promise.reject(new Error(data.msg || '请重新登录'))
|
||||
} else {
|
||||
uni.showToast({ title: data.msg || '请求失败', icon: 'none', duration: 2000 })
|
||||
return Promise.reject(new Error(data.msg))
|
||||
}
|
||||
} else if (statusCode === 401) {
|
||||
uni.removeStorageSync('token')
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
return Promise.reject(new Error('未授权'))
|
||||
} else {
|
||||
return Promise.reject(new Error(`网络错误: ${statusCode}`))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用请求方法
|
||||
*/
|
||||
func request (options: RequestOptions): Promise<ResponseData> {
|
||||
let config: RequestOptions = {
|
||||
url: options.url.startsWith('http') ? options.url : BASE_URL + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || null,
|
||||
header: options.header || null,
|
||||
timeout: options.timeout || TIMEOUT
|
||||
}
|
||||
|
||||
config = requestInterceptor(config)
|
||||
|
||||
const showLoading = options.loading !== false
|
||||
if (showLoading) {
|
||||
uni.showLoading({ title: options.loadingText || '加载中...', mask: true })
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: config.url,
|
||||
method: config.method as UniRequestMethod,
|
||||
data: config.data,
|
||||
header: config.header,
|
||||
timeout: config.timeout,
|
||||
success: (response: UniRequestSuccessCallbackResult) => {
|
||||
responseInterceptor(response).then(resolve).catch(reject)
|
||||
},
|
||||
fail: (error: UniRequestFailCallbackResult) => {
|
||||
uni.showToast({ title: error.errMsg || '网络请求失败', icon: 'none', duration: 2000 })
|
||||
reject(new Error(error.errMsg))
|
||||
},
|
||||
complete: () => {
|
||||
if (showLoading) uni.hideLoading()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求
|
||||
*/
|
||||
export func get (url: string, params: UTSJSONObject | null = null): Promise<ResponseData> {
|
||||
return request({ url, method: 'GET', data: params })
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求
|
||||
*/
|
||||
export func post (url: string, data: UTSJSONObject | null = null): Promise<ResponseData> {
|
||||
return request({ url, method: 'POST', data: data })
|
||||
}
|
||||
|
||||
export const config = { BASE_URL, TIMEOUT }
|
||||
39
app/api/trade.uts
Normal file
39
app/api/trade.uts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 交易API
|
||||
*/
|
||||
import { get, post } from './request.uts'
|
||||
|
||||
/**
|
||||
* 买入
|
||||
*/
|
||||
export func buy (coinCode: string, price: string, quantity: string): Promise<any> {
|
||||
return post('/api/trade/buy', { coinCode, price, quantity } as UTSJSONObject)
|
||||
}
|
||||
|
||||
/**
|
||||
* 卖出
|
||||
*/
|
||||
export func sell (coinCode: string, price: string, quantity: string): Promise<any> {
|
||||
return post('/api/trade/sell', { coinCode, price, quantity } as UTSJSONObject)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取交易记录
|
||||
*/
|
||||
export func getOrders (coinCode: string | null, direction: number | null, pageNum: number, pageSize: number): Promise<any> {
|
||||
const params: UTSJSONObject = { pageNum: pageNum, pageSize: pageSize }
|
||||
if (coinCode !== null) {
|
||||
params['coinCode'] = coinCode
|
||||
}
|
||||
if (direction !== null) {
|
||||
params['direction'] = direction
|
||||
}
|
||||
return get('/api/trade/orders', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单详情
|
||||
*/
|
||||
export func getOrderDetail (orderNo: string): Promise<any> {
|
||||
return get('/api/trade/order/detail', { orderNo } as UTSJSONObject)
|
||||
}
|
||||
39
app/api/user.uts
Normal file
39
app/api/user.uts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 用户API
|
||||
*/
|
||||
import { get, post } from './request.uts'
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
export func login (username: string, password: string): Promise<any> {
|
||||
return post('/api/user/login', { username, password } as UTSJSONObject)
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
export func register (username: string, password: string): Promise<any> {
|
||||
return post('/api/user/register', { username, password } as UTSJSONObject)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export func getUserInfo (): Promise<any> {
|
||||
return get('/api/user/info', null)
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传KYC资料
|
||||
*/
|
||||
export func uploadKyc (idCardFront: string, idCardBack: string): Promise<any> {
|
||||
return post('/api/user/kyc', { idCardFront, idCardBack } as UTSJSONObject)
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
export func logout (): Promise<any> {
|
||||
return post('/api/user/logout', null)
|
||||
}
|
||||
21
app/index.html
Normal file
21
app/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
<meta name="theme-color" content="#4A90D9" />
|
||||
<title>藤编企业</title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!--app-html-->
|
||||
</div>
|
||||
<script type="module" src="/main.uts"></script>
|
||||
</body>
|
||||
</html>
|
||||
15
app/main.uts
Normal file
15
app/main.uts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createSSRApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
export function createApp(): UTSJSONObject {
|
||||
const app = createSSRApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
|
||||
return {
|
||||
app,
|
||||
pinia
|
||||
}
|
||||
}
|
||||
52
app/manifest.json
Normal file
52
app/manifest.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "模拟所",
|
||||
"appid": "__UNI__MONISUO01",
|
||||
"description": "虚拟货币模拟交易平台,支持Android、iOS、鸿蒙系统",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
"transformPx": false,
|
||||
"app-plus": {
|
||||
"usingComponents": true,
|
||||
"nvueStyleCompiler": "uni-app",
|
||||
"compilerVersion": 3,
|
||||
"splashscreen": {
|
||||
"alwaysShowBeforeRender": true,
|
||||
"waiting": true,
|
||||
"autoclose": true,
|
||||
"delay": 0
|
||||
},
|
||||
"distribute": {
|
||||
"android": {
|
||||
"permissions": [
|
||||
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>"
|
||||
],
|
||||
"minSdkVersion": 21,
|
||||
"targetSdkVersion": 34
|
||||
},
|
||||
"ios": {
|
||||
"dSYMs": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"quickapp": {},
|
||||
"mp-weixin": {
|
||||
"appid": "",
|
||||
"setting": { "urlCheck": false },
|
||||
"usingComponents": true
|
||||
},
|
||||
"h5": {
|
||||
"title": "模拟所",
|
||||
"router": { "mode": "hash", "base": "./" }
|
||||
},
|
||||
"uni-app-x": {},
|
||||
"app-harmony": {
|
||||
"minSDKVersion": 11,
|
||||
"targetSDKVersion": 12,
|
||||
"compileSDKVersion": 12,
|
||||
"package": "com.monisuo.app",
|
||||
"projectName": "MonisuoApp"
|
||||
},
|
||||
"vueVersion": "3"
|
||||
}
|
||||
39
app/package.json
Normal file
39
app/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "rattan-app",
|
||||
"version": "1.0.0",
|
||||
"description": "藤编企业移动端应用 - 支持Android、iOS、鸿蒙",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev:app": "uni -p app",
|
||||
"dev:app-android": "uni -p app-android",
|
||||
"dev:app-ios": "uni -p app-ios",
|
||||
"dev:app-harmony": "uni -p app-harmony",
|
||||
"dev:h5": "uni",
|
||||
"dev:mp-weixin": "uni -p mp-weixin",
|
||||
"build:app": "uni build -p app",
|
||||
"build:app-android": "uni build -p app-android",
|
||||
"build:app-ios": "uni build -p app-ios",
|
||||
"build:app-harmony": "uni build -p app-harmony",
|
||||
"build:h5": "uni build",
|
||||
"build:mp-weixin": "uni build -p mp-weixin"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-4020920250116001",
|
||||
"@dcloudio/uni-app-harmony": "3.0.0-4020920250116001",
|
||||
"@dcloudio/uni-app-plus": "3.0.0-4020920250116001",
|
||||
"@dcloudio/uni-components": "3.0.0-4020920250116001",
|
||||
"@dcloudio/uni-h5": "3.0.0-4020920250116001",
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-4020920250116001",
|
||||
"vue": "^3.5.13",
|
||||
"pinia": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/types": "^3.4.14",
|
||||
"@dcloudio/uni-automator": "3.0.0-4020920250116001",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-4020920250116001",
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-4020920250116001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-4020920250116001",
|
||||
"sass": "^1.83.0",
|
||||
"vite": "^6.0.6"
|
||||
}
|
||||
}
|
||||
106
app/pages.json
Normal file
106
app/pages.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "模拟所",
|
||||
"navigationBarBackgroundColor": "#1A1A2E",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/market/market",
|
||||
"style": {
|
||||
"navigationBarTitleText": "行情",
|
||||
"navigationBarBackgroundColor": "#1A1A2E",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/trade/trade",
|
||||
"style": {
|
||||
"navigationBarTitleText": "交易",
|
||||
"navigationBarBackgroundColor": "#1A1A2E",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/asset/asset",
|
||||
"style": {
|
||||
"navigationBarTitleText": "资产",
|
||||
"navigationBarBackgroundColor": "#1A1A2E",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/mine",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的",
|
||||
"navigationBarBackgroundColor": "#1A1A2E",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录",
|
||||
"navigationBarBackgroundColor": "#1A1A2E",
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/register/register",
|
||||
"style": {
|
||||
"navigationBarTitleText": "注册",
|
||||
"navigationBarBackgroundColor": "#1A1A2E",
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "模拟所",
|
||||
"navigationBarBackgroundColor": "#1A1A2E",
|
||||
"backgroundColor": "#1A1A2E"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#666666",
|
||||
"selectedColor": "#00D4AA",
|
||||
"borderStyle": "black",
|
||||
"backgroundColor": "#16213E",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页",
|
||||
"iconPath": "static/tabbar/home.png",
|
||||
"selectedIconPath": "static/tabbar/home-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/market/market",
|
||||
"text": "行情",
|
||||
"iconPath": "static/tabbar/market.png",
|
||||
"selectedIconPath": "static/tabbar/market-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/trade/trade",
|
||||
"text": "交易",
|
||||
"iconPath": "static/tabbar/trade.png",
|
||||
"selectedIconPath": "static/tabbar/trade-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/asset/asset",
|
||||
"text": "资产",
|
||||
"iconPath": "static/tabbar/asset.png",
|
||||
"selectedIconPath": "static/tabbar/asset-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/mine/mine",
|
||||
"text": "我的",
|
||||
"iconPath": "static/tabbar/mine.png",
|
||||
"selectedIconPath": "static/tabbar/mine-active.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
448
app/pages/asset/asset.uvue
Normal file
448
app/pages/asset/asset.uvue
Normal file
@@ -0,0 +1,448 @@
|
||||
<template>
|
||||
<view class="asset-container">
|
||||
<!-- 总资产卡片 -->
|
||||
<view class="total-card">
|
||||
<text class="total-label">总资产估值(USDT)</text>
|
||||
<text class="total-value">${{ totalAssets }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 账户切换 -->
|
||||
<view class="account-tabs">
|
||||
<text :class="['tab', activeAccount === 'fund' ? 'active' : '']" @click="activeAccount = 'fund'">资金账户</text>
|
||||
<text :class="['tab', activeAccount === 'trade' ? 'active' : '']" @click="activeAccount = 'trade'">交易账户</text>
|
||||
</view>
|
||||
|
||||
<!-- 资金账户 -->
|
||||
<view v-if="activeAccount === 'fund'" class="account-section">
|
||||
<view class="balance-card">
|
||||
<text class="balance-label">USDT余额</text>
|
||||
<text class="balance-value">{{ fundBalance }}</text>
|
||||
</view>
|
||||
<view class="action-btns">
|
||||
<button class="action-btn deposit" @click="showDeposit = true">
|
||||
<text class="btn-text">充值</text>
|
||||
</button>
|
||||
<button class="action-btn withdraw" @click="showWithdraw = true">
|
||||
<text class="btn-text">提现</text>
|
||||
</button>
|
||||
<button class="action-btn transfer" @click="showTransfer = true">
|
||||
<text class="btn-text">划转</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 交易账户 -->
|
||||
<view v-if="activeAccount === 'trade'" class="trade-section">
|
||||
<view class="position-list">
|
||||
<view class="position-item" v-for="(pos, index) in positions" :key="index">
|
||||
<view class="pos-left">
|
||||
<text class="pos-icon">{{ pos.icon }}</text>
|
||||
<view class="pos-info">
|
||||
<text class="pos-code">{{ pos.coinCode }}</text>
|
||||
<text class="pos-name">{{ pos.coinName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pos-right">
|
||||
<text class="pos-quantity">{{ pos.quantity }}</text>
|
||||
<text class="pos-value">≈ ${{ pos.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 充值弹窗 -->
|
||||
<view v-if="showDeposit" class="modal-mask" @click="showDeposit = false">
|
||||
<view class="modal-content" @click.stop="">
|
||||
<text class="modal-title">充值</text>
|
||||
<view class="modal-form">
|
||||
<text class="form-label">充值金额(USDT)</text>
|
||||
<input class="form-input" type="digit" v-model="depositAmount" placeholder="请输入金额" />
|
||||
</view>
|
||||
<view class="modal-btns">
|
||||
<button class="modal-btn cancel" @click="showDeposit = false"><text class="btn-text">取消</text></button>
|
||||
<button class="modal-btn confirm" @click="handleDeposit"><text class="btn-text">确认</text></button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提现弹窗 -->
|
||||
<view v-if="showWithdraw" class="modal-mask" @click="showWithdraw = false">
|
||||
<view class="modal-content" @click.stop="">
|
||||
<text class="modal-title">提现</text>
|
||||
<view class="modal-form">
|
||||
<text class="form-label">提现金额(USDT)</text>
|
||||
<input class="form-input" type="digit" v-model="withdrawAmount" placeholder="请输入金额" />
|
||||
</view>
|
||||
<view class="modal-btns">
|
||||
<button class="modal-btn cancel" @click="showWithdraw = false"><text class="btn-text">取消</text></button>
|
||||
<button class="modal-btn confirm" @click="handleWithdraw"><text class="btn-text">确认</text></button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 划转弹窗 -->
|
||||
<view v-if="showTransfer" class="modal-mask" @click="showTransfer = false">
|
||||
<view class="modal-content" @click.stop="">
|
||||
<text class="modal-title">资金划转</text>
|
||||
<view class="transfer-direction">
|
||||
<text :class="['dir-item', transferDir === 1 ? 'active' : '']" @click="transferDir = 1">资金→交易</text>
|
||||
<text :class="['dir-item', transferDir === 2 ? 'active' : '']" @click="transferDir = 2">交易→资金</text>
|
||||
</view>
|
||||
<view class="modal-form">
|
||||
<text class="form-label">划转金额(USDT)</text>
|
||||
<input class="form-input" type="digit" v-model="transferAmount" placeholder="请输入金额" />
|
||||
</view>
|
||||
<view class="modal-btns">
|
||||
<button class="modal-btn cancel" @click="showTransfer = false"><text class="btn-text">取消</text></button>
|
||||
<button class="modal-btn confirm" @click="handleTransfer"><text class="btn-text">确认</text></button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { getOverview, getTradeAccount, transfer } from '@/api/asset.uts'
|
||||
import { deposit, withdraw } from '@/api/fund.uts'
|
||||
|
||||
type PositionItem = {
|
||||
coinCode: string
|
||||
coinName: string
|
||||
quantity: string
|
||||
value: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
const totalAssets = ref('0.00')
|
||||
const fundBalance = ref('0.00')
|
||||
const positions = ref<PositionItem[]>([])
|
||||
const activeAccount = ref('fund')
|
||||
|
||||
const showDeposit = ref(false)
|
||||
const showWithdraw = ref(false)
|
||||
const showTransfer = ref(false)
|
||||
const depositAmount = ref('')
|
||||
const withdrawAmount = ref('')
|
||||
const transferAmount = ref('')
|
||||
const transferDir = ref(1)
|
||||
|
||||
onShow(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
func loadData () {
|
||||
getOverview()
|
||||
.then((res) => {
|
||||
const data = res.data as UTSJSONObject
|
||||
totalAssets.value = formatAmount(data['totalAssets'] as number)
|
||||
fundBalance.value = formatAmount(data['fundBalance'] as number)
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
getTradeAccount()
|
||||
.then((res) => {
|
||||
const data = res.data as UTSJSONObject
|
||||
const list = data['positions'] as Array<UTSJSONObject>
|
||||
const items: PositionItem[] = []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i]
|
||||
items.push({
|
||||
coinCode: item['coinCode'] as string,
|
||||
coinName: item['coinName'] as string,
|
||||
quantity: (item['quantity'] as number).toFixed(4),
|
||||
value: formatAmount(item['value'] as number),
|
||||
icon: getCoinIcon(item['coinCode'] as string)
|
||||
})
|
||||
}
|
||||
positions.value = items
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
func handleDeposit () {
|
||||
if (depositAmount.value === '' || parseFloat(depositAmount.value) <= 0) {
|
||||
uni.showToast({ title: '请输入有效金额', icon: 'none' })
|
||||
return
|
||||
}
|
||||
deposit(depositAmount.value, null)
|
||||
.then(() => {
|
||||
uni.showToast({ title: '申请成功,等待审批', icon: 'success' })
|
||||
showDeposit.value = false
|
||||
depositAmount.value = ''
|
||||
loadData()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
func handleWithdraw () {
|
||||
if (withdrawAmount.value === '' || parseFloat(withdrawAmount.value) <= 0) {
|
||||
uni.showToast({ title: '请输入有效金额', icon: 'none' })
|
||||
return
|
||||
}
|
||||
withdraw(withdrawAmount.value, null)
|
||||
.then(() => {
|
||||
uni.showToast({ title: '申请成功,等待审批', icon: 'success' })
|
||||
showWithdraw.value = false
|
||||
withdrawAmount.value = ''
|
||||
loadData()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
func handleTransfer () {
|
||||
if (transferAmount.value === '' || parseFloat(transferAmount.value) <= 0) {
|
||||
uni.showToast({ title: '请输入有效金额', icon: 'none' })
|
||||
return
|
||||
}
|
||||
transfer(transferDir.value, transferAmount.value)
|
||||
.then(() => {
|
||||
uni.showToast({ title: '划转成功', icon: 'success' })
|
||||
showTransfer.value = false
|
||||
transferAmount.value = ''
|
||||
loadData()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
func formatAmount (value: number): string {
|
||||
return value.toFixed(2)
|
||||
}
|
||||
|
||||
func getCoinIcon (code: string): string {
|
||||
const icons: Map<string, string> = new Map()
|
||||
icons.set('BTC', '₿')
|
||||
icons.set('ETH', 'Ξ')
|
||||
icons.set('SOL', '◎')
|
||||
icons.set('USDT', '₮')
|
||||
return icons.get(code) || '●'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.asset-container {
|
||||
min-height: 100vh;
|
||||
background: #1A1A2E;
|
||||
padding: 24rpx;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.total-card {
|
||||
background: linear-gradient(135deg, #00D4AA 0%, #00B894 100%);
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.total-value {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.account-tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background: #16213E;
|
||||
border-radius: 16rpx;
|
||||
padding: 8rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 16rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: rgba(0, 212, 170, 0.2);
|
||||
color: #00D4AA;
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
background: #16213E;
|
||||
border-radius: 20rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #00D4AA;
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-btn.deposit { background: #00C853; }
|
||||
.action-btn.withdraw { background: #FF5252; }
|
||||
.action-btn.transfer { background: #00D4AA; }
|
||||
|
||||
.btn-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.position-list {
|
||||
background: #16213E;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.position-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.position-item:last-child { border-bottom: none; }
|
||||
|
||||
.pos-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pos-icon {
|
||||
font-size: 32rpx;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.pos-info { display: flex; flex-direction: column; }
|
||||
.pos-code { font-size: 28rpx; font-weight: bold; color: #fff; }
|
||||
.pos-name { font-size: 22rpx; color: rgba(255, 255, 255, 0.5); }
|
||||
|
||||
.pos-right { display: flex; flex-direction: column; align-items: flex-end; }
|
||||
.pos-quantity { font-size: 28rpx; color: #fff; }
|
||||
.pos-value { font-size: 22rpx; color: rgba(255, 255, 255, 0.5); }
|
||||
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 600rpx;
|
||||
background: #16213E;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.modal-form {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.transfer-direction {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12rpx;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
.dir-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 16rpx 0;
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.dir-item.active {
|
||||
background: #00D4AA;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-btns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-btn.cancel { background: rgba(255, 255, 255, 0.1); }
|
||||
.modal-btn.confirm { background: #00D4AA; }
|
||||
</style>
|
||||
305
app/pages/index/index.uvue
Normal file
305
app/pages/index/index.uvue
Normal file
@@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<view class="index-container">
|
||||
<!-- 资产概览卡片 -->
|
||||
<view class="asset-card">
|
||||
<text class="asset-label">总资产(USDT)</text>
|
||||
<text class="asset-value">{{ totalAsset }}</text>
|
||||
<view class="asset-row">
|
||||
<view class="asset-item">
|
||||
<text class="asset-item-label">资金账户</text>
|
||||
<text class="asset-item-value">{{ fundBalance }}</text>
|
||||
</view>
|
||||
<view class="asset-item">
|
||||
<text class="asset-item-label">交易账户</text>
|
||||
<text class="asset-item-value">{{ tradeBalance }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<view class="quick-actions">
|
||||
<view class="action-item" @click="goToRecharge">
|
||||
<text class="action-icon">充</text>
|
||||
<text class="action-text">充值</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToWithdraw">
|
||||
<text class="action-icon">提</text>
|
||||
<text class="action-text">提现</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToTransfer">
|
||||
<text class="action-icon">转</text>
|
||||
<text class="action-text">划转</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goToTrade">
|
||||
<text class="action-icon">币</text>
|
||||
<text class="action-text">交易</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 持仓列表 -->
|
||||
<view class="holding-section">
|
||||
<text class="section-title">我的持仓</text>
|
||||
<view class="holding-list">
|
||||
<view class="holding-item" v-for="(item, index) in holdings" :key="index">
|
||||
<view class="holding-info">
|
||||
<text class="coin-code">{{ item.coinCode }}</text>
|
||||
<text class="coin-quantity">{{ item.quantity }}</text>
|
||||
</view>
|
||||
<view class="holding-value">
|
||||
<text class="value-text">{{ item.value }} USDT</text>
|
||||
<text :class="['profit-text', item.profit >= 0 ? 'up' : 'down']">
|
||||
{{ item.profit >= 0 ? '+' : '' }}{{ item.profit }}%
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-hint" v-if="holdings.length === 0">
|
||||
<text class="empty-text">暂无持仓</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { getAssetOverview, getTradeAccounts } from '@/api/asset.uts'
|
||||
|
||||
type HoldingItem = {
|
||||
coinCode: string
|
||||
quantity: string
|
||||
value: string
|
||||
profit: number
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
totalAsset: '0.00' as string,
|
||||
fundBalance: '0.00' as string,
|
||||
tradeBalance: '0.00' as string,
|
||||
holdings: [] as HoldingItem[]
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadAssetData()
|
||||
},
|
||||
methods: {
|
||||
async loadAssetData() {
|
||||
try {
|
||||
const res = await getAssetOverview()
|
||||
const data = res.data
|
||||
this.totalAsset = this.formatAmount(data['totalAsset'] as number)
|
||||
this.fundBalance = this.formatAmount(data['fundBalance'] as number)
|
||||
this.tradeBalance = this.formatAmount(data['tradeBalance'] as number)
|
||||
|
||||
// 加载持仓
|
||||
const tradeRes = await getTradeAccounts()
|
||||
const list = tradeRes.data['list'] as any[]
|
||||
this.holdings = []
|
||||
if (list !== null && list.length > 0) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i]
|
||||
const qty = item['quantity'] as number
|
||||
if (qty > 0) {
|
||||
this.holdings.push({
|
||||
coinCode: item['coinCode'] as string,
|
||||
quantity: qty.toFixed(6),
|
||||
value: (item['totalValue'] as number).toFixed(2),
|
||||
profit: item['profitRate'] as number
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载资产数据失败', e)
|
||||
}
|
||||
},
|
||||
formatAmount(value: number): string {
|
||||
if (value === null || value === undefined) {
|
||||
return '0.00'
|
||||
}
|
||||
return value.toFixed(2)
|
||||
},
|
||||
goToRecharge() {
|
||||
uni.navigateTo({ url: '/pages/asset/asset?action=deposit' })
|
||||
},
|
||||
goToWithdraw() {
|
||||
uni.navigateTo({ url: '/pages/asset/asset?action=withdraw' })
|
||||
},
|
||||
goToTransfer() {
|
||||
uni.navigateTo({ url: '/pages/asset/asset?action=transfer' })
|
||||
},
|
||||
goToTrade() {
|
||||
uni.switchTab({ url: '/pages/trade/trade' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.index-container {
|
||||
min-height: 100vh;
|
||||
background: $bg-color-dark;
|
||||
padding: $spacing-base;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.asset-card {
|
||||
background: linear-gradient(135deg, $primary-color 0%, $primary-color-dark 100%);
|
||||
border-radius: $border-radius-lg;
|
||||
padding: $spacing-lg;
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
|
||||
.asset-label {
|
||||
font-size: $font-size-base;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.asset-value {
|
||||
font-size: 56rpx;
|
||||
font-weight: bold;
|
||||
color: #FFFFFF;
|
||||
display: block;
|
||||
margin-bottom: $spacing-base;
|
||||
}
|
||||
|
||||
.asset-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.asset-item-label {
|
||||
font-size: $font-size-sm;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.asset-item-value {
|
||||
font-size: $font-size-lg;
|
||||
color: #FFFFFF;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background: $bg-color-card;
|
||||
border-radius: $border-radius-lg;
|
||||
padding: $spacing-base;
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: rgba(0, 212, 170, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: $font-size-lg;
|
||||
color: $primary-color;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.holding-section {
|
||||
background: $bg-color-card;
|
||||
border-radius: $border-radius-lg;
|
||||
padding: $spacing-base;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: $font-size-lg;
|
||||
color: $text-color;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: $spacing-base;
|
||||
}
|
||||
|
||||
.holding-list {
|
||||
min-height: 100rpx;
|
||||
}
|
||||
|
||||
.holding-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $spacing-base 0;
|
||||
border-bottom: 1rpx solid $border-color;
|
||||
}
|
||||
|
||||
.holding-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.holding-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.coin-code {
|
||||
font-size: $font-size-lg;
|
||||
color: $text-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.coin-quantity {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.holding-value {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.value-text {
|
||||
font-size: $font-size-base;
|
||||
color: $text-color;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profit-text {
|
||||
font-size: $font-size-sm;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.profit-text.up {
|
||||
color: $up-color;
|
||||
}
|
||||
|
||||
.profit-text.down {
|
||||
color: $down-color;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: $font-size-base;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
</style>
|
||||
169
app/pages/login/login.uvue
Normal file
169
app/pages/login/login.uvue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<view class="login-container">
|
||||
<view class="login-header">
|
||||
<text class="app-title">模拟所</text>
|
||||
<text class="app-subtitle">虚拟货币模拟交易平台</text>
|
||||
</view>
|
||||
|
||||
<view class="login-form">
|
||||
<view class="form-item">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="username"
|
||||
placeholder="请输入用户名"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<input
|
||||
class="input"
|
||||
type="password"
|
||||
v-model="password"
|
||||
placeholder="请输入密码"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<button class="login-btn" @click="handleLogin">登录</button>
|
||||
|
||||
<view class="register-link">
|
||||
<text class="link-text" @click="goToRegister">还没有账号?立即注册</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { post } from '@/api/request.uts'
|
||||
import { login } from '@/api/user.uts'
|
||||
|
||||
type LoginResponse = {
|
||||
token: string
|
||||
user: UTSJSONObject
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
username: '' as string,
|
||||
password: '' as string
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleLogin() {
|
||||
if (this.username === '' || this.username === null) {
|
||||
uni.showToast({ title: '请输入用户名', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (this.password === '' || this.password === null) {
|
||||
uni.showToast({ title: '请输入密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await login({
|
||||
username: this.username,
|
||||
password: this.password
|
||||
} as UTSJSONObject)
|
||||
|
||||
const token = res.data['token'] as string
|
||||
uni.setStorageSync('token', token)
|
||||
|
||||
if (res.data['user'] !== null) {
|
||||
uni.setStorageSync('userInfo', JSON.stringify(res.data['user']))
|
||||
}
|
||||
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
console.error('登录失败', e)
|
||||
}
|
||||
},
|
||||
goToRegister() {
|
||||
uni.navigateTo({ url: '/pages/register/register' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
background: $bg-color-dark;
|
||||
padding: 120rpx 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 56rpx;
|
||||
font-weight: bold;
|
||||
color: $primary-color;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.app-subtitle {
|
||||
font-size: $font-size-base;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
background: $bg-color-card;
|
||||
border-radius: $border-radius-base;
|
||||
padding: 0 32rpx;
|
||||
font-size: $font-size-base;
|
||||
color: $text-color;
|
||||
border: 2rpx solid $border-color;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: $text-color-placeholder;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
background: $primary-color;
|
||||
border-radius: $border-radius-base;
|
||||
color: #FFFFFF;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: bold;
|
||||
margin-top: 48rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.login-btn:active {
|
||||
background: $primary-color-dark;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
text-align: center;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: $primary-color;
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
</style>
|
||||
300
app/pages/market/market.uvue
Normal file
300
app/pages/market/market.uvue
Normal file
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<view class="market-container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
v-model="keyword"
|
||||
placeholder="搜索币种"
|
||||
placeholder-class="placeholder"
|
||||
@input="onSearch"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 分类标签 -->
|
||||
<view class="tabs">
|
||||
<text :class="['tab', activeTab === 'all' ? 'active' : '']" @click="activeTab = 'all'">全部</text>
|
||||
<text :class="['tab', activeTab === 'realtime' ? 'active' : '']" @click="activeTab = 'realtime'">实时</text>
|
||||
<text :class="['tab', activeTab === 'hot' ? 'active' : '']" @click="activeTab = 'hot'">热门</text>
|
||||
</view>
|
||||
|
||||
<!-- 币种列表 -->
|
||||
<view class="coin-list">
|
||||
<view class="list-header">
|
||||
<text class="header-item name">币种</text>
|
||||
<text class="header-item price">最新价</text>
|
||||
<text class="header-item change">涨跌幅</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="scroll-area">
|
||||
<view class="coin-item" v-for="(coin, index) in filteredCoins" :key="index" @click="goTrade(coin)">
|
||||
<view class="coin-left">
|
||||
<text class="coin-icon">{{ coin.icon }}</text>
|
||||
<view class="coin-info">
|
||||
<text class="coin-code">{{ coin.code }}/USDT</text>
|
||||
<text class="coin-name">{{ coin.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coin-center">
|
||||
<text class="coin-price">${{ coin.price }}</text>
|
||||
</view>
|
||||
<view class="coin-right">
|
||||
<view :class="['change-box', coin.change >= 0 ? 'up' : 'down']">
|
||||
<text class="change-text">{{ coin.change >= 0 ? '+' : '' }}{{ coin.change }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { getCoinList } from '@/api/market.uts'
|
||||
|
||||
type CoinItem = {
|
||||
code: string
|
||||
name: string
|
||||
price: string
|
||||
change: number
|
||||
icon: string
|
||||
priceType: number
|
||||
}
|
||||
|
||||
const keyword = ref('')
|
||||
const activeTab = ref('all')
|
||||
const allCoins = ref<CoinItem[]>([])
|
||||
const filteredCoins = ref<CoinItem[]>([])
|
||||
|
||||
onShow(() => {
|
||||
loadCoins()
|
||||
})
|
||||
|
||||
func loadCoins () {
|
||||
getCoinList()
|
||||
.then((res) => {
|
||||
const data = res.data as UTSJSONObject
|
||||
const list = data['list'] as Array<UTSJSONObject>
|
||||
const coins: CoinItem[] = []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i]
|
||||
coins.push({
|
||||
code: item['code'] as string,
|
||||
name: item['name'] as string,
|
||||
price: formatPrice(item['price'] as number),
|
||||
change: (item['change24h'] as number) || 0,
|
||||
icon: getCoinIcon(item['code'] as string),
|
||||
priceType: item['priceType'] as number
|
||||
})
|
||||
}
|
||||
allCoins.value = coins
|
||||
filterCoins()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
func filterCoins () {
|
||||
let result: CoinItem[] = []
|
||||
|
||||
if (activeTab.value === 'realtime') {
|
||||
result = allCoins.value.filter((c) => c.priceType === 1)
|
||||
} else if (activeTab.value === 'hot') {
|
||||
result = allCoins.value.slice(0, 6)
|
||||
} else {
|
||||
result = allCoins.value
|
||||
}
|
||||
|
||||
if (keyword.value.trim() !== '') {
|
||||
const kw = keyword.value.trim().toLowerCase()
|
||||
result = result.filter((c) =>
|
||||
c.code.toLowerCase().includes(kw) || c.name.toLowerCase().includes(kw)
|
||||
)
|
||||
}
|
||||
|
||||
filteredCoins.value = result
|
||||
}
|
||||
|
||||
func onSearch () {
|
||||
filterCoins()
|
||||
}
|
||||
|
||||
watch(activeTab, () => {
|
||||
filterCoins()
|
||||
})
|
||||
|
||||
func formatPrice (value: number): string {
|
||||
if (value >= 1000) return value.toFixed(2)
|
||||
if (value >= 1) return value.toFixed(4)
|
||||
return value.toFixed(6)
|
||||
}
|
||||
|
||||
func getCoinIcon (code: string): string {
|
||||
const icons: Map<string, string> = new Map()
|
||||
icons.set('BTC', '₿')
|
||||
icons.set('ETH', 'Ξ')
|
||||
icons.set('SOL', '◎')
|
||||
icons.set('USDT', '₮')
|
||||
icons.set('DOGE', '🐕')
|
||||
icons.set('XRP', '✕')
|
||||
return icons.get(code) || '●'
|
||||
}
|
||||
|
||||
func goTrade (coin: CoinItem) {
|
||||
uni.switchTab({ url: '/pages/trade/trade' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.market-container {
|
||||
min-height: 100vh;
|
||||
background: #1A1A2E;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
height: 72rpx;
|
||||
background: #16213E;
|
||||
border-radius: 36rpx;
|
||||
padding: 0 32rpx;
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0 24rpx 16rpx;
|
||||
}
|
||||
|
||||
.tab {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin-right: 32rpx;
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #00D4AA;
|
||||
border-bottom: 4rpx solid #00D4AA;
|
||||
}
|
||||
|
||||
.coin-list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.header-item {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.header-item.name { width: 40%; }
|
||||
.header-item.price { width: 30%; text-align: center; }
|
||||
.header-item.change { width: 30%; text-align: right; }
|
||||
|
||||
.scroll-area {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.coin-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.coin-left {
|
||||
width: 40%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.coin-icon {
|
||||
font-size: 36rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.coin-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.coin-code {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.coin-name {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.coin-center {
|
||||
width: 30%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.coin-price {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.coin-right {
|
||||
width: 30%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.change-box {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.change-box.up {
|
||||
background: rgba(0, 200, 83, 0.2);
|
||||
}
|
||||
|
||||
.change-box.down {
|
||||
background: rgba(255, 82, 82, 0.2);
|
||||
}
|
||||
|
||||
.change-text {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.change-box.up .change-text {
|
||||
color: #00C853;
|
||||
}
|
||||
|
||||
.change-box.down .change-text {
|
||||
color: #FF5252;
|
||||
}
|
||||
</style>
|
||||
209
app/pages/mine/mine.uvue
Normal file
209
app/pages/mine/mine.uvue
Normal file
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<view class="mine-container">
|
||||
<!-- 用户信息 -->
|
||||
<view class="user-card">
|
||||
<view class="avatar">
|
||||
<text class="avatar-text">{{ avatarText }}</text>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<text class="username">{{ username }}</text>
|
||||
<text class="user-level">普通用户</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能列表 -->
|
||||
<view class="menu-list">
|
||||
<view class="menu-item" @click="goToKyc">
|
||||
<text class="menu-icon">实名</text>
|
||||
<text class="menu-text">实名认证</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @click="goToSecurity">
|
||||
<text class="menu-icon">安全</text>
|
||||
<text class="menu-text">安全设置</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" @click="goToAbout">
|
||||
<text class="menu-icon">关于</text>
|
||||
<text class="menu-text">关于我们</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退出按钮 -->
|
||||
<button class="logout-btn" @click="handleLogout">退出登录</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
username: '' as string
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
avatarText(): string {
|
||||
if (this.username !== null && this.username.length > 0) {
|
||||
return this.username.substring(0, 1).toUpperCase()
|
||||
}
|
||||
return 'U'
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadUserInfo()
|
||||
},
|
||||
methods: {
|
||||
loadUserInfo() {
|
||||
const userInfoStr = uni.getStorageSync('userInfo') as string
|
||||
if (userInfoStr !== null && userInfoStr !== '') {
|
||||
try {
|
||||
const userInfo = JSON.parse(userInfoStr) as UTSJSONObject
|
||||
this.username = userInfo['username'] as string
|
||||
} catch (e) {
|
||||
this.username = '用户'
|
||||
}
|
||||
} else {
|
||||
this.username = '用户'
|
||||
}
|
||||
},
|
||||
goToKyc() {
|
||||
uni.showToast({ title: '功能开发中', icon: 'none' })
|
||||
},
|
||||
goToSecurity() {
|
||||
uni.showToast({ title: '功能开发中', icon: 'none' })
|
||||
},
|
||||
goToAbout() {
|
||||
uni.showToast({ title: '模拟所 v1.0.0', icon: 'none' })
|
||||
},
|
||||
handleLogout() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.mine-container {
|
||||
min-height: 100vh;
|
||||
background: $bg-color-dark;
|
||||
padding: $spacing-base;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $spacing-lg;
|
||||
background: $bg-color-card;
|
||||
border-radius: $border-radius-lg;
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
background: $primary-color;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: $spacing-base;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 48rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: $font-size-xl;
|
||||
color: $text-color;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.user-level {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
background: $bg-color-card;
|
||||
border-radius: $border-radius-lg;
|
||||
overflow: hidden;
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $spacing-base $spacing-lg;
|
||||
border-bottom: 1rpx solid $border-color;
|
||||
}
|
||||
|
||||
.menu-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
background: $primary-color;
|
||||
border-radius: $border-radius-base;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: $font-size-sm;
|
||||
color: #FFFFFF;
|
||||
margin-right: $spacing-base;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
flex: 1;
|
||||
font-size: $font-size-base;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: $font-size-base;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: $error-color;
|
||||
border-radius: $border-radius-base;
|
||||
color: #FFFFFF;
|
||||
font-size: $font-size-lg;
|
||||
border: none;
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.logout-btn:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
203
app/pages/register/register.uvue
Normal file
203
app/pages/register/register.uvue
Normal file
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<view class="register-container">
|
||||
<!-- Logo区域 -->
|
||||
<view class="logo-section">
|
||||
<text class="logo-text">₿</text>
|
||||
<text class="app-name">注册账号</text>
|
||||
</view>
|
||||
|
||||
<!-- 注册表单 -->
|
||||
<view class="form-section">
|
||||
<view class="form-item">
|
||||
<text class="form-label">账号</text>
|
||||
<input
|
||||
class="form-input"
|
||||
type="text"
|
||||
v-model="username"
|
||||
placeholder="请输入账号(4-20位字母数字)"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">密码</text>
|
||||
<input
|
||||
class="form-input"
|
||||
type="password"
|
||||
v-model="password"
|
||||
placeholder="请输入密码(至少6位)"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">确认密码</text>
|
||||
<input
|
||||
class="form-input"
|
||||
type="password"
|
||||
v-model="confirmPassword"
|
||||
placeholder="请再次输入密码"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 注册按钮 -->
|
||||
<button class="register-btn" :disabled="loading" @click="handleRegister">
|
||||
<text class="btn-text">{{ loading ? '注册中...' : '注 册' }}</text>
|
||||
</button>
|
||||
|
||||
<!-- 登录入口 -->
|
||||
<view class="login-row">
|
||||
<text class="login-text">已有账号?</text>
|
||||
<text class="login-link" @click="goLogin">立即登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { register } from '@/api/user.uts'
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
func handleRegister () {
|
||||
const usernameValue = username.value.trim()
|
||||
const passwordValue = password.value.trim()
|
||||
const confirmPasswordValue = confirmPassword.value.trim()
|
||||
|
||||
if (usernameValue === '' || usernameValue.length < 4) {
|
||||
uni.showToast({ title: '账号至少4位', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (passwordValue === '' || passwordValue.length < 6) {
|
||||
uni.showToast({ title: '密码至少6位', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (passwordValue !== confirmPasswordValue) {
|
||||
uni.showToast({ title: '两次密码不一致', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
register(usernameValue, passwordValue)
|
||||
.then((res) => {
|
||||
const data = res.data as UTSJSONObject
|
||||
uni.setStorageSync('token', data['token'] as string)
|
||||
uni.setStorageSync('userInfo', JSON.stringify(data['userInfo']))
|
||||
|
||||
uni.showToast({ title: '注册成功', icon: 'success' })
|
||||
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
}, 1000)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('注册失败:', error)
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
func goLogin () {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.register-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #1A1A2E 0%, #16213E 100%);
|
||||
padding: 0 60rpx;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
padding-top: 120rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 80rpx;
|
||||
color: #00D4AA;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16rpx;
|
||||
padding: 0 32rpx;
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.register-btn {
|
||||
margin-top: 48rpx;
|
||||
height: 96rpx;
|
||||
background: linear-gradient(90deg, #00D4AA 0%, #00B894 100%);
|
||||
border-radius: 48rpx;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.login-row {
|
||||
margin-top: 32rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.login-link {
|
||||
font-size: 26rpx;
|
||||
color: #00D4AA;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
</style>
|
||||
361
app/pages/trade/trade.uvue
Normal file
361
app/pages/trade/trade.uvue
Normal file
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<view class="trade-container">
|
||||
<!-- 交易对选择 -->
|
||||
<view class="pair-selector" @click="showPairPicker = true">
|
||||
<text class="pair-text">{{ selectedPair }}</text>
|
||||
<text class="pair-arrow">▼</text>
|
||||
</view>
|
||||
|
||||
<!-- 价格信息 -->
|
||||
<view class="price-section">
|
||||
<text class="current-price">${{ currentPrice }}</text>
|
||||
<text :class="['price-change', priceChange >= 0 ? 'up' : 'down']">
|
||||
{{ priceChange >= 0 ? '+' : '' }}{{ priceChange }}%
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 买卖切换 -->
|
||||
<view class="trade-tabs">
|
||||
<text :class="['trade-tab', tradeType === 'buy' ? 'active buy' : '']" @click="tradeType = 'buy'">买入</text>
|
||||
<text :class="['trade-tab', tradeType === 'sell' ? 'active sell' : '']" @click="tradeType = 'sell'">卖出</text>
|
||||
</view>
|
||||
|
||||
<!-- 交易表单 -->
|
||||
<view class="trade-form">
|
||||
<view class="form-item">
|
||||
<text class="form-label">价格(USDT)</text>
|
||||
<view class="input-row">
|
||||
<input class="form-input" type="digit" v-model="price" />
|
||||
<text class="input-unit">USDT</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">数量</text>
|
||||
<view class="input-row">
|
||||
<input class="form-input" type="digit" v-model="quantity" />
|
||||
<text class="input-unit">{{ coinCode }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="amount-row">
|
||||
<text class="amount-label">交易金额</text>
|
||||
<text class="amount-value">{{ totalAmount }} USDT</text>
|
||||
</view>
|
||||
|
||||
<view class="balance-row">
|
||||
<text class="balance-label">可用</text>
|
||||
<text class="balance-value">{{ availableBalance }} USDT</text>
|
||||
</view>
|
||||
|
||||
<!-- 交易按钮 -->
|
||||
<button :class="['trade-btn', tradeType]" @click="handleTrade">
|
||||
<text class="btn-text">{{ tradeType === 'buy' ? '买入' : '卖出' }} {{ coinCode }}</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 持仓信息 -->
|
||||
<view class="position-section">
|
||||
<text class="section-title">当前持仓</text>
|
||||
<view class="position-info">
|
||||
<text class="position-label">{{ coinCode }}</text>
|
||||
<text class="position-value">{{ positionQuantity }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { buy, sell } from '@/api/trade.uts'
|
||||
import { getOverview, getTradeAccount } from '@/api/asset.uts'
|
||||
import { getCoinDetail } from '@/api/market.uts'
|
||||
|
||||
const coinCode = ref('BTC')
|
||||
const selectedPair = ref('BTC/USDT')
|
||||
const currentPrice = ref('0.00')
|
||||
const priceChange = ref(0)
|
||||
const tradeType = ref('buy')
|
||||
const price = ref('')
|
||||
const quantity = ref('')
|
||||
const availableBalance = ref('0.00')
|
||||
const positionQuantity = ref('0.0000')
|
||||
const showPairPicker = ref(false)
|
||||
|
||||
const totalAmount = computed((): string => {
|
||||
const p = parseFloat(price.value) || 0
|
||||
const q = parseFloat(quantity.value) || 0
|
||||
return (p * q).toFixed(2)
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
func loadData () {
|
||||
loadBalance()
|
||||
loadCoinInfo()
|
||||
loadPosition()
|
||||
}
|
||||
|
||||
func loadBalance () {
|
||||
getOverview()
|
||||
.then((res) => {
|
||||
const data = res.data as UTSJSONObject
|
||||
// 获取交易账户USDT余额
|
||||
const positions = data['positions'] as Array<UTSJSONObject>
|
||||
for (let i = 0; i < positions.length; i++) {
|
||||
const pos = positions[i]
|
||||
if (pos['coinCode'] === 'USDT') {
|
||||
availableBalance.value = (pos['quantity'] as number).toFixed(2)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
func loadCoinInfo () {
|
||||
getCoinDetail(coinCode.value)
|
||||
.then((res) => {
|
||||
const data = res.data as UTSJSONObject
|
||||
currentPrice.value = formatPrice(data['price'] as number)
|
||||
priceChange.value = (data['change24h'] as number) || 0
|
||||
price.value = currentPrice.value
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
func loadPosition () {
|
||||
getTradeAccount()
|
||||
.then((res) => {
|
||||
const data = res.data as UTSJSONObject
|
||||
const positions = data['positions'] as Array<UTSJSONObject>
|
||||
for (let i = 0; i < positions.length; i++) {
|
||||
const pos = positions[i]
|
||||
if (pos['coinCode'] === coinCode.value) {
|
||||
positionQuantity.value = (pos['quantity'] as number).toFixed(4)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
func handleTrade () {
|
||||
const priceValue = parseFloat(price.value) || 0
|
||||
const quantityValue = parseFloat(quantity.value) || 0
|
||||
|
||||
if (priceValue <= 0) {
|
||||
uni.showToast({ title: '请输入有效价格', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (quantityValue <= 0) {
|
||||
uni.showToast({ title: '请输入有效数量', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const api = tradeType.value === 'buy' ? buy : sell
|
||||
|
||||
api(coinCode.value, price.value, quantity.value)
|
||||
.then((res) => {
|
||||
uni.showToast({ title: tradeType.value === 'buy' ? '买入成功' : '卖出成功', icon: 'success' })
|
||||
price.value = ''
|
||||
quantity.value = ''
|
||||
loadData()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
func formatPrice (value: number): string {
|
||||
if (value >= 1000) return value.toFixed(2)
|
||||
if (value >= 1) return value.toFixed(4)
|
||||
return value.toFixed(6)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.trade-container {
|
||||
min-height: 100vh;
|
||||
background: #1A1A2E;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.pair-selector {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.pair-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.pair-arrow {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.price-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.current-price {
|
||||
font-size: 56rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.price-change {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.price-change.up { color: #00C853; }
|
||||
.price-change.down { color: #FF5252; }
|
||||
|
||||
.trade-tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 32rpx;
|
||||
background: #16213E;
|
||||
border-radius: 16rpx;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
.trade-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 16rpx 0;
|
||||
font-size: 30rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.trade-tab.active.buy {
|
||||
background: rgba(0, 200, 83, 0.2);
|
||||
color: #00C853;
|
||||
}
|
||||
|
||||
.trade-tab.active.sell {
|
||||
background: rgba(255, 82, 82, 0.2);
|
||||
color: #FF5252;
|
||||
}
|
||||
|
||||
.trade-form {
|
||||
margin-top: 32rpx;
|
||||
background: #16213E;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 12rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.input-unit {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.amount-row, .balance-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.amount-label, .balance-label {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.amount-value, .balance-value {
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.trade-btn {
|
||||
margin-top: 24rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.trade-btn.buy {
|
||||
background: linear-gradient(90deg, #00C853 0%, #00A844 100%);
|
||||
}
|
||||
|
||||
.trade-btn.sell {
|
||||
background: linear-gradient(90deg, #FF5252 0%, #E04040 100%);
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.position-section {
|
||||
margin-top: 32rpx;
|
||||
background: #16213E;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.position-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.position-label {
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.position-value {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #00D4AA;
|
||||
}
|
||||
</style>
|
||||
2
app/static/default-avatar.png
Normal file
2
app/static/default-avatar.png
Normal file
@@ -0,0 +1,2 @@
|
||||
<!-- 占位文件 - 请替换为实际的默认头像图片 -->
|
||||
<!-- 建议尺寸: 200x200 像素 -->
|
||||
2
app/static/logo.png
Normal file
2
app/static/logo.png
Normal file
@@ -0,0 +1,2 @@
|
||||
<!-- 占位文件 - 请替换为实际的 logo 图片 -->
|
||||
<!-- 建议尺寸: 512x512 像素 -->
|
||||
2
app/static/tabbar/asset-active.png
Normal file
2
app/static/tabbar/asset-active.png
Normal file
@@ -0,0 +1,2 @@
|
||||
<!-- 占位文件 - 首页图标(选中状态) -->
|
||||
<!-- 建议尺寸: 48x48 像素 -->
|
||||
2
app/static/tabbar/asset.png
Normal file
2
app/static/tabbar/asset.png
Normal file
@@ -0,0 +1,2 @@
|
||||
<!-- 占位文件 - 首页图标 -->
|
||||
<!-- 建议尺寸: 48x48 像素 -->
|
||||
2
app/static/tabbar/home-active.png
Normal file
2
app/static/tabbar/home-active.png
Normal file
@@ -0,0 +1,2 @@
|
||||
<!-- 占位文件 - 首页图标(选中状态) -->
|
||||
<!-- 建议尺寸: 48x48 像素 -->
|
||||
2
app/static/tabbar/home.png
Normal file
2
app/static/tabbar/home.png
Normal file
@@ -0,0 +1,2 @@
|
||||
<!-- 占位文件 - 首页图标 -->
|
||||
<!-- 建议尺寸: 48x48 像素 -->
|
||||
2
app/static/tabbar/market-active.png
Normal file
2
app/static/tabbar/market-active.png
Normal file
@@ -0,0 +1,2 @@
|
||||
<!-- 占位文件 - 首页图标(选中状态) -->
|
||||
<!-- 建议尺寸: 48x48 像素 -->
|
||||
2
app/static/tabbar/market.png
Normal file
2
app/static/tabbar/market.png
Normal file
@@ -0,0 +1,2 @@
|
||||
<!-- 占位文件 - 首页图标 -->
|
||||
<!-- 建议尺寸: 48x48 像素 -->
|
||||
2
app/static/tabbar/mine-active.png
Normal file
2
app/static/tabbar/mine-active.png
Normal file
@@ -0,0 +1,2 @@
|
||||
<!-- 占位文件 - 我的图标(选中状态) -->
|
||||
<!-- 建议尺寸: 48x48 像素 -->
|
||||
2
app/static/tabbar/mine.png
Normal file
2
app/static/tabbar/mine.png
Normal file
@@ -0,0 +1,2 @@
|
||||
<!-- 占位文件 - 我的图标 -->
|
||||
<!-- 建议尺寸: 48x48 像素 -->
|
||||
2
app/static/tabbar/trade-active.png
Normal file
2
app/static/tabbar/trade-active.png
Normal file
@@ -0,0 +1,2 @@
|
||||
<!-- 占位文件 - 首页图标(选中状态) -->
|
||||
<!-- 建议尺寸: 48x48 像素 -->
|
||||
2
app/static/tabbar/trade.png
Normal file
2
app/static/tabbar/trade.png
Normal file
@@ -0,0 +1,2 @@
|
||||
<!-- 占位文件 - 首页图标 -->
|
||||
<!-- 建议尺寸: 48x48 像素 -->
|
||||
54
app/uni.scss
Normal file
54
app/uni.scss
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* uni-app 全局样式变量 - 模拟所APP
|
||||
*/
|
||||
|
||||
/* 主题色 */
|
||||
$primary-color: #00D4AA;
|
||||
$primary-color-light: #00E6B8;
|
||||
$primary-color-dark: #00B894;
|
||||
|
||||
/* 状态色 */
|
||||
$success-color: #00C853;
|
||||
$warning-color: #FF9800;
|
||||
$error-color: #FF5252;
|
||||
$info-color: #2196F3;
|
||||
|
||||
/* 深色主题 */
|
||||
$bg-color-dark: #1A1A2E;
|
||||
$bg-color-card: #16213E;
|
||||
$text-color: #FFFFFF;
|
||||
$text-color-secondary: rgba(255, 255, 255, 0.6);
|
||||
$text-color-placeholder: rgba(255, 255, 255, 0.3);
|
||||
$border-color: rgba(255, 255, 255, 0.1);
|
||||
|
||||
/* 涨跌色 */
|
||||
$up-color: #00C853;
|
||||
$down-color: #FF5252;
|
||||
|
||||
/* 字体大小 */
|
||||
$font-size-xs: 22rpx;
|
||||
$font-size-sm: 24rpx;
|
||||
$font-size-base: 28rpx;
|
||||
$font-size-md: 30rpx;
|
||||
$font-size-lg: 32rpx;
|
||||
$font-size-xl: 36rpx;
|
||||
$font-size-xxl: 48rpx;
|
||||
|
||||
/* 间距 */
|
||||
$spacing-xs: 8rpx;
|
||||
$spacing-sm: 16rpx;
|
||||
$spacing-base: 24rpx;
|
||||
$spacing-md: 32rpx;
|
||||
$spacing-lg: 48rpx;
|
||||
$spacing-xl: 64rpx;
|
||||
|
||||
/* 圆角 */
|
||||
$border-radius-sm: 8rpx;
|
||||
$border-radius-base: 12rpx;
|
||||
$border-radius-lg: 20rpx;
|
||||
$border-radius-xl: 24rpx;
|
||||
$border-radius-round: 999rpx;
|
||||
|
||||
/* 阴影 */
|
||||
$box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.3);
|
||||
$box-shadow-lg: 0 4rpx 24rpx rgba(0, 0, 0, 0.4);
|
||||
31
app/vite.config.ts
Normal file
31
app/vite.config.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import uni from '@dcloudio/vite-plugin-uni'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [uni()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:9010',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
minify: 'terser',
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user