Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfebcee886 |
@@ -33,3 +33,5 @@ node_modules/
|
|||||||
# 前端构建产物不入库,仅保留 .gitkeep 以满足 go:embed
|
# 前端构建产物不入库,仅保留 .gitkeep 以满足 go:embed
|
||||||
web/dist/*
|
web/dist/*
|
||||||
!web/dist/.gitkeep
|
!web/dist/.gitkeep
|
||||||
|
|
||||||
|
art/
|
||||||
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,569 @@
|
|||||||
|
/* ============================================================
|
||||||
|
妄想症 · Paranoia 主题(致敬篇)
|
||||||
|
------------------------------------------------------------
|
||||||
|
在 <html> 上挂 .paranoia 即启用,可与 .dark 叠加:
|
||||||
|
|
||||||
|
html.paranoia 现实 —— 白之章,近乎空白的纸色
|
||||||
|
html.paranoia.dark 妄想 —— 黑之章,石蒜红与深渊
|
||||||
|
|
||||||
|
取色依据原作 PV 的用色习惯:
|
||||||
|
石蒜红 #e0245e 彼岸花与零羽
|
||||||
|
洛天依蓝 #66ccff 泠珞(也是 goodBaby 原本的品牌色)
|
||||||
|
言和青 #7ee8e0 / 言和黄 #f2d16b 颜语的守护者与加害者两面
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
html.paranoia {
|
||||||
|
/* 标题用衬线体,制造「档案 / 卷宗」的观感 */
|
||||||
|
--pa-serif: 'Noto Serif SC', 'Source Han Serif SC', 'Songti SC', STSong, SimSun, serif;
|
||||||
|
--pa-mono: ui-monospace, SFMono-Regular, Consolas, 'Courier New', monospace;
|
||||||
|
--pa-crimson: #e0245e;
|
||||||
|
--pa-crimson-soft: #f2568a;
|
||||||
|
--pa-blue: #66ccff;
|
||||||
|
--pa-cyan: #7ee8e0;
|
||||||
|
--pa-yellow: #f2d16b;
|
||||||
|
--pa-violet: #b98cff;
|
||||||
|
/* 卡片不再是圆润的「气泡」,而是切边的纸片 */
|
||||||
|
--gb-radius-card: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 现实:白之章 ---------- */
|
||||||
|
|
||||||
|
html.paranoia:not(.dark) {
|
||||||
|
--gb-primary: #b3163c;
|
||||||
|
--gb-primary-deep: #8c112f;
|
||||||
|
--gb-ink: #17121a;
|
||||||
|
--gb-ink-soft: #2a1c25;
|
||||||
|
--gb-bg: #f5f1ee;
|
||||||
|
--gb-card: #fffdfc;
|
||||||
|
--gb-shadow-card: 0 1px 2px rgb(90 24 48 / 0.05), 0 1px 10px rgb(90 24 48 / 0.05);
|
||||||
|
--gb-shadow-hover: 0 10px 26px rgb(120 20 50 / 0.14);
|
||||||
|
|
||||||
|
--pa-line: rgb(179 22 60 / 0.16);
|
||||||
|
--pa-line-soft: rgb(179 22 60 / 0.08);
|
||||||
|
--pa-line-strong: rgb(179 22 60 / 0.36);
|
||||||
|
--pa-glow: rgb(179 22 60 / 0.16);
|
||||||
|
--pa-ghost: rgb(43 20 32 / 0.055);
|
||||||
|
--pa-grain-opacity: 0.05;
|
||||||
|
--pa-veil: rgb(255 253 252 / 0.72);
|
||||||
|
|
||||||
|
--el-color-primary: #b3163c;
|
||||||
|
--el-color-primary-light-3: #c94f6d;
|
||||||
|
--el-color-primary-light-5: #d98899;
|
||||||
|
--el-color-primary-light-7: #e8b4bf;
|
||||||
|
--el-color-primary-light-8: #f0cad2;
|
||||||
|
--el-color-primary-light-9: #f8e7ea;
|
||||||
|
--el-color-primary-dark-2: #8f1230;
|
||||||
|
|
||||||
|
--el-bg-color: #fffdfc;
|
||||||
|
--el-bg-color-page: #f5f1ee;
|
||||||
|
--el-bg-color-overlay: #fffdfc;
|
||||||
|
--el-text-color-primary: #2b2028;
|
||||||
|
--el-text-color-regular: #4d3f47;
|
||||||
|
--el-text-color-secondary: #8a7880;
|
||||||
|
--el-text-color-placeholder: #a8969e;
|
||||||
|
--el-border-color: rgb(179 22 60 / 0.2);
|
||||||
|
--el-border-color-light: rgb(179 22 60 / 0.14);
|
||||||
|
--el-border-color-lighter: rgb(179 22 60 / 0.1);
|
||||||
|
--el-border-color-extra-light: rgb(179 22 60 / 0.08);
|
||||||
|
--el-fill-color: rgb(179 22 60 / 0.06);
|
||||||
|
--el-fill-color-light: rgb(179 22 60 / 0.04);
|
||||||
|
--el-fill-color-lighter: rgb(179 22 60 / 0.025);
|
||||||
|
--el-fill-color-blank: #fffdfc;
|
||||||
|
--el-mask-color: rgb(43 20 32 / 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 妄想:黑之章 ---------- */
|
||||||
|
|
||||||
|
html.paranoia.dark {
|
||||||
|
--gb-primary: #e0245e;
|
||||||
|
/* 深底之上需要更亮的强调色才读得清 */
|
||||||
|
--gb-primary-deep: #f2568a;
|
||||||
|
--gb-ink: #100a12;
|
||||||
|
--gb-ink-soft: #1d0f1b;
|
||||||
|
--gb-bg: #08070c;
|
||||||
|
--gb-card: #150f19;
|
||||||
|
--gb-shadow-card: 0 1px 2px rgb(0 0 0 / 0.5);
|
||||||
|
--gb-shadow-hover: 0 10px 30px rgb(0 0 0 / 0.55), 0 0 0 1px rgb(224 36 94 / 0.14);
|
||||||
|
|
||||||
|
--pa-line: rgb(224 36 94 / 0.24);
|
||||||
|
--pa-line-soft: rgb(224 36 94 / 0.12);
|
||||||
|
--pa-line-strong: rgb(224 36 94 / 0.5);
|
||||||
|
--pa-glow: rgb(224 36 94 / 0.34);
|
||||||
|
--pa-ghost: rgb(236 230 239 / 0.045);
|
||||||
|
--pa-grain-opacity: 0.075;
|
||||||
|
--pa-veil: rgb(10 8 14 / 0.72);
|
||||||
|
|
||||||
|
--el-color-primary: #e0245e;
|
||||||
|
--el-color-primary-light-3: #e6507e;
|
||||||
|
--el-color-primary-light-5: #ec7c9f;
|
||||||
|
--el-color-primary-light-7: #f2a8bf;
|
||||||
|
--el-color-primary-light-8: #f6bfd0;
|
||||||
|
--el-color-primary-light-9: #2a1622;
|
||||||
|
--el-color-primary-dark-2: #b31d4b;
|
||||||
|
|
||||||
|
--el-bg-color: #150f19;
|
||||||
|
--el-bg-color-page: #08070c;
|
||||||
|
--el-bg-color-overlay: #1b1220;
|
||||||
|
--el-text-color-primary: #ece6ef;
|
||||||
|
--el-text-color-regular: #cabfcb;
|
||||||
|
--el-text-color-secondary: #9a8b98;
|
||||||
|
--el-text-color-placeholder: #6f616d;
|
||||||
|
--el-border-color: rgb(224 36 94 / 0.26);
|
||||||
|
--el-border-color-light: rgb(224 36 94 / 0.18);
|
||||||
|
--el-border-color-lighter: rgb(224 36 94 / 0.14);
|
||||||
|
--el-border-color-extra-light: rgb(224 36 94 / 0.1);
|
||||||
|
--el-fill-color: rgb(224 36 94 / 0.12);
|
||||||
|
--el-fill-color-light: rgb(224 36 94 / 0.08);
|
||||||
|
--el-fill-color-lighter: rgb(224 36 94 / 0.05);
|
||||||
|
--el-fill-color-blank: #150f19;
|
||||||
|
--el-mask-color: rgb(4 3 6 / 0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 底色:纸纹与深渊雾气 ---------- */
|
||||||
|
|
||||||
|
html.paranoia,
|
||||||
|
html.paranoia body,
|
||||||
|
html.paranoia #app {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia body {
|
||||||
|
background-color: var(--gb-bg);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(1100px 620px at 8% -12%, rgb(224 36 94 / 0.1), transparent 62%),
|
||||||
|
radial-gradient(900px 520px at 102% 4%, rgb(102 204 255 / 0.07), transparent 58%),
|
||||||
|
radial-gradient(760px 760px at 50% 118%, rgb(224 36 94 / 0.08), transparent 60%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 排版 ---------- */
|
||||||
|
|
||||||
|
html.paranoia .page-header h2,
|
||||||
|
html.paranoia .el-card__header,
|
||||||
|
html.paranoia .el-dialog__title,
|
||||||
|
html.paranoia h1,
|
||||||
|
html.paranoia h2,
|
||||||
|
html.paranoia h3,
|
||||||
|
html.paranoia .pa-display {
|
||||||
|
font-family: var(--pa-serif);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .page-header h2 {
|
||||||
|
font-weight: 700;
|
||||||
|
/* 标题下压一道极细的红线,像卷宗上的分割线 */
|
||||||
|
padding-bottom: 6px;
|
||||||
|
border-bottom: 1px solid var(--pa-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .mono,
|
||||||
|
html.paranoia .stat-value,
|
||||||
|
html.paranoia .countdown-num,
|
||||||
|
html.paranoia .ring-pct {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 卡片:切边的纸片 ---------- */
|
||||||
|
|
||||||
|
html.paranoia .el-card {
|
||||||
|
border: 1px solid var(--pa-line);
|
||||||
|
box-shadow: var(--gb-shadow-card);
|
||||||
|
background: var(--gb-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .el-card__header {
|
||||||
|
border-bottom: 1px solid var(--pa-line);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia:not(.dark) .el-card {
|
||||||
|
background-image: linear-gradient(180deg, rgb(255 255 255 / 0.6), rgb(255 252 251 / 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia.dark .el-card {
|
||||||
|
background-image: linear-gradient(180deg, rgb(224 36 94 / 0.05), transparent 42%);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 按钮 ---------- */
|
||||||
|
|
||||||
|
html.paranoia .el-button {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .el-button--primary {
|
||||||
|
--el-button-text-color: #fff7fa;
|
||||||
|
--el-button-hover-text-color: #fff;
|
||||||
|
--el-button-active-text-color: #fff;
|
||||||
|
--el-button-bg-color: var(--gb-primary);
|
||||||
|
--el-button-border-color: var(--gb-primary);
|
||||||
|
--el-button-hover-bg-color: var(--pa-crimson-soft);
|
||||||
|
--el-button-hover-border-color: var(--pa-crimson-soft);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia.dark .el-button--primary {
|
||||||
|
--el-button-text-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .el-button--default:not(.is-disabled) {
|
||||||
|
background: transparent;
|
||||||
|
border-color: var(--pa-line);
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .el-button--default:not(.is-disabled):hover {
|
||||||
|
border-color: var(--pa-line-strong);
|
||||||
|
color: var(--gb-primary-deep);
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 表单与表格 ---------- */
|
||||||
|
|
||||||
|
html.paranoia .el-input__wrapper,
|
||||||
|
html.paranoia .el-textarea__inner,
|
||||||
|
html.paranoia .el-select__wrapper {
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .el-table {
|
||||||
|
--el-table-border-color: var(--pa-line-soft);
|
||||||
|
--el-table-header-bg-color: transparent;
|
||||||
|
--el-table-row-hover-bg-color: var(--el-fill-color-light);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .el-table tr,
|
||||||
|
html.paranoia .el-table th.el-table__cell {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .el-table th.el-table__cell {
|
||||||
|
font-family: var(--pa-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .el-tag {
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: var(--pa-mono);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .el-dialog,
|
||||||
|
html.paranoia .el-message-box {
|
||||||
|
border: 1px solid var(--pa-line);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .el-dialog__header {
|
||||||
|
border-bottom: 1px solid var(--pa-line-soft);
|
||||||
|
margin-right: 0;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .el-alert {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--pa-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .el-empty__description {
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .el-loading-mask {
|
||||||
|
background-color: var(--pa-veil);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 键盘焦点环改用石蒜红 */
|
||||||
|
html.paranoia :focus-visible {
|
||||||
|
outline: 2px solid var(--gb-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 滚动条 ---------- */
|
||||||
|
|
||||||
|
html.paranoia ::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(224 36 94 / 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgb(224 36 94 / 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
主题通用零件
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
/* 章节记号:b(坠落之章)/ #(上升之章) */
|
||||||
|
.pa-mark {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-family: var(--pa-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border: 1px solid var(--pa-line);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-mark em {
|
||||||
|
font-style: normal;
|
||||||
|
color: var(--gb-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-mark--plain {
|
||||||
|
border-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 竖排的幽灵字,用作背景里的「妄想」 */
|
||||||
|
.pa-ghost-text {
|
||||||
|
position: absolute;
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
font-family: var(--pa-serif);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--pa-ghost);
|
||||||
|
letter-spacing: 0.3em;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 细红分割线 */
|
||||||
|
.pa-rule {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: var(--pa-line-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-rule::before,
|
||||||
|
.pa-rule::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--pa-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 引文块 */
|
||||||
|
.pa-quote {
|
||||||
|
font-family: var(--pa-serif);
|
||||||
|
line-height: 2;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
border-left: 2px solid var(--gb-primary);
|
||||||
|
padding: 2px 0 2px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 胶片感的图片:统一降饱和,hover 时回血 */
|
||||||
|
.pa-film {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--gb-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-film img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
filter: saturate(0.72) contrast(1.06) brightness(0.94);
|
||||||
|
transition:
|
||||||
|
transform 0.6s var(--gb-ease),
|
||||||
|
filter 0.6s var(--gb-ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-film::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, transparent 45%, rgb(8 6 10 / 0.78) 100%),
|
||||||
|
radial-gradient(120% 90% at 50% 10%, transparent 40%, rgb(224 36 94 / 0.16) 100%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-film:hover img {
|
||||||
|
transform: scale(1.045);
|
||||||
|
filter: saturate(1) contrast(1.04) brightness(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 背景层 ---------- */
|
||||||
|
|
||||||
|
.pa-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 噪点:让大面积纯色不至于太平 */
|
||||||
|
.pa-backdrop__grain {
|
||||||
|
position: absolute;
|
||||||
|
inset: -40px;
|
||||||
|
opacity: var(--pa-grain-opacity);
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗角 */
|
||||||
|
.pa-backdrop__vignette {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(130% 100% at 50% 40%, transparent 42%, rgb(6 4 9 / 0.5) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia:not(.dark) .pa-backdrop__vignette {
|
||||||
|
background: radial-gradient(130% 100% at 50% 40%, transparent 55%, rgb(120 60 80 / 0.16) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 缓慢下移的扫描光带 */
|
||||||
|
.pa-backdrop__scan {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 34vh;
|
||||||
|
background: linear-gradient(180deg, transparent, rgb(224 36 94 / 0.07) 45%, transparent);
|
||||||
|
animation: pa-scan 26s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pa-scan {
|
||||||
|
0% {
|
||||||
|
transform: translate3d(0, -40vh, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate3d(0, 120vh, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 石蒜花瓣 */
|
||||||
|
.pa-backdrop__petals {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-petal {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: var(--pa-x, 50%);
|
||||||
|
width: calc(14px * var(--pa-scale, 1));
|
||||||
|
height: calc(14px * var(--pa-scale, 1));
|
||||||
|
opacity: 0;
|
||||||
|
animation: pa-fall var(--pa-dur, 22s) linear var(--pa-delay, 0s) infinite;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-petal svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
animation: pa-spin calc(var(--pa-dur, 22s) / 5) ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深色底上的石蒜红要提亮一点才看得见 */
|
||||||
|
html.paranoia.dark .pa-petal svg {
|
||||||
|
filter: brightness(1.5) saturate(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pa-fall {
|
||||||
|
0% {
|
||||||
|
transform: translate3d(0, -14vh, 0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
opacity: var(--pa-opacity, 0.5);
|
||||||
|
}
|
||||||
|
85% {
|
||||||
|
opacity: var(--pa-opacity, 0.5);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate3d(var(--pa-drift, 40px), 112vh, 0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pa-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(-24deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(24deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 呼吸的红色光晕,浮在角落 */
|
||||||
|
.pa-orb {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(70px);
|
||||||
|
animation: pa-breathe 16s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pa-breathe {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.42;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: scale(1.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 皮肤切换时强制让新画面浮到最上层 ---------- */
|
||||||
|
|
||||||
|
html.vt-skin::view-transition-old(root) {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.vt-skin::view-transition-new(root) {
|
||||||
|
z-index: 2147483646;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 不支持 View Transitions 的浏览器退化成整页淡变 */
|
||||||
|
html.no-vt.paranoia body,
|
||||||
|
html.no-vt.paranoia .el-card,
|
||||||
|
html.no-vt.paranoia .el-table tr,
|
||||||
|
html.no-vt.paranoia .aside,
|
||||||
|
html.no-vt.paranoia .header,
|
||||||
|
html.no-vt.paranoia .nav-item {
|
||||||
|
transition:
|
||||||
|
background-color 0.36s var(--gb-ease),
|
||||||
|
border-color 0.36s var(--gb-ease),
|
||||||
|
color 0.36s var(--gb-ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 响应式 ---------- */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* 小屏只留四片花瓣,省电也省注意力 */
|
||||||
|
.pa-backdrop__petals .pa-petal:nth-child(n + 5) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .page-header h2 {
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.pa-petal,
|
||||||
|
.pa-backdrop__scan,
|
||||||
|
.pa-orb {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-petal {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 妄想症主题的背景层:噪点、暗角、缓慢下移的扫描光带,
|
||||||
|
* 以及像石蒜花瓣一样飘落的色片。
|
||||||
|
*
|
||||||
|
* 数值全部由下标推出来,刷新页面时不会跳到别的位置;
|
||||||
|
* 小屏与「减少动效」偏好下会由 CSS 自动收敛。
|
||||||
|
*/
|
||||||
|
const PETAL_COUNT = 14
|
||||||
|
|
||||||
|
const petals = Array.from({ length: PETAL_COUNT }, (_, i) => {
|
||||||
|
const r1 = ((i * 97) % 100) / 100
|
||||||
|
const r2 = ((i * 53 + 17) % 100) / 100
|
||||||
|
const r3 = ((i * 37 + 11) % 100) / 100
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: i,
|
||||||
|
x: `${((i * 7.3 + 3.5) % 100).toFixed(2)}%`,
|
||||||
|
drift: `${Math.round((r1 - 0.5) * 170)}px`,
|
||||||
|
dur: `${(18 + r2 * 16).toFixed(1)}s`,
|
||||||
|
delay: `${(-i * 1.9).toFixed(1)}s`,
|
||||||
|
scale: (0.55 + r3 * 1.05).toFixed(2),
|
||||||
|
opacity: (0.32 + r1 * 0.36).toFixed(2),
|
||||||
|
color: i % 5 === 0 ? 'var(--pa-blue)' : i % 7 === 0 ? 'var(--pa-cyan)' : 'var(--pa-crimson)',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="pa-backdrop" aria-hidden="true">
|
||||||
|
<div
|
||||||
|
class="pa-orb"
|
||||||
|
style="
|
||||||
|
left: -6%;
|
||||||
|
top: 12%;
|
||||||
|
width: 42vw;
|
||||||
|
height: 42vw;
|
||||||
|
background: rgb(224 36 94 / 0.22);
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="pa-orb"
|
||||||
|
style="
|
||||||
|
right: -8%;
|
||||||
|
bottom: 4%;
|
||||||
|
width: 38vw;
|
||||||
|
height: 38vw;
|
||||||
|
background: rgb(102 204 255 / 0.14);
|
||||||
|
animation-delay: -6s;
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="pa-backdrop__scan" />
|
||||||
|
<div class="pa-backdrop__grain" />
|
||||||
|
|
||||||
|
<span class="pa-ghost-text" style="right: 3%; top: 8%; font-size: 92px">妄想</span>
|
||||||
|
<span class="pa-ghost-text" style="left: 1%; bottom: 6%; font-size: 58px">现实</span>
|
||||||
|
|
||||||
|
<div class="pa-backdrop__petals">
|
||||||
|
<span
|
||||||
|
v-for="p in petals"
|
||||||
|
:key="p.id"
|
||||||
|
class="pa-petal"
|
||||||
|
:style="{
|
||||||
|
'--pa-x': p.x,
|
||||||
|
'--pa-drift': p.drift,
|
||||||
|
'--pa-dur': p.dur,
|
||||||
|
'--pa-delay': p.delay,
|
||||||
|
'--pa-scale': p.scale,
|
||||||
|
'--pa-opacity': p.opacity,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M12 1.6c3.4 5.2 6.6 8.6 6.6 13.1 0 3.9-3 7.1-6.6 7.1S5.4 18.6 5.4 14.7C5.4 10.2 8.6 6.8 12 1.6z"
|
||||||
|
:fill="p.color"
|
||||||
|
/>
|
||||||
|
<path d="M12 3.6v16.4" stroke="rgb(0 0 0 / 0.22)" stroke-width="0.6" fill="none" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pa-backdrop__vignette" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import { Check, MagicStick } from '@element-plus/icons-vue'
|
||||||
|
import { skinOptions, useTheme, type ThemeSkin } from '@/composables/useTheme'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外观主题选择器。
|
||||||
|
*
|
||||||
|
* 用自绘的浮层而不是 el-popover:这样预览小样可以完全跟着皮肤走,
|
||||||
|
* 也不必和 teleport 出来的 popper 抢样式。
|
||||||
|
*/
|
||||||
|
const { skin, isDark, setSkin } = useTheme()
|
||||||
|
|
||||||
|
const open = ref(false)
|
||||||
|
const root = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const currentName = computed(() => skinOptions.find((o) => o.id === skin.value)?.name ?? '')
|
||||||
|
|
||||||
|
/** 每个皮肤的迷你预览配色,跟随当前明暗模式 */
|
||||||
|
function previewOf(id: ThemeSkin) {
|
||||||
|
const paranoia = id === 'paranoia'
|
||||||
|
if (isDark.value) {
|
||||||
|
return paranoia
|
||||||
|
? { bg: '#0d0a11', side: '#1d0f1b', card: '#150f19', accent: '#e0245e', line: '#3a2130' }
|
||||||
|
: { bg: '#101216', side: '#17191f', card: '#1a1d23', accent: '#66ccff', line: '#2b303a' }
|
||||||
|
}
|
||||||
|
return paranoia
|
||||||
|
? { bg: '#f5f1ee', side: '#241a22', card: '#fffdfc', accent: '#b3163c', line: '#e4d3d8' }
|
||||||
|
: { bg: '#f4f6f8', side: '#232630', card: '#ffffff', accent: '#3fa9e0', line: '#dde3e9' }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pick(id: ThemeSkin, event: MouseEvent) {
|
||||||
|
open.value = false
|
||||||
|
if (id === skin.value) return
|
||||||
|
await setSkin(id, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDocumentClick(event: MouseEvent) {
|
||||||
|
if (root.value && !root.value.contains(event.target as Node)) {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') open.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', onDocumentClick, true)
|
||||||
|
document.addEventListener('keydown', onKeydown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', onDocumentClick, true)
|
||||||
|
document.removeEventListener('keydown', onKeydown)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="root" class="skin-picker">
|
||||||
|
<button
|
||||||
|
class="icon-btn skin-btn"
|
||||||
|
type="button"
|
||||||
|
:aria-expanded="open"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
:title="`外观主题:${currentName}`"
|
||||||
|
@click.stop="open = !open"
|
||||||
|
>
|
||||||
|
<el-icon :size="17"><MagicStick /></el-icon>
|
||||||
|
<span class="skin-btn__pip" :class="skin" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Transition name="skin-pop">
|
||||||
|
<div v-if="open" class="skin-panel" role="dialog" aria-label="外观主题">
|
||||||
|
<div class="skin-panel__head">
|
||||||
|
<span class="skin-panel__title">外观主题</span>
|
||||||
|
<span class="skin-panel__hint">Appearance</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-for="option in skinOptions"
|
||||||
|
:key="option.id"
|
||||||
|
type="button"
|
||||||
|
class="skin-option"
|
||||||
|
:class="{ 'is-active': option.id === skin }"
|
||||||
|
@click.stop="pick(option.id, $event)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="skin-option__preview"
|
||||||
|
:style="{
|
||||||
|
background: previewOf(option.id).bg,
|
||||||
|
borderColor: previewOf(option.id).line,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<i class="skin-option__side" :style="{ background: previewOf(option.id).side }" />
|
||||||
|
<i class="skin-option__bar" :style="{ background: previewOf(option.id).accent }" />
|
||||||
|
<i
|
||||||
|
class="skin-option__card"
|
||||||
|
:style="{
|
||||||
|
background: previewOf(option.id).card,
|
||||||
|
borderColor: previewOf(option.id).line,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
class="skin-option__card skin-option__card--alt"
|
||||||
|
:style="{
|
||||||
|
background: previewOf(option.id).card,
|
||||||
|
borderColor: previewOf(option.id).line,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="skin-option__meta">
|
||||||
|
<span class="skin-option__name">
|
||||||
|
{{ option.name }}
|
||||||
|
<el-icon v-if="option.id === skin" :size="14" class="skin-option__check">
|
||||||
|
<Check />
|
||||||
|
</el-icon>
|
||||||
|
</span>
|
||||||
|
<span class="skin-option__sub">{{ option.sub }}</span>
|
||||||
|
<span class="skin-option__swatches">
|
||||||
|
<i v-for="color in option.swatches" :key="color" :style="{ background: color }" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="skin-panel__foot">
|
||||||
|
<span>暗色模式由顶栏的月亮按钮切换</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.skin-picker {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-btn {
|
||||||
|
position: relative;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s var(--gb-ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-btn:hover {
|
||||||
|
background: var(--el-fill-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-btn__pip {
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--gb-primary);
|
||||||
|
box-shadow: 0 0 0 2px var(--gb-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-btn__pip.paranoia {
|
||||||
|
background: #e0245e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 浮层 ---------- */
|
||||||
|
|
||||||
|
.skin-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 10px);
|
||||||
|
right: 0;
|
||||||
|
z-index: 1600;
|
||||||
|
width: 296px;
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--el-bg-color-overlay);
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 18px 44px rgb(16 12 24 / 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .skin-panel {
|
||||||
|
border-radius: 6px;
|
||||||
|
border-color: var(--pa-line);
|
||||||
|
box-shadow:
|
||||||
|
0 18px 44px rgb(0 0 0 / 0.4),
|
||||||
|
0 0 0 1px rgb(224 36 94 / 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-panel__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-panel__title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .skin-panel__title {
|
||||||
|
font-family: var(--pa-serif);
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-panel__hint {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 9px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: transparent;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background 0.2s var(--gb-ease),
|
||||||
|
border-color 0.2s var(--gb-ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .skin-option {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-option:hover {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-option.is-active {
|
||||||
|
border-color: var(--gb-primary);
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 迷你预览 ---------- */
|
||||||
|
|
||||||
|
.skin-option__preview {
|
||||||
|
position: relative;
|
||||||
|
flex: none;
|
||||||
|
width: 62px;
|
||||||
|
height: 44px;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.paranoia .skin-option__preview {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-option__side {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-option__bar {
|
||||||
|
position: absolute;
|
||||||
|
left: 15px;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 7px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-option__card {
|
||||||
|
position: absolute;
|
||||||
|
left: 22px;
|
||||||
|
top: 13px;
|
||||||
|
width: 32px;
|
||||||
|
height: 11px;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-option__card--alt {
|
||||||
|
top: 28px;
|
||||||
|
width: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-option__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-option__name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-option__check {
|
||||||
|
color: var(--gb-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-option__sub {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-option__swatches {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-option__swatches i {
|
||||||
|
width: 14px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-panel__foot {
|
||||||
|
padding: 6px 2px 0;
|
||||||
|
border-top: 1px solid var(--el-border-color-extra-light);
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 浮层进出场 ---------- */
|
||||||
|
|
||||||
|
.skin-pop-enter-active,
|
||||||
|
.skin-pop-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.18s var(--gb-ease),
|
||||||
|
transform 0.22s var(--gb-ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-pop-enter-from,
|
||||||
|
.skin-pop-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-6px) scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.skin-panel {
|
||||||
|
width: min(84vw, 296px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,23 +1,85 @@
|
|||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
const STORAGE_KEY = 'gb-theme'
|
/** 明暗模式的存储键 */
|
||||||
|
const DARK_STORAGE_KEY = 'gb-theme'
|
||||||
|
/** 外观皮肤(主题)的存储键 */
|
||||||
|
const SKIN_STORAGE_KEY = 'gb-skin'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可选皮肤:
|
||||||
|
* - default:原有的晴空配色
|
||||||
|
* - paranoia:致敬《妄想症 Paranoia》系列曲的外观主题
|
||||||
|
*/
|
||||||
|
export type ThemeSkin = 'default' | 'paranoia'
|
||||||
|
|
||||||
|
export interface SkinOption {
|
||||||
|
id: ThemeSkin
|
||||||
|
/** 皮肤名 */
|
||||||
|
name: string
|
||||||
|
/** 一句话说明 */
|
||||||
|
sub: string
|
||||||
|
/** 预览用的色板 */
|
||||||
|
swatches: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const skinOptions: SkinOption[] = [
|
||||||
|
{
|
||||||
|
id: 'default',
|
||||||
|
name: '晴空',
|
||||||
|
sub: 'goodBaby 默认外观',
|
||||||
|
swatches: ['#66ccff', '#3fa9e0', '#232630'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'paranoia',
|
||||||
|
name: '妄想症',
|
||||||
|
sub: 'Paranoia · 九重档案',
|
||||||
|
swatches: ['#e0245e', '#7ee8e0', '#f2d16b'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
/** 全局共享的主题状态,模块级单例,任何组件读到的都是同一份 */
|
/** 全局共享的主题状态,模块级单例,任何组件读到的都是同一份 */
|
||||||
const isDark = ref(false)
|
const isDark = ref(false)
|
||||||
|
const skin = ref<ThemeSkin>('default')
|
||||||
|
const isParanoia = computed(() => skin.value === 'paranoia')
|
||||||
|
|
||||||
/** 把主题应用到 <html> 上 */
|
function readStorage(key: string): string | null {
|
||||||
function applyTheme(dark: boolean) {
|
try {
|
||||||
|
return localStorage.getItem(key)
|
||||||
|
} catch {
|
||||||
|
// 隐私模式 / 禁用存储时静默降级为「本次会话内有效」
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeStorage(key: string, value: string) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, value)
|
||||||
|
} catch {
|
||||||
|
// 同上
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把明暗模式应用到 <html> 上 */
|
||||||
|
function applyDark(dark: boolean) {
|
||||||
document.documentElement.classList.toggle('dark', dark)
|
document.documentElement.classList.toggle('dark', dark)
|
||||||
isDark.value = dark
|
isDark.value = dark
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 把皮肤应用到 <html> 上,配色覆盖写在 theme-paranoia.css 里 */
|
||||||
|
function applySkin(next: ThemeSkin) {
|
||||||
|
document.documentElement.classList.toggle('paranoia', next === 'paranoia')
|
||||||
|
skin.value = next
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在应用启动时调用一次:恢复上次选择,没有记录时跟随系统。
|
* 在应用启动时调用一次:恢复上次选择,没有记录时跟随系统。
|
||||||
*/
|
*/
|
||||||
export function initTheme() {
|
export function initTheme() {
|
||||||
const saved = localStorage.getItem(STORAGE_KEY)
|
const savedDark = readStorage(DARK_STORAGE_KEY)
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
applyTheme(saved ? saved === 'dark' : prefersDark)
|
applyDark(savedDark ? savedDark === 'dark' : prefersDark)
|
||||||
|
|
||||||
|
applySkin(readStorage(SKIN_STORAGE_KEY) === 'paranoia' ? 'paranoia' : 'default')
|
||||||
|
|
||||||
// 标记浏览器是否支持圆形揭示,供 CSS 决定要不要退化成淡变
|
// 标记浏览器是否支持圆形揭示,供 CSS 决定要不要退化成淡变
|
||||||
if (typeof document.startViewTransition !== 'function') {
|
if (typeof document.startViewTransition !== 'function') {
|
||||||
@@ -26,36 +88,41 @@ export function initTheme() {
|
|||||||
|
|
||||||
// 用户没有手动选择过时,跟随系统变化
|
// 用户没有手动选择过时,跟随系统变化
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||||
if (!localStorage.getItem(STORAGE_KEY)) {
|
if (!readStorage(DARK_STORAGE_KEY)) {
|
||||||
applyTheme(e.matches)
|
applyDark(e.matches)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTheme() {
|
|
||||||
/**
|
/**
|
||||||
* 切换明暗主题。
|
* 用一次圆形揭示动画包住外观切换。
|
||||||
*
|
*
|
||||||
* 支持 View Transitions 时,以点击处为圆心做一次圆形揭示动画;
|
* 支持 View Transitions 时以点击处为圆心做圆形揭示;不支持(Firefox / Safari 旧版)
|
||||||
* 不支持(Firefox / Safari 旧版)或用户要求减少动效时直接切换。
|
* 或用户要求减少动效时直接切换。
|
||||||
|
*
|
||||||
|
* @param mutate 真正改变外观状态的函数
|
||||||
|
* @param event 触发切换的点击事件,用来决定圆心
|
||||||
|
* @param mode dark/light 沿用原有的进/退方向;skin 需要额外把新画面提到最上层
|
||||||
*/
|
*/
|
||||||
async function toggleTheme(event?: MouseEvent) {
|
async function reveal(
|
||||||
const next = !isDark.value
|
mutate: () => void,
|
||||||
const persist = () => {
|
event: MouseEvent | undefined,
|
||||||
applyTheme(next)
|
mode: 'dark' | 'light' | 'skin',
|
||||||
localStorage.setItem(STORAGE_KEY, next ? 'dark' : 'light')
|
) {
|
||||||
}
|
|
||||||
|
|
||||||
// 绑定后再判空,TS 才能正确收窄这个可选 API
|
|
||||||
const startViewTransition = document.startViewTransition?.bind(document)
|
const startViewTransition = document.startViewTransition?.bind(document)
|
||||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||||
|
|
||||||
if (!startViewTransition || reduceMotion) {
|
if (!startViewTransition || reduceMotion) {
|
||||||
persist()
|
mutate()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 以点击点为圆心;没有事件对象时从右上角(切换按钮所在位置)扩散
|
// 换皮肤时新旧画面可能同为暗色,靠这个类强制让新画面浮到上面
|
||||||
|
if (mode === 'skin') {
|
||||||
|
document.documentElement.classList.add('vt-skin')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从点击点向外扩散;没有事件对象时从右上角(切换按钮所在位置)扩散
|
||||||
const x = event?.clientX ?? window.innerWidth - 60
|
const x = event?.clientX ?? window.innerWidth - 60
|
||||||
const y = event?.clientY ?? 30
|
const y = event?.clientY ?? 30
|
||||||
// 半径取到最远的那个角,保证能覆盖整个视口
|
// 半径取到最远的那个角,保证能覆盖整个视口
|
||||||
@@ -64,28 +131,70 @@ export function useTheme() {
|
|||||||
Math.max(y, window.innerHeight - y),
|
Math.max(y, window.innerHeight - y),
|
||||||
)
|
)
|
||||||
|
|
||||||
const transition = startViewTransition(persist)
|
const transition = startViewTransition(mutate)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await transition.ready
|
await transition.ready
|
||||||
} catch {
|
} catch {
|
||||||
// 浏览器跳过了这次过渡(页面不可见、连续快速点击等)。
|
// 浏览器跳过了这次过渡(页面不可见、连续快速点击等)。
|
||||||
// 此时主题已经通过 persist() 应用好了,只是没有揭示动画,直接返回。
|
// 此时外观已由 mutate 应用好,只是没有揭示动画。
|
||||||
|
document.documentElement.classList.remove('vt-skin')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`]
|
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`]
|
||||||
|
|
||||||
document.documentElement.animate(
|
|
||||||
// 切到深色时让新画面扩散进来;切回浅色时让旧画面收缩出去
|
// 切到深色时让新画面扩散进来;切回浅色时让旧画面收缩出去
|
||||||
{ clipPath: next ? clipPath : [...clipPath].reverse() },
|
const pseudoElement =
|
||||||
|
mode === 'light' ? '::view-transition-old(root)' : '::view-transition-new(root)'
|
||||||
|
|
||||||
|
document.documentElement.animate(
|
||||||
|
{ clipPath: mode === 'light' ? [...clipPath].reverse() : clipPath },
|
||||||
{
|
{
|
||||||
duration: 480,
|
duration: 480,
|
||||||
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
pseudoElement: next ? '::view-transition-new(root)' : '::view-transition-old(root)',
|
pseudoElement,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await transition.finished
|
||||||
|
} catch {
|
||||||
|
// finished 在过渡被跳过时也会抛错,忽略即可
|
||||||
|
}
|
||||||
|
document.documentElement.classList.remove('vt-skin')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
/**
|
||||||
|
* 切换明暗主题。
|
||||||
|
*/
|
||||||
|
async function toggleTheme(event?: MouseEvent) {
|
||||||
|
const next = !isDark.value
|
||||||
|
await reveal(
|
||||||
|
() => {
|
||||||
|
applyDark(next)
|
||||||
|
writeStorage(DARK_STORAGE_KEY, next ? 'dark' : 'light')
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
next ? 'dark' : 'light',
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isDark, toggleTheme }
|
/**
|
||||||
|
* 切换外观皮肤。传入点击事件时以点击处为圆心做揭示动画。
|
||||||
|
*/
|
||||||
|
async function setSkin(next: ThemeSkin, event?: MouseEvent) {
|
||||||
|
if (next === skin.value) return
|
||||||
|
await reveal(
|
||||||
|
() => {
|
||||||
|
applySkin(next)
|
||||||
|
writeStorage(SKIN_STORAGE_KEY, next)
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
'skin',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isDark, isParanoia, skin, toggleTheme, setSkin }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,456 @@
|
|||||||
|
/**
|
||||||
|
* 《妄想症 Paranoia》系列资料
|
||||||
|
*
|
||||||
|
* 致敬对象:DELA 作曲 / 雨狸作词 / Lune 等人曲绘的 VOCALOID 中文原创系列曲
|
||||||
|
* (2015.11.28《一重加害》— 2017.03.17《九重现实》,共 475 天;
|
||||||
|
* 2025.11.28 发布十周年纪念曲《十重告别》)。
|
||||||
|
*
|
||||||
|
* 曲绘、歌词与设定的著作权均归原作者所有,此处仅作主题化的介绍与引用。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ch01 from '@/assets/paranoia/ch01.jpg'
|
||||||
|
import ch02 from '@/assets/paranoia/ch02.jpg'
|
||||||
|
import ch03 from '@/assets/paranoia/ch03.jpg'
|
||||||
|
import ch04 from '@/assets/paranoia/ch04.jpg'
|
||||||
|
import ch05 from '@/assets/paranoia/ch05.jpg'
|
||||||
|
import ch06 from '@/assets/paranoia/ch06.jpg'
|
||||||
|
import ch07 from '@/assets/paranoia/ch07.jpg'
|
||||||
|
import ch08 from '@/assets/paranoia/ch08.jpg'
|
||||||
|
import ch09 from '@/assets/paranoia/ch09.jpg'
|
||||||
|
import ch10 from '@/assets/paranoia/ch10.jpg'
|
||||||
|
|
||||||
|
/** 章:乐曲在系列中的位置 */
|
||||||
|
export type ParanoiaArc = '序曲' | '坠落之章' | '上升之章' | '十周年'
|
||||||
|
|
||||||
|
export interface ParanoiaChapter {
|
||||||
|
/** 重数,0 表示序曲 */
|
||||||
|
no: number
|
||||||
|
/** 中文数字,如「一」 */
|
||||||
|
numeral: string
|
||||||
|
/** 曲名 */
|
||||||
|
title: string
|
||||||
|
arc: ParanoiaArc
|
||||||
|
/** 乐谱记号:坠落之章为 b(降号),上升之章为 #(升号) */
|
||||||
|
sigil: string
|
||||||
|
/** 投稿日期 */
|
||||||
|
date: string
|
||||||
|
/** 演唱 */
|
||||||
|
singers: string
|
||||||
|
/** 视角角色 */
|
||||||
|
voices: string
|
||||||
|
/** 一句话剧情 */
|
||||||
|
summary: string
|
||||||
|
/** 章节简介 */
|
||||||
|
detail: string
|
||||||
|
/** 歌词摘录 */
|
||||||
|
lyric: string
|
||||||
|
/** 对应的妄想症条目 */
|
||||||
|
delusion: string
|
||||||
|
/** 临床描述 */
|
||||||
|
note: string
|
||||||
|
image: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 三重加害者、守护者、背叛者与第四面墙外之人 */
|
||||||
|
export interface ParanoiaCharacter {
|
||||||
|
name: string
|
||||||
|
/** 虚拟歌手 */
|
||||||
|
vocal: string
|
||||||
|
role: string
|
||||||
|
/** 主题色 */
|
||||||
|
color: string
|
||||||
|
/** 第二主题色,用于言语这种双重身份的角色 */
|
||||||
|
colorAlt?: string
|
||||||
|
/** 一句属于她的话 */
|
||||||
|
line: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 妄想档案:系列中逐章出现的临床条目 */
|
||||||
|
export interface ParanoiaDelusion {
|
||||||
|
index: string
|
||||||
|
term: string
|
||||||
|
alias: string
|
||||||
|
note: string
|
||||||
|
chapter: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const paranoiaChapters: ParanoiaChapter[] = [
|
||||||
|
{
|
||||||
|
no: 1,
|
||||||
|
numeral: '一',
|
||||||
|
title: '一重加害',
|
||||||
|
arc: '坠落之章',
|
||||||
|
sigil: 'b',
|
||||||
|
date: '2015.11.28',
|
||||||
|
singers: '洛天依 / 言和',
|
||||||
|
voices: '被害者泠珞 · 加害者颜语',
|
||||||
|
summary: '熟悉的街道上多了一个影子,被害妄想从此降临。',
|
||||||
|
detail:
|
||||||
|
'系列的开篇。以被害妄想为主题,由被害者泠珞、加害者颜语与背叛者零羽三人展开,剧情扑朔迷离,在留下巨大悬念的同时,也为最终的结局埋下伏笔。',
|
||||||
|
lyric: '别对我说 你身后是谁在揣度\n别靠近我 你这无锁链的恶魔',
|
||||||
|
delusion: '被害妄想',
|
||||||
|
note: '患者往往处于恐惧状态而胡乱推理和分析,坚持自己受到迫害或伤害,如言语上的针对、嘲弄、跟踪及监听等。',
|
||||||
|
image: ch01,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 2,
|
||||||
|
numeral: '二',
|
||||||
|
title: '二重变革',
|
||||||
|
arc: '坠落之章',
|
||||||
|
sigil: 'b',
|
||||||
|
date: '2015.12.12',
|
||||||
|
singers: '洛天依 / 言和',
|
||||||
|
voices: '被害者泠珞 · 加害者颜语',
|
||||||
|
summary: '凶手遵从风声鹤唳者的妄想而来,自虚无中诞生。',
|
||||||
|
detail:
|
||||||
|
'第一作的故事后续与不同视角描述。被害者与加害者之间的斗争正式开始:「我正是接受你『会被杀死』的强烈夙愿而自虚无诞生。」',
|
||||||
|
lyric: '这一重任扭曲泛滥的世界\n它可悲的明天就由我来终结',
|
||||||
|
delusion: '影响妄想 · 逆',
|
||||||
|
note: '患者认为自己的思维、情感与意志行为可以影响并支配外界的现实,即「心想事成」。',
|
||||||
|
image: ch02,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 3,
|
||||||
|
numeral: '三',
|
||||||
|
title: '三重爱恋',
|
||||||
|
arc: '坠落之章',
|
||||||
|
sigil: 'b',
|
||||||
|
date: '2016.01.22',
|
||||||
|
singers: '洛天依 / 言和',
|
||||||
|
voices: '被害者泠珞 · 守护者颜语 · 潜意识泠珞',
|
||||||
|
summary: '杀死幻想怪物之后,她被裁定防卫过当,于是开始了一场妄想中的恋爱。',
|
||||||
|
detail:
|
||||||
|
'盲目生活像陀螺一般旋转不停,动荡的视线引发不安。在梦魇深堕之处,她开始相信自己终于遇到了那个人。',
|
||||||
|
lyric: '约会在吊死鬼开的乐园\n看糖果和孤独成就了饕餮',
|
||||||
|
delusion: '情爱妄想',
|
||||||
|
note: '又称 de Clerambault 症候群。病人会以为自己正和某人恋爱,或另一个人深爱着自己;幻想中的恋人甚至只是一个「魅影」。',
|
||||||
|
image: ch03,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 4,
|
||||||
|
numeral: '四',
|
||||||
|
title: '四重罪孽',
|
||||||
|
arc: '坠落之章',
|
||||||
|
sigil: 'b',
|
||||||
|
date: '2016.03.26',
|
||||||
|
singers: '洛天依 / 言和 / 乐正绫',
|
||||||
|
voices: '潜意识泠珞 · 守护者颜语 · 背叛者零羽',
|
||||||
|
summary: '潜意识泠珞觉醒,察觉一切皆妄想,并宣告自己犯下的重重罪孽。',
|
||||||
|
detail:
|
||||||
|
'表现挣扎的一章。加害者的复活、守护者准备为爱牺牲的觉悟、背叛者扑朔迷离的死因,都替之后的剧情留下伏笔。',
|
||||||
|
lyric: '我犯下了重重罪孽 我到底是什么存在\n我是最恐怖的危害 能否从此逃开',
|
||||||
|
delusion: '自罪妄想',
|
||||||
|
note: '又称罪恶妄想。患者毫无根据地认为自己犯了严重错误和罪行,或将一件小事的错误无限夸大,以至于拒食或要求赎罪。',
|
||||||
|
image: ch04,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 5,
|
||||||
|
numeral: '五',
|
||||||
|
title: '五重空洞',
|
||||||
|
arc: '坠落之章',
|
||||||
|
sigil: 'b',
|
||||||
|
date: '2016.04.28',
|
||||||
|
singers: '洛天依 / 乐正绫',
|
||||||
|
voices: '潜意识泠珞 · 背叛者零羽',
|
||||||
|
summary: '妄想世界的夹缝里,彼岸花田中埋葬着背叛者零羽。',
|
||||||
|
detail:
|
||||||
|
'潜意识泠珞在彼岸花田中见到了埋葬在记忆深处的零羽,过去的种种记忆漫上心头。故事的真相揭起一角,过去与未来依旧扑朔迷离。',
|
||||||
|
lyric: '你停留在我无法追逐的记忆彼岸\n永远不会再回来',
|
||||||
|
delusion: '内心被揭露感',
|
||||||
|
note: '又称被洞悉感。患者认为其内心的想法未经表达,别人就知道了;严重时表层意识可能被穿透,导致潜意识暴露在外。',
|
||||||
|
image: ch05,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 6,
|
||||||
|
numeral: '六',
|
||||||
|
title: '六重不忠',
|
||||||
|
arc: '上升之章',
|
||||||
|
sigil: '#',
|
||||||
|
date: '2016.08.05',
|
||||||
|
singers: '洛天依 / 言和 / 乐正绫',
|
||||||
|
voices: '加害者 · 守护者 · 潜意识泠珞',
|
||||||
|
summary: '守护者先被策反,又因誓言死在加害者刀下,系列由此转入上升之章。',
|
||||||
|
detail:
|
||||||
|
'从本曲开始,系列进入 # 之章(上升之章)。她先期待、她先许愿,身为镜像怎能妥协;而誓言与爱,最终以死亡兑现。',
|
||||||
|
lyric: '再见吧终会相见 莫为此放弃明天\n在绝望之前 将我的誓言默念',
|
||||||
|
delusion: '嫉妒妄想综合征',
|
||||||
|
note: '又称奥赛罗综合征。患者坚信所爱之人不忠,即使有确切证据证明其并未背叛,怀疑的依据也捕风捉影。',
|
||||||
|
image: ch06,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 7,
|
||||||
|
numeral: '七',
|
||||||
|
title: '七重痼病',
|
||||||
|
arc: '上升之章',
|
||||||
|
sigil: '#',
|
||||||
|
date: '2016.12.16',
|
||||||
|
singers: '洛天依 / 言和 / 乐正绫',
|
||||||
|
voices: '被害者泠珞 · 加害者颜语 · 零羽',
|
||||||
|
summary: '加害者与潜意识同归于尽,她在白日的中心失去焦距。',
|
||||||
|
detail:
|
||||||
|
'第六作的后续。红石蒜在无人之地盛开,妄议着未来于是溃败。「如果谎言重复了千遍依然无法成为现实,那谎言应该如何生存下去呢?」',
|
||||||
|
lyric: '世界已发出信号 停止临终前无止境祈祷\n我接受她不在的未来',
|
||||||
|
delusion: '疑病妄想',
|
||||||
|
note: '患者毫无根据地坚信自己患了某种严重躯体疾病或不治之症。严重时称为虚无幻想——「本人已经不存在,只剩下一个躯壳空壳了」。',
|
||||||
|
image: ch07,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 8,
|
||||||
|
numeral: '八',
|
||||||
|
title: '八重回归',
|
||||||
|
arc: '上升之章',
|
||||||
|
sigil: '#',
|
||||||
|
date: '2017.03.10',
|
||||||
|
singers: '洛天依 / 乐正绫',
|
||||||
|
voices: '泠珞 · 零羽',
|
||||||
|
summary: '一切妄想破灭之后回归现实,同时与过往和梦想告别。',
|
||||||
|
detail:
|
||||||
|
'睁开眼重新堕入平凡的世界,辗转枯燥的时间一天又一天。所有羁绊终归于平淡,誓言悉数臣服现实的安排——「无意义乃这个世界存在的唯一意义。」',
|
||||||
|
lyric: '再见 再不见\n无比真实过的一切',
|
||||||
|
delusion: '回归空白',
|
||||||
|
note: '现实本身就需要和妄想区分开来。越痛苦,翅膀越充血,它们就越美丽——然后死亡就突然降临,只是轻飘飘地、轻飘飘地。',
|
||||||
|
image: ch08,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 9,
|
||||||
|
numeral: '九',
|
||||||
|
title: '九重现实',
|
||||||
|
arc: '上升之章',
|
||||||
|
sigil: '#',
|
||||||
|
date: '2017.03.17',
|
||||||
|
singers: '洛天依 / 乐正绫 · 言和和声',
|
||||||
|
voices: '泠珞 · 零羽',
|
||||||
|
summary: '完结作。故事的真正脉络,以及带着希望的未来。',
|
||||||
|
detail:
|
||||||
|
'从「与其生离,不如死别」到「面对凶险的今后」。不是自作多情,也不是阳光明媚,仅仅是空白的现实而已。白色,也许是可以着色的,也许是被擦除了色彩之后的。',
|
||||||
|
lyric: '你的声音击破梦魇一重一重\n自最遥远的彼方呼唤我',
|
||||||
|
delusion: '现实',
|
||||||
|
note: '「我所描述的一切或许并非真实,然而我的感受绝无虚假。」绝无虚假。',
|
||||||
|
image: ch09,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 10,
|
||||||
|
numeral: '十',
|
||||||
|
title: '十重告别',
|
||||||
|
arc: '十周年',
|
||||||
|
sigil: '✳',
|
||||||
|
date: '2025.11.28',
|
||||||
|
singers: '洛天依 / 言和 / 乐正绫 / 双狐座',
|
||||||
|
voices: '第四面墙外之人',
|
||||||
|
summary: '第四面墙外之人回望整个系列,把苦痛与欢愉收纳成歌颂明天的力量。',
|
||||||
|
detail:
|
||||||
|
'十周年纪念曲。打破第四面墙的元叙事视角,带领角色、创作者与听众共同回望这一切:告别或是全新的起点,全在他者的一念之间。',
|
||||||
|
lyric: '无论再见或再也不见 我们心声已然相遇见\n比十年更早之前 我便歌颂着明天',
|
||||||
|
delusion: '告别',
|
||||||
|
note: '载体或许会消失,但我们相遇过的回忆不会。无论是十年前还是十年后,都希望你能拥有属于自己的幸福。',
|
||||||
|
image: ch10,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const paranoiaCharacters: ParanoiaCharacter[] = [
|
||||||
|
{
|
||||||
|
name: '泠珞',
|
||||||
|
vocal: '洛天依',
|
||||||
|
role: '被害者',
|
||||||
|
color: '#66ccff',
|
||||||
|
line: '不承认那是命运,命运是被神抛弃的棋局。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '颜语',
|
||||||
|
vocal: '言和',
|
||||||
|
role: '加害者 / 守护者',
|
||||||
|
color: '#f2d16b',
|
||||||
|
colorAlt: '#7ee8e0',
|
||||||
|
line: '如果就此离开,还要怎么加害。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '零羽',
|
||||||
|
vocal: '乐正绫',
|
||||||
|
role: '背叛者',
|
||||||
|
color: '#e0245e',
|
||||||
|
line: '背对着我一跃而下的女孩,在呼唤。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '双狐座',
|
||||||
|
vocal: '第四面墙外之人',
|
||||||
|
role: '元叙事视角',
|
||||||
|
color: '#b98cff',
|
||||||
|
line: '告别或是全新的起点,全在他者的一念之间。',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const paranoiaDelusions: ParanoiaDelusion[] = [
|
||||||
|
{
|
||||||
|
index: '01',
|
||||||
|
term: '被害妄想',
|
||||||
|
alias: 'delusion of persecution',
|
||||||
|
note: '处于恐惧状态而胡乱推理和分析,坚持自己受到迫害或伤害,如言语上的针对、嘲弄、跟踪及监听。',
|
||||||
|
chapter: '一重加害',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '02',
|
||||||
|
term: '影响妄想',
|
||||||
|
alias: 'delusion of influence',
|
||||||
|
note: '认为自己的思维、情感、意志行为活动受到外界某种力量的支配、控制、操纵,不能自主。',
|
||||||
|
chapter: '二重变革',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '03',
|
||||||
|
term: '影响妄想 · 逆',
|
||||||
|
alias: 'reverse influence',
|
||||||
|
note: '认为自己的思维与意志不论多么荒谬,都可以影响并支配外界的现实,即「心想事成」。',
|
||||||
|
chapter: '二重变革',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '04',
|
||||||
|
term: '情爱妄想',
|
||||||
|
alias: "de Clerambault's syndrome",
|
||||||
|
note: '以为自己正和某人恋爱,或另一个人深爱着自己;幻想中的恋人甚至只是一个现实中并不存在的「魅影」。',
|
||||||
|
chapter: '三重爱恋',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '05',
|
||||||
|
term: '自罪妄想',
|
||||||
|
alias: 'delusion of guilt',
|
||||||
|
note: '又称罪恶妄想。毫无根据地认为自己犯了严重错误和罪行,罪大恶极、死有余辜,应该受到惩罚。',
|
||||||
|
chapter: '四重罪孽',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '06',
|
||||||
|
term: '内心被揭露感',
|
||||||
|
alias: 'experience of being revealed',
|
||||||
|
note: '又称被洞悉感。认为内心的想法未经表达别人就知道了;严重时表层意识被穿透,潜意识暴露在外。',
|
||||||
|
chapter: '五重空洞',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '07',
|
||||||
|
term: '投射性认同',
|
||||||
|
alias: 'projective identification',
|
||||||
|
note: '诱导他人以一种限定的方式行动或做出反应的人际行为模式,接受者往往被迫成为投射者感受的储存室。',
|
||||||
|
chapter: '五重空洞',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '08',
|
||||||
|
term: '嫉妒妄想',
|
||||||
|
alias: 'Othello syndrome',
|
||||||
|
note: '坚信所爱之人不贞,即使有确切证据也丝毫不动摇;思维推理荒谬,怀疑的依据捕风捉影。',
|
||||||
|
chapter: '六重不忠',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '09',
|
||||||
|
term: '疑病妄想',
|
||||||
|
alias: 'hypochondriacal delusion',
|
||||||
|
note: '毫无根据地坚信自己患了某种严重躯体疾病或不治之症,反复检查也不能纠正其歪曲的信念。',
|
||||||
|
chapter: '七重痼病',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '10',
|
||||||
|
term: '虚无幻想',
|
||||||
|
alias: 'nihilistic delusion',
|
||||||
|
note: '疑病妄想的重度形态:认为「本人已经不存在,只剩下一个躯壳空壳了」。',
|
||||||
|
chapter: '七重痼病',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 《九重现实》中对妄想的五条描述 */
|
||||||
|
export const paranoiaAxioms: string[] = [
|
||||||
|
'妄想是一个根本性障碍。',
|
||||||
|
'是慢性和终身频发的。',
|
||||||
|
'妄想有逻辑构造,并且内部成立。',
|
||||||
|
'妄想和普通逻辑推理不冲突,而且没有一般的行为异常。',
|
||||||
|
'自身经历了对自我参考的高度理解——对别人来说不重要的事件,对他们来说极为重要。',
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 主题化引文,用于顶栏滚动字幕与仪表盘签文 */
|
||||||
|
export const paranoiaQuotes: { text: string; from: string }[] = [
|
||||||
|
{ text: '谎言重复一千次就变成真理。所以,现实也是这样诞生的。', from: '《一重加害》' },
|
||||||
|
{ text: '梦魇中是谁背对着我纵身一跃,没说再见,也没有遗言。', from: '《一重加害》' },
|
||||||
|
{ text: '这一重任扭曲泛滥的世界,它可悲的明天就由我来终结。', from: '《二重变革》' },
|
||||||
|
{ text: '约会在吊死鬼开的乐园,看糖果和孤独成就了饕餮。', from: '《三重爱恋》' },
|
||||||
|
{ text: '我犯下了重重罪孽,我到底是什么存在。', from: '《四重罪孽》' },
|
||||||
|
{ text: '你停留在我无法追逐的记忆彼岸,永远不会再回来。', from: '《五重空洞》' },
|
||||||
|
{ text: '再见吧终会相见,莫为此放弃明天。', from: '《六重不忠》' },
|
||||||
|
{ text: '如果谎言重复了千遍依然无法成为现实,那谎言应该如何生存下去呢?', from: '《七重痼病》' },
|
||||||
|
{ text: '无意义乃这个世界存在的唯一意义。', from: '《八重回归》' },
|
||||||
|
{ text: '我所描述的一切或许并非真实,然而我的感受绝无虚假。', from: '《九重现实》' },
|
||||||
|
{ text: '比十年更早之前,我便歌颂着明天。', from: '《十重告别》' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 曲目列表:同系列其他作品 */
|
||||||
|
export const paranoiaSeries: { group: string; items: string[] }[] = [
|
||||||
|
{ group: '序曲', items: ['《NOVA》'] },
|
||||||
|
{
|
||||||
|
group: '正篇',
|
||||||
|
items: [
|
||||||
|
'《一重加害》',
|
||||||
|
'《二重变革》',
|
||||||
|
'《三重爱恋》',
|
||||||
|
'《四重罪孽》',
|
||||||
|
'《五重空洞》',
|
||||||
|
'《六重不忠》',
|
||||||
|
'《七重痼病》',
|
||||||
|
'《八重回归》',
|
||||||
|
'《九重现实》',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: '角色歌',
|
||||||
|
items: ['《三重恋爱》', '《泠重乞愿》', '《零重祈愿》'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: '外篇',
|
||||||
|
items: ['《自攻自受》', '《妄想 Paranoia》', '《妄想 Reality》', '《十重告别》'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 侧边栏章节谱:把应用的每个功能对应到系列的一重上。
|
||||||
|
* 坠落之章用 b(降号),上升之章用 #(升号)——这也是原作分章的方式。
|
||||||
|
*/
|
||||||
|
export interface ParanoiaChapterMark {
|
||||||
|
/** 章节记号 */
|
||||||
|
sigil: string
|
||||||
|
/** 展示用的重数 */
|
||||||
|
label: string
|
||||||
|
/** 曲名 */
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const paranoiaNavMarks: Record<string, ParanoiaChapterMark> = {
|
||||||
|
dashboard: { sigil: '✳', label: '序曲', title: 'NOVA' },
|
||||||
|
timers: { sigil: 'b', label: '一重', title: '一重加害' },
|
||||||
|
rules: { sigil: 'b', label: '二重', title: '二重变革' },
|
||||||
|
accounts: { sigil: 'b', label: '三重', title: '三重爱恋' },
|
||||||
|
gateways: { sigil: 'b', label: '四重', title: '四重罪孽' },
|
||||||
|
'gateway-rules': { sigil: 'b', label: '五重', title: '五重空洞' },
|
||||||
|
logs: { sigil: '#', label: '六重', title: '六重不忠' },
|
||||||
|
settings: { sigil: '#', label: '七重', title: '七重痼病' },
|
||||||
|
logout: { sigil: '#', label: '八重', title: '八重回归' },
|
||||||
|
paranoia: { sigil: '#', label: '九重', title: '九重现实' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const paranoiaCredits = {
|
||||||
|
title: '妄想症 Paranoia',
|
||||||
|
span: '2015.11.28 — 2017.03.17 · 共 475 天',
|
||||||
|
staff: [
|
||||||
|
{ role: '作编曲', name: 'DELA' },
|
||||||
|
{ role: '作词 / 策划', name: '雨狸' },
|
||||||
|
{ role: '曲绘', name: 'Lune、YAL、雨湘雪' },
|
||||||
|
{ role: 'PV', name: '一折起售、owen-z' },
|
||||||
|
{ role: '混音', name: 'POiSON' },
|
||||||
|
{ role: '调校', name: '花儿不哭、Digger、河谐、OQQ、流绪' },
|
||||||
|
],
|
||||||
|
notice:
|
||||||
|
'本主题为界面外观层面的同人致敬,非官方合作。「妄想症 Paranoia」系列曲目、曲绘、歌词与设定的著作权均归原作者所有;此处仅作介绍与引用,不作任何商业用途。',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chapterByNo(no: number): ParanoiaChapter | undefined {
|
||||||
|
return paranoiaChapters.find((item) => item.no === no)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按日期轮换一条引文,让「今日签文」每天都不一样 */
|
||||||
|
export function todaysQuote(date = new Date()) {
|
||||||
|
const start = new Date(date.getFullYear(), 0, 0)
|
||||||
|
const dayOfYear = Math.floor((date.getTime() - start.getTime()) / 86400000)
|
||||||
|
return paranoiaQuotes[dayOfYear % paranoiaQuotes.length]
|
||||||
|
}
|
||||||
@@ -15,10 +15,14 @@ import {
|
|||||||
Moon,
|
Moon,
|
||||||
Sunny,
|
Sunny,
|
||||||
Fold,
|
Fold,
|
||||||
|
Reading,
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
import LogoMark from '@/components/LogoMark.vue'
|
import LogoMark from '@/components/LogoMark.vue'
|
||||||
|
import ThemePicker from '@/components/ThemePicker.vue'
|
||||||
|
import ParanoiaBackdrop from '@/components/ParanoiaBackdrop.vue'
|
||||||
|
import { paranoiaNavMarks } from '@/data/paranoia'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -35,7 +39,7 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const menus = [
|
const baseMenus = [
|
||||||
{ path: '/dashboard', title: '仪表盘', icon: Odometer },
|
{ path: '/dashboard', title: '仪表盘', icon: Odometer },
|
||||||
{ path: '/timers', title: '定时器', icon: TimerIcon },
|
{ path: '/timers', title: '定时器', icon: TimerIcon },
|
||||||
{ path: '/rules', title: '规则', icon: Operation },
|
{ path: '/rules', title: '规则', icon: Operation },
|
||||||
@@ -49,7 +53,33 @@ const menus = [
|
|||||||
const activePath = computed(() => `/${route.path.split('/')[1] ?? ''}`)
|
const activePath = computed(() => `/${route.path.split('/')[1] ?? ''}`)
|
||||||
|
|
||||||
// 主题状态是全局单例,初始化在 main.ts 里完成
|
// 主题状态是全局单例,初始化在 main.ts 里完成
|
||||||
const { isDark, toggleTheme } = useTheme()
|
const { isDark, isParanoia, toggleTheme } = useTheme()
|
||||||
|
|
||||||
|
/** 妄想症皮肤下多出一个「妄想症」展柜,其余菜单不变 */
|
||||||
|
const menus = computed(() =>
|
||||||
|
isParanoia.value
|
||||||
|
? [...baseMenus, { path: '/paranoia', title: '妄想症', icon: Reading }]
|
||||||
|
: baseMenus,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 每个功能对应的章节记号,只在妄想症皮肤下显示 */
|
||||||
|
function markOf(path: string) {
|
||||||
|
return paranoiaNavMarks[path.slice(1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前所处章节,用于顶栏与侧边栏落款 */
|
||||||
|
const currentMark = computed(() => markOf(activePath.value) ?? paranoiaNavMarks.dashboard)
|
||||||
|
|
||||||
|
// 展柜属于皮肤的一部分:皮肤关掉(含直接输入地址进入)就退回仪表盘
|
||||||
|
watch(
|
||||||
|
isParanoia,
|
||||||
|
(on) => {
|
||||||
|
if (!on && route.path.startsWith('/paranoia')) {
|
||||||
|
router.push('/dashboard')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
await userStore.logout()
|
await userStore.logout()
|
||||||
@@ -59,7 +89,10 @@ async function handleLogout() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="layout">
|
<div class="layout" :class="{ 'skin-paranoia': isParanoia }">
|
||||||
|
<!-- 妄想症皮肤的背景层:噪点、暗角与飘落的石蒜花瓣 -->
|
||||||
|
<ParanoiaBackdrop v-if="isParanoia" />
|
||||||
|
|
||||||
<!-- 移动端抽屉遮罩 -->
|
<!-- 移动端抽屉遮罩 -->
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div v-if="drawerOpen" class="drawer-mask" @click="drawerOpen = false" />
|
<div v-if="drawerOpen" class="drawer-mask" @click="drawerOpen = false" />
|
||||||
@@ -71,7 +104,7 @@ async function handleLogout() {
|
|||||||
<div class="logo-mark"><LogoMark :size="22" /></div>
|
<div class="logo-mark"><LogoMark :size="22" /></div>
|
||||||
<div class="logo-name">
|
<div class="logo-name">
|
||||||
<span class="logo-text">goodBaby</span>
|
<span class="logo-text">goodBaby</span>
|
||||||
<span class="logo-sub">摇篮系统</span>
|
<span class="logo-sub">{{ isParanoia ? '妄想症 · PARANOIA' : '摇篮系统' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -84,12 +117,22 @@ async function handleLogout() {
|
|||||||
:class="{ active: activePath === item.path }"
|
:class="{ active: activePath === item.path }"
|
||||||
>
|
>
|
||||||
<el-icon :size="17"><component :is="item.icon" /></el-icon>
|
<el-icon :size="17"><component :is="item.icon" /></el-icon>
|
||||||
<span>{{ item.title }}</span>
|
<span class="nav-text">{{ item.title }}</span>
|
||||||
|
<span v-if="isParanoia" class="nav-mark mono">
|
||||||
|
{{ markOf(item.path)?.sigil }}{{ markOf(item.path)?.label }}
|
||||||
|
</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="aside-footer">
|
<div class="aside-footer">
|
||||||
<div class="footer-quote">goodBaby v2</div>
|
<template v-if="isParanoia">
|
||||||
|
<div class="footer-chapter">
|
||||||
|
<span class="footer-chapter__sigil mono">{{ currentMark?.sigil }} {{ currentMark?.label }}</span>
|
||||||
|
<span class="footer-chapter__title">{{ currentMark?.title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="footer-quote">九重妄想 · 幸存者 goodBaby v2</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="footer-quote">goodBaby v2</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -101,8 +144,12 @@ async function handleLogout() {
|
|||||||
<el-icon :size="18"><Fold /></el-icon>
|
<el-icon :size="18"><Fold /></el-icon>
|
||||||
</button>
|
</button>
|
||||||
<div class="header-title">{{ route.meta.title ?? '' }}</div>
|
<div class="header-title">{{ route.meta.title ?? '' }}</div>
|
||||||
|
<span v-if="isParanoia" class="pa-mark header-mark">
|
||||||
|
<em>{{ currentMark?.sigil }}</em>{{ currentMark?.title }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
|
<ThemePicker />
|
||||||
<button
|
<button
|
||||||
class="icon-btn theme-btn"
|
class="icon-btn theme-btn"
|
||||||
:title="isDark ? '切换到亮色' : '切换到暗色'"
|
:title="isDark ? '切换到亮色' : '切换到暗色'"
|
||||||
@@ -124,7 +171,8 @@ async function handleLogout() {
|
|||||||
<el-icon><Setting /></el-icon>设置
|
<el-icon><Setting /></el-icon>设置
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item divided @click="handleLogout">
|
<el-dropdown-item divided @click="handleLogout">
|
||||||
<el-icon><SwitchButton /></el-icon>退出登录
|
<el-icon><SwitchButton /></el-icon>
|
||||||
|
{{ isParanoia ? '八重回归 · 退出' : '退出登录' }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
@@ -399,8 +447,116 @@ async function handleLogout() {
|
|||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- 妄想症皮肤:侧边栏变成卷宗封皮 ---------- */
|
||||||
|
|
||||||
|
/* 背景层待在内容之下 */
|
||||||
|
.layout.skin-paranoia {
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.skin-paranoia > .content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.skin-paranoia .aside {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(224 36 94 / 0.18) 0%, transparent 36%),
|
||||||
|
linear-gradient(180deg, var(--gb-ink) 0%, var(--gb-ink-soft) 100%);
|
||||||
|
border-right: 1px solid rgb(224 36 94 / 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.skin-paranoia .logo-mark {
|
||||||
|
border-radius: 6px;
|
||||||
|
background: linear-gradient(135deg, #e0245e 0%, #7a0f2c 100%);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 4px 16px rgb(224 36 94 / 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.skin-paranoia .user-avatar {
|
||||||
|
background: linear-gradient(135deg, #e0245e 0%, #7a2ea0 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.skin-paranoia .logo-text {
|
||||||
|
font-family: var(--pa-serif);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.skin-paranoia .logo-sub {
|
||||||
|
color: rgb(242 86 138 / 0.75);
|
||||||
|
font-family: var(--pa-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.skin-paranoia .nav-item {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.skin-paranoia .nav-item:hover {
|
||||||
|
background: rgb(224 36 94 / 0.1);
|
||||||
|
border-color: rgb(224 36 94 / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.skin-paranoia .nav-item.active {
|
||||||
|
background: linear-gradient(135deg, #e0245e 0%, #8f1230 100%);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 4px 16px rgb(224 36 94 / 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 每一项右侧标出它对应的「重数」 */
|
||||||
|
.layout.skin-paranoia .nav-mark {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.skin-paranoia .nav-item.active .nav-mark {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.skin-paranoia .footer-chapter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
padding: 0 0 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid rgb(224 36 94 / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.skin-paranoia .footer-chapter__sigil {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
color: rgb(242 86 138 / 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.skin-paranoia .footer-chapter__title {
|
||||||
|
font-family: var(--pa-serif);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: rgb(255 255 255 / 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout.skin-paranoia .header-mark {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- 移动端 ---------- */
|
/* ---------- 移动端 ---------- */
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
/* 桌面端侧边栏常驻,压到背景层上面 */
|
||||||
|
.layout.skin-paranoia > .aside {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* 侧边栏脱离文档流,变成从左侧滑出的抽屉 */
|
/* 侧边栏脱离文档流,变成从左侧滑出的抽屉 */
|
||||||
.aside {
|
.aside {
|
||||||
@@ -451,5 +607,10 @@ async function handleLogout() {
|
|||||||
.main {
|
.main {
|
||||||
padding: 16px 14px 32px;
|
padding: 16px 14px 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 窄屏标题优先,章节记号召回抽屉里看 */
|
||||||
|
.layout.skin-paranoia .header-mark {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import App from './App.vue'
|
|||||||
import router from './router'
|
import router from './router'
|
||||||
import { initTheme } from './composables/useTheme'
|
import { initTheme } from './composables/useTheme'
|
||||||
import './assets/main.css'
|
import './assets/main.css'
|
||||||
|
// 皮肤样式放在最后,才能压过 Element Plus 与基础样式里的变量
|
||||||
|
import './assets/theme-paranoia.css'
|
||||||
|
|
||||||
// 挂载前先定好主题,避免登录页闪一下白底
|
// 挂载前先定好主题,避免登录页闪一下白底
|
||||||
initTheme()
|
initTheme()
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ const router = createRouter({
|
|||||||
component: () => import('@/views/SettingsView.vue'),
|
component: () => import('@/views/SettingsView.vue'),
|
||||||
meta: { title: '设置' },
|
meta: { title: '设置' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'paranoia',
|
||||||
|
name: 'paranoia',
|
||||||
|
component: () => import('@/views/ParanoiaView.vue'),
|
||||||
|
meta: { title: '妄想症' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: '/:pathMatch(.*)*', redirect: '/dashboard' },
|
{ path: '/:pathMatch(.*)*', redirect: '/dashboard' },
|
||||||
|
|||||||
@@ -17,9 +17,16 @@ import type { DashboardOverview } from '@/api/types'
|
|||||||
import { formatDateTime, formatDuration } from '@/utils/format'
|
import { formatDateTime, formatDuration } from '@/utils/format'
|
||||||
import { useIsMobile } from '@/composables/useBreakpoint'
|
import { useIsMobile } from '@/composables/useBreakpoint'
|
||||||
import CountUp from '@/components/CountUp.vue'
|
import CountUp from '@/components/CountUp.vue'
|
||||||
|
import { useTheme } from '@/composables/useTheme'
|
||||||
|
import { paranoiaNavMarks, todaysQuote } from '@/data/paranoia'
|
||||||
|
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
|
// 妄想症皮肤下,仪表盘顶上多一条「今日签文」
|
||||||
|
const { isParanoia } = useTheme()
|
||||||
|
const dailyQuote = todaysQuote()
|
||||||
|
const todayMark = paranoiaNavMarks.dashboard
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const overview = ref<DashboardOverview | null>(null)
|
const overview = ref<DashboardOverview | null>(null)
|
||||||
@@ -62,14 +69,22 @@ const ringTextColor = computed(() => {
|
|||||||
return 'var(--gb-primary-deep)'
|
return 'var(--gb-primary-deep)'
|
||||||
})
|
})
|
||||||
|
|
||||||
const stats = computed(() => [
|
/**
|
||||||
|
* 统计卡配色:默认皮肤沿用品牌蓝,妄想症皮肤换成三位主角的主题色
|
||||||
|
* (泠珞蓝 / 颜语青与黄 / 零羽石蒜红)。
|
||||||
|
*/
|
||||||
|
const stats = computed(() => {
|
||||||
|
const pa = isParanoia.value
|
||||||
|
const triggered = (overview.value?.triggered_count ?? 0) > 0
|
||||||
|
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
label: '定时器',
|
label: '定时器',
|
||||||
sub: `已启用 ${overview.value?.enabled_timers ?? 0} 个`,
|
sub: `已启用 ${overview.value?.enabled_timers ?? 0} 个`,
|
||||||
value: overview.value?.timer_count,
|
value: overview.value?.timer_count,
|
||||||
icon: TimerIcon,
|
icon: TimerIcon,
|
||||||
color: '#66ccff',
|
color: pa ? '#e0245e' : '#66ccff',
|
||||||
bg: 'rgb(102 204 255 / 0.14)',
|
bg: pa ? 'rgb(224 36 94 / 0.16)' : 'rgb(102 204 255 / 0.14)',
|
||||||
to: '/timers',
|
to: '/timers',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -77,8 +92,8 @@ const stats = computed(() => [
|
|||||||
sub: `定时器规则 ${overview.value?.rule_count ?? 0} 条`,
|
sub: `定时器规则 ${overview.value?.rule_count ?? 0} 条`,
|
||||||
value: overview.value?.rule_count,
|
value: overview.value?.rule_count,
|
||||||
icon: Operation,
|
icon: Operation,
|
||||||
color: '#6366f1',
|
color: pa ? '#b98cff' : '#6366f1',
|
||||||
bg: 'rgb(99 102 241 / 0.12)',
|
bg: pa ? 'rgb(185 140 255 / 0.14)' : 'rgb(99 102 241 / 0.12)',
|
||||||
to: '/rules',
|
to: '/rules',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -86,8 +101,8 @@ const stats = computed(() => [
|
|||||||
sub: `网关规则 ${overview.value?.gateway_rule_count ?? 0} 条`,
|
sub: `网关规则 ${overview.value?.gateway_rule_count ?? 0} 条`,
|
||||||
value: overview.value?.gateway_count,
|
value: overview.value?.gateway_count,
|
||||||
icon: Connection,
|
icon: Connection,
|
||||||
color: '#0ea5e9',
|
color: pa ? '#7ee8e0' : '#0ea5e9',
|
||||||
bg: 'rgb(14 165 233 / 0.12)',
|
bg: pa ? 'rgb(126 232 224 / 0.14)' : 'rgb(14 165 233 / 0.12)',
|
||||||
to: '/gateways',
|
to: '/gateways',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -95,8 +110,8 @@ const stats = computed(() => [
|
|||||||
sub: '第三方凭据',
|
sub: '第三方凭据',
|
||||||
value: overview.value?.account_count,
|
value: overview.value?.account_count,
|
||||||
icon: User,
|
icon: User,
|
||||||
color: '#f59e0b',
|
color: pa ? '#f2d16b' : '#f59e0b',
|
||||||
bg: 'rgb(245 158 11 / 0.12)',
|
bg: pa ? 'rgb(242 209 107 / 0.14)' : 'rgb(245 158 11 / 0.12)',
|
||||||
to: '/accounts',
|
to: '/accounts',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -104,14 +119,16 @@ const stats = computed(() => [
|
|||||||
sub: '规则已执行',
|
sub: '规则已执行',
|
||||||
value: overview.value?.triggered_count,
|
value: overview.value?.triggered_count,
|
||||||
icon: Warning,
|
icon: Warning,
|
||||||
color: (overview.value?.triggered_count ?? 0) > 0 ? '#ef4444' : '#94a3b8',
|
color: triggered ? '#ef4444' : pa ? '#8a7880' : '#94a3b8',
|
||||||
bg:
|
bg: triggered
|
||||||
(overview.value?.triggered_count ?? 0) > 0
|
|
||||||
? 'rgb(239 68 68 / 0.12)'
|
? 'rgb(239 68 68 / 0.12)'
|
||||||
|
: pa
|
||||||
|
? 'rgb(138 120 128 / 0.14)'
|
||||||
: 'rgb(148 163 184 / 0.12)',
|
: 'rgb(148 163 184 / 0.12)',
|
||||||
to: '/logs',
|
to: '/logs',
|
||||||
},
|
},
|
||||||
])
|
]
|
||||||
|
})
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -165,6 +182,21 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 妄想症皮肤:今日签文 -->
|
||||||
|
<div v-if="isParanoia" class="pa-daily gb-rise">
|
||||||
|
<div class="pa-daily__mark">
|
||||||
|
<span class="pa-daily__sigil mono">{{ todayMark.sigil }}</span>
|
||||||
|
<span class="pa-daily__label">{{ todayMark.label }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="pa-daily__body">
|
||||||
|
<p class="pa-daily__text">{{ dailyQuote.text }}</p>
|
||||||
|
<span class="pa-daily__from mono">{{ dailyQuote.from }}</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="pa-daily__more" @click="router.push('/paranoia')">
|
||||||
|
九重档案 →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 已触发警示 -->
|
<!-- 已触发警示 -->
|
||||||
<el-alert
|
<el-alert
|
||||||
v-if="overview && overview.triggered_count > 0"
|
v-if="overview && overview.triggered_count > 0"
|
||||||
@@ -462,4 +494,103 @@ onUnmounted(() => {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* ---------- 妄想症皮肤:今日签文 ---------- */
|
||||||
|
|
||||||
|
.pa-daily {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid var(--pa-line);
|
||||||
|
border-left: 3px solid var(--gb-primary);
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--gb-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-daily__mark {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
flex: none;
|
||||||
|
padding-right: 16px;
|
||||||
|
border-right: 1px solid var(--pa-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-daily__sigil {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--gb-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-daily__label {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-daily__body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-daily__text {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-family: var(--pa-serif);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-daily__from {
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-daily__more {
|
||||||
|
flex: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-family: var(--pa-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--gb-primary);
|
||||||
|
border: 1px solid var(--pa-line);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 0.2s var(--gb-ease),
|
||||||
|
background 0.2s var(--gb-ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-daily__more:hover {
|
||||||
|
border-color: var(--pa-line-strong);
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.pa-daily {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-daily__mark {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 6px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-daily__text {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pa-daily__more {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { siteApi, userApi } from '@/api'
|
import { siteApi, userApi } from '@/api'
|
||||||
@@ -7,11 +7,44 @@ import { ApiError } from '@/api/client'
|
|||||||
import type { SiteInfo } from '@/api/types'
|
import type { SiteInfo } from '@/api/types'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import LogoMark from '@/components/LogoMark.vue'
|
import LogoMark from '@/components/LogoMark.vue'
|
||||||
|
import ThemePicker from '@/components/ThemePicker.vue'
|
||||||
|
import ParanoiaBackdrop from '@/components/ParanoiaBackdrop.vue'
|
||||||
|
import { useTheme } from '@/composables/useTheme'
|
||||||
|
import { paranoiaCharacters } from '@/data/paranoia'
|
||||||
|
import loginArt from '@/assets/paranoia/ch09.jpg'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 登录页也能挑皮肤,这样不登录就能先看新主题
|
||||||
|
const { isParanoia } = useTheme()
|
||||||
|
|
||||||
|
/** 左侧理念区的文案:跟着皮肤换一副口吻 */
|
||||||
|
const copy = computed(() =>
|
||||||
|
isParanoia.value
|
||||||
|
? {
|
||||||
|
title: '妄想症',
|
||||||
|
slogan: 'Paranoia · 九重档案',
|
||||||
|
desc: '被害者泠珞、加害者与守护者颜语、背叛者零羽,\n九重正篇与四百七十五天的一场层层叠叠的戏剧。',
|
||||||
|
points: paranoiaCharacters
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((item) => `${item.name}【${item.vocal}】· ${item.role}`),
|
||||||
|
quote: '谎言重复一千次就变成真理。所以,现实也是这样诞生的。',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
title: 'goodBaby',
|
||||||
|
slogan: "摇篮系统 · Dead Man's Switch",
|
||||||
|
desc: '设定签到周期并定期签到;\n超时未签到时,系统会自动执行你预设的规则。',
|
||||||
|
points: [
|
||||||
|
'到期前通过钉钉机器人提醒签到',
|
||||||
|
'支持发送邮件、QQ、钉钉消息、B 站动态',
|
||||||
|
'支持自动公开 GitHub 仓库',
|
||||||
|
],
|
||||||
|
quote: '',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const site = ref<SiteInfo | null>(null)
|
const site = ref<SiteInfo | null>(null)
|
||||||
const mode = ref<'login' | 'register'>('login')
|
const mode = ref<'login' | 'register'>('login')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -64,21 +97,27 @@ async function submit() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="login-page">
|
<div
|
||||||
|
class="login-page"
|
||||||
|
:class="{ 'skin-paranoia': isParanoia }"
|
||||||
|
:style="isParanoia ? { '--pa-login-art': `url(${loginArt})` } : undefined"
|
||||||
|
>
|
||||||
|
<!-- 外观主题:登录前就能先看一眼新皮肤 -->
|
||||||
|
<div class="skin-corner"><ThemePicker /></div>
|
||||||
|
|
||||||
<!-- 左侧:项目介绍 -->
|
<!-- 左侧:项目介绍 -->
|
||||||
<div class="intro">
|
<div class="intro">
|
||||||
|
<!-- 妄想症皮肤下,花瓣落在曲绘之上 -->
|
||||||
|
<ParanoiaBackdrop v-if="isParanoia" />
|
||||||
|
|
||||||
<div class="intro-inner gb-rise">
|
<div class="intro-inner gb-rise">
|
||||||
<div class="intro-mark"><LogoMark :size="28" /></div>
|
<div class="intro-mark"><LogoMark :size="28" /></div>
|
||||||
<h1 class="intro-title">goodBaby</h1>
|
<h1 class="intro-title">{{ copy.title }}</h1>
|
||||||
<p class="intro-slogan">摇篮系统 · Dead Man's Switch</p>
|
<p class="intro-slogan">{{ copy.slogan }}</p>
|
||||||
<p class="intro-desc">
|
<p class="intro-desc">{{ copy.desc }}</p>
|
||||||
设定签到周期并定期签到;<br />
|
<p v-if="copy.quote" class="intro-quote">{{ copy.quote }}</p>
|
||||||
超时未签到时,系统会自动执行你预设的规则。
|
|
||||||
</p>
|
|
||||||
<ul class="intro-points">
|
<ul class="intro-points">
|
||||||
<li>到期前通过钉钉机器人提醒签到</li>
|
<li v-for="point in copy.points" :key="point">{{ point }}</li>
|
||||||
<li>支持发送邮件、QQ、钉钉消息、B 站动态</li>
|
|
||||||
<li>支持自动公开 GitHub 仓库</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,12 +176,24 @@ async function submit() {
|
|||||||
.login-page {
|
.login-page {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 皮肤开关固定在右上角,两种皮肤下都在同一位置 */
|
||||||
|
.skin-corner {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 左侧理念区 ---------- */
|
/* ---------- 左侧理念区 ---------- */
|
||||||
|
|
||||||
.intro {
|
.intro {
|
||||||
flex: 1.15;
|
flex: 1.15;
|
||||||
|
position: relative;
|
||||||
|
/* 隔离出图层,让飘落的花瓣落在曲绘之上、文案之下 */
|
||||||
|
isolation: isolate;
|
||||||
background:
|
background:
|
||||||
radial-gradient(ellipse at 20% 20%, rgb(102 204 255 / 0.16) 0%, transparent 55%),
|
radial-gradient(ellipse at 20% 20%, rgb(102 204 255 / 0.16) 0%, transparent 55%),
|
||||||
radial-gradient(ellipse at 85% 80%, rgb(102 204 255 / 0.12) 0%, transparent 50%),
|
radial-gradient(ellipse at 85% 80%, rgb(102 204 255 / 0.12) 0%, transparent 50%),
|
||||||
@@ -154,6 +205,8 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.intro-inner {
|
.intro-inner {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +243,17 @@ async function submit() {
|
|||||||
color: rgb(255 255 255 / 0.66);
|
color: rgb(255 255 255 / 0.66);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 2;
|
line-height: 2;
|
||||||
|
/* 文案里用 \n 换行,避免模板里散落 <br /> */
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-quote {
|
||||||
|
margin: 0 0 24px;
|
||||||
|
padding-left: 14px;
|
||||||
|
border-left: 2px solid var(--gb-primary);
|
||||||
|
color: rgb(255 255 255 / 0.78);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.95;
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro-points {
|
.intro-points {
|
||||||
@@ -261,6 +325,73 @@ async function submit() {
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- 妄想症皮肤:登录页换成一张曲绘 ---------- */
|
||||||
|
|
||||||
|
.login-page.skin-paranoia .intro {
|
||||||
|
/* 曲绘垫在最底下,压一层暗色的膜,让文字依然读得清 */
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgb(9 6 12 / 0.82) 0%,
|
||||||
|
rgb(9 6 12 / 0.9) 55%,
|
||||||
|
rgb(22 7 14 / 0.97) 100%
|
||||||
|
),
|
||||||
|
radial-gradient(ellipse at 22% 18%, rgb(224 36 94 / 0.4), transparent 58%),
|
||||||
|
var(--pa-login-art) center / cover no-repeat,
|
||||||
|
linear-gradient(160deg, #14060c 0%, #08070c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page.skin-paranoia .intro-mark {
|
||||||
|
border-radius: 6px;
|
||||||
|
background: linear-gradient(135deg, #e0245e 0%, #7a0f2c 100%);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 28px rgb(224 36 94 / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page.skin-paranoia .intro-title {
|
||||||
|
font-family: var(--pa-serif);
|
||||||
|
font-size: 38px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page.skin-paranoia .intro-slogan {
|
||||||
|
font-family: var(--pa-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.28em;
|
||||||
|
color: #f2568a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page.skin-paranoia .intro-desc {
|
||||||
|
color: rgb(255 255 255 / 0.7);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page.skin-paranoia .intro-quote {
|
||||||
|
font-family: var(--pa-serif);
|
||||||
|
color: rgb(255 255 255 / 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page.skin-paranoia .intro-points li {
|
||||||
|
color: rgb(255 255 255 / 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表点改成小小的菱形,像彼岸花的花瓣 */
|
||||||
|
.login-page.skin-paranoia .intro-points li::before {
|
||||||
|
border-radius: 1px;
|
||||||
|
background: #e0245e;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page.skin-paranoia .panel {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page.skin-paranoia .panel-title {
|
||||||
|
font-family: var(--pa-serif);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
/* 窄屏时隐藏左侧介绍,登录框居中占满 */
|
/* 窄屏时隐藏左侧介绍,登录框居中占满 */
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
.intro {
|
.intro {
|
||||||
|
|||||||