209 lines
5.6 KiB
Dart
209 lines
5.6 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
import '../data/region_data.dart';
|
||
|
||
/// 省→市→区三级联动结果。
|
||
class RegionResult {
|
||
const RegionResult({
|
||
required this.provinceCode,
|
||
required this.provinceName,
|
||
required this.cityCode,
|
||
required this.cityName,
|
||
required this.districtCode,
|
||
required this.districtName,
|
||
});
|
||
|
||
final String provinceCode;
|
||
final String provinceName;
|
||
final String cityCode;
|
||
final String cityName;
|
||
final String districtCode;
|
||
final String districtName;
|
||
|
||
/// 拼接展示文本(省名+市名+区名)。
|
||
String get label =>
|
||
'$provinceName$cityName$districtName';
|
||
}
|
||
|
||
/// 省→市→区三级联动底部弹起选择器。
|
||
///
|
||
/// 数据源统一来自 [RegionData](已加载的 `province.json`)。
|
||
/// 返回 [RegionResult](含 code 与 name);用户取消时弹出 null。
|
||
class RegionPicker extends StatefulWidget {
|
||
const RegionPicker({
|
||
super.key,
|
||
this.initialProvinceCode,
|
||
this.initialCityCode,
|
||
this.initialDistrictCode,
|
||
});
|
||
|
||
final String? initialProvinceCode;
|
||
final String? initialCityCode;
|
||
final String? initialDistrictCode;
|
||
|
||
/// 打开选择器底部弹窗,返回所选 [RegionResult] 或 null。
|
||
static Future<RegionResult?> show({
|
||
required BuildContext context,
|
||
String? initialProvinceCode,
|
||
String? initialCityCode,
|
||
String? initialDistrictCode,
|
||
}) {
|
||
return showModalBottomSheet<RegionResult>(
|
||
useSafeArea: true,
|
||
context: context,
|
||
isScrollControlled: true,
|
||
builder: (ctx) => RegionPicker(
|
||
initialProvinceCode: initialProvinceCode,
|
||
initialCityCode: initialCityCode,
|
||
initialDistrictCode: initialDistrictCode,
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
State<RegionPicker> createState() => _RegionPickerState();
|
||
}
|
||
|
||
class _RegionPickerState extends State<RegionPicker> {
|
||
final RegionData _region = RegionData.instance;
|
||
|
||
/// 当前选中层级:0=省 1=市 2=区。
|
||
int _level = 0;
|
||
String? _provinceCode;
|
||
String? _cityCode;
|
||
String? _districtCode;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_provinceCode = widget.initialProvinceCode;
|
||
_cityCode = widget.initialCityCode;
|
||
_districtCode = widget.initialDistrictCode;
|
||
// 若已带初始省,直接进入市层级以便快速定位。
|
||
if (_provinceCode != null && _provinceCode!.isNotEmpty) {
|
||
_level = 1;
|
||
if (_cityCode != null && _cityCode!.isNotEmpty) {
|
||
_level = 2;
|
||
}
|
||
}
|
||
}
|
||
|
||
List<RegionNode> get _currentList {
|
||
switch (_level) {
|
||
case 0:
|
||
return _region.provinces;
|
||
case 1:
|
||
return _region.citiesOf(_provinceCode ?? '');
|
||
case 2:
|
||
return _region.districtsOf(
|
||
_provinceCode ?? '',
|
||
_cityCode ?? '',
|
||
);
|
||
default:
|
||
return const [];
|
||
}
|
||
}
|
||
|
||
String _levelTitle() {
|
||
switch (_level) {
|
||
case 0:
|
||
return '选择省份';
|
||
case 1:
|
||
return '选择城市';
|
||
case 2:
|
||
return '选择区县';
|
||
default:
|
||
return '选择地区';
|
||
}
|
||
}
|
||
|
||
void _onSelect(RegionNode node) {
|
||
if (_level == 0) {
|
||
setState(() {
|
||
_provinceCode = node.code;
|
||
_cityCode = null;
|
||
_districtCode = null;
|
||
_level = 1;
|
||
});
|
||
} else if (_level == 1) {
|
||
setState(() {
|
||
_cityCode = node.code;
|
||
_districtCode = null;
|
||
_level = 2;
|
||
});
|
||
} else {
|
||
final provinceName = _region.nameOf(_provinceCode ?? '');
|
||
final cityName = _region.nameOf(_cityCode ?? '');
|
||
Navigator.pop(
|
||
context,
|
||
RegionResult(
|
||
provinceCode: _provinceCode ?? '',
|
||
provinceName: provinceName,
|
||
cityCode: _cityCode ?? '',
|
||
cityName: cityName,
|
||
districtCode: node.code,
|
||
districtName: node.name,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final nodes = _currentList;
|
||
return Container(
|
||
height: MediaQuery.of(context).size.height * 0.6,
|
||
padding: const EdgeInsets.only(top: 8),
|
||
child: Column(
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const SizedBox(width: 16),
|
||
Expanded(
|
||
child: Text(
|
||
_levelTitle(),
|
||
style: Theme.of(context)
|
||
.textTheme
|
||
.titleMedium
|
||
?.copyWith(fontWeight: FontWeight.bold),
|
||
),
|
||
),
|
||
if (_level > 0)
|
||
TextButton(
|
||
onPressed: () => setState(() => _level--),
|
||
child: const Text('上一级'),
|
||
),
|
||
const SizedBox(width: 8),
|
||
],
|
||
),
|
||
const Divider(height: 1),
|
||
Expanded(
|
||
child: ListView.separated(
|
||
itemCount: nodes.length,
|
||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||
itemBuilder: (context, index) {
|
||
final node = nodes[index];
|
||
final isSelected = _isSelected(node);
|
||
return ListTile(
|
||
title: Text(node.name),
|
||
trailing: isSelected
|
||
? Icon(Icons.check,
|
||
color: Theme.of(context).colorScheme.primary)
|
||
: const Icon(Icons.chevron_right),
|
||
onTap: () => _onSelect(node),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
bool _isSelected(RegionNode node) {
|
||
if (_level == 0) return node.code == _provinceCode;
|
||
if (_level == 1) return node.code == _cityCode;
|
||
return node.code == _districtCode;
|
||
}
|
||
}
|