feat(apps): update UI screens and shared components
- Update home screen with new composer and interactions - Update settings screens with new profile flow - Update calendar share dialog - Update contacts screen - Add new shared widgets: confirm_sheet, phone_prefix_selector - Add new formatters: phone_display_formatter - Update tests for modified components
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
String formatPhoneForDisplay(String? rawPhone) {
|
||||
final normalized = _normalizePhone(rawPhone);
|
||||
if (normalized == null) {
|
||||
return rawPhone?.trim() ?? '';
|
||||
}
|
||||
|
||||
if (normalized.startsWith('+86') && normalized.length == 14) {
|
||||
final local = normalized.substring(3);
|
||||
return '${local.substring(0, 3)}****${local.substring(7)}';
|
||||
}
|
||||
|
||||
if (!normalized.startsWith('+')) {
|
||||
return normalized;
|
||||
}
|
||||
final digits = normalized.substring(1);
|
||||
final countryCode = _detectCountryCode(digits);
|
||||
if (countryCode == null) {
|
||||
return normalized;
|
||||
}
|
||||
final localNumber = digits.substring(countryCode.length);
|
||||
if (localNumber.length <= 4) {
|
||||
return '+$countryCode $localNumber';
|
||||
}
|
||||
final tail = localNumber.substring(localNumber.length - 4);
|
||||
return '+$countryCode ****$tail';
|
||||
}
|
||||
|
||||
String? _normalizePhone(String? rawPhone) {
|
||||
if (rawPhone == null) {
|
||||
return null;
|
||||
}
|
||||
var phone = rawPhone.trim();
|
||||
for (final separator in const [' ', '-', '(', ')']) {
|
||||
phone = phone.replaceAll(separator, '');
|
||||
}
|
||||
if (phone.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (phone.startsWith('00') && phone.length > 2) {
|
||||
phone = '+${phone.substring(2)}';
|
||||
}
|
||||
if (!phone.startsWith('+') && RegExp(r'^\d+$').hasMatch(phone)) {
|
||||
phone = '+$phone';
|
||||
}
|
||||
return phone;
|
||||
}
|
||||
|
||||
String? _detectCountryCode(String digits) {
|
||||
const knownCodes = ['86', '1', '44', '81', '65', '33'];
|
||||
for (final code in knownCodes) {
|
||||
if (digits.startsWith(code) && digits.length > code.length + 3) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
for (int length = 3; length >= 1; length--) {
|
||||
if (length >= digits.length) {
|
||||
continue;
|
||||
}
|
||||
final candidate = digits.substring(0, length);
|
||||
if (candidate.startsWith('0')) {
|
||||
continue;
|
||||
}
|
||||
if (digits.length - length >= 4) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/theme/design_tokens.dart';
|
||||
import 'app_button.dart';
|
||||
|
||||
Future<bool> showConfirmSheet(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String message,
|
||||
String confirmText = '确认',
|
||||
String cancelText = '取消',
|
||||
bool isDestructive = false,
|
||||
}) async {
|
||||
final result = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (sheetContext) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.md,
|
||||
AppSpacing.none,
|
||||
AppSpacing.md,
|
||||
AppSpacing.md,
|
||||
),
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 14, color: AppColors.slate500),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
SizedBox(
|
||||
height: 52,
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.of(sheetContext).pop(true),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: isDestructive
|
||||
? AppColors.feedbackErrorIcon
|
||||
: AppColors.blue600,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
child: Text(
|
||||
confirmText,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
SizedBox(
|
||||
height: 52,
|
||||
child: AppButton(
|
||||
text: cancelText,
|
||||
isOutlined: true,
|
||||
onPressed: () => Navigator.of(sheetContext).pop(false),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
return result == true;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/theme/design_tokens.dart';
|
||||
|
||||
class PhonePrefixSelector extends StatelessWidget {
|
||||
const PhonePrefixSelector({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.items,
|
||||
this.onChanged,
|
||||
});
|
||||
|
||||
final String value;
|
||||
final List<String> items;
|
||||
final ValueChanged<String>? onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: AppSpacing.md, right: AppSpacing.sm),
|
||||
child: PopupMenuButton<String>(
|
||||
onSelected: onChanged,
|
||||
itemBuilder: (context) => items
|
||||
.map(
|
||||
(item) => PopupMenuItem<String>(value: item, child: Text(item)),
|
||||
)
|
||||
.toList(growable: false),
|
||||
color: AppColors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
const Icon(
|
||||
Icons.arrow_drop_down,
|
||||
size: 18,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user