Diamond_ore hai 2 semanas
achega
628049aff4

+ 214 - 0
README.md

@@ -0,0 +1,214 @@
+# 子系统接入示例
+
+这是一个完整的子系统接入SSO认证中心的示例项目,展示了如何在实际应用中集成OAuth 2.0单点登录。
+
+## 功能特性
+
+- ✅ OAuth 2.0 授权码模式集成
+- ✅ 自动跳转SSO登录页
+- ✅ 令牌自动管理和刷新
+- ✅ 用户信息同步
+- ✅ 统一登出
+- ✅ 受保护的API接口
+
+## 快速开始
+
+### 1. 配置SSO应用
+
+首先在SSO认证中心创建应用并获取:
+- Client ID (应用Key)
+- Client Secret (应用密钥)
+- 配置回调URL: `http://localhost:8001/auth/callback`
+
+### 2. 启动后端
+
+```bash
+cd backend
+
+# 安装依赖
+pip install -r requirements.txt
+
+# 配置环境变量
+cp .env.example .env
+# 编辑 .env 文件,填入SSO应用的配置信息
+
+# 启动服务
+python main.py
+```
+
+后端服务运行在 `http://localhost:8001`
+
+### 3. 启动前端
+
+```bash
+cd frontend
+
+# 安装依赖
+npm install
+
+# 启动开发服务器
+npm run dev
+```
+
+前端服务运行在 `http://localhost:3001`
+
+## 使用流程
+
+1. 访问 `http://localhost:3001`
+2. 点击登录按钮
+3. 自动跳转到SSO登录页 (`http://localhost:8000/oauth/authorize`)
+4. 输入用户名和密码登录
+5. 授权后自动跳转回子系统
+6. 子系统获取用户信息并建立会话
+
+## 技术实现
+
+### 后端实现要点
+
+1. **发起授权请求**
+```python
+@app.get("/auth/login")
+async def login():
+    params = {
+        "response_type": "code",
+        "client_id": CLIENT_ID,
+        "redirect_uri": REDIRECT_URI,
+        "scope": "profile email",
+        "state": generate_state()
+    }
+    auth_url = f"{SSO_BASE_URL}/oauth/authorize?{urlencode(params)}"
+    return RedirectResponse(url=auth_url)
+```
+
+2. **处理授权回调**
+```python
+@app.get("/auth/callback")
+async def auth_callback(code: str):
+    # 用授权码换取访问令牌
+    token_response = await exchange_code_for_token(code)
+    
+    # 获取用户信息
+    user_info = await get_user_info(token_response.access_token)
+    
+    # 建立本地会话
+    create_session(user_info, token_response)
+    
+    return redirect_to_frontend()
+```
+
+3. **保护API接口**
+```python
+async def get_current_user(credentials = Depends(security)):
+    token = credentials.credentials
+    
+    # 验证token
+    user_info = await verify_token_with_sso(token)
+    
+    return user_info
+
+@app.get("/api/protected")
+async def protected_route(user = Depends(get_current_user)):
+    return {"data": "protected data"}
+```
+
+### 前端实现要点
+
+1. **登录跳转**
+```typescript
+function login() {
+  window.location.href = 'http://localhost:8001/auth/login'
+}
+```
+
+2. **处理回调**
+```typescript
+// 在回调页面获取token
+const urlParams = new URLSearchParams(window.location.search)
+const token = urlParams.get('token')
+
+if (token) {
+  // 保存token
+  localStorage.setItem('access_token', token)
+  
+  // 跳转到首页
+  router.push('/dashboard')
+}
+```
+
+3. **API请求拦截**
+```typescript
+axios.interceptors.request.use(config => {
+  const token = localStorage.getItem('access_token')
+  if (token) {
+    config.headers.Authorization = `Bearer ${token}`
+  }
+  return config
+})
+```
+
+## API接口
+
+### 认证相关
+
+- `GET /auth/login` - 发起SSO登录
+- `GET /auth/callback` - 处理SSO回调
+- `GET /auth/logout` - 用户登出
+
+### 业务接口(需要认证)
+
+- `GET /api/user/profile` - 获取用户资料
+- `GET /api/products` - 获取产品列表
+- `GET /api/orders` - 获取订单列表
+
+## 注意事项
+
+1. **安全性**
+   - 生产环境必须使用HTTPS
+   - 妥善保管Client Secret
+   - 验证state参数防止CSRF攻击
+   - 定期刷新访问令牌
+
+2. **错误处理**
+   - 处理授权失败情况
+   - 处理令牌过期情况
+   - 处理网络错误
+
+3. **用户体验**
+   - 保存用户登录状态
+   - 自动刷新令牌
+   - 提供友好的错误提示
+
+## 扩展功能
+
+可以根据实际需求添加:
+
+- 用户信息本地缓存
+- 令牌自动刷新机制
+- 多租户支持
+- 权限验证
+- 审计日志
+
+## 故障排查
+
+### 常见问题
+
+1. **授权失败**
+   - 检查Client ID和Secret是否正确
+   - 检查回调URL是否匹配
+   - 检查SSO服务是否正常运行
+
+2. **令牌验证失败**
+   - 检查令牌是否过期
+   - 检查令牌格式是否正确
+   - 检查SSO服务连接是否正常
+
+3. **跨域问题**
+   - 检查CORS配置
+   - 检查代理配置
+
+## 参考资料
+
+- [OAuth 2.0 RFC 6749](https://tools.ietf.org/html/rfc6749)
+- [JWT RFC 7519](https://tools.ietf.org/html/rfc7519)
+- [FastAPI文档](https://fastapi.tiangolo.com/)
+- [Vue 3文档](https://vuejs.org/)

+ 12 - 0
backend/.env

@@ -0,0 +1,12 @@
+# SSO配置
+SSO_BASE_URL=http://localhost:8000
+CLIENT_ID=eqhoIdAyAWbA8MsYHsNqQqNLJbCayTjY
+CLIENT_SECRET=0070ebeYOmYQU28T85nkpedikDd6kBbkZ6LxVJzgznJvrb83HzcfOB1LCOwio4ML
+REDIRECT_URI=http://localhost:8001/auth/callback
+
+# 服务器配置
+HOST=0.0.0.0
+PORT=8001
+
+# 前端地址
+FRONTEND_URL=http://localhost:3001

+ 12 - 0
backend/.env.example

@@ -0,0 +1,12 @@
+# SSO配置
+SSO_BASE_URL=http://localhost:8000
+CLIENT_ID=your-client-id-from-sso-admin
+CLIENT_SECRET=your-client-secret-from-sso-admin
+REDIRECT_URI=http://localhost:8001/auth/callback
+
+# 服务器配置
+HOST=0.0.0.0
+PORT=8001
+
+# 前端地址
+FRONTEND_URL=http://localhost:3001

+ 813 - 0
backend/main.py

@@ -0,0 +1,813 @@
+"""
+子系统后端示例 - 集成SSO认证
+"""
+from fastapi import FastAPI, Depends, HTTPException, Request
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import RedirectResponse, JSONResponse
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+from pydantic import BaseModel
+from typing import Optional, Dict, Any
+import httpx
+import os
+from urllib.parse import urlencode
+import secrets
+from dotenv import load_dotenv
+
+# 加载环境变量
+load_dotenv()
+
+app = FastAPI(title="子系统示例", version="1.0.0")
+
+# CORS配置
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["http://localhost:3001"],  # 子系统前端地址
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+# SSO配置
+SSO_BASE_URL = os.getenv("SSO_BASE_URL", "http://localhost:8000")
+CLIENT_ID = os.getenv("CLIENT_ID", "your-client-id")
+CLIENT_SECRET = os.getenv("CLIENT_SECRET", "your-client-secret")
+REDIRECT_URI = os.getenv("REDIRECT_URI", "http://localhost:8001/auth/callback")
+
+print(f"🔧 SSO配置:")
+print(f"   SSO_BASE_URL: {SSO_BASE_URL}")
+print(f"   CLIENT_ID: {CLIENT_ID}")
+print(f"   CLIENT_SECRET: {CLIENT_SECRET[:20]}...")
+print(f"   REDIRECT_URI: {REDIRECT_URI}")
+
+security = HTTPBearer()
+
+class UserInfo(BaseModel):
+    """用户信息模型"""
+    id: str
+    username: str
+    email: str
+    phone: Optional[str] = None
+    avatar_url: Optional[str] = None
+    is_active: bool
+
+class TokenResponse(BaseModel):
+    """令牌响应模型"""
+    access_token: str
+    refresh_token: Optional[str] = None
+    token_type: str
+    expires_in: int
+    scope: Optional[str] = None
+
+# 内存存储(生产环境应使用数据库或Redis)
+user_sessions: Dict[str, UserInfo] = {}
+access_tokens: Dict[str, str] = {}  # token -> user_id
+
+async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> UserInfo:
+    """获取当前用户"""
+    token = credentials.credentials
+    
+    # 检查本地token缓存
+    user_id = access_tokens.get(token)
+    if user_id and user_id in user_sessions:
+        return user_sessions[user_id]
+    
+    # 向SSO服务验证token
+    async with httpx.AsyncClient() as client:
+        try:
+            response = await client.get(
+                f"{SSO_BASE_URL}/oauth/userinfo",
+                headers={"Authorization": f"Bearer {token}"}
+            )
+            
+            if response.status_code == 200:
+                data = response.json()
+                if data.get("code") == 0:
+                    user_data = data["data"]
+                    user_info = UserInfo(**user_data)
+                    
+                    # 缓存用户信息
+                    user_sessions[user_info.id] = user_info
+                    access_tokens[token] = user_info.id
+                    
+                    return user_info
+            
+            raise HTTPException(status_code=401, detail="无效的访问令牌")
+            
+        except httpx.RequestError:
+            raise HTTPException(status_code=503, detail="SSO服务不可用")
+
+@app.get("/")
+async def root():
+    """根路径"""
+    return {"message": "子系统示例", "version": "1.0.0"}
+
+@app.get("/auth/login")
+async def login(request: Request):
+    """发起SSO登录"""
+    # 生成state参数防止CSRF攻击
+    state = secrets.token_urlsafe(32)
+    
+    # 构建授权URL
+    params = {
+        "response_type": "code",
+        "client_id": CLIENT_ID,
+        "redirect_uri": REDIRECT_URI,
+        "scope": "profile email",
+        "state": state
+    }
+    
+    auth_url = f"{SSO_BASE_URL}/oauth/authorize?{urlencode(params)}"
+    
+    return RedirectResponse(url=auth_url)
+
+@app.get("/auth/callback")
+async def auth_callback(
+    code: Optional[str] = None,
+    state: Optional[str] = None,
+    error: Optional[str] = None
+):
+    """处理SSO回调"""
+    print(f"🔄 收到SSO回调: code={code[:20] if code else None}..., state={state}, error={error}")
+    
+    if error:
+        print(f"❌ 授权失败: {error}")
+        return JSONResponse(
+            status_code=400,
+            content={"error": error, "message": "授权失败"}
+        )
+    
+    if not code:
+        print("❌ 缺少授权码")
+        return JSONResponse(
+            status_code=400,
+            content={"error": "missing_code", "message": "缺少授权码"}
+        )
+    
+    # 用授权码换取访问令牌
+    async with httpx.AsyncClient() as client:
+        try:
+            print(f"🎫 开始令牌交换...")
+            print(f"   SSO令牌端点: {SSO_BASE_URL}/oauth/token")
+            print(f"   Client ID: {CLIENT_ID}")
+            print(f"   回调URI: {REDIRECT_URI}")
+            
+            token_data = {
+                "grant_type": "authorization_code",
+                "code": code,
+                "redirect_uri": REDIRECT_URI,
+                "client_id": CLIENT_ID,
+                "client_secret": CLIENT_SECRET
+            }
+            
+            print(f"   令牌请求数据: {dict(token_data)}")
+            
+            response = await client.post(
+                f"{SSO_BASE_URL}/oauth/token",
+                data=token_data
+            )
+            
+            print(f"   令牌响应状态: {response.status_code}")
+            print(f"   令牌响应内容: {response.text}")
+            
+            if response.status_code == 200:
+                data = response.json()
+                print(f"   解析的响应数据: {data}")
+                
+                # 检查响应格式
+                if "access_token" in data:
+                    # 直接的令牌响应格式
+                    token_info = data
+                    print(f"✅ 直接令牌格式: {token_info.get('access_token', '')[:20]}...")
+                elif data.get("code") == 0 and "data" in data:
+                    # 包装的响应格式
+                    token_info = data["data"]
+                    print(f"✅ 包装令牌格式: {token_info.get('access_token', '')[:20]}...")
+                else:
+                    print(f"❌ 未知的令牌响应格式: {data}")
+                    return JSONResponse(
+                        status_code=400,
+                        content={"error": "invalid_token_response", "message": "无效的令牌响应格式"}
+                    )
+                
+                # 获取用户信息
+                print(f"👤 获取用户信息...")
+                user_response = await client.get(
+                    f"{SSO_BASE_URL}/oauth/userinfo",
+                    headers={"Authorization": f"Bearer {token_info['access_token']}"}
+                )
+                
+                print(f"   用户信息响应状态: {user_response.status_code}")
+                print(f"   用户信息响应内容: {user_response.text}")
+                
+                if user_response.status_code == 200:
+                    user_data = user_response.json()
+                    print(f"   解析的用户数据: {user_data}")
+                    
+                    # 检查用户信息响应格式
+                    if "sub" in user_data:
+                        # 直接的用户信息格式
+                        user_info_data = user_data
+                        print(f"✅ 直接用户信息格式")
+                    elif user_data.get("code") == 0 and "data" in user_data:
+                        # 包装的用户信息格式
+                        user_info_data = user_data["data"]
+                        print(f"✅ 包装用户信息格式")
+                    else:
+                        print(f"❌ 未知的用户信息响应格式: {user_data}")
+                        return JSONResponse(
+                            status_code=400,
+                            content={"error": "invalid_user_response", "message": "无效的用户信息响应格式"}
+                        )
+                    
+                    # 构建用户信息对象
+                    user_info = UserInfo(
+                        id=user_info_data["sub"],
+                        username=user_info_data.get("username", ""),
+                        email=user_info_data.get("email", ""),
+                        phone=user_info_data.get("phone"),
+                        avatar_url=user_info_data.get("avatar_url"),
+                        is_active=True
+                    )
+                    
+                    # 缓存用户信息和token
+                    user_sessions[user_info.id] = user_info
+                    access_tokens[token_info["access_token"]] = user_info.id
+                    
+                    print(f"✅ 用户登录成功: {user_info.username}")
+                    
+                    # 登录成功,重定向到首页
+                    print(f"✅ 用户登录成功: {user_info.username}")
+                    
+                    # 构建首页URL,携带token和用户信息
+                    home_url = f"/home?token={token_info['access_token']}&user_id={user_info.id}&username={user_info.username}&email={user_info.email}"
+                    
+                    print(f"🏠 重定向到首页: {home_url[:80]}...")
+                    return RedirectResponse(url=home_url)
+                else:
+                    print(f"❌ 获取用户信息失败: {user_response.status_code}")
+                    return JSONResponse(
+                        status_code=400,
+                        content={"error": "user_info_failed", "message": "获取用户信息失败"}
+                    )
+            else:
+                print(f"❌ 令牌交换失败: {response.status_code}")
+                return JSONResponse(
+                    status_code=400,
+                    content={"error": "token_exchange_failed", "message": "令牌交换失败"}
+                )
+            
+        except httpx.RequestError as e:
+            print(f"❌ 网络请求错误: {e}")
+            return JSONResponse(
+                status_code=503,
+                content={"error": "sso_unavailable", "message": "SSO服务不可用"}
+            )
+        except Exception as e:
+            print(f"❌ 处理回调时发生错误: {e}")
+            return JSONResponse(
+                status_code=500,
+                content={"error": "callback_error", "message": f"回调处理错误: {str(e)}"}
+            )
+
+@app.get("/home")
+async def home_page(
+    token: str = None,
+    user_id: str = None,
+    username: str = None,
+    email: str = None
+):
+    """子系统首页"""
+    try:
+        print(f"🏠 访问首页: user={username}")
+        
+        # 构建首页HTML
+        home_html = f"""
+        <!DOCTYPE html>
+        <html lang="zh-CN">
+        <head>
+            <meta charset="UTF-8">
+            <meta name="viewport" content="width=device-width, initial-scale=1.0">
+            <title>子系统首页</title>
+            <style>
+                * {{
+                    margin: 0;
+                    padding: 0;
+                    box-sizing: border-box;
+                }}
+                
+                body {{
+                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+                    background: #f5f5f5;
+                    min-height: 100vh;
+                }}
+                
+                .header {{
+                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+                    color: white;
+                    padding: 20px 0;
+                    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+                }}
+                
+                .header-content {{
+                    max-width: 1200px;
+                    margin: 0 auto;
+                    padding: 0 20px;
+                    display: flex;
+                    justify-content: space-between;
+                    align-items: center;
+                }}
+                
+                .logo {{
+                    font-size: 24px;
+                    font-weight: bold;
+                }}
+                
+                .user-info {{
+                    display: flex;
+                    align-items: center;
+                    gap: 20px;
+                }}
+                
+                .user-avatar {{
+                    width: 40px;
+                    height: 40px;
+                    background: rgba(255, 255, 255, 0.2);
+                    border-radius: 50%;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    font-size: 18px;
+                }}
+                
+                .user-details {{
+                    display: flex;
+                    flex-direction: column;
+                }}
+                
+                .username {{
+                    font-weight: 500;
+                    font-size: 16px;
+                }}
+                
+                .email {{
+                    font-size: 14px;
+                    opacity: 0.8;
+                }}
+                
+                .logout-btn {{
+                    background: rgba(255, 255, 255, 0.2);
+                    border: 1px solid rgba(255, 255, 255, 0.3);
+                    color: white;
+                    padding: 8px 16px;
+                    border-radius: 6px;
+                    cursor: pointer;
+                    font-size: 14px;
+                    transition: all 0.3s;
+                }}
+                
+                .logout-btn:hover {{
+                    background: rgba(255, 255, 255, 0.3);
+                }}
+                
+                .main-content {{
+                    max-width: 1200px;
+                    margin: 40px auto;
+                    padding: 0 20px;
+                }}
+                
+                .welcome-section {{
+                    background: white;
+                    border-radius: 15px;
+                    padding: 40px;
+                    margin-bottom: 30px;
+                    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+                    text-align: center;
+                }}
+                
+                .welcome-title {{
+                    font-size: 32px;
+                    color: #333;
+                    margin-bottom: 10px;
+                }}
+                
+                .welcome-subtitle {{
+                    font-size: 18px;
+                    color: #666;
+                    margin-bottom: 30px;
+                }}
+                
+                .features-grid {{
+                    display: grid;
+                    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+                    gap: 20px;
+                    margin-bottom: 30px;
+                }}
+                
+                .feature-card {{
+                    background: white;
+                    border-radius: 12px;
+                    padding: 30px;
+                    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+                    transition: transform 0.3s, box-shadow 0.3s;
+                }}
+                
+                .feature-card:hover {{
+                    transform: translateY(-5px);
+                    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
+                }}
+                
+                .feature-icon {{
+                    font-size: 48px;
+                    margin-bottom: 20px;
+                }}
+                
+                .feature-title {{
+                    font-size: 20px;
+                    font-weight: 600;
+                    color: #333;
+                    margin-bottom: 10px;
+                }}
+                
+                .feature-description {{
+                    color: #666;
+                    line-height: 1.6;
+                    margin-bottom: 20px;
+                }}
+                
+                .feature-btn {{
+                    background: #007bff;
+                    color: white;
+                    padding: 10px 20px;
+                    border: none;
+                    border-radius: 6px;
+                    cursor: pointer;
+                    font-size: 14px;
+                    font-weight: 500;
+                    transition: background 0.3s;
+                    text-decoration: none;
+                    display: inline-block;
+                }}
+                
+                .feature-btn:hover {{
+                    background: #0056b3;
+                }}
+                
+                .api-section {{
+                    background: white;
+                    border-radius: 12px;
+                    padding: 30px;
+                    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+                }}
+                
+                .api-title {{
+                    font-size: 24px;
+                    color: #333;
+                    margin-bottom: 20px;
+                }}
+                
+                .api-result {{
+                    background: #f8f9fa;
+                    border: 1px solid #e9ecef;
+                    border-radius: 8px;
+                    padding: 20px;
+                    font-family: 'Courier New', monospace;
+                    font-size: 14px;
+                    max-height: 300px;
+                    overflow-y: auto;
+                    white-space: pre-wrap;
+                    margin-bottom: 20px;
+                }}
+                
+                .api-buttons {{
+                    display: flex;
+                    gap: 10px;
+                    flex-wrap: wrap;
+                }}
+                
+                .status-indicator {{
+                    display: inline-block;
+                    width: 8px;
+                    height: 8px;
+                    border-radius: 50%;
+                    margin-right: 8px;
+                }}
+                
+                .status-online {{
+                    background: #28a745;
+                }}
+                
+                .status-offline {{
+                    background: #dc3545;
+                }}
+                
+                @media (max-width: 768px) {{
+                    .header-content {{
+                        flex-direction: column;
+                        gap: 20px;
+                    }}
+                    
+                    .user-info {{
+                        flex-direction: column;
+                        text-align: center;
+                    }}
+                    
+                    .features-grid {{
+                        grid-template-columns: 1fr;
+                    }}
+                    
+                    .api-buttons {{
+                        flex-direction: column;
+                    }}
+                }}
+            </style>
+        </head>
+        <body>
+            <!-- 头部导航 -->
+            <header class="header">
+                <div class="header-content">
+                    <div class="logo">
+                        🏢 子系统示例
+                    </div>
+                    <div class="user-info">
+                        <div class="user-avatar">
+                            {username[0].upper() if username else '?'}
+                        </div>
+                        <div class="user-details">
+                            <div class="username">{username or '未知用户'}</div>
+                            <div class="email">{email or '未知邮箱'}</div>
+                        </div>
+                        <button class="logout-btn" onclick="logout()">
+                            🚪 退出登录
+                        </button>
+                    </div>
+                </div>
+            </header>
+            
+            <!-- 主要内容 -->
+            <main class="main-content">
+                <!-- 欢迎区域 -->
+                <section class="welcome-section">
+                    <h1 class="welcome-title">欢迎回来,{username or '用户'}!</h1>
+                    <p class="welcome-subtitle">您已通过SSO成功登录到子系统</p>
+                    <div style="display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 20px;">
+                        <span class="status-indicator status-online"></span>
+                        <span>系统状态:正常运行</span>
+                    </div>
+                </section>
+                
+                <!-- 功能卡片 -->
+                <div class="features-grid">
+                    <div class="feature-card">
+                        <div class="feature-icon">👤</div>
+                        <h3 class="feature-title">个人资料</h3>
+                        <p class="feature-description">查看和管理您的个人信息,包括基本资料和账户设置。</p>
+                        <button class="feature-btn" onclick="testAPI('/api/user/profile', '个人资料')">
+                            查看资料
+                        </button>
+                    </div>
+                    
+                    <div class="feature-card">
+                        <div class="feature-icon">📦</div>
+                        <h3 class="feature-title">产品管理</h3>
+                        <p class="feature-description">浏览和管理系统中的所有产品信息。</p>
+                        <button class="feature-btn" onclick="testAPI('/api/products', '产品列表')">
+                            查看产品
+                        </button>
+                    </div>
+                    
+                    <div class="feature-card">
+                        <div class="feature-icon">📋</div>
+                        <h3 class="feature-title">订单管理</h3>
+                        <p class="feature-description">查看您的订单历史和当前订单状态。</p>
+                        <button class="feature-btn" onclick="testAPI('/api/orders', '订单列表')">
+                            查看订单
+                        </button>
+                    </div>
+                </div>
+                
+                <!-- API测试区域 -->
+                <section class="api-section">
+                    <h2 class="api-title">📡 API测试区域</h2>
+                    <div class="api-result" id="apiResult">
+点击上方按钮测试API功能...
+                    </div>
+                    <div class="api-buttons">
+                        <button class="feature-btn" onclick="clearResult()">清空结果</button>
+                        <button class="feature-btn" onclick="showToken()">显示Token</button>
+                        <button class="feature-btn" onclick="testAllAPIs()">测试所有API</button>
+                    </div>
+                </section>
+            </main>
+            
+            <script>
+                // 存储用户信息和token
+                const userInfo = {{
+                    id: '{user_id or ''}',
+                    username: '{username or ''}',
+                    email: '{email or ''}',
+                    token: '{token or ''}'
+                }};
+                
+                // 将信息存储到localStorage
+                if (userInfo.token) {{
+                    localStorage.setItem('access_token', userInfo.token);
+                    localStorage.setItem('user_info', JSON.stringify(userInfo));
+                }}
+                
+                // API测试函数
+                async function testAPI(endpoint, name) {{
+                    const resultDiv = document.getElementById('apiResult');
+                    resultDiv.textContent = `正在请求 ${{name}}...`;
+                    
+                    try {{
+                        const response = await fetch(endpoint, {{
+                            headers: {{
+                                'Authorization': `Bearer ${{userInfo.token}}`
+                            }}
+                        }});
+                        
+                        const data = await response.json();
+                        
+                        resultDiv.textContent = `${{name}} API 响应:\\n\\n${{JSON.stringify(data, null, 2)}}`;
+                    }} catch (error) {{
+                        resultDiv.textContent = `${{name}} API 请求失败:\\n\\n${{error.message}}`;
+                    }}
+                }}
+                
+                // 测试所有API
+                async function testAllAPIs() {{
+                    const apis = [
+                        {{endpoint: '/api/user/profile', name: '个人资料'}},
+                        {{endpoint: '/api/products', name: '产品列表'}},
+                        {{endpoint: '/api/orders', name: '订单列表'}}
+                    ];
+                    
+                    const resultDiv = document.getElementById('apiResult');
+                    resultDiv.textContent = '正在测试所有API...\\n\\n';
+                    
+                    for (const api of apis) {{
+                        try {{
+                            const response = await fetch(api.endpoint, {{
+                                headers: {{
+                                    'Authorization': `Bearer ${{userInfo.token}}`
+                                }}
+                            }});
+                            
+                            const data = await response.json();
+                            resultDiv.textContent += `✅ ${{api.name}}: 成功\\n`;
+                        }} catch (error) {{
+                            resultDiv.textContent += `❌ ${{api.name}}: 失败 - ${{error.message}}\\n`;
+                        }}
+                    }}
+                }}
+                
+                // 显示Token
+                function showToken() {{
+                    const resultDiv = document.getElementById('apiResult');
+                    resultDiv.textContent = `访问令牌:\\n\\n${{userInfo.token}}\\n\\n令牌信息已复制到剪贴板(如果支持)`;
+                    
+                    // 尝试复制到剪贴板
+                    if (navigator.clipboard) {{
+                        navigator.clipboard.writeText(userInfo.token);
+                    }}
+                }}
+                
+                // 清空结果
+                function clearResult() {{
+                    document.getElementById('apiResult').textContent = '点击上方按钮测试API功能...';
+                }}
+                
+                // 退出登录
+                async function logout() {{
+                    if (confirm('确定要退出登录吗?')) {{
+                        try {{
+                            // 调用退出API
+                            await fetch('/auth/logout', {{
+                                method: 'GET',
+                                headers: {{
+                                    'Authorization': `Bearer ${{userInfo.token}}`
+                                }}
+                            }});
+                        }} catch (error) {{
+                            console.log('退出API调用失败:', error);
+                        }} finally {{
+                            // 清除本地存储
+                            localStorage.removeItem('access_token');
+                            localStorage.removeItem('user_info');
+                            
+                            // 重定向到登录页面
+                            window.location.href = '/auth/login';
+                        }}
+                    }}
+                }}
+                
+                // 页面加载完成后的初始化
+                document.addEventListener('DOMContentLoaded', function() {{
+                    console.log('首页加载完成');
+                    console.log('用户信息:', userInfo);
+                    
+                    // 如果没有token,重定向到登录页面
+                    if (!userInfo.token) {{
+                        alert('未检测到有效的登录信息,请重新登录');
+                        window.location.href = '/auth/login';
+                    }}
+                }});
+            </script>
+        </body>
+        </html>
+        """
+        
+        from fastapi.responses import HTMLResponse
+        return HTMLResponse(content=home_html)
+        
+    except Exception as e:
+        print(f"❌ 首页错误: {e}")
+        return {"error": "server_error", "message": "首页加载失败"}
+
+@app.get("/auth/logout")
+async def logout(credentials: HTTPAuthorizationCredentials = Depends(security)):
+    """用户登出"""
+    token = credentials.credentials
+    
+    # 清除本地缓存
+    user_id = access_tokens.pop(token, None)
+    if user_id:
+        user_sessions.pop(user_id, None)
+    
+    # 可选:通知SSO服务撤销token
+    async with httpx.AsyncClient() as client:
+        try:
+            await client.post(
+                f"{SSO_BASE_URL}/oauth/revoke",
+                data={"token": token, "token_type_hint": "access_token"}
+            )
+        except:
+            pass  # 忽略错误
+    
+    return {"message": "登出成功"}
+
+@app.get("/api/user/profile")
+async def get_user_profile(current_user: UserInfo = Depends(get_current_user)):
+    """获取用户资料"""
+    return {
+        "code": 0,
+        "message": "获取用户资料成功",
+        "data": current_user.dict()
+    }
+
+@app.get("/api/products")
+async def get_products(current_user: UserInfo = Depends(get_current_user)):
+    """获取产品列表(示例业务接口)"""
+    # 模拟产品数据
+    products = [
+        {"id": 1, "name": "产品A", "price": 100.0, "description": "这是产品A"},
+        {"id": 2, "name": "产品B", "price": 200.0, "description": "这是产品B"},
+        {"id": 3, "name": "产品C", "price": 300.0, "description": "这是产品C"},
+    ]
+    
+    return {
+        "code": 0,
+        "message": "获取产品列表成功",
+        "data": {
+            "products": products,
+            "total": len(products),
+            "user": current_user.username
+        }
+    }
+
+@app.get("/api/orders")
+async def get_orders(current_user: UserInfo = Depends(get_current_user)):
+    """获取订单列表(示例业务接口)"""
+    # 模拟订单数据
+    orders = [
+        {
+            "id": 1,
+            "product_name": "产品A",
+            "quantity": 2,
+            "total_price": 200.0,
+            "status": "已完成",
+            "created_at": "2024-01-15 10:30:00"
+        },
+        {
+            "id": 2,
+            "product_name": "产品B",
+            "quantity": 1,
+            "total_price": 200.0,
+            "status": "处理中",
+            "created_at": "2024-01-15 14:20:00"
+        }
+    ]
+    
+    return {
+        "code": 0,
+        "message": "获取订单列表成功",
+        "data": {
+            "orders": orders,
+            "total": len(orders),
+            "user": current_user.username
+        }
+    }
+
+if __name__ == "__main__":
+    import uvicorn
+    uvicorn.run(app, host="0.0.0.0", port=8001)

+ 19 - 0
backend/requirements.txt

@@ -0,0 +1,19 @@
+# FastAPI核心依赖
+fastapi==0.104.1
+uvicorn[standard]==0.24.0
+python-multipart==0.0.6
+
+# 数据库相关
+sqlalchemy==2.0.23
+aiomysql==0.2.0
+
+# 认证和安全
+python-jose[cryptography]==3.3.0
+httpx==0.25.2
+
+# 配置管理
+pydantic==2.5.0
+pydantic-settings==2.1.0
+
+# 开发工具
+python-dotenv==1.0.0

+ 24 - 0
frontend/.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 13 - 0
frontend/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8">
+    <link rel="icon" href="/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>子系统示例</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 1558 - 0
frontend/package-lock.json

@@ -0,0 +1,1558 @@
+{
+  "name": "subsystem-demo-frontend",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "subsystem-demo-frontend",
+      "version": "1.0.0",
+      "dependencies": {
+        "@element-plus/icons-vue": "^2.1.0",
+        "axios": "^1.6.0",
+        "element-plus": "^2.4.2",
+        "js-cookie": "^3.0.5",
+        "pinia": "^2.1.7",
+        "vue": "^3.3.8",
+        "vue-router": "^4.2.5"
+      },
+      "devDependencies": {
+        "@types/js-cookie": "^3.0.6",
+        "@types/node": "^20.8.10",
+        "@vitejs/plugin-vue": "^4.4.1",
+        "typescript": "~5.2.0",
+        "vite": "^4.5.0",
+        "vue-tsc": "^1.8.22"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+      "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.28.5"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+      "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+      "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@element-plus/icons-vue": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+      "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+      "license": "MIT",
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+      "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+      "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+      "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+      "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+      "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+      "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+      "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+      "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+      "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+      "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+      "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+      "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+      "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+      "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+      "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+      "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+      "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+      "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+      "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+      "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+      "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+      "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@floating-ui/core": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+      "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.10"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+      "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/core": "^1.7.3",
+        "@floating-ui/utils": "^0.2.10"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.10",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+      "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@popperjs/core": {
+      "name": "@sxzz/popperjs-es",
+      "version": "2.11.7",
+      "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
+      "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
+    "node_modules/@types/js-cookie": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
+      "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/lodash-es": {
+      "version": "4.17.12",
+      "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+      "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/lodash": "*"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "20.19.27",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
+      "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.20",
+      "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+      "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+      "license": "MIT"
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
+      "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^4.0.0 || ^5.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@volar/language-core": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz",
+      "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/source-map": "1.11.1"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz",
+      "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "muggle-string": "^0.3.1"
+      }
+    },
+    "node_modules/@volar/typescript": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz",
+      "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "1.11.1",
+        "path-browserify": "^1.0.1"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz",
+      "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.28.5",
+        "@vue/shared": "3.5.26",
+        "entities": "^7.0.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz",
+      "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.26",
+        "@vue/shared": "3.5.26"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz",
+      "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.28.5",
+        "@vue/compiler-core": "3.5.26",
+        "@vue/compiler-dom": "3.5.26",
+        "@vue/compiler-ssr": "3.5.26",
+        "@vue/shared": "3.5.26",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.6",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz",
+      "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.26",
+        "@vue/shared": "3.5.26"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/language-core": {
+      "version": "1.8.27",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz",
+      "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "~1.11.1",
+        "@volar/source-map": "~1.11.1",
+        "@vue/compiler-dom": "^3.3.0",
+        "@vue/shared": "^3.3.0",
+        "computeds": "^0.0.1",
+        "minimatch": "^9.0.3",
+        "muggle-string": "^0.3.1",
+        "path-browserify": "^1.0.1",
+        "vue-template-compiler": "^2.7.14"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz",
+      "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.26"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz",
+      "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.26",
+        "@vue/shared": "3.5.26"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz",
+      "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.26",
+        "@vue/runtime-core": "3.5.26",
+        "@vue/shared": "3.5.26",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz",
+      "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.26",
+        "@vue/shared": "3.5.26"
+      },
+      "peerDependencies": {
+        "vue": "3.5.26"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz",
+      "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==",
+      "license": "MIT"
+    },
+    "node_modules/@vueuse/core": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
+      "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.20",
+        "@vueuse/metadata": "10.11.1",
+        "@vueuse/shared": "10.11.1",
+        "vue-demi": ">=0.14.8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
+      "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
+      "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
+      "license": "MIT",
+      "dependencies": {
+        "vue-demi": ">=0.14.8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+      "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.4",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/computeds": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
+      "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.19",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
+      "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
+      "license": "MIT"
+    },
+    "node_modules/de-indent": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+      "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/element-plus": {
+      "version": "2.13.0",
+      "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.0.tgz",
+      "integrity": "sha512-qjxS+SBChvqCl6lU6ShiliLMN6WqFHiXQENYbAY3GKNflG+FS3jqn8JmQq0CBZq4koFqsi95NT1M6SL4whZfrA==",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^3.4.1",
+        "@element-plus/icons-vue": "^2.3.2",
+        "@floating-ui/dom": "^1.0.1",
+        "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
+        "@types/lodash": "^4.17.20",
+        "@types/lodash-es": "^4.17.12",
+        "@vueuse/core": "^10.11.0",
+        "async-validator": "^4.2.5",
+        "dayjs": "^1.11.19",
+        "lodash": "^4.17.21",
+        "lodash-es": "^4.17.21",
+        "lodash-unified": "^1.0.3",
+        "memoize-one": "^6.0.0",
+        "normalize-wheel-es": "^1.2.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.3.0"
+      }
+    },
+    "node_modules/entities": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz",
+      "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.18.20",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+      "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/android-arm": "0.18.20",
+        "@esbuild/android-arm64": "0.18.20",
+        "@esbuild/android-x64": "0.18.20",
+        "@esbuild/darwin-arm64": "0.18.20",
+        "@esbuild/darwin-x64": "0.18.20",
+        "@esbuild/freebsd-arm64": "0.18.20",
+        "@esbuild/freebsd-x64": "0.18.20",
+        "@esbuild/linux-arm": "0.18.20",
+        "@esbuild/linux-arm64": "0.18.20",
+        "@esbuild/linux-ia32": "0.18.20",
+        "@esbuild/linux-loong64": "0.18.20",
+        "@esbuild/linux-mips64el": "0.18.20",
+        "@esbuild/linux-ppc64": "0.18.20",
+        "@esbuild/linux-riscv64": "0.18.20",
+        "@esbuild/linux-s390x": "0.18.20",
+        "@esbuild/linux-x64": "0.18.20",
+        "@esbuild/netbsd-x64": "0.18.20",
+        "@esbuild/openbsd-x64": "0.18.20",
+        "@esbuild/sunos-x64": "0.18.20",
+        "@esbuild/win32-arm64": "0.18.20",
+        "@esbuild/win32-ia32": "0.18.20",
+        "@esbuild/win32-x64": "0.18.20"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "he": "bin/he"
+      }
+    },
+    "node_modules/js-cookie": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+      "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-es": {
+      "version": "4.17.22",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
+      "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-unified": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
+      "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/lodash-es": "*",
+        "lodash": "*",
+        "lodash-es": "*"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/memoize-one": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+      "license": "MIT"
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/muggle-string": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz",
+      "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/normalize-wheel-es": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+      "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/pinia": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
+      "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.3",
+        "vue-demi": "^0.14.10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.4.4",
+        "vue": "^2.7.0 || ^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.6",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
+    "node_modules/rollup": {
+      "version": "3.29.5",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
+      "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=14.18.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.7.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+      "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
+      "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vite": {
+      "version": "4.5.14",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
+      "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.18.10",
+        "postcss": "^8.4.27",
+        "rollup": "^3.27.1"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      },
+      "peerDependencies": {
+        "@types/node": ">= 14",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.5.26",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
+      "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.26",
+        "@vue/compiler-sfc": "3.5.26",
+        "@vue/runtime-dom": "3.5.26",
+        "@vue/server-renderer": "3.5.26",
+        "@vue/shared": "3.5.26"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "4.6.4",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+      "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/vue-template-compiler": {
+      "version": "2.7.16",
+      "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
+      "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "de-indent": "^1.0.2",
+        "he": "^1.2.0"
+      }
+    },
+    "node_modules/vue-tsc": {
+      "version": "1.8.27",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz",
+      "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/typescript": "~1.11.1",
+        "@vue/language-core": "1.8.27",
+        "semver": "^7.5.4"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      }
+    }
+  }
+}

