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 || 5273) } } const fallbackPort = Number(initialState?.web?.port || 5273) 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 } } 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, 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, 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 'mdi mdi-loading mdi-spin' } return activeSection.value === 'runtime' ? 'mdi mdi-server' : 'mdi mdi-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 } }