Browse Source

feat:bubble

0225304 1 day ago
parent
commit
5fed7df952

+ 6 - 1
myapp/src/app/app.module.ts

@@ -13,13 +13,18 @@ import { Diagnostic } from '@awesome-cordova-plugins/diagnostic/ngx';
 import Parse from "parse";
 import { AlertController } from '@ionic/angular';
 import { BrowserModule } from '@angular/platform-browser';
+import { IonicStorageModule } from '@ionic/storage-angular';
+import { FormsModule } from '@angular/forms';
 Parse.initialize("ncloudmaster");
 Parse.serverURL = "https://server.fmode.cn/parse";
 localStorage.setItem("NOVA_APIG_SERVER", 'aHR0cHMlM0ElMkYlMkZzZXJ2ZXIuZm1vZGUuY24lMkZhcGklMkZhcGlnJTJG')
 
 @NgModule({
  declarations: [AppComponent],
-   imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule,HttpClientModule],
+   imports: [ 
+    FormsModule,
+    IonicStorageModule.forRoot(),
+    BrowserModule, IonicModule.forRoot(), AppRoutingModule,HttpClientModule],
    providers: [{ 
    provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    Diagnostic,

+ 17 - 0
myapp/src/app/tab2/emotion-vent/emotion-vent-routing.module.ts

@@ -0,0 +1,17 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { EmotionVentPage } from './emotion-vent.page';
+
+const routes: Routes = [
+  {
+    path: '',
+    component: EmotionVentPage
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+})
+export class EmotionVentPageRoutingModule {}

+ 20 - 0
myapp/src/app/tab2/emotion-vent/emotion-vent.module.ts

@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { IonicModule } from '@ionic/angular';
+
+import { EmotionVentPageRoutingModule } from './emotion-vent-routing.module';
+
+import { EmotionVentPage } from './emotion-vent.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    EmotionVentPageRoutingModule
+  ],
+  declarations: [EmotionVentPage]
+})
+export class EmotionVentPageModule {}

+ 82 - 0
myapp/src/app/tab2/emotion-vent/emotion-vent.page.html

@@ -0,0 +1,82 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-buttons slot="start">
+      <ion-back-button defaultHref="/tabs/tab2"></ion-back-button>
+    </ion-buttons>
+    <ion-title>情绪发泄室</ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content class="vent-content" (touchmove)="onTouchMove($event)" (touchend)="onTouchEnd()">
+  <div class="bubble-container">
+    <div *ngFor="let bubble of bubbles" 
+         class="negative-bubble"
+         [style.left]="bubble.x + 'px'"
+         [style.top]="bubble.y + 'px'"
+         [style.background]="bubble.color"
+         [style.transform]="'scale(' + bubble.scale + ')'"
+         [class.burst]="bubble.burst"
+         (click)="onBubbleClick(bubble, $event)">
+      {{ bubble.text }}
+    </div>
+    
+    <!-- 粒子效果容器 -->
+    <div #particlesContainer class="particles-container"></div>
+  </div>
+
+  <!-- 锤子图标 -->
+  <div class="hammer" [style.left]="hammerX + 'px'" [style.top]="hammerY + 'px'">
+    🔨
+  </div>
+
+  <!-- 在ion-content内部添加 -->
+<ion-modal #keywordModal>
+  <ng-template>
+    <ion-header>
+      <ion-toolbar>
+        <ion-title>添加关键词</ion-title>
+        <ion-buttons slot="end">
+          <ion-button (click)="dismissModal()">关闭</ion-button>
+        </ion-buttons>
+      </ion-toolbar>
+    </ion-header>
+    <ion-content class="ion-padding">
+      <ion-item>
+        <!-- <ion-label position="floating">输入你想粉碎的负面情绪词</ion-label> -->
+        <ion-input [(ngModel)]="newKeyword" placeholder="例如: 焦虑、压力"></ion-input>
+      </ion-item>
+      
+      <ion-button expand="block" (click)="addKeyword()" class="ion-margin-top">
+        添加关键词
+      </ion-button>
+      
+      <ion-list *ngIf="customKeywords.length > 0">
+        <ion-list-header>
+          <ion-label>已添加的关键词</ion-label>
+        </ion-list-header>
+        <ion-item-sliding *ngFor="let keyword of customKeywords">
+          <ion-item>
+            <ion-label>{{ keyword }}</ion-label>
+            <ion-icon slot="end" name="trash-outline" (click)="removeKeyword(keyword)"></ion-icon>
+          </ion-item>
+        </ion-item-sliding>
+      </ion-list>
+    </ion-content>
+  </ng-template>
+</ion-modal>
+
+<!-- 在统计信息区域添加按钮 -->
+<div class="stats">
+  <div>已粉碎: {{ burstCount }} / {{ bubbles.length }}</div>
+  <div>
+    <ion-button (click)="resetBubbles()" fill="clear" size="small">
+      <ion-icon name="refresh" slot="start"></ion-icon>
+      重置
+    </ion-button>
+    <ion-button (click)="openKeywordModal()" fill="clear" size="small">
+      <ion-icon name="add" slot="start"></ion-icon>
+      添加关键词
+    </ion-button>
+  </div>
+</div>
+</ion-content>

