feat: 新增员工行为画像算法与费用风险标签体系

后端新增员工行为画像算法模块,支持标签规则引擎和评分计算,
完善员工模型、银行信息、序列化和导入逻辑,优化报销审批流
和工作流常量,增强 Hermes 同步和知识同步能力,前端新增费
用画像详情弹窗、雷达图和风险卡片组件,完善登录页和工作台
样式,优化文档中心和归档中心交互,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-28 12:09:49 +08:00
parent 04cd6d0f81
commit 8a4a777be7
96 changed files with 9835 additions and 704 deletions

View File

@@ -9,8 +9,7 @@
<article class="panel assistant-hero" :style="{ '--assistant-bg-image': `url(${homepageBackground})` }">
<div class="assistant-copy">
<h1>你的专属 <span>AI 财务助手</span></h1>
<p>智能理解财务业务提供数据洞察与方案建议高效处理日常事务</p>
<h1>{{ displayUserName }}我是您的 <span>AI 费用助手</span></h1>
<input
ref="fileInputRef"
@@ -92,10 +91,10 @@
</div>
</article>
<div class="capability-grid" aria-label="AI 财务助手能力">
<div :class="['capability-grid', capabilityGridClass]" aria-label="AI 财务助手能力">
<button
v-for="item in assistantCapabilities"
:key="item.title"
v-for="item in visibleAssistantCapabilities"
:key="item.key"
type="button"
class="capability-card panel"
:class="`capability-card--${item.tone}`"
@@ -226,7 +225,9 @@
<button
type="button"
class="detail-action"
@click="openPromptAssistant('查看我的费用画像详情,并总结 AI 使用、提单效率和预审通过率。')"
aria-haspopup="dialog"
:aria-expanded="expenseProfileModalOpen"
@click="openExpenseProfileModal"
>
<span>查看详情</span>
<i class="mdi mdi-chevron-right"></i>
@@ -255,12 +256,24 @@
</article>
</aside>
</div>
<ExpenseProfileDetailModal
:visible="expenseProfileModalOpen"
:user-name="displayUserName"
:metrics="expenseProfileModalMetrics"
:tags="expenseProfileTags"
:radar-dimensions="expenseProfileRadarDimensions"
:operations="expenseProfileOperations"
@close="closeExpenseProfileModal"
@explain="explainExpenseProfile"
/>
</section>
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import PanelHead from '../shared/PanelHead.vue'
import ExpenseProfileDetailModal from './ExpenseProfileDetailModal.vue'
import WorkbenchListIcon from '../shared/WorkbenchListIcon.vue'
import homepageBackground from '../../assets/homepage_backgraound.png'
import { useSystemState } from '../../composables/useSystemState.js'
@@ -268,6 +281,9 @@ import { useToast } from '../../composables/useToast.js'
import {
assistantCapabilities,
buildExpenseStatItems,
expenseProfileOperations,
expenseProfileRadarDimensions,
expenseProfileTags,
progressItems,
progressSteps,
quickPromptItems,
@@ -296,15 +312,45 @@ const selectedFiles = ref([])
const pendingAction = ref('')
const latestExpenseConversation = ref(null)
const hasLocalExpenseSnapshot = ref(false)
const expenseProfileModalOpen = ref(false)
const MAX_ATTACHMENTS = 10
const SESSION_TYPE_EXPENSE = 'expense'
const SESSION_TYPE_KNOWLEDGE = 'knowledge'
const FINANCIAL_CAPABILITY_KEYS = new Set(['budget-planning', 'finance-analysis'])
const FINANCIAL_CAPABILITY_ROLE_CODES = new Set(['budget_monitor', 'executive', 'admin'])
const FINANCIAL_CAPABILITY_ROLE_LABELS = new Set(['预算监控员', '高级财务人员', '管理员'])
const hasExpenseConversation = computed(() =>
Boolean(latestExpenseConversation.value?.conversation_id || latestExpenseConversation.value?.conversationId)
|| hasLocalExpenseSnapshot.value
)
const displayUserName = computed(() => {
const user = currentUser.value || {}
return String(user.name || user.username || '同事').trim() || '同事'
})
const expenseActionLabel = computed(() => (hasExpenseConversation.value ? '继续报销' : '新建报销'))
const currentRoleCodes = computed(() => {
const user = currentUser.value || {}
const rawCodes = Array.isArray(user.roleCodes)
? user.roleCodes
: Array.isArray(user.role_codes)
? user.role_codes
: []
return new Set(rawCodes.map((code) => String(code || '').trim().toLowerCase()).filter(Boolean))
})
const canViewFinancialCapabilities = computed(() => {
const user = currentUser.value || {}
const roleLabel = String(user.role || '').trim()
return Boolean(user.isAdmin)
|| FINANCIAL_CAPABILITY_ROLE_LABELS.has(roleLabel)
|| Array.from(currentRoleCodes.value).some((code) => FINANCIAL_CAPABILITY_ROLE_CODES.has(code))
})
const visibleAssistantCapabilities = computed(() =>
assistantCapabilities.filter((item) => canViewFinancialCapabilities.value || !FINANCIAL_CAPABILITY_KEYS.has(item.key))
)
const capabilityGridClass = computed(() =>
canViewFinancialCapabilities.value ? 'capability-grid--privileged' : 'capability-grid--standard'
)
const expenseStatItems = computed(() => buildExpenseStatItems(props.workbenchSummary))
const visibleExpenseStatItems = computed(() => {
const preferredKeys = ['monthly-amount', 'monthly-count', 'in-review', 'pending-payment']
@@ -318,6 +364,12 @@ const visibleUsageProfileMetrics = computed(() => {
.map((key) => usageProfileMetrics.find((item) => item.key === key))
.filter(Boolean)
})
const expenseProfileModalMetrics = computed(() => {
const preferredKeys = ['stay-duration', 'ai-usage', 'auto-pass-rate', 'audit-duration']
return preferredKeys
.map((key) => usageProfileMetrics.find((item) => item.key === key))
.filter(Boolean)
})
const visibleTodoItems = computed(() => todoItems.slice(0, 5))
const visibleProgressItems = computed(() => progressItems.slice(0, 5))
const todoAlertCount = computed(() => visibleTodoItems.value.length)
@@ -417,6 +469,19 @@ function openPromptAssistant(prompt) {
})
}
function openExpenseProfileModal() {
expenseProfileModalOpen.value = true
}
function closeExpenseProfileModal() {
expenseProfileModalOpen.value = false
}
function explainExpenseProfile() {
closeExpenseProfileModal()
openPromptAssistant('请根据我的费用画像标签、行为雷达和最近 5 次操作,解释我的费用使用特点和可以优化的地方。')
}
function handleWorkbenchEnter(event) {
if (event.isComposing) {
return