173 lines
9.3 KiB
TypeScript
173 lines
9.3 KiB
TypeScript
|
|
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; guideManual: string; yaoTitle: string; coinLabel: string; confirmBtn: string; summaryTitle: string; checkCategory: string; checkMethod: string; checkCost: string; submitBtn: string; progressLabel: string };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type CoinFace = '字' | '花';
|
|||
|
|
|
|||
|
|
function CoinImage({ face, size = 'w-16 h-16' }: { face: CoinFace; size?: string }) {
|
|||
|
|
return (
|
|||
|
|
<img
|
|||
|
|
src={face === '字' ? '/images/qigua/zi.jpg' : '/images/qigua/hua.jpg'}
|
|||
|
|
alt={face}
|
|||
|
|
className={`${size} rounded-full object-cover border border-amber-300 shadow-sm`}
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function ManualDivinationPage({ locale, divination: d }: Props) {
|
|||
|
|
const cats = d.categories.split(',');
|
|||
|
|
const [category, setCategory] = useState(cats[0]);
|
|||
|
|
const [question, setQuestion] = useState('');
|
|||
|
|
const [yaoIndex, setYaoIndex] = useState(0);
|
|||
|
|
const [coins, setCoins] = useState<[CoinFace, CoinFace, CoinFace]>(['字', '字', '字']);
|
|||
|
|
const [yaoResults, setYaoResults] = useState<CoinFace[][]>([]);
|
|||
|
|
|
|||
|
|
const flipCoin = (idx: number) => {
|
|||
|
|
const next: [CoinFace, CoinFace, CoinFace] = [...coins];
|
|||
|
|
next[idx] = next[idx] === '字' ? '花' : '字';
|
|||
|
|
setCoins(next);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const confirmYao = () => {
|
|||
|
|
const newResults = [...yaoResults, [...coins]];
|
|||
|
|
setYaoResults(newResults);
|
|||
|
|
if (newResults.length < 6) {
|
|||
|
|
setYaoIndex(newResults.length);
|
|||
|
|
setCoins(['字', '字', '字']);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const progress = yaoResults.length;
|
|||
|
|
const isYang = (c: CoinFace[]) => c.filter(x => x === '字').length % 2 === 1;
|
|||
|
|
const yaoNames = ['初爻', '二爻', '三爻', '四爻', '五爻', '上爻'];
|
|||
|
|
|
|||
|
|
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' ? 'Manual Cast' : d.checkMethod.replace(/^.*:|^.*: /, '')}</h1>
|
|||
|
|
<p className="text-[#666666] text-sm mt-1">{locale === 'en' ? 'Prepare three matching coins and enter six results from bottom line to top line.' : '准备三枚相同的钱币,从初爻到上爻依次录入六次结果。'}</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-[360px] flex flex-col gap-4 shrink-0">
|
|||
|
|
{/* Question panel */}
|
|||
|
|
<div className="bg-white rounded-2xl p-[22px] border border-slate-200 flex flex-col gap-4" style={{ height: '300px' }}>
|
|||
|
|
<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}
|
|||
|
|
className="flex-1 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>
|
|||
|
|
{/* Time panel */}
|
|||
|
|
<div className="bg-white rounded-2xl p-5 border border-slate-200 flex flex-col gap-3" style={{ height: '132px' }}>
|
|||
|
|
<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>
|
|||
|
|
{/* Guide panel */}
|
|||
|
|
<div className="bg-white rounded-2xl p-5 border border-slate-200 flex flex-col gap-3 flex-1 overflow-y-auto" style={{ height: '214px' }}>
|
|||
|
|
<h3 className="text-slate-900 text-base font-bold">{d.guideTitle}</h3>
|
|||
|
|
<p className="text-slate-500 text-[13px] whitespace-pre-line">{d.guideManual}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Center: Yao panel */}
|
|||
|
|
<div className="flex-1 bg-white rounded-2xl p-6 border border-slate-200 flex flex-col gap-4 overflow-y-auto">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<h3 className="text-slate-900 text-lg font-bold">{d.yaoTitle}</h3>
|
|||
|
|
<span className="text-violet-600 text-[13px] font-bold">{progress} / 6</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 6 Yao rows - from bottom to top (上爻 at top) */}
|
|||
|
|
<div className="flex flex-col gap-2.5">
|
|||
|
|
{[5, 4, 3, 2, 1, 0].map((i) => {
|
|||
|
|
const result = yaoResults[i];
|
|||
|
|
const isCurrent = i === yaoIndex && progress < 6;
|
|||
|
|
const isDone = result !== undefined;
|
|||
|
|
return (
|
|||
|
|
<div key={i}
|
|||
|
|
className={`flex items-center gap-4 h-[62px] px-3.5 rounded-[10px] ${isCurrent ? 'bg-violet-50 border border-violet-400' : isDone ? 'bg-slate-50' : 'bg-slate-50 border border-slate-200'}`}>
|
|||
|
|
<span className="text-slate-400 text-xs font-medium w-8">{yaoNames[i]}</span>
|
|||
|
|
{isDone ? (
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
{result.map((face, ci) => (
|
|||
|
|
<CoinImage key={ci} face={face} size="w-8 h-8" />
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<span className="text-slate-300 text-xs">待录入</span>
|
|||
|
|
)}
|
|||
|
|
{isDone && (
|
|||
|
|
<div className="ml-auto">
|
|||
|
|
{isYang(result) ? (
|
|||
|
|
<div className="w-12 h-2 bg-violet-600 rounded" />
|
|||
|
|
) : (
|
|||
|
|
<div 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>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Coin selector */}
|
|||
|
|
{progress < 6 && (
|
|||
|
|
<div className="bg-slate-50 rounded-xl p-4 flex flex-col items-center gap-4" style={{ minHeight: '142px' }}>
|
|||
|
|
<div className="flex items-center gap-6">
|
|||
|
|
{coins.map((face, ci) => (
|
|||
|
|
<div key={ci} className="flex flex-col items-center gap-2" onClick={() => flipCoin(ci)} style={{ cursor: 'pointer', width: '86px' }}>
|
|||
|
|
<div className={`w-16 h-16 rounded-full border-2 flex items-center justify-center text-lg font-bold transition-all cursor-pointer select-none ${face === '字' ? 'bg-amber-100 border-amber-400 text-amber-700' : 'bg-slate-200 border-slate-400 text-slate-600'}`}>
|
|||
|
|
<CoinImage face={face} />
|
|||
|
|
</div>
|
|||
|
|
<span className="text-slate-400 text-xs">{face === '字' ? '正面' : '反面'}</span>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{progress < 6 && (
|
|||
|
|
<button onClick={confirmYao} className="w-full h-10 rounded-xl bg-violet-600 text-white text-[13px] font-bold hover:bg-violet-700 transition-colors">{d.confirmBtn}</button>
|
|||
|
|
)}
|
|||
|
|
</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>
|
|||
|
|
{/* Progress */}
|
|||
|
|
<div className="bg-slate-50 rounded-xl p-4 flex flex-col gap-2">
|
|||
|
|
<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={progress < 6}
|
|||
|
|
className={`w-full h-[46px] rounded-full text-sm font-bold transition-colors ${progress >= 6 ? 'bg-violet-600 text-white hover:bg-violet-700' : 'bg-slate-300 text-slate-400 cursor-not-allowed'}`}>
|
|||
|
|
{d.submitBtn}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|