|
|
@@ -52,6 +52,11 @@ class HomePage extends ConsumerWidget {
|
|
|
child: Column(
|
|
|
children: [
|
|
|
_buildBanner(ref, l10n),
|
|
|
+ // 经理版:待审批红色角标卡片置顶
|
|
|
+ if (summary.userRole == 'manager') ...[
|
|
|
+ const SizedBox(height: AppSpacing.md),
|
|
|
+ _buildPendingApprovalCard(context, summary, l10n),
|
|
|
+ ],
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
_buildInitiateGrid(context, summary, l10n),
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
@@ -129,14 +134,14 @@ class HomePage extends ConsumerWidget {
|
|
|
onTap: () => context.push('/vehicle/apply'),
|
|
|
),
|
|
|
_GridItem(
|
|
|
- icon: Icons.access_time_outlined,
|
|
|
+ icon: Icons.more_time_outlined,
|
|
|
label: l10n.get('overtimeApplication'),
|
|
|
onTap: () => context.push('/overtime/apply'),
|
|
|
),
|
|
|
// 管理员额外显示"发布公告"
|
|
|
if (summary.userRole == 'admin')
|
|
|
_GridItem(
|
|
|
- icon: Icons.campaign_outlined,
|
|
|
+ icon: Icons.add_alert_outlined,
|
|
|
label: l10n.get('publishAnnouncement'),
|
|
|
onTap: () => context.push('/announcement/create'),
|
|
|
),
|
|
|
@@ -160,31 +165,43 @@ class HomePage extends ConsumerWidget {
|
|
|
icon: Icons.description_outlined,
|
|
|
label: l10n.get('applicationRecords'),
|
|
|
onTap: () => context.push('/expense-apply/list'),
|
|
|
+ iconColor: colors.infoText,
|
|
|
+ bgColor: colors.infoLightBg,
|
|
|
),
|
|
|
_GridItem(
|
|
|
icon: Icons.receipt_outlined,
|
|
|
label: l10n.get('expenseRecords'),
|
|
|
onTap: () => context.push('/expense/list'),
|
|
|
+ iconColor: colors.infoText,
|
|
|
+ bgColor: colors.infoLightBg,
|
|
|
),
|
|
|
_GridItem(
|
|
|
- icon: Icons.access_time_outlined,
|
|
|
+ icon: Icons.schedule_outlined,
|
|
|
label: l10n.get('overtimeRecords'),
|
|
|
onTap: () => context.push('/overtime/list'),
|
|
|
+ iconColor: colors.infoText,
|
|
|
+ bgColor: colors.infoLightBg,
|
|
|
),
|
|
|
_GridItem(
|
|
|
- icon: Icons.directions_car_outlined,
|
|
|
+ icon: Icons.local_taxi_outlined,
|
|
|
label: l10n.get('vehicleRecords'),
|
|
|
onTap: () => context.push('/vehicle/list'),
|
|
|
+ iconColor: colors.infoText,
|
|
|
+ bgColor: colors.infoLightBg,
|
|
|
),
|
|
|
_GridItem(
|
|
|
icon: Icons.edit_note_outlined,
|
|
|
label: l10n.get('outingLogs'),
|
|
|
onTap: () => context.push('/outing-log/list'),
|
|
|
+ iconColor: colors.infoText,
|
|
|
+ bgColor: colors.infoLightBg,
|
|
|
),
|
|
|
_GridItem(
|
|
|
icon: Icons.campaign_outlined,
|
|
|
label: l10n.get('companyAnnouncements'),
|
|
|
onTap: () => context.push('/announcement/list'),
|
|
|
+ iconColor: colors.infoText,
|
|
|
+ bgColor: colors.infoLightBg,
|
|
|
),
|
|
|
];
|
|
|
|
|
|
@@ -206,26 +223,36 @@ class HomePage extends ConsumerWidget {
|
|
|
icon: Icons.bar_chart_outlined,
|
|
|
label: l10n.get('reportExpenseApply'),
|
|
|
onTap: () => context.push('/report/expense-apply-detail'),
|
|
|
+ iconColor: colors.primary700,
|
|
|
+ bgColor: colors.primary50,
|
|
|
),
|
|
|
_GridItem(
|
|
|
icon: Icons.pie_chart_outline,
|
|
|
label: l10n.get('reportExpense'),
|
|
|
onTap: () => context.push('/report/expense-detail'),
|
|
|
+ iconColor: colors.primary700,
|
|
|
+ bgColor: colors.primary50,
|
|
|
),
|
|
|
_GridItem(
|
|
|
icon: Icons.query_stats_outlined,
|
|
|
label: l10n.get('reportOvertime'),
|
|
|
onTap: () => context.push('/report/overtime-detail'),
|
|
|
+ iconColor: colors.primary700,
|
|
|
+ bgColor: colors.primary50,
|
|
|
),
|
|
|
_GridItem(
|
|
|
icon: Icons.map_outlined,
|
|
|
label: l10n.get('reportVehicle'),
|
|
|
onTap: () => context.push('/report/vehicle-detail'),
|
|
|
+ iconColor: colors.primary700,
|
|
|
+ bgColor: colors.primary50,
|
|
|
),
|
|
|
_GridItem(
|
|
|
icon: Icons.explore_outlined,
|
|
|
label: l10n.get('reportOutingLog'),
|
|
|
onTap: () => context.push('/report/outing-log-detail'),
|
|
|
+ iconColor: colors.primary700,
|
|
|
+ bgColor: colors.primary50,
|
|
|
),
|
|
|
];
|
|
|
|
|
|
@@ -277,10 +304,14 @@ class HomePage extends ConsumerWidget {
|
|
|
width: 36,
|
|
|
height: 36,
|
|
|
decoration: BoxDecoration(
|
|
|
- color: colors.primaryLight,
|
|
|
+ color: item.bgColor ?? colors.primary50,
|
|
|
borderRadius: BorderRadius.circular(10),
|
|
|
),
|
|
|
- child: Icon(item.icon, size: 22, color: colors.primary),
|
|
|
+ child: Icon(
|
|
|
+ item.icon,
|
|
|
+ size: 22,
|
|
|
+ color: item.iconColor ?? colors.primary,
|
|
|
+ ),
|
|
|
),
|
|
|
const SizedBox(height: AppSpacing.xs),
|
|
|
Text(
|
|
|
@@ -350,7 +381,8 @@ class HomePage extends ConsumerWidget {
|
|
|
Expanded(
|
|
|
child: _buildStatCard(
|
|
|
title: l10n.get('monthlySubmitted'),
|
|
|
- value: '${summary.monthlySubmittedCount} 笔',
|
|
|
+ value:
|
|
|
+ '${summary.monthlySubmittedCount} ${l10n.get('unitItem')}',
|
|
|
valueColor: colors.textPrimary,
|
|
|
colors: colors,
|
|
|
onTap: () => context.push('/expense-apply/list'),
|
|
|
@@ -363,120 +395,115 @@ class HomePage extends ConsumerWidget {
|
|
|
}
|
|
|
|
|
|
// ===================================================================
|
|
|
- // 经理版:待审批角标 + 部门快捷看板(3 卡片)
|
|
|
+ // 经理版:待审批红色角标卡片
|
|
|
// ===================================================================
|
|
|
|
|
|
- Widget _buildManagerDashboard(
|
|
|
+ Widget _buildPendingApprovalCard(
|
|
|
BuildContext context,
|
|
|
HomeSummary summary,
|
|
|
AppLocalizations l10n,
|
|
|
) {
|
|
|
final colors = Theme.of(context).extension<AppColorsExtension>()!;
|
|
|
- return Column(
|
|
|
- children: [
|
|
|
- // 待审批卡片(红色角标)
|
|
|
- GestureDetector(
|
|
|
- onTap: () => context.push('/messages'),
|
|
|
- child: Container(
|
|
|
- width: double.infinity,
|
|
|
- padding: const EdgeInsets.all(AppSpacing.md),
|
|
|
- decoration: BoxDecoration(
|
|
|
- color: colors.bgCard,
|
|
|
- borderRadius: BorderRadius.circular(8),
|
|
|
+ return GestureDetector(
|
|
|
+ onTap: () => context.push('/messages'),
|
|
|
+ child: Container(
|
|
|
+ width: double.infinity,
|
|
|
+ padding: const EdgeInsets.all(AppSpacing.md),
|
|
|
+ decoration: BoxDecoration(
|
|
|
+ color: colors.bgCard,
|
|
|
+ borderRadius: BorderRadius.circular(8),
|
|
|
+ ),
|
|
|
+ child: Row(
|
|
|
+ children: [
|
|
|
+ Container(
|
|
|
+ width: 40,
|
|
|
+ height: 40,
|
|
|
+ decoration: BoxDecoration(
|
|
|
+ color: colors.dangerBg,
|
|
|
+ borderRadius: BorderRadius.circular(8),
|
|
|
+ ),
|
|
|
+ child: Icon(Icons.task_alt, color: colors.danger, size: 24),
|
|
|
),
|
|
|
- child: Row(
|
|
|
- children: [
|
|
|
- Container(
|
|
|
- width: 40,
|
|
|
- height: 40,
|
|
|
- decoration: BoxDecoration(
|
|
|
- color: colors.dangerBg,
|
|
|
- borderRadius: BorderRadius.circular(8),
|
|
|
- ),
|
|
|
- child: Icon(Icons.task_alt, color: colors.danger, size: 24),
|
|
|
+ const SizedBox(width: AppSpacing.sm),
|
|
|
+ Expanded(
|
|
|
+ child: Text(
|
|
|
+ l10n.get('pendingApproval'),
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: AppFontSizes.subtitle,
|
|
|
+ fontWeight: FontWeight.w600,
|
|
|
+ color: colors.textPrimary,
|
|
|
),
|
|
|
- const SizedBox(width: AppSpacing.sm),
|
|
|
- Expanded(
|
|
|
- child: Text(
|
|
|
- l10n.get('pendingApproval'),
|
|
|
- style: TextStyle(
|
|
|
- fontSize: AppFontSizes.subtitle,
|
|
|
- fontWeight: FontWeight.w600,
|
|
|
- color: colors.textPrimary,
|
|
|
- ),
|
|
|
- ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (summary.pendingApprovalCount > 0)
|
|
|
+ Container(
|
|
|
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
|
+ decoration: BoxDecoration(
|
|
|
+ color: colors.danger,
|
|
|
+ borderRadius: BorderRadius.circular(10),
|
|
|
),
|
|
|
- if (summary.pendingApprovalCount > 0)
|
|
|
- Container(
|
|
|
- padding: const EdgeInsets.symmetric(
|
|
|
- horizontal: 8,
|
|
|
- vertical: 2,
|
|
|
- ),
|
|
|
- decoration: BoxDecoration(
|
|
|
- color: colors.danger,
|
|
|
- borderRadius: BorderRadius.circular(10),
|
|
|
- ),
|
|
|
- child: Text(
|
|
|
- '${summary.pendingApprovalCount}',
|
|
|
- style: const TextStyle(
|
|
|
- fontSize: AppFontSizes.caption,
|
|
|
- color: Colors.white,
|
|
|
- fontWeight: FontWeight.w600,
|
|
|
- ),
|
|
|
- ),
|
|
|
+ child: Text(
|
|
|
+ '${summary.pendingApprovalCount}',
|
|
|
+ style: const TextStyle(
|
|
|
+ fontSize: AppFontSizes.caption,
|
|
|
+ color: Colors.white,
|
|
|
+ fontWeight: FontWeight.w600,
|
|
|
),
|
|
|
- const SizedBox(width: AppSpacing.xs),
|
|
|
- Icon(
|
|
|
- Icons.chevron_right,
|
|
|
- color: colors.textPlaceholder,
|
|
|
- size: 20,
|
|
|
),
|
|
|
- ],
|
|
|
- ),
|
|
|
- ),
|
|
|
+ ),
|
|
|
+ const SizedBox(width: AppSpacing.xs),
|
|
|
+ Icon(Icons.chevron_right, color: colors.textPlaceholder, size: 20),
|
|
|
+ ],
|
|
|
),
|
|
|
- const SizedBox(height: AppSpacing.md),
|
|
|
- // 部门快捷看板(3 卡片)
|
|
|
- SectionCard(
|
|
|
- title: l10n.get('deptDashboard'),
|
|
|
- showAction: false,
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ // ===================================================================
|
|
|
+ // 经理版:部门快捷看板
|
|
|
+ // ===================================================================
|
|
|
+
|
|
|
+ Widget _buildManagerDashboard(
|
|
|
+ BuildContext context,
|
|
|
+ HomeSummary summary,
|
|
|
+ AppLocalizations l10n,
|
|
|
+ ) {
|
|
|
+ final colors = Theme.of(context).extension<AppColorsExtension>()!;
|
|
|
+ return SectionCard(
|
|
|
+ title: l10n.get('deptDashboard'),
|
|
|
+ showAction: false,
|
|
|
+ children: [
|
|
|
+ Row(
|
|
|
children: [
|
|
|
- Row(
|
|
|
- children: [
|
|
|
- Expanded(
|
|
|
- child: _buildStatCard(
|
|
|
- title: l10n.get('deptMonthlyReimbursement'),
|
|
|
- value:
|
|
|
- '¥${_formatAmount(summary.deptMonthlyReimbursement)}',
|
|
|
- valueColor: colors.amountPrimary,
|
|
|
- colors: colors,
|
|
|
- onTap: () => context.push('/expense/list'),
|
|
|
- ),
|
|
|
- ),
|
|
|
- const SizedBox(width: AppSpacing.sm),
|
|
|
- Expanded(
|
|
|
- child: _buildStatCard(
|
|
|
- title: l10n.get('deptMonthlySubmitted'),
|
|
|
- value: '${summary.deptMonthlySubmittedCount} 笔',
|
|
|
- valueColor: colors.textPrimary,
|
|
|
- colors: colors,
|
|
|
- onTap: () => context.push('/expense-apply/list'),
|
|
|
- ),
|
|
|
- ),
|
|
|
- const SizedBox(width: AppSpacing.sm),
|
|
|
- Expanded(
|
|
|
- child: _buildStatCard(
|
|
|
- title: l10n.get('deptPendingDocuments'),
|
|
|
- value: '${summary.deptPendingDocuments} 笔',
|
|
|
- valueColor: colors.warning,
|
|
|
- colors: colors,
|
|
|
- onTap: () => context.push('/expense-apply/list'),
|
|
|
- ),
|
|
|
- ),
|
|
|
- ],
|
|
|
+ Expanded(
|
|
|
+ child: _buildStatCard(
|
|
|
+ title: l10n.get('deptMonthlyReimbursement'),
|
|
|
+ value: '¥${_formatAmount(summary.deptMonthlyReimbursement)}',
|
|
|
+ valueColor: colors.amountPrimary,
|
|
|
+ colors: colors,
|
|
|
+ onTap: () => context.push('/expense/list'),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(width: AppSpacing.sm),
|
|
|
+ Expanded(
|
|
|
+ child: _buildStatCard(
|
|
|
+ title: l10n.get('deptMonthlySubmitted'),
|
|
|
+ value:
|
|
|
+ '${summary.deptMonthlySubmittedCount} ${l10n.get('unitItem')}',
|
|
|
+ valueColor: colors.textPrimary,
|
|
|
+ colors: colors,
|
|
|
+ onTap: () => context.push('/expense-apply/list'),
|
|
|
+ ),
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
+ const SizedBox(height: AppSpacing.sm),
|
|
|
+ _buildStatCard(
|
|
|
+ title: l10n.get('deptPendingDocuments'),
|
|
|
+ value: '${summary.deptPendingDocuments} ${l10n.get('unitItem')}',
|
|
|
+ valueColor: colors.warning,
|
|
|
+ colors: colors,
|
|
|
+ onTap: () => context.push('/expense-apply/list'),
|
|
|
+ ),
|
|
|
],
|
|
|
);
|
|
|
}
|
|
|
@@ -501,7 +528,7 @@ class HomePage extends ConsumerWidget {
|
|
|
child: _buildStatCard(
|
|
|
title: l10n.get('paidTotal'),
|
|
|
value: '¥${_formatAmount(summary.paidTotal)}',
|
|
|
- valueColor: colors.success,
|
|
|
+ valueColor: colors.amountPrimary,
|
|
|
colors: colors,
|
|
|
valueFontSize: 18,
|
|
|
onTap: () => context.push('/expense/list'),
|
|
|
@@ -518,19 +545,17 @@ class HomePage extends ConsumerWidget {
|
|
|
onTap: () => context.push('/expense/list'),
|
|
|
),
|
|
|
),
|
|
|
- const SizedBox(width: AppSpacing.sm),
|
|
|
- Expanded(
|
|
|
- child: _buildStatCard(
|
|
|
- title: l10n.get('abnormalReturns'),
|
|
|
- value: '¥${_formatAmount(summary.abnormalReturns)}',
|
|
|
- valueColor: colors.danger,
|
|
|
- colors: colors,
|
|
|
- valueFontSize: 18,
|
|
|
- onTap: () => context.push('/expense/list'),
|
|
|
- ),
|
|
|
- ),
|
|
|
],
|
|
|
),
|
|
|
+ const SizedBox(height: AppSpacing.sm),
|
|
|
+ _buildStatCard(
|
|
|
+ title: l10n.get('abnormalReturns'),
|
|
|
+ value: '¥${_formatAmount(summary.abnormalReturns)}',
|
|
|
+ valueColor: colors.danger,
|
|
|
+ colors: colors,
|
|
|
+ valueFontSize: 18,
|
|
|
+ onTap: () => context.push('/expense/list'),
|
|
|
+ ),
|
|
|
],
|
|
|
);
|
|
|
}
|
|
|
@@ -557,20 +582,24 @@ class HomePage extends ConsumerWidget {
|
|
|
decoration: BoxDecoration(
|
|
|
color: colors.bgPage,
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
+ border: Border(left: BorderSide(color: valueColor, width: 3)),
|
|
|
),
|
|
|
child: Column(
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
children: [
|
|
|
- Text(
|
|
|
- value,
|
|
|
- style: TextStyle(
|
|
|
- fontSize: valueFontSize,
|
|
|
- fontWeight: FontWeight.w700,
|
|
|
- color: valueColor,
|
|
|
+ FittedBox(
|
|
|
+ fit: BoxFit.scaleDown,
|
|
|
+ alignment: Alignment.centerLeft,
|
|
|
+ child: Text(
|
|
|
+ value,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: valueFontSize,
|
|
|
+ fontWeight: FontWeight.w700,
|
|
|
+ color: valueColor,
|
|
|
+ ),
|
|
|
+ textAlign: TextAlign.left,
|
|
|
),
|
|
|
- textAlign: TextAlign.center,
|
|
|
- maxLines: 1,
|
|
|
- overflow: TextOverflow.ellipsis,
|
|
|
),
|
|
|
const SizedBox(height: AppSpacing.xs),
|
|
|
Text(
|
|
|
@@ -579,7 +608,8 @@ class HomePage extends ConsumerWidget {
|
|
|
fontSize: AppFontSizes.caption,
|
|
|
color: colors.textSecondary,
|
|
|
),
|
|
|
- textAlign: TextAlign.center,
|
|
|
+ maxLines: 2,
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
),
|
|
|
],
|
|
|
),
|
|
|
@@ -600,10 +630,14 @@ class _GridItem {
|
|
|
final IconData icon;
|
|
|
final String label;
|
|
|
final VoidCallback onTap;
|
|
|
+ final Color? iconColor;
|
|
|
+ final Color? bgColor;
|
|
|
|
|
|
const _GridItem({
|
|
|
required this.icon,
|
|
|
required this.label,
|
|
|
required this.onTap,
|
|
|
+ this.iconColor,
|
|
|
+ this.bgColor,
|
|
|
});
|
|
|
}
|