2 커밋 2d11e7e353 ... 1656796c1d

작성자 SHA1 메시지 날짜
  0235624 1656796c1d Merge branch 'master' of http://git.fmode.cn:3000/19323826807/travel-wed 1 주 전
  0235624 9a1ffaa1c7 feat;new 1 주 전

+ 1 - 1
travel-web/src/lib/ncloud.ts

@@ -1,7 +1,7 @@
 // CloudObject.ts
 
 let serverURL = `https://dev.fmode.cn/parse`;
-if (location.protocol == "http:") {
+if (location.protocol == "http://127.0.0.1:4040/apps") {
     serverURL = `http://dev.fmode.cn:1337/parse`;
 }
 

+ 174 - 18
travel-web/src/modules/pc-home/pages/page-dynamic/page-dynamic.html

@@ -248,32 +248,188 @@
   <div class="space-grid">
     <!-- 项目众包 -->
     <div class="space-card">
-      <div class="space-header">
-        <i class="fas fa-tasks space-icon"></i>
-        <h3 class="space-title">项目众包</h3>
+  <div class="space-header">
+    <i class="fas fa-tasks space-icon"></i>
+    <h3 class="space-title">项目众包</h3>
+  </div>
+  <div class="space-body">
+    <p>以景德镇陶市为原型设计任务大厅,各类任务以陶瓷器皿形式展示</p>
+    
+    <!-- 任务筛选区域 -->
+    <div class="task-filter">
+  <div class="filter-group">
+    <label>状态:</label>
+    <select [(ngModel)]="taskFilter.status" (change)="filterTasks()">
+      <option value="">全部状态</option>
+      <option *ngFor="let status of taskStatuses" [value]="status">
+        {{status}}
+      </option>
+    </select>
+  </div>
+  
+  <div class="filter-group">
+    <label>类型:</label>
+    <select [(ngModel)]="taskFilter.typeId" (change)="filterTasks()">
+      <option value="">全部类型</option>
+      <option *ngFor="let type of taskTypes" [value]="type.id">
+        {{type.name}}
+      </option>
+    </select>
+  </div>
+      
+      <div class="filter-group">
+        <label>奖励:</label>
+        <input type="number" [(ngModel)]="taskFilter.minReward" (change)="filterTasks()" placeholder="最小">
+        <span>至</span>
+        <input type="number" [(ngModel)]="taskFilter.maxReward" (change)="filterTasks()" placeholder="最大">
       </div>
-      <div class="space-body">
-        <p>以景德镇陶市为原型设计任务大厅,各类任务以陶瓷器皿形式展示</p>
-        
-        <div class="ceramic-items">
-          <div class="ceramic-item vase">
-            <i class="fas fa-book" style="font-size: 2rem; color: var(--primary-blue);"></i>
-            <span style="margin-top: 10px;">传记任务</span>
-          </div>
-          <div class="ceramic-item bowl">
-            <i class="fas fa-paint-brush" style="font-size: 2rem; color: var(--primary-blue);"></i>
-            <span style="margin-top: 10px;">IP设计</span>
+      
+      <button class="reset-btn" (click)="resetTaskFilter()">
+        <i class="fas fa-sync-alt"></i> 重置
+      </button>
+    </div>
+    
+    <!-- 任务展示区 -->
+    <div class="ceramic-tasks">
+      @for (task of filteredCrowdTasks; track task.id) {
+      <div class="ceramic-item" 
+           [class.vase]="task.get('reward') > 5000"
+           [class.bowl]="task.get('reward') <= 5000"
+           [id]="'task-' + task.id"
+           (click)="viewTaskDetail(task)">
+        <div class="task-icon">
+          <i class="fas {{getTaskTypeIcon(task)}}"></i>
+        </div>
+        <div class="task-content">
+          <h4>{{task.get('title') | truncate: 8}}</h4>
+          <p class="reward">
+            <i class="fas fa-coins"></i> 
+            {{task.get('reward')}} 积分
+          </p>
+          <p class="deadline">
+            <i class="fas fa-clock"></i> 
+            {{task.get('deadline') | date: 'yyyy-MM-dd'}}
+          </p>
+          <div class="task-status" [class.pending]="task.get('status') === '待领取'"
+               [class.progress]="task.get('status') === '进行中'"
+               [class.completed]="task.get('status') === '已完成'">
+            {{task.get('status')}}
           </div>
         </div>
         
-        <p>接单成功时触发"陶轮旋转"动画效果,增强交互体验</p>
-        
-        <button class="ocr-btn" style="width: 100%; margin-top: 1.5rem; background: var(--mountain-green);">
-          查看项目任务
+        @if (task.get('status') === '待领取' && currentUser) {
+        <div class="task-accept" (click)="$event.stopPropagation(); acceptTask(task)">
+          <i class="fas fa-hand-paper"></i>
+        </div>
+        }
+      </div>
+      }
+      
+      @if (filteredCrowdTasks.length === 0) {
+      <div class="no-tasks">
+        <i class="fas fa-inbox"></i>
+        <p>暂无匹配的任务</p>
+        <button class="reset-btn" (click)="resetTaskFilter()">
+          重置筛选条件
         </button>
       </div>
+      }
+    </div>
+    
+    <p>接单成功时触发"陶轮旋转"动画效果,增强交互体验</p>
+  </div>
+</div>
+
+<!-- 任务详情模态框 -->
+@if (showTaskDetail && selectedTask) {
+<div class="modal-backdrop" (click)="showTaskDetail = false">
+  <div class="modal-content" (click)="$event.stopPropagation()">
+    <button class="modal-close" (click)="showTaskDetail = false">
+      <i class="fas fa-times"></i>
+    </button>
+    
+    <div class="task-header">
+      <div class="task-icon">
+        <i class="fas {{getTaskTypeIcon(selectedTask)}}"></i>
+      </div>
+      <h3>{{selectedTask.get('title')}}</h3>
+      <div class="task-meta">
+        <span class="task-type">
+          <i class="fas fa-tag"></i> {{getTaskTypeName(selectedTask)}}
+        </span>
+        <span class="task-location">
+          <i class="fas fa-map-marker-alt"></i> {{getLocationName(selectedTask)}}
+        </span>
+      </div>
+    </div>
+    
+    <div class="task-details">
+      <div class="detail-row">
+        <div class="detail-item">
+          <i class="fas fa-coins"></i>
+          <div>
+            <strong>任务奖励</strong>
+            <p>{{selectedTask.get('reward')}} 积分</p>
+          </div>
+        </div>
+        
+        <div class="detail-item">
+          <i class="fas fa-clock"></i>
+          <div>
+            <strong>截止时间</strong>
+            <p>{{selectedTask.get('deadline') | date: 'yyyy-MM-dd'}}</p>
+          </div>
+        </div>
+      </div>
+      
+      <div class="detail-item full-width">
+        <i class="fas fa-user"></i>
+        <div>
+          <strong>发布者</strong>
+          <p>{{getPublisherName(selectedTask)}}</p>
+        </div>
+      </div>
+      
+      <div class="detail-item full-width">
+        <i class="fas fa-align-left"></i>
+        <div>
+          <strong>任务描述</strong>
+          <p>{{selectedTask.get('description')}}</p>
+        </div>
+      </div>
+      
+      <div class="detail-item full-width">
+        <i class="fas fa-tasks"></i>
+        <div>
+          <strong>任务状态</strong>
+          <p class="task-status" 
+             [class.pending]="selectedTask.get('status') === '待领取'"
+             [class.progress]="selectedTask.get('status') === '进行中'"
+             [class.completed]="selectedTask.get('status') === '已完成'">
+            {{selectedTask.get('status')}}
+          </p>
+        </div>
+      </div>
     </div>
     
+    <div class="modal-footer">
+      @if (selectedTask.get('status') === '待领取' && currentUser) {
+      <button class="accept-btn" (click)="acceptTask(selectedTask)" [disabled]="acceptingTask">
+        @if (acceptingTask) {
+        <i class="fas fa-spinner fa-spin"></i> 接单中...
+        } @else {
+        <i class="fas fa-hand-paper"></i> 接取任务
+        }
+      </button>
+      }
+      <button class="close-btn" (click)="showTaskDetail = false">
+        <i class="fas fa-times"></i> 关闭
+      </button>
+    </div>
+  </div>
+</div>
+}
+    
     <!-- 资源交易所 -->
     <div class="space-card">
       <div class="space-header">

+ 339 - 0
travel-web/src/modules/pc-home/pages/page-dynamic/page-dynamic.scss

@@ -619,6 +619,345 @@
   padding: 2rem;
 }
 
