后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
632 lines
21 KiB
JavaScript
632 lines
21 KiB
JavaScript
import { computed, nextTick, ref } from 'vue'
|
||
|
||
const COMMON_DESTINATION_PREFIXES = [
|
||
'上海',
|
||
'北京',
|
||
'广州',
|
||
'深圳',
|
||
'杭州',
|
||
'南京',
|
||
'苏州',
|
||
'成都',
|
||
'重庆',
|
||
'武汉',
|
||
'西安',
|
||
'天津',
|
||
'宁波',
|
||
'青岛',
|
||
'长沙',
|
||
'郑州',
|
||
'济南',
|
||
'合肥',
|
||
'福州',
|
||
'厦门',
|
||
'昆明',
|
||
'南昌',
|
||
'沈阳',
|
||
'大连',
|
||
'无锡',
|
||
'佛山',
|
||
'东莞'
|
||
]
|
||
|
||
const CHINESE_DAY_NUMBERS = {
|
||
一: 1,
|
||
二: 2,
|
||
两: 2,
|
||
三: 3,
|
||
四: 4,
|
||
五: 5,
|
||
六: 6,
|
||
七: 7,
|
||
八: 8,
|
||
九: 9,
|
||
十: 10
|
||
}
|
||
|
||
const COMPOSER_DATE_RANGE_PREFIX_RE = /^20\d{2}-\d{1,2}-\d{1,2}(?:\s*至\s*20\d{2}-\d{1,2}-\d{1,2})?[,,。\s]*/u
|
||
const COMPOSER_LABELED_TIME_PREFIX_RE = /^(?:业务)?发生时间[::]\s*[^,,。\n]+(?:至\s*[^,,。\n]+)?[,,。\s]*/u
|
||
|
||
function normalizeComposerText(value) {
|
||
return String(value || '').trim().replace(/\s+/g, ' ')
|
||
}
|
||
|
||
function parseDayCount(value) {
|
||
const text = String(value || '').trim()
|
||
const numericValue = Number.parseInt(text, 10)
|
||
if (Number.isFinite(numericValue) && numericValue > 0) {
|
||
return numericValue
|
||
}
|
||
if (text === '十') {
|
||
return 10
|
||
}
|
||
if (/^十[一二三四五六七八九]$/.test(text)) {
|
||
return 10 + (CHINESE_DAY_NUMBERS[text.slice(1)] || 0)
|
||
}
|
||
if (/^[一二两三四五六七八九]十$/.test(text)) {
|
||
return (CHINESE_DAY_NUMBERS[text.slice(0, 1)] || 1) * 10
|
||
}
|
||
if (/^[一二两三四五六七八九]十[一二三四五六七八九]$/.test(text)) {
|
||
return (CHINESE_DAY_NUMBERS[text.slice(0, 1)] || 1) * 10 + (CHINESE_DAY_NUMBERS[text.slice(2)] || 0)
|
||
}
|
||
return CHINESE_DAY_NUMBERS[text] || 0
|
||
}
|
||
|
||
function calculateBusinessDays(businessTimeContext) {
|
||
const startDate = String(businessTimeContext?.start_date || '').trim()
|
||
const endDate = String(businessTimeContext?.end_date || startDate).trim()
|
||
if (!startDate || !endDate || startDate > endDate) {
|
||
return 0
|
||
}
|
||
const startAt = Date.parse(`${startDate}T00:00:00Z`)
|
||
const endAt = Date.parse(`${endDate}T00:00:00Z`)
|
||
if (!Number.isFinite(startAt) || !Number.isFinite(endAt)) {
|
||
return 0
|
||
}
|
||
return Math.max(1, Math.round((endAt - startAt) / 86400000) + 1)
|
||
}
|
||
|
||
function stripBusinessTimePrefix(text) {
|
||
return normalizeComposerText(text)
|
||
.replace(COMPOSER_LABELED_TIME_PREFIX_RE, '')
|
||
.replace(COMPOSER_DATE_RANGE_PREFIX_RE, '')
|
||
.trim()
|
||
}
|
||
|
||
function resolveDestinationFromText(text) {
|
||
const normalized = normalizeComposerText(text).replace(/\s+/g, '')
|
||
const targetMatch = normalized.match(/(?:出差|去|到|赴|前往)([^,,。;;]+)/u)
|
||
const targetText = String(targetMatch?.[1] || '').trim()
|
||
if (!targetText) {
|
||
return ''
|
||
}
|
||
|
||
const knownDestination = COMMON_DESTINATION_PREFIXES.find((item) => targetText.startsWith(item))
|
||
if (knownDestination) {
|
||
return knownDestination
|
||
}
|
||
|
||
const verbIndex = targetText.search(/支撑|支持|部署|实施|驻场|出差|拜访|处理|办理|参加|进行|协助|服务器|项目/u)
|
||
if (verbIndex > 0) {
|
||
return targetText.slice(0, verbIndex)
|
||
}
|
||
return targetText.slice(0, 12)
|
||
}
|
||
|
||
function resolveTripDaysFromText(text, businessTimeContext) {
|
||
const dayMatch = normalizeComposerText(text).match(/(?:出差|共|总计)?\s*([0-9]+|[一二两三四五六七八九十]{1,3})\s*天/u)
|
||
const explicitDays = parseDayCount(dayMatch?.[1])
|
||
return explicitDays || calculateBusinessDays(businessTimeContext)
|
||
}
|
||
|
||
function resolveReasonFromText(text, destination) {
|
||
let reason = normalizeComposerText(text)
|
||
.replace(/^(?:出差|去|到|赴|前往)\s*/u, '')
|
||
.trim()
|
||
|
||
if (destination && reason.startsWith(destination)) {
|
||
reason = reason.slice(destination.length).trim()
|
||
}
|
||
|
||
return reason
|
||
.replace(/[,,。\s]*(?:出差|共|总计)?\s*(?:[0-9]+|[一二两三四五六七八九十]{1,3})\s*天/u, '')
|
||
.replace(/[,,。\s]*(?:申请|发起|办理)?(?:差旅费|差旅|费用)?报销(?:申请)?[。.!!]?$/u, '')
|
||
.replace(/^[,,。;;\s]+|[,,。;;\s]+$/gu, '')
|
||
.trim()
|
||
}
|
||
|
||
export function buildStructuredComposerSubmitText(rawText, businessTimeContext = null) {
|
||
const normalizedText = normalizeComposerText(rawText)
|
||
const timeDisplay = String(
|
||
businessTimeContext?.business_time ||
|
||
businessTimeContext?.time_range ||
|
||
businessTimeContext?.display_value ||
|
||
''
|
||
).trim()
|
||
if (!timeDisplay || !normalizedText) {
|
||
return normalizedText
|
||
}
|
||
|
||
const bodyText = stripBusinessTimePrefix(normalizedText)
|
||
if (!bodyText) {
|
||
return `发生时间:${timeDisplay}`
|
||
}
|
||
|
||
const destination = resolveDestinationFromText(bodyText)
|
||
const reason = resolveReasonFromText(bodyText, destination)
|
||
const days = resolveTripDaysFromText(bodyText, businessTimeContext)
|
||
const lines = [`发生时间:${timeDisplay}`]
|
||
|
||
if (destination) {
|
||
lines.push(`地点:${destination}`)
|
||
}
|
||
if (reason) {
|
||
lines.push(`事由:${reason}`)
|
||
}
|
||
if (days > 0 && (days > 1 || /出差|差旅|至/.test(timeDisplay) || /出差|差旅/.test(bodyText))) {
|
||
lines.push(`天数:${days}天`)
|
||
}
|
||
|
||
return lines.join('\n')
|
||
}
|
||
|
||
export function useTravelReimbursementComposerTools({
|
||
currentUser,
|
||
activeReviewPayload,
|
||
reviewInlineForm,
|
||
latestReviewMessage,
|
||
currentInsight,
|
||
messages,
|
||
composerDraft,
|
||
composerTextareaRef,
|
||
adjustComposerTextareaHeight,
|
||
scrollToBottom,
|
||
toast,
|
||
calculateTravelReimbursement,
|
||
createMessage,
|
||
buildReviewSlotMap,
|
||
isValidIsoDateString,
|
||
buildLocallySyncedReviewPayload,
|
||
formatDateInputValue,
|
||
onComposerDateSelection
|
||
}) {
|
||
const composerDatePickerOpen = ref(false)
|
||
const composerDateMode = ref('single')
|
||
const composerSingleDate = ref(formatDateInputValue())
|
||
const composerRangeStartDate = ref(formatDateInputValue())
|
||
const composerRangeEndDate = ref(formatDateInputValue())
|
||
const composerBusinessTimeTags = ref([])
|
||
const composerBusinessTimeDraftTouched = ref(false)
|
||
const travelCalculatorOpen = ref(false)
|
||
const travelCalculatorBusy = ref(false)
|
||
const travelCalculatorError = ref('')
|
||
const travelCalculatorResult = ref(null)
|
||
const travelCalculatorForm = ref({
|
||
days: '1',
|
||
location: ''
|
||
})
|
||
const composerCanApplyDateSelection = computed(() => {
|
||
if (composerDateMode.value === 'single') {
|
||
return Boolean(composerSingleDate.value)
|
||
}
|
||
return Boolean(
|
||
composerRangeStartDate.value
|
||
&& composerRangeEndDate.value
|
||
&& composerRangeStartDate.value <= composerRangeEndDate.value
|
||
)
|
||
})
|
||
const travelCalculatorCanSubmit = computed(() =>
|
||
!travelCalculatorBusy.value
|
||
&& Number(travelCalculatorForm.value.days) >= 1
|
||
&& Boolean(String(travelCalculatorForm.value.location || '').trim())
|
||
)
|
||
function buildComposerBusinessTimeLabel() {
|
||
if (composerDateMode.value === 'single') {
|
||
return composerSingleDate.value
|
||
}
|
||
if (composerRangeStartDate.value === composerRangeEndDate.value) {
|
||
return composerRangeStartDate.value
|
||
}
|
||
return `${composerRangeStartDate.value} 至 ${composerRangeEndDate.value}`
|
||
}
|
||
|
||
function hasComposerBusinessTimeSelection() {
|
||
return composerBusinessTimeTags.value.length > 0 || composerBusinessTimeDraftTouched.value
|
||
}
|
||
|
||
function buildComposerBusinessTimeContextFromSelection() {
|
||
const mode = composerDateMode.value === 'range' ? 'range' : 'single'
|
||
const startDate = String(mode === 'range' ? composerRangeStartDate.value : composerSingleDate.value).trim()
|
||
const endDate = String(mode === 'range' ? composerRangeEndDate.value : startDate).trim()
|
||
if (!isValidIsoDateString(startDate) || !isValidIsoDateString(endDate) || startDate > endDate) {
|
||
return null
|
||
}
|
||
|
||
const displayValue = mode === 'range' && startDate !== endDate
|
||
? `${startDate} 至 ${endDate}`
|
||
: startDate
|
||
return {
|
||
mode,
|
||
start_date: startDate,
|
||
end_date: endDate,
|
||
occurred_date: startDate,
|
||
time_range: displayValue,
|
||
business_time: displayValue,
|
||
time_range_raw: buildComposerBusinessTimeLabel()
|
||
}
|
||
}
|
||
|
||
function buildComposerBusinessTimeContext() {
|
||
if (!hasComposerBusinessTimeSelection()) {
|
||
return null
|
||
}
|
||
|
||
return buildComposerBusinessTimeContextFromSelection()
|
||
}
|
||
|
||
function buildComposerBusinessTimeSelection() {
|
||
const context = buildComposerBusinessTimeContextFromSelection()
|
||
if (!context) {
|
||
return null
|
||
}
|
||
return {
|
||
label: buildComposerBusinessTimeLabel(),
|
||
context,
|
||
mode: context.mode,
|
||
startDate: context.start_date,
|
||
endDate: context.end_date
|
||
}
|
||
}
|
||
|
||
function mergeBusinessTimeIntoExtraContext(extraContext, businessTimeContext) {
|
||
if (!businessTimeContext) {
|
||
return extraContext
|
||
}
|
||
|
||
const baseReviewFormValues =
|
||
extraContext.review_form_values && typeof extraContext.review_form_values === 'object'
|
||
? extraContext.review_form_values
|
||
: {}
|
||
|
||
return {
|
||
...extraContext,
|
||
occurred_date: businessTimeContext.occurred_date,
|
||
business_time: businessTimeContext.business_time,
|
||
business_time_context: {
|
||
mode: businessTimeContext.mode,
|
||
start_date: businessTimeContext.start_date,
|
||
end_date: businessTimeContext.end_date,
|
||
display_value: businessTimeContext.business_time
|
||
},
|
||
review_form_values: {
|
||
...baseReviewFormValues,
|
||
occurred_date: businessTimeContext.occurred_date,
|
||
time_range: businessTimeContext.time_range,
|
||
business_time: businessTimeContext.business_time,
|
||
time_range_raw: businessTimeContext.time_range_raw
|
||
}
|
||
}
|
||
}
|
||
|
||
function syncComposerBusinessTimeToReviewCard(businessTimeContext) {
|
||
if (!businessTimeContext || !activeReviewPayload.value) {
|
||
return
|
||
}
|
||
|
||
const nextInlineState = {
|
||
...reviewInlineForm.value,
|
||
occurred_date: businessTimeContext.occurred_date
|
||
}
|
||
const nextReviewPayload = buildLocallySyncedReviewPayload(activeReviewPayload.value, nextInlineState)
|
||
reviewInlineForm.value = nextInlineState
|
||
if (latestReviewMessage.value) {
|
||
latestReviewMessage.value.reviewPayload = nextReviewPayload
|
||
}
|
||
if (currentInsight.value?.agent) {
|
||
currentInsight.value = {
|
||
...currentInsight.value,
|
||
agent: {
|
||
...currentInsight.value.agent,
|
||
reviewPayload: nextReviewPayload
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function resolveComposerSubmitText(explicitRawText) {
|
||
const draftPart = String(explicitRawText ?? composerDraft.value).trim()
|
||
const tagPart = composerBusinessTimeTags.value.map((item) => item.label).join(',')
|
||
if (!tagPart) {
|
||
return draftPart
|
||
}
|
||
if (!draftPart) {
|
||
return tagPart
|
||
}
|
||
return `${tagPart},${draftPart}`
|
||
}
|
||
|
||
function resolveComposerDisplaySubmitText(rawText) {
|
||
const businessTimeContext = buildComposerBusinessTimeContext()
|
||
if (!businessTimeContext) {
|
||
return String(rawText || '').trim()
|
||
}
|
||
return buildStructuredComposerSubmitText(rawText, businessTimeContext)
|
||
}
|
||
|
||
function toggleComposerDatePicker() {
|
||
composerDatePickerOpen.value = !composerDatePickerOpen.value
|
||
if (composerDatePickerOpen.value) {
|
||
travelCalculatorOpen.value = false
|
||
}
|
||
}
|
||
|
||
function closeComposerDatePicker() {
|
||
composerDatePickerOpen.value = false
|
||
}
|
||
|
||
function setComposerDateMode(mode) {
|
||
composerDateMode.value = mode === 'range' ? 'range' : 'single'
|
||
}
|
||
|
||
async function commitComposerDateSelection({ closePicker = true, focusComposer = true } = {}) {
|
||
if (!composerCanApplyDateSelection.value) {
|
||
return false
|
||
}
|
||
|
||
const selection = buildComposerBusinessTimeSelection()
|
||
if (!selection) {
|
||
return false
|
||
}
|
||
|
||
const handled = onComposerDateSelection?.(selection) === true
|
||
if (handled) {
|
||
composerBusinessTimeDraftTouched.value = false
|
||
composerBusinessTimeTags.value = []
|
||
} else {
|
||
composerBusinessTimeDraftTouched.value = true
|
||
composerBusinessTimeTags.value = [
|
||
{
|
||
id: `biz-time-${Date.now()}`,
|
||
label: selection.label
|
||
}
|
||
]
|
||
syncComposerBusinessTimeToReviewCard(selection.context)
|
||
}
|
||
|
||
if (closePicker) {
|
||
composerDatePickerOpen.value = false
|
||
}
|
||
await nextTick()
|
||
adjustComposerTextareaHeight()
|
||
if (focusComposer) {
|
||
composerTextareaRef.value?.focus()
|
||
}
|
||
return true
|
||
}
|
||
|
||
function handleComposerDateInputChange(part = 'single') {
|
||
if (composerDateMode.value !== 'range' || part === 'single') {
|
||
void commitComposerDateSelection()
|
||
return
|
||
}
|
||
|
||
if (part === 'range-start') {
|
||
if (!composerRangeEndDate.value || composerRangeEndDate.value < composerRangeStartDate.value) {
|
||
composerRangeEndDate.value = composerRangeStartDate.value
|
||
}
|
||
if (!onComposerDateSelection) {
|
||
composerBusinessTimeDraftTouched.value = true
|
||
syncComposerBusinessTimeToReviewCard(buildComposerBusinessTimeContextFromSelection())
|
||
}
|
||
return
|
||
}
|
||
|
||
void commitComposerDateSelection()
|
||
}
|
||
|
||
function removeComposerBusinessTimeTag(tagId) {
|
||
composerBusinessTimeTags.value = composerBusinessTimeTags.value.filter((item) => item.id !== tagId)
|
||
if (!composerBusinessTimeTags.value.length) {
|
||
composerBusinessTimeDraftTouched.value = false
|
||
}
|
||
}
|
||
|
||
function handleComposerDatePickerOutside(event) {
|
||
if (!composerDatePickerOpen.value && !travelCalculatorOpen.value) {
|
||
return
|
||
}
|
||
if (event.target instanceof Element && event.target.closest('.composer-date-anchor')) {
|
||
return
|
||
}
|
||
if (event.target instanceof Element && event.target.closest('.travel-calculator-anchor')) {
|
||
return
|
||
}
|
||
if (composerDatePickerOpen.value) {
|
||
composerDatePickerOpen.value = false
|
||
}
|
||
if (travelCalculatorOpen.value && !travelCalculatorBusy.value) {
|
||
travelCalculatorOpen.value = false
|
||
}
|
||
}
|
||
|
||
async function applyComposerDateSelection() {
|
||
await commitComposerDateSelection()
|
||
}
|
||
|
||
function resolveTravelCalculatorInitialDays() {
|
||
const businessTimeContext = buildComposerBusinessTimeContext()
|
||
if (!businessTimeContext) {
|
||
return 1
|
||
}
|
||
const startDate = businessTimeContext.start_date
|
||
const endDate = businessTimeContext.end_date || startDate
|
||
if (!isValidIsoDateString(startDate) || !isValidIsoDateString(endDate) || startDate > endDate) {
|
||
return 1
|
||
}
|
||
const startAt = Date.parse(`${startDate}T00:00:00Z`)
|
||
const endAt = Date.parse(`${endDate}T00:00:00Z`)
|
||
if (!Number.isFinite(startAt) || !Number.isFinite(endAt)) {
|
||
return 1
|
||
}
|
||
return Math.max(1, Math.round((endAt - startAt) / 86400000) + 1)
|
||
}
|
||
|
||
function resolveTravelCalculatorInitialLocation() {
|
||
const slotMap = buildReviewSlotMap(activeReviewPayload.value)
|
||
const candidates = [
|
||
reviewInlineForm.value.location,
|
||
slotMap.business_location?.normalized_value,
|
||
slotMap.business_location?.value,
|
||
slotMap.location?.normalized_value,
|
||
slotMap.location?.value,
|
||
currentUser.value?.location
|
||
]
|
||
return String(candidates.find((item) => String(item || '').trim()) || '').trim()
|
||
}
|
||
|
||
function openTravelCalculator() {
|
||
closeComposerDatePicker()
|
||
travelCalculatorError.value = ''
|
||
travelCalculatorResult.value = null
|
||
travelCalculatorForm.value = {
|
||
days: String(resolveTravelCalculatorInitialDays()),
|
||
location: resolveTravelCalculatorInitialLocation()
|
||
}
|
||
travelCalculatorOpen.value = true
|
||
}
|
||
|
||
function toggleTravelCalculator() {
|
||
if (travelCalculatorOpen.value) {
|
||
closeTravelCalculator()
|
||
return
|
||
}
|
||
openTravelCalculator()
|
||
}
|
||
|
||
function closeTravelCalculator() {
|
||
if (travelCalculatorBusy.value) {
|
||
return
|
||
}
|
||
travelCalculatorOpen.value = false
|
||
}
|
||
|
||
function formatTravelCalculatorMoney(value) {
|
||
const amount = Number(value)
|
||
if (!Number.isFinite(amount)) {
|
||
return String(value || '0')
|
||
}
|
||
return amount.toFixed(2)
|
||
}
|
||
|
||
function buildTravelCalculatorResultText(result) {
|
||
const days = Number(result?.days) || 1
|
||
const location = String(result?.location || '').trim() || '未填写地点'
|
||
const matchedCity = String(result?.matched_city || location).trim()
|
||
const grade = String(result?.grade || '').trim() || '当前职级'
|
||
const gradeBandLabel = String(result?.grade_band_label || result?.grade_band || '').trim() || '对应档位'
|
||
const allowanceRegion = String(result?.allowance_region || '').trim() || '默认区域'
|
||
const ruleName = String(result?.rule_name || '').trim() || '公司差旅费报销规则'
|
||
const ruleVersion = String(result?.rule_version || '').trim()
|
||
const hotelRate = formatTravelCalculatorMoney(result?.hotel_rate)
|
||
const hotelAmount = formatTravelCalculatorMoney(result?.hotel_amount)
|
||
const mealRate = formatTravelCalculatorMoney(result?.meal_allowance_rate)
|
||
const basicRate = formatTravelCalculatorMoney(result?.basic_allowance_rate)
|
||
const allowanceRate = formatTravelCalculatorMoney(result?.total_allowance_rate)
|
||
const allowanceAmount = formatTravelCalculatorMoney(result?.allowance_amount)
|
||
const totalAmount = formatTravelCalculatorMoney(result?.total_amount)
|
||
const ruleVersionText = ruleVersion ? `(${ruleVersion})` : ''
|
||
const user = currentUser.value || {}
|
||
const displayName = String(user.name || user.display_name || user.username || '').trim()
|
||
const greeting = displayName ? `您好,${displayName},` : '您好,'
|
||
|
||
return [
|
||
`${greeting}根据您输入的地点和天数,我匹配到您要出差的地区为:**${matchedCity}**,出差天数为:**${days} 天**,我根据公司的报销文件给您预估金额如下:`,
|
||
'',
|
||
`**参考可报销合计:${totalAmount} 元**`,
|
||
'',
|
||
'| 项目 | 标准口径 | 天数 | 小计 |',
|
||
'| --- | --- | ---: | ---: |',
|
||
`| 住宿费 | ${matchedCity} / ${grade}(${gradeBandLabel})标准:${hotelRate} 元/天 | ${days} | ${hotelAmount} 元 |`,
|
||
`| 出差补贴 | ${allowanceRegion}:伙食 ${mealRate} 元 + 基本 ${basicRate} 元 = ${allowanceRate} 元/天 | ${days} | ${allowanceAmount} 元 |`,
|
||
'',
|
||
'**计算过程**',
|
||
`1. 住宿费:${hotelRate} × ${days} = ${hotelAmount} 元`,
|
||
`2. 出差补贴:(${mealRate} + ${basicRate}) × ${days} = ${allowanceRate} × ${days} = ${allowanceAmount} 元`,
|
||
`3. 合计:${hotelAmount} + ${allowanceAmount} = ${totalAmount} 元`,
|
||
'',
|
||
`**规则依据**:${ruleName}${ruleVersionText}。出差地点“${location}”匹配为“${matchedCity}”,当前职级“${grade}”匹配“${gradeBandLabel}”档。`,
|
||
'',
|
||
'这个结果是提交前的规则测算参考,最终仍以实际票据、审批意见和财务复核口径为准。'
|
||
].join('\n')
|
||
}
|
||
|
||
async function submitTravelCalculator() {
|
||
if (!travelCalculatorCanSubmit.value) {
|
||
travelCalculatorError.value = '请填写出差天数和地点后再计算。'
|
||
return
|
||
}
|
||
|
||
travelCalculatorBusy.value = true
|
||
travelCalculatorError.value = ''
|
||
try {
|
||
const user = currentUser.value || {}
|
||
const payload = await calculateTravelReimbursement({
|
||
days: Math.max(1, Number.parseInt(String(travelCalculatorForm.value.days || '1'), 10) || 1),
|
||
location: String(travelCalculatorForm.value.location || '').trim(),
|
||
grade: String(user.grade || '').trim()
|
||
})
|
||
travelCalculatorResult.value = payload
|
||
messages.value.push(createMessage('assistant', buildTravelCalculatorResultText(payload), [], {
|
||
meta: ['差旅计算器'],
|
||
metaTone: 'low'
|
||
}))
|
||
travelCalculatorOpen.value = false
|
||
nextTick(scrollToBottom)
|
||
} catch (error) {
|
||
travelCalculatorError.value = error?.message || '差旅金额测算失败,请稍后重试。'
|
||
} finally {
|
||
travelCalculatorBusy.value = false
|
||
}
|
||
}
|
||
|
||
return {
|
||
composerDatePickerOpen,
|
||
composerDateMode,
|
||
composerSingleDate,
|
||
composerRangeStartDate,
|
||
composerRangeEndDate,
|
||
composerBusinessTimeTags,
|
||
composerBusinessTimeDraftTouched,
|
||
composerCanApplyDateSelection,
|
||
travelCalculatorOpen,
|
||
travelCalculatorBusy,
|
||
travelCalculatorError,
|
||
travelCalculatorResult,
|
||
travelCalculatorForm,
|
||
travelCalculatorCanSubmit,
|
||
buildComposerBusinessTimeLabel,
|
||
hasComposerBusinessTimeSelection,
|
||
buildComposerBusinessTimeSelection,
|
||
buildComposerBusinessTimeContext,
|
||
mergeBusinessTimeIntoExtraContext,
|
||
syncComposerBusinessTimeToReviewCard,
|
||
resolveComposerSubmitText,
|
||
resolveComposerDisplaySubmitText,
|
||
toggleComposerDatePicker,
|
||
closeComposerDatePicker,
|
||
setComposerDateMode,
|
||
handleComposerDateInputChange,
|
||
removeComposerBusinessTimeTag,
|
||
handleComposerDatePickerOutside,
|
||
applyComposerDateSelection,
|
||
resolveTravelCalculatorInitialDays,
|
||
resolveTravelCalculatorInitialLocation,
|
||
openTravelCalculator,
|
||
toggleTravelCalculator,
|
||
closeTravelCalculator,
|
||
formatTravelCalculatorMoney,
|
||
buildTravelCalculatorResultText,
|
||
submitTravelCalculator
|
||
}
|
||
}
|