Files
X-Financial/web/src/composables/useSetupView.js
DESKTOP-72TV0V4\caoxiaozhu ae63766c91 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.
2026-05-06 22:23:42 +08:00

384 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { computed, reactive, ref, watch } from 'vue'
function createForm(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: initialState?.web?.host || '127.0.0.1',
web_port: initialState?.web?.port || 5173,
server_host: initialState?.server?.host || '127.0.0.1',
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) {
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: form.web_host.trim(),
web_port: Number(form.web_port),
server_host: 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({
web_host: form.web_host.trim(),
web_port: String(form.web_port).trim(),
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.web_host.trim() &&
String(form.web_port).trim() &&
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 summaryItems = computed(() => [
{
label: '企业信息',
detail: form.company_name.trim() || '未完成',
complete: companyReady.value
},
{
label: '管理员安全',
detail: form.admin_username.trim() || form.admin_email.trim() || '未完成',
complete: adminReady.value
},
{
label: '运行端口',
detail: `${form.web_host}:${form.web_port} / ${form.server_host}:${form.server_port}`,
complete: runtimeReady.value
},
{
label: '数据库',
detail: `${form.postgres_host}:${form.postgres_port}/${form.postgres_db}`,
complete: databaseReady.value
}
])
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 '请先填写 Web 与 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,
summaryItems,
testButtonIcon,
testButtonLabel,
testSetup
}
}