style(ui): 换用 Memphis/neo-brutalism 视觉风格

参考 skills.yangsir.net:粗墨描边 + 硬偏移投影 + 平涂蜡笔色(橙/黄/粉/蓝/薄荷),
替换原二次元粉紫渐变体系。设计令牌集中在 style.css 的 --ink/--shadow-ink/
--m-* 变量,共享组件 .card/.btn-*/.field 与新工具类 (.memphis-dots/.ink-card/
.memphis-press) 使全站统一;暗色模式反转为浅色描边版本。

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-10-08 16:58:14 +08:00
co-authored by Devin
parent 910651ddba
commit 313d54a5ec
9 changed files with 507 additions and 362 deletions
+10 -10
View File
@@ -13,22 +13,22 @@ const html = computed(() => marked.parse(announcement.content.trim() || '暂无
<div v-if="announcement.show"
class="fixed inset-0 z-[80] bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4"
@click.self="dismissAnnouncement">
<div class="w-full max-w-lg rounded-2xl bg-white text-slate-800 shadow-2xl overflow-hidden flex flex-col max-h-[80vh]">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between gap-3 shrink-0">
<div class="w-full max-w-lg rounded-xl bg-white text-slate-800 border-[3px] border-[#111] shadow-[8px_8px_0_#111] overflow-hidden flex flex-col max-h-[80vh]">
<div class="px-6 py-4 border-b-[2.5px] border-b-[#111] flex items-center justify-between gap-3 shrink-0">
<div class="flex items-center gap-2">
<span class="w-7 h-7 rounded-lg bg-violet-500/15 text-violet-600 grid place-items-center">
<span class="w-7 h-7 rounded-lg bg-[#fef08a] text-[#111] border-2 border-[#111] grid place-items-center">
<Icon name="spark" class="w-4 h-4" />
</span>
<h2 class="text-base font-semibold">公告</h2>
<h2 class="text-base font-extrabold">公告</h2>
</div>
<button @click="dismissAnnouncement" class="text-slate-400 hover:text-slate-700 transition-colors">
<Icon name="close" class="w-5 h-5" />
</button>
</div>
<div class="ann-body px-6 py-5 overflow-y-auto" v-html="html"></div>
<div class="px-6 py-4 border-t border-slate-100 flex justify-end shrink-0">
<div class="px-6 py-4 border-t-[2.5px] border-t-[#111] flex justify-end shrink-0">
<button @click="dismissAnnouncement"
class="rounded-lg bg-slate-900 text-white hover:bg-slate-700 px-5 py-2 text-sm font-medium transition-colors">
class="rounded-lg bg-[#ec5b13] text-white border-[2.5px] border-[#111] shadow-[3px_3px_0_#111] hover:-translate-x-0.5 hover:-translate-y-0.5 hover:shadow-[4px_4px_0_#111] active:translate-y-0.5 active:shadow-none px-5 py-2 text-sm font-extrabold transition-all">
我知道了
</button>
</div>
@@ -51,12 +51,12 @@ const html = computed(() => marked.parse(announcement.content.trim() || '暂无
.ann-body :deep(ul) { list-style: disc; }
.ann-body :deep(ol) { list-style: decimal; }
.ann-body :deep(li) { margin: 0.2em 0; }
.ann-body :deep(a) { color: rgb(124 58 237); text-decoration: underline; }
.ann-body :deep(a) { color: var(--m-orange); text-decoration: underline; }
.ann-body :deep(strong) { font-weight: 700; color: rgb(15 23 42); }
.ann-body :deep(code) { background: rgb(241 245 249); padding: 0.1em 0.35em; border-radius: 0.3rem; font-size: 0.85em; }
.ann-body :deep(pre) { background: rgb(241 245 249); padding: 0.8em; border-radius: 0.5rem; overflow-x: auto; margin: 0.6em 0; }
.ann-body :deep(code) { background: rgb(254 240 138 / 0.5); padding: 0.1em 0.35em; border-radius: 0.3rem; font-size: 0.85em; }
.ann-body :deep(pre) { background: #111; color: #f5f5f5; padding: 0.8em; border-radius: 0.5rem; overflow-x: auto; margin: 0.6em 0; }
.ann-body :deep(pre code) { background: none; padding: 0; }
.ann-body :deep(blockquote) { border-left: 3px solid rgb(196 181 253); padding-left: 0.9em; color: rgb(100 116 139); margin: 0.6em 0; }
.ann-body :deep(blockquote) { border-left: 3px solid var(--m-orange); padding-left: 0.9em; color: rgb(100 116 139); margin: 0.6em 0; }
.ann-body :deep(hr) { border: none; border-top: 1px solid rgb(226 232 240); margin: 1em 0; }
.ann-body :deep(img) { max-width: 100%; border-radius: 0.5rem; margin: 0.5em 0; }
</style>
+57 -53
View File
@@ -164,9 +164,9 @@ async function submit() {
<div class="absolute inset-0 bg-black/70 backdrop-blur-md" @click="closeLogin"></div>
<div class="card relative z-10 w-full max-w-[26rem]">
<!-- ambient brand glow -->
<div class="pointer-events-none absolute -top-24 -right-16 h-56 w-56 rounded-full bg-fuchsia-500/20 blur-3xl"></div>
<div class="pointer-events-none absolute -bottom-24 -left-16 h-56 w-56 rounded-full bg-violet-500/20 blur-3xl"></div>
<!-- ambient memphis crayon washes -->
<div class="pointer-events-none absolute -top-24 -right-16 h-56 w-56 rounded-full bg-[#fef08a]/60 blur-3xl"></div>
<div class="pointer-events-none absolute -bottom-24 -left-16 h-56 w-56 rounded-full bg-[#ffc1cc]/50 blur-3xl"></div>
<div class="relative p-7">
<button class="absolute top-4 right-4 grid h-7 w-7 place-items-center rounded-lg text-[color:var(--fg-3)] hover:text-[color:var(--fg)] hover:bg-[var(--hover)] transition-colors"
@@ -195,7 +195,7 @@ async function submit() {
<transition name="swap" mode="out-in">
<p v-if="mode === 'register' && auth.pendingInvite" class="invite-banner">
<svg viewBox="0 0 24 24" class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 12v10H4V12"/><path d="M2 7h20v5H2z"/><path d="M12 22V7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/></svg>
<span>已应用邀请码 <span class="font-mono font-semibold text-emerald-200">{{ auth.pendingInvite }}</span>,完成首次生图后邀请人得 3 积分</span>
<span>已应用邀请码 <span class="font-mono font-bold text-emerald-800 dark:text-emerald-200">{{ auth.pendingInvite }}</span>,完成首次生图后邀请人得 3 积分</span>
</p>
</transition>
@@ -262,44 +262,44 @@ async function submit() {
<style scoped>
.card {
border-radius: 1.25rem;
background: linear-gradient(165deg, #ffffff 0%, #f6f6fb 100%);
border-radius: 1.1rem;
background: var(--surface);
color: var(--fg);
border: 1px solid var(--hairline);
box-shadow: 0 30px 70px -22px rgb(15 23 42 / 0.25);
border: 3px solid var(--ink);
box-shadow: 10px 10px 0 var(--shadow-ink);
overflow: hidden;
}
html.dark .card {
background: linear-gradient(165deg, #14161f 0%, #0d0f15 100%);
background: #1c1d24;
color: rgb(255 255 255 / 0.9);
border: none;
box-shadow: 0 30px 70px -20px rgb(0 0 0 / 0.7);
}
.logo-halo {
position: relative;
filter: drop-shadow(0 8px 18px rgb(168 85 247 / 0.45));
filter: drop-shadow(0 6px 0 rgb(236 91 19 / 0.35));
}
/* segmented tab control */
.tabs {
display: flex; gap: 0.25rem; padding: 0.25rem;
border-radius: 0.75rem;
display: flex; gap: 0.3rem; padding: 0.3rem;
border-radius: 0.7rem;
background: var(--surface-2);
border: 1px solid var(--hairline);
border: 2px solid var(--ink);
}
.tab {
flex: 1; padding: 0.45rem 0; border-radius: 0.55rem;
font-size: 0.8125rem; color: var(--fg-3);
transition: background 0.18s, color 0.18s, box-shadow 0.18s;
flex: 1; padding: 0.45rem 0; border-radius: 0.5rem;
font-size: 0.8125rem; font-weight: 700; color: var(--fg-3);
border: 2px solid transparent;
transition: background 0.18s, color 0.18s, box-shadow 0.18s, border-color 0.18s;
}
.tab:hover { color: var(--fg-2); }
.tab:hover { color: var(--fg); }
.tab-on {
color: rgb(124 58 237);
background: linear-gradient(135deg, rgb(167 139 250 / 0.22), rgb(236 72 153 / 0.18));
box-shadow: 0 1px 0 rgb(255 255 255 / 0.08) inset, 0 4px 12px -4px rgb(168 85 247 / 0.5);
color: #111;
background: var(--m-yellow);
border-color: var(--ink);
box-shadow: 2px 2px 0 var(--shadow-ink);
}
html.dark .tab-on { color: white; }
html.dark .tab-on { color: #fff; background: var(--m-orange); }
/* icon-prefixed input */
.lm-field { position: relative; }
@@ -308,21 +308,18 @@ html.dark .tab-on { color: white; }
width: 1.05rem; height: 1.05rem; color: var(--fg-3);
pointer-events: none; transition: color 0.18s;
}
.lm-field:focus-within .ic { color: rgb(124 58 237 / 0.95); }
html.dark .lm-field:focus-within .ic { color: rgb(196 181 253 / 0.95); }
.lm-field:focus-within .ic { color: var(--m-orange); }
.fld {
width: 100%; padding: 0.7rem 0.85rem 0.7rem 2.4rem; border-radius: 0.7rem;
background: rgb(15 23 42 / 0.03); border: 1px solid var(--hairline);
width: 100%; padding: 0.7rem 0.85rem 0.7rem 2.4rem; border-radius: 0.6rem;
background: var(--surface); border: 2px solid var(--ink-soft);
color: var(--fg); font-size: 0.875rem; outline: none;
transition: border-color 0.18s, box-shadow 0.18s, background 0.18s;
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
}
html.dark .fld { background: rgb(255 255 255 / 0.04); }
html.dark .fld { background: rgb(255 255 255 / 0.05); }
.fld:focus {
border-color: rgb(167 139 250 / 0.65);
background: rgb(124 58 237 / 0.04);
box-shadow: 0 0 0 3px rgb(167 139 250 / 0.18);
border-color: var(--ink);
box-shadow: 3px 3px 0 var(--m-orange);
}
html.dark .fld:focus { background: rgb(255 255 255 / 0.06); }
.fld::placeholder { color: var(--fg-faint); }
/* Keep autofill on-theme in dark (otherwise it paints white/yellow). */
html.dark .fld:-webkit-autofill,
@@ -343,16 +340,18 @@ html.dark .fld:-webkit-autofill:focus {
.eye:hover { color: var(--fg); background: var(--hover); }
.code-btn {
flex-shrink: 0; height: 2.7rem; padding: 0 0.85rem; border-radius: 0.7rem;
flex-shrink: 0; height: 2.7rem; padding: 0 0.85rem; border-radius: 0.6rem;
min-width: 5.5rem; /* keep width stable between text / spinner */
display: inline-flex; align-items: center; justify-content: center;
font-size: 0.75rem; white-space: nowrap;
color: rgb(124 58 237); /* violet-600 — readable on the light card */
background: rgb(167 139 250 / 0.12); border: 1px solid rgb(167 139 250 / 0.4);
transition: background 0.15s, opacity 0.15s;
font-size: 0.75rem; font-weight: 800; white-space: nowrap;
color: #111;
background: var(--m-yellow); border: 2px solid var(--ink);
box-shadow: 2px 2px 0 var(--shadow-ink);
transition: background 0.15s, box-shadow 0.15s, transform 0.15s, opacity 0.15s;
}
html.dark .code-btn { color: rgb(196 181 253 / 0.95); background: rgb(167 139 250 / 0.1); border-color: rgb(167 139 250 / 0.3); }
.code-btn:hover:not(:disabled) { background: rgb(167 139 250 / 0.2); }
html.dark .code-btn { color: #111; }
.code-btn:hover:not(:disabled) { transform: translate(-1px, -1px); box-shadow: 3px 3px 0 var(--shadow-ink); }
.code-btn:active:not(:disabled) { transform: scale(0.96) translateY(1px); box-shadow: none; }
/* Disabled (sending / countdown): just dim — keep the theme text color so it
never washes out to invisible white on the light card. */
.code-btn:disabled { opacity: 0.55; cursor: not-allowed; }
@@ -361,14 +360,15 @@ html.dark .code-btn { color: rgb(196 181 253 / 0.95); background: rgb(167 139 25
.btn-primary {
display: flex; align-items: center; justify-content: center; gap: 0.5rem;
padding: 0.72rem 0; border-radius: 0.7rem; font-size: 0.9rem; font-weight: 600;
padding: 0.72rem 0; border-radius: 0.65rem; font-size: 0.9rem; font-weight: 800;
letter-spacing: 0.02em; color: white; margin-top: 0.35rem;
background: linear-gradient(135deg, #a855f7 0%, #7c3aed 50%, #ec4899 100%);
box-shadow: 0 10px 24px -8px rgb(168 85 247 / 0.6);
transition: transform 0.12s, box-shadow 0.18s, filter 0.18s;
background: var(--m-orange);
border: 2.5px solid var(--ink);
box-shadow: 4px 4px 0 var(--shadow-ink);
transition: transform 0.13s, box-shadow 0.13s, filter 0.15s;
}
.btn-primary:hover:not(:disabled) { filter: brightness(1.08); box-shadow: 0 12px 28px -8px rgb(168 85 247 / 0.75); }
.btn-primary:active:not(:disabled) { transform: translateY(1px); }
.btn-primary:hover:not(:disabled) { filter: brightness(1.06); transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--shadow-ink); }
.btn-primary:active:not(:disabled) { transform: scale(0.98) translateY(2px); box-shadow: 0 0 0 var(--shadow-ink); }
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
.spinner {
@@ -380,18 +380,22 @@ html.dark .code-btn { color: rgb(196 181 253 / 0.95); background: rgb(167 139 25
.invite-banner {
display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.85rem;
padding: 0.6rem 0.75rem; border-radius: 0.7rem; font-size: 0.75rem; line-height: 1.4;
color: rgb(167 243 208);
background: rgb(16 185 129 / 0.1); border: 1px solid rgb(16 185 129 / 0.25);
padding: 0.6rem 0.75rem; border-radius: 0.6rem; font-size: 0.75rem; line-height: 1.4;
color: #065f46; font-weight: 600;
background: var(--m-mint); border: 2px solid var(--ink);
}
.invite-banner svg { color: rgb(110 231 183); }
html.dark .invite-banner { color: rgb(167 243 208); background: rgb(16 185 129 / 0.15); }
.invite-banner svg { color: rgb(5 150 105); }
html.dark .invite-banner svg { color: rgb(110 231 183); }
.msg {
display: flex; align-items: center; gap: 0.4rem; margin-top: 0.85rem;
font-size: 0.78rem; line-height: 1.4;
font-size: 0.78rem; font-weight: 600; line-height: 1.4;
}
.msg-err { color: rgb(253 164 175); }
.msg-ok { color: rgb(110 231 183); }
.msg-err { color: rgb(220 38 38); }
html.dark .msg-err { color: rgb(253 164 175); }
.msg-ok { color: rgb(5 150 105); }
html.dark .msg-ok { color: rgb(110 231 183); }
/* modal in/out */
.modal-enter-active { transition: opacity 0.2s ease; }
+7 -7
View File
@@ -103,15 +103,15 @@ onUnmounted(clearTimers)
<transition name="pay-fade">
<div v-if="payment.show" class="fixed inset-0 z-[85] bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4"
@click.self="!paid && finish()">
<div class="w-full max-w-sm rounded-2xl bg-white text-slate-800 shadow-2xl overflow-hidden">
<div class="px-5 py-4 border-b border-slate-100 flex items-center justify-between">
<div class="w-full max-w-sm rounded-xl bg-white text-slate-800 border-[3px] border-[#111] shadow-[8px_8px_0_#111] overflow-hidden">
<div class="px-5 py-4 border-b-[2.5px] border-b-[#111] flex items-center justify-between">
<h2 class="text-base font-semibold">{{ paid ? '支付成功' : dead ? '支付超时' : (isJump ? '支付监控中' : '扫码支付') }}</h2>
<button v-if="!paid" @click="finish" class="text-slate-400 hover:text-slate-700"><Icon name="close" class="w-5 h-5" /></button>
</div>
<!-- paid -->
<div v-if="paid" class="px-6 py-10 text-center">
<div class="w-16 h-16 mx-auto rounded-full bg-emerald-100 text-emerald-600 grid place-items-center mb-4">
<div class="w-16 h-16 mx-auto rounded-full bg-[#a7f3d0] text-emerald-700 border-[2.5px] border-[#111] shadow-[4px_4px_0_#111] grid place-items-center mb-4">
<Icon name="spark" class="w-8 h-8" />
</div>
<p class="text-lg font-semibold text-slate-800">充值成功</p>
@@ -127,7 +127,7 @@ onUnmounted(clearTimers)
<p class="text-lg font-semibold text-slate-700">支付超时</p>
<p class="text-sm text-slate-500 mt-1">二维码已失效,订单已取消</p>
<p class="text-xs text-slate-400 mt-1">如需充值请重新下单</p>
<button @click="finish" class="mt-5 rounded-lg bg-slate-900 text-white hover:bg-slate-700 px-5 py-2 text-sm font-medium transition-colors">关闭</button>
<button @click="finish" class="mt-5 rounded-lg bg-[#ec5b13] text-white border-[2.5px] border-[#111] shadow-[3px_3px_0_#111] hover:-translate-y-0.5 active:translate-y-0.5 active:shadow-none px-5 py-2 text-sm font-extrabold transition-all">关闭</button>
</div>
<!-- pending -->
@@ -135,7 +135,7 @@ onUnmounted(clearTimers)
<!-- qrcode: scan to pay -->
<template v-if="!isJump">
<div class="flex justify-center mb-3">
<div class="w-[228px] h-[228px] rounded-2xl ring-1 ring-slate-200 shadow-sm grid place-items-center overflow-hidden bg-white p-3.5">
<div class="w-[228px] h-[228px] rounded-xl border-[2.5px] border-[#111] shadow-[4px_4px_0_#111] grid place-items-center overflow-hidden bg-white p-3.5">
<img v-if="qrUrl" :src="qrUrl" alt="支付二维码" class="w-full h-full rounded-lg" />
<span v-else class="text-xs text-slate-400">二维码生成中…</span>
</div>
@@ -145,7 +145,7 @@ onUnmounted(clearTimers)
<!-- jump (no qrcode): the cashier opened in a new tab — just monitor -->
<template v-else>
<div class="flex flex-col items-center justify-center py-7 mb-2">
<div class="w-12 h-12 rounded-full border-2 border-violet-200 border-t-violet-500 animate-spin mb-3"></div>
<div class="w-12 h-12 rounded-full border-[3px] border-[#fecaca] border-t-[#ec5b13] animate-spin mb-3"></div>
<p class="text-sm font-medium text-slate-700">支付监控中…</p>
<p class="text-xs text-slate-400 mt-1">已打开支付页面,完成后自动到账</p>
</div>
@@ -155,7 +155,7 @@ onUnmounted(clearTimers)
<div class="flex justify-between"><dt class="text-slate-400">订单号</dt><dd class="font-mono text-xs text-slate-700">{{ order.id }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-400">状态</dt><dd class="text-amber-600 font-medium">{{ statusLabel }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-400">金额</dt><dd class="font-semibold text-slate-800">¥{{ order.amount }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-400">充值积分</dt><dd class="text-violet-600 font-semibold">{{ order.points }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-400">充值积分</dt><dd class="text-[#ec5b13] font-semibold">{{ order.points }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-400">下单时间</dt><dd class="text-xs text-slate-600">{{ fmtTime(order.created_at) }}</dd></div>
<div class="flex justify-between"><dt class="text-slate-400">支付倒计时</dt><dd class="tabular-nums font-medium" :class="remaining > 0 ? 'text-slate-700' : 'text-rose-500'">{{ remaining > 0 ? mmss : '已超时' }}</dd></div>
</dl>
+5 -5
View File
@@ -90,17 +90,17 @@ onUnmounted(() => document.removeEventListener('mousedown', onDocClick))
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-1">
<div v-if="open" ref="panel"
class="absolute z-30 mt-2 w-full max-h-64 overflow-auto rounded-xl border border-[color:var(--hairline)]
bg-[var(--menu-bg)] backdrop-blur-xl p-1.5 shadow-2xl shadow-black/20 ring-1 ring-[color:var(--hairline)]">
class="absolute z-30 mt-2 w-full max-h-64 overflow-auto rounded-xl border-[2.5px] border-[color:var(--ink)]
bg-[var(--menu-bg)] p-1.5 shadow-[5px_5px_0_var(--shadow-ink)]">
<button v-for="(o, i) in options" :key="o.value" type="button"
:data-idx="i" @click="pick(o)" @mouseenter="active = i"
class="w-full flex items-center justify-between gap-2 rounded-lg px-3 py-2 text-left text-sm transition-colors"
class="w-full flex items-center justify-between gap-2 rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors"
:class="[
mono ? 'font-mono' : '',
i === active ? 'bg-[var(--hover)] text-[color:var(--fg)]' : 'text-[color:var(--fg-2)]',
i === active ? 'bg-[var(--m-yellow)] text-[#111]' : 'text-[color:var(--fg-2)]',
]">
<span class="truncate">{{ o.label }}</span>
<Icon v-if="o.value === modelValue" name="check" class="w-4 h-4 shrink-0 text-violet-400" />
<Icon v-if="o.value === modelValue" name="check" class="w-4 h-4 shrink-0 text-[var(--m-orange)]" />
</button>
<div v-if="!options.length" class="px-3 py-2 text-xs text-[color:var(--fg-3)]">无选项</div>
</div>