15207938132 2 сар өмнө
parent
commit
dbdba12578

+ 166 - 0
fashion-app/src/app/send-post/hwobs.service.ts

@@ -0,0 +1,166 @@
+import { Injectable } from '@angular/core';
+
+// @ts-ignore
+import ObsClient from "esdk-obs-browserjs"
+
+import Parse from "parse";
+
+/**
+ * HwobsDir 华为OBS目录接口
+ * @public
+ */
+export interface HwobsDir{
+  Prefix:string  // "storage/2023/"
+
+}
+
+/**
+ * HwobsDir 华为OBS文件接口
+ * @public
+ */
+export interface HwobsFile{
+  ETag: "\"f0ec968fe51ab48348307e06476122eb\""
+  Key:string  //"storage/3mkf41033623275.png"
+  LastModified:string //"2023-11-08T12:03:13.008Z"
+  Owner:object // {ID: '09971a1979800fb60fbbc00ada51f7e0'}
+  Size:string //"25839"
+  StorageClass:string //"STANDARD"
+}
+
+/**
+ * HwobsProvider 华为OBS文件服务
+ * @public
+ */
+export class HwobsProvider {
+  obsClient:ObsClient
+  bucketName:string
+  host:string
+  globalPrefix:string = ""
+  constructor(options:{
+    host:string
+    bucketName:string
+    access_key_id:string
+    secret_access_key:string
+    prefix?:string
+    server?:string
+  }) {
+      this.globalPrefix = options.prefix || ""
+      this.host = options?.host
+      this.bucketName = options?.bucketName
+      this.obsClient = new ObsClient({
+        access_key_id: options.access_key_id,       
+        secret_access_key: options.secret_access_key,
+        // 这里以华南-广州为例,其他地区请按实际情况填写
+        server: options?.server||'https://obs.cn-south-1.myhuaweicloud.com'
+    });
+  }
+
+  /**
+   * 目录及检索相关函数
+   */
+  listDir(prefix:any):Promise<{
+    dirs:Array<HwobsDir>,
+    files:Array<HwobsFile>
+  }>{
+    return new Promise((resolve,reject)=>{
+        this.obsClient.listObjects({
+          Bucket : this.bucketName,
+          Prefix : prefix,
+          Delimiter: '/'
+        }, (err:any, result:any) => {
+          if(err){              
+            console.error('Error-->' + err);   
+            reject(err)    
+          }else{              
+            console.log('Status-->' + result.CommonMsg.Status);
+            console.log(result)
+                    if(result.CommonMsg.Status < 300 && result.InterfaceResult){
+                          for(var j in result.InterfaceResult.Contents){
+                                    console.log('Contents[' + j +  ']:');
+                                    console.log('Key-->' + result.InterfaceResult.Contents[j]['Key']);
+                                    console.log('Owner[ID]-->' + result.InterfaceResult.Contents[j]['Owner']['ID']);
+                            }
+                    }       
+                    let dirs:HwobsDir[] = result.InterfaceResult.CommonPrefixes
+                    let files:HwobsFile[] = result.InterfaceResult.Contents
+                    resolve({dirs:dirs,files:files})     
+          }
+        });
+      })
+
+  }
+
+  /**
+   * 文件上传相关函数
+   * @param file 
+   * @param key 
+   * @returns 
+   */
+  async uploadFile(file:File,key:string):Promise<Parse.Object>{
+    console.log(this.globalPrefix,key)
+    // key 文件上传后的全部路径
+    // /storage/<公司账套>/<应用名称>/年月日/<文件名>.<文件后缀>
+    // /storage/web2023/<学号>/年月日/<文件名>.<文件后缀>
+    let attach = await this.checkFileExists(file);
+    if(attach?.id) return attach
+      return new Promise((resolve,reject)=>{
+        this.obsClient.putObject({
+              Bucket : this.bucketName,
+              Key : this.globalPrefix+key,
+              SourceFile : file
+        },  async (err:any, result:any) => {
+              if(err){
+                    console.error('Error-->' + err);
+                    reject(err)
+              }else{
+                console.log('Status-->' + result.CommonMsg.Status);
+                let attach = await this.saveAttachment(file,this.globalPrefix+key)
+                resolve(attach)
+              }
+        });
+      })
+  }
+  Attachment = Parse.Object.extend("Attachment")
+
+  async checkFileExists(file:any):Promise<Parse.Object>{
+    let hash = await this.getFileHash(file)
+    // 文件HASH查重,避免重复上传
+    let attach:Parse.Object
+    let query = new Parse.Query("Attachment")
+    query.equalTo("hash",hash);
+    query.equalTo("size",file.size);
+    let exists:any = await query.first();
+    if(!exists?.id) exists = new this.Attachment()
+    attach = exists
+    return attach
+  }
+  async saveAttachment(file:File,key:string){
+    console.log("saveAttachment",key)
+    let hash = await this.getFileHash(file)
+    let attach = await this.checkFileExists(file)
+    attach.set("name",file.name)
+    attach.set("size",file.size)
+    attach.set("mime",file.type)
+    attach.set("url",this.host + key)
+    attach.set("hash",hash)
+    attach = await attach.save()
+    return attach
+  }
+
+  async getFileHash(file:File) {
+    return new Promise((resolve, reject) => {
+      const reader = new FileReader();
+      reader.onload = async (event:any) => {
+        const buffer = event.target.result;
+        const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
+        const hashArray = Array.from(new Uint8Array(hashBuffer));
+        const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
+        resolve(hashHex);
+      };
+      reader.onerror = (event:any) => {
+        reject(event.target.error);
+      };
+      reader.readAsArrayBuffer(file);
+    });
+  }
+}

