Bläddra i källkod

feat:chat-assistant&page-association

0235967 1 vecka sedan
förälder
incheckning
01698b9244

+ 11 - 3
travel-web/src/app/app.ts

@@ -1,13 +1,21 @@
 import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
 import { RouterOutlet } from '@angular/router';
+import { ChatAssistant } from '../modules/shared/chat-assistant/chat-assistant';
+import { ChatService } from '../modules/shared/chat-assistant/chat.service';
 import '@fortawesome/fontawesome-free/css/all.min.css';
 
 @Component({
   selector: 'app-root',
-  imports: [RouterOutlet],
-  templateUrl: './app.html',
+  standalone: true,
+  imports: [CommonModule, RouterOutlet, ChatAssistant],
+  template: `
+    <router-outlet></router-outlet>
+    <app-chat-assistant *ngIf="chatService.isChatVisible$ | async"></app-chat-assistant>
+  `,
   styleUrl: './app.scss'
 })
 export class App {
+  constructor(public chatService: ChatService) {}
   protected title = '赣鄱文化云';
-}
+}

+ 0 - 190
travel-web/src/modules/pc-home/pages/chat-assistant/chat-assistant.scss

@@ -1,190 +0,0 @@
-@use "sass:color";
-
-.chat-container {
-    position: fixed;
-    bottom: 20px;
-    right: 20px;
-    width: 350px;
-    max-width: 90%;
-    height: 500px;
-    background: white;
-    border-radius: 15px;
-    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
-    display: flex;
-    flex-direction: column;
-    z-index: 1000;
-    overflow: hidden;
-    transform: translateY(20px);
-    opacity: 0;
-    animation: fadeInUp 0.3s ease-out forwards;
-  }
-  
-  @keyframes fadeInUp {
-    to {
-      transform: translateY(0);
-      opacity: 1;
-    }
-  }
-  
-  .chat-header {
-    background: linear-gradient(90deg, var(--mountain-green), #5d8c5a);
-    color: white;
-    padding: 15px;
-    display: flex;
-    align-items: center;
-    position: relative;
-    
-    i {
-      font-size: 1.2rem;
-      margin-right: 10px;
-    }
-    
-    h3 {
-      margin: 0;
-      font-size: 1rem;
-      font-weight: 600;
-    }
-    
-    .close-btn {
-      position: absolute;
-      right: 15px;
-      background: transparent;
-      border: none;
-      color: white;
-      cursor: pointer;
-      font-size: 1rem;
-    }
-  }
-  
-  .chat-messages {
-    flex: 1;
-    padding: 15px;
-    overflow-y: auto;
-    background: #f9f9f9;
-    
-    .message {
-      display: flex;
-      margin-bottom: 15px;
-      
-      &.user {
-        justify-content: flex-end;
-        
-        .content {
-          background: var(--primary-blue);
-          color: white;
-          border-radius: 15px 15px 0 15px;
-        }
-      }
-      
-      &.assistant {
-        justify-content: flex-start;
-        
-        .content {
-          background: white;
-          color: var(--dark-charcoal);
-          border-radius: 15px 15px 15px 0;
-          box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
-        }
-      }
-      
-      .avatar {
-        width: 32px;
-        height: 32px;
-        border-radius: 50%;
-        background: #eee;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        margin: 0 10px;
-        flex-shrink: 0;
-        
-        i {
-          color: var(--mountain-green);
-        }
-      }
-      
-      .content {
-        max-width: 70%;
-        padding: 10px 15px;
-        
-        .text {
-          white-space: pre-wrap;
-          line-height: 1.4;
-        }
-      }
-    }
-    
-    .loading-dots {
-      display: flex;
-      padding: 10px 0;
-      
-      span {
-        width: 8px;
-        height: 8px;
-        margin: 0 3px;
-        background: var(--mountain-green);
-        border-radius: 50%;
-        display: inline-block;
-        animation: bounce 1.4s infinite ease-in-out both;
-        
-        &:nth-child(1) {
-          animation-delay: -0.32s;
-        }
-        
-        &:nth-child(2) {
-          animation-delay: -0.16s;
-        }
-      }
-    }
-  }
-  
-  @keyframes bounce {
-    0%, 80%, 100% { 
-      transform: scale(0);
-    }
-    40% { 
-      transform: scale(1.0);
-    }
-  }
-  
-  .chat-input {
-    display: flex;
-    padding: 10px;
-    border-top: 1px solid #eee;
-    background: white;
-    
-    input {
-      flex: 1;
-      padding: 10px 15px;
-      border: 1px solid #ddd;
-      border-radius: 30px;
-      outline: none;
-      font-size: 0.9rem;
-      
-      &:focus {
-        border-color: var(--primary-blue);
-      }
-    }
-    
-    button {
-      width: 40px;
-      height: 40px;
-      border-radius: 50%;
-      background: var(--mountain-green);
-      color: white;
-      border: none;
-      margin-left: 10px;
-      cursor: pointer;
-      transition: all 0.3s ease;
-      
-      &:hover {
-        background: color.scale(#5d8c5a, $lightness: -10%);
-        /* 或者使用 color.adjust(#5d8c5a, $lightness: -10%) */
-      }
-      
-      &:disabled {
-        background: #ccc;
-        cursor: not-allowed;
-      }
-    }
-  }

+ 24 - 3
travel-web/src/modules/pc-home/pages/page-association/page-association.html

@@ -54,10 +54,31 @@
   
   <div class="constitution-container">
     <div class="search-box">
-      <input #searchInput type="text" class="search-input" placeholder="输入关键词检索协会章程 (如:红色文化、会员权利...)" (keyup.enter)="highlightSearch(searchInput.value)">
-      <button class="search-btn" (click)="highlightSearch(searchInput.value)"><i class="fas fa-search"></i> 智能检索</button>
+      <input #searchInput type="text" class="search-input" 
+      placeholder="输入关键词检索协会章程 (如:红色文化、会员权利...)" 
+      (keyup.enter)="searchConstitution(searchInput.value)"
+      [(ngModel)]="searchTerm">
+      <button class="search-btn" (click)="searchConstitution(searchInput.value)">
+        <i class="fas fa-search"></i> 智能检索
+      </button>
+      <button class="clear-btn" (click)="clearSearch()" *ngIf="searchTerm">
+        <i class="fas fa-times"></i> 清除
+      </button>
     </div>
-    <div class="constitution-content">
+
+    <div class="search-results" *ngIf="searchTerm && highlightCount > 0">
+      <p>找到 <strong>{{highlightCount}}</strong> 处匹配 "<strong>{{searchTerm}}</strong>" 的内容</p>
+      <div class="navigation-buttons">
+        <button (click)="navigateHighlight(-1)" [disabled]="currentHighlightIndex === 0">
+          <i class="fas fa-chevron-up"></i> 上一个
+        </button>
+        <button (click)="navigateHighlight(1)" [disabled]="currentHighlightIndex === highlightCount - 1">
+          <i class="fas fa-chevron-down"></i> 下一个
+        </button>
+      </div>
+    </div>
+
+    <div class="constitution-content" #constitutionContent>
       <h3>江西省数字文化发展协会章程</h3>
       <p>(2023年6月修订)</p>
       

+ 222 - 52
travel-web/src/modules/pc-home/pages/page-association/page-association.scss

@@ -1,14 +1,16 @@
+// 颜色变量定义
 :host {
-  --wood-primary: #8B4513;   /* 木质主色 */
-  --wood-secondary: #A0522D; /* 木质辅色 */
-  --wood-light: #DEB887;     /* 木质亮色 */
-  --brick-color: #7D5A50;    /* 青砖色 */
-  --porcelain-blue: #2a5daa; /* 青花蓝 */
-  --gold-yellow: #e8c34d;    /* 稻穗金 */
-  --mountain-green: #4a6b3d; /* 山林绿 */
-  --river-blue: #4a86e8;     /* 赣江蓝 */
+  --wood-primary: #8B4513;   // 木质主色
+  --wood-secondary: #A0522D; // 木质辅色
+  --wood-light: #DEB887;     // 木质亮色
+  --brick-color: #7D5A50;    // 青砖色
+  --porcelain-blue: #2a5daa; // 青花蓝
+  --gold-yellow: #e8c34d;    // 稻穗金
+  --mountain-green: #4a6b3d; // 山林绿
+  --river-blue: #4a86e8;     // 赣江蓝
 }
 
+// 基础样式
 * {
   margin: 0;
   padding: 0;
@@ -24,9 +26,9 @@ body {
   background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="%23f5f2e9"/><path d="M0,0 L100,100 M100,0 L0,100" stroke="%23e8e0d022" stroke-width="1"/></svg>');
 }
 
-/* 页面标题 */
+// 页面标题
 .page-header {
-  background: linear-gradient(lch(31.77% 44.55 69.48 / 0.983)), 
+  background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.7)), 
               url('https://images.unsplash.com/photo-1606193431680-8f5a06d61c9b?ixlib=rb-4.0.3&auto=format&fit=crop&w=1950&q=80') center/cover;
   height: 300px;
   display: flex;
@@ -36,6 +38,7 @@ body {
   color: white;
   text-align: center;
   position: relative;
+  border-bottom: 5px solid var(--wood-primary);
 }
 
 .page-title {
@@ -43,18 +46,24 @@ body {
   font-weight: bold;
   margin-bottom: 1rem;
   text-shadow: 0 2px 10px rgba(0,0,0,0.5);
+  font-family: 'SimSun', serif;
 }
 
 .page-subtitle {
   font-size: 1.5rem;
   max-width: 800px;
+  padding: 0 2rem;
 }
 
-/* 内容区域 */
+// 内容区域
 .content-section {
   padding: 4rem 5%;
   background-color: #fcfaf5;
   position: relative;
+  margin: 2rem auto;
+  max-width: 1200px;
+  box-shadow: 0 0 20px rgba(0,0,0,0.1);
+  border-radius: 8px;
 }
 
 .content-section::before {
@@ -65,6 +74,7 @@ body {
   right: 0;
   height: 10px;
   background: linear-gradient(90deg, var(--wood-primary), var(--wood-secondary));
+  border-radius: 8px 8px 0 0;
 }
 
 .section-header {
@@ -83,6 +93,7 @@ body {
   background: linear-gradient(90deg, var(--wood-light), transparent 80%);
   padding: 0.5rem 1.5rem;
   border-radius: 0 30px 30px 0;
+  font-family: 'SimSun', serif;
 }
 
 .section-title::before {
@@ -104,7 +115,7 @@ body {
   margin-top: 2rem;
 }
 
-/* 走进协会 */
+// 走进协会
 .org-chart-container {
   flex: 1;
   min-width: 300px;
@@ -122,6 +133,12 @@ body {
   color: white;
   display: flex;
   align-items: center;
+  gap: 1rem;
+  font-size: 1.2rem;
+}
+
+.org-header i {
+  font-size: 1.5rem;
 }
 
 .org-body {
@@ -145,6 +162,7 @@ body {
   display: flex;
   border: 2px solid var(--wood-light);
   transition: all 0.3s ease;
+  position: relative;
 }
 
 .leader-card:hover {
@@ -159,10 +177,10 @@ body {
   background-position: center;
   background-repeat: no-repeat;
   position: relative;
-  border-radius: 50% / 60%; /* 水平半径50%,垂直半径60% */
+  border-radius: 50% / 60%;
   overflow: hidden;
+  margin: 1rem;
   
-  /* 移除或修改原有的伪元素样式 */
   &::after {
     content: '';
     position: absolute;
@@ -171,10 +189,9 @@ body {
     width: 100%;
     height: 100%;
     border: 3px solid var(--wood-light);
-    border-radius: 50% / 60%; /* 与父元素保持一致 */
+    border-radius: 50% / 60%;
     box-sizing: border-box;
     pointer-events: none;
-    /* 调整渐变效果 */
     background: radial-gradient(circle, transparent 60%, rgba(0,0,0,0.2));
   }
 }
@@ -199,16 +216,19 @@ body {
   font-size: 1.5rem;
   color: var(--wood-primary);
   margin-bottom: 0.5rem;
+  font-family: 'SimSun', serif;
 }
 
 .leader-title {
   color: var(--wood-secondary);
   margin-bottom: 1rem;
+  font-size: 0.95rem;
 }
 
 .leader-desc {
   color: #666;
   font-size: 0.95rem;
+  line-height: 1.7;
 }
 
 .ar-tag {
@@ -220,9 +240,10 @@ body {
   padding: 0.3rem 0.8rem;
   border-radius: 30px;
   font-size: 0.8rem;
+  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
 }
 
-/* 协会章程 */
+// 协会章程
 .constitution-container {
   margin-top: 3rem;
   background: white;
@@ -234,51 +255,118 @@ body {
 }
 
 .search-box {
-  padding: 1.5rem;
-  background: linear-gradient(135deg, #4a86e8 0%, #2a5daa 100%);
-  display: flex;
-  align-items: center;
   position: relative;
-  overflow: hidden;
-}
-
-.search-box::before {
-  content: '';
-  position: absolute;
-  top: -50px;
-  right: -50px;
-  width: 150px;
-  height: 150px;
-  background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M20,50 Q40,20 60,50 T100,50" fill="none" stroke="white" stroke-width="1" opacity="0.2"/></svg>');
-  background-size: contain;
-  transform: rotate(20deg);
+  display: flex;
+  gap: 10px;
+  padding: 1.5rem;
+  background: linear-gradient(135deg, var(--porcelain-blue) 0%, var(--river-blue) 100%);
+  border-radius: 12px 12px 0 0;
+  
+  &::before {
+    content: '';
+    position: absolute;
+    top: -50px;
+    right: -50px;
+    width: 150px;
+    height: 150px;
+    background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M20,50 Q40,20 60,50 T100,50" fill="none" stroke="white" stroke-width="1" opacity="0.2"/></svg>');
+    background-size: contain;
+    transform: rotate(20deg);
+  }
 }
 
 .search-input {
   flex: 1;
-  padding: 1rem 1.5rem;
+  padding: 0.8rem 1.5rem;
   border: none;
   border-radius: 50px;
-  font-size: 1.1rem;
-  box-shadow: 0 4px 10px rgba(0,0,0,0.2);
+  font-size: 1rem;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+  transition: all 0.3s ease;
+  
+  &:focus {
+    outline: none;
+    box-shadow: 0 2px 12px rgba(0,0,0,0.2);
+  }
 }
 
-.search-btn {
-  background: var(--gold-yellow);
-  color: var(--wood-primary);
+.search-btn, .clear-btn {
+  padding: 0.8rem 1.5rem;
   border: none;
-  padding: 1rem 2rem;
   border-radius: 50px;
-  margin-left: 1rem;
   font-weight: bold;
   cursor: pointer;
   transition: all 0.3s ease;
-  box-shadow: 0 4px 10px rgba(0,0,0,0.2);
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.search-btn {
+  background: var(--gold-yellow);
+  color: var(--wood-primary);
+  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+  
+  &:hover {
+    background: #d9b347;
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+  }
 }
 
-.search-btn:hover {
-  background: #d9b347;
-  transform: translateY(-2px);
+.clear-btn {
+  background: #f44336;
+  color: white;
+  
+  &:hover {
+    background: #d32f2f;
+  }
+}
+
+.search-results {
+  padding: 1rem 1.5rem;
+  background: rgba(255,255,255,0.9);
+  border-bottom: 1px solid #eee;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  
+  p {
+    margin: 0;
+    color: var(--wood-primary);
+    font-weight: bold;
+  }
+}
+
+.navigation-buttons {
+  display: flex;
+  gap: 10px;
+  
+  button {
+    padding: 0.5rem 1rem;
+    border: 1px solid var(--wood-light);
+    background: white;
+    color: var(--wood-primary);
+    border-radius: 4px;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    display: flex;
+    align-items: center;
+    
+    &:hover:not(:disabled) {
+      background: var(--wood-light);
+      color: white;
+    }
+    
+    &:disabled {
+      opacity: 0.5;
+      cursor: not-allowed;
+    }
+    
+    i {
+      margin-right: 5px;
+    }
+  }
 }
 
 .constitution-content {
@@ -289,6 +377,7 @@ body {
   font-family: 'SimSun', serif;
   line-height: 2;
   position: relative;
+  border-radius: 0 0 12px 12px;
 }
 
 .constitution-content::before {
@@ -304,13 +393,32 @@ body {
 }
 
 .highlight {
-  background-color: rgba(232, 195, 77, 0.3);
+  background-color: rgba(232, 195, 77, 0.5);
+  color: var(--wood-primary);
   padding: 0 2px;
   border-radius: 2px;
   font-weight: bold;
+  transition: all 0.2s ease;
+  
+  &.active-highlight {
+    background-color: var(--gold-yellow);
+    box-shadow: 0 0 0 2px var(--gold-yellow);
+    animation: pulse 1s infinite alternate;
+    position: relative;
+    z-index: 1;
+  }
 }
 
-/* 学术体系 */
+@keyframes pulse {
+  from {
+    box-shadow: 0 0 0 0 rgba(232, 195, 77, 0.7);
+  }
+  to {
+    box-shadow: 0 0 0 5px rgba(232, 195, 77, 0);
+  }
+}
+
+// 学术体系
 .academic-calendar {
   flex: 1;
   min-width: 300px;
@@ -327,6 +435,11 @@ body {
   color: white;
   display: flex;
   align-items: center;
+  gap: 1rem;
+}
+
+.calendar-header i {
+  font-size: 1.5rem;
 }
 
 .calendar-body {
@@ -350,6 +463,11 @@ body {
   color: white;
   display: flex;
   align-items: center;
+  gap: 1rem;
+}
+
+.standards-header i {
+  font-size: 1.5rem;
 }
 
 .standards-body {
@@ -377,6 +495,7 @@ body {
   margin-bottom: 1rem;
   padding-bottom: 0.5rem;
   border-bottom: 2px solid var(--wood-light);
+  font-family: 'SimSun', serif;
 }
 
 .version-content {
@@ -386,11 +505,13 @@ body {
 .diff-highlight {
   background-color: rgba(255, 0, 0, 0.1);
   text-decoration: line-through;
+  cursor: pointer;
 }
 
 .new-highlight {
   background-color: rgba(0, 128, 0, 0.1);
   font-weight: bold;
+  cursor: pointer;
 }
 
 .diff-highlight.active-highlight {
@@ -401,7 +522,7 @@ body {
   background-color: rgba(0, 128, 0, 0.2);
 }
 
-/* 人才培养 */
+// 人才培养
 .training-container {
   display: flex;
   flex-wrap: wrap;
@@ -425,6 +546,11 @@ body {
   color: white;
   display: flex;
   align-items: center;
+  gap: 1rem;
+}
+
+.training-header i {
+  font-size: 1.5rem;
 }
 
 .training-body {
@@ -514,6 +640,7 @@ body {
   font-size: 1.2rem;
   color: var(--wood-primary);
   margin-bottom: 0.5rem;
+  font-family: 'SimSun', serif;
 }
 
 .course-meta {
@@ -524,12 +651,13 @@ body {
   margin-top: 1rem;
 }
 
-/* 页脚 */
+// 页脚
 .footer {
   background: var(--wood-primary);
   color: #f0e6d2;
   padding: 3rem 5% 2rem;
   margin-top: 3rem;
+  border-top: 5px solid var(--wood-secondary);
 }
 
 .footer-content {
@@ -537,6 +665,8 @@ body {
   flex-wrap: wrap;
   gap: 3rem;
   margin-bottom: 2rem;
+  max-width: 1200px;
+  margin: 0 auto 2rem;
 }
 
 .footer-column {
@@ -549,6 +679,7 @@ body {
   margin-bottom: 1.5rem;
   position: relative;
   padding-bottom: 0.5rem;
+  font-family: 'SimSun', serif;
 }
 
 .footer-column h3::after {
@@ -567,6 +698,9 @@ body {
 
 .footer-column ul li {
   margin-bottom: 0.8rem;
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
 }
 
 .footer-column a {
@@ -585,9 +719,11 @@ body {
   border-top: 1px solid rgba(240, 230, 210, 0.3);
   color: #d9c7a7;
   font-size: 0.9rem;
+  max-width: 1200px;
+  margin: 0 auto;
 }
 
-/* 动画效果 */
+// 动画效果
 @keyframes float {
   0% { transform: translateY(0px); }
   50% { transform: translateY(-10px); }
@@ -598,7 +734,7 @@ body {
   animation: float 3s ease-in-out infinite;
 }
 
-/* 响应式设计 */
+// 响应式设计
 @media (max-width: 768px) {
   .section-content, .versions, .training-container {
     flex-direction: column;
@@ -611,5 +747,39 @@ body {
   .leader-img {
     width: 100%;
     height: 200px;
+    margin: 0;
+    border-radius: 0;
+  }
+  
+  .search-box {
+    flex-direction: column;
+    
+    button {
+      width: 100%;
+    }
+  }
+  
+  .search-results {
+    flex-direction: column;
+    gap: 1rem;
+    align-items: flex-start;
+  }
+}
+
+@media (max-width: 480px) {
+  .page-title {
+    font-size: 2.5rem;
+  }
+  
+  .page-subtitle {
+    font-size: 1.2rem;
+  }
+  
+  .section-title {
+    font-size: 1.8rem;
+  }
+  
+  .constitution-content {
+    padding: 1rem;
   }
 }

+ 256 - 38
travel-web/src/modules/pc-home/pages/page-association/page-association.ts

@@ -1,17 +1,26 @@
 import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
 import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
 import * as echarts from 'echarts';
 
 @Component({
   selector: 'app-page-association',
   standalone: true,
-  imports: [CommonModule],
+  imports: [CommonModule, FormsModule],
   templateUrl: './page-association.html',
   styleUrls: ['./page-association.scss']
 })
 export class PageAssociation implements AfterViewInit {
+  // 视图子元素引用
   @ViewChild('orgChart') orgChartElement!: ElementRef;
   @ViewChild('academicCalendar') academicCalendarElement!: ElementRef;
+  @ViewChild('constitutionContent') constitutionContent!: ElementRef;
+
+  // 搜索相关属性
+  searchTerm: string = '';
+  highlightCount: number = 0;
+  currentHighlightIndex: number = -1;
+  highlightElements: HTMLElement[] = [];
 
   // 课程数据
   courses = [
@@ -41,41 +50,52 @@ export class PageAssociation implements AfterViewInit {
     }
   ];
 
+  // 初始化生命周期钩子
   ngAfterViewInit(): void {
     this.initOrgChart();
     this.initAcademicCalendar();
   }
 
+  /**
+   * 初始化组织架构图
+   */
   initOrgChart(): void {
     const orgChart = echarts.init(this.orgChartElement.nativeElement);
-    orgChart.setOption({
+    const option = {
       tooltip: {
         trigger: 'item',
-        triggerOn: 'mousemove'
+        triggerOn: 'mousemove',
+        formatter: (params: any) => {
+          return `<strong>${params.name}</strong><br/>${params.data.department || '江西省数字文化发展协会'}`;
+        }
       },
       series: [
         {
           type: 'tree',
           data: [{
             name: '协会主席',
+            department: '全面负责协会工作',
             children: [{
               name: '常务副主席',
+              department: '分管日常事务',
               children: [
-                { name: '会员服务部' },
-                { name: '人才培养部' }
+                { name: '会员服务部', department: '会员发展与服务' },
+                { name: '人才培养部', department: '专业培训与认证' }
               ]
             }, {
               name: '秘书长',
+              department: '协调各部门工作',
               children: [
-                { name: '技术研发中心' },
-                { name: '平台建设部' }
+                { name: '技术研发中心', department: '数字技术研发' },
+                { name: '平台建设部', department: '系统平台开发' }
               ]
             }, {
               name: '学术委员会',
+              department: '学术研究与标准制定',
               children: [
-                { name: '红色文化研究所' },
-                { name: '非遗研究中心' },
-                { name: '数字技术实验室' }
+                { name: '红色文化研究所', department: '红色资源数字化' },
+                { name: '非遗研究中心', department: '非遗保护与传承' },
+                { name: '数字技术实验室', department: '前沿技术研究' }
               ]
             }]
           }],
@@ -84,13 +104,14 @@ export class PageAssociation implements AfterViewInit {
           bottom: '5%',
           right: '20%',
           symbolSize: 20,
-          symbol: 'circle',
+          symbol: 'roundRect',
           orient: 'vertical',
           expandAndCollapse: true,
           initialTreeDepth: 2,
           lineStyle: {
             width: 2,
-            color: '#8B4513'
+            color: '#8B4513',
+            curveness: 0.2
           },
           label: {
             position: 'top',
@@ -98,17 +119,21 @@ export class PageAssociation implements AfterViewInit {
             align: 'middle',
             fontSize: 14,
             fontWeight: 'bold',
-            color: '#8B4513'
+            color: '#8B4513',
+            fontFamily: 'Microsoft YaHei'
           },
           itemStyle: {
             color: '#DEB887',
             borderColor: '#8B4513',
-            borderWidth: 2
+            borderWidth: 2,
+            borderRadius: 5
           },
           emphasis: {
             itemStyle: {
               color: '#e8c34d',
-              borderColor: '#8B4513'
+              borderColor: '#8B4513',
+              shadowBlur: 10,
+              shadowColor: 'rgba(0, 0, 0, 0.3)'
             }
           },
           leaves: {
@@ -124,17 +149,26 @@ export class PageAssociation implements AfterViewInit {
           }
         }
       ]
-    });
+    };
     
+    orgChart.setOption(option);
     window.addEventListener('resize', () => orgChart.resize());
   }
 
+  /**
+   * 初始化学术日历
+   */
   initAcademicCalendar(): void {
     const academicCalendar = echarts.init(this.academicCalendarElement.nativeElement);
-    academicCalendar.setOption({
+    const option = {
       tooltip: {
-        formatter: function(params: any) {
-          return `${params.name}<br/>热度: ${params.value[2]}<br/>地点: ${params.data.location}`;
+        formatter: (params: any) => {
+          return `
+            <div style="font-weight:bold;margin-bottom:5px">${params.data.event}</div>
+            <div>日期: ${params.name}</div>
+            <div>热度: ${params.value[1]}</div>
+            <div>地点: ${params.data.location}</div>
+          `;
         }
       },
       visualMap: {
@@ -153,14 +187,17 @@ export class PageAssociation implements AfterViewInit {
         range: '2023-06',
         itemStyle: {
           borderWidth: 1,
-          borderColor: '#e8e0d0'
+          borderColor: '#e8e0d0',
+          color: '#f9f6f0'
         },
         dayLabel: {
-          color: '#8B4513'
+          color: '#8B4513',
+          fontSize: 12
         },
         monthLabel: {
           color: '#8B4513',
-          fontWeight: 'bold'
+          fontWeight: 'bold',
+          fontSize: 14
         },
         yearLabel: { show: false }
       },
@@ -177,41 +214,222 @@ export class PageAssociation implements AfterViewInit {
           ['2023-06-25', 55, '智慧旅游研讨会', '上饶'],
           ['2023-06-28', 95, '年度学术大会', '南昌']
         ].map((item) => ({
-          value: [item[0], item[1], item[1], item[2], item[3]]
-        }))
+          value: [item[0], item[1]],//日期,热度,事件,地点
+          event: item[2],
+          location:item[3]
+        })),
+        label: {
+          show: true,
+          formatter: (params: any) => {
+            return params.data.event.substring(0, 4);
+          },
+          color: '#333',
+          fontSize: 10
+        },
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 10,
+            shadowColor: 'rgba(0, 0, 0, 0.5)'
+          }
+        }
       }
-    });
+    };
     
+    academicCalendar.setOption(option);
     window.addEventListener('resize', () => academicCalendar.resize());
   }
 
-  highlightSearch(searchTerm: string): void {
-    if (searchTerm) {
-      const content = document.querySelector('.constitution-content');
-      if (content) {
-        const text = content.innerHTML;
-        const regex = new RegExp(searchTerm, 'gi');
-        const highlighted = text.replace(regex, match => 
-          `<span class="highlight">${match}</span>`
+  /**
+   * 搜索章程内容
+   * @param keyword 搜索关键词
+   */
+  searchConstitution(keyword: string): void {
+    if (!keyword.trim()) {
+      this.clearSearch();
+      return;
+    }
+    
+    this.searchTerm = keyword;
+    this.clearHighlights();
+    
+    const content = this.constitutionContent.nativeElement;
+    const textNodes = this.getTextNodes(content);
+    const regex = new RegExp(this.escapeRegExp(keyword), 'gi');
+    
+    let count = 0;
+    this.highlightElements = [];
+    
+    textNodes.forEach(node => {
+      if (node.nodeValue !== null) {
+        const textContent = node.nodeValue;
+        const matches = textContent.match(regex);
+
+      if (matches) {
+        count += matches.length;
+        const span = document.createElement('span');
+        span.className = 'search-highlight';
+        span.innerHTML = node.nodeValue.replace(regex, match => 
+          `<span class="highlight" tabindex="0">${match}</span>`
         );
-        content.innerHTML = highlighted;
+        
+        // 替换原始节点
+        const parent = node.parentNode;
+        if (parent) {
+          parent.replaceChild(span, node);
+        }
+        
+        // 收集所有高亮元素并添加事件监听
+        const highlights = span.querySelectorAll('.highlight');
+        highlights.forEach((el, index) => {
+          const htmlEl = el as HTMLElement;
+          this.highlightElements.push(htmlEl);
+          
+          // 添加键盘导航支持
+          htmlEl.addEventListener('keydown', (e) => {
+            if (e.key === 'Enter') {
+              this.navigateToHighlight(index);
+            }
+          });
+        });
+      }
       }
+    });
+    
+    this.highlightCount = count;
+    this.currentHighlightIndex = -1;
+    
+    if (count > 0) {
+      this.navigateToHighlight(0); // 自动跳转到第一个匹配项
     }
   }
 
+  /**
+   * 导航到指定高亮项
+   * @param index 高亮项索引
+   */
+  private navigateToHighlight(index: number): void {
+    if (index < 0 || index >= this.highlightCount) return;
+    
+    // 清除当前高亮激活状态
+    if (this.currentHighlightIndex >= 0) {
+      this.highlightElements[this.currentHighlightIndex].classList.remove('active-highlight');
+    }
+    
+    this.currentHighlightIndex = index;
+    const currentElement = this.highlightElements[this.currentHighlightIndex];
+    
+    // 设置新激活项
+    currentElement.classList.add('active-highlight');
+    currentElement.focus();
+    
+    // 滚动到视图
+    currentElement.scrollIntoView({ 
+      behavior: 'smooth', 
+      block: 'center',
+      inline: 'nearest'
+    });
+  }
+
+  /**
+   * 导航高亮项
+   * @param direction 方向 (1: 下一个, -1: 上一个)
+   */
+  navigateHighlight(direction: number): void {
+    if (this.highlightCount === 0) return;
+    
+    let newIndex = this.currentHighlightIndex + direction;
+    
+    // 循环导航
+    if (newIndex < 0) newIndex = this.highlightCount - 1;
+    if (newIndex >= this.highlightCount) newIndex = 0;
+    
+    this.navigateToHighlight(newIndex);
+  }
+
+  /**
+   * 清除搜索
+   */
+  clearSearch(): void {
+    this.searchTerm = '';
+    this.highlightCount = 0;
+    this.currentHighlightIndex = -1;
+    this.clearHighlights();
+  }
+
+  /**
+   * 清除所有高亮
+   */
+  private clearHighlights(): void {
+    if (!this.constitutionContent?.nativeElement) return;
+    
+    const content = this.constitutionContent.nativeElement;
+    const highlights: NodeListOf<HTMLElement> = content.querySelectorAll('.search-highlight');
+    
+    highlights.forEach(highlight => {
+      // 确保有子节点再执行替换
+      if (highlight.childNodes.length > 0) {
+      highlight.replaceWith(...Array.from(highlight.childNodes));
+    } else {
+      // 如果没有子节点,直接替换为文本内容
+      highlight.replaceWith(highlight.textContent || '');
+    }
+    });
+    
+    this.highlightElements = [];
+
+    // 重置搜索状态
+  this.highlightCount = 0;
+  this.currentHighlightIndex = -1;
+  }
+
+  /**
+   * 获取所有文本节点
+   * @param node 起始节点
+   * @returns 文本节点数组
+   */
+  private getTextNodes(node: Node): Node[] {
+    let textNodes: Node[] = [];
+    
+    if (node.nodeType === Node.TEXT_NODE) {
+      textNodes.push(node);
+    } else {
+      const children = node.childNodes;
+      for (let i = 0; i < children.length; i++) {
+        textNodes = textNodes.concat(this.getTextNodes(children[i]));
+      }
+    }
+    
+    return textNodes.filter(n => n.nodeValue?.trim().length ?? 0 > 0);
+  }
+
+  /**
+   * 转义正则表达式特殊字符
+   * @param string 原始字符串
+   * @returns 转义后的字符串
+   */
+  private escapeRegExp(string: string): string {
+    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+  }
+
+  /**
+   * 高亮版本差异
+   * @param event 鼠标事件
+   * @param type 高亮类型 ('diff' | 'new')
+   */
   highlightVersion(event: MouseEvent, type: 'diff' | 'new'): void {
-    const elements = document.querySelectorAll(`.${type}-highlight`);
+    const elements = document.querySelectorAll(`.${type}-highlight`) as NodeListOf<HTMLElement>;
+    
     elements.forEach(el => {
       el.classList.add('active-highlight');
     });
     
-    const handler = () => {
+    const mouseUpHandler = () => {
       elements.forEach(el => {
         el.classList.remove('active-highlight');
       });
-      document.removeEventListener('mouseup', handler);
+      document.removeEventListener('mouseup', mouseUpHandler);
     };
     
-    document.addEventListener('mouseup', handler);
+    document.addEventListener('mouseup', mouseUpHandler);
   }
 }

+ 4 - 3
travel-web/src/modules/pc-home/pages/page-home/page-home.html

@@ -162,7 +162,9 @@
           <i class="fas fa-dove"></i>
         </div>
         <p>您好!我是赣鄱小鹤,24小时为您服务</p>
-        <button (click)="toggleChat()" style="margin-top: 1.5rem; background: var(--mountain-green); color: white; border: none; padding: 0.8rem 2rem; border-radius: 30px; cursor: pointer; font-weight: bold; transition: all 0.3s ease;">
+        <button 
+        (click)="openChat()"
+        style="margin-top: 1.5rem; background: var(--mountain-green); color: white; border: none; padding: 0.8rem 2rem; border-radius: 30px; cursor: pointer; font-weight: bold; transition: all 0.3s ease;">
           开始咨询
         </button>
       </div>
@@ -227,5 +229,4 @@
   <div class="copyright">
     © 2023 江西数字文化发展协会 版权所有 | 赣ICP备12345678号
   </div>
-</footer>
-<app-chat-assistant *ngIf="showChat" (closeChat)="toggleChat()"></app-chat-assistant>
+</footer>

+ 7 - 6
travel-web/src/modules/pc-home/pages/page-home/page-home.ts

@@ -2,25 +2,26 @@
 import { Component, AfterViewInit, ViewChild, ElementRef, OnDestroy } from '@angular/core';
 import { Autoplay, Pagination, Navigation } from 'swiper/modules';
 import { CommonModule } from '@angular/common';
-import { ChatAssistant } from '../chat-assistant/chat-assistant';
+import { ChatService } from '../../../shared/chat-assistant/chat.service'; 
 import Swiper from 'swiper';
 import * as echarts from 'echarts';
 
 @Component({
   selector: 'app-page-home',
   standalone: true,
-  imports: [ChatAssistant,CommonModule],
+  imports: [CommonModule],
   templateUrl: './page-home.html',
-  styleUrls: ['./page-home.scss'],
+  styleUrls: ['./page-home.scss']
 })
 export class PageHome implements AfterViewInit, OnDestroy {
   @ViewChild('swiperContainer') swiperContainer!: ElementRef;
   @ViewChild('galleryChart') galleryChart!: ElementRef;
   @ViewChild('xrChart') xrChart!: ElementRef;
   @ViewChild('dashboardChart') dashboardChart!: ElementRef;
-  showChat=false;
-  toggleChat(){
-    this.showChat=!this.showChat;
+  constructor(private chatService: ChatService) {}
+
+  openChat() {
+    this.chatService.openChat();
   }
 
   private swiper: any;

+ 0 - 0
travel-web/src/modules/pc-home/pages/chat-assistant/chat-assistant.html → travel-web/src/modules/shared/chat-assistant/chat-assistant.html


+ 196 - 0
travel-web/src/modules/shared/chat-assistant/chat-assistant.scss

@@ -0,0 +1,196 @@
+@use "sass:color";
+
+/* 颜色变量定义 */
+$mountain-green: #5d8c5a;
+$primary-blue: #42a5f5;
+$light-yellow: #fff9e6;
+$light-green-border: #c8e6c9;
+
+.chat-container {
+  position: fixed;
+  bottom: 20px;
+  right: 20px;
+  width: 350px;
+  max-width: 90%;
+  height: 500px;
+  background: $light-yellow; /* 淡黄色背景 */
+  border: 2px solid $light-green-border; /* 淡绿色边框 */
+  border-radius: 15px;
+  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
+  display: flex;
+  flex-direction: column;
+  z-index: 1000;
+  overflow: hidden;
+  animation: fadeInUp 0.3s ease-out forwards;
+
+  /* 确保在最上层 */
+  &:not(:defined) {
+    display: none;
+  }
+}
+
+/* 头部样式 */
+.chat-header {
+  background: linear-gradient(
+    90deg, 
+    $mountain-green, 
+    color.adjust($mountain-green, $lightness: -10%)
+  );
+  color: white;
+  padding: 15px;
+  display: flex;
+  align-items: center;
+  position: relative;
+
+  i {
+    font-size: 1.2rem;
+    margin-right: 10px;
+  }
+
+  h3 {
+    margin: 0;
+    font-size: 1rem;
+    font-weight: 600;
+  }
+
+  .close-btn {
+    position: absolute;
+    right: 15px;
+    background: transparent;
+    border: none;
+    color: white;
+    cursor: pointer;
+    font-size: 1rem;
+    transition: transform 0.2s;
+
+    &:hover {
+      transform: scale(1.1);
+    }
+  }
+}
+
+/* 消息区域 */
+.chat-messages {
+  flex: 1;
+  padding: 15px;
+  overflow-y: auto;
+  background: $light-yellow; /* 淡黄色背景 */
+
+  .message {
+    display: flex;
+    margin-bottom: 15px;
+    transition: transform 0.3s ease;
+
+    &:hover {
+      transform: translateX(5px);
+    }
+
+    /* 用户消息气泡 */
+    &.user {
+      justify-content: flex-end;
+      
+      .content {
+        background: $primary-blue; /* 蓝色气泡 */
+        color: white;
+        border-radius: 15px 15px 0 15px;
+        box-shadow: 0 2px 5px rgba($primary-blue, 0.3);
+      }
+    }
+
+    /* AI消息气泡 */
+    &.assistant {
+      justify-content: flex-start;
+      
+      .content {
+        background: white;
+        color: #333;
+        border-radius: 15px 15px 15px 0;
+        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+        border: 1px solid #eee;
+      }
+    }
+
+    .avatar {
+      width: 32px;
+      height: 32px;
+      border-radius: 50%;
+      background: #f5f5f5;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin: 0 10px;
+      flex-shrink: 0;
+
+      i {
+        color: $mountain-green;
+      }
+    }
+
+    .content {
+      max-width: 70%;
+      padding: 10px 15px;
+      
+      .text {
+        white-space: pre-wrap;
+        line-height: 1.4;
+        font-size: 0.9rem;
+      }
+    }
+  }
+}
+
+/* 输入区域 */
+.chat-input {
+  display: flex;
+  padding: 10px;
+  border-top: 1px solid #eee;
+  background: white;
+
+  input {
+    flex: 1;
+    padding: 10px 15px;
+    border: 1px solid #ddd;
+    border-radius: 30px;
+    outline: none;
+    font-size: 0.9rem;
+    transition: all 0.3s;
+
+    &:focus {
+      border-color: $primary-blue;
+      box-shadow: 0 0 0 2px rgba($primary-blue, 0.2);
+    }
+  }
+
+  button {
+    width: 40px;
+    height: 40px;
+    border-radius: 50%;
+    background: $mountain-green;
+    color: white;
+    border: none;
+    margin-left: 10px;
+    cursor: pointer;
+    transition: all 0.3s;
+
+    &:hover {
+      background: color.adjust($mountain-green, $lightness: -10%);
+    }
+
+    &:disabled {
+      background: #ccc;
+      cursor: not-allowed;
+    }
+  }
+}
+
+/* 动画 */
+@keyframes fadeInUp {
+  from {
+    transform: translateY(20px);
+    opacity: 0;
+  }
+  to {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}

+ 4 - 2
travel-web/src/modules/pc-home/pages/chat-assistant/chat-assistant.ts → travel-web/src/modules/shared/chat-assistant/chat-assistant.ts

@@ -1,7 +1,8 @@
 import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
-import { TestCompletion } from '../../../../lib/completion';
+import { TestCompletion } from '../../../lib/completion';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
+import { ChatService } from './chat.service';
 
 @Component({
   selector: 'app-chat-assistant',
@@ -19,13 +20,14 @@ export class ChatAssistant implements AfterViewInit {
   userInput = '';
   isLoading = false;
   
-  constructor() {}
+  constructor(private chatService: ChatService) {}
 
   ngAfterViewInit() {
     this.scrollToBottom();
   }
 
   closeChat() {
+    this.chatService.closeChat(); // 使用服务关闭
     // 这里添加关闭聊天窗口的逻辑
     console.log('Close chat clicked');
     // 如果需要实际关闭,可以添加相应逻辑

+ 24 - 0
travel-web/src/modules/shared/chat-assistant/chat.service.ts

@@ -0,0 +1,24 @@
+// shared/chat-assistant/chat.service.ts
+import { Injectable } from '@angular/core';
+import { BehaviorSubject } from 'rxjs';
+
+@Injectable({
+  providedIn: 'root' // 全局单例服务
+})
+export class ChatService {
+  // 使用BehaviorSubject存储当前状态(默认关闭)
+  private isChatVisible = new BehaviorSubject<boolean>(false);
+  
+  // 公开为可观察对象
+  isChatVisible$ = this.isChatVisible.asObservable();
+
+  // 打开聊天窗口
+  openChat() {
+    this.isChatVisible.next(true);
+  }
+
+  // 关闭聊天窗口
+  closeChat() {
+    this.isChatVisible.next(false);
+  }
+}