feat(device): 实现闹钟同步与相册管理,并新增拨号与设备设置入口

This commit is contained in:
TongTongStudio
2026-10-21 09:33:27 +08:00
parent bfb106f1fb
commit 7435588fa7
48 changed files with 6766 additions and 565 deletions

View File

@@ -107,7 +107,7 @@ class DioClient {
try {
// 对接 open 模块刷新接口,路径需带 v1/open/ 前缀。
final response = await _refreshDio.post<dynamic>(
'v1/open/refresh-token',
'v1/open/auth/refresh-token',
queryParameters: {'refreshToken': refreshToken},
);
debugPrint('[DioClient] _refreshToken: response status=${response.statusCode} body=${response.data}');
@@ -155,6 +155,25 @@ class DioClient {
}) =>
_request(() => instance.put<dynamic>(path, data: data));
/// 统一 POST 请求(multipart 文件上传),返回解析后的 data。
static Future<dynamic> postMultipart(
String path, {
required FormData formData,
Map<String, dynamic>? queryParameters,
}) =>
_request(() => instance.post<dynamic>(
path,
data: formData,
queryParameters: queryParameters,
));
/// 下载静态资源文件到本地路径(图片等二进制内容,不走 JSON 业务解析)。
///
/// [url] 支持相对路径(如 `/static/wallpaper/xxx.jpg`),会基于 baseUrl 拼接。
static Future<void> downloadToFile(String url, String savePath) async {
await instance.download(url, savePath);
}
/// 统一 DELETE 请求,返回解析后的 data。
static Future<dynamic> delete(String path) =>
_request(() => instance.delete<dynamic>(path));
@@ -252,7 +271,11 @@ class _AuthInterceptor extends Interceptor {
class _ResponseInterceptor extends Interceptor {
/// 解析响应体,提取 data 字段;code != "00000" 时抛出业务错误。
static dynamic parse(Response<dynamic> response) {
final body = response.data as Map<String, dynamic>?;
// 下载等场景响应体可能是二进制(ResponseBody 等),先做类型判定再强转,
// 避免对非 JSON 数据直接 `as Map` 抛 TypeError。
final body = response.data is Map<String, dynamic>
? response.data as Map<String, dynamic>
: null;
if (body == null) {
throw const ApiException(code: '-1', message: '响应为空');
}
@@ -283,7 +306,9 @@ class _ErrorInterceptor extends Interceptor {
void onError(DioException err, ErrorInterceptorHandler handler) {
final response = err.response;
final statusCode = response?.statusCode;
final body = response?.data as Map<String, dynamic>?;
// 响应体可能是二进制(ResponseBody 等),先类型判定再取业务码,避免强转抛错。
final data = response?.data;
final body = data is Map<String, dynamic> ? data : null;
final code = body?['code']?.toString();
// 访问权限异常(A0300):通常意味着当前令牌的端标识不符(如旧令牌未携带

View File

@@ -0,0 +1,27 @@
import 'package:flutter/services.dart';
/// 拨打电话工具。
///
/// 通过原生 MethodChannel(channel 名与系统 UI 工具共用 `com.ttstd.familycare/app`)
/// 调起系统拨号盘:
/// - Android:`Intent.ACTION_DIAL`(`tel:` 号码),无需拨号权限;
/// - iOS:`UIApplication.open` 打开 `tel:` URL,系统弹确认后拨号。
class PhoneCallUtil {
const PhoneCallUtil._();
static const MethodChannel _channel = MethodChannel('com.ttstd.familycare/app');
/// 打开系统拨号盘拨打 [phone]。
///
/// 成功返回 null;失败返回错误信息(如号码为空、设备无拨号能力)。
static Future<String?> dial(String phone) async {
if (phone.isEmpty) return '电话号码为空';
try {
final ok = await _channel
.invokeMethod<bool>('dialPhone', {'phone': phone});
return ok == true ? null : '无法打开拨号盘';
} on PlatformException catch (e) {
return e.message ?? '拨号失败';
}
}
}

View File

@@ -107,6 +107,7 @@ class LabeledField extends StatelessWidget {
this.maxLength,
this.enabled = true,
this.suffix,
this.labelTrailing,
super.key,
});
@@ -121,19 +122,36 @@ class LabeledField extends StatelessWidget {
/// 输入框右侧控件(如「获取验证码」按钮)。
final Widget? suffix;
/// 标签右侧的小字提示(如「区分大小写」),可为空。
final String? labelTrailing;
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w600,
color: AppColors.sub,
),
Row(
children: [
Text(
label,
style: const TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w600,
color: AppColors.sub,
),
),
if (labelTrailing != null) ...[
const SizedBox(width: 6),
Text(
labelTrailing!,
style: const TextStyle(
fontSize: 11,
color: AppColors.orange,
),
),
],
],
),
const SizedBox(height: 6),
Row(