v0.1.12: 弹框有道(ModalShell 统一壳 + 底部独立一排 + 全按钮检修)
【弹框体系】 - ModalShell 统一壳:六弹框(事件/战报/纸笺/成员/定鼎)全部迁移 - 右上独立×(与内容分离)+ ESC 全局关闭 + 遮罩策略(详情可点、事件禁遮罩) - 事件「稍后再议」:requeueEvent 按回队列下月再弹(不消灭不误弃) - 底部独立一排 footer:明显分隔线+独立底色,与内容区分;返回重选/出战、 合上战报、翻过此页、暂不落印/落印分列其位 - 加宽:事件680/战报720/纸笺620/成员740/定鼎700;min(92vw,N) 零横滚;内容区滚动+word-break 【全按钮检修】 - 求经台 UI 入口补上(藏书阁3级/150灵石/两年冷却三条件 disabled 提示) - 冲关二次确认(成功率约X%预览)——走火可能陨落的高危操作不再是一点即发 - 丹房炼制资源不足即禁用+title;集市收藏品购买灵石不足禁用 - 外交赠礼改 40/120/300 三档 + 每档预览「关系+N」换算 【测试】923→937(requeue 语义/赠礼档位矩阵/求经四态/弹框规格带) 金钟罩零漂移(纯 UI 层),typecheck/build 通过
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { ModalShell } from './ModalShell'
|
||||
|
||||
export function BattleModal() {
|
||||
const battleView = useGameStore((s) => s.battleView)
|
||||
@@ -7,13 +8,15 @@ export function BattleModal() {
|
||||
const winnerLabel =
|
||||
battleView.winner === 'player' ? '✦ 我方得胜 ✦' : battleView.winner === 'enemy' ? '✕ 我方败北 ✕' : '◌ 平分秋色 ◌'
|
||||
|
||||
const close = () => useGameStore.setState({ battleView: undefined })
|
||||
return (
|
||||
<div className="modal-outer">
|
||||
<div className="modal battle-modal">
|
||||
<div className="modal-title" style={{ fontSize: '1.3rem' }}>{battleView.title}</div>
|
||||
<div className="modal-cat">
|
||||
{battleView.year}年{battleView.month}月 · <span className={battleView.winner === 'player' ? 'good' : battleView.winner === 'enemy' ? 'bad' : 'warn'}>{winnerLabel}</span>
|
||||
</div>
|
||||
<ModalShell
|
||||
title={battleView.title}
|
||||
category={`${battleView.year}年${battleView.month}月 · ${winnerLabel}`}
|
||||
width={720}
|
||||
onClose={close}
|
||||
footer={<button className="btn btn-primary" onClick={close}>合上战报</button>}
|
||||
>
|
||||
<div className="battle-lines">
|
||||
{battleView.lines.map((l, i) => (
|
||||
<div key={i}>{l}</div>
|
||||
@@ -24,11 +27,7 @@ export function BattleModal() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<button className="btn btn-primary" onClick={() => useGameStore.setState({ battleView: undefined })}>合上战报</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalShell>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { describeRealm } from '../../game/data/realms'
|
||||
import { Character } from '../../game/types/domain'
|
||||
import { combatPowerOf } from '../../game/engine/systems/combat'
|
||||
import { FORMATIONS, FormationId } from '../../game/data/formations'
|
||||
import { ModalShell } from './ModalShell'
|
||||
|
||||
export function EventModal() {
|
||||
const pendingEventId = useGameStore((s) => s.pendingEventId)
|
||||
@@ -52,15 +53,42 @@ export function EventModal() {
|
||||
)
|
||||
}
|
||||
|
||||
const dorm = () => {
|
||||
// 「稍后再议」:把事件按回队列,不消灭、不误弃
|
||||
world.requeueEvent(pendingEventId)
|
||||
closeModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-outer">
|
||||
<div className="modal">
|
||||
<div className="modal-title">{pendingEventDef.name}</div>
|
||||
<div className="modal-cat">
|
||||
{eventCategoryName(pendingEventDef.category)}
|
||||
{pendingEventDef.once && ' · 仅此一次'}
|
||||
</div>
|
||||
<div className="modal-text">{pendingEventDef.text}</div>
|
||||
<ModalShell
|
||||
title={pendingEventDef.name}
|
||||
category={`${eventCategoryName(pendingEventDef.category)}${pendingEventDef.once ? ' · 仅此一次' : ''}`}
|
||||
width={680}
|
||||
allowOuter={false}
|
||||
onClose={dorm}
|
||||
closeLabel="稍后再议"
|
||||
footer={
|
||||
squadPicking ? (
|
||||
<>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => {
|
||||
setSquadPicking(false)
|
||||
setSquadSel([])
|
||||
}}
|
||||
>
|
||||
返回重选
|
||||
</button>
|
||||
<button className="btn btn-primary" disabled={squadSel.length === 0} onClick={() => choose(raidIdx)}>
|
||||
{squadSel.length === 0 ? '族中无可战之人' : `出战!(共${squadSel.length}人)`}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button className="btn" onClick={dorm}>稍后再议</button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="modal-text">{pendingEventDef.text}</div>
|
||||
|
||||
{squadPicking && (
|
||||
<div className="modal-squad" style={{ marginBottom: 14 }}>
|
||||
@@ -95,21 +123,7 @@ export function EventModal() {
|
||||
|
||||
<div className="modal-opts">
|
||||
{squadPicking ? (
|
||||
<>
|
||||
<button className="opt-btn" onClick={() => choose(raidIdx)}>
|
||||
出战!(点将已毕,共{squadSel.length}人)
|
||||
<span className="hint">依所选人手迎击来犯之敌</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => {
|
||||
setSquadPicking(false)
|
||||
setSquadSel([])
|
||||
}}
|
||||
>
|
||||
返回重选
|
||||
</button>
|
||||
</>
|
||||
<div className="dim2">依选迎战(若现选不满员将由家族精锐补齐)</div>
|
||||
) : (
|
||||
pendingEventDef.options.map((o, i) => {
|
||||
const isRaid = raidIdx === i
|
||||
@@ -123,7 +137,6 @@ export function EventModal() {
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalShell>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import { TECHNIQUES } from '../../game/data/techniques'
|
||||
import { ITEMS, ARTIFACT_POWER } from '../../game/data/items'
|
||||
import { combatPowerOf } from '../../game/engine/systems/combat'
|
||||
import { lifespanOf } from '../../game/engine/systems/lifecycle'
|
||||
import { perAttemptChance } from '../../game/engine/systems/cultivation'
|
||||
import { POSTS, POST_ORDER } from '../../game/data/posts'
|
||||
import { ModalShell } from './ModalShell'
|
||||
import { buildBiography } from '../../game/core/biography'
|
||||
|
||||
function aspName(c: Character): string {
|
||||
@@ -55,9 +57,31 @@ export function MemberModal({ member }: { member: Character }) {
|
||||
const power = member.alive ? combatPowerOf(w, member) : 0
|
||||
const lifespan = lifespanOf(w, member)
|
||||
|
||||
const close = () => setSelected(undefined)
|
||||
const confirmBreak = () => {
|
||||
const chance = Math.round(perAttemptChance(w, member) * 100)
|
||||
if (window.confirm(`${member.name} 冲击【${nextName}】,成功率约 ${chance}%。或以族运为注?`)) {
|
||||
w.assistedBreakthrough(member.id)
|
||||
bump()
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="modal-outer" onClick={() => setSelected(undefined)}>
|
||||
<div className="modal member-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<ModalShell
|
||||
title={member.name}
|
||||
category={member.alive ? '第' + member.generation + '代 · ' + (member.gender === 'male' ? '男' : '女') + ' · ' + w.ageOf(member) + '岁' : '第' + member.generation + '代 · 已故'}
|
||||
width={740}
|
||||
onClose={close}
|
||||
footer={
|
||||
<>
|
||||
{member.children.length > 0 && (
|
||||
<span className="dim2">
|
||||
子女:{member.children.map((cid) => s.members[cid]?.name ?? '?').join('、')}
|
||||
</span>
|
||||
)}
|
||||
<button className="btn" onClick={close}>阖上</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="modal-title" style={{ fontSize: '1.4rem' }}>
|
||||
{member.name}
|
||||
{!member.alive && <span className="bad" style={{ fontSize: '1rem' }}> · 殁</span>}
|
||||
@@ -129,10 +153,7 @@ export function MemberModal({ member }: { member: Character }) {
|
||||
{member.alive && member.realmProgress >= 100 && member.realm.major !== 'spirit' && (
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={() => {
|
||||
w.assistedBreakthrough(member.id)
|
||||
bump()
|
||||
}}
|
||||
onClick={confirmBreak}
|
||||
>
|
||||
冲击{nextName}
|
||||
</button>
|
||||
@@ -308,15 +329,6 @@ export function MemberModal({ member }: { member: Character }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
|
||||
{member.children.length > 0 && (
|
||||
<span className="dim2">
|
||||
子女:{member.children.map((cid) => s.members[cid]?.name ?? '?').join('、')}
|
||||
</span>
|
||||
)}
|
||||
<button className="btn" onClick={() => setSelected(undefined)}>阖上</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalShell>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useEffect, type ReactNode } from 'react'
|
||||
|
||||
export interface ModalShellProps {
|
||||
title: string
|
||||
category?: string
|
||||
width?: number
|
||||
allowOuter?: boolean
|
||||
onClose: () => void
|
||||
footer?: ReactNode
|
||||
footerAlign?: 'right' | 'center' | 'space'
|
||||
children: ReactNode
|
||||
closeLabel?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一弹框壳(0.1.12):
|
||||
* - 右上独立关闭钮(与内容完全分离)
|
||||
* - ESC 键关闭
|
||||
* - 底部独立一排(footer)与内容区明显区分(分隔线 + 底色差异 + 粘性)
|
||||
* - 宽度 min(92vw, Npx),零横向滚动条
|
||||
*/
|
||||
export function ModalShell({
|
||||
title,
|
||||
category,
|
||||
width = 680,
|
||||
allowOuter = true,
|
||||
onClose,
|
||||
footer,
|
||||
footerAlign = 'right',
|
||||
children,
|
||||
closeLabel = '关闭'
|
||||
}: ModalShellProps) {
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.stopPropagation()
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [onClose])
|
||||
|
||||
return (
|
||||
<div className="modal-outer" onClick={allowOuter ? onClose : undefined}>
|
||||
<div className="modal" style={{ width: `min(92vw, ${width}px)` }} onClick={(e) => e.stopPropagation()} data-modal-kind="shell">
|
||||
<button className="modal-close" onClick={onClose} title={closeLabel} aria-label={closeLabel}>
|
||||
×
|
||||
</button>
|
||||
<div className="modal-title">{title}</div>
|
||||
{category && <div className="modal-cat">{category}</div>}
|
||||
<div className="modal-body">{children}</div>
|
||||
{footer && (
|
||||
<div className={`modal-footer ${footerAlign === 'center' ? 'center' : footerAlign === 'space' ? 'space' : ''}`}>
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { YearlyReport } from '../../game/types/domain'
|
||||
import { ModalShell } from './ModalShell'
|
||||
|
||||
export function PaperModal() {
|
||||
const report = useGameStore((s) => s.paperReport)
|
||||
@@ -10,10 +11,13 @@ export function PaperModal() {
|
||||
const y = report.year
|
||||
|
||||
return (
|
||||
<div className="modal-outer" onClick={closePaper}>
|
||||
<div className="modal yearpaper-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-title" style={{ fontSize: '1.3rem' }}>岁 末 族 簿</div>
|
||||
<div className="modal-cat">{y}年 全 族 收 支 与 人 丁 情 形</div>
|
||||
<ModalShell
|
||||
title="岁 末 族 簿"
|
||||
category={`${y}年 全 族 收 支 与 人 丁 情 形`}
|
||||
width={620}
|
||||
onClose={closePaper}
|
||||
footer={<button className="btn btn-primary" onClick={closePaper}>翻 过 此 页</button>}
|
||||
>
|
||||
<div className="modal-text" style={{ textAlign: 'center', fontSize: '0.98rem', lineHeight: 2.1 }}>
|
||||
<div>灵石盈亏 <b className={nets >= 0 ? 'good' : 'bad'}>{nets >= 0 ? `+${nets}` : nets}</b> 枚</div>
|
||||
<div>人丁 <b className="good">新增 {report.births}</b> · <b className="bad">辞世 {report.deaths}</b></div>
|
||||
@@ -21,12 +25,6 @@ export function PaperModal() {
|
||||
{report.deaths > report.births && <div className="bad">丁口在减 —— 传宗接代是头等大事</div>}
|
||||
{report.births > report.deaths && <div className="good">丁口渐旺,香火有望。</div>}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<button className="btn btn-primary" onClick={closePaper}>
|
||||
翻 过 此 页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalShell>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { LegacyArch } from '../../game/core/legacy'
|
||||
import { sBell } from '../sound'
|
||||
import { ModalShell } from './ModalShell'
|
||||
|
||||
function dimLabel(k: string): string {
|
||||
const map: Record<string, string> = {
|
||||
@@ -13,12 +14,34 @@ function dimLabel(k: string): string {
|
||||
}
|
||||
|
||||
export function ResolveModal({ arch, onClose }: { arch: LegacyArch; onClose: () => void }) {
|
||||
const close = onClose
|
||||
return (
|
||||
<div className="modal-outer">
|
||||
<div className="modal resolve-modal">
|
||||
<div className="modal-cat" style={{ color: '#97835c', letterSpacing: 4 }}>
|
||||
—— 百年望气 · 定鼎之文 ——
|
||||
</div>
|
||||
<ModalShell
|
||||
title={`「${arch.title}」`}
|
||||
category={'—— 百年望气 · 定鼎之文 ——'}
|
||||
width={700}
|
||||
onClose={close}
|
||||
footer={
|
||||
<>
|
||||
<button className="btn" onClick={close}>暂不落印</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
const w = useGameStore.getState().world
|
||||
if (w) {
|
||||
sBell()
|
||||
w.resolveLegacyNow()
|
||||
useGameStore.getState().bump()
|
||||
useGameStore.getState().refreshSlots()
|
||||
}
|
||||
close()
|
||||
}}
|
||||
>
|
||||
落 印 定 局
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="resolve-title">「{arch.title}」</div>
|
||||
<div className="resolve-dims">
|
||||
{Object.entries(arch.dims)
|
||||
@@ -46,26 +69,7 @@ export function ResolveModal({ arch, onClose }: { arch: LegacyArch; onClose: ()
|
||||
))}
|
||||
</div>
|
||||
<div className="resolve-stamp">仙途家族志 · 青册</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 10, marginTop: 14 }}>
|
||||
<button className="btn" onClick={onClose}>暂不落印</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
const w = useGameStore.getState().world
|
||||
if (w) {
|
||||
sBell()
|
||||
w.resolveLegacyNow()
|
||||
useGameStore.getState().bump()
|
||||
useGameStore.getState().refreshSlots()
|
||||
}
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
落 印 定 局
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalShell>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useGameStore } from '../store'
|
||||
import { npcById } from '../../game/data/npcs'
|
||||
import { giftNpc, makePeace, marryNpcFamily } from '../../game/engine/systems/diplomacy'
|
||||
import { giftNpc, makePeace, marryNpcFamily, calcGiftGain, GIFT_TIERS } from '../../game/engine/systems/diplomacy'
|
||||
import { useMemo } from 'react'
|
||||
import { MAJOR_NAMES } from '../../game/data/realms'
|
||||
|
||||
@@ -50,9 +50,18 @@ export default function DiplomacyPanel() {
|
||||
<span className="dim2">{MAJOR_NAMES[def.leaderRealm]}宗主</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<button className="btn btn-sm" onClick={() => { giftNpc(w, npc.id, 120); bump() }}>
|
||||
赠礼120
|
||||
</button>
|
||||
{GIFT_TIERS.map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
className="btn btn-sm"
|
||||
disabled={w.state.family.stones < t}
|
||||
title={`关系 +${calcGiftGain(t)}`}
|
||||
onClick={() => { giftNpc(w, npc.id, t); bump() }}
|
||||
>
|
||||
赠礼{t}(+{calcGiftGain(t)})
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={npc.relation < 25 || married}
|
||||
|
||||
@@ -64,6 +64,7 @@ export default function MarketPanel() {
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={fam.stones < price(item.id) * count}
|
||||
title={fam.stones < price(item.id) * count ? '灵石不足' : ''}
|
||||
onClick={() => {
|
||||
buyItem(w, item.id, count)
|
||||
bump()
|
||||
@@ -97,8 +98,18 @@ export default function MarketPanel() {
|
||||
<td>
|
||||
{fam.buildings['danfang'] && (
|
||||
<>
|
||||
<button className="btn btn-sm" onClick={() => { w.craftPill('qiyuan'); bump() }}>炼气丹</button>{' '}
|
||||
<button className="btn btn-sm" onClick={() => { w.craftPill('ningyuan'); bump() }}>炼元丹</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={(inv['lingcao'] ?? 0) < 15 || fam.stones < 20}
|
||||
title="需灵草15+灵石20"
|
||||
onClick={() => { w.craftPill('qiyuan'); bump() }}
|
||||
>炼气丹</button>{' '}
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={(inv['lingcao'] ?? 0) < 25 || (inv['beastcore'] ?? 0) < 4 || fam.stones < 60}
|
||||
title="需灵草25+兽核4+灵石60"
|
||||
onClick={() => { w.craftPill('ningyuan'); bump() }}
|
||||
>炼元丹</button>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -69,6 +69,19 @@ export default function TerritoryPanel() {
|
||||
) : (
|
||||
<span className="gold">已臻极致</span>
|
||||
)}
|
||||
{id === 'cangshu' && (
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={(fam.buildings['cangshu'] ?? 0) < 3 || fam.stones < 150 || world.state.year - ((fam.flag['sutraCD'] as number) ?? -9) < 2}
|
||||
title={((fam.buildings['cangshu'] ?? 0) < 3 ? '需藏书阁3级' : fam.stones < 150 ? '需150灵石' : '两年一访')}
|
||||
onClick={() => {
|
||||
world.seekSutra()
|
||||
bump()
|
||||
}}
|
||||
>
|
||||
求经
|
||||
</button>
|
||||
)}
|
||||
{id === 'zongci' && (
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
|
||||
Reference in New Issue
Block a user