feat(device): 实现闹钟同步与相册管理,并新增拨号与设备设置入口
This commit is contained in:
@@ -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):通常意味着当前令牌的端标识不符(如旧令牌未携带
|
||||
|
||||
27
lib/core/utils/phone_call_util.dart
Normal file
27
lib/core/utils/phone_call_util.dart
Normal 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 ?? '拨号失败';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user