f695dd86e9
- 修复硬币翻转动画:从 @keyframes 改为 CSS transition 实现双向动画 - 修复教程自动显示:将 setTutorialChecked 移入 setTimeout 回调, 避免 useEffect cleanup 提前清除 timer 导致 setGuideStep 不执行 - 添加 AppShell UserSettingsContext 共享 userProfile - 实现教程结束后调用 updateUserSettings 标记 manual_divination_shown - 添加点击已确认爻进行编辑的功能 (editingIndex 状态) - 确认爻后不再重置硬币状态 - 积分显示从硬编码改为读取 API 返回值 - 手机端教程使用 absolute 定位替代 fixed 避免滚动偏移 - 添加 isMobile 响应式状态追踪窗口大小变化
146 lines
8.2 KiB
TypeScript
146 lines
8.2 KiB
TypeScript
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>
|
||
);
|
||
}
|