+ 31 - 5
fashion-app/src/app/send-post/send-post.component.html

@@ -17,7 +17,7 @@
   </ion-toolbar>
 </ion-header>
 
-<ion-content>
+<ion-content class="content">
   <div class="input-area">
     <div class="title-input">
       <ion-input 
@@ -29,12 +29,25 @@
       <span class="char-counter">{{ remainingChars }} </span>
     </div>
     <ion-textarea 
-      [(ngModel)]="inputText" 
+      [(ngModel)]="inputText" [autoGrow]="true"  
       placeholder="分享你的穿搭..." >
     </ion-textarea>
   </div>
+
+
+
 </ion-content>
 
+
+<!--标签-->
+  <div class="tag-container">
+    <div class="tag"><ion-icon name="bookmark-outline" class="tagpicture"></ion-icon>标签1</div>
+    <div class="tag"><ion-icon name="bookmark-outline" class="tagpicture"></ion-icon>标签2</div>
+    <div class="tag"><ion-icon name="bookmark-outline" class="tagpicture"></ion-icon>标签3</div>
+    <div class="tag"><ion-icon name="bookmark-outline" class="tagpicture"></ion-icon>标签4</div>
+  </div>
+
+
 <ion-footer class="footer">
   <div class="left-icons">
     <div class="footer-button" (click)="uploadImage()">
@@ -43,7 +56,7 @@
     <div fill="clear"class="footer-button">
       <ion-icon name="at-outline"></ion-icon>
     </div>
-    <div fill="clear" class="footer-button">
+    <div fill="clear" class="footer-button" (click)="openEmojiPicker()">
       <ion-icon name="happy-outline"></ion-icon>
     </div>
   </div>
@@ -54,7 +67,20 @@
       <ion-icon name="close-outline" slot="end"></ion-icon>
 
   </div>
+<!-- 隐藏的文件输入 -->
+<input type="file" accept="image/*" (change)="onFileChange($event)" style="display: none;" #fileInput>
+
+<!-- 表情模拟框 -->
+<ion-modal [isOpen]="isEmojiPickerOpen" (didDismiss)="closeEmojiPicker()" [initialBreakpoint]="0.71" [breakpoints]="[0, 0.25,0.50,0.71]" handleBehavior="cycle">
+  <ng-template>
+    <ion-content class="emoji-picker">
+      <div class="emoji-container">
+        <div *ngFor="let emoji of emojis" (click)="addEmoji(emoji)" class="emoji-button"> <!-- 表情按钮 -->
+          {{ emoji }}
+        </div>
+      </div>
+    </ion-content>
+  </ng-template>
+</ion-modal>
 
-  <!-- 隐藏的文件输入 -->
-  <input type="file" accept="image/*" (change)="onFileSelected($event)" style="display: none;" #fileInput>
 </ion-footer>

+ 104 - 2
fashion-app/src/app/send-post/send-post.component.scss

@@ -25,8 +25,15 @@
     }
   }
   
+  .content {
+    display: flex;
+    flex-direction: column; /* 垂直排列子元素 */
+    height: 100%; /* 确保内容区域占满整个高度 */
+  }
+
   .input-area {
-    height: 670px; /* 设置输入区域的高度 */
+    flex: 1; /* 让输入区域占用剩余空间 */
+    
     padding: 10px; /* 设置内边距 */
     font-size: 18px;
   }
@@ -37,6 +44,7 @@
     align-items: center; /* 垂直居中对齐 */
     margin-bottom: 10px; /* 标题输入框与文本区域之间的间距 */
   }
