BuildPanel.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import { useState } from "react";
  2. import { api } from "../api/client";
  3. import type { ThemeProp } from "../types";
  4. import styles from "./BuildPanel.module.css";
  5. interface Props {
  6. creativeId: string;
  7. creativeStatus: string;
  8. selectedPlatforms: string[];
  9. theme: Record<string, string>;
  10. themeProps: ThemeProp[];
  11. onBuildComplete: () => void;
  12. }
  13. export default function BuildPanel({
  14. creativeId,
  15. creativeStatus,
  16. selectedPlatforms,
  17. theme,
  18. themeProps,
  19. onBuildComplete,
  20. }: Props) {
  21. const [building, setBuilding] = useState(false);
  22. const [buildId, setBuildId] = useState<string | null>(null);
  23. const [buildError, setBuildError] = useState("");
  24. const canBuild = creativeStatus === "assets_ready" || creativeStatus === "built";
  25. async function handleBuild() {
  26. if (selectedPlatforms.length === 0) {
  27. setBuildError("请至少选择一个目标平台");
  28. return;
  29. }
  30. setBuilding(true);
  31. setBuildError("");
  32. setBuildId(null);
  33. try {
  34. // 合并 theme:用 themeProps 的 default 补全缺失值
  35. const mergedTheme: Record<string, string> = {};
  36. for (const prop of themeProps) {
  37. mergedTheme[prop.key] = theme[prop.key] ?? prop.default;
  38. }
  39. const res = await api.triggerBuild(creativeId, {
  40. platforms: selectedPlatforms,
  41. theme: mergedTheme,
  42. });
  43. const bid = res.data.id;
  44. setBuildId(bid);
  45. // 轮询构建状态
  46. await pollBuildStatus(bid);
  47. onBuildComplete();
  48. } catch (err: any) {
  49. setBuildError(err.message);
  50. } finally {
  51. setBuilding(false);
  52. }
  53. }
  54. async function pollBuildStatus(bid: string): Promise<void> {
  55. return new Promise((resolve, reject) => {
  56. const interval = setInterval(async () => {
  57. try {
  58. const res = await api.getBuildStatus(bid);
  59. if (res.data.status === "completed") {
  60. clearInterval(interval);
  61. resolve();
  62. } else if (res.data.status === "failed") {
  63. clearInterval(interval);
  64. reject(new Error(res.data.errorLog || "构建失败"));
  65. }
  66. } catch (err: any) {
  67. clearInterval(interval);
  68. reject(err);
  69. }
  70. }, 2000);
  71. // 超时 120 秒
  72. setTimeout(() => {
  73. clearInterval(interval);
  74. reject(new Error("构建超时,请刷新查看状态"));
  75. }, 120_000);
  76. });
  77. }
  78. return (
  79. <div className={styles.wrapper}>
  80. <button
  81. onClick={handleBuild}
  82. disabled={!canBuild || building}
  83. className={styles.buildBtn}
  84. >
  85. {building ? (
  86. <>
  87. <span className={styles.spinner} /> 构建中…
  88. </>
  89. ) : (
  90. "🚀 开始构建"
  91. )}
  92. </button>
  93. {!canBuild && (
  94. <p className={styles.hint}>请先上传素材后再构建</p>
  95. )}
  96. {buildError && <p className={styles.error}>构建失败:{buildError}</p>}
  97. {buildId && !building && !buildError && (
  98. <p className={styles.success}>构建完成!请在下方下载产物。</p>
  99. )}
  100. </div>
  101. );
  102. }