Files
ChronicleOfTheImmortalClan/src/renderer/ui/panels/ChroniclePanel.tsx
T

165 lines
6.2 KiB
TypeScript

import { useGameStore } from '../store'
import { useMemo, useState, memo } from 'react'
const CAT_NAME: Record<string, string> = {
birth: '诞庆',
marriage: '姻缘',
death: '祭丧',
breakthrough: '突破',
battle: '战事',
trade: '商贸',
diplomacy: '外交',
exploration: '寻宝',
building: '营造',
event: '奇遇',
misc: '家常'
}
interface ChEntry {
id: string
year: number
month: number
category: string
important: boolean
text: string
}
function grouped(entries: ChEntry[]): { year: number; items: ChEntry[] }[] {
const map = new Map<number, typeof entries>()
for (const e of entries) {
const g = map.get(e.year) ?? []
g.push(e)
map.set(e.year, g)
}
return [...map.entries()].sort((a, b) => b[0] - a[0]).map(([year, items]) => ({ year, items }))
}
function statOf(g: { items: { category: string }[] }): string {
const births = g.items.filter((i) => i.category === 'birth').length
const deaths = g.items.filter((i) => i.category === 'death').length
const bt = g.items.filter((i) => i.category === 'breakthrough').length
const parts: string[] = []
if (births) parts.push(`生${births}`)
if (deaths) parts.push(`殇${deaths}`)
if (bt) parts.push(`突破${bt}`)
return parts.join(' · ')
}
export default function ChroniclePanel() {
const world = useGameStore((s) => s.world)
const revision = useGameStore((s) => s.revision)
void revision
const [filter, setFilter] = useState<string>('all')
const [battleId, setBattleId] = useState<string | null>(null)
const [draft, setDraft] = useState('')
const entries = useMemo(() => {
if (!world) return []
const all = [...world.state.chronicle]
const filtered = filter === 'all' ? all : all.filter((e) => e.category === filter)
return filtered.slice().sort((a, b) => (b.year - a.year) || (b.month - a.month))
}, [world, filter, world ? world.state.chronicle.length : 0])
if (!world) return null
const battles = world.state.battles.slice().reverse()
const battle = battles.find((b) => b.id === battleId)
const groupedEntries = useMemo(() => grouped(entries), [entries])
return (
<div>
<div className="help-text">
族史一卷:系统自动记下的家族大事,供族人题翰墨于旁。倒序排列,可回看任意一年的兴衰细节。
</div>
<div style={{ display: 'flex', gap: 8, marginBottom: 10 }}>
<input
className="ch-draft"
placeholder="题一笔族史……(如:某年宗祠大修,阖族祭祖)"
value={draft}
maxLength={80}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && draft.trim()) {
world.chronicle('misc', draft.trim(), undefined, false)
setDraft('')
}
}}
/>
<button
className="btn btn-sm"
disabled={!draft.trim()}
onClick={() => {
world.chronicle('misc', draft.trim(), undefined, false)
setDraft('')
}}
>落笔</button>
</div>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }}>
<button className={`btn btn-sm ${filter === 'all' ? 'btn-primary' : ''}`} onClick={() => setFilter('all')}>全部</button>
{Object.entries(CAT_NAME).map(([k, v]) => (
<button key={k} className={`btn btn-sm ${filter === k ? 'btn-primary' : ''}`} onClick={() => setFilter(k)}>
{v}
</button>
))}
</div>
<div className="chronicle-list">
{groupedEntries.map((g) => (
<div key={g.year}>
<div className="ch-yearbar">{g.year} <span className="dim2">{g.items.length} · {statOf(g)}</span></div>
{g.items.map((e) => (
<div key={e.id} className={`ch-item ${e.important ? 'gold' : ''}`}>
<span className="ch-month">{e.month}</span>
<span>
<span className="tag" style={{ marginRight: 8 }}>{CAT_NAME[e.category] ?? e.category}</span>
{e.text}
</span>
</div>
))}
</div>
))}
{entries.length === 0 && <div className="dim2">暂无记载。</div>}
</div>
<div className="card-title" style={{ fontSize: '1.02rem', marginTop: 22 }}>战报匣(全文留存)</div>
{battle ? (
<div className="mission-card">
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<b>{battle.title}</b>
<span className="dim2" style={{ fontSize: '0.85rem' }}>
{battle.year}{battle.month} ·{' '}
<span className={battle.winner === 'player' ? 'good' : battle.winner === 'enemy' ? 'bad' : 'warn'}>
{battle.winner === 'player' ? '我方得胜' : battle.winner === 'enemy' ? '我方败北' : '平分秋色'}
</span>
</span>
<button className="btn btn-sm" style={{ marginLeft: 'auto' }} onClick={() => setBattleId(null)}>
收起
</button>
</div>
<div className="dim" style={{ fontSize: '0.92rem', lineHeight: 1.8, marginTop: 8 }}>
{battle.lines.slice(1).map((l, i) => (
<div key={i}>· {l}</div>
))}
{battle.losses.length > 0 && (
<div className="bad">我方折损:{battle.losses.join('、')}</div>
)}
</div>
</div>
) : battles.length === 0 ? (
<div className="dim2" style={{ marginBottom: 6 }}>尚无战事可录。</div>
) : (
<div>
{battles.map((b) => (
<div key={b.id} className="set-row">
<span>
<span className="ch-year" style={{ marginRight: 10 }}>{b.year}{b.month}</span>
{b.title}
<span className={`tag ${b.winner === 'player' ? 'good' : b.winner === 'enemy' ? 'bad' : 'warn'}`} style={{ marginLeft: 8 }}>
{b.winner === 'player' ? '胜' : b.winner === 'enemy' ? '负' : '平'}
</span>
</span>
<button className="btn btn-sm" onClick={() => setBattleId(b.id)}></button>
</div>
))}
</div>
)}
</div>
)
}