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;
|
|||
|
|
}
|
|||
|
|
}
|