更新城市选择
This commit is contained in:
281
lib/common/pojo/city.dart
Normal file
281
lib/common/pojo/city.dart
Normal file
@@ -0,0 +1,281 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:ef/base/getx/getx.dart';
|
||||
import 'package:ef/ef.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:vbvs_app/controller/setting/language/language_controller.dart';
|
||||
import 'package:vbvs_app/language/AppLanguage.dart';
|
||||
part 'city.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class CityModel {
|
||||
int? id; // 城市id
|
||||
String? value; // 显示值(国家/省份/城市名称)
|
||||
String? label; // 标签(通常与value相同)
|
||||
String? country; // 国家名称
|
||||
String? province; // 省份名称
|
||||
String? city; // 城市名称
|
||||
String? UTC; // 时区
|
||||
List<CityModel>? children;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
String? keyword = "";
|
||||
@JsonKey(ignore: true)
|
||||
Color? color = Color(0xFFFFFFFF);
|
||||
|
||||
CityModel({
|
||||
this.id,
|
||||
this.value,
|
||||
this.label,
|
||||
this.country,
|
||||
this.province,
|
||||
this.city,
|
||||
this.UTC,
|
||||
this.children,
|
||||
});
|
||||
|
||||
// 从JSON反序列化时的异常处理
|
||||
factory CityModel.fromJson(Map<String, dynamic> json) {
|
||||
try {
|
||||
return _$CityModelFromJson(json);
|
||||
} catch (e) {
|
||||
print('Error parsing CityModel: $e');
|
||||
return CityModel(); // 返回空的CityModel
|
||||
}
|
||||
}
|
||||
|
||||
// 序列化为JSON时的异常处理
|
||||
Map<String, dynamic> toJson() => _$CityModelToJson(this);
|
||||
|
||||
// 获取显示名称(根据层级显示)
|
||||
String get displayName {
|
||||
if (city != null && city!.isNotEmpty) {
|
||||
// 三级:国家-省份-城市
|
||||
return '${country ?? ''}-${province ?? ''}-${city ?? value ?? ''}';
|
||||
} else if (province != null && province!.isNotEmpty) {
|
||||
// 二级:国家-省份
|
||||
return '${country ?? ''}-${province ?? value ?? ''}';
|
||||
} else {
|
||||
// 一级:国家
|
||||
return country ?? value ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CityModelController extends GetControllerEx<CityModel> {
|
||||
List<CityModel> cityList = [];
|
||||
LanguageController languageController = Get.find();
|
||||
RxInt tmp = 1.obs;
|
||||
RxBool isLoading = false.obs;
|
||||
List<String>? searchResults = []; // 新增:搜索结果列表
|
||||
|
||||
CityModelController() {
|
||||
attr = GetModel(CityModel()).obs;
|
||||
}
|
||||
|
||||
// 加载城市数据并赋值给cityList
|
||||
Future<void> loadAndSetCityData() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
ef.log("开始加载城市数据...");
|
||||
|
||||
final data = await _loadCityData();
|
||||
cityList = data;
|
||||
|
||||
ef.log("城市数据加载完成,共${cityList.length}个国家");
|
||||
if (cityList.isNotEmpty) {
|
||||
// 打印前几个国家用于调试
|
||||
for (var i = 0; i < min(3, cityList.length); i++) {
|
||||
final country = cityList[i];
|
||||
ef.log(
|
||||
"国家: ${country.value}, 省份数量: ${country.children?.length ?? 0}");
|
||||
}
|
||||
}
|
||||
|
||||
update();
|
||||
} catch (e) {
|
||||
ef.log("加载城市数据失败:$e");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 内部加载方法
|
||||
Future<List<CityModel>> _loadCityData() async {
|
||||
try {
|
||||
// 获取当前语言代码
|
||||
String currentLanguageCode = AppLanguage().getCurrentLanguageCode();
|
||||
|
||||
// 根据当前语言代码构建文件名
|
||||
final String fileName = 'assets/city/city_$currentLanguageCode.json';
|
||||
|
||||
// 读取对应语言的JSON文件
|
||||
final String jsonString = await rootBundle.loadString(fileName);
|
||||
|
||||
// 解析JSON数据并转换为 CityModel 列表
|
||||
final List<dynamic> jsonList = jsonDecode(jsonString);
|
||||
|
||||
// 将 Map 转换为 CityModel
|
||||
return jsonList.map((json) => CityModel.fromJson(json)).toList();
|
||||
} catch (e) {
|
||||
ef.log("_loadCityData 失败:$e");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 根据ID查找城市(支持国家、省份、城市所有层级)
|
||||
CityModel? findCityById(String id) {
|
||||
if (cityList.isEmpty) {
|
||||
ef.log("cityList为空,无法查找城市,请先调用 loadAndSetCityData()");
|
||||
return null;
|
||||
}
|
||||
|
||||
final int? targetId = int.tryParse(id);
|
||||
if (targetId == null) {
|
||||
ef.log("ID格式错误:$id");
|
||||
return null;
|
||||
}
|
||||
|
||||
ef.log("开始查找ID为 $targetId 的城市数据...");
|
||||
|
||||
// 遍历三级结构
|
||||
for (var country in cityList) {
|
||||
// 检查国家
|
||||
if (country.id == targetId) {
|
||||
ef.log("找到匹配的国家: ${country.value}");
|
||||
return CityModel(
|
||||
id: country.id,
|
||||
value: country.value,
|
||||
label: country.label,
|
||||
country: country.value,
|
||||
province: null,
|
||||
city: null,
|
||||
UTC: country.UTC,
|
||||
);
|
||||
}
|
||||
|
||||
// 检查省份
|
||||
for (var province in country.children ?? []) {
|
||||
if (province.id == targetId) {
|
||||
ef.log("找到匹配的省份: ${province.value}, 国家: ${country.value}");
|
||||
return CityModel(
|
||||
id: province.id,
|
||||
value: province.value,
|
||||
label: province.label,
|
||||
country: country.value,
|
||||
province: province.value,
|
||||
city: null,
|
||||
UTC: province.UTC,
|
||||
);
|
||||
}
|
||||
|
||||
// 检查城市
|
||||
for (var city in province.children ?? []) {
|
||||
if (city.id == targetId) {
|
||||
ef.log(
|
||||
"找到匹配的城市: ${city.value}, 省份: ${province.value}, 国家: ${country.value}");
|
||||
return CityModel(
|
||||
id: city.id,
|
||||
value: city.value,
|
||||
label: city.label,
|
||||
country: country.value,
|
||||
province: province.value,
|
||||
city: city.value,
|
||||
UTC: city.UTC,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ef.log("未找到ID为 $targetId 的城市数据");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 根据名称查找城市
|
||||
CityModel? findCityByName(
|
||||
String countryName, String provinceName, String cityName) {
|
||||
if (cityList.isEmpty) {
|
||||
ef.log("cityList为空,无法查找城市");
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var country in cityList) {
|
||||
if (country.value == countryName) {
|
||||
for (var province in country.children ?? []) {
|
||||
if (province.value == provinceName) {
|
||||
for (var city in province.children ?? []) {
|
||||
if (city.value == cityName) {
|
||||
return city;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查是否已加载数据
|
||||
bool get isDataLoaded => cityList.isNotEmpty;
|
||||
|
||||
void searchCities(String keyword) {
|
||||
model.keyword = keyword;
|
||||
searchResults?.clear();
|
||||
|
||||
if (keyword.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final keywordLower = keyword.toLowerCase();
|
||||
|
||||
// 遍历所有城市数据
|
||||
for (var country in cityList) {
|
||||
final countryName = country.value ?? country.country ?? '';
|
||||
|
||||
for (var province in country.children ?? []) {
|
||||
final provinceName = province.value ?? province.province ?? '';
|
||||
|
||||
for (var city in province.children ?? []) {
|
||||
final cityName = city.value ?? city.city ?? '';
|
||||
final displayName = '$countryName-$provinceName-$cityName';
|
||||
|
||||
// 模糊匹配
|
||||
if (countryName.toLowerCase().contains(keywordLower) ||
|
||||
provinceName.toLowerCase().contains(keywordLower) ||
|
||||
cityName.toLowerCase().contains(keywordLower) ||
|
||||
displayName.toLowerCase().contains(keywordLower)) {
|
||||
searchResults?.add(displayName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据显示名称获取对应的 CityModel
|
||||
CityModel? getCityByDisplayName(String displayName) {
|
||||
final parts = displayName.split('-');
|
||||
if (parts.length != 3) return null;
|
||||
|
||||
final countryName = parts[0];
|
||||
final provinceName = parts[1];
|
||||
final cityName = parts[2];
|
||||
|
||||
for (var country in cityList) {
|
||||
if ((country.value ?? country.country ?? '') == countryName) {
|
||||
for (var province in country.children ?? []) {
|
||||
if ((province.value ?? province.province ?? '') == provinceName) {
|
||||
for (var city in province.children ?? []) {
|
||||
if ((city.value ?? city.city ?? '') == cityName) {
|
||||
return city;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
31
lib/common/pojo/city.g.dart
Normal file
31
lib/common/pojo/city.g.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'city.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
CityModel _$CityModelFromJson(Map<String, dynamic> json) => CityModel(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
value: json['value'] as String?,
|
||||
label: json['label'] as String?,
|
||||
country: json['country'] as String?,
|
||||
province: json['province'] as String?,
|
||||
city: json['city'] as String?,
|
||||
UTC: json['UTC'] as String?,
|
||||
children: (json['children'] as List<dynamic>?)
|
||||
?.map((e) => CityModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CityModelToJson(CityModel instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'value': instance.value,
|
||||
'label': instance.label,
|
||||
'country': instance.country,
|
||||
'province': instance.province,
|
||||
'city': instance.city,
|
||||
'UTC': instance.UTC,
|
||||
'children': instance.children,
|
||||
};
|
||||
247
lib/common/util/ListSearchWidget.dart
Normal file
247
lib/common/util/ListSearchWidget.dart
Normal file
@@ -0,0 +1,247 @@
|
||||
import 'package:ef/ef.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutterflow_ui/flutterflow_ui.dart';
|
||||
import 'package:vbvs_app/common/util/FitTool.dart';
|
||||
import '../../common/util/MyUtils.dart';
|
||||
|
||||
class ListSearchWidget extends GetView {
|
||||
final String? keyword;
|
||||
final Color? color;
|
||||
String? hint;
|
||||
Function? onChange;
|
||||
Function? findCallback;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final List<String>? searchResults; // 新增:搜索结果列表
|
||||
final Function(String)? onResultTap; // 新增:点击结果的回调
|
||||
final bool showResultList; // 新增:是否显示结果列表,默认不显示
|
||||
|
||||
ListSearchWidget({
|
||||
required this.keyword,
|
||||
required this.color,
|
||||
this.hint = "请输入关键字",
|
||||
this.findCallback,
|
||||
this.onChange,
|
||||
this.padding,
|
||||
this.searchResults, // 搜索结果
|
||||
this.onResultTap, // 点击结果回调
|
||||
this.showResultList = false, // 默认不显示结果列表
|
||||
});
|
||||
|
||||
final RxBool _showResults = false.obs; // 控制是否显示结果列表
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 搜索框部分
|
||||
Padding(
|
||||
padding: padding ?? EdgeInsetsDirectional.fromSTEB(30.rpx, 0, 30.rpx, 0),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: themeController.currentColor.sc3,
|
||||
borderRadius: BorderRadius.circular(16.rpx),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(35.rpx, 0, 35.rpx, 0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(
|
||||
width: 25.rpx,
|
||||
height: 25.rpx,
|
||||
decoration: BoxDecoration(),
|
||||
child: SvgPicture.asset(
|
||||
'assets/img/icon/query.svg',
|
||||
fit: BoxFit.cover,
|
||||
color: stringToColor("#333333"),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: 100.rpx,
|
||||
height: 60.rpx,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional(-1, 0),
|
||||
child: TextFormField(
|
||||
autofocus: false,
|
||||
obscureText: false,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
labelStyle: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 26.rpx,
|
||||
letterSpacing: 0.0,
|
||||
),
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 26.rpx,
|
||||
letterSpacing: 0.0,
|
||||
color: themeController.currentColor.sc4,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Color(0x00000000),
|
||||
width: 1.rpx,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8.rpx),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Color(0x00000000),
|
||||
width: 1.rpx,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8.rpx),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
width: 1.rpx,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8.rpx),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
width: 1.rpx,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8.rpx),
|
||||
),
|
||||
filled: false,
|
||||
fillColor: themeController.currentColor.sc22,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: 12.rpx, horizontal: 12.rpx),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 26.rpx,
|
||||
color: Colors.black,
|
||||
letterSpacing: 0.0,
|
||||
),
|
||||
onChanged: (d) {
|
||||
onChange?.call(d);
|
||||
// 输入时自动显示结果列表(如果开启了显示功能)
|
||||
if (showResultList && d.isNotEmpty) {
|
||||
_showResults.value = true;
|
||||
} else {
|
||||
_showResults.value = false;
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
// 点击输入框时显示结果列表(如果开启了显示功能且有搜索结果)
|
||||
if (showResultList && searchResults != null && searchResults!.isNotEmpty) {
|
||||
_showResults.value = true;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
].divide(SizedBox(width: 6.rpx)),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(26.rpx, 0, 0, 0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
findCallback?.call();
|
||||
// 点击搜索按钮后显示结果列表(如果开启了显示功能)
|
||||
if (showResultList) {
|
||||
_showResults.value = true;
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 30.rpx,
|
||||
child: VerticalDivider(
|
||||
thickness: 2.rpx,
|
||||
color: stringToColor("#333333"),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'搜索'.tr,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 30.rpx,
|
||||
letterSpacing: 0.0,
|
||||
color: stringToColor("#333333"),
|
||||
),
|
||||
),
|
||||
].divide(SizedBox(width: 26.rpx)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 搜索结果列表(可选显示)
|
||||
if (showResultList) ...[
|
||||
Obx(() {
|
||||
if (!_showResults.value || searchResults == null || searchResults!.isEmpty) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 10.rpx),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8.rpx),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 4.rpx,
|
||||
offset: Offset(0, 2.rpx),
|
||||
),
|
||||
],
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 200.rpx, // 限制最大高度
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: ClampingScrollPhysics(),
|
||||
itemCount: searchResults!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final result = searchResults![index];
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 20.rpx,
|
||||
vertical: 8.rpx,
|
||||
),
|
||||
title: Text(
|
||||
result,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 26.rpx,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
onResultTap?.call(result);
|
||||
_showResults.value = false; // 选择后隐藏结果列表
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user