|
|
@@ -8,11 +8,13 @@ import '../../core/utils/date_utils.dart' as du;
|
|
|
import '../../shared/widgets/form_section.dart';
|
|
|
import '../../shared/widgets/form_field_row.dart';
|
|
|
import '../../shared/widgets/app_skeletons.dart';
|
|
|
+import '../../shared/widgets/attachment_download_helper.dart';
|
|
|
import '../../shared/widgets/action_bar.dart';
|
|
|
import '../../shared/widgets/status_banner.dart';
|
|
|
import 'expense_model.dart';
|
|
|
import '../../core/i18n/app_localizations.dart';
|
|
|
import '../../shared/models/bill_attachment.dart';
|
|
|
+import '../../shared/models/bill_file_rights.dart';
|
|
|
import '../../core/theme/app_colors.dart';
|
|
|
import '../../core/theme/app_colors_extension.dart';
|
|
|
import 'expense_api.dart';
|
|
|
@@ -31,11 +33,11 @@ class ExpenseDetailPage extends ConsumerStatefulWidget {
|
|
|
ConsumerState<ExpenseDetailPage> createState() => _ExpenseDetailPageState();
|
|
|
}
|
|
|
|
|
|
-class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
-{
|
|
|
+class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage> {
|
|
|
ExpenseModel? _expense;
|
|
|
List<BillAttachment> _attachments = [];
|
|
|
bool _attachAvailable = false;
|
|
|
+ BillFileRights _billFileRights = BillFileRights.none;
|
|
|
bool _isLoading = true;
|
|
|
String? _error;
|
|
|
Map<String, dynamic>? _billStatus;
|
|
|
@@ -64,13 +66,20 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
final expense = await api.fetchDetail(widget.billNo);
|
|
|
setState(() => _expense = expense);
|
|
|
|
|
|
- // 2. 加载附件(非致命)
|
|
|
+ // 2. 加载附件权限(非致命)
|
|
|
+ BillFileRights billFileRights = BillFileRights.none;
|
|
|
+ try {
|
|
|
+ billFileRights = await api.getBillFileRights('BX');
|
|
|
+ } catch (_) {}
|
|
|
+
|
|
|
+ // 3. 加载附件(非致命)
|
|
|
try {
|
|
|
_attachAvailable = await api.checkAttachHealth();
|
|
|
} catch (_) {
|
|
|
_attachAvailable = false;
|
|
|
}
|
|
|
- if (_attachAvailable) {
|
|
|
+ _billFileRights = billFileRights;
|
|
|
+ if (_attachAvailable && billFileRights.canBrowseAttachments) {
|
|
|
try {
|
|
|
_attachments = await api.getAttachments('BX', widget.billNo);
|
|
|
} catch (_) {
|
|
|
@@ -95,7 +104,9 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
} catch (e) {
|
|
|
setState(() => _error = e.toString());
|
|
|
} finally {
|
|
|
- setState(() { _isLoading = false; });
|
|
|
+ setState(() {
|
|
|
+ _isLoading = false;
|
|
|
+ });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -117,7 +128,10 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
const SizedBox(height: 12),
|
|
|
Text(
|
|
|
_error!,
|
|
|
- style: TextStyle(fontSize: AppFontSizes.body, color: colors.danger),
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.body,
|
|
|
+ color: colors.danger,
|
|
|
+ ),
|
|
|
textAlign: TextAlign.center,
|
|
|
),
|
|
|
const SizedBox(height: 16),
|
|
|
@@ -144,14 +158,23 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
if (_statusLoading) ...[
|
|
|
const Padding(
|
|
|
padding: EdgeInsets.symmetric(vertical: 8),
|
|
|
- child: Center(child: TDLoading(size: TDLoadingSize.small, icon: TDLoadingIcon.activity)),
|
|
|
+ child: Center(
|
|
|
+ child: TDLoading(
|
|
|
+ size: TDLoadingSize.small,
|
|
|
+ icon: TDLoadingIcon.activity,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
),
|
|
|
],
|
|
|
- Builder(builder: (ctx) {
|
|
|
- final badge = _buildStatusBadge(l10n);
|
|
|
- if (badge == null) return const SizedBox.shrink();
|
|
|
- return Column(children: [badge, const SizedBox(height: 16)]);
|
|
|
- }),
|
|
|
+ Builder(
|
|
|
+ builder: (ctx) {
|
|
|
+ final badge = _buildStatusBadge(l10n);
|
|
|
+ if (badge == null) return const SizedBox.shrink();
|
|
|
+ return Column(
|
|
|
+ children: [badge, const SizedBox(height: 16)],
|
|
|
+ );
|
|
|
+ },
|
|
|
+ ),
|
|
|
_buildBasicInfoSection(expense, l10n, colors),
|
|
|
const SizedBox(height: 16),
|
|
|
_buildExpenseDetailSection(expense, l10n, colors),
|
|
|
@@ -169,7 +192,9 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
showCenter: false,
|
|
|
rightLabel: l10n.get('editExpense'),
|
|
|
onRightTap: () async {
|
|
|
- final result = await GoRouter.of(context).push('/expense/edit/${widget.billNo}');
|
|
|
+ final result = await GoRouter.of(
|
|
|
+ context,
|
|
|
+ ).push('/expense/edit/${widget.billNo}');
|
|
|
if (result == true && mounted) _loadData();
|
|
|
},
|
|
|
),
|
|
|
@@ -184,20 +209,50 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
final approvalStatus = _billStatus?['approvalStatus'] as String?;
|
|
|
|
|
|
if (isClosed) {
|
|
|
- return StatusBanner(icon: Icons.lock_outline, statusText: l10n.get('statusClosed'), subText: '', color: Colors.blueGrey);
|
|
|
+ return StatusBanner(
|
|
|
+ icon: Icons.lock_outline,
|
|
|
+ statusText: l10n.get('statusClosed'),
|
|
|
+ subText: '',
|
|
|
+ color: Colors.blueGrey,
|
|
|
+ );
|
|
|
}
|
|
|
if (isTransferred) {
|
|
|
- return StatusBanner(icon: Icons.swap_horiz, statusText: l10n.get('statusTransferred'), subText: '', color: Colors.blueGrey);
|
|
|
+ return StatusBanner(
|
|
|
+ icon: Icons.swap_horiz,
|
|
|
+ statusText: l10n.get('statusTransferred'),
|
|
|
+ subText: '',
|
|
|
+ color: Colors.blueGrey,
|
|
|
+ );
|
|
|
}
|
|
|
switch (approvalStatus) {
|
|
|
case 'SHCOUNT4':
|
|
|
- return StatusBanner(icon: Icons.check_circle_outline, statusText: l10n.get('statusApproved'), subText: '', color: Colors.green);
|
|
|
+ return StatusBanner(
|
|
|
+ icon: Icons.check_circle_outline,
|
|
|
+ statusText: l10n.get('statusApproved'),
|
|
|
+ subText: '',
|
|
|
+ color: Colors.green,
|
|
|
+ );
|
|
|
case 'SHCOUNT5':
|
|
|
- return StatusBanner(icon: Icons.cancel_outlined, statusText: l10n.get('statusFinalRejected'), subText: '', color: Colors.red);
|
|
|
+ return StatusBanner(
|
|
|
+ icon: Icons.cancel_outlined,
|
|
|
+ statusText: l10n.get('statusFinalRejected'),
|
|
|
+ subText: '',
|
|
|
+ color: Colors.red,
|
|
|
+ );
|
|
|
case 'SHCOUNT1':
|
|
|
- return StatusBanner(icon: Icons.hourglass_empty, statusText: l10n.get('statusPendingReview'), subText: '', color: Colors.blue);
|
|
|
+ return StatusBanner(
|
|
|
+ icon: Icons.hourglass_empty,
|
|
|
+ statusText: l10n.get('statusPendingReview'),
|
|
|
+ subText: '',
|
|
|
+ color: Colors.blue,
|
|
|
+ );
|
|
|
case 'SHCOUNT2':
|
|
|
- return StatusBanner(icon: Icons.block_outlined, statusText: l10n.get('statusReviewRejected'), subText: '', color: Colors.deepOrange);
|
|
|
+ return StatusBanner(
|
|
|
+ icon: Icons.block_outlined,
|
|
|
+ statusText: l10n.get('statusReviewRejected'),
|
|
|
+ subText: '',
|
|
|
+ color: Colors.deepOrange,
|
|
|
+ );
|
|
|
default:
|
|
|
return null;
|
|
|
}
|
|
|
@@ -205,78 +260,102 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
|
|
|
// ═══ 基本信息 ═══
|
|
|
Widget _buildBasicInfoSection(
|
|
|
- ExpenseModel expense, AppLocalizations l10n, AppColorsExtension colors) {
|
|
|
+ ExpenseModel expense,
|
|
|
+ AppLocalizations l10n,
|
|
|
+ AppColorsExtension colors,
|
|
|
+ ) {
|
|
|
return FormSection(
|
|
|
title: l10n.get('basicInfo'),
|
|
|
leadingIcon: Icons.info_outline,
|
|
|
children: [
|
|
|
FormFieldRow(
|
|
|
- label: l10n.get('expenseNo'),
|
|
|
- value: expense.expenseNo,
|
|
|
- readOnly: true,
|
|
|
- showArrow: false),
|
|
|
+ label: l10n.get('expenseNo'),
|
|
|
+ value: expense.expenseNo,
|
|
|
+ readOnly: true,
|
|
|
+ showArrow: false,
|
|
|
+ ),
|
|
|
const SizedBox(height: 16),
|
|
|
FormFieldRow(
|
|
|
- label: l10n.get('date'),
|
|
|
- value: du.DateUtils.formatDate(expense.createTime),
|
|
|
- readOnly: true,
|
|
|
- showArrow: false),
|
|
|
+ label: l10n.get('date'),
|
|
|
+ value: du.DateUtils.formatDate(expense.createTime),
|
|
|
+ readOnly: true,
|
|
|
+ showArrow: false,
|
|
|
+ ),
|
|
|
const SizedBox(height: 16),
|
|
|
FormFieldRow(
|
|
|
- label: l10n.get('expensePersonnel'),
|
|
|
- value: expense.applicantName.isNotEmpty
|
|
|
- ? '${expense.applicantId}/${expense.applicantName}'
|
|
|
- : '-',
|
|
|
- readOnly: true,
|
|
|
- showArrow: false),
|
|
|
+ label: l10n.get('expensePersonnel'),
|
|
|
+ value: expense.applicantName.isNotEmpty
|
|
|
+ ? '${expense.applicantId}/${expense.applicantName}'
|
|
|
+ : '-',
|
|
|
+ readOnly: true,
|
|
|
+ showArrow: false,
|
|
|
+ ),
|
|
|
const SizedBox(height: 16),
|
|
|
FormFieldRow(
|
|
|
- label: l10n.get('expenseDept'),
|
|
|
- value: expense.deptName.isNotEmpty
|
|
|
- ? '${expense.deptId}/${expense.deptName}'
|
|
|
- : '-',
|
|
|
- readOnly: true,
|
|
|
- showArrow: false),
|
|
|
+ label: l10n.get('expenseDept'),
|
|
|
+ value: expense.deptName.isNotEmpty
|
|
|
+ ? '${expense.deptId}/${expense.deptName}'
|
|
|
+ : '-',
|
|
|
+ readOnly: true,
|
|
|
+ showArrow: false,
|
|
|
+ ),
|
|
|
const SizedBox(height: 16),
|
|
|
SizedBox(
|
|
|
height: 24,
|
|
|
- child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
|
|
|
- Text(l10n.get('expenseReason'),
|
|
|
- style: TextStyle(fontSize: AppFontSizes.subtitle, color: colors.textSecondary)),
|
|
|
- const SizedBox(width: 8),
|
|
|
- Expanded(
|
|
|
- child: Text(expense.purpose.isNotEmpty ? expense.purpose : '-',
|
|
|
+ child: Row(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
+ children: [
|
|
|
+ Text(
|
|
|
+ l10n.get('expenseReason'),
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.subtitle,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(width: 8),
|
|
|
+ Expanded(
|
|
|
+ child: Text(
|
|
|
+ expense.purpose.isNotEmpty ? expense.purpose : '-',
|
|
|
textAlign: TextAlign.end,
|
|
|
maxLines: 2,
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
- style: TextStyle(fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w500, color: colors.textPrimary)),
|
|
|
- ),
|
|
|
- ]),
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.subtitle,
|
|
|
+ fontWeight: FontWeight.w500,
|
|
|
+ color: colors.textPrimary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
),
|
|
|
const SizedBox(height: 16),
|
|
|
FormFieldRow(
|
|
|
- label: l10n.get('voucherNo'),
|
|
|
- value: expense.voucherNo.isNotEmpty ? expense.voucherNo : '-',
|
|
|
- readOnly: true,
|
|
|
- showArrow: false),
|
|
|
+ label: l10n.get('voucherNo'),
|
|
|
+ value: expense.voucherNo.isNotEmpty ? expense.voucherNo : '-',
|
|
|
+ readOnly: true,
|
|
|
+ showArrow: false,
|
|
|
+ ),
|
|
|
const SizedBox(height: 16),
|
|
|
FormFieldRow(
|
|
|
- label: l10n.get('currency'),
|
|
|
- value: expense.currencyCode.isNotEmpty ? expense.currencyCode : '-',
|
|
|
- readOnly: true,
|
|
|
- showArrow: false),
|
|
|
+ label: l10n.get('currency'),
|
|
|
+ value: expense.currencyCode.isNotEmpty ? expense.currencyCode : '-',
|
|
|
+ readOnly: true,
|
|
|
+ showArrow: false,
|
|
|
+ ),
|
|
|
const SizedBox(height: 16),
|
|
|
FormFieldRow(
|
|
|
- label: l10n.get('paymentMethod'),
|
|
|
- value: expense.paymentMethod.isNotEmpty ? expense.paymentMethod : '-',
|
|
|
- readOnly: true,
|
|
|
- showArrow: false),
|
|
|
+ label: l10n.get('paymentMethod'),
|
|
|
+ value: expense.paymentMethod.isNotEmpty ? expense.paymentMethod : '-',
|
|
|
+ readOnly: true,
|
|
|
+ showArrow: false,
|
|
|
+ ),
|
|
|
const SizedBox(height: 16),
|
|
|
FormFieldRow(
|
|
|
- label: l10n.get('remark'),
|
|
|
- value: expense.remark.isNotEmpty ? expense.remark : '-',
|
|
|
- readOnly: true,
|
|
|
- showArrow: false,
|
|
|
+ label: l10n.get('remark'),
|
|
|
+ value: expense.remark.isNotEmpty ? expense.remark : '-',
|
|
|
+ readOnly: true,
|
|
|
+ showArrow: false,
|
|
|
),
|
|
|
],
|
|
|
);
|
|
|
@@ -284,7 +363,10 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
|
|
|
// ═══ 费用明细 ═══
|
|
|
Widget _buildExpenseDetailSection(
|
|
|
- ExpenseModel expense, AppLocalizations l10n, AppColorsExtension colors) {
|
|
|
+ ExpenseModel expense,
|
|
|
+ AppLocalizations l10n,
|
|
|
+ AppColorsExtension colors,
|
|
|
+ ) {
|
|
|
final totalAmount = expense.details.fold<double>(
|
|
|
0,
|
|
|
(sum, d) => sum + d.totalAmount,
|
|
|
@@ -300,8 +382,13 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
if (expense.details.isEmpty)
|
|
|
Padding(
|
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
|
- child: Text(l10n.get('noDetailData'),
|
|
|
- style: TextStyle(fontSize: AppFontSizes.body, color: colors.textPlaceholder)),
|
|
|
+ child: Text(
|
|
|
+ l10n.get('noDetailData'),
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.body,
|
|
|
+ color: colors.textPlaceholder,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
)
|
|
|
else
|
|
|
...expense.details.asMap().entries.map((e) {
|
|
|
@@ -312,108 +399,237 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
return GestureDetector(
|
|
|
onTap: () => _showExpenseDetailDialog(context, d),
|
|
|
child: Container(
|
|
|
- margin: const EdgeInsets.symmetric(vertical: 6),
|
|
|
- padding: const EdgeInsets.all(12),
|
|
|
- decoration: BoxDecoration(color: colors.bgPage, borderRadius: BorderRadius.circular(8)),
|
|
|
- child: Row(
|
|
|
- children: [
|
|
|
- Expanded(
|
|
|
- child: Column(
|
|
|
- crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
- children: [
|
|
|
- Row(
|
|
|
- children: [
|
|
|
- Expanded(
|
|
|
- child: Text(title,
|
|
|
- style: TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w500, color: colors.textPrimary)),
|
|
|
+ margin: const EdgeInsets.symmetric(vertical: 6),
|
|
|
+ padding: const EdgeInsets.all(12),
|
|
|
+ decoration: BoxDecoration(
|
|
|
+ color: colors.bgPage,
|
|
|
+ borderRadius: BorderRadius.circular(8),
|
|
|
+ ),
|
|
|
+ child: Row(
|
|
|
+ children: [
|
|
|
+ Expanded(
|
|
|
+ child: Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
+ children: [
|
|
|
+ Row(
|
|
|
+ children: [
|
|
|
+ Expanded(
|
|
|
+ child: Text(
|
|
|
+ title,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.body,
|
|
|
+ fontWeight: FontWeight.w500,
|
|
|
+ color: colors.textPrimary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.end,
|
|
|
+ children: [
|
|
|
+ Text(
|
|
|
+ '¥${d.totalAmount.toStringAsFixed(2)}',
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.body,
|
|
|
+ fontWeight: FontWeight.w600,
|
|
|
+ color: colors.amountPrimary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (d.approvedAmount > 0)
|
|
|
+ Text(
|
|
|
+ '¥${d.approvedAmount.toStringAsFixed(2)}',
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.body,
|
|
|
+ fontWeight: FontWeight.w600,
|
|
|
+ color: colors.success,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 2),
|
|
|
+ Text(
|
|
|
+ '${l10n.get('amountExcludingTax')}: ¥${d.amount.toStringAsFixed(2)}',
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (d.taxAmount > 0)
|
|
|
+ Text(
|
|
|
+ '${l10n.get('taxAmount')}: ¥${d.taxAmount.toStringAsFixed(2)}',
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
),
|
|
|
- Column(
|
|
|
- crossAxisAlignment: CrossAxisAlignment.end,
|
|
|
- children: [
|
|
|
- Text('¥${d.totalAmount.toStringAsFixed(2)}',
|
|
|
- style: TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.amountPrimary)),
|
|
|
- if (d.approvedAmount > 0)
|
|
|
- Text('¥${d.approvedAmount.toStringAsFixed(2)}',
|
|
|
- style: TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.success)),
|
|
|
- ],
|
|
|
+ if (d.taxRate > 0)
|
|
|
+ Text(
|
|
|
+ '${l10n.get('taxRate')}: ${d.taxRate.toStringAsFixed(0)}%',
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
),
|
|
|
- ],
|
|
|
- ),
|
|
|
- const SizedBox(height: 2),
|
|
|
- Text('${l10n.get('amountExcludingTax')}: ¥${d.amount.toStringAsFixed(2)}',
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
|
|
|
- if (d.taxAmount > 0)
|
|
|
- Text('${l10n.get('taxAmount')}: ¥${d.taxAmount.toStringAsFixed(2)}',
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
|
|
|
- if (d.taxRate > 0)
|
|
|
- Text('${l10n.get('taxRate')}: ${d.taxRate.toStringAsFixed(0)}%',
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
|
|
|
- if (d.acctSubjectName.isNotEmpty)
|
|
|
- Text('${l10n.get('acctSubject')}: ${d.acctSubjectId}/${d.acctSubjectName}',
|
|
|
- maxLines: 1, overflow: TextOverflow.ellipsis,
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
|
|
|
- if (d.aeNo.isNotEmpty)
|
|
|
- Text('${l10n.get('expenseApplyNo')}: ${d.aeNo}',
|
|
|
- maxLines: 1, overflow: TextOverflow.ellipsis,
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
|
|
|
- if (d.aeDd.isNotEmpty)
|
|
|
- Text('${l10n.get('applyDate')}: ${d.aeDd.length >= 10 ? d.aeDd.substring(0, 10) : d.aeDd}',
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
|
|
|
- if (d.projectName.isNotEmpty)
|
|
|
- Text('${l10n.get('project')}: ${d.projectId}/${d.projectName}',
|
|
|
- maxLines: 1, overflow: TextOverflow.ellipsis,
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
|
|
|
- if (d.costDeptName.isNotEmpty)
|
|
|
- Text('${l10n.get('costDept')}: ${d.costDeptId}/${d.costDeptName}',
|
|
|
- maxLines: 1, overflow: TextOverflow.ellipsis,
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
|
|
|
- if (d.customerVendorName.isNotEmpty)
|
|
|
- Text('${l10n.get('customerVendor')}: ${d.customerVendorId}/${d.customerVendorName}',
|
|
|
- maxLines: 1, overflow: TextOverflow.ellipsis,
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
|
|
|
- if (d.sqManName.isNotEmpty)
|
|
|
- Text('${l10n.get('applicant')}: ${d.sqMan}/${d.sqManName}',
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
|
|
|
- if (d.bankAccountName.isNotEmpty)
|
|
|
- Text('${l10n.get('bankAccountName')}: ${d.bankAccountName}',
|
|
|
- maxLines: 1, overflow: TextOverflow.ellipsis,
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
|
|
|
- if (d.bankName.isNotEmpty)
|
|
|
- Text('${l10n.get('bankName')}: ${d.bankName}',
|
|
|
- maxLines: 1, overflow: TextOverflow.ellipsis,
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
|
|
|
- if (d.bankAccount.isNotEmpty)
|
|
|
- Text('${l10n.get('bankAccount')}: ${d.bankAccount}',
|
|
|
- maxLines: 1, overflow: TextOverflow.ellipsis,
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
|
|
|
- if (d.remark.isNotEmpty)
|
|
|
- Text('${l10n.get('remark')}: ${d.remark}',
|
|
|
- maxLines: 2, overflow: TextOverflow.ellipsis,
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, color: colors.textSecondary)),
|
|
|
- ],
|
|
|
+ if (d.acctSubjectName.isNotEmpty)
|
|
|
+ Text(
|
|
|
+ '${l10n.get('acctSubject')}: ${d.acctSubjectId}/${d.acctSubjectName}',
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (d.aeNo.isNotEmpty)
|
|
|
+ Text(
|
|
|
+ '${l10n.get('expenseApplyNo')}: ${d.aeNo}',
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (d.aeDd.isNotEmpty)
|
|
|
+ Text(
|
|
|
+ '${l10n.get('applyDate')}: ${d.aeDd.length >= 10 ? d.aeDd.substring(0, 10) : d.aeDd}',
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (d.projectName.isNotEmpty)
|
|
|
+ Text(
|
|
|
+ '${l10n.get('project')}: ${d.projectId}/${d.projectName}',
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (d.costDeptName.isNotEmpty)
|
|
|
+ Text(
|
|
|
+ '${l10n.get('costDept')}: ${d.costDeptId}/${d.costDeptName}',
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (d.customerVendorName.isNotEmpty)
|
|
|
+ Text(
|
|
|
+ '${l10n.get('customerVendor')}: ${d.customerVendorId}/${d.customerVendorName}',
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (d.sqManName.isNotEmpty)
|
|
|
+ Text(
|
|
|
+ '${l10n.get('applicant')}: ${d.sqMan}/${d.sqManName}',
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (d.bankAccountName.isNotEmpty)
|
|
|
+ Text(
|
|
|
+ '${l10n.get('bankAccountName')}: ${d.bankAccountName}',
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (d.bankName.isNotEmpty)
|
|
|
+ Text(
|
|
|
+ '${l10n.get('bankName')}: ${d.bankName}',
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (d.bankAccount.isNotEmpty)
|
|
|
+ Text(
|
|
|
+ '${l10n.get('bankAccount')}: ${d.bankAccount}',
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (d.remark.isNotEmpty)
|
|
|
+ Text(
|
|
|
+ '${l10n.get('remark')}: ${d.remark}',
|
|
|
+ maxLines: 2,
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
),
|
|
|
- ),
|
|
|
- ],
|
|
|
- ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
),
|
|
|
);
|
|
|
}),
|
|
|
if (expense.details.isNotEmpty) ...[
|
|
|
const SizedBox(height: 8),
|
|
|
- Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
|
|
- Text(l10n.get('total'),
|
|
|
- style: TextStyle(fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.textPrimary)),
|
|
|
- Column(
|
|
|
- crossAxisAlignment: CrossAxisAlignment.end,
|
|
|
- children: [
|
|
|
- Text('¥${totalAmount.toStringAsFixed(2)}',
|
|
|
- style: TextStyle(fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w700, color: colors.amountPrimary)),
|
|
|
- if (totalApproved > 0)
|
|
|
- Text('${l10n.get('approvedAmount')} ¥${totalApproved.toStringAsFixed(2)}',
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, fontWeight: FontWeight.w600, color: colors.success)),
|
|
|
- ],
|
|
|
- ),
|
|
|
- ]),
|
|
|
+ Row(
|
|
|
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
+ children: [
|
|
|
+ Text(
|
|
|
+ l10n.get('totalExpense'),
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.body,
|
|
|
+ fontWeight: FontWeight.w600,
|
|
|
+ color: colors.textPrimary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ Text(
|
|
|
+ '¥${totalAmount.toStringAsFixed(2)}',
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.subtitle,
|
|
|
+ fontWeight: FontWeight.w700,
|
|
|
+ color: colors.amountPrimary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 4),
|
|
|
+ Row(
|
|
|
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
+ children: [
|
|
|
+ Text(
|
|
|
+ l10n.get('approvedTotal'),
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.body,
|
|
|
+ fontWeight: FontWeight.w600,
|
|
|
+ color: colors.textPrimary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ Text(
|
|
|
+ '¥${totalApproved.toStringAsFixed(2)}',
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.subtitle,
|
|
|
+ fontWeight: FontWeight.w700,
|
|
|
+ color: totalApproved > 0 ? colors.success : colors.textPrimary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
],
|
|
|
],
|
|
|
);
|
|
|
@@ -424,13 +640,38 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
}
|
|
|
|
|
|
// ═══ 附件 ═══
|
|
|
- Widget _buildAttachmentSection(AppLocalizations l10n, AppColorsExtension colors) {
|
|
|
+ Widget _buildAttachmentSection(
|
|
|
+ AppLocalizations l10n,
|
|
|
+ AppColorsExtension colors,
|
|
|
+ ) {
|
|
|
if (!_attachAvailable) {
|
|
|
return FormSection(
|
|
|
title: l10n.get('attachments'),
|
|
|
leadingIcon: Icons.attach_file_outlined,
|
|
|
- children: [Text(l10n.get('attachServiceUnavailable'),
|
|
|
- style: TextStyle(fontSize: AppFontSizes.body, color: colors.textPlaceholder))],
|
|
|
+ children: [
|
|
|
+ Text(
|
|
|
+ l10n.get('attachServiceUnavailable'),
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.body,
|
|
|
+ color: colors.textPlaceholder,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ }
|
|
|
+ if (!_billFileRights.canBrowseAttachments) {
|
|
|
+ return FormSection(
|
|
|
+ title: l10n.get('attachments'),
|
|
|
+ leadingIcon: Icons.attach_file_outlined,
|
|
|
+ children: [
|
|
|
+ Text(
|
|
|
+ l10n.get('noAttachmentPermission'),
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.body,
|
|
|
+ color: colors.textPlaceholder,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
);
|
|
|
}
|
|
|
|
|
|
@@ -442,16 +683,31 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
|
|
|
final children = <Widget>[];
|
|
|
if (_attachments.isEmpty) {
|
|
|
- children.add(Text(l10n.get('noAttachment'),
|
|
|
- style: TextStyle(fontSize: AppFontSizes.body, color: colors.textPlaceholder)));
|
|
|
+ children.add(
|
|
|
+ Text(
|
|
|
+ l10n.get('noAttachment'),
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.body,
|
|
|
+ color: colors.textPlaceholder,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ );
|
|
|
} else {
|
|
|
// 表头附件
|
|
|
if (headerAtts.isNotEmpty) {
|
|
|
- children.add(Padding(
|
|
|
- padding: const EdgeInsets.only(bottom: 8),
|
|
|
- child: Text(l10n.get('headerAttachments'),
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, fontWeight: FontWeight.w600, color: colors.textSecondary)),
|
|
|
- ));
|
|
|
+ children.add(
|
|
|
+ Padding(
|
|
|
+ padding: const EdgeInsets.only(bottom: 8),
|
|
|
+ child: Text(
|
|
|
+ l10n.get('headerAttachments'),
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ fontWeight: FontWeight.w600,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ );
|
|
|
for (final a in headerAtts) {
|
|
|
children.add(_buildAttachmentRow(a, colors));
|
|
|
}
|
|
|
@@ -459,11 +715,19 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
// 表身附件(按明细行分组)
|
|
|
for (final entry in bodyGroups.entries) {
|
|
|
children.add(const SizedBox(height: 8));
|
|
|
- children.add(Padding(
|
|
|
- padding: const EdgeInsets.only(bottom: 8),
|
|
|
- child: Text('${l10n.get('detailLine')} ${entry.key}',
|
|
|
- style: TextStyle(fontSize: AppFontSizes.caption, fontWeight: FontWeight.w600, color: colors.textSecondary)),
|
|
|
- ));
|
|
|
+ children.add(
|
|
|
+ Padding(
|
|
|
+ padding: const EdgeInsets.only(bottom: 8),
|
|
|
+ child: Text(
|
|
|
+ '${l10n.get('detailLine')} ${entry.key}',
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ fontWeight: FontWeight.w600,
|
|
|
+ color: colors.textSecondary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ );
|
|
|
for (final a in entry.value) {
|
|
|
children.add(_buildAttachmentRow(a, colors));
|
|
|
}
|
|
|
@@ -478,7 +742,14 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
}
|
|
|
|
|
|
Widget _buildAttachmentRow(BillAttachment a, AppColorsExtension colors) {
|
|
|
- final isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].contains(a.ext.toLowerCase());
|
|
|
+ final isImage = [
|
|
|
+ 'jpg',
|
|
|
+ 'jpeg',
|
|
|
+ 'png',
|
|
|
+ 'gif',
|
|
|
+ 'bmp',
|
|
|
+ 'webp',
|
|
|
+ ].contains(a.ext.toLowerCase());
|
|
|
return GestureDetector(
|
|
|
onTap: () => _openAttachment(a),
|
|
|
child: Container(
|
|
|
@@ -488,18 +759,43 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
color: colors.bgPage,
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
),
|
|
|
- child: Row(children: [
|
|
|
- if (isImage)
|
|
|
- _ExpAttachmentThumbnail(api: ref.read(expenseApiProvider), attachment: a, size: 40)
|
|
|
- else
|
|
|
- Icon(_fileTypeIcon(a.ext), size: 40, color: colors.primary),
|
|
|
- const SizedBox(width: 10),
|
|
|
- Expanded(
|
|
|
- child: Text(a.fileName,
|
|
|
- maxLines: 1, overflow: TextOverflow.ellipsis,
|
|
|
- style: TextStyle(fontSize: AppFontSizes.body, color: colors.textPrimary)),
|
|
|
- ),
|
|
|
- ]),
|
|
|
+ child: Row(
|
|
|
+ children: [
|
|
|
+ if (isImage)
|
|
|
+ _ExpAttachmentThumbnail(
|
|
|
+ api: ref.read(expenseApiProvider),
|
|
|
+ attachment: a,
|
|
|
+ size: 40,
|
|
|
+ )
|
|
|
+ else
|
|
|
+ Icon(_fileTypeIcon(a.ext), size: 40, color: colors.primary),
|
|
|
+ const SizedBox(width: 10),
|
|
|
+ Expanded(
|
|
|
+ child: Text(
|
|
|
+ a.fileName,
|
|
|
+ maxLines: 1,
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.body,
|
|
|
+ color: colors.textPrimary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(width: 8),
|
|
|
+ GestureDetector(
|
|
|
+ onTap: () => AttachmentDownloadHelper.downloadAndSave(
|
|
|
+ context,
|
|
|
+ a,
|
|
|
+ ref.read(expenseApiProvider).downloadAttachment,
|
|
|
+ ),
|
|
|
+ child: Icon(
|
|
|
+ Icons.download_outlined,
|
|
|
+ size: 22,
|
|
|
+ color: colors.primary,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
@@ -512,7 +808,8 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
if (isImage) {
|
|
|
// 图片 → 弹窗预览,内部自动下载并显示 loading
|
|
|
final api = ref.read(expenseApiProvider);
|
|
|
- AttachmentPreview.show(context,
|
|
|
+ AttachmentPreview.show(
|
|
|
+ context,
|
|
|
loader: api.downloadAttachment(a.id),
|
|
|
fileName: a.fileName,
|
|
|
loadingText: l10n.get('loading'),
|
|
|
@@ -543,11 +840,22 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
|
|
|
IconData _fileTypeIcon(String ext) {
|
|
|
switch (ext.toLowerCase()) {
|
|
|
- case 'pdf': return Icons.picture_as_pdf;
|
|
|
- case 'doc': case 'docx': return Icons.description;
|
|
|
- case 'xls': case 'xlsx': return Icons.table_chart;
|
|
|
- case 'jpg': case 'jpeg': case 'png': case 'gif': case 'bmp': return Icons.image_outlined;
|
|
|
- default: return Icons.insert_drive_file;
|
|
|
+ case 'pdf':
|
|
|
+ return Icons.picture_as_pdf;
|
|
|
+ case 'doc':
|
|
|
+ case 'docx':
|
|
|
+ return Icons.description;
|
|
|
+ case 'xls':
|
|
|
+ case 'xlsx':
|
|
|
+ return Icons.table_chart;
|
|
|
+ case 'jpg':
|
|
|
+ case 'jpeg':
|
|
|
+ case 'png':
|
|
|
+ case 'gif':
|
|
|
+ case 'bmp':
|
|
|
+ return Icons.image_outlined;
|
|
|
+ default:
|
|
|
+ return Icons.insert_drive_file;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -559,10 +867,19 @@ class _ExpenseDetailPageState extends ConsumerState<ExpenseDetailPage>
|
|
|
child: Row(
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
children: [
|
|
|
- Icon(Icons.rocket_launch_outlined, size: 16, color: colors.textPlaceholder),
|
|
|
+ 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)),
|
|
|
+ Text(
|
|
|
+ l10n.get('pageFooter'),
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: colors.textPlaceholder,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
],
|
|
|
),
|
|
|
),
|
|
|
@@ -575,9 +892,14 @@ class _ExpAttachmentThumbnail extends StatefulWidget {
|
|
|
final ExpenseApi api;
|
|
|
final BillAttachment attachment;
|
|
|
final double size;
|
|
|
- const _ExpAttachmentThumbnail({required this.api, required this.attachment, required this.size});
|
|
|
+ const _ExpAttachmentThumbnail({
|
|
|
+ required this.api,
|
|
|
+ required this.attachment,
|
|
|
+ required this.size,
|
|
|
+ });
|
|
|
@override
|
|
|
- State<_ExpAttachmentThumbnail> createState() => _ExpAttachmentThumbnailState();
|
|
|
+ State<_ExpAttachmentThumbnail> createState() =>
|
|
|
+ _ExpAttachmentThumbnailState();
|
|
|
}
|
|
|
|
|
|
class _ExpAttachmentThumbnailState extends State<_ExpAttachmentThumbnail> {
|
|
|
@@ -593,7 +915,11 @@ class _ExpAttachmentThumbnailState extends State<_ExpAttachmentThumbnail> {
|
|
|
Future<void> _load() async {
|
|
|
try {
|
|
|
final bytes = await widget.api.downloadAttachment(widget.attachment.id);
|
|
|
- if (mounted) setState(() { _bytes = bytes; _loading = false; });
|
|
|
+ if (mounted)
|
|
|
+ setState(() {
|
|
|
+ _bytes = bytes;
|
|
|
+ _loading = false;
|
|
|
+ });
|
|
|
} catch (_) {
|
|
|
if (mounted) setState(() => _loading = false);
|
|
|
}
|
|
|
@@ -602,15 +928,33 @@ class _ExpAttachmentThumbnailState extends State<_ExpAttachmentThumbnail> {
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
if (_loading) {
|
|
|
- return SizedBox(width: widget.size, height: widget.size,
|
|
|
- child: const Center(child: SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))));
|
|
|
+ return SizedBox(
|
|
|
+ width: widget.size,
|
|
|
+ height: widget.size,
|
|
|
+ child: const Center(
|
|
|
+ child: SizedBox(
|
|
|
+ width: 16,
|
|
|
+ height: 16,
|
|
|
+ child: CircularProgressIndicator(strokeWidth: 2),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ );
|
|
|
}
|
|
|
if (_bytes != null) {
|
|
|
return ClipRRect(
|
|
|
borderRadius: BorderRadius.circular(4),
|
|
|
- child: Image.memory(_bytes!, width: widget.size, height: widget.size, fit: BoxFit.cover),
|
|
|
+ child: Image.memory(
|
|
|
+ _bytes!,
|
|
|
+ width: widget.size,
|
|
|
+ height: widget.size,
|
|
|
+ fit: BoxFit.cover,
|
|
|
+ ),
|
|
|
);
|
|
|
}
|
|
|
- return Icon(Icons.broken_image, size: widget.size * 0.6, color: Colors.grey);
|
|
|
+ return Icon(
|
|
|
+ Icons.broken_image,
|
|
|
+ size: widget.size * 0.6,
|
|
|
+ color: Colors.grey,
|
|
|
+ );
|
|
|
}
|
|
|
}
|