import { ref } from 'vue' import { APPLICATION_TRANSPORT_MODE_OPTIONS, applyApplicationPolicyEstimateError, applyApplicationPolicyEstimateResult, buildApplicationPreviewRows, buildApplicationPolicyEstimateRequest, buildLocalApplicationPreviewMessage, normalizeApplicationPreview, resolveApplicationDaysFromDateRange, refreshApplicationPreviewTransportEstimate } from '../../utils/expenseApplicationPreview.js' import { buildWorkbenchDateLabel, canApplyWorkbenchDateSelection, getTodayDateValue } from '../../utils/workbenchComposerDate.js' function parseEditorDateMatches(value) { const text = String(value || '').trim() 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 endDate = matches[1] || startDate return { dateMode: matches.length > 1 && startDate !== endDate ? 'range' : 'single', singleDate: startDate, rangeStartDate: startDate, rangeEndDate: endDate } } 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() { return { messageId: '', fieldKey: '', draftValue: '', dateMode: 'single', singleDate: getTodayDateValue(), rangeStartDate: getTodayDateValue(), rangeEndDate: getTodayDateValue(), committing: false } } function shouldRefreshTransportEstimate(fieldKey) { return ['transportMode', 'time', 'time_return', 'location', 'days'].includes(fieldKey) } function resolveEditorCurrentUser(currentUser) { if (currentUser && typeof currentUser === 'object' && 'value' in currentUser) { return currentUser.value || {} } return currentUser || {} } function buildEditedApplicationPreviewFields(fields = {}, editor = {}, nextValue = '') { const isDateField = isApplicationPreviewDateField(editor.fieldKey) const nextFields = { ...fields, [isDateField ? 'time' : editor.fieldKey]: nextValue } if (isDateField) { const resolvedDays = resolveApplicationDaysFromDateRange(nextValue) if (resolvedDays) { nextFields.days = resolvedDays } } 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 = {}) { const fields = preview?.fields || {} return normalizeApplicationPreview({ ...preview, fields: { ...fields, transportPolicy: '正在预估交通费用...', policyEstimate: '正在同步费用测算...', transportEstimatedAmount: '查询中' } }) } export function useApplicationPreviewEditor({ persistSessionState, toast, calculateTravelReimbursement, currentUser } = {}) { const applicationPreviewEditor = ref(buildEmptyEditor()) async function refreshApplicationPreviewEstimate(preview = {}) { const user = resolveEditorCurrentUser(currentUser) const estimateRequest = buildApplicationPolicyEstimateRequest(preview, user) if (estimateRequest.canCalculate && typeof calculateTravelReimbursement === 'function') { try { const result = await calculateTravelReimbursement(estimateRequest.payload) return applyApplicationPolicyEstimateResult(preview, result, user) } catch (error) { console.warn('Application preview estimate refresh failed:', error) return applyApplicationPolicyEstimateError(preview, error, user) } } return refreshApplicationPreviewTransportEstimate(preview) } function resolveApplicationPreviewRows(message) { return buildApplicationPreviewRows(message?.applicationPreview || {}) } function resolveApplicationPreviewEditorControl(fieldKey) { if (fieldKey === 'transportMode') return 'select' if (isApplicationPreviewDateField(fieldKey)) return 'date' return 'text' } function resolveApplicationPreviewEditorOptions(fieldKey) { return fieldKey === 'transportMode' ? APPLICATION_TRANSPORT_MODE_OPTIONS : [] } function isApplicationPreviewEditing(message, fieldKey) { return ( String(applicationPreviewEditor.value.messageId || '') === String(message?.id || '') && applicationPreviewEditor.value.fieldKey === fieldKey ) } function openApplicationPreviewEditor(message, fieldKey, value) { if (!message?.applicationPreview || !fieldKey) return const targetRow = buildApplicationPreviewRows(message.applicationPreview) .find((row) => row.key === fieldKey) if (targetRow && targetRow.editable === false) return const normalizedValue = String(value || '').trim() === '待补充' ? '' : String(value || '') const fields = message.applicationPreview.fields || {} const dateState = isApplicationPreviewDateField(fieldKey) ? parseEditorDateValue(fields.time || normalizedValue) : {} applicationPreviewEditor.value = { messageId: String(message.id || ''), fieldKey, draftValue: fieldKey === 'transportMode' && !APPLICATION_TRANSPORT_MODE_OPTIONS.includes(normalizedValue) ? '' : normalizedValue, committing: false, ...dateState } } function cancelApplicationPreviewEditor() { applicationPreviewEditor.value = buildEmptyEditor() } function isApplicationPreviewDateEditorOpen(message) { return ( isApplicationPreviewEditing(message, 'time') || isApplicationPreviewEditing(message, 'time_return') ) } function setApplicationPreviewDateMode(mode) { applicationPreviewEditor.value.dateMode = mode === 'range' ? 'range' : 'single' } function canApplyApplicationPreviewDateSelection() { const editor = applicationPreviewEditor.value return canApplyWorkbenchDateSelection({ mode: editor.dateMode, singleDate: editor.singleDate, rangeStartDate: editor.rangeStartDate, rangeEndDate: editor.rangeEndDate }) } async function commitApplicationPreviewEditor(message, options = {}) { const editor = applicationPreviewEditor.value if (editor.committing) { return false } if (!message?.applicationPreview || String(editor.messageId || '') !== String(message.id || '') || !editor.fieldKey) { cancelApplicationPreviewEditor() return false } applicationPreviewEditor.value = { ...editor, committing: true } const nextValue = buildApplicationPreviewEditorValue( message.applicationPreview.fields || {}, editor, options ) if (isApplicationPreviewDateField(editor.fieldKey) && !nextValue) { toast?.('请先选择有效日期。') applicationPreviewEditor.value = { ...applicationPreviewEditor.value, committing: false } return false } const nextPreview = normalizeApplicationPreview({ ...message.applicationPreview, fields: buildEditedApplicationPreviewFields( message.applicationPreview.fields || {}, editor, nextValue ) }) const needRefreshEstimate = shouldRefreshTransportEstimate(editor.fieldKey) message.applicationPreview = needRefreshEstimate ? buildTransportEstimatePendingPreview(nextPreview) : nextPreview message.text = buildLocalApplicationPreviewMessage(message.applicationPreview) cancelApplicationPreviewEditor() persistSessionState?.() if (needRefreshEstimate) { const refreshedPreview = await refreshApplicationPreviewEstimate(nextPreview) message.applicationPreview = refreshedPreview message.text = buildLocalApplicationPreviewMessage(refreshedPreview) persistSessionState?.() toast?.('已更新出行方式和费用测算。') return true } toast?.('已更新核对表内容。') return true } async function commitApplicationPreviewDateEditor(message) { if (!canApplyApplicationPreviewDateSelection()) { toast?.('请确认结束日期不早于开始日期。') return false } return commitApplicationPreviewEditor(message, { useDateSelection: true }) } function handleApplicationPreviewEditorKeydown(event, message) { if (event.key === 'Enter') { event.preventDefault() commitApplicationPreviewEditor(message) return } if (event.key === 'Escape') { event.preventDefault() cancelApplicationPreviewEditor() } } return { applicationPreviewEditor, resolveApplicationPreviewRows, resolveApplicationPreviewEditorControl, resolveApplicationPreviewEditorOptions, refreshApplicationPreviewEstimate, isApplicationPreviewEditing, isApplicationPreviewDateEditorOpen, openApplicationPreviewEditor, commitApplicationPreviewEditor, commitApplicationPreviewDateEditor, cancelApplicationPreviewEditor, setApplicationPreviewDateMode, canApplyApplicationPreviewDateSelection, handleApplicationPreviewEditorKeydown } }