初始化

This commit is contained in:
zhu
2026-03-10 13:36:40 +08:00
commit b03e64957c
111 changed files with 4536 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
class Config {
///获取环境
static String getEnv() {
const env = String.fromEnvironment('ENV', defaultValue: 'dev');
return env;
}
///获取接口地址
static String baseUrl() {
if (getEnv() == 'dev') {
return 'https://mindapp.test.tuzuu.com/api';
} else {
return 'https://mindapp.cells.org.cn/api';
}
}
}

View File

View File

@@ -0,0 +1,30 @@
import 'dart:async';
import 'global_event.dart';
/// 全局事件总线
class EventBus {
EventBus._();
static final _instance = EventBus._();
factory EventBus() => _instance;
// StreamController广播模式可以让多个地方监听
final StreamController<GlobalEvent> _controller = StreamController.broadcast();
/// 发送事件
void publish(GlobalEvent event) {
_controller.add(event);
}
/// 监听事件
Stream<GlobalEvent> get stream => _controller.stream;
/// 关闭流(一般应用生命周期结束时调用)
void dispose() {
_controller.close();
}
}

View File

@@ -0,0 +1,16 @@
import 'package:app/data/models/common/version_dto.dart';
///基类
abstract class GlobalEvent {
const GlobalEvent();
}
///重新登录
class UnauthorizedEvent extends GlobalEvent {}
///版本更新
class VersionUpdateEvent extends GlobalEvent {
final VersionDto version;
const VersionUpdateEvent(this.version);
}

View File

@@ -0,0 +1,65 @@
import 'package:app/data/models/api_dto.dart';
import 'package:app/data/repository/auto_repo.dart';
import 'package:dio/dio.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import '../event/event_bus.dart';
import '../event/global_event.dart';
///请求拦截器
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
String? token = await AuthRepo.getToken();
options.headers['Authorization'] = 'Bearer $token';
return handler.next(options);
}
///响应拦截器
void onResponse(Response<dynamic> response, ResponseInterceptorHandler handler) async {
var apiResponse = ApiDto.fromJson(response.data);
if (apiResponse.code == 1) {
response.data = apiResponse.data;
handler.next(response);
} else if (apiResponse.code == 401) {
final bus = EventBus();
bus.publish(UnauthorizedEvent());
handler.next(response);
} else {
handler.reject(
DioException(
requestOptions: response.requestOptions,
response: response,
error: {'code': apiResponse.code, 'message': apiResponse.message},
),
);
showError(apiResponse.message);
}
}
///错误响应
void onError(DioException e, ErrorInterceptorHandler handler) async {
var title = "";
if (e.type == DioExceptionType.connectionTimeout) {
title = "请求超时";
} else if (e.type == DioExceptionType.badResponse) {
if (e.response?.statusCode == 401) {
final bus = EventBus();
bus.publish(UnauthorizedEvent());
title = "登录信息已失效,请重新登录";
} else if (e.response?.statusCode == 404) {
title = "接口404不存在";
} else {
title = "500";
}
} else if (e.type == DioExceptionType.connectionError) {
title = "网络连接失败";
} else {
title = "异常其他错误";
}
showError(title);
handler.next(e);
}
///显示错误信息
void showError(String message) {
EasyLoading.showError(message);
}

View File

@@ -0,0 +1,43 @@
import 'package:dio/dio.dart';
import '../config/global.dart';
import 'interceptor.dart';
class Request {
static Dio _dio = Dio();
//返回单例
factory Request() {
return Request._instance();
}
//初始化
Request._instance() {
//创建基本配置
final BaseOptions options = BaseOptions(
baseUrl: Config.baseUrl(),
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
);
_dio = Dio(options);
_dio.interceptors.add(InterceptorsWrapper(
onRequest: onRequest,
onResponse: onResponse,
onError: onError,
));
}
///get请求
Future<T> get<T>(String path, [Map<String, dynamic>? params]) async {
var res = await _dio.get(path, queryParameters: params);
return res.data;
}
///post请求
Future<T> post<T>(String path, Object? data) async {
var res = await _dio.post(path, data: data);
return res.data;
}
}

