Add WeChat auth entry for H5

This commit is contained in:
2026-07-05 19:48:32 +08:00
parent a4b59add07
commit fa2d82333c
4 changed files with 100 additions and 5 deletions

View File

@@ -16,3 +16,8 @@ SCHEDULE_CRON=0 1 * * *
SCHEDULE_TIMEZONE=Asia/Shanghai SCHEDULE_TIMEZONE=Asia/Shanghai
MOCK_MODE=true MOCK_MODE=true
# H5 WeChat OAuth. Leave empty to use the frontend demo authorization flow.
NEXT_PUBLIC_WECHAT_APP_ID=
NEXT_PUBLIC_WECHAT_AUTH_REDIRECT_URI=
NEXT_PUBLIC_WECHAT_AUTH_SCOPE=snsapi_userinfo

View File

@@ -128,6 +128,20 @@ export default function H5QAExperience() {
.finally(() => setLoadingQa(false)); .finally(() => setLoadingQa(false));
}, []); }, []);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
const state = params.get("state");
if (code && state === "hyqa_h5") {
setUser({
mode: "wechat",
name: "微信已授权",
phone: "等待后端换取登录态"
});
window.history.replaceState(null, "", window.location.pathname);
}
}, []);
useEffect(() => { useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
}, [messages, activeTab]); }, [messages, activeTab]);
@@ -141,6 +155,15 @@ export default function H5QAExperience() {
setAuthOpen(false); setAuthOpen(false);
} }
function mockWechatLogin() {
setUser({
mode: "wechat",
name: "微信用户",
phone: "微信授权演示"
});
setAuthOpen(false);
}
function persistMessages(nextMessages: ChatMessage[]) { function persistMessages(nextMessages: ChatMessage[]) {
setMessages(nextMessages); setMessages(nextMessages);
setSessions((current) => { setSessions((current) => {
@@ -413,6 +436,7 @@ export default function H5QAExperience() {
user={user} user={user}
onClose={() => setAuthOpen(false)} onClose={() => setAuthOpen(false)}
onMockLogin={mockLogin} onMockLogin={mockLogin}
onWechatMockLogin={mockWechatLogin}
onLogout={() => { onLogout={() => {
setUser(null); setUser(null);
setAuthOpen(false); setAuthOpen(false);

View File

@@ -1,11 +1,12 @@
"use client"; "use client";
import { LogIn, ShieldCheck, UserPlus, X } from "lucide-react"; import { LogIn, MessageCircle, ShieldCheck, UserPlus, X } from "lucide-react";
import { buildWechatOAuthUrl, getWechatAuthConfig, isWechatBrowser } from "@/lib/wechatAuth";
export type MockUser = { export type MockUser = {
name: string; name: string;
phone: string; phone: string;
mode: "login" | "register"; mode: "login" | "register" | "wechat";
}; };
export default function MockAuthPanel({ export default function MockAuthPanel({
@@ -13,23 +14,36 @@ export default function MockAuthPanel({
user, user,
onClose, onClose,
onMockLogin, onMockLogin,
onWechatMockLogin,
onLogout onLogout
}: { }: {
open: boolean; open: boolean;
user: MockUser | null; user: MockUser | null;
onClose: () => void; onClose: () => void;
onMockLogin: (mode: MockUser["mode"]) => void; onMockLogin: (mode: MockUser["mode"]) => void;
onWechatMockLogin: () => void;
onLogout: () => void; onLogout: () => void;
}) { }) {
if (!open) return null; if (!open) return null;
const wechatConfig = getWechatAuthConfig();
const inWechat = typeof navigator !== "undefined" && isWechatBrowser(navigator.userAgent);
function startWechatAuth() {
if (wechatConfig) {
window.location.href = buildWechatOAuthUrl(wechatConfig);
return;
}
onWechatMockLogin();
}
return ( return (
<div className="absolute inset-0 z-30 flex items-end bg-slate-950/35 px-3 pb-3"> <div className="absolute inset-0 z-30 flex items-end bg-slate-950/35 px-3 pb-3">
<section className="w-full rounded-t-[18px] rounded-b-md bg-white p-4 shadow-2xl"> <section className="w-full rounded-t-[18px] rounded-b-md bg-white p-4 shadow-2xl">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div> <div>
<h2 className="text-lg font-semibold text-ink"></h2> <h2 className="text-lg font-semibold text-ink"></h2>
<p className="mt-1 text-sm text-slate-500"> mock </p> <p className="mt-1 text-sm text-slate-500">使</p>
</div> </div>
<button onClick={onClose} className="flex h-9 w-9 items-center justify-center rounded-md hover:bg-slate-100" aria-label="关闭"> <button onClick={onClose} className="flex h-9 w-9 items-center justify-center rounded-md hover:bg-slate-100" aria-label="关闭">
<X className="h-5 w-5" aria-hidden /> <X className="h-5 w-5" aria-hidden />
@@ -54,8 +68,15 @@ export default function MockAuthPanel({
) : ( ) : (
<div className="grid gap-3"> <div className="grid gap-3">
<button <button
onClick={() => onMockLogin("login")} onClick={startWechatAuth}
className="flex h-11 items-center justify-center gap-2 rounded-md bg-jade text-sm font-medium text-white hover:bg-teal-800" className="flex h-11 items-center justify-center gap-2 rounded-md bg-jade text-sm font-medium text-white hover:bg-teal-800"
>
<MessageCircle className="h-4 w-4" aria-hidden />
{wechatConfig ? "微信授权登录" : "微信授权演示"}
</button>
<button
onClick={() => onMockLogin("login")}
className="flex h-11 items-center justify-center gap-2 rounded-md border border-line text-sm font-medium text-ink hover:bg-slate-50"
> >
<LogIn className="h-4 w-4" aria-hidden /> <LogIn className="h-4 w-4" aria-hidden />
mock mock
@@ -71,7 +92,9 @@ export default function MockAuthPanel({
)} )}
<div className="mt-4 rounded-md bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-800"> <div className="mt-4 rounded-md bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-800">
{wechatConfig
? `已配置微信 OAuth${inWechat ? ",当前在微信内置浏览器中。" : "。如果不在微信内打开,授权页可能无法完成。"}`
: "尚未配置 NEXT_PUBLIC_WECHAT_APP_ID当前按钮只模拟微信授权不写入后端。"}
</div> </div>
</section> </section>
</div> </div>

View File

@@ -0,0 +1,43 @@
const WECHAT_OAUTH_URL = "https://open.weixin.qq.com/connect/oauth2/authorize";
export type WechatAuthConfig = {
appId: string;
redirectUri: string;
scope: "snsapi_base" | "snsapi_userinfo";
state: string;
};
export function getWechatAuthConfig(): WechatAuthConfig | null {
const appId = process.env.NEXT_PUBLIC_WECHAT_APP_ID;
const redirectUri =
process.env.NEXT_PUBLIC_WECHAT_AUTH_REDIRECT_URI ||
(typeof window !== "undefined" ? `${window.location.origin}/h5` : "");
const scope = (process.env.NEXT_PUBLIC_WECHAT_AUTH_SCOPE || "snsapi_userinfo") as WechatAuthConfig["scope"];
if (!appId || !redirectUri) {
return null;
}
return {
appId,
redirectUri,
scope,
state: "hyqa_h5"
};
}
export function buildWechatOAuthUrl(config: WechatAuthConfig): string {
const params = new URLSearchParams({
appid: config.appId,
redirect_uri: config.redirectUri,
response_type: "code",
scope: config.scope,
state: config.state
});
return `${WECHAT_OAUTH_URL}?${params.toString()}#wechat_redirect`;
}
export function isWechatBrowser(userAgent: string): boolean {
return /MicroMessenger/i.test(userAgent);
}