feat: 引入 ECharts 统一图表并完善员工画像标签分页
后端优化员工行为画像服务和辅助函数,完善系统设置模型和 配置持久化,前端引入 ECharts 替换所有图表组件实现统一 渲染,新增员工画像标签分页器和数字员工工作记录组件,优 化工作台响应式布局和登录页过渡动画,完善预算中心和数字 员工页面样式细节。
This commit is contained in:
79
web/src/composables/useEcharts.js
Normal file
79
web/src/composables/useEcharts.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { nextTick, onBeforeUnmount, onMounted, watch } from 'vue'
|
||||
import { init } from 'echarts/core'
|
||||
|
||||
export function useEcharts(chartElement, chartOptions) {
|
||||
let chartInstance = null
|
||||
let resizeObserver = null
|
||||
let renderFrame = 0
|
||||
|
||||
function renderChart() {
|
||||
if (!chartElement.value) {
|
||||
return
|
||||
}
|
||||
if (!chartInstance) {
|
||||
chartInstance = init(chartElement.value, null, { renderer: 'canvas' })
|
||||
chartInstance.resize()
|
||||
}
|
||||
chartInstance.setOption(chartOptions.value, true)
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
chartInstance?.resize()
|
||||
}
|
||||
|
||||
function scheduleRender() {
|
||||
if (typeof window === 'undefined') {
|
||||
renderChart()
|
||||
return
|
||||
}
|
||||
if (renderFrame) {
|
||||
window.cancelAnimationFrame(renderFrame)
|
||||
}
|
||||
renderFrame = window.requestAnimationFrame(() => {
|
||||
renderFrame = 0
|
||||
renderChart()
|
||||
})
|
||||
}
|
||||
|
||||
function bindResize() {
|
||||
if (!chartElement.value) {
|
||||
return
|
||||
}
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
resizeObserver = new ResizeObserver(handleResize)
|
||||
resizeObserver.observe(chartElement.value)
|
||||
}
|
||||
window.addEventListener('resize', handleResize)
|
||||
}
|
||||
|
||||
function unbindResize() {
|
||||
resizeObserver?.disconnect()
|
||||
resizeObserver = null
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderChart()
|
||||
bindResize()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unbindResize()
|
||||
if (renderFrame && typeof window !== 'undefined') {
|
||||
window.cancelAnimationFrame(renderFrame)
|
||||
renderFrame = 0
|
||||
}
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
})
|
||||
|
||||
watch(chartOptions, () => {
|
||||
nextTick(scheduleRender)
|
||||
}, { deep: true })
|
||||
|
||||
return {
|
||||
renderChart
|
||||
}
|
||||
}
|
||||
@@ -83,11 +83,11 @@ export const navItems = [
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
label: '日志管理',
|
||||
navHint: '查看 Hermes 调用与系统运行日志',
|
||||
label: '系统日志',
|
||||
navHint: '查看系统运行日志',
|
||||
icon: icons.logs,
|
||||
title: '日志管理',
|
||||
desc: '集中查看 Hermes 归纳任务进度、调用明细与系统运行日志。'
|
||||
title: '系统日志',
|
||||
desc: '集中查看系统运行日志、结构化事件和请求追踪信息。'
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
|
||||
@@ -107,6 +107,10 @@ export function useSettings() {
|
||||
pageState.value = maskConfiguredRenderSecret(maskConfiguredModelSecrets(nextState))
|
||||
persistSettings(pageState.value)
|
||||
updateBrandPreviewFromState(pageState.value)
|
||||
|
||||
if (nextState.appearanceForm?.themeSkin) {
|
||||
setThemeSkin(nextState.appearanceForm.themeSkin)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettingsSnapshot() {
|
||||
@@ -123,6 +127,7 @@ export function useSettings() {
|
||||
function buildSettingsPayload() {
|
||||
return {
|
||||
companyForm: { ...pageState.value.companyForm },
|
||||
appearanceForm: { ...pageState.value.appearanceForm },
|
||||
adminForm: { ...pageState.value.adminForm },
|
||||
sessionForm: { ...pageState.value.sessionForm },
|
||||
llmForm: buildLlmPayload(pageState.value.llmForm),
|
||||
@@ -307,10 +312,16 @@ export function useSettings() {
|
||||
|
||||
function selectThemeSkin(skinId) {
|
||||
setThemeSkin(skinId)
|
||||
pageState.value.appearanceForm.themeSkin = skinId
|
||||
}
|
||||
|
||||
function saveAppearanceSection() {
|
||||
toast('界面皮肤已应用到当前浏览器。')
|
||||
async function saveAppearanceSection() {
|
||||
await persistRemoteSettings('界面皮肤已保存并应用到企业配置。', {
|
||||
preserveModelApiKeys: true,
|
||||
preserveAdminPasswords: true,
|
||||
preserveRenderSecret: true,
|
||||
preserveMailPassword: true
|
||||
})
|
||||
}
|
||||
|
||||
async function saveLlmSection() {
|
||||
|
||||
@@ -13,6 +13,8 @@ import { setRuntimeApiBaseUrl } from '../services/api.js'
|
||||
import { checkBackendHealth } from './useBackendHealth.js'
|
||||
import { resolveDefaultAuthorizedRoute } from '../utils/accessControl.js'
|
||||
import { useToast } from './useToast.js'
|
||||
import { fetchSettings } from '../services/settings.js'
|
||||
import { setThemeSkin } from './useThemeSkin.js'
|
||||
|
||||
const AUTH_STORAGE_KEY = 'x-financial-authenticated'
|
||||
const AUTH_USERNAME_KEY = 'x-financial-auth-username'
|
||||
@@ -86,68 +88,68 @@ function readStoredUsername() {
|
||||
}
|
||||
|
||||
function buildAnonymousUser() {
|
||||
return {
|
||||
username: '',
|
||||
name: '',
|
||||
role: '',
|
||||
department: '',
|
||||
departmentName: '',
|
||||
position: '',
|
||||
grade: '',
|
||||
employeeNo: '',
|
||||
managerName: '',
|
||||
location: '',
|
||||
costCenter: '',
|
||||
financeOwnerName: '',
|
||||
riskProfile: {},
|
||||
roleCodes: [],
|
||||
email: '',
|
||||
avatar: '',
|
||||
isAdmin: false
|
||||
return {
|
||||
username: '',
|
||||
name: '',
|
||||
role: '',
|
||||
department: '',
|
||||
departmentName: '',
|
||||
position: '',
|
||||
grade: '',
|
||||
employeeNo: '',
|
||||
managerName: '',
|
||||
location: '',
|
||||
costCenter: '',
|
||||
financeOwnerName: '',
|
||||
riskProfile: {},
|
||||
roleCodes: [],
|
||||
email: '',
|
||||
avatar: '',
|
||||
isAdmin: false
|
||||
}
|
||||
}
|
||||
|
||||
function buildLegacyAdminUser(username = '') {
|
||||
function buildLegacyAdminUser(username = '') {
|
||||
const normalized = String(username || '').trim()
|
||||
const name = normalized || DEFAULT_USER_NAME
|
||||
|
||||
return {
|
||||
username: normalized,
|
||||
name,
|
||||
role: DEFAULT_USER_ROLE,
|
||||
department: '',
|
||||
departmentName: '',
|
||||
position: DEFAULT_USER_ROLE,
|
||||
grade: '',
|
||||
employeeNo: '',
|
||||
managerName: '',
|
||||
location: '',
|
||||
costCenter: '',
|
||||
financeOwnerName: '',
|
||||
riskProfile: {},
|
||||
roleCodes: ['manager'],
|
||||
email: '',
|
||||
avatar: name.slice(0, 1).toUpperCase(),
|
||||
isAdmin: true
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePlatformAdminFlag(payload, roleCodes = []) {
|
||||
const username = String(payload?.username || payload?.account || '').trim().toLowerCase()
|
||||
const role = String(payload?.role || '').trim().toLowerCase()
|
||||
const normalizedRoleCodes = roleCodes.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean)
|
||||
|
||||
return (
|
||||
Boolean(payload?.isAdmin)
|
||||
|| username === 'admin'
|
||||
|| role === 'admin'
|
||||
|| role === '管理员'
|
||||
|| role === '系统管理员'
|
||||
|| normalizedRoleCodes.includes('admin')
|
||||
)
|
||||
}
|
||||
|
||||
function readStoredUser() {
|
||||
return {
|
||||
username: normalized,
|
||||
name,
|
||||
role: DEFAULT_USER_ROLE,
|
||||
department: '',
|
||||
departmentName: '',
|
||||
position: DEFAULT_USER_ROLE,
|
||||
grade: '',
|
||||
employeeNo: '',
|
||||
managerName: '',
|
||||
location: '',
|
||||
costCenter: '',
|
||||
financeOwnerName: '',
|
||||
riskProfile: {},
|
||||
roleCodes: ['manager'],
|
||||
email: '',
|
||||
avatar: name.slice(0, 1).toUpperCase(),
|
||||
isAdmin: true
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePlatformAdminFlag(payload, roleCodes = []) {
|
||||
const username = String(payload?.username || payload?.account || '').trim().toLowerCase()
|
||||
const role = String(payload?.role || '').trim().toLowerCase()
|
||||
const normalizedRoleCodes = roleCodes.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean)
|
||||
|
||||
return (
|
||||
Boolean(payload?.isAdmin)
|
||||
|| username === 'admin'
|
||||
|| role === 'admin'
|
||||
|| role === '管理员'
|
||||
|| role === '系统管理员'
|
||||
|| normalizedRoleCodes.includes('admin')
|
||||
)
|
||||
}
|
||||
|
||||
function readStoredUser() {
|
||||
if (typeof window === 'undefined') {
|
||||
return buildAnonymousUser()
|
||||
}
|
||||
@@ -162,26 +164,26 @@ function readStoredUser() {
|
||||
const name = String(payload.name || username || DEFAULT_USER_NAME).trim()
|
||||
const roleCodes = Array.isArray(payload.roleCodes) ? payload.roleCodes.filter(Boolean) : []
|
||||
|
||||
return {
|
||||
username,
|
||||
name,
|
||||
role: String(payload.role || DEFAULT_USER_ROLE),
|
||||
department: String(payload.department || payload.departmentName || ''),
|
||||
departmentName: String(payload.departmentName || payload.department || ''),
|
||||
position: String(payload.position || ''),
|
||||
grade: String(payload.grade || ''),
|
||||
employeeNo: String(payload.employeeNo || payload.employee_no || ''),
|
||||
managerName: String(payload.managerName || payload.manager_name || ''),
|
||||
location: String(payload.location || ''),
|
||||
costCenter: String(payload.costCenter || payload.cost_center || ''),
|
||||
financeOwnerName: String(payload.financeOwnerName || payload.finance_owner_name || ''),
|
||||
riskProfile: payload.riskProfile && typeof payload.riskProfile === 'object' ? payload.riskProfile : {},
|
||||
roleCodes,
|
||||
email: String(payload.email || ''),
|
||||
avatar: String(payload.avatar || name.slice(0, 1).toUpperCase()),
|
||||
isAdmin: resolvePlatformAdminFlag(payload, roleCodes)
|
||||
}
|
||||
}
|
||||
return {
|
||||
username,
|
||||
name,
|
||||
role: String(payload.role || DEFAULT_USER_ROLE),
|
||||
department: String(payload.department || payload.departmentName || ''),
|
||||
departmentName: String(payload.departmentName || payload.department || ''),
|
||||
position: String(payload.position || ''),
|
||||
grade: String(payload.grade || ''),
|
||||
employeeNo: String(payload.employeeNo || payload.employee_no || ''),
|
||||
managerName: String(payload.managerName || payload.manager_name || ''),
|
||||
location: String(payload.location || ''),
|
||||
costCenter: String(payload.costCenter || payload.cost_center || ''),
|
||||
financeOwnerName: String(payload.financeOwnerName || payload.finance_owner_name || ''),
|
||||
riskProfile: payload.riskProfile && typeof payload.riskProfile === 'object' ? payload.riskProfile : {},
|
||||
roleCodes,
|
||||
email: String(payload.email || ''),
|
||||
avatar: String(payload.avatar || name.slice(0, 1).toUpperCase()),
|
||||
isAdmin: resolvePlatformAdminFlag(payload, roleCodes)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return buildLegacyAdminUser(readStoredUsername())
|
||||
}
|
||||
@@ -359,6 +361,15 @@ export function installSessionNavigation(router) {
|
||||
.then((state) => {
|
||||
applyBootstrapState(state)
|
||||
setRuntimeApiBaseUrl(resolveBrowserApiBaseUrl(state))
|
||||
fetchSettings()
|
||||
.then((snapshot) => {
|
||||
if (snapshot?.appearanceForm?.themeSkin) {
|
||||
setThemeSkin(snapshot.appearanceForm.themeSkin)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('Failed to load remote theme settings:', error)
|
||||
})
|
||||
router.isReady().then(() => reconcileEntryRoute(router))
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -624,14 +635,14 @@ async function handleLogin(credentials) {
|
||||
password: credentials.password
|
||||
})
|
||||
|
||||
const responseUser = response?.user || buildAnonymousUser()
|
||||
const responseRoleCodes = Array.isArray(responseUser.roleCodes) ? responseUser.roleCodes.filter(Boolean) : []
|
||||
const user = {
|
||||
...responseUser,
|
||||
roleCodes: responseRoleCodes,
|
||||
isAdmin: resolvePlatformAdminFlag(responseUser, responseRoleCodes)
|
||||
}
|
||||
loggedIn.value = true
|
||||
const responseUser = response?.user || buildAnonymousUser()
|
||||
const responseRoleCodes = Array.isArray(responseUser.roleCodes) ? responseUser.roleCodes.filter(Boolean) : []
|
||||
const user = {
|
||||
...responseUser,
|
||||
roleCodes: responseRoleCodes,
|
||||
isAdmin: resolvePlatformAdminFlag(responseUser, responseRoleCodes)
|
||||
}
|
||||
loggedIn.value = true
|
||||
persistAuthState(true, user)
|
||||
currentUser.value = user
|
||||
touchAuthActivity(true)
|
||||
|
||||
Reference in New Issue
Block a user