feat: 数字员工财务报告体系与定时提醒及看板快照调度
- 新增数字员工财务报告生成、邮件投递与渲染调度器 - 引入员工画像扫描调度与定时提醒任务 - 完善财务看板快照、排行口径与部门人员占比计算 - 优化数字员工工作看板仪表盘与技能目录 - 增强前端总览页图表、工作台摘要与顶部导航栏交互 - 新增差旅申请规划推动提醒与报销创建会话状态管理 - 补充财务报告、看板调度、数字员工工作记录测试覆盖
This commit is contained in:
@@ -4,6 +4,7 @@ import { ElButton } from 'element-plus/es/components/button/index.mjs'
|
||||
import BudgetTrendChart from '../../components/charts/BudgetTrendChart.vue'
|
||||
import EnterpriseDetailCard from '../../components/shared/EnterpriseDetailCard.vue'
|
||||
import EnterpriseDetailPage from '../../components/shared/EnterpriseDetailPage.vue'
|
||||
import EnterprisePagination from '../../components/shared/EnterprisePagination.vue'
|
||||
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
|
||||
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
|
||||
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
|
||||
@@ -98,6 +99,7 @@ export default {
|
||||
emits: ['openAssistant', 'detail-open-change', 'detail-topbar-change'],
|
||||
components: {
|
||||
BudgetTrendChart,
|
||||
EnterprisePagination,
|
||||
EnterpriseSelect,
|
||||
EnterpriseDetailCard,
|
||||
EnterpriseDetailPage,
|
||||
@@ -169,9 +171,6 @@ export default {
|
||||
const currentBudgetPage = computed(() =>
|
||||
Math.min(Math.max(1, budgetPage.value), totalBudgetPages.value)
|
||||
)
|
||||
const budgetPageNumbers = computed(() =>
|
||||
Array.from({ length: totalBudgetPages.value }, (_, index) => index + 1)
|
||||
)
|
||||
const visibleBudgetRows = computed(() => {
|
||||
const pageSize = Number(budgetPageSize.value || 8)
|
||||
const start = (currentBudgetPage.value - 1) * pageSize
|
||||
@@ -227,7 +226,7 @@ export default {
|
||||
artLabel: '预算列表为空',
|
||||
tips: ['可以调整年度、季度、状态或关键词后重试。']
|
||||
}))
|
||||
const pageSummary = computed(() => `共 ${totalBudgetRows.value} 条,目前第 ${currentBudgetPage.value} 页`)
|
||||
const pageSummary = computed(() => `共 ${totalBudgetRows.value} 条,目前第 ${currentBudgetPage.value} / ${totalBudgetPages.value} 页`)
|
||||
|
||||
function buildBudgetAssistantContext(row, mode = 'edit') {
|
||||
if (!row) return null
|
||||
@@ -425,7 +424,6 @@ export default {
|
||||
budgetKeyword,
|
||||
budgetLoading,
|
||||
budgetPage: currentBudgetPage,
|
||||
budgetPageNumbers,
|
||||
budgetPageSize,
|
||||
budgetPageSizeOptions,
|
||||
budgetScopeTabs,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
|
||||
import EnterprisePagination from '../../components/shared/EnterprisePagination.vue'
|
||||
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
|
||||
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
|
||||
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
|
||||
@@ -452,6 +453,7 @@ export default {
|
||||
name: 'EmployeeManagementView',
|
||||
components: {
|
||||
ConfirmDialog,
|
||||
EnterprisePagination,
|
||||
EnterpriseSelect,
|
||||
TableLoadingState,
|
||||
TableEmptyState
|
||||
@@ -672,6 +674,7 @@ export default {
|
||||
|
||||
const totalCount = computed(() => filteredEmployees.value.length)
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize.value)))
|
||||
const pageSummary = computed(() => `共 ${totalCount.value} 条,目前第 ${currentPage.value} / ${totalPages.value} 页`)
|
||||
|
||||
const visibleEmployees = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
@@ -1469,6 +1472,7 @@ export default {
|
||||
hasEmployeeFilters,
|
||||
totalCount,
|
||||
totalPages,
|
||||
pageSummary,
|
||||
resetFilters,
|
||||
handleEmployeeEmptyAction,
|
||||
openEmployeeDetail,
|
||||
|
||||
@@ -152,12 +152,6 @@ export default {
|
||||
filteredSystemLogEntries.value.filter((entry) => entry.level === 'INFO').length
|
||||
)
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize.value)))
|
||||
const visiblePageItems = computed(() => {
|
||||
if (totalPages.value <= 6) {
|
||||
return Array.from({ length: totalPages.value }, (_, index) => index + 1)
|
||||
}
|
||||
return [1, 2, 3, 4, 5, 'ellipsis', totalPages.value]
|
||||
})
|
||||
const visibleSystemLogEntries = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return filteredSystemLogEntries.value.slice(start, start + pageSize.value)
|
||||
@@ -300,7 +294,6 @@ export default {
|
||||
systemSearchKeyword,
|
||||
totalCount,
|
||||
totalPages,
|
||||
visiblePageItems,
|
||||
visibleSystemLogEntries
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
|
||||
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
|
||||
import EnterprisePagination from '../../components/shared/EnterprisePagination.vue'
|
||||
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
|
||||
import { useSystemState } from '../../composables/useSystemState.js'
|
||||
import { useToast } from '../../composables/useToast.js'
|
||||
@@ -87,7 +87,7 @@ export default {
|
||||
name: 'PoliciesView',
|
||||
components: {
|
||||
ConfirmDialog,
|
||||
EnterpriseSelect,
|
||||
EnterprisePagination,
|
||||
TableLoadingState
|
||||
},
|
||||
emits: ['summary-change'],
|
||||
@@ -182,9 +182,10 @@ export default {
|
||||
&& activeFolderIngestStats.value.syncing === 0
|
||||
)
|
||||
|
||||
const totalCount = computed(() => filteredDocuments.value.length)
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize.value)))
|
||||
const visibleDocuments = computed(() => {
|
||||
const totalCount = computed(() => filteredDocuments.value.length)
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize.value)))
|
||||
const pageSummary = computed(() => `共 ${totalCount.value} 条,目前第 ${currentPage.value} / ${totalPages.value} 页`)
|
||||
const visibleDocuments = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return filteredDocuments.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
@@ -636,6 +637,7 @@ export default {
|
||||
loading,
|
||||
pageSize,
|
||||
pageSizeOptions,
|
||||
pageSummary,
|
||||
pageSizes,
|
||||
onlyOfficeError,
|
||||
onlyOfficeHostId,
|
||||
|
||||
@@ -49,6 +49,13 @@ import {
|
||||
buildLocalApplicationPreviewMessage,
|
||||
normalizeApplicationPreview
|
||||
} from '../../utils/expenseApplicationPreview.js'
|
||||
import {
|
||||
TRAVEL_PLANNING_ACTION_GENERATE,
|
||||
TRAVEL_PLANNING_ACTION_SKIP,
|
||||
buildTravelPlanningNudgeMessage,
|
||||
buildTravelPlanningRecommendation,
|
||||
buildTravelPlanningSuggestedActions
|
||||
} from '../../utils/travelApplicationPlanning.js'
|
||||
import {
|
||||
calculateTravelReimbursement,
|
||||
createExpenseClaimItem,
|
||||
@@ -524,6 +531,14 @@ export default {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
initialPromptAutoSubmit: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
initialApplicationPreview: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
initialFiles: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -629,7 +644,9 @@ export default {
|
||||
handleApplicationPreviewEditorKeydown
|
||||
} = useApplicationPreviewEditor({
|
||||
persistSessionState,
|
||||
toast
|
||||
toast,
|
||||
calculateTravelReimbursement,
|
||||
currentUser
|
||||
})
|
||||
|
||||
function applyLinkedApplicationPreviewDateSelection(selection) {
|
||||
@@ -1372,6 +1389,14 @@ export default {
|
||||
currentInsight.value =
|
||||
currentInsight.value
|
||||
|| buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value, currentUser.value)
|
||||
if (props.initialApplicationPreview && typeof props.initialApplicationPreview === 'object') {
|
||||
const applicationPreview = normalizeApplicationPreview(props.initialApplicationPreview)
|
||||
messages.value.push(createMessage('assistant', buildLocalApplicationPreviewMessage(applicationPreview), [], {
|
||||
meta: ['修改申请'],
|
||||
applicationPreview
|
||||
}))
|
||||
persistSessionState()
|
||||
}
|
||||
if (props.initialPrompt?.trim() || props.initialFiles.length) {
|
||||
const initialMerge = mergeFilesWithLimit([], Array.from(props.initialFiles), MAX_ATTACHMENTS)
|
||||
composerDraft.value = props.initialPrompt.trim()
|
||||
@@ -1380,7 +1405,12 @@ export default {
|
||||
if (initialMerge.overflowCount > 0) {
|
||||
toast(`一次最多上传 ${MAX_ATTACHMENTS} 份附件,已保留前 ${MAX_ATTACHMENTS} 份。`)
|
||||
}
|
||||
submitComposer()
|
||||
nextTick(() => {
|
||||
adjustComposerTextareaHeight()
|
||||
})
|
||||
if (props.initialPromptAutoSubmit !== false) {
|
||||
submitComposer()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1576,6 +1606,32 @@ export default {
|
||||
if (await handleGuidedSuggestedAction(message, action)) return
|
||||
if (await handleSceneSelectionApplicationGate(message, action)) return
|
||||
|
||||
if (actionType === TRAVEL_PLANNING_ACTION_GENERATE) {
|
||||
if (!lockSuggestedActionMessage(message, action)) return
|
||||
const sourcePreview = action?.payload?.applicationPreview || action?.payload?.preview || null
|
||||
const sourceDraftPayload = action?.payload?.draftPayload || action?.payload?.draft_payload || null
|
||||
const recommendation = buildTravelPlanningRecommendation(sourcePreview, sourceDraftPayload)
|
||||
if (recommendation) {
|
||||
messages.value.push(createMessage('user', '生成行程规划'))
|
||||
messages.value.push(createMessage('assistant', recommendation, [], {
|
||||
meta: ['行程规划建议']
|
||||
}))
|
||||
nextTick(scrollToBottom)
|
||||
persistSessionState()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (actionType === TRAVEL_PLANNING_ACTION_SKIP) {
|
||||
if (!lockSuggestedActionMessage(message, action)) return
|
||||
messages.value.push(createMessage('assistant', '好的,本次先保留申请结果。后续需要规划交通或酒店时,可以继续在这里告诉我。', [], {
|
||||
meta: ['暂不规划']
|
||||
}))
|
||||
nextTick(scrollToBottom)
|
||||
persistSessionState()
|
||||
return
|
||||
}
|
||||
|
||||
if (actionType === ASSISTANT_SCOPE_ACTION_SWITCH) {
|
||||
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
||||
const targetSessionType = String(actionPayload.session_type || '').trim()
|
||||
@@ -2033,6 +2089,17 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveApplicationEditClaimId() {
|
||||
if (activeSessionType.value !== SESSION_TYPE_APPLICATION) {
|
||||
return ''
|
||||
}
|
||||
const request = linkedRequest.value || {}
|
||||
if (!request.applicationEditMode) {
|
||||
return ''
|
||||
}
|
||||
return String(request.claimId || request.claim_id || '').trim()
|
||||
}
|
||||
|
||||
async function confirmApplicationSubmit() {
|
||||
const message = applicationSubmitConfirmDialog.value.message
|
||||
if (!message || submitting.value || reviewActionBusy.value) {
|
||||
@@ -2044,6 +2111,7 @@ export default {
|
||||
const applicationSubmitText = applicationPreview
|
||||
? buildApplicationPreviewSubmitText(applicationPreview)
|
||||
: '确认提交'
|
||||
const applicationEditClaimId = resolveApplicationEditClaimId()
|
||||
applicationSubmitConfirmDialog.value = {
|
||||
open: false,
|
||||
message: null
|
||||
@@ -2059,7 +2127,16 @@ export default {
|
||||
feedbackOperationType: 'submit_application',
|
||||
extraContext: {
|
||||
application_preview: applicationPreview,
|
||||
user_input_text: applicationSubmitText
|
||||
user_input_text: applicationSubmitText,
|
||||
...(applicationEditClaimId
|
||||
? {
|
||||
application_edit_claim_id: applicationEditClaimId,
|
||||
application_edit_claim_no: String(linkedRequest.value?.claimNo || linkedRequest.value?.id || '').trim(),
|
||||
application_edit_mode: true,
|
||||
draft_claim_id: applicationEditClaimId,
|
||||
selected_claim_id: applicationEditClaimId
|
||||
}
|
||||
: {})
|
||||
}
|
||||
})
|
||||
const draftPayload = payload?.result?.draft_payload || {}
|
||||
@@ -2074,6 +2151,23 @@ export default {
|
||||
documentType: 'application'
|
||||
})
|
||||
}
|
||||
const planningText = buildTravelPlanningNudgeMessage(applicationPreview, draftPayload)
|
||||
const planningActions = buildTravelPlanningSuggestedActions(applicationPreview, draftPayload).map((action) => ({
|
||||
...action,
|
||||
payload: {
|
||||
...(action.payload || {}),
|
||||
applicationPreview,
|
||||
draftPayload
|
||||
}
|
||||
}))
|
||||
if (planningText && planningActions.length) {
|
||||
messages.value.push(createMessage('assistant', planningText, [], {
|
||||
meta: ['行程规划推荐'],
|
||||
suggestedActions: planningActions
|
||||
}))
|
||||
persistSessionState()
|
||||
nextTick(scrollToBottom)
|
||||
}
|
||||
} finally {
|
||||
reviewActionBusy.value = false
|
||||
}
|
||||
|
||||
@@ -460,11 +460,17 @@ export default {
|
||||
const isDraftRequest = computed(() => request.value.approvalKey === 'draft')
|
||||
const isEditableRequest = computed(() => ['draft', 'supplement'].includes(request.value.approvalKey))
|
||||
const canOpenAiEntry = computed(() => isEditableRequest.value)
|
||||
const canModifyReturnedApplication = computed(() => (
|
||||
isApplicationDocument.value
|
||||
&& isEditableRequest.value
|
||||
&& isCurrentApplicant.value
|
||||
&& String(request.value.status || '').trim().toLowerCase() === 'returned'
|
||||
))
|
||||
const canManageCurrentClaim = computed(() => canManageExpenseClaims(currentUser.value))
|
||||
const isArchivedRequest = computed(() => isArchivedRequestView(request.value))
|
||||
const canDeleteRequest = computed(() => {
|
||||
if (isApplicationDocument.value) {
|
||||
return isPlatformAdminUser(currentUser.value)
|
||||
return isPlatformAdminUser(currentUser.value) || (isEditableRequest.value && isCurrentApplicant.value)
|
||||
}
|
||||
if (isArchivedRequest.value) {
|
||||
return canDeleteArchivedExpenseClaims(currentUser.value)
|
||||
@@ -1007,7 +1013,7 @@ export default {
|
||||
if (analysis) {
|
||||
return {
|
||||
label: analysis.label || '已上传',
|
||||
tone: analysis.severity === 'pass' ? 'pass' : analysis.severity || 'low',
|
||||
tone: normalizeRiskTone(analysis.severity || 'low'),
|
||||
headline: analysis.headline || 'AI提示',
|
||||
summary: analysis.summary || '',
|
||||
points: Array.isArray(analysis.points) ? analysis.points : [],
|
||||
@@ -1858,7 +1864,9 @@ export default {
|
||||
toast(
|
||||
isArchivedRequest.value
|
||||
? '已归档单据不能删除,只有高级管理员可以执行删除。'
|
||||
: '当前单据已进入流程,只有高级财务人员可以删除。'
|
||||
: isApplicationDocument.value
|
||||
? '当前申请单已进入审批流程,只有退回后申请人本人或系统管理员可以删除。'
|
||||
: '当前单据已进入流程,只有高级财务人员可以删除。'
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -2019,6 +2027,76 @@ export default {
|
||||
})
|
||||
}
|
||||
|
||||
function buildApplicationEditPreview() {
|
||||
const factEntries = applicationDetailFactItems.value
|
||||
.map((item) => [String(item?.label || '').trim(), String(item?.value || '').trim()])
|
||||
.filter(([label, value]) => label && value)
|
||||
const facts = new Map(factEntries)
|
||||
const pickFact = (...labels) => {
|
||||
for (const label of labels) {
|
||||
const value = facts.get(label)
|
||||
if (value) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
const tripStart = pickFact('出发时间')
|
||||
const tripReturn = pickFact('返回时间')
|
||||
const time = tripStart && tripReturn && tripStart !== tripReturn
|
||||
? `${tripStart} 至 ${tripReturn}`
|
||||
: pickFact('行程时间', '申请时间', '招待时间', '发生时间') || tripStart
|
||||
|
||||
return {
|
||||
sourceText: '修改申请',
|
||||
modelReviewStatus: 'template',
|
||||
fields: {
|
||||
applicationType: pickFact('申请类型') || request.value.typeLabel || '费用申请',
|
||||
applicant: request.value.profileName || request.value.person || request.value.applicant || '',
|
||||
grade: pickFact('职级') || request.value.profileGrade || '',
|
||||
department: request.value.profileDepartment || request.value.departmentName || request.value.department || '',
|
||||
position: request.value.profilePosition || request.value.employeePosition || request.value.position || '',
|
||||
managerName: request.value.profileManager || request.value.managerName || request.value.manager || '',
|
||||
time,
|
||||
location: pickFact('地点') || request.value.location || request.value.city || '',
|
||||
reason: pickFact('事由') || request.value.reason || '',
|
||||
days: pickFact('天数'),
|
||||
transportMode: pickFact('出行方式'),
|
||||
lodgingDailyCap: pickFact('住宿上限/天'),
|
||||
subsidyDailyCap: pickFact('补贴标准/天'),
|
||||
transportPolicy: pickFact('交通费用口径'),
|
||||
policyEstimate: pickFact('规则测算参考'),
|
||||
amount: pickFact('系统预估费用', '用户预估费用', '预计金额') || request.value.amountDisplay || request.value.amount || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleModifyApplication() {
|
||||
if (!canModifyReturnedApplication.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const claimId = String(request.value?.claimId || '').trim()
|
||||
emit('openAssistant', {
|
||||
source: 'application',
|
||||
sessionType: 'application',
|
||||
prompt: '',
|
||||
applicationPreview: buildApplicationEditPreview(),
|
||||
request: {
|
||||
...request.value,
|
||||
applicationEditMode: true
|
||||
},
|
||||
restoreLatestConversation: false,
|
||||
initialPromptAutoSubmit: false,
|
||||
scope: claimId
|
||||
? {
|
||||
type: 'claim',
|
||||
claimId
|
||||
}
|
||||
: null
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
closeAttachmentPreview()
|
||||
})
|
||||
@@ -2032,7 +2110,7 @@ export default {
|
||||
applicationDetailFactItems, relatedApplicationFactItems,
|
||||
approveBusyText, approveConfirmText, approveConfirmTitle, canDeleteRequest, canManageCurrentClaim,
|
||||
canNavigateAttachmentPreview,
|
||||
canOpenAiEntry, canApproveRequest, canPayRequest, canReturnRequest, canSubmit, canPreviewAttachment,
|
||||
canModifyReturnedApplication, canOpenAiEntry, canApproveRequest, canPayRequest, canReturnRequest, canSubmit, canPreviewAttachment,
|
||||
closeApproveConfirmDialog, closeDeleteDialog, closeAttachmentPreview, closePayConfirmDialog, closeSubmitConfirmDialog,
|
||||
closeRiskOverrideDialog,
|
||||
closeReturnDialog, confirmApproveRequest, confirmDeleteRequest, confirmSubmitRequest, confirmReturnRequest,
|
||||
@@ -2046,6 +2124,7 @@ export default {
|
||||
expenseTypeOptions: EXPENSE_TYPE_OPTIONS,
|
||||
goToNextSubmitRisk, goToPreviousSubmitRisk,
|
||||
handleAddExpenseItem, handleApproveRequest, handleDeleteRequest, handleExpenseFileChange,
|
||||
handleModifyApplication,
|
||||
handlePayRequest,
|
||||
handleReturnRequest, handleSubmit, heroFactItems, isApplicationDocument, isDraftRequest, isEditableRequest, isTravelRequest,
|
||||
isMajorExpenseRisk,
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
const DIGITAL_EMPLOYEE_AGENT = 'hermes'
|
||||
export const DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS = ['积累', '升级', '整理', '评估']
|
||||
|
||||
export const DIGITAL_EMPLOYEE_VISIBLE_TASK_TYPES = new Set([
|
||||
'finance_dashboard_snapshot',
|
||||
'digital_employee_reminder_scan',
|
||||
'employee_behavior_profile_scan',
|
||||
'department_expense_baseline_accumulate',
|
||||
'budget_overrun_precontrol_evaluate',
|
||||
'multi_evidence_consistency_evaluate',
|
||||
'travel_spatiotemporal_consistency_evaluate',
|
||||
'global_risk_scan',
|
||||
'finance_policy_knowledge_organize'
|
||||
])
|
||||
|
||||
const TASK_TYPE_LABELS = {
|
||||
finance_dashboard_snapshot: '财务经营快照沉淀',
|
||||
digital_employee_reminder_scan: '定时提醒与待办扫描',
|
||||
daily_risk_scan: '每日风险巡检',
|
||||
global_risk_scan: '财务风险图谱巡检',
|
||||
employee_behavior_profile_scan: '员工行为画像巡检',
|
||||
department_expense_baseline_accumulate: '部门费用基线沉淀',
|
||||
budget_overrun_precontrol_evaluate: '预算占用与超标预警',
|
||||
multi_evidence_consistency_evaluate: '单据多凭证一致性评估',
|
||||
travel_spatiotemporal_consistency_evaluate: '差旅时空一致性评估',
|
||||
weekly_ar_summary: '周度应收账龄汇总',
|
||||
weekly_expense_report: '周度费用洞察',
|
||||
rule_review_digest: '规则待审摘要',
|
||||
@@ -15,9 +33,15 @@ const TASK_TYPE_LABELS = {
|
||||
}
|
||||
|
||||
const TASK_TYPE_SKILL_CATEGORIES = {
|
||||
finance_dashboard_snapshot: '整理',
|
||||
digital_employee_reminder_scan: '升级',
|
||||
daily_risk_scan: '评估',
|
||||
global_risk_scan: '评估',
|
||||
employee_behavior_profile_scan: '评估',
|
||||
employee_behavior_profile_scan: '积累',
|
||||
department_expense_baseline_accumulate: '积累',
|
||||
budget_overrun_precontrol_evaluate: '评估',
|
||||
multi_evidence_consistency_evaluate: '评估',
|
||||
travel_spatiotemporal_consistency_evaluate: '评估',
|
||||
weekly_ar_summary: '整理',
|
||||
weekly_expense_report: '整理',
|
||||
rule_review_digest: '升级',
|
||||
@@ -145,6 +169,12 @@ export function isDigitalEmployeeAsset(source = {}) {
|
||||
)
|
||||
}
|
||||
|
||||
export function shouldDisplayDigitalEmployeeAsset(source = {}) {
|
||||
const content = parseDigitalEmployeeContent(source.current_version_content)
|
||||
const taskType = resolveDigitalEmployeeTaskType(source, content)
|
||||
return DIGITAL_EMPLOYEE_VISIBLE_TASK_TYPES.has(taskType)
|
||||
}
|
||||
|
||||
export function formatDigitalEmployeeCron(value) {
|
||||
const raw = normalizeDigitalEmployeeText(value)
|
||||
if (!raw) {
|
||||
|
||||
@@ -18,9 +18,31 @@ const KNOWLEDGE_JOB_TYPES = new Set([
|
||||
'finance_policy_knowledge_organize'
|
||||
])
|
||||
|
||||
export const VISIBLE_DIGITAL_EMPLOYEE_WORK_TASK_TYPES = new Set([
|
||||
'finance_dashboard_snapshot',
|
||||
'digital_employee_reminder_scan',
|
||||
'employee_behavior_profile_scan',
|
||||
'department_expense_baseline_accumulate',
|
||||
'budget_overrun_precontrol_evaluate',
|
||||
'multi_evidence_consistency_evaluate',
|
||||
'travel_spatiotemporal_consistency_evaluate',
|
||||
'global_risk_scan',
|
||||
'finance_policy_knowledge_organize'
|
||||
])
|
||||
|
||||
const DAILY_COMPACT_TASK_TYPES = new Set([
|
||||
'finance_dashboard_snapshot'
|
||||
])
|
||||
|
||||
const TASK_TYPE_LABELS = {
|
||||
finance_dashboard_snapshot: '财务经营快照沉淀',
|
||||
digital_employee_reminder_scan: '定时提醒与待办扫描',
|
||||
global_risk_scan: '财务风险图谱巡检',
|
||||
employee_behavior_profile_scan: '员工行为画像巡检',
|
||||
department_expense_baseline_accumulate: '部门费用基线沉淀',
|
||||
budget_overrun_precontrol_evaluate: '预算占用与超标预警',
|
||||
multi_evidence_consistency_evaluate: '单据多凭证一致性评估',
|
||||
travel_spatiotemporal_consistency_evaluate: '差旅时空一致性评估',
|
||||
risk_clue_collect: '风险线索归集',
|
||||
finance_policy_knowledge_organize: '知识制度整理',
|
||||
knowledge_index_sync: '知识制度整理',
|
||||
@@ -29,10 +51,16 @@ const TASK_TYPE_LABELS = {
|
||||
}
|
||||
|
||||
const TASK_CODE_TO_TYPE = {
|
||||
'task.hermes.finance_dashboard_snapshot': 'finance_dashboard_snapshot',
|
||||
'task.hermes.digital_employee_reminder_scan': 'digital_employee_reminder_scan',
|
||||
'task.hermes.global_risk_scan': 'global_risk_scan',
|
||||
'task.hermes.employee_behavior_profile_scan': 'employee_behavior_profile_scan',
|
||||
'task.hermes.risk_rule_discovery': 'risk_clue_collect',
|
||||
'task.hermes.finance_policy_knowledge_organize': 'finance_policy_knowledge_organize'
|
||||
'task.hermes.department_expense_baseline_accumulate': 'department_expense_baseline_accumulate',
|
||||
'task.hermes.budget_overrun_precontrol_evaluate': 'budget_overrun_precontrol_evaluate',
|
||||
'task.hermes.multi_evidence_consistency_evaluate': 'multi_evidence_consistency_evaluate',
|
||||
'task.hermes.travel_spatiotemporal_consistency_evaluate': 'travel_spatiotemporal_consistency_evaluate',
|
||||
'task.hermes.finance_policy_knowledge_organize': 'finance_policy_knowledge_organize',
|
||||
'task.hermes.risk_rule_discovery': 'risk_clue_collect'
|
||||
}
|
||||
|
||||
function toObject(value) {
|
||||
@@ -52,6 +80,12 @@ function resolveTaskTypeFromToolName(value) {
|
||||
if (name.includes('financial_risk_graph')) {
|
||||
return 'global_risk_scan'
|
||||
}
|
||||
if (name.includes('finance_dashboard_snapshot') || name.includes('finance_dashboard')) {
|
||||
return 'finance_dashboard_snapshot'
|
||||
}
|
||||
if (name.includes('digital_employee_reminder') || name.includes('reminder')) {
|
||||
return 'digital_employee_reminder_scan'
|
||||
}
|
||||
if (name.includes('employee_behavior_profile')) {
|
||||
return 'employee_behavior_profile_scan'
|
||||
}
|
||||
@@ -128,6 +162,43 @@ export function resolveWorkRecordTaskType(run) {
|
||||
return ''
|
||||
}
|
||||
|
||||
export function isVisibleDigitalEmployeeWorkRecord(run) {
|
||||
const taskType = resolveWorkRecordTaskType(run)
|
||||
return VISIBLE_DIGITAL_EMPLOYEE_WORK_TASK_TYPES.has(taskType)
|
||||
}
|
||||
|
||||
function resolveWorkRecordDayKey(run) {
|
||||
const date = new Date(run?.started_at || run?.finished_at || '')
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return 'unknown'
|
||||
}
|
||||
return date.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
export function compactDigitalEmployeeWorkRecords(items = []) {
|
||||
const rows = []
|
||||
const compactedKeys = new Set()
|
||||
|
||||
for (const run of items) {
|
||||
const taskType = resolveWorkRecordTaskType(run)
|
||||
if (!VISIBLE_DIGITAL_EMPLOYEE_WORK_TASK_TYPES.has(taskType)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (DAILY_COMPACT_TASK_TYPES.has(taskType)) {
|
||||
const key = `${taskType}:${resolveWorkRecordDayKey(run)}`
|
||||
if (compactedKeys.has(key)) {
|
||||
continue
|
||||
}
|
||||
compactedKeys.add(key)
|
||||
}
|
||||
|
||||
rows.push(run)
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
export function resolveWorkRecordTaskLabel(run) {
|
||||
const taskType = resolveWorkRecordTaskType(run)
|
||||
return TASK_TYPE_LABELS[taskType] || ''
|
||||
@@ -135,6 +206,12 @@ export function resolveWorkRecordTaskLabel(run) {
|
||||
|
||||
export function resolveWorkRecordProductKind(run) {
|
||||
const taskType = resolveWorkRecordTaskType(run)
|
||||
if (taskType === 'finance_dashboard_snapshot') {
|
||||
return 'finance_snapshot'
|
||||
}
|
||||
if (taskType === 'digital_employee_reminder_scan') {
|
||||
return 'reminder_scan'
|
||||
}
|
||||
if (taskType === 'global_risk_scan') {
|
||||
return 'risk_graph'
|
||||
}
|
||||
|
||||
@@ -458,6 +458,7 @@ export function sanitizeRequest(request) {
|
||||
|
||||
const normalized = {
|
||||
claimId: String(request.claimId || request.claim_id || '').trim(),
|
||||
claimNo: String(request.claimNo || request.claim_no || request.documentNo || '').trim(),
|
||||
id: String(request.id || '').trim(),
|
||||
typeLabel: String(request.typeLabel || request.category || '').trim(),
|
||||
reason: String(request.reason || request.title || '').trim(),
|
||||
@@ -468,7 +469,8 @@ export function sanitizeRequest(request) {
|
||||
amount: String(request.amount || '').trim(),
|
||||
node: String(request.node || '').trim(),
|
||||
approval: String(request.approval || '').trim(),
|
||||
travel: String(request.travel || '').trim()
|
||||
travel: String(request.travel || '').trim(),
|
||||
applicationEditMode: Boolean(request.applicationEditMode || request.application_edit_mode)
|
||||
}
|
||||
|
||||
return Object.values(normalized).some(Boolean) ? normalized : null
|
||||
|
||||
@@ -150,7 +150,7 @@ function resolveRequestBusinessStage(request = {}) {
|
||||
|
||||
function normalizeTone(value) {
|
||||
const tone = normalizeText(value).toLowerCase()
|
||||
if (tone === 'pass') return 'pass'
|
||||
if (['pass', 'success', 'ok', 'normal', 'none', 'compliant', 'approved'].includes(tone)) return 'pass'
|
||||
if (tone === 'high') return 'high'
|
||||
if (tone === 'medium') return 'medium'
|
||||
if (tone === 'low') return 'low'
|
||||
|
||||
@@ -2,9 +2,13 @@ import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
APPLICATION_TRANSPORT_MODE_OPTIONS,
|
||||
applyApplicationPolicyEstimateError,
|
||||
applyApplicationPolicyEstimateResult,
|
||||
buildApplicationPreviewRows,
|
||||
buildApplicationPolicyEstimateRequest,
|
||||
buildLocalApplicationPreviewMessage,
|
||||
normalizeApplicationPreview,
|
||||
resolveApplicationDaysFromDateRange,
|
||||
refreshApplicationPreviewTransportEstimate
|
||||
} from '../../utils/expenseApplicationPreview.js'
|
||||
import { waitForMockApplicationTransportQuote } from '../../utils/expenseApplicationEstimate.js'
|
||||
@@ -44,6 +48,27 @@ function shouldRefreshTransportEstimate(fieldKey) {
|
||||
return ['transportMode', 'time', '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 nextFields = {
|
||||
...fields,
|
||||
[editor.fieldKey]: nextValue
|
||||
}
|
||||
if (editor.fieldKey === 'time') {
|
||||
const resolvedDays = resolveApplicationDaysFromDateRange(nextValue)
|
||||
if (resolvedDays) {
|
||||
nextFields.days = resolvedDays
|
||||
}
|
||||
}
|
||||
return nextFields
|
||||
}
|
||||
|
||||
function buildTransportEstimatePendingPreview(preview = {}) {
|
||||
const fields = preview?.fields || {}
|
||||
return normalizeApplicationPreview({
|
||||
@@ -57,9 +82,29 @@ function buildTransportEstimatePendingPreview(preview = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
export function useApplicationPreviewEditor({ persistSessionState, toast } = {}) {
|
||||
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 || {})
|
||||
}
|
||||
@@ -158,25 +203,29 @@ export function useApplicationPreviewEditor({ persistSessionState, toast } = {})
|
||||
}
|
||||
const nextPreview = normalizeApplicationPreview({
|
||||
...message.applicationPreview,
|
||||
fields: {
|
||||
...(message.applicationPreview.fields || {}),
|
||||
[editor.fieldKey]: nextValue
|
||||
}
|
||||
fields: buildEditedApplicationPreviewFields(
|
||||
message.applicationPreview.fields || {},
|
||||
editor,
|
||||
nextValue
|
||||
)
|
||||
})
|
||||
const needRefreshTransport = shouldRefreshTransportEstimate(editor.fieldKey) && String(nextPreview.fields?.transportMode || '').trim()
|
||||
message.applicationPreview = needRefreshTransport
|
||||
const needRefreshEstimate = shouldRefreshTransportEstimate(editor.fieldKey)
|
||||
const transportMode = String(nextPreview.fields?.transportMode || '').trim()
|
||||
message.applicationPreview = needRefreshEstimate
|
||||
? buildTransportEstimatePendingPreview(nextPreview)
|
||||
: nextPreview
|
||||
message.text = buildLocalApplicationPreviewMessage(message.applicationPreview)
|
||||
cancelApplicationPreviewEditor()
|
||||
persistSessionState?.()
|
||||
if (needRefreshTransport) {
|
||||
await waitForMockApplicationTransportQuote({
|
||||
transportMode: nextPreview.fields.transportMode,
|
||||
location: nextPreview.fields.matchedCity || nextPreview.fields.location,
|
||||
time: nextPreview.fields.time
|
||||
})
|
||||
const refreshedPreview = refreshApplicationPreviewTransportEstimate(nextPreview)
|
||||
if (needRefreshEstimate) {
|
||||
if (transportMode) {
|
||||
await waitForMockApplicationTransportQuote({
|
||||
transportMode,
|
||||
location: nextPreview.fields.matchedCity || nextPreview.fields.location,
|
||||
time: nextPreview.fields.time
|
||||
})
|
||||
}
|
||||
const refreshedPreview = await refreshApplicationPreviewEstimate(nextPreview)
|
||||
message.applicationPreview = refreshedPreview
|
||||
message.text = buildLocalApplicationPreviewMessage(refreshedPreview)
|
||||
persistSessionState?.()
|
||||
|
||||
@@ -207,6 +207,7 @@ export function useTravelReimbursementSessionState({
|
||||
shouldPersistLocalSnapshot
|
||||
&& props.entrySource !== 'budget'
|
||||
&& !String(props.initialPrompt || '').trim()
|
||||
&& !props.initialApplicationPreview
|
||||
&& !props.initialFiles.length
|
||||
const persistedInitialSnapshot = readAssistantSessionSnapshot(resolveCurrentUserId(), initialSessionType)
|
||||
const persistedInitialState = canRestorePersistedInitialState
|
||||
|
||||
Reference in New Issue
Block a user