|
|
@@ -1,5 +1,5 @@
|
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
-import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
|
|
+import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
import { px2vmin, px2vw } from '@/utils/viewport';
|
|
|
import { getOssUrl } from '@/utils/oss';
|
|
|
@@ -58,6 +58,14 @@ function floorConversationEnergyPrice(item: ChatbotShopCatalogItem): number {
|
|
|
return Math.floor(pe);
|
|
|
}
|
|
|
|
|
|
+/** 判断商品是否已购满(购买数量已达上限) */
|
|
|
+function isPurchaseLimitReached(item: ChatbotShopCatalogItem): boolean {
|
|
|
+ const max = item.max_per_user;
|
|
|
+ const owned = item.owned_qty ?? 0;
|
|
|
+ if (max == null) return false;
|
|
|
+ return owned >= max;
|
|
|
+}
|
|
|
+
|
|
|
type FurnitureFilterKey =
|
|
|
| 'all'
|
|
|
| 'table_cabinet'
|
|
|
@@ -138,19 +146,8 @@ function layoutItemOccupiesPreviewSlot(
|
|
|
|
|
|
type FurnitureActionModalMode = 'equip' | 'remove';
|
|
|
|
|
|
-/** 仅允许应用内相对路径,避免 open redirect */
|
|
|
-function parseShopReturnPath(state: unknown): string | null {
|
|
|
- if (!state || typeof state !== 'object') return null;
|
|
|
- const from = (state as { from?: unknown }).from;
|
|
|
- if (typeof from !== 'string' || from.length === 0 || from.length > 512) return null;
|
|
|
- if (!from.startsWith('/') || from.startsWith('//')) return null;
|
|
|
- if (from.includes('://')) return null;
|
|
|
- return from.split('#')[0] || null;
|
|
|
-}
|
|
|
-
|
|
|
export const LumiShopPage = () => {
|
|
|
const navigate = useNavigate();
|
|
|
- const location = useLocation();
|
|
|
const [searchParams] = useSearchParams();
|
|
|
const { t, i18n } = useTranslation('common');
|
|
|
const { t: tProfile } = useTranslation('profile');
|
|
|
@@ -164,13 +161,8 @@ export const LumiShopPage = () => {
|
|
|
}, []);
|
|
|
|
|
|
const handleBack = useCallback(() => {
|
|
|
- const target = parseShopReturnPath(location.state);
|
|
|
- if (target) {
|
|
|
- navigate(target);
|
|
|
- return;
|
|
|
- }
|
|
|
- navigate(-1);
|
|
|
- }, [navigate, location.state]);
|
|
|
+ navigate('/chatbot');
|
|
|
+ }, [navigate]);
|
|
|
|
|
|
const onBackClick = useMemo(
|
|
|
() => wrapTrackedClick('lumi_shop', 'back', handleBack),
|
|
|
@@ -341,19 +333,19 @@ export const LumiShopPage = () => {
|
|
|
}, [refreshChatbotLayout]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
- if (!activePurchaseItem) {
|
|
|
+ const item = activePurchaseItem || activeFurnitureModalItem;
|
|
|
+ if (!item) {
|
|
|
setFurnitureEquipPreview(null);
|
|
|
return;
|
|
|
}
|
|
|
- const isBackdrop =
|
|
|
- activePurchaseItem.item_type === 'backdrop' || activePurchaseItem.biz_type === 'wallpaper';
|
|
|
+ const isBackdrop = item.item_type === 'backdrop' || item.biz_type === 'wallpaper';
|
|
|
if (isBackdrop) {
|
|
|
setFurnitureEquipPreview(null);
|
|
|
return;
|
|
|
}
|
|
|
let cancelled = false;
|
|
|
void ensureSession()
|
|
|
- .then(({ userId }) => getChatbotFurnitureEquipPreview(userId, activePurchaseItem.item_id))
|
|
|
+ .then(({ userId }) => getChatbotFurnitureEquipPreview(userId, item.item_id))
|
|
|
.then((res) => {
|
|
|
if (cancelled) return;
|
|
|
if (res.success && res.data) {
|
|
|
@@ -368,22 +360,35 @@ export const LumiShopPage = () => {
|
|
|
return () => {
|
|
|
cancelled = true;
|
|
|
};
|
|
|
- }, [activePurchaseItem]);
|
|
|
+ }, [activePurchaseItem, activeFurnitureModalItem]);
|
|
|
|
|
|
const filteredFurnitureCatalogItems = useMemo(() => {
|
|
|
const paidOnly = furnitureCatalogItems.filter((item) => floorConversationEnergyPrice(item) > 0);
|
|
|
- if (furnitureFilter === 'all') return paidOnly;
|
|
|
- return paidOnly.filter((item) => item.biz_type === furnitureFilter);
|
|
|
+ let filtered = furnitureFilter === 'all' ? paidOnly : paidOnly.filter((item) => item.biz_type === furnitureFilter);
|
|
|
+
|
|
|
+ // 排序规则:
|
|
|
+ // 1. 可再次购买的商品(未购满)排在前面
|
|
|
+ // 2. 已购满的商品(无法再购买)排在后面
|
|
|
+ return filtered.sort((a, b) => {
|
|
|
+ const aMaxed = isPurchaseLimitReached(a);
|
|
|
+ const bMaxed = isPurchaseLimitReached(b);
|
|
|
+ if (aMaxed && !bMaxed) return 1;
|
|
|
+ if (!aMaxed && bMaxed) return -1;
|
|
|
+ return 0;
|
|
|
+ });
|
|
|
}, [furnitureFilter, furnitureCatalogItems]);
|
|
|
const activeBackdropImageName =
|
|
|
chatbotBackdrop?.image_name ?? (chatbotLayoutReady ? defaultBackdropImageName : null);
|
|
|
const activeLayoutItems = chatbotLayoutItems;
|
|
|
const activeLayoutItemsForRender = useMemo(() => {
|
|
|
- if (!activePurchaseItem || !furnitureEquipPreview) {
|
|
|
+ const preview =
|
|
|
+ furnitureEquipPreview &&
|
|
|
+ (activePurchaseItem || (activeFurnitureModalItem && furnitureModalMode === 'equip'));
|
|
|
+ if (!preview) {
|
|
|
return activeLayoutItems;
|
|
|
}
|
|
|
return activeLayoutItems.filter((item) => !layoutItemOccupiesPreviewSlot(item, furnitureEquipPreview.slot));
|
|
|
- }, [activeLayoutItems, activePurchaseItem, furnitureEquipPreview]);
|
|
|
+ }, [activeLayoutItems, activePurchaseItem, activeFurnitureModalItem, furnitureModalMode, furnitureEquipPreview]);
|
|
|
const cellWidth = chatbotGrid.cell_width ?? 96;
|
|
|
const cellHeight = chatbotGrid.cell_height ?? 96;
|
|
|
const gridCols = chatbotGrid.grid_cols ?? 4;
|
|
|
@@ -654,17 +659,10 @@ export const LumiShopPage = () => {
|
|
|
setActivePurchaseItem(item);
|
|
|
return;
|
|
|
}
|
|
|
- if (repeatable && !buyMore) {
|
|
|
- showFurnitureNotice(tProfile('lumiShopPurchaseLimitReached'));
|
|
|
- return;
|
|
|
- }
|
|
|
|
|
|
- /**
|
|
|
- * shop_items.max_per_user 有值时:实际拥有数 owned_qty ≥ 上限则无法再购,
|
|
|
- * 仅提示(含幕布/墙纸),不打开装备弹窗或切换幕布。max 为 null 表示不限购。
|
|
|
- */
|
|
|
- if (item.is_owned && max != null && owned >= max) {
|
|
|
- showFurnitureNotice(tProfile('lumiShopLimitedOwnedHint'));
|
|
|
+ // 墙纸/幕布特殊处理:已装备则提示,未装备则直接切换
|
|
|
+ if (isBackdrop && item.is_in_use) {
|
|
|
+ showFurnitureNotice(tProfile('lumiWallpaperFixedNotice'));
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -678,6 +676,7 @@ export const LumiShopPage = () => {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // 其他已拥有商品:打开装备/卸下弹窗
|
|
|
setActiveFurnitureModalItem(item);
|
|
|
setFurnitureModalMode(item.is_in_use ? 'remove' : 'equip');
|
|
|
},
|
|
|
@@ -850,7 +849,9 @@ export const LumiShopPage = () => {
|
|
|
/>
|
|
|
);
|
|
|
})}
|
|
|
- {activePurchaseItem && furnitureEquipPreview && furnitureEquipPreviewRect ? (
|
|
|
+ {((activePurchaseItem || (activeFurnitureModalItem && furnitureModalMode === 'equip')) &&
|
|
|
+ furnitureEquipPreview &&
|
|
|
+ furnitureEquipPreviewRect) ? (
|
|
|
<img
|
|
|
src={getOssUrl(furnitureEquipPreview.preview_image_name)}
|
|
|
alt=""
|
|
|
@@ -1451,40 +1452,65 @@ export const LumiShopPage = () => {
|
|
|
pointerEvents: 'none',
|
|
|
}}
|
|
|
/>
|
|
|
- <div
|
|
|
- style={{
|
|
|
- position: 'relative',
|
|
|
- width: px2vmin(70),
|
|
|
- height: px2vmin(20),
|
|
|
- flexShrink: 0,
|
|
|
- pointerEvents: 'none',
|
|
|
- }}
|
|
|
- >
|
|
|
- <img
|
|
|
- src={conversationEnergyStoneCount}
|
|
|
- alt=""
|
|
|
- draggable={false}
|
|
|
+ {item.is_owned && isPurchaseLimitReached(item) ? (
|
|
|
+ <div
|
|
|
style={{
|
|
|
- width: '100%',
|
|
|
- height: '100%',
|
|
|
- objectFit: 'contain',
|
|
|
- display: 'block',
|
|
|
+ width: px2vmin(50),
|
|
|
+ height: px2vmin(20),
|
|
|
+ backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
|
|
+ borderRadius: px2vmin(20),
|
|
|
+ display: 'flex',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ flexShrink: 0,
|
|
|
}}
|
|
|
- />
|
|
|
- <span
|
|
|
+ >
|
|
|
+ <span
|
|
|
+ style={{
|
|
|
+ fontSize: px2vmin(12),
|
|
|
+ color: '#fff',
|
|
|
+ fontWeight: 400,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {isZh ? '已拥有' : 'Owned'}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div
|
|
|
style={{
|
|
|
- position: 'absolute',
|
|
|
- top: '50%',
|
|
|
- left: '60%',
|
|
|
- transform: 'translate(-50%, -50%)',
|
|
|
- fontSize: px2vmin(10),
|
|
|
- color: '#000',
|
|
|
- fontWeight: 500,
|
|
|
+ position: 'relative',
|
|
|
+ width: px2vmin(70),
|
|
|
+ height: px2vmin(20),
|
|
|
+ flexShrink: 0,
|
|
|
+ pointerEvents: 'none',
|
|
|
}}
|
|
|
>
|
|
|
- {formatCatalogEnergyCount(item)}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
+ <img
|
|
|
+ src={conversationEnergyStoneCount}
|
|
|
+ alt=""
|
|
|
+ draggable={false}
|
|
|
+ style={{
|
|
|
+ width: '100%',
|
|
|
+ height: '100%',
|
|
|
+ objectFit: 'contain',
|
|
|
+ display: 'block',
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <span
|
|
|
+ style={{
|
|
|
+ position: 'absolute',
|
|
|
+ top: '50%',
|
|
|
+ left: '60%',
|
|
|
+ transform: 'translate(-50%, -50%)',
|
|
|
+ fontSize: px2vmin(10),
|
|
|
+ color: '#000',
|
|
|
+ fontWeight: 500,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {formatCatalogEnergyCount(item)}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
</button>
|
|
|
))}
|
|
|
</div>
|