feat: 新增预算费控模型与报销审批流引擎
后端新增预算费控服务和报销单审批流模块,引入申请人费用画像 算法,优化知识库 RAG 运行时和同步逻辑,完善报销单工作流常 量和明细同步,更新差旅报销规则电子表格,前端新增预算分析 组件和数字员工模型,完善审批对话框和洞察面板交互,优化侧 边栏和顶栏样式,补充单元测试。
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user