feat: 优化网络绑定配置支持外部访问

主要修改点:
1. 网络绑定调整
   - .env.example: WEB_HOST/VITE_WEB_HOST 从 127.0.0.1 改为 0.0.0.0
   - server/src/app/core/config.py: 默认 web_host 从 127.0.0.1 改为 0.0.0.0
   - web/package.json: Vite 脚本 host 从 127.0.0.1 改为 0.0.0.0
   - web/vite.config.js: normalizeState 中 WEB_HOST 默认值从 127.0.0.1 改为 0.0.0.0

2. CORS 配置扩展
   - .env.example: CORS_ORIGINS 添加 http://0.0.0.0:5173

3. Shell 脚本权限修复
   - server/start.sh, start.sh: 添加可执行权限 (644 -> 755)

4. Setup 表单与验证逻辑简化
   - web/src/composables/useSetupView.js:
     - 新增 readCurrentWebEndpoint() 从 window.location 自动检测当前 web host/port
     - 简化 runtimeInputsReady: 移除 web_host/web_port 必填验证,仅保留 server_host/server_port
     - 简化 buildRuntimeFingerprint(): 移除 web_host/web_port
     - buildPayload() 改用 readCurrentWebEndpoint() 解析 web 配置
   - web/vite.config.js:
     - 新增 resolveRuntimePayload(): 运行时解析 web_host/web_port
     - 移除 validateRuntimePayload() 中的 web_host/web_port 字段验证
     - 移除 testRuntimePorts() 中 web 端口冲突检测逻辑
     - 移除 canReuseCurrentWebPort() 函数

5. CSS 清理
   - web/src/assets/styles/views/setup-view.css: 移除未使用的 .setup-summary-grid 和 .setup-summary-item 样式
This commit is contained in:
2026-05-08 09:27:45 +08:00
parent adda87a01d
commit 828e8f5aaf
11 changed files with 61 additions and 132 deletions

View File

@@ -13,9 +13,9 @@ VITE_COMPANY_CODE=
VITE_ADMIN_EMAIL= VITE_ADMIN_EMAIL=
# Admin login credentials are stored separately under server/.secrets/ # Admin login credentials are stored separately under server/.secrets/
WEB_HOST=127.0.0.1 WEB_HOST=0.0.0.0
WEB_PORT=5173 WEB_PORT=5173
VITE_WEB_HOST=127.0.0.1 VITE_WEB_HOST=0.0.0.0
VITE_WEB_PORT=5173 VITE_WEB_PORT=5173
SERVER_HOST=127.0.0.1 SERVER_HOST=127.0.0.1
@@ -43,4 +43,4 @@ SQLALCHEMY_ECHO=false
REDIS_URL= REDIS_URL=
VITE_REDIS_URL= VITE_REDIS_URL=
CORS_ORIGINS='["http://127.0.0.1:5173","http://localhost:5173"]' CORS_ORIGINS='["http://127.0.0.1:5173","http://localhost:5173","http://0.0.0.0:5173"]'

View File

@@ -27,7 +27,7 @@ class Settings(BaseSettings):
company_code: str = Field(default="", alias="COMPANY_CODE") company_code: str = Field(default="", alias="COMPANY_CODE")
admin_email: str = Field(default="", alias="ADMIN_EMAIL") admin_email: str = Field(default="", alias="ADMIN_EMAIL")
web_host: str = Field(default="127.0.0.1", alias="WEB_HOST") web_host: str = Field(default="0.0.0.0", alias="WEB_HOST")
web_port: int = Field(default=5173, alias="WEB_PORT") web_port: int = Field(default=5173, alias="WEB_PORT")
app_host: str = Field(default="127.0.0.1", alias="SERVER_HOST") app_host: str = Field(default="127.0.0.1", alias="SERVER_HOST")
app_port: int = Field(default=8000, alias="SERVER_PORT") app_port: int = Field(default=8000, alias="SERVER_PORT")

0
server/start.sh Normal file → Executable file
View File

0
start.sh Normal file → Executable file
View File

View File

