refactor: 收口推荐滑动卡模型
This commit is contained in:
@@ -1249,6 +1249,14 @@
|
|||||||
- 验证方式:`npm run test -- src/components/rpg-entry/rpgEntryPublicGalleryViewModel.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "recommend|edutainment"`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "logged out home recommendation next starts the next puzzle work"`、针对变更文件执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
- 验证方式:`npm run test -- src/components/rpg-entry/rpgEntryPublicGalleryViewModel.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "recommend|edutainment"`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "logged out home recommendation next starts the next puzzle work"`、针对变更文件执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
||||||
- 关联文档:`docs/technical/【前端架构】RecommendFeedViewModel收口计划-2026-06-03.md`。
|
- 关联文档:`docs/technical/【前端架构】RecommendFeedViewModel收口计划-2026-06-03.md`。
|
||||||
|
|
||||||
|
## 2026-06-03 Recommend Swipe Deck Model 收口
|
||||||
|
|
||||||
|
- 背景:移动端推荐首页 swipe deck 的拖拽阈值、offset clamp、commit 方向、rail class 和分享文案仍留在 `RpgEntryHomeView.tsx` 页面 Implementation 内,页面同时承载 DOM pointer 副作用和纯规则。
|
||||||
|
- 决策:新增 `src/components/rpg-entry/rpgEntryRecommendSwipeDeckModel.ts` 作为 Recommend Swipe Deck Module,Interface 收口 `hasRecommendDragStarted`、`clampRecommendDragOffset`、`resolveRecommendDragCommitDirection`、`resolveRecommendCommitOffset`、`buildRecommendSwipeRailClassName`、`shouldAnimateRecommendSwipe` 与 `buildRecommendShareText`;页面仅保留 pointer capture、DOM 高度读取、动画 timer、clipboard 与 like/remix/open 副作用 Adapter。
|
||||||
|
- 影响范围:移动端推荐首页 swipe 手势、上一条 / 下一条动画、推荐分享文案与未登录时的直接切换行为。
|
||||||
|
- 验证方式:`npm run test -- src/components/rpg-entry/rpgEntryRecommendSwipeDeckModel.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "recommend|edutainment"`、`npm run test -- src/components/rpg-entry/rpgEntryPublicGalleryViewModel.test.ts -t "recommend"`、针对新 Module 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
||||||
|
- 关联文档:`docs/technical/【前端架构】RecommendSwipeDeckModel收口计划-2026-06-03.md`。
|
||||||
|
|
||||||
## 2026-06-03 Ranking ViewModel 收口
|
## 2026-06-03 Ranking ViewModel 收口
|
||||||
|
|
||||||
- 背景:排行 tab 的文案、metric label 与空态文案在 `RpgEntryHomeView.tsx`,排序和 metric value 在 `rpgEntryPublicGalleryViewModel.ts`,同一 `PlatformRankingTab` 的 Interface 分散且页面需要类型断言取 active config。
|
- 背景:排行 tab 的文案、metric label 与空态文案在 `RpgEntryHomeView.tsx`,排序和 metric value 在 `rpgEntryPublicGalleryViewModel.ts`,同一 `PlatformRankingTab` 的 Interface 分散且页面需要类型断言取 active config。
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_
|
|||||||
|
|
||||||
推荐 feed 的公开作品去重、普通内容过滤、active 窗口与上一条 / 下一条回环选择也收口到 `src/components/rpg-entry/rpgEntryPublicGalleryViewModel.ts`,规则见 [【前端架构】RecommendFeedViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91RecommendFeedViewModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
推荐 feed 的公开作品去重、普通内容过滤、active 窗口与上一条 / 下一条回环选择也收口到 `src/components/rpg-entry/rpgEntryPublicGalleryViewModel.ts`,规则见 [【前端架构】RecommendFeedViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91RecommendFeedViewModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|
||||||
|
移动端推荐首页 swipe deck 的拖拽阈值、offset clamp、commit 方向、rail class 和分享文案收口到 `src/components/rpg-entry/rpgEntryRecommendSwipeDeckModel.ts`,规则见 [【前端架构】RecommendSwipeDeckModel收口计划-2026-06-03.md](./technical/【前端架构】RecommendSwipeDeckModel收口计划-2026-06-03.md)。
|
||||||
|
|
||||||
排行频道的默认 tab、tab 文案、空态文案、排序字段与指标 label/value 收口到 `src/components/rpg-entry/rpgEntryPublicGalleryViewModel.ts`,规则见 [【前端架构】RankingViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91RankingViewModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
排行频道的默认 tab、tab 文案、空态文案、排序字段与指标 label/value 收口到 `src/components/rpg-entry/rpgEntryPublicGalleryViewModel.ts`,规则见 [【前端架构】RankingViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91RankingViewModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|
||||||
每日任务卡片与任务中心弹窗的任务选择、进度、状态标签和按钮文案收口到 `src/components/rpg-entry/rpgEntryProfileTaskViewModel.ts`,规则见 [【前端架构】ProfileTaskViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91ProfileTaskViewModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
每日任务卡片与任务中心弹窗的任务选择、进度、状态标签和按钮文案收口到 `src/components/rpg-entry/rpgEntryProfileTaskViewModel.ts`,规则见 [【前端架构】ProfileTaskViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91ProfileTaskViewModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# RecommendSwipeDeckModel 收口计划
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
移动端推荐首页的纵向 swipe deck 曾把拖拽阈值、offset clamp、commit 方向、rail class 和分享文案直接放在 `RpgEntryHomeView.tsx` Implementation 内。页面因此同时理解 DOM pointer 副作用、动画副作用与推荐卡纯规则,后续调整手势阈值或分享文案时缺少稳定测试面。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
- 新增 `src/components/rpg-entry/rpgEntryRecommendSwipeDeckModel.ts` 作为 Recommend Swipe Deck Module。
|
||||||
|
- Module Interface 收口:
|
||||||
|
- `hasRecommendDragStarted`
|
||||||
|
- `clampRecommendDragOffset`
|
||||||
|
- `resolveRecommendDragCommitDirection`
|
||||||
|
- `resolveRecommendCommitOffset`
|
||||||
|
- `buildRecommendSwipeRailClassName`
|
||||||
|
- `shouldAnimateRecommendSwipe`
|
||||||
|
- `buildRecommendShareText`
|
||||||
|
- `RpgEntryHomeView.tsx` 保留 pointer capture、DOM 高度读取、`setTimeout`、clipboard、like/remix/open 等副作用 Adapter;推荐卡纯规则不再散落在页面 Implementation 内。
|
||||||
|
|
||||||
|
## Interface 约束
|
||||||
|
|
||||||
|
- swipe 阈值、commit 动画时长和 drag fallback limit 只从 Module 导出,不在页面重复定义。
|
||||||
|
- `deltaY < 0` 表示上滑进入下一条,返回方向 `1`;`deltaY > 0` 表示下滑进入上一条,返回方向 `-1`。
|
||||||
|
- 未达到 commit 阈值时必须返回 `null`,页面 Adapter 只负责把 offset 归零。
|
||||||
|
- rail class 仅由 `offsetY` 与 `commitDirection` 决定,CSS class 名保持现有命名。
|
||||||
|
- 分享文案只使用公开作品名、作品号和详情 URL;公开作品码解析与复制副作用仍在页面 Adapter。
|
||||||
|
|
||||||
|
## Depth / Leverage / Locality
|
||||||
|
|
||||||
|
- **Depth**:页面传入少量数值或公开作品身份,即可得到拖拽状态、提交方向、动画 class 和分享文案。
|
||||||
|
- **Leverage**:调整推荐 swipe 体验时只需改 Module 与单测,交互测试仍护页面 Adapter。
|
||||||
|
- **Locality**:pointer 事件生命周期与纯规则分离,推荐卡手势和分享规则集中到一个小 Module。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
|
||||||
|
- `npm run test -- src/components/rpg-entry/rpgEntryRecommendSwipeDeckModel.test.ts`
|
||||||
|
- `npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "recommend|edutainment"`
|
||||||
|
- `npm run test -- src/components/rpg-entry/rpgEntryPublicGalleryViewModel.test.ts -t "recommend"`
|
||||||
|
- `npx eslint src/components/rpg-entry/rpgEntryRecommendSwipeDeckModel.ts src/components/rpg-entry/rpgEntryRecommendSwipeDeckModel.test.ts --max-warnings 0`
|
||||||
|
- `npx eslint src/components/rpg-entry/RpgEntryHomeView.tsx --quiet`
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run check:encoding`
|
||||||
@@ -189,6 +189,17 @@ import {
|
|||||||
selectPlatformRecommendFeedWindow,
|
selectPlatformRecommendFeedWindow,
|
||||||
sortPlatformCategoryEntries,
|
sortPlatformCategoryEntries,
|
||||||
} from './rpgEntryPublicGalleryViewModel';
|
} from './rpgEntryPublicGalleryViewModel';
|
||||||
|
import {
|
||||||
|
buildRecommendShareText,
|
||||||
|
buildRecommendSwipeRailClassName,
|
||||||
|
clampRecommendDragOffset,
|
||||||
|
hasRecommendDragStarted,
|
||||||
|
RECOMMEND_ENTRY_COMMIT_ANIMATION_MS,
|
||||||
|
type RecommendSwipeDirection,
|
||||||
|
resolveRecommendCommitOffset,
|
||||||
|
resolveRecommendDragCommitDirection,
|
||||||
|
shouldAnimateRecommendSwipe,
|
||||||
|
} from './rpgEntryRecommendSwipeDeckModel';
|
||||||
import {
|
import {
|
||||||
buildPlatformWorldDisplayTags,
|
buildPlatformWorldDisplayTags,
|
||||||
describePlatformPublicWorkKind,
|
describePlatformPublicWorkKind,
|
||||||
@@ -305,9 +316,6 @@ const AVATAR_OUTPUT_SIZE = 256;
|
|||||||
const AVATAR_ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
const AVATAR_ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
||||||
const PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS = 4200;
|
const PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS = 4200;
|
||||||
const PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code'] as const;
|
const PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code'] as const;
|
||||||
const RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX = 36;
|
|
||||||
const RECOMMEND_ENTRY_COMMIT_ANIMATION_MS = 180;
|
|
||||||
const RECOMMEND_ENTRY_DRAG_LIMIT_PX = 160;
|
|
||||||
const WECHAT_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
|
const WECHAT_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
|
||||||
const WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS = [800, 1600, 3000] as const;
|
const WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS = [800, 1600, 3000] as const;
|
||||||
const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180;
|
const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180;
|
||||||
@@ -4746,7 +4754,7 @@ export function RpgEntryHomeView({
|
|||||||
const nextRecommendEntry = recommendFeedWindow.nextEntry;
|
const nextRecommendEntry = recommendFeedWindow.nextEntry;
|
||||||
const [recommendDragOffsetY, setRecommendDragOffsetY] = useState(0);
|
const [recommendDragOffsetY, setRecommendDragOffsetY] = useState(0);
|
||||||
const [recommendDragCommitDirection, setRecommendDragCommitDirection] =
|
const [recommendDragCommitDirection, setRecommendDragCommitDirection] =
|
||||||
useState<1 | -1 | null>(null);
|
useState<RecommendSwipeDirection | null>(null);
|
||||||
const [recommendShareState, setRecommendShareState] = useState<
|
const [recommendShareState, setRecommendShareState] = useState<
|
||||||
'idle' | 'copied' | 'failed'
|
'idle' | 'copied' | 'failed'
|
||||||
>('idle');
|
>('idle');
|
||||||
@@ -4759,7 +4767,7 @@ export function RpgEntryHomeView({
|
|||||||
dragging: boolean;
|
dragging: boolean;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const commitRecommendDrag = useCallback(
|
const commitRecommendDrag = useCallback(
|
||||||
(direction: 1 | -1) => {
|
(direction: RecommendSwipeDirection) => {
|
||||||
if (recommendDragCommitDirection) {
|
if (recommendDragCommitDirection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4767,9 +4775,8 @@ export function RpgEntryHomeView({
|
|||||||
setRecommendDragCommitDirection(direction);
|
setRecommendDragCommitDirection(direction);
|
||||||
const panelHeight =
|
const panelHeight =
|
||||||
recommendCardStageRef.current?.getBoundingClientRect().height ?? 0;
|
recommendCardStageRef.current?.getBoundingClientRect().height ?? 0;
|
||||||
const commitDistance = panelHeight > 0 ? panelHeight : window.innerHeight;
|
|
||||||
setRecommendDragOffsetY(
|
setRecommendDragOffsetY(
|
||||||
direction === 1 ? -commitDistance : commitDistance,
|
resolveRecommendCommitOffset(direction, panelHeight, window.innerHeight),
|
||||||
);
|
);
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
if (direction === 1) {
|
if (direction === 1) {
|
||||||
@@ -4818,9 +4825,7 @@ export function RpgEntryHomeView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deltaY = event.clientY - drag.startY;
|
const deltaY = event.clientY - drag.startY;
|
||||||
drag.dragging =
|
drag.dragging = drag.dragging || hasRecommendDragStarted(deltaY);
|
||||||
drag.dragging ||
|
|
||||||
Math.abs(deltaY) >= RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX / 2;
|
|
||||||
if (!drag.dragging) {
|
if (!drag.dragging) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4828,9 +4833,7 @@ export function RpgEntryHomeView({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const cardHeight =
|
const cardHeight =
|
||||||
recommendCardStageRef.current?.getBoundingClientRect().height ?? 0;
|
recommendCardStageRef.current?.getBoundingClientRect().height ?? 0;
|
||||||
const dragLimit =
|
setRecommendDragOffsetY(clampRecommendDragOffset(deltaY, cardHeight));
|
||||||
cardHeight > 0 ? cardHeight : RECOMMEND_ENTRY_DRAG_LIMIT_PX;
|
|
||||||
setRecommendDragOffsetY(Math.max(-dragLimit, Math.min(dragLimit, deltaY)));
|
|
||||||
}, []);
|
}, []);
|
||||||
const endRecommendDrag = useCallback(
|
const endRecommendDrag = useCallback(
|
||||||
(event: PointerEvent<HTMLElement>) => {
|
(event: PointerEvent<HTMLElement>) => {
|
||||||
@@ -4842,12 +4845,13 @@ export function RpgEntryHomeView({
|
|||||||
event.currentTarget.releasePointerCapture?.(drag.pointerId);
|
event.currentTarget.releasePointerCapture?.(drag.pointerId);
|
||||||
recommendDragStartRef.current = null;
|
recommendDragStartRef.current = null;
|
||||||
const deltaY = event.clientY - drag.startY;
|
const deltaY = event.clientY - drag.startY;
|
||||||
if (Math.abs(deltaY) < RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX) {
|
const commitDirection = resolveRecommendDragCommitDirection(deltaY);
|
||||||
|
if (!commitDirection) {
|
||||||
setRecommendDragOffsetY(0);
|
setRecommendDragOffsetY(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
commitRecommendDrag(deltaY < 0 ? 1 : -1);
|
commitRecommendDrag(commitDirection);
|
||||||
},
|
},
|
||||||
[commitRecommendDrag],
|
[commitRecommendDrag],
|
||||||
);
|
);
|
||||||
@@ -4865,16 +4869,17 @@ export function RpgEntryHomeView({
|
|||||||
const recommendRailStyle = {
|
const recommendRailStyle = {
|
||||||
transform: `translate3d(0, ${recommendDragOffsetY}px, 0)`,
|
transform: `translate3d(0, ${recommendDragOffsetY}px, 0)`,
|
||||||
} satisfies CSSProperties;
|
} satisfies CSSProperties;
|
||||||
const recommendRailClassName = recommendDragCommitDirection
|
const recommendRailClassName = buildRecommendSwipeRailClassName({
|
||||||
? 'platform-recommend-swipe-rail--committing'
|
offsetY: recommendDragOffsetY,
|
||||||
: recommendDragOffsetY === 0
|
commitDirection: recommendDragCommitDirection,
|
||||||
? 'platform-recommend-swipe-rail--settled'
|
});
|
||||||
: 'platform-recommend-swipe-rail--dragging';
|
|
||||||
const selectNextRecommendEntry = useCallback(() => {
|
const selectNextRecommendEntry = useCallback(() => {
|
||||||
if (
|
if (
|
||||||
isAuthenticated &&
|
shouldAnimateRecommendSwipe({
|
||||||
activeRecommendEntry &&
|
isAuthenticated,
|
||||||
recommendedFeedEntries.length > 1
|
hasActiveEntry: Boolean(activeRecommendEntry),
|
||||||
|
entryCount: recommendedFeedEntries.length,
|
||||||
|
})
|
||||||
) {
|
) {
|
||||||
commitRecommendDrag(1);
|
commitRecommendDrag(1);
|
||||||
return;
|
return;
|
||||||
@@ -4908,7 +4913,11 @@ export function RpgEntryHomeView({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareText = `邀请你来玩《${entry.worldName}》\n作品号:${publicWorkCode}\n${buildPublicWorkDetailUrl(publicWorkCode)}`;
|
const shareText = buildRecommendShareText({
|
||||||
|
entry,
|
||||||
|
publicWorkCode,
|
||||||
|
detailUrl: buildPublicWorkDetailUrl(publicWorkCode),
|
||||||
|
});
|
||||||
void copyTextToClipboard(shareText).then((copied) => {
|
void copyTextToClipboard(shareText).then((copied) => {
|
||||||
setRecommendShareState(copied ? 'copied' : 'failed');
|
setRecommendShareState(copied ? 'copied' : 'failed');
|
||||||
if (recommendShareResetTimerRef.current !== null) {
|
if (recommendShareResetTimerRef.current !== null) {
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildRecommendShareText,
|
||||||
|
buildRecommendSwipeRailClassName,
|
||||||
|
clampRecommendDragOffset,
|
||||||
|
hasRecommendDragStarted,
|
||||||
|
resolveRecommendCommitOffset,
|
||||||
|
resolveRecommendDragCommitDirection,
|
||||||
|
shouldAnimateRecommendSwipe,
|
||||||
|
} from './rpgEntryRecommendSwipeDeckModel';
|
||||||
|
import type { PlatformPuzzleGalleryCard } from './rpgEntryWorldPresentation';
|
||||||
|
|
||||||
|
describe('rpgEntryRecommendSwipeDeckModel', () => {
|
||||||
|
test('detects drag start and clamps offset to the card stage', () => {
|
||||||
|
expect(hasRecommendDragStarted(17)).toBe(false);
|
||||||
|
expect(hasRecommendDragStarted(18)).toBe(true);
|
||||||
|
expect(clampRecommendDragOffset(240, 120)).toBe(120);
|
||||||
|
expect(clampRecommendDragOffset(-240, 120)).toBe(-120);
|
||||||
|
expect(clampRecommendDragOffset(240, 0)).toBe(160);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolves commit direction and commit offset', () => {
|
||||||
|
expect(resolveRecommendDragCommitDirection(35)).toBeNull();
|
||||||
|
expect(resolveRecommendDragCommitDirection(-36)).toBe(1);
|
||||||
|
expect(resolveRecommendDragCommitDirection(36)).toBe(-1);
|
||||||
|
expect(resolveRecommendCommitOffset(1, 320, 720)).toBe(-320);
|
||||||
|
expect(resolveRecommendCommitOffset(-1, 0, 720)).toBe(720);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('builds rail class and animation guard state', () => {
|
||||||
|
expect(
|
||||||
|
buildRecommendSwipeRailClassName({ offsetY: 0, commitDirection: null }),
|
||||||
|
).toBe('platform-recommend-swipe-rail--settled');
|
||||||
|
expect(
|
||||||
|
buildRecommendSwipeRailClassName({ offsetY: 24, commitDirection: null }),
|
||||||
|
).toBe('platform-recommend-swipe-rail--dragging');
|
||||||
|
expect(
|
||||||
|
buildRecommendSwipeRailClassName({ offsetY: -320, commitDirection: 1 }),
|
||||||
|
).toBe('platform-recommend-swipe-rail--committing');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
shouldAnimateRecommendSwipe({
|
||||||
|
isAuthenticated: true,
|
||||||
|
hasActiveEntry: true,
|
||||||
|
entryCount: 2,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
shouldAnimateRecommendSwipe({
|
||||||
|
isAuthenticated: true,
|
||||||
|
hasActiveEntry: true,
|
||||||
|
entryCount: 1,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('builds recommend share text from public work identity', () => {
|
||||||
|
expect(
|
||||||
|
buildRecommendShareText({
|
||||||
|
entry: buildPuzzleEntry(),
|
||||||
|
publicWorkCode: 'PZ-OCEAN',
|
||||||
|
detailUrl: 'https://example.test/works/detail?work=PZ-OCEAN',
|
||||||
|
}),
|
||||||
|
).toBe(
|
||||||
|
'邀请你来玩《潮汐拼图》\n作品号:PZ-OCEAN\nhttps://example.test/works/detail?work=PZ-OCEAN',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildPuzzleEntry(): PlatformPuzzleGalleryCard {
|
||||||
|
return {
|
||||||
|
sourceType: 'puzzle',
|
||||||
|
workId: 'puzzle-work-ocean',
|
||||||
|
profileId: 'puzzle-profile-ocean',
|
||||||
|
publicWorkCode: 'PZ-OCEAN',
|
||||||
|
ownerUserId: 'user-1',
|
||||||
|
authorDisplayName: '拼图作者',
|
||||||
|
worldName: '潮汐拼图',
|
||||||
|
subtitle: '潮汐副标题',
|
||||||
|
summaryText: '潮汐摘要',
|
||||||
|
coverImageSrc: null,
|
||||||
|
themeTags: ['海潮'],
|
||||||
|
visibility: 'published',
|
||||||
|
publishedAt: '2026-06-03T08:00:00.000Z',
|
||||||
|
updatedAt: '2026-06-03T08:00:00.000Z',
|
||||||
|
};
|
||||||
|
}
|
||||||
75
src/components/rpg-entry/rpgEntryRecommendSwipeDeckModel.ts
Normal file
75
src/components/rpg-entry/rpgEntryRecommendSwipeDeckModel.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import type { PlatformPublicGalleryCard } from './rpgEntryWorldPresentation';
|
||||||
|
|
||||||
|
export const RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX = 36;
|
||||||
|
export const RECOMMEND_ENTRY_COMMIT_ANIMATION_MS = 180;
|
||||||
|
export const RECOMMEND_ENTRY_DRAG_LIMIT_PX = 160;
|
||||||
|
|
||||||
|
export type RecommendSwipeDirection = 1 | -1;
|
||||||
|
|
||||||
|
export type RecommendSwipeRailState = {
|
||||||
|
offsetY: number;
|
||||||
|
commitDirection: RecommendSwipeDirection | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 收口推荐卡纵向滑动的纯判定,页面只保留 pointer 与动画副作用。 */
|
||||||
|
export function hasRecommendDragStarted(deltaY: number) {
|
||||||
|
return Math.abs(deltaY) >= RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clampRecommendDragOffset(
|
||||||
|
deltaY: number,
|
||||||
|
stageHeight: number,
|
||||||
|
) {
|
||||||
|
const dragLimit =
|
||||||
|
stageHeight > 0 ? stageHeight : RECOMMEND_ENTRY_DRAG_LIMIT_PX;
|
||||||
|
return Math.max(-dragLimit, Math.min(dragLimit, deltaY));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRecommendDragCommitDirection(
|
||||||
|
deltaY: number,
|
||||||
|
): RecommendSwipeDirection | null {
|
||||||
|
if (Math.abs(deltaY) < RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return deltaY < 0 ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRecommendCommitOffset(
|
||||||
|
direction: RecommendSwipeDirection,
|
||||||
|
stageHeight: number,
|
||||||
|
viewportHeight: number,
|
||||||
|
) {
|
||||||
|
const commitDistance = stageHeight > 0 ? stageHeight : viewportHeight;
|
||||||
|
return direction === 1 ? -commitDistance : commitDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRecommendSwipeRailClassName(
|
||||||
|
state: RecommendSwipeRailState,
|
||||||
|
) {
|
||||||
|
if (state.commitDirection) {
|
||||||
|
return 'platform-recommend-swipe-rail--committing';
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.offsetY === 0
|
||||||
|
? 'platform-recommend-swipe-rail--settled'
|
||||||
|
: 'platform-recommend-swipe-rail--dragging';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldAnimateRecommendSwipe(params: {
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
hasActiveEntry: boolean;
|
||||||
|
entryCount: number;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
params.isAuthenticated && params.hasActiveEntry && params.entryCount > 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRecommendShareText(params: {
|
||||||
|
entry: PlatformPublicGalleryCard;
|
||||||
|
publicWorkCode: string;
|
||||||
|
detailUrl: string;
|
||||||
|
}) {
|
||||||
|
return `邀请你来玩《${params.entry.worldName}》\n作品号:${params.publicWorkCode}\n${params.detailUrl}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user