Files
CloudHome-Ops/lib/widgets/region_picker.dart

209 lines
5.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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