个人页面更新

This commit is contained in:
2026-08-13 21:13:18 +08:00
parent 476201d714
commit 832eb4bea2
23 changed files with 7169 additions and 12 deletions

View File

@@ -1,6 +1,7 @@
<script setup>
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { Monitor, Smartphone } from 'lucide-vue-next'
import CommandPalette from './components/CommandPalette.vue'
import PromptSkillFab from './components/PromptSkillFab.vue'
@@ -47,13 +48,13 @@ onBeforeUnmount(() => {
title="桌面视图"
aria-label="桌面视图"
@click="togglePhone(false)"
>🖥</button>
><Monitor :size="17" :stroke-width="2" /></button>
<button
:class="{ active: phoneMode }"
title="手机预览"
aria-label="手机预览"
@click="togglePhone(true)"
>📱</button>
><Smartphone :size="17" :stroke-width="2" /></button>
</div>
<transition name="phone-pop">
@@ -103,6 +104,7 @@ onBeforeUnmount(() => {
border-radius: 50%;
border: none;
background: transparent;
color: #d7dae6;
font-size: 17px;
line-height: 1;
cursor: pointer;
@@ -111,6 +113,12 @@ onBeforeUnmount(() => {
opacity: 0.55;
transition: background 0.18s ease, opacity 0.18s ease, transform 0.18s ease;
}
.device-toggle button svg {
display: block;
}
.device-toggle button.active {
color: #fff;
}
.device-toggle button:hover {
opacity: 1;
transform: scale(1.08);

View File

@@ -4,6 +4,18 @@ import { corpProfiles } from '../../data/corpProfiles'
const c = corpProfiles.ai
/* 模型卡 */
const mcIdx = ref(-1)
const mc = ref(null)
function openMc(i) {
mcIdx.value = i
mc.value = c.cases[i]
}
function closeMc() {
mcIdx.value = -1
mc.value = null
}
const question = '用一句话介绍深思大模型'
const answer = '我是深思大模型128K 上下文、40+ 行业方案,既能陪你头脑风暴,也能钻进产线找瑕疵 —— 大事小事,先深思,再回答。'
const typedQ = ref('')
@@ -148,7 +160,7 @@ onBeforeUnmount(clearAll)
<h2 class="al-h2">已经在这些行业上岗</h2>
</div>
<div class="al-cases">
<article v-for="(p, i) in c.cases" :key="p.name" class="al-case" :style="{ '--d': i * 0.07 + 's' }">
<article v-for="(p, i) in c.cases" :key="p.name" class="al-case" :style="{ '--d': i * 0.07 + 's' }" @click="openMc(i)">
<div class="al-case-media">
<img :src="p.image" :alt="p.name" loading="lazy" />
<span class="al-case-no">CASE 0{{ i + 1 }}</span>
@@ -157,11 +169,53 @@ onBeforeUnmount(clearAll)
<span class="al-case-tag">{{ p.tag }}</span>
<h3>{{ p.name }}</h3>
<p>{{ p.desc }}</p>
<i class="al-case-more"> 查看模型卡</i>
</div>
</article>
</div>
</section>
<!-- 模型卡 -->
<Transition name="al-card">
<div v-if="mc" class="al-mask" @click.self="closeMc">
<article class="al-modelcard">
<button class="al-mc-x" aria-label="关闭" @click="closeMc"></button>
<header class="al-mc-head">
<p class="al-mc-path">deepthink / <b>{{ mc.name }}</b></p>
<div class="al-mc-badges">
<span>{{ mc.tag }}</span>
<span class="ok">production</span>
<span class="ctx">128K ctx</span>
</div>
</header>
<img class="al-mc-img" :src="mc.image" :alt="mc.name" />
<section class="al-mc-block">
<b># Model Description</b>
<p>{{ mc.story }}</p>
</section>
<section class="al-mc-block">
<b># Capabilities</b>
<ul>
<li v-for="h in mc.highlights" :key="h">{{ h }}</li>
</ul>
</section>
<div class="al-mc-cols">
<section class="al-mc-block">
<b># Release Notes</b>
<p v-for="t in mc.timeline" :key="t.at" class="al-mc-tl"><i>{{ t.at }}</i>{{ t.event }}</p>
</section>
<section class="al-mc-block">
<b># Evals</b>
<div class="al-mc-mx">
<div v-for="m in mc.metrics" :key="m.label"><b>{{ m.value }}</b><span>{{ m.label }}</span></div>
</div>
</section>
</div>
<footer class="al-mc-foot">License: 演示用途 · 模型可以很大,落地必须很轻 </footer>
</article>
</div>
</Transition>
<div class="al-stats">
<div v-for="s in c.stats" :key="s.label"><b>{{ s.value }}</b><span>{{ s.label }}</span></div>
</div>
@@ -779,4 +833,216 @@ onBeforeUnmount(clearAll)
display: none;
}
}
/* ---- 模型卡 ---- */
.al-case {
cursor: pointer;
}
.al-case-more {
display: inline-block;
margin-top: 8px;
font-style: normal;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.1em;
color: #7c6cff;
opacity: 0;
transition: opacity 0.2s ease;
}
.al-case:hover .al-case-more {
opacity: 1;
}
.al-mask {
position: fixed;
inset: 0;
z-index: 520;
display: grid;
place-items: center;
padding: 20px;
background: rgba(4, 4, 10, 0.82);
backdrop-filter: blur(6px);
}
.al-modelcard {
position: relative;
width: min(620px, 94vw);
max-height: 88vh;
overflow-y: auto;
background: #0c0c18;
border: 1px solid rgba(124, 108, 255, 0.5);
border-radius: 16px;
color: #e8e5ff;
box-shadow: 0 0 50px rgba(124, 108, 255, 0.2), 0 30px 80px rgba(0, 0, 0, 0.7);
}
.al-mc-x {
position: absolute;
right: 14px;
top: 14px;
z-index: 3;
width: 34px;
height: 34px;
border: 1px solid rgba(124, 108, 255, 0.45);
border-radius: 50%;
background: rgba(12, 12, 24, 0.8);
color: #c9c2ff;
font-size: 14px;
cursor: pointer;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.al-mc-x:hover {
border-color: #7c6cff;
box-shadow: 0 0 16px rgba(124, 108, 255, 0.5);
}
.al-mc-head {
padding: 22px 26px 14px;
border-bottom: 1px solid rgba(124, 108, 255, 0.25);
}
.al-mc-path {
margin: 0 0 10px;
font-family: Consolas, monospace;
font-size: 14px;
color: #8f88bd;
}
.al-mc-path b {
color: #fff;
font-size: 17px;
}
.al-mc-badges {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.al-mc-badges span {
font-family: Consolas, monospace;
font-size: 10.5px;
letter-spacing: 0.06em;
padding: 3px 12px;
border-radius: 999px;
border: 1px solid rgba(124, 108, 255, 0.45);
color: #c9c2ff;
}
.al-mc-badges span.ok {
border-color: rgba(77, 255, 176, 0.5);
color: #4dffb0;
}
.al-mc-badges span.ctx {
border-color: rgba(0, 224, 255, 0.5);
color: #00e0ff;
}
.al-mc-img {
display: block;
width: calc(100% - 52px);
margin: 16px 26px 0;
aspect-ratio: 16 / 7;
object-fit: cover;
border-radius: 10px;
border: 1px solid rgba(124, 108, 255, 0.35);
}
.al-mc-block {
padding: 16px 26px 0;
}
.al-mc-block > b {
display: block;
margin-bottom: 8px;
font-family: Consolas, monospace;
font-size: 12px;
color: #7c6cff;
}
.al-mc-block p {
margin: 0;
font-size: 12.5px;
line-height: 1.95;
color: #b5aede;
}
.al-mc-block ul {
margin: 0;
padding: 0;
list-style: none;
}
.al-mc-block ul li {
position: relative;
padding: 4px 0 4px 20px;
font-size: 12.5px;
line-height: 1.8;
color: #d5d0f2;
}
.al-mc-block ul li::before {
content: '✦';
position: absolute;
left: 0;
color: #00e0ff;
font-size: 10px;
}
.al-mc-cols {
display: grid;
grid-template-columns: 1.25fr 1fr;
padding-bottom: 10px;
}
.al-mc-tl {
display: flex;
gap: 10px;
margin: 0 0 6px !important;
font-size: 12px !important;
}
.al-mc-tl i {
flex: 0 0 62px;
font-style: normal;
font-family: Consolas, monospace;
font-size: 10.5px;
color: #8f88bd;
font-variant-numeric: tabular-nums;
}
.al-mc-mx {
display: grid;
gap: 8px;
}
.al-mc-mx > div {
background: linear-gradient(160deg, rgba(124, 108, 255, 0.14), rgba(0, 224, 255, 0.05));
border: 1px solid rgba(124, 108, 255, 0.35);
border-radius: 10px;
padding: 8px 12px;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.al-mc-mx b {
font-size: 15.5px;
font-weight: 800;
color: #fff;
text-shadow: 0 0 16px rgba(124, 108, 255, 0.7);
}
.al-mc-mx span {
font-size: 10.5px;
color: #8f88bd;
}
.al-mc-foot {
padding: 14px 26px 18px;
border-top: 1px solid rgba(124, 108, 255, 0.25);
font-family: Consolas, monospace;
font-size: 10px;
letter-spacing: 0.1em;
color: #8f88bd;
text-align: center;
}
.al-card-enter-active,
.al-card-leave-active {
transition: opacity 0.26s ease;
}
.al-card-enter-active .al-modelcard,
.al-card-leave-active .al-modelcard {
transition: transform 0.32s cubic-bezier(0.25, 1.1, 0.4, 1);
}
.al-card-enter-from,
.al-card-leave-to {
opacity: 0;
}
.al-card-enter-from .al-modelcard,
.al-card-leave-to .al-modelcard {
transform: translateY(26px) scale(0.95);
}
@media (max-width: 560px) {
.al-mc-cols {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -45,6 +45,19 @@ function useSample(s) {
prompt.value = s
generate()
}
/* 画框详情 */
const frameColors = ['#ff6b9d', '#4d96ff', '#6bcb77', '#ffb020']
const frIdx = ref(-1)
const fr = ref(null)
function openFrame(i) {
frIdx.value = i
fr.value = c.cases[i]
}
function closeFrame() {
frIdx.value = -1
fr.value = null
}
</script>
<template>
@@ -110,6 +123,57 @@ function useSample(s) {
</div>
</section>
<!-- 深思全家桶画展 -->
<section class="ap-sec">
<h2 class="ap-h2">深思全家桶 · 挂进画框看</h2>
<div class="ap-frames">
<figure
v-for="(p, i) in c.cases"
:key="p.name"
class="ap-framed"
:style="{ '--fc': frameColors[i], '--r': ((i % 2 ? 1 : -1) * 1.4) + 'deg' }"
@click="openFrame(i)"
>
<span class="ap-framed-nail" aria-hidden="true"></span>
<div class="ap-framed-art"><img :src="p.image" :alt="p.name" loading="lazy" /></div>
<figcaption>{{ p.name }}<em>{{ p.tag }}</em></figcaption>
</figure>
</div>
</section>
<!-- 画框详情 -->
<Transition name="ap-hang">
<div v-if="fr" class="ap-mask" @click.self="closeFrame">
<article class="ap-easel" :style="{ '--fc': frameColors[frIdx] }">
<button class="ap-easel-x" aria-label="关闭" @click="closeFrame"></button>
<div class="ap-easel-frame">
<img :src="fr.image" :alt="fr.name" />
<span class="ap-easel-light" aria-hidden="true"></span>
</div>
<div class="ap-easel-plaque">
<h3>{{ fr.name }}</h3>
<p class="ap-easel-meta">{{ fr.tag }} · 深思智能 · prompt: AI 落进现实</p>
<p class="ap-easel-story">{{ fr.story }}</p>
<div class="ap-easel-block">
<b>🎨 亮点笔触</b>
<ul>
<li v-for="(h, i) in fr.highlights" :key="h" :style="{ '--lc': frameColors[i % 4] }">{{ h }}</li>
</ul>
</div>
<div class="ap-easel-block">
<b>🖌 创作过程</b>
<p v-for="t in fr.timeline" :key="t.at" class="ap-easel-tl"><i>{{ t.at }}</i>{{ t.event }}</p>
</div>
<div class="ap-easel-mx">
<div v-for="(m, i) in fr.metrics" :key="m.label" :style="{ '--fc': frameColors[i % 4] }">
<b>{{ m.value }}</b><span>{{ m.label }}</span>
</div>
</div>
</div>
</article>
</div>
</Transition>
<div class="ap-stats">
<div v-for="s in c.stats" :key="s.label"><b>{{ s.value }}</b><span>{{ s.label }}</span></div>
</div>
@@ -572,4 +636,252 @@ function useSample(s) {
font-size: 11px !important;
color: #6f6996 !important;
}
/* ---- 深思全家桶画展 ---- */
.ap-frames {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 22px;
}
.ap-framed {
position: relative;
margin: 0;
padding: 12px 12px 8px;
background: #fff;
border: 8px solid var(--fc, #ff6b9d);
border-radius: 4px;
box-shadow: 0 12px 26px rgba(30, 20, 60, 0.35);
transform: rotate(var(--r, 0deg));
cursor: pointer;
transition: transform 0.22s ease, box-shadow 0.22s ease;
}
.ap-framed:hover {
transform: rotate(0deg) translateY(-6px);
box-shadow: 0 20px 40px rgba(30, 20, 60, 0.45);
}
.ap-framed-nail {
position: absolute;
left: 50%;
top: -18px;
transform: translateX(-50%);
width: 10px;
height: 10px;
border-radius: 50%;
background: #d8d3e8;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
}
.ap-framed-art {
overflow: hidden;
border-radius: 2px;
}
.ap-framed-art img {
display: block;
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
transition: transform 0.3s ease;
}
.ap-framed:hover .ap-framed-art img {
transform: scale(1.05);
}
.ap-framed figcaption {
padding: 8px 2px 4px;
font-size: 12.5px;
font-weight: 700;
color: #3a3355;
}
.ap-framed figcaption em {
font-style: normal;
font-size: 10.5px;
font-weight: 400;
color: #8f88ad;
margin-left: 6px;
}
.ap-mask {
position: fixed;
inset: 0;
z-index: 520;
display: grid;
place-items: center;
padding: 20px;
background: rgba(24, 16, 44, 0.76);
backdrop-filter: blur(5px);
}
.ap-easel {
position: relative;
display: grid;
grid-template-columns: 1fr 1fr;
width: min(880px, 94vw);
max-height: 88vh;
background: #f7f5fc;
border-radius: 18px;
overflow: hidden;
box-shadow: 0 32px 84px rgba(10, 5, 30, 0.6);
}
.ap-easel-x {
position: absolute;
right: 14px;
top: 14px;
z-index: 3;
width: 34px;
height: 34px;
border: none;
border-radius: 50%;
background: rgba(58, 51, 85, 0.85);
color: #fff;
font-size: 14px;
cursor: pointer;
transition: transform 0.16s ease;
}
.ap-easel-x:hover {
transform: rotate(90deg);
}
.ap-easel-frame {
position: relative;
display: grid;
place-items: center;
background:
radial-gradient(ellipse 70% 50% at 50% 0%, rgba(255, 255, 255, 0.16), transparent 65%),
#241d3e;
padding: clamp(22px, 4vw, 44px);
}
.ap-easel-frame img {
width: 100%;
max-height: 66vh;
object-fit: cover;
border: 12px solid var(--fc, #ff6b9d);
border-radius: 4px;
outline: 4px solid #fff;
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.45);
}
.ap-easel-light {
position: absolute;
left: 50%;
top: 0;
width: 60%;
height: 100%;
transform: translateX(-50%);
background: linear-gradient(180deg, rgba(255, 244, 214, 0.16), transparent 62%);
pointer-events: none;
}
.ap-easel-plaque {
padding: clamp(22px, 3.4vw, 36px) clamp(20px, 3vw, 32px);
overflow-y: auto;
}
.ap-easel-plaque h3 {
margin: 0 0 4px;
font-size: clamp(21px, 2.8vw, 27px);
font-weight: 900;
color: #2c2547;
}
.ap-easel-meta {
margin: 0 0 12px;
font-size: 11.5px;
color: #8f88ad;
}
.ap-easel-story {
margin: 0 0 14px;
font-size: 13px;
line-height: 1.95;
color: #4c4470;
}
.ap-easel-block {
margin-bottom: 14px;
}
.ap-easel-block > b {
display: block;
margin-bottom: 6px;
font-size: 13px;
color: #2c2547;
}
.ap-easel-block ul {
margin: 0;
padding: 0;
list-style: none;
}
.ap-easel-block ul li {
position: relative;
padding: 4px 0 4px 20px;
font-size: 12.5px;
line-height: 1.8;
color: #4c4470;
}
.ap-easel-block ul li::before {
content: '';
position: absolute;
left: 2px;
top: 11px;
width: 9px;
height: 9px;
border-radius: 3px;
background: var(--lc, #ff6b9d);
transform: rotate(20deg);
}
.ap-easel-tl {
display: flex;
gap: 10px;
margin: 0 0 5px !important;
}
.ap-easel-tl i {
flex: 0 0 62px;
font-style: normal;
font-weight: 700;
color: #7c6cff;
font-variant-numeric: tabular-nums;
}
.ap-easel-mx {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.ap-easel-mx > div {
background: #fff;
border: 2.5px solid var(--fc, #ff6b9d);
border-radius: 12px;
padding: 9px 6px;
text-align: center;
}
.ap-easel-mx b {
display: block;
font-size: 16px;
font-weight: 900;
color: #2c2547;
}
.ap-easel-mx span {
font-size: 10.5px;
color: #8f88ad;
}
.ap-hang-enter-active,
.ap-hang-leave-active {
transition: opacity 0.26s ease;
}
.ap-hang-enter-active .ap-easel {
transition: transform 0.4s cubic-bezier(0.3, 1.3, 0.5, 1);
transform-origin: 50% -40px;
}
.ap-hang-leave-active .ap-easel {
transition: transform 0.22s ease;
}
.ap-hang-enter-from,
.ap-hang-leave-to {
opacity: 0;
}
.ap-hang-enter-from .ap-easel {
transform: rotate(-3deg) translateY(-22px);
}
.ap-hang-leave-to .ap-easel {
transform: scale(0.95);
}
@media (max-width: 760px) {
.ap-easel {
grid-template-columns: 1fr;
overflow-y: auto;
}
.ap-easel-frame img {
max-height: 34vh;
}
.ap-easel-plaque {
overflow-y: visible;
}
}
</style>

View File

@@ -1,7 +1,21 @@
<script setup>
import { ref } from 'vue'
import { corpProfiles } from '../../data/corpProfiles'
const c = corpProfiles.ai
/* 参数铭牌 */
const plateIdx = ref(-1)
const plate = ref(null)
function openPlate(i) {
plateIdx.value = i
plate.value = c.cases[i]
}
function closePlate() {
plateIdx.value = -1
plate.value = null
}
const specs = [
{ k: '重复定位精度', v: '±0.02mm' },
{ k: '最大负载', v: '35kg' },
@@ -173,7 +187,7 @@ const scenes = [
<section class="ar-sec">
<h2 class="ar-h2">// 与深思大模型协同</h2>
<div class="ar-cases">
<article v-for="(p, i) in c.cases" :key="p.name" class="ar-case" :style="{ '--d': i * 0.06 + 's' }">
<article v-for="(p, i) in c.cases" :key="p.name" class="ar-case" :style="{ '--d': i * 0.06 + 's' }" @click="openPlate(i)">
<div class="ar-case-head">
<span>COOP-FILE 0{{ i + 1 }} · {{ p.tag }}</span>
<i class="ar-case-led" aria-hidden="true"></i>
@@ -183,12 +197,58 @@ const scenes = [
<div>
<h3>{{ p.name }}</h3>
<p>{{ p.desc }}</p>
<i class="ar-case-more">READ NAMEPLATE </i>
</div>
</div>
</article>
</div>
</section>
<!-- 参数铭牌 -->
<Transition name="ar-bolt">
<div v-if="plate" class="ar-mask" @click.self="closePlate">
<article class="ar-plate">
<i class="ar-rivet tl" aria-hidden="true"></i>
<i class="ar-rivet tr" aria-hidden="true"></i>
<i class="ar-rivet bl" aria-hidden="true"></i>
<i class="ar-rivet br" aria-hidden="true"></i>
<button class="ar-plate-x" aria-label="关闭" @click="closePlate"></button>
<header class="ar-plate-head">
<p class="ar-plate-kicker">DEEPTHINK ROBOTICS · NAMEPLATE</p>
<h3>{{ plate.name }}</h3>
<span class="ar-plate-model">MODEL DT-{{ 100 + plateIdx * 11 }} · {{ plate.tag }}</span>
</header>
<div class="ar-plate-img">
<img :src="plate.image" :alt="plate.name" />
<span class="ar-plate-warn" aria-hidden="true"> 运行区域 · 请勿倚靠</span>
</div>
<section class="ar-plate-block">
<b>// DESIGN LOG 研发志</b>
<p>{{ plate.story }}</p>
</section>
<section class="ar-plate-block">
<b>// RATED SPECS 额定参数</b>
<ul>
<li v-for="h in plate.highlights" :key="h">{{ h }}</li>
</ul>
</section>
<div class="ar-plate-cols">
<section class="ar-plate-block">
<b>// SERVICE RECORD 服役记录</b>
<p v-for="t in plate.timeline" :key="t.at" class="ar-plate-tl"><i>{{ t.at }}</i>{{ t.event }}</p>
</section>
<section class="ar-plate-block">
<b>// GAUGES 表盘</b>
<div class="ar-plate-mx">
<div v-for="m in plate.metrics" :key="m.label"><b>{{ m.value }}</b><span>{{ m.label }}</span></div>
</div>
</section>
</div>
<footer class="ar-plate-foot">SN 2026-{{ String(plateIdx + 1).padStart(4, '0') }} · MADE BY DEEPTHINK · 铭牌为风格演示</footer>
</article>
</div>
</Transition>
<div class="ar-stats">
<div v-for="s in c.stats" :key="s.label"><b>{{ s.value }}</b><span>{{ s.label }}</span></div>
</div>
@@ -718,4 +778,248 @@ const scenes = [
grid-template-columns: 96px 1fr;
}
}
/* ---- 参数铭牌 ---- */
.ar-case {
cursor: pointer;
}
.ar-case-more {
display: inline-block;
margin-top: 8px;
font-style: normal;
font-family: Consolas, monospace;
font-size: 10px;
letter-spacing: 0.16em;
color: #ffb020;
opacity: 0;
transition: opacity 0.2s ease;
}
.ar-case:hover .ar-case-more {
opacity: 1;
}
.ar-mask {
position: fixed;
inset: 0;
z-index: 520;
display: grid;
place-items: center;
padding: 20px;
background: rgba(10, 11, 13, 0.82);
backdrop-filter: blur(4px);
}
.ar-plate {
position: relative;
width: min(600px, 94vw);
max-height: 88vh;
overflow-y: auto;
background:
repeating-linear-gradient(45deg, transparent 0 22px, rgba(255, 176, 32, 0.04) 22px 24px),
linear-gradient(170deg, #26282e, #1b1d21);
border: 2px solid #3a3d44;
border-radius: 10px;
color: #e8e9eb;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 30px 76px rgba(0, 0, 0, 0.7);
}
.ar-rivet {
position: absolute;
width: 14px;
height: 14px;
border-radius: 50%;
background: radial-gradient(circle at 34% 30%, #6a6e77, #2c2e33 70%);
box-shadow: inset 0 -1.5px 3px rgba(0, 0, 0, 0.6), 0 1px 1px rgba(255, 255, 255, 0.1);
z-index: 2;
}
.ar-rivet.tl { left: 10px; top: 10px; }
.ar-rivet.tr { right: 10px; top: 10px; }
.ar-rivet.bl { left: 10px; bottom: 10px; }
.ar-rivet.br { right: 10px; bottom: 10px; }
.ar-plate-x {
position: absolute;
right: 30px;
top: 16px;
z-index: 3;
width: 32px;
height: 32px;
border: 1.5px solid #4a4d55;
border-radius: 6px;
background: #26282e;
color: #ffb020;
font-size: 13px;
cursor: pointer;
transition: border-color 0.18s ease;
}
.ar-plate-x:hover {
border-color: #ffb020;
}
.ar-plate-head {
padding: 24px 32px 12px;
border-bottom: 2px solid #3a3d44;
}
.ar-plate-kicker {
margin: 0 0 8px;
font-family: Consolas, monospace;
font-size: 10px;
letter-spacing: 0.26em;
color: #8d9099;
}
.ar-plate-head h3 {
margin: 0 0 6px;
font-size: clamp(21px, 2.8vw, 26px);
font-weight: 900;
letter-spacing: 0.04em;
}
.ar-plate-model {
display: inline-block;
font-family: Consolas, monospace;
font-size: 11px;
letter-spacing: 0.14em;
color: #ffb020;
border: 1px solid rgba(255, 176, 32, 0.45);
padding: 3px 12px;
}
.ar-plate-img {
position: relative;
margin: 16px 32px 0;
border: 2px solid #3a3d44;
overflow: hidden;
}
.ar-plate-img img {
display: block;
width: 100%;
aspect-ratio: 16 / 7;
object-fit: cover;
filter: saturate(0.8) contrast(1.05);
}
.ar-plate-warn {
position: absolute;
left: 0;
bottom: 0;
right: 0;
background: repeating-linear-gradient(-45deg, rgba(255, 176, 32, 0.9) 0 14px, #1b1d21 14px 28px);
color: #1b1d21;
font-size: 10px;
font-weight: 900;
letter-spacing: 0.14em;
text-align: center;
padding: 3px 0;
text-shadow: 0 0 4px rgba(255, 232, 178, 0.9);
}
.ar-plate-block {
padding: 16px 32px 0;
}
.ar-plate-block > b {
display: block;
margin-bottom: 8px;
font-family: Consolas, monospace;
font-size: 10.5px;
letter-spacing: 0.16em;
color: #ffb020;
}
.ar-plate-block p {
margin: 0;
font-size: 12.5px;
line-height: 1.95;
color: #b8bac0;
}
.ar-plate-block ul {
margin: 0;
padding: 0;
list-style: none;
border: 1px solid #3a3d44;
}
.ar-plate-block ul li {
position: relative;
padding: 8px 12px 8px 30px;
font-size: 12.5px;
color: #d5d7db;
}
.ar-plate-block ul li + li {
border-top: 1px solid #2c2e33;
}
.ar-plate-block ul li::before {
content: '▣';
position: absolute;
left: 12px;
color: #ffb020;
font-size: 10px;
}
.ar-plate-cols {
display: grid;
grid-template-columns: 1.25fr 1fr;
gap: 4px;
padding-bottom: 12px;
}
.ar-plate-tl {
display: flex;
gap: 10px;
margin: 0 0 7px !important;
font-size: 12px !important;
}
.ar-plate-tl i {
flex: 0 0 62px;
font-style: normal;
font-family: Consolas, monospace;
font-size: 10.5px;
color: #8d9099;
font-variant-numeric: tabular-nums;
}
.ar-plate-mx {
display: grid;
gap: 8px;
}
.ar-plate-mx > div {
background: #1b1d21;
border: 1px solid #3a3d44;
border-left: 3px solid #ffb020;
padding: 8px 12px;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.ar-plate-mx b {
font-family: Consolas, monospace;
font-size: 15px;
font-weight: 700;
color: #ffb020;
text-shadow: 0 0 12px rgba(255, 176, 32, 0.4);
}
.ar-plate-mx span {
font-size: 10.5px;
color: #8d9099;
}
.ar-plate-foot {
padding: 12px 32px 20px;
border-top: 2px solid #3a3d44;
font-family: Consolas, monospace;
font-size: 9.5px;
letter-spacing: 0.18em;
color: #8d9099;
text-align: center;
}
.ar-bolt-enter-active,
.ar-bolt-leave-active {
transition: opacity 0.24s ease;
}
.ar-bolt-enter-active .ar-plate {
transition: transform 0.3s cubic-bezier(0.2, 1.2, 0.4, 1);
}
.ar-bolt-leave-active .ar-plate {
transition: transform 0.2s ease;
}
.ar-bolt-enter-from,
.ar-bolt-leave-to {
opacity: 0;
}
.ar-bolt-enter-from .ar-plate {
transform: translateY(-24px) scale(1.03);
}
.ar-bolt-leave-to .ar-plate {
transform: scale(0.97);
}
@media (max-width: 560px) {
.ar-plate-cols {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -1,7 +1,21 @@
<script setup>
import { ref } from 'vue'
import { corpProfiles } from '../../data/corpProfiles'
const c = corpProfiles.ai
/* 识别报告 */
const rptIdx = ref(-1)
const rpt = ref(null)
function openRpt(i) {
rptIdx.value = i
rpt.value = c.cases[i]
}
function closeRpt() {
rptIdx.value = -1
rpt.value = null
}
const detections = [
{ label: '安全帽 ✓', conf: '99.2%', x: '12%', y: '18%', w: '30%', h: '38%' },
{ label: '人员越界 ⚠', conf: '97.6%', x: '56%', y: '30%', w: '32%', h: '46%' },
@@ -116,6 +130,69 @@ const events = [
</div>
</section>
<!-- 识别档案库 -->
<section class="av2-sec">
<h2 class="av2-h2">// 识别档案库 · 深思产品矩阵</h2>
<div class="av2-files">
<button v-for="(p, i) in c.cases" :key="p.name" class="av2-file" :style="{ '--d': i * 0.06 + 's' }" @click="openRpt(i)">
<div class="av2-file-cam">
<img :src="p.image" :alt="p.name" loading="lazy" />
<span class="av2-file-id">TARGET-0{{ i + 1 }}</span>
<i class="av2-file-focus" aria-hidden="true"></i>
</div>
<b>{{ p.name }}</b>
<span>{{ p.tag }} · 点击生成识别报告</span>
</button>
</div>
</section>
<!-- 识别报告 -->
<Transition name="av2-boot">
<div v-if="rpt" class="av2-mask" @click.self="closeRpt">
<article class="av2-report">
<div class="av2-report-bar">
<span><i class="av2-report-dot" aria-hidden="true"></i>RECOGNITION REPORT · TARGET-0{{ rptIdx + 1 }}</span>
<button class="av2-report-x" aria-label="关闭" @click="closeRpt"></button>
</div>
<div class="av2-report-cam">
<img :src="rpt.image" :alt="rpt.name" />
<span class="av2-scan" aria-hidden="true"></span>
<div class="av2-box av2-report-box">
<b>{{ rpt.name }} </b>
<span>99.{{ 4 + rptIdx }}%</span>
</div>
</div>
<header class="av2-report-head">
<h3>{{ rpt.name }}</h3>
<span>{{ rpt.tag }} · CLASS: PRODUCT · STATUS: <em>VERIFIED</em></span>
</header>
<section class="av2-report-block">
<b>// ANALYSIS 分析摘要</b>
<p>{{ rpt.story }}</p>
</section>
<section class="av2-report-block">
<b>// FEATURES 检出特征</b>
<ul>
<li v-for="(h, i) in rpt.highlights" :key="h"><em>F{{ i + 1 }}</em>{{ h }}</li>
</ul>
</section>
<div class="av2-report-cols">
<section class="av2-report-block">
<b>// EVENT LOG 事件日志</b>
<p v-for="t in rpt.timeline" :key="t.at" class="av2-report-tl"><i>{{ t.at }}</i>{{ t.event }}</p>
</section>
<section class="av2-report-block">
<b>// CONFIDENCE 置信数据</b>
<div class="av2-report-mx">
<div v-for="m in rpt.metrics" :key="m.label"><b>{{ m.value }}</b><span>{{ m.label }}</span></div>
</div>
</section>
</div>
<footer class="av2-report-foot">REPORT SIGNED BY DEEPTHINK VISION · 数据为风格演示</footer>
</article>
</div>
</Transition>
<div class="av2-stats">
<div v-for="s in c.stats" :key="s.label"><b>{{ s.value }}</b><span>{{ s.label }}</span></div>
</div>
@@ -699,4 +776,304 @@ const events = [
.av2-feat-txt { grid-area: txt; }
.av2-feat-status { grid-area: st; justify-self: end; }
}
/* ---- 识别档案库 · 识别报告 ---- */
.av2-files {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 14px;
}
.av2-file {
padding: 0 0 12px;
background: #071a11;
border: 1px solid rgba(0, 255, 170, 0.3);
border-radius: 10px;
overflow: hidden;
cursor: pointer;
text-align: left;
font-family: inherit;
color: inherit;
animation: av2-file-in 0.5s ease both;
animation-delay: var(--d, 0s);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
@keyframes av2-file-in {
from {
opacity: 0;
transform: translateY(12px);
}
}
.av2-file:hover {
transform: translateY(-4px);
border-color: #00ffaa;
box-shadow: 0 0 26px rgba(0, 255, 170, 0.18);
}
.av2-file-cam {
position: relative;
overflow: hidden;
}
.av2-file-cam img {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
filter: saturate(0.75) hue-rotate(8deg);
}
.av2-file-id {
position: absolute;
left: 8px;
top: 8px;
font-family: Consolas, monospace;
font-size: 9px;
letter-spacing: 0.16em;
color: #00ffaa;
background: rgba(4, 18, 12, 0.85);
padding: 3px 8px;
}
.av2-file-focus {
position: absolute;
inset: 12px;
border: 1px dashed rgba(0, 255, 170, 0.5);
opacity: 0;
transition: opacity 0.2s ease;
pointer-events: none;
}
.av2-file:hover .av2-file-focus {
opacity: 1;
animation: av2-focus 1.2s ease-in-out infinite;
}
@keyframes av2-focus {
50% { inset: 8px; }
}
.av2-file b {
display: block;
padding: 10px 14px 2px;
font-size: 14px;
color: #d9ffef;
}
.av2-file span {
display: block;
padding: 0 14px;
font-family: Consolas, monospace;
font-size: 10px;
color: #4e8f74;
letter-spacing: 0.05em;
}
.av2-mask {
position: fixed;
inset: 0;
z-index: 520;
display: grid;
place-items: center;
padding: 20px;
background: rgba(1, 8, 5, 0.86);
backdrop-filter: blur(4px);
}
.av2-report {
position: relative;
width: min(620px, 94vw);
max-height: 88vh;
display: flex;
flex-direction: column;
background: #04120c;
border: 1px solid rgba(0, 255, 170, 0.45);
color: #d9ffef;
box-shadow: 0 0 46px rgba(0, 255, 170, 0.14), 0 30px 80px rgba(0, 0, 0, 0.7);
overflow: hidden;
}
.av2-report-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 16px;
background: rgba(0, 255, 170, 0.07);
border-bottom: 1px solid rgba(0, 255, 170, 0.35);
font-family: Consolas, monospace;
font-size: 11px;
letter-spacing: 0.16em;
color: #00ffaa;
}
.av2-report-bar > span {
display: inline-flex;
align-items: center;
gap: 8px;
}
.av2-report-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ff5f56;
animation: av2-rec-dot 1.2s ease-in-out infinite;
}
@keyframes av2-rec-dot {
50% { opacity: 0.25; }
}
.av2-report-x {
width: 28px;
height: 28px;
border: 1px solid rgba(0, 255, 170, 0.4);
background: transparent;
color: #00ffaa;
font-size: 12px;
cursor: pointer;
}
.av2-report-x:hover {
background: rgba(0, 255, 170, 0.15);
}
.av2-report-cam {
position: relative;
flex-shrink: 0;
overflow: hidden;
}
.av2-report-cam img {
display: block;
width: 100%;
aspect-ratio: 16 / 6.5;
object-fit: cover;
filter: saturate(0.75) hue-rotate(8deg);
}
.av2-report-box {
left: 10%;
top: 16%;
width: 34%;
height: 58%;
position: absolute;
}
.av2-report-head {
padding: 14px 24px 0;
}
.av2-report-head h3 {
margin: 0 0 4px;
font-size: clamp(19px, 2.6vw, 24px);
font-weight: 800;
color: #eafff6;
text-shadow: 0 0 18px rgba(0, 255, 170, 0.4);
}
.av2-report-head span {
font-family: Consolas, monospace;
font-size: 10.5px;
letter-spacing: 0.12em;
color: #4e8f74;
}
.av2-report-head em {
font-style: normal;
color: #00ffaa;
}
.av2-report {
overflow-y: auto;
}
.av2-report-block {
padding: 14px 24px 0;
}
.av2-report-block > b {
display: block;
margin-bottom: 7px;
font-family: Consolas, monospace;
font-size: 10.5px;
letter-spacing: 0.16em;
color: #00ffaa;
}
.av2-report-block p {
margin: 0;
font-size: 12.5px;
line-height: 1.95;
color: #a8d4c0;
}
.av2-report-block ul {
margin: 0;
padding: 0;
list-style: none;
}
.av2-report-block ul li {
display: flex;
gap: 10px;
padding: 4px 0;
font-size: 12.5px;
line-height: 1.8;
color: #c6ecd9;
}
.av2-report-block ul li em {
flex: 0 0 26px;
font-style: normal;
font-family: Consolas, monospace;
font-size: 10.5px;
color: #4e8f74;
}
.av2-report-cols {
display: grid;
grid-template-columns: 1.25fr 1fr;
padding-bottom: 12px;
}
.av2-report-tl {
display: flex;
gap: 10px;
margin: 0 0 6px !important;
font-size: 12px !important;
}
.av2-report-tl i {
flex: 0 0 60px;
font-style: normal;
font-family: Consolas, monospace;
font-size: 10.5px;
color: #4e8f74;
font-variant-numeric: tabular-nums;
}
.av2-report-mx {
display: grid;
gap: 8px;
}
.av2-report-mx > div {
border: 1px solid rgba(0, 255, 170, 0.3);
background: rgba(0, 255, 170, 0.05);
padding: 8px 12px;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.av2-report-mx b {
font-family: Consolas, monospace;
font-size: 15px;
color: #00ffaa;
text-shadow: 0 0 12px rgba(0, 255, 170, 0.5);
}
.av2-report-mx span {
font-size: 10.5px;
color: #4e8f74;
}
.av2-report-foot {
padding: 12px 24px 18px;
border-top: 1px solid rgba(0, 255, 170, 0.25);
font-family: Consolas, monospace;
font-size: 9.5px;
letter-spacing: 0.16em;
color: #4e8f74;
text-align: center;
}
.av2-boot-enter-active,
.av2-boot-leave-active {
transition: opacity 0.22s ease;
}
.av2-boot-enter-active .av2-report {
transition: transform 0.3s cubic-bezier(0.25, 1.1, 0.4, 1);
}
.av2-boot-leave-active .av2-report {
transition: transform 0.2s ease;
}
.av2-boot-enter-from,
.av2-boot-leave-to {
opacity: 0;
}
.av2-boot-enter-from .av2-report {
transform: scaleY(0.5);
}
.av2-boot-leave-to .av2-report {
transform: scale(0.97);
}
@media (max-width: 560px) {
.av2-report-cols {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -35,6 +35,18 @@ const skills = [
{ icon: '🎵', name: '氛围点唱', desc: '按心情选歌,支持哼唱识曲' },
{ icon: '👵', name: '长辈模式', desc: '方言识别 + 大音量慢速播报' },
]
/* 声纹卡 */
const vpIdx = ref(-1)
const vp = ref(null)
function openVp(i) {
vpIdx.value = i
vp.value = c.cases[i]
}
function closeVp() {
vpIdx.value = -1
vp.value = null
}
</script>
<template>
@@ -106,6 +118,57 @@ const skills = [
</div>
</section>
<!-- 深思全家桶 · 语音介绍 -->
<section class="av-sec">
<h2 class="av-h2">问它:深思家还有什么?</h2>
<div class="av-vps">
<button v-for="(p, i) in c.cases" :key="p.name" class="av-vp" :style="{ '--d': i * 0.07 + 's' }" @click="openVp(i)">
<img :src="p.image" :alt="p.name" loading="lazy" />
<span class="av-vp-txt">
<b>{{ p.name }}</b>
<em>{{ p.tag }} · 点击听语音介绍</em>
</span>
<span class="av-eq av-vp-eq" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></span>
</button>
</div>
</section>
<!-- 声纹卡 -->
<Transition name="av-pop">
<div v-if="vp" class="av-mask" @click.self="closeVp">
<article class="av-voicecard">
<button class="av-vc-x" aria-label="关闭" @click="closeVp"></button>
<header class="av-vc-head">
<span class="av-vc-orb" aria-hidden="true"><i></i><i></i><i></i></span>
<h3>{{ vp.name }}</h3>
<p>声纹卡 VOICEPRINT · {{ vp.tag }} · 0:{{ 32 + vpIdx * 9 }}</p>
</header>
<div class="av-vc-wave" aria-hidden="true">
<i v-for="n in 36" :key="n" :style="{ '--n': n }"></i>
</div>
<img class="av-vc-img" :src="vp.image" :alt="vp.name" />
<section class="av-vc-block">
<b>📝 语音转写</b>
<p>{{ vp.story }}</p>
</section>
<section class="av-vc-block">
<b> 重点摘录</b>
<ul>
<li v-for="h in vp.highlights" :key="h">{{ h }}</li>
</ul>
</section>
<section class="av-vc-block">
<b>🕐 时间轴书签</b>
<p v-for="(t, i) in vp.timeline" :key="t.at" class="av-vc-tl"><i>0:{{ String(8 + i * 11).padStart(2, '0') }}</i><em>{{ t.at }}</em>{{ t.event }}</p>
</section>
<div class="av-vc-mx">
<div v-for="m in vp.metrics" :key="m.label"><b>{{ m.value }}</b><span>{{ m.label }}</span></div>
</div>
<footer class="av-vc-foot">本条语音由深思语音合成(演示) · 声纹已脱敏</footer>
</article>
</div>
</Transition>
<div class="av-stats">
<div v-for="s in c.stats" :key="s.label"><b>{{ s.value }}</b><span>{{ s.label }}</span></div>
</div>
@@ -603,4 +666,276 @@ const skills = [
font-size: 11px !important;
color: #9bbdb7 !important;
}
/* ---- 深思全家桶 · 声纹卡 ---- */
.av-vps {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 12px;
}
.av-vp {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #fff;
border: 1.5px solid #d5eeea;
border-radius: 999px;
cursor: pointer;
text-align: left;
font-family: inherit;
animation: av-vp-in 0.5s ease both;
animation-delay: var(--d, 0s);
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
@keyframes av-vp-in {
from {
opacity: 0;
transform: translateY(12px);
}
}
.av-vp:hover {
transform: translateY(-3px);
border-color: #2ec4b6;
box-shadow: 0 12px 26px rgba(46, 196, 182, 0.16);
}
.av-vp img {
width: 46px;
height: 46px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #d5eeea;
flex-shrink: 0;
}
.av-vp-txt {
flex: 1;
min-width: 0;
}
.av-vp-txt b {
display: block;
font-size: 13.5px;
color: #1d4a44;
}
.av-vp-txt em {
font-style: normal;
font-size: 10.5px;
color: #7ba8a1;
}
.av-vp-eq {
flex-shrink: 0;
}
.av-mask {
position: fixed;
inset: 0;
z-index: 520;
display: grid;
place-items: center;
padding: 20px;
background: rgba(16, 66, 60, 0.5);
backdrop-filter: blur(5px);
}
.av-voicecard {
position: relative;
width: min(460px, 94vw);
max-height: 88vh;
overflow-y: auto;
background: #f7fbfa;
border-radius: 24px;
color: #1d4a44;
box-shadow: 0 28px 66px rgba(16, 66, 60, 0.4);
}
.av-vc-x {
position: absolute;
right: 12px;
top: 12px;
z-index: 3;
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
color: #fff;
font-size: 13px;
cursor: pointer;
}
.av-vc-x:hover {
background: rgba(255, 255, 255, 0.48);
}
.av-vc-head {
padding: 22px 24px 12px;
background: linear-gradient(160deg, #2ec4b6, #1d9f92);
color: #fff;
text-align: center;
}
.av-vc-orb {
position: relative;
display: inline-block;
width: 52px;
height: 52px;
margin-bottom: 8px;
}
.av-vc-orb i {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.7);
animation: av-vc-pulse 2s ease-out infinite;
}
.av-vc-orb i:nth-child(2) { animation-delay: 0.55s; }
.av-vc-orb i:nth-child(3) { animation-delay: 1.1s; }
@keyframes av-vc-pulse {
from {
transform: scale(0.45);
opacity: 1;
}
to {
transform: scale(1.2);
opacity: 0;
}
}
.av-vc-head h3 {
margin: 0 0 4px;
font-size: 21px;
font-weight: 800;
}
.av-vc-head p {
margin: 0;
font-size: 10.5px;
letter-spacing: 0.14em;
opacity: 0.85;
}
.av-vc-wave {
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
height: 54px;
padding: 0 24px;
background: linear-gradient(160deg, #1d9f92, #17877c);
}
.av-vc-wave i {
width: 4px;
border-radius: 2px;
background: rgba(255, 255, 255, 0.85);
height: calc(8px + (var(--n) * 7 % 30) * 1px);
animation: av-vc-bar 1s ease-in-out infinite;
animation-delay: calc(var(--n) * -0.08s);
}
@keyframes av-vc-bar {
50% { transform: scaleY(0.35); }
}
.av-vc-img {
display: block;
width: calc(100% - 48px);
margin: 16px 24px 0;
aspect-ratio: 16 / 7;
object-fit: cover;
border-radius: 14px;
border: 2px solid #d5eeea;
}
.av-vc-block {
padding: 14px 24px 0;
}
.av-vc-block > b {
display: block;
margin-bottom: 6px;
font-size: 12.5px;
color: #17877c;
letter-spacing: 0.06em;
}
.av-vc-block p {
margin: 0;
font-size: 12.5px;
line-height: 1.95;
color: #40685f;
}
.av-vc-block ul {
margin: 0;
padding: 0;
list-style: none;
}
.av-vc-block ul li {
position: relative;
padding: 4px 0 4px 20px;
font-size: 12.5px;
line-height: 1.8;
color: #40685f;
}
.av-vc-block ul li::before {
content: '◉';
position: absolute;
left: 0;
color: #2ec4b6;
font-size: 10px;
}
.av-vc-tl {
display: flex !important;
gap: 8px;
margin: 0 0 5px !important;
align-items: baseline;
}
.av-vc-tl i {
flex: 0 0 44px;
font-style: normal;
font-size: 10.5px;
font-weight: 700;
color: #2ec4b6;
font-variant-numeric: tabular-nums;
}
.av-vc-tl em {
flex: 0 0 62px;
font-style: normal;
font-size: 10.5px;
color: #7ba8a1;
font-variant-numeric: tabular-nums;
}
.av-vc-mx {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
padding: 14px 24px 0;
}
.av-vc-mx > div {
background: #fff;
border: 2px solid #d5eeea;
border-radius: 14px;
padding: 9px 6px;
text-align: center;
}
.av-vc-mx b {
display: block;
font-size: 16px;
font-weight: 800;
color: #17877c;
}
.av-vc-mx span {
font-size: 10.5px;
color: #7ba8a1;
}
.av-vc-foot {
padding: 14px 24px 18px;
text-align: center;
font-size: 10.5px;
color: #9bbdb7;
}
.av-pop-enter-active,
.av-pop-leave-active {
transition: opacity 0.24s ease;
}
.av-pop-enter-active .av-voicecard {
transition: transform 0.36s cubic-bezier(0.3, 1.3, 0.5, 1);
}
.av-pop-leave-active .av-voicecard {
transition: transform 0.2s ease;
}
.av-pop-enter-from,
.av-pop-leave-to {
opacity: 0;
}
.av-pop-enter-from .av-voicecard {
transform: translateY(34px) scale(0.94);
}
.av-pop-leave-to .av-voicecard {
transform: scale(0.96);
}
</style>

View File

@@ -18,6 +18,18 @@ const fmt = (n) => '¥ ' + n.toLocaleString('zh-CN')
const estOf = (i) => fmt(Math.round(basePrices[i] * 1.2 / 1000) * 1000) + ' ' + fmt(Math.round(basePrices[i] * 1.8 / 1000) * 1000)
const overHigh = (i) => bids.value[i].price >= basePrices[i] * 1.8
/* 拍卖图录页 */
const catIdx = ref(-1)
const cat = ref(null)
function openCat(i) {
catIdx.value = i
cat.value = c.cases[i]
}
function closeCat() {
catIdx.value = -1
cat.value = null
}
/* 开拍倒计时(演示:距今 3 天后 19:30 */
const target = (() => {
const d = new Date()
@@ -84,9 +96,10 @@ onBeforeUnmount(() => clearInterval(timer))
<h2 class="au-h2"><span>本场拍品 · LOTS</span></h2>
<div class="au-lots">
<article v-for="(w, i) in c.cases" :key="w.name" class="au-lot">
<div class="au-lot-img">
<div class="au-lot-img" role="button" tabindex="0" @click="openCat(i)" @keydown.enter="openCat(i)">
<span class="au-lot-no">LOT {{ String(i + 1).padStart(3, '0') }}</span>
<img :src="w.image" :alt="w.name" loading="lazy" />
<span class="au-lot-open">翻开图录 </span>
</div>
<div class="au-lot-info">
<h3>{{ w.name }}</h3>
@@ -103,9 +116,45 @@ onBeforeUnmount(() => clearInterval(timer))
</div>
</article>
</div>
<p class="au-note">* 演示竞价举牌不花钱请放心过瘾</p>
<p class="au-note">* 演示竞价举牌不花钱请放心过瘾 · 点击拍品大图可翻阅图录页</p>
</section>
<!-- 拍卖图录页 -->
<Transition name="au-page">
<div v-if="cat" class="au-mask" @click.self="closeCat">
<article class="au-cat">
<button class="au-cat-x" aria-label="关闭" @click="closeCat"></button>
<div class="au-cat-plate">
<img :src="cat.image" :alt="cat.name" />
<span>PLATE {{ catIdx + 1 }}</span>
</div>
<div class="au-cat-page">
<p class="au-cat-kicker">LUMINA AUCTION · CATALOGUE</p>
<p class="au-cat-lotno">LOT {{ String(catIdx + 1).padStart(3, '0') }}</p>
<h3>{{ cat.name }}</h3>
<p class="au-cat-meta">{{ cat.tag }} · {{ cat.year }} · 附馆藏证书与出版著录</p>
<p class="au-cat-est">估价 {{ estOf(catIdx) }} <em :class="{ flash: bids[catIdx].flash }">现价 {{ fmt(bids[catIdx].price) }}</em></p>
<div class="au-cat-rule" aria-hidden="true"><i></i></div>
<p class="au-cat-story">{{ cat.story }}</p>
<div class="au-cat-block">
<b>图录要点 · NOTES</b>
<ul>
<li v-for="h in cat.highlights" :key="h">{{ h }}</li>
</ul>
</div>
<div class="au-cat-block">
<b>来源与著录 · PROVENANCE</b>
<p v-for="t in cat.timeline" :key="t.at" class="au-cat-tl"><i>{{ t.at }}</i>{{ t.event }}</p>
</div>
<div class="au-cat-mx">
<div v-for="m in cat.metrics" :key="m.label"><b>{{ m.value }}</b><span>{{ m.label }}</span></div>
</div>
<button class="au-cat-bid" @click="bid(catIdx)"><i class="au-paddle" aria-hidden="true">88</i>在图录页举牌 · {{ bids[catIdx].count }} 次出价</button>
</div>
</article>
</div>
</Transition>
<!-- 关于 -->
<section id="house" class="au-sec">
<h2 class="au-h2"><span>关于拍场 · THE HOUSE</span></h2>
@@ -707,4 +756,279 @@ onBeforeUnmount(() => clearInterval(timer))
grid-column: 2;
}
}
/* ---- 拍卖图录页 ---- */
.au-lot-img {
cursor: pointer;
}
.au-lot-open {
position: absolute;
left: 50%;
bottom: 14px;
transform: translateX(-50%) translateY(6px);
z-index: 2;
background: rgba(23, 19, 14, 0.92);
border: 1px solid rgba(212, 175, 55, 0.65);
color: #d4af37;
font-size: 11px;
letter-spacing: 0.18em;
padding: 6px 16px;
opacity: 0;
transition: opacity 0.22s ease, transform 0.22s ease;
pointer-events: none;
white-space: nowrap;
}
.au-lot-img:hover .au-lot-open {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.au-mask {
position: fixed;
inset: 0;
z-index: 520;
display: grid;
place-items: center;
padding: 22px;
background: rgba(10, 8, 5, 0.8);
backdrop-filter: blur(5px);
}
.au-cat {
position: relative;
display: grid;
grid-template-columns: 0.95fr 1.05fr;
width: min(900px, 94vw);
max-height: 88vh;
background: #f4eee0;
color: #2b2317;
box-shadow: 0 34px 90px rgba(0, 0, 0, 0.65);
}
.au-cat::before {
content: '';
position: absolute;
inset: 10px;
border: 1px solid rgba(139, 115, 50, 0.4);
pointer-events: none;
}
.au-cat-x {
position: absolute;
right: 16px;
top: 16px;
z-index: 3;
width: 34px;
height: 34px;
border: 1px solid rgba(139, 115, 50, 0.5);
border-radius: 50%;
background: #f4eee0;
color: #6b5a33;
font-size: 14px;
cursor: pointer;
transition: background 0.18s ease, color 0.18s ease;
}
.au-cat-x:hover {
background: #2b2317;
color: #d4af37;
}
.au-cat-plate {
position: relative;
background: #17130e;
display: grid;
place-items: center;
padding: clamp(18px, 3vw, 36px);
}
.au-cat-plate img {
width: 100%;
max-height: 72vh;
object-fit: cover;
border: 8px solid #2b2317;
outline: 1px solid rgba(212, 175, 55, 0.5);
box-shadow: 0 16px 44px rgba(0, 0, 0, 0.55);
}
.au-cat-plate span {
position: absolute;
left: 50%;
bottom: 10px;
transform: translateX(-50%);
font-size: 9.5px;
letter-spacing: 0.3em;
color: rgba(212, 175, 55, 0.75);
}
.au-cat-page {
padding: clamp(24px, 3.5vw, 42px) clamp(20px, 3vw, 38px);
overflow-y: auto;
}
.au-cat-kicker {
margin: 0 0 4px;
font-size: 9.5px;
letter-spacing: 0.34em;
color: #8b7332;
}
.au-cat-lotno {
margin: 0 0 8px;
font-family: 'Playfair Display', 'Noto Serif SC', serif;
font-size: 13px;
letter-spacing: 0.22em;
color: #6b5a33;
}
.au-cat-page h3 {
margin: 0 0 4px;
font-family: 'Playfair Display', 'Noto Serif SC', serif;
font-size: clamp(22px, 2.8vw, 30px);
color: #17130e;
}
.au-cat-meta {
margin: 0 0 8px;
font-size: 11.5px;
color: #6b5a33;
}
.au-cat-est {
margin: 0 0 6px;
font-size: 13px;
font-weight: 700;
color: #2b2317;
}
.au-cat-est em {
margin-left: 10px;
font-style: normal;
font-size: 12px;
color: #8b3122;
font-variant-numeric: tabular-nums;
}
.au-cat-est em.flash {
animation: au-cat-flash 0.6s ease;
}
@keyframes au-cat-flash {
30% {
background: rgba(212, 175, 55, 0.45);
}
}
.au-cat-rule {
display: flex;
align-items: center;
gap: 12px;
margin: 12px 0 14px;
color: #8b7332;
}
.au-cat-rule::before,
.au-cat-rule::after {
content: '';
flex: 1;
border-top: 1px solid rgba(139, 115, 50, 0.45);
}
.au-cat-story {
margin: 0 0 16px;
font-size: 13px;
line-height: 2.05;
color: #453a26;
}
.au-cat-block {
margin-bottom: 15px;
}
.au-cat-block > b {
display: block;
margin-bottom: 7px;
font-size: 10.5px;
letter-spacing: 0.26em;
color: #8b7332;
}
.au-cat-block ul {
margin: 0;
padding: 0;
list-style: none;
}
.au-cat-block ul li {
position: relative;
padding: 4px 0 4px 20px;
font-size: 12.5px;
line-height: 1.8;
color: #2b2317;
}
.au-cat-block ul li::before {
content: '❧';
position: absolute;
left: 0;
color: #8b7332;
font-size: 11px;
}
.au-cat-tl {
display: flex;
gap: 12px;
margin: 0 0 5px;
font-size: 12px;
color: #453a26;
}
.au-cat-tl i {
flex: 0 0 62px;
font-style: normal;
color: #8b7332;
font-variant-numeric: tabular-nums;
}
.au-cat-mx {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: rgba(139, 115, 50, 0.45);
border: 1px solid rgba(139, 115, 50, 0.45);
margin-bottom: 16px;
}
.au-cat-mx > div {
background: #f4eee0;
padding: 10px 6px;
text-align: center;
}
.au-cat-mx b {
display: block;
font-family: 'Playfair Display', 'Noto Serif SC', serif;
font-size: 18px;
color: #17130e;
}
.au-cat-mx span {
font-size: 10px;
color: #6b5a33;
}
.au-cat-bid {
display: inline-flex;
align-items: center;
gap: 10px;
border: 1px solid #17130e;
background: #17130e;
color: #d4af37;
font-size: 13px;
letter-spacing: 0.14em;
padding: 12px 22px;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
}
.au-cat-bid:hover {
background: #d4af37;
color: #17130e;
}
.au-page-enter-active,
.au-page-leave-active {
transition: opacity 0.26s ease;
}
.au-page-enter-active .au-cat,
.au-page-leave-active .au-cat {
transition: transform 0.34s cubic-bezier(0.25, 1, 0.4, 1);
transform-origin: left center;
}
.au-page-enter-from,
.au-page-leave-to {
opacity: 0;
}
.au-page-enter-from .au-cat,
.au-page-leave-to .au-cat {
transform: perspective(1400px) rotateY(-14deg) translateX(-16px);
}
@media (max-width: 760px) {
.au-cat {
grid-template-columns: 1fr;
overflow-y: auto;
}
.au-cat-plate img {
max-height: 36vh;
}
.au-cat-page {
overflow-y: visible;
}
}
</style>

View File

@@ -1,7 +1,20 @@
<script setup>
import { ref } from 'vue'
import { corpProfiles } from '../../data/corpProfiles'
const c = corpProfiles.art
/* 先锋展览手册 */
const zineIdx = ref(-1)
const zine = ref(null)
function openZine(i) {
zineIdx.value = i
zine.value = c.cases[i]
}
function closeZine() {
zineIdx.value = -1
zine.value = null
}
</script>
<template>
@@ -36,18 +49,60 @@ const c = corpProfiles.art
<section id="shows" class="ag-sec">
<h2 class="ag-h2">CURRENT<br />SHOWS </h2>
<div class="ag-shows">
<article v-for="(w, i) in c.cases" :key="w.name" class="ag-show" :class="'s' + i">
<article v-for="(w, i) in c.cases" :key="w.name" class="ag-show" :class="'s' + i" @click="openZine(i)">
<span class="ag-show-no">{{ String(i + 1).padStart(2, '0') }}</span>
<i class="ag-show-tape" aria-hidden="true"></i>
<img :src="w.image" :alt="w.name" loading="lazy" />
<div class="ag-show-info">
<h3>{{ w.name }}</h3>
<p>{{ w.tag }} / {{ w.year }} {{ w.desc }}</p>
<span class="ag-show-more">OPEN ZINE 翻手册</span>
</div>
</article>
</div>
</section>
<!-- 先锋展览手册 -->
<Transition name="ag-slam">
<div v-if="zine" class="ag-mask" @click.self="closeZine">
<article class="ag-zine">
<button class="ag-zine-x" aria-label="关闭" @click="closeZine"> CLOSE</button>
<header class="ag-zine-head">
<span class="ag-zine-no">ZINE {{ String(zineIdx + 1).padStart(2, '0') }}</span>
<h3 class="ag-glitch" :data-text="zine.name">{{ zine.name }}</h3>
<p class="ag-zine-meta">{{ zine.tag }} / {{ zine.year }} / FREE ENTRY</p>
</header>
<div class="ag-zine-img">
<img :src="zine.image" :alt="zine.name" />
<span aria-hidden="true">FIG.{{ zineIdx + 1 }} INSTALLATION VIEW</span>
</div>
<section class="ag-zine-block">
<b>STATEMENT 陈述</b>
<p>{{ zine.story }}</p>
</section>
<section class="ag-zine-block">
<b>WHY GO 看点</b>
<ul>
<li v-for="h in zine.highlights" :key="h">{{ h }}</li>
</ul>
</section>
<div class="ag-zine-cols">
<section class="ag-zine-block">
<b>LOG 记录</b>
<p v-for="t in zine.timeline" :key="t.at" class="ag-zine-tl"><i>{{ t.at }}</i>{{ t.event }}</p>
</section>
<section class="ag-zine-block">
<b>DATA 数据</b>
<div class="ag-zine-mx">
<div v-for="m in zine.metrics" :key="m.label"><b>{{ m.value }}</b><span>{{ m.label }}</span></div>
</div>
</section>
</div>
<footer class="ag-zine-foot">NO MEANING ALLOWED 手册免费带走 LUMINA*</footer>
</article>
</div>
</Transition>
<!-- 宣言 -->
<section id="manifesto" class="ag-sec ag-manifesto">
<h2 class="ag-h2">MANIFESTO</h2>
@@ -545,4 +600,233 @@ const c = corpProfiles.art
gap: 6px;
}
}
/* ---- 先锋展览手册 ---- */
.ag-show {
cursor: pointer;
}
.ag-show-more {
display: inline-block;
margin-top: 8px;
font-size: 11px;
font-weight: 900;
letter-spacing: 0.1em;
color: #101010;
background: #08d9d6;
padding: 3px 10px;
opacity: 0;
transform: translateY(4px) rotate(-2deg);
transition: opacity 0.18s ease, transform 0.18s ease;
}
.ag-show:hover .ag-show-more {
opacity: 1;
transform: translateY(0) rotate(-2deg);
}
.ag-mask {
position: fixed;
inset: 0;
z-index: 520;
display: grid;
place-items: center;
padding: 20px;
background: rgba(16, 16, 16, 0.88);
}
.ag-zine {
position: relative;
width: min(640px, 94vw);
max-height: 88vh;
overflow-y: auto;
background: #101010;
border: 3px solid #f4f4f4;
box-shadow: 12px 12px 0 #ff2e63;
color: #f4f4f4;
}
.ag-zine-x {
position: sticky;
top: 0;
left: 100%;
z-index: 3;
display: block;
border: none;
background: #ff2e63;
color: #101010;
font-size: 12px;
font-weight: 900;
letter-spacing: 0.1em;
padding: 8px 14px;
cursor: pointer;
}
.ag-zine-x:hover {
background: #08d9d6;
}
.ag-zine-head {
padding: 8px 26px 18px;
border-bottom: 3px solid #f4f4f4;
}
.ag-zine-no {
display: inline-block;
font-size: 11px;
font-weight: 900;
letter-spacing: 0.2em;
background: #f4f4f4;
color: #101010;
padding: 2px 10px;
transform: rotate(-1.5deg);
}
.ag-zine-head h3 {
margin: 12px 0 6px;
font-size: clamp(28px, 5vw, 44px);
font-weight: 900;
letter-spacing: -0.03em;
line-height: 1.05;
}
.ag-zine-meta {
margin: 0;
font-size: 11.5px;
font-weight: 700;
letter-spacing: 0.14em;
color: #8d8d8d;
}
.ag-zine-img {
position: relative;
border-bottom: 3px solid #f4f4f4;
}
.ag-zine-img img {
display: block;
width: 100%;
aspect-ratio: 16 / 8;
object-fit: cover;
filter: grayscale(0.35) contrast(1.12);
}
.ag-zine-img span {
position: absolute;
left: 0;
bottom: 0;
background: #101010;
color: #08d9d6;
font-size: 9.5px;
font-weight: 900;
letter-spacing: 0.18em;
padding: 4px 12px;
}
.ag-zine-block {
padding: 16px 26px;
}
.ag-zine-block > b {
display: block;
margin-bottom: 8px;
font-size: 12px;
font-weight: 900;
letter-spacing: 0.16em;
color: #ff2e63;
}
.ag-zine-block p {
margin: 0;
font-size: 13px;
line-height: 1.95;
color: #cfcfcf;
}
.ag-zine-block ul {
margin: 0;
padding: 0;
list-style: none;
}
.ag-zine-block ul li {
position: relative;
padding: 4px 0 4px 24px;
font-size: 13px;
line-height: 1.8;
color: #f4f4f4;
font-weight: 700;
}
.ag-zine-block ul li::before {
content: '✖';
position: absolute;
left: 0;
color: #08d9d6;
}
.ag-zine-cols {
display: grid;
grid-template-columns: 1.2fr 1fr;
border-top: 1.5px dashed #4a4a4a;
}
.ag-zine-cols .ag-zine-block + .ag-zine-block {
border-left: 1.5px dashed #4a4a4a;
}
.ag-zine-tl {
margin: 0 0 7px !important;
font-size: 12px !important;
}
.ag-zine-tl i {
display: block;
font-style: normal;
font-weight: 900;
color: #08d9d6;
font-variant-numeric: tabular-nums;
}
.ag-zine-mx {
display: grid;
gap: 8px;
}
.ag-zine-mx > div {
border: 2px solid #f4f4f4;
padding: 8px 10px;
transform: rotate(-0.6deg);
}
.ag-zine-mx > div:nth-child(2) {
transform: rotate(0.8deg);
border-color: #ff2e63;
}
.ag-zine-mx > div:nth-child(3) {
transform: rotate(-1deg);
border-color: #08d9d6;
}
.ag-zine-mx b {
display: block;
font-size: 19px;
font-weight: 900;
}
.ag-zine-mx span {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
color: #8d8d8d;
}
.ag-zine-foot {
padding: 12px 26px 18px;
border-top: 3px solid #f4f4f4;
font-size: 10.5px;
font-weight: 900;
letter-spacing: 0.2em;
color: #8d8d8d;
}
.ag-slam-enter-active,
.ag-slam-leave-active {
transition: opacity 0.22s ease;
}
.ag-slam-enter-active .ag-zine {
transition: transform 0.28s cubic-bezier(0.2, 1.4, 0.4, 1);
}
.ag-slam-leave-active .ag-zine {
transition: transform 0.2s ease;
}
.ag-slam-enter-from,
.ag-slam-leave-to {
opacity: 0;
}
.ag-slam-enter-from .ag-zine {
transform: scale(1.12) rotate(2deg);
}
.ag-slam-leave-to .ag-zine {
transform: scale(0.94);
}
@media (max-width: 560px) {
.ag-zine-cols {
grid-template-columns: 1fr;
}
.ag-zine-cols .ag-zine-block + .ag-zine-block {
border-left: none;
border-top: 1.5px dashed #4a4a4a;
}
}
</style>

View File

@@ -1,4 +1,5 @@
<script setup>
import { ref } from 'vue'
import { corpProfiles } from '../../data/corpProfiles'
const c = corpProfiles.art
@@ -7,6 +8,19 @@ const rarities = ['GENESIS', 'RARE', 'ULTRA', 'LIMITED']
const hues = [265, 190, 320, 205]
const tick = chains.map((h, i) => `${h}${String(2600 + i * 137).slice(-4)} 已铸造`).join(' ◇ ') + ' ◇ '
/* 数字藏品档案 */
const dossierIdx = ref(-1)
const dossier = ref(null)
function openDossier(i) {
dossierIdx.value = i
dossier.value = c.cases[i]
}
function closeDossier() {
dossierIdx.value = -1
dossier.value = null
}
const hashOf = (i) => `${chains[i]}${String(2600 + i * 137).slice(-4)}`
/* 全息卡片:跟随光标的 3D 倾斜与眩光 */
function tilt(e) {
const el = e.currentTarget
@@ -67,6 +81,7 @@ function untilt(e) {
:style="{ '--hue': hues[i], '--d': (i * 90 + 80) + 'ms' }"
@mousemove="tilt"
@mouseleave="untilt"
@click="openDossier(i)"
>
<div class="ad-card-img">
<img :src="w.image" :alt="w.name" loading="lazy" />
@@ -79,10 +94,50 @@ function untilt(e) {
<span>限量 {{ (i + 2) * 50 }} </span>
<b>{{ w.year }} 铸造</b>
</div>
<span class="ad-card-more"> 打开链上档案</span>
</article>
</div>
</section>
<!-- 数字藏品档案 -->
<Transition name="ad-holo">
<div v-if="dossier" class="ad-mask" @click.self="closeDossier">
<article class="ad-dossier" :style="{ '--hue': hues[dossierIdx] }">
<button class="ad-dossier-x" aria-label="关闭" @click="closeDossier"></button>
<div class="ad-dossier-vis">
<img :src="dossier.image" :alt="dossier.name" />
<span class="ad-dossier-scan" aria-hidden="true"></span>
<span class="ad-rare ad-dossier-rare">{{ rarities[dossierIdx] }}</span>
</div>
<div class="ad-dossier-info">
<p class="ad-dossier-kicker">TOKEN DOSSIER · 链上档案</p>
<h3>{{ dossier.name }} · 数字版</h3>
<dl class="ad-dossier-meta">
<div><dt>TOKEN</dt><dd>{{ hashOf(dossierIdx) }}</dd></div>
<div><dt>EDITION</dt><dd>限量 {{ (dossierIdx + 2) * 50 }} </dd></div>
<div><dt>MINTED</dt><dd>{{ dossier.year }}</dd></div>
<div><dt>SOURCE</dt><dd>{{ dossier.tag }}</dd></div>
</dl>
<p class="ad-dossier-story">{{ dossier.story }}</p>
<div class="ad-dossier-block">
<b>// TRAITS 特征</b>
<ul>
<li v-for="h in dossier.highlights" :key="h">{{ h }}</li>
</ul>
</div>
<div class="ad-dossier-block">
<b>// ON-CHAIN LOG 链上记录</b>
<p v-for="t in dossier.timeline" :key="t.at" class="ad-dossier-tl"><i>{{ t.at }}</i>{{ t.event }}</p>
</div>
<div class="ad-dossier-mx">
<div v-for="m in dossier.metrics" :key="m.label"><b>{{ m.value }}</b><span>{{ m.label }}</span></div>
</div>
<p class="ad-dossier-note"> 档案已验证 · 数字孪生与实体展品一一对应</p>
</div>
</article>
</div>
</Transition>
<!-- 实验室 -->
<section id="lab" class="ad-sec">
<h2 class="ad-h2"><span class="ad-h2-tag">// LAB_MODULES</span>数字馆实验室</h2>
@@ -580,4 +635,248 @@ function untilt(e) {
font-size: 11px !important;
color: #55497e !important;
}
/* ---- 数字藏品档案 ---- */
.ad-card {
cursor: pointer;
}
.ad-card-more {
display: block;
margin: 8px 16px 14px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
color: #00e0ff;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.ad-card:hover .ad-card-more {
opacity: 1;
transform: translateY(0);
}
.ad-mask {
position: fixed;
inset: 0;
z-index: 520;
display: grid;
place-items: center;
padding: 20px;
background: rgba(5, 3, 12, 0.82);
backdrop-filter: blur(6px);
}
.ad-dossier {
position: relative;
display: grid;
grid-template-columns: 0.9fr 1.1fr;
width: min(900px, 94vw);
max-height: 88vh;
background: #0d0920;
border: 1px solid hsla(var(--hue, 265), 90%, 65%, 0.55);
border-radius: 18px;
overflow: hidden;
box-shadow: 0 0 60px hsla(var(--hue, 265), 90%, 55%, 0.28), 0 30px 80px rgba(0, 0, 0, 0.6);
}
.ad-dossier-x {
position: absolute;
right: 14px;
top: 14px;
z-index: 4;
width: 34px;
height: 34px;
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: 50%;
background: rgba(10, 7, 20, 0.7);
color: #efeaff;
font-size: 14px;
cursor: pointer;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.ad-dossier-x:hover {
border-color: #00e0ff;
box-shadow: 0 0 14px rgba(0, 224, 255, 0.5);
}
.ad-dossier-vis {
position: relative;
overflow: hidden;
background: conic-gradient(from 220deg at 60% 40%, hsla(var(--hue, 265), 90%, 55%, 0.35), transparent 40%), #080514;
}
.ad-dossier-vis img {
width: 100%;
height: 100%;
min-height: 320px;
object-fit: cover;
opacity: 0.92;
}
.ad-dossier-scan {
position: absolute;
inset: 0;
background: linear-gradient(transparent 0 46%, hsla(var(--hue, 265), 100%, 75%, 0.22) 50%, transparent 54% 100%);
background-size: 100% 260%;
animation: ad-scan 3.4s linear infinite;
pointer-events: none;
}
@keyframes ad-scan {
from { background-position: 0 130%; }
to { background-position: 0 -130%; }
}
.ad-dossier-rare {
position: absolute;
left: 14px;
top: 14px;
}
.ad-dossier-info {
padding: clamp(22px, 3vw, 34px) clamp(20px, 2.6vw, 30px);
overflow-y: auto;
}
.ad-dossier-kicker {
margin: 0 0 6px;
font-family: Consolas, monospace;
font-size: 10.5px;
letter-spacing: 0.26em;
color: hsla(var(--hue, 265), 90%, 75%, 0.9);
}
.ad-dossier-info h3 {
margin: 0 0 14px;
font-size: clamp(20px, 2.6vw, 26px);
font-weight: 800;
background: linear-gradient(90deg, #b56bff, #00e0ff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.ad-dossier-meta {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin: 0 0 14px;
}
.ad-dossier-meta > div {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 9px;
padding: 8px 12px;
}
.ad-dossier-meta dt {
font-family: Consolas, monospace;
font-size: 9px;
letter-spacing: 0.2em;
color: #7d6fae;
}
.ad-dossier-meta dd {
margin: 3px 0 0;
font-size: 13px;
font-weight: 700;
color: #efeaff;
font-family: Consolas, 'PingFang SC', monospace;
}
.ad-dossier-story {
margin: 0 0 14px;
font-size: 12.5px;
line-height: 1.95;
color: #b7abdd;
}
.ad-dossier-block {
margin-bottom: 14px;
}
.ad-dossier-block > b {
display: block;
margin-bottom: 7px;
font-family: Consolas, monospace;
font-size: 10.5px;
letter-spacing: 0.18em;
color: #00e0ff;
}
.ad-dossier-block ul {
margin: 0;
padding: 0;
list-style: none;
}
.ad-dossier-block ul li {
position: relative;
padding: 4px 0 4px 20px;
font-size: 12.5px;
line-height: 1.8;
color: #d9d2f2;
}
.ad-dossier-block ul li::before {
content: '◈';
position: absolute;
left: 0;
color: hsla(var(--hue, 265), 90%, 70%, 0.95);
font-size: 11px;
}
.ad-dossier-tl {
display: flex;
gap: 12px;
margin: 0 0 5px;
font-size: 12px;
color: #b7abdd;
}
.ad-dossier-tl i {
flex: 0 0 62px;
font-style: normal;
font-family: Consolas, monospace;
color: #7d6fae;
font-variant-numeric: tabular-nums;
}
.ad-dossier-mx {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 12px;
}
.ad-dossier-mx > div {
background: linear-gradient(160deg, hsla(var(--hue, 265), 80%, 55%, 0.18), rgba(255, 255, 255, 0.03));
border: 1px solid hsla(var(--hue, 265), 80%, 65%, 0.4);
border-radius: 10px;
padding: 9px 6px;
text-align: center;
}
.ad-dossier-mx b {
display: block;
font-size: 16.5px;
font-weight: 800;
color: #fff;
text-shadow: 0 0 14px hsla(var(--hue, 265), 90%, 70%, 0.7);
}
.ad-dossier-mx span {
font-size: 10px;
color: #9d90c8;
}
.ad-dossier-note {
margin: 0;
font-size: 10.5px;
letter-spacing: 0.08em;
color: #7d6fae;
}
.ad-holo-enter-active,
.ad-holo-leave-active {
transition: opacity 0.26s ease;
}
.ad-holo-enter-active .ad-dossier,
.ad-holo-leave-active .ad-dossier {
transition: transform 0.34s cubic-bezier(0.25, 1.1, 0.4, 1);
}
.ad-holo-enter-from,
.ad-holo-leave-to {
opacity: 0;
}
.ad-holo-enter-from .ad-dossier,
.ad-holo-leave-to .ad-dossier {
transform: perspective(1100px) rotateX(6deg) scale(0.92);
}
@media (max-width: 760px) {
.ad-dossier {
grid-template-columns: 1fr;
overflow-y: auto;
}
.ad-dossier-vis img {
min-height: 0;
max-height: 34vh;
}
.ad-dossier-info {
overflow-y: visible;
}
}
</style>

View File

@@ -1,9 +1,22 @@
<script setup>
import { ref } from 'vue'
import { corpProfiles } from '../../data/corpProfiles'
const c = corpProfiles.art
const crayons = ['#ff6b9d', '#ffb020', '#6bcb77', '#4d96ff']
const rots = ['-1.8deg', '1.6deg', '-1.2deg', '2.2deg']
/* 儿童导览卡 */
const cardIdx = ref(-1)
const card = ref(null)
function openCard(i) {
cardIdx.value = i
card.value = c.cases[i]
}
function closeCard() {
cardIdx.value = -1
card.value = null
}
const courses = [
{ icon: '🖍️', name: '蜡笔涂鸦班', age: '36 岁', desc: '把墙上的想象力搬到纸上(合法的那种)' },
{ icon: '✂️', name: '彩纸拼贴课', age: '59 岁', desc: '撕、剪、贴,一张彩纸变一个世界' },
@@ -60,15 +73,52 @@ const courses = [
<circle cx="995" cy="12" r="4.5" fill="#8c6f4a" />
</svg>
<div class="ak-works">
<figure v-for="(w, i) in c.cases" :key="w.name" class="ak-work" :style="{ '--c': crayons[(i + 1) % 4], '--r': ((i % 2 ? 1 : -1) * 2) + 'deg' }">
<figure v-for="(w, i) in c.cases" :key="w.name" class="ak-work" :style="{ '--c': crayons[(i + 1) % 4], '--r': ((i % 2 ? 1 : -1) * 2) + 'deg' }" @click="openCard(i)">
<img :src="w.image" :alt="w.name" loading="lazy" />
<figcaption>{{ w.name }}<span>灵感来自{{ w.tag }}</span></figcaption>
<figcaption>{{ w.name }}<span>灵感来自{{ w.tag }}</span><em class="ak-work-more">🔍 领导览卡</em></figcaption>
<span v-if="i % 2" class="ak-tape"></span>
<span v-else class="ak-pin" aria-hidden="true"></span>
</figure>
</div>
</section>
<!-- 儿童导览卡 -->
<Transition name="ak-bounce">
<div v-if="card" class="ak-mask" @click.self="closeCard">
<article class="ak-guide" :style="{ '--c': crayons[cardIdx % 4] }">
<button class="ak-guide-x" aria-label="关闭" @click="closeCard"></button>
<header class="ak-guide-head">
<span class="ak-guide-badge">小小导览卡 No.{{ cardIdx + 1 }}</span>
<h3>{{ card.name }}</h3>
<p>{{ card.tag }} · {{ card.year }} · 小朋友也能看懂的版本</p>
</header>
<div class="ak-guide-img">
<img :src="card.image" :alt="card.name" />
</div>
<section class="ak-guide-block">
<b>🗣 讲给你听</b>
<p>{{ card.story }}</p>
</section>
<section class="ak-guide-block">
<b>👀 看什么</b>
<ul>
<li v-for="(h, i) in card.highlights" :key="h" :style="{ '--lc': crayons[i % 4] }">{{ h }}</li>
</ul>
</section>
<section class="ak-guide-block">
<b>📅 展览小日历</b>
<p v-for="t in card.timeline" :key="t.at" class="ak-guide-tl"><i>{{ t.at }}</i>{{ t.event }}</p>
</section>
<div class="ak-guide-mx">
<div v-for="(m, i) in card.metrics" :key="m.label" :style="{ '--c': crayons[i % 4] }">
<b>{{ m.value }}</b><span>{{ m.label }}</span>
</div>
</div>
<footer class="ak-guide-foot">🌟 看完盖个章:今天也是小小艺术家!</footer>
</article>
</div>
</Transition>
<!-- 数据 -->
<div class="ak-stats">
<div v-for="(s, i) in c.stats" :key="s.label" :style="{ '--c': crayons[i] }">
@@ -496,4 +546,202 @@ const courses = [
margin-top: 12px;
}
}
/* ---- 儿童导览卡 ---- */
.ak-work {
cursor: pointer;
}
.ak-work-more {
display: block;
margin-top: 4px;
font-style: normal;
font-size: 11px;
color: #ff6b9d;
opacity: 0;
transform: scale(0.9);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.ak-work:hover .ak-work-more {
opacity: 1;
transform: scale(1);
}
.ak-mask {
position: fixed;
inset: 0;
z-index: 520;
display: grid;
place-items: center;
padding: 20px;
background: rgba(74, 63, 53, 0.55);
backdrop-filter: blur(4px);
}
.ak-guide {
position: relative;
width: min(520px, 94vw);
max-height: 88vh;
overflow-y: auto;
background: #fffdf6;
border: 4px solid var(--c, #ffb020);
border-radius: 26px;
box-shadow: 0 20px 54px rgba(74, 63, 53, 0.4), 0 0 0 8px rgba(255, 255, 255, 0.35);
}
.ak-guide-x {
position: absolute;
right: 14px;
top: 14px;
z-index: 3;
width: 38px;
height: 38px;
border: 3px solid #fff;
border-radius: 50%;
background: var(--c, #ffb020);
color: #fff;
font-size: 15px;
font-weight: 900;
cursor: pointer;
box-shadow: 0 4px 10px rgba(74, 63, 53, 0.3);
transition: transform 0.18s ease;
}
.ak-guide-x:hover {
transform: rotate(90deg) scale(1.08);
}
.ak-guide-head {
padding: 22px 24px 12px;
text-align: center;
}
.ak-guide-badge {
display: inline-block;
background: var(--c, #ffb020);
color: #fff;
font-size: 12px;
padding: 4px 16px;
border-radius: 999px;
transform: rotate(-2deg);
box-shadow: 0 3px 0 rgba(74, 63, 53, 0.2);
}
.ak-guide-head h3 {
margin: 12px 0 4px;
font-size: 24px;
color: #4a3f35;
}
.ak-guide-head p {
margin: 0;
font-size: 12px;
color: #a08a72;
}
.ak-guide-img {
padding: 0 24px;
}
.ak-guide-img img {
display: block;
width: 100%;
aspect-ratio: 16 / 8;
object-fit: cover;
border-radius: 16px;
border: 4px dashed var(--c, #ffb020);
}
.ak-guide-block {
padding: 14px 26px 2px;
}
.ak-guide-block > b {
display: inline-block;
margin-bottom: 6px;
font-size: 14.5px;
color: #4a3f35;
background: linear-gradient(transparent 62%, color-mix(in srgb, var(--c, #ffb020) 38%, transparent) 62%);
padding: 0 4px;
}
.ak-guide-block p {
margin: 0;
font-size: 13px;
line-height: 1.95;
color: #6d5c4c;
}
.ak-guide-block ul {
margin: 0;
padding: 0;
list-style: none;
}
.ak-guide-block ul li {
position: relative;
padding: 4px 0 4px 24px;
font-size: 13px;
line-height: 1.8;
color: #6d5c4c;
}
.ak-guide-block ul li::before {
content: '';
position: absolute;
left: 4px;
top: 12px;
width: 10px;
height: 10px;
border-radius: 3px;
background: var(--lc, #ffb020);
transform: rotate(12deg);
}
.ak-guide-tl {
display: flex;
gap: 10px;
margin: 0 0 5px !important;
}
.ak-guide-tl i {
flex: 0 0 64px;
font-style: normal;
font-weight: 700;
color: var(--c, #ffb020);
font-variant-numeric: tabular-nums;
}
.ak-guide-mx {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
padding: 14px 26px 4px;
}
.ak-guide-mx > div {
background: #fff;
border: 3px solid var(--c, #ffb020);
border-radius: 14px;
padding: 10px 6px;
text-align: center;
transform: rotate(-1deg);
}
.ak-guide-mx > div:nth-child(2) {
transform: rotate(1.4deg);
}
.ak-guide-mx b {
display: block;
font-size: 17px;
color: #4a3f35;
}
.ak-guide-mx span {
font-size: 10.5px;
color: #a08a72;
}
.ak-guide-foot {
padding: 12px 26px 20px;
text-align: center;
font-size: 13px;
color: #8c6f4a;
}
.ak-bounce-enter-active,
.ak-bounce-leave-active {
transition: opacity 0.24s ease;
}
.ak-bounce-enter-active .ak-guide {
transition: transform 0.4s cubic-bezier(0.3, 1.6, 0.5, 1);
}
.ak-bounce-leave-active .ak-guide {
transition: transform 0.2s ease;
}
.ak-bounce-enter-from,
.ak-bounce-leave-to {
opacity: 0;
}
.ak-bounce-enter-from .ak-guide {
transform: scale(0.7) rotate(-4deg);
}
.ak-bounce-leave-to .ak-guide {
transform: scale(0.92);
}
</style>

View File

@@ -1,7 +1,20 @@
<script setup>
import { ref } from 'vue'
import { corpProfiles } from '../../data/corpProfiles'
const c = corpProfiles.art
/* 白盒展签详情 */
const selIdx = ref(-1)
const sel = ref(null)
function openLabel(i) {
selIdx.value = i
sel.value = c.cases[i]
}
function closeLabel() {
selIdx.value = -1
sel.value = null
}
</script>
<template>
@@ -35,6 +48,7 @@ const c = corpProfiles.art
class="aw-work"
:class="'p' + i"
:style="{ '--d': (i * 0.14) + 's' }"
@click="openLabel(i)"
>
<i class="aw-wire" aria-hidden="true"></i>
<div class="aw-frame">
@@ -46,11 +60,45 @@ const c = corpProfiles.art
<b>{{ w.name }}</b>
<span>{{ w.tag }} · {{ w.year }}</span>
<p>{{ w.desc }}</p>
<i class="aw-plaque-more">走近看展签 </i>
</figcaption>
</figure>
</div>
</section>
<!-- 白盒展签详情 -->
<Transition name="aw-fade">
<div v-if="sel" class="aw-mask" @click.self="closeLabel">
<article class="aw-detail">
<button class="aw-detail-x" aria-label="关闭" @click="closeLabel"></button>
<div class="aw-detail-art">
<img :src="sel.image" :alt="sel.name" />
<i class="aw-detail-lamp" aria-hidden="true"></i>
</div>
<div class="aw-detail-label">
<em>CAT. {{ String(selIdx + 1).padStart(3, '0') }} {{ sel.tag }}</em>
<h3>{{ sel.name }}</h3>
<span class="aw-detail-year">{{ c.name }} · {{ sel.year }}</span>
<p class="aw-detail-story">{{ sel.story }}</p>
<div class="aw-detail-block">
<b>看点</b>
<ul>
<li v-for="h in sel.highlights" :key="h">{{ h }}</li>
</ul>
</div>
<div class="aw-detail-block">
<b>展览记录</b>
<p v-for="t in sel.timeline" :key="t.at" class="aw-detail-tl"><i>{{ t.at }}</i>{{ t.event }}</p>
</div>
<div class="aw-detail-mx">
<div v-for="m in sel.metrics" :key="m.label"><b>{{ m.value }}</b><span>{{ m.label }}</span></div>
</div>
<p class="aw-detail-note">请勿触摸作品 · 欢迎触摸这张展签</p>
</div>
</article>
</div>
</Transition>
<!-- 关于楼层索引式条目 -->
<section id="about" class="aw-sec aw-about">
<p class="aw-sec-label">ABOUT 美术馆</p>
@@ -592,4 +640,222 @@ const c = corpProfiles.art
padding-left: clamp(48px, 14vw, 128px);
}
}
/* ---- 白盒展签详情 ---- */
.aw-work {
cursor: pointer;
}
.aw-plaque-more {
display: block;
margin-top: 6px;
font-style: normal;
font-size: 10.5px;
letter-spacing: 0.12em;
color: #1c1c1c;
opacity: 0;
transform: translateX(-4px);
transition: opacity 0.22s ease, transform 0.22s ease;
}
.aw-work:hover .aw-plaque-more {
opacity: 1;
transform: translateX(0);
}
.aw-mask {
position: fixed;
inset: 0;
z-index: 520;
display: grid;
place-items: center;
padding: 22px;
background: rgba(28, 28, 28, 0.55);
backdrop-filter: blur(6px);
}
.aw-detail {
position: relative;
display: grid;
grid-template-columns: 1.1fr 0.9fr;
width: min(880px, 94vw);
max-height: 88vh;
background: #fcfcfa;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.4);
}
.aw-detail-x {
position: absolute;
right: 14px;
top: 14px;
z-index: 3;
width: 36px;
height: 36px;
border: 1px solid #d9d9d2;
border-radius: 50%;
background: #fcfcfa;
color: #1c1c1c;
font-size: 14px;
cursor: pointer;
transition: background 0.18s ease, color 0.18s ease;
}
.aw-detail-x:hover {
background: #1c1c1c;
color: #fcfcfa;
border-color: #1c1c1c;
}
.aw-detail-art {
position: relative;
display: grid;
place-items: center;
background: #f1f1ec;
padding: clamp(20px, 4vw, 44px);
overflow: hidden;
}
.aw-detail-art img {
width: 100%;
max-height: 70vh;
object-fit: cover;
border: 10px solid #20201e;
outline: 1px solid #d9d9d2;
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28);
}
.aw-detail-lamp {
position: absolute;
left: 50%;
top: 0;
width: 240px;
height: 150px;
transform: translateX(-50%);
background: radial-gradient(ellipse 50% 100% at 50% 0%, rgba(255, 250, 230, 0.75), transparent 70%);
pointer-events: none;
}
.aw-detail-label {
padding: clamp(22px, 3.5vw, 40px) clamp(20px, 3vw, 36px);
overflow-y: auto;
}
.aw-detail-label em {
font-style: normal;
font-size: 10px;
letter-spacing: 0.3em;
color: #7a7a76;
}
.aw-detail-label h3 {
margin: 10px 0 4px;
font-size: clamp(20px, 2.6vw, 27px);
font-weight: 700;
letter-spacing: 0.02em;
}
.aw-detail-year {
display: block;
font-size: 11.5px;
color: #7a7a76;
margin-bottom: 16px;
}
.aw-detail-story {
margin: 0 0 18px;
padding-bottom: 16px;
font-size: 13px;
line-height: 2.05;
color: #45443f;
border-bottom: 1px solid #ecece7;
}
.aw-detail-block {
margin-bottom: 16px;
}
.aw-detail-block > b {
display: block;
margin-bottom: 8px;
font-size: 10.5px;
letter-spacing: 0.3em;
color: #7a7a76;
font-weight: 700;
}
.aw-detail-block ul {
margin: 0;
padding: 0;
list-style: none;
}
.aw-detail-block ul li {
position: relative;
padding: 4px 0 4px 18px;
font-size: 12.5px;
line-height: 1.8;
color: #2c2c28;
}
.aw-detail-block ul li::before {
content: '';
position: absolute;
left: 2px;
top: 13px;
width: 6px;
height: 6px;
border: 1px solid #1c1c1c;
}
.aw-detail-tl {
display: flex;
gap: 12px;
margin: 0 0 5px;
font-size: 12px;
color: #45443f;
}
.aw-detail-tl i {
flex: 0 0 62px;
font-style: normal;
color: #9c9c96;
font-variant-numeric: tabular-nums;
}
.aw-detail-mx {
display: grid;
grid-template-columns: repeat(3, 1fr);
border-top: 1px solid #1c1c1c;
border-bottom: 1px solid #1c1c1c;
margin-bottom: 14px;
}
.aw-detail-mx > div {
padding: 12px 6px;
text-align: center;
}
.aw-detail-mx > div + div {
border-left: 1px solid #ecece7;
}
.aw-detail-mx b {
display: block;
font-size: 18px;
font-weight: 700;
}
.aw-detail-mx span {
font-size: 10px;
letter-spacing: 0.08em;
color: #7a7a76;
}
.aw-detail-note {
margin: 0;
font-size: 10.5px;
letter-spacing: 0.14em;
color: #9c9c96;
}
.aw-fade-enter-active,
.aw-fade-leave-active {
transition: opacity 0.26s ease;
}
.aw-fade-enter-active .aw-detail,
.aw-fade-leave-active .aw-detail {
transition: transform 0.32s cubic-bezier(0.25, 1, 0.4, 1);
}
.aw-fade-enter-from,
.aw-fade-leave-to {
opacity: 0;
}
.aw-fade-enter-from .aw-detail,
.aw-fade-leave-to .aw-detail {
transform: translateY(20px) scale(0.97);
}
@media (max-width: 760px) {
.aw-detail {
grid-template-columns: 1fr;
overflow-y: auto;
}
.aw-detail-art img {
max-height: 38vh;
}
.aw-detail-label {
overflow-y: visible;
}
}
</style>

View File

@@ -58,7 +58,7 @@ const fmt = (n) => n.toLocaleString('en-US')
<span>SELECTED WORKS</span>
</div>
<div class="at-works">
<router-link v-for="p in works" :key="p.id" to="/corp/furn-atelier/products" class="at-work">
<router-link v-for="p in works" :key="p.id" :to="`/corp/furn-atelier/products#${p.id}`" class="at-work">
<div class="at-work-img">
<img :src="`https://picsum.photos/seed/${p.seed}/780/920`" :alt="p.name" loading="lazy" />
<span class="at-work-view">VIEW INDEX </span>

320
src/pages/lab/AsciiArt.vue Normal file
View File

@@ -0,0 +1,320 @@
<script setup>
import { ref } from 'vue'
const output = ref('')
const cols = ref(80)
const charset = ref('classic')
const invert = ref(false)
const copied = ref(false)
const fileName = ref('')
const dragOver = ref(false)
const CHARSETS = {
classic: '@%#*+=-:. ',
blocks: '█▓▒░ ',
simple: '#o+. ',
digits: '9731. ',
}
let lastImage = null
function handleFile(file) {
if (!file || !file.type.startsWith('image/')) return
fileName.value = file.name
const img = new Image()
img.onload = () => {
lastImage = img
render()
URL.revokeObjectURL(img.src)
}
img.src = URL.createObjectURL(file)
}
function onPick(e) {
handleFile(e.target.files[0])
e.target.value = ''
}
function onDrop(e) {
dragOver.value = false
handleFile(e.dataTransfer.files[0])
}
function render() {
if (!lastImage) return
const img = lastImage
const w = Math.min(160, Math.max(20, cols.value))
// 字符高宽比约 2:1,压缩纵向
const h = Math.max(4, Math.round((img.height / img.width) * w * 0.52))
const cv = document.createElement('canvas')
cv.width = w
cv.height = h
const ctx = cv.getContext('2d')
ctx.drawImage(img, 0, 0, w, h)
const data = ctx.getImageData(0, 0, w, h).data
const chars = CHARSETS[charset.value]
let out = ''
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const i = (y * w + x) * 4
let lum = (0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2]) / 255
// 透明像素当白
const alpha = data[i + 3] / 255
lum = lum * alpha + (1 - alpha)
if (invert.value) lum = 1 - lum
const idx = Math.min(chars.length - 1, Math.floor(lum * chars.length))
out += chars[idx]
}
out += '\n'
}
output.value = out
}
function demo() {
// 内置示例:canvas 画一只简笔猫再转字符
const cv = document.createElement('canvas')
cv.width = cv.height = 200
const c = cv.getContext('2d')
c.fillStyle = '#fff'
c.fillRect(0, 0, 200, 200)
c.fillStyle = '#111'
c.beginPath()
c.arc(100, 115, 62, 0, Math.PI * 2)
c.fill()
c.beginPath()
c.moveTo(52, 80); c.lineTo(40, 28); c.lineTo(88, 58)
c.moveTo(148, 80); c.lineTo(160, 28); c.lineTo(112, 58)
c.fill()
c.fillStyle = '#fff'
c.beginPath()
c.arc(78, 105, 10, 0, Math.PI * 2)
c.arc(122, 105, 10, 0, Math.PI * 2)
c.fill()
const img = new Image()
img.onload = () => {
lastImage = img
fileName.value = '内置示例 · 一只猫'
render()
}
img.src = cv.toDataURL()
}
async function copy() {
if (!output.value) return
try {
await navigator.clipboard.writeText(output.value)
copied.value = true
setTimeout(() => (copied.value = false), 1400)
} catch { /* 剪贴板不可用 */ }
}
function download() {
if (!output.value) return
const a = document.createElement('a')
a.href = URL.createObjectURL(new Blob([output.value], { type: 'text/plain' }))
a.download = 'ascii-art.txt'
a.click()
URL.revokeObjectURL(a.href)
}
</script>
<template>
<div class="aa">
<nav class="lab-nav">
<router-link to="/lab" class="lab-back"> 实验室</router-link>
<h1 class="lab-title">ASCII 字符画</h1>
<span class="lab-badge">CANVAS</span>
</nav>
<p class="lab-desc">拖一张图进来,按亮度映射成字符画;可调宽度字符集反相,支持复制与下载 txt图片只在本地处理,不上传</p>
<div class="aa-controls">
<label
class="aa-drop"
:class="{ over: dragOver }"
@dragover.prevent="dragOver = true"
@dragleave="dragOver = false"
@drop.prevent="onDrop"
>
<input type="file" accept="image/*" @change="onPick" />
{{ fileName || '点击选图 / 拖图到这里' }}
</label>
<button class="aa-demo" @click="demo">🐱 用示例图</button>
<label class="aa-slider">
{{ cols }} 字符
<input v-model.number="cols" type="range" min="30" max="160" @change="render" />
</label>
<div class="aa-sets">
<button
v-for="(v, k) in CHARSETS"
:key="k"
:class="{ on: charset === k }"
@click="charset = k; render()"
>
{{ { classic: '经典', blocks: '色块', simple: '简洁', digits: '数字' }[k] }}
</button>
</div>
<label class="aa-invert"><input v-model="invert" type="checkbox" @change="render" />反相</label>
<button class="aa-act" :class="{ ok: copied }" :disabled="!output" @click="copy">{{ copied ? ' 已复制' : '复制' }}</button>
<button class="aa-act" :disabled="!output" @click="download">下载 .txt</button>
</div>
<pre v-if="output" class="aa-output">{{ output }}</pre>
<div v-else class="aa-empty">
<p>还没有图选一张本地图片,或者点用示例图看效果</p>
</div>
</div>
</template>
<style scoped>
.aa {
min-height: 100vh;
background: #0b0e14;
color: #e8ecf4;
font-family: 'Space Grotesk', 'Noto Serif SC', sans-serif;
padding: 24px clamp(16px, 5vw, 64px) 60px;
}
.lab-nav {
display: flex;
align-items: center;
gap: 16px;
max-width: 1060px;
margin: 0 auto 10px;
}
.lab-back {
color: #f59e0b;
text-decoration: none;
font-size: 13px;
border: 1px solid rgba(245, 158, 11, 0.4);
border-radius: 8px;
padding: 7px 14px;
}
.lab-back:hover { background: rgba(245, 158, 11, 0.12); }
.lab-title { margin: 0; font-size: 22px; font-weight: 700; }
.lab-badge {
font-size: 10px;
letter-spacing: 0.22em;
color: #5eead4;
border: 1px solid rgba(94, 234, 212, 0.4);
border-radius: 99px;
padding: 4px 10px;
}
.lab-desc {
max-width: 1060px;
margin: 0 auto 18px;
font-size: 13.5px;
color: rgba(232, 236, 244, 0.66);
line-height: 1.8;
}
.aa-controls {
max-width: 1060px;
margin: 0 auto 16px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 14px;
}
.aa-drop {
position: relative;
border: 1.5px dashed #2a2f3d;
border-radius: 12px;
padding: 11px 20px;
font-size: 12.5px;
color: rgba(232, 236, 244, 0.7);
cursor: pointer;
transition: all 0.2s ease;
max-width: 260px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.aa-drop.over,
.aa-drop:hover {
border-color: #f59e0b;
color: #f59e0b;
}
.aa-drop input {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
}
.aa-demo {
background: #13161f;
border: 1px solid #2a2f3d;
border-radius: 10px;
color: rgba(232, 236, 244, 0.78);
font-size: 12.5px;
padding: 10px 15px;
cursor: pointer;
}
.aa-demo:hover { border-color: #5eead4; color: #5eead4; }
.aa-slider {
display: flex;
align-items: center;
gap: 9px;
font-size: 12px;
color: rgba(232, 236, 244, 0.65);
}
.aa-slider input { accent-color: #f59e0b; }
.aa-sets {
display: flex;
gap: 6px;
}
.aa-sets button {
background: none;
border: 1px solid #2a2f3d;
border-radius: 99px;
color: rgba(232, 236, 244, 0.7);
font-size: 11.5px;
padding: 7px 13px;
cursor: pointer;
}
.aa-sets button.on { border-color: #5eead4; color: #5eead4; }
.aa-invert {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: rgba(232, 236, 244, 0.65);
}
.aa-invert input { accent-color: #f59e0b; }
.aa-act {
background: #13161f;
border: 1px solid #2a2f3d;
border-radius: 10px;
color: rgba(232, 236, 244, 0.78);
font-size: 12.5px;
padding: 10px 15px;
cursor: pointer;
}
.aa-act:disabled { opacity: 0.4; cursor: default; }
.aa-act.ok { border-color: #5eead4; color: #5eead4; }
.aa-output {
max-width: 1060px;
margin: 0 auto;
background: #0d1017;
border: 1px solid #222736;
border-radius: 14px;
padding: 18px;
overflow: auto;
font-family: Consolas, 'Courier New', monospace;
font-size: 9px;
line-height: 1.05;
letter-spacing: 0.06em;
color: #7dd487;
max-height: 70vh;
}
.aa-empty {
max-width: 1060px;
margin: 0 auto;
border: 1.5px dashed #222736;
border-radius: 14px;
padding: 70px 20px;
text-align: center;
}
.aa-empty p {
margin: 0;
font-size: 13px;
color: rgba(232, 236, 244, 0.4);
}
</style>

View File

@@ -1,5 +1,9 @@
<script setup>
import { ref } from 'vue'
import { profile } from '../../data/profile'
/* 玻璃详情对话框 */
const dialog = ref(null)
</script>
<template>
@@ -7,6 +11,7 @@ import { profile } from '../../data/profile'
<!-- 吸顶导航光泽玻璃条 -->
<nav class="ae-nav">
<router-link to="/" class="ae-back"> 返回合集</router-link>
<router-link to="/style/aero/gadgets" class="ae-back">🧩 侧边栏小工具</router-link>
<span class="ae-nav-title">niangao aero</span>
<span class="ae-nav-time">2026 / 08 / 11</span>
</nav>
@@ -52,7 +57,16 @@ import { profile } from '../../data/profile'
<section class="ae-sec">
<h2 class="ae-h2">作品橱窗</h2>
<div class="ae-works">
<article v-for="p in profile.projects" :key="p.name" class="ae-work ae-glossy">
<article
v-for="p in profile.projects"
:key="p.name"
class="ae-work ae-glossy click"
role="button"
tabindex="0"
title="打开属性对话框"
@click="dialog = p"
@keyup.enter="dialog = p"
>
<div class="ae-work-img">
<img :src="p.image" :alt="p.name" loading="lazy" />
<span class="ae-shine"></span>
@@ -61,7 +75,7 @@ import { profile } from '../../data/profile'
<p class="ae-work-desc">{{ p.desc }}</p>
<div class="ae-work-foot">
<span v-for="t in p.tags" :key="t" class="ae-chip">{{ t }}</span>
<b>{{ p.year }} · {{ p.stars }}</b>
<b>{{ p.year }} · {{ p.stars }} · 详情 </b>
</div>
</article>
</div>
@@ -74,10 +88,218 @@ import { profile } from '../../data/profile'
</span>
</footer>
</div>
<!-- 玻璃详情对话框 -->
<Transition name="ae-dialog">
<div v-if="dialog" class="ae-dlg-mask" @click.self="dialog = null">
<div class="ae-dlg">
<header class="ae-dlg-bar">
<span class="ae-dlg-ic">💠</span>
<b>{{ dialog.name }} 属性</b>
<button class="ae-dlg-x" title="关闭" @click="dialog = null"></button>
</header>
<div class="ae-dlg-body">
<div class="ae-dlg-hero">
<img :src="dialog.image" :alt="dialog.name" loading="lazy" />
<span class="ae-shine"></span>
</div>
<p class="ae-dlg-sub">{{ dialog.alias }} · {{ dialog.year }} · {{ dialog.stars }}</p>
<p class="ae-dlg-story">{{ dialog.story }}</p>
<p class="ae-dlg-h"> 功能亮点</p>
<p v-for="(h, hi) in dialog.highlights" :key="hi" class="ae-dlg-li"><i></i>{{ h }}</p>
<p class="ae-dlg-h"> 版本历程</p>
<p v-for="t in dialog.timeline" :key="t.at" class="ae-dlg-li tl"><b>{{ t.at }}</b>{{ t.event }}</p>
<div class="ae-dlg-metrics">
<span v-for="m in dialog.metrics" :key="m.label"><b>{{ m.value }}</b>{{ m.label }}</span>
</div>
</div>
<footer class="ae-dlg-foot">
<span>{{ dialog.tags.join(' · ') }}</span>
<button class="ae-dlg-ok" @click="dialog = null">确定</button>
</footer>
</div>
</div>
</Transition>
</div>
</template>
<style scoped>
/* ---- 玻璃详情对话框 ---- */
.ae-work.click {
cursor: pointer;
}
.ae-dlg-mask {
position: fixed;
inset: 0;
z-index: 500;
background: rgba(28, 62, 94, 0.4);
backdrop-filter: blur(5px);
display: flex;
align-items: center;
justify-content: center;
padding: 22px;
}
.ae-dlg {
width: min(520px, 100%);
max-height: 86vh;
display: flex;
flex-direction: column;
border-radius: 12px;
overflow: hidden;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(228, 244, 255, 0.94));
border: 1px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 0 0 4px rgba(120, 190, 240, 0.35), 0 30px 70px rgba(20, 50, 80, 0.5);
}
.ae-dlg-bar {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(160, 214, 250, 0.75) 48%, rgba(108, 189, 242, 0.72) 52%, rgba(190, 230, 252, 0.85));
border-bottom: 1px solid rgba(120, 180, 220, 0.5);
flex-shrink: 0;
}
.ae-dlg-bar b {
flex: 1;
font-size: 13.5px;
color: #14406e;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.8);
}
.ae-dlg-x {
width: 26px;
height: 22px;
border-radius: 5px;
border: 1px solid rgba(160, 60, 60, 0.6);
background: linear-gradient(180deg, #ff9f96, #e0574a);
color: #fff;
font-size: 10px;
cursor: pointer;
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.6);
}
.ae-dlg-body {
flex: 1;
overflow-y: auto;
padding: 14px 18px;
}
.ae-dlg-hero {
position: relative;
border-radius: 10px;
overflow: hidden;
margin-bottom: 10px;
border: 1px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 8px 20px rgba(20, 64, 110, 0.2);
}
.ae-dlg-hero img {
width: 100%;
aspect-ratio: 16 / 7;
object-fit: cover;
display: block;
}
.ae-dlg-sub {
margin: 0 0 8px;
font-size: 11.5px;
color: #557a99;
}
.ae-dlg-story {
margin: 0 0 10px;
font-size: 13px;
line-height: 1.85;
color: #1d3a52;
}
.ae-dlg-h {
margin: 12px 0 6px;
font-size: 12.5px;
font-weight: 700;
color: #1a6fb5;
}
.ae-dlg-li {
margin: 0 0 6px;
font-size: 12.5px;
line-height: 1.7;
color: #2c4f6e;
display: flex;
gap: 8px;
align-items: baseline;
}
.ae-dlg-li i {
width: 9px;
height: 9px;
border-radius: 50%;
background: radial-gradient(circle at 32% 28%, #d6f2c8, #6bbf4e 65%, #3f8f2e);
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.8);
flex-shrink: 0;
transform: translateY(1px);
}
.ae-dlg-li.tl b {
color: #1a6fb5;
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
.ae-dlg-metrics {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin: 10px 0 4px;
}
.ae-dlg-metrics span {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(214, 238, 252, 0.9));
border: 1px solid rgba(150, 200, 235, 0.7);
border-radius: 10px;
padding: 8px 13px;
font-size: 10.5px;
color: #557a99;
display: flex;
flex-direction: column;
gap: 2px;
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.9);
}
.ae-dlg-metrics b {
font-size: 15px;
color: #14406e;
}
.ae-dlg-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 16px;
border-top: 1px solid rgba(150, 200, 235, 0.55);
background: rgba(255, 255, 255, 0.6);
font-size: 11.5px;
color: #557a99;
flex-shrink: 0;
}
.ae-dlg-ok {
min-width: 88px;
padding: 7px 18px;
border-radius: 8px;
border: 1px solid rgba(60, 130, 60, 0.55);
background: linear-gradient(180deg, #d9f2c8, #8fd47a 48%, #5cb548 52%, #a4e08c);
color: #14421c;
font-family: inherit;
font-size: 13px;
font-weight: 700;
cursor: pointer;
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.8), 0 3px 8px rgba(40, 100, 40, 0.25);
}
.ae-dlg-ok:hover {
filter: brightness(1.06);
}
.ae-dialog-enter-active,
.ae-dialog-leave-active {
transition: opacity 0.22s ease;
}
.ae-dialog-enter-active .ae-dlg {
transition: transform 0.26s cubic-bezier(0.2, 1.2, 0.4, 1);
}
.ae-dialog-enter-from,
.ae-dialog-leave-to {
opacity: 0;
}
.ae-dialog-enter-from .ae-dlg {
transform: scale(0.82);
}
.ae {
min-height: 100vh;
background: linear-gradient(180deg, #6cbdf2 0%, #a8ddfa 34%, #cdeefd 52%, #8fd47a 72%, #4faf3f 100%);

View File

@@ -0,0 +1,597 @@
<script setup>
import { ref } from 'vue'
import { profile } from '../../data/profile'
const drawer = ref(false)
/* 作品详情底部抽屉(Material You bottom sheet) */
const workSheet = ref(null)
const apps = [
{ icon: '☎', name: '电话' },
{ icon: '✉', name: '短信' },
{ icon: '⌘', name: '终端' },
{ icon: '♫', name: '音乐' },
{ icon: '⚙', name: '设置' },
{ icon: '⌗', name: '相册' },
{ icon: '✎', name: '备忘' },
{ icon: '⏱', name: '时钟' },
]
</script>
<template>
<div class="and">
<div class="g-phone">
<!-- 状态栏 -->
<header class="g-status">
<span>21:47</span>
<b class="g-cam"></b>
<span class="g-icons"> </span>
</header>
<!-- At a Glance -->
<div class="g-glance">
<p class="g-clock">21:47</p>
<p class="g-date">8月12日周三 · 多云转写代码 26°</p>
</div>
<!-- 大号资料小组件 -->
<section class="g-widget hero">
<img :src="profile.avatars.cute" alt="头像" class="g-avatar" />
<div>
<p class="g-widget-title">{{ profile.name }}</p>
<p class="g-widget-sub">{{ profile.job }} · {{ profile.age }} · {{ profile.location }}</p>
<p class="g-widget-body">{{ profile.bioShort }}</p>
</div>
</section>
<!-- 技能小组件:圆环 -->
<section class="g-widget">
<p class="g-widget-name">技能电量 · Material You</p>
<div class="g-rings">
<div v-for="(sk, i) in profile.skills" :key="sk.name" class="g-ring" :class="`r${i % 4}`">
<i class="g-ring-fill" :style="{ '--pct': sk.level }"></i>
<b>{{ sk.level }}</b>
<span>{{ sk.name.split(' / ')[0] }}</span>
</div>
</div>
</section>
<!-- 作品通知卡 -->
<section class="g-widget">
<p class="g-widget-name">作品动态</p>
<button v-for="p in profile.projects" :key="p.name" class="g-notif tap" @click="workSheet = p">
<span class="g-notif-icon"></span>
<div class="g-notif-body">
<p class="g-notif-title">{{ p.name }} <small>{{ p.alias }}</small></p>
<p class="g-notif-text">{{ p.desc }}</p>
<p class="g-notif-meta">{{ p.year }} · {{ p.stars }} · {{ p.tags.join(' · ') }} · 轻点展开</p>
</div>
</button>
</section>
<!-- 搜索药丸 -->
<div class="g-search">
<span></span>
<em>搜索联系方式</em>
<b @click="drawer = !drawer"></b>
</div>
<!-- dock -->
<footer class="g-dock">
<router-link to="/" class="g-app" title="返回合集"><i class="d0"></i></router-link>
<router-link to="/style/android-home/panel" class="g-app" title="通知中心"><i class="d0">🔔</i></router-link>
<button class="g-app" @click="drawer = true"><i class="d1"></i></button>
<a class="g-app" :href="`mailto:${profile.contacts[1].value}`"><i class="d2"></i></a>
<button class="g-app" @click="drawer = true"><i class="d3"></i></button>
</footer>
<!-- 应用抽屉(联系方式) -->
<transition name="g-sheet">
<div v-if="drawer" class="g-drawer" @click.self="drawer = false">
<div class="g-sheet">
<i class="g-handle"></i>
<p class="g-sheet-title">联系方式</p>
<div v-for="c in profile.contacts" :key="c.label" class="g-contact">
<b>{{ c.label }}</b><span>{{ c.value }}</span>
</div>
<p class="g-sheet-title" style="margin-top: 22px">常用应用</p>
<div class="g-grid">
<div v-for="a in apps" :key="a.name" class="g-appcell">
<i>{{ a.icon }}</i>
<span>{{ a.name }}</span>
</div>
</div>
<p class="g-motto">{{ profile.motto }}</p>
</div>
</div>
</transition>
<!-- 作品详情 bottom sheet -->
<transition name="g-sheet">
<div v-if="workSheet" class="g-drawer" @click.self="workSheet = null">
<div class="g-sheet g-work-sheet">
<i class="g-handle"></i>
<img :src="workSheet.image" :alt="workSheet.name" class="g-work-hero" loading="lazy" />
<p class="g-sheet-title" style="margin-top: 12px">
{{ workSheet.name }} <small class="g-work-sub">{{ workSheet.alias }} · {{ workSheet.year }} · {{ workSheet.stars }}</small>
</p>
<p class="g-work-story">{{ workSheet.story }}</p>
<div class="g-work-chips">
<span v-for="t in workSheet.tags" :key="t">{{ t }}</span>
</div>
<div class="g-work-metrics">
<div v-for="m in workSheet.metrics" :key="m.label">
<b>{{ m.value }}</b><span>{{ m.label }}</span>
</div>
</div>
<p class="g-work-label">亮点</p>
<p v-for="(h, hi) in workSheet.highlights" :key="hi" class="g-work-li"> {{ h }}</p>
<p class="g-work-label">时间线</p>
<p v-for="t in workSheet.timeline" :key="t.at" class="g-work-li tl"><b>{{ t.at }}</b>{{ t.event }}</p>
<button class="g-work-close" @click="workSheet = null">收起</button>
</div>
</div>
</transition>
</div>
</div>
</template>
<style scoped>
.and {
min-height: 100vh;
background: linear-gradient(170deg, #3a3f52, #23283a);
display: flex;
justify-content: center;
padding: 30px 12px 40px;
font-family: 'Space Grotesk', 'Noto Serif SC', Roboto, sans-serif;
}
.g-phone {
position: relative;
width: min(430px, 100%);
background:
radial-gradient(ellipse 90% 40% at 80% -6%, rgba(255, 200, 170, 0.55), transparent 60%),
radial-gradient(ellipse 80% 44% at 12% 106%, rgba(178, 200, 255, 0.5), transparent 62%),
linear-gradient(170deg, #f4e5dc, #e8e0f4 55%, #dbe4f2);
border-radius: 34px;
border: 8px solid #14161f;
padding: 12px 16px 90px;
overflow: hidden;
box-shadow: 0 30px 70px rgba(0, 0, 0, 0.5);
color: #2f2a36;
}
.g-status {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12.5px;
font-weight: 600;
padding: 2px 6px 0;
}
.g-cam {
width: 12px;
height: 12px;
border-radius: 50%;
background: #14161f;
}
.g-icons {
letter-spacing: 0.14em;
font-size: 11px;
}
.g-glance {
padding: 26px 10px 18px;
}
.g-clock {
margin: 0;
font-size: 56px;
font-weight: 500;
line-height: 1;
letter-spacing: -0.02em;
color: #4a3f66;
}
.g-date {
margin: 8px 0 0;
font-size: 13.5px;
color: rgba(47, 42, 54, 0.7);
}
.g-widget {
background: rgba(255, 255, 255, 0.72);
backdrop-filter: blur(6px);
border-radius: 26px;
padding: 16px 18px;
margin-bottom: 12px;
}
.g-widget.hero {
display: flex;
gap: 14px;
align-items: center;
background: #4a3f66;
color: #f4e5dc;
}
.g-avatar {
width: 64px;
height: 64px;
border-radius: 50%;
flex-shrink: 0;
background: #f4e5dc;
}
.g-widget-title {
margin: 0;
font-size: 20px;
font-weight: 700;
}
.g-widget-sub {
margin: 3px 0 6px;
font-size: 12px;
opacity: 0.8;
}
.g-widget-body {
margin: 0;
font-size: 12.5px;
line-height: 1.7;
font-family: 'Noto Serif SC', serif;
opacity: 0.92;
}
.g-widget-name {
margin: 0 0 14px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.06em;
color: rgba(47, 42, 54, 0.6);
}
/* 圆环 */
.g-rings {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px 8px;
}
.g-ring {
position: relative;
display: grid;
place-items: center;
text-align: center;
padding-top: 4px;
}
.g-ring-fill {
position: relative;
width: 66px;
height: 66px;
border-radius: 50%;
background: conic-gradient(var(--rc) calc(var(--pct) * 1%), rgba(0, 0, 0, 0.08) 0);
display: grid;
place-items: center;
}
.g-ring-fill::after {
content: '';
width: 48px;
height: 48px;
border-radius: 50%;
background: #fff;
}
.g-ring b {
position: absolute;
top: 26px;
font-size: 15px;
font-weight: 700;
}
.g-ring span {
margin-top: 7px;
font-size: 11px;
color: rgba(47, 42, 54, 0.75);
}
.g-ring.r0 { --rc: #7c6cdb; }
.g-ring.r1 { --rc: #d96a8b; }
.g-ring.r2 { --rc: #4a9d7f; }
.g-ring.r3 { --rc: #d9924a; }
/* 通知卡 */
.g-notif {
display: flex;
gap: 12px;
padding: 11px 4px;
border-top: 1px solid rgba(47, 42, 54, 0.08);
}
.g-notif:first-of-type {
border-top: none;
}
.g-notif-icon {
width: 38px;
height: 38px;
flex-shrink: 0;
display: grid;
place-items: center;
background: #e4dcf4;
color: #4a3f66;
border-radius: 14px;
font-size: 17px;
}
.g-notif-body {
min-width: 0;
}
.g-notif-title {
margin: 0 0 3px;
font-size: 14px;
font-weight: 700;
}
.g-notif-title small {
font-weight: 400;
font-size: 11px;
color: rgba(47, 42, 54, 0.55);
margin-left: 5px;
}
.g-notif-text {
margin: 0 0 4px;
font-size: 12.5px;
line-height: 1.7;
color: rgba(47, 42, 54, 0.8);
font-family: 'Noto Serif SC', serif;
}
.g-notif-meta {
margin: 0;
font-size: 11px;
color: rgba(47, 42, 54, 0.5);
}
.g-search {
display: flex;
align-items: center;
gap: 10px;
background: #e4dcf4;
border-radius: 999px;
padding: 13px 20px;
font-size: 13.5px;
color: rgba(47, 42, 54, 0.65);
}
.g-search em {
flex: 1;
font-style: normal;
}
.g-search b {
cursor: pointer;
}
.g-dock {
position: absolute;
left: 16px;
right: 16px;
bottom: 14px;
display: flex;
justify-content: space-around;
background: rgba(255, 255, 255, 0.66);
backdrop-filter: blur(8px);
border-radius: 30px;
padding: 10px 8px;
}
.g-app {
border: none;
background: none;
cursor: pointer;
text-decoration: none;
}
.g-app i {
width: 52px;
height: 52px;
display: grid;
place-items: center;
border-radius: 50%;
font-size: 21px;
font-style: normal;
transition: transform 0.16s ease;
}
.g-app:hover i {
transform: scale(1.1);
}
.g-app .d0 { background: #4a3f66; color: #f4e5dc; }
.g-app .d1 { background: #d96a8b; color: #fff; }
.g-app .d2 { background: #4a9d7f; color: #fff; }
.g-app .d3 { background: #d9924a; color: #fff; }
/* 抽屉 */
.g-drawer {
position: absolute;
inset: 0;
background: rgba(20, 22, 31, 0.4);
backdrop-filter: blur(2px);
display: flex;
align-items: flex-end;
z-index: 10;
}
.g-sheet {
width: 100%;
max-height: 82%;
overflow-y: auto;
background: #f6f1fb;
border-radius: 30px 30px 0 0;
padding: 12px 22px 26px;
}
.g-handle {
display: block;
width: 40px;
height: 4px;
border-radius: 2px;
background: rgba(47, 42, 54, 0.25);
margin: 0 auto 16px;
}
.g-sheet-title {
margin: 0 0 10px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
color: rgba(47, 42, 54, 0.55);
}
.g-contact {
display: flex;
justify-content: space-between;
background: #fff;
border-radius: 16px;
padding: 13px 16px;
margin-bottom: 8px;
font-size: 13.5px;
}
.g-contact b {
color: #4a3f66;
}
.g-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px 8px;
}
.g-appcell {
text-align: center;
}
.g-appcell i {
width: 50px;
height: 50px;
margin: 0 auto;
display: grid;
place-items: center;
background: #e4dcf4;
color: #4a3f66;
border-radius: 18px;
font-size: 20px;
font-style: normal;
}
.g-appcell span {
display: block;
margin-top: 5px;
font-size: 11px;
color: rgba(47, 42, 54, 0.7);
}
.g-motto {
margin: 20px 0 0;
text-align: center;
font-size: 12px;
color: rgba(47, 42, 54, 0.55);
font-family: 'Noto Serif SC', serif;
}
.g-sheet-enter-active,
.g-sheet-leave-active {
transition: opacity 0.25s ease;
}
.g-sheet-enter-active .g-sheet,
.g-sheet-leave-active .g-sheet {
transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.g-sheet-enter-from,
.g-sheet-leave-to {
opacity: 0;
}
.g-sheet-enter-from .g-sheet,
.g-sheet-leave-to .g-sheet {
transform: translateY(100%);
}
/* ---- 作品详情 sheet ---- */
.g-notif.tap {
width: 100%;
background: none;
border-right: none;
border-left: none;
border-bottom: none;
font: inherit;
color: inherit;
text-align: left;
cursor: pointer;
border-radius: 14px;
transition: background 0.15s;
}
.g-notif.tap:hover {
background: rgba(47, 42, 54, 0.05);
}
.g-work-hero {
width: 100%;
aspect-ratio: 16 / 8;
object-fit: cover;
border-radius: 22px;
margin-top: 10px;
}
.g-work-sub {
font-size: 11.5px;
color: #7a7286;
font-weight: 400;
margin-left: 6px;
}
.g-work-story {
margin: 8px 0 12px;
font-size: 13px;
line-height: 1.8;
color: #4b4553;
}
.g-work-chips {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.g-work-chips span {
background: #e3d7f3;
color: #4f3d6b;
border-radius: 10px;
padding: 5px 12px;
font-size: 11.5px;
font-weight: 600;
}
.g-work-metrics {
display: flex;
gap: 10px;
margin-bottom: 14px;
}
.g-work-metrics > div {
flex: 1;
background: #ece4f6;
border-radius: 18px;
padding: 12px 10px;
text-align: center;
display: flex;
flex-direction: column;
gap: 2px;
}
.g-work-metrics b {
font-size: 17px;
color: #2f2a36;
}
.g-work-metrics span {
font-size: 10.5px;
color: #7a7286;
}
.g-work-label {
margin: 12px 0 6px;
font-size: 12px;
font-weight: 700;
color: #6b5f7d;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.g-work-li {
margin: 0 0 5px;
font-size: 12.5px;
line-height: 1.7;
color: #4b4553;
}
.g-work-li.tl b {
color: #6247aa;
margin-right: 8px;
font-variant-numeric: tabular-nums;
}
.g-work-close {
display: block;
width: 100%;
margin-top: 16px;
background: #6247aa;
color: #fff;
border: none;
border-radius: 22px;
padding: 12px;
font-size: 14px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
}
.g-work-close:hover {
filter: brightness(1.1);
}
</style>

View File

@@ -0,0 +1,645 @@
<script setup>
import { ref } from 'vue'
import { profile } from '../../data/profile'
const credits = ref(0)
const picked = ref(-1)
function insertCoin() {
credits.value++
}
function pick(i) {
if (credits.value < 1) return
picked.value = i
}
/* 机台投币 → 看铭牌 */
const plaque = ref(null)
const shakeIdx = ref(-1)
function playCab(p, i) {
if (credits.value < 1) {
shakeIdx.value = i
setTimeout(() => (shakeIdx.value = -1), 500)
return
}
credits.value--
plaque.value = p
}
</script>
<template>
<div class="arc">
<nav class="a-nav">
<router-link to="/" class="a-back"> 离场</router-link>
<router-link to="/style/arcade/breakout" class="a-back"> 打砖块机台</router-link>
<span class="a-credit">CREDIT {{ String(credits).padStart(2, '0') }}</span>
</nav>
<!-- 跑马灯招牌 -->
<header class="a-marquee">
<i v-for="n in 22" :key="n" class="a-bulb" :style="{ animationDelay: (n % 4) * 0.25 + 's' }"></i>
<h1>NIANGAO ARCADE</h1>
<p>{{ profile.name }} · {{ profile.job }} · SINCE 2001</p>
</header>
<p class="a-insert" :class="{ ok: credits > 0 }" @click="insertCoin">
{{ credits > 0 ? '▼ 选择你的技能 ▼' : '☞ INSERT COIN ☜(点这里投币)' }}
</p>
<!-- 选人界面:技能就是可选角色 -->
<section class="a-select">
<div
v-for="(sk, i) in profile.skills"
:key="sk.name"
class="a-fighter"
:class="{ picked: picked === i, locked: credits < 1 }"
@click="pick(i)"
>
<div class="a-portrait">
<img :src="`https://api.dicebear.com/9.x/pixel-art/svg?seed=${sk.en}`" :alt="sk.name" />
</div>
<p class="a-fighter-name">{{ sk.name }}</p>
<div class="a-power">
<span>PWR</span>
<div class="a-power-bar"><i :style="{ width: sk.level + '%' }"></i></div>
<b>{{ sk.level }}</b>
</div>
<p v-if="picked === i" class="a-ready">READY!</p>
</div>
</section>
<!-- 机台一条街:作品 -->
<section class="a-cabs-wrap">
<h2 class="a-title">作品机台 · 投币试玩</h2>
<div class="a-cabs">
<article
v-for="(p, i) in profile.projects"
:key="p.name"
class="a-cab"
:class="[`c${i % 4}`, { shake: shakeIdx === i }]"
>
<div class="a-cab-marquee">{{ p.name }}</div>
<div class="a-cab-screen">
<p>{{ p.desc }}</p>
</div>
<div class="a-cab-panel">
<i class="a-stick"></i>
<b class="a-btn r"></b>
<b class="a-btn y"></b>
<button class="a-slot" :title="credits > 0 ? '投币 1 枚,查看机台铭牌' : '先在上面投币!'" @click="playCab(p, i)">
<i></i>投币
</button>
</div>
<footer>{{ p.year }} · HI-SCORE {{ p.stars }}</footer>
</article>
</div>
</section>
<footer class="a-footer">
<p class="a-rank">
<span>名次</span><span>玩家</span><span>联系方式</span>
</p>
<p v-for="(c, i) in profile.contacts" :key="c.label" class="a-rank-row">
<span>{{ ['1ST', '2ND', '3RD'][i] }}</span><span>{{ c.label.toUpperCase() }}</span><span>{{ c.value }}</span>
</p>
<p class="a-fin">{{ profile.motto }} GAME OVER? CONTINUE!</p>
</footer>
<!-- 机台铭牌 -->
<Transition name="a-plaque">
<div v-if="plaque" class="a-plaque-mask" @click.self="plaque = null">
<div class="a-plaque">
<header class="a-plaque-head">
<span class="a-plaque-rivet"></span>
<b>机台铭牌 · SPEC PLATE</b>
<span class="a-plaque-rivet"></span>
</header>
<h3 class="a-plaque-title">{{ plaque.name }}</h3>
<p class="a-plaque-serial">SERIAL NO. {{ plaque.slug.toUpperCase() }}-{{ plaque.year }} · {{ plaque.alias }}</p>
<p class="a-plaque-story">{{ plaque.story }}</p>
<div class="a-plaque-grid">
<div class="a-plaque-block">
<b> 机能特性</b>
<p v-for="(h, hi) in plaque.highlights" :key="hi"> {{ h }}</p>
</div>
<div class="a-plaque-block">
<b> 出厂记录</b>
<p v-for="t in plaque.timeline" :key="t.at"><i>{{ t.at }}</i>{{ t.event }}</p>
<b style="margin-top: 10px"> 性能参数</b>
<p v-for="m in plaque.metrics" :key="m.label"><i>{{ m.label }}</i>{{ m.value }}</p>
</div>
</div>
<footer class="a-plaque-foot">
<span>{{ plaque.tags.join(' / ') }}</span>
<button @click="plaque = null">继续游戏 </button>
</footer>
</div>
</div>
</Transition>
</div>
</template>
<style scoped>
.arc {
min-height: 100vh;
background:
radial-gradient(ellipse at 50% -20%, rgba(155, 92, 255, 0.25), transparent 55%),
repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.3) 0 2px, transparent 2px 4px),
#120a1e;
color: #f4f1ff;
font-family: 'Press Start 2P', monospace;
padding: 22px clamp(12px, 4vw, 60px) 60px;
}
.a-nav {
position: sticky;
top: 10px;
z-index: 60;
background: rgba(8, 8, 14, 0.62);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 12px;
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
max-width: 920px;
margin: 0 auto 28px;
}
.a-back {
color: #ffd23f;
text-decoration: none;
font-size: 11px;
border: 2px solid #ffd23f;
padding: 8px 12px;
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.6);
}
.a-back:hover {
background: #ffd23f;
color: #120a1e;
}
.a-credit {
font-size: 11px;
color: #5eead4;
animation: a-blink 1.2s steps(2, end) infinite;
}
.a-marquee {
position: relative;
max-width: 920px;
margin: 0 auto;
text-align: center;
border: 4px solid #ff2bd6;
border-radius: 14px;
padding: 34px 16px 24px;
background: linear-gradient(180deg, #2b1147, #1b0b33);
box-shadow: 0 0 34px rgba(255, 43, 214, 0.35), inset 0 0 24px rgba(255, 43, 214, 0.14);
overflow: hidden;
}
.a-bulb {
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
background: #ffd23f;
box-shadow: 0 0 10px #ffd23f;
animation: a-blink 1s steps(2, end) infinite;
}
.a-bulb:nth-child(-n + 11) { top: 7px; }
.a-bulb:nth-child(n + 12) { bottom: 7px; }
.a-bulb:nth-child(1), .a-bulb:nth-child(12) { left: 4%; }
.a-bulb:nth-child(2), .a-bulb:nth-child(13) { left: 13%; }
.a-bulb:nth-child(3), .a-bulb:nth-child(14) { left: 22%; }
.a-bulb:nth-child(4), .a-bulb:nth-child(15) { left: 31%; }
.a-bulb:nth-child(5), .a-bulb:nth-child(16) { left: 40%; }
.a-bulb:nth-child(6), .a-bulb:nth-child(17) { left: 49%; }
.a-bulb:nth-child(7), .a-bulb:nth-child(18) { left: 58%; }
.a-bulb:nth-child(8), .a-bulb:nth-child(19) { left: 67%; }
.a-bulb:nth-child(9), .a-bulb:nth-child(20) { left: 76%; }
.a-bulb:nth-child(10), .a-bulb:nth-child(21) { left: 85%; }
.a-bulb:nth-child(11), .a-bulb:nth-child(22) { left: 94%; }
@keyframes a-blink {
50% { opacity: 0.25; box-shadow: none; }
}
.a-marquee h1 {
margin: 0 0 14px;
font-size: clamp(20px, 4.4vw, 38px);
color: #ffd23f;
text-shadow: 0 0 18px rgba(255, 210, 63, 0.7), 4px 4px 0 #ff2bd6;
letter-spacing: 0.06em;
}
.a-marquee p {
margin: 0;
font-size: 10px;
color: #5eead4;
line-height: 1.9;
}
.a-insert {
max-width: 920px;
margin: 26px auto;
text-align: center;
font-size: 12px;
color: #ffd23f;
cursor: pointer;
animation: a-blink 1.4s steps(2, end) infinite;
user-select: none;
}
.a-insert.ok {
color: #5eead4;
animation: none;
}
.a-insert:active {
transform: scale(0.96);
}
/* 选人 */
.a-select {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 14px;
max-width: 920px;
margin: 0 auto;
}
.a-fighter {
position: relative;
border: 3px solid #3d2a66;
background: #1b0f33;
padding: 12px 10px 12px;
text-align: center;
cursor: pointer;
transition: border-color 0.15s ease, transform 0.15s ease;
}
.a-fighter:hover {
border-color: #5eead4;
transform: translateY(-3px);
}
.a-fighter.locked {
cursor: not-allowed;
opacity: 0.75;
}
.a-fighter.picked {
border-color: #ffd23f;
box-shadow: 0 0 20px rgba(255, 210, 63, 0.4);
animation: a-pick 0.3s steps(3, end);
}
@keyframes a-pick {
from { transform: scale(0.9); }
}
.a-portrait {
width: 72px;
height: 72px;
margin: 0 auto 10px;
border: 3px solid #3d2a66;
background: #2b1147;
image-rendering: pixelated;
}
.a-portrait img {
width: 100%;
height: 100%;
display: block;
}
.a-fighter-name {
margin: 0 0 8px;
font-size: 9.5px;
line-height: 1.5;
color: #f4f1ff;
}
.a-power {
display: flex;
align-items: center;
gap: 5px;
}
.a-power span {
font-size: 8px;
color: #ff2bd6;
}
.a-power-bar {
flex: 1;
height: 8px;
border: 2px solid #3d2a66;
background: #120a1e;
}
.a-power-bar i {
display: block;
height: 100%;
background: repeating-linear-gradient(90deg, #5eead4 0 4px, #2b8f80 4px 6px);
}
.a-power b {
font-size: 9px;
color: #ffd23f;
}
.a-ready {
position: absolute;
left: 50%;
top: 34%;
transform: translateX(-50%) rotate(-8deg);
margin: 0;
font-size: 13px;
color: #ffd23f;
text-shadow: 2px 2px 0 #ff2bd6;
animation: a-blink 0.5s steps(2, end) infinite;
pointer-events: none;
}
/* 机台 */
.a-cabs-wrap {
max-width: 920px;
margin: 52px auto 0;
}
.a-title {
margin: 0 0 22px;
font-size: 13px;
color: #ff2bd6;
text-shadow: 0 0 14px rgba(255, 43, 214, 0.6);
letter-spacing: 0.08em;
}
.a-cabs {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 18px;
}
.a-cab {
border: 3px solid;
border-radius: 10px 10px 4px 4px;
overflow: hidden;
transition: transform 0.2s ease;
}
.a-cab:hover {
transform: translateY(-4px);
}
.a-cab.c0 { border-color: #ff2bd6; background: #240f3d; }
.a-cab.c1 { border-color: #5eead4; background: #0d2b30; }
.a-cab.c2 { border-color: #ffd23f; background: #33270a; }
.a-cab.c3 { border-color: #7a89f2; background: #131c3d; }
.a-cab-marquee {
padding: 9px 8px;
font-size: 10px;
text-align: center;
color: #120a1e;
letter-spacing: 0.04em;
}
.c0 .a-cab-marquee { background: #ff2bd6; }
.c1 .a-cab-marquee { background: #5eead4; }
.c2 .a-cab-marquee { background: #ffd23f; }
.c3 .a-cab-marquee { background: #7a89f2; }
.a-cab-screen {
margin: 10px;
border: 2px solid rgba(255, 255, 255, 0.2);
background: #050208;
min-height: 108px;
padding: 10px;
}
.a-cab-screen p {
margin: 0;
font-family: 'Noto Serif SC', serif;
font-size: 12px;
line-height: 1.85;
color: rgba(244, 241, 255, 0.85);
}
.a-cab-panel {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 14px 10px;
}
.a-slot {
margin-left: auto;
display: flex;
align-items: center;
gap: 6px;
background: #14141f;
border: 1px solid #3a3a52;
color: #ffd75e;
font-family: inherit;
font-size: 10px;
letter-spacing: 0.12em;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
}
.a-slot i {
width: 3px;
height: 12px;
background: #ffd75e;
border-radius: 2px;
box-shadow: 0 0 6px rgba(255, 215, 94, 0.8);
}
.a-slot:hover {
background: #24243a;
border-color: #ffd75e;
}
.a-cab.shake {
animation: a-shake 0.4s ease;
}
@keyframes a-shake {
20% { transform: translateX(-5px) rotate(-1deg); }
40% { transform: translateX(5px) rotate(1deg); }
60% { transform: translateX(-4px); }
80% { transform: translateX(4px); }
}
/* ---- 机台铭牌 ---- */
.a-plaque-mask {
position: fixed;
inset: 0;
z-index: 500;
background: rgba(6, 4, 18, 0.72);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.a-plaque {
width: min(620px, 100%);
max-height: 84vh;
overflow-y: auto;
background:
repeating-linear-gradient(115deg, rgba(255, 255, 255, 0.045) 0 2px, transparent 2px 5px),
linear-gradient(165deg, #3a3f4e, #262a36 60%, #30343f);
border: 3px solid #5b6274;
border-radius: 10px;
padding: 18px 22px;
color: #dfe4f0;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.7), inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
.a-plaque-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.a-plaque-head b {
font-size: 12px;
letter-spacing: 0.28em;
color: #9aa3ba;
}
.a-plaque-rivet {
width: 12px;
height: 12px;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #cdd4e4, #6a7288 60%, #3c4252);
box-shadow: inset 0 -1px 2px rgba(0, 0, 0, 0.6);
}
.a-plaque-title {
margin: 0 0 4px;
font-size: 24px;
color: #ffd75e;
text-shadow: 0 0 14px rgba(255, 215, 94, 0.45);
letter-spacing: 0.06em;
}
.a-plaque-serial {
margin: 0 0 12px;
font-size: 11px;
letter-spacing: 0.14em;
color: #8b93a8;
}
.a-plaque-story {
margin: 0 0 14px;
font-size: 13px;
line-height: 1.85;
color: #c6cddd;
border-left: 3px solid #ffd75e;
padding-left: 12px;
}
.a-plaque-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 620px) {
.a-plaque-grid {
grid-template-columns: 1fr;
}
}
.a-plaque-block b {
display: block;
font-size: 11.5px;
letter-spacing: 0.1em;
color: #7ee3ff;
margin-bottom: 7px;
}
.a-plaque-block p {
margin: 0 0 6px;
font-size: 12px;
line-height: 1.65;
color: #b8c0d4;
}
.a-plaque-block p i {
font-style: normal;
color: #ffd75e;
margin-right: 8px;
font-variant-numeric: tabular-nums;
}
.a-plaque-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-top: 16px;
border-top: 1px dashed #4a5065;
padding-top: 12px;
font-size: 11px;
color: #8b93a8;
}
.a-plaque-foot button {
background: #ffd75e;
border: none;
color: #262a36;
font-family: inherit;
font-weight: 700;
font-size: 12px;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
letter-spacing: 0.08em;
}
.a-plaque-foot button:hover {
filter: brightness(1.1);
}
.a-plaque-enter-active,
.a-plaque-leave-active {
transition: opacity 0.2s ease;
}
.a-plaque-enter-active .a-plaque {
transition: transform 0.26s cubic-bezier(0.2, 1.2, 0.4, 1);
}
.a-plaque-enter-from,
.a-plaque-leave-to {
opacity: 0;
}
.a-plaque-enter-from .a-plaque {
transform: translateY(40px) scale(0.92);
}
.a-stick {
width: 14px;
height: 14px;
border-radius: 50%;
background: radial-gradient(circle at 34% 30%, #ff6b6b, #b81f1f);
position: relative;
}
.a-stick::after {
content: '';
position: absolute;
left: 5.5px;
top: 12px;
width: 3px;
height: 9px;
background: #55493c;
}
.a-btn {
width: 12px;
height: 12px;
border-radius: 50%;
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.6);
}
.a-btn.r { background: #ff4911; }
.a-btn.y { background: #ffd23f; }
.a-cab footer {
padding: 0 14px 12px;
font-size: 8.5px;
color: rgba(244, 241, 255, 0.6);
letter-spacing: 0.06em;
}
/* 排行榜页脚 */
.a-footer {
max-width: 640px;
margin: 54px auto 0;
}
.a-rank,
.a-rank-row {
display: grid;
grid-template-columns: 64px 96px 1fr;
gap: 12px;
margin: 0;
padding: 8px 4px;
font-size: 10px;
}
.a-rank {
color: #ff2bd6;
border-bottom: 2px solid #3d2a66;
}
.a-rank-row {
color: #5eead4;
border-bottom: 1px dashed rgba(61, 42, 102, 0.8);
}
.a-rank-row span:first-child {
color: #ffd23f;
}
.a-rank-row span:last-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.a-fin {
margin: 26px 0 0;
text-align: center;
font-size: 10px;
line-height: 2;
color: rgba(244, 241, 255, 0.66);
font-family: 'Noto Serif SC', serif;
font-size: 12.5px;
}
</style>

View File

@@ -0,0 +1,587 @@
<script setup>
import { ref } from 'vue'
import { profile } from '../../data/profile'
const deco = ['◆', '❖', '◇', '✦']
/* 烫金节目单 */
const gala = ref(null)
const galaIdx = ref(0)
function openGala(p, i) {
gala.value = p
galaIdx.value = i
}
</script>
<template>
<div class="deco">
<nav class="d-nav">
<router-link to="/" class="d-back"> 返回合集</router-link>
<router-link to="/style/art-deco/cabaret" class="d-back"> 歌舞厅节目单</router-link>
<span class="d-nav-line"></span>
<span class="d-nav-est">EST. MMI</span>
</nav>
<!-- 日出扇形 hero -->
<header class="d-hero">
<div class="d-sunburst" aria-hidden="true">
<i v-for="n in 13" :key="n" :style="{ transform: `rotate(${(n - 7) * 9}deg)` }"></i>
</div>
<p class="d-over">华丽年代 · LE MAGNIFIQUE</p>
<h1 class="d-name">{{ profile.nameEn }}</h1>
<p class="d-name-zh">{{ profile.name }}</p>
<div class="d-divider"><i></i><b></b><i></i></div>
<p class="d-role">{{ profile.job }} · {{ profile.age }} · 杭州</p>
<p class="d-bio">{{ profile.bio }}</p>
</header>
<!-- 技能:鎏金阶梯表 -->
<section class="d-section">
<h2 class="d-title"><span></span>技艺六品<em>LES TALENTS</em></h2>
<div class="d-skills">
<div v-for="(sk, i) in profile.skills" :key="sk.name" class="d-skill">
<span class="d-skill-deco">{{ deco[i % 4] }}</span>
<div class="d-skill-main">
<div class="d-skill-head">
<b>{{ sk.name }}</b>
<span>{{ sk.level }}</span>
</div>
<div class="d-meter"><i :style="{ width: sk.level + '%' }"></i></div>
</div>
</div>
</div>
</section>
<!-- 作品:对称金框展柜 -->
<section class="d-section">
<h2 class="d-title"><span></span>作品陈列<em>LES ŒUVRES</em></h2>
<div class="d-works">
<article
v-for="(p, i) in profile.projects"
:key="p.name"
class="d-work click"
role="button"
tabindex="0"
title="展开烫金节目单"
@click="openGala(p, i)"
@keyup.enter="openGala(p, i)"
>
<header class="d-work-top">
<span class="d-work-no">N° {{ String(i + 1).padStart(2, '0') }}</span>
<span class="d-work-star"> {{ p.stars }}</span>
</header>
<h3>{{ p.name }}</h3>
<p class="d-work-alias"> {{ p.alias }} </p>
<p class="d-work-desc">{{ p.desc }}</p>
<footer class="d-work-tags">{{ p.tags.join(' · ') }}</footer>
<i class="d-corner tl"></i><i class="d-corner tr"></i><i class="d-corner bl"></i><i class="d-corner br"></i>
</article>
</div>
</section>
<footer class="d-footer">
<div class="d-divider"><i></i><b></b><i></i></div>
<p v-for="c in profile.contacts" :key="c.label" class="d-contact">
<span>{{ c.label }}</span>{{ c.value }}
</p>
<p class="d-fin">« {{ profile.motto }} »</p>
</footer>
<!-- 烫金节目单 -->
<Transition name="d-gala">
<div v-if="gala" class="d-gala-mask" @click.self="gala = null">
<div class="d-gala">
<i class="d-gala-sun" aria-hidden="true"></i>
<button class="d-gala-x" title="合上节目单" @click="gala = null"></button>
<p class="d-gala-no">PROGRAMME · N° {{ String(galaIdx + 1).padStart(2, '0') }}</p>
<h3 class="d-gala-title">{{ gala.name }}</h3>
<p class="d-gala-sub"> {{ gala.alias }} · {{ gala.year }} </p>
<div class="d-divider"><i></i><b>{{ deco[galaIdx % 4] }}</b><i></i></div>
<p class="d-gala-story">{{ gala.story }}</p>
<p class="d-gala-h"> ACTES · 华彩段落</p>
<p v-for="(h, hi) in gala.highlights" :key="hi" class="d-gala-li">
<span class="d-gala-li-no">{{ ['I', 'II', 'III', 'IV', 'V'][hi % 5] }}.</span>{{ h }}
</p>
<p class="d-gala-h"> CHRONIQUE · 编年</p>
<p v-for="t in gala.timeline" :key="t.at" class="d-gala-li tl"><b>{{ t.at }}</b>{{ t.event }}</p>
<div class="d-gala-metrics">
<span v-for="m in gala.metrics" :key="m.label"><b>{{ m.value }}</b>{{ m.label }}</span>
<span><b>{{ gala.stars }}</b>ÉTOILES</span>
</div>
<p class="d-gala-foot">{{ gala.tags.join(' · ') }} · GRAND FOYER</p>
</div>
</div>
</Transition>
</div>
</template>
<style scoped>
/* ---- 烫金节目单 ---- */
.d-work.click {
cursor: pointer;
}
.d-gala-mask {
position: fixed;
inset: 0;
z-index: 500;
background: rgba(8, 8, 12, 0.78);
display: flex;
align-items: center;
justify-content: center;
padding: 22px;
}
.d-gala {
position: relative;
width: min(480px, 100%);
max-height: 86vh;
overflow-y: auto;
background: linear-gradient(175deg, #14141c, #0e0e14);
border: 1px solid #c9a86a;
outline: 1px solid #c9a86a;
outline-offset: -8px;
padding: 30px 32px;
color: #e8dcc0;
text-align: center;
box-shadow: 0 0 60px rgba(201, 168, 106, 0.18), 0 34px 80px rgba(0, 0, 0, 0.7);
scrollbar-width: thin;
scrollbar-color: #c9a86a transparent;
}
.d-gala-sun {
position: absolute;
top: -1px;
left: 50%;
transform: translateX(-50%);
width: 130px;
height: 65px;
background:
repeating-conic-gradient(from 180deg at 50% 100%, #c9a86a 0 3deg, transparent 3deg 12deg);
border-radius: 0 0 130px 130px;
opacity: 0.4;
pointer-events: none;
}
.d-gala-x {
position: absolute;
top: 14px;
right: 14px;
width: 28px;
height: 28px;
border: 1px solid #c9a86a;
background: none;
color: #c9a86a;
cursor: pointer;
font-size: 11px;
}
.d-gala-no {
margin: 10px 0 6px;
font-size: 9.5px;
letter-spacing: 0.5em;
color: #a3854e;
}
.d-gala-title {
margin: 0;
font-size: 30px;
letter-spacing: 0.16em;
background: linear-gradient(180deg, #f2dfae, #c9a86a 55%, #f0d9a0);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-family: 'Noto Serif SC', Georgia, serif;
}
.d-gala-sub {
margin: 6px 0 0;
font-size: 11px;
letter-spacing: 0.3em;
color: #a3854e;
}
.d-gala-story {
margin: 0 0 12px;
font-size: 12.5px;
line-height: 2.1;
color: #d8ccae;
text-align: left;
font-family: 'Noto Serif SC', serif;
}
.d-gala-h {
margin: 14px 0 8px;
font-size: 11px;
letter-spacing: 0.4em;
color: #c9a86a;
}
.d-gala-li {
margin: 0 0 6px;
font-size: 12px;
line-height: 1.9;
color: #cfc3a2;
text-align: left;
display: flex;
gap: 10px;
}
.d-gala-li-no {
color: #c9a86a;
flex-shrink: 0;
min-width: 22px;
text-align: right;
}
.d-gala-li.tl b {
color: #c9a86a;
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
.d-gala-metrics {
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
margin: 14px 0 10px;
}
.d-gala-metrics span {
border: 1px solid #c9a86a;
padding: 9px 15px;
font-size: 9.5px;
letter-spacing: 0.1em;
color: #a3854e;
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
clip-path: polygon(10px 0, calc(100% - 10px) 0, 100% 50%, calc(100% - 10px) 100%, 10px 100%, 0 50%);
background: rgba(201, 168, 106, 0.06);
}
.d-gala-metrics b {
font-size: 15px;
color: #f2dfae;
}
.d-gala-foot {
margin: 0;
font-size: 10px;
letter-spacing: 0.24em;
color: #a3854e;
}
.d-gala-enter-active,
.d-gala-leave-active {
transition: opacity 0.26s ease;
}
.d-gala-enter-active .d-gala {
transition: transform 0.34s cubic-bezier(0.2, 1, 0.35, 1);
}
.d-gala-enter-from,
.d-gala-leave-to {
opacity: 0;
}
.d-gala-enter-from .d-gala {
transform: translateY(34px) scale(0.94);
}
.deco {
min-height: 100vh;
background:
radial-gradient(ellipse 90% 40% at 50% -6%, rgba(212, 180, 106, 0.16), transparent 70%),
repeating-linear-gradient(90deg, transparent 0 68px, rgba(212, 180, 106, 0.05) 68px 69px),
linear-gradient(175deg, #101410, #0a0d0b 55%, #0c100d);
color: #e8dcc0;
font-family: 'Playfair Display', 'Noto Serif SC', serif;
padding: 26px clamp(16px, 6vw, 90px) 60px;
}
.d-nav {
position: sticky;
top: 10px;
z-index: 60;
background: rgba(8, 8, 14, 0.62);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 12px;
padding: 8px 12px;
display: flex;
align-items: center;
gap: 18px;
max-width: 980px;
margin: 0 auto 40px;
}
.d-back {
color: #d4b46a;
text-decoration: none;
font-size: 12px;
letter-spacing: 0.3em;
border: 1px solid rgba(212, 180, 106, 0.45);
padding: 8px 18px;
transition: all 0.25s ease;
}
.d-back:hover {
background: rgba(212, 180, 106, 0.14);
letter-spacing: 0.38em;
}
.d-nav-line {
flex: 1;
height: 1px;
background: linear-gradient(90deg, rgba(212, 180, 106, 0.5), transparent);
}
.d-nav-est {
font-size: 11px;
letter-spacing: 0.32em;
color: rgba(212, 180, 106, 0.6);
}
.d-hero {
position: relative;
max-width: 980px;
margin: 0 auto;
text-align: center;
padding: 84px 12px 46px;
border: 1px solid rgba(212, 180, 106, 0.4);
outline: 1px solid rgba(212, 180, 106, 0.16);
outline-offset: 6px;
overflow: hidden;
}
.d-sunburst {
position: absolute;
left: 50%;
top: -12px;
transform: translateX(-50%);
width: 0;
height: 0;
}
.d-sunburst i {
position: absolute;
left: -1.5px;
top: 0;
width: 3px;
height: 74px;
transform-origin: 50% 0;
background: linear-gradient(rgba(212, 180, 106, 0.85), transparent);
animation: d-ray 3.6s ease-in-out infinite alternate;
}
@keyframes d-ray {
from { height: 60px; opacity: 0.65; }
to { height: 84px; opacity: 1; }
}
.d-over {
margin: 0 0 16px;
font-size: 12px;
letter-spacing: 0.52em;
color: rgba(232, 220, 192, 0.66);
}
.d-name {
margin: 0;
font-size: clamp(34px, 6.4vw, 68px);
font-weight: 700;
letter-spacing: 0.08em;
background: linear-gradient(178deg, #f4e6bd 20%, #d4b46a 58%, #9c7c3c);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-transform: uppercase;
}
.d-name-zh {
margin: 8px 0 0;
font-size: 17px;
letter-spacing: 0.62em;
text-indent: 0.62em;
color: #d4b46a;
}
.d-divider {
display: flex;
align-items: center;
gap: 14px;
max-width: 380px;
margin: 26px auto;
}
.d-divider i {
flex: 1;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(212, 180, 106, 0.75));
}
.d-divider i:last-child {
background: linear-gradient(90deg, rgba(212, 180, 106, 0.75), transparent);
}
.d-divider b {
color: #d4b46a;
font-size: 13px;
}
.d-role {
margin: 0 0 18px;
font-size: 12.5px;
letter-spacing: 0.34em;
color: rgba(232, 220, 192, 0.72);
}
.d-bio {
max-width: 560px;
margin: 0 auto;
font-size: 14.5px;
line-height: 2;
color: rgba(232, 220, 192, 0.85);
}
.d-section {
max-width: 980px;
margin: 64px auto 0;
}
.d-title {
display: flex;
align-items: baseline;
gap: 16px;
font-size: 21px;
font-weight: 700;
letter-spacing: 0.3em;
color: #f0e3bd;
margin: 0 0 28px;
}
.d-title span {
font-size: 14px;
color: #d4b46a;
border: 1px solid rgba(212, 180, 106, 0.55);
width: 30px;
height: 30px;
display: grid;
place-items: center;
transform: rotate(45deg);
}
.d-title span {
line-height: 1;
}
.d-title em {
font-style: normal;
font-size: 13px;
letter-spacing: 0.5em;
color: rgba(232, 220, 192, 0.5);
}
.d-skills {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px 34px;
}
.d-skill {
display: flex;
gap: 14px;
align-items: center;
}
.d-skill-deco {
color: #d4b46a;
font-size: 15px;
}
.d-skill-main {
flex: 1;
}
.d-skill-head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 7px;
}
.d-skill-head b {
font-size: 14.5px;
font-weight: 600;
letter-spacing: 0.06em;
}
.d-skill-head span {
font-size: 12px;
color: #d4b46a;
letter-spacing: 0.14em;
}
.d-meter {
height: 7px;
border: 1px solid rgba(212, 180, 106, 0.45);
padding: 1.5px;
}
.d-meter i {
display: block;
height: 100%;
background: repeating-linear-gradient(105deg, #d4b46a 0 7px, #8f7136 7px 9px);
animation: d-fill 1.1s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
@keyframes d-fill {
from { width: 0 !important; }
}
.d-works {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
gap: 22px;
}
.d-work {
position: relative;
border: 1px solid rgba(212, 180, 106, 0.4);
padding: 22px 22px 18px;
background: linear-gradient(170deg, rgba(212, 180, 106, 0.07), transparent 40%);
transition: transform 0.25s ease, border-color 0.25s ease;
}
.d-work:hover {
transform: translateY(-4px);
border-color: rgba(212, 180, 106, 0.85);
}
.d-work-top {
display: flex;
justify-content: space-between;
font-size: 11px;
letter-spacing: 0.22em;
color: rgba(212, 180, 106, 0.85);
margin-bottom: 14px;
}
.d-work h3 {
margin: 0;
font-size: 21px;
letter-spacing: 0.05em;
color: #f0e3bd;
}
.d-work-alias {
margin: 6px 0 12px;
font-size: 11.5px;
letter-spacing: 0.3em;
color: rgba(232, 220, 192, 0.55);
}
.d-work-desc {
margin: 0 0 16px;
font-size: 13px;
line-height: 1.9;
color: rgba(232, 220, 192, 0.8);
}
.d-work-tags {
font-size: 11px;
letter-spacing: 0.18em;
color: #d4b46a;
border-top: 1px solid rgba(212, 180, 106, 0.28);
padding-top: 12px;
}
.d-corner {
position: absolute;
width: 10px;
height: 10px;
border: 1.5px solid #d4b46a;
}
.d-corner.tl { left: 4px; top: 4px; border-right: none; border-bottom: none; }
.d-corner.tr { right: 4px; top: 4px; border-left: none; border-bottom: none; }
.d-corner.bl { left: 4px; bottom: 4px; border-right: none; border-top: none; }
.d-corner.br { right: 4px; bottom: 4px; border-left: none; border-top: none; }
.d-footer {
max-width: 980px;
margin: 70px auto 0;
text-align: center;
}
.d-contact {
margin: 6px 0;
font-size: 13px;
letter-spacing: 0.12em;
color: rgba(232, 220, 192, 0.8);
}
.d-contact span {
display: inline-block;
min-width: 88px;
color: #d4b46a;
letter-spacing: 0.3em;
font-size: 11px;
text-transform: uppercase;
}
.d-fin {
margin: 26px 0 0;
font-size: 13.5px;
font-style: italic;
color: rgba(232, 220, 192, 0.6);
}
@media (max-width: 640px) {
.d-hero { padding-top: 70px; }
.d-title { flex-wrap: wrap; }
}
</style>

View File

@@ -0,0 +1,594 @@
<script setup>
import { ref } from 'vue'
import { profile } from '../../data/profile'
/* 观测手记 */
const obs = ref(null)
const obsKind = ref('彗星过境')
function openObs(p, i) {
obs.value = p
obsKind.value = ['彗星过境', '超新星爆发', '流星雨', '行星连珠'][i % 4]
}
// 技能连成星座:圆盘上按等级分布
function starPos(i, total) {
const angle = (i / total) * Math.PI * 2 - Math.PI / 2
const r = 34 + (i % 3) * 13
return {
left: 50 + Math.cos(angle) * r + '%',
top: 50 + Math.sin(angle) * r + '%',
}
}
const zodiac = ['♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓']
</script>
<template>
<div class="ast">
<nav class="as-nav">
<router-link to="/" class="as-back"> 收起星盘</router-link>
<router-link to="/style/astrolabe/connect" class="as-back"> 连星成座</router-link>
<span class="as-obs">年糕天文台 · 观测编号 No.25</span>
</nav>
<header class="as-hero">
<p class="as-over">天球星图 · 观测对象</p>
<h1>{{ profile.name }}</h1>
<p class="as-sub">{{ profile.job }} · {{ profile.age }}° · 升起于{{ profile.location }}</p>
</header>
<!-- 星盘 -->
<div class="as-chart">
<i class="as-ring r1"></i>
<i class="as-ring r2"></i>
<i class="as-ring r3"></i>
<!-- 黄道十二宫 -->
<span
v-for="(z, i) in zodiac"
:key="z"
class="as-zodiac"
:style="{ transform: `rotate(${i * 30}deg) translateY(-158px) rotate(${-i * 30}deg)` }"
>{{ z }}</span>
<!-- 技能星 -->
<div
v-for="(sk, i) in profile.skills"
:key="sk.name"
class="as-star"
:style="starPos(i, profile.skills.length)"
>
<i :style="{ '--m': sk.level / 100 }"></i>
<span class="as-star-tip">
<b>{{ sk.name }}</b>
<em>星等 {{ (6 - sk.level / 20).toFixed(1) }} · 亮度 {{ sk.level }}</em>
</span>
</div>
<!-- 中心 -->
<div class="as-core"></div>
<svg class="as-lines" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
<polygon
points="50,9.6 85,29.4 92,50 74,84.6 26,84.6 15.7,29.4"
fill="none"
stroke="rgba(212,183,106,.4)"
stroke-width="0.35"
stroke-dasharray="1.4 1"
/>
</svg>
</div>
<p class="as-hint">悬停星点可读出技能星等 · 连线即年糕座主星群</p>
<p class="as-bio">{{ profile.bio }}</p>
<!-- 作品:天象记录 -->
<section class="as-section">
<h2 class="as-title">重要天象记录</h2>
<div class="as-events">
<article
v-for="(p, i) in profile.projects"
:key="p.name"
class="as-event click"
role="button"
tabindex="0"
title="翻开观测手记"
@click="openObs(p, i)"
@keyup.enter="openObs(p, i)"
>
<header>
<span class="as-event-kind">{{ ['彗星过境', '超新星爆发', '流星雨', '行星连珠'][i % 4] }}</span>
<span class="as-event-year">{{ p.year }} A.D.</span>
</header>
<h3>{{ p.name }}<small>{{ p.alias }}</small></h3>
<p>{{ p.desc }}</p>
<footer>观测亮度 {{ p.stars }} · 光谱型 {{ p.tags.join('-') }}</footer>
</article>
</div>
</section>
<footer class="as-footer">
<p class="as-obs-note">观测站通讯:</p>
<p v-for="c in profile.contacts" :key="c.label"><b>{{ c.label }}</b>{{ c.value }}</p>
<p class="as-fin">{{ profile.motto }} 用蚀刻针刻于铜盘背面</p>
</footer>
<!-- 观测手记 -->
<Transition name="as-obs">
<div v-if="obs" class="as-obs-mask" @click.self="obs = null">
<div class="as-obs">
<i class="as-obs-ring" aria-hidden="true"></i>
<button class="as-obs-x" title="收起手记" @click="obs = null"></button>
<p class="as-obs-kind">{{ obsKind }} · {{ obs.year }} A.D.</p>
<h3 class="as-obs-title">{{ obs.name }}<small>{{ obs.alias }}</small></h3>
<p class="as-obs-story">观测记录:{{ obs.story }}</p>
<p class="as-obs-h"> 光谱分析</p>
<p v-for="(h, hi) in obs.highlights" :key="hi" class="as-obs-li"> {{ h }}</p>
<p class="as-obs-h"> 星历</p>
<p v-for="t in obs.timeline" :key="t.at" class="as-obs-li eph"><b>{{ t.at }}</b>{{ t.event }}</p>
<div class="as-obs-metrics">
<span v-for="m in obs.metrics" :key="m.label"><b>{{ m.value }}</b>{{ m.label }}</span>
<span><b>{{ obs.stars }}</b>视星等</span>
</div>
<p class="as-obs-foot">仪器:{{ obs.tags.join(' · ') }} · 黄铜圆盘校准无误</p>
</div>
</div>
</Transition>
</div>
</template>
<style scoped>
/* ---- 观测手记 ---- */
.as-event.click {
cursor: pointer;
}
.as-obs-mask {
position: fixed;
inset: 0;
z-index: 500;
background: rgba(4, 8, 20, 0.82);
display: flex;
align-items: center;
justify-content: center;
padding: 22px;
}
.as-obs {
position: relative;
width: min(460px, 100%);
max-height: 86vh;
overflow-y: auto;
background:
radial-gradient(circle at 80% 12%, rgba(200, 170, 110, 0.12), transparent 40%),
linear-gradient(175deg, #101a30, #0a1222);
border: 1.5px solid #c8aa6e;
border-radius: 12px;
padding: 28px 30px;
color: #d8e2f5;
box-shadow: 0 0 46px rgba(200, 170, 110, 0.16);
scrollbar-width: thin;
scrollbar-color: #c8aa6e transparent;
}
.as-obs-ring {
position: absolute;
top: -40px;
right: -40px;
width: 120px;
height: 120px;
border: 1.5px dashed rgba(200, 170, 110, 0.4);
border-radius: 50%;
animation: as-rotate 22s linear infinite;
pointer-events: none;
}
@keyframes as-rotate {
to {
transform: rotate(360deg);
}
}
.as-obs-x {
position: absolute;
top: 12px;
right: 12px;
width: 27px;
height: 27px;
border-radius: 50%;
border: 1px solid rgba(200, 170, 110, 0.6);
background: none;
color: #c8aa6e;
cursor: pointer;
font-size: 11px;
}
.as-obs-kind {
margin: 0 0 8px;
font-size: 10.5px;
letter-spacing: 0.4em;
color: #c8aa6e;
}
.as-obs-title {
margin: 0 0 12px;
font-size: 24px;
font-family: 'Noto Serif SC', serif;
color: #f0e6d2;
}
.as-obs-title small {
font-size: 11.5px;
color: #8fa3c8;
margin-left: 8px;
}
.as-obs-story {
margin: 0 0 12px;
font-size: 12.5px;
line-height: 2.05;
color: #c2cfe8;
}
.as-obs-h {
margin: 12px 0 6px;
font-size: 11px;
letter-spacing: 0.4em;
color: #c8aa6e;
}
.as-obs-li {
margin: 0 0 6px;
font-size: 12px;
line-height: 1.95;
color: #b0c0dd;
}
.as-obs-li.eph b {
color: #8fd0ff;
margin-right: 8px;
font-variant-numeric: tabular-nums;
}
.as-obs-metrics {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin: 12px 0;
}
.as-obs-metrics span {
border: 1px solid rgba(200, 170, 110, 0.45);
border-radius: 50%;
width: 74px;
height: 74px;
font-size: 9px;
color: #8fa3c8;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
text-align: center;
}
.as-obs-metrics b {
font-size: 13.5px;
color: #f0e6d2;
}
.as-obs-foot {
margin: 0;
font-size: 10.5px;
color: #6e7fa0;
}
.as-obs-enter-active,
.as-obs-leave-active {
transition: opacity 0.28s ease;
}
.as-obs-enter-active .as-obs {
transition: transform 0.36s cubic-bezier(0.2, 1, 0.35, 1);
}
.as-obs-enter-from,
.as-obs-leave-to {
opacity: 0;
}
.as-obs-enter-from .as-obs {
transform: rotate(-3deg) scale(0.88);
}
.ast {
min-height: 100vh;
background:
radial-gradient(1.4px 1.4px at 12% 18%, rgba(255, 255, 255, 0.7), transparent),
radial-gradient(1px 1px at 78% 8%, rgba(255, 255, 255, 0.5), transparent),
radial-gradient(1.2px 1.2px at 90% 56%, rgba(212, 183, 106, 0.7), transparent),
radial-gradient(1px 1px at 26% 74%, rgba(255, 255, 255, 0.45), transparent),
radial-gradient(1.4px 1.4px at 58% 88%, rgba(212, 183, 106, 0.5), transparent),
radial-gradient(ellipse at 50% 0%, #14203d 0%, #0b1226 55%, #070c1a 100%);
color: #e6e9f4;
font-family: 'Noto Serif SC', serif;
padding: 26px clamp(16px, 5vw, 72px) 60px;
overflow-x: hidden;
}
.as-nav {
position: sticky;
top: 10px;
z-index: 60;
background: rgba(8, 8, 14, 0.62);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 12px;
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
max-width: 900px;
margin: 0 auto 40px;
flex-wrap: wrap;
gap: 10px;
}
.as-back {
color: #d4b76a;
text-decoration: none;
font-size: 13px;
letter-spacing: 0.18em;
border: 1px solid rgba(212, 183, 106, 0.45);
border-radius: 999px;
padding: 8px 18px;
transition: all 0.25s ease;
}
.as-back:hover {
background: rgba(212, 183, 106, 0.12);
box-shadow: 0 0 18px rgba(212, 183, 106, 0.25);
}
.as-obs {
font-size: 11px;
letter-spacing: 0.3em;
color: rgba(230, 233, 244, 0.5);
}
.as-hero {
text-align: center;
margin-bottom: 34px;
}
.as-over {
margin: 0 0 10px;
font-size: 11px;
letter-spacing: 0.5em;
color: rgba(212, 183, 106, 0.75);
}
.as-hero h1 {
margin: 0 0 8px;
font-size: clamp(34px, 5.4vw, 52px);
font-weight: 900;
letter-spacing: 0.2em;
background: linear-gradient(178deg, #f2ebd8, #d4b76a 75%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.as-sub {
margin: 0;
font-size: 13px;
letter-spacing: 0.22em;
color: rgba(230, 233, 244, 0.66);
}
/* 星盘 */
.as-chart {
position: relative;
width: min(380px, 88vw);
aspect-ratio: 1;
margin: 0 auto;
}
.as-ring {
position: absolute;
border-radius: 50%;
border: 1px solid rgba(212, 183, 106, 0.4);
}
.as-ring.r1 {
inset: 0;
border-width: 1.5px;
background:
repeating-conic-gradient(rgba(212, 183, 106, 0.14) 0 0.4deg, transparent 0.4deg 30deg);
animation: as-spin 90s linear infinite;
}
.as-ring.r2 {
inset: 13%;
border-style: dashed;
animation: as-spin 60s linear infinite reverse;
}
.as-ring.r3 {
inset: 27%;
opacity: 0.6;
}
@keyframes as-spin {
to { transform: rotate(1turn); }
}
.as-zodiac {
position: absolute;
left: 50%;
top: 50%;
margin: -11px 0 0 -11px;
width: 22px;
height: 22px;
display: grid;
place-items: center;
font-size: 14px;
color: rgba(212, 183, 106, 0.85);
}
@media (max-width: 460px) {
.as-zodiac { display: none; }
}
.as-lines {
position: absolute;
inset: 12%;
width: 76%;
height: 76%;
pointer-events: none;
}
.as-star {
position: absolute;
transform: translate(-50%, -50%);
z-index: 3;
}
.as-star > i {
font-style: normal;
font-size: calc(14px + var(--m) * 14px);
color: #f2ebd8;
text-shadow: 0 0 calc(8px + var(--m) * 18px) rgba(212, 183, 106, 0.95);
cursor: default;
animation: as-twinkle 2.6s ease-in-out infinite;
display: inline-block;
}
.as-star:nth-child(odd) > i {
animation-delay: 1.2s;
}
@keyframes as-twinkle {
50% { opacity: 0.62; }
}
.as-star-tip {
position: absolute;
left: 50%;
bottom: calc(100% + 8px);
transform: translateX(-50%);
white-space: nowrap;
background: rgba(11, 18, 38, 0.94);
border: 1px solid rgba(212, 183, 106, 0.55);
border-radius: 8px;
padding: 7px 12px;
text-align: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 9;
}
.as-star:hover .as-star-tip {
opacity: 1;
transform: translateX(-50%) translateY(-3px);
}
.as-star-tip b {
display: block;
font-size: 12.5px;
color: #f2ebd8;
}
.as-star-tip em {
font-style: normal;
font-size: 10.5px;
color: rgba(212, 183, 106, 0.9);
font-family: 'Space Grotesk', sans-serif;
}
.as-core {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 52px;
height: 52px;
display: grid;
place-items: center;
border-radius: 50%;
background: radial-gradient(circle at 36% 30%, #f2ebd8, #d4b76a 65%);
color: #14203d;
font-family: 'Ma Shan Zheng', cursive;
font-size: 26px;
box-shadow: 0 0 44px rgba(212, 183, 106, 0.55);
z-index: 4;
}
.as-hint {
text-align: center;
margin: 20px 0 0;
font-size: 11.5px;
letter-spacing: 0.16em;
color: rgba(230, 233, 244, 0.45);
}
.as-bio {
max-width: 560px;
margin: 30px auto 0;
text-align: center;
font-size: 14px;
line-height: 2.05;
color: rgba(230, 233, 244, 0.85);
}
.as-section {
max-width: 900px;
margin: 60px auto 0;
}
.as-title {
text-align: center;
margin: 0 0 28px;
font-size: 20px;
font-weight: 700;
letter-spacing: 0.3em;
color: #d4b76a;
}
.as-events {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
gap: 20px;
}
.as-event {
border: 1px solid rgba(212, 183, 106, 0.35);
border-radius: 4px;
background: rgba(20, 32, 61, 0.5);
padding: 18px 20px 15px;
transition: border-color 0.25s ease, box-shadow 0.25s ease, transform 0.25s ease;
}
.as-event:hover {
border-color: rgba(212, 183, 106, 0.8);
box-shadow: 0 0 30px rgba(212, 183, 106, 0.12);
transform: translateY(-4px);
}
.as-event header {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-size: 11px;
letter-spacing: 0.18em;
}
.as-event-kind {
color: #d4b76a;
}
.as-event-year {
color: rgba(230, 233, 244, 0.5);
font-family: 'Space Grotesk', sans-serif;
}
.as-event h3 {
margin: 0 0 8px;
font-size: 19px;
font-weight: 900;
color: #f2ebd8;
}
.as-event h3 small {
margin-left: 8px;
font-size: 11.5px;
font-weight: 400;
color: rgba(230, 233, 244, 0.5);
}
.as-event p {
margin: 0 0 12px;
font-size: 13px;
line-height: 1.9;
color: rgba(230, 233, 244, 0.85);
}
.as-event footer {
font-size: 11px;
letter-spacing: 0.1em;
color: rgba(212, 183, 106, 0.85);
border-top: 1px dashed rgba(212, 183, 106, 0.3);
padding-top: 10px;
}
.as-footer {
max-width: 900px;
margin: 56px auto 0;
text-align: center;
}
.as-obs-note {
font-size: 12px;
letter-spacing: 0.3em;
color: rgba(230, 233, 244, 0.5);
margin: 0 0 12px;
}
.as-footer p {
margin: 5px 0;
font-size: 13.5px;
}
.as-footer b {
display: inline-block;
min-width: 80px;
color: #d4b76a;
font-size: 11px;
letter-spacing: 0.24em;
text-transform: uppercase;
font-family: 'Space Grotesk', sans-serif;
}
.as-fin {
margin-top: 24px !important;
font-size: 12px !important;
color: rgba(230, 233, 244, 0.5);
}
</style>

View File

@@ -0,0 +1,266 @@
<script setup>
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { profile } from '../../../data/profile'
const now = ref(new Date())
let clock = null
/* 便签 */
const NOTE_KEY = 'aero-gadget-note'
const note = ref('')
function saveNote() {
localStorage.setItem(NOTE_KEY, note.value)
}
/* 天气(本地随机模拟) */
const weathers = [
{ icon: '☀️', name: '晴', t: 31 },
{ icon: '🌤️', name: '多云', t: 28 },
{ icon: '🌧️', name: '小雨', t: 24 },
{ icon: '⛈️', name: '雷阵雨', t: 26 },
]
const weather = ref(weathers[0])
/* CPU 仪表(模拟) */
const cpu = ref(32)
const ram = ref(58)
let meter = null
/* 幻灯片 */
const slides = ['🌉', '🏔️', '🌊', '🌸', '🌇']
const slideIdx = ref(0)
let slideTimer = null
onMounted(() => {
note.value = localStorage.getItem(NOTE_KEY) || '双击便签开始记录…\n(内容会自动保存)'
weather.value = weathers[Math.floor(Math.random() * weathers.length)]
clock = setInterval(() => (now.value = new Date()), 1000)
meter = setInterval(() => {
cpu.value = Math.max(8, Math.min(96, cpu.value + Math.round(Math.random() * 18 - 9)))
ram.value = Math.max(30, Math.min(90, ram.value + Math.round(Math.random() * 8 - 4)))
}, 1400)
slideTimer = setInterval(() => (slideIdx.value = (slideIdx.value + 1) % slides.length), 3000)
})
onBeforeUnmount(() => {
clearInterval(clock)
clearInterval(meter)
clearInterval(slideTimer)
})
const hh = () => String(now.value.getHours()).padStart(2, '0')
const mm = () => String(now.value.getMinutes()).padStart(2, '0')
const ss = () => String(now.value.getSeconds()).padStart(2, '0')
</script>
<template>
<div class="ag">
<nav class="ag-nav">
<router-link to="/style/aero" class="ag-back"> 返回 Aero 桌面</router-link>
<h1>侧边栏小工具</h1>
</nav>
<div class="ag-desktop">
<p class="ag-wallhint">Aero 桌面 · 小工具全部可用:便签能写字时钟在走仪表在跳</p>
<aside class="ag-sidebar">
<!-- 时钟 -->
<section class="ag-gadget">
<p class="ag-g-title">🕐 时钟</p>
<p class="ag-clock">{{ hh() }}:{{ mm() }}<small>:{{ ss() }}</small></p>
<p class="ag-g-sub">{{ now.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'long' }) }}</p>
</section>
<!-- 天气 -->
<section class="ag-gadget">
<p class="ag-g-title">🌈 天气 · {{ profile.location.split(' · ')[1] }}</p>
<div class="ag-weather">
<span class="ag-w-icon">{{ weather.icon }}</span>
<div>
<b>{{ weather.t }}°C</b>
<span>{{ weather.name }} · 适合写码</span>
</div>
</div>
</section>
<!-- 便签 -->
<section class="ag-gadget note">
<p class="ag-g-title">📝 便签(自动保存)</p>
<textarea v-model="note" rows="4" @input="saveNote"></textarea>
</section>
<!-- 仪表 -->
<section class="ag-gadget">
<p class="ag-g-title">📊 系统仪表</p>
<div class="ag-meter">
<span>CPU</span>
<div class="ag-bar"><i :style="{ width: cpu + '%' }" :class="{ hot: cpu > 80 }"></i></div>
<b>{{ cpu }}%</b>
</div>
<div class="ag-meter">
<span>内存</span>
<div class="ag-bar"><i :style="{ width: ram + '%' }" :class="{ hot: ram > 80 }"></i></div>
<b>{{ ram }}%</b>
</div>
</section>
<!-- 幻灯片 -->
<section class="ag-gadget">
<p class="ag-g-title">🖼 幻灯片</p>
<div class="ag-slide">
<transition name="ag-fade" mode="out-in">
<span :key="slideIdx" class="ag-slide-pic">{{ slides[slideIdx] }}</span>
</transition>
</div>
<div class="ag-dots">
<i v-for="(s, i) in slides" :key="i" :class="{ on: slideIdx === i }" @click="slideIdx = i"></i>
</div>
</section>
</aside>
</div>
</div>
</template>
<style scoped>
.ag {
min-height: 100vh;
background:
radial-gradient(ellipse 80% 50% at 20% 0%, rgba(120, 200, 255, 0.35), transparent 60%),
linear-gradient(160deg, #3a7ec4 0%, #2c5a94 45%, #1c3a64 100%);
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
color: #fff;
padding: 22px clamp(14px, 4vw, 48px) 40px;
}
.ag-nav {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 18px;
flex-wrap: wrap;
}
.ag-back {
color: #fff;
text-decoration: none;
font-size: 13px;
background: rgba(255, 255, 255, 0.18);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 8px;
padding: 7px 16px;
}
.ag-back:hover { background: rgba(255, 255, 255, 0.3); }
.ag-nav h1 { margin: 0; font-size: 20px; font-weight: 400; text-shadow: 0 2px 8px rgba(0, 30, 60, 0.5); }
.ag-desktop {
display: flex;
justify-content: space-between;
gap: 20px;
min-height: 70vh;
}
.ag-wallhint {
align-self: center;
max-width: 380px;
font-size: 14px;
line-height: 2;
color: rgba(255, 255, 255, 0.75);
text-shadow: 0 2px 6px rgba(0, 30, 60, 0.6);
}
@media (max-width: 720px) {
.ag-desktop { flex-direction: column; }
.ag-wallhint { max-width: none; }
}
.ag-sidebar {
width: 300px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
@media (max-width: 720px) { .ag-sidebar { width: 100%; } }
.ag-gadget {
background: linear-gradient(160deg, rgba(255, 255, 255, 0.32), rgba(255, 255, 255, 0.12));
backdrop-filter: blur(14px);
border: 1px solid rgba(255, 255, 255, 0.45);
border-radius: 12px;
padding: 13px 16px;
box-shadow: 0 10px 30px rgba(0, 30, 60, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.6);
}
.ag-g-title { margin: 0 0 8px; font-size: 12px; color: rgba(255, 255, 255, 0.85); }
.ag-clock {
margin: 0;
font-size: 38px;
font-weight: 300;
text-shadow: 0 2px 10px rgba(0, 30, 60, 0.5);
font-variant-numeric: tabular-nums;
}
.ag-clock small { font-size: 18px; opacity: 0.7; }
.ag-g-sub { margin: 2px 0 0; font-size: 12px; opacity: 0.75; }
.ag-weather { display: flex; align-items: center; gap: 14px; }
.ag-w-icon { font-size: 42px; filter: drop-shadow(0 4px 10px rgba(0, 30, 60, 0.4)); }
.ag-weather b { display: block; font-size: 24px; font-weight: 400; }
.ag-weather span { font-size: 12px; opacity: 0.75; }
.ag-gadget.note textarea {
width: 100%;
box-sizing: border-box;
background: rgba(255, 250, 200, 0.92);
border: none;
border-radius: 6px;
padding: 10px 12px;
font-family: inherit;
font-size: 13px;
color: #5a4c20;
resize: vertical;
outline: none;
line-height: 1.7;
}
.ag-meter {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
font-size: 12px;
}
.ag-meter span { width: 36px; }
.ag-bar {
flex: 1;
height: 9px;
border-radius: 5px;
background: rgba(0, 30, 60, 0.35);
overflow: hidden;
}
.ag-bar i {
display: block;
height: 100%;
border-radius: 5px;
background: linear-gradient(90deg, #7ee787, #4cc2ff);
transition: width 0.6s ease;
}
.ag-bar i.hot { background: linear-gradient(90deg, #ffb84c, #ff6a4c); }
.ag-meter b { min-width: 36px; text-align: right; font-size: 11.5px; }
.ag-slide {
height: 110px;
border-radius: 8px;
background: rgba(0, 30, 60, 0.3);
display: grid;
place-items: center;
margin-bottom: 8px;
overflow: hidden;
}
.ag-slide-pic { font-size: 60px; filter: drop-shadow(0 6px 14px rgba(0, 30, 60, 0.5)); }
.ag-fade-enter-active, .ag-fade-leave-active { transition: all 0.4s ease; }
.ag-fade-enter-from { opacity: 0; transform: translateX(30px); }
.ag-fade-leave-to { opacity: 0; transform: translateX(-30px); }
.ag-dots { display: flex; justify-content: center; gap: 6px; }
.ag-dots i {
width: 7px;
height: 7px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
cursor: pointer;
}
.ag-dots i.on { background: #fff; }
</style>

View File

@@ -0,0 +1,248 @@
<script setup>
import { computed, ref } from 'vue'
import { profile } from '../../../data/profile'
const wifi = ref(true)
const bt = ref(false)
const dnd = ref(false)
const torch = ref(false)
const dark = ref(true)
const brightness = ref(72)
const notifications = ref([
{ id: 1, app: '糯米终端', icon: '🖥️', time: '刚刚', title: '构建完成', body: 'niangao-theme v2.1 构建成功,用时 6.2s,全绿 ✅' },
{ id: 2, app: '饭粒清单', icon: '✅', time: '8 分钟前', title: '今日待办 3/5', body: '「给杰伦钢琴房加延音」已完成,还剩 2 件小事' },
{ id: 3, app: 'Git 信使', icon: '📦', time: '32 分钟前', title: 'CodeMochi 有新 Star', body: '⭐ 总数来到 2,301,有人 fork 了你的倔强' },
{ id: 4, app: '像素厨房', icon: '🍳', time: '1 小时前', title: '每日挑战刷新', body: '今日菜谱:三分熟的正则表达式' },
])
function dismiss(n) {
notifications.value = notifications.value.filter((x) => x.id !== n.id)
}
function clearAll() {
notifications.value = []
}
const bgStyle = computed(() => ({
filter: `brightness(${0.55 + (brightness.value / 100) * 0.55})`,
}))
</script>
<template>
<div class="ap" :class="{ light: !dark }">
<div class="ap-phone" :style="bgStyle">
<!-- 状态栏 -->
<header class="ap-status">
<span>{{ new Date().toTimeString().slice(0, 5) }}</span>
<span class="ap-status-r">{{ wifi ? '📶' : '' }}{{ dnd ? '🌙' : '' }} 🔋86%</span>
</header>
<!-- 快捷开关 -->
<section class="ap-quick">
<button :class="{ on: wifi }" @click="wifi = !wifi"><i>📶</i>WLAN</button>
<button :class="{ on: bt }" @click="bt = !bt"><i>🅱</i>蓝牙</button>
<button :class="{ on: dnd }" @click="dnd = !dnd"><i>🌙</i>勿扰</button>
<button :class="{ on: torch }" @click="torch = !torch"><i>🔦</i>手电筒</button>
<button :class="{ on: dark }" @click="dark = !dark"><i>🌗</i>深色模式</button>
<router-link to="/style/android-home" class="ap-exitq"><i></i>回桌面</router-link>
</section>
<!-- 亮度 -->
<div class="ap-bright">
<span></span>
<input v-model.number="brightness" type="range" min="10" max="100" />
<b>{{ brightness }}%</b>
</div>
<!-- 通知 -->
<section class="ap-notis">
<header class="ap-notis-head">
<span>通知中心</span>
<button v-if="notifications.length" @click="clearAll">全部清除</button>
</header>
<TransitionGroup name="ap-slide">
<article v-for="n in notifications" :key="n.id" class="ap-noti">
<span class="ap-noti-icon">{{ n.icon }}</span>
<div class="ap-noti-body">
<p class="ap-noti-meta">{{ n.app }} · {{ n.time }}</p>
<p class="ap-noti-title">{{ n.title }}</p>
<p class="ap-noti-text">{{ n.body }}</p>
</div>
<button class="ap-noti-x" @click="dismiss(n)"></button>
</article>
</TransitionGroup>
<p v-if="!notifications.length" class="ap-empty">🔕 没有新通知,清净</p>
</section>
<!-- 小部件 -->
<section class="ap-widgets">
<div class="ap-widget clock">
<b>{{ new Date().toTimeString().slice(0, 5) }}</b>
<span>{{ profile.location }}</span>
</div>
<div class="ap-widget me">
<img :src="profile.avatars.default" alt="" />
<div>
<b>{{ profile.name }}</b>
<span>{{ profile.job }} · 在线</span>
</div>
</div>
</section>
<!-- 手电筒覆盖 -->
<div v-if="torch" class="ap-torch" @click="torch = false">
<p>🔦 手电筒已开启<br /><small>(点击关闭,费电)</small></p>
</div>
</div>
</div>
</template>
<style scoped>
.ap {
min-height: 100vh;
background: #101216;
display: grid;
place-items: center;
padding: 26px 12px;
font-family: 'Roboto', 'HarmonyOS Sans SC', 'Noto Sans SC', sans-serif;
transition: background 0.3s ease;
}
.ap.light { background: #c8d0dc; }
.ap-phone {
width: min(400px, 96vw);
min-height: 760px;
border-radius: 30px;
background: linear-gradient(170deg, #1a2030, #12141c);
border: 3px solid #2a3040;
padding: 14px 14px 24px;
color: #eef2f8;
position: relative;
overflow: hidden;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
transition: filter 0.2s ease, background 0.3s ease;
}
.ap.light .ap-phone { background: linear-gradient(170deg, #eef2fa, #dce4f0); color: #1a2030; border-color: #b8c4d8; }
.ap-status {
display: flex;
justify-content: space-between;
font-size: 12.5px;
padding: 4px 10px 12px;
opacity: 0.85;
}
.ap-quick {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 9px;
margin-bottom: 12px;
}
.ap-quick button,
.ap-exitq {
border: none;
border-radius: 16px;
background: rgba(255, 255, 255, 0.08);
color: inherit;
font-family: inherit;
font-size: 12px;
padding: 12px 6px;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
text-decoration: none;
transition: background 0.2s ease;
}
.ap.light .ap-quick button, .ap.light .ap-exitq { background: rgba(255, 255, 255, 0.65); }
.ap-quick i { font-style: normal; font-size: 19px; }
.ap-quick button.on { background: #3c6df0; color: #fff; }
.ap-bright {
display: flex;
align-items: center;
gap: 10px;
background: rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 10px 14px;
margin-bottom: 14px;
font-size: 13px;
}
.ap.light .ap-bright { background: rgba(255, 255, 255, 0.65); }
.ap-bright input { flex: 1; accent-color: #3c6df0; }
.ap-bright b { font-size: 11.5px; min-width: 36px; text-align: right; }
.ap-notis-head {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12.5px;
opacity: 0.7;
padding: 0 6px 8px;
}
.ap-notis-head button {
border: none;
background: none;
color: #3c9df0;
font-size: 12px;
cursor: pointer;
}
.ap-noti {
display: flex;
gap: 11px;
background: rgba(255, 255, 255, 0.08);
border-radius: 18px;
padding: 13px 14px;
margin-bottom: 8px;
align-items: flex-start;
}
.ap.light .ap-noti { background: rgba(255, 255, 255, 0.72); }
.ap-noti-icon { font-size: 24px; }
.ap-noti-body { flex: 1; min-width: 0; }
.ap-noti-meta { margin: 0 0 3px; font-size: 10.5px; opacity: 0.55; }
.ap-noti-title { margin: 0 0 3px; font-size: 13.5px; font-weight: 700; }
.ap-noti-text { margin: 0; font-size: 12.5px; line-height: 1.6; opacity: 0.8; }
.ap-noti-x {
border: none;
background: none;
color: inherit;
opacity: 0.4;
cursor: pointer;
font-size: 12px;
}
.ap-noti-x:hover { opacity: 1; }
.ap-empty { text-align: center; font-size: 13px; opacity: 0.5; padding: 18px 0; }
.ap-slide-leave-active { transition: all 0.28s ease; }
.ap-slide-leave-to { opacity: 0; transform: translateX(60px); }
.ap-slide-move { transition: transform 0.28s ease; }
.ap-widgets {
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: 9px;
margin-top: 14px;
}
.ap-widget {
border-radius: 18px;
background: rgba(255, 255, 255, 0.08);
padding: 14px;
}
.ap.light .ap-widget { background: rgba(255, 255, 255, 0.72); }
.ap-widget.clock b { display: block; font-size: 26px; font-weight: 300; }
.ap-widget.clock span { font-size: 11px; opacity: 0.6; }
.ap-widget.me { display: flex; align-items: center; gap: 10px; }
.ap-widget.me img { width: 40px; height: 40px; border-radius: 50%; background: #ffdfbf; }
.ap-widget.me b { display: block; font-size: 13.5px; }
.ap-widget.me span { font-size: 11px; opacity: 0.6; }
.ap-torch {
position: absolute;
inset: 0;
background: rgba(255, 252, 235, 0.96);
display: grid;
place-items: center;
color: #6a5c20;
text-align: center;
font-size: 17px;
cursor: pointer;
z-index: 10;
line-height: 2;
}
</style>

View File

@@ -0,0 +1,272 @@
<script setup>
import { onBeforeUnmount, onMounted, ref } from 'vue'
const cvs = ref(null)
const state = ref('ready') // ready | run | over | win
const score = ref(0)
const lives = ref(3)
const best = ref(0)
const KEY = 'arcade-breakout-best'
const W = 640
const H = 420
let ctx, raf
let paddle, ball, bricks
const COLORS = ['#ff2d55', '#ff9500', '#ffe135', '#4cd964', '#5ac8fa', '#c86bff']
function buildBricks() {
bricks = []
for (let r = 0; r < 6; r++) {
for (let c = 0; c < 10; c++) {
bricks.push({ x: 24 + c * 60, y: 46 + r * 24, w: 52, h: 17, color: COLORS[r], alive: true, pt: (6 - r) * 10 })
}
}
}
function reset(full = true) {
paddle = { x: W / 2, w: 92, h: 12 }
ball = { x: W / 2, y: H - 70, vx: 3.2 * (Math.random() > 0.5 ? 1 : -1), vy: -4 }
if (full) {
buildBricks()
score.value = 0
lives.value = 3
}
}
function start() {
if (state.value === 'run') return
if (state.value !== 'ready') reset(true)
state.value = 'run'
}
function loop() {
raf = requestAnimationFrame(loop)
// 背景
ctx.fillStyle = '#0a0a14'
ctx.fillRect(0, 0, W, H)
ctx.strokeStyle = 'rgba(90,200,250,.12)'
for (let y = 0; y < H; y += 4) {
ctx.beginPath()
ctx.moveTo(0, y)
ctx.lineTo(W, y)
ctx.stroke()
}
if (state.value === 'run') {
ball.x += ball.vx
ball.y += ball.vy
if (ball.x < 8 || ball.x > W - 8) ball.vx *= -1
if (ball.y < 8) ball.vy *= -1
// 板子
if (ball.y > H - 34 && ball.y < H - 18 && Math.abs(ball.x - paddle.x) < paddle.w / 2 + 8 && ball.vy > 0) {
ball.vy = -Math.abs(ball.vy) - 0.06
ball.vx += ((ball.x - paddle.x) / (paddle.w / 2)) * 2.2
ball.vx = Math.max(-6, Math.min(6, ball.vx))
}
// 掉落
if (ball.y > H + 10) {
lives.value--
if (lives.value <= 0) {
state.value = 'over'
saveBest()
} else {
reset(false)
}
}
// 砖块
for (const b of bricks) {
if (!b.alive) continue
if (ball.x > b.x - 8 && ball.x < b.x + b.w + 8 && ball.y > b.y - 8 && ball.y < b.y + b.h + 8) {
b.alive = false
score.value += b.pt
const fromSide = ball.x < b.x || ball.x > b.x + b.w
if (fromSide) ball.vx *= -1
else ball.vy *= -1
break
}
}
if (bricks.every((b) => !b.alive)) {
state.value = 'win'
saveBest()
}
}
// 砖
bricks.forEach((b) => {
if (!b.alive) return
ctx.fillStyle = b.color
ctx.fillRect(b.x, b.y, b.w, b.h)
ctx.fillStyle = 'rgba(255,255,255,.25)'
ctx.fillRect(b.x, b.y, b.w, 4)
})
// 板
ctx.fillStyle = '#5ac8fa'
ctx.fillRect(paddle.x - paddle.w / 2, H - 26, paddle.w, paddle.h)
ctx.fillStyle = '#fff'
ctx.fillRect(paddle.x - paddle.w / 2, H - 26, paddle.w, 3)
// 球
ctx.fillStyle = '#ffe135'
ctx.beginPath()
ctx.arc(ball.x, ball.y, 7, 0, Math.PI * 2)
ctx.fill()
ctx.textAlign = 'center'
if (state.value !== 'run') {
ctx.fillStyle = 'rgba(0,0,0,.55)'
ctx.fillRect(0, 0, W, H)
ctx.fillStyle = state.value === 'win' ? '#4cd964' : '#ffe135'
ctx.font = 'bold 24px "Press Start 2P", monospace'
ctx.fillText(state.value === 'win' ? 'YOU WIN!' : state.value === 'over' ? 'GAME OVER' : 'BREAKOUT', W / 2, 170)
ctx.fillStyle = '#fff'
ctx.font = '14px sans-serif'
if (state.value === 'ready') ctx.fillText('移动鼠标控制挡板,点击发球', W / 2, 210)
else ctx.fillText(`得分 ${score.value} · 点击再来一局`, W / 2, 210)
}
}
function saveBest() {
if (score.value > best.value) {
best.value = score.value
localStorage.setItem(KEY, String(best.value))
}
}
function onMove(e) {
const rect = cvs.value.getBoundingClientRect()
const x = ((e.clientX - rect.left) / rect.width) * W
paddle.x = Math.max(paddle.w / 2, Math.min(W - paddle.w / 2, x))
}
onMounted(() => {
best.value = Number(localStorage.getItem(KEY)) || 0
ctx = cvs.value.getContext('2d')
reset()
loop()
})
onBeforeUnmount(() => cancelAnimationFrame(raf))
</script>
<template>
<div class="ab">
<nav class="ab-nav">
<router-link to="/style/arcade" class="ab-back"> 离开机厅</router-link>
<h1>打砖块 · 隐藏机台</h1>
<span class="ab-best">HI-SCORE {{ best }}</span>
</nav>
<div class="ab-cab">
<div class="ab-marquee"> BRICK BUSTER </div>
<div class="ab-hud">
<span>SCORE <b>{{ score }}</b></span>
<span>LIVES <b>{{ '♥'.repeat(Math.max(0, lives)) || '—' }}</b></span>
</div>
<canvas ref="cvs" width="640" height="420" @pointermove="onMove" @pointerdown="start"></canvas>
<div class="ab-panel">
<i class="ab-stick"></i>
<b class="ab-btn r"></b>
<b class="ab-btn y"></b>
</div>
</div>
<p class="ab-fin">鼠标左右移动挡板,点击开始 · 清光 60 块砖通关</p>
</div>
</template>
<style scoped>
.ab {
min-height: 100vh;
background:
radial-gradient(ellipse 60% 40% at 50% 0%, rgba(90, 200, 250, 0.14), transparent 60%),
#0d0716;
color: #fff;
font-family: 'Press Start 2P', 'Zpix', monospace;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px clamp(12px, 4vw, 48px) 44px;
}
.ab-nav {
display: flex;
align-items: center;
gap: 16px;
width: min(700px, 100%);
margin-bottom: 16px;
flex-wrap: wrap;
}
.ab-back {
color: #5ac8fa;
text-decoration: none;
font-size: 11px;
border: 2px solid #5ac8fa;
padding: 6px 12px;
}
.ab-back:hover { background: #5ac8fa; color: #0d0716; }
.ab-nav h1 { margin: 0; font-size: 14px; }
.ab-best { margin-left: auto; font-size: 10px; color: #ffe135; }
.ab-cab {
width: min(700px, 100%);
background: linear-gradient(#2a1a3e, #180e28);
border: 3px solid #43305e;
border-radius: 16px;
padding: 14px 16px 18px;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
}
.ab-marquee {
text-align: center;
font-size: 13px;
color: #ffe135;
text-shadow: 0 0 12px rgba(255, 225, 53, 0.7);
padding: 8px 0 12px;
animation: ab-flicker 2.4s steps(3, end) infinite;
}
@keyframes ab-flicker {
92% { opacity: 1; }
96% { opacity: 0.5; }
}
.ab-hud {
display: flex;
justify-content: space-between;
font-size: 10px;
color: #9aa0b4;
padding: 0 4px 8px;
}
.ab-hud b { color: #fff; margin-left: 6px; }
.ab-cab canvas {
display: block;
width: 100%;
height: auto;
border: 3px solid #0a0a14;
border-radius: 8px;
cursor: crosshair;
touch-action: none;
}
.ab-panel {
display: flex;
justify-content: center;
align-items: center;
gap: 30px;
padding-top: 16px;
}
.ab-stick {
width: 16px;
height: 16px;
border-radius: 50%;
background: #e04038;
box-shadow: 0 -12px 0 -4px #999, 0 4px 10px rgba(0, 0, 0, 0.5);
}
.ab-btn {
width: 26px;
height: 26px;
border-radius: 50%;
box-shadow: inset 0 -4px 0 rgba(0, 0, 0, 0.4);
}
.ab-btn.r { background: #ff2d55; }
.ab-btn.y { background: #ffe135; }
.ab-fin {
margin: 16px 0 0;
font-size: 9.5px;
color: #9aa0b4;
line-height: 1.8;
text-align: center;
}
</style>

View File

@@ -0,0 +1,170 @@
<script setup>
import { ref } from 'vue'
const ACTS = [
{ time: '20:00', name: '开场 · 香槟塔金字塔', desc: '侍者以完美对称的阵型入场,倒香槟的弧线经过精确计算。', icon: '🥂' },
{ time: '20:30', name: '爵士乐队 · 黑金四重奏', desc: '萨克斯的每个音符都镀了金边,鼓点像打字机一样规整。', icon: '🎷' },
{ time: '21:15', name: '歌舞 · 孔雀扇之舞', desc: '十二把折扇同时展开,构成一幅完美的装饰派扇形图案。', icon: '💃' },
{ time: '22:00', name: '魔术 · 消失的边框', desc: '魔术师让一整面金色边框凭空消失,设计师们掌声最响。', icon: '🎩' },
{ time: '23:00', name: '压轴 · 年糕大乐队', desc: '据说主唱是位程序员,歌单里全是原创,谢幕曲叫《构建通过》。', icon: '🎤' },
]
const booked = ref(new Set())
function toggle(i) {
const s = new Set(booked.value)
s.has(i) ? s.delete(i) : s.add(i)
booked.value = s
}
</script>
<template>
<div class="ac">
<nav class="ac-nav">
<router-link to="/style/art-deco" class="ac-back"> 回大堂</router-link>
<h1>金孔雀歌舞厅 · 今夜节目单</h1>
<span class="ac-booked">已预定 {{ booked.size }} </span>
</nav>
<main class="ac-bill">
<header class="ac-bill-head">
<div class="ac-fan"><i v-for="n in 11" :key="n" :style="{ transform: `rotate(${(n - 6) * 9}deg)` }"></i></div>
<p class="ac-est">歌舞升平 · 夜夜笙歌</p>
<h2>PROGRAMME</h2>
<p class="ac-date">壹玖贰玖年 · 每晚八点</p>
</header>
<div class="ac-divider"><i></i><b></b><i></i></div>
<article
v-for="(a, i) in ACTS"
:key="a.name"
class="ac-act"
:class="{ on: booked.has(i) }"
@click="toggle(i)"
>
<span class="ac-time">{{ a.time }}</span>
<div class="ac-act-info">
<h3><i>{{ a.icon }}</i>{{ a.name }}</h3>
<p>{{ a.desc }}</p>
</div>
<span class="ac-book">{{ booked.has(i) ? '✓ 已订座' : '订座' }}</span>
</article>
<footer class="ac-foot">
<p>着装要求:金线,或至少心里有金线</p>
<p class="ac-note">点击节目即可订座 · 座位无限,气派有限</p>
</footer>
</main>
</div>
</template>
<style scoped>
.ac {
min-height: 100vh;
background:
radial-gradient(ellipse 70% 40% at 50% 0%, rgba(217, 185, 106, 0.12), transparent 60%),
#14100c;
color: #e8dcc0;
font-family: 'Noto Serif SC', serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px clamp(12px, 4vw, 48px) 46px;
}
.ac-nav {
display: flex;
align-items: center;
gap: 16px;
width: min(640px, 100%);
margin-bottom: 18px;
flex-wrap: wrap;
}
.ac-back {
color: #d9b96a;
text-decoration: none;
font-size: 12.5px;
letter-spacing: 0.14em;
border: 1px solid rgba(217, 185, 106, 0.5);
padding: 7px 16px;
}
.ac-back:hover { background: rgba(217, 185, 106, 0.08); }
.ac-nav h1 { margin: 0; font-size: 17px; font-weight: 400; letter-spacing: 0.2em; }
.ac-booked { margin-left: auto; font-size: 12px; color: #d9b96a; }
.ac-bill {
width: min(640px, 100%);
background: #1c1610;
border: 2px solid #d9b96a;
outline: 1px solid rgba(217, 185, 106, 0.35);
outline-offset: 5px;
padding: 30px clamp(18px, 5vw, 44px) 24px;
}
.ac-bill-head { text-align: center; }
.ac-fan {
position: relative;
height: 40px;
margin-bottom: 8px;
}
.ac-fan i {
position: absolute;
left: 50%;
bottom: 0;
width: 2px;
height: 38px;
background: linear-gradient(to top, #d9b96a, transparent);
transform-origin: bottom center;
}
.ac-est { margin: 0 0 6px; font-size: 11px; letter-spacing: 0.5em; color: rgba(232, 220, 192, 0.55); }
.ac-bill-head h2 {
margin: 0 0 6px;
font-size: 30px;
font-weight: 400;
letter-spacing: 0.4em;
color: #d9b96a;
}
.ac-date { margin: 0; font-size: 12px; letter-spacing: 0.24em; color: rgba(232, 220, 192, 0.5); }
.ac-divider {
display: flex;
align-items: center;
gap: 12px;
margin: 20px 0;
}
.ac-divider i { flex: 1; height: 1px; background: rgba(217, 185, 106, 0.4); }
.ac-divider b { color: #d9b96a; font-size: 13px; }
.ac-act {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 15px 12px;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s ease;
}
.ac-act:hover { background: rgba(217, 185, 106, 0.05); }
.ac-act.on { border-color: rgba(217, 185, 106, 0.55); background: rgba(217, 185, 106, 0.07); }
.ac-time {
font-size: 13px;
color: #d9b96a;
letter-spacing: 0.1em;
min-width: 48px;
padding-top: 2px;
}
.ac-act-info { flex: 1; }
.ac-act-info h3 { margin: 0 0 5px; font-size: 15.5px; font-weight: 700; letter-spacing: 0.06em; }
.ac-act-info h3 i { font-style: normal; margin-right: 8px; }
.ac-act-info p { margin: 0; font-size: 12px; line-height: 1.8; color: rgba(232, 220, 192, 0.6); }
.ac-book {
font-size: 11px;
letter-spacing: 0.14em;
color: rgba(232, 220, 192, 0.5);
border: 1px solid rgba(232, 220, 192, 0.3);
padding: 4px 12px;
align-self: center;
flex-shrink: 0;
}
.ac-act.on .ac-book { color: #14100c; background: #d9b96a; border-color: #d9b96a; }
.ac-foot { text-align: center; border-top: 1px solid rgba(217, 185, 106, 0.35); margin-top: 14px; padding-top: 14px; }
.ac-foot p { margin: 0 0 5px; font-size: 12px; letter-spacing: 0.14em; }
.ac-note { font-size: 10.5px !important; color: rgba(232, 220, 192, 0.45); }
</style>

View File

@@ -0,0 +1,213 @@
<script setup>
import { computed, ref } from 'vue'
/* 点星连线:按顺序连出「码农座」 */
const STARS = [
{ id: 0, x: 22, y: 26, name: '启蒙星' },
{ id: 1, x: 38, y: 18, name: '语法星' },
{ id: 2, x: 55, y: 28, name: '框架星' },
{ id: 3, x: 70, y: 20, name: '架构星' },
{ id: 4, x: 62, y: 48, name: '调试星' },
{ id: 5, x: 44, y: 56, name: '重构星' },
{ id: 6, x: 28, y: 66, name: '开源星' },
{ id: 7, x: 52, y: 76, name: '发版星' },
]
const path = ref([])
const done = computed(() => path.value.length === STARS.length)
function tap(s) {
if (done.value) return
const expect = path.value.length
if (s.id === expect) {
path.value = [...path.value, s.id]
}
}
function reset() {
path.value = []
}
const lines = computed(() =>
path.value.slice(1).map((id, i) => {
const a = STARS[path.value[i]]
const b = STARS[id]
return { x1: a.x, y1: a.y, x2: b.x, y2: b.y }
}),
)
const nextId = computed(() => path.value.length)
</script>
<template>
<div class="asc">
<nav class="asc-nav">
<router-link to="/style/astrolabe" class="asc-back"> 收起星盘</router-link>
<h1>连星成座 · 码农座</h1>
<button class="asc-reset" @click="reset"> 重连</button>
</nav>
<p class="asc-tip">
{{ done ? '「码农座」连成!传说仰望它的人,构建从不失败。' : `按亮起的顺序连接星星(下一颗:${STARS[nextId]?.name})` }}
</p>
<div class="asc-sky">
<!-- 背景星尘 -->
<i v-for="n in 40" :key="'d' + n" class="asc-dust" :style="{ left: ((n * 53) % 100) + '%', top: ((n * 37) % 100) + '%', animationDelay: (n % 7) * 0.4 + 's' }"></i>
<!-- 连线 -->
<svg class="asc-lines" viewBox="0 0 100 100" preserveAspectRatio="none">
<line v-for="(l, i) in lines" :key="i" :x1="l.x1" :y1="l.y1" :x2="l.x2" :y2="l.y2" />
</svg>
<!-- 星星 -->
<button
v-for="s in STARS"
:key="s.id"
class="asc-star"
:class="{ lit: path.includes(s.id), next: s.id === nextId && !done }"
:style="{ left: s.x + '%', top: s.y + '%' }"
@click="tap(s)"
>
<i></i>
<span>{{ s.name }}</span>
</button>
<!-- 完成光效 -->
<div v-if="done" class="asc-glow"></div>
</div>
<div v-if="done" class="asc-reading">
<p class="asc-reading-title"> 星座解读</p>
<p>码农座,冬夜可见(机房常年可见)主星调试星光变周期不定,象征此座之人耐心极佳好奇心旺盛,唯独在需求评审日光芒黯淡</p>
</div>
</div>
</template>
<style scoped>
.asc {
min-height: 100vh;
background: radial-gradient(ellipse 80% 60% at 50% 100%, #1a2444, #0a0e20 70%);
color: #dce4f8;
font-family: 'Noto Serif SC', serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 22px clamp(12px, 4vw, 48px) 44px;
}
.asc-nav {
display: flex;
align-items: center;
gap: 16px;
width: min(720px, 100%);
margin-bottom: 8px;
flex-wrap: wrap;
}
.asc-back {
color: #ffd97a;
text-decoration: none;
font-size: 13px;
border: 1px solid rgba(255, 217, 122, 0.45);
border-radius: 99px;
padding: 7px 16px;
}
.asc-back:hover { background: rgba(255, 217, 122, 0.08); }
.asc-nav h1 { margin: 0; font-size: 19px; font-weight: 900; letter-spacing: 0.16em; }
.asc-reset {
margin-left: auto;
border: 1px solid rgba(220, 228, 248, 0.3);
background: none;
color: #dce4f8;
font-family: inherit;
font-size: 12.5px;
border-radius: 99px;
padding: 7px 15px;
cursor: pointer;
}
.asc-tip { width: min(720px, 100%); margin: 0 0 12px; font-size: 13px; color: #ffd97a; min-height: 20px; }
.asc-sky {
position: relative;
width: min(720px, 100%);
aspect-ratio: 16/10;
border: 1px solid rgba(220, 228, 248, 0.15);
border-radius: 16px;
overflow: hidden;
background: radial-gradient(circle at 70% 20%, rgba(122, 140, 220, 0.1), transparent 50%);
}
.asc-dust {
position: absolute;
width: 2px;
height: 2px;
border-radius: 50%;
background: rgba(220, 228, 248, 0.5);
animation: asc-tw 3s ease-in-out infinite;
}
@keyframes asc-tw {
50% { opacity: 0.15; }
}
.asc-lines { position: absolute; inset: 0; width: 100%; height: 100%; }
.asc-lines line {
stroke: rgba(255, 217, 122, 0.75);
stroke-width: 0.45;
stroke-linecap: round;
filter: drop-shadow(0 0 2px rgba(255, 217, 122, 0.7));
animation: asc-draw 0.4s ease;
}
@keyframes asc-draw {
from { opacity: 0; }
}
.asc-star {
position: absolute;
transform: translate(-50%, -50%);
border: none;
background: none;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
z-index: 3;
}
.asc-star i {
width: 12px;
height: 12px;
border-radius: 50%;
background: rgba(220, 228, 248, 0.4);
transition: all 0.25s ease;
}
.asc-star span {
font-size: 10.5px;
color: rgba(220, 228, 248, 0.5);
letter-spacing: 0.1em;
white-space: nowrap;
}
.asc-star.next i {
background: #ffd97a;
box-shadow: 0 0 18px rgba(255, 217, 122, 0.9);
animation: asc-pulse 1s ease-in-out infinite;
}
@keyframes asc-pulse {
50% { transform: scale(1.5); }
}
.asc-star.lit i { background: #ffd97a; box-shadow: 0 0 12px rgba(255, 217, 122, 0.8); }
.asc-star.lit span { color: #ffd97a; }
.asc-glow {
position: absolute;
inset: 0;
background: radial-gradient(ellipse 46% 40% at 46% 44%, rgba(255, 217, 122, 0.1), transparent 65%);
animation: asc-bloom 1.6s ease;
pointer-events: none;
}
@keyframes asc-bloom {
from { opacity: 0; }
}
.asc-reading {
width: min(720px, 100%);
margin-top: 14px;
background: rgba(255, 217, 122, 0.06);
border: 1px dashed rgba(255, 217, 122, 0.4);
border-radius: 12px;
padding: 14px 18px;
animation: asc-draw 0.5s ease;
}
.asc-reading-title { margin: 0 0 7px; font-size: 13px; color: #ffd97a; letter-spacing: 0.2em; }
.asc-reading p:last-child { margin: 0; font-size: 13px; line-height: 2; color: rgba(220, 228, 248, 0.82); }
</style>