+ 28 - 0
frontend/package.json

@@ -0,0 +1,28 @@
+{
+  "name": "subsystem-demo-frontend",
+  "version": "1.0.0",
+  "description": "子系统前端示例",
+  "type": "module",
+  "scripts": {
+    "dev": "vite --port 3001",
+    "build": "vue-tsc && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "vue": "^3.3.8",
+    "vue-router": "^4.2.5",
+    "pinia": "^2.1.7",
+    "axios": "^1.6.0",
+    "element-plus": "^2.4.2",
+    "@element-plus/icons-vue": "^2.1.0",
+    "js-cookie": "^3.0.5"
+  },
+  "devDependencies": {
+    "@types/node": "^20.8.10",
+    "@types/js-cookie": "^3.0.6",
+    "@vitejs/plugin-vue": "^4.4.1",
+    "typescript": "~5.2.0",
+    "vite": "^4.5.0",
+    "vue-tsc": "^1.8.22"
+  }
+}

+ 38 - 0
frontend/src/App.vue

@@ -0,0 +1,38 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted } from 'vue'
+import { useAuthStore } from '@/stores/auth'
+
+const authStore = useAuthStore()
+
+onMounted(() => {
+  // 检查登录状态
+  authStore.checkAuth()
+})
+</script>
+
+<style>
+#app {
+  font-family: Avenir, Helvetica, Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  color: #2c3e50;
+  height: 100vh;
+}
+
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+  background-color: #f5f5f5;
+}
+</style>

