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:
@@ -616,8 +616,8 @@
|
|||||||
border-left: 0;
|
border-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-detail-fact span,
|
.application-detail-fact > span,
|
||||||
.application-detail-fact strong {
|
.application-detail-fact > strong {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -625,7 +625,7 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-detail-fact span {
|
.application-detail-fact > span {
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -637,10 +637,11 @@
|
|||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 750;
|
font-weight: 750;
|
||||||
|
gap: 8px;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-detail-fact.highlight span {
|
.application-detail-fact.highlight > span {
|
||||||
background: var(--theme-primary-soft);
|
background: var(--theme-primary-soft);
|
||||||
color: var(--theme-primary-active);
|
color: var(--theme-primary-active);
|
||||||
}
|
}
|
||||||
@@ -654,6 +655,77 @@
|
|||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.application-detail-fact-value {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-detail-edit-btn,
|
||||||
|
.application-detail-edit-confirm,
|
||||||
|
.application-detail-edit-cancel {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-detail-edit-btn {
|
||||||
|
opacity: 0;
|
||||||
|
transition:
|
||||||
|
opacity 0.16s ease,
|
||||||
|
background 0.16s ease,
|
||||||
|
color 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-detail-fact.editable:hover .application-detail-edit-btn,
|
||||||
|
.application-detail-edit-btn:focus-visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-detail-edit-btn:hover:not(:disabled),
|
||||||
|
.application-detail-edit-btn:focus-visible,
|
||||||
|
.application-detail-edit-confirm:hover:not(:disabled),
|
||||||
|
.application-detail-edit-cancel:hover:not(:disabled) {
|
||||||
|
background: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.1);
|
||||||
|
color: var(--theme-primary-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-detail-edit-confirm {
|
||||||
|
background: rgba(22, 163, 74, 0.1);
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-detail-edit-cancel {
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-detail-edit-btn:disabled,
|
||||||
|
.application-detail-edit-confirm:disabled,
|
||||||
|
.application-detail-edit-cancel:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-detail-fact.editing strong {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-detail-editor-control {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-detail-editor-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.related-application-facts {
|
.related-application-facts {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ export function useAppShell() {
|
|||||||
sessionType: '',
|
sessionType: '',
|
||||||
budgetContext: null,
|
budgetContext: null,
|
||||||
initialPromptAutoSubmit: true,
|
initialPromptAutoSubmit: true,
|
||||||
initialApplicationPreview: null
|
initialApplicationPreview: null,
|
||||||
|
initialDraftPayload: null
|
||||||
})
|
})
|
||||||
const smartEntrySessionId = ref(0)
|
const smartEntrySessionId = ref(0)
|
||||||
const smartEntryRevealToken = ref(0)
|
const smartEntryRevealToken = ref(0)
|
||||||
@@ -337,7 +338,8 @@ export function useAppShell() {
|
|||||||
sessionType: '',
|
sessionType: '',
|
||||||
budgetContext: null,
|
budgetContext: null,
|
||||||
initialPromptAutoSubmit: true,
|
initialPromptAutoSubmit: true,
|
||||||
initialApplicationPreview: null
|
initialApplicationPreview: null,
|
||||||
|
initialDraftPayload: null
|
||||||
}
|
}
|
||||||
smartEntrySessionId.value += 1
|
smartEntrySessionId.value += 1
|
||||||
}
|
}
|
||||||
@@ -504,7 +506,8 @@ export function useAppShell() {
|
|||||||
? payload.budgetContext
|
? payload.budgetContext
|
||||||
: null,
|
: null,
|
||||||
initialPromptAutoSubmit: false,
|
initialPromptAutoSubmit: false,
|
||||||
initialApplicationPreview: null
|
initialApplicationPreview: null,
|
||||||
|
initialDraftPayload: null
|
||||||
}
|
}
|
||||||
smartEntrySessionId.value += 1
|
smartEntrySessionId.value += 1
|
||||||
return
|
return
|
||||||
@@ -531,6 +534,9 @@ export function useAppShell() {
|
|||||||
initialPromptAutoSubmit: payload.initialPromptAutoSubmit !== false,
|
initialPromptAutoSubmit: payload.initialPromptAutoSubmit !== false,
|
||||||
initialApplicationPreview: payload.applicationPreview && typeof payload.applicationPreview === 'object'
|
initialApplicationPreview: payload.applicationPreview && typeof payload.applicationPreview === 'object'
|
||||||
? payload.applicationPreview
|
? payload.applicationPreview
|
||||||
|
: null,
|
||||||
|
initialDraftPayload: payload.draftPayload && typeof payload.draftPayload === 'object'
|
||||||
|
? payload.draftPayload
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
smartEntrySessionId.value += 1
|
smartEntrySessionId.value += 1
|
||||||
|
|||||||
@@ -227,6 +227,7 @@
|
|||||||
:initial-budget-context="smartEntryContext.budgetContext"
|
:initial-budget-context="smartEntryContext.budgetContext"
|
||||||
:initial-prompt-auto-submit="smartEntryContext.initialPromptAutoSubmit"
|
:initial-prompt-auto-submit="smartEntryContext.initialPromptAutoSubmit"
|
||||||
:initial-application-preview="smartEntryContext.initialApplicationPreview"
|
:initial-application-preview="smartEntryContext.initialApplicationPreview"
|
||||||
|
:initial-draft-payload="smartEntryContext.initialDraftPayload"
|
||||||
:entry-source="smartEntryContext.source"
|
:entry-source="smartEntryContext.source"
|
||||||
:request-context="smartEntryContext.request"
|
:request-context="smartEntryContext.request"
|
||||||
:invalidated-draft-claim-id="smartEntryInvalidatedDraftClaimId"
|
:invalidated-draft-claim-id="smartEntryInvalidatedDraftClaimId"
|
||||||
|
|||||||
@@ -43,10 +43,84 @@
|
|||||||
v-for="item in applicationDetailFactItems"
|
v-for="item in applicationDetailFactItems"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
class="application-detail-fact"
|
class="application-detail-fact"
|
||||||
:class="{ highlight: item.highlight, emphasis: item.emphasis }"
|
:class="{
|
||||||
|
highlight: item.highlight,
|
||||||
|
emphasis: item.emphasis,
|
||||||
|
editable: canEditApplicationDetailItem(item),
|
||||||
|
editing: isApplicationDetailEditing(item)
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<span>{{ item.label }}</span>
|
<span>{{ item.label }}</span>
|
||||||
<strong>{{ item.value }}</strong>
|
<strong>
|
||||||
|
<template v-if="isApplicationDetailEditing(item)">
|
||||||
|
<ElDatePicker
|
||||||
|
v-if="resolveApplicationDetailEditorControl(item) === 'date'"
|
||||||
|
v-model="applicationDetailEditor.draftValue"
|
||||||
|
class="application-detail-editor-control"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
format="YYYY/MM/DD"
|
||||||
|
popper-class="detail-editor-date-popper"
|
||||||
|
:clearable="false"
|
||||||
|
:disabled="applicationDetailEditor.saving"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<EnterpriseSelect
|
||||||
|
v-else-if="resolveApplicationDetailEditorControl(item) === 'select'"
|
||||||
|
v-model="applicationDetailEditor.draftValue"
|
||||||
|
class="application-detail-editor-control application-detail-editor-select"
|
||||||
|
:options="APPLICATION_TRANSPORT_MODE_OPTIONS"
|
||||||
|
clearable
|
||||||
|
:teleported="false"
|
||||||
|
:disabled="applicationDetailEditor.saving"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<ElInput
|
||||||
|
v-else
|
||||||
|
v-model="applicationDetailEditor.draftValue"
|
||||||
|
class="application-detail-editor-control"
|
||||||
|
clearable
|
||||||
|
:disabled="applicationDetailEditor.saving"
|
||||||
|
@click.stop
|
||||||
|
@keydown.enter.stop.prevent="saveApplicationDetailEdit(item)"
|
||||||
|
@keydown.esc.stop.prevent="cancelApplicationDetailEditor"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="application-detail-edit-confirm"
|
||||||
|
type="button"
|
||||||
|
title="保存"
|
||||||
|
aria-label="保存"
|
||||||
|
:disabled="applicationDetailEditor.saving"
|
||||||
|
@click.stop="saveApplicationDetailEdit(item)"
|
||||||
|
>
|
||||||
|
<i :class="applicationDetailEditor.saving ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-check'"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="application-detail-edit-cancel"
|
||||||
|
type="button"
|
||||||
|
title="取消"
|
||||||
|
aria-label="取消"
|
||||||
|
:disabled="applicationDetailEditor.saving"
|
||||||
|
@click.stop="cancelApplicationDetailEditor"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-close"></i>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="application-detail-fact-value">{{ item.value }}</span>
|
||||||
|
<button
|
||||||
|
v-if="canEditApplicationDetailItem(item)"
|
||||||
|
class="application-detail-edit-btn"
|
||||||
|
type="button"
|
||||||
|
title="编辑"
|
||||||
|
aria-label="编辑"
|
||||||
|
:disabled="actionBusy"
|
||||||
|
@click.stop="openApplicationDetailEditor(item)"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-pencil-outline"></i>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TravelRequestBudgetAnalysis
|
<TravelRequestBudgetAnalysis
|
||||||
@@ -458,16 +532,6 @@
|
|||||||
<i class="mdi mdi-trash-can-outline"></i>
|
<i class="mdi mdi-trash-can-outline"></i>
|
||||||
{{ deleteBusy ? '删除中' : deleteActionLabel }}
|
{{ deleteBusy ? '删除中' : deleteActionLabel }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
v-if="canModifyReturnedApplication"
|
|
||||||
class="secondary-action"
|
|
||||||
type="button"
|
|
||||||
:disabled="actionBusy"
|
|
||||||
@click="handleModifyApplication"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-pencil-outline"></i>
|
|
||||||
修改申请
|
|
||||||
</button>
|
|
||||||
<button class="approve-action" type="button" :disabled="!canSubmit" @click="handleSubmit">
|
<button class="approve-action" type="button" :disabled="!canSubmit" @click="handleSubmit">
|
||||||
<i :class="submitActionIcon"></i>
|
<i :class="submitActionIcon"></i>
|
||||||
{{ submitActionLabel }}
|
{{ submitActionLabel }}
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
initialDraftPayload: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
initialFiles: {
|
initialFiles: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
|||||||
|
|
||||||
import { useSystemState } from '../../composables/useSystemState.js'
|
import { useSystemState } from '../../composables/useSystemState.js'
|
||||||
import { useToast } from '../../composables/useToast.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 {
|
import {
|
||||||
canApproveBudgetExpenseApplications,
|
canApproveBudgetExpenseApplications,
|
||||||
canApproveLeaderExpenseClaims,
|
canApproveLeaderExpenseClaims,
|
||||||
@@ -23,6 +28,13 @@ import {
|
|||||||
import { buildRiskViewerContext } from '../../utils/riskVisibility.js'
|
import { buildRiskViewerContext } from '../../utils/riskVisibility.js'
|
||||||
import { resolveProgressStepsForViewer } from '../../utils/requestProgressViewer.js'
|
import { resolveProgressStepsForViewer } from '../../utils/requestProgressViewer.js'
|
||||||
import { isArchivedRequestView, normalizeRequestForUi } from '../../utils/requestViewModel.js'
|
import { isArchivedRequestView, normalizeRequestForUi } from '../../utils/requestViewModel.js'
|
||||||
|
import {
|
||||||
|
APPLICATION_TRANSPORT_MODE_OPTIONS,
|
||||||
|
applyApplicationPolicyEstimateError,
|
||||||
|
applyApplicationPolicyEstimateResult,
|
||||||
|
buildApplicationPolicyEstimateRequest,
|
||||||
|
normalizeApplicationPreview
|
||||||
|
} from '../../utils/expenseApplicationPreview.js'
|
||||||
import {
|
import {
|
||||||
EXPENSE_TYPE_OPTIONS,
|
EXPENSE_TYPE_OPTIONS,
|
||||||
buildFallbackExpenseItems,
|
buildFallbackExpenseItems,
|
||||||
@@ -43,6 +55,11 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
|||||||
const { currentUser } = useSystemState()
|
const { currentUser } = useSystemState()
|
||||||
const expenseItems = ref([])
|
const expenseItems = ref([])
|
||||||
const expenseAttachmentMeta = reactive({})
|
const expenseAttachmentMeta = reactive({})
|
||||||
|
const applicationDetailEditor = reactive({
|
||||||
|
fieldKey: '',
|
||||||
|
draftValue: '',
|
||||||
|
saving: false
|
||||||
|
})
|
||||||
const riskFlagPreviewSnapshot = ref(null)
|
const riskFlagPreviewSnapshot = ref(null)
|
||||||
let actionBusy = { value: false }
|
let actionBusy = { value: false }
|
||||||
const getActionBusy = () => Boolean(actionBusy?.value)
|
const getActionBusy = () => Boolean(actionBusy?.value)
|
||||||
@@ -92,11 +109,10 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
|||||||
const isEditableRequest = computed(() => ['draft', 'supplement'].includes(request.value.approvalKey))
|
const isEditableRequest = computed(() => ['draft', 'supplement'].includes(request.value.approvalKey))
|
||||||
const canOpenAiEntry = computed(() => isEditableRequest.value)
|
const canOpenAiEntry = computed(() => isEditableRequest.value)
|
||||||
const isCurrentApplicant = computed(() => isCurrentRequestApplicant(request.value, currentUser.value))
|
const isCurrentApplicant = computed(() => isCurrentRequestApplicant(request.value, currentUser.value))
|
||||||
const canModifyReturnedApplication = computed(() => (
|
const canModifyApplication = computed(() => (
|
||||||
isApplicationDocument.value
|
isApplicationDocument.value
|
||||||
&& isEditableRequest.value
|
&& isEditableRequest.value
|
||||||
&& isCurrentApplicant.value
|
&& isCurrentApplicant.value
|
||||||
&& String(request.value.status || '').trim().toLowerCase() === 'returned'
|
|
||||||
))
|
))
|
||||||
const canManageCurrentClaim = computed(() => canManageExpenseClaims(currentUser.value))
|
const canManageCurrentClaim = computed(() => canManageExpenseClaims(currentUser.value))
|
||||||
const isArchivedRequest = computed(() => isArchivedRequestView(request.value))
|
const isArchivedRequest = computed(() => isArchivedRequestView(request.value))
|
||||||
@@ -261,6 +277,7 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
|||||||
|| approvalFlow.returnBusy.value
|
|| approvalFlow.returnBusy.value
|
||||||
|| approvalFlow.approveBusy.value
|
|| approvalFlow.approveBusy.value
|
||||||
|| paymentFlow.payBusy.value
|
|| paymentFlow.payBusy.value
|
||||||
|
|| applicationDetailEditor.saving
|
||||||
|| smartEntryRecognitionBusy.value
|
|| smartEntryRecognitionBusy.value
|
||||||
|| Boolean(uploadingExpenseId.value)
|
|| Boolean(uploadingExpenseId.value)
|
||||||
|| Boolean(deletingAttachmentId.value)
|
|| Boolean(deletingAttachmentId.value)
|
||||||
@@ -350,6 +367,15 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
|||||||
)
|
)
|
||||||
const applicationDetailFactItems = computed(() => buildApplicationDetailFactItems(request.value))
|
const applicationDetailFactItems = computed(() => buildApplicationDetailFactItems(request.value))
|
||||||
const relatedApplicationFactItems = computed(() => buildRelatedApplicationFactItems(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(
|
watch(
|
||||||
request,
|
request,
|
||||||
@@ -366,6 +392,7 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
|||||||
attachmentPreview.closeAttachmentPreview()
|
attachmentPreview.closeAttachmentPreview()
|
||||||
}
|
}
|
||||||
expenseEditor.resetExpenseWorkState()
|
expenseEditor.resetExpenseWorkState()
|
||||||
|
cancelApplicationDetailEditor()
|
||||||
void attachmentPreview.syncExpenseAttachmentMeta()
|
void attachmentPreview.syncExpenseAttachmentMeta()
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@@ -403,6 +430,8 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
|||||||
return {
|
return {
|
||||||
sourceText: '修改申请',
|
sourceText: '修改申请',
|
||||||
modelReviewStatus: 'template',
|
modelReviewStatus: 'template',
|
||||||
|
applicationEditMode: true,
|
||||||
|
editableFields: applicationEditEditableFields,
|
||||||
fields: {
|
fields: {
|
||||||
applicationType: pickFact('申请类型') || request.value.typeLabel || '费用申请',
|
applicationType: pickFact('申请类型') || request.value.typeLabel || '费用申请',
|
||||||
applicant: request.value.profileName || request.value.person || request.value.applicant || '',
|
applicant: request.value.profileName || request.value.person || request.value.applicant || '',
|
||||||
@@ -424,27 +453,163 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleModifyApplication() {
|
function buildApplicationEditDraftPayload() {
|
||||||
if (!canModifyReturnedApplication.value) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const claimId = String(request.value?.claimId || '').trim()
|
applicationDetailEditor.saving = true
|
||||||
emit('openAssistant', {
|
try {
|
||||||
source: 'application',
|
const preview = await refreshEditedApplicationPreviewEstimate(buildEditedApplicationPreview(item))
|
||||||
sessionType: 'application',
|
const payload = await runAiApplicationPreviewAction({
|
||||||
prompt: '',
|
actionType: AI_APPLICATION_ACTION_SAVE_DRAFT,
|
||||||
applicationPreview: buildApplicationEditPreview(),
|
applicationPreview: preview,
|
||||||
request: {
|
currentUser: currentUser.value || {},
|
||||||
...request.value,
|
draftPayload: buildApplicationEditDraftPayload()
|
||||||
applicationEditMode: true
|
})
|
||||||
},
|
const draftPayload = payload?.result?.draft_payload || payload?.draft_payload || {}
|
||||||
restoreLatestConversation: false,
|
emit('request-updated', {
|
||||||
initialPromptAutoSubmit: false,
|
claimId: String(draftPayload.claim_id || request.value.claimId || '').trim(),
|
||||||
scope: claimId
|
claimNo: String(draftPayload.claim_no || request.value.claimNo || request.value.documentNo || '').trim(),
|
||||||
? { type: 'claim', claimId }
|
status: String(draftPayload.status || request.value.status || '').trim(),
|
||||||
: null
|
approvalStage: String(draftPayload.approval_stage || request.value.node || '').trim()
|
||||||
})
|
})
|
||||||
|
cancelApplicationDetailEditor()
|
||||||
|
toast('申请信息已更新。')
|
||||||
|
} catch (error) {
|
||||||
|
toast(error?.message || '申请信息更新失败,请稍后重试。')
|
||||||
|
} finally {
|
||||||
|
applicationDetailEditor.saving = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -461,22 +626,25 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
|||||||
...expenseEditor,
|
...expenseEditor,
|
||||||
...paymentFlow,
|
...paymentFlow,
|
||||||
...riskSubmit,
|
...riskSubmit,
|
||||||
|
APPLICATION_TRANSPORT_MODE_OPTIONS,
|
||||||
|
applicationDetailEditor,
|
||||||
applicationDetailFactItems,
|
applicationDetailFactItems,
|
||||||
relatedApplicationFactItems,
|
relatedApplicationFactItems,
|
||||||
|
canEditApplicationDetailItem,
|
||||||
canDeleteRequest,
|
canDeleteRequest,
|
||||||
canManageCurrentClaim,
|
canManageCurrentClaim,
|
||||||
canModifyReturnedApplication,
|
canModifyApplication,
|
||||||
canOpenAiEntry,
|
canOpenAiEntry,
|
||||||
canApproveRequest,
|
canApproveRequest,
|
||||||
canReturnRequest,
|
canReturnRequest,
|
||||||
currentProgressRingMotion,
|
currentProgressRingMotion,
|
||||||
expenseItems,
|
expenseItems,
|
||||||
expenseTypeOptions: EXPENSE_TYPE_OPTIONS,
|
expenseTypeOptions: EXPENSE_TYPE_OPTIONS,
|
||||||
handleModifyApplication,
|
|
||||||
hasLeaderApprovalEvents,
|
hasLeaderApprovalEvents,
|
||||||
hasSingleLeaderApprovalEvent,
|
hasSingleLeaderApprovalEvent,
|
||||||
heroFactItems,
|
heroFactItems,
|
||||||
isApplicationDocument,
|
isApplicationDocument,
|
||||||
|
isApplicationDetailEditing,
|
||||||
isDraftRequest,
|
isDraftRequest,
|
||||||
isEditableRequest,
|
isEditableRequest,
|
||||||
isTravelRequest,
|
isTravelRequest,
|
||||||
@@ -485,8 +653,12 @@ export function useTravelRequestDetailSetup(props, { emit }) {
|
|||||||
profile,
|
profile,
|
||||||
progressSteps,
|
progressSteps,
|
||||||
request,
|
request,
|
||||||
|
cancelApplicationDetailEditor,
|
||||||
|
openApplicationDetailEditor,
|
||||||
resolveExpenseReasonHelper,
|
resolveExpenseReasonHelper,
|
||||||
resolveExpenseReasonPlaceholder,
|
resolveExpenseReasonPlaceholder,
|
||||||
|
resolveApplicationDetailEditorControl,
|
||||||
|
saveApplicationDetailEdit,
|
||||||
showApplicationLeaderOpinion,
|
showApplicationLeaderOpinion,
|
||||||
showBudgetAnalysis,
|
showBudgetAnalysis,
|
||||||
showStageRiskAdvice,
|
showStageRiskAdvice,
|
||||||
|
|||||||
@@ -221,9 +221,13 @@ export function useTravelReimbursementCreateViewLifecycle({
|
|||||||
|| buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value, currentUser.value)
|
|| buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value, currentUser.value)
|
||||||
if (props.initialApplicationPreview && typeof props.initialApplicationPreview === 'object') {
|
if (props.initialApplicationPreview && typeof props.initialApplicationPreview === 'object') {
|
||||||
const applicationPreview = normalizeApplicationPreview(props.initialApplicationPreview)
|
const applicationPreview = normalizeApplicationPreview(props.initialApplicationPreview)
|
||||||
|
const draftPayload = props.initialDraftPayload && typeof props.initialDraftPayload === 'object'
|
||||||
|
? props.initialDraftPayload
|
||||||
|
: null
|
||||||
messages.value.push(createMessage('assistant', buildLocalApplicationPreviewMessage(applicationPreview), [], {
|
messages.value.push(createMessage('assistant', buildLocalApplicationPreviewMessage(applicationPreview), [], {
|
||||||
meta: ['修改申请'],
|
meta: ['修改申请'],
|
||||||
applicationPreview
|
applicationPreview,
|
||||||
|
draftPayload
|
||||||
}))
|
}))
|
||||||
persistSessionState()
|
persistSessionState()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,10 +182,13 @@ test('application entry keeps its own assistant source without creating a separa
|
|||||||
test('application edit prefill opens assistant without auto submit', () => {
|
test('application edit prefill opens assistant without auto submit', () => {
|
||||||
assert.match(appShellRouteView, /:initial-prompt-auto-submit="smartEntryContext\.initialPromptAutoSubmit"/)
|
assert.match(appShellRouteView, /:initial-prompt-auto-submit="smartEntryContext\.initialPromptAutoSubmit"/)
|
||||||
assert.match(appShellRouteView, /:initial-application-preview="smartEntryContext\.initialApplicationPreview"/)
|
assert.match(appShellRouteView, /:initial-application-preview="smartEntryContext\.initialApplicationPreview"/)
|
||||||
|
assert.match(appShellRouteView, /:initial-draft-payload="smartEntryContext\.initialDraftPayload"/)
|
||||||
assert.match(appShellComposable, /initialPromptAutoSubmit:\s*true/)
|
assert.match(appShellComposable, /initialPromptAutoSubmit:\s*true/)
|
||||||
assert.match(appShellComposable, /initialApplicationPreview:\s*null/)
|
assert.match(appShellComposable, /initialApplicationPreview:\s*null/)
|
||||||
|
assert.match(appShellComposable, /initialDraftPayload:\s*null/)
|
||||||
assert.match(appShellComposable, /initialPromptAutoSubmit:\s*payload\.initialPromptAutoSubmit !== false/)
|
assert.match(appShellComposable, /initialPromptAutoSubmit:\s*payload\.initialPromptAutoSubmit !== false/)
|
||||||
assert.match(appShellComposable, /initialApplicationPreview:\s*payload\.applicationPreview && typeof payload\.applicationPreview === 'object'/)
|
assert.match(appShellComposable, /initialApplicationPreview:\s*payload\.applicationPreview && typeof payload\.applicationPreview === 'object'/)
|
||||||
|
assert.match(appShellComposable, /initialDraftPayload:\s*payload\.draftPayload && typeof payload\.draftPayload === 'object'/)
|
||||||
assert.match(
|
assert.match(
|
||||||
assistantScript,
|
assistantScript,
|
||||||
/initialPromptAutoSubmit:\s*\{[\s\S]*type:\s*Boolean[\s\S]*default:\s*true/
|
/initialPromptAutoSubmit:\s*\{[\s\S]*type:\s*Boolean[\s\S]*default:\s*true/
|
||||||
@@ -194,9 +197,13 @@ test('application edit prefill opens assistant without auto submit', () => {
|
|||||||
assistantScript,
|
assistantScript,
|
||||||
/initialApplicationPreview:\s*\{[\s\S]*type:\s*Object[\s\S]*default:\s*null/
|
/initialApplicationPreview:\s*\{[\s\S]*type:\s*Object[\s\S]*default:\s*null/
|
||||||
)
|
)
|
||||||
|
assert.match(
|
||||||
|
assistantScript,
|
||||||
|
/initialDraftPayload:\s*\{[\s\S]*type:\s*Object[\s\S]*default:\s*null/
|
||||||
|
)
|
||||||
assert.match(
|
assert.match(
|
||||||
assistantSurface,
|
assistantSurface,
|
||||||
/props\.initialApplicationPreview[\s\S]*normalizeApplicationPreview\(props\.initialApplicationPreview\)[\s\S]*createMessage\('assistant', buildLocalApplicationPreviewMessage\(applicationPreview\)/
|
/props\.initialApplicationPreview[\s\S]*normalizeApplicationPreview\(props\.initialApplicationPreview\)[\s\S]*const draftPayload = props\.initialDraftPayload[\s\S]*createMessage\('assistant', buildLocalApplicationPreviewMessage\(applicationPreview\)[\s\S]*draftPayload/
|
||||||
)
|
)
|
||||||
assert.match(assistantSessionStateScript, /&& !props\.initialApplicationPreview/)
|
assert.match(assistantSessionStateScript, /&& !props\.initialApplicationPreview/)
|
||||||
assert.match(
|
assert.match(
|
||||||
|
|||||||
@@ -1593,27 +1593,35 @@ test('application detail uses application labels instead of reimbursement labels
|
|||||||
assert.match(detailViewTemplate, /当前申请单已进入流程,详情页仅展示状态与申请信息。/)
|
assert.match(detailViewTemplate, /当前申请单已进入流程,详情页仅展示状态与申请信息。/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('returned application detail can open assistant with editable prefill', () => {
|
test('draft or returned application detail edits allowed facts inline', () => {
|
||||||
assert.match(
|
assert.doesNotMatch(detailViewTemplate, /修改申请/)
|
||||||
detailViewTemplate,
|
assert.match(detailViewTemplate, /canEditApplicationDetailItem\(item\)/)
|
||||||
/v-if="canModifyReturnedApplication"[\s\S]*@click="handleModifyApplication"[\s\S]*修改申请/
|
assert.match(detailViewTemplate, /application-detail-edit-btn/)
|
||||||
)
|
assert.match(detailViewTemplate, /openApplicationDetailEditor\(item\)/)
|
||||||
|
assert.match(detailViewTemplate, /saveApplicationDetailEdit\(item\)/)
|
||||||
|
assert.doesNotMatch(detailViewScript, /handleModifyApplication/)
|
||||||
assert.match(
|
assert.match(
|
||||||
detailViewScript,
|
detailViewScript,
|
||||||
/const canModifyReturnedApplication = computed\(\(\) => \([\s\S]*isApplicationDocument\.value[\s\S]*isCurrentApplicant\.value[\s\S]*returned/
|
/const canModifyApplication = computed\(\(\) => \([\s\S]*isApplicationDocument\.value[\s\S]*isEditableRequest\.value[\s\S]*isCurrentApplicant\.value[\s\S]*\)\)/
|
||||||
)
|
)
|
||||||
assert.match(detailViewScript, /function buildApplicationEditPreview\(\)/)
|
assert.match(detailViewScript, /function buildApplicationEditPreview\(\)/)
|
||||||
assert.match(detailViewScript, /applicationDetailFactItems\.value[\s\S]*sourceText:\s*'修改申请'/)
|
assert.match(detailViewScript, /applicationDetailFactItems\.value[\s\S]*sourceText:\s*'修改申请'/)
|
||||||
assert.match(detailViewScript, /fields:\s*\{[\s\S]*applicationType:[\s\S]*reason:[\s\S]*transportMode:/)
|
|
||||||
assert.match(detailViewScript, /function handleModifyApplication\(\)/)
|
|
||||||
assert.match(detailViewScript, /source:\s*'application'/)
|
|
||||||
assert.match(detailViewScript, /sessionType:\s*'application'/)
|
|
||||||
assert.match(detailViewScript, /prompt:\s*''/)
|
|
||||||
assert.match(detailViewScript, /applicationPreview:\s*buildApplicationEditPreview\(\)/)
|
|
||||||
assert.match(detailViewScript, /applicationEditMode:\s*true/)
|
assert.match(detailViewScript, /applicationEditMode:\s*true/)
|
||||||
assert.match(detailViewScript, /initialPromptAutoSubmit:\s*false/)
|
assert.match(detailViewScript, /editableFields:\s*applicationEditEditableFields/)
|
||||||
assert.match(detailViewScript, /canModifyReturnedApplication,/)
|
assert.match(detailViewScript, /fields:\s*\{[\s\S]*applicationType:[\s\S]*reason:[\s\S]*transportMode:/)
|
||||||
assert.match(detailViewScript, /handleModifyApplication,/)
|
assert.match(detailViewScript, /function buildApplicationEditDraftPayload\(\)/)
|
||||||
|
assert.match(detailViewScript, /draft_type:\s*'expense_application'/)
|
||||||
|
assert.match(detailViewScript, /claim_id:\s*claimId/)
|
||||||
|
assert.match(detailViewScript, /application_edit_mode:\s*true/)
|
||||||
|
assert.match(detailViewScript, /function canEditApplicationDetailItem\(item = \{\}\)/)
|
||||||
|
assert.match(detailViewScript, /function openApplicationDetailEditor\(item = \{\}\)/)
|
||||||
|
assert.match(detailViewScript, /async function saveApplicationDetailEdit\(item = \{\}\)/)
|
||||||
|
assert.match(detailViewScript, /runAiApplicationPreviewAction\(\{[\s\S]*AI_APPLICATION_ACTION_SAVE_DRAFT/)
|
||||||
|
assert.match(detailViewScript, /emit\('request-updated'/)
|
||||||
|
assert.match(detailViewScript, /canModifyApplication,/)
|
||||||
|
assert.match(detailViewScript, /canEditApplicationDetailItem,/)
|
||||||
|
assert.match(detailViewScript, /openApplicationDetailEditor,/)
|
||||||
|
assert.match(detailViewScript, /saveApplicationDetailEdit,/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('application detail does not show optional travel receipt reminders', () => {
|
test('application detail does not show optional travel receipt reminders', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user