View File

@@ -0,0 +1,37 @@
import 'dart:ui';
abstract class AppColorsBase {
/// 品牌主色
Color get primaryStart;
Color get primaryEnd;
// 灰度
Color get textPrimary;
Color get textSecondary;
Color get textTertiary;
// 状态颜色
Color get success;
Color get warning;
Color get info;
Color get danger;
// 容器色
Color get surfaceContainerLowest;
Color get surfaceContainerLow;
Color get surfaceContainer;
Color get surfaceContainerHigh; //白色卡片 / item
//阴影颜色
Color get shadow;
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'app_colors_base.dart'; // 假设你之前定义了 AppColorsBase
TextTheme buildTextTheme(AppColorsBase colors) {
return TextTheme(
// 标题层级
titleLarge: TextStyle(
fontSize: 22, // 比正文大6
fontWeight: FontWeight.w700,
color: colors.textPrimary,
),
titleMedium: TextStyle(
fontSize: 20, // 比正文大4
fontWeight: FontWeight.w700,
color: colors.textPrimary,
),
titleSmall: TextStyle(
fontSize: 18, // 比正文大2
fontWeight: FontWeight.w700,
color: colors.textPrimary,
),
// 正文字体
bodyLarge: TextStyle(
fontSize: 18, // 稍大正文
color: colors.textPrimary,
),
bodyMedium: TextStyle(
fontSize: 16, // 正文标准
color: colors.textPrimary,
),
bodySmall: TextStyle(
fontSize: 14, // 辅助正文
color: colors.textPrimary,
),
// 标签/提示文字
labelLarge: TextStyle(
fontSize: 14, // 比正文小一点
fontWeight: FontWeight.w500,
color: colors.textSecondary,
),
labelMedium: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: colors.textSecondary,
),
labelSmall: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: colors.textSecondary,
),
);
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'app_colors_base.dart';
@immutable
class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
final AppColorsBase baseTheme;
const AppThemeExtension({required this.baseTheme});
@override
ThemeExtension<AppThemeExtension> copyWith({
AppColorsBase? baseTheme,
}) {
return AppThemeExtension(
baseTheme: baseTheme ?? this.baseTheme,
);
}
@override
AppThemeExtension lerp(AppThemeExtension? other, double t) {
if (other is! AppThemeExtension) return this;
return t < 0.5 ? this : other; // 或者 this/base 混合逻辑
}
}
extension AppThemeExt on BuildContext {
AppThemeExtension get themeEx => Theme.of(this).extension<AppThemeExtension>()!;
Gradient get primaryGradient => LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [themeEx.baseTheme.primaryStart, themeEx.baseTheme.primaryEnd],
);
Color get primaryStart => themeEx.baseTheme.primaryStart;
Color get primaryEnd => themeEx.baseTheme.primaryEnd;
///主题
Color get success => themeEx.baseTheme.success;
Color get warning => themeEx.baseTheme.warning;
Color get danger => themeEx.baseTheme.danger;
Color get info => themeEx.baseTheme.info;
//字体灰度
Color get textSecondary => themeEx.baseTheme.textSecondary;
Color get textTertiary => themeEx.baseTheme.textTertiary;
double get pagePadding => 12;
}

42
lib/core/theme/theme.dart Normal file
View File

@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'base/app_colors_base.dart';
import 'base/app_text_style.dart';
import 'base/app_theme_ext.dart';
export 'themes/light_theme.dart';
export 'base/app_theme_ext.dart';
class AppTheme {
static ThemeData createTheme(AppColorsBase themeBase) {
final textTheme = buildTextTheme(themeBase);
return ThemeData(
useMaterial3: true,
fontFamily: "资源圆体",
scaffoldBackgroundColor: themeBase.surfaceContainerHigh,
colorScheme: ColorScheme.fromSeed(
primary: themeBase.primaryStart,
seedColor: themeBase.primaryStart,
brightness: Brightness.light,
onSurfaceVariant: themeBase.textSecondary,
//背景色
surfaceContainerHigh: themeBase.surfaceContainerHigh,
surfaceContainer: themeBase.surfaceContainer,
surfaceContainerLow: themeBase.surfaceContainerLow,
surfaceContainerLowest: themeBase.surfaceContainerLowest,
//阴影
shadow: themeBase.shadow,
),
textTheme: textTheme,
extensions: [AppThemeExtension(baseTheme: themeBase)],
// pageTransitionsTheme: const PageTransitionsTheme(),
appBarTheme: AppBarTheme(
backgroundColor: Colors.white,
titleTextStyle: textTheme.titleMedium,
scrolledUnderElevation: 0,
),
);
}
}