+
   
   .title-input ion-input {
     border: none; /* 移除默认边框 */
@@ -44,6 +52,9 @@
     flex: 1; /* 使输入框填满可用空间 */
     margin-right: 10px; /* 输入框与字数计数器之间的间距 */
   }
+
+ 
+
   
   .char-counter {
     color: gray; /* 字数计数器的颜色 */
@@ -84,4 +95,95 @@
     margin-right: 20px;
     margin-top: 30px;
     margin-bottom: 10px;
-  }
+  }
+
+
+  /* 表情选择器样式 */
+.emoji-picker {
+  --background: transparent; /* 去除默认样式 */
+  background-color:white; /* 背景颜色 */
+  padding: 10px;
+  display: flex;
+  justify-content: center; /* 水平居中 */
+  overflow: hidden; /* 隐藏多余内容 */
+}
+
+/* 表情容器用于支持滚动 */
+.emoji-container {
+  display: flex;
+  flex-wrap: wrap; /* 允许换行 */
+  overflow-y: auto; /* 允许纵向滚动 */
+  max-height: 70vh; /* 最大高度,防止超出屏幕 */
+  width: 100%; /* 容器宽度 */
+}
+
+/* 表情按钮 */
+.emoji-button {
+  margin: 5px; /* 每个表情与顶部的间距 */
+  font-size: 28px; /* 字体大小 */
+  height: 40px; /* 按钮高度 */
+  width: 40px; /* 按钮宽度 */
+  display: flex; /* 使用 flexbox 对齐 */
+  align-items: center; /* 垂直居中 */
+  justify-content: center; /* 水平居中 */
+  --background: transparent; /* 背景透明 */
+  --box-shadow: none; /* 去掉阴影 */
+  --outline: none; /* 去掉轮廓 */
+  border: none; /* 去掉边框 */
+}
+//标签
+.tag-container {
+  display: flex;
+  justify-content: flex-start; /* 标签靠左对齐 */
+  padding: 10px; /* 标签容器内边距 */
+  margin-bottom: 10px; /* 将标签容器推到底部 */
+}
+
+.tag {
+  background-color: #f0f0f0; // 灰色背景
+  color: black; // 黑色字体
+  border-radius: 20px; // 椭圆形效果
+  padding: 5px 10px; // 标签内边距
+  margin-right: 17px; // 标签之间的间距
+  font-size: 14px; // 标签字体大小
+  display: inline-flex; // 让标签内容居中
+  align-items: center; // 垂直居中
+}
+
+.tagpicture{
+  margin-left: 5px;
+  margin-right: 5px;
+}
+
+
+.posts-container {
+  width: 100%; // 宽度占满父元素
+}
+
+.post {
+  background-color: #f9f9f9; // 背景色
+  border: 1px solid #ddd; // 边框
+  border-radius: 8px; // 圆角
+  padding: 10px;
+  margin-bottom: 10px; // 帖子之间的间距
+}
+
+.post h3 {
+  margin: 0; // 去掉默认的 margin
+}
+
+.post p {
+  margin: 5px 0; // 设置段落的 margin
+}
+
+.image-gallery {
+  display: flex; // 使用 flexbox 来排列图片
+  flex-wrap: wrap; // 允许换行
+}
+
+.post-image {
+  width: 100px; // 设置图片的宽度
+  height: 100px; // 设置图片的高度
+  object-fit: cover; // 保持图片的比例
+  margin-right: 5px; // 图片之间的间距
+}

+ 99 - 27
fashion-app/src/app/send-post/send-post.component.ts

@@ -1,21 +1,23 @@
 import { CommonModule } from '@angular/common';
-import { Component, OnInit } from '@angular/core';
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
 import { FormsModule } from '@angular/forms';
 import { Router } from '@angular/router';
-import { NavController } from '@ionic/angular';
+import {  NavController } from '@ionic/angular';
 import { IonButton, IonButtons,  IonContent, IonFooter, IonHeader, IonIcon, IonInput, IonItem,  IonTextarea,  IonTitle, IonToolbar,  } from '@ionic/angular/standalone';
 import { addIcons } from 'ionicons';
-import { atOutline, chevronBackSharp, closeOutline, happyOutline, imageOutline } from 'ionicons/icons';
+import { atOutline,  bookmarkOutline, chevronBackSharp, closeOutline, happyOutline, imageOutline, } from 'ionicons/icons';
 import { CloudObject, CloudUser } from 'src/lib/ncloud';
+import { IonModal,  } from '@ionic/angular/standalone'; // 导入独立组件
+import { HwobsProvider } from './hwobs.service';
 
