|
|
@@ -14,8 +14,10 @@ import {
|
|
|
getChatbotOwnedItems,
|
|
|
type ChatbotLayoutBackdrop,
|
|
|
type ChatbotLayoutItem,
|
|
|
- type ChatbotBackdropSlot,
|
|
|
getUserUIState,
|
|
|
+ getChatbotFurnitureEquipPreview,
|
|
|
+ type ChatbotBackdropSlot,
|
|
|
+ type ChatbotFurnitureEquipPreviewData,
|
|
|
type ChatbotOwnedItem,
|
|
|
unequipChatbotFurniture,
|
|
|
updateUserUIState,
|
|
|
@@ -71,6 +73,54 @@ const CHATBOT_Z_INDEX = {
|
|
|
GRID_LINES: 1,
|
|
|
GRID_DYNAMIC_MIN: 3,
|
|
|
} as const;
|
|
|
+
|
|
|
+/** 与网格内已放置家具同一套坐标算法,用于装备预览层 */
|
|
|
+function computeChatbotSlotRectPx(
|
|
|
+ slot: ChatbotBackdropSlot,
|
|
|
+ gridOriginX: number,
|
|
|
+ gridOriginY: number,
|
|
|
+ cellWidth: number,
|
|
|
+ cellHeight: number,
|
|
|
+): { leftPx: number; topPx: number; widthPx: number; heightPx: number; zIndex: number } {
|
|
|
+ const slotLeftPx = gridOriginX + (slot.start_col - 1) * cellWidth;
|
|
|
+ const slotTopPx = gridOriginY + (slot.start_row - 1) * cellHeight;
|
|
|
+ const slotWidthPx = slot.size_cols * cellWidth;
|
|
|
+ const slotHeightPx = slot.size_rows * cellHeight;
|
|
|
+ const isPixelSlot = slot.render_mode === 'pixel';
|
|
|
+ const offsetX = slot.offset_x ?? 0;
|
|
|
+ const offsetY = slot.offset_y ?? 0;
|
|
|
+ const widthPx = isPixelSlot ? (slot.pixel_width ?? slotWidthPx) : slotWidthPx;
|
|
|
+ const heightPx = isPixelSlot ? (slot.pixel_height ?? slotHeightPx) : slotHeightPx;
|
|
|
+ const leftPx = isPixelSlot ? slotLeftPx + offsetX : slotLeftPx;
|
|
|
+ const topPx = isPixelSlot ? slotTopPx + offsetY : slotTopPx;
|
|
|
+ /** 高于槽位描边层 (20) 与已放置家具,保证预览可见 */
|
|
|
+ const zIndex = Math.max(slot.z_index ?? 2, CHATBOT_Z_INDEX.GRID_DYNAMIC_MIN, 20) + 10;
|
|
|
+ return { leftPx, topPx, widthPx, heightPx, zIndex };
|
|
|
+}
|
|
|
+
|
|
|
+/** 装备预览时隐藏该槽位上已有家具:优先 slot_code,无则按槽位几何比对(旧布局) */
|
|
|
+function layoutItemOccupiesPreviewSlot(
|
|
|
+ item: ChatbotLayoutItem,
|
|
|
+ previewSlot: ChatbotBackdropSlot,
|
|
|
+): boolean {
|
|
|
+ const code = (previewSlot.slot_code || '').trim();
|
|
|
+ const itemCode = (item.slot_code || '').trim();
|
|
|
+ if (itemCode && code) {
|
|
|
+ return itemCode === code;
|
|
|
+ }
|
|
|
+ return (
|
|
|
+ item.start_row === previewSlot.start_row &&
|
|
|
+ item.start_col === previewSlot.start_col &&
|
|
|
+ item.size_rows === previewSlot.size_rows &&
|
|
|
+ item.size_cols === previewSlot.size_cols &&
|
|
|
+ (item.render_mode ?? 'grid') === (previewSlot.render_mode ?? 'grid') &&
|
|
|
+ (item.offset_x ?? 0) === (previewSlot.offset_x ?? 0) &&
|
|
|
+ (item.offset_y ?? 0) === (previewSlot.offset_y ?? 0) &&
|
|
|
+ (item.pixel_width ?? null) === (previewSlot.pixel_width ?? null) &&
|
|
|
+ (item.pixel_height ?? null) === (previewSlot.pixel_height ?? null)
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
const SLOT_HIGHLIGHT_COLORS = [
|
|
|
'#00FF62',
|
|
|
'#FFB020',
|
|
|
@@ -206,6 +256,9 @@ export const LumiOutfitsPage = () => {
|
|
|
const [activeFurnitureModalItem, setActiveFurnitureModalItem] = useState<ChatbotOwnedItem | null>(null);
|
|
|
const [furnitureModalMode, setFurnitureModalMode] = useState<FurnitureActionModalMode>('equip');
|
|
|
const [furnitureSubmitting, setFurnitureSubmitting] = useState<boolean>(false);
|
|
|
+ const [furnitureEquipPreview, setFurnitureEquipPreview] = useState<ChatbotFurnitureEquipPreviewData | null>(
|
|
|
+ null,
|
|
|
+ );
|
|
|
const [chatbotGrid, setChatbotGrid] = useState(() => ({
|
|
|
grid_rows: 5,
|
|
|
grid_cols: 4,
|
|
|
@@ -278,6 +331,37 @@ export const LumiOutfitsPage = () => {
|
|
|
};
|
|
|
}, [refreshChatbotLayout]);
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
+ if (!activeFurnitureModalItem || furnitureModalMode !== 'equip') {
|
|
|
+ setFurnitureEquipPreview(null);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const isBackdrop =
|
|
|
+ activeFurnitureModalItem.item_type === 'backdrop' ||
|
|
|
+ activeFurnitureModalItem.biz_type === 'wallpaper';
|
|
|
+ if (isBackdrop) {
|
|
|
+ setFurnitureEquipPreview(null);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ let cancelled = false;
|
|
|
+ void ensureSession()
|
|
|
+ .then(({ userId }) => getChatbotFurnitureEquipPreview(userId, activeFurnitureModalItem.item_id))
|
|
|
+ .then((res) => {
|
|
|
+ if (cancelled) return;
|
|
|
+ if (res.success && res.data) {
|
|
|
+ setFurnitureEquipPreview(res.data);
|
|
|
+ } else {
|
|
|
+ setFurnitureEquipPreview(null);
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ if (!cancelled) setFurnitureEquipPreview(null);
|
|
|
+ });
|
|
|
+ return () => {
|
|
|
+ cancelled = true;
|
|
|
+ };
|
|
|
+ }, [activeFurnitureModalItem, furnitureModalMode]);
|
|
|
+
|
|
|
const filteredOwnedFurnitureItems = useMemo(() => {
|
|
|
if (furnitureFilter === 'all') return ownedFurnitureItems;
|
|
|
return ownedFurnitureItems.filter((item) => item.biz_type === furnitureFilter);
|
|
|
@@ -285,6 +369,13 @@ export const LumiOutfitsPage = () => {
|
|
|
const activeBackdropImageName =
|
|
|
chatbotBackdrop?.image_name ?? (chatbotLayoutReady ? defaultBackdropImageName : null);
|
|
|
const activeLayoutItems = chatbotLayoutItems;
|
|
|
+ const activeLayoutItemsForRender = useMemo(() => {
|
|
|
+ if (!activeFurnitureModalItem || furnitureModalMode !== 'equip' || !furnitureEquipPreview) {
|
|
|
+ return activeLayoutItems;
|
|
|
+ }
|
|
|
+ const previewSlot = furnitureEquipPreview.slot;
|
|
|
+ return activeLayoutItems.filter((item) => !layoutItemOccupiesPreviewSlot(item, previewSlot));
|
|
|
+ }, [activeLayoutItems, activeFurnitureModalItem, furnitureModalMode, furnitureEquipPreview]);
|
|
|
const cellWidth = chatbotGrid.cell_width ?? 96;
|
|
|
const cellHeight = chatbotGrid.cell_height ?? 96;
|
|
|
const gridRows = chatbotGrid.grid_rows ?? 5;
|
|
|
@@ -293,6 +384,17 @@ export const LumiOutfitsPage = () => {
|
|
|
/** 5×4 在幕布层内水平居中,槽位相对网格左上角 */
|
|
|
const gridOriginX = 0;
|
|
|
|
|
|
+ const furnitureEquipPreviewRect = useMemo(() => {
|
|
|
+ if (!furnitureEquipPreview) return null;
|
|
|
+ return computeChatbotSlotRectPx(
|
|
|
+ furnitureEquipPreview.slot,
|
|
|
+ gridOriginX,
|
|
|
+ gridOriginY,
|
|
|
+ cellWidth,
|
|
|
+ cellHeight,
|
|
|
+ );
|
|
|
+ }, [furnitureEquipPreview, cellWidth, cellHeight]);
|
|
|
+
|
|
|
const handleSelectFurnitureTab = useCallback(() => {
|
|
|
setLumiBagTab('furniture');
|
|
|
}, []);
|
|
|
@@ -518,7 +620,7 @@ export const LumiOutfitsPage = () => {
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
- {activeLayoutItems.map((item, idx) => {
|
|
|
+ {activeLayoutItemsForRender.map((item, idx) => {
|
|
|
const slotLeftPx = gridOriginX + (item.start_col - 1) * cellWidth;
|
|
|
const slotTopPx = gridOriginY + (item.start_row - 1) * cellHeight;
|
|
|
const slotWidthPx = item.size_cols * cellWidth;
|
|
|
@@ -581,6 +683,45 @@ export const LumiOutfitsPage = () => {
|
|
|
/>
|
|
|
);
|
|
|
})}
|
|
|
+
|
|
|
+ {activeFurnitureModalItem &&
|
|
|
+ furnitureModalMode === 'equip' &&
|
|
|
+ furnitureEquipPreview &&
|
|
|
+ furnitureEquipPreviewRect ? (
|
|
|
+ <>
|
|
|
+ <div
|
|
|
+ className="absolute pointer-events-none"
|
|
|
+ style={{
|
|
|
+ left: px2vmin(furnitureEquipPreviewRect.leftPx),
|
|
|
+ top: px2vmin(furnitureEquipPreviewRect.topPx),
|
|
|
+ width: px2vmin(furnitureEquipPreviewRect.widthPx),
|
|
|
+ height: px2vmin(furnitureEquipPreviewRect.heightPx),
|
|
|
+ border: '2PX dashed rgba(255, 255, 255, 0.7)',
|
|
|
+ boxSizing: 'border-box',
|
|
|
+ zIndex: furnitureEquipPreviewRect.zIndex + 1,
|
|
|
+ borderRadius: px2vmin(6),
|
|
|
+ }}
|
|
|
+ aria-hidden
|
|
|
+ />
|
|
|
+ <img
|
|
|
+ src={getOssUrl(furnitureEquipPreview.preview_image_name)}
|
|
|
+ alt=""
|
|
|
+ className="absolute pointer-events-none"
|
|
|
+ fetchPriority="low"
|
|
|
+ loading="eager"
|
|
|
+ draggable={false}
|
|
|
+ style={{
|
|
|
+ width: px2vmin(furnitureEquipPreviewRect.widthPx),
|
|
|
+ height: px2vmin(furnitureEquipPreviewRect.heightPx),
|
|
|
+ left: px2vmin(furnitureEquipPreviewRect.leftPx),
|
|
|
+ top: px2vmin(furnitureEquipPreviewRect.topPx),
|
|
|
+ zIndex: furnitureEquipPreviewRect.zIndex,
|
|
|
+ objectFit: 'contain',
|
|
|
+ opacity: 0.88,
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </>
|
|
|
+ ) : null}
|
|
|
</div>
|
|
|
</div>
|
|
|
|