Browse Source

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

chengc 2 hours ago
parent
commit
115f5fc1ab

+ 5 - 1
assets/i18n/en.json

@@ -510,6 +510,7 @@
     "reportOvertimeDetail": "Overtime Detail Report",
     "overtimeInfo": "Overtime Info",
     "overtimeType": "Overtime Type",
+    "overtimeDetailReason": "Overtime Reason",
     "overtimeReason": "Overtime Reason",
     "reason": "Reason",
     "endTimeMustLater": "End time must be later than start time",
@@ -536,7 +537,7 @@
     "holiday": "Holiday",
     "overtimeRequest": "Overtime Request",
     "editOvertime": "Edit Overtime",
-    "overtimeApplyNo": "Overtime Application No.",
+    "overtimeApplyNo": "Application No.",
     "employee": "Employee",
     "dep": "Department",
     "jbType": "Overtime Type",
@@ -563,10 +564,13 @@
     "enterRemark": "Please enter remark",
     "confirmEdit": "Confirm Edit",
     "overtimeHoursPositive": "Overtime hours must be greater than 0",
+    "addOvertimeDetail": "Add Overtime Detail",
+    "editOvertimeDetail": "Edit Overtime Detail",
     "addDetail": "Add Detail",
     "overtimeDetails": "Overtime Details",
     "noDetailHint": "Please add overtime detail",
     "selectDept": "Please select department first",
+    "selectApplicant": "Please select applicant first",
     "addAtLeastOneDetail": "Please add at least one overtime detail",
     "autoFromDate": "Auto from overtime date",
     "detailConfirmHint": "Confirm submit this overtime detail?",

+ 5 - 1
assets/i18n/zh_CN.json

@@ -510,6 +510,7 @@
     "reportOvertimeDetail": "加班明细报表",
     "overtimeInfo": "加班信息",
     "overtimeType": "加班类型",
+    "overtimeDetailReason": "加班事由",
     "overtimeReason": "加班原因",
     "reason": "事由",
     "endTimeMustLater": "结束时间必须晚于开始时间",
@@ -536,7 +537,7 @@
     "holiday": "节假日",
     "overtimeRequest": "加班申请",
     "editOvertime": "编辑加班申请",
-    "overtimeApplyNo": "加班申请单号",
+    "overtimeApplyNo": "申请单号",
     "employee": "员工",
     "dep": "部门",
     "jbType": "加班类型",
@@ -563,10 +564,13 @@
     "enterRemark": "请输入备注",
     "confirmEdit": "确认修改",
     "overtimeHoursPositive": "加班时长必须大于0",
+    "addOvertimeDetail": "添加加班明细",
+    "editOvertimeDetail": "编辑加班明细",
     "addDetail": "添加明细",
     "overtimeDetails": "加班明细",
     "noDetailHint": "请添加加班明细",
     "selectDept": "请先选择申请部门",
+    "selectApplicant": "请先选择申请人",
     "addAtLeastOneDetail": "请至少添加一条加班明细",
     "autoFromDate": "自动从加班日期获取",
     "detailConfirmHint": "确认提交此加班明细?",

+ 5 - 1
assets/i18n/zh_TW.json

@@ -510,6 +510,7 @@
     "reportOvertimeDetail": "加班明細報表",
     "overtimeInfo": "加班信息",
     "overtimeType": "加班類型",
+    "overtimeDetailReason": "加班事由",
     "overtimeReason": "加班原因",
     "reason": "事由",
     "endTimeMustLater": "結束時間必須晚於開始時間",
@@ -536,7 +537,7 @@
     "holiday": "節假日",
     "overtimeRequest": "加班申請",
     "editOvertime": "編輯加班申請",
-    "overtimeApplyNo": "加班申請單號",
+    "overtimeApplyNo": "申請單號",
     "employee": "員工",
     "dep": "部門",
     "jbType": "加班類型",
@@ -563,10 +564,13 @@
     "enterRemark": "請輸入備註",
     "confirmEdit": "確認修改",
     "overtimeHoursPositive": "加班時長必須大於0",
+    "addOvertimeDetail": "添加加班明細",
+    "editOvertimeDetail": "編輯加班明細",
     "addDetail": "添加明細",
     "overtimeDetails": "加班明細",
     "noDetailHint": "請添加加班明細",
     "selectDept": "請先選擇申請部門",
+    "selectApplicant": "請先選擇申請人",
     "addAtLeastOneDetail": "請至少添加一條加班明細",
     "autoFromDate": "自動從加班日期獲取",
     "detailConfirmHint": "確認提交此加班明細?",

+ 6 - 11
lib/core/router/app_router.dart

