diff --git a/app/App.uvue b/app/App.uvue deleted file mode 100644 index ebff13e..0000000 --- a/app/App.uvue +++ /dev/null @@ -1,60 +0,0 @@ - - - diff --git a/app/PACKAGING.md b/app/PACKAGING.md deleted file mode 100644 index 7a5c9d8..0000000 --- a/app/PACKAGING.md +++ /dev/null @@ -1,176 +0,0 @@ -# 模拟所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) diff --git a/app/README.md b/app/README.md deleted file mode 100644 index b306086..0000000 --- a/app/README.md +++ /dev/null @@ -1,203 +0,0 @@ -# 藤编企业移动端应用 (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 diff --git a/app/api/asset.uts b/app/api/asset.uts deleted file mode 100644 index c46f014..0000000 --- a/app/api/asset.uts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 资产API - */ -import { get, post } from './request.uts' - -/** - * 获取资产总览 - */ -export func getAssetOverview (): Promise { - return get('/api/asset/overview', null) -} - -/** - * 获取资金账户 - */ -export func getFundAccount (): Promise { - return get('/api/asset/fund', null) -} - -/** - * 获取交易账户 - */ -export func getTradeAccounts (): Promise { - return get('/api/asset/trade', null) -} - -/** - * 资金划转 - */ -export func transfer (direction: number, amount: string): Promise { - return post('/api/asset/transfer', { direction, amount } as UTSJSONObject) -} - -/** - * 获取资金流水 - */ -export func getFlows (flowType: number | null, pageNum: number, pageSize: number): Promise { - const params: UTSJSONObject = { pageNum: pageNum, pageSize: pageSize } - if (flowType !== null) { - params['flowType'] = flowType - } - return get('/api/asset/flow', params) -} diff --git a/app/api/fund.uts b/app/api/fund.uts deleted file mode 100644 index 95ce0f0..0000000 --- a/app/api/fund.uts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 充提API - */ -import { get, post } from './request.uts' - -/** - * 申请充值 - */ -export func deposit (amount: string, remark: string | null): Promise { - return post('/api/fund/deposit', { amount, remark } as UTSJSONObject) -} - -/** - * 申请提现 - */ -export func withdraw (amount: string, remark: string | null): Promise { - return post('/api/fund/withdraw', { amount, remark } as UTSJSONObject) -} - -/** - * 取消订单 - */ -export func cancelOrder (orderNo: string): Promise { - return post('/api/fund/cancel', { orderNo } as UTSJSONObject) -} - -/** - * 获取充提记录 - */ -export func getOrders (type: number | null, pageNum: number, pageSize: number): Promise { - const params: UTSJSONObject = { pageNum: pageNum, pageSize: pageSize } - if (type !== null) { - params['type'] = type - } - return get('/api/fund/orders', params) -} diff --git a/app/api/index.uts b/app/api/index.uts deleted file mode 100644 index 7c6b222..0000000 --- a/app/api/index.uts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * 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' diff --git a/app/api/market.uts b/app/api/market.uts deleted file mode 100644 index e16d6b4..0000000 --- a/app/api/market.uts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * 行情API - */ -import { get } from './request.uts' - -/** - * 获取币种列表 - */ -export func getCoinList (): Promise { - return get('/api/market/list', null) -} - -/** - * 获取币种详情 - */ -export func getCoinDetail (code: string): Promise { - return get('/api/market/detail', { code } as UTSJSONObject) -} - -/** - * 搜索币种 - */ -export func searchCoins (keyword: string): Promise { - return get('/api/market/search', { keyword } as UTSJSONObject) -} diff --git a/app/api/request.uts b/app/api/request.uts deleted file mode 100644 index 07021bd..0000000 --- a/app/api/request.uts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * 网络请求封装 - 模拟所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 { - 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 { - 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 { - return request({ url, method: 'GET', data: params }) -} - -/** - * POST 请求 - */ -export func post (url: string, data: UTSJSONObject | null = null): Promise { - return request({ url, method: 'POST', data: data }) -} - -export const config = { BASE_URL, TIMEOUT } diff --git a/app/api/trade.uts b/app/api/trade.uts deleted file mode 100644 index cdb6b42..0000000 --- a/app/api/trade.uts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * 交易API - */ -import { get, post } from './request.uts' - -/** - * 买入 - */ -export func buy (coinCode: string, price: string, quantity: string): Promise { - return post('/api/trade/buy', { coinCode, price, quantity } as UTSJSONObject) -} - -/** - * 卖出 - */ -export func sell (coinCode: string, price: string, quantity: string): Promise { - return post('/api/trade/sell', { coinCode, price, quantity } as UTSJSONObject) -} - -/** - * 获取交易记录 - */ -export func getOrders (coinCode: string | null, direction: number | null, pageNum: number, pageSize: number): Promise { - 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 { - return get('/api/trade/order/detail', { orderNo } as UTSJSONObject) -} diff --git a/app/api/user.uts b/app/api/user.uts deleted file mode 100644 index 4589b7a..0000000 --- a/app/api/user.uts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * 用户API - */ -import { get, post } from './request.uts' - -/** - * 用户登录 - */ -export func login (username: string, password: string): Promise { - return post('/api/user/login', { username, password } as UTSJSONObject) -} - -/** - * 用户注册 - */ -export func register (username: string, password: string): Promise { - return post('/api/user/register', { username, password } as UTSJSONObject) -} - -/** - * 获取用户信息 - */ -export func getUserInfo (): Promise { - return get('/api/user/info', null) -} - -/** - * 上传KYC资料 - */ -export func uploadKyc (idCardFront: string, idCardBack: string): Promise { - return post('/api/user/kyc', { idCardFront, idCardBack } as UTSJSONObject) -} - -/** - * 退出登录 - */ -export func logout (): Promise { - return post('/api/user/logout', null) -} diff --git a/app/index.html b/app/index.html deleted file mode 100644 index 58581cc..0000000 --- a/app/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - 藤编企业 - - - - -
- -
- - - diff --git a/app/main.uts b/app/main.uts deleted file mode 100644 index 6ead2ea..0000000 --- a/app/main.uts +++ /dev/null @@ -1,15 +0,0 @@ -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 - } -} diff --git a/app/manifest.json b/app/manifest.json deleted file mode 100644 index a3dc3fb..0000000 --- a/app/manifest.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "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": [ - "", - "", - "" - ], - "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" -} diff --git a/app/package.json b/app/package.json deleted file mode 100644 index 8b5fa9e..0000000 --- a/app/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "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" - } -} diff --git a/app/pages.json b/app/pages.json deleted file mode 100644 index 06b202d..0000000 --- a/app/pages.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "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" - } - ] - } -} diff --git a/app/pages/asset/asset.uvue b/app/pages/asset/asset.uvue deleted file mode 100644 index 8da72f2..0000000 --- a/app/pages/asset/asset.uvue +++ /dev/null @@ -1,448 +0,0 @@ - - - - - diff --git a/app/pages/index/index.uvue b/app/pages/index/index.uvue deleted file mode 100644 index d4ec591..0000000 --- a/app/pages/index/index.uvue +++ /dev/null @@ -1,305 +0,0 @@ - - - - - diff --git a/app/pages/login/login.uvue b/app/pages/login/login.uvue deleted file mode 100644 index 15a1a68..0000000 --- a/app/pages/login/login.uvue +++ /dev/null @@ -1,169 +0,0 @@ - - - - - diff --git a/app/pages/market/market.uvue b/app/pages/market/market.uvue deleted file mode 100644 index 005ebfd..0000000 --- a/app/pages/market/market.uvue +++ /dev/null @@ -1,300 +0,0 @@ - - - - - diff --git a/app/pages/mine/mine.uvue b/app/pages/mine/mine.uvue deleted file mode 100644 index 7281fd5..0000000 --- a/app/pages/mine/mine.uvue +++ /dev/null @@ -1,209 +0,0 @@ - - - - - diff --git a/app/pages/register/register.uvue b/app/pages/register/register.uvue deleted file mode 100644 index 38cebb9..0000000 --- a/app/pages/register/register.uvue +++ /dev/null @@ -1,203 +0,0 @@ - - - - - diff --git a/app/pages/trade/trade.uvue b/app/pages/trade/trade.uvue deleted file mode 100644 index f4bc833..0000000 --- a/app/pages/trade/trade.uvue +++ /dev/null @@ -1,361 +0,0 @@ - - - - - diff --git a/app/static/default-avatar.png b/app/static/default-avatar.png deleted file mode 100644 index 53d8a50..0000000 --- a/app/static/default-avatar.png +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/static/logo.png b/app/static/logo.png deleted file mode 100644 index 4c02d3e..0000000 --- a/app/static/logo.png +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/static/tabbar/asset-active.png b/app/static/tabbar/asset-active.png deleted file mode 100644 index ba3ecd0..0000000 --- a/app/static/tabbar/asset-active.png +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/static/tabbar/asset.png b/app/static/tabbar/asset.png deleted file mode 100644 index f527a21..0000000 --- a/app/static/tabbar/asset.png +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/static/tabbar/home-active.png b/app/static/tabbar/home-active.png deleted file mode 100644 index ba3ecd0..0000000 --- a/app/static/tabbar/home-active.png +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/static/tabbar/home.png b/app/static/tabbar/home.png deleted file mode 100644 index f527a21..0000000 --- a/app/static/tabbar/home.png +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/static/tabbar/market-active.png b/app/static/tabbar/market-active.png deleted file mode 100644 index ba3ecd0..0000000 --- a/app/static/tabbar/market-active.png +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/static/tabbar/market.png b/app/static/tabbar/market.png deleted file mode 100644 index f527a21..0000000 --- a/app/static/tabbar/market.png +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/static/tabbar/mine-active.png b/app/static/tabbar/mine-active.png deleted file mode 100644 index 2a9ae54..0000000 --- a/app/static/tabbar/mine-active.png +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/static/tabbar/mine.png b/app/static/tabbar/mine.png deleted file mode 100644 index 2e9387d..0000000 --- a/app/static/tabbar/mine.png +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/static/tabbar/trade-active.png b/app/static/tabbar/trade-active.png deleted file mode 100644 index ba3ecd0..0000000 --- a/app/static/tabbar/trade-active.png +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/static/tabbar/trade.png b/app/static/tabbar/trade.png deleted file mode 100644 index f527a21..0000000 --- a/app/static/tabbar/trade.png +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/app/uni.scss b/app/uni.scss deleted file mode 100644 index 57d958e..0000000 --- a/app/uni.scss +++ /dev/null @@ -1,54 +0,0 @@ -/** - * 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); diff --git a/app/vite.config.ts b/app/vite.config.ts deleted file mode 100644 index 644529c..0000000 --- a/app/vite.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -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 - } - } - } -}) diff --git a/deploy/bt_webhook.sh b/deploy/bt_webhook.sh new file mode 100644 index 0000000..a96ba00 --- /dev/null +++ b/deploy/bt_webhook.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# ============================================ +# 宝塔 Webhook 自动部署脚本 - Monisuo +# ============================================ + +# 配置项 - 请根据实际情况修改 +PROJECT_PATH="/opt/monisuo" # 项目部署路径 +GIT_REPO="http://sion:woshisaw.@8.155.172.147:3001/sion/monisuo.git" # Git仓库地址 +JAR_NAME="monisuo-1.0.jar" # JAR包名称 +LOG_FILE="/opt/monisuo/deploy.log" # 部署日志文件 + +# 记录日志函数 +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> $LOG_FILE +} + +log "========== 开始部署 ==========" + +# 进入项目目录 +cd $PROJECT_PATH || { + log "错误: 无法进入目录 $PROJECT_PATH" + exit 1 +} + +# 拉取最新代码 +log "正在拉取最新代码..." +git pull origin main >> $LOG_FILE 2>&1 + +if [ $? -ne 0 ]; then + log "错误: Git pull 失败" + exit 1 +fi + +log "代码拉取成功" + +# 检查是否有更新 +CHANGED=$(git diff --name-only HEAD~1 HEAD) +log "变更文件: $CHANGED" + +# 如果有Java代码变更,重新打包 +if echo "$CHANGED" | grep -q "src/"; then + log "检测到Java代码变更,开始重新打包..." + + # Maven打包 + mvn clean package -DskipTests >> $LOG_FILE 2>&1 + + if [ $? -ne 0 ]; then + log "错误: Maven打包失败" + exit 1 + fi + + log "Maven打包成功" +fi + +# 重启后端服务 +log "正在重启后端服务..." + +# 停止旧服务 +pkill -f $JAR_NAME +sleep 2 + +# 启动新服务 +nohup java -jar $PROJECT_PATH/target/$JAR_NAME --spring.profiles.active=dev > $PROJECT_PATH/app.log 2>&1 & + +if [ $? -eq 0 ]; then + log "后端服务启动成功" +else + log "错误: 后端服务启动失败" + exit 1 +fi + +# 检查服务是否启动成功 +sleep 5 +if pgrep -f $JAR_NAME > /dev/null; then + log "服务运行正常" +else + log "错误: 服务启动后未运行" + exit 1 +fi + +log "========== 部署完成 ==========" +echo "Deploy Success!" diff --git a/deploy/deploy_h5.sh b/deploy/deploy_h5.sh new file mode 100644 index 0000000..bb523a9 --- /dev/null +++ b/deploy/deploy_h5.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# H5前端部署脚本 + +PROJECT_PATH="/opt/monisuo/h5" +GIT_REPO="http://sion:woshisaw.@8.155.172.147:3001/sion/monisuo.git" + +echo "开始部署H5前端..." + +# 创建目录 +mkdir -p $PROJECT_PATH + +# 拉取代码(如果已存在) +if [ -d "$PROJECT_PATH/.git" ]; then + cd $PROJECT_PATH + git pull origin main +else + git clone $GIT_REPO $PROJECT_PATH + cd $PROJECT_PATH/app +fi + +# 注意:H5需要通过HBuilderX构建 +# 构建后的文件在 dist/build/h5 目录 +# 将构建好的文件上传到服务器即可 + +echo "请先在本地使用HBuilderX构建H5:" +echo "1. 发行 → 网站-H5手机版" +echo "2. 构建完成后将 dist/build/h5 目录上传到服务器" +echo "3. Nginx配置指向该目录" diff --git a/flutter_monisuo/README.md b/flutter_monisuo/README.md new file mode 100644 index 0000000..dc2b6cf --- /dev/null +++ b/flutter_monisuo/README.md @@ -0,0 +1,106 @@ +# Flutter Monisuo - 虚拟货币模拟交易平台 + +## 项目概述 + +Flutter 版本的模拟所 APP,支持 Android、iOS 平台。 + +## 项目结构 + +``` +flutter_monisuo/ +├── lib/ +│ ├── main.dart # 应用入口 +│ ├── core/ # 核心模块 +│ │ ├── constants/ # 颜色、API端点常量 +│ │ ├── theme/ # 主题配置 +│ │ ├── network/ # Dio 网络封装 +│ │ └── storage/ # SharedPreferences +│ ├── data/ +│ │ ├── models/ # 数据模型 +│ │ └── services/ # API 服务 +│ ├── providers/ # 状态管理 +│ └── ui/ +│ ├── common/ # 公共组件 +│ └── pages/ # 页面 +│ ├── auth/ # 登录/注册 +│ ├── home/ # 首页 +│ ├── market/ # 行情 +│ ├── trade/ # 交易 +│ ├── asset/ # 资产 +│ └── mine/ # 我的 +├── assets/ # 资源文件 +└── pubspec.yaml # 依赖配置 +``` + +## 功能模块 + +### 用户模块 +- 用户登录 +- 用户注册 +- 用户信息 +- 退出登录 + +### 行情模块 +- 币种列表 +- 币种搜索 +- 实时价格 + +### 交易模块 +- 买入/卖出 +- 交易记录 +- 持仓管理 + +### 资产模块 +- 资产总览 +- 资金账户 +- 交易账户 +- 充值/提现/划转 + +## 技术栈 + +- Flutter 3.x +- Provider (状态管理) +- Dio (网络请求) +- SharedPreferences (本地存储) + +## 运行项目 + +### 前置条件 +- Flutter SDK 已安装 +- Android Studio / VS Code + +### 运行步骤 + +```bash +# 1. 进入项目目录 +cd flutter_monisuo + +# 2. 获取依赖 +flutter pub get + +# 3. 运行项目 +flutter run + +# 或者指定平台 +flutter run -d android # Android +flutter run -d chrome # Chrome (Web) +``` + +## API 配置 + +API 基础地址配置在 `lib/core/constants/api_endpoints.dart`: + +```dart +static const String baseUrl = 'http://8.155.172.147:5010'; +``` + +## 主题色 + +- 主色: `#00D4AA` +- 涨色: `#00C853` +- 跌色: `#FF5252` +- 背景: `#1A1A2E` + +## 作者 + +Monisuo Team diff --git a/flutter_monisuo/lib/core/constants/api_endpoints.dart b/flutter_monisuo/lib/core/constants/api_endpoints.dart new file mode 100644 index 0000000..dbb34fd --- /dev/null +++ b/flutter_monisuo/lib/core/constants/api_endpoints.dart @@ -0,0 +1,80 @@ +/// API 端点配置 +class ApiEndpoints { + ApiEndpoints._(); + + /// 基础URL + static const String baseUrl = 'http://8.155.172.147:5010'; + + // ==================== 用户模块 ==================== + + /// 用户登录 + static const String login = '/api/user/login'; + + /// 用户注册 + static const String register = '/api/user/register'; + + /// 获取用户信息 + static const String userInfo = '/api/user/info'; + + /// 上传KYC资料 + static const String kyc = '/api/user/kyc'; + + /// 退出登录 + static const String logout = '/api/user/logout'; + + // ==================== 行情模块 ==================== + + /// 获取币种列表 + static const String coinList = '/api/market/list'; + + /// 获取币种详情 + static const String coinDetail = '/api/market/detail'; + + /// 搜索币种 + static const String coinSearch = '/api/market/search'; + + // ==================== 交易模块 ==================== + + /// 买入 + static const String buy = '/api/trade/buy'; + + /// 卖出 + static const String sell = '/api/trade/sell'; + + /// 获取交易记录 + static const String tradeOrders = '/api/trade/orders'; + + /// 获取订单详情 + static const String tradeOrderDetail = '/api/trade/order/detail'; + + // ==================== 资产模块 ==================== + + /// 获取资产总览 + static const String assetOverview = '/api/asset/overview'; + + /// 获取资金账户 + static const String fundAccount = '/api/asset/fund'; + + /// 获取交易账户 + static const String tradeAccount = '/api/asset/trade'; + + /// 资金划转 + static const String transfer = '/api/asset/transfer'; + + /// 获取资金流水 + static const String assetFlow = '/api/asset/flow'; + + // ==================== 充提模块 ==================== + + /// 申请充值 + static const String deposit = '/api/fund/deposit'; + + /// 申请提现 + static const String withdraw = '/api/fund/withdraw'; + + /// 取消订单 + static const String cancelOrder = '/api/fund/cancel'; + + /// 获取充提记录 + static const String fundOrders = '/api/fund/orders'; +} diff --git a/flutter_monisuo/lib/core/constants/app_colors.dart b/flutter_monisuo/lib/core/constants/app_colors.dart new file mode 100644 index 0000000..ae21080 --- /dev/null +++ b/flutter_monisuo/lib/core/constants/app_colors.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +/// 应用颜色常量 +class AppColors { + AppColors._(); + + // 主题色 + static const Color primary = Color(0xFF00D4AA); + static const Color primaryLight = Color(0xFF00E6B8); + static const Color primaryDark = Color(0xFF00B894); + + // 状态色 + static const Color success = Color(0xFF00C853); + static const Color warning = Color(0xFFFF9800); + static const Color error = Color(0xFFFF5252); + static const Color info = Color(0xFF2196F3); + + // 涨跌色 + static const Color up = Color(0xFF00C853); + static const Color down = Color(0xFFFF5252); + + // 深色主题背景 + static const Color background = Color(0xFF1A1A2E); + static const Color cardBackground = Color(0xFF16213E); + static const Color scaffoldBackground = Color(0xFF1A1A2E); + + // 文字颜色 + static const Color textPrimary = Colors.white; + static const Color textSecondary = Color(0x99FFFFFF); + static const Color textHint = Color(0x4DFFFFFF); + static const Color textDisabled = Color(0x33FFFFFF); + + // 边框和分割线 + static const Color border = Color(0x1AFFFFFF); + static const Color divider = Color(0x1AFFFFFF); + + // 输入框 + static const Color inputBackground = Color(0xFF16213E); + static const Color inputBorder = Color(0x33FFFFFF); + static const Color inputFocusBorder = Color(0xFF00D4AA); + + // 按钮渐变 + static const LinearGradient primaryGradient = LinearGradient( + colors: [primary, primaryDark], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ); + + // 买入按钮渐变 + static const LinearGradient buyGradient = LinearGradient( + colors: [Color(0xFF00C853), Color(0xFF00A844)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ); + + // 卖出按钮渐变 + static const LinearGradient sellGradient = LinearGradient( + colors: [Color(0xFFFF5252), Color(0xFFD32F2F)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ); +} diff --git a/flutter_monisuo/lib/core/network/dio_client.dart b/flutter_monisuo/lib/core/network/dio_client.dart new file mode 100644 index 0000000..9146407 --- /dev/null +++ b/flutter_monisuo/lib/core/network/dio_client.dart @@ -0,0 +1,155 @@ +import 'package:dio/dio.dart'; +import '../storage/local_storage.dart'; +import 'api_exception.dart'; + +/// API 响应模型 +class ApiResponse { + final bool success; + final String? message; + final T? data; + final String? code; + + ApiResponse({ + required this.success, + this.message, + this.data, + this.code, + }); + + factory ApiResponse.success(T data, [String? message]) { + return ApiResponse( + success: true, + data: data, + message: message, + code: '0000', + ); + } + + factory ApiResponse.fail(String message, [String? code]) { + return ApiResponse( + success: false, + message: message, + code: code, + ); + } + + factory ApiResponse.unauthorized(String message) { + return ApiResponse( + success: false, + message: message, + code: '0002', + ); + } + + factory ApiResponse.fromJson( + Map json, + T Function(dynamic)? fromJsonT, + ) { + final code = json['code'] as String? ?? ''; + final msg = json['msg'] as String? ?? ''; + + if (code == '0000') { + final data = json['data']; + if (fromJsonT != null && data != null) { + return ApiResponse.success(fromJsonT(data), msg); + } + return ApiResponse.success(data as T?, msg); + } else if (code == '0002') { + return ApiResponse.unauthorized(msg); + } else { + return ApiResponse.fail(msg, code); + } + } +} + +/// Dio 网络客户端 +class DioClient { + late final Dio _dio; + + DioClient() { + _dio = Dio(BaseOptions( + baseUrl: 'http://8.155.172.147:5010', + connectTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(seconds: 30), + headers: { + 'Content-Type': 'application/json', + }, + )); + + _dio.interceptors.add(_AuthInterceptor()); + _dio.interceptors.add(LogInterceptor( + requestHeader: false, + responseHeader: false, + error: true, + )); + } + + /// GET 请求 + Future> get( + String path, { + Map? queryParameters, + T Function(dynamic)? fromJson, + }) async { + try { + final response = await _dio.get(path, queryParameters: queryParameters); + return _handleResponse(response, fromJson); + } on DioException catch (e) { + return _handleError(e); + } + } + + /// POST 请求 + Future> post( + String path, { + dynamic data, + T Function(dynamic)? fromJson, + }) async { + try { + final response = await _dio.post(path, data: data); + return _handleResponse(response, fromJson); + } on DioException catch (e) { + return _handleError(e); + } + } + + /// 处理响应 + ApiResponse _handleResponse( + Response response, + T Function(dynamic)? fromJson, + ) { + final data = response.data; + if (data is Map) { + return ApiResponse.fromJson(data, fromJson); + } + return ApiResponse.fail('响应数据格式错误'); + } + + /// 处理错误 + ApiResponse _handleError(DioException e) { + if (e.response?.statusCode == 401) { + LocalStorage.clearUserData(); + return ApiResponse.unauthorized('登录已过期,请重新登录'); + } + return ApiResponse.fail(e.message ?? '网络请求失败'); + } +} + +/// 认证拦截器 +class _AuthInterceptor extends Interceptor { + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + final token = LocalStorage.getToken(); + if (token != null && token.isNotEmpty) { + options.headers['Authorization'] = 'Bearer $token'; + } + super.onRequest(options, handler); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + if (err.response?.statusCode == 401) { + LocalStorage.clearUserData(); + } + super.onError(err, handler); + } +} diff --git a/flutter_monisuo/lib/core/storage/local_storage.dart b/flutter_monisuo/lib/core/storage/local_storage.dart new file mode 100644 index 0000000..dd19060 --- /dev/null +++ b/flutter_monisuo/lib/core/storage/local_storage.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// 本地存储服务 +class LocalStorage { + LocalStorage._(); + + static const String _tokenKey = 'token'; + static const String _userInfoKey = 'userInfo'; + + static SharedPreferences? _prefs; + + /// 初始化 + static Future init() async { + _prefs = await SharedPreferences.getInstance(); + } + + /// 获取实例 + static SharedPreferences get prefs { + if (_prefs == null) { + throw Exception('LocalStorage not initialized. Call init() first.'); + } + return _prefs!; + } + + // ==================== Token 管理 ==================== + + /// 保存 Token + static Future saveToken(String token) async { + await prefs.setString(_tokenKey, token); + } + + /// 获取 Token + static String? getToken() { + return prefs.getString(_tokenKey); + } + + /// 移除 Token + static Future removeToken() async { + await prefs.remove(_tokenKey); + } + + /// 是否已登录 + static bool get isLoggedIn => getToken() != null && getToken()!.isNotEmpty; + + // ==================== 用户信息管理 ==================== + + /// 保存用户信息 + static Future saveUserInfo(Map userInfo) async { + await prefs.setString(_userInfoKey, jsonEncode(userInfo)); + } + + /// 获取用户信息 + static Map? getUserInfo() { + final str = prefs.getString(_userInfoKey); + if (str == null) return null; + try { + return jsonDecode(str) as Map; + } catch (e) { + return null; + } + } + + /// 移除用户信息 + static Future removeUserInfo() async { + await prefs.remove(_userInfoKey); + } + + // ==================== 通用方法 ==================== + + /// 保存字符串 + static Future setString(String key, String value) async { + await prefs.setString(key, value); + } + + /// 获取字符串 + static String? getString(String key) { + return prefs.getString(key); + } + + /// 保存布尔值 + static Future setBool(String key, bool value) async { + await prefs.setBool(key, value); + } + + /// 获取布尔值 + static bool? getBool(String key) { + return prefs.getBool(key); + } + + /// 清除所有数据 + static Future clearAll() async { + await prefs.clear(); + } + + /// 清除用户数据(退出登录时调用) + static Future clearUserData() async { + await removeToken(); + await removeUserInfo(); + } +} diff --git a/flutter_monisuo/lib/core/theme/app_theme.dart b/flutter_monisuo/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..445332a --- /dev/null +++ b/flutter_monisuo/lib/core/theme/app_theme.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import '../constants/app_colors.dart'; + +/// 应用主题配置 +class AppTheme { + AppTheme._(); + + /// 深色主题 + static ThemeData get darkTheme { + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + scaffoldBackgroundColor: AppColors.background, + primaryColor: AppColors.primary, + colorScheme: const ColorScheme.dark( + primary: AppColors.primary, + secondary: AppColors.primaryLight, + error: AppColors.error, + surface: AppColors.cardBackground, + ), + appBarTheme: const AppBarTheme( + backgroundColor: AppColors.background, + foregroundColor: AppColors.textPrimary, + elevation: 0, + centerTitle: true, + titleTextStyle: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: AppColors.cardBackground, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.border), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.border), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.primary), + ), + hintStyle: const TextStyle(color: AppColors.textHint), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 48), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: AppColors.primary, + ), + ), + dividerTheme: const DividerThemeData( + color: AppColors.border, + thickness: 1, + ), + cardTheme: CardTheme( + color: AppColors.cardBackground, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ); + } +} + +/// 文本样式 +class AppTextStyles { + AppTextStyles._(); + + static const TextStyle heading1 = TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ); + + static const TextStyle heading2 = TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ); + + static const TextStyle heading3 = TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ); + + static const TextStyle body1 = TextStyle( + fontSize: 16, + color: AppColors.textPrimary, + ); + + static const TextStyle body2 = TextStyle( + fontSize: 14, + color: AppColors.textPrimary, + ); + + static const TextStyle caption = TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + ); + + static const TextStyle hint = TextStyle( + fontSize: 14, + color: AppColors.textHint, + ); +} + +/// 间距常量 +class AppSpacing { + AppSpacing._(); + + static const double xs = 4.0; + static const double sm = 8.0; + static const double md = 16.0; + static const double lg = 24.0; + static const double xl = 32.0; + static const double xxl = 48.0; +} + +/// 圆角常量 +class AppRadius { + AppRadius._(); + + static const double sm = 8.0; + static const double md = 12.0; + static const double lg = 16.0; + static const double xl = 24.0; + static const double full = 999.0; +} diff --git a/flutter_monisuo/lib/data/models/account_models.dart b/flutter_monisuo/lib/data/models/account_models.dart new file mode 100644 index 0000000..cf418fe --- /dev/null +++ b/flutter_monisuo/lib/data/models/account_models.dart @@ -0,0 +1,166 @@ +/// 资产总览模型 +class AssetOverview { + final String totalAsset; + final String fundBalance; + final String tradeBalance; + final String totalProfit; + + AssetOverview({ + required this.totalAsset, + required this.fundBalance, + required this.tradeBalance, + required this.totalProfit, + }); + + factory AssetOverview.fromJson(Map json) { + return AssetOverview( + totalAsset: json['totalAsset']?.toString() ?? '0.00', + fundBalance: json['fundBalance']?.toString() ?? '0.00', + tradeBalance: json['tradeBalance']?.toString() ?? '0.00', + totalProfit: json['totalProfit']?.toString() ?? '0.00', + ); + } +} + +/// 资金账户模型 +class AccountFund { + final int id; + final int userId; + final String balance; + final String frozenBalance; + final DateTime? updateTime; + + AccountFund({ + required this.id, + required this.userId, + required this.balance, + required this.frozenBalance, + this.updateTime, + }); + + factory AccountFund.fromJson(Map json) { + return AccountFund( + id: json['id'] as int? ?? 0, + userId: json['userId'] as int? ?? 0, + balance: json['balance']?.toString() ?? '0.00', + frozenBalance: json['frozenBalance']?.toString() ?? '0.00', + updateTime: json['updateTime'] != null + ? DateTime.tryParse(json['updateTime']) + : null, + ); + } +} + +/// 交易账户模型(持仓) +class AccountTrade { + final int id; + final int userId; + final String coinCode; + final String quantity; + final String avgPrice; + final String totalCost; + final String currentValue; + final String profit; + final double profitRate; + final DateTime? updateTime; + + AccountTrade({ + required this.id, + required this.userId, + required this.coinCode, + required this.quantity, + required this.avgPrice, + required this.totalCost, + required this.currentValue, + required this.profit, + required this.profitRate, + this.updateTime, + }); + + factory AccountTrade.fromJson(Map json) { + return AccountTrade( + id: json['id'] as int? ?? 0, + userId: json['userId'] as int? ?? 0, + coinCode: json['coinCode'] as String? ?? '', + quantity: json['quantity']?.toString() ?? '0', + avgPrice: json['avgPrice']?.toString() ?? '0.00', + totalCost: json['totalCost']?.toString() ?? '0.00', + currentValue: json['currentValue']?.toString() ?? '0.00', + profit: json['profit']?.toString() ?? '0.00', + profitRate: (json['profitRate'] as num?)?.toDouble() ?? 0, + updateTime: json['updateTime'] != null + ? DateTime.tryParse(json['updateTime']) + : null, + ); + } + + /// 格式化盈亏率 + String get formattedProfitRate { + final prefix = profitRate >= 0 ? '+' : ''; + return '$prefix${profitRate.toStringAsFixed(2)}%'; + } + + /// 是否盈利 + bool get isProfit => profitRate >= 0; +} + +/// 资金流水模型 +class AccountFlow { + final int id; + final int userId; + final String flowType; + final String amount; + final String balanceBefore; + final String balanceAfter; + final String remark; + final DateTime? createTime; + + AccountFlow({ + required this.id, + required this.userId, + required this.flowType, + required this.amount, + required this.balanceBefore, + required this.balanceAfter, + required this.remark, + this.createTime, + }); + + factory AccountFlow.fromJson(Map json) { + return AccountFlow( + id: json['id'] as int? ?? 0, + userId: json['userId'] as int? ?? 0, + flowType: json['flowType']?.toString() ?? '', + amount: json['amount']?.toString() ?? '0.00', + balanceBefore: json['balanceBefore']?.toString() ?? '0.00', + balanceAfter: json['balanceAfter']?.toString() ?? '0.00', + remark: json['remark']?.toString() ?? '', + createTime: json['createTime'] != null + ? DateTime.tryParse(json['createTime']) + : null, + ); + } + + /// 流水类型文字 + String get flowTypeText { + switch (flowType) { + case '1': + return '充值'; + case '2': + return '提现'; + case '3': + return '转入交易账户'; + case '4': + return '从交易账户转出'; + case '5': + return '卖出收入'; + case '6': + return '买入支出'; + default: + return '未知'; + } + } + + /// 是否为收入 + bool get isIncome => ['1', '3', '5'].contains(flowType); +} diff --git a/flutter_monisuo/lib/data/models/coin.dart b/flutter_monisuo/lib/data/models/coin.dart new file mode 100644 index 0000000..935b0cf --- /dev/null +++ b/flutter_monisuo/lib/data/models/coin.dart @@ -0,0 +1,106 @@ +/// 币种模型 +class Coin { + final int id; + final String code; + final String name; + final String? icon; + final double price; + final double? priceUsd; + final double? priceCny; + final int priceType; // 1=实时价格, 2=管理员设置 + final double change24h; + final double? high24h; + final double? low24h; + final double? volume24h; + final int status; + final int sort; + + Coin({ + required this.id, + required this.code, + required this.name, + this.icon, + required this.price, + this.priceUsd, + this.priceCny, + required this.priceType, + required this.change24h, + this.high24h, + this.low24h, + this.volume24h, + required this.status, + this.sort = 0, + }); + + factory Coin.fromJson(Map json) { + return Coin( + id: json['id'] as int? ?? 0, + code: json['code'] as String? ?? '', + name: json['name'] as String? ?? '', + icon: json['icon'] as String?, + price: (json['price'] as num?)?.toDouble() ?? 0, + priceUsd: (json['priceUsd'] as num?)?.toDouble(), + priceCny: (json['priceCny'] as num?)?.toDouble(), + priceType: json['priceType'] as int? ?? 1, + change24h: (json['change24h'] as num?)?.toDouble() ?? 0, + high24h: (json['high24h'] as num?)?.toDouble(), + low24h: (json['low24h'] as num?)?.toDouble(), + volume24h: (json['volume24h'] as num?)?.toDouble(), + status: json['status'] as int? ?? 1, + sort: json['sort'] as int? ?? 0, + ); + } + + Map toJson() { + return { + 'id': id, + 'code': code, + 'name': name, + 'icon': icon, + 'price': price, + 'priceUsd': priceUsd, + 'priceCny': priceCny, + 'priceType': priceType, + 'change24h': change24h, + 'high24h': high24h, + 'low24h': low24h, + 'volume24h': volume24h, + 'status': status, + 'sort': sort, + }; + } + + /// 显示图标(Unicode 符号) + String get displayIcon { + const icons = { + 'BTC': '\u20BF', + 'ETH': '\u039E', + 'SOL': '\u25CD', + 'USDT': '\u20AE', + 'DOGE': '\uD83D\uDC15', + 'XRP': '\u2715', + 'BNB': '\u25B3', + 'ADA': '\u25C6', + }; + return icons[code] ?? '\u25CF'; + } + + /// 格式化价格显示 + String get formattedPrice { + if (price >= 1000) return price.toStringAsFixed(2); + if (price >= 1) return price.toStringAsFixed(4); + return price.toStringAsFixed(6); + } + + /// 格式化涨跌幅 + String get formattedChange { + final prefix = change24h >= 0 ? '+' : ''; + return '$prefix${change24h.toStringAsFixed(2)}%'; + } + + /// 是否上涨 + bool get isUp => change24h >= 0; + + /// 是否为实时价格 + bool get isRealtime => priceType == 1; +} diff --git a/flutter_monisuo/lib/data/models/order_models.dart b/flutter_monisuo/lib/data/models/order_models.dart new file mode 100644 index 0000000..d9b64e1 --- /dev/null +++ b/flutter_monisuo/lib/data/models/order_models.dart @@ -0,0 +1,139 @@ +/// 交易订单模型 +class OrderTrade { + final int id; + final String orderNo; + final int userId; + final String coinCode; + final int direction; // 1=买入, 2=卖出 + final String price; + final String quantity; + final String amount; + final int status; // 1=待处理, 2=已完成, 3=已取消 + final DateTime? createTime; + final DateTime? updateTime; + + OrderTrade({ + required this.id, + required this.orderNo, + required this.userId, + required this.coinCode, + required this.direction, + required this.price, + required this.quantity, + required this.amount, + required this.status, + this.createTime, + this.updateTime, + }); + + factory OrderTrade.fromJson(Map json) { + return OrderTrade( + id: json['id'] as int? ?? 0, + orderNo: json['orderNo'] as String? ?? '', + userId: json['userId'] as int? ?? 0, + coinCode: json['coinCode'] as String? ?? '', + direction: json['direction'] as int? ?? 1, + price: json['price']?.toString() ?? '0.00', + quantity: json['quantity']?.toString() ?? '0', + amount: json['amount']?.toString() ?? '0.00', + status: json['status'] as int? ?? 1, + createTime: json['createTime'] != null + ? DateTime.tryParse(json['createTime']) + : null, + updateTime: json['updateTime'] != null + ? DateTime.tryParse(json['updateTime']) + : null, + ); + } + + /// 方向文字 + String get directionText => direction == 1 ? '买入' : '卖出'; + + /// 状态文字 + String get statusText { + switch (status) { + case 1: + return '待处理'; + case 2: + return '已完成'; + case 3: + return '已取消'; + default: + return '未知'; + } + } + + /// 是否为买入 + bool get isBuy => direction == 1; +} + +/// 充提订单模型 +class OrderFund { + final int id; + final String orderNo; + final int userId; + final int orderType; // 1=充值, 2=提现 + final String amount; + final int status; // 1=待审核, 2=已通过, 3=已拒绝, 4=已取消 + final String remark; + final String? auditRemark; + final DateTime? createTime; + final DateTime? auditTime; + + OrderFund({ + required this.id, + required this.orderNo, + required this.userId, + required this.orderType, + required this.amount, + required this.status, + required this.remark, + this.auditRemark, + this.createTime, + this.auditTime, + }); + + factory OrderFund.fromJson(Map json) { + return OrderFund( + id: json['id'] as int? ?? 0, + orderNo: json['orderNo'] as String? ?? '', + userId: json['userId'] as int? ?? 0, + orderType: json['orderType'] as int? ?? 1, + amount: json['amount']?.toString() ?? '0.00', + status: json['status'] as int? ?? 1, + remark: json['remark']?.toString() ?? '', + auditRemark: json['auditRemark']?.toString(), + createTime: json['createTime'] != null + ? DateTime.tryParse(json['createTime']) + : null, + auditTime: json['auditTime'] != null + ? DateTime.tryParse(json['auditTime']) + : null, + ); + } + + /// 订单类型文字 + String get orderTypeText => orderType == 1 ? '充值' : '提现'; + + /// 状态文字 + String get statusText { + switch (status) { + case 1: + return '待审核'; + case 2: + return '已通过'; + case 3: + return '已拒绝'; + case 4: + return '已取消'; + default: + return '未知'; + } + } + + /// 是否为充值 + bool get isDeposit => orderType == 1; + + /// 是否可取消 + bool get canCancel => status == 1; +} diff --git a/flutter_monisuo/lib/data/models/user.dart b/flutter_monisuo/lib/data/models/user.dart new file mode 100644 index 0000000..f25c71e --- /dev/null +++ b/flutter_monisuo/lib/data/models/user.dart @@ -0,0 +1,80 @@ +/// 用户模型 +class User { + final int id; + final String username; + final String? nickname; + final String? avatar; + final String? phone; + final String? email; + final int kycStatus; + final int status; + final DateTime? lastLoginTime; + final DateTime? createTime; + + User({ + required this.id, + required this.username, + this.nickname, + this.avatar, + this.phone, + this.email, + required this.kycStatus, + required this.status, + this.lastLoginTime, + this.createTime, + }); + + factory User.fromJson(Map json) { + return User( + id: json['id'] as int? ?? 0, + username: json['username'] as String? ?? '', + nickname: json['nickname'] as String?, + avatar: json['avatar'] as String?, + phone: json['phone'] as String?, + email: json['email'] as String?, + kycStatus: json['kycStatus'] as int? ?? 0, + status: json['status'] as int? ?? 1, + lastLoginTime: json['lastLoginTime'] != null + ? DateTime.tryParse(json['lastLoginTime']) + : null, + createTime: json['createTime'] != null + ? DateTime.tryParse(json['createTime']) + : null, + ); + } + + Map toJson() { + return { + 'id': id, + 'username': username, + 'nickname': nickname, + 'avatar': avatar, + 'phone': phone, + 'email': email, + 'kycStatus': kycStatus, + 'status': status, + 'lastLoginTime': lastLoginTime?.toIso8601String(), + 'createTime': createTime?.toIso8601String(), + }; + } + + /// 获取头像显示文字(用户名首字母) + String get avatarText => + username.isNotEmpty ? username.substring(0, 1).toUpperCase() : 'U'; + + /// KYC 状态文字 + String get kycStatusText { + switch (kycStatus) { + case 0: + return '未认证'; + case 1: + return '审核中'; + case 2: + return '已认证'; + case 3: + return '认证失败'; + default: + return '未知'; + } + } +} diff --git a/flutter_monisuo/lib/data/services/asset_service.dart b/flutter_monisuo/lib/data/services/asset_service.dart new file mode 100644 index 0000000..53e6dfe --- /dev/null +++ b/flutter_monisuo/lib/data/services/asset_service.dart @@ -0,0 +1,86 @@ +import '../../core/constants/api_endpoints.dart'; +import '../../core/network/dio_client.dart'; +import '../models/account_models.dart'; + +/// 资产服务 +class AssetService { + final DioClient _client; + + AssetService(this._client); + + /// 获取资产总览 + Future> getOverview() async { + final response = await _client.get>( + ApiEndpoints.assetOverview, + ); + + if (response.success && response.data != null) { + return ApiResponse.success( + AssetOverview.fromJson(response.data!), + response.message, + ); + } + return ApiResponse.fail(response.message ?? '获取资产总览失败'); + } + + /// 获取资金账户 + Future> getFundAccount() async { + final response = await _client.get>( + ApiEndpoints.fundAccount, + ); + + if (response.success && response.data != null) { + return ApiResponse.success( + AccountFund.fromJson(response.data!), + response.message, + ); + } + return ApiResponse.fail(response.message ?? '获取资金账户失败'); + } + + /// 获取交易账户 + Future>> getTradeAccount() async { + final response = await _client.get>( + ApiEndpoints.tradeAccount, + ); + + if (response.success && response.data != null) { + final list = response.data!['list'] as List?; + final accounts = list?.map((e) => AccountTrade.fromJson(e as Map)).toList() ?? []; + return ApiResponse.success(accounts, response.message); + } + return ApiResponse.fail(response.message ?? '获取交易账户失败'); + } + + /// 资金划转 + Future> transfer({ + required int direction, + required String amount, + }) async { + return _client.post( + ApiEndpoints.transfer, + data: { + 'direction': direction, + 'amount': amount, + }, + ); + } + + /// 获取资金流水 + Future>> getFlow({ + int? flowType, + int pageNum = 1, + int pageSize = 20, + }) async { + final params = { + 'pageNum': pageNum, + 'pageSize': pageSize, + }; + if (flowType != null) params['flowType'] = flowType; + + return _client.get>( + ApiEndpoints.assetFlow, + queryParameters: params, + ); + } +} diff --git a/flutter_monisuo/lib/data/services/fund_service.dart b/flutter_monisuo/lib/data/services/fund_service.dart new file mode 100644 index 0000000..a3aa15a --- /dev/null +++ b/flutter_monisuo/lib/data/services/fund_service.dart @@ -0,0 +1,80 @@ +import '../../core/constants/api_endpoints.dart'; +import '../../core/network/dio_client.dart'; +import '../models/order_models.dart'; + +/// 充提服务 +class FundService { + final DioClient _client; + + FundService(this._client); + + /// 申请充值 + Future>> deposit({ + required String amount, + String? remark, + }) async { + return _client.post>( + ApiEndpoints.deposit, + data: { + 'amount': amount, + if (remark != null) 'remark': remark, + }, + ); + } + + /// 申请提现 + Future>> withdraw({ + required String amount, + String? remark, + }) async { + return _client.post>( + ApiEndpoints.withdraw, + data: { + 'amount': amount, + if (remark != null) 'remark': remark, + }, + ); + } + + /// 取消订单 + Future> cancelOrder(String orderNo) async { + return _client.post( + ApiEndpoints.cancelOrder, + data: {'orderNo': orderNo}, + ); + } + + /// 获取充提记录 + Future>> getOrders({ + int? type, + int pageNum = 1, + int pageSize = 20, + }) async { + final params = { + 'pageNum': pageNum, + 'pageSize': pageSize, + }; + if (type != null) params['type'] = type; + + return _client.get>( + ApiEndpoints.fundOrders, + queryParameters: params, + ); + } + + /// 获取充提订单详情 + Future> getOrderDetail(String orderNo) async { + final response = await _client.get>( + ApiEndpoints.fundOrders, + queryParameters: {'orderNo': orderNo}, + ); + + if (response.success && response.data != null) { + return ApiResponse.success( + OrderFund.fromJson(response.data!), + response.message, + ); + } + return ApiResponse.fail(response.message ?? '获取订单详情失败'); + } +} diff --git a/flutter_monisuo/lib/data/services/market_service.dart b/flutter_monisuo/lib/data/services/market_service.dart new file mode 100644 index 0000000..7e9981f --- /dev/null +++ b/flutter_monisuo/lib/data/services/market_service.dart @@ -0,0 +1,55 @@ +import '../../core/constants/api_endpoints.dart'; +import '../../core/network/dio_client.dart'; +import '../models/coin.dart'; + +/// 行情服务 +class MarketService { + final DioClient _client; + + MarketService(this._client); + + /// 获取币种列表 + Future>> getCoinList() async { + final response = await _client.get>( + ApiEndpoints.coinList, + ); + + if (response.success && response.data != null) { + final list = response.data!['list'] as List?; + final coins = list?.map((e) => Coin.fromJson(e as Map)).toList() ?? []; + return ApiResponse.success(coins, response.message); + } + return ApiResponse.fail(response.message ?? '获取币种列表失败'); + } + + /// 获取币种详情 + Future> getCoinDetail(String code) async { + final response = await _client.get>( + ApiEndpoints.coinDetail, + queryParameters: {'code': code}, + ); + + if (response.success && response.data != null) { + return ApiResponse.success( + Coin.fromJson(response.data!), + response.message, + ); + } + return ApiResponse.fail(response.message ?? '获取币种详情失败'); + } + + /// 搜索币种 + Future>> searchCoins(String keyword) async { + final response = await _client.get>( + ApiEndpoints.coinSearch, + queryParameters: {'keyword': keyword}, + ); + + if (response.success && response.data != null) { + final list = response.data!['list'] as List?; + final coins = list?.map((e) => Coin.fromJson(e as Map)).toList() ?? []; + return ApiResponse.success(coins, response.message); + } + return ApiResponse.fail(response.message ?? '搜索失败'); + } +} diff --git a/flutter_monisuo/lib/data/services/trade_service.dart b/flutter_monisuo/lib/data/services/trade_service.dart new file mode 100644 index 0000000..106b5e3 --- /dev/null +++ b/flutter_monisuo/lib/data/services/trade_service.dart @@ -0,0 +1,78 @@ +import '../../core/constants/api_endpoints.dart'; +import '../../core/network/dio_client.dart'; +import '../models/order_models.dart'; + +/// 交易服务 +class TradeService { + final DioClient _client; + + TradeService(this._client); + + /// 买入 + Future>> buy({ + required String coinCode, + required String price, + required String quantity, + }) async { + return _client.post>( + ApiEndpoints.buy, + data: { + 'coinCode': coinCode, + 'price': price, + 'quantity': quantity, + }, + ); + } + + /// 卖出 + Future>> sell({ + required String coinCode, + required String price, + required String quantity, + }) async { + return _client.post>( + ApiEndpoints.sell, + data: { + 'coinCode': coinCode, + 'price': price, + 'quantity': quantity, + }, + ); + } + + /// 获取交易记录 + Future>> getOrders({ + String? coinCode, + int? direction, + int pageNum = 1, + int pageSize = 20, + }) async { + final params = { + 'pageNum': pageNum, + 'pageSize': pageSize, + }; + if (coinCode != null) params['coinCode'] = coinCode; + if (direction != null) params['direction'] = direction; + + return _client.get>( + ApiEndpoints.tradeOrders, + queryParameters: params, + ); + } + + /// 获取订单详情 + Future> getOrderDetail(String orderNo) async { + final response = await _client.get>( + ApiEndpoints.tradeOrderDetail, + queryParameters: {'orderNo': orderNo}, + ); + + if (response.success && response.data != null) { + return ApiResponse.success( + OrderTrade.fromJson(response.data!), + response.message, + ); + } + return ApiResponse.fail(response.message ?? '获取订单详情失败'); + } +} diff --git a/flutter_monisuo/lib/data/services/user_service.dart b/flutter_monisuo/lib/data/services/user_service.dart new file mode 100644 index 0000000..0753110 --- /dev/null +++ b/flutter_monisuo/lib/data/services/user_service.dart @@ -0,0 +1,56 @@ +import '../../core/constants/api_endpoints.dart'; +import '../../core/network/dio_client.dart'; +import '../models/user.dart'; + +/// 用户服务 +class UserService { + final DioClient _client; + + UserService(this._client); + + /// 用户登录 + Future>> login( + String username, + String password, + ) async { + return _client.post>( + ApiEndpoints.login, + data: {'username': username, 'password': password}, + ); + } + + /// 用户注册 + Future>> register( + String username, + String password, + ) async { + return _client.post>( + ApiEndpoints.register, + data: {'username': username, 'password': password}, + ); + } + + /// 获取用户信息 + Future> getUserInfo() async { + return _client.get( + ApiEndpoints.userInfo, + fromJson: (data) => User.fromJson(data as Map), + ); + } + + /// 上传 KYC 资料 + Future> uploadKyc( + String idCardFront, + String idCardBack, + ) async { + return _client.post( + ApiEndpoints.kyc, + data: {'idCardFront': idCardFront, 'idCardBack': idCardBack}, + ); + } + + /// 退出登录 + Future> logout() async { + return _client.post(ApiEndpoints.logout); + } +} diff --git a/flutter_monisuo/lib/main.dart b/flutter_monisuo/lib/main.dart new file mode 100644 index 0000000..709282e --- /dev/null +++ b/flutter_monisuo/lib/main.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'core/constants/app_colors.dart'; +import 'core/network/dio_client.dart'; +import 'core/storage/local_storage.dart'; +import 'core/theme/app_theme.dart'; +import 'data/services/user_service.dart'; +import 'data/services/market_service.dart'; +import 'data/services/trade_service.dart'; +import 'data/services/asset_service.dart'; +import 'data/services/fund_service.dart'; +import 'providers/auth_provider.dart'; +import 'providers/market_provider.dart'; +import 'providers/asset_provider.dart'; +import 'ui/pages/auth/login_page.dart'; +import 'ui/pages/main/main_page.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // 初始化本地存储 + final prefs = await SharedPreferences.getInstance(); + await LocalStorage.init(); + + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + // 服务 + Provider(create: (_) => DioClient()), + ProxyProvider( + create: (_) => throw UnimplementedError(), + update: (_, client) => UserService(client), + ), + ProxyProvider( + create: (_) => throw UnimplementedError(), + update: (_, client) => MarketService(client), + ), + ProxyProvider( + create: (_) => throw UnimplementedError(), + update: (_, client) => TradeService(client), + ), + ProxyProvider( + create: (_) => throw UnimplementedError(), + update: (_, client) => AssetService(client), + ), + ProxyProvider( + create: (_) => throw UnimplementedError(), + update: (_, client) => FundService(client), + ), + // 状态管理 + ProxyProvider2( + create: (_) => throw UnimplementedError(), + update: (_, userService, __) => AuthProvider(userService), + ), + ProxyProvider( + create: (_) => throw UnimplementedError(), + update: (_, service) => MarketProvider(service), + ), + ProxyProvider2( + create: (_) => throw UnimplementedError(), + update: (_, assetService, fundService) => AssetProvider(assetService, fundService), + ), + ], + child: MaterialApp( + title: '模拟所', + debugShowCheckedModeBanner: false, + theme: AppTheme.darkTheme, + home: Consumer( + builder: (context, auth, _) { + if (auth.isLoading) { + return const Scaffold( + backgroundColor: AppColors.background, + body: Center( + child: CircularProgressIndicator(color: AppColors.primary), + ), + ); + } + if (auth.isLoggedIn) { + return const MainPage(); + } + return const LoginPage(); + }, + ), + ), + ); + } +} diff --git a/flutter_monisuo/lib/providers/asset_provider.dart b/flutter_monisuo/lib/providers/asset_provider.dart new file mode 100644 index 0000000..bf1969e --- /dev/null +++ b/flutter_monisuo/lib/providers/asset_provider.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import '../data/models/account_models.dart'; +import '../data/services/asset_service.dart'; +import '../data/services/fund_service.dart'; +import '../core/network/dio_client.dart'; + +/// 资产状态管理 +class AssetProvider extends ChangeNotifier { + final AssetService _assetService; + final FundService _fundService; + + AssetOverview? _overview; + AccountFund? _fundAccount; + List _tradeAccounts = []; + List _flows = []; + bool _isLoading = false; + bool _isLoadingFlows = false; + String? _error; + + AssetProvider(this._assetService, this._fundService); + + // Getters + AssetOverview? get overview => _overview; + AccountFund? get fundAccount => _fundAccount; + List get tradeAccounts => _tradeAccounts; + List get holdings => _tradeAccounts; + List get flows => _flows; + bool get isLoading => _isLoading; + bool get isLoadingFlows => _isLoadingFlows; + String? get error => _error; + + /// 加载资产总览 + Future loadOverview() async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + final response = await _assetService.getOverview(); + if (response.success) { + _overview = response.data; + } else { + _error = response.message; + } + } catch (e) { + _error = '加载失败: $e'; + } + + _isLoading = false; + notifyListeners(); + } + + /// 加载资金账户 + Future loadFundAccount() async { + try { + final response = await _assetService.getFundAccount(); + if (response.success) { + _fundAccount = response.data; + notifyListeners(); + } + } catch (_) { + // 忽略错误 + } + } + + /// 加载交易账户 + Future loadTradeAccount() async { + try { + final response = await _assetService.getTradeAccount(); + if (response.success) { + _tradeAccounts = response.data ?? []; + notifyListeners(); + } + } catch (_) { + // 忽略错误 + } + } + + /// 加载资金流水 + Future loadFlows({int? flowType, int pageNum = 1}) async { + _isLoadingFlows = true; + notifyListeners(); + + try { + final response = await _assetService.getFlow( + flowType: flowType, + pageNum: pageNum, + ); + if (response.success && response.data != null) { + final list = response.data!['list'] as List?; + _flows = list?.map((e) => AccountFlow.fromJson(e as Map)).toList() ?? []; + } + } catch (_) { + // 忽略错误 + } + + _isLoadingFlows = false; + notifyListeners(); + } + + /// 划转资金 + Future> transfer({ + required int direction, + required String amount, + }) async { + try { + final response = await _assetService.transfer( + direction: direction, + amount: amount, + ); + if (response.success) { + // 刷新数据 + await loadOverview(); + await loadFundAccount(); + await loadTradeAccount(); + } + return response; + } catch (e) { + return ApiResponse.fail('划转失败: $e'); + } + } + + /// 充值 + Future> deposit({required String amount, String? remark}) async { + try { + final response = await _fundService.deposit(amount: amount, remark: remark); + if (response.success) { + await loadOverview(); + await loadFundAccount(); + } + return response; + } catch (e) { + return ApiResponse.fail('充值申请失败: $e'); + } + } + + /// 提现 + Future> withdraw({required String amount, String? remark}) async { + try { + final response = await _fundService.withdraw(amount: amount, remark: remark); + if (response.success) { + await loadOverview(); + await loadFundAccount(); + } + return response; + } catch (e) { + return ApiResponse.fail('提现申请失败: $e'); + } + } + + /// 刷新所有资产数据 + Future refreshAll() async { + await Future.wait([ + loadOverview(), + loadFundAccount(), + loadTradeAccount(), + ]); + } +} diff --git a/flutter_monisuo/lib/providers/auth_provider.dart b/flutter_monisuo/lib/providers/auth_provider.dart new file mode 100644 index 0000000..b017755 --- /dev/null +++ b/flutter_monisuo/lib/providers/auth_provider.dart @@ -0,0 +1,151 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import '../core/network/dio_client.dart'; +import '../core/storage/local_storage.dart'; +import '../data/models/user.dart'; +import '../data/services/user_service.dart'; + +/// 认证状态管理 +class AuthProvider extends ChangeNotifier { + final UserService _userService; + + User? _user; + bool _isLoggedIn = false; + bool _isLoading = false; + String? _token; + + AuthProvider(this._userService) { + _checkAuth(); + } + + // Getters + User? get user => _user; + bool get isLoggedIn => _isLoggedIn; + bool get isLoading => _isLoading; + String? get token => _token; + + /// 检查登录状态 + Future _checkAuth() async { + _isLoading = true; + notifyListeners(); + + _token = LocalStorage.getToken(); + _isLoggedIn = _token != null && _token!.isNotEmpty; + + if (_isLoggedIn) { + final userJson = LocalStorage.getUserInfo(); + if (userJson != null) { + _user = User.fromJson(userJson); + } + } + + _isLoading = false; + notifyListeners(); + } + + /// 登录 + Future> login(String username, String password) async { + _isLoading = true; + notifyListeners(); + + try { + final response = await _userService.login(username, password); + + if (response.success && response.data != null) { + _token = response.data!['token'] as String?; + final userJson = response.data!['user'] as Map? ?? + response.data!['userInfo'] as Map?; + + if (_token != null) { + await LocalStorage.saveToken(_token!); + } + if (userJson != null) { + await LocalStorage.saveUserInfo(userJson); + _user = User.fromJson(userJson); + } + + _isLoggedIn = true; + notifyListeners(); + return ApiResponse.success(_user!, response.message); + } + + _isLoading = false; + notifyListeners(); + return ApiResponse.fail(response.message ?? '登录失败'); + } catch (e) { + _isLoading = false; + notifyListeners(); + return ApiResponse.fail('登录失败: $e'); + } + } + + /// 注册 + Future> register(String username, String password) async { + _isLoading = true; + notifyListeners(); + + try { + final response = await _userService.register(username, password); + + if (response.success && response.data != null) { + _token = response.data!['token'] as String?; + final userJson = response.data!['userInfo'] as Map?; + + if (_token != null) { + await LocalStorage.saveToken(_token!); + } + if (userJson != null) { + await LocalStorage.saveUserInfo(userJson); + _user = User.fromJson(userJson); + } + + _isLoggedIn = true; + notifyListeners(); + return ApiResponse.success(_user!, response.message); + } + + _isLoading = false; + notifyListeners(); + return ApiResponse.fail(response.message ?? '注册失败'); + } catch (e) { + _isLoading = false; + notifyListeners(); + return ApiResponse.fail('注册失败: $e'); + } + } + + /// 退出登录 + Future logout() async { + _isLoading = true; + notifyListeners(); + + try { + await _userService.logout(); + } catch (_) { + // 忽略退出登录的接口错误 + } + + await LocalStorage.clearUserData(); + _user = null; + _token = null; + _isLoggedIn = false; + _isLoading = false; + notifyListeners(); + } + + /// 刷新用户信息 + Future refreshUserInfo() async { + if (!_isLoggedIn) return; + + try { + final response = await _userService.getUserInfo(); + if (response.success && response.data != null) { + _user = response.data; + await LocalStorage.saveUserInfo(_user!.toJson()); + notifyListeners(); + } + } catch (_) { + // 忽略错误 + } + } +} diff --git a/flutter_monisuo/lib/providers/market_provider.dart b/flutter_monisuo/lib/providers/market_provider.dart new file mode 100644 index 0000000..2a6c530 --- /dev/null +++ b/flutter_monisuo/lib/providers/market_provider.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import '../data/models/coin.dart'; +import '../data/services/market_service.dart'; + +/// 行情状态管理 +class MarketProvider extends ChangeNotifier { + final MarketService _marketService; + + List _allCoins = []; + List _filteredCoins = []; + String _activeTab = 'all'; + String _searchKeyword = ''; + bool _isLoading = false; + String? _error; + + MarketProvider(this._marketService); + + // Getters + List get coins => _filteredCoins; + List get allCoins => _allCoins; + bool get isLoading => _isLoading; + String? get error => _error; + String get activeTab => _activeTab; + String get searchKeyword => _searchKeyword; + + /// 加载币种列表 + Future loadCoins() async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + final response = await _marketService.getCoinList(); + + if (response.success) { + _allCoins = response.data ?? []; + _filterCoins(); + } else { + _error = response.message; + } + } catch (e) { + _error = '加载失败: $e'; + } + + _isLoading = false; + notifyListeners(); + } + + /// 设置分类标签 + void setTab(String tab) { + _activeTab = tab; + _filterCoins(); + notifyListeners(); + } + + /// 搜索 + void search(String keyword) { + _searchKeyword = keyword; + _filterCoins(); + notifyListeners(); + } + + /// 清除搜索 + void clearSearch() { + _searchKeyword = ''; + _filterCoins(); + notifyListeners(); + } + + /// 筛选币种 + void _filterCoins() { + List result = List.from(_allCoins); + + // 按分类筛选 + if (_activeTab == 'realtime') { + result = result.where((c) => c.isRealtime).toList(); + } else if (_activeTab == 'hot') { + result = result.take(6).toList(); + } + + // 按关键词筛选 + if (_searchKeyword.isNotEmpty) { + final kw = _searchKeyword.toLowerCase(); + result = result.where((c) => + c.code.toLowerCase().contains(kw) || + c.name.toLowerCase().contains(kw)).toList(); + } + + _filteredCoins = result; + } + + /// 根据代码获取币种 + Coin? getCoinByCode(String code) { + try { + return _allCoins.firstWhere((c) => c.code == code); + } catch (_) { + return null; + } + } + + /// 刷新 + Future refresh() async { + await loadCoins(); + } +} diff --git a/flutter_monisuo/lib/routes/app_routes.dart b/flutter_monisuo/lib/routes/app_routes.dart new file mode 100644 index 0000000..da0e1d5 --- /dev/null +++ b/flutter_monisuo/lib/routes/app_routes.dart @@ -0,0 +1,9 @@ +/// 路由常量 +class AppRoutes { + AppRoutes._(); + + static const String splash = '/'; + static const String login = '/login'; + static const String register = '/register'; + static const String main = '/main'; +} diff --git a/flutter_monisuo/lib/ui/pages/asset/asset_page.dart b/flutter_monisuo/lib/ui/pages/asset/asset_page.dart new file mode 100644 index 0000000..49a99a3 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/asset/asset_page.dart @@ -0,0 +1,455 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../core/constants/app_colors.dart'; +import '../../../providers/asset_provider.dart'; + +/// 资产页面 +class AssetPage extends StatefulWidget { + const AssetPage({super.key}); + + @override + State createState() => _AssetPageState(); +} + +class _AssetPageState extends State with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + int _activeTab = 0; // 0=资金账户, 1=交易账户 + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadData(); + }); + } + + void _loadData() { + context.read().refreshAll(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: AppColors.background, + body: Consumer( + builder: (context, provider, _) { + return RefreshIndicator( + onRefresh: provider.refreshAll, + color: AppColors.primary, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildAssetCard(provider), + const SizedBox(height: 16), + _buildAccountTabs(), + const SizedBox(height: 16), + _activeTab == 0 + ? _buildFundAccount(provider) + : _buildTradeAccount(provider), + ], + ), + ), + ); + }, + ), + ); + } + + Widget _buildAssetCard(AssetProvider provider) { + final overview = provider.overview; + return Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [AppColors.primary, AppColors.primaryDark], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + const Text( + '总资产估值(USDT)', + style: TextStyle(fontSize: 14, color: Colors.white70), + ), + const SizedBox(height: 8), + Text( + overview?.totalAsset ?? '0.00', + style: const TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.trending_up, color: Colors.white70, size: 16), + const SizedBox(width: 4), + Text( + '总盈亏: ${overview?.totalProfit ?? '0.00'} USDT', + style: const TextStyle(fontSize: 14, color: Colors.white70), + ), + ], + ), + ], + ), + ); + } + + Widget _buildAccountTabs() { + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: AppColors.cardBackground, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => setState(() => _activeTab = 0), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: _activeTab == 0 ? AppColors.primary : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + '资金账户', + style: TextStyle( + color: _activeTab == 0 ? Colors.white : AppColors.textSecondary, + fontWeight: _activeTab == 0 ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () => setState(() => _activeTab = 1), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: _activeTab == 1 ? AppColors.primary : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + '交易账户', + style: TextStyle( + color: _activeTab == 1 ? Colors.white : AppColors.textSecondary, + fontWeight: _activeTab == 1 ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildFundAccount(AssetProvider provider) { + final fund = provider.fundAccount; + return Column( + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColors.cardBackground, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'USDT余额', + style: TextStyle(fontSize: 14, color: AppColors.textSecondary), + ), + const SizedBox(height: 8), + Text( + fund?.balance ?? '0.00', + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => _showDepositDialog(provider), + icon: const Icon(Icons.add, size: 18), + label: const Text('充值'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.success, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () => _showWithdrawDialog(provider), + icon: const Icon(Icons.remove, size: 18), + label: const Text('提现'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.warning, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () => _showTransferDialog(provider), + icon: const Icon(Icons.swap_horiz, size: 18), + label: const Text('划转'), + ), + ), + ], + ), + ], + ), + ), + ], + ); + } + + Widget _buildTradeAccount(AssetProvider provider) { + final holdings = provider.holdings; + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.cardBackground, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '持仓列表', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 16), + if (holdings.isEmpty) + const Center( + child: Padding( + padding: EdgeInsets.all(32), + child: Text( + '暂无持仓', + style: TextStyle(color: AppColors.textSecondary), + ), + ), + ) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: holdings.length, + separatorBuilder: (_, __) => const Divider(color: AppColors.border), + itemBuilder: (context, index) { + final holding = holdings[index]; + return ListTile( + contentPadding: EdgeInsets.zero, + leading: CircleAvatar( + backgroundColor: AppColors.primary.withOpacity(0.1), + child: Text( + holding.coinCode.substring(0, 1), + style: const TextStyle(color: AppColors.primary), + ), + ), + title: Text( + holding.coinCode, + style: const TextStyle( + color: AppColors.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text( + '数量: ${holding.quantity}', + style: const TextStyle(color: AppColors.textSecondary, fontSize: 12), + ), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${holding.currentValue} USDT', + style: const TextStyle(color: AppColors.textPrimary), + ), + Text( + holding.formattedProfitRate, + style: TextStyle( + color: holding.isProfit ? AppColors.up : AppColors.down, + fontSize: 12, + ), + ), + ], + ), + ); + }, + ), + ], + ), + ); + } + + void _showDepositDialog(AssetProvider provider) { + final controller = TextEditingController(); + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppColors.cardBackground, + title: const Text('充值', style: TextStyle(color: AppColors.textPrimary)), + content: TextField( + controller: controller, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + style: const TextStyle(color: AppColors.textPrimary), + decoration: const InputDecoration( + hintText: '请输入充值金额(USDT)', + hintStyle: TextStyle(color: AppColors.textHint), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () async { + Navigator.pop(context); + final response = await provider.deposit(amount: controller.text); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(response.message ?? (response.success ? '申请成功' : '申请失败'))), + ); + } + }, + child: const Text('确认'), + ), + ], + ), + ); + } + + void _showWithdrawDialog(AssetProvider provider) { + final controller = TextEditingController(); + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppColors.cardBackground, + title: const Text('提现', style: TextStyle(color: AppColors.textPrimary)), + content: TextField( + controller: controller, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + style: const TextStyle(color: AppColors.textPrimary), + decoration: const InputDecoration( + hintText: '请输入提现金额(USDT)', + hintStyle: TextStyle(color: AppColors.textHint), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () async { + Navigator.pop(context); + final response = await provider.withdraw(amount: controller.text); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(response.message ?? (response.success ? '申请成功' : '申请失败'))), + ); + } + }, + child: const Text('确认'), + ), + ], + ), + ); + } + + void _showTransferDialog(AssetProvider provider) { + final controller = TextEditingController(); + int direction = 1; // 1=资金转交易, 2=交易转资金 + showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setState) => AlertDialog( + backgroundColor: AppColors.cardBackground, + title: const Text('划转', style: TextStyle(color: AppColors.textPrimary)), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ChoiceChip( + label: const Text('资金→交易'), + selected: direction == 1, + onSelected: (v) => setState(() => direction = 1), + ), + const SizedBox(width: 8), + ChoiceChip( + label: const Text('交易→资金'), + selected: direction == 2, + onSelected: (v) => setState(() => direction = 2), + ), + ], + ), + const SizedBox(height: 16), + TextField( + controller: controller, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + style: const TextStyle(color: AppColors.textPrimary), + decoration: const InputDecoration( + hintText: '请输入划转金额(USDT)', + hintStyle: TextStyle(color: AppColors.textHint), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () async { + Navigator.pop(context); + final response = await provider.transfer( + direction: direction, + amount: controller.text, + ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(response.message ?? (response.success ? '划转成功' : '划转失败'))), + ); + } + }, + child: const Text('确认'), + ), + ], + ), + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/auth/login_page.dart b/flutter_monisuo/lib/ui/pages/auth/login_page.dart new file mode 100644 index 0000000..f6956a6 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/auth/login_page.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../core/constants/app_colors.dart'; +import '../../../providers/auth_provider.dart'; +import '../main/main_page.dart'; +import 'register_page.dart'; + +/// 登录页面 +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + final _formKey = GlobalKey(); + bool _obscurePassword = true; + + @override + void dispose() { + _usernameController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 60), + // Logo + const Center( + child: Text( + '模拟所', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + ), + ), + const SizedBox(height: 8), + const Center( + child: Text( + '虚拟货币模拟交易平台', + style: TextStyle( + fontSize: 14, + color: AppColors.textSecondary, + ), + ), + ), + const SizedBox(height: 48), + // 用户名输入 + TextFormField( + controller: _usernameController, + style: const TextStyle(color: AppColors.textPrimary), + decoration: InputDecoration( + hintText: '请输入用户名', + prefixIcon: const Icon(Icons.person_outline, color: AppColors.textSecondary), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '请输入用户名'; + } + return null; + }, + ), + const SizedBox(height: 16), + // 密码输入 + TextFormField( + controller: _passwordController, + obscureText: _obscurePassword, + style: const TextStyle(color: AppColors.textPrimary), + decoration: InputDecoration( + hintText: '请输入密码', + prefixIcon: const Icon(Icons.lock_outline, color: AppColors.textSecondary), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + color: AppColors.textSecondary, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '请输入密码'; + } + return null; + }, + ), + const SizedBox(height: 32), + // 登录按钮 + Consumer( + builder: (context, auth, _) { + return ElevatedButton( + onPressed: auth.isLoading ? null : _handleLogin, + child: auth.isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text('登录'), + ); + }, + ), + const SizedBox(height: 16), + // 注册链接 + Center( + child: TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const RegisterPage()), + ); + }, + child: const Text( + '还没有账号?立即注册', + style: TextStyle(fontSize: 14), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Future _handleLogin() async { + if (!_formKey.currentState!.validate()) return; + + final auth = context.read(); + final response = await auth.login( + _usernameController.text.trim(), + _passwordController.text, + ); + + if (response.success && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('登录成功')), + ); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const MainPage()), + ); + } else if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(response.message ?? '登录失败')), + ); + } + } +} diff --git a/flutter_monisuo/lib/ui/pages/auth/register_page.dart b/flutter_monisuo/lib/ui/pages/auth/register_page.dart new file mode 100644 index 0000000..b48a607 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/auth/register_page.dart @@ -0,0 +1,223 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../core/constants/app_colors.dart'; +import '../../../providers/auth_provider.dart'; +import '../main/main_page.dart'; + +/// 注册页面 +class RegisterPage extends StatefulWidget { + const RegisterPage({super.key}); + + @override + State createState() => _RegisterPageState(); +} + +class _RegisterPageState extends State { + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + final _formKey = GlobalKey(); + bool _obscurePassword = true; + bool _obscureConfirmPassword = true; + + @override + void dispose() { + _usernameController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary), + onPressed: () => Navigator.pop(context), + ), + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Logo + const Center( + child: Text( + '\u20BF', + style: TextStyle( + fontSize: 48, + color: AppColors.primary, + ), + ), + ), + const SizedBox(height: 16), + const Center( + child: Text( + '注册账号', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + ), + const SizedBox(height: 48), + // 用户名 + TextFormField( + controller: _usernameController, + style: const TextStyle(color: AppColors.textPrimary), + decoration: const InputDecoration( + hintText: '请输入账号(4-20位字母数字)', + prefixIcon: Icon(Icons.person_outline, color: AppColors.textSecondary), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '请输入账号'; + } + if (value.length < 4) { + return '账号至少4位'; + } + if (value.length > 20) { + return '账号最多20位'; + } + return null; + }, + ), + const SizedBox(height: 16), + // 密码 + TextFormField( + controller: _passwordController, + obscureText: _obscurePassword, + style: const TextStyle(color: AppColors.textPrimary), + decoration: InputDecoration( + hintText: '请输入密码(至少6位)', + prefixIcon: const Icon(Icons.lock_outline, color: AppColors.textSecondary), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + color: AppColors.textSecondary, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '请输入密码'; + } + if (value.length < 6) { + return '密码至少6位'; + } + return null; + }, + ), + const SizedBox(height: 16), + // 确认密码 + TextFormField( + controller: _confirmPasswordController, + obscureText: _obscureConfirmPassword, + style: const TextStyle(color: AppColors.textPrimary), + decoration: InputDecoration( + hintText: '请再次输入密码', + prefixIcon: const Icon(Icons.lock_outline, color: AppColors.textSecondary), + suffixIcon: IconButton( + icon: Icon( + _obscureConfirmPassword ? Icons.visibility_off : Icons.visibility, + color: AppColors.textSecondary, + ), + onPressed: () { + setState(() { + _obscureConfirmPassword = !_obscureConfirmPassword; + }); + }, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '请再次输入密码'; + } + if (value != _passwordController.text) { + return '两次密码不一致'; + } + return null; + }, + ), + const SizedBox(height: 32), + // 注册按钮 + Consumer( + builder: (context, auth, _) { + return ElevatedButton( + onPressed: auth.isLoading ? null : _handleRegister, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: auth.isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text('注 册', style: TextStyle(fontSize: 16)), + ); + }, + ), + const SizedBox(height: 16), + // 登录链接 + Center( + child: TextButton( + onPressed: () => Navigator.pop(context), + child: const Text( + '已有账号?立即登录', + style: TextStyle(fontSize: 14), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Future _handleRegister() async { + if (!_formKey.currentState!.validate()) return; + + final auth = context.read(); + final response = await auth.register( + _usernameController.text.trim(), + _passwordController.text, + ); + + if (response.success && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('注册成功')), + ); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const MainPage()), + ); + } else if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(response.message ?? '注册失败')), + ); + } + } +} diff --git a/flutter_monisuo/lib/ui/pages/home/home_page.dart b/flutter_monisuo/lib/ui/pages/home/home_page.dart new file mode 100644 index 0000000..7518908 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/home/home_page.dart @@ -0,0 +1,442 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../core/constants/app_colors.dart'; +import '../../../providers/asset_provider.dart'; +import '../../../providers/auth_provider.dart'; +import '../asset/asset_page.dart'; +import '../trade/trade_page.dart'; + +/// 首页 +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadData(); + }); + } + + void _loadData() { + final assetProvider = context.read(); + assetProvider.loadOverview(); + assetProvider.loadTradeAccount(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: AppColors.background, + body: Consumer( + builder: (context, provider, _) { + return RefreshIndicator( + onRefresh: () => provider.refreshAll(), + color: AppColors.primary, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + _buildHeader(), + const SizedBox(height: 20), + _buildAssetCard(provider), + const SizedBox(height: 16), + _buildQuickActions(), + const SizedBox(height: 24), + _buildHoldings(provider), + ], + ), + ), + ); + }, + ), + ); + } + + Widget _buildHeader() { + return Consumer( + builder: (context, auth, _) { + final user = auth.user; + return Row( + children: [ + CircleAvatar( + radius: 20, + backgroundColor: AppColors.primary.withOpacity(0.2), + child: Text( + user?.avatarText ?? 'U', + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '你好,${user?.username ?? '用户'}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 2), + const Text( + '欢迎来到模拟所', + style: TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + ), + ), + ], + ), + ), + ], + ); + }, + ); + } + + Widget _buildAssetCard(AssetProvider provider) { + final overview = provider.overview; + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [AppColors.primary, AppColors.primaryDark], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '总资产(USDT)', + style: TextStyle( + fontSize: 14, + color: Colors.white70, + ), + ), + const SizedBox(height: 8), + Text( + overview?.totalAsset ?? '0.00', + style: const TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildAssetItem('资金账户', overview?.fundBalance ?? '0.00'), + _buildAssetItem('交易账户', overview?.tradeBalance ?? '0.00'), + ], + ), + ], + ), + ); + } + + Widget _buildAssetItem(String label, String value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle(fontSize: 12, color: Colors.white70), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], + ); + } + + Widget _buildQuickActions() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.cardBackground, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildActionItem('充', '充值', AppColors.success, () => _showDeposit()), + _buildActionItem('提', '提现', AppColors.warning, () => _showWithdraw()), + _buildActionItem('转', '划转', AppColors.primary, () => _showTransfer()), + _buildActionItem('币', '交易', AppColors.info, () => _navigateToTrade()), + ], + ), + ); + } + + Widget _buildActionItem(String icon, String text, Color color, VoidCallback onTap) { + return GestureDetector( + onTap: onTap, + child: Column( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: color.withOpacity(0.15), + shape: BoxShape.circle, + ), + child: Center( + child: Text( + icon, + style: TextStyle( + fontSize: 18, + color: color, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(height: 8), + Text( + text, + style: const TextStyle( + fontSize: 12, + color: AppColors.textPrimary, + ), + ), + ], + ), + ); + } + + Widget _buildHoldings(AssetProvider provider) { + final holdings = provider.holdings; + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.cardBackground, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '我的持仓', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + Icon( + Icons.chevron_right, + color: AppColors.textSecondary, + size: 20, + ), + ], + ), + const SizedBox(height: 16), + if (holdings.isEmpty) + const Center( + child: Padding( + padding: EdgeInsets.all(32), + child: Column( + children: [ + Icon( + Icons.account_balance_wallet_outlined, + size: 48, + color: AppColors.textHint, + ), + SizedBox(height: 12), + Text( + '暂无持仓', + style: TextStyle( + color: AppColors.textSecondary, + fontSize: 14, + ), + ), + SizedBox(height: 4), + Text( + '快去交易吧~', + style: TextStyle( + color: AppColors.textHint, + fontSize: 12, + ), + ), + ], + ), + ), + ) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: holdings.length > 5 ? 5 : holdings.length, + separatorBuilder: (_, __) => const Divider(color: AppColors.border), + itemBuilder: (context, index) { + final holding = holdings[index]; + return _buildHoldingItem(holding); + }, + ), + ], + ), + ); + } + + Widget _buildHoldingItem(holding) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: AppColors.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(18), + ), + child: Center( + child: Text( + holding.coinCode.substring(0, 1), + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + holding.coinCode, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + Text( + holding.quantity, + style: const TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + ), + ), + ], + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${holding.currentValue} USDT', + style: const TextStyle( + color: AppColors.textPrimary, + fontWeight: FontWeight.w500, + ), + ), + Text( + holding.formattedProfitRate, + style: TextStyle( + color: holding.isProfit ? AppColors.up : AppColors.down, + fontSize: 12, + ), + ), + ], + ), + ], + ), + ); + } + + void _showDeposit() { + // 显示充值弹窗 + _showActionDialog('充值', '请输入充值金额(USDT)', (amount) { + context.read().deposit(amount: amount); + }); + } + + void _showWithdraw() { + // 显示提现弹窗 + _showActionDialog('提现', '请输入提现金额(USDT)', (amount) { + context.read().withdraw(amount: amount); + }); + } + + void _showTransfer() { + // 显示划转弹窗 + _showActionDialog('划转', '请输入划转金额(USDT)', (amount) { + context.read().transfer(direction: 1, amount: amount); + }); + } + + void _showActionDialog(String title, String hint, Function(String) onSubmit) { + final controller = TextEditingController(); + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppColors.cardBackground, + title: Text(title, style: const TextStyle(color: AppColors.textPrimary)), + content: TextField( + controller: controller, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + style: const TextStyle(color: AppColors.textPrimary), + decoration: InputDecoration( + hintText: hint, + hintStyle: const TextStyle(color: AppColors.textHint), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + TextButton( + onPressed: () { + onSubmit(controller.text); + Navigator.pop(context); + }, + child: const Text('确认'), + ), + ], + ), + ); + } + + void _navigateToTrade() { + // 切换到交易页 + } +} diff --git a/flutter_monisuo/lib/ui/pages/main/main_page.dart b/flutter_monisuo/lib/ui/pages/main/main_page.dart new file mode 100644 index 0000000..a32ac67 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/main/main_page.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import '../../../core/constants/app_colors.dart'; +import '../home/home_page.dart'; +import '../market/market_page.dart'; +import '../trade/trade_page.dart'; +import '../asset/asset_page.dart'; +import '../mine/mine_page.dart'; + +/// 主页面(包含底部导航) +class MainPage extends StatefulWidget { + const MainPage({super.key}); + + @override + State createState() => _MainPageState(); +} + +class _MainPageState extends State { + int _currentIndex = 0; + + final List _pages = [ + const HomePage(), + const MarketPage(), + const TradePage(), + const AssetPage(), + const MinePage(), + ]; + + final List<_TabItem> _tabs = [ + _TabItem('首页', Icons.home_outlined, Icons.home), + _TabItem('行情', Icons.show_chart_outlined, Icons.show_chart), + _TabItem('交易', Icons.swap_horiz_outlined, Icons.swap_horiz), + _TabItem('资产', Icons.account_balance_wallet_outlined, Icons.account_balance_wallet), + _TabItem('我的', Icons.person_outline, Icons.person), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: IndexedStack( + index: _currentIndex, + children: _pages, + ), + bottomNavigationBar: _buildBottomNav(), + ); + } + + Widget _buildBottomNav() { + return Container( + decoration: const BoxDecoration( + color: AppColors.cardBackground, + border: Border(top: BorderSide(color: AppColors.border)), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: _tabs.asMap().entries.map((entry) { + final index = entry.key; + final tab = entry.value; + final isSelected = index == _currentIndex; + + return GestureDetector( + onTap: () => setState(() => _currentIndex = index), + behavior: HitTestBehavior.opaque, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isSelected ? tab.selectedIcon : tab.icon, + color: isSelected ? AppColors.primary : AppColors.textSecondary, + size: 24, + ), + const SizedBox(height: 4), + Text( + tab.label, + style: TextStyle( + fontSize: 12, + color: isSelected ? AppColors.primary : AppColors.textSecondary, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + ), + ); + } +} + +class _TabItem { + final String label; + final IconData icon; + final IconData selectedIcon; + + _TabItem(this.label, this.icon, this.selectedIcon); +} + +/// IndexedStack 用于保持页面状态 +class IndexedStack extends StatefulWidget { + final int index; + final List children; + + const IndexedStack({ + super.key, + required this.index, + required this.children, + }); + + @override + State createState() => _IndexedStackState(); +} + +class _IndexedStackState extends State { + @override + Widget build(BuildContext context) { + return Stack( + children: widget.children.asMap().entries.map((entry) { + return Positioned.fill( + child: Offstage( + offstage: entry.key != widget.index, + child: TickerMode( + enabled: entry.key == widget.index, + child: entry.value, + ), + ), + ); + }).toList(), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/market/market_page.dart b/flutter_monisuo/lib/ui/pages/market/market_page.dart new file mode 100644 index 0000000..7e8516b --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/market/market_page.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../core/constants/app_colors.dart'; +import '../../../data/models/coin.dart'; +import '../../../providers/market_provider.dart'; + +/// 行情页面 +class MarketPage extends StatefulWidget { + const MarketPage({super.key}); + + @override + State createState() => _MarketPageState(); +} + +class _MarketPageState extends State with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + final _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().loadCoins(); + }); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: AppColors.background, + body: Consumer( + builder: (context, provider, _) { + return Column( + children: [ + _buildSearchBar(provider), + _buildTabs(provider), + Expanded( + child: _buildCoinList(provider), + ), + ], + ); + }, + ), + ); + } + + Widget _buildSearchBar(MarketProvider provider) { + return Padding( + padding: const EdgeInsets.all(16), + child: TextField( + controller: _searchController, + style: const TextStyle(color: AppColors.textPrimary), + onChanged: provider.search, + decoration: InputDecoration( + hintText: '搜索币种...', + prefixIcon: const Icon(Icons.search, color: AppColors.textSecondary), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear, color: AppColors.textSecondary), + onPressed: () { + _searchController.clear(); + provider.clearSearch(); + }, + ) + : null, + ), + ), + ); + } + + Widget _buildTabs(MarketProvider provider) { + final tabs = [ + {'key': 'all', 'label': '全部'}, + {'key': 'realtime', 'label': '实时'}, + {'key': 'hot', 'label': '热门'}, + ]; + + return Container( + height: 44, + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: tabs.map((tab) { + final isActive = provider.activeTab == tab['key']; + return GestureDetector( + onTap: () => provider.setTab(tab['key']!), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: isActive ? AppColors.primary : AppColors.cardBackground, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + tab['label']!, + style: TextStyle( + color: isActive ? Colors.white : AppColors.textSecondary, + fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + ); + }).toList(), + ), + ); + } + + Widget _buildCoinList(MarketProvider provider) { + if (provider.isLoading) { + return const Center( + child: CircularProgressIndicator(color: AppColors.primary), + ); + } + + if (provider.error != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(provider.error!, style: const TextStyle(color: AppColors.error)), + const SizedBox(height: 16), + ElevatedButton( + onPressed: provider.loadCoins, + child: const Text('重试'), + ), + ], + ), + ); + } + + final coins = provider.coins; + if (coins.isEmpty) { + return const Center( + child: Text('暂无数据', style: TextStyle(color: AppColors.textSecondary)), + ); + } + + return RefreshIndicator( + onRefresh: provider.refresh, + color: AppColors.primary, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: coins.length, + itemBuilder: (context, index) => _buildCoinItem(coins[index]), + ), + ); + } + + Widget _buildCoinItem(Coin coin) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.cardBackground, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + // 图标 + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: AppColors.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(22), + ), + child: Center( + child: Text( + coin.displayIcon, + style: const TextStyle( + fontSize: 20, + color: AppColors.primary, + ), + ), + ), + ), + const SizedBox(width: 12), + // 名称 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${coin.code}/USDT', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + Text( + coin.name, + style: const TextStyle( + fontSize: 12, + color: AppColors.textSecondary, + ), + ), + ], + ), + ), + // 涨跌幅 + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: coin.isUp ? AppColors.up.withOpacity(0.2) : AppColors.down.withOpacity(0.2), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + coin.formattedChange, + style: TextStyle( + color: coin.isUp ? AppColors.up : AppColors.down, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/mine/mine_page.dart b/flutter_monisuo/lib/ui/pages/mine/mine_page.dart new file mode 100644 index 0000000..1a96982 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/mine/mine_page.dart @@ -0,0 +1,240 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../core/constants/app_colors.dart'; +import '../../../providers/auth_provider.dart'; + +/// 我的页面 +class MinePage extends StatefulWidget { + const MinePage({super.key}); + + @override + State createState() => _MinePageState(); +} + +class _MinePageState extends State with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: AppColors.background, + body: Consumer( + builder: (context, auth, _) { + final user = auth.user; + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildUserCard(user), + const SizedBox(height: 16), + _buildMenuList(context, auth), + const SizedBox(height: 24), + _buildLogoutButton(context, auth), + ], + ), + ); + }, + ), + ); + } + + Widget _buildUserCard(user) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColors.cardBackground, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + CircleAvatar( + radius: 32, + backgroundColor: AppColors.primary.withOpacity(0.2), + child: Text( + user?.avatarText ?? 'U', + style: const TextStyle( + fontSize: 24, + color: AppColors.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + user?.username ?? '未登录', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: AppColors.primary.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '普通用户', + style: const TextStyle( + fontSize: 12, + color: AppColors.primary, + ), + ), + ), + ], + ), + ), + const Icon(Icons.chevron_right, color: AppColors.textSecondary), + ], + ), + ); + } + + Widget _buildMenuList(BuildContext context, AuthProvider auth) { + return Container( + decoration: BoxDecoration( + color: AppColors.cardBackground, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + _buildMenuItem(Icons.verified_user, '实名认证', () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('功能开发中')), + }), + const Divider(color: AppColors.border, height: 1), + _buildMenuItem(Icons.security, '安全设置', () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('功能开发中'))); + }), + const Divider(color: AppColors.border, height: 1), + _buildMenuItem(Icons.info_outline, '关于我们', () { + showAboutDialog(context); + }), + ], + ), + ); + } + + Widget _buildMenuItem(IconData icon, String title, VoidCallback onTap) { + return ListTile( + leading: Icon(icon, color: AppColors.primary), + title: Text( + title, + style: const TextStyle(color: AppColors.textPrimary), + ), + trailing: const Icon(Icons.chevron_right, color: AppColors.textSecondary), + onTap: onTap, + ); + } + + Widget _buildLogoutButton(BuildContext context, AuthProvider auth) { + return Container( + width: double.infinity, + height: 48, + decoration: BoxDecoration( + color: AppColors.error.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: TextButton( + onPressed: () => _showLogoutDialog(context, auth), + child: const Text( + '退出登录', + style: TextStyle( + color: AppColors.error, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ); + } + + void _showLogoutDialog(BuildContext context, AuthProvider auth) { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppColors.cardBackground, + title: const Text('确认退出', style: TextStyle(color: AppColors.textPrimary)), + content: const Text( + '确定要退出登录吗?', + style: TextStyle(color: AppColors.textSecondary), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.error, + ), + onPressed: () async { + Navigator.pop(context); + await auth.logout(); + if (context.mounted) { + Navigator.pushReplacementNamed(context, '/login'); + } + }, + child: const Text('退出'), + ), + ], + ), + ); + } + + void showAboutDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppColors.cardBackground, + title: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.primary.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: const Center( + child: Text('₿', style: TextStyle(fontSize: 20, color: AppColors.primary)), + ), + ), + const SizedBox(width: 12), + const Text('模拟所', style: TextStyle(color: AppColors.textPrimary)), + ], + ), + content: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '虚拟货币模拟交易平台', + style: TextStyle(color: AppColors.textSecondary), + ), + SizedBox(height: 16), + Text( + '版本: 1.0.0', + style: TextStyle(color: AppColors.textHint, fontSize: 12), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('确定'), + ), + ], + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/trade/trade_page.dart b/flutter_monisuo/lib/ui/pages/trade/trade_page.dart new file mode 100644 index 0000000..c3f0469 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/trade/trade_page.dart @@ -0,0 +1,316 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../core/constants/app_colors.dart'; +import '../../../data/models/coin.dart'; +import '../../../providers/market_provider.dart'; +import '../../../providers/asset_provider.dart'; + +/// 交易页面 +class TradePage extends StatefulWidget { + const TradePage({super.key}); + + @override + State createState() => _TradePageState(); +} + +class _TradePageState extends State with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + int _tradeType = 0; // 0=买入, 1=卖出 + Coin? _selectedCoin; + final _priceController = TextEditingController(); + final _quantityController = TextEditingController(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadData(); + }); + } + + void _loadData() { + context.read().loadCoins(); + context.read().loadOverview(); + } + + @override + void dispose() { + _priceController.dispose(); + _quantityController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: AppColors.background, + body: Consumer2( + builder: (context, market, asset, _) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildCoinSelector(market), + const SizedBox(height: 16), + _buildPriceCard(), + const SizedBox(height: 16), + _buildTradeForm(asset), + const SizedBox(height: 16), + _buildTradeButton(), + ], + ), + ); + }, + ), + ); + } + + Widget _buildCoinSelector(MarketProvider market) { + final coins = market.allCoins; + if (_selectedCoin == null && coins.isNotEmpty) { + _selectedCoin = coins.first; + _priceController.text = _selectedCoin!.formattedPrice; + } + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.cardBackground, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: AppColors.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(22), + ), + child: Center( + child: Text( + _selectedCoin?.displayIcon ?? '?', + style: const TextStyle(fontSize: 20, color: AppColors.primary), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _selectedCoin != null ? '${_selectedCoin!.code}/USDT' : '选择币种', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 4), + Text( + _selectedCoin != null ? _selectedCoin!.name : '点击选择交易对', + style: const TextStyle(fontSize: 12, color: AppColors.textSecondary), + ), + ], + ), + ), + const Icon(Icons.chevron_right, color: AppColors.textSecondary), + ], + ), + ); + } + + Widget _buildPriceCard() { + if (_selectedCoin == null) { + return const SizedBox.shrink(); + } + + final coin = _selectedCoin!; + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.cardBackground, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('最新价', style: TextStyle(fontSize: 12, color: AppColors.textSecondary)), + const SizedBox(height: 4), + Text( + '\$${coin.formattedPrice}', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + ], + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: coin.isUp ? AppColors.up.withOpacity(0.2) : AppColors.down.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + coin.formattedChange, + style: TextStyle( + fontSize: 16, + color: coin.isUp ? AppColors.up : AppColors.down, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildTradeForm(AssetProvider asset) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.cardBackground, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + // 买入/卖出切换 + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => setState(() => _tradeType = 0), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: _tradeType == 0 ? AppColors.up : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + '买入', + style: TextStyle( + color: _tradeType == 0 ? Colors.white : AppColors.up, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: GestureDetector( + onTap: () => setState(() => _tradeType = 1), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: _tradeType == 1 ? AppColors.down : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + '卖出', + style: TextStyle( + color: _tradeType == 1 ? Colors.white : AppColors.down, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 20), + // 价格输入 + TextField( + controller: _priceController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + style: const TextStyle(color: AppColors.textPrimary), + decoration: const InputDecoration( + labelText: '价格(USDT)', + suffixText: 'USDT', + ), + ), + const SizedBox(height: 12), + // 数量输入 + TextField( + controller: _quantityController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + style: const TextStyle(color: AppColors.textPrimary), + decoration: const InputDecoration( + labelText: '数量', + suffixText: _selectedCoin?.code ?? '', + ), + ), + const SizedBox(height: 12), + // 交易金额 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('交易金额', style: TextStyle(color: AppColors.textSecondary)), + Text( + '${_calculateAmount()} USDT', + style: const TextStyle(color: AppColors.textPrimary, fontWeight: FontWeight.w600), + ), + ], + ), + const SizedBox(height: 8), + // 可用余额 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('可用', style: TextStyle(color: AppColors.textSecondary)), + Text( + '${asset.overview?.tradeBalance ?? '0.00'} USDT', + style: const TextStyle(color: AppColors.textSecondary), + ), + ], + ), + ], + ), + ); + } + + String _calculateAmount() { + final price = double.tryParse(_priceController.text) ?? 0; + final quantity = double.tryParse(_quantityController.text) ?? 0; + return (price * quantity).toStringAsFixed(2); + } + + Widget _buildTradeButton() { + final isBuy = _tradeType == 0; + return Container( + width: double.infinity, + height: 48, + decoration: BoxDecoration( + gradient: isBuy ? AppColors.buyGradient : AppColors.sellGradient, + borderRadius: BorderRadius.circular(12), + ), + child: Center( + child: Text( + isBuy ? '买入 ${_selectedCoin?.code ?? ''}' : '卖出 ${_selectedCoin?.code ?? ''}', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ); + } +} diff --git a/flutter_monisuo/pubspec.yaml b/flutter_monisuo/pubspec.yaml new file mode 100644 index 0000000..ca345b2 --- /dev/null +++ b/flutter_monisuo/pubspec.yaml @@ -0,0 +1,52 @@ +name: flutter_monisuo +description: 模拟所 - 虚拟货币模拟交易平台 +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + + # 状态管理 + provider: ^6.1.1 + + # 网络请求 + dio: ^5.4.0 + + # 本地存储 + shared_preferences: ^2.2.2 + + # 路由管理 + go_router: ^13.0.0 + + # UI组件 + flutter_svg: ^2.0.9 + shimmer: ^3.0.0 + pull_to_refresh: ^2.0.0 + + # 工具类 + intl: ^0.18.1 + decimal: ^2.3.3 + + # 图标 + cupertino_icons: ^1.0.6 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.1 + +flutter: + uses-material-design: true + + assets: + - assets/images/ + - assets/icons/tabbar/ + + fonts: + - family: AppIcons + fonts: + - asset: assets/fonts/AppIcons.ttf