|
|
@@ -9,6 +9,7 @@ import {
|
|
|
ApiError,
|
|
|
ensureSession,
|
|
|
equipChatbotFurniture,
|
|
|
+ getChatbotFurnitureEquipPreview,
|
|
|
getChatbotLayout,
|
|
|
getUserInfo,
|
|
|
getChatbotShopCatalog,
|
|
|
@@ -17,6 +18,8 @@ import {
|
|
|
removeChatbotShopCartItem,
|
|
|
purchaseChatbotShopCartItems,
|
|
|
purchaseChatbotShopItem,
|
|
|
+ type ChatbotBackdropSlot,
|
|
|
+ type ChatbotFurnitureEquipPreviewData,
|
|
|
type ChatbotLayoutBackdrop,
|
|
|
type ChatbotLayoutItem,
|
|
|
type ChatbotShopCatalogItem,
|
|
|
@@ -82,6 +85,57 @@ const CHATBOT_Z_INDEX = {
|
|
|
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;
|
|
|
+ /**
|
|
|
+ * 预览层级与实际已摆放家具保持一致,避免跨槽位错误遮挡:
|
|
|
+ * - 同槽位已摆放家具会在渲染时被过滤掉;
|
|
|
+ * - 其他槽位按数据库 z_index 正常前后关系显示。
|
|
|
+ */
|
|
|
+ const zIndex = Math.max(slot.z_index ?? 0, CHATBOT_Z_INDEX.GRID_DYNAMIC_MIN) + 1;
|
|
|
+ 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)
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
type FurnitureActionModalMode = 'equip' | 'remove';
|
|
|
|
|
|
/** 仅允许应用内相对路径,避免 open redirect */
|
|
|
@@ -185,6 +239,9 @@ export const LumiShopPage = () => {
|
|
|
const [activeFurnitureModalItem, setActiveFurnitureModalItem] = useState<ChatbotShopCatalogItem | 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,
|
|
|
@@ -283,6 +340,36 @@ export const LumiShopPage = () => {
|
|
|
};
|
|
|
}, [refreshChatbotLayout]);
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
+ if (!activePurchaseItem) {
|
|
|
+ setFurnitureEquipPreview(null);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const isBackdrop =
|
|
|
+ activePurchaseItem.item_type === 'backdrop' || activePurchaseItem.biz_type === 'wallpaper';
|
|
|
+ if (isBackdrop) {
|
|
|
+ setFurnitureEquipPreview(null);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ let cancelled = false;
|
|
|
+ void ensureSession()
|
|
|
+ .then(({ userId }) => getChatbotFurnitureEquipPreview(userId, activePurchaseItem.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;
|
|
|
+ };
|
|
|
+ }, [activePurchaseItem]);
|
|
|
+
|
|
|
const filteredFurnitureCatalogItems = useMemo(() => {
|
|
|
const paidOnly = furnitureCatalogItems.filter((item) => floorConversationEnergyPrice(item) > 0);
|
|
|
if (furnitureFilter === 'all') return paidOnly;
|
|
|
@@ -291,12 +378,28 @@ export const LumiShopPage = () => {
|
|
|
const activeBackdropImageName =
|
|
|
chatbotBackdrop?.image_name ?? (chatbotLayoutReady ? defaultBackdropImageName : null);
|
|
|
const activeLayoutItems = chatbotLayoutItems;
|
|
|
+ const activeLayoutItemsForRender = useMemo(() => {
|
|
|
+ if (!activePurchaseItem || !furnitureEquipPreview) {
|
|
|
+ return activeLayoutItems;
|
|
|
+ }
|
|
|
+ return activeLayoutItems.filter((item) => !layoutItemOccupiesPreviewSlot(item, furnitureEquipPreview.slot));
|
|
|
+ }, [activeLayoutItems, activePurchaseItem, furnitureEquipPreview]);
|
|
|
const cellWidth = chatbotGrid.cell_width ?? 96;
|
|
|
const cellHeight = chatbotGrid.cell_height ?? 96;
|
|
|
const gridCols = chatbotGrid.grid_cols ?? 4;
|
|
|
const gridOriginY = 0;
|
|
|
/** 5×4 区整体在幕布层水平居中,槽位坐标相对网格左上角 */
|
|
|
const gridOriginX = 0;
|
|
|
+ const furnitureEquipPreviewRect = useMemo(() => {
|
|
|
+ if (!furnitureEquipPreview) return null;
|
|
|
+ return computeChatbotSlotRectPx(
|
|
|
+ furnitureEquipPreview.slot,
|
|
|
+ gridOriginX,
|
|
|
+ gridOriginY,
|
|
|
+ cellWidth,
|
|
|
+ cellHeight,
|
|
|
+ );
|
|
|
+ }, [furnitureEquipPreview, gridOriginX, gridOriginY, cellWidth, cellHeight]);
|
|
|
|
|
|
const handleSelectFurnitureTab = useCallback(() => {
|
|
|
setLumiBagTab('furniture');
|
|
|
@@ -714,7 +817,7 @@ export const LumiShopPage = () => {
|
|
|
zIndex: 1,
|
|
|
}}
|
|
|
>
|
|
|
- {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;
|
|
|
@@ -747,6 +850,25 @@ export const LumiShopPage = () => {
|
|
|
/>
|
|
|
);
|
|
|
})}
|
|
|
+ {activePurchaseItem && furnitureEquipPreview && furnitureEquipPreviewRect ? (
|
|
|
+ <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>
|
|
|
|