fix: 修复 Docker 部署 API 地址与数据库连接问题

This commit is contained in:
2026-05-09 09:29:34 +08:00
parent 86568660a4
commit c2315f68dc
15 changed files with 665 additions and 119 deletions

View File

@@ -315,44 +315,123 @@ th {
.list-foot {
display: grid;
grid-template-columns: auto auto 1fr auto;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 10px;
color: #64748b;
font-size: 13px;
}
.list-foot button {
min-height: 32px;
border: 1px solid #d7e0ea;
border-radius: 8px;
background: #fff;
color: #334155;
font-weight: 750;
gap: 16px;
margin-top: 8px;
}
.pager {
display: inline-flex;
justify-content: center;
gap: 6px;
padding: 4px;
border: 1px solid #e2e8f0;
border-radius: 12px;
background: #f8fafc;
}
.pager button {
width: 32px;
height: 32px;
padding: 0;
border: 0;
border-radius: 9px;
background: transparent;
color: #334155;
font-size: 14px;
font-weight: 800;
transition: background 160ms ease, color 160ms ease, box-shadow 160ms ease;
}
.pager button:hover:not(.active) {
background: #fff;
color: #059669;
box-shadow: 0 1px 4px rgba(15, 23, 42, .08);
}
.pager button.active {
border-color: #059669;
background: #059669;
color: #fff;
box-shadow: 0 8px 16px rgba(5, 150, 105, .20);
}
.list-foot input {
width: 42px;
height: 30px;
.list-foot .page-summary {
color: #64748b;
font-size: 14px;
font-weight: 650;
}
.page-nav {
color: #64748b;
}
.page-size-wrap {
position: relative;
justify-self: end;
}
.page-size {
justify-self: end;
min-width: 112px;
min-height: 38px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
padding: 0 14px;
border: 1px solid #d7e0ea;
border-radius: 7px;
text-align: center;
border-radius: 10px;
background: #fff;
box-shadow: 0 1px 2px rgba(15, 23, 42, .04);
color: #334155;
font-size: 14px;
font-weight: 750;
white-space: nowrap;
transition: border-color 160ms ease, color 160ms ease;
}
.page-size:hover {
border-color: rgba(16, 185, 129, .32);
color: #0f9f78;
}
.page-size-dropdown {
position: absolute;
bottom: calc(100% + 6px);
right: 0;
z-index: 40;
display: grid;
border: 1px solid #d7e0ea;
border-radius: 10px;
background: #fff;
box-shadow: 0 12px 32px rgba(15, 23, 42, .14);
overflow: hidden;
}
.page-size-dropdown button {
height: 36px;
display: grid;
place-items: center;
border: 0;
border-radius: 0;
background: transparent;
color: #334155;
font-size: 13px;
font-weight: 750;
white-space: nowrap;
padding: 0 20px;
transition: background 120ms ease, color 120ms ease;
}
.page-size-dropdown button:hover {
background: #f0fdf4;
color: #059669;
}
.page-size-dropdown button.active {
background: #059669;
color: #fff;
}
.preview-column {
@@ -640,4 +719,15 @@ th {
.list-foot {
grid-template-columns: 1fr;
}
.list-foot {
gap: 12px;
justify-items: stretch;
}
.pager,
.page-size-wrap,
.page-size {
justify-self: stretch;
}
}

View File

@@ -27,26 +27,8 @@ const authIdleTimeoutMs =
? authIdleTimeoutMinutes * 60 * 1000
: 30 * 60 * 1000
function resolveBrowserApiBaseUrl(state) {
const server = state?.server || {}
const configuredHost = String(server.host || '127.0.0.1').trim()
const port = Number(server.port || 8000)
const apiPrefix = String(import.meta.env.VITE_API_BASE_PREFIX || '/api/v1').replace(/\/$/, '') || '/api/v1'
if (typeof window === 'undefined') {
return `http://${configuredHost}:${port}${apiPrefix}`
}
const browserHost = window.location.hostname
const normalizedHost = configuredHost.toLowerCase()
const host =
normalizedHost === '0.0.0.0' ||
normalizedHost === '::' ||
(normalizedHost === '127.0.0.1' && browserHost && browserHost !== '127.0.0.1' && browserHost !== 'localhost')
? browserHost
: configuredHost
return `http://${host}:${port}${apiPrefix}`
function resolveBrowserApiBaseUrl() {
return '/api/v1'
}
let sessionRouter = null

View File

@@ -39,9 +39,11 @@ function readStoredApiBaseUrl() {
return resolveBrowserReachableApiBaseUrl(window.localStorage.getItem(API_BASE_STORAGE_KEY) || '')
}
let runtimeApiBaseUrl = resolveBrowserReachableApiBaseUrl(
readStoredApiBaseUrl() || import.meta.env.VITE_API_BASE_URL || '/api/v1'
)
let runtimeApiBaseUrl = normalizeApiBaseUrl('/api/v1')
if (typeof window !== 'undefined') {
window.localStorage.removeItem(API_BASE_STORAGE_KEY)
}
export function setRuntimeApiBaseUrl(value) {
runtimeApiBaseUrl = resolveBrowserReachableApiBaseUrl(value)

View File

@@ -63,7 +63,7 @@
</thead>
<tbody>
<tr
v-for="doc in filteredDocuments"
v-for="doc in visibleDocuments"
:key="doc.name"
class="doc-row"
:class="{ selected: selectedDocument?.name === doc.name }"
@@ -93,6 +93,7 @@
</div>
<footer class="list-foot">
<template v-if="false">
<span> {{ filteredDocuments.length }} </span>
<button type="button">10/ <i class="mdi mdi-chevron-down"></i></button>
<div class="pager" aria-label="分页">
@@ -102,6 +103,45 @@
<button type="button" aria-label="下一页"><i class="mdi mdi-chevron-right"></i></button>
</div>
<label>前往 <input value="1" aria-label="页码" /> </label>
</template>
<span class="page-summary"> {{ totalCount }} 目前第 {{ currentPage }} </span>
<div class="pager" aria-label="分页">
<button class="page-nav" type="button" :disabled="currentPage === 1" aria-label="上一页" @click="currentPage--">
<i class="mdi mdi-chevron-left"></i>
</button>
<button
v-for="page in totalPages"
:key="page"
class="page-number"
:class="{ active: currentPage === page }"
type="button"
:aria-current="currentPage === page ? 'page' : undefined"
@click="currentPage = page"
>
{{ page }}
</button>
<button class="page-nav" type="button" :disabled="currentPage === totalPages" aria-label="下一页" @click="currentPage++">
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
<div class="page-size-wrap">
<button class="page-size" type="button" @click="pageSizeOpen = !pageSizeOpen">
{{ pageSize }} /<i class="mdi mdi-chevron-down"></i>
</button>
<div v-if="pageSizeOpen" class="page-size-dropdown" role="listbox">
<button
v-for="size in pageSizes"
:key="size"
type="button"
role="option"
:aria-selected="pageSize === size"
:class="{ active: pageSize === size }"
@click="changePageSize(size)"
>
{{ size }} /
</button>
</div>
</div>
</footer>
</section>
</div>

View File

@@ -1,5 +1,7 @@
import { computed, ref } from 'vue'
import { watch } from 'vue'
export default {
name: 'PoliciesView' ,
setup(props, { emit }) {
@@ -230,6 +232,30 @@ export default {
})
)
const currentPage = ref(1)
const pageSize = ref(10)
const pageSizes = [10, 20, 50]
const pageSizeOpen = ref(false)
const totalCount = computed(() => filteredDocuments.value.length)
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize.value)))
const visibleDocuments = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return filteredDocuments.value.slice(start, start + pageSize.value)
})
function changePageSize(size) {
pageSize.value = size
pageSizeOpen.value = false
currentPage.value = 1
}
watch(filteredDocuments, () => {
currentPage.value = 1
pageSizeOpen.value = false
})
return {
folderSearch,
activeFolder,
@@ -237,7 +263,15 @@ export default {
folders,
documents,
filteredFolders,
filteredDocuments
filteredDocuments,
currentPage,
pageSize,
pageSizes,
pageSizeOpen,
totalCount,
totalPages,
visibleDocuments,
changePageSize
}
}
}

