diff --git a/CLAUDE.md b/CLAUDE.md
index 35cb2b9..5cd9069 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -251,6 +251,33 @@ const handleSubmit = async () => {
- `frontend/src/pages/business-trip/select-cc.tsx`
- `frontend/src/pages/business-trip/apply.tsx`
+#### 问题3:导航时草稿保存不及时
+**现象:** 用户输入内容后快速点击选择人员,返回时输入的内容丢失
+
+**原因:** React 的 useEffect 是异步执行的,用户快速导航时,useEffect 可能还没来得及保存到 sessionStorage
+
+**解决方案:** 在导航处理函数中立即保存草稿
+
+```tsx
+const handleSelectApprovers = () => {
+ // 导航前立即保存草稿
+ const draftData = { reason, itineraries }
+ sessionStorage.setItem('businessTripDraft', JSON.stringify(draftData))
+ navigate('/business-trip/apply/select-approver', { state: { selected: approvers, type: 'approver' } })
+}
+
+const handleSelectCc = () => {
+ // 导航前立即保存草稿
+ const draftData = { reason, itineraries }
+ sessionStorage.setItem('businessTripDraft', JSON.stringify(draftData))
+ navigate('/business-trip/apply/select-cc', { state: { selected: ccUsers, type: 'cc' } })
+}
+```
+
+**关键点:**
+- ✅ 导航前同步保存草稿数据,不依赖 useEffect
+- ✅ 保留 useEffect 自动保存作为备份机制(防止用户不导航就离开页面)
+
### Ant Design Mobile DatePicker 点击无反应
**问题:** DatePicker 组件使用 children render prop 方式时,点击无法弹出选择框
diff --git a/backend/app/Bootstrap/Database.php b/backend/app/Bootstrap/Database.php
deleted file mode 100644
index 268aaed..0000000
--- a/backend/app/Bootstrap/Database.php
+++ /dev/null
@@ -1,15 +0,0 @@
-header('Authorization');
- $token = str_replace('Bearer ', '', $token);
-
- // 如果没有 token,返回 401
- if (!$token) {
- return json([
- 'code' => 401,
- 'message' => '未登录',
- 'data' => null,
- ]);
- }
-
- // 验证 token
- $payload = JwtUtil::decode($token);
- if (!$payload) {
- return json([
- 'code' => 401,
- 'message' => '登录已过期',
- 'data' => null,
- ]);
- }
-
- // 将用户ID注入到请求中
- $request->userId = (int) ($payload['user_id'] ?? 0);
- $request->companyId = (int) ($payload['company_id'] ?? 0);
-
- return $next($request);
- }
-}
diff --git a/backend/app/Controller/ApprovalController.php b/backend/app/controller/ApprovalController.php
similarity index 97%
rename from backend/app/Controller/ApprovalController.php
rename to backend/app/controller/ApprovalController.php
index e55858e..ea88c45 100644
--- a/backend/app/Controller/ApprovalController.php
+++ b/backend/app/controller/ApprovalController.php
@@ -1,8 +1,8 @@
getUserId($request);
+ $id = (int) $this->getRouteParam($request, 'id');
+ if (!$id) {
+ return $this->error('参数错误');
+ }
+
+ try {
+ $this->service->deleteDraft($userId, $id);
+ return $this->success(null, '删除成功');
+ } catch (\Exception $e) {
+ return $this->error($e->getMessage());
+ }
+ }
+
+ /**
+ * 审批操作
+ */
+ public function approve(Request $request): Response
+ {
+ $userId = $this->getUserId($request);
+ $id = (int) $this->getRouteParam($request, 'id');
+ if (!$id) {
+ return $this->error('参数错误');
+ }
+
+ $data = $request->post();
+ $data = $data ?? [];
+ $action = $data['action'] ?? 'approve'; // approve 或 reject
+ $comment = $data['comment'] ?? '';
+
+ try {
+ $result = $this->service->approve($userId, $id, $action, $comment);
+ return $this->success($result, $action === 'approve' ? '已同意' : '已拒绝');
+ } catch (\Exception $e) {
+ return $this->error($e->getMessage());
+ }
+ }
+
/**
* 取消出差申请
*/
diff --git a/backend/app/Controller/UserController.php b/backend/app/controller/UserController.php
similarity index 97%
rename from backend/app/Controller/UserController.php
rename to backend/app/controller/UserController.php
index 8de999c..1893380 100644
--- a/backend/app/Controller/UserController.php
+++ b/backend/app/controller/UserController.php
@@ -1,8 +1,8 @@
header('Authorization');
- $token = str_replace('Bearer ', '', $token);
- if (!$token || !JwtUtil::verify($token)) {
+ // 移除 "Bearer " 前缀(如果存在)
+ if ($token && str_starts_with($token, 'Bearer ')) {
+ $token = substr($token, 7);
+ }
+
+ // 如果没有 token,返回 401
+ if (!$token) {
return json([
'code' => 401,
- 'message' => 'Unauthorized',
+ 'message' => '未登录',
'data' => null,
- ], 401);
+ ]);
}
- // 解析 Token 并设置用户信息到请求
+ // 验证 token
$payload = JwtUtil::decode($token);
- if ($payload) {
- $request->userId = $payload['user_id'] ?? null;
- $request->companyId = $payload['company_id'] ?? null;
+ if (!$payload) {
+ return json([
+ 'code' => 401,
+ 'message' => '登录已过期',
+ 'data' => null,
+ ]);
}
- return $handler($request);
+ // 将用户ID注入到请求中
+ $request->userId = (int) ($payload['user_id'] ?? 0);
+ $request->companyId = (int) ($payload['company_id'] ?? 0);
+
+ return $next($request);
}
}
diff --git a/backend/app/Model/ApprovalFlow.php b/backend/app/model/ApprovalFlow.php
similarity index 97%
rename from backend/app/Model/ApprovalFlow.php
rename to backend/app/model/ApprovalFlow.php
index 9150784..ad79961 100644
--- a/backend/app/Model/ApprovalFlow.php
+++ b/backend/app/model/ApprovalFlow.php
@@ -1,6 +1,6 @@
where('user_id', $userId);
break;
case 'pending':
- // 获取待审批的申请
+ // 待我审批:需要查询当前用户是审批人且还未审批的申请
$pendingOrderIds = ApprovalOrder::where('status', ApprovalOrder::STATUS_PENDING)
->where('business_type', 'business_trip')
- ->pluck('business_id')
+ ->pluck('id')
->toArray();
- $query->whereIn('id', $pendingOrderIds);
+
+ if (empty($pendingOrderIds)) {
+ $query->whereRaw('1 = 0'); // 返回空结果
+ } else {
+ // 获取这些审批单中当前用户待审批的,排除已审批的
+ $approvedOrderIds = ApprovalRecord::whereIn('order_id', $pendingOrderIds)
+ ->where('approver_id', $userId)
+ ->whereIn('action', ['approve', 'reject'])
+ ->pluck('order_id')
+ ->toArray();
+
+ $pendingOrderIds = array_diff($pendingOrderIds, $approvedOrderIds);
+
+ $query->whereIn('approval_order_id', $pendingOrderIds);
+ }
+ break;
+ case 'approved':
+ // 我已审批:当前用户已经审批过的申请
+ $approvedOrderIds = ApprovalRecord::where('approver_id', $userId)
+ ->whereIn('action', ['approve', 'reject'])
+ ->pluck('order_id')
+ ->toArray();
+
+ if (empty($approvedOrderIds)) {
+ $query->whereRaw('1 = 0'); // 返回空结果
+ } else {
+ $query->whereIn('approval_order_id', $approvedOrderIds);
+ }
break;
}
@@ -194,10 +222,32 @@ class BusinessTripService
/**
* 获取出差申请详情
*/
- public function getDetail(int $id): BusinessTrip
+ public function getDetail(int $id): array
{
- return BusinessTrip::with(['user', 'itineraries'])
+ $businessTrip = BusinessTrip::with(['user', 'itineraries'])
->findOrFail($id);
+
+ $result = $businessTrip->toArray();
+
+ // 获取审批单信息
+ if ($businessTrip->approval_order_id) {
+ $approvalOrder = ApprovalOrder::with(['records.approver'])
+ ->find($businessTrip->approval_order_id);
+
+ if ($approvalOrder) {
+ $result['approval_order'] = $approvalOrder->toArray();
+
+ // 获取审批人和抄送人信息
+ $formData = $approvalOrder->form_data ?? [];
+ $result['approvers'] = $formData['approvers'] ?? [];
+ $result['cc_users'] = $formData['cc_users'] ?? [];
+
+ // 获取审批记录
+ $result['approval_records'] = $approvalOrder->records->toArray();
+ }
+ }
+
+ return $result;
}
/**
@@ -300,4 +350,113 @@ class BusinessTripService
return $businessTrip->fresh();
}
+
+ /**
+ * 删除草稿
+ */
+ public function deleteDraft(int $userId, int $id): bool
+ {
+ $businessTrip = BusinessTrip::findOrFail($id);
+
+ if ($businessTrip->user_id !== $userId) {
+ throw new \Exception('无权操作此申请');
+ }
+
+ if ($businessTrip->status !== BusinessTrip::STATUS_DRAFT) {
+ throw new \Exception('只能删除草稿');
+ }
+
+ return $this->db->transaction(function () use ($businessTrip) {
+ // 删除行程明细
+ BusinessTripItinerary::where('business_trip_id', $businessTrip->id)->delete();
+
+ // 删除申请
+ return $businessTrip->delete();
+ });
+ }
+
+ /**
+ * 审批操作
+ */
+ public function approve(int $userId, int $businessTripId, string $action, string $comment = ''): BusinessTrip
+ {
+ return $this->db->transaction(function () use ($userId, $businessTripId, $action, $comment) {
+ $businessTrip = BusinessTrip::with(['itineraries', 'approvalOrder'])->findOrFail($businessTripId);
+
+ if (!$businessTrip->approval_order_id) {
+ throw new \Exception('未找到审批单');
+ }
+
+ $approvalOrder = $businessTrip->approvalOrder;
+
+ if ($approvalOrder->status !== ApprovalOrder::STATUS_PENDING) {
+ throw new \Exception('当前状态无法审批');
+ }
+
+ // 检查用户是否是当前审批人
+ $currentNodes = $approvalOrder->current_nodes ?? [];
+ $isApprover = false;
+
+ foreach ($currentNodes as $nodeId) {
+ $node = ApprovalNode::where('order_id', $approvalOrder->id)
+ ->where('id', $nodeId)
+ ->first();
+
+ if (!$node) {
+ continue;
+ }
+
+ // 检查是否是单人审批节点
+ if ($node->node_type === 'single_approve') {
+ $approvers = $node->approvers ?? [];
+ if (in_array($userId, $approvers)) {
+ $isApprover = true;
+ break;
+ }
+ }
+ }
+
+ if (!$isApprover) {
+ throw new \Exception('您不是当前审批人');
+ }
+
+ // 检查是否已经审批过
+ $existingRecord = ApprovalRecord::where('order_id', $approvalOrder->id)
+ ->where('approver_id', $userId)
+ ->whereIn('action', ['approve', 'reject'])
+ ->first();
+
+ if ($existingRecord) {
+ throw new \Exception('您已经审批过了');
+ }
+
+ // 记录审批操作
+ ApprovalRecord::create([
+ 'order_id' => $approvalOrder->id,
+ 'node_id' => $currentNodes[0] ?? 1,
+ 'node_type' => 'single_approve',
+ 'approver_id' => $userId,
+ 'action' => $action,
+ 'comment' => $comment,
+ ]);
+
+ // 更新审批单状态
+ if ($action === 'reject') {
+ // 拒绝:直接结束流程
+ $approvalOrder->status = ApprovalOrder::STATUS_REJECTED;
+ $approvalOrder->current_nodes = [];
+ $businessTrip->status = BusinessTrip::STATUS_REJECTED;
+ } else {
+ // 同意:检查是否还有后续节点(简化版:直接通过)
+ $approvalOrder->status = ApprovalOrder::STATUS_APPROVED;
+ $approvalOrder->current_nodes = [];
+ $businessTrip->status = BusinessTrip::STATUS_APPROVED;
+ }
+
+ $approvalOrder->save();
+ $businessTrip->save();
+
+ return $businessTrip->fresh(['itineraries', 'approvalOrder']);
+ });
+ }
}
diff --git a/backend/app/Service/FeishuService.php b/backend/app/service/FeishuService.php
similarity index 98%
rename from backend/app/Service/FeishuService.php
rename to backend/app/service/FeishuService.php
index 3b3d92a..ea6b371 100644
--- a/backend/app/Service/FeishuService.php
+++ b/backend/app/service/FeishuService.php
@@ -1,11 +1,11 @@
middleware([\app\middleware\AuthMiddleware::class]);
+
+// 审批路由(需要认证)
+Route::group('/api/v1/approval', function () {
+ Route::get('/list', [app\controller\ApprovalController::class, 'list']);
+ Route::get('/detail/{id}', [app\controller\ApprovalController::class, 'detail']);
+ Route::post('/approve/{id}', [app\controller\ApprovalController::class, 'approve']);
+ Route::post('/reject/{id}', [app\controller\ApprovalController::class, 'reject']);
+ Route::post('/cancel/{id}', [app\controller\ApprovalController::class, 'cancel']);
+})->middleware([\app\middleware\AuthMiddleware::class]);
+
+// 出差审批路由(需要认证)
+Route::group('/api/v1/business-trip', function () {
+ Route::get('/list', [app\controller\BusinessTripController::class, 'list']);
+ Route::get('/detail/{id}', [app\controller\BusinessTripController::class, 'detail']);
+ Route::post('/create', [app\controller\BusinessTripController::class, 'create']);
+ Route::post('/update/{id}', [app\controller\BusinessTripController::class, 'update']);
+ Route::post('/submit/{id}', [app\controller\BusinessTripController::class, 'submit']);
+ Route::post('/cancel/{id}', [app\controller\BusinessTripController::class, 'cancel']);
+ Route::delete('/{id}', [app\controller\BusinessTripController::class, 'deleteDraft']);
+ Route::post('/approve/{id}', [app\controller\BusinessTripController::class, 'approve']);
+})->middleware([\app\middleware\AuthMiddleware::class]);
+
// 默认路由
Route::fallback(function () {
return json(['code' => 404, 'message' => 'Not Found']);
diff --git a/backend/scripts/import_old_data.php b/backend/scripts/import_old_data.php
index 4cd3b43..87bc841 100644
--- a/backend/scripts/import_old_data.php
+++ b/backend/scripts/import_old_data.php
@@ -11,7 +11,7 @@
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../app/Util/Db.php';
-use App\Util\Db;
+use app\util\Db;
class DataImporter
{
diff --git a/frontend/dev-tools.html b/frontend/dev-tools.html
new file mode 100644
index 0000000..c150370
--- /dev/null
+++ b/frontend/dev-tools.html
@@ -0,0 +1,74 @@
+
+
+
+
+ Token 设置工具
+
+
+
+ 🔧 开发调试工具
+
+
+
步骤 1:复制 Token
+
+
+
+
+
+
+
步骤 2:设置 Token
+
+ - 按 F12 打开开发者工具
+ - 切换到 Console 标签
+ - 粘贴并执行:
localStorage.setItem('token', 刚才复制的token)
+
+
+
+
+
步骤 3:验证
+
+
+
+
+
+
+
diff --git a/frontend/set-token.html b/frontend/set-token.html
new file mode 100644
index 0000000..1c9b334
--- /dev/null
+++ b/frontend/set-token.html
@@ -0,0 +1,101 @@
+
+
+
+
+
+ 设置 Token
+
+
+
+
+
🎯 设置登录 Token
+
+ 点击下方按钮设置登录凭证,然后关闭此页面并刷新主页面。
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/layout/MobileLayout.tsx b/frontend/src/components/layout/MobileLayout.tsx
index 0dbcd01..9d6de77 100644
--- a/frontend/src/components/layout/MobileLayout.tsx
+++ b/frontend/src/components/layout/MobileLayout.tsx
@@ -10,7 +10,7 @@ export default function MobileLayout() {
const mainRef = useRef(null)
const showHeader = location.pathname !== '/login'
- const showTabBar = !location.pathname.includes('/business-trip/')
+ const showTabBar = false // 暂时隐藏底部 Tab
// 路由变化时滚动到顶部
useEffect(() => {
diff --git a/frontend/src/components/layout/TabBar.tsx b/frontend/src/components/layout/TabBar.tsx
index e00a810..b64f82c 100644
--- a/frontend/src/components/layout/TabBar.tsx
+++ b/frontend/src/components/layout/TabBar.tsx
@@ -1,5 +1,5 @@
import { useNavigate, useLocation } from 'react-router-dom'
-import { AppOutline, AppstoreOutline, UserOutline } from 'antd-mobile-icons'
+import { AppOutline, AppstoreOutline } from 'antd-mobile-icons'
import { useTranslation } from 'react-i18next'
import styles from './TabBar.module.css'
@@ -13,7 +13,6 @@ interface TabItem {
const tabs: TabItem[] = [
{ key: 'home', path: '/dashboard', icon: , labelKey: 'tab.home' },
{ key: 'apps', path: '/apps', icon: , labelKey: 'tab.apps' },
- { key: 'profile', path: '/profile', icon: , labelKey: 'tab.profile' },
]
export default function TabBar() {
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 736250f..3b66a8f 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -68,3 +68,55 @@ select {
padding-bottom: env(safe-area-inset-bottom);
}
}
+
+/* Dialog 组件层级修复 - 确保在所有固定定位元素之上 */
+/* Dialog 容器 - 最高优先级 */
+.adm-dialog {
+ position: fixed !important;
+ top: 0 !important;
+ left: 0 !important;
+ width: 100% !important;
+ height: 100% !important;
+ z-index: 99999 !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+}
+
+/* Dialog 遮罩 - 确保在容器内但有正确的层级 */
+.adm-dialog .adm-dialog-mask {
+ position: absolute !important;
+ top: 0 !important;
+ left: 0 !important;
+ width: 100% !important;
+ height: 100% !important;
+ z-index: 1 !important;
+ background-color: rgba(0, 0, 0, 0.45) !important;
+}
+
+/* Dialog 内容容器 - 在遮罩之上 */
+.adm-dialog .adm-dialog-content {
+ position: relative !important;
+ z-index: 2 !important;
+}
+
+/* Dialog 内部元素 - 确保在内容容器之上 */
+.adm-dialog .adm-dialog-body,
+.adm-dialog .adm-dialog-header,
+.adm-dialog .adm-dialog-footer,
+.adm-dialog .adm-button {
+ position: relative !important;
+ z-index: 3 !important;
+}
+
+/* 处理 antd-mobile 5.x 的实际 DOM 结构 */
+/* antd-mobile 5.x 的 Dialog 可能直接渲染到 body 下 */
+body > .adm-dialog {
+ z-index: 99999 !important;
+}
+
+/* 确保 mask 不会覆盖 dialog 内容 */
+.adm-dialog-mask ~ .adm-dialog-content {
+ position: relative !important;
+ z-index: 10 !important;
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 8f63160..7d84477 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -8,6 +8,7 @@ import 'antd-mobile/es/global'
import 'antd-mobile/es/components/popup/popup.css'
import 'antd-mobile/es/components/mask/mask.css'
import 'antd-mobile/es/components/date-picker/date-picker.css'
+import 'antd-mobile/es/components/dialog/dialog.css'
import router from './router'
import { initRem } from './utils/rem'
import './i18n'
diff --git a/frontend/src/pages/business-trip/apply.module.css b/frontend/src/pages/business-trip/apply.module.css
index e9dc709..8b80fd5 100644
--- a/frontend/src/pages/business-trip/apply.module.css
+++ b/frontend/src/pages/business-trip/apply.module.css
@@ -155,10 +155,13 @@
z-index: 10000 !important;
}
-:global(.adm-mask) {
+/* 只影响 Popup 的 mask,不影响 Dialog */
+:global(.adm-popup .adm-mask) {
z-index: 9999 !important;
}
+/* Dialog 样式在 index.css 中统一设置 */
+
/* Selector 组件样式覆盖 */
.field :global(.adm-selector) {
flex: 1;
@@ -407,7 +410,7 @@
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
display: flex;
gap: 12px;
- z-index: 9999;
+ z-index: 100;
}
.saveBtn,
diff --git a/frontend/src/pages/business-trip/apply.tsx b/frontend/src/pages/business-trip/apply.tsx
index 170f2b7..eb4ec2f 100644
--- a/frontend/src/pages/business-trip/apply.tsx
+++ b/frontend/src/pages/business-trip/apply.tsx
@@ -1,4 +1,4 @@
-import { useState, useMemo, useEffect } from 'react'
+import { useState, useMemo, useEffect, useRef } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { DatePicker, Input, Toast, Dialog, TextArea, Selector } from 'antd-mobile'
@@ -65,31 +65,118 @@ export default function BusinessTripApply() {
const [approvers, setApprovers] = useState([])
const [ccUsers, setCcUsers] = useState([])
+ // 手动保存草稿的函数
+ const saveDraft = () => {
+ const draftData = {
+ reason,
+ itineraries,
+ }
+ sessionStorage.setItem('businessTripDraft', JSON.stringify(draftData))
+ console.log('💾 Manually saved draft:', { reason, itineraries })
+ }
+
// DatePicker 可见性状态
const [datePickerVisible, setDatePickerVisible] = useState<{
index: number
field: 'startTime' | 'endTime'
} | null>(null)
- // 处理从选择页面返回的数据
+ // 使用 ref 来跟踪是否已经恢复过草稿
+ const hasRestoredRef = useRef(false)
+
+ // 处理从选择页面返回的数据和草稿恢复
useEffect(() => {
- // 检查 sessionStorage 中是否有选择的数据
- const approversData = sessionStorage.getItem('selectedApprovers')
- const ccData = sessionStorage.getItem('selectedCc')
+ const fromSelection = location.state?.fromSelection
- // 从 sessionStorage 恢复状态(组件重新挂载时需要)
- if (approversData) {
- const approvers = JSON.parse(approversData) as ApproverItem[]
- setApprovers(approvers)
- console.log('✅ Restored approvers from sessionStorage:', approvers)
+ console.log('📍 Page loaded/updated:', {
+ fromSelection,
+ path: location.pathname
+ })
+
+ // 恢复草稿数据(每次组件挂载都需要恢复)
+ const draftData = sessionStorage.getItem('businessTripDraft')
+ console.log('📦 Draft data in sessionStorage:', draftData)
+
+ if (draftData) {
+ try {
+ const draft = JSON.parse(draftData)
+ console.log('📄 Parsed draft:', draft)
+
+ // 恢复理由
+ if (draft.reason) {
+ console.log('✏️ Restoring reason:', draft.reason)
+ setReason(draft.reason)
+ }
+
+ // 恢复行程数据
+ if (draft.itineraries && draft.itineraries.length > 0) {
+ const restoredItineraries = draft.itineraries.map((item: any) => ({
+ ...item,
+ startTime: item.startTime ? new Date(item.startTime) : null,
+ endTime: item.endTime ? new Date(item.endTime) : null,
+ }))
+ console.log('🛤️ Restoring itineraries:', restoredItineraries)
+ setItineraries(restoredItineraries)
+ }
+
+ // 有草稿数据,也尝试恢复人员数据
+ if (!fromSelection) {
+ const approversData = sessionStorage.getItem('selectedApprovers')
+ if (approversData) {
+ const approvers = JSON.parse(approversData) as ApproverItem[]
+ setApprovers(approvers)
+ console.log('✅ Restored approvers (with draft):', approvers)
+ }
+
+ const ccData = sessionStorage.getItem('selectedCc')
+ if (ccData) {
+ const cc = JSON.parse(ccData) as ApproverItem[]
+ setCcUsers(cc)
+ console.log('✅ Restored CC (with draft):', cc)
+ }
+ }
+ } catch (error) {
+ console.error('❌ Failed to restore draft:', error)
+ }
+ } else {
+ console.log('⚠️ No draft data found in sessionStorage')
+ // 没有草稿数据,清除旧的人员选择数据(开始新申请)
+ if (!fromSelection) {
+ console.log('🧹 Clearing old selection data (new application)')
+ sessionStorage.removeItem('selectedApprovers')
+ sessionStorage.removeItem('selectedCc')
+ setApprovers([])
+ setCcUsers([])
+ }
}
- if (ccData) {
- const cc = JSON.parse(ccData) as ApproverItem[]
- setCcUsers(cc)
- console.log('✅ Restored CC from sessionStorage:', cc)
+ // 从选择页面返回,恢复所有人员数据
+ if (fromSelection) {
+ console.log('🔄 Returning from selection page')
+
+ const type = location.state?.type
+ console.log('📋 Selection type:', type)
+
+ // 恢复所有人员数据(审批人 + 抄送人)
+ const approversData = sessionStorage.getItem('selectedApprovers')
+ if (approversData) {
+ const approvers = JSON.parse(approversData) as ApproverItem[]
+ setApprovers(approvers)
+ console.log('✅ Restored approvers from sessionStorage:', approvers)
+ } else {
+ setApprovers([])
+ }
+
+ const ccData = sessionStorage.getItem('selectedCc')
+ if (ccData) {
+ const cc = JSON.parse(ccData) as ApproverItem[]
+ setCcUsers(cc)
+ console.log('✅ Restored CC from sessionStorage:', cc)
+ } else {
+ setCcUsers([])
+ }
}
- }, []) // 只在组件挂载时执行一次
+ }, [location.state, location.pathname])
// 计算总时长(累加所有行程的天数)
const totalDuration = useMemo(() => {
@@ -102,7 +189,7 @@ export default function BusinessTripApply() {
}, [itineraries])
const handleAddItinerary = () => {
- setItineraries([...itineraries, {
+ const newItineraries = [...itineraries, {
startTime: null,
endTime: null,
departure: '',
@@ -110,7 +197,9 @@ export default function BusinessTripApply() {
transport: '',
tripType: '' as 'oneway' | 'roundtrip' | '',
remark: ''
- }])
+ }]
+ setItineraries(newItineraries)
+ saveDraft()
}
const handleRemoveItinerary = (index: number) => {
@@ -118,24 +207,54 @@ export default function BusinessTripApply() {
Toast.show('至少保留一条行程')
return
}
- setItineraries(itineraries.filter((_, i) => i !== index))
+ const newItineraries = itineraries.filter((_, i) => i !== index)
+ setItineraries(newItineraries)
+ saveDraft()
}
const handleUpdateItinerary = (index: number, field: keyof ItineraryItem, value: any) => {
const updated = [...itineraries]
updated[index] = { ...updated[index], [field]: value }
setItineraries(updated)
+ saveDraft()
}
const handleSelectApprovers = () => {
+ console.log('🚀 Navigating to select approvers')
+ saveDraft() // 导航前保存草稿
+
+ // 如果草稿中没有实际内容(空理由且没有有效行程),清除旧的人员数据
+ const hasContent = reason.trim() !== '' || itineraries.some(item => item.startTime || item.destination.trim() !== '')
+ if (!hasContent) {
+ sessionStorage.removeItem('selectedApprovers')
+ sessionStorage.removeItem('selectedCc')
+ console.log('🧹 Cleared old selection data (no content)')
+ }
+
navigate('/business-trip/apply/select-approver', { state: { selected: approvers, type: 'approver' } })
}
const handleSelectCc = () => {
+ console.log('🚀 Navigating to select CC')
+ saveDraft() // 导航前保存草稿
+
+ // 如果草稿中没有实际内容(空理由且没有有效行程),清除旧的人员数据
+ const hasContent = reason.trim() !== '' || itineraries.some(item => item.startTime || item.destination.trim() !== '')
+ if (!hasContent) {
+ sessionStorage.removeItem('selectedApprovers')
+ sessionStorage.removeItem('selectedCc')
+ console.log('🧹 Cleared old selection data (no content)')
+ }
+
navigate('/business-trip/apply/select-cc', { state: { selected: ccUsers, type: 'cc' } })
}
const handleSave = async () => {
+ // 移除当前焦点,避免按钮保持高亮状态
+ if (document.activeElement instanceof HTMLElement) {
+ document.activeElement.blur()
+ }
+
try {
// 验证必填字段
if (!reason.trim()) {
@@ -170,6 +289,7 @@ export default function BusinessTripApply() {
// 保存成功后清除 sessionStorage
sessionStorage.removeItem('selectedApprovers')
sessionStorage.removeItem('selectedCc')
+ sessionStorage.removeItem('businessTripDraft')
Toast.show('草稿已保存')
navigate('/business-trip')
@@ -179,6 +299,11 @@ export default function BusinessTripApply() {
}
const handleSubmit = () => {
+ // 移除当前焦点,避免按钮保持高亮状态
+ if (document.activeElement instanceof HTMLElement) {
+ document.activeElement.blur()
+ }
+
if (!reason.trim()) {
Toast.show(t('businessTrip.pleaseEnterReason'))
return
@@ -197,6 +322,8 @@ export default function BusinessTripApply() {
Dialog.confirm({
content: t('businessTrip.confirmSubmit'),
+ confirmText: '确认',
+ cancelText: '取消',
onConfirm: async () => {
try {
// 准备行程数据
@@ -228,6 +355,7 @@ export default function BusinessTripApply() {
// 提交成功后清除 sessionStorage
sessionStorage.removeItem('selectedApprovers')
sessionStorage.removeItem('selectedCc')
+ sessionStorage.removeItem('businessTripDraft')
Toast.show(t('businessTrip.submitSuccess'))
navigate('/business-trip')
@@ -248,7 +376,10 @@ export default function BusinessTripApply() {