feat: 新增归档中心页面并完善知识库与报销查询能力

新增前端归档中心视图及相关工具函数,扩充知识库文档分类和
提取器支持多种格式,增强编排器报销查询的多维度检索,优
化本体规则和用户代理审核消息,前端完善报销创建和审批详
情交互细节,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-22 16:00:19 +08:00
parent 1f15699013
commit 88ff04bef8
120 changed files with 6236 additions and 643 deletions

View File

@@ -1,5 +1,171 @@
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
}
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(/^(?:业务)?发生时间[:]\s*[^,。\n]+(?:至\s*[^,。\n]+)?[,。\s]*/u, '')
.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,
@@ -51,12 +217,12 @@ export function useTravelReimbursementComposerTools({
)
function buildComposerBusinessTimeLabel() {
if (composerDateMode.value === 'single') {
return `业务发生时间:${composerSingleDate.value}`
return `发生时间${composerSingleDate.value}`
}
if (composerRangeStartDate.value === composerRangeEndDate.value) {
return `业务发生时间:${composerRangeStartDate.value}`
return `发生时间${composerRangeStartDate.value}`
}
return `业务发生时间:${composerRangeStartDate.value}${composerRangeEndDate.value}`
return `发生时间${composerRangeStartDate.value}${composerRangeEndDate.value}`
}
function hasComposerBusinessTimeSelection() {
@@ -156,6 +322,14 @@ export function useTravelReimbursementComposerTools({
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) {
@@ -377,6 +551,7 @@ export function useTravelReimbursementComposerTools({
mergeBusinessTimeIntoExtraContext,
syncComposerBusinessTimeToReviewCard,
resolveComposerSubmitText,
resolveComposerDisplaySubmitText,
toggleComposerDatePicker,
closeComposerDatePicker,
setComposerDateMode,