| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726 |
- // ignore_for_file: use_build_context_synchronously
- import 'package:flutter/material.dart';
- import 'package:flutter/services.dart';
- import 'package:tdesign_flutter/tdesign_flutter.dart';
- import '../../../core/i18n/app_localizations.dart';
- import '../../../core/theme/app_colors.dart';
- import '../../../core/theme/app_colors_extension.dart';
- /// 加班明细输入数据。
- class OvertimeDetailData {
- final String empCode;
- final String shiftType;
- final String startDate;
- final String endDate;
- final double approvedHours;
- final String isApproved;
- final String directOt;
- final double otQuantity;
- final String reason;
- final String handleMethod;
- final String chargeMethod;
- final String remark;
- final String isClosed;
- final String outBillNo;
- const OvertimeDetailData({
- required this.empCode,
- required this.shiftType,
- required this.startDate,
- required this.endDate,
- required this.approvedHours,
- required this.isApproved,
- required this.directOt,
- required this.otQuantity,
- required this.reason,
- required this.handleMethod,
- required this.chargeMethod,
- required this.remark,
- required this.isClosed,
- required this.outBillNo,
- });
- }
- /// 加班明细录入弹窗。
- class OvertimeDetailDialog extends StatefulWidget {
- final AppLocalizations l10n;
- final OvertimeDetailData? initialData;
- const OvertimeDetailDialog({super.key, required this.l10n, this.initialData});
- /// 显示弹窗,返回 [OvertimeDetailData] 或 `null`(取消时)。
- static Future<OvertimeDetailData?> show(
- BuildContext context, {
- required AppLocalizations l10n,
- OvertimeDetailData? initialData,
- }) {
- FocusScope.of(context).unfocus();
- return Navigator.push<OvertimeDetailData>(
- context,
- TDSlidePopupRoute<OvertimeDetailData>(
- slideTransitionFrom: SlideTransitionFrom.bottom,
- isDismissible: true,
- builder: (_) =>
- OvertimeDetailDialog(l10n: l10n, initialData: initialData),
- ),
- );
- }
- @override
- State<OvertimeDetailDialog> createState() => _OvertimeDetailDialogState();
- }
- class _OvertimeDetailDialogState extends State<OvertimeDetailDialog> {
- final _empCodeCtrl = TextEditingController();
- final _shiftTypeCtrl = TextEditingController();
- String _startDate = '';
- String _endDate = '';
- final _approvedHoursCtrl = TextEditingController();
- String _isApproved = 'N';
- String _directOt = 'N';
- final _reasonCtrl = TextEditingController();
- final _handleMethodCtrl = TextEditingController();
- final _chargeMethodCtrl = TextEditingController();
- final _remarkCtrl = TextEditingController();
- String _isClosed = 'N';
- final _outBillNoCtrl = TextEditingController();
- final _scrollCtrl = ScrollController();
- AppLocalizations get _l10n => widget.l10n;
- bool get _isEdit => widget.initialData != null;
- @override
- void initState() {
- super.initState();
- final d = widget.initialData;
- if (d != null) {
- _empCodeCtrl.text = d.empCode;
- _shiftTypeCtrl.text = d.shiftType;
- _startDate = d.startDate;
- _endDate = d.endDate;
- _approvedHoursCtrl.text = d.approvedHours > 0
- ? d.approvedHours.toString()
- : '';
- _isApproved = d.isApproved;
- _directOt = d.directOt;
- _reasonCtrl.text = d.reason;
- _handleMethodCtrl.text = d.handleMethod;
- _chargeMethodCtrl.text = d.chargeMethod;
- _remarkCtrl.text = d.remark;
- _isClosed = d.isClosed;
- _outBillNoCtrl.text = d.outBillNo;
- }
- }
- @override
- void dispose() {
- _empCodeCtrl.dispose();
- _shiftTypeCtrl.dispose();
- _approvedHoursCtrl.dispose();
- _reasonCtrl.dispose();
- _handleMethodCtrl.dispose();
- _chargeMethodCtrl.dispose();
- _remarkCtrl.dispose();
- _outBillNoCtrl.dispose();
- _scrollCtrl.dispose();
- super.dispose();
- }
- void _confirm() {
- if (_empCodeCtrl.text.trim().isEmpty) {
- TDToast.showText('请输入员工代号', context: context);
- return;
- }
- final otQuantity = double.tryParse(_approvedHoursCtrl.text) ?? 0;
- Navigator.pop(
- context,
- OvertimeDetailData(
- empCode: _empCodeCtrl.text.trim(),
- shiftType: _shiftTypeCtrl.text,
- startDate: _startDate,
- endDate: _endDate,
- approvedHours: otQuantity,
- isApproved: _isApproved,
- directOt: _directOt,
- otQuantity: otQuantity,
- reason: _reasonCtrl.text,
- handleMethod: _handleMethodCtrl.text,
- chargeMethod: _chargeMethodCtrl.text,
- remark: _remarkCtrl.text,
- isClosed: _isClosed,
- outBillNo: _outBillNoCtrl.text,
- ),
- );
- }
- @override
- Widget build(BuildContext context) {
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- return AnimatedPadding(
- padding: EdgeInsets.only(
- bottom: MediaQuery.of(context).viewInsets.bottom,
- ),
- duration: const Duration(milliseconds: 200),
- child: SafeArea(
- child: ConstrainedBox(
- constraints: BoxConstraints(
- maxHeight: MediaQuery.of(context).size.height * 0.85,
- ),
- child: Container(
- decoration: BoxDecoration(
- color: colors.bgPage,
- borderRadius: const BorderRadius.vertical(
- top: Radius.circular(16),
- ),
- ),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- _buildHeader(colors),
- Flexible(
- child: GestureDetector(
- onTap: () => FocusScope.of(context).unfocus(),
- behavior: HitTestBehavior.translucent,
- child: SingleChildScrollView(
- controller: _scrollCtrl,
- keyboardDismissBehavior:
- ScrollViewKeyboardDismissBehavior.manual,
- padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- _inputCard(
- label: '员工代号',
- required: true,
- controller: _empCodeCtrl,
- hintText: '请输入',
- ),
- const SizedBox(height: 12),
- _inputCard(
- label: '班别',
- required: false,
- controller: _shiftTypeCtrl,
- hintText: '请输入',
- ),
- const SizedBox(height: 12),
- _datePickerCard(
- label: '开始日期',
- value: _startDate,
- hint: '请选择',
- colors: colors,
- onPick: (d) => setState(() => _startDate = d),
- onClear: _startDate.isNotEmpty
- ? () => setState(() => _startDate = '')
- : null,
- ),
- const SizedBox(height: 12),
- _datePickerCard(
- label: '结束日期',
- value: _endDate,
- hint: '请选择',
- colors: colors,
- onPick: (d) => setState(() => _endDate = d),
- onClear: _endDate.isNotEmpty
- ? () => setState(() => _endDate = '')
- : null,
- ),
- const SizedBox(height: 12),
- _inputCard(
- label: '加班核准量',
- required: false,
- controller: _approvedHoursCtrl,
- hintText: '0',
- keyboardType: const TextInputType.numberWithOptions(
- decimal: true,
- ),
- inputFormatters: [
- FilteringTextInputFormatter.allow(
- RegExp(r'^\d*\.?\d{0,2}$'),
- ),
- ],
- onChanged: () => setState(() {}),
- ),
- const SizedBox(height: 12),
- _toggleCard(
- label: '核准',
- value: _isApproved,
- options: const ['Y', 'N'],
- onChanged: (v) => setState(() => _isApproved = v),
- ),
- const SizedBox(height: 12),
- _toggleCard(
- label: '直接转加班',
- value: _directOt,
- options: const ['Y', 'N'],
- onChanged: (v) => setState(() => _directOt = v),
- ),
- const SizedBox(height: 12),
- // 加班数量(只读,=核准量)
- _readOnlyCard(
- label: '加班数量',
- value: _approvedHoursCtrl.text.isNotEmpty
- ? _approvedHoursCtrl.text
- : '0',
- ),
- const SizedBox(height: 12),
- _inputCard(
- label: '原因说明',
- required: false,
- controller: _reasonCtrl,
- hintText: '请输入',
- ),
- const SizedBox(height: 12),
- _inputCard(
- label: '处理方式',
- required: false,
- controller: _handleMethodCtrl,
- hintText: '请输入',
- ),
- const SizedBox(height: 12),
- _inputCard(
- label: '收费方式',
- required: false,
- controller: _chargeMethodCtrl,
- hintText: '请输入',
- ),
- const SizedBox(height: 12),
- _inputCard(
- label: '备注',
- required: false,
- controller: _remarkCtrl,
- hintText: '请输入',
- ),
- const SizedBox(height: 12),
- _toggleCard(
- label: '结案',
- value: _isClosed,
- options: const ['Y', 'N'],
- onChanged: (v) => setState(() => _isClosed = v),
- ),
- const SizedBox(height: 12),
- _inputCard(
- label: '转出单号',
- required: false,
- controller: _outBillNoCtrl,
- hintText: '请输入',
- ),
- ],
- ),
- ),
- ),
- ),
- Container(
- padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
- decoration: BoxDecoration(
- color: colors.bgCard,
- border: Border(
- top: BorderSide(color: colors.border, width: 0.5),
- ),
- ),
- child: Row(
- children: [
- Expanded(
- child: TDButton(
- text: _l10n.get('cancel'),
- size: TDButtonSize.large,
- type: TDButtonType.outline,
- shape: TDButtonShape.rectangle,
- theme: TDButtonTheme.defaultTheme,
- onTap: () => Navigator.pop(context),
- ),
- ),
- const SizedBox(width: 12),
- Expanded(
- child: TDButton(
- text: _isEdit ? '确认修改' : '添加',
- size: TDButtonSize.large,
- type: TDButtonType.fill,
- shape: TDButtonShape.rectangle,
- theme: TDButtonTheme.primary,
- onTap: _confirm,
- ),
- ),
- ],
- ),
- ),
- ],
- ),
- ),
- ),
- ),
- );
- }
- // ── 标题栏 ──
- Widget _buildHeader(AppColorsExtension colors) {
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Center(
- child: Container(
- margin: const EdgeInsets.only(top: 8, bottom: 4),
- width: 36,
- height: 4,
- decoration: BoxDecoration(
- color: colors.border,
- borderRadius: BorderRadius.circular(2),
- ),
- ),
- ),
- Padding(
- padding: const EdgeInsets.fromLTRB(20, 8, 12, 16),
- child: Row(
- children: [
- const SizedBox(width: 24),
- Expanded(
- child: Center(
- child: Text(
- '加班明细',
- style: TextStyle(
- fontSize: AppFontSizes.title,
- fontWeight: FontWeight.w600,
- color: colors.textPrimary,
- ),
- ),
- ),
- ),
- GestureDetector(
- onTap: () => Navigator.pop(context),
- child: Padding(
- padding: const EdgeInsets.all(4),
- child: Icon(
- Icons.close,
- size: 20,
- color: colors.textSecondary,
- ),
- ),
- ),
- ],
- ),
- ),
- ],
- );
- }
- // ── 通用输入卡片 ──
- Widget _inputCard({
- required String label,
- required bool required,
- required TextEditingController controller,
- required String hintText,
- TextInputType? keyboardType,
- List<TextInputFormatter>? inputFormatters,
- VoidCallback? onChanged,
- }) {
- final tdTheme = TDTheme.of(context);
- final hasValue = controller.text.isNotEmpty;
- return Container(
- padding: const EdgeInsets.only(left: 16, right: 10, top: 12, bottom: 12),
- decoration: BoxDecoration(
- color: tdTheme.bgColorContainer,
- borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
- border: Border.all(color: tdTheme.componentStrokeColor),
- ),
- child: Row(
- children: [
- TDText(
- label,
- maxLines: 1,
- overflow: TextOverflow.visible,
- font: tdTheme.fontBodyLarge,
- fontWeight: FontWeight.w400,
- style: const TextStyle(letterSpacing: 0),
- ),
- if (required)
- Padding(
- padding: const EdgeInsets.only(left: 4),
- child: TDText(
- '*',
- font: tdTheme.fontBodyLarge,
- fontWeight: FontWeight.w400,
- style: TextStyle(color: tdTheme.errorColor6),
- ),
- ),
- const SizedBox(width: 12),
- Expanded(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.end,
- mainAxisSize: MainAxisSize.max,
- children: [
- Flexible(
- child: TextField(
- controller: controller,
- textAlign: TextAlign.end,
- keyboardType: keyboardType,
- inputFormatters: inputFormatters,
- style: TextStyle(
- fontSize: 16,
- color: tdTheme.textColorPrimary,
- ),
- decoration: InputDecoration(
- hintText: hintText,
- hintStyle: TextStyle(
- fontSize: 16,
- color: tdTheme.textColorPlaceholder,
- ),
- border: InputBorder.none,
- isDense: true,
- contentPadding: EdgeInsets.zero,
- ),
- onChanged: (_) => setState(() {}),
- ),
- ),
- const SizedBox(width: 4),
- SizedBox(
- width: 18,
- height: 18,
- child: hasValue
- ? GestureDetector(
- onTap: () {
- controller.clear();
- onChanged?.call();
- setState(() {});
- },
- child: Icon(
- Icons.close,
- size: 18,
- color: tdTheme.textColorPlaceholder,
- ),
- )
- : null,
- ),
- ],
- ),
- ),
- ],
- ),
- );
- }
- // ── 日期 Picker 卡片 ──
- Widget _datePickerCard({
- required String label,
- required String value,
- required String hint,
- required AppColorsExtension colors,
- required ValueChanged<String> onPick,
- VoidCallback? onClear,
- }) {
- final tdTheme = TDTheme.of(context);
- final hasValue = value.isNotEmpty;
- return GestureDetector(
- onTap: () => _pickDate(onPick),
- child: Container(
- padding: const EdgeInsets.only(
- left: 16,
- right: 10,
- top: 12,
- bottom: 12,
- ),
- decoration: BoxDecoration(
- color: tdTheme.bgColorContainer,
- borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
- border: Border.all(color: tdTheme.componentStrokeColor),
- ),
- child: Row(
- children: [
- TDText(
- label,
- maxLines: 1,
- overflow: TextOverflow.visible,
- font: tdTheme.fontBodyLarge,
- fontWeight: FontWeight.w400,
- style: const TextStyle(letterSpacing: 0),
- ),
- const SizedBox(width: 12),
- Expanded(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.end,
- mainAxisSize: MainAxisSize.max,
- children: [
- Flexible(
- child: TDText(
- value.isEmpty ? hint : value,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- font: tdTheme.fontBodyLarge,
- fontWeight: FontWeight.w400,
- textColor: value.isEmpty
- ? tdTheme.textColorPlaceholder
- : tdTheme.textColorPrimary,
- textAlign: TextAlign.end,
- ),
- ),
- const SizedBox(width: 4),
- SizedBox(
- width: 18,
- height: 18,
- child: hasValue
- ? GestureDetector(
- onTap: onClear,
- child: Icon(
- Icons.close,
- size: 18,
- color: tdTheme.textColorPlaceholder,
- ),
- )
- : Icon(
- Icons.chevron_right,
- size: 18,
- color: tdTheme.textColorPlaceholder,
- ),
- ),
- ],
- ),
- ),
- ],
- ),
- ),
- );
- }
- // ── 切换卡片(Y/N) ──
- Widget _toggleCard({
- required String label,
- required String value,
- required List<String> options,
- required ValueChanged<String> onChanged,
- }) {
- final tdTheme = TDTheme.of(context);
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- return Container(
- padding: const EdgeInsets.only(left: 16, right: 10, top: 10, bottom: 10),
- decoration: BoxDecoration(
- color: tdTheme.bgColorContainer,
- borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
- border: Border.all(color: tdTheme.componentStrokeColor),
- ),
- child: Row(
- children: [
- TDText(
- label,
- maxLines: 1,
- overflow: TextOverflow.visible,
- font: tdTheme.fontBodyLarge,
- fontWeight: FontWeight.w400,
- style: const TextStyle(letterSpacing: 0),
- ),
- const Spacer(),
- ...options.map((opt) {
- final sel = value == opt;
- return Padding(
- padding: const EdgeInsets.only(left: 8),
- child: GestureDetector(
- onTap: () => onChanged(opt),
- child: Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 12,
- vertical: 6,
- ),
- decoration: BoxDecoration(
- color: sel
- ? colors.primary.withValues(alpha: 0.1)
- : tdTheme.bgColorContainer,
- borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
- border: Border.all(
- color: sel
- ? colors.primary
- : tdTheme.componentStrokeColor,
- ),
- ),
- child: Text(
- opt == 'Y' ? '是' : '否',
- style: TextStyle(
- fontSize: 14,
- color: sel ? colors.primary : tdTheme.textColorPrimary,
- ),
- ),
- ),
- ),
- );
- }),
- ],
- ),
- );
- }
- // ── 只读卡片 ──
- Widget _readOnlyCard({required String label, required String value}) {
- final tdTheme = TDTheme.of(context);
- return Container(
- padding: const EdgeInsets.only(left: 16, right: 10, top: 12, bottom: 12),
- decoration: BoxDecoration(
- color: tdTheme.bgColorContainer,
- borderRadius: BorderRadius.circular(tdTheme.radiusDefault),
- border: Border.all(color: tdTheme.componentStrokeColor),
- ),
- child: Row(
- children: [
- TDText(
- label,
- maxLines: 1,
- overflow: TextOverflow.visible,
- font: tdTheme.fontBodyLarge,
- fontWeight: FontWeight.w400,
- style: const TextStyle(letterSpacing: 0),
- ),
- const SizedBox(width: 12),
- Expanded(
- child: Row(
- mainAxisAlignment: MainAxisAlignment.end,
- mainAxisSize: MainAxisSize.max,
- children: [
- Flexible(
- child: TDText(
- value,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- font: tdTheme.fontBodyLarge,
- fontWeight: FontWeight.w400,
- textColor: tdTheme.textColorPrimary,
- textAlign: TextAlign.end,
- ),
- ),
- ],
- ),
- ),
- ],
- ),
- );
- }
- void _pickDate(ValueChanged<String> onPick) {
- final l10n = AppLocalizations.of(context);
- final colors = Theme.of(context).extension<AppColorsExtension>()!;
- final now = DateTime.now();
- FocusManager.instance.primaryFocus?.unfocus();
- TDPicker.showDatePicker(
- context,
- title: l10n.get('selectDate'),
- backgroundColor: colors.bgCard,
- useYear: true,
- useMonth: true,
- useDay: true,
- useHour: false,
- useMinute: false,
- useSecond: false,
- useWeekDay: false,
- dateStart: const [2020, 1, 1],
- dateEnd: [now.year + 1, 12, 31],
- initialDate: [now.year, now.month, now.day],
- onConfirm: (selected) {
- onPick(
- '${selected['year']}-${selected['month'].toString().padLeft(2, '0')}-${selected['day'].toString().padLeft(2, '0')}',
- );
- Navigator.of(context).pop();
- },
- );
- }
- }
|