2026-05-09 03:04:09 +00:00
|
|
|
|
<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>
|
2026-05-29 13:17:39 +08:00
|
|
|
|
<i v-if="section.complete" class="mdi mdi-check setup-nav-check"></i>
|
2026-05-09 03:04:09 +00:00
|
|
|
|
</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">
|
2026-05-29 13:17:39 +08:00
|
|
|
|
<i :class="['mdi', submitting ? 'mdi-loading mdi-spin' : 'mdi-check']"></i>
|
2026-05-09 03:04:09 +00:00
|
|
|
|
<span>{{ submitting ? '写入配置中...' : '完成初始化并进入登录' }}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<p v-if="progressMessage" class="setup-complete-progress">
|
2026-05-29 13:17:39 +08:00
|
|
|
|
<i class="mdi mdi-loading mdi-spin"></i>
|
2026-05-09 03:04:09 +00:00
|
|
|
|
<span>{{ progressMessage }}</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</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 地址由当前已启动的前端实例自动确定,这一步只需要配置并检测后端端口。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field-grid field-grid-2">
|
|
|
|
|
|
<label class="field">
|
|
|
|
|
|
<span>Server Host</span>
|
|
|
|
|
|
<input v-model.trim="form.server_host" type="text" placeholder="0.0.0.0" 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>
|
|
|
|
|
|
|
|
|
|
|
|
</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>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="startupVisible" class="setup-modal-backdrop" role="alertdialog" aria-modal="true">
|
|
|
|
|
|
<section class="setup-startup-modal" aria-label="后端启动进度">
|
|
|
|
|
|
<header class="setup-startup-head">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="setup-kicker setup-kicker-light">BACKEND STARTUP</p>
|
|
|
|
|
|
<h2>正在完成系统启动</h2>
|
|
|
|
|
|
<span>{{ progressMessage || '正在准备后端服务...' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="setup-startup-spinner" aria-hidden="true">
|
2026-05-29 13:17:39 +08:00
|
|
|
|
<i v-if="!startupCountdownSeconds" class="mdi mdi-loading mdi-spin"></i>
|
2026-05-09 03:04:09 +00:00
|
|
|
|
<strong v-else>{{ startupCountdownSeconds }}</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="setup-startup-body">
|
|
|
|
|
|
<ol class="setup-startup-steps">
|
|
|
|
|
|
<li
|
|
|
|
|
|
v-for="step in startupSteps"
|
|
|
|
|
|
:key="step.id"
|
|
|
|
|
|
:class="['setup-startup-step', `is-${step.status || 'pending'}`]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i :class="startupStepIcon(step.status)"></i>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>{{ step.label }}</strong>
|
|
|
|
|
|
<span>{{ step.detail }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ol>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="setup-startup-console" aria-label="后端启动日志">
|
|
|
|
|
|
<div class="setup-startup-console-head">
|
|
|
|
|
|
<strong>执行日志</strong>
|
|
|
|
|
|
<span>server/logs/bootstrap-backend.log</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<pre class="setup-startup-log">{{ startupLog || '等待后端启动输出...' }}</pre>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</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: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
progressMessage: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
startupCountdownSeconds: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
default: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
startupLog: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
startupSteps: {
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
default: () => []
|
|
|
|
|
|
},
|
|
|
|
|
|
startupVisible: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
testButtonIcon,
|
|
|
|
|
|
testButtonLabel,
|
|
|
|
|
|
testSetup
|
|
|
|
|
|
} = useSetupView(props, emit)
|
|
|
|
|
|
|
|
|
|
|
|
function startupStepIcon(status) {
|
|
|
|
|
|
if (status === 'success') {
|
2026-05-29 13:17:39 +08:00
|
|
|
|
return 'mdi mdi-check-circle'
|
2026-05-09 03:04:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (status === 'error') {
|
2026-05-29 13:17:39 +08:00
|
|
|
|
return 'mdi mdi-close-circle'
|
2026-05-09 03:04:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (status === 'running') {
|
2026-05-29 13:17:39 +08:00
|
|
|
|
return 'mdi mdi-loading mdi-spin'
|
2026-05-09 03:04:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 13:17:39 +08:00
|
|
|
|
return 'mdi mdi-circle-outline'
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped src="../assets/styles/views/setup-view.css"></style>
|