@@ -172,6 +172,7 @@ GoRouter createAppRouter() => GoRouter(
     GoRoute(
       path: '/overtime-apply/create',
       pageBuilder: (context, state) => MaterialPage(
+        key: ValueKey(state.uri.toString()),
         child: AppScaffold(
           navConfig: NavBarConfig(showBack: true),
           body: const OvertimeApplyCreatePage(),
@@ -226,6 +227,7 @@ GoRouter createAppRouter() => GoRouter(
     GoRoute(
       path: '/vehicle-apply/create',
       pageBuilder: (context, state) => MaterialPage(
+        key: ValueKey(state.uri.toString()),
         child: AppScaffold(
           navConfig: NavBarConfig(showBack: true),
           body: const VehicleApplyCreatePage(),
@@ -247,15 +249,6 @@ GoRouter createAppRouter() => GoRouter(
     GoRoute(
       path: '/vehicle-apply/edit/:billNo',
       pageBuilder: (context, state) => MaterialPage(
-        child: AppScaffold(
-          navConfig: NavBarConfig(showBack: true),
-          body: VehicleApplyEditPage(billNo: state.pathParameters['billNo']!),
-        ),
-      ),
-    ),
-    GoRoute(
-      path: '/vehicle-apply/edit/:billNo',
-      pageBuilder: (context, state) => MaterialPage(
         key: ValueKey(state.uri.toString()),
         child: AppScaffold(
           navConfig: NavBarConfig(
@@ -286,7 +279,8 @@ GoRouter createAppRouter() => GoRouter(
     ),
     GoRoute(
       path: '/outing-log/create',
-      pageBuilder: (context, _) => MaterialPage(
+      pageBuilder: (context, state) => MaterialPage(
+        key: ValueKey(state.uri.toString()),
         child: AppScaffold(
           navConfig: NavBarConfig(showBack: true),
           body: const OutingLogCreatePage(),
@@ -336,7 +330,8 @@ GoRouter createAppRouter() => GoRouter(
     ),
     GoRoute(
       path: '/announcement/create',
-      pageBuilder: (context, _) => MaterialPage(
+      pageBuilder: (context, state) => MaterialPage(
+        key: ValueKey(state.uri.toString()),
         child: AppScaffold(
           navConfig: NavBarConfig(showBack: true),
           body: const AnnouncementCreatePage(),

+ 16 - 0
lib/core/theme/app_colors.dart

@@ -95,6 +95,16 @@ class AppColors {
   static const Color amountPrimary = amountPositive;
 
   // ═══════════════════════════════════════════
+  //  时长色(加班时数/天数,区别于金额色和品牌色)
+  // ═══════════════════════════════════════════
+  static const Color timePrimary = Color(0xFF0D9488); // 青蓝 Teal-600
+
+  // ═══════════════════════════════════════════
+  //  车牌色(车辆识别,区别于品牌色和时长色)
+  // ═══════════════════════════════════════════
+  static const Color platePrimary = Color(0xFF4F46E5); // 靛蓝 Indigo-600
+
+  // ═══════════════════════════════════════════
   //  信息色(已还车/GPS/时间线/审批信息)
   // ═══════════════════════════════════════════
   static const Color infoText = Color(0xFF3B82C4);
@@ -207,6 +217,12 @@ class AppDarkColors {
   static const Color amountNeutral = Color(0xFFA6A6A6);
   static const Color amountPrimary = amountPositive;
 
+  // ── 时长色(加班时数/天数,区别于金额色和品牌色)──
+  static const Color timePrimary = Color(0xFF2DD4BF); // 青蓝 Teal-400 深色背景
+
+  // ── 车牌色(车辆识别,区别于品牌色和时长色)──
+  static const Color platePrimary = Color(0xFF818CF8); // 靛蓝 Indigo-400 深色背景
+
   // ── 信息色 ──
   static const Color infoText = Color(0xFF6AA8E6);
   static const Color infoBg = Color(0xFF0E1F33);

+ 10 - 0
lib/core/theme/app_colors_extension.dart

@@ -24,6 +24,8 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
   final Color amountNegative;
   final Color amountNeutral;
   final Color amountPrimary;
+  final Color timePrimary;
+  final Color platePrimary;
   final Color infoText;
   final Color infoBg;
   final Color infoLightBg;
@@ -80,6 +82,8 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
     required this.amountNegative,
     required this.amountNeutral,
     required this.amountPrimary,
+    required this.timePrimary,
+    required this.platePrimary,
     required this.infoText,
     required this.infoBg,
     required this.infoLightBg,
@@ -137,6 +141,8 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
     amountNegative: AppColors.amountNegative,
     amountNeutral: AppColors.amountNeutral,
     amountPrimary: AppColors.amountPrimary,
+    timePrimary: AppColors.timePrimary,
+    platePrimary: AppColors.platePrimary,
     infoText: AppColors.infoText,
     infoBg: AppColors.infoBg,
     infoLightBg: AppColors.infoLightBg,
@@ -194,6 +200,8 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
     amountNegative: AppDarkColors.amountNegative,
     amountNeutral: AppDarkColors.amountNeutral,
     amountPrimary: AppDarkColors.amountPrimary,
+    timePrimary: AppDarkColors.timePrimary,
+    platePrimary: AppDarkColors.platePrimary,
     infoText: AppDarkColors.infoText,
     infoBg: AppDarkColors.infoBg,
     infoLightBg: AppDarkColors.infoLightBg,
@@ -257,6 +265,8 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
       amountNegative: Color.lerp(amountNegative, other.amountNegative, t)!,
       amountNeutral: Color.lerp(amountNeutral, other.amountNeutral, t)!,
       amountPrimary: Color.lerp(amountPrimary, other.amountPrimary, t)!,
+      timePrimary: Color.lerp(timePrimary, other.timePrimary, t)!,
+      platePrimary: Color.lerp(platePrimary, other.platePrimary, t)!,
       infoText: Color.lerp(infoText, other.infoText, t)!,
       infoBg: Color.lerp(infoBg, other.infoBg, t)!,
       infoLightBg: Color.lerp(infoLightBg, other.infoLightBg, t)!,

+ 152 - 52
lib/features/overtime/overtime_apply_create_page.dart

@@ -214,6 +214,37 @@ class _OvertimeApplyCreatePageState
       () => ref.read(pageBackProvider.notifier).state = () => _doPop(),
     );
 
+    final pageContent = 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),
+                    _buildDetailsSection(l10n),
+                    const SizedBox(height: 24),
+                    _buildPageFooter(),
+                  ],
+                ),
+              ),
+            ),
+          ),
+          _buildBottomBar(l10n),
+        ],
+      ),
+    );
+
     return FutureBuilder<bool>(
       future: _draftFuture,
       builder: (ctx, snapshot) {
@@ -226,39 +257,7 @@ class _OvertimeApplyCreatePageState
           });
         }
 
-        Future.microtask(
-          () => 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),
-                        _buildDetailsSection(l10n),
-                        const SizedBox(height: 24),
-                        _buildPageFooter(),
-                      ],
-                    ),
-                  ),
-                ),
-              ),
-              _buildBottomBar(l10n),
-            ],
-          ),
-        );
+        return pageContent;
       },
     );
   }
@@ -273,6 +272,8 @@ class _OvertimeApplyCreatePageState
       _remarkController.text = data['remark'] as String? ?? '';
       _selectedDeptId = data['deptId'] as String? ?? '';
       _selectedDeptName = data['deptName'] as String? ?? '';
+      _selectedApplicantId = data['applicantId'] as String? ?? '';
+      _selectedApplicantName = data['applicantName'] as String? ?? '';
       _details.clear();
       final detailList = data['details'] as List<dynamic>?;
       if (detailList != null) {
@@ -286,6 +287,7 @@ class _OvertimeApplyCreatePageState
               salNo: m['salNo'] as String? ?? '',
               salName: m['salName'] as String? ?? '',
               dep: m['dep'] as String? ?? '',
+              depName: m['depName'] as String? ?? '',
               jbType: m['jbType'] as String? ?? 'WORKING_DAY',
               jbDate: m['jbDate'] as String? ?? '',
               startTime: m['startTime'] as String? ?? '',
@@ -320,6 +322,7 @@ class _OvertimeApplyCreatePageState
             'salNo': d.salNo,
             'salName': d.salName,
             'dep': d.dep,
+            'depName': d.depName,
             'jbType': d.jbType,
             'jbDate': d.jbDate,
             'startTime': d.startTime,
@@ -340,6 +343,8 @@ class _OvertimeApplyCreatePageState
       'remark': _remarkController.text,
       'deptId': _selectedDeptId,
       'deptName': _selectedDeptName,
+      'applicantId': _selectedApplicantId,
+      'applicantName': _selectedApplicantName,
       'details': detailList,
     });
   }
@@ -354,7 +359,7 @@ class _OvertimeApplyCreatePageState
       barrierDismissible: false,
       builder: (ctx) => TDAlertDialog(
         title: l10n.get('draftFound'),
-        content: l10n.get('draftFoundHint'),
+        content: l10n.get('draftRestorePrompt'),
         buttonStyle: TDDialogButtonStyle.text,
         leftBtn: TDDialogButtonOptions(
           title: l10n.get('discard'),
@@ -405,6 +410,7 @@ class _OvertimeApplyCreatePageState
         const SizedBox(height: 16),
         FormFieldRow(
           label: l10n.get('applicant'),
+          required: true,
           value: _selectedApplicantId.isNotEmpty
               ? '$_selectedApplicantId/$_selectedApplicantName'
               : '',
@@ -499,32 +505,60 @@ class _OvertimeApplyCreatePageState
                                 ),
                               ),
                               Text(
-                                '${d.jbHours.toStringAsFixed(1)}h',
+                                '${d.jbHours.toStringAsFixed(1)}${l10n.get('hours')}',
                                 style: TextStyle(
                                   fontSize: AppFontSizes.body,
                                   fontWeight: FontWeight.w600,
-                                  color: colors.amountPrimary,
+                                  color: colors.timePrimary,
                                 ),
                               ),
                             ],
                           ),
                           if (d.jbType.isNotEmpty) ...[
-                            const SizedBox(height: 4),
+                            const SizedBox(height: 2),
                             _detailLabel(
                               '${l10n.get('jbType')}: ${_jbTypeLabel(d.jbType, l10n)}',
                               colors,
                             ),
                             _detailLabel(
-                              '${l10n.get('date')}: ${d.jbDate}',
+                              '${l10n.get('applyDate')}: ${d.jbDate}',
                               colors,
                             ),
                           ],
-                          if (d.startTime.isNotEmpty &&
-                              d.endTime.isNotEmpty) ...[
+                          if (d.startTime.isNotEmpty) ...[
                             const SizedBox(height: 2),
-                            _detailLabel(
-                              '${d.startTime} ~ ${d.endTime}',
-                              colors,
+                            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)!,
+                                  ),
+                              ],
+                            ),
+                          ],
+                          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) ...[
@@ -534,26 +568,46 @@ class _OvertimeApplyCreatePageState
                               colors,
                             ),
                           ],
