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
@@ -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,
+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)}
@@ -60,6 +60,37 @@
flex: 1;
}
.userList {
display: flex;
margin-bottom: 12px;
}
.userList:last-child {
margin-bottom: 0;
}
.userListLabel {
color: #999;
font-size: 14px;
width: 80px;
flex-shrink: 0;
}
.users {
display: flex;
flex-wrap: wrap;
gap: 8px;
flex: 1;
}
.user {
background: #f5f5f5;
padding: 4px 12px;
border-radius: 16px;
font-size: 13px;
color: #333;
}
.section {
margin-bottom: 16px;
}
@@ -94,11 +125,77 @@
}
.timeline {
padding: 16px 0;
padding: 8px 0;
}
.timelineItem {
position: relative;
padding-left: 40px;
padding-bottom: 20px;
}
.timelineItem:last-child {
padding-bottom: 0;
}
.timelineItem:not(:last-child)::before {
content: '';
position: absolute;
left: 11px;
top: 24px;
bottom: 0;
width: 2px;
background: #e8e8e8;
}
.timelineDot {
position: absolute;
left: 0;
top: 0;
width: 24px;
height: 24px;
border-radius: 50%;
background: #1677ff;
display: flex;
align-items: center;
justify-content: center;
}
.timelineDotIcon {
font-size: 12px;
}
.timelineContent {
padding: 4px 0;
}
.timelineTitle {
font-weight: 500;
color: #333;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.timelineName {
font-size: 13px;
color: #666;
font-weight: 400;
}
.timelineTime {
font-size: 12px;
color: #999;
}
.timelineComment {
font-size: 13px;
color: #666;
margin-top: 4px;
background: #f5f5f5;
padding: 6px 10px;
border-radius: 4px;
}
.timelineTitle {
@@ -152,6 +249,20 @@
flex: 1;
}
.deleteBtn {
flex: 0 0 auto;
min-width: 120px;
}
.commentBox {
padding: 8px 0;
}
.commentBox :global(.adm-text-area) {
background: #f5f5f5;
border-radius: 8px;
}
:global(.adm-card-body) {
padding: 16px;
}
+211 -64
View File
@@ -1,9 +1,17 @@
import { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { NavBar, Card, Tag, Button, Dialog, Toast } from 'antd-mobile'
import { LeftOutline, ClockCircleOutline, CheckCircleOutline, CloseCircleOutline, ExclamationCircleOutline } from 'antd-mobile-icons'
import { businessTripApi, approvalApi } from '../../services/api'
import { NavBar, Card, Tag, Button, Dialog, Toast, TextArea } from 'antd-mobile'
import {
LeftOutline,
ClockCircleOutline,
CheckCircleOutline,
CloseCircleOutline,
ExclamationCircleOutline,
DeleteOutline
} from 'antd-mobile-icons'
import { businessTripApi, userApi } from '../../services/api'
import { useUserStore } from '@/stores/user'
import dayjs from 'dayjs'
import styles from './detail.module.css'
@@ -27,6 +35,17 @@ interface Itinerary {
remark: string
}
interface ApprovalRecord {
id: number
approver_id: number
action: string
comment: string | null
created_at: string
approver: {
name: string
}
}
interface BusinessTripDetail {
id: number
reason: string
@@ -42,15 +61,30 @@ interface BusinessTripDetail {
email: string
}
itineraries: Itinerary[]
approval_order?: {
id: number
status: string
form_data: {
approvers?: any[]
cc_users?: any[]
}
}
approvers?: any[]
cc_users?: any[]
approval_records?: ApprovalRecord[]
}
export default function BusinessTripDetail() {
const { t } = useTranslation()
const navigate = useNavigate()
const { id } = useParams()
const { user } = useUserStore()
const [detail, setDetail] = useState<BusinessTripDetail | null>(null)
const [loading, setLoading] = useState(true)
const [commentVisible, setCommentVisible] = useState(false)
const [comment, setComment] = useState('')
const [actionType, setActionType] = useState<'approve' | 'reject'>('approve')
// 加载详情
const loadDetail = async () => {
@@ -72,6 +106,24 @@ export default function BusinessTripDetail() {
loadDetail()
}, [id])
// 删除草稿
const handleDelete = () => {
Dialog.confirm({
content: '确定要删除此草稿吗?删除后无法恢复',
confirmText: '删除',
cancelText: '取消',
onConfirm: async () => {
try {
await businessTripApi.deleteDraft(Number(id))
Toast.show('删除成功')
navigate('/business-trip')
} catch (error: any) {
Toast.show(error.message || '删除失败')
}
},
})
}
// 撤销申请
const handleCancel = () => {
Dialog.confirm({
@@ -93,13 +145,39 @@ export default function BusinessTripDetail() {
navigate(`/business-trip/apply?id=${id}`)
}
// 打开审批意见框
const handleApprove = (action: 'approve' | 'reject') => {
setActionType(action)
setComment('')
setCommentVisible(true)
}
// 提交审批
const handleConfirmApprove = async () => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur()
}
try {
await businessTripApi.approve(Number(id), actionType, comment)
Toast.show(actionType === 'approve' ? '已同意' : '已拒绝')
setCommentVisible(false)
loadDetail()
} catch (error: any) {
Toast.show(error.message || '操作失败')
}
}
if (loading || !detail) {
return <div className={styles.loading}>加载中...</div>
}
const config = statusConfig[detail.status] || statusConfig.draft
const canEdit = detail.status === 'draft'
const canCancel = detail.status === 'pending'
const isOwner = detail.user?.name === user?.name
const canEdit = detail.status === 'draft' && isOwner
const canDelete = detail.status === 'draft' && isOwner
const canCancel = detail.status === 'pending' && isOwner
const canApprove = detail.status === 'pending' && !isOwner
// 计算天数
const calculateDays = (start: string, end: string) => {
@@ -119,6 +197,22 @@ export default function BusinessTripDetail() {
return dayjs(dateStr).format('YYYY-MM-DD')
}
// 获取审批记录配置
const getRecordConfig = (action: string) => {
switch (action) {
case 'submit':
return { color: '#999', icon: '📝', text: '提交申请' }
case 'approve':
return { color: '#52c41a', icon: '✅', text: '已同意' }
case 'reject':
return { color: '#ff4d4f', icon: '❌', text: '已拒绝' }
case 'cancel':
return { color: '#999', icon: '🔄', text: '已撤销' }
default:
return { color: '#999', icon: '📌', text: action }
}
}
return (
<div className={styles.detail}>
<NavBar onBack={() => navigate(-1)}>出差申请详情</NavBar>
@@ -138,10 +232,6 @@ export default function BusinessTripDetail() {
<span className={styles.label}>申请人:</span>
<span className={styles.value}>{detail.user?.name}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.label}>联系电话:</span>
<span className={styles.value}>{detail.user?.phone}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.label}>出发日期:</span>
<span className={styles.value}>{formatDate(detail.start_date)}</span>
@@ -158,15 +248,39 @@ export default function BusinessTripDetail() {
<span className={styles.label}>目的地:</span>
<span className={styles.value}>{detail.destination}</span>
</div>
{detail.estimated_cost && detail.estimated_cost !== '0.00' && (
<div className={styles.infoItem}>
<span className={styles.label}>预估费用:</span>
<span className={styles.value}>¥{detail.estimated_cost}</span>
</div>
)}
</div>
</Card>
{/* 审批人和抄送人 */}
{(detail.approvers?.length || detail.cc_users?.length) && (
<Card title="审批信息" className={styles.section}>
{detail.approvers && detail.approvers.length > 0 && (
<div className={styles.userList}>
<div className={styles.userListLabel}>审批人:</div>
<div className={styles.users}>
{detail.approvers.map((u: any) => (
<div key={u.id} className={styles.user}>
<span>{u.name}</span>
</div>
))}
</div>
</div>
)}
{detail.cc_users && detail.cc_users.length > 0 && (
<div className={styles.userList}>
<div className={styles.userListLabel}>抄送人:</div>
<div className={styles.users}>
{detail.cc_users.map((u: any) => (
<div key={u.id} className={styles.user}>
<span>{u.name}</span>
</div>
))}
</div>
</div>
)}
</Card>
)}
{/* 行程明细 */}
<Card title="行程明细" className={styles.section}>
{detail.itineraries && detail.itineraries.length > 0 ? (
@@ -216,63 +330,55 @@ export default function BusinessTripDetail() {
)}
</Card>
{/* 审批进度 */}
<Card title="审批进度" className={styles.section}>
{/* 审批记录 */}
<Card title="审批记录" className={styles.section}>
<div className={styles.timeline}>
<div className={styles.timelineItem}>
<div className={styles.timelineDot}></div>
<div className={styles.timelineContent}>
<div className={styles.timelineTitle}>提交申请</div>
<div className={styles.timelineTime}>
{dayjs(detail.created_at).format('YYYY-MM-DD HH:mm')}
</div>
</div>
</div>
{detail.status === 'pending' && (
<div className={styles.timelineItem}>
<div className={styles.timelineDotPending}></div>
<div className={styles.timelineContent}>
<div className={styles.timelineTitle}>等待审批中...</div>
<div className={styles.timelineDesc}>审批人将收到通知并处理您的申请</div>
</div>
</div>
)}
{detail.status === 'approved' && (
<div className={styles.timelineItem}>
<div className={styles.timelineDotSuccess}>
<CheckCircleOutline className={styles.timelineIconSuccess} />
</div>
<div className={styles.timelineContent}>
<div className={styles.timelineTitle}>审批已通过</div>
</div>
</div>
)}
{detail.status === 'rejected' && (
<div className={styles.timelineItem}>
<div className={styles.timelineDotError}>
<CloseCircleOutline className={styles.timelineIconError} />
</div>
<div className={styles.timelineContent}>
<div className={styles.timelineTitle}>审批已拒绝</div>
</div>
</div>
)}
{detail.status === 'cancelled' && (
<div className={styles.timelineItem}>
<div className={styles.timelineDotCancelled}></div>
<div className={styles.timelineContent}>
<div className={styles.timelineTitle}>已撤销</div>
<div className={styles.timelineDesc}>申请人撤销了此申请</div>
</div>
</div>
{detail.approval_records && detail.approval_records.length > 0 ? (
detail.approval_records.map((record) => {
const recordConfig = getRecordConfig(record.action)
return (
<div key={record.id} className={styles.timelineItem}>
<div className={styles.timelineDot} style={{ background: recordConfig.color }}>
<span className={styles.timelineDotIcon}>{recordConfig.icon}</span>
</div>
<div className={styles.timelineContent}>
<div className={styles.timelineTitle}>
{recordConfig.text}
<span className={styles.timelineName}>
{record.approver?.name}
</span>
</div>
<div className={styles.timelineTime}>
{dayjs(record.created_at).format('YYYY-MM-DD HH:mm')}
</div>
{record.comment && (
<div className={styles.timelineComment}>备注: {record.comment}</div>
)}
</div>
</div>
)
})
) : (
<div className={styles.empty}>暂无审批记录</div>
)}
</div>
</Card>
</div>
{/* 底部操作栏 */}
{(canEdit || canCancel) && (
{(canEdit || canDelete || canCancel || canApprove) && (
<div className={styles.footer}>
{canDelete && (
<Button
block
color="danger"
fill="outline"
onClick={handleDelete}
className={styles.deleteBtn}
>
<DeleteOutline /> 删除草稿
</Button>
)}
{canEdit && (
<Button block color="primary" onClick={handleEdit}>
编辑申请
@@ -283,8 +389,49 @@ export default function BusinessTripDetail() {
撤销申请
</Button>
)}
{canApprove && (
<>
<Button
block
color="danger"
fill="outline"
onClick={() => handleApprove('reject')}
>
拒绝
</Button>
<Button
block
color="primary"
onClick={() => handleApprove('approve')}
>
同意
</Button>
</>
)}
</div>
)}
{/* 审批意见弹框 */}
<Dialog
visible={commentVisible}
title={actionType === 'approve' ? '同意申请' : '拒绝申请'}
content={
<div className={styles.commentBox}>
<TextArea
placeholder="请输入审批意见(可选)"
value={comment}
onChange={setComment}
rows={3}
maxLength={200}
/>
</div>
}
onConfirm={handleConfirmApprove}
onClose={() => setCommentVisible(false)}
onCancel={() => setCommentVisible(false)}
confirmText="确认"
cancelText="取消"
/>
</div>
)
}
@@ -5,7 +5,7 @@
}
.item {
padding: 12px 16px !important;
padding: 0 12px !important;
}
.card {
@@ -14,11 +14,15 @@
overflow: hidden;
}
.card :global(.adm-card-body) {
padding: 8px 12px !important;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
margin-bottom: 6px;
}
.reason {
@@ -32,7 +36,7 @@
.content {
display: flex;
flex-direction: column;
gap: 8px;
gap: 4px;
}
.info {
@@ -34,17 +34,10 @@ export default function SelectUserPage() {
const state = location.state as LocationState
const [searchValue, setSearchValue] = useState('')
// 优先从 location.state 获取,否则从 sessionStorage 获取
// 只从 sessionStorage 获取对应类型的数据
const getInitialSelected = () => {
if (state?.selected) return state.selected
if (state?.type === 'approver') {
const data = sessionStorage.getItem('selectedApprovers')
return data ? JSON.parse(data) : []
} else if (state?.type === 'cc') {
const data = sessionStorage.getItem('selectedCc')
return data ? JSON.parse(data) : []
}
return []
const data = sessionStorage.getItem('selectedApprovers')
return data ? JSON.parse(data) : []
}
const [selectedUsers, setSelectedUsers] = useState<UserItem[]>(getInitialSelected())
const [allUsers, setAllUsers] = useState<UserItem[]>([])
@@ -118,7 +111,8 @@ export default function SelectUserPage() {
} else if (type === 'cc') {
sessionStorage.setItem('selectedCc', JSON.stringify(selectedUsers))
}
navigate(-1)
// 使用 navigate 而不是 navigate(-1),传递标志表示从选择页面返回
navigate('/business-trip/apply', { state: { fromSelection: true, type } })
}
const handleAddApprover = () => {
@@ -34,9 +34,8 @@ export default function SelectCcPage() {
const state = location.state as LocationState
const [searchValue, setSearchValue] = useState('')
// 优先从 location.state 获取,否则从 sessionStorage 获取
// 只从 sessionStorage 获取对应类型的数据
const getInitialSelected = () => {
if (state?.selected) return state.selected
const data = sessionStorage.getItem('selectedCc')
return data ? JSON.parse(data) : []
}
@@ -107,7 +106,8 @@ export default function SelectCcPage() {
const handleConfirm = () => {
// 使用 sessionStorage 来传递数据
sessionStorage.setItem('selectedCc', JSON.stringify(selectedUsers))
navigate(-1)
// 使用 navigate 而不是 navigate(-1),传递标志表示从选择页面返回
navigate('/business-trip/apply', { state: { fromSelection: true, type: 'cc' } })
}
return (