fix: 修复 Docker 部署 API 地址与数据库连接问题
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user