Explorar o código

feat: 事前申请表单页重构 + 枚举中心 + mock数据层

【ExpenseApplicationApplyPage 重构】
- 枚举统一引入(Urgency/ExpenseType/TransportType 等替换硬编码)
- 补全所有字段状态绑定(IsOvernight/TransportType/EntertainmentLevel/Guest/CompanionCount 等)
- 必填星标系统(FormFieldRow/_label 支持 required 红*)
- FormSection 新增 leadingIcon(7 个 Section 各配线性图标)
- 间距统一规范(行间 16px,标签→控件 8px,Section 间 16px)
- 特急选项红色适配
- 费用类型选中弹出 TDMessage 提示
- 费用明细卡片样式(圆角/左对齐/无分割线/删除图标 primary50+primary700)
- 输入弹窗改为 showGeneralDialog+TDInputDialog(带初值+清空)
- 明细弹窗改为 TDDropdownMenu 下拉+左右同行布局+必填标记
- TDTextarea 样式统一(bordered+minLines+maxLines)
- 预算行改为 FormFieldRow 模式
- 深色模式/多语言全面适配

【枚举中心 + Mock 数据层】
- lib/core/constants/enums.dart:10 组枚举(DB §6 全覆盖)
- lib/core/data/mock_api_data.dart:项目/科目/预算/费用类别
- 费用大类→明细类别联动过滤(expenseTypeCategories)

【i18n】
- +28 个键(费用类别/交通工具/单位/提示/校验等)
- entertainmentVip/Written 补全
- 三语对齐 670+ 键

【其他修复】
- FormFieldRow 点击整行 + 字体统一 subtitle(16)
- FormSection 标题 fontSize title(18)
- 附件文案修正 6→9(DB 一致)
- DB 文档附件类型同步更新
chengc hai 5 días
pai
achega
67907e1944

+ 30 - 5
assets/i18n/en.json

@@ -196,7 +196,7 @@
   "expenseProject": "Expense Item",
   "expenseReason": "Expense Reason",
   "enterExpenseReason": "Enter reason",
-  "selectProject": "Select project",
+  "selectProject": "Select Project",
   "selectSubject": "Select subject",
   "selectCostCenter": "Select cost center",
   "selectBank": "Select bank",
@@ -244,7 +244,7 @@
   "noDetailHint": "No details, tap above to add",
   "overBudget": "Over budget {amount}",
   "attachmentUpload": "Attachment Upload",
-  "maxAttachment": "Up to 6 images or PDF",
+  "maxAttachment": "Up to 9 attachments, support image/PDF/Word/Excel (image ≤10MB, doc ≤20MB)",
   "attachments": "Attachments",
   "outingDetail": "Outing Detail",
   "outingType": "Outing Type",
@@ -492,7 +492,6 @@
   "workday": "Workday",
   "weekend": "Weekend",
   "holiday": "Holiday",
-  "paid": "Paid",
   "businessShort": "Business",
   "exportPlaceholder": "Export (placeholder)",
   "unitItem": "items",
@@ -571,7 +570,6 @@
   "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",
@@ -632,6 +630,10 @@
   "withdrawConfirm": "Confirm Withdrawal",
   "withdrawConfirmTip": "Confirm withdrawal of this application? The approval process will be terminated.",
   "noMoreData": "No more data",
+  "hintTravelFields": "Please fill in travel details (dates, transport, etc.)",
+  "hintEntertainmentFields": "Please fill in entertainment details (target, level, guests, etc.)",
+  "hintMeetingFields": "Please fill in meeting details (dates, venue, etc.)",
+  "companionNotExceedGuest": "Internal attendees cannot exceed external guests",
   "scopeMyApplications": "My Requests",
   "scopeSubordinates": "Subordinates",
   "searchExpense": "Search report no. or applicant",
@@ -646,5 +648,28 @@
   "filterExpenseEntertainment": "Entertainment",
   "filterExpenseOffice": "Office Expense",
   "filterExpenseMeeting": "Meeting Expense",