@@ -4,10 +4,10 @@
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "vite --host 127.0.0.1", "start": "vite --host 0.0.0.0",
"dev": "vite --host 127.0.0.1", "dev": "vite --host 0.0.0.0",
"build": "vite build", "build": "vite build",
"preview": "vite preview --host 127.0.0.1" "preview": "vite preview --host 0.0.0.0"
}, },
"dependencies": { "dependencies": {
"@primevue/themes": "^4.5.4", "@primevue/themes": "^4.5.4",

View File

@@ -426,46 +426,6 @@
text-shadow: 0 2px 16px rgba(4, 9, 7, 0.22); text-shadow: 0 2px 16px rgba(4, 9, 7, 0.22);
} }
.setup-summary-grid {
display: grid;
gap: 12px;
}
.setup-summary-item {
padding: 16px 18px;
border: 1px solid rgba(16, 185, 129, 0.14);
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
background: rgba(248, 250, 252, 0.9);
}
.setup-summary-item strong {
display: block;
color: #0f172a;
font-size: 14px;
}
.setup-summary-item span {
display: block;
margin-top: 4px;
color: #64748b;
font-size: 12px;
line-height: 1.55;
}
.setup-summary-item .pi-check-circle {
color: #10b981;
font-size: 18px;
}
.setup-summary-item .pi-clock {
color: #f59e0b;
font-size: 18px;
}
.setup-error { .setup-error {
margin-top: 22px; margin-top: 22px;
padding: 14px 16px; padding: 14px 16px;

View File

@@ -1,6 +1,25 @@
import { computed, reactive, ref, watch } from 'vue' 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
}
}
function createForm(initialState) { function createForm(initialState) {
const currentWeb = readCurrentWebEndpoint(initialState)
return { return {
company_name: initialState?.company?.name || '', company_name: initialState?.company?.name || '',
company_code: initialState?.company?.code || '', company_code: initialState?.company?.code || '',
@@ -8,8 +27,8 @@ function createForm(initialState) {
admin_username: '', admin_username: '',
admin_password: '', admin_password: '',
admin_password_confirm: '', admin_password_confirm: '',
web_host: initialState?.web?.host || '127.0.0.1', web_host: currentWeb.host,
web_port: initialState?.web?.port || 5173, web_port: currentWeb.port,
server_host: initialState?.server?.host || '127.0.0.1', server_host: initialState?.server?.host || '127.0.0.1',
server_port: initialState?.server?.port || 8000, server_port: initialState?.server?.port || 8000,
postgres_host: initialState?.database?.host || '127.0.0.1', postgres_host: initialState?.database?.host || '127.0.0.1',
@@ -22,6 +41,13 @@ function createForm(initialState) {
} }
function buildPayload(form) { function buildPayload(form) {
const currentWeb = readCurrentWebEndpoint({
web: {
host: form.web_host,
port: form.web_port
}
})
return { return {
company_name: form.company_name.trim(), company_name: form.company_name.trim(),
company_code: form.company_code.trim(), company_code: form.company_code.trim(),
@@ -29,8 +55,8 @@ function buildPayload(form) {
admin_username: form.admin_username.trim(), admin_username: form.admin_username.trim(),
admin_password: String(form.admin_password || ''), admin_password: String(form.admin_password || ''),
admin_password_confirm: String(form.admin_password_confirm || ''), admin_password_confirm: String(form.admin_password_confirm || ''),
web_host: form.web_host.trim(), web_host: currentWeb.host,
web_port: Number(form.web_port), web_port: currentWeb.port,
server_host: form.server_host.trim(), server_host: form.server_host.trim(),
server_port: Number(form.server_port), server_port: Number(form.server_port),
postgres_host: form.postgres_host.trim(), postgres_host: form.postgres_host.trim(),
@@ -44,8 +70,6 @@ function buildPayload(form) {
function buildRuntimeFingerprint(form) { function buildRuntimeFingerprint(form) {
return JSON.stringify({ return JSON.stringify({
web_host: form.web_host.trim(),
web_port: String(form.web_port).trim(),
server_host: form.server_host.trim(), server_host: form.server_host.trim(),
server_port: String(form.server_port).trim() server_port: String(form.server_port).trim()
}) })
@@ -112,8 +136,6 @@ export function useSetupView(props, emit) {
}) })
const runtimeInputsReady = computed(() => { const runtimeInputsReady = computed(() => {
return Boolean( return Boolean(
form.web_host.trim() &&
String(form.web_port).trim() &&
form.server_host.trim() && form.server_host.trim() &&
String(form.server_port).trim() String(form.server_port).trim()
) )
@@ -151,7 +173,7 @@ export function useSetupView(props, emit) {
id: 'runtime', id: 'runtime',
index: '03', index: '03',
title: '运行端口', title: '运行端口',
desc: '单独检测 Web 与后端端口占用。', desc: 'Web 端口跟随当前启动实例,只检测后端端口占用。',
complete: runtimeReady.value complete: runtimeReady.value
}, },
{ {
@@ -168,38 +190,15 @@ export function useSetupView(props, emit) {
const runtimeEndpoints = computed(() => [ const runtimeEndpoints = computed(() => [
{ {
label: 'Web', label: 'Web 当前访问',
value: `${form.web_host}:${form.web_port}` value: `${form.web_host}:${form.web_port}`
}, },
{ {
label: 'Server', label: 'Server 待启动',
value: `${form.server_host}:${form.server_port}` 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(() => { const currentTestMessage = computed(() => {
if (activeSection.value === 'runtime') { if (activeSection.value === 'runtime') {
return props.runtimeTestMessage return props.runtimeTestMessage
@@ -290,7 +289,7 @@ export function useSetupView(props, emit) {
if (activeSection.value === 'runtime') { if (activeSection.value === 'runtime') {
if (!runtimeInputsReady.value) { if (!runtimeInputsReady.value) {
return '请先填写 Web 与 Server 的主机和端口。' return '请先填写 Server 的主机和端口。'
} }
if (!props.runtimeTestPassed) { if (!props.runtimeTestPassed) {
@@ -375,7 +374,6 @@ export function useSetupView(props, emit) {
showTestAction, showTestAction,
submitForm, submitForm,
submitHint, submitHint,
summaryItems,
testButtonIcon, testButtonIcon,
testButtonLabel, testButtonLabel,
testSetup testSetup

View File

@@ -38,7 +38,7 @@ function readClientBootstrapState() {
admin_email: env.VITE_ADMIN_EMAIL || '' admin_email: env.VITE_ADMIN_EMAIL || ''
}, },
web: { web: {
host: env.VITE_WEB_HOST || '127.0.0.1', host: env.VITE_WEB_HOST || '0.0.0.0',
port: Number(env.VITE_WEB_PORT || 5173) port: Number(env.VITE_WEB_PORT || 5173)
}, },
server: { server: {

View File

@@ -126,20 +126,10 @@
<section v-else-if="activeSection === 'runtime'" class="setup-stage"> <section v-else-if="activeSection === 'runtime'" class="setup-stage">
<div class="section-head"> <div class="section-head">
<h3>运行端口配置</h3> <h3>运行端口配置</h3>
<p>这一步只检测 Web Server 端口占用情况不检测数据库</p> <p>Web 地址由当前已启动的前端实例自动确定这一步只需要配置并检测后端端口</p>
</div> </div>
<div class="field-grid field-grid-2"> <div class="field-grid field-grid-2">
<label class="field">
<span>Web Host</span>
<input v-model.trim="form.web_host" type="text" placeholder="127.0.0.1" required />
</label>
<label class="field">
<span>Web Port</span>
<input v-model.number="form.web_port" type="number" min="1" max="65535" required />
</label>
<label class="field"> <label class="field">
<span>Server Host</span> <span>Server Host</span>
<input v-model.trim="form.server_host" type="text" placeholder="127.0.0.1" required /> <input v-model.trim="form.server_host" type="text" placeholder="127.0.0.1" required />
@@ -210,15 +200,6 @@
</label> </label>
</div> </div>
<div class="setup-summary-grid">
<article v-for="item in summaryItems" :key="item.label" class="setup-summary-item">
<div>
<strong>{{ item.label }}</strong>
<span>{{ item.detail }}</span>
</div>
<i :class="['pi', item.complete ? 'pi-check-circle' : 'pi-clock']"></i>
</article>
</div>
</section> </section>
<p v-if="currentTestMessage" :class="['setup-status', currentTestPassed ? 'is-success' : 'is-danger']"> <p v-if="currentTestMessage" :class="['setup-status', currentTestPassed ? 'is-success' : 'is-danger']">
@@ -306,7 +287,6 @@ const {
showTestAction, showTestAction,
submitForm, submitForm,
submitHint, submitHint,
summaryItems,
testButtonIcon, testButtonIcon,
testButtonLabel, testButtonLabel,
testSetup testSetup

2
web/start.sh Normal file → Executable file
View File

@@ -24,7 +24,7 @@ if [ -f "$ROOT_ENV_FILE" ]; then
set +a set +a
fi fi
WEB_HOST="${WEB_HOST:-127.0.0.1}" WEB_HOST="${WEB_HOST:-0.0.0.0}"
WEB_PORT="${WEB_PORT:-5173}" WEB_PORT="${WEB_PORT:-5173}"
export VITE_SETUP_COMPLETED="${SETUP_COMPLETED:-false}" export VITE_SETUP_COMPLETED="${SETUP_COMPLETED:-false}"

View File

@@ -309,7 +309,7 @@ function normalizeState(env) {
configured: Boolean(readAdminSecret()) configured: Boolean(readAdminSecret())
}, },
web: { web: {
host: env.WEB_HOST || '127.0.0.1', host: env.WEB_HOST || '0.0.0.0',
port: Number(env.WEB_PORT || 5173) port: Number(env.WEB_PORT || 5173)
}, },
server: { server: {
@@ -350,7 +350,6 @@ function sendJson(res, statusCode, payload) {
function validateRuntimePayload(payload) { function validateRuntimePayload(payload) {
const fields = [ const fields = [
['web_host', 'Web Host'],
['server_host', 'Server Host'] ['server_host', 'Server Host']
] ]
@@ -361,7 +360,6 @@ function validateRuntimePayload(payload) {
} }
const portFields = [ const portFields = [
['web_port', 'Web Port'],
['server_port', 'Server Port'] ['server_port', 'Server Port']
] ]
@@ -376,6 +374,14 @@ function validateRuntimePayload(payload) {
return '' return ''
} }
function resolveRuntimePayload(payload, currentEnv) {
return {
...payload,
web_host: String(payload.web_host || currentEnv.WEB_HOST || '0.0.0.0').trim(),
web_port: Number(payload.web_port || currentEnv.WEB_PORT || 5173)
}
}
function validateDatabasePayload(payload) { function validateDatabasePayload(payload) {
const fields = [ const fields = [
['postgres_host', 'PostgreSQL Host'], ['postgres_host', 'PostgreSQL Host'],
@@ -444,13 +450,6 @@ function validateSetupPayload(payload) {
return validateIdentityPayload(payload) || validateRuntimePayload(payload) || validateDatabasePayload(payload) return validateIdentityPayload(payload) || validateRuntimePayload(payload) || validateDatabasePayload(payload)
} }
function canReuseCurrentWebPort(payload, currentEnv) {
return (
Number(payload.web_port) === Number(currentEnv.WEB_PORT || 5173) &&
hostsConflict(String(payload.web_host || '').trim(), currentEnv.WEB_HOST || '127.0.0.1')
)
}
async function assertPortAvailable(host, port) { async function assertPortAvailable(host, port) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const tester = net.createServer() const tester = net.createServer()
@@ -468,7 +467,7 @@ async function assertPortAvailable(host, port) {
}) })
} }
async function testRuntimePorts(payload, currentEnv) { async function testRuntimePorts(payload) {
const webPort = Number(payload.web_port) const webPort = Number(payload.web_port)
const serverPort = Number(payload.server_port) const serverPort = Number(payload.server_port)
const webHost = String(payload.web_host || '').trim() const webHost = String(payload.web_host || '').trim()
@@ -478,14 +477,6 @@ async function testRuntimePorts(payload, currentEnv) {
throw new Error('Web 与 Server 不能使用同一个主机与端口组合。') throw new Error('Web 与 Server 不能使用同一个主机与端口组合。')
} }
if (!canReuseCurrentWebPort(payload, currentEnv)) {
try {
await assertPortAvailable(webHost, webPort)
} catch {
throw new Error(`Web 端口 ${webHost}:${webPort} 已被占用。`)
}
}
try { try {
await assertPortAvailable(serverHost, serverPort) await assertPortAvailable(serverHost, serverPort)
} catch { } catch {
@@ -569,7 +560,7 @@ function localSetupPlugin() {
return return
} }
const payload = await readJsonBody(req) const payload = resolveRuntimePayload(await readJsonBody(req), readEnvState())
const validationError = validateRuntimePayload(payload) const validationError = validateRuntimePayload(payload)
if (validationError) { if (validationError) {
@@ -578,8 +569,8 @@ function localSetupPlugin() {
} }
try { try {
await testRuntimePorts(payload, readEnvState()) await testRuntimePorts(payload)
sendJson(res, 200, { ok: true, detail: '端口占用检测通过。' }) sendJson(res, 200, { ok: true, detail: 'Server 端口占用检测通过。' })
} catch (error) { } catch (error) {
sendJson(res, 400, { sendJson(res, 400, {
ok: false, ok: false,
@@ -636,7 +627,8 @@ function localSetupPlugin() {
return return
} }
const payload = await readJsonBody(req) const currentEnv = readEnvState()
const payload = resolveRuntimePayload(await readJsonBody(req), currentEnv)
const validationError = validateSetupPayload(payload) const validationError = validateSetupPayload(payload)
if (validationError) { if (validationError) {
@@ -645,7 +637,7 @@ function localSetupPlugin() {
} }
try { try {
await testRuntimePorts(payload, readEnvState()) await testRuntimePorts(payload)
await testDatabaseConnection(payload) await testDatabaseConnection(payload)
} catch (error) { } catch (error) {
sendJson(res, 400, { sendJson(res, 400, {
@@ -656,7 +648,6 @@ function localSetupPlugin() {
persistAdminCredentials(payload) persistAdminCredentials(payload)
const currentEnv = readEnvState()
const apiBaseUrl = buildApiBaseUrl(payload, currentEnv) const apiBaseUrl = buildApiBaseUrl(payload, currentEnv)
updateEnvFile({ updateEnvFile({