完善移动端错误提示中文化
This commit is contained in:
@@ -1,8 +1,79 @@
|
||||
// 功能描述:封装工作人员端 HTTP 请求,并将服务端错误转换为安全、可读的中文提示。
|
||||
// 版本:1.1.0
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
const Map<int, String> _apiErrorMessages = {
|
||||
404: '记录不存在',
|
||||
1001: '请求标识缺失',
|
||||
1002: '登录信息缺失,请重新登录',
|
||||
1003: '接口密钥缺失',
|
||||
1004: '请求头参数缺失',
|
||||
1101: '暂无数据',
|
||||
1102: '请求解析失败',
|
||||
1103: '请填写必填参数',
|
||||
1104: '无权执行此操作',
|
||||
1105: '服务端数据处理失败',
|
||||
1106: '服务端数据处理失败',
|
||||
1107: '服务内部错误,请稍后重试',
|
||||
1108: '密码错误',
|
||||
1110: '账号已禁用',
|
||||
1111: '当前记录已停用',
|
||||
1112: '记录不存在',
|
||||
1301: '登录状态已失效,请重新登录',
|
||||
1302: '登录状态已失效,请重新登录',
|
||||
1303: '登录状态已失效,请重新登录',
|
||||
1304: '登录状态已失效,请重新登录',
|
||||
1305: '登录状态已失效,请重新登录',
|
||||
1306: '登录状态已失效,请重新登录',
|
||||
1307: '登录状态已变更,请重新登录',
|
||||
1308: '登录已过期,请重新登录',
|
||||
1309: '登录状态已失效,请重新登录',
|
||||
1310: '登录状态已失效,请重新登录',
|
||||
1311: '登录状态已失效,请重新登录',
|
||||
1312: '登录状态已失效,请重新登录',
|
||||
1313: '登录失败,请稍后重试',
|
||||
1314: '登录状态已失效,请重新登录',
|
||||
1501: '服务暂时不可用,请稍后重试',
|
||||
1502: '服务暂时不可用,请稍后重试',
|
||||
1503: '服务暂时不可用,请稍后重试',
|
||||
1504: '服务暂时不可用,请稍后重试',
|
||||
1505: '服务暂时不可用,请稍后重试',
|
||||
1506: '服务暂时不可用,请稍后重试',
|
||||
1507: '服务暂时不可用,请稍后重试',
|
||||
1702: '操作已取消',
|
||||
1703: '操作失败,请稍后重试',
|
||||
1704: '请求参数错误',
|
||||
1705: '请求超时,请稍后重试',
|
||||
1706: '记录已存在',
|
||||
1707: '无权执行此操作',
|
||||
1708: '系统繁忙,请稍后重试',
|
||||
1709: '当前状态不支持此操作',
|
||||
1710: '操作已中止',
|
||||
1711: '输入内容超出允许范围',
|
||||
1712: '当前功能暂不支持',
|
||||
1713: '服务暂时不可用,请稍后重试',
|
||||
1714: '服务数据异常,请稍后重试',
|
||||
1715: '请先登录',
|
||||
};
|
||||
|
||||
final RegExp _chineseCharacterPattern = RegExp(r'[\u3400-\u9fff]');
|
||||
|
||||
/// 将服务端错误转换为中文用户提示。
|
||||
///
|
||||
/// [code] 是稳定错误码,[message] 是服务端原始消息;返回值不会透传未知英文技术信息。
|
||||
String localizeApiErrorMessage(int code, String? message) {
|
||||
final mappedMessage = _apiErrorMessages[code];
|
||||
if (mappedMessage != null) return mappedMessage;
|
||||
|
||||
final originalMessage = message?.trim() ?? '';
|
||||
if (_chineseCharacterPattern.hasMatch(originalMessage)) return originalMessage;
|
||||
return '操作失败,请稍后重试';
|
||||
}
|
||||
|
||||
/// 表示客户端可识别并可安全展示的接口异常。
|
||||
class ApiException implements Exception {
|
||||
const ApiException(this.code, this.message);
|
||||
|
||||
@@ -13,6 +84,7 @@ class ApiException implements Exception {
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
/// 负责工作人员端统一 HTTP 请求、鉴权头和响应解析。
|
||||
class ApiClient {
|
||||
ApiClient(this._tokenProvider, {http.Client? client, String? baseUrl})
|
||||
: _client = client ?? http.Client(),
|
||||
@@ -53,7 +125,7 @@ class ApiClient {
|
||||
filename: file.name,
|
||||
),
|
||||
);
|
||||
final response = await http.Response.fromStream(await request.send());
|
||||
final response = await _sendRequest(request);
|
||||
final details = _decode(response);
|
||||
return jsonMap(details)['uri'] as String? ?? '';
|
||||
}
|
||||
@@ -73,22 +145,40 @@ class ApiClient {
|
||||
request.headers['content-type'] = 'application/json; charset=UTF-8';
|
||||
request.body = jsonEncode(body);
|
||||
}
|
||||
final response = await http.Response.fromStream(
|
||||
await _client.send(request),
|
||||
);
|
||||
final response = await _sendRequest(request);
|
||||
return _decode(response);
|
||||
}
|
||||
|
||||
/// 发送请求并统一处理网络连接异常。
|
||||
Future<http.Response> _sendRequest(http.BaseRequest request) async {
|
||||
try {
|
||||
return await http.Response.fromStream(await _client.send(request));
|
||||
} catch (_) {
|
||||
throw const ApiException(500, '网络连接失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析统一响应结构,并按错误码生成中文提示。
|
||||
Object? _decode(http.Response response) {
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw ApiException(response.statusCode, '网络请求失败(${response.statusCode})');
|
||||
}
|
||||
final decoded = jsonDecode(response.body);
|
||||
Object? decoded;
|
||||
try {
|
||||
decoded = jsonDecode(response.body);
|
||||
} on FormatException {
|
||||
throw const ApiException(500, '服务端响应格式错误');
|
||||
}
|
||||
if (decoded is! Map<String, Object?>) {
|
||||
throw const ApiException(500, '服务端响应格式错误');
|
||||
}
|
||||
final code = (decoded['code'] as num?)?.toInt() ?? 500;
|
||||
if (code != 0) throw ApiException(code, decoded['message'] as String? ?? '操作失败');
|
||||
if (code != 0) {
|
||||
throw ApiException(
|
||||
code,
|
||||
localizeApiErrorMessage(code, decoded['message'] as String?),
|
||||
);
|
||||
}
|
||||
return decoded['details'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// 功能描述:提供工作人员端手机号密码登录界面及本地输入校验。
|
||||
// 版本:1.1.0
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../app/dependencies.dart';
|
||||
import '../../../data/services/api_client.dart';
|
||||
|
||||
/// 工作人员端手机号密码登录页面。
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({required this.session, super.key});
|
||||
|
||||
@@ -11,22 +15,41 @@ class LoginPage extends StatefulWidget {
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
/// 管理登录输入、提交状态和错误提示。
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
static final RegExp _phonePattern = RegExp(r'^1[3-9]\d{9}$');
|
||||
|
||||
final _phone = TextEditingController();
|
||||
final _password = TextEditingController();
|
||||
bool _busy = false;
|
||||
String? _error;
|
||||
String? _phoneError;
|
||||
bool _obscurePassword = true;
|
||||
|
||||
/// 校验手机号并提交登录请求。
|
||||
Future<void> _login() async {
|
||||
final phone = _phone.text.trim();
|
||||
if (!_phonePattern.hasMatch(phone)) {
|
||||
setState(() {
|
||||
_phoneError = '请输入正确的11位手机号';
|
||||
_error = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_busy = true;
|
||||
_phoneError = null;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
await widget.session.login(_phone.text.trim(), _password.text);
|
||||
await widget.session.login(phone, _password.text);
|
||||
} catch (error) {
|
||||
if (mounted) setState(() => _error = error.toString());
|
||||
// 登录接口使用统一密码错误码,页面不区分账号是否存在,避免泄露账号状态。
|
||||
final message = error is ApiException
|
||||
? (error.code == 1108 ? '手机号或密码错误' : error.toString())
|
||||
: '登录失败,请稍后重试';
|
||||
if (mounted) setState(() => _error = message);
|
||||
} finally {
|
||||
if (mounted) setState(() => _busy = false);
|
||||
}
|
||||
@@ -84,9 +107,13 @@ class _LoginPageState extends State<LoginPage> {
|
||||
TextField(
|
||||
controller: _phone,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: const InputDecoration(
|
||||
onChanged: (_) {
|
||||
if (_phoneError != null) setState(() => _phoneError = null);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: '工作人员手机号',
|
||||
prefixIcon: Icon(Icons.phone_outlined),
|
||||
prefixIcon: const Icon(Icons.phone_outlined),
|
||||
errorText: _phoneError,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
Reference in New Issue
Block a user