Files
cozsweet-frontend-nextjs/src/app/chat/components/image-upload-button.tsx
T

120 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/**
* ImageUploadButton 图片上传按钮
*
* 原始 Dart: lib/ui/chat/widgets/image_upload_button.dart159 行)
*
* 原 Dart 使用 image_picker 库(移动端原生相册/相机)
* 本轮使用 Web File API`<input type="file">`)以避免添加原生依赖
*
* 行为:
* - 点击按钮 → 弹出底部 sheet(相册 / 取消)
* - 选择图片 → 读取为 base64 → dispatch ChatSendImage
* - 上传中按钮显示加载指示器
*/
import { useRef, useState } from "react";
import { useChatDispatch } from "@/stores/chat/chat-context";
import styles from "./image-upload-button.module.css";
export interface ImageUploadButtonProps {
disabled?: boolean;
}
export function ImageUploadButton({ disabled = false }: ImageUploadButtonProps) {
const dispatch = useChatDispatch();
const fileInputRef = useRef<HTMLInputElement>(null);
const [showSheet, setShowSheet] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const handleFile = async (file: File) => {
if (isUploading) return;
setIsUploading(true);
setShowSheet(false);
try {
const buffer = await file.arrayBuffer();
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
const base64 = window.btoa(binary);
const ext = file.name.split(".").pop()?.toLowerCase() || "png";
const mime = file.type || `image/${ext}`;
const dataUri = `data:${mime};base64,${base64}`;
dispatch({ type: "ChatSendImage", imageBase64: dataUri });
} catch (e) {
console.error("[ImageUpload] failed:", e);
} finally {
setIsUploading(false);
}
};
return (
<>
<button
type="button"
className={styles.button}
disabled={disabled || isUploading}
onClick={() => setShowSheet(true)}
aria-label="Upload image"
>
{isUploading ? "⏳" : "🖼"}
</button>
{/* 隐藏的文件 input(触发文件选择) */}
<input
ref={fileInputRef}
type="file"
accept="image/*"
className={styles.hidden}
onChange={(e) => {
const file = e.currentTarget.files?.[0];
if (file) void handleFile(file);
e.currentTarget.value = ""; // 允许重选同一文件
}}
/>
{/* 底部选择 sheet(相册/取消) */}
{showSheet && (
<>
<div
style={{
position: "fixed",
inset: 0,
zIndex: 49,
background: "rgba(0,0,0,0.4)",
}}
onClick={() => setShowSheet(false)}
/>
<div className={styles.bottomSheet} role="dialog">
<button
type="button"
className={styles.sheetItem}
onClick={() => fileInputRef.current?.click()}
>
<span className={styles.sheetIcon} aria-hidden="true">
🖼
</span>
<span>Choose from gallery</span>
</button>
<button
type="button"
className={styles.sheetItem}
onClick={() => setShowSheet(false)}
>
<span className={styles.sheetIcon} aria-hidden="true">
</span>
<span>Cancel</span>
</button>
</div>
</>
)}
</>
);
}