Files
X-Financial/web/src/composables/useSetupView.js
caoxiaozhu 0fac8b615f feat(web): 优化差旅详情、风险建议卡片与文档中心交互
- 拆分阶段风险建议卡片样式到独立文件
- 完善差旅申请审批对话框与详情视图交互
- 调整文档中心列表共享样式与状态筛选
- 同步应用外壳、视图初始化与系统状态 composables
2026-06-17 14:39:12 +08:00

404 lines
11 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 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
}
}