fix: 修复页面间状态丢失问题并优化选择页面

- 修复选择审批人后再选择抄送人导致状态丢失的问题
- 使用独立的 sessionStorage key (selectedApprovers, selectedCc)
- 组件每次挂载时从 sessionStorage 恢复状态
- 只在提交/保存成功后清除 sessionStorage
- 优化人员选择页面布局和样式
- 更新 CLAUDE.md 文档记录问题和解决方案

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-25 15:22:12 +08:00
parent 2d39357cc3
commit 09d4cb8054
51 changed files with 2944 additions and 647 deletions
+151 -33
View File
@@ -1,9 +1,10 @@
import { useState, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import { useState, useMemo, useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { DatePicker, Input, Toast, Dialog, TextArea, Selector } from 'antd-mobile'
import { AddOutline, DeleteOutline } from 'antd-mobile-icons'
import dayjs from 'dayjs'
import { businessTripApi } from '../../services/api'
import styles from './apply.module.css'
interface ItineraryItem {
@@ -12,7 +13,7 @@ interface ItineraryItem {
departure: string
destination: string
transport: string
tripType: 'oneway' | 'roundtrip'
tripType: 'oneway' | 'roundtrip' | ''
remark: string
}
@@ -55,14 +56,41 @@ const transportOptions = [
export default function BusinessTripApply() {
const { t } = useTranslation()
const navigate = useNavigate()
const location = useLocation()
const [reason, setReason] = useState('')
const [itineraries, setItineraries] = useState<ItineraryItem[]>([
{ startTime: null, endTime: null, departure: '', destination: '', transport: '', tripType: 'oneway', remark: '' }
{ startTime: null, endTime: null, departure: '', destination: '', transport: '', tripType: '' as 'oneway' | 'roundtrip' | '', remark: '' }
])
const [approvers, setApprovers] = useState<ApproverItem[]>([])
const [ccUsers, setCcUsers] = useState<ApproverItem[]>([])
// DatePicker 可见性状态
const [datePickerVisible, setDatePickerVisible] = useState<{
index: number
field: 'startTime' | 'endTime'
} | null>(null)
// 处理从选择页面返回的数据
useEffect(() => {
// 检查 sessionStorage 中是否有选择的数据
const approversData = sessionStorage.getItem('selectedApprovers')
const ccData = sessionStorage.getItem('selectedCc')
// 从 sessionStorage 恢复状态(组件重新挂载时需要)
if (approversData) {
const approvers = JSON.parse(approversData) as ApproverItem[]
setApprovers(approvers)
console.log('✅ Restored approvers from sessionStorage:', approvers)
}
if (ccData) {
const cc = JSON.parse(ccData) as ApproverItem[]
setCcUsers(cc)
console.log('✅ Restored CC from sessionStorage:', cc)
}
}, []) // 只在组件挂载时执行一次
// 计算总时长(累加所有行程的天数)
const totalDuration = useMemo(() => {
let total = 0
@@ -80,7 +108,7 @@ export default function BusinessTripApply() {
departure: '',
destination: '',
transport: '',
tripType: 'oneway',
tripType: '' as 'oneway' | 'roundtrip' | '',
remark: ''
}])
}
@@ -107,8 +135,47 @@ export default function BusinessTripApply() {
navigate('/business-trip/apply/select-cc', { state: { selected: ccUsers, type: 'cc' } })
}
const handleSave = () => {
Toast.show('草稿已保存')
const handleSave = async () => {
try {
// 验证必填字段
if (!reason.trim()) {
Toast.show(t('businessTrip.pleaseEnterReason'))
return
}
const hasValidItinerary = itineraries.some(item => item.startTime && item.destination.trim())
if (!hasValidItinerary) {
Toast.show('请至少填写一条完整行程')
return
}
// 准备行程数据
const itineraryData = itineraries
.filter(item => item.startTime) // 只保存有开始时间的行程
.map(item => ({
start_time: dayjs(item.startTime).format('YYYY-MM-DD HH:mm:ss'),
end_time: item.endTime ? dayjs(item.endTime).format('YYYY-MM-DD HH:mm:ss') : '',
departure: item.departure || '',
destination: item.destination || '',
transport: item.transport || '',
trip_type: item.tripType || 'oneway',
remark: item.remark || '',
}))
await businessTripApi.create({
reason,
itineraries: itineraryData,
})
// 保存成功后清除 sessionStorage
sessionStorage.removeItem('selectedApprovers')
sessionStorage.removeItem('selectedCc')
Toast.show('草稿已保存')
navigate('/business-trip')
} catch (error: any) {
Toast.show(error.message || '保存失败')
}
}
const handleSubmit = () => {
@@ -123,11 +190,50 @@ export default function BusinessTripApply() {
return
}
if (approvers.length === 0) {
Toast.show('请选择审批人')
return
}
Dialog.confirm({
content: t('businessTrip.confirmSubmit'),
onConfirm: async () => {
Toast.show(t('businessTrip.submitSuccess'))
navigate('/business-trip')
try {
// 准备行程数据
const itineraryData = itineraries
.filter(item => item.startTime) // 只保存有开始时间的行程
.map(item => ({
start_time: dayjs(item.startTime).format('YYYY-MM-DD HH:mm:ss'),
end_time: item.endTime ? dayjs(item.endTime).format('YYYY-MM-DD HH:mm:ss') : '',
departure: item.departure || '',
destination: item.destination || '',
transport: item.transport || '',
trip_type: item.tripType || 'oneway',
remark: item.remark || '',
}))
// 先创建草稿
const result = await businessTripApi.create({
reason,
itineraries: itineraryData,
})
// 再提交
await businessTripApi.submit(
result.id,
approvers.map(a => a.id),
ccUsers.map(u => u.id)
)
// 提交成功后清除 sessionStorage
sessionStorage.removeItem('selectedApprovers')
sessionStorage.removeItem('selectedCc')
Toast.show(t('businessTrip.submitSuccess'))
navigate('/business-trip')
} catch (error: any) {
Toast.show(error.message || '提交失败')
}
}
})
}
@@ -165,7 +271,7 @@ export default function BusinessTripApply() {
<div key={index} className={styles.itineraryItem}>
{itineraries.length > 1 && (
<div className={styles.itineraryHeader}>
<span className={styles.itineraryIndex}>{index + 1}</span>
<span className={styles.itineraryIndex}>行程{index + 1}</span>
<button
className={styles.removeBtn}
onClick={() => handleRemoveItinerary(index)}
@@ -177,33 +283,43 @@ export default function BusinessTripApply() {
<div className={styles.dateField}>
<label className={styles.required}>{t('businessTrip.itineraryStartTime')}</label>
<DatePicker
value={item.startTime}
onConfirm={(val) => handleUpdateItinerary(index, 'startTime', val)}
precision="minute"
<div
className={styles.dateClickable}
onClick={() => setDatePickerVisible({ index, field: 'startTime' })}
>
{value => (
<div className={styles.dateClickable}>
{value ? dayjs(value).format('YYYY-MM-DD HH:mm') : t('common.select')}
</div>
)}
</DatePicker>
{item.startTime ? dayjs(item.startTime).format('YYYY-MM-DD HH:mm') : t('common.select')}
</div>
<DatePicker
visible={datePickerVisible?.index === index && datePickerVisible?.field === 'startTime'}
onConfirm={(val) => {
handleUpdateItinerary(index, 'startTime', val)
setDatePickerVisible(null)
}}
onClose={() => setDatePickerVisible(null)}
onCancel={() => setDatePickerVisible(null)}
precision="minute"
/>
</div>
<div className={styles.dateField}>
<label className={styles.required}>{t('businessTrip.itineraryEndTime')}</label>
<div
className={styles.dateClickable}
onClick={() => setDatePickerVisible({ index, field: 'endTime' })}
>
{item.endTime ? dayjs(item.endTime).format('YYYY-MM-DD HH:mm') : t('common.select')}
</div>
<DatePicker
value={item.endTime}
onConfirm={(val) => handleUpdateItinerary(index, 'endTime', val)}
visible={datePickerVisible?.index === index && datePickerVisible?.field === 'endTime'}
onConfirm={(val) => {
handleUpdateItinerary(index, 'endTime', val)
setDatePickerVisible(null)
}}
onClose={() => setDatePickerVisible(null)}
onCancel={() => setDatePickerVisible(null)}
precision="minute"
{...(item.startTime && { min: item.startTime })}
>
{value => (
<div className={styles.dateClickable}>
{value ? dayjs(value).format('YYYY-MM-DD HH:mm') : t('common.select')}
</div>
)}
</DatePicker>
/>
</div>
<div className={styles.field}>
@@ -214,7 +330,7 @@ export default function BusinessTripApply() {
</div>
<div className={styles.field}>
<label>{t('businessTrip.itineraryDeparture')}</label>
<label className={styles.required}>{t('businessTrip.itineraryDeparture')}</label>
<Input
placeholder={t('businessTrip.itineraryDeparture')}
value={item.departure}
@@ -234,20 +350,22 @@ export default function BusinessTripApply() {
</div>
<div className={styles.field}>
<label>{t('businessTrip.itineraryTransport')}</label>
<label className={styles.required}>{t('businessTrip.itineraryTransport')}</label>
<Selector
options={transportOptions}
value={item.transport ? [item.transport] : []}
onChange={(val) => handleUpdateItinerary(index, 'transport', val[0] || '')}
showCheckMark={false}
/>
</div>
<div className={styles.field}>
<label>{t('businessTrip.tripType')}</label>
<label className={styles.required}>{t('businessTrip.tripType')}</label>
<Selector
options={tripTypeOptions}
value={[item.tripType]}
value={item.tripType ? [item.tripType] : []}
onChange={(val) => handleUpdateItinerary(index, 'tripType', val[0] as 'oneway' | 'roundtrip')}
showCheckMark={false}
/>
</div>