diff --git a/hunliji-api/configsection/section.go b/hunliji-api/configsection/section.go index 7c3406a..a02429a 100644 --- a/hunliji-api/configsection/section.go +++ b/hunliji-api/configsection/section.go @@ -29,9 +29,13 @@ var configSectionKeys = map[string][]string{ "copy": { "formalText1", "formalText2", "formalPageHeight", }, + "theme": { + "siteTheme", "sectionIndexEnabled", "calendarStyle", "endingFrameStyle", + }, "visual": { "introExitEffect", "scrollMode", "freeScrollInterval", "pageSwitchDuration", "firstScreenDuration", "petalEnabled", "bubbleEnabled", "introEnabled", "danmakuEnabled", "danmakuShowTime", "nineGridAnimate", + "giftEffectMode", }, "music": { "musicList", "activeMusicUrl", "musicPlayMode", "musicRandomEnabled", diff --git a/vite-tailwindcss/src/components/CanvasEffects.vue b/vite-tailwindcss/src/components/CanvasEffects.vue index 7a08c9f..465e123 100644 --- a/vite-tailwindcss/src/components/CanvasEffects.vue +++ b/vite-tailwindcss/src/components/CanvasEffects.vue @@ -10,13 +10,18 @@ const props = defineProps({ bubbleEnabled: { type: Boolean, default: true }, /** 预览/抽屉等遮罩时关闭粒子绘制 */ active: { type: Boolean, default: true }, + /** 主题 fx 色板注入:空则用默认暖彩 */ + petalPalette: { type: Array, default: null }, + heartPalette: { type: Array, default: null }, }) const canvasRef = ref(null) const petals = [] const hearts = [] -const petalColors = ['#f7dce3', '#fbeee3', '#f3c9d6', '#efdcc8', '#f9e7d6'] -const heartColors = ['#c9b08a', '#e2b3c0', '#d4af37', '#e8c9d2'] +const DEFAULT_PETALS = ['#f7dce3', '#fbeee3', '#f3c9d6', '#efdcc8', '#f9e7d6'] +const DEFAULT_HEARTS = ['#c9b08a', '#e2b3c0', '#d4af37', '#e8c9d2'] +const petalColorsOf = () => (props.petalPalette?.length ? props.petalPalette : DEFAULT_PETALS) +const heartColorsOf = () => (props.heartPalette?.length ? props.heartPalette : DEFAULT_HEARTS) let ctx = null let rafId = 0 @@ -39,7 +44,8 @@ function resetPetal(p, initial = false) { p.rotation = rand(0, Math.PI * 2) p.rotationSpeed = rand(-0.9, 0.9) p.opacity = rand(0.28, 0.7) - p.color = petalColors[Math.floor(rand(0, petalColors.length))] + const pc = petalColorsOf() + p.color = pc[Math.floor(rand(0, pc.length))] } function resetHeart(h, initial = false) { @@ -50,7 +56,8 @@ function resetHeart(h, initial = false) { h.sway = rand(8, 26) h.phase = rand(0, Math.PI * 2) h.opacity = rand(0.1, 0.32) - h.color = heartColors[Math.floor(rand(0, heartColors.length))] + const hc = heartColorsOf() + h.color = hc[Math.floor(rand(0, hc.length))] } function seedParticles() { diff --git a/vite-tailwindcss/src/components/LikeBurst.vue b/vite-tailwindcss/src/components/LikeBurst.vue index 9abdb77..83afa34 100644 --- a/vite-tailwindcss/src/components/LikeBurst.vue +++ b/vite-tailwindcss/src/components/LikeBurst.vue @@ -30,7 +30,7 @@ const GLOW_BLUR = isIOS ? 4 : 10 const GLOW_ALPHA = 0.55 const DPR_CAP = isIOS ? 1.5 : 2 -/** 多色爱心:主色 */ +/** 多色爱心:主色(可被主题 fx 色板覆盖) */ const HEART_COLORS = [ '#ff2442', '#ff2d55', '#ff4770', '#ff6b8a', '#f472b6', '#ec4899', '#e879f9', '#c084fc', @@ -47,6 +47,14 @@ const GLOW_COLORS = [ '#fdba74', '#f5d0b0', '#ffffff', ] +const props = defineProps({ + /** 主题色板注入:空则用默认 */ + colors: { type: Array, default: null }, + glowColors: { type: Array, default: null }, +}) +const heartPalette = () => (props.colors?.length ? props.colors : HEART_COLORS) +const glowPalette = () => (props.glowColors?.length ? props.glowColors : GLOW_COLORS) + const pick = (arr) => arr[Math.floor(Math.random() * arr.length)] const rand = (min, max) => min + Math.random() * (max - min) @@ -199,8 +207,8 @@ function createHeart(x, y, now) { birthTime: now, maxAge: rand(HEART_MIN_LIFE, HEART_MAX_LIFE), baseSize: rand(HEART_MIN_SIZE, HEART_MAX_SIZE), - color: pick(HEART_COLORS), - glowColor: pick(GLOW_COLORS), + color: pick(heartPalette()), + glowColor: pick(glowPalette()), peakScale: rand(0.75, 1.35), riseSpeed: rand(RISE_SPEED_MIN, RISE_SPEED_MAX), driftAmp: rand(DRIFT_AMP_MIN, DRIFT_AMP_MAX), diff --git a/vite-tailwindcss/src/components/PhotoGallery.vue b/vite-tailwindcss/src/components/PhotoGallery.vue index b0d5e42..5340fff 100644 --- a/vite-tailwindcss/src/components/PhotoGallery.vue +++ b/vite-tailwindcss/src/components/PhotoGallery.vue @@ -214,8 +214,8 @@ :class="{ 'g-heart-done': heartDone }" >
heartSlotsFor(props.page)) + function heartReveal(i) { - if (i === 5 || i === 12) return 'scale' - if (i === 0 || i === 2 || i === 3 || i === 4 || i === 9) return 'left' - if (i === 1 || i === 6 || i === 7 || i === 8 || i === 11) return 'right' - return 'up' + // 按槽位横向位置决定入场方向:左半从左入、右半从右入、中轴缩放 + const slot = heartSlots.value[i] + if (!slot) return 'up' + const cx = slot.l + slot.w / 2 + if (cx > 42 && cx < 58) return 'scale' + return cx <= 42 ? 'left' : 'right' } /** 爱心拼合完成:触发一次心跳 + 小心心飘散 */ const heartDone = computed(() => - props.page?.type === 'heart-collage' && shownCount.value >= HEART_SLOTS.length + props.page?.type === 'heart-collage' && shownCount.value >= heartSlots.value.length ) diff --git a/vite-tailwindcss/src/components/themes/SectionHeading.vue b/vite-tailwindcss/src/components/themes/SectionHeading.vue index 7bbb0d8..d24310a 100644 --- a/vite-tailwindcss/src/components/themes/SectionHeading.vue +++ b/vite-tailwindcss/src/components/themes/SectionHeading.vue @@ -201,6 +201,156 @@ html[data-theme='pixel'] .sh-note { font-size: 10px; } +/* —— 水墨:楷体 + 墨横 + 朱砂点 —— */ +html[data-theme='inkwash'] .sh-kicker { + font-family: var(--font-kai); + letter-spacing: 0.4em; +} +html[data-theme='inkwash'] .sh-title { + font-family: var(--font-kai); + letter-spacing: 0.36em; + text-indent: 0.36em; +} +html[data-theme='inkwash'] .sh-rule-bottom { + display: block; + width: 46px; + height: 3px; + background: linear-gradient(90deg, transparent, var(--c-ink) 30%, var(--c-ink) 70%, transparent); + border-radius: 2px; +} + +/* —— 电影:场记板 SCENE —— */ +html[data-theme='cinema'] .sh-index { + display: block; + font-family: var(--font-number); + font-style: normal; + font-size: 13px; + letter-spacing: 0.2em; + color: var(--c-on-primary); + background: var(--c-primary); + padding: 3px 10px; + border-radius: 3px; + transform: rotate(-2deg); +} +html[data-theme='cinema'] .sh-index::before { + content: 'SCENE '; +} +html[data-theme='cinema'] .sh-title { + font-family: var(--font-serif); + font-weight: 700; + letter-spacing: 0.2em; +} +html[data-theme='cinema'] .sh-kicker { + font-family: var(--font-cinzel); +} + +/* —— 胶片:帧号标记 —— */ +html[data-theme='kodak'] .sh-index { + display: block; + font-family: var(--font-number); + font-style: normal; + font-size: 12px; + letter-spacing: 0.16em; + color: #ff9a3c; + text-shadow: 0 0 6px rgba(255, 130, 30, 0.55); +} +html[data-theme='kodak'] .sh-index::before { + content: '▸ '; +} +html[data-theme='kodak'] .sh-index::after { + content: 'A'; +} +html[data-theme='kodak'] .sh-title { + font-weight: 700; + letter-spacing: 0.14em; +} + +/* —— 文艺:罗马分幕 —— */ +html[data-theme='arthouse'] .sh { + gap: 10px; + margin-bottom: 30px; +} +html[data-theme='arthouse'] .sh-kicker { + font-family: var(--font-display); + letter-spacing: 0.56em; +} +html[data-theme='arthouse'] .sh-title { + font-family: var(--font-display); + font-weight: 400; + letter-spacing: 0.3em; + text-indent: 0.3em; +} +html[data-theme='arthouse'] .sh-rule-bottom { + display: block; + width: 1px; + height: 22px; + background: rgba(var(--c-line-rgb), 0.6); +} + +/* —— 旅行:邮戳圆章 —— */ +html[data-theme='passport'] .sh-index { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + font-family: var(--font-number); + font-style: normal; + font-size: 13px; + color: var(--c-primary); + border: 2px solid rgba(var(--c-primary-rgb), 0.55); + border-radius: 50%; + transform: rotate(-10deg); +} +html[data-theme='passport'] .sh-kicker { + font-family: var(--font-number); + letter-spacing: 0.34em; +} +html[data-theme='passport'] .sh-title { + letter-spacing: 0.22em; + font-weight: 600; +} + +/* —— 手账:标签贴 + 胶带 —— */ +html[data-theme='scrapbook'] .sh-title { + font-family: var(--font-longcang); + padding: 6px 20px; + background: var(--c-card); + border: 1px dashed rgba(var(--c-primary-rgb), 0.45); + border-radius: 6px; + transform: rotate(-1.4deg); + box-shadow: 0 6px 16px rgba(69, 58, 48, 0.1); +} +html[data-theme='scrapbook'] .sh-kicker { + font-family: var(--font-liujian); + text-transform: none; + letter-spacing: 0.18em; +} + +/* —— Y2K:窗口标题条 —— */ +html[data-theme='y2k'] .sh-title { + font-weight: 800; + letter-spacing: 0.1em; + background: linear-gradient(180deg, #7a4ae0 10%, #4a90e0 50%, #e04ad0 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +html[data-theme='y2k'] .sh-kicker { + font-family: var(--font-pixel); + font-size: 10px; + letter-spacing: 0.2em; + color: var(--c-line); + text-transform: none; +} +html[data-theme='y2k'] .sh-rule-bottom { + display: block; + width: 60px; + height: 4px; + border-radius: 2px; + background: repeating-linear-gradient(90deg, #7a4ae0 0 8px, #4adce0 8px 16px); +} + /* —— 极简主义:小字 + 一条细线 + 大留白 —— */ html[data-theme='zen'] .sh { gap: 12px; diff --git a/vite-tailwindcss/src/components/themes/covers/CoverArthouse.vue b/vite-tailwindcss/src/components/themes/covers/CoverArthouse.vue new file mode 100644 index 0000000..b3a36bb --- /dev/null +++ b/vite-tailwindcss/src/components/themes/covers/CoverArthouse.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/vite-tailwindcss/src/components/themes/covers/CoverCinema.vue b/vite-tailwindcss/src/components/themes/covers/CoverCinema.vue new file mode 100644 index 0000000..8cca807 --- /dev/null +++ b/vite-tailwindcss/src/components/themes/covers/CoverCinema.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/vite-tailwindcss/src/components/themes/covers/CoverClassic.vue b/vite-tailwindcss/src/components/themes/covers/CoverClassic.vue index d65d739..d068432 100644 --- a/vite-tailwindcss/src/components/themes/covers/CoverClassic.vue +++ b/vite-tailwindcss/src/components/themes/covers/CoverClassic.vue @@ -1,5 +1,5 @@ + + + + + + + + + + + + + +