-addIcons({chevronBackSharp,imageOutline,atOutline,happyOutline,closeOutline });
+addIcons({chevronBackSharp,imageOutline,atOutline,happyOutline,closeOutline,bookmarkOutline });
 @Component({
   selector: 'app-send-post',
   templateUrl: './send-post.component.html',
   styleUrls: ['./send-post.component.scss'],
   standalone: true,
    imports: [IonHeader,IonToolbar,IonTitle,IonContent,IonItem,
-      IonButton,IonIcon,IonButtons,IonInput,IonFooter,CommonModule,FormsModule,IonTextarea,],
+      IonButton,IonIcon,IonButtons,IonInput,IonFooter,CommonModule,FormsModule,IonTextarea,IonModal],
 })
 export class SendPostComponent  implements OnInit {
   inputText: string = ''; // 用于绑定输入框的内容
@@ -24,7 +26,21 @@ export class SendPostComponent  implements OnInit {
   remainingChars: number = 20; // 剩余字符数
   images: string[] = []; // 存储图片的数组
   tags: string[] = []; // 存储标签的数组
-  ngOnInit() {}
+  isPost: boolean = false; // 帖子发布状态
+  @Input() url:string = "";
+  @Output() onUrlChange:EventEmitter<string> = new EventEmitter<string>()
+
+  uploader:HwobsProvider|undefined
+
+  ngOnInit() {
+    this.uploader = new HwobsProvider({
+      bucketName:"nova-cloud",
+      prefix:"dev/jxnu/storage/",
+      host:"https://app.fmode.cn/",
+      access_key_id:"XSUWJSVMZNHLWFAINRZ1",
+      secret_access_key:"P4TyfwfDovVNqz08tI1IXoLWXyEOSTKJRVlsGcV6"
+    });
+  }
 
   constructor(private router: Router,private navCtrl: NavController) {}
 
@@ -40,10 +56,10 @@ publishPost() {
     console.warn('按钮处于默认样式,无法发布帖子');
     return; // 如果按钮处于默认样式,直接返回,不执行后续逻辑
 }
-
+this.isPost = true; // 发布成功后,将 isPost 设置为 true
   // 创建一个 CloudObject 实例,表示要保存的帖子
   const post = new CloudObject('post'); // 表名为 'post'
-
+ 
   // 设置帖子内容
   post.set({
     user: new CloudUser().toPointer(), // 指向当前用户
@@ -66,6 +82,7 @@ publishPost() {
     .catch((error:any) => {
       console.error('发布帖子时出错:', error);
     });
+    this.isPost = true; // 发布成功后,将 isPost 设置为 true
 }
 
 //  删除标签
@@ -78,28 +95,83 @@ publishPost() {
     }
 
 
-     // 处理文件选择
-  uploadImage() {
-    const fileInput = document.querySelector('input[type=file]') as HTMLInputElement; // 使用类型断言;
-    if (fileInput) {
-      fileInput.click(); // 点击文件输入框
+    uploadImage() {
+      const fileInput = document.querySelector('input[type=file]') as HTMLInputElement;
+      if (fileInput) {
+        fileInput.click(); // 点击文件输入框
+      }
     }
-  }
-
-  // 处理文件选择后的事件
-  onFileSelected(event: Event) {
-    const input = event.target as HTMLInputElement;
-    if (input.files && input.files.length > 0) {
-      const file = input.files[0];
-      const reader = new FileReader();
 
-      reader.onload = (e: any) => {
-        // 将图片数据存储到 images 数组中
-        this.images.push(e.target.result); // 将 Base64 编码的图片数据添加到数组
-      };
+    file:File|undefined
+    fileData:any = ""
+    fileList:File[] = []
+  
+    async upload(){
+      let filename = this.file?.name;
+      let dateStr = `${new Date().getFullYear()}${new Date().getMonth()+1}${new Date().getDate()}`;
+      let hourStr = `${new Date().getHours()}${new Date().getMinutes()+1}${new Date().getSeconds()}`;
+      let key = `${dateStr}/${hourStr}-${filename}`;
+      // let key = `storage/${filename}`
+      if(this.file){
+        let attachment = await this.uploader?.uploadFile(this.file,key);
+        console.log(attachment);
+        this.url = attachment?.get("url");
+        console.log(this.url);
+        this.onUrlChange.emit(this.url);
+        console.log(this.url);
+        this.images.push(this.url);
+      }
+      
+    }
+    /**
+     * 文件选择器 选择文件触发事件
+     * @param event 
+     */
+    async onFileChange(event:any){
+      console.log(event)
+      // 将选择的文件列表,赋值给fileList
+      this.fileList = event?.target?.files;
+      // 默认将第一个文件,显示在展示区域
+      this.setFile(event?.target?.files?.[0]);
+      this.upload();
+      this.fileList =[];
+    }
+  
+    /**
+     * 设置展示区域文件
+     * @param file 
+     */
+    async setFile(file:any){
+      // 将文件设置为展示区域文件
+      this.file = file
 
-      reader.readAsDataURL(file); // 读取文件为 Data URL
     }
-  }
+  
+
+
+    isEmojiPickerOpen: boolean = false; // 控制表情选择器的打开状态
+  emojis: string[] = ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '🫠', '😉', '😊', '😇', '🥰', '😍', '🤩', '😘',
+     '😗', '☺️', '😚', '😙', '🥲', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🫢', '🫣', '🤫', '🤔', '🫡', '🤐', '🤨', '😐',
+      '😑', '😶', '🫥', '😶‍🌫️', '😏', '😒', '🙄', '😬', '😮‍💨', '🤥', '🫨', '🙂‍↔️', '🙂‍↕️', '😌', '😔', '😪', '🤤', '😴', '🫩', '😷', 
+      '🤒', '🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '🥴', '😵', '😵‍💫', '🤯', '🤠', '🥳', '🥸', '😎', '🤓', '🧐', '😕', '🫤', '😟', 
+      '🙁', '☹️', '😮', '😯', '😲', '😳', '🥺', '🥹', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣', '😞', '😓', 
+      '😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '😈', '👿', '💀', '☠️', '💩', '🤡', '👹', '👺', '👻', '👽', '👾', '🤖', '😺', 
+      '😸', '😹', '😻', '😼', '😽', '🙀', '😿', '😾', '🙈', '🙉', '🙊', '💌', '💘', '❤️', '🖤', '💋', '💯', '💢', '💥', '💫', 
+      '💦', '💤']; // 表情数组
+// 打开表情选择器
+openEmojiPicker() {
+  this.isEmojiPickerOpen = true;
+}
+
+// 关闭表情选择器
+closeEmojiPicker() {
+  this.isEmojiPickerOpen = false; // 关闭模态框
+}
+
+// 添加表情到输入框
+addEmoji(emoji: string) {
+  this.inputText += emoji; // 将选中的表情添加到输入框
  
+}
+
 }

+ 14 - 1
fashion-app/src/app/tab1/tab1.page.html

@@ -29,8 +29,21 @@
 
   <div *ngIf="selectedSegment === '关注'" class="content">
     <h2>关注的内容</h2>
-    <p>这里是关注的内容,展示用户关注的项目、文章或其他信息。</p>
+
+    <ng-container *ngIf="currentUser ">
+      <div *ngFor="let post of posts" class="post">
+        <h3>{{ post.get('title') }}</h3>
+        <p>{{ post.get('content') }}</p>
+        <div class="image-gallery">
+          <img *ngFor="let image of post.get('image')" [src]="image" alt="Post Image" class="post-image">
+        </div>
+        <div class="tag-container">
+          <div class="tag" *ngFor="let tag of post.get('tag')">{{ tag }}</div>
+        </div>
+      </div>
+    </ng-container>
   </div>
+
   <div *ngIf="selectedSegment === '发现'" class="content">
     <h2>发现的内容</h2>
     <p>这里是发现的内容,展示推荐的项目或新发现的内容。</p>

+ 22 - 0
fashion-app/src/app/tab1/tab1.page.ts

@@ -7,6 +7,7 @@ import { EditRatingStarComponent } from '../edit-rating-star/edit-rating-star.co
 import { FormsModule } from '@angular/forms'; // 导入 FormsModule
 import { addIcons } from 'ionicons';
 import { CommonModule } from '@angular/common';
+import { CloudObject, CloudQuery, CloudUser } from 'src/lib/ncloud';
 
 addIcons({ menu, search });
 
@@ -29,6 +30,27 @@ goSearchPage(){
   this.router.navigate(['/tabs/test']);
 }
 
+posts: CloudObject[] = []; // 用于存储用户帖子
+  currentUser: CloudUser | null = null; // 当前用户
+
+
+  async ngOnInit() {
+    // 获取当前用户
+    this.currentUser = new CloudUser();
+    await this.currentUser.current(); // 获取当前用户信息
+
+    // 如果用户已登录,查询用户的帖子
+    if (this.currentUser.id) {
+      await this.fetchUserPosts();
+    }
+  }
+
+  async fetchUserPosts() {
+    const query = new CloudQuery('post');
+    query.equalTo('user', this.currentUser?.toPointer()); // 查询当前用户的帖子
+    this.posts = await query.find(); // 获取帖子列表
+  }
+
 
   }