FateForge / fateforge /static /index.html
FateForge
Deploy to HF Spaces: Vue SPA + FastAPI, Docker SDK, LLM offline fallback
c9e11de
Raw
History Blame Contribute Delete
26 kB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>FateForge</title>
<link rel="icon" type="image/png" href="/static/assets/favicon.png" />
<!-- Tailwind Play CDN (无构建步骤) -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
ink: { 50:'#f4ede0', 100:'#e6dcc4', 200:'#c9bfa1', 300:'#8a7e62', 400:'#5a4f3d', 500:'#2a241c', 600:'#1a1612', 700:'#0f0c09' },
skyq: { 500:'#5d7a8a', 600:'#3d5a6a' },
jade: { 500:'#7ba88a' },
bloodred: { 500:'#7a2a1a' },
icelake: { 500:'#3a6878' },
// 焦茶色(略带红棕的墨色) —— 用于 placeholder 和墨晕底色
burntea: { 500: '#5a4a36', 600: '#3a2e22' },
// 墨绿 —— "遁其一"按钮底色
inkgreen: { 500: '#3a5848' },
// 古金 —— "遁其一"按钮线/字
goldink: { 500: '#b8923c', 600: '#8a6c28' },
},
fontFamily: {
shu: ['"Ma Shan Zheng"','"STKaiti"','"KaiTi"','"STZhongsong"','serif'],
kai: ['"KaiTi"','"STKaiti"','serif'],
sans: ['"Noto Serif SC"','"Noto Serif"','serif'],
serif:['"Noto Serif SC"','"Noto Serif"','serif'],
},
}
}
}
</script>
<!-- 字体: Google Fonts (墨韵必备) -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&family=Noto+Serif+SC:wght@300;500;700&family=Noto+Serif:ital,wght@0,400;0,600;1,400&family=ZCOOL+XiaoWei&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/static/style.css" />
<!-- Vue 3 (全局构建) -->
<script src="https://unpkg.com/vue@3.4.27/dist/vue.global.prod.js"></script>
</head>
<body :class="lang === 'en' ? 'lang-en' : 'lang-zh'" class="font-sans antialiased select-none overflow-hidden h-screen">
<div id="app" class="relative w-full h-full overflow-hidden">
<!-- ==================== 烟雾背景 (所有画面通用) ==================== -->
<div class="mist-layer" aria-hidden="true">
<div v-for="i in 8" :key="'m'+i" class="mist-blob"
:style="mistStyle(i)"></div>
</div>
<!-- ==================== 右上角 极简竹节式菜单 (仅首页显示) ==================== -->
<nav v-if="scene === 'yuanqi'" class="top-menu" aria-label="top menu">
<span class="top-menu-bracket" aria-hidden="true"></span>
<button @click="openBook" class="top-menu-btn" :title="t('menu_book')">
<span class="top-menu-glyph"></span>
<span class="top-menu-label">{{ t('menu_book') }}</span>
</button>
<span class="top-menu-divider" aria-hidden="true"></span>
<button @click="toggleLang" class="top-menu-btn top-menu-lang" :title="t('menu_lang')">
{{ t('menu_lang') }}
</button>
<span class="top-menu-divider" aria-hidden="true"></span>
<button @click="openAbout" class="top-menu-btn" :title="t('menu_about')">
{{ t('menu_about') }}
</button>
<span class="top-menu-bracket" aria-hidden="true"></span>
</nav>
<!-- ==================== 画面一 缘起 ==================== -->
<transition name="fade-slow">
<section v-if="scene === 'yuanqi'" class="scene bg-element-wood flex flex-col items-center justify-center px-6">
<h1 class="text-2xl md:text-3xl text-ink-100 font-shu text-center leading-loose tracking-widest opacity-90 mb-12 ink-seep whitespace-pre-line">
{{ t('yuanqi_h1') }}
</h1>
<textarea v-model="question"
rows="3"
maxlength="120"
:placeholder="t('yuanqi_placeholder')"
class="yuanqi-textarea whitespace-pre-line"></textarea>
<div class="mt-2 text-xs text-ink-300/60 tracking-wider">{{ question.length }} / 120</div>
<button @click="startDivination"
:disabled="!question.trim()"
:class="['cast-btn', 'mt-10 px-12 py-3 font-shu text-xl tracking-[0.4em]',
question.trim() ? 'is-active' : 'is-disabled']">
<span class="cast-btn-text">{{ t('yuanqi_start') }}</span>
</button>
</section>
</transition>
<!-- ==================== 画面二 窥机 ==================== -->
<transition name="fade-slow">
<section v-if="scene === 'kuiji'" class="scene bg-element-earth flex flex-col items-center justify-center">
<!-- 阶段 A: 龟壳 -->
<div v-if="phase === 'turtle'" class="flex flex-col items-center">
<p class="mb-6 text-ink-200/80 font-kai tracking-widest kuiji-text text-center">
{{ t('kuiji_turtle_hint_p1') }}<span class="text-ink-50"> {{ t('kuiji_turtle_hint_p2') }}</span>
</p>
<p class="mb-2 text-ink-300/70 font-kai kuiji-subtext">
{{ t('kuiji_clicked') }} <span class="text-ink-50 kuiji-number mx-1">{{ clicks }}</span> / 6 {{ t('yao_unit') }}
</p>
<!-- 龟壳:优先用户自备 PNG,无则退到 SVG 占位 -->
<div class="turtle-wrap" @click="onTurtleClick" :class="{ shake: shaking }">
<img :src="$root.assets.turtle" alt="龟壳" class="turtle-img" draggable="false"
@error="$event.target.src='/static/assets/turtle.svg'"/>
<div class="click-hint">{{ t('kuiji_touch') }}</div>
</div>
</div>
<!-- 阶段 B: 铜钱翻转 -->
<div v-if="phase === 'coins'" class="flex flex-col items-center">
<p class="mb-8 text-ink-200/80 font-kai tracking-widest kuiji-text">
{{ t('kuiji_yao') }} <span class="text-ink-50 kuiji-number">{{ clicks }}</span> {{ t('yao_unit') }}
</p>
<div class="flex gap-6">
<div v-for="(c, i) in currentCoins" :key="i"
:class="['coin', c.flipped ? 'flipped' : '']">
<div class="coin-inner">
<div class="coin-face">
<img :src="c.face === 3 ? $root.assets.coinHead : $root.assets.coinTail"
:alt="c.face === 3 ? t('kuiji_coin_head') : t('kuiji_coin_tail')"
@error="$event.target.style.display='none'"/>
</div>
</div>
</div>
</div>
<p class="mt-8 text-ink-300/70 font-kai kuiji-subtext">
<span v-if="lastLineValue !== null">
<span v-if="lastLineValue === 9" class="text-jade-500">━ ○ {{ t('kuiji_old_yang') }}</span>
<span v-else-if="lastLineValue === 8" class="text-ink-50">━━━ {{ t('kuiji_young_yang') }}</span>
<span v-else-if="lastLineValue === 6" class="text-bloodred-500">━ × {{ t('kuiji_old_yin') }}</span>
<span v-else class="text-ink-300">━ ━ {{ t('kuiji_young_yin') }}</span>
</span>
</p>
</div>
<!-- 阶段 C: 六爻成形 -->
<div v-if="phase === 'hexagon'" class="flex flex-col items-center">
<div class="ink-paper ink-paper-rounded w-full max-w-2xl p-8 md:p-12 flex flex-col items-center">
<p class="mb-4 text-burntea-500 font-shu kuiji-section-title ink-seep">{{ t('hex_done') }}</p>
<div class="hex-lines">
<div v-for="(g, idx) in lines" :key="idx"
:class="['hex-line', g.yang ? 'yang' : 'yin', g.moving ? 'moving' : '']">
<span v-if="g.moving" class="line-mark">{{ g.value === 9 ? '○' : '×' }}</span>
</div>
</div>
<p class="mt-8 text-ink-700 font-shu text-5xl tracking-widest ink-seep">{{ displayHexName }}</p>
<p class="mt-3 text-burntea-500 font-shu hex-judgment-text">{{ t('hex_judgment') }}{{ displayHexJudgment }}</p>
<button @click="goScene3" class="reveal-btn mt-10 px-10 py-3 font-shu text-xl tracking-[0.4em]">
<span class="reveal-btn-text">{{ t('hex_reveal') }}</span>
</button>
</div>
</div>
</section>
</transition>
<!-- ==================== 画面三 破迷 (听天命·窥天机) ==================== -->
<transition name="fade-slow">
<section v-if="scene === 'pomi'"
:class="['scene flex flex-col items-center px-6', pomiBgClass, ominousClass, levelToneClass]">
<div class="hex-seal mb-4">
<div :class="auspiciousLevelClass">{{ displayLevel }}</div>
<div class="text-ink-700 font-shu mt-2 text-3xl tracking-widest">{{ displayHexName }}</div>
<div v-if="lang === 'zh'" class="text-burntea-500 font-kai mt-1 kuiji-subtext tracking-widest">
{{ upperTrigram }}上 · {{ lowerTrigram }}下 · {{ t('hex_ordinal_prefix') }} {{ hexOrder }} {{ t('hex_ordinal_suffix') }}
</div>
<div v-else class="text-burntea-500 font-kai mt-1 kuiji-subtext tracking-widest">
{{ upperTrigram }} / {{ lowerTrigram }} · {{ displayLevel }}
</div>
</div>
<div class="w-full max-w-2xl text-center mt-4 mb-6">
<div class="text-burntea-500 font-kai kuiji-subtext tracking-widest">
{{ t('pomi_question_prefix') }} <span class="text-ink-700">{{ question }}</span>
</div>
</div>
<!-- 卡片: 文人书信感. 中文 max-w-3xl(默认), 大段卦辞自动 max-w-4xl; 英文 max-w-5xl(默认), 长卦辞 max-w-6xl. -->
<div :class="['ink-paper ink-paper-rounded ink-paper-floating ink-paper-scrollable flex flex-col items-center',
'w-full max-w-3xl xl:max-w-4xl p-10 md:p-14',
lang === 'en' ? 'xl:max-w-5xl 2xl:max-w-6xl md:p-12' : '']"
@scroll="onPomiScroll"
ref="pomiScrollRoot">
<!-- 加载态: 天机渗透中 (destinyText 还没出来) -->
<div v-if="!destinyText && !readingError"
class="text-center text-burntea-500 font-kai kuiji-subtext tracking-widest ink-seep-loop py-6">
{{ t('pomi_loading') }}
</div>
<!-- 错误态 -->
<div v-else-if="readingError"
class="text-bloodred-500 font-kai text-center kuiji-subtext">
{{ readingError }}
</div>
<!-- 凶/中平偏凶/大凶 的过渡态: 前路晦暗 → 文字消失 → 遁其一按钮 -->
<template v-else-if="isOminousFlow">
<!-- 阶段1: 解卦正文模糊 + 提示语 -->
<div v-if="pomiStage === 'obscure' || pomiStage === 'loading'" class="w-full">
<div class="reading-text font-kai whitespace-pre-wrap text-burntea-500 leading-loose hex-card-text"
:class="pomiStage === 'obscure' ? 'reading-text-blurred' : ''">
{{ destinyText }}
</div>
<div v-if="pomiStage === 'obscure'" class="mt-8 text-center text-burntea-500 font-kai kuiji-section-title ink-seep">
{{ t('pomi_obscure_l1') }}
</div>
<div v-if="pomiStage === 'obscure'" class="mt-3 text-center text-burntea-500 font-kai kuiji-section-title ink-seep">
{{ t('pomi_obscure_l2') }}
</div>
</div>
<!-- 阶段2: "遁其一" — 整个卡片中央显示按钮, 蒙版盖住下面的心法 -->
<div v-else-if="pomiStage === 'reveal_btn'" class="w-full relative">
<!-- 心法本体: 在卡片里自然铺开(永远渲染, 但被蒙版遮住) -->
<div v-if="remedyText" class="w-full">
<h3 class="font-shu text-2xl text-center tracking-widest text-burntea-500 mb-4 ink-seep">
{{ t('pomi_xintai') }} · {{ displayRemedy }}
</h3>
<div class="reading-text font-kai whitespace-pre-wrap hex-card-text"
:class="previewCleared ? 'text-ink-700' : 'text-burntea-500'">
{{ remedyText }}
</div>
<div class="w-full mt-12 mb-2"></div>
</div>
<!-- 蒙版: 盖住心法, 红色晕染 + 重模糊. previewCleared=true 时淡出 -->
<div class="reveal-veil" :class="{ 'is-lifted': previewCleared }" aria-hidden="true"></div>
<!-- 按钮: 居中浮在卡片中央 (absolute, 居中) -->
<div class="reveal-one-center">
<button @click="onRevealOne" class="reveal-one-btn"
:class="{ 'is-fading-out': fadingOut }">
{{ t('pomi_reveal_btn') }}
</button>
</div>
</div>
<!-- 阶段3: 心法已清晰, 下方直接显示"乾坤未定且循其一"按钮 -->
<div v-else-if="pomiStage === 'revealing' && !burning" class="w-full" :class="{ 'is-dispersing': dispersing }">
<h3 class="font-shu text-2xl text-center tracking-widest text-burntea-500 mb-4 ink-seep">
{{ t('pomi_xintai') }} · {{ displayRemedy }}
</h3>
<div class="reading-text font-kai whitespace-pre-wrap text-ink-700 leading-loose hex-card-text typing-text">
{{ remedyText }}
</div>
<div class="w-full mt-10 text-center">
<button @click="onBreakFree" class="breakfree-btn">
{{ t('pomi_breakfree') }}
</button>
</div>
</div>
<!-- 燃烧动画阶段 (短暂的过渡, 后被 revealing 取代) -->
<div v-else-if="pomiStage === 'revealing' && burning" class="w-full">
<div class="burn-flash is-burning" aria-hidden="true"></div>
<div class="burn-mist is-burning" aria-hidden="true"></div>
<div class="text-center text-burntea-500 font-kai kuiji-section-title ink-seep">
{{ t('pomi_burning') }}
</div>
</div>
</template>
<!-- 吉/中平/中平偏吉/大吉: lumped - 直接显示正文 + 心法 + 启程按钮 (滚到底才显示) -->
<template v-else>
<div v-if="destinyText" class="w-full">
<div class="reading-text font-kai whitespace-pre-wrap text-ink-700 leading-loose hex-card-text">
{{ destinyText }}
</div>
</div>
<div v-if="remedyText" class="w-full mt-6">
<h3 class="font-shu text-2xl text-center tracking-widest text-burntea-500 mb-4 ink-seep">
{{ t('pomi_xintai') }} · {{ displayRemedy }}
</h3>
<div class="reading-text font-kai whitespace-pre-wrap text-ink-700 leading-loose hex-card-text">
{{ remedyText }}
</div>
</div>
<!-- 启程按钮: 锦囊出现后直接显示 -->
<div class="w-full text-center mt-10">
<button @click="departToHome" class="depart-btn px-10 py-3 font-shu text-xl tracking-[0.4em]">
{{ t('pomi_depart') }}
</button>
</div>
</template>
</div>
</section>
</transition>
<!-- ==================== 画面四 自渡 (仅中吉/大吉/中平/中平偏吉 砍掉,此屏保留兼容) ==================== -->
<transition name="fade-slow">
<section v-if="scene === 'zidu'" :class="['scene bg-element-water flex flex-col items-center px-6 overflow-y-auto', levelToneClass]">
<h1 class="text-xl md:text-2xl font-shu tracking-widest text-center mt-4 ink-seep"
:class="auspicious === '吉' ? 'text-jade-500 jade-glow' : auspicious === '凶' ? 'text-bloodred-500' : 'text-ink-50'">
{{ auspicious === '吉' ? '天时已至,顺势而为。' : auspicious === '凶' ? '命虽天定,心能转境。' : '时机未至,静水流深。' }}<br/>
{{ ctaSub }}
</h1>
<div class="ink-paper ink-paper-rounded w-full max-w-2xl p-8 md:p-10 mt-6">
<h2 class="font-shu text-2xl tracking-[0.4em] mb-2 text-center"
:class="auspicious === '吉' ? 'text-jade-500' : auspicious === '凶' ? 'text-bloodred-500' : 'text-ink-50'">
{{ remedyTag }}
</h2>
<div class="text-ink-700 font-kai leading-loose text-base whitespace-pre-line">{{ remedyText }}</div>
<div class="mt-6 pt-4 border-t border-ink-300/30 text-xs text-burntea-500 font-kai text-center opacity-70">
{{ t('zidu_summary') }}{{ hexName }} ({{ upperTrigram }}上 · {{ lowerTrigram }}下) · {{ t('zidu_asked') }}{{ question }}
</div>
</div>
<div class="w-full max-w-2xl mt-8 mb-12 text-center select-none touch-none">
<p class="text-burntea-500 font-kai kuiji-subtext tracking-widest mb-3">
{{ t('zidu_swipe_hint_p1') }}<span class="text-ink-700"> {{ t('zidu_swipe_hint_p2') }} </span>{{ t('zidu_swipe_hint_p3') }}
</p>
<div class="swipe-zone"
@pointerdown="onSwipeStart"
@pointermove="onSwipeMove"
@pointerup="onSwipeEnd"
@pointercancel="onSwipeEnd"
@click="onSwipeClick"
ref="swipeZone"
role="button" :aria-label="t('zidu_swipe_aria')">
<div class="swipe-track" :style="swipeTrackStyle"></div>
<div class="swipe-handle" :style="swipeHandleStyle">
<span class="swipe-handle-glyph">{{ t('zidu_swipe_glyph') }}</span>
</div>
</div>
</div>
</section>
</transition>
<!-- ==================== 乾坤志 弹层 (历史) ==================== -->
<transition name="fade">
<div v-if="bookOpen" class="fixed inset-0 z-50 bg-white/95 backdrop-blur-sm overflow-y-auto"
@click.self="closeBook">
<div class="max-w-3xl mx-auto px-6 py-12">
<div class="flex items-center justify-between mb-8">
<h2 class="text-2xl font-shu text-ink-700 tracking-widest">{{ t('book_title') }}</h2>
<button @click="closeBook" class="text-burntea-500 hover:text-ink-700 kuiji-subtext font-kai">{{ t('book_close') }}</button>
</div>
<p class="text-burntea-500 font-kai text-xs mb-6 tracking-widest opacity-70">
{{ t('book_local_only') }}{{ history.length }} {{ t('book_count_unit') }}
</p>
<div v-if="!history.length" class="text-center text-burntea-500 font-kai mt-20 tracking-widest opacity-60">
{{ t('book_empty') }}
</div>
<article v-for="h in history" :key="h.id"
class="book-entry ink-paper ink-paper-rounded p-6 mb-4 cursor-pointer transition"
@click="toggleHistory(h.id)">
<div class="flex justify-between items-baseline mb-2">
<h3 class="text-ink-700 font-shu text-lg tracking-widest">
{{ h.hexName }}<span class="text-burntea-500 kuiji-subtext font-kai ml-2 opacity-60">{{ h.date }}</span>
</h3>
<span class="flex items-center gap-2">
<span :class="['text-xs font-kai tracking-widest px-2 py-0.5 rounded-sm',
h.auspicious === '吉' ? 'text-jade-500 border border-jade-500/30'
: h.auspicious === '凶' ? 'text-bloodred-500 border border-bloodred-500/30'
: 'text-burntea-500 border border-ink-300/30']">
{{ h.auspicious }}
</span>
<span class="text-burntea-500 text-xs font-kai opacity-60">{{ expandedId === h.id ? t('book_collapse') : t('book_expand') }}</span>
</span>
</div>
<p class="text-burntea-500 font-kai text-xs mb-3 opacity-70">{{ t('zidu_asked') }}{{ h.question }}</p>
<div v-if="expandedId !== h.id" class="text-burntea-500 font-kai kuiji-subtext leading-loose italic opacity-70">
<span v-if="h.destinyText">{{ h.destinyText.slice(0, 60) }}…</span>
<span v-else>({{ t('book_no_reading') }})</span>
</div>
<div v-else class="space-y-4 ink-seep">
<div>
<h4 class="text-ink-700 font-shu text-base tracking-widest mb-2">{{ t('book_hex_label') }}</h4>
<div class="text-burntea-500 font-kai kuiji-subtext">
<span v-if="h.upper && h.lower">{{ h.upper }}上 · {{ h.lower }}下</span>
<span v-if="h.changedName"> · {{ t('book_changed_to') }} {{ h.changedName }}</span>
<span v-if="h.moving && h.moving.length"> · {{ t('book_moving') }} {{ h.moving.map(i=>['初','二','三','四','五','上'][i-1]+'爻').join('、') }}</span>
</div>
<div v-if="h.judgment" class="text-burntea-500 font-kai text-xs mt-1 opacity-70">{{ t('hex_judgment') }}{{ h.judgment }}</div>
</div>
<div v-if="h.destinyText">
<h4 class="text-ink-700 font-shu text-base tracking-widest mb-2">{{ t('book_destiny_label') }}</h4>
<p class="text-ink-700 font-kai kuiji-subtext leading-loose whitespace-pre-wrap">{{ h.destinyText }}</p>
</div>
<div v-if="h.remedyText">
<h4 class="text-jade-500 font-shu text-base tracking-widest mb-2">{{ t('book_remedy_label') }}</h4>
<p class="text-ink-700 font-kai kuiji-subtext leading-loose whitespace-pre-wrap">{{ h.remedyText }}</p>
</div>
<div v-if="!h.destinyText && !h.remedyText && h.text"
class="text-ink-700 font-kai kuiji-subtext leading-loose whitespace-pre-wrap">
<h4 class="text-ink-700 font-shu text-base tracking-widest mb-2">{{ t('book_reading_label') }}</h4>
{{ h.text }}
</div>
<div class="text-right pt-2 border-t border-ink-300/20">
<button @click.stop="deleteHistory(h.id)"
class="text-burntea-500 hover:text-bloodred-500 text-xs font-kai opacity-60 hover:opacity-100">
{{ t('book_burn') }}
</button>
</div>
</div>
</article>
<div v-if="history.length" class="text-center mt-8">
<button @click="clearHistory" class="text-burntea-500 hover:text-bloodred-500 text-xs font-kai tracking-widest opacity-60 hover:opacity-100">
{{ t('book_clear_all') }}
</button>
</div>
</div>
</div>
</transition>
<!-- ==================== 关于 极简水墨卡片 ==================== -->
<transition name="fade">
<div v-if="aboutOpen" class="fixed inset-0 z-50 flex items-center justify-center px-6"
@click.self="closeAbout">
<div class="about-card ink-paper ink-paper-rounded ink-paper-floating w-full max-w-lg p-8 md:p-10">
<div class="flex items-center justify-between mb-6">
<h2 class="font-shu text-2xl text-ink-700 tracking-widest">{{ t('about_title') }}</h2>
<button @click="closeAbout" class="text-burntea-500 hover:text-ink-700 kuiji-subtext font-kai">{{ t('book_close') }}</button>
</div>
<article class="space-y-5 text-ink-700 font-kai leading-loose kuiji-subtext whitespace-pre-line">
<section>
<h3 class="font-shu text-base text-ink-700 tracking-widest mb-2">{{ t('about_priv_title') }}</h3>
<p>{{ t('about_priv_body') }}</p>
</section>
<section>
<h3 class="font-shu text-base text-ink-700 tracking-widest mb-2">{{ t('about_disc_title') }}</h3>
<p>{{ t('about_disc_body') }}</p>
</section>
</article>
</div>
</div>
</transition>
<!-- ==================== 启程回归遮罩 (吉局 → 模糊覆盖 → 返回首页) ==================== -->
<transition>
<div v-if="departing" class="depart-overlay" aria-hidden="true"></div>
</transition>
<!-- ==================== 自定义水墨确认弹窗 ==================== -->
<transition name="fade">
<div v-if="confirmDialog.open" class="fixed inset-0 z-[70] flex items-center justify-center px-6 ink-modal-mask"
@click.self="confirmDialogResolve(false)">
<div class="ink-modal" role="dialog" aria-modal="true">
<p class="ink-modal-text">{{ confirmDialog.message }}</p>
<div class="ink-modal-actions">
<button @click="confirmDialogResolve(false)" class="ink-modal-btn ink-modal-btn-cancel">
{{ confirmDialog.cancelLabel }}
</button>
<button @click="confirmDialogResolve(true)" class="ink-modal-btn ink-modal-btn-confirm">
{{ confirmDialog.confirmLabel }}
</button>
</div>
</div>
</div>
</transition>
<!-- ==================== 自定义水墨"二选一"选择弹窗 ==================== -->
<transition name="fade">
<div v-if="choiceDialog.open" class="fixed inset-0 z-[70] flex items-center justify-center px-6 ink-modal-mask"
@click.self="choiceDialogResolve('secondary')">
<div class="ink-modal" role="dialog" aria-modal="true">
<p class="ink-modal-text">{{ choiceDialog.message }}</p>
<div class="ink-modal-actions">
<button @click="choiceDialogResolve('secondary')" class="ink-modal-btn ink-modal-btn-cancel">
{{ choiceDialog.secondaryLabel }}
</button>
<button @click="choiceDialogResolve('primary')" class="ink-modal-btn ink-modal-btn-confirm">
{{ choiceDialog.primaryLabel }}
</button>
</div>
</div>
</div>
</transition>
<!-- ==================== 终场雾散 ==================== -->
<transition name="fade-slow">
<div v-if="mistCleared" class="finale-overlay">
<div class="finale-quote">
<p v-for="(l, i) in finalLines" :key="i" class="finale-line font-shu">{{ l }}</p>
<button @click="reset"
class="mt-16 px-10 py-2 font-shu text-base tracking-[0.4em] border border-jade-500/40 text-jade-500 hover:bg-jade-500/10 rounded-sm opacity-0"
style="animation: finaleLine 2s ease-out 5.5s forwards;">
{{ t('finale_restart') }}
</button>
</div>
</div>
</transition>
</div>
<script src="/static/app.js"></script>
</body>
</html>