docker000 / web /play /index.html
seachen's picture
Playable frontend: FastAPI + canvas shop UI
9fdeb41 verified
Raw
History Blame Contribute Delete
12 kB
<!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>