chengc hai 1 día
pai
achega
a5f773e3d3

+ 3 - 1
assets/i18n/en.json

@@ -946,6 +946,8 @@
     "attachServiceUnavailable": "Attachment service unavailable",
     "downloadFailed": "Download failed",
     "openFailed": "Open failed",
-    "downloading": "Loading…"
+    "downloading": "Loading…",
+    "headerAttachments": "Header Attachments",
+    "detailLine": "Detail Line"
   }
 }

+ 3 - 1
assets/i18n/zh_CN.json

@@ -796,6 +796,8 @@
     "attachServiceUnavailable": "附件服务暂不可用",
     "downloadFailed": "下载失败",
     "openFailed": "打开失败",
-    "downloading": "加载中…"
+    "downloading": "加载中…",
+    "headerAttachments": "表头附件",
+    "detailLine": "明细行"
   }
 }

+ 3 - 1
assets/i18n/zh_TW.json

@@ -796,6 +796,8 @@
     "attachServiceUnavailable": "附件服務暫不可用",
     "downloadFailed": "下載失敗",
     "openFailed": "開啟失敗",
-    "downloading": "載入中…"
+    "downloading": "載入中…",
+    "headerAttachments": "表頭附件",
+    "detailLine": "明細行"
   }
 }

+ 30 - 1
lib/features/expense/expense_create_page.dart

@@ -1,4 +1,5 @@
 import 'dart:async';
+import 'dart:io';
 
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -1011,12 +1012,14 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
           final api = ref.read(expenseApiProvider);
           final billNo = await api.submit(data);
           // 上传附件(billNo 提取失败则跳过,不影响主流程)
-          if (billNo != null && _attachmentController.files.isNotEmpty) {
+          if (billNo != null) {
             final now = DateTime.now();
             final effDd = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} '
                 '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}.'
                 '${now.millisecond.toString().padLeft(3, '0')}';
             final usr = HostAppChannel.usr;
+
+            // 表头附件
             for (var i = 0; i < _attachmentController.files.length; i++) {
               final file = _attachmentController.files[i];
               try {
@@ -1035,6 +1038,32 @@ class _ExpenseCreatePageState extends ConsumerState<ExpenseCreatePage>
                 // Attachment upload failure is non-fatal
               }
             }
+
+            // 明细附件
+            for (var i = 0; i < state.expense.details.length; i++) {
+              final detail = state.expense.details[i];
+              if (detail.attachments.isEmpty) continue;
+              for (var j = 0; j < detail.attachments.length; j++) {
+                final file = File(detail.attachments[j]);
+                try {
+                  if (!await file.exists()) continue;
+                  final fileName = file.path.split('/').last;
+                  await api.uploadAttachment(file.path, {
+                    'BIL_ID': 'BX',
+                    'BIL_NO': billNo,
+                    'SRCITM': i + 1,
+                    'ITM': j + 1,
+                    'TAG': 1,
+                    'EFF_DD': effDd,
+                    'USR': usr,
+                    'FILENAME': fileName,
+                    'EXT': fileName.split('.').last,
+                  });
+                } catch (_) {
+                  // Detail attachment upload failure is non-fatal
+                }
+              }
+            }
           }
           await ExpenseCreateController.deleteDraft();
           if (mounted) {

+ 20 - 0
lib/features/expense/expense_detail_page.dart

@@ -151,6 +151,8 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
             _buildExpenseDetailSection(expense, l10n, colors),
             const SizedBox(height: 16),
             _buildAttachmentSection(l10n, colors),
+            const SizedBox(height: 24),
+            _buildPageFooter(colors),
           ],
         ),
       ),
@@ -492,6 +494,24 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
       default: return Icons.insert_drive_file;
     }
   }
+
+  Widget _buildPageFooter(AppColorsExtension colors) {
+    final l10n = AppLocalizations.of(context);
+    return Center(
+      child: Padding(
+        padding: const EdgeInsets.only(bottom: 16),
+        child: Row(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            Icon(Icons.rocket_launch_outlined, size: 16, color: colors.textPlaceholder),
+            const SizedBox(width: 6),
+            Text(l10n.get('pageFooter'),
+                style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textPlaceholder)),
+          ],
+        ),
+      ),
+    );
+  }
 }
 
 /// 附件缩略图 — 自动调用 DownloadAttachment 加载图片

+ 10 - 3
lib/features/expense/expense_list_page.dart

@@ -9,7 +9,6 @@ import '../../core/theme/app_colors_extension.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
 import '../../shared/widgets/list_card.dart';
-import '../../shared/widgets/status_tag.dart';
 import '../../shared/widgets/empty_state.dart';
 import '../../shared/widgets/skeleton_list_card.dart';
 import '../../shared/widgets/list_footer.dart';
