feat(web): 申请单预览编辑器增强与报销流程细节适配
- useApplicationPreviewEditor 扩展字段编辑与校验,useTravelReimbursementApplicationPreviewDateEditor 微调日期处理 - travelReimbursementExpenseQueryModel/reimbursements 服务/expenseApplicationPreview 适配工号/邮箱字段与关联动作 - useWorkbenchAiApplicationPreviewFlow/usePersonalWorkbenchAiMode 接入关联门控后的预览流转 - TravelReimbursementCreateView 调整入口,TravelReimbursementMessageItem 适配 - 新增 expense-application-fast-preview 测试,更新 attachment-association-confirmation、review-drawer-switch 测试
This commit is contained in:
@@ -207,7 +207,7 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<span
|
<span
|
||||||
class="application-preview-text"
|
class="application-preview-text"
|
||||||
:class="{ 'application-preview-date-chip': row.key === 'time' && !row.missing }"
|
:class="{ 'application-preview-date-chip': ['time', 'time_return'].includes(row.key) && !row.missing }"
|
||||||
>{{ row.value }}</span>
|
>{{ row.value }}</span>
|
||||||
<button
|
<button
|
||||||
v-if="row.editable"
|
v-if="row.editable"
|
||||||
@@ -276,7 +276,7 @@
|
|||||||
|
|
||||||
<Transition name="structured-card-reveal" appear>
|
<Transition name="structured-card-reveal" appear>
|
||||||
<div
|
<div
|
||||||
v-if="message.role === 'assistant' && !message.reviewPayload && !message.queryPayload && message.suggestedActions?.length"
|
v-if="message.role === 'assistant' && !message.reviewPayload && (!message.queryPayload || message.queryPayload.selectionMode === 'reimbursement_application_association') && message.suggestedActions?.length"
|
||||||
class="message-suggested-actions"
|
class="message-suggested-actions"
|
||||||
:class="{ 'compact-guidance-actions': message.assistantVariant === 'compact_guidance' }"
|
:class="{ 'compact-guidance-actions': message.assistantVariant === 'compact_guidance' }"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
|||||||
currentUser,
|
currentUser,
|
||||||
persistCurrentConversation,
|
persistCurrentConversation,
|
||||||
pushInlineUserMessage,
|
pushInlineUserMessage,
|
||||||
|
replaceInlineMessage,
|
||||||
removeWorkbenchDateTag,
|
removeWorkbenchDateTag,
|
||||||
resolveLatestInlineUserPrompt,
|
resolveLatestInlineUserPrompt,
|
||||||
scrollInlineConversationToBottom,
|
scrollInlineConversationToBottom,
|
||||||
@@ -565,6 +566,20 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
|||||||
return files.length ? '请帮我处理已上传的附件。' : ''
|
return files.length ? '请帮我处理已上传的附件。' : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isReimbursementCreationIntent(prompt = '') {
|
||||||
|
const compact = String(prompt || '').replace(/\s+/g, '')
|
||||||
|
if (!compact || !/报销|报账/.test(compact)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (/标准|制度|规则|政策|口径|怎么|如何|能不能|可以吗|查一下|查询|状态|进度|多少钱|多少/.test(compact)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
/^(我要|我想|我需要|帮我|请帮我|需要|发起|新建|创建|提交|办理|走).{0,16}(报销|报账)/.test(compact) ||
|
||||||
|
/^(报销|报账)(一下|一笔|单|流程)?$/.test(compact)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function handleAiAnswerMarkdownClick(event) {
|
function handleAiAnswerMarkdownClick(event) {
|
||||||
const target = event?.target
|
const target = event?.target
|
||||||
const link = target?.closest?.('a[href^="#ai-open-document-detail:"], a[href^="#ai-open-application-detail:"]')
|
const link = target?.closest?.('a[href^="#ai-open-document-detail:"], a[href^="#ai-open-application-detail:"]')
|
||||||
@@ -600,6 +615,11 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isReimbursementCreationIntent(cleanPrompt)) {
|
||||||
|
void expenseFlow.startAiReimbursementAssociationGate(cleanPrompt, entry.label || '我要报销')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (conversationId.value === AI_SEARCH_CONVERSATION_ID) {
|
if (conversationId.value === AI_SEARCH_CONVERSATION_ID) {
|
||||||
conversationId.value = ''
|
conversationId.value = ''
|
||||||
conversationMessages.value = []
|
conversationMessages.value = []
|
||||||
@@ -636,7 +656,7 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
|||||||
|
|
||||||
function runAiModeAction(item) {
|
function runAiModeAction(item) {
|
||||||
if (String(item?.label || '').trim() === '发起报销') {
|
if (String(item?.label || '').trim() === '发起报销') {
|
||||||
expenseFlow.pushInlineExpenseSceneSelectionPrompt(item.prompt, item.label)
|
void expenseFlow.startAiReimbursementAssociationGate(item.prompt, item.label)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
startInlineConversation(item.prompt, item, Array.from(selectedFiles.value))
|
startInlineConversation(item.prompt, item, Array.from(selectedFiles.value))
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function isApplicationPreviewEstimatePendingPreview(applicationPreview = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shouldRefreshInlineApplicationPreviewEstimate(fieldKey = '') {
|
function shouldRefreshInlineApplicationPreviewEstimate(fieldKey = '') {
|
||||||
return ['transportMode', 'time', 'location', 'days'].includes(String(fieldKey || '').trim())
|
return ['transportMode', 'time', 'time_return', 'location', 'days'].includes(String(fieldKey || '').trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkbenchAiApplicationPreviewFlow({
|
export function useWorkbenchAiApplicationPreviewFlow({
|
||||||
|
|||||||
@@ -72,24 +72,24 @@ async function fetchAllExpenseClaimPages(fetchPage, params = {}) {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchExpenseClaims(params = {}) {
|
export function fetchExpenseClaims(params = {}, options = {}) {
|
||||||
return apiRequest(`/reimbursements/claims${buildListQuery(params)}`)
|
return apiRequest(`/reimbursements/claims${buildListQuery(params)}`, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchAllExpenseClaims(params = {}) {
|
export function fetchAllExpenseClaims(params = {}) {
|
||||||
return fetchAllExpenseClaimPages(fetchExpenseClaims, params)
|
return fetchAllExpenseClaimPages(fetchExpenseClaims, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchApprovalExpenseClaims(params = {}) {
|
export function fetchApprovalExpenseClaims(params = {}, options = {}) {
|
||||||
return apiRequest(`/reimbursements/claims/approvals${buildListQuery(params)}`)
|
return apiRequest(`/reimbursements/claims/approvals${buildListQuery(params)}`, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchAllApprovalExpenseClaims(params = {}) {
|
export function fetchAllApprovalExpenseClaims(params = {}) {
|
||||||
return fetchAllExpenseClaimPages(fetchApprovalExpenseClaims, params)
|
return fetchAllExpenseClaimPages(fetchApprovalExpenseClaims, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchArchivedExpenseClaims(params = {}) {
|
export function fetchArchivedExpenseClaims(params = {}, options = {}) {
|
||||||
return apiRequest(`/reimbursements/claims/archives${buildListQuery(params)}`)
|
return apiRequest(`/reimbursements/claims/archives${buildListQuery(params)}`, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchAllArchivedExpenseClaims(params = {}) {
|
export function fetchAllArchivedExpenseClaims(params = {}) {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export {
|
|||||||
export function buildApplicationPolicyEstimateRequest(preview = {}, currentUser = {}) {
|
export function buildApplicationPolicyEstimateRequest(preview = {}, currentUser = {}) {
|
||||||
const normalized = normalizeApplicationPreview(preview)
|
const normalized = normalizeApplicationPreview(preview)
|
||||||
const fields = normalized.fields || {}
|
const fields = normalized.fields || {}
|
||||||
const days = parseApplicationDaysValue(fields.days)
|
const days = parseApplicationDaysValue(fields.days) || parseApplicationDaysValue(resolveDaysFromDateRange(fields.time))
|
||||||
const location = String(fields.location || '').trim()
|
const location = String(fields.location || '').trim()
|
||||||
const grade = String(fields.grade || resolveCurrentUserGrade(currentUser)).trim()
|
const grade = String(fields.grade || resolveCurrentUserGrade(currentUser)).trim()
|
||||||
const applicationType = String(fields.applicationType || '').trim()
|
const applicationType = String(fields.applicationType || '').trim()
|
||||||
@@ -112,6 +112,7 @@ export function applyApplicationPolicyEstimateResult(preview = {}, result = {},
|
|||||||
fields: {
|
fields: {
|
||||||
...fields,
|
...fields,
|
||||||
grade,
|
grade,
|
||||||
|
days: parseApplicationDaysValue(fields.days) ? fields.days : `${days}天`,
|
||||||
lodgingDailyCap: formatDailyPolicyMoney(result?.hotel_rate),
|
lodgingDailyCap: formatDailyPolicyMoney(result?.hotel_rate),
|
||||||
subsidyDailyCap: formatDailyPolicyMoney(result?.total_allowance_rate),
|
subsidyDailyCap: formatDailyPolicyMoney(result?.total_allowance_rate),
|
||||||
transportPolicy: APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT,
|
transportPolicy: APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT,
|
||||||
@@ -174,6 +175,7 @@ export function applyApplicationPolicyEstimateResult(preview = {}, result = {},
|
|||||||
fields: {
|
fields: {
|
||||||
...fields,
|
...fields,
|
||||||
grade,
|
grade,
|
||||||
|
days: parseApplicationDaysValue(fields.days) ? fields.days : `${days}天`,
|
||||||
lodgingDailyCap: formatDailyPolicyMoney(result?.hotel_rate),
|
lodgingDailyCap: formatDailyPolicyMoney(result?.hotel_rate),
|
||||||
subsidyDailyCap: formatDailyPolicyMoney(result?.total_allowance_rate),
|
subsidyDailyCap: formatDailyPolicyMoney(result?.total_allowance_rate),
|
||||||
transportPolicy: buildTransportPolicyText(fields.transportMode, matchedCity || fields.location, transportEstimate, fields.time),
|
transportPolicy: buildTransportPolicyText(fields.transportMode, matchedCity || fields.location, transportEstimate, fields.time),
|
||||||
@@ -388,7 +390,7 @@ export function buildApplicationPreviewRows(preview = {}) {
|
|||||||
key: 'time_return',
|
key: 'time_return',
|
||||||
label: '返回时间',
|
label: '返回时间',
|
||||||
value: tripDates.endDate || '待补充',
|
value: tripDates.endDate || '待补充',
|
||||||
editable: false,
|
editable: item.editable !== false,
|
||||||
highlight: Boolean(item.highlight),
|
highlight: Boolean(item.highlight),
|
||||||
missing
|
missing
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { continueStewardApplicationFieldCompletion, handleSuggestedAction, isSuggestedActionSelected, runShortcut } = useTravelReimbursementSuggestedActions({
|
const { continueStewardApplicationFieldCompletion, handleSuggestedAction, isSuggestedActionSelected, runShortcut } = useTravelReimbursementSuggestedActions({
|
||||||
applicationPreviewEditor, attachedFiles, buildExpenseSceneSelectionActions, buildExpenseSceneSelectionMessage, commitApplicationPreviewEditor, composerDraft, composerFilesExpanded, composerTextareaRef, createMessage, currentUser, emit, handleGuidedShortcut, handleGuidedSuggestedAction, handleSceneSelectionApplicationGate, lockSuggestedActionMessage, mergeFilesWithLimit, messages, nextTick, openApplicationPreviewEditor: openApplicationPreviewEditorFromUi, persistSessionState, resolveApplicationPreviewMissingFields, reviewActionBusy, router, scrollToBottom, sessionSwitchBusy, startExpenseSceneSelectionAfterIntentConfirmation, submitComposer, submitComposerInternal, submitting, switchSessionType, toast, adjustComposerTextareaHeight
|
applicationPreviewEditor, attachedFiles, buildExpenseSceneSelectionActions, buildExpenseSceneSelectionMessage, commitApplicationPreviewEditor, composerDraft, composerFilesExpanded, composerTextareaRef, createMessage, currentUser, emit, fetchExpenseClaims, handleGuidedShortcut, handleGuidedSuggestedAction, handleSceneSelectionApplicationGate, lockSuggestedActionMessage, mergeFilesWithLimit, messages, nextTick, openApplicationPreviewEditor: openApplicationPreviewEditorFromUi, persistSessionState, resolveApplicationPreviewMissingFields, reviewActionBusy, router, scrollToBottom, sessionSwitchBusy, startExpenseSceneSelectionAfterIntentConfirmation, submitComposer, submitComposerInternal, submitting, switchSessionType, toast, adjustComposerTextareaHeight
|
||||||
})
|
})
|
||||||
const {
|
const {
|
||||||
canShowTravelCalculator,
|
canShowTravelCalculator,
|
||||||
|
|||||||
@@ -247,6 +247,9 @@ export function buildExpenseQueryWindowLabel(queryPayload) {
|
|||||||
if (queryPayload.selectionMode === 'draft_association') {
|
if (queryPayload.selectionMode === 'draft_association') {
|
||||||
return '先选择要关联的草稿,确认后我再识别附件并归集到该单据。'
|
return '先选择要关联的草稿,确认后我再识别附件并归集到该单据。'
|
||||||
}
|
}
|
||||||
|
if (queryPayload.selectionMode === 'reimbursement_application_association') {
|
||||||
|
return '先确认是否关联申请单;选择关联后我会用申请单信息生成报销草稿。'
|
||||||
|
}
|
||||||
|
|
||||||
if (queryPayload.windowStartDate && queryPayload.windowEndDate) {
|
if (queryPayload.windowStartDate && queryPayload.windowEndDate) {
|
||||||
return `${queryPayload.windowStartDate} 至 ${queryPayload.windowEndDate}`
|
return `${queryPayload.windowStartDate} 至 ${queryPayload.windowEndDate}`
|
||||||
|
|||||||
@@ -17,9 +17,15 @@ import {
|
|||||||
getTodayDateValue
|
getTodayDateValue
|
||||||
} from '../../utils/workbenchComposerDate.js'
|
} from '../../utils/workbenchComposerDate.js'
|
||||||
|
|
||||||
function parseEditorDateValue(value) {
|
function parseEditorDateMatches(value) {
|
||||||
const text = String(value || '').trim()
|
const text = String(value || '').trim()
|
||||||
const matches = [...text.matchAll(/20\d{2}-\d{1,2}-\d{1,2}/g)].map((item) => item[0])
|
return [...text.matchAll(/20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/g)]
|
||||||
|
.map((item) => normalizeEditorIsoDate(item[0]))
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEditorDateValue(value) {
|
||||||
|
const matches = parseEditorDateMatches(value)
|
||||||
const startDate = matches[0] || getTodayDateValue()
|
const startDate = matches[0] || getTodayDateValue()
|
||||||
const endDate = matches[1] || startDate
|
const endDate = matches[1] || startDate
|
||||||
return {
|
return {
|
||||||
@@ -30,6 +36,113 @@ function parseEditorDateValue(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isApplicationPreviewDateField(fieldKey = '') {
|
||||||
|
return ['time', 'time_return'].includes(String(fieldKey || '').trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatApplicationPreviewDateRange(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 normalizeEditorIsoDate(value = '') {
|
||||||
|
const match = String(value || '').trim().match(/^(20\d{2})[-/.](\d{1,2})[-/.](\d{1,2})$/)
|
||||||
|
if (!match) return ''
|
||||||
|
return buildEditorIsoDate(Number(match[1]), Number(match[2]), Number(match[3]))
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEditorIsoDate(year, month, day) {
|
||||||
|
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return ''
|
||||||
|
const date = new Date(Date.UTC(year, month - 1, day))
|
||||||
|
if (
|
||||||
|
Number.isNaN(date.getTime()) ||
|
||||||
|
date.getUTCFullYear() !== year ||
|
||||||
|
date.getUTCMonth() + 1 !== month ||
|
||||||
|
date.getUTCDate() !== day
|
||||||
|
) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
String(year).padStart(4, '0'),
|
||||||
|
String(month).padStart(2, '0'),
|
||||||
|
String(day).padStart(2, '0')
|
||||||
|
].join('-')
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEditorDateParts(value = '') {
|
||||||
|
const normalized = normalizeEditorIsoDate(value)
|
||||||
|
const match = normalized.match(/^(20\d{2})-(\d{2})-(\d{2})$/)
|
||||||
|
if (!match) return null
|
||||||
|
return {
|
||||||
|
year: Number(match[1]),
|
||||||
|
month: Number(match[2]),
|
||||||
|
day: Number(match[3]),
|
||||||
|
value: normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDaysToEditorDate(value = '', offset = 0) {
|
||||||
|
const parts = parseEditorDateParts(value)
|
||||||
|
if (!parts) return ''
|
||||||
|
const date = new Date(Date.UTC(parts.year, parts.month - 1, parts.day))
|
||||||
|
date.setUTCDate(date.getUTCDate() + offset)
|
||||||
|
return buildEditorIsoDate(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate())
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEditorDateFromMonthDay(referenceDate = '', month = 0, day = 0) {
|
||||||
|
const reference = parseEditorDateParts(referenceDate) || parseEditorDateParts(getTodayDateValue())
|
||||||
|
if (!reference) return ''
|
||||||
|
const year = month < reference.month ? reference.year + 1 : reference.year
|
||||||
|
return buildEditorIsoDate(year, month, day)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEditorDateFromDay(referenceDate = '', day = 0) {
|
||||||
|
const reference = parseEditorDateParts(referenceDate) || parseEditorDateParts(getTodayDateValue())
|
||||||
|
if (!reference) return ''
|
||||||
|
let year = reference.year
|
||||||
|
let month = reference.month
|
||||||
|
let candidate = buildEditorIsoDate(year, month, day)
|
||||||
|
if (candidate && candidate < reference.value) {
|
||||||
|
month += 1
|
||||||
|
if (month > 12) {
|
||||||
|
month = 1
|
||||||
|
year += 1
|
||||||
|
}
|
||||||
|
candidate = buildEditorIsoDate(year, month, day)
|
||||||
|
}
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEditorDateInput(value = '', referenceDate = '') {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) return ''
|
||||||
|
|
||||||
|
const fullDate = text.match(/20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/)
|
||||||
|
if (fullDate) {
|
||||||
|
return normalizeEditorIsoDate(fullDate[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthDay = text.match(/(\d{1,2})\s*月\s*(\d{1,2})\s*日?/) || text.match(/(\d{1,2})[/-](\d{1,2})/)
|
||||||
|
if (monthDay) {
|
||||||
|
return buildEditorDateFromMonthDay(referenceDate, Number(monthDay[1]), Number(monthDay[2]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayOnly = text.match(/^(\d{1,2})\s*日?$/)
|
||||||
|
if (dayOnly) {
|
||||||
|
return buildEditorDateFromDay(referenceDate, Number(dayOnly[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/大后天/.test(text)) return addDaysToEditorDate(referenceDate || getTodayDateValue(), 3)
|
||||||
|
if (/后天/.test(text)) return addDaysToEditorDate(referenceDate || getTodayDateValue(), 2)
|
||||||
|
if (/明天/.test(text)) return addDaysToEditorDate(referenceDate || getTodayDateValue(), 1)
|
||||||
|
if (/今天/.test(text)) return normalizeEditorIsoDate(referenceDate || getTodayDateValue())
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
function buildEmptyEditor() {
|
function buildEmptyEditor() {
|
||||||
return {
|
return {
|
||||||
messageId: '',
|
messageId: '',
|
||||||
@@ -44,7 +157,7 @@ function buildEmptyEditor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shouldRefreshTransportEstimate(fieldKey) {
|
function shouldRefreshTransportEstimate(fieldKey) {
|
||||||
return ['transportMode', 'time', 'location', 'days'].includes(fieldKey)
|
return ['transportMode', 'time', 'time_return', 'location', 'days'].includes(fieldKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveEditorCurrentUser(currentUser) {
|
function resolveEditorCurrentUser(currentUser) {
|
||||||
@@ -55,11 +168,12 @@ function resolveEditorCurrentUser(currentUser) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildEditedApplicationPreviewFields(fields = {}, editor = {}, nextValue = '') {
|
function buildEditedApplicationPreviewFields(fields = {}, editor = {}, nextValue = '') {
|
||||||
|
const isDateField = isApplicationPreviewDateField(editor.fieldKey)
|
||||||
const nextFields = {
|
const nextFields = {
|
||||||
...fields,
|
...fields,
|
||||||
[editor.fieldKey]: nextValue
|
[isDateField ? 'time' : editor.fieldKey]: nextValue
|
||||||
}
|
}
|
||||||
if (editor.fieldKey === 'time') {
|
if (isDateField) {
|
||||||
const resolvedDays = resolveApplicationDaysFromDateRange(nextValue)
|
const resolvedDays = resolveApplicationDaysFromDateRange(nextValue)
|
||||||
if (resolvedDays) {
|
if (resolvedDays) {
|
||||||
nextFields.days = resolvedDays
|
nextFields.days = resolvedDays
|
||||||
@@ -68,6 +182,45 @@ function buildEditedApplicationPreviewFields(fields = {}, editor = {}, nextValue
|
|||||||
return nextFields
|
return nextFields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildApplicationPreviewDateSelectionValue(editor = {}) {
|
||||||
|
return buildWorkbenchDateLabel({
|
||||||
|
mode: editor.dateMode,
|
||||||
|
singleDate: editor.singleDate,
|
||||||
|
rangeStartDate: editor.rangeStartDate,
|
||||||
|
rangeEndDate: editor.rangeEndDate
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApplicationPreviewTextDateValue(fields = {}, editor = {}) {
|
||||||
|
const currentDateState = parseEditorDateValue(fields.time)
|
||||||
|
const referenceDate = currentDateState.rangeStartDate || currentDateState.singleDate || getTodayDateValue()
|
||||||
|
const typedDates = parseEditorDateMatches(editor.draftValue)
|
||||||
|
if (typedDates.length >= 2) {
|
||||||
|
return formatApplicationPreviewDateRange(typedDates[0], typedDates[typedDates.length - 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
const typedDate = typedDates[0] || normalizeEditorDateInput(editor.draftValue, referenceDate) || String(editor.draftValue || '').trim()
|
||||||
|
if (!typedDate) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editor.fieldKey === 'time_return') {
|
||||||
|
return formatApplicationPreviewDateRange(currentDateState.rangeStartDate, typedDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentEndDate = currentDateState.rangeEndDate || typedDate
|
||||||
|
return formatApplicationPreviewDateRange(typedDate, currentEndDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApplicationPreviewEditorValue(fields = {}, editor = {}, options = {}) {
|
||||||
|
if (!isApplicationPreviewDateField(editor.fieldKey)) {
|
||||||
|
return String(editor.draftValue || '').trim()
|
||||||
|
}
|
||||||
|
return options.useDateSelection
|
||||||
|
? buildApplicationPreviewDateSelectionValue(editor)
|
||||||
|
: buildApplicationPreviewTextDateValue(fields, editor)
|
||||||
|
}
|
||||||
|
|
||||||
function buildTransportEstimatePendingPreview(preview = {}) {
|
function buildTransportEstimatePendingPreview(preview = {}) {
|
||||||
const fields = preview?.fields || {}
|
const fields = preview?.fields || {}
|
||||||
return normalizeApplicationPreview({
|
return normalizeApplicationPreview({
|
||||||
@@ -110,7 +263,7 @@ export function useApplicationPreviewEditor({
|
|||||||
|
|
||||||
function resolveApplicationPreviewEditorControl(fieldKey) {
|
function resolveApplicationPreviewEditorControl(fieldKey) {
|
||||||
if (fieldKey === 'transportMode') return 'select'
|
if (fieldKey === 'transportMode') return 'select'
|
||||||
if (fieldKey === 'time') return 'date'
|
if (isApplicationPreviewDateField(fieldKey)) return 'date'
|
||||||
return 'text'
|
return 'text'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +284,10 @@ export function useApplicationPreviewEditor({
|
|||||||
.find((row) => row.key === fieldKey)
|
.find((row) => row.key === fieldKey)
|
||||||
if (targetRow && targetRow.editable === false) return
|
if (targetRow && targetRow.editable === false) return
|
||||||
const normalizedValue = String(value || '').trim() === '待补充' ? '' : String(value || '')
|
const normalizedValue = String(value || '').trim() === '待补充' ? '' : String(value || '')
|
||||||
const dateState = fieldKey === 'time' ? parseEditorDateValue(normalizedValue) : {}
|
const fields = message.applicationPreview.fields || {}
|
||||||
|
const dateState = isApplicationPreviewDateField(fieldKey)
|
||||||
|
? parseEditorDateValue(fields.time || normalizedValue)
|
||||||
|
: {}
|
||||||
applicationPreviewEditor.value = {
|
applicationPreviewEditor.value = {
|
||||||
messageId: String(message.id || ''),
|
messageId: String(message.id || ''),
|
||||||
fieldKey,
|
fieldKey,
|
||||||
@@ -148,7 +304,10 @@ export function useApplicationPreviewEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isApplicationPreviewDateEditorOpen(message) {
|
function isApplicationPreviewDateEditorOpen(message) {
|
||||||
return isApplicationPreviewEditing(message, 'time')
|
return (
|
||||||
|
isApplicationPreviewEditing(message, 'time') ||
|
||||||
|
isApplicationPreviewEditing(message, 'time_return')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setApplicationPreviewDateMode(mode) {
|
function setApplicationPreviewDateMode(mode) {
|
||||||
@@ -165,17 +324,7 @@ export function useApplicationPreviewEditor({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildApplicationPreviewDateDraftValue() {
|
async function commitApplicationPreviewEditor(message, options = {}) {
|
||||||
const editor = applicationPreviewEditor.value
|
|
||||||
return buildWorkbenchDateLabel({
|
|
||||||
mode: editor.dateMode,
|
|
||||||
singleDate: editor.singleDate,
|
|
||||||
rangeStartDate: editor.rangeStartDate,
|
|
||||||
rangeEndDate: editor.rangeEndDate
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function commitApplicationPreviewEditor(message) {
|
|
||||||
const editor = applicationPreviewEditor.value
|
const editor = applicationPreviewEditor.value
|
||||||
if (editor.committing) {
|
if (editor.committing) {
|
||||||
return false
|
return false
|
||||||
@@ -189,10 +338,12 @@ export function useApplicationPreviewEditor({
|
|||||||
committing: true
|
committing: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextValue = editor.fieldKey === 'time'
|
const nextValue = buildApplicationPreviewEditorValue(
|
||||||
? buildApplicationPreviewDateDraftValue()
|
message.applicationPreview.fields || {},
|
||||||
: String(editor.draftValue || '').trim()
|
editor,
|
||||||
if (editor.fieldKey === 'time' && !nextValue) {
|
options
|
||||||
|
)
|
||||||
|
if (isApplicationPreviewDateField(editor.fieldKey) && !nextValue) {
|
||||||
toast?.('请先选择有效日期。')
|
toast?.('请先选择有效日期。')
|
||||||
applicationPreviewEditor.value = {
|
applicationPreviewEditor.value = {
|
||||||
...applicationPreviewEditor.value,
|
...applicationPreviewEditor.value,
|
||||||
@@ -232,7 +383,7 @@ export function useApplicationPreviewEditor({
|
|||||||
toast?.('请确认结束日期不早于开始日期。')
|
toast?.('请确认结束日期不早于开始日期。')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return commitApplicationPreviewEditor(message)
|
return commitApplicationPreviewEditor(message, { useDateSelection: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleApplicationPreviewEditorKeydown(event, message) {
|
function handleApplicationPreviewEditorKeydown(event, message) {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { watch } from 'vue'
|
import { watch } from 'vue'
|
||||||
|
|
||||||
|
function isApplicationPreviewDateField(fieldKey = '') {
|
||||||
|
return ['time', 'time_return'].includes(String(fieldKey || '').trim())
|
||||||
|
}
|
||||||
|
|
||||||
export function useTravelReimbursementApplicationPreviewDateEditor({
|
export function useTravelReimbursementApplicationPreviewDateEditor({
|
||||||
applicationPreviewEditor,
|
applicationPreviewEditor,
|
||||||
cancelApplicationPreviewEditor,
|
cancelApplicationPreviewEditor,
|
||||||
@@ -17,7 +21,7 @@ export function useTravelReimbursementApplicationPreviewDateEditor({
|
|||||||
}) {
|
}) {
|
||||||
function applyLinkedApplicationPreviewDateSelection(selection) {
|
function applyLinkedApplicationPreviewDateSelection(selection) {
|
||||||
const editor = applicationPreviewEditor.value
|
const editor = applicationPreviewEditor.value
|
||||||
if (editor.fieldKey !== 'time' || !editor.messageId) {
|
if (!isApplicationPreviewDateField(editor.fieldKey) || !editor.messageId) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,13 +55,13 @@ export function useTravelReimbursementApplicationPreviewDateEditor({
|
|||||||
|
|
||||||
function openApplicationPreviewEditorFromUi(message, fieldKey, value) {
|
function openApplicationPreviewEditorFromUi(message, fieldKey, value) {
|
||||||
openApplicationPreviewEditor(message, fieldKey, value)
|
openApplicationPreviewEditor(message, fieldKey, value)
|
||||||
if (fieldKey === 'time' && isApplicationPreviewEditing(message, 'time')) {
|
if (isApplicationPreviewDateField(fieldKey) && isApplicationPreviewEditing(message, fieldKey)) {
|
||||||
syncComposerDateFromApplicationEditor()
|
syncComposerDateFromApplicationEditor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(composerDatePickerOpen, (open, previousOpen) => {
|
watch(composerDatePickerOpen, (open, previousOpen) => {
|
||||||
if (!open && previousOpen && applicationPreviewEditor.value.fieldKey === 'time') {
|
if (!open && previousOpen && isApplicationPreviewDateField(applicationPreviewEditor.value.fieldKey)) {
|
||||||
cancelApplicationPreviewEditor()
|
cancelApplicationPreviewEditor()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -117,8 +117,12 @@ test('attachment upload association uses conversation selection instead of legac
|
|||||||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementFlow.js', import.meta.url)),
|
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementFlow.js', import.meta.url)),
|
||||||
'utf8'
|
'utf8'
|
||||||
)
|
)
|
||||||
|
const flowToolSource = readFileSync(
|
||||||
|
fileURLToPath(new URL('../src/views/scripts/travelReimbursementFlowToolModel.js', import.meta.url)),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
const conversationSource = readFileSync(
|
const conversationSource = readFileSync(
|
||||||
fileURLToPath(new URL('../src/views/scripts/travelReimbursementConversationModel.js', import.meta.url)),
|
fileURLToPath(new URL('../src/views/scripts/travelReimbursementConversationSessionModel.js', import.meta.url)),
|
||||||
'utf8'
|
'utf8'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,7 +144,7 @@ test('attachment upload association uses conversation selection instead of legac
|
|||||||
assert.match(submitComposerSource, /const appendToCurrentFlow = Boolean\(options\.appendToCurrentFlow\)/)
|
assert.match(submitComposerSource, /const appendToCurrentFlow = Boolean\(options\.appendToCurrentFlow\)/)
|
||||||
assert.match(submitComposerSource, /if \(!appendToCurrentFlow\) \{\s*resetFlowRun\(\)\s*\} else \{\s*clearFlowSimulationTimers\(\)/)
|
assert.match(submitComposerSource, /if \(!appendToCurrentFlow\) \{\s*resetFlowRun\(\)\s*\} else \{\s*clearFlowSimulationTimers\(\)/)
|
||||||
assert.match(flowSource, /link_to_existing_draft:\s*\{[\s\S]*key:\s*'attachment-association'/)
|
assert.match(flowSource, /link_to_existing_draft:\s*\{[\s\S]*key:\s*'attachment-association'/)
|
||||||
assert.match(flowSource, /responseMessage\.includes\('关联'\)[\s\S]*key:\s*'attachment-association'/)
|
assert.match(flowToolSource, /responseMessage\.includes\('关联'\)[\s\S]*key:\s*'attachment-association'/)
|
||||||
assert.match(flowSource, /'draft-risk-review'/)
|
assert.match(flowSource, /'draft-risk-review'/)
|
||||||
assert.match(flowSource, /草稿风险识别/)
|
assert.match(flowSource, /草稿风险识别/)
|
||||||
assert.match(conversationSource, /'attachment-association':\s*\{[\s\S]*title:\s*'票据关联草稿'/)
|
assert.match(conversationSource, /'attachment-association':\s*\{[\s\S]*title:\s*'票据关联草稿'/)
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ test('application preview uses selected date range and business-specific time la
|
|||||||
assert.equal(rows.find((row) => row.key === 'time')?.value, '2026-02-20')
|
assert.equal(rows.find((row) => row.key === 'time')?.value, '2026-02-20')
|
||||||
assert.equal(rows.find((row) => row.key === 'time_return')?.label, '返回时间')
|
assert.equal(rows.find((row) => row.key === 'time_return')?.label, '返回时间')
|
||||||
assert.equal(rows.find((row) => row.key === 'time_return')?.value, '2026-02-23')
|
assert.equal(rows.find((row) => row.key === 'time_return')?.value, '2026-02-23')
|
||||||
|
assert.equal(rows.find((row) => row.key === 'time_return')?.editable, true)
|
||||||
assert.match(submitText, /出发时间:2026-02-20/)
|
assert.match(submitText, /出发时间:2026-02-20/)
|
||||||
assert.match(submitText, /返回时间:2026-02-23/)
|
assert.match(submitText, /返回时间:2026-02-23/)
|
||||||
assert.match(submitText, /事由:支撑国网仿生产环境部署/)
|
assert.match(submitText, /事由:支撑国网仿生产环境部署/)
|
||||||
@@ -1610,6 +1611,52 @@ test('application preview calculates base policy estimate when transport mode is
|
|||||||
assert.equal(staleEstimatePreview.fields.amount, '1,400元(不含交通)')
|
assert.equal(staleEstimatePreview.fields.amount, '1,400元(不含交通)')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('application preview estimate infers days from completed date range', () => {
|
||||||
|
const currentUser = { name: '\u674e\u6587\u9759', grade: 'P5', location: '\u6b66\u6c49' }
|
||||||
|
const preview = normalizeApplicationPreview({
|
||||||
|
fields: {
|
||||||
|
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
|
||||||
|
time: '2026-06-23 \u81f3 2026-06-25',
|
||||||
|
location: '\u5317\u4eac',
|
||||||
|
reason: '\u652f\u6491\u5ba2\u6237\u73b0\u573a\u5b9e\u65bd',
|
||||||
|
days: '',
|
||||||
|
transportMode: '',
|
||||||
|
grade: 'P5'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const request = buildApplicationPolicyEstimateRequest(preview, currentUser)
|
||||||
|
|
||||||
|
assert.equal(request.canCalculate, true)
|
||||||
|
assert.deepEqual(request.payload, {
|
||||||
|
days: 3,
|
||||||
|
location: '\u5317\u4eac',
|
||||||
|
grade: 'P5',
|
||||||
|
transport_mode: null,
|
||||||
|
origin_location: '\u6b66\u6c49',
|
||||||
|
travel_date: '2026-06-23'
|
||||||
|
})
|
||||||
|
|
||||||
|
const estimatedPreview = applyApplicationPolicyEstimateResult(preview, {
|
||||||
|
days: 3,
|
||||||
|
location: '\u5317\u4eac',
|
||||||
|
matched_city: '\u5317\u4eac',
|
||||||
|
grade: 'P5',
|
||||||
|
hotel_rate: 450,
|
||||||
|
hotel_amount: 1350,
|
||||||
|
total_allowance_rate: 100,
|
||||||
|
allowance_amount: 300,
|
||||||
|
total_amount: 1650,
|
||||||
|
rule_name: '\u516c\u53f8\u5dee\u65c5\u8d39\u62a5\u9500\u89c4\u5219',
|
||||||
|
rule_version: 'v1.0.0'
|
||||||
|
}, currentUser)
|
||||||
|
|
||||||
|
assert.equal(estimatedPreview.fields.days, '3\u5929')
|
||||||
|
assert.equal(estimatedPreview.fields.lodgingDailyCap, '450\u5143/\u5929')
|
||||||
|
assert.equal(estimatedPreview.fields.subsidyDailyCap, '100\u5143/\u5929')
|
||||||
|
assert.equal(estimatedPreview.fields.amount, '1,650\u5143\uff08\u4e0d\u542b\u4ea4\u901a\uff09')
|
||||||
|
assert.match(estimatedPreview.fields.policyEstimate, /\u4ea4\u901a\u5f85\u8865\u5145/)
|
||||||
|
})
|
||||||
|
|
||||||
test('application preview editor refreshes transport estimate after mode change', async () => {
|
test('application preview editor refreshes transport estimate after mode change', async () => {
|
||||||
const preview = applyApplicationPolicyEstimateResult(
|
const preview = applyApplicationPolicyEstimateResult(
|
||||||
buildLocalApplicationPreview('申请 2026-05-25 至 2026-05-27 去上海出差3天,服务项目部署', {
|
buildLocalApplicationPreview('申请 2026-05-25 至 2026-05-27 去上海出差3天,服务项目部署', {
|
||||||
@@ -1726,3 +1773,197 @@ test('application preview editor recalculates days and subsidy after date range
|
|||||||
assert.equal(message.applicationPreview.fields.subsidyDailyCap, '100\u5143/\u5929')
|
assert.equal(message.applicationPreview.fields.subsidyDailyCap, '100\u5143/\u5929')
|
||||||
assert.match(message.applicationPreview.fields.policyEstimate, /\u8865\u8d34 400\u5143/)
|
assert.match(message.applicationPreview.fields.policyEstimate, /\u8865\u8d34 400\u5143/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('application preview editor can edit return date from table row', async () => {
|
||||||
|
const preview = normalizeApplicationPreview({
|
||||||
|
fields: {
|
||||||
|
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
|
||||||
|
time: '2026-02-20 \u81f3 2026-02-23',
|
||||||
|
location: '\u4e0a\u6d77',
|
||||||
|
reason: '\u670d\u52a1\u9879\u76ee\u90e8\u7f72',
|
||||||
|
days: '4\u5929',
|
||||||
|
transportMode: '\u706b\u8f66',
|
||||||
|
amount: '',
|
||||||
|
grade: 'P5',
|
||||||
|
applicant: '\u674e\u6587\u9759',
|
||||||
|
department: '\u6280\u672f\u90e8',
|
||||||
|
position: '\u8d22\u52a1\u667a\u80fd\u5316\u4ea7\u54c1\u7ecf\u7406',
|
||||||
|
managerName: '\u5411\u4e07\u7ea2'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const message = {
|
||||||
|
id: 'application-preview-editor-return-date-message',
|
||||||
|
applicationPreview: preview,
|
||||||
|
text: ''
|
||||||
|
}
|
||||||
|
const requestedPayloads = []
|
||||||
|
const editor = useApplicationPreviewEditor({
|
||||||
|
persistSessionState: () => {},
|
||||||
|
toast: () => {},
|
||||||
|
currentUser: ref({ grade: 'P5' }),
|
||||||
|
calculateTravelReimbursement: async (payload) => {
|
||||||
|
requestedPayloads.push(payload)
|
||||||
|
return {
|
||||||
|
days: payload.days,
|
||||||
|
location: payload.location,
|
||||||
|
matched_city: payload.location,
|
||||||
|
grade: payload.grade,
|
||||||
|
hotel_rate: 450,
|
||||||
|
hotel_amount: 2250,
|
||||||
|
total_allowance_rate: 100,
|
||||||
|
allowance_amount: 500,
|
||||||
|
total_amount: 2750,
|
||||||
|
rule_name: '\u516c\u53f8\u5dee\u65c5\u8d39\u62a5\u9500\u89c4\u5219',
|
||||||
|
rule_version: 'v1.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.openApplicationPreviewEditor(message, 'time_return', '2026-02-23')
|
||||||
|
assert.equal(editor.resolveApplicationPreviewEditorControl('time_return'), 'date')
|
||||||
|
assert.equal(editor.applicationPreviewEditor.value.dateMode, 'range')
|
||||||
|
assert.equal(editor.applicationPreviewEditor.value.rangeStartDate, '2026-02-20')
|
||||||
|
editor.applicationPreviewEditor.value.rangeEndDate = '2026-02-24'
|
||||||
|
const committed = await editor.commitApplicationPreviewDateEditor(message)
|
||||||
|
|
||||||
|
assert.equal(committed, true)
|
||||||
|
assert.deepEqual(requestedPayloads.at(-1), {
|
||||||
|
days: 5,
|
||||||
|
location: '\u4e0a\u6d77',
|
||||||
|
grade: 'P5',
|
||||||
|
transport_mode: '\u706b\u8f66',
|
||||||
|
origin_location: null,
|
||||||
|
travel_date: '2026-02-20'
|
||||||
|
})
|
||||||
|
assert.equal(message.applicationPreview.fields.time, '2026-02-20 \u81f3 2026-02-24')
|
||||||
|
assert.equal(message.applicationPreview.fields.days, '5\u5929')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('application preview editor can edit return date from inline table input', async () => {
|
||||||
|
const preview = normalizeApplicationPreview({
|
||||||
|
fields: {
|
||||||
|
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
|
||||||
|
time: '2026-02-20 \u81f3 2026-02-23',
|
||||||
|
location: '\u4e0a\u6d77',
|
||||||
|
reason: '\u670d\u52a1\u9879\u76ee\u90e8\u7f72',
|
||||||
|
days: '4\u5929',
|
||||||
|
transportMode: '\u706b\u8f66',
|
||||||
|
amount: '',
|
||||||
|
grade: 'P5',
|
||||||
|
applicant: '\u674e\u6587\u9759',
|
||||||
|
department: '\u6280\u672f\u90e8',
|
||||||
|
position: '\u8d22\u52a1\u667a\u80fd\u5316\u4ea7\u54c1\u7ecf\u7406',
|
||||||
|
managerName: '\u5411\u4e07\u7ea2'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const message = {
|
||||||
|
id: 'application-preview-editor-inline-return-date-message',
|
||||||
|
applicationPreview: preview,
|
||||||
|
text: ''
|
||||||
|
}
|
||||||
|
const requestedPayloads = []
|
||||||
|
const editor = useApplicationPreviewEditor({
|
||||||
|
persistSessionState: () => {},
|
||||||
|
toast: () => {},
|
||||||
|
currentUser: ref({ grade: 'P5' }),
|
||||||
|
calculateTravelReimbursement: async (payload) => {
|
||||||
|
requestedPayloads.push(payload)
|
||||||
|
return {
|
||||||
|
days: payload.days,
|
||||||
|
location: payload.location,
|
||||||
|
matched_city: payload.location,
|
||||||
|
grade: payload.grade,
|
||||||
|
hotel_rate: 450,
|
||||||
|
hotel_amount: 2250,
|
||||||
|
total_allowance_rate: 100,
|
||||||
|
allowance_amount: 500,
|
||||||
|
total_amount: 2750,
|
||||||
|
rule_name: '\u516c\u53f8\u5dee\u65c5\u8d39\u62a5\u9500\u89c4\u5219',
|
||||||
|
rule_version: 'v1.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.openApplicationPreviewEditor(message, 'time_return', '2026-02-23')
|
||||||
|
editor.applicationPreviewEditor.value.draftValue = '2026-02-24'
|
||||||
|
const committed = await editor.commitApplicationPreviewEditor(message)
|
||||||
|
|
||||||
|
assert.equal(committed, true)
|
||||||
|
assert.deepEqual(requestedPayloads.at(-1), {
|
||||||
|
days: 5,
|
||||||
|
location: '\u4e0a\u6d77',
|
||||||
|
grade: 'P5',
|
||||||
|
transport_mode: '\u706b\u8f66',
|
||||||
|
origin_location: null,
|
||||||
|
travel_date: '2026-02-20'
|
||||||
|
})
|
||||||
|
assert.equal(message.applicationPreview.fields.time, '2026-02-20 \u81f3 2026-02-24')
|
||||||
|
assert.equal(message.applicationPreview.fields.time_return, undefined)
|
||||||
|
assert.equal(message.applicationPreview.fields.days, '5\u5929')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('application preview editor estimates after shorthand return date input', async () => {
|
||||||
|
const preview = normalizeApplicationPreview({
|
||||||
|
fields: {
|
||||||
|
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
|
||||||
|
time: '2026-06-23',
|
||||||
|
location: '\u5317\u4eac',
|
||||||
|
reason: '\u652f\u6491\u5ba2\u6237\u73b0\u573a\u5b9e\u65bd',
|
||||||
|
days: '',
|
||||||
|
transportMode: '',
|
||||||
|
amount: '',
|
||||||
|
grade: 'P5',
|
||||||
|
applicant: '\u674e\u6587\u9759',
|
||||||
|
department: '\u6280\u672f\u90e8',
|
||||||
|
position: '\u8d22\u52a1\u667a\u80fd\u5316\u4ea7\u54c1\u7ecf\u7406',
|
||||||
|
managerName: '\u5411\u4e07\u7ea2'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const message = {
|
||||||
|
id: 'application-preview-editor-shorthand-return-date-message',
|
||||||
|
applicationPreview: preview,
|
||||||
|
text: ''
|
||||||
|
}
|
||||||
|
const requestedPayloads = []
|
||||||
|
const editor = useApplicationPreviewEditor({
|
||||||
|
persistSessionState: () => {},
|
||||||
|
toast: () => {},
|
||||||
|
currentUser: ref({ grade: 'P5', location: '\u6b66\u6c49' }),
|
||||||
|
calculateTravelReimbursement: async (payload) => {
|
||||||
|
requestedPayloads.push(payload)
|
||||||
|
return {
|
||||||
|
days: payload.days,
|
||||||
|
location: payload.location,
|
||||||
|
matched_city: payload.location,
|
||||||
|
grade: payload.grade,
|
||||||
|
hotel_rate: 450,
|
||||||
|
hotel_amount: 1350,
|
||||||
|
total_allowance_rate: 100,
|
||||||
|
allowance_amount: 300,
|
||||||
|
total_amount: 1650,
|
||||||
|
rule_name: '\u516c\u53f8\u5dee\u65c5\u8d39\u62a5\u9500\u89c4\u5219',
|
||||||
|
rule_version: 'v1.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.openApplicationPreviewEditor(message, 'time_return', '\u5f85\u8865\u5145')
|
||||||
|
editor.applicationPreviewEditor.value.draftValue = '6\u670825\u65e5'
|
||||||
|
const committed = await editor.commitApplicationPreviewEditor(message)
|
||||||
|
|
||||||
|
assert.equal(committed, true)
|
||||||
|
assert.deepEqual(requestedPayloads.at(-1), {
|
||||||
|
days: 3,
|
||||||
|
location: '\u5317\u4eac',
|
||||||
|
grade: 'P5',
|
||||||
|
transport_mode: null,
|
||||||
|
origin_location: '\u6b66\u6c49',
|
||||||
|
travel_date: '2026-06-23'
|
||||||
|
})
|
||||||
|
assert.equal(message.applicationPreview.fields.time, '2026-06-23 \u81f3 2026-06-25')
|
||||||
|
assert.equal(message.applicationPreview.fields.days, '3\u5929')
|
||||||
|
assert.equal(message.applicationPreview.fields.lodgingDailyCap, '450\u5143/\u5929')
|
||||||
|
assert.equal(message.applicationPreview.fields.subsidyDailyCap, '100\u5143/\u5929')
|
||||||
|
assert.equal(message.applicationPreview.fields.amount, '1,650\u5143\uff08\u4e0d\u542b\u4ea4\u901a\uff09')
|
||||||
|
assert.match(message.applicationPreview.fields.policyEstimate, /\u4ea4\u901a\u5f85\u8865\u5145/)
|
||||||
|
})
|
||||||
|
|||||||
@@ -396,7 +396,7 @@ test('submit composer scopes the side panel to intent overview, document upload,
|
|||||||
|
|
||||||
test('expense query answers keep one clear result structure with document center jump link', () => {
|
test('expense query answers keep one clear result structure with document center jump link', () => {
|
||||||
assert.doesNotMatch(createViewTemplateSurface, /message\.meta\?\.length/)
|
assert.doesNotMatch(createViewTemplateSurface, /message\.meta\?\.length/)
|
||||||
assert.match(createViewTemplateSurface, /!message\.reviewPayload && !message\.queryPayload && message\.suggestedActions\?\.length/)
|
assert.match(createViewTemplateSurface, /!message\.reviewPayload && \(!message\.queryPayload \|\| message\.queryPayload\.selectionMode === 'reimbursement_application_association'\) && message\.suggestedActions\?\.length/)
|
||||||
assert.match(createViewTemplateSurface, /!message\.reviewPayload && !message\.queryPayload && message\.citations\?\.length/)
|
assert.match(createViewTemplateSurface, /!message\.reviewPayload && !message\.queryPayload && message\.citations\?\.length/)
|
||||||
assert.match(createViewTemplateSurface, /message\.queryPayload\.title \|\| \(message\.queryPayload\.selectionMode === 'draft_association' \? '选择关联草稿' : '最近 5 条筛选结果'\)/)
|
assert.match(createViewTemplateSurface, /message\.queryPayload\.title \|\| \(message\.queryPayload\.selectionMode === 'draft_association' \? '选择关联草稿' : '最近 5 条筛选结果'\)/)
|
||||||
assert.match(createViewTemplateSurface, /v-html="ui\.renderMarkdown\(ui\.buildExpenseQueryHint\(message\.queryPayload\)\)"/)
|
assert.match(createViewTemplateSurface, /v-html="ui\.renderMarkdown\(ui\.buildExpenseQueryHint\(message\.queryPayload\)\)"/)
|
||||||
|
|||||||
Reference in New Issue
Block a user