快乐的梦鱼 2 周之前
父節點
當前提交
60b4b66dde
共有 2 個文件被更改,包括 162 次插入29 次删除
  1. 156 29
      src/pages/chat/core/speical/ssemp.ts
  2. 6 0
      src/pages/home/village/introd/card.vue

+ 156 - 29
src/pages/chat/core/speical/ssemp.ts

@@ -11,35 +11,156 @@ export interface SSEOptions {
 type SSEOpenHandler = ((event?: Event) => void) | null;
 type SSEMessageHandler = ((event: MessageEvent<string>) => void) | null;
 type SSEErrorHandler = ((error: unknown) => void) | null;
+type ByteArray = Uint8Array<ArrayBufferLike>;
 
-function createTextDecoder() {
-  if (typeof TextDecoder !== "undefined") {
-    return new TextDecoder("utf-8");
+type ChunkDecoder = {
+  decode: (data: unknown) => string;
+  flush: () => string;
+};
+
+function toUint8Array(data: unknown): ByteArray | null {
+  if (data instanceof Uint8Array) {
+    return data as ByteArray;
+  }
+  if (data instanceof ArrayBuffer) {
+    return new Uint8Array(data) as ByteArray;
+  }
+  if (ArrayBuffer.isView(data)) {
+    return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) as ByteArray;
   }
   return null;
 }
 
-function decodeBufferChunk(data: unknown, decoder: TextDecoder | null): string {
-  if (typeof data === "string") {
-    return data;
-  }
+function decodeUtf8Bytes(bytes: ByteArray, previousTail: ByteArray): { text: string; tail: ByteArray } {
+  const merged = previousTail.length
+    ? (() => {
+      const tmp = new Uint8Array(previousTail.length + bytes.length);
+      tmp.set(previousTail, 0);
+      tmp.set(bytes, previousTail.length);
+      return tmp as ByteArray;
+    })()
+    : bytes;
+
+  let text = "";
+  let i = 0;
+
+  while (i < merged.length) {
+    const b1 = merged[i];
+
+    if (b1 <= 0x7f) {
+      text += String.fromCharCode(b1);
+      i += 1;
+      continue;
+    }
 
-  if (data instanceof ArrayBuffer) {
-    if (decoder) {
-      return decoder.decode(new Uint8Array(data), { stream: true });
+    let needed = 0;
+    if (b1 >= 0xc2 && b1 <= 0xdf) needed = 2;
+    else if (b1 >= 0xe0 && b1 <= 0xef) needed = 3;
+    else if (b1 >= 0xf0 && b1 <= 0xf4) needed = 4;
+    else {
+      text += "\ufffd";
+      i += 1;
+      continue;
     }
-    return String.fromCharCode(...new Uint8Array(data));
-  }
 
-  if (ArrayBuffer.isView(data)) {
-    const typedArray = data as Uint8Array;
-    if (decoder) {
-      return decoder.decode(typedArray, { stream: true });
+    if (i + needed > merged.length) {
+      break;
     }
-    return String.fromCharCode(...typedArray);
+
+    const b2 = merged[i + 1];
+    if ((b2 & 0xc0) !== 0x80) {
+      text += "\ufffd";
+      i += 1;
+      continue;
+    }
+
+    if (needed === 2) {
+      const codePoint = ((b1 & 0x1f) << 6) | (b2 & 0x3f);
+      text += String.fromCharCode(codePoint);
+      i += 2;
+      continue;
+    }
+
+    const b3 = merged[i + 2];
+    if ((b3 & 0xc0) !== 0x80) {
+      text += "\ufffd";
+      i += 1;
+      continue;
+    }
+
+    if (needed === 3) {
+      const codePoint = ((b1 & 0x0f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f);
+      text += String.fromCharCode(codePoint);
+      i += 3;
+      continue;
+    }
+
+    const b4 = merged[i + 3];
+    if ((b4 & 0xc0) !== 0x80) {
+      text += "\ufffd";
+      i += 1;
+      continue;
+    }
+
+    const codePoint =
+      ((b1 & 0x07) << 18) |
+      ((b2 & 0x3f) << 12) |
+      ((b3 & 0x3f) << 6) |
+      (b4 & 0x3f);
+    const cp = codePoint - 0x10000;
+    text += String.fromCharCode((cp >> 10) + 0xd800, (cp & 0x3ff) + 0xdc00);
+    i += 4;
   }
 
-  return "";
+  return {
+    text,
+    tail: i < merged.length ? (merged.slice(i) as ByteArray) : (new Uint8Array(0) as ByteArray),
+  };
+}
+
+function createChunkDecoder(): ChunkDecoder {
+  if (typeof TextDecoder !== "undefined") {
+    const decoder = new TextDecoder("utf-8");
+    return {
+      decode(data: unknown) {
+        if (typeof data === "string") {
+          return data;
+        }
+        const bytes = toUint8Array(data);
+        if (!bytes) {
+          return "";
+        }
+        return decoder.decode(bytes, { stream: true });
+      },
+      flush() {
+        return decoder.decode();
+      },
+    };
+  }
+
+  let tail: ByteArray = new Uint8Array(0) as ByteArray;
+  return {
+    decode(data: unknown) {
+      if (typeof data === "string") {
+        return data;
+      }
+      const bytes = toUint8Array(data);
+      if (!bytes) {
+        return "";
+      }
+      const decoded = decodeUtf8Bytes(bytes, tail);
+      tail = decoded.tail;
+      return decoded.text;
+    },
+    flush() {
+      if (!tail.length) {
+        return "";
+      }
+      const decoded = decodeUtf8Bytes(new Uint8Array(0) as ByteArray, tail);
+      tail = new Uint8Array(0) as ByteArray;
+      return decoded.text;
+    },
+  };
 }
 
 function parsePayload(payload?: string): string | Record<string, unknown> | ArrayBuffer | undefined {
@@ -182,9 +303,10 @@ export class SSE {
   }
 
   private startWithUniRequest() {
-    const decoder = createTextDecoder();
+    const chunkDecoder = createChunkDecoder();
     const method = this.options.method || "GET";
     const payloadData = parsePayload(this.options.payload);
+    let hasReceivedChunk = false;
 
     const requestTask = uni.request({
       url: this.url,
@@ -204,8 +326,14 @@ export class SSE {
         if (this.closed) {
           return;
         }
-        const text = decodeBufferChunk(res.data, decoder);
-        this.consumeSseChunk(text);
+        if (!hasReceivedChunk) {
+          const text = chunkDecoder.decode(res.data);
+          this.consumeSseChunk(text);
+        }
+        const tail = chunkDecoder.flush();
+        if (tail) {
+          this.consumeSseChunk(tail);
+        }
         this.flushRemainder();
       },
       fail: (error) => {
@@ -220,10 +348,11 @@ export class SSE {
     };
     if (typeof chunkableTask.onChunkReceived === "function") {
       chunkableTask.onChunkReceived((result: { data: ArrayBuffer }) => {
+        hasReceivedChunk = true;
         if (this.closed) {
           return;
         }
-        const text = decodeBufferChunk(result.data, decoder);
+        const text = chunkDecoder.decode(result.data);
         this.consumeSseChunk(text);
       });
     }
@@ -265,22 +394,20 @@ export class SSE {
       }
 
       const reader = response.body.getReader();
-      const decoder = createTextDecoder();
+      const chunkDecoder = createChunkDecoder();
 
       while (!this.closed) {
         const result = await reader.read();
         if (result.done) {
           break;
         }
-        const chunkText = decodeBufferChunk(result.value, decoder);
+        const chunkText = chunkDecoder.decode(result.value);
         this.consumeSseChunk(chunkText);
       }
 
-      if (decoder) {
-        const tail = decoder.decode();
-        if (tail) {
-          this.consumeSseChunk(tail);
-        }
+      const tail = chunkDecoder.flush();
+      if (tail) {
+        this.consumeSseChunk(tail);
       }
       this.flushRemainder();
     } catch (error) {

+ 6 - 0
src/pages/home/village/introd/card.vue

@@ -212,6 +212,7 @@
             :userName="item.nickName ?? ''"
             :likes="item.likeCount"
             :isLike="false"
+            @click="handleGoPost(item.id)"
           />
         </MasonryGridItem>
       </MasonryGrid>
@@ -385,4 +386,9 @@ function handleGoPublish() {
     villageId: villageStore.currentVillage?.id ?? undefined,
   });
 }
+function handleGoPost(id: number) {
+  navTo('/pages/home/post/detail', {
+    id: id,
+  });
+}
 </script>