Files
eryao/web/src/components/AutoDivinationPage.tsx
T
ZL-Q f695dd86e9 fix(web): 修复手动起卦教程、硬币动画与积分显示
- 修复硬币翻转动画:从 @keyframes 改为 CSS transition 实现双向动画
- 修复教程自动显示:将 setTutorialChecked 移入 setTimeout 回调,
  避免 useEffect cleanup 提前清除 timer 导致 setGuideStep 不执行
- 添加 AppShell UserSettingsContext 共享 userProfile
- 实现教程结束后调用 updateUserSettings 标记 manual_divination_shown
- 添加点击已确认爻进行编辑的功能 (editingIndex 状态)
- 确认爻后不再重置硬币状态
- 积分显示从硬编码改为读取 API 返回值
- 手机端教程使用 absolute 定位替代 fixed 避免滚动偏移
- 添加 isMobile 响应式状态追踪窗口大小变化
2026-05-09 23:35:53 +08:00

146 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import Icon from './Icon';
import { getPointsBalance, type PointsBalance } from '../lib/api';
interface Props {
locale: string;
dashboard: { brandName: string; navHome: string; navStore: string; navDivination: string; navManual: string; navAuto: string; navHistory: string; navLanguage: string; navSettings: string; logout: string };
divination: { questionTitle: string; questionPlaceholder: string; categoryLabel: string; categories: string; timeTitle: string; timeHint: string; guideTitle: string; guideAuto: string; shakeTitle: string; shakeBtn: string; hexPreview: string; summaryTitle: string; checkCategory: string; checkMethod: string; checkCost: string; submitBtn: string; progressLabel: string };
}
export default function AutoDivinationPage({ locale, divination: d }: Props) {
const cats = d.categories.split(',');
const [category, setCategory] = useState(cats[0]);
const [question, setQuestion] = useState('');
const [progress, setProgress] = useState(0);
const [hexLines, setHexLines] = useState<boolean[]>([]);
const [isShaking, setIsShaking] = useState(false);
const [points, setPoints] = useState<PointsBalance | null>(null);
useEffect(() => {
getPointsBalance().then(setPoints).catch(() => {});
}, []);
const handleShake = () => {
setIsShaking(true);
setTimeout(() => {
const newProgress = progress + 1;
setProgress(newProgress);
const line = Math.random() > 0.5;
setHexLines(prev => [...prev, line]);
setIsShaking(false);
}, 600);
};
const done = progress >= 6;
return (
<div className="flex flex-col gap-[22px] min-h-full">
<div className="flex items-center justify-between">
<div>
<h1 className="text-[#333333] text-[28px] font-bold leading-tight">{locale === 'en' ? 'Auto Cast' : d.checkMethod.replace(/^.*|^.*: /, '').replace('手动', '自动')}</h1>
<p className="text-[#666666] text-sm mt-1">{locale === 'en' ? 'Click the button or shake your device to generate six coin results.' : '点击按钮或摇动设备生成铜钱结果,连续 6 次形成完整卦象。'}</p>
</div>
<div className="hidden md:flex items-center gap-2 h-10 px-3.5 rounded-full bg-white border border-slate-200 text-[#333333] text-[13px] font-semibold">
<Icon name="paid" className="w-[18px] h-[18px] text-violet-700" />
{locale === 'en'
? `Available ${points?.availableBalance ?? '...'} credits · This time ${points?.runCost ?? 20} credits`
: `可用 ${points?.availableBalance ?? '...'} 积分 · 本次 ${points?.runCost ?? 20} 积分`}
</div>
</div>
<div className="flex flex-col xl:flex-row gap-[22px] min-h-0 flex-1">
{/* Left: Question + Time + Guide */}
<div className="w-full xl:w-[340px] flex flex-col gap-4 shrink-0">
<div className="bg-white rounded-2xl p-[22px] border border-slate-200 flex flex-col gap-4">
<h3 className="text-slate-900 text-lg font-bold">{d.questionTitle}</h3>
<div className="flex items-center justify-between h-[42px] px-3 rounded-[10px] bg-slate-50 border border-slate-300">
<span className="text-slate-600 text-sm">{category}</span>
<select value={category} onChange={e => setCategory(e.target.value)} className="bg-transparent text-sm outline-none cursor-pointer">
{cats.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<textarea value={question} onChange={e => setQuestion(e.target.value)} placeholder={d.questionPlaceholder} rows={3}
className="w-full px-3.5 py-3 rounded-[10px] bg-white border border-slate-300 text-sm focus:outline-none focus:border-violet-400 resize-none" />
</div>
<div className="bg-white rounded-2xl p-5 border border-slate-200 flex flex-col gap-3">
<h3 className="text-slate-900 text-base font-bold">{d.timeTitle}</h3>
<div className="flex items-center justify-between h-[42px] px-3 rounded-[10px] bg-slate-50">
<input type="datetime-local" className="bg-transparent text-sm outline-none w-full" />
<Icon name="calendar_today" className="w-[18px] h-[18px] text-slate-400" />
</div>
</div>
<div className="bg-white rounded-2xl p-5 border border-slate-200 flex flex-col gap-3 flex-1 overflow-y-auto">
<h3 className="text-slate-900 text-base font-bold">{d.guideTitle}</h3>
<p className="text-slate-500 text-[13px] whitespace-pre-line">{d.guideAuto}</p>
</div>
</div>
{/* Center: Shake panel */}
<div className="flex-1 bg-white rounded-2xl p-6 border border-slate-200 flex flex-col gap-[18px]">
<div className="flex items-center justify-between">
<h3 className="text-slate-900 text-lg font-bold">{d.shakeTitle}</h3>
<span className="text-violet-600 text-[13px] font-bold">{progress} / 6</span>
</div>
{/* Coin stage */}
<div className="bg-slate-50 rounded-2xl p-[22px] flex items-center justify-center gap-6" style={{ minHeight: '194px' }}>
{[0, 1, 2].map(i => (
<div key={i} className="flex flex-col items-center gap-2" style={{ width: '86px' }}>
<img
src={isShaking ? '/images/qigua/hua.jpg' : '/images/qigua/zi.jpg'}
alt={locale === 'en' ? 'coin' : '铜钱'}
className={`w-16 h-16 rounded-full object-cover border border-amber-300 shadow-sm transition-all ${isShaking ? 'animate-pulse' : ''}`}
/>
<span className="text-slate-400 text-xs">{'铜钱'}</span>
</div>
))}
</div>
{/* Shake button */}
<div className="flex flex-col items-center gap-2.5" style={{ height: '82px', justifyContent: 'center' }}>
{!done && (
<button onClick={handleShake} disabled={isShaking}
className="flex items-center gap-2 px-8 py-2.5 rounded-full bg-violet-600 text-white text-sm font-bold hover:bg-violet-700 disabled:opacity-50 transition-colors">
<Icon name="casino" className="w-[18px] h-[18px]" />
{d.shakeBtn}
</button>
)}
{done && <p className="text-violet-600 text-sm font-medium"></p>}
</div>
{/* Hexagram preview */}
<div className="bg-white rounded-xl p-[18px] border border-slate-200 flex-1 flex flex-col gap-3 overflow-y-auto">
<p className="text-slate-900 text-base font-bold">{d.hexPreview}</p>
<div className="flex flex-col gap-2">
{hexLines.length > 0 ? hexLines.map((isYang, i) => isYang ? (
<div key={i} className="w-12 h-2 bg-violet-600 rounded" />
) : (
<div key={i} className="flex gap-1.5"><div className="w-4 h-2 bg-violet-400 rounded" /><div className="w-4 h-2 bg-violet-400 rounded" /></div>
)) : (
<p className="text-slate-300 text-sm"></p>
)}
</div>
</div>
</div>
{/* Right: Summary */}
<div className="w-full xl:w-[300px] bg-white rounded-2xl p-[22px] border border-slate-200 flex flex-col gap-[18px] shrink-0">
<h3 className="text-slate-900 text-lg font-bold">{d.summaryTitle}</h3>
<div className="bg-slate-50 rounded-xl p-4 flex flex-col gap-2" style={{ height: '94px' }}>
<p className="text-slate-500 text-[13px]">{d.progressLabel}</p>
<p className="text-violet-600 text-[28px] font-bold">{progress} / 6</p>
</div>
<p className="text-slate-500 text-sm">{d.checkCategory}</p>
<p className="text-slate-500 text-sm">{d.checkMethod}</p>
<p className="text-slate-500 text-sm">{d.checkCost}</p>
<div className="flex-1" />
<button disabled={!done}
className={`w-full h-[46px] rounded-full text-sm font-bold transition-colors ${done ? 'bg-violet-600 text-white hover:bg-violet-700' : 'bg-slate-300 text-slate-400 cursor-not-allowed'}`}>
{d.submitBtn}
</button>
</div>
</div>
</div>
);
}