全新popiart网站 react项目重构
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

370 lines
21 KiB

import { Message } from "@arco-design/web-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { createCharacter } from "@/api/api";
import AvatarCropModal from "@/components/AvatarCropModal";
import { useCopyText } from "@/hooks/useCopyText";
import "./index.scss";
import { ROUTES } from "@/constants";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const AGREEMENT_CHECKBOX_OFF_ICON = new URL("/src/assets/images/ic_checkbox_agreement_off.png", import.meta.url).href;
const AGREEMENT_CHECKBOX_ON_ICON = new URL("/src/assets/images/ic_checkbox_agreement_on.png", import.meta.url).href;
type CreateRoleModalProps = {
visible: boolean;
taskList: any[];
initialTaskId?: string | null;
onClose: () => void;
onCreated?: (taskId: string) => void;
};
function formatCreateDate(task: any): string {
const raw = task?.createTime;
if (raw == null || raw === "") return "";
const numeric = Number(raw);
const millis = Number.isFinite(numeric) ? (String(raw).length === 10 ? numeric * 1000 : numeric) : Number.NaN;
const date = Number.isFinite(millis) ? new Date(millis) : new Date(String(raw));
if (Number.isNaN(date.getTime())) return "";
const year = date.getFullYear();
const month = `${date.getMonth() + 1}`.padStart(2, "0");
const day = `${date.getDate()}`.padStart(2, "0");
const hour = `${date.getHours()}`.padStart(2, "0");
const minute = `${date.getMinutes()}`.padStart(2, "0");
return `${year}-${month}-${day} ${hour}:${minute}`;
}
export default function CreateRoleModal({ visible, taskList, initialTaskId, onClose, onCreated }: CreateRoleModalProps) {
const { t } = useTranslation();
const navigate = useNavigate();
const [activeTaskItem, setActiveTaskItem] = useState<any>(null);
const [avatar, setAvatar] = useState(activeTaskItem?.resultList?.[0]?.images?.[0] ?? "");
const [avatarOriginImage, setAvatarOriginImage] = useState(activeTaskItem?.resultList?.[0]?.images?.[0] ?? "");
const [cropModalImage, setCropModalImage] = useState("");
const [characterName, setCharacterName] = useState("");
const [description, setDescription] = useState("");
const [isPublic, setIsPublic] = useState(false);
const [agreementChecked, setAgreementChecked] = useState(false);
const [creating, setCreating] = useState(false);
const thumbListRef = useRef<HTMLDivElement>(null);
const copyText = useCopyText();
const userCreatedCharacter = activeTaskItem?.userCreatedCharacter;
const hasCreatedCharacter = Number(userCreatedCharacter?.id) > 0;
useEffect(() => {
if (visible) return;
setActiveTaskItem(null);
setAvatar("");
setAvatarOriginImage("");
setCropModalImage("");
setCharacterName("");
setDescription("");
setIsPublic(false);
setAgreementChecked(false);
setCreating(false);
}, [visible]);
useEffect(() => {
if (!visible) return;
if (initialTaskId) {
const selectedTask = taskList.find((task) => String(task.id) === String(initialTaskId));
if (selectedTask) {
setActiveTaskItem(selectedTask);
return;
}
}
setActiveTaskItem(taskList[0] ?? null);
}, [visible, initialTaskId, taskList]);
useEffect(() => {
if (!visible) return;
if (!activeTaskItem) return;
const existed = taskList.some((task) => String(task.id) === String(activeTaskItem.id));
if (!existed) {
setActiveTaskItem(taskList[0] ?? null);
return;
}
const nextTask = taskList.find((task) => String(task.id) === String(activeTaskItem.id));
if (nextTask && nextTask !== activeTaskItem) {
setActiveTaskItem(nextTask);
}
}, [visible, taskList, activeTaskItem]);
useEffect(() => {
const nextOriginImage = hasCreatedCharacter
? (userCreatedCharacter?.avatar ?? "")
: (activeTaskItem?.resultList?.[0]?.images?.[0] ?? "");
setCropModalImage("");
setAvatarOriginImage(nextOriginImage);
setAvatar(nextOriginImage);
setCharacterName(hasCreatedCharacter ? (userCreatedCharacter?.characterName ?? "") : "");
setDescription(hasCreatedCharacter ? (userCreatedCharacter?.description ?? "") : "");
}, [activeTaskItem, hasCreatedCharacter, userCreatedCharacter?.avatar, userCreatedCharacter?.characterName, userCreatedCharacter?.description]);
const openCropModal = useCallback(() => {
if (!avatarOriginImage) return;
const rawAvatarOriginImage = avatarOriginImage.split("#")[0];
setCropModalImage(`${rawAvatarOriginImage}#crop=${Date.now()}`);
}, [avatarOriginImage]);
const handleCreateCharacter = useCallback(async () => {
if (hasCreatedCharacter) {
Message.warning(t("pages.charactersCreate.createRoleModal.alreadyCreated"));
return;
}
if (!avatar) {
Message.warning(t("pages.charactersCreate.createRoleModal.uploadAvatarFirst"));
return;
}
if (!characterName.trim()) {
Message.warning(t("pages.charactersCreate.createRoleModal.inputName"));
return;
}
if (!description.trim()) {
Message.warning(t("pages.charactersCreate.createRoleModal.inputDescription"));
return;
}
if (!agreementChecked) {
Message.warning(t("pages.charactersCreate.createRoleModal.agreementRequired"));
return;
}
if (!activeTaskItem?.id) {
Message.warning(t("pages.charactersCreate.createRoleModal.taskNotFound"));
return;
}
setCreating(true);
try {
await createCharacter({
taskId: activeTaskItem.id,
avatar,
characterName,
description,
isPublic,
});
Message.success(t("pages.charactersCreate.createRoleModal.createSuccess"));
onCreated?.(String(activeTaskItem.id));
onClose();
navigate(ROUTES.CHARACTERS);
} catch {
Message.error(t("pages.charactersCreate.createRoleModal.createFailed"));
} finally {
setCreating(false);
}
}, [activeTaskItem?.id, agreementChecked, avatar, characterName, description, hasCreatedCharacter, isPublic, navigate, onClose, onCreated, t]);
if (!visible) return null;
const handleScrollThumbList = (direction: "left" | "right") => {
const container = thumbListRef.current;
if (!container) return;
const offset = direction === "left" ? -121 : 121;
container.scrollBy({ left: offset, behavior: "smooth" });
};
return (
<div className="charactersCreateRoleModal">
<div className="charactersCreateRoleModal__mask" onClick={onClose} />
<div className="charactersCreateRoleModal__container" role="dialog" aria-modal="true" aria-label={t("pages.charactersCreate.createRoleModal.dialogAria")}>
<section className="charactersCreateRoleModal__previewSection">
<div className="charactersCreateRoleModal__previewMain">
{activeTaskItem?.resultList?.[0]?.images?.[0] ? <img alt="" className="charactersCreateRoleModal__previewMainImage" src={activeTaskItem.resultList[0].images[0]} /> : null}
</div>
<div className="charactersCreateRoleModal__thumbSection">
<button
type="button"
className="charactersCreateRoleModal__thumbArrow "
onClick={() => handleScrollThumbList("left")}
style={{ transform: "rotate(180deg)" }}
>
<img alt="" src={new URL("/src/assets/images/ic_arrow_right_gray.png", import.meta.url).href} />
</button>
<div ref={thumbListRef} className="charactersCreateRoleModal__thumbList">
{taskList.map((task, index) => {
const taskId = String(task.id ?? index);
const thumbSrc = task.resultList[0]?.images?.[0] ?? "";
const isActive = String(task.id) === String(activeTaskItem?.id);
return (
<button
key={taskId}
type="button"
className={`charactersCreateRoleModal__thumbItem${isActive ? " charactersCreateRoleModal__thumbItem_active" : ""}`}
onClick={() => {
setActiveTaskItem(task);
}}
>
{thumbSrc ? <img alt="" className="charactersCreateRoleModal__thumbImage" src={thumbSrc} /> : null}
{!isActive ? <span className="charactersCreateRoleModal__thumbMask" /> : null}
</button>
);
})}
</div>
<button
type="button"
className="charactersCreateRoleModal__thumbArrow "
onClick={() => handleScrollThumbList("right")}
>
<img alt="" src={new URL("/src/assets/images/ic_arrow_right_gray.png", import.meta.url).href} />
</button>
</div>
</section>
<section className="charactersCreateRoleModal__formSection">
<button type="button" className="charactersCreateRoleModal__close" aria-label={t("button.close")} onClick={onClose}>
<img alt="" src={new URL("/src/assets/images/ic_close.png", import.meta.url).href} />
</button>
<div className="charactersCreateRoleModal__formScrollArea">
<div className="charactersCreateRoleModal__avatarAdd">
{
avatar ?
<button type="button" className="charactersCreateRoleModal__preview" >
<img alt="" className="charactersCreateRoleModal__previewImage" src={avatar} />
</button>
:
<img alt="" className="charactersCreateRoleModal__avatarAddImage" src={new URL("/src/assets/images/ic_avatar_add.png", import.meta.url).href} />
}
</div>
<div className="charactersCreateRoleModal__avatarUpdate">
<button type="button" className="charactersCreateRoleModal__avatarUpdateBtn" onClick={openCropModal} disabled={hasCreatedCharacter}>
</button>
</div>
<div className="charactersCreateRoleModal__formBody">
<div className="charactersCreateRoleModal__fieldRow">
<label className="charactersCreateRoleModal__fieldLabel">{t("character_detail.name_label")}</label>
<input
className="charactersCreateRoleModal__nameInput"
value={characterName}
onChange={(event) => setCharacterName(event.target.value)}
placeholder={t("pages.charactersCreate.createRoleModal.required")}
disabled={hasCreatedCharacter}
/>
</div>
<div className="charactersCreateRoleModal__fieldBlock">
<label className="charactersCreateRoleModal__fieldLabel">{t("pages.charactersCreate.createRoleModal.descriptionLabel")}</label>
<textarea
className="charactersCreateRoleModal__introInput"
value={description}
onChange={(event) => setDescription(event.target.value)}
placeholder={t("pages.charactersCreate.createRoleModal.descriptionPlaceholder")}
disabled={hasCreatedCharacter}
/>
</div>
<div className="charactersCreateRoleModal__toggleRow">
<span className="charactersCreateRoleModal__fieldLabel">{t("pages.charactersCreate.createRoleModal.publicDisplay")}</span>
<button
type="button"
className={`charactersCreateRoleModal__toggle${isPublic ? " charactersCreateRoleModal__toggle_on" : ""}`}
onClick={() => setIsPublic((prev) => !prev)}
aria-label={t("pages.charactersCreate.createRoleModal.publicDisplaySwitchAria")}
aria-pressed={isPublic}
>
<span className="charactersCreateRoleModal__toggleKnob" />
<span className="charactersCreateRoleModal__toggleText">{isPublic ? "ON" : "OFF"}</span>
</button>
</div>
<div className="charactersCreateRoleModal__infoGroup">
<div className="charactersCreateRoleModal__promptBlock">
<span className="charactersCreateRoleModal__fieldLabel">{t("pages.assetDetails.promptLabel")}</span>
<p className="charactersCreateRoleModal__promptText">
{activeTaskItem?.chatPrompt ?? ""}
</p>
<img
alt=""
className="charactersCreateRoleModal__promptCopyIcon"
src={new URL("/src/assets/images/ic_copy.png", import.meta.url).href}
onClick={() => void copyText(activeTaskItem?.chatPrompt)}
/>
</div>
<div className="charactersCreateRoleModal__materialsBlock">
<span className="charactersCreateRoleModal__fieldLabel">{t("pages.assetDetails.useMaterial")}</span>
{/* <div className="charactersCreateRoleModal__materialsList">
{
activeTaskItem?.characterList?.map((character: any, index: number) => (
<div key={`${character.id}-${index}`} className="charactersCreateRoleModal__materialItem">
<img alt="" className="charactersCreateRoleModal__materialImage" src={character.threeViewImage} />
</div>
))
}
{activeTaskItem?.images?.map((image: string, index: number) => (
<div key={`${image}-${index}`} className="charactersCreateRoleModal__materialItem">
<img alt="" className="charactersCreateRoleModal__materialImage" src={image} />
</div>
))}
</div> */}
</div>
<div className="charactersCreateRoleModal__metaRow">
<span className="charactersCreateRoleModal__metaLabel">{t("pages.assetDetails.model")}</span>
<span className="charactersCreateRoleModal__metaValue">{activeTaskItem?.aiModelName ?? ""}</span>
</div>
<div className="charactersCreateRoleModal__metaRow">
<span className="charactersCreateRoleModal__metaLabel">{t("pages.assetDetails.ratio")}</span>
<span className="charactersCreateRoleModal__metaValue">{activeTaskItem?.aspectRatio ?? ""}</span>
</div>
<div className="charactersCreateRoleModal__metaRow">
<span className="charactersCreateRoleModal__metaLabel">{t("pages.assetDetails.size")}</span>
<span className="charactersCreateRoleModal__metaValue">{activeTaskItem?.width && activeTaskItem?.height ? `${activeTaskItem.width}x${activeTaskItem.height}` : ""}</span>
</div>
<div className="charactersCreateRoleModal__metaRow">
<span className="charactersCreateRoleModal__metaLabel">{t("pages.charactersCreate.createRoleModal.createDate")}</span>
<span className="charactersCreateRoleModal__metaValue">{formatCreateDate(activeTaskItem)}</span>
</div>
</div>
</div>
</div>
<div className="charactersCreateRoleModal__footer">
<div className="charactersCreateRoleModal__agreement">
<button
type="button"
className="charactersCreateRoleModal__agreementCheckbox"
aria-label={t("pages.charactersCreate.createRoleModal.agreeTermsAria")}
aria-checked={agreementChecked}
onClick={() => setAgreementChecked((prev) => !prev)}
>
<img
alt=""
className="charactersCreateRoleModal__agreementCheckboxIcon"
src={agreementChecked ? AGREEMENT_CHECKBOX_ON_ICON : AGREEMENT_CHECKBOX_OFF_ICON}
/>
</button>
<p className="charactersCreateRoleModal__agreementText">
{t("pages.charactersCreate.createRoleModal.uploadMeansAgree")}
<a href={ROUTES.LEGAL + "#user-agreement"} target="_blank" rel="noreferrer">{t("app.user_terms")}</a>
{t("auth.agreement_sub2")}
<a href={ROUTES.LEGAL + "#privacy-policy"} target="_blank" rel="noreferrer">{t("app.privacy_policy")}</a>
</p>
</div>
<button
type="button"
className="charactersCreateRoleModal__submitBtn"
onClick={() => {
if (hasCreatedCharacter) {
Message.warning(t("pages.charactersCreate.createRoleModal.alreadyCreated"));
return;
}
void handleCreateCharacter();
}}
disabled={creating}
aria-disabled={hasCreatedCharacter || creating}
style={hasCreatedCharacter ? { opacity: 0.5, cursor: "not-allowed" } : undefined}
>
{t("pages.charactersCreate.createRole")}
</button>
</div>
</section>
</div>
<AvatarCropModal
image={cropModalImage}
onUploaded={(uploadedAvatarUrl) => {
setAvatar(uploadedAvatarUrl);
setCropModalImage("");
}}
onClose={() => setCropModalImage("")}
/>
</div>
);
}