refactor: 将前端从 uni-app x 重构为 Flutter
变更内容: - 删除 uni-app x 项目 (app/ 目录) - 新增 Flutter 项目 (flutter_monisuo/ 目录) - 新增部署脚本 (deploy/ 目录) Flutter 项目功能: - 用户登录/注册 - 首页资产概览 - 行情币种列表 - 交易买卖操作 - 资产账户管理 - 充值/提现/划转 - 深色主题 - JWT Token 认证 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
80
flutter_monisuo/lib/core/constants/api_endpoints.dart
Normal file
80
flutter_monisuo/lib/core/constants/api_endpoints.dart
Normal file
@@ -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';
|
||||
}
|
||||
62
flutter_monisuo/lib/core/constants/app_colors.dart
Normal file
62
flutter_monisuo/lib/core/constants/app_colors.dart
Normal file
@@ -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,
|
||||
);
|
||||
}
|
||||
155
flutter_monisuo/lib/core/network/dio_client.dart
Normal file
155
flutter_monisuo/lib/core/network/dio_client.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../storage/local_storage.dart';
|
||||
import 'api_exception.dart';
|
||||
|
||||
/// API 响应模型
|
||||
class ApiResponse<T> {
|
||||
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<String, dynamic> 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<ApiResponse<T>> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
T Function(dynamic)? fromJson,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get(path, queryParameters: queryParameters);
|
||||
return _handleResponse<T>(response, fromJson);
|
||||
} on DioException catch (e) {
|
||||
return _handleError<T>(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// POST 请求
|
||||
Future<ApiResponse<T>> post<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
T Function(dynamic)? fromJson,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(path, data: data);
|
||||
return _handleResponse<T>(response, fromJson);
|
||||
} on DioException catch (e) {
|
||||
return _handleError<T>(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理响应
|
||||
ApiResponse<T> _handleResponse<T>(
|
||||
Response response,
|
||||
T Function(dynamic)? fromJson,
|
||||
) {
|
||||
final data = response.data;
|
||||
if (data is Map<String, dynamic>) {
|
||||
return ApiResponse.fromJson(data, fromJson);
|
||||
}
|
||||
return ApiResponse.fail('响应数据格式错误');
|
||||
}
|
||||
|
||||
/// 处理错误
|
||||
ApiResponse<T> _handleError<T>(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);
|
||||
}
|
||||
}
|
||||
101
flutter_monisuo/lib/core/storage/local_storage.dart
Normal file
101
flutter_monisuo/lib/core/storage/local_storage.dart
Normal file
@@ -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<void> 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<void> saveToken(String token) async {
|
||||
await prefs.setString(_tokenKey, token);
|
||||
}
|
||||
|
||||
/// 获取 Token
|
||||
static String? getToken() {
|
||||
return prefs.getString(_tokenKey);
|
||||
}
|
||||
|
||||
/// 移除 Token
|
||||
static Future<void> removeToken() async {
|
||||
await prefs.remove(_tokenKey);
|
||||
}
|
||||
|
||||
/// 是否已登录
|
||||
static bool get isLoggedIn => getToken() != null && getToken()!.isNotEmpty;
|
||||
|
||||
// ==================== 用户信息管理 ====================
|
||||
|
||||
/// 保存用户信息
|
||||
static Future<void> saveUserInfo(Map<String, dynamic> userInfo) async {
|
||||
await prefs.setString(_userInfoKey, jsonEncode(userInfo));
|
||||
}
|
||||
|
||||
/// 获取用户信息
|
||||
static Map<String, dynamic>? getUserInfo() {
|
||||
final str = prefs.getString(_userInfoKey);
|
||||
if (str == null) return null;
|
||||
try {
|
||||
return jsonDecode(str) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 移除用户信息
|
||||
static Future<void> removeUserInfo() async {
|
||||
await prefs.remove(_userInfoKey);
|
||||
}
|
||||
|
||||
// ==================== 通用方法 ====================
|
||||
|
||||
/// 保存字符串
|
||||
static Future<void> setString(String key, String value) async {
|
||||
await prefs.setString(key, value);
|
||||
}
|
||||
|
||||
/// 获取字符串
|
||||
static String? getString(String key) {
|
||||
return prefs.getString(key);
|
||||
}
|
||||
|
||||
/// 保存布尔值
|
||||
static Future<void> setBool(String key, bool value) async {
|
||||
await prefs.setBool(key, value);
|
||||
}
|
||||
|
||||
/// 获取布尔值
|
||||
static bool? getBool(String key) {
|
||||
return prefs.getBool(key);
|
||||
}
|
||||
|
||||
/// 清除所有数据
|
||||
static Future<void> clearAll() async {
|
||||
await prefs.clear();
|
||||
}
|
||||
|
||||
/// 清除用户数据(退出登录时调用)
|
||||
static Future<void> clearUserData() async {
|
||||
await removeToken();
|
||||
await removeUserInfo();
|
||||
}
|
||||
}
|
||||
149
flutter_monisuo/lib/core/theme/app_theme.dart
Normal file
149
flutter_monisuo/lib/core/theme/app_theme.dart
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user