Selaa lähdekoodia

fix #19900 #19888 加班申请单、用车申请单

chengc 2 viikkoa sitten
vanhempi
commit
404e56ceb3

+ 1 - 0
assets/i18n/en.json

@@ -126,6 +126,7 @@
     "goToSettings": "Settings",
     "selectDateTime": "Select date & time",
     "selectDate": "Select date",
+    "selectTime": "Select time",
     "optional": "Optional",
     "pleaseEnter": "Please enter",
     "enterNumber": "Please enter a number",

+ 1 - 0
assets/i18n/zh_CN.json

@@ -123,6 +123,7 @@
     "goToSettings": "去设置",
     "selectDateTime": "选择日期时间",
     "selectDate": "选择日期",
+    "selectTime": "选择时间",
     "optional": "选填",
     "pleaseEnter": "请输入",
     "enterNumber": "请输入数字",

+ 1 - 0
assets/i18n/zh_TW.json

@@ -126,6 +126,7 @@
     "goToSettings": "去設定",
     "selectDateTime": "選擇日期時間",
     "selectDate": "選擇日期",
+    "selectTime": "選擇時間",
     "optional": "選填",
     "pleaseEnter": "請輸入",
     "enterNumber": "請輸入數字",

+ 32 - 47
lib/features/overtime/overtime_apply_create_page.dart

