| <!doctype html> |
| <html lang="zh-CN" data-flavor="mocha"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <meta name="color-scheme" content="dark light" /> |
| <meta name="theme-color" content="#1e1e2e" /> |
| <title>小镇角落小店</title> |
| <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=Inter:wght@400;600;700&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet" /> |
| <link rel="stylesheet" href="/play/styles.css" /> |
| <script src="https://unpkg.com/vue@3.5.13/dist/vue.global.prod.js"></script> |
| </head> |
| <body> |
| <div id="app" v-cloak> |
| <a class="skip-link" href="#main">跳到主内容</a> |
|
|
| <header class="topbar" role="banner"> |
| <div class="brand"> |
| <div class="logo" aria-hidden="true">🌷</div> |
| <div> |
| <h1>小镇角落小店</h1> |
| <p class="guide">{{ view?.guide || '正在布置小店…' }}</p> |
| </div> |
| </div> |
|
|
| <nav class="phase-track" v-if="view" aria-label="营业时辰"> |
| <span |
| v-for="p in view.phases" |
| :key="p.id" |
| class="phase-pill" |
| :class="{ on: p.active }" |
| :aria-current="p.active ? 'step' : null" |
| >{{ p.label }}</span> |
| </nav> |
|
|
| <div class="top-meta" v-if="view"> |
| <strong>{{ view.meta.day_label }}</strong> |
| <span>{{ view.meta.phase_zh }}</span> |
| <span class="money-pill" aria-label="店内现金">¥ {{ view.wallet }}</span> |
| <div class="flavor-switch" role="group" aria-label="Catppuccin 主题"> |
| <button |
| type="button" |
| :aria-pressed="flavor === 'mocha'" |
| @click="setFlavor('mocha')" |
| >Mocha</button> |
| <button |
| type="button" |
| :aria-pressed="flavor === 'latte'" |
| @click="setFlavor('latte')" |
| >Latte</button> |
| </div> |
| </div> |
| </header> |
|
|
| <main id="main" class="shell" v-if="view"> |
| <section class="stage-card" :class="'light-' + view.atmosphere.light" aria-label="店铺舞台"> |
| <div class="stage-bg" :style="bgStyle" aria-hidden="true"></div> |
| <canvas ref="canvas" width="576" height="480" role="img" aria-label="店铺俯视图"></canvas> |
|
|
| <div class="floaters" aria-live="polite"> |
| <div v-for="f in floaters" :key="f.id" class="floater" :class="f.kind">{{ f.text }}</div> |
| </div> |
|
|
| <div class="mood-bar"> |
| <span aria-hidden="true">💭</span> |
| <span>{{ view.atmosphere.mood_line || '店里还很安静。' }}</span> |
| <span class="live" v-if="view.agents.length">店内 {{ view.agents.length }} 人</span> |
| <span class="live dim" v-else-if="view.ui.can_tick">等待客人中…</span> |
| </div> |
|
|
| <div class="guest-rail" aria-label="今日来客"> |
| <div class="rail-title">今日来客</div> |
| <div class="rail-body" v-if="(view.guests_today || []).length"> |
| <div |
| class="guest-card" |
| v-for="g in view.guests_today" |
| :key="g.agent_id" |
| :class="{ left: g.left, here: isHere(g.agent_id) }" |
| > |
| <img v-if="g.portrait" :src="'/' + g.portrait" :alt="g.name + ' 肖像'" /> |
| <div class="avatar-fallback" v-else aria-hidden="true">{{ (g.name || '?')[0] }}</div> |
| <div> |
| <b>{{ g.name }}</b> |
| <small>{{ g.kind_zh }} · {{ g.left ? '已离开' : g.state_zh }}</small> |
| </div> |
| </div> |
| </div> |
| <div class="rail-empty" v-else> |
| {{ view.meta.phase === 'PREP' ? '开门后,客人会出现在这里。' : '还没有客人进店,再等等…' }} |
| </div> |
| </div> |
| </section> |
|
|
| <aside class="hud" aria-label="经营面板"> |
| <div class="card hero-actions"> |
| <template v-if="view.meta.phase === 'PREP'"> |
| <h2>营业前 · 开张准备</h2> |
| <p class="hint ok">现在可以进货、上架、调灯光。</p> |
| <button class="primary big" @click="openShop" :disabled="busy">开始营业</button> |
| <div class="btn-grid"> |
| <button type="button" @click="quickPrep" :disabled="busy">一键备货</button> |
| <button type="button" class="ghost" @click="newGame" :disabled="busy">新开一局</button> |
| </div> |
| </template> |
|
|
| <template v-else-if="view.meta.phase === 'NIGHT'"> |
| <h2>夜晚 · 打烊</h2> |
| <div class="stat-grid"> |
| <div><em>进店</em><b>{{ view.stats.enters }}</b></div> |
| <div><em>成交</em><b>{{ view.stats.sales }}</b></div> |
| <div><em>营收</em><b>¥{{ view.stats.revenue }}</b></div> |
| </div> |
| <button class="primary big" @click="sleep" :disabled="busy">入睡 · 下一天</button> |
| <button type="button" class="ghost" @click="newGame" :disabled="busy">新开一局</button> |
| </template> |
|
|
| <template v-else> |
| <h2>营业中</h2> |
| <div class="stat-grid"> |
| <div><em>进店</em><b>{{ view.stats.enters }}</b></div> |
| <div><em>成交</em><b>{{ view.stats.sales }}</b></div> |
| <div><em>营收</em><b>¥{{ view.stats.revenue }}</b></div> |
| </div> |
| <div class="btn-grid"> |
| <button type="button" class="stable" @click="togglePause"> |
| {{ view.meta.paused ? '继续' : '暂停' }} |
| </button> |
| <button type="button" class="stable" @click="tickOnce" :disabled="view.meta.paused"> |
| 推进一步 |
| </button> |
| <button type="button" class="stable" @click="fastNight">快进到夜晚</button> |
| <button type="button" class="ghost stable" @click="newGame">新开一局</button> |
| </div> |
| <div class="speed-row"> |
| <label for="speed">自动流逝</label> |
| <select id="speed" v-model.number="speed" @change="setupAuto"> |
| <option :value="0">关闭</option> |
| <option :value="350">正常</option> |
| <option :value="180">两倍速</option> |
| <option :value="90">四倍速</option> |
| </select> |
| </div> |
| <p class="hint">营业中不能进货;到「夜晚」或第二天「营业前」再补货。</p> |
| </template> |
| </div> |
|
|
| <div class="card" v-if="view.meta.phase === 'PREP' || view.meta.phase === 'NIGHT'"> |
| <h2>进货与上架</h2> |
| <p class="hint" :class="{ ok: view.ui.can_restock }">{{ view.ui.restock_hint }}</p> |
| <div class="form-row"> |
| <select v-model="restockId" :disabled="!view.ui.can_restock" aria-label="进货商品"> |
| <option v-for="p in catalogOptions" :key="'r'+p.product_id" :value="p.product_id"> |
| {{ p.name }}(¥{{ p.price }}) |
| </option> |
| </select> |
| <button type="button" @click="restock" :disabled="busy || !view.ui.can_restock">进货 +1</button> |
| </div> |
| <p class="hint" :class="{ ok: view.ui.can_stock_display }">{{ view.ui.stock_hint }}</p> |
| <div class="form-row triple" v-if="view.ui.can_stock_display"> |
| <select v-model="stockPid" aria-label="上架商品"> |
| <option v-for="p in stockOptions" :key="'s'+p.product_id" :value="p.product_id"> |
| {{ p.name }} ×{{ p.qty ?? '—' }} |
| </option> |
| </select> |
| <select v-model="stockSlot" aria-label="货架位"> |
| <option v-for="d in view.displays" :key="d.slot" :value="d.slot"> |
| {{ d.name }} / {{ d.slot }} |
| </option> |
| </select> |
| <button type="button" @click="stock" :disabled="busy">上架</button> |
| </div> |
| <div class="btn-grid three light-row" role="group" aria-label="灯光"> |
| <button type="button" :class="{ active: view.atmosphere.light==='warm' }" @click="setLight('warm')">暖光</button> |
| <button type="button" :class="{ active: view.atmosphere.light==='cool' }" @click="setLight('cool')">冷光</button> |
| <button type="button" :class="{ active: view.atmosphere.light==='off' }" @click="setLight('off')">关灯</button> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <h2>氛围</h2> |
| <div class="atm" v-for="d in view.atmosphere.dims" :key="d.key"> |
| <span>{{ d.label }}</span> |
| <div class="bar" role="progressbar" :aria-valuenow="d.pct" aria-valuemin="0" aria-valuemax="100" :aria-label="d.label"> |
| <i :style="{ width: d.pct + '%' }"></i> |
| </div> |
| <span class="pct">{{ d.pct }}</span> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <h2>店内见闻</h2> |
| <ul class="log" aria-live="polite"> |
| <li v-for="(line, i) in reversedLog" :key="i">{{ line }}</li> |
| </ul> |
| </div> |
| </aside> |
| </main> |
|
|
| <div class="loading" v-else role="status"> |
| <div class="spinner" aria-hidden="true"></div> |
| <p>{{ error || '正在布置小店…' }}</p> |
| <button v-if="error" type="button" class="primary" @click="newGame">重试</button> |
| </div> |
|
|
| <div class="modal" v-if="showNight && view?.night" @click.self="showNight=false" role="dialog" aria-modal="true" aria-labelledby="night-title"> |
| <div class="modal-card"> |
| <div class="modal-kicker">夜记</div> |
| <h2 id="night-title">{{ view.meta.day_label }} · 打烊了</h2> |
| <p class="lead">{{ view.night.mood }}</p> |
| <p class="diary">「{{ view.night.diary }}」</p> |
| <div class="night-stats"> |
| <div><em>进店</em><b>{{ view.night.enters }}</b></div> |
| <div><em>成交</em><b>{{ view.night.sales }}</b></div> |
| <div><em>营收</em><b>¥{{ view.night.revenue }}</b></div> |
| </div> |
| <p class="vibe" v-if="view.night.top_dims?.length">今日气息:{{ view.night.top_dims.join(' · ') }}</p> |
| <button type="button" class="primary wide" @click="showNight=false">收下这份夜晚</button> |
| </div> |
| </div> |
|
|
| <div class="modal" v-if="showHelp" @click.self="showHelp=false" role="dialog" aria-modal="true" aria-labelledby="help-title"> |
| <div class="modal-card"> |
| <div class="modal-kicker">怎么玩 · Catppuccin</div> |
| <h2 id="help-title">三步经营你的花店</h2> |
| <ol class="help-list"> |
| <li><b>营业前</b>:进货、上架,然后点「开始营业」</li> |
| <li><b>营业中</b>:自动流逝,看客人进店、闲逛、结账</li> |
| <li><b>夜晚</b>:读夜记,点「入睡」进入下一天</li> |
| </ol> |
| <p class="hint ok">界面使用 Catppuccin 配色;右上角可在 Mocha / Latte 间切换。</p> |
| <button type="button" class="primary wide" @click="showHelp=false">我知道了,开始</button> |
| </div> |
| </div> |
|
|
| <div class="toast-stack" aria-live="polite" aria-relevant="additions"> |
| <div v-for="t in toasts" :key="t.id" class="toast" :class="t.kind">{{ t.text }}</div> |
| </div> |
|
|
| <footer class="foot"> |
| 毕业设计演示 · Vue 3 · |
| <span>Catppuccin {{ flavor === 'mocha' ? 'Mocha' : 'Latte' }}</span> |
| </footer> |
| </div> |
| <style> |
| .skip-link { |
| position: absolute; |
| left: -9999px; |
| top: 0; |
| z-index: 100; |
| padding: 0.5rem 0.75rem; |
| background: var(--ctp-mauve, #cba6f7); |
| color: var(--ctp-crust, #11111b); |
| border-radius: 0 0 8px 0; |
| font-weight: 700; |
| } |
| .skip-link:focus { |
| left: 0; |
| } |
| </style> |
| <script src="/play/app.js"></script> |
| </body> |
| </html> |
|
|