feat: 新增预算费控模型与报销审批流引擎

后端新增预算费控服务和报销单审批流模块,引入申请人费用画像
算法,优化知识库 RAG 运行时和同步逻辑,完善报销单工作流常
量和明细同步,更新差旅报销规则电子表格,前端新增预算分析
组件和数字员工模型,完善审批对话框和洞察面板交互,优化侧
边栏和顶栏样式,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-27 17:31:27 +08:00
parent cbb98f4469
commit d4d5d40569
75 changed files with 5393 additions and 686 deletions

View File

@@ -22,14 +22,15 @@
<label
v-for="option in options"
:key="option.code"
:class="['return-reason-option', { active: selectedCodes.includes(option.code) }]"
:class="['return-reason-option', { active: isOptionActive(option.code) }]"
>
<input
v-model="selectedCodes"
type="checkbox"
:type="application ? 'radio' : 'checkbox'"
:name="application ? 'application-return-reason' : undefined"
:checked="isOptionActive(option.code)"
:value="option.code"
:disabled="busy"
@change="handleOptionChange"
@change="handleOptionChange(option)"
/>
<i :class="option.icon"></i>
<strong>{{ option.label }}</strong>
@@ -99,6 +100,12 @@ const APPLICATION_RETURN_REASON_OPTIONS = [
label: '前置材料需补充',
icon: 'mdi mdi-file-document-plus-outline',
defaultReason: '请补充会议通知、客户邀约、项目安排或其他能支撑申请必要性的材料。'
},
{
code: 'application_other',
label: '其他',
icon: 'mdi mdi-pencil-box-outline',
defaultReason: ''
}
]
@@ -117,12 +124,18 @@ const props = defineProps({
const emit = defineEmits(['close', 'confirm'])
const selectedCodes = ref([])
const selectedApplicationCode = ref('')
const reasonText = ref('')
const touched = ref(false)
const selectionTouched = ref(false)
const lastAutoReason = ref('')
const options = computed(() => (props.application ? APPLICATION_RETURN_REASON_OPTIONS : CLAIM_RETURN_REASON_OPTIONS))
const selectedReasonCodes = computed(() => (
props.application
? (selectedApplicationCode.value ? [selectedApplicationCode.value] : [])
: selectedCodes.value
))
const dialogBadge = computed(() => (props.application ? '退回申请' : '退回单据'))
const optionsTitle = computed(() => (props.application ? '退单选项' : '默认风险点'))
const optionsAriaLabel = computed(() => (props.application ? '申请退单选项' : '默认退回风险点'))
@@ -133,10 +146,10 @@ const reasonPlaceholder = computed(() => (
))
const trimmedReason = computed(() => reasonText.value.trim())
const selectionError = computed(() => {
if (!props.application || !selectionTouched.value || selectedCodes.value.length > 0) {
if (!props.application || !selectionTouched.value || selectedReasonCodes.value.length > 0) {
return ''
}
return '请选择至少一个退单选项,便于后续看板统计。'
return '请选择一个退单选项,便于后续看板统计。'
})
const reasonError = computed(() => {
if (!touched.value || trimmedReason.value.length >= 6) {
@@ -159,6 +172,7 @@ watch(
(open) => {
if (open) {
selectedCodes.value = []
selectedApplicationCode.value = ''
reasonText.value = ''
touched.value = false
selectionTouched.value = false
@@ -167,25 +181,35 @@ watch(
}
)
watch(selectedCodes, () => {
if (!props.application) {
return
}
const defaultReason = selectedCodes.value
.map((code) => options.value.find((option) => option.code === code)?.defaultReason || '')
.filter(Boolean)
.join('\n')
function syncApplicationDefaultReason(option) {
const defaultReason = String(option?.defaultReason || '').trim()
const canAutoFill = !touched.value || !reasonText.value.trim() || reasonText.value === lastAutoReason.value
if (canAutoFill) {
reasonText.value = defaultReason
}
lastAutoReason.value = defaultReason
})
}
function handleOptionChange() {
function isOptionActive(code) {
return props.application ? selectedApplicationCode.value === code : selectedCodes.value.includes(code)
}
function handleOptionChange(option) {
selectionTouched.value = true
if (props.application) {
selectedApplicationCode.value = option.code
syncApplicationDefaultReason(option)
return
}
const selected = new Set(selectedCodes.value)
if (selected.has(option.code)) {
selected.delete(option.code)
} else {
selected.add(option.code)
}
selectedCodes.value = Array.from(selected)
}
function handleClose() {
@@ -197,13 +221,13 @@ function handleClose() {
function handleConfirm() {
touched.value = true
selectionTouched.value = true
if ((props.application && selectedCodes.value.length === 0) || trimmedReason.value.length < 6 || props.busy) {
if ((props.application && selectedReasonCodes.value.length === 0) || trimmedReason.value.length < 6 || props.busy) {
return
}
emit('confirm', {
reason: trimmedReason.value,
reason_codes: [...selectedCodes.value]
reason_codes: [...selectedReasonCodes.value]
})
}
</script>