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.
593 lines
25 KiB
593 lines
25 KiB
import { useCopyText } from "@/hooks/useCopyText";
|
|
import "./index.scss";
|
|
import { addCharacterLike, addUserFollow, delCharacterLike, deleteCharacter, deleteUserFollow, getCharacterDetail, updateCharacter } from "@/api/api";
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Dropdown, Input, Message, Modal } from "@arco-design/web-react";
|
|
import AvatarCropModal from "@/components/AvatarCropModal";
|
|
import { formatLocalDate } from "@/utils";
|
|
import { useUserStore } from "@/store/useUserStore";
|
|
import { useGlobalPromptImageStore } from "@/store/useMakeSameStore";
|
|
|
|
|
|
function formatTaskCreateDate(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 interface CharacterDetailDialogProps {
|
|
id: string | number;
|
|
onClose?: () => void;
|
|
onUseCharacter?: () => void;
|
|
onDeleted?: (id: string | number) => void;
|
|
onUpdated?: (payload: {
|
|
id: string | number;
|
|
title: string;
|
|
description: string;
|
|
isPublic: boolean;
|
|
avatar: string;
|
|
}) => void;
|
|
}
|
|
|
|
export default function CharacterDetailDialog({
|
|
id: characterId,
|
|
onClose,
|
|
onUseCharacter,
|
|
onDeleted,
|
|
onUpdated,
|
|
}: CharacterDetailDialogProps) {
|
|
const { t } = useTranslation();
|
|
const user: any = useUserStore((s) => s.user);
|
|
const [railMenuOpen, setRailMenuOpen] = useState(false);
|
|
const [isEditingCharacterName, setIsEditingCharacterName] = useState(false);
|
|
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
|
const [isSavingCharacterName, setIsSavingCharacterName] = useState(false);
|
|
const [isSavingDescription, setIsSavingDescription] = useState(false);
|
|
const [characterName, setCharacterName] = useState("");
|
|
const [description, setDescription] = useState("");
|
|
const [isPublic, setIsPublic] = useState(false);
|
|
const [cropModalImage, setCropModalImage] = useState("");
|
|
const [characterDetail, setCharacterDetail] = useState<any>({});
|
|
const skipNextNameBlurRef = useRef(false);
|
|
const skipNextIntroBlurRef = useRef(false);
|
|
const requestAppendImageUrl = useGlobalPromptImageStore((s) => s.requestAppendImageUrl);
|
|
const placeCaretAtEnd = useCallback((eventTarget: EventTarget | null) => {
|
|
if (!(eventTarget instanceof HTMLInputElement) && !(eventTarget instanceof HTMLTextAreaElement)) return;
|
|
const target = eventTarget;
|
|
const valueLength = target.value.length;
|
|
requestAnimationFrame(() => {
|
|
try {
|
|
target.setSelectionRange(valueLength, valueLength);
|
|
} catch {
|
|
// Some input modes may not support selection APIs.
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
// 是否是自己的角色
|
|
const isOwnCharacter = useMemo(() => {
|
|
const uploadUserId = characterDetail?.uploadUserId;
|
|
const currentUserId = user?.id;
|
|
if (uploadUserId == null || currentUserId == null) return false;
|
|
return String(uploadUserId) === String(currentUserId);
|
|
}, [characterDetail?.uploadUserId, user?.id]);
|
|
// 是否用户上传角色(非官方、用户自创)
|
|
const isUserUploadCharacter = useMemo(() => {
|
|
return characterDetail?.characterType === 2;
|
|
}, [characterDetail?.characterType]);
|
|
const characterNameInputWidth = useMemo(() => {
|
|
const textLength = Array.from(characterName || "").length;
|
|
const computedWidth = textLength * 14 + 28;
|
|
return Math.min(200, Math.max(69, computedWidth));
|
|
}, [characterName]);
|
|
|
|
useEffect(() => {
|
|
if (characterId == null || characterId === "") return;
|
|
let cancelled = false;
|
|
void getCharacterDetail(characterId)
|
|
.then((res: any) => {
|
|
if (cancelled) return;
|
|
const detail = res?.data ?? {};
|
|
setCharacterDetail(detail);
|
|
setCharacterName(detail?.title ?? "");
|
|
setDescription(detail?.description ?? "");
|
|
setIsPublic(Boolean(detail?.isPublic));
|
|
})
|
|
.catch(() => {
|
|
if (cancelled) return;
|
|
setCharacterDetail({});
|
|
setCharacterName("");
|
|
setDescription("");
|
|
setIsPublic(false);
|
|
});
|
|
setIsEditingCharacterName(false);
|
|
setIsEditingDescription(false);
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [characterId]);
|
|
|
|
const copyText = useCopyText();
|
|
|
|
const addFollow = (userId: any) => {
|
|
void addUserFollow(userId).then((res: any) => {
|
|
if (res?.data) {
|
|
setCharacterDetail((prev: any) => ({
|
|
...prev,
|
|
userInfo: { ...prev.userInfo, userFollowed: true },
|
|
}));
|
|
}
|
|
});
|
|
};
|
|
|
|
const delFollow = (userId: any) => {
|
|
void deleteUserFollow(userId).then((res: any) => {
|
|
if (res?.data) {
|
|
setCharacterDetail((prev: any) => ({
|
|
...prev,
|
|
userInfo: { ...prev.userInfo, userFollowed: false },
|
|
}));
|
|
}
|
|
});
|
|
};
|
|
|
|
// 点赞
|
|
const toggleLike = () => {
|
|
|
|
if (!characterDetail.userLiked) {
|
|
|
|
void addCharacterLike(characterDetail.id).then((res: any) => {
|
|
if (res?.data) {
|
|
setCharacterDetail((prev: any) => ({
|
|
...prev,
|
|
userLiked: true,
|
|
likeNum: res?.data.likeNum,
|
|
}));
|
|
}
|
|
});
|
|
} else {
|
|
void delCharacterLike(characterDetail.id).then((res: any) => {
|
|
if (res?.data) {
|
|
setCharacterDetail((prev: any) => ({
|
|
...prev,
|
|
userLiked: false,
|
|
likeNum: res?.data.likeNum,
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
};
|
|
// 下载
|
|
const handleDownload = () => {
|
|
window.open(characterDetail.threeViewImage, '_blank');
|
|
};
|
|
|
|
const handleDeleteCharacter = () => {
|
|
const id = characterDetail?.id ?? characterId;
|
|
if (id == null || id === "") {
|
|
Message.error(t("pages.charactersCreate.roleIdMissing"));
|
|
return;
|
|
}
|
|
if (!isUserUploadCharacter || !isOwnCharacter) return;
|
|
Modal.confirm({
|
|
title: t("pages.charactersCreate.confirmDeleteTitle"),
|
|
content: t("pages.charactersCreate.confirmDeleteContent"),
|
|
okText: t("pages.assetDetails.delete"),
|
|
cancelText: t("button.cancel"),
|
|
okButtonProps: { status: "danger" },
|
|
onOk: async () => {
|
|
try {
|
|
await deleteCharacter(id);
|
|
Message.success(t("pages.assetDetails.deleteSuccess"));
|
|
onDeleted?.(id);
|
|
onClose?.();
|
|
} catch {
|
|
Message.error(t("pages.assetDetails.deleteFailed"));
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
// 复制描述
|
|
const handleCopyDescription = () => {
|
|
void copyText(characterDetail?.description);
|
|
};
|
|
|
|
const submitCharacterUpdate = async (overrides?: Partial<{ characterName: string; description: string; isPublic: boolean; avatar: string }>) => {
|
|
const id = characterDetail?.id ?? characterId;
|
|
if (id == null || id === "") return false;
|
|
const payload = {
|
|
id,
|
|
characterName: overrides?.characterName ?? characterDetail?.title ?? "",
|
|
description: overrides?.description ?? characterDetail?.description ?? "",
|
|
isPublic: overrides?.isPublic ?? Boolean(characterDetail?.isPublic),
|
|
avatar: overrides?.avatar ?? characterDetail?.avatar ?? characterDetail?.headImage ?? "",
|
|
};
|
|
try {
|
|
await updateCharacter(payload);
|
|
setCharacterDetail((prev: any) => ({
|
|
...prev,
|
|
title: payload.characterName,
|
|
description: payload.description,
|
|
isPublic: payload.isPublic,
|
|
avatar: payload.avatar,
|
|
}));
|
|
onUpdated?.({
|
|
id,
|
|
title: payload.characterName,
|
|
description: payload.description,
|
|
isPublic: payload.isPublic,
|
|
avatar: payload.avatar,
|
|
});
|
|
setCharacterName(payload.characterName);
|
|
setDescription(payload.description);
|
|
setIsPublic(payload.isPublic);
|
|
return true;
|
|
} catch {
|
|
Message.error(t("pages.charactersCreate.updateFailed"));
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const handleStartEditName = () => {
|
|
setCharacterName(characterDetail?.title ?? "");
|
|
setIsEditingCharacterName(true);
|
|
};
|
|
|
|
const handleCancelEditName = () => {
|
|
setCharacterName(characterDetail?.title ?? "");
|
|
setIsEditingCharacterName(false);
|
|
};
|
|
|
|
const handleFinishEditName = async () => {
|
|
if (!isEditingCharacterName || isSavingCharacterName) return;
|
|
const trimmedName = characterName.trim();
|
|
const fallbackName = characterDetail?.title ?? "";
|
|
const nextName = trimmedName || fallbackName;
|
|
if (nextName === fallbackName) {
|
|
setCharacterName(nextName);
|
|
setIsEditingCharacterName(false);
|
|
return;
|
|
}
|
|
setIsSavingCharacterName(true);
|
|
const ok = await submitCharacterUpdate({ characterName: nextName });
|
|
setIsSavingCharacterName(false);
|
|
if (ok) setIsEditingCharacterName(false);
|
|
};
|
|
|
|
const handleStartEditIntro = () => {
|
|
setDescription(characterDetail?.description ?? "");
|
|
setIsEditingDescription(true);
|
|
};
|
|
|
|
const handleCancelEditIntro = () => {
|
|
setDescription(characterDetail?.description ?? "");
|
|
setIsEditingDescription(false);
|
|
};
|
|
|
|
const handleFinishEditIntro = async () => {
|
|
if (!isEditingDescription || isSavingDescription) return;
|
|
const trimmedIntro = description.trim();
|
|
const fallbackIntro = characterDetail?.description ?? "";
|
|
const nextIntro = trimmedIntro || fallbackIntro;
|
|
if (nextIntro === fallbackIntro) {
|
|
setDescription(nextIntro);
|
|
setIsEditingDescription(false);
|
|
return;
|
|
}
|
|
setIsSavingDescription(true);
|
|
const ok = await submitCharacterUpdate({ description: nextIntro });
|
|
setIsSavingDescription(false);
|
|
if (ok) setIsEditingDescription(false);
|
|
};
|
|
|
|
const handleTogglePublic = () => {
|
|
setIsPublic((prev) => {
|
|
const next = !prev;
|
|
void submitCharacterUpdate({ isPublic: next });
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const handleOpenAvatarCrop = () => {
|
|
if (!isOwnCharacter || !isUserUploadCharacter) return;
|
|
const sourceImage = String(characterDetail?.threeViewImage ?? "").trim();
|
|
if (!sourceImage) return;
|
|
setCropModalImage(sourceImage);
|
|
};
|
|
|
|
const promptQuickActions = ["5s 虚实结合 Vlog", "拿起角色手办", "剧情九宫格", "趣味表情包", "喝咖啡", "逛公园", "和爱丽丝聚餐~", "一张臭美照~"];
|
|
const mediaActions = ["生成视频", "超清", "细节修复"];
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className={`character_detail_dialog${isOwnCharacter ? " character_detail_dialog--own" : ""}${isUserUploadCharacter ? " character_detail_dialog--user-created" : ""
|
|
}`}
|
|
>
|
|
<div className="character_detail_dialog__left">
|
|
<div className="character_detail_dialog__previewInner">
|
|
<div className="character_detail_dialog__gradient">
|
|
<div className="character_detail_dialog__gradientHeader">
|
|
<p className="character_detail_dialog__gradientTitle">{t("character_detail.card_title")}</p>
|
|
<img className="character_detail_dialog__brandMark" src={new URL('/src/assets/images/logo-full-white.png', import.meta.url).href} alt="" width={63} height={16} />
|
|
</div>
|
|
<div className="character_detail_dialog__cardFrame">
|
|
<img className="character_detail_dialog__cardImage" src={characterDetail.threeViewImage} alt="" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="character_detail_dialog__railTriggerBlock">
|
|
<img
|
|
className="character_detail_dialog__railTriggerIcon"
|
|
src={new URL("/src/assets/images/ic_close.png", import.meta.url).href}
|
|
alt=""
|
|
onClick={() => onClose?.()}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<aside className="character_detail_dialog__right">
|
|
<div className="character_detail_dialog__rightColumn">
|
|
<div className="character_detail_dialog__mainContent">
|
|
<div className="character_detail_dialog__authorRow">
|
|
{characterDetail.userInfo && isUserUploadCharacter ? (
|
|
<div className="character_detail_dialog__authorInfo">
|
|
<img className="character_detail_dialog__authorAvatar" src={characterDetail.userInfo.avatar} alt="" />
|
|
<p className="character_detail_dialog__authorName">{characterDetail.userInfo.name}</p>
|
|
{!characterDetail.userInfo.userFollowed ? (
|
|
<button type="button" className="character_detail_dialog__authorFollowBtn" onClick={() => addFollow(characterDetail.userInfo.id)}>
|
|
+{t("pages.profile.stats.following")}
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
className="character_detail_dialog__authorFollowBtn character_detail_dialog__authorFollowBtn_active"
|
|
onClick={() => delFollow(characterDetail.userInfo.userId)}
|
|
>
|
|
{t("pages.charactersCreate.followed")}
|
|
</button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div />
|
|
)}
|
|
<div className="character_detail_dialog__authorActions">
|
|
<button type="button" className="character_detail_dialog__likeRow" onClick={() => { toggleLike() }}>
|
|
<img src={new URL(`/src/assets/images/${characterDetail.userLiked ? "ic_like_active" : "ic_like"}.png`, import.meta.url).href} alt="" />
|
|
{characterDetail.likeNum || 0}
|
|
</button>
|
|
<Dropdown
|
|
trigger="click"
|
|
position="bl"
|
|
popupVisible={railMenuOpen}
|
|
onVisibleChange={setRailMenuOpen}
|
|
droplist={
|
|
<div id="character-details-rail-menu" className="character_detail_dialog__floatMenu" role="menu" aria-label={t("character_detail.download")}>
|
|
<button type="button" className="character_detail_dialog__floatRow" role="menuitem" onClick={handleDownload}>
|
|
<img className="character_detail_dialog__floatRowIcon" src={new URL("/src/assets/images/ic_download.png", import.meta.url).href} alt="" />
|
|
{t("character_detail.download")}
|
|
</button>
|
|
{isUserUploadCharacter && isOwnCharacter ? (
|
|
<button type="button" className="character_detail_dialog__floatRow" role="menuitem" onClick={handleDeleteCharacter}>
|
|
<img className="character_detail_dialog__floatRowIcon" src={new URL("/src/assets/images/ic_delete.png", import.meta.url).href} alt="" />
|
|
{t("pages.assetDetails.delete")}
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
}
|
|
>
|
|
<button
|
|
type="button"
|
|
className="character_detail_dialog__moreBtn"
|
|
aria-label={t("character_detail.download")}
|
|
aria-haspopup="menu"
|
|
aria-expanded={railMenuOpen}
|
|
aria-controls="character-details-rail-menu"
|
|
>
|
|
<img src={new URL("/src/assets/images/ic_more.png", import.meta.url).href} alt="" />
|
|
</button>
|
|
</Dropdown>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="character_detail_dialog__metaHintRow">
|
|
<span className="character_detail_dialog__metaHintText">{formatLocalDate(characterDetail.createTime ?? characterDetail.taskInfo?.createTime)}</span>
|
|
<span className="character_detail_dialog__metaHintDivider" />
|
|
<span className="character_detail_dialog__metaHintText">内容由AI生成</span>
|
|
</div>
|
|
<div className="character_detail_dialog__divider" role="presentation" />
|
|
|
|
<div className="character_detail_dialog__avatarSection">
|
|
<div className="character_detail_dialog__avatarPreview">
|
|
<img className="character_detail_dialog__avatarPreviewImage" src={characterDetail.avatar || characterDetail.headImage || characterDetail.threeViewImage} alt="" />
|
|
</div>
|
|
</div>
|
|
{false && isOwnCharacter && isUserUploadCharacter && (
|
|
<div className="character_detail_dialog__avatarUpdate">
|
|
<button type="button" className="character_detail_dialog__avatarUpdateBtn" onClick={handleOpenAvatarCrop}>
|
|
修改头像
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<div className="character_detail_dialog__nameRow">
|
|
<div className="character_detail_dialog__nameValue">
|
|
{!isEditingCharacterName ? (
|
|
<>
|
|
<div className="character_detail_dialog__nameValueText">{characterDetail.title}</div>
|
|
{isOwnCharacter && <img
|
|
className="character_detail_dialog__edit"
|
|
src={new URL("/src/assets/images/ic_character_edit.png", import.meta.url).href}
|
|
alt=""
|
|
onClick={handleStartEditName}
|
|
/>}
|
|
</>
|
|
) : (
|
|
<div className="character_detail_dialog__nameEditField">
|
|
<Input
|
|
autoFocus
|
|
className="character_detail_dialog__nameInput"
|
|
style={{ width: `${characterNameInputWidth}px` }}
|
|
value={characterName}
|
|
onChange={setCharacterName}
|
|
onFocus={(event) => placeCaretAtEnd(event.target)}
|
|
onBlur={() => {
|
|
if (skipNextNameBlurRef.current) {
|
|
skipNextNameBlurRef.current = false;
|
|
return;
|
|
}
|
|
void handleFinishEditName();
|
|
}}
|
|
onPressEnter={() => { void handleFinishEditName(); }}
|
|
onKeyDown={(event) => {
|
|
if (event.key === "Escape") {
|
|
skipNextNameBlurRef.current = true;
|
|
handleCancelEditName();
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="character_detail_dialog__intro">
|
|
<p className="character_detail_dialog__introTitle">{t("character_detail.role_intro_title")}</p>
|
|
<div className="character_detail_dialog__introBody">
|
|
{!isEditingDescription ? (
|
|
<>
|
|
<p className="character_detail_dialog__introText">{characterDetail.description}</p>
|
|
{isOwnCharacter && <img
|
|
className="character_detail_dialog__edit"
|
|
src={new URL("/src/assets/images/ic_character_edit.png", import.meta.url).href}
|
|
alt=""
|
|
onClick={handleStartEditIntro}
|
|
/>}
|
|
</>
|
|
) : (
|
|
<Input.TextArea
|
|
autoFocus
|
|
className="character_detail_dialog__introInput"
|
|
value={description}
|
|
onChange={setDescription}
|
|
onFocus={(event) => placeCaretAtEnd(event.target)}
|
|
onBlur={() => {
|
|
if (skipNextIntroBlurRef.current) {
|
|
skipNextIntroBlurRef.current = false;
|
|
return;
|
|
}
|
|
void handleFinishEditIntro();
|
|
}}
|
|
autoSize={{ minRows: 3, maxRows: 8 }}
|
|
onKeyDown={(event) => {
|
|
if (event.key === "Escape") {
|
|
skipNextIntroBlurRef.current = true;
|
|
handleCancelEditIntro();
|
|
}
|
|
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") void handleFinishEditIntro();
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{isOwnCharacter && (
|
|
<div className="character_detail_dialog__toggleRow">
|
|
<span className="character_detail_dialog__toggleLabel">{t("pages.charactersCreate.createRoleModal.publicDisplay")}</span>
|
|
<button
|
|
type="button"
|
|
className="character_detail_dialog__toggleStatus"
|
|
onClick={handleTogglePublic}
|
|
aria-label={t("pages.charactersCreate.createRoleModal.publicDisplaySwitchAria")}
|
|
aria-pressed={isPublic}
|
|
>
|
|
<span className={`character_detail_dialog__toggleStatusPart${isPublic ? " character_detail_dialog__toggleStatusPart_active" : ""}`}>公开</span>
|
|
<span className="character_detail_dialog__toggleStatusSlash"> / </span>
|
|
<span className={`character_detail_dialog__toggleStatusPart${!isPublic ? " character_detail_dialog__toggleStatusPart_active" : ""}`}>未公开</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{characterDetail.taskInfo != null && (
|
|
<div className="character_detail_dialog__taskInlineMeta">
|
|
<span className="character_detail_dialog__taskInlineMetaItem">{characterDetail.taskInfo?.aiModelName ?? ""}</span>
|
|
<span className="character_detail_dialog__taskInlineMetaDivider" />
|
|
<span className="character_detail_dialog__taskInlineMetaItem">
|
|
{characterDetail.taskInfo?.width && characterDetail.taskInfo?.height
|
|
? `${characterDetail.taskInfo.width}P`
|
|
: ""}
|
|
</span>
|
|
<span className="character_detail_dialog__taskInlineMetaDivider" />
|
|
<span className="character_detail_dialog__taskInlineMetaItem">{characterDetail.taskInfo?.aspectRatio ?? ""}</span>
|
|
<span className="character_detail_dialog__taskInlineMetaDivider" />
|
|
<span className="character_detail_dialog__taskInlineMetaItem">详细信息</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="character_detail_dialog__sidebarFoot">
|
|
<div className="character_detail_dialog__actionPanel character_detail_dialog__actionPanel_prompt">
|
|
{promptQuickActions.map((label) => (
|
|
<button key={label} type="button" className="character_detail_dialog__panelActionBtn">
|
|
<span className="character_detail_dialog__panelActionIcon" />
|
|
<span className="character_detail_dialog__panelActionText">{label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="character_detail_dialog__actionPanel character_detail_dialog__actionPanel_media">
|
|
{mediaActions.map((label) => (
|
|
<button key={label} type="button" className="character_detail_dialog__panelActionBtn">
|
|
<span className="character_detail_dialog__panelActionIcon" />
|
|
<span className="character_detail_dialog__panelActionText">{label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="character_detail_dialog__actionPanel character_detail_dialog__actionPanel_primary">
|
|
<div
|
|
className="character_detail_dialog__panelActionBtn character_detail_dialog__panelActionBtn_active"
|
|
onClick={() => {
|
|
const refImage = String(characterDetail?.threeViewImage ?? "").trim();
|
|
const refTitle = String(characterDetail?.title ?? "").trim();
|
|
if (!refImage) return;
|
|
requestAppendImageUrl(refImage, refTitle, 103);
|
|
onClose?.();
|
|
}}
|
|
>
|
|
<span className="character_detail_dialog__panelActionIcon" />
|
|
<span className="character_detail_dialog__panelActionText">{t("character_detail.use_character")}</span>
|
|
</div>
|
|
<button type="button" className="character_detail_dialog__panelActionBtn" onClick={onUseCharacter}>
|
|
<span className="character_detail_dialog__panelActionIcon" />
|
|
<span className="character_detail_dialog__panelActionText">做同款</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
<AvatarCropModal
|
|
image={cropModalImage}
|
|
onUploaded={(uploadedAvatarUrl) => {
|
|
setCharacterDetail((prev: any) => ({
|
|
...prev,
|
|
avatar: uploadedAvatarUrl,
|
|
}));
|
|
void submitCharacterUpdate({ avatar: uploadedAvatarUrl });
|
|
setCropModalImage("");
|
|
}}
|
|
onClose={() => setCropModalImage("")}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|