Browse Source

新增小客服

warrior 1 week ago
parent
commit
c610708caf

+ 13 - 1
projects/live-app/src/app/components/flutter-comp/flutter-comp.component.ts

@@ -40,7 +40,19 @@ export class FlutterCompComponent implements OnInit {
       beginPoint: 0, // 起始播放时间点(单位秒),在一些浏览器中可能无效
       accurate: true, // 是否启用精准模式(使用requestVideoFrameCallback提升融合效果,浏览器不兼容时自动降级)
     });
-    vapPlayer.play(); // 开始播放
+    // vapPlayer.play(); // 开始播放
+
+    // 尝试通过用户交互触发播放
+    if (document.hidden) {
+      document.addEventListener('visibilitychange', () => {
+        if (!document.hidden) {
+          vapPlayer?.play();
+        }
+      });
+    } else {
+      vapPlayer?.play();
+    }
+
     vapPlayer.on('ended', () => {
       // 监听播放完成的事件
       vapPlayer.destroy();

+ 1 - 1
projects/live-app/src/modules/tabs/notice/notice.component.html

@@ -67,7 +67,7 @@
             </div>
             <ion-icon style="color: #afafaf" name="chevron-forward-outline"></ion-icon>
           </ion-item>
-          <ion-item class="li" (click)="isOpen = true">
+          <ion-item class="li" (click)="toUrl('/user/service')">
             <img src="img/客服.png" class="avatar" slot="start" alt="avatar" />
             <div class="li-right">
               <div class="name">小客服</div>

+ 144 - 0
projects/live-app/src/modules/user/online-service/online-service.component.html

@@ -0,0 +1,144 @@
+<nav title="小客服"></nav>
+<ion-content
+  [scrollEvents]="true"
+  [fullscreen]="true"
+  (ionScrollStart)="handleScrollStart()"
+  (ionScroll)="handleScroll($any($event))"
+  (ionScrollEnd)="handleScrollEnd()"
+  class="content"
+  (click)="changeShowEmoji(true)"
+>
+  <div #scrollMsg id="scrollMsg" class="scroll">
+    @for (item of msgServe.messageMapList[channel]; track $index) {
+    <div class="clearfix message-box">
+      @if ($index == 0 || item.timestamp -
+      msgServe.messageMapList[channel][$index-1].timestamp > 600) {
+      <div class="time-box">
+        @if (item.istoday) {
+        <div class="time">{{ item.timestamp | showDate }}</div>
+        }@else {
+        <div class="time">
+          {{ item.timestamp | showDate }}
+        </div>
+        }
+      </div>
+      }
+
+      <!-- 他人信息 -->
+      @if (!item.is_self) {
+      <div class="msg-bloak no_self">
+        <img
+          class="avatar fl"
+          (click)="toUrl('user/profile/' + item.publisher)"
+          mode="widthFix"
+          src="{{ item.avatar }}"
+        />
+        <!-- 文字消息 msg_type == 1 || text -->
+        @if (item.msg_type == 1 || item.msg_type == 'text') {
+        <div class="msg-card">
+          @if(channel == 'global_room'){
+          <span class="text-item_status">{{
+            item.name || "用户" + item.publisher
+          }}</span>
+          }
+          <div class="text-item fl text-item_left">
+            <p>{{ item.content }}</p>
+          </div>
+        </div>
+        }
+        <!-- 图片消息 msg_type == 2 || img -->
+        @else if(item.msg_type == 2 || item.msg_type == 'img' || item.msg_type
+        == 'image') {
+        <img
+          class="img-item fl"
+          [src]="item.content"
+          (click)="predivimg(item.content)"
+        />
+        <div class="text-item fl text-item_left">
+          <p>{{ item.content }}</p>
+        </div>
+        }
+      </div>
+      } @else {
+      <div class="msg-bloak self">
+        <!-- 自己信息 -->
+        @if (item.msg_type == 1 || item.msg_type == 'text') {
+        <div class="text-item fr text-item_right">
+          <p>{{ item.content }}</p>
+        </div>
+        } @if (item.msg_type == 2 || item.msg_type == 'img' || item.msg_type ==
+        'image') {
+        <img
+          class="img-item fr"
+          src="{{ item.content }}"
+          (click)="predivimg(item.content)"
+          bindtap="predivimg"
+          data-src="{{ item.content }}"
+        />
+        } @if (item.msg_type == 3) {
+        <div class="text-item fr text-item_right">
+          <p>{{ item.content }}</p>
+        </div>
+        }
+        <img class="avatar fr" [src]="item.avatar" />
+      </div>
+      }
+    </div>
+    }
+  </div>
+</ion-content>
+<ion-footer>
+  <ion-toolbar class="footer-tool">
+    <ion-input
+      slot="start"
+      labelPlacement="stacked"
+      [clearInput]="true"
+      placeholder="文明发言"
+      [(ngModel)]="text"
+      (keydown)="comfirmText($event)"
+      [disabled]="false"
+    >
+    </ion-input>
+    <div
+      [ngClass]="{
+        tools: true
+      }"
+      slot="end"
+    >
+      <ion-icon name="happy-outline" (click)="changeShowEmoji()"></ion-icon>
+ 
+      <span class="splice">|</span>
+      <!-- <div class="send">发送</div> -->
+      <ion-button
+        class="send"
+        fill="outline"
+        size="small"
+        [disabled]="text.length == 0 || disabled"
+        (click)="comfirmText()"
+        >发送</ion-button
+      >
+    </div>
+  </ion-toolbar>
+  <div [style.height]="height + 'px'"></div>
+  @if (showEmoji) {
+  <div class="b-1px-b"></div>
+  } @if (showEmoji) {
+  <div class="emoji-content">
+    <div class="emoji-box">
+      @for (item of aiServ.emojis; track $index) {
+      <div class="emoji-item">
+        <div class="emoji-img" (click)="emojiChoose(item.char)">
+          {{ item.char }}
+        </div>
+      </div>
+      }
+      <div class="emoji-item__del" (click)="delEmoji()">
+        <img
+          class="emoji-img"
+          src="https://common.file.futurestack.cn/images/wxapp/card_multi/images//chat/del.svg"
+        />
+      </div>
+    </div>
+  </div>
+  }
+</ion-footer>

+ 156 - 0
projects/live-app/src/modules/user/online-service/online-service.component.scss

@@ -0,0 +1,156 @@
+.content {
+  // padding-bottom: 12.8205vw;
+  .scroll {
+    overflow: hidden;
+    padding-bottom: 12.8205vw;
+  }
+  .avatar {
+    width: 10.2564vw;
+    height: 10.2564vw;
+  }
+  .message-box {
+    margin-bottom: 7.6923vw;
+    .time-box {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      height: 12.8205vw;
+      font-size: 3.0769vw;
+      color: #fff;
+      .time {
+        background: rgba(0, 0, 0, 0.2);
+        border-radius: 1.0256vw;
+        padding: 0 2.5641vw;
+      }
+    }
+    .text-item {
+      position: relative;
+      line-height: 1.5;
+      font-size: 4.1026vw;
+      padding: 2.5641vw;
+      max-width: 56.4103vw;
+      border-radius: 1.0256vw;
+      word-wrap: break-word;
+      min-height: 10.2564vw;
+      min-width: 10.2564vw;
+    }
+    .msg-bloak {
+      display: flex;
+      .text-item_left {
+        background: #f1f1f1;
+      }
+      .text-item_left::before {
+        content: "";
+        position: absolute;
+        left: -2.8205vw;
+        top: calc(5.1282vw - 1.5385vw);
+        border: 1.5385vw solid transparent;
+        border-right-color: #f1f1f1;
+        z-index: 20;
+      }
+      .text-item_right {
+        background: #a2e65b;
+      }
+      .text-item_right::before {
+        content: "";
+        position: absolute;
+        left: calc(100% - 0.2564vw);
+        top: calc(5.1282vw - 1.5385vw);
+        border: 1.5385vw solid transparent;
+        border-left-color: #a2e65b;
+      }
+    }
+    .self {
+      justify-content: end;
+    }
+    .no_self {
+      justify-content: start;
+    }
+    .text-item_status {
+      // height: 10.2564vw;
+      display: flex;
+      align-items: center;
+      // margin-right: 3.0769vw;
+      font-size: 3.0769vw;
+      color: #888;
+    }
+
+    .text-item_status--fail {
+      color: red;
+    }
+
+    .text-item_status--success {
+      color: #28bf39;
+    }
+  }
+  .img-item {
+    width: 35.8974vw;
+    height: 25.641vw;
+    object-fit: contain;
+  }
+}
+.footer-tool {
+  .tools {
+    // width: 41.0256vw;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    flex-shrink: 0;
+    ion-icon {
+      font-size: 7.6923vw;
+    }
+    .splice {
+      margin: 0 0.5128vw;
+    }
+  }
+  .tools-maxwid {
+    width: 41.0256vw;
+  }
+}
+.emoji-content {
+  background: #fbfbfb;
+  height: 41.0256vw;
+  overflow-y: scroll;
+  .emoji-box {
+    display: flex;
+    flex-wrap: wrap;
+    width: 100%;
+    .emoji-item {
+      width: 7.6923vw;
+      height: 7.6923vw;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      .emoji-img {
+        width: 5.1282vw;
+        height: 5.1282vw;
+      }
+    }
+    .emoji-item__del {
+      width: 7.6923vw;
+      height: 7.6923vw;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      .emoji-img {
+        width: 5.1282vw;
+        height: 5.1282vw;
+      }
+    }
+  }
+}
+ion-popover {
+  --width: 38.4615vw;
+}
+.clear {
+  font-size: 3.5897vw;
+}
+.fl {
+  margin-right: 2.5641vw;
+  float: left;
+}
+
+.fr {
+  float: right;
+  margin-left: 2.5641vw;
+}

+ 28 - 0
projects/live-app/src/modules/user/online-service/online-service.component.spec.ts

@@ -0,0 +1,28 @@
+/* tslint:disable:no-unused-variable */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+
+import { OnlineServiceComponent } from './online-service.component';
+
+describe('OnlineServiceComponent', () => {
+  let component: OnlineServiceComponent;
+  let fixture: ComponentFixture<OnlineServiceComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ OnlineServiceComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(OnlineServiceComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 186 - 0
projects/live-app/src/modules/user/online-service/online-service.component.ts

@@ -0,0 +1,186 @@
+import { DatePipe, CommonModule } from '@angular/common';
+import { HttpClient } from '@angular/common/http';
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { SharedModule } from '../../shared.module';
+
+import * as Parse from 'parse';
+import { MessageService } from '../../../services/message.service';
+import {
+  AlertController,
+  ionicStandaloneModules,
+  LoadingController,
+  ScrollDetail,
+  ToastController,
+} from '../../ionic-standalone.modules';
+import { AiChatService } from '../../../services/aichart.service';
+import { AccountService } from '../../../services/account.service';
+import { DeviceService } from '../../../services/device.service';
+import { NavComponent } from '../../../app/components/nav/nav.component';
+
+@Component({
+  selector: 'app-online-service',
+  templateUrl: './online-service.component.html',
+  styleUrls: ['./online-service.component.scss'],
+  standalone: true,
+  imports: [
+    ...ionicStandaloneModules,
+    NavComponent,
+    FormsModule,
+    // DatePipe,
+    CommonModule,
+    SharedModule,
+  ],
+})
+export class OnlineServiceComponent implements OnInit {
+
+  channel: string = 'admin';
+  uid: string = '';
+  profile?: Parse.Object; // 对方身份
+  targetUser?: Parse.Object; // 对方用户
+  text: string = '';
+  showPreView: boolean = false; //预览图片
+  viewImg: string = '';
+  height: number = 0;
+  showEmoji: boolean = false;
+  timer: any;
+  currentScroll: number = 0; //滚动条位置
+  disabled: boolean = false;
+
+  constructor(
+    private router: Router,
+    private http: HttpClient,
+    // public datePipe: DatePipe,
+    private toastController: ToastController,
+    private alertController: AlertController,
+    private loadingCtrl: LoadingController,
+    private activateRoute: ActivatedRoute,
+    public msgServe: MessageService,
+    public aiServ: AiChatService,
+    public accServ: AccountService,
+    public deviceSer: DeviceService
+  ) {
+    msgServe.pageFun = this.updatePage;
+    msgServe.pageFun();
+  }
+
+  ngOnInit() {
+    this.activateRoute.paramMap.subscribe(async (params) => {
+      this.refresh();
+    });
+  }
+  updatePage() {
+    setTimeout(() => {
+      let dom = document.getElementById('scrollMsg');
+      if (dom) {
+        dom.scrollIntoView({
+          behavior: 'smooth',
+          block: 'end',
+          inline: 'nearest',
+        });
+      }
+    }, 500);
+  }
+  async refresh() {
+    const loading = await this.loadingCtrl.create({
+      message: '加载中',
+    });
+    loading.present();
+    await this.msgServe.initRTM();
+    await this.msgServe.subscribeMessage(
+      this.channel,
+      {
+        message: true,
+      }
+    ); //订阅消息
+    loading.dismiss();
+  }
+
+
+  ngOnDestroy(): void {
+    this.msgServe.pageFun = () => {};
+    if (!this.uid) {
+      this.msgServe.unsubscribeMessage(this.channel);
+    }
+  }
+  /* 开始滑动 */
+  handleScrollStart() {
+    // console.log('scroll start');
+  }
+  /* 滑动中 */
+  handleScroll(ev: CustomEvent<ScrollDetail>) {
+    let srcollop = ev.detail.scrollTop;
+    this.currentScroll = srcollop;
+  }
+  /* 结束滚动 */
+  async handleScrollEnd() {
+    console.log(this.currentScroll);
+    if (this.currentScroll === 0) {
+      this.timer && clearTimeout(this.timer);
+      const loading = await this.loadingCtrl.create({
+        message: '消息加载中',
+        duration: 1500,
+      });
+      loading.present();
+      this.timer = setTimeout(() => {}, 500);
+    }
+  }
+  //调起表情
+  changeShowEmoji(isClose?: boolean) {
+    this.showEmoji = isClose ? false : !this.showEmoji;
+    this.height = 0;
+  }
+  //添加表情
+  emojiChoose(value: any) {
+    console.log(value);
+    this.text = this.text + value;
+  }
+  //删除表情
+  delEmoji() {
+    this.text = this.text.substring(0, this.text.length - 2);
+  }
+  //发送文字
+  comfirmText(e?: any) {
+    if (e && e.keyCode != 13) {
+      return;
+    }
+    this.disabled = true;
+    if (this.text == '' && this.text.trim() == '') {
+      this.presentToast('聊天内容不能为空');
+      this.disabled = false;
+      return;
+    }
+    this.send({
+      msg_type: 'text',
+      content: this.text,
+    });
+  }
+  async send(param: { msg_type: string; content: string }) {
+    await this.msgServe.publishMessage(param.content, this.channel);
+    this.text = '';
+    this.disabled = false;
+  }
+  predivimg<T>(value: string) {
+    this.showPreView = true;
+    this.viewImg = value;
+  }
+  onBack() {
+    history.back();
+  }
+  onSendGift() {
+    console.log('点击送出礼物');
+    // this.liveService.get_duration()
+  }
+  async presentToast(title: string, time?: number, color?: string) {
+    const toast = await this.toastController.create({
+      message: title,
+      duration: time || 1500,
+      color: color || 'danger',
+    });
+    toast.present();
+  }
+  toUrl(url: string) {
+    this.router.navigateByUrl(url);
+  }
+}

+ 5 - 0
projects/live-app/src/modules/user/user.modules.routes.ts

@@ -14,6 +14,7 @@ import { GiftIncomeComponent } from './gift-income/gift-income.component';
 import { RankingComponent } from './ranking/ranking.component';
 import { FriendsComponent } from './friends/friends.component';
 import { GiftLogComponent } from './gift-log/gift-log.component';
+import { OnlineServiceComponent } from './online-service/online-service.component';
 const routes: Routes = [
   {
     path: 'profile/:id',//主页
@@ -72,6 +73,10 @@ const routes: Routes = [
     path: 'giftlog', //礼物记录
     component: GiftLogComponent,
   },
+  {
+    path: 'service', //小客服
+    component: OnlineServiceComponent,
+  },
 ]
 @NgModule({
   imports: [RouterModule.forChild(routes)],

+ 1 - 1
projects/live-app/src/services/message.service.ts

@@ -94,7 +94,7 @@ export class MessageService {
     // let states = ['CONNECTED', 'CONNECTING'];
     if (this.options.connectState) return;
     await this.getToken();
-    const rtmConfig = { logLevel: 'INFO', logUpload: false };
+    const rtmConfig = { logLevel: 'error', logUpload: false };
     this.rtmClient = new AgoraRTM.RTM(this.appid, this.userId, rtmConfig);
     this.joinReady();
     await this.loginRTM();