@@ -175,7 +174,11 @@ class _ExpenseListContent extends ConsumerWidget {
       final oldItems = itemsAsync.valueOrNull ?? [];
       if (oldItems.isEmpty) return ListView(children: [const SizedBox(height: 120), EmptyState(message: l10n.get('noExpenses'))]);
       return ListView.builder(padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), itemCount: oldItems.length, itemBuilder: (_, i) {
-        final card = ListCard(cardNo: oldItems[i].expenseNo, amount: '¥${oldItems[i].totalAmount.toStringAsFixed(2)}', applicant: oldItems[i].applicantName, description: oldItems[i].purpose, date: du.DateUtils.formatDate(oldItems[i].createTime), statusTag: StatusTag.fromStatus(oldItems[i].status, l10n), onTap: () { context.push('/expense/detail/${oldItems[i].expenseNo}'); });
+        final item = oldItems[i];
+        final applicant = item.deptName.isNotEmpty
+            ? '${item.applicantName} · ${item.deptName}'
+            : item.applicantName;
+        final card = ListCard(cardNo: item.expenseNo, amount: '¥${item.totalAmount.toStringAsFixed(2)}', applicant: applicant, description: item.purpose, date: du.DateUtils.formatDate(item.expenseDate ?? item.createTime), onTap: () { context.push('/expense/detail/${item.expenseNo}'); });
         return Padding(padding: const EdgeInsets.only(bottom: 16), child: card);
       });
     }
@@ -184,7 +187,11 @@ class _ExpenseListContent extends ConsumerWidget {
     if (items.isEmpty) return ListView(children: [const SizedBox(height: 120), EmptyState(message: l10n.get('noExpenses'))]);
     return ListView.builder(padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), itemCount: items.length + 1, itemBuilder: (_, i) {
       if (i == items.length) return ListFooter(itemCount: items.length);
-      final card = ListCard(cardNo: items[i].expenseNo, amount: '¥${items[i].totalAmount.toStringAsFixed(2)}', applicant: items[i].applicantName, description: items[i].purpose, date: du.DateUtils.formatDate(items[i].createTime), statusTag: StatusTag.fromStatus(items[i].status, l10n), onTap: () { context.push('/expense/detail/${items[i].expenseNo}'); });
+      final item = items[i];
+      final applicant = item.deptName.isNotEmpty
+          ? '${item.applicantName} · ${item.deptName}'
+          : item.applicantName;
+      final card = ListCard(cardNo: item.expenseNo, amount: '¥${item.totalAmount.toStringAsFixed(2)}', applicant: applicant, description: item.purpose, date: du.DateUtils.formatDate(item.expenseDate ?? item.createTime), onTap: () { context.push('/expense/detail/${item.expenseNo}'); });
       return Padding(padding: const EdgeInsets.only(bottom: 16), child: card);
     });
   }

+ 1 - 1
lib/features/expense/expense_model.dart

@@ -94,7 +94,7 @@ class ExpenseModel {
       voucherNo: json['vohNo'] as String? ?? '',
       approvedAmount: (json['approvedAmount'] as num?)?.toDouble() ?? 0.0,
       totalAmount: (json['totalAmount'] as num?)?.toDouble() ?? 0.0,
-      purpose: json['reason'] as String? ?? '',
+      purpose: json['reason'] as String? ?? json['purpose'] as String? ?? '',
       remark: json['rem'] as String? ?? '',
       paymentMethod: json['payId'] as String? ?? '',
       isInvoiceVerified: json['isInvoiceVerified'] as bool? ?? false,

+ 2 - 2
lib/features/expense_apply/expense_apply_model.dart

@@ -78,8 +78,8 @@ class ExpenseApplyModel {
       deptId: json['dept'] as String? ?? '',
       deptName: json['deptName'] as String? ?? json['dep'] as String? ?? '',
       estimatedAmount: (json['estimatedAmount'] as num?)?.toDouble() ?? 0.0,
-      urgency: json['priority'] as String? ?? 'normal',
-      purpose: json['reason'] as String? ?? '',
+      urgency: json['urgency'] as String? ?? 'normal',
+      purpose: json['reason'] as String? ?? json['purpose'] as String? ?? '',
       remark: json['rem'] as String? ?? '',
       effectiveDate: json['effDd'] != null
           ? DateTime.parse(json['effDd'] as String)

+ 1 - 1
lib/shared/widgets/list_card.dart

@@ -50,7 +50,7 @@ class ListCard extends StatelessWidget {
           ],
           const SizedBox(height: 8),
           Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
-            Flexible(child: Text(date, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: colors.textPlaceholder))),
+            Flexible(child: Text(date, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: colors.textSecondary))),
             const SizedBox(width: 8),
             ?statusTag,
           ]),