Bläddra i källkod

feat: 全面升级 — 骨架屏、国际化、角色系统、色彩体系、工作台优化

## 骨架屏
- 新增 SkeletonMessageCard 匹配消息卡片布局
- 首次加载骨架屏 vs 下拉刷新 header loading 区分
- 修复空列表下拉刷新闪骨架屏问题
- 移除 FutureProvider.autoDispose 解决切 tab 闪骨架屏

## 国际化 (i18n)
- 列表页/详情页/报表页/工作台/消息页/个人中心全量去硬编码
- TDesign 组件国际化:TDResourceI18nDelegate + 52 个 td* key
- zh_CN/zh_TW/en 三语言同步 (620+ key)

## 角色系统
- currentRoleProvider + 4 派生布尔 provider
- 个人中心角色切换(TDActionSheet)
- 工作台按角色差异化看板
- 经理版待审批卡片置顶 + 下属审批 TDChip 切换
- 财务版报销详情合规查验 + 打款归档角色隔离
- 个人中心管理员权限管理入口
- 报表页角色动态切换

## 色彩体系
- primary 梯度降饱和(浅色端 S∝L²,越亮越柔和)
- primaryLight #B3EDFC→#D8EFFB (L=91%)
- semantic bg 统一柔和度,infoBg/paidBg 去同色
- 金刚区三区域分色(发起蓝/记录信息蓝/报表深蓝)
- 消除跨区域重复图标

## 工作台优化
- 看板卡片左侧色条 + FittedBox 数值自适应 + maxLines:2
- 经理/财务版 3 卡 → 2+1 布局(单卡宽度 +63%)
- 快捷看板图标增强

## 列表页完善
- 费用报销新增待付款/已付款 tab
- 加班/用车新增已撤回 tab
- Tab 状态与 PRD/DB 文档对齐
- ref.invalidate 冗余清理
chengc 1 vecka sedan
förälder
incheckning
a26aabf921
45 ändrade filer med 2722 tillägg och 980 borttagningar
  1. 151 1
      assets/i18n/en.json
  2. 151 1
      assets/i18n/zh_CN.json
  3. 157 1
      assets/i18n/zh_TW.json
  4. 5 0
      lib/app.dart
  5. 18 0
      lib/core/auth/role_provider.dart
  6. 56 34
      lib/core/theme/app_colors.dart
  7. 191 0
      lib/core/theme/tdesign_resource_delegate.dart
  8. 1 2
      lib/features/admin/admin_permissions_page.dart
  9. 32 28
      lib/features/announcement/announcement_create_page.dart
  10. 19 19
      lib/features/announcement/announcement_detail_page.dart
  11. 2 2
      lib/features/announcement/announcement_list_controller.dart
  12. 31 21
      lib/features/announcement/announcement_list_page.dart
  13. 79 6
      lib/features/expense/expense_detail_page.dart
  14. 1 1
      lib/features/expense/expense_list_controller.dart
  15. 207 53
      lib/features/expense/expense_list_page.dart
  16. 72 67
      lib/features/expense_application/expense_application_apply_page.dart
  17. 28 23
      lib/features/expense_application/expense_application_detail_page.dart
  18. 1 1
      lib/features/expense_application/expense_application_list_controller.dart
  19. 252 91
      lib/features/expense_application/expense_application_list_page.dart
  20. 4 2
      lib/features/home/home_controller.dart
  21. 160 126
      lib/features/home/home_page.dart
  22. 23 0
      lib/features/messages/message_api.dart
  23. 2 0
      lib/features/messages/message_controller.dart
  24. 71 25
      lib/features/messages/message_list_page.dart
  25. 39 19
      lib/features/outing_log/outing_log_create_page.dart
  26. 18 14
      lib/features/outing_log/outing_log_detail_page.dart
  27. 1 1
      lib/features/outing_log/outing_log_list_controller.dart
  28. 90 65
      lib/features/outing_log/outing_log_list_page.dart
  29. 1 1
      lib/features/overtime/overtime_list_controller.dart
  30. 226 74
      lib/features/overtime/overtime_list_page.dart
  31. 53 0
      lib/features/profile/profile_page.dart
  32. 29 22
      lib/features/report/expense_apply_detail_report_page.dart
  33. 38 27
      lib/features/report/expense_detail_report_page.dart
  34. 21 19
      lib/features/report/outing_log_report_page.dart
  35. 29 22
      lib/features/report/overtime_detail_report_page.dart
  36. 37 25
      lib/features/report/vehicle_detail_report_page.dart
  37. 43 43
      lib/features/vehicle/vehicle_apply_page.dart
  38. 54 48
      lib/features/vehicle/vehicle_detail_page.dart
  39. 1 1
      lib/features/vehicle/vehicle_list_controller.dart
  40. 231 76
      lib/features/vehicle/vehicle_list_page.dart
  41. 9 6
      lib/shared/models/approval_status.dart
  42. 2 0
      lib/shared/models/json_helper.dart
  43. 3 1
      lib/shared/widgets/list_card.dart
  44. 70 0
      lib/shared/widgets/skeleton_list_card.dart
  45. 13 12
      lib/shared/widgets/status_tag.dart

+ 151 - 1
assets/i18n/en.json

@@ -12,6 +12,8 @@
   "noVehicles": "No Vehicle Records",
   "noAnnouncements": "No Announcements",
   "noOutingLogs": "No Outing Logs",
+  "noDrafts": "No Drafts",
+  "noCompletedRecords": "No Completed Records",
   "noExpenseApplications": "No Applications",
   "initiate": "New",
   "records": "Records",
@@ -30,6 +32,7 @@
   "companyAnnouncements": "Announcements",
   "myApprovals": "My Approvals",
   "myApplications": "My Applications",
+  "subordinateRecords": "Subordinate Records",
   "myExpenses": "My Expenses",
   "outingLog": "Outing Log",
   "announcements": "Announcements",
@@ -50,11 +53,15 @@
   "markUnread": "Unread",
   "delete": "Delete",
   "all": "All",
+  "myDrafts": "My Drafts",
   "draft": "Draft",
+  "completed": "Completed",
   "pending": "Pending",
   "approved": "Approved",
   "rejected": "Rejected",
   "revoked": "Revoked",
+  "expired": "Expired",
+  "paid": "Paid",
   "returned": "Returned",
   "save": "Save",
   "submit": "Submit",
@@ -127,6 +134,7 @@
   "urgent": "Urgent",
   "public": "All",
   "newComment": "New Comment",
+  "salespersonLabel": "Sales: {name} · {dept}",
   "noPlan": "No Plan",
   "noWorkSummary": "No Work Summary",
   "downloadAttachment": "Download",
@@ -470,5 +478,147 @@
   "licensePlate": "Plate No.",
   "vehiclePurpose": "Vehicle Purpose",
   "addExpenseDetailFirst": "Please add expense details first",
-  "submitConfirmContent": "Mileage and cost cannot be modified after submission. Continue?"
+  "submitConfirmContent": "Mileage and cost cannot be modified after submission. Continue?",
+  "workday": "Workday",
+  "weekend": "Weekend",
+  "holiday": "Holiday",
+  "paid": "Paid",
+  "businessShort": "Business",
+  "exportPlaceholder": "Export (placeholder)",
+  "unitItem": "items",
+  "addAtLeastOneDetail": "Please add at least one expense detail",
+  "byDeptHint": "Multi-select by department tree",
+  "byUserHint": "Multi-select by employee search",
+  "checkInAddress": "Check-in Address",
+  "checkInTime": "Check-in Time",
+  "completeFormInfo": "Please complete the form",
+  "confirmExit": "Confirm Exit",
+  "confirmReset": "Confirm Reset",
+  "continueEditing": "Continue Editing",
+  "customerName": "Customer Name",
+  "detailRemark": "Detail Remark",
+  "discardAndExit": "Discard & Exit",
+  "enterNumber": "Enter number",
+  "entertainmentExpense": "Entertainment Expense",
+  "entertainmentLevel": "Entertainment Level",
+  "entertainmentTargetUnit": "Target Organization",
+  "estimatedEndDate": "Estimated End Date",
+  "estimatedStartDate": "Estimated Start Date",
+  "expenseCategory": "Expense Category",
+  "externalCount": "External Attendees",
+  "followUpOptional": "Follow-up plan (optional)",
+  "gpsLocatingWait": "GPS locating, please wait",
+  "important": "Important",
+  "internalCount": "Internal Attendees",
+  "isOvernight": "Overnight",
+  "isTaxIncluded": "Tax Included",
+  "mapPickerComingSoon": "Map picker coming soon",
+  "markedAsRead": "Marked as read",
+  "meetingExpense": "Meeting Expense",
+  "meetingLocation": "Meeting Location",
+  "mockAttachmentAdded": "Attachment added (mock)",
+  "mockExpandReadList": "Mock: Expand read list",
+  "mockExpandUnreadList": "Mock: Expand unread list",
+  "mockOpenNavigation": "Mock: Open native navigation",
+  "optional": "Optional",
+  "overBudgetTriggerApproval": "Over budget. Submission will trigger executive approval.",
+  "personUnit": "person(s)",
+  "pleaseEnter": "Please enter",
+  "pleaseEnterLocation": "Please enter location",
+  "pleaseEnterMeetingLocation": "Please enter meeting location",
+  "quantity": "Quantity",
+  "quantityPricePositive": "Quantity and price must be greater than 0",
+  "relatedContractNo": "Related Contract No.",
+  "resetWarning": "This will clear all content. This action cannot be undone.",
+  "returnTimeMustLater": "Return time must be later than departure time",
+  "salesperson": "Salesperson",
+  "searchEmployee": "Search Employee",
+  "selectAtLeastOneExpenseType": "Please select at least one expense type",
+  "selectDate": "Select Date",
+  "selectEntertainmentLevel": "Select Entertainment Level",
+  "selectEstimatedEndDate": "Please select estimated end date",
+  "selectEstimatedStartDate": "Please select estimated start date",
+  "selectExpenseCategory": "Select Expense Category",
+  "selectExpiryDate": "Select Expiry Date",
+  "selectTransport": "Select Transport",
+  "selectUnit": "Select Unit",
+  "submitFailedRetry": "Submission failed, please try again later",
+  "submittedAwaitingApproval": "Submitted, awaiting approval",
+  "tapToViewNavigation": "Tap to view navigation",
+  "transportType": "Transport Type",
+  "travelExpense": "Travel Expense",
+  "unit": "Unit",
+  "unitPrice": "Unit Price",
+  "unsavedContentWarning": "Unsaved content will be lost. Continue?",
+  "venue": "Venue",
+  "workSummaryRequiredHint": "Please fill in work summary (required)",
+  "dingPromptSent": "Sent reminders to {count} unread employees",
+  "readCount": "Read {count} person(s)",
+  "unreadCount": "Unread {count} person(s)",
+  "typeAndPublishDate": "{type} · Will display after publishing",
+  "titleNotFilled": "(Title not filled)",
+  "contentNotFilled": "(Content not filled)",
+  "mockPhotoTaken": "Mock photo: Photo #{idx} taken (watermark: {time} | {lat}, {lng})",
+  "announcementExpired": "This announcement expired on {date}",
+  "returnCarArchivedAt": "Return archived at {time}",
+  "selectProject": "Select Project",
+  "selectedCount": "{count} selected",
+  "watermarkHintDynamic": "Auto watermark: server time + GPS ({lat}°N, {lng}°E)",
+  "tdOpen": "On",
+  "tdClose": "Off",
+  "tdCancel": "Cancel",
+  "tdConfirm": "Confirm",
+  "tdOther": "Other",
+  "tdReset": "Reset",
+  "tdLoading": "Loading",
+  "tdLoadingWithPoint": "Loading...",
+  "tdKnew": "Got it",
+  "tdRefreshing": "Refreshing",
+  "tdReleaseRefresh": "Release to refresh",
+  "tdPullToRefresh": "Pull to refresh",
+  "tdCompleteRefresh": "Refresh complete",
+  "tdDays": "d",
+  "tdHours": "h",
+  "tdMinutes": "min",
+  "tdSeconds": "s",
+  "tdMilliseconds": "ms",
+  "tdYearLabel": "Y",
+  "tdMonthLabel": "M",
+  "tdDateLabel": "D",
+  "tdWeeksLabel": "W",
+  "tdSunday": "Sun",
+  "tdMonday": "Mon",
+  "tdTuesday": "Tue",
+  "tdWednesday": "Wed",
+  "tdThursday": "Thu",
+  "tdFriday": "Fri",
+  "tdSaturday": "Sat",
+  "tdYear": "Year",
+  "tdJanuary": "Jan",
+  "tdFebruary": "Feb",
+  "tdMarch": "Mar",
+  "tdApril": "Apr",
+  "tdMay": "May",
+  "tdJune": "Jun",
+  "tdJuly": "Jul",
+  "tdAugust": "Aug",
+  "tdSeptember": "Sep",
+  "tdOctober": "Oct",
+  "tdNovember": "Nov",
+  "tdDecember": "Dec",
+  "tdTime": "Time",
+  "tdStart": "Start",
+  "tdEnd": "End",
+  "tdNotRated": "Not rated",
+  "tdCascadeLabel": "Select option",
+  "tdBack": "Back",
+  "tdTop": "Top",
+  "tdEmptyData": "No data",
+  "confirmPaymentAndArchive": "Confirm Payment & Archive",
+  "confirmPaymentAndArchiveTip": "Confirm payment and archive for this expense? This action cannot be undone.",
+  "nextPendingPayment": "Next Pending Payment",
+  "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."
 }

+ 151 - 1
assets/i18n/zh_CN.json

@@ -12,6 +12,8 @@
   "noVehicles": "暂无用车记录",
   "noAnnouncements": "暂无公告",
   "noOutingLogs": "暂无外出日志",
+  "noDrafts": "暂无草稿",
+  "noCompletedRecords": "暂无已完成记录",
   "noExpenseApplications": "暂无报销申请",
   "initiate": "发起",
   "records": "记录",
@@ -30,6 +32,7 @@
   "companyAnnouncements": "公司公告",
   "myApprovals": "我的审批",
   "myApplications": "我的申请",
+  "subordinateRecords": "下属记录",
   "myExpenses": "我的报销",
   "outingLog": "外出日志",
   "announcements": "公告通知",
@@ -50,11 +53,15 @@
   "markUnread": "未读",
   "delete": "删除",
   "all": "全部",
+  "myDrafts": "我的草稿",
   "draft": "草稿",
+  "completed": "已完成",
   "pending": "审批中",
   "approved": "已通过",
   "rejected": "已拒绝",
   "revoked": "已撤回",
+  "expired": "已过期",
+  "paid": "已付款",
   "returned": "已还车",
   "save": "保存",
   "submit": "提交",
@@ -127,6 +134,7 @@
   "urgent": "紧急",
   "public": "全员",
   "newComment": "新点评",
+  "salespersonLabel": "业务员:{name} · {dept}",
   "noPlan": " · 暂无计划",
   "noWorkSummary": "暂无工作总结",
   "downloadAttachment": "附件下载",
@@ -470,5 +478,147 @@
   "licensePlate": "车牌号",
   "vehiclePurpose": "用车目的",
   "addExpenseDetailFirst": "请在明细中添加费用项",
-  "submitConfirmContent": "提交后里程和费用不可再修改,是否继续?"
+  "submitConfirmContent": "提交后里程和费用不可再修改,是否继续?",
+  "workday": "工作日",
+  "weekend": "休息日",
+  "holiday": "节假日",
+  "paid": "已付款",
+  "businessShort": "商务",
+  "exportPlaceholder": "导出功能(占位)",
+  "unitItem": "笔",
+  "addAtLeastOneDetail": "请至少添加一行费用明细",
+  "byDeptHint": "按部门树多选",
+  "byUserHint": "按员工搜索多选",
+  "checkInAddress": "签到地址",
+  "checkInTime": "签到时间",
+  "completeFormInfo": "请完善表单信息",
+  "confirmExit": "确认退出",
+  "confirmReset": "确认重置",
+  "continueEditing": "继续编辑",
+  "customerName": "客户名",
+  "detailRemark": "明细说明",
+  "discardAndExit": "放弃并退出",
+  "enterNumber": "请输入数字",
+  "entertainmentExpense": "招待费专用",
+  "entertainmentLevel": "招待层级",
+  "entertainmentTargetUnit": "招待对象单位",
+  "estimatedEndDate": "预计结束日期",
+  "estimatedStartDate": "预计开始日期",
+  "expenseCategory": "费用类别",
+  "externalCount": "外部人数",
+  "followUpOptional": "后续推进计划(选填)",
+  "gpsLocatingWait": "GPS定位中,请稍后",
+  "important": "重要",
+  "internalCount": "内部陪同人数",
+  "isOvernight": "是否过夜",
+  "isTaxIncluded": "是否含税",
+  "mapPickerComingSoon": "地图选点即将开放",
+  "markedAsRead": "已标记为已读",
+  "meetingExpense": "会议费专用",
+  "meetingLocation": "会议地点",
+  "mockAttachmentAdded": "已添加附件(mock)",
+  "mockExpandReadList": "模拟:展开已读员工列表",
+  "mockExpandUnreadList": "模拟:展开未读员工列表",
+  "mockOpenNavigation": "模拟:打开原生导航",
+  "optional": "选填",
+  "overBudgetTriggerApproval": "您的申请金额已超支,提交后将自动触发高管特批流程",
+  "personUnit": "人",
+  "pleaseEnter": "请输入",
+  "pleaseEnterLocation": "请输入地点",
+  "pleaseEnterMeetingLocation": "请输入会议地点",
+  "quantity": "数量",
+  "quantityPricePositive": "数量和单价必须大于0",
+  "relatedContractNo": "关联合同号",
+  "resetWarning": "将清空所有已填内容,此操作不可撤销",
+  "returnTimeMustLater": "还车时间必须晚于出车时间",
+  "salesperson": "业务员",
+  "searchEmployee": "搜索员工",
+  "selectAtLeastOneExpenseType": "请至少选择一项费用类型",
+  "selectDate": "选择日期",
+  "selectEntertainmentLevel": "选择招待层级",
+  "selectEstimatedEndDate": "请选择预计结束日期",
+  "selectEstimatedStartDate": "请选择预计开始日期",
+  "selectExpenseCategory": "选择费用类别",
+  "selectExpiryDate": "选择过期日期",
+  "selectTransport": "选择交通工具",
+  "selectUnit": "选择单位",
+  "submitFailedRetry": "提交失败,请稍后重试",
+  "submittedAwaitingApproval": "已提交,等待审批",
+  "tapToViewNavigation": "点击查看导航",
+  "transportType": "交通方式",
+  "travelExpense": "差旅费专用",
+  "unit": "单位",
+  "unitPrice": "单价",
+  "unsavedContentWarning": "当前内容尚未保存,是否退出?",
+  "venue": "场地",
+  "workSummaryRequiredHint": "请填写本次外勤工作总结(必填)",
+  "dingPromptSent": "已向 {count} 名未读员工发送催办通知",
+  "readCount": "已读 {count} 人",
+  "unreadCount": "未读 {count} 人",
+  "typeAndPublishDate": "{type} · 发布后将显示",
+  "titleNotFilled": "(未填写标题)",
+  "contentNotFilled": "(未填写正文)",
+  "mockPhotoTaken": "模拟拍照:已拍摄第 {idx} 张照片(含水印:{time} | {lat}, {lng})",
+  "announcementExpired": "该公告已于 {date} 过期",
+  "returnCarArchivedAt": "已还车归档于 {time}",
+  "selectProject": "选择关联项目",
+  "selectedCount": "已选 {count} 人",
+  "watermarkHintDynamic": "照片将自动添加水印:服务器授时 + GPS经纬度({lat}°N, {lng}°E)",
+  "tdOpen": "开",
+  "tdClose": "关",
+  "tdCancel": "取消",
+  "tdConfirm": "确定",
+  "tdOther": "其它",
+  "tdReset": "重置",
+  "tdLoading": "加载中",
+  "tdLoadingWithPoint": "加载中...",
+  "tdKnew": "知道了",
+  "tdRefreshing": "正在刷新",
+  "tdReleaseRefresh": "松开刷新",
+  "tdPullToRefresh": "下拉刷新",
+  "tdCompleteRefresh": "刷新完成",
+  "tdDays": "天",
+  "tdHours": "时",
+  "tdMinutes": "分",
+  "tdSeconds": "秒",
+  "tdMilliseconds": "毫秒",
+  "tdYearLabel": "年",
+  "tdMonthLabel": "月",
+  "tdDateLabel": "日",
+  "tdWeeksLabel": "周",
+  "tdSunday": "日",
+  "tdMonday": "一",
+  "tdTuesday": "二",
+  "tdWednesday": "三",
+  "tdThursday": "四",
+  "tdFriday": "五",
+  "tdSaturday": "六",
+  "tdYear": "年",
+  "tdJanuary": "1月",
+  "tdFebruary": "2月",
+  "tdMarch": "3月",
+  "tdApril": "4月",
+  "tdMay": "5月",
+  "tdJune": "6月",
+  "tdJuly": "7月",
+  "tdAugust": "8月",
+  "tdSeptember": "9月",
+  "tdOctober": "10月",
+  "tdNovember": "11月",
+  "tdDecember": "12月",
+  "tdTime": "时间",
+  "tdStart": "开始",
+  "tdEnd": "结束",
+  "tdNotRated": "未评分",
+  "tdCascadeLabel": "选择选项",
+  "tdBack": "返回",
+  "tdTop": "顶部",
+  "tdEmptyData": "暂无数据",
+  "confirmPaymentAndArchive": "确认打款并归档",
+  "confirmPaymentAndArchiveTip": "确认将本笔报销打款并归档?操作后不可撤销。",
+  "nextPendingPayment": "下一笔待付款",
+  "allPaymentsProcessed": "全部待付款单据已处理完毕",
+  "paymentArchiveSuccess": "打款归档成功",
+  "withdrawConfirm": "确认撤回",
+  "withdrawConfirmTip": "确认撤回该申请吗?撤回后审批流程将终止。"
 }

+ 157 - 1
assets/i18n/zh_TW.json

@@ -12,6 +12,8 @@
   "noVehicles": "暫無用車記錄",
   "noAnnouncements": "暫無公告",
   "noOutingLogs": "暫無外勤日誌",
+  "noDrafts": "暫無草稿",
+  "noCompletedRecords": "暫無已完成記錄",
   "noExpenseApplications": "暫無申請",
   "initiate": "發起",
   "records": "記錄",
@@ -30,6 +32,7 @@
   "companyAnnouncements": "公司公告",
   "myApprovals": "我的審批",
   "myApplications": "我的申請",
+  "subordinateRecords": "下屬記錄",
   "myExpenses": "我的報銷",
   "outingLog": "外勤日誌",
   "announcements": "公告通知",
@@ -50,11 +53,15 @@
   "markUnread": "未讀",
   "delete": "刪除",
   "all": "全部",
+  "myDrafts": "我的草稿",
   "draft": "草稿",
+  "completed": "已完成",
   "pending": "審批中",
   "approved": "已通過",
   "rejected": "已拒絕",
   "revoked": "已撤回",
+  "expired": "已過期",
+  "paid": "已付款",
   "returned": "已還車",
   "save": "保存",
   "submit": "提交",
@@ -127,6 +134,7 @@
   "urgent": "緊急",
   "public": "全員",
   "newComment": "新點評",
+  "salespersonLabel": "業務員:{name} · {dept}",
   "noPlan": " · 暫無計劃",
   "noWorkSummary": "暫無工作總結",
   "downloadAttachment": "附件下載",
@@ -143,5 +151,153 @@
   "opinion": "意見:",
   "currentNode": "當前節點",
   "waitHandle": "待處理",
-  "inputComment": "輸入點評…"
+  "inputComment": "輸入點評…",
+  "workday": "工作日",
+  "weekend": "休息日",
+  "holiday": "節假日",
+  "businessShort": "商務",
+  "exportPlaceholder": "匯出功能(佔位)",
+  "filterStatus": "審批狀態",
+  "filterPayment": "付款狀態",
+  "filterVehicle": "車輛篩選",
+  "filterUsage": "用途篩選",
+  "filterReception": "接待",
+  "custom": "自定義",
+  "official": "公務",
+  "unitItem": "筆",
+  "addAtLeastOneDetail": "請至少新增一行費用明細",
+  "byDeptHint": "按部門樹多選",
+  "byUserHint": "按員工搜尋多選",
+  "checkInAddress": "簽到地址",
+  "checkInTime": "簽到時間",
+  "completeFormInfo": "請完善表單資訊",
+  "confirmExit": "確認退出",
+  "confirmReset": "確認重置",
+  "continueEditing": "繼續編輯",
+  "customerName": "客戶名",
+  "detailRemark": "明細說明",
+  "discardAndExit": "放棄並退出",
+  "enterNumber": "請輸入數字",
+  "entertainmentExpense": "招待費專用",
+  "entertainmentLevel": "招待層級",
+  "entertainmentTargetUnit": "招待對象單位",
+  "estimatedEndDate": "預計結束日期",
+  "estimatedStartDate": "預計開始日期",
+  "expenseCategory": "費用類別",
+  "externalCount": "外部人數",
+  "followUpOptional": "後續推進計劃(選填)",
+  "gpsLocatingWait": "GPS定位中,請稍後",
+  "important": "重要",
+  "internalCount": "內部陪同人數",
+  "isOvernight": "是否過夜",
+  "isTaxIncluded": "是否含稅",
+  "mapPickerComingSoon": "地圖選點即將開放",
+  "markedAsRead": "已標記為已讀",
+  "meetingExpense": "會議費專用",
+  "meetingLocation": "會議地點",
+  "mockAttachmentAdded": "已新增附件(mock)",
+  "mockExpandReadList": "模擬:展開已讀員工列表",
+  "mockExpandUnreadList": "模擬:展開未讀員工列表",
+  "mockOpenNavigation": "模擬:打開原生導航",
+  "optional": "選填",
+  "overBudgetTriggerApproval": "您的申請金額已超支,提交後將自動觸發高管特批流程",
+  "personUnit": "人",
+  "pleaseEnter": "請輸入",
+  "pleaseEnterLocation": "請輸入地點",
+  "pleaseEnterMeetingLocation": "請輸入會議地點",
+  "quantity": "數量",
+  "quantityPricePositive": "數量和單價必須大於0",
+  "relatedContractNo": "關聯合約號",
+  "resetWarning": "將清空所有已填內容,此操作不可撤銷",
+  "returnTimeMustLater": "還車時間必須晚於出車時間",
+  "salesperson": "業務員",
+  "searchEmployee": "搜尋員工",
+  "selectAtLeastOneExpenseType": "請至少選擇一項費用類型",
+  "selectDate": "選擇日期",
+  "selectEntertainmentLevel": "選擇招待層級",
+  "selectEstimatedEndDate": "請選擇預計結束日期",
+  "selectEstimatedStartDate": "請選擇預計開始日期",
+  "selectExpenseCategory": "選擇費用類別",
+  "selectExpiryDate": "選擇過期日期",
+  "selectTransport": "選擇交通工具",
+  "selectUnit": "選擇單位",
+  "submitFailedRetry": "提交失敗,請稍後重試",
+  "submittedAwaitingApproval": "已提交,等待審批",
+  "tapToViewNavigation": "點擊檢視導航",
+  "transportType": "交通方式",
+  "travelExpense": "差旅費專用",
+  "unit": "單位",
+  "unitPrice": "單價",
+  "unsavedContentWarning": "當前內容尚未儲存,是否退出?",
+  "venue": "場地",
+  "workSummaryRequiredHint": "請填寫本次外勤工作總結(必填)",
+  "dingPromptSent": "已向 {count} 名未讀員工發送催辦通知",
+  "readCount": "已讀 {count} 人",
+  "unreadCount": "未讀 {count} 人",
+  "typeAndPublishDate": "{type} · 發佈後將顯示",
+  "titleNotFilled": "(未填寫標題)",
+  "contentNotFilled": "(未填寫正文)",
+  "mockPhotoTaken": "模擬拍照:已拍攝第 {idx} 張照片(含浮水印:{time} | {lat}, {lng})",
+  "announcementExpired": "該公告已於 {date} 過期",
+  "returnCarArchivedAt": "已還車歸檔於 {time}",
+  "selectProject": "選擇關聯項目",
+  "selectedCount": "已選 {count} 人",
+  "watermarkHintDynamic": "照片將自動加入浮水印:伺服器授時 + GPS經緯度({lat}°N, {lng}°E)",
+  "tdOpen": "開",
+  "tdClose": "關",
+  "tdCancel": "取消",
+  "tdConfirm": "確定",
+  "tdOther": "其它",
+  "tdReset": "重置",
+  "tdLoading": "載入中",
+  "tdLoadingWithPoint": "載入中...",
+  "tdKnew": "知道了",
+  "tdRefreshing": "正在刷新",
+  "tdReleaseRefresh": "鬆開刷新",
+  "tdPullToRefresh": "下拉刷新",
+  "tdCompleteRefresh": "刷新完成",
+  "tdDays": "天",
+  "tdHours": "時",
+  "tdMinutes": "分",
+  "tdSeconds": "秒",
+  "tdMilliseconds": "毫秒",
+  "tdYearLabel": "年",
+  "tdMonthLabel": "月",
+  "tdDateLabel": "日",
+  "tdWeeksLabel": "週",
+  "tdSunday": "日",
+  "tdMonday": "一",
+  "tdTuesday": "二",
+  "tdWednesday": "三",
+  "tdThursday": "四",
+  "tdFriday": "五",
+  "tdSaturday": "六",
+  "tdYear": "年",
+  "tdJanuary": "1月",
+  "tdFebruary": "2月",
+  "tdMarch": "3月",
+  "tdApril": "4月",
+  "tdMay": "5月",
+  "tdJune": "6月",
+  "tdJuly": "7月",
+  "tdAugust": "8月",
+  "tdSeptember": "9月",
+  "tdOctober": "10月",
+  "tdNovember": "11月",
+  "tdDecember": "12月",
+  "tdTime": "時間",
+  "tdStart": "開始",
+  "tdEnd": "結束",
+  "tdNotRated": "未評分",
+  "tdCascadeLabel": "選擇選項",
+  "tdBack": "返回",
+  "tdTop": "頂部",
+  "tdEmptyData": "暫無數據",
+  "confirmPaymentAndArchive": "確認打款並歸檔",
+  "confirmPaymentAndArchiveTip": "確認將本筆報銷打款並歸檔?操作後不可撤銷。",
+  "nextPendingPayment": "下一筆待付款",
+  "allPaymentsProcessed": "全部待付款單據已處理完畢",
+  "paymentArchiveSuccess": "打款歸檔成功",
+  "withdrawConfirm": "確認撤回",
+  "withdrawConfirmTip": "確認撤回該申請嗎?撤回後審批流程將終止。"
 }

