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

316
web/src/views/SetupView.vue Normal file
View File

@@ -0,0 +1,316 @@
<template>
<main class="setup-page">
<aside class="setup-context">
<div class="setup-brand">
<div class="setup-brand-mark" aria-hidden="true">
<span class="setup-brand-ring"></span>
<span class="setup-brand-core">XF</span>
</div>
<div>
<p class="setup-kicker">INITIAL SETUP</p>
<h1>初始化配置</h1>
</div>
</div>
<p class="setup-lead">
先完成 4 个必要步骤再进入主登录界面扩展服务当前不参与初始化完成条件
</p>
<nav class="setup-nav" aria-label="初始化步骤">
<button
v-for="section in sections"
:key="section.id"
class="setup-nav-item"
:class="{ 'is-active': activeSection === section.id, 'is-complete': section.complete }"
type="button"
@click="goToSection(section.id)"
>
<span class="setup-nav-index">{{ section.index }}</span>
<span class="setup-nav-copy">
<strong>{{ section.title }}</strong>
<small>{{ section.desc }}</small>
</span>
<i v-if="section.complete" class="pi pi-check setup-nav-check"></i>
</button>
</nav>
<div class="setup-progress">
<strong>{{ completionCount }} / {{ sections.length }} 已完成</strong>
<p>企业信息管理员安全运行端口数据库连接都通过后左下角会自动出现完成初始化按钮</p>
</div>
<div v-if="canSubmit" class="setup-complete">
<p>所有必要步骤已通过检测可以写入配置并进入登录界面</p>
<button class="primary-btn setup-complete-btn" type="button" :disabled="submitting" @click="submitForm">
<i class="pi pi-check"></i>
<span>{{ submitting ? '写入配置中...' : '完成初始化并进入登录' }}</span>
</button>
</div>
</aside>
<section class="setup-panel">
<header class="setup-panel-head">
<div>
<p class="setup-kicker setup-kicker-light">{{ activeStep.index }}</p>
<h2>{{ activeStep.title }}</h2>
<p class="setup-panel-desc">{{ activeStep.desc }}</p>
</div>
<span class="setup-chip" :class="{ 'is-success': activeStep.complete }">
{{ activeStep.complete ? '已完成' : '待配置' }}
</span>
</header>
<div class="setup-form">
<section v-if="activeSection === 'company'" class="setup-stage">
<div class="section-head">
<h3>企业基础信息</h3>
<p>这里仅保留企业名称与企业编码不放管理员邮箱</p>
</div>
<div class="field-grid field-grid-2">
<label class="field">
<span>企业名称</span>
<input v-model.trim="form.company_name" type="text" placeholder="请输入企业名称" required />
</label>
<label class="field">
<span>企业编码</span>
<input v-model.trim="form.company_code" type="text" placeholder="例如 FIN" />
</label>
</div>
</section>
<section v-else-if="activeSection === 'admin'" class="setup-stage">
<div class="section-head">
<h3>管理员安全</h3>
<p>管理员邮箱账号和密码在这里配置密码不会写入 `.env`只会保存哈希后的密文</p>
</div>
<div class="field-grid field-grid-2">
<label class="field">
<span>管理员邮箱</span>
<input v-model.trim="form.admin_email" type="email" placeholder="admin@company.com" />
</label>
<label class="field">
<span>管理员账号</span>
<input v-model.trim="form.admin_username" type="text" placeholder="例如 superadmin" required />
</label>
<label class="field">
<span>管理员密码</span>
<input
v-model="form.admin_password"
type="password"
placeholder="请输入管理员密码"
autocomplete="new-password"
required
/>
</label>
<label class="field">
<span>确认密码</span>
<input
v-model="form.admin_password_confirm"
type="password"
placeholder="请再次输入管理员密码"
autocomplete="new-password"
required
/>
</label>
</div>
<p class="field-group-note">管理员密码当前暂定至少 5 </p>
</section>
<section v-else-if="activeSection === 'runtime'" class="setup-stage">
<div class="section-head">
<h3>运行端口配置</h3>
<p>这一步只检测 Web Server 端口占用情况不检测数据库</p>
</div>
<div class="field-grid field-grid-2">
<label class="field">
<span>Web Host</span>
<input v-model.trim="form.web_host" type="text" placeholder="127.0.0.1" required />
</label>
<label class="field">
<span>Web Port</span>
<input v-model.number="form.web_port" type="number" min="1" max="65535" required />
</label>
<label class="field">
<span>Server Host</span>
<input v-model.trim="form.server_host" type="text" placeholder="127.0.0.1" required />
</label>
<label class="field">
<span>Server Port</span>
<input v-model.number="form.server_port" type="number" min="1" max="65535" required />
</label>
</div>
<div class="setup-runtime">
<article v-for="item in runtimeEndpoints" :key="item.label">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</article>
</div>
</section>
<section v-else class="setup-stage">
<div class="section-head">
<h3>数据库连接</h3>
<p>这里检测 PostgreSQL 连接Redis 作为扩展服务暂时可选不影响完成初始化</p>
</div>
<div class="field-grid field-grid-2">
<label class="field">
<span>PostgreSQL Host</span>
<input v-model.trim="form.postgres_host" type="text" placeholder="127.0.0.1" required />
</label>
<label class="field">
<span>PostgreSQL Port</span>
<input v-model.number="form.postgres_port" type="number" min="1" max="65535" required />
</label>
<label class="field">
<span>数据库名称</span>
<input v-model.trim="form.postgres_db" type="text" placeholder="x_financial" required />
</label>
<label class="field">
<span>数据库用户</span>
<input v-model.trim="form.postgres_user" type="text" placeholder="postgres" required />
</label>
<label class="field field-span-2">
<span>数据库密码</span>
<input
v-model="form.postgres_password"
type="password"
placeholder="请输入数据库密码"
autocomplete="new-password"
required
/>
</label>
</div>
<div class="optional-block">
<div class="optional-block-head">
<strong>扩展服务</strong>
<span>可选</span>
</div>
<label class="field">
<span>Redis URL</span>
<input v-model.trim="form.redis_url" type="text" placeholder="redis://127.0.0.1:6379/0" />
</label>
</div>
<div class="setup-summary-grid">
<article v-for="item in summaryItems" :key="item.label" class="setup-summary-item">
<div>
<strong>{{ item.label }}</strong>
<span>{{ item.detail }}</span>
</div>
<i :class="['pi', item.complete ? 'pi-check-circle' : 'pi-clock']"></i>
</article>
</div>
</section>
<p v-if="currentTestMessage" :class="['setup-status', currentTestPassed ? 'is-success' : 'is-danger']">
{{ currentTestMessage }}
</p>
<p v-if="errorMessage" class="setup-error">{{ errorMessage }}</p>
<p v-if="submitHint" class="setup-gate">{{ submitHint }}</p>
<footer class="setup-actions">
<div class="setup-actions-right">
<button
v-if="showTestAction"
class="secondary-btn secondary-btn-strong"
type="button"
:disabled="!canTest"
@click="testSetup"
>
<i :class="testButtonIcon"></i>
<span>{{ testButtonLabel }}</span>
</button>
</div>
</footer>
</div>
</section>
</main>
</template>
<script setup>
import { useSetupView } from '../composables/useSetupView.js'
const props = defineProps({
initialState: {
type: Object,
default: () => ({})
},
submitting: {
type: Boolean,
default: false
},
runtimeTesting: {
type: Boolean,
default: false
},
databaseTesting: {
type: Boolean,
default: false
},
runtimeTestPassed: {
type: Boolean,
default: false
},
databaseTestPassed: {
type: Boolean,
default: false
},
runtimeTestMessage: {
type: String,
default: ''
},
databaseTestMessage: {
type: String,
default: ''
},
errorMessage: {
type: String,
default: ''
}
})
const emit = defineEmits(['submit', 'runtime-test', 'database-test', 'runtime-dirty', 'database-dirty'])
const {
activeSection,
activeStep,
canSubmit,
canTest,
completionCount,
currentTestMessage,
currentTestPassed,
form,
goToSection,
runtimeEndpoints,
sections,
showTestAction,
submitForm,
submitHint,
summaryItems,
testButtonIcon,
testButtonLabel,
testSetup
} = useSetupView(props, emit)
</script>
<style scoped src="../assets/styles/views/setup-view.css"></style>