feat: 优化图片传输功能,添加发送和接收图片的状态管理

This commit is contained in:
MatrixSeven
2025-08-02 14:55:25 +08:00
parent 91b1ef5972
commit afa94c1e9e
14 changed files with 86 additions and 37 deletions

View File

@@ -250,29 +250,43 @@ build_frontend() {
print_verbose "执行 SSG 构建..."
# 临时移除 API 目录
api_backup_name=""
if [ -d "src/app/api" ]; then
mv src/app/api /tmp/next-api-backup-$(date +%s) 2>/dev/null || true
api_backup_name="next-api-backup-$(date +%s)-$$"
mv src/app/api "/tmp/$api_backup_name" 2>/dev/null || true
print_verbose "API 目录已备份到: /tmp/$api_backup_name"
fi
# 构建
build_success=true
if [ "$VERBOSE" = true ]; then
NEXT_EXPORT=true yarn build
NEXT_EXPORT=true yarn build || build_success=false
else
NEXT_EXPORT=true yarn build > build.log 2>&1
if [ $? -ne 0 ]; then
NEXT_EXPORT=true yarn build > build.log 2>&1 || build_success=false
if [ "$build_success" = false ]; then
print_error "前端构建失败,查看 $FRONTEND_DIR/build.log"
cat build.log
# 恢复 API 目录后再退出
if [ -n "$api_backup_name" ] && [ -d "/tmp/$api_backup_name" ]; then
mv "/tmp/$api_backup_name" src/app/api 2>/dev/null || true
print_verbose "已恢复 API 目录"
fi
exit 1
fi
rm -f build.log
fi
# 恢复 API 目录
api_backup=$(ls /tmp/next-api-backup-* 2>/dev/null | head -1)
if [ -n "$api_backup" ]; then
mv "$api_backup" src/app/api 2>/dev/null || true
if [ -n "$api_backup_name" ] && [ -d "/tmp/$api_backup_name" ]; then
mv "/tmp/$api_backup_name" src/app/api 2>/dev/null || true
print_verbose "已恢复 API 目录"
elif [ -n "$api_backup_name" ]; then
print_warning "API 目录备份丢失,无法恢复: /tmp/$api_backup_name"
fi
# 清理历史备份文件保留最近1小时的
find /tmp -name "next-api-backup-*" -mmin +60 -exec rm -rf {} \; 2>/dev/null || true
cd "$PROJECT_ROOT"
# 验证构建结果
@@ -483,11 +497,22 @@ show_summary() {
error_cleanup() {
print_error "构建过程中发生错误"
# 尝试恢复 API 目录
api_backup=$(ls /tmp/next-api-backup-* 2>/dev/null | head -1)
if [ -n "$api_backup" ] && [ -d "$FRONTEND_DIR" ]; then
mv "$api_backup" "$FRONTEND_DIR/src/app/api" 2>/dev/null || true
print_verbose "已恢复 API 目录"
# 尝试恢复 API 目录 - 查找所有可能的备份
local current_process_backups=$(ls /tmp/next-api-backup-*-$$ 2>/dev/null || true)
local other_backups=$(ls /tmp/next-api-backup-* 2>/dev/null | grep -v "\-$$" | head -1 || true)
# 优先恢复当前进程的备份
if [ -n "$current_process_backups" ]; then
for backup in $current_process_backups; do
if [ -d "$backup" ] && [ -d "$FRONTEND_DIR" ]; then
mv "$backup" "$FRONTEND_DIR/src/app/api" 2>/dev/null || true
print_verbose "已恢复 API 目录: $backup"
break
fi
done
elif [ -n "$other_backups" ] && [ -d "$FRONTEND_DIR" ]; then
mv "$other_backups" "$FRONTEND_DIR/src/app/api" 2>/dev/null || true
print_verbose "已恢复 API 目录: $other_backups"
fi
exit 1

View File

@@ -35,7 +35,8 @@ export default function TextTransfer({
const [isLoading, setIsLoading] = useState(false);
const [isRoomCreated, setIsRoomCreated] = useState(false);
const [connectedUsers, setConnectedUsers] = useState(0);
const [images, setImages] = useState<string[]>([]);
const [sentImages, setSentImages] = useState<string[]>([]); // 发送的图片
const [receivedImages, setReceivedImages] = useState<string[]>([]); // 接收的图片
const [imagePreview, setImagePreview] = useState<string | null>(null); // 图片预览状态
const [previewImage, setPreviewImage] = useState<string | null>(null); // 图片预览弹窗状态
const [hasShownJoinSuccess, setHasShownJoinSuccess] = useState(false); // 防止重复显示加入成功消息
@@ -112,8 +113,15 @@ export default function TextTransfer({
case 'image-send':
// 接收到发送的图片
if (message.payload?.imageData) {
setImages(prev => [...prev, message.payload.imageData]);
showToast('收到新的图片!', 'success');
console.log('接收到图片数据:', message.payload.imageData.substring(0, 100) + '...');
// 验证图片数据格式
if (message.payload.imageData.startsWith('data:image/')) {
setReceivedImages(prev => [...prev, message.payload.imageData]);
showToast('收到新的图片!', 'success');
} else {
console.error('无效的图片数据格式:', message.payload.imageData.substring(0, 50));
showToast('收到的图片格式不正确', 'error');
}
}
break;
@@ -335,6 +343,7 @@ export default function TextTransfer({
// 转为base64质量为0.8
const compressedDataUrl = canvas.toDataURL('image/jpeg', 0.8);
console.log('图片压缩完成,数据长度:', compressedDataUrl.length, '前100字符:', compressedDataUrl.substring(0, 100));
resolve(compressedDataUrl);
} catch (error) {
reject(new Error('图片压缩失败: ' + error));
@@ -370,7 +379,7 @@ export default function TextTransfer({
try {
showToast('正在处理图片...', 'info');
const compressedImageData = await compressImage(file);
setImages(prev => [...prev, compressedImageData]);
setSentImages(prev => [...prev, compressedImageData]);
// 发送图片给其他用户
if (websocket && isConnected) {
@@ -442,8 +451,12 @@ export default function TextTransfer({
<button
onClick={(e) => {
e.stopPropagation();
const index = images.indexOf(src);
downloadImage(src, index);
// 尝试在发送和接收的图片中查找
let index = sentImages.indexOf(src);
if (index === -1) {
index = receivedImages.indexOf(src);
}
downloadImage(src, index >= 0 ? index : 0);
}}
className="bg-white/20 hover:bg-white/30 backdrop-blur-sm text-white p-2 rounded-lg shadow-lg transition-all hover:scale-105"
title="下载图片"
@@ -619,15 +632,15 @@ export default function TextTransfer({
</div>
)}
{/* 图片展示区域 */}
{images.length > 0 && (
{/* 发送方显示已发送的图片 */}
{mode === 'send' && sentImages.length > 0 && (
<div className="mt-6">
<h3 className="text-lg font-medium text-slate-800 mb-3 flex items-center">
<Image className="w-5 h-5 mr-2" />
({images.length})
({sentImages.length})
</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{images.map((img, index) => (
{sentImages.map((img, index) => (
<div key={index} className="relative group overflow-hidden">
<img
src={img}
@@ -770,21 +783,32 @@ export default function TextTransfer({
</div>
)}
{/* 接收到的图片展示 */}
{images.length > 0 && (
{/* 接收方显示接收到的图片 */}
{mode === 'receive' && receivedImages.length > 0 && (
<div className="mt-6">
<h3 className="text-lg font-medium text-slate-800 mb-3 flex items-center">
<Image className="w-5 h-5 mr-2" />
({images.length})
({receivedImages.length})
</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{images.map((img, index) => (
{receivedImages.map((img, index) => (
<div key={index} className="relative group overflow-hidden">
<img
src={img}
alt={`图片 ${index + 1}`}
className="w-full h-24 object-cover rounded-lg border-2 border-slate-200 hover:border-emerald-400 transition-all duration-200 cursor-pointer bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50"
onClick={() => setPreviewImage(img)}
onLoad={(e) => {
console.log(`图片 ${index + 1} 加载成功`);
}}
onError={(e) => {
console.error(`图片 ${index + 1} 加载失败:`, img.substring(0, 100));
e.currentTarget.style.backgroundColor = '#f1f5f9';
e.currentTarget.style.display = 'flex';
e.currentTarget.style.alignItems = 'center';
e.currentTarget.style.justifyContent = 'center';
e.currentTarget.innerHTML = `<span style="color: #64748b; font-size: 12px;">图片加载失败</span>`;
}}
/>
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-opacity rounded-lg"></div>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
2:I[6801,["177","static/chunks/app/layout-d0f2a5cbcfca20f5.js"],"ToastProvider"]
3:I[7555,[],""]
4:I[1295,[],""]
5:I[5055,["660","static/chunks/660-236c74c3e8221aec.js","974","static/chunks/app/page-28117649abd2bba1.js"],"default"]
5:I[5055,["984","static/chunks/984-39bc34483f07a61c.js","974","static/chunks/app/page-f438e315cafde810.js"],"default"]
6:I[9665,[],"OutletBoundary"]
8:I[4911,[],"AsyncMetadataOutlet"]
a:I[9665,[],"ViewportBoundary"]
@@ -11,8 +11,8 @@ d:"$Sreact.suspense"
f:I[8393,[],""]
:HL["/_next/static/media/569ce4b8f30dc480-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
:HL["/_next/static/media/93f479601ee12b01-s.p.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
:HL["/_next/static/css/e6f72b7992dea2ee.css","style"]
0:{"P":null,"b":"C3SLXyByF2YDtLzyWwMjW","p":"","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/e6f72b7992dea2ee.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"zh-CN","children":["$","body",null,{"className":"__variable_5cfdac __variable_9a8899 antialiased","children":["$","$L2",null,{"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L5",null,{}],null,["$","$L6",null,{"children":["$L7",["$","$L8",null,{"promise":"$@9"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$La",null,{"children":"$Lb"}],["$","meta",null,{"name":"next-size-adjust","content":""}]],["$","$Lc",null,{"children":["$","div",null,{"hidden":true,"children":["$","$d",null,{"fallback":null,"children":"$Le"}]}]}]]}],false]],"m":"$undefined","G":["$f",[]],"s":false,"S":true}
:HL["/_next/static/css/7908b4c934e87974.css","style"]
0:{"P":null,"b":"dIaE9ChLg6gKLX3iG0ErS","p":"","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/7908b4c934e87974.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"zh-CN","children":["$","body",null,{"className":"__variable_5cfdac __variable_9a8899 antialiased","children":["$","$L2",null,{"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[["$","$L5",null,{}],null,["$","$L6",null,{"children":["$L7",["$","$L8",null,{"promise":"$@9"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$La",null,{"children":"$Lb"}],["$","meta",null,{"name":"next-size-adjust","content":""}]],["$","$Lc",null,{"children":["$","div",null,{"hidden":true,"children":["$","$d",null,{"fallback":null,"children":"$Le"}]}]}]]}],false]],"m":"$undefined","G":["$f",[]],"s":false,"S":true}
b:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
7:null
10:I[8175,[],"IconMark"]