+ 5 - 0
lib/app.dart

@@ -5,6 +5,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'core/theme/app_theme.dart';
 import 'core/theme/theme_mode_provider.dart';
+import 'core/theme/tdesign_resource_delegate.dart';
 import 'core/router/app_router.dart';
 import 'core/network/api_client.dart';
 import 'core/auth/auth_service.dart';
@@ -35,6 +36,10 @@ class App extends ConsumerWidget {
     final locale = ref.watch(localeProvider);
     final themeMode = ref.watch(themeModeProvider);
     TDTheme.needMultiTheme();
+    TDTheme.setResourceBuilder(
+      (context) => TDResourceI18nDelegate(context),
+      needAlwaysBuild: true,
+    );
     return MaterialApp.router(
         key: ValueKey(locale),
         title: 'TBOSS OA',

+ 18 - 0
lib/core/auth/role_provider.dart

@@ -0,0 +1,18 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+/// 当前用户角色 Provider,默认 'admin' 方便开发测试全部视图
+final currentRoleProvider = StateProvider<String>((ref) => 'admin');
+
+/// 角色权限派生 Provider(布尔值)
+final isAdminProvider = Provider<bool>((ref) => ref.watch(currentRoleProvider) == 'admin');
+final isFinanceProvider = Provider<bool>((ref) => ref.watch(currentRoleProvider) == 'finance');
+final isManagerProvider = Provider<bool>((ref) => ref.watch(currentRoleProvider) == 'manager');
+final isEmployeeProvider = Provider<bool>((ref) => ref.watch(currentRoleProvider) == 'employee');
+
+/// 角色列表(value -> 中文标签)
+const roleOptions = [
+  ('admin', '系统管理员'),
+  ('finance', '财务人员'),
+  ('manager', '部门经理'),
+  ('employee', '普通员工'),
+];

+ 56 - 34
lib/core/theme/app_colors.dart

@@ -8,17 +8,31 @@ class AppColors {
   AppColors._();
 
   // ═══════════════════════════════════════════
-  //  品牌色梯度(H=199° S=100% L 渐变)
-  // ═══════════════════════════════════════════
-  static const Color primary50 = Color(0xFFE6F9FE);   // L=95% 最浅背景
-  static const Color primary100 = Color(0xFFB3EDFC);  // L=85% Tag 浅底
-  static const Color primary200 = Color(0xFF80DFFA);  // L=74% hover
-  static const Color primary300 = Color(0xFF4DD1F8);  // L=64% 边框
-  static const Color primary400 = Color(0xFF1ABEF5);  // L=53% 次级按钮
+  //  品牌色梯度(H=199° 饱和递减式浅色)
+  // ═══════════════════════════════════════════
+  //
+  //  关键改进:浅色端降低饱和度,使 primaryLight 更"透气"
+  //    S = S₅₀₀ × (L/L₅₀₀)² → 越亮越柔和
+  //
+  //  primary50   L=96% S≈30%  最浅底色,仅微弱的品牌色感
+  //  primary100  L=88% S≈42%  Tag 底/图标底/选中 Chip ← primaryLight
+  //  primary200  L=78% S≈58%  hover 态
+  //  primary300  L=66% S≈76%  边框/聚焦环
+  //  primary400  L=54% S≈90%  次级按钮
+  //  primary500  L=48% S=100% ★ 主色 #00ABF3
+  //  primary600  L=38% S=100% 按压态/深文字
+  //  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 primary700 = Color(0xFF006794);  // L=29% 深文字
+  static const Color primary800 = Color(0xFF004563);  // L=19% 深底
   static const Color primary900 = Color(0xFF002233);  // L=10% 最深
 
   // ── 兼容旧名 ──
@@ -59,11 +73,11 @@ class AppColors {
   //  语义色
   // ═══════════════════════════════════════════
   static const Color success = Color(0xFF0EA371);       // 成功/已通过
-  static const Color successBg = Color(0xFFE6F9F2);     // 成功浅底
+  static const Color successBg = Color(0xFFEBF9F3);     // 成功浅底(微调更清新)
   static const Color warning = Color(0xFFE37318);       // 警告/审批中
-  static const Color warningBg = Color(0xFFFFF3E8);     // 警告浅底
+  static const Color warningBg = Color(0xFFFFF6ED);     // 警告浅底(更温暖)
   static const Color danger = Color(0xFFDC2626);        // 危险/已拒绝/删除
-  static const Color dangerBg = Color(0xFFFEF2F2);      // 危险浅底
+  static const Color dangerBg = Color(0xFFFEF4F4);      // 危险浅底(更柔和)
 
   // ═══════════════════════════════════════════
   //  金额色(独立于成功/危险语义)
@@ -76,27 +90,27 @@ class AppColors {
   static const Color amountPrimary = amountPositive;
 
   // ═══════════════════════════════════════════
-  //  信息色(已还车/已付款/GPS/时间线)
+  //  信息色(已还车/GPS/时间线/审批信息
   // ═══════════════════════════════════════════
   static const Color infoText = Color(0xFF3B82C4);
-  static const Color infoBg = Color(0xFFEBF4FC);
-  static const Color infoLightBg = Color(0xFFF0F7FD);
+  static const Color infoBg = Color(0xFFEDF6FC);
+  static const Color infoLightBg = Color(0xFFF4F9FD);
   static const Color infoBorder = Color(0xFFC8DFF0);
   static const Color timelineInactive = Color(0xFFE5E8EB);
 
   // ═══════════════════════════════════════════
-  //  支付状态色
+  //  支付状态色(区别于信息色)
   // ═══════════════════════════════════════════
   static const Color paidText = Color(0xFF2563B0);      // 已付款-深蓝
-  static const Color paidBg = Color(0xFFEBF4FC);        // 已付款-浅蓝
+  static const Color paidBg = Color(0xFFF0F6FF);        // 已付款-浅蓝(区别于 infoBg)
   static const Color unpaidText = Color(0xFFCA8A04);    // 待付款-琥珀
-  static const Color unpaidBg = Color(0xFFFFF9EB);      // 待付款-浅琥珀
+  static const Color unpaidBg = Color(0xFFFFFCEF);      // 待付款-浅琥珀(更柔和)
 
   // ═══════════════════════════════════════════
   //  撤回/草稿/过期
   // ═══════════════════════════════════════════
   static const Color revokedText = Color(0xFF7C6F8A);
-  static const Color revokedBg = Color(0xFFF4F2F5);
+  static const Color revokedBg = Color(0xFFF6F3F8);
   static const Color bgExpired = Color(0xFFF9FAFB);
   static const Color swipeDeleteBg = Color(0xFFF2F4F6);
 
@@ -113,7 +127,7 @@ class AppColors {
   //  遮罩 / 高亮
   // ═══════════════════════════════════════════
   static const Color overlay = Color(0x66000000);       // 弹窗蒙层 40%
-  static const Color highlight = Color(0xFFE6F9FE);     // 列表选中高亮(primary50)
+  static const Color highlight = Color(0xFFEDF7FD);     // 列表选中高亮(= primary50)
 }
 
 /// 深色主题 — 感知等价映射
@@ -122,12 +136,20 @@ class AppColors {
 class AppDarkColors {
   AppDarkColors._();
 
-  // ── 品牌色梯度(主色保持,浅色调暗)──
-  static const Color primary50 = Color(0xFF0D2430);
-  static const Color primary100 = Color(0xFF103545);
-  static const Color primary200 = Color(0xFF13475C);
-  static const Color primary300 = Color(0xFF165973);
-  static const Color primary400 = Color(0xFF19708E);
+  // ── 品牌色梯度(主色保持,浅色端提升明度以区分深色背景)──
+  //
+  //  改进:浅色端提高 L 值,使 primaryLight 在深色背景下清晰可辨
+  //    primary50  L=13%  极暗底色
+  //    primary100 L=20%  Tag 底/图标底 ← primaryLight
+  //    primary200 L=27%  hover
+  //    primary300 L=34%  边框
+  //    primary400 L=42%  次级按钮
+  //    primary500 L=48%  ★ 主色 #00ABF3
+  static const Color primary50 = Color(0xFF112835);
+  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);
   static const Color primary500 = Color(0xFF00ABF3);
   static const Color primary600 = Color(0xFF33BBF5);
   static const Color primary700 = Color(0xFF66CCF8);
@@ -135,7 +157,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;
 
   // ── 中性灰 ──
@@ -164,13 +186,13 @@ class AppDarkColors {
 
   static const Color border = Color(0xFF333333);
 
-  // ── 语义色(饱和度微降,适配深色底)──
+  // ── 语义色(深色底适配:亮前景 + 暗背景)──
   static const Color success = Color(0xFF22C55E);
   static const Color successBg = Color(0xFF052E16);
   static const Color warning = Color(0xFFF59E0B);
   static const Color warningBg = Color(0xFF2D1B06);
   static const Color danger = Color(0xFFEF4444);
-  static const Color dangerBg = Color(0xFF2D0A0A);
+  static const Color dangerBg = Color(0xFF2E0B0B);
 
   // ── 金额色 ──
   static const Color amountPositive = Color(0xFF5BB8F0); // 正金额(亮蓝,适配深色底)
@@ -181,13 +203,13 @@ class AppDarkColors {
   // ── 信息色 ──
   static const Color infoText = Color(0xFF6AA8E6);
   static const Color infoBg = Color(0xFF0E1F33);
-  static const Color infoLightBg = Color(0xFF112238);
+  static const Color infoLightBg = Color(0xFF11243A);
   static const Color infoBorder = Color(0xFF1E3D5C);
   static const Color timelineInactive = Color(0xFF333333);
 
-  // ── 支付状态 ──
+  // ── 支付状态(深色底:信息色与支付色区分)──
   static const Color paidText = Color(0xFF6AA8E6);
-  static const Color paidBg = Color(0xFF0E1F33);
+  static const Color paidBg = Color(0xFF0F2238);        // 区别于 infoBg
   static const Color unpaidText = Color(0xFFFBBF24);
   static const Color unpaidBg = Color(0xFF2D1B06);
 
@@ -204,9 +226,9 @@ class AppDarkColors {
   static const Color chart4 = Color(0xFFA78BFA);
   static const Color chart5 = Color(0xFF6AA8E6);
 
-  // ── 遮罩 ──
+  // ── 遮罩 / 高亮 ──
   static const Color overlay = Color(0xCC000000);
-  static const Color highlight = Color(0xFF0D2430);
+  static const Color highlight = Color(0xFF112835);     // = primary50
 }
 
 /// Pencil 设计字号体系

+ 191 - 0
lib/core/theme/tdesign_resource_delegate.dart

@@ -0,0 +1,191 @@
+import 'package:flutter/material.dart';
+import 'package:tdesign_flutter/tdesign_flutter.dart';
+import '../i18n/app_localizations.dart';
+
+/// TDesign 组件国际化资源代理
+///
+/// 将 [TDResourceDelegate] 的 63 个文本 getter 映射到 [AppLocalizations],
+/// 实现 TDesign 组件文本随应用语言切换。
+///
+/// 在 [MaterialApp] 初始化后注册:
+/// ```dart
+/// TDTheme.setResourceBuilder(
+///   (context) => TDResourceI18nDelegate(context),
+///   needAlwaysBuild: true,
+/// );
+/// ```
+class TDResourceI18nDelegate extends TDResourceDelegate {
+  final BuildContext context;
+
+  TDResourceI18nDelegate(this.context);
+
+  AppLocalizations get l10n => AppLocalizations.of(context);
+
+  // ── TDSwitch ──
+  @override
+  String get open => l10n.get('tdOpen');
+
+  @override
+  String get close => l10n.get('tdClose');
+
+  // ── TDBadge ──
+  @override
+  String get badgeZero => '0';
+
+  // ── TDAlertDialog / TDPicker ──
+  @override
+  String get cancel => l10n.get('tdCancel');
+
+  @override
+  String get confirm => l10n.get('tdConfirm');
+
+  @override
+  String get other => l10n.get('tdOther');
+
+  @override
+  String get reset => l10n.get('tdReset');
+
+  // ── TDLoading ──
+  @override
+  String get loading => l10n.get('tdLoading');
+
+  @override
+  String get loadingWithPoint => l10n.get('tdLoadingWithPoint');
+
+  // ── TDConfirmDialog ──
+  @override
+  String get knew => l10n.get('tdKnew');
+
+  // ── TDRefreshHeader ──
+  @override
+  String get refreshing => l10n.get('tdRefreshing');
+
+  @override
+  String get releaseRefresh => l10n.get('tdReleaseRefresh');
+
+  @override
+  String get pullToRefresh => l10n.get('tdPullToRefresh');
+
+  @override
+  String get completeRefresh => l10n.get('tdCompleteRefresh');
+
+  // ── TDTimeCounter ──
+  @override
+  String get days => l10n.get('tdDays');
+
+  @override
+  String get hours => l10n.get('tdHours');
+
+  @override
+  String get minutes => l10n.get('tdMinutes');
+
+  @override
+  String get seconds => l10n.get('tdSeconds');
+
+  @override
+  String get milliseconds => l10n.get('tdMilliseconds');
+
+  // ── TDDatePicker ──
+  @override
+  String get yearLabel => l10n.get('tdYearLabel');
+
+  @override
+  String get monthLabel => l10n.get('tdMonthLabel');
+
+  @override
+  String get dateLabel => l10n.get('tdDateLabel');
+
+  @override
+  String get weeksLabel => l10n.get('tdWeeksLabel');
+
+  // ── TDCalendarHeader 星期 ──
+  @override
+  String get sunday => l10n.get('tdSunday');
+
+  @override
+  String get monday => l10n.get('tdMonday');
+
+  @override
+  String get tuesday => l10n.get('tdTuesday');
+
+  @override
+  String get wednesday => l10n.get('tdWednesday');
+
+  @override
+  String get thursday => l10n.get('tdThursday');
+
+  @override
+  String get friday => l10n.get('tdFriday');
+
+  @override
+  String get saturday => l10n.get('tdSaturday');
+
+  // ── TDCalendar 月份 ──
+  @override
+  String get year => l10n.get('tdYear');
+
+  @override
+  String get january => l10n.get('tdJanuary');
+
+  @override
+  String get february => l10n.get('tdFebruary');
+
+  @override
+  String get march => l10n.get('tdMarch');
+
+  @override
+  String get april => l10n.get('tdApril');
+
+  @override
+  String get may => l10n.get('tdMay');
+
+  @override
+  String get june => l10n.get('tdJune');
+
+  @override
+  String get july => l10n.get('tdJuly');
+
+  @override
+  String get august => l10n.get('tdAugust');
+
+  @override
+  String get september => l10n.get('tdSeptember');
+
+  @override
+  String get october => l10n.get('tdOctober');
+
+  @override
+  String get november => l10n.get('tdNovember');
+
+  @override
+  String get december => l10n.get('tdDecember');
+
+  // ── TDCalendar 时间选择 ──
+  @override
+  String get time => l10n.get('tdTime');
+
+  @override
+  String get start => l10n.get('tdStart');
+
+  @override
+  String get end => l10n.get('tdEnd');
+
+  // ── TDRate ──
+  @override
+  String get notRated => l10n.get('tdNotRated');
+
+  // ── TDCascader ──
+  @override
+  String get cascadeLabel => l10n.get('tdCascadeLabel');
+
+  // ── TDBackTop ──
+  @override
+  String get back => l10n.get('tdBack');
+
+  @override
+  String get top => l10n.get('tdTop');
+
+  // ── TDTable / TDEmpty ──
+  @override
+  String get emptyData => l10n.get('tdEmptyData');
+}

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

@@ -2,7 +2,6 @@ import 'dart:async';
 import 'package:flutter/material.dart';
 import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
-import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../shell/nav_bar_config.dart';
@@ -114,7 +113,7 @@ class _AdminPermissionsPageState extends ConsumerState<AdminPermissionsPage> {
 
   @override
   Widget build(BuildContext context) {
-    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    //final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
     ref
         .read(navBarConfigProvider.notifier)

+ 32 - 28
lib/features/announcement/announcement_create_page.dart

@@ -71,10 +71,11 @@ class _AnnouncementCreatePageState
   }
 
   void _pickExpiryDate() {
+    final l10n = AppLocalizations.of(context);
     final initial = _expiryDate ?? DateTime.now().add(const Duration(days: 30));
     TDPicker.showDatePicker(
       context,
-      title: '选择过期日期',
+      title: l10n.get('selectExpiryDate'),
       useYear: true,
       useMonth: true,
       useDay: true,
@@ -105,6 +106,7 @@ class _AnnouncementCreatePageState
   }
 
   Widget _buildScopeDrawerContent(BuildContext ctx) {
+    final l10n = AppLocalizations.of(ctx);
     return StatefulBuilder(
       builder: (context, setInnerState) {
         final colors = Theme.of(context).extension<AppColorsExtension>()!;
@@ -118,7 +120,7 @@ class _AnnouncementCreatePageState
                 mainAxisAlignment: MainAxisAlignment.spaceBetween,
                 children: [
                   Text(
-                    '接收范围',
+                    l10n.get('recipientScope'),
                     style: TextStyle(
                       fontSize: 16,
                       fontWeight: FontWeight.w600,
@@ -144,24 +146,24 @@ class _AnnouncementCreatePageState
                   children: [
                     _buildScopeOption(
                       context,
-                      '全员',
-                      '所有员工均可查看',
+                      l10n.get('allStaff'),
+                      l10n.get('scopeAllStaff'),
                       0,
                       setInnerState,
                     ),
                     const SizedBox(height: 8),
                     _buildScopeOption(
                       context,
-                      '按部门',
-                      '按部门树多选',
+                      l10n.get('byDept'),
+                      l10n.get('byDeptHint'),
                       1,
                       setInnerState,
                     ),
                     const SizedBox(height: 8),
                     _buildScopeOption(
                       context,
-                      '按指定用户',
-                      '按员工搜索多选',
+                      l10n.get('byUser'),
+                      l10n.get('byUserHint'),
                       2,
                       setInnerState,
                     ),
@@ -172,7 +174,7 @@ class _AnnouncementCreatePageState
               if (_scopeMode == 1) ...[
                 const SizedBox(height: 12),
                 Text(
-                  '选择部门',
+                  l10n.get('selectDept'),
                   style: TextStyle(fontSize: 13, color: colors.textSecondary),
                 ),
                 const SizedBox(height: 4),
@@ -199,7 +201,7 @@ class _AnnouncementCreatePageState
               if (_scopeMode == 2) ...[
                 const SizedBox(height: 12),
                 Text(
-                  '搜索员工',
+                  l10n.get('searchEmployee'),
                   style: TextStyle(fontSize: 13, color: colors.textSecondary),
                 ),
                 const SizedBox(height: 4),
@@ -212,11 +214,11 @@ class _AnnouncementCreatePageState
                     color: colors.bgPage,
                     borderRadius: BorderRadius.circular(4),
                   ),
-                  child: TDInput(hintText: '输入姓名或工号搜索'),
+                  child: TDInput(hintText: l10n.get('searchEmployeeHint')),
                 ),
                 const SizedBox(height: 8),
                 Text(
-                  '已选 ${_selectedUsers.length} 人',
+                  l10n.getString('selectedCount', args: {'count': '${_selectedUsers.length}'}),
                   style: TextStyle(fontSize: 12, color: colors.textSecondary),
                 ),
               ],
@@ -234,11 +236,11 @@ class _AnnouncementCreatePageState
                   mainAxisAlignment: MainAxisAlignment.spaceBetween,
                   children: [
                     Text(
-                      '覆盖人数',
+                      l10n.get('coverageCount'),
                       style: TextStyle(fontSize: 14, color: colors.textPrimary),
                     ),
                     Text(
-                      '${_scopeMode == 0 ? _totalCoverage : (_scopeMode == 1 ? _selectedDepts.length * 15 : _selectedUsers.length)} ',
+                      '${_scopeMode == 0 ? _totalCoverage : (_scopeMode == 1 ? _selectedDepts.length * 15 : _selectedUsers.length)} ${l10n.get('personUnit')}',
                       style: TextStyle(
                         fontSize: 16,
                         fontWeight: FontWeight.w700,
@@ -252,7 +254,7 @@ class _AnnouncementCreatePageState
               SizedBox(
                 width: double.infinity,
                 child: TDButton(
-                  text: '确认',
+                  text: l10n.get('confirm'),
                   size: TDButtonSize.large,
                   theme: TDButtonTheme.primary,
                   isBlock: true,
@@ -353,7 +355,7 @@ class _AnnouncementCreatePageState
                       Container(width: 60, height: 4, color: colors.danger),
                       const SizedBox(height: 16),
                       Text(
-                        _titleCtrl.text.isEmpty ? '(未填写标题)' : _titleCtrl.text,
+                        _titleCtrl.text.isEmpty ? l10n.get('titleNotFilled') : _titleCtrl.text,
                         style: TextStyle(
                           fontSize: 20,
                           fontWeight: FontWeight.w700,
@@ -362,7 +364,7 @@ class _AnnouncementCreatePageState
                       ),
                       const SizedBox(height: 12),
                       Text(
-                        '$_type · 发布后将显示',
+                        l10n.getString('typeAndPublishDate', args: {'type': _type}),
                         style: TextStyle(
                           fontSize: 13,
                           color: colors.textSecondary,
@@ -377,7 +379,7 @@ class _AnnouncementCreatePageState
                       const SizedBox(height: 16),
                       Text(
                         _contentCtrl.text.isEmpty
-                            ? '(未填写正文)'
+                            ? l10n.get('contentNotFilled')
                             : _contentCtrl.text,
                         style: TextStyle(
                           fontSize: 14,
@@ -428,11 +430,13 @@ class _AnnouncementCreatePageState
   }
 
   void _saveDraft() {
-    TDToast.showText('已保存为草稿', context: context);
+    final l10n = AppLocalizations.of(context);
+    TDToast.showText(l10n.get('draftSavedToast'), context: context);
   }
 
   void _pickAttachment() {
-    TDToast.showText('模拟:选择附件(PDF/图片/Word/Excel,≤20MB)', context: context);
+    final l10n = AppLocalizations.of(context);
+    TDToast.showText(l10n.get('attachmentPicker'), context: context);
   }
 
   @override
@@ -562,7 +566,7 @@ class _AnnouncementCreatePageState
                       children: [
                         if (_attachments.isEmpty)
                           Text(
-                            '最多5个附件,支持PDF/图片/Word/Excel,单文件≤20MB',
+                            l10n.get('attachmentLimit'),
                             style: TextStyle(
                               fontSize: 12,
                               color: colors.textPlaceholder,
@@ -665,14 +669,14 @@ class _AnnouncementCreatePageState
                                         color: colors.primary,
                                       ),
                                     ),
-                                    Text(
-                                      ' · $_totalCoverage 人',
-                                      style: TextStyle(
-                                        fontSize: 12,
-                                        color: colors.textPlaceholder,
+                                      Text(
+                                        l10n.get('coverageCount'),
+                                        style: TextStyle(
+                                          fontSize: 12,
+                                          color: colors.textPlaceholder,
+                                        ),
                                       ),
-                                    ),
-                                    const SizedBox(width: 4),
+                                      const SizedBox(width: 4),
                                     Icon(
                                       Icons.chevron_right,
                                       size: 14,

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

@@ -9,6 +9,7 @@ import '../../core/i18n/app_localizations.dart';
 import 'announcement_list_controller.dart';
 import 'announcement_model.dart';
 import '../../core/theme/app_colors_extension.dart';
+import '../../core/auth/role_provider.dart';
 
 class AnnouncementDetailPage extends ConsumerStatefulWidget {
   final String id;
@@ -22,7 +23,6 @@ class AnnouncementDetailPage extends ConsumerStatefulWidget {
 class _AnnouncementDetailPageState
     extends ConsumerState<AnnouncementDetailPage> {
   late AnnouncementModel _item;
-  final bool _isAdmin = true;
 
   @override
   void initState() {
@@ -35,8 +35,9 @@ class _AnnouncementDetailPageState
     // 停留 ≥2s 自动标记已读
     Timer(const Duration(seconds: 2), () {
       if (mounted) {
+        final l10n = AppLocalizations.of(context);
         TDToast.showText(
-          '已标记为已读',
+          l10n.get('markedAsRead'),
           context: context,
           duration: const Duration(seconds: 1),
         );
@@ -46,6 +47,7 @@ class _AnnouncementDetailPageState
 
   @override
   Widget build(BuildContext context) {
+    final isAdmin = ref.watch(isAdminProvider);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
 
@@ -75,7 +77,7 @@ class _AnnouncementDetailPageState
               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
               color: colors.danger,
               child: Text(
-                '该公告已于 ${du.DateUtils.formatDateTime(_item.expiryDate!)} 过期',
+                l10n.getString('announcementExpired', args: {'date': du.DateUtils.formatDateTime(_item.expiryDate!)}),
                 style: const TextStyle(fontSize: 13, color: Colors.white),
                 textAlign: TextAlign.center,
               ),
@@ -171,7 +173,7 @@ class _AnnouncementDetailPageState
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: [
                   Text(
-                    '附件下载',
+                    l10n.get('downloadAttachment'),
                     style: TextStyle(
                       fontSize: 14,
                       fontWeight: FontWeight.w600,
@@ -232,7 +234,7 @@ class _AnnouncementDetailPageState
             ),
 
           // 管理员增量:已读/未读统计 + DING
-          if (_isAdmin)
+          if (isAdmin)
             Padding(
               padding: const EdgeInsets.all(16),
               child: Container(
@@ -246,7 +248,7 @@ class _AnnouncementDetailPageState
                   crossAxisAlignment: CrossAxisAlignment.start,
                   children: [
                     Text(
-                      '全员触达率',
+                      l10n.get('auditTracking'),
                       style: TextStyle(
                         fontSize: 14,
                         fontWeight: FontWeight.w600,
@@ -258,7 +260,7 @@ class _AnnouncementDetailPageState
                       children: [
                         GestureDetector(
                           onTap: () {
-                            TDToast.showText('模拟:展开已读员工列表', context: context);
+                            TDToast.showText(l10n.get('mockExpandReadList'), context: context);
                           },
                           child: Container(
                             padding: const EdgeInsets.symmetric(
@@ -270,7 +272,7 @@ class _AnnouncementDetailPageState
                               borderRadius: BorderRadius.circular(16),
                             ),
                             child: Text(
-                              '已读 ${_item.readCount} 人',
+                              l10n.getString('readCount', args: {'count': '${_item.readCount}'}),
                               style: TextStyle(
                                 fontSize: 12,
                                 color: colors.success,
@@ -281,7 +283,7 @@ class _AnnouncementDetailPageState
                         const SizedBox(width: 12),
                         GestureDetector(
                           onTap: () {
-                            TDToast.showText('模拟:展开未读员工列表', context: context);
+                            TDToast.showText(l10n.get('mockExpandUnreadList'), context: context);
                           },
                           child: Container(
                             padding: const EdgeInsets.symmetric(
@@ -293,7 +295,7 @@ class _AnnouncementDetailPageState
                               borderRadius: BorderRadius.circular(16),
                             ),
                             child: Text(
-                              '未读 ${_item.unreadCount} 人',
+                              l10n.getString('unreadCount', args: {'count': '${_item.unreadCount}'}),
                               style: TextStyle(
                                 fontSize: 12,
                                 color: colors.statusGray,
@@ -307,7 +309,7 @@ class _AnnouncementDetailPageState
                     GestureDetector(
                       onTap: () {
                         TDToast.showText(
-                          '已向 ${_item.unreadCount} 名未读员工发送催办通知',
+                          l10n.getString('dingPromptSent', args: {'count': '${_item.unreadCount}'}),
                           context: context,
                         );
                       },
@@ -321,8 +323,8 @@ class _AnnouncementDetailPageState
                           color: colors.danger,
                           borderRadius: BorderRadius.circular(20),
                         ),
-                        child: const Text(
-                          'DING 催办',
+                        child: Text(
+                          l10n.get('dingReminder'),
                           textAlign: TextAlign.center,
                           style: TextStyle(
                             fontSize: 14,
@@ -344,18 +346,16 @@ class _AnnouncementDetailPageState
 
   Widget _buildTypeTag(String type) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final l10n = AppLocalizations.of(context);
     Color bgColor;
     Color textColor;
-    switch (type) {
-      case '人事与制度':
+    if (type == l10n.get('hrPolicy')) {
         bgColor = colors.successBg;
         textColor = colors.success;
-        break;
-      case '放假与活动':
+    } else if (type == l10n.get('holidayActivity')) {
         bgColor = colors.warningBg;
         textColor = colors.warning;
-        break;
-      default:
+    } else {
         bgColor = colors.primaryLight;
         textColor = colors.primary;
     }

+ 2 - 2
lib/features/announcement/announcement_list_controller.dart

@@ -1,4 +1,5 @@
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../../core/auth/role_provider.dart';
 import 'announcement_model.dart';
 
 final mockAnnouncements = <AnnouncementModel>[
@@ -102,11 +103,10 @@ final mockAnnouncements = <AnnouncementModel>[
 ];
 
 final announcementTabProvider = StateProvider<int>((ref) => 0);
-final isAdminProvider = StateProvider<bool>((ref) => true);
 
 final announcementRefreshProvider = StateProvider<int>((ref) => 0);
 
-final filteredAnnouncementsProvider = FutureProvider.autoDispose
+final filteredAnnouncementsProvider = FutureProvider
     .family<List<AnnouncementModel>, int>((ref, tabIndex) async {
       ref.watch(announcementRefreshProvider);
       final isAdmin = ref.watch(isAdminProvider);

+ 31 - 21
lib/features/announcement/announcement_list_page.dart

@@ -10,6 +10,7 @@ import '../../core/utils/responsive.dart';
 import '../../shared/widgets/empty_state.dart';
 import '../../shared/widgets/skeleton_list_card.dart';
 import '../../core/i18n/app_localizations.dart';
+import '../../core/auth/role_provider.dart';
 import 'announcement_list_controller.dart';
 import 'announcement_model.dart';
 
@@ -24,16 +25,16 @@ class _AnnouncementListPageState extends ConsumerState<AnnouncementListPage>
     with TickerProviderStateMixin {
   late TabController _tabCtrl;
 
-  static List<String> _getTabLabels(bool isAdmin) => isAdmin
-      ? ['全部', '通知公告', '人事与制度', '放假与活动', '我的草稿']
-      : ['全部', '通知公告', '人事与制度', '放假与活动'];
+  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')];
 
   @override
   void initState() {
     super.initState();
     final isAdmin = ref.read(isAdminProvider);
     _tabCtrl = TabController(
-      length: _getTabLabels(isAdmin).length,
+      length: isAdmin ? 5 : 4,
       vsync: this,
     );
     _tabCtrl.addListener(_onTabChanged);
@@ -54,10 +55,10 @@ class _AnnouncementListPageState extends ConsumerState<AnnouncementListPage>
   @override
   Widget build(BuildContext context) {
     final isAdmin = ref.watch(isAdminProvider);
-    final tabs = _getTabLabels(isAdmin);
+    final l10n = AppLocalizations.of(context);
+    final tabs = _getTabLabels(isAdmin, l10n);
     final tabIndex = ref.watch(announcementTabProvider);
     final r = ResponsiveHelper.of(context);
-    final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
 
     // Handle TabController recreation when tab count changes (isAdmin toggle)
@@ -140,7 +141,6 @@ class _AnnouncementListPageState extends ConsumerState<AnnouncementListPage>
                 dividerHeight: 0,
                 labelPadding: const EdgeInsets.symmetric(horizontal: 12),
                 onTap: (index) {
-                  ref.invalidate(filteredAnnouncementsProvider);
                   ref.read(announcementTabProvider.notifier).state = index;
                 },
               ),
@@ -192,11 +192,15 @@ class _AnnouncementTabContent extends ConsumerWidget {
     BuildContext context,
     WidgetRef ref,
   ) {
+    final l10n = AppLocalizations.of(context);
     if (itemsAsync.isReloading) {
       final oldItems = itemsAsync.valueOrNull ?? [];
       if (oldItems.isEmpty) {
-        return SkeletonLoadingList(
-          cardBuilder: () => const SkeletonAnnouncementCard(),
+        return ListView(
+          children: [
+            const SizedBox(height: 120),
+            EmptyState(message: l10n.get('noAnnouncements')),
+          ],
         );
       }
       return ListView.builder(
@@ -208,9 +212,9 @@ class _AnnouncementTabContent extends ConsumerWidget {
 
     if (itemsAsync.hasError) {
       return ListView(
-        children: const [
-          SizedBox(height: 120),
-          EmptyState(message: '加载失败'),
+        children: [
+          const SizedBox(height: 120),
+          EmptyState(message: l10n.get('loadFailed')),
         ],
       );
     }
@@ -218,9 +222,9 @@ class _AnnouncementTabContent extends ConsumerWidget {
     final items = itemsAsync.requireValue;
     if (items.isEmpty) {
       return ListView(
-        children: const [
-          SizedBox(height: 120),
-          EmptyState(message: '暂无行政公告'),
+        children: [
+          const SizedBox(height: 120),
+          EmptyState(message: l10n.get('noAnnouncements')),
         ],
       );
     }
@@ -233,6 +237,7 @@ class _AnnouncementTabContent extends ConsumerWidget {
   }
 
   Widget _buildAnnouncementCard(BuildContext context, AnnouncementModel item) {
+    final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final expired = item.isExpired;
     return Padding(
@@ -264,8 +269,8 @@ class _AnnouncementTabContent extends ConsumerWidget {
                         color: colors.danger,
                         borderRadius: BorderRadius.circular(2),
                       ),
-                      child: const Text('置顶',
-                          style: TextStyle(
+                      child: Text(l10n.get('pinTopTag'),
+                          style: const TextStyle(
                               fontSize: 10, color: Colors.white)),
                     ),
                   // 标题
@@ -295,7 +300,7 @@ class _AnnouncementTabContent extends ConsumerWidget {
                         color: colors.bgPage,
                         borderRadius: BorderRadius.circular(3),
                       ),
-                      child: Text('已过期',
+                      child: Text(l10n.get('expired'),
                           style: TextStyle(
                               fontSize: 10, color: colors.textPlaceholder)),
                     ),
@@ -319,7 +324,7 @@ class _AnnouncementTabContent extends ConsumerWidget {
                 children: [
                   Row(
                     children: [
-                      _buildTypeTag(context, item.typeLabel),
+                      _buildTypeTag(context, item.typeLabel, l10n),
                       const SizedBox(width: 8),
                       Text(
                         item.publisherName,
@@ -342,7 +347,7 @@ class _AnnouncementTabContent extends ConsumerWidget {
     );
   }
 
-  Widget _buildTypeTag(BuildContext context, String type) {
+  Widget _buildTypeTag(BuildContext context, String type, AppLocalizations l10n) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     Color bgColor;
     Color textColor;
@@ -359,6 +364,11 @@ class _AnnouncementTabContent extends ConsumerWidget {
         bgColor = colors.primaryLight;
         textColor = colors.primary;
     }
+    final displayText = switch (type) {
+      '人事与制度' => l10n.get('hrPolicy'),
+      '放假与活动' => l10n.get('holidayActivity'),
+      _ => l10n.get('noticeAnnouncement'),
+    };
     return Container(
       padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
       decoration: BoxDecoration(
@@ -366,7 +376,7 @@ class _AnnouncementTabContent extends ConsumerWidget {
         borderRadius: BorderRadius.circular(3),
       ),
       child:
-          Text(type, style: TextStyle(fontSize: 11, color: textColor)),
+          Text(displayText, style: TextStyle(fontSize: 11, color: textColor)),
     );
   }
 }

+ 79 - 6
lib/features/expense/expense_detail_page.dart

@@ -14,6 +14,7 @@ import '../../core/i18n/app_localizations.dart';
 import 'expense_list_controller.dart';
 import '../../core/theme/app_colors.dart';
 import '../../core/theme/app_colors_extension.dart';
+import '../../core/auth/role_provider.dart';
 
 class ExpenseDetailPage extends ConsumerWidget {
   final String id;
@@ -28,6 +29,9 @@ class ExpenseDetailPage extends ConsumerWidget {
     );
     final l10n = AppLocalizations.of(context);
 
+    final isFinance = ref.watch(isFinanceProvider);
+    final isAdmin = ref.watch(isAdminProvider);
+
     ref
         .read(navBarConfigProvider.notifier)
         .update(
@@ -56,18 +60,18 @@ class ExpenseDetailPage extends ConsumerWidget {
                 const SizedBox(height: 16),
                 _buildInvoiceSection(expense, l10n, colors),
                 const SizedBox(height: 16),
-                _buildComplianceSection(expense, l10n, colors),
+                if (isFinance) _buildComplianceSection(expense, l10n, colors),
                 const SizedBox(height: 16),
                 if (expense.approvalRecords.isNotEmpty ||
                     expense.approvalChain.isNotEmpty)
                   _buildApprovalSection(expense, l10n),
                 const SizedBox(height: 16),
-                _buildArchiveSection(expense, l10n),
+                if (isFinance || isAdmin) _buildArchiveSection(expense, l10n),
               ],
             ),
           ),
         ),
-        _buildBottomBar(context, expense),
+        _buildBottomBar(context, expense, isFinance: isFinance, isAdmin: isAdmin),
       ],
     );
   }
@@ -464,11 +468,63 @@ class ExpenseDetailPage extends ConsumerWidget {
     );
   }
 
-  Widget _buildBottomBar(BuildContext context, ExpenseModel expense) {
+  Widget _buildBottomBar(
+    BuildContext context,
+    ExpenseModel expense, {
+    required bool isFinance,
+    required bool isAdmin,
+  }) {
     final l10n = AppLocalizations.of(context);
     final canWithdraw =
         expense.status == 'pending' || expense.status == 'draft';
 
+    // 财务角色:已审批 + 未付款 → 显示打款归档按钮
+    if (isFinance &&
+        expense.status == 'approved' &&
+        expense.paymentStatus == 'unpaid') {
+      return ActionBar(
+        showLeft: true,
+        leftLabel: l10n.get('confirmPaymentAndArchive'),
+        centerLabel: l10n.get('nextPendingPayment'),
+        rightLabel: l10n.get('confirmPaymentAndArchive'),
+        onLeftTap: () {
+          showDialog(
+            context: context,
+            builder: (ctx) => TDAlertDialog(
+              title: l10n.get('confirmPaymentAndArchive'),
+              content: l10n.get('confirmPaymentAndArchiveTip'),
+              leftBtn: TDDialogButtonOptions(
+                title: l10n.get('cancel'),
+                action: () => Navigator.pop(ctx),
+              ),
+              rightBtn: TDDialogButtonOptions(
+                title: l10n.get('confirm'),
+                action: () {
+                  Navigator.pop(ctx);
+                  TDToast.showText(
+                    l10n.get('paymentArchiveSuccess'),
+                    context: context,
+                  );
+                  context.pop();
+                },
+              ),
+            ),
+          );
+        },
+        onCenterTap: () {
+          TDToast.showText(
+            l10n.get('allPaymentsProcessed'),
+            context: context,
+          );
+        },
+      );
+    }
+
+    // 非 employee/manager 角色不显示撤回按钮
+    if (isFinance || isAdmin) {
+      return const SizedBox.shrink();
+    }
+
     if (!canWithdraw) {
       return const SizedBox.shrink();
     }
@@ -478,8 +534,25 @@ class ExpenseDetailPage extends ConsumerWidget {
       centerLabel: l10n.get('withdrawApplication'),
       rightLabel: l10n.get('submitApproval'),
       onCenterTap: () {
-        TDToast.showText(l10n.get('withdrawn'), context: context);
-        context.pop();
+        showDialog(
+          context: context,
+          builder: (ctx) => TDAlertDialog(
+            title: l10n.get('withdrawConfirm'),
+            content: l10n.get('withdrawConfirmTip'),
+            leftBtn: TDDialogButtonOptions(
+              title: l10n.get('cancel'),
+              action: () => Navigator.pop(ctx),
+            ),
+            rightBtn: TDDialogButtonOptions(
+              title: l10n.get('confirm'),
+              action: () {
+                Navigator.pop(ctx);
+                TDToast.showText(l10n.get('withdrawn'), context: context);
+                context.pop();
+              },
+            ),
+          ),
+        );
       },
       onRightTap: null,
     );

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

@@ -419,7 +419,7 @@ final expenseDateEndProvider = StateProvider<DateTime?>((ref) => null);
 
 final expenseRefreshProvider = StateProvider<int>((ref) => 0);
 
-final expenseListProvider = FutureProvider.autoDispose
+final expenseListProvider = FutureProvider
     .family<List<ExpenseModel>, String>((ref, status) async {
       ref.watch(expensePageProvider);
       ref.watch(expenseDateStartProvider);

+ 207 - 53
lib/features/expense/expense_list_page.dart

@@ -7,6 +7,7 @@ import '../shell/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
+import '../../core/auth/role_provider.dart';
 import '../../shared/widgets/list_card.dart';
 import '../../shared/widgets/status_tag.dart';
 import '../../shared/widgets/empty_state.dart';
@@ -16,6 +17,8 @@ import '../../core/i18n/app_localizations.dart';
 import 'expense_list_controller.dart';
 import 'expense_model.dart';
 
+final _scopeProvider = StateProvider<String>((ref) => 'my');
+
 class ExpenseListPage extends ConsumerStatefulWidget {
   const ExpenseListPage({super.key});
   @override
@@ -24,22 +27,32 @@ class ExpenseListPage extends ConsumerStatefulWidget {
 
 class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
     with TickerProviderStateMixin {
-  static const _tabLabels = ['全部', '草稿', '审批中', '已通过', '已拒绝', '已撤回'];
+  List<String> _getTabLabels(AppLocalizations l10n) => [
+    l10n.get('all'),
+    l10n.get('draft'),
+    l10n.get('pending'),
+    l10n.get('approved'),
+    l10n.get('statusWaitPay'),
+    l10n.get('paid'),
+    l10n.get('rejected'),
+    l10n.get('revoked'),
+  ];
   static const _tabKeys = [
     '',
     'draft',
     'pending',
     'approved',
+    'unpaid',
+    'paid',
     'rejected',
     'revoked',
   ];
-
   late final TabController _tabCtrl;
 
   @override
   void initState() {
     super.initState();
-    _tabCtrl = TabController(length: _tabLabels.length, vsync: this);
+    _tabCtrl = TabController(length: _tabKeys.length, vsync: this);
     _tabCtrl.addListener(() {
       if (!_tabCtrl.indexIsChanging) {
         ref.read(expenseStatusFilterProvider.notifier).state =
@@ -61,6 +74,7 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
     final dateEnd = ref.watch(expenseDateEndProvider);
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final isManager = ref.watch(isManagerProvider);
 
     // Sync TabController with external filter changes
     final targetIdx = _tabKeys.indexOf(status);
@@ -73,34 +87,38 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
     }
 
     final filterGroups = [
-      FilterGroup(title: '日期范围', 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: '结束日期',
-          type: FilterSectionType.dateRange,
-          startDate: dateStart,
-          endDate: dateEnd,
-          onStartChanged: (v) =>
-              ref.read(expenseDateStartProvider.notifier).state = v,
-          onEndChanged: (v) =>
-              ref.read(expenseDateEndProvider.notifier).state = v,
-        ),
-      ]),
+      FilterGroup(
+        title: '日期范围',
+        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: '结束日期',
+            type: FilterSectionType.dateRange,
+            startDate: dateStart,
+            endDate: dateEnd,
+            onStartChanged: (v) =>
+                ref.read(expenseDateStartProvider.notifier).state = v,
+            onEndChanged: (v) =>
+                ref.read(expenseDateEndProvider.notifier).state = v,
+          ),
+        ],
+      ),
     ];
     final hasFilter = FilterBar.hasActiveFilter(filterGroups);
-    final onFilterReset = () {
+    void onFilterReset() {
       ref.read(expenseDateStartProvider.notifier).state = null;
       ref.read(expenseDateEndProvider.notifier).state = null;
-    };
+    }
 
     ref
         .read(navBarConfigProvider.notifier)
@@ -118,7 +136,10 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
               ),
               child: Stack(
                 children: [
-                  Icon(TDIcons.filter, color: hasFilter ? colors.primary : colors.textPrimary),
+                  Icon(
+                    TDIcons.filter,
+                    color: hasFilter ? colors.primary : colors.textPrimary,
+                  ),
                   if (hasFilter)
                     Positioned(
                       right: -2,
@@ -140,11 +161,13 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
         );
     return Column(
       children: [
+        if (isManager)
+          _buildScopeChip(colors),
         Container(
           color: colors.bgCard,
           padding: const EdgeInsets.symmetric(horizontal: 8),
           child: TDTabBar(
-            tabs: _tabLabels.map((l) => TDTab(text: l)).toList(),
+            tabs: _getTabLabels(l10n).map((l) => TDTab(text: l)).toList(),
             controller: _tabCtrl,
             isScrollable: true,
             labelColor: colors.primary,
@@ -156,7 +179,6 @@ class _ExpenseListPageState extends ConsumerState<ExpenseListPage>
             dividerHeight: 0,
             labelPadding: const EdgeInsets.symmetric(horizontal: 12),
             onTap: (index) {
-              ref.invalidate(expenseListProvider);
               ref.read(expenseStatusFilterProvider.notifier).state =
                   _tabKeys[index];
             },
@@ -178,6 +200,52 @@ 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,
+                ),
+              ),
+            ),
+          ),
+          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,
+                ),
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
   Widget _buildTabContent(int tabIdx) {
     final r = ResponsiveHelper.of(context);
     return Center(
@@ -197,6 +265,7 @@ class _ExpenseTabContent extends ConsumerWidget {
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     final itemsAsync = ref.watch(expenseListProvider(statusKey));
+    final scope = ref.watch(_scopeProvider);
 
     if (itemsAsync.isLoading && !itemsAsync.hasValue) {
       return const SkeletonLoadingList();
@@ -207,41 +276,63 @@ class _ExpenseTabContent extends ConsumerWidget {
       onRefresh: () async {
         ref.read(expenseRefreshProvider.notifier).state++;
       },
-      child: _buildContent(itemsAsync, context, ref),
+      child: _buildContent(itemsAsync, context, ref, scope),
     );
   }
 
   Widget _buildContent(
     AsyncValue<List<ExpenseModel>> itemsAsync,
-    
+
     BuildContext context,
     WidgetRef ref,
+    String scope,
   ) {
+    final l10n = AppLocalizations.of(context);
+    final isSub = scope == 'sub';
     if (itemsAsync.isReloading) {
       final oldItems = itemsAsync.valueOrNull ?? [];
-      if (oldItems.isEmpty) return const SkeletonLoadingList();
+      if (oldItems.isEmpty) {
+        return ListView(
+          children: [
+            const SizedBox(height: 120),
+            EmptyState(message: l10n.get('noExpenses')),
+          ],
+        );
+      }
       return ListView.builder(
         padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
         itemCount: oldItems.length,
-        itemBuilder: (_, i) => Padding(
-          padding: const EdgeInsets.only(bottom: 16),
-          child: ListCard(
+        itemBuilder: (_, i) {
+          final desc = isSub
+              ? '${oldItems[i].expenseType} — ${oldItems[i].applicantName}\n申请人: ${oldItems[i].applicantName} · ${oldItems[i].deptName}'
+              : '${oldItems[i].expenseType} — ${oldItems[i].applicantName}';
+          final card = ListCard(
             cardNo: oldItems[i].reportNo,
             amount: '¥${oldItems[i].totalAmount.toStringAsFixed(2)}',
-            description: '${oldItems[i].expenseType} — ${oldItems[i].applicantName}',
+            description: desc,
             date: du.DateUtils.formatDate(oldItems[i].createTime),
-            statusTag: StatusTag.fromStatus(oldItems[i].status),
+            statusTag: StatusTag.fromStatus(oldItems[i].status, l10n),
             onTap: () => context.push('/expense/detail/${oldItems[i].id}'),
-          ),
-        ),
+          );
+          if (isSub && oldItems[i].status == 'pending') {
+            return Padding(
+              padding: const EdgeInsets.only(bottom: 16),
+              child: _buildSwipeApprove(card, oldItems[i].id),
+            );
+          }
+          return Padding(
+            padding: const EdgeInsets.only(bottom: 16),
+            child: card,
+          );
+        },
       );
     }
 
     if (itemsAsync.hasError) {
       return ListView(
-        children: const [
-          SizedBox(height: 120),
-          EmptyState(message: '加载失败'),
+        children: [
+          const SizedBox(height: 120),
+          EmptyState(message: l10n.get('loadFailed')),
         ],
       );
     }
@@ -249,9 +340,9 @@ class _ExpenseTabContent extends ConsumerWidget {
     final items = itemsAsync.requireValue;
     if (items.isEmpty) {
       return ListView(
-        children: const [
-          SizedBox(height: 120),
-          EmptyState(message: '暂无报销单'),
+        children: [
+          const SizedBox(height: 120),
+          EmptyState(message: l10n.get('noExpenses')),
         ],
       );
     }
@@ -259,17 +350,80 @@ class _ExpenseTabContent extends ConsumerWidget {
     return ListView.builder(
       padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
       itemCount: items.length,
-      itemBuilder: (_, i) => Padding(
-        padding: const EdgeInsets.only(bottom: 16),
-        child: ListCard(
+      itemBuilder: (_, i) {
+        final desc = isSub
+            ? '${items[i].expenseType} — ${items[i].applicantName}\n申请人: ${items[i].applicantName} · ${items[i].deptName}'
+            : '${items[i].expenseType} — ${items[i].applicantName}';
+        final card = ListCard(
           cardNo: items[i].reportNo,
           amount: '¥${items[i].totalAmount.toStringAsFixed(2)}',
-          description: '${items[i].expenseType} — ${items[i].applicantName}',
+          description: desc,
           date: du.DateUtils.formatDate(items[i].createTime),
-          statusTag: StatusTag.fromStatus(items[i].status),
+          statusTag: StatusTag.fromStatus(items[i].status, l10n),
           onTap: () => context.push('/expense/detail/${items[i].id}'),
-        ),
-      ),
+        );
+        if (isSub && items[i].status == 'pending') {
+          return Padding(
+            padding: const EdgeInsets.only(bottom: 16),
+            child: _buildSwipeApprove(card, items[i].id),
+          );
+        }
+        return Padding(
+          padding: const EdgeInsets.only(bottom: 16),
+          child: card,
+        );
+      },
+    );
+  }
+
+  Widget _buildSwipeApprove(Widget card, String itemId) {
+    return Builder(
+      builder: (ctx) {
+        final screenWidth = MediaQuery.of(ctx).size.width;
+        return TDSwipeCell(
+          groupTag: 'expense_approve',
+          right: TDSwipeCellPanel(
+            extentRatio: 100 / screenWidth,
+            children: [
+              TDSwipeCellAction(
+                label: '',
+                backgroundColor: Colors.transparent,
+                builder: (_) => Container(
+                  margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
+                  decoration: BoxDecoration(
+                    color: Colors.green,
+                    borderRadius: BorderRadius.circular(8),
+                  ),
+                  alignment: Alignment.center,
+                  padding: const EdgeInsets.symmetric(horizontal: 12),
+                  child: const Text(
+                    '一键同意',
+                    style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600),
+                  ),
+                ),
+                onPressed: (_) async {
+                  final confirmed = await showDialog<bool>(
+                    context: ctx,
+                    builder: (dCtx) => TDAlertDialog(
+                      title: '确认审批',
+                      content: '确认同意该报销?',
+                      leftBtn: TDDialogButtonOptions(title: '取消', action: () => Navigator.of(dCtx).pop(false)),
+                      rightBtn: TDDialogButtonOptions(title: '确认', action: () => Navigator.of(dCtx).pop(true)),
+                    ),
+                  );
+                  if (confirmed == true) {
+                    // TODO: 接入实际审批 API
+                    if (ctx.mounted) {
+                      TDToast.showSuccess('已审批通过', context: ctx);
+                    }
+                  }
+                },
+              ),
+            ],
+          ),
+          cell: card,
+        );
+      },
     );
   }
 }

+ 72 - 67
lib/features/expense_application/expense_application_apply_page.dart

@@ -79,10 +79,10 @@ class _ExpenseApplicationApplyPageState
             onBack: () {
               if (_hasUnsaved()) {
                 _showConfirmDialog(
-                  '确认退出',
-                  '当前内容尚未保存,是否退出?',
-                  '继续编辑',
-                  '放弃并退出',
+                  l10n.get('confirmExit'),
+                  l10n.get('unsavedContentWarning'),
+                  l10n.get('continueEditing'),
+                  l10n.get('discardAndExit'),
                   () => context.pop(),
                 );
               } else {
@@ -98,10 +98,10 @@ class _ExpenseApplicationApplyPageState
         if (!didPop) {
           if (_hasUnsaved()) {
             _showConfirmDialog(
-              '确认退出',
-              '当前内容尚未保存,是否退出?',
-              '继续编辑',
-              '放弃并退出',
+              l10n.get('confirmExit'),
+              l10n.get('unsavedContentWarning'),
+              l10n.get('continueEditing'),
+              l10n.get('discardAndExit'),
               () => context.pop(),
             );
           } else {
@@ -288,7 +288,7 @@ class _ExpenseApplicationApplyPageState
 
   Widget _buildTravelFields(AppLocalizations l10n) {
     return FormSection(
-      title: '差旅费专用',
+      title: l10n.get('travelExpense'),
       children: [
         FormFieldRow(
           label: l10n.get('estimatedStartDate'),
@@ -315,7 +315,7 @@ class _ExpenseApplicationApplyPageState
         FormFieldRow(
           label: l10n.get('transportType'),
           value: '高铁/动车',
-          onTap: () => _showListPicker('选择交通工具', [
+          onTap: () => _showListPicker(l10n.get('selectTransport'), [
             '飞机',
             '高铁/动车',
             '火车(普速)',
@@ -328,36 +328,36 @@ class _ExpenseApplicationApplyPageState
 
   Widget _buildEntertainmentFields(AppLocalizations l10n) {
     return FormSection(
-      title: '招待费专用',
+      title: l10n.get('entertainmentExpense'),
       children: [
         FormFieldRow(
-          label: '招待对象单位',
+          label: l10n.get('entertainmentTargetUnit'),
           value: _entertainmentTarget,
-          hint: '请输入',
+          hint: l10n.get('pleaseEnter'),
           onTap: () => _showTextInput(
-            '招待对象单位',
+            l10n.get('entertainmentTargetUnit'),
             (v) => setState(() => _entertainmentTarget = v),
           ),
         ),
         FormFieldRow(
-          label: '招待层级',
-          value: '普通',
-          onTap: () => _showListPicker('选择招待层级', ['普通', '重要', 'VIP'], (_) {}),
+          label: l10n.get('entertainmentLevel'),
+          value: l10n.get('normal'),
+          onTap: () => _showListPicker(l10n.get('selectEntertainmentLevel'), [l10n.get('normal'), l10n.get('important'), 'VIP'], (_) {}),
         ),
         FormFieldRow(
-          label: '外部人数',
+          label: l10n.get('externalCount'),
           value: '3',
-          onTap: () => _showNumberInput('外部人数', (_) {}),
+          onTap: () => _showNumberInput(l10n.get('externalCount'), (_) {}),
         ),
         FormFieldRow(
-          label: '内部陪同人数',
+          label: l10n.get('internalCount'),
           value: '2',
-          onTap: () => _showNumberInput('内部陪同人数', (_) {}),
+          onTap: () => _showNumberInput(l10n.get('internalCount'), (_) {}),
         ),
         FormFieldRow(
           label: l10n.get('venue'),
           value: _venue,
-          hint: '请输入地点',
+          hint: l10n.get('pleaseEnterLocation'),
           onTap: () => _showTextInput(
             l10n.get('venue'),
             (v) => setState(() => _venue = v),
@@ -369,7 +369,7 @@ class _ExpenseApplicationApplyPageState
 
   Widget _buildMeetingFields(AppLocalizations l10n) {
     return FormSection(
-      title: '会议费专用',
+      title: l10n.get('meetingExpense'),
       children: [
         FormFieldRow(
           label: l10n.get('estimatedStartDate'),
@@ -384,8 +384,8 @@ class _ExpenseApplicationApplyPageState
         FormFieldRow(
           label: l10n.get('venue'),
           value: _venue,
-          hint: '请输入会议地点',
-          onTap: () => _showTextInput('会议地点', (_) {}),
+          hint: l10n.get('pleaseEnterMeetingLocation'),
+          onTap: () => _showTextInput(l10n.get('meetingLocation'), (_) {}),
         ),
       ],
     );
@@ -409,7 +409,7 @@ class _ExpenseApplicationApplyPageState
           value: _selectedProjectName,
           hint: l10n.get('selectProject'),
           onTap: () {
-            _showListPicker('选择关联项目', _mockProjects.map((p) => p.$1).toList(), (
+            _showListPicker(l10n.get('selectProject'), _mockProjects.map((p) => p.$1).toList(), (
               v,
             ) {
               final p = _mockProjects.firstWhere((x) => x.$1 == v);
@@ -429,7 +429,7 @@ class _ExpenseApplicationApplyPageState
           onTap: _selectedProjectId != null
               ? () {
                   _showListPicker(
-                    '选择预算科目',
+                    l10n.get('selectSubject'),
                     _mockSubjects.map((s) => s.$1).toList(),
                     (v) {
                       final s = _mockSubjects.firstWhere((x) => x.$1 == v);
@@ -445,11 +445,11 @@ class _ExpenseApplicationApplyPageState
         _buildBudgetRow(l10n),
         const SizedBox(height: 8),
         FormFieldRow(
-          label: '关联合同号',
+          label: l10n.get('relatedContractNo'),
           value: _referenceNoController.text,
-          hint: '选填',
+          hint: l10n.get('optional'),
           onTap: () => _showTextInput(
-            '关联合同号',
+            l10n.get('relatedContractNo'),
             (v) => setState(() {
               _referenceNoController.text = v;
               _referenceNoController.selection = TextSelection.fromPosition(
@@ -608,7 +608,7 @@ class _ExpenseApplicationApplyPageState
                 const SizedBox(width: 6),
                 Expanded(
                   child: Text(
-                    '您的申请金额已超支,提交后将自动触发高管特批流程',
+                    l10n.get('overBudgetTriggerApproval'),
                     style: TextStyle(
                       fontSize: AppFontSizes.caption,
                       color: colors.danger,
@@ -636,6 +636,7 @@ class _ExpenseApplicationApplyPageState
   static const _units = ['张', '间', '人', '天', '套', '个'];
 
   void _showDetailDialog() {
+    final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     String cat = 'transport';
     String unit = '张';
@@ -647,18 +648,18 @@ class _ExpenseApplicationApplyPageState
       context: context,
       builder: (ctx) => StatefulBuilder(
         builder: (ctx, setDlg) => TDAlertDialog(
-          title: '添加费用明细',
+          title: l10n.get('addExpenseDetail'),
           contentWidget: Column(
             mainAxisSize: MainAxisSize.min,
             crossAxisAlignment: CrossAxisAlignment.start,
             children: [
-              _label('费用类别'),
+              _label(l10n.get('expenseCategory')),
               const SizedBox(height: 4),
               GestureDetector(
                 onTap: () {
                   Navigator.pop(ctx);
                   _showListPicker(
-                    '选择费用类别',
+                    l10n.get('selectExpenseCategory'),
                     _detailCategories.map((c) => c.$2).toList(),
                     (v) {
                       cat = _detailCategories.firstWhere((c) => c.$2 == v).$1;
@@ -695,7 +696,7 @@ class _ExpenseApplicationApplyPageState
                     child: Column(
                       crossAxisAlignment: CrossAxisAlignment.start,
                       children: [
-                        _label('数量'),
+                        _label(l10n.get('quantity')),
                         const SizedBox(height: 4),
                         TDInput(controller: qtyCtrl, hintText: '>0'),
                       ],
@@ -706,12 +707,12 @@ class _ExpenseApplicationApplyPageState
                     child: Column(
                       crossAxisAlignment: CrossAxisAlignment.start,
                       children: [
-                        _label('单位'),
+                        _label(l10n.get('unit')),
                         const SizedBox(height: 4),
                         GestureDetector(
                           onTap: () {
                             Navigator.pop(ctx);
-                            _showListPicker('选择单位', _units, (v) {
+                            _showListPicker(l10n.get('selectUnit'), _units, (v) {
                               unit = v;
                               _showDetailDialog();
                             });
@@ -746,27 +747,27 @@ class _ExpenseApplicationApplyPageState
                 ],
               ),
               const SizedBox(height: 12),
-              _label('单价'),
+              _label(l10n.get('unitPrice')),
               const SizedBox(height: 4),
               TDInput(controller: priceCtrl, hintText: '>0'),
               const SizedBox(height: 12),
-              _label('明细说明'),
+              _label(l10n.get('detailRemark')),
               const SizedBox(height: 4),
-              TDInput(controller: remarkCtrl, hintText: '选填'),
+              TDInput(controller: remarkCtrl, hintText: l10n.get('optional')),
             ],
           ),
           leftBtn: TDDialogButtonOptions(
-            title: '取消',
+            title: l10n.get('cancel'),
             action: () => Navigator.pop(ctx),
           ),
           rightBtn: TDDialogButtonOptions(
-            title: '确定',
+            title: l10n.get('confirm'),
             titleColor: colors.primary,
             action: () {
               final q = int.tryParse(qtyCtrl.text) ?? 0;
               final p = double.tryParse(priceCtrl.text) ?? 0;
               if (q <= 0 || p <= 0) {
-                TDToast.showText('数量和单价必须大于0', context: context);
+                TDToast.showText(l10n.get('quantityPricePositive'), context: context);
                 return;
               }
               setState(
@@ -858,7 +859,7 @@ class _ExpenseApplicationApplyPageState
                       '附件_${DateTime.now().millisecondsSinceEpoch}.jpg',
                     ),
                   );
-                  TDToast.showText('已添加附件(mock)', context: context);
+                  TDToast.showText(l10n.get('mockAttachmentAdded'), context: context);
                 },
                 child: Container(
                   width: 80,
@@ -893,39 +894,39 @@ class _ExpenseApplicationApplyPageState
       showLeft: isDraft,
       onLeftTap: isDraft
           ? () => _showConfirmDialog(
-              '确认重置',
-              '将清空所有已填内容,此操作不可撤销',
-              '取消',
-              '确认重置',
+              l10n.get('confirmReset'),
+              l10n.get('resetWarning'),
+              l10n.get('cancel'),
+              l10n.get('confirmReset'),
               _resetAll,
             )
           : null,
       onCenterTap: () {
-        TDToast.showSuccess('已保存为草稿', context: context);
+        TDToast.showSuccess(l10n.get('draftSavedToast'), context: context);
         context.pop();
       },
       onRightTap: () {
-        final err = _validate();
+        final err = _validate(l10n);
         if (err.isNotEmpty) {
           TDToast.showText(err.first, context: context);
           return;
         }
-        TDToast.showSuccess('已提交,等待审批', context: context);
+        TDToast.showSuccess(l10n.get('submittedAwaitingApproval'), context: context);
         context.pop();
       },
     );
   }
 
-  List<String> _validate() {
+  List<String> _validate(AppLocalizations l10n) {
     final e = <String>[];
-    if (_expenseTypes.isEmpty) e.add('请至少选择一项费用类型');
-    if (_purposeController.text.trim().isEmpty) e.add('请填写费用事由');
-    if (_selectedProjectId == null) e.add('请选择关联项目');
-    if (_selectedSubjectId == null) e.add('请选择预算科目');
-    if (_details.isEmpty) e.add('请至少添加一行费用明细');
+    if (_expenseTypes.isEmpty) e.add(l10n.get('selectAtLeastOneExpenseType'));
+    if (_purposeController.text.trim().isEmpty) e.add(l10n.get('enterFeeReason'));
+    if (_selectedProjectId == null) e.add(l10n.get('selectSubject'));
+    if (_selectedSubjectId == null) e.add(l10n.get('selectSubject'));
+    if (_details.isEmpty) e.add(l10n.get('addAtLeastOneDetail'));
     if (_expenseTypes.contains('travel')) {
-      if (_estimatedStartDate.isEmpty) e.add('请选择预计开始日期');
-      if (_estimatedEndDate.isEmpty) e.add('请选择预计结束日期');
+      if (_estimatedStartDate.isEmpty) e.add(l10n.get('selectEstimatedStartDate'));
+      if (_estimatedEndDate.isEmpty) e.add(l10n.get('selectEstimatedEndDate'));
     }
     return e;
   }
@@ -991,6 +992,7 @@ class _ExpenseApplicationApplyPageState
     List<String> items,
     Function(String) onPick,
   ) {
+    final l10n = AppLocalizations.of(context);
     showDialog(
       context: context,
       builder: (ctx) => TDAlertDialog(
@@ -1013,7 +1015,7 @@ class _ExpenseApplicationApplyPageState
           ),
         ),
         leftBtn: TDDialogButtonOptions(
-          title: '取消',
+          title: l10n.get('cancel'),
           action: () => Navigator.pop(ctx),
         ),
       ),
@@ -1021,19 +1023,20 @@ class _ExpenseApplicationApplyPageState
   }
 
   void _showTextInput(String title, Function(String) onConfirm) {
+    final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final c = TextEditingController();
     showDialog(
       context: context,
       builder: (ctx) => TDAlertDialog(
         title: title,
-        contentWidget: TDInput(controller: c, hintText: '请输入'),
+        contentWidget: TDInput(controller: c, hintText: l10n.get('pleaseEnter')),
         leftBtn: TDDialogButtonOptions(
-          title: '取消',
+          title: l10n.get('cancel'),
           action: () => Navigator.pop(ctx),
         ),
         rightBtn: TDDialogButtonOptions(
-          title: '确定',
+          title: l10n.get('confirm'),
           titleColor: colors.primary,
           action: () {
             onConfirm(c.text);
@@ -1045,6 +1048,7 @@ class _ExpenseApplicationApplyPageState
   }
 
   void _showNumberInput(String title, Function(int) onConfirm) {
+    final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final c = TextEditingController();
     showDialog(
@@ -1054,14 +1058,14 @@ class _ExpenseApplicationApplyPageState
         contentWidget: TDInput(
           controller: c,
           inputType: TextInputType.number,
-          hintText: '请输入数字',
+          hintText: l10n.get('enterNumber'),
         ),
         leftBtn: TDDialogButtonOptions(
-          title: '取消',
+          title: l10n.get('cancel'),
           action: () => Navigator.pop(ctx),
         ),
         rightBtn: TDDialogButtonOptions(
-          title: '确定',
+          title: l10n.get('confirm'),
           titleColor: colors.primary,
           action: () {
             onConfirm(int.tryParse(c.text) ?? 0);
@@ -1073,10 +1077,11 @@ class _ExpenseApplicationApplyPageState
   }
 
   void _pickDate(Function(String) onPick) {
+    final l10n = AppLocalizations.of(context);
     final now = DateTime.now();
     TDPicker.showDatePicker(
       context,
-      title: '选择日期',
+      title: l10n.get('selectDate'),
       useYear: true,
       useMonth: true,
       useDay: true,

+ 28 - 23
lib/features/expense_application/expense_application_detail_page.dart

@@ -44,9 +44,9 @@ class ExpenseApplicationDetailPage extends ConsumerWidget {
             padding: const EdgeInsets.all(16),
             child: Column(
               children: [
-                _buildStatusBanner(app, colors),
+                _buildStatusBanner(context, app, colors),
                 const SizedBox(height: 4),
-                _buildSubmitTime(app, colors),
+                _buildSubmitTime(context, app, colors),
                 const SizedBox(height: 16),
                 _buildBasicInfoSection(app, l10n, colors),
                 const SizedBox(height: 16),
@@ -68,22 +68,24 @@ class ExpenseApplicationDetailPage extends ConsumerWidget {
   }
 
   Widget _buildStatusBanner(
+    BuildContext context,
     ExpenseApplicationModel app,
     AppColorsExtension colors,
   ) {
+    final l10n = AppLocalizations.of(context);
     final (icon, color, label) = switch (app.status) {
-      'approved' => (Icons.check_circle, colors.success, '已通过'),
-      'rejected' => (Icons.cancel, colors.danger, '已拒绝'),
-      'draft' => (Icons.edit, colors.statusGray, '草稿'),
-      _ => (Icons.schedule, colors.warning, '审批中'),
+      'approved' => (Icons.check_circle, colors.success, l10n.get('approved')),
+      'rejected' => (Icons.cancel, colors.danger, l10n.get('rejected')),
+      'draft' => (Icons.edit, colors.statusGray, l10n.get('draft')),
+      _ => (Icons.schedule, colors.warning, l10n.get('pending')),
     };
     final approverText = switch (app.status) {
       'approved' when app.approvalRecords.isNotEmpty =>
-        '审批人:${app.approvalRecords.last.approverName}',
+        '${l10n.get('approver')}:${app.approvalRecords.last.approverName}',
       'rejected' when app.approvalRecords.isNotEmpty =>
-        '拒绝人:${app.approvalRecords.last.approverName}',
+        '${l10n.get('rejecter')}:${app.approvalRecords.last.approverName}',
       'pending' when app.currentApproverId.isNotEmpty =>
-        '当前审批人:${app.currentApproverId}',
+        '${l10n.get('currentApprover')}:${app.currentApproverId}',
       _ => '',
     };
     return StatusBanner(
@@ -95,15 +97,17 @@ class ExpenseApplicationDetailPage extends ConsumerWidget {
   }
 
   Widget _buildSubmitTime(
+    BuildContext context,
     ExpenseApplicationModel app,
     AppColorsExtension colors,
   ) {
+    final l10n = AppLocalizations.of(context);
     return Padding(
       padding: const EdgeInsets.only(left: 4, top: 4),
       child: Align(
         alignment: Alignment.centerLeft,
         child: Text(
-          '提交时间:${du.DateUtils.formatDateTime(app.createTime)}',
+          '${l10n.get('submitTimeText')}:${du.DateUtils.formatDateTime(app.createTime)}',
           style: TextStyle(
             fontSize: AppFontSizes.caption,
             color: colors.textPlaceholder,
@@ -119,8 +123,8 @@ class ExpenseApplicationDetailPage extends ConsumerWidget {
     AppColorsExtension colors,
   ) {
     String urgencyLabel = switch (app.urgency) {
-      'urgent' => '紧急',
-      'normal' => '普通',
+      'urgent' => l10n.get('urgent'),
+      'normal' => l10n.get('normal'),
       _ => app.urgency,
     };
 
@@ -140,7 +144,7 @@ class ExpenseApplicationDetailPage extends ConsumerWidget {
           showArrow: false,
         ),
         FormFieldRow(
-          label: '费用类型',
+          label: l10n.get('expenseType'),
           value: app.expenseType,
           readOnly: true,
           showArrow: false,
@@ -152,7 +156,7 @@ class ExpenseApplicationDetailPage extends ConsumerWidget {
             mainAxisAlignment: MainAxisAlignment.spaceBetween,
             children: [
               Text(
-                '申请金额',
+                l10n.get('expenseAmount'),
                 style: TextStyle(
                   fontSize: AppFontSizes.body,
                   color: colors.textSecondary,
@@ -170,21 +174,21 @@ class ExpenseApplicationDetailPage extends ConsumerWidget {
           ),
         ),
         FormFieldRow(
-          label: '关联项目',
+          label: l10n.get('relatedProject'),
           value: app.projectName.isNotEmpty ? app.projectName : null,
           hint: '-',
           readOnly: true,
           showArrow: false,
         ),
         FormFieldRow(
-          label: '预算科目',
+          label: l10n.get('budgetSubject'),
           value: app.budgetSubjectId.isNotEmpty ? app.budgetSubjectId : null,
           hint: '-',
           readOnly: true,
           showArrow: false,
         ),
         FormFieldRow(
-          label: '紧急程度',
+          label: l10n.get('emergencyLevel'),
           value: urgencyLabel,
           readOnly: true,
           showArrow: false,
@@ -214,7 +218,7 @@ class ExpenseApplicationDetailPage extends ConsumerWidget {
               Expanded(
                 flex: 3,
                 child: Text(
-                  '费用项目',
+                  l10n.get('expenseProject'),
                   style: TextStyle(
                     fontSize: AppFontSizes.caption,
                     fontWeight: FontWeight.w500,
@@ -225,7 +229,7 @@ class ExpenseApplicationDetailPage extends ConsumerWidget {
               Expanded(
                 flex: 2,
                 child: Text(
-                  '金额',
+                  l10n.get('amount'),
                   textAlign: TextAlign.right,
                   style: TextStyle(
                     fontSize: AppFontSizes.caption,
@@ -241,7 +245,7 @@ class ExpenseApplicationDetailPage extends ConsumerWidget {
           Padding(
             padding: EdgeInsets.symmetric(vertical: 8),
             child: Text(
-              '暂无明细数据',
+              l10n.get('noDetailData'),
               style: TextStyle(
                 fontSize: AppFontSizes.body,
                 color: colors.textPlaceholder,
@@ -337,6 +341,7 @@ class ExpenseApplicationDetailPage extends ConsumerWidget {
   }
 
   Widget _buildBottomBar(BuildContext context, ExpenseApplicationModel app) {
+    final l10n = AppLocalizations.of(context);
     final canWithdraw = app.status == 'pending' || app.status == 'draft';
 
     if (!canWithdraw) {
@@ -345,10 +350,10 @@ class ExpenseApplicationDetailPage extends ConsumerWidget {
 
     return ActionBar(
       showLeft: false,
-      centerLabel: '撤回申请',
-      rightLabel: '提交审批',
+      centerLabel: l10n.get('withdrawApplication'),
+      rightLabel: l10n.get('submitApproval'),
       onCenterTap: () {
-        TDToast.showText('已撤回', context: context);
+        TDToast.showText(l10n.get('withdrawn'), context: context);
         context.pop();
       },
       onRightTap: null,

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

@@ -298,7 +298,7 @@ final expenseApplyTypeFilterProvider = StateProvider<String?>((ref) => null);
 final expenseApplyUrgencyFilterProvider = StateProvider<String?>((ref) => null);
 final expenseApplyRefreshProvider = StateProvider<int>((ref) => 0);
 
-final expenseApplicationListProvider = FutureProvider.autoDispose
+final expenseApplicationListProvider = FutureProvider
     .family<List<ExpenseApplicationModel>, String>((ref, status) async {
       ref.watch(expenseAppPageProvider);
       ref.watch(expenseApplyRefreshProvider);

+ 252 - 91
lib/features/expense_application/expense_application_list_page.dart

@@ -7,6 +7,7 @@ import '../shell/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/utils/responsive.dart';
+import '../../core/auth/role_provider.dart';
 import '../../shared/widgets/list_card.dart';
 import '../../shared/widgets/status_tag.dart';
 import '../../shared/widgets/empty_state.dart';
@@ -16,6 +17,8 @@ import '../../core/i18n/app_localizations.dart';
 import 'expense_application_list_controller.dart';
 import 'expense_application_model.dart';
 
+final _scopeProvider = StateProvider<String>((ref) => 'my');
+
 class ExpenseApplicationListPage extends ConsumerStatefulWidget {
   const ExpenseApplicationListPage({super.key});
   @override
@@ -26,15 +29,29 @@ class ExpenseApplicationListPage extends ConsumerStatefulWidget {
 class _ExpenseApplicationListPageState
     extends ConsumerState<ExpenseApplicationListPage>
     with TickerProviderStateMixin {
-  static const _tabLabels = ['全部', '草稿', '审批中', '已通过', '已拒绝', '已撤回'];
-  static const _tabKeys = ['', 'draft', 'pending', 'approved', 'rejected', 'revoked'];
+  List<String> _getTabLabels(AppLocalizations l10n) => [
+    l10n.get('all'),
+    l10n.get('draft'),
+    l10n.get('pending'),
+    l10n.get('approved'),
+    l10n.get('rejected'),
+    l10n.get('revoked'),
+  ];
+  static const _tabKeys = [
+    '',
+    'draft',
+    'pending',
+    'approved',
+    'rejected',
+    'revoked',
+  ];
 
   late final TabController _tabCtrl;
 
   @override
   void initState() {
     super.initState();
-    _tabCtrl = TabController(length: _tabLabels.length, vsync: this);
+    _tabCtrl = TabController(length: _tabKeys.length, vsync: this);
     _tabCtrl.addListener(() {
       if (!_tabCtrl.indexIsChanging) {
         ref.read(expenseApplyStatusFilterProvider.notifier).state =
@@ -58,68 +75,79 @@ class _ExpenseApplicationListPageState
     final urgencyFilter = ref.watch(expenseApplyUrgencyFilterProvider);
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final isManager = ref.watch(isManagerProvider);
 
     final filterGroups = [
-      FilterGroup(title: '日期范围', 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: '结束日期',
-          type: FilterSectionType.dateRange,
-          startDate: dateStart,
-          endDate: dateEnd,
-          onStartChanged: (v) =>
-              ref.read(expenseApplyDateStartProvider.notifier).state = v,
-          onEndChanged: (v) =>
-              ref.read(expenseApplyDateEndProvider.notifier).state = v,
-        ),
-      ]),
-      FilterGroup(title: '其它', type: FilterGroupType.other, sections: [
-        FilterSection(
-          label: '费用类型',
-          type: FilterSectionType.multiSelect,
-          options: const [
-            FilterOption(value: 'travel', label: '差旅费'),
-            FilterOption(value: 'entertainment', label: '业务招待费'),
-            FilterOption(value: 'office', label: '办公费'),
-            FilterOption(value: 'meeting', label: '会议费'),
-          ],
-          selectedValues: typeFilter != null ? [typeFilter] : null,
-          onMultiChanged: (v) =>
-              ref.read(expenseApplyTypeFilterProvider.notifier).state =
-                  v.isNotEmpty ? v.first : null,
-        ),
-        FilterSection(
-          label: '紧急程度',
-          type: FilterSectionType.singleSelect,
-          options: const [
-            FilterOption(value: 'normal', label: '普通'),
-            FilterOption(value: 'urgent', label: '紧急'),
-            FilterOption(value: 'critical', label: '特急'),
-          ],
-          selectedValue: urgencyFilter,
-          onChanged: (v) =>
-              ref.read(expenseApplyUrgencyFilterProvider.notifier).state = v,
-        ),
-      ]),
+      FilterGroup(
+        title: '日期范围',
+        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: '结束日期',
+            type: FilterSectionType.dateRange,
+            startDate: dateStart,
+            endDate: dateEnd,
+            onStartChanged: (v) =>
+                ref.read(expenseApplyDateStartProvider.notifier).state = v,
+            onEndChanged: (v) =>
+                ref.read(expenseApplyDateEndProvider.notifier).state = v,
+          ),
+        ],
+      ),
+      FilterGroup(
+        title: '其它',
+        type: FilterGroupType.other,
+        sections: [
+          FilterSection(
+            label: '费用类型',
+            type: FilterSectionType.multiSelect,
+            options: const [
+              FilterOption(value: 'travel', label: '差旅费'),
+              FilterOption(value: 'entertainment', label: '业务招待费'),
+              FilterOption(value: 'office', label: '办公费'),
+              FilterOption(value: 'meeting', label: '会议费'),
+            ],
+            selectedValues: typeFilter != null ? [typeFilter] : null,
+            onMultiChanged: (v) =>
+                ref.read(expenseApplyTypeFilterProvider.notifier).state =
+                    v.isNotEmpty ? v.first : null,
+          ),
+          FilterSection(
+            label: '紧急程度',
+            type: FilterSectionType.singleSelect,
+            options: const [
+              FilterOption(value: 'normal', label: '普通'),
+              FilterOption(value: 'urgent', label: '紧急'),
+              FilterOption(value: 'critical', label: '特急'),
+            ],
+            selectedValue: urgencyFilter,
+            onChanged: (v) =>
+                ref.read(expenseApplyUrgencyFilterProvider.notifier).state = v,
+          ),
+        ],
+      ),
     ];
     final hasFilter = FilterBar.hasActiveFilter(filterGroups);
-    final onFilterReset = () {
+    void onFilterReset() {
       ref.read(expenseApplyDateStartProvider.notifier).state = null;
       ref.read(expenseApplyDateEndProvider.notifier).state = null;
       ref.read(expenseApplyTypeFilterProvider.notifier).state = null;
       ref.read(expenseApplyUrgencyFilterProvider.notifier).state = null;
-    };
+    }
 
-    ref.read(navBarConfigProvider.notifier).update(
+    ref
+        .read(navBarConfigProvider.notifier)
+        .update(
           NavBarConfig(
             title: l10n.get('expenseApplyList'),
             showBack: true,
@@ -133,7 +161,10 @@ class _ExpenseApplicationListPageState
               ),
               child: Stack(
                 children: [
-                  Icon(TDIcons.filter, color: hasFilter ? colors.primary : colors.textPrimary),
+                  Icon(
+                    TDIcons.filter,
+                    color: hasFilter ? colors.primary : colors.textPrimary,
+                  ),
                   if (hasFilter)
                     Positioned(
                       right: -2,
@@ -156,7 +187,9 @@ class _ExpenseApplicationListPageState
 
     // Sync TabController when external filter changes
     final targetIdx = _tabKeys.indexOf(status);
-    if (targetIdx >= 0 && _tabCtrl.index != targetIdx && !_tabCtrl.indexIsChanging) {
+    if (targetIdx >= 0 &&
+        _tabCtrl.index != targetIdx &&
+        !_tabCtrl.indexIsChanging) {
       WidgetsBinding.instance.addPostFrameCallback((_) {
         if (mounted) _tabCtrl.animateTo(targetIdx);
       });
@@ -164,11 +197,13 @@ class _ExpenseApplicationListPageState
 
     return Column(
       children: [
+        if (isManager)
+          _buildScopeChip(colors),
         Container(
           color: colors.bgCard,
           padding: const EdgeInsets.symmetric(horizontal: 8),
           child: TDTabBar(
-            tabs: _tabLabels.map((l) => TDTab(text: l)).toList(),
+            tabs: _getTabLabels(l10n).map((l) => TDTab(text: l)).toList(),
             controller: _tabCtrl,
             labelColor: colors.primary,
             unselectedLabelColor: colors.textSecondary,
@@ -180,7 +215,6 @@ class _ExpenseApplicationListPageState
             isScrollable: true,
             labelPadding: const EdgeInsets.symmetric(horizontal: 12),
             onTap: (index) {
-              ref.invalidate(expenseApplicationListProvider);
               ref.read(expenseApplyStatusFilterProvider.notifier).state =
                   _tabKeys[index];
             },
@@ -202,6 +236,52 @@ 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,
+                ),
+              ),
+            ),
+          ),
+          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,
+                ),
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
   Widget _buildTabContent(int tabIdx) {
     final r = ResponsiveHelper.of(context);
     final statusKey = _tabKeys[tabIdx];
@@ -209,10 +289,7 @@ class _ExpenseApplicationListPageState
     return Center(
       child: ConstrainedBox(
         constraints: BoxConstraints(maxWidth: r.listMaxWidth),
-        child: _ExpenseApplyList(
-          statusKey: statusKey,
-          tabIdx: tabIdx,
-        ),
+        child: _ExpenseApplyList(statusKey: statusKey, tabIdx: tabIdx),
       ),
     );
   }
@@ -222,14 +299,12 @@ class _ExpenseApplyList extends ConsumerWidget {
   final String statusKey;
   final int tabIdx;
 
-  const _ExpenseApplyList({
-    required this.statusKey,
-    required this.tabIdx,
-  });
+  const _ExpenseApplyList({required this.statusKey, required this.tabIdx});
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     final itemsAsync = ref.watch(expenseApplicationListProvider(statusKey));
+    final scope = ref.watch(_scopeProvider);
 
     // 首次加载该 tab:骨架
     if (itemsAsync.isLoading && !itemsAsync.hasValue) {
@@ -242,7 +317,7 @@ class _ExpenseApplyList extends ConsumerWidget {
       onRefresh: () async {
         ref.read(expenseApplyRefreshProvider.notifier).state++;
       },
-      child: _buildContent(itemsAsync, context, ref),
+      child: _buildContent(itemsAsync, context, ref, scope),
     );
   }
 
@@ -250,33 +325,56 @@ class _ExpenseApplyList extends ConsumerWidget {
     AsyncValue<List<ExpenseApplicationModel>> itemsAsync,
     BuildContext context,
     WidgetRef ref,
+    String scope,
   ) {
+    final l10n = AppLocalizations.of(context);
+    final isSub = scope == 'sub';
     // 重新加载中(下拉刷新/筛选变化):保留旧数据
     if (itemsAsync.isReloading) {
       final oldItems = itemsAsync.valueOrNull ?? [];
-      if (oldItems.isEmpty) return const SkeletonLoadingList();
+      if (oldItems.isEmpty) {
+        return ListView(
+          children: [
+            const SizedBox(height: 120),
+            EmptyState(message: l10n.get('noExpenseApplications')),
+          ],
+        );
+      }
       return ListView.builder(
         padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
         itemCount: oldItems.length,
-        itemBuilder: (_, i) => Padding(
-          padding: const EdgeInsets.only(bottom: 16),
-          child: ListCard(
+        itemBuilder: (_, i) {
+          final desc = isSub
+              ? '${oldItems[i].expenseType} — ${oldItems[i].purpose}\n申请人: ${oldItems[i].applicantName} · ${oldItems[i].deptName}'
+              : '${oldItems[i].expenseType} — ${oldItems[i].purpose}';
+          final card = ListCard(
             cardNo: oldItems[i].applicationNo,
             amount: '¥${oldItems[i].estimatedAmount.toStringAsFixed(2)}',
-            description: '${oldItems[i].expenseType} — ${oldItems[i].purpose}',
+            description: desc,
             date: du.DateUtils.formatDate(oldItems[i].createTime),
-            statusTag: StatusTag.fromStatus(oldItems[i].status),
-            onTap: () => context.push('/expense-apply/detail/${oldItems[i].id}'),
-          ),
-        ),
+            statusTag: StatusTag.fromStatus(oldItems[i].status, l10n),
+            onTap: () =>
+                context.push('/expense-apply/detail/${oldItems[i].id}'),
+          );
+          if (isSub && oldItems[i].status == 'pending') {
+            return Padding(
+              padding: const EdgeInsets.only(bottom: 16),
+              child: _buildSwipeApprove(card, oldItems[i].id),
+            );
+          }
+          return Padding(
+            padding: const EdgeInsets.only(bottom: 16),
+            child: card,
+          );
+        },
       );
     }
 
     if (itemsAsync.hasError) {
       return ListView(
-        children: const [
-          SizedBox(height: 120),
-          EmptyState(message: '加载失败'),
+        children: [
+          const SizedBox(height: 120),
+          EmptyState(message: l10n.get('loadFailed')),
         ],
       );
     }
@@ -284,9 +382,9 @@ class _ExpenseApplyList extends ConsumerWidget {
     final items = itemsAsync.requireValue;
     if (items.isEmpty) {
       return ListView(
-        children: const [
-          SizedBox(height: 120),
-          EmptyState(message: '暂无报销申请'),
+        children: [
+          const SizedBox(height: 120),
+          EmptyState(message: l10n.get('noExpenseApplications')),
         ],
       );
     }
@@ -294,17 +392,80 @@ class _ExpenseApplyList extends ConsumerWidget {
     return ListView.builder(
       padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
       itemCount: items.length,
-      itemBuilder: (_, i) => Padding(
-        padding: const EdgeInsets.only(bottom: 16),
-        child: ListCard(
+      itemBuilder: (_, i) {
+        final desc = isSub
+            ? '${items[i].expenseType} — ${items[i].purpose}\n申请人: ${items[i].applicantName} · ${items[i].deptName}'
+            : '${items[i].expenseType} — ${items[i].purpose}';
+        final card = ListCard(
           cardNo: items[i].applicationNo,
           amount: '¥${items[i].estimatedAmount.toStringAsFixed(2)}',
-          description: '${items[i].expenseType} — ${items[i].purpose}',
+          description: desc,
           date: du.DateUtils.formatDate(items[i].createTime),
-          statusTag: StatusTag.fromStatus(items[i].status),
+          statusTag: StatusTag.fromStatus(items[i].status, l10n),
           onTap: () => context.push('/expense-apply/detail/${items[i].id}'),
-        ),
-      ),
+        );
+        if (isSub && items[i].status == 'pending') {
+          return Padding(
+            padding: const EdgeInsets.only(bottom: 16),
+            child: _buildSwipeApprove(card, items[i].id),
+          );
+        }
+        return Padding(
+          padding: const EdgeInsets.only(bottom: 16),
+          child: card,
+        );
+      },
+    );
+  }
+
+  Widget _buildSwipeApprove(Widget card, String itemId) {
+    return Builder(
+      builder: (ctx) {
+        final screenWidth = MediaQuery.of(ctx).size.width;
+        return TDSwipeCell(
+          groupTag: 'expense_apply_approve',
+          right: TDSwipeCellPanel(
+            extentRatio: 100 / screenWidth,
+            children: [
+              TDSwipeCellAction(
+                label: '',
+                backgroundColor: Colors.transparent,
+                builder: (_) => Container(
+                  margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
+                  decoration: BoxDecoration(
+                    color: Colors.green,
+                    borderRadius: BorderRadius.circular(8),
+                  ),
+                  alignment: Alignment.center,
+                  padding: const EdgeInsets.symmetric(horizontal: 12),
+                  child: const Text(
+                    '一键同意',
+                    style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600),
+                  ),
+                ),
+                onPressed: (_) async {
+                  final confirmed = await showDialog<bool>(
+                    context: ctx,
+                    builder: (dCtx) => TDAlertDialog(
+                      title: '确认审批',
+                      content: '确认同意该申请?',
+                      leftBtn: TDDialogButtonOptions(title: '取消', action: () => Navigator.of(dCtx).pop(false)),
+                      rightBtn: TDDialogButtonOptions(title: '确认', action: () => Navigator.of(dCtx).pop(true)),
+                    ),
+                  );
+                  if (confirmed == true) {
+                    // TODO: 接入实际审批 API
+                    if (ctx.mounted) {
+                      TDToast.showSuccess('已审批通过', context: ctx);
+                    }
+                  }
+                },
+              ),
+            ],
+          ),
+          cell: card,
+        );
+      },
     );
   }
 }

+ 4 - 2
lib/features/home/home_controller.dart

@@ -1,4 +1,5 @@
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../../core/auth/role_provider.dart';
 
 /// 系统 Banner 模型
 class SysBanner {
@@ -76,8 +77,9 @@ final bannerProvider = Provider<List<SysBanner>>((ref) {
 ///   'finance'  → 财务版
 ///   'admin'    → 管理员版
 final homeSummaryProvider = FutureProvider<HomeSummary>((ref) async {
-  return const HomeSummary(
-    userRole: 'admin',
+  final role = ref.watch(currentRoleProvider);
+  return HomeSummary(
+    userRole: role,
     // 员工版数据
     monthlyReimbursement: 12800.50,
     monthlySubmittedCount: 8,

+ 160 - 126
lib/features/home/home_page.dart

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

+ 23 - 0
lib/features/messages/message_api.dart

@@ -0,0 +1,23 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../../core/network/api_client.dart';
+import '../../app.dart';
+import 'message_model.dart';
+
+final messageApiProvider = Provider<MessageApi>((ref) => MessageApi(ref.read(apiClientProvider)));
+
+class MessageApi {
+  final ApiClient _client;
+  MessageApi(this._client);
+
+  Future<List<MessageModel>> fetchList({int page = 1, int pageSize = 20}) async {
+    final response = await _client.get<Map<String, dynamic>>(
+      '/messages',
+      queryParameters: {'page': page, 'pageSize': pageSize},
+    );
+    final data = response.data!;
+    final items = (data['items'] as List<dynamic>)
+        .map((e) => MessageModel.fromJson(e as Map<String, dynamic>))
+        .toList();
+    return items;
+  }
+}

+ 2 - 0
lib/features/messages/message_controller.dart

@@ -2,6 +2,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'message_model.dart';
 
 final messageListProvider = FutureProvider<List<MessageModel>>((ref) async {
+  // 模拟网络延迟,使骨架屏可见
+  await Future.delayed(const Duration(milliseconds: 1000));
   return MessageModel.mockMessages;
 });
 

+ 71 - 25
lib/features/messages/message_list_page.dart

@@ -6,8 +6,8 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 import '../../shared/widgets/empty_state.dart';
 import '../shell/nav_bar_config.dart';
 import '../../core/i18n/app_localizations.dart';
-import '../../shared/widgets/loading_widget.dart';
 import '../../shared/widgets/message_item.dart';
+import '../../shared/widgets/skeleton_list_card.dart';
 import 'message_controller.dart';
 import 'message_model.dart';
 import '../../core/theme/app_colors.dart';
@@ -74,39 +74,85 @@ class MessageListPage extends ConsumerWidget {
           );
     }
 
+    // 首次加载:展示骨架屏,不包裹 EasyRefresh
+    if (messagesAsync.isLoading && !messagesAsync.hasValue) {
+      return SkeletonLoadingList(
+        cardBuilder: () => const SkeletonMessageCard(),
+      );
+    }
+
     return EasyRefresh(
       header: TDRefreshHeader(),
       onRefresh: () async {
         ref.invalidate(messageListProvider);
         await ref.read(messageListProvider.future);
       },
-      child: messagesAsync.when(
-        loading: () => const SingleChildScrollView(
-          physics: AlwaysScrollableScrollPhysics(),
-          child: LoadingWidget(),
-        ),
-        error: (_, _) => SingleChildScrollView(
+      child: _buildContent(context, ref, messagesAsync, l10n),
+    );
+  }
+
+  /// 根据加载状态构建列表内容
+  ///
+  /// - [AsyncValue.isReloading]:下拉刷新中,展示旧数据 + header loading
+  /// - [AsyncValue.hasError]:错误状态
+  /// - 空数据:空状态
+  /// - 正常数据:消息列表
+  Widget _buildContent(
+    BuildContext context,
+    WidgetRef ref,
+    AsyncValue<List<MessageModel>> messagesAsync,
+    AppLocalizations l10n,
+  ) {
+    // 下拉刷新中:保留旧数据 + EasyRefresh header loading
+    if (messagesAsync.isReloading) {
+      final oldItems = messagesAsync.valueOrNull ?? [];
+      if (oldItems.isEmpty) {
+        return SingleChildScrollView(
           physics: const AlwaysScrollableScrollPhysics(),
-          child: EmptyState(message: l10n.get('loadFailed')),
+          child: EmptyState(message: l10n.get('noMessages')),
+        );
+      }
+      return ListView.separated(
+        padding: const EdgeInsets.fromLTRB(
+          AppSpacing.md,
+          AppSpacing.md,
+          AppSpacing.md,
+          AppSpacing.lg,
         ),
-        data: (messages) => messages.isEmpty
-            ? SingleChildScrollView(
-                physics: const AlwaysScrollableScrollPhysics(),
-                child: EmptyState(message: l10n.get('noMessages')),
-              )
-            : ListView.separated(
-                padding: const EdgeInsets.fromLTRB(
-                  AppSpacing.md,
-                  AppSpacing.md,
-                  AppSpacing.md,
-                  AppSpacing.lg,
-                ),
-                itemCount: messages.length,
-                separatorBuilder: (_, _) => const SizedBox(height: 12),
-                itemBuilder: (_, i) =>
-                    _buildItem(context, ref, messages[i], l10n),
-              ),
+        itemCount: oldItems.length,
+        separatorBuilder: (_, _) => const SizedBox(height: 12),
+        itemBuilder: (_, i) => _buildItem(context, ref, oldItems[i], l10n),
+      );
+    }
+
+    // 错误
+    if (messagesAsync.hasError) {
+      return SingleChildScrollView(
+        physics: const AlwaysScrollableScrollPhysics(),
+        child: EmptyState(message: l10n.get('loadFailed')),
+      );
+    }
+
+    // 空数据
+    final messages = messagesAsync.requireValue;
+    if (messages.isEmpty) {
+      return SingleChildScrollView(
+        physics: const AlwaysScrollableScrollPhysics(),
+        child: EmptyState(message: l10n.get('noMessages')),
+      );
+    }
+
+    // 正常列表
+    return ListView.separated(
+      padding: const EdgeInsets.fromLTRB(
+        AppSpacing.md,
+        AppSpacing.md,
+        AppSpacing.md,
+        AppSpacing.lg,
       ),
+      itemCount: messages.length,
+      separatorBuilder: (_, _) => const SizedBox(height: 12),
+      itemBuilder: (_, i) => _buildItem(context, ref, messages[i], l10n),
     );
   }
 

+ 39 - 19
lib/features/outing_log/outing_log_create_page.dart

@@ -99,19 +99,20 @@ class _OutingLogCreatePageState extends ConsumerState<OutingLogCreatePage> {
   }
 
   Future<void> _pickContact() async {
+    final l10n = AppLocalizations.of(context);
     if (_selectedCustomer == null) {
-      TDToast.showText('请先选择客户名称', context: context);
+      TDToast.showText(l10n.get('selectCustomerFirst'), context: context);
       return;
     }
     final contacts = _mockContacts[_selectedCustomer];
     if (contacts == null || contacts.isEmpty) {
-      TDToast.showText('该客户暂无联系人', context: context);
+      TDToast.showText(l10n.get('noContact'), context: context);
       return;
     }
     final result = await showDialog<Map<String, String>>(
       context: context,
       builder: (ctx) => TDAlertDialog.vertical(
-        title: '选择联系人',
+        title: l10n.get('selectContact'),
         buttons: contacts
             .map(
               (c) => TDDialogButtonOptions(
@@ -128,8 +129,9 @@ class _OutingLogCreatePageState extends ConsumerState<OutingLogCreatePage> {
   }
 
   Future<void> _takePhoto() async {
+    final l10n = AppLocalizations.of(context);
     if (_photos.length >= _maxPhotos) {
-      TDToast.showText('最多拍摄9张照片', context: context);
+      TDToast.showText(l10n.get('maxPhotoCount'), context: context);
       return;
     }
     final idx = _photos.length + 1;
@@ -138,7 +140,15 @@ class _OutingLogCreatePageState extends ConsumerState<OutingLogCreatePage> {
     });
     if (context.mounted) {
       TDToast.showText(
-        '模拟拍照:已拍摄第 $idx 张照片(含水印:${DateTime.now().toString().substring(0, 19)} | $_gpsLat°N, $_gpsLng°E)',
+        l10n.getString(
+          'mockPhotoTaken',
+          args: {
+            'idx': '$idx',
+            'time': DateTime.now().toString().substring(0, 19),
+            'lat': '$_gpsLat°N',
+            'lng': '$_gpsLng°E',
+          },
+        ),
         context: context,
       );
     }
@@ -149,39 +159,42 @@ class _OutingLogCreatePageState extends ConsumerState<OutingLogCreatePage> {
   }
 
   Future<void> _simulateGps() async {
+    final l10n = AppLocalizations.of(context);
     setState(() {
       _gpsFailed = false;
       _gpsAddress = '深圳市南山区科技园南路88号';
       _gpsAccuracy = 15.0;
     });
-    TDToast.showText('GPS定位成功', context: context);
+    TDToast.showText(l10n.get('gpsSuccess'), context: context);
   }
 
   Future<void> _saveDraft() async {
+    final l10n = AppLocalizations.of(context);
     if (context.mounted) {
-      TDToast.showText('已保存为草稿', context: context);
+      TDToast.showText(l10n.get('draftSavedToast'), context: context);
     }
   }
 
   Future<void> _submit() async {
+    final l10n = AppLocalizations.of(context);
     if (_gpsFailed) {
-      TDToast.showText('无法获取GPS定位,请检查位置权限', context: context);
+      TDToast.showText(l10n.get('gpsPermission'), context: context);
       return;
     }
     if (_gpsAddress.isEmpty) {
-      TDToast.showText('GPS定位中,请稍后', context: context);
+      TDToast.showText(l10n.get('gpsLocatingWait'), context: context);
       return;
     }
     if (_photos.isEmpty) {
-      TDToast.showText('请至少拍摄一张现场照片', context: context);
+      TDToast.showText(l10n.get('requiredPhotos'), context: context);
       return;
     }
     if (_summaryCtrl.text.trim().isEmpty) {
-      TDToast.showText('请填写工作总结', context: context);
+      TDToast.showText(l10n.get('requiredSummary'), context: context);
       return;
     }
     if (context.mounted) {
-      TDToast.showText('外勤日志提交成功', context: context);
+      TDToast.showText(l10n.get('outingLogSubmitted'), context: context);
       context.pop();
     }
   }
@@ -311,7 +324,7 @@ class _OutingLogCreatePageState extends ConsumerState<OutingLogCreatePage> {
                           child: TDInput(
                             controller: _summaryCtrl,
                             maxLines: 5,
-                            hintText: '请填写本次外勤工作总结(必填)',
+                            hintText: l10n.get('workSummaryRequiredHint'),
                           ),
                         ),
                       ],
@@ -331,7 +344,7 @@ class _OutingLogCreatePageState extends ConsumerState<OutingLogCreatePage> {
                           ),
                           child: TDInput(
                             controller: _planCtrl,
-                            hintText: '后续推进计划(选填)',
+                            hintText: l10n.get('followUpOptional'),
                           ),
                         ),
                       ],
@@ -372,7 +385,7 @@ class _OutingLogCreatePageState extends ConsumerState<OutingLogCreatePage> {
                                     ),
                                     SizedBox(height: 4),
                                     Text(
-                                      '点击拍照(至少1张)',
+                                      l10n.get('tapToTakePhoto'),
                                       style: TextStyle(
                                         fontSize: 12,
                                         color: colors.textPlaceholder,
@@ -431,7 +444,13 @@ class _OutingLogCreatePageState extends ConsumerState<OutingLogCreatePage> {
                               const SizedBox(width: 6),
                               Expanded(
                                 child: Text(
-                                  '照片将自动添加水印:服务器授时 + GPS经纬度(${_gpsLat.toStringAsFixed(4)}°N, ${_gpsLng.toStringAsFixed(4)}°E)',
+                                  l10n.getString(
+                                    'watermarkHintDynamic',
+                                    args: {
+                                      'lat': _gpsLat.toStringAsFixed(4),
+                                      'lng': _gpsLng.toStringAsFixed(4),
+                                    },
+                                  ),
                                   style: TextStyle(
                                     fontSize: 11,
                                     color: colors.textSecondary,
@@ -462,6 +481,7 @@ class _OutingLogCreatePageState extends ConsumerState<OutingLogCreatePage> {
   }
 
   Widget _buildGpsSection() {
+    final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     if (_gpsFailed) {
       return Container(
@@ -480,12 +500,12 @@ class _OutingLogCreatePageState extends ConsumerState<OutingLogCreatePage> {
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: [
                   Text(
-                    '无法获取当前位置',
+                    l10n.get('gpsFailed'),
                     style: TextStyle(fontSize: 14, color: colors.textPrimary),
                   ),
                   SizedBox(height: 4),
                   Text(
-                    '请检查位置权限设置',
+                    l10n.get('gpsFailedHint'),
                     style: TextStyle(fontSize: 12, color: colors.textSecondary),
                   ),
                 ],
@@ -503,7 +523,7 @@ class _OutingLogCreatePageState extends ConsumerState<OutingLogCreatePage> {
                   borderRadius: BorderRadius.circular(4),
                 ),
                 child: Text(
-                  '重试',
+                  l10n.get('retry'),
                   style: TextStyle(fontSize: 12, color: colors.primary),
                 ),
               ),

+ 18 - 14
lib/features/outing_log/outing_log_detail_page.dart

@@ -1,7 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
-import '../../core/theme/app_colors.dart';
 import '../shell/nav_bar_config.dart';
 import '../../core/utils/date_utils.dart' as du;
 import '../../core/i18n/app_localizations.dart';
@@ -9,6 +8,7 @@ import 'outing_log_list_controller.dart';
 import 'outing_log_comment.dart';
 import 'outing_log_model.dart';
 import '../../core/theme/app_colors_extension.dart';
+import '../../core/auth/role_provider.dart';
 
 class OutingLogDetailPage extends ConsumerStatefulWidget {
   final String id;
@@ -21,7 +21,7 @@ class OutingLogDetailPage extends ConsumerStatefulWidget {
 
 class _OutingLogDetailPageState extends ConsumerState<OutingLogDetailPage> {
   late OutingLogModel _log;
-  final bool _isManager = true; // 模拟经理角色
+  // _isManager 现在从 role_provider 动态获取,见 build 方法
   int _rating = 0;
   final _commentCtrl = TextEditingController();
   final List<OutingLogComment> _comments = [];
@@ -57,17 +57,18 @@ class _OutingLogDetailPageState extends ConsumerState<OutingLogDetailPage> {
   }
 
   void _sendComment() {
+    final l10n = AppLocalizations.of(context);
     if (_rating == 0) {
       ScaffoldMessenger.of(
         context,
-      ).showSnackBar(const SnackBar(content: Text('请选择评分')));
+      ).showSnackBar(SnackBar(content: Text(l10n.get('selectRating'))));
       return;
     }
     final content = _commentCtrl.text.trim();
     if (content.isEmpty) {
       ScaffoldMessenger.of(
         context,
-      ).showSnackBar(const SnackBar(content: Text('请输入点评内容')));
+      ).showSnackBar(SnackBar(content: Text(l10n.get('enterComment'))));
       return;
     }
     setState(() {
@@ -88,12 +89,13 @@ class _OutingLogDetailPageState extends ConsumerState<OutingLogDetailPage> {
     });
     ScaffoldMessenger.of(
       context,
-    ).showSnackBar(const SnackBar(content: Text('点评已发送')));
+    ).showSnackBar(SnackBar(content: Text(l10n.get('commentSent'))));
   }
 
   @override
   Widget build(BuildContext context) {
-    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final isManager = ref.watch(isManagerProvider);
+    //final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
 
     ref
@@ -130,7 +132,7 @@ class _OutingLogDetailPageState extends ConsumerState<OutingLogDetailPage> {
                 _buildPhotoSection(),
                 const SizedBox(height: 8),
                 _buildCommentSection(),
-                if (_isManager) _buildManagerCommentInput(),
+                if (isManager) _buildManagerCommentInput(),
                 const SizedBox(height: 24),
               ],
             ),
@@ -142,6 +144,7 @@ class _OutingLogDetailPageState extends ConsumerState<OutingLogDetailPage> {
 
   Widget _buildMapPlaceholder() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final l10n = AppLocalizations.of(context);
     return Container(
       width: double.infinity,
       height: 160,
@@ -150,7 +153,7 @@ class _OutingLogDetailPageState extends ConsumerState<OutingLogDetailPage> {
         onTap: () {
           ScaffoldMessenger.of(
             context,
-          ).showSnackBar(const SnackBar(content: Text('模拟:打开原生导航')));
+          ).showSnackBar(SnackBar(content: Text(l10n.get('mockOpenNavigation'))));
         },
         child: Container(
           decoration: BoxDecoration(
@@ -166,7 +169,7 @@ class _OutingLogDetailPageState extends ConsumerState<OutingLogDetailPage> {
                     Icon(Icons.map_outlined, size: 40, color: colors.primary),
                     SizedBox(height: 4),
                     Text(
-                      '点击查看导航',
+                      l10n.get('tapToViewNavigation'),
                       style: TextStyle(fontSize: 12, color: colors.primary),
                     ),
                   ],
@@ -198,6 +201,7 @@ class _OutingLogDetailPageState extends ConsumerState<OutingLogDetailPage> {
   }
 
   Widget _buildInfoSection() {
+    final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     return Padding(
       padding: const EdgeInsets.all(16),
@@ -220,15 +224,15 @@ class _OutingLogDetailPageState extends ConsumerState<OutingLogDetailPage> {
               ),
             ),
             const SizedBox(height: 12),
-            _buildInfoRow('业务员', _log.salespersonName),
+            _buildInfoRow(l10n.get('salesperson'), _log.salespersonName),
             Divider(height: 12, color: colors.border),
-            _buildInfoRow('部门', _log.deptName),
+            _buildInfoRow(l10n.get('dept'), _log.deptName),
             Divider(height: 12, color: colors.border),
-            _buildInfoRow('客户名', _log.customerName),
+            _buildInfoRow(l10n.get('customerName'), _log.customerName),
             Divider(height: 12, color: colors.border),
-            _buildInfoRow('签到地址', _log.checkInAddress),
+            _buildInfoRow(l10n.get('checkInAddress'), _log.checkInAddress),
             Divider(height: 12, color: colors.border),
-            _buildInfoRow('签到时间', du.DateUtils.formatDateTime(_log.createTime)),
+            _buildInfoRow(l10n.get('checkInTime'), du.DateUtils.formatDateTime(_log.createTime)),
           ],
         ),
       ),

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

@@ -157,7 +157,7 @@ final outingLogDateEndProvider = StateProvider<DateTime?>((ref) => null);
 
 final outingLogRefreshProvider = StateProvider<int>((ref) => 0);
 
-final filteredOutingLogsProvider = FutureProvider.autoDispose
+final filteredOutingLogsProvider = FutureProvider
     .family<List<OutingLogModel>, int>((ref, tabIndex) async {
       ref.watch(outingLogDateStartProvider);
       ref.watch(outingLogDateEndProvider);

+ 90 - 65
lib/features/outing_log/outing_log_list_page.dart

@@ -23,15 +23,20 @@ class OutingLogListPage extends ConsumerStatefulWidget {
 
 class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
     with TickerProviderStateMixin {
-  final _tabLabels = ['全部', '草稿', '已完成'];
   final bool _isManager = true; // 模拟经理角色
+  static const _tabKeys = ['', 'draft', 'completed'];
+  List<String> _getTabLabels(AppLocalizations l10n) => [
+    l10n.get('all'),
+    l10n.get('draft'),
+    l10n.get('completed'),
+  ];
 
   late final TabController _tabCtrl;
 
   @override
   void initState() {
     super.initState();
-    _tabCtrl = TabController(length: _tabLabels.length, vsync: this);
+    _tabCtrl = TabController(length: _tabKeys.length, vsync: this);
     _tabCtrl.addListener(() {
       if (!_tabCtrl.indexIsChanging) {
         ref.read(outingLogTabProvider.notifier).state = _tabCtrl.index;
@@ -63,34 +68,38 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
     }
 
     final filterGroups = [
-      FilterGroup(title: '日期范围', 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: '结束日期',
-          type: FilterSectionType.dateRange,
-          startDate: dateStart,
-          endDate: dateEnd,
-          onStartChanged: (v) =>
-              ref.read(outingLogDateStartProvider.notifier).state = v,
-          onEndChanged: (v) =>
-              ref.read(outingLogDateEndProvider.notifier).state = v,
-        ),
-      ]),
+      FilterGroup(
+        title: '日期范围',
+        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: '结束日期',
+            type: FilterSectionType.dateRange,
+            startDate: dateStart,
+            endDate: dateEnd,
+            onStartChanged: (v) =>
+                ref.read(outingLogDateStartProvider.notifier).state = v,
+            onEndChanged: (v) =>
+                ref.read(outingLogDateEndProvider.notifier).state = v,
+          ),
+        ],
+      ),
     ];
     final hasFilter = FilterBar.hasActiveFilter(filterGroups);
-    final onFilterReset = () {
+    void onFilterReset() {
       ref.read(outingLogDateStartProvider.notifier).state = null;
       ref.read(outingLogDateEndProvider.notifier).state = null;
-    };
+    }
 
     ref
         .read(navBarConfigProvider.notifier)
@@ -108,7 +117,10 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
               ),
               child: Stack(
                 children: [
-                  Icon(TDIcons.filter, color: hasFilter ? colors.primary : colors.textPrimary),
+                  Icon(
+                    TDIcons.filter,
+                    color: hasFilter ? colors.primary : colors.textPrimary,
+                  ),
                   if (hasFilter)
                     Positioned(
                       right: -2,
@@ -133,12 +145,12 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
         constraints: BoxConstraints(maxWidth: r.listMaxWidth),
         child: Column(
           children: [
-            if (_isManager) _buildScopeChip(scopeIndex),
+            if (_isManager) _buildScopeChip(scopeIndex, l10n),
             Container(
               color: colors.bgCard,
               padding: const EdgeInsets.symmetric(horizontal: 8),
               child: TDTabBar(
-                tabs: _tabLabels.map((l) => TDTab(text: l)).toList(),
+                tabs: _getTabLabels(l10n).map((l) => TDTab(text: l)).toList(),
                 controller: _tabCtrl,
                 isScrollable: true,
                 labelColor: colors.primary,
@@ -150,7 +162,6 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
                 dividerHeight: 0,
                 labelPadding: const EdgeInsets.symmetric(horizontal: 12),
                 onTap: (index) {
-                  ref.invalidate(filteredOutingLogsProvider);
                   ref.read(outingLogTabProvider.notifier).state = index;
                 },
               ),
@@ -161,7 +172,7 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
                 color: colors.bgPage,
                 child: TabBarView(
                   controller: _tabCtrl,
-                  children: List.generate(_tabLabels.length, (tabIdx) {
+                  children: List.generate(_tabKeys.length, (tabIdx) {
                     return _OutingLogTabContent(
                       tabIndex: tabIdx,
                       isManager: _isManager,
@@ -176,7 +187,7 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
     );
   }
 
-  Widget _buildScopeChip(int scopeIndex) {
+  Widget _buildScopeChip(int scopeIndex, AppLocalizations l10n) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     return Container(
       width: double.infinity,
@@ -184,9 +195,9 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
       decoration: BoxDecoration(color: colors.bgCard),
       child: Row(
         children: [
-          _buildChip('我的发起', 0, scopeIndex),
+          _buildChip(l10n.get('myApplications'), 0, scopeIndex),
           const SizedBox(width: 12),
-          _buildChip('下属记录', 1, scopeIndex),
+          _buildChip(l10n.get('subordinateRecords'), 1, scopeIndex),
         ],
       ),
     );
@@ -202,9 +213,7 @@ class _OutingLogListPageState extends ConsumerState<OutingLogListPage>
         decoration: BoxDecoration(
           color: selected ? colors.primaryLight : colors.bgPage,
           borderRadius: BorderRadius.circular(16),
-          border: Border.all(
-            color: selected ? colors.primary : colors.border,
-          ),
+          border: Border.all(color: selected ? colors.primary : colors.border),
         ),
         child: Text(
           label,
@@ -223,17 +232,14 @@ class _OutingLogTabContent extends ConsumerWidget {
   final int tabIndex;
   final bool isManager;
 
-  const _OutingLogTabContent({
-    required this.tabIndex,
-    required this.isManager,
-  });
+  const _OutingLogTabContent({required this.tabIndex, required this.isManager});
 
-  static String _statusLabel(String status) {
+  static String _statusLabel(String status, AppLocalizations l10n) {
     switch (status) {
       case 'draft':
-        return '草稿';
+        return l10n.get('draft');
       case 'completed':
-        return '已完成';
+        return l10n.get('completed');
       default:
         return status;
     }
@@ -263,11 +269,20 @@ class _OutingLogTabContent extends ConsumerWidget {
     BuildContext context,
     WidgetRef ref,
   ) {
+    final l10n = AppLocalizations.of(context);
     if (itemsAsync.isReloading) {
       final oldItems = itemsAsync.valueOrNull ?? [];
       if (oldItems.isEmpty) {
-        return SkeletonLoadingList(
-          cardBuilder: () => const SkeletonOutingLogCard(),
+        final message = tabIndex == 1
+            ? l10n.get('noDrafts')
+            : tabIndex == 2
+            ? l10n.get('noCompletedRecords')
+            : l10n.get('noOutingLogs');
+        return ListView(
+          children: [
+            const SizedBox(height: 120),
+            EmptyState(message: message),
+          ],
         );
       }
       return ListView.builder(
@@ -279,9 +294,9 @@ class _OutingLogTabContent extends ConsumerWidget {
 
     if (itemsAsync.hasError) {
       return ListView(
-        children: const [
-          SizedBox(height: 120),
-          EmptyState(message: '加载失败'),
+        children: [
+          const SizedBox(height: 120),
+          EmptyState(message: l10n.get('loadFailed')),
         ],
       );
     }
@@ -289,10 +304,10 @@ class _OutingLogTabContent extends ConsumerWidget {
     final items = itemsAsync.requireValue;
     if (items.isEmpty) {
       final message = tabIndex == 1
-          ? '暂无草稿'
+          ? l10n.get('noDrafts')
           : tabIndex == 2
-              ? '暂无已完成记录'
-              : '暂无外勤日志';
+          ? l10n.get('noCompletedRecords')
+          : l10n.get('noOutingLogs');
       return ListView(
         children: [
           const SizedBox(height: 120),
@@ -309,7 +324,11 @@ class _OutingLogTabContent extends ConsumerWidget {
   }
 
   Widget _buildOutingLogItem(
-      BuildContext context, WidgetRef ref, OutingLogModel item) {
+    BuildContext context,
+    WidgetRef ref,
+    OutingLogModel item,
+  ) {
+    final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final scopeIndex = ref.watch(outingLogScopeProvider);
     return Padding(
@@ -328,8 +347,7 @@ class _OutingLogTabContent extends ConsumerWidget {
               // 编号
               Text(
                 item.visitNo,
-                style: TextStyle(
-                    fontSize: 11, color: colors.textPlaceholder),
+                style: TextStyle(fontSize: 11, color: colors.textPlaceholder),
               ),
               const SizedBox(height: 4),
               // 客户名称 + 状态 + 新点评标记
@@ -353,13 +371,15 @@ class _OutingLogTabContent extends ConsumerWidget {
                         Container(
                           margin: const EdgeInsets.only(right: 6),
                           padding: const EdgeInsets.symmetric(
-                              horizontal: 6, vertical: 2),
+                            horizontal: 6,
+                            vertical: 2,
+                          ),
                           decoration: BoxDecoration(
                             color: colors.warningBg,
                             borderRadius: BorderRadius.circular(3),
                           ),
                           child: Text(
-                            '新点评',
+                            l10n.get('newComment'),
                             style: TextStyle(
                               fontSize: 10,
                               fontWeight: FontWeight.w600,
@@ -368,7 +388,7 @@ class _OutingLogTabContent extends ConsumerWidget {
                           ),
                         ),
                       StatusTag(
-                        text: _statusLabel(item.status),
+                        text: _statusLabel(item.status, l10n),
                         textColor: item.isCompleted
                             ? colors.success
                             : colors.statusGray,
@@ -386,9 +406,11 @@ class _OutingLogTabContent extends ConsumerWidget {
                 Padding(
                   padding: const EdgeInsets.only(bottom: 4),
                   child: Text(
-                    '业务员:${item.salespersonName} · ${item.deptName}',
-                    style: TextStyle(
-                        fontSize: 12, color: colors.primary),
+                    l10n.getString('salespersonLabel', args: {
+                      'name': item.salespersonName,
+                      'dept': item.deptName,
+                    }),
+                    style: TextStyle(fontSize: 12, color: colors.primary),
                   ),
                 ),
               // 签到地址
@@ -396,8 +418,7 @@ class _OutingLogTabContent extends ConsumerWidget {
                 item.checkInAddress,
                 maxLines: 1,
                 overflow: TextOverflow.ellipsis,
-                style: TextStyle(
-                    fontSize: 12, color: colors.textSecondary),
+                style: TextStyle(fontSize: 12, color: colors.textSecondary),
               ),
               const SizedBox(height: 4),
               // 工作总结摘要(50字截取) + 日期
@@ -412,14 +433,18 @@ class _OutingLogTabContent extends ConsumerWidget {
                       maxLines: 1,
                       overflow: TextOverflow.ellipsis,
                       style: TextStyle(
-                          fontSize: 12, color: colors.textPlaceholder),
+                        fontSize: 12,
+                        color: colors.textPlaceholder,
+                      ),
                     ),
                   ),
                   const SizedBox(width: 8),
                   Text(
                     du.DateUtils.formatDate(item.createTime),
                     style: TextStyle(
-                        fontSize: 11, color: colors.textPlaceholder),
+                      fontSize: 11,
+                      color: colors.textPlaceholder,
+                    ),
                   ),
                 ],
               ),

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

@@ -122,7 +122,7 @@ final overtimeTypeFilterProvider = StateProvider<String?>((ref) => null);
 
 final overtimeRefreshProvider = StateProvider<int>((ref) => 0);
 
-final overtimeListProvider = FutureProvider.autoDispose
+final overtimeListProvider = FutureProvider
     .family<List<OvertimeModel>, String>((ref, status) async {
       ref.watch(overtimePageProvider);
       ref.watch(overtimeDateStartProvider);

+ 226 - 74
lib/features/overtime/overtime_list_page.dart

@@ -6,6 +6,7 @@ import 'package:easy_refresh/easy_refresh.dart';
 import '../shell/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';
 import '../../shared/widgets/list_card.dart';
 import '../../shared/widgets/status_tag.dart';
 import '../../shared/widgets/empty_state.dart';
@@ -15,6 +16,8 @@ import '../../core/i18n/app_localizations.dart';
 import 'overtime_list_controller.dart';
 import 'overtime_model.dart';
 
+final _scopeProvider = StateProvider<String>((ref) => 'my');
+
 class OvertimeListPage extends ConsumerStatefulWidget {
   const OvertimeListPage({super.key});
   @override
@@ -23,21 +26,22 @@ class OvertimeListPage extends ConsumerStatefulWidget {
 
 class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
     with TickerProviderStateMixin {
-  static const _tabLabels = ['全部', '草稿', '审批中', '已通过', '已拒绝'];
-  static const _tabKeys = [
-    '',
-    'draft',
-    'pending',
-    'approved',
-    'rejected',
+  List<String> _getTabLabels(AppLocalizations l10n) => [
+    l10n.get('all'),
+    l10n.get('draft'),
+    l10n.get('pending'),
+    l10n.get('approved'),
+    l10n.get('rejected'),
+    l10n.get('revoked'),
   ];
+  static const _tabKeys = ['', 'draft', 'pending', 'approved', 'rejected', 'withdrawn'];
 
   late final TabController _tabCtrl;
 
   @override
   void initState() {
     super.initState();
-    _tabCtrl = TabController(length: _tabLabels.length, vsync: this);
+    _tabCtrl = TabController(length: _tabKeys.length, vsync: this);
     _tabCtrl.addListener(() {
       if (!_tabCtrl.indexIsChanging) {
         ref.read(overtimeStatusFilterProvider.notifier).state =
@@ -60,6 +64,7 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
     final otTypeFilter = ref.watch(overtimeTypeFilterProvider);
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final isManager = ref.watch(isManagerProvider);
 
     // Sync TabController with external filter changes
     final targetIdx = _tabKeys.indexOf(status);
@@ -72,49 +77,57 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
     }
 
     final filterGroups = [
-      FilterGroup(title: '日期范围', 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: '结束日期',
-          type: FilterSectionType.dateRange,
-          startDate: dateStart,
-          endDate: dateEnd,
-          onStartChanged: (v) =>
-              ref.read(overtimeDateStartProvider.notifier).state = v,
-          onEndChanged: (v) =>
-              ref.read(overtimeDateEndProvider.notifier).state = v,
-        ),
-      ]),
-      FilterGroup(title: '其它', type: FilterGroupType.other, sections: [
-        FilterSection(
-          label: '加班类型',
-          type: FilterSectionType.singleSelect,
-          options: const [
-            FilterOption(value: 'workday', label: '工作日加班'),
-            FilterOption(value: 'weekend', label: '休息日加班'),
-            FilterOption(value: 'holiday', label: '节假日加班'),
-          ],
-          selectedValue: otTypeFilter,
-          onChanged: (v) =>
-              ref.read(overtimeTypeFilterProvider.notifier).state = v,
-        ),
-      ]),
+      FilterGroup(
+        title: '日期范围',
+        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: '结束日期',
+            type: FilterSectionType.dateRange,
+            startDate: dateStart,
+            endDate: dateEnd,
+            onStartChanged: (v) =>
+                ref.read(overtimeDateStartProvider.notifier).state = v,
+            onEndChanged: (v) =>
+                ref.read(overtimeDateEndProvider.notifier).state = v,
+          ),
+        ],
+      ),
+      FilterGroup(
+        title: '其它',
+        type: FilterGroupType.other,
+        sections: [
+          FilterSection(
+            label: '加班类型',
+            type: FilterSectionType.singleSelect,
+            options: const [
+              FilterOption(value: 'workday', label: '工作日加班'),
+              FilterOption(value: 'weekend', label: '休息日加班'),
+              FilterOption(value: 'holiday', label: '节假日加班'),
+            ],
+            selectedValue: otTypeFilter,
+            onChanged: (v) =>
+                ref.read(overtimeTypeFilterProvider.notifier).state = v,
+          ),
+        ],
+      ),
     ];
     final hasFilter = FilterBar.hasActiveFilter(filterGroups);
-    final onFilterReset = () {
+    void onFilterReset() {
       ref.read(overtimeDateStartProvider.notifier).state = null;
       ref.read(overtimeDateEndProvider.notifier).state = null;
       ref.read(overtimeTypeFilterProvider.notifier).state = null;
-    };
+    }
 
     ref
         .read(navBarConfigProvider.notifier)
@@ -132,7 +145,10 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
               ),
               child: Stack(
                 children: [
-                  Icon(TDIcons.filter, color: hasFilter ? colors.primary : colors.textPrimary),
+                  Icon(
+                    TDIcons.filter,
+                    color: hasFilter ? colors.primary : colors.textPrimary,
+                  ),
                   if (hasFilter)
                     Positioned(
                       right: -2,
@@ -154,11 +170,13 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
         );
     return Column(
       children: [
+        if (isManager)
+          _buildScopeChip(colors),
         Container(
           color: colors.bgCard,
           padding: const EdgeInsets.symmetric(horizontal: 8),
           child: TDTabBar(
-            tabs: _tabLabels.map((l) => TDTab(text: l)).toList(),
+            tabs: _getTabLabels(l10n).map((l) => TDTab(text: l)).toList(),
             controller: _tabCtrl,
             isScrollable: true,
             labelColor: colors.primary,
@@ -170,7 +188,6 @@ class _OvertimeListPageState extends ConsumerState<OvertimeListPage>
             dividerHeight: 0,
             labelPadding: const EdgeInsets.symmetric(horizontal: 12),
             onTap: (index) {
-              ref.invalidate(overtimeListProvider);
               ref.read(overtimeStatusFilterProvider.notifier).state =
                   _tabKeys[index];
             },
@@ -192,6 +209,52 @@ 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,
+                ),
+              ),
+            ),
+          ),
+          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,
+                ),
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
   Widget _buildTabContent(int tabIdx) {
     return _OvertimeTabContent(statusKey: _tabKeys[tabIdx]);
   }
@@ -205,6 +268,7 @@ class _OvertimeTabContent extends ConsumerWidget {
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     final itemsAsync = ref.watch(overtimeListProvider(statusKey));
+    final scope = ref.watch(_scopeProvider);
 
     if (itemsAsync.isLoading && !itemsAsync.hasValue) {
       return const SkeletonLoadingList();
@@ -215,41 +279,65 @@ class _OvertimeTabContent extends ConsumerWidget {
       onRefresh: () async {
         ref.read(overtimeRefreshProvider.notifier).state++;
       },
-      child: _buildContent(itemsAsync, context, ref),
+      child: _buildContent(itemsAsync, context, ref, scope),
     );
   }
 
   Widget _buildContent(
     AsyncValue<List<OvertimeModel>> itemsAsync,
-    
+
     BuildContext context,
     WidgetRef ref,
+    String scope,
   ) {
+    final l10n = AppLocalizations.of(context);
+    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final isSub = scope == 'sub';
     if (itemsAsync.isReloading) {
       final oldItems = itemsAsync.valueOrNull ?? [];
-      if (oldItems.isEmpty) return const SkeletonLoadingList();
+      if (oldItems.isEmpty) {
+        return ListView(
+          children: [
+            const SizedBox(height: 120),
+            EmptyState(message: l10n.get('noOvertimes')),
+          ],
+        );
+      }
       return ListView.builder(
         padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
         itemCount: oldItems.length,
-        itemBuilder: (_, i) => Padding(
-          padding: const EdgeInsets.only(bottom: 16),
-          child: ListCard(
+        itemBuilder: (_, i) {
+          final desc = isSub
+              ? '${oldItems[i].otType} · ${oldItems[i].compensationType}\n申请人: ${oldItems[i].applicantName} · ${oldItems[i].deptName}'
+              : '${oldItems[i].otType} · ${oldItems[i].compensationType}';
+          final card = ListCard(
             cardNo: oldItems[i].applicationNo,
-            description: '${oldItems[i].otType} · ${oldItems[i].compensationType}',
-            amount: '${oldItems[i].otHours.toStringAsFixed(1)}小时',
+            description: desc,
+            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),
+            statusTag: StatusTag.fromStatus(oldItems[i].status, l10n),
             onTap: () => context.push('/overtime/detail/${oldItems[i].id}'),
-          ),
-        ),
+          );
+          if (isSub && oldItems[i].status == 'pending') {
+            return Padding(
+              padding: const EdgeInsets.only(bottom: 16),
+              child: _buildSwipeApprove(card, oldItems[i].id),
+            );
+          }
+          return Padding(
+            padding: const EdgeInsets.only(bottom: 16),
+            child: card,
+          );
+        },
       );
     }
 
     if (itemsAsync.hasError) {
       return ListView(
-        children: const [
-          SizedBox(height: 120),
-          EmptyState(message: '加载失败'),
+        children: [
+          const SizedBox(height: 120),
+          EmptyState(message: l10n.get('loadFailed')),
         ],
       );
     }
@@ -257,9 +345,9 @@ class _OvertimeTabContent extends ConsumerWidget {
     final items = itemsAsync.requireValue;
     if (items.isEmpty) {
       return ListView(
-        children: const [
-          SizedBox(height: 120),
-          EmptyState(message: '暂无加班记录'),
+        children: [
+          const SizedBox(height: 120),
+          EmptyState(message: l10n.get('noOvertimes')),
         ],
       );
     }
@@ -267,17 +355,81 @@ class _OvertimeTabContent extends ConsumerWidget {
     return ListView.builder(
       padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
       itemCount: items.length,
-      itemBuilder: (_, i) => Padding(
-        padding: const EdgeInsets.only(bottom: 16),
-        child: ListCard(
+      itemBuilder: (_, i) {
+        final desc = isSub
+            ? '${items[i].otType} · ${items[i].compensationType}\n申请人: ${items[i].applicantName} · ${items[i].deptName}'
+            : '${items[i].otType} · ${items[i].compensationType}';
+        final card = ListCard(
           cardNo: items[i].applicationNo,
-          description: '${items[i].otType} · ${items[i].compensationType}',
-          amount: '${items[i].otHours.toStringAsFixed(1)}小时',
+          description: desc,
+          amount: '${items[i].otHours.toStringAsFixed(1)}${l10n.get('hours')}',
+          amountColor: colors.textPrimary,
           date: du.DateUtils.formatDate(items[i].otDate),
-          statusTag: StatusTag.fromStatus(items[i].status),
+          statusTag: StatusTag.fromStatus(items[i].status, l10n),
           onTap: () => context.push('/overtime/detail/${items[i].id}'),
-        ),
-      ),
+        );
+        if (isSub && items[i].status == 'pending') {
+          return Padding(
+            padding: const EdgeInsets.only(bottom: 16),
+            child: _buildSwipeApprove(card, items[i].id),
+          );
+        }
+        return Padding(
+          padding: const EdgeInsets.only(bottom: 16),
+          child: card,
+        );
+      },
+    );
+  }
+
+  Widget _buildSwipeApprove(Widget card, String itemId) {
+    return Builder(
+      builder: (ctx) {
+        final screenWidth = MediaQuery.of(ctx).size.width;
+        return TDSwipeCell(
+          groupTag: 'overtime_approve',
+          right: TDSwipeCellPanel(
+            extentRatio: 100 / screenWidth,
+            children: [
+              TDSwipeCellAction(
+                label: '',
+                backgroundColor: Colors.transparent,
+                builder: (_) => Container(
+                  margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
+                  decoration: BoxDecoration(
+                    color: Colors.green,
+                    borderRadius: BorderRadius.circular(8),
+                  ),
+                  alignment: Alignment.center,
+                  padding: const EdgeInsets.symmetric(horizontal: 12),
+                  child: const Text(
+                    '一键同意',
+                    style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600),
+                  ),
+                ),
+                onPressed: (_) async {
+                  final confirmed = await showDialog<bool>(
+                    context: ctx,
+                    builder: (dCtx) => TDAlertDialog(
+                      title: '确认审批',
+                      content: '确认同意该加班申请?',
+                      leftBtn: TDDialogButtonOptions(title: '取消', action: () => Navigator.of(dCtx).pop(false)),
+                      rightBtn: TDDialogButtonOptions(title: '确认', action: () => Navigator.of(dCtx).pop(true)),
+                    ),
+                  );
+                  if (confirmed == true) {
+                    // TODO: 接入实际审批 API
+                    if (ctx.mounted) {
+                      TDToast.showSuccess('已审批通过', context: ctx);
+                    }
+                  }
+                },
+              ),
+            ],
+          ),
+          cell: card,
+        );
+      },
     );
   }
 }

+ 53 - 0
lib/features/profile/profile_page.dart

@@ -5,6 +5,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 import '../../core/i18n/app_localizations.dart';
 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 '../../core/theme/app_colors.dart';
@@ -20,6 +21,8 @@ class ProfilePage extends ConsumerWidget {
     final currentLocale = ref.watch(localeProvider);
     final themeMode = ref.watch(themeModeProvider);
     final isDark = themeMode == ThemeMode.dark;
+    final currentRole = ref.watch(currentRoleProvider);
+    final isAdmin = currentRole == 'admin';
 
     if (location.startsWith('/profile')) {
       ref
@@ -44,6 +47,14 @@ class ProfilePage extends ConsumerWidget {
             padding: const EdgeInsets.symmetric(horizontal: 16),
             child: Column(
               children: [
+                // ── 角色切换(测试用)──
+                ProfileMenuItem(
+                  icon: Icons.people,
+                  label: '角色切换',
+                  trailing: _roleName(currentRole),
+                  onTap: () => _showRoleSheet(context, ref, currentRole),
+                ),
+                const SizedBox(height: 8),
                 // ── 语言设置 ──
                 ProfileMenuItem(
                   icon: Icons.language,
@@ -65,6 +76,14 @@ class ProfilePage extends ConsumerWidget {
                   ),
                 ),
                 const SizedBox(height: 8),
+                if (isAdmin) ...[
+                  ProfileMenuItem(
+                    icon: Icons.admin_panel_settings,
+                    label: '权限管理',
+                    onTap: () => context.push('/admin/permissions'),
+                  ),
+                  const SizedBox(height: 8),
+                ],
                 // ── 关于 ──
                 ProfileMenuItem(
                   icon: Icons.info_outline,
@@ -167,6 +186,40 @@ class ProfilePage extends ConsumerWidget {
     return '简体中文';
   }
 
+  String _roleName(String role) {
+    for (final (value, label) in roleOptions) {
+      if (value == role) return label;
+    }
+    return role;
+  }
+
+  void _showRoleSheet(
+    BuildContext context,
+    WidgetRef ref,
+    String currentRole,
+  ) {
+    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+
+    TDActionSheet.showListActionSheet(
+      context,
+      showCancel: false,
+      useSafeArea: false,
+      items: roleOptions.map((r) {
+        final isSelected = r.$1 == currentRole;
+        return TDActionSheetItem(
+          label: isSelected ? '${r.$2}  ✓' : r.$2,
+          textStyle: TextStyle(
+            color: isSelected ? colors.primary : colors.textPrimary,
+            fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
+          ),
+        );
+      }).toList(),
+      onSelected: (item, index) {
+        ref.read(currentRoleProvider.notifier).state = roleOptions[index].$1;
+      },
+    );
+  }
+
   void _showLanguageSheet(
     BuildContext context,
     WidgetRef ref,

+ 29 - 22
lib/features/report/expense_apply_detail_report_page.dart

@@ -2,13 +2,12 @@ import 'package:flutter/material.dart';
 import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:fl_chart/fl_chart.dart';
-import '../../core/theme/app_colors.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../shell/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
+import '../../core/auth/role_provider.dart';
 
 /// 事前申请明细报表 - 页面22
-enum _Role { employee, manager, finance, admin }
 
 class ExpenseApplyDetailReportPage extends ConsumerStatefulWidget {
   const ExpenseApplyDetailReportPage({super.key});
@@ -20,12 +19,9 @@ class ExpenseApplyDetailReportPage extends ConsumerStatefulWidget {
 class _ExpenseApplyDetailReportPageState
     extends ConsumerState<ExpenseApplyDetailReportPage> {
   int _timeFilterIdx = 0;
-  final _timeFilters = ['本月', '本季', '本年'];
   DateTime? _customStart;
   DateTime? _customEnd;
   int _statusFilterIdx = 0;
-  final _statusFilters = ['全部', '已通过', '已拒绝', '已撤回'];
-  final _role = _Role.admin;
 
   // Mock 近12月数据(单位:k)
   static const _months = [
@@ -84,6 +80,7 @@ class _ExpenseApplyDetailReportPageState
   Widget build(BuildContext context) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
+    final role = ref.watch(currentRoleProvider);
     ref
         .read(navBarConfigProvider.notifier)
         .update(
@@ -93,8 +90,8 @@ class _ExpenseApplyDetailReportPageState
             onBack: () => context.pop(),
           ),
         );
-    final showDetail = _role != _Role.employee;
-    final showExport = _role == _Role.finance || _role == _Role.admin;
+    final showDetail = role != 'employee';
+    final showExport = role == 'finance' || role == 'admin';
     return Scaffold(
       body: SingleChildScrollView(
         child: Column(
@@ -112,9 +109,9 @@ class _ExpenseApplyDetailReportPageState
           ? FloatingActionButton.small(
               onPressed: () {
                 ScaffoldMessenger.of(context).showSnackBar(
-                  const SnackBar(
-                    content: Text('导出功能(占位)'),
-                    duration: Duration(seconds: 2),
+                  SnackBar(
+                    content: Text(l10n.get('exportPlaceholder')),
+                    duration: const Duration(seconds: 2),
                   ),
                 );
               },
@@ -128,13 +125,19 @@ class _ExpenseApplyDetailReportPageState
   // ── 时间筛选 ──
   Widget _buildTimeFilter() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final l10n = AppLocalizations.of(context);
+    final timeLabels = [
+      l10n.get('filterThisMonth'),
+      l10n.get('filterThisQuarter'),
+      l10n.get('filterThisYear'),
+    ];
     return Container(
       width: double.infinity,
       padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
       color: colors.bgCard,
       child: Row(
         children: [
-          ...List.generate(_timeFilters.length, (i) {
+          ...List.generate(timeLabels.length, (i) {
             final sel = i == _timeFilterIdx;
             return Padding(
               padding: const EdgeInsets.only(right: 8),
@@ -150,7 +153,7 @@ class _ExpenseApplyDetailReportPageState
                     borderRadius: BorderRadius.circular(16),
                   ),
                   child: Text(
-                    _timeFilters[i],
+                    timeLabels[i],
                     style: TextStyle(
                       fontSize: 14,
                       fontWeight: sel ? FontWeight.w600 : FontWeight.normal,
@@ -178,7 +181,7 @@ class _ExpenseApplyDetailReportPageState
                   Text(
                     _customStart != null && _customEnd != null
                         ? '${_customStart!.month}/${_customStart!.day}-${_customEnd!.month}/${_customEnd!.day}'
-                        : '自定义',
+                        : l10n.get('custom'),
                     style: TextStyle(fontSize: 14, color: colors.textSecondary),
                   ),
                 ],
@@ -212,6 +215,12 @@ class _ExpenseApplyDetailReportPageState
   Widget _buildStatusFilter() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
+    final statusLabels = [
+      l10n.get('filterAll'),
+      l10n.get('approved'),
+      l10n.get('rejected'),
+      l10n.get('revoked'),
+    ];
     return Container(
       width: double.infinity,
       padding: const EdgeInsets.fromLTRB(12, 4, 12, 12),
@@ -223,7 +232,7 @@ class _ExpenseApplyDetailReportPageState
             style: TextStyle(fontSize: 13, color: colors.textSecondary),
           ),
           const SizedBox(width: 4),
-          ...List.generate(_statusFilters.length, (i) {
+          ...List.generate(statusLabels.length, (i) {
             final sel = i == _statusFilterIdx;
             return Padding(
               padding: const EdgeInsets.only(right: 6),
@@ -240,7 +249,7 @@ class _ExpenseApplyDetailReportPageState
                     border: sel ? Border.all(color: colors.primary) : null,
                   ),
                   child: Text(
-                    _statusFilters[i],
+                    statusLabels[i],
                     style: TextStyle(
                       fontSize: 12,
                       color: sel ? colors.primary : colors.textSecondary,
@@ -346,7 +355,8 @@ class _ExpenseApplyDetailReportPageState
   Widget _buildChartSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
-    final isManager = _role == _Role.manager;
+    final role = ref.watch(currentRoleProvider);
+    final isManager = role == 'manager';
     final title = isManager
         ? l10n.get('chartDeptApplyCompare')
         : l10n.get('chartTitle4');
@@ -629,6 +639,7 @@ class _ExpenseApplyDetailReportPageState
   Widget _buildDeptListSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
+    final role = ref.watch(currentRoleProvider);
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
       child: Container(
@@ -659,7 +670,7 @@ class _ExpenseApplyDetailReportPageState
                       color: colors.textPrimary,
                     ),
                   ),
-                  if (_role == _Role.manager)
+                  if (role == 'manager')
                     Text(
                       l10n.get('clickChartToFilter'),
                       style: TextStyle(
@@ -673,8 +684,7 @@ class _ExpenseApplyDetailReportPageState
             Container(
               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
               decoration: BoxDecoration(
-                color: colors.bgPage,
-                border: Border(bottom: BorderSide(color: colors.border)),
+                color: colors.bgDisabled,
               ),
               child: Row(
                 children: [
@@ -721,9 +731,6 @@ class _ExpenseApplyDetailReportPageState
                   horizontal: 16,
                   vertical: 10,
                 ),
-                decoration: BoxDecoration(
-                  border: Border(bottom: BorderSide(color: colors.border)),
-                ),
                 child: Row(
                   children: [
                     SizedBox(

+ 38 - 27
lib/features/report/expense_detail_report_page.dart

@@ -2,13 +2,12 @@ import 'package:flutter/material.dart';
 import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:fl_chart/fl_chart.dart';
-import '../../core/theme/app_colors.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../shell/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
+import '../../core/auth/role_provider.dart';
 
 /// 费用报销明细报表 - 页面23
-enum _Role { employee, manager, finance, admin }
 
 class ExpenseDetailReportPage extends ConsumerStatefulWidget {
   const ExpenseDetailReportPage({super.key});
@@ -20,14 +19,10 @@ class ExpenseDetailReportPage extends ConsumerStatefulWidget {
 class _ExpenseDetailReportPageState
     extends ConsumerState<ExpenseDetailReportPage> {
   int _timeFilterIdx = 0;
-  final _timeFilters = ['本月', '本季', '本年'];
   DateTime? _customStart;
   DateTime? _customEnd;
   int _statusFilterIdx = 0;
-  final _statusFilters = ['全部', '已通过', '已拒绝'];
   int _payFilterIdx = 0;
-  final _payFilters = ['全部', '待付款', '已付款'];
-  final _role = _Role.admin;
 
   static const _months = [
     '1月',
@@ -84,6 +79,7 @@ class _ExpenseDetailReportPageState
   Widget build(BuildContext context) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
+    final role = ref.watch(currentRoleProvider);
     ref
         .read(navBarConfigProvider.notifier)
         .update(
@@ -93,8 +89,8 @@ class _ExpenseDetailReportPageState
             onBack: () => context.pop(),
           ),
         );
-    final showDetail = _role != _Role.employee;
-    final showExport = _role == _Role.finance || _role == _Role.admin;
+    final showDetail = role != 'employee';
+    final showExport = role == 'finance' || role == 'admin';
     return Scaffold(
       body: SingleChildScrollView(
         child: Column(
@@ -112,9 +108,9 @@ class _ExpenseDetailReportPageState
           ? FloatingActionButton.small(
               onPressed: () {
                 ScaffoldMessenger.of(context).showSnackBar(
-                  const SnackBar(
-                    content: Text('导出功能(占位)'),
-                    duration: Duration(seconds: 2),
+                  SnackBar(
+                    content: Text(l10n.get('exportPlaceholder')),
+                    duration: const Duration(seconds: 2),
                   ),
                 );
               },
@@ -128,13 +124,19 @@ class _ExpenseDetailReportPageState
   // ── 时间筛选 ──
   Widget _buildTimeFilter() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final l10n = AppLocalizations.of(context);
+    final timeLabels = [
+      l10n.get('filterThisMonth'),
+      l10n.get('filterThisQuarter'),
+      l10n.get('filterThisYear'),
+    ];
     return Container(
       width: double.infinity,
       padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
       color: colors.bgCard,
       child: Row(
         children: [
-          ...List.generate(_timeFilters.length, (i) {
+          ...List.generate(timeLabels.length, (i) {
             final sel = i == _timeFilterIdx;
             return Padding(
               padding: const EdgeInsets.only(right: 8),
@@ -150,7 +152,7 @@ class _ExpenseDetailReportPageState
                     borderRadius: BorderRadius.circular(16),
                   ),
                   child: Text(
-                    _timeFilters[i],
+                    timeLabels[i],
                     style: TextStyle(
                       fontSize: 14,
                       fontWeight: sel ? FontWeight.w600 : FontWeight.normal,
@@ -178,7 +180,7 @@ class _ExpenseDetailReportPageState
                   Text(
                     _customStart != null && _customEnd != null
                         ? '${_customStart!.month}/${_customStart!.day}-${_customEnd!.month}/${_customEnd!.day}'
-                        : '自定义',
+                        : l10n.get('custom'),
                     style: TextStyle(fontSize: 14, color: colors.textSecondary),
                   ),
                 ],
@@ -211,6 +213,17 @@ class _ExpenseDetailReportPageState
   // ── 审批状态 + 付款状态 ──
   Widget _buildStatusFilters() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final l10n = AppLocalizations.of(context);
+    final statusLabels = [
+      l10n.get('filterAll'),
+      l10n.get('approved'),
+      l10n.get('rejected'),
+    ];
+    final payLabels = [
+      l10n.get('filterAll'),
+      l10n.get('statusWaitPay'),
+      l10n.get('paid'),
+    ];
     return Container(
       width: double.infinity,
       padding: const EdgeInsets.fromLTRB(12, 4, 12, 12),
@@ -221,11 +234,11 @@ class _ExpenseDetailReportPageState
           Row(
             children: [
               Text(
-                '审批状态:',
+                '${l10n.get('filterStatus')}:',
                 style: TextStyle(fontSize: 13, color: colors.textSecondary),
               ),
               const SizedBox(width: 4),
-              ...List.generate(_statusFilters.length, (i) {
+              ...List.generate(statusLabels.length, (i) {
                 final sel = i == _statusFilterIdx;
                 return Padding(
                   padding: const EdgeInsets.only(right: 6),
@@ -242,7 +255,7 @@ class _ExpenseDetailReportPageState
                         border: sel ? Border.all(color: colors.primary) : null,
                       ),
                       child: Text(
-                        _statusFilters[i],
+                        statusLabels[i],
                         style: TextStyle(
                           fontSize: 12,
                           color: sel ? colors.primary : colors.textSecondary,
@@ -258,11 +271,11 @@ class _ExpenseDetailReportPageState
           Row(
             children: [
               Text(
-                '付款状态:',
+                '${l10n.get('filterPayment')}:',
                 style: TextStyle(fontSize: 13, color: colors.textSecondary),
               ),
               const SizedBox(width: 4),
-              ...List.generate(_payFilters.length, (i) {
+              ...List.generate(payLabels.length, (i) {
                 final sel = i == _payFilterIdx;
                 return Padding(
                   padding: const EdgeInsets.only(right: 6),
@@ -279,7 +292,7 @@ class _ExpenseDetailReportPageState
                         border: sel ? Border.all(color: colors.primary) : null,
                       ),
                       child: Text(
-                        _payFilters[i],
+                        payLabels[i],
                         style: TextStyle(
                           fontSize: 12,
                           color: sel ? colors.primary : colors.textSecondary,
@@ -387,7 +400,8 @@ class _ExpenseDetailReportPageState
   Widget _buildChartSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
-    final isManager = _role == _Role.manager;
+    final role = ref.watch(currentRoleProvider);
+    final isManager = role == 'manager';
     final title = isManager
         ? l10n.get('chartDeptExpenseCompare')
         : l10n.get('chartTitle1');
@@ -668,6 +682,7 @@ class _ExpenseDetailReportPageState
   Widget _buildDeptListSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
+    final role = ref.watch(currentRoleProvider);
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
       child: Container(
@@ -698,7 +713,7 @@ class _ExpenseDetailReportPageState
                       color: colors.textPrimary,
                     ),
                   ),
-                  if (_role == _Role.manager)
+                  if (role == 'manager')
                     Text(
                       l10n.get('clickChartToFilter'),
                       style: TextStyle(
@@ -712,8 +727,7 @@ class _ExpenseDetailReportPageState
             Container(
               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
               decoration: BoxDecoration(
-                color: colors.bgPage,
-                border: Border(bottom: BorderSide(color: colors.border)),
+                color: colors.bgDisabled,
               ),
               child: Row(
                 children: [
@@ -760,9 +774,6 @@ class _ExpenseDetailReportPageState
                   horizontal: 16,
                   vertical: 10,
                 ),
-                decoration: BoxDecoration(
-                  border: Border(bottom: BorderSide(color: colors.border)),
-                ),
                 child: Row(
                   children: [
                     SizedBox(

+ 21 - 19
lib/features/report/outing_log_report_page.dart

@@ -2,13 +2,12 @@ import 'package:flutter/material.dart';
 import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:fl_chart/fl_chart.dart';
-import '../../core/theme/app_colors.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../shell/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
+import '../../core/auth/role_provider.dart';
 
 /// 外勤日志明细报表 - 页面26
-enum _Role { employee, manager, finance, admin }
 
 class OutingLogReportPage extends ConsumerStatefulWidget {
   const OutingLogReportPage({super.key});
@@ -19,11 +18,9 @@ class OutingLogReportPage extends ConsumerStatefulWidget {
 
 class _OutingLogReportPageState extends ConsumerState<OutingLogReportPage> {
   int _timeFilterIdx = 0;
-  final _timeFilters = ['本月', '本季', '本年'];
   DateTime? _customStart;
   DateTime? _customEnd;
   final _customerSearchCtrl = TextEditingController();
-  final _role = _Role.admin;
 
   static const _months = [
     '1月',
@@ -88,6 +85,7 @@ class _OutingLogReportPageState extends ConsumerState<OutingLogReportPage> {
   Widget build(BuildContext context) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
+    final role = ref.watch(currentRoleProvider);
     ref
         .read(navBarConfigProvider.notifier)
         .update(
@@ -97,8 +95,8 @@ class _OutingLogReportPageState extends ConsumerState<OutingLogReportPage> {
             onBack: () => context.pop(),
           ),
         );
-    final showDetail = _role != _Role.employee;
-    final showExport = _role == _Role.finance || _role == _Role.admin;
+    final showDetail = role != 'employee';
+    final showExport = role == 'finance' || role == 'admin';
     return Scaffold(
       body: SingleChildScrollView(
         child: Column(
@@ -116,9 +114,9 @@ class _OutingLogReportPageState extends ConsumerState<OutingLogReportPage> {
           ? FloatingActionButton.small(
               onPressed: () {
                 ScaffoldMessenger.of(context).showSnackBar(
-                  const SnackBar(
-                    content: Text('导出功能(占位)'),
-                    duration: Duration(seconds: 2),
+                  SnackBar(
+                    content: Text(l10n.get('exportPlaceholder')),
+                    duration: const Duration(seconds: 2),
                   ),
                 );
               },
@@ -131,13 +129,19 @@ class _OutingLogReportPageState extends ConsumerState<OutingLogReportPage> {
 
   Widget _buildTimeFilter() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final l10n = AppLocalizations.of(context);
+    final timeLabels = [
+      l10n.get('filterThisMonth'),
+      l10n.get('filterThisQuarter'),
+      l10n.get('filterThisYear'),
+    ];
     return Container(
       width: double.infinity,
       padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
       color: colors.bgCard,
       child: Row(
         children: [
-          ...List.generate(_timeFilters.length, (i) {
+          ...List.generate(timeLabels.length, (i) {
             final sel = i == _timeFilterIdx;
             return Padding(
               padding: const EdgeInsets.only(right: 8),
@@ -153,7 +157,7 @@ class _OutingLogReportPageState extends ConsumerState<OutingLogReportPage> {
                     borderRadius: BorderRadius.circular(16),
                   ),
                   child: Text(
-                    _timeFilters[i],
+                    timeLabels[i],
                     style: TextStyle(
                       fontSize: 14,
                       fontWeight: sel ? FontWeight.w600 : FontWeight.normal,
@@ -181,7 +185,7 @@ class _OutingLogReportPageState extends ConsumerState<OutingLogReportPage> {
                   Text(
                     _customStart != null && _customEnd != null
                         ? '${_customStart!.month}/${_customStart!.day}-${_customEnd!.month}/${_customEnd!.day}'
-                        : '自定义',
+                        : l10n.get('custom'),
                     style: TextStyle(fontSize: 14, color: colors.textSecondary),
                   ),
                 ],
@@ -370,7 +374,8 @@ class _OutingLogReportPageState extends ConsumerState<OutingLogReportPage> {
   Widget _buildChartSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
-    final isManager = _role == _Role.manager;
+    final role = ref.watch(currentRoleProvider);
+    final isManager = role == 'manager';
     final title = isManager
         ? l10n.get('chartDeptOutingCompare')
         : l10n.get('chartTitle5');
@@ -676,6 +681,7 @@ class _OutingLogReportPageState extends ConsumerState<OutingLogReportPage> {
   Widget _buildDeptListSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
+    final role = ref.watch(currentRoleProvider);
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
       child: Container(
@@ -706,7 +712,7 @@ class _OutingLogReportPageState extends ConsumerState<OutingLogReportPage> {
                       color: colors.textPrimary,
                     ),
                   ),
-                  if (_role == _Role.manager)
+                  if (role == 'manager')
                     Text(
                       l10n.get('clickChartToFilter'),
                       style: TextStyle(
@@ -720,8 +726,7 @@ class _OutingLogReportPageState extends ConsumerState<OutingLogReportPage> {
             Container(
               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
               decoration: BoxDecoration(
-                color: colors.bgPage,
-                border: Border(bottom: BorderSide(color: colors.border)),
+                color: colors.bgDisabled,
               ),
               child: Row(
                 children: [
@@ -768,9 +773,6 @@ class _OutingLogReportPageState extends ConsumerState<OutingLogReportPage> {
                   horizontal: 16,
                   vertical: 10,
                 ),
-                decoration: BoxDecoration(
-                  border: Border(bottom: BorderSide(color: colors.border)),
-                ),
                 child: Row(
                   children: [
                     SizedBox(

+ 29 - 22
lib/features/report/overtime_detail_report_page.dart

@@ -2,13 +2,12 @@ import 'package:flutter/material.dart';
 import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:fl_chart/fl_chart.dart';
-import '../../core/theme/app_colors.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../shell/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
+import '../../core/auth/role_provider.dart';
 
 /// 加班明细报表 - 页面24
-enum _Role { employee, manager, finance, admin }
 
 class OvertimeDetailReportPage extends ConsumerStatefulWidget {
   const OvertimeDetailReportPage({super.key});
@@ -20,12 +19,9 @@ class OvertimeDetailReportPage extends ConsumerStatefulWidget {
 class _OvertimeDetailReportPageState
     extends ConsumerState<OvertimeDetailReportPage> {
   int _timeFilterIdx = 0;
-  final _timeFilters = ['本月', '本季', '本年'];
   DateTime? _customStart;
   DateTime? _customEnd;
   int _typeFilterIdx = 0;
-  final _typeFilters = ['全部', '工作日', '休息日', '节假日'];
-  final _role = _Role.admin;
 
   // 堆叠柱状图数据:各月 工作日/休息日/节假日 工时
   static const _months = [
@@ -97,6 +93,7 @@ class _OvertimeDetailReportPageState
   Widget build(BuildContext context) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
+    final role = ref.watch(currentRoleProvider);
     ref
         .read(navBarConfigProvider.notifier)
         .update(
@@ -106,8 +103,8 @@ class _OvertimeDetailReportPageState
             onBack: () => context.pop(),
           ),
         );
-    final showDetail = _role != _Role.employee;
-    final showExport = _role == _Role.finance || _role == _Role.admin;
+    final showDetail = role != 'employee';
+    final showExport = role == 'finance' || role == 'admin';
     return Scaffold(
       body: SingleChildScrollView(
         child: Column(
@@ -125,9 +122,9 @@ class _OvertimeDetailReportPageState
           ? FloatingActionButton.small(
               onPressed: () {
                 ScaffoldMessenger.of(context).showSnackBar(
-                  const SnackBar(
-                    content: Text('导出功能(占位)'),
-                    duration: Duration(seconds: 2),
+                  SnackBar(
+                    content: Text(l10n.get('exportPlaceholder')),
+                    duration: const Duration(seconds: 2),
                   ),
                 );
               },
@@ -140,13 +137,19 @@ class _OvertimeDetailReportPageState
 
   Widget _buildTimeFilter() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final l10n = AppLocalizations.of(context);
+    final timeLabels = [
+      l10n.get('filterThisMonth'),
+      l10n.get('filterThisQuarter'),
+      l10n.get('filterThisYear'),
+    ];
     return Container(
       width: double.infinity,
       padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
       color: colors.bgCard,
       child: Row(
         children: [
-          ...List.generate(_timeFilters.length, (i) {
+          ...List.generate(timeLabels.length, (i) {
             final sel = i == _timeFilterIdx;
             return Padding(
               padding: const EdgeInsets.only(right: 8),
@@ -162,7 +165,7 @@ class _OvertimeDetailReportPageState
                     borderRadius: BorderRadius.circular(16),
                   ),
                   child: Text(
-                    _timeFilters[i],
+                    timeLabels[i],
                     style: TextStyle(
                       fontSize: 14,
                       fontWeight: sel ? FontWeight.w600 : FontWeight.normal,
@@ -190,7 +193,7 @@ class _OvertimeDetailReportPageState
                   Text(
                     _customStart != null && _customEnd != null
                         ? '${_customStart!.month}/${_customStart!.day}-${_customEnd!.month}/${_customEnd!.day}'
-                        : '自定义',
+                        : l10n.get('custom'),
                     style: TextStyle(fontSize: 14, color: colors.textSecondary),
                   ),
                 ],
@@ -223,6 +226,12 @@ class _OvertimeDetailReportPageState
   Widget _buildTypeFilter() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
+    final typeLabels = [
+      l10n.get('filterAll'),
+      l10n.get('workday'),
+      l10n.get('weekend'),
+      l10n.get('holiday'),
+    ];
     return Container(
       width: double.infinity,
       padding: const EdgeInsets.fromLTRB(12, 4, 12, 12),
@@ -234,7 +243,7 @@ class _OvertimeDetailReportPageState
             style: TextStyle(fontSize: 13, color: colors.textSecondary),
           ),
           const SizedBox(width: 4),
-          ...List.generate(_typeFilters.length, (i) {
+          ...List.generate(typeLabels.length, (i) {
             final sel = i == _typeFilterIdx;
             return Padding(
               padding: const EdgeInsets.only(right: 6),
@@ -251,7 +260,7 @@ class _OvertimeDetailReportPageState
                     border: sel ? Border.all(color: colors.primary) : null,
                   ),
                   child: Text(
-                    _typeFilters[i],
+                    typeLabels[i],
                     style: TextStyle(
                       fontSize: 12,
                       color: sel ? colors.primary : colors.textSecondary,
@@ -356,7 +365,8 @@ class _OvertimeDetailReportPageState
   Widget _buildChartSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
-    final isManager = _role == _Role.manager;
+    final role = ref.watch(currentRoleProvider);
+    final isManager = role == 'manager';
     final title = isManager
         ? l10n.get('chartDeptOvertimeCompare')
         : l10n.get('chartTitle2');
@@ -623,6 +633,7 @@ class _OvertimeDetailReportPageState
   Widget _buildDeptListSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
+    final role = ref.watch(currentRoleProvider);
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
       child: Container(
@@ -653,7 +664,7 @@ class _OvertimeDetailReportPageState
                       color: colors.textPrimary,
                     ),
                   ),
-                  if (_role == _Role.manager)
+                  if (role == 'manager')
                     Text(
                       l10n.get('clickChartToFilter'),
                       style: TextStyle(
@@ -667,8 +678,7 @@ class _OvertimeDetailReportPageState
             Container(
               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
               decoration: BoxDecoration(
-                color: colors.bgPage,
-                border: Border(bottom: BorderSide(color: colors.border)),
+                color: colors.bgDisabled,
               ),
               child: Row(
                 children: [
@@ -703,9 +713,6 @@ class _OvertimeDetailReportPageState
                   horizontal: 16,
                   vertical: 10,
                 ),
-                decoration: BoxDecoration(
-                  border: Border(bottom: BorderSide(color: colors.border)),
-                ),
                 child: Row(
                   children: [
                     SizedBox(

+ 37 - 25
lib/features/report/vehicle_detail_report_page.dart

@@ -2,13 +2,12 @@ import 'package:flutter/material.dart';
 import 'package:go_router/go_router.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:fl_chart/fl_chart.dart';
-import '../../core/theme/app_colors.dart';
 import '../../core/i18n/app_localizations.dart';
 import '../shell/nav_bar_config.dart';
 import '../../core/theme/app_colors_extension.dart';
+import '../../core/auth/role_provider.dart';
 
 /// 用车明细报表 - 页面25
-enum _Role { employee, manager, finance, admin }
 
 class VehicleDetailReportPage extends ConsumerStatefulWidget {
   const VehicleDetailReportPage({super.key});
@@ -20,14 +19,12 @@ class VehicleDetailReportPage extends ConsumerStatefulWidget {
 class _VehicleDetailReportPageState
     extends ConsumerState<VehicleDetailReportPage> {
   int _timeFilterIdx = 0;
-  final _timeFilters = ['本月', '本季', '本年'];
   DateTime? _customStart;
   DateTime? _customEnd;
   int _vehicleFilterIdx = 0;
-  final _vehicleFilters = ['全部', '京A·88888', '京B·66666', '京C·12345'];
+  // 车牌号为 mock 数据,不需要翻译
+  static const _vehiclePlateFilters = ['京A·88888', '京B·66666', '京C·12345'];
   int _usageFilterIdx = 0;
-  final _usageFilters = ['全部', '接待', '商务', '公务'];
-  final _role = _Role.admin;
 
   static const _months = [
     '1月',
@@ -86,6 +83,7 @@ class _VehicleDetailReportPageState
   Widget build(BuildContext context) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
+    final role = ref.watch(currentRoleProvider);
     ref
         .read(navBarConfigProvider.notifier)
         .update(
@@ -95,8 +93,8 @@ class _VehicleDetailReportPageState
             onBack: () => context.pop(),
           ),
         );
-    final showDetail = _role != _Role.employee;
-    final showExport = _role == _Role.finance || _role == _Role.admin;
+    final showDetail = role != 'employee';
+    final showExport = role == 'finance' || role == 'admin';
     return Scaffold(
       body: SingleChildScrollView(
         child: Column(
@@ -114,9 +112,9 @@ class _VehicleDetailReportPageState
           ? FloatingActionButton.small(
               onPressed: () {
                 ScaffoldMessenger.of(context).showSnackBar(
-                  const SnackBar(
-                    content: Text('导出功能(占位)'),
-                    duration: Duration(seconds: 2),
+                  SnackBar(
+                    content: Text(l10n.get('exportPlaceholder')),
+                    duration: const Duration(seconds: 2),
                   ),
                 );
               },
@@ -129,13 +127,19 @@ class _VehicleDetailReportPageState
 
   Widget _buildTimeFilter() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final l10n = AppLocalizations.of(context);
+    final timeLabels = [
+      l10n.get('filterThisMonth'),
+      l10n.get('filterThisQuarter'),
+      l10n.get('filterThisYear'),
+    ];
     return Container(
       width: double.infinity,
       padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
       color: colors.bgCard,
       child: Row(
         children: [
-          ...List.generate(_timeFilters.length, (i) {
+          ...List.generate(timeLabels.length, (i) {
             final sel = i == _timeFilterIdx;
             return Padding(
               padding: const EdgeInsets.only(right: 8),
@@ -151,7 +155,7 @@ class _VehicleDetailReportPageState
                     borderRadius: BorderRadius.circular(16),
                   ),
                   child: Text(
-                    _timeFilters[i],
+                    timeLabels[i],
                     style: TextStyle(
                       fontSize: 14,
                       fontWeight: sel ? FontWeight.w600 : FontWeight.normal,
@@ -179,7 +183,7 @@ class _VehicleDetailReportPageState
                   Text(
                     _customStart != null && _customEnd != null
                         ? '${_customStart!.month}/${_customStart!.day}-${_customEnd!.month}/${_customEnd!.day}'
-                        : '自定义',
+                        : l10n.get('custom'),
                     style: TextStyle(fontSize: 14, color: colors.textSecondary),
                   ),
                 ],
@@ -212,6 +216,16 @@ class _VehicleDetailReportPageState
   Widget _buildExtraFilters() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
+    final vehicleLabels = [
+      l10n.get('filterAll'),
+      ..._vehiclePlateFilters,
+    ];
+    final usageLabels = [
+      l10n.get('filterAll'),
+      l10n.get('filterReception'),
+      l10n.get('businessShort'),
+      l10n.get('official'),
+    ];
     return Container(
       width: double.infinity,
       padding: const EdgeInsets.fromLTRB(12, 4, 12, 12),
@@ -227,7 +241,7 @@ class _VehicleDetailReportPageState
               ),
               const SizedBox(width: 4),
               ...List.generate(
-                _vehicleFilters.length > 3 ? 3 : _vehicleFilters.length,
+                vehicleLabels.length > 3 ? 3 : vehicleLabels.length,
                 (i) {
                   final sel = i == _vehicleFilterIdx;
                   return Padding(
@@ -247,7 +261,7 @@ class _VehicleDetailReportPageState
                               : null,
                         ),
                         child: Text(
-                          _vehicleFilters[i],
+                          vehicleLabels[i],
                           style: TextStyle(
                             fontSize: 12,
                             color: sel ? colors.primary : colors.textSecondary,
@@ -268,7 +282,7 @@ class _VehicleDetailReportPageState
                 style: TextStyle(fontSize: 13, color: colors.textSecondary),
               ),
               const SizedBox(width: 4),
-              ...List.generate(_usageFilters.length, (i) {
+              ...List.generate(usageLabels.length, (i) {
                 final sel = i == _usageFilterIdx;
                 return Padding(
                   padding: const EdgeInsets.only(right: 6),
@@ -285,7 +299,7 @@ class _VehicleDetailReportPageState
                         border: sel ? Border.all(color: colors.primary) : null,
                       ),
                       child: Text(
-                        _usageFilters[i],
+                        usageLabels[i],
                         style: TextStyle(
                           fontSize: 12,
                           color: sel ? colors.primary : colors.textSecondary,
@@ -392,7 +406,8 @@ class _VehicleDetailReportPageState
   Widget _buildChartSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
-    final isManager = _role == _Role.manager;
+    final role = ref.watch(currentRoleProvider);
+    final isManager = role == 'manager';
     final title = isManager
         ? l10n.get('chartDeptVehicleCompare')
         : l10n.get('chartTitle3');
@@ -699,6 +714,7 @@ class _VehicleDetailReportPageState
   Widget _buildDeptListSection() {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     final l10n = AppLocalizations.of(context);
+    final role = ref.watch(currentRoleProvider);
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
       child: Container(
@@ -729,7 +745,7 @@ class _VehicleDetailReportPageState
                       color: colors.textPrimary,
                     ),
                   ),
-                  if (_role == _Role.manager)
+                  if (role == 'manager')
                     Text(
                       l10n.get('clickChartToFilter'),
                       style: TextStyle(
@@ -743,8 +759,7 @@ class _VehicleDetailReportPageState
             Container(
               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
               decoration: BoxDecoration(
-                color: colors.bgPage,
-                border: Border(bottom: BorderSide(color: colors.border)),
+                color: colors.bgDisabled,
               ),
               child: Row(
                 children: [
@@ -791,9 +806,6 @@ class _VehicleDetailReportPageState
                   horizontal: 16,
                   vertical: 10,
                 ),
-                decoration: BoxDecoration(
-                  border: Border(bottom: BorderSide(color: colors.border)),
-                ),
                 child: Row(
                   children: [
                     SizedBox(

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

@@ -95,9 +95,9 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
                   children: [
                     // 车牌号
                     FormFieldRow(
-                      label: '车牌号',
+                      label: l10n.get('licensePlate'),
                       value: v.vehicleId.isNotEmpty ? v.vehicleId : null,
-                      hint: '请选择车牌号',
+                      hint: l10n.get('selectLicensePlate'),
                       onTap: () => _showVehiclePicker(ctrl),
                     ),
                     // 排期冲突提示
@@ -108,17 +108,17 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
                     const SizedBox(height: 8),
                     // 用车目的 (TDPicker)
                     FormFieldRow(
-                      label: '用车目的',
+                      label: l10n.get('vehiclePurpose'),
                       value: _purposeLabel(v.purpose),
-                      hint: '请选择用车目的',
+                      hint: l10n.get('selectVehicleReason'),
                       onTap: () => _showPurposePicker(ctrl),
                     ),
                     const SizedBox(height: 8),
                     // 始发地 (auto-filled, editable)
                     _buildLocationField(
-                      label: '始发地',
+                      label: l10n.get('origin'),
                       controller: _originController,
-                      hint: 'GPS定位中…',
+                      hint: l10n.get('gpsLocating'),
                       onChanged: ctrl.updateOrigin,
                       showMapIcon: false,
                       onMapTap: null,
@@ -126,26 +126,26 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
                     const SizedBox(height: 8),
                     // 目的地 (with map icon)
                     _buildLocationField(
-                      label: '目的地',
+                      label: l10n.get('destination'),
                       controller: _destinationController,
-                      hint: '请输入目的地',
+                      hint: l10n.get('enterDestination'),
                       onChanged: ctrl.updateDestination,
                       showMapIcon: true,
                       onMapTap: () {
-                        TDToast.showText('地图选点即将开放', context: context);
+                        TDToast.showText(l10n.get('mapPickerComingSoon'), context: context);
                       },
                     ),
                     const SizedBox(height: 8),
                     // 出车时间
                     FormFieldRow(
-                      label: '出车时间',
+                      label: l10n.get('departTime'),
                       value: du.DateUtils.formatDateTime(v.startTime),
                       onTap: () =>
                           _pickDateTime(ctrl.updateStartTime, v.startTime),
                     ),
                     // 还车时间
                     FormFieldRow(
-                      label: '还车时间',
+                      label: l10n.get('returnTime'),
                       value: du.DateUtils.formatDateTime(v.endTime),
                       onTap: () => _pickDateTime(ctrl.updateEndTime, v.endTime),
                     ),
@@ -153,7 +153,7 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
                       Padding(
                         padding: EdgeInsets.only(top: 4),
                         child: Text(
-                          '还车时间必须晚于出车时间',
+                          l10n.get('returnTimeMustLater'),
                           style: TextStyle(
                             fontSize: AppFontSizes.caption,
                             color: colors.danger,
@@ -163,10 +163,10 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
                     const SizedBox(height: 8),
                     // 同行人数
                     FormFieldRow(
-                      label: '同行人数',
-                      value: '${v.passengerCount}',
+                      label: l10n.get('passengerCount'),
+                      value: '${v.passengerCount}${l10n.get('personUnit')}',
                       onTap: () => _showNumberInput(
-                        '同行人数',
+                        l10n.get('passengerCount'),
                         ctrl.updatePassengerCount,
                         v.passengerCount,
                       ),
@@ -181,14 +181,14 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
         ),
         ActionBar(
           showLeft: false,
-          centerLabel: '存草稿',
-          rightLabel: '提交审批',
+          centerLabel: l10n.get('saveDraftShort'),
+          rightLabel: l10n.get('submitApproval'),
           onCenterTap: state.isSubmitting
               ? null
               : () async {
                   await ctrl.saveDraft();
                   if (context.mounted) {
-                    TDToast.showText('已保存为草稿', context: context);
+                    TDToast.showText(l10n.get('draftSavedToast'), context: context);
                     context.pop();
                   }
                 },
@@ -200,16 +200,16 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
                   final timeOk = v.endTime.isAfter(v.startTime);
                   setState(() => _showReasonError = !reasonOk);
                   if (!reasonOk || !vehicleOk || !timeOk) {
-                    TDToast.showText('请完善表单信息', context: context);
+                    TDToast.showText(l10n.get('completeFormInfo'), context: context);
                     return;
                   }
                   final ok = await ctrl.submit();
                   if (context.mounted) {
                     if (ok) {
-                      TDToast.showText('已提交,等待审批', context: context);
+                      TDToast.showText(l10n.get('submittedAwaitingApproval'), context: context);
                       context.pop();
                     } else {
-                      TDToast.showText('提交失败,请稍后重试', context: context);
+                      TDToast.showText(l10n.get('submitFailedRetry'), context: context);
                     }
                   }
                 },
@@ -219,32 +219,29 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
   }
 
   String _purposeLabel(String key) {
+    final l10n = AppLocalizations.of(context);
     switch (key) {
       case 'reception':
-        return '客户接待';
+        return l10n.get('customerReception');
       case 'business':
-        return '商务出行';
+        return l10n.get('businessTrip');
       case 'official':
-        return '公务';
+        return l10n.get('official');
       default:
         return key;
     }
   }
 
   String _purposeKey(String label) {
-    switch (label) {
-      case '客户接待':
-        return 'reception';
-      case '商务出行':
-        return 'business';
-      case '公务':
-        return 'official';
-      default:
-        return label;
-    }
+    final l10n = AppLocalizations.of(context);
+    if (label == l10n.get('customerReception')) return 'reception';
+    if (label == l10n.get('businessTrip')) return 'business';
+    if (label == l10n.get('official')) return 'official';
+    return label;
   }
 
   Widget _buildConflictWarning() {
+    final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     return Container(
       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
@@ -259,7 +256,7 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
           const SizedBox(width: 8),
           Expanded(
             child: Text(
-              '该时段车辆已被预订,请选择其他车辆或调整时间',
+              l10n.get('vehicleOccupiedPeriod'),
               style: TextStyle(
                 fontSize: AppFontSizes.caption,
                 color: colors.danger,
@@ -272,12 +269,13 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
   }
 
   Widget _buildReasonField(VehicleApplyController ctrl) {
+    final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
         Text(
-          '用车事由',
+          l10n.get('vehicleReason'),
           style: TextStyle(
             fontSize: AppFontSizes.body,
             color: colors.textSecondary,
@@ -286,7 +284,7 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
         const SizedBox(height: 8),
         TDInput(
           controller: _reasonController,
-          hintText: '请填写用车事由',
+          hintText: l10n.get('enterVehicleReason'),
           onChanged: (v) {
             ctrl.updateReason(v);
             setState(() => _showReasonError = false);
@@ -296,7 +294,7 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
           Padding(
             padding: EdgeInsets.only(top: 4),
             child: Text(
-              '请填写用车事由',
+              l10n.get('enterVehicleReason'),
               style: TextStyle(
                 fontSize: AppFontSizes.caption,
                 color: colors.danger,
@@ -365,6 +363,7 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
     VehicleApplyState state,
     VehicleApplyController ctrl,
   ) {
+    final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
@@ -374,7 +373,7 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
           mainAxisAlignment: MainAxisAlignment.spaceBetween,
           children: [
             Text(
-              '同行人',
+              l10n.get('companion'),
               style: TextStyle(
                 fontSize: AppFontSizes.body,
                 color: colors.textSecondary,
@@ -401,7 +400,7 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
                     ),
                     SizedBox(width: 4),
                     Text(
-                      '添加',
+                      l10n.get('add'),
                       style: TextStyle(
                         fontSize: AppFontSizes.caption,
                         color: colors.primary,
@@ -446,7 +445,7 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
 
   void _showPurposePicker(VehicleApplyController ctrl) {
     final l10n = AppLocalizations.of(context);
-    const purposes = ['客户接待', '商务出行', '公务'];
+    final purposes = [l10n.get('customerReception'), l10n.get('businessTrip'), l10n.get('official')];
     TDPicker.showMultiPicker(
       context,
       title: l10n.get('selectVehicleReason'),
@@ -503,6 +502,7 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
   }
 
   void _showNumberInput(String title, void Function(int) onSave, int current) {
+    final l10n = AppLocalizations.of(context);
     final ctrl = TextEditingController(text: '$current');
     showDialog(
       context: context,
@@ -510,11 +510,11 @@ class _VehicleApplyPageState extends ConsumerState<VehicleApplyPage> {
         title: title,
         contentWidget: TDInput(controller: ctrl, hintText: '请输入数字'),
         leftBtn: TDDialogButtonOptions(
-          title: '取消',
+          title: l10n.get('cancel'),
           action: () => Navigator.pop(context),
         ),
         rightBtn: TDDialogButtonOptions(
-          title: '确定',
+          title: l10n.get('confirm'),
           theme: TDButtonTheme.primary,
           action: () {
             onSave(int.tryParse(ctrl.text) ?? 1);

+ 54 - 48
lib/features/vehicle/vehicle_detail_page.dart

@@ -30,13 +30,14 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
   bool _isSubmittingReturn = false;
 
   String _purposeLabel(String key) {
+    final l10n = AppLocalizations.of(context);
     switch (key) {
       case 'reception':
-        return '客户接待';
+        return l10n.get('customerReception');
       case 'business':
-        return '商务出行';
+        return l10n.get('businessTrip');
       case 'official':
-        return '公务';
+        return l10n.get('official');
       default:
         return key;
     }
@@ -88,7 +89,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
                 ),
                 const SizedBox(height: 8),
                 Text(
-                  '提交时间:${du.DateUtils.formatDateTime(vehicle.createTime)}',
+                  '${l10n.get('submitTimeText')}:${du.DateUtils.formatDateTime(vehicle.createTime)}',
                   style: TextStyle(
                     fontSize: AppFontSizes.caption,
                     color: colors.textSecondary,
@@ -115,6 +116,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
 
   Widget _buildInfoSection(VehicleModel vehicle) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final l10n = AppLocalizations.of(context);
     return Container(
       padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
       decoration: BoxDecoration(
@@ -123,21 +125,21 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
       ),
       child: Column(
         children: [
-          _infoRow('申请人', vehicle.applicantName),
-          _infoRow('所属部门', vehicle.deptName),
+          _infoRow(l10n.get('applicant'), vehicle.applicantName),
+          _infoRow(l10n.get('department'), vehicle.deptName),
           _infoRow(
-            '车牌号',
-            vehicle.vehicleId.isNotEmpty ? vehicle.vehicleId : '未指定',
+            l10n.get('licensePlate'),
+            vehicle.vehicleId.isNotEmpty ? vehicle.vehicleId : l10n.get('noVehicle'),
           ),
-          _infoRow('用车目的', _purposeLabel(vehicle.purpose)),
-          _infoRow('始发地', vehicle.origin.isNotEmpty ? vehicle.origin : '未填写'),
+          _infoRow(l10n.get('vehiclePurpose'), _purposeLabel(vehicle.purpose)),
+          _infoRow(l10n.get('origin'), vehicle.origin.isNotEmpty ? vehicle.origin : l10n.get('unknown')),
           _infoRow(
-            '目的地',
-            vehicle.destination.isNotEmpty ? vehicle.destination : '未填写',
+            l10n.get('destination'),
+            vehicle.destination.isNotEmpty ? vehicle.destination : l10n.get('unknown'),
           ),
-          _infoRow('出车时间', du.DateUtils.formatDateTime(vehicle.startTime)),
-          _infoRow('还车时间', du.DateUtils.formatDateTime(vehicle.endTime)),
-          _infoRow('同行人数', '${vehicle.passengerCount}人'),
+          _infoRow(l10n.get('departTime'), du.DateUtils.formatDateTime(vehicle.startTime)),
+          _infoRow(l10n.get('returnTime'), du.DateUtils.formatDateTime(vehicle.endTime)),
+          _infoRow(l10n.get('passengerCount'), '${vehicle.passengerCount}${l10n.get('personUnit')}'),
         ],
       ),
     );
@@ -176,6 +178,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
 
   Widget _buildMapSection(VehicleModel vehicle) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final l10n = AppLocalizations.of(context);
     return Container(
       padding: const EdgeInsets.all(16),
       decoration: BoxDecoration(
@@ -186,7 +189,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
           Text(
-            '行程路线',
+            l10n.get('tripRoute'),
             style: TextStyle(
               fontSize: AppFontSizes.subtitle,
               fontWeight: FontWeight.w600,
@@ -217,7 +220,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
                           ),
                           const SizedBox(width: 4),
                           Text(
-                            vehicle.origin.isNotEmpty ? vehicle.origin : '始发地',
+                            vehicle.origin.isNotEmpty ? vehicle.origin : l10n.get('origin'),
                             style: TextStyle(
                               fontSize: 13,
                               color: colors.textPrimary,
@@ -245,7 +248,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
                           Text(
                             vehicle.destination.isNotEmpty
                                 ? vehicle.destination
-                                : '目的地',
+                                : l10n.get('destination'),
                             style: TextStyle(
                               fontSize: 13,
                               color: colors.textPrimary,
@@ -261,7 +264,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
                   right: 8,
                   child: GestureDetector(
                     onTap: () {
-                      TDToast.showText('导航即将开放', context: context);
+                      TDToast.showText(l10n.get('navigationComingSoon'), context: context);
                     },
                     child: Container(
                       padding: const EdgeInsets.symmetric(
@@ -272,14 +275,14 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
                         color: colors.primary,
                         borderRadius: BorderRadius.circular(12),
                       ),
-                      child: const Row(
+                      child: Row(
                         mainAxisSize: MainAxisSize.min,
                         children: [
-                          Icon(Icons.navigation, size: 14, color: Colors.white),
-                          SizedBox(width: 4),
+                          const Icon(Icons.navigation, size: 14, color: Colors.white),
+                          const SizedBox(width: 4),
                           Text(
-                            '导航',
-                            style: TextStyle(fontSize: 12, color: Colors.white),
+                            l10n.get('navigation'),
+                            style: const TextStyle(fontSize: 12, color: Colors.white),
                           ),
                         ],
                       ),
@@ -296,6 +299,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
 
   Widget _buildReturnRegistration(VehicleModel vehicle) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final l10n = AppLocalizations.of(context);
     final isEndOdometerValid =
         _endOdometerCtrl.text.isNotEmpty &&
         (double.tryParse(_endOdometerCtrl.text) ?? 0) >=
@@ -316,7 +320,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
               Icon(Icons.drive_eta, size: 20, color: colors.primary),
               const SizedBox(width: 8),
               Text(
-                '还车登记',
+                l10n.get('returnCarRegister'),
                 style: TextStyle(
                   fontSize: AppFontSizes.subtitle,
                   fontWeight: FontWeight.w600,
@@ -336,13 +340,13 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
                 mainAxisAlignment: MainAxisAlignment.spaceBetween,
                 children: [
                   Text(
-                    '实还时间',
+                    l10n.get('actualReturnTime'),
                     style: TextStyle(fontSize: 14, color: colors.textSecondary),
                   ),
                   Text(
                     _actualReturnTime != null
                         ? du.DateUtils.formatDateTime(_actualReturnTime!)
-                        : '请选择',
+                        : l10n.get('pleaseSelect'),
                     style: TextStyle(
                       fontSize: 14,
                       color: _actualReturnTime != null
@@ -359,9 +363,9 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
               padding: const EdgeInsets.only(bottom: 8),
               child: Text(
                 _actualReturnTime!.isBefore(vehicle.endTime)
-                    ? '提示:提前还车'
+                    ? l10n.get('earlyReturn')
                     : _actualReturnTime!.isAfter(vehicle.endTime)
-                    ? '警告:已超出原计划还车时间'
+                    ? l10n.get('overReturnTime')
                     : '',
                 style: TextStyle(
                   fontSize: AppFontSizes.caption,
@@ -375,7 +379,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
           // 出车前里程
           TDInput(
             controller: _startOdometerCtrl,
-            hintText: '出车前里程(公里)',
+            hintText: l10n.get('mileageBefore'),
             inputType: TextInputType.number,
             onChanged: (v) => setState(() {}),
           ),
@@ -383,7 +387,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
           // 还车后里程
           TDInput(
             controller: _endOdometerCtrl,
-            hintText: '还车后里程(公里)',
+            hintText: l10n.get('mileageAfter'),
             inputType: TextInputType.number,
             onChanged: (v) => setState(() {}),
           ),
@@ -391,7 +395,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
             Padding(
               padding: EdgeInsets.only(top: 4),
               child: Text(
-                '还车后里程不能小于出车前里程',
+                l10n.get('mileageInvalid'),
                 style: TextStyle(
                   fontSize: AppFontSizes.caption,
                   color: colors.danger,
@@ -402,7 +406,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
           // 实际费用
           TDInput(
             controller: _actualCostCtrl,
-            hintText: '实际费用金额(元)',
+            hintText: l10n.get('actualCost'),
             inputType: TextInputType.number,
             onChanged: (v) => setState(() {}),
           ),
@@ -410,7 +414,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
           // 费用备注
           TDInput(
             controller: _costRemarkCtrl,
-            hintText: '费用备注(路桥费/停车费等)',
+            hintText: l10n.get('costRemarkLabel'),
             inputType: TextInputType.text,
             onChanged: (v) => setState(() {}),
           ),
@@ -430,9 +434,9 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
                     ? _submitReturn
                     : null,
                 borderRadius: BorderRadius.circular(22),
-                child: const Center(
+                child: Center(
                   child: Text(
-                    '确认还车',
+                    l10n.get('confirmReturnCar'),
                     style: TextStyle(
                       fontSize: AppFontSizes.body,
                       fontWeight: FontWeight.w500,
@@ -472,7 +476,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
           action: () {
             Navigator.pop(ctx);
             setState(() => _isSubmittingReturn = true);
-            TDToast.showText('还车登记已提交', context: context);
+            TDToast.showText(l10n.get('returnCarSubmitted'), context: context);
             setState(() => _isSubmittingReturn = false);
           },
         ),
@@ -579,17 +583,18 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
 
   Widget _buildActionBar(BuildContext context, VehicleModel vehicle) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final l10n = AppLocalizations.of(context);
     Widget? actionButton;
     if (vehicle.status == 'draft') {
-      actionButton = _singleButton('编辑', colors.primary, () {
+      actionButton = _singleButton(l10n.get('edit'), colors.primary, () {
         context.push('/vehicle/apply', extra: vehicle.id);
       });
     } else if (vehicle.status == 'pending') {
-      actionButton = _singleButton('撤回申请', colors.primary, () {
-        TDToast.showText('已撤回', context: context);
+      actionButton = _singleButton(l10n.get('withdrawApplication'), colors.primary, () {
+        TDToast.showText(l10n.get('withdrawn'), context: context);
       });
     } else if (vehicle.status == 'rejected') {
-      actionButton = _singleButton('重新编辑', colors.primary, () {
+      actionButton = _singleButton(l10n.get('reEdit'), colors.primary, () {
         context.push('/vehicle/apply', extra: vehicle.id);
       });
     } else if (vehicle.status == 'returned') {
@@ -599,7 +604,7 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
         decoration: BoxDecoration(color: colors.bgCard),
         child: Center(
           child: Text(
-            '已还车归档于 ${vehicle.actualReturnTime != null ? du.DateUtils.formatDateTime(vehicle.actualReturnTime!) : ''}',
+            l10n.getString('returnCarArchivedAt', args: {'time': vehicle.actualReturnTime != null ? du.DateUtils.formatDateTime(vehicle.actualReturnTime!) : ''}),
             style: TextStyle(
               fontSize: AppFontSizes.caption,
               color: colors.textSecondary,
@@ -648,19 +653,20 @@ class _VehicleDetailPageState extends ConsumerState<VehicleDetailPage> {
 
   (IconData, Color, String) _statusProps(String status) {
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final l10n = AppLocalizations.of(context);
     switch (status) {
       case 'approved':
-        return (Icons.check_circle, colors.success, '已通过');
+        return (Icons.check_circle, colors.success, l10n.get('approved'));
       case 'rejected':
-        return (Icons.cancel, colors.danger, '已拒绝');
+        return (Icons.cancel, colors.danger, l10n.get('rejected'));
       case 'draft':
-        return (Icons.edit_note, colors.statusGray, '草稿');
+        return (Icons.edit_note, colors.statusGray, l10n.get('draft'));
       case 'withdrawn':
-        return (Icons.cancel_outlined, colors.revokedText, '已撤回');
+        return (Icons.cancel_outlined, colors.revokedText, l10n.get('revoked'));
       case 'returned':
-        return (Icons.assignment_return, colors.primary, '已还车');
+        return (Icons.assignment_return, colors.primary, l10n.get('returned'));
       default:
-        return (Icons.access_time, colors.warning, '审批中');
+        return (Icons.access_time, colors.warning, l10n.get('pending'));
     }
   }
 

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

@@ -139,7 +139,7 @@ final vehiclePurposeFilterProvider = StateProvider<String?>((ref) => null);
 
 final vehicleRefreshProvider = StateProvider<int>((ref) => 0);
 
-final vehicleListProvider = FutureProvider.autoDispose
+final vehicleListProvider = FutureProvider
     .family<List<VehicleModel>, String>((ref, status) async {
       ref.watch(vehiclePageProvider);
       ref.watch(vehicleDateStartProvider);

+ 231 - 76
lib/features/vehicle/vehicle_list_page.dart

@@ -5,6 +5,7 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
 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 '../../core/utils/date_utils.dart' as du;
 import '../../shared/widgets/empty_state.dart';
@@ -14,6 +15,8 @@ import '../../core/i18n/app_localizations.dart';
 import 'vehicle_list_controller.dart';
 import 'vehicle_model.dart';
 
+final _scopeProvider = StateProvider<String>((ref) => 'my');
+
 class VehicleListPage extends ConsumerStatefulWidget {
   const VehicleListPage({super.key});
   @override
@@ -22,13 +25,22 @@ class VehicleListPage extends ConsumerStatefulWidget {
 
 class _VehicleListPageState extends ConsumerState<VehicleListPage>
     with TickerProviderStateMixin {
-  static const _tabLabels = ['全部', '草稿', '审批中', '已通过', '已拒绝', '已还车'];
+  List<String> _getTabLabels(AppLocalizations l10n) => [
+    l10n.get('all'),
+    l10n.get('draft'),
+    l10n.get('pending'),
+    l10n.get('approved'),
+    l10n.get('rejected'),
+    l10n.get('revoked'),
+    l10n.get('returned'),
+  ];
   static const _tabKeys = [
     '',
     'draft',
     'pending',
     'approved',
     'rejected',
+    'withdrawn',
     'returned',
   ];
 
@@ -37,7 +49,7 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
   @override
   void initState() {
     super.initState();
-    _tabCtrl = TabController(length: _tabLabels.length, vsync: this);
+    _tabCtrl = TabController(length: _tabKeys.length, vsync: this);
     _tabCtrl.addListener(() {
       if (!_tabCtrl.indexIsChanging) {
         ref.read(vehicleStatusFilterProvider.notifier).state =
@@ -60,6 +72,7 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
     final purposeFilter = ref.watch(vehiclePurposeFilterProvider);
     final l10n = AppLocalizations.of(context);
     final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    final isManager = ref.watch(isManagerProvider);
 
     // Sync TabController with external filter changes
     final targetIdx = _tabKeys.indexOf(status);
@@ -72,49 +85,57 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
     }
 
     final filterGroups = [
-      FilterGroup(title: '日期范围', 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: '结束日期',
-          type: FilterSectionType.dateRange,
-          startDate: dateStart,
-          endDate: dateEnd,
-          onStartChanged: (v) =>
-              ref.read(vehicleDateStartProvider.notifier).state = v,
-          onEndChanged: (v) =>
-              ref.read(vehicleDateEndProvider.notifier).state = v,
-        ),
-      ]),
-      FilterGroup(title: '其它', type: FilterGroupType.other, sections: [
-        FilterSection(
-          label: '用车目的',
-          type: FilterSectionType.singleSelect,
-          options: const [
-            FilterOption(value: 'reception', label: '客户接待'),
-            FilterOption(value: 'business', label: '商务出行'),
-            FilterOption(value: 'official', label: '公务'),
-          ],
-          selectedValue: purposeFilter,
-          onChanged: (v) =>
-              ref.read(vehiclePurposeFilterProvider.notifier).state = v,
-        ),
-      ]),
+      FilterGroup(
+        title: '日期范围',
+        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: '结束日期',
+            type: FilterSectionType.dateRange,
+            startDate: dateStart,
+            endDate: dateEnd,
+            onStartChanged: (v) =>
+                ref.read(vehicleDateStartProvider.notifier).state = v,
+            onEndChanged: (v) =>
+                ref.read(vehicleDateEndProvider.notifier).state = v,
+          ),
+        ],
+      ),
+      FilterGroup(
+        title: '其它',
+        type: FilterGroupType.other,
+        sections: [
+          FilterSection(
+            label: '用车目的',
+            type: FilterSectionType.singleSelect,
+            options: const [
+              FilterOption(value: 'reception', label: '客户接待'),
+              FilterOption(value: 'business', label: '商务出行'),
+              FilterOption(value: 'official', label: '公务'),
+            ],
+            selectedValue: purposeFilter,
+            onChanged: (v) =>
+                ref.read(vehiclePurposeFilterProvider.notifier).state = v,
+          ),
+        ],
+      ),
     ];
     final hasFilter = FilterBar.hasActiveFilter(filterGroups);
-    final onFilterReset = () {
+    void onFilterReset() {
       ref.read(vehicleDateStartProvider.notifier).state = null;
       ref.read(vehicleDateEndProvider.notifier).state = null;
       ref.read(vehiclePurposeFilterProvider.notifier).state = null;
-    };
+    }
 
     ref
         .read(navBarConfigProvider.notifier)
@@ -132,7 +153,10 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
               ),
               child: Stack(
                 children: [
-                  Icon(TDIcons.filter, color: hasFilter ? colors.primary : colors.textPrimary),
+                  Icon(
+                    TDIcons.filter,
+                    color: hasFilter ? colors.primary : colors.textPrimary,
+                  ),
                   if (hasFilter)
                     Positioned(
                       right: -2,
@@ -154,11 +178,13 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
         );
     return Column(
       children: [
+        if (isManager)
+          _buildScopeChip(colors),
         Container(
           color: colors.bgCard,
           padding: const EdgeInsets.symmetric(horizontal: 8),
           child: TDTabBar(
-            tabs: _tabLabels.map((l) => TDTab(text: l)).toList(),
+            tabs: _getTabLabels(l10n).map((l) => TDTab(text: l)).toList(),
             controller: _tabCtrl,
             isScrollable: true,
             labelColor: colors.primary,
@@ -170,7 +196,6 @@ class _VehicleListPageState extends ConsumerState<VehicleListPage>
             dividerHeight: 0,
             labelPadding: const EdgeInsets.symmetric(horizontal: 12),
             onTap: (index) {
-              ref.invalidate(vehicleListProvider);
               ref.read(vehicleStatusFilterProvider.notifier).state =
                   _tabKeys[index];
             },
@@ -192,6 +217,52 @@ 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,
+                ),
+              ),
+            ),
+          ),
+          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,
+                ),
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
   Widget _buildTabContent(int tabIdx) {
     return _VehicleTabContent(statusKey: _tabKeys[tabIdx]);
   }
@@ -205,6 +276,7 @@ class _VehicleTabContent extends ConsumerWidget {
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     final itemsAsync = ref.watch(vehicleListProvider(statusKey));
+    final scope = ref.watch(_scopeProvider);
 
     if (itemsAsync.isLoading && !itemsAsync.hasValue) {
       return SkeletonLoadingList(
@@ -217,38 +289,53 @@ class _VehicleTabContent extends ConsumerWidget {
       onRefresh: () async {
         ref.read(vehicleRefreshProvider.notifier).state++;
       },
-      child: _buildContent(itemsAsync, context, ref),
+      child: _buildContent(itemsAsync, context, ref, scope),
     );
   }
 
   Widget _buildContent(
     AsyncValue<List<VehicleModel>> itemsAsync,
-    
+
     BuildContext context,
     WidgetRef ref,
+    String scope,
   ) {
+    final l10n = AppLocalizations.of(context);
+    final isSub = scope == 'sub';
     if (itemsAsync.isReloading) {
       final oldItems = itemsAsync.valueOrNull ?? [];
       if (oldItems.isEmpty) {
-        return SkeletonLoadingList(
-          cardBuilder: () => const SkeletonVehicleCard(),
+        return ListView(
+          children: [
+            const SizedBox(height: 120),
+            EmptyState(message: l10n.get('noVehicles')),
+          ],
         );
       }
       return ListView.builder(
         padding: const EdgeInsets.all(16),
         itemCount: oldItems.length,
-        itemBuilder: (_, i) => Padding(
-          padding: const EdgeInsets.only(bottom: 16),
-          child: _buildVehicleListItem(context, oldItems[i]),
-        ),
+        itemBuilder: (_, i) {
+          final card = _buildVehicleListItem(context, oldItems[i], isSub: isSub);
+          if (isSub && oldItems[i].status == 'pending') {
+            return Padding(
+              padding: const EdgeInsets.only(bottom: 16),
+              child: _buildSwipeApprove(card, oldItems[i].id),
+            );
+          }
+          return Padding(
+            padding: const EdgeInsets.only(bottom: 16),
+            child: card,
+          );
+        },
       );
     }
 
     if (itemsAsync.hasError) {
       return ListView(
-        children: const [
-          SizedBox(height: 120),
-          EmptyState(message: '加载失败'),
+        children: [
+          const SizedBox(height: 120),
+          EmptyState(message: l10n.get('loadFailed')),
         ],
       );
     }
@@ -256,9 +343,9 @@ class _VehicleTabContent extends ConsumerWidget {
     final items = itemsAsync.requireValue;
     if (items.isEmpty) {
       return ListView(
-        children: const [
-          SizedBox(height: 120),
-          EmptyState(message: '暂无用车记录'),
+        children: [
+          const SizedBox(height: 120),
+          EmptyState(message: l10n.get('noVehicles')),
         ],
       );
     }
@@ -266,14 +353,24 @@ class _VehicleTabContent extends ConsumerWidget {
     return ListView.builder(
       padding: const EdgeInsets.all(16),
       itemCount: items.length,
-      itemBuilder: (_, i) => Padding(
-        padding: const EdgeInsets.only(bottom: 16),
-        child: _buildVehicleListItem(context, items[i]),
-      ),
+      itemBuilder: (_, i) {
+        final card = _buildVehicleListItem(context, items[i], isSub: isSub);
+        if (isSub && items[i].status == 'pending') {
+          return Padding(
+            padding: const EdgeInsets.only(bottom: 16),
+            child: _buildSwipeApprove(card, items[i].id),
+          );
+        }
+        return Padding(
+          padding: const EdgeInsets.only(bottom: 16),
+          child: card,
+        );
+      },
     );
   }
 
-  Widget _buildVehicleListItem(BuildContext context, VehicleModel item) {
+  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;
@@ -282,27 +379,27 @@ class _VehicleTabContent extends ConsumerWidget {
       case 'pending':
         bg = colors.warningBg;
         fg = colors.warning;
-        label = '审批中';
+        label = l10n.get('pending');
       case 'approved':
         bg = colors.successBg;
         fg = colors.success;
-        label = '已通过';
+        label = l10n.get('approved');
       case 'rejected':
         bg = colors.dangerBg;
         fg = colors.danger;
-        label = '已拒绝';
+        label = l10n.get('rejected');
       case 'draft':
         bg = colors.bgPage;
         fg = colors.statusGray;
-        label = '草稿';
+        label = l10n.get('draft');
       case 'revoked':
         bg = colors.revokedBg;
         fg = colors.revokedText;
-        label = '已撤回';
+        label = l10n.get('revoked');
       case 'returned':
         bg = const Color(0xFFEDF2FC);
         fg = const Color(0xFF5A8CDB);
-        label = '已还车';
+        label = l10n.get('returned');
       default:
         bg = colors.bgPage;
         fg = colors.statusGray;
@@ -351,6 +448,19 @@ class _VehicleTabContent extends ConsumerWidget {
                 ),
               ],
             ),
+            if (isSub) ...[
+              const SizedBox(height: 4),
+              Align(
+                alignment: Alignment.centerLeft,
+                child: Text(
+                  '申请人: ${item.applicantName} · ${item.deptName}',
+                  style: TextStyle(
+                    fontSize: AppFontSizes.caption,
+                    color: colors.textPlaceholder,
+                  ),
+                ),
+              ),
+            ],
             const SizedBox(height: 6),
             // R2: 申请单号 + 用途标签
             Row(
@@ -374,10 +484,7 @@ class _VehicleTabContent extends ConsumerWidget {
                   ),
                   child: Text(
                     item.purpose.isNotEmpty ? item.purpose : '公务',
-                    style: TextStyle(
-                      fontSize: 10,
-                      color: colors.primary,
-                    ),
+                    style: TextStyle(fontSize: 10, color: colors.primary),
                   ),
                 ),
               ],
@@ -390,10 +497,7 @@ class _VehicleTabContent extends ConsumerWidget {
                 Flexible(
                   child: Text(
                     '${item.origin.isNotEmpty ? item.origin : '未知'} → ${item.destination.isNotEmpty ? item.destination : '未知'}',
-                    style: TextStyle(
-                      fontSize: 13,
-                      color: colors.textSecondary,
-                    ),
+                    style: TextStyle(fontSize: 13, color: colors.textSecondary),
                     overflow: TextOverflow.ellipsis,
                   ),
                 ),
@@ -412,4 +516,55 @@ class _VehicleTabContent extends ConsumerWidget {
       ),
     );
   }
+
+  Widget _buildSwipeApprove(Widget card, String itemId) {
+    return Builder(
+      builder: (ctx) {
+        final screenWidth = MediaQuery.of(ctx).size.width;
+        return TDSwipeCell(
+          groupTag: 'vehicle_approve',
+          right: TDSwipeCellPanel(
+            extentRatio: 100 / screenWidth,
+            children: [
+              TDSwipeCellAction(
+                label: '',
+                backgroundColor: Colors.transparent,
+                builder: (_) => Container(
+                  margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
+                  decoration: BoxDecoration(
+                    color: Colors.green,
+                    borderRadius: BorderRadius.circular(8),
+                  ),
+                  alignment: Alignment.center,
+                  padding: const EdgeInsets.symmetric(horizontal: 12),
+                  child: const Text(
+                    '一键同意',
+                    style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600),
+                  ),
+                ),
+                onPressed: (_) async {
+                  final confirmed = await showDialog<bool>(
+                    context: ctx,
+                    builder: (dCtx) => TDAlertDialog(
+                      title: '确认审批',
+                      content: '确认同意该用车申请?',
+                      leftBtn: TDDialogButtonOptions(title: '取消', action: () => Navigator.of(dCtx).pop(false)),
+                      rightBtn: TDDialogButtonOptions(title: '确认', action: () => Navigator.of(dCtx).pop(true)),
+                    ),
+                  );
+                  if (confirmed == true) {
+                    // TODO: 接入实际审批 API
+                    if (ctx.mounted) {
+                      TDToast.showSuccess('已审批通过', context: ctx);
+                    }
+                  }
+                },
+              ),
+            ],
+          ),
+          cell: card,
+        );
+      },
+    );
+  }
 }

+ 9 - 6
lib/shared/models/approval_status.dart

@@ -1,3 +1,5 @@
+import '../../core/i18n/app_localizations.dart';
+
 enum ApprovalStatus {
   draft,
   pending,
@@ -5,18 +7,19 @@ enum ApprovalStatus {
   rejected,
   withdrawn;
 
-  String get label {
+  /// 返回本地化的状态标签文本
+  String label(AppLocalizations l10n) {
     switch (this) {
       case ApprovalStatus.draft:
-        return '草稿';
+        return l10n.get('statusDraft');
       case ApprovalStatus.pending:
-        return '待审批';
+        return l10n.get('statusWaitApprove');
       case ApprovalStatus.approved:
-        return '已通过';
+        return l10n.get('statusApproved');
       case ApprovalStatus.rejected:
-        return '已拒绝';
+        return l10n.get('statusRejected');
       case ApprovalStatus.withdrawn:
-        return '已撤回';
+        return l10n.get('statusRevoked');
     }
   }
 }

+ 2 - 0
lib/shared/models/json_helper.dart

@@ -0,0 +1,2 @@
+/// JSON 解析辅助 — 将 int 或 String 值统一转为 String
+String s(dynamic v) => v?.toString() ?? '';

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

@@ -6,6 +6,7 @@ class ListCard extends StatelessWidget {
   final String cardNo;
   final String description;
   final String amount;
+  final Color? amountColor;
   final String date;
   final Widget? statusTag;
   final VoidCallback? onTap;
@@ -15,6 +16,7 @@ class ListCard extends StatelessWidget {
     required this.cardNo,
     required this.description,
     required this.amount,
+    this.amountColor,
     required this.date,
     this.statusTag,
     this.onTap,
@@ -57,7 +59,7 @@ class ListCard extends StatelessWidget {
                   style: TextStyle(
                     fontSize: 16,
                     fontWeight: FontWeight.w700,
-                    color: colors.amountPrimary,
+                    color: amountColor ?? colors.amountPrimary,
                   ),
                 ),
               ],

+ 70 - 0
lib/shared/widgets/skeleton_list_card.dart

@@ -202,6 +202,76 @@ class SkeletonAnnouncementCard extends StatelessWidget {
   }
 }
 
+/// Skeleton 占位卡片,匹配 [MessageItem] 布局(基于 TDSkeleton.fromRowCol):
+///
+/// 真实 MessageItem 结构:
+/// ┌──────────────────────────────────────────┐
+/// │ [● 40×40]  标题文字...        MM-DD HH:mm │ [●] │
+/// │            发送人                         │     │
+/// │            摘要内容...                    │     │
+/// └──────────────────────────────────────────┘
+class SkeletonMessageCard extends StatelessWidget {
+  const SkeletonMessageCard({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    final colors = Theme.of(context).extension<AppColorsExtension>()!;
+    return Container(
+      height: 88,
+      padding: const EdgeInsets.all(12),
+      decoration: BoxDecoration(
+        color: colors.bgCard,
+        borderRadius: BorderRadius.circular(8),
+      ),
+      child: Row(
+        children: [
+          // 左侧圆形图标占位 40×40
+          TDSkeleton.fromRowCol(
+            animation: TDSkeletonAnimation.flashed,
+            rowCol: TDSkeletonRowCol(
+              objects: const [
+                [TDSkeletonRowColObj.rect(width: 40, height: 40, flex: 0)],
+              ],
+            ),
+          ),
+          const SizedBox(width: 12),
+          // 中间三行文字骨架
+          Expanded(
+            child: TDSkeleton.fromRowCol(
+              animation: TDSkeletonAnimation.flashed,
+              rowCol: TDSkeletonRowCol(
+                objects: const [
+                  // R1: 标题(宽) + 时间(窄)
+                  [
+                    TDSkeletonRowColObj.text(height: 17, flex: 3),
+                    TDSkeletonRowColObj.spacer(flex: 1),
+                    TDSkeletonRowColObj.text(width: 80, height: 13, flex: 0),
+                  ],
+                  // R2: 发送人
+                  [TDSkeletonRowColObj.text(width: 80, height: 13, flex: 0)],
+                  // R3: 摘要
+                  [TDSkeletonRowColObj.text(height: 13, flex: 2)],
+                ],
+                style: TDSkeletonRowColStyle(rowSpacing: (_) => 2),
+              ),
+            ),
+          ),
+          const SizedBox(width: 12),
+          // 右侧未读红点占位 8×8
+          TDSkeleton.fromRowCol(
+            animation: TDSkeletonAnimation.flashed,
+            rowCol: TDSkeletonRowCol(
+              objects: const [
+                [TDSkeletonRowColObj.rect(width: 8, height: 8, flex: 0)],
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
 /// 列表加载态:骨架卡片占位
 ///
 /// [cardCount] 骨架卡片数量,默认 5

+ 13 - 12
lib/shared/widgets/status_tag.dart

@@ -1,6 +1,7 @@
 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 {
@@ -19,34 +20,34 @@ class StatusTag extends StatelessWidget {
     this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
   });
 
-  factory StatusTag.fromStatus(String status) {
+  factory StatusTag.fromStatus(String status, AppLocalizations l10n) {
     final (bg, fg) = _statusProps(status);
-    return StatusTag(text: _statusLabel(status), backgroundColor: bg, textColor: fg);
+    return StatusTag(text: _statusLabel(status, l10n), backgroundColor: bg, textColor: fg);
   }
 
-  factory StatusTag.fromPaymentStatus(String status, String paymentStatus) {
+  factory StatusTag.fromPaymentStatus(String status, String paymentStatus, AppLocalizations l10n) {
     if (status == 'approved') {
       if (paymentStatus == 'paid') {
-        return StatusTag(text: '已付款', backgroundColor: AppColors.paidBg, textColor: AppColors.paidText);
+        return StatusTag(text: l10n.get('paid'), backgroundColor: AppColors.paidBg, textColor: AppColors.paidText);
       }
-      return StatusTag(text: '待付款', backgroundColor: AppColors.unpaidBg, textColor: AppColors.unpaidText);
+      return StatusTag(text: l10n.get('statusWaitPay'), backgroundColor: AppColors.unpaidBg, textColor: AppColors.unpaidText);
     }
-    return StatusTag.fromStatus(status);
+    return StatusTag.fromStatus(status, l10n);
   }
 
-  static String _statusLabel(String s) {
+  static String _statusLabel(String s, AppLocalizations l10n) {
     switch (s) {
       case 'pending':
-        return '审批中';
+        return l10n.get('statusPending');
       case 'approved':
-        return '已通过';
+        return l10n.get('statusApproved');
       case 'rejected':
-        return '已拒绝';
+        return l10n.get('statusRejected');
       case 'draft':
-        return '草稿';
+        return l10n.get('statusDraft');
       case 'revoked':
       case 'withdrawn':
-        return '已撤回';
+        return l10n.get('statusRevoked');
       default:
         return s;
     }