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:
caoxiaozhu
2026-06-26 22:42:29 +08:00
parent c4b5fcc067
commit d5a8f84703
9 changed files with 396 additions and 58 deletions

View File

@@ -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;
} }

View File

@@ -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

View File

@@ -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"

View File

@@ -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 }}

View File

@@ -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: () => []

View File

@@ -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
},
restoreLatestConversation: false,
initialPromptAutoSubmit: false,
scope: claimId
? { type: 'claim', claimId }
: null
}) })
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(() => { 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,

View File

@@ -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()
} }

View File

@@ -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(

View File

@@ -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', () => {