完善移动端错误提示中文化

This commit is contained in:
czl231
2026-09-02 21:07:47 +08:00
parent 0f2475098f
commit dd250d80a0
10 changed files with 521 additions and 156 deletions

View File

@@ -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'];
}
}

View File

@@ -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),