View File

@@ -0,0 +1,49 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import '../base/app_colors_base.dart';
class LightTheme extends AppColorsBase {
@override
Color get primaryStart => const Color(0xff44EAB3);
@override
Color get primaryEnd => const Color(0xff1EDCDA);
@override
Color get textPrimary => const Color(0xff222932);
@override
Color get textSecondary => const Color(0xffA6B0BE);
@override
Color get textTertiary => const Color(0xffC7CDD5);
@override
Color get success => const Color(0xff00D4B5);
@override
Color get warning => const Color(0xffFF9200);
@override
Color get info => const Color(0xffededed);
@override
Color get danger => const Color(0xffFF4900);
@override
Color get surfaceContainerLowest => Color(0xffE0E0E0);
@override
Color get surfaceContainerLow => Color(0xffF0F0F0);
@override
Color get surfaceContainer => Color(0xFFF2F5FA);
@override
Color get surfaceContainerHigh => Color(0xffFFFFFF);
@override
Color get shadow => const Color.fromRGBO(0, 0, 0, 0.1);
}

View File

@@ -0,0 +1,32 @@
/// 格式化时间
String formatDate(dynamic date, [String format = 'YYYY-MM-DD hh:mm:ss']) {
DateTime dateTime;
if (date is String) {
// 如果是字符串类型,尝试将其解析为 DateTime
dateTime = DateTime.tryParse(date) ?? DateTime.now();
} else if (date is DateTime) {
// 如果是 DateTime 类型,直接使用
dateTime = date;
} else {
// 如果不是合法的输入类型,默认使用当前时间
dateTime = DateTime.now();
}
final yyyy = dateTime.year.toString();
final MM = (dateTime.month).toString().padLeft(2, '0');
final dd = (dateTime.day).toString().padLeft(2, '0');
final HH = (dateTime.hour).toString().padLeft(2, '0');
final mm = (dateTime.minute).toString().padLeft(2, '0');
final ss = (dateTime.second).toString().padLeft(2, '0');
String result = format
.replaceFirst(RegExp('YYYY'), '$yyyy')
.replaceFirst(RegExp('MM'), MM)
.replaceFirst(RegExp('DD'), dd)
.replaceFirst(RegExp('hh'), HH)
.replaceFirst(RegExp('mm'), mm)
.replaceFirst(RegExp('ss'), ss);
return result;
}

View File

@@ -0,0 +1,54 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
class Storage {
/// 存储数据
static Future<void> set(String key, dynamic value) async {
SharedPreferences sp = await SharedPreferences.getInstance();
if (value is String) {
sp.setString(key, value);
} else if (value is int) {
sp.setInt(key, value);
} else if (value is bool) {
sp.setBool(key, value);
} else if (value is double) {
sp.setDouble(key, value);
} else if (value is Map) {
String jsonStr = jsonEncode(value);
sp.setString(key, jsonStr);
}
}
/// 获取数据
/// - [decoder] 解码器,不传默认返回原数据
static Future<T?> get<T>(
String key, {
T Function(dynamic json)? decoder,
}) async {
final sp = await SharedPreferences.getInstance();
final value = sp.get(key);
//如果不存在
if (value == null) return null;
// 如果需要反序列化
if (decoder != null && value is String) {
return decoder(jsonDecode(value));
}
return value as T?;
}
//删除数据
static Future<void> remove(key) async {
SharedPreferences sp = await SharedPreferences.getInstance();
sp.remove(key);
}
//判断键是否存在
static Future<bool> hasKey(String key) async {
SharedPreferences sp = await SharedPreferences.getInstance();
return sp.containsKey(key);
}
}