+ 195 - 0
myapp/src/app/tab2/emotion-vent/emotion-vent.page.scss

@@ -0,0 +1,195 @@
+.vent-content {
+  --background: linear-gradient(to bottom, #1a1a2e, #16213e);
+  overflow: hidden;
+  position: relative;
+  touch-action: none;
+}
+
+.bubble-container {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+
+.negative-bubble {
+  position: absolute;
+  width: 80px;
+  height: 80px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: white;
+  font-weight: bold;
+  text-align: center;
+  cursor: pointer;
+  user-select: none;
+  transition: transform 0.2s ease-out;
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+  animation: float 3s infinite ease-in-out;
+  z-index: 10;
+  
+  &:hover {
+    transform: scale(1.1);
+  }
+  
+  &.burst {
+    animation: burst 0.5s forwards;
+  }
+}
+
+@keyframes float {
+  0%, 100% {
+    transform: translateY(0);
+  }
+  50% {
+    transform: translateY(-10px);
+  }
+}
+
+
+.hammer {
+  position: absolute;
+  font-size: 40px;
+  transform: translate(-50%, -50%) rotate(45deg);
+  z-index: 20;
+  pointer-events: none;
+  transition: transform 0.1s ease-out;
+}
+/* 新增粒子效果样式 */
+.particles-container {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+  z-index: 15;
+}
+
+.particle {
+  position: absolute;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  pointer-events: none;
+  animation: particle-fly 1s ease-out forwards;
+}
+
+@keyframes particle-fly {
+  0% {
+    transform: translate(0, 0) scale(1);
+    opacity: 1;
+  }
+  100% {
+    transform: translate(var(--tx), var(--ty)) scale(0);
+    opacity: 0;
+  }
+}
+
+/* 增强气泡爆裂动画 */
+@keyframes burst {
+  0% {
+    transform: scale(1);
+    opacity: 1;
+    filter: brightness(1);
+  }
+  20% {
+    transform: scale(1.3);
+    filter: brightness(1.5);
+  }
+  50% {
+    transform: scale(0.8);
+    opacity: 0.8;
+    filter: brightness(1.2) blur(1px);
+  }
+  80% {
+    transform: scale(1.1);
+    opacity: 0.5;
+  }
+  100% {
+    transform: scale(0);
+    opacity: 0;
+    display: none;
+  }
+}
+
+/* 添加屏幕震动效果 */
+.shake {
+  animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
+}
+
+@keyframes shake {
+  10%, 90% { transform: translate3d(-1px, 0, 0); }
+  20%, 80% { transform: translate3d(2px, 0, 0); }
+  30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
+  40%, 60% { transform: translate3d(4px, 0, 0); }
+}
+
+
+/* 裂纹效果 */
+.crack-effect {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  font-size: 30px;
+  opacity: 0.7;
+  animation: crackAppear 0.3s;
+}
+
+@keyframes crackAppear {
+  0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0; }
+  50% { transform: translate(-50%, -50%) scale(1.2); opacity: 1; }
+  100% { transform: translate(-50%, -50%) scale(1); opacity: 0.7; }
+}
+
+/* 气泡类型特定样式 */
+.negative-bubble.hard {
+  border: 2px solid rgba(255, 255, 255, 0.8);
+  box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
+}
+
+.negative-bubble.fragile {
+  border: 1px dashed rgba(255, 255, 255, 0.6);
+  animation: fragilePulse 2s infinite;
+}
+
+@keyframes fragilePulse {
+  0% { opacity: 0.9; }
+  50% { opacity: 1; }
+  100% { opacity: 0.9; }
+}
+
+/* 关键词模态框样式 */
+.keyword-input {
+  margin-bottom: 20px;
+}
+
+/* 统计按钮区域 */
+.stats {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  position: absolute;
+  bottom: 20px;
+  left: 0;
+  right: 0;
+  text-align: center;
+  color: white;
+  z-index: 30;
+  background: rgba(0, 0, 0, 0.5);
+  padding: 10px 20px;
+  border-radius: 20px;
+  margin: 0 20px;
+  
+  div {
+    display: flex;
+    align-items: center;
+  }
+  
+  ion-button {
+    --color: white;
+    margin-left: 10px;
+  }
+}

+ 17 - 0
myapp/src/app/tab2/emotion-vent/emotion-vent.page.spec.ts

@@ -0,0 +1,17 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { EmotionVentPage } from './emotion-vent.page';
+
+describe('EmotionVentPage', () => {
+  let component: EmotionVentPage;
+  let fixture: ComponentFixture<EmotionVentPage>;
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(EmotionVentPage);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 335 - 0
myapp/src/app/tab2/emotion-vent/emotion-vent.page.ts

@@ -0,0 +1,335 @@
+import { Component, OnInit, ViewChild, ElementRef, Renderer2 } from '@angular/core';
+import { Router } from '@angular/router';
+import { IonModal } from '@ionic/angular';
+import { Storage } from '@ionic/storage-angular';
+
+interface Bubble {
+  x: number;
+  y: number;
+  text: string;
+  color: string;
+  scale: number;
+  burst: boolean;
+  speedX: number;
+  speedY: number;
+  type: 'normal' | 'hard' | 'fragile'; // 气泡类型
+  health: number; // 生命值
+  hits: number; // 已击中次数
+}
+
+@Component({
+  selector: 'app-emotion-vent',
+  templateUrl: './emotion-vent.page.html',
+  styleUrls: ['./emotion-vent.page.scss'],
+  standalone:false,
+})
+export class EmotionVentPage implements OnInit {
+  @ViewChild('particlesContainer', { static: false }) particlesContainer!: ElementRef;
+  @ViewChild('keywordModal') keywordModal!: IonModal;
+  newKeyword = '';
+  customKeywords: string[] = [];
+  bubbles: Bubble[] = [];
+  burstCount = 0;
+  hammerX = 0;
+  hammerY = 0;
+  hammerVisible = false;
+  negativeWords = [
+    '加班', '分手', '压力', '焦虑', '失眠', '孤独', '失败', 
+    '被拒', '拖延', '迷茫', '自卑', '愤怒', '委屈', '失望',
+    '疲惫', '无助', '内卷', 'PUA', '崩溃', 'emo'
+  ];
+  colors = [
+    '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8',
+    '#F06292', '#7986CB', '#9575CD', '#64B5F6', '#4DB6AC'
+  ];
+  animationFrameId: number=0;
+
+  constructor(
+    private router: Router,
+    private renderer: Renderer2,
+    private storage: Storage
+  ) {}
+
+  async ngOnInit() {
+    await this.storage.create();
+    this.loadCustomKeywords();
+    this.generateBubbles();
+    this.startBubbleMovement();
+  }
+
+  // 打开关键词模态框
+async openKeywordModal() {
+  await this.loadCustomKeywords();
+  this.keywordModal.present();
+}
+
+// 关闭模态框
+dismissModal() {
+  this.keywordModal.dismiss();
+}
+
+// 添加关键词
+async addKeyword() {
+  if (this.newKeyword.trim() && !this.customKeywords.includes(this.newKeyword.trim())) {
+    this.customKeywords.push(this.newKeyword.trim());
+    await this.storage.set('customKeywords', this.customKeywords);
+    this.newKeyword = '';
+    this.resetBubbles(); // 重新生成气泡以包含新关键词
+  }
+}
+
+// 移除关键词
+async removeKeyword(keyword: string) {
+  this.customKeywords = this.customKeywords.filter(k => k !== keyword);
+  await this.storage.set('customKeywords', this.customKeywords);
+  this.resetBubbles(); // 重新生成气泡
+}
+
+// 加载保存的关键词
+async loadCustomKeywords() {
+  const savedKeywords = await this.storage.get('customKeywords');
+  this.customKeywords = savedKeywords || [];
+}
+
+  // 修改generateBubbles方法
+generateBubbles() {
+  const bubbleCount = 15;
+  this.bubbles = [];
+  this.burstCount = 0;
+  // 合并默认关键词和自定义关键词
+  const allKeywords = [...this.negativeWords, ...this.customKeywords];
+  
+  for (let i = 0; i < bubbleCount; i++) {
+    const typeRoll = Math.random();
+    const randomWord = allKeywords[Math.floor(Math.random() * allKeywords.length)];
+    let type: 'normal' | 'hard' | 'fragile', health: number;
+    
+    if (typeRoll < 0.2) { // 20%几率生成坚硬气泡
+      type = 'hard';
+      health = 2;
+    } else if (typeRoll < 0.4) { // 20%几率生成脆弱气泡
+      type = 'fragile';
+      health = 1;
+    } else { // 60%普通气泡
+      type = 'normal';
+      health = 1;
+    }
+    
+    //const randomWord = this.negativeWords[Math.floor(Math.random() * this.negativeWords.length)];
+    const randomColor = this.getColorForType(type);
+    
+    this.bubbles.push({
+      x: Math.random() * (window.innerWidth - 100),
+      y: Math.random() * (window.innerHeight - 100),
+      text: randomWord,
+      color: randomColor,
+      scale: this.getScaleForType(type),
+      burst: false,
+      speedX: (Math.random() - 0.5) * 2,
+      speedY: (Math.random() - 0.5) * 2,
+      type: type,
+      health: health,
+      hits: 0
+    });
+  }
+}
+
+// 根据类型获取颜色
+getColorForType(type: string): string {
+  switch(type) {
+    case 'hard':
+      return '#FF6B6B'; // 红色系
+    case 'fragile':
+      return '#4ECDC4'; // 青色系
+    default:
+      return this.colors[Math.floor(Math.random() * this.colors.length)];
+  }
+}
+
+// 根据类型获取大小
+getScaleForType(type: string): number {
+  switch(type) {
+    case 'hard':
+      return 0.9 + Math.random() * 0.3; // 稍大
+    case 'fragile':
+      return 0.7 + Math.random() * 0.2; // 稍小
+    default:
+      return 0.8 + Math.random() * 0.4;
+  }
+}
+
+  startBubbleMovement() {
+    const moveBubbles = () => {
+      this.bubbles.forEach(bubble => {
+        if (!bubble.burst) {
+          bubble.x += bubble.speedX;
+          bubble.y += bubble.speedY;
+          
+          // 边界检测
+          if (bubble.x <= 0 || bubble.x >= window.innerWidth - 80) {
+            bubble.speedX *= -1;
+          }
+          if (bubble.y <= 0 || bubble.y >= window.innerHeight - 80) {
+            bubble.speedY *= -1;
+          }
+        }
+      });
+      
+      this.animationFrameId = requestAnimationFrame(moveBubbles);
+    };
+    
+    moveBubbles();
+  }
+
+  onTouchMove(event: TouchEvent) {
+    if (event.touches.length > 0) {
+      this.hammerX = event.touches[0].clientX;
+      this.hammerY = event.touches[0].clientY;
+      
+      // 检测碰撞
+      this.checkCollision(this.hammerX, this.hammerY);
+    }
+  }
+
+  onTouchEnd() {
+    // 触摸结束时可以添加一些效果
+  }
+
+  checkCollision(x: number, y: number) {
+    this.bubbles.forEach(bubble => {
+      if (!bubble.burst) {
+        const distance = Math.sqrt(
+          Math.pow(x - (bubble.x + 40), 2) + 
+          Math.pow(y - (bubble.y + 40), 2)
+        );
+        
+        if (distance < 40) {
+          this.burstBubble(bubble);
+        }
+      }
+    });
+  }
+
+ burstBubble(bubble: Bubble) {
+  // 增加击中次数
+  bubble.hits++;
+  
+  // 如果未达到爆裂条件,只显示击中效果
+  if (bubble.hits < bubble.health) {
+    this.showHitEffect(bubble);
+    return;
+  }
+  
+  if (!bubble.burst) {
+    bubble.burst = true;
+    this.burstCount++;
+    
+    // 根据不同类型播放不同音效
+    this.playBurstSound(bubble.type);
+    
+    
+    // 所有气泡都爆裂后显示完成消息
+    if (this.burstCount === this.bubbles.length) {
+      setTimeout(() => {
+        this.showCompletionMessage();
+      }, 1000);
+    }
+  }
+}
+
+// 显示击中效果
+showHitEffect(bubble: Bubble) {
+  // 震动效果
+  const element = document.querySelector(`.negative-bubble[style*="left: ${bubble.x}px"][style*="top: ${bubble.y}px"]`);
+  if (element) {
+    element.classList.add('shake');
+    setTimeout(() => {
+      element.classList.remove('shake');
+    }, 500);
+  }
+  
+  // 显示裂纹效果
+  this.showCrackEffect(bubble);
+  
+  // 播放击中音效
+  this.playHitSound();
+}
+
+// 显示裂纹效果
+showCrackEffect(bubble: Bubble) {
+  const crack = document.createElement('div');
+  crack.className = 'crack-effect';
+  crack.innerHTML = '💢'; // 可以使用Unicode字符或SVG
+  
+  const element = document.querySelector(`.negative-bubble[style*="left: ${bubble.x}px"][style*="top: ${bubble.y}px"]`);
+  if (element) {
+    crack.style.position = 'absolute';
+    crack.style.fontSize = '30px';
+    crack.style.animation = 'fadeInOut 1s';
+    element.appendChild(crack);
+    
+    setTimeout(() => {
+      crack.remove();
+    }, 1000);
+  }
+}
+
+  playBurstSound(type: string) {
+    // 这里可以添加爆裂音效
+    const sounds = [
+      'assets/sounds/pop1.mp3',
+    ];
+    const sound = new Audio(sounds[Math.floor(Math.random() * sounds.length)]);
+    sound.volume = 0.3;
+    sound.play().catch(e => console.log('Audio play error:', e));
+  }
+
+  resetBubbles() {
+    cancelAnimationFrame(this.animationFrameId);
+    this.generateBubbles();
+    this.startBubbleMovement();
+  }
+
+// 添加点击处理方法
+onBubbleClick(bubble: Bubble, event: MouseEvent) {
+  //this.createRipple(event, bubble);
+  this.burstBubble(bubble);
+}
+
+// // 播放击中音效
+playHitSound() {
+  const sounds = [
+    'assets/sounds/pop2.mp3'
+  ];
+  const sound = new Audio(sounds[Math.floor(Math.random() * sounds.length)]);
+  sound.volume = 0.2;
+  sound.play().catch(e => console.log('Audio play error:', e));
+}
+
+showCompletionMessage() {
+    // 创建更漂亮的完成消息
+    const message = document.createElement('div');
+    message.innerHTML = `
+      <div style="
+        position: fixed;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        background: rgba(0,0,0,0.8);
+        color: white;
+        padding: 20px;
+        border-radius: 10px;
+        text-align: center;
+        z-index: 100;
+        animation: zoomIn 0.5s;
+      ">
+        <h2>🎉 干得漂亮! 🎉</h2>
+        <p>你已经粉碎了所有负面情绪!</p>
+        <ion-button onclick="this.parentElement.remove()" fill="clear" style="color: white;">
+          继续发泄
+        </ion-button>
+      </div>
+    `;
+    document.body.appendChild(message);
+  }
+}

+ 4 - 0
myapp/src/app/tab2/tab2-routing.module.ts

@@ -18,6 +18,10 @@ const routes: Routes = [
   {
     path: 'dynamic-create',
     loadChildren: () => import('./dynamic-create/dynamic-create.module').then( m => m.DynamicCreatePageModule)
+  },
+  {
+    path: 'emotion-vent',
+    loadChildren: () => import('./emotion-vent/emotion-vent.module').then( m => m.EmotionVentPageModule)
   }
 ];
 

+ 1 - 1
myapp/src/app/tab2/tab2.page.html

@@ -22,7 +22,7 @@
     <div (click)="importData()" class="function-card card-1">树洞</div>
     <div (click)="goThankslist('清单')" class="function-card card-2">感恩清单</div>
     <div class="function-card card-3">漂流瓶</div>
-    <div class="function-card card-4">情绪发泄室</div>
+    <div (click)="goVentRoom()" class="function-card card-4">情绪发泄室</div>
   </div>
   
   <!-- 动态日记标题栏 -->

+ 3 - 0
myapp/src/app/tab2/tab2.page.ts

@@ -40,6 +40,9 @@ export class Tab2Page implements OnInit {
     })
   }
 
+  goVentRoom(){
+    this.navCtrl.navigateForward(["tabs","tab2","emotion-vent"])
+  }
   // async chaXun(){
   //   //获取当前用户
   //   let user:any =new CloudUser();

BIN
myapp/src/assets/sounds/pop1.mp3


BIN
myapp/src/assets/sounds/pop2.mp3