feat: add employee management, backend health check, and UI improvements
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user