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:
saturn
2026-04-03 22:36:41 +08:00
parent 854b932e67
commit 78b93331b4
136 changed files with 3393 additions and 875 deletions

View File

@@ -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}
/>

View File

@@ -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" />

View File

@@ -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)]">

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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")}

View File

@@ -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"

View File

@@ -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}

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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

View File

@@ -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

View File

@@ -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')

View File

@@ -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
}

View File

@@ -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>
)}

View File

@@ -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)

View File

@@ -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" />

View File

@@ -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

View File

@@ -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">

View File

@@ -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)]">

View File

@@ -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

View File

@@ -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>}

View File

@@ -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)]">

View File

@@ -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>}

View File

@@ -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)]">

View File

@@ -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}