View File

@@ -106,9 +106,36 @@ function parseEnv(text) {
return result
}
const envOverridePrefixes = ['APP_', 'WEB_', 'SERVER_', 'POSTGRES_', 'VITE_', 'LOG_']
const envOverrideKeys = new Set([
'API_V1_PREFIX',
'SETUP_COMPLETED',
'COMPANY_NAME',
'COMPANY_CODE',
'ADMIN_EMAIL',
'DATABASE_URL',
'SQLALCHEMY_ECHO',
'REDIS_URL',
'CORS_ORIGINS'
])
function shouldOverlayEnvKey(key) {
return envOverrideKeys.has(key) || envOverridePrefixes.some((prefix) => key.startsWith(prefix))
}
function readEnvState() {
ensureEnvFile()
return parseEnv(fs.readFileSync(envFile, 'utf8'))
const state = parseEnv(fs.readFileSync(envFile, 'utf8'))
for (const [key, value] of Object.entries(process.env)) {
if (!shouldOverlayEnvKey(key) || value == null || value === '') {
continue
}
state[key] = String(value)
}
return state
}
function readAdminSecret() {
@@ -334,9 +361,7 @@ function buildCorsOrigins(payload) {
function buildApiBaseUrl(payload, currentEnv) {
const apiPrefix = currentEnv.API_V1_PREFIX || '/api/v1'
const host = resolveBrowserApiHost(payload.server_host, payload.web_host)
const port = String(payload.server_port || '').trim()
return `http://${host}:${port}${apiPrefix}`
return apiPrefix
}
function buildServerHealthUrl(env) {
@@ -586,9 +611,21 @@ async function loadPgClient() {
async function testDatabaseConnection(payload) {
const Client = await loadPgClient()
const requestedHost = String(payload.postgres_host || '').trim()
const requestedHostNormalized = normalizeLoopbackHost(requestedHost)
const dockerPostgresHost = String(process.env.POSTGRES_HOST || '').trim()
const containerPostgresPort = Number(process.env.POSTGRES_PORT || 5432)
const shouldUseDockerPostgres =
dockerPostgresHost === 'postgres' &&
['127.0.0.1', 'localhost', '0.0.0.0', '::1', '::'].includes(requestedHostNormalized)
const effectiveHost =
shouldUseDockerPostgres ? 'postgres' : requestedHost
const effectivePort =
shouldUseDockerPostgres ? containerPostgresPort : Number(payload.postgres_port)
const client = new Client({
host: String(payload.postgres_host || '').trim(),
port: Number(payload.postgres_port),
host: effectiveHost,
port: effectivePort,
database: String(payload.postgres_db || '').trim(),
user: String(payload.postgres_user || '').trim(),
password: String(payload.postgres_password || ''),
@@ -738,9 +775,21 @@ async function startBackendAndWait() {
const stdout = fs.openSync(logFile, 'a')
const stderr = fs.openSync(logFile, 'a')
const freshEnv = { ...process.env }
const envFileContent = fs.readFileSync(envFile, 'utf-8')
for (const line of envFileContent.split('\n')) {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) continue
const eqIdx = trimmed.indexOf('=')
if (eqIdx < 0) continue
const key = trimmed.slice(0, eqIdx).trim()
const val = trimmed.slice(eqIdx + 1).trim().replace(/^['"]|['"]$/g, '')
freshEnv[key] = val
}
const child = spawn('bash', [path.join(rootDir, 'start.sh'), 'server'], {
cwd: rootDir,
detached: true,
env: freshEnv,
stdio: ['ignore', stdout, stderr]
})
@@ -980,6 +1029,12 @@ export default defineConfig({
envExampleFile,
path.join(rootDir, 'server', 'logs', '**')
]
},
proxy: {
'/api': {
target: `http://127.0.0.1:${process.env.SERVER_PORT || 8000}`,
changeOrigin: true
}
}
},
plugins: [vue(), localSetupPlugin()]

View File

@@ -1,9 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
#!/usr/bin/env sh
set -eu
if (set -o pipefail) >/dev/null 2>&1; then
set -o pipefail
fi
export MSYS_NO_PATHCONV=1
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_PATH="$0"
case "$SCRIPT_PATH" in
/*) ;;
*) SCRIPT_PATH="$(pwd)/$SCRIPT_PATH" ;;
esac
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
cd "$SCRIPT_DIR"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
ROOT_ENV_FILE="$ROOT_DIR/.env"
@@ -14,14 +23,45 @@ GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
info() { printf '%b\n' "${GREEN}[INFO]${NC} $*"; }
warn() { printf '%b\n' "${YELLOW}[WARN]${NC} $*"; }
error() { printf '%b\n' "${RED}[ERROR]${NC} $*"; exit 1; }
if [ -f "$ROOT_ENV_FILE" ]; then
ENV_OVERRIDE_WEB_HOST_SET=false
ENV_OVERRIDE_SERVER_HOST_SET=false
ENV_OVERRIDE_POSTGRES_HOST_SET=false
if [ "${WEB_HOST+x}" = x ]; then
ENV_OVERRIDE_WEB_HOST_SET=true
ENV_OVERRIDE_WEB_HOST="$WEB_HOST"
fi
if [ "${SERVER_HOST+x}" = x ]; then
ENV_OVERRIDE_SERVER_HOST_SET=true
ENV_OVERRIDE_SERVER_HOST="$SERVER_HOST"
fi
if [ "${POSTGRES_HOST+x}" = x ]; then
ENV_OVERRIDE_POSTGRES_HOST_SET=true
ENV_OVERRIDE_POSTGRES_HOST="$POSTGRES_HOST"
fi
set -a
. "$ROOT_ENV_FILE"
set +a
if [ "$ENV_OVERRIDE_WEB_HOST_SET" = true ]; then
WEB_HOST="$ENV_OVERRIDE_WEB_HOST"
fi
if [ "$ENV_OVERRIDE_SERVER_HOST_SET" = true ]; then
SERVER_HOST="$ENV_OVERRIDE_SERVER_HOST"
fi
if [ "$ENV_OVERRIDE_POSTGRES_HOST_SET" = true ]; then
POSTGRES_HOST="$ENV_OVERRIDE_POSTGRES_HOST"
fi
fi
if [ "${X_FINANCIAL_FORCE_SETUP:-false}" = "true" ]; then
@@ -66,26 +106,24 @@ windows_project_path() {
wslpath -w "$SCRIPT_DIR"
}
shell_quote_single() {
printf "%s" "$1" | sed "s/'/''/g"
}
run_windows_powershell() {
local command="$1"
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$command"
powershell_command="$1"
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$powershell_command"
}
run_windows_npm_install() {
local win_path
local win_path_ps
win_path="$(windows_project_path)"
win_path_ps="${win_path//\'/\'\'}"
win_path_ps="$(shell_quote_single "$win_path")"
run_windows_powershell "Set-Location -LiteralPath '$win_path_ps'; npm install"
}
run_windows_npm_start() {
local win_path
local win_path_ps
win_path="$(windows_project_path)"
win_path_ps="${win_path//\'/\'\'}"
win_path_ps="$(shell_quote_single "$win_path")"
exec powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Set-Location -LiteralPath '$win_path_ps'; npm start -- --host $WEB_HOST --port $WEB_PORT"
}
@@ -96,11 +134,8 @@ dependencies_ready() {
[ -f "node_modules/vue-router/package.json" ] || return 1
if use_windows_npm; then
local win_path
local win_path_ps
win_path="$(windows_project_path)"
win_path_ps="${win_path//\'/\'\'}"
win_path_ps="$(shell_quote_single "$win_path")"
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Set-Location -LiteralPath '$win_path_ps'; node -e \"require('rollup'); require('pg'); require('vue-router')\"" >/dev/null 2>&1
return $?
fi