-  "filterCritical": "Critical"
+  "filterCritical": "Critical",
+  "costCategoryTransport": "Transport",
+  "costCategoryHotel": "Hotel",
+  "costCategoryOfficeSupplies": "Office Supplies",
+  "costCategoryMeals": "Meals",
+  "costCategoryMaterials": "Materials",
+  "costCategoryService": "Service Fee",
+  "costCategoryOther": "Other",
+  "expenseTypeProcurement": "Procurement",
+  "expenseTypeActivity": "Activity",
+  "expenseTypeTraining": "Training",
+  "transportPlane": "Plane",
+  "transportHighSpeedRail": "High-Speed Rail",
+  "transportTrain": "Train",
+  "transportSelfDrive": "Self-Drive",
+  "compensationOvertimePay": "OT Pay",
+  "compensationCompLeave": "Comp Leave",
+  "compensationMixed": "Mixed",
+  "unitPiece": "Piece",
+  "unitRoom": "Room",
+  "unitPerson": "Person",
+  "unitDay": "Day",
+  "unitSet": "Set",
+  "entertainmentVip": "VIP"
 }

+ 30 - 5
assets/i18n/zh_CN.json

@@ -196,7 +196,7 @@
   "expenseProject": "费用项目",
   "expenseReason": "报销事由",
   "enterExpenseReason": "请输入报销事由",
-  "selectProject": "选择项目",
+  "selectProject": "选择关联项目",
   "selectSubject": "请选择科目",
   "selectCostCenter": "请选择成本中心",
   "selectBank": "请选择开户银行",
@@ -244,7 +244,7 @@
   "noDetailHint": "暂无明细,点击上方添加",
   "overBudget": "超出预算{amount}",
   "attachmentUpload": "附件上传",
-  "maxAttachment": "最多上传6张图片或PDF文件",
+  "maxAttachment": "最多上传9个附件,支持图片/PDF/Word/Excel(图片≤10MB,文档≤20MB)",
   "attachments": "附件",
   "outingDetail": "外出详情",
   "outingType": "外出类型",
@@ -492,7 +492,6 @@
   "workday": "工作日",
   "weekend": "休息日",
   "holiday": "节假日",
-  "paid": "已付款",
   "businessShort": "商务",
   "exportPlaceholder": "导出功能(占位)",
   "unitItem": "笔",
@@ -571,7 +570,6 @@
   "mockPhotoTaken": "模拟拍照:已拍摄第 {idx} 张照片(含水印:{time} | {lat}, {lng})",
   "announcementExpired": "该公告已于 {date} 过期",
   "returnCarArchivedAt": "已还车归档于 {time}",
-  "selectProject": "选择关联项目",
   "selectedCount": "已选 {count} 人",
   "watermarkHintDynamic": "照片将自动添加水印:服务器授时 + GPS经纬度({lat}°N, {lng}°E)",
   "tdOpen": "开",
@@ -632,6 +630,10 @@
   "withdrawConfirm": "确认撤回",
   "withdrawConfirmTip": "确认撤回该申请吗?撤回后审批流程将终止。",
   "noMoreData": "没有更多了",
+  "hintTravelFields": "请填写差旅专用字段(行程日期、交通方式等)",
+  "hintEntertainmentFields": "请填写招待专用字段(招待对象、层级、人数等)",
+  "hintMeetingFields": "请填写会议专用字段(会议日期、地点等)",
+  "companionNotExceedGuest": "内部陪同人数不能大于外部招待人数",
   "scopeMyApplications": "我的发起",
   "scopeSubordinates": "下属审批",
   "searchExpense": "搜索报销单号或申请人",
@@ -646,5 +648,28 @@
   "filterExpenseEntertainment": "业务招待费",
   "filterExpenseOffice": "办公费",
   "filterExpenseMeeting": "会议费",
-  "filterCritical": "特急"
+  "filterCritical": "特急",
+  "costCategoryTransport": "交通费",
+  "costCategoryHotel": "住宿费",
+  "costCategoryOfficeSupplies": "办公用品",
+  "costCategoryMeals": "餐饮费",
+  "costCategoryMaterials": "材料费",
+  "costCategoryService": "服务费",
+  "costCategoryOther": "其他",
+  "expenseTypeProcurement": "日常采购",
+  "expenseTypeActivity": "活动经费",
+  "expenseTypeTraining": "培训费",
+  "transportPlane": "飞机",
+  "transportHighSpeedRail": "高铁/动车",
+  "transportTrain": "火车(普速)",
+  "transportSelfDrive": "自驾",
+  "compensationOvertimePay": "加班费",
+  "compensationCompLeave": "调休",
+  "compensationMixed": "混合",
+  "unitPiece": "张",
+  "unitRoom": "间",
+  "unitPerson": "人",
+  "unitDay": "天",
+  "unitSet": "套",
+  "entertainmentVip": "VIP"
 }

