feat(config): 更新环境配置与 API 处理逻辑
修改 .env.example,补充环境切换说明并新增生产环境 API 地址配置。 更新 next.config.ts:使用 API_BASE_URL 进行 API 重写配置,确保 API 路由一致性。 重构 login-form.tsx:移除 API 配置检查逻辑,简化登录流程。 调整 admin-http.ts:通过 Next.js 代理转发 API 请求,提升后端通信稳定性。
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 管理端本地配置示例:复制为 .env.local 后按需修改
|
# 管理端本地配置示例:复制为 .env.local 后按需修改
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
# 手动切换环境:保留一个生效,另一个注释掉
|
||||||
|
|
||||||
# 必填:Laravel 应用根 URL(无尾部斜杠)。axios 会请求 {此地址}/api/v1/...
|
# 测试
|
||||||
# 需保证 Laravel 已允许该来源的 CORS(本地管理端为 http://localhost:3801 等)。
|
API_BASE_URL=http://127.0.0.1:8000
|
||||||
NEXT_PUBLIC_LOTTERY_API_BASE_URL=http://127.0.0.1:8000
|
# 线上
|
||||||
|
# API_BASE_URL=https://api.your-production-domain.com
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const apiBaseUrl = process.env.API_BASE_URL?.trim() || "http://127.0.0.1:8000";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
allowedDevOrigins: ["192.168.0.101"],
|
allowedDevOrigins: ["192.168.0.101"],
|
||||||
reactCompiler: true,
|
reactCompiler: true,
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/api/:path*",
|
||||||
|
destination: `${apiBaseUrl}/api/:path*`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
async redirects() {
|
async redirects() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ShieldCheckIcon, TriangleAlertIcon } from "lucide-react";
|
import { ShieldCheckIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { isAxiosError } from "axios";
|
import { isAxiosError } from "axios";
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { AdminLanguageSwitcher } from "@/components/admin/admin-language-switcher";
|
import { AdminLanguageSwitcher } from "@/components/admin/admin-language-switcher";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
@@ -25,10 +24,6 @@ export function LoginForm() {
|
|||||||
const setBearerToken = useAdminSessionStore((s) => s.setBearerToken);
|
const setBearerToken = useAdminSessionStore((s) => s.setBearerToken);
|
||||||
const setAdminProfile = useAdminSessionStore((s) => s.setAdminProfile);
|
const setAdminProfile = useAdminSessionStore((s) => s.setAdminProfile);
|
||||||
|
|
||||||
const apiConfigured =
|
|
||||||
process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL?.trim() !== "" &&
|
|
||||||
process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL !== undefined;
|
|
||||||
|
|
||||||
const [account, setAccount] = useState("");
|
const [account, setAccount] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [captchaCode, setCaptchaCode] = useState("");
|
const [captchaCode, setCaptchaCode] = useState("");
|
||||||
@@ -39,9 +34,6 @@ export function LoginForm() {
|
|||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
const loadCaptcha = useCallback(async () => {
|
const loadCaptcha = useCallback(async () => {
|
||||||
if (!apiConfigured) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoadingCaptcha(true);
|
setLoadingCaptcha(true);
|
||||||
try {
|
try {
|
||||||
const data = await getAdminCaptcha();
|
const data = await getAdminCaptcha();
|
||||||
@@ -58,7 +50,7 @@ export function LoginForm() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoadingCaptcha(false);
|
setLoadingCaptcha(false);
|
||||||
}
|
}
|
||||||
}, [apiConfigured, t]);
|
}, [t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (readToken()) {
|
if (readToken()) {
|
||||||
@@ -75,11 +67,6 @@ export function LoginForm() {
|
|||||||
|
|
||||||
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!apiConfigured) {
|
|
||||||
toast.error(t("apiBaseMissingToast"));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!captchaKey || !captchaSrc) {
|
if (!captchaKey || !captchaSrc) {
|
||||||
toast.error(t("captchaRequired"));
|
toast.error(t("captchaRequired"));
|
||||||
void loadCaptcha();
|
void loadCaptcha();
|
||||||
@@ -156,19 +143,6 @@ export function LoginForm() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<CardContent className="flex flex-col gap-5 sm:px-8">
|
<CardContent className="flex flex-col gap-5 sm:px-8">
|
||||||
{!apiConfigured ? (
|
|
||||||
<Alert variant="destructive" className="text-left">
|
|
||||||
<TriangleAlertIcon />
|
|
||||||
<AlertTitle>{t("apiMissingTitle")}</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
{t("apiMissingDescriptionPrefix")}{" "}
|
|
||||||
<code className="rounded bg-muted px-1 py-0.5 text-xs">
|
|
||||||
NEXT_PUBLIC_LOTTERY_API_BASE_URL
|
|
||||||
</code>{" "}
|
|
||||||
{t("apiMissingDescriptionSuffix")}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="admin-account" className="text-sm font-medium">
|
<Label htmlFor="admin-account" className="text-sm font-medium">
|
||||||
{t("account")}
|
{t("account")}
|
||||||
@@ -224,7 +198,7 @@ export function LoginForm() {
|
|||||||
className="flex h-11 min-w-[156px] shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-xl border border-border/80 bg-muted/35 px-2 shadow-[inset_0_1px_2px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.03] transition-[box-shadow,transform] hover:bg-muted/45 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:opacity-50 dark:bg-muted/25 dark:shadow-[inset_0_1px_2px_rgba(0,0,0,0.2)] dark:ring-white/[0.06] dark:hover:bg-muted/35"
|
className="flex h-11 min-w-[156px] shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-xl border border-border/80 bg-muted/35 px-2 shadow-[inset_0_1px_2px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.03] transition-[box-shadow,transform] hover:bg-muted/45 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:opacity-50 dark:bg-muted/25 dark:shadow-[inset_0_1px_2px_rgba(0,0,0,0.2)] dark:ring-white/[0.06] dark:hover:bg-muted/35"
|
||||||
onClick={() => void loadCaptcha()}
|
onClick={() => void loadCaptcha()}
|
||||||
disabled={
|
disabled={
|
||||||
loadingCaptcha || !apiConfigured || submitting
|
loadingCaptcha || submitting
|
||||||
}
|
}
|
||||||
aria-label={loadingCaptcha ? t("captchaLoading") : t("captchaRefresh")}
|
aria-label={loadingCaptcha ? t("captchaLoading") : t("captchaRefresh")}
|
||||||
>
|
>
|
||||||
@@ -251,7 +225,7 @@ export function LoginForm() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
className="h-11 w-full text-base font-medium shadow-sm"
|
className="h-11 w-full text-base font-medium shadow-sm"
|
||||||
disabled={submitting || !apiConfigured}
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
{submitting ? t("submitting") : t("submit")}
|
{submitting ? t("submitting") : t("submit")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -9,15 +9,13 @@ import { withAdminLocaleHeaders } from "@/lib/admin-locale";
|
|||||||
import { LotteryApiBizError, LotteryApiEnvelopeError } from "@/types/api/errors";
|
import { LotteryApiBizError, LotteryApiEnvelopeError } from "@/types/api/errors";
|
||||||
import { isApiEnvelope } from "@/types/api/envelope";
|
import { isApiEnvelope } from "@/types/api/envelope";
|
||||||
|
|
||||||
const baseURL = process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL?.trim();
|
|
||||||
|
|
||||||
/** 是否已配置后台 API 根地址(客户端/服务端均可用 `NEXT_PUBLIC_*`) */
|
|
||||||
export function hasLotteryAdminApiBaseUrl(): boolean {
|
export function hasLotteryAdminApiBaseUrl(): boolean {
|
||||||
return baseURL !== undefined && baseURL !== "";
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const adminHttp = axios.create({
|
export const adminHttp = axios.create({
|
||||||
baseURL: baseURL && baseURL !== "" ? baseURL : undefined,
|
// 统一走 Next 同源 /api 代理,由 next.config.ts 的 API_BASE_URL 转发到后端。
|
||||||
|
baseURL: "/api",
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
headers: { Accept: "application/json" },
|
headers: { Accept: "application/json" },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user