Fix prop confirmation bug, add Wan 2.7 model, refine multiple UI details, improve prop generation quality and aspect ratio, remove text overlays from Asset Center created images, and optimize prop filtering logic
This commit is contained in:
@@ -463,7 +463,7 @@ export default function AssetsStage({
|
||||
onRegenerateGroup={handleRegenerateLocationGroup}
|
||||
onUndo={handleUndoLocation}
|
||||
onImageClick={setPreviewImage}
|
||||
onImageEdit={(locId, imgIdx) => handleOpenLocationImageEdit(locId, imgIdx)}
|
||||
onImageEdit={(locId, imgIdx) => handleOpenLocationImageEdit(locId, imgIdx, 'location')}
|
||||
onCopyFromGlobal={handleCopyLocationFromGlobal}
|
||||
filterIds={episodeAssetIds?.locIds ?? null}
|
||||
/>
|
||||
@@ -488,7 +488,7 @@ export default function AssetsStage({
|
||||
void propAssetActions.revertRender({ id: propId }).catch(() => undefined)
|
||||
}}
|
||||
onImageClick={setPreviewImage}
|
||||
onImageEdit={() => undefined}
|
||||
onImageEdit={(propId, imgIdx) => handleOpenLocationImageEdit(propId, imgIdx, 'prop')}
|
||||
onCopyFromGlobal={handleCopyPropFromGlobal}
|
||||
filterIds={episodeAssetIds?.propIds ?? null}
|
||||
/>
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function WorkspaceAssetLibraryModal({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar" data-asset-scroll-container="1">
|
||||
<div className="flex-1 overflow-y-auto p-6 app-scrollbar" data-asset-scroll-container="1">
|
||||
{assetsLoading && !hasCharacters && !hasLocations && (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-[var(--glass-text-tertiary)] animate-pulse">
|
||||
<TaskStatusInline state={assetsLoadingState} className="text-base [&>span]:text-base" />
|
||||
|
||||
@@ -133,8 +133,8 @@ export default function AddLocationModal({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-[var(--glass-overlay)] flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-[var(--glass-bg-surface)] rounded-xl shadow-xl max-w-2xl w-full max-h-[85vh] overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<div className="bg-[var(--glass-bg-surface)] rounded-xl shadow-xl max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col">
|
||||
<div className="p-6 overflow-y-auto app-scrollbar flex-1 min-h-0">
|
||||
{/* 标题 */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--glass-text-primary)]">
|
||||
|
||||
@@ -35,10 +35,12 @@ interface EditingPropState {
|
||||
propId: string
|
||||
propName: string
|
||||
summary: string
|
||||
description: string
|
||||
variantId?: string
|
||||
}
|
||||
|
||||
interface LocationImageEditModalState {
|
||||
assetType: 'location' | 'prop'
|
||||
locationName: string
|
||||
}
|
||||
|
||||
@@ -140,7 +142,7 @@ export default function AssetsStageModals({
|
||||
|
||||
{imageEditModal && (
|
||||
<ImageEditModal
|
||||
type="location"
|
||||
type={imageEditModal.assetType}
|
||||
name={imageEditModal.locationName}
|
||||
onClose={closeImageEditModal}
|
||||
onConfirm={handleLocationImageEdit}
|
||||
@@ -238,6 +240,7 @@ export default function AssetsStageModals({
|
||||
propId={editingProp.propId}
|
||||
propName={editingProp.propName}
|
||||
summary={editingProp.summary}
|
||||
description={editingProp.description}
|
||||
variantId={editingProp.variantId}
|
||||
projectId={projectId}
|
||||
onClose={closeEditingProp}
|
||||
|
||||
@@ -454,6 +454,7 @@ export default function CharacterCard({
|
||||
mode="single"
|
||||
characterName={character.name}
|
||||
changeReason={appearance.changeReason}
|
||||
aspectClassName="aspect-[3/2]"
|
||||
currentImageUrl={currentImageUrl}
|
||||
selectedIndex={selectedIndex}
|
||||
hasMultipleImages={hasMultipleImages}
|
||||
|
||||
@@ -101,11 +101,11 @@ export default function CharacterProfileDialog({
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[var(--glass-overlay)]" onClick={onClose}>
|
||||
<div
|
||||
className="bg-[var(--glass-bg-surface)] rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto m-4"
|
||||
className="bg-[var(--glass-bg-surface)] rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col m-4"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div className="sticky top-0 bg-[var(--glass-bg-surface)] border-b border-[var(--glass-stroke-base)] px-6 py-4 flex items-center justify-between">
|
||||
<div className="bg-[var(--glass-bg-surface)] border-b border-[var(--glass-stroke-base)] px-6 py-4 flex items-center justify-between shrink-0">
|
||||
<h2 className="text-xl font-semibold text-[var(--glass-text-primary)]">{t('characterProfile.editDialogTitle', { name: characterName })}</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -116,7 +116,7 @@ export default function CharacterProfileDialog({
|
||||
</div>
|
||||
|
||||
{/* 表单内容 */}
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="p-6 space-y-4 overflow-y-auto app-scrollbar flex-1 min-h-0">
|
||||
{/* 角色层级 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--glass-text-secondary)] mb-2">{t('characterProfile.importanceLevel')}</label>
|
||||
@@ -261,7 +261,7 @@ export default function CharacterProfileDialog({
|
||||
</div>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<div className="sticky bottom-0 bg-[var(--glass-bg-surface)] border-t border-[var(--glass-stroke-base)] px-6 py-4 flex gap-3 justify-end">
|
||||
<div className="bg-[var(--glass-bg-surface)] border-t border-[var(--glass-stroke-base)] px-6 py-4 flex gap-3 justify-end shrink-0">
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={isSaving}
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function CharacterSection({
|
||||
if (!element) return
|
||||
const scrollContainer = (element.closest('[data-asset-scroll-container="1"]') ||
|
||||
document.querySelector('[data-asset-scroll-container="1"]') ||
|
||||
element.closest('.custom-scrollbar')) as HTMLElement | null
|
||||
element.closest('.app-scrollbar')) as HTMLElement | null
|
||||
|
||||
if (scrollAnimationRef.current !== null) {
|
||||
window.cancelAnimationFrame(scrollAnimationRef.current)
|
||||
@@ -287,18 +287,18 @@ export default function CharacterSection({
|
||||
{t("character.assetCount", { count: sortedAppearances.length })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 从资产中心复制按钮 */}
|
||||
<div className="flex flex-col items-end gap-1.5">
|
||||
{/* 从资产中心导入按钮 */}
|
||||
<button
|
||||
onClick={() => onCopyFromGlobal(character.id)}
|
||||
className="text-xs text-[var(--glass-tone-info-fg)] hover:text-[var(--glass-tone-info-fg)] flex items-center gap-1 px-2 py-1 rounded-lg hover:bg-[var(--glass-tone-info-bg)] transition-colors"
|
||||
>
|
||||
<AppIcon name="copy" className="w-4 h-4" />
|
||||
<AppIcon name="arrowDownCircle" className="w-4 h-4" />
|
||||
{t("character.copyFromGlobal")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDeleteCharacter(character.id)}
|
||||
className="text-xs text-[var(--glass-tone-danger-fg)] hover:text-[var(--glass-tone-danger-fg)] flex items-center gap-1"
|
||||
className="text-xs text-[var(--glass-tone-danger-fg)] hover:text-[var(--glass-tone-danger-fg)] flex items-center gap-1 px-2 py-1 rounded-lg hover:bg-[var(--glass-tone-danger-bg)] transition-colors"
|
||||
>
|
||||
<AppIcon name="trash" className="w-4 h-4" />
|
||||
{t("character.delete")}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useState, useRef } from 'react'
|
||||
import { AppIcon } from '@/components/ui/icons'
|
||||
|
||||
interface ImageEditModalProps {
|
||||
type: 'character' | 'location'
|
||||
type: 'character' | 'location' | 'prop'
|
||||
name: string
|
||||
onClose: () => void
|
||||
onConfirm: (modifyPrompt: string, extraImageUrls?: string[]) => void
|
||||
@@ -28,10 +28,16 @@ export default function ImageEditModal({
|
||||
const [editImages, setEditImages] = useState<string[]>([])
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const title = type === 'character' ? t('imageEdit.editCharacterImage') : t('imageEdit.editLocationImage')
|
||||
const title = type === 'character'
|
||||
? t('imageEdit.editCharacterImage')
|
||||
: type === 'prop'
|
||||
? t('imageEdit.editPropImage')
|
||||
: t('imageEdit.editLocationImage')
|
||||
const subtitle = type === 'character'
|
||||
? t('imageEdit.characterLabel', { name })
|
||||
: t('imageEdit.locationLabel', { name })
|
||||
: type === 'prop'
|
||||
? t('imageEdit.propLabel', { name })
|
||||
: t('imageEdit.locationLabel', { name })
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!modifyPrompt.trim()) {
|
||||
@@ -88,14 +94,14 @@ export default function ImageEditModal({
|
||||
return (
|
||||
<div className="fixed inset-0 bg-[var(--glass-overlay)] z-50 flex items-center justify-center p-4">
|
||||
<div
|
||||
className="bg-[var(--glass-bg-surface)] rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto"
|
||||
className="bg-[var(--glass-bg-surface)] rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col"
|
||||
onPaste={handlePaste}
|
||||
>
|
||||
<div className="p-6 border-b">
|
||||
<div className="p-6 border-b shrink-0">
|
||||
<h3 className="text-lg font-bold text-[var(--glass-text-primary)]">{title}</h3>
|
||||
<p className="text-sm text-[var(--glass-text-tertiary)] mt-1">{subtitle} · {t('imageEdit.subtitle')}</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="p-6 space-y-4 overflow-y-auto app-scrollbar flex-1 min-h-0">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--glass-text-secondary)] mb-2">{t('imageEdit.editInstruction')}</label>
|
||||
<textarea
|
||||
@@ -103,6 +109,8 @@ export default function ImageEditModal({
|
||||
onChange={(e) => setModifyPrompt(e.target.value)}
|
||||
placeholder={type === 'character'
|
||||
? t('imageEdit.characterPlaceholder')
|
||||
: type === 'prop'
|
||||
? t('imageEdit.propPlaceholder')
|
||||
: t('imageEdit.locationPlaceholder')
|
||||
}
|
||||
className="w-full h-24 px-3 py-2 border border-[var(--glass-stroke-strong)] rounded-lg focus:ring-2 focus:ring-[var(--glass-tone-info-fg)] focus:border-[var(--glass-stroke-focus)] resize-none"
|
||||
|
||||
@@ -20,6 +20,8 @@ import { getImageGenerationCountOptions } from '@/lib/image-generation/count'
|
||||
import { useImageGenerationCount } from '@/lib/image-generation/use-image-generation-count'
|
||||
import { countGeneratedImageSlots, resolveDisplayImageSlots } from '@/lib/image-generation/slot-state'
|
||||
import { AppIcon } from '@/components/ui/icons'
|
||||
import { AI_EDIT_BUTTON_CLASS, AI_EDIT_ICON_CLASS } from '@/components/ui/ai-edit-style'
|
||||
import AISparklesIcon from '@/components/ui/icons/AISparklesIcon'
|
||||
import { canGenerateLocationBackedAsset } from './location-backed-asset'
|
||||
|
||||
interface LocationCardProps {
|
||||
@@ -37,7 +39,7 @@ interface LocationCardProps {
|
||||
activeTaskKeys?: Set<string>
|
||||
onClearTaskKey?: (key: string) => void
|
||||
projectId: string
|
||||
onConfirmSelection?: (locationId: string) => void
|
||||
onConfirmSelection?: (locationId: string) => Promise<void> | void
|
||||
}
|
||||
|
||||
export default function LocationCard({
|
||||
@@ -179,6 +181,7 @@ export default function LocationCard({
|
||||
const hasPreviousVersion = location.images?.some(img => img.previousImageUrl) || false
|
||||
|
||||
const showSelectionMode = displaySlotCount > 1
|
||||
const singleImageAspectClassName = assetType === 'prop' ? 'aspect-[3/2]' : 'aspect-square'
|
||||
|
||||
// 选择模式:显示名字在上,三张图片在下
|
||||
if (showSelectionMode) {
|
||||
@@ -271,7 +274,9 @@ export default function LocationCard({
|
||||
onConfirmSelection={selectedIndex !== null && onConfirmSelection
|
||||
? () => {
|
||||
setIsConfirmingSelection(true)
|
||||
onConfirmSelection(location.id)
|
||||
void Promise.resolve(onConfirmSelection(location.id)).finally(() => {
|
||||
setIsConfirmingSelection(false)
|
||||
})
|
||||
}
|
||||
: undefined}
|
||||
/>
|
||||
@@ -297,11 +302,10 @@ export default function LocationCard({
|
||||
{!isTaskRunning && currentImageUrl && onImageEdit && (
|
||||
<button
|
||||
onClick={() => onImageEdit(location.id, currentImageIndex)}
|
||||
className="w-7 h-7 rounded-full flex items-center justify-center transition-all shadow-sm"
|
||||
style={{ background: 'linear-gradient(135deg, #6366f1, #8b5cf6)' }}
|
||||
className={`w-7 h-7 rounded-full flex items-center justify-center transition-all active:scale-95 ${AI_EDIT_BUTTON_CLASS}`}
|
||||
title={t('image.edit')}
|
||||
>
|
||||
<AppIcon name="edit" className="w-4 h-4 text-white" />
|
||||
<AISparklesIcon className={`w-4 h-4 ${AI_EDIT_ICON_CLASS}`} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -340,7 +344,7 @@ export default function LocationCard({
|
||||
className="flex-shrink-0 w-5 h-5 rounded hover:bg-[var(--glass-tone-info-bg)] flex items-center justify-center transition-colors"
|
||||
title={t('character.copyFromGlobal')}
|
||||
>
|
||||
<AppIcon name="copy" className="w-3.5 h-3.5 text-[var(--glass-tone-info-fg)]" />
|
||||
<AppIcon name="arrowDownCircle" className="w-3.5 h-3.5 text-[var(--glass-tone-info-fg)]" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -361,7 +365,7 @@ export default function LocationCard({
|
||||
)
|
||||
|
||||
const firstImage = location.images?.[0]
|
||||
const canGenerate = canGenerateLocationBackedAsset(location)
|
||||
const canGenerate = canGenerateLocationBackedAsset(location, assetType)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 glass-surface-elevated p-3">
|
||||
@@ -376,6 +380,7 @@ export default function LocationCard({
|
||||
<LocationImageList
|
||||
mode="single"
|
||||
locationName={location.name}
|
||||
aspectClassName={singleImageAspectClassName}
|
||||
currentImageUrl={currentImageUrl}
|
||||
selectedIndex={selectedIndex}
|
||||
hasMultipleImages={hasMultipleImages}
|
||||
|
||||
@@ -29,7 +29,7 @@ interface LocationSectionProps {
|
||||
// 🔥 V6.6 重构:重命名为 handleGenerateImage
|
||||
handleGenerateImage: (type: 'character' | 'location' | 'prop', id: string, appearanceId?: string, count?: number) => Promise<void>
|
||||
onSelectImage: (locationId: string, imageIndex: number | null) => void
|
||||
onConfirmSelection: (locationId: string) => void
|
||||
onConfirmSelection: (locationId: string) => Promise<void> | void
|
||||
onRegenerateSingle: (locationId: string, imageIndex: number) => Promise<void>
|
||||
onRegenerateGroup: (locationId: string, count?: number) => Promise<void>
|
||||
onUndo: (locationId: string) => void
|
||||
@@ -149,7 +149,7 @@ export default function LocationSection({
|
||||
activeTaskKeys={activeTaskKeys}
|
||||
onClearTaskKey={onClearTaskKey}
|
||||
projectId={projectId}
|
||||
onConfirmSelection={assetType === 'location' ? onConfirmSelection : undefined}
|
||||
onConfirmSelection={onConfirmSelection}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ type CharacterCardGalleryProps =
|
||||
mode: 'single'
|
||||
characterName: string
|
||||
changeReason: string
|
||||
aspectClassName: string
|
||||
currentImageUrl: string | null | undefined
|
||||
selectedIndex: number | null
|
||||
hasMultipleImages: boolean
|
||||
@@ -105,14 +106,14 @@ export default function CharacterCardGallery(props: CharacterCardGalleryProps) {
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="rounded-lg overflow-hidden border-2 border-[var(--glass-stroke-base)] relative">
|
||||
<div className={`relative overflow-hidden rounded-lg border-2 border-[var(--glass-stroke-base)] ${props.aspectClassName}`}>
|
||||
{props.currentImageUrl ? (
|
||||
<div className="relative w-full">
|
||||
<div className="relative h-full w-full">
|
||||
<MediaImageWithLoading
|
||||
src={props.currentImageUrl}
|
||||
alt={`${props.characterName} - ${props.changeReason}`}
|
||||
containerClassName="w-full min-h-[120px]"
|
||||
className="w-full h-auto object-contain cursor-pointer hover:opacity-90 transition-opacity"
|
||||
containerClassName="h-full w-full"
|
||||
className="h-full w-full object-contain cursor-pointer hover:opacity-90 transition-opacity"
|
||||
onClick={() => props.onImageClick(props.currentImageUrl!)}
|
||||
/>
|
||||
{props.selectedIndex !== null && props.hasMultipleImages && (
|
||||
@@ -122,7 +123,7 @@ export default function CharacterCardGallery(props: CharacterCardGalleryProps) {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full bg-[var(--glass-bg-muted)] flex items-center justify-center">
|
||||
<div className="flex h-full w-full items-center justify-center bg-[var(--glass-bg-muted)]">
|
||||
{appearanceErrorDisplay && !props.isAppearanceTaskRunning ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 px-4 text-center">
|
||||
<AppIcon name="alert" className="w-8 h-8 text-[var(--glass-tone-danger-fg)] mb-2" />
|
||||
|
||||
@@ -31,10 +31,12 @@ interface EditingProp {
|
||||
propId: string
|
||||
propName: string
|
||||
summary: string
|
||||
description: string
|
||||
variantId?: string
|
||||
}
|
||||
|
||||
interface ImageEditModal {
|
||||
assetType: 'location' | 'prop'
|
||||
locationId: string
|
||||
imageIndex: number
|
||||
locationName: string
|
||||
@@ -137,20 +139,24 @@ export function useAssetModals({
|
||||
}
|
||||
|
||||
const handleEditProp = (prop: Prop) => {
|
||||
const firstImage = prop.images?.[0]
|
||||
setEditingProp({
|
||||
propId: prop.id,
|
||||
propName: prop.name,
|
||||
summary: prop.summary || prop.images?.[0]?.description || '',
|
||||
variantId: prop.images?.[0]?.id,
|
||||
summary: prop.summary || '',
|
||||
description: firstImage?.description || prop.summary || '',
|
||||
variantId: firstImage?.id,
|
||||
})
|
||||
}
|
||||
|
||||
// 打开场景图片编辑弹窗
|
||||
const handleOpenLocationImageEdit = (locationId: string, imageIndex: number) => {
|
||||
const location = locations.find(l => l.id === locationId)
|
||||
const handleOpenLocationImageEdit = (locationId: string, imageIndex: number, assetType: 'location' | 'prop' = 'location') => {
|
||||
const assetsOfType = assetType === 'prop' ? props : locations
|
||||
const location = assetsOfType.find(l => l.id === locationId)
|
||||
if (!location) return
|
||||
|
||||
setImageEditModal({
|
||||
assetType,
|
||||
locationId,
|
||||
imageIndex,
|
||||
locationName: location.name
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useCallback } from 'react'
|
||||
import { logInfo as _ulogInfo } from '@/lib/logging/core'
|
||||
import { isAbortError } from '@/lib/error-utils'
|
||||
import {
|
||||
useAssetActions,
|
||||
useModifyProjectCharacterImage,
|
||||
useModifyProjectLocationImage,
|
||||
useUndoProjectCharacterImage,
|
||||
useUndoProjectLocationImage,
|
||||
useUpdateProjectAppearanceDescription,
|
||||
@@ -29,6 +29,7 @@ interface EditingLocationState {
|
||||
}
|
||||
|
||||
interface LocationImageEditState {
|
||||
assetType: 'location' | 'prop'
|
||||
locationId: string
|
||||
imageIndex: number
|
||||
locationName: string
|
||||
@@ -73,7 +74,8 @@ export function useAssetsImageEdit({
|
||||
closeCharacterImageEditModal,
|
||||
}: UseAssetsImageEditParams) {
|
||||
const modifyCharacterImage = useModifyProjectCharacterImage(projectId)
|
||||
const modifyLocationImage = useModifyProjectLocationImage(projectId)
|
||||
const locationAssetActions = useAssetActions({ scope: 'project', projectId, kind: 'location' })
|
||||
const propAssetActions = useAssetActions({ scope: 'project', projectId, kind: 'prop' })
|
||||
const undoCharacterImage = useUndoProjectCharacterImage(projectId)
|
||||
const undoLocationImage = useUndoProjectLocationImage(projectId)
|
||||
const updateAppearanceDescription = useUpdateProjectAppearanceDescription(projectId)
|
||||
@@ -111,29 +113,31 @@ export function useAssetsImageEdit({
|
||||
|
||||
const handleLocationImageEdit = useCallback(async (modifyPrompt: string, extraImageUrls?: string[]) => {
|
||||
if (!imageEditModal) return
|
||||
const { locationId, imageIndex, locationName } = imageEditModal
|
||||
const { assetType, locationId, imageIndex, locationName } = imageEditModal
|
||||
|
||||
closeImageEditModal()
|
||||
|
||||
_ulogInfo(`[场景编辑] 开始编辑 ${locationName}, locationId=${locationId}, imageIndex=${imageIndex}`)
|
||||
const assetLabel = assetType === 'prop' ? '道具' : '场景'
|
||||
const editAction = assetType === 'prop' ? propAssetActions : locationAssetActions
|
||||
|
||||
modifyLocationImage.mutate(
|
||||
{ locationId, imageIndex, modifyPrompt, extraImageUrls },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
const result = (data || {}) as { descriptionUpdated?: boolean }
|
||||
_ulogInfo(`[场景编辑] ✅ 完成: ${locationName}`)
|
||||
const descNote = result.descriptionUpdated ? t('stage.updateSuccess') : ''
|
||||
showToast(`${locationName} ${t('image.editSuccess')}${descNote}`, 'success')
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
_ulogInfo(`[场景编辑] ❌ 失败: ${locationName}`, error)
|
||||
if (isAbortError(error)) return
|
||||
showToast(`${t('image.editFailed')}: ${getErrorMessage(error)}`, 'error')
|
||||
},
|
||||
},
|
||||
)
|
||||
}, [closeImageEditModal, imageEditModal, modifyLocationImage, showToast, t])
|
||||
_ulogInfo(`[${assetLabel}编辑] 开始编辑 ${locationName}, locationId=${locationId}, imageIndex=${imageIndex}`)
|
||||
|
||||
void editAction.modifyRender({
|
||||
id: locationId,
|
||||
imageIndex,
|
||||
modifyPrompt,
|
||||
extraImageUrls,
|
||||
}).then((data) => {
|
||||
const result = (data || {}) as { descriptionUpdated?: boolean }
|
||||
_ulogInfo(`[${assetLabel}编辑] ✅ 完成: ${locationName}`)
|
||||
const descNote = result.descriptionUpdated ? t('stage.updateSuccess') : ''
|
||||
showToast(`${locationName} ${t('image.editSuccess')}${descNote}`, 'success')
|
||||
}).catch((error: unknown) => {
|
||||
_ulogInfo(`[${assetLabel}编辑] ❌ 失败: ${locationName}`, error)
|
||||
if (isAbortError(error)) return
|
||||
showToast(`${t('image.editFailed')}: ${getErrorMessage(error)}`, 'error')
|
||||
})
|
||||
}, [closeImageEditModal, imageEditModal, locationAssetActions, propAssetActions, showToast, t])
|
||||
|
||||
const handleCharacterImageEdit = useCallback(async (modifyPrompt: string, extraImageUrls?: string[]) => {
|
||||
if (!characterImageEditModal) return
|
||||
|
||||
@@ -58,7 +58,7 @@ export function useLocationActions({
|
||||
const regenerateGroup = useRegenerateLocationGroup(projectId)
|
||||
const deleteLocationMutation = useDeleteProjectLocation(projectId)
|
||||
const selectLocationImageMutation = useSelectProjectLocationImage(projectId)
|
||||
const confirmLocationSelectionMutation = useConfirmProjectLocationSelection(projectId)
|
||||
const confirmLocationSelectionMutation = useConfirmProjectLocationSelection(projectId, assetType)
|
||||
const updateLocationDescriptionMutation = useUpdateProjectLocationDescription(projectId)
|
||||
|
||||
// 删除场景
|
||||
@@ -96,9 +96,6 @@ export function useLocationActions({
|
||||
|
||||
// 确认选择并删除其他候选图片
|
||||
const handleConfirmLocationSelection = useCallback(async (locationId: string) => {
|
||||
if (assetType === 'prop') {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await confirmLocationSelectionMutation.mutateAsync({ locationId })
|
||||
showToast?.(`✓ ${t('image.confirmSuccess')}`, 'success')
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { Location, Prop } from '@/types/project'
|
||||
|
||||
export function canGenerateLocationBackedAsset(asset: Location | Prop): boolean {
|
||||
if (asset.summary && asset.summary.trim().length > 0) {
|
||||
export function canGenerateLocationBackedAsset(
|
||||
asset: Location | Prop,
|
||||
assetType: 'location' | 'prop',
|
||||
): boolean {
|
||||
if (assetType === 'location' && asset.summary && asset.summary.trim().length > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ type LocationImageListProps =
|
||||
| {
|
||||
mode: 'single'
|
||||
locationName: string
|
||||
aspectClassName: string
|
||||
currentImageUrl: string | null | undefined
|
||||
selectedIndex: number | null
|
||||
hasMultipleImages: boolean
|
||||
@@ -161,14 +162,14 @@ export default function LocationImageList(props: LocationImageListProps) {
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="rounded-lg overflow-hidden border-2 border-[var(--glass-stroke-base)] relative">
|
||||
<div className={`relative overflow-hidden rounded-lg border-2 border-[var(--glass-stroke-base)] ${props.aspectClassName}`}>
|
||||
{props.currentImageUrl ? (
|
||||
<div className="relative w-full">
|
||||
<div className="relative h-full w-full">
|
||||
<MediaImageWithLoading
|
||||
src={props.currentImageUrl}
|
||||
alt={props.locationName}
|
||||
containerClassName="w-full min-h-[120px]"
|
||||
className="w-full h-auto object-contain cursor-pointer hover:opacity-90 transition-opacity"
|
||||
containerClassName="h-full w-full"
|
||||
className="h-full w-full object-contain cursor-pointer hover:opacity-90 transition-opacity"
|
||||
onClick={() => props.onImageClick(props.currentImageUrl!)}
|
||||
/>
|
||||
{props.selectedIndex !== null && props.hasMultipleImages && (
|
||||
@@ -178,7 +179,7 @@ export default function LocationImageList(props: LocationImageListProps) {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full bg-[var(--glass-bg-muted)] flex items-center justify-center">
|
||||
<div className="flex h-full w-full items-center justify-center bg-[var(--glass-bg-muted)]">
|
||||
{locationErrorDisplay && !props.isTaskRunning ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 px-4 text-center">
|
||||
<AppIcon name="alert" className="w-8 h-8 text-[var(--glass-tone-danger-fg)] mb-2" />
|
||||
@@ -186,7 +187,7 @@ export default function LocationImageList(props: LocationImageListProps) {
|
||||
<div className="text-[var(--glass-tone-danger-fg)] text-xs max-w-full break-words">{locationErrorDisplay.message}</div>
|
||||
</div>
|
||||
) : (
|
||||
<AppIcon name="globe2" className="w-8 h-8 text-[var(--glass-text-tertiary)]" />
|
||||
<AppIcon name="image" className="w-8 h-8 text-[var(--glass-text-tertiary)]" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -462,7 +462,7 @@ export default function ScriptViewAssetsPanel({
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex-1 min-h-0 glass-surface-modal overflow-hidden p-4 pr-3">
|
||||
<div className="flex h-full flex-col gap-6 overflow-y-auto pr-1 custom-scrollbar">
|
||||
<div className="flex h-full flex-col gap-6 overflow-y-auto pr-1 app-scrollbar">
|
||||
{assetsLoading && characters.length === 0 && locations.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-[var(--glass-text-tertiary)] animate-pulse">
|
||||
<TaskStatusInline state={assetsLoadingState} />
|
||||
@@ -490,7 +490,7 @@ export default function ScriptViewAssetsPanel({
|
||||
{showAddChar && mounted && createPortal(
|
||||
<div ref={charEditorPopoverRef} className="fixed right-4 bottom-4 z-[80] glass-surface-modal w-[min(24rem,calc(100vw-2rem))] h-[min(560px,calc(100vh-2rem))] p-3 animate-fadeIn flex flex-col shadow-2xl">
|
||||
<div className="shrink-0 text-xs text-[var(--glass-text-tertiary)]">{tCommon('edit')} · {tScript('asset.activeCharacters')}</div>
|
||||
<div className="mt-3 flex-1 min-h-0 space-y-4 overflow-y-auto pr-1 custom-scrollbar">
|
||||
<div className="mt-3 flex-1 min-h-0 space-y-4 overflow-y-auto pr-1 app-scrollbar">
|
||||
{isAllClipsMode && (
|
||||
<div className="rounded-lg border border-[var(--glass-stroke-base)] bg-[var(--glass-bg-muted)]/40 p-2 text-[11px] text-[var(--glass-text-tertiary)]">
|
||||
当前为“全部片段”视图,文案要求仅在单片段视图可编辑
|
||||
@@ -646,7 +646,7 @@ export default function ScriptViewAssetsPanel({
|
||||
{showAddLoc && mounted && createPortal(
|
||||
<div ref={locEditorPopoverRef} className="fixed right-4 bottom-4 z-[80] glass-surface-modal w-[min(24rem,calc(100vw-2rem))] h-[min(560px,calc(100vh-2rem))] p-3 animate-fadeIn flex flex-col shadow-2xl">
|
||||
<div className="shrink-0 text-xs text-[var(--glass-text-tertiary)]">{tCommon('edit')} · {tScript('asset.activeLocations')}</div>
|
||||
<div className="mt-3 flex-1 min-h-0 overflow-y-auto pr-1 custom-scrollbar">
|
||||
<div className="mt-3 flex-1 min-h-0 overflow-y-auto pr-1 app-scrollbar">
|
||||
{isAllClipsMode && (
|
||||
<div className="mb-3 rounded-lg border border-[var(--glass-stroke-base)] bg-[var(--glass-bg-muted)]/40 p-2 text-[11px] text-[var(--glass-text-tertiary)]">
|
||||
当前为“全部片段”视图,场景文案要求仅在单片段视图可编辑
|
||||
@@ -775,7 +775,7 @@ export default function ScriptViewAssetsPanel({
|
||||
{showAddProp && mounted && createPortal(
|
||||
<div ref={propEditorPopoverRef} className="fixed right-4 bottom-4 z-[80] glass-surface-modal w-[min(24rem,calc(100vw-2rem))] h-[min(560px,calc(100vh-2rem))] p-3 animate-fadeIn flex flex-col shadow-2xl">
|
||||
<div className="shrink-0 text-xs text-[var(--glass-text-tertiary)]">{tCommon('edit')} · 道具</div>
|
||||
<div className="mt-3 flex-1 min-h-0 overflow-y-auto pr-1 custom-scrollbar">
|
||||
<div className="mt-3 flex-1 min-h-0 overflow-y-auto pr-1 app-scrollbar">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{props.map((prop) => {
|
||||
const isSelected = pendingPropIds.has(prop.id)
|
||||
|
||||
@@ -141,7 +141,7 @@ export default function ScriptViewScriptPanel({
|
||||
</div>
|
||||
|
||||
<div className="flex-1 glass-surface-elevated overflow-hidden flex flex-col relative w-full min-h-[300px]">
|
||||
<div className="lg:absolute lg:inset-0 overflow-y-auto p-6 space-y-4 custom-scrollbar">
|
||||
<div className="lg:absolute lg:inset-0 overflow-y-auto p-6 space-y-4 app-scrollbar">
|
||||
{clips.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-[var(--glass-text-tertiary)]">
|
||||
<AppIcon name="fileFold" className="h-10 w-10 mb-2" />
|
||||
|
||||
@@ -95,15 +95,15 @@ export default function ImageEditModal({
|
||||
return (
|
||||
<div className="fixed inset-0 bg-[var(--glass-overlay)] z-50 flex items-center justify-center p-4">
|
||||
<div
|
||||
className="bg-[var(--glass-bg-surface)] rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto"
|
||||
className="bg-[var(--glass-bg-surface)] rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col"
|
||||
onPaste={handlePaste}
|
||||
>
|
||||
<div className="p-6 border-b">
|
||||
<div className="p-6 border-b shrink-0">
|
||||
<h3 className="text-lg font-bold text-[var(--glass-text-primary)]">{t('imageEdit.title')}</h3>
|
||||
<p className="text-sm text-[var(--glass-text-tertiary)] mt-1">{t('imageEdit.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="p-6 space-y-4 overflow-y-auto app-scrollbar flex-1 min-h-0">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--glass-text-secondary)] mb-2">{t('prompts.aiInstruction')}</label>
|
||||
<textarea
|
||||
|
||||
@@ -26,16 +26,16 @@ export default function VideoPromptModal({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-[var(--glass-overlay)] flex items-center justify-center z-50" onClick={onCancel}>
|
||||
<div className="bg-[var(--glass-bg-surface)] rounded-lg max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="bg-[var(--glass-bg-surface)] rounded-lg max-w-2xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col" onClick={(e) => e.stopPropagation()}>
|
||||
{/* 标题栏 */}
|
||||
<div className="sticky top-0 bg-[var(--glass-bg-surface)] border-b px-6 py-4 flex items-center justify-between">
|
||||
<div className="bg-[var(--glass-bg-surface)] border-b px-6 py-4 flex items-center justify-between shrink-0">
|
||||
<h3 className="text-lg font-bold">{t('promptModal.title', { number: panelIndex + 1 })}</h3>
|
||||
<button onClick={onCancel} className="text-[var(--glass-text-tertiary)] hover:text-[var(--glass-text-secondary)]">
|
||||
<AppIcon name="close" className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="p-6 space-y-4 overflow-y-auto app-scrollbar flex-1 min-h-0">
|
||||
{/* 镜头信息 */}
|
||||
<div className="p-3 bg-[var(--glass-bg-muted)] rounded-lg text-sm space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -93,8 +93,8 @@ export function AddLocationModal({ folderId, onClose, onSuccess }: AddLocationMo
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 glass-overlay flex items-center justify-center z-50 p-4">
|
||||
<div className="glass-surface-modal max-w-lg w-full max-h-[85vh] overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<div className="glass-surface-modal max-w-lg w-full max-h-[85vh] overflow-hidden flex flex-col">
|
||||
<div className="p-6 overflow-y-auto app-scrollbar flex-1 min-h-0">
|
||||
{/* 标题 */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--glass-text-primary)]">
|
||||
|
||||
@@ -23,7 +23,7 @@ interface AssetGridProps {
|
||||
isDownloading?: boolean
|
||||
selectedFolderId: string | null
|
||||
onImageClick?: (url: string) => void
|
||||
onImageEdit?: (type: 'character' | 'location', id: string, name: string, imageIndex: number, appearanceIndex?: number) => void
|
||||
onImageEdit?: (type: 'character' | 'location' | 'prop', id: string, name: string, imageIndex: number, appearanceIndex?: number) => void
|
||||
onVoiceDesign?: (characterId: string, characterName: string) => void
|
||||
onCharacterEdit?: (character: unknown, appearance: unknown) => void
|
||||
onLocationEdit?: (location: unknown, imageIndex: number) => void
|
||||
|
||||
@@ -391,14 +391,14 @@ export function CharacterCard({ character, onImageClick, onImageEdit, onVoiceDes
|
||||
<input ref={voiceInputRef} type="file" accept="audio/*" onChange={handleUploadVoice} className="hidden" />
|
||||
|
||||
{/* 图片区域 */}
|
||||
<div className="relative bg-[var(--glass-bg-muted)] min-h-[100px]">
|
||||
<div className="relative aspect-[3/2] bg-[var(--glass-bg-muted)]">
|
||||
{displayImageUrl ? (
|
||||
<>
|
||||
<MediaImageWithLoading
|
||||
src={displayImageUrl}
|
||||
alt={character.name}
|
||||
containerClassName="w-full min-h-[120px]"
|
||||
className="w-full h-auto object-contain cursor-zoom-in"
|
||||
containerClassName="h-full w-full"
|
||||
className="h-full w-full object-contain cursor-zoom-in"
|
||||
onClick={() => onImageClick?.(displayImageUrl)}
|
||||
/>
|
||||
{/* 操作按钮 - 非生成时显示 */}
|
||||
@@ -422,7 +422,7 @@ export function CharacterCard({ character, onImageClick, onImageEdit, onVoiceDes
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-[var(--glass-text-tertiary)]">
|
||||
<div className="flex h-full flex-col items-center justify-center px-4 py-6 text-[var(--glass-text-tertiary)]">
|
||||
<AppIcon name="image" className="w-12 h-12 mb-3" />
|
||||
<ImageGenerationInlineCountButton
|
||||
prefix={<span>{tAssets('image.generateCountPrefix')}</span>}
|
||||
|
||||
@@ -172,8 +172,8 @@ export function CharacterEditModal({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 glass-overlay flex items-center justify-center z-50 p-4">
|
||||
<div className="glass-surface-modal max-w-2xl w-full max-h-[80vh] overflow-y-auto">
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="glass-surface-modal max-w-2xl w-full max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<div className="p-6 space-y-4 overflow-y-auto app-scrollbar flex-1 min-h-0">
|
||||
{/* 标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-[var(--glass-text-primary)]">
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
resolveDisplayImageSlots,
|
||||
} from '@/lib/image-generation/slot-state'
|
||||
import { AppIcon } from '@/components/ui/icons'
|
||||
import { AI_EDIT_BUTTON_CLASS, AI_EDIT_ICON_CLASS } from '@/components/ui/ai-edit-style'
|
||||
import AISparklesIcon from '@/components/ui/icons/AISparklesIcon'
|
||||
|
||||
interface LocationImage {
|
||||
id: string
|
||||
@@ -50,7 +52,7 @@ interface LocationCardProps {
|
||||
location: Location
|
||||
assetType?: 'location' | 'prop'
|
||||
onImageClick?: (url: string) => void
|
||||
onImageEdit?: (type: 'character' | 'location', id: string, name: string, imageIndex: number) => void
|
||||
onImageEdit?: (type: 'character' | 'location' | 'prop', id: string, name: string, imageIndex: number) => void
|
||||
onEdit?: (location: Location, imageIndex: number) => void
|
||||
}
|
||||
|
||||
@@ -98,6 +100,7 @@ export function LocationCard({ location, assetType = 'location', onImageClick, o
|
||||
})
|
||||
const displaySlotCount = displaySelectionImages.length
|
||||
const hasMultipleImages = generatedImageCount > 1
|
||||
const singleImageAspectClassName = assetType === 'prop' ? 'aspect-[3/2]' : 'aspect-square'
|
||||
const displayTaskPresentation = isTaskRunning
|
||||
? resolveTaskPresentationState({
|
||||
phase: 'processing',
|
||||
@@ -380,14 +383,14 @@ export function LocationCard({ location, assetType = 'location', onImageClick, o
|
||||
<input ref={fileInputRef} type="file" accept="image/*" onChange={handleUpload} className="hidden" />
|
||||
|
||||
{/* 图片区域 */}
|
||||
<div className="relative bg-[var(--glass-bg-muted)] min-h-[100px]">
|
||||
<div className={`relative bg-[var(--glass-bg-muted)] ${singleImageAspectClassName}`}>
|
||||
{displayImageUrl ? (
|
||||
<>
|
||||
<MediaImageWithLoading
|
||||
src={displayImageUrl}
|
||||
alt={location.name}
|
||||
containerClassName="w-full min-h-[120px]"
|
||||
className="w-full h-auto object-contain cursor-zoom-in"
|
||||
containerClassName="h-full w-full"
|
||||
className="h-full w-full object-contain cursor-zoom-in"
|
||||
onClick={() => onImageClick?.(displayImageUrl)}
|
||||
/>
|
||||
{/* 操作按钮 - 非生成时显示 */}
|
||||
@@ -396,8 +399,11 @@ export function LocationCard({ location, assetType = 'location', onImageClick, o
|
||||
<button onClick={() => fileInputRef.current?.click()} disabled={uploadImage.isPending} className="glass-btn-base glass-btn-secondary h-7 w-7 rounded-full">
|
||||
<AppIcon name="upload" className="w-4 h-4 text-[var(--glass-tone-success-fg)]" />
|
||||
</button>
|
||||
<button onClick={() => onImageEdit?.('location', location.id, location.name, currentImageIndex)} className="glass-btn-base glass-btn-tone-info h-7 w-7 rounded-full">
|
||||
<AppIcon name="edit" className="w-4 h-4" />
|
||||
<button
|
||||
onClick={() => onImageEdit?.(assetType === 'prop' ? 'prop' : 'location', location.id, location.name, currentImageIndex)}
|
||||
className={`h-7 w-7 rounded-full flex items-center justify-center transition-all active:scale-95 ${AI_EDIT_BUTTON_CLASS}`}
|
||||
>
|
||||
<AISparklesIcon className={`w-4 h-4 ${AI_EDIT_ICON_CLASS}`} />
|
||||
</button>
|
||||
<button onClick={() => handleGenerate()} className="glass-btn-base glass-btn-secondary h-7 w-7 rounded-full">
|
||||
<AppIcon name="refresh" className="w-4 h-4 text-[var(--glass-tone-info-fg)]" />
|
||||
@@ -411,8 +417,8 @@ export function LocationCard({ location, assetType = 'location', onImageClick, o
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-[var(--glass-text-tertiary)]">
|
||||
<AppIcon name="globe2" className="w-12 h-12 mb-3" />
|
||||
<div className="flex h-full flex-col items-center justify-center px-4 py-6 text-[var(--glass-text-tertiary)]">
|
||||
<AppIcon name="image" className="w-12 h-12 mb-3" />
|
||||
<ImageGenerationInlineCountButton
|
||||
prefix={<span>{tAssets('image.generateCountPrefix')}</span>}
|
||||
suffix={<span>{tAssets('image.generateCountSuffix')}</span>}
|
||||
|
||||
@@ -163,8 +163,8 @@ export function LocationEditModal({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 glass-overlay flex items-center justify-center z-50 p-4">
|
||||
<div className="glass-surface-modal max-w-2xl w-full max-h-[80vh] overflow-y-auto">
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="glass-surface-modal max-w-2xl w-full max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<div className="p-6 space-y-4 overflow-y-auto app-scrollbar flex-1 min-h-0">
|
||||
{/* 标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-[var(--glass-text-primary)]">
|
||||
|
||||
@@ -45,6 +45,7 @@ export default function AssetHubPage() {
|
||||
})
|
||||
const characterActions = useAssetActions({ scope: 'global', kind: 'character' })
|
||||
const locationActions = useAssetActions({ scope: 'global', kind: 'location' })
|
||||
const propActions = useAssetActions({ scope: 'global', kind: 'prop' })
|
||||
const refreshAssets = useRefreshAssets({ scope: 'global' })
|
||||
|
||||
const loading = foldersLoading || assetsLoading
|
||||
@@ -58,7 +59,7 @@ export default function AssetHubPage() {
|
||||
const [editingFolder, setEditingFolder] = useState<{ id: string; name: string } | null>(null)
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null)
|
||||
const [imageEditModal, setImageEditModal] = useState<{
|
||||
type: 'character' | 'location'
|
||||
type: 'character' | 'location' | 'prop'
|
||||
id: string
|
||||
name: string
|
||||
imageIndex: number
|
||||
@@ -101,6 +102,7 @@ export default function AssetHubPage() {
|
||||
propId: string
|
||||
propName: string
|
||||
summary: string
|
||||
description: string
|
||||
variantId?: string
|
||||
} | null>(null)
|
||||
|
||||
@@ -159,7 +161,7 @@ export default function AssetHubPage() {
|
||||
}
|
||||
|
||||
// 打开图片编辑弹窗
|
||||
const handleOpenImageEdit = (type: 'character' | 'location', id: string, name: string, imageIndex: number, appearanceIndex?: number) => {
|
||||
const handleOpenImageEdit = (type: 'character' | 'location' | 'prop', id: string, name: string, imageIndex: number, appearanceIndex?: number) => {
|
||||
setImageEditModal({ type, id, name, imageIndex, appearanceIndex })
|
||||
}
|
||||
|
||||
@@ -189,6 +191,15 @@ export default function AssetHubPage() {
|
||||
}).catch(() => {
|
||||
alert(t('editFailed'))
|
||||
})
|
||||
} else if (type === 'prop') {
|
||||
void propActions.modifyRender({
|
||||
id,
|
||||
imageIndex,
|
||||
modifyPrompt,
|
||||
extraImageUrls,
|
||||
}).catch(() => {
|
||||
alert(t('editFailed'))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,13 +301,14 @@ export default function AssetHubPage() {
|
||||
id: string
|
||||
name: string
|
||||
summary: string | null
|
||||
images: Array<{ id: string; imageIndex: number }>
|
||||
images: Array<{ id: string; imageIndex: number; description: string | null }>
|
||||
}
|
||||
const variant = typedProp.images.find((image) => image.imageIndex === imageIndex)
|
||||
setPropEditModal({
|
||||
propId: typedProp.id,
|
||||
propName: typedProp.name,
|
||||
summary: typedProp.summary || '',
|
||||
description: variant?.description || typedProp.summary || '',
|
||||
variantId: variant?.id,
|
||||
})
|
||||
}
|
||||
@@ -615,6 +627,7 @@ export default function AssetHubPage() {
|
||||
propId={propEditModal.propId}
|
||||
propName={propEditModal.propName}
|
||||
summary={propEditModal.summary}
|
||||
description={propEditModal.description}
|
||||
variantId={propEditModal.variantId}
|
||||
onClose={() => setPropEditModal(null)}
|
||||
onRefresh={refreshAssets}
|
||||
|
||||
Reference in New Issue
Block a user