diff --git a/.env.example b/.env.example index 2b72098..f424f05 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8000 +NEXT_PUBLIC_APP_BASE_PATH=/fl API_HOST=0.0.0.0 API_PORT=8000 API_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 079335f..b35c243 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -70,6 +70,7 @@ jobs: push: true build-args: | NEXT_PUBLIC_API_BASE_URL=${{ vars.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:8000' }} + NEXT_PUBLIC_APP_BASE_PATH=${{ vars.NEXT_PUBLIC_APP_BASE_PATH || '/fl' }} tags: | ${{ steps.vars.outputs.web_image }}:${{ steps.vars.outputs.image_tag }} ${{ steps.vars.outputs.web_image }}:latest @@ -171,6 +172,7 @@ jobs: API_IMAGE=${API_IMAGE}:${IMAGE_TAG} WEB_IMAGE=${WEB_IMAGE}:${IMAGE_TAG} NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL} + NEXT_PUBLIC_APP_BASE_PATH=/fl FLOWER_BASIC_AUTH=${FLOWER_BASIC_AUTH} ENV diff --git a/deploy/dev-deploy/.env.dev b/deploy/dev-deploy/.env.dev index 3511f27..844e6dc 100644 --- a/deploy/dev-deploy/.env.dev +++ b/deploy/dev-deploy/.env.dev @@ -52,5 +52,7 @@ POSTGRES_DB=fquiz POSTGRES_USER=fquiz POSTGRES_PASSWORD=replace_me +NEXT_PUBLIC_APP_BASE_PATH=/fl + API_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 API_CORS_ORIGIN_REGEX= diff --git a/deploy/dev-deploy/compose.yml b/deploy/dev-deploy/compose.yml index 1c8f523..8cff7cf 100644 --- a/deploy/dev-deploy/compose.yml +++ b/deploy/dev-deploy/compose.yml @@ -197,11 +197,13 @@ services: args: NODE_BASE_IMAGE: ${NODE_BASE_IMAGE} NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + NEXT_PUBLIC_APP_BASE_PATH: ${NEXT_PUBLIC_APP_BASE_PATH:-/fl} container_name: ${COMPOSE_PROJECT_NAME}-web depends_on: - api environment: NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + NEXT_PUBLIC_APP_BASE_PATH: ${NEXT_PUBLIC_APP_BASE_PATH:-/fl} NODE_ENV: production ports: - "3000:3000" diff --git a/deploy/pro-deploy/.env b/deploy/pro-deploy/.env index 2e83f6c..4521160 100644 --- a/deploy/pro-deploy/.env +++ b/deploy/pro-deploy/.env @@ -16,6 +16,7 @@ API_PORT=8000 FLOWER_PORT=5555 NEXT_PUBLIC_API_BASE_URL=https://quiz.example.com/api +NEXT_PUBLIC_APP_BASE_PATH=/fl CELERY_LOG_LEVEL=INFO CELERY_WORKER_CONCURRENCY=4 CELERY_WORKER_QUEUES=default,celery diff --git a/deploy/pro-deploy/.env.prod b/deploy/pro-deploy/.env.prod index 19b3b61..4a4aff5 100644 --- a/deploy/pro-deploy/.env.prod +++ b/deploy/pro-deploy/.env.prod @@ -52,5 +52,7 @@ POSTGRES_DB=fquiz POSTGRES_USER=fquiz POSTGRES_PASSWORD=replace_strong_password +NEXT_PUBLIC_APP_BASE_PATH=/fl + API_CORS_ORIGINS=https://quiz.example.com API_CORS_ORIGIN_REGEX= diff --git a/deploy/pro-deploy/compose.yml b/deploy/pro-deploy/compose.yml index ba1fe89..e1d19fb 100644 --- a/deploy/pro-deploy/compose.yml +++ b/deploy/pro-deploy/compose.yml @@ -160,6 +160,7 @@ services: - api environment: NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + NEXT_PUBLIC_APP_BASE_PATH: ${NEXT_PUBLIC_APP_BASE_PATH:-/fl} NODE_ENV: production ports: - "3000:3000" diff --git a/web/Dockerfile b/web/Dockerfile index 83c0cb6..d4105d5 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -12,7 +12,9 @@ FROM ${NODE_BASE_IMAGE} AS builder WORKDIR /app ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 +ARG NEXT_PUBLIC_APP_BASE_PATH=/fl ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL} +ENV NEXT_PUBLIC_APP_BASE_PATH=${NEXT_PUBLIC_APP_BASE_PATH} COPY --from=deps /app/node_modules ./node_modules COPY . . @@ -23,6 +25,8 @@ WORKDIR /app ENV NODE_ENV=production ENV PORT=3000 +ARG NEXT_PUBLIC_APP_BASE_PATH=/fl +ENV NEXT_PUBLIC_APP_BASE_PATH=${NEXT_PUBLIC_APP_BASE_PATH} RUN addgroup -S nodejs && adduser -S nextjs -G nodejs diff --git a/web/next.config.ts b/web/next.config.ts index 98aa7ac..1c89abd 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -1,8 +1,21 @@ import type { NextConfig } from "next"; import path from "node:path"; +function normalizeBasePath(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + if (!trimmed || trimmed === "/") { + return undefined; + } + + const normalized = trimmed.startsWith("/") ? trimmed : `/${trimmed}`; + return normalized.replace(/\/+$/, ""); +} + +const basePath = normalizeBasePath(process.env.NEXT_PUBLIC_APP_BASE_PATH); + const nextConfig: NextConfig = { output: "standalone", + basePath, webpack(config) { config.resolve = config.resolve ?? {}; config.resolve.alias = { diff --git a/web/src/app/admin/layout.tsx b/web/src/app/admin/layout.tsx index 17a5774..13ab9f3 100644 --- a/web/src/app/admin/layout.tsx +++ b/web/src/app/admin/layout.tsx @@ -39,6 +39,7 @@ import { useTopicSubscription } from "@/hooks/use-topic-subscription"; import { readApiError } from "@/lib/api"; import type { MenuTreeItem } from "@/types/auth"; import { useThemeAppearance } from "@/components/ui-antd"; +import { withBasePath } from "@/lib/base-path"; const { Header, Sider, Content } = AntLayout; const AntResult = Result as unknown as ComponentType; @@ -364,7 +365,7 @@ export default function AdminLayout({ children }: { children: ReactNode }) { className="ml-2 flex items-center gap-2 text-inherit no-underline md:ml-0" > 高压电塔图标 - + {canManage && (