Browse Source

学习概览

15270821319 6 months ago
parent
commit
54eb1ad6dc

+ 4 - 0
AiStudy-app/src/app/app.routes.ts

@@ -45,5 +45,9 @@ export const routes: Routes = [
     path: 'favorite-exercises',
     loadComponent: () => import('./pages/favorite-exercises/favorite-exercises.page')
       .then(m => m.FavoriteExercisesPage)
+  },
+  {
+    path: 'learning-overview',
+    loadChildren: () => import('./pages/learning-overview/learning-overview.routes').then(m => m.routes)
   }
 ];

+ 3 - 0
AiStudy-app/src/app/pages/learning-design/learning-design.page.html

@@ -129,6 +129,9 @@
 
   <!-- 操作按钮 -->
   <div class="action-buttons" *ngIf="isComplete">
+    <ion-button expand="block" (click)="saveLearningPlan()" *ngIf="showPlanResult">
+      保存学习计划
+    </ion-button>
     <ion-button expand="block" (click)="restartPlan()">
       重新规划
     </ion-button>

+ 53 - 2
AiStudy-app/src/app/pages/learning-design/learning-design.page.ts

@@ -10,7 +10,8 @@ import {
   IonProgressBar,
   IonButton,
   IonIcon,
-  ModalController
+  ModalController,
+  AlertController
 } from '@ionic/angular/standalone';
 import { NgClass, NgFor, NgIf } from '@angular/common';
 import Parse from 'parse';
@@ -20,6 +21,8 @@ import { TaskGeneratePlan } from '../../../agent/tasks/learning-plan/3.generate-
 import { TaskExecutor } from '../../../agent/agent.start';
 import { AgentTaskStep } from '../../../agent/agent.task';
 import { TaskCollectDetails } from '../../../agent/tasks/learning-plan/2.5.collect-details';