+ 29 - 2
assets/i18n/zh_TW.json

@@ -407,7 +407,7 @@
   "noDetailHint": "暂无明细,点击上方添加",
   "overBudget": "超出預算{amount}",
   "attachmentUpload": "附件上傳",
-  "maxAttachment": "最多上傳6张图片或PDF文件",
+  "maxAttachment": "最多上傳9個附件,支援圖片/PDF/Word/Excel(圖片≤10MB,文檔≤20MB)",
   "attachments": "附件",
   "outingDetail": "外出詳情",
   "outingType": "外出类型",
@@ -644,5 +644,32 @@
   "licensePlate": "車牌號",
   "vehiclePurpose": "用車目的",
   "addExpenseDetailFirst": "請在明细中添加費用項",
-  "submitConfirmContent": "提交后里程和費用不可再修改,是否继续?"
+  "submitConfirmContent": "提交后里程和費用不可再修改,是否继续?",
+  "costCategoryTransport": "交通费",
+  "costCategoryHotel": "住宿费",
+  "costCategoryOfficeSupplies": "办公用品",
+  "costCategoryMeals": "餐饮费",
+  "costCategoryMaterials": "材料费",
+  "costCategoryService": "服务费",
+  "costCategoryOther": "其他",
+  "expenseTypeProcurement": "日常采购",
+  "expenseTypeActivity": "活动经费",
+  "expenseTypeTraining": "培训费",
+  "transportPlane": "飞机",
+  "transportHighSpeedRail": "高铁/动车",
+  "transportTrain": "火车(普速)",
+  "transportSelfDrive": "自驾",
+  "compensationOvertimePay": "加班费",
+  "compensationCompLeave": "调休",
+  "compensationMixed": "混合",
+  "unitPiece": "张",
+  "unitRoom": "间",
+  "unitPerson": "人",
+  "unitDay": "天",
+  "unitSet": "套",
+  "companionNotExceedGuest": "內部陪同人數不能大於外部招待人數",
+  "hintTravelFields": "請填寫差旅專用欄位(行程日期、交通方式等)",
+  "hintEntertainmentFields": "請填寫招待專用欄位(招待對象、層級、人數等)",
+  "hintMeetingFields": "請填寫會議專用欄位(會議日期、地點等)",
+  "entertainmentVip": "VIP"
 }

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

@@ -267,7 +267,7 @@ OA 独立权限系统的权限点定义表,存储所有可用权限编码及
 | **IsDeleted** | BIT | ✅ | DEFAULT 0 |
 
 **应用层约束**(前端 + .NET 校验):
-- expense_apply:≤9 张,图片 ≤10MB / PDF ≤20MB
+- expense_apply:≤9 个,图片 ≤10MB / PDF/Word/Excel ≤20MB
 - expense:≤9 张,图片 ≤10MB
 - outing_log:1~9 张,仅支持 image 类型,≤10MB
 - announcement:≤5 个,PDF/图片/Word/Excel ≤20MB

+ 145 - 0
lib/core/constants/enums.dart

