feat: add employee management, backend health check, and UI improvements

This commit is contained in:
2026-05-07 11:50:10 +08:00
parent a5db09f41e
commit c00db75c13
59 changed files with 3926 additions and 5796 deletions

View File

@@ -9,6 +9,21 @@ import {
import { useToast } from './useToast.js'
const AUTH_STORAGE_KEY = 'x-financial-authenticated'
const AUTH_USERNAME_KEY = 'x-financial-auth-username'
const AUTH_LAST_ACTIVITY_KEY = 'x-financial-auth-last-activity'
const DEFAULT_USER_NAME = '系统管理员'
const DEFAULT_USER_ROLE = '财务管理员'
const SESSION_ACTIVITY_EVENTS = ['pointerdown', 'keydown', 'scroll', 'touchstart', 'visibilitychange']
const authIdleTimeoutMinutes = Number(import.meta.env.VITE_AUTH_IDLE_TIMEOUT_MINUTES || 30)
const authIdleTimeoutMs =
Number.isFinite(authIdleTimeoutMinutes) && authIdleTimeoutMinutes > 0
? authIdleTimeoutMinutes * 60 * 1000
: 30 * 60 * 1000
let sessionRouter = null
let sessionTimeoutHandle = 0
let sessionMonitoringInstalled = false
let lastActivityWriteAt = 0
function readClientBootstrapState() {
const env = import.meta.env
@@ -51,17 +66,176 @@ function readAuthState() {
return window.sessionStorage.getItem(AUTH_STORAGE_KEY) === 'true'
}
function persistAuthState(value) {
function readStoredUsername() {
if (typeof window === 'undefined') {
return ''
}
return window.sessionStorage.getItem(AUTH_USERNAME_KEY) || ''
}
function readLastActivityAt() {
if (typeof window === 'undefined') {
return 0
}
return Number(window.sessionStorage.getItem(AUTH_LAST_ACTIVITY_KEY) || 0)
}
function buildCurrentUser(username = '') {
const normalized = String(username || '').trim()
const name = normalized || DEFAULT_USER_NAME
return {
name,
role: DEFAULT_USER_ROLE,
avatar: name.slice(0, 1).toUpperCase()
}
}
function isSessionExpired(now = Date.now()) {
if (!readAuthState()) {
return false
}
const lastActivityAt = readLastActivityAt()
if (!lastActivityAt) {
return true
}
return now - lastActivityAt > authIdleTimeoutMs
}
function persistAuthState(value, username = '') {
if (typeof window === 'undefined') {
return
}
if (value) {
window.sessionStorage.setItem(AUTH_STORAGE_KEY, 'true')
window.sessionStorage.setItem(AUTH_USERNAME_KEY, String(username || '').trim())
return
}
window.sessionStorage.removeItem(AUTH_STORAGE_KEY)
window.sessionStorage.removeItem(AUTH_USERNAME_KEY)
window.sessionStorage.removeItem(AUTH_LAST_ACTIVITY_KEY)
}
function clearSessionTimeout() {
if (typeof window === 'undefined' || !sessionTimeoutHandle) {
return
}
window.clearTimeout(sessionTimeoutHandle)
sessionTimeoutHandle = 0
}
function redirectToLogin() {
if (sessionRouter?.currentRoute?.value?.name === 'login') {
return
}
if (sessionRouter) {
sessionRouter.replace({ name: 'login' })
return
}
if (typeof window !== 'undefined' && window.location.pathname !== '/login') {
window.location.assign('/login')
}
}
function scheduleSessionTimeout() {
clearSessionTimeout()
if (typeof window === 'undefined' || !readAuthState()) {
return
}
const lastActivityAt = readLastActivityAt()
if (!lastActivityAt) {
return
}
const remaining = authIdleTimeoutMs - (Date.now() - lastActivityAt)
if (remaining <= 0) {
logout('timeout', { notify: true })
return
}
sessionTimeoutHandle = window.setTimeout(() => {
logout('timeout', { notify: true })
}, remaining)
}
function touchAuthActivity(force = false) {
if (typeof window === 'undefined' || !readAuthState()) {
return
}
const now = Date.now()
if (!force && now - lastActivityWriteAt < 1000) {
scheduleSessionTimeout()
return
}
window.sessionStorage.setItem(AUTH_LAST_ACTIVITY_KEY, String(now))
lastActivityWriteAt = now
scheduleSessionTimeout()
}
function handleSessionActivity(event) {
if (typeof document !== 'undefined' && event?.type === 'visibilitychange' && document.visibilityState !== 'visible') {
return
}
touchAuthActivity()
}
function installSessionMonitoring() {
if (sessionMonitoringInstalled || typeof window === 'undefined') {
return
}
sessionMonitoringInstalled = true
SESSION_ACTIVITY_EVENTS.forEach((eventName) => {
window.addEventListener(eventName, handleSessionActivity, { passive: true })
})
}
function syncAuthSession(options = {}) {
const shouldNotify = Boolean(options.notify)
if (!readAuthState()) {
loggedIn.value = false
currentUser.value = buildCurrentUser('')
clearSessionTimeout()
return false
}
if (isSessionExpired()) {
logout('timeout', { notify: shouldNotify, redirect: false })
return false
}
loggedIn.value = true
currentUser.value = buildCurrentUser(readStoredUsername())
scheduleSessionTimeout()
return true
}
export function installSessionNavigation(router) {
sessionRouter = router
installSessionMonitoring()
if (readAuthState() && !isSessionExpired()) {
scheduleSessionTimeout()
}
}
const bootstrapState = ref(readClientBootstrapState())
@@ -75,7 +249,12 @@ const runtimeTestMessage = ref('')
const databaseTestMessage = ref('')
const loginSubmitting = ref(false)
const loginError = ref('')
const loggedIn = ref(readAuthState())
const loggedIn = ref(readAuthState() && !isSessionExpired())
const currentUser = ref(buildCurrentUser(readStoredUsername()))
if (!loggedIn.value && readAuthState()) {
persistAuthState(false)
}
const { toast } = useToast()
@@ -91,8 +270,7 @@ function applyBootstrapState(state) {
bootstrapState.value = state
if (!state.initialized) {
loggedIn.value = false
persistAuthState(false)
logout('reset', { redirect: false })
}
}
@@ -110,6 +288,7 @@ function resetFromClientEnv() {
applyBootstrapState(readClientBootstrapState())
clearSetupRuntimeState()
loginError.value = ''
currentUser.value = buildCurrentUser(readStoredUsername())
}
async function handleSetupSubmit(payload) {
@@ -209,11 +388,12 @@ async function handleLogin(credentials) {
})
loggedIn.value = true
persistAuthState(true)
persistAuthState(true, credentials.username)
currentUser.value = buildCurrentUser(credentials.username)
touchAuthActivity(true)
return true
} catch (error) {
loggedIn.value = false
persistAuthState(false)
logout('invalid', { redirect: false })
loginError.value = error.message || '登录失败,请检查管理员账号和密码。'
toast(loginError.value)
return false
@@ -222,9 +402,22 @@ async function handleLogin(credentials) {
}
}
function logout() {
function logout(reason = 'manual', options = {}) {
const notify = options.notify ?? reason === 'timeout'
const redirect = options.redirect ?? reason !== 'invalid'
loggedIn.value = false
persistAuthState(false)
currentUser.value = buildCurrentUser('')
clearSessionTimeout()
if (notify) {
toast(reason === 'timeout' ? '登录已超时,请重新登录。' : '已退出登录。')
}
if (redirect) {
redirectToLogin()
}
}
function handleRecoverPassword() {
@@ -236,6 +429,9 @@ function handleSsoLogin() {
}
function resolveEntryRoute() {
loggedIn.value = syncAuthSession()
currentUser.value = buildCurrentUser(readStoredUsername())
if (!isInitialized.value) {
return { name: 'setup' }
}
@@ -251,6 +447,7 @@ export function useSystemState() {
return {
bootstrapState,
companyProfile,
currentUser,
databaseTestMessage,
databaseTestPassed,
databaseTesting,
@@ -273,6 +470,7 @@ export function useSystemState() {
runtimeTestPassed,
runtimeTesting,
setupError,
setupSubmitting
setupSubmitting,
syncAuthSession
}
}