feat(web): add authenticated app shell
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import { useState } from 'react';
|
||||
import Icon from './Icon';
|
||||
|
||||
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 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 120 credits · This time 20 credits' : '可用 120 积分 · 本次 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user