ith5cn 1 сар өмнө
parent
commit
416905a800

+ 21 - 0
public/demo-game.html

@@ -184,6 +184,27 @@
         }
       });
 
+      // 监听来自 SDK 的消息(使用封装好的 onLogout 方法)
+      GameSDK.onLogout(() => {
+        const btn = document.getElementById('btn-login');
+        const status = document.getElementById('login-status');
+        const resLog = document.getElementById('login-res');
+        
+        // 重置登录按钮和状态
+        btn.disabled = false;
+        btn.innerHTML = '调用 SDK 登录';
+        btn.className = 'w-full py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-xl font-medium transition-colors shadow-sm shadow-blue-200 active:scale-95 duration-150';
+        
+        status.textContent = '未登录';
+        status.className = 'text-xs px-2 py-1 rounded-full bg-slate-100 text-slate-500';
+        
+        // 隐藏/清除日志
+        resLog.classList.add('hidden');
+        resLog.textContent = '';
+        
+        console.log('游戏收到通知 (通过 GameSDK.onLogout):退出登录成功,已重置 UI');
+      });
+
     });
   </script>
 </body>

+ 19 - 0
public/game-sdk.js

@@ -4,6 +4,7 @@
  */
 (function() {
   const pendingRequests = new Map();
+  let logoutCallback = null;
 
   // 生成唯一 ID
   function generateId() {
@@ -14,6 +15,16 @@
   window.addEventListener('message', (event) => {
     try {
       const message = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
+      
+      // 1. 处理广播消息(无 requestId)
+      if (message && !message.requestId) {
+        if (message.type === 'LOGOUT SUCCESS' && logoutCallback) {
+          logoutCallback();
+        }
+        return;
+      }
+
+      // 2. 处理带 requestId 的请求结果
       if (message && message.requestId) {
         console.log('[SDK] Received message from parent:', message);
         const request = pendingRequests.get(message.requestId);
@@ -49,6 +60,14 @@
 
   // 暴露全局对象 GameSDK
   window.GameSDK = {
+    /**
+     * 注册退出登录回调
+     * @param {Function} callback 
+     */
+    onLogout: function(callback) {
+      logoutCallback = callback;
+    },
+
     /**
      * 调用登录
      * @returns {Promise<{token: string, userId: string, username: string}>}

+ 19 - 5
src/App.tsx

@@ -1,7 +1,7 @@
 import { useEffect, useRef, useState } from 'react';
 import { eventBus } from './lib/EventBus';
 import { pmBridge } from './lib/PostMessageBridge';
-import { useSetAtom } from 'jotai'
+import { useSetAtom, useAtomValue } from 'jotai'
 import { openModalAction } from './store';
 import { LoginIndex } from './components/login';
 import { RealNameIndex } from './components/real-name';
@@ -11,13 +11,17 @@ import { getH5GameLinkApi } from './api';
 import { getURLparams } from './utils';
 import { init } from './components/init';
 import { nativeInit } from './components/init/native-init';
-import SlideBar from './components/slide-bar';
+import SlideBarIndex from './components/slide-bar';
+import { FloatingButton } from './components/FloatingButton';
+import { userStateAtom } from './store/user-atom';
+
 function App() {
   const iframeRef = useRef<HTMLIFrameElement>(null);
-  const [iframeUrl, setIframeUrl] = useState<string>('');
+  const [iframeUrl, setIframeUrl] = useState<string>('http://localhost:5173/demo-game.html');
+  const [isSlideBarOpen, setIsSlideBarOpen] = useState(false);
 
   const openModal = useSetAtom(openModalAction);
-  // const modalState = useAtomValue(modalStackAtom);
+  const userState = useAtomValue(userStateAtom);
  
   const getGameUrl = async () => {
     const res = await getH5GameLinkApi({game_id: getURLparams('game_id') || ''})
@@ -79,6 +83,7 @@ function App() {
   useEffect(() => {
     getGameUrl()
   }, []);
+
   return (
     <div className="w-full h-screen relative bg-white font-sans overflow-hidden">
       
@@ -95,7 +100,16 @@ function App() {
       <LoginIndex />
       <RealNameIndex />
       <PaymentIndex />
-      <SlideBar />
+      
+      {/* 登录后显示浮标球 */}
+      {userState.token && (
+        <FloatingButton onClick={() => setIsSlideBarOpen(true)} />
+      )}
+
+      {/* 侧边栏 */}
+      {isSlideBarOpen && (
+        <SlideBarIndex onClose={() => setIsSlideBarOpen(false)} />
+      )}
      
 
     </div>

+ 31 - 0
src/api/login.ts

@@ -64,4 +64,35 @@ export const forgetPasswordUpdatePasswordApi = (data: {
   mobile:string
 })=>{
   return request.post('/sdk/auth/update_pwd_by_code', data)
+}
+
+// 修改密码
+export const updatePasswordApi = (data: {
+  user_name: string,
+  user_pwd: string,
+  new_user_pwd: string
+}) => {
+  return request.post('/sdk/user/update_pwd', data)
+}
+
+// 发送绑定手机验证码
+export const sendBindMobileCodeApi = (data: {
+  user_name: string,
+  mobile: string
+}) => {
+  return request.post('/sdk/user/send_bind_mobile_code', data)
+}
+
+// 绑定手机
+export const bindMobileApi = (data: {
+  user_name: string,
+  mobile: string,
+  code: string
+}) => {
+  return request.post('/sdk/user/bind_mobile', data)
+}
+
+// 退出登录
+export const logoutApi = () => {
+  return request.post('/sdk/auth/logout')
 }

+ 9 - 4
src/components/FloatingButton.tsx

@@ -2,13 +2,17 @@ import { useState, useRef } from 'react';
 import Draggable, { type DraggableEvent, type DraggableData } from 'react-draggable';
 import { Gamepad2 } from 'lucide-react';
 
-export function FloatingButton() {
+interface FloatingButtonProps {
+  onClick: () => void;
+}
+
+export function FloatingButton({ onClick }: FloatingButtonProps) {
   const [position, setPosition] = useState({ x: 20, y: 100 });
   const [isDragging, setIsDragging] = useState(false);
   const [isIdle, setIsIdle] = useState(false);
   const nodeRef = useRef<HTMLDivElement>(null);
   
-  const idleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+  const idleTimerRef = useRef<any>(null);
 
   // 清除空闲倒计时
   const clearIdleTimer = () => {
@@ -71,8 +75,9 @@ export function FloatingButton() {
   };
 
   const handleClick = () => {
-    // 待办:展开菜单等操作
-    console.log('Floating button clicked');
+    if (!isDragging) {
+      onClick();
+    }
   };
 
   return (

+ 39 - 0
src/components/agreement/index.tsx

@@ -0,0 +1,39 @@
+import { ChevronLeft } from "lucide-react";
+
+export interface AgreementProps {
+    title: string;
+    url: string;
+    onBack: () => void;
+}
+
+export const Agreement = ({ title, url, onBack }: AgreementProps) => {
+    // 补全基础路径,如果是相对路径的话
+    const fullUrl = url.startsWith('http') ? url : `${import.meta.env.VITE_APP_BASE_URL}${url}`;
+
+    return (
+        <div className="fixed inset-0 z-[2000] flex items-center justify-center bg-black/60 backdrop-blur-sm px-4">
+            <div className="absolute inset-0" onClick={onBack}></div>
+            <div className="relative z-20 w-full max-w-[360px] h-[80vh] max-h-[640px] flex flex-col bg-background-light dark:bg-background-dark rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 fade-in duration-200">
+                {/* Header */}
+                <div className="flex items-center border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 px-4 py-3 shadow-sm">
+                    <button 
+                        onClick={onBack}
+                        className="p-1 -ml-1 rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-400 transition-colors"
+                    >
+                        <ChevronLeft size="24" />
+                    </button>
+                    <h1 className="flex-1 text-center font-bold text-slate-900 dark:text-slate-100 pr-8">{title}</h1>
+                </div>
+
+                {/* Content Container */}
+                <div className="flex-1 relative bg-white">
+                    <iframe 
+                        src={fullUrl} 
+                        className="absolute inset-0 w-full h-full border-none"
+                        title={title}
+                    />
+                </div>
+            </div>
+        </div>
+    );
+};

+ 12 - 1
src/components/login/account-login.tsx

@@ -4,6 +4,7 @@ import { ForgetPasswordIndex } from "../forget-password";
 import { useState } from "react";
 import { loginAccountApi } from "../../api/login"
 import { pmBridge } from "../../lib/PostMessageBridge"
+import { Agreement } from "../agreement";
 
 
 export interface AccountLoginProps {
@@ -18,6 +19,7 @@ export const AccountLogin = ({ switchPhoneLogin, switchRegister }: AccountLoginP
     const [userPwd, setUserPwd] = useState('')
     const [loading, setLoading] = useState(false)
     const [isShowForgetPassword, setIsshowPassword] = useState(false)
+    const [showAgreement, setShowAgreement] = useState<{ title: string; url: string } | null>(null);
 
     const handleLogin = async () => {
         if (!userName) {
@@ -129,7 +131,7 @@ export const AccountLogin = ({ switchPhoneLogin, switchRegister }: AccountLoginP
                     {/* Footer Privacy */}
                     <div className="px-6 pb-6 text-center">
                         <p className="text-[10px] text-slate-400 leading-relaxed">
-                            登录即代表您已同意 <a className="text-primary hover:underline" href="#">服务协议</a> 和 <a className="text-primary hover:underline" href="#">隐私政策</a>
+                            登录即代表您已同意 <button className="text-primary hover:underline" onClick={() => setShowAgreement({ title: '用户协议', url: '/static/user.html' })}>用户协议</button> 和 <button className="text-primary hover:underline" onClick={() => setShowAgreement({ title: '隐私政策', url: '/static/ys.html' })}>隐私政策</button>
                         </p>
                     </div>
                 </div>
@@ -137,6 +139,15 @@ export const AccountLogin = ({ switchPhoneLogin, switchRegister }: AccountLoginP
             {
                 isShowForgetPassword ? <ForgetPasswordIndex close={() => { setIsshowPassword(false) }} /> : <></>
             }
+            {
+                showAgreement && (
+                    <Agreement 
+                        title={showAgreement.title} 
+                        url={showAgreement.url} 
+                        onBack={() => setShowAgreement(null)} 
+                    />
+                )
+            }
 
         </>
 

+ 10 - 1
src/components/login/phone-login.tsx

@@ -1,6 +1,7 @@
 import { useState, useEffect } from 'react';
 import { loginSendCodeApi, getAccountListByCodeApi } from '../../api/login';
 import { SelectAccount, type Account } from './select-account';
+import { Agreement } from '../agreement';
 
 export interface phoneLoginProps {
     switchAccountLogin: () => void;
@@ -14,6 +15,7 @@ export const PhoneLogin = ({ switchAccountLogin, switchRegister }: phoneLoginPro
     const [loading, setLoading] = useState(false);
     const [accountList, setAccountList] = useState<Account[]>([]);
     const [showSelectAccount, setShowSelectAccount] = useState(false);
+    const [showAgreement, setShowAgreement] = useState<{ title: string; url: string } | null>(null);
 
     useEffect(() => {
         let timer: ReturnType<typeof setInterval>;
@@ -165,7 +167,7 @@ export const PhoneLogin = ({ switchAccountLogin, switchRegister }: phoneLoginPro
                 {/* Footer Privacy */}
                 <div className="px-6 pb-6 text-center">
                     <p className="text-[10px] text-slate-400 leading-relaxed">
-                        登录即代表您已同意 <a className="text-primary hover:underline" href="#">服务协议</a> 和 <a className="text-primary hover:underline" href="#">隐私政策</a>
+                        登录即代表您已同意 <button className="text-primary hover:underline" onClick={() => setShowAgreement({ title: '用户协议', url: '/static/user.html' })}>用户协议</button> 和 <button className="text-primary hover:underline" onClick={() => setShowAgreement({ title: '隐私政策', url: '/static/ys.html' })}>隐私政策</button>
                     </p>
                 </div>
             </div>
@@ -179,6 +181,13 @@ export const PhoneLogin = ({ switchAccountLogin, switchRegister }: phoneLoginPro
                 onClose={() => setShowSelectAccount(false)} 
             />
         )}
+        {showAgreement && (
+            <Agreement 
+                title={showAgreement.title} 
+                url={showAgreement.url} 
+                onBack={() => setShowAgreement(null)} 
+            />
+        )}
         </>
     );
 };

+ 143 - 0
src/components/slide-bar/bind-phone/index.tsx

@@ -0,0 +1,143 @@
+import { ChevronLeft } from "lucide-react";
+import { useState, useEffect, useRef } from "react";
+import { sendBindMobileCodeApi, bindMobileApi } from "../../../api/login";
+import { useAtomValue } from "jotai";
+import { userStateAtom } from "../../../store/user-atom";
+
+export interface BindPhoneProps {
+    onBack: () => void;
+}
+
+export const BindPhone = ({ onBack }: BindPhoneProps) => {
+    const userState = useAtomValue(userStateAtom);
+    const [mobile, setMobile] = useState('');
+    const [code, setCode] = useState('');
+    const [loading, setLoading] = useState(false);
+    const [countdown, setCountdown] = useState(0);
+    const timerRef = useRef<any>(null);
+
+    useEffect(() => {
+        if (countdown > 0) {
+            timerRef.current = setTimeout(() => {
+                setCountdown(countdown - 1);
+            }, 1000);
+        } else {
+            if (timerRef.current) clearTimeout(timerRef.current);
+        }
+        return () => {
+            if (timerRef.current) clearTimeout(timerRef.current);
+        };
+    }, [countdown]);
+
+    const handleSendCode = async () => {
+        if (!mobile) {
+            alert('请输入手机号');
+            return;
+        }
+        if (!/^1[3-9]\d{9}$/.test(mobile)) {
+            alert('请输入正确的手机号格式');
+            return;
+        }
+
+        setLoading(true);
+        try {
+            await sendBindMobileCodeApi({
+                user_name: userState.username,
+                mobile: mobile
+            });
+            alert('验证码已发送');
+            setCountdown(60);
+        } catch (error: any) {
+            alert(error.message || '发送失败');
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    const handleSubmit = async () => {
+        if (!mobile) {
+            alert('请输入手机号');
+            return;
+        }
+        if (!code) {
+            alert('请输入验证码');
+            return;
+        }
+
+        setLoading(true);
+        try {
+            await bindMobileApi({
+                user_name: userState.username,
+                mobile: mobile,
+                code: code
+            });
+            alert('手机号绑定成功');
+            onBack();
+        } catch (error: any) {
+            alert(error.message || '绑定失败');
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    return (
+        <div className="fixed left-0 top-0 flex h-screen overflow-hidden bg-white">
+            <div className="relative z-20 flex h-full w-full">
+                <div className="flex h-full w-80 flex-col bg-background-light dark:bg-background-dark shadow-2xl">
+                    <div className="flex items-center border-b border-slate-200 dark:border-slate-800 py-3 px-2">
+                        <button 
+                            onClick={onBack}
+                            className="text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
+                        >
+                            <ChevronLeft size="24" />
+                        </button>
+                        <h1 className="text-md text-center pl-2 flex-1 pr-8 text-slate-900 dark:text-slate-100">绑定手机</h1>
+                    </div>
+
+                    <div className="flex-1 p-6 space-y-4">
+                        <div className="space-y-2">
+                            <label className="text-sm font-medium text-slate-700 dark:text-slate-300">手机号</label>
+                            <input
+                                type="tel"
+                                placeholder="请输入手机号"
+                                className="w-full rounded-lg border border-slate-200 bg-white p-2.5 text-sm focus:border-primary focus:outline-none dark:border-slate-800 dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder:text-slate-400"
+                                value={mobile}
+                                onChange={(e) => setMobile(e.target.value)}
+                            />
+                        </div>
+
+                        <div className="space-y-2">
+                            <label className="text-sm font-medium text-slate-700 dark:text-slate-300">验证码</label>
+                            <div className="flex gap-2">
+                                <input
+                                    type="text"
+                                    placeholder="请输入验证码"
+                                    className="flex-1 rounded-lg border border-slate-200 bg-white p-2.5 text-sm focus:border-primary focus:outline-none dark:border-slate-800 dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder:text-slate-400"
+                                    value={code}
+                                    onChange={(e) => setCode(e.target.value)}
+                                />
+                                <button
+                                    onClick={handleSendCode}
+                                    disabled={loading || countdown > 0}
+                                    className={`w-28 rounded-lg bg-slate-100 text-xs font-medium text-slate-600 transition-all hover:bg-slate-200 disabled:opacity-50 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700`}
+                                >
+                                    {countdown > 0 ? `${countdown}s` : '获取验证码'}
+                                </button>
+                            </div>
+                        </div>
+
+                        <button
+                            onClick={handleSubmit}
+                            disabled={loading}
+                            className={`mt-6 w-full rounded-lg bg-primary py-3 font-bold text-white shadow-lg transition-all hover:bg-primary/90 ${
+                                loading ? 'opacity-70 cursor-not-allowed' : ''
+                            }`}
+                        >
+                            {loading ? '提交中...' : '提交绑定'}
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    );
+};

+ 144 - 0
src/components/slide-bar/change-password/index.tsx

@@ -0,0 +1,144 @@
+import { ChevronLeft, Eye, EyeOff } from "lucide-react";
+import { useState } from "react";
+import { updatePasswordApi } from "../../../api/login";
+import { useAtomValue } from "jotai";
+import { userStateAtom } from "../../../store/user-atom";
+
+export interface ChangePasswordProps {
+    onBack: () => void;
+}
+
+export const ChangePassword = ({ onBack }: ChangePasswordProps) => {
+    const userState = useAtomValue(userStateAtom);
+    const [oldPassword, setOldPassword] = useState('');
+    const [newPassword, setNewPassword] = useState('');
+    const [confirmPassword, setConfirmPassword] = useState('');
+    const [showOldPassword, setShowOldPassword] = useState(false);
+    const [showNewPassword, setShowNewPassword] = useState(false);
+    const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+    const [loading, setLoading] = useState(false);
+
+    const handleSubmit = async () => {
+        if (!oldPassword) {
+            alert('请输入旧密码');
+            return;
+        }
+        if (!newPassword) {
+            alert('请输入新密码');
+            return;
+        }
+        if (newPassword !== confirmPassword) {
+            alert('两次输入的新密码不一致');
+            return;
+        }
+        if (newPassword.length < 6) {
+            alert('新密码长度不能少于6位');
+            return;
+        }
+
+        setLoading(true);
+        try {
+            await updatePasswordApi({
+                user_name: userState.username,
+                user_pwd: oldPassword,
+                new_user_pwd: newPassword
+            });
+            alert('密码修改成功');
+            onBack();
+        } catch (error: any) {
+            alert(error.message || '修改失败');
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    return (
+        <div className="fixed left-0 top-0 flex h-screen overflow-hidden bg-white">
+            <div className="relative z-20 flex h-full w-full">
+                <div className="flex h-full w-80 flex-col bg-background-light dark:bg-background-dark shadow-2xl">
+                    <div className="flex items-center border-b border-slate-200 dark:border-slate-800 py-3 px-2">
+                        <button 
+                            onClick={onBack}
+                            className="text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
+                        >
+                            <ChevronLeft size="24" />
+                        </button>
+                        <h1 className="text-md text-center pl-2 flex-1 pr-8 text-slate-900 dark:text-slate-100">修改密码</h1>
+                    </div>
+
+                    <div className="flex-1 p-6 space-y-4">
+                        <div className="space-y-2">
+                            <label className="text-sm font-medium text-slate-700 dark:text-slate-300">旧密码</label>
+                            <div className="relative">
+                                <input
+                                    type={showOldPassword ? "text" : "password"}
+                                    placeholder="请输入旧密码"
+                                    className="w-full rounded-lg border border-slate-200 bg-white p-2.5 pr-10 text-sm focus:border-primary focus:outline-none dark:border-slate-800 dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder:text-slate-400"
+                                    value={oldPassword}
+                                    onChange={(e) => setOldPassword(e.target.value)}
+                                />
+                                <button
+                                    type="button"
+                                    onClick={() => setShowOldPassword(!showOldPassword)}
+                                    className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"
+                                >
+                                    {showOldPassword ? <Eye size={18} /> : <EyeOff size={18} />}
+                                </button>
+                            </div>
+                        </div>
+
+                        <div className="space-y-2">
+                            <label className="text-sm font-medium text-slate-700 dark:text-slate-300">新密码</label>
+                            <div className="relative">
+                                <input
+                                    type={showNewPassword ? "text" : "password"}
+                                    placeholder="请输入新密码"
+                                    className="w-full rounded-lg border border-slate-200 bg-white p-2.5 pr-10 text-sm focus:border-primary focus:outline-none dark:border-slate-800 dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder:text-slate-400"
+                                    value={newPassword}
+                                    onChange={(e) => setNewPassword(e.target.value)}
+                                />
+                                <button
+                                    type="button"
+                                    onClick={() => setShowNewPassword(!showNewPassword)}
+                                    className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"
+                                >
+                                    {showNewPassword ? <Eye size={18} /> : <EyeOff size={18} />}
+                                </button>
+                            </div>
+                        </div>
+
+                        <div className="space-y-2">
+                            <label className="text-sm font-medium text-slate-700 dark:text-slate-300">确认新密码</label>
+                            <div className="relative">
+                                <input
+                                    type={showConfirmPassword ? "text" : "password"}
+                                    placeholder="请再次输入新密码"
+                                    className="w-full rounded-lg border border-slate-200 bg-white p-2.5 pr-10 text-sm focus:border-primary focus:outline-none dark:border-slate-800 dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder:text-slate-400"
+                                    value={confirmPassword}
+                                    onChange={(e) => setConfirmPassword(e.target.value)}
+                                />
+                                <button
+                                    type="button"
+                                    onClick={() => setShowConfirmPassword(!showConfirmPassword)}
+                                    className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"
+                                >
+                                    {showConfirmPassword ? <Eye size={18} /> : <EyeOff size={18} />}
+                                </button>
+                            </div>
+                        </div>
+
+                        <button
+                            onClick={handleSubmit}
+                            disabled={loading}
+                            className={`mt-6 w-full rounded-lg bg-primary py-3 font-bold text-white shadow-lg transition-all hover:bg-primary/90 ${
+                                loading ? 'opacity-70 cursor-not-allowed' : ''
+                            }`}
+                        >
+                            {loading ? '提交中...' : '提交修改'}
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    );
+};

+ 41 - 48
src/components/slide-bar/index.tsx

@@ -1,55 +1,48 @@
-import { Bolt, ChevronRight, IdCard, Key, Lock, Smartphone, SquareArrowRightExit } from "lucide-react";
+import { useState } from "react";
+import { ChangePassword } from "./change-password";
+import { BindPhone } from "./bind-phone";
+import { RealName } from "./real-name";
+import { Agreement } from "../agreement";
+import SlideBar from "./slide-bar";
 
-export default function SlideBar() {
-    return (
-        <>
-            <div className="fixed left-0 top-0 flex h-screen w-[80%] overflow-hidden">
-
-                <div className="absolute inset-0 z-10 bg-white backdrop-blur-sm"></div>
+interface SlideBarIndexProps {
+    onClose: () => void;
+}
 
-                <div className="relative z-20 flex h-full w-full">
-                    <div className="flex h-full w-80 flex-col bg-background-light dark:bg-background-dark shadow-2xl">
+export default function SlideBarIndex({ onClose }: SlideBarIndexProps) {
+    const [showChangePassword, setShowChangePassword] = useState(false);
+    const [showBindPhone, setShowBindPhone] = useState(false);
+    const [showRealName, setShowRealName] = useState(false);
+    const [showAgreement, setShowAgreement] = useState<{ title: string; url: string } | null>(null);
+    const [showSlideBar, setShowSlideBar] = useState(true);
 
-                        <div className="p-6 pb-4 border-b border-slate-200 dark:border-slate-800">
-                            <div className="flex">
-                                <div className="flex flex-col">
-                                    <h2 className="text-xl font-bold tracking-tight text-slate-900 dark:text-slate-100">CyberWarrior_99</h2>
-                                    <p className="text-xs font-medium text-primary bg-primary/10 px-2 py-0.5 rounded-full inline-block mt-1">ID: #8842-X01</p>
-                                </div>
-                            </div>
-                        </div>
+    const handleBack = () => {
+        setShowChangePassword(false);
+        setShowBindPhone(false);
+        setShowRealName(false);
+        setShowAgreement(null);
+        setShowSlideBar(true);
+    };
 
-                        <div className="flex-1 overflow-y-auto py-4">
-                            <nav className="space-y-2">
-                                <p className="px-3 pb-2 text-xs font-semibold uppercase tracking-wider text-slate-400">账户安全</p>
-                                <button className="border-b border-slate-200 dark:border-slate-800 flex w-full items-center gap-4  px-3 py-3 text-slate-700 transition-colors hover:bg-primary/10 hover:text-primary dark:text-slate-300 dark:hover:bg-primary/20 dark:hover:text-primary">
-                                    <span className="material-symbols-outlined"><Smartphone size="16" /></span>
-                                    <span className="text-[14px] font-bold">修改手机</span>
-                                    <span className="material-symbols-outlined ml-auto text-slate-300"><ChevronRight size="16" /></span>
-                                </button>
-                                <button className="border-b border-slate-200 dark:border-slate-800 flex w-full items-center gap-4  px-3 py-3 text-slate-700 transition-colors hover:bg-primary/10 hover:text-primary dark:text-slate-300 dark:hover:bg-primary/20 dark:hover:text-primary">
-                                    <span className="material-symbols-outlined"><Key size="16" /></span>
-                                    <span className="text-[14px] font-bold">绑定密码</span>
-                                    <span className="material-symbols-outlined ml-auto text-slate-300"><ChevronRight size="16" /></span>
-                                </button>
-                                <button className="border-b border-slate-200 dark:border-slate-800 flex w-full items-center gap-4  px-3 py-3 text-slate-700 transition-colors hover:bg-primary/10 hover:text-primary dark:text-slate-300 dark:hover:bg-primary/20 dark:hover:text-primary">
-                                    <span className="material-symbols-outlined"><IdCard size="16" /></span>
-                                    <span className="text-[14px] font-bold">实名认证</span>
-                                    <span className="material-symbols-outlined ml-auto text-slate-300"><ChevronRight size="16" /></span>
-                                </button>
-                               </nav>
-                        </div>
-                        <div className="p-4 border-t border-slate-200 dark:border-slate-800">
-                            <button className="flex w-full items-center justify-center gap-2 rounded-xl bg-slate-100 py-3 text-[14px] font-bold text-slate-900 transition-colors hover:bg-red-50 hover:text-red-600 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-red-900/30 dark:hover:text-red-400">
-                                <span className="material-symbols-outlined text-xl"><SquareArrowRightExit size="16" /></span>
-                                <span>退出登录</span>
-                            </button>
-                            <p className="mt-4 text-center text-[10px] text-slate-400">SDK Version {import.meta.env.VITE_SDK_VERSION}</p>
-                        </div>
-                    </div>
-                    <div className="flex-1 cursor-pointer"></div>
-                </div>
+    return (
+        <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm">
+            <div className="absolute inset-0" onClick={onClose}></div>
+            <div className="relative z-110">
+                {showSlideBar && (
+                    <SlideBar 
+                        onChangePassword={() => { setShowChangePassword(true); setShowSlideBar(false); }} 
+                        onBindPhone={() => { setShowBindPhone(true); setShowSlideBar(false); }}
+                        onRealName={() => { setShowRealName(true); setShowSlideBar(false); }}
+                        onAgreement={() => { setShowAgreement({ title: '用户协议', url: '/static/user.html' }); setShowSlideBar(false); }}
+                        onPrivacy={() => { setShowAgreement({ title: '隐私政策', url: '/static/ys.html' }); setShowSlideBar(false); }}
+                        onClose={onClose}
+                    />
+                )}
+                {showChangePassword && <ChangePassword onBack={handleBack} />}
+                {showBindPhone && <BindPhone onBack={handleBack} />}
+                {showRealName && <RealName onBack={handleBack} />}
+                {showAgreement && <Agreement title={showAgreement.title} url={showAgreement.url} onBack={handleBack} />}
             </div>
-        </>
+        </div>
     );
 }

+ 103 - 0
src/components/slide-bar/real-name/index.tsx

@@ -0,0 +1,103 @@
+import { ChevronLeft } from "lucide-react";
+import { useState } from "react";
+import { realNameAuthApi } from "../../../api/login";
+
+export interface RealNameProps {
+    onBack: () => void;
+}
+
+export const RealName = ({ onBack }: RealNameProps) => {
+    const [realName, setRealName] = useState('');
+    const [idCard, setIdCard] = useState('');
+    const [loading, setLoading] = useState(false);
+
+    const handleSubmit = async () => {
+        if (!realName) {
+            alert('请输入真实姓名');
+            return;
+        }
+        if (!idCard) {
+            alert('请输入身份证号');
+            return;
+        }
+        
+        // 简单的身份证号校验
+        const idCardReg = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/;
+        if (!idCardReg.test(idCard)) {
+            alert('请输入正确的身份证号格式');
+            return;
+        }
+
+        setLoading(true);
+        try {
+            await realNameAuthApi({
+                real_name: realName,
+                id_card: idCard
+            });
+            alert('实名认证成功');
+            onBack();
+        } catch (error: any) {
+            alert(error.message || '认证失败');
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    return (
+        <div className="fixed left-0 top-0 flex h-screen overflow-hidden bg-white">
+            <div className="relative z-20 flex h-full w-full">
+                <div className="flex h-full w-80 flex-col bg-background-light dark:bg-background-dark shadow-2xl">
+                    <div className="flex items-center border-b border-slate-200 dark:border-slate-800 py-3 px-2">
+                        <button 
+                            onClick={onBack}
+                            className="text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
+                        >
+                            <ChevronLeft size="24" />
+                        </button>
+                        <h1 className="text-md text-center pl-2 flex-1 pr-8 text-slate-900 dark:text-slate-100">实名认证</h1>
+                    </div>
+
+                    <div className="flex-1 p-6 space-y-4">
+                        <div className="space-y-2">
+                            <label className="text-sm font-medium text-slate-700 dark:text-slate-300">真实姓名</label>
+                            <input
+                                type="text"
+                                placeholder="请输入真实姓名"
+                                className="w-full rounded-lg border border-slate-200 bg-white p-2.5 text-sm focus:border-primary focus:outline-none dark:border-slate-800 dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder:text-slate-400"
+                                value={realName}
+                                onChange={(e) => setRealName(e.target.value)}
+                            />
+                        </div>
+
+                        <div className="space-y-2">
+                            <label className="text-sm font-medium text-slate-700 dark:text-slate-300">身份证号</label>
+                            <input
+                                type="text"
+                                placeholder="请输入身份证号"
+                                className="w-full rounded-lg border border-slate-200 bg-white p-2.5 text-sm focus:border-primary focus:outline-none dark:border-slate-800 dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder:text-slate-400"
+                                value={idCard}
+                                onChange={(e) => setIdCard(e.target.value)}
+                            />
+                        </div>
+
+                        <div className="mt-4 rounded-lg bg-orange-50 p-3 dark:bg-orange-900/20">
+                            <p className="text-[10px] leading-relaxed text-orange-600 dark:text-orange-400">
+                                温馨提示:根据国家相关法律法规,该游戏需要进行实名认证。我们承诺会对您的信息严格保密。
+                            </p>
+                        </div>
+
+                        <button
+                            onClick={handleSubmit}
+                            disabled={loading}
+                            className={`mt-6 w-full rounded-lg bg-primary py-3 font-bold text-white shadow-lg transition-all hover:bg-primary/90 ${
+                                loading ? 'opacity-70 cursor-not-allowed' : ''
+                            }`}
+                        >
+                            {loading ? '提交中...' : '提交认证'}
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    );
+};

+ 102 - 0
src/components/slide-bar/slide-bar.tsx

@@ -0,0 +1,102 @@
+import { ChevronRight, IdCard, Key, Smartphone, SquareArrowRightExit } from "lucide-react";
+import { logout } from "../../store/user-atom";
+import { pmBridge } from "../../lib/PostMessageBridge";
+import { logoutApi } from "../../api/login";
+
+export default function SlideBar({ 
+    onChangePassword, 
+    onBindPhone, 
+    onRealName,
+    onAgreement,
+    onPrivacy,
+    onClose 
+}: { 
+    onChangePassword: () => void, 
+    onBindPhone: () => void, 
+    onRealName: () => void,
+    onAgreement: () => void,
+    onPrivacy: () => void,
+    onClose: () => void
+}) {
+    
+    const handleLogout = async () => {
+        try {
+            // 1. 调用退出登录接口
+            await logoutApi();
+        } catch (error) {
+            console.error('Logout API failed:', error);
+        } finally {
+            // 无论接口是否成功,都执行本地清理逻辑
+            
+            // 2. 清除本地存储和状态
+            logout();
+            
+            // 3. 通知父页面(游戏)
+            pmBridge.sendToIframe('LOGOUT SUCCESS');
+
+            // 4. 关闭侧边栏
+            onClose();
+        }
+    };
+
+    return (
+            <div className="fixed left-0 top-0 flex h-screen overflow-hidden bg-white">
+
+                <div className="relative z-20 flex h-full w-full" onClick={(e) => e.stopPropagation()}>
+                    <div className="flex h-full w-80 flex-col bg-background-light dark:bg-background-dark shadow-2xl">
+
+                        <div className="p-6 pb-4 border-b border-slate-200 dark:border-slate-800">
+                            <div className="flex">
+                                <div className="flex flex-col">
+                                    <h2 className="text-xl font-bold tracking-tight text-slate-900 dark:text-slate-100">CyberWarrior_99</h2>
+                                    <p className="text-xs font-medium text-primary bg-primary/10 px-2 py-0.5 rounded-full inline-block mt-1">ID: #8842-X01</p>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div className="flex-1 overflow-y-auto py-4">
+                            <nav className="space-y-2">
+                                <p className="px-3 pb-2 text-xs font-semibold uppercase tracking-wider text-slate-400">账户安全</p>
+                                <button className="border-b border-slate-200 dark:border-slate-800 flex w-full items-center gap-4  px-3 py-3 text-slate-700 transition-colors hover:bg-primary/10 hover:text-primary dark:text-slate-300 dark:hover:bg-primary/20 dark:hover:text-primary" onClick={onBindPhone}>
+                                    <span className="material-symbols-outlined"><Smartphone size="16" /></span>
+                                    <span className="text-[14px] font-bold">绑定手机</span>
+                                    <span className="material-symbols-outlined ml-auto text-slate-300"><ChevronRight size="16" /></span>
+                                </button>
+                                <button className="border-b border-slate-200 dark:border-slate-800 flex w-full items-center gap-4  px-3 py-3 text-slate-700 transition-colors hover:bg-primary/10 hover:text-primary dark:text-slate-300 dark:hover:bg-primary/20 dark:hover:text-primary" onClick={onChangePassword}>
+                                    <span className="material-symbols-outlined"><Key size="16" /></span>
+                                    <span className="text-[14px] font-bold">修改密码</span>
+                                    <span className="material-symbols-outlined ml-auto text-slate-300"><ChevronRight size="16" /></span>
+                                </button>
+                                <button className="border-b border-slate-200 dark:border-slate-800 flex w-full items-center gap-4  px-3 py-3 text-slate-700 transition-colors hover:bg-primary/10 hover:text-primary dark:text-slate-300 dark:hover:bg-primary/20 dark:hover:text-primary" onClick={onRealName}>
+                                    <span className="material-symbols-outlined"><IdCard size="16" /></span>
+                                    <span className="text-[14px] font-bold">实名认证</span>
+                                    <span className="material-symbols-outlined ml-auto text-slate-300"><ChevronRight size="16" /></span>
+                                </button>
+
+                                <p className="px-3 pb-2 pt-4 text-xs font-semibold uppercase tracking-wider text-slate-400">法律协议</p>
+                                <button className="border-b border-slate-200 dark:border-slate-800 flex w-full items-center gap-4  px-3 py-3 text-slate-700 transition-colors hover:bg-primary/10 hover:text-primary dark:text-slate-300 dark:hover:bg-primary/20 dark:hover:text-primary" onClick={onAgreement}>
+                                    <span className="text-[14px] font-bold px-1">用户协议</span>
+                                    <span className="material-symbols-outlined ml-auto text-slate-300"><ChevronRight size="16" /></span>
+                                </button>
+                                <button className="border-b border-slate-200 dark:border-slate-800 flex w-full items-center gap-4  px-3 py-3 text-slate-700 transition-colors hover:bg-primary/10 hover:text-primary dark:text-slate-300 dark:hover:bg-primary/20 dark:hover:text-primary" onClick={onPrivacy}>
+                                    <span className="text-[14px] font-bold px-1">隐私政策</span>
+                                    <span className="material-symbols-outlined ml-auto text-slate-300"><ChevronRight size="16" /></span>
+                                </button>
+                            </nav>
+                        </div>
+                        <div className="p-4 border-t border-slate-200 dark:border-slate-800">
+                            <button 
+                                onClick={handleLogout}
+                                className="flex w-full items-center justify-center gap-2 rounded-xl bg-slate-100 py-3 text-[14px] font-bold text-slate-900 transition-colors hover:bg-red-50 hover:text-red-600 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-red-900/30 dark:hover:text-red-400"
+                            >
+                                <span className="material-symbols-outlined text-xl"><SquareArrowRightExit size="16" /></span>
+                                <span>退出登录</span>
+                            </button>
+                            <p className="mt-4 text-center text-[10px] text-slate-400">SDK Version {import.meta.env.VITE_SDK_VERSION}</p>
+                        </div>
+                    </div>
+                    <div className="flex-1 cursor-pointer" onClick={onClose}></div>
+                </div>
+            </div>
+    );
+}