+ 21 - 0
frontend/src/main.ts

@@ -0,0 +1,21 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import ElementPlus from 'element-plus'
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+import 'element-plus/dist/index.css'
+
+import App from './App.vue'
+import router from './router'
+
+const app = createApp(App)
+
+// 注册Element Plus图标
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
+
+app.use(createPinia())
+app.use(router)
+app.use(ElementPlus)
+
+app.mount('#app')

+ 67 - 0
frontend/src/router/index.ts

@@ -0,0 +1,67 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import type { RouteRecordRaw } from 'vue-router'
+import { useAuthStore } from '@/stores/auth'
+
+const routes: RouteRecordRaw[] = [
+  {
+    path: '/',
+    redirect: '/dashboard'
+  },
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('@/views/Login.vue'),
+    meta: { requiresGuest: true }
+  },
+  {
+    path: '/auth/callback',
+    name: 'AuthCallback',
+    component: () => import('@/views/AuthCallback.vue')
+  },
+  {
+    path: '/dashboard',
+    name: 'Dashboard',
+    component: () => import('@/views/Dashboard.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/products',
+    name: 'Products',
+    component: () => import('@/views/Products.vue'),
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/orders',
+    name: 'Orders',
+    component: () => import('@/views/Orders.vue'),
+    meta: { requiresAuth: true }
+  }
+]
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes
+})
+
+// 路由守卫
+router.beforeEach(async (to, from, next) => {
+  const authStore = useAuthStore()
+  
+  // 检查是否需要认证
+  if (to.meta.requiresAuth) {
+    if (!authStore.isAuthenticated) {
+      next({ name: 'Login' })
+      return
+    }
+  }
+  
+  // 检查是否需要访客状态
+  if (to.meta.requiresGuest && authStore.isAuthenticated) {
+    next({ name: 'Dashboard' })
+    return
+  }
+  
+  next()
+})
+
+export default router