View File

@@ -0,0 +1,77 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:permission_handler/permission_handler.dart';
///判断是否是安卓
bool isAndroid(){
return Platform.isAndroid;
}
/// 封装通用权限处理方法
/// - [permissions] 需要检查的权限列表
/// - [onGranted] 当所有权限都被授予时调用的回调
/// - [onDenied] 当有权限被拒绝时调用的回调
/// - [onPermanentlyDenied] 当有权限被永久拒绝时调用的回调(可选,默认打开设置页)
Future<void> checkAndRequestPermissions({
required List<Permission> permissions,
required VoidCallback onGranted,
VoidCallback? onDenied,
VoidCallback? onPermanentlyDenied,
}) async {
// 判断当前权限状态
Map<Permission, PermissionStatus> statuses = {};
for (final permission in permissions) {
statuses[permission] = await permission.status;
}
// 筛选出未授权的权限
final needRequest = statuses.entries.where((entry) => !entry.value.isGranted).map((entry) => entry.key).toList();
// 如果全部已有权限
if (needRequest.isEmpty) {
onGranted();
return;
}
// 请求未授权的权限
final requestResult = await needRequest.request();
//是否全部授权
bool allGranted = true;
//是否有任何一个授权了
bool anyPermanentlyDenied = false;
for (final permission in permissions) {
final status = requestResult[permission] ?? await permission.status;
if (status.isPermanentlyDenied) {
anyPermanentlyDenied = true;
allGranted = false;
break;
} else if (!status.isGranted) {
allGranted = false;
}
}
if (allGranted) {
onGranted();
} else if (anyPermanentlyDenied) {
if (onPermanentlyDenied != null) {
onPermanentlyDenied();
} else {
openAppSettings(); // 可选:默认打开设置页
}
} else {
if (onDenied != null) {
onDenied();
}
}
}
///复制文本到剪切板
void copyToClipboard(String text) {
Clipboard.setData(ClipboardData(text: text));
EasyLoading.showToast('已复制到剪切板');
}

View File

@@ -0,0 +1,81 @@
//下载文件
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class LocalDownload {
static Future<String> getLocalFilePath(String url, String path) async {
Uri uri = Uri.parse(url);
String fileName = uri.pathSegments.last;
//获取下载目录
Directory dir = await getApplicationCacheDirectory();
Directory uploadPath = Directory("${dir.path}$path/");
return uploadPath.path + fileName;
}
/// 公用下载方法
/// url 下载网络地址
/// path 存储地址,如/test
/// onProgress 下载回调函数
/// onDone 下载完毕回调
static downLoadFile({
required url,
required path,
required Function(double) onProgress,
required Function(String) onDone,
}) async {
HttpClient client = HttpClient();
Uri uri = Uri.parse(url);
//获取本地文件路径
String filePath = await getLocalFilePath(url, path);
// 发起 get 请求
HttpClientRequest request = await client.getUrl(uri);
// 响应
HttpClientResponse response = await request.close();
int contentLength = response.contentLength; // 获取文件总大小
int bytesReceived = 0; // 已接收的字节数
List<int> chunkList = [];
if (response.statusCode == 200) {
response.listen(
(List<int> chunk) {
chunkList.addAll(chunk);
bytesReceived += chunk.length; //更新已接受的字节数
//进度
double progress = bytesReceived * 100 / contentLength * 100;
progress = (progress / 100).truncateToDouble();
onProgress(progress);
},
onDone: () async {
//下载完毕
client.close();
File file = File(filePath);
if (!file.existsSync()) {
file.createSync(recursive: true);
await file.writeAsBytes(chunkList);
}
onDone(file.path);
},
onError: () {
client.close();
},
cancelOnError: true,
);
}
}
///获取本地地址
///
static Future<String> getFilePath({
required url,
required path,
}) async {
//获取本地文件路径
String filePath = await getLocalFilePath(url, path);
File file = File(filePath);
if (file.existsSync()) {
return file.path;
} else {
return '';
}
}
}

