fix: infinite session creation loop, add sign image animation, align hexagram UI with Flutter
- Fix DivinationProcessingOverlay infinite re-render loop by using refs for params/yaoStates/onError instead of direct effect dependencies - Remove column headers from hexagram detail card (matching Flutter) - Keep domain-specific terms (六神, 六亲, etc.) in Chinese for all locales - Translate ganZhiInfo/wuXingWangShuai/ganZhiKongWang section titles to English - Add sign image overlay animation on fresh divination result (scale + translate)
This commit is contained in:
@@ -110,6 +110,12 @@ export default function DivinationProcessingOverlay({
|
||||
const [flipAngle, setFlipAngle] = useState(0);
|
||||
const [result, setResult] = useState<DivinationResultData | null>(null);
|
||||
const isFlippingRef = useRef(false);
|
||||
const paramsRef = useRef(params);
|
||||
const yaoStatesRef = useRef(yaoStates);
|
||||
const onErrorRef = useRef(onError);
|
||||
paramsRef.current = params;
|
||||
yaoStatesRef.current = yaoStates;
|
||||
onErrorRef.current = onError;
|
||||
|
||||
// 翻牌动画
|
||||
useEffect(() => {
|
||||
@@ -154,7 +160,7 @@ export default function DivinationProcessingOverlay({
|
||||
async function runDivination() {
|
||||
try {
|
||||
// 1. 提交起卦请求
|
||||
const { threadId, runId } = await enqueueDivinationRun(params, yaoStates);
|
||||
const { threadId, runId } = await enqueueDivinationRun(paramsRef.current, yaoStatesRef.current);
|
||||
invalidatePoints();
|
||||
|
||||
if (aborted) return;
|
||||
@@ -261,8 +267,8 @@ export default function DivinationProcessingOverlay({
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!aborted && onError) {
|
||||
onError(error instanceof Error ? error : new Error(String(error)));
|
||||
if (!aborted && onErrorRef.current) {
|
||||
onErrorRef.current(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,7 +278,7 @@ export default function DivinationProcessingOverlay({
|
||||
return () => {
|
||||
aborted = true;
|
||||
};
|
||||
}, [params, yaoStates, onError]);
|
||||
}, []);
|
||||
|
||||
const currentCard = cards[cardIndex];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { historyMessageToResultData, type DivinationResultData, type YaoType } from '../lib/api';
|
||||
import { useHistoryThread } from '../lib/resources';
|
||||
@@ -134,6 +134,69 @@ function FocusPointsCard({ points, title, copyLabel }: { points: string[]; title
|
||||
);
|
||||
}
|
||||
|
||||
const SIGN_ANIM_SIZE = 380;
|
||||
const SIGN_ANIM_DISPLAY_MS = 800;
|
||||
const SIGN_ANIM_SHRINK_MS = 600;
|
||||
|
||||
function SignAnimationOverlay({
|
||||
src,
|
||||
targetRef,
|
||||
onAnimationEnd,
|
||||
}: {
|
||||
src: string;
|
||||
targetRef: React.RefObject<HTMLDivElement | null>;
|
||||
onAnimationEnd: () => void;
|
||||
}) {
|
||||
const [shrinking, setShrinking] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setShrinking(true), SIGN_ANIM_DISPLAY_MS);
|
||||
return () => clearTimeout(t);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (shrinking) {
|
||||
const t = setTimeout(onAnimationEnd, SIGN_ANIM_SHRINK_MS + 100);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
}, [shrinking, onAnimationEnd]);
|
||||
|
||||
let imgStyle: React.CSSProperties = {
|
||||
width: SIGN_ANIM_SIZE,
|
||||
height: SIGN_ANIM_SIZE,
|
||||
borderRadius: 14,
|
||||
boxShadow: '0 20px 40px rgba(0,0,0,0.25)',
|
||||
animation: 'signAppear 400ms ease-out',
|
||||
transition: `transform ${SIGN_ANIM_SHRINK_MS}ms cubic-bezier(0.4,0,0.2,1), opacity ${SIGN_ANIM_SHRINK_MS - 100}ms ease 100ms`,
|
||||
};
|
||||
|
||||
if (shrinking && targetRef.current) {
|
||||
const rect = targetRef.current.getBoundingClientRect();
|
||||
const dx = rect.left + rect.width / 2 - window.innerWidth / 2;
|
||||
const dy = rect.top + rect.height / 2 - window.innerHeight / 2;
|
||||
const s = rect.width / SIGN_ANIM_SIZE;
|
||||
imgStyle.transform = `translate(${dx}px, ${dy}px) scale(${s})`;
|
||||
imgStyle.opacity = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div
|
||||
className="absolute inset-0 bg-slate-100/95"
|
||||
style={{
|
||||
backdropFilter: 'blur(8px)',
|
||||
transition: `opacity ${SIGN_ANIM_SHRINK_MS}ms ease-out`,
|
||||
opacity: shrinking ? 0 : 1,
|
||||
}}
|
||||
/>
|
||||
<div className="relative overflow-hidden" style={imgStyle}>
|
||||
<img src={src} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<style>{`@keyframes signAppear{from{transform:scale(.85);opacity:0}to{transform:scale(1);opacity:1}}`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WarningCard({ message }: { message: string }) {
|
||||
return (
|
||||
<div className="bg-amber-50 rounded-xl p-3.5 flex items-start gap-2.5">
|
||||
@@ -284,29 +347,6 @@ function HexagramDetailCard({ data, t }: { data: DivinationResultData; t: Record
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 text-xs text-slate-400">
|
||||
<div className="flex-1 flex items-center gap-1">
|
||||
<span className="w-5 text-center">{t.yaoColSpirit}</span>
|
||||
<span className="w-7 text-center">{t.yaoColRelation}</span>
|
||||
<span className="w-5 text-center">{t.yaoColBranch}</span>
|
||||
<span className="w-5 text-center">{t.yaoColElement}</span>
|
||||
<span className="flex-1" />
|
||||
<span className="w-4 text-center">{t.yaoColChange}</span>
|
||||
<span className="w-4 text-center">{t.yaoColMark}</span>
|
||||
</div>
|
||||
{hasChangingYao && (
|
||||
<div className="flex-1 flex items-center gap-1">
|
||||
<span className="w-5 text-center">{t.yaoColSpirit}</span>
|
||||
<span className="w-7 text-center">{t.yaoColRelation}</span>
|
||||
<span className="w-5 text-center">{t.yaoColBranch}</span>
|
||||
<span className="w-5 text-center">{t.yaoColElement}</span>
|
||||
<span className="flex-1" />
|
||||
<span className="w-4 text-center">{t.yaoColChange}</span>
|
||||
<span className="w-4 text-center" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{[5, 4, 3, 2, 1, 0].map((idx) => (
|
||||
<YaoDetailRow
|
||||
key={idx}
|
||||
@@ -346,6 +386,11 @@ export default function DivinationResultPage({ locale, translations: t }: Props)
|
||||
const [data, setData] = useState<DivinationResultData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [canFollowUp, setCanFollowUp] = useState(true);
|
||||
const [signAnimating, setSignAnimating] = useState(() => {
|
||||
return !!(location.state as { result?: DivinationResultData } | null)?.result;
|
||||
});
|
||||
const signImageRef = useRef<HTMLDivElement>(null);
|
||||
const handleSignAnimationEnd = useCallback(() => setSignAnimating(false), []);
|
||||
|
||||
useEffect(() => {
|
||||
// 1. Try router state (from divination flow)
|
||||
@@ -421,6 +466,13 @@ export default function DivinationResultPage({ locale, translations: t }: Props)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 min-h-full">
|
||||
{signAnimating && (
|
||||
<SignAnimationOverlay
|
||||
src={getSignImageSrc(data.signType)}
|
||||
targetRef={signImageRef}
|
||||
onAnimationEnd={handleSignAnimationEnd}
|
||||
/>
|
||||
)}
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3.5">
|
||||
<button
|
||||
@@ -439,7 +491,7 @@ export default function DivinationResultPage({ locale, translations: t }: Props)
|
||||
<div className="flex-1 flex flex-col gap-3.5 overflow-y-auto pr-1">
|
||||
{/* Hero: sign image + gua name + question + tags */}
|
||||
<div className="bg-white rounded-2xl p-5 border border-slate-200 flex items-center gap-5">
|
||||
<div className="w-[116px] h-[116px] rounded-[14px] overflow-hidden shrink-0 shadow-lg">
|
||||
<div ref={signImageRef} className="w-[116px] h-[116px] rounded-[14px] overflow-hidden shrink-0 shadow-lg">
|
||||
<img src={getSignImageSrc(data.signType)} alt={getSignTypeLabel(data.signType, t)} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-2.5">
|
||||
|
||||
Reference in New Issue
Block a user