PreviewPanel.tsx 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import { useState, useEffect } from "react";
  2. import { api } from "../api/client";
  3. import styles from "./PreviewPanel.module.css";
  4. interface Props {
  5. creativeId: string;
  6. creativeStatus: string;
  7. theme: Record<string, string>;
  8. }
  9. export default function PreviewPanel({ creativeId, creativeStatus, theme }: Props) {
  10. const [previewUrl, setPreviewUrl] = useState<string | null>(null);
  11. const [loading, setLoading] = useState(false);
  12. const [error, setError] = useState("");
  13. const [loaded, setLoaded] = useState(false);
  14. const canPreview = creativeStatus === "assets_ready" || creativeStatus === "built" || creativeStatus === "building";
  15. // 预览启动后 5s 超时
  16. useEffect(() => {
  17. if (!previewUrl || loaded) return;
  18. const timer = setTimeout(() => {
  19. if (!loaded) setError("预览加载超时,请检查控制台或重试");
  20. }, 10000);
  21. return () => clearTimeout(timer);
  22. }, [previewUrl, loaded]);
  23. // 退出页面时停止预览
  24. useEffect(() => {
  25. return () => {
  26. if (previewUrl) {
  27. api.stopPreview(creativeId).catch(() => {});
  28. }
  29. };
  30. }, []); // eslint-disable-line react-hooks/exhaustive-deps
  31. async function handleStart() {
  32. setLoading(true);
  33. setError("");
  34. setLoaded(false);
  35. try {
  36. const res = await api.startPreview(creativeId, theme);
  37. setPreviewUrl(res.data.url);
  38. } catch (err: any) {
  39. setError(err.message);
  40. } finally {
  41. setLoading(false);
  42. }
  43. }
  44. async function handleStop() {
  45. try {
  46. await api.stopPreview(creativeId);
  47. } catch {}
  48. setPreviewUrl(null);
  49. setLoaded(false);
  50. setError("");
  51. }
  52. return (
  53. <div className={styles.wrapper}>
  54. <div className={styles.header}>
  55. <h3 className={styles.title}>实时预览</h3>
  56. {!previewUrl ? (
  57. <button
  58. onClick={handleStart}
  59. disabled={!canPreview || loading}
  60. className={styles.startBtn}
  61. >
  62. {loading ? "启动中…" : "▶ 开始预览"}
  63. </button>
  64. ) : (
  65. <button onClick={handleStop} className={styles.stopBtn}>
  66. 关闭预览
  67. </button>
  68. )}
  69. </div>
  70. {!canPreview && !previewUrl && (
  71. <p className={styles.hint}>上传素材后即可预览</p>
  72. )}
  73. {error && <p className={styles.error}>{error}</p>}
  74. {loading && (
  75. <div className={styles.loadingBox}>
  76. <span className={styles.spinner} />
  77. <span>Vite 开发服务器启动中,请稍候…</span>
  78. </div>
  79. )}
  80. {previewUrl && (
  81. <div className={styles.frameWrap}>
  82. <iframe
  83. src={previewUrl}
  84. className={styles.frame}
  85. title="广告预览"
  86. onLoad={() => setLoaded(true)}
  87. />
  88. </div>
  89. )}
  90. </div>
  91. );
  92. }