165 lines
6.2 KiB
TypeScript
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>
|
|
)
|
|
}
|