+ 94 - 0
frontend/src/stores/auth.ts

@@ -0,0 +1,94 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import axios from 'axios'
+
+interface User {
+  id: string
+  username: string
+  email: string
+  phone?: string
+  avatar_url?: string
+  is_active: boolean
+}
+
+export const useAuthStore = defineStore('auth', () => {
+  const user = ref<User | null>(null)
+  const token = ref<string | null>(localStorage.getItem('access_token'))
+  const loading = ref(false)
+
+  const isAuthenticated = computed(() => !!token.value && !!user.value)
+
+  // 设置token
+  const setToken = (newToken: string) => {
+    token.value = newToken
+    localStorage.setItem('access_token', newToken)
+    
+    // 设置axios默认header
+    axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`
+  }
+
+  // 清除token
+  const clearToken = () => {
+    token.value = null
+    user.value = null
+    localStorage.removeItem('access_token')
+    delete axios.defaults.headers.common['Authorization']
+  }
+
+  // 获取用户信息
+  const fetchUserInfo = async () => {
+    if (!token.value) return
+    
+    try {
+      const response = await axios.get('/api/user/profile')
+      if (response.data.code === 0) {
+        user.value = response.data.data
+      }
+    } catch (error) {
+      console.error('获取用户信息失败:', error)
+      clearToken()
+      throw error
+    }
+  }
+
+  // 检查认证状态
+  const checkAuth = async () => {
+    if (token.value && !user.value) {
+      try {
+        await fetchUserInfo()
+      } catch (error) {
+        clearToken()
+      }
+    }
+    
+    // 设置axios默认header
+    if (token.value) {
+      axios.defaults.headers.common['Authorization'] = `Bearer ${token.value}`
+    }
+  }
+
+  // 登出
+  const logout = async () => {
+    try {
+      if (token.value) {
+        await axios.get('/auth/logout')
+      }
+    } catch (error) {
+      console.error('登出失败:', error)
+    } finally {
+      clearToken()
+    }
+  }
+
+  return {
+    user,
+    token,
+    loading,
+    isAuthenticated,
+    setToken,
+    clearToken,
+    fetchUserInfo,
+    checkAuth,
+    logout
+  }
+})

+ 123 - 0
frontend/src/views/AuthCallback.vue

@@ -0,0 +1,123 @@
+<template>
+  <div class="callback-container">
+    <el-card class="callback-box">
+      <div v-if="loading" class="loading-content">
+        <el-icon class="is-loading" size="48"><Loading /></el-icon>
+        <h2>正在处理登录...</h2>
+        <p>请稍候,系统正在验证您的身份</p>
+      </div>
+      
+      <div v-else-if="error" class="error-content">
+        <el-icon size="48" color="#F56C6C"><Warning /></el-icon>
+        <h2>登录失败</h2>
+        <p>{{ error }}</p>
+        <el-button type="primary" @click="$router.push('/login')">
+          重新登录
+        </el-button>
+      </div>
+      
+      <div v-else class="success-content">
+        <el-icon size="48" color="#67C23A"><SuccessFilled /></el-icon>
+        <h2>登录成功</h2>
+        <p>正在跳转到系统首页...</p>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { useAuthStore } from '@/stores/auth'
+
+const router = useRouter()
+const route = useRoute()
+const authStore = useAuthStore()
+
+const loading = ref(true)
+const error = ref('')
+
+onMounted(async () => {
+  try {
+    // 从URL参数获取token
+    const token = route.query.token as string
+    const errorParam = route.query.error as string
+    
+    if (errorParam) {
+      error.value = '授权失败: ' + errorParam
+      loading.value = false
+      return
+    }
+    
+    if (!token) {
+      error.value = '未收到访问令牌'
+      loading.value = false
+      return
+    }
+    
+    // 保存token
+    authStore.setToken(token)
+    
+    // 获取用户信息
+    await authStore.fetchUserInfo()
+    
+    loading.value = false
+    
+    ElMessage.success('登录成功')
+    
+    // 延迟跳转,让用户看到成功提示
+    setTimeout(() => {
+      router.push('/dashboard')
+    }, 1000)
+    
+  } catch (err: any) {
+    console.error('处理登录回调失败:', err)
+    error.value = err.message || '登录处理失败'
+    loading.value = false
+  }
+})
+</script>
+
+<style scoped>
+.callback-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 100vh;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.callback-box {
+  width: 400px;
+  text-align: center;
+  padding: 40px;
+}
+
+.loading-content,
+.error-content,
+.success-content {
+  padding: 20px;
+}
+
+.loading-content h2,
+.error-content h2,
+.success-content h2 {
+  margin: 20px 0 10px 0;
+  color: #333;
+  font-size: 20px;
+  font-weight: 600;
+}
+
+.loading-content p,
+.error-content p,
+.success-content p {
+  margin: 10px 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.error-content .el-button {
+  margin-top: 20px;
+}
+</style>

+ 295 - 0
frontend/src/views/Dashboard.vue

@@ -0,0 +1,295 @@
+<template>
+  <div class="dashboard">
+    <el-container>
+      <!-- 头部 -->
+      <el-header class="header">
+        <div class="header-left">
+          <h1>子系统示例</h1>
+        </div>
+        <div class="header-right">
+          <el-dropdown @command="handleCommand">
+            <span class="user-info">
+              <el-avatar :src="authStore.user?.avatar_url" :size="32">
+                {{ authStore.user?.username?.charAt(0).toUpperCase() }}
+              </el-avatar>
+              <span class="username">{{ authStore.user?.username }}</span>
+              <el-icon><ArrowDown /></el-icon>
+            </span>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item command="profile">
+                  <el-icon><User /></el-icon>
+                  个人资料
+                </el-dropdown-item>
+                <el-dropdown-item divided command="logout">
+                  <el-icon><SwitchButton /></el-icon>
+                  退出登录
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </div>
+      </el-header>
+
+      <el-container>
+        <!-- 侧边栏 -->
+        <el-aside width="200px" class="sidebar">
+          <el-menu
+            :default-active="activeMenu"
+            class="sidebar-menu"
+            router
+          >
+            <el-menu-item index="/dashboard">
+              <el-icon><House /></el-icon>
+              <span>仪表盘</span>
+            </el-menu-item>
+            <el-menu-item index="/products">
+              <el-icon><Grid /></el-icon>
+              <span>产品管理</span>
+            </el-menu-item>
+            <el-menu-item index="/orders">
+              <el-icon><Document /></el-icon>
+              <span>订单管理</span>
+            </el-menu-item>
+          </el-menu>
+        </el-aside>
+
+        <!-- 主内容区 -->
+        <el-main class="main-content">
+          <div class="welcome-section">
+            <h2>欢迎使用子系统示例</h2>
+            <p>这是一个集成了SSO认证的子系统示例</p>
+            <p>当前用户:{{ authStore.user?.username }} ({{ authStore.user?.email }})</p>
+          </div>
+
+          <!-- 功能卡片 -->
+          <div class="feature-grid">
+            <el-card class="feature-card" @click="$router.push('/products')">
+              <div class="feature-content">
+                <el-icon size="48" color="#409EFF"><Grid /></el-icon>
+                <h3>产品管理</h3>
+                <p>管理系统中的产品信息</p>
+              </div>
+            </el-card>
+
+            <el-card class="feature-card" @click="$router.push('/orders')">
+              <div class="feature-content">
+                <el-icon size="48" color="#67C23A"><Document /></el-icon>
+                <h3>订单管理</h3>
+                <p>查看和管理用户订单</p>
+              </div>
+            </el-card>
+
+            <el-card class="feature-card">
+              <div class="feature-content">
+                <el-icon size="48" color="#E6A23C"><User /></el-icon>
+                <h3>用户中心</h3>
+                <p>管理个人信息和设置</p>
+              </div>
+            </el-card>
+          </div>
+
+          <!-- SSO集成说明 -->
+          <el-card class="info-card">
+            <template #header>
+              <span>SSO集成说明</span>
+            </template>
+            <div class="info-content">
+              <p>✅ 已成功集成SSO单点登录</p>
+              <p>✅ 用户信息自动同步</p>
+              <p>✅ 统一认证和授权</p>
+              <p>✅ 安全的令牌验证</p>
+              
+              <el-divider />
+              
+              <h4>技术特性:</h4>
+              <ul>
+                <li>OAuth 2.0 授权码模式</li>
+                <li>JWT令牌验证</li>
+                <li>自动令牌刷新</li>
+                <li>统一登出</li>
+              </ul>
+            </div>
+          </el-card>
+        </el-main>
+      </el-container>
+    </el-container>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useAuthStore } from '@/stores/auth'
+
+const router = useRouter()
+const route = useRoute()
+const authStore = useAuthStore()
+
+const activeMenu = computed(() => route.path)
+
+// 处理下拉菜单命令
+const handleCommand = async (command: string) => {
+  switch (command) {
+    case 'profile':
+      ElMessage.info('个人资料功能开发中...')
+      break
+    case 'logout':
+      try {
+        await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
+        
+        await authStore.logout()
+        ElMessage.success('已退出登录')
+        router.push('/login')
+      } catch (error) {
+        // 用户取消
+      }
+      break
+  }
+}
+</script>
+
+<style scoped>
+.dashboard {
+  height: 100vh;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background: #fff;
+  border-bottom: 1px solid #e6e6e6;
+  padding: 0 20px;
+}
+
+.header-left h1 {
+  margin: 0;
+  color: #333;
+  font-size: 20px;
+  font-weight: 600;
+}
+
+.header-right {
+  display: flex;
+  align-items: center;
+}
+
+.user-info {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  padding: 8px;
+  border-radius: 4px;
+  transition: background-color 0.3s;
+}
+
+.user-info:hover {
+  background-color: #f5f5f5;
+}
+
+.username {
+  font-size: 14px;
+  color: #333;
+}
+
+.sidebar {
+  background: #fff;
+  border-right: 1px solid #e6e6e6;
+}
+
+.sidebar-menu {
+  border-right: none;
+}
+
+.main-content {
+  background: #f5f5f5;
+  padding: 20px;
+}
+
+.welcome-section {
+  margin-bottom: 24px;
+  padding: 20px;
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.welcome-section h2 {
+  margin: 0 0 8px 0;
+  color: #333;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.welcome-section p {
+  margin: 4px 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.feature-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+  gap: 16px;
+  margin-bottom: 24px;
+}
+
+.feature-card {
+  cursor: pointer;
+  transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.feature-card:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.feature-content {
+  text-align: center;
+  padding: 20px;
+}
+
+.feature-content h3 {
+  margin: 16px 0 8px 0;
+  color: #333;
+  font-size: 18px;
+  font-weight: 600;
+}
+
+.feature-content p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.info-card {
+  margin-bottom: 24px;
+}
+
+.info-content p {
+  margin: 8px 0;
+  color: #333;
+}
+
+.info-content h4 {
+  margin: 16px 0 8px 0;
+  color: #333;
+}
+
+.info-content ul {
+  margin: 8px 0;
+  padding-left: 20px;
+}
+
+.info-content li {
+  margin: 4px 0;
+  color: #666;
+}
+</style>

+ 109 - 0
frontend/src/views/Login.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="login-container">
+    <div class="login-box">
+      <div class="login-header">
+        <h1>子系统示例</h1>
+        <p>请通过SSO认证中心登录</p>
+      </div>
+      
+      <div class="login-content">
+        <el-button 
+          type="primary" 
+          size="large" 
+          :loading="loading"
+          @click="handleSSOLogin"
+          class="sso-login-btn"
+        >
+          <el-icon><Key /></el-icon>
+          通过SSO登录
+        </el-button>
+        
+        <div class="login-info">
+          <p>点击上方按钮将跳转到SSO认证中心进行登录</p>
+          <p>登录成功后将自动返回本系统</p>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { useAuthStore } from '@/stores/auth'
+
+const router = useRouter()
+const authStore = useAuthStore()
+const loading = ref(false)
+
+const handleSSOLogin = () => {
+  loading.value = true
+  // 跳转到后端的SSO登录端点
+  window.location.href = '/auth/login'
+}
+
+// 如果已经登录,重定向到仪表盘
+if (authStore.isAuthenticated) {
+  router.push('/dashboard')
+}
+</script>
+
+<style scoped>
+.login-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 100vh;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.login-box {
+  width: 400px;
+  padding: 40px;
+  background: white;
+  border-radius: 10px;
+  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+  text-align: center;
+}
+
+.login-header {
+  margin-bottom: 30px;
+}
+
+.login-header h1 {
+  color: #333;
+  margin-bottom: 10px;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.login-header p {
+  color: #666;
+  font-size: 14px;
+}
+
+.login-content {
+  width: 100%;
+}
+
+.sso-login-btn {
+  width: 100%;
+  height: 48px;
+  font-size: 16px;
+  margin-bottom: 20px;
+}
+
+.login-info {
+  padding: 20px;
+  background: #f8f9fa;
+  border-radius: 8px;
+  border-left: 4px solid #409EFF;
+}
+
+.login-info p {
+  margin: 8px 0;
+  color: #666;
+  font-size: 14px;
+  line-height: 1.5;
+}
+</style>

+ 223 - 0
frontend/src/views/Orders.vue

@@ -0,0 +1,223 @@
+<template>
+  <div class="orders-page">
+    <div class="page-header">
+      <h2>订单管理</h2>
+      <p>查看和管理用户订单信息</p>
+    </div>
+
+    <el-card>
+      <template #header>
+        <div class="card-header">
+          <span>订单列表</span>
+          <el-button type="primary" @click="loadOrders">
+            <el-icon><Refresh /></el-icon>
+            刷新
+          </el-button>
+        </div>
+      </template>
+
+      <div v-loading="loading">
+        <el-table :data="orders" style="width: 100%">
+          <el-table-column prop="id" label="订单ID" width="100" />
+          <el-table-column prop="product_name" label="产品名称" />
+          <el-table-column prop="quantity" label="数量" width="100" />
+          <el-table-column prop="total_price" label="总价" width="120">
+            <template #default="{ row }">
+              ¥{{ row.total_price.toFixed(2) }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="status" label="状态" width="100">
+            <template #default="{ row }">
+              <el-tag :type="getStatusType(row.status)">
+                {{ row.status }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="created_at" label="创建时间" width="180" />
+          <el-table-column label="操作" width="200">
+            <template #default="{ row }">
+              <el-button size="small" @click="viewOrder(row)">
+                查看详情
+              </el-button>
+              <el-button 
+                size="small" 
+                type="warning" 
+                v-if="row.status === '处理中'"
+                @click="cancelOrder(row)"
+              >
+                取消
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <div v-if="!loading && orders.length === 0" class="empty-state">
+          <el-empty description="暂无订单数据" />
+        </div>
+      </div>
+    </el-card>
+
+    <!-- 统计信息 -->
+    <div class="stats-grid">
+      <el-card class="stats-card">
+        <div class="stats-content">
+          <div class="stats-number">{{ orders.length }}</div>
+          <div class="stats-label">总订单数</div>
+        </div>
+      </el-card>
+
+      <el-card class="stats-card">
+        <div class="stats-content">
+          <div class="stats-number">{{ completedOrders }}</div>
+          <div class="stats-label">已完成</div>
+        </div>
+      </el-card>
+
+      <el-card class="stats-card">
+        <div class="stats-content">
+          <div class="stats-number">{{ processingOrders }}</div>
+          <div class="stats-label">处理中</div>
+        </div>
+      </el-card>
+
+      <el-card class="stats-card">
+        <div class="stats-content">
+          <div class="stats-number">¥{{ totalAmount.toFixed(2) }}</div>
+          <div class="stats-label">总金额</div>
+        </div>
+      </el-card>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import axios from 'axios'
+
+const loading = ref(false)
+const orders = ref<any[]>([])
+
+// 计算属性
+const completedOrders = computed(() => 
+  orders.value.filter(order => order.status === '已完成').length
+)
+
+const processingOrders = computed(() => 
+  orders.value.filter(order => order.status === '处理中').length
+)
+
+const totalAmount = computed(() => 
+  orders.value.reduce((sum, order) => sum + order.total_price, 0)
+)
+
+const loadOrders = async () => {
+  loading.value = true
+  try {
+    const response = await axios.get('/api/orders')
+    if (response.data.code === 0) {
+      orders.value = response.data.data.orders
+      ElMessage.success('订单列表加载成功')
+    } else {
+      ElMessage.error(response.data.message || '加载失败')
+    }
+  } catch (error: any) {
+    console.error('加载订单列表失败:', error)
+    ElMessage.error('加载订单列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const getStatusType = (status: string) => {
+  switch (status) {
+    case '已完成':
+      return 'success'
+    case '处理中':
+      return 'warning'
+    case '已取消':
+      return 'danger'
+    default:
+      return 'info'
+  }
+}
+
+const viewOrder = (order: any) => {
+  ElMessage.info(`查看订单详情: ${order.id}`)
+}
+
+const cancelOrder = (order: any) => {
+  ElMessage.info(`取消订单: ${order.id}`)
+}
+
+onMounted(() => {
+  loadOrders()
+})
+</script>
+
+<style scoped>
+.orders-page {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 20px;
+}
+
+.page-header h2 {
+  margin: 0 0 8px 0;
+  color: #333;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.page-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.empty-state {
+  padding: 40px 0;
+}
+
+.stats-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+  gap: 16px;
+  margin-top: 20px;
+}
+
+.stats-card {
+  cursor: pointer;
+  transition: transform 0.2s;
+}
+
+.stats-card:hover {
+  transform: translateY(-2px);
+}
+
+.stats-content {
+  text-align: center;
+  padding: 20px;
+}
+
+.stats-number {
+  font-size: 24px;
+  font-weight: 600;
+  color: #333;
+  line-height: 1;
+}
+
+.stats-label {
+  font-size: 14px;
+  color: #666;
+  margin-top: 8px;
+}
+</style>

+ 146 - 0
frontend/src/views/Products.vue

@@ -0,0 +1,146 @@
+<template>
+  <div class="products-page">
+    <div class="page-header">
+      <h2>产品管理</h2>
+      <p>这里展示了通过SSO认证保护的业务接口数据</p>
+    </div>
+
+    <el-card>
+      <template #header>
+        <div class="card-header">
+          <span>产品列表</span>
+          <el-button type="primary" @click="loadProducts">
+            <el-icon><Refresh /></el-icon>
+            刷新
+          </el-button>
+        </div>
+      </template>
+
+      <div v-loading="loading">
+        <el-table :data="products" style="width: 100%">
+          <el-table-column prop="id" label="ID" width="80" />
+          <el-table-column prop="name" label="产品名称" />
+          <el-table-column prop="price" label="价格">
+            <template #default="{ row }">
+              ¥{{ row.price.toFixed(2) }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="description" label="描述" />
+          <el-table-column label="操作" width="200">
+            <template #default="{ row }">
+              <el-button size="small" @click="viewProduct(row)">
+                查看
+              </el-button>
+              <el-button size="small" type="primary" @click="editProduct(row)">
+                编辑
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <div v-if="!loading && products.length === 0" class="empty-state">
+          <el-empty description="暂无产品数据" />
+        </div>
+      </div>
+    </el-card>
+
+    <!-- 用户信息展示 -->
+    <el-card class="user-info-card">
+      <template #header>
+        <span>当前用户信息</span>
+      </template>
+      <div class="user-info">
+        <p><strong>用户名:</strong> {{ authStore.user?.username }}</p>
+        <p><strong>邮箱:</strong> {{ authStore.user?.email }}</p>
+        <p><strong>状态:</strong> 
+          <el-tag :type="authStore.user?.is_active ? 'success' : 'danger'">
+            {{ authStore.user?.is_active ? '活跃' : '非活跃' }}
+          </el-tag>
+        </p>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { useAuthStore } from '@/stores/auth'
+import axios from 'axios'
+
+const authStore = useAuthStore()
+const loading = ref(false)
+const products = ref<any[]>([])
+
+const loadProducts = async () => {
+  loading.value = true
+  try {
+    const response = await axios.get('/api/products')
+    if (response.data.code === 0) {
+      products.value = response.data.data.products
+      ElMessage.success('产品列表加载成功')
+    } else {
+      ElMessage.error(response.data.message || '加载失败')
+    }
+  } catch (error: any) {
+    console.error('加载产品列表失败:', error)
+    ElMessage.error('加载产品列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const viewProduct = (product: any) => {
+  ElMessage.info(`查看产品: ${product.name}`)
+}
+
+const editProduct = (product: any) => {
+  ElMessage.info(`编辑产品: ${product.name}`)
+}
+
+onMounted(() => {
+  loadProducts()
+})
+</script>
+
+<style scoped>
+.products-page {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 20px;
+}
+
+.page-header h2 {
+  margin: 0 0 8px 0;
+  color: #333;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.page-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.empty-state {
+  padding: 40px 0;
+}
+
+.user-info-card {
+  margin-top: 20px;
+}
+
+.user-info p {
+  margin: 8px 0;
+  color: #333;
+}
+</style>

+ 26 - 0
frontend/vite.config.ts

@@ -0,0 +1,26 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import { resolve } from 'path'
+
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    alias: {
+      '@': resolve(__dirname, 'src'),
+    },
+  },
+  server: {
+    port: 3001,
+    host: true,
+    proxy: {
+      '/api': {
+        target: 'http://localhost:8001',
+        changeOrigin: true,
+      },
+      '/auth': {
+        target: 'http://localhost:8001',
+        changeOrigin: true,
+      },
+    },
+  },
+})