refactor: 重构后端目录结构和命名规范
- 将后端目录名从大写改为小写 (Controller->controller 等) - 将命名空间从 App\ 改为 app\ (符合 webman 官方规范) - 移除不必要的 bootstrap 目录 - 完善出差审批业务功能: - 添加删除草稿功能 - 添加审批操作功能(同意/拒绝) - 完善审批详情页,显示审批人和审批记录 - 前端优化: - 调整列表页行高 - 隐藏底部导航栏 - 仪表盘只显示3个快捷入口 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<ApproverItem[]>([])
|
||||
const [ccUsers, setCcUsers] = useState<ApproverItem[]>([])
|
||||
|
||||
// 手动保存草稿的函数
|
||||
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() {
|
||||
<TextArea
|
||||
placeholder={t('businessTrip.pleaseEnterReason')}
|
||||
value={reason}
|
||||
onChange={setReason}
|
||||
onChange={(val) => {
|
||||
setReason(val)
|
||||
saveDraft()
|
||||
}}
|
||||
rows={3}
|
||||
maxLength={500}
|
||||
/>
|
||||
@@ -292,8 +423,11 @@ export default function BusinessTripApply() {
|
||||
<DatePicker
|
||||
visible={datePickerVisible?.index === index && datePickerVisible?.field === 'startTime'}
|
||||
onConfirm={(val) => {
|
||||
handleUpdateItinerary(index, 'startTime', val)
|
||||
const updated = [...itineraries]
|
||||
updated[index] = { ...updated[index], startTime: val }
|
||||
setItineraries(updated)
|
||||
setDatePickerVisible(null)
|
||||
saveDraft()
|
||||
}}
|
||||
onClose={() => setDatePickerVisible(null)}
|
||||
onCancel={() => setDatePickerVisible(null)}
|
||||
@@ -312,8 +446,11 @@ export default function BusinessTripApply() {
|
||||
<DatePicker
|
||||
visible={datePickerVisible?.index === index && datePickerVisible?.field === 'endTime'}
|
||||
onConfirm={(val) => {
|
||||
handleUpdateItinerary(index, 'endTime', val)
|
||||
const updated = [...itineraries]
|
||||
updated[index] = { ...updated[index], endTime: val }
|
||||
setItineraries(updated)
|
||||
setDatePickerVisible(null)
|
||||
saveDraft()
|
||||
}}
|
||||
onClose={() => setDatePickerVisible(null)}
|
||||
onCancel={() => setDatePickerVisible(null)}
|
||||
|
||||
Reference in New Issue
Block a user