-                          if (d.attPeriod.isNotEmpty)
+                          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)}${d.compensationCount > 0 ? ' (${d.compensationCount.toStringAsFixed(1)})' : ''}',
+                              '${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)
+                          if (d.adr.isNotEmpty) ...[
+                            const SizedBox(height: 2),
                             _detailLabel(
                               '${l10n.get('adr')}: ${d.adr}',
                               colors,
                             ),
+                          ],
                           if (d.reason.isNotEmpty) ...[
                             const SizedBox(height: 2),
-                            _detailLabel(d.reason, colors),
+                            _detailLabel(
+                              '${l10n.get('overtimeDetailReason')}: ${d.reason}',
+                              colors,
+                            ),
+                          ],
+                          if (d.rem.isNotEmpty) ...[
+                            const SizedBox(height: 2),
+                            _detailLabel(
+                              '${l10n.get('remark')}: ${d.rem}',
+                              colors,
+                            ),
                           ],
                         ],
                       ),
@@ -587,11 +641,11 @@ class _OvertimeApplyCreatePageState
                 ),
               ),
               Text(
-                '${_totalHours().toStringAsFixed(1)}h',
+                '${_totalHours().toStringAsFixed(1)}${l10n.get('hours')}',
                 style: TextStyle(
                   fontSize: AppFontSizes.subtitle,
                   fontWeight: FontWeight.w700,
-                  color: colors.amountPrimary,
+                  color: colors.timePrimary,
                 ),
               ),
             ],
@@ -620,6 +674,29 @@ class _OvertimeApplyCreatePageState
     }
   }
 
+  String? _dayOfWeekLabel(String dateTimeStr, AppLocalizations l10n) {
+    final dt = DateTime.tryParse(dateTimeStr);
+    if (dt == null) return null;
+    switch (dt.weekday) {
+      case 1:
+        return l10n.get('monday');
+      case 2:
+        return l10n.get('tuesday');
+      case 3:
+        return l10n.get('wednesday');
+      case 4:
+        return l10n.get('thursday');
+      case 5:
+        return l10n.get('friday');
+      case 6:
+        return l10n.get('saturday');
+      case 7:
+        return l10n.get('sunday');
+      default:
+        return null;
+    }
+  }
+
   String _compensationTypeLabel(String type, AppLocalizations l10n) {
     switch (type) {
       case 'OVERTIME_PAY':
@@ -650,6 +727,23 @@ class _OvertimeApplyCreatePageState
     );
   }
 
+  Widget _dayOfWeekTag(String label) {
+    final tdTheme = TDTheme.of(context);
+    return Container(
+      padding: const EdgeInsets.symmetric(horizontal: 6),
+      decoration: BoxDecoration(
+        color: tdTheme.brandColor1,
+        borderRadius: BorderRadius.circular(4),
+      ),
+      child: TDText(
+        label,
+        font: tdTheme.fontBodySmall,
+        fontWeight: FontWeight.w500,
+        textColor: tdTheme.brandColor7,
+      ),
+    );
+  }
+
   Future<void> _showDetailDialog({int? editIndex}) async {
     if (_addingDetail) return;
     _addingDetail = true;
@@ -664,6 +758,7 @@ class _OvertimeApplyCreatePageState
           salNo: d.salNo,
           salName: d.salName,
           dep: d.dep,
+          depName: d.depName,
           jbType: d.jbType,
           jbDate: d.jbDate,
           startTime: d.startTime,
@@ -695,6 +790,7 @@ class _OvertimeApplyCreatePageState
             salNo: result.salNo,
             salName: result.salName,
             dep: result.dep,
+            depName: result.depName,
             jbType: result.jbType,
             jbDate: result.jbDate,
             startTime: result.startTime,
@@ -836,6 +932,7 @@ class _OvertimeApplyCreatePageState
     }
     if (_details.isEmpty) e.add(l10n.get('addAtLeastOneDetail'));
     if (_selectedDeptId.isEmpty) e.add(l10n.get('selectDept'));
+    if (_selectedApplicantId.isEmpty) e.add(l10n.get('selectApplicant'));
     return e;
   }
 
@@ -872,8 +969,7 @@ class _OvertimeApplyCreatePageState
   bool _hasUnsaved() =>
       _reasonController.text.isNotEmpty ||
       _details.isNotEmpty ||
-      _remarkController.text.isNotEmpty ||
-      _selectedDeptId.isNotEmpty;
+      _remarkController.text.isNotEmpty;
 
   void _clearLocalState() {
     _reasonController.clear();
@@ -882,6 +978,8 @@ class _OvertimeApplyCreatePageState
     _detailIdCounter = 1;
     _selectedDeptId = '';
     _selectedDeptName = '';
+    _selectedApplicantId = '';
+    _selectedApplicantName = '';
   }
 
   void _unfocus() => FocusScope.of(context).unfocus();
@@ -1001,6 +1099,7 @@ class _DetailItem {
   final String salNo;
   final String salName;
   final String dep;
+  final String depName;
   final String jbType;
   final String jbDate;
   final String startTime;
@@ -1021,6 +1120,7 @@ class _DetailItem {
     this.salNo = '',
     this.salName = '',
     this.dep = '',
+    this.depName = '',
     this.jbType = 'WORKING_DAY',
     this.jbDate = '',
     this.startTime = '',
@@ -1029,7 +1129,7 @@ class _DetailItem {
     this.jbDays = 0.0,
     this.attPeriod = '',
     this.reason = '',
-    this.compensationType = 'OVERTIME_PAY',
+    this.compensationType = '',
     this.compensationCount = 0.0,
     this.adr = '',
     this.rem = '',

+ 146 - 139
lib/features/overtime/overtime_apply_detail_page.dart

@@ -242,16 +242,18 @@ class _OvertimeApplyDetailPageState
         const SizedBox(height: 16),
         FormFieldRow(
           label: l10n.get('applicant'),
-          value: app.salName.isNotEmpty ? app.salName : app.salNo,
+          value: app.salNo.isNotEmpty
+              ? '${app.salNo}${app.salName.isNotEmpty ? '/${app.salName}' : ''}'
+              : '-',
           readOnly: true,
           showArrow: false,
         ),
         const SizedBox(height: 16),
         FormFieldRow(
           label: l10n.get('dep'),
-          value: app.depName.isNotEmpty
-              ? app.depName
-              : (app.dep.isNotEmpty ? app.dep : '-'),
+          value: app.dep.isNotEmpty
+              ? '${app.dep}${app.depName.isNotEmpty ? '/${app.depName}' : ''}'
+              : '-',
           readOnly: true,
           showArrow: false,
         ),
@@ -275,22 +277,6 @@ class _OvertimeApplyDetailPageState
         ),
         const SizedBox(height: 16),
         FormFieldRow(
-          label: l10n.get('usr'),
-          value: app.usr.isNotEmpty ? app.usr : '-',
-          readOnly: true,
-          showArrow: false,
-        ),
-        const SizedBox(height: 16),
-        FormFieldRow(
-          label: l10n.get('recordDd'),
-          value: app.recordDd != null
-              ? du.DateUtils.formatDate(app.recordDd!)
-              : '-',
-          readOnly: true,
-          showArrow: false,
-        ),
-        const SizedBox(height: 16),
-        FormFieldRow(
           label: l10n.get('chkMan'),
           value: app.chkMan.isNotEmpty ? app.chkMan : '-',
           readOnly: true,
@@ -346,11 +332,11 @@ class _OvertimeApplyDetailPageState
                 ),
               ),
               Text(
-                '${_totalHours(app).toStringAsFixed(1)}h',
+                '${_totalHours(app).toStringAsFixed(1)}${l10n.get('hours')}',
                 style: TextStyle(
                   fontSize: AppFontSizes.subtitle,
                   fontWeight: FontWeight.w700,
-                  color: colors.amountPrimary,
+                  color: colors.timePrimary,
                 ),
               ),
             ],
