Browse Source

feat: optimize tabs & token.guard

ryanemax 6 days ago
parent
commit
51db309fe0

+ 481 - 0
example-book/5.4.home.tml

@@ -0,0 +1,481 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+    <title>灵犀问诊AI项目首页原型设计</title>
+    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css">
+    <style>
+        /* 苹果HIG风格基础设置 */
+        :root {
+            --ios-bg: #f2f2f7;
+            --ios-card: #ffffff;
+            --ios-primary: #007aff;
+            --ios-danger: #ff3b30;
+            --ios-warning: #ff9500;
+            --ios-success: #34c759;
+            --ios-text-primary: #1c1c1e;
+            --ios-text-secondary: #636366;
+            --ios-border: #d1d1d6;
+            --ios-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+        }
+
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+            -webkit-tap-highlight-color: transparent;
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
+        }
+
+        body {
+            background: var(--ios-bg);
+            color: var(--ios-text-primary);
+            line-height: 1.5;
+            padding-bottom: env(safe-area-inset-bottom);
+        }
+
+        /* 状态栏区域 */
+        .status-bar {
+            padding: 12px 16px 8px;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            font-size: 14px;
+            color: var(--ios-text-secondary);
+        }
+
+        .emergency-btn {
+            position: fixed;
+            top: 16px;
+            right: 16px;
+            width: 44px;
+            height: 44px;
+            background: var(--ios-danger);
+            border-radius: 22px;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            color: white;
+            box-shadow: var(--ios-shadow);
+            z-index: 100;
+            animation: pulse 2s infinite;
+        }
+
+        @keyframes pulse {
+            0%, 100% { transform: scale(1); }
+            50% { transform: scale(1.1); }
+        }
+
+        /* 用户信息卡 */
+        .user-card {
+            background: var(--ios-card);
+            margin: 16px;
+            padding: 20px;
+            border-radius: 12px;
+            box-shadow: var(--ios-shadow);
+            display: flex;
+            align-items: center;
+        }
+
+        .user-avatar {
+            width: 56px;
+            height: 56px;
+            background: linear-gradient(135deg, #64d2ff 0%, #5e5ce6 100%);
+            border-radius: 28px;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            color: white;
+            font-size: 24px;
+            font-weight: 500;
+            margin-right: 16px;
+        }
+
+        .user-info {
+            flex: 1;
+        }
+
+        .user-name {
+            font-size: 18px;
+            font-weight: 600;
+            margin-bottom: 4px;
+        }
+
+        .health-status {
+            display: inline-block;
+            padding: 4px 10px;
+            background: #e5f9e7;
+            color: var(--ios-success);
+            border-radius: 10px;
+            font-size: 13px;
+            font-weight: 500;
+        }
+
+        /* 症状输入区 */
+        .symptom-input {
+            margin: 0 16px 24px;
+        }
+
+        .input-container {
+            position: relative;
+            background: var(--ios-card);
+            border-radius: 12px;
+            box-shadow: var(--ios-shadow);
+            padding: 16px;
+        }
+
+        .input-field {
+            width: 100%;
+            border: none;
+            outline: none;
+            font-size: 16px;
+            padding: 12px 16px;
+            background: #f7f7f8;
+            border-radius: 10px;
+            color: var(--ios-text-primary);
+        }
+
+        .input-actions {
+            display: flex;
+            margin-top: 12px;
+        }
+
+        .action-btn {
+            flex: 1;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            padding: 8px;
+            color: var(--ios-primary);
+            font-size: 14px;
+        }
+
+        .action-btn i {
+            margin-right: 6px;
+        }
+
+        /* 紧急服务区 */
+        .emergency-services {
+            margin: 0 16px 24px;
+        }
+
+        .section-title {
+            font-size: 20px;
+            font-weight: 600;
+            margin-bottom: 16px;
+            padding-left: 4px;
+        }
+
+        .service-grid {
+            display: grid;
+            grid-template-columns: repeat(2, 1fr);
+            gap: 12px;
+        }
+
+        .service-card {
+            background: var(--ios-card);
+            border-radius: 12px;
+            padding: 20px 16px;
+            box-shadow: var(--ios-shadow);
+            text-align: center;
+            transition: transform 0.2s;
+        }
+
+        .service-card:active {
+            transform: scale(0.98);
+        }
+
+        .service-icon {
+            width: 40px;
+            height: 40px;
+            background: linear-gradient(135deg, #ffd426 0%, #ff375f 100%);
+            border-radius: 20px;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            color: white;
+            margin: 0 auto 12px;
+            font-size: 18px;
+        }
+
+        .service-name {
+            font-size: 16px;
+            font-weight: 500;
+            margin-bottom: 4px;
+        }
+
+        .service-desc {
+            font-size: 13px;
+            color: var(--ios-text-secondary);
+        }
+
+        /* 健康数据区 */
+        .health-data {
+            margin: 0 16px 24px;
+        }
+
+        .data-card {
+            background: var(--ios-card);
+            border-radius: 12px;
+            padding: 16px;
+            box-shadow: var(--ios-shadow);
+        }
+
+        .data-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-bottom: 16px;
+        }
+
+        .data-title {
+            font-size: 17px;
+            font-weight: 600;
+        }
+
+        .data-more {
+            color: var(--ios-primary);
+            font-size: 15px;
+        }
+
+        .data-grid {
+            display: grid;
+            grid-template-columns: repeat(3, 1fr);
+            gap: 12px;
+        }
+
+        .data-item {
+            text-align: center;
+        }
+
+        .data-value {
+            font-size: 22px;
+            font-weight: 600;
+            margin-bottom: 2px;
+        }
+
+        .data-label {
+            font-size: 13px;
+            color: var(--ios-text-secondary);
+        }
+
+        /* 底部导航 */
+        .tab-bar {
+            position: fixed;
+            bottom: 0;
+            left: 0;
+            right: 0;
+            background: rgba(255, 255, 255, 0.9);
+            backdrop-filter: blur(20px);
+            display: flex;
+            padding: 10px 0;
+            padding-bottom: calc(10px + env(safe-area-inset-bottom));
+            border-top: 1px solid var(--ios-border);
+        }
+
+        .tab-item {
+            flex: 1;
+            text-align: center;
+            color: var(--ios-text-secondary);
+            font-size: 12px;
+        }
+
+        .tab-item.active {
+            color: var(--ios-primary);
+        }
+
+        .tab-icon {
+            font-size: 22px;
+            margin-bottom: 2px;
+        }
+
+        /* 医疗特殊样式 */
+        .medical-warning {
+            color: var(--ios-danger);
+            font-weight: 500;
+        }
+
+        .medical-normal {
+            color: var(--ios-success);
+        }
+
+        /* 响应式调整 */
+        @media (min-width: 768px) {
+            body {
+                max-width: 500px;
+                margin: 0 auto;
+                border-left: 1px solid var(--ios-border);
+                border-right: 1px solid var(--ios-border);
+            }
+        }
+    </style>
+</head>
+<body>
+    <!-- 状态栏 -->
+    <div class="status-bar">
+        <span id="current-time">下午2:30</span>
+        <span><i class="fas fa-wifi"></i> 中国移动</span>
+    </div>
+
+    <!-- 紧急按钮 -->
+    <button class="emergency-btn" id="emergencyBtn">
+        <i class="fas fa-plus"></i>
+    </button>
+
+    <!-- 用户信息 -->
+    <div class="user-card">
+        <div class="user-avatar">张</div>
+        <div class="user-info">
+            <div class="user-name">下午好,张先生</div>
+            <div class="health-status"><i class="fas fa-heartbeat"></i> 健康状况良好</div>
+        </div>
+    </div>
+
+    <!-- 症状输入 -->
+    <div class="symptom-input">
+        <div class="input-container">
+            <input type="text" class="input-field" placeholder="描述症状或上传报告...">
+            <div class="input-actions">
+                <div class="action-btn">
+                    <i class="fas fa-microphone"></i> 语音
+                </div>
+                <div class="action-btn">
+                    <i class="fas fa-camera"></i> 拍照
+                </div>
+                <div class="action-btn">
+                    <i class="fas fa-history"></i> 历史
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- 紧急服务 -->
+    <div class="emergency-services">
+        <h2 class="section-title">紧急服务</h2>
+        <div class="service-grid">
+            <div class="service-card" id="fastBtn">
+                <div class="service-icon">
+                    <i class="fas fa-brain"></i>
+                </div>
+                <div class="service-name">卒中评估</div>
+                <div class="service-desc">FAST快速筛查</div>
+            </div>
+            <div class="service-card" id="poisonBtn">
+                <div class="service-icon" style="background: linear-gradient(135deg, #32d74b 0%, #0a84ff 100%);">
+                    <i class="fas fa-biohazard"></i>
+                </div>
+                <div class="service-name">中毒咨询</div>
+                <div class="service-desc">24小时在线</div>
+            </div>
+        </div>
+    </div>
+
+    <!-- 健康数据 -->
+    <div class="health-data">
+        <h2 class="section-title">健康数据</h2>
+        <div class="data-card">
+            <div class="data-header">
+                <div class="data-title">今日健康指标</div>
+                <div class="data-more">更多 <i class="fas fa-chevron-right"></i></div>
+            </div>
+            <div class="data-grid">
+                <div class="data-item">
+                    <div class="data-value medical-normal">72</div>
+                    <div class="data-label">心率</div>
+                </div>
+                <div class="data-item">
+                    <div class="data-value">118/78</div>
+                    <div class="data-label">血压</div>
+                </div>
+                <div class="data-item">
+                    <div class="data-value medical-warning">6.2</div>
+                    <div class="data-label">血糖</div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- 底部导航 -->
+    <div class="tab-bar">
+        <div class="tab-item active">
+            <div class="tab-icon"><i class="fas fa-home"></i></div>
+            <div>首页</div>
+        </div>
+        <div class="tab-item">
+            <div class="tab-icon"><i class="fas fa-comment-medical"></i></div>
+            <div>问诊</div>
+        </div>
+        <div class="tab-item">
+            <div class="tab-icon"><i class="fas fa-pills"></i></div>
+            <div>药品</div>
+        </div>
+        <div class="tab-item">
+            <div class="tab-icon"><i class="fas fa-user-md"></i></div>
+            <div>我的</div>
+        </div>
+    </div>
+
+    <script>
+        // 更新时间显示
+        function updateTime() {
+            const now = new Date();
+            const hours = now.getHours();
+            const minutes = now.getMinutes().toString().padStart(2, '0');
+            const period = hours >= 12 ? '下午' : '上午';
+            const displayHours = hours > 12 ? hours - 12 : hours;
+            document.getElementById('current-time').textContent = `${period}${displayHours}:${minutes}`;
+        }
+        setInterval(updateTime, 1000);
+        updateTime();
+
+        // 紧急按钮交互
+        document.getElementById('emergencyBtn').addEventListener('click', function() {
+            // iOS风格震动反馈
+            if (window.navigator.vibrate) {
+                window.navigator.vibrate([50, 50, 50]);
+            }
+            
+            // 显示紧急菜单
+            alert('已触发紧急联络\n将通知您的紧急联系人');
+        });
+
+        // 卒中评估按钮
+        document.getElementById('fastBtn').addEventListener('click', function() {
+            alert('FAST评估指南:\nF-面部下垂\nA-手臂无力\nS-言语困难\nT-及时就医');
+        });
+
+        // 中毒咨询按钮
+        document.getElementById('poisonBtn').addEventListener('click', function() {
+            alert('中毒应急处理:\n1. 立即脱离毒源\n2. 保留毒物样本\n3. 拨打120');
+        });
+
+        // 输入框聚焦效果
+        const inputField = document.querySelector('.input-field');
+        inputField.addEventListener('focus', function() {
+            this.parentElement.style.boxShadow = '0 0 0 2px rgba(0, 122, 255, 0.3)';
+        });
+        inputField.addEventListener('blur', function() {
+            this.parentElement.style.boxShadow = 'var(--ios-shadow)';
+        });
+
+        // 模拟健康数据更新
+        setInterval(() => {
+            const heartRate = Math.floor(65 + Math.random() * 10);
+            const bloodPressure = `${110 + Math.floor(Math.random() * 10)}/${70 + Math.floor(Math.random() * 8)}`;
+            
+            document.querySelectorAll('.data-value')[0].textContent = heartRate;
+            document.querySelectorAll('.data-value')[1].textContent = bloodPressure;
+            
+            // 心率异常提示
+            if (heartRate > 75) {
+                document.querySelectorAll('.data-value')[0].classList.add('medical-warning');
+                document.querySelectorAll('.data-value')[0].classList.remove('medical-normal');
+            } else {
+                document.querySelectorAll('.data-value')[0].classList.remove('medical-warning');
+                document.querySelectorAll('.data-value')[0].classList.add('medical-normal');
+            }
+        }, 3000);
+    </script>
+</body>
+</html>

+ 5 - 0
src/app/app.component.ts

@@ -1,6 +1,11 @@
 import { Component } from '@angular/core';
 import { IonApp, IonRouterOutlet } from '@ionic/angular/standalone';
 
+// 添加Icons
+import { addIcons } from 'ionicons';
+import * as icons from 'ionicons/icons';
+addIcons(icons);
+
 @Component({
   selector: 'app-root',
   templateUrl: 'app.component.html',

+ 70 - 12
src/app/tab2/tab2.page.html

@@ -1,19 +1,77 @@
 <ion-header [translucent]="true">
-  <ion-toolbar>
+  <ion-toolbar color="primary">
     <ion-title>
-      Chat模块组件演示
+      <ion-icon name="chatbubbles" class="ion-align-self-center"></ion-icon>
+      &nbsp;智能对话助手
     </ion-title>
   </ion-toolbar>
 </ion-header>
 
-<!-- {{title}} -->
-<ion-content [fullscreen]="true">
-  <h1>页面:配置路由和参数的聊天页</h1>
-  <ion-button (click)="goChat()">开始页面聊天</ion-button>
-  <h1>组件:直接弹出的聊天组件</h1>
-  <ion-button (click)="openChat()">开始新聊天</ion-button>
-  <ion-button (click)="restoreChat('yHEHqMQDNv')">恢复会话</ion-button>
-  <h1>示例:门诊问诊的智能体示例(ChatPanel组件)</h1>
-  <ion-button (click)="openInquiry()">进入门诊</ion-button>
+<ion-content [fullscreen]="true" class="ion-padding">
+  <div class="welcome-section">
+    <ion-card class="welcome-card" color="light">
+      <ion-card-header>
+        <ion-card-title>欢迎使用智能对话系统</ion-card-title>
+        <ion-card-subtitle>选择您需要的服务开始对话</ion-card-subtitle>
+      </ion-card-header>
+      <ion-card-content>
+        <p>我们的智能助手可以为您提供多种专业领域的咨询服务,点击下方按钮开始体验。</p>
+      </ion-card-content>
+    </ion-card>
+  </div>
 
-</ion-content>
+  <ion-grid class="features-grid">
+    <ion-row>
+      <ion-col size="12" size-md="6">
+        <ion-card class="feature-card" button (click)="openChat()">
+          <ion-card-header>
+            <ion-icon name="chatbox" color="primary" class="feature-icon"></ion-icon>
+            <ion-card-title>开始新聊天</ion-card-title>
+          </ion-card-header>
+          <ion-card-content>
+            与我们的智能助手进行自由对话,解答您的各种疑问。
+          </ion-card-content>
+          <ion-button fill="clear" expand="block">
+            开始对话
+            <ion-icon slot="end" name="arrow-forward"></ion-icon>
+          </ion-button>
+        </ion-card>
+      </ion-col>
+
+      <ion-col size="12" size-md="6">
+        <ion-card class="feature-card" button (click)="restoreChat('yHEHqMQDNv')">
+          <ion-card-header>
+            <ion-icon name="time" color="secondary" class="feature-icon"></ion-icon>
+            <ion-card-title>恢复会话</ion-card-title>
+          </ion-card-header>
+          <ion-card-content>
+            继续您之前的对话,无需重复说明问题背景。
+          </ion-card-content>
+          <ion-button fill="clear" expand="block" color="secondary">
+            恢复会话
+            <ion-icon slot="end" name="arrow-forward"></ion-icon>
+          </ion-button>
+        </ion-card>
+      </ion-col>
+    </ion-row>
+
+    <ion-row>
+      <ion-col size="12">
+        <ion-card class="special-feature" button (click)="openInquiry()">
+          <ion-card-header>
+            <ion-icon name="medical" color="danger" class="feature-icon"></ion-icon>
+            <ion-card-title>门诊问诊服务</ion-card-title>
+            <ion-card-subtitle>专业医疗咨询</ion-card-subtitle>
+          </ion-card-header>
+          <ion-card-content>
+            我们的医疗智能助手可以为您提供初步的医疗咨询和建议,帮助您了解可能的健康问题。
+          </ion-card-content>
+          <ion-button fill="clear" expand="block" color="danger">
+            进入门诊
+            <ion-icon slot="end" name="medkit"></ion-icon>
+          </ion-button>
+        </ion-card>
+      </ion-col>
+    </ion-row>
+  </ion-grid>
+</ion-content>

+ 65 - 0
src/app/tab2/tab2.page.scss

@@ -0,0 +1,65 @@
+/* 页面整体样式 */
+ion-content {
+    --background: #f5f5f5;
+  }
+  
+  /* 欢迎卡片样式 */
+  .welcome-card {
+    text-align: center;
+    margin: 20px auto;
+    max-width: 800px;
+    border-radius: 15px;
+    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
+    ion-card-header {
+      padding-bottom: 0;
+    }
+  }
+  
+  /* 功能卡片样式 */
+  .features-grid {
+    max-width: 1200px;
+    margin: 0 auto;
+  }
+  
+  .feature-card {
+    height: 100%;
+    border-radius: 12px;
+    transition: transform 0.3s ease, box-shadow 0.3s ease;
+    &:hover {
+      transform: translateY(-5px);
+      box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
+    }
+    ion-card-header {
+      text-align: center;
+      padding-bottom: 0;
+    }
+    ion-card-content {
+      min-height: 80px;
+    }
+  }
+  
+  .feature-icon {
+    font-size: 48px;
+    margin-bottom: 16px;
+  }
+  
+  /* 特殊功能卡片样式 */
+  .special-feature {
+    margin-top: 20px;
+    border-radius: 12px;
+    background: linear-gradient(135deg, #fff8f8, #ffffff);
+    border-left: 4px solid var(--ion-color-danger);
+    ion-card-header {
+      text-align: center;
+    }
+    ion-card-content {
+      min-height: 80px;
+    }
+  }
+  
+  /* 响应式调整 */
+  @media (max-width: 768px) {
+    .feature-card, .special-feature {
+      margin-bottom: 20px;
+    }
+  }

+ 20 - 0
src/app/tab2/tab2.page.ts

@@ -5,6 +5,17 @@ import { ExploreContainerComponent } from '../explore-container/explore-containe
 import { ChatPanelOptions, FmChatModalInput, FmodeChat, FmodeChatMessage, openChatPanelModal } from 'fmode-ng';
 // import { ModalAudioMessageComponent } from 'fmode-ng/lib/aigc/chat/chat-modal-input/modal-audio-message/modal-audio-message.component';
 
+import { 
+  IonCard,
+  IonCardHeader,
+  IonCardTitle,
+  IonCardSubtitle,
+  IonCardContent,
+  IonGrid,
+  IonRow,
+  IonCol,
+  IonIcon
+} from '@ionic/angular/standalone';
 @Component({
   selector: 'app-tab2',
   templateUrl: 'tab2.page.html',
@@ -13,6 +24,15 @@ import { ChatPanelOptions, FmChatModalInput, FmodeChat, FmodeChatMessage, openCh
   imports: [
     IonHeader, IonToolbar, IonTitle, IonContent, ExploreContainerComponent,
     IonButton,
+    IonCard,
+    IonCardHeader,
+    IonCardTitle,
+    IonCardSubtitle,
+    IonCardContent,
+    IonGrid,
+    IonRow,
+    IonCol,
+    IonIcon,
     // ASR语音输入模块
     FmChatModalInput,
     // ModalAudioMessageComponent

+ 11 - 9
src/app/tabs/tabs.page.html

@@ -1,21 +1,23 @@
 <ion-tabs>
   <ion-tab-bar slot="bottom">
 
-    <ion-tab-button tab="tab3" href="/tabs/tab3">
-      <ion-icon aria-hidden="true" name="square"></ion-icon>
-      <ion-label>向量</ion-label>
-    </ion-tab-button>
-
     <ion-tab-button tab="tab1" href="/tabs/tab1">
-      <ion-icon aria-hidden="true" name="triangle"></ion-icon>
-      <ion-label>任务</ion-label>
+      <ion-icon aria-hidden="true" name="trail-sign-outline"></ion-icon>
+      <ion-label>工作流</ion-label>
     </ion-tab-button>
 
     <ion-tab-button tab="tab2" href="/tabs/tab2">
-      <ion-icon aria-hidden="true" name="ellipse"></ion-icon>
-      <ion-label>对话</ion-label>
+      <ion-icon aria-hidden="true" name="chatbubble-ellipses-outline"></ion-icon>
+      <ion-label>智能体</ion-label>
+    </ion-tab-button>
+
+    <ion-tab-button tab="tab3" href="/tabs/tab3">
+      <ion-icon aria-hidden="true" name="book-outline"></ion-icon>
+      <ion-label>知识库</ion-label>
     </ion-tab-button>
 
    
+
+   
   </ion-tab-bar>
 </ion-tabs>

+ 3 - 1
src/app/tabs/tabs.page.ts

@@ -8,7 +8,9 @@ import { triangle, ellipse, square } from 'ionicons/icons';
   templateUrl: 'tabs.page.html',
   styleUrls: ['tabs.page.scss'],
   standalone: true,
-  imports: [IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel],
+  imports: [
+    IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel,
+  ],
 })
 export class TabsPage {
   public environmentInjector = inject(EnvironmentInjector);

+ 7 - 2
src/app/tabs/tabs.routes.ts

@@ -1,5 +1,6 @@
 import { Routes } from '@angular/router';
 import { TabsPage } from './tabs.page';
+import { TokenGuard } from 'src/lib/user/token-guard/token.guard';
 
 export const routes: Routes = [
   {
@@ -8,16 +9,20 @@ export const routes: Routes = [
     children: [
       {
         path: 'tab1',
+        canActivate:[TokenGuard],
         loadComponent: () =>
-          import('../tab1/tab1.page').then((m) => m.Tab1Page),
+          import('../../modules/flow/page-flow-test/page-flow-test.component').then((m) => m.PageFlowTestComponent),
+          // import('../tab1/tab1.page').then((m) => m.Tab1Page),
       },
       {
         path: 'tab2',
+        canActivate:[TokenGuard],
         loadComponent: () =>
           import('../tab2/tab2.page').then((m) => m.Tab2Page),
       },
       {
         path: 'tab3',
+        canActivate:[TokenGuard],
         loadComponent: () =>
           import('../tab3/tab3.page').then((m) => m.Tab3Page),
       },
@@ -30,7 +35,7 @@ export const routes: Routes = [
   },
   {
     path: '',
-    redirectTo: '/tabs/tab3',
+    redirectTo: '/tabs/tab1',
     pathMatch: 'full',
   },
 ];

+ 240 - 0
src/lib/user/token-guard/token.guard.ts

@@ -0,0 +1,240 @@
+/**
+@desc
+请您帮我设计一个ionic/angular项目的TokenGuard路由守卫,检查localStorage是否有token值。若不存在,通过dom构建ui交互提示用户填写token(不使用modal和angular逻辑)。若存在token或填写后,则调用接口:
+curl -X GET \
+  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
+  -H "X-Parse-Session-Token: r:pnktnjyb996sj4p156gjtp4im" \
+  https://YOUR.PARSE-SERVER.HERE/parse/users/me 
+可用fetch请求验证token是否正常。
+若不正常,提示错误请重新填写直到填写了有效token。
+若正常则返回true。
+接口地址为https://server.fmode.cn/parse 应用id为 ncloudmaster。
+
+@example 路由守卫使用
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import { TokenGuard } from './guards/token.guard';
+
+const routes: Routes = [
+  {
+    path: 'protected',
+    loadChildren: () => import('./protected/protected.module').then(m => m.ProtectedPageModule),
+    canActivate: [TokenGuard]
+  },
+  // 其他路由...
+];
+
+@NgModule({
+  imports: [RouterModule.forRoot(routes)],
+  exports: [RouterModule]
+})
+export class AppRoutingModule {}
+
+ */
+
+// src/app/guards/token.guard.ts
+import { Injectable } from '@angular/core';
+import { CanActivate, Router } from '@angular/router';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class TokenGuard implements CanActivate {
+  private readonly PARSE_SERVER_URL = 'https://server.fmode.cn/parse';
+  private readonly APPLICATION_ID = 'ncloudmaster';
+
+  constructor(
+    private http: HttpClient,
+    private router: Router
+  ) {}
+
+  async canActivate(): Promise<boolean> {
+    let token = localStorage.getItem('parseSessionToken');
+
+    if (!token) {
+      token = await this.showTokenPrompt();
+      if (!token) {
+        this.router.navigate(['/login']);
+        return false;
+      }
+    }
+
+    const isValid = await this.validateToken(token);
+    if (!isValid) {
+      localStorage.removeItem('parseSessionToken');
+      return this.canActivate();
+    }
+
+    localStorage.setItem('parseSessionToken', token);
+    return true;
+  }
+
+  private async validateToken(token: string): Promise<boolean> {
+    const headers = new HttpHeaders({
+      'X-Parse-Application-Id': this.APPLICATION_ID,
+      'X-Parse-Session-Token': token
+    });
+
+    try {
+      const response: any = await this.http.get(
+        `${this.PARSE_SERVER_URL}/users/me`,
+        { headers }
+      ).toPromise();
+      return !!response?.objectId;
+    } catch (error) {
+      return false;
+    }
+  }
+
+  private showTokenPrompt(): Promise<string | null> {
+    return new Promise((resolve) => {
+      // 创建遮罩层
+      const overlay = document.createElement('div');
+      overlay.style.position = 'fixed';
+      overlay.style.top = '0';
+      overlay.style.left = '0';
+      overlay.style.width = '100%';
+      overlay.style.height = '100%';
+      overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
+      overlay.style.display = 'flex';
+      overlay.style.justifyContent = 'center';
+      overlay.style.alignItems = 'center';
+      overlay.style.zIndex = '1000';
+
+      // 创建对话框
+      const dialog = document.createElement('div');
+      dialog.style.backgroundColor = 'white';
+      dialog.style.padding = '20px';
+      dialog.style.borderRadius = '8px';
+      dialog.style.width = '80%';
+      dialog.style.maxWidth = '400px';
+
+      // 创建标题
+      const title = document.createElement('h3');
+      title.textContent = '请输入 Token';
+      title.style.marginTop = '0';
+      title.style.color = "black";
+      dialog.appendChild(title);
+
+      // 创建描述
+      // 使用以下指令获取token
+      
+      const descEl:HTMLElement = document.createElement("div");
+      descEl.innerHTML = `获取token方法:<br>
+      1.登录<a href="https://ai.fmode.cn" target="_blank">https://ai.fmode.cn</a><br>
+      2.按F12进入调试——打开控制台Console<br>
+      3.输入指令:<br>
+      <span style="color:blue;">JSON.parse(localStorage.getItem("Parse/ncloudmaster/currentUser"))?.sessionToken</span><br>
+      4.复制字符串内容,形如:<br>
+      <span style="color:red">r:xxxxxxxxxxxxx</span>
+      `
+      descEl.style.color = "black"
+      dialog.appendChild(descEl);
+
+      // 创建错误消息容器
+      const errorMsg = document.createElement('div');
+      errorMsg.style.color = 'red';
+      errorMsg.style.minHeight = '20px';
+      errorMsg.style.margin = '10px 0';
+      dialog.appendChild(errorMsg);
+
+      // 创建输入框
+      const input = document.createElement('input');
+      input.type = 'text';
+      input.placeholder = '请输入您的 Token';
+      input.style.width = '100%';
+      input.style.padding = '10px';
+      input.style.marginBottom = '15px';
+      input.style.boxSizing = 'border-box';
+      dialog.appendChild(input);
+
+      // 创建按钮容器
+      const buttonContainer = document.createElement('div');
+      buttonContainer.style.display = 'flex';
+      buttonContainer.style.justifyContent = 'flex-end';
+      buttonContainer.style.gap = '10px';
+
+      // 创建提交按钮
+      const submitBtn = document.createElement('button');
+      submitBtn.textContent = '提交';
+      submitBtn.style.padding = '8px 16px';
+      submitBtn.style.backgroundColor = '#3880ff';
+      submitBtn.style.color = 'white';
+      submitBtn.style.border = 'none';
+      submitBtn.style.borderRadius = '4px';
+      submitBtn.disabled = true;
+
+      // 创建取消按钮
+      const cancelBtn = document.createElement('button');
+      cancelBtn.textContent = '取消';
+      cancelBtn.style.padding = '8px 16px';
+      cancelBtn.style.backgroundColor = '#eb445a';
+      cancelBtn.style.color = 'white';
+      cancelBtn.style.border = 'none';
+      cancelBtn.style.borderRadius = '4px';
+
+      // 添加按钮到容器
+      buttonContainer.appendChild(cancelBtn);
+      buttonContainer.appendChild(submitBtn);
+      dialog.appendChild(buttonContainer);
+
+      // 添加到遮罩层
+      overlay.appendChild(dialog);
+      document.body.appendChild(overlay);
+
+      // 自动聚焦输入框
+      input.focus();
+
+      // 输入验证
+      input.addEventListener('input', () => {
+        submitBtn.disabled = !input.value.trim();
+      });
+
+      // 取消按钮事件
+      const cleanup = () => {
+        document.body.removeChild(overlay);
+      };
+
+      cancelBtn.addEventListener('click', () => {
+        cleanup();
+        resolve(null);
+      });
+
+      // 提交按钮事件
+      submitBtn.addEventListener('click', async () => {
+        const token = input.value.trim();
+        if (!token) return;
+
+        const isValid = await this.validateToken(token);
+        if (isValid) {
+          cleanup();
+          resolve(token);
+        } else {
+          errorMsg.textContent = 'Token 无效,请重新输入';
+          input.value = '';
+          submitBtn.disabled = true;
+          input.focus();
+        }
+      });
+
+      // 回车键提交
+      input.addEventListener('keypress', async (e) => {
+        if (e.key === 'Enter' && input.value.trim()) {
+          const token = input.value.trim();
+          const isValid = await this.validateToken(token);
+          
+          if (isValid) {
+            cleanup();
+            resolve(token);
+          } else {
+            errorMsg.textContent = 'Token 无效,请重新输入';
+            input.value = '';
+            submitBtn.disabled = true;
+            input.focus();
+          }
+        }
+      });
+    });
+  }
+}

+ 1 - 1
src/modules/task/page-test-completion/page-test-completion.component.ts

@@ -48,7 +48,7 @@ export class PageTestCompletionComponent implements OnInit {
       { role: 'user', content: diagnosisPrompt+`患者症状描述:${this.userDescription}` }
     ];
 
-    localStorage.setItem("token",'r:9485439b35a9ce34bb95a9f3cfec2e6e'), // 替换为您的实际token
+    // localStorage.setItem("token",'r:9485439b35a9ce34bb95a9f3cfec2e6e'), // 替换为您的实际token
     TestFmodeChatCompletion(
       {
         messages