@@ -0,0 +1,145 @@
+/// OA 模块数据库枚举常量。
+///
+/// PRD/Database §6 枚举取值统一维护点。
+/// 枚举值(value)与数据库 VARCHAR 存储值一致,
+/// labelKey 为 i18n 键,前端展示通过 `l10n.get(labelKey)` 获取。
+class EnumEntry {
+  final String value;
+  final String labelKey;
+  const EnumEntry(this.value, this.labelKey);
+}
+
+// ═══════════════════════════════════════════
+//  §6.1 单据审批状态
+// ═══════════════════════════════════════════
+class ApprovalStatus {
+  ApprovalStatus._();
+  static const draft = EnumEntry('draft', 'draft');
+  static const pending = EnumEntry('pending', 'statusPending');
+  static const approved = EnumEntry('approved', 'statusApproved');
+  static const rejected = EnumEntry('rejected', 'statusRejected');
+  static const withdrawn = EnumEntry('withdrawn', 'statusWithdrawn');
+  static const completed = EnumEntry('completed', 'completed');
+  static const returned = EnumEntry('returned', 'returned');
+  static const values = [draft, pending, approved, rejected, withdrawn];
+}
+
+// ═══════════════════════════════════════════
+//  §6.2 付款状态
+// ═══════════════════════════════════════════
+class PaymentStatus {
+  PaymentStatus._();
+  static const unpaid = EnumEntry('unpaid', 'statusWaitPay');
+  static const paid = EnumEntry('paid', 'paid');
+  static const values = [unpaid, paid];
+}
+
+// ═══════════════════════════════════════════
+//  §6.3 费用大类(ExpenseTypes)
+// ═══════════════════════════════════════════
+class ExpenseType {
+  ExpenseType._();
+  static const travel = EnumEntry('travel', 'filterExpenseTravel');
+  static const entertainment = EnumEntry(
+    'entertainment',
+    'filterExpenseEntertainment',
+  );
+  static const procurement = EnumEntry('procurement', 'expenseTypeProcurement');
+  static const activity = EnumEntry('activity', 'expenseTypeActivity');
+  static const office = EnumEntry('office', 'filterExpenseOffice');
+  static const meeting = EnumEntry('meeting', 'filterExpenseMeeting');
+  static const training = EnumEntry('training', 'expenseTypeTraining');
+  static const values = [
+    travel,
+    entertainment,
+    procurement,
+    activity,
+    office,
+    meeting,
+    training,
+  ];
+}
+
+// ═══════════════════════════════════════════
+//  §6.4 紧急程度
+// ═══════════════════════════════════════════
+class Urgency {
+  Urgency._();
+  static const normal = EnumEntry('normal', 'normal');
+  static const urgent = EnumEntry('urgent', 'urgent');
+  static const critical = EnumEntry('critical', 'filterCritical');
+  static const values = [normal, urgent, critical];
+}
+
+// ═══════════════════════════════════════════
+//  §6.5 交通工具
+// ═══════════════════════════════════════════
+class TransportType {
+  TransportType._();
+  static const plane = EnumEntry('plane', 'transportPlane');
+  static const highSpeedRail = EnumEntry(
+    'high_speed_rail',
+    'transportHighSpeedRail',
+  );
+  static const train = EnumEntry('train', 'transportTrain');
+  static const selfDrive = EnumEntry('self_drive', 'transportSelfDrive');
+  static const values = [plane, highSpeedRail, train, selfDrive];
+}
+
+// ═══════════════════════════════════════════
+//  §6.6 招待层级
+// ═══════════════════════════════════════════
+class EntertainmentLevel {
+  EntertainmentLevel._();
+  static const normal = EnumEntry('normal', 'normal');
+  static const important = EnumEntry('important', 'important');
+  static const vip = EnumEntry('vip', 'entertainmentVip');
+  static const values = [normal, important, vip];
+}
+
+// ═══════════════════════════════════════════
+//  §6.9 加班类型
+// ═══════════════════════════════════════════
+class OtType {
+  OtType._();
+  static const workday = EnumEntry('workday', 'workdayOvertime');
+  static const weekend = EnumEntry('weekend', 'weekendOvertime');
+  static const holiday = EnumEntry('holiday', 'holidayOvertime');
+  static const values = [workday, weekend, holiday];
+}
+
+// ═══════════════════════════════════════════
+//  §6.10 补偿方式
+// ═══════════════════════════════════════════
+class CompensationType {
+  CompensationType._();
+  static const overtimePay = EnumEntry(
+    'overtime_pay',
+    'compensationOvertimePay',
+  );
+  static const compLeave = EnumEntry('comp_leave', 'compensationCompLeave');
+  static const mixed = EnumEntry('mixed', 'compensationMixed');
+  static const values = [overtimePay, compLeave, mixed];
+}
+
+// ═══════════════════════════════════════════
+//  §6.11 用车目的
+// ═══════════════════════════════════════════
+class VehiclePurpose {
+  VehiclePurpose._();
+  static const reception = EnumEntry('reception', 'customerReception');
+  static const business = EnumEntry('business', 'businessTrip');
+  static const official = EnumEntry('official', 'official');
+  static const values = [reception, business, official];
+}
+
+// ═══════════════════════════════════════════
+//  §6.14 公告分类
+// ═══════════════════════════════════════════
+class AnnouncementType {
+  AnnouncementType._();
+  static const notice = EnumEntry('notice', 'noticeAnnouncement');
+  static const policy = EnumEntry('policy', 'hrPolicy');
+  static const activity = EnumEntry('activity', 'holidayActivity');
+  static const values = [notice, policy, activity];
+}

