Преглед на файлове

feat: 全项目深色主题/多语言/UI组件重构

【筛选面板重构】
- FilterBar 重命名为 ListFilterPanel,TDSlidePopupRoute → TDDrawer
- 日期范围双 section 合并为单 section,自动展开两行
- 新增本周/上月/近3个月快捷日期按钮,替换为自定义 chip 组件
- 日期校验改用 TDMessage,支持日期范围持久化与默认值传参
- 新增 ListFilterPanel 使用文档与新增筛选条件步骤说明

【列表页增强】
- NavBar 过滤图标大小统一、红点逻辑修复(新增 filterVersion)
- 6个列表页新增 TDSearchBar(i18n + 深色适配)
- 列表底部新增 ListFooter '没有更多了'(分页就绪)
- Tab key 统一 'revoked' → 'withdrawn'(DB枚举一致)
- 5个列表页 scope chip 样式统一、i18n/深色适配
- 车辆列表 status switch 修复 withdrawn 分支
- 外勤日志 isManager 改为 provider

【深色主题全面适配】
- AppTheme TDThemeData 补充 6 个缺失色键
- bgSecondaryContainer 浅色/深色值规范化
- 搜索框提示词对比度修复
- 公告已读条目文字亮度调降
- StatusTag 重构为 Theme 动态取色,消除硬编码
- TDDatePicker/TDMultiPicker 深色适配(Theme包裹+customSelectWidget)
- 底部 TabBar 深色适配(显式传色+divider透明)

【多语言完善】
- zh_TW.json 补齐 319 个缺失键(646键对齐zh_CN)
- 新增 20+ i18n 键(搜索提示词/快捷日期/校验提示/chip标签等)
- 列表页 FilterGroup/Section/Option label 全量 i18n 化

【硬编码颜色清除】
- 公告/管理员/外勤详情硬编码色 → theme 色
- 所有不透明硬编码颜色归零

【其他】
- app_shell.dart 删除,nav_bar_config.dart 移入 shared/widgets
- TDBottomTabBar capsule→filled 样式
- AppScaffold TabBar 间距/分割线修复
chengc преди 5 дни
родител
ревизия
971098f515
променени са 44 файла, в които са добавени 1750 реда и са изтрити 1097 реда
  1. 29 3
      assets/i18n/en.json
  2. 29 3
      assets/i18n/zh_CN.json
  3. 348 3
      assets/i18n/zh_TW.json
  4. 1 1
      docs/superpowers/specs/tboss-oa-design.md
  5. 2 1
      docs/superpowers/specs/tboss-oa-prd.md
  6. 47 41
      lib/core/theme/app_colors.dart
  7. 24 11
      lib/core/theme/app_colors_extension.dart
  8. 17 15
      lib/core/theme/app_theme.dart
  9. 2 2
      lib/features/admin/admin_permissions_page.dart
  10. 1 1
      lib/features/announcement/announcement_create_page.dart
  11. 1 1
      lib/features/announcement/announcement_detail_page.dart
  12. 69 28
      lib/features/announcement/announcement_list_page.dart
  13. 1 1
      lib/features/expense/expense_apply_page.dart
  14. 1 1
      lib/features/expense/expense_detail_page.dart
  15. 1 1
      lib/features/expense/expense_list_controller.dart
  16. 86 63
      lib/features/expense/expense_list_page.dart
  17. 1 1
      lib/features/expense_application/expense_application_apply_page.dart
  18. 1 1
      lib/features/expense_application/expense_application_detail_page.dart
  19. 1 1
      lib/features/expense_application/expense_application_list_controller.dart
  20. 118 75
      lib/features/expense_application/expense_application_list_page.dart
  21. 1 1
      lib/features/home/home_page.dart
  22. 1 1
      lib/features/messages/message_list_page.dart
  23. 1 1
      lib/features/outing_log/outing_log_create_page.dart
  24. 10 7
      lib/features/outing_log/outing_log_detail_page.dart
  25. 92 87
      lib/features/outing_log/outing_log_list_page.dart
  26. 1 1
      lib/features/overtime/overtime_apply_page.dart
  27. 3 3
      lib/features/overtime/overtime_detail_page.dart
  28. 110 70
      lib/features/overtime/overtime_list_page.dart
  29. 1 1
      lib/features/profile/profile_page.dart
  30. 2 2
      lib/features/report/expense_apply_detail_report_page.dart
  31. 1 1
      lib/features/report/expense_detail_report_page.dart
  32. 1 1
      lib/features/report/outing_log_report_page.dart
  33. 1 1
      lib/features/report/overtime_detail_report_page.dart
  34. 1 1
      lib/features/report/vehicle_detail_report_page.dart
  35. 0 236
      lib/features/shell/app_shell.dart
  36. 1 1
      lib/features/vehicle/vehicle_apply_page.dart
  37. 2 2
      lib/features/vehicle/vehicle_detail_page.dart
  38. 106 121
      lib/features/vehicle/vehicle_list_page.dart
  39. 1 1
      lib/shared/models/approval_status.dart
  40. 100 27
      lib/shared/widgets/app_scaffold.dart
  41. 386 233
      lib/shared/widgets/filter_bar.dart
  42. 72 0
      lib/shared/widgets/list_footer.dart
  43. 10 3
      lib/features/shell/nav_bar_config.dart
  44. 66 41
      lib/shared/widgets/status_tag.dart

+ 29 - 3
assets/i18n/en.json

@@ -59,7 +59,6 @@
   "pending": "Pending",
   "approved": "Approved",
   "rejected": "Rejected",
-  "revoked": "Revoked",
   "expired": "Expired",
   "paid": "Paid",
   "returned": "Returned",
@@ -76,6 +75,17 @@
   "filterThisQuarter": "This Quarter",
   "filterThisYear": "This Year",
   "filterThisWeek": "This Week",
+  "filter7Days": "7 Days",
+  "filter30Days": "30 Days",
+  "filterLastMonth": "Last Month",
+  "filterLast3Months": "Last 3 Months",
+  "filterTitle": "Filter",
+  "filterStartDate": "Start Date",
+  "filterEndDate": "End Date",
+  "filterSelectStartDate": "Select Start Date",
+  "filterSelectEndDate": "Select End Date",
+  "filterDateStartAfterEnd": "Start date cannot be after end date",
+  "filterDateEndBeforeStart": "End date cannot be before start date",
   "filterNotice": "Notices",
   "filterHr": "HR & Policy",
   "filterHoliday": "Holiday",
@@ -93,7 +103,7 @@
   "statusApproved": "Approved",
   "statusRejected": "Rejected",
   "statusDraft": "Draft",
-  "statusRevoked": "Revoked",
+  "statusWithdrawn": "Withdrawn",
   "statusReturned": "Returned",
   "statusDisabled": "Disabled",
   "statusWaitApprove": "Pending Approval",
@@ -620,5 +630,21 @@
   "allPaymentsProcessed": "All pending payments have been processed",
   "paymentArchiveSuccess": "Payment archived successfully",
   "withdrawConfirm": "Confirm Withdrawal",
-  "withdrawConfirmTip": "Confirm withdrawal of this application? The approval process will be terminated."
+  "withdrawConfirmTip": "Confirm withdrawal of this application? The approval process will be terminated.",
+  "noMoreData": "No more data",
+  "scopeMyApplications": "My Requests",
+  "scopeSubordinates": "Subordinates",
+  "searchExpense": "Search report no. or applicant",
+  "searchExpenseApply": "Search application no. or applicant",
+  "searchOvertime": "Search OT no. or applicant",
+  "searchVehicle": "Search vehicle no. or applicant",
+  "searchOutingLog": "Search customer name or salesperson",
+  "searchAnnouncement": "Search announcement title",
+  "filterDateRange": "Date Range",
+  "filterGroupOther": "Other",
+  "filterExpenseTravel": "Travel Expense",
+  "filterExpenseEntertainment": "Entertainment",
+  "filterExpenseOffice": "Office Expense",
+  "filterExpenseMeeting": "Meeting Expense",
+  "filterCritical": "Critical"
 }

+ 29 - 3
assets/i18n/zh_CN.json

@@ -59,7 +59,6 @@
   "pending": "审批中",
   "approved": "已通过",
   "rejected": "已拒绝",
-  "revoked": "已撤回",
   "expired": "已过期",
   "paid": "已付款",
   "returned": "已还车",
@@ -76,6 +75,17 @@
   "filterThisQuarter": "本季",
   "filterThisYear": "本年",
   "filterThisWeek": "本周",
+  "filter7Days": "7天内",
+  "filter30Days": "30天内",
+  "filterLastMonth": "上月",
+  "filterLast3Months": "近3个月",
+  "filterTitle": "筛选条件",
+  "filterStartDate": "起始日期",
+  "filterEndDate": "结束日期",
+  "filterSelectStartDate": "选择起始日期",
+  "filterSelectEndDate": "选择结束日期",
+  "filterDateStartAfterEnd": "起始日期不能大于结束日期",
+  "filterDateEndBeforeStart": "结束日期不能小于起始日期",
   "filterNotice": "通知公告",
   "filterHr": "人事与制度",
   "filterHoliday": "放假与活动",
@@ -93,7 +103,7 @@
   "statusApproved": "已通过",
   "statusRejected": "已拒绝",
   "statusDraft": "草稿",
-  "statusRevoked": "已撤回",
+  "statusWithdrawn": "已撤回",
   "statusReturned": "已还车",
   "statusDisabled": "已禁用",
   "statusWaitApprove": "待审批",
@@ -620,5 +630,21 @@
   "allPaymentsProcessed": "全部待付款单据已处理完毕",
   "paymentArchiveSuccess": "打款归档成功",
   "withdrawConfirm": "确认撤回",
-  "withdrawConfirmTip": "确认撤回该申请吗?撤回后审批流程将终止。"
+  "withdrawConfirmTip": "确认撤回该申请吗?撤回后审批流程将终止。",
+  "noMoreData": "没有更多了",
+  "scopeMyApplications": "我的发起",
+  "scopeSubordinates": "下属审批",
+  "searchExpense": "搜索报销单号或申请人",
+  "searchExpenseApply": "搜索申请单号或申请人",
+  "searchOvertime": "搜索加班单号或申请人",
+  "searchVehicle": "搜索用车单号或申请人",
+  "searchOutingLog": "搜索客户名称或业务员",
+  "searchAnnouncement": "搜索公告标题",
+  "filterDateRange": "日期范围",
+  "filterGroupOther": "其它",
+  "filterExpenseTravel": "差旅费",
+  "filterExpenseEntertainment": "业务招待费",
+  "filterExpenseOffice": "办公费",
+  "filterExpenseMeeting": "会议费",
+  "filterCritical": "特急"
 }

+ 348 - 3
assets/i18n/zh_TW.json

@@ -59,7 +59,6 @@
   "pending": "審批中",
   "approved": "已通過",
   "rejected": "已拒絕",
-  "revoked": "已撤回",
   "expired": "已過期",
   "paid": "已付款",
   "returned": "已還車",
@@ -76,6 +75,17 @@
   "filterThisQuarter": "本季",
   "filterThisYear": "本年",
   "filterThisWeek": "本週",
+  "filter7Days": "7天內",
+  "filter30Days": "30天內",
+  "filterLastMonth": "上月",
+  "filterLast3Months": "近3個月",
+  "filterTitle": "篩選條件",
+  "filterStartDate": "起始日期",
+  "filterEndDate": "結束日期",
+  "filterSelectStartDate": "選擇起始日期",
+  "filterSelectEndDate": "選擇結束日期",
+  "filterDateStartAfterEnd": "起始日期不能大於結束日期",
+  "filterDateEndBeforeStart": "結束日期不能小於起始日期",
   "filterNotice": "通知公告",
   "filterHr": "人事與制度",
   "filterHoliday": "放假與活動",
@@ -93,7 +103,7 @@
   "statusApproved": "已通過",
   "statusRejected": "已拒絕",
   "statusDraft": "草稿",
-  "statusRevoked": "已撤回",
+  "statusWithdrawn": "已撤回",
   "statusReturned": "已還車",
   "statusDisabled": "已禁用",
   "statusWaitApprove": "待審批",
@@ -299,5 +309,340 @@
   "allPaymentsProcessed": "全部待付款單據已處理完畢",
   "paymentArchiveSuccess": "打款歸檔成功",
   "withdrawConfirm": "確認撤回",
-  "withdrawConfirmTip": "確認撤回該申請嗎?撤回後審批流程將終止。"
+  "withdrawConfirmTip": "確認撤回該申請嗎?撤回後審批流程將終止。",
+  "noMoreData": "沒有更多了",
+  "scopeMyApplications": "我的發起",
+  "scopeSubordinates": "下屬審批",
+  "searchExpense": "搜尋報銷單號或申請人",
+  "searchExpenseApply": "搜尋申請單號或申請人",
+  "searchOvertime": "搜尋加班單號或申請人",
+  "searchVehicle": "搜尋用車單號或申請人",
+  "searchOutingLog": "搜尋客戶名稱或業務員",
+  "searchAnnouncement": "搜尋公告標題",
+  "filterDateRange": "日期範圍",
+  "filterGroupOther": "其它",
+  "filterExpenseTravel": "差旅費",
+  "filterExpenseEntertainment": "業務招待費",
+  "filterExpenseOffice": "辦公費",
+  "filterExpenseMeeting": "會議費",
+  "filterCritical": "特急",
+  "applicant": "申請人",
+  "department": "所属部門",
+  "expenseType": "費用类型",
+  "expenseAmount": "報销金額",
+  "relatedProject": "關联項目",
+  "budgetSubject": "預算科目",
+  "costCenter": "成本中心",
+  "totalExpense": "報销總額",
+  "receiptAccount": "收款账户",
+  "bankName": "開户银行",
+  "accountName": "账户名称",
+  "bankAccount": "银行账號",
+  "expenseDetails": "費用明细",
+  "addExpenseDetail": "添加費用明细",
+  "invoiceUpload": "發票上傳",
+  "maxInvoices": "最多上傳9张發票",
+  "addDetail": "添加明细",
+  "expenseName": "費用名称",
+  "amount": "金額",
+  "description": "描述",
+  "invoiceCheck": "發票合規检查",
+  "invoiceCheck1": "發票信息與報销一致",
+  "invoiceCheck2": "發票金額與報销金額一致",
+  "invoiceCheck3": "發票日期在有效期内",
+  "invoiceCheck4": "發票抬頭為公司全称",
+  "approvalFlow": "審批流程",
+  "financialArchive": "財務歸檔",
+  "voucherNo": "凭證號",
+  "archiveDate": "歸檔日期",
+  "archiver": "歸檔人",
+  "financeDept": "財務部",
+  "expenseProject": "費用項目",
+  "expenseReason": "報销事由",
+  "enterExpenseReason": "請輸入報销事由",
+  "selectSubject": "請選择科目",
+  "selectCostCenter": "請選择成本中心",
+  "selectBank": "請選择開户银行",
+  "enterBankAccount": "請輸入银行账號",
+  "overtimeInfo": "加班信息",
+  "overtimeType": "加班类型",
+  "compensationMethod": "补偿方式",
+  "netOvertimeHours": "净加班時長",
+  "overtimeReason": "加班事由",
+  "enterOvertimeReason": "請詳细描述加班原因",
+  "workdayOvertime": "工作日加班",
+  "weekendOvertime": "休息日加班",
+  "holidayOvertime": "节假日加班",
+  "overtimePay": "加班費",
+  "compLeave": "调休",
+  "vehicleInfo": "用車信息",
+  "selectVehicle": "選择車辆",
+  "selectPlate": "請選择車牌",
+  "vehicleOccupied": "该車辆在此時段已被占用",
+  "vehicleReason": "用車事由",
+  "departureLocation": "出發地点",
+  "gpsLocating": "GPS定位中…",
+  "destination": "目的地点",
+  "enterDestination": "請輸入目的地点",
+  "passengerCount": "同行人数",
+  "estimatedTime": "預計時間",
+  "estimatedMileage": "預估里程",
+  "tripPreview": "行程預覽",
+  "mapPreview": "地图路線預覽",
+  "noVehicle": "未指定車辆",
+  "sedan": "轿車",
+  "suv": "SUV",
+  "businessVan": "商務車",
+  "customerReception": "客户接待",
+  "businessTrip": "商務出行",
+  "internalAffairs": "内部公務",
+  "selectVehicleReason": "選择用車事由",
+  "enterField": "請輸入{field}",
+  "emergencyLevel": "紧急程度",
+  "feeType": "費用类型",
+  "feeReason": "費用事由",
+  "enterFeeReason": "請輸入費用事由",
+  "relatedControl": "關联管控",
+  "availableBudget": "可用預算餘額",
+  "noDetailHint": "暂无明细,点击上方添加",
+  "overBudget": "超出預算{amount}",
+  "attachmentUpload": "附件上傳",
+  "maxAttachment": "最多上傳6张图片或PDF文件",
+  "attachments": "附件",
+  "outingDetail": "外出詳情",
+  "outingType": "外出类型",
+  "outingLocation": "外出地点",
+  "enterLocation": "請輸入外出地点",
+  "outingReason": "外出事由",
+  "enterOutingReason": "請填写外出事由及工作内容...",
+  "workSummary": "工作總結",
+  "followUp": "后续計劃",
+  "sitePhotos": "现场照片",
+  "customerVisit": "客户拜访",
+  "outingAffairs": "外出辦事",
+  "selectOutingType": "選择外出类型",
+  "announcementContent": "公告内容",
+  "enterContent": "請輸入公告正文内容...",
+  "announcementType": "公告类型",
+  "publishSettings": "發布設置",
+  "pinAnnouncement": "置顶公告",
+  "validUntil": "有效期至",
+  "recipientScope": "接收範围",
+  "addAttachment": "添加附件",
+  "selectAnnouncementType": "選择公告类型",
+  "selectRecipientScope": "選择接收範围",
+  "auditTracking": "全員触達率審計追踪",
+  "dingReminder": "一键强力 DING 催辦",
+  "hrPolicy": "人事與制度",
+  "holidayActivity": "放假與活动",
+  "searchByNameOrId": "輸入姓名或工號進行检索...",
+  "approver": "審批人",
+  "financeStaff": "財務人員",
+  "systemAdmin": "系統管理員",
+  "regularEmployee": "普通員工",
+  "employeeId": "工號:",
+  "itDept": "信息技术部",
+  "adminDept": "行政部",
+  "marketDept": "市场部",
+  "techDept": "技术部",
+  "yearApproved": "本年已核销",
+  "monthCount": "本月笔数",
+  "waitApprove": "待審批",
+  "waitPayment": "待付款",
+  "chartTitle1": "近12個月報销金額 vs 審批通過金額",
+  "chartDesc1": "双折線對比图",
+  "monthNetHours": "本月净工時",
+  "overtimeCount": "加班次数",
+  "compHours": "调休小時",
+  "settleCount": "結算次数",
+  "chartTitle2": "近12個月加班工時趋势",
+  "chartDesc2": "柱状趋势图",
+  "monthVehicle": "本月用車",
+  "totalMileage": "累計里程",
+  "totalCost": "費用合計",
+  "notReturned": "未還車",
+  "chartTitle3": "近12個月用車次数 vs 累計費用",
+  "chartDesc3": "双軸折線图",
+  "yearTotalApp": "本年累計申請",
+  "approvedCount": "已通過笔数",
+  "approvedAmount": "已通過金額",
+  "chartTitle4": "近12個月申請金額 vs 已通過金額",
+  "chartDesc4": "双折線對比图",
+  "monthVisits": "本月拜访",
+  "visitCustomers": "拜访客户",
+  "avgRating": "平均评分",
+  "notReviewed": "未点评",
+  "chartTitle5": "近12個月拜访次数 vs 平均评分",
+  "chartDesc5": "双軸折線图",
+  "enterAmount": "請輸入金額",
+  "enterValidAmount": "請輸入有效金額",
+  "amountMustPositive": "金額必须大于0",
+  "maxChars": "最多輸入{max}個字符",
+  "reports": "報表",
+  "publishAnnouncement": "發布公告",
+  "overtimeRecords": "加班記錄",
+  "vehicleRecords": "用車記錄",
+  "reportExpenseApply": "事前申請報表",
+  "reportExpense": "費用報销報表",
+  "reportOvertime": "加班報表",
+  "reportVehicle": "用車報表",
+  "reportOutingLog": "外勤日志報表",
+  "pendingApproval": "待審批",
+  "markAllRead": "全部已读",
+  "paidTotal": "已支付總額",
+  "pendingPaymentTotal": "待付款總額",
+  "abnormalReturns": "异常退回",
+  "withdrawNotice": "撤回通知",
+  "expiryReminder": "過期提醒",
+  "deptDashboard": "部門快捷看板",
+  "financeDashboard": "財務看盘",
+  "deptMonthlyReimbursement": "部門本月報销",
+  "deptMonthlySubmitted": "部門本月單據",
+  "deptPendingDocuments": "部門在途單據",
+  "expenseList": "報销列表",
+  "expenseApply": "費用報销",
+  "expenseDetail": "報销詳情",
+  "editExpense": "编辑報销",
+  "overtimeList": "加班記錄",
+  "overtimeDetail": "加班詳情",
+  "overtimeApply": "加班申請",
+  "vehicleList": "用車記錄",
+  "vehicleApply": "用車申請",
+  "vehicleDetail": "用車詳情",
+  "expenseApplyList": "事前申請列表",
+  "expenseApplyDetail": "事前申請詳情",
+  "expenseApplyRequest": "事前申請",
+  "outingLogList": "外出日志",
+  "outingLogCreate": "新建外出日志",
+  "outingLogDetail": "外出日志詳情",
+  "announcementList": "公告列表",
+  "announcementDetail": "公告詳情",
+  "announcementCreate": "發布公告",
+  "messageNotifications": "消息通知",
+  "reportExpenseDetail": "費用報销明细報表",
+  "reportExpenseApplyDetail": "事前申請明细報表",
+  "reportOvertimeDetail": "加班明细報表",
+  "reportVehicleDetail": "用車明细報表",
+  "reportOutingLogDetail": "外勤日志明细報表",
+  "permissionManagement": "权限管理",
+  "invoiceAttachment": "發票附件",
+  "customerInfo": "客户信息",
+  "close": "關闭",
+  "confirmSubmit": "确认提交",
+  "confirmPublish": "确认發布",
+  "origin": "始發地",
+  "companion": "同行人",
+  "selectLicensePlate": "選择車牌號",
+  "selectCompanion": "選择同行人",
+  "returnCarRegister": "還車登記",
+  "confirmReturnCar": "确认還車",
+  "actualReturnTime": "實還時間",
+  "tripRoute": "行程路線",
+  "reEdit": "重新编辑",
+  "withdrawApplication": "撤回申請",
+  "returnCarArchived": "已還車歸檔",
+  "mileageBefore": "出車前里程(公里)",
+  "mileageAfter": "還車后里程(公里)",
+  "actualCost": "實際費用金額(元)",
+  "returnCarSubmitted": "還車登記已提交",
+  "navigation": "導航",
+  "navigationComingSoon": "導航即将開放",
+  "selectReturnTime": "選择實還時間",
+  "vehicleOccupiedPeriod": "该時段車辆已被預订,請選择其他車辆或调整時間",
+  "enterVehicleReason": "請填写用車事由",
+  "mileageInvalid": "還車后里程不能小于出車前里程",
+  "costRemarkLabel": "費用备注(路桥費/停車費等)",
+  "departTime": "出車時間",
+  "returnTime": "還車時間",
+  "earlyReturn": "提示:提前還車",
+  "overReturnTime": "警告:已超出原計劃還車時間",
+  "submitTimeText": "提交時間",
+  "arriveTime": "還車時間",
+  "preview": "預覽",
+  "statTotalApproved": "累計核销總額",
+  "statMonthCount": "当月笔数",
+  "statPendingApprove": "待審批笔数",
+  "statPendingPayment": "待付款笔数",
+  "statTotalApply": "累計申請總額",
+  "statApprovedCount": "已通過笔数",
+  "statApprovedAmount": "已通過金額",
+  "statMonthHours": "当月净工時",
+  "statMonthTrips": "当月次数",
+  "statTotalMileage": "累計里程",
+  "statTotalCost": "路桥停車費",
+  "statNotReturned": "未還車数",
+  "statMonthVisits": "当月拜访次数",
+  "statVisitedCustomers": "拜访客户数",
+  "statAvgRating": "平均评分",
+  "statNotReviewed": "未点评日志数",
+  "rejecter": "拒绝人",
+  "currentApprover": "当前審批人",
+  "expenseApplyImport": "一键導入已通過的事前申請",
+  "importApprovedPreApp": "一键導入已通過的事前申請",
+  "projectSelection": "項目選择功能開發中",
+  "budgetSubjectSelection": "預算科目功能開發中",
+  "costCenterSelection": "成本中心功能開發中",
+  "bankSelection": "選择開户银行功能開發中",
+  "bankAccountInput": "银行账號輸入功能開發中",
+  "hours": "小時",
+  "permissionEdit": "权限编辑",
+  "quickPresets": "快捷套餐",
+  "permissionItems": "权限点",
+  "changeLog": "变更記錄",
+  "recentItems": "最近{count}条",
+  "confirmSave": "确认保存",
+  "clickChartToFilter": "点击柱状图可筛選",
+  "chartDeptExpenseCompare": "部門報销金額 vs 審批通過金額對比",
+  "chartDeptApplyCompare": "部門申請金額 vs 已通過金額對比",
+  "chartDeptOvertimeCompare": "部門加班總工時對比",
+  "chartDeptVehicleCompare": "部門用車次数 vs 費用對比",
+  "chartDeptOutingCompare": "部門拜访次数 vs 平均评分對比",
+  "selectOvertimeType": "選择加班类型",
+  "selectCompensationMethod": "選择补偿方式",
+  "comments": "点评",
+  "managerComment": "经理点评",
+  "noPhotos": "暂无照片",
+  "noComments": "暂无点评",
+  "selectRating": "請選择评分",
+  "enterComment": "請輸入点评内容",
+  "commentSent": "点评已發送",
+  "requiredSummary": "請填写工作總結",
+  "requiredPhotos": "請至少拍摄一张现场照片",
+  "outingLogSubmitted": "外勤日志提交成功",
+  "gpsFailed": "无法获取当前位置",
+  "gpsFailedHint": "請检查位置权限設置",
+  "retry": "重试",
+  "gpsPermission": "无法获取GPS定位,請检查位置权限",
+  "gpsSuccess": "GPS定位成功",
+  "draftSavedToast": "已保存為草稿",
+  "selectContact": "選择联系人",
+  "selectContactHint": "点击選择联系人(選填)",
+  "searchCustomer": "請輸入客户名称搜索",
+  "noContact": "该客户暂无联系人",
+  "selectCustomerFirst": "請先選择客户名称",
+  "takePhoto": "拍照",
+  "maxPhotoCount": "最多拍摄9张照片",
+  "limitReached": "已達上限",
+  "tapToTakePhoto": "点击拍照(至少1张)",
+  "watermarkHint": "照片将自动添加水印:服務器授時 + GPS经纬度",
+  "enterTitle": "請輸入公告标题(必填)",
+  "announcementTypes": "選择公告类型",
+  "previewTitle": "預覽",
+  "confirmPublishTitle": "确认發布",
+  "confirmPublishContent": "确认發布公告「{title}」?",
+  "announcementPublished": "公告發布成功",
+  "attachmentPicker": "模拟:選择附件(PDF/图片/Word/Excel,≤20MB)",
+  "attachmentLimit": "最多5個附件,支持PDF/图片/Word/Excel,單文件≤20MB",
+  "expiryNever": "永不過期(可選)",
+  "allStaff": "全員",
+  "byDept": "按部門",
+  "byUser": "按指定用户",
+  "selectDept": "選择部門",
+  "searchEmployeeHint": "輸入姓名或工號搜索",
+  "coverageCount": "覆盖人数",
+  "scopeAllStaff": "所有員工均可查看",
+  "licensePlate": "車牌號",
+  "vehiclePurpose": "用車目的",
+  "addExpenseDetailFirst": "請在明细中添加費用項",
+  "submitConfirmContent": "提交后里程和費用不可再修改,是否继续?"
 }

