Add vue-router, login/setup flow and backend logging

Refactor frontend to route-based navigation with vue-router, add
system setup and login pages with API integration. Add structured
logging, access-log middleware and startup lifecycle to FastAPI
backend.
This commit is contained in:
2026-05-06 22:23:42 +08:00
parent 83d7da3d62
commit ae63766c91
35 changed files with 3762 additions and 404 deletions

View File

@@ -0,0 +1,278 @@
import { computed, ref } from 'vue'
import {
loginBootstrapAdmin,
saveBootstrapConfig,
testBootstrapDatabase,
testBootstrapRuntime
} from '../services/bootstrap.js'
import { useToast } from './useToast.js'
const AUTH_STORAGE_KEY = 'x-financial-authenticated'
function readClientBootstrapState() {
const env = import.meta.env
return {
initialized: String(env.VITE_SETUP_COMPLETED || '').toLowerCase() === 'true',
company: {
name: env.VITE_COMPANY_NAME || '',
code: env.VITE_COMPANY_CODE || '',
admin_email: env.VITE_ADMIN_EMAIL || ''
},
web: {
host: env.VITE_WEB_HOST || '127.0.0.1',
port: Number(env.VITE_WEB_PORT || 5173)
},
server: {
host: env.VITE_SERVER_HOST || '127.0.0.1',
port: Number(env.VITE_SERVER_PORT || 8000)
},
database: {
driver: 'postgresql',
host: env.VITE_POSTGRES_HOST || '127.0.0.1',
port: Number(env.VITE_POSTGRES_PORT || 5432),
name: env.VITE_POSTGRES_DB || 'x_financial',
username: env.VITE_POSTGRES_USER || 'postgres',
password_configured: false
},
redis: {
enabled: Boolean(env.VITE_REDIS_URL),
url: env.VITE_REDIS_URL || ''
}
}
}
function readAuthState() {
if (typeof window === 'undefined') {
return false
}
return window.sessionStorage.getItem(AUTH_STORAGE_KEY) === 'true'
}
function persistAuthState(value) {
if (typeof window === 'undefined') {
return
}
if (value) {
window.sessionStorage.setItem(AUTH_STORAGE_KEY, 'true')
return
}
window.sessionStorage.removeItem(AUTH_STORAGE_KEY)
}
const bootstrapState = ref(readClientBootstrapState())
const setupSubmitting = ref(false)
const setupError = ref('')
const runtimeTesting = ref(false)
const databaseTesting = ref(false)
const runtimeTestPassed = ref(false)
const databaseTestPassed = ref(false)
const runtimeTestMessage = ref('')
const databaseTestMessage = ref('')
const loginSubmitting = ref(false)
const loginError = ref('')
const loggedIn = ref(readAuthState())
const { toast } = useToast()
const companyProfile = computed(() => ({
name: bootstrapState.value.company?.name || '',
code: bootstrapState.value.company?.code || '',
adminEmail: bootstrapState.value.company?.admin_email || ''
}))
const isInitialized = computed(() => Boolean(bootstrapState.value.initialized))
function applyBootstrapState(state) {
bootstrapState.value = state
if (!state.initialized) {
loggedIn.value = false
persistAuthState(false)
}
}
function clearSetupRuntimeState() {
runtimeTesting.value = false
databaseTesting.value = false
runtimeTestPassed.value = false
databaseTestPassed.value = false
runtimeTestMessage.value = ''
databaseTestMessage.value = ''
setupError.value = ''
}
function resetFromClientEnv() {
applyBootstrapState(readClientBootstrapState())
clearSetupRuntimeState()
loginError.value = ''
}
async function handleSetupSubmit(payload) {
if (!runtimeTestPassed.value) {
setupError.value = '请先完成运行端口检测。'
toast(setupError.value)
return false
}
if (!databaseTestPassed.value) {
setupError.value = '请先完成数据库连接检测。'
toast(setupError.value)
return false
}
setupSubmitting.value = true
setupError.value = ''
try {
const state = await saveBootstrapConfig(payload)
applyBootstrapState(state)
toast('初始化配置已写入。现在可以进入登录页。')
return true
} catch (error) {
setupError.value = error.message || '初始化配置写入失败,请稍后重试。'
toast(setupError.value)
return false
} finally {
setupSubmitting.value = false
}
}
async function handleRuntimeTest(payload) {
runtimeTesting.value = true
runtimeTestMessage.value = ''
setupError.value = ''
try {
const result = await testBootstrapRuntime(payload)
runtimeTestPassed.value = true
runtimeTestMessage.value = result.detail || '端口占用检测通过。'
toast(runtimeTestMessage.value)
} catch (error) {
runtimeTestPassed.value = false
runtimeTestMessage.value = error.message || '端口占用检测失败。'
toast(runtimeTestMessage.value)
} finally {
runtimeTesting.value = false
}
}
async function handleDatabaseTest(payload) {
databaseTesting.value = true
databaseTestMessage.value = ''
setupError.value = ''
try {
const result = await testBootstrapDatabase(payload)
databaseTestPassed.value = true
databaseTestMessage.value = result.detail || '数据库连接检测通过。'
toast(databaseTestMessage.value)
} catch (error) {
databaseTestPassed.value = false
databaseTestMessage.value = error.message || '数据库连接检测失败。'
toast(databaseTestMessage.value)
} finally {
databaseTesting.value = false
}
}
function handleRuntimeDirty() {
runtimeTestPassed.value = false
runtimeTestMessage.value = ''
if (setupError.value === '请先完成运行端口检测。') {
setupError.value = ''
}
}
function handleDatabaseDirty() {
databaseTestPassed.value = false
databaseTestMessage.value = ''
if (setupError.value === '请先完成数据库连接检测。') {
setupError.value = ''
}
}
async function handleLogin(credentials) {
loginSubmitting.value = true
loginError.value = ''
try {
await loginBootstrapAdmin({
username: credentials.username,
password: credentials.password
})
loggedIn.value = true
persistAuthState(true)
return true
} catch (error) {
loggedIn.value = false
persistAuthState(false)
loginError.value = error.message || '登录失败,请检查管理员账号和密码。'
toast(loginError.value)
return false
} finally {
loginSubmitting.value = false
}
}
function logout() {
loggedIn.value = false
persistAuthState(false)
}
function handleRecoverPassword() {
toast('请联系系统管理员重置密码。管理员密码不会写入 .env。')
}
function handleSsoLogin() {
toast('SSO 登录暂未启用。')
}
function resolveEntryRoute() {
if (!isInitialized.value) {
return { name: 'setup' }
}
if (!loggedIn.value) {
return { name: 'login' }
}
return { name: 'app-overview' }
}
export function useSystemState() {
return {
bootstrapState,
companyProfile,
databaseTestMessage,
databaseTestPassed,
databaseTesting,
handleDatabaseDirty,
handleDatabaseTest,
handleLogin,
handleRecoverPassword,
handleRuntimeDirty,
handleRuntimeTest,
handleSetupSubmit,
handleSsoLogin,
isInitialized,
loggedIn,
loginError,
loginSubmitting,
logout,
resetFromClientEnv,
resolveEntryRoute,
runtimeTestMessage,
runtimeTestPassed,
runtimeTesting,
setupError,
setupSubmitting
}
}