fix: 修复页面间状态丢失问题并优化选择页面
- 修复选择审批人后再选择抄送人导致状态丢失的问题 - 使用独立的 sessionStorage key (selectedApprovers, selectedCc) - 组件每次挂载时从 sessionStorage 恢复状态 - 只在提交/保存成功后清除 sessionStorage - 优化人员选择页面布局和样式 - 更新 CLAUDE.md 文档记录问题和解决方案 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user