feat: 增强员工管理与报销单全流程功能
- 新增员工Excel导入服务(employee_spreadsheet)及导入/导出API端点 - 员工服务增加批量创建、邮箱唯一校验、组织架构关联等能力 - 报销单提交补充身份回填、部门信息透传及预审结果展示优化 - 认证流程增加部门信息(departmentName)并在schema中同步扩展 - 用户Agent服务增加部门关联与报销单回填逻辑 - 前端员工管理页面全面重构,新增导入导出、搜索过滤、分页等功能 - 前端审批中心、审计、差旅报销等视图交互与样式优化 - 新增TableLoadingState共享组件及员工导入测试用例
This commit is contained in:
@@ -43,6 +43,24 @@ const INTENT_LABELS = {
|
||||
operate: '动作请求'
|
||||
}
|
||||
|
||||
const REVIEW_RISK_LEVEL_META = {
|
||||
high: {
|
||||
label: '高风险',
|
||||
icon: 'mdi mdi-alert-octagon-outline',
|
||||
suggestion: '建议先处理该项,再提交审批;如果属于合理业务场景,请补充说明或附件。'
|
||||
},
|
||||
warning: {
|
||||
label: '需关注',
|
||||
icon: 'mdi mdi-alert-circle-outline',
|
||||
suggestion: '提交前建议核对业务真实性、附件完整性和规则口径。'
|
||||
},
|
||||
info: {
|
||||
label: '提示',
|
||||
icon: 'mdi mdi-information-outline',
|
||||
suggestion: '该项主要用于辅助判断,可结合当前单据情况继续核对。'
|
||||
}
|
||||
}
|
||||
|
||||
const DOCUMENT_TYPE_LABELS = {
|
||||
travel_ticket: '行程单/机票/车票',
|
||||
flight_itinerary: '机票/航班行程单',
|
||||
@@ -1503,7 +1521,7 @@ function buildDraftSavedPayload({
|
||||
secondaryStatusLabel: typeCode === 'travel' ? '行程状态' : '票据状态',
|
||||
secondaryStatusValue: documents.length ? '待继续完善' : '待上传票据',
|
||||
secondaryStatusTone: documents.length ? 'warning' : 'neutral',
|
||||
riskSummary: riskItems[0] || (missingItems.length ? '仍有识别字段待补充,请继续完善。' : 'AI 已生成草稿,可继续补充后提交。'),
|
||||
riskSummary: riskItems[0]?.summary || (missingItems.length ? '仍有识别字段待补充,请继续完善。' : 'AI 已生成草稿,可继续补充后提交。'),
|
||||
attachmentSummary,
|
||||
expenseTableSummary: documents.length
|
||||
? `已关联 ${documents.length} 份票据,请继续在报销页补充和确认`
|
||||
@@ -2451,16 +2469,43 @@ function buildReviewRiskSummary(reviewPayload) {
|
||||
return '当前版本暂未生成风险评分结果。'
|
||||
}
|
||||
|
||||
function normalizeReviewRiskLevel(level) {
|
||||
const normalized = String(level || '').trim().toLowerCase()
|
||||
if (normalized === 'danger' || normalized === 'error' || normalized === 'critical') return 'high'
|
||||
if (normalized === 'warn' || normalized === 'medium') return 'warning'
|
||||
if (normalized === 'high' || normalized === 'warning' || normalized === 'info') return normalized
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function buildReviewRiskItems(reviewPayload) {
|
||||
return resolveReviewRiskBriefs(reviewPayload)
|
||||
.map((brief) => {
|
||||
.map((brief, index) => {
|
||||
const title = String(brief?.title || '').trim()
|
||||
const content = String(brief?.content || '').trim()
|
||||
if (title && content) return `${title}:${content}`
|
||||
return content || title
|
||||
const detail = String(brief?.detail || '').trim()
|
||||
const suggestion = String(brief?.suggestion || '').trim()
|
||||
const level = normalizeReviewRiskLevel(brief?.level)
|
||||
const meta = REVIEW_RISK_LEVEL_META[level] || REVIEW_RISK_LEVEL_META.info
|
||||
const fallbackTitle = content ? `风险提示 ${index + 1}` : '风险提示'
|
||||
const normalizedTitle = title || fallbackTitle
|
||||
const summary = content || normalizedTitle
|
||||
|
||||
if (!normalizedTitle && !summary) return null
|
||||
|
||||
return {
|
||||
key: `${level}-${normalizedTitle}-${index}`,
|
||||
title: normalizedTitle,
|
||||
summary,
|
||||
detail: detail || content || '当前风险项没有返回更长解释,建议结合票据、报销事由和规则要求进行复核。',
|
||||
level,
|
||||
levelLabel: meta.label,
|
||||
icon: meta.icon,
|
||||
sourceLabel: title === '历史报销画像' ? '历史记录' : 'AI预审',
|
||||
suggestion: suggestion || meta.suggestion
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.slice(0, 4)
|
||||
.slice(0, 6)
|
||||
}
|
||||
|
||||
function resolveInlineReviewSlotValue(slotKey, inlineState = createEmptyInlineReviewState()) {
|
||||
@@ -2904,6 +2949,7 @@ export default {
|
||||
const composerRangeStartDate = ref(formatDateInputValue())
|
||||
const composerRangeEndDate = ref(formatDateInputValue())
|
||||
const composerBusinessTimeTags = ref([])
|
||||
const composerBusinessTimeDraftTouched = ref(false)
|
||||
const attachedFiles = ref([])
|
||||
const composerFilesExpanded = ref(false)
|
||||
const submitting = ref(false)
|
||||
@@ -2947,6 +2993,10 @@ export default {
|
||||
const activeReviewDocumentIndex = ref(0)
|
||||
const reviewDrawerMode = ref(REVIEW_DRAWER_MODE_REVIEW)
|
||||
const insightPanelCollapsed = ref(false)
|
||||
const reviewRiskDetailDialog = ref({
|
||||
open: false,
|
||||
item: null
|
||||
})
|
||||
const documentPreviewDialog = ref({
|
||||
open: false,
|
||||
filename: '',
|
||||
@@ -3107,7 +3157,6 @@ export default {
|
||||
const reviewRiskEmpty = computed(() => reviewRiskScore.value === null && !reviewRiskItems.value.length)
|
||||
const reviewDocumentDrawerAvailable = computed(() => reviewDocumentCount.value > 0)
|
||||
const reviewRiskDrawerAvailable = computed(() => !reviewRiskEmpty.value)
|
||||
const reviewRiskActionAvailable = computed(() => reviewRiskItems.value.length > 0)
|
||||
const reviewFlowDrawerAvailable = computed(() => flowSteps.value.length > 0)
|
||||
const recognizedNarratives = computed(() => buildReviewRecognizedLines(activeReviewPayload.value))
|
||||
const reviewRecognitionNotes = computed(() => buildReviewRecognitionNotes(activeReviewPayload.value))
|
||||
@@ -3932,6 +3981,91 @@ export default {
|
||||
return `业务发生时间:${composerRangeStartDate.value} 至 ${composerRangeEndDate.value}`
|
||||
}
|
||||
|
||||
function hasComposerBusinessTimeSelection() {
|
||||
return composerBusinessTimeTags.value.length > 0 || composerBusinessTimeDraftTouched.value
|
||||
}
|
||||
|
||||
function buildComposerBusinessTimeContext() {
|
||||
if (!hasComposerBusinessTimeSelection()) {
|
||||
return null
|
||||
}
|
||||
|
||||
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 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(',')
|
||||
@@ -3956,8 +4090,16 @@ export default {
|
||||
composerDateMode.value = mode === 'range' ? 'range' : 'single'
|
||||
}
|
||||
|
||||
function handleComposerDateInputChange() {
|
||||
composerBusinessTimeDraftTouched.value = true
|
||||
syncComposerBusinessTimeToReviewCard(buildComposerBusinessTimeContext())
|
||||
}
|
||||
|
||||
function removeComposerBusinessTimeTag(tagId) {
|
||||
composerBusinessTimeTags.value = composerBusinessTimeTags.value.filter((item) => item.id !== tagId)
|
||||
if (!composerBusinessTimeTags.value.length) {
|
||||
composerBusinessTimeDraftTouched.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleComposerDatePickerOutside(event) {
|
||||
@@ -3975,12 +4117,14 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
composerBusinessTimeDraftTouched.value = true
|
||||
composerBusinessTimeTags.value = [
|
||||
{
|
||||
id: `biz-time-${Date.now()}`,
|
||||
label: buildComposerBusinessTimeLabel()
|
||||
}
|
||||
]
|
||||
syncComposerBusinessTimeToReviewCard(buildComposerBusinessTimeContext())
|
||||
composerDatePickerOpen.value = false
|
||||
await nextTick()
|
||||
adjustComposerTextareaHeight()
|
||||
@@ -4432,13 +4576,19 @@ export default {
|
||||
})
|
||||
}
|
||||
|
||||
function explainCurrentReviewRisk() {
|
||||
if (!activeReviewPayload.value || submitting.value || reviewActionBusy.value) return
|
||||
submitComposer({
|
||||
rawText: '请解释一下当前这笔报销的合规风险和待补充项。',
|
||||
userText: '查看全部风险项',
|
||||
systemGenerated: true
|
||||
})
|
||||
function openReviewRiskDetail(item) {
|
||||
if (!item) return
|
||||
reviewRiskDetailDialog.value = {
|
||||
open: true,
|
||||
item
|
||||
}
|
||||
}
|
||||
|
||||
function closeReviewRiskDetail() {
|
||||
reviewRiskDetailDialog.value = {
|
||||
...reviewRiskDetailDialog.value,
|
||||
open: false
|
||||
}
|
||||
}
|
||||
|
||||
function goReviewDocument(direction) {
|
||||
@@ -4642,9 +4792,13 @@ export default {
|
||||
}
|
||||
if (!rawText && !files.length) return
|
||||
|
||||
const extraContext = options.extraContext && typeof options.extraContext === 'object'
|
||||
const initialExtraContext = options.extraContext && typeof options.extraContext === 'object'
|
||||
? { ...options.extraContext }
|
||||
: {}
|
||||
const selectedBusinessTimeContext = isKnowledgeSession.value ? null : buildComposerBusinessTimeContext()
|
||||
const extraContext = isKnowledgeSession.value
|
||||
? initialExtraContext
|
||||
: mergeBusinessTimeIntoExtraContext(initialExtraContext, selectedBusinessTimeContext)
|
||||
const reviewAction = String(extraContext.review_action || '').trim()
|
||||
const reviewAttachmentNames = extractReviewAttachmentNames(activeReviewPayload.value)
|
||||
const hasExistingDocumentEvent =
|
||||
@@ -4699,6 +4853,7 @@ export default {
|
||||
|
||||
composerDraft.value = ''
|
||||
composerBusinessTimeTags.value = []
|
||||
composerBusinessTimeDraftTouched.value = false
|
||||
clearAttachedFiles()
|
||||
if (fileInputRef.value) {
|
||||
fileInputRef.value.value = ''
|
||||
@@ -4769,6 +4924,12 @@ export default {
|
||||
department_name: user.department || user.departmentName || '',
|
||||
position: user.position || '',
|
||||
grade: user.grade || '',
|
||||
employee_no: user.employeeNo || user.employee_no || '',
|
||||
manager_name: user.managerName || user.manager_name || '',
|
||||
employee_location: user.location || '',
|
||||
cost_center: user.costCenter || user.cost_center || '',
|
||||
finance_owner_name: user.financeOwnerName || user.finance_owner_name || '',
|
||||
employee_risk_profile: user.riskProfile && typeof user.riskProfile === 'object' ? user.riskProfile : {},
|
||||
...buildClientTimeContext(),
|
||||
session_type: activeSessionType.value,
|
||||
entry_source: props.entrySource,
|
||||
@@ -4802,16 +4963,6 @@ export default {
|
||||
? ''
|
||||
: String(payload?.result?.draft_payload?.claim_id || '').trim() || draftClaimId.value
|
||||
|
||||
const resolvedDraftClaimId = String(payload?.result?.draft_payload?.claim_id || draftClaimId.value || '').trim()
|
||||
if (!isKnowledgeSession.value && resolvedDraftClaimId && files.length) {
|
||||
try {
|
||||
await syncComposerFilesToDraft(resolvedDraftClaimId, files)
|
||||
} catch (error) {
|
||||
console.warn('Failed to persist composer attachments to draft claim:', error)
|
||||
toast(error?.message || '票据已识别,但附件持久化失败,请重试上传。')
|
||||
}
|
||||
}
|
||||
|
||||
replaceMessage(
|
||||
pendingMessage.id,
|
||||
createMessage('assistant', payload?.result?.answer || payload?.result?.message || '智能体已完成处理。', [], {
|
||||
@@ -4832,6 +4983,14 @@ export default {
|
||||
mergeFilePreviews(filePreviews, ocrFilePreviews)
|
||||
)
|
||||
completeFlowResult(payload, flowRunDetail)
|
||||
|
||||
const resolvedDraftClaimId = String(payload?.result?.draft_payload?.claim_id || draftClaimId.value || '').trim()
|
||||
if (!isKnowledgeSession.value && resolvedDraftClaimId && files.length) {
|
||||
syncComposerFilesToDraft(resolvedDraftClaimId, files).catch((error) => {
|
||||
console.warn('Failed to persist composer attachments to draft claim:', error)
|
||||
toast(error?.message || '票据已识别,但附件原件保存失败,请重试上传。')
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
clearFlowSimulationTimers()
|
||||
failCurrentFlowStep(error)
|
||||
@@ -5144,6 +5303,7 @@ export default {
|
||||
toggleComposerDatePicker,
|
||||
closeComposerDatePicker,
|
||||
setComposerDateMode,
|
||||
handleComposerDateInputChange,
|
||||
removeComposerBusinessTimeTag,
|
||||
flowSteps,
|
||||
flowRunId,
|
||||
@@ -5213,7 +5373,7 @@ export default {
|
||||
reviewRiskSummary,
|
||||
reviewRiskItems,
|
||||
reviewRiskEmpty,
|
||||
reviewRiskActionAvailable,
|
||||
reviewRiskDetailDialog,
|
||||
recognizedNarratives,
|
||||
reviewRecognitionNotes,
|
||||
reviewDocumentSummaries,
|
||||
@@ -5298,7 +5458,8 @@ export default {
|
||||
selectReviewCategory,
|
||||
selectReviewOtherCategory,
|
||||
queryDraftByClaimNo,
|
||||
explainCurrentReviewRisk,
|
||||
openReviewRiskDetail,
|
||||
closeReviewRiskDetail,
|
||||
goReviewDocument,
|
||||
openActiveReviewDocumentPreview,
|
||||
closeDocumentPreview,
|
||||
|
||||
Reference in New Issue
Block a user