+ 85 - 0
lib/core/data/mock_api_data.dart

@@ -0,0 +1,85 @@
+/// Mock ERP API 数据。
+///
+/// 模拟 .NET → ERP 适配器接口返回的数据结构。
+/// 后续接入真实 API 时,替换对应的 provider 实现即可,
+/// 页面逻辑无需修改。
+// ═══════════════════════════════════════════
+//  项目(ProjectService)
+// ═══════════════════════════════════════════
+class Project {
+  final int id;
+  final String name;
+  const Project({required this.id, required this.name});
+}
+
+const mockProjects = [
+  Project(id: 100, name: '华东市场拓展'),
+  Project(id: 101, name: 'ERP系统升级'),
+  Project(id: 102, name: '新产品研发'),
+  Project(id: 103, name: '华南渠道建设'),
+];
+
+// ═══════════════════════════════════════════
+//  预算科目(SubjectService)
+// ═══════════════════════════════════════════
+class BudgetSubject {
+  final int id;
+  final String name;
+  const BudgetSubject({required this.id, required this.name});
+}
+
+const mockBudgetSubjects = [
+  BudgetSubject(id: 5, name: '差旅费'),
+  BudgetSubject(id: 6, name: '招待费'),
+  BudgetSubject(id: 7, name: '办公费'),
+  BudgetSubject(id: 8, name: '培训费'),
+];
+
+/// 可用预算余额查询
+double getMockBudget(int projectId, int subjectId) {
+  if (subjectId == 5) return 50000.00;
+  if (subjectId == 6) return 30000.00;
+  if (subjectId == 7) return 15000.00;
+  if (subjectId == 8) return 20000.00;
+  return 0;
+}
+
+// ═══════════════════════════════════════════
+//  费用类别字典(SysCostCategory)
+// ═══════════════════════════════════════════
+class CostCategory {
+  final String code;
+  final String nameKey;
+  const CostCategory({required this.code, required this.nameKey});
+}
+
+const mockCostCategories = [
+  CostCategory(code: 'transport', nameKey: 'costCategoryTransport'),
+  CostCategory(code: 'hotel', nameKey: 'costCategoryHotel'),
+  CostCategory(code: 'office_supplies', nameKey: 'costCategoryOfficeSupplies'),
+  CostCategory(code: 'meals', nameKey: 'costCategoryMeals'),
+  CostCategory(code: 'materials', nameKey: 'costCategoryMaterials'),
+  CostCategory(code: 'service', nameKey: 'costCategoryService'),
+  CostCategory(code: 'other', nameKey: 'costCategoryOther'),
+];
+
+/// 费用大类 → 可选明细类别 code 列表
+const expenseTypeCategories = <String, List<String>>{
+  'travel': ['transport', 'hotel', 'meals'],
+  'entertainment': ['meals', 'service', 'other'],
+  'procurement': ['office_supplies', 'materials', 'other'],
+  'activity': ['meals', 'materials', 'service', 'other'],
+  'office': ['office_supplies', 'other'],
+  'meeting': ['meals', 'service', 'other'],
+  'training': ['materials', 'service', 'other'],
+};
+
+/// 计量单位(i18n key)
+const unitOptions = [
+  'unitPiece',
+  'unitRoom',
+  'unitPerson',
+  'unitDay',
+  'unitSet',
+  'unitItem',
+];

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 614 - 368
lib/features/expense_application/expense_application_apply_page.dart