View File

@@ -0,0 +1,46 @@
enum FileType {
image,
pdf,
other,
}
class FileMeta {
final String name; // 不含后缀
final String extension; // 不含点
final FileType type;
const FileMeta({
required this.name,
required this.extension,
required this.type,
});
}
FileMeta parseFileMeta(String path) {
final uri = Uri.parse(path);
final fileName = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : '';
if (!fileName.contains('.')) {
return const FileMeta(
name: '',
extension: '',
type: FileType.other,
);
}
final index = fileName.lastIndexOf('.');
final name = fileName.substring(0, index);
final ext = fileName.substring(index + 1).toLowerCase();
final type = switch (ext) {
'jpg' || 'jpeg' || 'png' || 'webp' || 'gif' => FileType.image,
'pdf' => FileType.pdf,
_ => FileType.other,
};
return FileMeta(
name: name,
extension: ext,
type: type,
);
}

View File

@@ -0,0 +1,70 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
/// 选择文件
/// - [type] 文件类型
/// - [allowedExtensions] 允许的扩展名
/// - [maxSize] 最大文件大小
Future<List<File>> selectFile({
required FileType type,
List<String>? allowedExtensions,
int maxSize = 1024 * 1024 * 20,
}) async {
List<File> files = [];
bool hasOversize = false;
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: type,
allowedExtensions: allowedExtensions,
);
// 判断空
if (result == null) return [];
//进行文件大小,压缩、等操作
for (final e in result.files) {
if (e.size > maxSize) {
hasOversize = true;
continue;
}
if (e.path == null) continue;
if (type == FileType.image) {
final compressed = await compressImage(File(e.path!));
files.add(compressed);
} else {
files.add(File(e.path!));
}
}
//提示
if (hasOversize) {
EasyLoading.showInfo('已过滤超过 20MB 的文件');
}
return files;
}
/// 压缩图片
Future<File> compressImage(File file) async {
final dir = await getTemporaryDirectory();
final targetPath = p.join(
dir.path,
'${DateTime.now().millisecondsSinceEpoch}.jpg',
);
final result = await FlutterImageCompress.compressAndGetFile(
file.absolute.path,
targetPath,
quality: 80,
format: CompressFormat.jpeg,
minWidth: 1080,
minHeight: 1080,
);
if (result == null) {
throw Exception('Image compression failed');
}
return File(result.path);
}

View File

@@ -0,0 +1,59 @@
import 'dart:io';
import 'package:app/data/api/common_api.dart';
import 'package:app/data/models/common/qiu_token_dto.dart';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import '../../config/global.dart';
class QinUpload {
///获取七牛token
static Future<QiuTokenDto> _getQiuToken(File file, String path) async {
// 读取文件的字节数据
final fileBytes = await file.readAsBytes();
String fileMd5 = md5.convert(fileBytes).toString();
//前缀
var prefix = Config.getEnv() == "dev" ? "test" : "release";
var suffix = file.path.split(".").last;
var res = await getQiuTokenApi(
"shangyiapp/$prefix/$path/$fileMd5.$suffix",
);
return res;
}
///上传文件
/// - [file] 文件
/// - [path] 目标目录,不以/开头和结尾
static Future<String?> upload({
required File file,
required String path,
}) async {
var qiuToken = await _getQiuToken(file, path);
//数据
FormData formData = FormData.fromMap({
"file": await MultipartFile.fromFile(file.path),
"token": qiuToken.upToken,
"fname": qiuToken.fileKey,
"key": qiuToken.fileKey,
});
try {
Dio dio = Dio();
Response response = await dio.post(
qiuToken.uploadUrl!,
data: formData,
onSendProgress: (int sent, int total) {
// double progress = sent * 100 / total * 100;
// progress = (progress / 100).truncateToDouble();
},
);
String key = response.data['key'];
return "https://${qiuToken.domain}/$key";
} catch (e) {
EasyLoading.showError("上传失败");
return null;
}
}
}