8.4 KiB
رفع مشکل عکس نادرست در پوستر دانلودی صفحه پزشک (اشتراکگذاری)
پروژه
nobat724_front
زمینه
در صفحهی عمومی پزشک (/doctor/[slug]) یک دکمهی «اشتراک گذاری» هست که یک Modal باز میکند. داخل Modal گزینهی «دانلود» یک پوستر PDF میسازد. پوستر شامل عکس پروفایل پزشک، QR کد، لوگو و اطلاعات پزشک است.
مسیر تست:
http://localhost:3000/doctor/f58d3dba-6ce0-4bd4-85d9-9d787a0b9324 → «اشتراک گذاری» → «دانلود»
مشکل / هدف
وقتی روی «دانلود» کلیک میشود، پوستر PDF دانلود میشود ولی عکس آن درست نیست — عکس پروفایل پزشک و/یا QR کد در فایل خروجی خالی/خراب/ناقص است.
علت ریشهای: handleDownloadPDF بلافاصله html2canvas را صدا میزند بدون اینکه منتظر بارگذاری کامل تصاویر بماند:
- عکس پروفایل با
next/image(<Image>) رندر میشود که از دامنهی remote (api.clinic-pro.ir) از طریق optimizer آدرس/_next/image?url=...میآید و lazy load دارد؛ ممکن است هنگام capture هنوز paint نشده باشد. - QR کد بهصورت async در
useEffectتولید میشود (QRCode.toDataURL(...).then(setQrCodeData))؛ اگر کاربر سریع دانلود بزند، بهجای QR متن «در حال بارگذاری...» در پوستر میافتد. - هیچ await برای اتمام لود شدن
<img>ها قبل از snapshot وجود ندارد.
فایلهای مرتبط
| فایل | نقش |
|---|---|
components/doctor/detailDoctor/Share.js |
Modal اشتراکگذاری؛ Poster را در یک div مخفی (hiddenRef) رندر میکند |
components/doctor/detailDoctor/List.js |
آیکونهای اشتراک + handleDownloadPDF (منطق html2canvas + jsPDF) — هستهی باگ |
components/doctor/poster/index.js |
چیدمان پوستر |
components/doctor/poster/ImageProfile.js |
عکس پروفایل با next/image |
components/doctor/poster/QrImg.js |
تولید async QR کد |
helper/index.js |
imageUrl(url, fallback) |
وضعیت فعلی
components/doctor/detailDoctor/List.js (بخش دانلود)
const handleDownloadPDF = async () => {
const element = hiddenRef.current;
if (!element) return;
const canvas = await html2canvas(element, {
backgroundColor: "#ffffff",
useCORS: true,
scale: 2,
});
const imgData = canvas.toDataURL("image/png");
// ... jsPDF ...
pdf.save("poster.pdf");
};
components/doctor/poster/ImageProfile.js
import Image from "next/image";
import { imageUrl } from "@/helper";
function ImageProfile({ data }) {
return (
<div className="bg-stroke-circle ml-[10px] mt-[57px] p-[9px] rounded-full overflow-hidden">
<Image
src={imageUrl(data?.img?.[0]?.url)}
width={173}
height={173}
alt="profile-user"
className="rounded-full"
/>
</div>
);
}
components/doctor/poster/QrImg.js (خلاصه)
useEffect(() => {
const info = getStateInfoClient();
setHost(info.host);
if (info.fullUrl) QRCode.toDataURL(info.fullUrl).then(setQrCodeData);
}, []);
// ...
{qrCodeData ? <img src={qrCodeData} .../> : <p>در حال بارگذاری...</p>}
وظایف
۱. منتظر بارگذاری کامل همهی <img>ها بمان قبل از html2canvas
در List.js یک helper اضافه کن که همهی <img>های داخل element را پیدا کند و تا complete شدن (و در صورت امکان decode()) منتظر بماند. سپس در handleDownloadPDF قبل از html2canvas آن را await کن.
const waitForImages = async (element) => {
const imgs = Array.from(element.querySelectorAll("img"));
await Promise.all(
imgs.map((img) => {
if (img.complete && img.naturalWidth !== 0) {
return img.decode?.().catch(() => {}) ?? Promise.resolve();
}
return new Promise((resolve) => {
img.onload = () => resolve();
img.onerror = () => resolve(); // خراب بودن یک عکس نباید کل دانلود را بلاک کند
});
})
);
};
در handleDownloadPDF:
const element = hiddenRef.current;
if (!element) return;
await waitForImages(element);
const canvas = await html2canvas(element, {
backgroundColor: "#ffffff",
useCORS: true,
scale: 2,
imageTimeout: 15000,
});
۲. اطمینان از آماده بودن QR قبل از دانلود
اگر QR هنوز تولید نشده باشد، پوستر متن «در حال بارگذاری...» را نشان میدهد. راهحل ساده و مطمئن: دکمهی دانلود تا آماده شدن QR غیرفعال/در حالت لودینگ باشد.
- در
QrImg.jsیک callback مثلonReadyبپذیر و بعد ازsetQrCodeDataصدا بزن؛ این وضعیت را تاShare.jsبالا ببر و بهList.jsبده، یا - سادهتر: در
handleDownloadPDFبعد ازwaitForImages، اگر داخلelementعنصری با متن «در حال بارگذاری...» بود، چند صد میلیثانیه polling کوتاه انجام بده تاimgمربوط به QR ظاهر شود (سقف زمانی بگذار).
رویکرد اول (state واقعی) تمیزتر است؛ آن را ترجیح بده.
۳. سازگاری عکس پروفایل با html2canvas
عکس پروفایل با next/image از طریق /_next/image (same-origin) سرو میشود، پس مشکل CORS نباید داشته باشد و نیازی به تبدیل به <img> خام نیست. اگر بعد از وظیفهی ۱ باز هم عکس پروفایل در خروجی خالی بود:
- fallback
imageUrlمقدار/assets/images/user.png(local) را برمیگرداند که همیشه قابل رندر است — این را نگهدار. - در صورت نیاز
priorityیاunoptimizedبه<Image>اضافه کن تا lazy load حذف شود و از همان ابتدا لود شود:
<Image
src={imageUrl(data?.img?.[0]?.url)}
width={173}
height={173}
alt="profile-user"
className="rounded-full"
priority
unoptimized
/>
unoptimizedباعث میشود آدرس مستقیم عکس (بدون/_next/image) رندر شود؛ اگر دامنهی remote هدر CORS نفرستد ممکن است canvas آلوده شود. اگر این حالت را انتخاب کردی حتماً خروجی را تست کن؛ در غیر این صورت فقطpriorityکافی است.
۴. UX دکمهی دانلود
- هنگام تولید پوستر، دکمهی «دانلود» را در حالت loading/disabled بگذار تا کلیک دوباره منجر به دانلودهای موازی نشود.
- در صورت throw شدن
html2canvas، باtoast.error(...)(فارسی) خطا بده و state را ریست کن.react-toastifyاز قبل import شده.
نکات مهم
- Poster در
Share.jsداخل یک div باopacity-0 pointer-events-none -z-10ولی با ابعاد ثابت600x700و درtop-0 left-0رندر میشود (یعنی داخل viewport است و لود میشود). این ساختار را نگهدار — html2canvas به عنصر رندرشدهی واقعی نیاز دارد؛display:noneنکن. - نسخهها:
html2canvas@^1.4.1،jspdf@^3.0.4. Tailwind نسخهی ۳ است (رنگها rgb هستند، مشکل oklch نداریم). - کلاسهای
bg-posterوbg-stroke-circleدرglobals.cssاز تصاویر local (public/assets/images/...) بهعنوانbackground-imageاستفاده میکنند؛ html2canvas background-image را رندر میکند و چون local هستند مشکلی ندارند. - همهی رشتههای جدید فارسی باشند.
- بعد از تغییر، سناریو را واقعی تست کن: باز کردن Modal، کلیک روی دانلود، و باز کردن PDF خروجی برای تأیید حضور عکس پروفایل + QR + لوگو.