@@ -368,10 +354,16 @@ class _OvertimeApplyDetailPageState
     AppLocalizations l10n,
     AppColorsExtension colors,
   ) {
+    // 开始/结束时间格式化
+    String formatDateTime(DateTime dt) {
+      return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
+          '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
+    }
+
     return GestureDetector(
       onTap: () => OvertimeApplyDetailViewDialog.show(context, d),
       child: Container(
-        margin: const EdgeInsets.symmetric(vertical: 8),
+        margin: const EdgeInsets.symmetric(vertical: 6),
         padding: const EdgeInsets.all(12),
         decoration: BoxDecoration(
           color: colors.bgPage,
@@ -380,138 +372,180 @@ class _OvertimeApplyDetailPageState
         child: Column(
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
+            // 第一行:员工 + 时长
             Row(
-              mainAxisAlignment: MainAxisAlignment.spaceBetween,
               children: [
                 Expanded(
                   child: Text(
                     '${d.salNo}${d.salName.isNotEmpty ? '/${d.salName}' : ''}',
+                    maxLines: 1,
+                    overflow: TextOverflow.ellipsis,
                     style: TextStyle(
-                      fontSize: AppFontSizes.subtitle,
+                      fontSize: AppFontSizes.body,
+                      fontWeight: FontWeight.w500,
                       color: colors.textPrimary,
                     ),
                   ),
                 ),
                 Text(
-                  '${d.jbHours.toStringAsFixed(1)}h',
+                  '${d.jbHours.toStringAsFixed(1)}${l10n.get('hours')}',
                   style: TextStyle(
-                    fontSize: AppFontSizes.caption,
+                    fontSize: AppFontSizes.body,
                     fontWeight: FontWeight.w600,
-                    color: colors.amountPrimary,
+                    color: colors.timePrimary,
                   ),
                 ),
               ],
             ),
-            if (d.dep.isNotEmpty) ...[
-              const SizedBox(height: 4),
-              Text(
-                '${l10n.get('dep')}: ${d.dep}',
-                style: TextStyle(
-                  fontSize: AppFontSizes.caption,
-                  color: colors.textSecondary,
-                ),
-              ),
-            ],
-            const SizedBox(height: 4),
-            Text(
-              '${l10n.get('jbType')}: ${_jbTypeLabel(d.jbType, l10n)}',
-              style: TextStyle(
-                fontSize: AppFontSizes.caption,
-                color: colors.textSecondary,
+            if (d.jbType.isNotEmpty) ...[
+              const SizedBox(height: 2),
+              _detailLabel(
+                '${l10n.get('jbType')}: ${_jbTypeLabel(d.jbType, l10n)}',
+                colors,
               ),
-            ),
-            if (d.jbDate != null) ...[
-              const SizedBox(height: 4),
-              Text(
-                '${l10n.get('jbDate')}: ${du.DateUtils.formatDate(d.jbDate!)}',
-                style: TextStyle(
-                  fontSize: AppFontSizes.caption,
-                  color: colors.textSecondary,
+              if (d.jbDate != null)
+                _detailLabel(
+                  '${l10n.get('applyDate')}: ${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),
+                  ),
+                ],
               ),
             ],
-            if (d.startTime != null && d.endTime != null) ...[
-              const SizedBox(height: 4),
-              Text(
-                '${_formatTime(d.startTime!)} ~ ${_formatTime(d.endTime!)}',
-                style: TextStyle(
-                  fontSize: AppFontSizes.caption,
-                  color: colors.textSecondary,
-                ),
+            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: 4),
-              Text(
+              const SizedBox(height: 2),
+              _detailLabel(
                 '${l10n.get('overtimeDays')}: ${d.jbDays.toStringAsFixed(1)}',
-                style: TextStyle(
-                  fontSize: AppFontSizes.caption,
-                  color: colors.textSecondary,
-                ),
+                colors,
               ),
             ],
             if (d.attPeriod.isNotEmpty) ...[
-              const SizedBox(height: 4),
-              Text(
+              const SizedBox(height: 2),
+              _detailLabel(
                 '${l10n.get('attPeriod')}: ${d.attPeriod}',
-                style: TextStyle(
-                  fontSize: AppFontSizes.caption,
-                  color: colors.textSecondary,
-                ),
+                colors,
               ),
             ],
-            if (d.reason.isNotEmpty) ...[
-              const SizedBox(height: 4),
-              Text(
-                d.reason,
-                maxLines: 2,
-                overflow: TextOverflow.ellipsis,
-                style: TextStyle(
-                  fontSize: AppFontSizes.caption,
-                  color: colors.textSecondary,
-                ),
+            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,
+                ),
             ],
-            const SizedBox(height: 4),
-            Text(
-              '${l10n.get('compensationType')}: ${_compensationTypeLabel(d.compensationType, l10n)}${d.compensationCount > 0 ? ' (${d.compensationCount.toStringAsFixed(1)})' : ''}',
-              style: TextStyle(
-                fontSize: AppFontSizes.caption,
-                color: colors.textSecondary,
-              ),
-            ),
             if (d.adr.isNotEmpty) ...[
-              const SizedBox(height: 4),
-              Text(
+              const SizedBox(height: 2),
+              _detailLabel(
                 '${l10n.get('adr')}: ${d.adr}',
-                style: TextStyle(
-                  fontSize: AppFontSizes.caption,
-                  color: colors.textSecondary,
-                ),
+                colors,
               ),
             ],
-            if (d.rem.isNotEmpty) ...[
-              const SizedBox(height: 4),
-              Text(
-                '${l10n.get('remark')}: ${d.rem}',
-                style: TextStyle(
-                  fontSize: AppFontSizes.caption,
-                  color: colors.textSecondary,
-                ),
+            if (d.reason.isNotEmpty) ...[
+              const SizedBox(height: 2),
+              _detailLabel(
+                '${l10n.get('overtimeDetailReason')}: ${d.reason}',
+                colors,
               ),
             ],
-            if (d.dayOfWeek > 0) ...[
-              const SizedBox(height: 4),
-              Text(
-                '${l10n.get('dayOfWeek')}: ${_dayOfWeekLabel(d.dayOfWeek, l10n)}',
-                style: TextStyle(
-                  fontSize: AppFontSizes.caption,
-                  color: colors.textSecondary,
-                ),
+            if (d.rem.isNotEmpty) ...[
+              const SizedBox(height: 2),
+              _detailLabel(
+                '${l10n.get('remark')}: ${d.rem}',
+                colors,
               ),
             ],
           ],
+        ),
+      ),
+    );
+  }
+
+  String _dayOfWeekLabelFromDate(DateTime dt, AppLocalizations l10n) {
+    switch (dt.weekday) {
+      case 1:
+        return l10n.get('monday');
+      case 2:
+        return l10n.get('tuesday');
+      case 3:
+        return l10n.get('wednesday');
+      case 4:
+        return l10n.get('thursday');
+      case 5:
+        return l10n.get('friday');
+      case 6:
+        return l10n.get('saturday');
+      case 7:
+        return l10n.get('sunday');
+      default:
+        return '';
+    }
+  }
+
+  Widget _detailLabel(String text, AppColorsExtension colors) {
+    return Padding(
+      padding: const EdgeInsets.only(top: 2),
+      child: Text(
+        text,
+        maxLines: 2,
+        overflow: TextOverflow.ellipsis,
+        style: TextStyle(
+          fontSize: AppFontSizes.caption,
+          color: colors.textSecondary,
+        ),
+      ),
+    );
+  }
+
+  Widget _dayOfWeekTag(String label) {
+    final tdTheme = TDTheme.of(context);
+    return Container(
+      padding: const EdgeInsets.symmetric(horizontal: 6),
+      decoration: BoxDecoration(
+        color: tdTheme.brandColor1,
+        borderRadius: BorderRadius.circular(4),
       ),
+      child: TDText(
+        label,
+        font: tdTheme.fontBodySmall,
+        fontWeight: FontWeight.w500,
+        textColor: tdTheme.brandColor7,
       ),
     );
   }
@@ -549,33 +583,6 @@ class _OvertimeApplyDetailPageState
     }
   }
 
-  String _dayOfWeekLabel(int dayOfWeek, AppLocalizations l10n) {
-    switch (dayOfWeek) {
-      case 0:
-        return '-';
-      case 1:
-        return l10n.get('sunday');
-      case 2:
-        return l10n.get('monday');
-      case 3:
-        return l10n.get('tuesday');
-      case 4:
-        return l10n.get('wednesday');
-      case 5:
-        return l10n.get('thursday');
-      case 6:
-        return l10n.get('friday');
-      case 7:
-        return l10n.get('saturday');
-      default:
-        return '-';
-    }
-  }
-
-  String _formatTime(DateTime dt) {
-    return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
-  }
-
   Widget _buildPageFooter(AppColorsExtension colors) {
     final l10n = AppLocalizations.of(context);
     return Center(

+ 2 - 2
lib/features/overtime/overtime_apply_list_page.dart

@@ -384,8 +384,8 @@ class _OvertimeApplyListContent extends ConsumerWidget {
         : m.reason;
     return ListCard(
       cardNo: m.jbNo,
-      amount: m.totalJbHours > 0 ? '${m.totalJbHours.toStringAsFixed(1)}h' : '',
-      amountColor: colors.primary,
+      amount: m.totalJbHours > 0 ? '${m.totalJbHours.toStringAsFixed(1)}${l10n.get('hours')}' : '',
+      amountColor: colors.timePrimary,
       applicant: deptLabel,
       description: desc,
       date: m.jbDd != null ? du.DateUtils.formatDate(m.jbDd!) : '',

+ 6 - 0
lib/features/overtime/overtime_apply_model.dart

@@ -134,6 +134,7 @@ class OvertimeApplyDetailModel {
   final String salNo; // SAL_NO 员工代号
   final String salName; // 员工姓名(瞬态)
   final String dep; // DEP 部门
+  final String depName; // DEP_NAME 部门名称(瞬态)
   final String
   jbType; // JB_TYPE: WORKING_DAY/REST_DAY/PUBLIC_HOLIDAY/SPECIAL_HOLIDAY/OTHER
   final DateTime? jbDate; // JB_DATE 加班日期
@@ -156,6 +157,7 @@ class OvertimeApplyDetailModel {
     this.salNo = '',
     this.salName = '',
     this.dep = '',
+    this.depName = '',
     this.jbType = 'WORKING_DAY',
     this.jbDate,
     this.startTime,
@@ -178,6 +180,7 @@ class OvertimeApplyDetailModel {
       salNo: json['SAL_NO'] as String? ?? '',
       salName: json['SAL_NAME'] as String? ?? '',
       dep: json['DEP'] as String? ?? '',
+      depName: json['DEP_NAME'] as String? ?? '',
       jbType: json['JB_TYPE'] as String? ?? 'WORKING_DAY',
       jbDate: json['JB_DATE'] != null
           ? DateTime.tryParse(json['JB_DATE'] as String)
@@ -212,6 +215,7 @@ class OvertimeApplyDetailModel {
     'SAL_NO': salNo,
     'SAL_NAME': salName,
     'DEP': dep,
+    'DEP_NAME': depName,
     'JB_TYPE': jbType,
     'JB_DATE': jbDate?.toIso8601String(),
     'START_TIME': startTime?.toIso8601String(),
@@ -233,6 +237,7 @@ class OvertimeApplyDetailModel {
     String? salNo,
     String? salName,
     String? dep,
+    String? depName,
     String? jbType,
     DateTime? jbDate,
     DateTime? startTime,
@@ -253,6 +258,7 @@ class OvertimeApplyDetailModel {
       salNo: salNo ?? this.salNo,
       salName: salName ?? this.salName,
       dep: dep ?? this.dep,
+      depName: depName ?? this.depName,
       jbType: jbType ?? this.jbType,
       jbDate: jbDate ?? this.jbDate,
       startTime: startTime ?? this.startTime,

+ 251 - 73
lib/features/overtime/widgets/overtime_apply_detail_dialog.dart

@@ -18,6 +18,7 @@ class OvertimeDetailData {
   final String salNo;
   final String salName;
   final String dep;
+  final String depName;
   final String jbType;
   final String jbDate;
   final String startTime;
@@ -37,6 +38,7 @@ class OvertimeDetailData {
     this.salNo = '',
     this.salName = '',
     this.dep = '',
+    this.depName = '',
     this.jbType = 'WORKING_DAY',
     this.jbDate = '',
     this.startTime = '',
@@ -45,7 +47,7 @@ class OvertimeDetailData {
     this.jbDays = 0.0,
     this.attPeriod = '',
     this.reason = '',
-    this.compensationType = 'OVERTIME_PAY',
+    this.compensationType = '',
     this.compensationCount = 0.0,
     this.adr = '',
     this.rem = '',
@@ -116,7 +118,7 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
   final _reasonCtrl = TextEditingController();
   final _reasonFocus = FocusNode();
   // 补偿类型
-  String _compensationType = 'OVERTIME_PAY';
+  String _compensationType = '';
   // 折算补偿次数
   final _compCountCtrl = TextEditingController();
   final _compCountFocus = FocusNode();
@@ -150,6 +152,11 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
 
   bool get _isEdit => widget.initialData != null;
 
+  String _todayStr() {
+    final now = DateTime.now();
+    return '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
+  }
+
   @override
   void initState() {
     super.initState();
@@ -158,25 +165,39 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
         ? EmployeeItem(salNo: d.salNo, name: d.salName)
         : null;
     _dep = d?.dep ?? '';
-    _depName = d?.dep ?? '';
+    _depName = d?.depName ?? '';
     _jbType = d?.jbType ?? 'WORKING_DAY';
     _jbDate = d?.jbDate ?? '';
     if (!_isEdit && _jbDate.isEmpty) {
       final now = DateTime.now();
       _jbDate = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
-      _attPeriod = _jbDate.substring(0, 7);
     }
     _startTime = d?.startTime ?? '';
     _endTime = d?.endTime ?? '';
+    // 新增模式:开始/结束时间给默认值
+    if (!_isEdit) {
+      final dateStr = _jbDate.isNotEmpty ? _jbDate : _todayStr();
+      final weekday = DateTime.tryParse(dateStr)?.weekday ?? DateTime.now().weekday;
+      if (_startTime.isEmpty) {
+        final h = (weekday <= DateTime.friday) ? 18 : 9;
+        _startTime = '$dateStr ${h.toString().padLeft(2, '0')}:00';
+      }
+      if (_endTime.isEmpty) {
+        final h = (weekday <= DateTime.friday) ? 19 : 18;
+        _endTime = '$dateStr ${h.toString().padLeft(2, '0')}:00';
+      }
+      _attPeriod = dateStr.substring(0, 7);
+      _autoCalcHours();
+    }
     if (d != null) {
       _hoursCtrl.text = d.jbHours > 0 ? d.jbHours.toString() : '';
       _daysCtrl.text = d.jbDays > 0 ? d.jbDays.toString() : '';
+      _attPeriod = d.attPeriod;
     }
-    _attPeriod = d?.attPeriod ?? '';
     if (d != null) {
       _reasonCtrl.text = d.reason;
     }
-    _compensationType = d?.compensationType ?? 'OVERTIME_PAY';
+    _compensationType = d?.compensationType ?? '';
     if (d != null) {
       _compCountCtrl.text = d.compensationCount > 0
           ? d.compensationCount.toString()
@@ -187,8 +208,14 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
       _remarkCtrl.text = d.rem;
     }
     _reasonFocus.addListener(() => _ensureVisible(_reasonFocus));
-    _hoursFocus.addListener(() => _ensureVisible(_hoursFocus));
-    _daysFocus.addListener(() => _ensureVisible(_daysFocus));
+    _hoursFocus.addListener(() {
+      _ensureVisible(_hoursFocus);
+      if (!_hoursFocus.hasFocus) _roundToHalf(_hoursCtrl);
+    });
+    _daysFocus.addListener(() {
+      _ensureVisible(_daysFocus);
+      if (!_daysFocus.hasFocus) _roundToHalf(_daysCtrl);
+    });
     _compCountFocus.addListener(() => _ensureVisible(_compCountFocus));
     _adrFocus.addListener(() => _ensureVisible(_adrFocus));
     _remarkFocus.addListener(() => _ensureVisible(_remarkFocus));
@@ -317,6 +344,7 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
         salNo: _selEmployee!.salNo,
         salName: _selEmployee!.name,
         dep: _dep,
+        depName: _depName,
         jbType: _jbType,
         jbDate: _jbDate,
         startTime: _startTime,
@@ -372,10 +400,9 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
                         mainAxisSize: MainAxisSize.min,
                         crossAxisAlignment: CrossAxisAlignment.stretch,
                         children: [
+                          // ── 必填 ──
                           _buildEmployeePicker(colors),
                           const SizedBox(height: 12),
-                          _buildDeptPicker(colors),
-                          const SizedBox(height: 12),
                           _buildJbTypeSelector(colors),
                           const SizedBox(height: 12),
                           _buildJbDatePicker(colors),
@@ -383,6 +410,9 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
                           _buildStartTimePicker(colors),
                           const SizedBox(height: 12),
                           _buildEndTimePicker(colors),
+                          // ── 非必填 ──
+                          const SizedBox(height: 12),
+                          _buildDeptPicker(colors),
                           const SizedBox(height: 12),
                           _buildHoursInput(colors),
                           const SizedBox(height: 12),
@@ -446,7 +476,7 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
               Expanded(
                 child: Center(
                   child: Text(
-                    _l10n.get('addDetail'),
+                    _isEdit ? _l10n.get('editOvertimeDetail') : _l10n.get('addOvertimeDetail'),
                     style: TextStyle(
                       fontSize: AppFontSizes.title,
                       fontWeight: FontWeight.w600,
@@ -680,6 +710,29 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
     }
   }
 
+  String? _dayOfWeekLabel(String dateTimeStr) {
+    final dt = DateTime.tryParse(dateTimeStr);
+    if (dt == null) return null;
+    switch (dt.weekday) {
+      case 1:
+        return _l10n.get('monday');
+      case 2:
+        return _l10n.get('tuesday');
+      case 3:
+        return _l10n.get('wednesday');
+      case 4:
+        return _l10n.get('thursday');
+      case 5:
+        return _l10n.get('friday');
+      case 6:
+        return _l10n.get('saturday');
+      case 7:
+        return _l10n.get('sunday');
+      default:
+        return null;
+    }
+  }
+
   // ── 4. 申请日期 ──
   Widget _buildJbDatePicker(AppColorsExtension colors) {
     return _datePickerCard(
@@ -701,6 +754,7 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
       hint: _l10n.get('pleaseSelect'),
       colors: colors,
       required: true,
+      showDayOfWeek: true,
       onPick: (d) {
         setState(() {
           _startTime = d;
@@ -727,6 +781,7 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
       hint: _l10n.get('pleaseSelect'),
       colors: colors,
       required: true,
+      showDayOfWeek: true,
       onPick: (d) {
         setState(() {
           _endTime = d;
@@ -746,61 +801,59 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
 
   void _autoCalcHours() {
     if (_startTime.isEmpty || _endTime.isEmpty) return;
-    try {
-      final stParts = _startTime.split(' ');
-      final etParts = _endTime.split(' ');
-      if (stParts.length < 2 || etParts.length < 2) return;
-      final stTime = stParts[1];
-      final etTime = etParts[1];
-      final st = _parseTime(stTime);
-      final et = _parseTime(etTime);
-      if (st == null || et == null) return;
-      double hours = et - st;
-      if (hours < 0) hours += 24.0;
-      if (hours > 0) {
-        // 按0.5半小时模式向上取整
-        final rounded = (hours * 2).ceil() / 2.0;
-        _hoursCtrl.text = rounded.toStringAsFixed(1);
-      }
-      _autoCalcDays();
-    } catch (_) {}
+    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;
+    }
+
+    if (totalHours > 0) {
+      _hoursCtrl.text = ((totalHours * 2).ceil() / 2.0).toStringAsFixed(1);
+    }
+    _autoCalcDays();
   }
 
   void _autoCalcDays() {
-    if (_startTime.isEmpty || _endTime.isEmpty) return;
-    try {
-      final stDateStr = _startTime.split(' ').first;
-      final etDateStr = _endTime.split(' ').first;
-      final stDate = DateTime.tryParse(stDateStr);
-      final etDate = DateTime.tryParse(etDateStr);
-      if (stDate == null || etDate == null) return;
-      final diffDays = etDate.difference(stDate).inDays;
-      if (diffDays < 0) return;
-      // 按0.5半天模式向上取整
-      final ceilDays = (diffDays * 2).ceil() / 2.0;
-      _daysCtrl.text = ceilDays.toStringAsFixed(1);
-    } catch (_) {}
+    final hours = double.tryParse(_hoursCtrl.text) ?? 0;
+    if (hours <= 0) return;
+    // 8小时工作制:天数 = 小时 ÷ 8
+    final diffDays = hours / 8.0;
+    _daysCtrl.text = ((diffDays * 2).ceil() / 2.0).toStringAsFixed(1);
   }
 
-  double? _parseTime(String time) {
-    final parts = time.split(':');
-    if (parts.length < 2) return null;
-    final h = double.tryParse(parts[0]);
-    final m = double.tryParse(parts[1]);
-    if (h == null || m == null) return null;
-    return h + m / 60.0;
+  /// 手动输入时失焦自动按 0.5 向上取整
+  void _roundToHalf(TextEditingController controller) {
+    final text = controller.text.trim();
+    if (text.isEmpty) return;
+    final value = double.tryParse(text);
+    if (value == null || value <= 0) return;
+    final rounded = (value * 2).ceil() / 2.0;
+    final formatted = rounded.toStringAsFixed(1);
+    if (controller.text != formatted) {
+      controller.text = formatted;
+    }
   }
 
   // ── 6. 加班时长 ──
   Widget _buildHoursInput(AppColorsExtension colors) {
     return _inputCard(
       label: _l10n.get('overtimeHours'),
-      required: true,
+      required: false,
       controller: _hoursCtrl,
       hintText: '>0',
       keyboardType: const TextInputType.numberWithOptions(decimal: true),
       inputFormatters: [
-        FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,1}$')),
+        FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?[05]?$')),
       ],
       focusNode: _hoursFocus,
     );
@@ -815,7 +868,7 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
       hintText: '0',
       keyboardType: const TextInputType.numberWithOptions(decimal: true),
       inputFormatters: [
-        FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')),
+        FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?[05]?$')),
       ],
       focusNode: _daysFocus,
     );
@@ -841,7 +894,7 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
     return TDTextarea(
       controller: _reasonCtrl,
       focusNode: _reasonFocus,
-      label: _l10n.get('overtimeReason'),
+      label: _l10n.get('overtimeDetailReason'),
       hintText: _l10n.get('enterReason'),
       maxLines: 3,
       minLines: 1,
@@ -858,11 +911,12 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
 
   // ── 10. 补偿类型 ──
   Widget _buildCompensationTypeSelector(AppColorsExtension colors) {
-    final selLabel = _compensationTypeLabel(_compensationType);
+    final hasValue = _compensationType.isNotEmpty;
+    final selLabel = hasValue ? _compensationTypeLabel(_compensationType) : _l10n.get('pleaseSelect');
     return _pickerCard(
       label: _l10n.get('compensationType'),
-      required: true,
-      hasValue: true,
+      required: false,
+      hasValue: hasValue,
       currentLabel: selLabel,
       onTap: () => _showCompensationTypePicker(),
     );
@@ -881,7 +935,16 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
         if (selected.isNotEmpty) {
           final idx = selected.first is int ? selected.first as int : 0;
           if (idx >= 0 && idx < _compensationTypeOptions.length) {
-            setState(() => _compensationType = _compensationTypeOptions[idx]);
+            final newType = _compensationTypeOptions[idx];
+            setState(() {
+              _compensationType = newType;
+              // 无补偿 → 清空;其他 → 默认取 JB_DAYS
+              if (newType == 'NO_COMPENSATION') {
+                _compCountCtrl.clear();
+              } else if (_compCountCtrl.text.isEmpty && _daysCtrl.text.isNotEmpty) {
+                _compCountCtrl.text = _daysCtrl.text;
+              }
+            });
           }
         }
         Navigator.of(context).pop();
@@ -906,6 +969,46 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
 
   // ── 11. 折算补偿次数 ──
   Widget _buildCompCountInput(AppColorsExtension colors) {
+    // 无补偿时显示空
+    if (_compensationType == 'NO_COMPENSATION') {
+      final tdTheme = TDTheme.of(context);
+      return GestureDetector(
+        onTap: () {},
+        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(
+                _l10n.get('compensationCount'),
+                maxLines: 1,
+                overflow: TextOverflow.visible,
+                font: tdTheme.fontBodyLarge,
+                fontWeight: FontWeight.w400,
+                style: const TextStyle(letterSpacing: 0),
+              ),
+              const SizedBox(width: 12),
+              Expanded(
+                child: TDText(
+                  '',
+                  maxLines: 1,
+                  textAlign: TextAlign.end,
+                  font: tdTheme.fontBodyLarge,
+                  fontWeight: FontWeight.w400,
+                  textColor: tdTheme.textColorPlaceholder,
+                ),
+              ),
+            ],
+          ),
+        ),
+      );
+    }
     return _inputCard(
       label: _l10n.get('compensationCount'),
       required: false,
@@ -989,11 +1092,13 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
     required ValueChanged<String> onPick,
     VoidCallback? onClear,
     bool required = false,
+    bool showDayOfWeek = false,
   }) {
     final tdTheme = TDTheme.of(context);
     final hasValue = value.isNotEmpty;
+    final dayOfWeek = showDayOfWeek && hasValue ? _dayOfWeekLabel(value) : null;
     return GestureDetector(
-      onTap: () => _pickDateTime(label, onPick),
+      onTap: () => _pickDateTime(label, onPick, currentValue: value),
       child: Container(
         padding: const EdgeInsets.only(
           left: 16,
@@ -1008,9 +1113,17 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
         ),
         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(right: 2),
+                padding: const EdgeInsets.only(left: 4),
                 child: TDText(
                   '*',
                   font: tdTheme.fontBodyLarge,
@@ -1018,14 +1131,6 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
                   style: TextStyle(color: tdTheme.errorColor6),
                 ),
               ),
-            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(
@@ -1045,6 +1150,25 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
                       textAlign: TextAlign.end,
                     ),
                   ),
+                  if (dayOfWeek != null) ...[
+                    const SizedBox(width: 6),
+                    Container(
+                      padding: const EdgeInsets.symmetric(
+                        horizontal: 6,
+                        vertical: 2,
+                      ),
+                      decoration: BoxDecoration(
+                        color: tdTheme.brandColor1,
+                        borderRadius: BorderRadius.circular(4),
+                      ),
+                      child: TDText(
+                        dayOfWeek,
+                        font: TDTheme.of(context).fontBodySmall,
+                        fontWeight: FontWeight.w500,
+                        textColor: tdTheme.brandColor7,
+                      ),
+                    ),
+                  ],
                   const SizedBox(width: 4),
                   SizedBox(
                     width: 18,
@@ -1073,13 +1197,63 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
     );
   }
 
-  void _pickDateTime(String label, ValueChanged<String> onPick) {
+  void _pickDateTime(String label, ValueChanged<String> onPick,
+      {String currentValue = ''}) {
     final l10n = _l10n;
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final now = DateTime.now();
     final isDateOnly =
         label == _l10n.get('applyDate') || label == _l10n.get('attPeriod');
     final isAttPeriod = label == _l10n.get('attPeriod');
+    final isStartTime = label == _l10n.get('startTime');
+    final isEndTime = label == _l10n.get('endTime');
+
+    // 根据当前值定位初始日期,无值时默认当月/当天
+    int initYear = now.year;
+    int initMonth = now.month;
+    int initDay = now.day;
+    int initHour = now.hour;
+    int initMinute = now.minute;
+
+    if (currentValue.isNotEmpty) {
+      if (isAttPeriod) {
+        // yyyy-MM
+        final parts = currentValue.split('-');
+        if (parts.length == 2) {
+          initYear = int.tryParse(parts[0]) ?? now.year;
+          initMonth = int.tryParse(parts[1]) ?? now.month;
+          initDay = 1;
+        }
+      } else {
+        // yyyy-MM-dd 或 yyyy-MM-dd HH:mm
+        final dt = DateTime.tryParse(currentValue);
+        if (dt != null) {
+          initYear = dt.year;
+          initMonth = dt.month;
+          initDay = dt.day;
+          initHour = dt.hour;
+          initMinute = dt.minute;
+        }
+      }
+    } 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;
     FocusManager.instance.primaryFocus?.unfocus();
     TDPicker.showDatePicker(
       context,
@@ -1088,13 +1262,17 @@ class _OvertimeApplyDetailDialogState extends State<OvertimeApplyDetailDialog> {
       useYear: true,
       useMonth: true,
       useDay: !isAttPeriod,
-      useHour: !isDateOnly,
-      useMinute: !isDateOnly,
+      useHour: useTime,
+      useMinute: useTime,
       useSecond: false,
       useWeekDay: false,
-      dateStart: const [2020, 1, 1],
-      dateEnd: [now.year + 1, 12, 31],
-      initialDate: [now.year, now.month, now.day],
+      dateStart: useTime ? const [2020, 1, 1, 0, 0] : const [2020, 1, 1],
+      dateEnd: useTime
+          ? [now.year + 1, 12, 31, 23, 59]
+          : [now.year + 1, 12, 31],
+      initialDate: useTime
+          ? [initYear, initMonth, initDay, initHour, initMinute]
+          : [initYear, initMonth, initDay],
       onConfirm: (selected) {
         final year = selected['year'];
         final month = selected['month'].toString().padLeft(2, '0');

+ 98 - 28
lib/features/overtime/widgets/overtime_apply_detail_view_dialog.dart

@@ -35,9 +35,13 @@ class OvertimeApplyDetailViewDialog extends StatelessWidget {
     final d = detail;
 
     final empLabel = d.salName.isNotEmpty
-        ? '${d.salNo} / ${d.salName}'
+        ? '${d.salNo}/${d.salName}'
         : d.salNo;
 
+    final deptLabel = d.depName.isNotEmpty
+        ? '${d.dep}/${d.depName}'
+        : (d.dep.isNotEmpty ? d.dep : '-');
+
     return ConstrainedBox(
       constraints: BoxConstraints(
         maxHeight: MediaQuery.of(context).size.height * 0.8,
@@ -84,38 +88,47 @@ class OvertimeApplyDetailViewDialog extends StatelessWidget {
                     crossAxisAlignment: CrossAxisAlignment.start,
                     children: [
                       _row(l10n.get('employee'), empLabel, colors),
-                      if (d.dep.isNotEmpty)
-                        _row(l10n.get('dep'), d.dep, colors),
+                      _row(l10n.get('dep'), deptLabel, colors),
                       _row(l10n.get('jbType'), _jbTypeLabel(d.jbType), colors),
                       if (d.jbDate != null)
-                        _row(l10n.get('date'),
+                        _row(l10n.get('applyDate'),
                             _formatDate(d.jbDate!), colors),
                       if (d.startTime != null)
-                        _row(l10n.get('startTime'),
-                            _formatDateTime(d.startTime!), colors),
+                        _rowWithTag(
+                          context,
+                          l10n.get('startTime'),
+                          _formatDateTime(d.startTime!),
+                          _dayOfWeekFromDate(d.startTime!),
+                          colors,
+                        ),
                       if (d.endTime != null)
-                        _row(l10n.get('endTime'),
-                            _formatDateTime(d.endTime!), colors),
+                        _rowWithTag(
+                          context,
+                          l10n.get('endTime'),
+                          _formatDateTime(d.endTime!),
+                          _dayOfWeekFromDate(d.endTime!),
+                          colors,
+                        ),
                       _row(l10n.get('overtimeHours'),
-                          '${d.jbHours.toStringAsFixed(1)}h', colors),
+                          '${d.jbHours.toStringAsFixed(1)}${l10n.get('hours')}', colors,
+                          valueColor: colors.timePrimary),
                       _row(l10n.get('overtimeDays'),
-                          '${d.jbDays.toStringAsFixed(1)}', colors),
+                          d.jbDays.toStringAsFixed(1), colors),
                       if (d.attPeriod.isNotEmpty)
                         _row(l10n.get('attPeriod'), d.attPeriod, colors),
                       if (d.reason.isNotEmpty)
-                        _row(l10n.get('reason'), d.reason, colors),
-                      _row(l10n.get('compensationType'),
-                          _compensationTypeLabel(d.compensationType), colors),
-                      if (d.compensationCount > 0)
+                        _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),
                       if (d.adr.isNotEmpty)
                         _row(l10n.get('adr'), d.adr, colors),
                       if (d.rem.isNotEmpty)
                         _row(l10n.get('remark'), d.rem, colors),
-                      if (d.dayOfWeek > 0)
-                        _row(l10n.get('dayOfWeek'),
-                            _dayOfWeekLabel(d.dayOfWeek), colors),
                     ],
                   ),
                 ),
@@ -127,7 +140,8 @@ class OvertimeApplyDetailViewDialog extends StatelessWidget {
     );
   }
 
-  Widget _row(String label, String value, AppColorsExtension colors) {
+  Widget _row(String label, String value, AppColorsExtension colors,
+      {Color? valueColor}) {
     return Padding(
       padding: const EdgeInsets.only(bottom: 12),
       child: Row(
@@ -148,7 +162,7 @@ class OvertimeApplyDetailViewDialog extends StatelessWidget {
               value,
               style: TextStyle(
                 fontSize: 14,
-                color: colors.textPrimary,
+                color: valueColor ?? colors.textPrimary,
               ),
             ),
           ),
@@ -157,6 +171,62 @@ class OvertimeApplyDetailViewDialog extends StatelessWidget {
     );
   }
 
+  Widget _rowWithTag(
+    BuildContext context,
+    String label,
+    String value,
+    String tag,
+    AppColorsExtension colors,
+  ) {
+    final tdTheme = TDTheme.of(context);
+    return Padding(
+      padding: const EdgeInsets.only(bottom: 12),
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          SizedBox(
+            width: 100,
+            child: Text(
+              label,
+              style: TextStyle(
+                fontSize: 14,
+                color: colors.textSecondary,
+              ),
+            ),
+          ),
+          Expanded(
+            child: Wrap(
+              crossAxisAlignment: WrapCrossAlignment.center,
+              spacing: 6,
+              children: [
+                Text(
+                  value,
+                  style: TextStyle(
+                    fontSize: 14,
+                    color: colors.textPrimary,
+                  ),
+                ),
+                Container(
+                  padding: const EdgeInsets.symmetric(horizontal: 6),
+                  decoration: BoxDecoration(
+                    color: tdTheme.brandColor1,
+                    borderRadius: BorderRadius.circular(4),
+                  ),
+                  child: TDText(
+                    tag,
+                    font: tdTheme.fontBodySmall,
+                    fontWeight: FontWeight.w500,
+                    textColor: tdTheme.brandColor7,
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
   String _formatDate(DateTime d) {
     return '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
   }
@@ -197,22 +267,22 @@ class OvertimeApplyDetailViewDialog extends StatelessWidget {
     }
   }
 
-  String _dayOfWeekLabel(int dow) {
-    switch (dow) {
+  String _dayOfWeekFromDate(DateTime dt) {
+    switch (dt.weekday) {
       case 1:
-        return l10n.get('sunday');
-      case 2:
         return l10n.get('monday');
-      case 3:
+      case 2:
         return l10n.get('tuesday');
-      case 4:
+      case 3:
         return l10n.get('wednesday');
-      case 5:
+      case 4:
         return l10n.get('thursday');
-      case 6:
+      case 5:
         return l10n.get('friday');
-      case 7:
+      case 6:
         return l10n.get('saturday');
+      case 7:
+        return l10n.get('sunday');
       default:
         return '';
     }

+ 1 - 1
lib/features/vehicle/vehicle_apply_create_page.dart

@@ -929,7 +929,7 @@ class _VehicleApplyCreatePageState
           api.getEmployees(keyword: keyword, page: page, size: 20),
       labelBuilder: (e) => e.name.isEmpty ? e.salNo : '${e.salNo} ${e.name}',
     );
-    if (selected != null && mounted) {
+    if (mounted) {
       setState(() {
         for (final emp in selected) {
           if (!_passengers.any((p) => p.salNo == emp.salNo)) {

+ 1 - 10
lib/features/vehicle/vehicle_apply_detail_page.dart

@@ -291,6 +291,7 @@ class _VehicleApplyDetailPageState
           readOnly: true,
           showArrow: false,
           bold: true,
+          valueColor: colors.platePrimary,
         ),
         const SizedBox(height: 16),
         FormFieldRow(
@@ -470,16 +471,6 @@ class _VehicleApplyDetailPageState
   }
 
   // ═══ 工具方法 ═══
-  Widget _label(String t, AppColorsExtension colors) {
-    return Text(
-      t,
-      style: TextStyle(
-        fontSize: AppFontSizes.subtitle,
-        color: colors.textSecondary,
-      ),
-    );
-  }
-
   Widget _buildPageFooter(AppColorsExtension colors) {
     final l10n = AppLocalizations.of(context);
     return Center(

+ 1 - 1
lib/features/vehicle/vehicle_apply_list_page.dart

@@ -380,7 +380,7 @@ class _VehicleApplyListContent extends ConsumerWidget {
       description: descParts.isNotEmpty ? descParts.join('\n') : '-',
       date: m.ycDd != null ? du.DateUtils.formatDate(m.ycDd!) : '-',
       amount: m.licensePlate.isNotEmpty ? m.licensePlate : '-',
-      amountColor: colors.primary,
+      amountColor: colors.platePrimary,
       statusTag: _buildAuditTag(m, l10n, colors),
       onTap: () => context.push('/vehicle-apply/detail/${m.ycNo}'),
     );

+ 3 - 1
lib/shared/widgets/form_field_row.dart

@@ -19,6 +19,7 @@ class FormFieldRow extends StatelessWidget {
   final VoidCallback? onClear;
   final bool bold;
   final bool showMoreOnOverflow;
+  final Color? valueColor;
 
   const FormFieldRow({
     super.key,
@@ -32,6 +33,7 @@ class FormFieldRow extends StatelessWidget {
     this.onClear,
     this.bold = false,
     this.showMoreOnOverflow = true,
+    this.valueColor,
   });
 
   @override
@@ -126,7 +128,7 @@ class FormFieldRow extends StatelessWidget {
                     fontSize: AppFontSizes.subtitle,
                     fontWeight: bold ? FontWeight.w600 : FontWeight.normal,
                     color: hasValue
-                        ? colors.textPrimary
+                        ? (valueColor ?? colors.textPrimary)
                         : colors.textPlaceholder,
                   );
                   final displayText = hasValue