- useApplicationPreviewEditor 扩展字段编辑与校验,useTravelReimbursementApplicationPreviewDateEditor 微调日期处理 - travelReimbursementExpenseQueryModel/reimbursements 服务/expenseApplicationPreview 适配工号/邮箱字段与关联动作 - useWorkbenchAiApplicationPreviewFlow/usePersonalWorkbenchAiMode 接入关联门控后的预览流转 - TravelReimbursementCreateView 调整入口,TravelReimbursementMessageItem 适配 - 新增 expense-application-fast-preview 测试,更新 attachment-association-confirmation、review-drawer-switch 测试
418 lines
14 KiB
JavaScript
418 lines
14 KiB
JavaScript
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
|
|
}
|
|
}
|