refactor(web): 应用外壳/差旅详情/报销创建视图适配主题与多 task
- AppShellRouteView/useAppShell 适配主题皮肤与会话入口 - TravelRequestDetailView/travelRequestDetailSetup 差旅详情适配,travel-request-detail-view.css 调整 - TravelReimbursementCreateView/useTravelReimbursementCreateViewLifecycle 创建视图适配 - 更新 app-shell-financial-assistant-entry/travel-request-detail-risk-advice 测试
This commit is contained in:
@@ -2,6 +2,11 @@ import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
||||
|
||||
import { useSystemState } from '../../composables/useSystemState.js'
|
||||
import { useToast } from '../../composables/useToast.js'
|
||||
import {
|
||||
AI_APPLICATION_ACTION_SAVE_DRAFT,
|
||||
runAiApplicationPreviewAction
|
||||
} from '../../services/aiApplicationPreviewActions.js'
|
||||
import { calculateTravelReimbursement } from '../../services/reimbursements.js'
|
||||
import {
|
||||
canApproveBudgetExpenseApplications,
|
||||
canApproveLeaderExpenseClaims,
|
||||
@@ -23,6 +28,13 @@ import {
|
||||
import { buildRiskViewerContext } from '../../utils/riskVisibility.js'
|
||||
import { resolveProgressStepsForViewer } from '../../utils/requestProgressViewer.js'
|
||||
import { isArchivedRequestView, normalizeRequestForUi } from '../../utils/requestViewModel.js'
|
||||
import {
|
||||
APPLICATION_TRANSPORT_MODE_OPTIONS,
|
||||
applyApplicationPolicyEstimateError,
|
||||
applyApplicationPolicyEstimateResult,
|
||||
buildApplicationPolicyEstimateRequest,
|
||||
normalizeApplicationPreview
|
||||
} from '../../utils/expenseApplicationPreview.js'
|
||||
import {
|
||||
EXPENSE_TYPE_OPTIONS,
|
||||
buildFallbackExpenseItems,
|
||||
@@ -43,6 +55,11 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
||||
const { currentUser } = useSystemState()
|
||||
const expenseItems = ref([])
|
||||
const expenseAttachmentMeta = reactive({})
|
||||
const applicationDetailEditor = reactive({
|
||||
fieldKey: '',
|
||||
draftValue: '',
|
||||
saving: false
|
||||
})
|
||||
const riskFlagPreviewSnapshot = ref(null)
|
||||
let actionBusy = { value: false }
|
||||
const getActionBusy = () => Boolean(actionBusy?.value)
|
||||
@@ -92,11 +109,10 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
||||
const isEditableRequest = computed(() => ['draft', 'supplement'].includes(request.value.approvalKey))
|
||||
const canOpenAiEntry = computed(() => isEditableRequest.value)
|
||||
const isCurrentApplicant = computed(() => isCurrentRequestApplicant(request.value, currentUser.value))
|
||||
const canModifyReturnedApplication = computed(() => (
|
||||
const canModifyApplication = computed(() => (
|
||||
isApplicationDocument.value
|
||||
&& isEditableRequest.value
|
||||
&& isCurrentApplicant.value
|
||||
&& String(request.value.status || '').trim().toLowerCase() === 'returned'
|
||||
))
|
||||
const canManageCurrentClaim = computed(() => canManageExpenseClaims(currentUser.value))
|
||||
const isArchivedRequest = computed(() => isArchivedRequestView(request.value))
|
||||
@@ -261,6 +277,7 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
||||
|| approvalFlow.returnBusy.value
|
||||
|| approvalFlow.approveBusy.value
|
||||
|| paymentFlow.payBusy.value
|
||||
|| applicationDetailEditor.saving
|
||||
|| smartEntryRecognitionBusy.value
|
||||
|| Boolean(uploadingExpenseId.value)
|
||||
|| Boolean(deletingAttachmentId.value)
|
||||
@@ -350,6 +367,15 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
||||
)
|
||||
const applicationDetailFactItems = computed(() => buildApplicationDetailFactItems(request.value))
|
||||
const relatedApplicationFactItems = computed(() => buildRelatedApplicationFactItems(request.value))
|
||||
const applicationEditEditableFields = ['reason', 'time', 'location', 'transportMode']
|
||||
const applicationDetailEditableFactKeys = new Set([
|
||||
'reason',
|
||||
'location',
|
||||
'transport_mode',
|
||||
'trip_start_time',
|
||||
'trip_return_time',
|
||||
'time'
|
||||
])
|
||||
|
||||
watch(
|
||||
request,
|
||||
@@ -366,6 +392,7 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
||||
attachmentPreview.closeAttachmentPreview()
|
||||
}
|
||||
expenseEditor.resetExpenseWorkState()
|
||||
cancelApplicationDetailEditor()
|
||||
void attachmentPreview.syncExpenseAttachmentMeta()
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -403,6 +430,8 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
||||
return {
|
||||
sourceText: '修改申请',
|
||||
modelReviewStatus: 'template',
|
||||
applicationEditMode: true,
|
||||
editableFields: applicationEditEditableFields,
|
||||
fields: {
|
||||
applicationType: pickFact('申请类型') || request.value.typeLabel || '费用申请',
|
||||
applicant: request.value.profileName || request.value.person || request.value.applicant || '',
|
||||
@@ -424,27 +453,163 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleModifyApplication() {
|
||||
if (!canModifyReturnedApplication.value) {
|
||||
function buildApplicationEditDraftPayload() {
|
||||
const claimId = String(request.value?.claimId || '').trim()
|
||||
const claimNo = String(request.value?.claimNo || request.value?.documentNo || request.value?.id || '').trim()
|
||||
return {
|
||||
draft_type: 'expense_application',
|
||||
document_type: 'expense_application',
|
||||
claim_id: claimId,
|
||||
claim_no: claimNo,
|
||||
status: String(request.value?.status || request.value?.approvalKey || '').trim(),
|
||||
approval_stage: String(request.value?.node || request.value?.approvalStage || '待提交').trim(),
|
||||
title: String(request.value?.typeLabel || '费用申请').trim(),
|
||||
application_edit_mode: true
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeApplicationDetailEditorValue(value = '') {
|
||||
const text = String(value || '').trim()
|
||||
return text === '待补充' ? '' : text
|
||||
}
|
||||
|
||||
function resolveApplicationDetailFactValue(key = '') {
|
||||
const targetKey = String(key || '').trim()
|
||||
return String(applicationDetailFactItems.value.find((item) => item?.key === targetKey)?.value || '').trim()
|
||||
}
|
||||
|
||||
function buildApplicationDetailDateRange(startDate = '', endDate = '') {
|
||||
const start = String(startDate || '').trim()
|
||||
const end = String(endDate || '').trim()
|
||||
if (!start && !end) return ''
|
||||
if (!start) return end
|
||||
if (!end || end === start) return start
|
||||
return `${start} 至 ${end}`
|
||||
}
|
||||
|
||||
function resolveApplicationDetailDays(startDate = '', endDate = '') {
|
||||
const start = String(startDate || '').trim()
|
||||
const end = String(endDate || '').trim()
|
||||
if (!start || !end) return ''
|
||||
const startTime = new Date(`${start}T00:00:00`).getTime()
|
||||
const endTime = new Date(`${end}T00:00:00`).getTime()
|
||||
if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || endTime < startTime) {
|
||||
return ''
|
||||
}
|
||||
return `${Math.round((endTime - startTime) / 86400000) + 1}天`
|
||||
}
|
||||
|
||||
function canEditApplicationDetailItem(item = {}) {
|
||||
return (
|
||||
canModifyApplication.value
|
||||
&& applicationDetailEditableFactKeys.has(String(item?.key || '').trim())
|
||||
)
|
||||
}
|
||||
|
||||
function isApplicationDetailEditing(item = {}) {
|
||||
return String(applicationDetailEditor.fieldKey || '') === String(item?.key || '')
|
||||
}
|
||||
|
||||
function resolveApplicationDetailEditorControl(item = {}) {
|
||||
const key = String(item?.key || '').trim()
|
||||
if (['trip_start_time', 'trip_return_time', 'time'].includes(key)) {
|
||||
return 'date'
|
||||
}
|
||||
if (key === 'transport_mode') {
|
||||
return 'select'
|
||||
}
|
||||
return 'text'
|
||||
}
|
||||
|
||||
function openApplicationDetailEditor(item = {}) {
|
||||
if (!canEditApplicationDetailItem(item) || applicationDetailEditor.saving) {
|
||||
return
|
||||
}
|
||||
applicationDetailEditor.fieldKey = String(item.key || '').trim()
|
||||
applicationDetailEditor.draftValue = normalizeApplicationDetailEditorValue(item.value)
|
||||
}
|
||||
|
||||
function cancelApplicationDetailEditor() {
|
||||
applicationDetailEditor.fieldKey = ''
|
||||
applicationDetailEditor.draftValue = ''
|
||||
}
|
||||
|
||||
function buildEditedApplicationPreview(item = {}) {
|
||||
const key = String(item?.key || '').trim()
|
||||
const nextValue = normalizeApplicationDetailEditorValue(applicationDetailEditor.draftValue)
|
||||
const preview = buildApplicationEditPreview()
|
||||
const fields = { ...(preview.fields || {}) }
|
||||
|
||||
if (key === 'reason') {
|
||||
fields.reason = nextValue
|
||||
} else if (key === 'location') {
|
||||
fields.location = nextValue
|
||||
} else if (key === 'transport_mode') {
|
||||
fields.transportMode = nextValue
|
||||
} else if (key === 'time') {
|
||||
fields.time = nextValue
|
||||
} else if (key === 'trip_start_time' || key === 'trip_return_time') {
|
||||
const startDate = key === 'trip_start_time'
|
||||
? nextValue
|
||||
: resolveApplicationDetailFactValue('trip_start_time')
|
||||
const endDate = key === 'trip_return_time'
|
||||
? nextValue
|
||||
: resolveApplicationDetailFactValue('trip_return_time')
|
||||
fields.time = buildApplicationDetailDateRange(startDate, endDate)
|
||||
fields.days = resolveApplicationDetailDays(startDate, endDate) || fields.days
|
||||
}
|
||||
|
||||
return normalizeApplicationPreview({
|
||||
...preview,
|
||||
fields
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshEditedApplicationPreviewEstimate(preview = {}) {
|
||||
const estimateRequest = buildApplicationPolicyEstimateRequest(preview, currentUser.value || {})
|
||||
if (!estimateRequest.canCalculate) {
|
||||
return preview
|
||||
}
|
||||
try {
|
||||
const result = await calculateTravelReimbursement(estimateRequest.payload)
|
||||
return applyApplicationPolicyEstimateResult(preview, result, currentUser.value || {})
|
||||
} catch (error) {
|
||||
return applyApplicationPolicyEstimateError(preview, error, currentUser.value || {})
|
||||
}
|
||||
}
|
||||
|
||||
async function saveApplicationDetailEdit(item = {}) {
|
||||
if (!isApplicationDetailEditing(item) || applicationDetailEditor.saving) {
|
||||
return
|
||||
}
|
||||
if (!String(request.value?.claimId || '').trim()) {
|
||||
toast('当前申请缺少单据标识,暂不能修改。')
|
||||
return
|
||||
}
|
||||
|
||||
const claimId = String(request.value?.claimId || '').trim()
|
||||
emit('openAssistant', {
|
||||
source: 'application',
|
||||
sessionType: 'application',
|
||||
prompt: '',
|
||||
applicationPreview: buildApplicationEditPreview(),
|
||||
request: {
|
||||
...request.value,
|
||||
applicationEditMode: true
|
||||
},
|
||||
restoreLatestConversation: false,
|
||||
initialPromptAutoSubmit: false,
|
||||
scope: claimId
|
||||
? { type: 'claim', claimId }
|
||||
: null
|
||||
})
|
||||
applicationDetailEditor.saving = true
|
||||
try {
|
||||
const preview = await refreshEditedApplicationPreviewEstimate(buildEditedApplicationPreview(item))
|
||||
const payload = await runAiApplicationPreviewAction({
|
||||
actionType: AI_APPLICATION_ACTION_SAVE_DRAFT,
|
||||
applicationPreview: preview,
|
||||
currentUser: currentUser.value || {},
|
||||
draftPayload: buildApplicationEditDraftPayload()
|
||||
})
|
||||
const draftPayload = payload?.result?.draft_payload || payload?.draft_payload || {}
|
||||
emit('request-updated', {
|
||||
claimId: String(draftPayload.claim_id || request.value.claimId || '').trim(),
|
||||
claimNo: String(draftPayload.claim_no || request.value.claimNo || request.value.documentNo || '').trim(),
|
||||
status: String(draftPayload.status || request.value.status || '').trim(),
|
||||
approvalStage: String(draftPayload.approval_stage || request.value.node || '').trim()
|
||||
})
|
||||
cancelApplicationDetailEditor()
|
||||
toast('申请信息已更新。')
|
||||
} catch (error) {
|
||||
toast(error?.message || '申请信息更新失败,请稍后重试。')
|
||||
} finally {
|
||||
applicationDetailEditor.saving = false
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -461,22 +626,25 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
||||
...expenseEditor,
|
||||
...paymentFlow,
|
||||
...riskSubmit,
|
||||
APPLICATION_TRANSPORT_MODE_OPTIONS,
|
||||
applicationDetailEditor,
|
||||
applicationDetailFactItems,
|
||||
relatedApplicationFactItems,
|
||||
canEditApplicationDetailItem,
|
||||
canDeleteRequest,
|
||||
canManageCurrentClaim,
|
||||
canModifyReturnedApplication,
|
||||
canModifyApplication,
|
||||
canOpenAiEntry,
|
||||
canApproveRequest,
|
||||
canReturnRequest,
|
||||
currentProgressRingMotion,
|
||||
expenseItems,
|
||||
expenseTypeOptions: EXPENSE_TYPE_OPTIONS,
|
||||
handleModifyApplication,
|
||||
hasLeaderApprovalEvents,
|
||||
hasSingleLeaderApprovalEvent,
|
||||
heroFactItems,
|
||||
isApplicationDocument,
|
||||
isApplicationDetailEditing,
|
||||
isDraftRequest,
|
||||
isEditableRequest,
|
||||
isTravelRequest,
|
||||
@@ -485,8 +653,12 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
||||
profile,
|
||||
progressSteps,
|
||||
request,
|
||||
cancelApplicationDetailEditor,
|
||||
openApplicationDetailEditor,
|
||||
resolveExpenseReasonHelper,
|
||||
resolveExpenseReasonPlaceholder,
|
||||
resolveApplicationDetailEditorControl,
|
||||
saveApplicationDetailEdit,
|
||||
showApplicationLeaderOpinion,
|
||||
showBudgetAnalysis,
|
||||
showStageRiskAdvice,
|
||||
|
||||
Reference in New Issue
Block a user