feat(client): 录单表单优化 + 可搜索下拉支持新建 + 日期选择器组件

新增 DatePickerField 日期选择器与 date_util;可搜索下拉 SearchableOptionField
支持内联新建选项;入库/出库录单表单、商品页、财务页、app_shell 导航相应调整。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-20 07:05:22 +08:00
parent e892ab45b2
commit a76724b385
12 changed files with 2868 additions and 1624 deletions
+56
View File
@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jiu_client/core/utils/date_util.dart';
import 'package:jiu_client/widgets/date_picker_field.dart';
void main() {
group('date_util', () {
test('formatYmd 补零', () {
expect(formatYmd(DateTime(2026, 6, 9)), '2026-06-09');
expect(formatYmd(DateTime(2026, 12, 31)), '2026-12-31');
});
test('parseYmd 解析/容错', () {
expect(parseYmd('2026-06-19'), DateTime(2026, 6, 19));
expect(parseYmd('2026-06-19T08:00:00'), DateTime(2026, 6, 19));
expect(parseYmd(null), isNull);
expect(parseYmd(''), isNull);
});
test('composeYmd 正常组合', () {
expect(composeYmd(2026, 6, 15), '2026-06-15');
});
test('composeYmd 把超界的日 clamp 回当月最大值', () {
expect(composeYmd(2026, 2, 31), '2026-02-28'); // 平年 2 月
expect(composeYmd(2024, 2, 31), '2024-02-29'); // 闰年 2 月
expect(composeYmd(2026, 4, 31), '2026-04-30'); // 4 月 30 天
});
test('composeYmd 任一为空返回 null', () {
expect(composeYmd(null, 6, 15), isNull);
expect(composeYmd(2026, null, 15), isNull);
expect(composeYmd(2026, 6, null), isNull);
});
});
group('DatePickerField', () {
testWidgets('渲染三个年/月/日下拉框', (tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: DatePickerField(value: '2026-06-19', onChanged: (_) {}),
),
));
await tester.pumpAndSettle();
expect(find.byType(DropdownMenu<int>), findsNWidgets(3));
});
testWidgets('初始值回显到三个下拉框', (tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: DatePickerField(value: '2026-06-19', onChanged: (_) {}),
),
));
await tester.pumpAndSettle();
expect(find.widgetWithText(DropdownMenu<int>, '2026'), findsOneWidget);
expect(find.widgetWithText(DropdownMenu<int>, '6'), findsOneWidget);
expect(find.widgetWithText(DropdownMenu<int>, '19'), findsOneWidget);
});
});
}
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jiu_client/widgets/searchable_option_field.dart';
void main() {
Widget host({
required ValueChanged<int?> onChanged,
Future<int?> Function(String)? onCreate,
}) {
return MaterialApp(
home: Scaffold(
body: SearchableOptionField(
options: [OptionItem(id: 1, name: '53度')],
selectedId: null,
hint: '选择系列',
dialogTitle: '选择系列',
onChanged: onChanged,
onCreate: onCreate,
),
),
);
}
testWidgets('搜不到时显示「新增到基础数据」入口', (tester) async {
await tester.pumpWidget(host(onChanged: (_) {}, onCreate: (_) async => 99));
// 打开搜索对话框
await tester.tap(find.byType(SearchableOptionField));
await tester.pumpAndSettle();
// 输入不存在的关键字
await tester.enterText(find.byType(TextField).first, '52度');
await tester.pumpAndSettle();
expect(find.textContaining('新增「52度」到基础数据'), findsOneWidget);
});
testWidgets('确认新增后回填选中新 id', (tester) async {
int? selected;
await tester.pumpWidget(
host(onChanged: (v) => selected = v, onCreate: (_) async => 99));
await tester.tap(find.byType(SearchableOptionField));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).first, '52度');
await tester.pumpAndSettle();
await tester.tap(find.textContaining('新增「52度」到基础数据'));
await tester.pumpAndSettle();
// 确认框
await tester.tap(find.text('确认新增'));
await tester.pumpAndSettle();
expect(selected, 99);
});
testWidgets('未提供 onCreate 时不显示新增入口', (tester) async {
await tester.pumpWidget(host(onChanged: (_) {}));
await tester.tap(find.byType(SearchableOptionField));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).first, '52度');
await tester.pumpAndSettle();
expect(find.textContaining('新增'), findsNothing);
});
}