Files
X-Financial/web/src/composables/useSetupView.js

404 lines
11 KiB
JavaScript
Raw Normal View History

import { computed, reactive, ref, watch } from 'vue'
function readCurrentWebEndpoint(initialState) {
if (typeof window === 'undefined') {
return {
host: initialState?.web?.host || '0.0.0.0',
port: Number(initialState?.web?.port || 5173)
}
}
const fallbackPort = Number(initialState?.web?.port || 5173)
const port = Number(window.location.port || fallbackPort)
return {
host: window.location.hostname || initialState?.web?.host || '0.0.0.0',
port: Number.isInteger(port) && port > 0 ? port : fallbackPort
}
}
feat: 启用后端自动启动与 Setup 引导流程增强 主要修改点: 1. 网络绑定扩展至 Server - .env.example: SERVER_HOST/VITE_SERVER_HOST 从 127.0.0.1 改为 0.0.0.0 - server/src/app/core/config.py: 默认 app_host 从 127.0.0.1 改为 0.0.0.0 2. 启动脚本重构(重命名以区分职责) - server/start.sh → server/server_start.sh - web/start.sh → web/web_start.sh - 根目录 start.sh: 更新调用路径(./start.sh → ./server_start.sh, ./web_start.sh) - 根目录 start.sh: 新增 setup_ready() 检测函数 - 根目录 start.sh: server_probe_host() 支持 0.0.0.0 探测转换为 127.0.0.1 - 根目录 start.sh: start_setup_web() 新增 X_FINANCIAL_FORCE_SETUP=true 3. API URL 动态化 (web/src/services/api.js) - 从 localStorage 持久化读取 API Base URL - 新增 setRuntimeApiBaseUrl() / getRuntimeApiBaseUrl() 导出函数 - buildUrl() 改用运行时 runtimeApiBaseUrl 4. 浏览器 Host 智能解析 (web/vite.config.js) - 新增 resolveBrowserApiHost(): 根据 server_host/web_host 配置决定浏览器端使用的 API Host - buildApiBaseUrl() 改用 resolveBrowserApiHost() - 新增后端启动状态管理: backendStartState / cloneBackendStartState() / updateBackendStep() - 后端启动分 5 步追踪: config → deps → server → health → done - 新增 backendStartPromise 避免重复启动 5. Setup 表单逻辑增强 (web/src/composables/useSetupView.js) - 新增 shouldExposeServerHost(): 判断浏览器 host 是否非本地 - 新增 resolveInitialServerHost(): 当浏览器访问且 server_host 为本地时暴露 0.0.0.0 - buildPayload(): 根据 shouldExposeServerHost() 自动将 127.0.0.1/localhost 转为 0.0.0.0 6. Bootstrap API 扩展 (web/src/services/bootstrap.js) - 新增 startBootstrapBackend(): POST /bootstrap/backend 触发后端启动 - 新增 fetchBootstrapBackendStatus(): GET /bootstrap/backend 查询后端状态 7. Session 导航增强 (web/src/composables/useSystemState.js) - 新增 resolveBrowserApiBaseUrl(): 智能解析浏览器端 API Base URL - installSessionNavigation(): 调用 fetchBootstrapState() 同步引导状态 - 新增 reconcileEntryRoute(): 根据引导状态协调路由 - VITE_SERVER_HOST 默认值从 127.0.0.1 改为 0.0.0.0 8. Setup 视图增强 (web/src/views/SetupRouteView.vue) - 向 SetupView 传递启动进度相关 props: startupCountdownSeconds, startupLog, startupSteps, startupVisible, progressMessage 9. CSS 新增 (web/src/assets/styles/views/setup-view.css) - .setup-complete-progress: 进度文字样式 - .setup-modal-backdrop: 模态框遮罩 - .setup-startup-modal: 启动模态框容器 - .setup-startup-head / .setup-startup-body / .setup-startup-spinner: 模态框头部/内容/加载动画 - .setup-startup-steps / .setup-startup-step: 步骤列表及单个步骤 - .setup-startup-step.is-running / .is-success / .is-failed: 步骤状态样式 - .setup-startup-log: 启动日志区域
2026-05-08 10:52:54 +08:00
function shouldExposeServerHost() {
if (typeof window === 'undefined') {
return false
}
const host = String(window.location.hostname || '').toLowerCase()
return Boolean(host && host !== '127.0.0.1' && host !== 'localhost' && host !== '::1')
}
function resolveInitialServerHost(initialState) {
const host = String(initialState?.server?.host || '0.0.0.0').trim()
const normalized = host.toLowerCase()
if (shouldExposeServerHost() && (normalized === '127.0.0.1' || normalized === 'localhost' || normalized === '::1')) {
return '0.0.0.0'
}
return host || '0.0.0.0'
}
function createForm(initialState) {
const currentWeb = readCurrentWebEndpoint(initialState)
return {
company_name: initialState?.company?.name || '',
company_code: initialState?.company?.code || '',
admin_email: initialState?.company?.admin_email || '',
admin_username: '',
admin_password: '',
admin_password_confirm: '',
web_host: currentWeb.host,
web_port: currentWeb.port,
feat: 启用后端自动启动与 Setup 引导流程增强 主要修改点: 1. 网络绑定扩展至 Server - .env.example: SERVER_HOST/VITE_SERVER_HOST 从 127.0.0.1 改为 0.0.0.0 - server/src/app/core/config.py: 默认 app_host 从 127.0.0.1 改为 0.0.0.0 2. 启动脚本重构(重命名以区分职责) - server/start.sh → server/server_start.sh - web/start.sh → web/web_start.sh - 根目录 start.sh: 更新调用路径(./start.sh → ./server_start.sh, ./web_start.sh) - 根目录 start.sh: 新增 setup_ready() 检测函数 - 根目录 start.sh: server_probe_host() 支持 0.0.0.0 探测转换为 127.0.0.1 - 根目录 start.sh: start_setup_web() 新增 X_FINANCIAL_FORCE_SETUP=true 3. API URL 动态化 (web/src/services/api.js) - 从 localStorage 持久化读取 API Base URL - 新增 setRuntimeApiBaseUrl() / getRuntimeApiBaseUrl() 导出函数 - buildUrl() 改用运行时 runtimeApiBaseUrl 4. 浏览器 Host 智能解析 (web/vite.config.js) - 新增 resolveBrowserApiHost(): 根据 server_host/web_host 配置决定浏览器端使用的 API Host - buildApiBaseUrl() 改用 resolveBrowserApiHost() - 新增后端启动状态管理: backendStartState / cloneBackendStartState() / updateBackendStep() - 后端启动分 5 步追踪: config → deps → server → health → done - 新增 backendStartPromise 避免重复启动 5. Setup 表单逻辑增强 (web/src/composables/useSetupView.js) - 新增 shouldExposeServerHost(): 判断浏览器 host 是否非本地 - 新增 resolveInitialServerHost(): 当浏览器访问且 server_host 为本地时暴露 0.0.0.0 - buildPayload(): 根据 shouldExposeServerHost() 自动将 127.0.0.1/localhost 转为 0.0.0.0 6. Bootstrap API 扩展 (web/src/services/bootstrap.js) - 新增 startBootstrapBackend(): POST /bootstrap/backend 触发后端启动 - 新增 fetchBootstrapBackendStatus(): GET /bootstrap/backend 查询后端状态 7. Session 导航增强 (web/src/composables/useSystemState.js) - 新增 resolveBrowserApiBaseUrl(): 智能解析浏览器端 API Base URL - installSessionNavigation(): 调用 fetchBootstrapState() 同步引导状态 - 新增 reconcileEntryRoute(): 根据引导状态协调路由 - VITE_SERVER_HOST 默认值从 127.0.0.1 改为 0.0.0.0 8. Setup 视图增强 (web/src/views/SetupRouteView.vue) - 向 SetupView 传递启动进度相关 props: startupCountdownSeconds, startupLog, startupSteps, startupVisible, progressMessage 9. CSS 新增 (web/src/assets/styles/views/setup-view.css) - .setup-complete-progress: 进度文字样式 - .setup-modal-backdrop: 模态框遮罩 - .setup-startup-modal: 启动模态框容器 - .setup-startup-head / .setup-startup-body / .setup-startup-spinner: 模态框头部/内容/加载动画 - .setup-startup-steps / .setup-startup-step: 步骤列表及单个步骤 - .setup-startup-step.is-running / .is-success / .is-failed: 步骤状态样式 - .setup-startup-log: 启动日志区域
2026-05-08 10:52:54 +08:00
server_host: resolveInitialServerHost(initialState),
server_port: initialState?.server?.port || 8000,
postgres_host: initialState?.database?.host || '127.0.0.1',
postgres_port: initialState?.database?.port || 5432,
postgres_db: initialState?.database?.name || 'x_financial',
postgres_user: initialState?.database?.username || 'postgres',
postgres_password: '',
redis_url: initialState?.redis?.url || ''
}
}
function buildPayload(form) {
const currentWeb = readCurrentWebEndpoint({
web: {
host: form.web_host,
port: form.web_port
}
})
return {
company_name: form.company_name.trim(),
company_code: form.company_code.trim(),
admin_email: form.admin_email.trim(),
admin_username: form.admin_username.trim(),
admin_password: String(form.admin_password || ''),
admin_password_confirm: String(form.admin_password_confirm || ''),
web_host: currentWeb.host,
web_port: currentWeb.port,
feat: 启用后端自动启动与 Setup 引导流程增强 主要修改点: 1. 网络绑定扩展至 Server - .env.example: SERVER_HOST/VITE_SERVER_HOST 从 127.0.0.1 改为 0.0.0.0 - server/src/app/core/config.py: 默认 app_host 从 127.0.0.1 改为 0.0.0.0 2. 启动脚本重构(重命名以区分职责) - server/start.sh → server/server_start.sh - web/start.sh → web/web_start.sh - 根目录 start.sh: 更新调用路径(./start.sh → ./server_start.sh, ./web_start.sh) - 根目录 start.sh: 新增 setup_ready() 检测函数 - 根目录 start.sh: server_probe_host() 支持 0.0.0.0 探测转换为 127.0.0.1 - 根目录 start.sh: start_setup_web() 新增 X_FINANCIAL_FORCE_SETUP=true 3. API URL 动态化 (web/src/services/api.js) - 从 localStorage 持久化读取 API Base URL - 新增 setRuntimeApiBaseUrl() / getRuntimeApiBaseUrl() 导出函数 - buildUrl() 改用运行时 runtimeApiBaseUrl 4. 浏览器 Host 智能解析 (web/vite.config.js) - 新增 resolveBrowserApiHost(): 根据 server_host/web_host 配置决定浏览器端使用的 API Host - buildApiBaseUrl() 改用 resolveBrowserApiHost() - 新增后端启动状态管理: backendStartState / cloneBackendStartState() / updateBackendStep() - 后端启动分 5 步追踪: config → deps → server → health → done - 新增 backendStartPromise 避免重复启动 5. Setup 表单逻辑增强 (web/src/composables/useSetupView.js) - 新增 shouldExposeServerHost(): 判断浏览器 host 是否非本地 - 新增 resolveInitialServerHost(): 当浏览器访问且 server_host 为本地时暴露 0.0.0.0 - buildPayload(): 根据 shouldExposeServerHost() 自动将 127.0.0.1/localhost 转为 0.0.0.0 6. Bootstrap API 扩展 (web/src/services/bootstrap.js) - 新增 startBootstrapBackend(): POST /bootstrap/backend 触发后端启动 - 新增 fetchBootstrapBackendStatus(): GET /bootstrap/backend 查询后端状态 7. Session 导航增强 (web/src/composables/useSystemState.js) - 新增 resolveBrowserApiBaseUrl(): 智能解析浏览器端 API Base URL - installSessionNavigation(): 调用 fetchBootstrapState() 同步引导状态 - 新增 reconcileEntryRoute(): 根据引导状态协调路由 - VITE_SERVER_HOST 默认值从 127.0.0.1 改为 0.0.0.0 8. Setup 视图增强 (web/src/views/SetupRouteView.vue) - 向 SetupView 传递启动进度相关 props: startupCountdownSeconds, startupLog, startupSteps, startupVisible, progressMessage 9. CSS 新增 (web/src/assets/styles/views/setup-view.css) - .setup-complete-progress: 进度文字样式 - .setup-modal-backdrop: 模态框遮罩 - .setup-startup-modal: 启动模态框容器 - .setup-startup-head / .setup-startup-body / .setup-startup-spinner: 模态框头部/内容/加载动画 - .setup-startup-steps / .setup-startup-step: 步骤列表及单个步骤 - .setup-startup-step.is-running / .is-success / .is-failed: 步骤状态样式 - .setup-startup-log: 启动日志区域
2026-05-08 10:52:54 +08:00
server_host: shouldExposeServerHost() && ['127.0.0.1', 'localhost', '::1'].includes(form.server_host.trim().toLowerCase())
? '0.0.0.0'
: form.server_host.trim(),
server_port: Number(form.server_port),
postgres_host: form.postgres_host.trim(),
postgres_port: Number(form.postgres_port),
postgres_db: form.postgres_db.trim(),
postgres_user: form.postgres_user.trim(),
postgres_password: String(form.postgres_password || ''),
redis_url: form.redis_url.trim()
}
}
function buildRuntimeFingerprint(form) {
return JSON.stringify({
server_host: form.server_host.trim(),
server_port: String(form.server_port).trim()
})
}
function buildDatabaseFingerprint(form) {
return JSON.stringify({
postgres_host: form.postgres_host.trim(),
postgres_port: String(form.postgres_port).trim(),
postgres_db: form.postgres_db.trim(),
postgres_user: form.postgres_user.trim(),
postgres_password: String(form.postgres_password || ''),
redis_url: form.redis_url.trim()
})
}
function isEmail(value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(String(value || '').trim())
}
export function useSetupView(props, emit) {
const form = reactive(createForm(props.initialState))
const activeSection = ref('company')
let syncingFromProps = false
watch(
() => props.initialState,
(state) => {
syncingFromProps = true
Object.assign(form, createForm(state))
queueMicrotask(() => {
syncingFromProps = false
})
},
{ deep: true }
)
watch(
() => buildRuntimeFingerprint(form),
(_value, oldValue) => {
if (oldValue !== undefined && !syncingFromProps) {
emit('runtime-dirty')
}
}
)
watch(
() => buildDatabaseFingerprint(form),
(_value, oldValue) => {
if (oldValue !== undefined && !syncingFromProps) {
emit('database-dirty')
}
}
)
const companyReady = computed(() => form.company_name.trim().length >= 2)
const adminReady = computed(() => {
return Boolean(
isEmail(form.admin_email) &&
form.admin_username.trim().length >= 4 &&
String(form.admin_password || '').length >= 5 &&
form.admin_password === form.admin_password_confirm
)
})
const runtimeInputsReady = computed(() => {
return Boolean(
form.server_host.trim() &&
String(form.server_port).trim()
)
})
const databaseInputsReady = computed(() => {
return Boolean(
form.postgres_host.trim() &&
String(form.postgres_port).trim() &&
form.postgres_db.trim() &&
form.postgres_user.trim() &&
String(form.postgres_password || '').length > 0
)
})
const runtimeReady = computed(() => runtimeInputsReady.value && props.runtimeTestPassed)
const databaseReady = computed(() => databaseInputsReady.value && props.databaseTestPassed)
const finalReady = computed(() => companyReady.value && adminReady.value && runtimeReady.value && databaseReady.value)
const sections = computed(() => [
{
id: 'company',
index: '01',
title: '企业信息',
desc: '填写企业名称与识别编码。',
complete: companyReady.value
},
{
id: 'admin',
index: '02',
title: '管理员安全',
desc: '配置管理员邮箱、账号与密码。',
complete: adminReady.value
},
{
id: 'runtime',
index: '03',
title: '运行端口',
desc: 'Web 端口跟随当前启动实例,只检测后端端口占用。',
complete: runtimeReady.value
},
{
id: 'database',
index: '04',
title: '数据库',
desc: '检测 PostgreSQL 连接Redis 暂时可选。',
complete: databaseReady.value
}
])
const activeStep = computed(() => sections.value.find((section) => section.id === activeSection.value) || sections.value[0])
const completionCount = computed(() => sections.value.filter((section) => section.complete).length)
const runtimeEndpoints = computed(() => [
{
label: 'Web 当前访问',
value: `${form.web_host}:${form.web_port}`
},
{
label: 'Server 待启动',
value: `${form.server_host}:${form.server_port}`
}
])
const currentTestMessage = computed(() => {
if (activeSection.value === 'runtime') {
return props.runtimeTestMessage
}
if (activeSection.value === 'database') {
return props.databaseTestMessage
}
return ''
})
const currentTestPassed = computed(() => {
if (activeSection.value === 'runtime') {
return props.runtimeTestPassed
}
if (activeSection.value === 'database') {
return props.databaseTestPassed
}
return false
})
const showTestAction = computed(() => ['runtime', 'database'].includes(activeSection.value))
const testButtonLabel = computed(() => {
if (activeSection.value === 'runtime') {
return props.runtimeTesting ? '检测中...' : '检测端口占用'
}
if (activeSection.value === 'database') {
return props.databaseTesting ? '检测中...' : '检测数据库连接'
}
return ''
})
const testButtonIcon = computed(() => {
if ((activeSection.value === 'runtime' && props.runtimeTesting) || (activeSection.value === 'database' && props.databaseTesting)) {
return 'pi pi-spin pi-spinner'
}
return activeSection.value === 'runtime' ? 'pi pi-server' : 'pi pi-database'
})
const canRuntimeTest = computed(() => Boolean(runtimeInputsReady.value && !props.submitting && !props.runtimeTesting && !props.databaseTesting))
const canDatabaseTest = computed(() => Boolean(databaseInputsReady.value && !props.submitting && !props.runtimeTesting && !props.databaseTesting))
const canTest = computed(() => {
if (activeSection.value === 'runtime') {
return canRuntimeTest.value
}
if (activeSection.value === 'database') {
return canDatabaseTest.value
}
return false
})
const submitHint = computed(() => {
if (activeSection.value === 'admin') {
if (!form.admin_email.trim() && !form.admin_username.trim() && !String(form.admin_password || '').length) {
return ''
}
if (!form.admin_email.trim()) {
return '请填写管理员邮箱。'
}
if (!isEmail(form.admin_email)) {
return '管理员邮箱格式不正确。'
}
if (form.admin_username.trim() && form.admin_username.trim().length < 4) {
return '管理员账号至少 4 位。'
}
if (String(form.admin_password || '').length > 0 && String(form.admin_password || '').length < 5) {
return '管理员密码当前至少 5 位。'
}
if (
String(form.admin_password_confirm || '').length > 0 &&
form.admin_password !== form.admin_password_confirm
) {
return '两次输入的管理员密码不一致。'
}
}
if (activeSection.value === 'runtime') {
if (!runtimeInputsReady.value) {
return '请先填写 Server 的主机和端口。'
}
if (!props.runtimeTestPassed) {
return '请先完成端口占用检测。'
}
}
if (activeSection.value === 'database') {
if (!databaseInputsReady.value) {
return '请先填写 PostgreSQL 连接信息。'
}
if (!props.databaseTestPassed) {
return '请先完成数据库连接检测。'
}
}
if (activeSection.value === 'company') {
return ''
}
if (!companyReady.value) {
return '请先完成企业信息。'
}
if (!adminReady.value) {
return '请先完成管理员安全配置。'
}
if (!runtimeReady.value) {
return '请先完成运行端口检测。'
}
if (!databaseReady.value) {
return '请先完成数据库连接检测。'
}
return ''
})
function goToSection(id) {
activeSection.value = id
}
function submitForm() {
if (!finalReady.value || props.submitting) {
return
}
emit('submit', buildPayload(form))
}
function testSetup() {
if (!canTest.value) {
return
}
const payload = buildPayload(form)
if (activeSection.value === 'runtime') {
emit('runtime-test', payload)
return
}
if (activeSection.value === 'database') {
emit('database-test', payload)
}
}
return {
activeSection,
activeStep,
canSubmit: finalReady,
canTest,
completionCount,
currentTestMessage,
currentTestPassed,
form,
goToSection,
runtimeEndpoints,
sections,
showTestAction,
submitForm,
submitHint,
testButtonIcon,
testButtonLabel,
testSetup
}
}