From 9d6498bf197d710b79ba7ce5c34718ba151bda6f Mon Sep 17 00:00:00 2001 From: selfrelease Date: Thu, 25 Jun 2026 20:58:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=99=BB=E5=BD=95=E9=A1=B5=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20SSR=20=E9=A2=84=E5=8F=96=E6=9C=BA=E6=9E=84=E5=88=97?= =?UTF-8?q?=E8=A1=A8=EF=BC=8C=E6=B6=88=E9=99=A4=E9=A6=96=E5=B1=8F=E9=97=AA?= =?UTF-8?q?=E7=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **问题** - 登录页机构下拉在客户端 useEffect 中拉取,首屏会闪现"正在加载机构列表" - 浏览器端多一跳 fetch 请求,体感延迟明显 **改动** - page.tsx → 服务端组件(async),SSR 时内网直连 /api/v1/organizations - login-form.tsx → 客户端组件,接收 initialOrgs,保留表单交互 - useEffect 降级为仅当 SSR 为空时的客户端兜底 - force-dynamic + cache: no-store 保证机构变化即时生效 **验证** - 本地 next start:首屏 HTML 直接含机构名,无占位符 - 构建路由从 ○ Static 变为 ƒ Dynamic --- apps/web/src/app/(auth)/login/login-form.tsx | 225 +++++++++++++++++ apps/web/src/app/(auth)/login/page.tsx | 241 ++----------------- 2 files changed, 248 insertions(+), 218 deletions(-) create mode 100644 apps/web/src/app/(auth)/login/login-form.tsx diff --git a/apps/web/src/app/(auth)/login/login-form.tsx b/apps/web/src/app/(auth)/login/login-form.tsx new file mode 100644 index 0000000..55c7810 --- /dev/null +++ b/apps/web/src/app/(auth)/login/login-form.tsx @@ -0,0 +1,225 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { useAuthStore } from "@/stores/auth"; +import type { Organization } from "@/stores/auth"; +import api from "@/lib/api"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Shield, + Building2, + Sparkles, + BookOpen, + FileText, + Brain, + GraduationCap, +} from "lucide-react"; + +export default function LoginForm({ initialOrgs }: { initialOrgs: Organization[] }) { + const [email, setEmail] = useState("fazhiribao@govai.gov.cn"); + const [password, setPassword] = useState("admin123"); + const [loading, setLoading] = useState(false); + const [errorMsg, setErrorMsg] = useState(""); + const [orgs, setOrgs] = useState(initialOrgs); + const [selectedOrg, setSelectedOrg] = useState(initialOrgs[0]?.id ?? ""); + const { login, switchOrg } = useAuthStore(); + const router = useRouter(); + + // SSR 已注入机构时直接使用,无需再请求; + // 仅当服务端取数失败(首屏机构为空)时,客户端兜底拉取一次 + useEffect(() => { + if (initialOrgs.length > 0) return; + api + .get("/api/v1/organizations") + .then((data) => { + setOrgs(data); + if (data.length > 0) setSelectedOrg(data[0].id); + }) + .catch(() => {}); + }, [initialOrgs.length]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!email || !password) { + setErrorMsg("请输入邮箱和密码"); + return; + } + if (!selectedOrg) { + setErrorMsg("请选择所属机构"); + return; + } + + setLoading(true); + setErrorMsg(""); + try { + await login(email, password, selectedOrg); + const user = useAuthStore.getState().user; + // 平台管理员不绑定机构,登录后保留 super_admin 身份; + // 仅机构管理员在所选机构与自身归属不一致时才触发切换 + if (user && user.role === "admin" && user.org_id !== selectedOrg) { + await switchOrg(selectedOrg); + } + router.push(user?.role === "super_admin" ? "/platform/overview" : "/store"); + } catch (err) { + setErrorMsg(err instanceof Error ? err.message : "登录失败,请检查账号和密码"); + } finally { + setLoading(false); + } + }; + + const features = [ + { icon: Sparkles, text: "AI 驱动的智能办公" }, + { icon: FileText, text: "一键生成公文与报告" }, + { icon: BookOpen, text: "智能知识库问答" }, + { icon: Brain, text: "多场景 AI 应用中心" }, + { icon: GraduationCap, text: "支持多机构独立部署" }, + ]; + + return ( +
+
+ {/* 左侧品牌区 - 桌面端显示 */} +
+
+
+
+ +
+
+

AI 智能应用平台

+

提升效能 · 赋能智慧办公

+
+
+ +

+ 面向政务与高校场景的一站式 AI + 应用平台,集成文档生成、智能问答、数据分析等核心能力,助力组织数智化转型。 +

+ +
+ {features.map(({ icon: Icon, text }) => ( +
+
+ +
+ {text} +
+ ))} +
+
+
+ + {/* 右侧登录区 */} +
+
+ {/* 移动端标题 - 仅在小屏显示 */} +
+ +

AI 智能应用平台

+

提升效能 · 赋能智慧办公

+
+ + {/* 表单区域 */} +
+

+ 欢迎登录 +

+

请选择机构并输入账号密码

+ +
+ {errorMsg && ( +
+ {errorMsg} +
+ )} + +
+ + {orgs.length > 0 ? ( + + ) : ( + + )} +
+ +
+ + setEmail(e.target.value)} + autoComplete="email" + required + className="h-11 rounded-xl border-gray-200 bg-gray-50/50 px-4 focus:ring-2 focus:ring-blue-500/40 focus:border-blue-400 hover:border-gray-300" + /> +
+ +
+ + setPassword(e.target.value)} + autoComplete="current-password" + required + className="h-11 rounded-xl border-gray-200 bg-gray-50/50 px-4 focus:ring-2 focus:ring-blue-500/40 focus:border-blue-400 hover:border-gray-300" + /> +
+ + +
+ +
+ 还没有账号?{" "} + + 申请注册 + +
+ +
+ 本系统仅限授权人员使用 · 数据安全等级:机构内部 +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx index 304b063..b77ca0e 100644 --- a/apps/web/src/app/(auth)/login/page.tsx +++ b/apps/web/src/app/(auth)/login/page.tsx @@ -1,222 +1,27 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { useRouter } from "next/navigation"; -import Link from "next/link"; -import { useAuthStore } from "@/stores/auth"; import type { Organization } from "@/stores/auth"; -import api from "@/lib/api"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Shield, - Building2, - Sparkles, - BookOpen, - FileText, - Brain, - GraduationCap, -} from "lucide-react"; +import LoginForm from "./login-form"; -export default function LoginPage() { - const [email, setEmail] = useState("fazhiribao@govai.gov.cn"); - const [password, setPassword] = useState("admin123"); - const [loading, setLoading] = useState(false); - const [errorMsg, setErrorMsg] = useState(""); - const [orgs, setOrgs] = useState([]); - const [selectedOrg, setSelectedOrg] = useState(""); - const { login, switchOrg } = useAuthStore(); - const router = useRouter(); +// 登录页改为服务端渲染:SSR 阶段在 web 服务端内网直连 API 取机构列表, +// 首屏 HTML 即带机构数据,消除客户端"正在加载机构列表"的闪烁。 +// no-store 保证机构增删改即时生效,无需重新构建或重启。 +export const dynamic = "force-dynamic"; - useEffect(() => { - api - .get("/api/v1/organizations") - .then((data) => { - setOrgs(data); - if (data.length > 0) setSelectedOrg(data[0].id); - }) - .catch(() => {}); - }, []); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!email || !password) { - setErrorMsg("请输入邮箱和密码"); - return; - } - if (!selectedOrg) { - setErrorMsg("请选择所属机构"); - return; - } - - setLoading(true); - setErrorMsg(""); - try { - await login(email, password, selectedOrg); - const user = useAuthStore.getState().user; - // 平台管理员不绑定机构,登录后保留 super_admin 身份; - // 仅机构管理员在所选机构与自身归属不一致时才触发切换 - if (user && user.role === "admin" && user.org_id !== selectedOrg) { - await switchOrg(selectedOrg); - } - router.push(user?.role === "super_admin" ? "/platform/overview" : "/store"); - } catch (err) { - setErrorMsg(err instanceof Error ? err.message : "登录失败,请检查账号和密码"); - } finally { - setLoading(false); - } - }; - - const features = [ - { icon: Sparkles, text: "AI 驱动的智能办公" }, - { icon: FileText, text: "一键生成公文与报告" }, - { icon: BookOpen, text: "智能知识库问答" }, - { icon: Brain, text: "多场景 AI 应用中心" }, - { icon: GraduationCap, text: "支持多机构独立部署" }, - ]; - - return ( -
-
- {/* 左侧品牌区 - 桌面端显示 */} -
-
-
-
- -
-
-

AI 智能应用平台

-

提升效能 · 赋能智慧办公

-
-
- -

- 面向政务与高校场景的一站式 AI - 应用平台,集成文档生成、智能问答、数据分析等核心能力,助力组织数智化转型。 -

- -
- {features.map(({ icon: Icon, text }) => ( -
-
- -
- {text} -
- ))} -
-
-
- - {/* 右侧登录区 */} -
-
- {/* 移动端标题 - 仅在小屏显示 */} -
- -

AI 智能应用平台

-

提升效能 · 赋能智慧办公

-
- - {/* 表单区域 */} -
-

- 欢迎登录 -

-

请选择机构并输入账号密码

- -
- {errorMsg && ( -
- {errorMsg} -
- )} - -
- - {orgs.length > 0 ? ( - - ) : ( - - )} -
- -
- - setEmail(e.target.value)} - autoComplete="email" - required - className="h-11 rounded-xl border-gray-200 bg-gray-50/50 px-4 focus:ring-2 focus:ring-blue-500/40 focus:border-blue-400 hover:border-gray-300" - /> -
- -
- - setPassword(e.target.value)} - autoComplete="current-password" - required - className="h-11 rounded-xl border-gray-200 bg-gray-50/50 px-4 focus:ring-2 focus:ring-blue-500/40 focus:border-blue-400 hover:border-gray-300" - /> -
- - -
- -
- 还没有账号?{" "} - - 申请注册 - -
- -
- 本系统仅限授权人员使用 · 数据安全等级:机构内部 -
-
-
-
-
-
- ); +async function getOrganizations(): Promise { + const base = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080"; + try { + const res = await fetch(`${base}/api/v1/organizations`, { + cache: "no-store", + headers: { "Content-Type": "application/json" }, + }); + if (!res.ok) return []; + const json = (await res.json()) as { code: number; data: Organization[] }; + return json.code === 0 && Array.isArray(json.data) ? json.data : []; + } catch { + return []; + } } + +export default async function LoginPage() { + const orgs = await getOrganizations(); + return ; +} \ No newline at end of file