feat: 引入 ECharts 统一图表并完善员工画像标签分页

后端优化员工行为画像服务和辅助函数,完善系统设置模型和
配置持久化,前端引入 ECharts 替换所有图表组件实现统一
渲染,新增员工画像标签分页器和数字员工工作记录组件,优
化工作台响应式布局和登录页过渡动画,完善预算中心和数字
员工页面样式细节。
This commit is contained in:
caoxiaozhu
2026-05-28 16:24:59 +08:00
parent 8a4a777be7
commit e384318046
53 changed files with 4698 additions and 2468 deletions

View File

@@ -221,7 +221,7 @@
<article class="panel workbench-card side-panel usage-profile-panel">
<div class="section-head side-card-head">
<h2>用画像</h2>
<h2>画像</h2>
<button
type="button"
class="detail-action"
@@ -230,11 +230,11 @@
@click="openExpenseProfileModal"
>
<span>查看详情</span>
<i class="mdi mdi-chevron-right"></i>
<i :class="employeeProfileLoading ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-chevron-right'"></i>
</button>
</div>
<div class="insight-profile-list" aria-label="用画像">
<div class="insight-profile-list" aria-label="画像">
<div
v-for="metric in visibleUsageProfileMetrics"
:key="metric.key"
@@ -264,8 +264,10 @@
:tags="expenseProfileTags"
:radar-dimensions="expenseProfileRadarDimensions"
:operations="expenseProfileOperations"
:loading="employeeProfileLoading"
:error-message="employeeProfileError"
:empty-reason="expenseProfileEmptyReason"
@close="closeExpenseProfileModal"
@explain="explainExpenseProfile"
/>
</section>
</template>
@@ -281,20 +283,26 @@ import { useToast } from '../../composables/useToast.js'
import {
assistantCapabilities,
buildExpenseStatItems,
expenseProfileOperations,
expenseProfileRadarDimensions,
expenseProfileTags,
progressItems,
progressSteps,
quickPromptItems,
todoItems,
usageProfileMetrics
} from '../../data/personalWorkbench.js'
import { fetchAgentRuns } from '../../services/agentAssets.js'
import { clearUserConversations, fetchLatestConversation } from '../../services/orchestrator.js'
import { fetchCurrentEmployeeLatestProfile } from '../../services/reimbursements.js'
import {
ASSISTANT_SESSION_SNAPSHOT_EVENT,
hasAssistantSessionSnapshot
} from '../../utils/assistantSessionSnapshot.js'
import {
buildProfileOperationsFromAgentRuns,
buildUserProfileMetricCards,
buildUserProfileSummaryMetrics,
normalizeUserProfileRadarDimensions,
normalizeUserProfileTags,
resolveCurrentUserProfileError
} from '../../utils/employeeProfileViewModel.js'
const props = defineProps({
showHeader: { type: Boolean, default: true },
@@ -313,6 +321,11 @@ const pendingAction = ref('')
const latestExpenseConversation = ref(null)
const hasLocalExpenseSnapshot = ref(false)
const expenseProfileModalOpen = ref(false)
const employeeProfile = ref(null)
const employeeProfileRuns = ref([])
const employeeProfileLoading = ref(false)
const employeeProfileError = ref('')
let employeeProfileLoadSeq = 0
const MAX_ATTACHMENTS = 10
const SESSION_TYPE_EXPENSE = 'expense'
const SESSION_TYPE_KNOWLEDGE = 'knowledge'
@@ -359,16 +372,34 @@ const visibleExpenseStatItems = computed(() => {
.filter(Boolean)
})
const visibleUsageProfileMetrics = computed(() => {
const preferredKeys = ['ai-usage', 'submit-efficiency', 'auto-pass-rate', 'audit-duration']
return preferredKeys
.map((key) => usageProfileMetrics.find((item) => item.key === key))
.filter(Boolean)
return buildUserProfileMetricCards(
employeeProfile.value,
employeeProfileRuns.value,
currentUser.value
).slice(0, 4)
})
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)
return buildUserProfileSummaryMetrics(
employeeProfile.value,
employeeProfileRuns.value,
currentUser.value
)
})
const expenseProfileTags = computed(() => normalizeUserProfileTags(employeeProfile.value))
const expenseProfileRadarDimensions = computed(() => normalizeUserProfileRadarDimensions(employeeProfile.value))
const expenseProfileOperations = computed(() =>
buildProfileOperationsFromAgentRuns(employeeProfileRuns.value, currentUser.value)
)
const expenseProfileEmptyReason = computed(() => String(employeeProfile.value?.empty_reason || '').trim())
const currentUserProfileKey = computed(() => {
const user = currentUser.value || {}
return [
user.username,
user.email,
user.name,
user.employeeNo,
user.employee_no
].map((item) => String(item || '').trim()).filter(Boolean).join('|')
})
const visibleTodoItems = computed(() => todoItems.slice(0, 5))
const visibleProgressItems = computed(() => progressItems.slice(0, 5))
@@ -469,19 +500,46 @@ function openPromptAssistant(prompt) {
})
}
async function loadCurrentEmployeeProfile() {
const sequence = ++employeeProfileLoadSeq
employeeProfileLoading.value = true
employeeProfileError.value = ''
const [profileResult, runsResult] = await Promise.allSettled([
fetchCurrentEmployeeLatestProfile({
scene: 'operations',
window_days: 90,
expense_type_scope: 'overall'
}),
fetchAgentRuns({ limit: 100 })
])
if (sequence !== employeeProfileLoadSeq) {
return
}
if (profileResult.status === 'fulfilled') {
employeeProfile.value = profileResult.value || null
} else {
employeeProfile.value = null
employeeProfileError.value = resolveCurrentUserProfileError(profileResult.reason)
}
employeeProfileRuns.value = runsResult.status === 'fulfilled' ? runsResult.value || [] : []
employeeProfileLoading.value = false
}
function openExpenseProfileModal() {
expenseProfileModalOpen.value = true
if (!employeeProfile.value && !employeeProfileLoading.value) {
void loadCurrentEmployeeProfile()
}
}
function closeExpenseProfileModal() {
expenseProfileModalOpen.value = false
}
function explainExpenseProfile() {
closeExpenseProfileModal()
openPromptAssistant('请根据我的费用画像标签、行为雷达和最近 5 次操作,解释我的费用使用特点和可以优化的地方。')
}
function handleWorkbenchEnter(event) {
if (event.isComposing) {
return
@@ -570,6 +628,7 @@ async function handleExpenseConversationAction() {
onMounted(() => {
refreshLocalExpenseSnapshot()
refreshLatestExpenseConversation()
loadCurrentEmployeeProfile()
window.addEventListener(ASSISTANT_SESSION_SNAPSHOT_EVENT, handleAssistantSessionSnapshotChange)
})
@@ -585,6 +644,12 @@ watch(
}
}
)
watch(currentUserProfileKey, (nextKey, previousKey) => {
if (nextKey && nextKey !== previousKey) {
loadCurrentEmployeeProfile()
}
})
</script>
<style scoped src="../../assets/styles/components/personal-workbench.css"></style>