feat: 更新光标组件和场景交互
- 在 Cursor 组件中添加禁用状态,增强用户交互体验。 - 更新多个场景以使用新的光标控制逻辑,确保光标的启用状态与展品访问状态相匹配。 - 在信件组件中添加输入校验功能,提升用户填写体验。 - 调整文本内容和引导对话,确保信息准确性和一致性。
This commit is contained in:
1 parent
a6570efa19
commit
d7ff7d3a45
25 files changed
+769
-158
No files matched your search
@@ -2,6 +2,7 @@ export { useDragAndDrop } from "./useDragAndDrop";
|
||||
export { usePageScale } from "./usePageScale";
|
||||
export { useTouch } from "./useTouch";
|
||||
export { useZoomAndDrag } from "./useZoomAndDrag";
|
||||
export { useCursorController } from "./useCursorController";
|
||||
export type {
|
||||
UseTouchOptions,
|
||||
UseTouchReturn,
|
||||
@@ -12,3 +13,4 @@ export type {
|
||||
UseZoomAndDragOptions,
|
||||
UseZoomAndDragReturn,
|
||||
} from "./useZoomAndDrag";
|
||||
export type { CursorControllerConfig } from "./useCursorController";
|
||||
@@ -0,0 +1,332 @@
|
||||
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
|
||||
/** Cursor 解锁规则 */
|
||||
export interface CursorUnlockRule {
|
||||
/** cursor ID */
|
||||
id: string | number;
|
||||
/** 依赖的展品 ID 列表,当这些展品都被访问后,自动解锁此 cursor */
|
||||
dependsOn?: (string | number)[];
|
||||
/** 初始是否启用(如果未配置 dependsOn,则使用此值) */
|
||||
initialEnabled?: boolean;
|
||||
}
|
||||
|
||||
/** Cursor 控制器配置 */
|
||||
export interface CursorControllerConfig {
|
||||
/** 方式1:使用解锁规则(推荐,更灵活) */
|
||||
unlockRules?: CursorUnlockRule[];
|
||||
/** 方式2:使用依赖映射(简化配置,格式:{ cursorId: [依赖的展品Id列表] }) */
|
||||
dependencies?: Record<string | number, (string | number)[]>;
|
||||
/** 已访问的展品 ID(支持 Set 或数组,内部会自动处理引用问题) */
|
||||
visited?: Set<string | number> | (string | number)[];
|
||||
/** 初始启用的 cursor ID 列表(向后兼容,如果没有配置 unlockRules 或 dependencies 则使用此值) */
|
||||
initialEnabled?: (string | number)[];
|
||||
/** cursor 完成时的回调函数 */
|
||||
onCursorComplete?: (id: string | number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cursor 控制器 Hook(混合方案)
|
||||
* 支持自动解锁和手动控制,提供两种配置方式
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // 方式1:使用解锁规则
|
||||
* const { isEnabled } = useCursorController({
|
||||
* unlockRules: [
|
||||
* { id: 1, initialEnabled: true }, // cursor 1 初始启用
|
||||
* { id: 2, dependsOn: [1] }, // cursor 2 依赖展品1
|
||||
* { id: 3, dependsOn: [1, 2] }, // cursor 3 依赖展品1和2
|
||||
* ],
|
||||
* visited: visited,
|
||||
* });
|
||||
*
|
||||
* // 方式2:使用依赖映射(更简洁)
|
||||
* const { isEnabled } = useCursorController({
|
||||
* initialEnabled: [1],
|
||||
* dependencies: {
|
||||
* 2: [1], // cursor 2 依赖展品 1
|
||||
* 3: [1, 2], // cursor 3 依赖展品 1 和 2
|
||||
* },
|
||||
* visited: visited, // 可以传入 Set 或数组,内部会自动处理
|
||||
* });
|
||||
*
|
||||
* // 方式3:向后兼容(手动控制)
|
||||
* const { enable, isEnabled } = useCursorController({
|
||||
* initialEnabled: [1],
|
||||
* });
|
||||
* enable(2);
|
||||
*
|
||||
* // 在组件中使用
|
||||
* <Cursor disabled={!isEnabled(1)} />
|
||||
* ```
|
||||
*/
|
||||
export const useCursorController = (config: CursorControllerConfig = {}) => {
|
||||
const {
|
||||
unlockRules = [],
|
||||
dependencies = {},
|
||||
visited,
|
||||
initialEnabled = [],
|
||||
} = config;
|
||||
|
||||
// 手动覆盖的状态(优先级高于自动解锁)
|
||||
const [manualOverrides, setManualOverrides] = useState<
|
||||
Map<string | number, boolean>
|
||||
>(new Map());
|
||||
|
||||
// 将 visited(Set 或数组)序列化为稳定的 key,用于比较内容变化
|
||||
// 无论传入 Set 还是数组,都转换为数组再序列化,避免引用问题
|
||||
const visitedKey = useMemo(() => {
|
||||
if (!visited) return "";
|
||||
const visitedArray = Array.isArray(visited) ? visited : Array.from(visited);
|
||||
if (visitedArray.length === 0) return "";
|
||||
const sorted = [...visitedArray].sort((a, b) => Number(a) - Number(b));
|
||||
return sorted.join(",");
|
||||
}, [visited]);
|
||||
|
||||
// 使用 ref 存储上一次的 key,避免不必要的重新计算
|
||||
const prevKeyRef = useRef<string>("");
|
||||
|
||||
// 使用 ref 存储最新的 visited,避免在依赖中使用对象引用
|
||||
const visitedRef = useRef<Set<string | number>>(new Set());
|
||||
|
||||
// 将 visited(Set 或数组)转换为 Set,并更新 ref
|
||||
// 只在内容变化时更新,避免引用问题
|
||||
useMemo(() => {
|
||||
if (!visited) {
|
||||
visitedRef.current = new Set<string | number>();
|
||||
return;
|
||||
}
|
||||
const newSet = Array.isArray(visited) ? new Set(visited) : visited;
|
||||
visitedRef.current = newSet;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visitedKey]); // 使用 visitedKey 而不是 visited 对象,避免引用问题
|
||||
|
||||
// 计算初始启用的 cursor
|
||||
const getInitialEnabled = useCallback(() => {
|
||||
const enabled = new Set<string | number>();
|
||||
const currentVisitedSet = visitedRef.current; // 使用 ref 中的最新值
|
||||
|
||||
// 优先使用 unlockRules
|
||||
if (unlockRules.length > 0) {
|
||||
unlockRules.forEach((rule) => {
|
||||
if (rule.dependsOn && rule.dependsOn.length > 0) {
|
||||
// 检查依赖是否都满足
|
||||
const allDepsMet = rule.dependsOn.every((dep) =>
|
||||
currentVisitedSet.has(dep)
|
||||
);
|
||||
if (allDepsMet) {
|
||||
enabled.add(rule.id);
|
||||
}
|
||||
} else if (rule.initialEnabled) {
|
||||
enabled.add(rule.id);
|
||||
}
|
||||
});
|
||||
return enabled;
|
||||
}
|
||||
|
||||
// 其次使用 dependencies
|
||||
if (Object.keys(dependencies).length > 0) {
|
||||
Object.entries(dependencies).forEach(([cursorId, deps]) => {
|
||||
const id = isNaN(Number(cursorId)) ? cursorId : Number(cursorId);
|
||||
if (
|
||||
deps.length === 0 ||
|
||||
deps.every((dep) => currentVisitedSet.has(dep))
|
||||
) {
|
||||
enabled.add(id);
|
||||
}
|
||||
});
|
||||
initialEnabled.forEach((id) => enabled.add(id));
|
||||
return enabled;
|
||||
}
|
||||
|
||||
// 最后使用 initialEnabled(向后兼容)
|
||||
initialEnabled.forEach((id) => enabled.add(id));
|
||||
return enabled;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [unlockRules, dependencies, visitedKey, initialEnabled]);
|
||||
|
||||
// 将 manualOverrides 序列化为 key
|
||||
const manualOverridesKey = useMemo(() => {
|
||||
if (manualOverrides.size === 0) return "";
|
||||
const sorted = Array.from(manualOverrides.entries())
|
||||
.sort(([a], [b]) => Number(a) - Number(b))
|
||||
.map(([id, enabled]) => `${id}:${enabled}`)
|
||||
.join(",");
|
||||
return sorted;
|
||||
}, [manualOverrides]);
|
||||
|
||||
// 根据解锁规则和 visited 状态计算应该启用的 cursor
|
||||
const calculateEnabledCursors = useCallback(() => {
|
||||
const enabled = new Set<string | number>();
|
||||
// 使用 ref 中的最新值,避免依赖问题
|
||||
const currentVisitedSet = visitedRef.current;
|
||||
|
||||
// 优先使用 unlockRules
|
||||
if (unlockRules.length > 0) {
|
||||
unlockRules.forEach((rule) => {
|
||||
// 手动覆盖优先
|
||||
if (manualOverrides.has(rule.id)) {
|
||||
if (manualOverrides.get(rule.id)) {
|
||||
enabled.add(rule.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 自动解锁逻辑
|
||||
if (rule.dependsOn && rule.dependsOn.length > 0) {
|
||||
const allDepsMet = rule.dependsOn.every((dep) =>
|
||||
currentVisitedSet.has(dep)
|
||||
);
|
||||
if (allDepsMet) {
|
||||
enabled.add(rule.id);
|
||||
}
|
||||
} else if (rule.initialEnabled) {
|
||||
enabled.add(rule.id);
|
||||
}
|
||||
});
|
||||
return enabled;
|
||||
}
|
||||
|
||||
// 其次使用 dependencies
|
||||
if (Object.keys(dependencies).length > 0) {
|
||||
const allCursorIds = new Set([
|
||||
...initialEnabled,
|
||||
...Object.keys(dependencies).map((key) =>
|
||||
isNaN(Number(key)) ? key : Number(key)
|
||||
),
|
||||
]);
|
||||
|
||||
allCursorIds.forEach((id) => {
|
||||
// 手动覆盖优先
|
||||
if (manualOverrides.has(id)) {
|
||||
if (manualOverrides.get(id)) {
|
||||
enabled.add(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 自动解锁逻辑
|
||||
const deps = dependencies[id];
|
||||
if (deps && deps.length > 0) {
|
||||
const allDepsMet = deps.every((dep) => currentVisitedSet.has(dep));
|
||||
if (allDepsMet) {
|
||||
enabled.add(id);
|
||||
}
|
||||
} else if (initialEnabled.includes(id)) {
|
||||
enabled.add(id);
|
||||
}
|
||||
});
|
||||
return enabled;
|
||||
}
|
||||
|
||||
// 最后使用 initialEnabled(向后兼容)
|
||||
initialEnabled.forEach((id) => {
|
||||
if (manualOverrides.has(id)) {
|
||||
if (manualOverrides.get(id)) {
|
||||
enabled.add(id);
|
||||
}
|
||||
} else {
|
||||
enabled.add(id);
|
||||
}
|
||||
});
|
||||
|
||||
return enabled;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
unlockRules,
|
||||
dependencies,
|
||||
visitedKey,
|
||||
initialEnabled,
|
||||
manualOverridesKey,
|
||||
]);
|
||||
|
||||
const [enabledCursors, setEnabledCursors] =
|
||||
useState<Set<string | number>>(getInitialEnabled);
|
||||
|
||||
// 当 visited 或 manualOverrides 内容变化时,自动重新计算
|
||||
useEffect(() => {
|
||||
const currentKey = `${visitedKey}|${manualOverridesKey}`;
|
||||
// 只在 key 真正变化时才重新计算
|
||||
if (prevKeyRef.current !== currentKey) {
|
||||
prevKeyRef.current = currentKey;
|
||||
setEnabledCursors(calculateEnabledCursors());
|
||||
}
|
||||
}, [visitedKey, manualOverridesKey, calculateEnabledCursors]);
|
||||
|
||||
// 激活 cursor(手动控制,会覆盖自动解锁)
|
||||
const enable = useCallback((ids: string | number | (string | number)[]) => {
|
||||
setManualOverrides((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
const idsArray = Array.isArray(ids) ? ids : [ids];
|
||||
idsArray.forEach((id) => {
|
||||
newMap.set(id, true);
|
||||
});
|
||||
return newMap;
|
||||
});
|
||||
setEnabledCursors((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
const idsArray = Array.isArray(ids) ? ids : [ids];
|
||||
idsArray.forEach((id) => {
|
||||
newSet.add(id);
|
||||
});
|
||||
return newSet;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 禁用 cursor
|
||||
const disable = useCallback((ids: string | number | (string | number)[]) => {
|
||||
setManualOverrides((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
const idsArray = Array.isArray(ids) ? ids : [ids];
|
||||
idsArray.forEach((id) => {
|
||||
newMap.set(id, false);
|
||||
});
|
||||
return newMap;
|
||||
});
|
||||
setEnabledCursors((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
const idsArray = Array.isArray(ids) ? ids : [ids];
|
||||
idsArray.forEach((id) => {
|
||||
newSet.delete(id);
|
||||
});
|
||||
return newSet;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 检查是否启用
|
||||
const isEnabled = useCallback(
|
||||
(id: string | number) => {
|
||||
return enabledCursors.has(id);
|
||||
},
|
||||
[enabledCursors]
|
||||
);
|
||||
|
||||
// 获取所有启用的 cursor
|
||||
const getEnabledCursors = useCallback(() => {
|
||||
return new Set(enabledCursors);
|
||||
}, [enabledCursors]);
|
||||
|
||||
// 清除手动覆盖,恢复自动解锁
|
||||
const clearManualOverride = useCallback((id: string | number) => {
|
||||
setManualOverrides((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.delete(id);
|
||||
return newMap;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 重置状态
|
||||
const reset = useCallback(() => {
|
||||
setManualOverrides(new Map());
|
||||
setEnabledCursors(getInitialEnabled());
|
||||
}, [getInitialEnabled]);
|
||||
|
||||
return {
|
||||
enable,
|
||||
disable,
|
||||
isEnabled,
|
||||
getEnabledCursors,
|
||||
clearManualOverride,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user