+ 1 - 1
docs/superpowers/specs/tboss-oa-design.md

@@ -1128,7 +1128,7 @@
 | `--info-text` | `#3B82C4` | `#6AA8E6` | 已还车/已付款 |
 | `--paid-text` | `#2563B0` | `#6AA8E6` | 已付款文字 |
 | `--unpaid-text` | `#CA8A04` | `#FBBF24` | 待付款文字 |
-| `--revoked-text` | `#7C6F8A` | `#9A8DA0` | 已撤回文字 |
+| `--withdrawn-text` | `#7C6F8A` | `#9A8DA0` | 已撤回文字 |
 | `--chart-1~5` | 5 色 | 5 色 | fl_chart 图表 |
 
 #### 6.2 字号层级

Файловите разлики са ограничени, защото са твърде много
+ 2 - 1
docs/superpowers/specs/tboss-oa-prd.md


+ 47 - 41
lib/core/theme/app_colors.dart

@@ -24,16 +24,18 @@ class AppColors {
   //  primary700  L=29% S=95%  深色背景上文字
   //  primary800  L=19% S=90%  深底色
   //  primary900  L=10% S=85%  最深
-  static const Color primary50 = Color(0xFFEDF7FD);   // L=96% 极淡蓝底
-  static const Color primary100 = Color(0xFFD8EFFB);  // L=91% ★ primaryLight:Chip/图标底
-  static const Color primary200 = Color(0xFFA2DCF6);  // L=78% hover
-  static const Color primary300 = Color(0xFF6CCCF2);  // L=66% 边框
-  static const Color primary400 = Color(0xFF28BCF3);  // L=54% 次级按钮
-  static const Color primary500 = Color(0xFF00ABF3);  // L=48% ★ 主色
-  static const Color primary600 = Color(0xFF0089C4);  // L=38% 按压态
-  static const Color primary700 = Color(0xFF006794);  // L=29% 深文字
-  static const Color primary800 = Color(0xFF004563);  // L=19% 深底色
-  static const Color primary900 = Color(0xFF002233);  // L=10% 最深
+  static const Color primary50 = Color(0xFFEDF7FD); // L=96% 极淡蓝底
+  static const Color primary100 = Color(
+    0xFFD8EFFB,
+  ); // L=91% ★ primaryLight:Chip/图标底
+  static const Color primary200 = Color(0xFFA2DCF6); // L=78% hover
+  static const Color primary300 = Color(0xFF6CCCF2); // L=66% 边框
+  static const Color primary400 = Color(0xFF28BCF3); // L=54% 次级按钮
+  static const Color primary500 = Color(0xFF00ABF3); // L=48% ★ 主色
+  static const Color primary600 = Color(0xFF0089C4); // L=38% 按压态
+  static const Color primary700 = Color(0xFF006794); // L=29% 深文字
+  static const Color primary800 = Color(0xFF004563); // L=19% 深底色
+  static const Color primary900 = Color(0xFF002233); // L=10% 最深
 
   // ── 兼容旧名 ──
   static const Color primary = primary500;
@@ -46,25 +48,28 @@ class AppColors {
   static const Color gray50 = Color(0xFFF9FAFB);
   static const Color gray100 = Color(0xFFF2F4F6);
   static const Color gray200 = Color(0xFFE5E8EB);
-  static const Color gray300 = Color(0xFFD1D6DB);   // 占位符 / 禁用文字
+  static const Color gray300 = Color(0xFFD1D6DB); // 占位符 / 禁用文字
   static const Color gray400 = Color(0xFF9CA3AF);
-  static const Color gray500 = Color(0xFF6B7280);   // 二级文字
+  static const Color gray500 = Color(0xFF6B7280); // 二级文字
   static const Color gray600 = Color(0xFF4B5563);
   static const Color gray700 = Color(0xFF374151);
-  static const Color gray800 = Color(0xFF1F2937);   // 主文字
-  static const Color gray900 = Color(0xFF111827);   // 最深文字(极少用)
+  static const Color gray800 = Color(0xFF1F2937); // 主文字
+  static const Color gray900 = Color(0xFF111827); // 最深文字(极少用)
 
   // ── 文本三档 ──
-  static const Color textPrimary = Color(0xFF1F2937);    // #1F2937 WCAG AAA
-  static const Color textSecondary = Color(0xFF6B7280);  // #6B7280 WCAG AA
-  static const Color textPlaceholder = Color(0xFFD1D6DB); // #D1D6DB WCAG AA(白底 4.5:1)
+  static const Color textPrimary = Color(0xFF1F2937); // #1F2937 WCAG AAA
+  static const Color textSecondary = Color(0xFF6B7280); // #6B7280 WCAG AA
+  static const Color textPlaceholder = Color(
+    0xFFD1D6DB,
+  ); // #D1D6DB WCAG AA(白底 4.5:1)
   static const Color textDisabled = Color(0xFFD1D6DB);
   static const Color statusGray = Color(0xFF9CA3AF);
 
   // ── 背景三档 ──
-  static const Color bgPage = Color(0xFFF2F4F6);    // 页面底
-  static const Color bgCard = Color(0xFFFFFFFF);    // 卡片/表单
+  static const Color bgPage = Color(0xFFF2F4F6); // 页面底
+  static const Color bgCard = Color(0xFFFFFFFF); // 卡片/表单
   static const Color bgDisabled = Color(0xFFE5E8EB); // 禁用态
+  static const Color bgSecondaryContainer = Color(0xFFE6E8EB); // 次级容器(搜索框等)
 
   // ── 边框 ──
   static const Color border = Color(0xFFE5E8EB);
@@ -72,19 +77,19 @@ class AppColors {
   // ═══════════════════════════════════════════
   //  语义色
   // ═══════════════════════════════════════════
-  static const Color success = Color(0xFF0EA371);       // 成功/已通过
-  static const Color successBg = Color(0xFFEBF9F3);     // 成功浅底(微调更清新)
-  static const Color warning = Color(0xFFE37318);       // 警告/审批中
-  static const Color warningBg = Color(0xFFFFF6ED);     // 警告浅底(更温暖)
-  static const Color danger = Color(0xFFDC2626);        // 危险/已拒绝/删除
-  static const Color dangerBg = Color(0xFFFEF4F4);      // 危险浅底(更柔和)
+  static const Color success = Color(0xFF0EA371); // 成功/已通过
+  static const Color successBg = Color(0xFFEBF9F3); // 成功浅底(微调更清新)
+  static const Color warning = Color(0xFFE37318); // 警告/审批中
+  static const Color warningBg = Color(0xFFFFF6ED); // 警告浅底(更温暖)
+  static const Color danger = Color(0xFFDC2626); // 危险/已拒绝/删除
+  static const Color dangerBg = Color(0xFFFEF4F4); // 危险浅底(更柔和)
 
   // ═══════════════════════════════════════════
   //  金额色(独立于成功/危险语义)
   // ═══════════════════════════════════════════
   static const Color amountPositive = Color(0xFF1D7AB5); // 正金额(品牌蓝,区分 success)
   static const Color amountNegative = Color(0xFFDC2626); // 负金额/超支
-  static const Color amountNeutral = Color(0xFF6B7280);  // 普通数值
+  static const Color amountNeutral = Color(0xFF6B7280); // 普通数值
 
   // ── 兼容旧名 ──
   static const Color amountPrimary = amountPositive;
@@ -101,16 +106,16 @@ class AppColors {
   // ═══════════════════════════════════════════
   //  支付状态色(区别于信息色)
   // ═══════════════════════════════════════════
-  static const Color paidText = Color(0xFF2563B0);      // 已付款-深蓝
-  static const Color paidBg = Color(0xFFF0F6FF);        // 已付款-浅蓝(区别于 infoBg)
-  static const Color unpaidText = Color(0xFFCA8A04);    // 待付款-琥珀
-  static const Color unpaidBg = Color(0xFFFFFCEF);      // 待付款-浅琥珀(更柔和)
+  static const Color paidText = Color(0xFF2563B0); // 已付款-深蓝
+  static const Color paidBg = Color(0xFFF0F6FF); // 已付款-浅蓝(区别于 infoBg)
+  static const Color unpaidText = Color(0xFFCA8A04); // 待付款-琥珀
+  static const Color unpaidBg = Color(0xFFFFFCEF); // 待付款-浅琥珀(更柔和)
 
   // ═══════════════════════════════════════════
   //  撤回/草稿/过期
   // ═══════════════════════════════════════════
-  static const Color revokedText = Color(0xFF7C6F8A);
-  static const Color revokedBg = Color(0xFFF6F3F8);
+  static const Color withdrawnText = Color(0xFF7C6F8A);
+  static const Color withdrawnBg = Color(0xFFF6F3F8);
   static const Color bgExpired = Color(0xFFF9FAFB);
   static const Color swipeDeleteBg = Color(0xFFF2F4F6);
 
@@ -126,8 +131,8 @@ class AppColors {
   // ═══════════════════════════════════════════
   //  遮罩 / 高亮
   // ═══════════════════════════════════════════
-  static const Color overlay = Color(0x66000000);       // 弹窗蒙层 40%
-  static const Color highlight = Color(0xFFEDF7FD);     // 列表选中高亮(= primary50)
+  static const Color overlay = Color(0x66000000); // 弹窗蒙层 40%
+  static const Color highlight = Color(0xFFEDF7FD); // 列表选中高亮(= primary50)
 }
 
 /// 深色主题 — 感知等价映射
@@ -146,7 +151,7 @@ class AppDarkColors {
   //    primary400 L=42%  次级按钮
   //    primary500 L=48%  ★ 主色 #00ABF3
   static const Color primary50 = Color(0xFF112835);
-  static const Color primary100 = Color(0xFF16384D);    // ★ primaryLight:可见蓝调
+  static const Color primary100 = Color(0xFF16384D); // ★ primaryLight:可见蓝调
   static const Color primary200 = Color(0xFF1B4965);
   static const Color primary300 = Color(0xFF1F5A7E);
   static const Color primary400 = Color(0xFF1F709E);
@@ -157,7 +162,7 @@ class AppDarkColors {
   static const Color primary900 = Color(0xFFCCEEFD);
 
   static const Color primary = primary500;
-  static const Color primaryActive = Color(0xFF0089C4);   // 深色下仍用中等深度
+  static const Color primaryActive = Color(0xFF0089C4); // 深色下仍用中等深度
   static const Color primaryLight = primary100;
 
   // ── 中性灰 ──
@@ -173,7 +178,7 @@ class AppDarkColors {
   static const Color gray900 = Color(0xFFF2F2F2);
 
   // ── 文本三档 ──
-  static const Color textPrimary = Color(0xFFE6E6E6);
+  static const Color textPrimary = Color(0xFFD9D9D9);
   static const Color textSecondary = Color(0xFFA6A6A6);
   static const Color textPlaceholder = Color(0xFF595959);
   static const Color textDisabled = Color(0xFF595959);
@@ -183,6 +188,7 @@ class AppDarkColors {
   static const Color bgPage = Color(0xFF0D0D0D);
   static const Color bgCard = Color(0xFF1A1A1A);
   static const Color bgDisabled = Color(0xFF262626);
+  static const Color bgSecondaryContainer = Color(0xFF3A3A3A); // 次级容器(搜索框等)
 
   static const Color border = Color(0xFF333333);
 
@@ -209,13 +215,13 @@ class AppDarkColors {
 
   // ── 支付状态(深色底:信息色与支付色区分)──
   static const Color paidText = Color(0xFF6AA8E6);
-  static const Color paidBg = Color(0xFF0F2238);        // 区别于 infoBg
+  static const Color paidBg = Color(0xFF0F2238); // 区别于 infoBg
   static const Color unpaidText = Color(0xFFFBBF24);
   static const Color unpaidBg = Color(0xFF2D1B06);
 
   // ── 撤回/草稿/过期 ──
-  static const Color revokedText = Color(0xFF9A8DA0);
-  static const Color revokedBg = Color(0xFF1E1A22);
+  static const Color withdrawnText = Color(0xFF9A8DA0);
+  static const Color withdrawnBg = Color(0xFF1E1A22);
   static const Color bgExpired = Color(0xFF1A1A1A);
   static const Color swipeDeleteBg = Color(0xFF262626);
 
@@ -228,7 +234,7 @@ class AppDarkColors {
 
   // ── 遮罩 / 高亮 ──
   static const Color overlay = Color(0xCC000000);
-  static const Color highlight = Color(0xFF112835);     // = primary50
+  static const Color highlight = Color(0xFF112835); // = primary50
 }
 
 /// Pencil 设计字号体系

+ 24 - 11
lib/core/theme/app_colors_extension.dart

@@ -43,9 +43,10 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
   final Color bgPage;
   final Color bgCard;
   final Color bgDisabled;
+  final Color bgSecondaryContainer;
   final Color border;
-  final Color revokedText;
-  final Color revokedBg;
+  final Color withdrawnText;
+  final Color withdrawnBg;
   final Color chart1;
   final Color chart2;
   final Color chart3;
@@ -97,9 +98,10 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
     required this.bgPage,
     required this.bgCard,
     required this.bgDisabled,
+    required this.bgSecondaryContainer,
     required this.border,
-    required this.revokedText,
-    required this.revokedBg,
+    required this.withdrawnText,
+    required this.withdrawnBg,
     required this.chart1,
     required this.chart2,
     required this.chart3,
@@ -152,9 +154,10 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
     bgPage: AppColors.bgPage,
     bgCard: AppColors.bgCard,
     bgDisabled: AppColors.bgDisabled,
+    bgSecondaryContainer: AppColors.bgSecondaryContainer,
     border: AppColors.border,
-    revokedText: AppColors.revokedText,
-    revokedBg: AppColors.revokedBg,
+    withdrawnText: AppColors.withdrawnText,
+    withdrawnBg: AppColors.withdrawnBg,
     chart1: AppColors.chart1,
     chart2: AppColors.chart2,
     chart3: AppColors.chart3,
@@ -207,9 +210,10 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
     bgPage: AppDarkColors.bgPage,
     bgCard: AppDarkColors.bgCard,
     bgDisabled: AppDarkColors.bgDisabled,
+    bgSecondaryContainer: AppDarkColors.bgSecondaryContainer,
     border: AppDarkColors.border,
-    revokedText: AppDarkColors.revokedText,
-    revokedBg: AppDarkColors.revokedBg,
+    withdrawnText: AppDarkColors.withdrawnText,
+    withdrawnBg: AppDarkColors.withdrawnBg,
     chart1: AppDarkColors.chart1,
     chart2: AppDarkColors.chart2,
     chart3: AppDarkColors.chart3,
@@ -253,7 +257,11 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
       infoBg: Color.lerp(infoBg, other.infoBg, t)!,
       infoLightBg: Color.lerp(infoLightBg, other.infoLightBg, t)!,
       infoBorder: Color.lerp(infoBorder, other.infoBorder, t)!,
-      timelineInactive: Color.lerp(timelineInactive, other.timelineInactive, t)!,
+      timelineInactive: Color.lerp(
+        timelineInactive,
+        other.timelineInactive,
+        t,
+      )!,
       paidText: Color.lerp(paidText, other.paidText, t)!,
       paidBg: Color.lerp(paidBg, other.paidBg, t)!,
       unpaidText: Color.lerp(unpaidText, other.unpaidText, t)!,
@@ -268,9 +276,14 @@ class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
       bgPage: Color.lerp(bgPage, other.bgPage, t)!,
       bgCard: Color.lerp(bgCard, other.bgCard, t)!,
       bgDisabled: Color.lerp(bgDisabled, other.bgDisabled, t)!,
+      bgSecondaryContainer: Color.lerp(
+        bgSecondaryContainer,
+        other.bgSecondaryContainer,
+        t,
+      )!,
       border: Color.lerp(border, other.border, t)!,
-      revokedText: Color.lerp(revokedText, other.revokedText, t)!,
-      revokedBg: Color.lerp(revokedBg, other.revokedBg, t)!,
+      withdrawnText: Color.lerp(withdrawnText, other.withdrawnText, t)!,
+      withdrawnBg: Color.lerp(withdrawnBg, other.withdrawnBg, t)!,
       chart1: Color.lerp(chart1, other.chart1, t)!,
       chart2: Color.lerp(chart2, other.chart2, t)!,
       chart3: Color.lerp(chart3, other.chart3, t)!,

+ 17 - 15
lib/core/theme/app_theme.dart

@@ -7,20 +7,14 @@ class AppTheme {
   AppTheme._();
 
   static Map<String, Font> _fontMap() => <String, Font>{
-        'fontBodyMedium': Font(size: AppFontSizes.body.toInt(), lineHeight: 22),
-        'fontBodySmall': Font(
-          size: AppFontSizes.caption.toInt(),
-          lineHeight: 20,
-        ),
-        'fontTitleMedium': Font(
-          size: AppFontSizes.subtitle.toInt(),
-          lineHeight: 24,
-        ),
-        'fontTitleLarge': Font(
-          size: AppFontSizes.title.toInt(),
-          lineHeight: 26,
-        ),
-      };
+    'fontBodyMedium': Font(size: AppFontSizes.body.toInt(), lineHeight: 22),
+    'fontBodySmall': Font(size: AppFontSizes.caption.toInt(), lineHeight: 20),
+    'fontTitleMedium': Font(
+      size: AppFontSizes.subtitle.toInt(),
+      lineHeight: 24,
+    ),
+    'fontTitleLarge': Font(size: AppFontSizes.title.toInt(), lineHeight: 26),
+  };
 
   /// TDesign 亮色主题
   static TDThemeData get tdThemeData {
@@ -35,10 +29,14 @@ class AppTheme {
         'errorNormalColor': AppColors.danger,
         'textColorPrimary': AppColors.textPrimary,
         'textColorSecondary': AppColors.textSecondary,
-        'textColorPlaceholder': AppColors.textPlaceholder,
+        'textColorPlaceholder': AppColors.gray400,
+        'textDisabledColor': AppColors.textDisabled,
         'bgColorPage': AppColors.bgPage,
         'bgColorContainer': AppColors.bgCard,
+        'bgColorSecondaryContainer': AppColors.bgDisabled,
         'borderColor': AppColors.border,
+        'brandLightColor': AppColors.primaryLight,
+        'componentStrokeColor': AppColors.border,
       },
       fontMap: _fontMap(),
     );
@@ -58,9 +56,13 @@ class AppTheme {
         'textColorPrimary': AppDarkColors.textPrimary,
         'textColorSecondary': AppDarkColors.textSecondary,
         'textColorPlaceholder': AppDarkColors.textPlaceholder,
+        'textDisabledColor': AppDarkColors.textDisabled,
         'bgColorPage': AppDarkColors.bgPage,
         'bgColorContainer': AppDarkColors.bgCard,
+        'bgColorSecondaryContainer': AppDarkColors.bgSecondaryContainer,
         'borderColor': AppDarkColors.border,
+        'brandLightColor': AppDarkColors.primaryLight,
+        'componentStrokeColor': AppDarkColors.border,
       },
       fontMap: _fontMap(),
     );

+ 2 - 2
lib/features/admin/admin_permissions_page.dart

@@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/i18n/app_localizations.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 
 /// 权限管理 - 页面3.2 【管理员专属】
 class AdminPermissionsPage extends ConsumerStatefulWidget {
@@ -202,7 +202,7 @@ class _AdminPermissionsPageState extends ConsumerState<AdminPermissionsPage> {
         child: Container(
           padding: const EdgeInsets.all(12),
           decoration: BoxDecoration(
-            color: emp.isActive ? colors.bgCard : const Color(0xFFF9F9F9),
+            color: emp.isActive ? colors.bgCard : colors.bgDisabled,
             borderRadius: BorderRadius.circular(8),
             boxShadow: const [
               BoxShadow(

+ 1 - 1
lib/features/announcement/announcement_create_page.dart

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/responsive.dart';
 import '../../shared/widgets/action_bar.dart';
 import '../../shared/widgets/form_section.dart';

+ 1 - 1
lib/features/announcement/announcement_detail_page.dart

@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/i18n/app_localizations.dart';
 import 'announcement_list_controller.dart';

+ 69 - 28
lib/features/announcement/announcement_list_page.dart

@@ -4,11 +4,12 @@ import 'package:go_router/go_router.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:easy_refresh/easy_refresh.dart';
 import '../../core/theme/app_colors_extension.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
 import '../../shared/widgets/empty_state.dart';
 import '../../shared/widgets/skeleton_list_card.dart';
+import '../../shared/widgets/list_footer.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../core/auth/role_provider.dart';
 import 'announcement_list_controller.dart';
@@ -25,18 +26,27 @@ class _AnnouncementListPageState extends ConsumerState<AnnouncementListPage>
     with TickerProviderStateMixin {
   late TabController _tabCtrl;
 
-  static List<String> _getTabLabels(bool isAdmin, AppLocalizations l10n) => isAdmin
-      ? [l10n.get('all'), l10n.get('noticeAnnouncement'), l10n.get('hrPolicy'), l10n.get('holidayActivity'), l10n.get('myDrafts')]
-      : [l10n.get('all'), l10n.get('noticeAnnouncement'), l10n.get('hrPolicy'), l10n.get('holidayActivity')];
+  static List<String> _getTabLabels(bool isAdmin, AppLocalizations l10n) =>
+      isAdmin
+      ? [
+          l10n.get('all'),
+          l10n.get('noticeAnnouncement'),
+          l10n.get('hrPolicy'),
+          l10n.get('holidayActivity'),
+          l10n.get('draft'),
+        ]
+      : [
+          l10n.get('all'),
+          l10n.get('noticeAnnouncement'),
+          l10n.get('hrPolicy'),
+          l10n.get('holidayActivity'),
+        ];
 
   @override
   void initState() {
     super.initState();
     final isAdmin = ref.read(isAdminProvider);
-    _tabCtrl = TabController(
-      length: isAdmin ? 5 : 4,
-      vsync: this,
-    );
+    _tabCtrl = TabController(length: isAdmin ? 5 : 4, vsync: this);
     _tabCtrl.addListener(_onTabChanged);
   }
 
@@ -127,6 +137,15 @@ class _AnnouncementListPageState extends ConsumerState<AnnouncementListPage>
           children: [
             Container(
               color: colors.bgCard,
+              padding: EdgeInsets.zero,
+              child: TDSearchBar(
+                placeHolder: l10n.get('searchAnnouncement'),
+                needCancel: true,
+                style: TDSearchStyle.round,
+              ),
+            ),
+            Container(
+              color: colors.bgCard,
               padding: const EdgeInsets.symmetric(horizontal: 8),
               child: TDTabBar(
                 tabs: tabs.map((l) => TDTab(text: l)).toList(),
@@ -231,8 +250,11 @@ class _AnnouncementTabContent extends ConsumerWidget {
 
     return ListView.builder(
       padding: const EdgeInsets.symmetric(vertical: 8),
-      itemCount: items.length,
-      itemBuilder: (_, i) => _buildAnnouncementCard(context, items[i]),
+      itemCount: items.length + 1,
+      itemBuilder: (_, i) {
+        if (i == items.length) return ListFooter(itemCount: items.length);
+        return _buildAnnouncementCard(context, items[i]);
+      },
     );
   }
 
@@ -249,9 +271,7 @@ class _AnnouncementTabContent extends ConsumerWidget {
           padding: const EdgeInsets.all(12),
           decoration: BoxDecoration(
             borderRadius: BorderRadius.circular(8),
-            color: expired
-                ? const Color(0xFFF9F9F9)
-                : colors.bgCard,
+            color: expired ? colors.bgDisabled : colors.bgCard,
           ),
           child: Column(
             crossAxisAlignment: CrossAxisAlignment.start,
@@ -264,14 +284,20 @@ class _AnnouncementTabContent extends ConsumerWidget {
                     Container(
                       margin: const EdgeInsets.only(right: 6),
                       padding: const EdgeInsets.symmetric(
-                          horizontal: 4, vertical: 1),
+                        horizontal: 4,
+                        vertical: 1,
+                      ),
                       decoration: BoxDecoration(
                         color: colors.danger,
                         borderRadius: BorderRadius.circular(2),
                       ),
-                      child: Text(l10n.get('pinTopTag'),
-                          style: const TextStyle(
-                              fontSize: 10, color: Colors.white)),
+                      child: Text(
+                        l10n.get('pinTopTag'),
+                        style: const TextStyle(
+                          fontSize: 10,
+                          color: Colors.white,
+                        ),
+                      ),
                     ),
                   // 标题
                   Expanded(
@@ -285,8 +311,7 @@ class _AnnouncementTabContent extends ConsumerWidget {
                         color: expired
                             ? colors.textPlaceholder
                             : colors.textPrimary,
-                        decoration:
-                            expired ? TextDecoration.lineThrough : null,
+                        decoration: expired ? TextDecoration.lineThrough : null,
                       ),
                     ),
                   ),
@@ -295,14 +320,20 @@ class _AnnouncementTabContent extends ConsumerWidget {
                     Container(
                       margin: const EdgeInsets.only(left: 6),
                       padding: const EdgeInsets.symmetric(
-                          horizontal: 6, vertical: 2),
+                        horizontal: 6,
+                        vertical: 2,
+                      ),
                       decoration: BoxDecoration(
                         color: colors.bgPage,
                         borderRadius: BorderRadius.circular(3),
                       ),
-                      child: Text(l10n.get('expired'),
-                          style: TextStyle(
-                              fontSize: 10, color: colors.textPlaceholder)),
+                      child: Text(
+                        l10n.get('expired'),
+                        style: TextStyle(
+                          fontSize: 10,
+                          color: colors.textPlaceholder,
+                        ),
+                      ),
                     ),
                   // 未读红点
                   if (!item.isRead && !expired)
@@ -329,14 +360,18 @@ class _AnnouncementTabContent extends ConsumerWidget {
                       Text(
                         item.publisherName,
                         style: TextStyle(
-                            fontSize: 12, color: colors.textSecondary),
+                          fontSize: 12,
+                          color: colors.textSecondary,
+                        ),
                       ),
                     ],
                   ),
                   Text(
                     du.DateUtils.formatDateTime(item.publishTime),
                     style: TextStyle(
-                        fontSize: 12, color: colors.textPlaceholder),
+                      fontSize: 12,
+                      color: colors.textPlaceholder,
+                    ),
                   ),
                 ],
               ),
@@ -347,7 +382,11 @@ class _AnnouncementTabContent extends ConsumerWidget {
     );
   }
 
-  Widget _buildTypeTag(BuildContext context, String type, AppLocalizations l10n) {
+  Widget _buildTypeTag(
+    BuildContext context,
+    String type,
+    AppLocalizations l10n,
+  ) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     Color bgColor;
     Color textColor;
@@ -375,8 +414,10 @@ class _AnnouncementTabContent extends ConsumerWidget {
         color: bgColor,
         borderRadius: BorderRadius.circular(3),
       ),
-      child:
-          Text(displayText, style: TextStyle(fontSize: 11, color: textColor)),
+      child: Text(
+        displayText,
+        style: TextStyle(fontSize: 11, color: textColor),
+      ),
     );
   }
 }

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

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:go_router/go_router.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/responsive.dart';
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';

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

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';

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

@@ -365,7 +365,7 @@ final mockExpenses = <ExpenseModel>[
     expenseType: '差旅费',
     totalAmount: 1800.00,
     invoiceCount: 2,
-    status: 'revoked',
+    status: 'withdrawn',
     purpose: '上海出差(已撤回)',
     accountBankName: '中国银行',
     accountHolderName: '张三',

+ 86 - 63
lib/features/expense/expense_list_page.dart

@@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:easy_refresh/easy_refresh.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
@@ -12,7 +12,8 @@ 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/filter_bar.dart';
+import '../../shared/widgets/list_filter_panel.dart';
+import '../../shared/widgets/list_footer.dart';
 import '../../core/i18n/app_localizations.dart';
 import 'expense_list_controller.dart';
 import 'expense_model.dart';
@@ -35,7 +36,7 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
     l10n.get('statusWaitPay'),
     l10n.get('paid'),
     l10n.get('rejected'),
-    l10n.get('revoked'),
+    l10n.get('withdrawn'),
   ];
   static const _tabKeys = [
     '',
@@ -45,7 +46,7 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
     'unpaid',
     'paid',
     'rejected',
-    'revoked',
+    'withdrawn',
   ];
   late final TabController _tabCtrl;
 
@@ -88,21 +89,11 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
 
     final filterGroups = [
       FilterGroup(
-        title: '日期范围',
+        title: l10n.get('filterDateRange'),
         type: FilterGroupType.dateRange,
         sections: [
           FilterSection(
-            label: '起始日期',
-            type: FilterSectionType.dateRange,
-            startDate: dateStart,
-            endDate: dateEnd,
-            onStartChanged: (v) =>
-                ref.read(expenseDateStartProvider.notifier).state = v,
-            onEndChanged: (v) =>
-                ref.read(expenseDateEndProvider.notifier).state = v,
-          ),
-          FilterSection(
-            label: '结束日期',
+            label: l10n.get('filterDateRange'),
             type: FilterSectionType.dateRange,
             startDate: dateStart,
             endDate: dateEnd,
@@ -114,7 +105,9 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
         ],
       ),
     ];
-    final hasFilter = FilterBar.hasActiveFilter(filterGroups);
+    final hasFilter = ListFilterPanel.hasActiveFilter(filterGroups);
+    final filterVersion = Object.hash(dateStart, dateEnd);
+    final now = DateTime.now();
     void onFilterReset() {
       ref.read(expenseDateStartProvider.notifier).state = null;
       ref.read(expenseDateEndProvider.notifier).state = null;
@@ -128,16 +121,20 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
             showBack: true,
             showRight: true,
             rightWidget: GestureDetector(
-              onTap: () => FilterBar.show(
+              onTap: () => ListFilterPanel.show(
                 context,
                 groups: filterGroups,
                 onReset: onFilterReset,
                 onConfirm: () {},
+                defaultStartDate: DateTime(now.year, now.month, 1),
+                defaultEndDate: DateTime(now.year, now.month, now.day),
               ),
               child: Stack(
+                clipBehavior: Clip.none,
                 children: [
                   Icon(
                     TDIcons.filter,
+                    size: 22,
                     color: hasFilter ? colors.primary : colors.textPrimary,
                   ),
                   if (hasFilter)
@@ -156,13 +153,28 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
                 ],
               ),
             ),
+            hasFilter: hasFilter,
+            filterVersion: filterVersion,
             onBack: () => context.pop(),
           ),
         );
     return Column(
       children: [
         if (isManager)
-          _buildScopeChip(colors),
+          Container(
+            color: colors.bgCard,
+            padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
+            child: _buildScopeChip(colors),
+          ),
+        Container(
+          color: colors.bgCard,
+          padding: EdgeInsets.zero,
+          child: TDSearchBar(
+            placeHolder: l10n.get('searchExpense'),
+            needCancel: true,
+            style: TDSearchStyle.round,
+          ),
+        ),
         Container(
           color: colors.bgCard,
           padding: const EdgeInsets.symmetric(horizontal: 8),
@@ -202,47 +214,47 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
 
   Widget _buildScopeChip(AppColorsExtension colors) {
     final scope = ref.watch(_scopeProvider);
-    return Padding(
-      padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
-      child: Row(
-        children: [
-          GestureDetector(
-            onTap: () => ref.read(_scopeProvider.notifier).state = 'my',
-            child: Container(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
-              decoration: BoxDecoration(
-                color: scope == 'my' ? colors.primary : colors.bgPage,
-                borderRadius: BorderRadius.circular(16),
-              ),
-              child: Text(
-                '我的发起',
-                style: TextStyle(
-                  fontSize: 13,
-                  color: scope == 'my' ? Colors.white : colors.textSecondary,
-                ),
+    final l10n = AppLocalizations.of(context);
+    return Row(
+      children: [
+        GestureDetector(
+          onTap: () => ref.read(_scopeProvider.notifier).state = 'my',
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+            decoration: BoxDecoration(
+              color: scope == 'my' ? colors.primary : colors.bgPage,
+              borderRadius: BorderRadius.circular(16),
+              border: scope == 'my' ? null : Border.all(color: colors.border),
+            ),
+            child: Text(
+              l10n.get('scopeMyApplications'),
+              style: TextStyle(
+                fontSize: 13,
+                color: scope == 'my' ? colors.bgCard : colors.textSecondary,
               ),
             ),
           ),
-          const SizedBox(width: 8),
-          GestureDetector(
-            onTap: () => ref.read(_scopeProvider.notifier).state = 'sub',
-            child: Container(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
-              decoration: BoxDecoration(
-                color: scope == 'sub' ? colors.primary : colors.bgPage,
-                borderRadius: BorderRadius.circular(16),
-              ),
-              child: Text(
-                '下属审批',
-                style: TextStyle(
-                  fontSize: 13,
-                  color: scope == 'sub' ? Colors.white : colors.textSecondary,
-                ),
+        ),
+        const SizedBox(width: 8),
+        GestureDetector(
+          onTap: () => ref.read(_scopeProvider.notifier).state = 'sub',
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+            decoration: BoxDecoration(
+              color: scope == 'sub' ? colors.primary : colors.bgPage,
+              borderRadius: BorderRadius.circular(16),
+              border: scope == 'sub' ? null : Border.all(color: colors.border),
+            ),
+            child: Text(
+              l10n.get('scopeSubordinates'),
+              style: TextStyle(
+                fontSize: 13,
+                color: scope == 'sub' ? colors.bgCard : colors.textSecondary,
               ),
             ),
           ),
-        ],
-      ),
+        ),
+      ],
     );
   }
 
@@ -349,8 +361,9 @@ class _ExpenseTabContent extends ConsumerWidget {
 
     return ListView.builder(
       padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
-      itemCount: items.length,
+      itemCount: items.length + 1,
       itemBuilder: (_, i) {
+        if (i == items.length) return ListFooter(itemCount: items.length);
         final desc = isSub
             ? '${items[i].expenseType} — ${items[i].applicantName}\n申请人: ${items[i].applicantName} · ${items[i].deptName}'
             : '${items[i].expenseType} — ${items[i].applicantName}';
@@ -368,10 +381,7 @@ class _ExpenseTabContent extends ConsumerWidget {
             child: _buildSwipeApprove(card, items[i].id),
           );
         }
-        return Padding(
-          padding: const EdgeInsets.only(bottom: 16),
-          child: card,
-        );
+        return Padding(padding: const EdgeInsets.only(bottom: 16), child: card);
       },
     );
   }
@@ -389,7 +399,10 @@ class _ExpenseTabContent extends ConsumerWidget {
                 label: '',
                 backgroundColor: Colors.transparent,
                 builder: (_) => Container(
-                  margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
+                  margin: const EdgeInsets.symmetric(
+                    horizontal: 4,
+                    vertical: 8,
+                  ),
                   decoration: BoxDecoration(
                     color: Colors.green,
                     borderRadius: BorderRadius.circular(8),
@@ -398,7 +411,11 @@ class _ExpenseTabContent extends ConsumerWidget {
                   padding: const EdgeInsets.symmetric(horizontal: 12),
                   child: const Text(
                     '一键同意',
-                    style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600),
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontSize: 14,
+                      fontWeight: FontWeight.w600,
+                    ),
                   ),
                 ),
                 onPressed: (_) async {
@@ -407,8 +424,14 @@ class _ExpenseTabContent extends ConsumerWidget {
                     builder: (dCtx) => TDAlertDialog(
                       title: '确认审批',
                       content: '确认同意该报销?',
-                      leftBtn: TDDialogButtonOptions(title: '取消', action: () => Navigator.of(dCtx).pop(false)),
-                      rightBtn: TDDialogButtonOptions(title: '确认', action: () => Navigator.of(dCtx).pop(true)),
+                      leftBtn: TDDialogButtonOptions(
+                        title: '取消',
+                        action: () => Navigator.of(dCtx).pop(false),
+                      ),
+                      rightBtn: TDDialogButtonOptions(
+                        title: '确认',
+                        action: () => Navigator.of(dCtx).pop(true),
+                      ),
                     ),
                   );
                   if (confirmed == true) {

+ 1 - 1
lib/features/expense_application/expense_application_apply_page.dart

@@ -6,7 +6,7 @@ import '../../core/i18n/app_localizations.dart';
 import '../../shared/widgets/action_bar.dart';
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors_extension.dart';
 

+ 1 - 1
lib/features/expense_application/expense_application_detail_page.dart

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/form_field_row.dart';

+ 1 - 1
lib/features/expense_application/expense_application_list_controller.dart

@@ -265,7 +265,7 @@ final mockExpenseApplications = <ExpenseApplicationModel>[
     estimatedAmount: 1500.00,
     purpose: '部门办公设备采购(已撤回)',
     remark: '',
-    status: 'revoked',
+    status: 'withdrawn',
     currentApproverId: 'U100',
     approvalChain: ['U100'],
     urgency: 'normal',

+ 118 - 75
lib/features/expense_application/expense_application_list_page.dart

@@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:easy_refresh/easy_refresh.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
@@ -12,7 +12,8 @@ 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/filter_bar.dart';
+import '../../shared/widgets/list_filter_panel.dart';
+import '../../shared/widgets/list_footer.dart';
 import '../../core/i18n/app_localizations.dart';
 import 'expense_application_list_controller.dart';
 import 'expense_application_model.dart';
@@ -35,7 +36,7 @@ class _ExpenseApplicationListPageState
     l10n.get('pending'),
     l10n.get('approved'),
     l10n.get('rejected'),
-    l10n.get('revoked'),
+    l10n.get('withdrawn'),
   ];
   static const _tabKeys = [
     '',
@@ -43,7 +44,7 @@ class _ExpenseApplicationListPageState
     'pending',
     'approved',
     'rejected',
-    'revoked',
+    'withdrawn',
   ];
 
   late final TabController _tabCtrl;
@@ -79,21 +80,11 @@ class _ExpenseApplicationListPageState
 
     final filterGroups = [
       FilterGroup(
-        title: '日期范围',
+        title: l10n.get('filterDateRange'),
         type: FilterGroupType.dateRange,
         sections: [
           FilterSection(
-            label: '起始日期',
-            type: FilterSectionType.dateRange,
-            startDate: dateStart,
-            endDate: dateEnd,
-            onStartChanged: (v) =>
-                ref.read(expenseApplyDateStartProvider.notifier).state = v,
-            onEndChanged: (v) =>
-                ref.read(expenseApplyDateEndProvider.notifier).state = v,
-          ),
-          FilterSection(
-            label: '结束日期',
+            label: l10n.get('filterDateRange'),
             type: FilterSectionType.dateRange,
             startDate: dateStart,
             endDate: dateEnd,
@@ -105,17 +96,29 @@ class _ExpenseApplicationListPageState
         ],
       ),
       FilterGroup(
-        title: '其它',
+        title: l10n.get('other'),
         type: FilterGroupType.other,
         sections: [
           FilterSection(
-            label: '费用类型',
+            label: l10n.get('expenseType'),
             type: FilterSectionType.multiSelect,
-            options: const [
-              FilterOption(value: 'travel', label: '差旅费'),
-              FilterOption(value: 'entertainment', label: '业务招待费'),
-              FilterOption(value: 'office', label: '办公费'),
-              FilterOption(value: 'meeting', label: '会议费'),
+            options: [
+              FilterOption(
+                value: 'travel',
+                label: l10n.get('filterExpenseTravel'),
+              ),
+              FilterOption(
+                value: 'entertainment',
+                label: l10n.get('filterExpenseEntertainment'),
+              ),
+              FilterOption(
+                value: 'office',
+                label: l10n.get('filterExpenseOffice'),
+              ),
+              FilterOption(
+                value: 'meeting',
+                label: l10n.get('filterExpenseMeeting'),
+              ),
             ],
             selectedValues: typeFilter != null ? [typeFilter] : null,
             onMultiChanged: (v) =>
@@ -123,12 +126,15 @@ class _ExpenseApplicationListPageState
                     v.isNotEmpty ? v.first : null,
           ),
           FilterSection(
-            label: '紧急程度',
+            label: l10n.get('emergencyLevel'),
             type: FilterSectionType.singleSelect,
-            options: const [
-              FilterOption(value: 'normal', label: '普通'),
-              FilterOption(value: 'urgent', label: '紧急'),
-              FilterOption(value: 'critical', label: '特急'),
+            options: [
+              FilterOption(value: 'normal', label: l10n.get('normal')),
+              FilterOption(value: 'urgent', label: l10n.get('urgent')),
+              FilterOption(
+                value: 'critical',
+                label: l10n.get('filterCritical'),
+              ),
             ],
             selectedValue: urgencyFilter,
             onChanged: (v) =>
@@ -137,7 +143,14 @@ class _ExpenseApplicationListPageState
         ],
       ),
     ];
-    final hasFilter = FilterBar.hasActiveFilter(filterGroups);
+    final hasFilter = ListFilterPanel.hasActiveFilter(filterGroups);
+    final filterVersion = Object.hash(
+      dateStart,
+      dateEnd,
+      typeFilter,
+      urgencyFilter,
+    );
+    final now = DateTime.now();
     void onFilterReset() {
       ref.read(expenseApplyDateStartProvider.notifier).state = null;
       ref.read(expenseApplyDateEndProvider.notifier).state = null;
@@ -153,16 +166,20 @@ class _ExpenseApplicationListPageState
             showBack: true,
             showRight: true,
             rightWidget: GestureDetector(
-              onTap: () => FilterBar.show(
+              onTap: () => ListFilterPanel.show(
                 context,
                 groups: filterGroups,
                 onReset: onFilterReset,
                 onConfirm: () {},
+                defaultStartDate: DateTime(now.year, now.month, 1),
+                defaultEndDate: DateTime(now.year, now.month, now.day),
               ),
               child: Stack(
+                clipBehavior: Clip.none,
                 children: [
                   Icon(
                     TDIcons.filter,
+                    size: 22,
                     color: hasFilter ? colors.primary : colors.textPrimary,
                   ),
                   if (hasFilter)
@@ -181,6 +198,8 @@ class _ExpenseApplicationListPageState
                 ],
               ),
             ),
+            hasFilter: hasFilter,
+            filterVersion: filterVersion,
             onBack: () => context.pop(),
           ),
         );
@@ -198,7 +217,20 @@ class _ExpenseApplicationListPageState
     return Column(
       children: [
         if (isManager)
-          _buildScopeChip(colors),
+          Container(
+            color: colors.bgCard,
+            padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
+            child: _buildScopeChip(colors),
+          ),
+        Container(
+          color: colors.bgCard,
+          padding: EdgeInsets.zero,
+          child: TDSearchBar(
+            placeHolder: l10n.get('searchExpenseApply'),
+            needCancel: true,
+            style: TDSearchStyle.round,
+          ),
+        ),
         Container(
           color: colors.bgCard,
           padding: const EdgeInsets.symmetric(horizontal: 8),
@@ -238,47 +270,47 @@ class _ExpenseApplicationListPageState
 
   Widget _buildScopeChip(AppColorsExtension colors) {
     final scope = ref.watch(_scopeProvider);
-    return Padding(
-      padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
-      child: Row(
-        children: [
-          GestureDetector(
-            onTap: () => ref.read(_scopeProvider.notifier).state = 'my',
-            child: Container(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
-              decoration: BoxDecoration(
-                color: scope == 'my' ? colors.primary : colors.bgPage,
-                borderRadius: BorderRadius.circular(16),
-              ),
-              child: Text(
-                '我的发起',
-                style: TextStyle(
-                  fontSize: 13,
-                  color: scope == 'my' ? Colors.white : colors.textSecondary,
-                ),
+    final l10n = AppLocalizations.of(context);
+    return Row(
+      children: [
+        GestureDetector(
+          onTap: () => ref.read(_scopeProvider.notifier).state = 'my',
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+            decoration: BoxDecoration(
+              color: scope == 'my' ? colors.primary : colors.bgPage,
+              borderRadius: BorderRadius.circular(16),
+              border: scope == 'my' ? null : Border.all(color: colors.border),
+            ),
+            child: Text(
+              l10n.get('scopeMyApplications'),
+              style: TextStyle(
+                fontSize: 13,
+                color: scope == 'my' ? colors.bgCard : colors.textSecondary,
               ),
             ),
           ),
-          const SizedBox(width: 8),
-          GestureDetector(
-            onTap: () => ref.read(_scopeProvider.notifier).state = 'sub',
-            child: Container(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
-              decoration: BoxDecoration(
-                color: scope == 'sub' ? colors.primary : colors.bgPage,
-                borderRadius: BorderRadius.circular(16),
-              ),
-              child: Text(
-                '下属审批',
-                style: TextStyle(
-                  fontSize: 13,
-                  color: scope == 'sub' ? Colors.white : colors.textSecondary,
-                ),
+        ),
+        const SizedBox(width: 8),
+        GestureDetector(
+          onTap: () => ref.read(_scopeProvider.notifier).state = 'sub',
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+            decoration: BoxDecoration(
+              color: scope == 'sub' ? colors.primary : colors.bgPage,
+              borderRadius: BorderRadius.circular(16),
+              border: scope == 'sub' ? null : Border.all(color: colors.border),
+            ),
+            child: Text(
+              l10n.get('scopeSubordinates'),
+              style: TextStyle(
+                fontSize: 13,
+                color: scope == 'sub' ? colors.bgCard : colors.textSecondary,
               ),
             ),
           ),
-        ],
-      ),
+        ),
+      ],
     );
   }
 
@@ -391,8 +423,9 @@ class _ExpenseApplyList extends ConsumerWidget {
 
     return ListView.builder(
       padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
-      itemCount: items.length,
+      itemCount: items.length + 1,
       itemBuilder: (_, i) {
+        if (i == items.length) return ListFooter(itemCount: items.length);
         final desc = isSub
             ? '${items[i].expenseType} — ${items[i].purpose}\n申请人: ${items[i].applicantName} · ${items[i].deptName}'
             : '${items[i].expenseType} — ${items[i].purpose}';
@@ -410,10 +443,7 @@ class _ExpenseApplyList extends ConsumerWidget {
             child: _buildSwipeApprove(card, items[i].id),
           );
         }
-        return Padding(
-          padding: const EdgeInsets.only(bottom: 16),
-          child: card,
-        );
+        return Padding(padding: const EdgeInsets.only(bottom: 16), child: card);
       },
     );
   }
@@ -431,7 +461,10 @@ class _ExpenseApplyList extends ConsumerWidget {
                 label: '',
                 backgroundColor: Colors.transparent,
                 builder: (_) => Container(
-                  margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
+                  margin: const EdgeInsets.symmetric(
+                    horizontal: 4,
+                    vertical: 8,
+                  ),
                   decoration: BoxDecoration(
                     color: Colors.green,
                     borderRadius: BorderRadius.circular(8),
@@ -440,7 +473,11 @@ class _ExpenseApplyList extends ConsumerWidget {
                   padding: const EdgeInsets.symmetric(horizontal: 12),
                   child: const Text(
                     '一键同意',
-                    style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600),
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontSize: 14,
+                      fontWeight: FontWeight.w600,
+                    ),
                   ),
                 ),
                 onPressed: (_) async {
@@ -449,8 +486,14 @@ class _ExpenseApplyList extends ConsumerWidget {
                     builder: (dCtx) => TDAlertDialog(
                       title: '确认审批',
                       content: '确认同意该申请?',
-                      leftBtn: TDDialogButtonOptions(title: '取消', action: () => Navigator.of(dCtx).pop(false)),
-                      rightBtn: TDDialogButtonOptions(title: '确认', action: () => Navigator.of(dCtx).pop(true)),
+                      leftBtn: TDDialogButtonOptions(
+                        title: '取消',
+                        action: () => Navigator.of(dCtx).pop(false),
+                      ),
+                      rightBtn: TDDialogButtonOptions(
+                        title: '确认',
+                        action: () => Navigator.of(dCtx).pop(true),
+                      ),
                     ),
                   );
                   if (confirmed == true) {

+ 1 - 1
lib/features/home/home_page.dart

@@ -5,7 +5,7 @@ import 'package:go_router/go_router.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import '../../shared/widgets/section_card.dart';
 import '../../core/i18n/app_localizations.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import 'home_controller.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors_extension.dart';

+ 1 - 1
lib/features/messages/message_list_page.dart

@@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import '../../shared/widgets/empty_state.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../shared/widgets/message_item.dart';
 import '../../shared/widgets/skeleton_list_card.dart';

+ 1 - 1
lib/features/outing_log/outing_log_create_page.dart

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/responsive.dart';
 import '../../shared/widgets/form_section.dart';
 import '../../shared/widgets/action_bar.dart';

+ 10 - 7
lib/features/outing_log/outing_log_detail_page.dart

@@ -1,7 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/i18n/app_localizations.dart';
 import 'outing_log_list_controller.dart';
@@ -151,13 +151,13 @@ class _OutingLogDetailPageState extends ConsumerState<OutingLogDetailPage> {
       padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
       child: GestureDetector(
         onTap: () {
-          ScaffoldMessenger.of(
-            context,
-          ).showSnackBar(SnackBar(content: Text(l10n.get('mockOpenNavigation'))));
+          ScaffoldMessenger.of(context).showSnackBar(
+            SnackBar(content: Text(l10n.get('mockOpenNavigation'))),
+          );
         },
         child: Container(
           decoration: BoxDecoration(
-            color: const Color(0xFFE8F4FD),
+            color: colors.infoBg,
             borderRadius: BorderRadius.circular(8),
           ),
           child: Stack(
@@ -232,7 +232,10 @@ class _OutingLogDetailPageState extends ConsumerState<OutingLogDetailPage> {
             Divider(height: 12, color: colors.border),
             _buildInfoRow(l10n.get('checkInAddress'), _log.checkInAddress),
             Divider(height: 12, color: colors.border),
-            _buildInfoRow(l10n.get('checkInTime'), du.DateUtils.formatDateTime(_log.createTime)),
+            _buildInfoRow(
+              l10n.get('checkInTime'),
+              du.DateUtils.formatDateTime(_log.createTime),
+            ),
           ],
         ),
       ),
@@ -348,7 +351,7 @@ class _OutingLogDetailPageState extends ConsumerState<OutingLogDetailPage> {
                           width: 100,
                           height: 100,
                           decoration: BoxDecoration(
-                            color: const Color(0xFFD0E8F8),
+                            color: colors.primaryLight,
                             borderRadius: BorderRadius.circular(4),
                           ),
                           child: Center(

+ 92 - 87
lib/features/outing_log/outing_log_list_page.dart

@@ -4,17 +4,21 @@ import 'package:go_router/go_router.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:easy_refresh/easy_refresh.dart';
 import '../../core/theme/app_colors_extension.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
 import '../../shared/widgets/status_tag.dart';
 import '../../shared/widgets/empty_state.dart';
 import '../../shared/widgets/skeleton_list_card.dart';
-import '../../shared/widgets/filter_bar.dart';
+import '../../shared/widgets/list_filter_panel.dart';
+import '../../shared/widgets/list_footer.dart';
 import '../../core/i18n/app_localizations.dart';
+import '../../core/auth/role_provider.dart';
 import 'outing_log_list_controller.dart';
 import 'outing_log_model.dart';
 
+final _scopeProvider = StateProvider<String>((ref) => 'my');
+
 class OutingLogListPage extends ConsumerStatefulWidget {
   const OutingLogListPage({super.key});
   @override
@@ -23,7 +27,6 @@ class OutingLogListPage extends ConsumerStatefulWidget {
 
 class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
     with TickerProviderStateMixin {
-  final bool _isManager = true; // 模拟经理角色
   static const _tabKeys = ['', 'draft', 'completed'];
   List<String> _getTabLabels(AppLocalizations l10n) => [
     l10n.get('all'),
@@ -55,10 +58,10 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
     final tabIndex = ref.watch(outingLogTabProvider);
     final dateStart = ref.watch(outingLogDateStartProvider);
     final dateEnd = ref.watch(outingLogDateEndProvider);
-    final scopeIndex = ref.watch(outingLogScopeProvider);
     final r = ResponsiveHelper.of(context);
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final isManager = ref.watch(isManagerProvider);
 
     // Sync TabController with external filter changes
     if (_tabCtrl.index != tabIndex && !_tabCtrl.indexIsChanging) {
@@ -69,21 +72,11 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
 
     final filterGroups = [
       FilterGroup(
-        title: '日期范围',
+        title: l10n.get('filterDateRange'),
         type: FilterGroupType.dateRange,
         sections: [
           FilterSection(
-            label: '起始日期',
-            type: FilterSectionType.dateRange,
-            startDate: dateStart,
-            endDate: dateEnd,
-            onStartChanged: (v) =>
-                ref.read(outingLogDateStartProvider.notifier).state = v,
-            onEndChanged: (v) =>
-                ref.read(outingLogDateEndProvider.notifier).state = v,
-          ),
-          FilterSection(
-            label: '结束日期',
+            label: l10n.get('filterDateRange'),
             type: FilterSectionType.dateRange,
             startDate: dateStart,
             endDate: dateEnd,
@@ -95,7 +88,9 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
         ],
       ),
     ];
-    final hasFilter = FilterBar.hasActiveFilter(filterGroups);
+    final hasFilter = ListFilterPanel.hasActiveFilter(filterGroups);
+    final filterVersion = Object.hash(dateStart, dateEnd);
+    final now = DateTime.now();
     void onFilterReset() {
       ref.read(outingLogDateStartProvider.notifier).state = null;
       ref.read(outingLogDateEndProvider.notifier).state = null;
@@ -109,16 +104,20 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
             showBack: true,
             showRight: true,
             rightWidget: GestureDetector(
-              onTap: () => FilterBar.show(
+              onTap: () => ListFilterPanel.show(
                 context,
                 groups: filterGroups,
                 onReset: onFilterReset,
                 onConfirm: () {},
+                defaultStartDate: DateTime(now.year, now.month, 1),
+                defaultEndDate: DateTime(now.year, now.month, now.day),
               ),
               child: Stack(
+                clipBehavior: Clip.none,
                 children: [
                   Icon(
                     TDIcons.filter,
+                    size: 22,
                     color: hasFilter ? colors.primary : colors.textPrimary,
                   ),
                   if (hasFilter)
@@ -137,6 +136,8 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
                 ],
               ),
             ),
+            hasFilter: hasFilter,
+            filterVersion: filterVersion,
             onBack: () => context.pop(),
           ),
         );
@@ -145,7 +146,21 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
         constraints: BoxConstraints(maxWidth: r.listMaxWidth),
         child: Column(
           children: [
-            if (_isManager) _buildScopeChip(scopeIndex, l10n),
+            if (isManager)
+              Container(
+                color: colors.bgCard,
+                padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
+                child: _buildScopeChip(colors),
+              ),
+            Container(
+              color: colors.bgCard,
+              padding: EdgeInsets.zero,
+              child: TDSearchBar(
+                placeHolder: l10n.get('searchOutingLog'),
+                needCancel: true,
+                style: TDSearchStyle.round,
+              ),
+            ),
             Container(
               color: colors.bgCard,
               padding: const EdgeInsets.symmetric(horizontal: 8),
@@ -173,10 +188,7 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
                 child: TabBarView(
                   controller: _tabCtrl,
                   children: List.generate(_tabKeys.length, (tabIdx) {
-                    return _OutingLogTabContent(
-                      tabIndex: tabIdx,
-                      isManager: _isManager,
-                    );
+                    return _OutingLogTabContent(tabIndex: tabIdx);
                   }),
                 ),
               ),
@@ -187,63 +199,57 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
     );
   }
 
-  Widget _buildScopeChip(int scopeIndex, AppLocalizations l10n) {
-    final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    return Container(
-      width: double.infinity,
-      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
-      decoration: BoxDecoration(color: colors.bgCard),
-      child: Row(
-        children: [
-          _buildChip(l10n.get('myApplications'), 0, scopeIndex),
-          const SizedBox(width: 12),
-          _buildChip(l10n.get('subordinateRecords'), 1, scopeIndex),
-        ],
-      ),
-    );
-  }
-
-  Widget _buildChip(String label, int index, int current) {
-    final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    final selected = index == current;
-    return GestureDetector(
-      onTap: () => ref.read(outingLogScopeProvider.notifier).state = index,
-      child: Container(
-        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
-        decoration: BoxDecoration(
-          color: selected ? colors.primaryLight : colors.bgPage,
-          borderRadius: BorderRadius.circular(16),
-          border: Border.all(color: selected ? colors.primary : colors.border),
+  Widget _buildScopeChip(AppColorsExtension colors) {
+    final scope = ref.watch(_scopeProvider);
+    final l10n = AppLocalizations.of(context);
+    return Row(
+      children: [
+        GestureDetector(
+          onTap: () => ref.read(_scopeProvider.notifier).state = 'my',
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+            decoration: BoxDecoration(
+              color: scope == 'my' ? colors.primary : colors.bgPage,
+              borderRadius: BorderRadius.circular(16),
+              border: scope == 'my' ? null : Border.all(color: colors.border),
+            ),
+            child: Text(
+              l10n.get('scopeMyApplications'),
+              style: TextStyle(
+                fontSize: 13,
+                color: scope == 'my' ? colors.bgCard : colors.textSecondary,
+              ),
+            ),
+          ),
         ),
-        child: Text(
-          label,
-          style: TextStyle(
-            fontSize: 13,
-            fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
-            color: selected ? colors.primary : colors.textSecondary,
+        const SizedBox(width: 8),
+        GestureDetector(
+          onTap: () => ref.read(_scopeProvider.notifier).state = 'sub',
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+            decoration: BoxDecoration(
+              color: scope == 'sub' ? colors.primary : colors.bgPage,
+              borderRadius: BorderRadius.circular(16),
+              border: scope == 'sub' ? null : Border.all(color: colors.border),
+            ),
+            child: Text(
+              l10n.get('scopeSubordinates'),
+              style: TextStyle(
+                fontSize: 13,
+                color: scope == 'sub' ? colors.bgCard : colors.textSecondary,
+              ),
+            ),
           ),
         ),
-      ),
+      ],
     );
   }
 }
 
 class _OutingLogTabContent extends ConsumerWidget {
   final int tabIndex;
-  final bool isManager;
 
-  const _OutingLogTabContent({required this.tabIndex, required this.isManager});
-
-  static String _statusLabel(String status, AppLocalizations l10n) {
-    switch (status) {
-      case 'draft':
-        return l10n.get('draft');
-      case 'completed':
-        return l10n.get('completed');
-      default:
-        return status;
-    }
-  }
+  const _OutingLogTabContent({required this.tabIndex});
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
@@ -318,8 +324,11 @@ class _OutingLogTabContent extends ConsumerWidget {
 
     return ListView.builder(
       padding: const EdgeInsets.symmetric(vertical: 8),
-      itemCount: items.length,
-      itemBuilder: (_, i) => _buildOutingLogItem(context, ref, items[i]),
+      itemCount: items.length + 1,
+      itemBuilder: (_, i) {
+        if (i == items.length) return ListFooter(itemCount: items.length);
+        return _buildOutingLogItem(context, ref, items[i]);
+      },
     );
   }
 
@@ -328,9 +337,10 @@ class _OutingLogTabContent extends ConsumerWidget {
     WidgetRef ref,
     OutingLogModel item,
   ) {
+    final isManager = ref.watch(isManagerProvider);
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    final scopeIndex = ref.watch(outingLogScopeProvider);
+    final scope = ref.watch(_scopeProvider);
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
       child: GestureDetector(
@@ -387,29 +397,24 @@ class _OutingLogTabContent extends ConsumerWidget {
                             ),
                           ),
                         ),
-                      StatusTag(
-                        text: _statusLabel(item.status, l10n),
-                        textColor: item.isCompleted
-                            ? colors.success
-                            : colors.statusGray,
-                        backgroundColor: item.isCompleted
-                            ? colors.successBg
-                            : colors.bgPage,
-                      ),
+                      StatusTag.fromStatus(item.status, l10n),
                     ],
                   ),
                 ],
               ),
               const SizedBox(height: 4),
               // 经理增量:业务员姓名
-              if (isManager && scopeIndex == 1)
+              if (isManager && scope == 'sub')
                 Padding(
                   padding: const EdgeInsets.only(bottom: 4),
                   child: Text(
-                    l10n.getString('salespersonLabel', args: {
-                      'name': item.salespersonName,
-                      'dept': item.deptName,
-                    }),
+                    l10n.getString(
+                      'salespersonLabel',
+                      args: {
+                        'name': item.salespersonName,
+                        'dept': item.deptName,
+                      },
+                    ),
                     style: TextStyle(fontSize: 12, color: colors.primary),
                   ),
                 ),

+ 1 - 1
lib/features/overtime/overtime_apply_page.dart

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../shared/widgets/action_bar.dart';
 import '../../shared/widgets/form_section.dart';

+ 3 - 3
lib/features/overtime/overtime_detail_page.dart

@@ -1,7 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../shared/widgets/status_banner.dart';
 import '../../shared/widgets/approval_timeline.dart';
@@ -248,8 +248,8 @@ class OvertimeDetailPage extends ConsumerWidget {
         return (Icons.cancel, colors.danger, l10n.get('rejected'));
       case 'draft':
         return (Icons.edit_note, colors.statusGray, l10n.get('draft'));
-      case 'revoked':
-        return (Icons.cancel_outlined, colors.revokedText, l10n.get('revoked'));
+      case 'withdrawn':
+        return (Icons.cancel_outlined, colors.withdrawnText, l10n.get('withdrawn'));
       default:
         return (Icons.access_time, colors.warning, l10n.get('pending'));
     }

+ 110 - 70
lib/features/overtime/overtime_list_page.dart

@@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:easy_refresh/easy_refresh.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/auth/role_provider.dart';
@@ -11,7 +11,8 @@ 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/filter_bar.dart';
+import '../../shared/widgets/list_filter_panel.dart';
+import '../../shared/widgets/list_footer.dart';
 import '../../core/i18n/app_localizations.dart';
 import 'overtime_list_controller.dart';
 import 'overtime_model.dart';
@@ -32,9 +33,16 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
     l10n.get('pending'),
     l10n.get('approved'),
     l10n.get('rejected'),
-    l10n.get('revoked'),
+    l10n.get('withdrawn'),
+  ];
+  static const _tabKeys = [
+    '',
+    'draft',
+    'pending',
+    'approved',
+    'rejected',
+    'withdrawn',
   ];
-  static const _tabKeys = ['', 'draft', 'pending', 'approved', 'rejected', 'withdrawn'];
 
   late final TabController _tabCtrl;
 
@@ -78,21 +86,11 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
 
     final filterGroups = [
       FilterGroup(
-        title: '日期范围',
+        title: l10n.get('filterDateRange'),
         type: FilterGroupType.dateRange,
         sections: [
           FilterSection(
-            label: '起始日期',
-            type: FilterSectionType.dateRange,
-            startDate: dateStart,
-            endDate: dateEnd,
-            onStartChanged: (v) =>
-                ref.read(overtimeDateStartProvider.notifier).state = v,
-            onEndChanged: (v) =>
-                ref.read(overtimeDateEndProvider.notifier).state = v,
-          ),
-          FilterSection(
-            label: '结束日期',
+            label: l10n.get('filterDateRange'),
             type: FilterSectionType.dateRange,
             startDate: dateStart,
             endDate: dateEnd,
@@ -104,16 +102,25 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
         ],
       ),
       FilterGroup(
-        title: '其它',
+        title: l10n.get('other'),
         type: FilterGroupType.other,
         sections: [
           FilterSection(
-            label: '加班类型',
+            label: l10n.get('overtimeType'),
             type: FilterSectionType.singleSelect,
-            options: const [
-              FilterOption(value: 'workday', label: '工作日加班'),
-              FilterOption(value: 'weekend', label: '休息日加班'),
-              FilterOption(value: 'holiday', label: '节假日加班'),
+            options: [
+              FilterOption(
+                value: 'workday',
+                label: l10n.get('workdayOvertime'),
+              ),
+              FilterOption(
+                value: 'weekend',
+                label: l10n.get('weekendOvertime'),
+              ),
+              FilterOption(
+                value: 'holiday',
+                label: l10n.get('holidayOvertime'),
+              ),
             ],
             selectedValue: otTypeFilter,
             onChanged: (v) =>
@@ -122,7 +129,9 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
         ],
       ),
     ];
-    final hasFilter = FilterBar.hasActiveFilter(filterGroups);
+    final hasFilter = ListFilterPanel.hasActiveFilter(filterGroups);
+    final filterVersion = Object.hash(dateStart, dateEnd, otTypeFilter);
+    final now = DateTime.now();
     void onFilterReset() {
       ref.read(overtimeDateStartProvider.notifier).state = null;
       ref.read(overtimeDateEndProvider.notifier).state = null;
@@ -137,16 +146,20 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
             showBack: true,
             showRight: true,
             rightWidget: GestureDetector(
-              onTap: () => FilterBar.show(
+              onTap: () => ListFilterPanel.show(
                 context,
                 groups: filterGroups,
                 onReset: onFilterReset,
                 onConfirm: () {},
+                defaultStartDate: DateTime(now.year, now.month, 1),
+                defaultEndDate: DateTime(now.year, now.month, now.day),
               ),
               child: Stack(
+                clipBehavior: Clip.none,
                 children: [
                   Icon(
                     TDIcons.filter,
+                    size: 22,
                     color: hasFilter ? colors.primary : colors.textPrimary,
                   ),
                   if (hasFilter)
@@ -165,13 +178,28 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
                 ],
               ),
             ),
+            hasFilter: hasFilter,
+            filterVersion: filterVersion,
             onBack: () => context.pop(),
           ),
         );
     return Column(
       children: [
         if (isManager)
-          _buildScopeChip(colors),
+          Container(
+            color: colors.bgCard,
+            padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
+            child: _buildScopeChip(colors),
+          ),
+        Container(
+          color: colors.bgCard,
+          padding: EdgeInsets.zero,
+          child: TDSearchBar(
+            placeHolder: l10n.get('searchOvertime'),
+            needCancel: true,
+            style: TDSearchStyle.round,
+          ),
+        ),
         Container(
           color: colors.bgCard,
           padding: const EdgeInsets.symmetric(horizontal: 8),
@@ -211,47 +239,47 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
 
   Widget _buildScopeChip(AppColorsExtension colors) {
     final scope = ref.watch(_scopeProvider);
-    return Padding(
-      padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
-      child: Row(
-        children: [
-          GestureDetector(
-            onTap: () => ref.read(_scopeProvider.notifier).state = 'my',
-            child: Container(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
-              decoration: BoxDecoration(
-                color: scope == 'my' ? colors.primary : colors.bgPage,
-                borderRadius: BorderRadius.circular(16),
-              ),
-              child: Text(
-                '我的发起',
-                style: TextStyle(
-                  fontSize: 13,
-                  color: scope == 'my' ? Colors.white : colors.textSecondary,
-                ),
+    final l10n = AppLocalizations.of(context);
+    return Row(
+      children: [
+        GestureDetector(
+          onTap: () => ref.read(_scopeProvider.notifier).state = 'my',
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+            decoration: BoxDecoration(
+              color: scope == 'my' ? colors.primary : colors.bgPage,
+              borderRadius: BorderRadius.circular(16),
+              border: scope == 'my' ? null : Border.all(color: colors.border),
+            ),
+            child: Text(
+              l10n.get('scopeMyApplications'),
+              style: TextStyle(
+                fontSize: 13,
+                color: scope == 'my' ? colors.bgCard : colors.textSecondary,
               ),
             ),
           ),
-          const SizedBox(width: 8),
-          GestureDetector(
-            onTap: () => ref.read(_scopeProvider.notifier).state = 'sub',
-            child: Container(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
-              decoration: BoxDecoration(
-                color: scope == 'sub' ? colors.primary : colors.bgPage,
-                borderRadius: BorderRadius.circular(16),
-              ),
-              child: Text(
-                '下属审批',
-                style: TextStyle(
-                  fontSize: 13,
-                  color: scope == 'sub' ? Colors.white : colors.textSecondary,
-                ),
+        ),
+        const SizedBox(width: 8),
+        GestureDetector(
+          onTap: () => ref.read(_scopeProvider.notifier).state = 'sub',
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+            decoration: BoxDecoration(
+              color: scope == 'sub' ? colors.primary : colors.bgPage,
+              borderRadius: BorderRadius.circular(16),
+              border: scope == 'sub' ? null : Border.all(color: colors.border),
+            ),
+            child: Text(
+              l10n.get('scopeSubordinates'),
+              style: TextStyle(
+                fontSize: 13,
+                color: scope == 'sub' ? colors.bgCard : colors.textSecondary,
               ),
             ),
           ),
-        ],
-      ),
+        ),
+      ],
     );
   }
 
@@ -313,7 +341,8 @@ class _OvertimeTabContent extends ConsumerWidget {
           final card = ListCard(
             cardNo: oldItems[i].applicationNo,
             description: desc,
-            amount: '${oldItems[i].otHours.toStringAsFixed(1)}${l10n.get('hours')}',
+            amount:
+                '${oldItems[i].otHours.toStringAsFixed(1)}${l10n.get('hours')}',
             amountColor: colors.textPrimary,
             date: du.DateUtils.formatDate(oldItems[i].otDate),
             statusTag: StatusTag.fromStatus(oldItems[i].status, l10n),
@@ -354,8 +383,9 @@ class _OvertimeTabContent extends ConsumerWidget {
 
     return ListView.builder(
       padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
-      itemCount: items.length,
+      itemCount: items.length + 1,
       itemBuilder: (_, i) {
+        if (i == items.length) return ListFooter(itemCount: items.length);
         final desc = isSub
             ? '${items[i].otType} · ${items[i].compensationType}\n申请人: ${items[i].applicantName} · ${items[i].deptName}'
             : '${items[i].otType} · ${items[i].compensationType}';
@@ -374,10 +404,7 @@ class _OvertimeTabContent extends ConsumerWidget {
             child: _buildSwipeApprove(card, items[i].id),
           );
         }
-        return Padding(
-          padding: const EdgeInsets.only(bottom: 16),
-          child: card,
-        );
+        return Padding(padding: const EdgeInsets.only(bottom: 16), child: card);
       },
     );
   }
@@ -395,7 +422,10 @@ class _OvertimeTabContent extends ConsumerWidget {
                 label: '',
                 backgroundColor: Colors.transparent,
                 builder: (_) => Container(
-                  margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
+                  margin: const EdgeInsets.symmetric(
+                    horizontal: 4,
+                    vertical: 8,
+                  ),
                   decoration: BoxDecoration(
                     color: Colors.green,
                     borderRadius: BorderRadius.circular(8),
@@ -404,7 +434,11 @@ class _OvertimeTabContent extends ConsumerWidget {
                   padding: const EdgeInsets.symmetric(horizontal: 12),
                   child: const Text(
                     '一键同意',
-                    style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600),
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontSize: 14,
+                      fontWeight: FontWeight.w600,
+                    ),
                   ),
                 ),
                 onPressed: (_) async {
@@ -413,8 +447,14 @@ class _OvertimeTabContent extends ConsumerWidget {
                     builder: (dCtx) => TDAlertDialog(
                       title: '确认审批',
                       content: '确认同意该加班申请?',
-                      leftBtn: TDDialogButtonOptions(title: '取消', action: () => Navigator.of(dCtx).pop(false)),
-                      rightBtn: TDDialogButtonOptions(title: '确认', action: () => Navigator.of(dCtx).pop(true)),
+                      leftBtn: TDDialogButtonOptions(
+                        title: '取消',
+                        action: () => Navigator.of(dCtx).pop(false),
+                      ),
+                      rightBtn: TDDialogButtonOptions(
+                        title: '确认',
+                        action: () => Navigator.of(dCtx).pop(true),
+                      ),
                     ),
                   );
                   if (confirmed == true) {

+ 1 - 1
lib/features/profile/profile_page.dart

@@ -7,7 +7,7 @@ import '../../core/i18n/locale_provider.dart';
 import '../../core/theme/theme_mode_provider.dart';
 import '../../core/auth/role_provider.dart';
 import '../../shared/widgets/profile_menu_item.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors_extension.dart';
 

+ 2 - 2
lib/features/report/expense_apply_detail_report_page.dart

@@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:fl_chart/fl_chart.dart';
 import '../../core/i18n/app_localizations.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/auth/role_provider.dart';
 
@@ -219,7 +219,7 @@ class _ExpenseApplyDetailReportPageState
       l10n.get('filterAll'),
       l10n.get('approved'),
       l10n.get('rejected'),
-      l10n.get('revoked'),
+      l10n.get('withdrawn'),
     ];
     return Container(
       width: double.infinity,

+ 1 - 1
lib/features/report/expense_detail_report_page.dart

@@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:fl_chart/fl_chart.dart';
 import '../../core/i18n/app_localizations.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/auth/role_provider.dart';
 

+ 1 - 1
lib/features/report/outing_log_report_page.dart

@@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:fl_chart/fl_chart.dart';
 import '../../core/i18n/app_localizations.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/auth/role_provider.dart';
 

+ 1 - 1
lib/features/report/overtime_detail_report_page.dart

@@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:fl_chart/fl_chart.dart';
 import '../../core/i18n/app_localizations.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/auth/role_provider.dart';
 

+ 1 - 1
lib/features/report/vehicle_detail_report_page.dart

@@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:fl_chart/fl_chart.dart';
 import '../../core/i18n/app_localizations.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/auth/role_provider.dart';
 

+ 0 - 236
lib/features/shell/app_shell.dart

@@ -1,236 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:go_router/go_router.dart';
-import 'package:tdesign_flutter/tdesign_flutter.dart';
-import '../../core/i18n/app_localizations.dart';
-import 'nav_bar_config.dart';
-import '../../core/theme/app_colors.dart';
-import '../../core/theme/app_colors_extension.dart';
-
-class AppShell extends ConsumerStatefulWidget {
-  final Widget child;
-  const AppShell({super.key, required this.child});
-
-  @override
-  ConsumerState<AppShell> createState() => _AppShellState();
-}
-
-class _AppShellState extends ConsumerState<AppShell> {
-  final _tabRoutes = ['/messages', '/', '/profile'];
-
-  bool _showBottomBar(String location) {
-    return location == '/' || location == '/messages' || location == '/profile';
-  }
-
-  int _tabIndex(String location) {
-    if (location.startsWith('/messages')) return 0;
-    if (location == '/' || !location.startsWith('/profile')) return 1;
-    return 2;
-  }
-
-  NavBarConfig _rootConfig(String location, AppLocalizations l10n) {
-    if (location.startsWith('/messages')) {
-      return NavBarConfig(
-        title: l10n.get('tabMessages'),
-        showBack: true,
-        leadingIcon: Icons.close,
-      );
-    }
-    if (location == '/') {
-      return NavBarConfig(
-        title: l10n.get('appName'),
-        showBack: true,
-        leadingIcon: Icons.close,
-      );
-    }
-    if (location.startsWith('/profile')) {
-      return NavBarConfig(
-        title: l10n.get('tabProfile'),
-        showBack: true,
-        leadingIcon: Icons.close,
-      );
-    }
-    return NavBarConfig.home;
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    final l10n = AppLocalizations.of(context);
-    final location = GoRouterState.of(context).uri.toString();
-
-    // 更新路由记忆:当前 Tab 的最后位置
-    final tabIndex = _tabIndex(location);
-    if (_tabRoutes[tabIndex] != location && _showBottomBar(location)) {
-      _tabRoutes[tabIndex] = location;
-    }
-
-    final showBottomBar = _showBottomBar(location);
-    final config = showBottomBar
-        ? _rootConfig(location, l10n)
-        : ref.watch(navBarConfigProvider);
-
-    SystemChrome.setSystemUIOverlayStyle(
-      SystemUiOverlayStyle(
-        statusBarColor: colors.bgCard,
-        statusBarIconBrightness: Brightness.dark,
-        statusBarBrightness: Brightness.light,
-      ),
-    );
-
-    return Scaffold(
-      backgroundColor: colors.bgPage,
-      body: Column(
-        crossAxisAlignment: CrossAxisAlignment.stretch,
-        children: [
-          _buildNavBar(config, location, context),
-          Expanded(child: widget.child),
-          if (showBottomBar) _buildBottomBar(location, context, l10n),
-        ],
-      ),
-    );
-  }
-
-  Widget _buildNavBar(
-    NavBarConfig config,
-    String location,
-    BuildContext context,
-  ) {
-    final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    final isRoot = _showBottomBar(location);
-
-    List<TDNavBarItem>? leftItems;
-    if (config.showBack) {
-      final icon = config.leadingIcon ?? TDIcons.chevron_left;
-      leftItems = [
-        TDNavBarItem(
-          icon: icon,
-          iconSize: 22,
-          iconColor: colors.textPrimary,
-          action:
-              config.onBack ??
-              (isRoot
-                  ? () => SystemNavigator.pop()
-                  : () => GoRouter.of(context).pop()),
-        ),
-      ];
-    }
-
-    List<TDNavBarItem>? rightItems;
-    if (config.showRight && config.rightWidget != null) {
-      rightItems = [TDNavBarItem(iconWidget: config.rightWidget, iconSize: 22)];
-    }
-
-    return TDNavBar(
-      title: config.title,
-      titleColor: colors.textPrimary,
-      titleFontWeight: FontWeight.w600,
-      titleFont: Font(size: AppFontSizes.title.toInt(), lineHeight: 26),
-      backgroundColor: colors.bgCard,
-      height: 56,
-      centerTitle: true,
-      useDefaultBack: false,
-      screenAdaptation: true,
-      leftBarItems: leftItems,
-      rightBarItems: rightItems,
-    );
-  }
-
-  Widget _buildBottomBar(
-    String location,
-    BuildContext context,
-    AppLocalizations l10n,
-  ) {
-    final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    return LayoutBuilder(
-      builder: (ctx, constraints) {
-        if (constraints.maxWidth <= 0) {
-          return const SizedBox.shrink();
-        }
-        final bottomPadding = MediaQuery.of(ctx).padding.bottom;
-        return Padding(
-          padding: EdgeInsets.only(top: 8, bottom: 8 + bottomPadding),
-          child: TDBottomTabBar(
-            TDBottomTabBarBasicType.iconText,
-            useSafeArea: false,
-            componentType: TDBottomTabBarComponentType.label,
-            outlineType: TDBottomTabBarOutlineType.capsule,
-            currentIndex: _tabIndex(location),
-            navigationTabs: [
-              TDBottomTabBarTabConfig(
-                tabText: l10n.get('tabMessages'),
-                selectedIcon: Icon(
-                  Icons.notifications,
-                  size: 22,
-                  color: colors.primary,
-                ),
-                unselectedIcon: Icon(
-                  Icons.notifications_outlined,
-                  size: 22,
-                  color: colors.textSecondary,
-                ),
-                selectTabTextStyle: TextStyle(
-                  fontSize: 10,
-                  fontWeight: FontWeight.w600,
-                  color: colors.primary,
-                ),
-                unselectTabTextStyle: TextStyle(
-                  fontSize: 10,
-                  color: colors.textSecondary,
-                ),
-                onTap: () => context.go(_tabRoutes[0]),
-              ),
-              TDBottomTabBarTabConfig(
-                tabText: l10n.get('tabWorkbench'),
-                selectedIcon: Icon(
-                  Icons.dashboard,
-                  size: 22,
-                  color: colors.primary,
-                ),
-                unselectedIcon: Icon(
-                  Icons.dashboard_outlined,
-                  size: 22,
-                  color: colors.textSecondary,
-                ),
-                selectTabTextStyle: TextStyle(
-                  fontSize: 10,
-                  fontWeight: FontWeight.w600,
-                  color: colors.primary,
-                ),
-                unselectTabTextStyle: TextStyle(
-                  fontSize: 10,
-                  color: colors.textSecondary,
-                ),
-                onTap: () => context.go(_tabRoutes[1]),
-              ),
-              TDBottomTabBarTabConfig(
-                tabText: l10n.get('tabProfile'),
-                selectedIcon: Icon(
-                  Icons.person,
-                  size: 22,
-                  color: colors.primary,
-                ),
-                unselectedIcon: Icon(
-                  Icons.person_outline,
-                  size: 22,
-                  color: colors.textSecondary,
-                ),
-                selectTabTextStyle: TextStyle(
-                  fontSize: 10,
-                  fontWeight: FontWeight.w600,
-                  color: colors.primary,
-                ),
-                unselectTabTextStyle: TextStyle(
-                  fontSize: 10,
-                  color: colors.textSecondary,
-                ),
-                onTap: () => context.go(_tabRoutes[2]),
-              ),
-            ],
-          ),
-        );
-      },
-    );
-  }
-}

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

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../shared/widgets/action_bar.dart';
 import '../../shared/widgets/form_section.dart';

+ 2 - 2
lib/features/vehicle/vehicle_detail_page.dart

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
 import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../shared/widgets/status_banner.dart';
 import '../../shared/widgets/approval_timeline.dart';
@@ -662,7 +662,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
       case 'draft':
         return (Icons.edit_note, colors.statusGray, l10n.get('draft'));
       case 'withdrawn':
-        return (Icons.cancel_outlined, colors.revokedText, l10n.get('revoked'));
+        return (Icons.cancel_outlined, colors.withdrawnText, l10n.get('withdrawn'));
       case 'returned':
         return (Icons.assignment_return, colors.primary, l10n.get('returned'));
       default:

+ 106 - 121
lib/features/vehicle/vehicle_list_page.dart

@@ -6,11 +6,13 @@ import 'package:easy_refresh/easy_refresh.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/auth/role_provider.dart';
-import '../shell/nav_bar_config.dart';
+import '../../shared/widgets/nav_bar_config.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../shared/widgets/empty_state.dart';
 import '../../shared/widgets/skeleton_list_card.dart';
-import '../../shared/widgets/filter_bar.dart';
+import '../../shared/widgets/list_filter_panel.dart';
+import '../../shared/widgets/list_footer.dart';
+import '../../shared/widgets/status_tag.dart';
 import '../../core/i18n/app_localizations.dart';
 import 'vehicle_list_controller.dart';
 import 'vehicle_model.dart';
@@ -31,7 +33,7 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
     l10n.get('pending'),
     l10n.get('approved'),
     l10n.get('rejected'),
-    l10n.get('revoked'),
+    l10n.get('withdrawn'),
     l10n.get('returned'),
   ];
   static const _tabKeys = [
@@ -86,21 +88,11 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
 
     final filterGroups = [
       FilterGroup(
-        title: '日期范围',
+        title: l10n.get('filterDateRange'),
         type: FilterGroupType.dateRange,
         sections: [
           FilterSection(
-            label: '起始日期',
-            type: FilterSectionType.dateRange,
-            startDate: dateStart,
-            endDate: dateEnd,
-            onStartChanged: (v) =>
-                ref.read(vehicleDateStartProvider.notifier).state = v,
-            onEndChanged: (v) =>
-                ref.read(vehicleDateEndProvider.notifier).state = v,
-          ),
-          FilterSection(
-            label: '结束日期',
+            label: l10n.get('filterDateRange'),
             type: FilterSectionType.dateRange,
             startDate: dateStart,
             endDate: dateEnd,
@@ -112,16 +104,19 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
         ],
       ),
       FilterGroup(
-        title: '其它',
+        title: l10n.get('other'),
         type: FilterGroupType.other,
         sections: [
           FilterSection(
-            label: '用车目的',
+            label: l10n.get('vehiclePurpose'),
             type: FilterSectionType.singleSelect,
-            options: const [
-              FilterOption(value: 'reception', label: '客户接待'),
-              FilterOption(value: 'business', label: '商务出行'),
-              FilterOption(value: 'official', label: '公务'),
+            options: [
+              FilterOption(
+                value: 'reception',
+                label: l10n.get('customerReception'),
+              ),
+              FilterOption(value: 'business', label: l10n.get('businessTrip')),
+              FilterOption(value: 'official', label: l10n.get('official')),
             ],
             selectedValue: purposeFilter,
             onChanged: (v) =>
@@ -130,7 +125,9 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
         ],
       ),
     ];
-    final hasFilter = FilterBar.hasActiveFilter(filterGroups);
+    final hasFilter = ListFilterPanel.hasActiveFilter(filterGroups);
+    final filterVersion = Object.hash(dateStart, dateEnd, purposeFilter);
+    final now = DateTime.now();
     void onFilterReset() {
       ref.read(vehicleDateStartProvider.notifier).state = null;
       ref.read(vehicleDateEndProvider.notifier).state = null;
@@ -145,16 +142,20 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
             showBack: true,
             showRight: true,
             rightWidget: GestureDetector(
-              onTap: () => FilterBar.show(
+              onTap: () => ListFilterPanel.show(
                 context,
                 groups: filterGroups,
                 onReset: onFilterReset,
                 onConfirm: () {},
+                defaultStartDate: DateTime(now.year, now.month, 1),
+                defaultEndDate: DateTime(now.year, now.month, now.day),
               ),
               child: Stack(
+                clipBehavior: Clip.none,
                 children: [
                   Icon(
                     TDIcons.filter,
+                    size: 22,
                     color: hasFilter ? colors.primary : colors.textPrimary,
                   ),
                   if (hasFilter)
@@ -173,13 +174,28 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
                 ],
               ),
             ),
+            hasFilter: hasFilter,
+            filterVersion: filterVersion,
             onBack: () => context.pop(),
           ),
         );
     return Column(
       children: [
         if (isManager)
-          _buildScopeChip(colors),
+          Container(
+            color: colors.bgCard,
+            padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
+            child: _buildScopeChip(colors),
+          ),
+        Container(
+          color: colors.bgCard,
+          padding: EdgeInsets.zero,
+          child: TDSearchBar(
+            placeHolder: l10n.get('searchVehicle'),
+            needCancel: true,
+            style: TDSearchStyle.round,
+          ),
+        ),
         Container(
           color: colors.bgCard,
           padding: const EdgeInsets.symmetric(horizontal: 8),
@@ -219,47 +235,47 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
 
   Widget _buildScopeChip(AppColorsExtension colors) {
     final scope = ref.watch(_scopeProvider);
-    return Padding(
-      padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
-      child: Row(
-        children: [
-          GestureDetector(
-            onTap: () => ref.read(_scopeProvider.notifier).state = 'my',
-            child: Container(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
-              decoration: BoxDecoration(
-                color: scope == 'my' ? colors.primary : colors.bgPage,
-                borderRadius: BorderRadius.circular(16),
-              ),
-              child: Text(
-                '我的发起',
-                style: TextStyle(
-                  fontSize: 13,
-                  color: scope == 'my' ? Colors.white : colors.textSecondary,
-                ),
+    final l10n = AppLocalizations.of(context);
+    return Row(
+      children: [
+        GestureDetector(
+          onTap: () => ref.read(_scopeProvider.notifier).state = 'my',
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+            decoration: BoxDecoration(
+              color: scope == 'my' ? colors.primary : colors.bgPage,
+              borderRadius: BorderRadius.circular(16),
+              border: scope == 'my' ? null : Border.all(color: colors.border),
+            ),
+            child: Text(
+              l10n.get('scopeMyApplications'),
+              style: TextStyle(
+                fontSize: 13,
+                color: scope == 'my' ? colors.bgCard : colors.textSecondary,
               ),
             ),
           ),
-          const SizedBox(width: 8),
-          GestureDetector(
-            onTap: () => ref.read(_scopeProvider.notifier).state = 'sub',
-            child: Container(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
-              decoration: BoxDecoration(
-                color: scope == 'sub' ? colors.primary : colors.bgPage,
-                borderRadius: BorderRadius.circular(16),
-              ),
-              child: Text(
-                '下属审批',
-                style: TextStyle(
-                  fontSize: 13,
-                  color: scope == 'sub' ? Colors.white : colors.textSecondary,
-                ),
+        ),
+        const SizedBox(width: 8),
+        GestureDetector(
+          onTap: () => ref.read(_scopeProvider.notifier).state = 'sub',
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+            decoration: BoxDecoration(
+              color: scope == 'sub' ? colors.primary : colors.bgPage,
+              borderRadius: BorderRadius.circular(16),
+              border: scope == 'sub' ? null : Border.all(color: colors.border),
+            ),
+            child: Text(
+              l10n.get('scopeSubordinates'),
+              style: TextStyle(
+                fontSize: 13,
+                color: scope == 'sub' ? colors.bgCard : colors.textSecondary,
               ),
             ),
           ),
-        ],
-      ),
+        ),
+      ],
     );
   }
 
@@ -316,7 +332,11 @@ class _VehicleTabContent extends ConsumerWidget {
         padding: const EdgeInsets.all(16),
         itemCount: oldItems.length,
         itemBuilder: (_, i) {
-          final card = _buildVehicleListItem(context, oldItems[i], isSub: isSub);
+          final card = _buildVehicleListItem(
+            context,
+            oldItems[i],
+            isSub: isSub,
+          );
           if (isSub && oldItems[i].status == 'pending') {
             return Padding(
               padding: const EdgeInsets.only(bottom: 16),
@@ -352,8 +372,9 @@ class _VehicleTabContent extends ConsumerWidget {
 
     return ListView.builder(
       padding: const EdgeInsets.all(16),
-      itemCount: items.length,
+      itemCount: items.length + 1,
       itemBuilder: (_, i) {
+        if (i == items.length) return ListFooter(itemCount: items.length);
         final card = _buildVehicleListItem(context, items[i], isSub: isSub);
         if (isSub && items[i].status == 'pending') {
           return Padding(
@@ -361,50 +382,18 @@ class _VehicleTabContent extends ConsumerWidget {
             child: _buildSwipeApprove(card, items[i].id),
           );
         }
-        return Padding(
-          padding: const EdgeInsets.only(bottom: 16),
-          child: card,
-        );
+        return Padding(padding: const EdgeInsets.only(bottom: 16), child: card);
       },
     );
   }
 
-  Widget _buildVehicleListItem(BuildContext context, VehicleModel item, {bool isSub = false}) {
+  Widget _buildVehicleListItem(
+    BuildContext context,
+    VehicleModel item, {
+    bool isSub = false,
+  }) {
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    Color bg, fg;
-    String label;
-
-    switch (item.status) {
-      case 'pending':
-        bg = colors.warningBg;
-        fg = colors.warning;
-        label = l10n.get('pending');
-      case 'approved':
-        bg = colors.successBg;
-        fg = colors.success;
-        label = l10n.get('approved');
-      case 'rejected':
-        bg = colors.dangerBg;
-        fg = colors.danger;
-        label = l10n.get('rejected');
-      case 'draft':
-        bg = colors.bgPage;
-        fg = colors.statusGray;
-        label = l10n.get('draft');
-      case 'revoked':
-        bg = colors.revokedBg;
-        fg = colors.revokedText;
-        label = l10n.get('revoked');
-      case 'returned':
-        bg = const Color(0xFFEDF2FC);
-        fg = const Color(0xFF5A8CDB);
-        label = l10n.get('returned');
-      default:
-        bg = colors.bgPage;
-        fg = colors.statusGray;
-        label = item.status;
-    }
 
     return GestureDetector(
       onTap: () => context.push('/vehicle/detail/${item.id}'),
@@ -428,24 +417,7 @@ class _VehicleTabContent extends ConsumerWidget {
                     color: colors.textPrimary,
                   ),
                 ),
-                Container(
-                  padding: const EdgeInsets.symmetric(
-                    horizontal: 8,
-                    vertical: 2,
-                  ),
-                  decoration: BoxDecoration(
-                    color: bg,
-                    borderRadius: BorderRadius.circular(4),
-                  ),
-                  child: Text(
-                    label,
-                    style: TextStyle(
-                      fontSize: AppFontSizes.caption,
-                      fontWeight: FontWeight.w500,
-                      color: fg,
-                    ),
-                  ),
-                ),
+                StatusTag.fromStatus(item.status, l10n),
               ],
             ),
             if (isSub) ...[
@@ -530,7 +502,10 @@ class _VehicleTabContent extends ConsumerWidget {
                 label: '',
                 backgroundColor: Colors.transparent,
                 builder: (_) => Container(
-                  margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
+                  margin: const EdgeInsets.symmetric(
+                    horizontal: 4,
+                    vertical: 8,
+                  ),
                   decoration: BoxDecoration(
                     color: Colors.green,
                     borderRadius: BorderRadius.circular(8),
@@ -539,7 +514,11 @@ class _VehicleTabContent extends ConsumerWidget {
                   padding: const EdgeInsets.symmetric(horizontal: 12),
                   child: const Text(
                     '一键同意',
-                    style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600),
+                    style: TextStyle(
+                      color: Colors.white,
+                      fontSize: 14,
+                      fontWeight: FontWeight.w600,
+                    ),
                   ),
                 ),
                 onPressed: (_) async {
@@ -548,8 +527,14 @@ class _VehicleTabContent extends ConsumerWidget {
                     builder: (dCtx) => TDAlertDialog(
                       title: '确认审批',
                       content: '确认同意该用车申请?',
-                      leftBtn: TDDialogButtonOptions(title: '取消', action: () => Navigator.of(dCtx).pop(false)),
-                      rightBtn: TDDialogButtonOptions(title: '确认', action: () => Navigator.of(dCtx).pop(true)),
+                      leftBtn: TDDialogButtonOptions(
+                        title: '取消',
+                        action: () => Navigator.of(dCtx).pop(false),
+                      ),
+                      rightBtn: TDDialogButtonOptions(
+                        title: '确认',
+                        action: () => Navigator.of(dCtx).pop(true),
+                      ),
                     ),
                   );
                   if (confirmed == true) {

+ 1 - 1
lib/shared/models/approval_status.dart

@@ -19,7 +19,7 @@ enum ApprovalStatus {
       case ApprovalStatus.rejected:
         return l10n.get('statusRejected');
       case ApprovalStatus.withdrawn:
-        return l10n.get('statusRevoked');
+        return l10n.get('statusWithdrawn');
     }
   }
 }

+ 100 - 27
lib/shared/widgets/app_scaffold.dart

@@ -6,7 +6,7 @@ import 'package:go_router/go_router.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../../core/theme/app_colors_extension.dart';
-import '../../features/shell/nav_bar_config.dart';
+import 'nav_bar_config.dart';
 
 /// 应用级 Scaffold:NavBar + body + 可选 BottomTabBar
 ///
@@ -23,13 +23,25 @@ class AppScaffold extends ConsumerWidget {
 
   NavBarConfig _rootConfig(String location, AppLocalizations l10n) {
     if (location.startsWith('/messages')) {
-      return NavBarConfig(title: l10n.get('tabMessages'), showBack: true, leadingIcon: Icons.close);
+      return NavBarConfig(
+        title: l10n.get('tabMessages'),
+        showBack: true,
+        leadingIcon: Icons.close,
+      );
     }
     if (location == '/') {
-      return NavBarConfig(title: l10n.get('appName'), showBack: true, leadingIcon: Icons.close);
+      return NavBarConfig(
+        title: l10n.get('appName'),
+        showBack: true,
+        leadingIcon: Icons.close,
+      );
     }
     if (location.startsWith('/profile')) {
-      return NavBarConfig(title: l10n.get('tabProfile'), showBack: true, leadingIcon: Icons.close);
+      return NavBarConfig(
+        title: l10n.get('tabProfile'),
+        showBack: true,
+        leadingIcon: Icons.close,
+      );
     }
     return NavBarConfig.home;
   }
@@ -56,13 +68,17 @@ class AppScaffold extends ConsumerWidget {
       body: Column(
         crossAxisAlignment: CrossAxisAlignment.stretch,
         children: [
-          _NavBarView(config: config, location: location, onBack: () {
-            if (_isRootTab(location)) {
-              SystemNavigator.pop();
-            } else {
-              GoRouter.of(context).pop();
-            }
-          }),
+          _NavBarView(
+            config: config,
+            location: location,
+            onBack: () {
+              if (_isRootTab(location)) {
+                SystemNavigator.pop();
+              } else {
+                GoRouter.of(context).pop();
+              }
+            },
+          ),
           Expanded(child: body),
           if (showTabBar)
             Container(
@@ -80,7 +96,11 @@ class _NavBarView extends StatelessWidget {
   final String location;
   final VoidCallback onBack;
 
-  const _NavBarView({required this.config, required this.location, required this.onBack});
+  const _NavBarView({
+    required this.config,
+    required this.location,
+    required this.onBack,
+  });
 
   @override
   Widget build(BuildContext context) {
@@ -126,7 +146,11 @@ class _AppTabBar extends StatelessWidget {
 
   static int tabIndex(String location) {
     if (location.startsWith('/messages')) return 0;
-    if (location == '/' || (!location.startsWith('/messages') && !location.startsWith('/profile'))) return 1;
+    if (location == '/' ||
+        (!location.startsWith('/messages') &&
+            !location.startsWith('/profile'))) {
+      return 1;
+    }
     return 2;
   }
 
@@ -139,36 +163,85 @@ class _AppTabBar extends StatelessWidget {
         if (constraints.maxWidth <= 0) return const SizedBox.shrink();
         final bottomPadding = MediaQuery.of(ctx).padding.bottom;
         return Padding(
-          padding: EdgeInsets.only(top: 8, bottom: 8 + bottomPadding),
+          padding: EdgeInsets.only(top: 0, bottom: 8 + bottomPadding),
           child: TDBottomTabBar(
             TDBottomTabBarBasicType.iconText,
             useSafeArea: false,
             componentType: TDBottomTabBarComponentType.label,
-            outlineType: TDBottomTabBarOutlineType.capsule,
+            outlineType: TDBottomTabBarOutlineType.filled,
+            backgroundColor: colors.bgCard,
+            dividerColor: Colors.transparent,
+            selectedBgColor: colors.primaryLight,
+            unselectedBgColor: Colors.transparent,
             currentIndex: tabIndex(location),
             navigationTabs: [
               TDBottomTabBarTabConfig(
                 tabText: l10n.get('tabMessages'),
-                selectedIcon: Icon(Icons.notifications, size: 22, color: colors.primary),
-                unselectedIcon: Icon(Icons.notifications_outlined, size: 22, color: colors.textSecondary),
-                selectTabTextStyle: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.primary),
-                unselectTabTextStyle: TextStyle(fontSize: 10, color: colors.textSecondary),
+                selectedIcon: Icon(
+                  Icons.notifications,
+                  size: 22,
+                  color: colors.primary,
+                ),
+                unselectedIcon: Icon(
+                  Icons.notifications_outlined,
+                  size: 22,
+                  color: colors.textSecondary,
+                ),
+                selectTabTextStyle: TextStyle(
+                  fontSize: 10,
+                  fontWeight: FontWeight.w600,
+                  color: colors.primary,
+                ),
+                unselectTabTextStyle: TextStyle(
+                  fontSize: 10,
+                  color: colors.textSecondary,
+                ),
                 onTap: () => context.go('/messages'),
               ),
               TDBottomTabBarTabConfig(
                 tabText: l10n.get('tabWorkbench'),
-                selectedIcon: Icon(Icons.dashboard, size: 22, color: colors.primary),
-                unselectedIcon: Icon(Icons.dashboard_outlined, size: 22, color: colors.textSecondary),
-                selectTabTextStyle: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.primary),
-                unselectTabTextStyle: TextStyle(fontSize: 10, color: colors.textSecondary),
+                selectedIcon: Icon(
+                  Icons.dashboard,
+                  size: 22,
+                  color: colors.primary,
+                ),
+                unselectedIcon: Icon(
+                  Icons.dashboard_outlined,
+                  size: 22,
+                  color: colors.textSecondary,
+                ),
+                selectTabTextStyle: TextStyle(
+                  fontSize: 10,
+                  fontWeight: FontWeight.w600,
+                  color: colors.primary,
+                ),
+                unselectTabTextStyle: TextStyle(
+                  fontSize: 10,
+                  color: colors.textSecondary,
+                ),
                 onTap: () => context.go('/'),
               ),
               TDBottomTabBarTabConfig(
                 tabText: l10n.get('tabProfile'),
-                selectedIcon: Icon(Icons.person, size: 22, color: colors.primary),
-                unselectedIcon: Icon(Icons.person_outline, size: 22, color: colors.textSecondary),
-                selectTabTextStyle: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.primary),
-                unselectTabTextStyle: TextStyle(fontSize: 10, color: colors.textSecondary),
+                selectedIcon: Icon(
+                  Icons.person,
+                  size: 22,
+                  color: colors.primary,
+                ),
+                unselectedIcon: Icon(
+                  Icons.person_outline,
+                  size: 22,
+                  color: colors.textSecondary,
+                ),
+                selectTabTextStyle: TextStyle(
+                  fontSize: 10,
+                  fontWeight: FontWeight.w600,
+                  color: colors.primary,
+                ),
+                unselectTabTextStyle: TextStyle(
+                  fontSize: 10,
+                  color: colors.textSecondary,
+                ),
                 onTap: () => context.go('/profile'),
               ),
             ],

+ 386 - 233
lib/shared/widgets/filter_bar.dart

@@ -1,39 +1,100 @@
 import 'package:flutter/material.dart';
 import 'package:tdesign_flutter/tdesign_flutter.dart';
 import '../../core/theme/app_colors_extension.dart';
+import '../../core/i18n/app_localizations.dart';
 
-/// 筛选弹出面板辅助类
+/// 列表页筛选面板,从右侧滑出。
 ///
-/// 调用 [FilterBar.show] 从右侧弹出筛选面板。
+/// ## 调用方式
 ///
-/// [FilterGroup] 列表定义各组筛选维度。日期范围组内展示「起始日期」「结束日期」行,
-/// 点击弹出 [TDDatePicker];其余组点击弹出 [TDPicker.showMultiPicker]。
-class FilterBar {
-  FilterBar._();
+/// ```dart
+/// // 1. 声明筛选值的 provider
+/// final dateStart = ref.watch(expenseDateStartProvider);
+/// final dateEnd   = ref.watch(expenseDateEndProvider);
+///
+/// // 2. 构建 FilterGroup 列表
+/// final filterGroups = [
+///   FilterGroup(
+///     title: '日期范围',
+///     type: FilterGroupType.dateRange,
+///     sections: [
+///       FilterSection(
+///         label: '日期范围',
+///         type: FilterSectionType.dateRange,
+///         startDate: dateStart, endDate: dateEnd,
+///         onStartChanged: (v) => ref.read(startProvider.notifier).state = v,
+///         onEndChanged: (v) => ref.read(endProvider.notifier).state = v,
+///       ),
+///     ],
+///   ),
+/// ];
+///
+/// // 3. 维护辅助变量
+/// final hasFilter     = ListFilterPanel.hasActiveFilter(filterGroups);
+/// final filterVersion = Object.hash(dateStart, dateEnd);
+/// void onReset() { /* 清空所有筛选 provider */ }
+///
+/// // 4. 调用 show + 更新 NavBarConfig
+/// ListFilterPanel.show(context,
+///   groups: filterGroups,
+///   onReset: onReset,
+///   onConfirm: () {},
+///   defaultStartDate: DateTime(year, month, 1),
+///   defaultEndDate: DateTime(year, month, day),
+/// );
+/// ```
+///
+/// ## 新增筛选条件
+///
+/// ① 声明 provider → ② 在 `filterGroups` 中加 `FilterSection`
+/// → ③ 更新 `filterVersion = Object.hash(...)` 加入新值
+/// → ④ 更新 `onReset` 清空新 provider
+/// → ⑤ 列表请求逻辑接入新 provider。
+///
+/// `FilterSection.type` 支持 [FilterSectionType.dateRange](日期)、
+/// [FilterSectionType.singleSelect](单选)、[FilterSectionType.multiSelect](多选)。
+class ListFilterPanel {
+  ListFilterPanel._();
+
+  static TDDrawer? _currentDrawer;
 
   /// 从右侧弹出筛选面板
+  ///
+  /// [defaultStartDate]/[defaultEndDate] 可选默认日期范围,
+  /// 当 [FilterSection] 未提供日期值时作为初始值。
   static void show(
     BuildContext context, {
     required List<FilterGroup> groups,
     required VoidCallback onReset,
     required VoidCallback onConfirm,
+    DateTime? defaultStartDate,
+    DateTime? defaultEndDate,
   }) {
-    final screenWidth = MediaQuery.of(context).size.width;
-    const panelWidth = 300.0;
-    Navigator.of(context).push(
-      TDSlidePopupRoute(
-        slideTransitionFrom: SlideTransitionFrom.right,
-        modalWidth: panelWidth,
-        modalLeft: screenWidth - panelWidth,
-        modalTop: 0,
-        modalBarrierFull: true,
-        builder: (ctx) => _FilterPopup(
-          groups: groups,
-          onReset: onReset,
-          onConfirm: onConfirm,
-        ),
+    _currentDrawer?.close();
+
+    final drawer = TDDrawer(
+      context,
+      width: 300,
+      placement: TDDrawerPlacement.right,
+      closeOnOverlayClick: true,
+      showOverlay: true,
+      onClose: () {
+        _currentDrawer = null;
+      },
+      contentWidget: _FilterPopup(
+        groups: groups,
+        onReset: onReset,
+        onConfirm: onConfirm,
+        defaultStartDate: defaultStartDate,
+        defaultEndDate: defaultEndDate,
+        onClose: () {
+          _currentDrawer?.close();
+          _currentDrawer = null;
+        },
       ),
     );
+    _currentDrawer = drawer;
+    drawer.show();
   }
 
   /// 是否有筛选条件激活(给 NavBar 红点使用)
@@ -52,8 +113,8 @@ class _SectionValue {
   final List<FilterOption> options;
   final ValueChanged<String>? onChanged;
   final ValueChanged<List<String>>? onMultiChanged;
-  final ValueChanged<DateTime>? onStartChanged;
-  final ValueChanged<DateTime>? onEndChanged;
+  final ValueChanged<DateTime?>? onStartChanged;
+  final ValueChanged<DateTime?>? onEndChanged;
 
   DateTime? startDate;
   DateTime? endDate;
@@ -74,19 +135,15 @@ class _SectionValue {
     this.selectedValues,
   });
 
-  /// 将本地值回写到原始回调(仅非空值
+  /// 将本地值回写到原始回调(包括 null/空值,用于清除筛选
   void apply() {
     if (type == FilterSectionType.dateRange) {
-      if (startDate != null) onStartChanged?.call(startDate!);
-      if (endDate != null) onEndChanged?.call(endDate!);
+      onStartChanged?.call(startDate);
+      onEndChanged?.call(endDate);
     } else if (type == FilterSectionType.singleSelect) {
-      if (selectedValue != null && selectedValue!.isNotEmpty) {
-        onChanged?.call(selectedValue!);
-      }
+      onChanged?.call(selectedValue ?? '');
     } else if (type == FilterSectionType.multiSelect) {
-      if (selectedValues != null && selectedValues!.isNotEmpty) {
-        onMultiChanged?.call(selectedValues!);
-      }
+      onMultiChanged?.call(selectedValues ?? []);
     }
   }
 
@@ -107,11 +164,17 @@ class _FilterPopup extends StatefulWidget {
   final List<FilterGroup> groups;
   final VoidCallback onReset;
   final VoidCallback onConfirm;
+  final VoidCallback? onClose;
+  final DateTime? defaultStartDate;
+  final DateTime? defaultEndDate;
 
   const _FilterPopup({
     required this.groups,
     required this.onReset,
     required this.onConfirm,
+    this.onClose,
+    this.defaultStartDate,
+    this.defaultEndDate,
   });
 
   @override
@@ -136,8 +199,8 @@ class _FilterPopupState extends State<_FilterPopup> {
             onMultiChanged: s.onMultiChanged,
             onStartChanged: s.onStartChanged,
             onEndChanged: s.onEndChanged,
-            startDate: s.startDate,
-            endDate: s.endDate,
+            startDate: s.startDate ?? widget.defaultStartDate,
+            endDate: s.endDate ?? widget.defaultEndDate,
             selectedValue: s.selectedValue,
             selectedValues: s.selectedValues,
           ),
@@ -147,121 +210,132 @@ class _FilterPopupState extends State<_FilterPopup> {
   }
 
   void _applyAll() {
-    final pending = List<_SectionValue>.from(_values);
-    final onConfirm = widget.onConfirm;
-    Navigator.of(context).pop();
-    Future.delayed(const Duration(milliseconds: 300), () {
-      for (final v in pending) {
-        v.apply();
-      }
-      onConfirm();
-    });
+    for (final v in _values) {
+      v.apply();
+    }
+    final onClose = widget.onClose;
+    if (onClose != null) {
+      onClose();
+    } else {
+      Navigator.of(context).pop();
+    }
+    widget.onConfirm();
   }
 
   void _resetAll() {
     for (final v in _values) {
       v.reset();
     }
-    widget.onReset();
     setState(() {});
   }
 
   @override
   Widget build(BuildContext context) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    return Container(
-      color: TDTheme.of(context).bgColorContainer,
-      child: SafeArea(
-        child: Column(
-          crossAxisAlignment: CrossAxisAlignment.stretch,
-          children: [
-            // 标题栏
-            Padding(
-              padding: const EdgeInsets.fromLTRB(16, 28, 16, 14),
-              child: Row(
-                children: [
-                  Icon(
-                    TDIcons.filter_filled,
-                    size: 20,
-                    color: colors.primary,
-                  ),
-                  const SizedBox(width: 8),
-                  Expanded(
-                    child: Text(
-                      '过滤条件',
-                      style: TextStyle(
-                        fontSize: 18,
-                        fontWeight: FontWeight.w600,
-                        color: colors.textPrimary,
+    final l10n = AppLocalizations.of(context);
+    return LayoutBuilder(
+      builder: (context, constraints) => Container(
+        color: TDTheme.of(context).bgColorContainer,
+        child: SafeArea(
+          child: SizedBox(
+            height: constraints.maxHeight,
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.stretch,
+              children: [
+                // 标题栏
+                Padding(
+                  padding: const EdgeInsets.fromLTRB(16, 28, 16, 14),
+                  child: Row(
+                    children: [
+                      Icon(TDIcons.filter, size: 20, color: colors.textPrimary),
+                      const SizedBox(width: 8),
+                      Expanded(
+                        child: Text(
+                          l10n.get('filterTitle'),
+                          style: TextStyle(
+                            fontSize: 18,
+                            fontWeight: FontWeight.w600,
+                            color: colors.textPrimary,
+                          ),
+                        ),
                       ),
-                    ),
+                      GestureDetector(
+                        onTap: () {
+                          final onClose = widget.onClose;
+                          if (onClose != null) {
+                            onClose();
+                          } else {
+                            Navigator.pop(context);
+                          }
+                        },
+                        child: Icon(
+                          TDIcons.close,
+                          size: 22,
+                          color: colors.textSecondary,
+                        ),
+                      ),
+                    ],
                   ),
-                  GestureDetector(
-                    onTap: () => Navigator.pop(context),
-                    child: Icon(
-                      TDIcons.close,
-                      size: 22,
-                      color: colors.textSecondary,
+                ),
+
+                // 筛选项区域
+                Expanded(
+                  child: SingleChildScrollView(
+                    child: Column(
+                      children: [
+                        for (final g in widget.groups)
+                          _GroupSection(
+                            group: g,
+                            values: _valuesForGroup(g),
+                            onValuesChanged: () => setState(() {}),
+                          ),
+                      ],
                     ),
                   ),
-                ],
-              ),
-            ),
-
-            // 筛选项区域
-            Expanded(
-              child: SingleChildScrollView(
-                child: Column(
-                  children: [
-                    for (final g in widget.groups)
-                      _GroupSection(
-                        group: g,
-                        values: _valuesForGroup(g),
-                        onValuesChanged: () => setState(() {}),
-                      ),
-                  ],
                 ),
-              ),
-            ),
 
-            // 底部按钮
-            Padding(
-              padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
-              child: Row(
-                children: [
-                  Expanded(
-                    child: TDButton(
-                      text: '重置',
-                      size: TDButtonSize.medium,
-                      type: TDButtonType.outline,
-                      shape: TDButtonShape.rectangle,
-                      theme: TDButtonTheme.defaultTheme,
-                      style: TDButtonStyle(
-                        frameColor: colors.textPlaceholder,
-                        textColor: colors.textPlaceholder,
+                // 底部按钮
+                Padding(
+                  padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
+                  child: Row(
+                    children: [
+                      Expanded(
+                        child: TDButton(
+                          text: l10n.get('reset'),
+                          size: TDButtonSize.medium,
+                          type: TDButtonType.outline,
+                          shape: TDButtonShape.rectangle,
+                          theme: TDButtonTheme.defaultTheme,
+                          style: TDButtonStyle(
+                            frameColor: colors.textSecondary,
+                            frameWidth: 1,
+                            textColor: colors.textSecondary,
+                            backgroundColor: Colors.transparent,
+                          ),
+                          onTap: _resetAll,
+                        ),
                       ),
-                      onTap: _resetAll,
-                    ),
-                  ),
-                  const SizedBox(width: 12),
-                  Expanded(
-                    child: TDButton(
-                      text: '确定',
-                      size: TDButtonSize.medium,
-                      type: TDButtonType.fill,
-                      shape: TDButtonShape.rectangle,
-                      theme: TDButtonTheme.primary,
-                      style: TDButtonStyle(
-                        backgroundColor: colors.primary,
-                        textColor: colors.bgCard,
+                      const SizedBox(width: 12),
+                      Expanded(
+                        child: TDButton(
+                          text: l10n.get('confirm'),
+                          size: TDButtonSize.medium,
+                          type: TDButtonType.fill,
+                          shape: TDButtonShape.rectangle,
+                          theme: TDButtonTheme.primary,
+                          style: TDButtonStyle(
+                            backgroundColor: colors.primary,
+                            textColor: colors.bgCard,
+                          ),
+                          onTap: _applyAll,
+                        ),
                       ),
-                      onTap: _applyAll,
-                    ),
+                    ],
                   ),
-                ],
-              ),
+                ),
+              ],
             ),
-          ],
+          ),
         ),
       ),
     );
@@ -292,11 +366,8 @@ class _GroupSection extends StatelessWidget {
   void _applyQuickDate(DateTime start, DateTime end) {
     for (final v in values) {
       if (v.type == FilterSectionType.dateRange) {
-        if (v.label == '起始日期') {
-          v.startDate = start;
-        } else if (v.label == '结束日期') {
-          v.endDate = end;
-        }
+        v.startDate = start;
+        v.endDate = end;
       }
     }
     onValuesChanged();
@@ -305,9 +376,21 @@ class _GroupSection extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final l10n = AppLocalizations.of(context);
     final isDateGroup = group.type == FilterGroupType.dateRange;
     final now = DateTime.now();
     final today = DateTime(now.year, now.month, now.day);
+    final monthEnd = DateTime(today.year, today.month + 1, 0);
+    final quarterEndMonth = ((today.month - 1) ~/ 3 + 1) * 3;
+    final quarterEnd = DateTime(today.year, quarterEndMonth + 1, 0);
+    final yearEnd = DateTime(today.year, 12, 31);
+    final weekStart = today.subtract(Duration(days: today.weekday - 1));
+    final weekEnd = today.add(
+      Duration(days: DateTime.daysPerWeek - today.weekday),
+    );
+    final lastMonthStart = DateTime(today.year, today.month - 1, 1);
+    final lastMonthEnd = DateTime(today.year, today.month, 0);
+    final last3MonthsStart = DateTime(today.year, today.month - 3, today.day);
 
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
@@ -317,7 +400,7 @@ class _GroupSection extends StatelessWidget {
           child: Text(
             group.title,
             style: TextStyle(
-              fontSize: 12,
+              fontSize: 13,
               fontWeight: FontWeight.w500,
               color: colors.textPlaceholder,
             ),
@@ -327,39 +410,57 @@ class _GroupSection extends StatelessWidget {
           Padding(
             padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
             child: Wrap(
-              spacing: 8,
+              spacing: 6,
               runSpacing: 6,
               children: [
                 _QuickDateChip(
-                  label: '本月',
+                  label: l10n.get('filterThisWeek'),
+                  start: weekStart,
+                  end: weekEnd,
+                  onTap: (s, e) => _applyQuickDate(s, e),
+                ),
+                _QuickDateChip(
+                  label: l10n.get('filterThisMonth'),
                   start: DateTime(today.year, today.month, 1),
-                  end: today,
+                  end: monthEnd,
                   onTap: (s, e) => _applyQuickDate(s, e),
                 ),
                 _QuickDateChip(
-                  label: '本季',
+                  label: l10n.get('filterThisQuarter'),
                   start: DateTime(
                     today.year,
                     ((today.month - 1) ~/ 3) * 3 + 1,
                     1,
                   ),
-                  end: today,
+                  end: quarterEnd,
                   onTap: (s, e) => _applyQuickDate(s, e),
                 ),
                 _QuickDateChip(
-                  label: '本年',
+                  label: l10n.get('filterThisYear'),
                   start: DateTime(today.year, 1, 1),
+                  end: yearEnd,
+                  onTap: (s, e) => _applyQuickDate(s, e),
+                ),
+                _QuickDateChip(
+                  label: l10n.get('filterLastMonth'),
+                  start: lastMonthStart,
+                  end: lastMonthEnd,
+                  onTap: (s, e) => _applyQuickDate(s, e),
+                ),
+                _QuickDateChip(
+                  label: l10n.get('filterLast3Months'),
+                  start: last3MonthsStart,
                   end: today,
                   onTap: (s, e) => _applyQuickDate(s, e),
                 ),
                 _QuickDateChip(
-                  label: '7天内',
+                  label: l10n.get('filter7Days'),
                   start: today.subtract(const Duration(days: 6)),
                   end: today,
                   onTap: (s, e) => _applyQuickDate(s, e),
                 ),
                 _QuickDateChip(
-                  label: '30天内',
+                  label: l10n.get('filter30Days'),
                   start: today.subtract(const Duration(days: 29)),
                   end: today,
                   onTap: (s, e) => _applyQuickDate(s, e),
@@ -369,7 +470,16 @@ class _GroupSection extends StatelessWidget {
           ),
           ...values
               .where((v) => v.type == FilterSectionType.dateRange)
-              .map((v) => _DateRow(value: v, onChanged: onValuesChanged)),
+              .expand(
+                (v) => [
+                  _DateRow(value: v, isStart: true, onChanged: onValuesChanged),
+                  _DateRow(
+                    value: v,
+                    isStart: false,
+                    onChanged: onValuesChanged,
+                  ),
+                ],
+              ),
         ] else
           ...values.map(
             (v) => _FilterRow(value: v, onChanged: onValuesChanged),
@@ -396,14 +506,20 @@ class _QuickDateChip extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    return TDButton(
-      text: label,
-      size: TDButtonSize.small,
-      type: TDButtonType.outline,
-      shape: TDButtonShape.round,
-      theme: TDButtonTheme.primary,
-      style: TDButtonStyle(textColor: colors.primary),
+    return GestureDetector(
       onTap: () => onTap(start, end),
+      child: Container(
+        padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
+        decoration: BoxDecoration(
+          border: Border.all(color: colors.border),
+          borderRadius: BorderRadius.circular(14),
+          color: Colors.transparent,
+        ),
+        child: Text(
+          label,
+          style: TextStyle(fontSize: 13, color: colors.textSecondary),
+        ),
+      ),
     );
   }
 }
@@ -414,16 +530,17 @@ class _QuickDateChip extends StatelessWidget {
 
 class _DateRow extends StatelessWidget {
   final _SectionValue value;
+  final bool isStart;
   final VoidCallback onChanged;
 
-  const _DateRow({required this.value, required this.onChanged});
+  const _DateRow({
+    required this.value,
+    required this.isStart,
+    required this.onChanged,
+  });
 
-  String _fmt(DateTime? dt, bool isStart) {
-    if (dt == null) {
-      final now = DateTime.now();
-      final d = isStart ? DateTime(now.year, now.month, 1) : now;
-      return '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
-    }
+  String _fmt(DateTime? dt) {
+    if (dt == null) return '';
     return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
   }
 
@@ -437,42 +554,79 @@ class _DateRow extends StatelessWidget {
 
     if (!context.mounted) return;
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final theme = Theme.of(context);
+    final selectHighlight = Container(
+      height: 40,
+      decoration: BoxDecoration(
+        color: colors.primary.withValues(alpha: 0.08),
+        borderRadius: BorderRadius.all(Radius.circular(8)),
+      ),
+    );
     await showModalBottomSheet<void>(
       context: context,
-      backgroundColor: colors.bgCard,
+      backgroundColor: Colors.transparent,
       shape: RoundedRectangleBorder(
         borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
       ),
       builder: (ctx) {
-        return TDDatePicker(
-          title: isStart ? '选择起始日期' : '选择结束日期',
-          backgroundColor: colors.bgCard,
-          model: DatePickerModel(
-            useYear: true,
-            useMonth: true,
-            useDay: true,
-            useHour: false,
-            useMinute: false,
-            useSecond: false,
-            useWeekDay: false,
-            dateStart: [2020, 1, 1],
-            dateEnd: [now.year + 1, 12, 31],
-            dateInitial: [initial.year, initial.month, initial.day],
+        return Theme(
+          data: theme,
+          child: TDDatePicker(
+            title: isStart
+                ? AppLocalizations.of(context).get('filterSelectStartDate')
+                : AppLocalizations.of(context).get('filterSelectEndDate'),
+            backgroundColor: colors.bgCard,
+            customSelectWidget: selectHighlight,
+            model: DatePickerModel(
+              useYear: true,
+              useMonth: true,
+              useDay: true,
+              useHour: false,
+              useMinute: false,
+              useSecond: false,
+              useWeekDay: false,
+              dateStart: [2020, 1, 1],
+              dateEnd: [now.year + 1, 12, 31],
+              dateInitial: [initial.year, initial.month, initial.day],
+            ),
+            onConfirm: (selected) {
+              picked = DateTime(
+                selected['year'] ?? initial.year,
+                selected['month'] ?? initial.month,
+                selected['day'] ?? initial.day,
+              );
+              Navigator.of(ctx).pop();
+            },
+            onCancel: (_) => Navigator.of(ctx).pop(),
           ),
-          onConfirm: (selected) {
-            picked = DateTime(
-              selected['year'] ?? initial.year,
-              selected['month'] ?? initial.month,
-              selected['day'] ?? initial.day,
-            );
-            Navigator.of(ctx).pop();
-          },
-          onCancel: (_) => Navigator.of(ctx).pop(),
         );
       },
     );
 
     if (picked != null && context.mounted) {
+      final l10n = AppLocalizations.of(context);
+      if (isStart && value.endDate != null && picked!.isAfter(value.endDate!)) {
+        TDMessage.showMessage(
+          context: context,
+          content: l10n.get('filterDateStartAfterEnd'),
+          icon: TDIcons.error_circle_filled,
+          theme: MessageTheme.warning,
+          duration: 3000,
+        );
+        return;
+      }
+      if (!isStart &&
+          value.startDate != null &&
+          picked!.isBefore(value.startDate!)) {
+        TDMessage.showMessage(
+          context: context,
+          content: l10n.get('filterDateEndBeforeStart'),
+          icon: TDIcons.error_circle_filled,
+          theme: MessageTheme.warning,
+          duration: 3000,
+        );
+        return;
+      }
       if (isStart) {
         value.startDate = picked;
       } else {
@@ -485,7 +639,6 @@ class _DateRow extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
-    final isStart = value.label == '起始日期';
     final date = isStart ? value.startDate : value.endDate;
     final icon = isStart ? TDIcons.calendar_1 : TDIcons.calendar_2;
 
@@ -499,11 +652,10 @@ class _DateRow extends StatelessWidget {
             Icon(icon, size: 16, color: colors.textSecondary),
             const SizedBox(width: 8),
             Text(
-              value.label,
-              style: TextStyle(
-                fontSize: 14,
-                color: colors.textSecondary,
-              ),
+              isStart
+                  ? AppLocalizations.of(context).get('filterStartDate')
+                  : AppLocalizations.of(context).get('filterEndDate'),
+              style: TextStyle(fontSize: 14, color: colors.textSecondary),
             ),
             const Spacer(),
             SizedBox(
@@ -511,13 +663,8 @@ class _DateRow extends StatelessWidget {
               child: Align(
                 alignment: Alignment.centerRight,
                 child: Text(
-                  _fmt(date, isStart),
-                  style: TextStyle(
-                    fontSize: 14,
-                    color: date != null
-                        ? colors.primary
-                        : colors.textPlaceholder,
-                  ),
+                  _fmt(date),
+                  style: TextStyle(fontSize: 14, color: colors.primary),
                   maxLines: 1,
                   overflow: TextOverflow.ellipsis,
                 ),
@@ -567,30 +714,48 @@ class _FilterRow extends StatelessWidget {
     }
 
     if (!context.mounted) return;
-    TDPicker.showMultiPicker(
-      context,
-      title: value.label,
-      data: [labels],
-      initialIndexes: initialIndexes.isNotEmpty ? initialIndexes : null,
-      onConfirm: (selected) {
-        if (isMulti) {
-          final values = <String>[];
-          for (final s in selected) {
-            if (s is int && s >= 0 && s < options.length) {
-              values.add(options[s].value);
-            }
-          }
-          value.selectedValues = values;
-        } else {
-          if (selected.isNotEmpty && selected[0] is int) {
-            final idx = selected[0] as int;
-            if (idx >= 0 && idx < options.length) {
-              value.selectedValue = options[idx].value;
+    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final theme = Theme.of(context);
+    final selectHighlight = Container(
+      height: 40,
+      decoration: BoxDecoration(
+        color: colors.primary.withValues(alpha: 0.08),
+        borderRadius: BorderRadius.all(Radius.circular(8)),
+      ),
+    );
+    await showModalBottomSheet<void>(
+      context: context,
+      backgroundColor: Colors.transparent,
+      builder: (ctx) => Theme(
+        data: theme,
+        child: TDMultiPicker(
+          title: value.label,
+          backgroundColor: colors.bgCard,
+          customSelectWidget: selectHighlight,
+          data: [labels],
+          initialIndexes: initialIndexes.isNotEmpty ? initialIndexes : null,
+          onConfirm: (selected) {
+            if (isMulti) {
+              final values = <String>[];
+              for (final s in selected) {
+                if (s is int && s >= 0 && s < options.length) {
+                  values.add(options[s].value);
+                }
+              }
+              value.selectedValues = values;
+            } else {
+              if (selected.isNotEmpty && selected[0] is int) {
+                final idx = selected[0] as int;
+                if (idx >= 0 && idx < options.length) {
+                  value.selectedValue = options[idx].value;
+                }
+              }
             }
-          }
-        }
-        WidgetsBinding.instance.addPostFrameCallback((_) => onChanged());
-      },
+            Navigator.of(ctx).pop();
+            onChanged();
+          },
+        ),
+      ),
     );
   }
 
@@ -626,18 +791,11 @@ class _FilterRow extends StatelessWidget {
         padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
         child: Row(
           children: [
-            Icon(
-              TDIcons.filter_1,
-              size: 16,
-              color: colors.textSecondary,
-            ),
+            Icon(TDIcons.filter_1, size: 16, color: colors.textSecondary),
             const SizedBox(width: 8),
             Text(
               value.label,
-              style: TextStyle(
-                fontSize: 14,
-                color: colors.textSecondary,
-              ),
+              style: TextStyle(fontSize: 14, color: colors.textSecondary),
             ),
             const Spacer(),
             SizedBox(
@@ -645,13 +803,8 @@ class _FilterRow extends StatelessWidget {
               child: Align(
                 alignment: Alignment.centerRight,
                 child: Text(
-                  summary.isNotEmpty ? summary : '全部',
-                  style: TextStyle(
-                    fontSize: 14,
-                    color: summary.isNotEmpty
-                        ? colors.primary
-                        : colors.textPlaceholder,
-                  ),
+                  summary,
+                  style: TextStyle(fontSize: 14, color: colors.primary),
                   maxLines: 1,
                   overflow: TextOverflow.ellipsis,
                 ),
@@ -688,8 +841,8 @@ class FilterSection {
   final ValueChanged<List<String>>? onMultiChanged;
   final DateTime? startDate;
   final DateTime? endDate;
-  final ValueChanged<DateTime>? onStartChanged;
-  final ValueChanged<DateTime>? onEndChanged;
+  final ValueChanged<DateTime?>? onStartChanged;
+  final ValueChanged<DateTime?>? onEndChanged;
 
   const FilterSection({
     required this.label,

+ 72 - 0
lib/shared/widgets/list_footer.dart

@@ -0,0 +1,72 @@
+import 'package:flutter/material.dart';
+import 'package:tdesign_flutter/tdesign_flutter.dart';
+import '../../core/i18n/app_localizations.dart';
+
+/// 列表底部「没有更多了」提示,仅在列表有数据且已全部加载完成时显示。
+///
+/// ## 用法
+///
+/// 放在 [ListView.builder] 末尾,作为最后一项:
+///
+/// ```dart
+/// ListView.builder(
+///   itemCount: items.length + 1,          // +1 给 footer 留位置
+///   itemBuilder: (_, i) {
+///     if (i == items.length) {
+///       return ListFooter(
+///         itemCount: items.length,
+///         hasMore: page < totalPages,     // 分页时传真实状态
+///       );
+///     }
+///     return _buildItem(items[i]);
+///   },
+/// );
+/// ```
+///
+/// ## 显示逻辑
+///
+/// | itemCount | hasMore | 结果 |
+/// |-----------|---------|------|
+/// | > 0       | false   | 显示「没有更多了」 |
+/// | > 0       | true    | 不显示(还有下一页) |
+/// | 0         | 任意    | 不显示(空列表) |
+class ListFooter extends StatelessWidget {
+  /// 当前列表中的项数。用于判断列表是否为空。
+  final int itemCount;
+
+  /// 是否还有更多数据待加载。
+  ///
+  /// 默认 `false`(全量数据已加载完成)。分页场景下传入 `true` 可避免过早显示。
+  final bool hasMore;
+
+  const ListFooter({super.key, required this.itemCount, this.hasMore = false});
+
+  @override
+  Widget build(BuildContext context) {
+    // 仅在「有数据」且「无更多」时显示
+    if (itemCount == 0 || hasMore) return const SizedBox.shrink();
+
+    final l10n = AppLocalizations.of(context);
+    return Padding(
+      padding: const EdgeInsets.symmetric(vertical: 20),
+      child: Row(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          Icon(
+            TDIcons.component_divider_horizontal,
+            size: 14,
+            color: TDTheme.of(context).textColorPlaceholder,
+          ),
+          const SizedBox(width: 6),
+          Text(
+            l10n.get('noMoreData'),
+            style: TextStyle(
+              fontSize: 12,
+              color: TDTheme.of(context).textColorPlaceholder,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 10 - 3
lib/features/shell/nav_bar_config.dart

@@ -10,6 +10,8 @@ class NavBarConfig {
   final Widget? rightWidget;
   final VoidCallback? onBack;
   final IconData? leadingIcon;
+  final bool hasFilter;
+  final int filterVersion;
 
   const NavBarConfig({
     required this.title,
@@ -18,6 +20,8 @@ class NavBarConfig {
     this.rightWidget,
     this.onBack,
     this.leadingIcon,
+    this.hasFilter = false,
+    this.filterVersion = 0,
   });
 
   /// 根页面默认配置(无返回按钮)
@@ -50,11 +54,14 @@ class NavBarConfig {
       other is NavBarConfig &&
       other.title == title &&
       other.showBack == showBack &&
-      other.showRight == showRight;
-  // 故意不比较 onBack / rightWidget,避免每次 build 新闭包触发无限重建
+      other.showRight == showRight &&
+      other.hasFilter == hasFilter &&
+      other.filterVersion == filterVersion;
+  // 故意不比较 onBack / rightWidget / leadingIcon,避免每次 build 新闭包触发无限重建
 
   @override
-  int get hashCode => Object.hash(title, showBack, showRight);
+  int get hashCode =>
+      Object.hash(title, showBack, showRight, hasFilter, filterVersion);
 }
 
 /// NavBar 配置变更器,内部做相等判断避免无限重建

+ 66 - 41
lib/shared/widgets/status_tag.dart

@@ -1,38 +1,54 @@
-import '../../core/theme/app_colors.dart';
 import 'package:flutter/material.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/i18n/app_localizations.dart';
 
-/// Pencil Component/StatusTag — 审批状态标签
+/// 审批状态标签,深色 / 浅色主题自适应。
 class StatusTag extends StatelessWidget {
-  final String text;
-  final Color? textColor;
-  final Color? backgroundColor;
+  /// 状态原始值(pending / approved / rejected / draft / withdrawn / returned / completed)。
+  final String status;
+
+  /// 可选:自定义标签文本,不传则根据 [status] 自动查 i18n。
+  final String? text;
+
+  /// 可选:是否是付款状态标签(approved 时区分 paid / unpaid)。
+  final String? paymentStatus;
+
   final double borderRadius;
   final EdgeInsets padding;
 
   const StatusTag({
     super.key,
-    required this.text,
-    this.textColor,
-    this.backgroundColor,
+    required this.status,
+    this.text,
+    this.paymentStatus,
     this.borderRadius = 4,
     this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
   });
 
   factory StatusTag.fromStatus(String status, AppLocalizations l10n) {
-    final (bg, fg) = _statusProps(status);
-    return StatusTag(text: _statusLabel(status, l10n), backgroundColor: bg, textColor: fg);
+    return StatusTag(status: status, text: _statusLabel(status, l10n));
   }
 
-  factory StatusTag.fromPaymentStatus(String status, String paymentStatus, AppLocalizations l10n) {
+  factory StatusTag.fromPaymentStatus(
+    String status,
+    String paymentStatus,
+    AppLocalizations l10n,
+  ) {
     if (status == 'approved') {
       if (paymentStatus == 'paid') {
-        return StatusTag(text: l10n.get('paid'), backgroundColor: AppColors.paidBg, textColor: AppColors.paidText);
+        return StatusTag(
+          status: status,
+          text: l10n.get('paid'),
+          paymentStatus: paymentStatus,
+        );
       }
-      return StatusTag(text: l10n.get('statusWaitPay'), backgroundColor: AppColors.unpaidBg, textColor: AppColors.unpaidText);
+      return StatusTag(
+        status: status,
+        text: l10n.get('statusWaitPay'),
+        paymentStatus: paymentStatus,
+      );
     }
-    return StatusTag.fromStatus(status, l10n);
+    return StatusTag(status: status, text: _statusLabel(status, l10n));
   }
 
   static String _statusLabel(String s, AppLocalizations l10n) {
@@ -45,49 +61,58 @@ class StatusTag extends StatelessWidget {
         return l10n.get('statusRejected');
       case 'draft':
         return l10n.get('statusDraft');
-      case 'revoked':
       case 'withdrawn':
-        return l10n.get('statusRevoked');
+        return l10n.get('statusWithdrawn');
+      case 'returned':
+        return l10n.get('returned');
+      case 'completed':
+        return l10n.get('completed');
       default:
         return s;
     }
   }
 
-  static (Color, Color) _statusProps(String s) {
-    switch (s) {
-      case 'pending':
-        return (AppColors.warningBg, AppColors.warning);
-      case 'approved':
-        return (AppColors.successBg, AppColors.success);
-      case 'rejected':
-        return (AppColors.dangerBg, AppColors.danger);
-      case 'draft':
-        return (AppColors.bgDisabled, AppColors.statusGray);
-      case 'revoked':
-      case 'withdrawn':
-        return (AppColors.revokedBg, AppColors.revokedText);
-      default:
-        return (AppColors.bgPage, AppColors.statusGray);
-    }
-  }
-
   @override
   Widget build(BuildContext context) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final (bg, fg) = _statusColors(colors);
     return Container(
       padding: padding,
       decoration: BoxDecoration(
-        color: backgroundColor ?? colors.successBg,
+        color: bg,
         borderRadius: BorderRadius.circular(borderRadius),
       ),
       child: Text(
-        text,
-        style: TextStyle(
-          fontSize: 12,
-          fontWeight: FontWeight.w500,
-          color: textColor ?? colors.success,
-        ),
+        text ?? status,
+        style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: fg),
       ),
     );
   }
+
+  (Color, Color) _statusColors(AppColorsExtension c) {
+    if (status == 'approved' && paymentStatus == 'paid') {
+      return (c.paidBg, c.paidText);
+    }
+    if (status == 'approved' && paymentStatus == 'unpaid') {
+      return (c.unpaidBg, c.unpaidText);
+    }
+    switch (status) {
+      case 'pending':
+        return (c.warningBg, c.warning);
+      case 'approved':
+        return (c.successBg, c.success);
+      case 'rejected':
+        return (c.dangerBg, c.danger);
+      case 'draft':
+        return (c.bgDisabled, c.statusGray);
+      case 'withdrawn':
+        return (c.withdrawnBg, c.withdrawnText);
+      case 'returned':
+        return (c.infoBg, c.infoText);
+      case 'completed':
+        return (c.successBg, c.success);
+      default:
+        return (c.bgPage, c.statusGray);
+    }
+  }
 }