@@ -523,53 +523,32 @@ class _OvertimeApplyCreatePageState
                               colors,
                             ),
                             _detailLabel(
-                              '${l10n.get('applyDate')}: ${d.jbDate}',
+                              '${l10n.get('jbDate')}: ${d.jbDate}',
                               colors,
                             ),
                           ],
                           if (d.startTime.isNotEmpty) ...[
                             const SizedBox(height: 2),
-                            Row(
-                              crossAxisAlignment: CrossAxisAlignment.center,
-                              children: [
-                                Expanded(
-                                  child: _detailLabel(
-                                    '${l10n.get('startTime')}: ${d.startTime}',
-                                    colors,
-                                  ),
-                                ),
-                                if (_dayOfWeekLabel(d.startTime, l10n) != null)
-                                  _dayOfWeekTag(
-                                    _dayOfWeekLabel(d.startTime, l10n)!,
-                                  ),
-                              ],
+                            _detailLabel(
+                              '${l10n.get('startTime')}: ${_fmtTime(d.startTime)}',
+                              colors,
                             ),
                           ],
                           if (d.endTime.isNotEmpty) ...[
                             const SizedBox(height: 2),
-                            Row(
-                              crossAxisAlignment: CrossAxisAlignment.center,
-                              children: [
-                                Expanded(
-                                  child: _detailLabel(
-                                    '${l10n.get('endTime')}: ${d.endTime}',
-                                    colors,
-                                  ),
-                                ),
-                                if (_dayOfWeekLabel(d.endTime, l10n) != null)
-                                  _dayOfWeekTag(
-                                    _dayOfWeekLabel(d.endTime, l10n)!,
-                                  ),
-                              ],
-                            ),
-                          ],
-                          if (d.jbDays > 0) ...[
-                            const SizedBox(height: 2),
                             _detailLabel(
-                              '${l10n.get('overtimeDays')}: ${d.jbDays.toStringAsFixed(1)}',
+                              '${l10n.get('endTime')}: ${_fmtTime(d.endTime)}',
                               colors,
                             ),
                           ],
+                          // TODO: 加班天数暂时隐藏
+                          // if (d.jbDays > 0) ...[
+                          //   const SizedBox(height: 2),
+                          //   _detailLabel(
+                          //     '${l10n.get('overtimeDays')}: ${d.jbDays.toStringAsFixed(1)}',
+                          //     colors,
+                          //   ),
+                          // ],
                           if (d.attPeriod.isNotEmpty) ...[
                             const SizedBox(height: 2),
                             _detailLabel(
@@ -577,19 +556,20 @@ class _OvertimeApplyCreatePageState
                               colors,
                             ),
                           ],
-                          if (d.compensationType.isNotEmpty) ...[
-                            const SizedBox(height: 2),
-                            _detailLabel(
-                              '${l10n.get('compensationType')}: ${_compensationTypeLabel(d.compensationType, l10n)}',
-                              colors,
-                            ),
-                            if (d.compensationType != 'NO_COMPENSATION' &&
-                                d.compensationCount > 0)
-                              _detailLabel(
-                                '${l10n.get('compensationCount')}: ${d.compensationCount.toStringAsFixed(1)}',
-                                colors,
-                              ),
-                          ],
+                          // TODO: 补偿类型、补偿次数暂时隐藏
+                          // if (d.compensationType.isNotEmpty) ...[
+                          //   const SizedBox(height: 2),
+                          //   _detailLabel(
+                          //     '${l10n.get('compensationType')}: ${_compensationTypeLabel(d.compensationType, l10n)}',
+                          //     colors,
+                          //   ),
+                          //   if (d.compensationType != 'NO_COMPENSATION' &&
+                          //       d.compensationCount > 0)
+                          //     _detailLabel(
+                          //       '${l10n.get('compensationCount')}: ${d.compensationCount.toStringAsFixed(1)}',
+                          //       colors,
+                          //     ),
+                          // ],
                           if (d.adr.isNotEmpty) ...[
                             const SizedBox(height: 2),
                             _detailLabel(
@@ -676,6 +656,11 @@ class _OvertimeApplyCreatePageState
     }
   }
 
+  String _fmtTime(String dt) {
+    if (dt.length >= 16) return dt.substring(11, 16);
+    return dt;
+  }
+
   String? _dayOfWeekLabel(String dateTimeStr, AppLocalizations l10n) {
     final dt = DateTime.tryParse(dateTimeStr);
     if (dt == null) return null;

+ 31 - 41
lib/features/overtime/overtime_apply_detail_page.dart

@@ -323,6 +323,10 @@ class _OvertimeApplyDetailPageState
           '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
     }
 
+    String formatHHmm(DateTime dt) {
+      return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
+    }
+
     return GestureDetector(
       onTap: () => OvertimeApplyDetailViewDialog.show(context, d),
       child: Container(
@@ -368,64 +372,50 @@ class _OvertimeApplyDetailPageState
               ),
               if (d.jbDate != null)
                 _detailLabel(
-                  '${l10n.get('applyDate')}: ${du.DateUtils.formatDate(d.jbDate!)}',
+                  '${l10n.get('jbDate')}: ${du.DateUtils.formatDate(d.jbDate!)}',
                   colors,
                 ),
             ],
             if (d.startTime != null) ...[
               const SizedBox(height: 2),
-              Row(
-                crossAxisAlignment: CrossAxisAlignment.center,
-                children: [
-                  Expanded(
-                    child: _detailLabel(
-                      '${l10n.get('startTime')}: ${formatDateTime(d.startTime!)}',
-                      colors,
-                    ),
-                  ),
-                  _dayOfWeekTag(_dayOfWeekLabelFromDate(d.startTime!, l10n)),
-                ],
+              _detailLabel(
+                '${l10n.get('startTime')}: ${formatHHmm(d.startTime!)}',
+                colors,
               ),
             ],
             if (d.endTime != null) ...[
               const SizedBox(height: 2),
-              Row(
-                crossAxisAlignment: CrossAxisAlignment.center,
-                children: [
-                  Expanded(
-                    child: _detailLabel(
-                      '${l10n.get('endTime')}: ${formatDateTime(d.endTime!)}',
-                      colors,
-                    ),
-                  ),
-                  _dayOfWeekTag(_dayOfWeekLabelFromDate(d.endTime!, l10n)),
-                ],
-              ),
-            ],
-            if (d.jbDays > 0) ...[
-              const SizedBox(height: 2),
               _detailLabel(
-                '${l10n.get('overtimeDays')}: ${d.jbDays.toStringAsFixed(1)}',
+                '${l10n.get('endTime')}: ${formatHHmm(d.endTime!)}',
                 colors,
               ),
             ],
+            // TODO: 加班天数暂时隐藏
+            // if (d.jbDays > 0) ...[
+            //   const SizedBox(height: 2),
+            //   _detailLabel(
+            //     '${l10n.get('overtimeDays')}: ${d.jbDays.toStringAsFixed(1)}',
+            //     colors,
+            //   ),
+            // ],
             if (d.attPeriod.isNotEmpty) ...[
               const SizedBox(height: 2),
               _detailLabel('${l10n.get('attPeriod')}: ${d.attPeriod}', colors),
             ],
-            if (d.compensationType.isNotEmpty) ...[
-              const SizedBox(height: 2),
-              _detailLabel(
-                '${l10n.get('compensationType')}: ${_compensationTypeLabel(d.compensationType, l10n)}',
-                colors,
-              ),
-              if (d.compensationType != 'NO_COMPENSATION' &&
-                  d.compensationCount > 0)
-                _detailLabel(
-                  '${l10n.get('compensationCount')}: ${d.compensationCount.toStringAsFixed(1)}',
-                  colors,
-                ),
-            ],
+            // TODO: 补偿类型、补偿次数暂时隐藏
+            // if (d.compensationType.isNotEmpty) ...[
+            //   const SizedBox(height: 2),
+            //   _detailLabel(
+            //     '${l10n.get('compensationType')}: ${_compensationTypeLabel(d.compensationType, l10n)}',
+            //     colors,
+            //   ),
+            //   if (d.compensationType != 'NO_COMPENSATION' &&
+            //       d.compensationCount > 0)
+            //     _detailLabel(
+            //       '${l10n.get('compensationCount')}: ${d.compensationCount.toStringAsFixed(1)}',
+            //       colors,
+            //     ),
+            // ],
             if (d.adr.isNotEmpty) ...[
               const SizedBox(height: 2),
               _detailLabel('${l10n.get('adr')}: ${d.adr}', colors),

+ 32 - 47
lib/features/overtime/overtime_apply_edit_page.dart

@@ -495,53 +495,32 @@ class _OvertimeApplyEditPageState extends ConsumerState<OvertimeApplyEditPage> {
                               colors,
                             ),
                             _detailLabel(
-                              '${l10n.get('applyDate')}: ${d.jbDate}',
+                              '${l10n.get('jbDate')}: ${d.jbDate}',
                               colors,
                             ),
                           ],
                           if (d.startTime.isNotEmpty) ...[
                             const SizedBox(height: 2),
-                            Row(
-                              crossAxisAlignment: CrossAxisAlignment.center,
-                              children: [
-                                Expanded(
-                                  child: _detailLabel(
-                                    '${l10n.get('startTime')}: ${d.startTime}',
-                                    colors,
-                                  ),
-                                ),
-                                if (_dayOfWeekLabel(d.startTime, l10n) != null)
-                                  _dayOfWeekTag(
-                                    _dayOfWeekLabel(d.startTime, l10n)!,
-                                  ),
-                              ],
+                            _detailLabel(
+                              '${l10n.get('startTime')}: ${_fmtTime(d.startTime)}',
+                              colors,
                             ),
                           ],
                           if (d.endTime.isNotEmpty) ...[
                             const SizedBox(height: 2),
-                            Row(
-                              crossAxisAlignment: CrossAxisAlignment.center,
-                              children: [
-                                Expanded(
-                                  child: _detailLabel(
-                                    '${l10n.get('endTime')}: ${d.endTime}',
-                                    colors,
-                                  ),
-                                ),
-                                if (_dayOfWeekLabel(d.endTime, l10n) != null)
-                                  _dayOfWeekTag(
-                                    _dayOfWeekLabel(d.endTime, l10n)!,
-                                  ),
-                              ],
-                            ),
-                          ],
-                          if (d.jbDays > 0) ...[
-                            const SizedBox(height: 2),
                             _detailLabel(
-                              '${l10n.get('overtimeDays')}: ${d.jbDays.toStringAsFixed(1)}',
+                              '${l10n.get('endTime')}: ${_fmtTime(d.endTime)}',
                               colors,
                             ),
                           ],
+                          // TODO: 加班天数暂时隐藏
+                          // if (d.jbDays > 0) ...[
+                          //   const SizedBox(height: 2),
+                          //   _detailLabel(
+                          //     '${l10n.get('overtimeDays')}: ${d.jbDays.toStringAsFixed(1)}',
+                          //     colors,
+                          //   ),
+                          // ],
                           if (d.attPeriod.isNotEmpty) ...[
                             const SizedBox(height: 2),
                             _detailLabel(
@@ -549,19 +528,20 @@ class _OvertimeApplyEditPageState extends ConsumerState<OvertimeApplyEditPage> {
                               colors,
                             ),
                           ],
-                          if (d.compensationType.isNotEmpty) ...[
-                            const SizedBox(height: 2),
-                            _detailLabel(
-                              '${l10n.get('compensationType')}: ${_compensationTypeLabel(d.compensationType, l10n)}',
-                              colors,
-                            ),
-                            if (d.compensationType != 'NO_COMPENSATION' &&
-                                d.compensationCount > 0)
-                              _detailLabel(
-                                '${l10n.get('compensationCount')}: ${d.compensationCount.toStringAsFixed(1)}',
-                                colors,
-                              ),
-                          ],
+                          // TODO: 补偿类型、补偿次数暂时隐藏
+                          // if (d.compensationType.isNotEmpty) ...[
+                          //   const SizedBox(height: 2),
+                          //   _detailLabel(
+                          //     '${l10n.get('compensationType')}: ${_compensationTypeLabel(d.compensationType, l10n)}',
+                          //     colors,
+                          //   ),
+                          //   if (d.compensationType != 'NO_COMPENSATION' &&
+                          //       d.compensationCount > 0)
+                          //     _detailLabel(
+                          //       '${l10n.get('compensationCount')}: ${d.compensationCount.toStringAsFixed(1)}',
+                          //       colors,
+                          //     ),
+                          // ],
                           if (d.adr.isNotEmpty) ...[
                             const SizedBox(height: 2),
                             _detailLabel(
@@ -648,6 +628,11 @@ class _OvertimeApplyEditPageState extends ConsumerState<OvertimeApplyEditPage> {
     }
   }
 
+  String _fmtTime(String dt) {
+    if (dt.length >= 16) return dt.substring(11, 16);
+    return dt;
+  }
+
   String? _dayOfWeekLabel(String dateTimeStr, AppLocalizations l10n) {
     final dt = DateTime.tryParse(dateTimeStr);
     if (dt == null) return null;

+ 3 - 1
lib/features/overtime/overtime_apply_list_page.dart

@@ -384,7 +384,9 @@ class _OvertimeApplyListContent extends ConsumerWidget {
         : m.reason;
     return ListCard(
       cardNo: m.jbNo,
-      amount: m.totalJbHours > 0 ? '${m.totalJbHours.toStringAsFixed(1)}${l10n.get('hours')}' : '',
+      amount: m.totalJbHours > 0
+          ? '${m.totalJbHours.toStringAsFixed(1)}${l10n.get('hours')}'
+          : '',
       amountColor: colors.timePrimary,
       applicant: deptLabel,
       description: desc,

+ 60 - 47
lib/features/overtime/widgets/overtime_apply_detail_dialog.dart

@@ -353,11 +353,11 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
         startTime: _startTime,
         endTime: _endTime,
         jbHours: hours,
-        jbDays: double.tryParse(_daysCtrl.text) ?? 0,
+        jbDays: 0,
         attPeriod: _attPeriod,
         reason: _reasonCtrl.text.trim(),
         compensationType: _compensationType,
-        compensationCount: double.tryParse(_compCountCtrl.text) ?? 0,
+        compensationCount: 0,
         adr: _adrCtrl.text,
         rem: _remarkCtrl.text,
       ),
@@ -419,15 +419,16 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
                           const SizedBox(height: 12),
                           _buildDeptPicker(colors),
                           const SizedBox(height: 12),
-                          _buildDaysInput(colors),
-                          const SizedBox(height: 12),
                           _buildAttPeriodInput(colors),
                           const SizedBox(height: 12),
                           _buildReasonInput(colors),
-                          const SizedBox(height: 12),
-                          _buildCompensationTypeSelector(colors),
-                          const SizedBox(height: 12),
-                          _buildCompCountInput(colors),
+                          // TODO: 加班天数、补偿类型、补偿次数暂时隐藏
+                          // const SizedBox(height: 12),
+                          // _buildDaysInput(colors),
+                          // const SizedBox(height: 12),
+                          // _buildCompensationTypeSelector(colors),
+                          // const SizedBox(height: 12),
+                          // _buildCompCountInput(colors),
                           const SizedBox(height: 12),
                           _buildAdrInput(colors),
                           const SizedBox(height: 12),
@@ -738,32 +739,33 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
     }
   }
 
-  // ── 4. 申请日期 ──
+  // ── 4. 加班日期 ──
   Widget _buildJbDatePicker(AppColorsExtension colors) {
     return _datePickerCard(
-      label: _l10n.get('applyDate'),
+      label: _l10n.get('jbDate'),
       value: _jbDate,
       hint: _l10n.get('pleaseSelect'),
       colors: colors,
       required: true,
+      showDayOfWeek: true,
       onPick: (d) => setState(() => _jbDate = d),
-      onClear: _jbDate.isNotEmpty ? () => setState(() => _jbDate = '') : null,
     );
   }
 
-  // ── 4. 开始时间 ──
+  // ── 5. 开始时间 ──
   Widget _buildStartTimePicker(AppColorsExtension colors) {
     return _datePickerCard(
       label: _l10n.get('startTime'),
-      value: _startTime,
+      value: _startTime.isNotEmpty ? _fmtTime(_startTime) : '',
       hint: _l10n.get('pleaseSelect'),
       colors: colors,
       required: true,
-      showDayOfWeek: true,
+      isTimeOnly: true,
       onPick: (d) {
+        final dateStr = _jbDate.isNotEmpty ? _jbDate : _todayStr();
         setState(() {
-          _startTime = d;
-          if (d.length >= 7) _attPeriod = d.substring(0, 7);
+          _startTime = '$dateStr $d';
+          if (dateStr.length >= 7) _attPeriod = dateStr.substring(0, 7);
           _autoCalcHours();
         });
       },
@@ -778,18 +780,19 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
     );
   }
 
-  // ── 5. 结束时间 ──
+  // ── 6. 结束时间 ──
   Widget _buildEndTimePicker(AppColorsExtension colors) {
     return _datePickerCard(
       label: _l10n.get('endTime'),
-      value: _endTime,
+      value: _endTime.isNotEmpty ? _fmtTime(_endTime) : '',
       hint: _l10n.get('pleaseSelect'),
       colors: colors,
       required: true,
-      showDayOfWeek: true,
+      isTimeOnly: true,
       onPick: (d) {
+        final dateStr = _jbDate.isNotEmpty ? _jbDate : _todayStr();
         setState(() {
-          _endTime = d;
+          _endTime = '$dateStr $d';
           _autoCalcHours();
         });
       },
@@ -804,23 +807,18 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
     );
   }
 
+  String _fmtTime(String dt) {
+    if (dt.length >= 16) return dt.substring(11, 16);
+    return dt;
+  }
+
   void _autoCalcHours() {
     if (_startTime.isEmpty || _endTime.isEmpty) return;
     final st = DateTime.tryParse(_startTime);
     final et = DateTime.tryParse(_endTime);
     if (st == null || et == null || et.isBefore(st)) return;
 
-    // 按自然日逐天累加,每天最多 8 小时
-    double totalHours = 0;
-    DateTime dayStart = DateTime(st.year, st.month, st.day);
-    while (dayStart.isBefore(et)) {
-      final dayEnd = dayStart.add(const Duration(days: 1));
-      final segStart = st.isAfter(dayStart) ? st : dayStart;
-      final segEnd = et.isBefore(dayEnd) ? et : dayEnd;
-      final segHours = segEnd.difference(segStart).inMinutes / 60.0;
-      totalHours += segHours > 8.0 ? 8.0 : segHours;
-      dayStart = dayEnd;
-    }
+    final totalHours = et.difference(st).inMinutes / 60.0;
 
     if (totalHours > 0) {
       _hoursCtrl.text = ((totalHours * 2).ceil() / 2.0).toStringAsFixed(1);
@@ -1177,12 +1175,21 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
     VoidCallback? onClear,
     bool required = false,
     bool showDayOfWeek = false,
+    bool isTimeOnly = false,
   }) {
     final tdTheme = TDTheme.of(context);
     final hasValue = value.isNotEmpty;
-    final dayOfWeek = showDayOfWeek && hasValue ? _dayOfWeekLabel(value) : null;
+    final jbDateFull = _jbDate.isNotEmpty ? _jbDate : _todayStr();
+    final dayOfWeek = showDayOfWeek && jbDateFull.isNotEmpty
+        ? _dayOfWeekLabel(jbDateFull)
+        : null;
     return GestureDetector(
-      onTap: () => _pickDateTime(label, onPick, currentValue: value),
+      onTap: () => _pickDateTime(
+        label,
+        onPick,
+        currentValue: isTimeOnly && _startTime.isNotEmpty ? _startTime : value,
+        isTimeOnly: isTimeOnly,
+      ),
       child: Container(
         padding: const EdgeInsets.only(
           left: 16,
@@ -1285,12 +1292,15 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
     String label,
     ValueChanged<String> onPick, {
     String currentValue = '',
+    bool isTimeOnly = false,
   }) {
     final l10n = _l10n;
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final now = DateTime.now();
     final isDateOnly =
-        label == _l10n.get('applyDate') || label == _l10n.get('attPeriod');
+        label == _l10n.get('applyDate') ||
+        label == _l10n.get('jbDate') ||
+        label == _l10n.get('attPeriod');
     final isAttPeriod = label == _l10n.get('attPeriod');
     final isStartTime = label == _l10n.get('startTime');
     final isEndTime = label == _l10n.get('endTime');
@@ -1304,7 +1314,6 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
 
     if (currentValue.isNotEmpty) {
       if (isAttPeriod) {
-        // yyyy-MM
         final parts = currentValue.split('-');
         if (parts.length == 2) {
           initYear = int.tryParse(parts[0]) ?? now.year;
@@ -1312,7 +1321,6 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
           initDay = 1;
         }
       } else {
-        // yyyy-MM-dd 或 yyyy-MM-dd HH:mm
         final dt = DateTime.tryParse(currentValue);
         if (dt != null) {
           initYear = dt.year;
@@ -1323,28 +1331,26 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
         }
       }
     } else if (isStartTime) {
-      // 开始时间无当前值:工作日默认 18:00,周末默认 09:00
       final date = _jbDate.isNotEmpty ? DateTime.tryParse(_jbDate) : null;
       final weekday = date?.weekday ?? now.weekday;
       initHour = (weekday <= DateTime.friday) ? 18 : 9;
       initMinute = 0;
     } else if (isEndTime) {
-      // 结束时间无当前值:工作日默认 19:00,周末默认 18:00
       final date = _jbDate.isNotEmpty ? DateTime.tryParse(_jbDate) : null;
       final weekday = date?.weekday ?? now.weekday;
       initHour = (weekday <= DateTime.friday) ? 19 : 18;
       initMinute = 0;
     }
 
-    final useTime = !isDateOnly;
+    final useTime = isTimeOnly || !isDateOnly;
     FocusManager.instance.primaryFocus?.unfocus();
     TDPicker.showDatePicker(
       context,
-      title: l10n.get('selectDate'),
+      title: isTimeOnly ? l10n.get('selectTime') : l10n.get('selectDate'),
       backgroundColor: colors.bgCard,
-      useYear: true,
-      useMonth: true,
-      useDay: !isAttPeriod,
+      useYear: !isTimeOnly,
+      useMonth: !isTimeOnly,
+      useDay: !isTimeOnly && !isAttPeriod,
       useHour: useTime,
       useMinute: useTime,
       useSecond: false,
@@ -1357,16 +1363,23 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
           ? [initYear, initMonth, initDay, initHour, initMinute]
           : [initYear, initMonth, initDay],
       onConfirm: (selected) {
-        final year = selected['year'];
-        final month = selected['month'].toString().padLeft(2, '0');
-        final day = selected['day'].toString().padLeft(2, '0');
         final hour = selected['hour']?.toString().padLeft(2, '0') ?? '00';
         final minute = selected['minute']?.toString().padLeft(2, '0') ?? '00';
-        if (isAttPeriod) {
+        if (isTimeOnly) {
+          onPick('$hour:$minute');
+        } else if (isAttPeriod) {
+          final year = selected['year'];
+          final month = selected['month'].toString().padLeft(2, '0');
           onPick('$year-$month');
         } else if (isDateOnly) {
+          final year = selected['year'];
+          final month = selected['month'].toString().padLeft(2, '0');
+          final day = selected['day'].toString().padLeft(2, '0');
           onPick('$year-$month-$day');
         } else {
+          final year = selected['year'];
+          final month = selected['month'].toString().padLeft(2, '0');
+          final day = selected['day'].toString().padLeft(2, '0');
           onPick('$year-$month-$day $hour:$minute');
         }
         Navigator.of(context).pop();

+ 44 - 40
lib/features/overtime/widgets/overtime_apply_detail_view_dialog.dart

@@ -34,9 +34,7 @@ class OvertimeApplyDetailViewDialog extends StatelessWidget {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final d = detail;
 
-    final empLabel = d.salName.isNotEmpty
-        ? '${d.salNo}/${d.salName}'
-        : d.salNo;
+    final empLabel = d.salName.isNotEmpty ? '${d.salNo}/${d.salName}' : d.salNo;
 
     final deptLabel = d.depName.isNotEmpty
         ? '${d.dep}/${d.depName}'
@@ -91,40 +89,47 @@ class OvertimeApplyDetailViewDialog extends StatelessWidget {
                       _row(l10n.get('dep'), deptLabel, colors),
                       _row(l10n.get('jbType'), _jbTypeLabel(d.jbType), colors),
                       if (d.jbDate != null)
-                        _row(l10n.get('applyDate'),
-                            _formatDate(d.jbDate!), colors),
+                        _row(
+                          l10n.get('jbDate'),
+                          _formatDate(d.jbDate!),
+                          colors,
+                        ),
                       if (d.startTime != null)
-                        _rowWithTag(
-                          context,
+                        _row(
                           l10n.get('startTime'),
-                          _formatDateTime(d.startTime!),
-                          _dayOfWeekFromDate(d.startTime!),
+                          _fmtTimeFromDate(d.startTime!),
                           colors,
                         ),
                       if (d.endTime != null)
-                        _rowWithTag(
-                          context,
+                        _row(
                           l10n.get('endTime'),
-                          _formatDateTime(d.endTime!),
-                          _dayOfWeekFromDate(d.endTime!),
+                          _fmtTimeFromDate(d.endTime!),
                           colors,
                         ),
-                      _row(l10n.get('overtimeHours'),
-                          '${d.jbHours.toStringAsFixed(1)}${l10n.get('hours')}', colors,
-                          valueColor: colors.timePrimary),
-                      _row(l10n.get('overtimeDays'),
-                          d.jbDays.toStringAsFixed(1), colors),
+                      _row(
+                        l10n.get('overtimeHours'),
+                        '${d.jbHours.toStringAsFixed(1)}${l10n.get('hours')}',
+                        colors,
+                        valueColor: colors.timePrimary,
+                      ),
+                      // TODO: 加班天数暂时隐藏
+                      // _row(l10n.get('overtimeDays'), d.jbDays.toStringAsFixed(1), colors),
                       if (d.attPeriod.isNotEmpty)
                         _row(l10n.get('attPeriod'), d.attPeriod, colors),
                       if (d.reason.isNotEmpty)
-                        _row(l10n.get('overtimeDetailReason'), d.reason, colors),
-                      if (d.compensationType.isNotEmpty)
-                        _row(l10n.get('compensationType'),
-                            _compensationTypeLabel(d.compensationType), colors),
-                      if (d.compensationType != 'NO_COMPENSATION' &&
-                          d.compensationCount > 0)
-                        _row(l10n.get('compensationCount'),
-                            d.compensationCount.toStringAsFixed(1), colors),
+                        _row(
+                          l10n.get('overtimeDetailReason'),
+                          d.reason,
+                          colors,
+                        ),
+                      // TODO: 补偿类型、补偿次数暂时隐藏
+                      // if (d.compensationType.isNotEmpty)
+                      //   _row(l10n.get('compensationType'),
+                      //       _compensationTypeLabel(d.compensationType), colors),
+                      // if (d.compensationType != 'NO_COMPENSATION' &&
+                      //     d.compensationCount > 0)
+                      //   _row(l10n.get('compensationCount'),
+                      //       d.compensationCount.toStringAsFixed(1), colors),
                       if (d.adr.isNotEmpty)
                         _row(l10n.get('adr'), d.adr, colors),
                       if (d.rem.isNotEmpty)
@@ -140,8 +145,12 @@ class OvertimeApplyDetailViewDialog extends StatelessWidget {
     );
   }
 
-  Widget _row(String label, String value, AppColorsExtension colors,
-      {Color? valueColor}) {
+  Widget _row(
+    String label,
+    String value,
+    AppColorsExtension colors, {
+    Color? valueColor,
+  }) {
     return Padding(
       padding: const EdgeInsets.only(bottom: 12),
       child: Row(
@@ -151,10 +160,7 @@ class OvertimeApplyDetailViewDialog extends StatelessWidget {
             width: 100,
             child: Text(
               label,
-              style: TextStyle(
-                fontSize: 14,
-                color: colors.textSecondary,
-              ),
+              style: TextStyle(fontSize: 14, color: colors.textSecondary),
             ),
           ),
           Expanded(
@@ -188,10 +194,7 @@ class OvertimeApplyDetailViewDialog extends StatelessWidget {
             width: 100,
             child: Text(
               label,
-              style: TextStyle(
-                fontSize: 14,
-                color: colors.textSecondary,
-              ),
+              style: TextStyle(fontSize: 14, color: colors.textSecondary),
             ),
           ),
           Expanded(
@@ -201,10 +204,7 @@ class OvertimeApplyDetailViewDialog extends StatelessWidget {
               children: [
                 Text(
                   value,
-                  style: TextStyle(
-                    fontSize: 14,
-                    color: colors.textPrimary,
-                  ),
+                  style: TextStyle(fontSize: 14, color: colors.textPrimary),
                 ),
                 Container(
                   padding: const EdgeInsets.symmetric(horizontal: 6),
@@ -235,6 +235,10 @@ class OvertimeApplyDetailViewDialog extends StatelessWidget {
     return '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')} ${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}';
   }
 
+  String _fmtTimeFromDate(DateTime d) {
+    return '${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}';
+  }
+
   String _jbTypeLabel(String type) {
     switch (type) {
       case 'WORKING_DAY':

+ 133 - 30
lib/features/vehicle/vehicle_apply_create_page.dart

@@ -61,6 +61,10 @@ class _VehicleApplyCreatePageState
   // ── 同行信息 ──
   final List<EmployeeItem> _passengers = [];
 
+  // ── 草稿 ──
+  late Future<bool> _draftFuture;
+  bool _draftHandled = false;
+
   // ── 参考数据 ──
   List<DepartmentItem> _departments = [];
   List<EmployeeItem> _employees = [];
@@ -97,10 +101,88 @@ class _VehicleApplyCreatePageState
     _departments = [];
     _refDataLoading = true;
     _refDataFuture = null;
+    _draftFuture = DraftStorage.has(_draftKey);
     _loadRefData();
     WidgetsBinding.instance.addPostFrameCallback((_) => _checkDataReady());
   }
 
+  void _showDraftDialog() {
+    final l10n = AppLocalizations.of(context);
+    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    FocusManager.instance.primaryFocus?.unfocus();
+    showDialog(
+      context: context,
+      barrierDismissible: false,
+      builder: (ctx) => TDAlertDialog(
+        title: l10n.get('draftFound'),
+        content: l10n.get('draftRestorePrompt'),
+        leftBtn: TDDialogButtonOptions(
+          title: l10n.get('discard'),
+          titleColor: colors.textSecondary,
+          action: () {
+            Navigator.pop(ctx);
+            DraftStorage.delete(_draftKey);
+          },
+        ),
+        rightBtn: TDDialogButtonOptions(
+          title: l10n.get('restore'),
+          titleColor: colors.primary,
+          action: () async {
+            Navigator.pop(ctx);
+            final data = await DraftStorage.load(_draftKey);
+            if (data != null && mounted) {
+              setState(() => _applyDraftData(data));
+            }
+          },
+        ),
+      ),
+    );
+  }
+
+  void _applyDraftData(Map<String, dynamic> data) {
+    _reasonController.text = data['reason'] as String? ?? '';
+    _originAdr = data['originAdr'] as String? ?? '';
+    _originLat = (data['originLat'] as num?)?.toDouble();
+    _originLng = (data['originLng'] as num?)?.toDouble();
+    _destAdr = data['destAdr'] as String? ?? '';
+    _destLat = (data['destLat'] as num?)?.toDouble();
+    _destLng = (data['destLng'] as num?)?.toDouble();
+    final startStr = data['startTime'] as String?;
+    _startTime = startStr != null ? DateTime.tryParse(startStr) : null;
+    final endStr = data['endTime'] as String?;
+    _endTime = endStr != null ? DateTime.tryParse(endStr) : null;
+    _licensePlate = data['licensePlate'] as String? ?? '';
+    _vehicleType = data['vehicleType'] as String? ?? '';
+    _brand = data['brand'] as String? ?? '';
+    _odometerBegin = data['odometerBegin'] as int? ?? 0;
+    _seats = data['seats'] as int? ?? 0;
+    _driverName = data['driverName'] as String? ?? '';
+    _selectedDeptId = data['selectedDeptId'] as String? ?? '';
+    _selectedDeptName = data['selectedDeptName'] as String? ?? '';
+    _selectedApplicantId = data['selectedApplicantId'] as String? ?? '';
+    _selectedApplicantName = data['selectedApplicantName'] as String? ?? '';
+    // 恢复同行人
+    final pCount = data['passengerCount'] as int? ?? 0;
+    final pName = data['passengerName'] as String? ?? '';
+    if (pCount > 0 && pName.isNotEmpty) {
+      final parts = pName.split(';');
+      for (final part in parts) {
+        if (part.isEmpty) continue;
+        final idx = part.indexOf('/');
+        if (idx > 0) {
+          _passengers.add(
+            EmployeeItem(
+              salNo: part.substring(0, idx),
+              name: part.substring(idx + 1),
+            ),
+          );
+        } else {
+          _passengers.add(EmployeeItem(salNo: part, name: part));
+        }
+      }
+    }
+  }
+
   void _checkDataReady() {
     if (!_refDataLoading && mounted) {
       setState(() => _firstBuild = false);
@@ -217,39 +299,53 @@ class _VehicleApplyCreatePageState
       () => ref.read(pageBackProvider.notifier).state = () => _doPop(),
     );
 
-    return PopScope(
-      canPop: false,
-      onPopInvokedWithResult: (didPop, _) {
-        if (didPop) return;
-        _doPop();
-      },
-      child: Column(
-        children: [
-          Expanded(
-            child: GestureDetector(
-              onTap: () => FocusScope.of(context).unfocus(),
-              child: SingleChildScrollView(
-                controller: _scrollCtrl,
-                padding: const EdgeInsets.all(16),
-                child: Column(
-                  children: [
-                    _buildBasicInfo(l10n),
-                    const SizedBox(height: 16),
-                    _buildVehicleInfo(l10n),
-                    const SizedBox(height: 16),
-                    _buildTripInfo(l10n),
-                    const SizedBox(height: 16),
-                    _buildPassengerInfo(l10n),
-                    const SizedBox(height: 24),
-                    _buildPageFooter(),
-                  ],
+    return FutureBuilder<bool>(
+      future: _draftFuture,
+      builder: (ctx, snapshot) {
+        final hasDraft = snapshot.hasData && snapshot.data == true;
+
+        if (hasDraft && !_draftHandled) {
+          _draftHandled = true;
+          WidgetsBinding.instance.addPostFrameCallback((_) {
+            if (mounted) _showDraftDialog();
+          });
+        }
+
+        return PopScope(
+          canPop: false,
+          onPopInvokedWithResult: (didPop, _) {
+            if (didPop) return;
+            _doPop();
+          },
+          child: Column(
+            children: [
+              Expanded(
+                child: GestureDetector(
+                  onTap: () => FocusScope.of(context).unfocus(),
+                  child: SingleChildScrollView(
+                    controller: _scrollCtrl,
+                    padding: const EdgeInsets.all(16),
+                    child: Column(
+                      children: [
+                        _buildBasicInfo(l10n),
+                        const SizedBox(height: 16),
+                        _buildVehicleInfo(l10n),
+                        const SizedBox(height: 16),
+                        _buildTripInfo(l10n),
+                        const SizedBox(height: 16),
+                        _buildPassengerInfo(l10n),
+                        const SizedBox(height: 24),
+                        _buildPageFooter(),
+                      ],
+                    ),
+                  ),
                 ),
               ),
-            ),
+              _buildBottomBar(l10n),
+            ],
           ),
-          _buildBottomBar(l10n),
-        ],
-      ),
+        );
+      },
     );
   }
 
@@ -723,10 +819,15 @@ class _VehicleApplyCreatePageState
 
   // ═══ 草稿 ═══
   Future<void> _saveDraftToStorage() async {
+    if (!_hasUnsaved()) return;
     await DraftStorage.save(_draftKey, {
       'reason': _reasonController.text,
       'originAdr': _originAdr,
+      'originLat': _originLat,
+      'originLng': _originLng,
       'destAdr': _destAdr,
+      'destLat': _destLat,
+      'destLng': _destLng,
       'startTime': _startTime?.toIso8601String(),
       'endTime': _endTime?.toIso8601String(),
       'licensePlate': _licensePlate,
@@ -739,6 +840,8 @@ class _VehicleApplyCreatePageState
       'passengerName': _passengers.map((p) => '${p.salNo}/${p.name}').join(';'),
       'selectedDeptId': _selectedDeptId,
       'selectedDeptName': _selectedDeptName,
+      'selectedApplicantId': _selectedApplicantId,
+      'selectedApplicantName': _selectedApplicantName,
     });
   }