+ 26 - 10
lib/shared/widgets/form_field_row.dart

@@ -2,15 +2,16 @@ import '../../core/theme/app_colors.dart';
 import 'package:flutter/material.dart';
 import '../../core/theme/app_colors_extension.dart';
 
-/// Pencil Component/FormField — 表单字段行
+/// 表单字段行,左侧标签 + 右侧值 + 可选箭头。
 ///
-/// 左侧标签(14号/secondary) + 右侧值(14号/可placeholder色) + 可选箭头图标
+/// [required] 为 true 时标签前显示红色星号。
 class FormFieldRow extends StatelessWidget {
   final String label;
   final String? value;
   final String? hint;
   final bool showArrow;
   final bool readOnly;
+  final bool required;
   final VoidCallback? onTap;
 
   const FormFieldRow({
@@ -20,6 +21,7 @@ class FormFieldRow extends StatelessWidget {
     this.hint,
     this.showArrow = true,
     this.readOnly = false,
+    this.required = false,
     this.onTap,
   });
 
@@ -29,17 +31,31 @@ class FormFieldRow extends StatelessWidget {
     final hasValue = value != null && value!.isNotEmpty;
     return GestureDetector(
       onTap: onTap,
+      behavior: HitTestBehavior.opaque,
       child: Container(
-        height: 44,
-        padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
+        padding: EdgeInsets.zero,
         child: Row(
           mainAxisAlignment: MainAxisAlignment.spaceBetween,
           children: [
-            Text(
-              label,
-              style: TextStyle(
-                fontSize: AppFontSizes.body,
-                color: colors.textSecondary,
+            Text.rich(
+              TextSpan(
+                children: [
+                  TextSpan(
+                    text: label,
+                    style: TextStyle(
+                      fontSize: AppFontSizes.subtitle,
+                      color: colors.textSecondary,
+                    ),
+                  ),
+                  if (required)
+                    TextSpan(
+                      text: ' *',
+                      style: TextStyle(
+                        fontSize: AppFontSizes.subtitle,
+                        color: colors.danger,
+                      ),
+                    ),
+                ],
               ),
             ),
             Row(
@@ -48,7 +64,7 @@ class FormFieldRow extends StatelessWidget {
                 Text(
                   hasValue ? value! : (hint ?? '请选择或填写'),
                   style: TextStyle(
-                    fontSize: AppFontSizes.body,
+                    fontSize: AppFontSizes.subtitle,
                     color: hasValue
                         ? (readOnly ? colors.textPrimary : colors.textPrimary)
                         : colors.textPlaceholder,

+ 18 - 7
lib/shared/widgets/form_section.dart

@@ -7,6 +7,7 @@ import '../../core/theme/app_colors_extension.dart';
 /// 标题行(16号/600字重) + 右侧可选操作按钮(带plus图标) + 内容区
 class FormSection extends StatelessWidget {
   final String title;
+  final IconData? leadingIcon;
   final String? actionText;
   final IconData? actionIcon;
   final VoidCallback? onActionTap;
@@ -16,6 +17,7 @@ class FormSection extends StatelessWidget {
   const FormSection({
     super.key,
     required this.title,
+    this.leadingIcon,
     this.actionText,
     this.actionIcon,
     this.onActionTap,
@@ -38,13 +40,22 @@ class FormSection extends StatelessWidget {
           Row(
             mainAxisAlignment: MainAxisAlignment.spaceBetween,
             children: [
-              Text(
-                title,
-                style: TextStyle(
-                  fontSize: AppFontSizes.subtitle,
-                  fontWeight: FontWeight.w600,
-                  color: colors.textPrimary,
-                ),
+              Row(
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  if (leadingIcon != null) ...[
+                    Icon(leadingIcon, size: 18, color: colors.textPrimary),
+                    const SizedBox(width: 8),
+                  ],
+                  Text(
+                    title,
+                    style: TextStyle(
+                      fontSize: AppFontSizes.title,
+                      fontWeight: FontWeight.w600,
+                      color: colors.textPrimary,
+                    ),
+                  ),
+                ],
               ),
               if (showAction)
                 GestureDetector(