+/* 任务筛选 */
+.task-filter {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 1rem;
+  margin: 1.5rem 0;
+  padding: 1.2rem;
+  background: rgba(232, 195, 77, 0.1);
+  border-radius: 10px;
+  
+  .filter-group {
+    display: flex;
+    align-items: center;
+    gap: 0.5rem;
+    
+    label {
+      font-weight: bold;
+      color: var(--primary-blue);
+    }
+    
+    select, input {
+      padding: 0.5rem;
+      border: 1px solid #ddd;
+      border-radius: 5px;
+      background: white;
+    }
+    
+    input {
+      width: 80px;
+    }
+  }
+  
+  .reset-btn {
+    padding: 0.5rem 1rem;
+    background: var(--primary-blue);
+    color: white;
+    border: none;
+    border-radius: 20px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    
+    &:hover {
+      background: var(--river-blue);
+    }
+  }
+}
+
+/* 任务展示 */
+.ceramic-tasks {
+  min-height: 300px; /* 防止无任务时布局塌陷 */
+}
+
+/* 任务项样式优化 */
+.ceramic-item {
+  transition: transform 0.3s ease, box-shadow 0.3s ease;
+  
+  &:hover {
+    transform: translateY(-5px);
+    box-shadow: 0 5px 15px rgba(0,0,0,0.1);
+    z-index: 2;
+  }
+  
+  // 不同类型任务的颜色区分
+  &.vase {
+    border-color: var(--primary-blue);
+    background: linear-gradient(135deg, #e6f7ff 0%, #d1e8ff 100%);
+  }
+  
+  &.bowl {
+    border-color: var(--gold-yellow);
+    background: linear-gradient(135deg, #fff9db 0%, #fff3b0 100%);
+  }
+
+  
+  .task-icon {
+    width: 50px;
+    height: 50px;
+    background: rgba(42, 93, 170, 0.1);
+    border-radius: 50%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-bottom: 0.8rem;
+    
+    i {
+      font-size: 1.5rem;
+      color: var(--primary-blue);
+    }
+  }
+  
+  .task-content {
+    h4 {
+      font-size: 1.1rem;
+      margin-bottom: 0.5rem;
+      color: var(--dark-charcoal);
+    }
+    
+    p {
+      font-size: 0.9rem;
+      margin: 0.2rem 0;
+      color: #555;
+      
+      &.reward {
+        color: var(--gold-yellow);
+        font-weight: bold;
+      }
+      
+      &.deadline {
+        color: var(--ceramic-red);
+      }
+    }
+    
+    .task-status {
+      display: inline-block;
+      padding: 0.2rem 0.8rem;
+      border-radius: 15px;
+      font-size: 0.8rem;
+      margin-top: 0.5rem;
+      
+      &.pending {
+        background: rgba(74, 134, 232, 0.2);
+        color: var(--primary-blue);
+      }
+      
+      &.progress {
+        background: rgba(232, 195, 77, 0.2);
+        color: var(--gold-yellow);
+      }
+      
+      &.completed {
+        background: rgba(74, 107, 61, 0.2);
+        color: var(--mountain-green);
+      }
+    }
+  }
+  
+  .task-accept {
+    position: absolute;
+    bottom: 10px;
+    right: 10px;
+    width: 30px;
+    height: 30px;
+    background: rgba(74, 134, 232, 0.2);
+    border-radius: 50%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    z-index: 2;
+    
+    i {
+      color: var(--primary-blue);
+    }
+    
+    &:hover {
+      background: var(--primary-blue);
+      
+      i {
+        color: white;
+      }
+    }
+  }
+  
+  &:hover {
+    transform: translateY(-10px);
+    box-shadow: 0 10px 20px rgba(0,0,0,0.1);
+  }
+  
+  // 接单动画
+  &.accepting {
+    animation: potteryWheel 2s ease-in-out;
+  }
+}
+
+@keyframes potteryWheel {
+  0% { transform: rotate(0) translateY(0); }
+  25% { transform: rotate(90deg) translateY(-10px); }
+  50% { transform: rotate(180deg) translateY(0); }
+  75% { transform: rotate(270deg) translateY(-10px); }
+  100% { transform: rotate(360deg) translateY(0); }
+}
+
+/* 无任务提示 */
+.no-tasks {
+  grid-column: 1 / -1;
+  text-align: center;
+  padding: 2rem;
+  color: #666;
+  
+  i {
+    font-size: 3rem;
+    margin-bottom: 1rem;
+    color: var(--primary-blue);
+  }
+  
+  .reset-btn {
+    margin-top: 1rem;
+    padding: 0.5rem 1.5rem;
+    background: var(--primary-blue);
+    color: white;
+    border: none;
+    border-radius: 30px;
+    cursor: pointer;
+  }
+}
+
+/* 任务详情 */
+.task-header {
+  text-align: center;
+  margin-bottom: 1.5rem;
+  
+  .task-icon {
+    width: 80px;
+    height: 80px;
+    background: rgba(42, 93, 170, 0.1);
+    border-radius: 50%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin: 0 auto 1rem;
+    
+    i {
+      font-size: 2.5rem;
+      color: var(--primary-blue);
+    }
+  }
+  
+  h3 {
+    color: var(--primary-blue);
+    margin-bottom: 0.5rem;
+  }
+  
+  .task-meta {
+    display: flex;
+    justify-content: center;
+    gap: 1.5rem;
+    color: #666;
+    font-size: 0.95rem;
+  }
+}
+
+.task-details {
+  .detail-row {
+    display: flex;
+    gap: 1rem;
+    margin-bottom: 1rem;
+  }
+  
+  .detail-item {
+    flex: 1;
+    background: var(--porcelain-white);
+    padding: 1rem;
+    border-radius: 8px;
+    display: flex;
+    align-items: center;
+    gap: 0.8rem;
+    
+    i {
+      font-size: 1.5rem;
+      color: var(--primary-blue);
+    }
+    
+    &.full-width {
+      flex: 0 0 100%;
+      margin-bottom: 1rem;
+    }
+    
+    strong {
+      display: block;
+      margin-bottom: 0.3rem;
+      color: var(--primary-blue);
+    }
+  }
+  
+  .task-status {
+    display: inline-block;
+    padding: 0.3rem 1rem;
+    border-radius: 20px;
+    font-weight: bold;
+    
+    &.pending {
+      background: rgba(74, 134, 232, 0.2);
+      color: var(--primary-blue);
+    }
+    
+    &.progress {
+      background: rgba(232, 195, 77, 0.2);
+      color: var(--gold-yellow);
+    }
+    
+    &.completed {
+      background: rgba(74, 107, 61, 0.2);
+      color: var(--mountain-green);
+    }
+  }
+}
+
+.modal-footer {
+  display: flex;
+  justify-content: center;
+  gap: 1rem;
+  margin-top: 1.5rem;
+  
+  .accept-btn {
+    background: var(--primary-blue);
+    color: white;
+    border: none;
+    padding: 0.8rem 1.5rem;
+    border-radius: 30px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    
+    &:hover:not([disabled]) {
+      background: var(--river-blue);
+      transform: translateY(-3px);
+    }
+    
+    &[disabled] {
+      opacity: 0.7;
+      cursor: not-allowed;
+    }
+  }
+  
+  .close-btn {
+    background: #f0f0f0;
+    color: #555;
+    border: none;
+    padding: 0.8rem 1.5rem;
+    border-radius: 30px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    
+    &:hover {
+      background: #e0e0e0;
+    }
+  }
+}
+
 .ceramic-items {
   display: flex;
   justify-content: space-around;

+ 253 - 0
travel-web/src/modules/pc-home/pages/page-dynamic/page-dynamic.ts

@@ -62,6 +62,28 @@ export class PageDynamic implements AfterViewInit {
   };
   selectedAward: CloudObject | null = null;
   showAwardDetail = false;
+
+  //众包任务相关属性
+  crowdTasks: CloudObject[] = [];
+  filteredCrowdTasks: CloudObject[] = [];
+  
+  taskStatuses = ['待领取', '进行中', '已完成'] as const;
+  taskTypes = [
+  { id: '1', name: '设计', icon: 'fa-paint-brush' },
+  { id: '2', name: '开发', icon: 'fa-code' },
+  { id: '3', name: '文案', icon: 'fa-pen' },
+  { id: '4', name: '调研', icon: 'fa-search' },
+  { id: '5', name: '翻译', icon: 'fa-language' }
+];
+   taskFilter = {
+    status: '' as '' | '待领取' | '进行中' | '已完成',
+    typeId: '',
+    minReward: 0,
+    maxReward: 10000
+  };
+  selectedTask: CloudObject | null = null;
+  showTaskDetail = false;
+  acceptingTask = false;
   
   // 生成年份数组方法
   generateYears(start: number, end: number): number[] {
@@ -102,6 +124,10 @@ export class PageDynamic implements AfterViewInit {
     // 初始化模拟数据
     this.createMockAwards();
     await this.loadAwardData();
+
+    // 加载众包任务数据
+    await this.loadCrowdTasks();
+    await this.loadTaskTypes();
   }
 
   // 加载奖项数据
@@ -560,7 +586,234 @@ getAchievementIcon(category: string): string {
   return icons[category] || 'star';
 }
 
+// 加载众包任务
+  async loadCrowdTasks() {
+    try {
+      const query = new CloudQuery("CrowdTask");
+      query.include("type");
+      query.include("location");
+      query.include("publisher");
+      this.crowdTasks = await query.find();
+      this.filteredCrowdTasks = [...this.crowdTasks];
+    } catch (e) {
+      console.error('加载众包任务失败:', e);
+      // 创建模拟数据
+      this.createMockTasks();
+    }
+  }
+
+  // 加载任务类型
+  async loadTaskTypes() {
+  // 直接使用本地定义的 taskTypes,不再从云端加载
+  // 如果需要,可以保留从云端加载的逻辑,但需要调整类型
+  console.log('使用本地任务类型定义');
+  
+  // 如果需要从云端加载,可以这样调整:
+  /*
+  try {
+    const query = new CloudQuery("TaskType");
+    const types = await query.find();
+    
+    // 转换类型以匹配本地定义
+    this.taskTypes = types.map(t => ({
+      id: t.id!,
+      name: t.get('name') as string,
+      icon: t.get('icon') as string
+    }));
+  } catch (e) {
+    console.error('加载任务类型失败,使用本地定义', e);
+    // 保留本地定义
+  }
+  */
+}
+
+  // 创建模拟类型
+  createMockType(id: string, name: string, icon: string): CloudObject {
+    const type = new CloudObject("TaskType");
+    type.set({ name, icon });
+    type.id = id;
+    return type;
+  }
 
+  // 创建模拟任务
+  createMockTasks() {
+    const tasks = [
+      {
+        id: '1',
+        title: '江西旅游宣传册设计',
+        description: '设计一本24页的江西旅游宣传册,包含庐山、景德镇等景点',
+        reward: 5000,
+        deadline: new Date('2023-12-31'),
+        status: '待领取' as const,
+        publisher: { objectId: 'user1' },
+        type: { objectId: '1' },
+        location: { objectId: 'loc1' }
+      },
+      {
+        id: '2',
+        title: '文化协会官网开发',
+        description: '开发响应式网站,包含会员管理、活动发布等功能',
+        reward: 15000,
+        deadline: new Date('2023-11-30'),
+        status: '进行中' as const,
+        publisher: { objectId: 'user2' },
+        type: { objectId: '2' },
+        location: { objectId: 'loc2' }
+      },
+      {
+        id: '3',
+        title: '非遗文化研究报告',
+        description: '撰写关于江西非遗文化保护与传承的10万字研究报告',
+        reward: 8000,
+        deadline: new Date('2024-01-15'),
+        status: '待领取' as const,
+        publisher: { objectId: 'user3' },
+        type: { objectId: '4' },
+        location: { objectId: 'loc1' }
+      },
+      {
+        id: '4',
+        title: '瓷器产品文案翻译',
+        description: '将30件景德镇瓷器产品说明翻译成英文',
+        reward: 3000,
+        deadline: new Date('2023-10-31'),
+        status: '已完成' as const,
+        publisher: { objectId: 'user4' },
+        type: { objectId: '5' },
+        location: { objectId: 'loc3' }
+      }
+    ];
+    
+    this.crowdTasks = tasks.map(task => {
+      const obj = new CloudObject("CrowdTask");
+      obj.set(task);
+      obj.id = task.id;
+      return obj;
+    });
+    
+    this.filteredCrowdTasks = [...this.crowdTasks];
+     // 手动触发变更检测
+    this.cdr.detectChanges();
+  }
+
+  // 筛选任务
+  filterTasks() {
+    this.filteredCrowdTasks = this.crowdTasks.filter(task => {
+      const taskStatus = task.get('status');
+      const taskTypeId = task.get('type')?.objectId;
+      const reward = task.get('reward') || 0;
+      
+      // 状态筛选
+      if (this.taskFilter.status && taskStatus !== this.taskFilter.status) {
+        return false;
+      }
+      
+      // 类型筛选
+      if (this.taskFilter.typeId && taskTypeId !== this.taskFilter.typeId) {
+        return false;
+      }
+      
+      // 奖励范围筛选
+      if (reward < this.taskFilter.minReward || reward > this.taskFilter.maxReward) {
+        return false;
+      }
+      
+      return true;
+    });
+  }
+
+  // 重置筛选
+  resetTaskFilter() {
+    this.taskFilter = {
+      status: '',
+      typeId: '',
+      minReward: 0,
+      maxReward: 10000
+    };
+    this.filterTasks();
+  }
+
+  // 查看任务详情
+  viewTaskDetail(task: CloudObject) {
+    this.selectedTask = task;
+    this.showTaskDetail = true;
+  }
+
+  // 获取类型图标
+  getTaskTypeIcon(task: CloudObject): string {
+    const typeId = task.get('type')?.objectId;
+    const type = this.taskTypes.find(t => t.id === typeId);
+    return type?.icon || 'fa-tasks';
+  }
+
+  // 获取类型名称
+  getTaskTypeName(task: CloudObject): string {
+    const typeId = task.get('type')?.objectId;
+    const type = this.taskTypes.find(t => t.id === typeId);
+    return type?.name || '未知类型'; 
+  }
+
+  // 获取空间名称
+  getLocationName(task: CloudObject): string {
+    const location = task.get('location');
+    return location?.get('name') || '未知空间';
+  }
+
+  // 获取发布者名称
+  getPublisherName(task: CloudObject): string {
+    const publisher = task.get('publisher');
+    return publisher?.get('username') || '未知发布者';
+  }
+
+  // 接取任务
+  async acceptTask(task: CloudObject) {
+    if (!this.currentUser) {
+      alert('请先登录!');
+      return;
+    }
+    
+    if (task.get('status') !== '待领取') {
+      alert('该任务无法接取');
+      return;
+    }
+    
+    this.acceptingTask = true;
+    
+    try {
+      // 模拟接单过程
+      await new Promise(resolve => setTimeout(resolve, 1500));
+      
+      // 实际应用中应更新任务状态和执行者
+      // const updatedTask = new CloudObject("CrowdTask");
+      // updatedTask.id = task.id;
+      // updatedTask.set({
+      //   status: '进行中',
+      //   worker: this.currentUser.toPointer()
+      // });
+      // await updatedTask.save();
+      
+      // 更新本地状态
+     task.set({ status: '进行中' });
+     task.set({ worker: this.currentUser?.toPointer() });
+      
+      
+      // 触发陶轮旋转动画
+      const taskElement = document.querySelector(`#task-${task.id}`);
+      if (taskElement) {
+        taskElement.classList.add('accepting');
+        setTimeout(() => {
+          taskElement.classList.remove('accepting');
+        }, 2000);
+      }
+      
+      alert('接单成功!');
+    } catch (e) {
+      console.error('接单失败:', e);
+      alert('接单失败,请重试');
+    } finally {
+      this.acceptingTask = false;
+    }
+  }
 
   
  

+ 121 - 0
travel-web/src/modules/shared/nav-pc-top-menu/nav-pc-top-menu.html

@@ -16,7 +16,128 @@
             <a routerLink="/dynamic">会员服务</a>
             <a routerLink="/us">关于我们</a>
         </nav>
+        
+        <div class="user-section">
+            @if (isLoading) {
+                <div class="loading-spinner"></div>
+            } @else {
+                @if (user) {
+                    <div class="user-profile" (click)="toggleMenu()">
+                        <div class="avatar">
+                            @if (userAvatar) {
+                                <img [src]="userAvatar" alt="用户头像">
+                            } @else {
+                                <span>{{ userInitials }}</span>
+                            }
+                        </div>
+                        <span class="username">{{ user.get('username') || '用户' }}</span>
+                        <i class="fas fa-caret-down"></i>
+                    </div>
+                    @if (showMenu) {
+                        <div class="dropdown-menu">
+                            <a routerLink="/profile" class="dropdown-item">
+                                <i class="fas fa-user-circle"></i> 个人中心
+                            </a>
+                            <a (click)="logout()" class="dropdown-item">
+                                <i class="fas fa-sign-out-alt"></i> 退出登录
+                            </a>
+                        </div>
+                    }
+                } @else {
+                    <button class="btn btn-login" (click)="openAuthModal('login')">登录</button>
+                    <button class="btn btn-signup" (click)="openAuthModal('register')">注册</button>
+                }
+            }
+        </div>
     </div>
 </header>
 
+<!-- 登录/注册模态框 -->
+@if (showAuthModal) {
+    <div class="auth-modal-overlay" (click)="closeAuthModal()">
+        <div class="auth-modal" (click)="$event.stopPropagation()">
+            <button class="close-btn" (click)="closeAuthModal()">
+                <i class="fas fa-times"></i>
+            </button>
+            
+            <div class="auth-tabs">
+                <div class="tab" [class.active]="authType === 'login'" (click)="setAuthType('login')">登录</div>
+                <div class="tab" [class.active]="authType === 'register'" (click)="setAuthType('register')">注册</div>
+            </div>
+            
+            <div class="auth-form">
+                @if (authType === 'login') {
+                    <form (ngSubmit)="login()">
+                        <div class="form-group">
+                            <i class="fas fa-user"></i>
+                            <input type="text" placeholder="用户名/手机号" [(ngModel)]="loginUsername" name="loginUsername" required>
+                        </div>
+                        <div class="form-group">
+                            <i class="fas fa-lock"></i>
+                            <input type="password" placeholder="密码" [(ngModel)]="loginPassword" name="loginPassword" required>
+                        </div>
+                        <div class="remember-forgot">
+                            <label>
+                                <input type="checkbox" [(ngModel)]="rememberMe" name="rememberMe"> 记住我
+                            </label>
+                            <a href="#">忘记密码?</a>
+                        </div>
+                        <button type="submit" class="btn btn-primary" [disabled]="isLoggingIn">
+                            @if (isLoggingIn) {
+                                <i class="fas fa-spinner fa-spin"></i> 登录中...
+                            } @else {
+                                登录
+                            }
+                        </button>
+                        <div class="social-login">
+                            <p>快速登录</p>
+                            <div class="social-icons">
+                                <button class="social-btn wechat"><i class="fab fa-weixin"></i></button>
+                                <button class="social-btn qq"><i class="fab fa-qq"></i></button>
+                                <button class="social-btn weibo"><i class="fab fa-weibo"></i></button>
+                            </div>
+                        </div>
+                    </form>
+                } @else {
+                    <form (ngSubmit)="register()">
+                        <div class="form-group">
+                            <i class="fas fa-user"></i>
+                            <input type="text" placeholder="用户名" [(ngModel)]="registerUsername" name="registerUsername" required>
+                        </div>
+                        <div class="form-group">
+                            <i class="fas fa-envelope"></i>
+                            <input type="email" placeholder="邮箱" [(ngModel)]="registerEmail" name="registerEmail" required>
+                        </div>
+                        <div class="form-group">
+                            <i class="fas fa-phone"></i>
+                            <input type="tel" placeholder="手机号" [(ngModel)]="registerPhone" name="registerPhone" required>
+                        </div>
+                        <div class="form-group">
+                            <i class="fas fa-lock"></i>
+                            <input type="password" placeholder="密码" [(ngModel)]="registerPassword" name="registerPassword" required>
+                        </div>
+                        <div class="form-group">
+                            <i class="fas fa-lock"></i>
+                            <input type="password" placeholder="确认密码" [(ngModel)]="registerPasswordConfirm" name="registerPasswordConfirm" required>
+                        </div>
+                        <div class="terms">
+                            <label>
+                                <input type="checkbox" [(ngModel)]="acceptTerms" name="acceptTerms" required>
+                                我已阅读并同意<a href="#">《用户协议》</a>和<a href="#">《隐私政策》</a>
+                            </label>
+                        </div>
+                        <button type="submit" class="btn btn-primary" [disabled]="isRegistering">
+                            @if (isRegistering) {
+                                <i class="fas fa-spinner fa-spin"></i> 注册中...
+                            } @else {
+                                注册
+                            }
+                        </button>
+                    </form>
+                }
+            </div>
+        </div>
+    </div>
+}
+
 <router-outlet></router-outlet>

+ 355 - 0
travel-web/src/modules/shared/nav-pc-top-menu/nav-pc-top-menu.scss

@@ -102,4 +102,359 @@
     .logo-subtext {
         font-size: 0.8rem;
     }
+}
+
+/* 新增用户相关样式 */
+.user-section {
+    display: flex;
+    align-items: center;
+    gap: 1rem;
+    position: relative;
+
+    .loading-spinner {
+        width: 24px;
+        height: 24px;
+        border: 3px solid rgba(42, 93, 170, 0.3);
+        border-radius: 50%;
+        border-top-color: #2a5daa;
+        animation: spin 1s linear infinite;
+    }
+
+    @keyframes spin {
+        to { transform: rotate(360deg); }
+    }
+
+    .btn {
+        padding: 0.5rem 1rem;
+        border-radius: 4px;
+        font-weight: 500;
+        cursor: pointer;
+        transition: all 0.3s ease;
+        
+        &-login {
+            background: transparent;
+            border: 1px solid #2a5daa;
+            color: #2a5daa;
+            
+            &:hover {
+                background: #2a5daa;
+                color: white;
+            }
+        }
+        
+        &-signup {
+            background: #2a5daa;
+            border: 1px solid #2a5daa;
+            color: white;
+            
+            &:hover {
+                background: #1d4a8a;
+            }
+        }
+    }
+
+    .user-profile {
+        display: flex;
+        align-items: center;
+        gap: 0.5rem;
+        cursor: pointer;
+        padding: 0.5rem;
+        border-radius: 4px;
+        transition: background 0.3s;
+        
+        &:hover {
+            background: rgba(42, 93, 170, 0.1);
+        }
+        
+        .avatar {
+            width: 40px;
+            height: 40px;
+            border-radius: 50%;
+            background: linear-gradient(135deg, #2a5daa, #4a86e8);
+            color: white;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-weight: bold;
+            font-size: 1rem;
+            
+            img {
+                width: 100%;
+                height: 100%;
+                border-radius: 50%;
+                object-fit: cover;
+            }
+        }
+        
+        .username {
+            font-weight: 500;
+            color: #333;
+        }
+    }
+    
+    .dropdown-menu {
+        position: absolute;
+        top: 100%;
+        right: 0;
+        background: white;
+        box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+        border-radius: 8px;
+        overflow: hidden;
+        z-index: 1001;
+        min-width: 180px;
+        animation: fadeIn 0.3s ease;
+        
+        @keyframes fadeIn {
+            from { opacity: 0; transform: translateY(-10px); }
+            to { opacity: 1; transform: translateY(0); }
+        }
+        
+        .dropdown-item {
+            display: flex;
+            align-items: center;
+            padding: 0.75rem 1.5rem;
+            text-decoration: none;
+            color: #333;
+            transition: all 0.2s;
+            gap: 0.75rem;
+            
+            &:hover {
+                background: #f5f9ff;
+                color: #2a5daa;
+            }
+            
+            i {
+                width: 20px;
+                text-align: center;
+            }
+        }
+    }
+}
+
+/* 模态框样式 */
+.auth-modal-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, 0.6);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 2000;
+    backdrop-filter: blur(3px);
+}
+
+.auth-modal {
+    background: white;
+    border-radius: 16px;
+    width: 400px;
+    max-width: 90%;
+    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
+    position: relative;
+    overflow: hidden;
+    
+    .close-btn {
+        position: absolute;
+        top: 15px;
+        right: 15px;
+        background: none;
+        border: none;
+        font-size: 1.2rem;
+        color: #888;
+        cursor: pointer;
+        transition: color 0.3s;
+        z-index: 10;
+        
+        &:hover {
+            color: #2a5daa;
+        }
+    }
+    
+    .auth-tabs {
+        display: flex;
+        background: #f8f9fa;
+        border-bottom: 1px solid #eee;
+        
+        .tab {
+            flex: 1;
+            text-align: center;
+            padding: 1.2rem;
+            font-weight: 500;
+            cursor: pointer;
+            transition: all 0.3s;
+            color: #666;
+            
+            &.active {
+                color: #2a5daa;
+                border-bottom: 3px solid #2a5daa;
+                background: white;
+            }
+            
+            &:hover:not(.active) {
+                background: #f0f4ff;
+            }
+        }
+    }
+    
+    .auth-form {
+        padding: 2rem;
+        
+        .form-group {
+            position: relative;
+            margin-bottom: 1.2rem;
+            
+            i {
+                position: absolute;
+                left: 15px;
+                top: 50%;
+                transform: translateY(-50%);
+                color: #888;
+            }
+            
+            input {
+                width: 100%;
+                padding: 0.8rem 1rem 0.8rem 40px;
+                border: 1px solid #ddd;
+                border-radius: 8px;
+                font-size: 1rem;
+                transition: border-color 0.3s;
+                
+                &:focus {
+                    border-color: #2a5daa;
+                    box-shadow: 0 0 0 3px rgba(42, 93, 170, 0.2);
+                    outline: none;
+                }
+            }
+        }
+        
+        .remember-forgot {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-bottom: 1.5rem;
+            font-size: 0.9rem;
+            
+            label {
+                display: flex;
+                align-items: center;
+                gap: 0.5rem;
+            }
+            
+            a {
+                color: #2a5daa;
+                text-decoration: none;
+                
+                &:hover {
+                    text-decoration: underline;
+                }
+            }
+        }
+        
+        .terms {
+            margin: 1rem 0 1.5rem;
+            font-size: 0.9rem;
+            
+            a {
+                color: #2a5daa;
+                text-decoration: none;
+                
+                &:hover {
+                    text-decoration: underline;
+                }
+            }
+        }
+        
+        .btn-primary {
+            width: 100%;
+            padding: 0.9rem;
+            background: #2a5daa;
+            color: white;
+            border: none;
+            border-radius: 8px;
+            font-size: 1rem;
+            font-weight: 500;
+            cursor: pointer;
+            transition: background 0.3s;
+            
+            &:hover {
+                background: #1d4a8a;
+            }
+            
+            &:disabled {
+                background: #9bb6e0;
+                cursor: not-allowed;
+            }
+        }
+        
+        .social-login {
+            text-align: center;
+            margin-top: 1.5rem;
+            padding-top: 1.5rem;
+            border-top: 1px solid #eee;
+            
+            p {
+                margin-bottom: 1rem;
+                color: #666;
+                font-size: 0.9rem;
+            }
+            
+            .social-icons {
+                display: flex;
+                justify-content: center;
+                gap: 1rem;
+                
+                .social-btn {
+                    width: 45px;
+                    height: 45px;
+                    border-radius: 50%;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    font-size: 1.2rem;
+                    color: white;
+                    border: none;
+                    cursor: pointer;
+                    transition: transform 0.3s;
+                    
+                    &:hover {
+                        transform: translateY(-3px);
+                    }
+                    
+                    &.wechat {
+                        background: #09bb07;
+                    }
+                    
+                    &.qq {
+                        background: #12b7f5;
+                    }
+                    
+                    &.weibo {
+                        background: #e6162d;
+                    }
+                }
+            }
+        }
+    }
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+    .user-section {
+        .btn {
+            padding: 0.4rem 0.8rem;
+            font-size: 0.9rem;
+        }
+        
+        .user-profile {
+            .username {
+                display: none;
+            }
+        }
+    }
+    
+    .auth-modal {
+        width: 90%;
+    }
 }

+ 156 - 5
travel-web/src/modules/shared/nav-pc-top-menu/nav-pc-top-menu.ts

@@ -1,14 +1,165 @@
-
-import { Component } from '@angular/core';
-import { RouterModule } from '@angular/router';
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Router, RouterModule } from '@angular/router';
+import { FormsModule } from '@angular/forms';
+import { CloudUser } from '../../../lib/ncloud'; 
 
 @Component({
   selector: 'app-nav-pc-top-menu',
   standalone: true,
-  imports: [RouterModule],
+  imports: [CommonModule, RouterModule, FormsModule],
   templateUrl: './nav-pc-top-menu.html',
   styleUrls: ['./nav-pc-top-menu.scss']
 })
-export class NavPcTopMenu {
+export class NavPcTopMenu implements OnInit {
+  user: any = null;
+  userAvatar: string | null = null;
+  userInitials: string = '';
+  isLoading: boolean = true;
+  showMenu: boolean = false;
+  
+  // 登录注册模态框相关
+  showAuthModal: boolean = false;
+  authType: 'login' | 'register' = 'login';
+  
+  // 登录表单
+  loginUsername: string = '';
+  loginPassword: string = '';
+  rememberMe: boolean = true;
+  isLoggingIn: boolean = false;
+  
+  // 注册表单
+  registerUsername: string = '';
+  registerEmail: string = '';
+  registerPhone: string = '';
+  registerPassword: string = '';
+  registerPasswordConfirm: string = '';
+  acceptTerms: boolean = false;
+  isRegistering: boolean = false;
+
+  constructor(private router: Router) {}
+
+  async ngOnInit() {
+    // 初始化CloudUser
+    this.user = new CloudUser();
+    
+    // 尝试获取当前用户
+    try {
+      const currentUser = await this.user.current();
+      if (currentUser && this.user.id) {
+        this.user = currentUser;
+        this.generateUserInitials();
+      }
+    } catch (error) {
+      console.error('获取用户信息失败:', error);
+    } finally {
+      this.isLoading = false;
+    }
+  }
+
+  // 生成用户头像首字母
+  generateUserInitials() {
+    const username = this.user.get('username') || '';
+    if (username.length > 0) {
+      this.userInitials = username.charAt(0).toUpperCase();
+    }
+  }
+
+  toggleMenu() {
+    this.showMenu = !this.showMenu;
+  }
+
+  openAuthModal(type: 'login' | 'register') {
+    this.authType = type;
+    this.showAuthModal = true;
+    this.showMenu = false; // 关闭用户菜单
+  }
+
+  closeAuthModal() {
+    this.showAuthModal = false;
+    // 重置表单状态
+    this.isLoggingIn = false;
+    this.isRegistering = false;
+  }
+
+  setAuthType(type: 'login' | 'register') {
+    this.authType = type;
+  }
+
+  async login() {
+    if (!this.loginUsername || !this.loginPassword) {
+      return;
+    }
+    
+    this.isLoggingIn = true;
+    
+    try {
+      const loggedInUser = await this.user.login(this.loginUsername, this.loginPassword);
+      if (loggedInUser) {
+        this.user = loggedInUser;
+        this.generateUserInitials();
+        this.closeAuthModal();
+      }
+    } catch (error) {
+      console.error('登录失败:', error);
+      // 实际应用中应显示错误提示
+    } finally {
+      this.isLoggingIn = false;
+    }
+  }
+
+  async register() {
+    if (!this.registerUsername || !this.registerEmail || !this.registerPassword) {
+      return;
+    }
+    
+    if (this.registerPassword !== this.registerPasswordConfirm) {
+      // 实际应用中应显示错误提示
+      return;
+    }
+    
+    if (!this.acceptTerms) {
+      // 实际应用中应显示错误提示
+      return;
+    }
+    
+    this.isRegistering = true;
+    
+    try {
+      const additionalData = {
+        email: this.registerEmail,
+        phone: this.registerPhone
+      };
+      
+      const newUser = await this.user.signUp(
+        this.registerUsername, 
+        this.registerPassword, 
+        additionalData
+      );
+      
+      if (newUser) {
+        this.user = newUser;
+        this.generateUserInitials();
+        this.closeAuthModal();
+      }
+    } catch (error) {
+      console.error('注册失败:', error);
+      // 实际应用中应显示错误提示
+    } finally {
+      this.isRegistering = false;
+    }
+  }
 
+  async logout() {
+    try {
+      await this.user.logout();
+      this.user = null;
+      this.userAvatar = null;
+      this.userInitials = '';
+      this.showMenu = false;
+      this.router.navigate(['/home']);
+    } catch (error) {
+      console.error('退出登录失败:', error);
+    }
+  }
 }