refactor: 重构后端目录结构和命名规范

- 将后端目录名从大写改为小写 (Controller->controller 等)
- 将命名空间从 App\ 改为 app\ (符合 webman 官方规范)
- 移除不必要的 bootstrap 目录
- 完善出差审批业务功能:
  - 添加删除草稿功能
  - 添加审批操作功能(同意/拒绝)
  - 完善审批详情页,显示审批人和审批记录
- 前端优化:
  - 调整列表页行高
  - 隐藏底部导航栏
  - 仪表盘只显示3个快捷入口

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-25 18:09:04 +08:00
parent 09d4cb8054
commit 582f454a5a
47 changed files with 1297 additions and 360 deletions
+158 -21
View File
@@ -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)}