+import { LearningPlanService } from '../../services/learning-plan.service';
+import { CloudUser } from 'src/lib/ncloud';
 
 interface LearningPlan {
   userId: string;
@@ -61,7 +64,9 @@ export class LearningDesignPage implements OnInit {
 
   constructor(
     private router: Router,
-    private modalCtrl: ModalController
+    private modalCtrl: ModalController,
+    private alertController: AlertController,
+    private learningPlanService: LearningPlanService
   ) {}
 
   ngOnInit() {
@@ -108,4 +113,50 @@ export class LearningDesignPage implements OnInit {
   restartPlan() {
     this.startLearningPlanTask();
   }
+
+  async saveLearningPlan() {
+    try {
+      const currentUser = new CloudUser();
+      if (!currentUser.id) {
+        const alert = await this.alertController.create({
+          header: '提示',
+          message: '请先登录后再保存学习计划',
+          buttons: ['确定']
+        });
+        await alert.present();
+        return;
+      }
+
+      // 准备计划数据
+      const planData = {
+        userId: currentUser.id,
+        title: this.shareData.learningPlan.title,
+        overview: this.shareData.learningPlan.overview,
+        totalDuration: this.shareData.learningPlan.totalDuration,
+        weeklyHours: this.shareData.basicInfo['每周学习时间'],
+        stages: this.shareData.learningPlan.stages,
+        milestones: this.shareData.learningPlan.milestones,
+        successCriteria: this.shareData.learningPlan.successCriteria,
+        additionalSuggestions: this.shareData.learningPlan.additionalSuggestions
+      };
+
+      await this.learningPlanService.saveLearningPlan(planData);
+
+      const alert = await this.alertController.create({
+        header: '成功',
+        message: '学习计划已保存',
+        buttons: ['确定']
+      });
+      await alert.present();
+
+    } catch (error) {
+      console.error('保存学习计划失败:', error);
+      const alert = await this.alertController.create({
+        header: '错误',
+        message: '保存失败,请重试',
+        buttons: ['确定']
+      });
+      await alert.present();
+    }
+  }
 } 

+ 132 - 0
AiStudy-app/src/app/pages/learning-overview/learning-overview.page.html

@@ -0,0 +1,132 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-buttons slot="start">
+      <ion-back-button defaultHref="/tabs/tab2"></ion-back-button>
+    </ion-buttons>
+    <ion-title>学习概览</ion-title>
+    <ion-buttons slot="end">
+      <ion-button (click)="toggleEditMode()">
+        {{ isEditMode ? '完成' : '编辑' }}
+      </ion-button>
+    </ion-buttons>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content class="ion-padding">
+  <!-- 学习计划列表 -->
+  <div class="empty-state" *ngIf="learningPlans.length === 0">
+    <ion-icon name="book-outline" class="empty-icon"></ion-icon>
+    <h2>暂无学习计划</h2>
+    <p>点击"长期学习规划"开始制定你的学习计划</p>
+  </div>
+
+  <ion-list *ngIf="!selectedPlan && learningPlans.length > 0">
+    <ion-item-sliding *ngFor="let plan of learningPlans">
+      <ion-item [class.selected]="isSelected(plan.id!)" 
+                (click)="isEditMode ? toggleItemSelection(plan.id!) : viewPlanDetails(plan.id!)">
+        <ion-checkbox slot="start" 
+                     [checked]="isSelected(plan.id!)"
+                     *ngIf="isEditMode"
+                     (ionChange)="toggleItemSelection(plan.id!)">
+        </ion-checkbox>
+        <ion-label>
+          <h2>{{plan.title}}</h2>
+          <p class="overview-text">{{plan.overview}}</p>
+          <div class="plan-meta">
+            <ion-badge color="primary">
+              {{plan.totalDuration}}
+            </ion-badge>
+            <ion-text color="medium">
+              {{plan.createdAt | date:'yyyy-MM-dd HH:mm'}}
+            </ion-text>
+          </div>
+        </ion-label>
+        <ion-icon name="chevron-forward" slot="end" *ngIf="!isEditMode"></ion-icon>
+      </ion-item>
+    </ion-item-sliding>
+  </ion-list>
+
+  <!-- 底部操作栏 -->
+  <ion-footer *ngIf="isEditMode && learningPlans.length > 0">
+    <ion-toolbar>
+      <ion-buttons slot="start">
+        <ion-button (click)="toggleEditMode()">取消</ion-button>
+      </ion-buttons>
+      <ion-title>已选择 {{selectedItems.size}} 项</ion-title>
+      <ion-buttons slot="end">
+        <ion-button color="danger" 
+                    (click)="deleteSelectedPlans()"
+                    [disabled]="selectedItems.size === 0">
+          <ion-icon name="trash-outline" slot="start"></ion-icon>
+          删除
+        </ion-button>
+      </ion-buttons>
+    </ion-toolbar>
+  </ion-footer>
+
+  <!-- 计划详情 -->
+  <div *ngIf="selectedPlan" class="plan-details">
+    <ion-button fill="clear" (click)="selectedPlan = null">
+      <ion-icon name="arrow-back" slot="start"></ion-icon>
+      返回列表
+    </ion-button>
+
+    <h1>{{selectedPlan.title}}</h1>
+    <p class="overview">{{selectedPlan.overview}}</p>
+
+    <!-- 基本信息 -->
+    <ion-card>
+      <ion-card-header>
+        <ion-card-title>基本信息</ion-card-title>
+      </ion-card-header>
+      <ion-card-content>
+        <p><strong>总时长:</strong> {{selectedPlan.totalDuration}}</p>
+        <p><strong>每周学习时间:</strong> {{Math.round(selectedPlan.weeklyHours)}} 小时</p>
+      </ion-card-content>
+    </ion-card>
+
+    <!-- 学习阶段 -->
+    <ion-card *ngFor="let stage of selectedPlan.stages">
+      <ion-card-header>
+        <ion-card-title>{{stage.name}}</ion-card-title>
+        <ion-card-subtitle>时长: {{stage.duration}}</ion-card-subtitle>
+      </ion-card-header>
+      <ion-card-content>
+        <h3>阶段目标</h3>
+        <ul>
+          <li *ngFor="let goal of stage.goals">{{goal}}</li>
+        </ul>
+
+        <div *ngFor="let week of stage.weeklyPlans" class="week-plan">
+          <h4>{{week.week}} - {{week.focus}}</h4>
+          <div *ngFor="let task of week.tasks" class="task">
+            <h5>{{task.name}} ({{task.timeNeeded}})</h5>
+            <p>{{task.details}}</p>
+            <div *ngIf="task.resources?.length">
+              <strong>推荐资源:</strong>
+              <ul>
+                <li *ngFor="let resource of task.resources">{{resource}}</li>
+              </ul>
+            </div>
+          </div>
+        </div>
+      </ion-card-content>
+    </ion-card>
+
+    <!-- 里程碑 -->
+    <ion-card *ngIf="selectedPlan.milestones?.length">
+      <ion-card-header>
+        <ion-card-title>关键里程碑</ion-card-title>
+      </ion-card-header>
+      <ion-card-content>
+        <div *ngFor="let milestone of selectedPlan.milestones" class="milestone">
+          <h4>{{milestone.name}}</h4>
+          <p>预计达成时间: {{milestone.timing}}</p>
+          <ul>
+            <li *ngFor="let criterion of milestone.criteria">{{criterion}}</li>
+          </ul>
+        </div>
+      </ion-card-content>
+    </ion-card>
+  </div>
+</ion-content> 

+ 137 - 0
AiStudy-app/src/app/pages/learning-overview/learning-overview.page.scss

@@ -0,0 +1,137 @@
+.plan-details {
+  padding: 16px;
+
+  h1 {
+    font-size: 24px;
+    font-weight: 600;
+    color: var(--ion-color-dark);
+    margin-bottom: 8px;
+  }
+
+  .overview {
+    color: var(--ion-color-medium);
+    margin-bottom: 24px;
+  }
+
+  .week-plan {
+    margin-top: 16px;
+    padding-top: 16px;
+    border-top: 1px solid var(--ion-color-light);
+
+    h4 {
+      color: var(--ion-color-primary);
+      font-size: 16px;
+      margin-bottom: 12px;
+    }
+  }
+
+  .task {
+    background: var(--ion-color-light);
+    border-radius: 8px;
+    padding: 12px;
+    margin-bottom: 12px;
+
+    h5 {
+      font-size: 15px;
+      margin: 0 0 8px 0;
+      color: var(--ion-color-dark);
+    }
+
+    p {
+      margin: 0 0 8px 0;
+      color: var(--ion-color-medium);
+    }
+  }
+
+  .milestone {
+    margin-bottom: 16px;
+
+    h4 {
+      color: var(--ion-color-success);
+      margin-bottom: 8px;
+    }
+  }
+
+  ul {
+    margin: 8px 0;
+    padding-left: 20px;
+
+    li {
+      margin-bottom: 4px;
+      color: var(--ion-color-medium);
+    }
+  }
+}
+
+// 空状态样式
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 48px 20px;
+  text-align: center;
+
+  .empty-icon {
+    font-size: 64px;
+    color: var(--ion-color-medium);
+    margin-bottom: 16px;
+  }
+
+  h2 {
+    color: var(--ion-color-dark);
+    font-size: 20px;
+    margin-bottom: 8px;
+  }
+
+  p {
+    color: var(--ion-color-medium);
+    font-size: 14px;
+  }
+}
+
+// 列表项样式
+ion-item {
+  --padding-start: 16px;
+  --padding-end: 16px;
+  --padding-top: 12px;
+  --padding-bottom: 12px;
+  margin-bottom: 8px;
+  border-radius: 8px;
+  --background: var(--ion-color-light);
+
+  &.selected {
+    --background: var(--ion-color-light-shade);
+  }
+
+  h2 {
+    font-size: 16px;
+    font-weight: 500;
+    margin-bottom: 4px;
+  }
+
+  .overview-text {
+    color: var(--ion-color-medium);
+    font-size: 14px;
+    margin-bottom: 8px;
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
+  }
+
+  .plan-meta {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+
+    ion-badge {
+      font-size: 12px;
+      padding: 4px 8px;
+    }
+
+    ion-text {
+      font-size: 12px;
+    }
+  }
+} 

+ 211 - 0
AiStudy-app/src/app/pages/learning-overview/learning-overview.page.ts

@@ -0,0 +1,211 @@
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { 
+  IonHeader, 
+  IonToolbar, 
+  IonTitle, 
+  IonContent,
+  IonList,
+  IonItem,
+  IonLabel,
+  IonButton,
+  IonIcon,
+  IonButtons,
+  IonBackButton,
+  IonCard,
+  IonCardHeader,
+  IonCardTitle,
+  IonCardSubtitle,
+  IonCardContent,
+  IonText,
+  AlertController,
+  IonItemSliding,
+  IonCheckbox,
+  IonBadge,
+  IonFooter,
+  IonItemOption,
+  IonItemOptions
+} from '@ionic/angular/standalone';
+import { NgFor, NgIf, DatePipe, DecimalPipe } from '@angular/common';
+import { LearningPlanService, LearningPlan } from '../../services/learning-plan.service';
+import { CloudUser } from 'src/lib/ncloud';
+import { addIcons } from 'ionicons';
+import { 
+  chevronForward, 
+  createOutline, 
+  trashOutline, 
+  arrowBack,
+  ellipsisVertical,
+  checkmarkCircle,
+  bookOutline
+} from 'ionicons/icons';
+
+@Component({
+  selector: 'app-learning-overview',
+  templateUrl: './learning-overview.page.html',
+  styleUrls: ['./learning-overview.page.scss'],
+  standalone: true,
+  imports: [
+    IonHeader,
+    IonToolbar,
+    IonTitle,
+    IonContent,
+    IonList,
+    IonItem,
+    IonLabel,
+    IonButton,
+    IonIcon,
+    IonButtons,
+    IonBackButton,
+    IonCard,
+    IonCardHeader,
+    IonCardTitle,
+    IonCardSubtitle,
+    IonCardContent,
+    IonText,
+    IonItemSliding,
+    IonCheckbox,
+    IonBadge,
+    IonFooter,
+    IonItemOption,
+    IonItemOptions,
+    NgFor,
+    NgIf,
+    DatePipe,
+    DecimalPipe
+  ]
+})
+export class LearningOverviewPage implements OnInit {
+  learningPlans: LearningPlan[] = [];
+  selectedPlan: LearningPlan | null = null;
+  isEditMode: boolean = false;
+  selectedItems: Set<string> = new Set();
+  Math = Math;
+
+  constructor(
+    private router: Router,
+    private alertController: AlertController,
+    private learningPlanService: LearningPlanService
+  ) {
+    addIcons({ 
+      chevronForward, 
+      createOutline, 
+      trashOutline, 
+      arrowBack,
+      ellipsisVertical,
+      checkmarkCircle,
+      bookOutline
+    });
+  }
+
+  async ngOnInit() {
+    await this.loadLearningPlans();
+  }
+
+  async loadLearningPlans() {
+    try {
+      const currentUser = new CloudUser();
+      if (!currentUser.id) {
+        const alert = await this.alertController.create({
+          header: '提示',
+          message: '请先登录后查看学习计划',
+          buttons: ['确定']
+        });
+        await alert.present();
+        return;
+      }
+
+      this.learningPlans = await this.learningPlanService.getUserLearningPlans(currentUser.id);
+    } catch (error) {
+      console.error('加载学习计划失败:', error);
+      const alert = await this.alertController.create({
+        header: '错误',
+        message: '加载失败,请重试',
+        buttons: ['确定']
+      });
+      await alert.present();
+    }
+  }
+
+  async viewPlanDetails(planId: string) {
+    try {
+      this.selectedPlan = await this.learningPlanService.getLearningPlanById(planId);
+    } catch (error) {
+      console.error('获取计划详情失败:', error);
+      const alert = await this.alertController.create({
+        header: '错误',
+        message: '获取计划详情失败,请重试',
+        buttons: ['确定']
+      });
+      await alert.present();
+    }
+  }
+
+  toggleEditMode() {
+    this.isEditMode = !this.isEditMode;
+    if (!this.isEditMode) {
+      this.selectedItems.clear();
+    }
+  }
+
+  toggleItemSelection(planId: string) {
+    if (this.selectedItems.has(planId)) {
+      this.selectedItems.delete(planId);
+    } else {
+      this.selectedItems.add(planId);
+    }
+  }
+
+  isSelected(planId: string): boolean {
+    return this.selectedItems.has(planId);
+  }
+
+  async deleteSelectedPlans() {
+    if (this.selectedItems.size === 0) return;
+
+    const alert = await this.alertController.create({
+      header: '确认删除',
+      message: `确定要删除选中的 ${this.selectedItems.size} 个学习计划吗?`,
+      buttons: [
+        {
+          text: '取消',
+          role: 'cancel'
+        },
+        {
+          text: '删除',
+          role: 'destructive',
+          handler: async () => {
+            try {
+              // 实现批量删除逻辑
+              await Promise.all(
+                Array.from(this.selectedItems).map(id => 
+                  this.learningPlanService.deleteLearningPlan(id)
+                )
+              );
+              
+              await this.loadLearningPlans();
+              this.selectedItems.clear();
+              this.isEditMode = false;
+
+              const successAlert = await this.alertController.create({
+                header: '成功',
+                message: '删除成功',
+                buttons: ['确定']
+              });
+              await successAlert.present();
+            } catch (error) {
+              console.error('删除计划失败:', error);
+              const errorAlert = await this.alertController.create({
+                header: '错误',
+                message: '删除失败,请重试',
+                buttons: ['确定']
+              });
+              await errorAlert.present();
+            }
+          }
+        }
+      ]
+    });
+    await alert.present();
+  }
+} 

+ 9 - 0
AiStudy-app/src/app/pages/learning-overview/learning-overview.routes.ts

@@ -0,0 +1,9 @@
+import { Routes } from '@angular/router';
+import { LearningOverviewPage } from './learning-overview.page';
+
+export const routes: Routes = [
+  {
+    path: '',
+    component: LearningOverviewPage,
+  }
+]; 

+ 164 - 0
AiStudy-app/src/app/services/learning-plan.service.ts

@@ -0,0 +1,164 @@
+import { Injectable } from '@angular/core';
+import Parse from 'parse';
+import { CloudQuery, CloudUser } from 'src/lib/ncloud';
+
+// 定义学习计划接口
+export interface LearningPlan {
+  id?: string;
+  userId: string;
+  title: string;
+  overview: string;
+  totalDuration: string;
+  weeklyHours: number;
+  stages: {
+    name: string;
+    duration: string;
+    goals: string[];
+    weeklyPlans: {
+      week: string;
+      focus: string;
+      tasks: {
+        name: string;
+        timeNeeded: string;
+        details: string;
+        resources?: string[];
+      }[];
+    }[];
+  }[];
+  milestones: {
+    name: string;
+    timing: string;
+    criteria: string[];
+  }[];
+  successCriteria: string[];
+  additionalSuggestions: string[];
+  createdAt?: Date;
+  updatedAt?: Date;
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class LearningPlanService {
+  
+  // 保存学习计划
+  async saveLearningPlan(plan: LearningPlan): Promise<string> {
+    try {
+      const LearningPlan = Parse.Object.extend('LearningPlan');
+      const learningPlan = new LearningPlan();
+      
+      // 设置计划数据
+      learningPlan.set('userId', plan.userId);
+      learningPlan.set('title', plan.title);
+      learningPlan.set('overview', plan.overview);
+      learningPlan.set('totalDuration', plan.totalDuration);
+      learningPlan.set('weeklyHours', plan.weeklyHours);
+      learningPlan.set('stages', plan.stages);
+      learningPlan.set('milestones', plan.milestones);
+      learningPlan.set('successCriteria', plan.successCriteria);
+      learningPlan.set('additionalSuggestions', plan.additionalSuggestions);
+
+      const result = await learningPlan.save();
+      return result.id;
+    } catch (error) {
+      console.error('保存学习计划失败:', error);
+      throw error;
+    }
+  }
+
+  // 添加工具函数来格式化时长显示
+  private formatDuration(duration: string | number): string {
+    if (typeof duration === 'number') {
+      if (duration >= 720) { // 大于720小时显示月
+        return `${Math.round(duration / 720)}个月`;
+      } else {
+        return `${duration}小时`;
+      }
+    }
+    return duration; // 如果是字符串则直接返回
+  }
+
+  // 获取用户的所有学习计划
+  async getUserLearningPlans(userId: string): Promise<LearningPlan[]> {
+    try {
+      const LearningPlan = Parse.Object.extend('LearningPlan');
+      const query = new Parse.Query(LearningPlan);
+      query.equalTo('userId', userId);
+      query.descending('createdAt');
+
+      const results = await query.find();
+      return results.map(plan => {
+        const weeklyHours = plan.get('weeklyHours');
+        const totalDuration = plan.get('totalDuration');
+        
+        const learningPlan: LearningPlan = {
+          id: plan.id || undefined,
+          userId: plan.get('userId'),
+          title: plan.get('title'),
+          overview: plan.get('overview'),
+          totalDuration: this.formatDuration(totalDuration), // 格式化总时长
+          weeklyHours: typeof weeklyHours === 'string' ? 
+            parseFloat(weeklyHours) : 
+            Number(weeklyHours) || 0, // 确保转换为数字
+          stages: plan.get('stages') || [],
+          milestones: plan.get('milestones') || [],
+          successCriteria: plan.get('successCriteria') || [],
+          additionalSuggestions: plan.get('additionalSuggestions') || [],
+          createdAt: plan.get('createdAt'),
+          updatedAt: plan.get('updatedAt')
+        };
+        return learningPlan;
+      });
+    } catch (error) {
+      console.error('获取学习计划失败:', error);
+      throw error;
+    }
+  }
+
+  // 获取单个学习计划详情
+  async getLearningPlanById(planId: string): Promise<LearningPlan> {
+    try {
+      const LearningPlan = Parse.Object.extend('LearningPlan');
+      const query = new Parse.Query(LearningPlan);
+      const plan = await query.get(planId);
+      
+      const weeklyHours = plan.get('weeklyHours');
+      const totalDuration = plan.get('totalDuration');
+
+      const learningPlan: LearningPlan = {
+        id: plan.id || undefined,
+        userId: plan.get('userId'),
+        title: plan.get('title'),
+        overview: plan.get('overview'),
+        totalDuration: this.formatDuration(totalDuration), // 格式化总时长
+        weeklyHours: typeof weeklyHours === 'string' ? 
+          parseFloat(weeklyHours) : 
+          Number(weeklyHours) || 0, // 确保转换为数字
+        stages: plan.get('stages') || [],
+        milestones: plan.get('milestones') || [],
+        successCriteria: plan.get('successCriteria') || [],
+        additionalSuggestions: plan.get('additionalSuggestions') || [],
+        createdAt: plan.get('createdAt'),
+        updatedAt: plan.get('updatedAt')
+      };
+      
+      return learningPlan;
+    } catch (error) {
+      console.error('获取学习计划详情失败:', error);
+      throw error;
+    }
+  }
+
+  // 添加删除方法
+  async deleteLearningPlan(planId: string): Promise<void> {
+    try {
+      const LearningPlan = Parse.Object.extend('LearningPlan');
+      const query = new Parse.Query(LearningPlan);
+      const plan = await query.get(planId);
+      await plan.destroy();
+    } catch (error) {
+      console.error('删除学习计划失败:', error);
+      throw error;
+    }
+  }
+} 

+ 1 - 1
AiStudy-app/src/app/tab2/tab2.page.ts

@@ -97,7 +97,7 @@ export class Tab2Page implements OnInit {
         this.router.navigate(['/learning-history']);
         break;
       case 'learning-overview':
-        this.router.navigate(['/learning-history']);
+        this.router.navigate(['/learning-overview']);
         break;
       case 'interactive-practice':
         this.router.navigate(['/interactive-practice']);